/*
* Copyright (c) 2024 Vaughn Nugent
*
* Library: VNLib
* Package: VNLib.Plugins.Runtime
* File: PluginStackBuilder.cs
*
* PluginStackBuilder.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.Reflection;
using System.Collections.Generic;
using VNLib.Utils.Logging;
using VNLib.Utils.Extensions;
namespace VNLib.Plugins.Runtime
{
///
/// A construction class used to build a single plugin stack.
///
public sealed class PluginStackBuilder
{
private IPluginDiscoveryManager? DiscoveryManager;
private bool HotReload;
private TimeSpan ReloadDelay;
private IPluginConfigReader? PluginConfig;
private ILogProvider? DebugLog;
private Func? Loader;
///
/// Shortcut constructor for easy fluent chaining.
///
/// A new
public static PluginStackBuilder Create() => new();
///
/// Sets the plugin discovery manager used to find plugins
///
/// The discovery manager instance
/// The current builder instance for chaining
public PluginStackBuilder WithDiscoveryManager(IPluginDiscoveryManager discoveryManager)
{
DiscoveryManager = discoveryManager;
return this;
}
///
/// Enables hot reloading of the plugin assembly
///
/// The delay time after a change is detected before the assembly is reloaded
/// The current builder instance for chaining
public PluginStackBuilder EnableHotReload(TimeSpan reloadDelay)
{
HotReload = true;
ReloadDelay = reloadDelay;
return this;
}
///
/// Specifies the JSON host configuration data to pass to the plugin
///
/// The plugin configuration data
/// The current builder instance for chaining
public PluginStackBuilder WithConfigurationReader(IPluginConfigReader pluginConfig)
{
ArgumentNullException.ThrowIfNull(pluginConfig);
//Store binary copy
PluginConfig = pluginConfig;
return this;
}
///
/// The factory callback function used to get assembly loaders for
/// discovered plugins
///
/// The factory callback funtion
/// The current builder instance for chaining
public PluginStackBuilder WithLoaderFactory(Func loaderFactory)
{
Loader = loaderFactory;
return this;
}
///
/// Specifies the optional debug log provider to use for the plugin loader.
///
/// The optional log provider instance
///The current builder instance for chaining
public PluginStackBuilder WithDebugLog(ILogProvider logProvider)
{
DebugLog = logProvider;
return this;
}
///
/// Creates a snapshot of the current builder state and builds a plugin stack
///
/// The current builder instance for chaining
///
public IPluginStack ConfigureStack()
{
_ = DiscoveryManager ?? throw new ArgumentException("You must specify a plugin discovery manager");
_ = PluginConfig ?? throw new ArgumentException("A plugin confuration reader must be specified");
//Clone the current builder state
PluginStackBuilder clone = (PluginStackBuilder)MemberwiseClone();
return new PluginStack(clone);
}
/*
*
*/
internal sealed record class PluginStack(PluginStackBuilder Builder) : IPluginStack
{
private readonly LinkedList _plugins = new();
///
public IReadOnlyCollection Plugins => _plugins;
///
public void BuildStack()
{
//Discover all plugins
IPluginAssemblyLoader[] loaders = DiscoverPlugins(Builder.DebugLog);
//Create a loader for each plugin
foreach (IPluginAssemblyLoader loader in loaders)
{
RuntimePluginLoader plugin = new(loader, Builder.DebugLog);
_plugins.AddLast(plugin);
}
}
private IPluginAssemblyLoader[] DiscoverPlugins(ILogProvider? debugLog)
{
//Select only dirs with a dll that is named after the directory name
IEnumerable pluginPaths = Builder.DiscoveryManager!.DiscoverPluginFiles();
//Log the found plugin files
IEnumerable pluginFileNames = pluginPaths.Select(static s => $"{Path.GetFileName(s)}\n");
debugLog?.Debug("Found plugin assemblies: \n{files}", string.Concat(pluginFileNames));
LinkedList loaders = new ();
//Create a loader for each plugin
foreach (string pluginPath in pluginPaths)
{
PlugingAssemblyConfig pConf = new(Builder.PluginConfig!)
{
AssemblyFile = pluginPath,
WatchForReload = Builder.HotReload,
ReloadDelay = Builder.ReloadDelay,
Unloadable = Builder.HotReload
};
//Get assembly loader from the configration
IAssemblyLoader loader = Builder.Loader!.Invoke(pConf);
//Add to list
loaders.AddLast(new PluginAsmLoader(loader, pConf));
}
return loaders.ToArray();
}
///
public void Dispose()
{
//dispose all plugins
_plugins.TryForeach(static p => p.Dispose());
_plugins.Clear();
}
}
internal sealed record class PluginAsmLoader(IAssemblyLoader Loader, IPluginAssemblyLoadConfig Config) : IPluginAssemblyLoader
{
///
public void Dispose() => Loader.Dispose();
///
public Assembly GetAssembly() => Loader.GetAssembly();
///
public void Load() => Loader.Load();
///
public void Unload() => Loader.Unload();
}
internal sealed record class PlugingAssemblyConfig(IPluginConfigReader Config) : IPluginAssemblyLoadConfig
{
///
public bool Unloadable { get; init; }
///
public string AssemblyFile { get; init; } = string.Empty;
///
public bool WatchForReload { get; init; }
///
public TimeSpan ReloadDelay { get; init; }
///
public void ReadConfigurationData(Stream outputStream) => Config.ReadPluginConfigData(this, outputStream);
}
}
}