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