github.com/matthieu/go-ethereum@v1.13.2/console/console.go (about)

     1  // Copyright 2016 The go-ethereum Authors
     2  // This file is part of the go-ethereum library.
     3  //
     4  // The go-ethereum library is free software: you can redistribute it and/or modify
     5  // it under the terms of the GNU Lesser General Public License as published by
     6  // the Free Software Foundation, either version 3 of the License, or
     7  // (at your option) any later version.
     8  //
     9  // The go-ethereum library is distributed in the hope that it will be useful,
    10  // but WITHOUT ANY WARRANTY; without even the implied warranty of
    11  // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
    12  // GNU Lesser General Public License for more details.
    13  //
    14  // You should have received a copy of the GNU Lesser General Public License
    15  // along with the go-ethereum library. If not, see <http://www.gnu.org/licenses/>.
    16  
    17  package console
    18  
    19  import (
    20  	"fmt"
    21  	"io"
    22  	"io/ioutil"
    23  	"os"
    24  	"os/signal"
    25  	"path/filepath"
    26  	"regexp"
    27  	"sort"
    28  	"strings"
    29  	"syscall"
    30  
    31  	"github.com/dop251/goja"
    32  	"github.com/matthieu/go-ethereum/console/prompt"
    33  	"github.com/matthieu/go-ethereum/internal/jsre"
    34  	"github.com/matthieu/go-ethereum/internal/jsre/deps"
    35  	"github.com/matthieu/go-ethereum/internal/web3ext"
    36  	"github.com/matthieu/go-ethereum/rpc"
    37  	"github.com/mattn/go-colorable"
    38  	"github.com/peterh/liner"
    39  )
    40  
    41  var (
    42  	// u: unlock, s: signXX, sendXX, n: newAccount, i: importXX
    43  	passwordRegexp = regexp.MustCompile(`personal.[nusi]`)
    44  	onlyWhitespace = regexp.MustCompile(`^\s*$`)
    45  	exit           = regexp.MustCompile(`^\s*exit\s*;*\s*$`)
    46  )
    47  
    48  // HistoryFile is the file within the data directory to store input scrollback.
    49  const HistoryFile = "history"
    50  
    51  // DefaultPrompt is the default prompt line prefix to use for user input querying.
    52  const DefaultPrompt = "> "
    53  
    54  // Config is the collection of configurations to fine tune the behavior of the
    55  // JavaScript console.
    56  type Config struct {
    57  	DataDir  string              // Data directory to store the console history at
    58  	DocRoot  string              // Filesystem path from where to load JavaScript files from
    59  	Client   *rpc.Client         // RPC client to execute Ethereum requests through
    60  	Prompt   string              // Input prompt prefix string (defaults to DefaultPrompt)
    61  	Prompter prompt.UserPrompter // Input prompter to allow interactive user feedback (defaults to TerminalPrompter)
    62  	Printer  io.Writer           // Output writer to serialize any display strings to (defaults to os.Stdout)
    63  	Preload  []string            // Absolute paths to JavaScript files to preload
    64  }
    65  
    66  // Console is a JavaScript interpreted runtime environment. It is a fully fledged
    67  // JavaScript console attached to a running node via an external or in-process RPC
    68  // client.
    69  type Console struct {
    70  	client   *rpc.Client         // RPC client to execute Ethereum requests through
    71  	jsre     *jsre.JSRE          // JavaScript runtime environment running the interpreter
    72  	prompt   string              // Input prompt prefix string
    73  	prompter prompt.UserPrompter // Input prompter to allow interactive user feedback
    74  	histPath string              // Absolute path to the console scrollback history
    75  	history  []string            // Scroll history maintained by the console
    76  	printer  io.Writer           // Output writer to serialize any display strings to
    77  }
    78  
    79  // New initializes a JavaScript interpreted runtime environment and sets defaults
    80  // with the config struct.
    81  func New(config Config) (*Console, error) {
    82  	// Handle unset config values gracefully
    83  	if config.Prompter == nil {
    84  		config.Prompter = prompt.Stdin
    85  	}
    86  	if config.Prompt == "" {
    87  		config.Prompt = DefaultPrompt
    88  	}
    89  	if config.Printer == nil {
    90  		config.Printer = colorable.NewColorableStdout()
    91  	}
    92  
    93  	// Initialize the console and return
    94  	console := &Console{
    95  		client:   config.Client,
    96  		jsre:     jsre.New(config.DocRoot, config.Printer),
    97  		prompt:   config.Prompt,
    98  		prompter: config.Prompter,
    99  		printer:  config.Printer,
   100  		histPath: filepath.Join(config.DataDir, HistoryFile),
   101  	}
   102  	if err := os.MkdirAll(config.DataDir, 0700); err != nil {
   103  		return nil, err
   104  	}
   105  	if err := console.init(config.Preload); err != nil {
   106  		return nil, err
   107  	}
   108  	return console, nil
   109  }
   110  
   111  // init retrieves the available APIs from the remote RPC provider and initializes
   112  // the console's JavaScript namespaces based on the exposed modules.
   113  func (c *Console) init(preload []string) error {
   114  	c.initConsoleObject()
   115  
   116  	// Initialize the JavaScript <-> Go RPC bridge.
   117  	bridge := newBridge(c.client, c.prompter, c.printer)
   118  	if err := c.initWeb3(bridge); err != nil {
   119  		return err
   120  	}
   121  	if err := c.initExtensions(); err != nil {
   122  		return err
   123  	}
   124  
   125  	// Add bridge overrides for web3.js functionality.
   126  	c.jsre.Do(func(vm *goja.Runtime) {
   127  		c.initAdmin(vm, bridge)
   128  		c.initPersonal(vm, bridge)
   129  	})
   130  
   131  	// Preload JavaScript files.
   132  	for _, path := range preload {
   133  		if err := c.jsre.Exec(path); err != nil {
   134  			failure := err.Error()
   135  			if gojaErr, ok := err.(*goja.Exception); ok {
   136  				failure = gojaErr.String()
   137  			}
   138  			return fmt.Errorf("%s: %v", path, failure)
   139  		}
   140  	}
   141  
   142  	// Configure the input prompter for history and tab completion.
   143  	if c.prompter != nil {
   144  		if content, err := ioutil.ReadFile(c.histPath); err != nil {
   145  			c.prompter.SetHistory(nil)
   146  		} else {
   147  			c.history = strings.Split(string(content), "\n")
   148  			c.prompter.SetHistory(c.history)
   149  		}
   150  		c.prompter.SetWordCompleter(c.AutoCompleteInput)
   151  	}
   152  	return nil
   153  }
   154  
   155  func (c *Console) initConsoleObject() {
   156  	c.jsre.Do(func(vm *goja.Runtime) {
   157  		console := vm.NewObject()
   158  		console.Set("log", c.consoleOutput)
   159  		console.Set("error", c.consoleOutput)
   160  		vm.Set("console", console)
   161  	})
   162  }
   163  
   164  func (c *Console) initWeb3(bridge *bridge) error {
   165  	bnJS := string(deps.MustAsset("bignumber.js"))
   166  	web3JS := string(deps.MustAsset("web3.js"))
   167  	if err := c.jsre.Compile("bignumber.js", bnJS); err != nil {
   168  		return fmt.Errorf("bignumber.js: %v", err)
   169  	}
   170  	if err := c.jsre.Compile("web3.js", web3JS); err != nil {
   171  		return fmt.Errorf("web3.js: %v", err)
   172  	}
   173  	if _, err := c.jsre.Run("var Web3 = require('web3');"); err != nil {
   174  		return fmt.Errorf("web3 require: %v", err)
   175  	}
   176  	var err error
   177  	c.jsre.Do(func(vm *goja.Runtime) {
   178  		transport := vm.NewObject()
   179  		transport.Set("send", jsre.MakeCallback(vm, bridge.Send))
   180  		transport.Set("sendAsync", jsre.MakeCallback(vm, bridge.Send))
   181  		vm.Set("_consoleWeb3Transport", transport)
   182  		_, err = vm.RunString("var web3 = new Web3(_consoleWeb3Transport)")
   183  	})
   184  	return err
   185  }
   186  
   187  // initExtensions loads and registers web3.js extensions.
   188  func (c *Console) initExtensions() error {
   189  	// Compute aliases from server-provided modules.
   190  	apis, err := c.client.SupportedModules()
   191  	if err != nil {
   192  		return fmt.Errorf("api modules: %v", err)
   193  	}
   194  	aliases := map[string]struct{}{"eth": {}, "personal": {}}
   195  	for api := range apis {
   196  		if api == "web3" {
   197  			continue
   198  		}
   199  		aliases[api] = struct{}{}
   200  		if file, ok := web3ext.Modules[api]; ok {
   201  			if err = c.jsre.Compile(api+".js", file); err != nil {
   202  				return fmt.Errorf("%s.js: %v", api, err)
   203  			}
   204  		}
   205  	}
   206  
   207  	// Apply aliases.
   208  	c.jsre.Do(func(vm *goja.Runtime) {
   209  		web3 := getObject(vm, "web3")
   210  		for name := range aliases {
   211  			if v := web3.Get(name); v != nil {
   212  				vm.Set(name, v)
   213  			}
   214  		}
   215  	})
   216  	return nil
   217  }
   218  
   219  // initAdmin creates additional admin APIs implemented by the bridge.
   220  func (c *Console) initAdmin(vm *goja.Runtime, bridge *bridge) {
   221  	if admin := getObject(vm, "admin"); admin != nil {
   222  		admin.Set("sleepBlocks", jsre.MakeCallback(vm, bridge.SleepBlocks))
   223  		admin.Set("sleep", jsre.MakeCallback(vm, bridge.Sleep))
   224  		admin.Set("clearHistory", c.clearHistory)
   225  	}
   226  }
   227  
   228  // initPersonal redirects account-related API methods through the bridge.
   229  //
   230  // If the console is in interactive mode and the 'personal' API is available, override
   231  // the openWallet, unlockAccount, newAccount and sign methods since these require user
   232  // interaction. The original web3 callbacks are stored in 'jeth'. These will be called
   233  // by the bridge after the prompt and send the original web3 request to the backend.
   234  func (c *Console) initPersonal(vm *goja.Runtime, bridge *bridge) {
   235  	personal := getObject(vm, "personal")
   236  	if personal == nil || c.prompter == nil {
   237  		return
   238  	}
   239  	jeth := vm.NewObject()
   240  	vm.Set("jeth", jeth)
   241  	jeth.Set("openWallet", personal.Get("openWallet"))
   242  	jeth.Set("unlockAccount", personal.Get("unlockAccount"))
   243  	jeth.Set("newAccount", personal.Get("newAccount"))
   244  	jeth.Set("sign", personal.Get("sign"))
   245  	personal.Set("openWallet", jsre.MakeCallback(vm, bridge.OpenWallet))
   246  	personal.Set("unlockAccount", jsre.MakeCallback(vm, bridge.UnlockAccount))
   247  	personal.Set("newAccount", jsre.MakeCallback(vm, bridge.NewAccount))
   248  	personal.Set("sign", jsre.MakeCallback(vm, bridge.Sign))
   249  }
   250  
   251  func (c *Console) clearHistory() {
   252  	c.history = nil
   253  	c.prompter.ClearHistory()
   254  	if err := os.Remove(c.histPath); err != nil {
   255  		fmt.Fprintln(c.printer, "can't delete history file:", err)
   256  	} else {
   257  		fmt.Fprintln(c.printer, "history file deleted")
   258  	}
   259  }
   260  
   261  // consoleOutput is an override for the console.log and console.error methods to
   262  // stream the output into the configured output stream instead of stdout.
   263  func (c *Console) consoleOutput(call goja.FunctionCall) goja.Value {
   264  	var output []string
   265  	for _, argument := range call.Arguments {
   266  		output = append(output, fmt.Sprintf("%v", argument))
   267  	}
   268  	fmt.Fprintln(c.printer, strings.Join(output, " "))
   269  	return goja.Null()
   270  }
   271  
   272  // AutoCompleteInput is a pre-assembled word completer to be used by the user
   273  // input prompter to provide hints to the user about the methods available.
   274  func (c *Console) AutoCompleteInput(line string, pos int) (string, []string, string) {
   275  	// No completions can be provided for empty inputs
   276  	if len(line) == 0 || pos == 0 {
   277  		return "", nil, ""
   278  	}
   279  	// Chunck data to relevant part for autocompletion
   280  	// E.g. in case of nested lines eth.getBalance(eth.coinb<tab><tab>
   281  	start := pos - 1
   282  	for ; start > 0; start-- {
   283  		// Skip all methods and namespaces (i.e. including the dot)
   284  		if line[start] == '.' || (line[start] >= 'a' && line[start] <= 'z') || (line[start] >= 'A' && line[start] <= 'Z') {
   285  			continue
   286  		}
   287  		// Handle web3 in a special way (i.e. other numbers aren't auto completed)
   288  		if start >= 3 && line[start-3:start] == "web3" {
   289  			start -= 3
   290  			continue
   291  		}
   292  		// We've hit an unexpected character, autocomplete form here
   293  		start++
   294  		break
   295  	}
   296  	return line[:start], c.jsre.CompleteKeywords(line[start:pos]), line[pos:]
   297  }
   298  
   299  // Welcome show summary of current Geth instance and some metadata about the
   300  // console's available modules.
   301  func (c *Console) Welcome() {
   302  	message := "Welcome to the Geth JavaScript console!\n\n"
   303  
   304  	// Print some generic Geth metadata
   305  	if res, err := c.jsre.Run(`
   306  		var message = "instance: " + web3.version.node + "\n";
   307  		try {
   308  			message += "coinbase: " + eth.coinbase + "\n";
   309  		} catch (err) {}
   310  		message += "at block: " + eth.blockNumber + " (" + new Date(1000 * eth.getBlock(eth.blockNumber).timestamp) + ")\n";
   311  		try {
   312  			message += " datadir: " + admin.datadir + "\n";
   313  		} catch (err) {}
   314  		message
   315  	`); err == nil {
   316  		message += res.String()
   317  	}
   318  	// List all the supported modules for the user to call
   319  	if apis, err := c.client.SupportedModules(); err == nil {
   320  		modules := make([]string, 0, len(apis))
   321  		for api, version := range apis {
   322  			modules = append(modules, fmt.Sprintf("%s:%s", api, version))
   323  		}
   324  		sort.Strings(modules)
   325  		message += " modules: " + strings.Join(modules, " ") + "\n"
   326  	}
   327  	fmt.Fprintln(c.printer, message)
   328  }
   329  
   330  // Evaluate executes code and pretty prints the result to the specified output
   331  // stream.
   332  func (c *Console) Evaluate(statement string) {
   333  	defer func() {
   334  		if r := recover(); r != nil {
   335  			fmt.Fprintf(c.printer, "[native] error: %v\n", r)
   336  		}
   337  	}()
   338  	c.jsre.Evaluate(statement, c.printer)
   339  }
   340  
   341  // Interactive starts an interactive user session, where input is propted from
   342  // the configured user prompter.
   343  func (c *Console) Interactive() {
   344  	var (
   345  		prompt      = c.prompt             // the current prompt line (used for multi-line inputs)
   346  		indents     = 0                    // the current number of input indents (used for multi-line inputs)
   347  		input       = ""                   // the current user input
   348  		inputLine   = make(chan string, 1) // receives user input
   349  		inputErr    = make(chan error, 1)  // receives liner errors
   350  		requestLine = make(chan string)    // requests a line of input
   351  		interrupt   = make(chan os.Signal, 1)
   352  	)
   353  
   354  	// Monitor Ctrl-C. While liner does turn on the relevant terminal mode bits to avoid
   355  	// the signal, a signal can still be received for unsupported terminals. Unfortunately
   356  	// there is no way to cancel the line reader when this happens. The readLines
   357  	// goroutine will be leaked in this case.
   358  	signal.Notify(interrupt, syscall.SIGINT, syscall.SIGTERM)
   359  	defer signal.Stop(interrupt)
   360  
   361  	// The line reader runs in a separate goroutine.
   362  	go c.readLines(inputLine, inputErr, requestLine)
   363  	defer close(requestLine)
   364  
   365  	for {
   366  		// Send the next prompt, triggering an input read.
   367  		requestLine <- prompt
   368  
   369  		select {
   370  		case <-interrupt:
   371  			fmt.Fprintln(c.printer, "caught interrupt, exiting")
   372  			return
   373  
   374  		case err := <-inputErr:
   375  			if err == liner.ErrPromptAborted && indents > 0 {
   376  				// When prompting for multi-line input, the first Ctrl-C resets
   377  				// the multi-line state.
   378  				prompt, indents, input = c.prompt, 0, ""
   379  				continue
   380  			}
   381  			return
   382  
   383  		case line := <-inputLine:
   384  			// User input was returned by the prompter, handle special cases.
   385  			if indents <= 0 && exit.MatchString(line) {
   386  				return
   387  			}
   388  			if onlyWhitespace.MatchString(line) {
   389  				continue
   390  			}
   391  			// Append the line to the input and check for multi-line interpretation.
   392  			input += line + "\n"
   393  			indents = countIndents(input)
   394  			if indents <= 0 {
   395  				prompt = c.prompt
   396  			} else {
   397  				prompt = strings.Repeat(".", indents*3) + " "
   398  			}
   399  			// If all the needed lines are present, save the command and run it.
   400  			if indents <= 0 {
   401  				if len(input) > 0 && input[0] != ' ' && !passwordRegexp.MatchString(input) {
   402  					if command := strings.TrimSpace(input); len(c.history) == 0 || command != c.history[len(c.history)-1] {
   403  						c.history = append(c.history, command)
   404  						if c.prompter != nil {
   405  							c.prompter.AppendHistory(command)
   406  						}
   407  					}
   408  				}
   409  				c.Evaluate(input)
   410  				input = ""
   411  			}
   412  		}
   413  	}
   414  }
   415  
   416  // readLines runs in its own goroutine, prompting for input.
   417  func (c *Console) readLines(input chan<- string, errc chan<- error, prompt <-chan string) {
   418  	for p := range prompt {
   419  		line, err := c.prompter.PromptInput(p)
   420  		if err != nil {
   421  			errc <- err
   422  		} else {
   423  			input <- line
   424  		}
   425  	}
   426  }
   427  
   428  // countIndents returns the number of identations for the given input.
   429  // In case of invalid input such as var a = } the result can be negative.
   430  func countIndents(input string) int {
   431  	var (
   432  		indents     = 0
   433  		inString    = false
   434  		strOpenChar = ' '   // keep track of the string open char to allow var str = "I'm ....";
   435  		charEscaped = false // keep track if the previous char was the '\' char, allow var str = "abc\"def";
   436  	)
   437  
   438  	for _, c := range input {
   439  		switch c {
   440  		case '\\':
   441  			// indicate next char as escaped when in string and previous char isn't escaping this backslash
   442  			if !charEscaped && inString {
   443  				charEscaped = true
   444  			}
   445  		case '\'', '"':
   446  			if inString && !charEscaped && strOpenChar == c { // end string
   447  				inString = false
   448  			} else if !inString && !charEscaped { // begin string
   449  				inString = true
   450  				strOpenChar = c
   451  			}
   452  			charEscaped = false
   453  		case '{', '(':
   454  			if !inString { // ignore brackets when in string, allow var str = "a{"; without indenting
   455  				indents++
   456  			}
   457  			charEscaped = false
   458  		case '}', ')':
   459  			if !inString {
   460  				indents--
   461  			}
   462  			charEscaped = false
   463  		default:
   464  			charEscaped = false
   465  		}
   466  	}
   467  
   468  	return indents
   469  }
   470  
   471  // Execute runs the JavaScript file specified as the argument.
   472  func (c *Console) Execute(path string) error {
   473  	return c.jsre.Exec(path)
   474  }
   475  
   476  // Stop cleans up the console and terminates the runtime environment.
   477  func (c *Console) Stop(graceful bool) error {
   478  	if err := ioutil.WriteFile(c.histPath, []byte(strings.Join(c.history, "\n")), 0600); err != nil {
   479  		return err
   480  	}
   481  	if err := os.Chmod(c.histPath, 0600); err != nil { // Force 0600, even if it was different previously
   482  		return err
   483  	}
   484  	c.jsre.Stop(graceful)
   485  	return nil
   486  }