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