/*
* Copyright (c) 2022 Vaughn Nugent
*
* Library: VNLib
* Package: VNLib.Plugins.Runtime
* File: DynamicPluginLoader.cs
*
* DynamicPluginLoader.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.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
{
///
/// A runtime .NET assembly loader specialized to load
/// assemblies that export types.
///
public class RuntimePluginLoader : VnDisposeable
{
protected readonly PluginLoader Loader;
protected readonly string PluginPath;
protected readonly JsonDocument HostConfig;
protected readonly ILogProvider? Log;
protected readonly LinkedList LoadedPlugins;
///
/// A readonly collection of all loaded plugin wrappers
///
public IReadOnlyCollection LivePlugins => LoadedPlugins;
///
/// An event that is raised before the loader
/// unloads all plugin instances
///
protected event EventHandler? OnBeforeReloaded;
///
/// An event that is raised after a successfull reload of all new
/// plugins for the instance
///
protected event EventHandler? OnAfterReloaded;
///
/// Raised when the current loader has reloaded the assembly and
/// all plugins were successfully loaded.
///
public event EventHandler? Reloaded;
///
/// The current plugin's JSON configuration DOM loaded from the plugin's directory
/// if it exists. Only valid after first initalization
///
public JsonDocument? PluginConfigDOM { get; private set; }
///
/// Optional loader arguments object for the plugin
///
protected JsonElement? LoaderArgs { get; private set; }
///
/// The path of the plugin's configuration file. (Default = pluginPath.json)
///
public string PluginConfigPath { get; init; }
///
/// Creates a new with the specified
/// assembly location and host config.
///
///
/// A nullable log provider
/// The configuration DOM to merge with plugin config DOM and pass to enabled plugins
/// A value that specifies if the assembly can be unloaded
/// A value that spcifies if the loader will listen for changes to the assembly file and reload the plugins
/// A value that specifies if assembly dependencies are loaded on-demand
///
/// The argument may be null if is false
///
///
public RuntimePluginLoader(string pluginPath, JsonDocument? hostConfig = null, ILogProvider? log = null, bool unloadable = false, bool hotReload = false, bool lazy = false)
:this(
new PluginConfig(pluginPath)
{
IsUnloadable = unloadable || hotReload,
EnableHotReload = hotReload,
IsLazyLoaded = lazy,
ReloadDelay = TimeSpan.FromSeconds(1),
PreferSharedTypes = true,
DefaultContext = AssemblyLoadContext.Default
},
hostConfig, log)
{
}
///
/// Creates a new with the specified config and host config dom.
///
/// The plugin's loader configuration
/// The host/process configuration DOM
/// A log provider to write plugin unload log events to
///
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());
//Default to empty config if null
HostConfig = hostConfig ?? JsonDocument.Parse("{}");
Loader = new(config);
PluginPath = config.MainAssemblyPath;
Log = log;
Loader.Reloaded += Loader_Reloaded;
//Set the config path default
PluginConfigPath = Path.ChangeExtension(PluginPath, ".json");
LoadedPlugins = 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();
//Reload the assembly and
await InitLoaderAsync();
//fire after loaded
OnAfterReloaded?.Invoke(this, eventArgs);
//Raise the external reloaded event
Reloaded?.Invoke(this, EventArgs.Empty);
}
catch (Exception ex)
{
Log?.Error(ex);
}
}
///
/// Initializes the plugin loader, the assembly, and all public
/// types
///
/// A task that represents the initialization
public async Task InitLoaderAsync()
{
//Load the main assembly
Assembly PluginAsm = Loader.LoadDefaultAssembly();
//Get the plugin's configuration file
if (FileOperations.FileExists(PluginConfigPath))
{
//Open and read the config file
await using FileStream confStream = File.OpenRead(PluginConfigPath);
JsonDocumentOptions jdo = new()
{
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))
{
LoaderArgs = loaderEl;
}
}
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 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)
{
//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;
}
}
}
///
/// Manually reload the internal
/// which will reload the assembly and its plugins and endpoints
///
public void ReloadPlugin() => Loader.Reload();
///
/// Attempts to unload all plugins.
///
///
public void UnloadAll() => LoadedPlugins.TryForeach(lp => lp.UnloadPlugin(Log));
///
protected override void Free()
{
Loader.Dispose();
PluginConfigDOM?.Dispose();
}
}
}