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