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