aboutsummaryrefslogtreecommitdiffstats
path: root/console/console.go
diff options
context:
space:
mode:
authorPéter Szilágyi <peterke@gmail.com>2016-05-06 17:40:23 +0800
committerPéter Szilágyi <peterke@gmail.com>2016-05-30 22:25:23 +0800
commitffaf58f0a98bd987bbe76e8669bb22c405dcd62a (patch)
tree4cd484504d4a7089cfd85d22a2a9fa62c79abb7d /console/console.go
parentab664c7e17009729d1083d8d4f7c37eb387012d3 (diff)
downloadgo-tangerine-ffaf58f0a98bd987bbe76e8669bb22c405dcd62a.tar.gz
go-tangerine-ffaf58f0a98bd987bbe76e8669bb22c405dcd62a.tar.zst
go-tangerine-ffaf58f0a98bd987bbe76e8669bb22c405dcd62a.zip
cmd, console: split off the console into a reusable package
Diffstat (limited to 'console/console.go')
-rw-r--r--console/console.go369
1 files changed, 369 insertions, 0 deletions
diff --git a/console/console.go b/console/console.go
new file mode 100644
index 000000000..37c9f0afa
--- /dev/null
+++ b/console/console.go
@@ -0,0 +1,369 @@
+// Copyright 2015 The go-ethereum Authors
+// This file is part of the go-ethereum library.
+//
+// The go-ethereum library is free software: you can redistribute it and/or modify
+// it under the terms of the GNU Lesser General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// The go-ethereum library is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU Lesser General Public License for more details.
+//
+// You should have received a copy of the GNU Lesser General Public License
+// along with the go-ethereum library. If not, see <http://www.gnu.org/licenses/>.
+
+package console
+
+import (
+ "fmt"
+ "io"
+ "io/ioutil"
+ "os"
+ "os/signal"
+ "path/filepath"
+ "regexp"
+ "sort"
+ "strings"
+
+ "github.com/ethereum/go-ethereum/internal/jsre"
+ "github.com/ethereum/go-ethereum/internal/web3ext"
+ "github.com/ethereum/go-ethereum/rpc"
+ "github.com/peterh/liner"
+ "github.com/robertkrimen/otto"
+)
+
+var (
+ passwordRegexp = regexp.MustCompile("personal.[nus]")
+ onlyWhitespace = regexp.MustCompile("^\\s*$")
+ exit = regexp.MustCompile("^\\s*exit\\s*;*\\s*$")
+)
+
+// HistoryFile is the file within the data directory to store input scrollback.
+const HistoryFile = "history"
+
+// DefaultPrompt is the default prompt line prefix to use for user input querying.
+const DefaultPrompt = "> "
+
+// Config is te collection of configurations to fine tune the behavior of the
+// JavaScript console.
+type Config struct {
+ DataDir string // Data directory to store the console history at
+ DocRoot string // Filesystem path from where to load JavaScript files from
+ Client rpc.Client // RPC client to execute Ethereum requests through
+ Prompt string // Input prompt prefix string (defaults to DefaultPrompt)
+ Prompter UserPrompter // Input prompter to allow interactive user feedback (defaults to TerminalPrompter)
+ Printer io.Writer // Output writer to serialize any display strings to (defaults to os.Stdout)
+ Preload []string // Absolute paths to JavaScript files to preload
+}
+
+// Console is a JavaScript interpreted runtime environment. It is a fully fleged
+// JavaScript console attached to a running node via an external or in-process RPC
+// client.
+type Console struct {
+ client rpc.Client // RPC client to execute Ethereum requests through
+ jsre *jsre.JSRE // JavaScript runtime environment running the interpreter
+ prompt string // Input prompt prefix string
+ prompter UserPrompter // Input prompter to allow interactive user feedback
+ histPath string // Absolute path to the console scrollback history
+ history []string // Scroll history maintained by the console
+ printer io.Writer // Output writer to serialize any display strings to
+}
+
+func New(config Config) (*Console, error) {
+ // Handle unset config values gracefully
+ if config.Prompter == nil {
+ config.Prompter = TerminalPrompter
+ }
+ if config.Prompt == "" {
+ config.Prompt = DefaultPrompt
+ }
+ if config.Printer == nil {
+ config.Printer = os.Stdout
+ }
+ // Initialize the console and return
+ console := &Console{
+ client: config.Client,
+ jsre: jsre.New(config.DocRoot, config.Printer),
+ prompt: config.Prompt,
+ prompter: config.Prompter,
+ printer: config.Printer,
+ histPath: filepath.Join(config.DataDir, HistoryFile),
+ }
+ if err := console.init(config.Preload); err != nil {
+ return nil, err
+ }
+ return console, nil
+}
+
+// init retrieves the available APIs from the remote RPC provider and initializes
+// the console's JavaScript namespaces based on the exposed modules.
+func (c *Console) init(preload []string) error {
+ // Initialize the JavaScript <-> Go RPC bridge
+ bridge := newBridge(c.client, c.prompter, c.printer)
+ c.jsre.Set("jeth", struct{}{})
+
+ jethObj, _ := c.jsre.Get("jeth")
+ jethObj.Object().Set("send", bridge.Send)
+ jethObj.Object().Set("sendAsync", bridge.Send)
+
+ consoleObj, _ := c.jsre.Get("console")
+ consoleObj.Object().Set("log", c.consoleOutput)
+ consoleObj.Object().Set("error", c.consoleOutput)
+
+ // Load all the internal utility JavaScript libraries
+ if err := c.jsre.Compile("bignumber.js", jsre.BigNumber_JS); err != nil {
+ return fmt.Errorf("bignumber.js: %v", err)
+ }
+ if err := c.jsre.Compile("web3.js", jsre.Web3_JS); err != nil {
+ return fmt.Errorf("web3.js: %v", err)
+ }
+ if _, err := c.jsre.Run("var Web3 = require('web3');"); err != nil {
+ return fmt.Errorf("web3 require: %v", err)
+ }
+ if _, err := c.jsre.Run("var web3 = new Web3(jeth);"); err != nil {
+ return fmt.Errorf("web3 provider: %v", err)
+ }
+ // Load the supported APIs into the JavaScript runtime environment
+ apis, err := c.client.SupportedModules()
+ if err != nil {
+ return fmt.Errorf("api modules: %v", err)
+ }
+ flatten := "var eth = web3.eth; var personal = web3.personal; "
+ for api := range apis {
+ if api == "web3" {
+ continue // manually mapped or ignore
+ }
+ if file, ok := web3ext.Modules[api]; ok {
+ if err = c.jsre.Compile(fmt.Sprintf("%s.js", api), file); err != nil {
+ return fmt.Errorf("%s.js: %v", api, err)
+ }
+ flatten += fmt.Sprintf("var %s = web3.%s; ", api, api)
+ }
+ }
+ if _, err = c.jsre.Run(flatten); err != nil {
+ return fmt.Errorf("namespace flattening: %v", err)
+ }
+ // Initialize the global name register (disabled for now)
+ //c.jsre.Run(`var GlobalRegistrar = eth.contract(` + registrar.GlobalRegistrarAbi + `); registrar = GlobalRegistrar.at("` + registrar.GlobalRegistrarAddr + `");`)
+
+ // If the console is in interactive mode, instrument password related methods to query the user
+ if c.prompter != nil {
+ // Retrieve the account management object to instrument
+ personal, err := c.jsre.Get("personal")
+ if err != nil {
+ return err
+ }
+ // Override the unlockAccount and newAccount methods since these require user interaction.
+ // Assign the jeth.unlockAccount and jeth.newAccount in the Console the original web3 callbacks.
+ // These will be called by the jeth.* methods after they got the password from the user and send
+ // the original web3 request to the backend.
+ if obj := personal.Object(); obj != nil { // make sure the personal api is enabled over the interface
+ if _, err = c.jsre.Run(`jeth.unlockAccount = personal.unlockAccount;`); err != nil {
+ return fmt.Errorf("personal.unlockAccount: %v", err)
+ }
+ if _, err = c.jsre.Run(`jeth.newAccount = personal.newAccount;`); err != nil {
+ return fmt.Errorf("personal.newAccount: %v", err)
+ }
+ obj.Set("unlockAccount", bridge.UnlockAccount)
+ obj.Set("newAccount", bridge.NewAccount)
+ }
+ }
+ // The admin.sleep and admin.sleepBlocks are offered by the console and not by the RPC layer.
+ admin, err := c.jsre.Get("admin")
+ if err != nil {
+ return err
+ }
+ if obj := admin.Object(); obj != nil { // make sure the admin api is enabled over the interface
+ obj.Set("sleepBlocks", bridge.SleepBlocks)
+ obj.Set("sleep", bridge.Sleep)
+ }
+ // Preload any JavaScript files before starting the console
+ for _, path := range preload {
+ if err := c.jsre.Exec(path); err != nil {
+ return fmt.Errorf("%s: %v", path, jsErrorString(err))
+ }
+ }
+ // Configure the console's input prompter for scrollback and tab completion
+ if c.prompter != nil {
+ if content, err := ioutil.ReadFile(c.histPath); err != nil {
+ c.prompter.SetScrollHistory(nil)
+ } else {
+ c.prompter.SetScrollHistory(strings.Split(string(content), "\n"))
+ }
+ c.prompter.SetWordCompleter(c.AutoCompleteInput)
+ }
+ return nil
+}
+
+// consoleOutput is an override for the console.log and console.error methods to
+// stream the output into the configured output stream instead of stdout.
+func (c *Console) consoleOutput(call otto.FunctionCall) otto.Value {
+ output := []string{}
+ for _, argument := range call.ArgumentList {
+ output = append(output, fmt.Sprintf("%v", argument))
+ }
+ fmt.Fprintln(c.printer, strings.Join(output, " "))
+ return otto.Value{}
+}
+
+// AutoCompleteInput is a pre-assembled word completer to be used by the user
+// input prompter to provide hints to the user about the methods available.
+func (c *Console) AutoCompleteInput(line string, pos int) (string, []string, string) {
+ // No completions can be provided for empty inputs
+ if len(line) == 0 || pos == 0 {
+ return "", nil, ""
+ }
+ // Chunck data to relevant part for autocompletion
+ // E.g. in case of nested lines eth.getBalance(eth.coinb<tab><tab>
+ start := 0
+ for start = pos - 1; start > 0; start-- {
+ // Skip all methods and namespaces (i.e. including te dot)
+ if line[start] == '.' || (line[start] >= 'a' && line[start] <= 'z') || (line[start] >= 'A' && line[start] <= 'Z') {
+ continue
+ }
+ // Handle web3 in a special way (i.e. other numbers aren't auto completed)
+ if start >= 3 && line[start-3:start] == "web3" {
+ start -= 3
+ continue
+ }
+ // We've hit an unexpected character, autocomplete form here
+ start++
+ break
+ }
+ return line[:start], c.jsre.CompleteKeywords(line[start:pos]), line[pos:]
+}
+
+// Welcome show summary of current Geth instance and some metadata about the
+// console's available modules.
+func (c *Console) Welcome() {
+ // Print some generic Geth metadata
+ c.jsre.Run(`
+ (function () {
+ console.log("Welcome to the Geth JavaScript console!\n");
+ console.log("instance: " + web3.version.node);
+ console.log("coinbase: " + eth.coinbase);
+ console.log("at block: " + eth.blockNumber + " (" + new Date(1000 * eth.getBlock(eth.blockNumber).timestamp) + ")");
+ console.log(" datadir: " + admin.datadir);
+ })();
+ `)
+ // List all the supported modules for the user to call
+ if apis, err := c.client.SupportedModules(); err == nil {
+ modules := make([]string, 0, len(apis))
+ for api, version := range apis {
+ modules = append(modules, fmt.Sprintf("%s:%s", api, version))
+ }
+ sort.Strings(modules)
+ c.jsre.Run("(function () { console.log(' modules: " + strings.Join(modules, " ") + "'); })();")
+ }
+ c.jsre.Run("(function () { console.log(); })();")
+}
+
+// Evaluate executes code and pretty prints the result to the specified output
+// stream.
+func (c *Console) Evaluate(statement string) error {
+ defer func() {
+ if r := recover(); r != nil {
+ fmt.Fprintf(c.printer, "[native] error: %v\n", r)
+ }
+ }()
+ if err := c.jsre.Evaluate(statement, c.printer); err != nil {
+ fmt.Fprintf(c.printer, "%v\n", jsErrorString(err))
+ return err
+ }
+ return nil
+}
+
+// Interactive starts an interactive user session, where input is propted from
+// the configured user prompter.
+func (c *Console) Interactive() {
+ var (
+ prompt = c.prompt // Current prompt line (used for multi-line inputs)
+ indents = 0 // Current number of input indents (used for multi-line inputs)
+ input = "" // Current user input
+ scheduler = make(chan string) // Channel to send the next prompt on and receive the input
+ )
+ // Start a goroutine to listen for promt requests and send back inputs
+ go func() {
+ for {
+ // Read the next user input
+ line, err := c.prompter.PromptInput(<-scheduler)
+ if err != nil {
+ // In case of an error, either clear the prompt or fail
+ if err == liner.ErrPromptAborted { // ctrl-C
+ prompt, indents, input = c.prompt, 0, ""
+ scheduler <- ""
+ continue
+ }
+ close(scheduler)
+ return
+ }
+ // User input retrieved, send for interpretation and loop
+ scheduler <- line
+ }
+ }()
+ // Monitor Ctrl-C too in case the input is empty and we need to bail
+ abort := make(chan os.Signal, 1)
+ signal.Notify(abort, os.Interrupt)
+
+ // Start sending prompts to the user and reading back inputs
+ for {
+ // Send the next prompt, triggering an input read and process the result
+ scheduler <- prompt
+ select {
+ case <-abort:
+ // User forcefully quite the console
+ fmt.Fprintln(c.printer, "caught interrupt, exiting")
+ return
+
+ case line, ok := <-scheduler:
+ // User input was returned by the prompter, handle special cases
+ if !ok || (indents <= 0 && exit.MatchString(input)) {
+ return
+ }
+ if onlyWhitespace.MatchString(line) {
+ continue
+ }
+ // Append the line to the input and check for multi-line interpretation
+ input += line + "\n"
+
+ indents = strings.Count(input, "{") + strings.Count(input, "(") - strings.Count(input, "}") - strings.Count(input, ")")
+ if indents <= 0 {
+ prompt = c.prompt
+ } else {
+ prompt = strings.Repeat("..", indents*2) + " "
+ }
+ // If all the needed lines are present, save the command and run
+ if indents <= 0 {
+ if len(input) != 0 && input[0] != ' ' && !passwordRegexp.MatchString(input) {
+ c.history = append(c.history, input[:len(input)-1])
+ }
+ c.Evaluate(input)
+ input = ""
+ }
+ }
+ }
+}
+
+// Execute runs the JavaScript file specified as the argument.
+func (c *Console) Execute(path string) error {
+ return c.jsre.Exec(path)
+}
+
+// Stop cleans up the console and terminates the runtime envorinment.
+func (c *Console) Stop(graceful bool) error {
+ if err := ioutil.WriteFile(c.histPath, []byte(strings.Join(c.history, "\n")), os.ModePerm); err != nil {
+ return err
+ }
+ c.jsre.Stop(graceful)
+ return nil
+}
+
+// jsErrorString adds a backtrace to errors generated by otto.
+func jsErrorString(err error) string {
+ if ottoErr, ok := err.(*otto.Error); ok {
+ return ottoErr.String()
+ }
+ return err.Error()
+}