github.com/bcnmy/go-ethereum@v1.10.27/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/rpc" 38 "github.com/mattn/go-colorable" 39 "github.com/peterh/liner" 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 Ethereum 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 Ethereum 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 interactiveStopped chan struct{} 80 stopInteractiveCh chan struct{} 81 signalReceived chan struct{} 82 stopped chan struct{} 83 wg sync.WaitGroup 84 stopOnce sync.Once 85 } 86 87 // New initializes a JavaScript interpreted runtime environment and sets defaults 88 // with the config struct. 89 func New(config Config) (*Console, error) { 90 // Handle unset config values gracefully 91 if config.Prompter == nil { 92 config.Prompter = prompt.Stdin 93 } 94 if config.Prompt == "" { 95 config.Prompt = DefaultPrompt 96 } 97 if config.Printer == nil { 98 config.Printer = colorable.NewColorableStdout() 99 } 100 101 // Initialize the console and return 102 console := &Console{ 103 client: config.Client, 104 jsre: jsre.New(config.DocRoot, config.Printer), 105 prompt: config.Prompt, 106 prompter: config.Prompter, 107 printer: config.Printer, 108 histPath: filepath.Join(config.DataDir, HistoryFile), 109 interactiveStopped: make(chan struct{}), 110 stopInteractiveCh: make(chan struct{}), 111 signalReceived: make(chan struct{}, 1), 112 stopped: make(chan struct{}), 113 } 114 if err := os.MkdirAll(config.DataDir, 0700); err != nil { 115 return nil, err 116 } 117 if err := console.init(config.Preload); err != nil { 118 return nil, err 119 } 120 121 console.wg.Add(1) 122 go console.interruptHandler() 123 124 return console, nil 125 } 126 127 // init retrieves the available APIs from the remote RPC provider and initializes 128 // the console's JavaScript namespaces based on the exposed modules. 129 func (c *Console) init(preload []string) error { 130 c.initConsoleObject() 131 132 // Initialize the JavaScript <-> Go RPC bridge. 133 bridge := newBridge(c.client, c.prompter, c.printer) 134 if err := c.initWeb3(bridge); err != nil { 135 return err 136 } 137 if err := c.initExtensions(); err != nil { 138 return err 139 } 140 141 // Add bridge overrides for web3.js functionality. 142 c.jsre.Do(func(vm *goja.Runtime) { 143 c.initAdmin(vm, bridge) 144 c.initPersonal(vm, bridge) 145 }) 146 147 // Preload JavaScript files. 148 for _, path := range preload { 149 if err := c.jsre.Exec(path); err != nil { 150 failure := err.Error() 151 if gojaErr, ok := err.(*goja.Exception); ok { 152 failure = gojaErr.String() 153 } 154 return fmt.Errorf("%s: %v", path, failure) 155 } 156 } 157 158 // Configure the input prompter for history and tab completion. 159 if c.prompter != nil { 160 if content, err := os.ReadFile(c.histPath); err != nil { 161 c.prompter.SetHistory(nil) 162 } else { 163 c.history = strings.Split(string(content), "\n") 164 c.prompter.SetHistory(c.history) 165 } 166 c.prompter.SetWordCompleter(c.AutoCompleteInput) 167 } 168 return nil 169 } 170 171 func (c *Console) initConsoleObject() { 172 c.jsre.Do(func(vm *goja.Runtime) { 173 console := vm.NewObject() 174 console.Set("log", c.consoleOutput) 175 console.Set("error", c.consoleOutput) 176 vm.Set("console", console) 177 }) 178 } 179 180 func (c *Console) initWeb3(bridge *bridge) error { 181 if err := c.jsre.Compile("bignumber.js", deps.BigNumberJS); err != nil { 182 return fmt.Errorf("bignumber.js: %v", err) 183 } 184 if err := c.jsre.Compile("web3.js", deps.Web3JS); err != nil { 185 return fmt.Errorf("web3.js: %v", err) 186 } 187 if _, err := c.jsre.Run("var Web3 = require('web3');"); err != nil { 188 return fmt.Errorf("web3 require: %v", err) 189 } 190 var err error 191 c.jsre.Do(func(vm *goja.Runtime) { 192 transport := vm.NewObject() 193 transport.Set("send", jsre.MakeCallback(vm, bridge.Send)) 194 transport.Set("sendAsync", jsre.MakeCallback(vm, bridge.Send)) 195 vm.Set("_consoleWeb3Transport", transport) 196 _, err = vm.RunString("var web3 = new Web3(_consoleWeb3Transport)") 197 }) 198 return err 199 } 200 201 // initExtensions loads and registers web3.js extensions. 202 func (c *Console) initExtensions() error { 203 // Compute aliases from server-provided modules. 204 apis, err := c.client.SupportedModules() 205 if err != nil { 206 return fmt.Errorf("api modules: %v", err) 207 } 208 aliases := map[string]struct{}{"eth": {}, "personal": {}} 209 for api := range apis { 210 if api == "web3" { 211 continue 212 } 213 aliases[api] = struct{}{} 214 if file, ok := web3ext.Modules[api]; ok { 215 if err = c.jsre.Compile(api+".js", file); err != nil { 216 return fmt.Errorf("%s.js: %v", api, err) 217 } 218 } 219 } 220 221 // Apply aliases. 222 c.jsre.Do(func(vm *goja.Runtime) { 223 web3 := getObject(vm, "web3") 224 for name := range aliases { 225 if v := web3.Get(name); v != nil { 226 vm.Set(name, v) 227 } 228 } 229 }) 230 return nil 231 } 232 233 // initAdmin creates additional admin APIs implemented by the bridge. 234 func (c *Console) initAdmin(vm *goja.Runtime, bridge *bridge) { 235 if admin := getObject(vm, "admin"); admin != nil { 236 admin.Set("sleepBlocks", jsre.MakeCallback(vm, bridge.SleepBlocks)) 237 admin.Set("sleep", jsre.MakeCallback(vm, bridge.Sleep)) 238 admin.Set("clearHistory", c.clearHistory) 239 } 240 } 241 242 // initPersonal redirects account-related API methods through the bridge. 243 // 244 // If the console is in interactive mode and the 'personal' API is available, override 245 // the openWallet, unlockAccount, newAccount and sign methods since these require user 246 // interaction. The original web3 callbacks are stored in 'jeth'. These will be called 247 // by the bridge after the prompt and send the original web3 request to the backend. 248 func (c *Console) initPersonal(vm *goja.Runtime, bridge *bridge) { 249 personal := getObject(vm, "personal") 250 if personal == nil || c.prompter == nil { 251 return 252 } 253 jeth := vm.NewObject() 254 vm.Set("jeth", jeth) 255 jeth.Set("openWallet", personal.Get("openWallet")) 256 jeth.Set("unlockAccount", personal.Get("unlockAccount")) 257 jeth.Set("newAccount", personal.Get("newAccount")) 258 jeth.Set("sign", personal.Get("sign")) 259 personal.Set("openWallet", jsre.MakeCallback(vm, bridge.OpenWallet)) 260 personal.Set("unlockAccount", jsre.MakeCallback(vm, bridge.UnlockAccount)) 261 personal.Set("newAccount", jsre.MakeCallback(vm, bridge.NewAccount)) 262 personal.Set("sign", jsre.MakeCallback(vm, bridge.Sign)) 263 } 264 265 func (c *Console) clearHistory() { 266 c.history = nil 267 c.prompter.ClearHistory() 268 if err := os.Remove(c.histPath); err != nil { 269 fmt.Fprintln(c.printer, "can't delete history file:", err) 270 } else { 271 fmt.Fprintln(c.printer, "history file deleted") 272 } 273 } 274 275 // consoleOutput is an override for the console.log and console.error methods to 276 // stream the output into the configured output stream instead of stdout. 277 func (c *Console) consoleOutput(call goja.FunctionCall) goja.Value { 278 var output []string 279 for _, argument := range call.Arguments { 280 output = append(output, fmt.Sprintf("%v", argument)) 281 } 282 fmt.Fprintln(c.printer, strings.Join(output, " ")) 283 return goja.Null() 284 } 285 286 // AutoCompleteInput is a pre-assembled word completer to be used by the user 287 // input prompter to provide hints to the user about the methods available. 288 func (c *Console) AutoCompleteInput(line string, pos int) (string, []string, string) { 289 // No completions can be provided for empty inputs 290 if len(line) == 0 || pos == 0 { 291 return "", nil, "" 292 } 293 // Chunk data to relevant part for autocompletion 294 // E.g. in case of nested lines eth.getBalance(eth.coinb<tab><tab> 295 start := pos - 1 296 for ; start > 0; start-- { 297 // Skip all methods and namespaces (i.e. including the dot) 298 if line[start] == '.' || (line[start] >= 'a' && line[start] <= 'z') || (line[start] >= 'A' && line[start] <= 'Z') { 299 continue 300 } 301 // Handle web3 in a special way (i.e. other numbers aren't auto completed) 302 if start >= 3 && line[start-3:start] == "web3" { 303 start -= 3 304 continue 305 } 306 // We've hit an unexpected character, autocomplete form here 307 start++ 308 break 309 } 310 return line[:start], c.jsre.CompleteKeywords(line[start:pos]), line[pos:] 311 } 312 313 // Welcome show summary of current Geth instance and some metadata about the 314 // console's available modules. 315 func (c *Console) Welcome() { 316 message := "Welcome to the Geth JavaScript console!\n\n" 317 318 // Print some generic Geth metadata 319 if res, err := c.jsre.Run(` 320 var message = "instance: " + web3.version.node + "\n"; 321 try { 322 message += "coinbase: " + eth.coinbase + "\n"; 323 } catch (err) {} 324 message += "at block: " + eth.blockNumber + " (" + new Date(1000 * eth.getBlock(eth.blockNumber).timestamp) + ")\n"; 325 try { 326 message += " datadir: " + admin.datadir + "\n"; 327 } catch (err) {} 328 message 329 `); err == nil { 330 message += res.String() 331 } 332 // List all the supported modules for the user to call 333 if apis, err := c.client.SupportedModules(); err == nil { 334 modules := make([]string, 0, len(apis)) 335 for api, version := range apis { 336 modules = append(modules, fmt.Sprintf("%s:%s", api, version)) 337 } 338 sort.Strings(modules) 339 message += " modules: " + strings.Join(modules, " ") + "\n" 340 } 341 message += "\nTo exit, press ctrl-d or type exit" 342 fmt.Fprintln(c.printer, message) 343 } 344 345 // Evaluate executes code and pretty prints the result to the specified output 346 // stream. 347 func (c *Console) Evaluate(statement string) { 348 defer func() { 349 if r := recover(); r != nil { 350 fmt.Fprintf(c.printer, "[native] error: %v\n", r) 351 } 352 }() 353 c.jsre.Evaluate(statement, c.printer) 354 355 // Avoid exiting Interactive when jsre was interrupted by SIGINT. 356 c.clearSignalReceived() 357 } 358 359 // interruptHandler runs in its own goroutine and waits for signals. 360 // When a signal is received, it interrupts the JS interpreter. 361 func (c *Console) interruptHandler() { 362 defer c.wg.Done() 363 364 // During Interactive, liner inhibits the signal while it is prompting for 365 // input. However, the signal will be received while evaluating JS. 366 // 367 // On unsupported terminals, SIGINT can also happen while prompting. 368 // Unfortunately, it is not possible to abort the prompt in this case and 369 // the c.readLines goroutine leaks. 370 sig := make(chan os.Signal, 1) 371 signal.Notify(sig, syscall.SIGINT) 372 defer signal.Stop(sig) 373 374 for { 375 select { 376 case <-sig: 377 c.setSignalReceived() 378 c.jsre.Interrupt(errors.New("interrupted")) 379 case <-c.stopInteractiveCh: 380 close(c.interactiveStopped) 381 c.jsre.Interrupt(errors.New("interrupted")) 382 case <-c.stopped: 383 return 384 } 385 } 386 } 387 388 func (c *Console) setSignalReceived() { 389 select { 390 case c.signalReceived <- struct{}{}: 391 default: 392 } 393 } 394 395 func (c *Console) clearSignalReceived() { 396 select { 397 case <-c.signalReceived: 398 default: 399 } 400 } 401 402 // StopInteractive causes Interactive to return as soon as possible. 403 func (c *Console) StopInteractive() { 404 select { 405 case c.stopInteractiveCh <- struct{}{}: 406 case <-c.stopped: 407 } 408 } 409 410 // Interactive starts an interactive user session, where input is prompted from 411 // the configured user prompter. 412 func (c *Console) Interactive() { 413 var ( 414 prompt = c.prompt // the current prompt line (used for multi-line inputs) 415 indents = 0 // the current number of input indents (used for multi-line inputs) 416 input = "" // the current user input 417 inputLine = make(chan string, 1) // receives user input 418 inputErr = make(chan error, 1) // receives liner errors 419 requestLine = make(chan string) // requests a line of input 420 ) 421 422 defer func() { 423 c.writeHistory() 424 }() 425 426 // The line reader runs in a separate goroutine. 427 go c.readLines(inputLine, inputErr, requestLine) 428 defer close(requestLine) 429 430 for { 431 // Send the next prompt, triggering an input read. 432 requestLine <- prompt 433 434 select { 435 case <-c.interactiveStopped: 436 fmt.Fprintln(c.printer, "node is down, exiting console") 437 return 438 439 case <-c.signalReceived: 440 // SIGINT received while prompting for input -> unsupported terminal. 441 // I'm not sure if the best choice would be to leave the console running here. 442 // Bash keeps running in this case. node.js does not. 443 fmt.Fprintln(c.printer, "caught interrupt, exiting") 444 return 445 446 case err := <-inputErr: 447 if err == liner.ErrPromptAborted { 448 // When prompting for multi-line input, the first Ctrl-C resets 449 // the multi-line state. 450 prompt, indents, input = c.prompt, 0, "" 451 continue 452 } 453 return 454 455 case line := <-inputLine: 456 // User input was returned by the prompter, handle special cases. 457 if indents <= 0 && exit.MatchString(line) { 458 return 459 } 460 if onlyWhitespace.MatchString(line) { 461 continue 462 } 463 // Append the line to the input and check for multi-line interpretation. 464 input += line + "\n" 465 indents = countIndents(input) 466 if indents <= 0 { 467 prompt = c.prompt 468 } else { 469 prompt = strings.Repeat(".", indents*3) + " " 470 } 471 // If all the needed lines are present, save the command and run it. 472 if indents <= 0 { 473 if len(input) > 0 && input[0] != ' ' && !passwordRegexp.MatchString(input) { 474 if command := strings.TrimSpace(input); len(c.history) == 0 || command != c.history[len(c.history)-1] { 475 c.history = append(c.history, command) 476 if c.prompter != nil { 477 c.prompter.AppendHistory(command) 478 } 479 } 480 } 481 c.Evaluate(input) 482 input = "" 483 } 484 } 485 } 486 } 487 488 // readLines runs in its own goroutine, prompting for input. 489 func (c *Console) readLines(input chan<- string, errc chan<- error, prompt <-chan string) { 490 for p := range prompt { 491 line, err := c.prompter.PromptInput(p) 492 if err != nil { 493 errc <- err 494 } else { 495 input <- line 496 } 497 } 498 } 499 500 // countIndents returns the number of indentations for the given input. 501 // In case of invalid input such as var a = } the result can be negative. 502 func countIndents(input string) int { 503 var ( 504 indents = 0 505 inString = false 506 strOpenChar = ' ' // keep track of the string open char to allow var str = "I'm ...."; 507 charEscaped = false // keep track if the previous char was the '\' char, allow var str = "abc\"def"; 508 ) 509 510 for _, c := range input { 511 switch c { 512 case '\\': 513 // indicate next char as escaped when in string and previous char isn't escaping this backslash 514 if !charEscaped && inString { 515 charEscaped = true 516 } 517 case '\'', '"': 518 if inString && !charEscaped && strOpenChar == c { // end string 519 inString = false 520 } else if !inString && !charEscaped { // begin string 521 inString = true 522 strOpenChar = c 523 } 524 charEscaped = false 525 case '{', '(': 526 if !inString { // ignore brackets when in string, allow var str = "a{"; without indenting 527 indents++ 528 } 529 charEscaped = false 530 case '}', ')': 531 if !inString { 532 indents-- 533 } 534 charEscaped = false 535 default: 536 charEscaped = false 537 } 538 } 539 540 return indents 541 } 542 543 // Stop cleans up the console and terminates the runtime environment. 544 func (c *Console) Stop(graceful bool) error { 545 c.stopOnce.Do(func() { 546 // Stop the interrupt handler. 547 close(c.stopped) 548 c.wg.Wait() 549 }) 550 551 c.jsre.Stop(graceful) 552 return nil 553 } 554 555 func (c *Console) writeHistory() error { 556 if err := os.WriteFile(c.histPath, []byte(strings.Join(c.history, "\n")), 0600); err != nil { 557 return err 558 } 559 return os.Chmod(c.histPath, 0600) // Force 0600, even if it was different previously 560 }