cad.net 插件式架构最小实现
基础
你只能卸载程序域,不能卸载程序集,
程序域本质上是个轻量级的进程,
一个dll内只有一个程序集(这是.NET设计标准)
一个子域加载一个插件dll和多个依赖dll,就很方便.
(我们希望全部程序集简单名是唯一的,可以构造一个map,记录每个域有什么程序集)
程序域分为主域和多个子域,主域就是平时用的,子域会继承主域依赖,
子域可以看见主域的程序集,主域看不见子域程序集.
主域用到的必然要加载到主域,子域可以加载版本更低的程序集!!
我们要把子域加载dll的信息提供到主域,例如CAD命令,程序集名,加载的路径.
当主域找不到依赖的时候,通过解析事件查找和处理.
主域和子域的程序集名可以相同,但是版本号要不同.
意味着结构是map<程序集简单名,map<版本,程序域通讯代理>>
我们可以先把 插件和依赖 加载到子域,
当主域需要某个依赖类型的时候,才卸载(连带插件其他依赖)并加载到主域.
最大程度隔离了: 插件和它的依赖
那么岂不是,某个子域加载多个程序集,
然后主域需要其中a程序集,就卸载子域,然后主域加载a.
然后要重建新子域,加载除a之后的程序集.
(下方已经实现)
设计概念
首先建立一个工程Proxy.dll,利用通讯代理进行加载,
它需要放在acad.exe旁边,否则会报错.
然后就能实现动态加载和卸载了.
你要注意通讯代理的跨域传递信息,反向传送信息更少,
例如不是把map传输出去比较,而是把要查询参数的传输进来比较,再传输结果出去.
例如迭代器使用前把整个传输出去,而不是每次迭代就传输一次.
程序域事件:
AppDomain.CurrentDomain.AssemblyResolve事件
会找不到时候会返回程序级完整名 args.Name,
触发解析的程序集全名:
"NonExistentAssembly, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null"
可通过 new AssemblyName(args.Name) 解析出详细的信息:
Name:程序集简单名称 "NonExistentAssembly"
Version:请求的版本号
CultureInfo:区域文化 neutral
PublicKeyToken:公钥标记,未签名则为null
dll文件可以通过此函数解析出详细信息:
var assInfo = AssemblyName.GetAssemblyName(file);
assInfo.Name 简单名
assInfo.Version 版本号
十分危险
如果子域传输出去的类型没有序列化标记,那么主域会触发寻找这个程序集(是插件dll内的程序集)
例如Assembly类就没有序列化标记,所以不能传输它.
而Version版本号有,就能传输版本号.
为什么这十分危险呢?
因为返回Assembly的话,触发了主域寻找插件,
那么会先卸载子域,再把缺失的dll加载到主域,
造成全部插件加载入主域,也就没有插件式功能了.
步骤
0x01 Loadx命令
1,不采用链式加载,而是只加载选中的dll.
加载的时候_dllPathMap记录程序集简单名和加载路径.
2,依赖是从解析事件加载的.
0x02 找不到依赖时触发事件
1,二次加载了新的dll,就会自动把旧的(程序集简单名相同)卸载了.
2,如果跨域找到同版本,报错.让域内解析事件实现自己的return ass;
3,跨越找到低版本,卸载域,并且去加载高版本到本域.
4,二次加载的dll没有之前的函数,但是触发引用了,
例如你直接编译了新的dll,它的程序集版本号更加高,
解析事件因此会找不到同版本了,返回null实现报错,不需要处理.
5,你可以自己设计加载低版本卸载高版本的,
只需要把版本比较逻辑改一下大于等于就好了,这非常灵活.
6,查找磁盘
解析事件查询时候,简单名程序集map是否命中?
没有:
触发加载dll
从_dllPathMap取出每个路径+"程序集简单名"=> 文件名.
还有在环境变量Path寻址.
如果dll文件存在,创建新的程序域并加载进去.
有:
判断程序集版本是否对应,
对应就直接返回程序集.
不对应就卸载,然后查询和加载.
0x03 卸载
看门狗倒计时进行惰性卸载.
主动卸载.
后次加载同一个dll(版本不同)时候卸载前次的.
0x04 调用命令
只有主程序域的才会被cad自动检索,其他域的命令要构造cmdMap.
1,简单测试就用未知命令事件检索cmdMap和调用.
2,动态编译把每个命令编译并加入主程序域
3,高版本有AddCommand加入cad自己的命令表.
针对跨GC Handle报错
似乎无解...若海告诉我只要调用了CAD命令,关闭时候必然弹出.
不过不影响我们调试就是了.
我怀疑CAD内部持有了子域资源(缓存),然后卸载程序域时候多释放了资源.
卸载程序域没有错误,但是关闭进程时候会在VS弹这个错误.
Release模式肯定没有问题,因为都不存在VS了.
0,扫描dll中全部静态字段持有COM接口,
然后报错要求用户自己改好.
1,程序域SetData/GetData传递COM接口,使得它计数只发生在主域.
但是要修改代码,每次取COM都是要从程序域获取.
而且它也是跨域传递,需要序列化,
COM是公共接口,传递会发生什么事情?触发解析事件吗?
没有测试过...
2,子域跟踪每个COM,最后释放.
缺点是要修改dll内部代码,而dll可能是第三方的.
3,拦截COM口,微软是否提供技术?
4,在进程退出前强制清理,这个很好,NET40支持
AppDomain.CurrentDomain.ProcessExit += (s, e) => {
Marshal.CleanupUnusedObjectsInCurrentContext();
};
5,进程隔离,再进行跨进程通讯,但是dll是要直接调用cad接口的,
这个没法做.
6,undo这里用com的地方更新了一下,
不能用静态持有COM字段.
https://www.cnblogs.com/JJBox/p/10214582.html
7,CCW包装器
var unkCookie = Marshal.GetIUnknownForObject(obj);
domain.SetData("unkCookie", unkCookie);
卸载流程
先停止线程,避免相互维护.
主要是反射 Thread/Task/线程池 的字段虽然可以,
但是 new Thread(...).Start(); 可以不让字段持有线程,
此时线程依然在执行,我无法反射停止这部分,其实也有方法...
不过我不应该做,为什么呢?
因为这是强行中断,不够安全,
应该要求 子插件开发者 把线程的释放也写入在Dispose(),
我也只需要遍历接口进行释放就好了.
如果 子插件开发者 不写正确,那么我的遍历过程,
可能被线程一直构造对象,我一直Dispose(),此时存在死循环可能性.
程序域卸载要在遍历执行Dispose()之前还是之后?
程序域卸载会全部停止子域全部线程,
但是线程内如果不断持有COM资源,发生RCW计数,引起无法清零.
发生跨GCHandle,此时不知道子域还是主域没有清空COM引用?
我觉得应该还是让 子插件开发者 通过程序域SetData/GetData传递COM接口.
也就是流程只有,
1,遍历全部程序集对象:实例化/静态的.
如果是IDisposable派生类,触发释放,内部由 子插件开发者 停止线程.
如果是COM对象,直接清空计数.
2,卸载程序域.
代码
[Serializable]
public struct AcadCmdInfo {
public string AssemblyName;
public string TypeName;
public string MethodName;
}
public class MinAssemblyInfo {
public Assembly Assembly;
public string File;
}
// 通讯代理类
public class RemoteExecutor : MarshalByRefObject, IDisposable {
// 卸载发起之后不允许调用命令,调用命令期间也不允许释放
private readonly ReaderWriterLockSlim _lock = new ReaderWriterLockSlim();
// 命令映射表<命令,函数调用信息>
private readonly Dictionary<string, AcadCmdInfo> _cmdMethodMap = new(StringComparer.OrdinalIgnoreCase);
// 程序集映射表<程序集简单名,程序集>
// 程序域内可以多个不同版本的程序集,此处也要一个map<version,Assembly>
// 但是这样太复杂了,干脆砍了这个功能,直接在入参上面处理掉.
private readonly Dictionary<string, MinAssemblyInfo> _loadedAssembliesMap = new();
// 跨域遍历需要完整发送,避免字段迭代产生来回通讯
[MethodImpl(MethodImplOptions.NoInlining)]
public Dictionary<string, AcadCmdInfo> GetAllCommands() => _cmdMethodMap;
[MethodImpl(MethodImplOptions.NoInlining)]
public string[] GetAllCommandNames() => _cmdMethodMap.Keys.ToArray();
[MethodImpl(MethodImplOptions.NoInlining)]
public string[] GetAllFile() => _loadedAssembliesMap.Select(pair => pair.Value.File).ToArray();
// 不能返回Assembly,它没有实现序列化接口,若返回会触发主域解析去找加载的dll.
[MethodImpl(MethodImplOptions.NoInlining)]
public Version? GetAssemblyVersion(string assemblyName) {
if (_loadedAssembliesMap.TryGetValue(assemblyName, out var assemblyInfo))
return assemblyInfo.Assembly.GetName().Version;
return null;
}
// 在此程序域加载程序集
[MethodImpl(MethodImplOptions.NoInlining)]
public bool LoadAssembly(string assemblyName, string file) {
if (_loadedAssembliesMap.ContainsKey(assemblyName)) {
throw new InvalidOperationException($"程序集 '{assemblyName}' 已加载,请使用不同程序域加载");
}
try {
var ass = Assembly.Load(File.ReadAllBytes(file));
var assInfo = new MinAssemblyInfo() { Assembly = ass, File = file };
_loadedAssembliesMap[assemblyName] = assInfo;
AddCmdMap(ass, assemblyName);
return true;
} catch { return false; }
}
public RemoteExecutor() {
AppDomain.CurrentDomain.AssemblyResolve += AssemblyResolve;
AppDomain.CurrentDomain.ProcessExit += (s, e) => Dispose();
AppDomain.CurrentDomain.DomainUnload += (s, e) => Dispose();
}
// 此时的 AppDomain.CurrentDomain 是子域
private Assembly? AssemblyResolve(object sender, ResolveEventArgs args) {
if (!AppDomain.CurrentDomain.Equals(sender)) return null;
// 解析程序集全名,提取简单名和版本号
var assInfo = new AssemblyName(args.Name);
var assemblyName = assInfo.Name;
var assemblyVersion = assInfo.Version;
if (_loadedAssembliesMap.TryGetValue(assemblyName, out var ass2))
return ass2.Assembly;
return null;
}
void AddCmdMap(Assembly ass, string assemblyName) {
try {
// 反射cad的命令特性
foreach (Type type in ass.GetExportedTypes()) {
var ms = type.GetMethods(BindingFlags.Public | BindingFlags.Static | BindingFlags.Instance);
foreach (MethodInfo method in ms) {
var cmdAttr = method.GetCustomAttribute<CommandMethodAttribute>();
if (cmdAttr is null) continue;
string commandName = cmdAttr.GlobalName;
_cmdMethodMap[commandName] = new AcadCmdInfo {
AssemblyName = assemblyName,
TypeName = type.FullName,
MethodName = method.Name
};
}
}
} catch(Exception ex) { Trace.WriteLine(ex.Message); throw; }
}
// 执行命令
[MethodImpl(MethodImplOptions.NoInlining)]
public void Invoke(string globalCommandName) {
_lock.EnterReadLock();
try {
if (IsDisposed) throw new ObjectDisposedException("RemoteExecutor is disposed.");
var info = _cmdMethodMap[globalCommandName];
InvokeMethod(info.AssemblyName, info.TypeName, info.MethodName, new object[0]);
} catch (Exception ex) {
int hr = Marshal.GetHRForException(ex);
var comEx = Marshal.GetExceptionForHR(hr);
File.WriteAllText(@"C:\CAD_COM_Error.txt",
$"HRESULT: 0x{hr:X8}\nCOM Exception: {comEx}\nStack Trace: {comEx.StackTrace}"
);
} finally {
_lock.ExitReadLock();
}
}
// 所有实例化映射表<方法全名称,实例化对象>
private readonly ConcurrentDictionary<string, object> _instanceCache = new();
// 所有实例化映射表<方法全名称,方法>
private readonly ConcurrentDictionary<string, MethodInfo> _methodCache = new();
object InvokeMethod(string assemblyName, string typeName, string methodName, object[] args) {
// 1. 生成缓存键(支持方法重载)
string paramTypes = args != null ? string.Join(",", args.Select(a => a?.GetType().Name ?? "null")) : "";
string fullName = $"{assemblyName}:{typeName}:{methodName}({paramTypes})";
object instance = null;
// 2. 尝试从缓存获取 MethodInfo
if (_methodCache.TryGetValue(fullName, out var method)) {
// 3. 如果是静态方法,直接调用(无需实例)
if (method.IsStatic)
return method.Invoke(null, args);
// 4. 非静态方法,从实例缓存获取实例
if (_instanceCache.TryGetValue(fullName, out instance))
return method.Invoke(instance, args);
}
// 5. 缓存未命中,走反射逻辑
if (!_loadedAssembliesMap.TryGetValue(assemblyName, out var assemblyInfo))
throw new KeyNotFoundException($"程序集不存在: {assemblyName}");
Type type = assemblyInfo.Assembly.GetType(typeName);
if (type is null)
throw new TypeLoadException($"类 {typeName}; 不存在此程序集中: {assemblyName}");
// 6. 获取方法(支持参数匹配)
method = type.GetMethod(methodName,
BindingFlags.Public | BindingFlags.Static | BindingFlags.Instance,
null,
args?.Select(a => a?.GetType()).ToArray() ?? Type.EmptyTypes,
null);
if (method is null)
throw new MissingMethodException($"方法 {methodName}; 不存在此类中: {typeName}");
// 7. 缓存 MethodInfo
_methodCache[fullName] = method;
// 8. 处理实例(非静态方法才缓存)
if (!method.IsStatic) {
instance = Activator.CreateInstance(type);
_instanceCache[fullName] = instance;
}
// 9. 调用方法
return method.Invoke(instance, args);
}
[MethodImpl(MethodImplOptions.NoInlining)]
public override object InitializeLifetimeService() => null;
public bool IsDisposed { get; private set; } = false;
~RemoteExecutor() => Dispose(false);
[MethodImpl(MethodImplOptions.NoInlining)]
public void Dispose() {
Dispose(true);
GC.SuppressFinalize(this);
}
protected virtual void Dispose(bool disposing) {
_lock.EnterWriteLock();
try {
if (IsDisposed) return;
IsDisposed = true;
if (disposing) {
// 先取消所有事件订阅
var events = AppDomain.CurrentDomain.GetAssemblies()
.SelectMany(a => a.GetTypes())
.SelectMany(t => t.GetEvents(BindingFlags.Static | BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic))
.ToList();
foreach (var evt in events) {
try {
// 尝试移除所有事件处理程序
var field = evt.DeclaringType.GetField(evt.Name,
BindingFlags.NonPublic | BindingFlags.Instance | BindingFlags.Static);
if (field != null) {
field.SetValue(null, null);
}
} catch {}
}
// 防止菱形引用
HashSet<object> hashset = new();
// 释放实例化资源
foreach (var instance in _instanceCache.Values) {
ReleaseResources(instance.GetType(), instance, hashset);
}
// 释放静态资源
foreach (var ass in _loadedAssembliesMap.Values) {
try {
foreach (var type in ass.Assembly.GetTypes()) {
if (type.IsClass) ReleaseResources(type, null, hashset);
}
} catch { }
}
// 清理缓存
_instanceCache.Clear();
_loadedAssembliesMap.Clear();
_cmdMethodMap.Clear();
// 清理程序域中RCW的上下文的COM对象.
Marshal.CleanupUnusedObjectsInCurrentContext();
// 触发GC
GC.Collect();
GC.WaitForPendingFinalizers();
/*
// 探测是不是COM泄露
var handleTable = new System.Collections.Hashtable();
foreach (var handle in System.Runtime.InteropServices.GCHandle.Alloc(null)) {
handleTable[handle] = handle.Target?.GetType().Name ?? "null";
}
File.WriteAllText(
@"C:\CAD_GCHandle_Leaks.txt",
$"Active GCHandles:\n{string.Join("\n", handleTable.Keys.Cast<object>().Select(k => $"{k} -> {handleTable[k]}"))}"
);
*/
}
} finally {
_lock.ExitWriteLock();
}
}
const BindingFlags _flags = BindingFlags.Static | BindingFlags.Instance |
BindingFlags.Public | BindingFlags.NonPublic;
// 释放资源
void ReleaseResources(Type objType, object obj, HashSet<object> hashset) {
// 静态时候obj is null的
if (obj is not null && (obj is string || objType.IsPrimitive || !hashset.Add(obj)))
return;
foreach (FieldInfo field in objType.GetFields(_flags)) {
if (field.IsLiteral) continue;
var value = field.GetValue(obj);
if (value is null) continue;
// IntPtr可能是随便写的一个值,而不是真正的非托管资源.
// 非托管资源的是通过Dispose/析构函数进行的,
// 如果发生内存泄露,是通过程序域卸载进行(它是进程嘛)
// 但是值类型内部可能持有引用的字段.
if (field.FieldType.IsValueType) {
if (ContainsReferenceTypes(field.FieldType))
ReleaseResources(field.FieldType, value, hashset);
// 重置值类型表示清空了引用,无根就会被GC清理.
if (!field.IsInitOnly)
field.SetValue(obj, Activator.CreateInstance(field.FieldType));
continue;
}
// 处理引用类型,包括只读字段
var valueType = value.GetType();
if (value is not string && !valueType.IsPrimitive && !hashset.Contains(value)) {
ReleaseResources(valueType, value, hashset);
}
Clean(valueType, value);
// 非只读字段清空值
if (!field.IsInitOnly) field.SetValue(obj, null);
}
}
// 检查类型是否包含引用类型字段
// 0=含引用, 1=值类型, 3=扫描中,
Dictionary<Type, int> _typeScanStatus = new();
bool ContainsReferenceTypes(Type type) {
if (_typeScanStatus.TryGetValue(type, out int status)) {
return status == 0 || status == 3;
}
if (!type.IsValueType) {
_typeScanStatus[type] = 0;
return true;
}
// 标记为 "正在扫描"(防止循环引用)
_typeScanStatus[type] = 3;
// 检查所有字段
bool hasRef = type.GetFields(BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic)
.Any(f => !f.FieldType.IsValueType || ContainsReferenceTypes(f.FieldType));
_typeScanStatus[type] = hasRef ? 0 : 1;
return hasRef;
}
// 清理字段资源
static void Clean(Type resourceType, object resource) {
// 处理IDisposable,包括显式实现
if (resource is IDisposable disposable) {
disposable.Dispose();
} else if (resourceType.GetInterface(nameof(IDisposable)) != null) {
((IDisposable)resource).Dispose();
}
// 释放COM对象,IDisposable派生类也可能是COM.
if (Marshal.IsComObject(resource)) {
// while (Marshal.ReleaseComObject(resource) > 0) { }
Marshal.FinalReleaseComObject(resource);
}
}
}
// 程序域和通讯代理
public class AppDomainContainer : IDisposable {
public readonly AppDomain Domain;
public readonly RemoteExecutor Proxy;
public readonly string Directory;
public AppDomainContainer(string dir) {
Directory = dir;
Guid guid = Guid.NewGuid();
var appName = $"JoinBox_{guid}";
var ads = new AppDomainSetup {
ApplicationName = appName,
ApplicationBase = AppDomain.CurrentDomain.BaseDirectory,
};
HashSet<string> binPaths = new() {
dir,
Path.GetDirectoryName(typeof(object).Assembly.Location),
ads.ApplicationBase,
};
// 多个用分号隔开,子域加载时候会先去此处寻找依赖,找不到才触发解析事件
ads.PrivateBinPath = string.Join(";", binPaths);
// 设置缓存目录
ads.CachePath = ads.ApplicationBase;
// 影像复制是打开还是关闭
ads.ShadowCopyFiles = "true";
// 设置目录的名称,这些目录包含要影像复制的程序集
ads.ShadowCopyDirectories = ads.PrivateBinPath;
ads.DisallowBindingRedirects = false;
ads.DisallowCodeDownload = true;
ads.ConfigurationFile = AppDomain.CurrentDomain.SetupInformation.ConfigurationFile;
#if false
// 从安全策略证据新建程序域(这句导致通讯类无法获取文件)
var adevidence = AppDomain.CurrentDomain.Evidence;
Domain = AppDomain.CreateDomain(_appName, adevidence, ads);
#endif
Domain = AppDomain.CreateDomain(appName, null, ads);
try {
Proxy = (RemoteExecutor)Domain.CreateInstanceAndUnwrap(
typeof(RemoteExecutor).Assembly.FullName,
typeof(RemoteExecutor).FullName);
} catch {
Assembly pluginAss = Assembly.GetExecutingAssembly();
string plugin = pluginAss.Location;
throw new ArgumentNullException(
$"{plugin} 通讯插件需要放到acad.exe旁边" +
$"但是c盘权限太高了,所以直接复制一份acad.exe所有文件到你的主工程Debug目录," +
$"项目的调试启动也要改到这个目录上面的acad.exe");
}
}
// 基础设施层
// 1,强制终止跨域线程,因为释放时候线程仍然在跑,可能构造对象
// 协作式终止线程很好,需要自己定义一套标准,例如遍历IStoppable接口下Stop方法
// 此处无法定义这个协作式标准,采用粗暴终止
// 2,释放程序域内的资源
// 3,卸载程序域,回收全部非托管资源.
public void Dispose() {
try {
Proxy.Dispose();
AppDomain.Unload(Domain);
// 清理程序域中RCW的上下文的COM对象.
Marshal.CleanupUnusedObjectsInCurrentContext();
// 触发GC
GC.Collect();
GC.WaitForPendingFinalizers();
} catch { }
}
}
public class AppDomainManager : IDisposable {
// map<程序集简单名,程序域信息>
public readonly Dictionary<string, Dictionary<Version, AppDomainContainer>> DomainMap = new();
// map<加载路径,HashSet<程序集简单名>> 路径是唯一的,遍历不需要消重
private readonly Dictionary<string, HashSet<string>> _dllPathMap = new();
public AppDomainManager() {
AppDomain.CurrentDomain.AssemblyResolve += AssemblyResolve;
AppDomain.CurrentDomain.ProcessExit += (s, e) => Dispose();
AppDomain.CurrentDomain.DomainUnload += (s, e) => Dispose();
// 记录跨域GCHandle错误
AppDomain.CurrentDomain.UnhandledException += (sender, e) => {
var ex = (Exception)e.ExceptionObject;
File.WriteAllText(
@"C:\CAD_AppDomainManager_Crash_Log.txt",
$"[{DateTime.Now}] Unhandled Exception:\n{ex}\nStack Trace:\n{ex.StackTrace}"
);
};
}
// 程序集解析事件处理
private Assembly? AssemblyResolve(object sender, ResolveEventArgs args) {
// 只负责主域?子域找不到会冒泡sender查找.
// if (!AppDomain.CurrentDomain.Equals(sender)) return null;
Assembly? resultAss = null;
// 解析程序集全名,提取简单名和版本号
var assInfo = new AssemblyName(args.Name);
var assemblyName = assInfo.Name;
var assemblyVersion = assInfo.Version;
// 当前域的程序集已经存在并版本一致就返回
var assList = AppDomain.CurrentDomain.GetAssemblies()
.Where(a => a.GetName().Name == assemblyName)
.OrderByDescending(a => a.GetName().Version)
.ToList();
if (assList.Count > 0) {
resultAss = assList.FirstOrDefault(a => a.GetName().Version == assemblyVersion);
if (resultAss is not null) return resultAss;
resultAss = assList.FirstOrDefault(); // 版本最新
}
// 程序集在其他子域,
// 版本不同是可以直接创建子域加载的
// 版本相同才需要卸载并加入主域,并且卸载前要记录它的全部程序集用于重建
// throw new("在子域解析事件中直接返回该程序集");
string[]? assFiles = null;
string? dir = null;
if (DomainMap.TryGetValue(assemblyName, out var versionContainerMap) &&
versionContainerMap.TryGetValue(assemblyVersion, out var container)) {
if (container.Domain == AppDomain.CurrentDomain) return resultAss;
// 读取这个程序域内全部程序集路径,用于重建
assFiles = container.Proxy.GetAllFile();
dir = container.Directory;
container.Dispose(); // 卸载子域(无法卸载主程序域)
versionContainerMap.Remove(assemblyVersion);
if (versionContainerMap.Count == 0) {
DomainMap.Remove(assemblyName);
}
}
// 磁盘加载:
// 1,重建程序域,把其余程序集加入
if (assFiles is not null) {
assFiles = assFiles.Where(file => {
var info = AssemblyName.GetAssemblyName(file);
return info.Name != assemblyName;
}).ToArray();
var newContainer = new AppDomainContainer(dir);
foreach(var assfile in assFiles) {
try { LoadAssembly(assfile, newContainer); } catch{}
}
}
// 2,加载目标到主程序域
var versionPathPairs = _dllPathMap
.Select(pair => Path.Combine(pair.Key, $"{assemblyName}.dll"))
.Where(File.Exists)
.Select(file => new KeyValuePair<Version, string>(
AssemblyName.GetAssemblyName(file).Version, file))
.OrderByDescending(pair => pair.Key)
.ToList();
// 检查是否找到任何文件
if (versionPathPairs.Count == 0)
return resultAss;
// 查找匹配版本或更高版本
var merge = versionPathPairs.FirstOrDefault(pair => pair.Key == assemblyVersion);
if (merge.Equals(default)) {
merge = versionPathPairs.First();
// 检查版本比当前低
if (merge.Key < assemblyVersion) return resultAss;
}
var file = merge.Value;
// 直接在当前域加载就行了,不然没返回值
try {
var ass = Assembly.Load(File.ReadAllBytes(file));
var assemblyDirectory = Path.GetDirectoryName(file);
SetPathMap(assemblyName, assemblyDirectory);
return ass;
} catch (Exception ex) {
return null;
}
}
// 加入路径,以便查找依赖时候检索
void SetPathMap(string assemblyName, string assemblyDirectory) {
if (!_dllPathMap.TryGetValue(assemblyDirectory, out var set)) {
set = new();
_dllPathMap[assemblyDirectory] = set;
}
set.Add(assemblyName);
}
/*
x0.dll 里面有命令 a和b.
x1.dll 里面有命令 a'和c,加载之后要如何卸载?
最后留下 a',b,c (模式0)
最后留下 a',c (模式1/2)
模式0: 不卸载,为了留下不同的命令.
相同版本加入不同域,调用时候用后次?
不行啊,versionContainerMap字典,不允许这种情况.
模式1: ==当前加载版本的卸载
模式2: <=当前加载版本的卸载
*/
public int LoadMode { get; set; } = 2;
// 加载程序集到独立程序域
public bool LoadAssembly(string file, AppDomainContainer? container = null) {
// 获取要加载的程序集文件路径
if (!File.Exists(file))
throw new FileNotFoundException($"文件不存在: {file}");
// 获取新程序集的版本号
var info = AssemblyName.GetAssemblyName(file);
var newName = info.Name;
var newVersion = info.Version;
// 先检查是否已经加载过,根据不同模式卸载
bool disCurrent = false;
if (DomainMap.TryGetValue(newName, out var versionContainerMap)) {
if (LoadMode == 1) {
if (versionContainerMap.TryGetValue(newVersion, out container)) {
container.Dispose();
versionContainerMap.Remove(newVersion);
disCurrent = true;
}
} else if (LoadMode == 2) {
var rs = versionContainerMap.Where(pair => pair.Key <= newVersion).ToArray();
foreach(var pair in rs) {
pair.Value.Dispose();
versionContainerMap.Remove(pair.Key);
if (pair.Key == newVersion)
disCurrent = true;
}
}
if (versionContainerMap.Count == 0)
DomainMap.Remove(newName);
}
versionContainerMap ??= new();
var assemblyDirectory = Path.GetDirectoryName(file);
if (disCurrent || container is null) {
container = new AppDomainContainer(assemblyDirectory);
}
container.Proxy.LoadAssembly(newName, file);
DomainMap[newName] = versionContainerMap;
versionContainerMap[newVersion] = container;
// 加入路径
SetPathMap(newName, assemblyDirectory);
return true;
}
public bool IsDisposed { get; private set; } = false;
~AppDomainManager() => Dispose(false);
public void Dispose() {
Dispose(true);
GC.SuppressFinalize(this);
}
protected virtual void Dispose(bool disposing) {
if (IsDisposed) return;
IsDisposed = true;
// 若海说加了这句就不会致命错误:
AppDomain.CurrentDomain.AssemblyResolve += AssemblyResolve;
if (disposing) {
// 清理程序域中RCW的上下文的COM对象.
Marshal.CleanupUnusedObjectsInCurrentContext();
foreach (var versionContainerMap in DomainMap.Values) {
foreach (var container in versionContainerMap.Values) {
container.Dispose();
}
}
// 万一释放时候需要调用其他模块,所以它最后才移除.
AppDomain.CurrentDomain.AssemblyResolve -= AssemblyResolve;
DomainMap.Clear();
_dllPathMap.Clear();
}
// 此处无需释放非托管资源(本例无)
}
}
public class LoadxCommands {
class MyAssemblyInfo {
public Version Version;
public string Name; // 程序集简单名
}
// 程序域管理类
static readonly AppDomainManager _adm = new();
// map<命令,程序集信息>
static readonly Dictionary<string, MyAssemblyInfo> _cmdAssMap = new(StringComparer.OrdinalIgnoreCase);
// 通过命令加载
[CommandMethod(nameof(Loadx))]
public void Loadx() {
string file = @"C:\Program Files\MyApp\MyDll.dll";
// 判断COM泄露源
var comTypeMap = ComInterfaceScanner.ScanComInterfacesInAssembly(file);
if (comTypeMap.Count > 0)
Env.Print($"发现静态字段持有COM对象,可能造成泄露");
foreach (var kv in comTypeMap) {
Env.Print($"COM对象: {kv.Key}");
Env.Print($"使用位置({kv.Value.Count}处):");
foreach (var location in kv.Value) {
Env.Print($" → {location}");
}
}
// 提取程序集简单名和提取路径
string directory = Path.GetDirectoryName(file);
string assemblyName = AssemblyName.GetAssemblyName(file).Name;
_adm.LoadAssembly(file);
// 加载dll时候会反射内部全部cad命令,
// 此处提取出来作为主程序域缓存,避免每次跨域查询.
// 遇到覆盖情况,要不要一组都卸载呢?感觉还是不要了,不同dll有相同命令进行后覆盖而已.
var versionContainerMap = _adm.DomainMap[assemblyName];
foreach(var pair in versionContainerMap) {
var version = pair.Key;
var container = pair.Value;
var cmdNames = container.Proxy.GetAllCommandNames();
foreach(var cmdName in cmdNames) {
if (_cmdAssMap.ContainsKey(cmdName))
Env.Print($"覆盖命令: {cmdName}");
_cmdAssMap[cmdName] = new MyAssemblyInfo {
Name = assemblyName,
Version = version
};
}
}
}
// 文档每次开启
[JAutoGo(Sequence.StartDocs)]
public void DocumentEveryOpen(Document doc) {
doc.UnknownCommand += UnknownCommand;
}
// 文档每次关闭
[JAutoGo(Sequence.EndDocs)]
public void DocumentEveryEnd(Document doc) {
doc.UnknownCommand -= UnknownCommand;
}
// 未知命令事件,会提示"未知命令"
// 可以在此处动态编译一段命令特性+跨域调用,然后加入主程序域.
// 这样下次就不会提示了.
private void UnknownCommand(object sender, UnknownCommandEventArgs e) {
var gcmd = e.GlobalCommandName;
if (!_cmdAssMap.TryGetValue(gcmd, out var assemblyInfo))
return;
if (_adm.DomainMap.TryGetValue(assemblyInfo.Name, out var versionContainerMap) &&
versionContainerMap.TryGetValue(assemblyInfo.Version, out var container)) {
container.Proxy.Invoke(gcmd);
return;
}
// 没有找到就清理一下它
_cmdAssMap.Remove(gcmd);
}
[JAutoGo(Sequence.EndLast)]
public void ProcessTerminate() {
// 先移除所有事件处理程序
var dm = Acap.DocumentManager;
if (dm != null) {
foreach (Document doc in dm) {
doc.UnknownCommand -= UnknownCommand;
}
}
// 然后清理域管理器
_adm.Dispose();
_cmdAssMap.Clear();
// 强制GC
GC.Collect();
GC.WaitForPendingFinalizers();
GC.Collect();
Marshal.CleanupUnusedObjectsInCurrentContext();
}
// 自执行初始化接口
[JAutoGo(Sequence.StartFirst)]
public void Init() {
var dm = Acap.DocumentManager;
var doc = dm.MdiActiveDocument;
// 遍历全部命令ARX有命令迭代器接口,没有提供给.net,
// 所以我们只能遍历pgp,优先pgp还是优先dll的
// 磁盘监控dll发生变化还没有做,
// VS编译期间可能是一个占用中的文件,此时读取会存在问题
// 通过编译事件实现终止监控?那不如编译后直接发送监控信息?
}
}
反射全部静态字段持有COM
using System;
using System.Reflection;
using System.Runtime.InteropServices;
using System.Collections.Generic;
using System.Linq;
public class ComInterfaceScanner {
/// <summary>
/// 扫描DLL中的COM接口
/// </summary>
/// <param name="dllPath">DLL文件路径</param>
/// <returns>接口映射字典[接口类型全名, 使用位置列表]</returns>
public static Dictionary<string, List<string>> ScanComInterfacesInAssembly(string dllPath) {
// 创建新的应用程序域(安全隔离)
AppDomain appDomain = AppDomain.CreateDomain("ComScannerDomain");
try {
// 在新域中创建扫描器实例
var scanner = (ComInterfaceScannerProxy)appDomain.CreateInstanceAndUnwrap(
Assembly.GetExecutingAssembly().FullName,
typeof(ComInterfaceScannerProxy).FullName);
return scanner.ScanComInterfaces(dllPath);
} catch (Exception ex) {
Console.WriteLine($"DLL加载或扫描失败: {ex.Message}");
return new Dictionary<string, List<string>>();
}
finally {
// 卸载应用程序域(释放程序集)
AppDomain.Unload(appDomain);
Console.WriteLine("应用程序域已卸载");
}
}
}
public class ComInterfaceScannerProxy : MarshalByRefObject {
/// <summary>
/// 扫描指定DLL中的COM接口
/// </summary>
public Dictionary<string, List<string>> ScanComInterfaces(string dllPath) {
var interfaceMap = new Dictionary<string, List<string>>();
try {
// 反射方式加载程序集(不执行代码)
Assembly assembly = Assembly.ReflectionOnlyLoadFrom(dllPath);
ScanForComInterfaces(assembly, interfaceMap);
}
catch (Exception ex) {
Console.WriteLine($"DLL扫描过程中出错: {ex.Message}");
}
return interfaceMap;
}
/// <summary>
/// 扫描程序集中的COM接口字段
/// </summary>
static void ScanForComInterfaces(Assembly assembly, Dictionary<string, List<string>> interfaceMap) {
foreach (Type type in assembly.GetTypes()) {
foreach (FieldInfo field in type.GetFields(BindingFlags.Static | BindingFlags.Public | BindingFlags.NonPublic)) {
if (!IsComInterface(field.FieldType)) continue;
string interfaceType = field.FieldType.FullName;
string fieldPath = $"{type.FullName}.{field.Name}";
// 构建接口映射字典
if (!interfaceMap.ContainsKey(interfaceType)) {
interfaceMap[interfaceType] = new List<string>();
}
interfaceMap[interfaceType].Add(fieldPath);
Console.WriteLine($"发现COM接口静态字段: {fieldPath} (接口类型: {interfaceType})");
}
}
}
/// <summary>
/// 判断是否为COM接口
/// </summary>
static bool IsComInterface(Type type) {
if (!type.IsInterface)
return false;
// 必须包含Guid特性
bool hasGuid = Attribute.IsDefined(type, typeof(GuidAttribute));
if (!hasGuid)
return false;
// 检查ComImport特性或COM风格接口
bool isComImport = Attribute.IsDefined(type, typeof(ComImportAttribute));
return isComImport || IsComStyleInterface(type);
}
/// <summary>
/// 判断是否为COM风格接口(IUnknown/IDispatch)
/// </summary>
static bool IsComStyleInterface(Type type) {
return type.GetInterfaces().Any(x =>
x.Name == "IUnknown" ||
x.Name == "IDispatch");
}
}
浙公网安备 33010602011771号