github.com/ethereum/go-ethereum@v1.16.1/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  // New creates and initializes a new JavaScript runtime environment (JSRE).
    71  // The runtime is configured with the provided assetPath for loading scripts and
    72  // an output writer for logging or printing results.
    73  //
    74  // The returned JSRE must be stopped by calling Stop() after use to release resources.
    75  // Attempting to use the JSRE after stopping it will result in undefined behavior.
    76  //
    77  // Parameters:
    78  //   - assetPath: The path to the directory containing script assets.
    79  //   - output: The writer used for logging or printing runtime output.
    80  //
    81  // Returns:
    82  //   - A pointer to the newly created JSRE instance.
    83  func New(assetPath string, output io.Writer) *JSRE {
    84  	re := &JSRE{
    85  		assetPath:     assetPath,
    86  		output:        output,
    87  		closed:        make(chan struct{}),
    88  		evalQueue:     make(chan *evalReq),
    89  		stopEventLoop: make(chan bool),
    90  		vm:            goja.New(),
    91  	}
    92  	go re.runEventLoop()
    93  	re.Set("loadScript", MakeCallback(re.vm, re.loadScript))
    94  	re.Set("inspect", re.prettyPrintJS)
    95  	return re
    96  }
    97  
    98  // randomSource returns a pseudo random value generator.
    99  func randomSource() *rand.Rand {
   100  	bytes := make([]byte, 8)
   101  	seed := time.Now().UnixNano()
   102  	if _, err := crand.Read(bytes); err == nil {
   103  		seed = int64(binary.LittleEndian.Uint64(bytes))
   104  	}
   105  
   106  	src := rand.NewSource(seed)
   107  	return rand.New(src)
   108  }
   109  
   110  // This function runs the main event loop from a goroutine that is started
   111  // when JSRE is created. Use Stop() before exiting to properly stop it.
   112  // The event loop processes vm access requests from the evalQueue in a
   113  // serialized way and calls timer callback functions at the appropriate time.
   114  
   115  // Exported functions always access the vm through the event queue. You can
   116  // call the functions of the goja vm directly to circumvent the queue. These
   117  // functions should be used if and only if running a routine that was already
   118  // called from JS through an RPC call.
   119  func (re *JSRE) runEventLoop() {
   120  	defer close(re.closed)
   121  
   122  	r := randomSource()
   123  	re.vm.SetRandSource(r.Float64)
   124  
   125  	registry := map[*jsTimer]*jsTimer{}
   126  	ready := make(chan *jsTimer)
   127  
   128  	newTimer := func(call goja.FunctionCall, interval bool) (*jsTimer, goja.Value) {
   129  		delay := call.Argument(1).ToInteger()
   130  		if 0 >= delay {
   131  			delay = 1
   132  		}
   133  		timer := &jsTimer{
   134  			duration: time.Duration(delay) * time.Millisecond,
   135  			call:     call,
   136  			interval: interval,
   137  		}
   138  		registry[timer] = timer
   139  
   140  		timer.timer = time.AfterFunc(timer.duration, func() {
   141  			ready <- timer
   142  		})
   143  
   144  		return timer, re.vm.ToValue(timer)
   145  	}
   146  
   147  	setTimeout := func(call goja.FunctionCall) goja.Value {
   148  		_, value := newTimer(call, false)
   149  		return value
   150  	}
   151  
   152  	setInterval := func(call goja.FunctionCall) goja.Value {
   153  		_, value := newTimer(call, true)
   154  		return value
   155  	}
   156  
   157  	clearTimeout := func(call goja.FunctionCall) goja.Value {
   158  		timer := call.Argument(0).Export()
   159  		if timer, ok := timer.(*jsTimer); ok {
   160  			timer.timer.Stop()
   161  			delete(registry, timer)
   162  		}
   163  		return goja.Undefined()
   164  	}
   165  	re.vm.Set("_setTimeout", setTimeout)
   166  	re.vm.Set("_setInterval", setInterval)
   167  	re.vm.RunString(`var setTimeout = function(args) {
   168  		if (arguments.length < 1) {
   169  			throw TypeError("Failed to execute 'setTimeout': 1 argument required, but only 0 present.");
   170  		}
   171  		return _setTimeout.apply(this, arguments);
   172  	}`)
   173  	re.vm.RunString(`var setInterval = function(args) {
   174  		if (arguments.length < 1) {
   175  			throw TypeError("Failed to execute 'setInterval': 1 argument required, but only 0 present.");
   176  		}
   177  		return _setInterval.apply(this, arguments);
   178  	}`)
   179  	re.vm.Set("clearTimeout", clearTimeout)
   180  	re.vm.Set("clearInterval", clearTimeout)
   181  
   182  	var waitForCallbacks bool
   183  
   184  loop:
   185  	for {
   186  		select {
   187  		case timer := <-ready:
   188  			// execute callback, remove/reschedule the timer
   189  			var arguments []interface{}
   190  			if len(timer.call.Arguments) > 2 {
   191  				tmp := timer.call.Arguments[2:]
   192  				arguments = make([]interface{}, 2+len(tmp))
   193  				for i, value := range tmp {
   194  					arguments[i+2] = value
   195  				}
   196  			} else {
   197  				arguments = make([]interface{}, 1)
   198  			}
   199  			arguments[0] = timer.call.Arguments[0]
   200  			call, isFunc := goja.AssertFunction(timer.call.Arguments[0])
   201  			if !isFunc {
   202  				panic(re.vm.ToValue("js error: timer/timeout callback is not a function"))
   203  			}
   204  			call(goja.Null(), timer.call.Arguments...)
   205  
   206  			_, inreg := registry[timer] // when clearInterval is called from within the callback don't reset it
   207  			if timer.interval && inreg {
   208  				timer.timer.Reset(timer.duration)
   209  			} else {
   210  				delete(registry, timer)
   211  				if waitForCallbacks && (len(registry) == 0) {
   212  					break loop
   213  				}
   214  			}
   215  		case req := <-re.evalQueue:
   216  			// run the code, send the result back
   217  			req.fn(re.vm)
   218  			close(req.done)
   219  			if waitForCallbacks && (len(registry) == 0) {
   220  				break loop
   221  			}
   222  		case waitForCallbacks = <-re.stopEventLoop:
   223  			if !waitForCallbacks || (len(registry) == 0) {
   224  				break loop
   225  			}
   226  		}
   227  	}
   228  
   229  	for _, timer := range registry {
   230  		timer.timer.Stop()
   231  		delete(registry, timer)
   232  	}
   233  }
   234  
   235  // Do executes the given function on the JS event loop.
   236  // When the runtime is stopped, fn will not execute.
   237  func (re *JSRE) Do(fn func(*goja.Runtime)) {
   238  	done := make(chan bool)
   239  	req := &evalReq{fn, done}
   240  	select {
   241  	case re.evalQueue <- req:
   242  		<-done
   243  	case <-re.closed:
   244  	}
   245  }
   246  
   247  // Stop terminates the event loop, optionally waiting for all timers to expire.
   248  func (re *JSRE) Stop(waitForCallbacks bool) {
   249  	timeout := time.NewTimer(10 * time.Millisecond)
   250  	defer timeout.Stop()
   251  
   252  	for {
   253  		select {
   254  		case <-re.closed:
   255  			return
   256  		case re.stopEventLoop <- waitForCallbacks:
   257  			<-re.closed
   258  			return
   259  		case <-timeout.C:
   260  			// JS is blocked, interrupt and try again.
   261  			re.vm.Interrupt(errors.New("JS runtime stopped"))
   262  		}
   263  	}
   264  }
   265  
   266  // Exec loads and executes the contents of a JavaScript file.
   267  // If a relative path is provided, the file is resolved relative to the JSRE's assetPath.
   268  // The file is read, compiled, and executed in the JSRE's runtime environment.
   269  //
   270  // Parameters:
   271  //   - file: The path to the JavaScript file to execute. Can be an absolute path or relative to assetPath.
   272  //
   273  // Returns:
   274  //   - error: An error if the file cannot be read, compiled, or executed.
   275  func (re *JSRE) Exec(file string) error {
   276  	code, err := os.ReadFile(common.AbsolutePath(re.assetPath, file))
   277  	if err != nil {
   278  		return err
   279  	}
   280  	return re.Compile(file, string(code))
   281  }
   282  
   283  // Run runs a piece of JS code.
   284  func (re *JSRE) Run(code string) (v goja.Value, err error) {
   285  	re.Do(func(vm *goja.Runtime) { v, err = vm.RunString(code) })
   286  	return v, err
   287  }
   288  
   289  // Set assigns value v to a variable in the JS environment.
   290  func (re *JSRE) Set(ns string, v interface{}) (err error) {
   291  	re.Do(func(vm *goja.Runtime) { vm.Set(ns, v) })
   292  	return err
   293  }
   294  
   295  // MakeCallback turns the given function into a function that's callable by JS.
   296  func MakeCallback(vm *goja.Runtime, fn func(Call) (goja.Value, error)) goja.Value {
   297  	return vm.ToValue(func(call goja.FunctionCall) goja.Value {
   298  		result, err := fn(Call{call, vm})
   299  		if err != nil {
   300  			panic(vm.NewGoError(err))
   301  		}
   302  		return result
   303  	})
   304  }
   305  
   306  // Evaluate executes code and pretty prints the result to the specified output stream.
   307  func (re *JSRE) Evaluate(code string, w io.Writer) {
   308  	re.Do(func(vm *goja.Runtime) {
   309  		val, err := vm.RunString(code)
   310  		if err != nil {
   311  			prettyError(vm, err, w)
   312  		} else {
   313  			prettyPrint(vm, val, w)
   314  		}
   315  		fmt.Fprintln(w)
   316  	})
   317  }
   318  
   319  // Interrupt stops the current JS evaluation.
   320  func (re *JSRE) Interrupt(v interface{}) {
   321  	done := make(chan bool)
   322  	noop := func(*goja.Runtime) {}
   323  
   324  	select {
   325  	case re.evalQueue <- &evalReq{noop, done}:
   326  		// event loop is not blocked.
   327  	default:
   328  		re.vm.Interrupt(v)
   329  	}
   330  }
   331  
   332  // Compile compiles and then runs a piece of JS code.
   333  func (re *JSRE) Compile(filename string, src string) (err error) {
   334  	re.Do(func(vm *goja.Runtime) { _, err = compileAndRun(vm, filename, src) })
   335  	return err
   336  }
   337  
   338  // loadScript loads and executes a JS file.
   339  func (re *JSRE) loadScript(call Call) (goja.Value, error) {
   340  	file := call.Argument(0).ToString().String()
   341  	file = common.AbsolutePath(re.assetPath, file)
   342  	source, err := os.ReadFile(file)
   343  	if err != nil {
   344  		return nil, fmt.Errorf("could not read file %s: %v", file, err)
   345  	}
   346  	value, err := compileAndRun(re.vm, file, string(source))
   347  	if err != nil {
   348  		return nil, fmt.Errorf("error while compiling or running script: %v", err)
   349  	}
   350  	return value, nil
   351  }
   352  
   353  func compileAndRun(vm *goja.Runtime, filename string, src string) (goja.Value, error) {
   354  	script, err := goja.Compile(filename, src, false)
   355  	if err != nil {
   356  		return goja.Null(), err
   357  	}
   358  	return vm.RunProgram(script)
   359  }