commit 70d5483fe1fd18d6016de8e00861db88bfc8d1be Author: Star Date: Fri Jul 18 15:27:22 2025 +0800 1 diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..47904c6 --- /dev/null +++ b/.gitignore @@ -0,0 +1,6 @@ +.* +!.gitignore +go.sum +node_modules +package.json +env.yml diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..d894367 --- /dev/null +++ b/LICENSE @@ -0,0 +1,9 @@ +MIT License + +Copyright (c) 2024 apigo + +Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. diff --git a/build/task_darwin_amd64 b/build/task_darwin_amd64 new file mode 100644 index 0000000..ce85592 Binary files /dev/null and b/build/task_darwin_amd64 differ diff --git a/data.go b/data.go new file mode 100644 index 0000000..0b9e210 --- /dev/null +++ b/data.go @@ -0,0 +1,74 @@ +package plugin + +import ( + "container/list" +) + +func (obj *PluginObject) Get(k string) any { + obj.taskDataLock.RLock() + defer obj.taskDataLock.RUnlock() + return obj.taskData[k] +} + +func (obj *PluginObject) Set(k string, v any) { + obj.taskDataLock.Lock() + defer obj.taskDataLock.Unlock() + obj.taskData[k] = v +} + +func (obj *PluginObject) GetSet(k string, fn func(old any) any) { + obj.taskDataLock.Lock() + defer obj.taskDataLock.Unlock() + obj.taskData[k] = fn(obj.taskData[k]) +} + +func (obj *PluginObject) Remove(k string) { + obj.taskDataLock.Lock() + defer obj.taskDataLock.Unlock() + delete(obj.taskData, k) +} + +func (obj *PluginObject) Push(k string, v any) { + obj.taskListLock.Lock() + defer obj.taskListLock.Unlock() + list1 := obj.taskList[k] + if list1 == nil { + list1 = list.New() + obj.taskList[k] = list1 + } + obj.taskList[k].PushBack(v) +} + +func (obj *PluginObject) Pop(k string) any { + obj.taskListLock.RLock() + list1 := obj.taskList[k] + obj.taskListLock.RUnlock() + if list1 == nil { + return nil + } + + obj.taskListLock.Lock() + defer obj.taskListLock.Unlock() + item := list1.Front() + if item == nil { + return nil + } + v := obj.taskList[k].Remove(item) + return v +} + +func (obj *PluginObject) CountList(k string) int { + obj.taskListLock.RLock() + defer obj.taskListLock.RUnlock() + list1 := obj.taskList[k] + if list1 == nil { + return 0 + } + return list1.Len() +} + +func (obj *PluginObject) RemoveList(k string) { + obj.taskListLock.Lock() + defer obj.taskListLock.Unlock() + delete(obj.taskList, k) +} diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..103e408 --- /dev/null +++ b/go.mod @@ -0,0 +1,32 @@ +module apigo.cc/gojs/task + +go 1.23.0 + +require ( + apigo.cc/gojs v0.0.17 + apigo.cc/gojs/console v0.0.2 + apigo.cc/gojs/file v0.0.4 + apigo.cc/gojs/runtime v0.0.3 + apigo.cc/gojs/util v0.0.11 + github.com/robfig/cron/v3 v3.0.1 + github.com/ssgo/log v1.7.7 + github.com/ssgo/u v1.7.20 +) + +require ( + github.com/ZZMarquis/gm v1.3.2 // 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-sourcemap/sourcemap v2.1.4+incompatible // indirect + github.com/google/pprof v0.0.0-20250630185457-6e76a2b096b5 // indirect + github.com/kr/text v0.2.0 // indirect + github.com/obscuren/ecies v0.0.0-20150213224233-7c0f4a9b18d9 // indirect + github.com/ssgo/config v1.7.9 // indirect + github.com/ssgo/standard v1.7.7 // indirect + github.com/ssgo/tool v0.4.29 // indirect + golang.org/x/crypto v0.40.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/plugin.go b/plugin.go new file mode 100644 index 0000000..e1d58bd --- /dev/null +++ b/plugin.go @@ -0,0 +1,48 @@ +package plugin + +import ( + "container/list" + "sync" + + "apigo.cc/gojs" + "apigo.cc/gojs/goja" + "github.com/robfig/cron/v3" +) + +const pluginName = "task" + +type PluginObject struct { + skipCron *cron.Cron + delayCron *cron.Cron + // asyncCron *cron.Cron + stopChan chan struct{} + isStarted bool + lock sync.RWMutex + tasks map[string]*Task + taskData map[string]any + taskDataLock sync.RWMutex + + taskList map[string]*list.List + taskListLock sync.RWMutex +} + +var _pluginObject = &PluginObject{ + tasks: map[string]*Task{}, + taskData: map[string]any{}, + taskList: map[string]*list.List{}, +} + +func init() { + tsCode := gojs.MakeTSCode(_pluginObject) + mappedObj := gojs.ToMap(_pluginObject) + gojs.Register("apigo.cc/gojs/"+pluginName, gojs.Module{ + ObjectMaker: func(vm *goja.Runtime) gojs.Map { + return mappedObj + }, + OnKill: func() { + _pluginObject.Stop() + }, + TsCode: tsCode, + Desc: "task api", + }) +} diff --git a/plugin_test.go b/plugin_test.go new file mode 100644 index 0000000..2988946 --- /dev/null +++ b/plugin_test.go @@ -0,0 +1,30 @@ +package plugin_test + +import ( + "fmt" + "strings" + "testing" + + "apigo.cc/gojs" + _ "apigo.cc/gojs/console" + _ "apigo.cc/gojs/file" + _ "apigo.cc/gojs/runtime" + _ "apigo.cc/gojs/util" + "github.com/ssgo/u" +) + +func TestPlugin(t *testing.T) { + gojs.ExportForDev() + for _, f := range u.ReadDirN(".") { + if strings.HasSuffix(f.Name, "_test.js") { + r, err := gojs.RunFile(f.Name) + if err != nil { + t.Fatal(u.Red(f.Name), u.BRed(err.Error())) + } else if r != true { + t.Fatal(u.Red(f.Name), u.BRed(u.JsonP(r))) + } else { + fmt.Println(u.Green(f.Name), u.BGreen("test succeess")) + } + } + } +} diff --git a/plugin_test.js b/plugin_test.js new file mode 100644 index 0000000..d8e0d5d --- /dev/null +++ b/plugin_test.js @@ -0,0 +1,44 @@ +import task from 'apigo.cc/gojs/task' +import rt from 'apigo.cc/gojs/runtime' +import co from 'apigo.cc/gojs/console' +import file from 'apigo.cc/gojs/file' +import u from 'apigo.cc/gojs/util' + +co.info('plugin test start') +try { + file.mkdir('testTasks/tmp') + task.asyncStart() + + for (let i = 0; i < 10; i++) { + let jsFileA = 'testTasks/tmp/a' + i + '.js' + let jsFileB = 'testTasks/tmp/b' + i + '.js' + file.copy('testTasks/a.js', jsFileA) + file.copy('testTasks/b.js', jsFileB) + task.newTask('@every 1s', jsFileA) + task.newTask('@every 1s', jsFileB) + } + + rt.sleep(3000) + task.stop() + + let aStarts = task.get('aStarts') + if (aStarts !== 10) return 'aStarts(' + aStarts + ') not 10' + let aStops = task.get('aStops') + if (aStops !== 10) return 'aStops(' + aStops + ') not 10' + let bStarts = task.get('bStarts') + if (bStarts !== 10) return 'bStarts(' + bStarts + ') not 10' + let bStops = task.get('bStops') + if (bStops !== 10) return 'bStops(' + bStops + ') not 10' + let aRunTimes = task.get('aRunTimes') + if (aRunTimes !== 30) return 'aRunTimes(' + aRunTimes + ') not 30' + + co.info() + + return true +} catch (ex) { + co.error(ex) + return false +} finally { + file.remove('testTasks/tmp') + co.info('plugin test end') +} diff --git a/task.go b/task.go new file mode 100644 index 0000000..986ce24 --- /dev/null +++ b/task.go @@ -0,0 +1,176 @@ +package plugin + +import ( + "fmt" + "sync" + "time" + + "apigo.cc/gojs" + "github.com/robfig/cron/v3" + "github.com/ssgo/log" + "github.com/ssgo/u" +) + +type Task struct { + file string + vm *gojs.Runtime + lock sync.RWMutex + mtime time.Time + entryId cron.EntryID + policy string +} + +func (obj *PluginObject) AsyncStart() { + obj.lock.Lock() + defer obj.lock.Unlock() + if !obj.isStarted { + obj.skipCron = cron.New(cron.WithSeconds(), cron.WithChain(cron.SkipIfStillRunning(cron.DefaultLogger))) + obj.skipCron.Start() + obj.delayCron = cron.New(cron.WithSeconds()) + obj.delayCron.Start() + // obj.asyncCron = cron.New(cron.WithSeconds()) + // obj.asyncCron.Start() + obj.isStarted = true + obj.stopChan = make(chan struct{}) + obj.skipCron.AddFunc("@every 5s", func() { + tasks := []*Task{} + obj.lock.RLock() + for _, task := range obj.tasks { + tasks = append(tasks, task) + } + obj.lock.RUnlock() + + for _, task := range tasks { + if fi := u.GetFileInfo(task.file); fi != nil { + if !fi.ModTime.Equal(task.mtime) { + // 文件变化,重新加载 + rt := gojs.New() + if _, err := rt.RunFile(task.file); err == nil { + task.lock.Lock() + task.vm = rt + task.mtime = fi.ModTime + task.lock.Unlock() + } else { + // 发生错误时仅更新 mtime,防止不断重新加载 + log.DefaultLogger.Error("failed to reload task code", "err", err.Error(), "file", task.file) + task.lock.Lock() + task.mtime = fi.ModTime + task.lock.Unlock() + } + } + } + } + }) + } +} + +func (obj *PluginObject) Start() { + obj.AsyncStart() + <-obj.stopChan +} + +func (obj *PluginObject) Stop() { + obj.lock.RLock() + isStarted := obj.isStarted + obj.lock.RUnlock() + if !isStarted { + return + } + defer close(obj.stopChan) + ctx1 := obj.skipCron.Stop() + ctx2 := obj.delayCron.Stop() + // ctx3 := obj.asyncCron.Stop() + <-ctx1.Done() + <-ctx2.Done() + // <-ctx3.Done() + + obj.lock.Lock() + defer obj.lock.Unlock() + for _, task := range obj.tasks { + task.RunFunc("onStop") + } + + obj.isStarted = false + obj.tasks = map[string]*Task{} +} + +func (obj *PluginObject) NewTask(spec string, file string, policy *string) error { + fi := u.GetFileInfo(file) + if fi == nil { + return u.Error("task file not exists") + } + + rt := gojs.New() + if _, err := rt.RunFile(file); err != nil { + return u.Error(err.Error()) + } + + task := &Task{ + file: file, + vm: rt, + mtime: fi.ModTime, + policy: u.String(policy), + } + if err := task.RunFunc("onStart"); err != nil { + return u.Error(err.Error()) + } + + var err error + switch task.policy { + case "skip": + task.entryId, err = obj.skipCron.AddFunc(spec, func() { + task.RunFunc("onRun") + }) + // case "async": + // task.entryId, err = obj.asyncCron.AddFunc(spec, func() { + // go func() { + // task.RunFunc("onRun") + // }() + // }) + default: + task.policy = "delay" + task.entryId, err = obj.delayCron.AddFunc(spec, func() { + task.RunFunc("onRun") + }) + } + + if err != nil { + return u.Error(err.Error()) + } + + obj.lock.Lock() + defer obj.lock.Unlock() + obj.tasks[spec+file] = task + return nil +} + +func (obj *PluginObject) RemoveTask(spec string, file string) error { + obj.lock.RLock() + task := obj.tasks[spec+file] + obj.lock.RUnlock() + if task == nil { + return nil + } + task.RunFunc("onStop") + switch task.policy { + case "skip": + obj.skipCron.Remove(task.entryId) + // case "async": + // obj.asyncCron.Remove(task.entryId) + default: + obj.delayCron.Remove(task.entryId) + } + + obj.lock.Lock() + defer obj.lock.Unlock() + delete(obj.tasks, spec+file) + return nil +} + +func (task *Task) RunFunc(fnName string) error { + task.lock.RLock() + rt := task.vm + task.lock.RUnlock() + _, err := rt.RunCode(fmt.Sprintf("if(%s)%s()", fnName, fnName)) + return err +} diff --git a/testTasks/a.js b/testTasks/a.js new file mode 100644 index 0000000..f22b75a --- /dev/null +++ b/testTasks/a.js @@ -0,0 +1,34 @@ +import rt from 'apigo.cc/gojs/runtime' +import co from 'apigo.cc/gojs/console' +import task from 'apigo.cc/gojs/task' +import u from 'apigo.cc/gojs/util' + +const taskName = '「a」' +let runTimes = 0 +// let t1 + +function onStart() { + // co.info(taskName, 'start') + task.getSet('aStarts', old => { + return u.int(old) + 1 + }) + // t1 = u.timestampMS() +} + +let i = 0 +function onRun() { + // co.info(taskName, 'run======================', runTimes, u.timestampMS() - t1) + runTimes++ + task.push('aRunTime', runTimes) + task.getSet('aRunTimes', old => { + return u.int(old) + 1 + }) + // rt.sleep(1500) +} + +function onStop() { + // co.info(taskName, 'stop') + task.getSet('aStops', old => { + return u.int(old) + 1 + }) +} diff --git a/testTasks/b.js b/testTasks/b.js new file mode 100644 index 0000000..a318d50 --- /dev/null +++ b/testTasks/b.js @@ -0,0 +1,33 @@ +import rt from 'apigo.cc/gojs/runtime' +import co from 'apigo.cc/gojs/console' +import task from 'apigo.cc/gojs/task' +import u from 'apigo.cc/gojs/util' + +const taskName = '「b」' +let runTimes = 0 + +function onStart() { + // co.info(taskName, 'start') + task.getSet('bStarts', old => { + return u.int(old) + 1 + }) +} + +let i = 0 +function onRun() { + runTimes++ + // co.info(taskName, 'run start', runTimes) + rt.sleep(200) + let v = task.pop('aRunTime') + task.getSet('Count' + u.string(v), old => { + return u.int(old) + 1 + }) + // co.info(taskName, 'run end', runTimes) +} + +function onStop() { + // co.info(taskName, 'stop') + task.getSet('bStops', old => { + return u.int(old) + 1 + }) +}