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