aboutsummaryrefslogtreecommitdiff
path: root/lib/Plugins.Runtime/src
diff options
context:
space:
mode:
Diffstat (limited to 'lib/Plugins.Runtime/src')
-rw-r--r--lib/Plugins.Runtime/src/IPluginEventListener.cs49
-rw-r--r--lib/Plugins.Runtime/src/IPluginEventRegistrar.cs47
-rw-r--r--lib/Plugins.Runtime/src/ITypedPluginConsumer.cs47
-rw-r--r--lib/Plugins.Runtime/src/LivePlugin.cs66
-rw-r--r--lib/Plugins.Runtime/src/LoaderExtensions.cs174
-rw-r--r--lib/Plugins.Runtime/src/PluginController.cs127
-rw-r--r--lib/Plugins.Runtime/src/PluginEventRegistration.cs69
-rw-r--r--lib/Plugins.Runtime/src/RuntimePluginLoader.cs201
-rw-r--r--lib/Plugins.Runtime/src/VNLib.Plugins.Runtime.csproj3
9 files changed, 571 insertions, 212 deletions
diff --git a/lib/Plugins.Runtime/src/IPluginEventListener.cs b/lib/Plugins.Runtime/src/IPluginEventListener.cs
new file mode 100644
index 0000000..5d97343
--- /dev/null
+++ b/lib/Plugins.Runtime/src/IPluginEventListener.cs
@@ -0,0 +1,49 @@
+/*
+* Copyright (c) 2023 Vaughn Nugent
+*
+* Library: VNLib
+* Package: VNLib.Plugins.Runtime
+* File: IPluginEventListener.cs
+*
+* IPluginEventListener.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/.
+*/
+
+namespace VNLib.Plugins.Runtime
+{
+ /// <summary>
+ /// Represents a plugin event consumer.
+ /// </summary>
+ public interface IPluginEventListener
+ {
+ /// <summary>
+ /// Called by the registered <see cref="PluginController"/>
+ /// to notify this listener that the plugins within the collection
+ /// have been initialized and loaded
+ /// </summary>
+ /// <param name="controller">The collection on which the load event occured</param>
+ /// <param name="state">The registration state parameter</param>
+ void OnPluginLoaded(PluginController controller, object? state);
+ /// <summary>
+ /// Called by the registered <see cref="PluginController"/>
+ /// to notify this listener that this plugins within the
+ /// collection have been unloaded
+ /// </summary>
+ /// <param name="controller">The controller that is reloading</param>
+ /// <param name="state">The registration state parameter</param>
+ void OnPluginUnloaded(PluginController controller, object? state);
+ }
+}
diff --git a/lib/Plugins.Runtime/src/IPluginEventRegistrar.cs b/lib/Plugins.Runtime/src/IPluginEventRegistrar.cs
new file mode 100644
index 0000000..120f437
--- /dev/null
+++ b/lib/Plugins.Runtime/src/IPluginEventRegistrar.cs
@@ -0,0 +1,47 @@
+/*
+* Copyright (c) 2023 Vaughn Nugent
+*
+* Library: VNLib
+* Package: VNLib.Plugins.Runtime
+* File: IPluginEventRegistrar.cs
+*
+* IPluginEventRegistrar.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/.
+*/
+
+namespace VNLib.Plugins.Runtime
+{
+ /// <summary>
+ /// Represents a type that accepts <see cref="IPluginEventListener"/>
+ /// event handlers and allow them to unload events
+ /// </summary>
+ public interface IPluginEventRegistrar
+ {
+ /// <summary>
+ /// Registers a plugin event listener
+ /// </summary>
+ /// <param name="listener">The event handler instance to register</param>
+ /// <param name="state">An optional state paremeter to pass to the event handler</param>
+ void Register(IPluginEventListener listener, object? state = null);
+
+ /// <summary>
+ /// Unregisters the event listener
+ /// </summary>
+ /// <param name="listener">The event handler instance to unregister</param>
+ /// <returns>A value that indicates if the event handler was successfully unregistered</returns>
+ bool Unregister(IPluginEventListener listener);
+ }
+}
diff --git a/lib/Plugins.Runtime/src/ITypedPluginConsumer.cs b/lib/Plugins.Runtime/src/ITypedPluginConsumer.cs
new file mode 100644
index 0000000..9c8a477
--- /dev/null
+++ b/lib/Plugins.Runtime/src/ITypedPluginConsumer.cs
@@ -0,0 +1,47 @@
+/*
+* Copyright (c) 2023 Vaughn Nugent
+*
+* Library: VNLib
+* Package: VNLib.Plugins.Runtime
+* File: ITypedPluginConsumer.cs
+*
+* ITypedPluginConsumer.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/.
+*/
+
+namespace VNLib.Plugins.Runtime
+{
+ /// <summary>
+ /// An abstraction that represents a consumer of a dynamically loaded type.
+ /// </summary>
+ /// <typeparam name="T">The service type to consume</typeparam>
+ public interface ITypedPluginConsumer<T>
+ {
+ /// <summary>
+ /// Invoked when the instance of the desired type is loaded.
+ /// This is a new instance of the desired type
+ /// </summary>
+ /// <param name="plugin">A new instance of the requested type</param>
+ void OnLoad(T plugin, object? state);
+
+ /// <summary>
+ /// Called when the loader that maintains the instance is unloading
+ /// the type.
+ /// </summary>
+ /// <param name="plugin">The instance of the type that is being unloaded</param>
+ void OnUnload(T plugin, object? state);
+ }
+}
diff --git a/lib/Plugins.Runtime/src/LivePlugin.cs b/lib/Plugins.Runtime/src/LivePlugin.cs
index c0011dd..ae4c90b 100644
--- a/lib/Plugins.Runtime/src/LivePlugin.cs
+++ b/lib/Plugins.Runtime/src/LivePlugin.cs
@@ -1,5 +1,5 @@
/*
-* Copyright (c) 2022 Vaughn Nugent
+* Copyright (c) 2023 Vaughn Nugent
*
* Library: VNLib
* Package: VNLib.Plugins.Runtime
@@ -27,12 +27,12 @@ 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
{
+
/// <summary>
/// <para>
/// Wrapper for a loaded <see cref="IPlugin"/> instance, used internally
@@ -46,6 +46,8 @@ namespace VNLib.Plugins.Runtime
/// </summary>
public class LivePlugin : IEquatable<IPlugin>, IEquatable<LivePlugin>
{
+ private bool _loaded;
+
/// <summary>
/// The plugin's <see cref="IPlugin.PluginName"/> property during load time
/// </summary>
@@ -57,14 +59,24 @@ namespace VNLib.Plugins.Runtime
/// by he current instance
/// </summary>
public IPlugin? Plugin { get; private set; }
+
+ /// <summary>
+ /// The assembly that this plugin was created from
+ /// </summary>
+ public Assembly OriginAsm { get; }
- private readonly Type PluginType;
+ /// <summary>
+ /// The exposed runtime type of the plugin. Equivalent to
+ /// calling <code>Plugin.GetType()</code>
+ /// </summary>
+ public Type PluginType { get; }
private ConsoleEventHandlerSignature? PluginConsoleHandler;
- internal LivePlugin(IPlugin plugin)
+ internal LivePlugin(IPlugin plugin, Assembly originAsm)
{
Plugin = plugin;
+ OriginAsm = originAsm;
PluginType = plugin.GetType();
GetConsoleHandler();
}
@@ -153,42 +165,38 @@ namespace VNLib.Plugins.Runtime
/// <summary>
/// Calls the <see cref="IPlugin.Load"/> method on the plugin if its loaded
/// </summary>
- internal void LoadPlugin() => Plugin?.Load();
+ internal void LoadPlugin()
+ {
+ //Load and set loaded flag
+ Plugin?.Load();
+ _loaded = true;
+ }
/// <summary>
- /// Unloads all loaded endpoints from
- /// that they were loaded to, then unloads the plugin.
+ /// Unloads the plugin, only if the plugin was successfully loaded by
+ /// calling the <see cref="IPlugin.Unload"/> event hook.
/// </summary>
- /// <param name="logSink">An optional log provider to write unload exceptions to</param>
- /// <remarks>
- /// If <paramref name="logSink"/> is no null unload exceptions are swallowed and written to the log
- /// </remarks>
- internal void UnloadPlugin(ILogProvider? logSink)
+ internal void UnloadPlugin()
{
- /*
- * We need to swallow plugin unload errors to avoid
- * unknown state, making sure endpoints are properly
- * unloaded!
- */
+ //Remove delegate handler to the plugin to remove refs
+ PluginConsoleHandler = null;
+
+ //Only call unload if the plugin successfully loaded
+ if (!_loaded)
+ {
+ return;
+ }
+
try
{
- //Unload the plugin
Plugin?.Unload();
}
- catch (Exception ex)
+ finally
{
- //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;
}
- Plugin = null;
- PluginConsoleHandler = null;
}
+
///<inheritdoc/>
public override bool Equals(object? obj)
{
diff --git a/lib/Plugins.Runtime/src/LoaderExtensions.cs b/lib/Plugins.Runtime/src/LoaderExtensions.cs
index 795dcf5..13fbb11 100644
--- a/lib/Plugins.Runtime/src/LoaderExtensions.cs
+++ b/lib/Plugins.Runtime/src/LoaderExtensions.cs
@@ -1,5 +1,5 @@
/*
-* Copyright (c) 2022 Vaughn Nugent
+* Copyright (c) 2023 Vaughn Nugent
*
* Library: VNLib
* Package: VNLib.Plugins.Runtime
@@ -23,98 +23,148 @@
*/
using System;
-using System.Collections.Generic;
+using System.IO;
using System.Linq;
+using System.Text.Json;
+using System.Threading.Tasks;
+
namespace VNLib.Plugins.Runtime
{
+ /// <summary>
+ /// Contains extension methods for PluginLoader library
+ /// </summary>
public static class LoaderExtensions
{
- /// <summary>
- /// Searches all plugins within the current loader for a
- /// single plugin that derrives the specified type
- /// </summary>
- /// <typeparam name="T">The type the plugin must derrive from</typeparam>
- /// <param name="loader"></param>
- /// <returns>The instance of the plugin that derrives from the specified type</returns>
- public static LivePlugin? GetExposedPlugin<T>(this RuntimePluginLoader loader)
+ /*
+ * Class that manages a collection registration for a specific type
+ * dependency, and redirects the event calls for the consumed service
+ */
+ private sealed class TypedRegistration<T> : IPluginEventListener where T: class
{
- return loader.LivePlugins
- .Where(static pl => typeof(T).IsAssignableFrom(pl.Plugin!.GetType()))
- .SingleOrDefault();
+ private readonly ITypedPluginConsumer<T> _consumerEvents;
+ private readonly object? _userState;
+
+ private T? _service;
+ private readonly Type _type;
+
+ public TypedRegistration(ITypedPluginConsumer<T> consumerEvents, Type type)
+ {
+ _consumerEvents = consumerEvents;
+ _type = type;
+ }
+
+
+ public void OnPluginLoaded(PluginController controller, object? state)
+ {
+ //Get the service from the loaded plugins
+ T service = controller.Plugins
+ .Where(pl => _type.IsAssignableFrom(pl.PluginType))
+ .Select(static pl => (T)pl.Plugin!)
+ .First();
+
+ //Call load with the exported type
+ _consumerEvents.OnLoad(service, _userState);
+
+ //Store for unload
+ _service = service;
+ }
+
+ public void OnPluginUnloaded(PluginController controller, object? state)
+ {
+ //Unload
+ _consumerEvents.OnUnload(_service!, _userState);
+ _service = null;
+ }
}
/// <summary>
- /// Searches all plugins within the current loader for a
- /// single plugin that derrives the specified type
+ /// Registers a plugin even handler for the current <see cref="PluginController"/>
+ /// for a specific type.
/// </summary>
- /// <typeparam name="T">The type the plugin must derrive from</typeparam>
- /// <param name="loader"></param>
- /// <returns>The instance of your custom type casted, or null if not found or could not be casted</returns>
- public static T? GetExposedTypeFromPlugin<T>(this RuntimePluginLoader loader) where T: class
+ /// <typeparam name="T"></typeparam>
+ /// <param name="collection"></param>
+ /// <param name="consumer">The typed plugin instance event consumer</param>
+ /// <returns>A <see cref="PluginEventRegistration"/> handle that manages this event registration</returns>
+ /// <exception cref="ArgumentException"></exception>
+ public static PluginEventRegistration RegisterForType<T>(this PluginController collection, ITypedPluginConsumer<T> consumer) where T: class
{
- LivePlugin? plugin = loader.LivePlugins
- .Where(static pl => typeof(T).IsAssignableFrom(pl.Plugin!.GetType()))
- .SingleOrDefault();
+ Type serviceType = typeof(T);
- return plugin?.Plugin as T;
+ //Confim the type is exposed by this collection
+ if(!ExposesType(collection, serviceType))
+ {
+ throw new ArgumentException("The requested type is not exposed in this assembly");
+ }
+
+ //Create new typed listener
+ TypedRegistration<T> reg = new(consumer, serviceType);
+
+ //register event handler
+ return Register(collection, reg, null);
}
/// <summary>
- /// Registers a listener delegate method to invoke when the
- /// current <see cref="RuntimePluginLoader"/> is reloaded, and passes
- /// the new instance of the specified type
+ /// Registers a handler to listen for plugin load/unload events
/// </summary>
- /// <typeparam name="T">The single plugin type to register a listener for</typeparam>
- /// <param name="loader"></param>
- /// <param name="reloaded">The delegate method to invoke when the loader has reloaded plugins</param>
/// <exception cref="ArgumentNullException"></exception>
- public static bool RegisterListenerForSingle<T>(this RuntimePluginLoader loader, Action<T, T> reloaded) where T: class
+ /// <returns>A <see cref="PluginEventRegistration"/> handle that will unregister the listener when disposed</returns>
+ public static PluginEventRegistration Register(this IPluginEventRegistrar reg, IPluginEventListener listener, object? state = null)
{
- _ = reloaded ?? throw new ArgumentNullException(nameof(reloaded));
+ reg.Register(listener, state);
+ return new(reg, listener);
+ }
- //try to get the casted type from the loader
- T? current = loader.GetExposedTypeFromPlugin<T>();
+ /// <summary>
+ /// Loads the configuration file into its <see cref="JsonDocument"/> format
+ /// for reading.
+ /// </summary>
+ /// <param name="loader"></param>
+ /// <returns>A new <see cref="JsonDocument"/> of the loaded configuration file</returns>
+ public static async Task<JsonDocument> GetPluginConfigAsync(this RuntimePluginLoader loader)
+ {
+ //Open and read the config file
+ await using FileStream confStream = File.OpenRead(loader.PluginConfigPath);
- if (current == null)
- {
- return false;
- }
- else
+ JsonDocumentOptions jdo = new()
{
- loader.Reloaded += delegate (object? sender, EventArgs args)
- {
- RuntimePluginLoader wpl = (sender as RuntimePluginLoader)!;
- //Get the new loaded type
- T newT = (wpl.GetExposedPlugin<T>()!.Plugin as T)!;
- //Invoke reloaded action
- reloaded(current, newT);
- //update the new current instance
- current = newT;
- };
-
- return true;
- }
+ AllowTrailingCommas = true,
+ CommentHandling = JsonCommentHandling.Skip,
+ };
+
+ //parse the plugin config file
+ return await JsonDocument.ParseAsync(confStream, jdo);
}
/// <summary>
- /// Gets all endpoints exposed by all exported plugin instances
- /// within the current loader
+ /// Determines if the current <see cref="PluginController"/>
+ /// exposes the desired type on is <see cref="IPlugin"/>
+ /// type.
/// </summary>
- /// <param name="loader"></param>
- /// <returns>An enumeration of all endpoints</returns>
- public static IEnumerable<IEndpoint> GetEndpoints(this RuntimePluginLoader loader) => loader.LivePlugins.SelectMany(static pl => pl.Plugin!.GetEndpoints());
+ /// <param name="collection"></param>
+ /// <param name="type">The desired type to request</param>
+ /// <returns>True if the plugin exposes the desired type, false otherwise</returns>
+ public static bool ExposesType(this PluginController collection, Type type)
+ {
+ return collection.Plugins
+ .Where(pl => type.IsAssignableFrom(pl.PluginType))
+ .Any();
+ }
/// <summary>
- /// Determines if any loaded plugin types exposes an instance of the
- /// specified type
+ /// Searches all plugins within the current loader for a
+ /// single plugin that derrives the specified type
/// </summary>
- /// <typeparam name="T"></typeparam>
+ /// <typeparam name="T">The type the plugin must derrive from</typeparam>
/// <param name="loader"></param>
- /// <returns>True if any plugin instance exposes a the specified type, false otherwise</returns>
- public static bool ExposesType<T>(this RuntimePluginLoader loader) where T : class
+ /// <returns>The instance of your custom type casted, or null if not found or could not be casted</returns>
+ public static T? GetExposedTypes<T>(this PluginController collection) where T: class
{
- return loader.LivePlugins.Any(static pl => typeof(T).IsAssignableFrom(pl.Plugin?.GetType()));
+ LivePlugin? plugin = collection.Plugins
+ .Where(static pl => typeof(T).IsAssignableFrom(pl.PluginType))
+ .SingleOrDefault();
+
+ return plugin?.Plugin as T;
}
}
}
diff --git a/lib/Plugins.Runtime/src/PluginController.cs b/lib/Plugins.Runtime/src/PluginController.cs
new file mode 100644
index 0000000..14ea7f0
--- /dev/null
+++ b/lib/Plugins.Runtime/src/PluginController.cs
@@ -0,0 +1,127 @@
+/*
+* Copyright (c) 2023 Vaughn Nugent
+*
+* Library: VNLib
+* Package: VNLib.Plugins.Runtime
+* File: PluginController.cs
+*
+* PluginController.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.Text.Json;
+using System.Reflection;
+using System.Collections.Generic;
+
+using VNLib.Utils.Extensions;
+
+namespace VNLib.Plugins.Runtime
+{
+ /// <summary>
+ /// Manages the lifetime of a collection of <see cref="IPlugin"/> instances,
+ /// and their dependent event listeners
+ /// </summary>
+ public sealed class PluginController : IPluginEventRegistrar
+ {
+ private readonly List<LivePlugin> _plugins;
+ private readonly List<KeyValuePair<IPluginEventListener, object?>> _listeners;
+
+ internal PluginController()
+ {
+ _plugins = new ();
+ _listeners = new ();
+ }
+
+ /// <summary>
+ /// The current collection of plugins. Valid before the unload event.
+ /// </summary>
+ public IEnumerable<LivePlugin> Plugins => _plugins;
+
+ ///<inheritdoc/>
+ ///<exception cref="ArgumentNullException"></exception>
+ public void Register(IPluginEventListener listener, object? state = null)
+ {
+ _ = listener ?? throw new ArgumentNullException(nameof(listener));
+
+ _listeners.Add(new(listener, state));
+ }
+
+ ///<inheritdoc/>
+ public bool Unregister(IPluginEventListener listener)
+ {
+ //Remove listener
+ return _listeners.RemoveAll(p => p.Key == listener) > 0;
+ }
+
+
+ internal void InitializePlugins(Assembly asm)
+ {
+ //get all Iplugin types
+ Type[] types = asm.GetTypes().Where(static type => !type.IsAbstract && typeof(IPlugin).IsAssignableFrom(type)).ToArray();
+
+ //Initialize the new plugin instances
+ IPlugin[] plugins = types.Select(static t => (IPlugin)Activator.CreateInstance(t)!).ToArray();
+
+ //Crate new containers
+ LivePlugin[] lps = plugins.Select(p => new LivePlugin(p, asm)).ToArray();
+
+ //Store containers
+ _plugins.AddRange(lps);
+ }
+
+ internal void ConfigurePlugins(JsonDocument hostDom, JsonDocument pluginDom, string[] cliArgs)
+ {
+ _plugins.TryForeach(lp => lp.InitConfig(hostDom, pluginDom));
+ _plugins.TryForeach(lp => lp.InitLog(cliArgs));
+ }
+
+ internal void LoadPlugins()
+ {
+ //Load all plugins
+ _plugins.TryForeach(static p => p.LoadPlugin());
+
+ //Notify event handlers
+ _listeners.TryForeach(l => l.Key.OnPluginLoaded(this, l.Value));
+ }
+
+ internal void UnloadPlugins()
+ {
+ try
+ {
+ //Notify event handlers
+ _listeners.TryForeach(l => l.Key.OnPluginUnloaded(this, l.Value));
+
+ //Unload plugin instances
+ _plugins.TryForeach(static p => p.UnloadPlugin());
+ }
+ finally
+ {
+ //Always
+ _plugins.Clear();
+ }
+ }
+
+ internal void Dispose()
+ {
+ _plugins.Clear();
+ _listeners.Clear();
+ }
+
+
+ }
+}
diff --git a/lib/Plugins.Runtime/src/PluginEventRegistration.cs b/lib/Plugins.Runtime/src/PluginEventRegistration.cs
new file mode 100644
index 0000000..37d6162
--- /dev/null
+++ b/lib/Plugins.Runtime/src/PluginEventRegistration.cs
@@ -0,0 +1,69 @@
+/*
+* Copyright (c) 2023 Vaughn Nugent
+*
+* Library: VNLib
+* Package: VNLib.Plugins.Runtime
+* File: PluginEventRegistration.cs
+*
+* PluginEventRegistration.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;
+
+namespace VNLib.Plugins.Runtime
+{
+ /// <summary>
+ /// Holds a registration for events to a given <see cref="IPluginEventRegistrar"/>.
+ /// The event listener is unregistered from events when this registration is disposed.
+ /// </summary>
+ public readonly record struct PluginEventRegistration : IDisposable
+ {
+ private readonly IPluginEventRegistrar _registrar;
+ private readonly IPluginEventListener _listener;
+
+ internal PluginEventRegistration(IPluginEventRegistrar container, IPluginEventListener listener)
+ {
+ _listener = listener;
+ _registrar = container;
+ }
+
+ /// <summary>
+ /// Unreigsers the listner and releases held resources
+ /// </summary>
+ public readonly void Dispose()
+ {
+ _ = _registrar?.Unregister(_listener);
+ }
+
+ /// <summary>
+ /// Unregisters a previously registered <see cref="IPluginEventListener"/>
+ /// from the <see cref="PluginController"/> it was registered to
+ /// </summary>
+ /// <exception cref="InvalidOperationException"></exception>
+ public readonly void Unregister()
+ {
+ if (_registrar == null)
+ {
+ return;
+ }
+ if (!_registrar.Unregister(_listener))
+ {
+ throw new InvalidOperationException("The listner has already been unregistered");
+ }
+ }
+ }
+}
diff --git a/lib/Plugins.Runtime/src/RuntimePluginLoader.cs b/lib/Plugins.Runtime/src/RuntimePluginLoader.cs
index c688f8b..d79def3 100644
--- a/lib/Plugins.Runtime/src/RuntimePluginLoader.cs
+++ b/lib/Plugins.Runtime/src/RuntimePluginLoader.cs
@@ -1,11 +1,11 @@
/*
-* Copyright (c) 2022 Vaughn Nugent
+* Copyright (c) 2023 Vaughn Nugent
*
* Library: VNLib
* Package: VNLib.Plugins.Runtime
-* File: DynamicPluginLoader.cs
+* File: RuntimePluginLoader.cs
*
-* DynamicPluginLoader.cs is part of VNLib.Plugins.Runtime which is part of the larger
+* RuntimePluginLoader.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
@@ -24,20 +24,16 @@
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
{
@@ -45,50 +41,23 @@ namespace VNLib.Plugins.Runtime
/// A runtime .NET assembly loader specialized to load
/// assemblies that export <see cref="IPlugin"/> types.
/// </summary>
- public class RuntimePluginLoader : VnDisposeable
+ public sealed class RuntimePluginLoader : VnDisposeable
{
- protected readonly PluginLoader Loader;
- protected readonly string PluginPath;
- protected readonly JsonDocument HostConfig;
- protected readonly ILogProvider? Log;
- protected readonly LinkedList<LivePlugin> LoadedPlugins;
-
- /// <summary>
- /// A readonly collection of all loaded plugin wrappers
- /// </summary>
- public IReadOnlyCollection<LivePlugin> LivePlugins => LoadedPlugins;
-
- /// <summary>
- /// An event that is raised before the loader
- /// unloads all plugin instances
- /// </summary>
- protected event EventHandler<PluginReloadedEventArgs>? OnBeforeReloaded;
- /// <summary>
- /// An event that is raised after a successfull reload of all new
- /// plugins for the instance
- /// </summary>
- protected event EventHandler? OnAfterReloaded;
-
- /// <summary>
- /// Raised when the current loader has reloaded the assembly and
- /// all plugins were successfully loaded.
- /// </summary>
- public event EventHandler? Reloaded;
+ private readonly PluginLoader Loader;
+ private readonly string PluginPath;
+ private readonly JsonDocument HostConfig;
+ private readonly ILogProvider? Log;
/// <summary>
- /// The current plugin's JSON configuration DOM loaded from the plugin's directory
- /// if it exists. Only valid after first initalization
+ /// Gets the plugin lifetime manager.
/// </summary>
- public JsonDocument? PluginConfigDOM { get; private set; }
- /// <summary>
- /// Optional loader arguments object for the plugin
- /// </summary>
- protected JsonElement? LoaderArgs { get; private set; }
+ public PluginController Controller { get; }
/// <summary>
/// The path of the plugin's configuration file. (Default = pluginPath.json)
/// </summary>
- public string PluginConfigPath { get; init; }
+ public string PluginConfigPath { get; }
+
/// <summary>
/// Creates a new <see cref="RuntimePluginLoader"/> with the specified
/// assembly location and host config.
@@ -98,18 +67,16 @@ namespace VNLib.Plugins.Runtime
/// <param name="hostConfig">The configuration DOM to merge with plugin config DOM and pass to enabled plugins</param>
/// <param name="unloadable">A value that specifies if the assembly can be unloaded</param>
/// <param name="hotReload">A value that spcifies if the loader will listen for changes to the assembly file and reload the plugins</param>
- /// <param name="lazy">A value that specifies if assembly dependencies are loaded on-demand</param>
/// <remarks>
/// The <paramref name="log"/> argument may be null if <paramref name="unloadable"/> is false
/// </remarks>
/// <exception cref="ArgumentNullException"></exception>
- public RuntimePluginLoader(string pluginPath, JsonDocument? hostConfig = null, ILogProvider? log = null, bool unloadable = false, bool hotReload = false, bool lazy = false)
+ public RuntimePluginLoader(string pluginPath, JsonDocument? hostConfig = null, ILogProvider? log = null, bool unloadable = false, bool hotReload = false)
:this(
new PluginConfig(pluginPath)
{
IsUnloadable = unloadable || hotReload,
EnableHotReload = hotReload,
- IsLazyLoaded = lazy,
ReloadDelay = TimeSpan.FromSeconds(1),
PreferSharedTypes = true,
DefaultContext = AssemblyLoadContext.Default
@@ -117,6 +84,7 @@ namespace VNLib.Plugins.Runtime
hostConfig, log)
{
}
+
/// <summary>
/// Creates a new <see cref="RuntimePluginLoader"/> with the specified config and host config dom.
/// </summary>
@@ -126,124 +94,119 @@ namespace VNLib.Plugins.Runtime
/// <exception cref="ArgumentNullException"></exception>
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());
-
+ //Shared types is required so the default load context shares types
+ config.PreferSharedTypes = true;
+
//Default to empty config if null
HostConfig = hostConfig ?? JsonDocument.Parse("{}");
+
Loader = new(config);
PluginPath = config.MainAssemblyPath;
Log = log;
- Loader.Reloaded += Loader_Reloaded;
+
+ //Only regiser reload handler if the load context is unloadable
+ if (config.IsUnloadable)
+ {
+ //Init reloaded event handler
+ Loader.Reloaded += Loader_Reloaded;
+ }
+
//Set the config path default
PluginConfigPath = Path.ChangeExtension(PluginPath, ".json");
- LoadedPlugins = new();
+
+ //Init container
+ Controller = 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();
+ //All plugins must be unloaded forst
+ UnloadAll();
+
//Reload the assembly and
- await InitLoaderAsync();
- //fire after loaded
- OnAfterReloaded?.Invoke(this, eventArgs);
- //Raise the external reloaded event
- Reloaded?.Invoke(this, EventArgs.Empty);
+ await InitializeController();
+
+ //Load plugins
+ LoadPlugins();
}
catch (Exception ex)
{
- Log?.Error(ex);
+ Log?.Error("Failed reload plugins for {loader}\n{ex}", PluginPath, ex);
}
}
/// <summary>
- /// Initializes the plugin loader, the assembly, and all public <see cref="IPlugin"/>
- /// types
+ /// Initializes the plugin loader, and populates the <see cref="Controller"/>
+ /// with initialized plugins.
/// </summary>
/// <returns>A task that represents the initialization</returns>
- public async Task InitLoaderAsync()
+ /// <exception cref="IOException"></exception>
+ /// <exception cref="FileNotFoundException"></exception>
+ public async Task InitializeController()
{
- //Load the main assembly
- Assembly PluginAsm = Loader.LoadDefaultAssembly();
- //Get the plugin's configuration file
- if (FileOperations.FileExists(PluginConfigPath))
+ JsonDocument? pluginConfig = null;
+
+ try
{
- //Open and read the config file
- await using FileStream confStream = File.OpenRead(PluginConfigPath);
- JsonDocumentOptions jdo = new()
+ //Get the plugin's configuration file
+ if (FileOperations.FileExists(PluginConfigPath))
{
- 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))
+ pluginConfig = await this.GetPluginConfigAsync();
+ }
+ else
{
- LoaderArgs = loaderEl;
+ //Set plugin config dom to an empty object if the file does not exist
+ pluginConfig = JsonDocument.Parse("{}");
}
+
+ //Load the main assembly
+ Assembly PluginAsm = Loader.LoadDefaultAssembly();
+
+ //Init container from the assembly
+ Controller.InitializePlugins(PluginAsm);
+
+ string[] cliArgs = Environment.GetCommandLineArgs();
+
+ //Configure log/doms
+ Controller.ConfigurePlugins(HostConfig, pluginConfig, cliArgs);
}
- 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<IPlugin> 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)
+ finally
{
- //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;
- }
+ pluginConfig?.Dispose();
}
}
+
+ /// <summary>
+ /// Loads all configured plugins by calling <see cref="IPlugin.Load"/>
+ /// event hook on the current thread. Loading exceptions are aggregated so not
+ /// to block individual loading.
+ /// </summary>
+ /// <exception cref="AggregateException"></exception>
+ public void LoadPlugins()
+ {
+ //Load all plugins
+ Controller.LoadPlugins();
+ }
+
/// <summary>
/// Manually reload the internal <see cref="PluginLoader"/>
- /// which will reload the assembly and its plugins and endpoints
+ /// which will reload the assembly and its plugins
/// </summary>
- public void ReloadPlugin() => Loader.Reload();
+ public void ReloadPlugins() => Loader.Reload();
/// <summary>
/// Attempts to unload all plugins.
/// </summary>
/// <exception cref="AggregateException"></exception>
- public void UnloadAll() => LoadedPlugins.TryForeach(lp => lp.UnloadPlugin(Log));
+ public void UnloadAll() => Controller.UnloadPlugins();
///<inheritdoc/>
protected override void Free()
{
+ Controller.Dispose();
Loader.Dispose();
- PluginConfigDOM?.Dispose();
}
}
diff --git a/lib/Plugins.Runtime/src/VNLib.Plugins.Runtime.csproj b/lib/Plugins.Runtime/src/VNLib.Plugins.Runtime.csproj
index 37a9e74..87dbcfe 100644
--- a/lib/Plugins.Runtime/src/VNLib.Plugins.Runtime.csproj
+++ b/lib/Plugins.Runtime/src/VNLib.Plugins.Runtime.csproj
@@ -5,7 +5,6 @@
<TargetFramework>net6.0</TargetFramework>
<RootNamespace>VNLib.Plugins.Runtime</RootNamespace>
<AssemblyName>VNLib.Plugins.Runtime</AssemblyName>
- <Version>1.0.1.1</Version>
<AnalysisLevel>latest-all</AnalysisLevel>
<RunAnalyzersDuringBuild>false</RunAnalyzersDuringBuild>
<GenerateDocumentationFile>True</GenerateDocumentationFile>
@@ -44,7 +43,7 @@
</ItemGroup>
<ItemGroup>
- <ProjectReference Include="..\..\Plugins.Essentials\src\VNLib.Plugins.Essentials.csproj" />
+ <ProjectReference Include="..\..\Plugins\src\VNLib.Plugins.csproj" />
<ProjectReference Include="..\..\Utils\src\VNLib.Utils.csproj" />
</ItemGroup>