Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 9 additions & 3 deletions Modules/Hosting/ApplicationHost.cs
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,12 @@ public class ApplicationHost
private string _apiName = null!;
private string _apiVersion = null!;

/// <summary>
/// 插件加载器实例,持有所有插件 AssemblyLoadContext 的强引用,
/// 确保在服务注册期间 ALC 不会被 GC 提前标记为 unloading。
/// </summary>
private PluginLoader _pluginLoader = null!;

/// <summary>
/// 初始化应用程序主机
/// </summary>
Expand Down Expand Up @@ -65,8 +71,8 @@ public ApplicationHost Build()

// 6. 加载插件
var pluginLogger = new SerilogLoggerFactory(Log.Logger).CreateLogger("PluginLoader");
var pluginLoader = new PluginLoader(_configuration, pluginLogger);
var plugins = pluginLoader.LoadPlugins();
_pluginLoader = new PluginLoader(_configuration, pluginLogger);
var plugins = _pluginLoader.LoadPlugins();

// 7. 检查依赖
var dependencyChecker = new PluginDependencyChecker(pluginLogger);
Expand All @@ -75,7 +81,7 @@ public ApplicationHost Build()
// 8. 注册服务
// 将插件集合注入到 DI 容器(作为单例),插件实现可从容器中获取此集合
builder.Services.AddSingleton(plugins);

var serviceRegistrar = new PluginServiceRegistrar(builder.Services, pluginLogger);
serviceRegistrar.AddSwaggerServices(_apiName, _apiVersion);
serviceRegistrar.RegisterPluginServices(plugins, _configuration);
Expand Down
40 changes: 38 additions & 2 deletions Modules/PluginManagement/PluginLoader.cs
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,17 @@ public class PluginLoader
private readonly IConfiguration _configuration;
private readonly ILogger _logger;

/// <summary>
/// 跟踪所有已创建的 AssemblyLoadContext,确保在插件服务注册期间 ALC 保持 alive 状态,
/// 防止因失去强引用而被 GC 提前标记为 unloading。
/// </summary>
private readonly List<PluginLoadContext> _loadContexts = new();

/// <summary>
/// 获取所有已创建的插件加载上下文列表。
/// </summary>
public IReadOnlyList<PluginLoadContext> LoadContexts => _loadContexts.AsReadOnly();

/// <summary>
/// 初始化插件加载器
/// </summary>
Expand All @@ -32,7 +43,7 @@ public PluginLoader(IConfiguration configuration, ILogger logger)
public List<IApiPlugin> LoadPlugins()
{
var loadedPlugins = new List<IApiPlugin>();

// 获取插件目录路径
string pluginsPath = Path.Combine(AppContext.BaseDirectory, "plugins");

Expand Down Expand Up @@ -221,8 +232,13 @@ private List<IApiPlugin> LoadPluginFromDirectory(string dirPath)
{
// 使用隔离加载上下文加载程序集,防止插件之间的依赖冲突
var loadContext = new PluginLoadContext(dllPath);

// 将 ALC 添加到跟踪列表,确保其在服务注册阶段保持 alive,
// 不会被 GC 标记为 unloading(isCollectible: true 的 ALC 若无强引用会被回收)
_loadContexts.Add(loadContext);

var assembly = loadContext.LoadFromAssemblyPath(dllPath);

// 从程序集中查找所有实现了 IApiPlugin 接口的类型
var pluginTypes = assembly.GetTypes()
.Where(t => typeof(IApiPlugin).IsAssignableFrom(t) && !t.IsInterface);
Expand All @@ -238,4 +254,24 @@ private List<IApiPlugin> LoadPluginFromDirectory(string dirPath)

return null;
}

/// <summary>
/// 卸载所有插件 AssemblyLoadContext,释放占用的资源。
/// 应在完成所有需要 ALC 的操作(如服务注册)后调用。
/// </summary>
public void UnloadAll()
{
foreach (var ctx in _loadContexts)
{
try
{
ctx.Unload();
}
catch (Exception ex)
{
_logger.LogWarning(ex, "Failed to unload plugin assembly load context");
}
}
_loadContexts.Clear();
}
}
135 changes: 106 additions & 29 deletions Modules/Services/ServiceRegistration.cs
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
using Microsoft.AspNetCore.Builder;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using System.Text.Json.Nodes;
using sharwapi.Contracts.Core;
using System.IO;

namespace sharwapi.Core.Modules.Services;

Expand Down Expand Up @@ -47,7 +49,7 @@ private void RegisterPluginServices(IApiPlugin plugin, IConfiguration configurat
{
// 构建插件配置文件的完整路径
var configPath = Path.Combine(AppContext.BaseDirectory, "config", $"{plugin.Name}.json");

// 确保配置文件存在,如果不存在则从插件的默认配置生成
EnsurePluginConfigFile(plugin, configPath);

Expand Down Expand Up @@ -84,50 +86,125 @@ private void EnsurePluginDataDirectory(IApiPlugin plugin)
}

/// <summary>
/// 确保插件配置文件存在
/// 如果配置文件不存在,从插件的 DefaultConfig 生成默认配置
/// 确保插件配置文件存在并保持与 DefaultConfig 同步
/// 如果配置文件不存在,从插件的 DefaultConfig 生成默认配置;
/// 如果已存在,则递归合并 DefaultConfig 中缺失的键,保留用户已有的值。
/// </summary>
private void EnsurePluginConfigFile(IApiPlugin plugin, string configPath)
{
// 检查配置文件是否已存在
if (!File.Exists(configPath))
// 确保配置目录存在
var configDir = Path.GetDirectoryName(configPath);
if (!string.IsNullOrEmpty(configDir) && !Directory.Exists(configDir))
{
Directory.CreateDirectory(configDir);
}

// 获取插件提供的默认配置
// 注意:如果 DefaultConfig 抛出异常,属于插件自身的问题,此处不做 catch
var defaultConfig = plugin.DefaultConfig;
if (defaultConfig == null) return;

var jsonOptions = new System.Text.Json.JsonSerializerOptions
{
// 获取插件提供的默认配置
var defaultConfig = plugin.DefaultConfig;
WriteIndented = true
};

if (defaultConfig != null)
if (!File.Exists(configPath))
{
// 配置文件不存在,直接写入 DefaultConfig
try
{
try
var jsonString = System.Text.Json.JsonSerializer.Serialize(defaultConfig, jsonOptions);
if (TryWriteConfigWithTimeout(configPath, jsonString, TimeSpan.FromSeconds(10)))
{
// 确保配置目录存在
var configDir = Path.GetDirectoryName(configPath);

if (!string.IsNullOrEmpty(configDir) && !Directory.Exists(configDir))
{
Directory.CreateDirectory(configDir);
}
_logger.LogInformation("Generated default configuration for plugin {PluginName} at {ConfigPath}", plugin.Name, configPath);
}
else
{
_logger.LogError("Timed out while generating default configuration for plugin {PluginName} at {ConfigPath}", plugin.Name, configPath);
}
}
catch (Exception ex)
{
_logger.LogError(ex, "Failed to generate default configuration for plugin {PluginName}", plugin.Name);
}
}
else
{
// 配置文件已存在,读取并与 DefaultConfig 递归合并缺失的键
try
{
var existingJson = File.ReadAllText(configPath);
var existingNode = JsonNode.Parse(existingJson) as JsonObject;
var defaultJsonString = System.Text.Json.JsonSerializer.Serialize(defaultConfig);
var defaultNode = JsonNode.Parse(defaultJsonString) as JsonObject;

// 配置 JSON 序列化选项为缩进格式,便于阅读
var jsonOptions = new System.Text.Json.JsonSerializerOptions
{
WriteIndented = true
};
if (existingNode != null && defaultNode != null)
{
MergeMissingKeys(existingNode, defaultNode);
var mergedJson = existingNode.ToJsonString(jsonOptions);

// 将默认配置序列化为 JSON 并写入文件
var jsonString = System.Text.Json.JsonSerializer.Serialize(defaultConfig, jsonOptions);
if (TryWriteConfigWithTimeout(configPath, jsonString, TimeSpan.FromSeconds(10)))
if (TryWriteConfigWithTimeout(configPath, mergedJson, TimeSpan.FromSeconds(10)))
{
_logger.LogInformation("Generated default configuration for plugin {PluginName} at {ConfigPath}", plugin.Name, configPath);
_logger.LogInformation("Merged default configuration for plugin {PluginName} at {ConfigPath}", plugin.Name, configPath);
}
else
{
_logger.LogError("Timed out while generating default configuration for plugin {PluginName} at {ConfigPath}", plugin.Name, configPath);
_logger.LogError("Timed out while merging configuration for plugin {PluginName} at {ConfigPath}", plugin.Name, configPath);
}
}
catch (Exception ex)
}
catch (Exception ex)
{
_logger.LogWarning(ex, "Failed to merge configuration for plugin {PluginName}, keeping existing config file unchanged", plugin.Name);
}
}
}

/// <summary>
/// 递归合并默认配置到现有配置中,仅补充缺失的键,不覆盖已有值也不删除多余键。
/// </summary>
/// <param name="existing">现有配置的 JsonObject</param>
/// <param name="defaults">默认配置的 JsonObject</param>
private static void MergeMissingKeys(JsonObject existing, JsonObject defaults)
{
// 构建现有配置键的大小写不敏感查找表
// 用于检测 DefaultConfig 中的键在现有配置中是否存在但大小写不同
var existingKeyMap = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase);
foreach (var kvp in existing)
{
existingKeyMap[kvp.Key] = kvp.Key;
}

foreach (var kvp in defaults)
{
if (kvp.Value == null) continue;

if (existing.ContainsKey(kvp.Key))
{
// 键存在且大小写完全匹配
if (kvp.Value is JsonObject defaultObj && existing[kvp.Key] is JsonObject existingObj)
{
// 双方都是嵌套对象 → 递归合并
MergeMissingKeys(existingObj, defaultObj);
}
// 其他情况(值类型、数组、混合类型等)→ 保留现有值,不做任何操作
}
else if (existingKeyMap.ContainsKey(kvp.Key))
{
// 存在大小写不同但实质相同的键 → 沿用现有键名
var actualKey = existingKeyMap[kvp.Key];
if (kvp.Value is JsonObject defaultObj && existing[actualKey] is JsonObject existingObj)
{
_logger.LogError(ex, "Failed to generate default configuration for plugin {PluginName}", plugin.Name);
// 双方都是嵌套对象 → 递归合并到已有的键名下
MergeMissingKeys(existingObj, defaultObj);
}
// 值类型或类型不一致 → 保留现有值,不做任何操作
}
else
{
// 键完全不存在 → 添加(使用 DefaultConfig 中的键名大小写)
existing[kvp.Key] = kvp.Value.DeepClone();
}
}
}
Expand All @@ -140,7 +217,7 @@ private void EnsurePluginConfigFile(IApiPlugin plugin, string configPath)
public void AddSwaggerServices(string apiName, string apiVersion)
{
_services.AddEndpointsApiExplorer();

_services.AddSwaggerGen(options =>
{
options.SwaggerDoc("v1", new Microsoft.OpenApi.Models.OpenApiInfo
Expand Down
Loading