Skip to content

Commit

Permalink
loading embedded plugins from memory
Browse files Browse the repository at this point in the history
  • Loading branch information
caunt committed Sep 26, 2024
1 parent d1406bb commit 99e9eb6
Show file tree
Hide file tree
Showing 5 changed files with 93 additions and 125 deletions.
14 changes: 10 additions & 4 deletions src/API/Plugins/IPluginService.cs
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,14 @@ namespace Void.Proxy.API.Plugins;

public interface IPluginService
{
public ValueTask LoadAsync(string path = "plugins", CancellationToken cancellationToken = default);
public ValueTask UnloadAsync(CancellationToken cancellationToken = default);
public IPlugin[] RegisterPlugins(string name, Assembly assembly);
public void UnregisterPlugins(IPlugin[] plugins);
public ValueTask LoadEmbeddedPluginsAsync(CancellationToken cancellationToken = default);
public ValueTask LoadPluginsAsync(string path = "plugins", CancellationToken cancellationToken = default);
public ValueTask LoadPluginsAsync(string assemblyName, Stream assemblyStream, CancellationToken cancellationToken = default);

public ValueTask UnloadPluginsAsync(CancellationToken cancellationToken = default);
public ValueTask UnloadPluginAsync(IPlugin plugin, CancellationToken cancellationToken = default);

public IPlugin[] GetPlugins(string assemblyName, Assembly assembly);
public void RegisterPlugin(IPlugin plugin);
public void UnregisterPlugin(IPlugin plugin);
}
5 changes: 3 additions & 2 deletions src/Platform/Platform.cs
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,8 @@ public async Task StartAsync(CancellationToken cancellationToken)

Directory.SetCurrentDirectory(Path.GetDirectoryName(Assembly.GetExecutingAssembly().Location)!);

await plugins.LoadAsync(cancellationToken: cancellationToken);
await plugins.LoadEmbeddedPluginsAsync(cancellationToken);
await plugins.LoadPluginsAsync(cancellationToken: cancellationToken);
await events.ThrowAsync<ProxyStartingEvent>(cancellationToken);

forwardings.RegisterDefault();
Expand Down Expand Up @@ -78,7 +79,7 @@ await _backgroundTask.ContinueWith(backgroundTask =>
await settings.SaveAsync(cancellationToken: cancellationToken);

await events.ThrowAsync<ProxyStoppedEvent>(cancellationToken);
await plugins.UnloadAsync(cancellationToken);
await plugins.UnloadPluginsAsync(cancellationToken);
}

public async Task ExecuteAsync(CancellationToken cancellationToken)
Expand Down
2 changes: 1 addition & 1 deletion src/Platform/Plugins/PluginDependencyService.cs
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@ public class PluginDependencyService(ILogger<PluginDependencyService> logger) :
private async ValueTask<string?> ResolveAssemblyFromNuGetAsync(AssemblyName assemblyName, CancellationToken cancellationToken = default)
{
var assemblyPath = await ResolveAssemblyFromOfflineNuGetAsync(assemblyName, cancellationToken);

if (string.IsNullOrWhiteSpace(assemblyPath))
assemblyPath = await ResolveAssemblyFromOnlineNuGetAsync(assemblyName, cancellationToken);

Expand Down
179 changes: 69 additions & 110 deletions src/Platform/Plugins/PluginService.cs
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,6 @@
using Void.Proxy.API.Events.Services;
using Void.Proxy.API.Plugins;
using Void.Proxy.Reflection;
using Void.Proxy.Utils;

namespace Void.Proxy.Plugins;

Expand All @@ -17,61 +16,92 @@ public class PluginService(ILogger<PluginService> logger, IEventService events,
private readonly List<WeakPluginReference> _references = [];
private readonly TimeSpan _unloadTimeout = TimeSpan.FromSeconds(10);

public async ValueTask LoadAsync(string path = "plugins", CancellationToken cancellationToken = default)
public async ValueTask LoadEmbeddedPluginsAsync(CancellationToken cancellationToken = default)
{
var assembly = Assembly.GetExecutingAssembly();
var pluginsResources = assembly.GetManifestResourceNames().Where(name => name.Contains(nameof(Plugins)));

foreach (var resourceName in pluginsResources)
{
if (assembly.GetManifestResourceStream(resourceName) is not { } stream)
{
logger.LogWarning("Embedded plugin {PluginName} couldn't be extracted", resourceName);
continue;
}

await LoadPluginsAsync(resourceName, stream, cancellationToken);
stream.Close();
}
}

public async ValueTask LoadPluginsAsync(string path = "plugins", CancellationToken cancellationToken = default)
{
var pluginsDirectoryInfo = new DirectoryInfo(path);

if (!pluginsDirectoryInfo.Exists)
pluginsDirectoryInfo.Create();

await ExtractEmbeddedPluginsAsync(path, cancellationToken);
var pluginsFiles = pluginsDirectoryInfo.GetFiles("*.dll").Select(fileInfo => fileInfo.FullName).ToArray();
logger.LogInformation("Loading {Count} plugins", pluginsFiles.Length);

var pluginPaths = pluginsDirectoryInfo.GetFiles("*.dll").Select(fileInfo => fileInfo.FullName).ToArray();
foreach (var pluginPath in pluginsFiles)
{
await using var stream = File.OpenRead(pluginPath);
await LoadPluginsAsync(Path.GetFileName(pluginPath), stream, cancellationToken);
}
}

logger.LogInformation("Loading {Count} plugins", pluginPaths.Length);
public async ValueTask LoadPluginsAsync(string assemblyName, Stream assemblyStream, CancellationToken cancellationToken = default)
{
var context = new PluginLoadContext(services.GetRequiredService<ILogger<PluginLoadContext>>(), dependencies, assemblyName, assemblyStream);

foreach (var pluginPath in pluginPaths)
{
var context = new PluginLoadContext(services.GetRequiredService<ILogger<PluginLoadContext>>(), dependencies, pluginPath);
logger.LogInformation("Loading {PluginName} plugin", context.Name);

logger.LogInformation("Loading {PluginName} plugin", context.Name);
var plugins = GetPlugins(assemblyName, context.PluginAssembly);

var plugins = RegisterPlugins(context.Name, context.PluginAssembly);
foreach (var plugin in plugins)
RegisterPlugin(plugin);

if (plugins.Length == 0)
{
logger.LogWarning("Plugin {PluginName} has no IPlugin implementations", context.Name);
continue;
}
if (plugins.Length == 0)
{
logger.LogWarning("Plugin {PluginName} has no IPlugin implementations", context.Name);
return;
}

var listeners = context.PluginAssembly.GetTypes().Where(typeof(IEventListener).IsAssignableFrom).Select(CreateListenerInstance).Cast<IEventListener?>().WhereNotNull().ToArray();
var listeners = context.PluginAssembly.GetTypes().Where(typeof(IEventListener).IsAssignableFrom).Select(CreateListenerInstance).Cast<IEventListener?>().WhereNotNull().ToArray();

if (listeners.Length == 0)
logger.LogWarning("Plugin {PluginName} has no event listeners", context.Name);
if (listeners.Length == 0)
logger.LogWarning("Plugin {PluginName} has no event listeners", context.Name);

events.RegisterListeners(listeners);
_references.Add(new WeakPluginReference(context, plugins, listeners));
events.RegisterListeners(listeners);
_references.Add(new WeakPluginReference(context, plugins, listeners));

foreach (var plugin in plugins)
await events.ThrowAsync(new PluginLoadEvent { Plugin = plugin }, cancellationToken);
foreach (var plugin in plugins)
await events.ThrowAsync(new PluginLoadEvent { Plugin = plugin }, cancellationToken);

logger.LogDebug("Loaded {Count} plugins from {PluginName}", plugins.Length, context.Name);
}
logger.LogDebug("Loaded {Count} plugins from {PluginName}", plugins.Length, context.Name);
}

public async ValueTask UnloadPluginsAsync(CancellationToken cancellationToken = default)
{
foreach (var plugin in _references.SelectMany(reference => reference.Plugins)) await UnloadPluginAsync(plugin, cancellationToken);
}

public async ValueTask UnloadAsync(CancellationToken cancellationToken = default)
public async ValueTask UnloadPluginAsync(IPlugin plugin, CancellationToken cancellationToken = default)
{
foreach (var reference in _references)
foreach (var reference in _references.Where(reference => reference.Plugins.Contains(plugin)))
{
if (!reference.IsAlive)
throw new Exception("Plugin context already unloaded");

foreach (var plugin in reference.Plugins)
await events.ThrowAsync(new PluginUnloadEvent { Plugin = plugin }, cancellationToken);

var name = reference.Context.Name;

UnregisterPlugins(reference.Plugins);
foreach (var referencePlugin in reference.Plugins)
{
await events.ThrowAsync(new PluginUnloadEvent { Plugin = referencePlugin }, cancellationToken);
UnregisterPlugin(referencePlugin);
}

events.UnregisterListeners(reference.Listeners);

reference.Context.Unload();
Expand Down Expand Up @@ -100,20 +130,18 @@ public async ValueTask UnloadAsync(CancellationToken cancellationToken = default
_references.RemoveAll(reference => !reference.IsAlive);
}

public IPlugin[] RegisterPlugins(string? name, Assembly assembly)
public IPlugin[] GetPlugins(string assemblyName, Assembly assembly)
{
var pluginInterface = typeof(IPlugin);

try
{
var plugins = assembly.GetTypes().Where(pluginInterface.IsAssignableFrom).Select(CreatePluginInstance).Cast<IPlugin?>().WhereNotNull().ToArray();

_plugins.AddRange(plugins);
return plugins;
}
catch (ReflectionTypeLoadException exception)
{
logger.LogError("Assembly {AssemblyName} cannot be loaded:", name);
logger.LogError("Assembly {AssemblyName} cannot be loaded:", assemblyName);

var noStackTrace = exception.LoaderExceptions.WhereNotNull().Where(loaderException => string.IsNullOrWhiteSpace(loaderException.StackTrace)).ToArray();

Expand All @@ -126,10 +154,14 @@ public IPlugin[] RegisterPlugins(string? name, Assembly assembly)
return [];
}

public void UnregisterPlugins(IPlugin[] plugins)
public void RegisterPlugin(IPlugin plugin)
{
foreach (var plugin in plugins)
_plugins.Remove(plugin);
_plugins.Add(plugin);
}

public void UnregisterPlugin(IPlugin plugin)
{
_plugins.Remove(plugin);
}

internal object? GetExistingInstance(Type type)
Expand All @@ -149,77 +181,4 @@ private object CreatePluginInstance(Type type)

return Activator.CreateInstance(type);
}

private async ValueTask ExtractEmbeddedPluginsAsync(string path, CancellationToken cancellationToken)
{
var platformAssembly = Assembly.GetExecutingAssembly();
var buildDate = platformAssembly.GetCustomAttribute<BuildDateAttribute>();

var embeddedPlugins = platformAssembly.GetManifestResourceNames().Where(name => name.Contains(nameof(Plugins))).Select(embeddedPluginName =>
{
var fileName = Path.Combine(path, embeddedPluginName);

return new
{
ResourceName = embeddedPluginName,
FileName = fileName,
Exists = File.Exists(fileName)
};
}).ToArray();

var extractedOnceBefore = embeddedPlugins.Any(plugin => plugin.Exists);

foreach (var plugin in embeddedPlugins)
{
var upgrading = false;

if (plugin.Exists)
{
if (buildDate is null)
continue;

var extractionDateTime = File.GetLastWriteTimeUtc(plugin.FileName);

if (extractionDateTime >= buildDate.DateTime)
continue;

logger.LogInformation("Upgrading {PluginName} plugin with newest build", plugin.ResourceName);
upgrading = true;
}

if (!upgrading && extractedOnceBefore)
{
logger.LogWarning("Embedded plugin {PluginName} disappeared", plugin.ResourceName);

var key = ConsoleKey.None;
while (key is ConsoleKey.None)
{
logger.LogInformation("Do you want to install it again? [y/n]");

var keyInfo = Console.ReadKey(true);

if (keyInfo.Key is not ConsoleKey.Y and not ConsoleKey.N)
continue;

key = keyInfo.Key;
}

if (key is ConsoleKey.N)
continue;

logger.LogInformation("Proceeding with installation.");
}

if (platformAssembly.GetManifestResourceStream(plugin.ResourceName) is not { } stream)
{
logger.LogWarning("Embedded plugin {PluginName} couldn't be extracted", plugin.ResourceName);
continue;
}

logger.LogInformation("Extracting {PluginName} embedded plugin", plugin.ResourceName);

await using var fileStream = File.OpenWrite(plugin.FileName);
await stream.CopyToAsync(fileStream, cancellationToken);
}
}
}
18 changes: 10 additions & 8 deletions src/Platform/Reflection/PluginLoadContext.cs
Original file line number Diff line number Diff line change
Expand Up @@ -10,17 +10,19 @@ public class PluginLoadContext : AssemblyLoadContext
private static readonly string[] SharedDependencies = [nameof(Microsoft)];
private static readonly string[] SystemDependencies = [nameof(System), "netstandard"];
private readonly IPluginDependencyService _dependencies;
private readonly AssemblyDependencyResolver _localResolver;
private readonly AssemblyDependencyResolver? _localDependencies;

private readonly ILogger<PluginLoadContext> _logger;

public PluginLoadContext(ILogger<PluginLoadContext> logger, IPluginDependencyService dependencies, string pluginPath) : base(Path.GetFileName(pluginPath), true)
public PluginLoadContext(ILogger<PluginLoadContext> logger, IPluginDependencyService dependencies, string assemblyName, Stream assemblyStream, string? componentAssemblyPath = null) : base(assemblyName, true)
{
_logger = logger;
_dependencies = dependencies;

_localResolver = new AssemblyDependencyResolver(pluginPath);
PluginAssembly = LoadFromAssemblyPath(pluginPath);
if (!string.IsNullOrWhiteSpace(componentAssemblyPath))
_localDependencies = new AssemblyDependencyResolver(componentAssemblyPath);

PluginAssembly = LoadFromStream(assemblyStream);
}

public Assembly PluginAssembly { get; }
Expand Down Expand Up @@ -54,8 +56,8 @@ protected override Assembly Load(AssemblyName assemblyName)
return Default.Assemblies.FirstOrDefault(loadedAssembly => loadedAssembly.GetName().Name == assemblyName.Name) ?? Default.LoadFromAssemblyName(assemblyName);

// fallback to local folder and NuGet
var assemblyPath = _localResolver.ResolveAssemblyToPath(assemblyName) ?? _dependencies.ResolveAssemblyPath(assemblyName);
var assemblyPath = _localDependencies?.ResolveAssemblyToPath(assemblyName) ?? _dependencies.ResolveAssemblyPath(assemblyName);

if (assemblyPath is not null)
assembly = LoadFromAssemblyPath(assemblyPath);

Expand All @@ -69,7 +71,7 @@ protected override Assembly Load(AssemblyName assemblyName)

protected override IntPtr LoadUnmanagedDll(string unmanagedDllName)
{
var libraryPath = _localResolver.ResolveUnmanagedDllToPath(unmanagedDllName);
return libraryPath != null ? LoadUnmanagedDllFromPath(libraryPath) : IntPtr.Zero;
var libraryPath = _localDependencies?.ResolveUnmanagedDllToPath(unmanagedDllName);
return libraryPath is null ? IntPtr.Zero : LoadUnmanagedDllFromPath(libraryPath);
}
}

0 comments on commit 99e9eb6

Please sign in to comment.