chore: import upstream snapshot with attribution
This commit is contained in:
@@ -0,0 +1,182 @@
|
||||
package luaplugin
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"log"
|
||||
"time"
|
||||
|
||||
lua "github.com/yuin/gopher-lua"
|
||||
)
|
||||
|
||||
const hookTimeout = 5 * time.Second
|
||||
|
||||
// Event name constants.
|
||||
const (
|
||||
EventAppStart = "app.start"
|
||||
EventAppQuit = "app.quit"
|
||||
EventPlaybackState = "playback.state"
|
||||
EventTrackChange = "track.change"
|
||||
EventTrackScrobble = "track.scrobble"
|
||||
EventPlayerSeek = "player.seek" // data: position, duration (seconds)
|
||||
EventPlayerVolume = "player.volume" // data: db
|
||||
EventPlayerEQ = "player.eq" // data: bands (10-array), preset
|
||||
EventPlayerMode = "player.mode" // data: shuffle (bool), repeat ("Off"/"All"/"One")
|
||||
EventQueueChange = "queue.change" // data: count, index, queued
|
||||
)
|
||||
|
||||
// Permission strings declared via plugin.register({ permissions = {...} }).
|
||||
// Kept as named constants so the guard call sites and docs don't drift.
|
||||
const (
|
||||
PermControl = "control"
|
||||
PermExec = "exec"
|
||||
PermKeymap = "keymap"
|
||||
)
|
||||
|
||||
// luaHook is a single event callback registered by a plugin.
|
||||
type luaHook struct {
|
||||
plugin *Plugin
|
||||
fn *lua.LFunction
|
||||
}
|
||||
|
||||
// callBounded runs fn on the plugin's LState under hookTimeout so a runaway
|
||||
// callback cannot hold the plugin mutex forever. The caller must already hold
|
||||
// p.mu. Results (if nret > 0) are left on the stack for the caller to read.
|
||||
func (p *Plugin) callBounded(nret int, fn *lua.LFunction, args ...lua.LValue) error {
|
||||
ctx, cancel := context.WithTimeout(context.Background(), hookTimeout)
|
||||
defer cancel()
|
||||
p.L.SetContext(ctx)
|
||||
defer p.L.RemoveContext()
|
||||
return p.L.CallByParam(lua.P{Fn: fn, NRet: nret, Protect: true}, args...)
|
||||
}
|
||||
|
||||
// logHookErr records a callback error to stderr and the plugin log.
|
||||
func (m *Manager) logHookErr(name, label string, err error) {
|
||||
log.Printf("[lua:%s] %s error: %v", name, label, err)
|
||||
if m.logger != nil {
|
||||
m.logger.log(name, "error", "%s error: %v", label, err)
|
||||
}
|
||||
}
|
||||
|
||||
// invokeHook calls a plugin's Lua callback under the plugin's mutex with a
|
||||
// bounded context. Logs any error to the plugin log. Used by every dispatch
|
||||
// site that fires Lua from Go (events, key binds, command handlers).
|
||||
func (m *Manager) invokeHook(h *luaHook, label string, args ...lua.LValue) {
|
||||
h.plugin.mu.Lock()
|
||||
defer h.plugin.mu.Unlock()
|
||||
|
||||
if err := h.plugin.callBounded(0, h.fn, args...); err != nil {
|
||||
m.logHookErr(h.plugin.Name, label, err)
|
||||
}
|
||||
}
|
||||
|
||||
// filterOutPlugin returns hooks with all entries owned by p removed. Reuses
|
||||
// the existing backing slice and zeroes the tail so dropped LFunction pointers
|
||||
// become garbage-collectible.
|
||||
func filterOutPlugin(hooks []*luaHook, p *Plugin) []*luaHook {
|
||||
filtered := hooks[:0]
|
||||
for _, h := range hooks {
|
||||
if h.plugin != p {
|
||||
filtered = append(filtered, h)
|
||||
}
|
||||
}
|
||||
for i := len(filtered); i < len(hooks); i++ {
|
||||
hooks[i] = nil
|
||||
}
|
||||
return filtered
|
||||
}
|
||||
|
||||
// Emit dispatches an event to all plugins that registered for it.
|
||||
// Each callback runs in its own goroutine with a timeout. The plugin's
|
||||
// mutex serializes all LState access so concurrent events are safe.
|
||||
func (m *Manager) Emit(event string, data map[string]any) {
|
||||
m.mu.RLock()
|
||||
defer m.mu.RUnlock()
|
||||
if m.closing {
|
||||
return
|
||||
}
|
||||
hooks := m.hooks[event]
|
||||
label := event + " handler"
|
||||
for _, h := range hooks {
|
||||
// Add under RLock so Close (which sets closing under Lock, then Wait)
|
||||
// can never miss an in-flight goroutine: either we register it before
|
||||
// closing is set, or we observe closing and skip.
|
||||
m.wg.Add(1)
|
||||
go func(h *luaHook) {
|
||||
defer m.wg.Done()
|
||||
m.invokeHookWithData(h, label, data)
|
||||
}(h)
|
||||
}
|
||||
}
|
||||
|
||||
// invokeHookWithData is Emit's per-hook goroutine: builds the arg table on the
|
||||
// plugin's LState (which requires holding plugin.mu) and then fires the
|
||||
// callback under the same lock via invokeHook's contract.
|
||||
func (m *Manager) invokeHookWithData(h *luaHook, label string, data map[string]any) {
|
||||
h.plugin.mu.Lock()
|
||||
defer h.plugin.mu.Unlock()
|
||||
|
||||
arg := dataToTable(h.plugin.L, data)
|
||||
if err := h.plugin.callBounded(0, h.fn, arg); err != nil {
|
||||
m.logHookErr(h.plugin.Name, label, err)
|
||||
}
|
||||
}
|
||||
|
||||
// EmitSync dispatches an event synchronously, blocking until all callbacks finish.
|
||||
// Used during shutdown to ensure all handlers complete before LStates are closed.
|
||||
func (m *Manager) EmitSync(event string, data map[string]any) {
|
||||
m.mu.RLock()
|
||||
hooks := m.hooks[event]
|
||||
m.mu.RUnlock()
|
||||
|
||||
for _, h := range hooks {
|
||||
h.plugin.mu.Lock()
|
||||
arg := dataToTable(h.plugin.L, data)
|
||||
_ = h.plugin.L.CallByParam(lua.P{
|
||||
Fn: h.fn,
|
||||
NRet: 0,
|
||||
Protect: true,
|
||||
}, arg)
|
||||
h.plugin.mu.Unlock()
|
||||
}
|
||||
}
|
||||
|
||||
// dataToTable converts a Go map to a Lua table.
|
||||
func dataToTable(L *lua.LState, data map[string]any) *lua.LTable {
|
||||
tbl := L.NewTable()
|
||||
if data == nil {
|
||||
return tbl
|
||||
}
|
||||
for k, v := range data {
|
||||
tbl.RawSetString(k, goToLua(L, v))
|
||||
}
|
||||
return tbl
|
||||
}
|
||||
|
||||
// goToLua converts a Go value to a Lua value.
|
||||
func goToLua(L *lua.LState, v any) lua.LValue {
|
||||
switch val := v.(type) {
|
||||
case nil:
|
||||
return lua.LNil
|
||||
case string:
|
||||
return lua.LString(val)
|
||||
case int:
|
||||
return lua.LNumber(val)
|
||||
case int64:
|
||||
return lua.LNumber(val)
|
||||
case float64:
|
||||
return lua.LNumber(val)
|
||||
case bool:
|
||||
return lua.LBool(val)
|
||||
case map[string]any:
|
||||
return dataToTable(L, val)
|
||||
case []float64:
|
||||
tbl := L.NewTable()
|
||||
for i, f := range val {
|
||||
tbl.RawSetInt(i+1, lua.LNumber(f))
|
||||
}
|
||||
return tbl
|
||||
default:
|
||||
return lua.LString(fmt.Sprintf("%v", val))
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user