diff options
author | vnugent <public@vaughnnugent.com> | 2023-10-14 15:41:17 -0400 |
---|---|---|
committer | vnugent <public@vaughnnugent.com> | 2023-10-14 15:41:17 -0400 |
commit | 62f9e126912fa9a620a361fb5b88d33506e096fb (patch) | |
tree | 78665fe8516c559821aa4358ca9e2734e475415a /lib/Plugins.Essentials.ServiceStack | |
parent | 0f0c991891b6be076a9a367627201eceeb6d354e (diff) |
some refactoring and tests
Diffstat (limited to 'lib/Plugins.Essentials.ServiceStack')
9 files changed, 529 insertions, 245 deletions
diff --git a/lib/Plugins.Essentials.ServiceStack/src/Construction/HttpServiceStackBuilder.cs b/lib/Plugins.Essentials.ServiceStack/src/Construction/HttpServiceStackBuilder.cs index 763eb26..c4d602b 100644 --- a/lib/Plugins.Essentials.ServiceStack/src/Construction/HttpServiceStackBuilder.cs +++ b/lib/Plugins.Essentials.ServiceStack/src/Construction/HttpServiceStackBuilder.cs @@ -50,6 +50,7 @@ namespace VNLib.Plugins.Essentials.ServiceStack.Construction private Action<ICollection<IServiceHost>>? _hostBuilder; private Func<ServiceGroup, IHttpServer>? _getServers; private Func<IPluginStack>? _getPlugins; + private IManualPlugin[]? manualPlugins; /// <summary> /// Uses the supplied callback to get a collection of virtual hosts @@ -93,7 +94,7 @@ namespace VNLib.Plugins.Essentials.ServiceStack.Construction /// <returns>The current instance for chaining</returns> public HttpServiceStackBuilder WithBuiltInHttp(Func<ServiceGroup, ITransportProvider> transport, HttpConfig config) { - return WithHttp(sg => new HttpServer(config, transport(sg), sg.Hosts.Select(static p => p.Processor))); + return WithBuiltInHttp(transport, sg => config); } /// <summary> @@ -108,6 +109,18 @@ namespace VNLib.Plugins.Essentials.ServiceStack.Construction } /// <summary> + /// Adds a collection of manual plugin instances to the stack. Every call + /// to this method will replace the previous collection. + /// </summary> + /// <param name="plugins">The array of plugins (or params) to add</param> + /// <returns>The current instance for chaining</returns> + public HttpServiceStackBuilder WithManualPlugins(params IManualPlugin[] plugins) + { + manualPlugins = plugins; + return this; + } + + /// <summary> /// Builds the new <see cref="HttpServiceStack"/> from the configured callbacks /// </summary> /// <returns>The newly constructed <see cref="HttpServiceStack"/> that may be used to manage your http services</returns> @@ -137,6 +150,9 @@ namespace VNLib.Plugins.Essentials.ServiceStack.Construction servers.AddLast(server); } + //Always init manual array + manualPlugins ??= Array.Empty<IManualPlugin>(); + //Only load plugins if the callback is configured IPluginStack? plugins = _getPlugins?.Invoke(); @@ -144,7 +160,9 @@ namespace VNLib.Plugins.Essentials.ServiceStack.Construction plugins ??= new EmptyPluginStack(); #pragma warning restore CA2000 // Dispose objects before losing scope - return new(servers, sd, plugins); + IPluginInitializer init = new PluginStackInitializer(plugins, manualPlugins); + + return new(servers, sd, init); } /* diff --git a/lib/Plugins.Essentials.ServiceStack/src/HttpServiceStack.cs b/lib/Plugins.Essentials.ServiceStack/src/HttpServiceStack.cs index 51c3091..7cd5ac4 100644 --- a/lib/Plugins.Essentials.ServiceStack/src/HttpServiceStack.cs +++ b/lib/Plugins.Essentials.ServiceStack/src/HttpServiceStack.cs @@ -29,7 +29,6 @@ using System.Collections.Generic; using VNLib.Utils; using VNLib.Net.Http; -using VNLib.Plugins.Runtime; namespace VNLib.Plugins.Essentials.ServiceStack { @@ -62,7 +61,7 @@ namespace VNLib.Plugins.Essentials.ServiceStack /// generate servers to listen for services exposed by the /// specified host context /// </summary> - internal HttpServiceStack(LinkedList<IHttpServer> servers, ServiceDomain serviceDomain, IPluginStack plugins) + internal HttpServiceStack(LinkedList<IHttpServer> servers, ServiceDomain serviceDomain, IPluginInitializer plugins) { _servers = servers; _serviceDomain = serviceDomain; diff --git a/lib/Plugins.Essentials.ServiceStack/src/IManagedPlugin.cs b/lib/Plugins.Essentials.ServiceStack/src/IManagedPlugin.cs index 852bb95..21c8e91 100644 --- a/lib/Plugins.Essentials.ServiceStack/src/IManagedPlugin.cs +++ b/lib/Plugins.Essentials.ServiceStack/src/IManagedPlugin.cs @@ -22,30 +22,17 @@ * along with this program. If not, see https://www.gnu.org/licenses/. */ +using System; using System.ComponentModel.Design; -using VNLib.Plugins.Runtime; - namespace VNLib.Plugins.Essentials.ServiceStack { - - /// <summary> /// Represents a plugin managed by a <see cref="IHttpPluginManager"/> that includes dynamically loaded plugins /// </summary> public interface IManagedPlugin { /// <summary> - /// Exposes the internal <see cref="PluginController"/> for the loaded plugin - /// </summary> - PluginController Controller { get; } - - /// <summary> - /// The file path to the loaded plugin - /// </summary> - string PluginPath { get; } - - /// <summary> /// The exposed services the inernal plugin provides /// </summary> /// <remarks> @@ -53,5 +40,30 @@ namespace VNLib.Plugins.Essentials.ServiceStack /// must listen for plugin load/unload events to respect lifecycles properly. /// </remarks> IServiceContainer Services { get; } + + /// <summary> + /// Internal call to get all exported plugin endpoints + /// </summary> + /// <returns></returns> + internal IEndpoint[] GetEndpoints(); + + /// <summary> + /// Internal notification that the plugin is loaded + /// </summary> + internal void OnPluginLoaded(); + + /// <summary> + /// Internal notification that the plugin is unloaded + /// </summary> + internal void OnPluginUnloaded(); + + /// <summary> + /// Sends the specified command to the desired plugin by it's name + /// </summary> + /// <param name="pluginName">The name of the plugin to find</param> + /// <param name="command">The command text to send to the plugin</param> + /// <param name="comp">The string name comparison type</param> + /// <returns>True if the command was sent successfully</returns> + internal bool SendCommandToPlugin(string pluginName, string command, StringComparison comp); } } diff --git a/lib/Plugins.Essentials.ServiceStack/src/IManualPlugin.cs b/lib/Plugins.Essentials.ServiceStack/src/IManualPlugin.cs new file mode 100644 index 0000000..e58067f --- /dev/null +++ b/lib/Plugins.Essentials.ServiceStack/src/IManualPlugin.cs @@ -0,0 +1,77 @@ +/* +* Copyright (c) 2023 Vaughn Nugent +* +* Library: VNLib +* Package: VNLib.Plugins.Essentials.ServiceStack +* File: IManualPlugin.cs +* +* IManualPlugin.cs is part of VNLib.Plugins.Essentials.ServiceStack which is part of the larger +* VNLib collection of libraries and utilities. +* +* VNLib.Plugins.Essentials.ServiceStack is free software: you can redistribute it and/or modify +* it under the terms of the GNU Affero 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.Essentials.ServiceStack 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 Affero General Public License for more details. +* +* You should have received a copy of the GNU Affero General Public License +* along with this program. If not, see https://www.gnu.org/licenses/. +*/ + +using System; +using System.ComponentModel.Design; + +namespace VNLib.Plugins.Essentials.ServiceStack +{ + /// <summary> + /// Represents a plugin that may be added to a service stack in user-code + /// instead of the conventional runtime plugin loading system + /// </summary> + public interface IManualPlugin : IDisposable + { + /// <summary> + /// The name of the plugin + /// </summary> + string Name { get; } + + /// <summary> + /// Collects all exported services for use within the service stack + /// </summary> + /// <param name="container">The container to add services to</param> + void GetAllExportedServices(IServiceContainer container); + + /// <summary> + /// Collects all exported endpoints to put into service + /// </summary> + /// <returns>The collection of endpoints</returns> + IEndpoint[] GetEndpoints(); + + /// <summary> + /// Initializes the plugin, called before accessing any other methods + /// </summary> + void Initialize(); + + /// <summary> + /// Loads the plugin, called after initialization but before getting + /// endpoints or services to allow for the plugin to configure itself + /// and perform initial setup + /// </summary> + void Load(); + + /// <summary> + /// Called when an unload was requested, either manually by the plugin controller + /// or when the service stack is unloading + /// </summary> + void Unload(); + + /// <summary> + /// Passes a console command to the plugin + /// </summary> + /// <param name="command">The raw command text to pass to the plugin from the console</param> + void OnConsoleCommand(string command); + } +} diff --git a/lib/Plugins.Essentials.ServiceStack/src/IPluginInitializer.cs b/lib/Plugins.Essentials.ServiceStack/src/IPluginInitializer.cs new file mode 100644 index 0000000..ac91f45 --- /dev/null +++ b/lib/Plugins.Essentials.ServiceStack/src/IPluginInitializer.cs @@ -0,0 +1,43 @@ +/* +* Copyright (c) 2023 Vaughn Nugent +* +* Library: VNLib +* Package: VNLib.Plugins.Essentials.ServiceStack +* File: IPluginInitializer.cs +* +* IPluginInitializer.cs is part of VNLib.Plugins.Essentials.ServiceStack which +* is part of the larger VNLib collection of libraries and utilities. +* +* VNLib.Plugins.Essentials.ServiceStack is free software: you can redistribute it and/or modify +* it under the terms of the GNU Affero 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.Essentials.ServiceStack 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 Affero General Public License for more details. +* +* You should have received a copy of the GNU Affero General Public License +* along with this program. If not, see https://www.gnu.org/licenses/. +*/ + + +using VNLib.Utils.Logging; +using VNLib.Plugins.Runtime; + +namespace VNLib.Plugins.Essentials.ServiceStack +{ + internal interface IPluginInitializer + { + void PrepareStack(IPluginEventListener listener); + + IManagedPlugin[] InitializePluginStack(ILogProvider eventLogger); + + void UnloadPlugins(); + + void ReloadPlugins(); + + void Dispose(); + } +} diff --git a/lib/Plugins.Essentials.ServiceStack/src/ManagedPlugin.cs b/lib/Plugins.Essentials.ServiceStack/src/ManagedPlugin.cs deleted file mode 100644 index 0c0dfa5..0000000 --- a/lib/Plugins.Essentials.ServiceStack/src/ManagedPlugin.cs +++ /dev/null @@ -1,99 +0,0 @@ -/* -* Copyright (c) 2023 Vaughn Nugent -* -* Library: VNLib -* Package: VNLib.Plugins.Essentials.ServiceStack -* File: ManagedPlugin.cs -* -* ManagedPlugin.cs is part of VNLib.Plugins.Essentials.ServiceStack which is part of the larger -* VNLib collection of libraries and utilities. -* -* VNLib.Plugins.Essentials.ServiceStack is free software: you can redistribute it and/or modify -* it under the terms of the GNU Affero 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.Essentials.ServiceStack 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 Affero General Public License for more details. -* -* You should have received a copy of the GNU Affero General Public License -* along with this program. If not, see https://www.gnu.org/licenses/. -*/ - -using System; -using System.Linq; -using System.Reflection; -using System.ComponentModel.Design; - -using VNLib.Plugins.Runtime; -using VNLib.Plugins.Attributes; - - -namespace VNLib.Plugins.Essentials.ServiceStack -{ - - internal sealed class ManagedPlugin : IManagedPlugin - { - internal RuntimePluginLoader Plugin { get; } - - ///<inheritdoc/> - public string PluginPath => Plugin.Config.AssemblyFile; - - private ServiceContainer? _services; - - public ManagedPlugin(RuntimePluginLoader loader) => Plugin = loader; - - ///<inheritdoc/> - public IServiceContainer Services - { - get - { - _ = _services ?? throw new InvalidOperationException("The service container is not currently loaded"); - return _services!; - } - } - - ///<inheritdoc/> - public PluginController Controller => Plugin.Controller; - - /* - * Automatically called after the plugin has successfully loaded - * by event handlers below - */ - - internal void OnPluginLoaded() - { - //If the service container is defined, dispose - _services?.Dispose(); - - //Init new service container - _services = new(); - - //Get types from plugin - foreach (LivePlugin plugin in Plugin.Controller.Plugins) - { - /* - * Get the exposed configurator method if declared, - * it may not be defined. - */ - ServiceConfigurator? callback = plugin.PluginType.GetMethods() - .Where(static m => m.GetCustomAttribute<ServiceConfiguratorAttribute>() != null && !m.IsAbstract) - .Select(m => m.CreateDelegate<ServiceConfigurator>(plugin.Plugin)) - .FirstOrDefault(); - - //Invoke if defined to expose services - callback?.Invoke(_services); - } - } - - internal void OnPluginUnloaded() - { - //Cleanup services no longer in use. Plugin is still valid until this method returns - _services?.Dispose(); - //Remove ref to services - _services = null; - } - } -} diff --git a/lib/Plugins.Essentials.ServiceStack/src/PluginManager.cs b/lib/Plugins.Essentials.ServiceStack/src/PluginManager.cs index 476f3f8..2f57367 100644 --- a/lib/Plugins.Essentials.ServiceStack/src/PluginManager.cs +++ b/lib/Plugins.Essentials.ServiceStack/src/PluginManager.cs @@ -24,10 +24,7 @@ using System; -using System.IO; using System.Linq; -using System.Diagnostics; -using System.Threading.Tasks; using System.Collections.Generic; using VNLib.Utils; @@ -43,23 +40,22 @@ namespace VNLib.Plugins.Essentials.ServiceStack /// instances, exposes controls, and relays stateful plugin events. /// </summary> internal sealed class PluginManager : VnDisposeable, IHttpPluginManager, IPluginEventListener - { - private readonly Dictionary<PluginController, ManagedPlugin> _managedPlugins; + { private readonly ServiceDomain _dependents; - private readonly IPluginStack _stack; - - private IEnumerable<LivePlugin> _livePlugins => _managedPlugins.SelectMany(static p => p.Key.Plugins); + private readonly IPluginInitializer _stack; /// <summary> /// The collection of internal controllers /// </summary> - public IEnumerable<IManagedPlugin> Plugins => _managedPlugins.Select(static p => p.Value); + public IEnumerable<IManagedPlugin> Plugins => _loadedPlugins; + + private IManagedPlugin[] _loadedPlugins; - public PluginManager(ServiceDomain dependents, IPluginStack stack) + public PluginManager(ServiceDomain dependents, IPluginInitializer stack) { _dependents = dependents; _stack = stack; - _managedPlugins = new(); + _loadedPlugins = Array.Empty<IManagedPlugin>(); } /// <summary> @@ -70,104 +66,29 @@ namespace VNLib.Plugins.Essentials.ServiceStack { _ = _stack ?? throw new InvalidOperationException("Plugin stack has not been set."); - /* - * Since we own the plugin stack, it is safe to build it here. - * This method is not public and should not be called more than - * once. Otherwise it can cause issues with the plugin stack. - */ - _stack.BuildStack(); - - //Register for plugin events - _stack.RegsiterListener(this, this); - - //Create plugin wrappers from loaded plugins - ManagedPlugin[] wrapper = _stack.Plugins.Select(p => new ManagedPlugin(p)).ToArray(); + _stack.PrepareStack(this); - //Add all wrappers to the managed plugins table - Array.ForEach(wrapper, w => _managedPlugins.Add(w.Plugin.Controller, w)); - - //Init remaining controllers single-threaded because it may mutate the table - _managedPlugins.Select(p => p.Value).TryForeach(w => InitializePlugin(w.Plugin, debugLog)); - - //Load stage, load all multithreaded - Parallel.ForEach(_managedPlugins.Values, wp => LoadPlugin(wp.Plugin, debugLog)); + //Initialize the plugin stack and store the loaded plugins + _loadedPlugins = _stack.InitializePluginStack(debugLog); debugLog.Information("Plugin loading completed"); } - /* - * Plugins are manually loaded by this manager instead of the stack shortcut extensions - * because I want to catch individual exceptions. - * - * I do not prefer this method as I would prefer loading is handled by the stack - * and the host not by this library. - * - * This will change in the future. - */ - - private void InitializePlugin(RuntimePluginLoader plugin, ILogProvider debugLog) - { - string fileName = Path.GetFileName(plugin.Config.AssemblyFile); - - try - { - //Initialzie plugin wrapper - plugin.InitializeController(); - - /* - * If the plugin assembly does not expose any plugin types or there is an issue loading the assembly, - * its types my not unify, then we should give the user feedback insead of a silent fail. - */ - if (!plugin.Controller.Plugins.Any()) - { - debugLog.Warn("No plugin instances were exposed via {asm} assembly. This may be due to an assebmly mismatch", fileName); - } - } - catch (Exception ex) - { - debugLog.Error("Exception raised during initialzation of {asm}. It has been removed from the collection\n{ex}", fileName, ex); - - //Remove the plugin from the table - _managedPlugins.Remove(plugin.Controller); - } - } - - private static void LoadPlugin(RuntimePluginLoader plugin, ILogProvider debugLog) - { - string fileName = Path.GetFileName(plugin.Config.AssemblyFile); - - Stopwatch sw = new(); - try - { - sw.Start(); - - //Load wrapper - plugin.LoadPlugins(); - - sw.Stop(); - - debugLog.Verbose("Loaded {pl} in {tm} ms", fileName, sw.ElapsedMilliseconds); - } - catch (Exception ex) - { - debugLog.Error("Exception raised during loading {asf}. Failed to load plugin \n{ex}", fileName, ex); - } - finally - { - sw.Stop(); - } - } /// <inheritdoc/> public bool SendCommandToPlugin(string pluginName, string message, StringComparison nameComparison = StringComparison.Ordinal) { Check(); - //Find the single plugin by its name - LivePlugin? pl = _livePlugins.Where(p => pluginName.Equals(p.PluginName, nameComparison)).SingleOrDefault(); + foreach(IManagedPlugin plugin in _loadedPlugins) + { + if(plugin.SendCommandToPlugin(pluginName, message, nameComparison)) + { + return true; + } + } - //Send the command - return pl?.SendConsoleMessage(message) ?? false; + return false; } /// <inheritdoc/> @@ -176,7 +97,7 @@ namespace VNLib.Plugins.Essentials.ServiceStack Check(); //Reload all plugins, causing an event cascade - _stack.ReloadAll(); + _stack.ReloadPlugins(); } /// <inheritdoc/> @@ -185,7 +106,7 @@ namespace VNLib.Plugins.Essentials.ServiceStack Check(); //Unload all plugin controllers - _stack.UnloadAll(); + _stack.UnloadPlugins(); /* * All plugin instances must be destroyed because the @@ -198,26 +119,15 @@ namespace VNLib.Plugins.Essentials.ServiceStack protected override void Free() { //Clear plugin table - _managedPlugins.Clear(); + _loadedPlugins = Array.Empty<IManagedPlugin>(); //Dispose the plugin stack _stack.Dispose(); } - /* - * When using a service stack an loading manually, plugins that have errors - * will not be captured by this instance. However when using the shortcut - * extensions, the events will be invoked regaldess if we loaded the plugin - * here. - */ - void IPluginEventListener.OnPluginLoaded(PluginController controller, object? state) { - //Make sure the plugin is managed by this manager - if(!_managedPlugins.TryGetValue(controller, out ManagedPlugin? mp)) - { - return; - } + IManagedPlugin mp = (state as IManagedPlugin)!; //Run onload method before invoking other handlers mp.OnPluginLoaded(); @@ -231,11 +141,7 @@ namespace VNLib.Plugins.Essentials.ServiceStack void IPluginEventListener.OnPluginUnloaded(PluginController controller, object? state) { - //Make sure the plugin is managed by this manager - if (!_managedPlugins.TryGetValue(controller, out ManagedPlugin? mp)) - { - return; - } + IManagedPlugin plugin = (state as IManagedPlugin)!; try { @@ -243,12 +149,12 @@ namespace VNLib.Plugins.Essentials.ServiceStack ServiceGroup[] deps = _dependents.ServiceGroups.Select(static d => d).ToArray(); //Run unloaded method - deps.TryForeach(d => d.OnPluginUnloaded(mp)); + deps.TryForeach(d => d.OnPluginUnloaded(plugin)); } finally { //always unload the plugin wrapper - mp.OnPluginUnloaded(); + plugin.OnPluginUnloaded(); } } } diff --git a/lib/Plugins.Essentials.ServiceStack/src/PluginStackInitializer.cs b/lib/Plugins.Essentials.ServiceStack/src/PluginStackInitializer.cs new file mode 100644 index 0000000..6ccd862 --- /dev/null +++ b/lib/Plugins.Essentials.ServiceStack/src/PluginStackInitializer.cs @@ -0,0 +1,329 @@ +/* +* Copyright (c) 2023 Vaughn Nugent +* +* Library: VNLib +* Package: VNLib.Plugins.Essentials.ServiceStack +* File: PluginStackInitializer.cs +* +* PluginStackInitializer.cs is part of VNLib.Plugins.Essentials.ServiceStack which +* is part of the larger VNLib collection of libraries and utilities. +* +* VNLib.Plugins.Essentials.ServiceStack is free software: you can redistribute it and/or modify +* it under the terms of the GNU Affero 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.Essentials.ServiceStack 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 Affero General Public License for more details. +* +* You should have received a copy of the GNU Affero General Public License +* along with this program. If not, see https://www.gnu.org/licenses/. +*/ + + +using System; +using System.IO; +using System.Linq; +using System.Reflection; +using System.Diagnostics; +using System.Threading.Tasks; +using System.Collections.Generic; +using System.ComponentModel.Design; + +using VNLib.Utils.Logging; +using VNLib.Plugins.Runtime; +using VNLib.Utils.Extensions; +using VNLib.Plugins.Attributes; + +namespace VNLib.Plugins.Essentials.ServiceStack +{ + + internal sealed record class PluginStackInitializer(IPluginStack Stack, IManualPlugin[] ManualPlugins) : IPluginInitializer + { + private readonly LinkedList<IManagedPlugin> _managedPlugins = new(); + private readonly LinkedList<ManualPluginWrapper> _manualPlugins = new(); + + ///<inheritdoc/> + public void PrepareStack(IPluginEventListener listener) + { + /* + * Since we own the plugin stack, it is safe to build it here. + * This method is not public and should not be called more than + * once. Otherwise it can cause issues with the plugin stack. + */ + Stack.BuildStack(); + + //Create plugin wrappers from loaded plugins + ManagedPlugin[] wrapper = Stack.Plugins.Select(static p => new ManagedPlugin(p)).ToArray(); + + //Add wrappers to list of managed plugins + Array.ForEach(wrapper, p => _managedPlugins.AddLast(p)); + + //Register for all plugins and pass the plugin instance as the state object + Array.ForEach(wrapper, p => p.Plugin.Controller.Register(listener, p)); + + //Add manual plugins to list of managed plugins + Array.ForEach(ManualPlugins, p => _manualPlugins.AddLast(new ManualPluginWrapper(p))); + } + + ///<inheritdoc/> + public IManagedPlugin[] InitializePluginStack(ILogProvider debugLog) + { + //single thread initialziation + LinkedList<IManagedPlugin> _loadedPlugins = new(); + + //Combine all managed plugins and initialize them individually + IEnumerable<IManagedPlugin> plugins = _managedPlugins.Union(_manualPlugins); + + foreach(IManagedPlugin p in plugins) + { + //Try init plugin and add it to the list of loaded plugins + if (InitializePluginCore(p, debugLog)) + { + _loadedPlugins.AddLast(p); + } + } + + //Load stage, load only initialized plugins + Parallel.ForEach(_loadedPlugins, wp => LoadPlugin(wp, debugLog)); + + return _loadedPlugins.ToArray(); + } + + ///<inheritdoc/> + public void UnloadPlugins() + { + Stack.UnloadAll(); + _manualPlugins.TryForeach(static mp => mp.Unload()); + } + + ///<inheritdoc/> + public void ReloadPlugins() + { + Stack.ReloadAll(); + + //Reload manual plugins + _manualPlugins.TryForeach(static mp => mp.Unload()); + _manualPlugins.TryForeach(static mp => mp.Load()); + } + + ///<inheritdoc/> + public void Dispose() + { + Stack.Dispose(); + _manualPlugins.TryForeach(static mp => mp.Dispose()); + _manualPlugins.Clear(); + } + + private static bool InitializePluginCore(IManagedPlugin plugin, ILogProvider debugLog) + { + try + { + if (plugin is ManagedPlugin mp) + { + //Initialzie plugin wrapper + mp.Plugin.InitializeController(); + + /* + * If the plugin assembly does not expose any plugin types or there is an issue loading the assembly, + * its types my not unify, then we should give the user feedback insead of a silent fail. + */ + if (!mp.Plugin.Controller.Plugins.Any()) + { + debugLog.Warn("No plugin instances were exposed via {asm} assembly. This may be due to an assebmly mismatch", plugin.ToString()); + } + } + else if(plugin is ManualPluginWrapper mpw) + { + //Initialzie plugin wrapper + mpw.Plugin.Initialize(); + } + else + { + Debug.Fail("Missed managed plugin wrapper type"); + } + + return true; + } + catch (Exception ex) + { + debugLog.Error("Exception raised during initialzation of {asm}. It has been removed from the collection\n{ex}", plugin.ToString(), ex); + } + + return false; + } + + private static void LoadPlugin(IManagedPlugin plugin, ILogProvider debugLog) + { + Stopwatch sw = new(); + try + { + sw.Start(); + + //Recover the base class used to load instances + if (plugin is ManagedPlugin mp) + { + mp.Plugin.LoadPlugins(); + } + else if (plugin is ManualPluginWrapper mpw) + { + mpw.Load(); + } + else + { + Debug.Fail("Missed managed plugin wrapper type"); + } + + sw.Stop(); + + debugLog.Verbose("Loaded {pl} in {tm} ms", plugin.ToString(), sw.ElapsedMilliseconds); + } + catch (Exception ex) + { + debugLog.Error("Exception raised during loading {asf}. Failed to load plugin \n{ex}", plugin.ToString(), ex); + } + finally + { + sw.Stop(); + } + } + + + private sealed record class ManagedPlugin(RuntimePluginLoader Plugin) : IManagedPlugin + { + private ServiceContainer? _services; + + ///<inheritdoc/> + public IServiceContainer Services + { + get + { + _ = _services ?? throw new InvalidOperationException("The service container is not currently loaded"); + return _services!; + } + } + + ///<inheritdoc/> + IEndpoint[] IManagedPlugin.GetEndpoints() => Plugin.Controller.GetOnlyWebPlugins().SelectMany(static pl => pl.Plugin!.GetEndpoints()).ToArray(); + + /* + * Automatically called after the plugin has successfully loaded + * by event handlers below + */ + + ///<inheritdoc/> + void IManagedPlugin.OnPluginLoaded() + { + //If the service container is defined, dispose + _services?.Dispose(); + + //Init new service container + _services = new(); + + //Get types from plugin + foreach (LivePlugin plugin in Plugin.Controller.Plugins) + { + /* + * Get the exposed configurator method if declared, + * it may not be defined. + */ + ServiceConfigurator? callback = plugin.PluginType.GetMethods() + .Where(static m => m.GetCustomAttribute<ServiceConfiguratorAttribute>() != null && !m.IsAbstract) + .Select(m => m.CreateDelegate<ServiceConfigurator>(plugin.Plugin)) + .FirstOrDefault(); + + //Invoke if defined to expose services + callback?.Invoke(_services); + } + } + + ///<inheritdoc/> + void IManagedPlugin.OnPluginUnloaded() + { + //Cleanup services no longer in use. Plugin is still valid until this method returns + _services?.Dispose(); + + //Remove ref to services + _services = null; + } + + ///<inheritdoc/> + bool IManagedPlugin.SendCommandToPlugin(string pluginName, string command, StringComparison comp) + { + //Get plugin + LivePlugin? plugin = Plugin.Controller.Plugins.FirstOrDefault(p => p.PluginName.Equals(pluginName, comp)); + + //If plugin is null, return false + if (plugin == null) + { + return false; + } + + return plugin.SendConsoleMessage(command); + } + + public override string ToString() => Path.GetFileName(Plugin.Config.AssemblyFile); + } + + private sealed record class ManualPluginWrapper(IManualPlugin Plugin) : IManagedPlugin, IDisposable + { + private ServiceContainer _container = new(); + + ///<inheritdoc/> + public IServiceContainer Services => _container; + + ///<inheritdoc/> + IEndpoint[] IManagedPlugin.GetEndpoints() => Plugin.GetEndpoints(); + + public void Load() + { + Plugin.Load(); + Plugin.GetAllExportedServices(Services); + } + + public void Unload() + { + Plugin.Unload(); + + //Unload and re-init container + _container.Dispose(); + _container = new(); + } + + public void Dispose() + { + //Dispose container + _container.Dispose(); + + //Dispose plugin + Plugin.Dispose(); + } + + ///<inheritdoc/> + bool IManagedPlugin.SendCommandToPlugin(string pluginName, string command, StringComparison comp) + { + + if (Plugin.Name.Equals(pluginName, comp)) + { + Plugin.OnConsoleCommand(command); + return true; + } + + return false; + } + + + /* + * SHOULD NEVER BE CALLED + */ + void IManagedPlugin.OnPluginLoaded() => throw new NotImplementedException(); + void IManagedPlugin.OnPluginUnloaded() => throw new NotImplementedException(); + + ///<inheritdoc/> + public override string ToString() => Plugin.Name; + } + + } +} diff --git a/lib/Plugins.Essentials.ServiceStack/src/ServiceGroup.cs b/lib/Plugins.Essentials.ServiceStack/src/ServiceGroup.cs index c8be44c..e504013 100644 --- a/lib/Plugins.Essentials.ServiceStack/src/ServiceGroup.cs +++ b/lib/Plugins.Essentials.ServiceStack/src/ServiceGroup.cs @@ -23,7 +23,6 @@ */ using System.Net; -using System.Linq; using System.Collections.Generic; using System.Runtime.CompilerServices; @@ -80,28 +79,28 @@ namespace VNLib.Plugins.Essentials.ServiceStack _endpointsForPlugins.Clear(); } - internal void OnPluginLoaded(IManagedPlugin controller) + internal void OnPluginLoaded(IManagedPlugin plugin) { //Get all new endpoints for plugin - IEndpoint[] newEndpoints = controller.Controller.GetOnlyWebPlugins().SelectMany(static pl => pl.Plugin!.GetEndpoints()).ToArray(); + IEndpoint[] newEndpoints = plugin.GetEndpoints(); //Add endpoints to dict - _endpointsForPlugins.AddOrUpdate(controller, newEndpoints); + _endpointsForPlugins.AddOrUpdate(plugin, newEndpoints); //Add endpoints to hosts - _vHosts.TryForeach(v => v.OnRuntimeServiceAttach(controller, newEndpoints)); + _vHosts.TryForeach(v => v.OnRuntimeServiceAttach(plugin, newEndpoints)); } - internal void OnPluginUnloaded(IManagedPlugin controller) + internal void OnPluginUnloaded(IManagedPlugin plugin) { //Get the old endpoints from the controller referrence and remove them - if (_endpointsForPlugins.TryGetValue(controller, out IEndpoint[]? oldEps)) + if (_endpointsForPlugins.TryGetValue(plugin, out IEndpoint[]? oldEps)) { //Remove the old endpoints - _vHosts.TryForeach(v => v.OnRuntimeServiceDetach(controller, oldEps)); + _vHosts.TryForeach(v => v.OnRuntimeServiceDetach(plugin, oldEps)); //remove controller ref - _ = _endpointsForPlugins.Remove(controller); + _ = _endpointsForPlugins.Remove(plugin); } } } |