diff --git a/framework/Furion/App/App.cs b/framework/Furion/App/App.cs index 2a299429d1b125762d6263f9047974c4f262f737..156458281ddb14d2906b7afc367cc818d7e8d5c4 100644 --- a/framework/Furion/App/App.cs +++ b/framework/Furion/App/App.cs @@ -5,7 +5,9 @@ using Microsoft.AspNetCore.Http; using Microsoft.Extensions.Configuration; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.DependencyModel; +using Microsoft.Extensions.FileProviders; using Microsoft.Extensions.Options; +using Microsoft.Extensions.Primitives; using StackExchange.Profiling; using System; using System.Collections.Concurrent; @@ -13,6 +15,7 @@ using System.Collections.Generic; using System.IO; using System.Linq; using System.Reflection; +using System.Runtime.CompilerServices; using System.Runtime.Loader; using System.Security.Claims; @@ -196,6 +199,18 @@ namespace Furion /// internal static ConcurrentBag AppStartups; + /// + /// 保存文件夹的监听 + /// + private static readonly ConcurrentDictionary FileProviders = + new ConcurrentDictionary(); + + /// + /// 插件上下文的弱引用 + /// + private static readonly ConcurrentDictionary PLCReferences = + new ConcurrentDictionary(); + /// /// 获取应用有效程序集 /// @@ -228,11 +243,89 @@ namespace Furion { var assemblyFileName = externalAssembly.EndsWith(".dll") ? externalAssembly : $"{externalAssembly}.dll"; // 加载程序集 - scanAssemblies.Add(Assembly.LoadFrom(Path.Combine(AppContext.BaseDirectory, assemblyFileName))); + //scanAssemblies.Add(Assembly.LoadFrom(Path.Combine(AppContext.BaseDirectory, assemblyFileName))); + + // 参照 + // https://docs.microsoft.com/zh-cn/dotnet/standard/assembly/unloadability + scanAssemblies.Add(LoadExternalAssembly(Path.Combine(AppContext.BaseDirectory, assemblyFileName))); } } return scanAssemblies; } + + /// + /// 加载外部程序集 + /// + /// + /// + private static Assembly LoadExternalAssembly(string assemblyFilePath) + { + // 文件名 + string assemblyFileName = Path.GetFileName(assemblyFilePath); + // 所在文件夹绝对路径 + string assemblyFileDirPath = Directory.GetParent(assemblyFilePath).FullName; + + // 初始化外部dll为插件Context + PluginLoadContext loadContext = new PluginLoadContext(assemblyFilePath); + // 记录弱引用,unload用 + var plcWR = new WeakReference(loadContext); + PLCReferences.AddOrUpdate(assemblyFilePath, plcWR, (oldkey, oldvalue) => plcWR); + + // 获取程序集 + Assembly assembly = loadContext.LoadFromAssemblyName(new AssemblyName(Path.GetFileNameWithoutExtension(assemblyFilePath))); ; + + //添加文件夹监听,key为文件的绝对路径,每个文件都需要有一个监听 + var key = assemblyFilePath; + if (!FileProviders.ContainsKey(key)) + { + IFileProvider _fileProvider = new PhysicalFileProvider(assemblyFileDirPath); + FileProviders.AddOrUpdate(key, _fileProvider, (oldkey, oldvalue) => _fileProvider); + } + + IFileProvider fileProvider = FileProviders[key]; + + // 文件修改时触发 + ChangeToken.OnChange( + () => fileProvider.Watch(assemblyFileName), + () => UpdateExternalAssembly(assemblyFilePath, assembly.FullName)); + + return assembly; + } + + /// + /// 更新外部程序集 + /// + /// + /// + [MethodImpl(MethodImplOptions.NoInlining)] + private static void UpdateExternalAssembly(string assemblyFilePath, string assemblyFullName) + { + lock (Assemblies) + { + Assemblies.ToList().RemoveAll(ass => ass.FullName == assemblyFullName); + + // 卸载老的 + if (PLCReferences.ContainsKey(assemblyFilePath)) + { + var plcWR = PLCReferences[assemblyFilePath]; + PLCReferences.Remove(assemblyFilePath, out _); + + if (plcWR.IsAlive) + { + ((PluginLoadContext)plcWR.Target).Unload(); + + // 需要做些延迟,AssemblyLoadContext机制不会立刻完成unload + for (int i = 0; plcWR.IsAlive && (i < 10); i++) + { + GC.Collect(); + GC.WaitForPendingFinalizers(); + } + } + } + + Assemblies.Append(LoadExternalAssembly(assemblyFilePath)); + }; + } } } \ No newline at end of file diff --git a/framework/Furion/App/Internal/PluginLoadContext.cs b/framework/Furion/App/Internal/PluginLoadContext.cs new file mode 100644 index 0000000000000000000000000000000000000000..b5910851fb56ea0e30d2d09a505bd20f23656f60 --- /dev/null +++ b/framework/Furion/App/Internal/PluginLoadContext.cs @@ -0,0 +1,31 @@ +using System.IO; +using System.Reflection; +using System.Runtime.Loader; + +namespace Furion +{ + /// + /// 插件化组件Context + /// + internal class PluginLoadContext : AssemblyLoadContext + { + private AssemblyDependencyResolver _resolver; + + public PluginLoadContext(string pluginPath) : base(isCollectible: true) + { + _resolver = new AssemblyDependencyResolver(pluginPath); + } + + protected override Assembly Load(AssemblyName assemblyName) + { + string assemblyPath = _resolver.ResolveAssemblyToPath(assemblyName); + if (assemblyPath != null) + { + using FileStream file = new FileStream(assemblyPath, FileMode.Open, FileAccess.ReadWrite, FileShare.ReadWrite); + return LoadFromStream(file); + } + + return null; + } + } +}