203 lines
4.0 KiB
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
|
|
}
|