Plugin System Overview
The s9s plugin system allows extending the functionality of s9s through modular plugins. Plugins can provide new views, overlay data on existing views, provide data to other plugins, and more.
Table of Contents
- Plugin Architecture
- Core Components
- Creating a Plugin
- Available Plugins
- Plugin Development Guidelines
- Testing Plugins
Plugin Architecture
s9s has two plugin systems:
1. Compile-time Plugin System (internal/plugin/)
The primary plugin system uses compile-time registration. Plugins implement interfaces from internal/plugin/interface.go and are registered at build time.
Base Plugin interface -- all plugins must implement:
GetInfo() Info-- metadata (name, version, author, dependencies, config schema)Init(ctx, config)-- initialize with configurationStart(ctx)/Stop(ctx)-- lifecycle managementHealth() HealthStatus-- health reporting
Specialized interfaces:
ViewPlugin-- provides custom TUI views (GetViews(),CreateView())OverlayPlugin-- overlays data on existing views (GetOverlays(),CreateOverlay())DataPlugin-- provides data to other plugins (GetDataProviders(),Subscribe(),Unsubscribe(),Query())ConfigurablePlugin-- runtime configuration support (GetConfig(),SetConfig(),ValidateConfig(),GetConfigUI())HookablePlugin-- event hooks (GetHooks(),RegisterHook())LifecycleAware-- lifecycle events (OnEnable(),OnDisable(),OnConfigChange())Prioritizable-- controls initialization order (GetPriority())
Plugin Manager (internal/plugin/manager.go) -- manages plugin lifecycle, dependencies, and health checks.
Plugin Registry (internal/plugin/registry.go) -- registers and indexes plugins, resolves dependencies.
2. Shared Library Plugin System (internal/plugins/)
The secondary system loads .so shared libraries at runtime. Plugins implement interfaces from internal/plugins/interface.go:
Base Plugin interface:
GetInfo() PluginInfo-- basic plugin info (name, version, description, author, website)Initialize(ctx, client dao.SlurmClient)-- initialize with SLURM client accessGetCommands() []Command-- provide CLI commands (Name, Description, Usage, Handler)GetViews() []View-- provide TUI views (GetName, GetTitle, Render, OnKey, Refresh, Init)GetKeyBindings() []KeyBinding-- custom key bindingsOnEvent(event Event)-- react to application events (ViewChanged, JobSubmitted, etc.)Cleanup()-- cleanup on unload
Plugin Manager (internal/plugins/interface.go: PluginManager) -- loads/unloads .so plugins from directories, sends events to all plugins.
Creating a Plugin
Basic Plugin Structure
package myplugin import ( "context" "github.com/jontk/s9s/internal/plugin" ) type MyPlugin struct { // Plugin state } func New() *MyPlugin { return &MyPlugin{} } func (p *MyPlugin) GetInfo() plugin.Info { return plugin.Info{ Name: "my-plugin", Version: "1.0.0", Description: "My awesome plugin", Author: "Your Name", License: "MIT", Requires: []string{}, // Dependencies Provides: []string{"feature1", "feature2"}, } } func (p *MyPlugin) Init(ctx context.Context, config map[string]interface{}) error { // Initialize plugin with configuration return nil } func (p *MyPlugin) Start(ctx context.Context) error { // Start plugin services return nil } func (p *MyPlugin) Stop(ctx context.Context) error { // Clean up resources return nil } func (p *MyPlugin) Health() plugin.HealthStatus { return plugin.HealthStatus{ Healthy: true, Status: "healthy", Message: "Plugin is running", } }
View Plugin Example
type MyViewPlugin struct { MyPlugin // Embed base plugin } func (p *MyViewPlugin) GetViews() []plugin.ViewInfo { return []plugin.ViewInfo{ { ID: "my-view", Name: "My View", Description: "Custom view for my data", Icon: "chart", Shortcut: "m", Category: "monitoring", }, } } func (p *MyViewPlugin) CreateView(ctx context.Context, viewID string) (plugin.View, error) { if viewID == "my-view" { return NewMyView(), nil } return nil, fmt.Errorf("unknown view: %s", viewID) }
Overlay Plugin Example
type MyOverlayPlugin struct { MyPlugin // Embed base plugin } func (p *MyOverlayPlugin) GetOverlays() []plugin.OverlayInfo { return []plugin.OverlayInfo{ { ID: "cpu-usage", Name: "CPU Usage", Description: "Adds CPU usage to job view", TargetViews: []string{"jobs"}, Priority: 100, }, } } func (p *MyOverlayPlugin) CreateOverlay(ctx context.Context, overlayID string) (plugin.Overlay, error) { if overlayID == "cpu-usage" { return NewCPUOverlay(), nil } return nil, fmt.Errorf("unknown overlay: %s", overlayID) }
Plugin Configuration
Plugins can define their configuration schema:
ConfigSchema: map[string]plugin.ConfigField{ "endpoint": { Type: "string", Description: "API endpoint URL", Default: "http://localhost:8080", Required: true, Validation: "^https?://", }, "refresh_rate": { Type: "int", Description: "Refresh rate in seconds", Default: 30, Required: false, }, }
Configuration is passed to the plugin during initialization and can be updated at runtime if the plugin implements ConfigurablePlugin.
Plugin Lifecycle
- Registration: Plugin is registered with the manager
- Initialization:
Init()is called with configuration - Start:
Start()is called to begin operations - Running: Plugin performs its functions
- Health Checks: Manager periodically calls
Health() - Stop:
Stop()is called for graceful shutdown
Available Plugins
Observability Plugin
Provides Prometheus integration for real-time metrics monitoring.
Features:
- Real-time CPU, memory, disk, and network metrics
- Job-level resource usage from cgroup-exporter
- Metric overlays on existing views
- Alert monitoring
- Historical data analysis
Configuration:
plugins: - name: observability enabled: true config: prometheus: endpoint: "http://prometheus:9090" timeout: "10s" refreshInterval: "30s" display: showOverlays: true enableAlerts: true
For detailed documentation on the observability plugin, see Observability Plugin.
Plugin Development Guidelines
- Error Handling: Always return meaningful errors
- Context Awareness: Respect context cancellation
- Resource Management: Clean up resources in
Stop() - Health Reporting: Provide accurate health status
- Configuration Validation: Validate config in
Init() - Logging: Use the debug logger for diagnostics
- Testing: Write unit tests for your plugin
Testing Plugins
func TestMyPlugin(t *testing.T) { plugin := New() // Test initialization err := plugin.Init(context.Background(), map[string]interface{}{ "endpoint": "http://test:8080", }) if err != nil { t.Fatalf("Init failed: %v", err) } // Test start err = plugin.Start(context.Background()) if err != nil { t.Fatalf("Start failed: %v", err) } // Test health health := plugin.Health() if !health.Healthy { t.Errorf("Plugin unhealthy: %s", health.Message) } // Test stop err = plugin.Stop(context.Background()) if err != nil { t.Fatalf("Stop failed: %v", err) } }
Future Enhancements
- Plugin marketplace/registry
- Plugin sandboxing
- Inter-plugin communication
- Plugin versioning and updates
- Resource limits enforcement
Next Steps
To get started developing plugins, see the Plugin Development Guide.