jsre.go 8.91 KB
Newer Older
1
// Copyright 2015 The go-ethereum Authors
2
// This file is part of the go-ethereum library.
3
//
4
// The go-ethereum library is free software: you can redistribute it and/or modify
5 6 7 8
// 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.
//
9
// The go-ethereum library is distributed in the hope that it will be useful,
10
// but WITHOUT ANY WARRANTY; without even the implied warranty of
11
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 13 14
// GNU Lesser General Public License for more details.
//
// You should have received a copy of the GNU Lesser General Public License
15
// along with the go-ethereum library. If not, see <http://www.gnu.org/licenses/>.
16

17
// Package jsre provides execution environment for JavaScript.
zelig's avatar
zelig committed
18 19 20
package jsre

import (
21 22
	crand "crypto/rand"
	"encoding/binary"
zelig's avatar
zelig committed
23
	"fmt"
24
	"io"
zelig's avatar
zelig committed
25
	"io/ioutil"
26
	"math/rand"
27
	"time"
28

zelig's avatar
zelig committed
29
	"github.com/ethereum/go-ethereum/common"
30
	"github.com/ethereum/go-ethereum/internal/jsre/deps"
31
	"github.com/robertkrimen/otto"
zelig's avatar
zelig committed
32 33
)

34 35 36 37 38
var (
	BigNumber_JS = deps.MustAsset("bignumber.js")
	Web3_JS      = deps.MustAsset("web3.js")
)

zelig's avatar
zelig committed
39 40 41 42 43 44 45 46 47
/*
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 {
48
	assetPath     string
49
	output        io.Writer
50 51
	evalQueue     chan *evalReq
	stopEventLoop chan bool
52
	closed        chan struct{}
53 54 55 56 57 58 59 60
}

// jsTimer is a single timer instance with a callback function
type jsTimer struct {
	timer    *time.Timer
	duration time.Duration
	interval bool
	call     otto.FunctionCall
zelig's avatar
zelig committed
61 62
}

63
// evalReq is a serialized vm execution request processed by runEventLoop.
64
type evalReq struct {
65
	fn   func(vm *otto.Otto)
66 67 68 69
	done chan bool
}

// runtime must be stopped with Stop() after use and cannot be used after stopping
70
func New(assetPath string, output io.Writer) *JSRE {
zelig's avatar
zelig committed
71
	re := &JSRE{
72
		assetPath:     assetPath,
73
		output:        output,
74
		closed:        make(chan struct{}),
75 76
		evalQueue:     make(chan *evalReq),
		stopEventLoop: make(chan bool),
zelig's avatar
zelig committed
77
	}
78
	go re.runEventLoop()
79
	re.Set("loadScript", re.loadScript)
80
	re.Set("inspect", re.prettyPrintJS)
zelig's avatar
zelig committed
81 82 83
	return re
}

84 85 86 87 88 89 90 91 92 93 94 95
// 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)
}

96 97 98 99
// 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.
100

101 102 103 104
// 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.
105 106
func (re *JSRE) runEventLoop() {
	defer close(re.closed)
107

108
	vm := otto.New()
109 110 111
	r := randomSource()
	vm.SetRandomSource(r.Float64)

112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155
	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()
	}
156 157 158 159 160 161 162 163 164 165 166 167 168 169
	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);
	}`)
170 171
	vm.Set("clearTimeout", clearTimeout)
	vm.Set("clearInterval", clearTimeout)
172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190

	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]
191
			_, err := vm.Call(`Function.call.call`, nil, arguments...)
192
			if err != nil {
193
				fmt.Println("js error:", err, arguments)
194
			}
195

196 197
			_, inreg := registry[timer] // when clearInterval is called from within the callback don't reset it
			if timer.interval && inreg {
198 199 200 201 202 203 204
				timer.timer.Reset(timer.duration)
			} else {
				delete(registry, timer)
				if waitForCallbacks && (len(registry) == 0) {
					break loop
				}
			}
205
		case req := <-re.evalQueue:
206
			// run the code, send the result back
207 208
			req.fn(vm)
			close(req.done)
209 210 211
			if waitForCallbacks && (len(registry) == 0) {
				break loop
			}
212
		case waitForCallbacks = <-re.stopEventLoop:
213 214 215 216 217 218 219 220 221 222 223 224
			if !waitForCallbacks || (len(registry) == 0) {
				break loop
			}
		}
	}

	for _, timer := range registry {
		timer.timer.Stop()
		delete(registry, timer)
	}
}

Felix Lange's avatar
Felix Lange committed
225
// Do executes the given function on the JS event loop.
226
func (re *JSRE) Do(fn func(*otto.Otto)) {
227 228
	done := make(chan bool)
	req := &evalReq{fn, done}
229
	re.evalQueue <- req
230 231 232
	<-done
}

233
// stops the event loop before exit, optionally waits for all timers to expire
234
func (re *JSRE) Stop(waitForCallbacks bool) {
235
	select {
236 237 238
	case <-re.closed:
	case re.stopEventLoop <- waitForCallbacks:
		<-re.closed
239
	}
240 241
}

zelig's avatar
zelig committed
242 243
// Exec(file) loads and runs the contents of a file
// if a relative path is given, the jsre's assetPath is used
244 245
func (re *JSRE) Exec(file string) error {
	code, err := ioutil.ReadFile(common.AbsolutePath(re.assetPath, file))
zelig's avatar
zelig committed
246 247 248
	if err != nil {
		return err
	}
249
	var script *otto.Script
250
	re.Do(func(vm *otto.Otto) {
251 252 253 254 255 256
		script, err = vm.Compile(file, code)
		if err != nil {
			return
		}
		_, err = vm.Run(script)
	})
zelig's avatar
zelig committed
257 258 259
	return err
}

260 261
// Bind assigns value v to a variable in the JS environment
// This method is deprecated, use Set.
262 263
func (re *JSRE) Bind(name string, v interface{}) error {
	return re.Set(name, v)
zelig's avatar
zelig committed
264 265
}

266
// Run runs a piece of JS code.
267 268
func (re *JSRE) Run(code string) (v otto.Value, err error) {
	re.Do(func(vm *otto.Otto) { v, err = vm.Run(code) })
269
	return v, err
zelig's avatar
zelig committed
270 271
}

272
// Get returns the value of a variable in the JS environment.
273 274
func (re *JSRE) Get(ns string) (v otto.Value, err error) {
	re.Do(func(vm *otto.Otto) { v, err = vm.Get(ns) })
275
	return v, err
zelig's avatar
zelig committed
276 277
}

278
// Set assigns value v to a variable in the JS environment.
279 280
func (re *JSRE) Set(ns string, v interface{}) (err error) {
	re.Do(func(vm *otto.Otto) { err = vm.Set(ns, v) })
281
	return err
zelig's avatar
zelig committed
282 283
}

284
// loadScript executes a JS script from inside the currently executing JS code.
285
func (re *JSRE) loadScript(call otto.FunctionCall) otto.Value {
zelig's avatar
zelig committed
286 287
	file, err := call.Argument(0).ToString()
	if err != nil {
288 289 290
		// TODO: throw exception
		return otto.FalseValue()
	}
291
	file = common.AbsolutePath(re.assetPath, file)
292 293 294
	source, err := ioutil.ReadFile(file)
	if err != nil {
		// TODO: throw exception
zelig's avatar
zelig committed
295 296
		return otto.FalseValue()
	}
297 298
	if _, err := compileAndRun(call.Otto, file, source); err != nil {
		// TODO: throw exception
zelig's avatar
zelig committed
299 300 301
		fmt.Println("err:", err)
		return otto.FalseValue()
	}
302
	// TODO: return evaluation result
zelig's avatar
zelig committed
303 304 305
	return otto.TrueValue()
}

306 307
// Evaluate executes code and pretty prints the result to the specified output
// stream.
308
func (re *JSRE) Evaluate(code string, w io.Writer) error {
309 310
	var fail error

311
	re.Do(func(vm *otto.Otto) {
312
		val, err := vm.Run(code)
313
		if err != nil {
314
			prettyError(vm, err, w)
315 316
		} else {
			prettyPrint(vm, val, w)
317
		}
318
		fmt.Fprintln(w)
319
	})
320
	return fail
zelig's avatar
zelig committed
321
}
322

323
// Compile compiles and then runs a piece of JS code.
324 325
func (re *JSRE) Compile(filename string, src interface{}) (err error) {
	re.Do(func(vm *otto.Otto) { _, err = compileAndRun(vm, filename, src) })
326 327 328 329 330
	return err
}

func compileAndRun(vm *otto.Otto, filename string, src interface{}) (otto.Value, error) {
	script, err := vm.Compile(filename, src)
331
	if err != nil {
332
		return otto.Value{}, err
333
	}
334
	return vm.Run(script)
335
}