console, internal/jsre: use github.com/dop251/goja (#20470)

This replaces the JavaScript interpreter used by the console with goja,
which is actively maintained and a lot faster than otto. Clef still uses otto
and eth/tracers still uses duktape, so we are currently dependent on three
different JS interpreters. We're looking to replace the remaining uses of otto
soon though.
pull/20595/head
Guillaume Ballet 5 years ago committed by Felix Lange
parent 60deeb103e
commit 7b68975a00
  1. 8
      cmd/geth/consolecmd_test.go
  2. 373
      console/bridge.go
  3. 207
      console/console.go
  4. 2
      console/console_test.go
  5. 3
      go.mod
  6. 8
      go.sum
  7. 36
      internal/jsre/completion.go
  8. 4
      internal/jsre/completion_test.go
  9. 187
      internal/jsre/jsre.go
  10. 34
      internal/jsre/jsre_test.go
  11. 174
      internal/jsre/pretty.go

@ -51,7 +51,9 @@ func TestConsoleWelcome(t *testing.T) {
geth.SetTemplateFunc("goarch", func() string { return runtime.GOARCH }) geth.SetTemplateFunc("goarch", func() string { return runtime.GOARCH })
geth.SetTemplateFunc("gover", runtime.Version) geth.SetTemplateFunc("gover", runtime.Version)
geth.SetTemplateFunc("gethver", func() string { return params.VersionWithCommit("", "") }) geth.SetTemplateFunc("gethver", func() string { return params.VersionWithCommit("", "") })
geth.SetTemplateFunc("niltime", func() string { return time.Unix(0, 0).Format(time.RFC1123) }) geth.SetTemplateFunc("niltime", func() string {
return time.Unix(0, 0).Format("Mon Jan 02 2006 15:04:05 GMT-0700 (MST)")
})
geth.SetTemplateFunc("apis", func() string { return ipcAPIs }) geth.SetTemplateFunc("apis", func() string { return ipcAPIs })
// Verify the actual welcome message to the required template // Verify the actual welcome message to the required template
@ -142,7 +144,9 @@ func testAttachWelcome(t *testing.T, geth *testgeth, endpoint, apis string) {
attach.SetTemplateFunc("gover", runtime.Version) attach.SetTemplateFunc("gover", runtime.Version)
attach.SetTemplateFunc("gethver", func() string { return params.VersionWithCommit("", "") }) attach.SetTemplateFunc("gethver", func() string { return params.VersionWithCommit("", "") })
attach.SetTemplateFunc("etherbase", func() string { return geth.Etherbase }) attach.SetTemplateFunc("etherbase", func() string { return geth.Etherbase })
attach.SetTemplateFunc("niltime", func() string { return time.Unix(0, 0).Format(time.RFC1123) }) attach.SetTemplateFunc("niltime", func() string {
return time.Unix(0, 0).Format("Mon Jan 02 2006 15:04:05 GMT-0700 (MST)")
})
attach.SetTemplateFunc("ipc", func() bool { return strings.HasPrefix(endpoint, "ipc") }) attach.SetTemplateFunc("ipc", func() bool { return strings.HasPrefix(endpoint, "ipc") })
attach.SetTemplateFunc("datadir", func() string { return geth.Datadir }) attach.SetTemplateFunc("datadir", func() string { return geth.Datadir })
attach.SetTemplateFunc("apis", func() string { return apis }) attach.SetTemplateFunc("apis", func() string { return apis })

@ -20,14 +20,16 @@ import (
"encoding/json" "encoding/json"
"fmt" "fmt"
"io" "io"
"reflect"
"strings" "strings"
"time" "time"
"github.com/dop251/goja"
"github.com/ethereum/go-ethereum/accounts/scwallet" "github.com/ethereum/go-ethereum/accounts/scwallet"
"github.com/ethereum/go-ethereum/accounts/usbwallet" "github.com/ethereum/go-ethereum/accounts/usbwallet"
"github.com/ethereum/go-ethereum/log" "github.com/ethereum/go-ethereum/common/hexutil"
"github.com/ethereum/go-ethereum/internal/jsre"
"github.com/ethereum/go-ethereum/rpc" "github.com/ethereum/go-ethereum/rpc"
"github.com/robertkrimen/otto"
) )
// bridge is a collection of JavaScript utility methods to bride the .js runtime // bridge is a collection of JavaScript utility methods to bride the .js runtime
@ -47,10 +49,18 @@ func newBridge(client *rpc.Client, prompter UserPrompter, printer io.Writer) *br
} }
} }
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 // 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 // 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. // RPC method (saved in jeth.newAccount) with it to actually execute the RPC call.
func (b *bridge) NewAccount(call otto.FunctionCall) (response otto.Value) { func (b *bridge) NewAccount(call jsre.Call) (goja.Value, error) {
var ( var (
password string password string
confirm string confirm string
@ -58,52 +68,57 @@ func (b *bridge) NewAccount(call otto.FunctionCall) (response otto.Value) {
) )
switch { switch {
// No password was specified, prompt the user for it // No password was specified, prompt the user for it
case len(call.ArgumentList) == 0: case len(call.Arguments) == 0:
if password, err = b.prompter.PromptPassword("Password: "); err != nil { if password, err = b.prompter.PromptPassword("Passphrase: "); err != nil {
throwJSException(err.Error()) return nil, err
} }
if confirm, err = b.prompter.PromptPassword("Repeat password: "); err != nil { if confirm, err = b.prompter.PromptPassword("Repeat passphrase: "); err != nil {
throwJSException(err.Error()) return nil, err
} }
if password != confirm { if password != confirm {
throwJSException("passwords don't match!") return nil, fmt.Errorf("passwords don't match!")
} }
// A single string password was specified, use that // A single string password was specified, use that
case len(call.ArgumentList) == 1 && call.Argument(0).IsString(): case len(call.Arguments) == 1 && call.Argument(0).ToString() != nil:
password, _ = call.Argument(0).ToString() password = call.Argument(0).ToString().String()
// Otherwise fail with some error
default: default:
throwJSException("expected 0 or 1 string argument") return nil, fmt.Errorf("expected 0 or 1 string argument")
} }
// Password acquired, execute the call and return // Password acquired, execute the call and return
ret, err := call.Otto.Call("jeth.newAccount", nil, password) newAccount, callable := goja.AssertFunction(getJeth(call.VM).Get("newAccount"))
if !callable {
return nil, fmt.Errorf("jeth.newAccount is not callable")
}
ret, err := newAccount(goja.Null(), call.VM.ToValue(password))
if err != nil { if err != nil {
throwJSException(err.Error()) return nil, err
} }
return ret return ret, nil
} }
// OpenWallet is a wrapper around personal.openWallet which can interpret and // OpenWallet is a wrapper around personal.openWallet which can interpret and
// react to certain error messages, such as the Trezor PIN matrix request. // react to certain error messages, such as the Trezor PIN matrix request.
func (b *bridge) OpenWallet(call otto.FunctionCall) (response otto.Value) { func (b *bridge) OpenWallet(call jsre.Call) (goja.Value, error) {
// Make sure we have a wallet specified to open // Make sure we have a wallet specified to open
if !call.Argument(0).IsString() { if call.Argument(0).ToObject(call.VM).ClassName() != "String" {
throwJSException("first argument must be the wallet URL to open") return nil, fmt.Errorf("first argument must be the wallet URL to open")
} }
wallet := call.Argument(0) wallet := call.Argument(0)
var passwd otto.Value var passwd goja.Value
if call.Argument(1).IsUndefined() || call.Argument(1).IsNull() { if goja.IsUndefined(call.Argument(1)) || goja.IsNull(call.Argument(1)) {
passwd, _ = otto.ToValue("") passwd = call.VM.ToValue("")
} else { } else {
passwd = call.Argument(1) passwd = call.Argument(1)
} }
// Open the wallet and return if successful in itself // Open the wallet and return if successful in itself
val, err := call.Otto.Call("jeth.openWallet", nil, wallet, passwd) openWallet, callable := goja.AssertFunction(getJeth(call.VM).Get("openWallet"))
if !callable {
return nil, fmt.Errorf("jeth.openWallet is not callable")
}
val, err := openWallet(goja.Null(), wallet, passwd)
if err == nil { if err == nil {
return val return val, nil
} }
// Wallet open failed, report error unless it's a PIN or PUK entry // Wallet open failed, report error unless it's a PIN or PUK entry
@ -111,32 +126,31 @@ func (b *bridge) OpenWallet(call otto.FunctionCall) (response otto.Value) {
case strings.HasSuffix(err.Error(), usbwallet.ErrTrezorPINNeeded.Error()): case strings.HasSuffix(err.Error(), usbwallet.ErrTrezorPINNeeded.Error()):
val, err = b.readPinAndReopenWallet(call) val, err = b.readPinAndReopenWallet(call)
if err == nil { if err == nil {
return val return val, nil
} }
val, err = b.readPassphraseAndReopenWallet(call) val, err = b.readPassphraseAndReopenWallet(call)
if err != nil { if err != nil {
throwJSException(err.Error()) return nil, err
} }
case strings.HasSuffix(err.Error(), scwallet.ErrPairingPasswordNeeded.Error()): case strings.HasSuffix(err.Error(), scwallet.ErrPairingPasswordNeeded.Error()):
// PUK input requested, fetch from the user and call open again // PUK input requested, fetch from the user and call open again
if input, err := b.prompter.PromptPassword("Please enter the pairing password: "); err != nil { input, err := b.prompter.PromptPassword("Please enter the pairing password: ")
throwJSException(err.Error()) if err != nil {
} else { return nil, err
passwd, _ = otto.ToValue(input)
} }
if val, err = call.Otto.Call("jeth.openWallet", nil, wallet, passwd); err != nil { passwd = call.VM.ToValue(input)
if val, err = openWallet(goja.Null(), wallet, passwd); err != nil {
if !strings.HasSuffix(err.Error(), scwallet.ErrPINNeeded.Error()) { if !strings.HasSuffix(err.Error(), scwallet.ErrPINNeeded.Error()) {
throwJSException(err.Error()) return nil, err
} else { } else {
// PIN input requested, fetch from the user and call open again // PIN input requested, fetch from the user and call open again
if input, err := b.prompter.PromptPassword("Please enter current PIN: "); err != nil { input, err := b.prompter.PromptPassword("Please enter current PIN: ")
throwJSException(err.Error()) if err != nil {
} else { return nil, err
passwd, _ = otto.ToValue(input)
} }
if val, err = call.Otto.Call("jeth.openWallet", nil, wallet, passwd); err != nil { if val, err = openWallet(goja.Null(), wallet, call.VM.ToValue(input)); err != nil {
throwJSException(err.Error()) return nil, err
} }
} }
} }
@ -144,52 +158,52 @@ func (b *bridge) OpenWallet(call otto.FunctionCall) (response otto.Value) {
case strings.HasSuffix(err.Error(), scwallet.ErrPINUnblockNeeded.Error()): case strings.HasSuffix(err.Error(), scwallet.ErrPINUnblockNeeded.Error()):
// PIN unblock requested, fetch PUK and new PIN from the user // PIN unblock requested, fetch PUK and new PIN from the user
var pukpin string var pukpin string
if input, err := b.prompter.PromptPassword("Please enter current PUK: "); err != nil { input, err := b.prompter.PromptPassword("Please enter current PUK: ")
throwJSException(err.Error()) if err != nil {
} else { return nil, err
pukpin = input
} }
if input, err := b.prompter.PromptPassword("Please enter new PIN: "); err != nil { pukpin = input
throwJSException(err.Error()) input, err = b.prompter.PromptPassword("Please enter new PIN: ")
} else { if err != nil {
pukpin += input return nil, err
} }
passwd, _ = otto.ToValue(pukpin) pukpin += input
if val, err = call.Otto.Call("jeth.openWallet", nil, wallet, passwd); err != nil {
throwJSException(err.Error()) if val, err = openWallet(goja.Null(), wallet, call.VM.ToValue(pukpin)); err != nil {
return nil, err
} }
case strings.HasSuffix(err.Error(), scwallet.ErrPINNeeded.Error()): case strings.HasSuffix(err.Error(), scwallet.ErrPINNeeded.Error()):
// PIN input requested, fetch from the user and call open again // PIN input requested, fetch from the user and call open again
if input, err := b.prompter.PromptPassword("Please enter current PIN: "); err != nil { input, err := b.prompter.PromptPassword("Please enter current PIN: ")
throwJSException(err.Error()) if err != nil {
} else { return nil, err
passwd, _ = otto.ToValue(input)
} }
if val, err = call.Otto.Call("jeth.openWallet", nil, wallet, passwd); err != nil { if val, err = openWallet(goja.Null(), wallet, call.VM.ToValue(input)); err != nil {
throwJSException(err.Error()) return nil, err
} }
default: default:
// Unknown error occurred, drop to the user // Unknown error occurred, drop to the user
throwJSException(err.Error()) return nil, err
} }
return val return val, nil
} }
func (b *bridge) readPassphraseAndReopenWallet(call otto.FunctionCall) (otto.Value, error) { func (b *bridge) readPassphraseAndReopenWallet(call jsre.Call) (goja.Value, error) {
var passwd otto.Value
wallet := call.Argument(0) wallet := call.Argument(0)
if input, err := b.prompter.PromptPassword("Please enter your password: "); err != nil { input, err := b.prompter.PromptPassword("Please enter your passphrase: ")
throwJSException(err.Error()) if err != nil {
} else { return nil, err
passwd, _ = otto.ToValue(input) }
openWallet, callable := goja.AssertFunction(getJeth(call.VM).Get("openWallet"))
if !callable {
return nil, fmt.Errorf("jeth.openWallet is not callable")
} }
return call.Otto.Call("jeth.openWallet", nil, wallet, passwd) return openWallet(goja.Null(), wallet, call.VM.ToValue(input))
} }
func (b *bridge) readPinAndReopenWallet(call otto.FunctionCall) (otto.Value, error) { func (b *bridge) readPinAndReopenWallet(call jsre.Call) (goja.Value, error) {
var passwd otto.Value
wallet := call.Argument(0) wallet := call.Argument(0)
// Trezor PIN matrix input requested, display the matrix to the user and fetch the data // 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, "Look at the device for number positions\n\n")
@ -199,155 +213,154 @@ func (b *bridge) readPinAndReopenWallet(call otto.FunctionCall) (otto.Value, err
fmt.Fprintf(b.printer, "--+---+--\n") fmt.Fprintf(b.printer, "--+---+--\n")
fmt.Fprintf(b.printer, "1 | 2 | 3\n\n") fmt.Fprintf(b.printer, "1 | 2 | 3\n\n")
if input, err := b.prompter.PromptPassword("Please enter current PIN: "); err != nil { input, err := b.prompter.PromptPassword("Please enter current PIN: ")
throwJSException(err.Error()) if err != nil {
} else { return nil, err
passwd, _ = otto.ToValue(input) }
openWallet, callable := goja.AssertFunction(getJeth(call.VM).Get("openWallet"))
if !callable {
return nil, fmt.Errorf("jeth.openWallet is not callable")
} }
return call.Otto.Call("jeth.openWallet", nil, wallet, passwd) return openWallet(goja.Null(), wallet, call.VM.ToValue(input))
} }
// UnlockAccount is a wrapper around the personal.unlockAccount RPC method that // UnlockAccount is a wrapper around the personal.unlockAccount RPC method that
// uses a non-echoing password prompt to acquire the passphrase and executes the // 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 // original RPC method (saved in jeth.unlockAccount) with it to actually execute
// the RPC call. // the RPC call.
func (b *bridge) UnlockAccount(call otto.FunctionCall) (response otto.Value) { func (b *bridge) UnlockAccount(call jsre.Call) (goja.Value, error) {
// Make sure we have an account specified to unlock // Make sure we have an account specified to unlock.
if !call.Argument(0).IsString() { if call.Argument(0).ExportType().Kind() != reflect.String {
throwJSException("first argument must be the account to unlock") return nil, fmt.Errorf("first argument must be the account to unlock")
} }
account := call.Argument(0) account := call.Argument(0)
// If password is not given or is the null value, prompt the user for it // If password is not given or is the null value, prompt the user for it.
var passwd otto.Value var passwd goja.Value
if goja.IsUndefined(call.Argument(1)) || goja.IsNull(call.Argument(1)) {
if call.Argument(1).IsUndefined() || call.Argument(1).IsNull() {
fmt.Fprintf(b.printer, "Unlock account %s\n", account) fmt.Fprintf(b.printer, "Unlock account %s\n", account)
if input, err := b.prompter.PromptPassword("Password: "); err != nil { input, err := b.prompter.PromptPassword("Passphrase: ")
throwJSException(err.Error()) if err != nil {
} else { return nil, err
passwd, _ = otto.ToValue(input)
} }
passwd = call.VM.ToValue(input)
} else { } else {
if !call.Argument(1).IsString() { if call.Argument(1).ExportType().Kind() != reflect.String {
throwJSException("password must be a string") return nil, fmt.Errorf("password must be a string")
} }
passwd = call.Argument(1) passwd = call.Argument(1)
} }
// Third argument is the duration how long the account must be unlocked.
duration := otto.NullValue() // Third argument is the duration how long the account should be unlocked.
if call.Argument(2).IsDefined() && !call.Argument(2).IsNull() { duration := goja.Null()
if !call.Argument(2).IsNumber() { if !goja.IsUndefined(call.Argument(2)) && !goja.IsNull(call.Argument(2)) {
throwJSException("unlock duration must be a number") if !isNumber(call.Argument(2)) {
return nil, fmt.Errorf("unlock duration must be a number")
} }
duration = call.Argument(2) duration = call.Argument(2)
} }
// Send the request to the backend and return
val, err := call.Otto.Call("jeth.unlockAccount", nil, account, passwd, duration) // Send the request to the backend and return.
if err != nil { unlockAccount, callable := goja.AssertFunction(getJeth(call.VM).Get("unlockAccount"))
throwJSException(err.Error()) if !callable {
return nil, fmt.Errorf("jeth.unlockAccount is not callable")
} }
return val return unlockAccount(goja.Null(), account, passwd, duration)
} }
// Sign is a wrapper around the personal.sign RPC method that uses a non-echoing password // 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 // prompt to acquire the passphrase and executes the original RPC method (saved in
// jeth.sign) with it to actually execute the RPC call. // jeth.sign) with it to actually execute the RPC call.
func (b *bridge) Sign(call otto.FunctionCall) (response otto.Value) { func (b *bridge) Sign(call jsre.Call) (goja.Value, error) {
var ( var (
message = call.Argument(0) message = call.Argument(0)
account = call.Argument(1) account = call.Argument(1)
passwd = call.Argument(2) passwd = call.Argument(2)
) )
if !message.IsString() { if message.ExportType().Kind() != reflect.String {
throwJSException("first argument must be the message to sign") return nil, fmt.Errorf("first argument must be the message to sign")
} }
if !account.IsString() { if account.ExportType().Kind() != reflect.String {
throwJSException("second argument must be the account to sign with") return nil, fmt.Errorf("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 the password is not given or null ask the user and ensure password is a string
if passwd.IsUndefined() || passwd.IsNull() { if goja.IsUndefined(passwd) || goja.IsNull(passwd) {
fmt.Fprintf(b.printer, "Give password for account %s\n", account) fmt.Fprintf(b.printer, "Give password for account %s\n", account)
if input, err := b.prompter.PromptPassword("Password: "); err != nil { input, err := b.prompter.PromptPassword("Password: ")
throwJSException(err.Error()) if err != nil {
} else { return nil, err
passwd, _ = otto.ToValue(input)
} }
} passwd = call.VM.ToValue(input)
if !passwd.IsString() { } else if passwd.ExportType().Kind() != reflect.String {
throwJSException("third argument must be the password to unlock the account") return nil, fmt.Errorf("third argument must be the password to unlock the account")
} }
// Send the request to the backend and return // Send the request to the backend and return
val, err := call.Otto.Call("jeth.sign", nil, message, account, passwd) sign, callable := goja.AssertFunction(getJeth(call.VM).Get("unlockAccount"))
if err != nil { if !callable {
throwJSException(err.Error()) return nil, fmt.Errorf("jeth.unlockAccount is not callable")
} }
return val return sign(goja.Null(), message, account, passwd)
} }
// Sleep will block the console for the specified number of seconds. // Sleep will block the console for the specified number of seconds.
func (b *bridge) Sleep(call otto.FunctionCall) (response otto.Value) { func (b *bridge) Sleep(call jsre.Call) (goja.Value, error) {
if call.Argument(0).IsNumber() { if !isNumber(call.Argument(0)) {
sleep, _ := call.Argument(0).ToInteger() return nil, fmt.Errorf("usage: sleep(<number of seconds>)")
time.Sleep(time.Duration(sleep) * time.Second)
return otto.TrueValue()
} }
return throwJSException("usage: sleep(<number of seconds>)") sleep := call.Argument(0).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 // SleepBlocks will block the console for a specified number of new blocks optionally
// until the given timeout is reached. // until the given timeout is reached.
func (b *bridge) SleepBlocks(call otto.FunctionCall) (response otto.Value) { func (b *bridge) SleepBlocks(call jsre.Call) (goja.Value, error) {
// Parse the input parameters for the sleep.
var ( var (
blocks = int64(0) blocks = int64(0)
sleep = int64(9999999999999999) // indefinitely sleep = int64(9999999999999999) // indefinitely
) )
// Parse the input parameters for the sleep nArgs := len(call.Arguments)
nArgs := len(call.ArgumentList)
if nArgs == 0 { if nArgs == 0 {
throwJSException("usage: sleepBlocks(<n blocks>[, max sleep in seconds])") return nil, fmt.Errorf("usage: sleepBlocks(<n blocks>[, max sleep in seconds])")
} }
if nArgs >= 1 { if nArgs >= 1 {
if call.Argument(0).IsNumber() { if !isNumber(call.Argument(0)) {
blocks, _ = call.Argument(0).ToInteger() return nil, fmt.Errorf("expected number as first argument")
} else {
throwJSException("expected number as first argument")
} }
blocks = call.Argument(0).ToInteger()
} }
if nArgs >= 2 { if nArgs >= 2 {
if call.Argument(1).IsNumber() { if isNumber(call.Argument(1)) {
sleep, _ = call.Argument(1).ToInteger() return nil, fmt.Errorf("expected number as second argument")
} else {
throwJSException("expected number as second argument")
} }
sleep = call.Argument(1).ToInteger()
} }
// go through the console, this will allow web3 to call the appropriate
// callbacks if a delayed response or notification is received. // Poll the current block number until either it or a timeout is reached.
blockNumber := func() int64 { var (
result, err := call.Otto.Run("eth.blockNumber") deadline = time.Now().Add(time.Duration(sleep) * time.Second)
lastNumber = ^hexutil.Uint64(0)
)
for time.Now().Before(deadline) {
var number hexutil.Uint64
err := b.client.Call(&number, "eth_blockNumber")
if err != nil { if err != nil {
throwJSException(err.Error()) return nil, err
} }
block, err := result.ToInteger() if number != lastNumber {
if err != nil { lastNumber = number
throwJSException(err.Error()) blocks--
} }
return block if blocks <= 0 {
} break
// Poll the current block number until either it ot a timeout is reached
targetBlockNr := blockNumber() + blocks
deadline := time.Now().Add(time.Duration(sleep) * time.Second)
for time.Now().Before(deadline) {
if blockNumber() >= targetBlockNr {
return otto.TrueValue()
} }
time.Sleep(time.Second) time.Sleep(time.Second)
} }
return otto.FalseValue() return call.VM.ToValue(true), nil
} }
type jsonrpcCall struct { type jsonrpcCall struct {
@ -357,15 +370,15 @@ type jsonrpcCall struct {
} }
// Send implements the web3 provider "send" method. // Send implements the web3 provider "send" method.
func (b *bridge) Send(call otto.FunctionCall) (response otto.Value) { func (b *bridge) Send(call jsre.Call) (goja.Value, error) {
// Remarshal the request into a Go value. // Remarshal the request into a Go value.
JSON, _ := call.Otto.Object("JSON") reqVal, err := call.Argument(0).ToObject(call.VM).MarshalJSON()
reqVal, err := JSON.Call("stringify", call.Argument(0))
if err != nil { if err != nil {
throwJSException(err.Error()) return nil, err
} }
var ( var (
rawReq = reqVal.String() rawReq = string(reqVal)
dec = json.NewDecoder(strings.NewReader(rawReq)) dec = json.NewDecoder(strings.NewReader(rawReq))
reqs []jsonrpcCall reqs []jsonrpcCall
batch bool batch bool
@ -381,10 +394,12 @@ func (b *bridge) Send(call otto.FunctionCall) (response otto.Value) {
} }
// Execute the requests. // Execute the requests.
resps, _ := call.Otto.Object("new Array()") var resps []*goja.Object
for _, req := range reqs { for _, req := range reqs {
resp, _ := call.Otto.Object(`({"jsonrpc":"2.0"})`) resp := call.VM.NewObject()
resp.Set("jsonrpc", "2.0")
resp.Set("id", req.ID) resp.Set("id", req.ID)
var result json.RawMessage var result json.RawMessage
err = b.client.Call(&result, req.Method, req.Params...) err = b.client.Call(&result, req.Method, req.Params...)
switch err := err.(type) { switch err := err.(type) {
@ -392,9 +407,14 @@ func (b *bridge) Send(call otto.FunctionCall) (response otto.Value) {
if result == nil { if result == nil {
// Special case null because it is decoded as an empty // Special case null because it is decoded as an empty
// raw message for some reason. // raw message for some reason.
resp.Set("result", otto.NullValue()) resp.Set("result", goja.Null())
} else { } else {
resultVal, err := JSON.Call("parse", string(result)) JSON := call.VM.Get("JSON").ToObject(call.VM)
parse, callable := goja.AssertFunction(JSON.Get("parse"))
if !callable {
return nil, fmt.Errorf("JSON.parse is not a function")
}
resultVal, err := parse(goja.Null(), call.VM.ToValue(string(result)))
if err != nil { if err != nil {
setError(resp, -32603, err.Error()) setError(resp, -32603, err.Error())
} else { } else {
@ -406,33 +426,38 @@ func (b *bridge) Send(call otto.FunctionCall) (response otto.Value) {
default: default:
setError(resp, -32603, err.Error()) setError(resp, -32603, err.Error())
} }
resps.Call("push", resp) resps = append(resps, resp)
} }
// Return the responses either to the callback (if supplied) // Return the responses either to the callback (if supplied)
// or directly as the return value. // or directly as the return value.
var result goja.Value
if batch { if batch {
response = resps.Value() result = call.VM.ToValue(resps)
} else { } else {
response, _ = resps.Get("0") result = resps[0]
} }
if fn := call.Argument(1); fn.Class() == "Function" { if fn, isFunc := goja.AssertFunction(call.Argument(1)); isFunc {
fn.Call(otto.NullValue(), otto.NullValue(), response) fn(goja.Null(), goja.Null(), result)
return otto.UndefinedValue() return goja.Undefined(), nil
} }
return response return result, nil
} }
func setError(resp *otto.Object, code int, msg string) { func setError(resp *goja.Object, code int, msg string) {
resp.Set("error", map[string]interface{}{"code": code, "message": msg}) resp.Set("error", map[string]interface{}{"code": code, "message": msg})
} }
// throwJSException panics on an otto.Value. The Otto VM will recover from the // isNumber returns true if input value is a JS number.
// Go panic and throw msg as a JavaScript error. func isNumber(v goja.Value) bool {
func throwJSException(msg interface{}) otto.Value { k := v.ExportType().Kind()
val, err := otto.ToValue(msg) return k >= reflect.Int && k <= reflect.Float64
if err != nil { }
log.Error("Failed to serialize JavaScript exception", "exception", msg, "err", err)
func getObject(vm *goja.Runtime, name string) *goja.Object {
v := vm.Get(name)
if v == nil {
return nil
} }
panic(val) return v.ToObject(vm)
} }

@ -28,12 +28,13 @@ import (
"strings" "strings"
"syscall" "syscall"
"github.com/dop251/goja"
"github.com/ethereum/go-ethereum/internal/jsre" "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/internal/web3ext"
"github.com/ethereum/go-ethereum/rpc" "github.com/ethereum/go-ethereum/rpc"
"github.com/mattn/go-colorable" "github.com/mattn/go-colorable"
"github.com/peterh/liner" "github.com/peterh/liner"
"github.com/robertkrimen/otto"
) )
var ( var (
@ -86,6 +87,7 @@ func New(config Config) (*Console, error) {
if config.Printer == nil { if config.Printer == nil {
config.Printer = colorable.NewColorableStdout() config.Printer = colorable.NewColorableStdout()
} }
// Initialize the console and return // Initialize the console and return
console := &Console{ console := &Console{
client: config.Client, client: config.Client,
@ -107,120 +109,141 @@ func New(config Config) (*Console, error) {
// init retrieves the available APIs from the remote RPC provider and initializes // init retrieves the available APIs from the remote RPC provider and initializes
// the console's JavaScript namespaces based on the exposed modules. // the console's JavaScript namespaces based on the exposed modules.
func (c *Console) init(preload []string) error { func (c *Console) init(preload []string) error {
// Initialize the JavaScript <-> Go RPC bridge c.initConsoleObject()
// Initialize the JavaScript <-> Go RPC bridge.
bridge := newBridge(c.client, c.prompter, c.printer) bridge := newBridge(c.client, c.prompter, c.printer)
c.jsre.Set("jeth", struct{}{}) 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)
}
}
jethObj, _ := c.jsre.Get("jeth") // Configure the input prompter for history and tab completion.
jethObj.Object().Set("send", bridge.Send) if c.prompter != nil {
jethObj.Object().Set("sendAsync", bridge.Send) if content, err := ioutil.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
}
consoleObj, _ := c.jsre.Get("console") func (c *Console) initConsoleObject() {
consoleObj.Object().Set("log", c.consoleOutput) c.jsre.Do(func(vm *goja.Runtime) {
consoleObj.Object().Set("error", c.consoleOutput) console := vm.NewObject()
console.Set("log", c.consoleOutput)
console.Set("error", c.consoleOutput)
vm.Set("console", console)
})
}
// Load all the internal utility JavaScript libraries func (c *Console) initWeb3(bridge *bridge) error {
if err := c.jsre.Compile("bignumber.js", jsre.BignumberJs); err != nil { bnJS := string(deps.MustAsset("bignumber.js"))
web3JS := string(deps.MustAsset("web3.js"))
if err := c.jsre.Compile("bignumber.js", bnJS); err != nil {
return fmt.Errorf("bignumber.js: %v", err) return fmt.Errorf("bignumber.js: %v", err)
} }
if err := c.jsre.Compile("web3.js", jsre.Web3Js); err != nil { if err := c.jsre.Compile("web3.js", web3JS); err != nil {
return fmt.Errorf("web3.js: %v", err) return fmt.Errorf("web3.js: %v", err)
} }
if _, err := c.jsre.Run("var Web3 = require('web3');"); err != nil { if _, err := c.jsre.Run("var Web3 = require('web3');"); err != nil {
return fmt.Errorf("web3 require: %v", err) return fmt.Errorf("web3 require: %v", err)
} }
if _, err := c.jsre.Run("var web3 = new Web3(jeth);"); err != nil { var err error
return fmt.Errorf("web3 provider: %v", err) c.jsre.Do(func(vm *goja.Runtime) {
} transport := vm.NewObject()
// Load the supported APIs into the JavaScript runtime environment 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
}
// initExtensions loads and registers web3.js extensions.
func (c *Console) initExtensions() error {
// Compute aliases from server-provided modules.
apis, err := c.client.SupportedModules() apis, err := c.client.SupportedModules()
if err != nil { if err != nil {
return fmt.Errorf("api modules: %v", err) return fmt.Errorf("api modules: %v", err)
} }
flatten := "var eth = web3.eth; var personal = web3.personal; " aliases := map[string]struct{}{"eth": {}, "personal": {}}
for api := range apis { for api := range apis {
if api == "web3" { if api == "web3" {
continue // manually mapped or ignore continue
} }
aliases[api] = struct{}{}
if file, ok := web3ext.Modules[api]; ok { if file, ok := web3ext.Modules[api]; ok {
// Load our extension for the module. if err = c.jsre.Compile(api+".js", file); err != nil {
if err = c.jsre.Compile(fmt.Sprintf("%s.js", api), file); err != nil {
return fmt.Errorf("%s.js: %v", api, err) return fmt.Errorf("%s.js: %v", api, err)
} }
flatten += fmt.Sprintf("var %s = web3.%s; ", api, api)
} else if obj, err := c.jsre.Run("web3." + api); err == nil && obj.IsObject() {
// Enable web3.js built-in extension if available.
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 // Apply aliases.
if c.prompter != nil { c.jsre.Do(func(vm *goja.Runtime) {
// Retrieve the account management object to instrument web3 := getObject(vm, "web3")
personal, err := c.jsre.Get("personal") for name := range aliases {
if err != nil { if v := web3.Get(name); v != nil {
return err vm.Set(name, v)
}
// Override the openWallet, unlockAccount, newAccount and sign methods since
// these require user interaction. Assign these method 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.openWallet = personal.openWallet;`); err != nil {
return fmt.Errorf("personal.openWallet: %v", err)
}
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)
}
if _, err = c.jsre.Run(`jeth.sign = personal.sign;`); err != nil {
return fmt.Errorf("personal.sign: %v", err)
}
obj.Set("openWallet", bridge.OpenWallet)
obj.Set("unlockAccount", bridge.UnlockAccount)
obj.Set("newAccount", bridge.NewAccount)
obj.Set("sign", bridge.Sign)
}
}
// 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)
obj.Set("clearHistory", c.clearHistory)
}
// Preload any JavaScript files before starting the console
for _, path := range preload {
if err := c.jsre.Exec(path); err != nil {
failure := err.Error()
if ottoErr, ok := err.(*otto.Error); ok {
failure = ottoErr.String()
} }
return fmt.Errorf("%s: %v", path, failure)
} }
})
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)
} }
// Configure the console's input prompter for scrollback and tab completion }
if c.prompter != nil {
if content, err := ioutil.ReadFile(c.histPath); err != nil { // initPersonal redirects account-related API methods through the bridge.
c.prompter.SetHistory(nil) //
} else { // If the console is in interactive mode and the 'personal' API is available, override
c.history = strings.Split(string(content), "\n") // the openWallet, unlockAccount, newAccount and sign methods since these require user
c.prompter.SetHistory(c.history) // 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.
c.prompter.SetWordCompleter(c.AutoCompleteInput) func (c *Console) initPersonal(vm *goja.Runtime, bridge *bridge) {
personal := getObject(vm, "personal")
if personal == nil || c.prompter == nil {
return
} }
return nil 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() { func (c *Console) clearHistory() {
@ -235,13 +258,13 @@ func (c *Console) clearHistory() {
// consoleOutput is an override for the console.log and console.error methods to // consoleOutput is an override for the console.log and console.error methods to
// stream the output into the configured output stream instead of stdout. // stream the output into the configured output stream instead of stdout.
func (c *Console) consoleOutput(call otto.FunctionCall) otto.Value { func (c *Console) consoleOutput(call goja.FunctionCall) goja.Value {
var output []string var output []string
for _, argument := range call.ArgumentList { for _, argument := range call.Arguments {
output = append(output, fmt.Sprintf("%v", argument)) output = append(output, fmt.Sprintf("%v", argument))
} }
fmt.Fprintln(c.printer, strings.Join(output, " ")) fmt.Fprintln(c.printer, strings.Join(output, " "))
return otto.Value{} return goja.Null()
} }
// AutoCompleteInput is a pre-assembled word completer to be used by the user // AutoCompleteInput is a pre-assembled word completer to be used by the user
@ -304,13 +327,13 @@ func (c *Console) Welcome() {
// Evaluate executes code and pretty prints the result to the specified output // Evaluate executes code and pretty prints the result to the specified output
// stream. // stream.
func (c *Console) Evaluate(statement string) error { func (c *Console) Evaluate(statement string) {
defer func() { defer func() {
if r := recover(); r != nil { if r := recover(); r != nil {
fmt.Fprintf(c.printer, "[native] error: %v\n", r) fmt.Fprintf(c.printer, "[native] error: %v\n", r)
} }
}() }()
return c.jsre.Evaluate(statement, c.printer) c.jsre.Evaluate(statement, c.printer)
} }
// Interactive starts an interactive user session, where input is propted from // Interactive starts an interactive user session, where input is propted from

@ -289,7 +289,7 @@ func TestPrettyError(t *testing.T) {
defer tester.Close(t) defer tester.Close(t)
tester.console.Evaluate("throw 'hello'") tester.console.Evaluate("throw 'hello'")
want := jsre.ErrorColor("hello") + "\n" want := jsre.ErrorColor("hello") + "\n\tat <eval>:1:7(1)\n\n"
if output := tester.output.String(); output != want { if output := tester.output.String(); output != want {
t.Fatalf("pretty error mismatch: have %s, want %s", output, want) t.Fatalf("pretty error mismatch: have %s, want %s", output, want)
} }

@ -16,13 +16,16 @@ require (
github.com/cloudflare/cloudflare-go v0.10.2-0.20190916151808-a80f83b9add9 github.com/cloudflare/cloudflare-go v0.10.2-0.20190916151808-a80f83b9add9
github.com/davecgh/go-spew v1.1.1 github.com/davecgh/go-spew v1.1.1
github.com/deckarep/golang-set v0.0.0-20180603214616-504e848d77ea github.com/deckarep/golang-set v0.0.0-20180603214616-504e848d77ea
github.com/dlclark/regexp2 v1.2.0 // indirect
github.com/docker/docker v1.4.2-0.20180625184442-8e610b2b55bf github.com/docker/docker v1.4.2-0.20180625184442-8e610b2b55bf
github.com/dop251/goja v0.0.0-20200106141417-aaec0e7bde29
github.com/edsrzf/mmap-go v0.0.0-20160512033002-935e0e8a636c github.com/edsrzf/mmap-go v0.0.0-20160512033002-935e0e8a636c
github.com/elastic/gosigar v0.8.1-0.20180330100440-37f05ff46ffa github.com/elastic/gosigar v0.8.1-0.20180330100440-37f05ff46ffa
github.com/fatih/color v1.3.0 github.com/fatih/color v1.3.0
github.com/fjl/memsize v0.0.0-20180418122429-ca190fb6ffbc github.com/fjl/memsize v0.0.0-20180418122429-ca190fb6ffbc
github.com/gballet/go-libpcsclite v0.0.0-20190607065134-2772fd86a8ff github.com/gballet/go-libpcsclite v0.0.0-20190607065134-2772fd86a8ff
github.com/go-ole/go-ole v1.2.1 // indirect github.com/go-ole/go-ole v1.2.1 // indirect
github.com/go-sourcemap/sourcemap v2.1.2+incompatible // indirect
github.com/go-stack/stack v1.8.0 github.com/go-stack/stack v1.8.0
github.com/golang/protobuf v1.3.2-0.20190517061210-b285ee9cfc6c github.com/golang/protobuf v1.3.2-0.20190517061210-b285ee9cfc6c
github.com/golang/snappy v0.0.1 github.com/golang/snappy v0.0.1

@ -57,8 +57,14 @@ github.com/deckarep/golang-set v0.0.0-20180603214616-504e848d77ea/go.mod h1:93vs
github.com/dgrijalva/jwt-go v3.2.0+incompatible h1:7qlOGliEKZXTDg6OTjfoBKDXWrumCAMpl/TFQ4/5kLM= github.com/dgrijalva/jwt-go v3.2.0+incompatible h1:7qlOGliEKZXTDg6OTjfoBKDXWrumCAMpl/TFQ4/5kLM=
github.com/dgrijalva/jwt-go v3.2.0+incompatible/go.mod h1:E3ru+11k8xSBh+hMPgOLZmtrrCbhqsmaPHjLKYnJCaQ= github.com/dgrijalva/jwt-go v3.2.0+incompatible/go.mod h1:E3ru+11k8xSBh+hMPgOLZmtrrCbhqsmaPHjLKYnJCaQ=
github.com/dgryski/go-sip13 v0.0.0-20181026042036-e10d5fee7954/go.mod h1:vAd38F8PWV+bWy6jNmig1y/TA+kYO4g3RSRF0IAv0no= github.com/dgryski/go-sip13 v0.0.0-20181026042036-e10d5fee7954/go.mod h1:vAd38F8PWV+bWy6jNmig1y/TA+kYO4g3RSRF0IAv0no=
github.com/dlclark/regexp2 v1.2.0 h1:8sAhBGEM0dRWogWqWyQeIJnxjWO6oIjl8FKqREDsGfk=
github.com/dlclark/regexp2 v1.2.0/go.mod h1:2pZnwuY/m+8K6iRw6wQdMtk+rH5tNGR1i55kozfMjCc=
github.com/docker/docker v1.4.2-0.20180625184442-8e610b2b55bf h1:sh8rkQZavChcmakYiSlqu2425CHyFXLZZnvm7PDpU8M= github.com/docker/docker v1.4.2-0.20180625184442-8e610b2b55bf h1:sh8rkQZavChcmakYiSlqu2425CHyFXLZZnvm7PDpU8M=
github.com/docker/docker v1.4.2-0.20180625184442-8e610b2b55bf/go.mod h1:eEKB0N0r5NX/I1kEveEz05bcu8tLC/8azJZsviup8Sk= github.com/docker/docker v1.4.2-0.20180625184442-8e610b2b55bf/go.mod h1:eEKB0N0r5NX/I1kEveEz05bcu8tLC/8azJZsviup8Sk=
github.com/dop251/goja v0.0.0-20191203121440-007eef3bc40f h1:vtCDQseO/Sbu5IZSoc2uzZ7CkSoai7OtpcwGFK5FlyE=
github.com/dop251/goja v0.0.0-20191203121440-007eef3bc40f/go.mod h1:Mw6PkjjMXWbTj+nnj4s3QPXq1jaT0s5pC0iFD4+BOAA=
github.com/dop251/goja v0.0.0-20200106141417-aaec0e7bde29 h1:Ewd9K+mC725sITA12QQHRqWj78NU4t7EhlFVVgdlzJg=
github.com/dop251/goja v0.0.0-20200106141417-aaec0e7bde29/go.mod h1:Mw6PkjjMXWbTj+nnj4s3QPXq1jaT0s5pC0iFD4+BOAA=
github.com/edsrzf/mmap-go v0.0.0-20160512033002-935e0e8a636c h1:JHHhtb9XWJrGNMcrVP6vyzO4dusgi/HnceHTgxSejUM= github.com/edsrzf/mmap-go v0.0.0-20160512033002-935e0e8a636c h1:JHHhtb9XWJrGNMcrVP6vyzO4dusgi/HnceHTgxSejUM=
github.com/edsrzf/mmap-go v0.0.0-20160512033002-935e0e8a636c/go.mod h1:YO35OhQPt3KJa3ryjFM5Bs14WD66h8eGKpfaBNrHW5M= github.com/edsrzf/mmap-go v0.0.0-20160512033002-935e0e8a636c/go.mod h1:YO35OhQPt3KJa3ryjFM5Bs14WD66h8eGKpfaBNrHW5M=
github.com/elastic/gosigar v0.8.1-0.20180330100440-37f05ff46ffa h1:XKAhUk/dtp+CV0VO6mhG2V7jA9vbcGcnYF/Ay9NjZrY= github.com/elastic/gosigar v0.8.1-0.20180330100440-37f05ff46ffa h1:XKAhUk/dtp+CV0VO6mhG2V7jA9vbcGcnYF/Ay9NjZrY=
@ -77,6 +83,8 @@ github.com/go-logfmt/logfmt v0.3.0 h1:8HUsc87TaSWLKwrnumgC8/YconD2fJQsRJAsWaPg2i
github.com/go-logfmt/logfmt v0.3.0/go.mod h1:Qt1PoO58o5twSAckw1HlFXLmHsOX5/0LbT9GBnD5lWE= github.com/go-logfmt/logfmt v0.3.0/go.mod h1:Qt1PoO58o5twSAckw1HlFXLmHsOX5/0LbT9GBnD5lWE=
github.com/go-ole/go-ole v1.2.1 h1:2lOsA72HgjxAuMlKpFiCbHTvu44PIVkZ5hqm3RSdI/E= github.com/go-ole/go-ole v1.2.1 h1:2lOsA72HgjxAuMlKpFiCbHTvu44PIVkZ5hqm3RSdI/E=
github.com/go-ole/go-ole v1.2.1/go.mod h1:7FAglXiTm7HKlQRDeOQ6ZNUHidzCWXuZWq/1dTyBNF8= github.com/go-ole/go-ole v1.2.1/go.mod h1:7FAglXiTm7HKlQRDeOQ6ZNUHidzCWXuZWq/1dTyBNF8=
github.com/go-sourcemap/sourcemap v2.1.2+incompatible h1:0b/xya7BKGhXuqFESKM4oIiRo9WOt2ebz7KxfreD6ug=
github.com/go-sourcemap/sourcemap v2.1.2+incompatible/go.mod h1:F8jJfvm2KbVjc5NqelyYJmf/v5J0dwNLS2mL4sNA1Jg=
github.com/go-stack/stack v1.8.0 h1:5SgMzNM5HxrEjV0ww2lTmX6E2Izsfxas4+YHWRs3Lsk= github.com/go-stack/stack v1.8.0 h1:5SgMzNM5HxrEjV0ww2lTmX6E2Izsfxas4+YHWRs3Lsk=
github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY= github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY=
github.com/gogo/protobuf v1.1.1/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ= github.com/gogo/protobuf v1.1.1/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ=

@ -20,35 +20,43 @@ import (
"sort" "sort"
"strings" "strings"
"github.com/robertkrimen/otto" "github.com/dop251/goja"
) )
// CompleteKeywords returns potential continuations for the given line. Since line is // CompleteKeywords returns potential continuations for the given line. Since line is
// evaluated, callers need to make sure that evaluating line does not have side effects. // evaluated, callers need to make sure that evaluating line does not have side effects.
func (jsre *JSRE) CompleteKeywords(line string) []string { func (jsre *JSRE) CompleteKeywords(line string) []string {
var results []string var results []string
jsre.Do(func(vm *otto.Otto) { jsre.Do(func(vm *goja.Runtime) {
results = getCompletions(vm, line) results = getCompletions(vm, line)
}) })
return results return results
} }
func getCompletions(vm *otto.Otto, line string) (results []string) { func getCompletions(vm *goja.Runtime, line string) (results []string) {
parts := strings.Split(line, ".") parts := strings.Split(line, ".")
objRef := "this" if len(parts) == 0 {
prefix := line return nil
if len(parts) > 1 {
objRef = strings.Join(parts[0:len(parts)-1], ".")
prefix = parts[len(parts)-1]
} }
obj, _ := vm.Object(objRef) // Find the right-most fully named object in the line. e.g. if line = "x.y.z"
if obj == nil { // and "x.y" is an object, obj will reference "x.y".
return nil obj := vm.GlobalObject()
for i := 0; i < len(parts)-1; i++ {
v := obj.Get(parts[i])
if v == nil {
return nil // No object was found
}
obj = v.ToObject(vm)
} }
// Go over the keys of the object and retain the keys matching prefix.
// Example: if line = "x.y.z" and "x.y" exists and has keys "zebu", "zebra"
// and "platypus", then "x.y.zebu" and "x.y.zebra" will be added to results.
prefix := parts[len(parts)-1]
iterOwnAndConstructorKeys(vm, obj, func(k string) { iterOwnAndConstructorKeys(vm, obj, func(k string) {
if strings.HasPrefix(k, prefix) { if strings.HasPrefix(k, prefix) {
if objRef == "this" { if len(parts) == 1 {
results = append(results, k) results = append(results, k)
} else { } else {
results = append(results, strings.Join(parts[:len(parts)-1], ".")+"."+k) results = append(results, strings.Join(parts[:len(parts)-1], ".")+"."+k)
@ -59,9 +67,9 @@ func getCompletions(vm *otto.Otto, line string) (results []string) {
// Append opening parenthesis (for functions) or dot (for objects) // Append opening parenthesis (for functions) or dot (for objects)
// if the line itself is the only completion. // if the line itself is the only completion.
if len(results) == 1 && results[0] == line { if len(results) == 1 && results[0] == line {
obj, _ := vm.Object(line) obj := obj.Get(parts[len(parts)-1])
if obj != nil { if obj != nil {
if obj.Class() == "Function" { if _, isfunc := goja.AssertFunction(obj); isfunc {
results[0] += "(" results[0] += "("
} else { } else {
results[0] += "." results[0] += "."

@ -39,6 +39,10 @@ func TestCompleteKeywords(t *testing.T) {
input string input string
want []string want []string
}{ }{
{
input: "St",
want: []string{"String"},
},
{ {
input: "x", input: "x",
want: []string{"x."}, want: []string{"x."},

@ -26,30 +26,30 @@ import (
"math/rand" "math/rand"
"time" "time"
"github.com/dop251/goja"
"github.com/ethereum/go-ethereum/common" "github.com/ethereum/go-ethereum/common"
"github.com/ethereum/go-ethereum/internal/jsre/deps"
"github.com/robertkrimen/otto"
) )
var ( // JSRE is a JS runtime environment embedding the goja interpreter.
BignumberJs = deps.MustAsset("bignumber.js") // It provides helper functions to load code from files, run code snippets
Web3Js = deps.MustAsset("web3.js") // and bind native go objects to JS.
) //
// The runtime runs all code on a dedicated event loop and does not expose the underlying
/* // goja runtime directly. To use the runtime, call JSRE.Do. When binding a Go function,
JSRE is a generic JS runtime environment embedding the otto JS interpreter. // use the Call type to gain access to the runtime.
It provides some helper functions to
- load code from files
- run code snippets
- require libraries
- bind native go objects
*/
type JSRE struct { type JSRE struct {
assetPath string assetPath string
output io.Writer output io.Writer
evalQueue chan *evalReq evalQueue chan *evalReq
stopEventLoop chan bool stopEventLoop chan bool
closed chan struct{} closed chan struct{}
vm *goja.Runtime
}
// Call is the argument type of Go functions which are callable from JS.
type Call struct {
goja.FunctionCall
VM *goja.Runtime
} }
// jsTimer is a single timer instance with a callback function // jsTimer is a single timer instance with a callback function
@ -57,12 +57,12 @@ type jsTimer struct {
timer *time.Timer timer *time.Timer
duration time.Duration duration time.Duration
interval bool interval bool
call otto.FunctionCall call goja.FunctionCall
} }
// evalReq is a serialized vm execution request processed by runEventLoop. // evalReq is a serialized vm execution request processed by runEventLoop.
type evalReq struct { type evalReq struct {
fn func(vm *otto.Otto) fn func(vm *goja.Runtime)
done chan bool done chan bool
} }
@ -74,9 +74,10 @@ func New(assetPath string, output io.Writer) *JSRE {
closed: make(chan struct{}), closed: make(chan struct{}),
evalQueue: make(chan *evalReq), evalQueue: make(chan *evalReq),
stopEventLoop: make(chan bool), stopEventLoop: make(chan bool),
vm: goja.New(),
} }
go re.runEventLoop() go re.runEventLoop()
re.Set("loadScript", re.loadScript) re.Set("loadScript", MakeCallback(re.vm, re.loadScript))
re.Set("inspect", re.prettyPrintJS) re.Set("inspect", re.prettyPrintJS)
return re return re
} }
@ -99,21 +100,20 @@ func randomSource() *rand.Rand {
// serialized way and calls timer callback functions at the appropriate time. // serialized way and calls timer callback functions at the appropriate time.
// Exported functions always access the vm through the event queue. You can // Exported functions always access the vm through the event queue. You can
// call the functions of the otto vm directly to circumvent the queue. These // call the functions of the goja vm directly to circumvent the queue. These
// functions should be used if and only if running a routine that was already // functions should be used if and only if running a routine that was already
// called from JS through an RPC call. // called from JS through an RPC call.
func (re *JSRE) runEventLoop() { func (re *JSRE) runEventLoop() {
defer close(re.closed) defer close(re.closed)
vm := otto.New()
r := randomSource() r := randomSource()
vm.SetRandomSource(r.Float64) re.vm.SetRandSource(r.Float64)
registry := map[*jsTimer]*jsTimer{} registry := map[*jsTimer]*jsTimer{}
ready := make(chan *jsTimer) ready := make(chan *jsTimer)
newTimer := func(call otto.FunctionCall, interval bool) (*jsTimer, otto.Value) { newTimer := func(call goja.FunctionCall, interval bool) (*jsTimer, goja.Value) {
delay, _ := call.Argument(1).ToInteger() delay := call.Argument(1).ToInteger()
if 0 >= delay { if 0 >= delay {
delay = 1 delay = 1
} }
@ -128,47 +128,43 @@ func (re *JSRE) runEventLoop() {
ready <- timer ready <- timer
}) })
value, err := call.Otto.ToValue(timer) return timer, re.vm.ToValue(timer)
if err != nil {
panic(err)
}
return timer, value
} }
setTimeout := func(call otto.FunctionCall) otto.Value { setTimeout := func(call goja.FunctionCall) goja.Value {
_, value := newTimer(call, false) _, value := newTimer(call, false)
return value return value
} }
setInterval := func(call otto.FunctionCall) otto.Value { setInterval := func(call goja.FunctionCall) goja.Value {
_, value := newTimer(call, true) _, value := newTimer(call, true)
return value return value
} }
clearTimeout := func(call otto.FunctionCall) otto.Value { clearTimeout := func(call goja.FunctionCall) goja.Value {
timer, _ := call.Argument(0).Export() timer := call.Argument(0).Export()
if timer, ok := timer.(*jsTimer); ok { if timer, ok := timer.(*jsTimer); ok {
timer.timer.Stop() timer.timer.Stop()
delete(registry, timer) delete(registry, timer)
} }
return otto.UndefinedValue() return goja.Undefined()
} }
vm.Set("_setTimeout", setTimeout) re.vm.Set("_setTimeout", setTimeout)
vm.Set("_setInterval", setInterval) re.vm.Set("_setInterval", setInterval)
vm.Run(`var setTimeout = function(args) { re.vm.RunString(`var setTimeout = function(args) {
if (arguments.length < 1) { if (arguments.length < 1) {
throw TypeError("Failed to execute 'setTimeout': 1 argument required, but only 0 present."); throw TypeError("Failed to execute 'setTimeout': 1 argument required, but only 0 present.");
} }
return _setTimeout.apply(this, arguments); return _setTimeout.apply(this, arguments);
}`) }`)
vm.Run(`var setInterval = function(args) { re.vm.RunString(`var setInterval = function(args) {
if (arguments.length < 1) { if (arguments.length < 1) {
throw TypeError("Failed to execute 'setInterval': 1 argument required, but only 0 present."); throw TypeError("Failed to execute 'setInterval': 1 argument required, but only 0 present.");
} }
return _setInterval.apply(this, arguments); return _setInterval.apply(this, arguments);
}`) }`)
vm.Set("clearTimeout", clearTimeout) re.vm.Set("clearTimeout", clearTimeout)
vm.Set("clearInterval", clearTimeout) re.vm.Set("clearInterval", clearTimeout)
var waitForCallbacks bool var waitForCallbacks bool
@ -178,8 +174,8 @@ loop:
case timer := <-ready: case timer := <-ready:
// execute callback, remove/reschedule the timer // execute callback, remove/reschedule the timer
var arguments []interface{} var arguments []interface{}
if len(timer.call.ArgumentList) > 2 { if len(timer.call.Arguments) > 2 {
tmp := timer.call.ArgumentList[2:] tmp := timer.call.Arguments[2:]
arguments = make([]interface{}, 2+len(tmp)) arguments = make([]interface{}, 2+len(tmp))
for i, value := range tmp { for i, value := range tmp {
arguments[i+2] = value arguments[i+2] = value
@ -187,11 +183,12 @@ loop:
} else { } else {
arguments = make([]interface{}, 1) arguments = make([]interface{}, 1)
} }
arguments[0] = timer.call.ArgumentList[0] arguments[0] = timer.call.Arguments[0]
_, err := vm.Call(`Function.call.call`, nil, arguments...) call, isFunc := goja.AssertFunction(timer.call.Arguments[0])
if err != nil { if !isFunc {
fmt.Println("js error:", err, arguments) panic(re.vm.ToValue("js error: timer/timeout callback is not a function"))
} }
call(goja.Null(), timer.call.Arguments...)
_, inreg := registry[timer] // when clearInterval is called from within the callback don't reset it _, inreg := registry[timer] // when clearInterval is called from within the callback don't reset it
if timer.interval && inreg { if timer.interval && inreg {
@ -204,7 +201,7 @@ loop:
} }
case req := <-re.evalQueue: case req := <-re.evalQueue:
// run the code, send the result back // run the code, send the result back
req.fn(vm) req.fn(re.vm)
close(req.done) close(req.done)
if waitForCallbacks && (len(registry) == 0) { if waitForCallbacks && (len(registry) == 0) {
break loop break loop
@ -223,7 +220,7 @@ loop:
} }
// Do executes the given function on the JS event loop. // Do executes the given function on the JS event loop.
func (re *JSRE) Do(fn func(*otto.Otto)) { func (re *JSRE) Do(fn func(*goja.Runtime)) {
done := make(chan bool) done := make(chan bool)
req := &evalReq{fn, done} req := &evalReq{fn, done}
re.evalQueue <- req re.evalQueue <- req
@ -246,70 +243,36 @@ func (re *JSRE) Exec(file string) error {
if err != nil { if err != nil {
return err return err
} }
var script *otto.Script return re.Compile(file, string(code))
re.Do(func(vm *otto.Otto) {
script, err = vm.Compile(file, code)
if err != nil {
return
}
_, err = vm.Run(script)
})
return err
}
// Bind assigns value v to a variable in the JS environment
// This method is deprecated, use Set.
func (re *JSRE) Bind(name string, v interface{}) error {
return re.Set(name, v)
} }
// Run runs a piece of JS code. // Run runs a piece of JS code.
func (re *JSRE) Run(code string) (v otto.Value, err error) { func (re *JSRE) Run(code string) (v goja.Value, err error) {
re.Do(func(vm *otto.Otto) { v, err = vm.Run(code) }) re.Do(func(vm *goja.Runtime) { v, err = vm.RunString(code) })
return v, err
}
// Get returns the value of a variable in the JS environment.
func (re *JSRE) Get(ns string) (v otto.Value, err error) {
re.Do(func(vm *otto.Otto) { v, err = vm.Get(ns) })
return v, err return v, err
} }
// Set assigns value v to a variable in the JS environment. // Set assigns value v to a variable in the JS environment.
func (re *JSRE) Set(ns string, v interface{}) (err error) { func (re *JSRE) Set(ns string, v interface{}) (err error) {
re.Do(func(vm *otto.Otto) { err = vm.Set(ns, v) }) re.Do(func(vm *goja.Runtime) { vm.Set(ns, v) })
return err return err
} }
// loadScript executes a JS script from inside the currently executing JS code. // MakeCallback turns the given function into a function that's callable by JS.
func (re *JSRE) loadScript(call otto.FunctionCall) otto.Value { func MakeCallback(vm *goja.Runtime, fn func(Call) (goja.Value, error)) goja.Value {
file, err := call.Argument(0).ToString() return vm.ToValue(func(call goja.FunctionCall) goja.Value {
if err != nil { result, err := fn(Call{call, vm})
// TODO: throw exception if err != nil {
return otto.FalseValue() panic(vm.NewGoError(err))
} }
file = common.AbsolutePath(re.assetPath, file) return result
source, err := ioutil.ReadFile(file) })
if err != nil {
// TODO: throw exception
return otto.FalseValue()
}
if _, err := compileAndRun(call.Otto, file, source); err != nil {
// TODO: throw exception
fmt.Println("err:", err)
return otto.FalseValue()
}
// TODO: return evaluation result
return otto.TrueValue()
} }
// Evaluate executes code and pretty prints the result to the specified output // Evaluate executes code and pretty prints the result to the specified output stream.
// stream. func (re *JSRE) Evaluate(code string, w io.Writer) {
func (re *JSRE) Evaluate(code string, w io.Writer) error { re.Do(func(vm *goja.Runtime) {
var fail error val, err := vm.RunString(code)
re.Do(func(vm *otto.Otto) {
val, err := vm.Run(code)
if err != nil { if err != nil {
prettyError(vm, err, w) prettyError(vm, err, w)
} else { } else {
@ -317,19 +280,33 @@ func (re *JSRE) Evaluate(code string, w io.Writer) error {
} }
fmt.Fprintln(w) fmt.Fprintln(w)
}) })
return fail
} }
// Compile compiles and then runs a piece of JS code. // Compile compiles and then runs a piece of JS code.
func (re *JSRE) Compile(filename string, src interface{}) (err error) { func (re *JSRE) Compile(filename string, src string) (err error) {
re.Do(func(vm *otto.Otto) { _, err = compileAndRun(vm, filename, src) }) re.Do(func(vm *goja.Runtime) { _, err = compileAndRun(vm, filename, src) })
return err return err
} }
func compileAndRun(vm *otto.Otto, filename string, src interface{}) (otto.Value, error) { // loadScript loads and executes a JS file.
script, err := vm.Compile(filename, src) func (re *JSRE) loadScript(call Call) (goja.Value, error) {
file := call.Argument(0).ToString().String()
file = common.AbsolutePath(re.assetPath, file)
source, err := ioutil.ReadFile(file)
if err != nil {
return nil, fmt.Errorf("Could not read file %s: %v", file, err)
}
value, err := compileAndRun(re.vm, file, string(source))
if err != nil {
return nil, fmt.Errorf("Error while compiling or running script: %v", err)
}
return value, nil
}
func compileAndRun(vm *goja.Runtime, filename string, src string) (goja.Value, error) {
script, err := goja.Compile(filename, src, false)
if err != nil { if err != nil {
return otto.Value{}, err return goja.Null(), err
} }
return vm.Run(script) return vm.RunProgram(script)
} }

@ -20,25 +20,24 @@ import (
"io/ioutil" "io/ioutil"
"os" "os"
"path" "path"
"reflect"
"testing" "testing"
"time" "time"
"github.com/robertkrimen/otto" "github.com/dop251/goja"
) )
type testNativeObjectBinding struct{} type testNativeObjectBinding struct {
vm *goja.Runtime
}
type msg struct { type msg struct {
Msg string Msg string
} }
func (no *testNativeObjectBinding) TestMethod(call otto.FunctionCall) otto.Value { func (no *testNativeObjectBinding) TestMethod(call goja.FunctionCall) goja.Value {
m, err := call.Argument(0).ToString() m := call.Argument(0).ToString().String()
if err != nil { return no.vm.ToValue(&msg{m})
return otto.UndefinedValue()
}
v, _ := call.Otto.ToValue(&msg{m})
return v
} }
func newWithTestJS(t *testing.T, testjs string) (*JSRE, string) { func newWithTestJS(t *testing.T, testjs string) (*JSRE, string) {
@ -51,7 +50,8 @@ func newWithTestJS(t *testing.T, testjs string) (*JSRE, string) {
t.Fatal("cannot create test.js:", err) t.Fatal("cannot create test.js:", err)
} }
} }
return New(dir, os.Stdout), dir jsre := New(dir, os.Stdout)
return jsre, dir
} }
func TestExec(t *testing.T) { func TestExec(t *testing.T) {
@ -66,11 +66,11 @@ func TestExec(t *testing.T) {
if err != nil { if err != nil {
t.Errorf("expected no error, got %v", err) t.Errorf("expected no error, got %v", err)
} }
if !val.IsString() { if val.ExportType().Kind() != reflect.String {
t.Errorf("expected string value, got %v", val) t.Errorf("expected string value, got %v", val)
} }
exp := "testMsg" exp := "testMsg"
got, _ := val.ToString() got := val.ToString().String()
if exp != got { if exp != got {
t.Errorf("expected '%v', got '%v'", exp, got) t.Errorf("expected '%v', got '%v'", exp, got)
} }
@ -90,11 +90,11 @@ func TestNatto(t *testing.T) {
if err != nil { if err != nil {
t.Errorf("expected no error, got %v", err) t.Errorf("expected no error, got %v", err)
} }
if !val.IsString() { if val.ExportType().Kind() != reflect.String {
t.Errorf("expected string value, got %v", val) t.Errorf("expected string value, got %v", val)
} }
exp := "testMsg" exp := "testMsg"
got, _ := val.ToString() got := val.ToString().String()
if exp != got { if exp != got {
t.Errorf("expected '%v', got '%v'", exp, got) t.Errorf("expected '%v', got '%v'", exp, got)
} }
@ -105,7 +105,7 @@ func TestBind(t *testing.T) {
jsre := New("", os.Stdout) jsre := New("", os.Stdout)
defer jsre.Stop(false) defer jsre.Stop(false)
jsre.Bind("no", &testNativeObjectBinding{}) jsre.Set("no", &testNativeObjectBinding{vm: jsre.vm})
_, err := jsre.Run(`no.TestMethod("testMsg")`) _, err := jsre.Run(`no.TestMethod("testMsg")`)
if err != nil { if err != nil {
@ -125,11 +125,11 @@ func TestLoadScript(t *testing.T) {
if err != nil { if err != nil {
t.Errorf("expected no error, got %v", err) t.Errorf("expected no error, got %v", err)
} }
if !val.IsString() { if val.ExportType().Kind() != reflect.String {
t.Errorf("expected string value, got %v", val) t.Errorf("expected string value, got %v", val)
} }
exp := "testMsg" exp := "testMsg"
got, _ := val.ToString() got := val.ToString().String()
if exp != got { if exp != got {
t.Errorf("expected '%v', got '%v'", exp, got) t.Errorf("expected '%v', got '%v'", exp, got)
} }

@ -19,12 +19,13 @@ package jsre
import ( import (
"fmt" "fmt"
"io" "io"
"reflect"
"sort" "sort"
"strconv" "strconv"
"strings" "strings"
"github.com/dop251/goja"
"github.com/fatih/color" "github.com/fatih/color"
"github.com/robertkrimen/otto"
) )
const ( const (
@ -52,29 +53,29 @@ var boringKeys = map[string]bool{
} }
// prettyPrint writes value to standard output. // prettyPrint writes value to standard output.
func prettyPrint(vm *otto.Otto, value otto.Value, w io.Writer) { func prettyPrint(vm *goja.Runtime, value goja.Value, w io.Writer) {
ppctx{vm: vm, w: w}.printValue(value, 0, false) ppctx{vm: vm, w: w}.printValue(value, 0, false)
} }
// prettyError writes err to standard output. // prettyError writes err to standard output.
func prettyError(vm *otto.Otto, err error, w io.Writer) { func prettyError(vm *goja.Runtime, err error, w io.Writer) {
failure := err.Error() failure := err.Error()
if ottoErr, ok := err.(*otto.Error); ok { if gojaErr, ok := err.(*goja.Exception); ok {
failure = ottoErr.String() failure = gojaErr.String()
} }
fmt.Fprint(w, ErrorColor("%s", failure)) fmt.Fprint(w, ErrorColor("%s", failure))
} }
func (re *JSRE) prettyPrintJS(call otto.FunctionCall) otto.Value { func (re *JSRE) prettyPrintJS(call goja.FunctionCall) goja.Value {
for _, v := range call.ArgumentList { for _, v := range call.Arguments {
prettyPrint(call.Otto, v, re.output) prettyPrint(re.vm, v, re.output)
fmt.Fprintln(re.output) fmt.Fprintln(re.output)
} }
return otto.UndefinedValue() return goja.Undefined()
} }
type ppctx struct { type ppctx struct {
vm *otto.Otto vm *goja.Runtime
w io.Writer w io.Writer
} }
@ -82,35 +83,47 @@ func (ctx ppctx) indent(level int) string {
return strings.Repeat(indentString, level) return strings.Repeat(indentString, level)
} }
func (ctx ppctx) printValue(v otto.Value, level int, inArray bool) { func (ctx ppctx) printValue(v goja.Value, level int, inArray bool) {
if goja.IsNull(v) || goja.IsUndefined(v) {
fmt.Fprint(ctx.w, SpecialColor(v.String()))
return
}
kind := v.ExportType().Kind()
switch { switch {
case v.IsObject(): case kind == reflect.Bool:
ctx.printObject(v.Object(), level, inArray) fmt.Fprint(ctx.w, SpecialColor("%t", v.ToBoolean()))
case v.IsNull(): case kind == reflect.String:
fmt.Fprint(ctx.w, SpecialColor("null")) fmt.Fprint(ctx.w, StringColor("%q", v.String()))
case v.IsUndefined(): case kind >= reflect.Int && kind <= reflect.Complex128:
fmt.Fprint(ctx.w, SpecialColor("undefined")) fmt.Fprint(ctx.w, NumberColor("%s", v.String()))
case v.IsString():
s, _ := v.ToString()
fmt.Fprint(ctx.w, StringColor("%q", s))
case v.IsBoolean():
b, _ := v.ToBoolean()
fmt.Fprint(ctx.w, SpecialColor("%t", b))
case v.IsNaN():
fmt.Fprint(ctx.w, NumberColor("NaN"))
case v.IsNumber():
s, _ := v.ToString()
fmt.Fprint(ctx.w, NumberColor("%s", s))
default: default:
fmt.Fprint(ctx.w, "<unprintable>") if obj, ok := v.(*goja.Object); ok {
ctx.printObject(obj, level, inArray)
} else {
fmt.Fprintf(ctx.w, "<unprintable %T>", v)
}
} }
} }
func (ctx ppctx) printObject(obj *otto.Object, level int, inArray bool) { // SafeGet attempt to get the value associated to `key`, and
switch obj.Class() { // catches the panic that goja creates if an error occurs in
// key.
func SafeGet(obj *goja.Object, key string) (ret goja.Value) {
defer func() {
if r := recover(); r != nil {
ret = goja.Undefined()
}
}()
ret = obj.Get(key)
return ret
}
func (ctx ppctx) printObject(obj *goja.Object, level int, inArray bool) {
switch obj.ClassName() {
case "Array", "GoArray": case "Array", "GoArray":
lv, _ := obj.Get("length") lv := obj.Get("length")
len, _ := lv.ToInteger() len := lv.ToInteger()
if len == 0 { if len == 0 {
fmt.Fprintf(ctx.w, "[]") fmt.Fprintf(ctx.w, "[]")
return return
@ -121,8 +134,8 @@ func (ctx ppctx) printObject(obj *otto.Object, level int, inArray bool) {
} }
fmt.Fprint(ctx.w, "[") fmt.Fprint(ctx.w, "[")
for i := int64(0); i < len; i++ { for i := int64(0); i < len; i++ {
el, err := obj.Get(strconv.FormatInt(i, 10)) el := obj.Get(strconv.FormatInt(i, 10))
if err == nil { if el != nil {
ctx.printValue(el, level+1, true) ctx.printValue(el, level+1, true)
} }
if i < len-1 { if i < len-1 {
@ -149,7 +162,7 @@ func (ctx ppctx) printObject(obj *otto.Object, level int, inArray bool) {
} }
fmt.Fprintln(ctx.w, "{") fmt.Fprintln(ctx.w, "{")
for i, k := range keys { for i, k := range keys {
v, _ := obj.Get(k) v := SafeGet(obj, k)
fmt.Fprintf(ctx.w, "%s%s: ", ctx.indent(level+1), k) fmt.Fprintf(ctx.w, "%s%s: ", ctx.indent(level+1), k)
ctx.printValue(v, level+1, false) ctx.printValue(v, level+1, false)
if i < len(keys)-1 { if i < len(keys)-1 {
@ -163,29 +176,25 @@ func (ctx ppctx) printObject(obj *otto.Object, level int, inArray bool) {
fmt.Fprintf(ctx.w, "%s}", ctx.indent(level)) fmt.Fprintf(ctx.w, "%s}", ctx.indent(level))
case "Function": case "Function":
// Use toString() to display the argument list if possible. robj := obj.ToString()
if robj, err := obj.Call("toString"); err != nil { desc := strings.Trim(strings.Split(robj.String(), "{")[0], " \t\n")
fmt.Fprint(ctx.w, FunctionColor("function()")) desc = strings.Replace(desc, " (", "(", 1)
} else { fmt.Fprint(ctx.w, FunctionColor("%s", desc))
desc := strings.Trim(strings.Split(robj.String(), "{")[0], " \t\n")
desc = strings.Replace(desc, " (", "(", 1)
fmt.Fprint(ctx.w, FunctionColor("%s", desc))
}
case "RegExp": case "RegExp":
fmt.Fprint(ctx.w, StringColor("%s", toString(obj))) fmt.Fprint(ctx.w, StringColor("%s", toString(obj)))
default: default:
if v, _ := obj.Get("toString"); v.IsFunction() && level <= maxPrettyPrintLevel { if level <= maxPrettyPrintLevel {
s, _ := obj.Call("toString") s := obj.ToString().String()
fmt.Fprintf(ctx.w, "<%s %s>", obj.Class(), s.String()) fmt.Fprintf(ctx.w, "<%s %s>", obj.ClassName(), s)
} else { } else {
fmt.Fprintf(ctx.w, "<%s>", obj.Class()) fmt.Fprintf(ctx.w, "<%s>", obj.ClassName())
} }
} }
} }
func (ctx ppctx) fields(obj *otto.Object) []string { func (ctx ppctx) fields(obj *goja.Object) []string {
var ( var (
vals, methods []string vals, methods []string
seen = make(map[string]bool) seen = make(map[string]bool)
@ -195,11 +204,22 @@ func (ctx ppctx) fields(obj *otto.Object) []string {
return return
} }
seen[k] = true seen[k] = true
if v, _ := obj.Get(k); v.IsFunction() {
methods = append(methods, k) key := SafeGet(obj, k)
} else { if key == nil {
// The value corresponding to that key could not be found
// (typically because it is backed by an RPC call that is
// not supported by this instance. Add it to the list of
// values so that it appears as `undefined` to the user.
vals = append(vals, k) vals = append(vals, k)
} else {
if _, callable := goja.AssertFunction(key); callable {
methods = append(methods, k)
} else {
vals = append(vals, k)
}
} }
} }
iterOwnAndConstructorKeys(ctx.vm, obj, add) iterOwnAndConstructorKeys(ctx.vm, obj, add)
sort.Strings(vals) sort.Strings(vals)
@ -207,13 +227,13 @@ func (ctx ppctx) fields(obj *otto.Object) []string {
return append(vals, methods...) return append(vals, methods...)
} }
func iterOwnAndConstructorKeys(vm *otto.Otto, obj *otto.Object, f func(string)) { func iterOwnAndConstructorKeys(vm *goja.Runtime, obj *goja.Object, f func(string)) {
seen := make(map[string]bool) seen := make(map[string]bool)
iterOwnKeys(vm, obj, func(prop string) { iterOwnKeys(vm, obj, func(prop string) {
seen[prop] = true seen[prop] = true
f(prop) f(prop)
}) })
if cp := constructorPrototype(obj); cp != nil { if cp := constructorPrototype(vm, obj); cp != nil {
iterOwnKeys(vm, cp, func(prop string) { iterOwnKeys(vm, cp, func(prop string) {
if !seen[prop] { if !seen[prop] {
f(prop) f(prop)
@ -222,10 +242,17 @@ func iterOwnAndConstructorKeys(vm *otto.Otto, obj *otto.Object, f func(string))
} }
} }
func iterOwnKeys(vm *otto.Otto, obj *otto.Object, f func(string)) { func iterOwnKeys(vm *goja.Runtime, obj *goja.Object, f func(string)) {
Object, _ := vm.Object("Object") Object := vm.Get("Object").ToObject(vm)
rv, _ := Object.Call("getOwnPropertyNames", obj.Value()) getOwnPropertyNames, isFunc := goja.AssertFunction(Object.Get("getOwnPropertyNames"))
gv, _ := rv.Export() if !isFunc {
panic(vm.ToValue("Object.getOwnPropertyNames isn't a function"))
}
rv, err := getOwnPropertyNames(goja.Null(), obj)
if err != nil {
panic(vm.ToValue(fmt.Sprintf("Error getting object properties: %v", err)))
}
gv := rv.Export()
switch gv := gv.(type) { switch gv := gv.(type) {
case []interface{}: case []interface{}:
for _, v := range gv { for _, v := range gv {
@ -240,32 +267,35 @@ func iterOwnKeys(vm *otto.Otto, obj *otto.Object, f func(string)) {
} }
} }
func (ctx ppctx) isBigNumber(v *otto.Object) bool { func (ctx ppctx) isBigNumber(v *goja.Object) bool {
// Handle numbers with custom constructor. // Handle numbers with custom constructor.
if v, _ := v.Get("constructor"); v.Object() != nil { if obj := v.Get("constructor").ToObject(ctx.vm); obj != nil {
if strings.HasPrefix(toString(v.Object()), "function BigNumber") { if strings.HasPrefix(toString(obj), "function BigNumber") {
return true return true
} }
} }
// Handle default constructor. // Handle default constructor.
BigNumber, _ := ctx.vm.Object("BigNumber.prototype") BigNumber := ctx.vm.Get("BigNumber").ToObject(ctx.vm)
if BigNumber == nil { if BigNumber == nil {
return false return false
} }
bv, _ := BigNumber.Call("isPrototypeOf", v) prototype := BigNumber.Get("prototype").ToObject(ctx.vm)
b, _ := bv.ToBoolean() isPrototypeOf, callable := goja.AssertFunction(prototype.Get("isPrototypeOf"))
return b if !callable {
return false
}
bv, _ := isPrototypeOf(prototype, v)
return bv.ToBoolean()
} }
func toString(obj *otto.Object) string { func toString(obj *goja.Object) string {
s, _ := obj.Call("toString") return obj.ToString().String()
return s.String()
} }
func constructorPrototype(obj *otto.Object) *otto.Object { func constructorPrototype(vm *goja.Runtime, obj *goja.Object) *goja.Object {
if v, _ := obj.Get("constructor"); v.Object() != nil { if v := obj.Get("constructor"); v != nil {
if v, _ = v.Object().Get("prototype"); v.Object() != nil { if v := v.ToObject(vm).Get("prototype"); v != nil {
return v.Object() return v.ToObject(vm)
} }
} }
return nil return nil

Loading…
Cancel
Save