Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion src/Directory.Build.props
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@
<SymbolPackageFormat>snupkg</SymbolPackageFormat>
<Company>The Neo Project</Company>
<GenerateDocumentationFile>true</GenerateDocumentationFile>
<VersionPrefix>3.8.1</VersionPrefix>
<VersionPrefix>3.8.2</VersionPrefix>
<TreatWarningsAsErrors>true</TreatWarningsAsErrors>
<WarningLevel>4</WarningLevel>
<AnalysisLevel>latest</AnalysisLevel>
Expand Down
103 changes: 40 additions & 63 deletions src/Neo/Plugins/Plugin.cs
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@ public abstract class Plugin : IDisposable
/// The directory containing the plugin folders. Files can be contained in any subdirectory.
/// </summary>
public static readonly string PluginsDirectory =
Combine(GetDirectoryName(AppContext.BaseDirectory)!, "Plugins");
Combine(AppContext.BaseDirectory, "Plugins");

private static readonly FileSystemWatcher? s_configWatcher;

Expand Down Expand Up @@ -96,7 +96,7 @@ static Plugin()
s_configWatcher.Created += ConfigWatcher_Changed;
s_configWatcher.Renamed += ConfigWatcher_Changed;
s_configWatcher.Deleted += ConfigWatcher_Changed;
AppDomain.CurrentDomain.AssemblyResolve += CurrentDomain_AssemblyResolve;
//AppDomain.CurrentDomain.AssemblyResolve += CurrentDomain_AssemblyResolve;
}

/// <summary>
Expand Down Expand Up @@ -126,36 +126,6 @@ private static void ConfigWatcher_Changed(object? sender, FileSystemEventArgs e)
}
}

private static Assembly? CurrentDomain_AssemblyResolve(object? sender, ResolveEventArgs args)
{
if (args.Name.Contains(".resources"))
return null;

AssemblyName an = new(args.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;

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;

try
{
return Assembly.Load(File.ReadAllBytes(path));
}
catch (Exception ex)
{
Utility.Log(nameof(Plugin), LogLevel.Error, ex);
return null;
}
}

public virtual void Dispose() { }

/// <summary>
Expand All @@ -168,47 +138,54 @@ protected IConfigurationSection GetConfiguration()
.GetSection("PluginConfiguration");
}

private static void LoadPlugin(Assembly assembly)
internal static void LoadPlugins()
{
foreach (var type in assembly.ExportedTypes)
if (Directory.Exists(PluginsDirectory) == false)
return;

foreach (var pluginPath in Directory.GetDirectories(PluginsDirectory))
{
if (!type.IsSubclassOf(typeof(Plugin))) continue;
if (type.IsAbstract) continue;
var pluginName = GetFileName(pluginPath);
var pluginFileName = Combine(PluginsDirectory, $"{pluginName}", $"{pluginName}.dll");

var constructor = type.GetConstructor(Type.EmptyTypes);
if (constructor == null) continue;
if (File.Exists(pluginFileName) == false)
continue;

try
// Provides isolated, dynamic loading and unloading of assemblies and
// their dependencies. Each ALC instance manages the resolution and
// loading of assemblies and supports loading multiple versions of the
// same assembly within a process by isolating them in different contexts.
var assemblyName = new AssemblyName(pluginName);
var pluginAssemblyContext = new PluginAssemblyLoadContext(assemblyName);
var pluginAssembly = pluginAssemblyContext.LoadFromAssemblyName(assemblyName);

var neoPluginClassType = pluginAssembly.ExportedTypes
.FirstOrDefault(
static f =>
f.IsAssignableTo(typeof(Plugin)) && f.IsAbstract == false
);

if (neoPluginClassType is null)
pluginAssemblyContext.Unload();
else
{
constructor.Invoke(null);
}
catch (Exception ex)
{
Utility.Log(nameof(Plugin), LogLevel.Error, ex);
}
}
}
var pluginClassConstructor = neoPluginClassType.GetConstructor(Type.EmptyTypes);

internal static void LoadPlugins()
{
if (!Directory.Exists(PluginsDirectory)) return;
List<Assembly> assemblies = [];
foreach (var rootPath in Directory.GetDirectories(PluginsDirectory))
{
foreach (var filename in Directory.EnumerateFiles(rootPath, "*.dll", SearchOption.TopDirectoryOnly))
{
try
if (pluginClassConstructor is null)
pluginAssemblyContext.Unload();
else
{
assemblies.Add(Assembly.Load(File.ReadAllBytes(filename)));
try
{
pluginClassConstructor.Invoke(null);
}
catch (Exception)
{
pluginAssemblyContext.Unload();
}
}
catch { }
}
}

foreach (var assembly in assemblies)
{
LoadPlugin(assembly);
}
}

/// <summary>
Expand Down
113 changes: 113 additions & 0 deletions src/Neo/Plugins/PluginAssemblyLoadContext.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,113 @@
// Copyright (C) 2015-2025 The Neo Project.
//
// PluginAssemblyLoadContext.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 System;
using System.Diagnostics.CodeAnalysis;
using System.IO;
using System.Reflection;
using System.Runtime.InteropServices;
using System.Runtime.Loader;

namespace Neo.Plugins
{
internal class PluginAssemblyLoadContext : AssemblyLoadContext
{
private readonly string _pluginName;

public PluginAssemblyLoadContext(AssemblyName pluginAssemblyName) : base(isCollectible: true)
{
_pluginName = pluginAssemblyName.Name!;
}

[return: MaybeNull]
protected override Assembly Load(AssemblyName assemblyName)
{
// Attempt to load the assembly from the specified plugin path
var assemblyFile = Path.Combine(Plugin.PluginsDirectory, _pluginName, $"{assemblyName.Name}.dll");

if (File.Exists(assemblyFile))
{
return LoadFromAssemblyPath(assemblyFile);
}

// Load plugin dependencies
assemblyFile = Path.Combine(Plugin.PluginsDirectory, $"{assemblyName.Name}", $"{assemblyName.Name}.dll");
if (File.Exists(assemblyFile))
{
return LoadFromAssemblyPath(assemblyFile);
}

// If not found in the plugin path, defer to the default load context
// This allows shared dependencies (like .NET runtime assemblies) to be resolved
return null;
}

protected override nint LoadUnmanagedDll(string unmanagedDllName)
{
var unmanagedDllFilename = GetUnmanagedDllFilename(Path.GetFileNameWithoutExtension(unmanagedDllName));

// Checks "Plugins\<Plugin Name>" directory
var unmanagedDllFile = Path.Combine(Plugin.PluginsDirectory, _pluginName, unmanagedDllFilename);
if (File.Exists(unmanagedDllFile))
{
return LoadUnmanagedDllFromPath(unmanagedDllFile);
}

// Checks "Plugins\<Plugin Name>\runtimes" directory
unmanagedDllFile = Path.Combine(
Plugin.PluginsDirectory,
_pluginName,
"runtimes",
RuntimeInformation.RuntimeIdentifier,
"native",
unmanagedDllFilename);
if (File.Exists(unmanagedDllFile))
{
return LoadUnmanagedDllFromPath(unmanagedDllFile);
}

// Checks "runtimes" directory
unmanagedDllFile = Path.Combine(
AppContext.BaseDirectory,
"runtimes",
RuntimeInformation.RuntimeIdentifier,
"native",
unmanagedDllFilename);
if (File.Exists(unmanagedDllFile))
{
return LoadUnmanagedDllFromPath(unmanagedDllFile);
}

// Checks "base" directory
unmanagedDllFile = Path.Combine(
AppContext.BaseDirectory,
unmanagedDllFilename);
if (File.Exists(unmanagedDllFile))
{
return LoadUnmanagedDllFromPath(unmanagedDllFile);
}

return nint.Zero;
}

private static string GetUnmanagedDllFilename(string unmanagedDllName)
{
var filename = $"{unmanagedDllName}.dll";

if (OperatingSystem.IsLinux())
filename = $"{unmanagedDllName}.so";
else if (OperatingSystem.IsMacOS())
filename = $"{unmanagedDllName}.dylib";

return filename;
}
}
}