diff options
author | vnugent <public@vaughnnugent.com> | 2023-03-09 01:48:28 -0500 |
---|---|---|
committer | vnugent <public@vaughnnugent.com> | 2023-03-09 01:48:28 -0500 |
commit | 5ddef0fcb742e77b99a0e17015d2eea0a1d4131a (patch) | |
tree | c1c88284b11b70d9f373215d8d54e8a168cc5700 /lib/Plugins.Essentials.ServiceStack | |
parent | dab71d5597fdfbe71f6ac310a240835716e952a5 (diff) |
Omega cache, session, and account provider complete overhaul
Diffstat (limited to 'lib/Plugins.Essentials.ServiceStack')
12 files changed, 736 insertions, 324 deletions
diff --git a/lib/Plugins.Essentials.ServiceStack/src/HttpServiceStack.cs b/lib/Plugins.Essentials.ServiceStack/src/HttpServiceStack.cs index 5800955..45282b3 100644 --- a/lib/Plugins.Essentials.ServiceStack/src/HttpServiceStack.cs +++ b/lib/Plugins.Essentials.ServiceStack/src/HttpServiceStack.cs @@ -1,5 +1,5 @@ /* -* Copyright (c) 2022 Vaughn Nugent +* Copyright (c) 2023 Vaughn Nugent * * Library: VNLib * Package: VNLib.Plugins.Essentials.ServiceStack @@ -22,15 +22,20 @@ * along with this program. If not, see https://www.gnu.org/licenses/. */ +using System; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; +using System.Collections.Generic; + using VNLib.Utils; using VNLib.Net.Http; namespace VNLib.Plugins.Essentials.ServiceStack { /// <summary> - /// The service domain controller that manages all - /// servers for an application based on a - /// <see cref="ServiceDomain"/> + /// An HTTP servicing stack that manages a collection of HTTP servers + /// their service domain /// </summary> public sealed class HttpServiceStack : VnDisposeable { @@ -48,7 +53,7 @@ namespace VNLib.Plugins.Essentials.ServiceStack /// <summary> /// The service domain's plugin controller /// </summary> - public IPluginController PluginController => _serviceDomain; + public IPluginManager PluginManager => _serviceDomain.PluginManager; /// <summary> /// Initializes a new <see cref="HttpServiceStack"/> that will @@ -74,14 +79,14 @@ namespace VNLib.Plugins.Essentials.ServiceStack //Init new linked cts to stop all servers if cancelled _cts = CancellationTokenSource.CreateLinkedTokenSource(parentToken); - LinkedList<Task> runners = new(); + //Start all servers + Task[] runners = _servers.Select(s => s.Start(_cts.Token)).ToArray(); - foreach(HttpServer server in _servers) - { - //Start servers and add run task to list - Task run = server.Start(_cts.Token); - runners.AddLast(run); - } + //Check for failed startups + Task? firstFault = runners.Where(static t => t.IsFaulted).FirstOrDefault(); + + //Raise first exception + firstFault?.GetAwaiter().GetResult(); //Task that waits for all to exit then cleans up WaitForAllTask = Task.WhenAll(runners) @@ -96,6 +101,8 @@ namespace VNLib.Plugins.Essentials.ServiceStack /// <returns>The task that completes when</returns> public Task StopAndWaitAsync() { + Check(); + _cts?.Cancel(); return WaitForAllTask; } @@ -103,7 +110,7 @@ namespace VNLib.Plugins.Essentials.ServiceStack private void OnAllServerExit(Task allExit) { //Unload the hosts - _serviceDomain.UnloadAll(); + _serviceDomain.TearDown(); } ///<inheritdoc/> diff --git a/lib/Plugins.Essentials.ServiceStack/src/HttpServiceStackBuilder.cs b/lib/Plugins.Essentials.ServiceStack/src/HttpServiceStackBuilder.cs index bb6e96f..0b75031 100644 --- a/lib/Plugins.Essentials.ServiceStack/src/HttpServiceStackBuilder.cs +++ b/lib/Plugins.Essentials.ServiceStack/src/HttpServiceStackBuilder.cs @@ -1,5 +1,5 @@ /* -* Copyright (c) 2022 Vaughn Nugent +* Copyright (c) 2023 Vaughn Nugent * * Library: VNLib * Package: VNLib.Plugins.Essentials.ServiceStack @@ -22,6 +22,10 @@ * along with this program. If not, see https://www.gnu.org/licenses/. */ +using System; +using System.Linq; +using System.Collections.Generic; + using VNLib.Net.Http; namespace VNLib.Plugins.Essentials.ServiceStack diff --git a/lib/Plugins.Essentials.ServiceStack/src/IPluginController.cs b/lib/Plugins.Essentials.ServiceStack/src/IPluginController.cs index 0871fdc..fb9c340 100644 --- a/lib/Plugins.Essentials.ServiceStack/src/IPluginController.cs +++ b/lib/Plugins.Essentials.ServiceStack/src/IPluginController.cs @@ -1,12 +1,12 @@ /* -* Copyright (c) 2022 Vaughn Nugent +* Copyright (c) 2023 Vaughn Nugent * * Library: VNLib * Package: VNLib.Plugins.Essentials.ServiceStack -* File: IPluginController.cs +* File: IPluginManager.cs * -* IPluginController.cs is part of VNLib.Plugins.Essentials.ServiceStack which is part of the larger -* VNLib collection of libraries and utilities. +* IPluginManager.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 @@ -22,7 +22,9 @@ * along with this program. If not, see https://www.gnu.org/licenses/. */ -using System.Text.Json; +using System; +using System.Threading.Tasks; +using System.Collections.Generic; using VNLib.Utils.Logging; @@ -32,16 +34,21 @@ namespace VNLib.Plugins.Essentials.ServiceStack /// Represents a live plugin controller that manages all /// plugins loaded in a <see cref="ServiceDomain"/> /// </summary> - public interface IPluginController + public interface IPluginManager { /// <summary> + /// The the plugins managed by this <see cref="IPluginManager"/> + /// </summary> + public IEnumerable<IManagedPlugin> Plugins { get; } + + /// <summary> /// Loads all plugins specified by the host config to the service manager, /// or attempts to load plugins by the default /// </summary> /// <param name="config">The configuration instance to pass to plugins</param> /// <param name="appLog">A log provider to write message and errors to</param> /// <returns>A task that resolves when all plugins are loaded</returns> - Task LoadPlugins(JsonDocument config, ILogProvider appLog); + Task LoadPluginsAsync(PluginLoadConfiguration config, ILogProvider appLog); /// <summary> /// Sends a message to a plugin identified by it's name. @@ -61,11 +68,8 @@ namespace VNLib.Plugins.Essentials.ServiceStack void ForceReloadAllPlugins(); /// <summary> - /// Unloads all service groups, removes them, and unloads all - /// loaded plugins + /// Unloads all loaded plugins and calls thier event handlers /// </summary> - /// <exception cref="AggregateException"></exception> - /// <exception cref="ObjectDisposedException"></exception> - void UnloadAll(); + void UnloadPlugins(); } } diff --git a/lib/Plugins.Essentials.ServiceStack/src/IPluginWrapper.cs b/lib/Plugins.Essentials.ServiceStack/src/IPluginWrapper.cs new file mode 100644 index 0000000..2e686ee --- /dev/null +++ b/lib/Plugins.Essentials.ServiceStack/src/IPluginWrapper.cs @@ -0,0 +1,54 @@ +/* +* Copyright (c) 2023 Vaughn Nugent +* +* Library: VNLib +* Package: VNLib.Plugins.Essentials.ServiceStack +* File: IPluginWrapper.cs +* +* IPluginWrapper.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.Plugins.Runtime; + +namespace VNLib.Plugins.Essentials.ServiceStack +{ + + /// <summary> + /// Represents a plugin managed by a <see cref="IPluginManager"/> 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> + /// WARNING: Services exposed by the plugin will abide by the plugin lifecycle, so consumers + /// must listen for plugin load/unload events to respect lifecycles properly. + /// </remarks> + IUnloadableServiceProvider Services { get; } + } +} diff --git a/lib/Plugins.Essentials.ServiceStack/src/IServiceHost.cs b/lib/Plugins.Essentials.ServiceStack/src/IServiceHost.cs index 0c8d6c1..bb4f65f 100644 --- a/lib/Plugins.Essentials.ServiceStack/src/IServiceHost.cs +++ b/lib/Plugins.Essentials.ServiceStack/src/IServiceHost.cs @@ -1,5 +1,5 @@ /* -* Copyright (c) 2022 Vaughn Nugent +* Copyright (c) 2023 Vaughn Nugent * * Library: VNLib * Package: VNLib.Plugins.Essentials.ServiceStack @@ -22,21 +22,43 @@ * along with this program. If not, see https://www.gnu.org/licenses/. */ +using VNLib.Net.Http; + namespace VNLib.Plugins.Essentials.ServiceStack { /// <summary> - /// Represents a host that exposes a processor for host events + /// Represents an HTTP service host which provides information required + /// for HttpServer routing and the <see cref="IWebRoot"/> for proccessing + /// incomming connections /// </summary> public interface IServiceHost { /// <summary> - /// The <see cref="EventProcessor"/> to process - /// incoming HTTP connections + /// The <see cref="IWebRoot"/> that handles HTTP connection + /// processing. /// </summary> - EventProcessor Processor { get; } + IWebRoot Processor { get; } + /// <summary> /// The host's transport infomration /// </summary> IHostTransportInfo TransportInfo { get; } + + /// <summary> + /// Called when a plugin is loaded and is endpoints are extracted + /// to be placed into service. + /// </summary> + /// <param name="plugin">The loaded plugin ready to be attached</param> + /// <param name="endpoints">The dynamic endpoints of a loading plugin</param> + void OnRuntimeServiceAttach(IManagedPlugin plugin, IEndpoint[] endpoints); + + /// <summary> + /// Called when a <see cref="ServiceDomain"/>'s <see cref="IPluginManager"/> + /// unloads a given plugin, and its originally discovered endpoints + /// </summary> + /// <param name="plugin">The unloading plugin to detach</param> + /// <param name="endpoints">The endpoints of the unloading plugin to remove from service</param> + void OnRuntimeServiceDetach(IManagedPlugin plugin, IEndpoint[] endpoints); + } } diff --git a/lib/Plugins.Essentials.ServiceStack/src/IUnloadableServiceProvider.cs b/lib/Plugins.Essentials.ServiceStack/src/IUnloadableServiceProvider.cs new file mode 100644 index 0000000..fa334bd --- /dev/null +++ b/lib/Plugins.Essentials.ServiceStack/src/IUnloadableServiceProvider.cs @@ -0,0 +1,42 @@ +/* +* Copyright (c) 2023 Vaughn Nugent +* +* Library: VNLib +* Package: VNLib.Plugins.Essentials.ServiceStack +* File: IUnloadableServiceProvider.cs +* +* IUnloadableServiceProvider.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.Threading; + +namespace VNLib.Plugins.Essentials.ServiceStack +{ + /// <summary> + /// A <see cref="IServiceProvider"/> that may be unloaded when the + /// assembly that is sharing the types are being disposed. + /// </summary> + public interface IUnloadableServiceProvider : IServiceProvider + { + /// <summary> + /// A token that is set cancelled state when the service provider + /// is unloaded. + /// </summary> + CancellationToken UnloadToken { get; } + } +} diff --git a/lib/Plugins.Essentials.ServiceStack/src/ManagedPlugin.cs b/lib/Plugins.Essentials.ServiceStack/src/ManagedPlugin.cs new file mode 100644 index 0000000..596ea83 --- /dev/null +++ b/lib/Plugins.Essentials.ServiceStack/src/ManagedPlugin.cs @@ -0,0 +1,202 @@ +/* +* 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.IO; +using System.Linq; +using System.Threading; +using System.Reflection; +using System.Threading.Tasks; +using System.ComponentModel.Design; + +using VNLib.Utils; +using VNLib.Plugins.Runtime; +using VNLib.Plugins.Attributes; + +namespace VNLib.Plugins.Essentials.ServiceStack +{ + + internal sealed class ManagedPlugin : VnDisposeable, IPluginEventListener, IManagedPlugin + { + private readonly IPluginEventListener _serviceDomainListener; + private readonly RuntimePluginLoader _plugin; + + private UnloadableServiceContainer? _services; + + public ManagedPlugin(string pluginPath, PluginLoadConfiguration config, IPluginEventListener listener) + { + PluginPath = pluginPath; + + //configure the loader + _plugin = new(pluginPath, config.HostConfig, config.PluginErrorLog, config.HotReload, config.HotReload); + + //Register listener before loading occurs + _plugin.Controller.Register(this, this); + + //Store listener to raise events + _serviceDomainListener = listener; + } + + ///<inheritdoc/> + public string PluginPath { get; } + + ///<inheritdoc/> + public IUnloadableServiceProvider Services + { + get + { + Check(); + return _services!; + } + } + + ///<inheritdoc/> + public PluginController Controller + { + get + { + Check(); + return _plugin.Controller; + } + } + + internal string PluginFileName => Path.GetFileName(PluginPath); + + internal Task InitializePluginsAsync() + { + Check(); + return _plugin.InitializeController(); + } + + internal void LoadPlugins() + { + Check(); + _plugin.LoadPlugins(); + } + + /* + * Automatically called after the plugin has successfully loaded + * by event handlers below + */ + private void ConfigureServices() + { + //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 ReloadPlugins() + { + Check(); + _plugin.ReloadPlugins(); + } + + internal void UnloadPlugins() + { + Check(); + + //unload plugins + _plugin.UnloadAll(); + + //Services will be cleaned up by the unload event + } + + void IPluginEventListener.OnPluginLoaded(PluginController controller, object? state) + { + //Initialize services after load, before passing event + ConfigureServices(); + + //Propagate event + _serviceDomainListener.OnPluginLoaded(controller, state); + } + + void IPluginEventListener.OnPluginUnloaded(PluginController controller, object? state) + { + //Cleanup services no longer in use. Plugin is still valid until this method returns + using (_services) + { + //Propagate event + _serviceDomainListener.OnPluginUnloaded(controller, state); + + //signal service cancel before disposing + _services?.SignalUnload(); + } + //Remove ref to services + _services = null; + } + + protected override void Free() + { + //Dispose services + _services?.Dispose(); + //Unregister the listener to cleanup resources + _plugin.Controller.Unregister(this); + //Dispose loader + _plugin.Dispose(); + } + + + private sealed class UnloadableServiceContainer : ServiceContainer, IUnloadableServiceProvider + { + private readonly CancellationTokenSource _cts; + + public UnloadableServiceContainer() : base() + { + _cts = new(); + } + + ///<inheritdoc/> + CancellationToken IUnloadableServiceProvider.UnloadToken => _cts.Token; + + /// <summary> + /// Signals to listensers that the service container will be unloading + /// </summary> + internal void SignalUnload() => _cts.Cancel(); + + protected override void Dispose(bool disposing) + { + base.Dispose(disposing); + _cts.Dispose(); + } + } + } +} diff --git a/lib/Plugins.Essentials.ServiceStack/src/PluginLoadConfiguration.cs b/lib/Plugins.Essentials.ServiceStack/src/PluginLoadConfiguration.cs new file mode 100644 index 0000000..4974e71 --- /dev/null +++ b/lib/Plugins.Essentials.ServiceStack/src/PluginLoadConfiguration.cs @@ -0,0 +1,62 @@ +/* +* Copyright (c) 2023 Vaughn Nugent +* +* Library: VNLib +* Package: VNLib.Plugins.Essentials.ServiceStack +* File: PluginLoadConfiguration.cs +* +* PluginLoadConfiguration.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.Text.Json; + +using VNLib.Utils.Logging; +using VNLib.Plugins.Runtime; + + +namespace VNLib.Plugins.Essentials.ServiceStack +{ + /// <summary> + /// Plugin loading configuration variables + /// </summary> + public readonly record struct PluginLoadConfiguration + { + /// <summary> + /// The directory containing the dynamic plugin assemblies to load + /// </summary> + public readonly string PluginDir { get; init; } + + /// <summary> + /// A value that indicates if the internal <see cref="PluginController"/> + /// allows for hot-reload/unloadable plugin assemblies. + /// </summary> + public readonly bool HotReload { get; init; } + + /// <summary> + /// The optional host configuration file to merge with plugin config + /// to pass to the loading plugin. + /// </summary> + public readonly JsonDocument? HostConfig { get; init; } + + /// <summary> + /// Passed to the underlying <see cref="RuntimePluginLoader"/> + /// holding plugins + /// </summary> + public readonly ILogProvider? PluginErrorLog { get; init; } + } +} diff --git a/lib/Plugins.Essentials.ServiceStack/src/PluginManager.cs b/lib/Plugins.Essentials.ServiceStack/src/PluginManager.cs new file mode 100644 index 0000000..cdcf7ba --- /dev/null +++ b/lib/Plugins.Essentials.ServiceStack/src/PluginManager.cs @@ -0,0 +1,240 @@ +/* +* Copyright (c) 2023 Vaughn Nugent +* +* Library: VNLib +* Package: VNLib.Plugins.Essentials.ServiceStack +* File: PluginManager.cs +* +* PluginManager.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.Diagnostics; +using System.Threading.Tasks; +using System.Collections.Generic; + +using VNLib.Utils; +using VNLib.Utils.IO; +using VNLib.Utils.Logging; +using VNLib.Utils.Extensions; +using VNLib.Plugins.Runtime; + +namespace VNLib.Plugins.Essentials.ServiceStack +{ + + /// <summary> + /// A sealed type that manages the plugin interaction layer. Manages the lifetime of plugin + /// instances, exposes controls, and relays stateful plugin events. + /// </summary> + internal sealed class PluginManager : VnDisposeable, IPluginManager, IPluginEventListener + { + private const string PLUGIN_FILE_EXTENSION = ".dll"; + + private readonly List<ManagedPlugin> _plugins; + private readonly IReadOnlyCollection<ServiceGroup> _dependents; + + + private IEnumerable<LivePlugin> _livePlugins => _plugins.SelectMany(static p => p.Controller.Plugins); + + /// <summary> + /// The collection of internal controllers + /// </summary> + public IEnumerable<IManagedPlugin> Plugins => _plugins; + + public PluginManager(IReadOnlyCollection<ServiceGroup> dependents) + { + _plugins = new(); + _dependents = dependents; + } + + /// <inheritdoc/> + /// <exception cref="ObjectDisposedException"></exception> + public Task LoadPluginsAsync(PluginLoadConfiguration config, ILogProvider appLog) + { + Check(); + + //Load all virtual file assemblies withing the plugin folder + DirectoryInfo dir = new(config.PluginDir); + + if (!dir.Exists) + { + appLog.Warn("Plugin directory {dir} does not exist. No plugins were loaded", config.PluginDir); + return Task.CompletedTask; + } + + appLog.Information("Loading plugins. Hot-reload: {en}", config.HotReload); + + //Enumerate all dll files within this dir + IEnumerable<DirectoryInfo> dirs = dir.EnumerateDirectories("*", SearchOption.TopDirectoryOnly); + + //Select only dirs with a dll that is named after the directory name + IEnumerable<string> pluginPaths = GetPluginPaths(dirs); + + IEnumerable<string> pluginFileNames = pluginPaths.Select(static s => $"{Path.GetFileName(s)}\n"); + + appLog.Debug("Found plugin files: \n{files}", string.Concat(pluginFileNames)); + + //Initialze plugin managers + ManagedPlugin[] wrappers = pluginPaths.Select(pw => new ManagedPlugin(pw, config, this)).ToArray(); + + //Add to loaded plugins + _plugins.AddRange(wrappers); + + //Load plugins + return InitiailzeAndLoadAsync(appLog); + } + + private static IEnumerable<string> GetPluginPaths(IEnumerable<DirectoryInfo> 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); + }); + } + + private async Task InitiailzeAndLoadAsync(ILogProvider debugLog) + { + //Load all async + Task[] initAll = _plugins.Select(p => InitializePlugin(p, debugLog)).ToArray(); + + //Wait for initalization + await Task.WhenAll(initAll).ConfigureAwait(false); + + //Load stage, load all multithreaded + Parallel.ForEach(_plugins, p => LoadPlugin(p, debugLog)); + + debugLog.Information("Plugin loading completed"); + } + + private async Task InitializePlugin(ManagedPlugin plugin, ILogProvider debugLog) + { + try + { + //Load wrapper + await plugin.InitializePluginsAsync().ConfigureAwait(true); + } + catch (Exception ex) + { + debugLog.Error(ex, $"Exception raised during initialzation of {plugin.PluginFileName}. It has been removed from the collection\n{ex}"); + + //Remove the plugin from the list while locking it + lock (_plugins) + { + _plugins.Remove(plugin); + } + + //Dispose the plugin + plugin.Dispose(); + } + } + + private static void LoadPlugin(ManagedPlugin plugin, ILogProvider debugLog) + { + Stopwatch sw = new(); + try + { + sw.Start(); + + //Load wrapper + plugin.LoadPlugins(); + + sw.Stop(); + + debugLog.Verbose("Loaded {pl} in {tm} ms", plugin.PluginFileName, sw.ElapsedMilliseconds); + } + catch (Exception ex) + { + debugLog.Error(ex, $"Exception raised during loading {plugin.PluginFileName}. Failed to load plugin \n{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(); + + //Send the command + return pl?.SendConsoleMessage(message) ?? false; + } + + /// <inheritdoc/> + public void ForceReloadAllPlugins() + { + //Reload all plugin managers + _plugins.TryForeach(static p => p.ReloadPlugins()); + } + + /// <inheritdoc/> + public void UnloadPlugins() + { + //Unload all plugin controllers + _plugins.TryForeach(static p => p.UnloadPlugins()); + + /* + * All plugin instances must be destroyed because the + * only way they will be loaded is from their files + * again, so they must be released + */ + _plugins.TryForeach(static p => p.Dispose()); + _plugins.Clear(); + } + + protected override void Free() + { + //Cleanup on dispose if unload failed + _plugins.TryForeach(static p => p.Dispose()); + _plugins.Clear(); + } + + void IPluginEventListener.OnPluginLoaded(PluginController controller, object? state) + { + //Get event listeners at event time because deps may be modified by the domain + ServiceGroup[] deps = _dependents.Select(static d => d).ToArray(); + + //run onload method + deps.TryForeach(d => d.OnPluginLoaded((IManagedPlugin)state!)); + } + + void IPluginEventListener.OnPluginUnloaded(PluginController controller, object? state) + { + //Get event listeners at event time because deps may be modified by the domain + ServiceGroup[] deps = _dependents.Select(static d => d).ToArray(); + + //Run unloaded method + deps.TryForeach(d => d.OnPluginUnloaded((IManagedPlugin)state!)); + } + } +} diff --git a/lib/Plugins.Essentials.ServiceStack/src/ServiceDomain.cs b/lib/Plugins.Essentials.ServiceStack/src/ServiceDomain.cs index 7b06e70..f0f9559 100644 --- a/lib/Plugins.Essentials.ServiceStack/src/ServiceDomain.cs +++ b/lib/Plugins.Essentials.ServiceStack/src/ServiceDomain.cs @@ -1,5 +1,5 @@ /* -* Copyright (c) 2022 Vaughn Nugent +* Copyright (c) 2023 Vaughn Nugent * * Library: VNLib * Package: VNLib.Plugins.Essentials.ServiceStack @@ -24,39 +24,23 @@ using System; using System.Net; -using System.Text.Json; -using System.Diagnostics; +using System.Linq; +using System.Collections.Generic; using VNLib.Utils; -using VNLib.Utils.IO; using VNLib.Utils.Extensions; -using VNLib.Utils.Logging; -using VNLib.Plugins.Runtime; -using VNLib.Plugins.Essentials.Content; -using VNLib.Plugins.Essentials.Sessions; namespace VNLib.Plugins.Essentials.ServiceStack { + /// <summary> /// Represents a domain of services and thier dynamically loaded plugins /// that will be hosted by an application service stack /// </summary> - public sealed class ServiceDomain : VnDisposeable, IPluginController + public sealed class ServiceDomain : VnDisposeable { - private const string PLUGIN_FILE_EXTENSION = ".dll"; - private const string DEFUALT_PLUGIN_DIR = "/plugins"; - private const string PLUGINS_CONFIG_ELEMENT = "plugins"; - private readonly LinkedList<ServiceGroup> _serviceGroups; - private readonly LinkedList<RuntimePluginLoader> _pluginLoaders; - - /// <summary> - /// Enumerates all loaded plugin instances - /// </summary> - public IEnumerable<IPlugin> Plugins => _pluginLoaders.SelectMany(static s => - s.LivePlugins.Where(static p => p.Plugin != null) - .Select(static s => s.Plugin!) - ); + private readonly PluginManager _plugins; /// <summary> /// Gets all service groups loaded in the service manager @@ -64,12 +48,19 @@ namespace VNLib.Plugins.Essentials.ServiceStack public IReadOnlyCollection<ServiceGroup> ServiceGroups => _serviceGroups; /// <summary> + /// Gets the internal <see cref="IPluginManager"/> that manages plugins for the entire + /// <see cref="ServiceDomain"/> + /// </summary> + public IPluginManager PluginManager => _plugins; + + /// <summary> /// Initializes a new empty <see cref="ServiceDomain"/> /// </summary> public ServiceDomain() { _serviceGroups = new(); - _pluginLoaders = new(); + //Init plugin manager and pass ref to service group collection + _plugins = new PluginManager(_serviceGroups); } /// <summary> @@ -78,8 +69,11 @@ namespace VNLib.Plugins.Essentials.ServiceStack /// </summary> /// <param name="hostBuilder">The callback method to build virtual hosts</param> /// <returns>A value that indicates if any virtual hosts were successfully loaded</returns> + /// <exception cref="ObjectDisposedException"></exception> public bool BuildDomain(Action<ICollection<IServiceHost>> hostBuilder) { + Check(); + //LL to store created hosts LinkedList<IServiceHost> hosts = new(); @@ -94,8 +88,11 @@ namespace VNLib.Plugins.Essentials.ServiceStack /// </summary> /// <param name="hosts">The enumeration of virtual hosts</param> /// <returns>A value that indicates if any virtual hosts were successfully loaded</returns> + /// <exception cref="ObjectDisposedException"></exception> public bool FromExisting(IEnumerable<IServiceHost> hosts) { + Check(); + //Get service groups and pass service group list CreateServiceGroups(_serviceGroups, hosts); return _serviceGroups.Any(); @@ -111,11 +108,12 @@ namespace VNLib.Plugins.Essentials.ServiceStack { IEnumerable<IServiceHost> groupHosts = hosts.Where(host => host.TransportInfo.TransportEndpoint.Equals(iface)); - IServiceHost[]? overlap = groupHosts.Where(vh => groupHosts.Select(static s => s.Processor.Hostname).Count(hostname => vh.Processor.Hostname == hostname) > 1).ToArray(); + //Find any duplicate hostnames for the same service gorup + IServiceHost[] overlap = groupHosts.Where(vh => groupHosts.Select(static s => s.Processor.Hostname).Count(hostname => vh.Processor.Hostname == hostname) > 1).ToArray(); - foreach (IServiceHost vh in overlap) + if(overlap.Length > 0) { - throw new ArgumentException($"The hostname '{vh.Processor.Hostname}' is already in use by another virtual host"); + throw new ArgumentException($"The hostname '{overlap.Last().Processor.Hostname}' is already in use by another virtual host"); } //init new service group around an interface and its roots @@ -125,235 +123,33 @@ namespace VNLib.Plugins.Essentials.ServiceStack } } - ///<inheritdoc/> - public Task LoadPlugins(JsonDocument config, ILogProvider appLog) - { - if (!config.RootElement.TryGetProperty(PLUGINS_CONFIG_ELEMENT, out JsonElement pluginEl)) - { - appLog.Information("Plugins element not defined in config, skipping plugin loading"); - return Task.CompletedTask; - } - - //Get the plugin directory, or set to default - string pluginDir = pluginEl.GetPropString("path") ?? Path.Combine(Directory.GetCurrentDirectory(), DEFUALT_PLUGIN_DIR); - //Get the hot reload flag - bool hotReload = pluginEl.TryGetProperty("hot_reload", out JsonElement hrel) && hrel.GetBoolean(); - - //Load all virtual file assemblies withing the plugin folder - DirectoryInfo dir = new(pluginDir); - - if (!dir.Exists) - { - appLog.Warn("Plugin directory {dir} does not exist. No plugins were loaded", pluginDir); - return Task.CompletedTask; - } - - appLog.Information("Loading plugins. Hot-reload: {en}", hotReload); - - //Enumerate all dll files within this dir - IEnumerable<DirectoryInfo> dirs = dir.EnumerateDirectories("*", SearchOption.TopDirectoryOnly); - - //Select only dirs with a dll that is named after the directory name - IEnumerable<string> pluginPaths = 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); - }); - - IEnumerable<string> pluginFileNames = pluginPaths.Select(static s => $"{Path.GetFileName(s)}\n"); - - appLog.Debug("Found plugin files: \n{files}", string.Concat(pluginFileNames)); - - LinkedList<Task> loading = new(); - - object listLock = new(); - - foreach (string pluginPath in pluginPaths) - { - async Task Load() - { - string pluginName = Path.GetFileName(pluginPath); - - RuntimePluginLoader plugin = new(pluginPath, config, appLog, hotReload, hotReload); - Stopwatch sw = new(); - try - { - sw.Start(); - - await plugin.InitLoaderAsync(); - - //Listen for reload events to remove and re-add endpoints - plugin.Reloaded += OnPluginReloaded; - - lock (listLock) - { - //Add to list - _pluginLoaders.AddLast(plugin); - } - - sw.Stop(); - - appLog.Verbose("Loaded {pl} in {tm} ms", pluginName, sw.ElapsedMilliseconds); - } - catch (Exception ex) - { - appLog.Error(ex, $"Exception raised during loading {pluginName}. Failed to load plugin \n{ex}"); - plugin.Dispose(); - } - finally - { - sw.Stop(); - } - } - - loading.AddLast(Load()); - } - - //Continuation to add all initial plugins to the service manager - void Continuation(Task t) - { - appLog.Verbose("Plugins loaded"); - - //Add inital endpoints for all plugins - _pluginLoaders.TryForeach(ldr => _serviceGroups.TryForeach(sg => sg.AddOrUpdateEndpointsForPlugin(ldr))); - - //Init session provider - InitSessionProvider(); - - //Init page router - InitPageRouter(); - } - - //wait for loading to completed - return Task.WhenAll(loading.ToArray()).ContinueWith(Continuation, CancellationToken.None, TaskContinuationOptions.ExecuteSynchronously, TaskScheduler.Default); - } - - ///<inheritdoc/> - public bool SendCommandToPlugin(string pluginName, string message, StringComparison nameComparison = StringComparison.Ordinal) - { - Check(); - //Find the single plugin by its name - LivePlugin? pl = _pluginLoaders.Select(p => - p.LivePlugins.Where(lp => pluginName.Equals(lp.PluginName, nameComparison)) - ) - .SelectMany(static lp => lp) - .SingleOrDefault(); - //Send the command - return pl?.SendConsoleMessage(message) ?? false; - } - - ///<inheritdoc/> - public void ForceReloadAllPlugins() - { - Check(); - _pluginLoaders.TryForeach(static pl => pl.ReloadPlugin()); - } - - ///<inheritdoc/> - public void UnloadAll() + /// <summary> + /// Tears down the service domain by unloading all plugins (calling their event handlers) + /// and destroying all <see cref="ServiceGroup"/>s. This instance may be rebuilt if this + /// method returns successfully. + /// </summary> + internal void TearDown() { Check(); - //Unload service groups before unloading plugins + /* + * Unloading plugins should trigger the OnPluginUnloading + * hook which should cause all dependencies to unload linked + * types. + */ + _plugins.UnloadPlugins(); + + //Manually cleanup if unload missed data _serviceGroups.TryForeach(static sg => sg.UnloadAll()); //empty service groups _serviceGroups.Clear(); - - //Unload all plugins - _pluginLoaders.TryForeach(static pl => pl.UnloadAll()); - } - - private void OnPluginReloaded(object? plugin, EventArgs empty) - { - //Update endpoints for the loader - RuntimePluginLoader reloaded = (plugin as RuntimePluginLoader)!; - - //Update all endpoints for the plugin - _serviceGroups.TryForeach(sg => sg.AddOrUpdateEndpointsForPlugin(reloaded)); - } - - private void InitSessionProvider() - { - //Callback to reload provider - void onSessionProviderReloaded(ISessionProvider old, ISessionProvider current) - { - _serviceGroups.TryForeach(sg => sg.UpdateSessionProvider(current)); - } - - try - { - //get the loader that contains the single session provider - RuntimePluginLoader? sessionLoader = _pluginLoaders - .Where(static s => s.ExposesType<ISessionProvider>()) - .SingleOrDefault(); - - //If session provider has been supplied, load it - if (sessionLoader != null) - { - //Get the session provider from the plugin loader - ISessionProvider sp = sessionLoader.GetExposedTypeFromPlugin<ISessionProvider>()!; - - //Init inital provider - onSessionProviderReloaded(null!, sp); - - //Register reload event - sessionLoader.RegisterListenerForSingle<ISessionProvider>(onSessionProviderReloaded); - } - } - catch (InvalidOperationException) - { - throw new TypeLoadException("More than one session provider plugin was defined in the plugin directory, cannot continue"); - } - } - - private void InitPageRouter() - { - //Callback to reload provider - void onRouterReloaded(IPageRouter old, IPageRouter current) - { - _serviceGroups.TryForeach(sg => sg.UpdatePageRouter(current)); - } - - try - { - - //get the loader that contains the single page router - RuntimePluginLoader? routerLoader = _pluginLoaders - .Where(static s => s.ExposesType<IPageRouter>()) - .SingleOrDefault(); - - //If router has been supplied, load it - if (routerLoader != null) - { - //Get initial value - IPageRouter sp = routerLoader.GetExposedTypeFromPlugin<IPageRouter>()!; - - //Init inital provider - onRouterReloaded(null!, sp); - - //Register reload event - routerLoader.RegisterListenerForSingle<IPageRouter>(onRouterReloaded); - } - } - catch (InvalidOperationException) - { - throw new TypeLoadException("More than one page router plugin was defined in the plugin directory, cannot continue"); - } } + ///<inheritdoc/> protected override void Free() { - //Dispose loaders - _pluginLoaders.TryForeach(static pl => pl.Dispose()); - _pluginLoaders.Clear(); + _plugins.Dispose(); _serviceGroups.Clear(); } } diff --git a/lib/Plugins.Essentials.ServiceStack/src/ServiceGroup.cs b/lib/Plugins.Essentials.ServiceStack/src/ServiceGroup.cs index f57a6f9..2801776 100644 --- a/lib/Plugins.Essentials.ServiceStack/src/ServiceGroup.cs +++ b/lib/Plugins.Essentials.ServiceStack/src/ServiceGroup.cs @@ -1,5 +1,5 @@ /* -* Copyright (c) 2022 Vaughn Nugent +* Copyright (c) 2023 Vaughn Nugent * * Library: VNLib * Package: VNLib.Plugins.Essentials.ServiceStack @@ -23,13 +23,11 @@ */ using System.Net; +using System.Linq; using System.Collections.Generic; using System.Runtime.CompilerServices; using VNLib.Utils.Extensions; -using VNLib.Plugins.Runtime; -using VNLib.Plugins.Essentials.Content; -using VNLib.Plugins.Essentials.Sessions; namespace VNLib.Plugins.Essentials.ServiceStack { @@ -42,7 +40,7 @@ namespace VNLib.Plugins.Essentials.ServiceStack public sealed class ServiceGroup { private readonly LinkedList<IServiceHost> _vHosts; - private readonly ConditionalWeakTable<RuntimePluginLoader, IEndpoint[]> _endpointsForPlugins; + private readonly ConditionalWeakTable<IManagedPlugin, IEndpoint[]> _endpointsForPlugins; /// <summary> /// The <see cref="IPEndPoint"/> transport endpoint for all loaded service hosts @@ -68,61 +66,43 @@ namespace VNLib.Plugins.Essentials.ServiceStack } /// <summary> - /// Sets the specified page rotuer for all virtual hosts + /// Manually detatches runtime services and their loaded endpoints from all + /// endpoints. /// </summary> - /// <param name="router">The page router to user</param> - internal void UpdatePageRouter(IPageRouter router) => _vHosts.TryForeach(v => v.Processor.SetPageRouter(router)); - /// <summary> - /// Sets the specified session provider for all virtual hosts - /// </summary> - /// <param name="current">The session provider to use</param> - internal void UpdateSessionProvider(ISessionProvider current) => _vHosts.TryForeach(v => v.Processor.SetSessionProvider(current)); + internal void UnloadAll() + { + //Remove all loaded endpoints + _vHosts.TryForeach(v => _endpointsForPlugins.TryForeach(eps => v.OnRuntimeServiceDetach(eps.Key, eps.Value))); - /// <summary> - /// Adds or updates all endpoints exported by all plugins - /// within the specified loader. All endpoints exposed - /// by a previously loaded instance are removed and all - /// currently exposed endpoints are added to all virtual - /// hosts - /// </summary> - /// <param name="loader">The plugin loader to get add/update endpoints from</param> - internal void AddOrUpdateEndpointsForPlugin(RuntimePluginLoader loader) + //Clear all hosts + _vHosts.Clear(); + //Clear all endpoints + _endpointsForPlugins.Clear(); + } + + internal void OnPluginLoaded(IManagedPlugin controller) { //Get all new endpoints for plugin - IEndpoint[] newEndpoints = loader.LivePlugins.SelectMany(static pl => pl.Plugin!.GetEndpoints()).ToArray(); - - //See if - if(_endpointsForPlugins.TryGetValue(loader, out IEndpoint[]? oldEps)) - { - //Remove old endpoints - _vHosts.TryForeach(v => v.Processor.RemoveEndpoint(oldEps)); - } + IEndpoint[] newEndpoints = controller.Controller.Plugins.SelectMany(static pl => pl.Plugin!.GetEndpoints()).ToArray(); //Add endpoints to dict - _endpointsForPlugins.AddOrUpdate(loader, newEndpoints); + _endpointsForPlugins.AddOrUpdate(controller, newEndpoints); //Add endpoints to hosts - _vHosts.TryForeach(v => v.Processor.AddEndpoint(newEndpoints)); + _vHosts.TryForeach(v => v.OnRuntimeServiceAttach(controller, newEndpoints)); } - /// <summary> - /// Unloads all previously stored endpoints, router, session provider, and - /// clears all internal data structures - /// </summary> - internal void UnloadAll() + internal void OnPluginUnloaded(IManagedPlugin controller) { - //Remove all loaded endpoints - _vHosts.TryForeach(v => _endpointsForPlugins.TryForeach(eps => v.Processor.RemoveEndpoint(eps.Value))); - - //Remove all routers - _vHosts.TryForeach(static v => v.Processor.SetPageRouter(null)); - //Remove all session providers - _vHosts.TryForeach(static v => v.Processor.SetSessionProvider(null)); + //Get the old endpoints from the controller referrence and remove them + if (_endpointsForPlugins.TryGetValue(controller, out IEndpoint[]? oldEps)) + { + //Remove the old endpoints + _vHosts.TryForeach(v => v.OnRuntimeServiceDetach(controller, oldEps)); - //Clear all hosts - _vHosts.Clear(); - //Clear all endpoints - _endpointsForPlugins.Clear(); + //remove controller ref + _ = _endpointsForPlugins.Remove(controller); + } } } } diff --git a/lib/Plugins.Essentials.ServiceStack/src/VNLib.Plugins.Essentials.ServiceStack.csproj b/lib/Plugins.Essentials.ServiceStack/src/VNLib.Plugins.Essentials.ServiceStack.csproj index 4918c49..dd7b562 100644 --- a/lib/Plugins.Essentials.ServiceStack/src/VNLib.Plugins.Essentials.ServiceStack.csproj +++ b/lib/Plugins.Essentials.ServiceStack/src/VNLib.Plugins.Essentials.ServiceStack.csproj @@ -2,7 +2,6 @@ <PropertyGroup> <TargetFramework>net6.0</TargetFramework> - <ImplicitUsings>enable</ImplicitUsings> <RootNamespace>VNLib.Plugins.Essentials.ServiceStack</RootNamespace> <AssemblyName>VNLib.Plugins.Essentials.ServiceStack</AssemblyName> <Nullable>enable</Nullable> |