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