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 }