github.com/ethereum/go-ethereum@v1.14.3/internal/jsre/jsre.go (about)

     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  // it under the terms of the GNU Lesser General Public License as published by
     6  // the Free Software Foundation, either version 3 of the License, or
     7  // (at your option) any later version.
     8  //
     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  // GNU Lesser General Public License for more details.
    13  //
    14  // 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.
    18  package jsre
    19  
    20  import (
    21  	crand "crypto/rand"
    22  	"encoding/binary"
    23  	"errors"
    24  	"fmt"
    25  	"io"
    26  	"math/rand"
    27  	"os"
    28  	"time"
    29  
    30  	"github.com/dop251/goja"
    31  	"github.com/ethereum/go-ethereum/common"
    32  )
    33  
    34  // JSRE is a JS runtime environment embedding the goja interpreter.
    35  // It provides helper functions to load code from files, run code snippets
    36  // and bind native go objects to JS.
    37  //
    38  // The runtime runs all code on a dedicated event loop and does not expose the underlying
    39  // goja runtime directly. To use the runtime, call JSRE.Do. When binding a Go function,
    40  // use the Call type to gain access to the runtime.
    41  type JSRE struct {
    42  	assetPath     string
    43  	output        io.Writer
    44  	evalQueue     chan *evalReq
    45  	stopEventLoop chan bool
    46  	closed        chan struct{}
    47  	vm            *goja.Runtime
    48  }
    49  
    50  // Call is the argument type of Go functions which are callable from JS.
    51  type Call struct {
    52  	goja.FunctionCall
    53  	VM *goja.Runtime
    54  }
    55  
    56  // jsTimer is a single timer instance with a callback function
    57  type jsTimer struct {
    58  	timer    *time.Timer
    59  	duration time.Duration
    60  	interval bool
    61  	call     goja.FunctionCall
    62  }
    63  
    64  // evalReq is a serialized vm execution request processed by runEventLoop.
    65  type evalReq struct {
    66  	fn   func(vm *goja.Runtime)
    67  	done chan bool
    68  }
    69  
    70  // runtime must be stopped with Stop() after use and cannot be used after stopping
    71  func New(assetPath string, output io.Writer) *JSRE {
    72  	re := &JSRE{
    73  		assetPath:     assetPath,
    74  		output:        output,
    75  		closed:        make(chan struct{}),
    76  		evalQueue:     make(chan *evalReq),
    77  		stopEventLoop: make(chan bool),
    78  		vm:            goja.New(),
    79  	}
    80  	go re.runEventLoop()
    81  	re.Set("loadScript", MakeCallback(re.vm, re.loadScript))
    82  	re.Set("inspect", re.prettyPrintJS)
    83  	return re
    84  }
    85  
    86  // randomSource returns a pseudo random value generator.
    87  func randomSource() *rand.Rand {
    88  	bytes := make([]byte, 8)
    89  	seed := time.Now().UnixNano()
    90  	if _, err := crand.Read(bytes); err == nil {
    91  		seed = int64(binary.LittleEndian.Uint64(bytes))
    92  	}
    93  
    94  	src := rand.NewSource(seed)
    95  	return rand.New(src)
    96  }
    97  
    98  // This function runs the main event loop from a goroutine that is started
    99  // when JSRE is created. Use Stop() before exiting to properly stop it.
   100  // The event loop processes vm access requests from the evalQueue in a
   101  // 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  // functions should be used if and only if running a routine that was already
   106  // called from JS through an RPC call.
   107  func (re *JSRE) runEventLoop() {
   108  	defer close(re.closed)
   109  
   110  	r := randomSource()
   111  	re.vm.SetRandSource(r.Float64)
   112  
   113  	registry := map[*jsTimer]*jsTimer{}
   114  	ready := make(chan *jsTimer)
   115  
   116  	newTimer := func(call goja.FunctionCall, interval bool) (*jsTimer, goja.Value) {
   117  		delay := call.Argument(1).ToInteger()
   118  		if 0 >= delay {
   119  			delay = 1
   120  		}
   121  		timer := &jsTimer{
   122  			duration: time.Duration(delay) * time.Millisecond,
   123  			call:     call,
   124  			interval: interval,
   125  		}
   126  		registry[timer] = timer
   127  
   128  		timer.timer = time.AfterFunc(timer.duration, func() {
   129  			ready <- timer
   130  		})
   131  
   132  		return timer, re.vm.ToValue(timer)
   133  	}
   134  
   135  	setTimeout := func(call goja.FunctionCall) goja.Value {
   136  		_, value := newTimer(call, false)
   137  		return value
   138  	}
   139  
   140  	setInterval := func(call goja.FunctionCall) goja.Value {
   141  		_, value := newTimer(call, true)
   142  		return value
   143  	}
   144  
   145  	clearTimeout := func(call goja.FunctionCall) goja.Value {
   146  		timer := call.Argument(0).Export()
   147  		if timer, ok := timer.(*jsTimer); ok {
   148  			timer.timer.Stop()
   149  			delete(registry, timer)
   150  		}
   151  		return goja.Undefined()
   152  	}
   153  	re.vm.Set("_setTimeout", setTimeout)
   154  	re.vm.Set("_setInterval", setInterval)
   155  	re.vm.RunString(`var setTimeout = function(args) {
   156  		if (arguments.length < 1) {
   157  			throw TypeError("Failed to execute 'setTimeout': 1 argument required, but only 0 present.");
   158  		}
   159  		return _setTimeout.apply(this, arguments);
   160  	}`)
   161  	re.vm.RunString(`var setInterval = function(args) {
   162  		if (arguments.length < 1) {
   163  			throw TypeError("Failed to execute 'setInterval': 1 argument required, but only 0 present.");
   164  		}
   165  		return _setInterval.apply(this, arguments);
   166  	}`)
   167  	re.vm.Set("clearTimeout", clearTimeout)
   168  	re.vm.Set("clearInterval", clearTimeout)
   169  
   170  	var waitForCallbacks bool
   171  
   172  loop:
   173  	for {
   174  		select {
   175  		case timer := <-ready:
   176  			// execute callback, remove/reschedule the timer
   177  			var arguments []interface{}
   178  			if len(timer.call.Arguments) > 2 {
   179  				tmp := timer.call.Arguments[2:]
   180  				arguments = make([]interface{}, 2+len(tmp))
   181  				for i, value := range tmp {
   182  					arguments[i+2] = value
   183  				}
   184  			} else {
   185  				arguments = make([]interface{}, 1)
   186  			}
   187  			arguments[0] = timer.call.Arguments[0]
   188  			call, isFunc := goja.AssertFunction(timer.call.Arguments[0])
   189  			if !isFunc {
   190  				panic(re.vm.ToValue("js error: timer/timeout callback is not a function"))
   191  			}
   192  			call(goja.Null(), timer.call.Arguments...)
   193  
   194  			_, inreg := registry[timer] // when clearInterval is called from within the callback don't reset it
   195  			if timer.interval && inreg {
   196  				timer.timer.Reset(timer.duration)
   197  			} else {
   198  				delete(registry, timer)
   199  				if waitForCallbacks && (len(registry) == 0) {
   200  					break loop
   201  				}
   202  			}
   203  		case req := <-re.evalQueue:
   204  			// run the code, send the result back
   205  			req.fn(re.vm)
   206  			close(req.done)
   207  			if waitForCallbacks && (len(registry) == 0) {
   208  				break loop
   209  			}
   210  		case waitForCallbacks = <-re.stopEventLoop:
   211  			if !waitForCallbacks || (len(registry) == 0) {
   212  				break loop
   213  			}
   214  		}
   215  	}
   216  
   217  	for _, timer := range registry {
   218  		timer.timer.Stop()
   219  		delete(registry, timer)
   220  	}
   221  }
   222  
   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  	done := make(chan bool)
   227  	req := &evalReq{fn, done}
   228  	select {
   229  	case re.evalQueue <- req:
   230  		<-done
   231  	case <-re.closed:
   232  	}
   233  }
   234  
   235  // Stop terminates the event loop, optionally waiting for all timers to expire.
   236  func (re *JSRE) Stop(waitForCallbacks bool) {
   237  	timeout := time.NewTimer(10 * time.Millisecond)
   238  	defer timeout.Stop()
   239  
   240  	for {
   241  		select {
   242  		case <-re.closed:
   243  			return
   244  		case re.stopEventLoop <- waitForCallbacks:
   245  			<-re.closed
   246  			return
   247  		case <-timeout.C:
   248  			// JS is blocked, interrupt and try again.
   249  			re.vm.Interrupt(errors.New("JS runtime stopped"))
   250  		}
   251  	}
   252  }
   253  
   254  // Exec(file) loads and runs the contents of a file
   255  // 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))
   258  	if err != nil {
   259  		return err
   260  	}
   261  	return re.Compile(file, string(code))
   262  }
   263  
   264  // Run runs a piece of JS code.
   265  func (re *JSRE) Run(code string) (v goja.Value, err error) {
   266  	re.Do(func(vm *goja.Runtime) { v, err = vm.RunString(code) })
   267  	return v, err
   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
   274  }
   275  
   276  // MakeCallback turns the given function into a function that's callable by JS.
   277  func MakeCallback(vm *goja.Runtime, fn func(Call) (goja.Value, error)) goja.Value {
   278  	return vm.ToValue(func(call goja.FunctionCall) goja.Value {
   279  		result, err := fn(Call{call, vm})
   280  		if err != nil {
   281  			panic(vm.NewGoError(err))
   282  		}
   283  		return result
   284  	})
   285  }
   286  
   287  // Evaluate executes code and pretty prints the result to the specified output stream.
   288  func (re *JSRE) Evaluate(code string, w io.Writer) {
   289  	re.Do(func(vm *goja.Runtime) {
   290  		val, err := vm.RunString(code)
   291  		if err != nil {
   292  			prettyError(vm, err, w)
   293  		} else {
   294  			prettyPrint(vm, val, w)
   295  		}
   296  		fmt.Fprintln(w)
   297  	})
   298  }
   299  
   300  // Interrupt stops the current JS evaluation.
   301  func (re *JSRE) Interrupt(v interface{}) {
   302  	done := make(chan bool)
   303  	noop := func(*goja.Runtime) {}
   304  
   305  	select {
   306  	case re.evalQueue <- &evalReq{noop, done}:
   307  		// event loop is not blocked.
   308  	default:
   309  		re.vm.Interrupt(v)
   310  	}
   311  }
   312  
   313  // Compile compiles and then runs a piece of JS code.
   314  func (re *JSRE) Compile(filename string, src string) (err error) {
   315  	re.Do(func(vm *goja.Runtime) { _, err = compileAndRun(vm, filename, src) })
   316  	return err
   317  }
   318  
   319  // loadScript loads and executes a JS file.
   320  func (re *JSRE) loadScript(call Call) (goja.Value, error) {
   321  	file := call.Argument(0).ToString().String()
   322  	file = common.AbsolutePath(re.assetPath, file)
   323  	source, err := os.ReadFile(file)
   324  	if err != nil {
   325  		return nil, fmt.Errorf("could not read file %s: %v", file, err)
   326  	}
   327  	value, err := compileAndRun(re.vm, file, string(source))
   328  	if err != nil {
   329  		return nil, fmt.Errorf("error while compiling or running script: %v", err)
   330  	}
   331  	return value, nil
   332  }
   333  
   334  func compileAndRun(vm *goja.Runtime, filename string, src string) (goja.Value, error) {
   335  	script, err := goja.Compile(filename, src, false)
   336  	if err != nil {
   337  		return goja.Null(), err
   338  	}
   339  	return vm.RunProgram(script)
   340  }