|
|
@@ -3,10 +3,11 @@ package jsre
|
|
|
import (
|
|
|
"fmt"
|
|
|
"io/ioutil"
|
|
|
-
|
|
|
- "github.com/robertkrimen/otto"
|
|
|
+ "sync"
|
|
|
+ "time"
|
|
|
|
|
|
"github.com/ethereum/go-ethereum/common"
|
|
|
+ "github.com/robertkrimen/otto"
|
|
|
)
|
|
|
|
|
|
/*
|
|
|
@@ -20,59 +21,261 @@ It provides some helper functions to
|
|
|
type JSRE struct {
|
|
|
assetPath string
|
|
|
vm *otto.Otto
|
|
|
+
|
|
|
+ evalQueue chan *evalReq
|
|
|
+ stopEventLoop chan bool
|
|
|
+ loopWg sync.WaitGroup
|
|
|
+}
|
|
|
+
|
|
|
+// jsTimer is a single timer instance with a callback function
|
|
|
+type jsTimer struct {
|
|
|
+ timer *time.Timer
|
|
|
+ duration time.Duration
|
|
|
+ interval bool
|
|
|
+ call otto.FunctionCall
|
|
|
}
|
|
|
|
|
|
+// evalResult is a structure to store the result of any serialized vm execution
|
|
|
+type evalResult struct {
|
|
|
+ result otto.Value
|
|
|
+ err error
|
|
|
+}
|
|
|
+
|
|
|
+// evalReq is a serialized vm execution request put in evalQueue and processed by runEventLoop
|
|
|
+type evalReq struct {
|
|
|
+ fn func(res *evalResult)
|
|
|
+ done chan bool
|
|
|
+ res evalResult
|
|
|
+}
|
|
|
+
|
|
|
+// runtime must be stopped with Stop() after use and cannot be used after stopping
|
|
|
func New(assetPath string) *JSRE {
|
|
|
re := &JSRE{
|
|
|
- assetPath,
|
|
|
- otto.New(),
|
|
|
+ assetPath: assetPath,
|
|
|
+ vm: otto.New(),
|
|
|
}
|
|
|
|
|
|
// load prettyprint func definition
|
|
|
re.vm.Run(pp_js)
|
|
|
re.vm.Set("loadScript", re.loadScript)
|
|
|
|
|
|
+ re.evalQueue = make(chan *evalReq)
|
|
|
+ re.stopEventLoop = make(chan bool)
|
|
|
+ re.loopWg.Add(1)
|
|
|
+ go re.runEventLoop()
|
|
|
+
|
|
|
return re
|
|
|
}
|
|
|
|
|
|
+// this function runs a piece of JS code either in a serialized way (when useEQ is true) or instantly, circumventing the evalQueue
|
|
|
+func (self *JSRE) run(src interface{}, useEQ bool) (value otto.Value, err error) {
|
|
|
+ if useEQ {
|
|
|
+ done := make(chan bool)
|
|
|
+ req := &evalReq{
|
|
|
+ fn: func(res *evalResult) {
|
|
|
+ res.result, res.err = self.vm.Run(src)
|
|
|
+ },
|
|
|
+ done: done,
|
|
|
+ }
|
|
|
+ self.evalQueue <- req
|
|
|
+ <-done
|
|
|
+ return req.res.result, req.res.err
|
|
|
+ } else {
|
|
|
+ return self.vm.Run(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 otto 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 (self *JSRE) runEventLoop() {
|
|
|
+ registry := map[*jsTimer]*jsTimer{}
|
|
|
+ ready := make(chan *jsTimer)
|
|
|
+
|
|
|
+ newTimer := func(call otto.FunctionCall, interval bool) (*jsTimer, otto.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
|
|
|
+ })
|
|
|
+
|
|
|
+ value, err := call.Otto.ToValue(timer)
|
|
|
+ if err != nil {
|
|
|
+ panic(err)
|
|
|
+ }
|
|
|
+
|
|
|
+ return timer, value
|
|
|
+ }
|
|
|
+
|
|
|
+ setTimeout := func(call otto.FunctionCall) otto.Value {
|
|
|
+ _, value := newTimer(call, false)
|
|
|
+ return value
|
|
|
+ }
|
|
|
+
|
|
|
+ setInterval := func(call otto.FunctionCall) otto.Value {
|
|
|
+ _, value := newTimer(call, true)
|
|
|
+ return value
|
|
|
+ }
|
|
|
+
|
|
|
+ clearTimeout := func(call otto.FunctionCall) otto.Value {
|
|
|
+ timer, _ := call.Argument(0).Export()
|
|
|
+ if timer, ok := timer.(*jsTimer); ok {
|
|
|
+ timer.timer.Stop()
|
|
|
+ delete(registry, timer)
|
|
|
+ }
|
|
|
+ return otto.UndefinedValue()
|
|
|
+ }
|
|
|
+
|
|
|
+ var waitForCallbacks bool
|
|
|
+
|
|
|
+loop:
|
|
|
+ for {
|
|
|
+ select {
|
|
|
+ case timer := <-ready:
|
|
|
+ // execute callback, remove/reschedule the timer
|
|
|
+ var arguments []interface{}
|
|
|
+ if len(timer.call.ArgumentList) > 2 {
|
|
|
+ tmp := timer.call.ArgumentList[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.ArgumentList[0]
|
|
|
+ _, err := self.vm.Call(`Function.call.call`, nil, arguments...)
|
|
|
+
|
|
|
+ if err != nil {
|
|
|
+ break loop
|
|
|
+ }
|
|
|
+ if timer.interval {
|
|
|
+ timer.timer.Reset(timer.duration)
|
|
|
+ } else {
|
|
|
+ delete(registry, timer)
|
|
|
+ if waitForCallbacks && (len(registry) == 0) {
|
|
|
+ break loop
|
|
|
+ }
|
|
|
+ }
|
|
|
+ case evalReq := <-self.evalQueue:
|
|
|
+ // run the code, send the result back
|
|
|
+ self.vm.Set("setTimeout", setTimeout)
|
|
|
+ self.vm.Set("setInterval", setInterval)
|
|
|
+ self.vm.Set("clearTimeout", clearTimeout)
|
|
|
+ self.vm.Set("clearInterval", clearTimeout)
|
|
|
+ evalReq.fn(&evalReq.res)
|
|
|
+ close(evalReq.done)
|
|
|
+ if waitForCallbacks && (len(registry) == 0) {
|
|
|
+ break loop
|
|
|
+ }
|
|
|
+ case waitForCallbacks = <-self.stopEventLoop:
|
|
|
+ if !waitForCallbacks || (len(registry) == 0) {
|
|
|
+ break loop
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ for _, timer := range registry {
|
|
|
+ timer.timer.Stop()
|
|
|
+ delete(registry, timer)
|
|
|
+ }
|
|
|
+
|
|
|
+ self.loopWg.Done()
|
|
|
+}
|
|
|
+
|
|
|
+// stops the event loop before exit, optionally waits for all timers to expire
|
|
|
+func (self *JSRE) Stop(waitForCallbacks bool) {
|
|
|
+ self.stopEventLoop <- waitForCallbacks
|
|
|
+ self.loopWg.Wait()
|
|
|
+}
|
|
|
+
|
|
|
// Exec(file) loads and runs the contents of a file
|
|
|
// if a relative path is given, the jsre's assetPath is used
|
|
|
func (self *JSRE) Exec(file string) error {
|
|
|
- return self.exec(common.AbsolutePath(self.assetPath, file))
|
|
|
+ return self.exec(common.AbsolutePath(self.assetPath, file), true)
|
|
|
}
|
|
|
|
|
|
-func (self *JSRE) exec(path string) error {
|
|
|
+// circumvents the eval queue, see runEventLoop
|
|
|
+func (self *JSRE) execWithoutEQ(file string) error {
|
|
|
+ return self.exec(common.AbsolutePath(self.assetPath, file), false)
|
|
|
+}
|
|
|
+
|
|
|
+func (self *JSRE) exec(path string, useEQ bool) error {
|
|
|
code, err := ioutil.ReadFile(path)
|
|
|
if err != nil {
|
|
|
return err
|
|
|
}
|
|
|
- _, err = self.vm.Run(code)
|
|
|
+ _, err = self.run(code, useEQ)
|
|
|
return err
|
|
|
}
|
|
|
|
|
|
+// assigns value v to a variable in the JS environment
|
|
|
func (self *JSRE) Bind(name string, v interface{}) (err error) {
|
|
|
- self.vm.Set(name, v)
|
|
|
+ self.Set(name, v)
|
|
|
return
|
|
|
}
|
|
|
|
|
|
+// runs a piece of JS code
|
|
|
func (self *JSRE) Run(code string) (otto.Value, error) {
|
|
|
- return self.vm.Run(code)
|
|
|
+ return self.run(code, true)
|
|
|
}
|
|
|
|
|
|
+// returns the value of a variable in the JS environment
|
|
|
func (self *JSRE) Get(ns string) (otto.Value, error) {
|
|
|
- return self.vm.Get(ns)
|
|
|
+ done := make(chan bool)
|
|
|
+ req := &evalReq{
|
|
|
+ fn: func(res *evalResult) {
|
|
|
+ res.result, res.err = self.vm.Get(ns)
|
|
|
+ },
|
|
|
+ done: done,
|
|
|
+ }
|
|
|
+ self.evalQueue <- req
|
|
|
+ <-done
|
|
|
+ return req.res.result, req.res.err
|
|
|
}
|
|
|
|
|
|
+// assigns value v to a variable in the JS environment
|
|
|
func (self *JSRE) Set(ns string, v interface{}) error {
|
|
|
- return self.vm.Set(ns, v)
|
|
|
+ done := make(chan bool)
|
|
|
+ req := &evalReq{
|
|
|
+ fn: func(res *evalResult) {
|
|
|
+ res.err = self.vm.Set(ns, v)
|
|
|
+ },
|
|
|
+ done: done,
|
|
|
+ }
|
|
|
+ self.evalQueue <- req
|
|
|
+ <-done
|
|
|
+ return req.res.err
|
|
|
}
|
|
|
|
|
|
+/*
|
|
|
+Executes a JS script from inside the currently executing JS code.
|
|
|
+Should only be called from inside an RPC routine.
|
|
|
+*/
|
|
|
func (self *JSRE) loadScript(call otto.FunctionCall) otto.Value {
|
|
|
file, err := call.Argument(0).ToString()
|
|
|
if err != nil {
|
|
|
return otto.FalseValue()
|
|
|
}
|
|
|
- if err := self.Exec(file); err != nil {
|
|
|
+ if err := self.execWithoutEQ(file); err != nil { // loadScript is only called from inside js
|
|
|
fmt.Println("err:", err)
|
|
|
return otto.FalseValue()
|
|
|
}
|
|
|
@@ -80,6 +283,7 @@ func (self *JSRE) loadScript(call otto.FunctionCall) otto.Value {
|
|
|
return otto.TrueValue()
|
|
|
}
|
|
|
|
|
|
+// uses the "prettyPrint" JS function to format a value
|
|
|
func (self *JSRE) PrettyPrint(v interface{}) (val otto.Value, err error) {
|
|
|
var method otto.Value
|
|
|
v, err = self.vm.ToValue(v)
|
|
|
@@ -93,6 +297,7 @@ func (self *JSRE) PrettyPrint(v interface{}) (val otto.Value, err error) {
|
|
|
return method.Call(method, v)
|
|
|
}
|
|
|
|
|
|
+// creates an otto value from a go type
|
|
|
func (self *JSRE) ToVal(v interface{}) otto.Value {
|
|
|
result, err := self.vm.ToValue(v)
|
|
|
if err != nil {
|
|
|
@@ -102,6 +307,7 @@ func (self *JSRE) ToVal(v interface{}) otto.Value {
|
|
|
return result
|
|
|
}
|
|
|
|
|
|
+// evaluates JS function and returns result in a pretty printed string format
|
|
|
func (self *JSRE) Eval(code string) (s string, err error) {
|
|
|
var val otto.Value
|
|
|
val, err = self.Run(code)
|
|
|
@@ -115,11 +321,12 @@ func (self *JSRE) Eval(code string) (s string, err error) {
|
|
|
return fmt.Sprintf("%v", val), nil
|
|
|
}
|
|
|
|
|
|
+// compiles and then runs a piece of JS code
|
|
|
func (self *JSRE) Compile(fn string, src interface{}) error {
|
|
|
script, err := self.vm.Compile(fn, src)
|
|
|
if err != nil {
|
|
|
return err
|
|
|
}
|
|
|
- self.vm.Run(script)
|
|
|
+ self.run(script, true)
|
|
|
return nil
|
|
|
}
|