js_engine.go 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388
  1. package ngjsvm
  2. import (
  3. "context"
  4. "errors"
  5. "git.swzry.com/zry/GoHiedaLogger/hiedalog"
  6. "github.com/dop251/goja"
  7. "github.com/spf13/afero"
  8. "io"
  9. "strings"
  10. )
  11. var (
  12. Err_JSVMErrorOpenScriptFile = errors.New("failed open script file")
  13. Err_JSVMErrorReadScriptFile = errors.New("failed read script file")
  14. Err_JSVMErrorCompileScriptFile = errors.New("failed compile script file")
  15. Err_JSVMErrorExceScriptFile = errors.New("failed exec script file")
  16. Err_JSVMErrorExceScriptResultNotObject = errors.New("script exec result not an object")
  17. Err_JSVMErrorNoSuchProperty = errors.New("failed get such property of object in script file")
  18. Err_JSVMErrorNotFunction = errors.New("property is not a function in script file")
  19. Err_JSVMCallOnNilObject = errors.New("js call on nil object")
  20. Err_JSVMAbortByInterruit = errors.New("js engine abort by interrupt")
  21. Err_JSVMScriptThrowException = errors.New("script throw an exception")
  22. Err_JSVMRuntimeEnvIsNil = errors.New("runtime env is nil")
  23. Err_JSVMErrorVMDisposed = errors.New("vm is disposed")
  24. )
  25. const (
  26. NoExportsTip = "loaded main script has no export object, " +
  27. "please use 'module.exports = xxx' to export it."
  28. )
  29. type RuntimeRegisterFunc func(vm *JSVM) *JSEnv
  30. type JSVMConfig struct {
  31. Filesystem afero.Fs
  32. Logger *hiedalog.HiedaLogger
  33. EngineLogPrefix string
  34. RuntimeEnvLogPrefix string
  35. UseStrictMode bool
  36. }
  37. type JSVM struct {
  38. filesystem afero.Fs
  39. logger *hiedalog.HiedaLogger
  40. engineLogPrefix string
  41. runtimeEnvLogPrefix string
  42. useStrictMode bool
  43. vm *goja.Runtime
  44. disposed bool
  45. mainObj *goja.Object
  46. setupFunc goja.Callable
  47. loopFunc goja.Callable
  48. exitHandlerFunc goja.Callable
  49. env *JSEnv
  50. mainProgram *goja.Program
  51. loopCtx context.Context
  52. loopCncl context.CancelFunc
  53. }
  54. func NewJSVM(cfg *JSVMConfig) *JSVM {
  55. vm := &JSVM{
  56. vm: goja.New(),
  57. filesystem: cfg.Filesystem,
  58. logger: cfg.Logger,
  59. engineLogPrefix: cfg.EngineLogPrefix,
  60. runtimeEnvLogPrefix: cfg.RuntimeEnvLogPrefix,
  61. useStrictMode: cfg.UseStrictMode,
  62. disposed: false,
  63. }
  64. vm.logger.LogPrint(vm.engineLogPrefix, hiedalog.DLN_INFO, "NagaeJSVM engine ready.")
  65. return vm
  66. }
  67. func (v *JSVM) SetRuntimeEnv(env *JSEnv) {
  68. v.env = env
  69. }
  70. func (v *JSVM) fixJsExtName(uri string) string {
  71. if strings.HasSuffix(uri, ".js") {
  72. return uri
  73. } else {
  74. return uri + ".js"
  75. }
  76. }
  77. func (v *JSVM) RegistryLoaderFunc(uri string) ([]byte, error) {
  78. fjs, err := v.filesystem.Open(v.fixJsExtName(uri))
  79. if err != nil {
  80. v.logger.LogPrint(v.engineLogPrefix, hiedalog.DLN_ERROR, "failed open js file: ", err)
  81. if fjs != nil {
  82. _ = fjs.Close()
  83. }
  84. return []byte{}, Err_JSVMErrorOpenScriptFile
  85. }
  86. djs, err := io.ReadAll(fjs)
  87. if fjs != nil {
  88. _ = fjs.Close()
  89. }
  90. if err != nil {
  91. v.logger.LogPrint(v.runtimeEnvLogPrefix, hiedalog.DLN_ERROR, "failed read js file: ", err)
  92. return []byte{}, Err_JSVMErrorReadScriptFile
  93. }
  94. return djs, nil
  95. }
  96. func (v *JSVM) LoadScript(uri string) error {
  97. if v.disposed {
  98. v.logger.LogPrint(v.engineLogPrefix, hiedalog.DLN_WARN, "can not do this because the vm is disposed")
  99. return Err_JSVMErrorVMDisposed
  100. }
  101. if v.env == nil {
  102. v.logger.LogPrint(v.engineLogPrefix, hiedalog.DLN_ERROR, "runtime env is nil")
  103. return Err_JSVMRuntimeEnvIsNil
  104. }
  105. v.env.EnableRegistry(v.vm)
  106. djs, err := v.RegistryLoaderFunc(uri)
  107. if err != nil {
  108. return err
  109. }
  110. prog, err := goja.Compile("main", string(djs), v.useStrictMode)
  111. if err != nil {
  112. v.logger.LogPrint(v.engineLogPrefix, hiedalog.DLN_ERROR, "failed compile js file: ", err)
  113. return Err_JSVMErrorCompileScriptFile
  114. }
  115. v.mainProgram = prog
  116. v.vm.ClearInterrupt()
  117. retval, err := v.vm.RunProgram(v.mainProgram)
  118. if err != nil {
  119. _, ok := err.(*goja.InterruptedError)
  120. if ok {
  121. v.logger.LogPrint(v.engineLogPrefix, hiedalog.DLN_ERROR, "engine shutdown by 'quit' call in main script.")
  122. return Err_JSVMAbortByInterruit
  123. }
  124. if jserr, jeok := err.(*goja.Exception); jeok {
  125. jemsg := jserr.Value().ToString()
  126. v.logger.LogPrint(v.engineLogPrefix, hiedalog.DLN_ERROR, "script throw an exception: ", jemsg)
  127. return Err_JSVMScriptThrowException
  128. }
  129. v.logger.LogPrint(v.engineLogPrefix, hiedalog.DLN_ERROR, "failed exec js file: ", err)
  130. return Err_JSVMErrorExceScriptFile
  131. }
  132. retstr := "null"
  133. if retval != nil {
  134. retstr = retval.String()
  135. }
  136. v.logger.LogPrint(v.engineLogPrefix, hiedalog.DLN_VERBOSE, "load main script with return value: ", retstr)
  137. val := v.vm.GlobalObject().Get("exports")
  138. if val == nil {
  139. v.logger.LogPrint(v.engineLogPrefix, hiedalog.DLN_ERROR, NoExportsTip)
  140. return Err_JSVMErrorExceScriptResultNotObject
  141. }
  142. if val.SameAs(goja.Undefined()) || val.SameAs(goja.Null()) {
  143. v.logger.LogPrint(v.engineLogPrefix, hiedalog.DLN_ERROR, NoExportsTip)
  144. return Err_JSVMErrorExceScriptResultNotObject
  145. }
  146. valobj := val.ToObject(v.vm)
  147. if valobj == nil {
  148. v.logger.LogPrint(v.engineLogPrefix, hiedalog.DLN_ERROR, NoExportsTip)
  149. return Err_JSVMErrorExceScriptResultNotObject
  150. }
  151. v.mainObj = valobj
  152. setupFuncObj := v.mainObj.Get("setup")
  153. setupFunc, err := v.checkFunc(setupFuncObj, "setup")
  154. if err != nil {
  155. return err
  156. }
  157. v.setupFunc = setupFunc
  158. loopFuncObj := v.mainObj.Get("loop")
  159. loopFunc, err := v.checkFunc(loopFuncObj, "loop")
  160. exitHdlObj := v.mainObj.Get("cleanup")
  161. useDefaultCleanUp := false
  162. if exitHdlObj == nil {
  163. useDefaultCleanUp = true
  164. } else {
  165. if exitHdlObj.SameAs(goja.Undefined()) {
  166. useDefaultCleanUp = true
  167. }
  168. }
  169. if useDefaultCleanUp {
  170. v.exitHandlerFunc = func(this goja.Value, args ...goja.Value) (goja.Value, error) {
  171. return nil, nil
  172. }
  173. v.logger.LogPrint(v.engineLogPrefix, hiedalog.DLN_VERBOSE, "no method 'cleanup' defined, use an empty one as default.")
  174. } else {
  175. exitHdl, xerr := v.checkFunc(exitHdlObj, "cleanup")
  176. if xerr != nil {
  177. return xerr
  178. }
  179. v.exitHandlerFunc = exitHdl
  180. }
  181. if err != nil {
  182. return err
  183. }
  184. v.loopFunc = loopFunc
  185. return nil
  186. }
  187. func (v *JSVM) checkFunc(jsval goja.Value, name string) (goja.Callable, error) {
  188. if v.disposed {
  189. v.logger.LogPrint(v.engineLogPrefix, hiedalog.DLN_WARN, "can not do this because the vm is disposed")
  190. return nil, Err_JSVMErrorVMDisposed
  191. }
  192. if jsval == nil {
  193. v.logger.LogPrintf(v.engineLogPrefix, hiedalog.DLN_ERROR, "failed get method '%s' in main script exports: undefined", name)
  194. return nil, Err_JSVMErrorNoSuchProperty
  195. }
  196. if jsval.SameAs(goja.Undefined()) {
  197. v.logger.LogPrintf(v.engineLogPrefix, hiedalog.DLN_ERROR, "failed get method '%s' in main script exports: undefined", name)
  198. return nil, Err_JSVMErrorNoSuchProperty
  199. }
  200. jfunc, ok := goja.AssertFunction(jsval)
  201. if !ok {
  202. v.logger.LogPrintf(v.engineLogPrefix, hiedalog.DLN_ERROR, "property '%s' is not a function", name)
  203. return nil, Err_JSVMErrorNotFunction
  204. }
  205. return jfunc, nil
  206. }
  207. func (v *JSVM) ExecSetup() error {
  208. if v.disposed {
  209. v.logger.LogPrint(v.engineLogPrefix, hiedalog.DLN_WARN, "can not do this because the vm is disposed")
  210. return Err_JSVMErrorVMDisposed
  211. }
  212. if v.setupFunc == nil {
  213. v.logger.LogPrint(v.engineLogPrefix, hiedalog.DLN_ERROR, "call on nil object 'setup'")
  214. return Err_JSVMCallOnNilObject
  215. }
  216. v.vm.ClearInterrupt()
  217. val, err := v.setupFunc(v.mainObj)
  218. if err != nil {
  219. _, ok := err.(*goja.InterruptedError)
  220. if ok {
  221. v.logger.LogPrint(v.engineLogPrefix, hiedalog.DLN_WARN, "JSVM interrupted when exec 'setup' script call.")
  222. return Err_JSVMAbortByInterruit
  223. }
  224. v.logger.LogPrint(v.engineLogPrefix, hiedalog.DLN_WARN, "call 'setup' function returns error: ", err)
  225. } else {
  226. res := "null"
  227. if val != nil {
  228. res = val.String()
  229. }
  230. v.logger.LogPrint(v.engineLogPrefix, hiedalog.DLN_DEBUG, "call 'setup' function ok. result: ", res)
  231. }
  232. return nil
  233. }
  234. func (v *JSVM) ExecCleanup() error {
  235. if v.disposed {
  236. v.logger.LogPrint(v.engineLogPrefix, hiedalog.DLN_WARN, "can not do this because the vm is disposed")
  237. return Err_JSVMErrorVMDisposed
  238. }
  239. if v.setupFunc == nil {
  240. v.logger.LogPrint(v.engineLogPrefix, hiedalog.DLN_ERROR, "call on nil object 'cleanup'")
  241. return Err_JSVMCallOnNilObject
  242. }
  243. v.vm.ClearInterrupt()
  244. val, err := v.exitHandlerFunc(v.mainObj)
  245. if err != nil {
  246. _, ok := err.(*goja.InterruptedError)
  247. if ok {
  248. v.logger.LogPrint(v.engineLogPrefix, hiedalog.DLN_ERROR, "JSVM interrupted when exec 'cleanup' script call.")
  249. return nil
  250. }
  251. v.logger.LogPrint(v.engineLogPrefix, hiedalog.DLN_WARN, "call 'cleanup' function returns error: ", err)
  252. } else {
  253. res := "null"
  254. if val != nil {
  255. res = val.String()
  256. }
  257. v.logger.LogPrint(v.engineLogPrefix, hiedalog.DLN_DEBUG, "call 'cleanup' function ok. result: ", res)
  258. }
  259. return nil
  260. }
  261. func (v *JSVM) execLoop() error {
  262. if v.disposed {
  263. v.logger.LogPrint(v.engineLogPrefix, hiedalog.DLN_WARN, "can not do this because the vm is disposed")
  264. return Err_JSVMErrorVMDisposed
  265. }
  266. if v.loopFunc == nil {
  267. v.logger.LogPrint(v.engineLogPrefix, hiedalog.DLN_ERROR, "call on nil object 'loop'")
  268. return Err_JSVMCallOnNilObject
  269. }
  270. val, err := v.loopFunc(v.mainObj)
  271. if err != nil {
  272. _, ok := err.(*goja.InterruptedError)
  273. if ok {
  274. v.logger.LogPrint(v.engineLogPrefix, hiedalog.DLN_INFO, "JSVM interrupted when exec 'loop' script call.")
  275. return Err_JSVMAbortByInterruit
  276. }
  277. v.logger.LogPrint(v.engineLogPrefix, hiedalog.DLN_WARN, "call 'loop' function returns error: ", err)
  278. } else {
  279. res := "null"
  280. if val != nil {
  281. res = val.String()
  282. }
  283. v.logger.LogPrint(v.engineLogPrefix, hiedalog.DLN_DEBUG, "call 'loop' function ok. result: ", res)
  284. }
  285. return nil
  286. }
  287. func (v *JSVM) RunLoop() error {
  288. if v.disposed {
  289. v.logger.LogPrint(v.engineLogPrefix, hiedalog.DLN_WARN, "can not do this because the vm is disposed")
  290. return Err_JSVMErrorVMDisposed
  291. }
  292. ctx, cncl := context.WithCancel(context.Background())
  293. v.loopCncl = cncl
  294. v.loopCtx = ctx
  295. v.vm.ClearInterrupt()
  296. RunLoopLoop:
  297. for {
  298. wch := make(chan error)
  299. go func() {
  300. rerr := v.execLoop()
  301. wch <- rerr
  302. }()
  303. select {
  304. case <-ctx.Done():
  305. {
  306. v.logger.LogPrint(v.engineLogPrefix, hiedalog.DLN_DEBUG, "'loop' function execution aborted by VM StopLoop.")
  307. v.vm.Interrupt(nil)
  308. break RunLoopLoop
  309. }
  310. case xrerr := <-wch:
  311. {
  312. if xrerr != nil {
  313. if xrerr == Err_JSVMAbortByInterruit {
  314. break RunLoopLoop
  315. } else {
  316. v.logger.LogPrint(v.engineLogPrefix, hiedalog.DLN_VERBOSE, "error in call 'loop' function: ", xrerr)
  317. }
  318. }
  319. break
  320. }
  321. }
  322. }
  323. v.logger.LogPrint(v.engineLogPrefix, hiedalog.DLN_INFO, "main loop end.")
  324. return nil
  325. }
  326. func (v *JSVM) StopLoop(err error) {
  327. if v.loopCncl != nil {
  328. v.loopCncl()
  329. }
  330. }
  331. func (v *JSVM) RegisterObject(name string, val interface{}) error {
  332. if v.disposed {
  333. v.logger.LogPrint(v.engineLogPrefix, hiedalog.DLN_WARN, "can not do this because the vm is disposed")
  334. return Err_JSVMErrorVMDisposed
  335. }
  336. return v.vm.Set(name, val)
  337. }
  338. func (v *JSVM) JSCallQuit() {
  339. if v.disposed {
  340. return
  341. }
  342. v.logger.LogPrint(v.engineLogPrefix, hiedalog.DLN_INFO, "script call 'quit'. send interrupt to JSVM.")
  343. v.vm.Interrupt(nil)
  344. }
  345. func (v *JSVM) VMToValue(val interface{}) goja.Value {
  346. return v.vm.ToValue(val)
  347. }
  348. func (v *JSVM) EmptyObject() *goja.Object {
  349. return v.vm.NewObject()
  350. }
  351. func (v *JSVM) GetGlobalObject() *goja.Object {
  352. return v.vm.GlobalObject()
  353. }
  354. func (v *JSVM) EngineDispose() {
  355. if v.disposed {
  356. return
  357. }
  358. if v.env != nil {
  359. v.env.EnvDispose()
  360. }
  361. v.logger.LogPrint(v.engineLogPrefix, hiedalog.DLN_DEBUG, "engine deactivated.")
  362. v.disposed = true
  363. }