/* * Copyright (c) 2024 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.IO; using System.Linq; using System.Text; using System.Text.Json; using System.Threading.Tasks; using System.Collections.Generic; using VNLib.Utils.IO; using VNLib.Utils.Extensions; namespace VNLib.Plugins.Runtime { /// /// A callback function signature for plugin plugin loading errors on plugin /// stacks. /// /// The loader that the exception occured on /// The exception cause of the error public delegate void PluginLoadErrorHandler(RuntimePluginLoader Loader, Exception exception); /// /// Contains extension methods for PluginLoader library /// public static class LoaderExtensions { /* * Class that manages a collection registration for a specific type * dependency, and redirects the event calls for the consumed service */ private sealed class TypedRegistration : IPluginEventListener where T: class { private readonly ITypedPluginConsumer _consumerEvents; private T? _service; private readonly Type _type; public TypedRegistration(ITypedPluginConsumer consumerEvents, Type type) { _consumerEvents = consumerEvents; _type = type; } public void OnPluginLoaded(PluginController controller, object? state) { //Get the service from the loaded plugins T service = controller.Plugins .Where(pl => _type.IsAssignableFrom(pl.PluginType)) .Select(static pl => (T)pl.Plugin!) .First(); //Call load with the exported type _consumerEvents.OnLoad(service, state); //Store for unload _service = service; } public void OnPluginUnloaded(PluginController controller, object? state) { //Unload _consumerEvents.OnUnload(_service!, state); _service = null; } } /// /// Registers a plugin even handler for the current /// for a specific type. /// /// /// /// The typed plugin instance event consumer /// A handle that manages this event registration /// public static PluginEventRegistration RegisterForType(this PluginController collection, ITypedPluginConsumer consumer) where T: class { Type serviceType = typeof(T); //Confim the type is exposed by this collection if(!ExposesType(collection, serviceType)) { throw new ArgumentException("The requested type is not exposed in this assembly"); } //Create new typed listener TypedRegistration reg = new(consumer, serviceType); //register event handler return Register(collection, reg, null); } /// /// Registers a handler to listen for plugin load/unload events /// /// /// A handle that will unregister the listener when disposed public static PluginEventRegistration Register(this IPluginEventRegistrar reg, IPluginEventListener listener, object? state = null) { reg.Register(listener, state); return new(reg, listener); } /// /// Determines if the current /// exposes the desired type on is /// type. /// /// /// The desired type to request /// True if the plugin exposes the desired type, false otherwise public static bool ExposesType(this PluginController collection, Type type) { return collection.Plugins .Where(pl => type.IsAssignableFrom(pl.PluginType)) .Any(); } /// /// Searches all plugins within the current loader for a /// single plugin that derrives the specified type /// /// The type the plugin must derrive from /// /// The instance of your custom type casted, or null if not found or could not be casted public static T? GetExposedTypes(this PluginController collection) where T: class { LivePlugin? plugin = collection.Plugins .Where(static pl => typeof(T).IsAssignableFrom(pl.PluginType)) .SingleOrDefault(); return plugin?.Plugin as T; } /// /// Serially initialzies all plugin lifecycle controllers and configures /// plugin instances. /// /// /// public static void InitializeAll(this IPluginStack runtime) { ArgumentNullException.ThrowIfNull(runtime, nameof(runtime)); foreach(RuntimePluginLoader loader in runtime.Plugins) { loader.InitializeController(); } } /// /// Invokes the load method for all plugin instances /// /// /// A value that indicates if plugins should be loaded concurrently or sequentially /// /// public static void InvokeLoad(this IPluginStack runtime, bool concurrent) { List exceptions = new (); //Add load exceptions into the list void onError(RuntimePluginLoader loader, Exception ex) => exceptions.Add(ex); //Invoke load with onError callback InvokeLoad(runtime, concurrent, onError); //If any exceptions occured, throw them now if(exceptions.Count > 0) { throw new AggregateException(exceptions); } } /// /// Invokes the load method for all plugin instances, and captures exceptions /// into the specified callback function. /// /// /// A value that indicates if plugins should be loaded concurrently or sequentially /// A callback function to handle error conditions instead of raising exceptions /// public static void InvokeLoad(this IPluginStack runtime, bool concurrent, PluginLoadErrorHandler onError) { ArgumentNullException.ThrowIfNull(runtime, nameof(runtime)); if (concurrent) { //Invoke load in parallel Parallel.ForEach(runtime.Plugins, p => { try { p.LoadPlugins(); } catch (Exception ex) { onError(p, ex); } }); } else { //Load sequentially foreach(RuntimePluginLoader loader in runtime.Plugins) { try { loader.LoadPlugins(); } catch (Exception ex) { onError(loader, ex); } } } } /// /// Invokes the unload method for all plugin instances /// /// /// /// public static void InvokeUnload(this IPluginStack runtime) { ArgumentNullException.ThrowIfNull(runtime); //try unloading all plugins runtime.Plugins.TryForeach(static p => p.UnloadPlugins()); } /// /// Unloads all plugins and the plugin assembly loader /// if unloading is supported. /// /// /// /// public static void UnloadAll(this IPluginStack runtime) { ArgumentNullException.ThrowIfNull(runtime); //try unloading all plugins and their loaders, dont invoke GC on each unload runtime.Plugins.TryForeach(static p => p.UnloadAll(false)); //Invoke a gc GC.Collect(); GC.WaitForPendingFinalizers(); } /// /// Reloads all plugins and each assembly loader /// /// /// /// public static void ReloadAll(this IPluginStack runtime) { ArgumentNullException.ThrowIfNull(runtime, nameof(runtime)); //try reloading all plugins runtime.Plugins.TryForeach(static p => p.ReloadPlugins(false)); //Invoke a gc GC.Collect(); GC.WaitForPendingFinalizers(); } /// /// Registers a plugin event listener for all plugins /// /// /// The event listener instance /// Optional state parameter /// public static void RegsiterListener(this IPluginStack runtime, IPluginEventListener listener, object? state = null) { ArgumentNullException.ThrowIfNull(runtime, nameof(runtime)); ArgumentNullException.ThrowIfNull(listener, nameof(listener)); //Register for all plugins foreach (PluginController controller in runtime.Plugins.Select(static p => p.Controller)) { controller.Register(listener, state); } } /// /// Unregisters a plugin event listener for all plugins /// /// /// The listener instance to unregister /// public static void UnregsiterListener(this IPluginStack runtime, IPluginEventListener listener) { ArgumentNullException.ThrowIfNull(runtime, nameof(runtime)); ArgumentNullException.ThrowIfNull(listener, nameof(listener)); //Unregister for all plugins foreach (PluginController controller in runtime.Plugins.Select(static p => p.Controller)) { controller.Unregister(listener); } } /// /// Configures the plugin stack to retrieve plugin-local json configuration files /// from the same directory as the plugin assembly file. /// /// /// An optional configuration element to pass to the plugin's host config element /// The current builder instance for chaining public static PluginStackBuilder WithLocalJsonConfig( this PluginStackBuilder builder, in JsonElement? hostConfig ) => WithJsonConfig(builder, in hostConfig, null); /// /// Configures the plugin stack to retrieve plugin-local json configuration files /// from the same directory as the plugin assembly file. /// /// /// The directory containing all configuration files for the stack /// An optional configuration element to pass to the plugin's host config element /// The current builder instance for chaining public static PluginStackBuilder WithJsonConfigDir( this PluginStackBuilder builder, in JsonElement? hostConfig, DirectoryInfo configDir ) { /* * Local function forces config files to be located in the * specified directory. */ string AltDirConfigFileFinder(IPluginAssemblyLoadConfig asmConfig) { //Get the plugin config file name string configFileName = Path.ChangeExtension(asmConfig.AssemblyFile, ".json"); configFileName = Path.GetFileName(configFileName); //Search for the file within the config directory return Path.Combine(configDir.FullName, configFileName); } //Use the alternate directory finder return WithJsonConfig(builder, in hostConfig, AltDirConfigFileFinder); } /// /// Configures the plugin stack to retrieve a json configuration file from the specified callbac function, /// or local to the assembly if the callback is null. /// /// /// An optional callback function that finds the plugin json config file from its assembly path /// An optional configuration element to pass to the plugin's host config element /// The current builder instance for chaining public static PluginStackBuilder WithJsonConfig( this PluginStackBuilder builder, in JsonElement? hostConfig, Func? getPluginJsonFile ) { ArgumentNullException.ThrowIfNull(builder, nameof(builder)); //Set default callback if not specified getPluginJsonFile ??= GetDefaultFileNameCallback; LocalFilePluginConfigReader reader; //Host config is optional if (hostConfig.HasValue) { //Clone the host config into binary using VnMemoryStream ms = new(); using (Utf8JsonWriter writer = new(ms)) { hostConfig.Value.WriteTo(writer); } //Create a reader from the binary reader = new LocalFilePluginConfigReader(ms.ToArray(), getPluginJsonFile); } else { //Empty json byte[] emptyJson = Encoding.UTF8.GetBytes("{}"); reader = new LocalFilePluginConfigReader(emptyJson, getPluginJsonFile); } //Store binary return builder.WithConfigurationReader(reader); static string GetDefaultFileNameCallback(IPluginAssemblyLoadConfig asmConfig) { /* * Just changing the asm file extention means the the json file * will be in the same directory as the plugin assembly file */ return Path.ChangeExtension(asmConfig.AssemblyFile, ".json"); } } /// /// Sets an empty/null configuration reader for the plugin stack. No /// configuration will be passed to plugins. /// /// /// The current builder instance for chaining /// public static PluginStackBuilder WithNullConfig(this PluginStackBuilder builder) { ArgumentNullException.ThrowIfNull(builder, nameof(builder)); return builder.WithConfigurationReader(new NullPluginConfigReader()); } /// /// Specifies the directory that the plugin loader will search for plugins in /// /// An array of search directories /// /// The current builder instance for chaining /// public static PluginStackBuilder WithSearchDirectories(this PluginStackBuilder builder, string[] paths) => WithSearchDirectories(builder, paths.Select(static p => new DirectoryInfo(p)).ToArray()); /// /// Specifies the directory that the plugin loader will search for plugins in /// /// The search directory instance /// /// The current builder instance for chaining /// public static PluginStackBuilder WithSearchDirectories(this PluginStackBuilder builder, DirectoryInfo[] dirs) { ArgumentNullException.ThrowIfNull(builder); ArgumentNullException.ThrowIfNull(dirs); builder.WithDiscoveryManager(discoveryManager: new PluginDirectorySearcher(dirs)); return builder; } /// /// Registers a new for the current plugin stack /// that will listen for plugin events and capture the exported services into a /// single pool. /// /// /// A new that will capture all exported services when loaded public static SharedPluginServiceProvider RegisterServiceProvider(this IPluginStack stack) { //Init new service provider SharedPluginServiceProvider provider = new(); //Register for all plugins RegsiterListener(stack, provider); return provider; } /// /// Gets the current collection of loaded plugins for the plugin stack /// /// /// An enumeration of all wrappers public static IEnumerable GetAllPlugins(this IPluginStack stack) => stack.Plugins.SelectMany(static p => p.Controller.Plugins); private sealed record class PluginDirectorySearcher(DirectoryInfo[] SearchDirs) : IPluginDiscoveryManager { private const string PLUGIN_FILE_EXTENSION = ".dll"; /// public string[] DiscoverPluginFiles() { /* * Accumulate all plugin child directores * from the search directories */ IEnumerable dirs = SearchDirs.SelectMany(static p => p.EnumerateDirectories("*", SearchOption.TopDirectoryOnly)); //Search all directories for plugins and return the paths return GetPluginPaths(dirs).ToArray(); } private static IEnumerable GetPluginPaths(IEnumerable 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); }); } } /* * Assumes plugin configuration data is stored in a json file with the same name as * the plugin assembly but with a json extension. * * The json file is local for the specific plugin and is not shared between plugins. The host * configuration is also required */ private sealed record class LocalFilePluginConfigReader(ReadOnlyMemory HostJson, Func GetConfigFilePathCallback) : IPluginConfigReader { public void ReadPluginConfigData(IPluginAssemblyLoadConfig asmConfig, Stream configData) { //Allow comments and trailing commas JsonDocumentOptions jdo = new() { AllowTrailingCommas = true, CommentHandling = JsonCommentHandling.Skip, }; //Get the plugin config file name string pluginConfigFile = GetConfigFilePathCallback(asmConfig); using JsonDocument hostConfig = JsonDocument.Parse(HostJson, jdo); if (FileOperations.FileExists(pluginConfigFile)) { //Open file stream to read data using FileStream pluginConfFileData = File.OpenRead(pluginConfigFile); using JsonDocument pluginConf = JsonDocument.Parse(pluginConfFileData, jdo); using JsonDocument merged = hostConfig.Merge(pluginConf,"host", "plugin"); //Write the merged config to the output stream using Utf8JsonWriter writer = new(configData); merged.WriteTo(writer); } else { byte[] pluginConfig = Encoding.UTF8.GetBytes("{}"); using JsonDocument pluginConf = JsonDocument.Parse(pluginConfig, jdo); using JsonDocument merged = hostConfig.Merge(pluginConf, "host", "plugin"); //Write the merged config to the output stream using Utf8JsonWriter writer = new(configData); merged.WriteTo(writer); } } } private sealed class NullPluginConfigReader : IPluginConfigReader { /// public void ReadPluginConfigData(IPluginAssemblyLoadConfig asmConfig, Stream outputStream) { //Do nothing } } } }