aboutsummaryrefslogtreecommitdiff
path: root/Plugins.Runtime/src
diff options
context:
space:
mode:
authorLibravatar vnugent <public@vaughnnugent.com>2023-01-08 14:44:01 -0500
committerLibravatar vnugent <public@vaughnnugent.com>2023-01-08 14:44:01 -0500
commitbe6dc557a3b819248b014992eb96c1cb21f8112b (patch)
tree5361530552856ba8154bfcfbfac8377549117c9e /Plugins.Runtime/src
parent072a1294646542a73007784d08a35ffcad557b1b (diff)
Initial commit
Diffstat (limited to 'Plugins.Runtime/src')
-rw-r--r--Plugins.Runtime/src/LivePlugin.cs220
-rw-r--r--Plugins.Runtime/src/LoaderExtensions.cs120
-rw-r--r--Plugins.Runtime/src/PluginUnloadExcpetion.cs46
-rw-r--r--Plugins.Runtime/src/RuntimePluginLoader.cs250
-rw-r--r--Plugins.Runtime/src/VNLib.Plugins.Runtime.csproj47
5 files changed, 683 insertions, 0 deletions
diff --git a/Plugins.Runtime/src/LivePlugin.cs b/Plugins.Runtime/src/LivePlugin.cs
new file mode 100644
index 0000000..0001990
--- /dev/null
+++ b/Plugins.Runtime/src/LivePlugin.cs
@@ -0,0 +1,220 @@
+/*
+* Copyright (c) 2022 Vaughn Nugent
+*
+* Library: VNLib
+* Package: VNLib.Plugins.Runtime
+* File: LivePlugin.cs
+*
+* LivePlugin.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.Linq;
+using System.Reflection;
+using System.Text.Json;
+
+using VNLib.Utils.Logging;
+using VNLib.Utils.Extensions;
+using VNLib.Plugins.Attributes;
+
+namespace VNLib.Plugins.Runtime
+{
+ /// <summary>
+ /// <para>
+ /// Wrapper for a loaded <see cref="IPlugin"/> instance, used internally
+ /// for a single instance.
+ /// </para>
+ /// <para>
+ /// Lifetime: for the existance of a single loaded
+ /// plugin instance. Created once per loaded plugin instance. Once the plugin
+ /// is unloaded, it is no longer useable.
+ /// </para>
+ /// </summary>
+ public class LivePlugin : IEquatable<IPlugin>, IEquatable<LivePlugin>
+ {
+ /// <summary>
+ /// The plugin's <see cref="IPlugin.PluginName"/> property during load time
+ /// </summary>
+ /// <exception cref="InvalidOperationException"></exception>
+ public string PluginName => Plugin?.PluginName ?? throw new InvalidOperationException("Plugin is not loaded");
+
+ /// <summary>
+ /// The underlying <see cref="IPlugin"/> that is warpped
+ /// by he current instance
+ /// </summary>
+ public IPlugin? Plugin { get; private set; }
+
+ private readonly Type PluginType;
+
+ private ConsoleEventHandler? PluginConsoleHandler;
+
+ internal LivePlugin(IPlugin plugin)
+ {
+ Plugin = plugin;
+ PluginType = plugin.GetType();
+ GetConsoleHandler();
+ }
+
+ private void GetConsoleHandler()
+ {
+ //Get the console handler method from the plugin instance
+ MethodInfo? handler = (from m in PluginType.GetMethods()
+ where m.GetCustomAttribute<ConsoleEventHandlerAttribute>() != null
+ select m)
+ .FirstOrDefault();
+ //Get a delegate handler for the plugin
+ PluginConsoleHandler = handler?.CreateDelegate<ConsoleEventHandler>(Plugin);
+ }
+
+ /// <summary>
+ /// Sets the plugin's configuration if it defines a <see cref="ConfigurationInitalizerAttribute"/>
+ /// on an instance method
+ /// </summary>
+ /// <param name="hostConfig">The host configuration DOM</param>
+ /// <param name="pluginConf">The plugin local configuration DOM</param>
+ internal void InitConfig(JsonDocument hostConfig, JsonDocument pluginConf)
+ {
+ //Get the console handler method from the plugin instance
+ MethodInfo? confHan = PluginType.GetMethods().Where(static m => m.GetCustomAttribute<ConfigurationInitalizerAttribute>() != null)
+ .FirstOrDefault();
+ //Get a delegate handler for the plugin
+ ConfigInitializer? configInit = confHan?.CreateDelegate<ConfigInitializer>(Plugin);
+ if (configInit == null)
+ {
+ return;
+ }
+ //Merge configurations before passing to plugin
+ JsonDocument merged = hostConfig.Merge(pluginConf, "host", PluginType.Name);
+ try
+ {
+ //Invoke
+ configInit.Invoke(merged);
+ }
+ catch
+ {
+ merged.Dispose();
+ throw;
+ }
+ }
+
+ /// <summary>
+ /// Invokes the plugin's log initalizer method if it defines a <see cref="LogInitializerAttribute"/>
+ /// on an instance method
+ /// </summary>
+ /// <param name="cliArgs">The current process's CLI args</param>
+ internal void InitLog(string[] cliArgs)
+ {
+ //Get the console handler method from the plugin instance
+ MethodInfo? logInit = (from m in PluginType.GetMethods()
+ where m.GetCustomAttribute<LogInitializerAttribute>() != null
+ select m)
+ .FirstOrDefault();
+ //Get a delegate handler for the plugin
+ LogInitializer? logFunc = logInit?.CreateDelegate<LogInitializer>(Plugin);
+ //Invoke
+ logFunc?.Invoke(cliArgs);
+ }
+
+ /// <summary>
+ /// Invokes the plugins console event handler if the type has one
+ /// and the plugin is loaded.
+ /// </summary>
+ /// <param name="message">The message to pass to the plugin handler</param>
+ /// <returns>
+ /// True if the command was sent to the plugin, false if the plugin is
+ /// unloaded or did not export a console event handler
+ /// </returns>
+ public bool SendConsoleMessage(string message)
+ {
+ //Make sure plugin is loaded and has a console handler
+ if (PluginConsoleHandler == null)
+ {
+ return false;
+ }
+ //Invoke plugin console handler
+ PluginConsoleHandler(message);
+ return true;
+ }
+
+ /// <summary>
+ /// Calls the <see cref="IPlugin.Load"/> method on the plugin if its loaded
+ /// </summary>
+ internal void LoadPlugin() => Plugin?.Load();
+
+ /// <summary>
+ /// Unloads all loaded endpoints from
+ /// that they were loaded to, then unloads the plugin.
+ /// </summary>
+ /// <param name="logSink">An optional log provider to write unload exceptions to</param>
+ /// <remarks>
+ /// If <paramref name="logSink"/> is no null unload exceptions are swallowed and written to the log
+ /// </remarks>
+ internal void UnloadPlugin(ILogProvider? logSink)
+ {
+ /*
+ * We need to swallow plugin unload errors to avoid
+ * unknown state, making sure endpoints are properly
+ * unloaded!
+ */
+ try
+ {
+ //Unload the plugin
+ Plugin?.Unload();
+ }
+ catch (Exception ex)
+ {
+ //Create an unload wrapper for the exception
+ PluginUnloadException wrapper = new("Exception raised during plugin unload", ex);
+ if (logSink == null)
+ {
+ throw wrapper;
+ }
+ //Write error to log sink
+ logSink.Error(wrapper);
+ }
+ Plugin = null;
+ PluginConsoleHandler = null;
+ }
+ ///<inheritdoc/>
+ public override bool Equals(object? obj)
+ {
+ Type? pluginType = Plugin?.GetType();
+ Type? otherType = obj?.GetType();
+ if(pluginType == null || otherType == null)
+ {
+ return false;
+ }
+ //If the other plugin is the same type as the current instance return true
+ return pluginType.FullName == otherType.FullName;
+ }
+ ///<inheritdoc/>
+ public bool Equals(LivePlugin? other)
+ {
+ return Equals(other?.Plugin);
+ }
+ ///<inheritdoc/>
+ public bool Equals(IPlugin? other)
+ {
+ return Equals((object?)other);
+ }
+ ///<inheritdoc/>
+ public override int GetHashCode()
+ {
+ return Plugin?.GetHashCode() ?? throw new InvalidOperationException("Plugin is null");
+ }
+ }
+}
diff --git a/Plugins.Runtime/src/LoaderExtensions.cs b/Plugins.Runtime/src/LoaderExtensions.cs
new file mode 100644
index 0000000..795dcf5
--- /dev/null
+++ b/Plugins.Runtime/src/LoaderExtensions.cs
@@ -0,0 +1,120 @@
+/*
+* Copyright (c) 2022 Vaughn Nugent
+*
+* Library: VNLib
+* Package: VNLib.Plugins.Runtime
+* File: LoaderExtensions.cs
+*
+* LoaderExtensions.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.Collections.Generic;
+using System.Linq;
+
+namespace VNLib.Plugins.Runtime
+{
+ public static class LoaderExtensions
+ {
+ /// <summary>
+ /// Searches all plugins within the current loader for a
+ /// single plugin that derrives the specified type
+ /// </summary>
+ /// <typeparam name="T">The type the plugin must derrive from</typeparam>
+ /// <param name="loader"></param>
+ /// <returns>The instance of the plugin that derrives from the specified type</returns>
+ public static LivePlugin? GetExposedPlugin<T>(this RuntimePluginLoader loader)
+ {
+ return loader.LivePlugins
+ .Where(static pl => typeof(T).IsAssignableFrom(pl.Plugin!.GetType()))
+ .SingleOrDefault();
+ }
+
+ /// <summary>
+ /// Searches all plugins within the current loader for a
+ /// single plugin that derrives the specified type
+ /// </summary>
+ /// <typeparam name="T">The type the plugin must derrive from</typeparam>
+ /// <param name="loader"></param>
+ /// <returns>The instance of your custom type casted, or null if not found or could not be casted</returns>
+ public static T? GetExposedTypeFromPlugin<T>(this RuntimePluginLoader loader) where T: class
+ {
+ LivePlugin? plugin = loader.LivePlugins
+ .Where(static pl => typeof(T).IsAssignableFrom(pl.Plugin!.GetType()))
+ .SingleOrDefault();
+
+ return plugin?.Plugin as T;
+ }
+
+ /// <summary>
+ /// Registers a listener delegate method to invoke when the
+ /// current <see cref="RuntimePluginLoader"/> is reloaded, and passes
+ /// the new instance of the specified type
+ /// </summary>
+ /// <typeparam name="T">The single plugin type to register a listener for</typeparam>
+ /// <param name="loader"></param>
+ /// <param name="reloaded">The delegate method to invoke when the loader has reloaded plugins</param>
+ /// <exception cref="ArgumentNullException"></exception>
+ public static bool RegisterListenerForSingle<T>(this RuntimePluginLoader loader, Action<T, T> reloaded) where T: class
+ {
+ _ = reloaded ?? throw new ArgumentNullException(nameof(reloaded));
+
+ //try to get the casted type from the loader
+ T? current = loader.GetExposedTypeFromPlugin<T>();
+
+ if (current == null)
+ {
+ return false;
+ }
+ else
+ {
+ loader.Reloaded += delegate (object? sender, EventArgs args)
+ {
+ RuntimePluginLoader wpl = (sender as RuntimePluginLoader)!;
+ //Get the new loaded type
+ T newT = (wpl.GetExposedPlugin<T>()!.Plugin as T)!;
+ //Invoke reloaded action
+ reloaded(current, newT);
+ //update the new current instance
+ current = newT;
+ };
+
+ return true;
+ }
+ }
+
+ /// <summary>
+ /// Gets all endpoints exposed by all exported plugin instances
+ /// within the current loader
+ /// </summary>
+ /// <param name="loader"></param>
+ /// <returns>An enumeration of all endpoints</returns>
+ public static IEnumerable<IEndpoint> GetEndpoints(this RuntimePluginLoader loader) => loader.LivePlugins.SelectMany(static pl => pl.Plugin!.GetEndpoints());
+
+ /// <summary>
+ /// Determines if any loaded plugin types exposes an instance of the
+ /// specified type
+ /// </summary>
+ /// <typeparam name="T"></typeparam>
+ /// <param name="loader"></param>
+ /// <returns>True if any plugin instance exposes a the specified type, false otherwise</returns>
+ public static bool ExposesType<T>(this RuntimePluginLoader loader) where T : class
+ {
+ return loader.LivePlugins.Any(static pl => typeof(T).IsAssignableFrom(pl.Plugin?.GetType()));
+ }
+ }
+}
diff --git a/Plugins.Runtime/src/PluginUnloadExcpetion.cs b/Plugins.Runtime/src/PluginUnloadExcpetion.cs
new file mode 100644
index 0000000..53f63b2
--- /dev/null
+++ b/Plugins.Runtime/src/PluginUnloadExcpetion.cs
@@ -0,0 +1,46 @@
+/*
+* Copyright (c) 2022 Vaughn Nugent
+*
+* Library: VNLib
+* Package: VNLib.Plugins.Runtime
+* File: PluginUnloadExcpetion.cs
+*
+* PluginUnloadExcpetion.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.Runtime.Serialization;
+
+namespace VNLib.Plugins.Runtime
+{
+ /// <summary>
+ /// A wrapper for exceptions that are raised during an
+ /// assembly plugin unload event. See <see cref="Exception.InnerException"/>
+ /// for details
+ /// </summary>
+ public class PluginUnloadException : Exception
+ {
+ public PluginUnloadException()
+ {}
+ public PluginUnloadException(string message) : base(message)
+ {}
+ public PluginUnloadException(string message, Exception innerException) : base(message, innerException)
+ {}
+ protected PluginUnloadException(SerializationInfo info, StreamingContext context) : base(info, context)
+ {}
+ }
+}
diff --git a/Plugins.Runtime/src/RuntimePluginLoader.cs b/Plugins.Runtime/src/RuntimePluginLoader.cs
new file mode 100644
index 0000000..c688f8b
--- /dev/null
+++ b/Plugins.Runtime/src/RuntimePluginLoader.cs
@@ -0,0 +1,250 @@
+/*
+* Copyright (c) 2022 Vaughn Nugent
+*
+* Library: VNLib
+* Package: VNLib.Plugins.Runtime
+* File: DynamicPluginLoader.cs
+*
+* DynamicPluginLoader.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.IO;
+using System.Linq;
+using System.Text.Json;
+using System.Reflection;
+using System.Runtime.Loader;
+using System.Threading.Tasks;
+using System.Collections.Generic;
+
+using McMaster.NETCore.Plugins;
+
+using VNLib.Utils;
+using VNLib.Utils.IO;
+using VNLib.Utils.Logging;
+using VNLib.Utils.Extensions;
+
+
+namespace VNLib.Plugins.Runtime
+{
+ /// <summary>
+ /// A runtime .NET assembly loader specialized to load
+ /// assemblies that export <see cref="IPlugin"/> types.
+ /// </summary>
+ public class RuntimePluginLoader : VnDisposeable
+ {
+ protected readonly PluginLoader Loader;
+ protected readonly string PluginPath;
+ protected readonly JsonDocument HostConfig;
+ protected readonly ILogProvider? Log;
+ protected readonly LinkedList<LivePlugin> LoadedPlugins;
+
+ /// <summary>
+ /// A readonly collection of all loaded plugin wrappers
+ /// </summary>
+ public IReadOnlyCollection<LivePlugin> LivePlugins => LoadedPlugins;
+
+ /// <summary>
+ /// An event that is raised before the loader
+ /// unloads all plugin instances
+ /// </summary>
+ protected event EventHandler<PluginReloadedEventArgs>? OnBeforeReloaded;
+ /// <summary>
+ /// An event that is raised after a successfull reload of all new
+ /// plugins for the instance
+ /// </summary>
+ protected event EventHandler? OnAfterReloaded;
+
+ /// <summary>
+ /// Raised when the current loader has reloaded the assembly and
+ /// all plugins were successfully loaded.
+ /// </summary>
+ public event EventHandler? Reloaded;
+
+ /// <summary>
+ /// The current plugin's JSON configuration DOM loaded from the plugin's directory
+ /// if it exists. Only valid after first initalization
+ /// </summary>
+ public JsonDocument? PluginConfigDOM { get; private set; }
+ /// <summary>
+ /// Optional loader arguments object for the plugin
+ /// </summary>
+ protected JsonElement? LoaderArgs { get; private set; }
+
+ /// <summary>
+ /// The path of the plugin's configuration file. (Default = pluginPath.json)
+ /// </summary>
+ public string PluginConfigPath { get; init; }
+ /// <summary>
+ /// Creates a new <see cref="RuntimePluginLoader"/> with the specified
+ /// assembly location and host config.
+ /// </summary>
+ /// <param name="pluginPath"></param>
+ /// <param name="log">A nullable log provider</param>
+ /// <param name="hostConfig">The configuration DOM to merge with plugin config DOM and pass to enabled plugins</param>
+ /// <param name="unloadable">A value that specifies if the assembly can be unloaded</param>
+ /// <param name="hotReload">A value that spcifies if the loader will listen for changes to the assembly file and reload the plugins</param>
+ /// <param name="lazy">A value that specifies if assembly dependencies are loaded on-demand</param>
+ /// <remarks>
+ /// The <paramref name="log"/> argument may be null if <paramref name="unloadable"/> is false
+ /// </remarks>
+ /// <exception cref="ArgumentNullException"></exception>
+ public RuntimePluginLoader(string pluginPath, JsonDocument? hostConfig = null, ILogProvider? log = null, bool unloadable = false, bool hotReload = false, bool lazy = false)
+ :this(
+ new PluginConfig(pluginPath)
+ {
+ IsUnloadable = unloadable || hotReload,
+ EnableHotReload = hotReload,
+ IsLazyLoaded = lazy,
+ ReloadDelay = TimeSpan.FromSeconds(1),
+ PreferSharedTypes = true,
+ DefaultContext = AssemblyLoadContext.Default
+ },
+ hostConfig, log)
+ {
+ }
+ /// <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="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, JsonDocument? hostConfig, ILogProvider? log)
+ {
+ //Add the assembly from which the IPlugin library was loaded from
+ config.SharedAssemblies.Add(typeof(IPlugin).Assembly.GetName());
+
+ //Default to empty config if null
+ HostConfig = hostConfig ?? JsonDocument.Parse("{}");
+ Loader = new(config);
+ PluginPath = config.MainAssemblyPath;
+ Log = log;
+ Loader.Reloaded += Loader_Reloaded;
+ //Set the config path default
+ PluginConfigPath = Path.ChangeExtension(PluginPath, ".json");
+ LoadedPlugins = new();
+ }
+
+ private async void Loader_Reloaded(object sender, PluginReloadedEventArgs eventArgs)
+ {
+ try
+ {
+ //Invoke reloaded events
+ OnBeforeReloaded?.Invoke(this, eventArgs);
+ //Unload all endpoints
+ LoadedPlugins.TryForeach(lp => lp.UnloadPlugin(Log));
+ //Clear list of loaded plugins
+ LoadedPlugins.Clear();
+ //Unload the plugin config
+ PluginConfigDOM?.Dispose();
+ //Reload the assembly and
+ await InitLoaderAsync();
+ //fire after loaded
+ OnAfterReloaded?.Invoke(this, eventArgs);
+ //Raise the external reloaded event
+ Reloaded?.Invoke(this, EventArgs.Empty);
+ }
+ catch (Exception ex)
+ {
+ Log?.Error(ex);
+ }
+ }
+
+ /// <summary>
+ /// Initializes the plugin loader, the assembly, and all public <see cref="IPlugin"/>
+ /// types
+ /// </summary>
+ /// <returns>A task that represents the initialization</returns>
+ public async Task InitLoaderAsync()
+ {
+ //Load the main assembly
+ Assembly PluginAsm = Loader.LoadDefaultAssembly();
+ //Get the plugin's configuration file
+ if (FileOperations.FileExists(PluginConfigPath))
+ {
+ //Open and read the config file
+ await using FileStream confStream = File.OpenRead(PluginConfigPath);
+ JsonDocumentOptions jdo = new()
+ {
+ AllowTrailingCommas = true,
+ CommentHandling = JsonCommentHandling.Skip,
+ };
+ //parse the plugin config file
+ PluginConfigDOM = await JsonDocument.ParseAsync(confStream, jdo);
+ //Store the config loader args
+ if (PluginConfigDOM.RootElement.TryGetProperty("loader_args", out JsonElement loaderEl))
+ {
+ LoaderArgs = loaderEl;
+ }
+ }
+ else
+ {
+ //Set plugin config dom to an empty object if the file does not exist
+ PluginConfigDOM = JsonDocument.Parse("{}");
+ LoaderArgs = null;
+ }
+
+ string[] cliArgs = Environment.GetCommandLineArgs();
+
+ //Get all types that implement the IPlugin interface
+ IEnumerable<IPlugin> plugins = PluginAsm.GetTypes().Where(static type => !type.IsAbstract && typeof(IPlugin).IsAssignableFrom(type))
+ //Create the plugin instances
+ .Select(static type => (Activator.CreateInstance(type) as IPlugin)!);
+ //Load all plugins that implement the Iplugin interface
+ foreach (IPlugin plugin in plugins)
+ {
+ //Load wrapper
+ LivePlugin lp = new(plugin);
+ try
+ {
+ //Init config
+ lp.InitConfig(HostConfig, PluginConfigDOM);
+ //Init log handler
+ lp.InitLog(cliArgs);
+ //Load the plugin
+ lp.LoadPlugin();
+ //Create new plugin loader for the plugin
+ LoadedPlugins.AddLast(lp);
+ }
+ catch (TargetInvocationException te) when (te.InnerException is not null)
+ {
+ throw te.InnerException;
+ }
+ }
+ }
+ /// <summary>
+ /// Manually reload the internal <see cref="PluginLoader"/>
+ /// which will reload the assembly and its plugins and endpoints
+ /// </summary>
+ public void ReloadPlugin() => Loader.Reload();
+
+ /// <summary>
+ /// Attempts to unload all plugins.
+ /// </summary>
+ /// <exception cref="AggregateException"></exception>
+ public void UnloadAll() => LoadedPlugins.TryForeach(lp => lp.UnloadPlugin(Log));
+
+ ///<inheritdoc/>
+ protected override void Free()
+ {
+ Loader.Dispose();
+ PluginConfigDOM?.Dispose();
+ }
+
+ }
+} \ No newline at end of file
diff --git a/Plugins.Runtime/src/VNLib.Plugins.Runtime.csproj b/Plugins.Runtime/src/VNLib.Plugins.Runtime.csproj
new file mode 100644
index 0000000..d435245
--- /dev/null
+++ b/Plugins.Runtime/src/VNLib.Plugins.Runtime.csproj
@@ -0,0 +1,47 @@
+<Project Sdk="Microsoft.NET.Sdk">
+
+ <PropertyGroup>
+ <Nullable>enable</Nullable>
+ <TargetFramework>net6.0</TargetFramework>
+ <Authors>Vaughn Nugent</Authors>
+ <Copyright>Copyright © 2022 Vaughn Nugent</Copyright>
+ <Description>A runtime plugin loader for .NET. Allows runtime loading and tracking of .NET assemblies
+that export the VNLib.Plugin.IPlugin interface.</Description>
+ <Version>1.0.1.1</Version>
+ <PackageProjectUrl>https://www.vaughnnugent.com/resources</PackageProjectUrl>
+ <SignAssembly>True</SignAssembly>
+ <AssemblyOriginatorKeyFile>\\vaughnnugent.com\Internal\Folder Redirection\vman\Documents\Programming\Software\StrongNameingKey.snk</AssemblyOriginatorKeyFile>
+ </PropertyGroup>
+
+ <!-- Resolve nuget dll files and store them in the output dir -->
+ <PropertyGroup>
+ <CopyLocalLockFileAssemblies>true</CopyLocalLockFileAssemblies>
+ <GenerateDocumentationFile>True</GenerateDocumentationFile>
+ <AnalysisLevel>latest-all</AnalysisLevel>
+ </PropertyGroup>
+ <PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Debug|AnyCPU'">
+ <Deterministic>False</Deterministic>
+ </PropertyGroup>
+ <PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Release|AnyCPU'">
+ <Deterministic>False</Deterministic>
+ </PropertyGroup>
+
+
+ <ItemGroup>
+ <PackageReference Include="ErrorProne.NET.CoreAnalyzers" Version="0.1.2">
+ <PrivateAssets>all</PrivateAssets>
+ <IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
+ </PackageReference>
+ <PackageReference Include="ErrorProne.NET.Structs" Version="0.1.2">
+ <PrivateAssets>all</PrivateAssets>
+ <IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
+ </PackageReference>
+ <PackageReference Include="McMaster.NETCore.Plugins" Version="1.4.0" />
+ </ItemGroup>
+
+ <ItemGroup>
+ <ProjectReference Include="..\..\Essentials\src\VNLib.Plugins.Essentials.csproj" />
+ <ProjectReference Include="..\..\Utils\src\VNLib.Utils.csproj" />
+ </ItemGroup>
+
+</Project>