github.com/tuotoo/go-ethereum@v1.7.4-0.20171121184211-049797d40a24/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 30 "github.com/ethereum/go-ethereum/internal/jsre" 31 "github.com/ethereum/go-ethereum/internal/web3ext" 32 "github.com/ethereum/go-ethereum/rpc" 33 "github.com/mattn/go-colorable" 34 "github.com/peterh/liner" 35 "github.com/robertkrimen/otto" 36 ) 37 38 var ( 39 passwordRegexp = regexp.MustCompile(`personal.[nus]`) 40 onlyWhitespace = regexp.MustCompile(`^\s*$`) 41 exit = regexp.MustCompile(`^\s*exit\s*;*\s*$`) 42 ) 43 44 // HistoryFile is the file within the data directory to store input scrollback. 45 const HistoryFile = "history" 46 47 // DefaultPrompt is the default prompt line prefix to use for user input querying. 48 const DefaultPrompt = "> " 49 50 // Config is te collection of configurations to fine tune the behavior of the 51 // JavaScript console. 52 type Config struct { 53 DataDir string // Data directory to store the console history at 54 DocRoot string // Filesystem path from where to load JavaScript files from 55 Client *rpc.Client // RPC client to execute Ethereum requests through 56 Prompt string // Input prompt prefix string (defaults to DefaultPrompt) 57 Prompter UserPrompter // Input prompter to allow interactive user feedback (defaults to TerminalPrompter) 58 Printer io.Writer // Output writer to serialize any display strings to (defaults to os.Stdout) 59 Preload []string // Absolute paths to JavaScript files to preload 60 } 61 62 // Console is a JavaScript interpreted runtime environment. It is a fully fleged 63 // JavaScript console attached to a running node via an external or in-process RPC 64 // client. 65 type Console struct { 66 client *rpc.Client // RPC client to execute Ethereum requests through 67 jsre *jsre.JSRE // JavaScript runtime environment running the interpreter 68 prompt string // Input prompt prefix string 69 prompter UserPrompter // Input prompter to allow interactive user feedback 70 histPath string // Absolute path to the console scrollback history 71 history []string // Scroll history maintained by the console 72 printer io.Writer // Output writer to serialize any display strings to 73 } 74 75 func New(config Config) (*Console, error) { 76 // Handle unset config values gracefully 77 if config.Prompter == nil { 78 config.Prompter = Stdin 79 } 80 if config.Prompt == "" { 81 config.Prompt = DefaultPrompt 82 } 83 if config.Printer == nil { 84 config.Printer = colorable.NewColorableStdout() 85 } 86 // Initialize the console and return 87 console := &Console{ 88 client: config.Client, 89 jsre: jsre.New(config.DocRoot, config.Printer), 90 prompt: config.Prompt, 91 prompter: config.Prompter, 92 printer: config.Printer, 93 histPath: filepath.Join(config.DataDir, HistoryFile), 94 } 95 if err := console.init(config.Preload); err != nil { 96 return nil, err 97 } 98 return console, nil 99 } 100 101 // init retrieves the available APIs from the remote RPC provider and initializes 102 // the console's JavaScript namespaces based on the exposed modules. 103 func (c *Console) init(preload []string) error { 104 // Initialize the JavaScript <-> Go RPC bridge 105 bridge := newBridge(c.client, c.prompter, c.printer) 106 c.jsre.Set("jeth", struct{}{}) 107 108 jethObj, _ := c.jsre.Get("jeth") 109 jethObj.Object().Set("send", bridge.Send) 110 jethObj.Object().Set("sendAsync", bridge.Send) 111 112 consoleObj, _ := c.jsre.Get("console") 113 consoleObj.Object().Set("log", c.consoleOutput) 114 consoleObj.Object().Set("error", c.consoleOutput) 115 116 // Load all the internal utility JavaScript libraries 117 if err := c.jsre.Compile("bignumber.js", jsre.BigNumber_JS); err != nil { 118 return fmt.Errorf("bignumber.js: %v", err) 119 } 120 if err := c.jsre.Compile("web3.js", jsre.Web3_JS); err != nil { 121 return fmt.Errorf("web3.js: %v", err) 122 } 123 if _, err := c.jsre.Run("var Web3 = require('web3');"); err != nil { 124 return fmt.Errorf("web3 require: %v", err) 125 } 126 if _, err := c.jsre.Run("var web3 = new Web3(jeth);"); err != nil { 127 return fmt.Errorf("web3 provider: %v", err) 128 } 129 // Load the supported APIs into the JavaScript runtime environment 130 apis, err := c.client.SupportedModules() 131 if err != nil { 132 return fmt.Errorf("api modules: %v", err) 133 } 134 flatten := "var eth = web3.eth; var personal = web3.personal; " 135 for api := range apis { 136 if api == "web3" { 137 continue // manually mapped or ignore 138 } 139 if file, ok := web3ext.Modules[api]; ok { 140 // Load our extension for the module. 141 if err = c.jsre.Compile(fmt.Sprintf("%s.js", api), file); err != nil { 142 return fmt.Errorf("%s.js: %v", api, err) 143 } 144 flatten += fmt.Sprintf("var %s = web3.%s; ", api, api) 145 } else if obj, err := c.jsre.Run("web3." + api); err == nil && obj.IsObject() { 146 // Enable web3.js built-in extension if available. 147 flatten += fmt.Sprintf("var %s = web3.%s; ", api, api) 148 } 149 } 150 if _, err = c.jsre.Run(flatten); err != nil { 151 return fmt.Errorf("namespace flattening: %v", err) 152 } 153 // Initialize the global name register (disabled for now) 154 //c.jsre.Run(`var GlobalRegistrar = eth.contract(` + registrar.GlobalRegistrarAbi + `); registrar = GlobalRegistrar.at("` + registrar.GlobalRegistrarAddr + `");`) 155 156 // If the console is in interactive mode, instrument password related methods to query the user 157 if c.prompter != nil { 158 // Retrieve the account management object to instrument 159 personal, err := c.jsre.Get("personal") 160 if err != nil { 161 return err 162 } 163 // Override the openWallet, unlockAccount, newAccount and sign methods since 164 // these require user interaction. Assign these method in the Console the 165 // original web3 callbacks. These will be called by the jeth.* methods after 166 // they got the password from the user and send the original web3 request to 167 // the backend. 168 if obj := personal.Object(); obj != nil { // make sure the personal api is enabled over the interface 169 if _, err = c.jsre.Run(`jeth.openWallet = personal.openWallet;`); err != nil { 170 return fmt.Errorf("personal.openWallet: %v", err) 171 } 172 if _, err = c.jsre.Run(`jeth.unlockAccount = personal.unlockAccount;`); err != nil { 173 return fmt.Errorf("personal.unlockAccount: %v", err) 174 } 175 if _, err = c.jsre.Run(`jeth.newAccount = personal.newAccount;`); err != nil { 176 return fmt.Errorf("personal.newAccount: %v", err) 177 } 178 if _, err = c.jsre.Run(`jeth.sign = personal.sign;`); err != nil { 179 return fmt.Errorf("personal.sign: %v", err) 180 } 181 obj.Set("openWallet", bridge.OpenWallet) 182 obj.Set("unlockAccount", bridge.UnlockAccount) 183 obj.Set("newAccount", bridge.NewAccount) 184 obj.Set("sign", bridge.Sign) 185 } 186 } 187 // The admin.sleep and admin.sleepBlocks are offered by the console and not by the RPC layer. 188 admin, err := c.jsre.Get("admin") 189 if err != nil { 190 return err 191 } 192 if obj := admin.Object(); obj != nil { // make sure the admin api is enabled over the interface 193 obj.Set("sleepBlocks", bridge.SleepBlocks) 194 obj.Set("sleep", bridge.Sleep) 195 } 196 // Preload any JavaScript files before starting the console 197 for _, path := range preload { 198 if err := c.jsre.Exec(path); err != nil { 199 failure := err.Error() 200 if ottoErr, ok := err.(*otto.Error); ok { 201 failure = ottoErr.String() 202 } 203 return fmt.Errorf("%s: %v", path, failure) 204 } 205 } 206 // Configure the console's input prompter for scrollback and tab completion 207 if c.prompter != nil { 208 if content, err := ioutil.ReadFile(c.histPath); err != nil { 209 c.prompter.SetHistory(nil) 210 } else { 211 c.history = strings.Split(string(content), "\n") 212 c.prompter.SetHistory(c.history) 213 } 214 c.prompter.SetWordCompleter(c.AutoCompleteInput) 215 } 216 return nil 217 } 218 219 // consoleOutput is an override for the console.log and console.error methods to 220 // stream the output into the configured output stream instead of stdout. 221 func (c *Console) consoleOutput(call otto.FunctionCall) otto.Value { 222 output := []string{} 223 for _, argument := range call.ArgumentList { 224 output = append(output, fmt.Sprintf("%v", argument)) 225 } 226 fmt.Fprintln(c.printer, strings.Join(output, " ")) 227 return otto.Value{} 228 } 229 230 // AutoCompleteInput is a pre-assembled word completer to be used by the user 231 // input prompter to provide hints to the user about the methods available. 232 func (c *Console) AutoCompleteInput(line string, pos int) (string, []string, string) { 233 // No completions can be provided for empty inputs 234 if len(line) == 0 || pos == 0 { 235 return "", nil, "" 236 } 237 // Chunck data to relevant part for autocompletion 238 // E.g. in case of nested lines eth.getBalance(eth.coinb<tab><tab> 239 start := pos - 1 240 for ; start > 0; start-- { 241 // Skip all methods and namespaces (i.e. including te dot) 242 if line[start] == '.' || (line[start] >= 'a' && line[start] <= 'z') || (line[start] >= 'A' && line[start] <= 'Z') { 243 continue 244 } 245 // Handle web3 in a special way (i.e. other numbers aren't auto completed) 246 if start >= 3 && line[start-3:start] == "web3" { 247 start -= 3 248 continue 249 } 250 // We've hit an unexpected character, autocomplete form here 251 start++ 252 break 253 } 254 return line[:start], c.jsre.CompleteKeywords(line[start:pos]), line[pos:] 255 } 256 257 // Welcome show summary of current Geth instance and some metadata about the 258 // console's available modules. 259 func (c *Console) Welcome() { 260 // Print some generic Geth metadata 261 fmt.Fprintf(c.printer, "Welcome to the Geth JavaScript console!\n\n") 262 c.jsre.Run(` 263 console.log("instance: " + web3.version.node); 264 console.log("coinbase: " + eth.coinbase); 265 console.log("at block: " + eth.blockNumber + " (" + new Date(1000 * eth.getBlock(eth.blockNumber).timestamp) + ")"); 266 console.log(" datadir: " + admin.datadir); 267 `) 268 // List all the supported modules for the user to call 269 if apis, err := c.client.SupportedModules(); err == nil { 270 modules := make([]string, 0, len(apis)) 271 for api, version := range apis { 272 modules = append(modules, fmt.Sprintf("%s:%s", api, version)) 273 } 274 sort.Strings(modules) 275 fmt.Fprintln(c.printer, " modules:", strings.Join(modules, " ")) 276 } 277 fmt.Fprintln(c.printer) 278 } 279 280 // Evaluate executes code and pretty prints the result to the specified output 281 // stream. 282 func (c *Console) Evaluate(statement string) error { 283 defer func() { 284 if r := recover(); r != nil { 285 fmt.Fprintf(c.printer, "[native] error: %v\n", r) 286 } 287 }() 288 return c.jsre.Evaluate(statement, c.printer) 289 } 290 291 // Interactive starts an interactive user session, where input is propted from 292 // the configured user prompter. 293 func (c *Console) Interactive() { 294 var ( 295 prompt = c.prompt // Current prompt line (used for multi-line inputs) 296 indents = 0 // Current number of input indents (used for multi-line inputs) 297 input = "" // Current user input 298 scheduler = make(chan string) // Channel to send the next prompt on and receive the input 299 ) 300 // Start a goroutine to listen for promt requests and send back inputs 301 go func() { 302 for { 303 // Read the next user input 304 line, err := c.prompter.PromptInput(<-scheduler) 305 if err != nil { 306 // In case of an error, either clear the prompt or fail 307 if err == liner.ErrPromptAborted { // ctrl-C 308 prompt, indents, input = c.prompt, 0, "" 309 scheduler <- "" 310 continue 311 } 312 close(scheduler) 313 return 314 } 315 // User input retrieved, send for interpretation and loop 316 scheduler <- line 317 } 318 }() 319 // Monitor Ctrl-C too in case the input is empty and we need to bail 320 abort := make(chan os.Signal, 1) 321 signal.Notify(abort, os.Interrupt) 322 323 // Start sending prompts to the user and reading back inputs 324 for { 325 // Send the next prompt, triggering an input read and process the result 326 scheduler <- prompt 327 select { 328 case <-abort: 329 // User forcefully quite the console 330 fmt.Fprintln(c.printer, "caught interrupt, exiting") 331 return 332 333 case line, ok := <-scheduler: 334 // User input was returned by the prompter, handle special cases 335 if !ok || (indents <= 0 && exit.MatchString(line)) { 336 return 337 } 338 if onlyWhitespace.MatchString(line) { 339 continue 340 } 341 // Append the line to the input and check for multi-line interpretation 342 input += line + "\n" 343 344 indents = countIndents(input) 345 if indents <= 0 { 346 prompt = c.prompt 347 } else { 348 prompt = strings.Repeat(".", indents*3) + " " 349 } 350 // If all the needed lines are present, save the command and run 351 if indents <= 0 { 352 if len(input) > 0 && input[0] != ' ' && !passwordRegexp.MatchString(input) { 353 if command := strings.TrimSpace(input); len(c.history) == 0 || command != c.history[len(c.history)-1] { 354 c.history = append(c.history, command) 355 if c.prompter != nil { 356 c.prompter.AppendHistory(command) 357 } 358 } 359 } 360 c.Evaluate(input) 361 input = "" 362 } 363 } 364 } 365 } 366 367 // countIndents returns the number of identations for the given input. 368 // In case of invalid input such as var a = } the result can be negative. 369 func countIndents(input string) int { 370 var ( 371 indents = 0 372 inString = false 373 strOpenChar = ' ' // keep track of the string open char to allow var str = "I'm ...."; 374 charEscaped = false // keep track if the previous char was the '\' char, allow var str = "abc\"def"; 375 ) 376 377 for _, c := range input { 378 switch c { 379 case '\\': 380 // indicate next char as escaped when in string and previous char isn't escaping this backslash 381 if !charEscaped && inString { 382 charEscaped = true 383 } 384 case '\'', '"': 385 if inString && !charEscaped && strOpenChar == c { // end string 386 inString = false 387 } else if !inString && !charEscaped { // begin string 388 inString = true 389 strOpenChar = c 390 } 391 charEscaped = false 392 case '{', '(': 393 if !inString { // ignore brackets when in string, allow var str = "a{"; without indenting 394 indents++ 395 } 396 charEscaped = false 397 case '}', ')': 398 if !inString { 399 indents-- 400 } 401 charEscaped = false 402 default: 403 charEscaped = false 404 } 405 } 406 407 return indents 408 } 409 410 // Execute runs the JavaScript file specified as the argument. 411 func (c *Console) Execute(path string) error { 412 return c.jsre.Exec(path) 413 } 414 415 // Stop cleans up the console and terminates the runtime envorinment. 416 func (c *Console) Stop(graceful bool) error { 417 if err := ioutil.WriteFile(c.histPath, []byte(strings.Join(c.history, "\n")), 0600); err != nil { 418 return err 419 } 420 if err := os.Chmod(c.histPath, 0600); err != nil { // Force 0600, even if it was different previously 421 return err 422 } 423 c.jsre.Stop(graceful) 424 return nil 425 }