feat(cordis): 完成阶段5访问介导与诊断

This commit is contained in:
2026-08-17 01:09:32 +08:00
parent de5ab449cd
commit 8738e633ee
35 changed files with 1540 additions and 50 deletions
+13 -1
View File
@@ -72,6 +72,8 @@
- 是否自动监听目录变化并协调外部 DLL revision 变化
- `externalModsReloadDelaySeconds`
- 文件变更后延迟多少秒再尝试热加载
- `externalAssemblyRevisionSoftLimit`
- 外部 DLL 常驻 revision 的软阈值;达到阈值时提示执行 Domain Reload 或重启进程,默认 `16`
- `enableHarmonyPatching`
- 是否启用 Harmony 自动补丁
- `enableNetworkSync`
@@ -209,6 +211,16 @@ ShrinkModLoader.LoadNewExternalMods();
如果 `watchExternalModsDirectory = true`,框架还会监听新增、修改、删除、重命名事件,经过主线程 debouncer 后提交一次完整组合;同一 burst 内的中间坏文件不会覆盖当前有效 revision。
### 常驻 revision 诊断
Unity/Mono 不能从当前 AppDomain 单独卸载已载入程序集。框架保留当前 revision 的生效语义,同时通过以下接口暴露实际常驻情况:
```csharp
var snapshot = ShrinkModDiagnostics.CaptureExternalAssemblies(settings);
```
快照包含当前与历史 revision、来源路径、程序集名、SHA-256 revision、载入字节数、常驻数量和软阈值状态。失败 revision 可以成为已载入的历史程序集,但不会成为 current;达到软阈值只告警,不伪造卸载行为。
## Harmony 热补丁
如果运行环境里存在 `0Harmony`ContextHost 会把每个模组的补丁租约作为可逆效应管理:激活时调用
@@ -649,7 +661,7 @@ public sealed class DemoFullMod : ShrinkModBase
- 模组 ID 必须全局唯一
- 必选依赖缺失或版本不满足时,装载直接失败
- 检测到循环依赖时,装载直接失败
- 外部 DLL 支持热添加,不支持热卸载
- 外部 DLL 支持当前组合的添加、替换与删除;已载入的程序集 revision 仍常驻,回滚和卸载单位是模组 fiber 与其效应
## 关键文件
@@ -56,6 +56,9 @@ namespace ShrinkModFramework
public string externalModsFolderName = "Mods";
public bool watchExternalModsDirectory = true;
public float externalModsReloadDelaySeconds = 0.5f;
[Min(1)]
[Tooltip("Mono 下外部程序集 revision 会常驻;历史数量达到该软阈值后提示 Domain Reload/重启。")]
public int externalAssemblyRevisionSoftLimit = 16;
[Header("Harmony")]
public bool enableHarmonyPatching = true;
@@ -15,6 +15,7 @@ namespace ShrinkModFramework
public string Path;
public string Revision;
public Assembly Assembly;
public long LoadedBytes;
}
internal sealed class RevisionState
@@ -27,10 +28,11 @@ namespace ShrinkModFramework
private static readonly Dictionary<string, ExternalAssemblyRevision> CurrentAssemblyRevisions =
new(StringComparer.OrdinalIgnoreCase);
private static readonly HashSet<Assembly> ExternalAssemblyHistory = new();
private static readonly Dictionary<Assembly, ExternalAssemblyRevision> ExternalAssemblyHistory = new();
private static readonly Dictionary<string, string> KnownAssemblyFiles = new(StringComparer.OrdinalIgnoreCase);
private static readonly object ResolveLock = new();
private static bool _resolveRegistered;
private static int _lastWarnedResidentCount;
public static IReadOnlyList<Assembly> LoadExternalAssemblies(ShrinkModFrameworkSettings settings, bool verboseLogging)
{
@@ -82,18 +84,22 @@ namespace ShrinkModFramework
}
var pdbPath = Path.ChangeExtension(dllPath, ".pdb");
var assembly = File.Exists(pdbPath)
? Assembly.Load(bytes, File.ReadAllBytes(pdbPath))
byte[] pdbBytes = null;
if (File.Exists(pdbPath))
pdbBytes = File.ReadAllBytes(pdbPath);
var assembly = pdbBytes != null
? Assembly.Load(bytes, pdbBytes)
: Assembly.Load(bytes);
var loadedRevision = new ExternalAssemblyRevision
{
Path = dllPath,
Revision = revision,
Assembly = assembly
Assembly = assembly,
LoadedBytes = bytes.LongLength + (pdbBytes?.LongLength ?? 0L)
};
CurrentAssemblyRevisions[dllPath] = loadedRevision;
ExternalAssemblyHistory.Add(assembly);
ExternalAssemblyHistory[assembly] = loadedRevision;
if (verboseLogging)
Debug.Log($"[ShrinkModFramework] 已加载外部模组程序集:{assembly.GetName().Name} ({revision[..12]})");
@@ -104,6 +110,8 @@ namespace ShrinkModFramework
}
}
WarnIfResidentLimitExceeded(settings);
foreach (var missingPath in CurrentAssemblyRevisions.Keys
.Where(path => !presentPaths.Contains(path))
.ToArray())
@@ -124,7 +132,30 @@ namespace ShrinkModFramework
}
internal static bool IsExternalAssembly(Assembly assembly) =>
assembly != null && ExternalAssemblyHistory.Contains(assembly);
assembly != null && ExternalAssemblyHistory.ContainsKey(assembly);
internal static ShrinkExternalAssemblyDiagnostic CaptureDiagnostic(int softLimit)
{
var normalizedLimit = Math.Max(1, softLimit);
var currentAssemblies = new HashSet<Assembly>(
CurrentAssemblyRevisions.Values.Select(item => item.Assembly));
var revisions = ExternalAssemblyHistory.Values
.OrderBy(item => item.Path, StringComparer.OrdinalIgnoreCase)
.ThenBy(item => item.Revision, StringComparer.Ordinal)
.Select(item => new ShrinkExternalAssemblyRevisionDiagnostic(
item.Path,
item.Revision,
item.Assembly.GetName().Name ?? string.Empty,
item.LoadedBytes,
currentAssemblies.Contains(item.Assembly)))
.ToArray();
return new ShrinkExternalAssemblyDiagnostic(
CurrentAssemblyRevisions.Count,
revisions,
revisions.Sum(item => item.LoadedBytes),
normalizedLimit);
}
internal static RevisionState CaptureState()
{
@@ -193,6 +224,21 @@ namespace ShrinkModFramework
{
ResetForTesting();
ExternalAssemblyHistory.Clear();
_lastWarnedResidentCount = 0;
}
private static void WarnIfResidentLimitExceeded(ShrinkModFrameworkSettings settings)
{
var limit = Math.Max(1, settings != null ? settings.externalAssemblyRevisionSoftLimit : 16);
var residentCount = ExternalAssemblyHistory.Count;
if (residentCount < limit || residentCount == _lastWarnedResidentCount)
return;
_lastWarnedResidentCount = residentCount;
var bytes = ExternalAssemblyHistory.Values.Sum(item => item.LoadedBytes);
Debug.LogWarning(
$"[ShrinkModFramework] 外部 DLL 常驻 revision 已达到 {residentCount} 个(载入文件约 {bytes} bytes" +
$"软阈值 {limit})。Mono 无法卸载这些 Assembly;建议在维护窗口执行 Domain Reload 或重启进程。");
}
private static string ComputeSha256(byte[] bytes)
@@ -0,0 +1,57 @@
using System;
using System.Collections.Generic;
namespace ShrinkModFramework
{
public sealed class ShrinkExternalAssemblyRevisionDiagnostic
{
internal ShrinkExternalAssemblyRevisionDiagnostic(string path, string revision, string assemblyName,
long loadedBytes, bool isCurrent)
{
Path = path;
Revision = revision;
AssemblyName = assemblyName;
LoadedBytes = loadedBytes;
IsCurrent = isCurrent;
}
public string Path { get; }
public string Revision { get; }
public string AssemblyName { get; }
public long LoadedBytes { get; }
public bool IsCurrent { get; }
}
/// <summary>Mono 外部程序集的 current 与常驻历史快照;LoadedBytes 仅统计载入 DLL/PDB 文件大小。</summary>
public sealed class ShrinkExternalAssemblyDiagnostic
{
internal ShrinkExternalAssemblyDiagnostic(int currentRevisionCount,
IReadOnlyList<ShrinkExternalAssemblyRevisionDiagnostic> residentRevisions,
long estimatedResidentBytes, int softLimit)
{
CurrentRevisionCount = currentRevisionCount;
ResidentRevisions = residentRevisions;
EstimatedResidentBytes = estimatedResidentBytes;
SoftLimit = softLimit;
}
public int CurrentRevisionCount { get; }
public IReadOnlyList<ShrinkExternalAssemblyRevisionDiagnostic> ResidentRevisions { get; }
public int ResidentRevisionCount => ResidentRevisions.Count;
public long EstimatedResidentBytes { get; }
public int SoftLimit { get; }
public bool IsSoftLimitExceeded => ResidentRevisionCount >= SoftLimit;
}
public static class ShrinkModDiagnostics
{
public static ShrinkExternalAssemblyDiagnostic CaptureExternalAssemblies(
ShrinkModFrameworkSettings settings = null)
{
if (settings == null)
settings = ShrinkModFrameworkSettings.Instance;
return ShrinkExternalModAssemblyLoader.CaptureDiagnostic(
settings != null ? settings.externalAssemblyRevisionSoftLimit : 16);
}
}
}
@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 20342cb92d99bc34ca99a41312b5a95d
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:
@@ -3,6 +3,7 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Reflection;
using System.Text.RegularExpressions;
using System.Threading;
@@ -241,6 +242,16 @@ namespace ShrinkModFramework.Tests
CopyFixture("ExternalFixture.Mod.V3.dll.bytes", target);
ShrinkModLoader.LoadNewExternalMods(settings);
AssertExternalFixture("3.0.0", "v3");
settings.externalAssemblyRevisionSoftLimit = 2;
var diagnostics = ShrinkModDiagnostics.CaptureExternalAssemblies(settings);
Assert.AreEqual(1, diagnostics.CurrentRevisionCount,
"同一路径只有最终有效 revision 是 current");
Assert.GreaterOrEqual(diagnostics.ResidentRevisionCount, 3,
"v1、失败但已加载的 v2、v3 Assembly 在 Mono 下都会常驻");
Assert.Greater(diagnostics.EstimatedResidentBytes, 0L);
Assert.IsTrue(diagnostics.IsSoftLimitExceeded);
Assert.AreEqual(1, diagnostics.ResidentRevisions.Count(item => item.IsCurrent));
}
finally
{