github.com/tirogen/go-ethereum@v1.10.12-0.20221226051715-250cfede41b6/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/mattn/go-colorable" 34 "github.com/peterh/liner" 35 "github.com/tirogen/go-ethereum/console/prompt" 36 "github.com/tirogen/go-ethereum/internal/jsre" 37 "github.com/tirogen/go-ethereum/internal/jsre/deps" 38 "github.com/tirogen/go-ethereum/internal/web3ext" 39 "github.com/tirogen/go-ethereum/log" 40 "github.com/tirogen/go-ethereum/rpc" 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": {}, "personal": {}} 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 jeth := vm.NewObject() 264 vm.Set("jeth", jeth) 265 jeth.Set("openWallet", personal.Get("openWallet")) 266 jeth.Set("unlockAccount", personal.Get("unlockAccount")) 267 jeth.Set("newAccount", personal.Get("newAccount")) 268 jeth.Set("sign", personal.Get("sign")) 269 personal.Set("openWallet", jsre.MakeCallback(vm, bridge.OpenWallet)) 270 personal.Set("unlockAccount", jsre.MakeCallback(vm, bridge.UnlockAccount)) 271 personal.Set("newAccount", jsre.MakeCallback(vm, bridge.NewAccount)) 272 personal.Set("sign", jsre.MakeCallback(vm, bridge.Sign)) 273 } 274 275 func (c *Console) clearHistory() { 276 c.history = nil 277 c.prompter.ClearHistory() 278 if err := os.Remove(c.histPath); err != nil { 279 fmt.Fprintln(c.printer, "can't delete history file:", err) 280 } else { 281 fmt.Fprintln(c.printer, "history file deleted") 282 } 283 } 284 285 // consoleOutput is an override for the console.log and console.error methods to 286 // stream the output into the configured output stream instead of stdout. 287 func (c *Console) consoleOutput(call goja.FunctionCall) goja.Value { 288 var output []string 289 for _, argument := range call.Arguments { 290 output = append(output, fmt.Sprintf("%v", argument)) 291 } 292 fmt.Fprintln(c.printer, strings.Join(output, " ")) 293 return goja.Null() 294 } 295 296 // AutoCompleteInput is a pre-assembled word completer to be used by the user 297 // input prompter to provide hints to the user about the methods available. 298 func (c *Console) AutoCompleteInput(line string, pos int) (string, []string, string) { 299 // No completions can be provided for empty inputs 300 if len(line) == 0 || pos == 0 { 301 return "", nil, "" 302 } 303 // Chunk data to relevant part for autocompletion 304 // E.g. in case of nested lines eth.getBalance(eth.coinb<tab><tab> 305 start := pos - 1 306 for ; start > 0; start-- { 307 // Skip all methods and namespaces (i.e. including the dot) 308 if line[start] == '.' || (line[start] >= 'a' && line[start] <= 'z') || (line[start] >= 'A' && line[start] <= 'Z') { 309 continue 310 } 311 // Handle web3 in a special way (i.e. other numbers aren't auto completed) 312 if start >= 3 && line[start-3:start] == "web3" { 313 start -= 3 314 continue 315 } 316 // We've hit an unexpected character, autocomplete form here 317 start++ 318 break 319 } 320 return line[:start], c.jsre.CompleteKeywords(line[start:pos]), line[pos:] 321 } 322 323 // Welcome show summary of current Geth instance and some metadata about the 324 // console's available modules. 325 func (c *Console) Welcome() { 326 message := "Welcome to the Geth JavaScript console!\n\n" 327 328 // Print some generic Geth metadata 329 if res, err := c.jsre.Run(` 330 var message = "instance: " + web3.version.node + "\n"; 331 try { 332 message += "coinbase: " + eth.coinbase + "\n"; 333 } catch (err) {} 334 message += "at block: " + eth.blockNumber + " (" + new Date(1000 * eth.getBlock(eth.blockNumber).timestamp) + ")\n"; 335 try { 336 message += " datadir: " + admin.datadir + "\n"; 337 } catch (err) {} 338 message 339 `); err == nil { 340 message += res.String() 341 } 342 // List all the supported modules for the user to call 343 if apis, err := c.client.SupportedModules(); err == nil { 344 modules := make([]string, 0, len(apis)) 345 for api, version := range apis { 346 modules = append(modules, fmt.Sprintf("%s:%s", api, version)) 347 } 348 sort.Strings(modules) 349 message += " modules: " + strings.Join(modules, " ") + "\n" 350 } 351 message += "\nTo exit, press ctrl-d or type exit" 352 fmt.Fprintln(c.printer, message) 353 } 354 355 // Evaluate executes code and pretty prints the result to the specified output 356 // stream. 357 func (c *Console) Evaluate(statement string) { 358 defer func() { 359 if r := recover(); r != nil { 360 fmt.Fprintf(c.printer, "[native] error: %v\n", r) 361 } 362 }() 363 c.jsre.Evaluate(statement, c.printer) 364 365 // Avoid exiting Interactive when jsre was interrupted by SIGINT. 366 c.clearSignalReceived() 367 } 368 369 // interruptHandler runs in its own goroutine and waits for signals. 370 // When a signal is received, it interrupts the JS interpreter. 371 func (c *Console) interruptHandler() { 372 defer c.wg.Done() 373 374 // During Interactive, liner inhibits the signal while it is prompting for 375 // input. However, the signal will be received while evaluating JS. 376 // 377 // On unsupported terminals, SIGINT can also happen while prompting. 378 // Unfortunately, it is not possible to abort the prompt in this case and 379 // the c.readLines goroutine leaks. 380 sig := make(chan os.Signal, 1) 381 signal.Notify(sig, syscall.SIGINT) 382 defer signal.Stop(sig) 383 384 for { 385 select { 386 case <-sig: 387 c.setSignalReceived() 388 c.jsre.Interrupt(errors.New("interrupted")) 389 case <-c.stopInteractiveCh: 390 close(c.interactiveStopped) 391 c.jsre.Interrupt(errors.New("interrupted")) 392 case <-c.stopped: 393 return 394 } 395 } 396 } 397 398 func (c *Console) setSignalReceived() { 399 select { 400 case c.signalReceived <- struct{}{}: 401 default: 402 } 403 } 404 405 func (c *Console) clearSignalReceived() { 406 select { 407 case <-c.signalReceived: 408 default: 409 } 410 } 411 412 // StopInteractive causes Interactive to return as soon as possible. 413 func (c *Console) StopInteractive() { 414 select { 415 case c.stopInteractiveCh <- struct{}{}: 416 case <-c.stopped: 417 } 418 } 419 420 // Interactive starts an interactive user session, where input is prompted from 421 // the configured user prompter. 422 func (c *Console) Interactive() { 423 var ( 424 prompt = c.prompt // the current prompt line (used for multi-line inputs) 425 indents = 0 // the current number of input indents (used for multi-line inputs) 426 input = "" // the current user input 427 inputLine = make(chan string, 1) // receives user input 428 inputErr = make(chan error, 1) // receives liner errors 429 requestLine = make(chan string) // requests a line of input 430 ) 431 432 defer func() { 433 c.writeHistory() 434 }() 435 436 // The line reader runs in a separate goroutine. 437 go c.readLines(inputLine, inputErr, requestLine) 438 defer close(requestLine) 439 440 for { 441 // Send the next prompt, triggering an input read. 442 requestLine <- prompt 443 444 select { 445 case <-c.interactiveStopped: 446 fmt.Fprintln(c.printer, "node is down, exiting console") 447 return 448 449 case <-c.signalReceived: 450 // SIGINT received while prompting for input -> unsupported terminal. 451 // I'm not sure if the best choice would be to leave the console running here. 452 // Bash keeps running in this case. node.js does not. 453 fmt.Fprintln(c.printer, "caught interrupt, exiting") 454 return 455 456 case err := <-inputErr: 457 if err == liner.ErrPromptAborted { 458 // When prompting for multi-line input, the first Ctrl-C resets 459 // the multi-line state. 460 prompt, indents, input = c.prompt, 0, "" 461 continue 462 } 463 return 464 465 case line := <-inputLine: 466 // User input was returned by the prompter, handle special cases. 467 if indents <= 0 && exit.MatchString(line) { 468 return 469 } 470 if onlyWhitespace.MatchString(line) { 471 continue 472 } 473 // Append the line to the input and check for multi-line interpretation. 474 input += line + "\n" 475 indents = countIndents(input) 476 if indents <= 0 { 477 prompt = c.prompt 478 } else { 479 prompt = strings.Repeat(".", indents*3) + " " 480 } 481 // If all the needed lines are present, save the command and run it. 482 if indents <= 0 { 483 if len(input) > 0 && input[0] != ' ' && !passwordRegexp.MatchString(input) { 484 if command := strings.TrimSpace(input); len(c.history) == 0 || command != c.history[len(c.history)-1] { 485 c.history = append(c.history, command) 486 if c.prompter != nil { 487 c.prompter.AppendHistory(command) 488 } 489 } 490 } 491 c.Evaluate(input) 492 input = "" 493 } 494 } 495 } 496 } 497 498 // readLines runs in its own goroutine, prompting for input. 499 func (c *Console) readLines(input chan<- string, errc chan<- error, prompt <-chan string) { 500 for p := range prompt { 501 line, err := c.prompter.PromptInput(p) 502 if err != nil { 503 errc <- err 504 } else { 505 input <- line 506 } 507 } 508 } 509 510 // countIndents returns the number of indentations for the given input. 511 // In case of invalid input such as var a = } the result can be negative. 512 func countIndents(input string) int { 513 var ( 514 indents = 0 515 inString = false 516 strOpenChar = ' ' // keep track of the string open char to allow var str = "I'm ...."; 517 charEscaped = false // keep track if the previous char was the '\' char, allow var str = "abc\"def"; 518 ) 519 520 for _, c := range input { 521 switch c { 522 case '\\': 523 // indicate next char as escaped when in string and previous char isn't escaping this backslash 524 if !charEscaped && inString { 525 charEscaped = true 526 } 527 case '\'', '"': 528 if inString && !charEscaped && strOpenChar == c { // end string 529 inString = false 530 } else if !inString && !charEscaped { // begin string 531 inString = true 532 strOpenChar = c 533 } 534 charEscaped = false 535 case '{', '(': 536 if !inString { // ignore brackets when in string, allow var str = "a{"; without indenting 537 indents++ 538 } 539 charEscaped = false 540 case '}', ')': 541 if !inString { 542 indents-- 543 } 544 charEscaped = false 545 default: 546 charEscaped = false 547 } 548 } 549 550 return indents 551 } 552 553 // Stop cleans up the console and terminates the runtime environment. 554 func (c *Console) Stop(graceful bool) error { 555 c.stopOnce.Do(func() { 556 // Stop the interrupt handler. 557 close(c.stopped) 558 c.wg.Wait() 559 }) 560 561 c.jsre.Stop(graceful) 562 return nil 563 } 564 565 func (c *Console) writeHistory() error { 566 if err := os.WriteFile(c.histPath, []byte(strings.Join(c.history, "\n")), 0600); err != nil { 567 return err 568 } 569 return os.Chmod(c.histPath, 0600) // Force 0600, even if it was different previously 570 }