mirror of
https://github.com/matt-dunleavy/plugin-manager.git
synced 2025-09-27 04:56:48 +08:00

This commit introduces significant improvements and new features to the plugin manager: - Add plugin discovery system and remote repository support - Implement plugin update system with digital signature verification - Enhance plugin lifecycle hooks (PreLoad, PostLoad, PreUnload) - Improve dependency management with custom version comparison - Introduce lazy loading for optimized plugin performance - Implement comprehensive error handling and logging - Enhance concurrency safety with fine-grained locking - Add plugin statistics tracking - Remove external version comparison dependencies - Improve hot-reload functionality with graceful shutdown - Add SSH key support for remote repositories - Implement Redbean server integration for plugin repositories This update significantly improves the plugin manager's functionality, security, and performance, providing a more robust and flexible system for managing plugins in Go applications.
362 lines
9.7 KiB
Go
362 lines
9.7 KiB
Go
// FILE: manager.go
|
|
|
|
package pluginmanager
|
|
|
|
import (
|
|
"fmt"
|
|
"path/filepath"
|
|
"plugin"
|
|
"strconv"
|
|
"strings"
|
|
"sync"
|
|
"time"
|
|
|
|
"go.uber.org/zap"
|
|
)
|
|
|
|
type Manager struct {
|
|
plugins map[string]*lazyPlugin
|
|
config *Config
|
|
dependencies map[string][]string
|
|
stats map[string]*PluginStats
|
|
eventBus *EventBus
|
|
sandbox Sandbox
|
|
logger *zap.Logger
|
|
publicKeyPath string
|
|
mu sync.RWMutex
|
|
}
|
|
|
|
type lazyPlugin struct {
|
|
path string
|
|
loaded Plugin
|
|
}
|
|
|
|
func (lp *lazyPlugin) load() error {
|
|
if lp.loaded == nil {
|
|
p, err := plugin.Open(lp.path)
|
|
if err != nil {
|
|
return fmt.Errorf("failed to open plugin: %w", err)
|
|
}
|
|
|
|
symPlugin, err := p.Lookup(PluginSymbol)
|
|
if err != nil {
|
|
return fmt.Errorf("failed to lookup plugin symbol: %w", err)
|
|
}
|
|
|
|
plugin, ok := symPlugin.(Plugin)
|
|
if !ok {
|
|
return fmt.Errorf("invalid plugin interface")
|
|
}
|
|
|
|
lp.loaded = plugin
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func NewManager(configPath, pluginDir, publicKeyPath string) (*Manager, error) {
|
|
config, err := LoadConfig(configPath)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to load config: %w", err)
|
|
}
|
|
|
|
logger, _ := zap.NewProduction()
|
|
|
|
sandboxDir := filepath.Join(pluginDir, "sandbox")
|
|
|
|
return &Manager{
|
|
plugins: make(map[string]*lazyPlugin),
|
|
config: config,
|
|
dependencies: make(map[string][]string),
|
|
stats: make(map[string]*PluginStats),
|
|
eventBus: NewEventBus(),
|
|
sandbox: NewLinuxSandbox(sandboxDir),
|
|
logger: logger,
|
|
publicKeyPath: publicKeyPath,
|
|
}, nil
|
|
}
|
|
|
|
func (m *Manager) LoadPlugin(path string) error {
|
|
m.mu.Lock()
|
|
defer m.mu.Unlock()
|
|
|
|
pluginName := filepath.Base(path)
|
|
if _, exists := m.plugins[pluginName]; exists {
|
|
return fmt.Errorf("plugin %s already loaded", pluginName)
|
|
}
|
|
|
|
if err := m.VerifyPluginSignature(path, m.publicKeyPath); err != nil {
|
|
return fmt.Errorf("failed to verify plugin signature: %w", err)
|
|
}
|
|
|
|
lazyPlug := &lazyPlugin{path: path}
|
|
if err := lazyPlug.load(); err != nil {
|
|
return fmt.Errorf("failed to load plugin %s: %w", pluginName, err)
|
|
}
|
|
|
|
plugin := lazyPlug.loaded
|
|
|
|
if err := plugin.PreLoad(); err != nil {
|
|
return fmt.Errorf("pre-load hook failed for %s: %w", pluginName, err)
|
|
}
|
|
|
|
if err := plugin.Init(); err != nil {
|
|
return fmt.Errorf("initialization failed for %s: %w", pluginName, err)
|
|
}
|
|
|
|
if err := plugin.PostLoad(); err != nil {
|
|
return fmt.Errorf("post-load hook failed for %s: %w", pluginName, err)
|
|
}
|
|
|
|
m.plugins[pluginName] = lazyPlug
|
|
m.stats[pluginName] = &PluginStats{}
|
|
|
|
metadata := plugin.Metadata()
|
|
m.dependencies[pluginName] = make([]string, 0, len(metadata.Dependencies))
|
|
for dep, constraint := range metadata.Dependencies {
|
|
m.dependencies[pluginName] = append(m.dependencies[pluginName], dep)
|
|
if err := m.checkDependency(dep, constraint); err != nil {
|
|
delete(m.plugins, pluginName)
|
|
delete(m.stats, pluginName)
|
|
delete(m.dependencies, pluginName)
|
|
return fmt.Errorf("dependency check failed for %s: %w", pluginName, err)
|
|
}
|
|
}
|
|
|
|
m.eventBus.Publish(PluginLoadedEvent{PluginName: pluginName})
|
|
m.logger.Info("Plugin loaded", zap.String("plugin", pluginName))
|
|
|
|
return nil
|
|
}
|
|
|
|
func (m *Manager) UnloadPlugin(name string) error {
|
|
m.mu.Lock()
|
|
defer m.mu.Unlock()
|
|
|
|
plugin, exists := m.plugins[name]
|
|
if !exists {
|
|
return ErrPluginNotFound
|
|
}
|
|
|
|
if err := plugin.loaded.PreUnload(); err != nil {
|
|
return fmt.Errorf("pre-unload hook failed for %s: %w", name, err)
|
|
}
|
|
|
|
if err := plugin.loaded.Shutdown(); err != nil {
|
|
return fmt.Errorf("shutdown failed for %s: %w", name, err)
|
|
}
|
|
|
|
delete(m.plugins, name)
|
|
delete(m.dependencies, name)
|
|
delete(m.stats, name)
|
|
|
|
m.eventBus.Publish(PluginUnloadedEvent{PluginName: name})
|
|
m.logger.Info("Plugin unloaded", zap.String("plugin", name))
|
|
|
|
return nil
|
|
}
|
|
|
|
func (m *Manager) ExecutePlugin(name string) error {
|
|
m.mu.RLock()
|
|
plugin, exists := m.plugins[name]
|
|
stats := m.stats[name]
|
|
m.mu.RUnlock()
|
|
|
|
if !exists {
|
|
return ErrPluginNotFound
|
|
}
|
|
|
|
if err := m.sandbox.Enable(); err != nil {
|
|
return fmt.Errorf("failed to enable sandbox for %s: %w", name, err)
|
|
}
|
|
defer m.sandbox.Disable()
|
|
|
|
if err := plugin.load(); err != nil {
|
|
return fmt.Errorf("failed to load plugin %s: %w", name, err)
|
|
}
|
|
|
|
start := time.Now()
|
|
err := plugin.loaded.Execute()
|
|
executionTime := time.Since(start)
|
|
|
|
m.mu.Lock()
|
|
stats.ExecutionCount++
|
|
stats.LastExecutionTime = executionTime
|
|
stats.TotalExecutionTime += executionTime
|
|
m.mu.Unlock()
|
|
|
|
if err != nil {
|
|
return fmt.Errorf("execution failed for %s: %w", name, err)
|
|
}
|
|
|
|
m.logger.Info("Plugin executed", zap.String("plugin", name), zap.Duration("duration", executionTime))
|
|
return nil
|
|
}
|
|
|
|
func (m *Manager) HotReload(name string, path string) error {
|
|
m.mu.Lock()
|
|
defer m.mu.Unlock()
|
|
|
|
oldPlugin, ok := m.plugins[name]
|
|
if !ok {
|
|
return ErrPluginNotFound
|
|
}
|
|
|
|
if err := m.VerifyPluginSignature(path, m.publicKeyPath); err != nil {
|
|
return fmt.Errorf("failed to verify new plugin signature: %w", err)
|
|
}
|
|
|
|
newLazyPlugin := &lazyPlugin{path: path}
|
|
if err := newLazyPlugin.load(); err != nil {
|
|
return fmt.Errorf("failed to load new version of %s: %w", name, err)
|
|
}
|
|
|
|
newPlugin := newLazyPlugin.loaded
|
|
|
|
metadata := newPlugin.Metadata()
|
|
for dep, constraint := range metadata.Dependencies {
|
|
if err := m.checkDependency(dep, constraint); err != nil {
|
|
return fmt.Errorf("dependency check failed for new version of %s: %w", name, err)
|
|
}
|
|
}
|
|
|
|
if err := newPlugin.Init(); err != nil {
|
|
return fmt.Errorf("initialization failed for new version of %s: %w", name, err)
|
|
}
|
|
|
|
if err := oldPlugin.loaded.PreUnload(); err != nil {
|
|
m.logger.Warn("Pre-unload hook failed for old version", zap.String("plugin", name), zap.Error(err))
|
|
}
|
|
if err := oldPlugin.loaded.Shutdown(); err != nil {
|
|
m.logger.Warn("Shutdown failed for old version", zap.String("plugin", name), zap.Error(err))
|
|
}
|
|
|
|
m.plugins[name] = newLazyPlugin
|
|
m.dependencies[name] = make([]string, 0, len(metadata.Dependencies))
|
|
for dep := range metadata.Dependencies {
|
|
m.dependencies[name] = append(m.dependencies[name], dep)
|
|
}
|
|
|
|
m.eventBus.Publish(PluginHotReloadedEvent{PluginName: name})
|
|
m.logger.Info("Plugin hot-reloaded", zap.String("plugin", name))
|
|
|
|
return nil
|
|
}
|
|
|
|
func (m *Manager) checkDependency(depName, constraint string) error {
|
|
depPlugin, exists := m.plugins[depName]
|
|
if !exists {
|
|
return fmt.Errorf("missing dependency: %s", depName)
|
|
}
|
|
|
|
if err := depPlugin.load(); err != nil {
|
|
return fmt.Errorf("failed to load dependency %s: %w", depName, err)
|
|
}
|
|
|
|
depVersion := depPlugin.loaded.Metadata().Version
|
|
if !isVersionCompatible(depVersion, constraint) {
|
|
return fmt.Errorf("incompatible version for dependency %s: required %s, got %s", depName, constraint, depVersion)
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func isVersionCompatible(currentVersion, constraint string) bool {
|
|
parts := strings.Split(constraint, " ")
|
|
if len(parts) != 2 {
|
|
return false
|
|
}
|
|
|
|
operator := parts[0]
|
|
requiredVersion := parts[1]
|
|
|
|
switch operator {
|
|
case ">=":
|
|
return compareVersions(currentVersion, requiredVersion) >= 0
|
|
case ">":
|
|
return compareVersions(currentVersion, requiredVersion) > 0
|
|
case "<=":
|
|
return compareVersions(currentVersion, requiredVersion) <= 0
|
|
case "<":
|
|
return compareVersions(currentVersion, requiredVersion) < 0
|
|
case "==":
|
|
return compareVersions(currentVersion, requiredVersion) == 0
|
|
default:
|
|
return false
|
|
}
|
|
}
|
|
|
|
func compareVersions(v1, v2 string) int {
|
|
parts1 := strings.Split(v1, ".")
|
|
parts2 := strings.Split(v2, ".")
|
|
|
|
for i := 0; i < len(parts1) && i < len(parts2); i++ {
|
|
n1, _ := strconv.Atoi(parts1[i])
|
|
n2, _ := strconv.Atoi(parts2[i])
|
|
|
|
if n1 < n2 {
|
|
return -1
|
|
} else if n1 > n2 {
|
|
return 1
|
|
}
|
|
}
|
|
|
|
if len(parts1) < len(parts2) {
|
|
return -1
|
|
} else if len(parts1) > len(parts2) {
|
|
return 1
|
|
}
|
|
|
|
return 0
|
|
}
|
|
|
|
func (m *Manager) EnablePlugin(name string) error {
|
|
if err := m.config.EnablePlugin(name); err != nil {
|
|
return err
|
|
}
|
|
return m.config.Save()
|
|
}
|
|
|
|
func (m *Manager) DisablePlugin(name string) error {
|
|
if err := m.config.DisablePlugin(name); err != nil {
|
|
return err
|
|
}
|
|
return m.config.Save()
|
|
}
|
|
|
|
func (m *Manager) LoadEnabledPlugins(pluginDir string) error {
|
|
enabled := m.config.EnabledPlugins()
|
|
for _, name := range enabled {
|
|
path := filepath.Join(pluginDir, name+".so")
|
|
if err := m.LoadPlugin(path); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func (m *Manager) ListPlugins() []string {
|
|
m.mu.RLock()
|
|
defer m.mu.RUnlock()
|
|
|
|
plugins := make([]string, 0, len(m.plugins))
|
|
for name := range m.plugins {
|
|
plugins = append(plugins, name)
|
|
}
|
|
return plugins
|
|
}
|
|
|
|
func (m *Manager) GetPluginStats(name string) (*PluginStats, error) {
|
|
m.mu.RLock()
|
|
defer m.mu.RUnlock()
|
|
|
|
stats, ok := m.stats[name]
|
|
if !ok {
|
|
return nil, ErrPluginNotFound
|
|
}
|
|
return stats, nil
|
|
}
|
|
|
|
func (m *Manager) SubscribeToEvent(eventName string, handler EventHandler) {
|
|
m.eventBus.Subscribe(eventName, handler)
|
|
}
|