From 715de5e4422f1b535e47e9390f73a3eafa86e355 Mon Sep 17 00:00:00 2001 From: Star Date: Thu, 24 Jul 2025 21:44:41 +0800 Subject: [PATCH] =?UTF-8?q?=E4=BD=BF=E7=94=A8=20gojs/task=20=E4=BB=A3?= =?UTF-8?q?=E6=9B=BF=E5=86=85=E7=BD=AE=E4=BB=BB=E5=8A=A1=20=E6=94=AF?= =?UTF-8?q?=E6=8C=81=20load=20=E5=8A=A0=E8=BD=BD=E7=9A=84=E6=9C=8D?= =?UTF-8?q?=E5=8A=A1=E7=83=AD=E6=9B=B4=E6=96=B0=EF=BC=88=E9=9C=80=E9=85=8D?= =?UTF-8?q?=E7=BD=AEhotLoad=EF=BC=89=20=E5=A2=9E=E5=BC=BA=E5=94=AF?= =?UTF-8?q?=E4=B8=80id=E8=8E=B7=E5=8F=96=EF=BC=88=E4=BD=BF=E7=94=A8?= =?UTF-8?q?=E6=96=B0=E7=AE=97=E6=B3=95=EF=BC=8C=E4=BE=9D=E8=B5=96Redis?= =?UTF-8?q?=EF=BC=89=20session=E6=94=AF=E6=8C=81=E5=9F=BA=E4=BA=8E?= =?UTF-8?q?=E7=BB=86=E7=B2=92=E5=BA=A6=E6=9D=83=E9=99=90=E5=8C=B9=E9=85=8D?= =?UTF-8?q?=EF=BC=88=E4=BC=A0=E5=85=A5=E6=94=AF=E6=8C=81=E7=9A=84=E5=8A=9F?= =?UTF-8?q?=E8=83=BD=E5=88=97=E8=A1=A8=E5=8C=B9=E9=85=8D=EF=BC=89?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- go.mod | 52 +++--- service.go | 336 ++++++++++++++++++++++++------------- service.ts | 61 +++---- session.go | 83 ++++++++- task.go | 299 +++++++++++++++++---------------- tests/api/echo.js | 5 +- tests/api/ws.js | 7 +- tests/service_pool_test.go | 5 +- tests/service_test.go | 32 ++++ tests/service_ws_test.go | 4 +- tests/start.js | 12 +- tests/start_ws.js | 13 +- tests/task.js | 18 +- 13 files changed, 580 insertions(+), 347 deletions(-) diff --git a/go.mod b/go.mod index 738896f..365bdb6 100644 --- a/go.mod +++ b/go.mod @@ -1,44 +1,54 @@ module apigo.cc/gojs/service -go 1.18 +go 1.23.0 require ( - apigo.cc/gojs v0.0.12 + apigo.cc/gojs v0.0.23 apigo.cc/gojs/console v0.0.2 - apigo.cc/gojs/http v0.0.3 - apigo.cc/gojs/util v0.0.8 + apigo.cc/gojs/file v0.0.4 + apigo.cc/gojs/http v0.0.7 + apigo.cc/gojs/runtime v0.0.3 + apigo.cc/gojs/task v0.0.4 + apigo.cc/gojs/util v0.0.13 github.com/gorilla/websocket v1.5.3 github.com/ssgo/config v1.7.9 - github.com/ssgo/discover v1.7.9 + github.com/ssgo/discover v1.7.10 github.com/ssgo/httpclient v1.7.8 - github.com/ssgo/log v1.7.7 - github.com/ssgo/redis v1.7.7 - github.com/ssgo/s v1.7.22 + github.com/ssgo/log v1.7.9 + github.com/ssgo/redis v1.7.8 + github.com/ssgo/s v1.7.24 github.com/ssgo/standard v1.7.7 - github.com/ssgo/u v1.7.13 + github.com/ssgo/u v1.7.21 ) require ( github.com/ZZMarquis/gm v1.3.2 // indirect - github.com/dlclark/regexp2 v1.11.4 // indirect - github.com/emmansun/gmsm v0.29.6 // indirect - github.com/fsnotify/fsnotify v1.8.0 // indirect + github.com/dlclark/regexp2 v1.11.5 // indirect + github.com/emmansun/gmsm v0.30.1 // indirect + github.com/fsnotify/fsnotify v1.9.0 // indirect github.com/go-ole/go-ole v1.3.0 // indirect + github.com/go-rod/rod v0.116.2 // indirect github.com/go-sourcemap/sourcemap v2.1.4+incompatible // indirect github.com/gomodule/redigo v1.9.2 // indirect - github.com/google/pprof v0.0.0-20230207041349-798e818bf904 // indirect - github.com/lufia/plan9stats v0.0.0-20240909124753-873cd0166683 // indirect + github.com/google/pprof v0.0.0-20250630185457-6e76a2b096b5 // indirect + github.com/lufia/plan9stats v0.0.0-20250317134145-8bc96cf8fc35 // indirect github.com/obscuren/ecies v0.0.0-20150213224233-7c0f4a9b18d9 // indirect github.com/power-devops/perfstat v0.0.0-20240221224432-82ca36839d55 // indirect + github.com/robfig/cron/v3 v3.0.1 // indirect github.com/shirou/gopsutil/v3 v3.24.5 // indirect github.com/shoenig/go-m1cpu v0.1.6 // indirect - github.com/ssgo/tool v0.4.28 // indirect - github.com/tklauser/go-sysconf v0.3.14 // indirect - github.com/tklauser/numcpus v0.9.0 // indirect + github.com/ssgo/tool v0.4.29 // indirect + github.com/tklauser/go-sysconf v0.3.15 // indirect + github.com/tklauser/numcpus v0.10.0 // indirect + github.com/ysmood/fetchup v0.2.3 // indirect + github.com/ysmood/goob v0.4.0 // indirect + github.com/ysmood/got v0.40.0 // indirect + github.com/ysmood/gson v0.7.3 // indirect + github.com/ysmood/leakless v0.9.0 // indirect github.com/yusufpapurcu/wmi v1.2.4 // indirect - golang.org/x/crypto v0.31.0 // indirect - golang.org/x/net v0.32.0 // indirect - golang.org/x/sys v0.28.0 // indirect - golang.org/x/text v0.21.0 // indirect + golang.org/x/crypto v0.40.0 // indirect + golang.org/x/net v0.42.0 // indirect + golang.org/x/sys v0.34.0 // indirect + golang.org/x/text v0.27.0 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect ) diff --git a/service.go b/service.go index f58aab7..c28dcca 100644 --- a/service.go +++ b/service.go @@ -37,6 +37,8 @@ var serviceMD string var server *s.AsyncServer var pools = map[string]*gojs.Pool{} +var poolsMTime = map[string]int64{} +var poolsConfig = map[string]gojs.PoolConfig{} var poolExists = map[string]bool{} var poolActionRegistered = map[string]bool{} @@ -71,6 +73,7 @@ type Config struct { LimitedMessage string Limiters map[string]*LimiterConfig LimiterRedis string + HotLoad int Proxy map[string]string Rewrite map[string]string @@ -90,8 +93,8 @@ func initConfig(opt *gojs.Obj, logger *log.Logger, vm *goja.Runtime) { s.SetWorkPath(u.String(startPath)) } // 处理配置 - serviceConfig = Config{"Session", "Device", "Client", "id", "", 3600, "auth failed", "verify failed", "too many requests", nil, "", map[string]string{}, map[string]string{}, map[string]string{}} - if errs := config.LoadConfig("service", &serviceConfig); errs != nil && len(errs) > 0 { + serviceConfig = Config{"Session", "Device", "Client", "id", "", 3600, "auth failed", "verify failed", "too many requests", nil, "", 0, map[string]string{}, map[string]string{}, map[string]string{}} + if errs := config.LoadConfig("service", &serviceConfig); len(errs) > 0 { panic(vm.NewGoError(errs[0])) } config.LoadConfig("service", &discover.Config) @@ -139,20 +142,33 @@ func initConfig(opt *gojs.Obj, logger *log.Logger, vm *goja.Runtime) { if authAccessToken && setAuthLevel < authLevel { setAuthLevel = s.GetAuthTokenLevel(request.Header.Get("Access-Token")) } + + authOk := false if setAuthLevel >= authLevel { + authOk = true // 默认通过 + if options != nil && options.Ext != nil && options.Ext["funcs"] != nil { + if needFuncs, ok := options.Ext["funcs"].([]string); ok && len(needFuncs) > 0 { + // 指定了细化的权限验证要求 + request.Set("funcs", needFuncs) + authOk = session.authFuncs(needFuncs) + } + } + } + + if authOk { return true, session + } + + msg := serviceConfig.AuthFieldMessage + if strings.Contains(msg, "{{") { + msg = strings.ReplaceAll(msg, "{{TARGET_AUTHLEVEL}}", u.String(authLevel)) + msg = strings.ReplaceAll(msg, "{{USER_AUTHLEVEL}}", u.String(setAuthLevel)) + } + var obj any + if json.Unmarshal([]byte(msg), &obj) == nil { + return false, obj } else { - msg := serviceConfig.AuthFieldMessage - if strings.Contains(msg, "{{") { - msg = strings.ReplaceAll(msg, "{{TARGET_AUTHLEVEL}}", u.String(authLevel)) - msg = strings.ReplaceAll(msg, "{{USER_AUTHLEVEL}}", u.String(setAuthLevel)) - } - var obj any - if json.Unmarshal([]byte(msg), &obj) == nil { - return false, obj - } else { - return false, msg - } + return false, msg } }) s.Init() @@ -180,6 +196,44 @@ func initConfig(opt *gojs.Obj, logger *log.Logger, vm *goja.Runtime) { } } } + + if serviceConfig.HotLoad > 0 { + s.NewTimerServer("hotload", time.Duration(serviceConfig.HotLoad)*time.Second, func(b *bool) { + tmpPoolsMTimes := map[string]int64{} + poolsLock.RLock() + for actionFile, mtime := range poolsMTime { + tmpPoolsMTimes[actionFile] = mtime + } + poolsLock.RUnlock() + + for actionFile, mtime := range tmpPoolsMTimes { + fi := u.GetFileInfo(actionFile) + if fi == nil { + continue + } + + newMtime := fi.ModTime.Unix() + if newMtime > mtime { + // 重新加载文件 + actionCode := u.ReadFileN(actionFile) + if !strings.Contains(actionCode, "function main(") { // || !strings.Contains(actionCode, ".register(") + logger.Error("hotload file %s failed, must be a js file with main function", actionFile) + continue + } + poolsLock.RLock() + oldOpt := poolsConfig[actionFile] + poolsLock.RUnlock() + + p := gojs.NewPoolByCode(actionCode, actionFile, oldOpt, logger) + poolsLock.Lock() + pools[actionFile] = p + poolsMTime[actionFile] = newMtime + poolsLock.Unlock() + logger.Info("hotload file %s success", actionFile, "mtime", fi.ModTime) + } + } + }, nil, nil) + } } func init() { @@ -312,72 +366,113 @@ func init() { s.ResetAllSets() return nil }, - "uniqueId": func(argsIn goja.FunctionCall, vm *goja.Runtime) goja.Value { - args := gojs.MakeArgs(&argsIn, vm) - size := args.Int(0) - var id string - if size >= 20 { - id = s.UniqueId20() - } else if size >= 16 { - id = s.UniqueId16() - } else if size >= 14 { - id = s.UniqueId14() - } else if size >= 12 { - id = s.UniqueId14()[0:12] - } else { - id = s.UniqueId() - } - return vm.ToValue(id) - }, - "uniqueIdL": func(argsIn goja.FunctionCall, vm *goja.Runtime) goja.Value { - args := gojs.MakeArgs(&argsIn, vm) - size := args.Int(0) - var id string - if size >= 20 { - id = s.UniqueId20() - } else if size >= 16 { - id = s.UniqueId16() - } else if size >= 14 { - id = s.UniqueId14() - } else if size >= 12 { - id = s.UniqueId14()[0:12] - } else { - id = s.UniqueId() - } - return vm.ToValue(strings.ToLower(id)) - }, + // "uniqueId": func(argsIn goja.FunctionCall, vm *goja.Runtime) goja.Value { + // args := gojs.MakeArgs(&argsIn, vm) + // size := args.Int(0) + // var id string + // if size >= 20 { + // id = s.UniqueId20() + // } else if size >= 16 { + // id = s.UniqueId16() + // } else if size >= 14 { + // id = s.UniqueId14() + // } else if size >= 12 { + // id = s.UniqueId14()[0:12] + // } else { + // id = s.UniqueId() + // } + // return vm.ToValue(id) + // }, + // "uniqueIdL": func(argsIn goja.FunctionCall, vm *goja.Runtime) goja.Value { + // args := gojs.MakeArgs(&argsIn, vm) + // size := args.Int(0) + // var id string + // if size >= 20 { + // id = s.UniqueId20() + // } else if size >= 16 { + // id = s.UniqueId16() + // } else if size >= 14 { + // id = s.UniqueId14() + // } else if size >= 12 { + // id = s.UniqueId14()[0:12] + // } else { + // id = s.UniqueId() + // } + // return vm.ToValue(strings.ToLower(id)) + // }, "id": func(argsIn goja.FunctionCall, vm *goja.Runtime) goja.Value { args := gojs.MakeArgs(&argsIn, vm).Check(1) - space := args.Str(0) - size := args.Int(1) - var id string - if size >= 12 { - id = s.Id12(space) - } else if size >= 10 { - id = s.Id10(space) - } else if size >= 8 { - id = s.Id8(space) - } else { - id = s.Id6(space) + size := args.Int(0) + if size == 0 { + size = 12 } - return vm.ToValue(id) - }, - "idL": func(argsIn goja.FunctionCall, vm *goja.Runtime) goja.Value { - args := gojs.MakeArgs(&argsIn, vm).Check(1) - space := args.Str(0) - size := args.Int(1) - var id string - if size >= 12 { - id = s.Id12(space) - } else if size >= 10 { - id = s.Id10(space) - } else if size >= 8 { - id = s.Id8(space) - } else { - id = s.Id6(space) + if size > 20 { + size = 20 } - return vm.ToValue(strings.ToLower(id)) + + rd := sessionRedis + if rd == nil { + // 使用u.Id + return vm.ToValue(u.UniqueId()[0:size]) + } + + tm := time.Now() + // 计算从2000年开始到现在的索引值(2000年时间戳:946656000)(5位62进制最小值:14776336)(可以表示901356495个,即142.9年至2142年) + secIndex := (tm.Unix()-946656000)/5 + 14776336 + secTag := int(tm.UnixMicro() % 5) + uid := u.AppendInt(nil, uint64(secIndex)) + secIndexKey := fmt.Sprintf("_SecIdx_%d", secIndex) + inSecIndex := rd.INCR(secIndexKey) + if inSecIndex == 1 { + rd.EXPIRE(secIndexKey, 6) + } + inSecIndexBytes := u.EncodeInt(uint64(inSecIndex)) + uid = u.AppendInt(uid, uint64(secTag*12+len(inSecIndexBytes))) // 长度位,防止不同长度+随机值发生碰撞(用毫秒的%4*12位基础+位数可以随机表示12位长度,看起来有变化) + uid = append(uid, inSecIndexBytes...) + + // 用随机数填充 + uid = u.FillInt(uid, size) + + // 交叉乱序 + uid = u.ExchangeInt(uid) + + // 散列乱序 + uid = u.HashInt(uid) + + return vm.ToValue(string(uid)) }, + // "id": func(argsIn goja.FunctionCall, vm *goja.Runtime) goja.Value { + // args := gojs.MakeArgs(&argsIn, vm).Check(1) + // space := args.Str(0) + // size := args.Int(1) + // var id string + // if size >= 12 { + // id = s.Id12(space) + // } else if size >= 10 { + // id = s.Id10(space) + // } else if size >= 8 { + // id = s.Id8(space) + // } else { + // id = s.Id6(space) + // } + // return vm.ToValue(id) + // }, + // "idL": func(argsIn goja.FunctionCall, vm *goja.Runtime) goja.Value { + // args := gojs.MakeArgs(&argsIn, vm).Check(1) + // space := args.Str(0) + // size := args.Int(1) + // var id string + // if size >= 12 { + // id = s.Id12(space) + // } else if size >= 10 { + // id = s.Id10(space) + // } else if size >= 8 { + // id = s.Id8(space) + // } else { + // id = s.Id6(space) + // } + // return vm.ToValue(strings.ToLower(id)) + // }, "register": func(argsIn goja.FunctionCall, vm *goja.Runtime) goja.Value { args := gojs.MakeArgs(&argsIn, vm).Check(2) o := args.Obj(0) @@ -443,7 +538,7 @@ func init() { NoBody: o.Bool("noBody"), NoLog200: o.Bool("noLog200"), Host: host, - //Ext: nil, + Ext: o.Map("ext"), Limiters: usedLimiters, } @@ -497,61 +592,66 @@ func init() { mainArgs = mainArgs1 } } - if !u.FileExists(actionFile) { + fi := u.GetFileInfo(actionFile) + if fi == nil { fullActionFile, _ := filepath.Abs(actionFile) panic(vm.NewGoError(errors.New("actionFile must be a js file path: " + fullActionFile))) } actionCode := u.ReadFileN(actionFile) - if !strings.Contains(actionCode, "function main(") || !strings.Contains(actionCode, ".register(") { - panic(vm.NewGoError(errors.New("actionFile must be a js file with main function and call service.register"))) + if !strings.Contains(actionCode, "function main(") { // || !strings.Contains(actionCode, ".register(") + panic(vm.NewGoError(errors.New("actionFile must be a js file with main function"))) } poolsLock.Lock() poolExists[actionFile] = true poolsLock.Unlock() - p := gojs.NewPoolByCode(actionCode, actionFile, gojs.PoolConfig{ + poolOpt := gojs.PoolConfig{ Min: mi, Max: ma, Idle: idle, Debug: debug, Args: mainArgs, - }, args.Logger) + } + p := gojs.NewPoolByCode(actionCode, actionFile, poolOpt, args.Logger) //p := gojs.NewLBByCode(actionCode, actionFile, gojs.LBConfig{ // Num: num, // Debug: debug, // Args: mainArgs, //}, args.Logger) + mtime := fi.ModTime.Unix() poolsLock.Lock() pools[actionFile] = p + poolsMTime[actionFile] = mtime + poolsConfig[actionFile] = poolOpt poolsLock.Unlock() return nil }, - "task": func(argsIn goja.FunctionCall, vm *goja.Runtime) goja.Value { - args := gojs.MakeArgs(&argsIn, vm).Check(1) - taskFile := args.Path(0) - interval := args.Int(1) - if interval == 0 { - interval = 1000 - } - if interval < 100 { - interval = 100 - } - if !u.FileExists(taskFile) { - panic(vm.NewGoError(errors.New("taskFile must be a js file path"))) - } - rt := gojs.New() - _, err := rt.RunFile(taskFile) - if err != nil { - panic(vm.NewGoError(err)) - } - s.NewTimerServer(taskFile, time.Duration(interval)*time.Millisecond, func(isRunning *bool) { - rt.RunCode("if(onRun)onRun()") - }, func() { - rt.RunCode("if(onStart)onStart()") - }, func() { - rt.RunCode("if(onStop)onStop()") - }) - return nil - }, + // "task": func(argsIn goja.FunctionCall, vm *goja.Runtime) goja.Value { + // args := gojs.MakeArgs(&argsIn, vm).Check(1) + // taskFile := args.Path(0) + // interval := args.Int(1) + // if interval == 0 { + // interval = 1000 + // } + // if interval < 100 { + // interval = 100 + // } + // if !u.FileExists(taskFile) { + // panic(vm.NewGoError(errors.New("taskFile must be a js file path"))) + // } + // rt := gojs.New() + // _, err := rt.RunFile(taskFile) + // if err != nil { + // panic(vm.NewGoError(err)) + // } + // s.NewTimerServer(taskFile, time.Duration(interval)*time.Millisecond, func(isRunning *bool) { + // rt.RunCode("if(onRun)onRun()") + // }, func() { + // rt.RunCode("if(onStart)onStart()") + // }, func() { + // rt.RunCode("if(onStop)onStop()") + // }) + // return nil + // }, "setTplFunc": func(argsIn goja.FunctionCall, vm *goja.Runtime) goja.Value { args := gojs.MakeArgs(&argsIn, vm).Check(1) fnObj := args.Obj(0) @@ -646,17 +746,17 @@ func init() { } return vm.ToValue(buf.String()) }, - "dataSet": DataSet, - "dataGet": DataGet, - "dataKeys": DataKeys, - "dataCount": DataCount, - "dataFetch": DataFetch, - "dataRemove": DataRemove, - "listPop": ListPop, - "listPush": ListPush, - "listCount": ListCount, - "listRemove": ListRemove, - "newCaller": NewCaller, + // "dataSet": DataSet, + // "dataGet": DataGet, + // "dataKeys": DataKeys, + // "dataCount": DataCount, + // "dataFetch": DataFetch, + // "dataRemove": DataRemove, + // "listPop": ListPop, + // "listPush": ListPush, + // "listCount": ListCount, + // "listRemove": ListRemove, + "newCaller": NewCaller, } gojs.Register("apigo.cc/gojs/service", gojs.Module{ diff --git a/service.ts b/service.ts index c53447b..080c3a2 100644 --- a/service.ts +++ b/service.ts @@ -6,22 +6,22 @@ export default { stop, register, load, - task, + // task, newCaller, - dataSet, - dataGet, - dataKeys, - dataCount, - dataFetch, - dataRemove, - listPush, - listPop, - listCount, - listRemove, + // dataSet, + // dataGet, + // dataKeys, + // dataCount, + // dataFetch, + // dataRemove, + // listPush, + // listPop, + // listCount, + // listRemove, id, - idL, - uniqueId, - uniqueIdL, + // idL, + // uniqueId, + // uniqueIdL, setTplFunc, tpl, } @@ -32,25 +32,26 @@ function stop(): void { } function register(option: RegisterOption, callback: (params: RequestParams) => void): any { return null } function load(serviceFile: string, poolConfig?: PoolConfig): void { } -function task(taskFile: string, interval: number = 1000): void { } +// function task(taskFile: string, interval: number = 1000): void { } function newCaller(): Caller { return null as any } -function dataSet(scope: string, key: string, value: any): void { } -function dataGet(scope: string, key: string): any { return null } -function dataKeys(scope: string): string[] { return [] } -function dataCount(scope: string): number { return 0 } -function dataFetch(scope: string): Map { return null as any } -function dataRemove(scope: string, key?: string): void { } +// function dataSet(scope: string, key: string, value: any): void { } +// function dataGet(scope: string, key: string): any { return null } +// function dataKeys(scope: string): string[] { return [] } +// function dataCount(scope: string): number { return 0 } +// function dataFetch(scope: string): Map { return null as any } +// function dataRemove(scope: string, key?: string): void { } -function listPush(scope: string, key: string, value: any): void { } -function listPop(scope: string, key: string): any { return null } -function listCount(scope: string): number { return 0 } -function listRemove(scope: string): void { } -function id(space: string, size?: number): string { return '' } -function idL(space: string, size?: number): string { return '' } -function uniqueId(size?: number): string { return '' } -function uniqueIdL(size?: number): string { return '' } +// function listPush(scope: string, key: string, value: any): void { } +// function listPop(scope: string, key: string): any { return null } +// function listCount(scope: string): number { return 0 } +// function listRemove(scope: string): void { } +function id(size?: number): string { return '' } +// function id(space: string, size?: number): string { return '' } +// function idL(space: string, size?: number): string { return '' } +// function uniqueId(size?: number): string { return '' } +// function uniqueIdL(size?: number): string { return '' } function setTplFunc(fnList: Object): void { } function tpl(file: string, data: Object): string { return '' } @@ -116,6 +117,7 @@ interface Config { limitedMessage: string | Object // 访问受限时的消息,默认为 too many requests,可以设置对象来返回JSON,可以使用模版 {{LIMITED_FROM}}、{{LIMITED_VALUE}} limiterRedis: string // 限流器使用的Redis连接,默认使用内存存储 limiters: Map // 限流器配置,from 为数据来源,例如:ip、user、device、header.User-Agent、in.phone 等(in表示从请求参数中获取),time为时间间隔,单位ms,times为 时间间隔内允许访问的次数 + hotLoad: number // 热加载配置,单位s,默认值:0,0表示不检测热加载 // gateway 的配置参数 proxy: Map // 代理配置,key为[host][path],value为代理的目标应用或URL @@ -160,6 +162,7 @@ interface RegisterOption { limiters: string[] verifies: Object requires: string[] + ext: Object onMessage: (params: OnMessageParams) => void onClose: (params: RequestParams) => void } diff --git a/session.go b/session.go index 38814b9..e0b1835 100644 --- a/session.go +++ b/session.go @@ -2,6 +2,7 @@ package service import ( "reflect" + "strings" "sync" "time" @@ -9,12 +10,14 @@ import ( "apigo.cc/gojs/goja" "github.com/ssgo/log" "github.com/ssgo/redis" + "github.com/ssgo/u" ) type Session struct { - id string - conn *redis.Redis - data map[string]any + id string + conn *redis.Redis + data map[string]any + funcAuthCache map[string]bool } var sessionRedis *redis.Redis @@ -40,9 +43,10 @@ func NewSession(id string, logger *log.Logger) *Session { } } return &Session{ - id: id, - conn: conn, - data: data, + id: id, + conn: conn, + data: data, + funcAuthCache: map[string]bool{}, } } @@ -102,6 +106,73 @@ func (session *Session) SetAuthLevel(argsIn goja.FunctionCall, vm *goja.Runtime) return nil } +func (session *Session) authFuncs(needFuncs []string) bool { + cacheKey := strings.Join(needFuncs, "; ") + if cachedResult, ok := session.funcAuthCache[cacheKey]; ok { + return cachedResult + } + + normalAuthOk := 0 + requiredAuthTotal := 0 + requiredAuthOk := 0 + isOk := false + if userFuncs, ok := session.data["funcs"].([]string); ok && len(userFuncs) > 0 { + if u.StringIn(userFuncs, "system.superAdmin.") { + isOk = true + } else { + // 统计必须有几个权限 + for _, needFunc := range needFuncs { + if needFunc[0] == '&' { + requiredAuthTotal++ + } + } + + // 检查是否有匹配的权限(左匹配) + for _, needFunc := range needFuncs { + isRequired := false + if needFunc[0] == '&' { + // 必须有此权限 + isRequired = true + needFunc = needFunc[1:] + } + for _, userFunc := range userFuncs { + if strings.HasPrefix(userFunc, needFunc) { + // 匹配成功 + if isRequired { + // 必须有此权限并且匹配成功 + requiredAuthOk++ + } else { + // 普通权限匹配成功 + normalAuthOk++ + } + break + } + } + if normalAuthOk > 0 && requiredAuthOk == requiredAuthTotal { + // 普通权限匹配成功,并且必须有权限也匹配成功,直接返回 + isOk = true + break + } + } + } + } + session.funcAuthCache[cacheKey] = isOk + return isOk +} + +func (session *Session) AuthFuncs(argsIn goja.FunctionCall, vm *goja.Runtime) goja.Value { + args := gojs.MakeArgs(&argsIn, vm).Check(1) + var needFuncs []string + if args.Arguments[0].ExportType().Kind() == reflect.Slice { + // 第一个参数是数组 + needFuncs = args.Arr(0).StrArray(0) + } else { + // 平铺的参数 + needFuncs = args.StrArray(0) + } + return vm.ToValue(session.authFuncs(needFuncs)) +} + func (session *Session) Save(argsIn goja.FunctionCall, vm *goja.Runtime) goja.Value { if session.conn == nil { now := time.Now().Unix() diff --git a/task.go b/task.go index 4a6c960..0d033a0 100644 --- a/task.go +++ b/task.go @@ -1,162 +1,165 @@ package service -import ( - "container/list" - "sync" +// type Task struct { +// spec string +// file string +// vm *gojs.Runtime +// lock sync.RWMutex +// mtime time.Time +// // policy string +// } +// var tasks []Task +// var tasksLock = sync.RWMutex{} - "apigo.cc/gojs" - "apigo.cc/gojs/goja" -) +// var taskData = map[string]map[string]any{} +// var taskDataLock = sync.RWMutex{} -var taskData = map[string]map[string]any{} -var taskDataLock = sync.RWMutex{} +// var taskList = map[string]*list.List{} +// var taskListLock = sync.RWMutex{} -var taskList = map[string]*list.List{} -var taskListLock = sync.RWMutex{} +// func DataSet(argsIn goja.FunctionCall, vm *goja.Runtime) goja.Value { +// args := gojs.MakeArgs(&argsIn, vm).Check(3) +// scope := args.Str(0) +// key := args.Str(1) +// value := args.Any(2) +// taskDataLock.Lock() +// defer taskDataLock.Unlock() +// if taskData[scope] == nil { +// taskData[scope] = map[string]any{} +// } +// taskData[scope][key] = value +// return nil +// } -func DataSet(argsIn goja.FunctionCall, vm *goja.Runtime) goja.Value { - args := gojs.MakeArgs(&argsIn, vm).Check(3) - scope := args.Str(0) - key := args.Str(1) - value := args.Any(2) - taskDataLock.Lock() - defer taskDataLock.Unlock() - if taskData[scope] == nil { - taskData[scope] = map[string]any{} - } - taskData[scope][key] = value - return nil -} +// func DataGet(argsIn goja.FunctionCall, vm *goja.Runtime) goja.Value { +// args := gojs.MakeArgs(&argsIn, vm).Check(2) +// scope := args.Str(0) +// key := args.Str(1) +// taskDataLock.RLock() +// defer taskDataLock.RUnlock() +// if taskData[scope] != nil { +// return vm.ToValue(taskData[scope][key]) +// } +// return nil +// } -func DataGet(argsIn goja.FunctionCall, vm *goja.Runtime) goja.Value { - args := gojs.MakeArgs(&argsIn, vm).Check(2) - scope := args.Str(0) - key := args.Str(1) - taskDataLock.RLock() - defer taskDataLock.RUnlock() - if taskData[scope] != nil { - return vm.ToValue(taskData[scope][key]) - } - return nil -} +// func DataKeys(argsIn goja.FunctionCall, vm *goja.Runtime) goja.Value { +// args := gojs.MakeArgs(&argsIn, vm).Check(1) +// scope := args.Str(0) +// taskDataLock.RLock() +// defer taskDataLock.RUnlock() +// if taskData[scope] != nil { +// keys := make([]string, len(taskData[scope])) +// i := 0 +// for key := range taskData[scope] { +// keys[i] = key +// i++ +// } +// return vm.ToValue(keys) +// } +// return nil +// } -func DataKeys(argsIn goja.FunctionCall, vm *goja.Runtime) goja.Value { - args := gojs.MakeArgs(&argsIn, vm).Check(1) - scope := args.Str(0) - taskDataLock.RLock() - defer taskDataLock.RUnlock() - if taskData[scope] != nil { - keys := make([]string, len(taskData[scope])) - i := 0 - for key := range taskData[scope] { - keys[i] = key - i++ - } - return vm.ToValue(keys) - } - return nil -} +// func DataCount(argsIn goja.FunctionCall, vm *goja.Runtime) goja.Value { +// args := gojs.MakeArgs(&argsIn, vm).Check(1) +// scope := args.Str(0) +// taskDataLock.RLock() +// defer taskDataLock.RUnlock() +// if taskData[scope] != nil { +// return vm.ToValue(len(taskData[scope])) +// } +// return vm.ToValue(0) +// } -func DataCount(argsIn goja.FunctionCall, vm *goja.Runtime) goja.Value { - args := gojs.MakeArgs(&argsIn, vm).Check(1) - scope := args.Str(0) - taskDataLock.RLock() - defer taskDataLock.RUnlock() - if taskData[scope] != nil { - return vm.ToValue(len(taskData[scope])) - } - return vm.ToValue(0) -} +// func DataFetch(argsIn goja.FunctionCall, vm *goja.Runtime) goja.Value { +// args := gojs.MakeArgs(&argsIn, vm).Check(1) +// scope := args.Str(0) +// taskDataLock.RLock() +// defer taskDataLock.RUnlock() +// if taskData[scope] != nil { +// all := make(map[string]any) +// for k, v := range taskData[scope] { +// all[k] = v +// } +// return vm.ToValue(all) +// } +// return nil +// } -func DataFetch(argsIn goja.FunctionCall, vm *goja.Runtime) goja.Value { - args := gojs.MakeArgs(&argsIn, vm).Check(1) - scope := args.Str(0) - taskDataLock.RLock() - defer taskDataLock.RUnlock() - if taskData[scope] != nil { - all := make(map[string]any) - for k, v := range taskData[scope] { - all[k] = v - } - return vm.ToValue(all) - } - return nil -} +// func DataRemove(argsIn goja.FunctionCall, vm *goja.Runtime) goja.Value { +// args := gojs.MakeArgs(&argsIn, vm).Check(1) +// scope := args.Str(0) +// key := args.Str(1) +// taskDataLock.Lock() +// defer taskDataLock.Unlock() +// if taskData[scope] != nil { +// if key != "" { +// delete(taskData[scope], key) +// } else { +// delete(taskData, scope) +// } +// } +// return nil +// } -func DataRemove(argsIn goja.FunctionCall, vm *goja.Runtime) goja.Value { - args := gojs.MakeArgs(&argsIn, vm).Check(1) - scope := args.Str(0) - key := args.Str(1) - taskDataLock.Lock() - defer taskDataLock.Unlock() - if taskData[scope] != nil { - if key != "" { - delete(taskData[scope], key) - } else { - delete(taskData, scope) - } - } - return nil -} +// func ListPush(argsIn goja.FunctionCall, vm *goja.Runtime) goja.Value { +// args := gojs.MakeArgs(&argsIn, vm).Check(2) +// scope := args.Str(0) +// value := args.Any(1) +// fromHead := args.Bool(2) +// taskListLock.Lock() +// defer taskListLock.Unlock() +// list1 := taskList[scope] +// if list1 == nil { +// list1 = list.New() +// taskList[scope] = list1 +// } +// if fromHead { +// list1.PushFront(value) +// } else { +// list1.PushBack(value) +// } +// return nil +// } -func ListPush(argsIn goja.FunctionCall, vm *goja.Runtime) goja.Value { - args := gojs.MakeArgs(&argsIn, vm).Check(2) - scope := args.Str(0) - value := args.Any(1) - fromHead := args.Bool(2) - taskListLock.Lock() - defer taskListLock.Unlock() - list1 := taskList[scope] - if list1 == nil { - list1 = list.New() - taskList[scope] = list1 - } - if fromHead { - list1.PushFront(value) - } else { - list1.PushBack(value) - } - return nil -} +// func ListPop(argsIn goja.FunctionCall, vm *goja.Runtime) goja.Value { +// args := gojs.MakeArgs(&argsIn, vm).Check(1) +// scope := args.Str(0) +// fromEnd := args.Bool(1) +// taskListLock.Lock() +// var item *list.Element +// defer taskListLock.Unlock() +// list1 := taskList[scope] +// if list1 != nil { +// if fromEnd { +// item = list1.Front() +// } else { +// item = list1.Back() +// } +// if item != nil { +// list1.Remove(item) +// return vm.ToValue(item.Value) +// } +// } +// return nil +// } -func ListPop(argsIn goja.FunctionCall, vm *goja.Runtime) goja.Value { - args := gojs.MakeArgs(&argsIn, vm).Check(1) - scope := args.Str(0) - fromEnd := args.Bool(1) - taskListLock.Lock() - var item *list.Element - defer taskListLock.Unlock() - list1 := taskList[scope] - if list1 != nil { - if fromEnd { - item = list1.Front() - } else { - item = list1.Back() - } - if item != nil { - list1.Remove(item) - return vm.ToValue(item.Value) - } - } - return nil -} +// func ListCount(argsIn goja.FunctionCall, vm *goja.Runtime) goja.Value { +// args := gojs.MakeArgs(&argsIn, vm).Check(1) +// scope := args.Str(0) +// taskListLock.RLock() +// defer taskListLock.RUnlock() +// if taskList[scope] != nil { +// return vm.ToValue(taskList[scope].Len()) +// } +// return vm.ToValue(0) +// } -func ListCount(argsIn goja.FunctionCall, vm *goja.Runtime) goja.Value { - args := gojs.MakeArgs(&argsIn, vm).Check(1) - scope := args.Str(0) - taskListLock.RLock() - defer taskListLock.RUnlock() - if taskList[scope] != nil { - return vm.ToValue(taskList[scope].Len()) - } - return vm.ToValue(0) -} - -func ListRemove(argsIn goja.FunctionCall, vm *goja.Runtime) goja.Value { - args := gojs.MakeArgs(&argsIn, vm).Check(1) - scope := args.Str(0) - taskListLock.Lock() - defer taskListLock.Unlock() - delete(taskList, scope) - return nil -} +// func ListRemove(argsIn goja.FunctionCall, vm *goja.Runtime) goja.Value { +// args := gojs.MakeArgs(&argsIn, vm).Check(1) +// scope := args.Str(0) +// taskListLock.Lock() +// defer taskListLock.Unlock() +// delete(taskList, scope) +// return nil +// } diff --git a/tests/api/echo.js b/tests/api/echo.js index edb87ee..6aff00c 100644 --- a/tests/api/echo.js +++ b/tests/api/echo.js @@ -1,13 +1,12 @@ import service from "apigo.cc/gojs/service" -import co from "apigo.cc/gojs/console" -import u from "apigo.cc/gojs/util" +import rt from "apigo.cc/gojs/runtime" function main() { service.register({ path: '/echo2', noLog200: true }, ({ args, response }) => { // setTimeout(() => { // response.end(args.name) // }, 10) - u.sleep(10) + rt.sleep(10) return args.name }) } diff --git a/tests/api/ws.js b/tests/api/ws.js index 272a4af..2951757 100644 --- a/tests/api/ws.js +++ b/tests/api/ws.js @@ -1,5 +1,6 @@ import s from "apigo.cc/gojs/service" import co from "apigo.cc/gojs/console" +import task from "apigo.cc/gojs/task" function main() { s.register({ @@ -9,11 +10,13 @@ function main() { }, onClose: ({ client }) => { co.info('ws closed', client.id) - s.dataRemove('wsTest', client.id) + // s.dataRemove('wsTest', client.id) + task.remove('wsTest_' + client.id) } }, ({ client }) => { co.info('ws connected', client.id) - s.dataSet('wsTest', client.id, client) + // s.dataSet('wsTest', client.id, client) + task.set('wsTest_' + client.id, client) client.write('Hello, World!') }) } diff --git a/tests/service_pool_test.go b/tests/service_pool_test.go index fa70c65..3f0cf50 100644 --- a/tests/service_pool_test.go +++ b/tests/service_pool_test.go @@ -2,12 +2,14 @@ package service_test import ( "fmt" + "os" "sync" "time" "apigo.cc/gojs" _ "apigo.cc/gojs/console" _ "apigo.cc/gojs/http" + _ "apigo.cc/gojs/runtime" "apigo.cc/gojs/service" _ "apigo.cc/gojs/service" _ "apigo.cc/gojs/util" @@ -38,6 +40,7 @@ func TestStartByPool(t *testing.T) { gojs.ExportForDev() rt2 = gojs.New() + u.CopyFile("api/echo.js", "api/echo_tmp.js") err := rt2.StartFromFile("start.js") if err != nil { t.Fatal("start failed", err) @@ -216,7 +219,7 @@ func TestStopByPool(t *testing.T) { t.Fatal("stop failed", err) } gojs.WaitAll() - + os.Remove("api/echo_tmp.js") runtime.GC() ms3 := runtime.MemStats{} runtime.ReadMemStats(&ms3) diff --git a/tests/service_test.go b/tests/service_test.go index 29ab9f8..84df60c 100644 --- a/tests/service_test.go +++ b/tests/service_test.go @@ -2,12 +2,16 @@ package service_test import ( "fmt" + "os" + "strings" "testing" "time" "apigo.cc/gojs" _ "apigo.cc/gojs/console" + _ "apigo.cc/gojs/file" _ "apigo.cc/gojs/http" + _ "apigo.cc/gojs/runtime" _ "apigo.cc/gojs/service" _ "apigo.cc/gojs/util" "github.com/ssgo/httpclient" @@ -22,6 +26,7 @@ const runTimes = 100 func TestStart(t *testing.T) { gojs.ExportForDev() rt = gojs.New() + u.CopyFile("api/echo.js", "api/echo_tmp.js") err := rt.StartFromFile("start.js") if err != nil { t.Fatal("start failed", err) @@ -55,6 +60,19 @@ func TestJsEcho(t *testing.T) { } } +func TestJsEcho2(t *testing.T) { + for i := 0; i < runTimes; i++ { + name := u.UniqueId() + r, err := rt.RunCode("test2('" + name + "')") + if err != nil { + t.Fatal("test2 js get failed, got error", err) + } else if r != name { + t.Fatal("test2 js get failed, name not match", r, name) + } + } + u.WriteFile("api/echo_tmp.js", strings.Replace(u.ReadFileN("api/echo.js"), "return args.name", "return args.name+'!!'", 1)) +} + func TestGoEcho(t *testing.T) { hc := httpclient.GetClientH2C(0) for i := 0; i < runTimes; i++ { @@ -118,6 +136,19 @@ func TestGoAsyncEcho(t *testing.T) { fmt.Println(u.BGreen("last name:"), lastName, lastResult) } +func TestJsEcho2WithHotLoad(t *testing.T) { + time.Sleep(time.Second) + for i := 0; i < runTimes; i++ { + name := u.UniqueId() + r, err := rt.RunCode("test2('" + name + "')") + if err != nil { + t.Fatal("test2 js get failed, got error", err) + } else if r != name+"!!" { + t.Fatal("test2 js get failed, name not match", r, name) + } + } +} + func TestStop(t *testing.T) { go func() { time.Sleep(100 * time.Millisecond) @@ -125,6 +156,7 @@ func TestStop(t *testing.T) { }() gojs.WaitAll() time.Sleep(100 * time.Millisecond) + os.Remove("api/echo_tmp.js") // if err != nil { // t.Fatal("stop failed", err) // } diff --git a/tests/service_ws_test.go b/tests/service_ws_test.go index 7784d26..9b59209 100644 --- a/tests/service_ws_test.go +++ b/tests/service_ws_test.go @@ -6,7 +6,9 @@ import ( "apigo.cc/gojs" _ "apigo.cc/gojs/console" _ "apigo.cc/gojs/http" + _ "apigo.cc/gojs/runtime" _ "apigo.cc/gojs/service" + _ "apigo.cc/gojs/task" _ "apigo.cc/gojs/util" "github.com/ssgo/u" @@ -43,7 +45,7 @@ func TestWS(t *testing.T) { } func TestStopWSByPool(t *testing.T) { - _, err := rt3.RunCode("s.stop()") + _, err := rt3.RunCode("s.stop();task.stop();") if err != nil { t.Fatal("stop failed", err) } diff --git a/tests/start.js b/tests/start.js index bbceffc..2339d5d 100644 --- a/tests/start.js +++ b/tests/start.js @@ -1,7 +1,8 @@ import s from "apigo.cc/gojs/service" import http from "apigo.cc/gojs/http" -import u from "apigo.cc/gojs/util" +import rt from "apigo.cc/gojs/runtime" import co from "apigo.cc/gojs/console" +import file from "apigo.cc/gojs/file" let h2c = http let urlPrefix @@ -33,16 +34,17 @@ function main() { }, rewrite: { '/echo2.js': '/echo.js' - } + }, + hotLoad: 1, }) s.register({ path: '/echo', noLog200: true }, ({ args, response }) => { // setTimeout(() => { // response.end(args.name) // }, 1) - u.sleep(1) + rt.sleep(1) return args.name }) - s.load('api/echo.js', { min: 20, max: 1000, idle: 100 }) + s.load('api/echo_tmp.js', { min: 20, max: 1000, idle: 100 }) s.load('api/user.js') let host = s.start() h2c = http.newH2C({ @@ -89,7 +91,7 @@ function testUser() { if (r.statusCode != 429) { return r } - u.sleep(100) + rt.sleep(100) // 测试限流器过期后允许的 1 次请求 r = h2c.get('/userInfo').object() diff --git a/tests/start_ws.js b/tests/start_ws.js index 031e3be..94c6cd0 100644 --- a/tests/start_ws.js +++ b/tests/start_ws.js @@ -1,13 +1,16 @@ import s from "apigo.cc/gojs/service" import http from "apigo.cc/gojs/http" -import u from "apigo.cc/gojs/util" +import rt from "apigo.cc/gojs/runtime" import co from "apigo.cc/gojs/console" +import task from "apigo.cc/gojs/task" let hc = http let urlPrefix function main() { s.load('api/ws.js') - s.task('task.js', 100) + // s.task('task.js', 100) + task.addTask("@every 1s", 'task.js') + task.start() let host = s.start() hc = http.new({ baseURL: 'http://' + host }) return host @@ -31,7 +34,7 @@ function testWS() { co.info('test ws abc ok') // ws.ping() - u.sleep(10) + rt.sleep(10) let pc = ws.pingCount() co.info('test ws ping ok', pc.pingTimes, pc.pongTimes) @@ -51,15 +54,13 @@ function testWS() { return r } co.info('test ws json ok') - - u.sleep(1000) + rt.sleep(2000) for (let i = 0; i < 5; i++) { let j = ws.read() if (i !== j.data) { return j } } - ws.close() return true } diff --git a/tests/task.js b/tests/task.js index fb1e2eb..51f5f3e 100644 --- a/tests/task.js +++ b/tests/task.js @@ -1,5 +1,6 @@ import s from 'apigo.cc/gojs/service' import co from 'apigo.cc/gojs/console' +import task from 'apigo.cc/gojs/task' function onStart() { co.info('task start') @@ -7,24 +8,27 @@ function onStart() { let i = 0 function onRun() { - let connCount = s.dataCount('wsTest') - if (connCount > 0) { - let conns = s.dataFetch('wsTest') + let keys = task.keys('wsTest_') + // let connCount = s.dataCount('wsTest') + if (keys.length > 0) { + // let conns = s.dataFetch('wsTest') + let conns = task.getAll('wsTest_') for (let id in conns) { let conn = conns[id] try { conn.write(i++) } catch (e) { co.error(e) - s.dataRemove('wsTest', id) + task.remove(id) } } } - co.info('task run', connCount) + co.info('task run', keys.length) } function onStop() { - s.dataRemove('wsTest') - co.info('task stop', s.dataCount('wsTest')) + // s.dataRemove('wsTest') + task.removeAll('wsTest_') + co.info('task stop') }