package luaplugin import ( "sync" "sync/atomic" "time" lua "github.com/yuin/gopher-lua" ) type timerEntry struct { id int64 plugin *Plugin ticker *time.Ticker timer *time.Timer done chan struct{} } type timerManager struct { mu sync.Mutex timers map[int64]*timerEntry nextID atomic.Int64 } func newTimerManager() *timerManager { return &timerManager{ timers: make(map[int64]*timerEntry), } } func (tm *timerManager) add(e *timerEntry) { tm.mu.Lock() tm.timers[e.id] = e tm.mu.Unlock() } func (tm *timerManager) cancel(id int64) { tm.mu.Lock() e, ok := tm.timers[id] if ok { close(e.done) if e.ticker != nil { e.ticker.Stop() } if e.timer != nil { e.timer.Stop() } delete(tm.timers, id) } tm.mu.Unlock() } func (tm *timerManager) stopAll() { tm.mu.Lock() for id, e := range tm.timers { close(e.done) if e.ticker != nil { e.ticker.Stop() } if e.timer != nil { e.timer.Stop() } delete(tm.timers, id) } tm.mu.Unlock() } func (tm *timerManager) stopPlugin(p *Plugin) { tm.mu.Lock() for id, e := range tm.timers { if e.plugin != p { continue } close(e.done) if e.ticker != nil { e.ticker.Stop() } if e.timer != nil { e.timer.Stop() } delete(tm.timers, id) } tm.mu.Unlock() } func (tm *timerManager) take(id int64) bool { tm.mu.Lock() defer tm.mu.Unlock() if _, ok := tm.timers[id]; !ok { return false } delete(tm.timers, id) return true } func (tm *timerManager) active(id int64) bool { tm.mu.Lock() defer tm.mu.Unlock() _, ok := tm.timers[id] return ok } // registerTimerAPI adds cliamp.timer.{after,every,cancel} to the cliamp table. // p is the owning plugin whose mutex protects all LState calls. func registerTimerAPI(L *lua.LState, cliamp *lua.LTable, tm *timerManager, p *Plugin) { tbl := L.NewTable() // cliamp.timer.after(secs, callback) -> id L.SetField(tbl, "after", L.NewFunction(func(L *lua.LState) int { secs := L.CheckNumber(1) fn := L.CheckFunction(2) id := tm.nextID.Add(1) d := time.Duration(float64(secs) * float64(time.Second)) t := time.NewTimer(d) e := &timerEntry{id: id, plugin: p, timer: t, done: make(chan struct{})} tm.add(e) go func() { select { case <-t.C: p.mu.Lock() if !tm.take(id) { p.mu.Unlock() return } _ = p.callBounded(0, fn) p.mu.Unlock() case <-e.done: } }() L.Push(lua.LNumber(id)) return 1 })) // cliamp.timer.every(secs, callback) -> id L.SetField(tbl, "every", L.NewFunction(func(L *lua.LState) int { secs := L.CheckNumber(1) fn := L.CheckFunction(2) id := tm.nextID.Add(1) d := time.Duration(float64(secs) * float64(time.Second)) ticker := time.NewTicker(d) e := &timerEntry{id: id, plugin: p, ticker: ticker, done: make(chan struct{})} tm.add(e) go func() { for { select { case <-ticker.C: p.mu.Lock() if !tm.active(id) { p.mu.Unlock() return } _ = p.callBounded(0, fn) p.mu.Unlock() case <-e.done: return } } }() L.Push(lua.LNumber(id)) return 1 })) // cliamp.timer.cancel(id) L.SetField(tbl, "cancel", L.NewFunction(func(L *lua.LState) int { id := L.CheckInt64(1) tm.cancel(id) return 0 })) L.SetField(cliamp, "timer", tbl) } // registerSleepAPI adds cliamp.sleep(secs) — a blocking sleep. // Note: this blocks the plugin's Lua VM, so other hooks for the same // plugin will be queued until the sleep completes. Max 10 seconds. func registerSleepAPI(L *lua.LState, cliamp *lua.LTable) { L.SetField(cliamp, "sleep", L.NewFunction(func(L *lua.LState) int { secs := float64(L.CheckNumber(1)) if secs > 0 && secs <= 10 { time.Sleep(time.Duration(secs * float64(time.Second))) } return 0 })) }