From be6dc557a3b819248b014992eb96c1cb21f8112b Mon Sep 17 00:00:00 2001 From: vnugent Date: Sun, 8 Jan 2023 14:44:01 -0500 Subject: Initial commit --- Plugins.Runtime/src/LivePlugin.cs | 220 ++++++++++++++++++++ Plugins.Runtime/src/LoaderExtensions.cs | 120 +++++++++++ Plugins.Runtime/src/PluginUnloadExcpetion.cs | 46 +++++ Plugins.Runtime/src/RuntimePluginLoader.cs | 250 +++++++++++++++++++++++ Plugins.Runtime/src/VNLib.Plugins.Runtime.csproj | 47 +++++ 5 files changed, 683 insertions(+) create mode 100644 Plugins.Runtime/src/LivePlugin.cs create mode 100644 Plugins.Runtime/src/LoaderExtensions.cs create mode 100644 Plugins.Runtime/src/PluginUnloadExcpetion.cs create mode 100644 Plugins.Runtime/src/RuntimePluginLoader.cs create mode 100644 Plugins.Runtime/src/VNLib.Plugins.Runtime.csproj (limited to 'Plugins.Runtime/src') diff --git a/Plugins.Runtime/src/LivePlugin.cs b/Plugins.Runtime/src/LivePlugin.cs new file mode 100644 index 0000000..0001990 --- /dev/null +++ b/Plugins.Runtime/src/LivePlugin.cs @@ -0,0 +1,220 @@ +/* +* Copyright (c) 2022 Vaughn Nugent +* +* Library: VNLib +* Package: VNLib.Plugins.Runtime +* File: LivePlugin.cs +* +* LivePlugin.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.Linq; +using System.Reflection; +using System.Text.Json; + +using VNLib.Utils.Logging; +using VNLib.Utils.Extensions; +using VNLib.Plugins.Attributes; + +namespace VNLib.Plugins.Runtime +{ + /// + /// + /// Wrapper for a loaded instance, used internally + /// for a single instance. + /// + /// + /// Lifetime: for the existance of a single loaded + /// plugin instance. Created once per loaded plugin instance. Once the plugin + /// is unloaded, it is no longer useable. + /// + /// + public class LivePlugin : IEquatable, IEquatable + { + /// + /// The plugin's property during load time + /// + /// + public string PluginName => Plugin?.PluginName ?? throw new InvalidOperationException("Plugin is not loaded"); + + /// + /// The underlying that is warpped + /// by he current instance + /// + public IPlugin? Plugin { get; private set; } + + private readonly Type PluginType; + + private ConsoleEventHandler? PluginConsoleHandler; + + internal LivePlugin(IPlugin plugin) + { + Plugin = plugin; + PluginType = plugin.GetType(); + GetConsoleHandler(); + } + + private void GetConsoleHandler() + { + //Get the console handler method from the plugin instance + MethodInfo? handler = (from m in PluginType.GetMethods() + where m.GetCustomAttribute() != null + select m) + .FirstOrDefault(); + //Get a delegate handler for the plugin + PluginConsoleHandler = handler?.CreateDelegate(Plugin); + } + + /// + /// Sets the plugin's configuration if it defines a + /// on an instance method + /// + /// The host configuration DOM + /// The plugin local configuration DOM + internal void InitConfig(JsonDocument hostConfig, JsonDocument pluginConf) + { + //Get the console handler method from the plugin instance + MethodInfo? confHan = PluginType.GetMethods().Where(static m => m.GetCustomAttribute() != null) + .FirstOrDefault(); + //Get a delegate handler for the plugin + ConfigInitializer? configInit = confHan?.CreateDelegate(Plugin); + if (configInit == null) + { + return; + } + //Merge configurations before passing to plugin + JsonDocument merged = hostConfig.Merge(pluginConf, "host", PluginType.Name); + try + { + //Invoke + configInit.Invoke(merged); + } + catch + { + merged.Dispose(); + throw; + } + } + + /// + /// Invokes the plugin's log initalizer method if it defines a + /// on an instance method + /// + /// The current process's CLI args + internal void InitLog(string[] cliArgs) + { + //Get the console handler method from the plugin instance + MethodInfo? logInit = (from m in PluginType.GetMethods() + where m.GetCustomAttribute() != null + select m) + .FirstOrDefault(); + //Get a delegate handler for the plugin + LogInitializer? logFunc = logInit?.CreateDelegate(Plugin); + //Invoke + logFunc?.Invoke(cliArgs); + } + + /// + /// Invokes the plugins console event handler if the type has one + /// and the plugin is loaded. + /// + /// The message to pass to the plugin handler + /// + /// True if the command was sent to the plugin, false if the plugin is + /// unloaded or did not export a console event handler + /// + public bool SendConsoleMessage(string message) + { + //Make sure plugin is loaded and has a console handler + if (PluginConsoleHandler == null) + { + return false; + } + //Invoke plugin console handler + PluginConsoleHandler(message); + return true; + } + + /// + /// Calls the method on the plugin if its loaded + /// + internal void LoadPlugin() => Plugin?.Load(); + + /// + /// Unloads all loaded endpoints from + /// that they were loaded to, then unloads the plugin. + /// + /// An optional log provider to write unload exceptions to + /// + /// If is no null unload exceptions are swallowed and written to the log + /// + internal void UnloadPlugin(ILogProvider? logSink) + { + /* + * We need to swallow plugin unload errors to avoid + * unknown state, making sure endpoints are properly + * unloaded! + */ + try + { + //Unload the plugin + Plugin?.Unload(); + } + catch (Exception ex) + { + //Create an unload wrapper for the exception + PluginUnloadException wrapper = new("Exception raised during plugin unload", ex); + if (logSink == null) + { + throw wrapper; + } + //Write error to log sink + logSink.Error(wrapper); + } + Plugin = null; + PluginConsoleHandler = null; + } + /// + public override bool Equals(object? obj) + { + Type? pluginType = Plugin?.GetType(); + Type? otherType = obj?.GetType(); + if(pluginType == null || otherType == null) + { + return false; + } + //If the other plugin is the same type as the current instance return true + return pluginType.FullName == otherType.FullName; + } + /// + public bool Equals(LivePlugin? other) + { + return Equals(other?.Plugin); + } + /// + public bool Equals(IPlugin? other) + { + return Equals((object?)other); + } + /// + public override int GetHashCode() + { + return Plugin?.GetHashCode() ?? throw new InvalidOperationException("Plugin is null"); + } + } +} diff --git a/Plugins.Runtime/src/LoaderExtensions.cs b/Plugins.Runtime/src/LoaderExtensions.cs new file mode 100644 index 0000000..795dcf5 --- /dev/null +++ b/Plugins.Runtime/src/LoaderExtensions.cs @@ -0,0 +1,120 @@ +/* +* Copyright (c) 2022 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.Collections.Generic; +using System.Linq; + +namespace VNLib.Plugins.Runtime +{ + public static class LoaderExtensions + { + /// + /// 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 the plugin that derrives from the specified type + public static LivePlugin? GetExposedPlugin(this RuntimePluginLoader loader) + { + return loader.LivePlugins + .Where(static pl => typeof(T).IsAssignableFrom(pl.Plugin!.GetType())) + .SingleOrDefault(); + } + + /// + /// 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? GetExposedTypeFromPlugin(this RuntimePluginLoader loader) where T: class + { + LivePlugin? plugin = loader.LivePlugins + .Where(static pl => typeof(T).IsAssignableFrom(pl.Plugin!.GetType())) + .SingleOrDefault(); + + return plugin?.Plugin as T; + } + + /// + /// Registers a listener delegate method to invoke when the + /// current is reloaded, and passes + /// the new instance of the specified type + /// + /// The single plugin type to register a listener for + /// + /// The delegate method to invoke when the loader has reloaded plugins + /// + public static bool RegisterListenerForSingle(this RuntimePluginLoader loader, Action reloaded) where T: class + { + _ = reloaded ?? throw new ArgumentNullException(nameof(reloaded)); + + //try to get the casted type from the loader + T? current = loader.GetExposedTypeFromPlugin(); + + if (current == null) + { + return false; + } + else + { + loader.Reloaded += delegate (object? sender, EventArgs args) + { + RuntimePluginLoader wpl = (sender as RuntimePluginLoader)!; + //Get the new loaded type + T newT = (wpl.GetExposedPlugin()!.Plugin as T)!; + //Invoke reloaded action + reloaded(current, newT); + //update the new current instance + current = newT; + }; + + return true; + } + } + + /// + /// Gets all endpoints exposed by all exported plugin instances + /// within the current loader + /// + /// + /// An enumeration of all endpoints + public static IEnumerable GetEndpoints(this RuntimePluginLoader loader) => loader.LivePlugins.SelectMany(static pl => pl.Plugin!.GetEndpoints()); + + /// + /// Determines if any loaded plugin types exposes an instance of the + /// specified type + /// + /// + /// + /// True if any plugin instance exposes a the specified type, false otherwise + public static bool ExposesType(this RuntimePluginLoader loader) where T : class + { + return loader.LivePlugins.Any(static pl => typeof(T).IsAssignableFrom(pl.Plugin?.GetType())); + } + } +} diff --git a/Plugins.Runtime/src/PluginUnloadExcpetion.cs b/Plugins.Runtime/src/PluginUnloadExcpetion.cs new file mode 100644 index 0000000..53f63b2 --- /dev/null +++ b/Plugins.Runtime/src/PluginUnloadExcpetion.cs @@ -0,0 +1,46 @@ +/* +* Copyright (c) 2022 Vaughn Nugent +* +* Library: VNLib +* Package: VNLib.Plugins.Runtime +* File: PluginUnloadExcpetion.cs +* +* PluginUnloadExcpetion.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.Runtime.Serialization; + +namespace VNLib.Plugins.Runtime +{ + /// + /// A wrapper for exceptions that are raised during an + /// assembly plugin unload event. See + /// for details + /// + public class PluginUnloadException : Exception + { + public PluginUnloadException() + {} + public PluginUnloadException(string message) : base(message) + {} + public PluginUnloadException(string message, Exception innerException) : base(message, innerException) + {} + protected PluginUnloadException(SerializationInfo info, StreamingContext context) : base(info, context) + {} + } +} diff --git a/Plugins.Runtime/src/RuntimePluginLoader.cs b/Plugins.Runtime/src/RuntimePluginLoader.cs new file mode 100644 index 0000000..c688f8b --- /dev/null +++ b/Plugins.Runtime/src/RuntimePluginLoader.cs @@ -0,0 +1,250 @@ +/* +* Copyright (c) 2022 Vaughn Nugent +* +* Library: VNLib +* Package: VNLib.Plugins.Runtime +* File: DynamicPluginLoader.cs +* +* DynamicPluginLoader.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.Json; +using System.Reflection; +using System.Runtime.Loader; +using System.Threading.Tasks; +using System.Collections.Generic; + +using McMaster.NETCore.Plugins; + +using VNLib.Utils; +using VNLib.Utils.IO; +using VNLib.Utils.Logging; +using VNLib.Utils.Extensions; + + +namespace VNLib.Plugins.Runtime +{ + /// + /// A runtime .NET assembly loader specialized to load + /// assemblies that export types. + /// + public class RuntimePluginLoader : VnDisposeable + { + protected readonly PluginLoader Loader; + protected readonly string PluginPath; + protected readonly JsonDocument HostConfig; + protected readonly ILogProvider? Log; + protected readonly LinkedList LoadedPlugins; + + /// + /// A readonly collection of all loaded plugin wrappers + /// + public IReadOnlyCollection LivePlugins => LoadedPlugins; + + /// + /// An event that is raised before the loader + /// unloads all plugin instances + /// + protected event EventHandler? OnBeforeReloaded; + /// + /// An event that is raised after a successfull reload of all new + /// plugins for the instance + /// + protected event EventHandler? OnAfterReloaded; + + /// + /// Raised when the current loader has reloaded the assembly and + /// all plugins were successfully loaded. + /// + public event EventHandler? Reloaded; + + /// + /// The current plugin's JSON configuration DOM loaded from the plugin's directory + /// if it exists. Only valid after first initalization + /// + public JsonDocument? PluginConfigDOM { get; private set; } + /// + /// Optional loader arguments object for the plugin + /// + protected JsonElement? LoaderArgs { get; private set; } + + /// + /// The path of the plugin's configuration file. (Default = pluginPath.json) + /// + public string PluginConfigPath { get; init; } + /// + /// Creates a new with the specified + /// assembly location and host config. + /// + /// + /// A nullable log provider + /// The configuration DOM to merge with plugin config DOM and pass to enabled plugins + /// A value that specifies if the assembly can be unloaded + /// A value that spcifies if the loader will listen for changes to the assembly file and reload the plugins + /// A value that specifies if assembly dependencies are loaded on-demand + /// + /// The argument may be null if is false + /// + /// + public RuntimePluginLoader(string pluginPath, JsonDocument? hostConfig = null, ILogProvider? log = null, bool unloadable = false, bool hotReload = false, bool lazy = false) + :this( + new PluginConfig(pluginPath) + { + IsUnloadable = unloadable || hotReload, + EnableHotReload = hotReload, + IsLazyLoaded = lazy, + ReloadDelay = TimeSpan.FromSeconds(1), + PreferSharedTypes = true, + DefaultContext = AssemblyLoadContext.Default + }, + hostConfig, log) + { + } + /// + /// Creates a new with the specified config and host config dom. + /// + /// The plugin's loader configuration + /// The host/process configuration DOM + /// A log provider to write plugin unload log events to + /// + public RuntimePluginLoader(PluginConfig config, JsonDocument? hostConfig, ILogProvider? log) + { + //Add the assembly from which the IPlugin library was loaded from + config.SharedAssemblies.Add(typeof(IPlugin).Assembly.GetName()); + + //Default to empty config if null + HostConfig = hostConfig ?? JsonDocument.Parse("{}"); + Loader = new(config); + PluginPath = config.MainAssemblyPath; + Log = log; + Loader.Reloaded += Loader_Reloaded; + //Set the config path default + PluginConfigPath = Path.ChangeExtension(PluginPath, ".json"); + LoadedPlugins = new(); + } + + private async void Loader_Reloaded(object sender, PluginReloadedEventArgs eventArgs) + { + try + { + //Invoke reloaded events + OnBeforeReloaded?.Invoke(this, eventArgs); + //Unload all endpoints + LoadedPlugins.TryForeach(lp => lp.UnloadPlugin(Log)); + //Clear list of loaded plugins + LoadedPlugins.Clear(); + //Unload the plugin config + PluginConfigDOM?.Dispose(); + //Reload the assembly and + await InitLoaderAsync(); + //fire after loaded + OnAfterReloaded?.Invoke(this, eventArgs); + //Raise the external reloaded event + Reloaded?.Invoke(this, EventArgs.Empty); + } + catch (Exception ex) + { + Log?.Error(ex); + } + } + + /// + /// Initializes the plugin loader, the assembly, and all public + /// types + /// + /// A task that represents the initialization + public async Task InitLoaderAsync() + { + //Load the main assembly + Assembly PluginAsm = Loader.LoadDefaultAssembly(); + //Get the plugin's configuration file + if (FileOperations.FileExists(PluginConfigPath)) + { + //Open and read the config file + await using FileStream confStream = File.OpenRead(PluginConfigPath); + JsonDocumentOptions jdo = new() + { + AllowTrailingCommas = true, + CommentHandling = JsonCommentHandling.Skip, + }; + //parse the plugin config file + PluginConfigDOM = await JsonDocument.ParseAsync(confStream, jdo); + //Store the config loader args + if (PluginConfigDOM.RootElement.TryGetProperty("loader_args", out JsonElement loaderEl)) + { + LoaderArgs = loaderEl; + } + } + else + { + //Set plugin config dom to an empty object if the file does not exist + PluginConfigDOM = JsonDocument.Parse("{}"); + LoaderArgs = null; + } + + string[] cliArgs = Environment.GetCommandLineArgs(); + + //Get all types that implement the IPlugin interface + IEnumerable plugins = PluginAsm.GetTypes().Where(static type => !type.IsAbstract && typeof(IPlugin).IsAssignableFrom(type)) + //Create the plugin instances + .Select(static type => (Activator.CreateInstance(type) as IPlugin)!); + //Load all plugins that implement the Iplugin interface + foreach (IPlugin plugin in plugins) + { + //Load wrapper + LivePlugin lp = new(plugin); + try + { + //Init config + lp.InitConfig(HostConfig, PluginConfigDOM); + //Init log handler + lp.InitLog(cliArgs); + //Load the plugin + lp.LoadPlugin(); + //Create new plugin loader for the plugin + LoadedPlugins.AddLast(lp); + } + catch (TargetInvocationException te) when (te.InnerException is not null) + { + throw te.InnerException; + } + } + } + /// + /// Manually reload the internal + /// which will reload the assembly and its plugins and endpoints + /// + public void ReloadPlugin() => Loader.Reload(); + + /// + /// Attempts to unload all plugins. + /// + /// + public void UnloadAll() => LoadedPlugins.TryForeach(lp => lp.UnloadPlugin(Log)); + + /// + protected override void Free() + { + Loader.Dispose(); + PluginConfigDOM?.Dispose(); + } + + } +} \ No newline at end of file diff --git a/Plugins.Runtime/src/VNLib.Plugins.Runtime.csproj b/Plugins.Runtime/src/VNLib.Plugins.Runtime.csproj new file mode 100644 index 0000000..d435245 --- /dev/null +++ b/Plugins.Runtime/src/VNLib.Plugins.Runtime.csproj @@ -0,0 +1,47 @@ + + + + enable + net6.0 + Vaughn Nugent + Copyright © 2022 Vaughn Nugent + A runtime plugin loader for .NET. Allows runtime loading and tracking of .NET assemblies +that export the VNLib.Plugin.IPlugin interface. + 1.0.1.1 + https://www.vaughnnugent.com/resources + True + \\vaughnnugent.com\Internal\Folder Redirection\vman\Documents\Programming\Software\StrongNameingKey.snk + + + + + true + True + latest-all + + + False + + + False + + + + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + + + + + + + + -- cgit