/*
* Copyright (c) 2023 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.Collections.Generic;
using VNLib.Utils.IO;
using VNLib.Utils.Extensions;
namespace VNLib.Plugins.Runtime
{
///
/// 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 readonly object? _userState;
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, _userState);
//Store for unload
_service = service;
}
public void OnPluginUnloaded(PluginController controller, object? state)
{
//Unload
_consumerEvents.OnUnload(_service!, _userState);
_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)
{
_ = runtime ?? throw new ArgumentNullException(nameof(runtime));
foreach(RuntimePluginLoader loader in runtime.Plugins)
{
loader.InitializeController();
}
}
///
/// Invokes the load method for all plugin instances
///
///
///
///
public static void InvokeLoad(this IPluginStack runtime)
{
_ = runtime ?? throw new ArgumentNullException(nameof(runtime));
//try loading all plugins
runtime.Plugins.TryForeach(static p => p.LoadPlugins());
}
///
/// Invokes the unload method for all plugin instances
///
///
///
///
public static void InvokeUnload(this IPluginStack runtime)
{
_ = runtime ?? throw new ArgumentNullException(nameof(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)
{
_ = runtime ?? throw new ArgumentNullException(nameof(runtime));
//try unloading all plugins and their loaders
runtime.Plugins.TryForeach(static p => p.UnloadAll());
}
///
/// Reloads all plugins and each assembly loader
///
///
///
///
public static void ReloadAll(this IPluginStack runtime)
{
_ = runtime ?? throw new ArgumentNullException(nameof(runtime));
//try reloading all plugins
runtime.Plugins.TryForeach(static p => p.ReloadPlugins());
}
///
/// 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)
{
_ = runtime ?? throw new ArgumentNullException(nameof(runtime));
_ = listener ?? throw new ArgumentNullException(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)
{
_ = runtime ?? throw new ArgumentNullException(nameof(runtime));
_ = listener ?? throw new ArgumentNullException(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)
{
_ = builder ?? throw new ArgumentNullException(nameof(builder));
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());
}
else
{
//Empty json
byte[] emptyJson = Encoding.UTF8.GetBytes("{}");
reader = new LocalFilePluginConfigReader(emptyJson);
}
//Store binary
return builder.WithConfigurationReader(reader);
}
///
/// Specifies the directory that the plugin loader will search for plugins in
///
/// The search directory path
///
/// The current builder instance for chaining
///
public static PluginStackBuilder WithSearchDirectory(this PluginStackBuilder builder, string path) => WithSearchDirectory(builder, new DirectoryInfo(path));
///
/// 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 WithSearchDirectory(this PluginStackBuilder builder, DirectoryInfo dir)
{
_ = builder ?? throw new ArgumentNullException(nameof(builder));
_ = dir ?? throw new ArgumentNullException(nameof(dir));
PluginDirectorySearcher dirSearcher = new (dir);
builder.WithDiscoveryManager(dirSearcher);
return builder;
}
///
/// 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 Dir) : IPluginDiscoveryManager
{
private const string PLUGIN_FILE_EXTENSION = ".dll";
///
public string[] DiscoverPluginFiles()
{
//Enumerate all dll files within the seach directory
IEnumerable dirs = Dir.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) : IPluginConfigReader
{
public void ReadPluginConfigData(IPluginAssemblyLoadConfig asmConfig, Stream configData)
{
//Allow comments and trailing commas
JsonDocumentOptions jdo = new()
{
AllowTrailingCommas = true,
CommentHandling = JsonCommentHandling.Skip,
};
//Config file is the same name as the assembly but with a json extension
string pluginConfigFile = Path.ChangeExtension(asmConfig.AssemblyFile, ".json");
using JsonDocument hConfig = JsonDocument.Parse(HostJson, jdo);
//Read the plugin config file
if (FileOperations.FileExists(pluginConfigFile))
{
//Open file stream to read data
using FileStream confStream = File.OpenRead(pluginConfigFile);
//Parse the config file
using JsonDocument pConfig = JsonDocument.Parse(confStream, jdo);
//Merge the configs
using JsonDocument merged = hConfig.Merge(pConfig,"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 pConfig = JsonDocument.Parse(pluginConfig, jdo);
//Merge the configs
using JsonDocument merged = hConfig.Merge(pConfig,"host", "plugin");
//Write the merged config to the output stream
using Utf8JsonWriter writer = new(configData);
merged.WriteTo(writer);
}
}
}
}
}