package plugins import ( "fmt" "log" "sync" "time" ) // Task represents a single background task instance. type Task struct { ID string Interval time.Duration Script string // relative path to .lua file (or "hook:name" for a Lua function) IsHook bool // if true, Script is a function name to call via CallHook stopCh chan struct{} stopped bool } // Scheduler manages background tasks for a plugin. type Scheduler struct { plugin *Plugin vm *LuaVM tasks []*Task mu sync.Mutex wg sync.WaitGroup } // NewScheduler creates a scheduler for a plugin. func NewScheduler(p *Plugin, vm *LuaVM) *Scheduler { return &Scheduler{ plugin: p, vm: vm, } } // AddTask adds a task from a BackgroundTask definition. func (s *Scheduler) AddTask(bg BackgroundTask) error { dur, err := parseDuration(bg.Interval) if err != nil { return fmt.Errorf("task %s: %w", bg.ID, err) } s.mu.Lock() defer s.mu.Unlock() task := &Task{ ID: bg.ID, Interval: dur, Script: bg.Script, stopCh: make(chan struct{}), } s.tasks = append(s.tasks, task) return nil } // Start begins all registered tasks. func (s *Scheduler) Start() { s.mu.Lock() defer s.mu.Unlock() for _, t := range s.tasks { if t.stopped { continue } s.wg.Add(1) go s.runTask(t) } } // Stop cancels all running tasks and waits for them to finish. func (s *Scheduler) Stop() { s.mu.Lock() for _, t := range s.tasks { if !t.stopped { close(t.stopCh) t.stopped = true } } tasks := s.tasks s.tasks = nil s.mu.Unlock() s.wg.Wait() _ = tasks // keep reference until wg done } func (s *Scheduler) runTask(t *Task) { defer s.wg.Done() ticker := time.NewTicker(t.Interval) defer ticker.Stop() for { select { case <-ticker.C: s.executeTask(t) case <-t.stopCh: return } } } func (s *Scheduler) executeTask(t *Task) { if s.vm == nil { return } if t.IsHook { if err := s.vm.CallHook(t.Script); err != nil { log.Printf("[plugins] task %s/%s hook error: %v", s.plugin.Meta.Name, t.ID, err) } return } if err := s.vm.LoadScript(t.Script); err != nil { log.Printf("[plugins] task %s/%s script error: %v", s.plugin.Meta.Name, t.ID, err) } } // parseDuration parses a human-readable interval like "5m", "1h", "30s". func parseDuration(s string) (time.Duration, error) { d, err := time.ParseDuration(s) if err == nil { return d, nil } // Try cron-like or other formats later return 0, fmt.Errorf("invalid interval %q: use Go duration format (e.g. 5m, 1h, 30s)", s) }