feat(cordis): 完成阶段5访问介导与诊断
This commit is contained in:
@@ -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;
|
||||
|
||||
+52
-6
@@ -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
|
||||
{
|
||||
|
||||
Reference in New Issue
Block a user