github.com/core-coin/go-core/v2@v2.1.9/console/console.go (about)

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