Skip to content

Commit 7345a68

Browse files
cschuchardt88shargonajara87NGDAdmin
authored
[Add] Plugin Loading (#4225)
* [`Add`] Fix Plugin Security & Dependency Loading * Update src/Neo/Plugins/Plugin.cs * Update src/Directory.Build.props * Update src/Neo/Plugins/Plugin.cs * Remove isolation from Plugins * Update src/Neo/Plugins/PluginAssemblyLoadContext.cs Co-authored-by: Alvaro <[email protected]> * Apply suggestions from code review * Removed unloading plugins on error * dotnet format --------- Co-authored-by: Shargon <[email protected]> Co-authored-by: Alvaro <[email protected]> Co-authored-by: NGD Admin <[email protected]>
1 parent 455e17a commit 7345a68

File tree

2 files changed

+147
-63
lines changed

2 files changed

+147
-63
lines changed

src/Neo/Plugins/Plugin.cs

Lines changed: 37 additions & 63 deletions
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,7 @@ public abstract class Plugin : IDisposable
3636
/// The directory containing the plugin folders. Files can be contained in any subdirectory.
3737
/// </summary>
3838
public static readonly string PluginsDirectory =
39-
Combine(GetDirectoryName(AppContext.BaseDirectory)!, "Plugins");
39+
Combine(AppContext.BaseDirectory, "Plugins");
4040

4141
private static readonly FileSystemWatcher? s_configWatcher;
4242

@@ -96,7 +96,6 @@ static Plugin()
9696
s_configWatcher.Created += ConfigWatcher_Changed;
9797
s_configWatcher.Renamed += ConfigWatcher_Changed;
9898
s_configWatcher.Deleted += ConfigWatcher_Changed;
99-
AppDomain.CurrentDomain.AssemblyResolve += CurrentDomain_AssemblyResolve;
10099
}
101100

102101
/// <summary>
@@ -126,36 +125,6 @@ private static void ConfigWatcher_Changed(object? sender, FileSystemEventArgs e)
126125
}
127126
}
128127

129-
private static Assembly? CurrentDomain_AssemblyResolve(object? sender, ResolveEventArgs args)
130-
{
131-
if (args.Name.Contains(".resources"))
132-
return null;
133-
134-
AssemblyName an = new(args.Name);
135-
136-
var assembly = AppDomain.CurrentDomain.GetAssemblies().FirstOrDefault(a => a.FullName == args.Name) ??
137-
AppDomain.CurrentDomain.GetAssemblies().FirstOrDefault(a => a.GetName().Name == an.Name);
138-
if (assembly != null) return assembly;
139-
140-
var filename = an.Name + ".dll";
141-
var path = filename;
142-
if (!File.Exists(path)) path = Combine(GetDirectoryName(AppContext.BaseDirectory)!, filename);
143-
if (!File.Exists(path)) path = Combine(PluginsDirectory, filename);
144-
if (!File.Exists(path) && !string.IsNullOrEmpty(args.RequestingAssembly?.GetName().Name))
145-
path = Combine(PluginsDirectory, args.RequestingAssembly!.GetName().Name!, filename);
146-
if (!File.Exists(path)) return null;
147-
148-
try
149-
{
150-
return Assembly.Load(File.ReadAllBytes(path));
151-
}
152-
catch (Exception ex)
153-
{
154-
Utility.Log(nameof(Plugin), LogLevel.Error, ex);
155-
return null;
156-
}
157-
}
158-
159128
public virtual void Dispose() { }
160129

161130
/// <summary>
@@ -168,47 +137,52 @@ protected IConfigurationSection GetConfiguration()
168137
.GetSection("PluginConfiguration");
169138
}
170139

171-
private static void LoadPlugin(Assembly assembly)
140+
internal static void LoadPlugins()
172141
{
173-
foreach (var type in assembly.ExportedTypes)
142+
if (Directory.Exists(PluginsDirectory) == false)
143+
return;
144+
145+
var pluginDirs = Directory.GetDirectories(PluginsDirectory);
146+
var pluginAssemblyContext = new PluginAssemblyLoadContext(pluginDirs);
147+
148+
foreach (var pluginPath in pluginDirs)
174149
{
175-
if (!type.IsSubclassOf(typeof(Plugin))) continue;
176-
if (type.IsAbstract) continue;
150+
var pluginName = GetFileName(pluginPath);
151+
var pluginFileName = Combine(pluginPath, $"{pluginName}.dll");
177152

178-
var constructor = type.GetConstructor(Type.EmptyTypes);
179-
if (constructor == null) continue;
153+
if (File.Exists(pluginFileName) == false)
154+
continue;
180155

181-
try
182-
{
183-
constructor.Invoke(null);
184-
}
185-
catch (Exception ex)
186-
{
187-
Utility.Log(nameof(Plugin), LogLevel.Error, ex);
188-
}
189-
}
190-
}
156+
// Provides isolated, dynamic loading and unloading of assemblies and
157+
// their dependencies. Each ALC instance manages the resolution and
158+
// loading of assemblies and supports loading multiple versions of the
159+
// same assembly within a process by isolating them in different contexts.
160+
var assemblyName = new AssemblyName(pluginName);
161+
var pluginAssembly = pluginAssemblyContext.LoadFromAssemblyName(assemblyName);
191162

192-
internal static void LoadPlugins()
193-
{
194-
if (!Directory.Exists(PluginsDirectory)) return;
195-
List<Assembly> assemblies = [];
196-
foreach (var rootPath in Directory.GetDirectories(PluginsDirectory))
197-
{
198-
foreach (var filename in Directory.EnumerateFiles(rootPath, "*.dll", SearchOption.TopDirectoryOnly))
163+
var neoPluginClassType = pluginAssembly.ExportedTypes
164+
.FirstOrDefault(
165+
static f =>
166+
f.IsAssignableTo(typeof(Plugin)) && f.IsAbstract == false
167+
);
168+
169+
if (neoPluginClassType is not null)
199170
{
200-
try
171+
var pluginClassConstructor = neoPluginClassType.GetConstructor(Type.EmptyTypes);
172+
173+
if (pluginClassConstructor is not null)
201174
{
202-
assemblies.Add(Assembly.Load(File.ReadAllBytes(filename)));
175+
try
176+
{
177+
pluginClassConstructor.Invoke(null);
178+
}
179+
catch (Exception ex)
180+
{
181+
Utility.Log($"{nameof(Plugin)}:{pluginName}", LogLevel.Error, ex.Message);
182+
}
203183
}
204-
catch { }
205184
}
206185
}
207-
208-
foreach (var assembly in assemblies)
209-
{
210-
LoadPlugin(assembly);
211-
}
212186
}
213187

214188
/// <summary>
Lines changed: 110 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,110 @@
1+
// Copyright (C) 2015-2025 The Neo Project.
2+
//
3+
// PluginAssemblyLoadContext.cs file belongs to the neo project and is free
4+
// software distributed under the MIT software license, see the
5+
// accompanying file LICENSE in the main directory of the
6+
// repository or http://www.opensource.org/licenses/mit-license.php
7+
// for more details.
8+
//
9+
// Redistribution and use in source and binary forms with or without
10+
// modifications are permitted.
11+
12+
using System;
13+
using System.Diagnostics.CodeAnalysis;
14+
using System.IO;
15+
using System.Reflection;
16+
using System.Runtime.InteropServices;
17+
using System.Runtime.Loader;
18+
19+
namespace Neo.Plugins
20+
{
21+
internal sealed class PluginAssemblyLoadContext : AssemblyLoadContext
22+
{
23+
private readonly string[] _searchPluginPaths;
24+
25+
public PluginAssemblyLoadContext(string[] searchPaths)
26+
: base(isCollectible: true)
27+
{
28+
_searchPluginPaths = searchPaths;
29+
}
30+
31+
[return: MaybeNull]
32+
protected override Assembly Load(AssemblyName assemblyName)
33+
{
34+
foreach (var path in _searchPluginPaths)
35+
{
36+
var assemblyFile = Path.Combine(path, $"{assemblyName.Name}.dll");
37+
38+
if (File.Exists(assemblyFile))
39+
{
40+
return LoadFromAssemblyPath(assemblyFile);
41+
}
42+
}
43+
44+
// If not found in the plugin path, defer to the default load context
45+
// This allows shared dependencies (like .NET runtime assemblies) to be resolved
46+
return null;
47+
}
48+
49+
protected override nint LoadUnmanagedDll(string unmanagedDllName)
50+
{
51+
var unmanagedDllFilename = GetUnmanagedDllFilename(Path.GetFileNameWithoutExtension(unmanagedDllName));
52+
53+
string unmanagedDllFile;
54+
55+
foreach (var path in _searchPluginPaths)
56+
{
57+
// Checks "Plugins\<Plugin Name>" directory
58+
unmanagedDllFile = Path.Combine(path, unmanagedDllFilename);
59+
if (File.Exists(unmanagedDllFile))
60+
{
61+
return LoadUnmanagedDllFromPath(unmanagedDllFile);
62+
}
63+
64+
// Checks "Plugins\<Plugin Name>\runtimes" directory
65+
unmanagedDllFile = Path.Combine(
66+
path,
67+
"runtimes",
68+
RuntimeInformation.RuntimeIdentifier,
69+
"native",
70+
unmanagedDllFilename);
71+
if (File.Exists(unmanagedDllFile))
72+
{
73+
return LoadUnmanagedDllFromPath(unmanagedDllFile);
74+
}
75+
}
76+
77+
// Fallback to `neo-cli` base directory.
78+
unmanagedDllFile = Path.Combine(
79+
AppContext.BaseDirectory,
80+
"runtimes",
81+
RuntimeInformation.RuntimeIdentifier,
82+
"native",
83+
unmanagedDllFilename);
84+
if (File.Exists(unmanagedDllFile))
85+
{
86+
return LoadUnmanagedDllFromPath(unmanagedDllFile);
87+
}
88+
89+
unmanagedDllFile = Path.Combine(AppContext.BaseDirectory, unmanagedDllFilename);
90+
if (File.Exists(unmanagedDllFile))
91+
{
92+
return LoadUnmanagedDllFromPath(unmanagedDllFile);
93+
}
94+
95+
return nint.Zero;
96+
}
97+
98+
private static string GetUnmanagedDllFilename(string unmanagedDllName)
99+
{
100+
var filename = $"{unmanagedDllName}.dll";
101+
102+
if (OperatingSystem.IsLinux())
103+
filename = $"{unmanagedDllName}.so";
104+
else if (OperatingSystem.IsMacOS())
105+
filename = $"{unmanagedDllName}.dylib";
106+
107+
return filename;
108+
}
109+
}
110+
}

0 commit comments

Comments
 (0)