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