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 }