package manager import ( "context" "fmt" "sync" "time" "git.yeij.top/AskaEth/Cyrene-Plugins/sdk" ) // PluginManager manages the lifecycle of all plugins and their tools. type PluginManager struct { mu sync.RWMutex plugins map[string]*pluginEntry registry *ToolRegistry host sdk.HostAPI } type pluginEntry struct { instance sdk.Plugin info sdk.PluginInfo cancel context.CancelFunc } func NewPluginManager(registry *ToolRegistry, host sdk.HostAPI) *PluginManager { return &PluginManager{ plugins: make(map[string]*pluginEntry), registry: registry, host: host, } } // Install registers a plugin instance. func (m *PluginManager) Install(plugin sdk.Plugin) error { meta := plugin.Metadata() m.mu.Lock() defer m.mu.Unlock() if _, exists := m.plugins[meta.Name]; exists { return fmt.Errorf("plugin %q is already installed", meta.Name) } m.plugins[meta.Name] = &pluginEntry{ instance: plugin, info: sdk.PluginInfo{ Metadata: meta, Status: sdk.StatusInstalled, InstalledAt: time.Now(), Enabled: false, }, } return nil } // Enable activates a plugin: Init → register tools → Start. func (m *PluginManager) Enable(ctx context.Context, pluginName string) error { m.mu.Lock() entry, ok := m.plugins[pluginName] m.mu.Unlock() if !ok { return fmt.Errorf("plugin %q not found", pluginName) } m.mu.Lock() entry.info.Status = sdk.StatusLoaded m.mu.Unlock() meta := entry.instance.Metadata() if err := entry.instance.Init(ctx, nil); err != nil { m.mu.Lock() entry.info.Status = sdk.StatusError m.mu.Unlock() return fmt.Errorf("plugin %q init failed: %w", meta.Name, err) } pluginCtx, cancel := context.WithCancel(context.Background()) if err := entry.instance.Start(pluginCtx, m.host); err != nil { cancel() m.mu.Lock() entry.info.Status = sdk.StatusError m.mu.Unlock() return fmt.Errorf("plugin %q start failed: %w", meta.Name, err) } tools := entry.instance.Tools() toolIDs := make([]string, 0, len(tools)) for _, t := range tools { if err := m.registry.Register(t); err != nil { m.registry.UnregisterAll(toolIDs) cancel() m.mu.Lock() entry.info.Status = sdk.StatusError m.mu.Unlock() return fmt.Errorf("plugin %q tool register failed: %w", meta.Name, err) } toolIDs = append(toolIDs, t.Definition().ID) } m.mu.Lock() entry.cancel = cancel entry.info.Status = sdk.StatusRunning entry.info.Enabled = true entry.info.Tools = toolIDs m.mu.Unlock() return nil } // Disable stops a plugin and unregisters its tools. func (m *PluginManager) Disable(ctx context.Context, pluginName string) error { m.mu.Lock() entry, ok := m.plugins[pluginName] m.mu.Unlock() if !ok { return fmt.Errorf("plugin %q not found", pluginName) } if err := entry.instance.Stop(ctx); err != nil { return fmt.Errorf("plugin %q stop failed: %w", pluginName, err) } if entry.cancel != nil { entry.cancel() } m.registry.UnregisterAll(entry.info.Tools) m.mu.Lock() entry.info.Status = sdk.StatusDisabled entry.info.Enabled = false entry.info.Tools = nil m.mu.Unlock() return nil } // List returns info for all installed plugins. func (m *PluginManager) List() []sdk.PluginInfo { m.mu.RLock() defer m.mu.RUnlock() result := make([]sdk.PluginInfo, 0, len(m.plugins)) for _, entry := range m.plugins { result = append(result, entry.info) } return result } // Get returns info for a single plugin. func (m *PluginManager) Get(pluginName string) (*sdk.PluginInfo, bool) { m.mu.RLock() defer m.mu.RUnlock() entry, ok := m.plugins[pluginName] if !ok { return nil, false } info := entry.info return &info, true } // EnableAll starts all installed plugins. func (m *PluginManager) EnableAll(ctx context.Context) []error { m.mu.RLock() names := make([]string, 0, len(m.plugins)) for name := range m.plugins { names = append(names, name) } m.mu.RUnlock() var errs []error for _, name := range names { if err := m.Enable(ctx, name); err != nil { errs = append(errs, fmt.Errorf("%s: %w", name, err)) } } return errs } // Uninstall removes a plugin completely. func (m *PluginManager) Uninstall(ctx context.Context, pluginName string) error { m.mu.RLock() entry, ok := m.plugins[pluginName] m.mu.RUnlock() if !ok { return fmt.Errorf("plugin %q not found", pluginName) } if entry.info.Status == sdk.StatusRunning { if err := m.Disable(ctx, pluginName); err != nil { return err } } m.mu.Lock() defer m.mu.Unlock() delete(m.plugins, pluginName) return nil } // Reload stops and re-starts a plugin. func (m *PluginManager) Reload(ctx context.Context, pluginName string) error { if err := m.Disable(ctx, pluginName); err != nil { return fmt.Errorf("reload disable: %w", err) } return m.Enable(ctx, pluginName) } // Shutdown stops all running plugins gracefully. func (m *PluginManager) Shutdown(ctx context.Context) []error { m.mu.RLock() names := make([]string, 0, len(m.plugins)) for name, entry := range m.plugins { if entry.info.Status == sdk.StatusRunning { names = append(names, name) } } m.mu.RUnlock() var errs []error for _, name := range names { if err := m.Disable(ctx, name); err != nil { errs = append(errs, err) } } return errs } // Registry returns the aggregated tool registry. func (m *PluginManager) Registry() *ToolRegistry { return m.registry }