diff --git a/src/Neo/Plugins/Plugin.cs b/src/Neo/Plugins/Plugin.cs index 864d6f5a3a..cdb0d70bc1 100644 --- a/src/Neo/Plugins/Plugin.cs +++ b/src/Neo/Plugins/Plugin.cs @@ -11,6 +11,7 @@ using Microsoft.Extensions.Configuration; using System.Reflection; +using System.Runtime.Loader; using static System.IO.Path; namespace Neo.Plugins; @@ -33,6 +34,7 @@ public abstract class Plugin : IDisposable Combine(GetDirectoryName(AppContext.BaseDirectory)!, "Plugins"); private static readonly FileSystemWatcher? s_configWatcher; + private static readonly List s_pluginLoadContexts = []; /// /// Indicates the root path of the plugin. @@ -90,7 +92,6 @@ static Plugin() s_configWatcher.Created += ConfigWatcher_Changed; s_configWatcher.Renamed += ConfigWatcher_Changed; s_configWatcher.Deleted += ConfigWatcher_Changed; - AppDomain.CurrentDomain.AssemblyResolve += CurrentDomain_AssemblyResolve; } /// @@ -125,33 +126,71 @@ private static void ConfigWatcher_Changed(object? sender, FileSystemEventArgs e) } } - private static Assembly? CurrentDomain_AssemblyResolve(object? sender, ResolveEventArgs args) + private sealed class PluginLoadContext : AssemblyLoadContext { - if (args.Name.Contains(".resources")) - return null; + private readonly AssemblyDependencyResolver _resolver; + private readonly string _pluginDirectory; + private readonly HashSet _sharedAssemblies; - AssemblyName an = new(args.Name); + public PluginLoadContext(string mainAssemblyPath) + : base(isCollectible: false) + { + _resolver = new AssemblyDependencyResolver(mainAssemblyPath); + _pluginDirectory = GetDirectoryName(mainAssemblyPath)!; + _sharedAssemblies = new(StringComparer.OrdinalIgnoreCase) + { + typeof(Plugin).Assembly.GetName().Name! + }; + } - var assembly = AppDomain.CurrentDomain.GetAssemblies().FirstOrDefault(a => a.FullName == args.Name) ?? - AppDomain.CurrentDomain.GetAssemblies().FirstOrDefault(a => a.GetName().Name == an.Name); - if (assembly != null) return assembly; + protected override Assembly? Load(AssemblyName assemblyName) + { + if (_sharedAssemblies.Contains(assemblyName.Name!)) + return Default.LoadFromAssemblyName(assemblyName); - var filename = an.Name + ".dll"; - var path = filename; - if (!File.Exists(path)) path = Combine(GetDirectoryName(AppContext.BaseDirectory)!, filename); - if (!File.Exists(path)) path = Combine(PluginsDirectory, filename); - if (!File.Exists(path) && !string.IsNullOrEmpty(args.RequestingAssembly?.GetName().Name)) - path = Combine(PluginsDirectory, args.RequestingAssembly!.GetName().Name!, filename); - if (!File.Exists(path)) return null; + var path = _resolver.ResolveAssemblyToPath(assemblyName); + if (path != null) + return LoadFromAssemblyPath(path); - try - { - return Assembly.Load(File.ReadAllBytes(path)); + if (assemblyName.Name is null) + return null; + + var localPath = Combine(_pluginDirectory, $"{assemblyName.Name}.dll"); + return File.Exists(localPath) ? LoadFromAssemblyPath(localPath) : null; } - catch (Exception ex) + + protected override IntPtr LoadUnmanagedDll(string unmanagedDllName) { - Utility.Log(nameof(Plugin), LogLevel.Error, ex); - return null; + var path = _resolver.ResolveUnmanagedDllToPath(unmanagedDllName); + if (path != null) + return LoadUnmanagedDllFromPath(path); + + var localPath = Combine(_pluginDirectory, unmanagedDllName); + if (File.Exists(localPath)) + return LoadUnmanagedDllFromPath(localPath); + + if (OperatingSystem.IsWindows()) + { + localPath = Combine(_pluginDirectory, $"{unmanagedDllName}.dll"); + if (File.Exists(localPath)) + return LoadUnmanagedDllFromPath(localPath); + } + + if (OperatingSystem.IsLinux()) + { + localPath = Combine(_pluginDirectory, $"lib{unmanagedDllName}.so"); + if (File.Exists(localPath)) + return LoadUnmanagedDllFromPath(localPath); + } + + if (OperatingSystem.IsMacOS()) + { + localPath = Combine(_pluginDirectory, $"lib{unmanagedDllName}.dylib"); + if (File.Exists(localPath)) + return LoadUnmanagedDllFromPath(localPath); + } + + return IntPtr.Zero; } } @@ -173,7 +212,7 @@ protected IConfigurationSection GetConfiguration() .GetSection("PluginConfiguration"); } - private static void LoadPlugin(Assembly assembly) + private static int LoadPlugin(Assembly assembly) { Type[] exportedTypes; @@ -189,6 +228,7 @@ private static void LoadPlugin(Assembly assembly) throw; } + var loaded = 0; foreach (var type in exportedTypes) { if (!type.IsSubclassOf(typeof(Plugin))) continue; @@ -200,36 +240,60 @@ private static void LoadPlugin(Assembly assembly) try { constructor.Invoke(null); + loaded++; } catch (Exception ex) { Utility.Log(nameof(Plugin), LogLevel.Error, $"Failed to initialize plugin type {type.FullName} of {assemblyName}: {ex}"); } } + + return loaded; } internal static void LoadPlugins() { if (!Directory.Exists(PluginsDirectory)) return; - List assemblies = []; foreach (var rootPath in Directory.GetDirectories(PluginsDirectory)) { - foreach (var filename in Directory.EnumerateFiles(rootPath, "*.dll", SearchOption.TopDirectoryOnly)) + var pluginName = new DirectoryInfo(rootPath).Name; + var mainAssemblyPath = Combine(rootPath, $"{pluginName}.dll"); + if (!File.Exists(mainAssemblyPath)) { - try - { - assemblies.Add(Assembly.Load(File.ReadAllBytes(filename))); - } - catch (Exception ex) + Utility.Log(nameof(Plugin), LogLevel.Warning, $"Plugin assembly not found: {mainAssemblyPath}"); + continue; + } + + try + { + var loadContext = new PluginLoadContext(mainAssemblyPath); + s_pluginLoadContexts.Add(loadContext); + var assembly = loadContext.LoadFromAssemblyPath(mainAssemblyPath); + var loaded = LoadPlugin(assembly); + if (loaded > 0) + continue; + + foreach (var filename in Directory.EnumerateFiles(rootPath, "*.dll", SearchOption.TopDirectoryOnly)) { - Utility.Log(nameof(Plugin), LogLevel.Error, $"Failed to load plugin assembly file {filename}: {ex}"); + if (string.Equals(filename, mainAssemblyPath, StringComparison.OrdinalIgnoreCase)) + continue; + + try + { + assembly = loadContext.LoadFromAssemblyPath(filename); + if (LoadPlugin(assembly) > 0) + break; + } + catch (Exception ex) + { + Utility.Log(nameof(Plugin), LogLevel.Error, $"Failed to load plugin assembly file {filename}: {ex}"); + } } } - } - - foreach (var assembly in assemblies) - { - LoadPlugin(assembly); + catch (Exception ex) + { + Utility.Log(nameof(Plugin), LogLevel.Error, $"Failed to load plugin assembly file {mainAssemblyPath}: {ex}"); + } } } diff --git a/tests/Neo.PluginFixture.Dependency/FixtureDependency.cs b/tests/Neo.PluginFixture.Dependency/FixtureDependency.cs new file mode 100644 index 0000000000..bd77a09719 --- /dev/null +++ b/tests/Neo.PluginFixture.Dependency/FixtureDependency.cs @@ -0,0 +1,17 @@ +// Copyright (C) 2015-2025 The Neo Project. +// +// FixtureDependency.cs file belongs to the neo project and is free +// software distributed under the MIT software license, see the +// accompanying file LICENSE in the main directory of the +// repository or http://www.opensource.org/licenses/mit-license.php +// for more details. +// +// Redistribution and use in source and binary forms with or without +// modifications are permitted. + +namespace Neo.PluginFixture.Dependency; + +public static class FixtureDependency +{ + public static string Value => "fixture-dependency"; +} diff --git a/tests/Neo.PluginFixture.Dependency/Neo.PluginFixture.Dependency.csproj b/tests/Neo.PluginFixture.Dependency/Neo.PluginFixture.Dependency.csproj new file mode 100644 index 0000000000..f5c6bb56f4 --- /dev/null +++ b/tests/Neo.PluginFixture.Dependency/Neo.PluginFixture.Dependency.csproj @@ -0,0 +1,8 @@ + + + + Neo.PluginFixture.Dependency + Library + + + diff --git a/tests/Neo.PluginFixture/FixturePlugin.cs b/tests/Neo.PluginFixture/FixturePlugin.cs new file mode 100644 index 0000000000..9a5ffda7a8 --- /dev/null +++ b/tests/Neo.PluginFixture/FixturePlugin.cs @@ -0,0 +1,20 @@ +// Copyright (C) 2015-2025 The Neo Project. +// +// FixturePlugin.cs file belongs to the neo project and is free +// software distributed under the MIT software license, see the +// accompanying file LICENSE in the main directory of the +// repository or http://www.opensource.org/licenses/mit-license.php +// for more details. +// +// Redistribution and use in source and binary forms with or without +// modifications are permitted. + +using Neo.Plugins; + +namespace Neo.PluginFixture; + +public sealed class FixturePlugin : Plugin +{ + public string GetDependencyAssemblyLocation() => + typeof(Dependency.FixtureDependency).Assembly.Location; +} diff --git a/tests/Neo.PluginFixture/Neo.PluginFixture.csproj b/tests/Neo.PluginFixture/Neo.PluginFixture.csproj new file mode 100644 index 0000000000..16eaa2a148 --- /dev/null +++ b/tests/Neo.PluginFixture/Neo.PluginFixture.csproj @@ -0,0 +1,13 @@ + + + + Neo.PluginFixture + Library + + + + + + + + diff --git a/tests/Neo.UnitTests/Neo.UnitTests.csproj b/tests/Neo.UnitTests/Neo.UnitTests.csproj index 31c8bd194b..9f1a361deb 100644 --- a/tests/Neo.UnitTests/Neo.UnitTests.csproj +++ b/tests/Neo.UnitTests/Neo.UnitTests.csproj @@ -21,6 +21,7 @@ + diff --git a/tests/Neo.UnitTests/Plugins/UT_PluginLoadContext.cs b/tests/Neo.UnitTests/Plugins/UT_PluginLoadContext.cs new file mode 100644 index 0000000000..72ab7eddfe --- /dev/null +++ b/tests/Neo.UnitTests/Plugins/UT_PluginLoadContext.cs @@ -0,0 +1,102 @@ +// Copyright (C) 2015-2025 The Neo Project. +// +// UT_PluginLoadContext.cs file belongs to the neo project and is free +// software distributed under the MIT software license, see the +// accompanying file LICENSE in the main directory of the +// repository or http://www.opensource.org/licenses/mit-license.php +// for more details. +// +// Redistribution and use in source and binary forms with or without +// modifications are permitted. + +using Neo.Plugins; +using System.Runtime.Loader; + +namespace Neo.UnitTests.Plugins; + +[TestClass] +public class UT_PluginLoadContext +{ + private const string FixtureAssemblyName = "Neo.PluginFixture"; + private const string DependencyAssemblyName = "Neo.PluginFixture.Dependency"; + private static readonly Lock s_locker = new(); + + [TestMethod] + public void TestLoadPluginsUsesIsolatedContext() + { + lock (s_locker) + { + try + { + var pluginRoot = PrepareFixturePluginDirectory(out var pluginAssemblyPath); + Plugin.Plugins.Clear(); + + Plugin.LoadPlugins(); + + var plugin = Plugin.Plugins.SingleOrDefault(p => + string.Equals(p.GetType().Assembly.Location, pluginAssemblyPath, StringComparison.OrdinalIgnoreCase)); + Assert.IsNotNull(plugin); + + var loadContext = AssemblyLoadContext.GetLoadContext(plugin.GetType().Assembly); + Assert.IsNotNull(loadContext); + Assert.AreNotSame(AssemblyLoadContext.Default, loadContext); + Assert.AreEqual(pluginAssemblyPath, plugin.GetType().Assembly.Location); + } + finally + { + Plugin.Plugins.Clear(); + } + } + } + + [TestMethod] + public void TestLoadPluginsResolvesDependencyFromPluginDirectory() + { + lock (s_locker) + { + try + { + var pluginRoot = PrepareFixturePluginDirectory(out _); + Plugin.Plugins.Clear(); + + Plugin.LoadPlugins(); + + var plugin = Plugin.Plugins.SingleOrDefault(p => + p.GetType().Assembly.Location.StartsWith(pluginRoot, StringComparison.OrdinalIgnoreCase)); + Assert.IsNotNull(plugin); + + var method = plugin.GetType().GetMethod("GetDependencyAssemblyLocation"); + Assert.IsNotNull(method); + + var location = method.Invoke(plugin, null) as string; + Assert.IsFalse(string.IsNullOrEmpty(location)); + Assert.AreEqual(Path.Combine(pluginRoot, $"{DependencyAssemblyName}.dll"), location); + } + finally + { + Plugin.Plugins.Clear(); + } + } + } + + private static string PrepareFixturePluginDirectory(out string pluginAssemblyPath) + { + var pluginsDir = Plugin.PluginsDirectory; + Directory.CreateDirectory(pluginsDir); + var pluginDirectoryName = $"{FixtureAssemblyName}.{Guid.NewGuid():N}"; + var pluginRoot = Path.Combine(pluginsDir, pluginDirectoryName); + Directory.CreateDirectory(pluginRoot); + + var sourcePluginPath = Path.Combine(AppContext.BaseDirectory, $"{FixtureAssemblyName}.dll"); + var sourceDependencyPath = Path.Combine(AppContext.BaseDirectory, $"{DependencyAssemblyName}.dll"); + + Assert.IsTrue(File.Exists(sourcePluginPath), $"Missing fixture plugin assembly: {sourcePluginPath}"); + Assert.IsTrue(File.Exists(sourceDependencyPath), $"Missing fixture dependency assembly: {sourceDependencyPath}"); + + pluginAssemblyPath = Path.Combine(pluginRoot, $"{pluginDirectoryName}.dll"); + File.Copy(sourcePluginPath, pluginAssemblyPath, true); + File.Copy(sourceDependencyPath, Path.Combine(pluginRoot, $"{DependencyAssemblyName}.dll"), true); + + return pluginRoot; + } +}