446 lines
10 KiB
Go
446 lines
10 KiB
Go
// Copyright 2021 Liuxiangchao iwind.liu@gmail.com. All rights reserved.
|
||
//go:build script
|
||
|
||
package js
|
||
|
||
import (
|
||
"errors"
|
||
"fmt"
|
||
"github.com/TeaOSLab/EdgeNode/internal/remotelogs"
|
||
"github.com/iwind/TeaGo/Tea"
|
||
"github.com/iwind/TeaGo/files"
|
||
"github.com/iwind/TeaGo/types"
|
||
"math/big"
|
||
"reflect"
|
||
"regexp"
|
||
"rogchap.com/v8go"
|
||
"strings"
|
||
"sync"
|
||
)
|
||
|
||
var namespaceReg = regexp.MustCompile(`^[a-zA-Z0-9_.]+$`)
|
||
|
||
type Context struct {
|
||
rawContext *v8go.Context
|
||
isolate *Isolate
|
||
objectTemplate *ObjectTemplate
|
||
|
||
goObjectId uint32
|
||
goObjectMap map[uint32]any
|
||
goObjectLocker sync.RWMutex
|
||
|
||
serverId int64
|
||
|
||
countUses int
|
||
}
|
||
|
||
func NewContext(rawContext *v8go.Context, isolate *Isolate) (*Context, error) {
|
||
var ctx = &Context{
|
||
rawContext: rawContext,
|
||
isolate: isolate,
|
||
goObjectMap: map[uint32]any{},
|
||
}
|
||
err := ctx.init()
|
||
if err != nil {
|
||
return nil, err
|
||
}
|
||
return ctx, nil
|
||
}
|
||
|
||
func (this *Context) init() error {
|
||
this.objectTemplate = NewObjectTemplate(this)
|
||
|
||
// 加载内置函数库
|
||
err := this.loadLibraries()
|
||
if err != nil {
|
||
return err
|
||
}
|
||
|
||
return nil
|
||
}
|
||
|
||
func (this *Context) Isolate() *Isolate {
|
||
return this.isolate
|
||
}
|
||
|
||
func (this *Context) Run(source string, origin string) (*v8go.Value, error) {
|
||
return this.rawContext.RunScript(source, origin)
|
||
}
|
||
|
||
func (this *Context) RunScript(script *Script) (*v8go.Value, error) {
|
||
return script.rawScript.Run(this.rawContext)
|
||
}
|
||
|
||
func (this *Context) RawContext() *v8go.Context {
|
||
return this.rawContext
|
||
}
|
||
|
||
func (this *Context) NewObjectTemplate() *ObjectTemplate {
|
||
return NewObjectTemplate(this)
|
||
}
|
||
|
||
// NewValue 包装新的值
|
||
// TODO 支持[]byte
|
||
func (this *Context) NewValue(value any) (*v8go.Value, error) {
|
||
if value == nil {
|
||
return nil, nil
|
||
}
|
||
switch x := value.(type) {
|
||
case *v8go.Value:
|
||
return x, nil
|
||
case *v8go.Object:
|
||
return x.Value, nil
|
||
case int8:
|
||
value = int32(x)
|
||
case uint8:
|
||
value = uint32(x)
|
||
case int:
|
||
value = int64(x)
|
||
case uint:
|
||
value = uint64(x)
|
||
case int16:
|
||
value = int32(x)
|
||
case uint16:
|
||
value = uint32(x)
|
||
case string, int32, uint32, bool, int64, uint64, *big.Int:
|
||
// 内置支持的类型,我们 DO NOTHING HERE
|
||
default:
|
||
var rv = reflect.ValueOf(value)
|
||
switch rv.Kind() {
|
||
case reflect.Slice:
|
||
arrayValue, err := this.Run("new Array()", "")
|
||
if err != nil {
|
||
return nil, err
|
||
}
|
||
var obj = arrayValue.Object()
|
||
err = obj.Set("length", uint32(rv.Len()))
|
||
if err != nil {
|
||
return nil, err
|
||
}
|
||
|
||
for i := 0; i < rv.Len(); i++ {
|
||
var rvv = rv.Index(i)
|
||
if !rvv.CanInterface() {
|
||
continue
|
||
}
|
||
ev, err := this.NewValue(rvv.Interface())
|
||
if err != nil {
|
||
return nil, err
|
||
}
|
||
err = obj.SetIdx(uint32(i), ev)
|
||
if err != nil {
|
||
return nil, err
|
||
}
|
||
}
|
||
|
||
return obj.Value, nil
|
||
case reflect.Map:
|
||
obj, err := this.objectTemplate.NewInstance(this)
|
||
if err != nil {
|
||
return nil, err
|
||
}
|
||
for _, kv := range rv.MapKeys() {
|
||
var rvv = rv.MapIndex(kv)
|
||
if !rvv.CanInterface() {
|
||
continue
|
||
}
|
||
ev, err := this.NewValue(rvv.Interface())
|
||
if err != nil {
|
||
return nil, err
|
||
}
|
||
var kind = kv.Kind()
|
||
|
||
// 支持map[any]Type
|
||
if kind == reflect.Interface {
|
||
kind = kv.Elem().Kind()
|
||
kv = kv.Elem()
|
||
}
|
||
switch kind {
|
||
case reflect.String:
|
||
err = obj.Set(kv.String(), ev)
|
||
if err != nil {
|
||
return nil, err
|
||
}
|
||
case reflect.Int, reflect.Uint, reflect.Int8, reflect.Uint8, reflect.Int16, reflect.Uint16, reflect.Int32, reflect.Uint32, reflect.Int64, reflect.Uint64, reflect.Float32, reflect.Float64:
|
||
err = obj.Set(types.String(kv.Interface()), ev)
|
||
if err != nil {
|
||
return nil, err
|
||
}
|
||
case reflect.Bool:
|
||
if kv.Bool() {
|
||
err = obj.Set("1", ev)
|
||
} else {
|
||
err = obj.Set("0", ev)
|
||
}
|
||
if err != nil {
|
||
return nil, err
|
||
}
|
||
default:
|
||
// 我们直接忽略那些类型不符的Key
|
||
}
|
||
}
|
||
return obj.Value, nil
|
||
case reflect.Struct:
|
||
obj, err := this.objectTemplate.NewInstance(this)
|
||
if err != nil {
|
||
return nil, err
|
||
}
|
||
var rt = rv.Type()
|
||
for i := 0; i < rt.NumField(); i++ {
|
||
var field = rt.Field(i)
|
||
if !field.IsExported() {
|
||
continue
|
||
}
|
||
var jsonTag = field.Tag.Get("json")
|
||
if len(jsonTag) == 0 {
|
||
jsonTag = strings.ToLower(field.Name[:1]) + field.Name[1:]
|
||
}
|
||
|
||
var rFieldValue = rv.Field(i)
|
||
if !rFieldValue.CanInterface() {
|
||
continue
|
||
}
|
||
ev, err := this.NewValue(rFieldValue.Interface())
|
||
if err != nil {
|
||
return nil, err
|
||
}
|
||
err = obj.Set(jsonTag, ev)
|
||
if err != nil {
|
||
return nil, err
|
||
}
|
||
}
|
||
return obj.Value, nil
|
||
case reflect.Ptr:
|
||
rv = reflect.Indirect(rv)
|
||
if rv.CanInterface() {
|
||
return this.NewValue(rv.Interface())
|
||
}
|
||
default:
|
||
|
||
}
|
||
}
|
||
return v8go.NewValue(this.isolate.RawIsolate(), value)
|
||
}
|
||
|
||
func (this *Context) AddGoObject(obj any) (objectId uint32) {
|
||
this.goObjectLocker.Lock()
|
||
defer this.goObjectLocker.Unlock()
|
||
|
||
this.goObjectId++
|
||
this.goObjectMap[this.goObjectId] = obj
|
||
return this.goObjectId
|
||
}
|
||
|
||
func (this *Context) AddGoRequestObject(obj RequestInterface) (objectId uint32) {
|
||
return this.AddGoObject(obj)
|
||
}
|
||
|
||
func (this *Context) AddGoResponseObject(obj ResponseInterface) (objectId uint32) {
|
||
return this.AddGoObject(obj)
|
||
}
|
||
|
||
func (this *Context) GoObject(objectId uint32) (obj any, ok bool) {
|
||
this.goObjectLocker.RLock()
|
||
obj, ok = this.goObjectMap[objectId]
|
||
this.goObjectLocker.RUnlock()
|
||
return
|
||
}
|
||
|
||
func (this *Context) GoObjectMap() map[uint32]any {
|
||
this.goObjectLocker.RLock()
|
||
defer this.goObjectLocker.RUnlock()
|
||
return this.goObjectMap
|
||
}
|
||
|
||
// Done 单次调用用Context之后,需要调用Done()来计数和清除绑定的系统资源
|
||
func (this *Context) Done() {
|
||
this.countUses++
|
||
|
||
for _, library := range SharedLibraryManager.All() {
|
||
library.JSDone(this)
|
||
}
|
||
|
||
this.goObjectLocker.Lock()
|
||
this.goObjectMap = map[uint32]any{}
|
||
this.goObjectId = 0
|
||
this.goObjectLocker.Unlock()
|
||
}
|
||
|
||
func (this *Context) Cleanup() {
|
||
this.Done()
|
||
}
|
||
|
||
func (this *Context) Close() {
|
||
this.Done()
|
||
|
||
for _, library := range SharedLibraryManager.All() {
|
||
library.JSDispose(this)
|
||
}
|
||
|
||
this.rawContext.Close()
|
||
}
|
||
|
||
func (this *Context) Global() *v8go.Object {
|
||
return this.rawContext.Global()
|
||
}
|
||
|
||
func (this *Context) loadLibraries() error {
|
||
// 运行公共函数脚本
|
||
_, err := this.Run(LibraryUtilsJavascript, "utils.js")
|
||
if err != nil {
|
||
return this.WrapJSErr(err)
|
||
}
|
||
|
||
// libraries
|
||
for _, library := range SharedLibraryManager.All() {
|
||
library.JSInit(this)
|
||
|
||
// 准备命名空间
|
||
var namespace = library.JSNamespace()
|
||
if !namespaceReg.MatchString(namespace) {
|
||
return errors.New("load library failed: invalid namespace '" + namespace + "'")
|
||
}
|
||
if !strings.HasPrefix(namespace, "gojs.") {
|
||
return errors.New("load library failed: namespace '" + namespace + "' must be started with prefix 'gojs.'")
|
||
}
|
||
|
||
_, err = this.Run("gojs.prepareNamespace(\""+namespace+"\")", "utils.js")
|
||
if err != nil {
|
||
return fmt.Errorf("prepare namespace '%s' failed: %w", namespace, err)
|
||
}
|
||
|
||
// 运行脚本
|
||
var prototypeJS = strings.ReplaceAll(library.JSPrototype(), "$this.", library.JSNamespace()+".")
|
||
_, err = this.Run(prototypeJS, namespace+".js")
|
||
if err != nil {
|
||
return fmt.Errorf("run '%s' prototype failed: %w", namespace, this.WrapJSErr(err))
|
||
}
|
||
|
||
// 增加函数
|
||
var reflectValue = reflect.ValueOf(library)
|
||
libValue, err := this.Run(namespace, "utils.js")
|
||
if err != nil {
|
||
return fmt.Errorf("get namespace object '%s' failed: %w", namespace, err)
|
||
}
|
||
var libObject = libValue.Object()
|
||
var argumentType = reflect.TypeOf(&FunctionArguments{})
|
||
for i := 0; i < reflectValue.NumMethod(); i++ {
|
||
var method = reflectValue.Method(i)
|
||
if !method.IsValid() {
|
||
continue
|
||
}
|
||
var typeMethod = reflectValue.Type().Method(i)
|
||
var methodName = typeMethod.Name
|
||
if strings.HasPrefix(methodName, "JS") {
|
||
continue
|
||
}
|
||
|
||
var methodType = typeMethod.Type
|
||
if methodType.NumIn() != 2 /** this, arguments **/ || methodType.NumOut() != 2 /** result, error **/ {
|
||
continue
|
||
}
|
||
|
||
if !methodType.In(1).AssignableTo(argumentType) {
|
||
continue
|
||
}
|
||
|
||
if methodType.Out(1).Name() != "error" {
|
||
continue
|
||
}
|
||
|
||
err = libObject.Set(methodName, v8go.NewFunctionTemplate(this.isolate.RawIsolate(), func(info *v8go.FunctionCallbackInfo) *v8go.Value {
|
||
out := method.Call([]reflect.Value{reflect.ValueOf(NewFunctionArguments(this, info))})
|
||
if !out[1].IsNil() {
|
||
errValue, _ := this.NewValue(out[1].Interface().(error).Error())
|
||
this.isolate.ThrowException(errValue)
|
||
return nil
|
||
}
|
||
|
||
v, err := this.NewValue(out[0].Interface())
|
||
if err != nil {
|
||
errValue, _ := this.NewValue(err.Error())
|
||
this.isolate.ThrowException(errValue)
|
||
return nil
|
||
}
|
||
return v
|
||
}).GetFunction(this.rawContext))
|
||
if err != nil {
|
||
return err
|
||
}
|
||
}
|
||
}
|
||
|
||
// common scripts
|
||
var commonScripts = sharedCommonScripts
|
||
if len(commonScripts) > 0 {
|
||
for _, commonScript := range commonScripts {
|
||
_, err := this.Run(commonScript.Code, commonScript.Filename)
|
||
if err != nil {
|
||
remotelogs.Error("SCRIPT", "load common script '"+commonScript.Filename+"' failed: "+err.Error())
|
||
}
|
||
}
|
||
}
|
||
|
||
// load common files from directory
|
||
err = this.loadLibrariesFromDisk()
|
||
if err != nil {
|
||
remotelogs.Error("SCRIPT", err.Error())
|
||
}
|
||
|
||
return nil
|
||
}
|
||
|
||
func (this *Context) WrapJSErr(err error) error {
|
||
if err == nil {
|
||
return nil
|
||
}
|
||
jsErr, ok := err.(*v8go.JSError)
|
||
if !ok {
|
||
return err
|
||
}
|
||
return fmt.Errorf("%w, location: %s", jsErr, jsErr.Location)
|
||
}
|
||
|
||
func (this *Context) SetServerId(serverId int64) {
|
||
this.serverId = serverId
|
||
}
|
||
|
||
func (this *Context) ServerId() int64 {
|
||
return this.serverId
|
||
}
|
||
|
||
func (this *Context) ReadLineFromLibrary(name string, line int) string {
|
||
if line < 1 {
|
||
return ""
|
||
}
|
||
for _, library := range SharedLibraryManager.All() {
|
||
if library.JSNamespace() == name || library.JSNamespace()+".js" == name {
|
||
var lines = strings.Split(library.JSPrototype(), "\n")
|
||
if len(lines) >= line {
|
||
return lines[line-1]
|
||
}
|
||
}
|
||
}
|
||
return ""
|
||
}
|
||
|
||
// 从硬盘中加载脚本
|
||
func (this *Context) loadLibrariesFromDisk() error {
|
||
var dir = Tea.Root + "/scripts/js/"
|
||
var jsFiles = files.NewFile(dir).List()
|
||
for _, file := range jsFiles {
|
||
if file.IsFile() && strings.HasSuffix(file.Name(), ".js") {
|
||
s, err := file.ReadAllString()
|
||
if err != nil {
|
||
return fmt.Errorf("load local common script '%s' failed: %w", file.Path(), err)
|
||
}
|
||
|
||
_, err = this.Run(s, file.Name())
|
||
if err != nil {
|
||
return fmt.Errorf("load local common script '%s' failed: %w", file.Path(), err)
|
||
}
|
||
}
|
||
}
|
||
return nil
|
||
}
|