7b68975a00
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.
313 lines
8.9 KiB
Go
313 lines
8.9 KiB
Go
// 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 jsre provides execution environment for JavaScript.
|
|
package jsre
|
|
|
|
import (
|
|
crand "crypto/rand"
|
|
"encoding/binary"
|
|
"fmt"
|
|
"io"
|
|
"io/ioutil"
|
|
"math/rand"
|
|
"time"
|
|
|
|
"github.com/dop251/goja"
|
|
"github.com/ethereum/go-ethereum/common"
|
|
)
|
|
|
|
// JSRE is a JS runtime environment embedding the goja interpreter.
|
|
// It provides helper functions to load code from files, run code snippets
|
|
// 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,
|
|
// use the Call type to gain access to the runtime.
|
|
type JSRE struct {
|
|
assetPath string
|
|
output io.Writer
|
|
evalQueue chan *evalReq
|
|
stopEventLoop chan bool
|
|
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
|
|
type jsTimer struct {
|
|
timer *time.Timer
|
|
duration time.Duration
|
|
interval bool
|
|
call goja.FunctionCall
|
|
}
|
|
|
|
// evalReq is a serialized vm execution request processed by runEventLoop.
|
|
type evalReq struct {
|
|
fn func(vm *goja.Runtime)
|
|
done chan bool
|
|
}
|
|
|
|
// runtime must be stopped with Stop() after use and cannot be used after stopping
|
|
func New(assetPath string, output io.Writer) *JSRE {
|
|
re := &JSRE{
|
|
assetPath: assetPath,
|
|
output: output,
|
|
closed: make(chan struct{}),
|
|
evalQueue: make(chan *evalReq),
|
|
stopEventLoop: make(chan bool),
|
|
vm: goja.New(),
|
|
}
|
|
go re.runEventLoop()
|
|
re.Set("loadScript", MakeCallback(re.vm, re.loadScript))
|
|
re.Set("inspect", re.prettyPrintJS)
|
|
return re
|
|
}
|
|
|
|
// randomSource returns a pseudo random value generator.
|
|
func randomSource() *rand.Rand {
|
|
bytes := make([]byte, 8)
|
|
seed := time.Now().UnixNano()
|
|
if _, err := crand.Read(bytes); err == nil {
|
|
seed = int64(binary.LittleEndian.Uint64(bytes))
|
|
}
|
|
|
|
src := rand.NewSource(seed)
|
|
return rand.New(src)
|
|
}
|
|
|
|
// This function runs the main event loop from a goroutine that is started
|
|
// when JSRE is created. Use Stop() before exiting to properly stop it.
|
|
// The event loop processes vm access requests from the evalQueue in a
|
|
// serialized way and calls timer callback functions at the appropriate time.
|
|
|
|
// Exported functions always access the vm through the event queue. You can
|
|
// 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
|
|
// called from JS through an RPC call.
|
|
func (re *JSRE) runEventLoop() {
|
|
defer close(re.closed)
|
|
|
|
r := randomSource()
|
|
re.vm.SetRandSource(r.Float64)
|
|
|
|
registry := map[*jsTimer]*jsTimer{}
|
|
ready := make(chan *jsTimer)
|
|
|
|
newTimer := func(call goja.FunctionCall, interval bool) (*jsTimer, goja.Value) {
|
|
delay := call.Argument(1).ToInteger()
|
|
if 0 >= delay {
|
|
delay = 1
|
|
}
|
|
timer := &jsTimer{
|
|
duration: time.Duration(delay) * time.Millisecond,
|
|
call: call,
|
|
interval: interval,
|
|
}
|
|
registry[timer] = timer
|
|
|
|
timer.timer = time.AfterFunc(timer.duration, func() {
|
|
ready <- timer
|
|
})
|
|
|
|
return timer, re.vm.ToValue(timer)
|
|
}
|
|
|
|
setTimeout := func(call goja.FunctionCall) goja.Value {
|
|
_, value := newTimer(call, false)
|
|
return value
|
|
}
|
|
|
|
setInterval := func(call goja.FunctionCall) goja.Value {
|
|
_, value := newTimer(call, true)
|
|
return value
|
|
}
|
|
|
|
clearTimeout := func(call goja.FunctionCall) goja.Value {
|
|
timer := call.Argument(0).Export()
|
|
if timer, ok := timer.(*jsTimer); ok {
|
|
timer.timer.Stop()
|
|
delete(registry, timer)
|
|
}
|
|
return goja.Undefined()
|
|
}
|
|
re.vm.Set("_setTimeout", setTimeout)
|
|
re.vm.Set("_setInterval", setInterval)
|
|
re.vm.RunString(`var setTimeout = function(args) {
|
|
if (arguments.length < 1) {
|
|
throw TypeError("Failed to execute 'setTimeout': 1 argument required, but only 0 present.");
|
|
}
|
|
return _setTimeout.apply(this, arguments);
|
|
}`)
|
|
re.vm.RunString(`var setInterval = function(args) {
|
|
if (arguments.length < 1) {
|
|
throw TypeError("Failed to execute 'setInterval': 1 argument required, but only 0 present.");
|
|
}
|
|
return _setInterval.apply(this, arguments);
|
|
}`)
|
|
re.vm.Set("clearTimeout", clearTimeout)
|
|
re.vm.Set("clearInterval", clearTimeout)
|
|
|
|
var waitForCallbacks bool
|
|
|
|
loop:
|
|
for {
|
|
select {
|
|
case timer := <-ready:
|
|
// execute callback, remove/reschedule the timer
|
|
var arguments []interface{}
|
|
if len(timer.call.Arguments) > 2 {
|
|
tmp := timer.call.Arguments[2:]
|
|
arguments = make([]interface{}, 2+len(tmp))
|
|
for i, value := range tmp {
|
|
arguments[i+2] = value
|
|
}
|
|
} else {
|
|
arguments = make([]interface{}, 1)
|
|
}
|
|
arguments[0] = timer.call.Arguments[0]
|
|
call, isFunc := goja.AssertFunction(timer.call.Arguments[0])
|
|
if !isFunc {
|
|
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
|
|
if timer.interval && inreg {
|
|
timer.timer.Reset(timer.duration)
|
|
} else {
|
|
delete(registry, timer)
|
|
if waitForCallbacks && (len(registry) == 0) {
|
|
break loop
|
|
}
|
|
}
|
|
case req := <-re.evalQueue:
|
|
// run the code, send the result back
|
|
req.fn(re.vm)
|
|
close(req.done)
|
|
if waitForCallbacks && (len(registry) == 0) {
|
|
break loop
|
|
}
|
|
case waitForCallbacks = <-re.stopEventLoop:
|
|
if !waitForCallbacks || (len(registry) == 0) {
|
|
break loop
|
|
}
|
|
}
|
|
}
|
|
|
|
for _, timer := range registry {
|
|
timer.timer.Stop()
|
|
delete(registry, timer)
|
|
}
|
|
}
|
|
|
|
// Do executes the given function on the JS event loop.
|
|
func (re *JSRE) Do(fn func(*goja.Runtime)) {
|
|
done := make(chan bool)
|
|
req := &evalReq{fn, done}
|
|
re.evalQueue <- req
|
|
<-done
|
|
}
|
|
|
|
// stops the event loop before exit, optionally waits for all timers to expire
|
|
func (re *JSRE) Stop(waitForCallbacks bool) {
|
|
select {
|
|
case <-re.closed:
|
|
case re.stopEventLoop <- waitForCallbacks:
|
|
<-re.closed
|
|
}
|
|
}
|
|
|
|
// Exec(file) loads and runs the contents of a file
|
|
// if a relative path is given, the jsre's assetPath is used
|
|
func (re *JSRE) Exec(file string) error {
|
|
code, err := ioutil.ReadFile(common.AbsolutePath(re.assetPath, file))
|
|
if err != nil {
|
|
return err
|
|
}
|
|
return re.Compile(file, string(code))
|
|
}
|
|
|
|
// Run runs a piece of JS code.
|
|
func (re *JSRE) Run(code string) (v goja.Value, err error) {
|
|
re.Do(func(vm *goja.Runtime) { v, err = vm.RunString(code) })
|
|
return v, err
|
|
}
|
|
|
|
// Set assigns value v to a variable in the JS environment.
|
|
func (re *JSRE) Set(ns string, v interface{}) (err error) {
|
|
re.Do(func(vm *goja.Runtime) { vm.Set(ns, v) })
|
|
return err
|
|
}
|
|
|
|
// MakeCallback turns the given function into a function that's callable by JS.
|
|
func MakeCallback(vm *goja.Runtime, fn func(Call) (goja.Value, error)) goja.Value {
|
|
return vm.ToValue(func(call goja.FunctionCall) goja.Value {
|
|
result, err := fn(Call{call, vm})
|
|
if err != nil {
|
|
panic(vm.NewGoError(err))
|
|
}
|
|
return result
|
|
})
|
|
}
|
|
|
|
// Evaluate executes code and pretty prints the result to the specified output stream.
|
|
func (re *JSRE) Evaluate(code string, w io.Writer) {
|
|
re.Do(func(vm *goja.Runtime) {
|
|
val, err := vm.RunString(code)
|
|
if err != nil {
|
|
prettyError(vm, err, w)
|
|
} else {
|
|
prettyPrint(vm, val, w)
|
|
}
|
|
fmt.Fprintln(w)
|
|
})
|
|
}
|
|
|
|
// Compile compiles and then runs a piece of JS code.
|
|
func (re *JSRE) Compile(filename string, src string) (err error) {
|
|
re.Do(func(vm *goja.Runtime) { _, err = compileAndRun(vm, filename, src) })
|
|
return err
|
|
}
|
|
|
|
// loadScript loads and executes a JS file.
|
|
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 {
|
|
return goja.Null(), err
|
|
}
|
|
return vm.RunProgram(script)
|
|
}
|