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