SilkroadBot / src /SilkroadBot.Core /Plugins /PluginManager.cs
Ahmedramadan24's picture
Add src/SilkroadBot.Core/Plugins/PluginManager.cs
6a53d63 verified
using System.Reflection;
using Microsoft.Extensions.Logging;
using SilkroadBot.Plugins.SDK.Attributes;
using SilkroadBot.Plugins.SDK.Interfaces;
namespace SilkroadBot.Core.Plugins;
/// <summary>
/// Manages plugin lifecycle: discovery, loading, initialization, and unloading.
/// Plugins are loaded from DLL assemblies in a configured directory.
/// </summary>
public class PluginManager : IDisposable
{
private readonly ILogger<PluginManager> _logger;
private readonly IServiceProvider _serviceProvider;
private readonly Dictionary<string, PluginInstance> _plugins = new();
private readonly string _pluginDirectory;
public IReadOnlyDictionary<string, PluginInstance> Plugins => _plugins;
public event Action<PluginInstance>? PluginLoaded;
public event Action<PluginInstance>? PluginUnloaded;
public event Action<string, Exception>? PluginError;
public PluginManager(
string pluginDirectory,
IServiceProvider serviceProvider,
ILogger<PluginManager> logger)
{
_pluginDirectory = pluginDirectory;
_serviceProvider = serviceProvider;
_logger = logger;
if (!Directory.Exists(_pluginDirectory))
Directory.CreateDirectory(_pluginDirectory);
}
/// <summary>
/// Discover and load all plugins from the plugin directory.
/// </summary>
public async Task DiscoverAndLoadAsync(IPluginContext context)
{
_logger.LogInformation("Scanning for plugins in: {Dir}", _pluginDirectory);
var dllFiles = Directory.GetFiles(_pluginDirectory, "*.dll", SearchOption.AllDirectories);
foreach (var dll in dllFiles)
{
try
{
await LoadPluginFromAssemblyAsync(dll, context);
}
catch (Exception ex)
{
_logger.LogError(ex, "Failed to load plugin from {Dll}", dll);
PluginError?.Invoke(dll, ex);
}
}
_logger.LogInformation("Loaded {Count} plugins", _plugins.Count);
}
/// <summary>
/// Load a single plugin from a DLL file.
/// </summary>
public async Task<PluginInstance?> LoadPluginFromAssemblyAsync(string dllPath, IPluginContext context)
{
var assembly = Assembly.LoadFrom(dllPath);
var pluginTypes = assembly.GetTypes()
.Where(t => typeof(IPlugin).IsAssignableFrom(t) && !t.IsAbstract && !t.IsInterface)
.ToList();
PluginInstance? lastLoaded = null;
foreach (var type in pluginTypes)
{
var attr = type.GetCustomAttribute<PluginAttribute>();
var plugin = (IPlugin)Activator.CreateInstance(type)!;
var instance = new PluginInstance
{
Plugin = plugin,
Assembly = assembly,
FilePath = dllPath,
Metadata = attr != null
? new PluginMetadata(attr.Id, attr.Name, attr.Description)
: new PluginMetadata(plugin.Id, plugin.Name, plugin.Description)
};
_logger.LogInformation("Loading plugin: {Name} v{Version} by {Author}",
plugin.Name, plugin.Version, plugin.Author);
await plugin.InitializeAsync(context);
_plugins[plugin.Id] = instance;
PluginLoaded?.Invoke(instance);
lastLoaded = instance;
}
return lastLoaded;
}
/// <summary>
/// Start a specific plugin.
/// </summary>
public async Task StartPluginAsync(string pluginId, CancellationToken ct = default)
{
if (!_plugins.TryGetValue(pluginId, out var instance))
throw new KeyNotFoundException($"Plugin '{pluginId}' not found");
await instance.Plugin.StartAsync(ct);
instance.IsRunning = true;
_logger.LogInformation("Started plugin: {Name}", instance.Plugin.Name);
}
/// <summary>
/// Stop a specific plugin.
/// </summary>
public async Task StopPluginAsync(string pluginId)
{
if (!_plugins.TryGetValue(pluginId, out var instance))
throw new KeyNotFoundException($"Plugin '{pluginId}' not found");
await instance.Plugin.StopAsync();
instance.IsRunning = false;
_logger.LogInformation("Stopped plugin: {Name}", instance.Plugin.Name);
}
/// <summary>
/// Start all loaded plugins.
/// </summary>
public async Task StartAllAsync(CancellationToken ct = default)
{
foreach (var kv in _plugins)
{
try { await StartPluginAsync(kv.Key, ct); }
catch (Exception ex)
{
_logger.LogError(ex, "Failed to start plugin {Id}", kv.Key);
PluginError?.Invoke(kv.Key, ex);
}
}
}
/// <summary>
/// Stop all running plugins.
/// </summary>
public async Task StopAllAsync()
{
foreach (var kv in _plugins.Where(p => p.Value.IsRunning))
{
try { await StopPluginAsync(kv.Key); }
catch (Exception ex)
{
_logger.LogError(ex, "Failed to stop plugin {Id}", kv.Key);
}
}
}
/// <summary>
/// Unload a plugin completely.
/// </summary>
public async Task UnloadPluginAsync(string pluginId)
{
if (!_plugins.TryGetValue(pluginId, out var instance))
return;
if (instance.IsRunning)
await StopPluginAsync(pluginId);
instance.Plugin.Dispose();
_plugins.Remove(pluginId);
PluginUnloaded?.Invoke(instance);
_logger.LogInformation("Unloaded plugin: {Name}", instance.Metadata.Name);
}
public void Dispose()
{
foreach (var kv in _plugins)
kv.Value.Plugin.Dispose();
_plugins.Clear();
}
}
/// <summary>
/// Represents a loaded plugin instance with metadata.
/// </summary>
public class PluginInstance
{
public required IPlugin Plugin { get; init; }
public required Assembly Assembly { get; init; }
public required string FilePath { get; init; }
public required PluginMetadata Metadata { get; init; }
public bool IsRunning { get; set; }
}
/// <summary>
/// Plugin metadata from attributes.
/// </summary>
public record PluginMetadata(string Id, string Name, string Description);