github.com/core-coin/go-core/v2@v2.1.9/internal/jsre/jsre.go (about)

     1  // Copyright 2015 by the Authors
     2  // This file is part of the go-core library.
     3  //
     4  // The go-core 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-core 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-core 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  
    31  	"github.com/core-coin/go-core/v2/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  func (re *JSRE) Do(fn func(*goja.Runtime)) {
   225  	done := make(chan bool)
   226  	req := &evalReq{fn, done}
   227  	re.evalQueue <- req
   228  	<-done
   229  }
   230  
   231  // stops the event loop before exit, optionally waits for all timers to expire
   232  func (re *JSRE) Stop(waitForCallbacks bool) {
   233  	select {
   234  	case <-re.closed:
   235  	case re.stopEventLoop <- waitForCallbacks:
   236  		<-re.closed
   237  	}
   238  }
   239  
   240  // Exec(file) loads and runs the contents of a file
   241  // if a relative path is given, the jsre's assetPath is used
   242  func (re *JSRE) Exec(file string) error {
   243  	code, err := ioutil.ReadFile(common.AbsolutePath(re.assetPath, file))
   244  	if err != nil {
   245  		return err
   246  	}
   247  	return re.Compile(file, string(code))
   248  }
   249  
   250  // Run runs a piece of JS code.
   251  func (re *JSRE) Run(code string) (v goja.Value, err error) {
   252  	re.Do(func(vm *goja.Runtime) { v, err = vm.RunString(code) })
   253  	return v, err
   254  }
   255  
   256  // Set assigns value v to a variable in the JS environment.
   257  func (re *JSRE) Set(ns string, v interface{}) (err error) {
   258  	re.Do(func(vm *goja.Runtime) { vm.Set(ns, v) })
   259  	return err
   260  }
   261  
   262  // MakeCallback turns the given function into a function that's callable by JS.
   263  func MakeCallback(vm *goja.Runtime, fn func(Call) (goja.Value, error)) goja.Value {
   264  	return vm.ToValue(func(call goja.FunctionCall) goja.Value {
   265  		result, err := fn(Call{call, vm})
   266  		if err != nil {
   267  			panic(vm.NewGoError(err))
   268  		}
   269  		return result
   270  	})
   271  }
   272  
   273  // Evaluate executes code and pretty prints the result to the specified output stream.
   274  func (re *JSRE) Evaluate(code string, w io.Writer) {
   275  	re.Do(func(vm *goja.Runtime) {
   276  		val, err := vm.RunString(code)
   277  		if err != nil {
   278  			prettyError(vm, err, w)
   279  		} else {
   280  			prettyPrint(vm, val, w)
   281  		}
   282  		fmt.Fprintln(w)
   283  	})
   284  }
   285  
   286  // Compile compiles and then runs a piece of JS code.
   287  func (re *JSRE) Compile(filename string, src string) (err error) {
   288  	re.Do(func(vm *goja.Runtime) { _, err = compileAndRun(vm, filename, src) })
   289  	return err
   290  }
   291  
   292  // loadScript loads and executes a JS file.
   293  func (re *JSRE) loadScript(call Call) (goja.Value, error) {
   294  	file := call.Argument(0).ToString().String()
   295  	file = common.AbsolutePath(re.assetPath, file)
   296  	source, err := ioutil.ReadFile(file)
   297  	if err != nil {
   298  		return nil, fmt.Errorf("Could not read file %s: %v", file, err)
   299  	}
   300  	value, err := compileAndRun(re.vm, file, string(source))
   301  	if err != nil {
   302  		return nil, fmt.Errorf("Error while compiling or running script: %v", err)
   303  	}
   304  	return value, nil
   305  }
   306  
   307  func compileAndRun(vm *goja.Runtime, filename string, src string) (goja.Value, error) {
   308  	script, err := goja.Compile(filename, src, false)
   309  	if err != nil {
   310  		return goja.Null(), err
   311  	}
   312  	return vm.RunProgram(script)
   313  }