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