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");
    }
}
posted @ 2025-03-29 22:00  惊惊  阅读(557)  评论(4)    收藏  举报