mirror of
https://github.com/matt-dunleavy/plugin-manager.git
synced 2025-09-27 00:05:51 +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
|
||||
|
||||
A flexible and robust plugin management system for Go applications.
|
||||
A flexible and robust plugin management library for Go applications.
|
||||
|
||||
## Features
|
||||
|
||||
@@ -19,6 +19,14 @@ To use this plugin manager in your Go project, run:
|
||||
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
|
||||
|
||||
### Creating a plugin
|
||||
@@ -83,7 +91,6 @@ import (
|
||||
)
|
||||
|
||||
func main() {
|
||||
|
||||
// Create a new plugin manager
|
||||
manager, err := pm.NewManager("plugins.json", "./plugins")
|
||||
if err != nil {
|
||||
@@ -115,7 +122,7 @@ func main() {
|
||||
}
|
||||
|
||||
// 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)
|
||||
})
|
||||
}
|
||||
@@ -136,19 +143,19 @@ The plugin manager uses a JSON configuration file to keep track of enabled plugi
|
||||
|
||||
## API Reference
|
||||
|
||||
- ### Manager
|
||||
### Manager
|
||||
|
||||
- `NewManager(configPath string, pluginDir string) (*Manager, error)`
|
||||
- `LoadPlugin(path string) error`
|
||||
- `UnloadPlugin(name string) error`
|
||||
- `ExecutePlugin(name string) error`
|
||||
- `HotReload(name string, path string) error`
|
||||
- `EnablePlugin(name string) error`
|
||||
- `DisablePlugin(name string) error`
|
||||
- `LoadEnabledPlugins(pluginDir string) error`
|
||||
- `ListPlugins() []string`
|
||||
- `GetPluginStats(name string) (*PluginStats, error)`
|
||||
- `SubscribeToEvent(eventName string, handler EventHandler)`
|
||||
- `NewManager(configPath string, pluginDir string) (*Manager, error)`
|
||||
- `LoadPlugin(path string) error`
|
||||
- `UnloadPlugin(name string) error`
|
||||
- `ExecutePlugin(name string) error`
|
||||
- `HotReload(name string, path string) error`
|
||||
- `EnablePlugin(name string) error`
|
||||
- `DisablePlugin(name string) error`
|
||||
- `LoadEnabledPlugins(pluginDir string) error`
|
||||
- `ListPlugins() []string`
|
||||
- `GetPluginStats(name string) (*PluginStats, error)`
|
||||
- `SubscribeToEvent(eventName string, handler EventHandler)`
|
||||
|
||||
### 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
|
||||
|
||||
import "errors"
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
)
|
||||
|
||||
var (
|
||||
ErrPluginAlreadyLoaded = errors.New("plugin already loaded")
|
||||
@@ -14,4 +17,21 @@ var (
|
||||
ErrMissingDependency = errors.New("missing plugin dependency")
|
||||
ErrCircularDependency = errors.New("circular plugin dependency detected")
|
||||
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 (
|
||||
"fmt"
|
||||
|
||||
pm "github.com/matt-dunleavy/plugin-manager"
|
||||
)
|
||||
|
||||
type HelloPlugin struct {
|
||||
greeting string
|
||||
}
|
||||
type HelloPlugin struct{}
|
||||
|
||||
func (p *HelloPlugin) Metadata() pm.PluginMetadata {
|
||||
return pm.PluginMetadata{
|
||||
Name: "HelloPlugin",
|
||||
Version: "1.0.0",
|
||||
Dependencies: []string{},
|
||||
Name: "HelloPlugin",
|
||||
Version: "1.0.0",
|
||||
Dependencies: map[string]string{},
|
||||
}
|
||||
}
|
||||
|
||||
func (p *HelloPlugin) Init() error {
|
||||
p.greeting = "Hello, World!"
|
||||
fmt.Println("HelloPlugin initialized")
|
||||
return nil
|
||||
}
|
||||
|
||||
func (p *HelloPlugin) Execute() error {
|
||||
fmt.Println(p.greeting)
|
||||
fmt.Println("Hello from HelloPlugin!")
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -33,4 +31,19 @@ func (p *HelloPlugin) Shutdown() error {
|
||||
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
|
@@ -2,6 +2,7 @@ package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
pm "github.com/matt-dunleavy/plugin-manager"
|
||||
)
|
||||
|
||||
@@ -9,9 +10,9 @@ type MathPlugin struct{}
|
||||
|
||||
func (p *MathPlugin) Metadata() pm.PluginMetadata {
|
||||
return pm.PluginMetadata{
|
||||
Name: "MathPlugin",
|
||||
Version: "1.0.0",
|
||||
Dependencies: []string{},
|
||||
Name: "MathPlugin",
|
||||
Version: "1.0.0",
|
||||
Dependencies: map[string]string{},
|
||||
}
|
||||
}
|
||||
|
||||
@@ -21,11 +22,8 @@ func (p *MathPlugin) Init() error {
|
||||
}
|
||||
|
||||
func (p *MathPlugin) Execute() error {
|
||||
a, b := 10, 5
|
||||
fmt.Printf("Addition: %d + %d = %d\n", a, b, a+b)
|
||||
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)
|
||||
result := p.Add(5, 3)
|
||||
fmt.Printf("MathPlugin: 5 + 3 = %d\n", result)
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -34,4 +32,23 @@ func (p *MathPlugin) Shutdown() error {
|
||||
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
|
10
go.mod
10
go.mod
@@ -1,3 +1,13 @@
|
||||
module github.com/matt-dunleavy/plugin-manager
|
||||
|
||||
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.
|
||||
// Use of this source code is subject to the MIT license
|
||||
// that can be found in the LICENSE file.
|
||||
// FILE: manager.go
|
||||
|
||||
package pluginmanager
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"path/filepath"
|
||||
"plugin"
|
||||
"strconv"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
"path/filepath"
|
||||
|
||||
"go.uber.org/zap"
|
||||
)
|
||||
|
||||
type Manager struct {
|
||||
plugins map[string]Plugin
|
||||
config *Config
|
||||
dependencies map[string][]string
|
||||
stats map[string]*PluginStats
|
||||
eventBus *EventBus
|
||||
sandbox Sandbox
|
||||
mu sync.RWMutex
|
||||
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
|
||||
}
|
||||
|
||||
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)
|
||||
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{
|
||||
plugins: make(map[string]Plugin),
|
||||
config: config,
|
||||
dependencies: make(map[string][]string),
|
||||
stats: make(map[string]*PluginStats),
|
||||
eventBus: NewEventBus(),
|
||||
sandbox: NewDefaultSandbox(pluginDir),
|
||||
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 {
|
||||
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()
|
||||
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()
|
||||
if _, exists := m.plugins[metadata.Name]; exists {
|
||||
return ErrPluginAlreadyLoaded
|
||||
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)
|
||||
}
|
||||
}
|
||||
|
||||
if err := m.checkDependencies(metadata); err != nil {
|
||||
return err
|
||||
}
|
||||
m.eventBus.Publish(PluginLoadedEvent{PluginName: pluginName})
|
||||
m.logger.Info("Plugin loaded", zap.String("plugin", pluginName))
|
||||
|
||||
m.plugins[metadata.Name] = plugin
|
||||
m.dependencies[metadata.Name] = metadata.Dependencies
|
||||
m.stats[metadata.Name] = &PluginStats{}
|
||||
|
||||
m.eventBus.Publish(PluginLoadedEvent{PluginName: metadata.Name})
|
||||
|
||||
return plugin.Init()
|
||||
return nil
|
||||
}
|
||||
|
||||
func (m *Manager) UnloadPlugin(name string) error {
|
||||
@@ -82,8 +137,12 @@ func (m *Manager) UnloadPlugin(name string) error {
|
||||
return ErrPluginNotFound
|
||||
}
|
||||
|
||||
if err := plugin.Shutdown(); err != nil {
|
||||
return err
|
||||
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)
|
||||
@@ -91,6 +150,7 @@ func (m *Manager) UnloadPlugin(name string) error {
|
||||
delete(m.stats, name)
|
||||
|
||||
m.eventBus.Publish(PluginUnloadedEvent{PluginName: name})
|
||||
m.logger.Info("Plugin unloaded", zap.String("plugin", name))
|
||||
|
||||
return nil
|
||||
}
|
||||
@@ -106,12 +166,16 @@ func (m *Manager) ExecutePlugin(name string) error {
|
||||
}
|
||||
|
||||
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()
|
||||
|
||||
if err := plugin.load(); err != nil {
|
||||
return fmt.Errorf("failed to load plugin %s: %w", name, err)
|
||||
}
|
||||
|
||||
start := time.Now()
|
||||
err := plugin.Execute()
|
||||
err := plugin.loaded.Execute()
|
||||
executionTime := time.Since(start)
|
||||
|
||||
m.mu.Lock()
|
||||
@@ -120,40 +184,129 @@ func (m *Manager) ExecutePlugin(name string) error {
|
||||
stats.TotalExecutionTime += executionTime
|
||||
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 {
|
||||
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()
|
||||
defer m.mu.Unlock()
|
||||
|
||||
oldPlugin, exists := m.plugins[name]
|
||||
if !exists {
|
||||
oldPlugin, ok := m.plugins[name]
|
||||
if !ok {
|
||||
return ErrPluginNotFound
|
||||
}
|
||||
|
||||
if err := oldPlugin.Shutdown(); err != nil {
|
||||
return err
|
||||
if err := m.VerifyPluginSignature(path, m.publicKeyPath); err != nil {
|
||||
return fmt.Errorf("failed to verify new plugin signature: %w", err)
|
||||
}
|
||||
|
||||
m.plugins[name] = newPlugin
|
||||
m.eventBus.Publish(PluginHotReloadedEvent{PluginName: name})
|
||||
newLazyPlugin := &lazyPlugin{path: path}
|
||||
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 {
|
||||
@@ -181,33 +334,10 @@ func (m *Manager) LoadEnabledPlugins(pluginDir string) error {
|
||||
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 {
|
||||
m.mu.RLock()
|
||||
defer m.mu.RUnlock()
|
||||
|
||||
|
||||
plugins := make([]string, 0, len(m.plugins))
|
||||
for name := range m.plugins {
|
||||
plugins = append(plugins, name)
|
||||
@@ -218,7 +348,7 @@ func (m *Manager) ListPlugins() []string {
|
||||
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
|
||||
@@ -228,4 +358,4 @@ func (m *Manager) GetPluginStats(name string) (*PluginStats, error) {
|
||||
|
||||
func (m *Manager) SubscribeToEvent(eventName string, handler EventHandler) {
|
||||
m.eventBus.Subscribe(eventName, handler)
|
||||
}
|
||||
}
|
||||
|
13
plugin.go
13
plugin.go
@@ -12,13 +12,18 @@ import (
|
||||
type PluginMetadata struct {
|
||||
Name string
|
||||
Version string
|
||||
Dependencies []string
|
||||
Dependencies map[string]string
|
||||
GoVersion string
|
||||
Signature []byte
|
||||
}
|
||||
|
||||
type Plugin interface {
|
||||
Metadata() PluginMetadata
|
||||
PreLoad() error
|
||||
Init() error
|
||||
PostLoad() error
|
||||
Execute() error
|
||||
PreUnload() error
|
||||
Shutdown() error
|
||||
}
|
||||
|
||||
@@ -33,17 +38,17 @@ const PluginSymbol = "Plugin"
|
||||
func LoadPlugin(path string) (Plugin, error) {
|
||||
p, err := plugin.Open(path)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
return nil, &PluginError{Op: "open", Err: err}
|
||||
}
|
||||
|
||||
symPlugin, err := p.Lookup(PluginSymbol)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
return nil, &PluginError{Op: "lookup", Err: err}
|
||||
}
|
||||
|
||||
plugin, ok := symPlugin.(Plugin)
|
||||
if !ok {
|
||||
return nil, ErrInvalidPluginInterface
|
||||
return nil, &PluginError{Op: "assert", Err: ErrInvalidPluginInterface}
|
||||
}
|
||||
|
||||
return plugin, nil
|
||||
|
66
sandbox.go
66
sandbox.go
@@ -7,7 +7,7 @@ package pluginmanager
|
||||
import (
|
||||
"os"
|
||||
"path/filepath"
|
||||
"log"
|
||||
"syscall"
|
||||
)
|
||||
|
||||
type Sandbox interface {
|
||||
@@ -16,38 +16,68 @@ type Sandbox interface {
|
||||
VerifyPluginPath(path string) error
|
||||
}
|
||||
|
||||
type DefaultSandbox struct {
|
||||
pluginDir string
|
||||
type LinuxSandbox struct {
|
||||
originalDir string
|
||||
originalUmask int
|
||||
chrootDir string
|
||||
}
|
||||
|
||||
func NewDefaultSandbox(pluginDir string) *DefaultSandbox {
|
||||
absPath, err := filepath.Abs(pluginDir)
|
||||
func NewLinuxSandbox(chrootDir string) *LinuxSandbox {
|
||||
if chrootDir == "" {
|
||||
chrootDir = "./sandbox"
|
||||
}
|
||||
return &LinuxSandbox{
|
||||
chrootDir: chrootDir,
|
||||
}
|
||||
}
|
||||
|
||||
func (s *LinuxSandbox) Enable() error {
|
||||
var err error
|
||||
s.originalDir, err = os.Getwd()
|
||||
if err != nil {
|
||||
log.Printf("Error getting absolute path for plugin directory: %v", err)
|
||||
return &DefaultSandbox{pluginDir: pluginDir}
|
||||
return err
|
||||
}
|
||||
return &DefaultSandbox{
|
||||
pluginDir: absPath,
|
||||
|
||||
if err := os.MkdirAll(s.chrootDir, 0755); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
func (s *DefaultSandbox) Enable() error {
|
||||
return os.Chdir(s.pluginDir)
|
||||
}
|
||||
if err := syscall.Chroot(s.chrootDir); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := os.Chdir("/"); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
s.originalUmask = syscall.Umask(0)
|
||||
|
||||
func (s *DefaultSandbox) Disable() error {
|
||||
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)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if !filepath.HasPrefix(absPath, s.pluginDir) {
|
||||
|
||||
if !filepath.HasPrefix(absPath, s.chrootDir) {
|
||||
return ErrPluginSandboxViolation
|
||||
}
|
||||
|
||||
|
||||
return nil
|
||||
}
|
Reference in New Issue
Block a user