gitlab.com/yannislg/go-pulse@v0.0.0-20210722055913-a3e24e95638d/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 "fmt" 21 "io" 22 "io/ioutil" 23 "os" 24 "os/signal" 25 "path/filepath" 26 "regexp" 27 "sort" 28 "strings" 29 "syscall" 30 31 "github.com/dop251/goja" 32 "github.com/ethereum/go-ethereum/internal/jsre" 33 "github.com/ethereum/go-ethereum/internal/jsre/deps" 34 "github.com/ethereum/go-ethereum/internal/web3ext" 35 "github.com/ethereum/go-ethereum/rpc" 36 "github.com/mattn/go-colorable" 37 "github.com/peterh/liner" 38 ) 39 40 var ( 41 passwordRegexp = regexp.MustCompile(`personal.[nus]`) 42 onlyWhitespace = regexp.MustCompile(`^\s*$`) 43 exit = regexp.MustCompile(`^\s*exit\s*;*\s*$`) 44 ) 45 46 // HistoryFile is the file within the data directory to store input scrollback. 47 const HistoryFile = "history" 48 49 // DefaultPrompt is the default prompt line prefix to use for user input querying. 50 const DefaultPrompt = "> " 51 52 // Config is the collection of configurations to fine tune the behavior of the 53 // JavaScript console. 54 type Config struct { 55 DataDir string // Data directory to store the console history at 56 DocRoot string // Filesystem path from where to load JavaScript files from 57 Client *rpc.Client // RPC client to execute Ethereum requests through 58 Prompt string // Input prompt prefix string (defaults to DefaultPrompt) 59 Prompter UserPrompter // Input prompter to allow interactive user feedback (defaults to TerminalPrompter) 60 Printer io.Writer // Output writer to serialize any display strings to (defaults to os.Stdout) 61 Preload []string // Absolute paths to JavaScript files to preload 62 } 63 64 // Console is a JavaScript interpreted runtime environment. It is a fully fledged 65 // JavaScript console attached to a running node via an external or in-process RPC 66 // client. 67 type Console struct { 68 client *rpc.Client // RPC client to execute Ethereum requests through 69 jsre *jsre.JSRE // JavaScript runtime environment running the interpreter 70 prompt string // Input prompt prefix string 71 prompter UserPrompter // Input prompter to allow interactive user feedback 72 histPath string // Absolute path to the console scrollback history 73 history []string // Scroll history maintained by the console 74 printer io.Writer // Output writer to serialize any display strings to 75 } 76 77 // New initializes a JavaScript interpreted runtime environment and sets defaults 78 // with the config struct. 79 func New(config Config) (*Console, error) { 80 // Handle unset config values gracefully 81 if config.Prompter == nil { 82 config.Prompter = Stdin 83 } 84 if config.Prompt == "" { 85 config.Prompt = DefaultPrompt 86 } 87 if config.Printer == nil { 88 config.Printer = colorable.NewColorableStdout() 89 } 90 91 // Initialize the console and return 92 console := &Console{ 93 client: config.Client, 94 jsre: jsre.New(config.DocRoot, config.Printer), 95 prompt: config.Prompt, 96 prompter: config.Prompter, 97 printer: config.Printer, 98 histPath: filepath.Join(config.DataDir, HistoryFile), 99 } 100 if err := os.MkdirAll(config.DataDir, 0700); err != nil { 101 return nil, err 102 } 103 if err := console.init(config.Preload); err != nil { 104 return nil, err 105 } 106 return console, nil 107 } 108 109 // init retrieves the available APIs from the remote RPC provider and initializes 110 // the console's JavaScript namespaces based on the exposed modules. 111 func (c *Console) init(preload []string) error { 112 c.initConsoleObject() 113 114 // Initialize the JavaScript <-> Go RPC bridge. 115 bridge := newBridge(c.client, c.prompter, c.printer) 116 if err := c.initWeb3(bridge); err != nil { 117 return err 118 } 119 if err := c.initExtensions(); err != nil { 120 return err 121 } 122 123 // Add bridge overrides for web3.js functionality. 124 c.jsre.Do(func(vm *goja.Runtime) { 125 c.initAdmin(vm, bridge) 126 c.initPersonal(vm, bridge) 127 }) 128 129 // Preload JavaScript files. 130 for _, path := range preload { 131 if err := c.jsre.Exec(path); err != nil { 132 failure := err.Error() 133 if gojaErr, ok := err.(*goja.Exception); ok { 134 failure = gojaErr.String() 135 } 136 return fmt.Errorf("%s: %v", path, failure) 137 } 138 } 139 140 // Configure the input prompter for history and tab completion. 141 if c.prompter != nil { 142 if content, err := ioutil.ReadFile(c.histPath); err != nil { 143 c.prompter.SetHistory(nil) 144 } else { 145 c.history = strings.Split(string(content), "\n") 146 c.prompter.SetHistory(c.history) 147 } 148 c.prompter.SetWordCompleter(c.AutoCompleteInput) 149 } 150 return nil 151 } 152 153 func (c *Console) initConsoleObject() { 154 c.jsre.Do(func(vm *goja.Runtime) { 155 console := vm.NewObject() 156 console.Set("log", c.consoleOutput) 157 console.Set("error", c.consoleOutput) 158 vm.Set("console", console) 159 }) 160 } 161 162 func (c *Console) initWeb3(bridge *bridge) error { 163 bnJS := string(deps.MustAsset("bignumber.js")) 164 web3JS := string(deps.MustAsset("web3.js")) 165 if err := c.jsre.Compile("bignumber.js", bnJS); err != nil { 166 return fmt.Errorf("bignumber.js: %v", err) 167 } 168 if err := c.jsre.Compile("web3.js", web3JS); err != nil { 169 return fmt.Errorf("web3.js: %v", err) 170 } 171 if _, err := c.jsre.Run("var Web3 = require('web3');"); err != nil { 172 return fmt.Errorf("web3 require: %v", err) 173 } 174 var err error 175 c.jsre.Do(func(vm *goja.Runtime) { 176 transport := vm.NewObject() 177 transport.Set("send", jsre.MakeCallback(vm, bridge.Send)) 178 transport.Set("sendAsync", jsre.MakeCallback(vm, bridge.Send)) 179 vm.Set("_consoleWeb3Transport", transport) 180 _, err = vm.RunString("var web3 = new Web3(_consoleWeb3Transport)") 181 }) 182 return err 183 } 184 185 // initExtensions loads and registers web3.js extensions. 186 func (c *Console) initExtensions() error { 187 // Compute aliases from server-provided modules. 188 apis, err := c.client.SupportedModules() 189 if err != nil { 190 return fmt.Errorf("api modules: %v", err) 191 } 192 aliases := map[string]struct{}{"eth": {}, "personal": {}} 193 for api := range apis { 194 if api == "web3" { 195 continue 196 } 197 aliases[api] = struct{}{} 198 if file, ok := web3ext.Modules[api]; ok { 199 if err = c.jsre.Compile(api+".js", file); err != nil { 200 return fmt.Errorf("%s.js: %v", api, err) 201 } 202 } 203 } 204 205 // Apply aliases. 206 c.jsre.Do(func(vm *goja.Runtime) { 207 web3 := getObject(vm, "web3") 208 for name := range aliases { 209 if v := web3.Get(name); v != nil { 210 vm.Set(name, v) 211 } 212 } 213 }) 214 return nil 215 } 216 217 // initAdmin creates additional admin APIs implemented by the bridge. 218 func (c *Console) initAdmin(vm *goja.Runtime, bridge *bridge) { 219 if admin := getObject(vm, "admin"); admin != nil { 220 admin.Set("sleepBlocks", jsre.MakeCallback(vm, bridge.SleepBlocks)) 221 admin.Set("sleep", jsre.MakeCallback(vm, bridge.Sleep)) 222 admin.Set("clearHistory", c.clearHistory) 223 } 224 } 225 226 // initPersonal redirects account-related API methods through the bridge. 227 // 228 // If the console is in interactive mode and the 'personal' API is available, override 229 // the openWallet, unlockAccount, newAccount and sign methods since these require user 230 // interaction. The original web3 callbacks are stored in 'jeth'. These will be called 231 // by the bridge after the prompt and send the original web3 request to the backend. 232 func (c *Console) initPersonal(vm *goja.Runtime, bridge *bridge) { 233 personal := getObject(vm, "personal") 234 if personal == nil || c.prompter == nil { 235 return 236 } 237 jeth := vm.NewObject() 238 vm.Set("jeth", jeth) 239 jeth.Set("openWallet", personal.Get("openWallet")) 240 jeth.Set("unlockAccount", personal.Get("unlockAccount")) 241 jeth.Set("newAccount", personal.Get("newAccount")) 242 jeth.Set("sign", personal.Get("sign")) 243 personal.Set("openWallet", jsre.MakeCallback(vm, bridge.OpenWallet)) 244 personal.Set("unlockAccount", jsre.MakeCallback(vm, bridge.UnlockAccount)) 245 personal.Set("newAccount", jsre.MakeCallback(vm, bridge.NewAccount)) 246 personal.Set("sign", jsre.MakeCallback(vm, bridge.Sign)) 247 } 248 249 func (c *Console) clearHistory() { 250 c.history = nil 251 c.prompter.ClearHistory() 252 if err := os.Remove(c.histPath); err != nil { 253 fmt.Fprintln(c.printer, "can't delete history file:", err) 254 } else { 255 fmt.Fprintln(c.printer, "history file deleted") 256 } 257 } 258 259 // consoleOutput is an override for the console.log and console.error methods to 260 // stream the output into the configured output stream instead of stdout. 261 func (c *Console) consoleOutput(call goja.FunctionCall) goja.Value { 262 var output []string 263 for _, argument := range call.Arguments { 264 output = append(output, fmt.Sprintf("%v", argument)) 265 } 266 fmt.Fprintln(c.printer, strings.Join(output, " ")) 267 return goja.Null() 268 } 269 270 // AutoCompleteInput is a pre-assembled word completer to be used by the user 271 // input prompter to provide hints to the user about the methods available. 272 func (c *Console) AutoCompleteInput(line string, pos int) (string, []string, string) { 273 // No completions can be provided for empty inputs 274 if len(line) == 0 || pos == 0 { 275 return "", nil, "" 276 } 277 // Chunck data to relevant part for autocompletion 278 // E.g. in case of nested lines eth.getBalance(eth.coinb<tab><tab> 279 start := pos - 1 280 for ; start > 0; start-- { 281 // Skip all methods and namespaces (i.e. including the dot) 282 if line[start] == '.' || (line[start] >= 'a' && line[start] <= 'z') || (line[start] >= 'A' && line[start] <= 'Z') { 283 continue 284 } 285 // Handle web3 in a special way (i.e. other numbers aren't auto completed) 286 if start >= 3 && line[start-3:start] == "web3" { 287 start -= 3 288 continue 289 } 290 // We've hit an unexpected character, autocomplete form here 291 start++ 292 break 293 } 294 return line[:start], c.jsre.CompleteKeywords(line[start:pos]), line[pos:] 295 } 296 297 // Welcome show summary of current Geth instance and some metadata about the 298 // console's available modules. 299 func (c *Console) Welcome() { 300 message := "Welcome to the Geth JavaScript console!\n\n" 301 302 // Print some generic Geth metadata 303 if res, err := c.jsre.Run(` 304 var message = "instance: " + web3.version.node + "\n"; 305 try { 306 message += "coinbase: " + eth.coinbase + "\n"; 307 } catch (err) {} 308 message += "at block: " + eth.blockNumber + " (" + new Date(1000 * eth.getBlock(eth.blockNumber).timestamp) + ")\n"; 309 try { 310 message += " datadir: " + admin.datadir + "\n"; 311 } catch (err) {} 312 message 313 `); err == nil { 314 message += res.String() 315 } 316 // List all the supported modules for the user to call 317 if apis, err := c.client.SupportedModules(); err == nil { 318 modules := make([]string, 0, len(apis)) 319 for api, version := range apis { 320 modules = append(modules, fmt.Sprintf("%s:%s", api, version)) 321 } 322 sort.Strings(modules) 323 message += " modules: " + strings.Join(modules, " ") + "\n" 324 } 325 fmt.Fprintln(c.printer, message) 326 } 327 328 // Evaluate executes code and pretty prints the result to the specified output 329 // stream. 330 func (c *Console) Evaluate(statement string) { 331 defer func() { 332 if r := recover(); r != nil { 333 fmt.Fprintf(c.printer, "[native] error: %v\n", r) 334 } 335 }() 336 c.jsre.Evaluate(statement, c.printer) 337 } 338 339 // Interactive starts an interactive user session, where input is propted from 340 // the configured user prompter. 341 func (c *Console) Interactive() { 342 var ( 343 prompt = c.prompt // the current prompt line (used for multi-line inputs) 344 indents = 0 // the current number of input indents (used for multi-line inputs) 345 input = "" // the current user input 346 inputLine = make(chan string, 1) // receives user input 347 inputErr = make(chan error, 1) // receives liner errors 348 requestLine = make(chan string) // requests a line of input 349 interrupt = make(chan os.Signal, 1) 350 ) 351 352 // Monitor Ctrl-C. While liner does turn on the relevant terminal mode bits to avoid 353 // the signal, a signal can still be received for unsupported terminals. Unfortunately 354 // there is no way to cancel the line reader when this happens. The readLines 355 // goroutine will be leaked in this case. 356 signal.Notify(interrupt, syscall.SIGINT, syscall.SIGTERM) 357 defer signal.Stop(interrupt) 358 359 // The line reader runs in a separate goroutine. 360 go c.readLines(inputLine, inputErr, requestLine) 361 defer close(requestLine) 362 363 for { 364 // Send the next prompt, triggering an input read. 365 requestLine <- prompt 366 367 select { 368 case <-interrupt: 369 fmt.Fprintln(c.printer, "caught interrupt, exiting") 370 return 371 372 case err := <-inputErr: 373 if err == liner.ErrPromptAborted && indents > 0 { 374 // When prompting for multi-line input, the first Ctrl-C resets 375 // the multi-line state. 376 prompt, indents, input = c.prompt, 0, "" 377 continue 378 } 379 return 380 381 case line := <-inputLine: 382 // User input was returned by the prompter, handle special cases. 383 if indents <= 0 && exit.MatchString(line) { 384 return 385 } 386 if onlyWhitespace.MatchString(line) { 387 continue 388 } 389 // Append the line to the input and check for multi-line interpretation. 390 input += line + "\n" 391 indents = countIndents(input) 392 if indents <= 0 { 393 prompt = c.prompt 394 } else { 395 prompt = strings.Repeat(".", indents*3) + " " 396 } 397 // If all the needed lines are present, save the command and run it. 398 if indents <= 0 { 399 if len(input) > 0 && input[0] != ' ' && !passwordRegexp.MatchString(input) { 400 if command := strings.TrimSpace(input); len(c.history) == 0 || command != c.history[len(c.history)-1] { 401 c.history = append(c.history, command) 402 if c.prompter != nil { 403 c.prompter.AppendHistory(command) 404 } 405 } 406 } 407 c.Evaluate(input) 408 input = "" 409 } 410 } 411 } 412 } 413 414 // readLines runs in its own goroutine, prompting for input. 415 func (c *Console) readLines(input chan<- string, errc chan<- error, prompt <-chan string) { 416 for p := range prompt { 417 line, err := c.prompter.PromptInput(p) 418 if err != nil { 419 errc <- err 420 } else { 421 input <- line 422 } 423 } 424 } 425 426 // countIndents returns the number of identations for the given input. 427 // In case of invalid input such as var a = } the result can be negative. 428 func countIndents(input string) int { 429 var ( 430 indents = 0 431 inString = false 432 strOpenChar = ' ' // keep track of the string open char to allow var str = "I'm ...."; 433 charEscaped = false // keep track if the previous char was the '\' char, allow var str = "abc\"def"; 434 ) 435 436 for _, c := range input { 437 switch c { 438 case '\\': 439 // indicate next char as escaped when in string and previous char isn't escaping this backslash 440 if !charEscaped && inString { 441 charEscaped = true 442 } 443 case '\'', '"': 444 if inString && !charEscaped && strOpenChar == c { // end string 445 inString = false 446 } else if !inString && !charEscaped { // begin string 447 inString = true 448 strOpenChar = c 449 } 450 charEscaped = false 451 case '{', '(': 452 if !inString { // ignore brackets when in string, allow var str = "a{"; without indenting 453 indents++ 454 } 455 charEscaped = false 456 case '}', ')': 457 if !inString { 458 indents-- 459 } 460 charEscaped = false 461 default: 462 charEscaped = false 463 } 464 } 465 466 return indents 467 } 468 469 // Execute runs the JavaScript file specified as the argument. 470 func (c *Console) Execute(path string) error { 471 return c.jsre.Exec(path) 472 } 473 474 // Stop cleans up the console and terminates the runtime environment. 475 func (c *Console) Stop(graceful bool) error { 476 if err := ioutil.WriteFile(c.histPath, []byte(strings.Join(c.history, "\n")), 0600); err != nil { 477 return err 478 } 479 if err := os.Chmod(c.histPath, 0600); err != nil { // Force 0600, even if it was different previously 480 return err 481 } 482 c.jsre.Stop(graceful) 483 return nil 484 }