mirror of
https://github.com/ethereum/go-ethereum.git
synced 2026-08-20 10:52:25 +00:00
Delete console directory
This commit is contained in:
parent
c4f01fc1f6
commit
cc5a40643c
6 changed files with 0 additions and 1594 deletions
|
|
@ -1,484 +0,0 @@
|
||||||
// Copyright 2016 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 (
|
|
||||||
"encoding/json"
|
|
||||||
"errors"
|
|
||||||
"fmt"
|
|
||||||
"io"
|
|
||||||
"reflect"
|
|
||||||
"strings"
|
|
||||||
"time"
|
|
||||||
|
|
||||||
"github.com/dop251/goja"
|
|
||||||
"github.com/ethereum/go-ethereum/accounts/scwallet"
|
|
||||||
"github.com/ethereum/go-ethereum/accounts/usbwallet"
|
|
||||||
"github.com/ethereum/go-ethereum/common/hexutil"
|
|
||||||
"github.com/ethereum/go-ethereum/console/prompt"
|
|
||||||
"github.com/ethereum/go-ethereum/internal/jsre"
|
|
||||||
"github.com/ethereum/go-ethereum/rpc"
|
|
||||||
)
|
|
||||||
|
|
||||||
// bridge is a collection of JavaScript utility methods to bride the .js runtime
|
|
||||||
// environment and the Go RPC connection backing the remote method calls.
|
|
||||||
type bridge struct {
|
|
||||||
client *rpc.Client // RPC client to execute Ethereum requests through
|
|
||||||
prompter prompt.UserPrompter // Input prompter to allow interactive user feedback
|
|
||||||
printer io.Writer // Output writer to serialize any display strings to
|
|
||||||
}
|
|
||||||
|
|
||||||
// newBridge creates a new JavaScript wrapper around an RPC client.
|
|
||||||
func newBridge(client *rpc.Client, prompter prompt.UserPrompter, printer io.Writer) *bridge {
|
|
||||||
return &bridge{
|
|
||||||
client: client,
|
|
||||||
prompter: prompter,
|
|
||||||
printer: printer,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func getJeth(vm *goja.Runtime) *goja.Object {
|
|
||||||
jeth := vm.Get("jeth")
|
|
||||||
if jeth == nil {
|
|
||||||
panic(vm.ToValue("jeth object does not exist"))
|
|
||||||
}
|
|
||||||
return jeth.ToObject(vm)
|
|
||||||
}
|
|
||||||
|
|
||||||
// NewAccount is a wrapper around the personal.newAccount RPC method that uses a
|
|
||||||
// non-echoing password prompt to acquire the passphrase and executes the original
|
|
||||||
// RPC method (saved in jeth.newAccount) with it to actually execute the RPC call.
|
|
||||||
func (b *bridge) NewAccount(call jsre.Call) (goja.Value, error) {
|
|
||||||
var (
|
|
||||||
password string
|
|
||||||
confirm string
|
|
||||||
err error
|
|
||||||
)
|
|
||||||
switch {
|
|
||||||
// No password was specified, prompt the user for it
|
|
||||||
case len(call.Arguments) == 0:
|
|
||||||
if password, err = b.prompter.PromptPassword("Passphrase: "); err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
if confirm, err = b.prompter.PromptPassword("Repeat passphrase: "); err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
if password != confirm {
|
|
||||||
return nil, errors.New("passwords don't match")
|
|
||||||
}
|
|
||||||
// A single string password was specified, use that
|
|
||||||
case len(call.Arguments) == 1 && call.Argument(0).ToString() != nil:
|
|
||||||
password = call.Argument(0).ToString().String()
|
|
||||||
default:
|
|
||||||
return nil, errors.New("expected 0 or 1 string argument")
|
|
||||||
}
|
|
||||||
// Password acquired, execute the call and return
|
|
||||||
newAccount, callable := goja.AssertFunction(getJeth(call.VM).Get("newAccount"))
|
|
||||||
if !callable {
|
|
||||||
return nil, errors.New("jeth.newAccount is not callable")
|
|
||||||
}
|
|
||||||
ret, err := newAccount(goja.Null(), call.VM.ToValue(password))
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
return ret, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// OpenWallet is a wrapper around personal.openWallet which can interpret and
|
|
||||||
// react to certain error messages, such as the Trezor PIN matrix request.
|
|
||||||
func (b *bridge) OpenWallet(call jsre.Call) (goja.Value, error) {
|
|
||||||
// Make sure we have a wallet specified to open
|
|
||||||
if call.Argument(0).ToObject(call.VM).ClassName() != "String" {
|
|
||||||
return nil, errors.New("first argument must be the wallet URL to open")
|
|
||||||
}
|
|
||||||
wallet := call.Argument(0)
|
|
||||||
|
|
||||||
var passwd goja.Value
|
|
||||||
if goja.IsUndefined(call.Argument(1)) || goja.IsNull(call.Argument(1)) {
|
|
||||||
passwd = call.VM.ToValue("")
|
|
||||||
} else {
|
|
||||||
passwd = call.Argument(1)
|
|
||||||
}
|
|
||||||
// Open the wallet and return if successful in itself
|
|
||||||
openWallet, callable := goja.AssertFunction(getJeth(call.VM).Get("openWallet"))
|
|
||||||
if !callable {
|
|
||||||
return nil, errors.New("jeth.openWallet is not callable")
|
|
||||||
}
|
|
||||||
val, err := openWallet(goja.Null(), wallet, passwd)
|
|
||||||
if err == nil {
|
|
||||||
return val, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// Wallet open failed, report error unless it's a PIN or PUK entry
|
|
||||||
switch {
|
|
||||||
case strings.HasSuffix(err.Error(), usbwallet.ErrTrezorPINNeeded.Error()):
|
|
||||||
val, err = b.readPinAndReopenWallet(call)
|
|
||||||
if err == nil {
|
|
||||||
return val, nil
|
|
||||||
}
|
|
||||||
val, err = b.readPassphraseAndReopenWallet(call)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
case strings.HasSuffix(err.Error(), scwallet.ErrPairingPasswordNeeded.Error()):
|
|
||||||
// PUK input requested, fetch from the user and call open again
|
|
||||||
input, err := b.prompter.PromptPassword("Please enter the pairing password: ")
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
passwd = call.VM.ToValue(input)
|
|
||||||
if val, err = openWallet(goja.Null(), wallet, passwd); err != nil {
|
|
||||||
if !strings.HasSuffix(err.Error(), scwallet.ErrPINNeeded.Error()) {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
// PIN input requested, fetch from the user and call open again
|
|
||||||
input, err := b.prompter.PromptPassword("Please enter current PIN: ")
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
if val, err = openWallet(goja.Null(), wallet, call.VM.ToValue(input)); err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
case strings.HasSuffix(err.Error(), scwallet.ErrPINUnblockNeeded.Error()):
|
|
||||||
// PIN unblock requested, fetch PUK and new PIN from the user
|
|
||||||
var pukpin string
|
|
||||||
input, err := b.prompter.PromptPassword("Please enter current PUK: ")
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
pukpin = input
|
|
||||||
input, err = b.prompter.PromptPassword("Please enter new PIN: ")
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
pukpin += input
|
|
||||||
|
|
||||||
if val, err = openWallet(goja.Null(), wallet, call.VM.ToValue(pukpin)); err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
case strings.HasSuffix(err.Error(), scwallet.ErrPINNeeded.Error()):
|
|
||||||
// PIN input requested, fetch from the user and call open again
|
|
||||||
input, err := b.prompter.PromptPassword("Please enter current PIN: ")
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
if val, err = openWallet(goja.Null(), wallet, call.VM.ToValue(input)); err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
default:
|
|
||||||
// Unknown error occurred, drop to the user
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
return val, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (b *bridge) readPassphraseAndReopenWallet(call jsre.Call) (goja.Value, error) {
|
|
||||||
wallet := call.Argument(0)
|
|
||||||
input, err := b.prompter.PromptPassword("Please enter your passphrase: ")
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
openWallet, callable := goja.AssertFunction(getJeth(call.VM).Get("openWallet"))
|
|
||||||
if !callable {
|
|
||||||
return nil, errors.New("jeth.openWallet is not callable")
|
|
||||||
}
|
|
||||||
return openWallet(goja.Null(), wallet, call.VM.ToValue(input))
|
|
||||||
}
|
|
||||||
|
|
||||||
func (b *bridge) readPinAndReopenWallet(call jsre.Call) (goja.Value, error) {
|
|
||||||
wallet := call.Argument(0)
|
|
||||||
// Trezor PIN matrix input requested, display the matrix to the user and fetch the data
|
|
||||||
fmt.Fprintf(b.printer, "Look at the device for number positions\n\n")
|
|
||||||
fmt.Fprintf(b.printer, "7 | 8 | 9\n")
|
|
||||||
fmt.Fprintf(b.printer, "--+---+--\n")
|
|
||||||
fmt.Fprintf(b.printer, "4 | 5 | 6\n")
|
|
||||||
fmt.Fprintf(b.printer, "--+---+--\n")
|
|
||||||
fmt.Fprintf(b.printer, "1 | 2 | 3\n\n")
|
|
||||||
|
|
||||||
input, err := b.prompter.PromptPassword("Please enter current PIN: ")
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
openWallet, callable := goja.AssertFunction(getJeth(call.VM).Get("openWallet"))
|
|
||||||
if !callable {
|
|
||||||
return nil, errors.New("jeth.openWallet is not callable")
|
|
||||||
}
|
|
||||||
return openWallet(goja.Null(), wallet, call.VM.ToValue(input))
|
|
||||||
}
|
|
||||||
|
|
||||||
// UnlockAccount is a wrapper around the personal.unlockAccount RPC method that
|
|
||||||
// uses a non-echoing password prompt to acquire the passphrase and executes the
|
|
||||||
// original RPC method (saved in jeth.unlockAccount) with it to actually execute
|
|
||||||
// the RPC call.
|
|
||||||
func (b *bridge) UnlockAccount(call jsre.Call) (goja.Value, error) {
|
|
||||||
if len(call.Arguments) < 1 {
|
|
||||||
return nil, errors.New("usage: unlockAccount(account, [ password, duration ])")
|
|
||||||
}
|
|
||||||
|
|
||||||
account := call.Argument(0)
|
|
||||||
// Make sure we have an account specified to unlock.
|
|
||||||
if goja.IsUndefined(account) || goja.IsNull(account) || account.ExportType().Kind() != reflect.String {
|
|
||||||
return nil, errors.New("first argument must be the account to unlock")
|
|
||||||
}
|
|
||||||
|
|
||||||
// If password is not given or is the null value, prompt the user for it.
|
|
||||||
var passwd goja.Value
|
|
||||||
if goja.IsUndefined(call.Argument(1)) || goja.IsNull(call.Argument(1)) {
|
|
||||||
fmt.Fprintf(b.printer, "Unlock account %s\n", account)
|
|
||||||
input, err := b.prompter.PromptPassword("Passphrase: ")
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
passwd = call.VM.ToValue(input)
|
|
||||||
} else {
|
|
||||||
if call.Argument(1).ExportType().Kind() != reflect.String {
|
|
||||||
return nil, errors.New("password must be a string")
|
|
||||||
}
|
|
||||||
passwd = call.Argument(1)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Third argument is the duration how long the account should be unlocked.
|
|
||||||
duration := goja.Null()
|
|
||||||
if !goja.IsUndefined(call.Argument(2)) && !goja.IsNull(call.Argument(2)) {
|
|
||||||
if !isNumber(call.Argument(2)) {
|
|
||||||
return nil, errors.New("unlock duration must be a number")
|
|
||||||
}
|
|
||||||
duration = call.Argument(2)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Send the request to the backend and return.
|
|
||||||
unlockAccount, callable := goja.AssertFunction(getJeth(call.VM).Get("unlockAccount"))
|
|
||||||
if !callable {
|
|
||||||
return nil, errors.New("jeth.unlockAccount is not callable")
|
|
||||||
}
|
|
||||||
return unlockAccount(goja.Null(), account, passwd, duration)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Sign is a wrapper around the personal.sign RPC method that uses a non-echoing password
|
|
||||||
// prompt to acquire the passphrase and executes the original RPC method (saved in
|
|
||||||
// jeth.sign) with it to actually execute the RPC call.
|
|
||||||
func (b *bridge) Sign(call jsre.Call) (goja.Value, error) {
|
|
||||||
if nArgs := len(call.Arguments); nArgs < 2 {
|
|
||||||
return nil, errors.New("usage: sign(message, account, [ password ])")
|
|
||||||
}
|
|
||||||
var (
|
|
||||||
message = call.Argument(0)
|
|
||||||
account = call.Argument(1)
|
|
||||||
passwd = call.Argument(2)
|
|
||||||
)
|
|
||||||
|
|
||||||
if goja.IsUndefined(message) || message.ExportType().Kind() != reflect.String {
|
|
||||||
return nil, errors.New("first argument must be the message to sign")
|
|
||||||
}
|
|
||||||
if goja.IsUndefined(account) || account.ExportType().Kind() != reflect.String {
|
|
||||||
return nil, errors.New("second argument must be the account to sign with")
|
|
||||||
}
|
|
||||||
|
|
||||||
// if the password is not given or null ask the user and ensure password is a string
|
|
||||||
if goja.IsUndefined(passwd) || goja.IsNull(passwd) {
|
|
||||||
fmt.Fprintf(b.printer, "Give password for account %s\n", account)
|
|
||||||
input, err := b.prompter.PromptPassword("Password: ")
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
passwd = call.VM.ToValue(input)
|
|
||||||
} else if passwd.ExportType().Kind() != reflect.String {
|
|
||||||
return nil, errors.New("third argument must be the password to unlock the account")
|
|
||||||
}
|
|
||||||
|
|
||||||
// Send the request to the backend and return
|
|
||||||
sign, callable := goja.AssertFunction(getJeth(call.VM).Get("sign"))
|
|
||||||
if !callable {
|
|
||||||
return nil, errors.New("jeth.sign is not callable")
|
|
||||||
}
|
|
||||||
return sign(goja.Null(), message, account, passwd)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Sleep will block the console for the specified number of seconds.
|
|
||||||
func (b *bridge) Sleep(call jsre.Call) (goja.Value, error) {
|
|
||||||
if nArgs := len(call.Arguments); nArgs < 1 {
|
|
||||||
return nil, errors.New("usage: sleep(<number of seconds>)")
|
|
||||||
}
|
|
||||||
sleepObj := call.Argument(0)
|
|
||||||
if goja.IsUndefined(sleepObj) || goja.IsNull(sleepObj) || !isNumber(sleepObj) {
|
|
||||||
return nil, errors.New("usage: sleep(<number of seconds>)")
|
|
||||||
}
|
|
||||||
sleep := sleepObj.ToFloat()
|
|
||||||
time.Sleep(time.Duration(sleep * float64(time.Second)))
|
|
||||||
return call.VM.ToValue(true), nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// SleepBlocks will block the console for a specified number of new blocks optionally
|
|
||||||
// until the given timeout is reached.
|
|
||||||
func (b *bridge) SleepBlocks(call jsre.Call) (goja.Value, error) {
|
|
||||||
// Parse the input parameters for the sleep.
|
|
||||||
var (
|
|
||||||
blocks = int64(0)
|
|
||||||
sleep = int64(9999999999999999) // indefinitely
|
|
||||||
)
|
|
||||||
nArgs := len(call.Arguments)
|
|
||||||
if nArgs == 0 {
|
|
||||||
return nil, errors.New("usage: sleepBlocks(<n blocks>[, max sleep in seconds])")
|
|
||||||
}
|
|
||||||
if nArgs >= 1 {
|
|
||||||
if goja.IsNull(call.Argument(0)) || goja.IsUndefined(call.Argument(0)) || !isNumber(call.Argument(0)) {
|
|
||||||
return nil, errors.New("expected number as first argument")
|
|
||||||
}
|
|
||||||
blocks = call.Argument(0).ToInteger()
|
|
||||||
}
|
|
||||||
if nArgs >= 2 {
|
|
||||||
if goja.IsNull(call.Argument(1)) || goja.IsUndefined(call.Argument(1)) || !isNumber(call.Argument(1)) {
|
|
||||||
return nil, errors.New("expected number as second argument")
|
|
||||||
}
|
|
||||||
sleep = call.Argument(1).ToInteger()
|
|
||||||
}
|
|
||||||
|
|
||||||
// Poll the current block number until either it or a timeout is reached.
|
|
||||||
deadline := time.Now().Add(time.Duration(sleep) * time.Second)
|
|
||||||
var lastNumber hexutil.Uint64
|
|
||||||
if err := b.client.Call(&lastNumber, "eth_blockNumber"); err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
for time.Now().Before(deadline) {
|
|
||||||
var number hexutil.Uint64
|
|
||||||
if err := b.client.Call(&number, "eth_blockNumber"); err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
if number != lastNumber {
|
|
||||||
lastNumber = number
|
|
||||||
blocks--
|
|
||||||
}
|
|
||||||
if blocks <= 0 {
|
|
||||||
break
|
|
||||||
}
|
|
||||||
time.Sleep(time.Second)
|
|
||||||
}
|
|
||||||
return call.VM.ToValue(true), nil
|
|
||||||
}
|
|
||||||
|
|
||||||
type jsonrpcCall struct {
|
|
||||||
ID int64
|
|
||||||
Method string
|
|
||||||
Params []interface{}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Send implements the web3 provider "send" method.
|
|
||||||
func (b *bridge) Send(call jsre.Call) (goja.Value, error) {
|
|
||||||
// Remarshal the request into a Go value.
|
|
||||||
reqVal, err := call.Argument(0).ToObject(call.VM).MarshalJSON()
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
var (
|
|
||||||
rawReq = string(reqVal)
|
|
||||||
dec = json.NewDecoder(strings.NewReader(rawReq))
|
|
||||||
reqs []jsonrpcCall
|
|
||||||
batch bool
|
|
||||||
)
|
|
||||||
dec.UseNumber() // avoid float64s
|
|
||||||
if rawReq[0] == '[' {
|
|
||||||
batch = true
|
|
||||||
dec.Decode(&reqs)
|
|
||||||
} else {
|
|
||||||
batch = false
|
|
||||||
reqs = make([]jsonrpcCall, 1)
|
|
||||||
dec.Decode(&reqs[0])
|
|
||||||
}
|
|
||||||
|
|
||||||
// Execute the requests.
|
|
||||||
var resps []*goja.Object
|
|
||||||
for _, req := range reqs {
|
|
||||||
resp := call.VM.NewObject()
|
|
||||||
resp.Set("jsonrpc", "2.0")
|
|
||||||
resp.Set("id", req.ID)
|
|
||||||
|
|
||||||
var result json.RawMessage
|
|
||||||
if err = b.client.Call(&result, req.Method, req.Params...); err == nil {
|
|
||||||
if result == nil {
|
|
||||||
// Special case null because it is decoded as an empty
|
|
||||||
// raw message for some reason.
|
|
||||||
resp.Set("result", goja.Null())
|
|
||||||
} else {
|
|
||||||
JSON := call.VM.Get("JSON").ToObject(call.VM)
|
|
||||||
parse, callable := goja.AssertFunction(JSON.Get("parse"))
|
|
||||||
if !callable {
|
|
||||||
return nil, errors.New("JSON.parse is not a function")
|
|
||||||
}
|
|
||||||
resultVal, err := parse(goja.Null(), call.VM.ToValue(string(result)))
|
|
||||||
if err != nil {
|
|
||||||
setError(resp, -32603, err.Error(), nil)
|
|
||||||
} else {
|
|
||||||
resp.Set("result", resultVal)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
code := -32603
|
|
||||||
var data interface{}
|
|
||||||
if err, ok := err.(rpc.Error); ok {
|
|
||||||
code = err.ErrorCode()
|
|
||||||
}
|
|
||||||
if err, ok := err.(rpc.DataError); ok {
|
|
||||||
data = err.ErrorData()
|
|
||||||
}
|
|
||||||
setError(resp, code, err.Error(), data)
|
|
||||||
}
|
|
||||||
resps = append(resps, resp)
|
|
||||||
}
|
|
||||||
// Return the responses either to the callback (if supplied)
|
|
||||||
// or directly as the return value.
|
|
||||||
var result goja.Value
|
|
||||||
if batch {
|
|
||||||
result = call.VM.ToValue(resps)
|
|
||||||
} else {
|
|
||||||
result = resps[0]
|
|
||||||
}
|
|
||||||
if fn, isFunc := goja.AssertFunction(call.Argument(1)); isFunc {
|
|
||||||
fn(goja.Null(), goja.Null(), result)
|
|
||||||
return goja.Undefined(), nil
|
|
||||||
}
|
|
||||||
return result, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func setError(resp *goja.Object, code int, msg string, data interface{}) {
|
|
||||||
err := make(map[string]interface{})
|
|
||||||
err["code"] = code
|
|
||||||
err["message"] = msg
|
|
||||||
if data != nil {
|
|
||||||
err["data"] = data
|
|
||||||
}
|
|
||||||
resp.Set("error", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
// isNumber returns true if input value is a JS number.
|
|
||||||
func isNumber(v goja.Value) bool {
|
|
||||||
k := v.ExportType().Kind()
|
|
||||||
return k >= reflect.Int && k <= reflect.Float64
|
|
||||||
}
|
|
||||||
|
|
||||||
func getObject(vm *goja.Runtime, name string) *goja.Object {
|
|
||||||
v := vm.Get(name)
|
|
||||||
if v == nil {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
return v.ToObject(vm)
|
|
||||||
}
|
|
||||||
|
|
@ -1,48 +0,0 @@
|
||||||
// Copyright 2020 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 (
|
|
||||||
"testing"
|
|
||||||
|
|
||||||
"github.com/dop251/goja"
|
|
||||||
"github.com/ethereum/go-ethereum/internal/jsre"
|
|
||||||
)
|
|
||||||
|
|
||||||
// TestUndefinedAsParam ensures that personal functions can receive
|
|
||||||
// `undefined` as a parameter.
|
|
||||||
func TestUndefinedAsParam(t *testing.T) {
|
|
||||||
b := bridge{}
|
|
||||||
call := jsre.Call{}
|
|
||||||
call.Arguments = []goja.Value{goja.Undefined()}
|
|
||||||
|
|
||||||
b.UnlockAccount(call)
|
|
||||||
b.Sign(call)
|
|
||||||
b.Sleep(call)
|
|
||||||
}
|
|
||||||
|
|
||||||
// TestNullAsParam ensures that personal functions can receive
|
|
||||||
// `null` as a parameter.
|
|
||||||
func TestNullAsParam(t *testing.T) {
|
|
||||||
b := bridge{}
|
|
||||||
call := jsre.Call{}
|
|
||||||
call.Arguments = []goja.Value{goja.Null()}
|
|
||||||
|
|
||||||
b.UnlockAccount(call)
|
|
||||||
b.Sign(call)
|
|
||||||
b.Sleep(call)
|
|
||||||
}
|
|
||||||
|
|
@ -1,567 +0,0 @@
|
||||||
// Copyright 2016 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 (
|
|
||||||
"errors"
|
|
||||||
"fmt"
|
|
||||||
"io"
|
|
||||||
"os"
|
|
||||||
"os/signal"
|
|
||||||
"path/filepath"
|
|
||||||
"regexp"
|
|
||||||
"sort"
|
|
||||||
"strings"
|
|
||||||
"sync"
|
|
||||||
"syscall"
|
|
||||||
|
|
||||||
"github.com/dop251/goja"
|
|
||||||
"github.com/ethereum/go-ethereum/console/prompt"
|
|
||||||
"github.com/ethereum/go-ethereum/internal/jsre"
|
|
||||||
"github.com/ethereum/go-ethereum/internal/jsre/deps"
|
|
||||||
"github.com/ethereum/go-ethereum/internal/web3ext"
|
|
||||||
"github.com/ethereum/go-ethereum/log"
|
|
||||||
"github.com/ethereum/go-ethereum/rpc"
|
|
||||||
"github.com/mattn/go-colorable"
|
|
||||||
"github.com/peterh/liner"
|
|
||||||
)
|
|
||||||
|
|
||||||
var (
|
|
||||||
// u: unlock, s: signXX, sendXX, n: newAccount, i: importXX
|
|
||||||
passwordRegexp = regexp.MustCompile(`personal.[nusi]`)
|
|
||||||
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 the 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 prompt.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 fledged
|
|
||||||
// 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 prompt.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
|
|
||||||
|
|
||||||
interactiveStopped chan struct{}
|
|
||||||
stopInteractiveCh chan struct{}
|
|
||||||
signalReceived chan struct{}
|
|
||||||
stopped chan struct{}
|
|
||||||
wg sync.WaitGroup
|
|
||||||
stopOnce sync.Once
|
|
||||||
}
|
|
||||||
|
|
||||||
// New initializes a JavaScript interpreted runtime environment and sets defaults
|
|
||||||
// with the config struct.
|
|
||||||
func New(config Config) (*Console, error) {
|
|
||||||
// Handle unset config values gracefully
|
|
||||||
if config.Prompter == nil {
|
|
||||||
config.Prompter = prompt.Stdin
|
|
||||||
}
|
|
||||||
if config.Prompt == "" {
|
|
||||||
config.Prompt = DefaultPrompt
|
|
||||||
}
|
|
||||||
if config.Printer == nil {
|
|
||||||
config.Printer = colorable.NewColorableStdout()
|
|
||||||
}
|
|
||||||
|
|
||||||
// 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),
|
|
||||||
interactiveStopped: make(chan struct{}),
|
|
||||||
stopInteractiveCh: make(chan struct{}),
|
|
||||||
signalReceived: make(chan struct{}, 1),
|
|
||||||
stopped: make(chan struct{}),
|
|
||||||
}
|
|
||||||
if err := os.MkdirAll(config.DataDir, 0700); err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
if err := console.init(config.Preload); err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
console.wg.Add(1)
|
|
||||||
go console.interruptHandler()
|
|
||||||
|
|
||||||
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 {
|
|
||||||
c.initConsoleObject()
|
|
||||||
|
|
||||||
// Initialize the JavaScript <-> Go RPC bridge.
|
|
||||||
bridge := newBridge(c.client, c.prompter, c.printer)
|
|
||||||
if err := c.initWeb3(bridge); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
if err := c.initExtensions(); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
// Add bridge overrides for web3.js functionality.
|
|
||||||
c.jsre.Do(func(vm *goja.Runtime) {
|
|
||||||
c.initAdmin(vm, bridge)
|
|
||||||
c.initPersonal(vm, bridge)
|
|
||||||
})
|
|
||||||
|
|
||||||
// Preload JavaScript files.
|
|
||||||
for _, path := range preload {
|
|
||||||
if err := c.jsre.Exec(path); err != nil {
|
|
||||||
failure := err.Error()
|
|
||||||
if gojaErr, ok := err.(*goja.Exception); ok {
|
|
||||||
failure = gojaErr.String()
|
|
||||||
}
|
|
||||||
return fmt.Errorf("%s: %v", path, failure)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Configure the input prompter for history and tab completion.
|
|
||||||
if c.prompter != nil {
|
|
||||||
if content, err := os.ReadFile(c.histPath); err != nil {
|
|
||||||
c.prompter.SetHistory(nil)
|
|
||||||
} else {
|
|
||||||
c.history = strings.Split(string(content), "\n")
|
|
||||||
c.prompter.SetHistory(c.history)
|
|
||||||
}
|
|
||||||
c.prompter.SetWordCompleter(c.AutoCompleteInput)
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c *Console) initConsoleObject() {
|
|
||||||
c.jsre.Do(func(vm *goja.Runtime) {
|
|
||||||
console := vm.NewObject()
|
|
||||||
console.Set("log", c.consoleOutput)
|
|
||||||
console.Set("error", c.consoleOutput)
|
|
||||||
vm.Set("console", console)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c *Console) initWeb3(bridge *bridge) error {
|
|
||||||
if err := c.jsre.Compile("bignumber.js", deps.BigNumberJS); err != nil {
|
|
||||||
return fmt.Errorf("bignumber.js: %v", err)
|
|
||||||
}
|
|
||||||
if err := c.jsre.Compile("web3.js", deps.Web3JS); 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)
|
|
||||||
}
|
|
||||||
var err error
|
|
||||||
c.jsre.Do(func(vm *goja.Runtime) {
|
|
||||||
transport := vm.NewObject()
|
|
||||||
transport.Set("send", jsre.MakeCallback(vm, bridge.Send))
|
|
||||||
transport.Set("sendAsync", jsre.MakeCallback(vm, bridge.Send))
|
|
||||||
vm.Set("_consoleWeb3Transport", transport)
|
|
||||||
_, err = vm.RunString("var web3 = new Web3(_consoleWeb3Transport)")
|
|
||||||
})
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
var defaultAPIs = map[string]string{"eth": "1.0", "net": "1.0", "debug": "1.0"}
|
|
||||||
|
|
||||||
// initExtensions loads and registers web3.js extensions.
|
|
||||||
func (c *Console) initExtensions() error {
|
|
||||||
const methodNotFound = -32601
|
|
||||||
apis, err := c.client.SupportedModules()
|
|
||||||
if err != nil {
|
|
||||||
if rpcErr, ok := err.(rpc.Error); ok && rpcErr.ErrorCode() == methodNotFound {
|
|
||||||
log.Warn("Server does not support method rpc_modules, using default API list.")
|
|
||||||
apis = defaultAPIs
|
|
||||||
} else {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Compute aliases from server-provided modules.
|
|
||||||
aliases := map[string]struct{}{"eth": {}}
|
|
||||||
for api := range apis {
|
|
||||||
if api == "web3" {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
aliases[api] = struct{}{}
|
|
||||||
if file, ok := web3ext.Modules[api]; ok {
|
|
||||||
if err = c.jsre.Compile(api+".js", file); err != nil {
|
|
||||||
return fmt.Errorf("%s.js: %v", api, err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Apply aliases.
|
|
||||||
c.jsre.Do(func(vm *goja.Runtime) {
|
|
||||||
web3 := getObject(vm, "web3")
|
|
||||||
for name := range aliases {
|
|
||||||
if v := web3.Get(name); v != nil {
|
|
||||||
vm.Set(name, v)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
})
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// initAdmin creates additional admin APIs implemented by the bridge.
|
|
||||||
func (c *Console) initAdmin(vm *goja.Runtime, bridge *bridge) {
|
|
||||||
if admin := getObject(vm, "admin"); admin != nil {
|
|
||||||
admin.Set("sleepBlocks", jsre.MakeCallback(vm, bridge.SleepBlocks))
|
|
||||||
admin.Set("sleep", jsre.MakeCallback(vm, bridge.Sleep))
|
|
||||||
admin.Set("clearHistory", c.clearHistory)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// initPersonal redirects account-related API methods through the bridge.
|
|
||||||
//
|
|
||||||
// If the console is in interactive mode and the 'personal' API is available, override
|
|
||||||
// the openWallet, unlockAccount, newAccount and sign methods since these require user
|
|
||||||
// interaction. The original web3 callbacks are stored in 'jeth'. These will be called
|
|
||||||
// by the bridge after the prompt and send the original web3 request to the backend.
|
|
||||||
func (c *Console) initPersonal(vm *goja.Runtime, bridge *bridge) {
|
|
||||||
personal := getObject(vm, "personal")
|
|
||||||
if personal == nil || c.prompter == nil {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
log.Warn("Enabling deprecated personal namespace")
|
|
||||||
jeth := vm.NewObject()
|
|
||||||
vm.Set("jeth", jeth)
|
|
||||||
jeth.Set("openWallet", personal.Get("openWallet"))
|
|
||||||
jeth.Set("unlockAccount", personal.Get("unlockAccount"))
|
|
||||||
jeth.Set("newAccount", personal.Get("newAccount"))
|
|
||||||
jeth.Set("sign", personal.Get("sign"))
|
|
||||||
personal.Set("openWallet", jsre.MakeCallback(vm, bridge.OpenWallet))
|
|
||||||
personal.Set("unlockAccount", jsre.MakeCallback(vm, bridge.UnlockAccount))
|
|
||||||
personal.Set("newAccount", jsre.MakeCallback(vm, bridge.NewAccount))
|
|
||||||
personal.Set("sign", jsre.MakeCallback(vm, bridge.Sign))
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c *Console) clearHistory() {
|
|
||||||
c.history = nil
|
|
||||||
c.prompter.ClearHistory()
|
|
||||||
if err := os.Remove(c.histPath); err != nil {
|
|
||||||
fmt.Fprintln(c.printer, "can't delete history file:", err)
|
|
||||||
} else {
|
|
||||||
fmt.Fprintln(c.printer, "history file deleted")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 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 goja.FunctionCall) goja.Value {
|
|
||||||
var output []string
|
|
||||||
for _, argument := range call.Arguments {
|
|
||||||
output = append(output, fmt.Sprintf("%v", argument))
|
|
||||||
}
|
|
||||||
fmt.Fprintln(c.printer, strings.Join(output, " "))
|
|
||||||
return goja.Null()
|
|
||||||
}
|
|
||||||
|
|
||||||
// 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, ""
|
|
||||||
}
|
|
||||||
// Chunk data to relevant part for autocompletion
|
|
||||||
// E.g. in case of nested lines eth.getBalance(eth.coinb<tab><tab>
|
|
||||||
start := pos - 1
|
|
||||||
for ; start > 0; start-- {
|
|
||||||
// Skip all methods and namespaces (i.e. including the dot)
|
|
||||||
c := line[start]
|
|
||||||
if c == '.' || (c >= 'a' && c <= 'z') || (c >= 'A' && c <= 'Z') || (c >= '1' && c <= '9') {
|
|
||||||
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() {
|
|
||||||
message := "Welcome to the Geth JavaScript console!\n\n"
|
|
||||||
|
|
||||||
// Print some generic Geth metadata
|
|
||||||
if res, err := c.jsre.Run(`
|
|
||||||
var message = "instance: " + web3.version.node + "\n";
|
|
||||||
try {
|
|
||||||
message += "coinbase: " + eth.coinbase + "\n";
|
|
||||||
} catch (err) {}
|
|
||||||
message += "at block: " + eth.blockNumber + " (" + new Date(1000 * eth.getBlock(eth.blockNumber).timestamp) + ")\n";
|
|
||||||
try {
|
|
||||||
message += " datadir: " + admin.datadir + "\n";
|
|
||||||
} catch (err) {}
|
|
||||||
message
|
|
||||||
`); err == nil {
|
|
||||||
message += res.String()
|
|
||||||
}
|
|
||||||
// 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)
|
|
||||||
message += " modules: " + strings.Join(modules, " ") + "\n"
|
|
||||||
}
|
|
||||||
message += "\nTo exit, press ctrl-d or type exit"
|
|
||||||
fmt.Fprintln(c.printer, message)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Evaluate executes code and pretty prints the result to the specified output
|
|
||||||
// stream.
|
|
||||||
func (c *Console) Evaluate(statement string) {
|
|
||||||
defer func() {
|
|
||||||
if r := recover(); r != nil {
|
|
||||||
fmt.Fprintf(c.printer, "[native] error: %v\n", r)
|
|
||||||
}
|
|
||||||
}()
|
|
||||||
c.jsre.Evaluate(statement, c.printer)
|
|
||||||
|
|
||||||
// Avoid exiting Interactive when jsre was interrupted by SIGINT.
|
|
||||||
c.clearSignalReceived()
|
|
||||||
}
|
|
||||||
|
|
||||||
// interruptHandler runs in its own goroutine and waits for signals.
|
|
||||||
// When a signal is received, it interrupts the JS interpreter.
|
|
||||||
func (c *Console) interruptHandler() {
|
|
||||||
defer c.wg.Done()
|
|
||||||
|
|
||||||
// During Interactive, liner inhibits the signal while it is prompting for
|
|
||||||
// input. However, the signal will be received while evaluating JS.
|
|
||||||
//
|
|
||||||
// On unsupported terminals, SIGINT can also happen while prompting.
|
|
||||||
// Unfortunately, it is not possible to abort the prompt in this case and
|
|
||||||
// the c.readLines goroutine leaks.
|
|
||||||
sig := make(chan os.Signal, 1)
|
|
||||||
signal.Notify(sig, syscall.SIGINT)
|
|
||||||
defer signal.Stop(sig)
|
|
||||||
|
|
||||||
for {
|
|
||||||
select {
|
|
||||||
case <-sig:
|
|
||||||
c.setSignalReceived()
|
|
||||||
c.jsre.Interrupt(errors.New("interrupted"))
|
|
||||||
case <-c.stopInteractiveCh:
|
|
||||||
close(c.interactiveStopped)
|
|
||||||
c.jsre.Interrupt(errors.New("interrupted"))
|
|
||||||
case <-c.stopped:
|
|
||||||
return
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c *Console) setSignalReceived() {
|
|
||||||
select {
|
|
||||||
case c.signalReceived <- struct{}{}:
|
|
||||||
default:
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c *Console) clearSignalReceived() {
|
|
||||||
select {
|
|
||||||
case <-c.signalReceived:
|
|
||||||
default:
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// StopInteractive causes Interactive to return as soon as possible.
|
|
||||||
func (c *Console) StopInteractive() {
|
|
||||||
select {
|
|
||||||
case c.stopInteractiveCh <- struct{}{}:
|
|
||||||
case <-c.stopped:
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Interactive starts an interactive user session, where input is prompted from
|
|
||||||
// the configured user prompter.
|
|
||||||
func (c *Console) Interactive() {
|
|
||||||
var (
|
|
||||||
prompt = c.prompt // the current prompt line (used for multi-line inputs)
|
|
||||||
indents = 0 // the current number of input indents (used for multi-line inputs)
|
|
||||||
input = "" // the current user input
|
|
||||||
inputLine = make(chan string, 1) // receives user input
|
|
||||||
inputErr = make(chan error, 1) // receives liner errors
|
|
||||||
requestLine = make(chan string) // requests a line of input
|
|
||||||
)
|
|
||||||
|
|
||||||
defer func() {
|
|
||||||
c.writeHistory()
|
|
||||||
}()
|
|
||||||
|
|
||||||
// The line reader runs in a separate goroutine.
|
|
||||||
go c.readLines(inputLine, inputErr, requestLine)
|
|
||||||
defer close(requestLine)
|
|
||||||
|
|
||||||
for {
|
|
||||||
// Send the next prompt, triggering an input read.
|
|
||||||
requestLine <- prompt
|
|
||||||
|
|
||||||
select {
|
|
||||||
case <-c.interactiveStopped:
|
|
||||||
fmt.Fprintln(c.printer, "node is down, exiting console")
|
|
||||||
return
|
|
||||||
|
|
||||||
case <-c.signalReceived:
|
|
||||||
// SIGINT received while prompting for input -> unsupported terminal.
|
|
||||||
// I'm not sure if the best choice would be to leave the console running here.
|
|
||||||
// Bash keeps running in this case. node.js does not.
|
|
||||||
fmt.Fprintln(c.printer, "caught interrupt, exiting")
|
|
||||||
return
|
|
||||||
|
|
||||||
case err := <-inputErr:
|
|
||||||
if err == liner.ErrPromptAborted {
|
|
||||||
// When prompting for multi-line input, the first Ctrl-C resets
|
|
||||||
// the multi-line state.
|
|
||||||
prompt, indents, input = c.prompt, 0, ""
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
return
|
|
||||||
|
|
||||||
case line := <-inputLine:
|
|
||||||
// User input was returned by the prompter, handle special cases.
|
|
||||||
if indents <= 0 && exit.MatchString(line) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
if onlyWhitespace.MatchString(line) {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
// Append the line to the input and check for multi-line interpretation.
|
|
||||||
input += line + "\n"
|
|
||||||
indents = countIndents(input)
|
|
||||||
if indents <= 0 {
|
|
||||||
prompt = c.prompt
|
|
||||||
} else {
|
|
||||||
prompt = strings.Repeat(".", indents*3) + " "
|
|
||||||
}
|
|
||||||
// If all the needed lines are present, save the command and run it.
|
|
||||||
if indents <= 0 {
|
|
||||||
if len(input) > 0 && input[0] != ' ' && !passwordRegexp.MatchString(input) {
|
|
||||||
if command := strings.TrimSpace(input); len(c.history) == 0 || command != c.history[len(c.history)-1] {
|
|
||||||
c.history = append(c.history, command)
|
|
||||||
if c.prompter != nil {
|
|
||||||
c.prompter.AppendHistory(command)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
c.Evaluate(input)
|
|
||||||
input = ""
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// readLines runs in its own goroutine, prompting for input.
|
|
||||||
func (c *Console) readLines(input chan<- string, errc chan<- error, prompt <-chan string) {
|
|
||||||
for p := range prompt {
|
|
||||||
line, err := c.prompter.PromptInput(p)
|
|
||||||
if err != nil {
|
|
||||||
errc <- err
|
|
||||||
} else {
|
|
||||||
input <- line
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// countIndents returns the number of indentations for the given input.
|
|
||||||
// In case of invalid input such as var a = } the result can be negative.
|
|
||||||
func countIndents(input string) int {
|
|
||||||
var (
|
|
||||||
indents = 0
|
|
||||||
inString = false
|
|
||||||
strOpenChar = ' ' // keep track of the string open char to allow var str = "I'm ....";
|
|
||||||
charEscaped = false // keep track if the previous char was the '\' char, allow var str = "abc\"def";
|
|
||||||
)
|
|
||||||
|
|
||||||
for _, c := range input {
|
|
||||||
switch c {
|
|
||||||
case '\\':
|
|
||||||
// indicate next char as escaped when in string and previous char isn't escaping this backslash
|
|
||||||
if !charEscaped && inString {
|
|
||||||
charEscaped = true
|
|
||||||
}
|
|
||||||
case '\'', '"':
|
|
||||||
if inString && !charEscaped && strOpenChar == c { // end string
|
|
||||||
inString = false
|
|
||||||
} else if !inString && !charEscaped { // begin string
|
|
||||||
inString = true
|
|
||||||
strOpenChar = c
|
|
||||||
}
|
|
||||||
charEscaped = false
|
|
||||||
case '{', '(':
|
|
||||||
if !inString { // ignore brackets when in string, allow var str = "a{"; without indenting
|
|
||||||
indents++
|
|
||||||
}
|
|
||||||
charEscaped = false
|
|
||||||
case '}', ')':
|
|
||||||
if !inString {
|
|
||||||
indents--
|
|
||||||
}
|
|
||||||
charEscaped = false
|
|
||||||
default:
|
|
||||||
charEscaped = false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return indents
|
|
||||||
}
|
|
||||||
|
|
||||||
// Stop cleans up the console and terminates the runtime environment.
|
|
||||||
func (c *Console) Stop(graceful bool) error {
|
|
||||||
c.stopOnce.Do(func() {
|
|
||||||
// Stop the interrupt handler.
|
|
||||||
close(c.stopped)
|
|
||||||
c.wg.Wait()
|
|
||||||
})
|
|
||||||
|
|
||||||
c.jsre.Stop(graceful)
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c *Console) writeHistory() error {
|
|
||||||
if err := os.WriteFile(c.histPath, []byte(strings.Join(c.history, "\n")), 0600); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
return os.Chmod(c.histPath, 0600) // Force 0600, even if it was different previously
|
|
||||||
}
|
|
||||||
|
|
@ -1,322 +0,0 @@
|
||||||
// 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 (
|
|
||||||
"bytes"
|
|
||||||
"errors"
|
|
||||||
"fmt"
|
|
||||||
"os"
|
|
||||||
"strings"
|
|
||||||
"testing"
|
|
||||||
"time"
|
|
||||||
|
|
||||||
"github.com/ethereum/go-ethereum/common"
|
|
||||||
"github.com/ethereum/go-ethereum/console/prompt"
|
|
||||||
"github.com/ethereum/go-ethereum/core"
|
|
||||||
"github.com/ethereum/go-ethereum/eth"
|
|
||||||
"github.com/ethereum/go-ethereum/eth/ethconfig"
|
|
||||||
"github.com/ethereum/go-ethereum/internal/jsre"
|
|
||||||
"github.com/ethereum/go-ethereum/miner"
|
|
||||||
"github.com/ethereum/go-ethereum/node"
|
|
||||||
)
|
|
||||||
|
|
||||||
const (
|
|
||||||
testInstance = "console-tester"
|
|
||||||
testAddress = "0x8605cdbbdb6d264aa742e77020dcbc58fcdce182"
|
|
||||||
)
|
|
||||||
|
|
||||||
// hookedPrompter implements UserPrompter to simulate use input via channels.
|
|
||||||
type hookedPrompter struct {
|
|
||||||
scheduler chan string
|
|
||||||
}
|
|
||||||
|
|
||||||
func (p *hookedPrompter) PromptInput(prompt string) (string, error) {
|
|
||||||
// Send the prompt to the tester
|
|
||||||
select {
|
|
||||||
case p.scheduler <- prompt:
|
|
||||||
case <-time.After(time.Second):
|
|
||||||
return "", errors.New("prompt timeout")
|
|
||||||
}
|
|
||||||
// Retrieve the response and feed to the console
|
|
||||||
select {
|
|
||||||
case input := <-p.scheduler:
|
|
||||||
return input, nil
|
|
||||||
case <-time.After(time.Second):
|
|
||||||
return "", errors.New("input timeout")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (p *hookedPrompter) PromptPassword(prompt string) (string, error) {
|
|
||||||
return "", errors.New("not implemented")
|
|
||||||
}
|
|
||||||
func (p *hookedPrompter) PromptConfirm(prompt string) (bool, error) {
|
|
||||||
return false, errors.New("not implemented")
|
|
||||||
}
|
|
||||||
func (p *hookedPrompter) SetHistory(history []string) {}
|
|
||||||
func (p *hookedPrompter) AppendHistory(command string) {}
|
|
||||||
func (p *hookedPrompter) ClearHistory() {}
|
|
||||||
func (p *hookedPrompter) SetWordCompleter(completer prompt.WordCompleter) {}
|
|
||||||
|
|
||||||
// tester is a console test environment for the console tests to operate on.
|
|
||||||
type tester struct {
|
|
||||||
workspace string
|
|
||||||
stack *node.Node
|
|
||||||
ethereum *eth.Ethereum
|
|
||||||
console *Console
|
|
||||||
input *hookedPrompter
|
|
||||||
output *bytes.Buffer
|
|
||||||
}
|
|
||||||
|
|
||||||
// newTester creates a test environment based on which the console can operate.
|
|
||||||
// Please ensure you call Close() on the returned tester to avoid leaks.
|
|
||||||
func newTester(t *testing.T, confOverride func(*ethconfig.Config)) *tester {
|
|
||||||
// Create a temporary storage for the node keys and initialize it
|
|
||||||
workspace := t.TempDir()
|
|
||||||
|
|
||||||
// Create a networkless protocol stack and start an Ethereum service within
|
|
||||||
stack, err := node.New(&node.Config{DataDir: workspace, UseLightweightKDF: true, Name: testInstance})
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("failed to create node: %v", err)
|
|
||||||
}
|
|
||||||
ethConf := ðconfig.Config{
|
|
||||||
Genesis: core.DeveloperGenesisBlock(11_500_000, nil),
|
|
||||||
Miner: miner.Config{
|
|
||||||
Etherbase: common.HexToAddress(testAddress),
|
|
||||||
},
|
|
||||||
}
|
|
||||||
if confOverride != nil {
|
|
||||||
confOverride(ethConf)
|
|
||||||
}
|
|
||||||
ethBackend, err := eth.New(stack, ethConf)
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("failed to register Ethereum protocol: %v", err)
|
|
||||||
}
|
|
||||||
// Start the node and assemble the JavaScript console around it
|
|
||||||
if err = stack.Start(); err != nil {
|
|
||||||
t.Fatalf("failed to start test stack: %v", err)
|
|
||||||
}
|
|
||||||
client := stack.Attach()
|
|
||||||
t.Cleanup(func() {
|
|
||||||
client.Close()
|
|
||||||
})
|
|
||||||
|
|
||||||
prompter := &hookedPrompter{scheduler: make(chan string)}
|
|
||||||
printer := new(bytes.Buffer)
|
|
||||||
|
|
||||||
console, err := New(Config{
|
|
||||||
DataDir: stack.DataDir(),
|
|
||||||
DocRoot: "testdata",
|
|
||||||
Client: client,
|
|
||||||
Prompter: prompter,
|
|
||||||
Printer: printer,
|
|
||||||
Preload: []string{"preload.js"},
|
|
||||||
})
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("failed to create JavaScript console: %v", err)
|
|
||||||
}
|
|
||||||
// Create the final tester and return
|
|
||||||
return &tester{
|
|
||||||
workspace: workspace,
|
|
||||||
stack: stack,
|
|
||||||
ethereum: ethBackend,
|
|
||||||
console: console,
|
|
||||||
input: prompter,
|
|
||||||
output: printer,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Close cleans up any temporary data folders and held resources.
|
|
||||||
func (env *tester) Close(t *testing.T) {
|
|
||||||
if err := env.console.Stop(false); err != nil {
|
|
||||||
t.Errorf("failed to stop embedded console: %v", err)
|
|
||||||
}
|
|
||||||
if err := env.stack.Close(); err != nil {
|
|
||||||
t.Errorf("failed to tear down embedded node: %v", err)
|
|
||||||
}
|
|
||||||
os.RemoveAll(env.workspace)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Tests that the node lists the correct welcome message, notably that it contains
|
|
||||||
// the instance name, coinbase account, block number, data directory and supported
|
|
||||||
// console modules.
|
|
||||||
func TestWelcome(t *testing.T) {
|
|
||||||
tester := newTester(t, nil)
|
|
||||||
defer tester.Close(t)
|
|
||||||
|
|
||||||
tester.console.Welcome()
|
|
||||||
|
|
||||||
output := tester.output.String()
|
|
||||||
if want := "Welcome"; !strings.Contains(output, want) {
|
|
||||||
t.Fatalf("console output missing welcome message: have\n%s\nwant also %s", output, want)
|
|
||||||
}
|
|
||||||
if want := fmt.Sprintf("instance: %s", testInstance); !strings.Contains(output, want) {
|
|
||||||
t.Fatalf("console output missing instance: have\n%s\nwant also %s", output, want)
|
|
||||||
}
|
|
||||||
if want := fmt.Sprintf("coinbase: %s", testAddress); !strings.Contains(output, want) {
|
|
||||||
t.Fatalf("console output missing coinbase: have\n%s\nwant also %s", output, want)
|
|
||||||
}
|
|
||||||
if want := "at block: 0"; !strings.Contains(output, want) {
|
|
||||||
t.Fatalf("console output missing sync status: have\n%s\nwant also %s", output, want)
|
|
||||||
}
|
|
||||||
if want := fmt.Sprintf("datadir: %s", tester.workspace); !strings.Contains(output, want) {
|
|
||||||
t.Fatalf("console output missing coinbase: have\n%s\nwant also %s", output, want)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Tests that JavaScript statement evaluation works as intended.
|
|
||||||
func TestEvaluate(t *testing.T) {
|
|
||||||
tester := newTester(t, nil)
|
|
||||||
defer tester.Close(t)
|
|
||||||
|
|
||||||
tester.console.Evaluate("2 + 2")
|
|
||||||
if output := tester.output.String(); !strings.Contains(output, "4") {
|
|
||||||
t.Fatalf("statement evaluation failed: have %s, want %s", output, "4")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Tests that the console can be used in interactive mode.
|
|
||||||
func TestInteractive(t *testing.T) {
|
|
||||||
// Create a tester and run an interactive console in the background
|
|
||||||
tester := newTester(t, nil)
|
|
||||||
defer tester.Close(t)
|
|
||||||
|
|
||||||
go tester.console.Interactive()
|
|
||||||
|
|
||||||
// Wait for a prompt and send a statement back
|
|
||||||
select {
|
|
||||||
case <-tester.input.scheduler:
|
|
||||||
case <-time.After(time.Second):
|
|
||||||
t.Fatalf("initial prompt timeout")
|
|
||||||
}
|
|
||||||
select {
|
|
||||||
case tester.input.scheduler <- "2+2":
|
|
||||||
case <-time.After(time.Second):
|
|
||||||
t.Fatalf("input feedback timeout")
|
|
||||||
}
|
|
||||||
// Wait for the second prompt and ensure first statement was evaluated
|
|
||||||
select {
|
|
||||||
case <-tester.input.scheduler:
|
|
||||||
case <-time.After(time.Second):
|
|
||||||
t.Fatalf("secondary prompt timeout")
|
|
||||||
}
|
|
||||||
if output := tester.output.String(); !strings.Contains(output, "4") {
|
|
||||||
t.Fatalf("statement evaluation failed: have %s, want %s", output, "4")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Tests that preloaded JavaScript files have been executed before user is given
|
|
||||||
// input.
|
|
||||||
func TestPreload(t *testing.T) {
|
|
||||||
tester := newTester(t, nil)
|
|
||||||
defer tester.Close(t)
|
|
||||||
|
|
||||||
tester.console.Evaluate("preloaded")
|
|
||||||
if output := tester.output.String(); !strings.Contains(output, "some-preloaded-string") {
|
|
||||||
t.Fatalf("preloaded variable missing: have %s, want %s", output, "some-preloaded-string")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Tests that the JavaScript objects returned by statement executions are properly
|
|
||||||
// pretty printed instead of just displaying "[object]".
|
|
||||||
func TestPrettyPrint(t *testing.T) {
|
|
||||||
tester := newTester(t, nil)
|
|
||||||
defer tester.Close(t)
|
|
||||||
|
|
||||||
tester.console.Evaluate("obj = {int: 1, string: 'two', list: [3, 3, 3], obj: {null: null, func: function(){}}}")
|
|
||||||
|
|
||||||
// Define some specially formatted fields
|
|
||||||
var (
|
|
||||||
one = jsre.NumberColor("1")
|
|
||||||
two = jsre.StringColor("\"two\"")
|
|
||||||
three = jsre.NumberColor("3")
|
|
||||||
null = jsre.SpecialColor("null")
|
|
||||||
fun = jsre.FunctionColor("function()")
|
|
||||||
)
|
|
||||||
// Assemble the actual output we're after and verify
|
|
||||||
want := `{
|
|
||||||
int: ` + one + `,
|
|
||||||
list: [` + three + `, ` + three + `, ` + three + `],
|
|
||||||
obj: {
|
|
||||||
null: ` + null + `,
|
|
||||||
func: ` + fun + `
|
|
||||||
},
|
|
||||||
string: ` + two + `
|
|
||||||
}
|
|
||||||
`
|
|
||||||
if output := tester.output.String(); output != want {
|
|
||||||
t.Fatalf("pretty print mismatch: have %s, want %s", output, want)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Tests that the JavaScript exceptions are properly formatted and colored.
|
|
||||||
func TestPrettyError(t *testing.T) {
|
|
||||||
tester := newTester(t, nil)
|
|
||||||
defer tester.Close(t)
|
|
||||||
tester.console.Evaluate("throw 'hello'")
|
|
||||||
|
|
||||||
want := jsre.ErrorColor("hello") + "\n\tat <eval>:1:1(1)\n\n"
|
|
||||||
if output := tester.output.String(); output != want {
|
|
||||||
t.Fatalf("pretty error mismatch: have %s, want %s", output, want)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Tests that tests if the number of indents for JS input is calculated correct.
|
|
||||||
func TestIndenting(t *testing.T) {
|
|
||||||
testCases := []struct {
|
|
||||||
input string
|
|
||||||
expectedIndentCount int
|
|
||||||
}{
|
|
||||||
{`var a = 1;`, 0},
|
|
||||||
{`"some string"`, 0},
|
|
||||||
{`"some string with (parenthesis`, 0},
|
|
||||||
{`"some string with newline
|
|
||||||
("`, 0},
|
|
||||||
{`function v(a,b) {}`, 0},
|
|
||||||
{`function f(a,b) { var str = "asd("; };`, 0},
|
|
||||||
{`function f(a) {`, 1},
|
|
||||||
{`function f(a, function(b) {`, 2},
|
|
||||||
{`function f(a, function(b) {
|
|
||||||
var str = "a)}";
|
|
||||||
});`, 0},
|
|
||||||
{`function f(a,b) {
|
|
||||||
var str = "a{b(" + a, ", " + b;
|
|
||||||
}`, 0},
|
|
||||||
{`var str = "\"{"`, 0},
|
|
||||||
{`var str = "'("`, 0},
|
|
||||||
{`var str = "\\{"`, 0},
|
|
||||||
{`var str = "\\\\{"`, 0},
|
|
||||||
{`var str = 'a"{`, 0},
|
|
||||||
{`var obj = {`, 1},
|
|
||||||
{`var obj = { {a:1`, 2},
|
|
||||||
{`var obj = { {a:1}`, 1},
|
|
||||||
{`var obj = { {a:1}, b:2}`, 0},
|
|
||||||
{`var obj = {}`, 0},
|
|
||||||
{`var obj = {
|
|
||||||
a: 1, b: 2
|
|
||||||
}`, 0},
|
|
||||||
{`var test = }`, -1},
|
|
||||||
{`var str = "a\""; var obj = {`, 1},
|
|
||||||
}
|
|
||||||
|
|
||||||
for i, tt := range testCases {
|
|
||||||
counted := countIndents(tt.input)
|
|
||||||
if counted != tt.expectedIndentCount {
|
|
||||||
t.Errorf("test %d: invalid indenting: have %d, want %d", i, counted, tt.expectedIndentCount)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,172 +0,0 @@
|
||||||
// Copyright 2016 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 prompt
|
|
||||||
|
|
||||||
import (
|
|
||||||
"fmt"
|
|
||||||
"strings"
|
|
||||||
|
|
||||||
"github.com/peterh/liner"
|
|
||||||
)
|
|
||||||
|
|
||||||
// Stdin holds the stdin line reader (also using stdout for printing prompts).
|
|
||||||
// Only this reader may be used for input because it keeps an internal buffer.
|
|
||||||
var Stdin = newTerminalPrompter()
|
|
||||||
|
|
||||||
// UserPrompter defines the methods needed by the console to prompt the user for
|
|
||||||
// various types of inputs.
|
|
||||||
type UserPrompter interface {
|
|
||||||
// PromptInput displays the given prompt to the user and requests some textual
|
|
||||||
// data to be entered, returning the input of the user.
|
|
||||||
PromptInput(prompt string) (string, error)
|
|
||||||
|
|
||||||
// PromptPassword displays the given prompt to the user and requests some textual
|
|
||||||
// data to be entered, but one which must not be echoed out into the terminal.
|
|
||||||
// The method returns the input provided by the user.
|
|
||||||
PromptPassword(prompt string) (string, error)
|
|
||||||
|
|
||||||
// PromptConfirm displays the given prompt to the user and requests a boolean
|
|
||||||
// choice to be made, returning that choice.
|
|
||||||
PromptConfirm(prompt string) (bool, error)
|
|
||||||
|
|
||||||
// SetHistory sets the input scrollback history that the prompter will allow
|
|
||||||
// the user to scroll back to.
|
|
||||||
SetHistory(history []string)
|
|
||||||
|
|
||||||
// AppendHistory appends an entry to the scrollback history. It should be called
|
|
||||||
// if and only if the prompt to append was a valid command.
|
|
||||||
AppendHistory(command string)
|
|
||||||
|
|
||||||
// ClearHistory clears the entire history
|
|
||||||
ClearHistory()
|
|
||||||
|
|
||||||
// SetWordCompleter sets the completion function that the prompter will call to
|
|
||||||
// fetch completion candidates when the user presses tab.
|
|
||||||
SetWordCompleter(completer WordCompleter)
|
|
||||||
}
|
|
||||||
|
|
||||||
// WordCompleter takes the currently edited line with the cursor position and
|
|
||||||
// returns the completion candidates for the partial word to be completed. If
|
|
||||||
// the line is "Hello, wo!!!" and the cursor is before the first '!', ("Hello,
|
|
||||||
// wo!!!", 9) is passed to the completer which may returns ("Hello, ", {"world",
|
|
||||||
// "Word"}, "!!!") to have "Hello, world!!!".
|
|
||||||
type WordCompleter func(line string, pos int) (string, []string, string)
|
|
||||||
|
|
||||||
// terminalPrompter is a UserPrompter backed by the liner package. It supports
|
|
||||||
// prompting the user for various input, among others for non-echoing password
|
|
||||||
// input.
|
|
||||||
type terminalPrompter struct {
|
|
||||||
*liner.State
|
|
||||||
warned bool
|
|
||||||
supported bool
|
|
||||||
normalMode liner.ModeApplier
|
|
||||||
rawMode liner.ModeApplier
|
|
||||||
}
|
|
||||||
|
|
||||||
// newTerminalPrompter creates a liner based user input prompter working off the
|
|
||||||
// standard input and output streams.
|
|
||||||
func newTerminalPrompter() *terminalPrompter {
|
|
||||||
p := new(terminalPrompter)
|
|
||||||
// Get the original mode before calling NewLiner.
|
|
||||||
// This is usually regular "cooked" mode where characters echo.
|
|
||||||
normalMode, _ := liner.TerminalMode()
|
|
||||||
// Turn on liner. It switches to raw mode.
|
|
||||||
p.State = liner.NewLiner()
|
|
||||||
rawMode, err := liner.TerminalMode()
|
|
||||||
if err != nil || !liner.TerminalSupported() {
|
|
||||||
p.supported = false
|
|
||||||
} else {
|
|
||||||
p.supported = true
|
|
||||||
p.normalMode = normalMode
|
|
||||||
p.rawMode = rawMode
|
|
||||||
// Switch back to normal mode while we're not prompting.
|
|
||||||
normalMode.ApplyMode()
|
|
||||||
}
|
|
||||||
p.SetCtrlCAborts(true)
|
|
||||||
p.SetTabCompletionStyle(liner.TabPrints)
|
|
||||||
p.SetMultiLineMode(true)
|
|
||||||
return p
|
|
||||||
}
|
|
||||||
|
|
||||||
// PromptInput displays the given prompt to the user and requests some textual
|
|
||||||
// data to be entered, returning the input of the user.
|
|
||||||
func (p *terminalPrompter) PromptInput(prompt string) (string, error) {
|
|
||||||
if p.supported {
|
|
||||||
p.rawMode.ApplyMode()
|
|
||||||
defer p.normalMode.ApplyMode()
|
|
||||||
} else {
|
|
||||||
// liner tries to be smart about printing the prompt
|
|
||||||
// and doesn't print anything if input is redirected.
|
|
||||||
// Un-smart it by printing the prompt always.
|
|
||||||
fmt.Print(prompt)
|
|
||||||
prompt = ""
|
|
||||||
defer fmt.Println()
|
|
||||||
}
|
|
||||||
return p.State.Prompt(prompt)
|
|
||||||
}
|
|
||||||
|
|
||||||
// PromptPassword displays the given prompt to the user and requests some textual
|
|
||||||
// data to be entered, but one which must not be echoed out into the terminal.
|
|
||||||
// The method returns the input provided by the user.
|
|
||||||
func (p *terminalPrompter) PromptPassword(prompt string) (passwd string, err error) {
|
|
||||||
if p.supported {
|
|
||||||
p.rawMode.ApplyMode()
|
|
||||||
defer p.normalMode.ApplyMode()
|
|
||||||
return p.State.PasswordPrompt(prompt)
|
|
||||||
}
|
|
||||||
if !p.warned {
|
|
||||||
fmt.Println("!! Unsupported terminal, password will be echoed.")
|
|
||||||
p.warned = true
|
|
||||||
}
|
|
||||||
// Just as in Prompt, handle printing the prompt here instead of relying on liner.
|
|
||||||
fmt.Print(prompt)
|
|
||||||
passwd, err = p.State.Prompt("")
|
|
||||||
fmt.Println()
|
|
||||||
return passwd, err
|
|
||||||
}
|
|
||||||
|
|
||||||
// PromptConfirm displays the given prompt to the user and requests a boolean
|
|
||||||
// choice to be made, returning that choice.
|
|
||||||
func (p *terminalPrompter) PromptConfirm(prompt string) (bool, error) {
|
|
||||||
input, err := p.Prompt(prompt + " [y/n] ")
|
|
||||||
if len(input) > 0 && strings.EqualFold(input[:1], "y") {
|
|
||||||
return true, nil
|
|
||||||
}
|
|
||||||
return false, err
|
|
||||||
}
|
|
||||||
|
|
||||||
// SetHistory sets the input scrollback history that the prompter will allow
|
|
||||||
// the user to scroll back to.
|
|
||||||
func (p *terminalPrompter) SetHistory(history []string) {
|
|
||||||
p.State.ReadHistory(strings.NewReader(strings.Join(history, "\n")))
|
|
||||||
}
|
|
||||||
|
|
||||||
// AppendHistory appends an entry to the scrollback history.
|
|
||||||
func (p *terminalPrompter) AppendHistory(command string) {
|
|
||||||
p.State.AppendHistory(command)
|
|
||||||
}
|
|
||||||
|
|
||||||
// ClearHistory clears the entire history
|
|
||||||
func (p *terminalPrompter) ClearHistory() {
|
|
||||||
p.State.ClearHistory()
|
|
||||||
}
|
|
||||||
|
|
||||||
// SetWordCompleter sets the completion function that the prompter will call to
|
|
||||||
// fetch completion candidates when the user presses tab.
|
|
||||||
func (p *terminalPrompter) SetWordCompleter(completer WordCompleter) {
|
|
||||||
p.State.SetWordCompleter(liner.WordCompleter(completer))
|
|
||||||
}
|
|
||||||
1
console/testdata/preload.js
vendored
1
console/testdata/preload.js
vendored
|
|
@ -1 +0,0 @@
|
||||||
var preloaded = "some-preloaded-string";
|
|
||||||
Loading…
Reference in a new issue