Files
plugin-manager/manager.go
Matt Dunleavy eedbbfec4a Implement major enhancements to plugin manager (v1.2.0 & v1.3.0)
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.
2024-07-06 14:37:17 -04:00

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)
}