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/RuntimePluginLoader.cs | 250 +++++++++++++++++++++++++++++ 1 file changed, 250 insertions(+) create mode 100644 Plugins.Runtime/src/RuntimePluginLoader.cs (limited to 'Plugins.Runtime/src/RuntimePluginLoader.cs') 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 -- cgit