mirror of
https://github.com/GopeedLab/gopeed.git
synced 2026-11-04 15:49:49 +00:00
268 lines
6.4 KiB
Go
268 lines
6.4 KiB
Go
package engine
|
|
|
|
import (
|
|
_ "embed"
|
|
"errors"
|
|
"fmt"
|
|
"sync"
|
|
|
|
"github.com/GopeedLab/gopeed/pkg/base"
|
|
gojaerror "github.com/GopeedLab/gopeed/pkg/download/engine/inject/error"
|
|
fetchapi "github.com/GopeedLab/gopeed/pkg/download/engine/inject/fetch"
|
|
"github.com/GopeedLab/gopeed/pkg/download/engine/inject/file"
|
|
"github.com/GopeedLab/gopeed/pkg/download/engine/inject/stream"
|
|
"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"
|
|
gojaurl "github.com/dop251/goja_nodejs/url"
|
|
)
|
|
|
|
//go:embed polyfill/out/index.js
|
|
var polyfillScript string
|
|
|
|
type Engine struct {
|
|
loop *eventloop.EventLoop
|
|
|
|
closeOnce sync.Once
|
|
cleanupMu sync.Mutex
|
|
cleanups []func()
|
|
|
|
Runtime *goja.Runtime
|
|
}
|
|
|
|
type JSFunction func(goja.FunctionCall) goja.Value
|
|
|
|
// RunString executes the script and returns the go type value
|
|
// if script result is promise, it will be resolved
|
|
func (e *Engine) RunString(script string) (value any, err error) {
|
|
return e.runOnLoop(func(runtime *goja.Runtime) (goja.Value, error) {
|
|
return runtime.RunString(script)
|
|
})
|
|
}
|
|
|
|
// CallFunction calls the function and returns the go type value
|
|
// if function result is promise, it will be resolved
|
|
func (e *Engine) CallFunction(fn any, args ...any) (value any, err error) {
|
|
return e.runOnLoop(func(runtime *goja.Runtime) (goja.Value, error) {
|
|
var jsArgs []goja.Value
|
|
for _, arg := range args {
|
|
jsArgs = append(jsArgs, runtime.ToValue(arg))
|
|
}
|
|
switch f := fn.(type) {
|
|
case goja.Callable:
|
|
if args == nil {
|
|
return f(nil)
|
|
}
|
|
return f(nil, jsArgs...)
|
|
case JSFunction:
|
|
return callExportedFunction(func(call goja.FunctionCall) goja.Value {
|
|
return f(call)
|
|
}, jsArgs...)
|
|
default:
|
|
return nil, fmt.Errorf("unsupported function type: %T", fn)
|
|
}
|
|
})
|
|
}
|
|
|
|
func (e *Engine) runOnLoop(fn func(runtime *goja.Runtime) (goja.Value, error)) (any, error) {
|
|
type result struct {
|
|
value any
|
|
err error
|
|
}
|
|
ch := make(chan result, 1)
|
|
ok := e.loop.RunOnLoop(func(runtime *goja.Runtime) {
|
|
var once sync.Once
|
|
sendResult := func(res result) {
|
|
once.Do(func() {
|
|
ch <- res
|
|
})
|
|
}
|
|
defer func() {
|
|
if r := recover(); r != nil {
|
|
switch v := r.(type) {
|
|
case error:
|
|
sendResult(result{err: v})
|
|
case goja.Value:
|
|
sendResult(result{err: exportJSError(v)})
|
|
default:
|
|
sendResult(result{err: fmt.Errorf("panic: %v", r)})
|
|
}
|
|
}
|
|
}()
|
|
value, err := fn(runtime)
|
|
if err != nil {
|
|
sendResult(result{err: err})
|
|
return
|
|
}
|
|
if p, ok := value.Export().(*goja.Promise); ok {
|
|
switch p.State() {
|
|
case goja.PromiseStateFulfilled:
|
|
sendResult(result{value: exportJSValue(p.Result())})
|
|
return
|
|
case goja.PromiseStateRejected:
|
|
sendResult(result{err: exportJSError(p.Result())})
|
|
return
|
|
}
|
|
promiseObj := value.ToObject(runtime)
|
|
thenVal := promiseObj.Get("then")
|
|
thenFn, ok := goja.AssertFunction(thenVal)
|
|
if !ok {
|
|
sendResult(result{err: errors.New("promise.then is not callable")})
|
|
return
|
|
}
|
|
onFulfilled := runtime.ToValue(func(call goja.FunctionCall) goja.Value {
|
|
sendResult(result{value: exportJSValue(call.Argument(0))})
|
|
return goja.Undefined()
|
|
})
|
|
onRejected := runtime.ToValue(func(call goja.FunctionCall) goja.Value {
|
|
sendResult(result{err: exportJSError(call.Argument(0))})
|
|
return goja.Undefined()
|
|
})
|
|
if _, err := thenFn(promiseObj, onFulfilled, onRejected); err != nil {
|
|
sendResult(result{err: err})
|
|
}
|
|
return
|
|
}
|
|
sendResult(result{value: exportJSValue(value)})
|
|
})
|
|
if !ok {
|
|
return nil, errors.New("engine loop terminated")
|
|
}
|
|
res := <-ch
|
|
if res.err != nil {
|
|
return nil, res.err
|
|
}
|
|
return res.value, nil
|
|
}
|
|
|
|
func (e *Engine) Close() {
|
|
e.closeOnce.Do(func() {
|
|
e.cleanupMu.Lock()
|
|
cleanups := e.cleanups
|
|
e.cleanups = nil
|
|
e.cleanupMu.Unlock()
|
|
for _, cleanup := range cleanups {
|
|
cleanup()
|
|
}
|
|
e.loop.Terminate()
|
|
})
|
|
}
|
|
|
|
func (e *Engine) addCleanup(cleanup func()) {
|
|
if cleanup == nil {
|
|
return
|
|
}
|
|
e.cleanupMu.Lock()
|
|
e.cleanups = append(e.cleanups, cleanup)
|
|
e.cleanupMu.Unlock()
|
|
}
|
|
|
|
type Config struct {
|
|
ProxyConfig *base.DownloaderProxyConfig
|
|
StreamConfig *stream.Config
|
|
}
|
|
|
|
func NewEngine(cfg *Config) *Engine {
|
|
if cfg == nil {
|
|
cfg = &Config{}
|
|
}
|
|
loop := eventloop.NewEventLoop()
|
|
engine := &Engine{
|
|
loop: loop,
|
|
}
|
|
loop.Start()
|
|
done := make(chan struct{})
|
|
loop.RunOnLoop(func(runtime *goja.Runtime) {
|
|
defer close(done)
|
|
engine.Runtime = runtime
|
|
runtime.SetFieldNameMapper(goja.TagFieldNameMapper("json", true))
|
|
vm.Enable(runtime)
|
|
gojaurl.Enable(runtime)
|
|
if err := gojaerror.Enable(runtime); err != nil {
|
|
return
|
|
}
|
|
if err := file.Enable(runtime); err != nil {
|
|
return
|
|
}
|
|
if _, err := runtime.RunString(polyfillScript); err != nil {
|
|
return
|
|
}
|
|
// polyfill global
|
|
if err := runtime.Set("global", runtime.GlobalObject()); err != nil {
|
|
return
|
|
}
|
|
// polyfill window
|
|
if err := runtime.Set("window", runtime.GlobalObject()); err != nil {
|
|
return
|
|
}
|
|
// polyfill window.location
|
|
if _, err := runtime.RunString("global.location = new URL('http://localhost');"); err != nil {
|
|
return
|
|
}
|
|
if err := stream.Enable(runtime, loop, cfg.StreamConfig); err != nil {
|
|
return
|
|
}
|
|
if err := fetchapi.Enable(runtime, loop, &fetchapi.Config{
|
|
ProxyHandler: cfg.ProxyConfig.ToHandler(),
|
|
RegisterCleanup: engine.addCleanup,
|
|
}); err != nil {
|
|
return
|
|
}
|
|
if err := xhr.Enable(runtime); err != nil {
|
|
return
|
|
}
|
|
return
|
|
})
|
|
<-done
|
|
return engine
|
|
}
|
|
|
|
func Run(script string) (value any, err error) {
|
|
engine := NewEngine(nil)
|
|
return engine.RunString(script)
|
|
}
|
|
|
|
func exportJSValue(value goja.Value) any {
|
|
if value == nil {
|
|
return nil
|
|
}
|
|
return value.Export()
|
|
}
|
|
|
|
func exportJSError(value goja.Value) error {
|
|
if value == nil || goja.IsUndefined(value) || goja.IsNull(value) {
|
|
return errors.New("promise rejected")
|
|
}
|
|
if err, ok := value.Export().(error); ok {
|
|
return err
|
|
}
|
|
stack := value.String()
|
|
if ro, ok := value.(*goja.Object); ok {
|
|
stackVal := ro.Get("stack")
|
|
if stackVal != nil && stackVal.String() != "" {
|
|
stack = stackVal.String()
|
|
}
|
|
}
|
|
return errors.New(stack)
|
|
}
|
|
|
|
func callExportedFunction(fn func(goja.FunctionCall) goja.Value, args ...goja.Value) (ret goja.Value, err error) {
|
|
defer func() {
|
|
if r := recover(); r != nil {
|
|
switch v := r.(type) {
|
|
case error:
|
|
err = v
|
|
case goja.Value:
|
|
err = exportJSError(v)
|
|
default:
|
|
err = fmt.Errorf("panic: %v", r)
|
|
}
|
|
}
|
|
}()
|
|
return fn(goja.FunctionCall{
|
|
This: nil,
|
|
Arguments: args,
|
|
}), nil
|
|
}
|