/*
* Copyright (c) 2024 Vaughn Nugent
*
* Library: VNLib
* Package: VNLib.Plugins.Runtime
* File: LoaderExtensions.cs
*
* LoaderExtensions.cs is part of VNLib.Plugins.Runtime which is part of the larger
* VNLib collection of libraries and utilities.
*
* VNLib.Plugins.Runtime is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published
* by the Free Software Foundation, either version 2 of the License,
* or (at your option) any later version.
*
* VNLib.Plugins.Runtime is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
* General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with VNLib.Plugins.Runtime. If not, see http://www.gnu.org/licenses/.
*/
using System;
using System.IO;
using System.Linq;
using System.Text;
using System.Text.Json;
using System.Threading.Tasks;
using System.Collections.Generic;
using VNLib.Utils.IO;
using VNLib.Utils.Extensions;
namespace VNLib.Plugins.Runtime
{
///
/// A callback function signature for plugin plugin loading errors on plugin
/// stacks.
///
/// The loader that the exception occured on
/// The exception cause of the error
public delegate void PluginLoadErrorHandler(RuntimePluginLoader Loader, Exception exception);
///
/// Contains extension methods for PluginLoader library
///
public static class LoaderExtensions
{
/*
* Class that manages a collection registration for a specific type
* dependency, and redirects the event calls for the consumed service
*/
private sealed class TypedRegistration : IPluginEventListener where T: class
{
private readonly ITypedPluginConsumer _consumerEvents;
private T? _service;
private readonly Type _type;
public TypedRegistration(ITypedPluginConsumer 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, state);
//Store for unload
_service = service;
}
public void OnPluginUnloaded(PluginController controller, object? state)
{
//Unload
_consumerEvents.OnUnload(_service!, state);
_service = null;
}
}
///
/// Registers a plugin even handler for the current
/// for a specific type.
///
///
///
/// The typed plugin instance event consumer
/// A handle that manages this event registration
///
public static PluginEventRegistration RegisterForType(this PluginController collection, ITypedPluginConsumer consumer) where T: class
{
Type serviceType = typeof(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 reg = new(consumer, serviceType);
//register event handler
return Register(collection, reg, null);
}
///
/// Registers a handler to listen for plugin load/unload events
///
///
/// A handle that will unregister the listener when disposed
public static PluginEventRegistration Register(this IPluginEventRegistrar reg, IPluginEventListener listener, object? state = null)
{
reg.Register(listener, state);
return new(reg, listener);
}
///
/// Determines if the current
/// exposes the desired type on is
/// type.
///
///
/// The desired type to request
/// True if the plugin exposes the desired type, false otherwise
public static bool ExposesType(this PluginController collection, Type type)
{
return collection.Plugins
.Where(pl => type.IsAssignableFrom(pl.PluginType))
.Any();
}
///
/// Searches all plugins within the current loader for a
/// single plugin that derrives the specified type
///
/// The type the plugin must derrive from
///
/// The instance of your custom type casted, or null if not found or could not be casted
public static T? GetExposedTypes(this PluginController collection) where T: class
{
LivePlugin? plugin = collection.Plugins
.Where(static pl => typeof(T).IsAssignableFrom(pl.PluginType))
.SingleOrDefault();
return plugin?.Plugin as T;
}
///
/// Serially initialzies all plugin lifecycle controllers and configures
/// plugin instances.
///
///
///
public static void InitializeAll(this IPluginStack runtime)
{
ArgumentNullException.ThrowIfNull(runtime, nameof(runtime));
foreach(RuntimePluginLoader loader in runtime.Plugins)
{
loader.InitializeController();
}
}
///
/// Invokes the load method for all plugin instances
///
///
/// A value that indicates if plugins should be loaded concurrently or sequentially
///
///
public static void InvokeLoad(this IPluginStack runtime, bool concurrent)
{
List exceptions = new ();
//Add load exceptions into the list
void onError(RuntimePluginLoader loader, Exception ex) => exceptions.Add(ex);
//Invoke load with onError callback
InvokeLoad(runtime, concurrent, onError);
//If any exceptions occured, throw them now
if(exceptions.Count > 0)
{
throw new AggregateException(exceptions);
}
}
///
/// Invokes the load method for all plugin instances, and captures exceptions
/// into the specified callback function.
///
///
/// A value that indicates if plugins should be loaded concurrently or sequentially
/// A callback function to handle error conditions instead of raising exceptions
///
public static void InvokeLoad(this IPluginStack runtime, bool concurrent, PluginLoadErrorHandler onError)
{
ArgumentNullException.ThrowIfNull(runtime, nameof(runtime));
if (concurrent)
{
//Invoke load in parallel
Parallel.ForEach(runtime.Plugins, p =>
{
try
{
p.LoadPlugins();
}
catch (Exception ex)
{
onError(p, ex);
}
});
}
else
{
//Load sequentially
foreach(RuntimePluginLoader loader in runtime.Plugins)
{
try
{
loader.LoadPlugins();
}
catch (Exception ex)
{
onError(loader, ex);
}
}
}
}
///
/// Invokes the unload method for all plugin instances
///
///
///
///
public static void InvokeUnload(this IPluginStack runtime)
{
ArgumentNullException.ThrowIfNull(runtime);
//try unloading all plugins
runtime.Plugins.TryForeach(static p => p.UnloadPlugins());
}
///
/// Unloads all plugins and the plugin assembly loader
/// if unloading is supported.
///
///
///
///
public static void UnloadAll(this IPluginStack runtime)
{
ArgumentNullException.ThrowIfNull(runtime);
//try unloading all plugins and their loaders, dont invoke GC on each unload
runtime.Plugins.TryForeach(static p => p.UnloadAll(false));
//Invoke a gc
GC.Collect();
GC.WaitForPendingFinalizers();
}
///
/// Reloads all plugins and each assembly loader
///
///
///
///
public static void ReloadAll(this IPluginStack runtime)
{
ArgumentNullException.ThrowIfNull(runtime, nameof(runtime));
//try reloading all plugins
runtime.Plugins.TryForeach(static p => p.ReloadPlugins(false));
//Invoke a gc
GC.Collect();
GC.WaitForPendingFinalizers();
}
///
/// Registers a plugin event listener for all plugins
///
///
/// The event listener instance
/// Optional state parameter
///
public static void RegsiterListener(this IPluginStack runtime, IPluginEventListener listener, object? state = null)
{
ArgumentNullException.ThrowIfNull(runtime, nameof(runtime));
ArgumentNullException.ThrowIfNull(listener, nameof(listener));
//Register for all plugins
foreach (PluginController controller in runtime.Plugins.Select(static p => p.Controller))
{
controller.Register(listener, state);
}
}
///
/// Unregisters a plugin event listener for all plugins
///
///
/// The listener instance to unregister
///
public static void UnregsiterListener(this IPluginStack runtime, IPluginEventListener listener)
{
ArgumentNullException.ThrowIfNull(runtime, nameof(runtime));
ArgumentNullException.ThrowIfNull(listener, nameof(listener));
//Unregister for all plugins
foreach (PluginController controller in runtime.Plugins.Select(static p => p.Controller))
{
controller.Unregister(listener);
}
}
///
/// Configures the plugin stack to retrieve plugin-local json configuration files
/// from the same directory as the plugin assembly file.
///
///
/// An optional configuration element to pass to the plugin's host config element
/// The current builder instance for chaining
public static PluginStackBuilder WithLocalJsonConfig(
this PluginStackBuilder builder,
in JsonElement? hostConfig
)
=> WithJsonConfig(builder, in hostConfig, null);
///
/// Configures the plugin stack to retrieve plugin-local json configuration files
/// from the same directory as the plugin assembly file.
///
///
/// The directory containing all configuration files for the stack
/// An optional configuration element to pass to the plugin's host config element
/// The current builder instance for chaining
public static PluginStackBuilder WithJsonConfigDir(
this PluginStackBuilder builder,
in JsonElement? hostConfig,
DirectoryInfo configDir
)
{
/*
* Local function forces config files to be located in the
* specified directory.
*/
string AltDirConfigFileFinder(IPluginAssemblyLoadConfig asmConfig)
{
//Get the plugin config file name
string configFileName = Path.ChangeExtension(asmConfig.AssemblyFile, ".json");
configFileName = Path.GetFileName(configFileName);
//Search for the file within the config directory
return Path.Combine(configDir.FullName, configFileName);
}
//Use the alternate directory finder
return WithJsonConfig(builder, in hostConfig, AltDirConfigFileFinder);
}
///
/// Configures the plugin stack to retrieve a json configuration file from the specified callbac function,
/// or local to the assembly if the callback is null.
///
///
/// An optional callback function that finds the plugin json config file from its assembly path
/// An optional configuration element to pass to the plugin's host config element
/// The current builder instance for chaining
public static PluginStackBuilder WithJsonConfig(
this PluginStackBuilder builder,
in JsonElement? hostConfig,
Func? getPluginJsonFile
)
{
ArgumentNullException.ThrowIfNull(builder, nameof(builder));
//Set default callback if not specified
getPluginJsonFile ??= GetDefaultFileNameCallback;
LocalFilePluginConfigReader reader;
//Host config is optional
if (hostConfig.HasValue)
{
//Clone the host config into binary
using VnMemoryStream ms = new();
using (Utf8JsonWriter writer = new(ms))
{
hostConfig.Value.WriteTo(writer);
}
//Create a reader from the binary
reader = new LocalFilePluginConfigReader(ms.ToArray(), getPluginJsonFile);
}
else
{
//Empty json
byte[] emptyJson = Encoding.UTF8.GetBytes("{}");
reader = new LocalFilePluginConfigReader(emptyJson, getPluginJsonFile);
}
//Store binary
return builder.WithConfigurationReader(reader);
static string GetDefaultFileNameCallback(IPluginAssemblyLoadConfig asmConfig)
{
/*
* Just changing the asm file extention means the the json file
* will be in the same directory as the plugin assembly file
*/
return Path.ChangeExtension(asmConfig.AssemblyFile, ".json");
}
}
///
/// Sets an empty/null configuration reader for the plugin stack. No
/// configuration will be passed to plugins.
///
///
/// The current builder instance for chaining
///
public static PluginStackBuilder WithNullConfig(this PluginStackBuilder builder)
{
ArgumentNullException.ThrowIfNull(builder, nameof(builder));
return builder.WithConfigurationReader(new NullPluginConfigReader());
}
///
/// Specifies the directory that the plugin loader will search for plugins in
///
/// An array of search directories
///
/// The current builder instance for chaining
///
public static PluginStackBuilder WithSearchDirectories(this PluginStackBuilder builder, string[] paths)
=> WithSearchDirectories(builder, paths.Select(static p => new DirectoryInfo(p)).ToArray());
///
/// Specifies the directory that the plugin loader will search for plugins in
///
/// The search directory instance
///
/// The current builder instance for chaining
///
public static PluginStackBuilder WithSearchDirectories(this PluginStackBuilder builder, DirectoryInfo[] dirs)
{
ArgumentNullException.ThrowIfNull(builder);
ArgumentNullException.ThrowIfNull(dirs);
builder.WithDiscoveryManager(discoveryManager: new PluginDirectorySearcher(dirs));
return builder;
}
///
/// Registers a new for the current plugin stack
/// that will listen for plugin events and capture the exported services into a
/// single pool.
///
///
/// A new that will capture all exported services when loaded
public static SharedPluginServiceProvider RegisterServiceProvider(this IPluginStack stack)
{
//Init new service provider
SharedPluginServiceProvider provider = new();
//Register for all plugins
RegsiterListener(stack, provider);
return provider;
}
///
/// Gets the current collection of loaded plugins for the plugin stack
///
///
/// An enumeration of all wrappers
public static IEnumerable GetAllPlugins(this IPluginStack stack) => stack.Plugins.SelectMany(static p => p.Controller.Plugins);
private sealed record class PluginDirectorySearcher(DirectoryInfo[] SearchDirs) : IPluginDiscoveryManager
{
private const string PLUGIN_FILE_EXTENSION = ".dll";
///
public string[] DiscoverPluginFiles()
{
/*
* Accumulate all plugin child directores
* from the search directories
*/
IEnumerable dirs = SearchDirs.SelectMany(static p => p.EnumerateDirectories("*", SearchOption.TopDirectoryOnly));
//Search all directories for plugins and return the paths
return GetPluginPaths(dirs).ToArray();
}
private static IEnumerable GetPluginPaths(IEnumerable 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);
});
}
}
/*
* Assumes plugin configuration data is stored in a json file with the same name as
* the plugin assembly but with a json extension.
*
* The json file is local for the specific plugin and is not shared between plugins. The host
* configuration is also required
*/
private sealed record class LocalFilePluginConfigReader(ReadOnlyMemory HostJson, Func GetConfigFilePathCallback)
: IPluginConfigReader
{
public void ReadPluginConfigData(IPluginAssemblyLoadConfig asmConfig, Stream configData)
{
//Allow comments and trailing commas
JsonDocumentOptions jdo = new()
{
AllowTrailingCommas = true,
CommentHandling = JsonCommentHandling.Skip,
};
//Get the plugin config file name
string pluginConfigFile = GetConfigFilePathCallback(asmConfig);
using JsonDocument hostConfig = JsonDocument.Parse(HostJson, jdo);
if (FileOperations.FileExists(pluginConfigFile))
{
//Open file stream to read data
using FileStream pluginConfFileData = File.OpenRead(pluginConfigFile);
using JsonDocument pluginConf = JsonDocument.Parse(pluginConfFileData, jdo);
using JsonDocument merged = hostConfig.Merge(pluginConf,"host", "plugin");
//Write the merged config to the output stream
using Utf8JsonWriter writer = new(configData);
merged.WriteTo(writer);
}
else
{
byte[] pluginConfig = Encoding.UTF8.GetBytes("{}");
using JsonDocument pluginConf = JsonDocument.Parse(pluginConfig, jdo);
using JsonDocument merged = hostConfig.Merge(pluginConf, "host", "plugin");
//Write the merged config to the output stream
using Utf8JsonWriter writer = new(configData);
merged.WriteTo(writer);
}
}
}
private sealed class NullPluginConfigReader : IPluginConfigReader
{
///
public void ReadPluginConfigData(IPluginAssemblyLoadConfig asmConfig, Stream outputStream)
{
//Do nothing
}
}
}
}