feat: enhanced extension script engine (#277)

1. update goja version
2. add node vm polyfill
3. non-stop event loop handle
4 improve logger print
This commit is contained in:
Levi authored and GitHub committed 2023-11-24 22:41:16 +08:00
1 parent 6f541715df
commit aad7395dde
7 files changed
+183 -51

No files matched your search

+44 -6
View File
@@ -3,14 +3,14 @@ package engine
import (
_ "embed"
"errors"
"fmt"
"github.com/GopeedLab/gopeed/pkg/download/engine/inject/file"
"github.com/GopeedLab/gopeed/pkg/download/engine/inject/formdata"
"github.com/GopeedLab/gopeed/pkg/download/engine/inject/vm"
"github.com/GopeedLab/gopeed/pkg/download/engine/inject/xhr"
"github.com/dop251/goja"
"github.com/dop251/goja_nodejs/eventloop"
"github.com/dop251/goja_nodejs/process"
"github.com/dop251/goja_nodejs/url"
"time"
)
//go:embed polyfill/out/index.js
@@ -27,12 +27,16 @@ type Engine struct {
func (e *Engine) RunString(script string) (value any, err error) {
defer func() {
if r := recover(); r != nil {
err = fmt.Errorf("%v", r)
err = r.(error)
}
}()
var result goja.Value
e.loop.Run(func(runtime *goja.Runtime) {
result, err = runtime.RunString(script)
if err == nil {
go e.await(result)
}
})
if err != nil {
return
@@ -45,9 +49,10 @@ func (e *Engine) RunString(script string) (value any, err error) {
func (e *Engine) CallFunction(fn goja.Callable, args ...any) (value any, err error) {
defer func() {
if r := recover(); r != nil {
err = fmt.Errorf("%v", r)
err = r.(error)
}
}()
var result goja.Value
e.loop.Run(func(runtime *goja.Runtime) {
if args == nil {
@@ -59,13 +64,42 @@ func (e *Engine) CallFunction(fn goja.Callable, args ...any) (value any, err err
}
result, err = fn(nil, jsArgs...)
}
if err == nil {
go e.await(result)
}
})
if err != nil {
return nil, err
return
}
return resolveResult(result)
}
// loop.Run will hang if the script result has a non-stop code, such as setInterval.
// Therefore, a trick must be used to run the script and wait for it to complete
func (e *Engine) await(value any) {
if value == nil {
return
}
if v, ok := value.(goja.Value); ok {
// if result is promise, wait for it to be resolved
if p, ok := v.Export().(*goja.Promise); ok {
// check promise state every 100 milliseconds, until it is resolved
for {
if p.State() == goja.PromiseStatePending {
time.Sleep(time.Second)
continue
}
break
}
// stop the event loop
e.loop.StopNoWait()
}
}
}
func (e *Engine) Close() {
e.loop.Stop()
}
@@ -78,8 +112,8 @@ func NewEngine() *Engine {
loop.Run(func(runtime *goja.Runtime) {
engine.Runtime = runtime
runtime.SetFieldNameMapper(goja.TagFieldNameMapper("json", true))
vm.Enable(runtime)
url.Enable(runtime)
process.Enable(runtime)
if err := file.Enable(runtime); err != nil {
return
}
@@ -96,6 +130,10 @@ func NewEngine() *Engine {
if err := runtime.Set("global", runtime.GlobalObject()); err != nil {
return
}
// polyfill window.location
if _, err := runtime.RunString("global.location = new URL('http://localhost');"); err != nil {
return
}
return
})
return engine
+60
View File
@@ -23,6 +23,7 @@ func TestPolyfill(t *testing.T) {
doTestPolyfill(t, "Blob")
doTestPolyfill(t, "FormData")
doTestPolyfill(t, "fetch")
doTestPolyfill(t, "__gopeed_create_vm")
}
func TestFetch(t *testing.T) {
@@ -191,6 +192,65 @@ function testTimeout(){
}
}
func TestVm(t *testing.T) {
engine := NewEngine()
value, err := engine.RunString(`
const vm = __gopeed_create_vm()
vm.set('a', 1)
vm.set('b', 2)
const result = vm.runString('a=a+1;b=b+1;a+b;')
const out = {
"a": vm.get('a'),
"b": vm.get('b'),
"result": result
}
out
`)
if err != nil {
t.Fatal(err)
}
want := map[string]any{
"a": 2,
"b": 3,
"result": 5,
}
if !test.JsonEqual(value, want) {
t.Fatalf("vm test failed, want %v, got %v", want, value)
}
}
func TestNonStopLoop(t *testing.T) {
engine := NewEngine()
_, err := engine.RunString(`
function leak(){
setInterval(() => {
},500)
}
function test(){
leak()
return new Promise((resolve, reject) => {
setTimeout(() => {
resolve('done')
}, 1000)
})
}
`)
if err != nil {
t.Fatal(err)
}
val, err := callTestFun(engine, "test")
if err != nil {
panic(err)
}
if val != "done" {
t.Fatalf("infinite loop test failed, want %s, got %s", "done", val)
}
}
func doTestPolyfill(t *testing.T, module string) {
value, err := Run(fmt.Sprintf(`
!!globalThis['%s']
+44
View File
@@ -0,0 +1,44 @@
package vm
import (
"github.com/dop251/goja"
"github.com/dop251/goja_nodejs/eventloop"
)
type Vm struct {
loop *eventloop.EventLoop
}
func (vm *Vm) Set(name string, value any) {
vm.loop.Run(func(runtime *goja.Runtime) {
runtime.Set(name, value)
})
}
func (vm *Vm) Get(name string) (value any) {
vm.loop.Run(func(runtime *goja.Runtime) {
value = runtime.Get(name)
})
return
}
func (vm *Vm) RunString(script string) (value any, err error) {
defer func() {
if r := recover(); r != nil {
err = r.(error)
}
}()
vm.loop.Run(func(runtime *goja.Runtime) {
value, err = runtime.RunString(script)
})
return
}
func Enable(runtime *goja.Runtime) error {
return runtime.Set("__gopeed_create_vm", func(call goja.FunctionCall) goja.Value {
return runtime.ToValue(&Vm{
loop: eventloop.NewEventLoop(),
})
})
}
+6 -6
View File
@@ -598,28 +598,28 @@ type InstanceLogger struct {
logger *logger.Logger
}
func (l *InstanceLogger) Debug(msg ...any) {
func (l *InstanceLogger) Debug(msg ...goja.Value) {
if l.devMode {
l.logger.Debug().Msg(l.append(msg...))
}
}
func (l *InstanceLogger) Info(msg ...any) {
func (l *InstanceLogger) Info(msg ...goja.Value) {
l.logger.Info().Msg(l.append(msg...))
}
func (l *InstanceLogger) Warn(msg ...any) {
func (l *InstanceLogger) Warn(msg ...goja.Value) {
l.logger.Warn().Msg(l.append(msg...))
}
func (l *InstanceLogger) Error(msg ...any) {
func (l *InstanceLogger) Error(msg ...goja.Value) {
l.logger.Error().Msg(l.append(msg...))
}
func (l *InstanceLogger) append(msg ...any) string {
func (l *InstanceLogger) append(msg ...goja.Value) string {
strMsg := make([]string, len(msg))
for i, m := range msg {
strMsg[i] = fmt.Sprint(m)
strMsg[i] = m.String()
}
return fmt.Sprintf("[%s] %s", l.identity, strings.Join(strMsg, " "))
}
+5 -4
View File
@@ -4,6 +4,7 @@ import (
"errors"
"github.com/GopeedLab/gopeed/internal/logger"
"github.com/GopeedLab/gopeed/pkg/base"
"github.com/dop251/goja"
"os"
"testing"
"time"
@@ -356,10 +357,10 @@ func TestDownloader_Extension_Logger(t *testing.T) {
il := newInstanceLogger(&Extension{
Name: "test",
}, logger)
il.Debug("msg1", "msg2")
il.Info("msg1", "msg2")
il.Warn("msg1", "msg2")
il.Error("msg1", "msg2")
il.Debug(goja.NaN(), goja.Undefined())
il.Info(goja.NaN(), goja.Undefined())
il.Warn(goja.NaN(), goja.Undefined())
il.Error(goja.NaN(), goja.Undefined())
}
func setupDownloader(fn func(downloader *Downloader)) {