aboutsummaryrefslogtreecommitdiff
path: root/lib/VNLib.Plugins.Extensions.Loading/src/LoadingExtensions.cs
diff options
context:
space:
mode:
Diffstat (limited to 'lib/VNLib.Plugins.Extensions.Loading/src/LoadingExtensions.cs')
-rw-r--r--lib/VNLib.Plugins.Extensions.Loading/src/LoadingExtensions.cs374
1 files changed, 304 insertions, 70 deletions
diff --git a/lib/VNLib.Plugins.Extensions.Loading/src/LoadingExtensions.cs b/lib/VNLib.Plugins.Extensions.Loading/src/LoadingExtensions.cs
index 743566d..62af9e3 100644
--- a/lib/VNLib.Plugins.Extensions.Loading/src/LoadingExtensions.cs
+++ b/lib/VNLib.Plugins.Extensions.Loading/src/LoadingExtensions.cs
@@ -1,5 +1,5 @@
/*
-* Copyright (c) 2022 Vaughn Nugent
+* Copyright (c) 2023 Vaughn Nugent
*
* Library: VNLib
* Package: VNLib.Plugins.Extensions.Loading
@@ -26,6 +26,7 @@ using System;
using System.IO;
using System.Linq;
using System.Text.Json;
+using System.Reflection;
using System.Threading.Tasks;
using System.Collections.Generic;
using System.Runtime.CompilerServices;
@@ -37,7 +38,7 @@ using VNLib.Utils.Extensions;
using VNLib.Plugins.Essentials.Accounts;
namespace VNLib.Plugins.Extensions.Loading
-{
+{
/// <summary>
/// Provides common loading (and unloading when required) extensions for plugins
@@ -103,45 +104,12 @@ namespace VNLib.Plugins.Extensions.Loading
/// <exception cref="OverflowException"></exception>
/// <exception cref="KeyNotFoundException"></exception>
/// <exception cref="ObjectDisposedException"></exception>
- public static PasswordHashing GetPasswords(this PluginBase plugin)
+ public static IPasswordHashingProvider GetPasswords(this PluginBase plugin)
{
plugin.ThrowIfUnloaded();
- //Get/load the passwords one time only
- return GetOrCreateSingleton(plugin, LoadPasswords);
+ //Check if a password configuration element is loaded, otherwise load with defaults
+ return plugin.GetOrCreateSingleton<SecretProvider>().Passwords;
}
-
- private static PasswordHashing LoadPasswords(PluginBase plugin)
- {
- PasswordHashing Passwords;
-
- //Create new session provider
- SecretProvider secrets = new();
-
- //Load the secret in the background
- secrets.LoadSecret(plugin);
-
- //See hashing params are defined
- IReadOnlyDictionary<string, JsonElement>? hashingArgs = plugin.TryGetConfig(PASSWORD_HASHING_KEY);
- if (hashingArgs != null)
- {
- //Get hashing arguments
- uint saltLen = hashingArgs["salt_len"].GetUInt32();
- uint hashLen = hashingArgs["hash_len"].GetUInt32();
- uint timeCost = hashingArgs["time_cost"].GetUInt32();
- uint memoryCost = hashingArgs["memory_cost"].GetUInt32();
- uint parallelism = hashingArgs["parallelism"].GetUInt32();
- //Load passwords
- Passwords = new(secrets, (int)saltLen, timeCost, memoryCost, parallelism, hashLen);
- }
- else
- {
- //Init default password hashing
- Passwords = new(secrets);
- }
- //return
- return Passwords;
- }
-
/// <summary>
/// Loads an assembly into the current plugins AppDomain and will unload when disposed
@@ -165,21 +133,21 @@ namespace VNLib.Plugins.Extensions.Loading
_ = assemblyName ?? throw new ArgumentNullException(nameof(assemblyName));
//get plugin directory from config
- IReadOnlyDictionary<string, JsonElement> config = plugin.GetConfig("plugins");
+ IConfigScope config = plugin.GetConfig("plugins");
/*
* Allow an assets directory to limit the scope of the search for the desired
* assembly, otherwise search all plugins directories
*/
- string? assetDir = config.GetPropString("assets");
+ string? assetDir = config.GetPropString(PLUGIN_ASSET_KEY);
assetDir ??= config["path"].GetString();
/*
* 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
*/
- _ = assetDir ?? throw new ArgumentNullException("assets", "No plugin path is defined for the current host configuration, this is likely a bug");
+ _ = assetDir ?? throw new ArgumentNullException(PLUGIN_ASSET_KEY, "No plugin path is defined for the current host configuration, this is likely a bug");
//Get the first file that matches the search file
string? asmFile = Directory.EnumerateFiles(assetDir, assemblyName, dirSearchOption).FirstOrDefault();
@@ -187,8 +155,7 @@ namespace VNLib.Plugins.Extensions.Loading
//Load the assembly
return AssemblyLoader<T>.Load(asmFile, plugin.UnloadToken);
- }
-
+ }
/// <summary>
/// Determintes if the current plugin config has a debug propety set
@@ -226,7 +193,7 @@ namespace VNLib.Plugins.Extensions.Loading
/// <param name="delayMs">An optional startup delay for the operation</param>
/// <returns>A task that completes when the deferred task completes </returns>
/// <exception cref="ObjectDisposedException"></exception>
- public static async Task ObserveTask(this PluginBase plugin, Func<Task> asyncTask, int delayMs = 0)
+ public static async Task ObserveWork(this PluginBase plugin, Func<Task> asyncTask, int delayMs = 0)
{
/*
* Motivation:
@@ -267,7 +234,6 @@ namespace VNLib.Plugins.Extensions.Loading
}
}
-
/// <summary>
/// Schedules work to begin after the specified delay to be observed by the plugin while
/// passing plugin specifie information. Exceptions are logged to the default plugin log
@@ -278,22 +244,22 @@ namespace VNLib.Plugins.Extensions.Loading
/// <returns>The task that represents the scheduled work</returns>
public static Task ObserveWork(this PluginBase plugin, IAsyncBackgroundWork work, int delayMs = 0)
{
- return ObserveTask(plugin, () => work.DoWorkAsync(plugin.Log, plugin.UnloadToken), delayMs);
+ return ObserveWork(plugin, () => work.DoWorkAsync(plugin.Log, plugin.UnloadToken), delayMs);
}
/// <summary>
/// Registers an event to occur when the plugin is unloaded on a background thread
/// and will cause the Plugin.Unload() method to block until the event completes
/// </summary>
- /// <param name="pbase"></param>
+ /// <param name="plugin"></param>
/// <param name="callback">The method to call when the plugin is unloaded</param>
/// <returns>A task that represents the registered work</returns>
/// <exception cref="ArgumentNullException"></exception>
/// <exception cref="ObjectDisposedException"></exception>
- public static Task RegisterForUnload(this PluginBase pbase, Action callback)
+ public static Task RegisterForUnload(this PluginBase plugin, Action callback)
{
//Test status
- pbase.ThrowIfUnloaded();
+ plugin.ThrowIfUnloaded();
_ = callback ?? throw new ArgumentNullException(nameof(callback));
//Wait method
@@ -307,9 +273,238 @@ namespace VNLib.Plugins.Extensions.Loading
}
//Registaer the task to cause the plugin to wait
- return pbase.ObserveTask(() => WaitForUnload(pbase, callback));
+ return plugin.ObserveWork(() => WaitForUnload(plugin, callback));
+ }
+
+ /// <summary>
+ /// <para>
+ /// Gets or inializes a singleton service of the desired type.
+ /// </para>
+ /// <para>
+ /// If the type derrives <see cref="IAsyncConfigurable"/> the <see cref="IAsyncConfigurable.ConfigureServiceAsync"/>
+ /// method is called once when the instance is loaded, and observed on the plugin scheduler.
+ /// </para>
+ /// <para>
+ /// If the type derrives <see cref="IAsyncBackgroundWork"/> the <see cref="IAsyncBackgroundWork.DoWorkAsync(ILogProvider, System.Threading.CancellationToken)"/>
+ /// method is called once when the instance is loaded, and observed on the plugin scheduler.
+ /// </para>
+ /// </summary>
+ /// <typeparam name="T"></typeparam>
+ /// <param name="plugin"></param>
+ /// <returns></returns>
+ /// <exception cref="KeyNotFoundException"></exception>
+ /// <exception cref="ObjectDisposedException"></exception>
+ /// <exception cref="EntryPointNotFoundException"></exception>
+ public static T GetOrCreateSingleton<T>(this PluginBase plugin)
+ {
+ //Add service to service continer
+ return GetOrCreateSingleton(plugin, CreateService<T>);
+ }
+
+ /// <summary>
+ /// <para>
+ /// Gets or inializes a singleton service of the desired type.
+ /// </para>
+ /// <para>
+ /// If the type derrives <see cref="IAsyncConfigurable"/> the <see cref="IAsyncConfigurable.ConfigureServiceAsync"/>
+ /// method is called once when the instance is loaded, and observed on the plugin scheduler.
+ /// </para>
+ /// <para>
+ /// If the type derrives <see cref="IAsyncBackgroundWork"/> the <see cref="IAsyncBackgroundWork.DoWorkAsync(ILogProvider, System.Threading.CancellationToken)"/>
+ /// method is called once when the instance is loaded, and observed on the plugin scheduler.
+ /// </para>
+ /// </summary>
+ /// <typeparam name="T"></typeparam>
+ /// <param name="plugin"></param>
+ /// <param name="configName">Overrids the default configuration property name</param>
+ /// <returns>The configured service singleton</returns>
+ /// <exception cref="KeyNotFoundException"></exception>
+ /// <exception cref="ObjectDisposedException"></exception>
+ /// <exception cref="EntryPointNotFoundException"></exception>
+ public static T GetOrCreateSingleton<T>(this PluginBase plugin, string configName)
+ {
+ //Add service to service continer
+ return GetOrCreateSingleton(plugin, (plugin) => CreateService<T>(plugin, configName));
+ }
+
+ /// <summary>
+ /// Configures the service asynchronously on the plugin's scheduler and returns a task
+ /// that represents the configuration work.
+ /// </summary>
+ /// <typeparam name="T">The service type</typeparam>
+ /// <param name="plugin"></param>
+ /// <param name="service">The service to configure</param>
+ /// <param name="delayMs">The time in milliseconds to delay the configuration task</param>
+ /// <returns>A task that complets when the load operation completes</returns>
+ /// <exception cref="ObjectDisposedException"></exception>
+ public static Task ConfigureServiceAsync<T>(this PluginBase plugin, T service, int delayMs = 0) where T : IAsyncConfigurable
+ {
+ //Register async load
+ return ObserveWork(plugin, () => service.ConfigureServiceAsync(plugin), delayMs);
+ }
+
+ /// <summary>
+ /// <para>
+ /// Creates and configures a new instance of the desired type and captures the configuration
+ /// information from the type.
+ /// </para>
+ /// <para>
+ /// If the type derrives <see cref="IAsyncConfigurable"/> the <see cref="IAsyncConfigurable.ConfigureServiceAsync"/>
+ /// method is called once when the instance is loaded, and observed on the plugin scheduler.
+ /// </para>
+ /// <para>
+ /// If the type derrives <see cref="IAsyncBackgroundWork"/> the <see cref="IAsyncBackgroundWork.DoWorkAsync(ILogProvider, System.Threading.CancellationToken)"/>
+ /// method is called once when the instance is loaded, and observed on the plugin scheduler.
+ /// </para>
+ /// <para>
+ /// If the type derrives <see cref="IDisposable"/> the <see cref="IDisposable.Dispose"/> method is called once when
+ /// the plugin is unloaded.
+ /// </para>
+ /// </summary>
+ /// <typeparam name="T">The service type</typeparam>
+ /// <param name="plugin"></param>
+ /// <returns>The a new instance configured service</returns>
+ /// <exception cref="KeyNotFoundException"></exception>
+ /// <exception cref="ObjectDisposedException"></exception>
+ /// <exception cref="EntryPointNotFoundException"></exception>
+ public static T CreateService<T>(this PluginBase plugin)
+ {
+ if (plugin.HasConfigForType<T>())
+ {
+ IConfigScope config = plugin.GetConfigForType<T>();
+ return CreateService<T>(plugin, config);
+ }
+ else
+ {
+ return CreateService<T>(plugin, (IConfigScope?)null);
+ }
+ }
+
+ /// <summary>
+ /// <para>
+ /// Creates and configures a new instance of the desired type, with the configuration property name
+ /// </para>
+ /// <para>
+ /// If the type derrives <see cref="IAsyncConfigurable"/> the <see cref="IAsyncConfigurable.ConfigureServiceAsync"/>
+ /// method is called once when the instance is loaded, and observed on the plugin scheduler.
+ /// </para>
+ /// <para>
+ /// If the type derrives <see cref="IAsyncBackgroundWork"/> the <see cref="IAsyncBackgroundWork.DoWorkAsync(ILogProvider, System.Threading.CancellationToken)"/>
+ /// method is called once when the instance is loaded, and observed on the plugin scheduler.
+ /// </para>
+ /// </summary>
+ /// <typeparam name="T">The service type</typeparam>
+ /// <param name="plugin"></param>
+ /// <param name="configName">The configuration element name to pass to the new instance</param>
+ /// <returns>The a new instance configured service</returns>
+ /// <exception cref="KeyNotFoundException"></exception>
+ /// <exception cref="ObjectDisposedException"></exception>
+ /// <exception cref="EntryPointNotFoundException"></exception>
+ public static T CreateService<T>(this PluginBase plugin, string configName)
+ {
+ IConfigScope config = plugin.GetConfig(configName);
+ return CreateService<T>(plugin, config);
+ }
+
+ /// <summary>
+ /// <para>
+ /// Creates and configures a new instance of the desired type, with the specified configuration scope
+ /// </para>
+ /// <para>
+ /// If the type derrives <see cref="IAsyncConfigurable"/> the <see cref="IAsyncConfigurable.ConfigureServiceAsync"/>
+ /// method is called once when the instance is loaded, and observed on the plugin scheduler.
+ /// </para>
+ /// <para>
+ /// If the type derrives <see cref="IAsyncBackgroundWork"/> the <see cref="IAsyncBackgroundWork.DoWorkAsync(ILogProvider, System.Threading.CancellationToken)"/>
+ /// method is called once when the instance is loaded, and observed on the plugin scheduler.
+ /// </para>
+ /// </summary>
+ /// <typeparam name="T">The service type</typeparam>
+ /// <param name="plugin"></param>
+ /// <param name="config">The configuration scope to pass directly to the new instance</param>
+ /// <returns>The a new instance configured service</returns>
+ /// <exception cref="KeyNotFoundException"></exception>
+ /// <exception cref="ObjectDisposedException"></exception>
+ /// <exception cref="EntryPointNotFoundException"></exception>
+ public static T CreateService<T>(this PluginBase plugin, IConfigScope? config)
+ {
+ plugin.ThrowIfUnloaded();
+
+ Type serviceType = typeof(T);
+
+ T service;
+
+ //Determin configuration requirments
+ if (ConfigurationExtensions.ConfigurationRequired(serviceType) || config != null)
+ {
+ if(config == null)
+ {
+ ConfigurationExtensions.ThrowConfigNotFoundForType(serviceType);
+ }
+
+ //Get the constructor for required or available config
+ ConstructorInfo? constructor = serviceType.GetConstructor(new Type[] { typeof(PluginBase), typeof(IConfigScope) });
+
+ //Make sure the constructor exists
+ _ = constructor ?? throw new EntryPointNotFoundException($"No constructor found for {serviceType.Name}");
+
+ //Call constructore
+ service = (T)constructor.Invoke(new object[2] { plugin, config });
+ }
+ else
+ {
+ //Get the constructor
+ ConstructorInfo? constructor = serviceType.GetConstructor(new Type[] { typeof(PluginBase) });
+
+ //Make sure the constructor exists
+ _ = constructor ?? throw new EntryPointNotFoundException($"No constructor found for {serviceType.Name}");
+
+ //Call constructore
+ service = (T)constructor.Invoke(new object[1] { plugin });
+ }
+
+ Task? loading = null;
+
+ //If the service is async configurable, configure it
+ if (service is IAsyncConfigurable asc)
+ {
+#pragma warning disable CA5394 // Do not use insecure randomness
+ int randomDelay = Random.Shared.Next(1, 100);
+#pragma warning restore CA5394 // Do not use insecure randomness
+
+ //Register async load
+ loading = plugin.ConfigureServiceAsync(asc, randomDelay);
+ }
+
+ //Allow background work loading
+ if (service is IAsyncBackgroundWork bw)
+ {
+
+#pragma warning disable CA5394 // Do not use insecure randomness
+ int randomDelay = Random.Shared.Next(10, 200);
+#pragma warning restore CA5394 // Do not use insecure randomness
+
+ //If the instances supports async loading, dont start work until its loaded
+ if(loading != null)
+ {
+ _ = loading.ContinueWith(t => ObserveWork(plugin, bw, randomDelay), TaskScheduler.Default);
+ }
+ else
+ {
+ _ = ObserveWork(plugin, bw, randomDelay);
+ }
+ }
+
+ //register dispose cleanup
+ if (service is IDisposable disp)
+ {
+ _ = plugin.RegisterForUnload(disp.Dispose);
+ }
+
+ return service;
}
+
private sealed class PluginLocalCache
{
private readonly PluginBase _plugin;
@@ -348,52 +543,91 @@ namespace VNLib.Plugins.Extensions.Loading
}
}
- private sealed class SecretProvider : ISecretProvider
+ [ConfigurationName(PASSWORD_HASHING_KEY, Required = false)]
+ private sealed class SecretProvider : VnDisposeable, ISecretProvider, IAsyncConfigurable
{
private byte[]? _pepper;
private Exception? _error;
- ///<inheritdoc/>
- public int BufferSize => _error != null ? throw _error : _pepper?.Length ?? 0;
+ public SecretProvider(PluginBase plugin, IConfigScope config)
+ {
+ if(config.TryGetValue("args", out JsonElement el))
+ {
+ //Convert to dict
+ IReadOnlyDictionary<string, JsonElement> hashingArgs = el.EnumerateObject().ToDictionary(static k => k.Name, static v => v.Value);
+
+ //Get hashing arguments
+ uint saltLen = hashingArgs["salt_len"].GetUInt32();
+ uint hashLen = hashingArgs["hash_len"].GetUInt32();
+ uint timeCost = hashingArgs["time_cost"].GetUInt32();
+ uint memoryCost = hashingArgs["memory_cost"].GetUInt32();
+ uint parallelism = hashingArgs["parallelism"].GetUInt32();
+ //Load passwords
+ Passwords = new(this, (int)saltLen, timeCost, memoryCost, parallelism, hashLen);
+ }
+ else
+ {
+ Passwords = new(this);
+ }
+ }
- public ERRNO GetSecret(Span<byte> buffer)
+ public SecretProvider(PluginBase plugin)
{
- if(_error != null)
+ Passwords = new(this);
+ }
+
+
+ public PasswordHashing Passwords { get; }
+
+ ///<inheritdoc/>
+ public int BufferSize
+ {
+ get
{
- throw _error;
+ Check();
+ return _pepper!.Length;
}
+ }
+
+ public ERRNO GetSecret(Span<byte> buffer)
+ {
+ Check();
//Coppy pepper to buffer
_pepper.CopyTo(buffer);
//Return pepper length
return _pepper!.Length;
}
- public void LoadSecret(PluginBase pbase)
+ protected override void Check()
+ {
+ base.Check();
+ if(_error != null)
+ {
+ throw _error;
+ }
+ }
+
+ protected override void Free()
{
- _ = pbase.ObserveTask(() => LoadSecretInternal(pbase));
+ //Clear the pepper if set
+ MemoryUtil.InitializeBlock(_pepper.AsSpan());
}
- private async Task LoadSecretInternal(PluginBase pbase)
+ public async Task ConfigureServiceAsync(PluginBase plugin)
{
try
{
//Get the pepper from secret storage
- _pepper = await pbase.TryGetSecretAsync(PASSWORD_HASHING_KEY).ToBase64Bytes();
-
- //Regsiter cleanup
- _ = pbase.RegisterForUnload(Clear);
+ _pepper = await plugin.TryGetSecretAsync(PASSWORD_HASHING_KEY).ToBase64Bytes();
}
- catch(Exception ex)
+ catch (Exception ex)
{
//Store exception for re-propagation
_error = ex;
- }
- }
- public void Clear()
- {
- //Clear the pepper if set
- MemoryUtil.InitializeBlock(_pepper.AsSpan());
+ //Propagate exception to system
+ throw;
+ }
}
}
}