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