github.com/ethereum/go-ethereum@v1.16.1/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  	})
   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  var defaultAPIs = map[string]string{"eth": "1.0", "net": "1.0", "debug": "1.0"}
   202  
   203  // initExtensions loads and registers web3.js extensions.
   204  func (c *Console) initExtensions() error {
   205  	const methodNotFound = -32601
   206  	apis, err := c.client.SupportedModules()
   207  	if err != nil {
   208  		if rpcErr, ok := err.(rpc.Error); ok && rpcErr.ErrorCode() == methodNotFound {
   209  			log.Warn("Server does not support method rpc_modules, using default API list.")
   210  			apis = defaultAPIs
   211  		} else {
   212  			return err
   213  		}
   214  	}
   215  
   216  	// Compute aliases from server-provided modules.
   217  	aliases := map[string]struct{}{"eth": {}}
   218  	for api := range apis {
   219  		if api == "web3" {
   220  			continue
   221  		}
   222  		aliases[api] = struct{}{}
   223  		if file, ok := web3ext.Modules[api]; ok {
   224  			if err = c.jsre.Compile(api+".js", file); err != nil {
   225  				return fmt.Errorf("%s.js: %v", api, err)
   226  			}
   227  		}
   228  	}
   229  
   230  	// Apply aliases.
   231  	c.jsre.Do(func(vm *goja.Runtime) {
   232  		web3 := getObject(vm, "web3")
   233  		for name := range aliases {
   234  			if v := web3.Get(name); v != nil {
   235  				vm.Set(name, v)
   236  			}
   237  		}
   238  	})
   239  	return nil
   240  }
   241  
   242  // initAdmin creates additional admin APIs implemented by the bridge.
   243  func (c *Console) initAdmin(vm *goja.Runtime, bridge *bridge) {
   244  	if admin := getObject(vm, "admin"); admin != nil {
   245  		admin.Set("sleepBlocks", jsre.MakeCallback(vm, bridge.SleepBlocks))
   246  		admin.Set("sleep", jsre.MakeCallback(vm, bridge.Sleep))
   247  		admin.Set("clearHistory", c.clearHistory)
   248  	}
   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  	// Chunk 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  		c := line[start]
   285  		if c == '.' || (c >= 'a' && c <= 'z') || (c >= 'A' && c <= 'Z') || (c >= '1' && c <= '9') {
   286  			continue
   287  		}
   288  		// We've hit an unexpected character, autocomplete form here
   289  		start++
   290  		break
   291  	}
   292  	return line[:start], c.jsre.CompleteKeywords(line[start:pos]), line[pos:]
   293  }
   294  
   295  // Welcome show summary of current Geth instance and some metadata about the
   296  // console's available modules.
   297  func (c *Console) Welcome() {
   298  	message := "Welcome to the Geth JavaScript console!\n\n"
   299  
   300  	// Print some generic Geth metadata
   301  	if res, err := c.jsre.Run(`
   302  		var message = "instance: " + web3.version.node + "\n";
   303  		message += "at block: " + eth.blockNumber + " (" + new Date(1000 * eth.getBlock(eth.blockNumber).timestamp) + ")\n";
   304  		try {
   305  			message += " datadir: " + admin.datadir + "\n";
   306  		} catch (err) {}
   307  		message
   308  	`); err == nil {
   309  		message += res.String()
   310  	}
   311  	// List all the supported modules for the user to call
   312  	if apis, err := c.client.SupportedModules(); err == nil {
   313  		modules := make([]string, 0, len(apis))
   314  		for api, version := range apis {
   315  			modules = append(modules, fmt.Sprintf("%s:%s", api, version))
   316  		}
   317  		sort.Strings(modules)
   318  		message += " modules: " + strings.Join(modules, " ") + "\n"
   319  	}
   320  	message += "\nTo exit, press ctrl-d or type exit"
   321  	fmt.Fprintln(c.printer, message)
   322  }
   323  
   324  // Evaluate executes code and pretty prints the result to the specified output
   325  // stream.
   326  func (c *Console) Evaluate(statement string) {
   327  	defer func() {
   328  		if r := recover(); r != nil {
   329  			fmt.Fprintf(c.printer, "[native] error: %v\n", r)
   330  		}
   331  	}()
   332  	c.jsre.Evaluate(statement, c.printer)
   333  
   334  	// Avoid exiting Interactive when jsre was interrupted by SIGINT.
   335  	c.clearSignalReceived()
   336  }
   337  
   338  // interruptHandler runs in its own goroutine and waits for signals.
   339  // When a signal is received, it interrupts the JS interpreter.
   340  func (c *Console) interruptHandler() {
   341  	defer c.wg.Done()
   342  
   343  	// During Interactive, liner inhibits the signal while it is prompting for
   344  	// input. However, the signal will be received while evaluating JS.
   345  	//
   346  	// On unsupported terminals, SIGINT can also happen while prompting.
   347  	// Unfortunately, it is not possible to abort the prompt in this case and
   348  	// the c.readLines goroutine leaks.
   349  	sig := make(chan os.Signal, 1)
   350  	signal.Notify(sig, syscall.SIGINT)
   351  	defer signal.Stop(sig)
   352  
   353  	for {
   354  		select {
   355  		case <-sig:
   356  			c.setSignalReceived()
   357  			c.jsre.Interrupt(errors.New("interrupted"))
   358  		case <-c.stopInteractiveCh:
   359  			close(c.interactiveStopped)
   360  			c.jsre.Interrupt(errors.New("interrupted"))
   361  		case <-c.stopped:
   362  			return
   363  		}
   364  	}
   365  }
   366  
   367  func (c *Console) setSignalReceived() {
   368  	select {
   369  	case c.signalReceived <- struct{}{}:
   370  	default:
   371  	}
   372  }
   373  
   374  func (c *Console) clearSignalReceived() {
   375  	select {
   376  	case <-c.signalReceived:
   377  	default:
   378  	}
   379  }
   380  
   381  // StopInteractive causes Interactive to return as soon as possible.
   382  func (c *Console) StopInteractive() {
   383  	select {
   384  	case c.stopInteractiveCh <- struct{}{}:
   385  	case <-c.stopped:
   386  	}
   387  }
   388  
   389  // Interactive starts an interactive user session, where input is prompted from
   390  // the configured user prompter.
   391  func (c *Console) Interactive() {
   392  	var (
   393  		prompt      = c.prompt             // the current prompt line (used for multi-line inputs)
   394  		indents     = 0                    // the current number of input indents (used for multi-line inputs)
   395  		input       = ""                   // the current user input
   396  		inputLine   = make(chan string, 1) // receives user input
   397  		inputErr    = make(chan error, 1)  // receives liner errors
   398  		requestLine = make(chan string)    // requests a line of input
   399  	)
   400  
   401  	defer func() {
   402  		c.writeHistory()
   403  	}()
   404  
   405  	// The line reader runs in a separate goroutine.
   406  	go c.readLines(inputLine, inputErr, requestLine)
   407  	defer close(requestLine)
   408  
   409  	for {
   410  		// Send the next prompt, triggering an input read.
   411  		requestLine <- prompt
   412  
   413  		select {
   414  		case <-c.interactiveStopped:
   415  			fmt.Fprintln(c.printer, "node is down, exiting console")
   416  			return
   417  
   418  		case <-c.signalReceived:
   419  			// SIGINT received while prompting for input -> unsupported terminal.
   420  			// I'm not sure if the best choice would be to leave the console running here.
   421  			// Bash keeps running in this case. node.js does not.
   422  			fmt.Fprintln(c.printer, "caught interrupt, exiting")
   423  			return
   424  
   425  		case err := <-inputErr:
   426  			if err == liner.ErrPromptAborted {
   427  				// When prompting for multi-line input, the first Ctrl-C resets
   428  				// the multi-line state.
   429  				prompt, indents, input = c.prompt, 0, ""
   430  				continue
   431  			}
   432  			return
   433  
   434  		case line := <-inputLine:
   435  			// User input was returned by the prompter, handle special cases.
   436  			if indents <= 0 && exit.MatchString(line) {
   437  				return
   438  			}
   439  			if onlyWhitespace.MatchString(line) {
   440  				continue
   441  			}
   442  			// Append the line to the input and check for multi-line interpretation.
   443  			input += line + "\n"
   444  			indents = countIndents(input)
   445  			if indents <= 0 {
   446  				prompt = c.prompt
   447  			} else {
   448  				prompt = strings.Repeat(".", indents*3) + " "
   449  			}
   450  			// If all the needed lines are present, save the command and run it.
   451  			if indents <= 0 {
   452  				if len(input) > 0 && input[0] != ' ' && !passwordRegexp.MatchString(input) {
   453  					if command := strings.TrimSpace(input); len(c.history) == 0 || command != c.history[len(c.history)-1] {
   454  						c.history = append(c.history, command)
   455  						if c.prompter != nil {
   456  							c.prompter.AppendHistory(command)
   457  						}
   458  					}
   459  				}
   460  				c.Evaluate(input)
   461  				input = ""
   462  			}
   463  		}
   464  	}
   465  }
   466  
   467  // readLines runs in its own goroutine, prompting for input.
   468  func (c *Console) readLines(input chan<- string, errc chan<- error, prompt <-chan string) {
   469  	for p := range prompt {
   470  		line, err := c.prompter.PromptInput(p)
   471  		if err != nil {
   472  			errc <- err
   473  		} else {
   474  			input <- line
   475  		}
   476  	}
   477  }
   478  
   479  // countIndents returns the number of indentations for the given input.
   480  // In case of invalid input such as var a = } the result can be negative.
   481  func countIndents(input string) int {
   482  	var (
   483  		indents     = 0
   484  		inString    = false
   485  		strOpenChar = ' '   // keep track of the string open char to allow var str = "I'm ....";
   486  		charEscaped = false // keep track if the previous char was the '\' char, allow var str = "abc\"def";
   487  	)
   488  
   489  	for _, c := range input {
   490  		switch c {
   491  		case '\\':
   492  			// indicate next char as escaped when in string and previous char isn't escaping this backslash
   493  			if !charEscaped && inString {
   494  				charEscaped = true
   495  			}
   496  		case '\'', '"':
   497  			if inString && !charEscaped && strOpenChar == c { // end string
   498  				inString = false
   499  			} else if !inString && !charEscaped { // begin string
   500  				inString = true
   501  				strOpenChar = c
   502  			}
   503  			charEscaped = false
   504  		case '{', '(':
   505  			if !inString { // ignore brackets when in string, allow var str = "a{"; without indenting
   506  				indents++
   507  			}
   508  			charEscaped = false
   509  		case '}', ')':
   510  			if !inString {
   511  				indents--
   512  			}
   513  			charEscaped = false
   514  		default:
   515  			charEscaped = false
   516  		}
   517  	}
   518  
   519  	return indents
   520  }
   521  
   522  // Stop cleans up the console and terminates the runtime environment.
   523  func (c *Console) Stop(graceful bool) error {
   524  	c.stopOnce.Do(func() {
   525  		// Stop the interrupt handler.
   526  		close(c.stopped)
   527  		c.wg.Wait()
   528  	})
   529  
   530  	c.jsre.Stop(graceful)
   531  	return nil
   532  }
   533  
   534  func (c *Console) writeHistory() error {
   535  	if err := os.WriteFile(c.histPath, []byte(strings.Join(c.history, "\n")), 0600); err != nil {
   536  		return err
   537  	}
   538  	return os.Chmod(c.histPath, 0600) // Force 0600, even if it was different previously
   539  }