github.com/klaytn/klaytn@v1.12.1/console/jsre/jsre.go (about)

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