git.pirl.io/community/pirl@v0.0.0-20201111064343-9d3d31ff74be/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  	"git.pirl.io/community/pirl/internal/jsre"
    33  	"git.pirl.io/community/pirl/internal/jsre/deps"
    34  	"git.pirl.io/community/pirl/internal/web3ext"
    35  	"git.pirl.io/community/pirl/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          // Current prompt line (used for multi-line inputs)
   344  		indents   = 0                 // Current number of input indents (used for multi-line inputs)
   345  		input     = ""                // Current user input
   346  		scheduler = make(chan string) // Channel to send the next prompt on and receive the input
   347  	)
   348  	// Start a goroutine to listen for prompt requests and send back inputs
   349  	go func() {
   350  		for {
   351  			// Read the next user input
   352  			line, err := c.prompter.PromptInput(<-scheduler)
   353  			if err != nil {
   354  				// In case of an error, either clear the prompt or fail
   355  				if err == liner.ErrPromptAborted { // ctrl-C
   356  					prompt, indents, input = c.prompt, 0, ""
   357  					scheduler <- ""
   358  					continue
   359  				}
   360  				close(scheduler)
   361  				return
   362  			}
   363  			// User input retrieved, send for interpretation and loop
   364  			scheduler <- line
   365  		}
   366  	}()
   367  	// Monitor Ctrl-C too in case the input is empty and we need to bail
   368  	abort := make(chan os.Signal, 1)
   369  	signal.Notify(abort, syscall.SIGINT, syscall.SIGTERM)
   370  
   371  	// Start sending prompts to the user and reading back inputs
   372  	for {
   373  		// Send the next prompt, triggering an input read and process the result
   374  		scheduler <- prompt
   375  		select {
   376  		case <-abort:
   377  			// User forcefully quite the console
   378  			fmt.Fprintln(c.printer, "caught interrupt, exiting")
   379  			return
   380  
   381  		case line, ok := <-scheduler:
   382  			// User input was returned by the prompter, handle special cases
   383  			if !ok || (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  
   392  			indents = countIndents(input)
   393  			if indents <= 0 {
   394  				prompt = c.prompt
   395  			} else {
   396  				prompt = strings.Repeat(".", indents*3) + " "
   397  			}
   398  			// If all the needed lines are present, save the command and run
   399  			if indents <= 0 {
   400  				if len(input) > 0 && input[0] != ' ' && !passwordRegexp.MatchString(input) {
   401  					if command := strings.TrimSpace(input); len(c.history) == 0 || command != c.history[len(c.history)-1] {
   402  						c.history = append(c.history, command)
   403  						if c.prompter != nil {
   404  							c.prompter.AppendHistory(command)
   405  						}
   406  					}
   407  				}
   408  				c.Evaluate(input)
   409  				input = ""
   410  			}
   411  		}
   412  	}
   413  }
   414  
   415  // countIndents returns the number of identations for the given input.
   416  // In case of invalid input such as var a = } the result can be negative.
   417  func countIndents(input string) int {
   418  	var (
   419  		indents     = 0
   420  		inString    = false
   421  		strOpenChar = ' '   // keep track of the string open char to allow var str = "I'm ....";
   422  		charEscaped = false // keep track if the previous char was the '\' char, allow var str = "abc\"def";
   423  	)
   424  
   425  	for _, c := range input {
   426  		switch c {
   427  		case '\\':
   428  			// indicate next char as escaped when in string and previous char isn't escaping this backslash
   429  			if !charEscaped && inString {
   430  				charEscaped = true
   431  			}
   432  		case '\'', '"':
   433  			if inString && !charEscaped && strOpenChar == c { // end string
   434  				inString = false
   435  			} else if !inString && !charEscaped { // begin string
   436  				inString = true
   437  				strOpenChar = c
   438  			}
   439  			charEscaped = false
   440  		case '{', '(':
   441  			if !inString { // ignore brackets when in string, allow var str = "a{"; without indenting
   442  				indents++
   443  			}
   444  			charEscaped = false
   445  		case '}', ')':
   446  			if !inString {
   447  				indents--
   448  			}
   449  			charEscaped = false
   450  		default:
   451  			charEscaped = false
   452  		}
   453  	}
   454  
   455  	return indents
   456  }
   457  
   458  // Execute runs the JavaScript file specified as the argument.
   459  func (c *Console) Execute(path string) error {
   460  	return c.jsre.Exec(path)
   461  }
   462  
   463  // Stop cleans up the console and terminates the runtime environment.
   464  func (c *Console) Stop(graceful bool) error {
   465  	if err := ioutil.WriteFile(c.histPath, []byte(strings.Join(c.history, "\n")), 0600); err != nil {
   466  		return err
   467  	}
   468  	if err := os.Chmod(c.histPath, 0600); err != nil { // Force 0600, even if it was different previously
   469  		return err
   470  	}
   471  	c.jsre.Stop(graceful)
   472  	return nil
   473  }