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 }