jsre.go 9.4 KB

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