github.com/aidoskuneen/adk-node@v0.0.0-20220315131952-2e32567cb7f4/internal/jsre/jsre.go (about)

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