github.1485827954.workers.dev/ethereum/go-ethereum@v1.14.3/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/ethereum/go-ethereum/console/prompt" 34 "github.com/ethereum/go-ethereum/internal/jsre" 35 "github.com/ethereum/go-ethereum/internal/jsre/deps" 36 "github.com/ethereum/go-ethereum/internal/web3ext" 37 "github.com/ethereum/go-ethereum/log" 38 "github.com/ethereum/go-ethereum/rpc" 39 "github.com/mattn/go-colorable" 40 "github.com/peterh/liner" 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{"eth": "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{}{"eth": {}} 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 eth.getBalance(eth.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 message += "at block: " + eth.blockNumber + " (" + new Date(1000 * eth.getBlock(eth.blockNumber).timestamp) + ")\n"; 329 try { 330 message += " datadir: " + admin.datadir + "\n"; 331 } catch (err) {} 332 message 333 `); err == nil { 334 message += res.String() 335 } 336 // List all the supported modules for the user to call 337 if apis, err := c.client.SupportedModules(); err == nil { 338 modules := make([]string, 0, len(apis)) 339 for api, version := range apis { 340 modules = append(modules, fmt.Sprintf("%s:%s", api, version)) 341 } 342 sort.Strings(modules) 343 message += " modules: " + strings.Join(modules, " ") + "\n" 344 } 345 message += "\nTo exit, press ctrl-d or type exit" 346 fmt.Fprintln(c.printer, message) 347 } 348 349 // Evaluate executes code and pretty prints the result to the specified output 350 // stream. 351 func (c *Console) Evaluate(statement string) { 352 defer func() { 353 if r := recover(); r != nil { 354 fmt.Fprintf(c.printer, "[native] error: %v\n", r) 355 } 356 }() 357 c.jsre.Evaluate(statement, c.printer) 358 359 // Avoid exiting Interactive when jsre was interrupted by SIGINT. 360 c.clearSignalReceived() 361 } 362 363 // interruptHandler runs in its own goroutine and waits for signals. 364 // When a signal is received, it interrupts the JS interpreter. 365 func (c *Console) interruptHandler() { 366 defer c.wg.Done() 367 368 // During Interactive, liner inhibits the signal while it is prompting for 369 // input. However, the signal will be received while evaluating JS. 370 // 371 // On unsupported terminals, SIGINT can also happen while prompting. 372 // Unfortunately, it is not possible to abort the prompt in this case and 373 // the c.readLines goroutine leaks. 374 sig := make(chan os.Signal, 1) 375 signal.Notify(sig, syscall.SIGINT) 376 defer signal.Stop(sig) 377 378 for { 379 select { 380 case <-sig: 381 c.setSignalReceived() 382 c.jsre.Interrupt(errors.New("interrupted")) 383 case <-c.stopInteractiveCh: 384 close(c.interactiveStopped) 385 c.jsre.Interrupt(errors.New("interrupted")) 386 case <-c.stopped: 387 return 388 } 389 } 390 } 391 392 func (c *Console) setSignalReceived() { 393 select { 394 case c.signalReceived <- struct{}{}: 395 default: 396 } 397 } 398 399 func (c *Console) clearSignalReceived() { 400 select { 401 case <-c.signalReceived: 402 default: 403 } 404 } 405 406 // StopInteractive causes Interactive to return as soon as possible. 407 func (c *Console) StopInteractive() { 408 select { 409 case c.stopInteractiveCh <- struct{}{}: 410 case <-c.stopped: 411 } 412 } 413 414 // Interactive starts an interactive user session, where input is prompted from 415 // the configured user prompter. 416 func (c *Console) Interactive() { 417 var ( 418 prompt = c.prompt // the current prompt line (used for multi-line inputs) 419 indents = 0 // the current number of input indents (used for multi-line inputs) 420 input = "" // the current user input 421 inputLine = make(chan string, 1) // receives user input 422 inputErr = make(chan error, 1) // receives liner errors 423 requestLine = make(chan string) // requests a line of input 424 ) 425 426 defer func() { 427 c.writeHistory() 428 }() 429 430 // The line reader runs in a separate goroutine. 431 go c.readLines(inputLine, inputErr, requestLine) 432 defer close(requestLine) 433 434 for { 435 // Send the next prompt, triggering an input read. 436 requestLine <- prompt 437 438 select { 439 case <-c.interactiveStopped: 440 fmt.Fprintln(c.printer, "node is down, exiting console") 441 return 442 443 case <-c.signalReceived: 444 // SIGINT received while prompting for input -> unsupported terminal. 445 // I'm not sure if the best choice would be to leave the console running here. 446 // Bash keeps running in this case. node.js does not. 447 fmt.Fprintln(c.printer, "caught interrupt, exiting") 448 return 449 450 case err := <-inputErr: 451 if err == liner.ErrPromptAborted { 452 // When prompting for multi-line input, the first Ctrl-C resets 453 // the multi-line state. 454 prompt, indents, input = c.prompt, 0, "" 455 continue 456 } 457 return 458 459 case line := <-inputLine: 460 // User input was returned by the prompter, handle special cases. 461 if indents <= 0 && exit.MatchString(line) { 462 return 463 } 464 if onlyWhitespace.MatchString(line) { 465 continue 466 } 467 // Append the line to the input and check for multi-line interpretation. 468 input += line + "\n" 469 indents = countIndents(input) 470 if indents <= 0 { 471 prompt = c.prompt 472 } else { 473 prompt = strings.Repeat(".", indents*3) + " " 474 } 475 // If all the needed lines are present, save the command and run it. 476 if indents <= 0 { 477 if len(input) > 0 && input[0] != ' ' && !passwordRegexp.MatchString(input) { 478 if command := strings.TrimSpace(input); len(c.history) == 0 || command != c.history[len(c.history)-1] { 479 c.history = append(c.history, command) 480 if c.prompter != nil { 481 c.prompter.AppendHistory(command) 482 } 483 } 484 } 485 c.Evaluate(input) 486 input = "" 487 } 488 } 489 } 490 } 491 492 // readLines runs in its own goroutine, prompting for input. 493 func (c *Console) readLines(input chan<- string, errc chan<- error, prompt <-chan string) { 494 for p := range prompt { 495 line, err := c.prompter.PromptInput(p) 496 if err != nil { 497 errc <- err 498 } else { 499 input <- line 500 } 501 } 502 } 503 504 // countIndents returns the number of indentations for the given input. 505 // In case of invalid input such as var a = } the result can be negative. 506 func countIndents(input string) int { 507 var ( 508 indents = 0 509 inString = false 510 strOpenChar = ' ' // keep track of the string open char to allow var str = "I'm ...."; 511 charEscaped = false // keep track if the previous char was the '\' char, allow var str = "abc\"def"; 512 ) 513 514 for _, c := range input { 515 switch c { 516 case '\\': 517 // indicate next char as escaped when in string and previous char isn't escaping this backslash 518 if !charEscaped && inString { 519 charEscaped = true 520 } 521 case '\'', '"': 522 if inString && !charEscaped && strOpenChar == c { // end string 523 inString = false 524 } else if !inString && !charEscaped { // begin string 525 inString = true 526 strOpenChar = c 527 } 528 charEscaped = false 529 case '{', '(': 530 if !inString { // ignore brackets when in string, allow var str = "a{"; without indenting 531 indents++ 532 } 533 charEscaped = false 534 case '}', ')': 535 if !inString { 536 indents-- 537 } 538 charEscaped = false 539 default: 540 charEscaped = false 541 } 542 } 543 544 return indents 545 } 546 547 // Stop cleans up the console and terminates the runtime environment. 548 func (c *Console) Stop(graceful bool) error { 549 c.stopOnce.Do(func() { 550 // Stop the interrupt handler. 551 close(c.stopped) 552 c.wg.Wait() 553 }) 554 555 c.jsre.Stop(graceful) 556 return nil 557 } 558 559 func (c *Console) writeHistory() error { 560 if err := os.WriteFile(c.histPath, []byte(strings.Join(c.history, "\n")), 0600); err != nil { 561 return err 562 } 563 return os.Chmod(c.histPath, 0600) // Force 0600, even if it was different previously 564 }