aboutsummaryrefslogtreecommitdiff
path: root/lib/Plugins.Runtime
diff options
context:
space:
mode:
Diffstat (limited to 'lib/Plugins.Runtime')
-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
11 files changed, 530 insertions, 97 deletions
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>