/* * Copyright (c) 2023 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.Collections.Generic; using VNLib.Utils.IO; using VNLib.Utils.Extensions; namespace VNLib.Plugins.Runtime { /// /// 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 readonly object? _userState; 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, _userState); //Store for unload _service = service; } public void OnPluginUnloaded(PluginController controller, object? state) { //Unload _consumerEvents.OnUnload(_service!, _userState); _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) { _ = runtime ?? throw new ArgumentNullException(nameof(runtime)); foreach(RuntimePluginLoader loader in runtime.Plugins) { loader.InitializeController(); } } /// /// Invokes the load method for all plugin instances /// /// /// /// public static void InvokeLoad(this IPluginStack runtime) { _ = runtime ?? throw new ArgumentNullException(nameof(runtime)); //try loading all plugins runtime.Plugins.TryForeach(static p => p.LoadPlugins()); } /// /// Invokes the unload method for all plugin instances /// /// /// /// public static void InvokeUnload(this IPluginStack runtime) { _ = runtime ?? throw new ArgumentNullException(nameof(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) { _ = runtime ?? throw new ArgumentNullException(nameof(runtime)); //try unloading all plugins and their loaders runtime.Plugins.TryForeach(static p => p.UnloadAll()); } /// /// Reloads all plugins and each assembly loader /// /// /// /// public static void ReloadAll(this IPluginStack runtime) { _ = runtime ?? throw new ArgumentNullException(nameof(runtime)); //try reloading all plugins runtime.Plugins.TryForeach(static p => p.ReloadPlugins()); } /// /// 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) { _ = 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); } } /// /// Unregisters a plugin event listener for all plugins /// /// /// The listener instance to unregister /// 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); } } /// /// 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) { _ = builder ?? throw new ArgumentNullException(nameof(builder)); 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()); } else { //Empty json byte[] emptyJson = Encoding.UTF8.GetBytes("{}"); reader = new LocalFilePluginConfigReader(emptyJson); } //Store binary return builder.WithConfigurationReader(reader); } /// /// Specifies the directory that the plugin loader will search for plugins in /// /// The search directory path /// /// The current builder instance for chaining /// public static PluginStackBuilder WithSearchDirectory(this PluginStackBuilder builder, string path) => WithSearchDirectory(builder, new DirectoryInfo(path)); /// /// 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 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; } /// /// 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 Dir) : IPluginDiscoveryManager { private const string PLUGIN_FILE_EXTENSION = ".dll"; /// public string[] DiscoverPluginFiles() { //Enumerate all dll files within the seach directory IEnumerable dirs = Dir.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) : IPluginConfigReader { public void ReadPluginConfigData(IPluginAssemblyLoadConfig asmConfig, Stream configData) { //Allow comments and trailing commas JsonDocumentOptions jdo = new() { AllowTrailingCommas = true, CommentHandling = JsonCommentHandling.Skip, }; //Config file is the same name as the assembly but with a json extension string pluginConfigFile = Path.ChangeExtension(asmConfig.AssemblyFile, ".json"); using JsonDocument hConfig = JsonDocument.Parse(HostJson, 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(configData); 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(configData); merged.WriteTo(writer); } } } } }