aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--lib/Plugins.Essentials.ServiceStack/src/HttpServiceStackBuilder.cs5
-rw-r--r--lib/Plugins.Essentials.ServiceStack/src/IPluginAssemblyLoaderFactory.cs43
-rw-r--r--lib/Plugins.Essentials.ServiceStack/src/IPluginController.cs5
-rw-r--r--lib/Plugins.Essentials.ServiceStack/src/ManagedPlugin.cs35
-rw-r--r--lib/Plugins.Essentials.ServiceStack/src/PluginLoadConfiguration.cs25
-rw-r--r--lib/Plugins.Essentials.ServiceStack/src/PluginManager.cs34
-rw-r--r--lib/Plugins.PluginBase/src/PluginBase.cs15
-rw-r--r--lib/Plugins.Runtime/README.md88
-rw-r--r--lib/Plugins.Runtime/src/AsmFileWatcher.cs99
-rw-r--r--lib/Plugins.Runtime/src/AssemblyWatcher.cs70
-rw-r--r--lib/Plugins.Runtime/src/IPluginAssemblyLoader.cs57
-rw-r--r--lib/Plugins.Runtime/src/IPluginAssemblyWatcher.cs43
-rw-r--r--lib/Plugins.Runtime/src/IPluginConfig.cs55
-rw-r--r--lib/Plugins.Runtime/src/IPluginReloadEventHandler.cs36
-rw-r--r--lib/Plugins.Runtime/src/LivePlugin.cs23
-rw-r--r--lib/Plugins.Runtime/src/LoaderExtensions.cs9
-rw-r--r--lib/Plugins.Runtime/src/RuntimePluginLoader.cs146
-rw-r--r--lib/Plugins.Runtime/src/VNLib.Plugins.Runtime.csproj1
-rw-r--r--lib/Plugins/src/Attributes/ConfigurationInitalizerAttribute.cs7
19 files changed, 624 insertions, 172 deletions
diff --git a/lib/Plugins.Essentials.ServiceStack/src/HttpServiceStackBuilder.cs b/lib/Plugins.Essentials.ServiceStack/src/HttpServiceStackBuilder.cs
index 25b6d5f..ae3b736 100644
--- a/lib/Plugins.Essentials.ServiceStack/src/HttpServiceStackBuilder.cs
+++ b/lib/Plugins.Essentials.ServiceStack/src/HttpServiceStackBuilder.cs
@@ -23,7 +23,6 @@
*/
using System;
-using System.Threading.Tasks;
using System.Collections.Generic;
using VNLib.Net.Http;
@@ -117,7 +116,7 @@ namespace VNLib.Plugins.Essentials.ServiceStack
/// </summary>
/// <returns>The newly constructed <see cref="HttpServiceStack"/> that may be used to manage your http services</returns>
/// <exception cref="ArgumentNullException"></exception>
- public async Task<HttpServiceStack> BuildAsync(PluginLoadConfiguration config, ILogProvider appLog)
+ public HttpServiceStack Build(IPluginLoadConfiguration config, ILogProvider appLog)
{
_ = _hostBuilder ?? throw new ArgumentNullException("WithDomainBuilder", "You have not configured a service domain configuration callback");
_ = _getServers ?? throw new ArgumentNullException("WithHttp", "You have not configured a IHttpServer configuration callback");
@@ -132,7 +131,7 @@ namespace VNLib.Plugins.Essentials.ServiceStack
}
//Load plugins async
- await sd.PluginManager.LoadPluginsAsync(config, appLog);
+ sd.PluginManager.LoadPlugins(config, appLog);
LinkedList<IHttpServer> servers = new();
diff --git a/lib/Plugins.Essentials.ServiceStack/src/IPluginAssemblyLoaderFactory.cs b/lib/Plugins.Essentials.ServiceStack/src/IPluginAssemblyLoaderFactory.cs
new file mode 100644
index 0000000..4cb5741
--- /dev/null
+++ b/lib/Plugins.Essentials.ServiceStack/src/IPluginAssemblyLoaderFactory.cs
@@ -0,0 +1,43 @@
+/*
+* Copyright (c) 2023 Vaughn Nugent
+*
+* Library: VNLib
+* Package: VNLib.Plugins.Essentials.ServiceStack
+* File: IPluginAssemblyLoaderFactory.cs
+*
+* IPluginAssemblyLoaderFactory.cs is part of VNLib.Plugins.Essentials.ServiceStack
+* which is part of the larger VNLib collection of libraries and utilities.
+*
+* VNLib.Plugins.Essentials.ServiceStack is free software: you can redistribute it and/or modify
+* it under the terms of the GNU Affero General Public License as
+* published by the Free Software Foundation, either version 2 of the
+* License, or (at your option) any later version.
+*
+* VNLib.Plugins.Essentials.ServiceStack is distributed in the hope that it will be useful,
+* but WITHOUT ANY WARRANTY; without even the implied warranty of
+* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+* GNU Affero General Public License for more details.
+*
+* You should have received a copy of the GNU Affero General Public License
+* along with this program. If not, see https://www.gnu.org/licenses/.
+*/
+
+using VNLib.Plugins.Runtime;
+
+namespace VNLib.Plugins.Essentials.ServiceStack
+{
+ /// <summary>
+ /// Represents a provider that creates a unique <see cref="IPluginAssemblyLoader"/> for the the
+ /// loaded assembly file.
+ /// </summary>
+ public interface IPluginAssemblyLoaderFactory
+ {
+ /// <summary>
+ /// Gets a new and unique <see cref="IPluginAssemblyLoader"/> instance for a given plugin assembly
+ /// file path
+ /// </summary>
+ /// <param name="pluginFile">The file path to the requested plugin assembly file</param>
+ /// <returns>The new <see cref="IPluginAssemblyLoader"/> to recover the plugin assembly manifest</returns>
+ IPluginAssemblyLoader GetLoaderForPluginFile(string pluginFile);
+ }
+}
diff --git a/lib/Plugins.Essentials.ServiceStack/src/IPluginController.cs b/lib/Plugins.Essentials.ServiceStack/src/IPluginController.cs
index fb9c340..3394208 100644
--- a/lib/Plugins.Essentials.ServiceStack/src/IPluginController.cs
+++ b/lib/Plugins.Essentials.ServiceStack/src/IPluginController.cs
@@ -23,7 +23,6 @@
*/
using System;
-using System.Threading.Tasks;
using System.Collections.Generic;
using VNLib.Utils.Logging;
@@ -45,10 +44,10 @@ namespace VNLib.Plugins.Essentials.ServiceStack
/// Loads all plugins specified by the host config to the service manager,
/// or attempts to load plugins by the default
/// </summary>
- /// <param name="config">The configuration instance to pass to plugins</param>
+ /// <param name="config">The plugin loading configuration</param>
/// <param name="appLog">A log provider to write message and errors to</param>
/// <returns>A task that resolves when all plugins are loaded</returns>
- Task LoadPluginsAsync(PluginLoadConfiguration config, ILogProvider appLog);
+ void LoadPlugins(IPluginLoadConfiguration config, ILogProvider appLog);
/// <summary>
/// Sends a message to a plugin identified by it's name.
diff --git a/lib/Plugins.Essentials.ServiceStack/src/ManagedPlugin.cs b/lib/Plugins.Essentials.ServiceStack/src/ManagedPlugin.cs
index 3bf99cb..54f0090 100644
--- a/lib/Plugins.Essentials.ServiceStack/src/ManagedPlugin.cs
+++ b/lib/Plugins.Essentials.ServiceStack/src/ManagedPlugin.cs
@@ -27,17 +27,12 @@ using System.IO;
using System.Linq;
using System.Threading;
using System.Reflection;
-using System.Runtime.Loader;
-using System.Threading.Tasks;
using System.ComponentModel.Design;
-using McMaster.NETCore.Plugins;
-
using VNLib.Utils;
using VNLib.Plugins.Runtime;
using VNLib.Plugins.Attributes;
-
namespace VNLib.Plugins.Essentials.ServiceStack
{
@@ -48,39 +43,21 @@ namespace VNLib.Plugins.Essentials.ServiceStack
private UnloadableServiceContainer? _services;
- public ManagedPlugin(string pluginPath, in PluginLoadConfiguration config, IPluginEventListener listener)
+ public ManagedPlugin(RuntimePluginLoader loader, IPluginEventListener listener)
{
- PluginPath = pluginPath;
-
- //Get the plugin config for the assembly
- PluginConfig pConfig = GetConfigForAssemblyPath(pluginPath, in config);
-
//configure the loader
- _plugin = new(pConfig, config.HostConfig, config.PluginErrorLog);
+ _plugin = loader;
- //Register listener before loading occurs
+ //Register loading event listener before loading occurs
_plugin.Controller.Register(this, this);
//Store listener to raise events
_serviceDomainListener = listener;
}
- private static PluginConfig GetConfigForAssemblyPath(string asmPath, in PluginLoadConfiguration loadConfig)
- {
- PluginConfig config = new(asmPath)
- {
- IsUnloadable = loadConfig.HotReload,
- EnableHotReload = loadConfig.HotReload,
- IsLazyLoaded = false,
- PreferSharedTypes = true,
- DefaultContext = AssemblyLoadContext.Default,
- ReloadDelay = loadConfig.ReloadDelay
- };
- return config;
- }
///<inheritdoc/>
- public string PluginPath { get; }
+ public string PluginPath => _plugin.Config.AssemblyFile;
///<inheritdoc/>
public IUnloadableServiceProvider Services
@@ -104,10 +81,10 @@ namespace VNLib.Plugins.Essentials.ServiceStack
internal string PluginFileName => Path.GetFileName(PluginPath);
- internal Task InitializePluginsAsync()
+ internal void InitializePlugins()
{
Check();
- return _plugin.InitializeController();
+ _plugin.InitializeController();
}
internal void LoadPlugins()
diff --git a/lib/Plugins.Essentials.ServiceStack/src/PluginLoadConfiguration.cs b/lib/Plugins.Essentials.ServiceStack/src/PluginLoadConfiguration.cs
index 894ae55..9ab9a82 100644
--- a/lib/Plugins.Essentials.ServiceStack/src/PluginLoadConfiguration.cs
+++ b/lib/Plugins.Essentials.ServiceStack/src/PluginLoadConfiguration.cs
@@ -3,9 +3,9 @@
*
* Library: VNLib
* Package: VNLib.Plugins.Essentials.ServiceStack
-* File: PluginLoadConfiguration.cs
+* File: IPluginLoadConfiguration.cs
*
-* PluginLoadConfiguration.cs is part of VNLib.Plugins.Essentials.ServiceStack
+* IPluginLoadConfiguration.cs is part of VNLib.Plugins.Essentials.ServiceStack
* which is part of the larger VNLib collection of libraries and utilities.
*
* VNLib.Plugins.Essentials.ServiceStack is free software: you can redistribute it and/or modify
@@ -30,38 +30,33 @@ using VNLib.Plugins.Runtime;
namespace VNLib.Plugins.Essentials.ServiceStack
{
+
/// <summary>
/// Plugin loading configuration variables
/// </summary>
- public readonly record struct PluginLoadConfiguration
+ public interface IPluginLoadConfiguration
{
/// <summary>
/// The directory containing the dynamic plugin assemblies to load
/// </summary>
- public readonly string PluginDir { get; init; }
-
- /// <summary>
- /// A value that indicates if the internal <see cref="PluginController"/>
- /// allows for hot-reload/unloadable plugin assemblies.
- /// </summary>
- public readonly bool HotReload { get; init; }
+ string PluginDir { get; }
/// <summary>
/// The optional host configuration file to merge with plugin config
/// to pass to the loading plugin.
/// </summary>
- public readonly JsonElement? HostConfig { get; init; }
+ JsonElement? HostConfig { get; }
/// <summary>
/// Passed to the underlying <see cref="RuntimePluginLoader"/>
/// holding plugins
/// </summary>
- public readonly ILogProvider? PluginErrorLog { get; init; }
+ ILogProvider? PluginErrorLog { get; }
/// <summary>
- /// If hot-reload is enabled, sets a time delay the file watcher waits when
- /// a plugin assembly has changed.
+ /// A factory instance the provides new <see cref="IPluginAssemblyLoader"/> instances
+ /// on demand from its plugin assembly path
/// </summary>
- public readonly TimeSpan ReloadDelay { get; init; }
+ public IPluginAssemblyLoaderFactory AssemblyLoaderFactory { get; init; }
}
}
diff --git a/lib/Plugins.Essentials.ServiceStack/src/PluginManager.cs b/lib/Plugins.Essentials.ServiceStack/src/PluginManager.cs
index e4e5670..2013a58 100644
--- a/lib/Plugins.Essentials.ServiceStack/src/PluginManager.cs
+++ b/lib/Plugins.Essentials.ServiceStack/src/PluginManager.cs
@@ -66,7 +66,7 @@ namespace VNLib.Plugins.Essentials.ServiceStack
/// <inheritdoc/>
/// <exception cref="ObjectDisposedException"></exception>
- public Task LoadPluginsAsync(PluginLoadConfiguration config, ILogProvider appLog)
+ public void LoadPlugins(IPluginLoadConfiguration config, ILogProvider appLog)
{
Check();
@@ -76,10 +76,10 @@ namespace VNLib.Plugins.Essentials.ServiceStack
if (!dir.Exists)
{
appLog.Warn("Plugin directory {dir} does not exist. No plugins were loaded", config.PluginDir);
- return Task.CompletedTask;
+ return;
}
- appLog.Information("Loading plugins. Hot-reload: {en}", config.HotReload);
+ appLog.Information("Loading managed plugins");
//Enumerate all dll files within this dir
IEnumerable<DirectoryInfo> dirs = dir.EnumerateDirectories("*", SearchOption.TopDirectoryOnly);
@@ -91,14 +91,21 @@ namespace VNLib.Plugins.Essentials.ServiceStack
appLog.Debug("Found plugin files: \n{files}", string.Concat(pluginFileNames));
- //Initialze plugin managers
- ManagedPlugin[] wrappers = pluginPaths.Select(pw => new ManagedPlugin(pw, config, this)).ToArray();
+ /*
+ * We need to get the assembly loader for the plugin file, then create its
+ * RuntimePluginLoader which will be passed to the Managed plugin instance
+ */
+
+ ManagedPlugin[] wrappers = pluginPaths.Select(pw => config.AssemblyLoaderFactory.GetLoaderForPluginFile(pw))
+ .Select(l => new RuntimePluginLoader(l, config.HostConfig, config.PluginErrorLog))
+ .Select(loader => new ManagedPlugin(loader, this))
+ .ToArray();
//Add to loaded plugins
_plugins.AddRange(wrappers);
//Load plugins
- return InitiailzeAndLoadAsync(appLog);
+ InitiailzeAndLoad(appLog);
}
private static IEnumerable<string> GetPluginPaths(IEnumerable<DirectoryInfo> dirs)
@@ -118,13 +125,10 @@ namespace VNLib.Plugins.Essentials.ServiceStack
});
}
- private async Task InitiailzeAndLoadAsync(ILogProvider debugLog)
+ private void InitiailzeAndLoad(ILogProvider debugLog)
{
//Load all async
- Task[] initAll = _plugins.Select(p => InitializePlugin(p, debugLog)).ToArray();
-
- //Wait for initalization
- await Task.WhenAll(initAll).ConfigureAwait(false);
+ _plugins.ToArray().TryForeach(p => InitializePlugin(p, debugLog));
//Load stage, load all multithreaded
Parallel.ForEach(_plugins, p => LoadPlugin(p, debugLog));
@@ -132,7 +136,7 @@ namespace VNLib.Plugins.Essentials.ServiceStack
debugLog.Information("Plugin loading completed");
}
- private async Task InitializePlugin(ManagedPlugin plugin, ILogProvider debugLog)
+ private void InitializePlugin(ManagedPlugin plugin, ILogProvider debugLog)
{
void LogAndRemovePlugin(Exception ex)
{
@@ -151,11 +155,7 @@ namespace VNLib.Plugins.Essentials.ServiceStack
try
{
//Load wrapper
- await plugin.InitializePluginsAsync().ConfigureAwait(true);
- }
- catch(AggregateException ae) when (ae.InnerException != null)
- {
- LogAndRemovePlugin(ae.InnerException);
+ plugin.InitializePlugins();
}
catch (Exception ex)
{
diff --git a/lib/Plugins.PluginBase/src/PluginBase.cs b/lib/Plugins.PluginBase/src/PluginBase.cs
index 80860e7..37dafc6 100644
--- a/lib/Plugins.PluginBase/src/PluginBase.cs
+++ b/lib/Plugins.PluginBase/src/PluginBase.cs
@@ -105,11 +105,18 @@ namespace VNLib.Plugins
/// a configuration object to the instance. This method populates the configuration objects if applicable.
/// </summary>
[ConfigurationInitalizer]
- public virtual void InitConfig(JsonDocument config)
+ public virtual void InitConfig(ReadOnlySpan<byte> config)
{
- _ = config ?? throw new ArgumentNullException(nameof(config));
- //Store config ref to dispose properly
- Configuration = config;
+ if (config.IsEmpty)
+ {
+ throw new ArgumentNullException(nameof(config));
+ }
+
+ //reader for the config value
+ Utf8JsonReader reader = new(config);
+
+ //Parse the config
+ Configuration = JsonDocument.ParseValue(ref reader);
}
/// <summary>
diff --git a/lib/Plugins.Runtime/README.md b/lib/Plugins.Runtime/README.md
index 98690bd..ca36900 100644
--- a/lib/Plugins.Runtime/README.md
+++ b/lib/Plugins.Runtime/README.md
@@ -1,55 +1,77 @@
# VNLib.Plugins.Runtime
-A library that manages the runtime loading/unloading of a managed .NET assembly that exposes one or more types that implement the VNLib.Plugins.IPlugin interface, and the plugins lifecycle. The `DynamicPluginLoader` class also handles "hot" assembly reload and exposes lifecycle hooks for applications to correctly detect those changes.
+A structured library for implementing runtime-loaded assemblies that expose types that implement the IPlugin runtime type and manages their lifecycle including unload-able (collectible) assemblies. Type instances are fully managed and carefully exposed as to safely control an instance's lifecycle.
-#### Builds
-Debug build w/ symbols & xml docs, release builds, NuGet packages, and individually packaged source code are available on my [website](https://www.vaughnnugent.com/resources/software). All tar-gzip (.tgz) files will have an associated .sha384 appended checksum of the desired download file.
+### Builds
+Debug build w/ symbols & xml docs, release builds, NuGet packages, and individually packaged source code are available on my [website](https://www.vaughnnugent.com/resources/software/modules). All tar-gzip (.tgz) files will have an associated .sha384 appended checksum of the desired download file.
-### 3rd Party Dependencies
-This library does not, modify, contribute, or affect the functionality of any of the 3rd party libraries below.
+## Implementation notes
+This library may seem over-complicated for managing runtime plugin loading, but it was designed to remedy the common pitfalls of dynamic type loading while also providing implementation freedom to the library consumer/developer. Dependency hell is a difficult issue to work around and I have found it is really up to the application use case to know how, when, and where to load dependencies that will avoid type mismatches but allow for the best interoperability. So that being said, it is your responsibility to implement the `IPluginAssemblyLoader` interface that loads the desired plugin assembly when required and may optionally implement type unloading (likely through a collectable `AssemblyLoadContext`).
-**VNLib.Plugins.Runtime** relies on a single 3rd party library [McMaster.NETCore.Plugins](https://github.com/natemcmaster/DotNetCorePlugins) from [Nate McMaster](https://github.com/natemcmaster) for runtime assembly loading. You must include this dependency in your project.
+#### Type sharing
+You must make sure the [VNLib.Plugins](../Plugins/README.md) assembly is shared between the host and the plugin's load context's, otherwise there will be a Runtime Type mismatch and loading will fail. The *System.CoreLib* must also be shared (this may usually be handled by falling back to the default load context). Configuration data is (optionally) passed to the `IPlugin` with a serialized utf8 JSON binary buffer and assumes your instance will de-serialize the JSON configuration data. If it does, you may want to make sure the JSON library you are using is shared with the plugin assembly. This was changed to passing serialized JSON binary to support future changes and helping avoiding dependency hell by requiring System.Text.Json library be shared, however its not perfect at the moment.
+
+Finally, assuming you wish to use the `IPlugin` library for Http event processing, you will need to share the [VNLib.Net.Plugins](../Net.Http/readme.md) assembly, otherwise event handling will fail, and endpoints may not even register if these types are not shared.
+
+#### Hot Reload
+Hot-reload is managed by this library if you wish to use it by setting the `IPluginConfig.WatchForReload` and the `IPluginConfig.ReloadDelay` properties correctly. Hot reload happens by watching the directory the assembly file resides for file changes. When a change has been detected, your `IPluginAssemblyLoader.Unload()` method is called and is expected to clean up resources and prepare for reloading. Unload may also be manually called if its enabled by the consumer of the `RuntimePluginLoader` instance.
+
+#### Unloading
+Unloading my be enabled, mutually exclusive to the hot-reload system. Which allows the consumer of the `RuntimePluginLoader` to manually unload plugin instances by calling `RuntimePluginLoader.UnloadAll()` and unload your `IPluginAssemblyLoader` instance, which it may then also load again at will. If unloading is disabled by your configuration calls to `RuntimePluginLoader.UnloadAll()` only unloads all plugin instances in the `PluginController` lifecycle controller. Loaded `IPlugin` instances are expected to be no longer in use and eligible for garbage collection after the `RuntimePluginLoader.UnloadAll()`, but this is only guaranteed if the consumer of the plugin respects the unload events and removes all references to ALL loaded types. (Hence the complexity of the event handling system)
## Usage
An XML documentation file is included for all public apis.
+### Consumer notes
+Dynamically loaded `IPlugin` instances are carefully wrapped behind multiple classes to help protect instances from improper consumption in an application, which may have undefined effects in your application. *Note* you should understand runtime assembly loading and how type isolation happens when loading via an `AssemblyLoadContext` paradigm. Plugin consumers are expected to abide by the lifecycle controller's api for proper usage. The lifecycle controller is 'event' driven, but requires registering event handlers, to avoid delegate memory leaks with a more *verbose* api (my preference). You should register your consumer event handlers before calling the `RuntimePluginLoader.LoadPlugins()` method to properly capture the `IPlugin` instance. It is safe to digest the `IPlugin` instance after this method is called by accessing the `plugin.Controller.Plugins` via the lifecycle controller. This method is **NOT** recommended, consumers should capture plugins via the event api and respect the load/unload events.
+
+When the unload event is fired from the lifecycle controller, all references to objects captured from the plugin are expected to be removed as soon as possible to make them eligible for garbage collection, and allow proper unloading.
+
+If you never intend to allow unloading, you may consume the `IPlugin` instances however you like as the protections provided by this library are not required or useful. If the type will never be unloaded, its safe to use everywhere in your application once its loaded.
+
+### Code
```programming language C#
- //RuntimePluginLoader is disposable to cleanup optional config files and cleanup all PluginLoader resources
- using RuntimePluginLoader plugin = new(<fqAssemblyPath>,...<args>);
-
- //Load assembly, optional config file, capture all IPlugin types, then call IPlugin.Load() and all other lifecycle hooks
- await plugin.InitLoaderAsync();
- //Listen for reload events
- plugin.Reloaded += (object? loader, EventAargs = null) =>{};
-
- //Get all endpoints from all exposed plugins
- IEndpoint[] endpoints = plugin.GetEndpoints().ToArray();
-
- //Your plugin types may also expose custom types, you may see if they are available
- if(plugin.ExposesType<IMyCustomType>())
- {
- IMyCustomType mt = plugin.GetExposedTypeFromPlugin<IMyCustomType>();
- }
-
+ //RuntimePluginLoader is disposable to cleanup optional config files and cleanup all resources
+ using RuntimePluginLoader plugin = new(<yourAssemblyLoader>,<hostConfig>,<errorLogProvider>?);
+
+ //Consumer may register an event handler to capture the on-load event to consume the plugin type
+ plugin.Controller.Register(<consumerEventHandler>, <optional state>?);
+
+ //Initializes the internal assemblyLoader, initializes the IPlugin instances into the lifecycle controller
+ plugin.InitializeController();
+
+ //Load all plugins that have been initialized and invokes registered loading event handlers
+ plugin.LoadPlugins();
+
+ //Safe to consume plugins directly from the lifecycle controller, but NOT recommended.
+ plugin.Controller.Plugins.First().Plugin;
+
//Trigger manual reload, will unload, then reload and trigger events
plugin.ReloadPlugin();
- //Unload all plugins
- plugin.UnloadAll();
-
+ //Unload plugins only without unloading provider
+ plugin.UnloadPlugins();
+
+ //Unload all plugins and underlying IPluginAssemblyLoader
+ plugin.UnloadAll();
+
//Leaving scope disposes the loader
```
-### Warnings
-##### Load/Unload/Hot reload
-When hot-reload is disabled and manual reloading is not expected, or unloading is also disabled, you not worry about reload events since the assemblies will never be unloaded. If unloading is disabled and `RuntimePluginLoader.UnloadAll()` is called, only the IPlugin lifecycle hooks will be called (`IPlugin.Unload();`), internal collections are cleared, but no other actions take place.
+## Warnings
-`RuntimePluginLoader.UnloadAll()` Should only be called when you are no longer using the assembly, and all **IPlugin** instances or custom types. The **VNLib.Plugins.Essentials.ServiceStack** library is careful to remove all instances of the exposed plugins, their endpoints, and all other custom types that were exposed, before calling this method.
+#### Security concerns
+Plugins are required to be loading into the same AppDomain as the library consume (no remoting whatsoever) so care must be taken to understand where assemblies are loaded from, knowing the loaded code will have access to the entire AppDomain's memory.
-Disposing the **RuntimePluginLoader** does not unload the plugins, but simply disposes any internal resources disposes the internal **PluginLoader**, so it should only be disposed after `RuntimePluginLoader.UnloadAll()` is called.
+**Hot reload should only be enabled for debugging/development purposes, you should understand the security implications and compatibility of .NET collectable assemblies**
-_Please see [McMaster.NETCore.Plugins](https://github.com/natemcmaster/DotNetCorePlugins) for more information on runtime .NET assembly loading and the dangers of doing so_
+With new api updates, you may consider verifying the plugin assembly (or its entire dependency chain) before loading it into the application domain.
-**Hot reload should only be enabled for debugging/development purposes, you should understand the security implications and compatibility of .NET collectable assemblies**
+#### Consumer warnings
+Again, consumers are expected to respect plugin lifecycle events and properly remove references when the lifecycle controller notifies of an unload event.
+
+Unless event handling is unregistered, events will be raised any time a manual unload/load event is called. Meaning that while it is safe to continually call the `UnloadPlugins()` or the `UnloadAll()` method, events will be raised on every call, even though the `IPlugin` instance references have been destroyed within the lifecycle controller.
+
+It is safe to call `ReloadPlugins()` even after a `UnloadPlugins()` or `UnloadAll()` method has been called, however `ReloadPlugins()` method will raise exceptions if unloading is not enabled.
## License
The software in this repository is licensed under the GNU GPL version 2.0 (or any later version).
diff --git a/lib/Plugins.Runtime/src/AsmFileWatcher.cs b/lib/Plugins.Runtime/src/AsmFileWatcher.cs
new file mode 100644
index 0000000..0aee21b
--- /dev/null
+++ b/lib/Plugins.Runtime/src/AsmFileWatcher.cs
@@ -0,0 +1,99 @@
+/*
+* Copyright (c) 2023 Vaughn Nugent
+*
+* Library: VNLib
+* Package: VNLib.Plugins.Runtime
+* File: AsmFileWatcher.cs
+*
+* AsmFileWatcher.cs is part of VNLib.Plugins.Runtime which is part of the larger
+* VNLib collection of libraries and utilities.
+*
+* VNLib.Plugins.Runtime is free software: you can redistribute it and/or modify
+* it under the terms of the GNU General Public License as published
+* by the Free Software Foundation, either version 2 of the License,
+* or (at your option) any later version.
+*
+* VNLib.Plugins.Runtime is distributed in the hope that it will be useful,
+* but WITHOUT ANY WARRANTY; without even the implied warranty of
+* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+* General Public License for more details.
+*
+* You should have received a copy of the GNU General Public License
+* along with VNLib.Plugins.Runtime. If not, see http://www.gnu.org/licenses/.
+*/
+
+using System.IO;
+using System.Threading;
+
+using VNLib.Utils;
+using VNLib.Utils.Extensions;
+
+namespace VNLib.Plugins.Runtime
+{
+ internal sealed class AsmFileWatcher : VnDisposeable
+ {
+ public IPluginReloadEventHandler Handler { get; }
+
+ private readonly IPluginAssemblyLoader _loaderSource;
+ private readonly Timer _delayTimer;
+ private readonly FileSystemWatcher _watcher;
+
+ private bool _pause;
+
+ public AsmFileWatcher(IPluginAssemblyLoader LoaderSource, IPluginReloadEventHandler handler)
+ {
+ Handler = handler;
+ _loaderSource = LoaderSource;
+
+ string dir = Path.GetDirectoryName(LoaderSource.Config.AssemblyFile)!;
+
+ //Configure watcher to notify only when the assembly file changes
+ _watcher = new FileSystemWatcher(dir)
+ {
+ Filter = "*.dll",
+ EnableRaisingEvents = false,
+ IncludeSubdirectories = false,
+ NotifyFilter = NotifyFilters.LastWrite
+ };
+
+ //Configure listener
+ _watcher.Changed += OnFileChanged;
+
+ _watcher.EnableRaisingEvents = true;
+
+ //setup delay timer to wait on the config
+ _delayTimer = new(OnTimeout, this, Timeout.InfiniteTimeSpan, Timeout.InfiniteTimeSpan);
+ }
+
+ void OnFileChanged(object sender, FileSystemEventArgs e)
+ {
+ //if were already waiting to process an event, we dont need to stage another
+ if (_pause)
+ {
+ return;
+ }
+
+ //Restart the timer to trigger reload event on elapsed
+ _delayTimer.Restart(_loaderSource.Config.ReloadDelay);
+ }
+
+ private void OnTimeout(object? state)
+ {
+ //Fire event
+ Handler.OnPluginUnloaded(_loaderSource);
+ _delayTimer.Stop();
+
+ //Clear pause flag
+ _pause = false;
+ }
+
+ protected override void Free()
+ {
+ _delayTimer.Dispose();
+
+ //Detach event handler and dispose watcher
+ _watcher.Changed -= OnFileChanged;
+ _watcher.Dispose();
+ }
+ }
+}
diff --git a/lib/Plugins.Runtime/src/AssemblyWatcher.cs b/lib/Plugins.Runtime/src/AssemblyWatcher.cs
new file mode 100644
index 0000000..1cf87b0
--- /dev/null
+++ b/lib/Plugins.Runtime/src/AssemblyWatcher.cs
@@ -0,0 +1,70 @@
+/*
+* Copyright (c) 2023 Vaughn Nugent
+*
+* Library: VNLib
+* Package: VNLib.Plugins.Runtime
+* File: AssemblyWatcher.cs
+*
+* AssemblyWatcher.cs is part of VNLib.Plugins.Runtime which is part
+* of the larger VNLib collection of libraries and utilities.
+*
+* VNLib.Plugins.Runtime is free software: you can redistribute it and/or modify
+* it under the terms of the GNU General Public License as published
+* by the Free Software Foundation, either version 2 of the License,
+* or (at your option) any later version.
+*
+* VNLib.Plugins.Runtime is distributed in the hope that it will be useful,
+* but WITHOUT ANY WARRANTY; without even the implied warranty of
+* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+* General Public License for more details.
+*
+* You should have received a copy of the GNU General Public License
+* along with VNLib.Plugins.Runtime. If not, see http://www.gnu.org/licenses/.
+*/
+
+using System.Collections.Generic;
+
+namespace VNLib.Plugins.Runtime
+{
+ internal sealed class AssemblyWatcher : IPluginAssemblyWatcher
+ {
+ private readonly object _lock = new ();
+ private readonly Dictionary<IPluginReloadEventHandler, AsmFileWatcher> _watchers;
+
+ public AssemblyWatcher()
+ {
+ _watchers = new();
+ }
+
+ public void StopWatching(IPluginReloadEventHandler handler)
+ {
+ lock (_lock)
+ {
+ //Find old watcher by its handler, then dispose it
+ if (_watchers.Remove(handler, out AsmFileWatcher? watcher))
+ {
+ //dispose the watcher
+ watcher.Dispose();
+ }
+ }
+ }
+
+ public void WatchAssembly(IPluginReloadEventHandler handler, IPluginAssemblyLoader loader)
+ {
+ lock(_lock)
+ {
+ if(_watchers.Remove(handler, out AsmFileWatcher? watcher))
+ {
+ //dispose the watcher
+ watcher.Dispose();
+ }
+
+ //Queue up a new watcher
+ watcher = new(loader, handler);
+
+ //Store watcher
+ _watchers.Add(handler, watcher);
+ }
+ }
+ }
+}
diff --git a/lib/Plugins.Runtime/src/IPluginAssemblyLoader.cs b/lib/Plugins.Runtime/src/IPluginAssemblyLoader.cs
new file mode 100644
index 0000000..2d04703
--- /dev/null
+++ b/lib/Plugins.Runtime/src/IPluginAssemblyLoader.cs
@@ -0,0 +1,57 @@
+/*
+* Copyright (c) 2023 Vaughn Nugent
+*
+* Library: VNLib
+* Package: VNLib.Plugins.Runtime
+* File: IPluginAssemblyLoader.cs
+*
+* IPluginAssemblyLoader.cs is part of VNLib.Plugins.Runtime which is
+* part of the larger VNLib collection of libraries and utilities.
+*
+* VNLib.Plugins.Runtime is free software: you can redistribute it and/or modify
+* it under the terms of the GNU General Public License as published
+* by the Free Software Foundation, either version 2 of the License,
+* or (at your option) any later version.
+*
+* VNLib.Plugins.Runtime is distributed in the hope that it will be useful,
+* but WITHOUT ANY WARRANTY; without even the implied warranty of
+* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+* General Public License for more details.
+*
+* You should have received a copy of the GNU General Public License
+* along with VNLib.Plugins.Runtime. If not, see http://www.gnu.org/licenses/.
+*/
+
+using System;
+using System.Reflection;
+
+namespace VNLib.Plugins.Runtime
+{
+ /// <summary>
+ /// Represents the bare assembly loader that gets a main assembly for a plugin and handles
+ /// type resolution, while providing loading/unloading
+ /// </summary>
+ public interface IPluginAssemblyLoader : IDisposable
+ {
+ /// <summary>
+ /// Gets the plugin's configuration information
+ /// </summary>
+ IPluginConfig Config { get; }
+
+ /// <summary>
+ /// Unloads the assembly loader if Config.Unloadable is true, otherwise does nothing
+ /// </summary>
+ void Unload();
+
+ /// <summary>
+ /// Prepares the loader for use
+ /// </summary>
+ void Load();
+
+ /// <summary>
+ /// Begins the loading process and recovers the default assembly
+ /// </summary>
+ /// <returns>The main assembly from the assembly file</returns>
+ Assembly GetAssembly();
+ }
+}
diff --git a/lib/Plugins.Runtime/src/IPluginAssemblyWatcher.cs b/lib/Plugins.Runtime/src/IPluginAssemblyWatcher.cs
new file mode 100644
index 0000000..21120b0
--- /dev/null
+++ b/lib/Plugins.Runtime/src/IPluginAssemblyWatcher.cs
@@ -0,0 +1,43 @@
+/*
+* Copyright (c) 2023 Vaughn Nugent
+*
+* Library: VNLib
+* Package: VNLib.Plugins.Runtime
+* File: IPluginAssemblyWatcher.cs
+*
+* IPluginAssemblyWatcher.cs is part of VNLib.Plugins.Runtime which is
+* part of the larger VNLib collection of libraries and utilities.
+*
+* VNLib.Plugins.Runtime is free software: you can redistribute it and/or modify
+* it under the terms of the GNU General Public License as published
+* by the Free Software Foundation, either version 2 of the License,
+* or (at your option) any later version.
+*
+* VNLib.Plugins.Runtime is distributed in the hope that it will be useful,
+* but WITHOUT ANY WARRANTY; without even the implied warranty of
+* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+* General Public License for more details.
+*
+* You should have received a copy of the GNU General Public License
+* along with VNLib.Plugins.Runtime. If not, see http://www.gnu.org/licenses/.
+*/
+
+namespace VNLib.Plugins.Runtime
+{
+ internal interface IPluginAssemblyWatcher
+ {
+ /// <summary>
+ /// Registers a new event handler to watch for plugin file load events if one or more
+ /// files within the plugin's directory changes
+ /// </summary>
+ /// <param name="handler">The handler that wishes to listen for assembly file events</param>
+ /// <param name="loader">The assembly loader to watch for files changes on</param>
+ void WatchAssembly(IPluginReloadEventHandler handler, IPluginAssemblyLoader loader);
+
+ /// <summary>
+ /// Unregisteres an event listener for assembly file events
+ /// </summary>
+ /// <param name="handler">The handler to unregister</param>
+ void StopWatching(IPluginReloadEventHandler handler);
+ }
+}
diff --git a/lib/Plugins.Runtime/src/IPluginConfig.cs b/lib/Plugins.Runtime/src/IPluginConfig.cs
new file mode 100644
index 0000000..c3130f3
--- /dev/null
+++ b/lib/Plugins.Runtime/src/IPluginConfig.cs
@@ -0,0 +1,55 @@
+/*
+* Copyright (c) 2023 Vaughn Nugent
+*
+* Library: VNLib
+* Package: VNLib.Plugins.Runtime
+* File: IPluginConfig.cs
+*
+* IPluginConfig.cs is part of VNLib.Plugins.Runtime which is part
+* of the larger VNLib collection of libraries and utilities.
+*
+* VNLib.Plugins.Runtime is free software: you can redistribute it and/or modify
+* it under the terms of the GNU General Public License as published
+* by the Free Software Foundation, either version 2 of the License,
+* or (at your option) any later version.
+*
+* VNLib.Plugins.Runtime is distributed in the hope that it will be useful,
+* but WITHOUT ANY WARRANTY; without even the implied warranty of
+* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+* General Public License for more details.
+*
+* You should have received a copy of the GNU General Public License
+* along with VNLib.Plugins.Runtime. If not, see http://www.gnu.org/licenses/.
+*/
+
+using System;
+
+namespace VNLib.Plugins.Runtime
+{
+ /// <summary>
+ /// Represents configuration information for a <see cref="IPluginAssemblyLoader"/>
+ /// instance.
+ /// </summary>
+ public interface IPluginConfig
+ {
+ /// <summary>
+ /// A value that indicates if the instance is unlodable.
+ /// </summary>
+ bool Unloadable { get; }
+
+ /// <summary>
+ /// The full file path to the assembly file to load
+ /// </summary>
+ string AssemblyFile { get; }
+
+ /// <summary>
+ /// A value that indicates if the plugin assembly should be watched for reload
+ /// </summary>
+ bool WatchForReload { get; }
+
+ /// <summary>
+ /// The delay which a watcher should wait to trigger a plugin reload after an assembly file changes
+ /// </summary>
+ TimeSpan ReloadDelay { get; }
+ }
+}
diff --git a/lib/Plugins.Runtime/src/IPluginReloadEventHandler.cs b/lib/Plugins.Runtime/src/IPluginReloadEventHandler.cs
new file mode 100644
index 0000000..d29ad8c
--- /dev/null
+++ b/lib/Plugins.Runtime/src/IPluginReloadEventHandler.cs
@@ -0,0 +1,36 @@
+/*
+* Copyright (c) 2023 Vaughn Nugent
+*
+* Library: VNLib
+* Package: VNLib.Plugins.Runtime
+* File: IPluginReloadEventHandler.cs
+*
+* IPluginReloadEventHandler.cs is part of VNLib.Plugins.Runtime which
+* is part of the larger VNLib collection of libraries and utilities.
+*
+* VNLib.Plugins.Runtime is free software: you can redistribute it and/or modify
+* it under the terms of the GNU General Public License as published
+* by the Free Software Foundation, either version 2 of the License,
+* or (at your option) any later version.
+*
+* VNLib.Plugins.Runtime is distributed in the hope that it will be useful,
+* but WITHOUT ANY WARRANTY; without even the implied warranty of
+* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+* General Public License for more details.
+*
+* You should have received a copy of the GNU General Public License
+* along with VNLib.Plugins.Runtime. If not, see http://www.gnu.org/licenses/.
+*/
+
+namespace VNLib.Plugins.Runtime
+{
+ internal interface IPluginReloadEventHandler
+ {
+ /// <summary>
+ /// Called every time a watched <see cref="IPluginAssemblyLoader"/> has a file in the directory
+ /// change
+ /// </summary>
+ /// <param name="loader">The <see cref="IPluginAssemblyLoader"/> that had a file change event occur</param>
+ void OnPluginUnloaded(IPluginAssemblyLoader loader);
+ }
+}
diff --git a/lib/Plugins.Runtime/src/LivePlugin.cs b/lib/Plugins.Runtime/src/LivePlugin.cs
index ae4c90b..67cacb4 100644
--- a/lib/Plugins.Runtime/src/LivePlugin.cs
+++ b/lib/Plugins.Runtime/src/LivePlugin.cs
@@ -27,6 +27,7 @@ using System.Linq;
using System.Reflection;
using System.Text.Json;
+using VNLib.Utils.IO;
using VNLib.Utils.Extensions;
using VNLib.Plugins.Attributes;
@@ -109,18 +110,22 @@ namespace VNLib.Plugins.Runtime
{
return;
}
+
//Merge configurations before passing to plugin
- JsonDocument merged = hostConfig.Merge(pluginConf, "host", PluginType.Name);
- try
- {
- //Invoke
- configInit.Invoke(merged);
- }
- catch
+ using JsonDocument merged = hostConfig.Merge(pluginConf, "host", PluginType.Name);
+
+ //Write the config to binary to pass it to the plugin
+ using VnMemoryStream vms = new();
+ using (Utf8JsonWriter writer = new(vms))
{
- merged.Dispose();
- throw;
+ merged.WriteTo(writer);
}
+
+ //Reset memstream
+ vms.Seek(0, System.IO.SeekOrigin.Begin);
+
+ //Invoke
+ configInit.Invoke(vms.AsSpan());
}
/// <summary>
diff --git a/lib/Plugins.Runtime/src/LoaderExtensions.cs b/lib/Plugins.Runtime/src/LoaderExtensions.cs
index 13fbb11..d4c100b 100644
--- a/lib/Plugins.Runtime/src/LoaderExtensions.cs
+++ b/lib/Plugins.Runtime/src/LoaderExtensions.cs
@@ -26,7 +26,6 @@ using System;
using System.IO;
using System.Linq;
using System.Text.Json;
-using System.Threading.Tasks;
namespace VNLib.Plugins.Runtime
@@ -121,10 +120,10 @@ namespace VNLib.Plugins.Runtime
/// </summary>
/// <param name="loader"></param>
/// <returns>A new <see cref="JsonDocument"/> of the loaded configuration file</returns>
- public static async Task<JsonDocument> GetPluginConfigAsync(this RuntimePluginLoader loader)
+ public static JsonDocument GetPluginConfigAsync(this RuntimePluginLoader loader)
{
//Open and read the config file
- await using FileStream confStream = File.OpenRead(loader.PluginConfigPath);
+ using FileStream confStream = File.OpenRead(loader.PluginConfigPath);
JsonDocumentOptions jdo = new()
{
@@ -133,7 +132,7 @@ namespace VNLib.Plugins.Runtime
};
//parse the plugin config file
- return await JsonDocument.ParseAsync(confStream, jdo);
+ return JsonDocument.Parse(confStream, jdo);
}
/// <summary>
@@ -156,7 +155,7 @@ namespace VNLib.Plugins.Runtime
/// single plugin that derrives the specified type
/// </summary>
/// <typeparam name="T">The type the plugin must derrive from</typeparam>
- /// <param name="loader"></param>
+ /// <param name="collection"></param>
/// <returns>The instance of your custom type casted, or null if not found or could not be casted</returns>
public static T? GetExposedTypes<T>(this PluginController collection) where T: class
{
diff --git a/lib/Plugins.Runtime/src/RuntimePluginLoader.cs b/lib/Plugins.Runtime/src/RuntimePluginLoader.cs
index 83aad21..e581a86 100644
--- a/lib/Plugins.Runtime/src/RuntimePluginLoader.cs
+++ b/lib/Plugins.Runtime/src/RuntimePluginLoader.cs
@@ -26,9 +26,6 @@ using System;
using System.IO;
using System.Text.Json;
using System.Reflection;
-using System.Threading.Tasks;
-
-using McMaster.NETCore.Plugins;
using VNLib.Utils;
using VNLib.Utils.IO;
@@ -40,72 +37,55 @@ namespace VNLib.Plugins.Runtime
/// A runtime .NET assembly loader specialized to load
/// assemblies that export <see cref="IPlugin"/> types.
/// </summary>
- public sealed class RuntimePluginLoader : VnDisposeable
+ public sealed class RuntimePluginLoader : VnDisposeable, IPluginReloadEventHandler
{
- private readonly PluginLoader Loader;
- private readonly string PluginPath;
+ private static readonly IPluginAssemblyWatcher Watcher = new AssemblyWatcher();
+
+ //private readonly IPluginAssemblyWatcher Watcher;
+ private readonly IPluginAssemblyLoader Loader;
private readonly JsonDocument HostConfig;
private readonly ILogProvider? Log;
/// <summary>
- /// Gets the plugin lifetime manager.
+ /// Gets the plugin assembly loader configuration information
+ /// </summary>
+ public IPluginConfig Config => Loader.Config;
+
+ /// <summary>
+ /// Gets the plugin lifecycle controller
/// </summary>
public PluginController Controller { get; }
/// <summary>
/// The path of the plugin's configuration file. (Default = pluginPath.json)
/// </summary>
- public string PluginConfigPath { get; }
-
+ public string PluginConfigPath => Path.ChangeExtension(Config.AssemblyFile, ".json");
+
/// <summary>
/// Creates a new <see cref="RuntimePluginLoader"/> with the specified config and host config dom.
/// </summary>
- /// <param name="config">The plugin's loader configuration </param>
+ /// <param name="loader">The plugin's assembly loader</param>
/// <param name="hostConfig">The host/process configuration DOM</param>
/// <param name="log">A log provider to write plugin unload log events to</param>
/// <exception cref="ArgumentNullException"></exception>
- public RuntimePluginLoader(PluginConfig config, JsonElement? hostConfig, ILogProvider? log)
+ public RuntimePluginLoader(IPluginAssemblyLoader loader, JsonElement? hostConfig, ILogProvider? log)
{
//Default to empty config if null, otherwise clone a copy of the host config element
HostConfig = hostConfig.HasValue ? Clone(hostConfig.Value) : JsonDocument.Parse("{}");
- Loader = new(config);
- PluginPath = config.MainAssemblyPath;
Log = log;
+ Loader = loader;
- //Only regiser reload handler if the load context is unloadable
- if (config.IsUnloadable)
+ //Configure watcher if requested
+ if (loader.Config.WatchForReload)
{
- //Init reloaded event handler
- Loader.Reloaded += Loader_Reloaded;
+ Watcher.WatchAssembly(this, loader);
}
- //Set the config path default
- PluginConfigPath = Path.ChangeExtension(PluginPath, ".json");
-
//Init container
Controller = new();
}
- private async void Loader_Reloaded(object sender, PluginReloadedEventArgs eventArgs)
- {
- try
- {
- //All plugins must be unloaded forst
- UnloadAll();
-
- //Reload the assembly and
- await InitializeController();
-
- //Load plugins
- LoadPlugins();
- }
- catch (Exception ex)
- {
- Log?.Error("Failed reload plugins for {loader}\n{ex}", PluginPath, ex);
- }
- }
-
/// <summary>
/// Initializes the plugin loader, and populates the <see cref="Controller"/>
/// with initialized plugins.
@@ -113,16 +93,19 @@ namespace VNLib.Plugins.Runtime
/// <returns>A task that represents the initialization</returns>
/// <exception cref="IOException"></exception>
/// <exception cref="FileNotFoundException"></exception>
- public async Task InitializeController()
+ public void InitializeController()
{
JsonDocument? pluginConfig = null;
try
{
+ //Prep the assembly loader
+ Loader.Load();
+
//Get the plugin's configuration file
if (FileOperations.FileExists(PluginConfigPath))
{
- pluginConfig = await this.GetPluginConfigAsync();
+ pluginConfig = this.GetPluginConfigAsync();
}
else
{
@@ -131,7 +114,7 @@ namespace VNLib.Plugins.Runtime
}
//Load the main assembly
- Assembly PluginAsm = Loader.LoadDefaultAssembly();
+ Assembly PluginAsm = Loader.GetAssembly();
//Init container from the assembly
Controller.InitializePlugins(PluginAsm);
@@ -156,23 +139,88 @@ namespace VNLib.Plugins.Runtime
public void LoadPlugins() => Controller.LoadPlugins();
/// <summary>
- /// Manually reload the internal <see cref="PluginLoader"/>
- /// which will reload the assembly and its plugins
+ /// Manually reload the internal <see cref="IPluginAssemblyLoader"/>
+ /// which will reload the assembly and re-initialize the controller
+ /// </summary>
+ /// <exception cref="AggregateException"></exception>
+ /// <exception cref="NotSupportedException"></exception>
+ public void ReloadPlugins()
+ {
+ //Not unloadable
+ if (!Loader.Config.Unloadable)
+ {
+ throw new NotSupportedException("The loading context is not unloadable, you may not dynamically reload plugins");
+ }
+
+ //All plugins must be unloaded forst
+ UnloadPlugins();
+
+ //Reload the assembly and
+ InitializeController();
+
+ //Load plugins
+ LoadPlugins();
+ }
+
+ /// <summary>
+ /// Calls the <see cref="IPlugin.Unload"/> method for all plugins within the lifecycle controller
+ /// and invokes the <see cref="IPluginEventListener.OnPluginUnloaded(PluginController, object?)"/>
+ /// for all listeners.
/// </summary>
- public void ReloadPlugins() => Loader.Reload();
+ /// <exception cref="AggregateException"></exception>
+ public void UnloadPlugins() => Controller.UnloadPlugins();
/// <summary>
- /// Attempts to unload all plugins.
+ /// Attempts to unload all plugins within the lifecycle controller, all event handlers
+ /// then attempts to unload the <see cref="IPluginAssemblyLoader"/> if dynamic unloading
+ /// is enabled, otherwise does nothing.
/// </summary>
/// <exception cref="AggregateException"></exception>
- public void UnloadAll() => Controller.UnloadPlugins();
+ public void UnloadAll()
+ {
+ UnloadPlugins();
+
+ //If the assembly loader is unloadable calls its unload method
+ if (Config.Unloadable)
+ {
+ Loader.Unload();
+ }
+ }
+
+ //Process unload events
+
+ void IPluginReloadEventHandler.OnPluginUnloaded(IPluginAssemblyLoader loader)
+ {
+ try
+ {
+ //All plugins must be unloaded before the assembly loader
+ UnloadPlugins();
+
+ //Unload the loader before initializing
+ loader.Unload();
+
+ //Reload the assembly and controller
+ InitializeController();
+
+ //Load plugins
+ LoadPlugins();
+ }
+ catch (Exception ex)
+ {
+ Log?.Error("Failed reload plugins for {loader}\n{ex}", Config.AssemblyFile, ex);
+ }
+ }
///<inheritdoc/>
protected override void Free()
{
+ //Stop watching for events
+ Watcher.StopWatching(this);
+
+ //Cleanup
Controller.Dispose();
- Loader.Dispose();
HostConfig.Dispose();
+ Loader.Dispose();
}
@@ -190,6 +238,6 @@ namespace VNLib.Plugins.Runtime
ms.Seek(0, SeekOrigin.Begin);
return JsonDocument.Parse(ms);
- }
+ }
}
} \ No newline at end of file
diff --git a/lib/Plugins.Runtime/src/VNLib.Plugins.Runtime.csproj b/lib/Plugins.Runtime/src/VNLib.Plugins.Runtime.csproj
index 87dbcfe..80ccdc7 100644
--- a/lib/Plugins.Runtime/src/VNLib.Plugins.Runtime.csproj
+++ b/lib/Plugins.Runtime/src/VNLib.Plugins.Runtime.csproj
@@ -39,7 +39,6 @@
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
- <PackageReference Include="McMaster.NETCore.Plugins" Version="1.4.0" />
</ItemGroup>
<ItemGroup>
diff --git a/lib/Plugins/src/Attributes/ConfigurationInitalizerAttribute.cs b/lib/Plugins/src/Attributes/ConfigurationInitalizerAttribute.cs
index 6903902..73234b4 100644
--- a/lib/Plugins/src/Attributes/ConfigurationInitalizerAttribute.cs
+++ b/lib/Plugins/src/Attributes/ConfigurationInitalizerAttribute.cs
@@ -1,5 +1,5 @@
/*
-* Copyright (c) 2022 Vaughn Nugent
+* Copyright (c) 2023 Vaughn Nugent
*
* Library: VNLib
* Package: VNLib.Plugins
@@ -23,7 +23,6 @@
*/
using System;
-using System.Text.Json;
namespace VNLib.Plugins.Attributes
{
@@ -34,7 +33,7 @@ namespace VNLib.Plugins.Attributes
/// <br></br>
/// A plugin host should invoke this method before <see cref="IPlugin.Load"/>
/// <br></br>
- /// Method signature <code>public void [methodname] (<see cref="JsonDocument"/> config)</code>
+ /// Method signature <code>public void [methodname] (<see cref="ReadOnlySpan{Byte}"/> config)</code>
/// </summary>
[AttributeUsage(AttributeTargets.Method, AllowMultiple = false, Inherited = true)]
public sealed class ConfigurationInitalizerAttribute : Attribute
@@ -44,5 +43,5 @@ namespace VNLib.Plugins.Attributes
/// Represents a safe configuration initializer delegate method
/// </summary>
/// <param name="config">The configuration object that plugin will use</param>
- public delegate void ConfigInitializer(JsonDocument config);
+ public delegate void ConfigInitializer(ReadOnlySpan<byte> config);
}