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