diff options
author | vnugent <public@vaughnnugent.com> | 2023-09-19 22:11:00 -0400 |
---|---|---|
committer | vnugent <public@vaughnnugent.com> | 2023-09-19 22:11:00 -0400 |
commit | 0b4e18b9a7d8e0aea23aef7efd3707674f223b2b (patch) | |
tree | a46aba32c4dee4e593267e34bc5f0830af7353ee /lib/Plugins.Runtime | |
parent | cdeda79bc7d358c617b05b17d24f3f3c79689379 (diff) |
Experimental plugin runtime updates
Diffstat (limited to 'lib/Plugins.Runtime')
-rw-r--r-- | lib/Plugins.Runtime/src/IAssemblyLoader.cs | 51 | ||||
-rw-r--r-- | lib/Plugins.Runtime/src/IPluginAssemblyLoader.cs | 22 | ||||
-rw-r--r-- | lib/Plugins.Runtime/src/IPluginConfig.cs | 7 | ||||
-rw-r--r-- | lib/Plugins.Runtime/src/IPluginDiscoveryManager.cs | 39 | ||||
-rw-r--r-- | lib/Plugins.Runtime/src/IPluginStack.cs | 47 | ||||
-rw-r--r-- | lib/Plugins.Runtime/src/LivePlugin.cs | 25 | ||||
-rw-r--r-- | lib/Plugins.Runtime/src/LoaderExtensions.cs | 222 | ||||
-rw-r--r-- | lib/Plugins.Runtime/src/PluginController.cs | 6 | ||||
-rw-r--r-- | lib/Plugins.Runtime/src/PluginStackBuilder.cs | 278 | ||||
-rw-r--r-- | lib/Plugins.Runtime/src/RuntimePluginLoader.cs | 83 |
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 |