cmd/geth, jsre, rpc: run all JS code on the event loop

Some JSRE methods (PrettyPrint, ToVal) bypassed the event loop. All
calls to the JS VM are now wrapped. In order to make this somewhat more
foolproof, the otto VM is now a local variable inside the event loop.
This commit is contained in:
Felix Lange 2015-05-25 02:27:37 +02:00
parent 394826f520
commit e221a449e0
5 changed files with 126 additions and 178 deletions

View File

@ -144,7 +144,8 @@ func (js *jsre) pendingTransactions(call otto.FunctionCall) otto.Value {
} }
} }
return js.re.ToVal(ltxs) v, _ := call.Otto.ToValue(ltxs)
return v
} }
func (js *jsre) resend(call otto.FunctionCall) otto.Value { func (js *jsre) resend(call otto.FunctionCall) otto.Value {
@ -175,7 +176,8 @@ func (js *jsre) resend(call otto.FunctionCall) otto.Value {
} }
js.ethereum.TxPool().RemoveTransactions(types.Transactions{tx.tx}) js.ethereum.TxPool().RemoveTransactions(types.Transactions{tx.tx})
return js.re.ToVal(ret) v, _ := call.Otto.ToValue(ret)
return v
} }
fmt.Println("first argument must be a transaction") fmt.Println("first argument must be a transaction")
@ -198,12 +200,13 @@ func (js *jsre) sign(call otto.FunctionCall) otto.Value {
fmt.Println(err) fmt.Println(err)
return otto.UndefinedValue() return otto.UndefinedValue()
} }
v, err := js.xeth.Sign(signer, data, false) signed, err := js.xeth.Sign(signer, data, false)
if err != nil { if err != nil {
fmt.Println(err) fmt.Println(err)
return otto.UndefinedValue() return otto.UndefinedValue()
} }
return js.re.ToVal(v) v, _ := call.Otto.ToValue(signed)
return v
} }
func (js *jsre) debugBlock(call otto.FunctionCall) otto.Value { func (js *jsre) debugBlock(call otto.FunctionCall) otto.Value {
@ -237,8 +240,8 @@ func (js *jsre) setHead(call otto.FunctionCall) otto.Value {
func (js *jsre) downloadProgress(call otto.FunctionCall) otto.Value { func (js *jsre) downloadProgress(call otto.FunctionCall) otto.Value {
current, max := js.ethereum.Downloader().Stats() current, max := js.ethereum.Downloader().Stats()
v, _ := call.Otto.ToValue(fmt.Sprintf("%d/%d", current, max))
return js.re.ToVal(fmt.Sprintf("%d/%d", current, max)) return v
} }
func (js *jsre) getBlockRlp(call otto.FunctionCall) otto.Value { func (js *jsre) getBlockRlp(call otto.FunctionCall) otto.Value {
@ -248,7 +251,8 @@ func (js *jsre) getBlockRlp(call otto.FunctionCall) otto.Value {
return otto.UndefinedValue() return otto.UndefinedValue()
} }
encoded, _ := rlp.EncodeToBytes(block) encoded, _ := rlp.EncodeToBytes(block)
return js.re.ToVal(fmt.Sprintf("%x", encoded)) v, _ := call.Otto.ToValue(fmt.Sprintf("%x", encoded))
return v
} }
func (js *jsre) setExtra(call otto.FunctionCall) otto.Value { func (js *jsre) setExtra(call otto.FunctionCall) otto.Value {
@ -278,8 +282,9 @@ func (js *jsre) setGasPrice(call otto.FunctionCall) otto.Value {
return otto.UndefinedValue() return otto.UndefinedValue()
} }
func (js *jsre) hashrate(otto.FunctionCall) otto.Value { func (js *jsre) hashrate(call otto.FunctionCall) otto.Value {
return js.re.ToVal(js.ethereum.Miner().HashRate()) v, _ := call.Otto.ToValue(js.ethereum.Miner().HashRate())
return v
} }
func (js *jsre) makeDAG(call otto.FunctionCall) otto.Value { func (js *jsre) makeDAG(call otto.FunctionCall) otto.Value {
@ -495,15 +500,18 @@ func (js *jsre) newAccount(call otto.FunctionCall) otto.Value {
fmt.Printf("Could not create the account: %v", err) fmt.Printf("Could not create the account: %v", err)
return otto.UndefinedValue() return otto.UndefinedValue()
} }
return js.re.ToVal(acct.Address.Hex()) v, _ := call.Otto.ToValue(acct.Address.Hex())
return v
} }
func (js *jsre) nodeInfo(call otto.FunctionCall) otto.Value { func (js *jsre) nodeInfo(call otto.FunctionCall) otto.Value {
return js.re.ToVal(js.ethereum.NodeInfo()) v, _ := call.Otto.ToValue(js.ethereum.NodeInfo())
return v
} }
func (js *jsre) peers(call otto.FunctionCall) otto.Value { func (js *jsre) peers(call otto.FunctionCall) otto.Value {
return js.re.ToVal(js.ethereum.PeersInfo()) v, _ := call.Otto.ToValue(js.ethereum.PeersInfo())
return v
} }
func (js *jsre) importChain(call otto.FunctionCall) otto.Value { func (js *jsre) importChain(call otto.FunctionCall) otto.Value {
@ -562,7 +570,8 @@ func (js *jsre) dumpBlock(call otto.FunctionCall) otto.Value {
statedb := state.New(block.Root(), js.ethereum.StateDb()) statedb := state.New(block.Root(), js.ethereum.StateDb())
dump := statedb.RawDump() dump := statedb.RawDump()
return js.re.ToVal(dump) v, _ := call.Otto.ToValue(dump)
return v
} }
func (js *jsre) waitForBlocks(call otto.FunctionCall) otto.Value { func (js *jsre) waitForBlocks(call otto.FunctionCall) otto.Value {
@ -611,7 +620,8 @@ func (js *jsre) waitForBlocks(call otto.FunctionCall) otto.Value {
return otto.UndefinedValue() return otto.UndefinedValue()
case height = <-wait: case height = <-wait:
} }
return js.re.ToVal(height.Uint64()) v, _ := call.Otto.ToValue(height.Uint64())
return v
} }
func (js *jsre) sleep(call otto.FunctionCall) otto.Value { func (js *jsre) sleep(call otto.FunctionCall) otto.Value {
@ -704,8 +714,8 @@ func (js *jsre) register(call otto.FunctionCall) otto.Value {
return otto.UndefinedValue() return otto.UndefinedValue()
} }
return js.re.ToVal(contenthash.Hex()) v, _ := call.Otto.ToValue(contenthash.Hex())
return v
} }
func (js *jsre) registerUrl(call otto.FunctionCall) otto.Value { func (js *jsre) registerUrl(call otto.FunctionCall) otto.Value {
@ -764,7 +774,8 @@ func (js *jsre) getContractInfo(call otto.FunctionCall) otto.Value {
fmt.Println(err) fmt.Println(err)
return otto.UndefinedValue() return otto.UndefinedValue()
} }
return js.re.ToVal(info) v, _ := call.Otto.ToValue(info)
return v
} }
func (js *jsre) startNatSpec(call otto.FunctionCall) otto.Value { func (js *jsre) startNatSpec(call otto.FunctionCall) otto.Value {

View File

@ -104,7 +104,7 @@ func newJSRE(ethereum *eth.Ethereum, libPath, corsDomain string, interactive boo
func (js *jsre) apiBindings(f xeth.Frontend) { func (js *jsre) apiBindings(f xeth.Frontend) {
xe := xeth.New(js.ethereum, f) xe := xeth.New(js.ethereum, f)
ethApi := rpc.NewEthereumApi(xe) ethApi := rpc.NewEthereumApi(xe)
jeth := rpc.NewJeth(ethApi, js.re.ToVal, js.re) jeth := rpc.NewJeth(ethApi, js.re)
js.re.Set("jeth", struct{}{}) js.re.Set("jeth", struct{}{})
t, _ := js.re.Get("jeth") t, _ := js.re.Get("jeth")

View File

@ -19,9 +19,7 @@ It provides some helper functions to
- bind native go objects - bind native go objects
*/ */
type JSRE struct { type JSRE struct {
assetPath string assetPath string
vm *otto.Otto
evalQueue chan *evalReq evalQueue chan *evalReq
stopEventLoop chan bool stopEventLoop chan bool
loopWg sync.WaitGroup loopWg sync.WaitGroup
@ -35,68 +33,37 @@ type jsTimer struct {
call otto.FunctionCall call otto.FunctionCall
} }
// evalResult is a structure to store the result of any serialized vm execution // evalReq is a serialized vm execution request processed by runEventLoop.
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 { type evalReq struct {
fn func(res *evalResult) fn func(vm *otto.Otto)
done chan bool done chan bool
res evalResult
} }
// runtime must be stopped with Stop() after use and cannot be used after stopping // runtime must be stopped with Stop() after use and cannot be used after stopping
func New(assetPath string) *JSRE { func New(assetPath string) *JSRE {
re := &JSRE{ re := &JSRE{
assetPath: assetPath, assetPath: assetPath,
vm: otto.New(), evalQueue: make(chan *evalReq),
stopEventLoop: make(chan bool),
} }
// 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) re.loopWg.Add(1)
go re.runEventLoop() go re.runEventLoop()
re.Compile("pp.js", pp_js) // load prettyprint func definition
re.Set("loadScript", re.loadScript)
return re return re
} }
// this function runs a piece of JS code either in a serialized way (when useEQ is true) or instantly, circumventing the evalQueue // This function runs the main event loop from a goroutine that is started
func (self *JSRE) run(src interface{}, useEQ bool) (value otto.Value, err error) { // when JSRE is created. Use Stop() before exiting to properly stop it.
if useEQ { // The event loop processes vm access requests from the evalQueue in a
done := make(chan bool) // serialized way and calls timer callback functions at the appropriate time.
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)
}
}
/* // Exported functions always access the vm through the event queue. You can
This function runs the main event loop from a goroutine that is started // call the functions of the otto vm directly to circumvent the queue. These
when JSRE is created. Use Stop() before exiting to properly stop it. // functions should be used if and only if running a routine that was already
The event loop processes vm access requests from the evalQueue in a // called from JS through an RPC call.
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() { func (self *JSRE) runEventLoop() {
vm := otto.New()
registry := map[*jsTimer]*jsTimer{} registry := map[*jsTimer]*jsTimer{}
ready := make(chan *jsTimer) ready := make(chan *jsTimer)
@ -143,10 +110,10 @@ func (self *JSRE) runEventLoop() {
} }
return otto.UndefinedValue() return otto.UndefinedValue()
} }
self.vm.Set("setTimeout", setTimeout) vm.Set("setTimeout", setTimeout)
self.vm.Set("setInterval", setInterval) vm.Set("setInterval", setInterval)
self.vm.Set("clearTimeout", clearTimeout) vm.Set("clearTimeout", clearTimeout)
self.vm.Set("clearInterval", clearTimeout) vm.Set("clearInterval", clearTimeout)
var waitForCallbacks bool var waitForCallbacks bool
@ -166,8 +133,7 @@ loop:
arguments = make([]interface{}, 1) arguments = make([]interface{}, 1)
} }
arguments[0] = timer.call.ArgumentList[0] arguments[0] = timer.call.ArgumentList[0]
_, err := self.vm.Call(`Function.call.call`, nil, arguments...) _, err := vm.Call(`Function.call.call`, nil, arguments...)
if err != nil { if err != nil {
fmt.Println("js error:", err, arguments) fmt.Println("js error:", err, arguments)
} }
@ -179,10 +145,10 @@ loop:
break loop break loop
} }
} }
case evalReq := <-self.evalQueue: case req := <-self.evalQueue:
// run the code, send the result back // run the code, send the result back
evalReq.fn(&evalReq.res) req.fn(vm)
close(evalReq.done) close(req.done)
if waitForCallbacks && (len(registry) == 0) { if waitForCallbacks && (len(registry) == 0) {
break loop break loop
} }
@ -201,6 +167,14 @@ loop:
self.loopWg.Done() self.loopWg.Done()
} }
// do schedules the given function on the 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 // stops the event loop before exit, optionally waits for all timers to expire
func (self *JSRE) Stop(waitForCallbacks bool) { func (self *JSRE) Stop(waitForCallbacks bool) {
self.stopEventLoop <- waitForCallbacks self.stopEventLoop <- waitForCallbacks
@ -210,119 +184,78 @@ func (self *JSRE) Stop(waitForCallbacks bool) {
// Exec(file) loads and runs the contents of a file // Exec(file) loads and runs the contents of a file
// if a relative path is given, the jsre's assetPath is used // if a relative path is given, the jsre's assetPath is used
func (self *JSRE) Exec(file string) error { func (self *JSRE) Exec(file string) error {
return self.exec(common.AbsolutePath(self.assetPath, file), true) code, err := ioutil.ReadFile(common.AbsolutePath(self.assetPath, file))
}
// 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 { if err != nil {
return err return err
} }
_, err = self.run(code, useEQ) self.do(func(vm *otto.Otto) { _, err = vm.Run(code) })
return err return err
} }
// assigns value v to a variable in the JS environment // Bind assigns value v to a variable in the JS environment
func (self *JSRE) Bind(name string, v interface{}) (err error) { // This method is deprecated, use Set.
self.Set(name, v) func (self *JSRE) Bind(name string, v interface{}) error {
return return self.Set(name, v)
} }
// runs a piece of JS code // Run runs a piece of JS code.
func (self *JSRE) Run(code string) (otto.Value, error) { func (self *JSRE) Run(code string) (v otto.Value, err error) {
return self.run(code, true) self.do(func(vm *otto.Otto) { v, err = vm.Run(code) })
return v, err
} }
// returns the value of a variable in the JS environment // Get returns the value of a variable in the JS environment.
func (self *JSRE) Get(ns string) (otto.Value, error) { func (self *JSRE) Get(ns string) (v otto.Value, err error) {
done := make(chan bool) self.do(func(vm *otto.Otto) { v, err = vm.Get(ns) })
req := &evalReq{ return v, err
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 // Set assigns value v to a variable in the JS environment.
func (self *JSRE) Set(ns string, v interface{}) error { func (self *JSRE) Set(ns string, v interface{}) (err error) {
done := make(chan bool) self.do(func(vm *otto.Otto) { err = vm.Set(ns, v) })
req := &evalReq{ return err
fn: func(res *evalResult) {
res.err = self.vm.Set(ns, v)
},
done: done,
}
self.evalQueue <- req
<-done
return req.res.err
} }
/* // loadScript executes a JS script from inside the currently executing JS code.
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 { func (self *JSRE) loadScript(call otto.FunctionCall) otto.Value {
file, err := call.Argument(0).ToString() file, err := call.Argument(0).ToString()
if err != nil { if err != nil {
// TODO: throw exception
return otto.FalseValue() return otto.FalseValue()
} }
if err := self.execWithoutEQ(file); err != nil { // loadScript is only called from inside js 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) fmt.Println("err:", err)
return otto.FalseValue() return otto.FalseValue()
} }
// TODO: return evaluation result
return otto.TrueValue() return otto.TrueValue()
} }
// uses the "prettyPrint" JS function to format a value // PrettyPrint writes v to standard output.
func (self *JSRE) PrettyPrint(v interface{}) (val otto.Value, err error) { func (self *JSRE) PrettyPrint(v interface{}) (val otto.Value, err error) {
var method otto.Value var method otto.Value
v, err = self.ToValue(v) self.do(func(vm *otto.Otto) {
if err != nil { val, err = vm.ToValue(v)
return if err != nil {
} return
method, err = self.vm.Get("prettyPrint") }
if err != nil { method, err = vm.Get("prettyPrint")
return if err != nil {
} return
return method.Call(method, v) }
val, err = method.Call(method, val)
})
return val, err
} }
// creates an otto value from a go type (serialized version) // Eval evaluates JS function and returns result in a pretty printed string format.
func (self *JSRE) ToValue(v interface{}) (otto.Value, error) {
done := make(chan bool)
req := &evalReq{
fn: func(res *evalResult) {
res.result, res.err = self.vm.ToValue(v)
},
done: done,
}
self.evalQueue <- req
<-done
return req.res.result, req.res.err
}
// creates an otto value from a go type (non-serialized version)
func (self *JSRE) ToVal(v interface{}) otto.Value {
result, err := self.vm.ToValue(v)
if err != nil {
fmt.Println("Value unknown:", err)
return otto.UndefinedValue()
}
return result
}
// evaluates JS function and returns result in a pretty printed string format
func (self *JSRE) Eval(code string) (s string, err error) { func (self *JSRE) Eval(code string) (s string, err error) {
var val otto.Value var val otto.Value
val, err = self.Run(code) val, err = self.Run(code)
@ -336,12 +269,16 @@ func (self *JSRE) Eval(code string) (s string, err error) {
return fmt.Sprintf("%v", val), nil return fmt.Sprintf("%v", val), nil
} }
// compiles and then runs a piece of JS code // Compile compiles and then runs a piece of JS code.
func (self *JSRE) Compile(fn string, src interface{}) error { func (self *JSRE) Compile(filename string, src interface{}) (err error) {
script, err := self.vm.Compile(fn, src) self.do(func(vm *otto.Otto) { _, err = compileAndRun(vm, filename, src) })
if err != nil { return err
return err }
}
self.run(script, true) func compileAndRun(vm *otto.Otto, filename string, src interface{}) (otto.Value, error) {
return nil script, err := vm.Compile(filename, src)
if err != nil {
return otto.Value{}, err
}
return vm.Run(script)
} }

View File

@ -1,16 +1,15 @@
package jsre package jsre
import ( import (
"github.com/robertkrimen/otto"
"io/ioutil" "io/ioutil"
"os" "os"
"testing" "testing"
"time" "time"
"github.com/robertkrimen/otto"
) )
type testNativeObjectBinding struct { type testNativeObjectBinding struct{}
toVal func(interface{}) otto.Value
}
type msg struct { type msg struct {
Msg string Msg string
@ -21,7 +20,8 @@ func (no *testNativeObjectBinding) TestMethod(call otto.FunctionCall) otto.Value
if err != nil { if err != nil {
return otto.UndefinedValue() return otto.UndefinedValue()
} }
return no.toVal(&msg{m}) v, _ := call.Otto.ToValue(&msg{m})
return v
} }
func TestExec(t *testing.T) { func TestExec(t *testing.T) {
@ -74,7 +74,7 @@ func TestNatto(t *testing.T) {
func TestBind(t *testing.T) { func TestBind(t *testing.T) {
jsre := New("/tmp") jsre := New("/tmp")
jsre.Bind("no", &testNativeObjectBinding{jsre.ToVal}) jsre.Bind("no", &testNativeObjectBinding{})
val, err := jsre.Run(`no.TestMethod("testMsg")`) val, err := jsre.Run(`no.TestMethod("testMsg")`)
if err != nil { if err != nil {

View File

@ -3,18 +3,18 @@ package rpc
import ( import (
"encoding/json" "encoding/json"
"fmt" "fmt"
"github.com/ethereum/go-ethereum/jsre" "github.com/ethereum/go-ethereum/jsre"
"github.com/robertkrimen/otto" "github.com/robertkrimen/otto"
) )
type Jeth struct { type Jeth struct {
ethApi *EthereumApi ethApi *EthereumApi
toVal func(interface{}) otto.Value
re *jsre.JSRE re *jsre.JSRE
} }
func NewJeth(ethApi *EthereumApi, toVal func(interface{}) otto.Value, re *jsre.JSRE) *Jeth { func NewJeth(ethApi *EthereumApi, re *jsre.JSRE) *Jeth {
return &Jeth{ethApi, toVal, re} return &Jeth{ethApi, re}
} }
func (self *Jeth) err(call otto.FunctionCall, code int, msg string, id interface{}) (response otto.Value) { func (self *Jeth) err(call otto.FunctionCall, code int, msg string, id interface{}) (response otto.Value) {