/*
* Copyright (c) 2024 Vaughn Nugent
*
* Library: VNLib
* Package: VNLib.Plugins.Extensions.Loading
* File: LoadingExtensions.cs
*
* LoadingExtensions.cs is part of VNLib.Plugins.Extensions.Loading which is part of the larger
* VNLib collection of libraries and utilities.
*
* VNLib.Plugins.Extensions.Loading is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* VNLib.Plugins.Extensions.Loading 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 Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see https://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 System.Collections.Concurrent;
using System.Diagnostics.CodeAnalysis;
using System.Runtime.CompilerServices;
using System.Runtime.ExceptionServices;
using VNLib.Utils.Logging;
using VNLib.Utils.Resources;
using VNLib.Utils.Extensions;
namespace VNLib.Plugins.Extensions.Loading
{
///
/// Provides common loading (and unloading when required) extensions for plugins
///
public static class LoadingExtensions
{
///
/// A key in the 'plugins' configuration object that specifies
/// an asset search directory
///
public const string DEBUG_CONFIG_KEY = "debug";
public const string SECRETS_CONFIG_KEY = "secrets";
public const string PASSWORD_HASHING_KEY = "passwords";
public const string CUSTOM_PASSWORD_ASM_KEY = "custom_asm";
/*
* Plugin local cache used for storing singletons for a plugin instance
*/
private static readonly ConditionalWeakTable _localCache = new();
private static readonly ConcurrentDictionary _assemblyCache = new();
///
/// Gets a previously cached service singleton for the desired plugin
///
/// The service instance type
/// The plugin to obtain or build the singleton for
/// The method to produce the singleton
/// The cached or newly created singleton
public static object GetOrCreateSingleton(PluginBase plugin, Type serviceType, Func serviceFactory)
{
//Get local cache
PluginLocalCache pc = _localCache.GetValue(plugin, PluginLocalCache.Create);
return pc.GetOrCreateService(serviceType, serviceFactory);
}
///
/// Gets a previously cached service singleton for the desired plugin
/// or creates a new singleton instance for the plugin
///
///
/// The plugin to obtain or build the singleton for
/// The method to produce the singleton
/// The cached or newly created singleton
public static T GetOrCreateSingleton(PluginBase plugin, Func serviceFactory)
=> (T)GetOrCreateSingleton(plugin, typeof(T), p => serviceFactory(p)!);
///
/// Gets the full file path for the assembly asset file name within the assets
/// directory.
///
///
/// The name of the assembly (ex: 'file.dll') to search for
/// Directory search flags
/// The full path to the assembly asset file, or null if the file does not exist
///
public static string? GetAssetFilePath(this PluginBase plugin, string assemblyName, SearchOption searchOption)
{
plugin.ThrowIfUnloaded();
ArgumentNullException.ThrowIfNull(assemblyName);
/*
* Allow an assets directory to limit the scope of the search for the desired
* assembly, otherwise search all plugins directories
*/
string? assetDir = plugin.GetAssetsPath();
assetDir ??= plugin.GetPluginsPath();
/*
* This should never happen since this method can only be called from a
* plugin context, which means this path was used to load the current plugin
*/
ArgumentNullException.ThrowIfNull(assetDir, "No plugin asset directory is defined for the current host configuration, this is likely a bug");
//Get the first file that matches the search file
return Directory.EnumerateFiles(assetDir, assemblyName, searchOption).FirstOrDefault();
}
///
/// Loads a managed assembly into the current plugin's load context and will unload when disposed
/// or the plugin is unloaded from the host application.
///
/// The desired exported type to load from the assembly
///
/// The name of the assembly (ex: 'file.dll') to search for
/// Directory/file search option
///
/// Explicitly define an to load the assmbly, and it's dependencies
/// into. If null, uses the plugin's alc.
///
/// The managing the loaded assmbly in the current AppDomain
///
///
///
/// The assembly is searched within the 'assets' directory specified in the plugin config
/// or the global plugins ('path' key) directory if an assets directory is not defined.
///
public static AssemblyLoader LoadAssembly(
this PluginBase plugin,
string assemblyName,
SearchOption dirSearchOption = SearchOption.AllDirectories,
AssemblyLoadContext? explictAlc = null)
{
//Get the file path for the assembly
string asmFile = GetAssetFilePath(plugin, assemblyName, dirSearchOption)
?? throw new FileNotFoundException($"Failed to find custom assembly {assemblyName} from plugin directory");
//Get the plugin's load context if not explicitly supplied
explictAlc ??= GetPluginLoadContext();
if (plugin.IsDebug())
{
plugin.Log.Verbose("Loading assembly {asm}: from file {file}", assemblyName, asmFile);
}
//Load the assembly
return AssemblyLoader.Load(asmFile, explictAlc, plugin.UnloadToken);
}
///
/// Loads a managed assembly into the current plugin's load context and will unload when disposed
/// or the plugin is unloaded from the host application.
///
///
/// The name of the assembly (ex: 'file.dll') to search for
/// Directory/file search option
///
/// Explicitly define an to load the assmbly, and it's dependencies
/// into. If null, uses the plugin's alc.
///
/// The managing the loaded assmbly in the current AppDomain
///
///
///
/// The assembly is searched within the 'assets' directory specified in the plugin config
/// or the global plugins ('path' key) directory if an assets directory is not defined.
///
public static ManagedLibrary LoadAssembly(
this PluginBase plugin,
string assemblyName,
SearchOption dirSearchOption = SearchOption.AllDirectories,
AssemblyLoadContext? explictAlc = null)
{
/*
* Using an assembly loader instance instead of managed library, so it respects
* the plugin's unload events. Returning the managed library instance will
* hide the overloads that would cause possible type load issues, so using
* an object as the generic type parameter shouldn't be an issue.
*/
return LoadAssembly