github.com/tirogen/go-ethereum@v1.10.12-0.20221226051715-250cfede41b6/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/mattn/go-colorable"
    34  	"github.com/peterh/liner"
    35  	"github.com/tirogen/go-ethereum/console/prompt"
    36  	"github.com/tirogen/go-ethereum/internal/jsre"
    37  	"github.com/tirogen/go-ethereum/internal/jsre/deps"
    38  	"github.com/tirogen/go-ethereum/internal/web3ext"
    39  	"github.com/tirogen/go-ethereum/log"
    40  	"github.com/tirogen/go-ethereum/rpc"
    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": {}, "personal": {}}
   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  	jeth := vm.NewObject()
   264  	vm.Set("jeth", jeth)
   265  	jeth.Set("openWallet", personal.Get("openWallet"))
   266  	jeth.Set("unlockAccount", personal.Get("unlockAccount"))
   267  	jeth.Set("newAccount", personal.Get("newAccount"))
   268  	jeth.Set("sign", personal.Get("sign"))
   269  	personal.Set("openWallet", jsre.MakeCallback(vm, bridge.OpenWallet))
   270  	personal.Set("unlockAccount", jsre.MakeCallback(vm, bridge.UnlockAccount))
   271  	personal.Set("newAccount", jsre.MakeCallback(vm, bridge.NewAccount))
   272  	personal.Set("sign", jsre.MakeCallback(vm, bridge.Sign))
   273  }
   274  
   275  func (c *Console) clearHistory() {
   276  	c.history = nil
   277  	c.prompter.ClearHistory()
   278  	if err := os.Remove(c.histPath); err != nil {
   279  		fmt.Fprintln(c.printer, "can't delete history file:", err)
   280  	} else {
   281  		fmt.Fprintln(c.printer, "history file deleted")
   282  	}
   283  }
   284  
   285  // consoleOutput is an override for the console.log and console.error methods to
   286  // stream the output into the configured output stream instead of stdout.
   287  func (c *Console) consoleOutput(call goja.FunctionCall) goja.Value {
   288  	var output []string
   289  	for _, argument := range call.Arguments {
   290  		output = append(output, fmt.Sprintf("%v", argument))
   291  	}
   292  	fmt.Fprintln(c.printer, strings.Join(output, " "))
   293  	return goja.Null()
   294  }
   295  
   296  // AutoCompleteInput is a pre-assembled word completer to be used by the user
   297  // input prompter to provide hints to the user about the methods available.
   298  func (c *Console) AutoCompleteInput(line string, pos int) (string, []string, string) {
   299  	// No completions can be provided for empty inputs
   300  	if len(line) == 0 || pos == 0 {
   301  		return "", nil, ""
   302  	}
   303  	// Chunk data to relevant part for autocompletion
   304  	// E.g. in case of nested lines eth.getBalance(eth.coinb<tab><tab>
   305  	start := pos - 1
   306  	for ; start > 0; start-- {
   307  		// Skip all methods and namespaces (i.e. including the dot)
   308  		if line[start] == '.' || (line[start] >= 'a' && line[start] <= 'z') || (line[start] >= 'A' && line[start] <= 'Z') {
   309  			continue
   310  		}
   311  		// Handle web3 in a special way (i.e. other numbers aren't auto completed)
   312  		if start >= 3 && line[start-3:start] == "web3" {
   313  			start -= 3
   314  			continue
   315  		}
   316  		// We've hit an unexpected character, autocomplete form here
   317  		start++
   318  		break
   319  	}
   320  	return line[:start], c.jsre.CompleteKeywords(line[start:pos]), line[pos:]
   321  }
   322  
   323  // Welcome show summary of current Geth instance and some metadata about the
   324  // console's available modules.
   325  func (c *Console) Welcome() {
   326  	message := "Welcome to the Geth JavaScript console!\n\n"
   327  
   328  	// Print some generic Geth metadata
   329  	if res, err := c.jsre.Run(`
   330  		var message = "instance: " + web3.version.node + "\n";
   331  		try {
   332  			message += "coinbase: " + eth.coinbase + "\n";
   333  		} catch (err) {}
   334  		message += "at block: " + eth.blockNumber + " (" + new Date(1000 * eth.getBlock(eth.blockNumber).timestamp) + ")\n";
   335  		try {
   336  			message += " datadir: " + admin.datadir + "\n";
   337  		} catch (err) {}
   338  		message
   339  	`); err == nil {
   340  		message += res.String()
   341  	}
   342  	// List all the supported modules for the user to call
   343  	if apis, err := c.client.SupportedModules(); err == nil {
   344  		modules := make([]string, 0, len(apis))
   345  		for api, version := range apis {
   346  			modules = append(modules, fmt.Sprintf("%s:%s", api, version))
   347  		}
   348  		sort.Strings(modules)
   349  		message += " modules: " + strings.Join(modules, " ") + "\n"
   350  	}
   351  	message += "\nTo exit, press ctrl-d or type exit"
   352  	fmt.Fprintln(c.printer, message)
   353  }
   354  
   355  // Evaluate executes code and pretty prints the result to the specified output
   356  // stream.
   357  func (c *Console) Evaluate(statement string) {
   358  	defer func() {
   359  		if r := recover(); r != nil {
   360  			fmt.Fprintf(c.printer, "[native] error: %v\n", r)
   361  		}
   362  	}()
   363  	c.jsre.Evaluate(statement, c.printer)
   364  
   365  	// Avoid exiting Interactive when jsre was interrupted by SIGINT.
   366  	c.clearSignalReceived()
   367  }
   368  
   369  // interruptHandler runs in its own goroutine and waits for signals.
   370  // When a signal is received, it interrupts the JS interpreter.
   371  func (c *Console) interruptHandler() {
   372  	defer c.wg.Done()
   373  
   374  	// During Interactive, liner inhibits the signal while it is prompting for
   375  	// input. However, the signal will be received while evaluating JS.
   376  	//
   377  	// On unsupported terminals, SIGINT can also happen while prompting.
   378  	// Unfortunately, it is not possible to abort the prompt in this case and
   379  	// the c.readLines goroutine leaks.
   380  	sig := make(chan os.Signal, 1)
   381  	signal.Notify(sig, syscall.SIGINT)
   382  	defer signal.Stop(sig)
   383  
   384  	for {
   385  		select {
   386  		case <-sig:
   387  			c.setSignalReceived()
   388  			c.jsre.Interrupt(errors.New("interrupted"))
   389  		case <-c.stopInteractiveCh:
   390  			close(c.interactiveStopped)
   391  			c.jsre.Interrupt(errors.New("interrupted"))
   392  		case <-c.stopped:
   393  			return
   394  		}
   395  	}
   396  }
   397  
   398  func (c *Console) setSignalReceived() {
   399  	select {
   400  	case c.signalReceived <- struct{}{}:
   401  	default:
   402  	}
   403  }
   404  
   405  func (c *Console) clearSignalReceived() {
   406  	select {
   407  	case <-c.signalReceived:
   408  	default:
   409  	}
   410  }
   411  
   412  // StopInteractive causes Interactive to return as soon as possible.
   413  func (c *Console) StopInteractive() {
   414  	select {
   415  	case c.stopInteractiveCh <- struct{}{}:
   416  	case <-c.stopped:
   417  	}
   418  }
   419  
   420  // Interactive starts an interactive user session, where input is prompted from
   421  // the configured user prompter.
   422  func (c *Console) Interactive() {
   423  	var (
   424  		prompt      = c.prompt             // the current prompt line (used for multi-line inputs)
   425  		indents     = 0                    // the current number of input indents (used for multi-line inputs)
   426  		input       = ""                   // the current user input
   427  		inputLine   = make(chan string, 1) // receives user input
   428  		inputErr    = make(chan error, 1)  // receives liner errors
   429  		requestLine = make(chan string)    // requests a line of input
   430  	)
   431  
   432  	defer func() {
   433  		c.writeHistory()
   434  	}()
   435  
   436  	// The line reader runs in a separate goroutine.
   437  	go c.readLines(inputLine, inputErr, requestLine)
   438  	defer close(requestLine)
   439  
   440  	for {
   441  		// Send the next prompt, triggering an input read.
   442  		requestLine <- prompt
   443  
   444  		select {
   445  		case <-c.interactiveStopped:
   446  			fmt.Fprintln(c.printer, "node is down, exiting console")
   447  			return
   448  
   449  		case <-c.signalReceived:
   450  			// SIGINT received while prompting for input -> unsupported terminal.
   451  			// I'm not sure if the best choice would be to leave the console running here.
   452  			// Bash keeps running in this case. node.js does not.
   453  			fmt.Fprintln(c.printer, "caught interrupt, exiting")
   454  			return
   455  
   456  		case err := <-inputErr:
   457  			if err == liner.ErrPromptAborted {
   458  				// When prompting for multi-line input, the first Ctrl-C resets
   459  				// the multi-line state.
   460  				prompt, indents, input = c.prompt, 0, ""
   461  				continue
   462  			}
   463  			return
   464  
   465  		case line := <-inputLine:
   466  			// User input was returned by the prompter, handle special cases.
   467  			if indents <= 0 && exit.MatchString(line) {
   468  				return
   469  			}
   470  			if onlyWhitespace.MatchString(line) {
   471  				continue
   472  			}
   473  			// Append the line to the input and check for multi-line interpretation.
   474  			input += line + "\n"
   475  			indents = countIndents(input)
   476  			if indents <= 0 {
   477  				prompt = c.prompt
   478  			} else {
   479  				prompt = strings.Repeat(".", indents*3) + " "
   480  			}
   481  			// If all the needed lines are present, save the command and run it.
   482  			if indents <= 0 {
   483  				if len(input) > 0 && input[0] != ' ' && !passwordRegexp.MatchString(input) {
   484  					if command := strings.TrimSpace(input); len(c.history) == 0 || command != c.history[len(c.history)-1] {
   485  						c.history = append(c.history, command)
   486  						if c.prompter != nil {
   487  							c.prompter.AppendHistory(command)
   488  						}
   489  					}
   490  				}
   491  				c.Evaluate(input)
   492  				input = ""
   493  			}
   494  		}
   495  	}
   496  }
   497  
   498  // readLines runs in its own goroutine, prompting for input.
   499  func (c *Console) readLines(input chan<- string, errc chan<- error, prompt <-chan string) {
   500  	for p := range prompt {
   501  		line, err := c.prompter.PromptInput(p)
   502  		if err != nil {
   503  			errc <- err
   504  		} else {
   505  			input <- line
   506  		}
   507  	}
   508  }
   509  
   510  // countIndents returns the number of indentations for the given input.
   511  // In case of invalid input such as var a = } the result can be negative.
   512  func countIndents(input string) int {
   513  	var (
   514  		indents     = 0
   515  		inString    = false
   516  		strOpenChar = ' '   // keep track of the string open char to allow var str = "I'm ....";
   517  		charEscaped = false // keep track if the previous char was the '\' char, allow var str = "abc\"def";
   518  	)
   519  
   520  	for _, c := range input {
   521  		switch c {
   522  		case '\\':
   523  			// indicate next char as escaped when in string and previous char isn't escaping this backslash
   524  			if !charEscaped && inString {
   525  				charEscaped = true
   526  			}
   527  		case '\'', '"':
   528  			if inString && !charEscaped && strOpenChar == c { // end string
   529  				inString = false
   530  			} else if !inString && !charEscaped { // begin string
   531  				inString = true
   532  				strOpenChar = c
   533  			}
   534  			charEscaped = false
   535  		case '{', '(':
   536  			if !inString { // ignore brackets when in string, allow var str = "a{"; without indenting
   537  				indents++
   538  			}
   539  			charEscaped = false
   540  		case '}', ')':
   541  			if !inString {
   542  				indents--
   543  			}
   544  			charEscaped = false
   545  		default:
   546  			charEscaped = false
   547  		}
   548  	}
   549  
   550  	return indents
   551  }
   552  
   553  // Stop cleans up the console and terminates the runtime environment.
   554  func (c *Console) Stop(graceful bool) error {
   555  	c.stopOnce.Do(func() {
   556  		// Stop the interrupt handler.
   557  		close(c.stopped)
   558  		c.wg.Wait()
   559  	})
   560  
   561  	c.jsre.Stop(graceful)
   562  	return nil
   563  }
   564  
   565  func (c *Console) writeHistory() error {
   566  	if err := os.WriteFile(c.histPath, []byte(strings.Join(c.history, "\n")), 0600); err != nil {
   567  		return err
   568  	}
   569  	return os.Chmod(c.histPath, 0600) // Force 0600, even if it was different previously
   570  }