aboutsummaryrefslogtreecommitdiff
path: root/lib/Plugins.Runtime
diff options
context:
space:
mode:
authorLibravatar vnugent <public@vaughnnugent.com>2023-09-19 22:11:00 -0400
committerLibravatar vnugent <public@vaughnnugent.com>2023-09-19 22:11:00 -0400
commit0b4e18b9a7d8e0aea23aef7efd3707674f223b2b (patch)
treea46aba32c4dee4e593267e34bc5f0830af7353ee /lib/Plugins.Runtime
parentcdeda79bc7d358c617b05b17d24f3f3c79689379 (diff)
Experimental plugin runtime updates
Diffstat (limited to 'lib/Plugins.Runtime')
-rw-r--r--lib/Plugins.Runtime/src/IAssemblyLoader.cs51
-rw-r--r--lib/Plugins.Runtime/src/IPluginAssemblyLoader.cs22
-rw-r--r--lib/Plugins.Runtime/src/IPluginConfig.cs7
-rw-r--r--lib/Plugins.Runtime/src/IPluginDiscoveryManager.cs39
-rw-r--r--lib/Plugins.Runtime/src/IPluginStack.cs47
-rw-r--r--lib/Plugins.Runtime/src/LivePlugin.cs25
-rw-r--r--lib/Plugins.Runtime/src/LoaderExtensions.cs222
-rw-r--r--lib/Plugins.Runtime/src/PluginController.cs6
-rw-r--r--lib/Plugins.Runtime/src/PluginStackBuilder.cs278
-rw-r--r--lib/Plugins.Runtime/src/RuntimePluginLoader.cs83
10 files changed, 653 insertions, 127 deletions
diff --git a/lib/Plugins.Runtime/src/IAssemblyLoader.cs b/lib/Plugins.Runtime/src/IAssemblyLoader.cs
new file mode 100644
index 0000000..816b622
--- /dev/null
+++ b/lib/Plugins.Runtime/src/IAssemblyLoader.cs
@@ -0,0 +1,51 @@
+/*
+* Copyright (c) 2023 Vaughn Nugent
+*
+* Library: VNLib
+* Package: VNLib.Plugins.Runtime
+* File: IAssemblyLoader.cs
+*
+* IAssemblyLoader.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 core assembly loading functionality
+ /// </summary>
+ public interface IAssemblyLoader : IDisposable
+ {
+ /// <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/IPluginAssemblyLoader.cs b/lib/Plugins.Runtime/src/IPluginAssemblyLoader.cs
index 2d04703..d75ac47 100644
--- a/lib/Plugins.Runtime/src/IPluginAssemblyLoader.cs
+++ b/lib/Plugins.Runtime/src/IPluginAssemblyLoader.cs
@@ -22,36 +22,18 @@
* 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
+ public interface IPluginAssemblyLoader : IAssemblyLoader
{
/// <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/IPluginConfig.cs b/lib/Plugins.Runtime/src/IPluginConfig.cs
index c3130f3..a460fc0 100644
--- a/lib/Plugins.Runtime/src/IPluginConfig.cs
+++ b/lib/Plugins.Runtime/src/IPluginConfig.cs
@@ -23,6 +23,7 @@
*/
using System;
+using System.IO;
namespace VNLib.Plugins.Runtime
{
@@ -51,5 +52,11 @@ namespace VNLib.Plugins.Runtime
/// The delay which a watcher should wait to trigger a plugin reload after an assembly file changes
/// </summary>
TimeSpan ReloadDelay { get; }
+
+ /// <summary>
+ /// Reads the host configuration into the given stream
+ /// </summary>
+ /// <param name="outputStream">The stream to write configurationd data to</param>
+ void ReadConfigurationData(Stream outputStream);
}
}
diff --git a/lib/Plugins.Runtime/src/IPluginDiscoveryManager.cs b/lib/Plugins.Runtime/src/IPluginDiscoveryManager.cs
new file mode 100644
index 0000000..234a935
--- /dev/null
+++ b/lib/Plugins.Runtime/src/IPluginDiscoveryManager.cs
@@ -0,0 +1,39 @@
+/*
+* Copyright (c) 2023 Vaughn Nugent
+*
+* Library: VNLib
+* Package: VNLib.Plugins.Runtime
+* File: IPluginDiscoveryManager.cs
+*
+* IPluginDiscoveryManager.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
+{
+ /// <summary>
+ /// Represents a plugin discovery manager that, when requestesd, discovers
+ /// all plugin assembly files.
+ /// </summary>
+ public interface IPluginDiscoveryManager
+ {
+ /// <summary>
+ /// Gets all plugin assembly files that should be loaded
+ /// </summary>
+ /// <returns></returns>
+ string[] DiscoverPluginFiles();
+ }
+}
diff --git a/lib/Plugins.Runtime/src/IPluginStack.cs b/lib/Plugins.Runtime/src/IPluginStack.cs
new file mode 100644
index 0000000..4bbd871
--- /dev/null
+++ b/lib/Plugins.Runtime/src/IPluginStack.cs
@@ -0,0 +1,47 @@
+/*
+* Copyright (c) 2023 Vaughn Nugent
+*
+* Library: VNLib
+* Package: VNLib.Plugins.Runtime
+* File: IPluginStack.cs
+*
+* IPluginStack.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;
+
+namespace VNLib.Plugins.Runtime
+{
+ /// <summary>
+ /// Provides a container and functionality to manage an entire collection
+ /// of plugins.
+ /// </summary>
+ public interface IPluginStack : IDisposable
+ {
+ /// <summary>
+ /// The collection of all plugin loaders
+ /// </summary>
+ IReadOnlyCollection<RuntimePluginLoader> Plugins { get; }
+
+ /// <summary>
+ /// Discovers all plugins for the runtime and populates
+ /// the <see cref="Plugins"/> collection.
+ /// </summary>
+ void BuildStack();
+ }
+} \ No newline at end of file
diff --git a/lib/Plugins.Runtime/src/LivePlugin.cs b/lib/Plugins.Runtime/src/LivePlugin.cs
index 67cacb4..573b520 100644
--- a/lib/Plugins.Runtime/src/LivePlugin.cs
+++ b/lib/Plugins.Runtime/src/LivePlugin.cs
@@ -25,10 +25,7 @@
using System;
using System.Linq;
using System.Reflection;
-using System.Text.Json;
-using VNLib.Utils.IO;
-using VNLib.Utils.Extensions;
using VNLib.Plugins.Attributes;
namespace VNLib.Plugins.Runtime
@@ -97,9 +94,8 @@ namespace VNLib.Plugins.Runtime
/// 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)
+ /// <param name="configData">The host configuration DOM</param>
+ internal void InitConfig(ReadOnlySpan<byte> configData)
{
//Get the console handler method from the plugin instance
MethodInfo? confHan = PluginType.GetMethods().Where(static m => m.GetCustomAttribute<ConfigurationInitalizerAttribute>() != null)
@@ -109,23 +105,10 @@ namespace VNLib.Plugins.Runtime
if (configInit == null)
{
return;
- }
-
- //Merge configurations before passing to plugin
- 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.WriteTo(writer);
- }
-
- //Reset memstream
- vms.Seek(0, System.IO.SeekOrigin.Begin);
+ }
//Invoke
- configInit.Invoke(vms.AsSpan());
+ configInit.Invoke(configData);
}
/// <summary>
diff --git a/lib/Plugins.Runtime/src/LoaderExtensions.cs b/lib/Plugins.Runtime/src/LoaderExtensions.cs
index e244aa6..4dc1253 100644
--- a/lib/Plugins.Runtime/src/LoaderExtensions.cs
+++ b/lib/Plugins.Runtime/src/LoaderExtensions.cs
@@ -26,7 +26,10 @@ using System;
using System.IO;
using System.Linq;
using System.Text.Json;
+using System.Collections.Generic;
+using VNLib.Utils.IO;
+using VNLib.Utils.Extensions;
namespace VNLib.Plugins.Runtime
{
@@ -113,28 +116,7 @@ namespace VNLib.Plugins.Runtime
reg.Register(listener, state);
return new(reg, listener);
}
-
- /// <summary>
- /// Loads the configuration file into its <see cref="JsonDocument"/> format
- /// for reading.
- /// </summary>
- /// <param name="loader"></param>
- /// <returns>A new <see cref="JsonDocument"/> of the loaded configuration file</returns>
- public static JsonDocument GetPluginConfig(this RuntimePluginLoader loader)
- {
- //Open and read the config file
- using FileStream confStream = File.OpenRead(loader.PluginConfigPath);
-
- JsonDocumentOptions jdo = new()
- {
- AllowTrailingCommas = true,
- CommentHandling = JsonCommentHandling.Skip,
- };
-
- //parse the plugin config file
- return JsonDocument.Parse(confStream, jdo);
- }
-
+
/// <summary>
/// Determines if the current <see cref="PluginController"/>
/// exposes the desired type on is <see cref="IPlugin"/>
@@ -165,5 +147,201 @@ namespace VNLib.Plugins.Runtime
return plugin?.Plugin as T;
}
+
+ /// <summary>
+ /// Serially initialzies all plugin lifecycle controllers and configures
+ /// plugin instances.
+ /// </summary>
+ /// <param name="runtime"></param>
+ /// <exception cref="ArgumentNullException"></exception>
+ public static void InitializeAll(this IPluginStack runtime)
+ {
+ _ = runtime ?? throw new ArgumentNullException(nameof(runtime));
+
+ foreach(RuntimePluginLoader loader in runtime.Plugins)
+ {
+ loader.InitializeController();
+ }
+ }
+
+ /// <summary>
+ /// Invokes the load method for all plugin instances
+ /// </summary>
+ /// <param name="runtime"></param>
+ /// <exception cref="ArgumentNullException"></exception>
+ /// <exception cref="AggregateException"></exception>
+ public static void InvokeLoad(this IPluginStack runtime)
+ {
+ _ = runtime ?? throw new ArgumentNullException(nameof(runtime));
+
+ //try loading all plugins
+ runtime.Plugins.TryForeach(static p => p.LoadPlugins());
+ }
+
+ /// <summary>
+ /// Invokes the unload method for all plugin instances
+ /// </summary>
+ /// <param name="runtime"></param>
+ /// <exception cref="ArgumentNullException"></exception>
+ /// <exception cref="AggregateException"></exception>
+ public static void InvokeUnload(this IPluginStack runtime)
+ {
+ _ = runtime ?? throw new ArgumentNullException(nameof(runtime));
+
+ //try unloading all plugins
+ runtime.Plugins.TryForeach(static p => p.UnloadPlugins());
+ }
+
+ /// <summary>
+ /// Unloads all plugins and the plugin assembly loader
+ /// if unloading is supported.
+ /// </summary>
+ /// <param name="runtime"></param>
+ /// <exception cref="ArgumentNullException"></exception>
+ /// <exception cref="AggregateException"></exception>
+ public static void UnloadAll(this IPluginStack runtime)
+ {
+ _ = runtime ?? throw new ArgumentNullException(nameof(runtime));
+
+ //try unloading all plugins and their loaders
+ runtime.Plugins.TryForeach(static p => p.UnloadAll());
+ }
+
+ /// <summary>
+ /// Reloads all plugins and each assembly loader
+ /// </summary>
+ /// <param name="runtime"></param>
+ /// <exception cref="ArgumentNullException"></exception>
+ /// <exception cref="AggregateException"></exception>
+ public static void ReloadAll(this IPluginStack runtime)
+ {
+ _ = runtime ?? throw new ArgumentNullException(nameof(runtime));
+
+ //try reloading all plugins
+ runtime.Plugins.TryForeach(static p => p.ReloadPlugins());
+ }
+
+ /// <summary>
+ /// Registers a plugin event listener for all plugins
+ /// </summary>
+ /// <param name="runtime"></param>
+ /// <param name="listener">The event listener instance</param>
+ /// <param name="state">Optional state parameter</param>
+ /// <exception cref="ArgumentNullException"></exception>
+ public static void RegsiterListener(this IPluginStack runtime, IPluginEventListener listener, object? state = null)
+ {
+ _ = runtime ?? throw new ArgumentNullException(nameof(runtime));
+ _ = listener ?? throw new ArgumentNullException(nameof(listener));
+
+ //Register for all plugins
+ foreach (PluginController controller in runtime.Plugins.Select(static p => p.Controller))
+ {
+ controller.Register(listener, state);
+ }
+ }
+
+ /// <summary>
+ /// Unregisters a plugin event listener for all plugins
+ /// </summary>
+ /// <param name="runtime"></param>
+ /// <param name="listener">The listener instance to unregister</param>
+ /// <exception cref="ArgumentNullException"></exception>
+ public static void UnregsiterListener(this IPluginStack runtime, IPluginEventListener listener)
+ {
+ _ = runtime ?? throw new ArgumentNullException(nameof(runtime));
+ _ = listener ?? throw new ArgumentNullException(nameof(listener));
+
+ //Unregister for all plugins
+ foreach (PluginController controller in runtime.Plugins.Select(static p => p.Controller))
+ {
+ controller.Unregister(listener);
+ }
+ }
+
+ /// <summary>
+ /// Specify the host configuration data to pass to the plugin
+ /// </summary>
+ /// <param name="builder"></param>
+ /// <param name="hostConfig">A configuration element to pass to the plugin's host config element</param>
+ /// <returns>The current builder instance for chaining</returns>
+ public static PluginStackBuilder WithConfigurationData(this PluginStackBuilder builder, JsonElement hostConfig)
+ {
+ _ = builder ?? throw new ArgumentNullException(nameof(builder));
+
+ //Clone the host config into binary
+ using VnMemoryStream ms = new();
+ using (Utf8JsonWriter writer = new(ms))
+ {
+ hostConfig.WriteTo(writer);
+ }
+
+ //Store binary
+ return builder.WithConfigurationData(ms.AsSpan());
+ }
+
+ /// <summary>
+ /// Specifies the directory that the plugin loader will search for plugins in
+ /// </summary>
+ /// <param name="path">The search directory path</param>
+ /// <param name="builder"></param>
+ /// <returns>The current builder instance for chaining</returns>
+ /// <exception cref="ArgumentNullException"></exception>
+ public static PluginStackBuilder WithSearchDirectory(this PluginStackBuilder builder, string path) => WithSearchDirectory(builder, new DirectoryInfo(path));
+
+ /// <summary>
+ /// Specifies the directory that the plugin loader will search for plugins in
+ /// </summary>
+ /// <param name="dir">The search directory instance</param>
+ /// <param name="builder"></param>
+ /// <returns>The current builder instance for chaining</returns>
+ /// <exception cref="ArgumentNullException"></exception>
+ public static PluginStackBuilder WithSearchDirectory(this PluginStackBuilder builder, DirectoryInfo dir)
+ {
+ _ = builder ?? throw new ArgumentNullException(nameof(builder));
+ _ = dir ?? throw new ArgumentNullException(nameof(dir));
+
+ PluginDirectorySearcher dirSearcher = new (dir);
+ builder.WithDiscoveryManager(dirSearcher);
+ return builder;
+ }
+
+ /// <summary>
+ /// Gets the current collection of loaded plugins for the plugin stack
+ /// </summary>
+ /// <param name="stack"></param>
+ /// <returns>An enumeration of all <see cref="LivePlugin"/> wrappers</returns>
+ public static IEnumerable<LivePlugin> GetAllPlugins(this IPluginStack stack) => stack.Plugins.SelectMany(static p => p.Controller.Plugins);
+
+ private sealed record class PluginDirectorySearcher(DirectoryInfo Dir) : IPluginDiscoveryManager
+ {
+ private const string PLUGIN_FILE_EXTENSION = ".dll";
+
+ ///<inheritdoc/>
+ public string[] DiscoverPluginFiles()
+ {
+ //Enumerate all dll files within the seach directory
+ IEnumerable<DirectoryInfo> dirs = Dir.EnumerateDirectories("*", SearchOption.TopDirectoryOnly);
+
+ //Search all directories for plugins and return the paths
+ return GetPluginPaths(dirs).ToArray();
+ }
+
+ private static IEnumerable<string> GetPluginPaths(IEnumerable<DirectoryInfo> dirs)
+ {
+ //Select only dirs with a dll that is named after the directory name
+ return dirs.Where(static pdir =>
+ {
+ string compined = Path.Combine(pdir.FullName, pdir.Name);
+ string FilePath = string.Concat(compined, PLUGIN_FILE_EXTENSION);
+ return FileOperations.FileExists(FilePath);
+ })
+ //Return the name of the dll file to import
+ .Select(static pdir =>
+ {
+ string compined = Path.Combine(pdir.FullName, pdir.Name);
+ return string.Concat(compined, PLUGIN_FILE_EXTENSION);
+ });
+ }
+ }
}
}
diff --git a/lib/Plugins.Runtime/src/PluginController.cs b/lib/Plugins.Runtime/src/PluginController.cs
index 53e0ae3..adb8ff9 100644
--- a/lib/Plugins.Runtime/src/PluginController.cs
+++ b/lib/Plugins.Runtime/src/PluginController.cs
@@ -24,10 +24,10 @@
using System;
using System.Linq;
-using System.Text.Json;
using System.Reflection;
using System.Collections.Generic;
+using VNLib.Utils.IO;
using VNLib.Utils.Extensions;
namespace VNLib.Plugins.Runtime
@@ -105,11 +105,11 @@ namespace VNLib.Plugins.Runtime
}
}
- internal void ConfigurePlugins(JsonDocument hostDom, JsonDocument pluginDom, string[] cliArgs)
+ internal void ConfigurePlugins(VnMemoryStream configData, string[] cliArgs)
{
lock (_stateLock)
{
- _plugins.TryForeach(lp => lp.InitConfig(hostDom, pluginDom));
+ _plugins.TryForeach(lp => lp.InitConfig(configData.AsSpan()));
_plugins.TryForeach(lp => lp.InitLog(cliArgs));
}
}
diff --git a/lib/Plugins.Runtime/src/PluginStackBuilder.cs b/lib/Plugins.Runtime/src/PluginStackBuilder.cs
new file mode 100644
index 0000000..5769f3e
--- /dev/null
+++ b/lib/Plugins.Runtime/src/PluginStackBuilder.cs
@@ -0,0 +1,278 @@
+/*
+* Copyright (c) 2023 Vaughn Nugent
+*
+* Library: VNLib
+* Package: VNLib.Plugins.Runtime
+* File: PluginStackBuilder.cs
+*
+* PluginStackBuilder.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;
+using System.Text.Json;
+using System.Reflection;
+using System.Collections.Generic;
+
+using VNLib.Utils.IO;
+using VNLib.Utils.Logging;
+using VNLib.Utils.Extensions;
+
+namespace VNLib.Plugins.Runtime
+{
+ /// <summary>
+ /// A construction class used to build a single plugin stack.
+ /// </summary>
+ public sealed class PluginStackBuilder
+ {
+ private IPluginDiscoveryManager? DiscoveryManager;
+ private bool HotReload;
+ private TimeSpan ReloadDelay;
+ private byte[]? HostConfigData;
+ private ILogProvider? DebugLog;
+
+ private Func<IPluginConfig, IAssemblyLoader>? Loader;
+
+ public static PluginStackBuilder Create() => new();
+
+ /// <summary>
+ /// Sets the plugin discovery manager used to find plugins
+ /// </summary>
+ /// <param name="discoveryManager">The discovery manager instance</param>
+ /// <returns>The current builder instance for chaining</returns>
+ public PluginStackBuilder WithDiscoveryManager(IPluginDiscoveryManager discoveryManager)
+ {
+ DiscoveryManager = discoveryManager;
+ return this;
+ }
+
+ /// <summary>
+ /// Enables hot reloading of the plugin assembly
+ /// </summary>
+ /// <param name="reloadDelay">The delay time after a change is detected before the assembly is reloaded</param>
+ /// <returns>The current builder instance for chaining</returns>
+ public PluginStackBuilder EnableHotReload(TimeSpan reloadDelay)
+ {
+ HotReload = true;
+ ReloadDelay = reloadDelay;
+ return this;
+ }
+
+ /// <summary>
+ /// Specifies the JSON host configuration data to pass to the plugin
+ /// </summary>
+ /// <param name="hostConfig"></param>
+ /// <returns>The current builder instance for chaining</returns>
+ public PluginStackBuilder WithConfigurationData(ReadOnlySpan<byte> hostConfig)
+ {
+ //Store binary copy
+ HostConfigData = hostConfig.ToArray();
+ return this;
+ }
+
+ /// <summary>
+ /// The factory callback function used to get assembly loaders for
+ /// discovered plugins
+ /// </summary>
+ /// <param name="loaderFactory">The factory callback funtion</param>
+ /// <returns>The current builder instance for chaining</returns>
+ public PluginStackBuilder WithLoaderFactory(Func<IPluginConfig, IAssemblyLoader> loaderFactory)
+ {
+ Loader = loaderFactory;
+ return this;
+ }
+
+ /// <summary>
+ /// Specifies the optional debug log provider to use for the plugin loader.
+ /// </summary>
+ /// <param name="logProvider">The optional log provider instance</param>
+ ///<returns>The current builder instance for chaining</returns>
+ public PluginStackBuilder WithDebugLog(ILogProvider logProvider)
+ {
+ DebugLog = logProvider;
+ return this;
+ }
+
+ /// <summary>
+ /// Creates a snapshot of the current builder state and builds a plugin stack
+ /// </summary>
+ /// <returns>The current builder instance for chaining</returns>
+ /// <exception cref="ArgumentException"></exception>
+ public IPluginStack BuildStack()
+ {
+ _ = DiscoveryManager ?? throw new ArgumentException("You must specify a plugin discovery manager");
+
+ //Create a default config if none was specified
+ HostConfigData ??= GetEmptyConfig();
+
+ //Clone the current builder state
+ PluginStackBuilder clone = (PluginStackBuilder)MemberwiseClone();
+
+ return new PluginStack(clone);
+ }
+
+ private static byte[] GetEmptyConfig() => Encoding.UTF8.GetBytes("{}");
+
+
+ /*
+ *
+ */
+ internal sealed record class PluginStack(PluginStackBuilder Builder) : IPluginStack
+ {
+ private readonly LinkedList<RuntimePluginLoader> _plugins = new();
+
+ ///<inheritdoc/>
+ public IReadOnlyCollection<RuntimePluginLoader> Plugins => _plugins;
+
+ ///<inheritdoc/>
+ public void BuildStack()
+ {
+ //Discover all plugins
+ IPluginAssemblyLoader[] loaders = DiscoverPlugins(Builder.DebugLog);
+
+ //Create a loader for each plugin
+ foreach (IPluginAssemblyLoader loader in loaders)
+ {
+ RuntimePluginLoader plugin = new(loader, Builder.DebugLog);
+ _plugins.AddLast(plugin);
+ }
+ }
+
+ private IPluginAssemblyLoader[] DiscoverPlugins(ILogProvider? debugLog)
+ {
+ //Select only dirs with a dll that is named after the directory name
+ IEnumerable<string> pluginPaths = Builder.DiscoveryManager!.DiscoverPluginFiles();
+
+ //Log the found plugin files
+ IEnumerable<string> pluginFileNames = pluginPaths.Select(static s => $"{Path.GetFileName(s)}\n");
+ debugLog?.Debug("Found plugin assemblies: \n{files}", string.Concat(pluginFileNames));
+
+ LinkedList<IPluginAssemblyLoader> loaders = new ();
+
+ //Create a loader for each plugin
+ foreach (string pluginPath in pluginPaths)
+ {
+ PlugingAssemblyConfig pConf = new(Builder.HostConfigData)
+ {
+ AssemblyFile = pluginPath,
+ WatchForReload = Builder.HotReload,
+ ReloadDelay = Builder.ReloadDelay,
+ Unloadable = Builder.HotReload
+ };
+
+ //Get assembly loader from the configration
+ IAssemblyLoader loader = Builder.Loader!.Invoke(pConf);
+
+ //Add to list
+ loaders.AddLast(new PluginAsmLoader(loader, pConf));
+ }
+
+ return loaders.ToArray();
+ }
+
+
+ ///<inheritdoc/>
+ public void Dispose()
+ {
+ //dispose all plugins
+ _plugins.TryForeach(static p => p.Dispose());
+ _plugins.Clear();
+ }
+ }
+
+ internal sealed record class PluginAsmLoader(IAssemblyLoader Loader, IPluginConfig Config) : IPluginAssemblyLoader
+ {
+ ///<inheritdoc/>
+ public void Dispose() => Loader.Dispose();
+
+ ///<inheritdoc/>
+ public Assembly GetAssembly() => Loader.GetAssembly();
+
+ ///<inheritdoc/>
+ public void Load() => Loader.Load();
+
+ ///<inheritdoc/>
+ public void Unload() => Loader.Unload();
+ }
+
+ internal sealed record class PlugingAssemblyConfig(ReadOnlyMemory<byte> HostConfig) : IPluginConfig
+ {
+ ///<inheritdoc/>
+ public bool Unloadable { get; init; }
+
+ ///<inheritdoc/>
+ public string AssemblyFile { get; init; } = string.Empty;
+
+ ///<inheritdoc/>
+ public bool WatchForReload { get; init; }
+
+ ///<inheritdoc/>
+ public TimeSpan ReloadDelay { get; init; }
+
+ /*
+ * The plugin config file is the same as the plugin assembly file,
+ * but with the .json extension
+ */
+ private string PluginConfigFile => Path.ChangeExtension(AssemblyFile, ".json");
+
+ ///<inheritdoc/>
+ public void ReadConfigurationData(Stream outputStream)
+ {
+ //Allow comments and trailing commas
+ JsonDocumentOptions jdo = new()
+ {
+ AllowTrailingCommas = true,
+ CommentHandling = JsonCommentHandling.Skip,
+ };
+
+ using JsonDocument hConfig = JsonDocument.Parse(HostConfig, jdo);
+
+ //Read the plugin config file
+ if (FileOperations.FileExists(PluginConfigFile))
+ {
+ //Open file stream to read data
+ using FileStream confStream = File.OpenRead(PluginConfigFile);
+
+ //Parse the config file
+ using JsonDocument pConfig = JsonDocument.Parse(confStream, jdo);
+
+ //Merge the configs
+ using JsonDocument merged = hConfig.Merge(pConfig,"host", "plugin");
+
+ //Write the merged config to the output stream
+ using Utf8JsonWriter writer = new(outputStream);
+ merged.WriteTo(writer);
+ }
+ else
+ {
+ byte[] pluginConfig = Encoding.UTF8.GetBytes("{}");
+
+ using JsonDocument pConfig = JsonDocument.Parse(pluginConfig, jdo);
+
+ //Merge the configs
+ using JsonDocument merged = hConfig.Merge(pConfig,"host", "plugin");
+
+ //Write the merged config to the output stream
+ using Utf8JsonWriter writer = new(outputStream);
+ merged.WriteTo(writer);
+ }
+ }
+ }
+ }
+}
diff --git a/lib/Plugins.Runtime/src/RuntimePluginLoader.cs b/lib/Plugins.Runtime/src/RuntimePluginLoader.cs
index 03a15a1..21d4691 100644
--- a/lib/Plugins.Runtime/src/RuntimePluginLoader.cs
+++ b/lib/Plugins.Runtime/src/RuntimePluginLoader.cs
@@ -24,7 +24,6 @@
using System;
using System.IO;
-using System.Text.Json;
using System.Reflection;
using VNLib.Utils;
@@ -42,7 +41,6 @@ namespace VNLib.Plugins.Runtime
private static readonly IPluginAssemblyWatcher Watcher = new AssemblyWatcher();
private readonly IPluginAssemblyLoader Loader;
- private readonly JsonDocument HostConfig;
private readonly ILogProvider? Log;
/// <summary>
@@ -54,11 +52,6 @@ namespace VNLib.Plugins.Runtime
/// 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 => Path.ChangeExtension(Config.AssemblyFile, ".json");
/// <summary>
/// Creates a new <see cref="RuntimePluginLoader"/> with the specified config and host config dom.
@@ -67,14 +60,11 @@ namespace VNLib.Plugins.Runtime
/// <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(IPluginAssemblyLoader loader, JsonElement? hostConfig, ILogProvider? log)
+ public RuntimePluginLoader(IPluginAssemblyLoader loader, ILogProvider? log)
{
Log = log;
Loader = loader ?? throw new ArgumentNullException(nameof(loader));
- //Default to empty config if null, otherwise clone a copy of the host config element
- HostConfig = hostConfig.HasValue ? Clone(hostConfig.Value) : JsonDocument.Parse("{}");
-
//Configure watcher if requested
if (loader.Config.WatchForReload)
{
@@ -94,39 +84,28 @@ namespace VNLib.Plugins.Runtime
/// <exception cref="FileNotFoundException"></exception>
public void InitializeController()
{
- JsonDocument? pluginConfig = null;
+ //Prep the assembly loader
+ Loader.Load();
- try
- {
- //Prep the assembly loader
- Loader.Load();
-
- //Get the plugin's configuration file
- if (FileOperations.FileExists(PluginConfigPath))
- {
- pluginConfig = this.GetPluginConfig();
- }
- else
- {
- //Set plugin config dom to an empty object if the file does not exist
- pluginConfig = JsonDocument.Parse("{}");
- }
-
- //Load the main assembly
- Assembly PluginAsm = Loader.GetAssembly();
-
- //Init container from the assembly
- Controller.InitializePlugins(PluginAsm);
-
- string[] cliArgs = Environment.GetCommandLineArgs();
-
- //Configure log/doms
- Controller.ConfigurePlugins(HostConfig, pluginConfig, cliArgs);
- }
- finally
- {
- pluginConfig?.Dispose();
- }
+ //Load the main assembly
+ Assembly PluginAsm = Loader.GetAssembly();
+
+ //Init container from the assembly
+ Controller.InitializePlugins(PluginAsm);
+
+ string[] cliArgs = Environment.GetCommandLineArgs();
+
+ //Write the config to binary to pass it to the plugin
+ using VnMemoryStream vms = new();
+
+ //Read config data
+ Loader.Config.ReadConfigurationData(vms);
+
+ //Reset memstream
+ vms.Seek(0, SeekOrigin.Begin);
+
+ //Configure log/doms
+ Controller.ConfigurePlugins(vms, cliArgs);
}
/// <summary>
@@ -218,25 +197,7 @@ namespace VNLib.Plugins.Runtime
//Cleanup
Controller.Dispose();
- HostConfig.Dispose();
Loader.Dispose();
}
-
-
- private static JsonDocument Clone(JsonElement hostConfig)
- {
- //Crate ms to write the current doc data to
- using VnMemoryStream ms = new();
-
- using (Utf8JsonWriter writer = new(ms))
- {
- hostConfig.WriteTo(writer);
- }
-
- //Reset ms
- ms.Seek(0, SeekOrigin.Begin);
-
- return JsonDocument.Parse(ms);
- }
}
} \ No newline at end of file