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