341 lines
		
	
	
		
			9.4 KiB
		
	
	
	
		
			Go
		
	
	
	
	
	
			
		
		
	
	
			341 lines
		
	
	
		
			9.4 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"
 | |
| 	"errors"
 | |
| 	"fmt"
 | |
| 	"io"
 | |
| 	"math/rand"
 | |
| 	"os"
 | |
| 	"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.
 | |
| // When the runtime is stopped, fn will not execute.
 | |
| func (re *JSRE) Do(fn func(*goja.Runtime)) {
 | |
| 	done := make(chan bool)
 | |
| 	req := &evalReq{fn, done}
 | |
| 	select {
 | |
| 	case re.evalQueue <- req:
 | |
| 		<-done
 | |
| 	case <-re.closed:
 | |
| 	}
 | |
| }
 | |
| 
 | |
| // Stop terminates the event loop, optionally waiting for all timers to expire.
 | |
| func (re *JSRE) Stop(waitForCallbacks bool) {
 | |
| 	timeout := time.NewTimer(10 * time.Millisecond)
 | |
| 	defer timeout.Stop()
 | |
| 
 | |
| 	for {
 | |
| 		select {
 | |
| 		case <-re.closed:
 | |
| 			return
 | |
| 		case re.stopEventLoop <- waitForCallbacks:
 | |
| 			<-re.closed
 | |
| 			return
 | |
| 		case <-timeout.C:
 | |
| 			// JS is blocked, interrupt and try again.
 | |
| 			re.vm.Interrupt(errors.New("JS runtime stopped"))
 | |
| 		}
 | |
| 	}
 | |
| }
 | |
| 
 | |
| // 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 := os.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)
 | |
| 	})
 | |
| }
 | |
| 
 | |
| // Interrupt stops the current JS evaluation.
 | |
| func (re *JSRE) Interrupt(v interface{}) {
 | |
| 	done := make(chan bool)
 | |
| 	noop := func(*goja.Runtime) {}
 | |
| 
 | |
| 	select {
 | |
| 	case re.evalQueue <- &evalReq{noop, done}:
 | |
| 		// event loop is not blocked.
 | |
| 	default:
 | |
| 		re.vm.Interrupt(v)
 | |
| 	}
 | |
| }
 | |
| 
 | |
| // 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 := os.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)
 | |
| }
 |