123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388 |
- package ngjsvm
- import (
- "context"
- "errors"
- "git.swzry.com/zry/GoHiedaLogger/hiedalog"
- "github.com/dop251/goja"
- "github.com/spf13/afero"
- "io"
- "strings"
- )
- var (
- Err_JSVMErrorOpenScriptFile = errors.New("failed open script file")
- Err_JSVMErrorReadScriptFile = errors.New("failed read script file")
- Err_JSVMErrorCompileScriptFile = errors.New("failed compile script file")
- Err_JSVMErrorExceScriptFile = errors.New("failed exec script file")
- Err_JSVMErrorExceScriptResultNotObject = errors.New("script exec result not an object")
- Err_JSVMErrorNoSuchProperty = errors.New("failed get such property of object in script file")
- Err_JSVMErrorNotFunction = errors.New("property is not a function in script file")
- Err_JSVMCallOnNilObject = errors.New("js call on nil object")
- Err_JSVMAbortByInterruit = errors.New("js engine abort by interrupt")
- Err_JSVMScriptThrowException = errors.New("script throw an exception")
- Err_JSVMRuntimeEnvIsNil = errors.New("runtime env is nil")
- Err_JSVMErrorVMDisposed = errors.New("vm is disposed")
- )
- const (
- NoExportsTip = "loaded main script has no export object, " +
- "please use 'module.exports = xxx' to export it."
- )
- type RuntimeRegisterFunc func(vm *JSVM) *JSEnv
- type JSVMConfig struct {
- Filesystem afero.Fs
- Logger *hiedalog.HiedaLogger
- EngineLogPrefix string
- RuntimeEnvLogPrefix string
- UseStrictMode bool
- }
- type JSVM struct {
- filesystem afero.Fs
- logger *hiedalog.HiedaLogger
- engineLogPrefix string
- runtimeEnvLogPrefix string
- useStrictMode bool
- vm *goja.Runtime
- disposed bool
- mainObj *goja.Object
- setupFunc goja.Callable
- loopFunc goja.Callable
- exitHandlerFunc goja.Callable
- env *JSEnv
- mainProgram *goja.Program
- loopCtx context.Context
- loopCncl context.CancelFunc
- }
- func NewJSVM(cfg *JSVMConfig) *JSVM {
- vm := &JSVM{
- vm: goja.New(),
- filesystem: cfg.Filesystem,
- logger: cfg.Logger,
- engineLogPrefix: cfg.EngineLogPrefix,
- runtimeEnvLogPrefix: cfg.RuntimeEnvLogPrefix,
- useStrictMode: cfg.UseStrictMode,
- disposed: false,
- }
- vm.logger.LogPrint(vm.engineLogPrefix, hiedalog.DLN_INFO, "NagaeJSVM engine ready.")
- return vm
- }
- func (v *JSVM) SetRuntimeEnv(env *JSEnv) {
- v.env = env
- }
- func (v *JSVM) fixJsExtName(uri string) string {
- if strings.HasSuffix(uri, ".js") {
- return uri
- } else {
- return uri + ".js"
- }
- }
- func (v *JSVM) RegistryLoaderFunc(uri string) ([]byte, error) {
- fjs, err := v.filesystem.Open(v.fixJsExtName(uri))
- if err != nil {
- v.logger.LogPrint(v.engineLogPrefix, hiedalog.DLN_ERROR, "failed open js file: ", err)
- if fjs != nil {
- _ = fjs.Close()
- }
- return []byte{}, Err_JSVMErrorOpenScriptFile
- }
- djs, err := io.ReadAll(fjs)
- if fjs != nil {
- _ = fjs.Close()
- }
- if err != nil {
- v.logger.LogPrint(v.runtimeEnvLogPrefix, hiedalog.DLN_ERROR, "failed read js file: ", err)
- return []byte{}, Err_JSVMErrorReadScriptFile
- }
- return djs, nil
- }
- func (v *JSVM) LoadScript(uri string) error {
- if v.disposed {
- v.logger.LogPrint(v.engineLogPrefix, hiedalog.DLN_WARN, "can not do this because the vm is disposed")
- return Err_JSVMErrorVMDisposed
- }
- if v.env == nil {
- v.logger.LogPrint(v.engineLogPrefix, hiedalog.DLN_ERROR, "runtime env is nil")
- return Err_JSVMRuntimeEnvIsNil
- }
- v.env.EnableRegistry(v.vm)
- djs, err := v.RegistryLoaderFunc(uri)
- if err != nil {
- return err
- }
- prog, err := goja.Compile("main", string(djs), v.useStrictMode)
- if err != nil {
- v.logger.LogPrint(v.engineLogPrefix, hiedalog.DLN_ERROR, "failed compile js file: ", err)
- return Err_JSVMErrorCompileScriptFile
- }
- v.mainProgram = prog
- v.vm.ClearInterrupt()
- retval, err := v.vm.RunProgram(v.mainProgram)
- if err != nil {
- _, ok := err.(*goja.InterruptedError)
- if ok {
- v.logger.LogPrint(v.engineLogPrefix, hiedalog.DLN_ERROR, "engine shutdown by 'quit' call in main script.")
- return Err_JSVMAbortByInterruit
- }
- if jserr, jeok := err.(*goja.Exception); jeok {
- jemsg := jserr.Value().ToString()
- v.logger.LogPrint(v.engineLogPrefix, hiedalog.DLN_ERROR, "script throw an exception: ", jemsg)
- return Err_JSVMScriptThrowException
- }
- v.logger.LogPrint(v.engineLogPrefix, hiedalog.DLN_ERROR, "failed exec js file: ", err)
- return Err_JSVMErrorExceScriptFile
- }
- retstr := "null"
- if retval != nil {
- retstr = retval.String()
- }
- v.logger.LogPrint(v.engineLogPrefix, hiedalog.DLN_VERBOSE, "load main script with return value: ", retstr)
- val := v.vm.GlobalObject().Get("exports")
- if val == nil {
- v.logger.LogPrint(v.engineLogPrefix, hiedalog.DLN_ERROR, NoExportsTip)
- return Err_JSVMErrorExceScriptResultNotObject
- }
- if val.SameAs(goja.Undefined()) || val.SameAs(goja.Null()) {
- v.logger.LogPrint(v.engineLogPrefix, hiedalog.DLN_ERROR, NoExportsTip)
- return Err_JSVMErrorExceScriptResultNotObject
- }
- valobj := val.ToObject(v.vm)
- if valobj == nil {
- v.logger.LogPrint(v.engineLogPrefix, hiedalog.DLN_ERROR, NoExportsTip)
- return Err_JSVMErrorExceScriptResultNotObject
- }
- v.mainObj = valobj
- setupFuncObj := v.mainObj.Get("setup")
- setupFunc, err := v.checkFunc(setupFuncObj, "setup")
- if err != nil {
- return err
- }
- v.setupFunc = setupFunc
- loopFuncObj := v.mainObj.Get("loop")
- loopFunc, err := v.checkFunc(loopFuncObj, "loop")
- exitHdlObj := v.mainObj.Get("cleanup")
- useDefaultCleanUp := false
- if exitHdlObj == nil {
- useDefaultCleanUp = true
- } else {
- if exitHdlObj.SameAs(goja.Undefined()) {
- useDefaultCleanUp = true
- }
- }
- if useDefaultCleanUp {
- v.exitHandlerFunc = func(this goja.Value, args ...goja.Value) (goja.Value, error) {
- return nil, nil
- }
- v.logger.LogPrint(v.engineLogPrefix, hiedalog.DLN_VERBOSE, "no method 'cleanup' defined, use an empty one as default.")
- } else {
- exitHdl, xerr := v.checkFunc(exitHdlObj, "cleanup")
- if xerr != nil {
- return xerr
- }
- v.exitHandlerFunc = exitHdl
- }
- if err != nil {
- return err
- }
- v.loopFunc = loopFunc
- return nil
- }
- func (v *JSVM) checkFunc(jsval goja.Value, name string) (goja.Callable, error) {
- if v.disposed {
- v.logger.LogPrint(v.engineLogPrefix, hiedalog.DLN_WARN, "can not do this because the vm is disposed")
- return nil, Err_JSVMErrorVMDisposed
- }
- if jsval == nil {
- v.logger.LogPrintf(v.engineLogPrefix, hiedalog.DLN_ERROR, "failed get method '%s' in main script exports: undefined", name)
- return nil, Err_JSVMErrorNoSuchProperty
- }
- if jsval.SameAs(goja.Undefined()) {
- v.logger.LogPrintf(v.engineLogPrefix, hiedalog.DLN_ERROR, "failed get method '%s' in main script exports: undefined", name)
- return nil, Err_JSVMErrorNoSuchProperty
- }
- jfunc, ok := goja.AssertFunction(jsval)
- if !ok {
- v.logger.LogPrintf(v.engineLogPrefix, hiedalog.DLN_ERROR, "property '%s' is not a function", name)
- return nil, Err_JSVMErrorNotFunction
- }
- return jfunc, nil
- }
- func (v *JSVM) ExecSetup() error {
- if v.disposed {
- v.logger.LogPrint(v.engineLogPrefix, hiedalog.DLN_WARN, "can not do this because the vm is disposed")
- return Err_JSVMErrorVMDisposed
- }
- if v.setupFunc == nil {
- v.logger.LogPrint(v.engineLogPrefix, hiedalog.DLN_ERROR, "call on nil object 'setup'")
- return Err_JSVMCallOnNilObject
- }
- v.vm.ClearInterrupt()
- val, err := v.setupFunc(v.mainObj)
- if err != nil {
- _, ok := err.(*goja.InterruptedError)
- if ok {
- v.logger.LogPrint(v.engineLogPrefix, hiedalog.DLN_WARN, "JSVM interrupted when exec 'setup' script call.")
- return Err_JSVMAbortByInterruit
- }
- v.logger.LogPrint(v.engineLogPrefix, hiedalog.DLN_WARN, "call 'setup' function returns error: ", err)
- } else {
- res := "null"
- if val != nil {
- res = val.String()
- }
- v.logger.LogPrint(v.engineLogPrefix, hiedalog.DLN_DEBUG, "call 'setup' function ok. result: ", res)
- }
- return nil
- }
- func (v *JSVM) ExecCleanup() error {
- if v.disposed {
- v.logger.LogPrint(v.engineLogPrefix, hiedalog.DLN_WARN, "can not do this because the vm is disposed")
- return Err_JSVMErrorVMDisposed
- }
- if v.setupFunc == nil {
- v.logger.LogPrint(v.engineLogPrefix, hiedalog.DLN_ERROR, "call on nil object 'cleanup'")
- return Err_JSVMCallOnNilObject
- }
- v.vm.ClearInterrupt()
- val, err := v.exitHandlerFunc(v.mainObj)
- if err != nil {
- _, ok := err.(*goja.InterruptedError)
- if ok {
- v.logger.LogPrint(v.engineLogPrefix, hiedalog.DLN_ERROR, "JSVM interrupted when exec 'cleanup' script call.")
- return nil
- }
- v.logger.LogPrint(v.engineLogPrefix, hiedalog.DLN_WARN, "call 'cleanup' function returns error: ", err)
- } else {
- res := "null"
- if val != nil {
- res = val.String()
- }
- v.logger.LogPrint(v.engineLogPrefix, hiedalog.DLN_DEBUG, "call 'cleanup' function ok. result: ", res)
- }
- return nil
- }
- func (v *JSVM) execLoop() error {
- if v.disposed {
- v.logger.LogPrint(v.engineLogPrefix, hiedalog.DLN_WARN, "can not do this because the vm is disposed")
- return Err_JSVMErrorVMDisposed
- }
- if v.loopFunc == nil {
- v.logger.LogPrint(v.engineLogPrefix, hiedalog.DLN_ERROR, "call on nil object 'loop'")
- return Err_JSVMCallOnNilObject
- }
- val, err := v.loopFunc(v.mainObj)
- if err != nil {
- _, ok := err.(*goja.InterruptedError)
- if ok {
- v.logger.LogPrint(v.engineLogPrefix, hiedalog.DLN_INFO, "JSVM interrupted when exec 'loop' script call.")
- return Err_JSVMAbortByInterruit
- }
- v.logger.LogPrint(v.engineLogPrefix, hiedalog.DLN_WARN, "call 'loop' function returns error: ", err)
- } else {
- res := "null"
- if val != nil {
- res = val.String()
- }
- v.logger.LogPrint(v.engineLogPrefix, hiedalog.DLN_DEBUG, "call 'loop' function ok. result: ", res)
- }
- return nil
- }
- func (v *JSVM) RunLoop() error {
- if v.disposed {
- v.logger.LogPrint(v.engineLogPrefix, hiedalog.DLN_WARN, "can not do this because the vm is disposed")
- return Err_JSVMErrorVMDisposed
- }
- ctx, cncl := context.WithCancel(context.Background())
- v.loopCncl = cncl
- v.loopCtx = ctx
- v.vm.ClearInterrupt()
- RunLoopLoop:
- for {
- wch := make(chan error)
- go func() {
- rerr := v.execLoop()
- wch <- rerr
- }()
- select {
- case <-ctx.Done():
- {
- v.logger.LogPrint(v.engineLogPrefix, hiedalog.DLN_DEBUG, "'loop' function execution aborted by VM StopLoop.")
- v.vm.Interrupt(nil)
- break RunLoopLoop
- }
- case xrerr := <-wch:
- {
- if xrerr != nil {
- if xrerr == Err_JSVMAbortByInterruit {
- break RunLoopLoop
- } else {
- v.logger.LogPrint(v.engineLogPrefix, hiedalog.DLN_VERBOSE, "error in call 'loop' function: ", xrerr)
- }
- }
- break
- }
- }
- }
- v.logger.LogPrint(v.engineLogPrefix, hiedalog.DLN_INFO, "main loop end.")
- return nil
- }
- func (v *JSVM) StopLoop(err error) {
- if v.loopCncl != nil {
- v.loopCncl()
- }
- }
- func (v *JSVM) RegisterObject(name string, val interface{}) error {
- if v.disposed {
- v.logger.LogPrint(v.engineLogPrefix, hiedalog.DLN_WARN, "can not do this because the vm is disposed")
- return Err_JSVMErrorVMDisposed
- }
- return v.vm.Set(name, val)
- }
- func (v *JSVM) JSCallQuit() {
- if v.disposed {
- return
- }
- v.logger.LogPrint(v.engineLogPrefix, hiedalog.DLN_INFO, "script call 'quit'. send interrupt to JSVM.")
- v.vm.Interrupt(nil)
- }
- func (v *JSVM) VMToValue(val interface{}) goja.Value {
- return v.vm.ToValue(val)
- }
- func (v *JSVM) EmptyObject() *goja.Object {
- return v.vm.NewObject()
- }
- func (v *JSVM) GetGlobalObject() *goja.Object {
- return v.vm.GlobalObject()
- }
- func (v *JSVM) EngineDispose() {
- if v.disposed {
- return
- }
- if v.env != nil {
- v.env.EnvDispose()
- }
- v.logger.LogPrint(v.engineLogPrefix, hiedalog.DLN_DEBUG, "engine deactivated.")
- v.disposed = true
- }
|