git.pirl.io/community/pirl@v0.0.0-20201111064343-9d3d31ff74be/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 "git.pirl.io/community/pirl/internal/jsre" 33 "git.pirl.io/community/pirl/internal/jsre/deps" 34 "git.pirl.io/community/pirl/internal/web3ext" 35 "git.pirl.io/community/pirl/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 // Current prompt line (used for multi-line inputs) 344 indents = 0 // Current number of input indents (used for multi-line inputs) 345 input = "" // Current user input 346 scheduler = make(chan string) // Channel to send the next prompt on and receive the input 347 ) 348 // Start a goroutine to listen for prompt requests and send back inputs 349 go func() { 350 for { 351 // Read the next user input 352 line, err := c.prompter.PromptInput(<-scheduler) 353 if err != nil { 354 // In case of an error, either clear the prompt or fail 355 if err == liner.ErrPromptAborted { // ctrl-C 356 prompt, indents, input = c.prompt, 0, "" 357 scheduler <- "" 358 continue 359 } 360 close(scheduler) 361 return 362 } 363 // User input retrieved, send for interpretation and loop 364 scheduler <- line 365 } 366 }() 367 // Monitor Ctrl-C too in case the input is empty and we need to bail 368 abort := make(chan os.Signal, 1) 369 signal.Notify(abort, syscall.SIGINT, syscall.SIGTERM) 370 371 // Start sending prompts to the user and reading back inputs 372 for { 373 // Send the next prompt, triggering an input read and process the result 374 scheduler <- prompt 375 select { 376 case <-abort: 377 // User forcefully quite the console 378 fmt.Fprintln(c.printer, "caught interrupt, exiting") 379 return 380 381 case line, ok := <-scheduler: 382 // User input was returned by the prompter, handle special cases 383 if !ok || (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 392 indents = countIndents(input) 393 if indents <= 0 { 394 prompt = c.prompt 395 } else { 396 prompt = strings.Repeat(".", indents*3) + " " 397 } 398 // If all the needed lines are present, save the command and run 399 if indents <= 0 { 400 if len(input) > 0 && input[0] != ' ' && !passwordRegexp.MatchString(input) { 401 if command := strings.TrimSpace(input); len(c.history) == 0 || command != c.history[len(c.history)-1] { 402 c.history = append(c.history, command) 403 if c.prompter != nil { 404 c.prompter.AppendHistory(command) 405 } 406 } 407 } 408 c.Evaluate(input) 409 input = "" 410 } 411 } 412 } 413 } 414 415 // countIndents returns the number of identations for the given input. 416 // In case of invalid input such as var a = } the result can be negative. 417 func countIndents(input string) int { 418 var ( 419 indents = 0 420 inString = false 421 strOpenChar = ' ' // keep track of the string open char to allow var str = "I'm ...."; 422 charEscaped = false // keep track if the previous char was the '\' char, allow var str = "abc\"def"; 423 ) 424 425 for _, c := range input { 426 switch c { 427 case '\\': 428 // indicate next char as escaped when in string and previous char isn't escaping this backslash 429 if !charEscaped && inString { 430 charEscaped = true 431 } 432 case '\'', '"': 433 if inString && !charEscaped && strOpenChar == c { // end string 434 inString = false 435 } else if !inString && !charEscaped { // begin string 436 inString = true 437 strOpenChar = c 438 } 439 charEscaped = false 440 case '{', '(': 441 if !inString { // ignore brackets when in string, allow var str = "a{"; without indenting 442 indents++ 443 } 444 charEscaped = false 445 case '}', ')': 446 if !inString { 447 indents-- 448 } 449 charEscaped = false 450 default: 451 charEscaped = false 452 } 453 } 454 455 return indents 456 } 457 458 // Execute runs the JavaScript file specified as the argument. 459 func (c *Console) Execute(path string) error { 460 return c.jsre.Exec(path) 461 } 462 463 // Stop cleans up the console and terminates the runtime environment. 464 func (c *Console) Stop(graceful bool) error { 465 if err := ioutil.WriteFile(c.histPath, []byte(strings.Join(c.history, "\n")), 0600); err != nil { 466 return err 467 } 468 if err := os.Chmod(c.histPath, 0600); err != nil { // Force 0600, even if it was different previously 469 return err 470 } 471 c.jsre.Stop(graceful) 472 return nil 473 }