Plugin Lifecycle and Management
Overview
The QuantumKat Plugin SDK provides a comprehensive plugin lifecycle management system through the PluginManager class. This document covers how plugins are loaded, initialized, managed, and disposed of throughout their lifecycle.
Plugin Lifecycle Phases
1. Discovery Phase
The plugin manager scans specified directories for plugin assemblies and identifies types that implement IPlugin.
2. Dependency Resolution Phase
Dependencies between plugins are analyzed and resolved, ensuring plugins are loaded in the correct order.
3. Loading Phase
Plugin assemblies are loaded into isolated contexts to prevent conflicts.
4. Initialization Phase
Plugins are instantiated and their Initialize method is called.
5. Service Registration Phase
Plugins register their services with the dependency injection container.
6. Runtime Phase
Plugins operate normally, handling events and providing services.
7. Shutdown Phase
Threaded plugins are gracefully stopped and resources are cleaned up.
PluginManager
The PluginManager class orchestrates the entire plugin lifecycle.
Constructor
public PluginManager(
IPluginServiceProvider sharedServiceProvider,
IConfiguration configuration,
ILogger logger)
Key Methods
LoadPlugins
public void LoadPlugins(IEnumerable<string> pluginPaths, bool throwOnError = true)
Loads plugins from the specified assembly paths with dependency resolution.
Process:
- Scans each assembly for
IPluginimplementations - Extracts metadata (name, version, dependencies)
- Resolves dependency order using topological sorting
- Detects and reports circular dependencies
- Loads plugins in dependency order
- Initializes each plugin with a
PluginBootstrapContext
Parameters:
pluginPaths: Collection of paths to plugin assembliesthrowOnError: If true, throws exceptions on plugin loading failures; if false, logs warnings and continues
RegisterAllPluginServices
public void RegisterAllPluginServices()
Registers services from all loaded plugins into the shared service provider.
Threaded Plugin Management
public async Task StartAllPluginsAsync(CancellationToken cancellationToken)
public async Task StopAllPluginsAsync(CancellationToken cancellationToken)
public async Task StartPluginAsync(IThreadedPlugin plugin, CancellationToken cancellationToken)
public async Task StopPluginAsync(IThreadedPlugin plugin, CancellationToken cancellationToken)
Plugin Load Context
Each plugin is loaded in its own PluginLoadContext for isolation:
public class PluginLoadContext : AssemblyLoadContext
{
private readonly AssemblyDependencyResolver _resolver;
public PluginLoadContext(string pluginPath) : base(isCollectible: true)
{
_resolver = new AssemblyDependencyResolver(pluginPath);
}
}
Benefits of Isolation
- Prevents Version Conflicts: Different plugins can use different versions of the same dependency
- Memory Management: Plugins can be unloaded to free memory
- Security: Plugins operate in isolated contexts
- Stability: Plugin crashes don't affect other plugins
PluginBootstrapContext
The bootstrap context provides plugins with essential services and configuration:
public class PluginBootstrapContext
{
public required IConfiguration Configuration { get; init; }
public required ILogger Logger { get; init; }
public required IServiceProvider CoreServices { get; init; }
public required string PluginDirectory { get; init; }
public IPluginEventRegistry? EventRegistry { get; init; }
public IPluginServiceProvider? SharedServiceProvider { get; init; }
}
Usage in Plugin Initialization
public void Initialize(PluginBootstrapContext context)
{
// Access configuration
var myConfig = context.Configuration.GetSection("MyPlugin");
// Use logger
context.Logger.LogInformation("Plugin initializing...");
// Access core services
var dbContext = context.CoreServices.GetRequiredService<IDbContext>();
// Register for named events with async predicate
context.EventRegistry?.SubscribeToMessage(
"command-handler",
async msg => msg.Content.StartsWith("!"),
HandleCommand);
// Register additional services dynamically
context.SharedServiceProvider?.RegisterServicesAndRebuild(services =>
{
services.AddTransient<IMyDynamicService, MyDynamicService>();
});
}
Dependency Resolution
Dependency Declaration
Plugins declare dependencies using the PluginDependencies property:
public Dictionary<string, List<string>> PluginDependencies => new()
{
["CorePlugin"] = [">=1.0.0"],
["DatabasePlugin"] = [">=2.1.0", "<3.0.0"],
["UtilityPlugin"] = ["==1.5.0"]
};
Resolution Algorithm
The plugin manager uses topological sorting to resolve dependencies:
- Dependency Graph Construction: Build a graph of plugin dependencies
- Cycle Detection: Detect and report circular dependencies
- Version Validation: Verify that dependency versions satisfy constraints
- Topological Sort: Order plugins so dependencies load before dependents
Error Handling
Common dependency resolution errors:
- Missing Dependency: Required plugin not found
- Version Mismatch: Available version doesn't satisfy constraints
- Circular Dependency: Plugins have circular references
Event System
The plugin SDK includes an event system for inter-plugin communication using named subscriptions.
PluginEventRegistry
public class PluginEventRegistry : IPluginEventRegistry
{
public void SubscribeToMessage(string name, Func<IMessage, Task<bool>> predicate, Func<IMessage, Task> handler);
public void UnsubscribeFromMessage(string name);
public void ClearAllSubscriptions();
public bool IsSubscribed(string name);
public Dictionary<string, (Func<IMessage, Task<bool>> predicate, Func<IMessage, Task> handler)> GetSubscriptions();
public async Task DispatchMessageAsync(IMessage message);
}
Event Registration with Named Subscriptions
// In plugin initialization
context.EventRegistry?.SubscribeToMessage(
"hello-command",
async message => message.Content.StartsWith("!hello"),
async message => {
await message.Channel.SendMessageAsync($"Hello {message.Author.Username}!");
});
Event Dispatching
// In your application - dispatch to all registered handlers
await pluginManager.DispatchMessageAsync(discordMessage);
Subscription Management
// Check if a handler is registered
bool hasHandler = eventRegistry?.IsSubscribed("hello-command") ?? false;
// Get all subscriptions from this plugin
var subs = eventRegistry?.GetSubscriptions();
// Unregister a specific handler
eventRegistry?.UnsubscribeFromMessage("hello-command");
// Clear all handlers from this plugin
eventRegistry?.ClearAllSubscriptions();
Complete Lifecycle Example
Here's a complete example showing plugin lifecycle management:
Application Setup
using QuantumKat.PluginSDK.Core.Extensions;
var services = new ServiceCollection();
services.AddLogging();
services.AddSingleton<IMyMainService, MyMainService>();
var configuration = new ConfigurationBuilder()
.AddJsonFile("appsettings.json")
.Build();
var logger = LoggerFactory.Create(builder => builder.AddConsole())
.CreateLogger<Program>();
// Setup plugin SDK
var pluginBuilder = services.AddPluginSDK(configuration, logger);
// Load plugins
var pluginPaths = Directory.GetFiles("plugins", "*.dll");
pluginBuilder.LoadPlugins(pluginPaths);
// Build service provider
var serviceProvider = pluginBuilder.Build();
// Get plugin manager
var pluginManager = serviceProvider.GetRequiredService<PluginManager>();
// Start threaded plugins
var cancellationTokenSource = new CancellationTokenSource();
await pluginManager.StartAllPluginsAsync(cancellationTokenSource.Token);
// Application runtime...
// Graceful shutdown
await pluginManager.StopAllPluginsAsync(cancellationTokenSource.Token);
Plugin Implementation
public class ExamplePlugin : IThreadedPlugin
{
private Timer? _timer;
private ILogger? _logger;
private bool _isRunning;
public string Name => "ExamplePlugin";
public string Description => "Example threaded plugin";
public string Version => "1.0.0";
public string Author => "Plugin Developer";
public Dictionary<string, List<string>> PluginDependencies => new()
{
["CoreUtilityPlugin"] = [">=1.0.0"]
};
public void Initialize(PluginBootstrapContext context)
{
_logger = context.Logger;
_logger.LogInformation("ExamplePlugin initializing...");
// Subscribe to events with named subscriptions
context.EventRegistry?.SubscribeToMessage(
"status-command",
async msg => msg.Content == "!status",
HandleStatusCommand);
}
public void RegisterServices(IServiceCollection services)
{
services.AddSingleton<IExampleService, ExampleService>();
}
public async Task StartAsync(CancellationToken cancellationToken)
{
_logger?.LogInformation("ExamplePlugin starting...");
_isRunning = true;
// Start background timer
_timer = new Timer(DoPeriodicWork, null, TimeSpan.Zero, TimeSpan.FromMinutes(5));
await Task.CompletedTask;
}
public async Task StopAsync(CancellationToken cancellationToken)
{
_logger?.LogInformation("ExamplePlugin stopping...");
_isRunning = false;
_timer?.Dispose();
_timer = null;
await Task.CompletedTask;
}
private async Task HandleStatusCommand(IMessage message)
{
var status = _isRunning ? "Running" : "Stopped";
await message.Channel.SendMessageAsync($"ExamplePlugin status: {status}");
}
private void DoPeriodicWork(object? state)
{
if (!_isRunning) return;
_logger?.LogDebug("ExamplePlugin doing periodic work...");
// Perform background tasks
}
}
Best Practices
1. Plugin Organization
- Keep plugins focused on single responsibilities
- Use clear, descriptive names
- Organize related plugins in groups
2. Dependency Management
- Minimize dependencies when possible
- Use version ranges rather than exact versions
- Document dependency requirements
3. Resource Management
- Always implement proper cleanup in
StopAsync - Dispose of resources in threaded plugins
- Handle cancellation tokens properly
4. Error Handling
- Log errors appropriately
- Don't let plugin errors crash the application
- Provide meaningful error messages
5. Performance
- Avoid blocking operations in initialization
- Use async/await properly in threaded plugins
- Monitor memory usage in long-running plugins
6. Testing
- Test plugins in isolation
- Verify dependency resolution
- Test startup and shutdown procedures