--- phase: 06-gateway-plugin-research plan: 01 type: execute wave: 1 depends_on: [] files_modified: [] autonomous: true requirements: [PLUG-01, PLUG-02] must_haves: truths: - "插件可以从指定目录动态加载" - "插件在独立的 AssemblyLoadContext 中运行" - "插件可以被卸载并释放内存" artifacts: - path: "src/yarpgateway/Plugins/PluginLoadContext.cs" provides: "ALC 隔离机制" - path: "src/yarpgateway/Plugins/PluginLoader.cs" provides: "插件发现和加载" - path: "src/yarpgateway/Plugins/PluginHost.cs" provides: "插件生命周期管理" - path: "tests/YarpGateway.Tests/Unit/Plugins/PluginLoadTests.cs" provides: "加载/卸载验证" key_links: - from: "PluginLoader" to: "PluginLoadContext" via: "实例化并加载程序集" - from: "PluginHost" to: "PluginLoader" via: "协调插件生命周期" --- # 计划 01:插件加载基础设施 实现插件动态加载基础设施,包括 AssemblyLoadContext 隔离、插件发现和生命周期管理。 **目的:** 为网关提供安全的插件加载机制,支持隔离和热重载。 **产出:** 可工作的插件加载系统,含单元测试。 @/Users/mac/.config/opencode/get-shit-done/workflows/execute-plan.md @/Users/mac/.config/opencode/get-shit-done/templates/summary.md @.planning/PROJECT.md @.planning/ROADMAP.md @.planning/STATE.md @.planning/phases/006-gateway-plugin-research/006-RESEARCH.md # 现有基础设施 @src/Fengling.Gateway.Plugin.Abstractions/IGatewayPlugin.cs @src/yarpgateway/Program.cs ```csharp // Fengling.Gateway.Plugin.Abstractions public interface IGatewayPlugin { string Name { get; } string Version { get; } string? Description { get; } Task OnLoadAsync(); Task OnUnloadAsync(); } ``` Task 1: 创建 PluginLoadContext 隔离机制 src/yarpgateway/Plugins/PluginLoadContext.cs, tests/YarpGateway.Tests/Unit/Plugins/PluginLoadContextTests.cs - Test 1: 加载插件程序集到独立 ALC - Test 2: 共享契约程序集使用默认 ALC - Test 3: 卸载 ALC 后内存被回收 创建可卸载的 AssemblyLoadContext: 1. 创建 `src/yarpgateway/Plugins/PluginLoadContext.cs` 2. 继承 AssemblyLoadContext,设置 isCollectible: true 3. 使用 AssemblyDependencyResolver 解析依赖 4. 关键:共享 `Fengling.Gateway.Plugin.Abstractions` 到默认 ALC ```csharp public sealed class PluginLoadContext : AssemblyLoadContext { private readonly AssemblyDependencyResolver _resolver; private readonly string _sharedAssemblyName = "Fengling.Gateway.Plugin.Abstractions"; public PluginLoadContext(string pluginPath) : base(isCollectible: true) { _resolver = new AssemblyDependencyResolver(pluginPath); } protected override Assembly? Load(AssemblyName assemblyName) { if (assemblyName.Name == _sharedAssemblyName) return null; // 使用默认 ALC var path = _resolver.ResolveAssemblyToPath(assemblyName); return path != null ? LoadFromAssemblyPath(path) : null; } protected override IntPtr LoadUnmanagedDll(string unmanagedDllName) { var path = _resolver.ResolveUnmanagedDllToPath(unmanagedDllName); return path != null ? LoadUnmanagedDllFromPath(path) : IntPtr.Zero; } } ``` **注意**:先写测试,确保卸载验证通过。 dotnet test tests/YarpGateway.Tests --filter "FullyQualifiedName~PluginLoadContextTests" --no-build - PluginLoadContext 类存在 - 测试验证隔离和卸载 - 构建通过 Task 2: 创建 PluginLoader 发现和加载逻辑 src/yarpgateway/Plugins/PluginLoader.cs, src/yarpgateway/Plugins/DiscoveredPlugin.cs, tests/YarpGateway.Tests/Unit/Plugins/PluginLoaderTests.cs - Test 1: 从目录发现插件程序集 - Test 2: 加载插件并返回 IGatewayPlugin 实例 - Test 3: 处理无效插件(返回 null 或异常) 创建插件发现和加载器: 1. 创建 `DiscoveredPlugin.cs` 记录: - Id, Name, Version, AssemblyPath, EntryPoint 2. 创建 `PluginLoader.cs`: - `DiscoverPlugins(string directory)` - 扫描目录发现插件 - `LoadPlugin(DiscoveredPlugin discovered)` - 加载并实例化 - 使用 System.Reflection.Metadata 或简单扫描 ```csharp public class PluginLoader { public IEnumerable DiscoverPlugins(string pluginDirectory) { // 扫描 plugin.json 或程序集属性 } public IGatewayPlugin? LoadPlugin(DiscoveredPlugin discovered) { var shadowPath = CreateShadowCopy(discovered.AssemblyPath); var alc = new PluginLoadContext(shadowPath); var assembly = alc.LoadFromAssemblyName(new AssemblyName(Path.GetFileNameWithoutExtension(discovered.AssemblyPath))); var type = assembly.GetType(discovered.EntryPoint); return Activator.CreateInstance(type) as IGatewayPlugin; } } ``` **影子复制**:创建 `CreateShadowCopy` 方法,将插件复制到临时目录。 dotnet test tests/YarpGateway.Tests --filter "FullyQualifiedName~PluginLoaderTests" --no-build - PluginLoader 类存在 - 可发现和加载插件 - 测试通过 Task 3: 创建 PluginHost 生命周期管理 src/yarpgateway/Plugins/PluginHost.cs, src/yarpgateway/Plugins/PluginHandle.cs, tests/YarpGateway.Tests/Unit/Plugins/PluginHostTests.cs - Test 1: LoadAllAsync 加载目录下所有插件 - Test 2: UnloadAsync 卸载指定插件 - Test 3: GetPlugins 返回当前加载的插件 - Test 4: 插件卸载后 WeakReference 显示已回收 创建插件生命周期管理器: 1. 创建 `PluginHandle.cs`: - 封装 PluginLoadContext 和 IGatewayPlugin - 实现 IAsyncDisposable - 提供 TrackUnloadability() 返回 WeakReference ```csharp public sealed class PluginHandle : IAsyncDisposable { private readonly PluginLoadContext _alc; private readonly IGatewayPlugin _plugin; private readonly string _shadowDirectory; private bool _disposed; public IGatewayPlugin Plugin => _plugin; public WeakReference TrackUnloadability() => new(_alc, trackResurrection: true); public async ValueTask DisposeAsync() { if (_disposed) return; _disposed = true; await _plugin.OnUnloadAsync(); _alc.Unload(); } } ``` 2. 创建 `PluginHost.cs`: - 注入 PluginLoader - 管理插件字典 (name -> PluginHandle) - 提供 LoadAllAsync, UnloadAsync, GetPlugins ```csharp public class PluginHost { private readonly ConcurrentDictionary _plugins = new(); private readonly PluginLoader _loader; public async Task LoadAllAsync(string pluginDirectory, CancellationToken ct = default) { var discovered = _loader.DiscoverPlugins(pluginDirectory); foreach (var d in discovered) { var handle = _loader.LoadPlugin(d); if (handle != null) { await handle.Plugin.OnLoadAsync(); _plugins[d.Id] = handle; } } } public async Task UnloadAsync(string pluginId) { if (_plugins.TryRemove(pluginId, out var handle)) { await handle.DisposeAsync(); } } } ``` dotnet test tests/YarpGateway.Tests --filter "FullyQualifiedName~PluginHostTests" --no-build - PluginHost 和 PluginHandle 类存在 - 可加载/卸载插件 - 卸载验证测试通过 1. dotnet build src/yarpgateway/YarpGateway.csproj 无错误 2. dotnet test tests/YarpGateway.Tests --filter "FullyQualifiedName~PluginLoad" 通过 3. 插件加载/卸载功能可用 - 插件可在独立 AssemblyLoadContext 中加载 - 插件可通过 WeakReference 验证卸载 - 所有单元测试通过 完成后创建 `.planning/phases/006-gateway-plugin-research/006-01-SUMMARY.md`