268 lines
6.4 KiB
Go
268 lines
6.4 KiB
Go
package plugins
|
|||
|
|
|
||
|
|
import (
|
||
|
|
"encoding/json"
|
||
|
|
"fmt"
|
||
|
|
"os"
|
||
|
|
"path/filepath"
|
||
|
|
"sync"
|
||
|
|
"time"
|
||
|
|
)
|
||
|
|
|
||
|
|
// PluginLoader loads and manages plugin lifecycle
|
||
|
|
type PluginLoader struct {
|
||
|
|
mu sync.RWMutex
|
||
|
|
registry *PluginRegistry
|
||
|
|
pluginPath string
|
||
|
|
loadedPlugins map[string]*LoadedPlugin
|
||
|
|
loadTime map[string]time.Time
|
||
|
|
failedLoads map[string]error
|
||
|
|
}
|
||
|
|
|
||
|
|
// LoadedPlugin represents a loaded plugin with additional metadata
|
||
|
|
type LoadedPlugin struct {
|
||
|
|
Plugin SkillPlugin
|
||
|
|
LoadedAt time.Time
|
||
|
|
ReloadCount int
|
||
|
|
LastError error
|
||
|
|
}
|
||
|
|
|
||
|
|
// PluginConfig represents plugin configuration from file
|
||
|
|
type PluginConfig struct {
|
||
|
|
Name string `json:"name"`
|
||
|
|
Version string `json:"version"`
|
||
|
|
Author string `json:"author"`
|
||
|
|
Description string `json:"description"`
|
||
|
|
Path string `json:"path"`
|
||
|
|
Config map[string]interface{} `json:"config,omitempty"`
|
||
|
|
Enabled bool `json:"enabled"`
|
||
|
|
}
|
||
|
|
|
||
|
|
// NewPluginLoader creates a new plugin loader
|
||
|
|
func NewPluginLoader(registry *PluginRegistry, pluginPath string) *PluginLoader {
|
||
|
|
return &PluginLoader{
|
||
|
|
registry: registry,
|
||
|
|
pluginPath: pluginPath,
|
||
|
|
loadedPlugins: make(map[string]*LoadedPlugin),
|
||
|
|
loadTime: make(map[string]time.Time),
|
||
|
|
failedLoads: make(map[string]error),
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
// LoadPlugin loads a plugin from URL
|
||
|
|
func (pl *PluginLoader) LoadPlugin(url string) error {
|
||
|
|
pl.mu.Lock()
|
||
|
|
defer pl.mu.Unlock()
|
||
|
|
|
||
|
|
if IsPluginURL(url) {
|
||
|
|
// Already loaded - no need to load from file
|
||
|
|
return nil
|
||
|
|
}
|
||
|
|
|
||
|
|
// Try to load from file
|
||
|
|
return pl.loadFromFileLocked(url)
|
||
|
|
}
|
||
|
|
|
||
|
|
// loadFromFileLocked loads a plugin from a file (must be called with lock held)
|
||
|
|
func (pl *PluginLoader) loadFromFileLocked(path string) error {
|
||
|
|
// Read config file
|
||
|
|
data, err := os.ReadFile(path)
|
||
|
|
if err != nil {
|
||
|
|
pl.failedLoads[path] = err
|
||
|
|
return fmt.Errorf("failed to read plugin config: %w", err)
|
||
|
|
}
|
||
|
|
|
||
|
|
var config PluginConfig
|
||
|
|
if err := json.Unmarshal(data, &config); err != nil {
|
||
|
|
pl.failedLoads[path] = err
|
||
|
|
return fmt.Errorf("failed to parse plugin config: %w", err)
|
||
|
|
}
|
||
|
|
|
||
|
|
// For now, return a placeholder plugin load
|
||
|
|
// In a real implementation, this would use reflection or plugin packages
|
||
|
|
// to dynamically load compiled plugins
|
||
|
|
pl.loadTime[config.Name] = time.Now()
|
||
|
|
|
||
|
|
return nil
|
||
|
|
}
|
||
|
|
|
||
|
|
// LoadPluginDirectory loads all plugins from a directory
|
||
|
|
func (pl *PluginLoader) LoadPluginDirectory(directory string) error {
|
||
|
|
entries, err := os.ReadDir(directory)
|
||
|
|
if err != nil {
|
||
|
|
return fmt.Errorf("failed to read plugin directory: %w", err)
|
||
|
|
}
|
||
|
|
|
||
|
|
for _, entry := range entries {
|
||
|
|
if entry.IsDir() {
|
||
|
|
continue
|
||
|
|
}
|
||
|
|
|
||
|
|
if filepath.Ext(entry.Name()) == ".json" {
|
||
|
|
pluginPath := filepath.Join(directory, entry.Name())
|
||
|
|
if err := pl.LoadPlugin(pluginPath); err != nil {
|
||
|
|
// Log error but continue loading other plugins
|
||
|
|
pl.mu.Lock()
|
||
|
|
pl.failedLoads[pluginPath] = err
|
||
|
|
pl.mu.Unlock()
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
return nil
|
||
|
|
}
|
||
|
|
|
||
|
|
// RegisterLoadedPlugin registers a loaded plugin with the registry
|
||
|
|
func (pl *PluginLoader) RegisterLoadedPlugin(plugin SkillPlugin, author string, config map[string]interface{}) error {
|
||
|
|
pl.mu.Lock()
|
||
|
|
defer pl.mu.Unlock()
|
||
|
|
|
||
|
|
if err := pl.registry.Register(plugin, author, config); err != nil {
|
||
|
|
pl.failedLoads[plugin.Name()] = err
|
||
|
|
return err
|
||
|
|
}
|
||
|
|
|
||
|
|
now := time.Now()
|
||
|
|
pl.loadedPlugins[plugin.Name()] = &LoadedPlugin{
|
||
|
|
Plugin: plugin,
|
||
|
|
LoadedAt: now,
|
||
|
|
LastError: nil,
|
||
|
|
}
|
||
|
|
pl.loadTime[plugin.Name()] = now
|
||
|
|
|
||
|
|
return nil
|
||
|
|
}
|
||
|
|
|
||
|
|
// UnloadPlugin unloads a plugin
|
||
|
|
func (pl *PluginLoader) UnloadPlugin(name string) error {
|
||
|
|
pl.mu.Lock()
|
||
|
|
defer pl.mu.Unlock()
|
||
|
|
|
||
|
|
if _, exists := pl.loadedPlugins[name]; !exists {
|
||
|
|
return fmt.Errorf("plugin not loaded: %s", name)
|
||
|
|
}
|
||
|
|
|
||
|
|
err := pl.registry.Unregister(name)
|
||
|
|
if err == nil {
|
||
|
|
delete(pl.loadedPlugins, name)
|
||
|
|
delete(pl.loadTime, name)
|
||
|
|
}
|
||
|
|
|
||
|
|
return err
|
||
|
|
}
|
||
|
|
|
||
|
|
// ReloadPlugin reloads a plugin
|
||
|
|
func (pl *PluginLoader) ReloadPlugin(name string) error {
|
||
|
|
pl.mu.Lock()
|
||
|
|
defer pl.mu.Unlock()
|
||
|
|
|
||
|
|
loadedPlugin, exists := pl.loadedPlugins[name]
|
||
|
|
if !exists {
|
||
|
|
return fmt.Errorf("plugin not loaded: %s", name)
|
||
|
|
}
|
||
|
|
|
||
|
|
// Re-validate plugin
|
||
|
|
if err := loadedPlugin.Plugin.Validate(); err != nil {
|
||
|
|
pl.failedLoads[name] = err
|
||
|
|
loadedPlugin.LastError = err
|
||
|
|
return fmt.Errorf("plugin validation failed: %w", err)
|
||
|
|
}
|
||
|
|
|
||
|
|
loadedPlugin.ReloadCount++
|
||
|
|
loadedPlugin.LastError = nil
|
||
|
|
pl.loadTime[name] = time.Now()
|
||
|
|
|
||
|
|
return nil
|
||
|
|
}
|
||
|
|
|
||
|
|
// GetLoadedPlugins returns all loaded plugins
|
||
|
|
func (pl *PluginLoader) GetLoadedPlugins() map[string]*LoadedPlugin {
|
||
|
|
pl.mu.RLock()
|
||
|
|
defer pl.mu.RUnlock()
|
||
|
|
|
||
|
|
result := make(map[string]*LoadedPlugin)
|
||
|
|
for name, plugin := range pl.loadedPlugins {
|
||
|
|
result[name] = plugin
|
||
|
|
}
|
||
|
|
|
||
|
|
return result
|
||
|
|
}
|
||
|
|
|
||
|
|
// GetFailedLoads returns all failed plugin loads
|
||
|
|
func (pl *PluginLoader) GetFailedLoads() map[string]error {
|
||
|
|
pl.mu.RLock()
|
||
|
|
defer pl.mu.RUnlock()
|
||
|
|
|
||
|
|
result := make(map[string]error)
|
||
|
|
for path, err := range pl.failedLoads {
|
||
|
|
result[path] = err
|
||
|
|
}
|
||
|
|
|
||
|
|
return result
|
||
|
|
}
|
||
|
|
|
||
|
|
// GetLoadTime returns when a plugin was loaded
|
||
|
|
func (pl *PluginLoader) GetLoadTime(name string) (time.Time, bool) {
|
||
|
|
pl.mu.RLock()
|
||
|
|
defer pl.mu.RUnlock()
|
||
|
|
|
||
|
|
t, exists := pl.loadTime[name]
|
||
|
|
return t, exists
|
||
|
|
}
|
||
|
|
|
||
|
|
// IsPluginLoaded checks if a plugin is loaded
|
||
|
|
func (pl *PluginLoader) IsPluginLoaded(name string) bool {
|
||
|
|
pl.mu.RLock()
|
||
|
|
defer pl.mu.RUnlock()
|
||
|
|
|
||
|
|
_, exists := pl.loadedPlugins[name]
|
||
|
|
return exists
|
||
|
|
}
|
||
|
|
|
||
|
|
// ExecutePlugin executes a loaded plugin
|
||
|
|
func (pl *PluginLoader) ExecutePlugin(name string, input map[string]interface{}) (map[string]interface{}, error) {
|
||
|
|
pl.mu.RLock()
|
||
|
|
if _, exists := pl.loadedPlugins[name]; !exists {
|
||
|
|
pl.mu.RUnlock()
|
||
|
|
return nil, fmt.Errorf("plugin not loaded: %s", name)
|
||
|
|
}
|
||
|
|
pl.mu.RUnlock()
|
||
|
|
|
||
|
|
return pl.registry.Execute(name, input)
|
||
|
|
}
|
||
|
|
|
||
|
|
// GetPluginStats returns stats for a loaded plugin
|
||
|
|
func (pl *PluginLoader) GetPluginStats(name string) (*PluginStats, error) {
|
||
|
|
pl.mu.RLock()
|
||
|
|
defer pl.mu.RUnlock()
|
||
|
|
|
||
|
|
if _, exists := pl.loadedPlugins[name]; !exists {
|
||
|
|
return nil, fmt.Errorf("plugin not loaded: %s", name)
|
||
|
|
}
|
||
|
|
|
||
|
|
return pl.registry.GetStats(), nil
|
||
|
|
}
|
||
|
|
|
||
|
|
// Close closes the plugin loader and unloads all plugins
|
||
|
|
func (pl *PluginLoader) Close() error {
|
||
|
|
pl.mu.Lock()
|
||
|
|
defer pl.mu.Unlock()
|
||
|
|
|
||
|
|
var lastErr error
|
||
|
|
for name := range pl.loadedPlugins {
|
||
|
|
if err := pl.registry.Unregister(name); err != nil {
|
||
|
|
lastErr = err
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
pl.loadedPlugins = make(map[string]*LoadedPlugin)
|
||
|
|
pl.loadTime = make(map[string]time.Time)
|
||
|
|
|
||
|
|
return lastErr
|
||
|
|
}
|
||
|
|
|
||
|
|
// GetPluginRegistry returns the underlying registry
|
||
|
|
func (pl *PluginLoader) GetPluginRegistry() *PluginRegistry {
|
||
|
|
return pl.registry
|
||
|
|
}
|