diff options
Diffstat (limited to 'internal/jsre/jsre.go')
-rw-r--r-- | internal/jsre/jsre.go | 327 |
1 files changed, 327 insertions, 0 deletions
diff --git a/internal/jsre/jsre.go b/internal/jsre/jsre.go new file mode 100644 index 000000000..8d8f4fc2a --- /dev/null +++ b/internal/jsre/jsre.go @@ -0,0 +1,327 @@ +// 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" + "sync" + "time" + + "github.com/ethereum/go-ethereum/common" + "github.com/robertkrimen/otto" +) + +/* +JSRE is a generic JS runtime environment embedding the otto JS interpreter. +It provides some helper functions to +- load code from files +- run code snippets +- require libraries +- bind native go objects +*/ +type JSRE struct { + assetPath string + output io.Writer + 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 +} + +// evalReq is a serialized vm execution request processed by runEventLoop. +type evalReq struct { + fn func(vm *otto.Otto) + 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, + evalQueue: make(chan *evalReq), + stopEventLoop: make(chan bool), + } + re.loopWg.Add(1) + go re.runEventLoop() + re.Set("loadScript", re.loadScript) + re.Set("inspect", 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 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() { + vm := otto.New() + r := randomSource() + vm.SetRandomSource(r.Float64) + + 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() + } + vm.Set("_setTimeout", setTimeout) + vm.Set("_setInterval", setInterval) + vm.Run(`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); + }`) + vm.Run(`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); + }`) + vm.Set("clearTimeout", clearTimeout) + 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.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 := vm.Call(`Function.call.call`, nil, arguments...) + if err != nil { + fmt.Println("js error:", err, 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 := <-self.evalQueue: + // run the code, send the result back + req.fn(vm) + close(req.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() +} + +// Do executes the given function on the JS event loop. +func (self *JSRE) Do(fn func(*otto.Otto)) { + done := make(chan bool) + req := &evalReq{fn, done} + self.evalQueue <- req + <-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 { + code, err := ioutil.ReadFile(common.AbsolutePath(self.assetPath, file)) + if err != nil { + return err + } + var script *otto.Script + self.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 (self *JSRE) Bind(name string, v interface{}) error { + return self.Set(name, v) +} + +// Run runs a piece of JS code. +func (self *JSRE) Run(code string) (v otto.Value, err error) { + self.Do(func(vm *otto.Otto) { v, err = vm.Run(code) }) + return v, err +} + +// Get returns the value of a variable in the JS environment. +func (self *JSRE) Get(ns string) (v otto.Value, err error) { + self.Do(func(vm *otto.Otto) { v, err = vm.Get(ns) }) + return v, err +} + +// Set assigns value v to a variable in the JS environment. +func (self *JSRE) Set(ns string, v interface{}) (err error) { + self.Do(func(vm *otto.Otto) { err = vm.Set(ns, v) }) + return err +} + +// loadScript executes a JS script from inside the currently executing JS code. +func (self *JSRE) loadScript(call otto.FunctionCall) otto.Value { + file, err := call.Argument(0).ToString() + if err != nil { + // TODO: throw exception + return otto.FalseValue() + } + file = common.AbsolutePath(self.assetPath, file) + 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 +// stream. +func (self *JSRE) Evaluate(code string, w io.Writer) error { + var fail error + + self.Do(func(vm *otto.Otto) { + val, err := vm.Run(code) + if err != nil { + fail = err + } else { + prettyPrint(vm, val, w) + fmt.Fprintln(w) + } + }) + return fail +} + +// Compile compiles and then runs a piece of JS code. +func (self *JSRE) Compile(filename string, src interface{}) (err error) { + self.Do(func(vm *otto.Otto) { _, err = compileAndRun(vm, filename, src) }) + return err +} + +func compileAndRun(vm *otto.Otto, filename string, src interface{}) (otto.Value, error) { + script, err := vm.Compile(filename, src) + if err != nil { + return otto.Value{}, err + } + return vm.Run(script) +} |