Initial commit

This commit is contained in:
Matt Dunleavy
2024-07-06 03:14:25 -04:00
commit 1bd3d1fce5
13 changed files with 777 additions and 0 deletions

9
LICENSE Normal file
View 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
View 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
View 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
View 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
View 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
View 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
View File

@@ -0,0 +1,6 @@
{
"enabled": {
"HelloPlugin": true,
"MathPlugin": true
}
}

View 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

View 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
View File

@@ -0,0 +1,3 @@
module github.com/matt-dunleavy/plugin-manager
go 1.22.4

200
manager.go Normal file
View 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
View 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
View 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
}