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);