mirror of
https://github.com/GopeedLab/gopeed.git
synced 2026-11-10 02:20:28 +00:00
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:
7 files changed
+183
-51
No files matched your search
@@ -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
|
||||
|
||||
@@ -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']
|
||||
|
||||
@@ -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(),
|
||||
})
|
||||
})
|
||||
}
|
||||
@@ -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, " "))
|
||||
}
|
||||
|
||||
@@ -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)) {
|
||||
|
||||
Reference in new issue
Block a user