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