src.dualinventive.com/go/devsim/simulator/js/js.go

203 lines
4.0 KiB
Go

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
}