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