package js import ( "errors" "io/ioutil" "os" "path/filepath" "strings" "sync" "time" "src.dualinventive.com/go/devsim/simulator" "src.dualinventive.com/go/devsim/simulator/js/console" "src.dualinventive.com/go/dinet" "github.com/dop251/goja" "github.com/dop251/goja_nodejs/require" ) const ( // Entrypoint is the javascript entrypoint file Entrypoint = "devsim.js" ) // ErrEntrypoint when the Entrypoint is non-existing or unable to be loaded var ErrEntrypoint = errors.New("entrypoint could not be loaded") // ErrPathInvalid when the javascript file is an invalid path var ErrPathInvalid = errors.New("path is invalid") // Simulator is a simulator vm with a JavaScript engine type Simulator struct { simulator.Info wg sync.WaitGroup intervalsWg sync.WaitGroup requestHandlers requestHandlers m simulator.Model entrypoint string dispatchC chan func() intervals []*interval vm *goja.Runtime } func isFilePathInBaseDir(baseDir, filePath string) bool { absPath, err := filepath.Abs(filePath) if err != nil { return false } return strings.HasPrefix(absPath, baseDir) } func newSourceLoader(cwd string) require.SourceLoader { return func(path string) ([]byte, error) { jsPath := filepath.Join(cwd, path) + ".js" if !isFilePathInBaseDir(cwd, jsPath) { return nil, ErrPathInvalid } // #nosec we allow running arbitrary javascript files return ioutil.ReadFile(jsPath) } } func vmSleep(call goja.FunctionCall) goja.Value { delayMs := call.Argument(0).ToInteger() time.Sleep(time.Duration(delayMs) * time.Millisecond) return nil } // newSimulator creates a new javascript Simulator func newSimulator(m simulator.Model, info simulator.Info) (*Simulator, error) { entrypoint := filepath.Join(info.Path(), Entrypoint) if _, err := os.Stat(entrypoint); os.IsNotExist(err) { return nil, ErrEntrypoint } s := &Simulator{m: m, vm: goja.New(), entrypoint: entrypoint, Info: info} cwd, err := filepath.Abs(info.Path()) if err != nil { return nil, err } r := require.NewRegistryWithLoader(newSourceLoader(cwd)) r.Enable(s.vm) console.Enable(s.vm, m) s.vm.Set("sleep", vmSleep) o := s.vm.NewObject() err = o.Set("request", s.request) if err != nil { return nil, err } err = o.Set("publish", s.publish) if err != nil { return nil, err } err = o.Set("info", s.info) if err != nil { return nil, err } err = o.Set("set", s.storageSet) if err != nil { return nil, err } err = o.Set("get", s.storageGet) if err != nil { return nil, err } err = o.Set("del", s.storageDel) if err != nil { return nil, err } err = o.Set("setInterval", s.setInterval) if err != nil { return nil, err } s.vm.Set("devsim", o) return s, nil } // Start loads the libdir bootstrap file and executes the simulator bootstrap js file func (s *Simulator) Start() error { if err := s.m.Connect(); err != nil { return err } s.dispatchC = make(chan func()) if err := s.runFile(s.entrypoint); err != nil { return err } // VM call dispatcher s.wg.Add(1) go func() { defer s.wg.Done() for fn := range s.dispatchC { fn() } }() // Recv s.wg.Add(1) go func() { defer s.wg.Done() for { msg, err := s.m.Recv() if err != nil && err == dinet.ErrClosed { return } if msg == nil { continue } s.requestHandleMsg(msg) } }() return nil } // Stop interrupts the vm and waits until it is stopped func (s *Simulator) Stop() error { err := s.m.Close() if err != nil { return err } err = s.m.Disconnect() for _, interval := range s.intervals { interval.stopChan <- 0 } s.intervalsWg.Wait() s.vm.Interrupt("stop") close(s.dispatchC) s.wg.Wait() return err } // runFile reads and runs the content in the Simulator func (s *Simulator) runFile(file string) error { // #nosec we allow running arbitrary javascript files js, err := ioutil.ReadFile(file) if err != nil { return err } _, err = s.vm.RunString(string(js[:])) return err } func (s *Simulator) info(call goja.FunctionCall) goja.Value { return nil }