This commit is contained in:
Star 2025-07-18 15:27:22 +08:00
commit 70d5483fe1
11 changed files with 486 additions and 0 deletions

6
.gitignore vendored Normal file
View File

@ -0,0 +1,6 @@
.*
!.gitignore
go.sum
node_modules
package.json
env.yml

9
LICENSE Normal file
View File

@ -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.

BIN
build/task_darwin_amd64 Normal file

Binary file not shown.

74
data.go Normal file
View File

@ -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)
}

32
go.mod Normal file
View File

@ -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
)

48
plugin.go Normal file
View File

@ -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",
})
}

30
plugin_test.go Normal file
View File

@ -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"))
}
}
}
}

44
plugin_test.js Normal file
View File

@ -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')
}

176
task.go Normal file
View File

@ -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
}

34
testTasks/a.js Normal file
View File

@ -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
})
}

33
testTasks/b.js Normal file
View File

@ -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
})
}