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