123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312 |
- // 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)
- }
|