github.com/klaytn/klaytn@v1.12.1/console/console.go (about)

     1  // Modifications Copyright 2018 The klaytn Authors
     2  // Copyright 2016 The go-ethereum Authors
     3  // This file is part of the go-ethereum library.
     4  //
     5  // The go-ethereum library is free software: you can redistribute it and/or modify
     6  // it under the terms of the GNU Lesser General Public License as published by
     7  // the Free Software Foundation, either version 3 of the License, or
     8  // (at your option) any later version.
     9  //
    10  // The go-ethereum library is distributed in the hope that it will be useful,
    11  // but WITHOUT ANY WARRANTY; without even the implied warranty of
    12  // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
    13  // GNU Lesser General Public License for more details.
    14  //
    15  // You should have received a copy of the GNU Lesser General Public License
    16  // along with the go-ethereum library. If not, see <http://www.gnu.org/licenses/>.
    17  //
    18  // This file is derived from console/console.go (2018/06/04).
    19  // Modified and improved for the klaytn development.
    20  
    21  package console
    22  
    23  import (
    24  	"fmt"
    25  	"io"
    26  	"os"
    27  	"os/signal"
    28  	"path/filepath"
    29  	"regexp"
    30  	"sort"
    31  	"strings"
    32  	"syscall"
    33  
    34  	"github.com/dop251/goja"
    35  	"github.com/klaytn/klaytn/console/jsre"
    36  	"github.com/klaytn/klaytn/console/jsre/deps"
    37  	"github.com/klaytn/klaytn/console/web3ext"
    38  	"github.com/klaytn/klaytn/log"
    39  	"github.com/klaytn/klaytn/networks/rpc"
    40  	"github.com/mattn/go-colorable"
    41  	"github.com/peterh/liner"
    42  )
    43  
    44  var (
    45  	passwordRegexp = regexp.MustCompile(`personal.[nus]`)
    46  	onlyWhitespace = regexp.MustCompile(`^\s*$`)
    47  	exit           = regexp.MustCompile(`^\s*exit\s*;*\s*$`)
    48  	logger         = log.NewModuleLogger(log.Console)
    49  )
    50  
    51  // HistoryFile is the file within the data directory to store input scrollback.
    52  const HistoryFile = "history"
    53  
    54  // DefaultPrompt is the default prompt line prefix to use for user input querying.
    55  const DefaultPrompt = "> "
    56  
    57  // Config is the collection of configurations to fine tune the behavior of the
    58  // JavaScript console.
    59  type Config struct {
    60  	DataDir  string       // Data directory to store the console history at
    61  	DocRoot  string       // Filesystem path from where to load JavaScript files from
    62  	Client   *rpc.Client  // RPC client to execute Klaytn requests through
    63  	Prompt   string       // Input prompt prefix string (defaults to DefaultPrompt)
    64  	Prompter UserPrompter // Input prompter to allow interactive user feedback (defaults to TerminalPrompter)
    65  	Printer  io.Writer    // Output writer to serialize any display strings to (defaults to os.Stdout)
    66  	Preload  []string     // Absolute paths to JavaScript files to preload
    67  }
    68  
    69  // Console is a JavaScript interpreted runtime environment. It is a fully fleged
    70  // JavaScript console attached to a running node via an external or in-process RPC
    71  // client.
    72  type Console struct {
    73  	client   *rpc.Client  // RPC client to execute Klaytn requests through
    74  	jsre     *jsre.JSRE   // JavaScript runtime environment running the interpreter
    75  	prompt   string       // Input prompt prefix string
    76  	prompter UserPrompter // Input prompter to allow interactive user feedback
    77  	histPath string       // Absolute path to the console scrollback history
    78  	history  []string     // Scroll history maintained by the console
    79  	printer  io.Writer    // Output writer to serialize any display strings to
    80  }
    81  
    82  func New(config Config) (*Console, error) {
    83  	// Handle unset config values gracefully
    84  	if config.Prompter == nil {
    85  		config.Prompter = Stdin
    86  	}
    87  	if config.Prompt == "" {
    88  		config.Prompt = DefaultPrompt
    89  	}
    90  	if config.Printer == nil {
    91  		config.Printer = colorable.NewColorableStdout()
    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, 0o700); 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 := os.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  	if err := c.jsre.Compile("bignumber.js", deps.BigNumberJS); err != nil {
   166  		return fmt.Errorf("bignumber.js: %v", err)
   167  	}
   168  	if err := c.jsre.Compile("web3.js", deps.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  var defaultAPIs = map[string]string{"eth": "1.0", "net": "1.0", "debug": "1.0"}
   186  
   187  // initExtensions loads and registers web3.js extensions.
   188  func (c *Console) initExtensions() error {
   189  	const methodNotFound = -32601
   190  	apis, err := c.client.SupportedModules()
   191  	if err != nil {
   192  		if rpcErr, ok := err.(rpc.Error); ok && rpcErr.ErrorCode() == methodNotFound {
   193  			logger.Warn("Server does not support method rpc_modules, using default API list.")
   194  			apis = defaultAPIs
   195  		} else {
   196  			return err
   197  		}
   198  	}
   199  
   200  	// Compute aliases from server-provided modules.
   201  	aliases := map[string]struct{}{"klay": {}, "eth": {}}
   202  	for api := range apis {
   203  		if api == "web3" {
   204  			continue
   205  		}
   206  		aliases[api] = struct{}{}
   207  		if file, ok := web3ext.Modules[api]; ok {
   208  			if err = c.jsre.Compile(api+".js", file); err != nil {
   209  				return fmt.Errorf("%s.js: %v", api, err)
   210  			}
   211  		}
   212  	}
   213  
   214  	// Apply aliases.
   215  	c.jsre.Do(func(vm *goja.Runtime) {
   216  		web3 := getObject(vm, "web3")
   217  		for name := range aliases {
   218  			if v := web3.Get(name); v != nil {
   219  				vm.Set(name, v)
   220  			}
   221  		}
   222  	})
   223  	return nil
   224  }
   225  
   226  // initAdmin creates additional admin APIs implemented by the bridge.
   227  func (c *Console) initAdmin(vm *goja.Runtime, bridge *bridge) {
   228  	if admin := getObject(vm, "admin"); admin != nil {
   229  		admin.Set("sleepBlocks", jsre.MakeCallback(vm, bridge.SleepBlocks))
   230  		admin.Set("sleep", jsre.MakeCallback(vm, bridge.Sleep))
   231  		admin.Set("clearHistory", c.clearHistory)
   232  	}
   233  }
   234  
   235  // initPersonal redirects account-related API methods through the bridge.
   236  //
   237  // If the console is in interactive mode and the 'personal' API is available, override
   238  // the openWallet, unlockAccount, newAccount and sign methods since these require user
   239  // interaction. The original web3 callbacks are stored in 'jeth'. These will be called
   240  // by the bridge after the prompt and send the original web3 request to the backend.
   241  func (c *Console) initPersonal(vm *goja.Runtime, bridge *bridge) {
   242  	personal := getObject(vm, "personal")
   243  	if personal == nil || c.prompter == nil {
   244  		return
   245  	}
   246  	// Geth deprecated the `personal` namespace.
   247  	// logger.Warn("Enabling deprecated personal namespace")
   248  	jeth := vm.NewObject()
   249  	vm.Set("jeth", jeth)
   250  	jeth.Set("openWallet", personal.Get("openWallet"))
   251  	jeth.Set("unlockAccount", personal.Get("unlockAccount"))
   252  	jeth.Set("newAccount", personal.Get("newAccount"))
   253  	jeth.Set("sign", personal.Get("sign"))
   254  	personal.Set("openWallet", jsre.MakeCallback(vm, bridge.OpenWallet))
   255  	personal.Set("unlockAccount", jsre.MakeCallback(vm, bridge.UnlockAccount))
   256  	personal.Set("newAccount", jsre.MakeCallback(vm, bridge.NewAccount))
   257  	personal.Set("sign", jsre.MakeCallback(vm, bridge.Sign))
   258  
   259  	subBridge := getObject(vm, "subbridge")
   260  	if subBridge != nil {
   261  		jeth.Set("unlockParentOperator", subBridge.Get("unlockParentOperator"))
   262  		subBridge.Set("unlockParentOperator", jsre.MakeCallback(vm, bridge.UnlockParentOperator))
   263  
   264  		jeth.Set("unlockChildOperator", subBridge.Get("unlockChildOperator"))
   265  		subBridge.Set("unlockChildOperator", jsre.MakeCallback(vm, bridge.UnlockChildOperator))
   266  	}
   267  }
   268  
   269  func (c *Console) clearHistory() {
   270  	c.history = nil
   271  	c.prompter.ClearHistory()
   272  	if err := os.Remove(c.histPath); err != nil {
   273  		fmt.Fprintln(c.printer, "can't delete history file:", err)
   274  	} else {
   275  		fmt.Fprintln(c.printer, "history file deleted")
   276  	}
   277  }
   278  
   279  // consoleOutput is an override for the console.log and console.error methods to
   280  // stream the output into the configured output stream instead of stdout.
   281  func (c *Console) consoleOutput(call goja.FunctionCall) goja.Value {
   282  	output := make([]string, len(call.Arguments))
   283  	for _, argument := range call.Arguments {
   284  		output = append(output, fmt.Sprintf("%v", argument))
   285  	}
   286  	fmt.Fprintln(c.printer, strings.Join(output, " "))
   287  	return goja.Null()
   288  }
   289  
   290  // AutoCompleteInput is a pre-assembled word completer to be used by the user
   291  // input prompter to provide hints to the user about the methods available.
   292  func (c *Console) AutoCompleteInput(line string, pos int) (string, []string, string) {
   293  	// No completions can be provided for empty inputs
   294  	if len(line) == 0 || pos == 0 {
   295  		return "", nil, ""
   296  	}
   297  	// Chunk data to relevant part for autocompletion
   298  	// E.g. in case of nested lines klay.getBalance(klay.coinb<tab><tab>
   299  	start := pos - 1
   300  	for ; start > 0; start-- {
   301  		// Skip all methods and namespaces (i.e. including the dot)
   302  		if line[start] == '.' || (line[start] >= 'a' && line[start] <= 'z') || (line[start] >= 'A' && line[start] <= 'Z') {
   303  			continue
   304  		}
   305  		// Handle web3 in a special way (i.e. other numbers aren't auto completed)
   306  		if start >= 3 && line[start-3:start] == "web3" {
   307  			start -= 3
   308  			continue
   309  		}
   310  		// We've hit an unexpected character, autocomplete form here
   311  		start++
   312  		break
   313  	}
   314  	return line[:start], c.jsre.CompleteKeywords(line[start:pos]), line[pos:]
   315  }
   316  
   317  // Welcome show summary of current node instance and some metadata about the
   318  // console's available modules.
   319  func (c *Console) Welcome() {
   320  	// Print some generic Klaytn metadata
   321  	fmt.Fprintf(c.printer, "Welcome to the Klaytn JavaScript console!\n\n")
   322  	c.jsre.Run(`
   323  		console.log("instance: " + web3.version.node);
   324  		console.log(" datadir: " + admin.datadir);
   325  	`)
   326  	// List all the supported modules for the user to call
   327  	if apis, err := c.client.SupportedModules(); err == nil {
   328  		modules := make([]string, 0, len(apis))
   329  		for api, version := range apis {
   330  			modules = append(modules, fmt.Sprintf("%s:%s", api, version))
   331  		}
   332  		sort.Strings(modules)
   333  		fmt.Fprintln(c.printer, "  modules:", strings.Join(modules, " "))
   334  	}
   335  	fmt.Fprintln(c.printer)
   336  }
   337  
   338  // Evaluate executes code and pretty prints the result to the specified output
   339  // stream.
   340  func (c *Console) Evaluate(statement string) {
   341  	defer func() {
   342  		if r := recover(); r != nil {
   343  			fmt.Fprintf(c.printer, "[native] error: %v\n", r)
   344  		}
   345  	}()
   346  	c.jsre.Evaluate(statement, c.printer)
   347  }
   348  
   349  // Interactive starts an interactive user session, where input is propted from
   350  // the configured user prompter.
   351  func (c *Console) Interactive() {
   352  	var (
   353  		prompt    = c.prompt          // Current prompt line (used for multi-line inputs)
   354  		indents   = 0                 // Current number of input indents (used for multi-line inputs)
   355  		input     = ""                // Current user input
   356  		scheduler = make(chan string) // Channel to send the next prompt on and receive the input
   357  	)
   358  	// Start a goroutine to listen for promt requests and send back inputs
   359  	go func() {
   360  		for {
   361  			// Read the next user input
   362  			line, err := c.prompter.PromptInput(<-scheduler)
   363  			if err != nil {
   364  				// In case of an error, either clear the prompt or fail
   365  				if err == liner.ErrPromptAborted { // ctrl-C
   366  					prompt, indents, input = c.prompt, 0, ""
   367  					scheduler <- ""
   368  					continue
   369  				}
   370  				close(scheduler)
   371  				return
   372  			}
   373  			// User input retrieved, send for interpretation and loop
   374  			scheduler <- line
   375  		}
   376  	}()
   377  	// Monitor Ctrl-C too in case the input is empty and we need to bail
   378  	abort := make(chan os.Signal, 1)
   379  	signal.Notify(abort, syscall.SIGINT, syscall.SIGTERM)
   380  
   381  	// Start sending prompts to the user and reading back inputs
   382  	for {
   383  		// Send the next prompt, triggering an input read and process the result
   384  		scheduler <- prompt
   385  		select {
   386  		case <-abort:
   387  			// User forcefully quite the console
   388  			fmt.Fprintln(c.printer, "caught interrupt, exiting")
   389  			return
   390  
   391  		case line, ok := <-scheduler:
   392  			// User input was returned by the prompter, handle special cases
   393  			if !ok || (indents <= 0 && exit.MatchString(line)) {
   394  				return
   395  			}
   396  			if onlyWhitespace.MatchString(line) {
   397  				continue
   398  			}
   399  			// Append the line to the input and check for multi-line interpretation
   400  			input += line + "\n"
   401  
   402  			indents = countIndents(input)
   403  			if indents <= 0 {
   404  				prompt = c.prompt
   405  			} else {
   406  				prompt = strings.Repeat(".", indents*3) + " "
   407  			}
   408  			// If all the needed lines are present, save the command and run
   409  			if indents <= 0 {
   410  				if len(input) > 0 && input[0] != ' ' && !passwordRegexp.MatchString(input) {
   411  					if command := strings.TrimSpace(input); len(c.history) == 0 || command != c.history[len(c.history)-1] {
   412  						c.history = append(c.history, command)
   413  						if c.prompter != nil {
   414  							c.prompter.AppendHistory(command)
   415  						}
   416  					}
   417  				}
   418  				c.Evaluate(input)
   419  				input = ""
   420  			}
   421  		}
   422  	}
   423  }
   424  
   425  // countIndents returns the number of identations for the given input.
   426  // In case of invalid input such as var a = } the result can be negative.
   427  func countIndents(input string) int {
   428  	var (
   429  		indents     = 0
   430  		inString    = false
   431  		strOpenChar = ' '   // keep track of the string open char to allow var str = "I'm ....";
   432  		charEscaped = false // keep track if the previous char was the '\' char, allow var str = "abc\"def";
   433  	)
   434  
   435  	for _, c := range input {
   436  		switch c {
   437  		case '\\':
   438  			// indicate next char as escaped when in string and previous char isn't escaping this backslash
   439  			if !charEscaped && inString {
   440  				charEscaped = true
   441  			}
   442  		case '\'', '"':
   443  			if inString && !charEscaped && strOpenChar == c { // end string
   444  				inString = false
   445  			} else if !inString && !charEscaped { // begin string
   446  				inString = true
   447  				strOpenChar = c
   448  			}
   449  			charEscaped = false
   450  		case '{', '(':
   451  			if !inString { // ignore brackets when in string, allow var str = "a{"; without indenting
   452  				indents++
   453  			}
   454  			charEscaped = false
   455  		case '}', ')':
   456  			if !inString {
   457  				indents--
   458  			}
   459  			charEscaped = false
   460  		default:
   461  			charEscaped = false
   462  		}
   463  	}
   464  
   465  	return indents
   466  }
   467  
   468  // Execute runs the JavaScript file specified as the argument.
   469  func (c *Console) Execute(path string) error {
   470  	return c.jsre.Exec(path)
   471  }
   472  
   473  // Stop cleans up the console and terminates the runtime environment.
   474  func (c *Console) Stop(graceful bool) error {
   475  	if err := os.WriteFile(c.histPath, []byte(strings.Join(c.history, "\n")), 0o600); err != nil {
   476  		return err
   477  	}
   478  	if err := os.Chmod(c.histPath, 0o600); err != nil { // Force 0600, even if it was different previously
   479  		return err
   480  	}
   481  	c.jsre.Stop(graceful)
   482  	return nil
   483  }