2026-03-05 00:04:26 -06:00
|
|
|
package plugins
|
|
|
|
|
|
|
|
|
|
import (
|
|
|
|
|
"context"
|
2026-03-19 16:29:26 -05:00
|
|
|
"encoding/json"
|
2026-03-05 00:04:26 -06:00
|
|
|
"errors"
|
|
|
|
|
"log"
|
|
|
|
|
"sync"
|
2026-05-04 19:56:36 -05:00
|
|
|
"time"
|
2026-03-05 00:04:26 -06:00
|
|
|
|
|
|
|
|
"overlord-client/cmd/agent/wire"
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
type Manager struct {
|
|
|
|
|
mu sync.Mutex
|
|
|
|
|
plugins map[string]*pluginInstance
|
|
|
|
|
writer wire.Writer
|
|
|
|
|
host HostInfo
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
type pluginInstance struct {
|
|
|
|
|
id string
|
|
|
|
|
manifest PluginManifest
|
2026-06-22 10:52:27 -05:00
|
|
|
native PluginRuntime
|
2026-03-05 00:04:26 -06:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func NewManager(writer wire.Writer, host HostInfo) *Manager {
|
|
|
|
|
return &Manager{
|
|
|
|
|
plugins: make(map[string]*pluginInstance),
|
|
|
|
|
writer: writer,
|
|
|
|
|
host: host,
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-03-19 16:29:26 -05:00
|
|
|
func (m *Manager) Load(ctx context.Context, manifest PluginManifest, binary []byte) error {
|
2026-04-20 13:46:59 -05:00
|
|
|
//garble:controlflow block_splits=max junk_jumps=max flatten_passes=max
|
2026-03-19 16:29:26 -05:00
|
|
|
if len(binary) == 0 {
|
|
|
|
|
return errors.New("empty plugin binary")
|
2026-03-05 00:04:26 -06:00
|
|
|
}
|
|
|
|
|
pluginID := manifest.ID
|
|
|
|
|
if pluginID == "" {
|
|
|
|
|
return errors.New("missing plugin id")
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
m.mu.Lock()
|
|
|
|
|
if existing, ok := m.plugins[pluginID]; ok {
|
2026-03-19 16:29:26 -05:00
|
|
|
existing.native.Close()
|
2026-03-05 00:04:26 -06:00
|
|
|
delete(m.plugins, pluginID)
|
|
|
|
|
}
|
|
|
|
|
m.mu.Unlock()
|
|
|
|
|
|
2026-06-22 10:52:27 -05:00
|
|
|
var np PluginRuntime
|
|
|
|
|
var err error
|
|
|
|
|
if manifest.RuntimeKind == "wasm" || manifest.WASM != "" {
|
|
|
|
|
np, err = loadWASMPlugin(ctx, manifest, binary)
|
|
|
|
|
} else {
|
2026-06-28 23:46:56 -05:00
|
|
|
np, err = loadNativePlugin(manifest, binary)
|
2026-06-22 10:52:27 -05:00
|
|
|
}
|
2026-03-05 00:04:26 -06:00
|
|
|
if err != nil {
|
|
|
|
|
return err
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
pi := &pluginInstance{
|
|
|
|
|
id: pluginID,
|
|
|
|
|
manifest: manifest,
|
2026-03-19 16:29:26 -05:00
|
|
|
native: np,
|
2026-03-05 00:04:26 -06:00
|
|
|
}
|
|
|
|
|
|
2026-03-19 16:29:26 -05:00
|
|
|
send := func(event string, payload []byte) {
|
|
|
|
|
var payloadVal interface{}
|
|
|
|
|
if len(payload) > 0 {
|
|
|
|
|
var parsed interface{}
|
|
|
|
|
if json.Unmarshal(payload, &parsed) == nil {
|
|
|
|
|
payloadVal = parsed
|
|
|
|
|
} else {
|
|
|
|
|
payloadVal = string(payload)
|
2026-03-05 00:04:26 -06:00
|
|
|
}
|
2026-03-19 16:29:26 -05:00
|
|
|
}
|
2026-05-04 19:56:36 -05:00
|
|
|
msg := wire.PluginEvent{
|
2026-03-19 16:29:26 -05:00
|
|
|
Type: "plugin_event",
|
|
|
|
|
PluginID: pluginID,
|
|
|
|
|
Event: event,
|
|
|
|
|
Payload: payloadVal,
|
|
|
|
|
}
|
2026-05-04 19:56:36 -05:00
|
|
|
go func() {
|
2026-05-07 19:41:47 -05:00
|
|
|
defer func() {
|
|
|
|
|
if r := recover(); r != nil {
|
|
|
|
|
log.Printf("[panic] plugin event send: %v", r)
|
|
|
|
|
}
|
|
|
|
|
}()
|
2026-05-04 19:56:36 -05:00
|
|
|
sendCtx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
|
|
|
|
|
defer cancel()
|
|
|
|
|
if err := wire.WriteMsg(sendCtx, m.writer, msg); err != nil {
|
|
|
|
|
log.Printf("[plugin] %s send event error: %v", pluginID, err)
|
|
|
|
|
}
|
|
|
|
|
}()
|
2026-03-05 00:04:26 -06:00
|
|
|
}
|
|
|
|
|
|
2026-03-19 16:29:26 -05:00
|
|
|
hostJSON, err := json.Marshal(m.host)
|
|
|
|
|
if err != nil {
|
|
|
|
|
np.Close()
|
|
|
|
|
return err
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if err := np.Load(send, hostJSON); err != nil {
|
|
|
|
|
np.Close()
|
2026-03-05 00:04:26 -06:00
|
|
|
return err
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
m.mu.Lock()
|
|
|
|
|
m.plugins[pluginID] = pi
|
|
|
|
|
m.mu.Unlock()
|
|
|
|
|
|
2026-04-04 20:06:23 -05:00
|
|
|
rt := np.Runtime()
|
|
|
|
|
freeable := rt != "go"
|
2026-06-22 10:52:27 -05:00
|
|
|
log.Printf("[plugin] loaded %s (runtime=%s, freeable=%v)", pluginID, rt, freeable)
|
2026-03-05 00:04:26 -06:00
|
|
|
return nil
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func (m *Manager) Dispatch(ctx context.Context, pluginId, event string, payload interface{}) error {
|
|
|
|
|
m.mu.Lock()
|
|
|
|
|
pi := m.plugins[pluginId]
|
|
|
|
|
m.mu.Unlock()
|
|
|
|
|
if pi == nil {
|
|
|
|
|
return nil
|
|
|
|
|
}
|
2026-03-19 16:29:26 -05:00
|
|
|
|
|
|
|
|
data, err := json.Marshal(payload)
|
|
|
|
|
if err != nil {
|
|
|
|
|
return err
|
|
|
|
|
}
|
|
|
|
|
return pi.native.Event(event, data)
|
2026-03-05 00:04:26 -06:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func (m *Manager) Close() {
|
|
|
|
|
m.mu.Lock()
|
|
|
|
|
defer m.mu.Unlock()
|
|
|
|
|
for id, pi := range m.plugins {
|
2026-04-04 20:06:23 -05:00
|
|
|
rt := pi.native.Runtime()
|
2026-03-19 16:29:26 -05:00
|
|
|
pi.native.Close()
|
2026-03-05 00:04:26 -06:00
|
|
|
delete(m.plugins, id)
|
2026-04-04 20:06:23 -05:00
|
|
|
if rt != "go" {
|
|
|
|
|
log.Printf("[plugin] unloaded %s (runtime=%s, memory freed)", id, rt)
|
|
|
|
|
} else {
|
|
|
|
|
log.Printf("[plugin] unloaded %s (runtime=go, memory leaked — see golang/go#11100)", id)
|
|
|
|
|
}
|
2026-03-05 00:04:26 -06:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func (m *Manager) Unload(pluginId string) {
|
|
|
|
|
m.mu.Lock()
|
|
|
|
|
defer m.mu.Unlock()
|
|
|
|
|
if pi, ok := m.plugins[pluginId]; ok {
|
2026-04-04 20:06:23 -05:00
|
|
|
rt := pi.native.Runtime()
|
2026-03-19 16:29:26 -05:00
|
|
|
pi.native.Close()
|
2026-03-05 00:04:26 -06:00
|
|
|
delete(m.plugins, pluginId)
|
2026-04-04 20:06:23 -05:00
|
|
|
if rt != "go" {
|
|
|
|
|
log.Printf("[plugin] unloaded %s (runtime=%s, memory freed)", pluginId, rt)
|
|
|
|
|
} else {
|
|
|
|
|
log.Printf("[plugin] unloaded %s (runtime=go, memory leaked — see golang/go#11100)", pluginId)
|
|
|
|
|
}
|
2026-03-05 00:04:26 -06:00
|
|
|
}
|
|
|
|
|
}
|