using System.Reflection; using System.Runtime.Loader; using System.Text.Json; using Purrse.Plugins.Abstractions; namespace Purrse.Api.Services; public class PluginService { private readonly ILogger _logger; private readonly string _pluginsPath; private readonly Dictionary _loadedPlugins = new(); public PluginService(ILogger logger, IConfiguration config) { _logger = logger; _pluginsPath = config["Plugins:Path"] ?? Path.Combine(AppContext.BaseDirectory, "plugins"); } public IEnumerable GetPlugins() => _loadedPlugins.Values.Select(p => p.Instance); public IEnumerable GetPlugins() where T : IPlugin => _loadedPlugins.Values.Select(p => p.Instance).OfType(); public async Task DiscoverAndLoadPluginsAsync() { if (!Directory.Exists(_pluginsPath)) { Directory.CreateDirectory(_pluginsPath); return; } foreach (var dir in Directory.GetDirectories(_pluginsPath)) { var manifestPath = Path.Combine(dir, "plugin.json"); if (!File.Exists(manifestPath)) continue; try { var json = await File.ReadAllTextAsync(manifestPath); var manifest = JsonSerializer.Deserialize(json); if (manifest == null) continue; await LoadPluginAsync(dir, manifest); } catch (Exception ex) { _logger.LogError(ex, "Failed to load plugin from {Directory}", dir); } } } private async Task LoadPluginAsync(string directory, PluginManifest manifest) { var assemblyPath = Path.Combine(directory, manifest.EntryAssembly); if (!File.Exists(assemblyPath)) { _logger.LogWarning("Plugin assembly not found: {Path}", assemblyPath); return; } var loadContext = new AssemblyLoadContext(manifest.Id, isCollectible: true); var assembly = loadContext.LoadFromAssemblyPath(assemblyPath); var pluginType = assembly.GetType(manifest.EntryType); if (pluginType == null) { _logger.LogWarning("Plugin type not found: {Type}", manifest.EntryType); loadContext.Unload(); return; } if (Activator.CreateInstance(pluginType) is not IPlugin plugin) { _logger.LogWarning("Type {Type} does not implement IPlugin", manifest.EntryType); loadContext.Unload(); return; } var context = new DefaultPluginContext(directory); await plugin.InitializeAsync(context); _loadedPlugins[manifest.Id] = new LoadedPlugin(manifest, plugin, loadContext); _logger.LogInformation("Loaded plugin: {Name} v{Version}", manifest.Name, manifest.Version); } public async Task UnloadPluginAsync(string pluginId) { if (_loadedPlugins.TryGetValue(pluginId, out var loaded)) { await loaded.Instance.ShutdownAsync(); loaded.LoadContext.Unload(); _loadedPlugins.Remove(pluginId); } } private record LoadedPlugin(PluginManifest Manifest, IPlugin Instance, AssemblyLoadContext LoadContext); } internal class DefaultPluginContext : IPluginContext { public string PluginDataDirectory { get; } public IServiceProvider Services { get; } = null!; public DefaultPluginContext(string dataDir) { PluginDataDirectory = dataDir; } public void Log(string message, Purrse.Plugins.Abstractions.LogLevel level = Purrse.Plugins.Abstractions.LogLevel.Information) { Console.WriteLine($"[{level}] {message}"); } }