jsre.go 9.44 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"
23
	"errors"
zelig's avatar
zelig committed
24
	"fmt"
25
	"io"
26
	"math/rand"
27
	"os"
28
	"time"
29

30
	"github.com/dop251/goja"
zelig's avatar
zelig committed
31
	"github.com/ethereum/go-ethereum/common"
zelig's avatar
zelig committed
32 33
)

34 35 36 37 38 39 40
// 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.
zelig's avatar
zelig committed
41
type JSRE struct {
42
	assetPath     string
43
	output        io.Writer
44 45
	evalQueue     chan *evalReq
	stopEventLoop chan bool
46
	closed        chan struct{}
47 48 49 50 51 52 53
	vm            *goja.Runtime
}

// Call is the argument type of Go functions which are callable from JS.
type Call struct {
	goja.FunctionCall
	VM *goja.Runtime
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
61
	call     goja.FunctionCall
zelig's avatar
zelig committed
62 63
}

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

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

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

98 99 100 101
// 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.
102

103
// Exported functions always access the vm through the event queue. You can
104
// call the functions of the goja vm directly to circumvent the queue. These
105 106
// functions should be used if and only if running a routine that was already
// called from JS through an RPC call.
107 108
func (re *JSRE) runEventLoop() {
	defer close(re.closed)
109

110
	r := randomSource()
111
	re.vm.SetRandSource(r.Float64)
112

113 114 115
	registry := map[*jsTimer]*jsTimer{}
	ready := make(chan *jsTimer)

116 117
	newTimer := func(call goja.FunctionCall, interval bool) (*jsTimer, goja.Value) {
		delay := call.Argument(1).ToInteger()
118 119 120 121 122 123 124 125 126 127 128 129 130 131
		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
		})

132
		return timer, re.vm.ToValue(timer)
133 134
	}

135
	setTimeout := func(call goja.FunctionCall) goja.Value {
136 137 138 139
		_, value := newTimer(call, false)
		return value
	}

140
	setInterval := func(call goja.FunctionCall) goja.Value {
141 142 143 144
		_, value := newTimer(call, true)
		return value
	}

145 146
	clearTimeout := func(call goja.FunctionCall) goja.Value {
		timer := call.Argument(0).Export()
147 148 149 150
		if timer, ok := timer.(*jsTimer); ok {
			timer.timer.Stop()
			delete(registry, timer)
		}
151
		return goja.Undefined()
152
	}
153 154 155
	re.vm.Set("_setTimeout", setTimeout)
	re.vm.Set("_setInterval", setInterval)
	re.vm.RunString(`var setTimeout = function(args) {
156 157 158 159 160
		if (arguments.length < 1) {
			throw TypeError("Failed to execute 'setTimeout': 1 argument required, but only 0 present.");
		}
		return _setTimeout.apply(this, arguments);
	}`)
161
	re.vm.RunString(`var setInterval = function(args) {
162 163 164 165 166
		if (arguments.length < 1) {
			throw TypeError("Failed to execute 'setInterval': 1 argument required, but only 0 present.");
		}
		return _setInterval.apply(this, arguments);
	}`)
167 168
	re.vm.Set("clearTimeout", clearTimeout)
	re.vm.Set("clearInterval", clearTimeout)
169 170 171 172 173 174 175 176 177

	var waitForCallbacks bool

loop:
	for {
		select {
		case timer := <-ready:
			// execute callback, remove/reschedule the timer
			var arguments []interface{}
178 179
			if len(timer.call.Arguments) > 2 {
				tmp := timer.call.Arguments[2:]
180 181 182 183 184 185 186
				arguments = make([]interface{}, 2+len(tmp))
				for i, value := range tmp {
					arguments[i+2] = value
				}
			} else {
				arguments = make([]interface{}, 1)
			}
187 188 189 190
			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"))
191
			}
192
			call(goja.Null(), timer.call.Arguments...)
193

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

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

Felix Lange's avatar
Felix Lange committed
223
// Do executes the given function on the JS event loop.
224
// When the runtime is stopped, fn will not execute.
225
func (re *JSRE) Do(fn func(*goja.Runtime)) {
226 227
	done := make(chan bool)
	req := &evalReq{fn, done}
228 229 230 231 232
	select {
	case re.evalQueue <- req:
		<-done
	case <-re.closed:
	}
233 234
}

235
// Stop terminates the event loop, optionally waiting for all timers to expire.
236
func (re *JSRE) Stop(waitForCallbacks bool) {
237 238 239 240 241 242 243 244 245 246 247 248 249 250
	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"))
		}
251
	}
252 253
}

zelig's avatar
zelig committed
254 255
// Exec(file) loads and runs the contents of a file
// if a relative path is given, the jsre's assetPath is used
256
func (re *JSRE) Exec(file string) error {
257
	code, err := os.ReadFile(common.AbsolutePath(re.assetPath, file))
zelig's avatar
zelig committed
258 259 260
	if err != nil {
		return err
	}
261
	return re.Compile(file, string(code))
zelig's avatar
zelig committed
262 263
}

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

270
// Set assigns value v to a variable in the JS environment.
271
func (re *JSRE) Set(ns string, v interface{}) (err error) {
272
	re.Do(func(vm *goja.Runtime) { vm.Set(ns, v) })
273
	return err
zelig's avatar
zelig committed
274 275
}

276 277 278 279 280 281 282 283 284
// 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
	})
zelig's avatar
zelig committed
285 286
}

287 288 289 290
// 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)
291
		if err != nil {
292
			prettyError(vm, err, w)
293 294
		} else {
			prettyPrint(vm, val, w)
295
		}
296
		fmt.Fprintln(w)
297
	})
zelig's avatar
zelig committed
298
}
299

300 301 302 303 304 305 306 307 308 309 310 311 312
// 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)
	}
}

313
// Compile compiles and then runs a piece of JS code.
314 315
func (re *JSRE) Compile(filename string, src string) (err error) {
	re.Do(func(vm *goja.Runtime) { _, err = compileAndRun(vm, filename, src) })
316 317 318
	return err
}

319 320 321 322
// 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)
323
	source, err := os.ReadFile(file)
324 325 326 327 328 329 330 331 332 333 334 335
	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)
336
	if err != nil {
337
		return goja.Null(), err
338
	}
339
	return vm.RunProgram(script)
340
}