mirror of
https://github.com/matt-dunleavy/plugin-manager.git
synced 2025-10-04 11:56:28 +08:00
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.
This commit is contained in:
25
.gitignore
vendored
Normal file
25
.gitignore
vendored
Normal file
@@ -0,0 +1,25 @@
|
|||||||
|
# If you prefer the allow list template instead of the deny list, see community template:
|
||||||
|
# https://github.com/github/gitignore/blob/main/community/Golang/Go.AllowList.gitignore
|
||||||
|
#
|
||||||
|
# Binaries for programs and plugins
|
||||||
|
*.exe
|
||||||
|
*.exe~
|
||||||
|
*.dylib
|
||||||
|
|
||||||
|
# Test binary, built with `go test -c`
|
||||||
|
*.test
|
||||||
|
|
||||||
|
# Output of the go coverage tool, specifically when used with LiteIDE
|
||||||
|
*.out
|
||||||
|
|
||||||
|
# Dependency directories (remove the comment below to include it)
|
||||||
|
# vendor/
|
||||||
|
.vscode/
|
||||||
|
.todo/
|
||||||
|
|
||||||
|
# Go workspace file
|
||||||
|
go.work
|
||||||
|
go.work.sum
|
||||||
|
|
||||||
|
# env file
|
||||||
|
.env
|
112
CHANGELOG.md
Normal file
112
CHANGELOG.md
Normal file
@@ -0,0 +1,112 @@
|
|||||||
|
# Changelog
|
||||||
|
|
||||||
|
## [1.3.0] - 2024-07-06
|
||||||
|
|
||||||
|
### Added
|
||||||
|
- Enhanced plugin lifecycle management
|
||||||
|
- Implemented `PreLoad`, `PostLoad`, and `PreUnload` hooks in the Plugin interface
|
||||||
|
- Added graceful shutdown for plugins during hot-reload
|
||||||
|
- Improved version compatibility checking
|
||||||
|
- Implemented custom version comparison logic without external dependencies
|
||||||
|
- Added support for version constraints (>=, >, <=, <, ==)
|
||||||
|
- Lazy loading for plugins
|
||||||
|
- Introduced `lazyPlugin` struct for deferred plugin loading
|
||||||
|
- Optimized plugin loading to occur only when necessary
|
||||||
|
- Comprehensive error handling
|
||||||
|
- Added detailed error messages throughout the codebase
|
||||||
|
- Improved error propagation and logging
|
||||||
|
- Enhanced concurrency safety
|
||||||
|
- Implemented fine-grained locking with `sync.RWMutex`
|
||||||
|
- Ensured thread-safety for all shared data access operations
|
||||||
|
- Plugin statistics tracking
|
||||||
|
- Added `PluginStats` struct to track execution count and times
|
||||||
|
- Implemented `GetPluginStats` method for retrieving plugin performance data
|
||||||
|
|
||||||
|
### Changed
|
||||||
|
- Refactored `Manager` struct to support new features
|
||||||
|
- Updated `LoadPlugin` method to incorporate new lifecycle hooks and lazy loading
|
||||||
|
- Modified `HotReload` method to include graceful shutdown of old plugin versions
|
||||||
|
- Improved `ExecutePlugin` method to work with lazy-loaded plugins and update statistics
|
||||||
|
- Enhanced `checkDependency` method to use the new version compatibility checking
|
||||||
|
|
||||||
|
### Removed
|
||||||
|
- Dependency on external version comparison libraries
|
||||||
|
|
||||||
|
### Security
|
||||||
|
- Added placeholder for plugin signature verification in `LoadPlugin` and `HotReload` methods
|
||||||
|
- Implemented `VerifyPluginSignature` method (to be fully implemented in future)
|
||||||
|
|
||||||
|
## [1.2.0] - 2024-07-05
|
||||||
|
|
||||||
|
### Added
|
||||||
|
- Plugin discovery system
|
||||||
|
- Implemented automatic plugin discovery in specified directories
|
||||||
|
- Remote plugin repository support
|
||||||
|
- Added `PluginRepository` struct for managing remote repositories
|
||||||
|
- Implemented `SetupRemoteRepository` function for configuring remote repositories
|
||||||
|
- Plugin update system
|
||||||
|
- Added `CheckForUpdates` and `UpdatePlugin` functions (placeholders to be implemented)
|
||||||
|
- Redbean server integration for plugin repositories
|
||||||
|
- Implemented `DeployRepository` function for easy deployment of plugin repositories
|
||||||
|
- Added automatic download and setup of Redbean server
|
||||||
|
- Digital signature verification for plugins
|
||||||
|
- Added `VerifyPluginSignature` method (placeholder to be fully implemented)
|
||||||
|
- Enhanced plugin lifecycle hooks
|
||||||
|
- Added `PreLoad` and `PostLoad` hooks to the Plugin interface
|
||||||
|
- Implemented `PreUnload` hook in addition to existing `Shutdown` method
|
||||||
|
- SSH key support for remote repositories
|
||||||
|
- Integrated SSH public key authentication for secure remote repository access
|
||||||
|
|
||||||
|
### Changed
|
||||||
|
- Updated `PluginMetadata` struct to include `Signature` field
|
||||||
|
- Modified `Manager` struct to include `publicKey` field for signature verification
|
||||||
|
- Enhanced `LoadPlugin` method to utilize new lifecycle hooks and signature verification
|
||||||
|
- Updated `UnloadPlugin` method to use the new `PreUnload` hook
|
||||||
|
- Refactored `HotReload` method to incorporate new plugin lifecycle
|
||||||
|
|
||||||
|
### Fixed
|
||||||
|
- Resolved issues with unused imports in discovery.go
|
||||||
|
- Fixed type mismatch in metadata.Dependencies assignment in manager.go
|
||||||
|
- Corrected public key type handling in SetupRemoteRepository function
|
||||||
|
|
||||||
|
## [1.1.0] - 2024-07-04
|
||||||
|
|
||||||
|
### Added
|
||||||
|
- Improved dependency management system
|
||||||
|
- Added version compatibility checks for plugin dependencies
|
||||||
|
- Implemented `checkDependencies` method in Manager
|
||||||
|
- Toolchain compatibility checks
|
||||||
|
- Added Go version compatibility check during plugin loading
|
||||||
|
- Enhanced sandboxing
|
||||||
|
- Implemented `LinuxSandbox` with chroot functionality
|
||||||
|
- Added `VerifyPluginPath` method to prevent loading plugins from outside the sandbox
|
||||||
|
- Lazy loading of plugins
|
||||||
|
- Introduced `lazyPlugin` struct for deferred plugin loading
|
||||||
|
- Comprehensive logging system
|
||||||
|
- Integrated zap logger for structured logging
|
||||||
|
- Enhanced error handling
|
||||||
|
- Created custom `PluginError` type for more informative error messages
|
||||||
|
- Improved hot-reloading mechanism
|
||||||
|
- Added support for graceful shutdown of old plugin versions
|
||||||
|
- Performance optimizations
|
||||||
|
- Optimized plugin loading and execution paths
|
||||||
|
|
||||||
|
### Changed
|
||||||
|
- Refactored `Manager` struct to support new features
|
||||||
|
- Updated `PluginMetadata` to include dependency information and Go version
|
||||||
|
- Modified `LoadPlugin` method to support lazy loading
|
||||||
|
- Updated `ExecutePlugin` method to work with lazy-loaded plugins
|
||||||
|
- Replaced `GetEventBus` method with `SubscribeToEvent` for better encapsulation
|
||||||
|
|
||||||
|
### Fixed
|
||||||
|
- Resolved potential race conditions in plugin management operations
|
||||||
|
- Improved error handling and reporting across all operations
|
||||||
|
|
||||||
|
## [1.0.0] - 2024-07-01
|
||||||
|
|
||||||
|
### Added
|
||||||
|
- Initial release of the plugin manager
|
||||||
|
- Basic plugin loading and unloading functionality
|
||||||
|
- Simple event system for plugin lifecycle events
|
||||||
|
- Configuration management for enabled/disabled plugins
|
||||||
|
- Basic plugin execution and stats collection
|
37
README.md
37
README.md
@@ -1,6 +1,6 @@
|
|||||||
# Plugin Manager for Go
|
# Plugin Manager for Go
|
||||||
|
|
||||||
A flexible and robust plugin management system for Go applications.
|
A flexible and robust plugin management library for Go applications.
|
||||||
|
|
||||||
## Features
|
## Features
|
||||||
|
|
||||||
@@ -19,6 +19,14 @@ To use this plugin manager in your Go project, run:
|
|||||||
go get github.com/matt-dunleavy/plugin-manager
|
go get github.com/matt-dunleavy/plugin-manager
|
||||||
```
|
```
|
||||||
|
|
||||||
|
## Installation
|
||||||
|
|
||||||
|
To use this plugin manager in your Go project, run:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
go get github.com/matt-dunleavy/plugin-manager
|
||||||
|
```
|
||||||
|
|
||||||
## Usage
|
## Usage
|
||||||
|
|
||||||
### Creating a plugin
|
### Creating a plugin
|
||||||
@@ -83,7 +91,6 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
func main() {
|
func main() {
|
||||||
|
|
||||||
// Create a new plugin manager
|
// Create a new plugin manager
|
||||||
manager, err := pm.NewManager("plugins.json", "./plugins")
|
manager, err := pm.NewManager("plugins.json", "./plugins")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -115,7 +122,7 @@ func main() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Subscribe to plugin events
|
// Subscribe to plugin events
|
||||||
manager.GetEventBus().Subscribe("PluginLoaded", func(e pm.Event) {
|
manager.SubscribeToEvent("PluginLoaded", func(e pm.Event) {
|
||||||
fmt.Printf("Plugin loaded: %s\n", e.(pm.PluginLoadedEvent).PluginName)
|
fmt.Printf("Plugin loaded: %s\n", e.(pm.PluginLoadedEvent).PluginName)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
@@ -136,19 +143,19 @@ The plugin manager uses a JSON configuration file to keep track of enabled plugi
|
|||||||
|
|
||||||
## API Reference
|
## API Reference
|
||||||
|
|
||||||
- ### Manager
|
### Manager
|
||||||
|
|
||||||
- `NewManager(configPath string, pluginDir string) (*Manager, error)`
|
- `NewManager(configPath string, pluginDir string) (*Manager, error)`
|
||||||
- `LoadPlugin(path string) error`
|
- `LoadPlugin(path string) error`
|
||||||
- `UnloadPlugin(name string) error`
|
- `UnloadPlugin(name string) error`
|
||||||
- `ExecutePlugin(name string) error`
|
- `ExecutePlugin(name string) error`
|
||||||
- `HotReload(name string, path string) error`
|
- `HotReload(name string, path string) error`
|
||||||
- `EnablePlugin(name string) error`
|
- `EnablePlugin(name string) error`
|
||||||
- `DisablePlugin(name string) error`
|
- `DisablePlugin(name string) error`
|
||||||
- `LoadEnabledPlugins(pluginDir string) error`
|
- `LoadEnabledPlugins(pluginDir string) error`
|
||||||
- `ListPlugins() []string`
|
- `ListPlugins() []string`
|
||||||
- `GetPluginStats(name string) (*PluginStats, error)`
|
- `GetPluginStats(name string) (*PluginStats, error)`
|
||||||
- `SubscribeToEvent(eventName string, handler EventHandler)`
|
- `SubscribeToEvent(eventName string, handler EventHandler)`
|
||||||
|
|
||||||
### EventBus
|
### EventBus
|
||||||
|
|
||||||
|
169
discovery.go
Normal file
169
discovery.go
Normal file
@@ -0,0 +1,169 @@
|
|||||||
|
// Copyright (C) 2024 Matt Dunleavy. All rights reserved.
|
||||||
|
// Use of this source code is subject to the MIT license
|
||||||
|
// that can be found in the LICENSE file.
|
||||||
|
|
||||||
|
package pluginmanager
|
||||||
|
|
||||||
|
import (
|
||||||
|
"crypto"
|
||||||
|
"crypto/rsa"
|
||||||
|
"crypto/sha256"
|
||||||
|
"crypto/x509"
|
||||||
|
"encoding/pem"
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
|
"net/http"
|
||||||
|
"os"
|
||||||
|
"os/exec"
|
||||||
|
"path/filepath"
|
||||||
|
"runtime"
|
||||||
|
"strings"
|
||||||
|
"go.uber.org/zap"
|
||||||
|
"golang.org/x/crypto/ssh"
|
||||||
|
)
|
||||||
|
|
||||||
|
type PluginRepository struct {
|
||||||
|
URL string
|
||||||
|
SSHKey string
|
||||||
|
PublicKey ssh.PublicKey
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *Manager) DiscoverPlugins(dir string) error {
|
||||||
|
return filepath.Walk(dir, func(path string, info os.FileInfo, err error) error {
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if filepath.Ext(path) == ".so" {
|
||||||
|
pluginName := strings.TrimSuffix(filepath.Base(path), ".so")
|
||||||
|
if err := m.LoadPlugin(path); err != nil {
|
||||||
|
m.logger.Warn("Failed to load discovered plugin", zap.String("plugin", pluginName), zap.Error(err))
|
||||||
|
} else {
|
||||||
|
m.logger.Info("Discovered and loaded plugin", zap.String("plugin", pluginName))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *Manager) SetupRemoteRepository(url, sshKeyPath string) (*PluginRepository, error) {
|
||||||
|
key, err := os.ReadFile(sshKeyPath)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to read SSH key: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
signer, err := ssh.ParsePrivateKey(key)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to parse SSH key: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return &PluginRepository{
|
||||||
|
URL: url,
|
||||||
|
SSHKey: string(key),
|
||||||
|
PublicKey: signer.PublicKey(),
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *Manager) DeployRepository(repo *PluginRepository, localPath string) error {
|
||||||
|
if err := m.downloadRedbean(localPath); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
cmd := exec.Command(filepath.Join(localPath, "redbean.com"), "-v")
|
||||||
|
if repo.URL != "" {
|
||||||
|
// Deploy via SSH
|
||||||
|
cmd = exec.Command("ssh", "-i", repo.SSHKey, repo.URL, filepath.Join(localPath, "redbean.com"), "-v")
|
||||||
|
}
|
||||||
|
|
||||||
|
output, err := cmd.CombinedOutput()
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to deploy repository: %w\nOutput: %s", err, string(output))
|
||||||
|
}
|
||||||
|
|
||||||
|
m.logger.Info("Repository deployed successfully", zap.String("output", string(output)))
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *Manager) downloadRedbean(localPath string) error {
|
||||||
|
resp, err := http.Get("https://redbean.dev/redbean-latest.com")
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to download redbean: %w", err)
|
||||||
|
}
|
||||||
|
defer resp.Body.Close()
|
||||||
|
|
||||||
|
out, err := os.Create(filepath.Join(localPath, "redbean.com"))
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to create redbean file: %w", err)
|
||||||
|
}
|
||||||
|
defer out.Close()
|
||||||
|
|
||||||
|
_, err = io.Copy(out, resp.Body)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to save redbean file: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if runtime.GOOS != "windows" {
|
||||||
|
if err := os.Chmod(filepath.Join(localPath, "redbean.com"), 0755); err != nil {
|
||||||
|
return fmt.Errorf("failed to set execute permission on redbean: %w", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *Manager) CheckForUpdates(repo *PluginRepository) ([]string, error) {
|
||||||
|
// Implement logic to check for updates from the repository
|
||||||
|
// This would typically involve making an HTTP request to the repository
|
||||||
|
// and comparing versions of installed plugins with available versions
|
||||||
|
return []string{}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *Manager) UpdatePlugin(repo *PluginRepository, pluginName string) error {
|
||||||
|
// Implement logic to download and update a specific plugin
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *Manager) VerifyPluginSignature(pluginPath string, publicKeyPath string) error {
|
||||||
|
// Read the plugin file
|
||||||
|
pluginData, err := os.ReadFile(pluginPath)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to read plugin file: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Read the signature file
|
||||||
|
signaturePath := pluginPath + ".sig"
|
||||||
|
signatureData, err := os.ReadFile(signaturePath)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to read signature file: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Read the public key
|
||||||
|
publicKeyData, err := os.ReadFile(publicKeyPath)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to read public key file: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Parse the public key
|
||||||
|
block, _ := pem.Decode(publicKeyData)
|
||||||
|
if block == nil {
|
||||||
|
return fmt.Errorf("failed to parse PEM block containing the public key")
|
||||||
|
}
|
||||||
|
|
||||||
|
publicKey, err := x509.ParsePKIXPublicKey(block.Bytes)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to parse public key: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
rsaPublicKey, ok := publicKey.(*rsa.PublicKey)
|
||||||
|
if !ok {
|
||||||
|
return fmt.Errorf("public key is not an RSA public key")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Verify the signature
|
||||||
|
hashed := sha256.Sum256(pluginData)
|
||||||
|
err = rsa.VerifyPKCS1v15(rsaPublicKey, crypto.SHA256, hashed[:], signatureData)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to verify signature: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
24
errors.go
24
errors.go
@@ -4,7 +4,10 @@
|
|||||||
|
|
||||||
package pluginmanager
|
package pluginmanager
|
||||||
|
|
||||||
import "errors"
|
import (
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
)
|
||||||
|
|
||||||
var (
|
var (
|
||||||
ErrPluginAlreadyLoaded = errors.New("plugin already loaded")
|
ErrPluginAlreadyLoaded = errors.New("plugin already loaded")
|
||||||
@@ -14,4 +17,21 @@ var (
|
|||||||
ErrMissingDependency = errors.New("missing plugin dependency")
|
ErrMissingDependency = errors.New("missing plugin dependency")
|
||||||
ErrCircularDependency = errors.New("circular plugin dependency detected")
|
ErrCircularDependency = errors.New("circular plugin dependency detected")
|
||||||
ErrPluginSandboxViolation = errors.New("plugin attempted to violate sandbox")
|
ErrPluginSandboxViolation = errors.New("plugin attempted to violate sandbox")
|
||||||
)
|
)
|
||||||
|
|
||||||
|
type PluginError struct {
|
||||||
|
Op string
|
||||||
|
Err error
|
||||||
|
Plugin string
|
||||||
|
}
|
||||||
|
|
||||||
|
func (e *PluginError) Error() string {
|
||||||
|
if e.Plugin != "" {
|
||||||
|
return fmt.Sprintf("plugin error: %s: %s: %v", e.Plugin, e.Op, e.Err)
|
||||||
|
}
|
||||||
|
return fmt.Sprintf("plugin error: %s: %v", e.Op, e.Err)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (e *PluginError) Unwrap() error {
|
||||||
|
return e.Err
|
||||||
|
}
|
@@ -1,99 +0,0 @@
|
|||||||
package main
|
|
||||||
|
|
||||||
import (
|
|
||||||
"fmt"
|
|
||||||
"log"
|
|
||||||
"os"
|
|
||||||
"path/filepath"
|
|
||||||
"time"
|
|
||||||
|
|
||||||
pm "github.com/matt-dunleavy/plugin-manager"
|
|
||||||
)
|
|
||||||
|
|
||||||
func main() {
|
|
||||||
// Get the current working directory
|
|
||||||
cwd, err := os.Getwd()
|
|
||||||
if err != nil {
|
|
||||||
log.Fatalf("Failed to get current working directory: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Set up paths
|
|
||||||
configPath := filepath.Join(cwd, "plugins.json")
|
|
||||||
pluginDir := filepath.Join(cwd, "plugins")
|
|
||||||
|
|
||||||
// Ensure plugin directory exists
|
|
||||||
if err := os.MkdirAll(pluginDir, 0755); err != nil {
|
|
||||||
log.Fatalf("Failed to create plugin directory: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Create a new plugin manager
|
|
||||||
manager, err := pm.NewManager(configPath, pluginDir)
|
|
||||||
if err != nil {
|
|
||||||
log.Fatalf("Failed to create plugin manager: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Subscribe to plugin events
|
|
||||||
manager.SubscribeToEvent("PluginLoaded", func(e pm.Event) {
|
|
||||||
fmt.Printf("Event: Plugin loaded - %s\n", e.(pm.PluginLoadedEvent).PluginName)
|
|
||||||
})
|
|
||||||
manager.SubscribeToEvent("PluginUnloaded", func(e pm.Event) {
|
|
||||||
fmt.Printf("Event: Plugin unloaded - %s\n", e.(pm.PluginUnloadedEvent).PluginName)
|
|
||||||
})
|
|
||||||
|
|
||||||
// Load plugins
|
|
||||||
pluginsToLoad := []string{"hello.so", "math.so"}
|
|
||||||
for _, plugin := range pluginsToLoad {
|
|
||||||
pluginPath := filepath.Join(pluginDir, plugin)
|
|
||||||
err := manager.LoadPlugin(pluginPath)
|
|
||||||
if err != nil {
|
|
||||||
log.Printf("Failed to load plugin %s: %v", plugin, err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// List loaded plugins
|
|
||||||
loadedPlugins := manager.ListPlugins()
|
|
||||||
fmt.Println("Loaded plugins:", loadedPlugins)
|
|
||||||
|
|
||||||
// Execute plugins
|
|
||||||
pluginsToExecute := []string{"HelloPlugin", "MathPlugin"}
|
|
||||||
for _, plugin := range pluginsToExecute {
|
|
||||||
err := manager.ExecutePlugin(plugin)
|
|
||||||
if err != nil {
|
|
||||||
log.Printf("Failed to execute %s: %v", plugin, err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Get plugin stats
|
|
||||||
for _, plugin := range pluginsToExecute {
|
|
||||||
stats, err := manager.GetPluginStats(plugin)
|
|
||||||
if err != nil {
|
|
||||||
fmt.Printf("%s stats not available: %v\n", plugin, err)
|
|
||||||
} else {
|
|
||||||
fmt.Printf("%s stats: %+v\n", plugin, stats)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Hot-reload HelloPlugin
|
|
||||||
time.Sleep(2 * time.Second) // Wait to simulate some time passing
|
|
||||||
fmt.Println("\nHot-reloading HelloPlugin...")
|
|
||||||
err = manager.HotReload("HelloPlugin", filepath.Join(pluginDir, "hello.so"))
|
|
||||||
if err != nil {
|
|
||||||
log.Printf("Failed to hot-reload HelloPlugin: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Execute hot-reloaded plugin
|
|
||||||
err = manager.ExecutePlugin("HelloPlugin")
|
|
||||||
if err != nil {
|
|
||||||
log.Printf("Failed to execute hot-reloaded HelloPlugin: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Unload plugins
|
|
||||||
for _, plugin := range loadedPlugins {
|
|
||||||
err := manager.UnloadPlugin(plugin)
|
|
||||||
if err != nil {
|
|
||||||
log.Printf("Failed to unload %s: %v", plugin, err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fmt.Println("\nFinal list of loaded plugins:", manager.ListPlugins())
|
|
||||||
}
|
|
5
examples/go.mod
Normal file
5
examples/go.mod
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
module example
|
||||||
|
|
||||||
|
go 1.22.4
|
||||||
|
|
||||||
|
require github.com/matt-dunleavy/plugin-manager v0.0.0-20240706084251-0b73b624d771
|
2
examples/go.sum
Normal file
2
examples/go.sum
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
github.com/matt-dunleavy/plugin-manager v0.0.0-20240706084251-0b73b624d771 h1:Imr5rapXzJ7lc7PAPmW65dZkuh+gXFqnBqdRL0vve6s=
|
||||||
|
github.com/matt-dunleavy/plugin-manager v0.0.0-20240706084251-0b73b624d771/go.mod h1:gtVoU5Qv+UkGsFALWzrbovBqELvvlxFunRyaz1PYjGo=
|
62
examples/main.go
Normal file
62
examples/main.go
Normal file
@@ -0,0 +1,62 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"log"
|
||||||
|
"path/filepath"
|
||||||
|
|
||||||
|
pm "github.com/matt-dunleavy/plugin-manager"
|
||||||
|
)
|
||||||
|
|
||||||
|
func main() {
|
||||||
|
// Initialize the plugin manager
|
||||||
|
manager, err := pm.NewManager("plugins.json", "./plugins", "public_key.pem")
|
||||||
|
if err != nil {
|
||||||
|
log.Fatalf("Failed to create plugin manager: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Subscribe to plugin events
|
||||||
|
manager.SubscribeToEvent("PluginLoaded", func(e pm.Event) {
|
||||||
|
fmt.Printf("Plugin loaded: %s\n", e.(pm.PluginLoadedEvent).PluginName)
|
||||||
|
})
|
||||||
|
|
||||||
|
// Load plugins
|
||||||
|
plugins := []string{"hello.so", "math.so"}
|
||||||
|
for _, plugin := range plugins {
|
||||||
|
err := manager.LoadPlugin(filepath.Join("./plugins", plugin))
|
||||||
|
if err != nil {
|
||||||
|
log.Printf("Failed to load plugin %s: %v", plugin, err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// List loaded plugins
|
||||||
|
loadedPlugins := manager.ListPlugins()
|
||||||
|
fmt.Println("Loaded plugins:", loadedPlugins)
|
||||||
|
|
||||||
|
// Execute plugins
|
||||||
|
for _, name := range loadedPlugins {
|
||||||
|
err := manager.ExecutePlugin(name)
|
||||||
|
if err != nil {
|
||||||
|
log.Printf("Failed to execute plugin %s: %v", name, err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get and print plugin stats
|
||||||
|
for _, name := range loadedPlugins {
|
||||||
|
stats, err := manager.GetPluginStats(name)
|
||||||
|
if err != nil {
|
||||||
|
log.Printf("Failed to get stats for plugin %s: %v", name, err)
|
||||||
|
} else {
|
||||||
|
fmt.Printf("Stats for %s: Executions: %d, Last execution time: %v\n",
|
||||||
|
name, stats.ExecutionCount, stats.LastExecutionTime)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Unload a plugin
|
||||||
|
err = manager.UnloadPlugin("hello.so")
|
||||||
|
if err != nil {
|
||||||
|
log.Printf("Failed to unload plugin: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
fmt.Println("Remaining plugins:", manager.ListPlugins())
|
||||||
|
}
|
@@ -1,6 +0,0 @@
|
|||||||
{
|
|
||||||
"enabled": {
|
|
||||||
"HelloPlugin": true,
|
|
||||||
"MathPlugin": true
|
|
||||||
}
|
|
||||||
}
|
|
@@ -2,29 +2,27 @@ package main
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
|
|
||||||
pm "github.com/matt-dunleavy/plugin-manager"
|
pm "github.com/matt-dunleavy/plugin-manager"
|
||||||
)
|
)
|
||||||
|
|
||||||
type HelloPlugin struct {
|
type HelloPlugin struct{}
|
||||||
greeting string
|
|
||||||
}
|
|
||||||
|
|
||||||
func (p *HelloPlugin) Metadata() pm.PluginMetadata {
|
func (p *HelloPlugin) Metadata() pm.PluginMetadata {
|
||||||
return pm.PluginMetadata{
|
return pm.PluginMetadata{
|
||||||
Name: "HelloPlugin",
|
Name: "HelloPlugin",
|
||||||
Version: "1.0.0",
|
Version: "1.0.0",
|
||||||
Dependencies: []string{},
|
Dependencies: map[string]string{},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (p *HelloPlugin) Init() error {
|
func (p *HelloPlugin) Init() error {
|
||||||
p.greeting = "Hello, World!"
|
|
||||||
fmt.Println("HelloPlugin initialized")
|
fmt.Println("HelloPlugin initialized")
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (p *HelloPlugin) Execute() error {
|
func (p *HelloPlugin) Execute() error {
|
||||||
fmt.Println(p.greeting)
|
fmt.Println("Hello from HelloPlugin!")
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -33,4 +31,19 @@ func (p *HelloPlugin) Shutdown() error {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (p *HelloPlugin) PreLoad() error {
|
||||||
|
fmt.Println("HelloPlugin pre-load")
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *HelloPlugin) PostLoad() error {
|
||||||
|
fmt.Println("HelloPlugin post-load")
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *HelloPlugin) PreUnload() error {
|
||||||
|
fmt.Println("HelloPlugin pre-unload")
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
var Plugin HelloPlugin
|
var Plugin HelloPlugin
|
@@ -2,6 +2,7 @@ package main
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
|
|
||||||
pm "github.com/matt-dunleavy/plugin-manager"
|
pm "github.com/matt-dunleavy/plugin-manager"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -9,9 +10,9 @@ type MathPlugin struct{}
|
|||||||
|
|
||||||
func (p *MathPlugin) Metadata() pm.PluginMetadata {
|
func (p *MathPlugin) Metadata() pm.PluginMetadata {
|
||||||
return pm.PluginMetadata{
|
return pm.PluginMetadata{
|
||||||
Name: "MathPlugin",
|
Name: "MathPlugin",
|
||||||
Version: "1.0.0",
|
Version: "1.0.0",
|
||||||
Dependencies: []string{},
|
Dependencies: map[string]string{},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -21,11 +22,8 @@ func (p *MathPlugin) Init() error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (p *MathPlugin) Execute() error {
|
func (p *MathPlugin) Execute() error {
|
||||||
a, b := 10, 5
|
result := p.Add(5, 3)
|
||||||
fmt.Printf("Addition: %d + %d = %d\n", a, b, a+b)
|
fmt.Printf("MathPlugin: 5 + 3 = %d\n", result)
|
||||||
fmt.Printf("Subtraction: %d - %d = %d\n", a, b, a-b)
|
|
||||||
fmt.Printf("Multiplication: %d * %d = %d\n", a, b, a*b)
|
|
||||||
fmt.Printf("Division: %d / %d = %d\n", a, b, a/b)
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -34,4 +32,23 @@ func (p *MathPlugin) Shutdown() error {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (p *MathPlugin) PreLoad() error {
|
||||||
|
fmt.Println("MathPlugin pre-load")
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *MathPlugin) PostLoad() error {
|
||||||
|
fmt.Println("MathPlugin post-load")
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *MathPlugin) PreUnload() error {
|
||||||
|
fmt.Println("MathPlugin pre-unload")
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *MathPlugin) Add(a, b int) int {
|
||||||
|
return a + b
|
||||||
|
}
|
||||||
|
|
||||||
var Plugin MathPlugin
|
var Plugin MathPlugin
|
10
go.mod
10
go.mod
@@ -1,3 +1,13 @@
|
|||||||
module github.com/matt-dunleavy/plugin-manager
|
module github.com/matt-dunleavy/plugin-manager
|
||||||
|
|
||||||
go 1.22.4
|
go 1.22.4
|
||||||
|
|
||||||
|
require (
|
||||||
|
go.uber.org/zap v1.27.0
|
||||||
|
golang.org/x/crypto v0.25.0
|
||||||
|
)
|
||||||
|
|
||||||
|
require (
|
||||||
|
go.uber.org/multierr v1.10.0 // indirect
|
||||||
|
golang.org/x/sys v0.22.0 // indirect
|
||||||
|
)
|
||||||
|
20
go.sum
Normal file
20
go.sum
Normal file
@@ -0,0 +1,20 @@
|
|||||||
|
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
||||||
|
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||||
|
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
||||||
|
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||||
|
github.com/stretchr/testify v1.8.1 h1:w7B6lhMri9wdJUVmEZPGGhZzrYTPvgJArz7wNPgYKsk=
|
||||||
|
github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
|
||||||
|
go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto=
|
||||||
|
go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE=
|
||||||
|
go.uber.org/multierr v1.10.0 h1:S0h4aNzvfcFsC3dRF1jLoaov7oRaKqRGC/pUEJ2yvPQ=
|
||||||
|
go.uber.org/multierr v1.10.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y=
|
||||||
|
go.uber.org/zap v1.27.0 h1:aJMhYGrd5QSmlpLMr2MftRKl7t8J8PTZPA732ud/XR8=
|
||||||
|
go.uber.org/zap v1.27.0/go.mod h1:GB2qFLM7cTU87MWRP2mPIjqfIDnGu+VIO4V/SdhGo2E=
|
||||||
|
golang.org/x/crypto v0.25.0 h1:ypSNr+bnYL2YhwoMt2zPxHFmbAN1KZs/njMG3hxUp30=
|
||||||
|
golang.org/x/crypto v0.25.0/go.mod h1:T+wALwcMOSE0kXgUAnPAHqTLW+XHgcELELW8VaDgm/M=
|
||||||
|
golang.org/x/sys v0.22.0 h1:RI27ohtqKCnwULzJLqkv897zojh5/DwS/ENaMzUOaWI=
|
||||||
|
golang.org/x/sys v0.22.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||||
|
golang.org/x/term v0.22.0 h1:BbsgPEJULsl2fV/AT3v15Mjva5yXKQDyKf+TbDz7QJk=
|
||||||
|
golang.org/x/term v0.22.0/go.mod h1:F3qCibpT5AMpCRfhfT53vVJwhLtIVHhB9XDjfFvnMI4=
|
||||||
|
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
||||||
|
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
324
manager.go
324
manager.go
@@ -1,76 +1,131 @@
|
|||||||
// Copyright (C) 2024 Matt Dunleavy. All rights reserved.
|
// FILE: manager.go
|
||||||
// Use of this source code is subject to the MIT license
|
|
||||||
// that can be found in the LICENSE file.
|
|
||||||
|
|
||||||
package pluginmanager
|
package pluginmanager
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"path/filepath"
|
||||||
|
"plugin"
|
||||||
|
"strconv"
|
||||||
|
"strings"
|
||||||
"sync"
|
"sync"
|
||||||
"time"
|
"time"
|
||||||
"path/filepath"
|
|
||||||
|
"go.uber.org/zap"
|
||||||
)
|
)
|
||||||
|
|
||||||
type Manager struct {
|
type Manager struct {
|
||||||
plugins map[string]Plugin
|
plugins map[string]*lazyPlugin
|
||||||
config *Config
|
config *Config
|
||||||
dependencies map[string][]string
|
dependencies map[string][]string
|
||||||
stats map[string]*PluginStats
|
stats map[string]*PluginStats
|
||||||
eventBus *EventBus
|
eventBus *EventBus
|
||||||
sandbox Sandbox
|
sandbox Sandbox
|
||||||
mu sync.RWMutex
|
logger *zap.Logger
|
||||||
|
publicKeyPath string
|
||||||
|
mu sync.RWMutex
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewManager(configPath string, pluginDir string) (*Manager, error) {
|
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)
|
config, err := LoadConfig(configPath)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, fmt.Errorf("failed to load config: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
logger, _ := zap.NewProduction()
|
||||||
|
|
||||||
|
sandboxDir := filepath.Join(pluginDir, "sandbox")
|
||||||
|
|
||||||
return &Manager{
|
return &Manager{
|
||||||
plugins: make(map[string]Plugin),
|
plugins: make(map[string]*lazyPlugin),
|
||||||
config: config,
|
config: config,
|
||||||
dependencies: make(map[string][]string),
|
dependencies: make(map[string][]string),
|
||||||
stats: make(map[string]*PluginStats),
|
stats: make(map[string]*PluginStats),
|
||||||
eventBus: NewEventBus(),
|
eventBus: NewEventBus(),
|
||||||
sandbox: NewDefaultSandbox(pluginDir),
|
sandbox: NewLinuxSandbox(sandboxDir),
|
||||||
|
logger: logger,
|
||||||
|
publicKeyPath: publicKeyPath,
|
||||||
}, nil
|
}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (m *Manager) LoadPlugin(path string) error {
|
func (m *Manager) LoadPlugin(path string) error {
|
||||||
if err := m.sandbox.VerifyPluginPath(path); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
if err := m.sandbox.Enable(); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
defer m.sandbox.Disable()
|
|
||||||
|
|
||||||
plugin, err := LoadPlugin(path)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
m.mu.Lock()
|
m.mu.Lock()
|
||||||
defer m.mu.Unlock()
|
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()
|
metadata := plugin.Metadata()
|
||||||
if _, exists := m.plugins[metadata.Name]; exists {
|
m.dependencies[pluginName] = make([]string, 0, len(metadata.Dependencies))
|
||||||
return ErrPluginAlreadyLoaded
|
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)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := m.checkDependencies(metadata); err != nil {
|
m.eventBus.Publish(PluginLoadedEvent{PluginName: pluginName})
|
||||||
return err
|
m.logger.Info("Plugin loaded", zap.String("plugin", pluginName))
|
||||||
}
|
|
||||||
|
|
||||||
m.plugins[metadata.Name] = plugin
|
return nil
|
||||||
m.dependencies[metadata.Name] = metadata.Dependencies
|
|
||||||
m.stats[metadata.Name] = &PluginStats{}
|
|
||||||
|
|
||||||
m.eventBus.Publish(PluginLoadedEvent{PluginName: metadata.Name})
|
|
||||||
|
|
||||||
return plugin.Init()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (m *Manager) UnloadPlugin(name string) error {
|
func (m *Manager) UnloadPlugin(name string) error {
|
||||||
@@ -82,8 +137,12 @@ func (m *Manager) UnloadPlugin(name string) error {
|
|||||||
return ErrPluginNotFound
|
return ErrPluginNotFound
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := plugin.Shutdown(); err != nil {
|
if err := plugin.loaded.PreUnload(); err != nil {
|
||||||
return err
|
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.plugins, name)
|
||||||
@@ -91,6 +150,7 @@ func (m *Manager) UnloadPlugin(name string) error {
|
|||||||
delete(m.stats, name)
|
delete(m.stats, name)
|
||||||
|
|
||||||
m.eventBus.Publish(PluginUnloadedEvent{PluginName: name})
|
m.eventBus.Publish(PluginUnloadedEvent{PluginName: name})
|
||||||
|
m.logger.Info("Plugin unloaded", zap.String("plugin", name))
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
@@ -106,12 +166,16 @@ func (m *Manager) ExecutePlugin(name string) error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if err := m.sandbox.Enable(); err != nil {
|
if err := m.sandbox.Enable(); err != nil {
|
||||||
return err
|
return fmt.Errorf("failed to enable sandbox for %s: %w", name, err)
|
||||||
}
|
}
|
||||||
defer m.sandbox.Disable()
|
defer m.sandbox.Disable()
|
||||||
|
|
||||||
|
if err := plugin.load(); err != nil {
|
||||||
|
return fmt.Errorf("failed to load plugin %s: %w", name, err)
|
||||||
|
}
|
||||||
|
|
||||||
start := time.Now()
|
start := time.Now()
|
||||||
err := plugin.Execute()
|
err := plugin.loaded.Execute()
|
||||||
executionTime := time.Since(start)
|
executionTime := time.Since(start)
|
||||||
|
|
||||||
m.mu.Lock()
|
m.mu.Lock()
|
||||||
@@ -120,40 +184,129 @@ func (m *Manager) ExecutePlugin(name string) error {
|
|||||||
stats.TotalExecutionTime += executionTime
|
stats.TotalExecutionTime += executionTime
|
||||||
m.mu.Unlock()
|
m.mu.Unlock()
|
||||||
|
|
||||||
return err
|
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 {
|
func (m *Manager) HotReload(name string, path string) error {
|
||||||
if err := m.sandbox.VerifyPluginPath(path); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
if err := m.sandbox.Enable(); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
defer m.sandbox.Disable()
|
|
||||||
|
|
||||||
newPlugin, err := LoadPlugin(path)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
m.mu.Lock()
|
m.mu.Lock()
|
||||||
defer m.mu.Unlock()
|
defer m.mu.Unlock()
|
||||||
|
|
||||||
oldPlugin, exists := m.plugins[name]
|
oldPlugin, ok := m.plugins[name]
|
||||||
if !exists {
|
if !ok {
|
||||||
return ErrPluginNotFound
|
return ErrPluginNotFound
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := oldPlugin.Shutdown(); err != nil {
|
if err := m.VerifyPluginSignature(path, m.publicKeyPath); err != nil {
|
||||||
return err
|
return fmt.Errorf("failed to verify new plugin signature: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
m.plugins[name] = newPlugin
|
newLazyPlugin := &lazyPlugin{path: path}
|
||||||
m.eventBus.Publish(PluginHotReloadedEvent{PluginName: name})
|
if err := newLazyPlugin.load(); err != nil {
|
||||||
|
return fmt.Errorf("failed to load new version of %s: %w", name, err)
|
||||||
|
}
|
||||||
|
|
||||||
return newPlugin.Init()
|
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 {
|
func (m *Manager) EnablePlugin(name string) error {
|
||||||
@@ -181,33 +334,10 @@ func (m *Manager) LoadEnabledPlugins(pluginDir string) error {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (m *Manager) checkDependencies(metadata PluginMetadata) error {
|
|
||||||
visited := make(map[string]bool)
|
|
||||||
return m.dfs(metadata.Name, visited)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (m *Manager) dfs(name string, visited map[string]bool) error {
|
|
||||||
if visited[name] {
|
|
||||||
return ErrCircularDependency
|
|
||||||
}
|
|
||||||
|
|
||||||
visited[name] = true
|
|
||||||
for _, dep := range m.dependencies[name] {
|
|
||||||
if _, exists := m.plugins[dep]; !exists {
|
|
||||||
return fmt.Errorf("%w: %s", ErrMissingDependency, dep)
|
|
||||||
}
|
|
||||||
if err := m.dfs(dep, visited); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
visited[name] = false
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (m *Manager) ListPlugins() []string {
|
func (m *Manager) ListPlugins() []string {
|
||||||
m.mu.RLock()
|
m.mu.RLock()
|
||||||
defer m.mu.RUnlock()
|
defer m.mu.RUnlock()
|
||||||
|
|
||||||
plugins := make([]string, 0, len(m.plugins))
|
plugins := make([]string, 0, len(m.plugins))
|
||||||
for name := range m.plugins {
|
for name := range m.plugins {
|
||||||
plugins = append(plugins, name)
|
plugins = append(plugins, name)
|
||||||
@@ -218,7 +348,7 @@ func (m *Manager) ListPlugins() []string {
|
|||||||
func (m *Manager) GetPluginStats(name string) (*PluginStats, error) {
|
func (m *Manager) GetPluginStats(name string) (*PluginStats, error) {
|
||||||
m.mu.RLock()
|
m.mu.RLock()
|
||||||
defer m.mu.RUnlock()
|
defer m.mu.RUnlock()
|
||||||
|
|
||||||
stats, ok := m.stats[name]
|
stats, ok := m.stats[name]
|
||||||
if !ok {
|
if !ok {
|
||||||
return nil, ErrPluginNotFound
|
return nil, ErrPluginNotFound
|
||||||
@@ -228,4 +358,4 @@ func (m *Manager) GetPluginStats(name string) (*PluginStats, error) {
|
|||||||
|
|
||||||
func (m *Manager) SubscribeToEvent(eventName string, handler EventHandler) {
|
func (m *Manager) SubscribeToEvent(eventName string, handler EventHandler) {
|
||||||
m.eventBus.Subscribe(eventName, handler)
|
m.eventBus.Subscribe(eventName, handler)
|
||||||
}
|
}
|
||||||
|
13
plugin.go
13
plugin.go
@@ -12,13 +12,18 @@ import (
|
|||||||
type PluginMetadata struct {
|
type PluginMetadata struct {
|
||||||
Name string
|
Name string
|
||||||
Version string
|
Version string
|
||||||
Dependencies []string
|
Dependencies map[string]string
|
||||||
|
GoVersion string
|
||||||
|
Signature []byte
|
||||||
}
|
}
|
||||||
|
|
||||||
type Plugin interface {
|
type Plugin interface {
|
||||||
Metadata() PluginMetadata
|
Metadata() PluginMetadata
|
||||||
|
PreLoad() error
|
||||||
Init() error
|
Init() error
|
||||||
|
PostLoad() error
|
||||||
Execute() error
|
Execute() error
|
||||||
|
PreUnload() error
|
||||||
Shutdown() error
|
Shutdown() error
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -33,17 +38,17 @@ const PluginSymbol = "Plugin"
|
|||||||
func LoadPlugin(path string) (Plugin, error) {
|
func LoadPlugin(path string) (Plugin, error) {
|
||||||
p, err := plugin.Open(path)
|
p, err := plugin.Open(path)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, &PluginError{Op: "open", Err: err}
|
||||||
}
|
}
|
||||||
|
|
||||||
symPlugin, err := p.Lookup(PluginSymbol)
|
symPlugin, err := p.Lookup(PluginSymbol)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, &PluginError{Op: "lookup", Err: err}
|
||||||
}
|
}
|
||||||
|
|
||||||
plugin, ok := symPlugin.(Plugin)
|
plugin, ok := symPlugin.(Plugin)
|
||||||
if !ok {
|
if !ok {
|
||||||
return nil, ErrInvalidPluginInterface
|
return nil, &PluginError{Op: "assert", Err: ErrInvalidPluginInterface}
|
||||||
}
|
}
|
||||||
|
|
||||||
return plugin, nil
|
return plugin, nil
|
||||||
|
66
sandbox.go
66
sandbox.go
@@ -7,7 +7,7 @@ package pluginmanager
|
|||||||
import (
|
import (
|
||||||
"os"
|
"os"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
"log"
|
"syscall"
|
||||||
)
|
)
|
||||||
|
|
||||||
type Sandbox interface {
|
type Sandbox interface {
|
||||||
@@ -16,38 +16,68 @@ type Sandbox interface {
|
|||||||
VerifyPluginPath(path string) error
|
VerifyPluginPath(path string) error
|
||||||
}
|
}
|
||||||
|
|
||||||
type DefaultSandbox struct {
|
type LinuxSandbox struct {
|
||||||
pluginDir string
|
originalDir string
|
||||||
|
originalUmask int
|
||||||
|
chrootDir string
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewDefaultSandbox(pluginDir string) *DefaultSandbox {
|
func NewLinuxSandbox(chrootDir string) *LinuxSandbox {
|
||||||
absPath, err := filepath.Abs(pluginDir)
|
if chrootDir == "" {
|
||||||
|
chrootDir = "./sandbox"
|
||||||
|
}
|
||||||
|
return &LinuxSandbox{
|
||||||
|
chrootDir: chrootDir,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *LinuxSandbox) Enable() error {
|
||||||
|
var err error
|
||||||
|
s.originalDir, err = os.Getwd()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Printf("Error getting absolute path for plugin directory: %v", err)
|
return err
|
||||||
return &DefaultSandbox{pluginDir: pluginDir}
|
|
||||||
}
|
}
|
||||||
return &DefaultSandbox{
|
|
||||||
pluginDir: absPath,
|
if err := os.MkdirAll(s.chrootDir, 0755); err != nil {
|
||||||
|
return err
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
func (s *DefaultSandbox) Enable() error {
|
if err := syscall.Chroot(s.chrootDir); err != nil {
|
||||||
return os.Chdir(s.pluginDir)
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if err := os.Chdir("/"); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
s.originalUmask = syscall.Umask(0)
|
||||||
|
|
||||||
func (s *DefaultSandbox) Disable() error {
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *DefaultSandbox) VerifyPluginPath(path string) error {
|
func (s *LinuxSandbox) Disable() error {
|
||||||
|
syscall.Umask(s.originalUmask)
|
||||||
|
|
||||||
|
if err := syscall.Chroot("."); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := os.Chdir(s.originalDir); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *LinuxSandbox) VerifyPluginPath(path string) error {
|
||||||
absPath, err := filepath.Abs(path)
|
absPath, err := filepath.Abs(path)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
if !filepath.HasPrefix(absPath, s.pluginDir) {
|
if !filepath.HasPrefix(absPath, s.chrootDir) {
|
||||||
return ErrPluginSandboxViolation
|
return ErrPluginSandboxViolation
|
||||||
}
|
}
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
Reference in New Issue
Block a user