/* * 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 { /// /// A construction class used to build a single plugin stack. /// public sealed class PluginStackBuilder { private IPluginDiscoveryManager? DiscoveryManager; private bool HotReload; private TimeSpan ReloadDelay; private byte[]? HostConfigData; private ILogProvider? DebugLog; private Func? Loader; /// /// Shortcut constructor for easy fluent chaining. /// /// A new public static PluginStackBuilder Create() => new(); /// /// Sets the plugin discovery manager used to find plugins /// /// The discovery manager instance /// The current builder instance for chaining public PluginStackBuilder WithDiscoveryManager(IPluginDiscoveryManager discoveryManager) { DiscoveryManager = discoveryManager; return this; } /// /// Enables hot reloading of the plugin assembly /// /// The delay time after a change is detected before the assembly is reloaded /// The current builder instance for chaining public PluginStackBuilder EnableHotReload(TimeSpan reloadDelay) { HotReload = true; ReloadDelay = reloadDelay; return this; } /// /// Specifies the JSON host configuration data to pass to the plugin /// /// /// The current builder instance for chaining public PluginStackBuilder WithConfigurationData(ReadOnlySpan hostConfig) { //Store binary copy HostConfigData = hostConfig.ToArray(); return this; } /// /// The factory callback function used to get assembly loaders for /// discovered plugins /// /// The factory callback funtion /// The current builder instance for chaining public PluginStackBuilder WithLoaderFactory(Func loaderFactory) { Loader = loaderFactory; return this; } /// /// Specifies the optional debug log provider to use for the plugin loader. /// /// The optional log provider instance ///The current builder instance for chaining public PluginStackBuilder WithDebugLog(ILogProvider logProvider) { DebugLog = logProvider; return this; } /// /// Creates a snapshot of the current builder state and builds a plugin stack /// /// The current builder instance for chaining /// public IPluginStack ConfigureStack() { _ = 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 _plugins = new(); /// public IReadOnlyCollection Plugins => _plugins; /// 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 pluginPaths = Builder.DiscoveryManager!.DiscoverPluginFiles(); //Log the found plugin files IEnumerable pluginFileNames = pluginPaths.Select(static s => $"{Path.GetFileName(s)}\n"); debugLog?.Debug("Found plugin assemblies: \n{files}", string.Concat(pluginFileNames)); LinkedList 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(); } /// public void Dispose() { //dispose all plugins _plugins.TryForeach(static p => p.Dispose()); _plugins.Clear(); } } internal sealed record class PluginAsmLoader(IAssemblyLoader Loader, IPluginConfig Config) : IPluginAssemblyLoader { /// public void Dispose() => Loader.Dispose(); /// public Assembly GetAssembly() => Loader.GetAssembly(); /// public void Load() => Loader.Load(); /// public void Unload() => Loader.Unload(); } internal sealed record class PlugingAssemblyConfig(ReadOnlyMemory HostConfig) : IPluginConfig { /// public bool Unloadable { get; init; } /// public string AssemblyFile { get; init; } = string.Empty; /// public bool WatchForReload { get; init; } /// 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"); /// 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); } } } } }