github.com/klaytn/klaytn@v1.12.1/console/console.go (about) 1 // Modifications Copyright 2018 The klaytn Authors 2 // Copyright 2016 The go-ethereum Authors 3 // This file is part of the go-ethereum library. 4 // 5 // The go-ethereum library is free software: you can redistribute it and/or modify 6 // it under the terms of the GNU Lesser General Public License as published by 7 // the Free Software Foundation, either version 3 of the License, or 8 // (at your option) any later version. 9 // 10 // The go-ethereum library is distributed in the hope that it will be useful, 11 // but WITHOUT ANY WARRANTY; without even the implied warranty of 12 // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 // GNU Lesser General Public License for more details. 14 // 15 // You should have received a copy of the GNU Lesser General Public License 16 // along with the go-ethereum library. If not, see <http://www.gnu.org/licenses/>. 17 // 18 // This file is derived from console/console.go (2018/06/04). 19 // Modified and improved for the klaytn development. 20 21 package console 22 23 import ( 24 "fmt" 25 "io" 26 "os" 27 "os/signal" 28 "path/filepath" 29 "regexp" 30 "sort" 31 "strings" 32 "syscall" 33 34 "github.com/dop251/goja" 35 "github.com/klaytn/klaytn/console/jsre" 36 "github.com/klaytn/klaytn/console/jsre/deps" 37 "github.com/klaytn/klaytn/console/web3ext" 38 "github.com/klaytn/klaytn/log" 39 "github.com/klaytn/klaytn/networks/rpc" 40 "github.com/mattn/go-colorable" 41 "github.com/peterh/liner" 42 ) 43 44 var ( 45 passwordRegexp = regexp.MustCompile(`personal.[nus]`) 46 onlyWhitespace = regexp.MustCompile(`^\s*$`) 47 exit = regexp.MustCompile(`^\s*exit\s*;*\s*$`) 48 logger = log.NewModuleLogger(log.Console) 49 ) 50 51 // HistoryFile is the file within the data directory to store input scrollback. 52 const HistoryFile = "history" 53 54 // DefaultPrompt is the default prompt line prefix to use for user input querying. 55 const DefaultPrompt = "> " 56 57 // Config is the collection of configurations to fine tune the behavior of the 58 // JavaScript console. 59 type Config struct { 60 DataDir string // Data directory to store the console history at 61 DocRoot string // Filesystem path from where to load JavaScript files from 62 Client *rpc.Client // RPC client to execute Klaytn requests through 63 Prompt string // Input prompt prefix string (defaults to DefaultPrompt) 64 Prompter UserPrompter // Input prompter to allow interactive user feedback (defaults to TerminalPrompter) 65 Printer io.Writer // Output writer to serialize any display strings to (defaults to os.Stdout) 66 Preload []string // Absolute paths to JavaScript files to preload 67 } 68 69 // Console is a JavaScript interpreted runtime environment. It is a fully fleged 70 // JavaScript console attached to a running node via an external or in-process RPC 71 // client. 72 type Console struct { 73 client *rpc.Client // RPC client to execute Klaytn requests through 74 jsre *jsre.JSRE // JavaScript runtime environment running the interpreter 75 prompt string // Input prompt prefix string 76 prompter UserPrompter // Input prompter to allow interactive user feedback 77 histPath string // Absolute path to the console scrollback history 78 history []string // Scroll history maintained by the console 79 printer io.Writer // Output writer to serialize any display strings to 80 } 81 82 func New(config Config) (*Console, error) { 83 // Handle unset config values gracefully 84 if config.Prompter == nil { 85 config.Prompter = Stdin 86 } 87 if config.Prompt == "" { 88 config.Prompt = DefaultPrompt 89 } 90 if config.Printer == nil { 91 config.Printer = colorable.NewColorableStdout() 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, 0o700); 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 := os.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 if err := c.jsre.Compile("bignumber.js", deps.BigNumberJS); err != nil { 166 return fmt.Errorf("bignumber.js: %v", err) 167 } 168 if err := c.jsre.Compile("web3.js", deps.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 var defaultAPIs = map[string]string{"eth": "1.0", "net": "1.0", "debug": "1.0"} 186 187 // initExtensions loads and registers web3.js extensions. 188 func (c *Console) initExtensions() error { 189 const methodNotFound = -32601 190 apis, err := c.client.SupportedModules() 191 if err != nil { 192 if rpcErr, ok := err.(rpc.Error); ok && rpcErr.ErrorCode() == methodNotFound { 193 logger.Warn("Server does not support method rpc_modules, using default API list.") 194 apis = defaultAPIs 195 } else { 196 return err 197 } 198 } 199 200 // Compute aliases from server-provided modules. 201 aliases := map[string]struct{}{"klay": {}, "eth": {}} 202 for api := range apis { 203 if api == "web3" { 204 continue 205 } 206 aliases[api] = struct{}{} 207 if file, ok := web3ext.Modules[api]; ok { 208 if err = c.jsre.Compile(api+".js", file); err != nil { 209 return fmt.Errorf("%s.js: %v", api, err) 210 } 211 } 212 } 213 214 // Apply aliases. 215 c.jsre.Do(func(vm *goja.Runtime) { 216 web3 := getObject(vm, "web3") 217 for name := range aliases { 218 if v := web3.Get(name); v != nil { 219 vm.Set(name, v) 220 } 221 } 222 }) 223 return nil 224 } 225 226 // initAdmin creates additional admin APIs implemented by the bridge. 227 func (c *Console) initAdmin(vm *goja.Runtime, bridge *bridge) { 228 if admin := getObject(vm, "admin"); admin != nil { 229 admin.Set("sleepBlocks", jsre.MakeCallback(vm, bridge.SleepBlocks)) 230 admin.Set("sleep", jsre.MakeCallback(vm, bridge.Sleep)) 231 admin.Set("clearHistory", c.clearHistory) 232 } 233 } 234 235 // initPersonal redirects account-related API methods through the bridge. 236 // 237 // If the console is in interactive mode and the 'personal' API is available, override 238 // the openWallet, unlockAccount, newAccount and sign methods since these require user 239 // interaction. The original web3 callbacks are stored in 'jeth'. These will be called 240 // by the bridge after the prompt and send the original web3 request to the backend. 241 func (c *Console) initPersonal(vm *goja.Runtime, bridge *bridge) { 242 personal := getObject(vm, "personal") 243 if personal == nil || c.prompter == nil { 244 return 245 } 246 // Geth deprecated the `personal` namespace. 247 // logger.Warn("Enabling deprecated personal namespace") 248 jeth := vm.NewObject() 249 vm.Set("jeth", jeth) 250 jeth.Set("openWallet", personal.Get("openWallet")) 251 jeth.Set("unlockAccount", personal.Get("unlockAccount")) 252 jeth.Set("newAccount", personal.Get("newAccount")) 253 jeth.Set("sign", personal.Get("sign")) 254 personal.Set("openWallet", jsre.MakeCallback(vm, bridge.OpenWallet)) 255 personal.Set("unlockAccount", jsre.MakeCallback(vm, bridge.UnlockAccount)) 256 personal.Set("newAccount", jsre.MakeCallback(vm, bridge.NewAccount)) 257 personal.Set("sign", jsre.MakeCallback(vm, bridge.Sign)) 258 259 subBridge := getObject(vm, "subbridge") 260 if subBridge != nil { 261 jeth.Set("unlockParentOperator", subBridge.Get("unlockParentOperator")) 262 subBridge.Set("unlockParentOperator", jsre.MakeCallback(vm, bridge.UnlockParentOperator)) 263 264 jeth.Set("unlockChildOperator", subBridge.Get("unlockChildOperator")) 265 subBridge.Set("unlockChildOperator", jsre.MakeCallback(vm, bridge.UnlockChildOperator)) 266 } 267 } 268 269 func (c *Console) clearHistory() { 270 c.history = nil 271 c.prompter.ClearHistory() 272 if err := os.Remove(c.histPath); err != nil { 273 fmt.Fprintln(c.printer, "can't delete history file:", err) 274 } else { 275 fmt.Fprintln(c.printer, "history file deleted") 276 } 277 } 278 279 // consoleOutput is an override for the console.log and console.error methods to 280 // stream the output into the configured output stream instead of stdout. 281 func (c *Console) consoleOutput(call goja.FunctionCall) goja.Value { 282 output := make([]string, len(call.Arguments)) 283 for _, argument := range call.Arguments { 284 output = append(output, fmt.Sprintf("%v", argument)) 285 } 286 fmt.Fprintln(c.printer, strings.Join(output, " ")) 287 return goja.Null() 288 } 289 290 // AutoCompleteInput is a pre-assembled word completer to be used by the user 291 // input prompter to provide hints to the user about the methods available. 292 func (c *Console) AutoCompleteInput(line string, pos int) (string, []string, string) { 293 // No completions can be provided for empty inputs 294 if len(line) == 0 || pos == 0 { 295 return "", nil, "" 296 } 297 // Chunk data to relevant part for autocompletion 298 // E.g. in case of nested lines klay.getBalance(klay.coinb<tab><tab> 299 start := pos - 1 300 for ; start > 0; start-- { 301 // Skip all methods and namespaces (i.e. including the dot) 302 if line[start] == '.' || (line[start] >= 'a' && line[start] <= 'z') || (line[start] >= 'A' && line[start] <= 'Z') { 303 continue 304 } 305 // Handle web3 in a special way (i.e. other numbers aren't auto completed) 306 if start >= 3 && line[start-3:start] == "web3" { 307 start -= 3 308 continue 309 } 310 // We've hit an unexpected character, autocomplete form here 311 start++ 312 break 313 } 314 return line[:start], c.jsre.CompleteKeywords(line[start:pos]), line[pos:] 315 } 316 317 // Welcome show summary of current node instance and some metadata about the 318 // console's available modules. 319 func (c *Console) Welcome() { 320 // Print some generic Klaytn metadata 321 fmt.Fprintf(c.printer, "Welcome to the Klaytn JavaScript console!\n\n") 322 c.jsre.Run(` 323 console.log("instance: " + web3.version.node); 324 console.log(" datadir: " + admin.datadir); 325 `) 326 // List all the supported modules for the user to call 327 if apis, err := c.client.SupportedModules(); err == nil { 328 modules := make([]string, 0, len(apis)) 329 for api, version := range apis { 330 modules = append(modules, fmt.Sprintf("%s:%s", api, version)) 331 } 332 sort.Strings(modules) 333 fmt.Fprintln(c.printer, " modules:", strings.Join(modules, " ")) 334 } 335 fmt.Fprintln(c.printer) 336 } 337 338 // Evaluate executes code and pretty prints the result to the specified output 339 // stream. 340 func (c *Console) Evaluate(statement string) { 341 defer func() { 342 if r := recover(); r != nil { 343 fmt.Fprintf(c.printer, "[native] error: %v\n", r) 344 } 345 }() 346 c.jsre.Evaluate(statement, c.printer) 347 } 348 349 // Interactive starts an interactive user session, where input is propted from 350 // the configured user prompter. 351 func (c *Console) Interactive() { 352 var ( 353 prompt = c.prompt // Current prompt line (used for multi-line inputs) 354 indents = 0 // Current number of input indents (used for multi-line inputs) 355 input = "" // Current user input 356 scheduler = make(chan string) // Channel to send the next prompt on and receive the input 357 ) 358 // Start a goroutine to listen for promt requests and send back inputs 359 go func() { 360 for { 361 // Read the next user input 362 line, err := c.prompter.PromptInput(<-scheduler) 363 if err != nil { 364 // In case of an error, either clear the prompt or fail 365 if err == liner.ErrPromptAborted { // ctrl-C 366 prompt, indents, input = c.prompt, 0, "" 367 scheduler <- "" 368 continue 369 } 370 close(scheduler) 371 return 372 } 373 // User input retrieved, send for interpretation and loop 374 scheduler <- line 375 } 376 }() 377 // Monitor Ctrl-C too in case the input is empty and we need to bail 378 abort := make(chan os.Signal, 1) 379 signal.Notify(abort, syscall.SIGINT, syscall.SIGTERM) 380 381 // Start sending prompts to the user and reading back inputs 382 for { 383 // Send the next prompt, triggering an input read and process the result 384 scheduler <- prompt 385 select { 386 case <-abort: 387 // User forcefully quite the console 388 fmt.Fprintln(c.printer, "caught interrupt, exiting") 389 return 390 391 case line, ok := <-scheduler: 392 // User input was returned by the prompter, handle special cases 393 if !ok || (indents <= 0 && exit.MatchString(line)) { 394 return 395 } 396 if onlyWhitespace.MatchString(line) { 397 continue 398 } 399 // Append the line to the input and check for multi-line interpretation 400 input += line + "\n" 401 402 indents = countIndents(input) 403 if indents <= 0 { 404 prompt = c.prompt 405 } else { 406 prompt = strings.Repeat(".", indents*3) + " " 407 } 408 // If all the needed lines are present, save the command and run 409 if indents <= 0 { 410 if len(input) > 0 && input[0] != ' ' && !passwordRegexp.MatchString(input) { 411 if command := strings.TrimSpace(input); len(c.history) == 0 || command != c.history[len(c.history)-1] { 412 c.history = append(c.history, command) 413 if c.prompter != nil { 414 c.prompter.AppendHistory(command) 415 } 416 } 417 } 418 c.Evaluate(input) 419 input = "" 420 } 421 } 422 } 423 } 424 425 // countIndents returns the number of identations for the given input. 426 // In case of invalid input such as var a = } the result can be negative. 427 func countIndents(input string) int { 428 var ( 429 indents = 0 430 inString = false 431 strOpenChar = ' ' // keep track of the string open char to allow var str = "I'm ...."; 432 charEscaped = false // keep track if the previous char was the '\' char, allow var str = "abc\"def"; 433 ) 434 435 for _, c := range input { 436 switch c { 437 case '\\': 438 // indicate next char as escaped when in string and previous char isn't escaping this backslash 439 if !charEscaped && inString { 440 charEscaped = true 441 } 442 case '\'', '"': 443 if inString && !charEscaped && strOpenChar == c { // end string 444 inString = false 445 } else if !inString && !charEscaped { // begin string 446 inString = true 447 strOpenChar = c 448 } 449 charEscaped = false 450 case '{', '(': 451 if !inString { // ignore brackets when in string, allow var str = "a{"; without indenting 452 indents++ 453 } 454 charEscaped = false 455 case '}', ')': 456 if !inString { 457 indents-- 458 } 459 charEscaped = false 460 default: 461 charEscaped = false 462 } 463 } 464 465 return indents 466 } 467 468 // Execute runs the JavaScript file specified as the argument. 469 func (c *Console) Execute(path string) error { 470 return c.jsre.Exec(path) 471 } 472 473 // Stop cleans up the console and terminates the runtime environment. 474 func (c *Console) Stop(graceful bool) error { 475 if err := os.WriteFile(c.histPath, []byte(strings.Join(c.history, "\n")), 0o600); err != nil { 476 return err 477 } 478 if err := os.Chmod(c.histPath, 0o600); err != nil { // Force 0600, even if it was different previously 479 return err 480 } 481 c.jsre.Stop(graceful) 482 return nil 483 }