mirror of
https://github.com/matt-dunleavy/plugin-manager.git
synced 2025-10-05 15:26:59 +08:00
Initial commit
This commit is contained in:
9
LICENSE
Normal file
9
LICENSE
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
MIT License
|
||||||
|
|
||||||
|
Copyright (C) 2024 Matthew James Dunleavy <matt@dunleavy.co>
|
||||||
|
|
||||||
|
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the “Software”), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
|
||||||
|
|
||||||
|
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
|
||||||
|
|
||||||
|
THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
166
README.md
Normal file
166
README.md
Normal file
@@ -0,0 +1,166 @@
|
|||||||
|
# Go Plugin Manager
|
||||||
|
|
||||||
|
A robust and flexible plugin management system for Go applications.
|
||||||
|
|
||||||
|
## Features
|
||||||
|
|
||||||
|
- Dynamic loading and unloading of plugins
|
||||||
|
- Plugin versioning and dependency management
|
||||||
|
- Hot-reloading of plugins
|
||||||
|
- Event system for plugin lifecycle events
|
||||||
|
- Basic sandboxing for improved security
|
||||||
|
- Metrics collection for plugin performance
|
||||||
|
|
||||||
|
## Installation
|
||||||
|
|
||||||
|
To use this plugin manager in your Go project, run:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
go get github.com/roguedynamic/plugin-manager
|
||||||
|
```
|
||||||
|
|
||||||
|
## Usage
|
||||||
|
|
||||||
|
### Creating a plugin
|
||||||
|
|
||||||
|
Plugins must implement the `Plugin` interface:
|
||||||
|
|
||||||
|
```go
|
||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
pm "github.com/roguedynamic/plugin-manager"
|
||||||
|
)
|
||||||
|
|
||||||
|
type MyPlugin struct{}
|
||||||
|
|
||||||
|
func (p *MyPlugin) Metadata() pm.PluginMetadata {
|
||||||
|
return pm.PluginMetadata{
|
||||||
|
Name: "MyPlugin",
|
||||||
|
Version: "1.0.0",
|
||||||
|
Dependencies: []string{},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *MyPlugin) Init() error {
|
||||||
|
fmt.Println("MyPlugin initialized")
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *MyPlugin) Execute() error {
|
||||||
|
fmt.Println("MyPlugin executed")
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *MyPlugin) Shutdown() error {
|
||||||
|
fmt.Println("MyPlugin shut down")
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
var Plugin MyPlugin
|
||||||
|
```
|
||||||
|
|
||||||
|
### Compiling Plugins
|
||||||
|
|
||||||
|
Compile your plugin with:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
go build -buildmode=plugin -o myplugin.so myplugin.go
|
||||||
|
```
|
||||||
|
|
||||||
|
### Using the Plugin Manager
|
||||||
|
|
||||||
|
Here's an example of how to use the plugin manager in your application:
|
||||||
|
|
||||||
|
```go
|
||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"log"
|
||||||
|
pm "github.com/roguedynamic/plugin-manager"
|
||||||
|
)
|
||||||
|
|
||||||
|
func main() {
|
||||||
|
// Create a new plugin manager
|
||||||
|
manager, err := pm.NewManager("config.json", "./plugins")
|
||||||
|
if err != nil {
|
||||||
|
log.Fatalf("Failed to create plugin manager: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Load a plugin
|
||||||
|
err = manager.LoadPlugin("./plugins/myplugin.so")
|
||||||
|
if err != nil {
|
||||||
|
log.Fatalf("Failed to load plugin: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Execute a plugin
|
||||||
|
err = manager.ExecutePlugin("MyPlugin")
|
||||||
|
if err != nil {
|
||||||
|
log.Fatalf("Failed to execute plugin: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Hot-reload a plugin
|
||||||
|
err = manager.HotReload("MyPlugin", "./plugins/myplugin_v2.so")
|
||||||
|
if err != nil {
|
||||||
|
log.Fatalf("Failed to hot-reload plugin: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Unload a plugin
|
||||||
|
err = manager.UnloadPlugin("MyPlugin")
|
||||||
|
if err != nil {
|
||||||
|
log.Fatalf("Failed to unload plugin: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Subscribe to plugin events
|
||||||
|
manager.GetEventBus().Subscribe("PluginLoaded", func(e pm.Event) {
|
||||||
|
fmt.Printf("Plugin loaded: %s\n", e.(pm.PluginLoadedEvent).PluginName)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Configuration
|
||||||
|
|
||||||
|
The plugin manager uses a JSON configuration file to keep track of enabled plugins. Here's an example `config.json`:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"enabled": {
|
||||||
|
"MyPlugin": true,
|
||||||
|
"AnotherPlugin": false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## API Reference
|
||||||
|
|
||||||
|
### 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`
|
||||||
|
|
||||||
|
### EventBus
|
||||||
|
|
||||||
|
- `Subscribe(eventName string, handler EventHandler)`
|
||||||
|
- `Publish(event Event)`
|
||||||
|
|
||||||
|
### Sandbox
|
||||||
|
|
||||||
|
- `Enable() error`
|
||||||
|
- `Disable() error`
|
||||||
|
- `VerifyPluginPath(path string) error`
|
||||||
|
|
||||||
|
## Contributing
|
||||||
|
|
||||||
|
Contributions are welcome! Please feel free to submit a Pull Request.
|
||||||
|
|
||||||
|
## License
|
||||||
|
|
||||||
|
This project is licensed under the MIT License - see the [LICENSE](LICENSE) file for details.
|
75
config.go
Normal file
75
config.go
Normal file
@@ -0,0 +1,75 @@
|
|||||||
|
package pluginmanager
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"os"
|
||||||
|
"sync"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Config struct {
|
||||||
|
Enabled map[string]bool `json:"enabled"`
|
||||||
|
path string
|
||||||
|
mu sync.RWMutex
|
||||||
|
}
|
||||||
|
|
||||||
|
func LoadConfig(path string) (*Config, error) {
|
||||||
|
config := &Config{
|
||||||
|
Enabled: make(map[string]bool),
|
||||||
|
path: path,
|
||||||
|
}
|
||||||
|
|
||||||
|
file, err := os.ReadFile(path)
|
||||||
|
if err != nil {
|
||||||
|
if os.IsNotExist(err) {
|
||||||
|
return config, nil
|
||||||
|
}
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := json.Unmarshal(file, &config); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return config, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Config) Save() error {
|
||||||
|
c.mu.RLock()
|
||||||
|
defer c.mu.RUnlock()
|
||||||
|
|
||||||
|
data, err := json.MarshalIndent(c, "", " ")
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
return os.WriteFile(c.path, data, 0644)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Config) EnablePlugin(name string) error {
|
||||||
|
c.mu.Lock()
|
||||||
|
defer c.mu.Unlock()
|
||||||
|
|
||||||
|
c.Enabled[name] = true
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Config) DisablePlugin(name string) error {
|
||||||
|
c.mu.Lock()
|
||||||
|
defer c.mu.Unlock()
|
||||||
|
|
||||||
|
c.Enabled[name] = false
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Config) EnabledPlugins() []string {
|
||||||
|
c.mu.RLock()
|
||||||
|
defer c.mu.RUnlock()
|
||||||
|
|
||||||
|
var enabled []string
|
||||||
|
for name, isEnabled := range c.Enabled {
|
||||||
|
if isEnabled {
|
||||||
|
enabled = append(enabled, name)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return enabled
|
||||||
|
}
|
13
errors.go
Normal file
13
errors.go
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
package pluginmanager
|
||||||
|
|
||||||
|
import "errors"
|
||||||
|
|
||||||
|
var (
|
||||||
|
ErrPluginAlreadyLoaded = errors.New("plugin already loaded")
|
||||||
|
ErrInvalidPluginInterface = errors.New("invalid plugin interface")
|
||||||
|
ErrPluginNotFound = errors.New("plugin not found")
|
||||||
|
ErrIncompatibleVersion = errors.New("incompatible plugin version")
|
||||||
|
ErrMissingDependency = errors.New("missing plugin dependency")
|
||||||
|
ErrCircularDependency = errors.New("circular plugin dependency detected")
|
||||||
|
ErrPluginSandboxViolation = errors.New("plugin attempted to violate sandbox")
|
||||||
|
)
|
60
event.go
Normal file
60
event.go
Normal file
@@ -0,0 +1,60 @@
|
|||||||
|
package pluginmanager
|
||||||
|
|
||||||
|
import (
|
||||||
|
"sync"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Event interface {
|
||||||
|
Name() string
|
||||||
|
}
|
||||||
|
|
||||||
|
type PluginLoadedEvent struct {
|
||||||
|
PluginName string
|
||||||
|
}
|
||||||
|
|
||||||
|
func (e PluginLoadedEvent) Name() string {
|
||||||
|
return "PluginLoaded"
|
||||||
|
}
|
||||||
|
|
||||||
|
type PluginUnloadedEvent struct {
|
||||||
|
PluginName string
|
||||||
|
}
|
||||||
|
|
||||||
|
func (e PluginUnloadedEvent) Name() string {
|
||||||
|
return "PluginUnloaded"
|
||||||
|
}
|
||||||
|
|
||||||
|
type PluginHotReloadedEvent struct {
|
||||||
|
PluginName string
|
||||||
|
}
|
||||||
|
|
||||||
|
func (e PluginHotReloadedEvent) Name() string {
|
||||||
|
return "PluginHotReloaded"
|
||||||
|
}
|
||||||
|
|
||||||
|
type EventHandler func(Event)
|
||||||
|
|
||||||
|
type EventBus struct {
|
||||||
|
handlers map[string][]EventHandler
|
||||||
|
mu sync.RWMutex
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewEventBus() *EventBus {
|
||||||
|
return &EventBus{
|
||||||
|
handlers: make(map[string][]EventHandler),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (eb *EventBus) Subscribe(eventName string, handler EventHandler) {
|
||||||
|
eb.mu.Lock()
|
||||||
|
defer eb.mu.Unlock()
|
||||||
|
eb.handlers[eventName] = append(eb.handlers[eventName], handler)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (eb *EventBus) Publish(event Event) {
|
||||||
|
eb.mu.RLock()
|
||||||
|
defer eb.mu.RUnlock()
|
||||||
|
for _, handler := range eb.handlers[event.Name()] {
|
||||||
|
go handler(event)
|
||||||
|
}
|
||||||
|
}
|
83
examples/app/main.go
Normal file
83
examples/app/main.go
Normal file
@@ -0,0 +1,83 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"log"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
pm "github.com/matt-dunleavy/plugin-manager"
|
||||||
|
)
|
||||||
|
|
||||||
|
func main() {
|
||||||
|
// Create a new plugin manager
|
||||||
|
manager, err := pm.NewManager("../plugins.json", "../plugins")
|
||||||
|
if err != nil {
|
||||||
|
log.Fatalf("Failed to create plugin manager: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Subscribe to plugin events
|
||||||
|
manager.GetEventBus().Subscribe("PluginLoaded", func(e pm.Event) {
|
||||||
|
fmt.Printf("Event: Plugin loaded - %s\n", e.(pm.PluginLoadedEvent).PluginName)
|
||||||
|
})
|
||||||
|
manager.GetEventBus().Subscribe("PluginUnloaded", func(e pm.Event) {
|
||||||
|
fmt.Printf("Event: Plugin unloaded - %s\n", e.(pm.PluginUnloadedEvent).PluginName)
|
||||||
|
})
|
||||||
|
|
||||||
|
// Load plugins
|
||||||
|
err = manager.LoadPlugin("../plugins/hello.so")
|
||||||
|
if err != nil {
|
||||||
|
log.Printf("Failed to load hello plugin: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
err = manager.LoadPlugin("../plugins/math.so")
|
||||||
|
if err != nil {
|
||||||
|
log.Printf("Failed to load math plugin: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// List loaded plugins
|
||||||
|
plugins := manager.ListPlugins()
|
||||||
|
fmt.Println("Loaded plugins:", plugins)
|
||||||
|
|
||||||
|
// Execute plugins
|
||||||
|
err = manager.ExecutePlugin("HelloPlugin")
|
||||||
|
if err != nil {
|
||||||
|
log.Printf("Failed to execute HelloPlugin: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
err = manager.ExecutePlugin("MathPlugin")
|
||||||
|
if err != nil {
|
||||||
|
log.Printf("Failed to execute MathPlugin: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get plugin stats
|
||||||
|
helloStats := manager.GetPluginStats("HelloPlugin")
|
||||||
|
mathStats := manager.GetPluginStats("MathPlugin")
|
||||||
|
|
||||||
|
fmt.Printf("HelloPlugin stats: %+v\n", helloStats)
|
||||||
|
fmt.Printf("MathPlugin stats: %+v\n", mathStats)
|
||||||
|
|
||||||
|
// Hot-reload HelloPlugin
|
||||||
|
time.Sleep(2 * time.Second) // Wait to simulate some time passing
|
||||||
|
fmt.Println("\nHot-reloading HelloPlugin...")
|
||||||
|
err = manager.HotReload("HelloPlugin", "../plugins/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
|
||||||
|
err = manager.UnloadPlugin("HelloPlugin")
|
||||||
|
if err != nil {
|
||||||
|
log.Printf("Failed to unload HelloPlugin: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
err = manager.UnloadPlugin("MathPlugin")
|
||||||
|
if err != nil {
|
||||||
|
log.Printf("Failed to unload MathPlugin: %v", err)
|
||||||
|
}
|
||||||
|
}
|
6
examples/plugins.json
Normal file
6
examples/plugins.json
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
{
|
||||||
|
"enabled": {
|
||||||
|
"HelloPlugin": true,
|
||||||
|
"MathPlugin": true
|
||||||
|
}
|
||||||
|
}
|
36
examples/plugins/hello/hello.go
Normal file
36
examples/plugins/hello/hello.go
Normal file
@@ -0,0 +1,36 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
pm "github.com/matt-dunleavy/plugin-manager"
|
||||||
|
)
|
||||||
|
|
||||||
|
type HelloPlugin struct {
|
||||||
|
greeting string
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *HelloPlugin) Metadata() pm.PluginMetadata {
|
||||||
|
return pm.PluginMetadata{
|
||||||
|
Name: "HelloPlugin",
|
||||||
|
Version: "1.0.0",
|
||||||
|
Dependencies: []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)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *HelloPlugin) Shutdown() error {
|
||||||
|
fmt.Println("HelloPlugin shut down")
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
var Plugin HelloPlugin
|
37
examples/plugins/math/math.go
Normal file
37
examples/plugins/math/math.go
Normal file
@@ -0,0 +1,37 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
pm "github.com/matt-dunleavy/plugin-manager"
|
||||||
|
)
|
||||||
|
|
||||||
|
type MathPlugin struct{}
|
||||||
|
|
||||||
|
func (p *MathPlugin) Metadata() pm.PluginMetadata {
|
||||||
|
return pm.PluginMetadata{
|
||||||
|
Name: "MathPlugin",
|
||||||
|
Version: "1.0.0",
|
||||||
|
Dependencies: []string{},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *MathPlugin) Init() error {
|
||||||
|
fmt.Println("MathPlugin initialized")
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
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)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *MathPlugin) Shutdown() error {
|
||||||
|
fmt.Println("MathPlugin shut down")
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
var Plugin MathPlugin
|
3
go.mod
Normal file
3
go.mod
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
module github.com/matt-dunleavy/plugin-manager
|
||||||
|
|
||||||
|
go 1.22.4
|
200
manager.go
Normal file
200
manager.go
Normal file
@@ -0,0 +1,200 @@
|
|||||||
|
package pluginmanager
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"sync"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Manager struct {
|
||||||
|
plugins map[string]Plugin
|
||||||
|
config *Config
|
||||||
|
dependencies map[string][]string
|
||||||
|
stats map[string]*PluginStats
|
||||||
|
eventBus *EventBus
|
||||||
|
sandbox Sandbox
|
||||||
|
mu sync.RWMutex
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewManager(configPath string, pluginDir string) (*Manager, error) {
|
||||||
|
config, err := LoadConfig(configPath)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return &Manager{
|
||||||
|
plugins: make(map[string]Plugin),
|
||||||
|
config: config,
|
||||||
|
dependencies: make(map[string][]string),
|
||||||
|
stats: make(map[string]*PluginStats),
|
||||||
|
eventBus: NewEventBus(),
|
||||||
|
sandbox: NewDefaultSandbox(pluginDir),
|
||||||
|
}, 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()
|
||||||
|
|
||||||
|
metadata := plugin.Metadata()
|
||||||
|
if _, exists := m.plugins[metadata.Name]; exists {
|
||||||
|
return ErrPluginAlreadyLoaded
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := m.checkDependencies(metadata); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
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()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *Manager) UnloadPlugin(name string) error {
|
||||||
|
m.mu.Lock()
|
||||||
|
defer m.mu.Unlock()
|
||||||
|
|
||||||
|
plugin, exists := m.plugins[name]
|
||||||
|
if !exists {
|
||||||
|
return ErrPluginNotFound
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := plugin.Shutdown(); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
delete(m.plugins, name)
|
||||||
|
delete(m.dependencies, name)
|
||||||
|
delete(m.stats, name)
|
||||||
|
|
||||||
|
m.eventBus.Publish(PluginUnloadedEvent{PluginName: name})
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *Manager) ExecutePlugin(name string) error {
|
||||||
|
m.mu.RLock()
|
||||||
|
plugin, exists := m.plugins[name]
|
||||||
|
stats := m.stats[name]
|
||||||
|
m.mu.RUnlock()
|
||||||
|
|
||||||
|
if !exists {
|
||||||
|
return ErrPluginNotFound
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := m.sandbox.Enable(); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
defer m.sandbox.Disable()
|
||||||
|
|
||||||
|
start := time.Now()
|
||||||
|
err := plugin.Execute()
|
||||||
|
executionTime := time.Since(start)
|
||||||
|
|
||||||
|
m.mu.Lock()
|
||||||
|
stats.ExecutionCount++
|
||||||
|
stats.LastExecutionTime = executionTime
|
||||||
|
stats.TotalExecutionTime += executionTime
|
||||||
|
m.mu.Unlock()
|
||||||
|
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
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 {
|
||||||
|
return ErrPluginNotFound
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := oldPlugin.Shutdown(); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
m.plugins[name] = newPlugin
|
||||||
|
m.eventBus.Publish(PluginHotReloadedEvent{PluginName: name})
|
||||||
|
|
||||||
|
return newPlugin.Init()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *Manager) EnablePlugin(name string) error {
|
||||||
|
if err := m.config.EnablePlugin(name); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return m.config.Save()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *Manager) DisablePlugin(name string) error {
|
||||||
|
if err := m.config.DisablePlugin(name); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return m.config.Save()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *Manager) LoadEnabledPlugins(pluginDir string) error {
|
||||||
|
enabled := m.config.EnabledPlugins()
|
||||||
|
for _, name := range enabled {
|
||||||
|
path := fmt.Sprintf("%s/%s.so", pluginDir, name)
|
||||||
|
if err := m.LoadPlugin(path); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
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
|
||||||
|
}
|
46
plugin.go
Normal file
46
plugin.go
Normal file
@@ -0,0 +1,46 @@
|
|||||||
|
package pluginmanager
|
||||||
|
|
||||||
|
import (
|
||||||
|
"plugin"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
type PluginMetadata struct {
|
||||||
|
Name string
|
||||||
|
Version string
|
||||||
|
Dependencies []string
|
||||||
|
}
|
||||||
|
|
||||||
|
type Plugin interface {
|
||||||
|
Metadata() PluginMetadata
|
||||||
|
Init() error
|
||||||
|
Execute() error
|
||||||
|
Shutdown() error
|
||||||
|
}
|
||||||
|
|
||||||
|
type PluginStats struct {
|
||||||
|
ExecutionCount int64
|
||||||
|
LastExecutionTime time.Duration
|
||||||
|
TotalExecutionTime time.Duration
|
||||||
|
}
|
||||||
|
|
||||||
|
const PluginSymbol = "Plugin"
|
||||||
|
|
||||||
|
func LoadPlugin(path string) (Plugin, error) {
|
||||||
|
p, err := plugin.Open(path)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
symPlugin, err := p.Lookup(PluginSymbol)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
plugin, ok := symPlugin.(Plugin)
|
||||||
|
if !ok {
|
||||||
|
return nil, ErrInvalidPluginInterface
|
||||||
|
}
|
||||||
|
|
||||||
|
return plugin, nil
|
||||||
|
}
|
43
sandbox.go
Normal file
43
sandbox.go
Normal file
@@ -0,0 +1,43 @@
|
|||||||
|
package pluginmanager
|
||||||
|
|
||||||
|
import (
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Sandbox interface {
|
||||||
|
Enable() error
|
||||||
|
Disable() error
|
||||||
|
VerifyPluginPath(path string) error
|
||||||
|
}
|
||||||
|
|
||||||
|
type DefaultSandbox struct {
|
||||||
|
pluginDir string
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewDefaultSandbox(pluginDir string) *DefaultSandbox {
|
||||||
|
return &DefaultSandbox{
|
||||||
|
pluginDir: pluginDir,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *DefaultSandbox) Enable() error {
|
||||||
|
return os.Chdir(s.pluginDir)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *DefaultSandbox) Disable() error {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *DefaultSandbox) VerifyPluginPath(path string) error {
|
||||||
|
absPath, err := filepath.Abs(path)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
if !filepath.HasPrefix(absPath, s.pluginDir) {
|
||||||
|
return ErrPluginSandboxViolation
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
Reference in New Issue
Block a user