console.go 17 KB

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