using System.Reflection;
using Microsoft.Extensions.Logging;
using SilkroadBot.Plugins.SDK.Attributes;
using SilkroadBot.Plugins.SDK.Interfaces;
namespace SilkroadBot.Core.Plugins;
///
/// Manages plugin lifecycle: discovery, loading, initialization, and unloading.
/// Plugins are loaded from DLL assemblies in a configured directory.
///
public class PluginManager : IDisposable
{
private readonly ILogger _logger;
private readonly IServiceProvider _serviceProvider;
private readonly Dictionary _plugins = new();
private readonly string _pluginDirectory;
public IReadOnlyDictionary Plugins => _plugins;
public event Action? PluginLoaded;
public event Action? PluginUnloaded;
public event Action? PluginError;
public PluginManager(
string pluginDirectory,
IServiceProvider serviceProvider,
ILogger logger)
{
_pluginDirectory = pluginDirectory;
_serviceProvider = serviceProvider;
_logger = logger;
if (!Directory.Exists(_pluginDirectory))
Directory.CreateDirectory(_pluginDirectory);
}
///
/// Discover and load all plugins from the plugin directory.
///
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);
}
///
/// Load a single plugin from a DLL file.
///
public async Task 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();
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;
}
///
/// Start a specific plugin.
///
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);
}
///
/// Stop a specific plugin.
///
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);
}
///
/// Start all loaded plugins.
///
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);
}
}
}
///
/// Stop all running plugins.
///
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);
}
}
}
///
/// Unload a plugin completely.
///
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();
}
}
///
/// Represents a loaded plugin instance with metadata.
///
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; }
}
///
/// Plugin metadata from attributes.
///
public record PluginMetadata(string Id, string Name, string Description);