aboutsummaryrefslogtreecommitdiff
path: root/VNLib.Plugins.Extensions.Loading
diff options
context:
space:
mode:
Diffstat (limited to 'VNLib.Plugins.Extensions.Loading')
-rw-r--r--VNLib.Plugins.Extensions.Loading/AssemblyLoader.cs68
-rw-r--r--VNLib.Plugins.Extensions.Loading/Events/EventHandle.cs114
-rw-r--r--VNLib.Plugins.Extensions.Loading/Events/EventManagment.cs56
-rw-r--r--VNLib.Plugins.Extensions.Loading/LoadingExtensions.cs213
-rw-r--r--VNLib.Plugins.Extensions.Loading/RoutingExtensions.cs128
-rw-r--r--VNLib.Plugins.Extensions.Loading/UserLoading.cs69
-rw-r--r--VNLib.Plugins.Extensions.Loading/VNLib.Plugins.Extensions.Loading.csproj4
-rw-r--r--VNLib.Plugins.Extensions.Loading/VaultSecrets.cs100
8 files changed, 406 insertions, 346 deletions
diff --git a/VNLib.Plugins.Extensions.Loading/AssemblyLoader.cs b/VNLib.Plugins.Extensions.Loading/AssemblyLoader.cs
index 5da16ec..a53bb0a 100644
--- a/VNLib.Plugins.Extensions.Loading/AssemblyLoader.cs
+++ b/VNLib.Plugins.Extensions.Loading/AssemblyLoader.cs
@@ -26,9 +26,11 @@ using System;
using System.Linq;
using System.Threading;
using System.Reflection;
+using System.Runtime.Loader;
+using System.Collections.Generic;
using McMaster.NETCore.Plugins;
-using System.Runtime.Loader;
+
using VNLib.Utils.Resources;
namespace VNLib.Plugins.Extensions.Loading
@@ -47,7 +49,6 @@ namespace VNLib.Plugins.Extensions.Loading
public class AssemblyLoader<T> : OpenResourceHandle<T>
{
private readonly PluginLoader _loader;
- private readonly Type _typeInfo;
private readonly CancellationTokenRegistration _reg;
private readonly Lazy<T> _instance;
@@ -58,13 +59,13 @@ namespace VNLib.Plugins.Extensions.Loading
private AssemblyLoader(PluginLoader loader, in CancellationToken unloadToken)
{
- _typeInfo = typeof(T);
_loader = loader;
//Init lazy loader
_instance = new(LoadAndGetExportedType, LazyThreadSafetyMode.PublicationOnly);
//Register dispose
_reg = unloadToken.Register(Dispose);
}
+
/// <summary>
/// Loads the default assembly and gets the expected export type,
/// creates a new instance, and calls its parameterless constructor
@@ -75,17 +76,36 @@ namespace VNLib.Plugins.Extensions.Loading
{
//Load the assembly
Assembly asm = _loader.LoadDefaultAssembly();
-
+
+ Type resourceType = typeof(T);
+
//See if the type is exported
Type exp = (from type in asm.GetExportedTypes()
- where _typeInfo.IsAssignableFrom(type)
+ where resourceType.IsAssignableFrom(type)
select type)
.FirstOrDefault()
- ?? throw new EntryPointNotFoundException($"Imported assembly does not export type {_typeInfo.FullName}");
+ ?? throw new EntryPointNotFoundException($"Imported assembly does not export desired type {resourceType.FullName}");
//Create instance
return (T)Activator.CreateInstance(exp)!;
}
+ /// <summary>
+ /// Creates a method delegate for the given method name from
+ /// the instance wrapped by the current loader
+ /// </summary>
+ /// <typeparam name="TDelegate"></typeparam>
+ /// <param name="methodName">The name of the method to recover</param>
+ /// <returns>The delegate method wrapper if found, null otherwise</returns>
+ /// <exception cref="ArgumentNullException"></exception>
+ /// <exception cref="AmbiguousMatchException"></exception>
+ public TDelegate? TryGetMethod<TDelegate>(string methodName) where TDelegate : Delegate
+ {
+ //get the type info of the actual resource
+ return Resource!.GetType()
+ .GetMethod(methodName, BindingFlags.Public | BindingFlags.Instance)
+ ?.CreateDelegate<TDelegate>(Resource);
+ }
+
///<inheritdoc/>
protected override void Free()
{
@@ -105,18 +125,36 @@ namespace VNLib.Plugins.Extensions.Loading
/// <param name="unloadToken">The plugin unload token</param>
internal static AssemblyLoader<T> Load(string assemblyName, CancellationToken unloadToken)
{
- PluginConfig conf = new(assemblyName)
+ Assembly executingAsm = Assembly.GetExecutingAssembly();
+ AssemblyLoadContext currentCtx = AssemblyLoadContext.GetLoadContext(executingAsm) ?? throw new InvalidOperationException("Could not get default assembly load context");
+
+ List<Type> shared = new ()
{
- IsUnloadable = true,
- EnableHotReload = false,
- PreferSharedTypes = true,
- DefaultContext = AssemblyLoadContext.GetLoadContext(Assembly.GetExecutingAssembly()) ?? AssemblyLoadContext.Default
+ typeof(T),
+ typeof(PluginBase),
};
+
+ //Add all types that have already been loaded
+ shared.AddRange(currentCtx.Assemblies.SelectMany(s => s.GetExportedTypes()));
- PluginLoader loader = new(conf);
-
- //Add the assembly the type originaged from
- conf.SharedAssemblies.Add(typeof(T).Assembly.GetName());
+ PluginLoader loader = PluginLoader.CreateFromAssemblyFile(assemblyName,
+ currentCtx.IsCollectible,
+ shared.ToArray(),
+ conf =>
+ {
+
+ /*
+ * Load context is required to be set to the executing assembly's load context
+ * because it is controlled by the host, so this loader should be considered a
+ * a "child" collection of assemblies
+ */
+ conf.DefaultContext = currentCtx;
+
+ conf.PreferSharedTypes = true;
+
+ //Share utils asm
+ conf.SharedAssemblies.Add(typeof(Utils.Memory.Memory).Assembly.GetName());
+ });
return new(loader, in unloadToken);
}
diff --git a/VNLib.Plugins.Extensions.Loading/Events/EventHandle.cs b/VNLib.Plugins.Extensions.Loading/Events/EventHandle.cs
deleted file mode 100644
index e9f3ff0..0000000
--- a/VNLib.Plugins.Extensions.Loading/Events/EventHandle.cs
+++ /dev/null
@@ -1,114 +0,0 @@
-/*
-* Copyright (c) 2022 Vaughn Nugent
-*
-* Library: VNLib
-* Package: VNLib.Plugins.Extensions.Loading
-* File: EventHandle.cs
-*
-* EventHandle.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 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.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
-* General Public License for more details.
-*
-* You should have received a copy of the GNU General Public License
-* along with VNLib.Plugins.Extensions.Loading. If not, see http://www.gnu.org/licenses/.
-*/
-
-using System;
-using System.Threading;
-using System.Threading.Tasks;
-
-using VNLib.Utils;
-using VNLib.Utils.Extensions;
-using VNLib.Utils.Logging;
-using VNLib.Utils.Resources;
-
-namespace VNLib.Plugins.Extensions.Loading.Events
-{
- /// <summary>
- /// Represents a handle to a scheduled event interval that is managed by the plugin but may be cancled by disposing the instance
- /// </summary>
- public class EventHandle : VnDisposeable
- {
- private readonly PluginBase _pbase;
- private readonly Timer _eventTimer;
- private readonly TimeSpan _interval;
- private readonly AsyncSchedulableCallback _callback;
-
- internal EventHandle(AsyncSchedulableCallback callback, TimeSpan interval, PluginBase pbase)
- {
- _pbase = pbase;
- _interval = interval;
- _callback = callback;
-
- //Init new timer
- _eventTimer = new(OnTimerElapsed, this, interval, interval);
-
- //Register dispose to unload token
- _ = pbase.UnloadToken.RegisterUnobserved(Dispose);
- }
-
- private void OnTimerElapsed(object? state)
- {
- //Run on task scheuler
- _ = Task.Run(RunInterval)
- .ConfigureAwait(false);
- }
-
- private async Task RunInterval()
- {
- try
- {
- await _callback(_pbase.Log, _pbase.UnloadToken);
- }
- catch (OperationCanceledException)
- {
- //unloaded
- _pbase.Log.Verbose("Interval callback canceled due to plugin unload or other event cancellation");
- }
- catch (Exception ex)
- {
- _pbase.Log.Error(ex, "Unhandled exception raised during timer callback");
- }
- }
-
- /// <summary>
- /// Invokes the event handler manually and observes the result.
- /// This method writes execptions to the plugin's default log provider.
- /// </summary>
- /// <returns></returns>
- /// <exception cref="ObjectDisposedException"></exception>
- public Task ManualInvoke()
- {
- Check();
- return Task.Run(RunInterval);
- }
-
-
- /// <summary>
- /// Pauses the event timer until the <see cref="OpenHandle"/> is released or disposed
- /// then resumes to the inital interval period
- /// </summary>
- /// <returns>A <see cref="OpenHandle"/> that restores the timer to its initial state when disposed</returns>
- /// <exception cref="ObjectDisposedException"></exception>
- public OpenHandle Pause()
- {
- Check();
- return _eventTimer.Stop(_interval);
- }
-
- ///<inheritdoc/>
- protected override void Free()
- {
- _eventTimer.Dispose();
- }
- }
-}
diff --git a/VNLib.Plugins.Extensions.Loading/Events/EventManagment.cs b/VNLib.Plugins.Extensions.Loading/Events/EventManagment.cs
index 356cb8b..af55852 100644
--- a/VNLib.Plugins.Extensions.Loading/Events/EventManagment.cs
+++ b/VNLib.Plugins.Extensions.Loading/Events/EventManagment.cs
@@ -50,17 +50,65 @@ namespace VNLib.Plugins.Extensions.Loading.Events
/// <param name="plugin"></param>
/// <param name="asyncCallback">An asyncrhonous callback method.</param>
/// <param name="interval">The event interval</param>
+ /// <param name="immediate">A value that indicates if the callback should be run as soon as possible</param>
/// <returns>An <see cref="EventHandle"/> that can manage the interval state</returns>
/// <exception cref="ObjectDisposedException"></exception>
/// <remarks>If exceptions are raised during callback execution, they are written to the plugin's default log provider</remarks>
- public static EventHandle ScheduleInterval(this PluginBase plugin, AsyncSchedulableCallback asyncCallback, TimeSpan interval)
+ public static void ScheduleInterval(this PluginBase plugin, AsyncSchedulableCallback asyncCallback, TimeSpan interval, bool immediate = false)
{
plugin.ThrowIfUnloaded();
plugin.Log.Verbose("Interval for {t} scheduled", interval);
- //Load new event handler
- return new(asyncCallback, interval, plugin);
+
+ //Run interval on plugins bg scheduler
+ _ = plugin.DeferTask(() => RunIntervalOnPluginScheduler(plugin, asyncCallback, interval, immediate));
}
+
+ private static async Task RunIntervalOnPluginScheduler(PluginBase plugin, AsyncSchedulableCallback callback, TimeSpan interval, bool immediate)
+ {
+
+ static async Task RunCallbackAsync(PluginBase plugin, AsyncSchedulableCallback callback)
+ {
+ try
+ {
+ //invoke interval callback
+ await callback(plugin.Log, plugin.UnloadToken).ConfigureAwait(false);
+ }
+ catch (OperationCanceledException)
+ {
+ //unloaded
+ plugin.Log.Verbose("Interval callback canceled due to plugin unload or other event cancellation");
+ }
+ catch (Exception ex)
+ {
+ plugin.Log.Error(ex, "Unhandled exception raised during timer callback");
+ }
+ }
+
+ //Run callback immediatly if requested
+ if (immediate)
+ {
+ await RunCallbackAsync(plugin, callback);
+ }
+
+ //Timer loop
+ while (true)
+ {
+ try
+ {
+ //await delay and wait for plugin cancellation
+ await Task.Delay(interval, plugin.UnloadToken);
+ }
+ catch (TaskCanceledException)
+ {
+ //Unload token canceled, exit loop
+ break;
+ }
+
+ await RunCallbackAsync(plugin, callback);
+ }
+ }
+
/// <summary>
/// Registers an <see cref="IIntervalScheduleable"/> type's event handler for
/// raising timed interval events
@@ -71,7 +119,7 @@ namespace VNLib.Plugins.Extensions.Loading.Events
/// <returns>An <see cref="EventHandle"/> that can manage the interval state</returns>
/// <exception cref="ObjectDisposedException"></exception>
/// <remarks>If exceptions are raised during callback execution, they are written to the plugin's default log provider</remarks>
- public static EventHandle ScheduleInterval(this PluginBase plugin, IIntervalScheduleable scheduleable, TimeSpan interval) =>
+ public static void ScheduleInterval(this PluginBase plugin, IIntervalScheduleable scheduleable, TimeSpan interval) =>
ScheduleInterval(plugin, scheduleable.OnIntervalAsync, interval);
}
}
diff --git a/VNLib.Plugins.Extensions.Loading/LoadingExtensions.cs b/VNLib.Plugins.Extensions.Loading/LoadingExtensions.cs
index 7c8caee..7e86900 100644
--- a/VNLib.Plugins.Extensions.Loading/LoadingExtensions.cs
+++ b/VNLib.Plugins.Extensions.Loading/LoadingExtensions.cs
@@ -46,10 +46,48 @@ namespace VNLib.Plugins.Extensions.Loading
public const string DEBUG_CONFIG_KEY = "debug";
public const string SECRETS_CONFIG_KEY = "secrets";
public const string PASSWORD_HASHING_KEY = "passwords";
-
- private static readonly ConditionalWeakTable<PluginBase, Lazy<PasswordHashing>> LazyPasswordTable = new();
+ /*
+ * Plugin local cache used for storing singletons for a plugin instance
+ */
+ private static readonly ConditionalWeakTable<PluginBase, PluginLocalCache> _localCache = new();
+
+ /// <summary>
+ /// Gets a previously cached service singleton for the desired plugin
+ /// </summary>
+ /// <param name="serviceType">The service instance type</param>
+ /// <param name="plugin">The plugin to obtain or build the singleton for</param>
+ /// <param name="serviceFactory">The method to produce the singleton</param>
+ /// <returns>The cached or newly created singleton</returns>
+ public static object GetOrCreateSingleton(PluginBase plugin, Type serviceType, Func<PluginBase, object> serviceFactory)
+ {
+ Lazy<object>? service;
+ //Get local cache
+ PluginLocalCache pc = _localCache.GetValue(plugin, PluginLocalCache.Create);
+ //Hold lock while get/set the singleton
+ lock (pc.SyncRoot)
+ {
+ //Check if service already exists
+ service = pc.GetService(serviceType);
+ //publish the service if it isnt loaded yet
+ service ??= pc.AddService(serviceType, serviceFactory);
+ }
+ //Deferred load of the service
+ return service.Value;
+ }
+
+ /// <summary>
+ /// Gets a previously cached service singleton for the desired plugin
+ /// or creates a new singleton instance for the plugin
+ /// </summary>
+ /// <typeparam name="T"></typeparam>
+ /// <param name="plugin">The plugin to obtain or build the singleton for</param>
+ /// <param name="serviceFactory">The method to produce the singleton</param>
+ /// <returns>The cached or newly created singleton</returns>
+ public static T GetOrCreateSingleton<T>(PluginBase plugin, Func<PluginBase, T> serviceFactory)
+ => (T)GetOrCreateSingleton(plugin, typeof(T), p => serviceFactory(p)!);
+
/// <summary>
/// Gets the plugins ambient <see cref="PasswordHashing"/> if loaded, or loads it if required. This class will
/// be unloaded when the plugin us unloaded.
@@ -63,59 +101,52 @@ namespace VNLib.Plugins.Extensions.Loading
{
plugin.ThrowIfUnloaded();
//Get/load the passwords one time only
- return LazyPasswordTable.GetValue(plugin, LoadPasswords).Value;
+ return GetOrCreateSingleton(plugin, LoadPasswords);
}
- private static Lazy<PasswordHashing> LoadPasswords(PluginBase plugin)
+
+ private static PasswordHashing LoadPasswords(PluginBase plugin)
{
- //Lazy load func
- PasswordHashing Load()
+ PasswordHashing Passwords;
+ //Get the global password system secret (pepper)
+ byte[] pepper = plugin.TryGetSecretAsync(PASSWORD_HASHING_KEY)
+ .ToBase64Bytes().Result ?? throw new KeyNotFoundException($"Missing required key '{PASSWORD_HASHING_KEY}' in secrets");
+
+ ERRNO cb(Span<byte> buffer)
+ {
+ //No longer valid peper if plugin is unloaded as its set to zero, so we need to protect it
+ plugin.ThrowIfUnloaded();
+
+ pepper.CopyTo(buffer);
+ return pepper.Length;
+ }
+
+ //See hashing params are defined
+ IReadOnlyDictionary<string, JsonElement>? hashingArgs = plugin.TryGetConfig(PASSWORD_HASHING_KEY);
+ if (hashingArgs != null)
{
- PasswordHashing Passwords;
- //Get the global password system secret (pepper)
- using SecretResult pepperEl = plugin.TryGetSecretAsync(PASSWORD_HASHING_KEY).Result ?? throw new KeyNotFoundException($"Missing required key '{PASSWORD_HASHING_KEY}' in secrets");
-
- byte[] pepper = pepperEl.GetFromBase64();
-
- ERRNO cb(Span<byte> buffer)
- {
- //No longer valid peper if plugin is unloaded as its set to zero, so we need to protect it
- plugin.ThrowIfUnloaded();
-
- pepper.CopyTo(buffer);
- return pepper.Length;
- }
-
- //See hashing params are defined
- IReadOnlyDictionary<string, JsonElement>? hashingArgs = plugin.TryGetConfig(PASSWORD_HASHING_KEY);
- if (hashingArgs is not 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(cb, pepper.Length, (int)saltLen, timeCost, memoryCost, parallelism, hashLen);
- }
- else
- {
- //Init default password hashing
- Passwords = new(cb, pepper.Length);
- }
-
- //Register event to cleanup the password class
- _ = plugin.UnloadToken.RegisterUnobserved(() =>
- {
- //Zero the pepper
- CryptographicOperations.ZeroMemory(pepper);
- LazyPasswordTable.Remove(plugin);
- });
- //return
- return Passwords;
+ //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(cb, pepper.Length, (int)saltLen, timeCost, memoryCost, parallelism, hashLen);
}
- //Return new lazy for
- return new Lazy<PasswordHashing>(Load);
+ else
+ {
+ //Init default password hashing
+ Passwords = new(cb, pepper.Length);
+ }
+
+ //Register event to cleanup the password class
+ _ = plugin.RegisterForUnload(() =>
+ {
+ //Zero the pepper
+ CryptographicOperations.ZeroMemory(pepper);
+ });
+ //return
+ return Passwords;
}
@@ -135,13 +166,15 @@ namespace VNLib.Plugins.Extensions.Loading
{
plugin.ThrowIfUnloaded();
_ = assemblyName ?? throw new ArgumentNullException(nameof(assemblyName));
- //get plugin directory from config
- string? pluginsBaseDir = plugin.GetConfig("plugins")["path"].GetString();
+ //get plugin directory from config
+ IReadOnlyDictionary<string, JsonElement> config = plugin.GetConfig("plugins");
+ string? pluginsBaseDir = 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
- */
+ */
_ = pluginsBaseDir ?? throw new ArgumentNullException("path", "No plugin path is defined for the current host configuration, this is likely a bug");
//Get the first file that matches the search file
@@ -151,6 +184,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
@@ -164,6 +198,7 @@ namespace VNLib.Plugins.Extensions.Loading
//Check for debug element
return plugin.PluginConfig.TryGetProperty(DEBUG_CONFIG_KEY, out JsonElement dbgEl) && dbgEl.GetBoolean();
}
+
/// <summary>
/// Internal exception helper to raise <see cref="ObjectDisposedException"/> if the plugin has been unlaoded
/// </summary>
@@ -214,7 +249,7 @@ namespace VNLib.Plugins.Extensions.Loading
try
{
//Await the task results
- await deferred;
+ await deferred.ConfigureAwait(false);
}
catch(Exception ex)
{
@@ -227,5 +262,73 @@ namespace VNLib.Plugins.Extensions.Loading
plugin.RemoveObservedTask(deferred);
}
}
+
+ /// <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="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)
+ {
+ //Test status
+ pbase.ThrowIfUnloaded();
+ _ = callback ?? throw new ArgumentNullException(nameof(callback));
+
+ //Wait method
+ static async Task WaitForUnload(PluginBase pb, Action callback)
+ {
+ //Wait for unload as a task on the threadpool to avoid deadlocks
+ await pb.UnloadToken.WaitHandle.WaitAsync()
+ .ConfigureAwait(false);
+
+ callback();
+ }
+
+ //Registaer the task to cause the plugin to wait
+ return pbase.DeferTask(() => WaitForUnload(pbase, callback));
+ }
+
+
+ private sealed class PluginLocalCache
+ {
+ private readonly PluginBase _plugin;
+
+ private readonly Dictionary<Type, Lazy<object>> _store;
+
+ public object SyncRoot { get; } = new();
+
+ private PluginLocalCache(PluginBase plugin)
+ {
+ _plugin = plugin;
+ _store = new();
+ //Register cleanup on unload
+ _ = _plugin.RegisterForUnload(() => _store.Clear());
+ }
+
+ public static PluginLocalCache Create(PluginBase plugin) => new(plugin);
+
+
+ public Lazy<object>? GetService(Type serviceType)
+ {
+ Lazy<object>? t = _store.Where(t => t.Key.IsAssignableTo(serviceType))
+ .Select(static tk => tk.Value)
+ .FirstOrDefault();
+ return t;
+ }
+
+ public Lazy<object> AddService(Type serviceType, Func<PluginBase, object> factory)
+ {
+ //Get lazy loader to invoke factory outside of cache lock
+ Lazy<object> lazyFactory = new(() => factory(_plugin), true);
+ //Store lazy factory
+ _store.Add(serviceType, lazyFactory);
+ //Pass the lazy factory back
+ return lazyFactory;
+ }
+ }
}
}
diff --git a/VNLib.Plugins.Extensions.Loading/RoutingExtensions.cs b/VNLib.Plugins.Extensions.Loading/RoutingExtensions.cs
index 0c2c222..9242522 100644
--- a/VNLib.Plugins.Extensions.Loading/RoutingExtensions.cs
+++ b/VNLib.Plugins.Extensions.Loading/RoutingExtensions.cs
@@ -50,48 +50,41 @@ namespace VNLib.Plugins.Extensions.Loading.Routing
public static T Route<T>(this PluginBase plugin, string? pluginConfigPathName) where T : IEndpoint
{
Type endpointType = typeof(T);
- try
+ //If the config attribute is not set, then ignore the config variables
+ if (string.IsNullOrWhiteSpace(pluginConfigPathName))
{
- //If the config attribute is not set, then ignore the config variables
- if (string.IsNullOrWhiteSpace(pluginConfigPathName))
- {
- ConstructorInfo? constructor = endpointType.GetConstructor(new Type[] { typeof(PluginBase) });
- _ = constructor ?? throw new EntryPointNotFoundException($"No constructor found for {endpointType.Name}");
- //Create the new endpoint and pass the plugin instance
- T endpoint = (T)constructor.Invoke(new object[] { plugin });
- //Register event handlers for the endpoint
- ScheduleIntervals(plugin, endpoint, endpointType, null);
- //Route the endpoint
- plugin.Route(endpoint);
-
- //Store ref to plugin for endpoint
- _pluginRefs.Add(endpoint, plugin);
-
- return endpoint;
- }
- else
- {
- ConstructorInfo? constructor = endpointType.GetConstructor(new Type[] { typeof(PluginBase), typeof(IReadOnlyDictionary<string, JsonElement>) });
- //Make sure the constructor exists
- _ = constructor ?? throw new EntryPointNotFoundException($"No constructor found for {endpointType.Name}");
- //Get config variables for the endpoint
- IReadOnlyDictionary<string, JsonElement> conf = plugin.GetConfig(pluginConfigPathName);
- //Create the new endpoint and pass the plugin instance along with the configuration object
- T endpoint = (T)constructor.Invoke(new object[] { plugin, conf });
- //Register event handlers for the endpoint
- ScheduleIntervals(plugin, endpoint, endpointType, conf);
- //Route the endpoint
- plugin.Route(endpoint);
+ ConstructorInfo? constructor = endpointType.GetConstructor(new Type[] { typeof(PluginBase) });
+ _ = constructor ?? throw new EntryPointNotFoundException($"No constructor found for {endpointType.Name}");
+ //Create the new endpoint and pass the plugin instance
+ T endpoint = (T)constructor.Invoke(new object[] { plugin });
+ //Register event handlers for the endpoint
+ ScheduleIntervals(plugin, endpoint, endpointType, null);
+ //Route the endpoint
+ plugin.Route(endpoint);
- //Store ref to plugin for endpoint
- _pluginRefs.Add(endpoint, plugin);
+ //Store ref to plugin for endpoint
+ _pluginRefs.Add(endpoint, plugin);
- return endpoint;
- }
+ return endpoint;
}
- catch (TargetInvocationException te) when (te.InnerException != null)
+ else
{
- throw te.InnerException;
+ ConstructorInfo? constructor = endpointType.GetConstructor(new Type[] { typeof(PluginBase), typeof(IReadOnlyDictionary<string, JsonElement>) });
+ //Make sure the constructor exists
+ _ = constructor ?? throw new EntryPointNotFoundException($"No constructor found for {endpointType.Name}");
+ //Get config variables for the endpoint
+ IReadOnlyDictionary<string, JsonElement> conf = plugin.GetConfig(pluginConfigPathName);
+ //Create the new endpoint and pass the plugin instance along with the configuration object
+ T endpoint = (T)constructor.Invoke(new object[] { plugin, conf });
+ //Register event handlers for the endpoint
+ ScheduleIntervals(plugin, endpoint, endpointType, conf);
+ //Route the endpoint
+ plugin.Route(endpoint);
+
+ //Store ref to plugin for endpoint
+ _pluginRefs.Add(endpoint, plugin);
+
+ return endpoint;
}
}
@@ -124,57 +117,44 @@ namespace VNLib.Plugins.Extensions.Loading.Routing
private static void ScheduleIntervals<T>(PluginBase plugin, T endpointInstance, Type epType, IReadOnlyDictionary<string, JsonElement>? endpointLocalConfig) where T : IEndpoint
{
- List<EventHandle> registered = new();
- try
- {
- //Get all methods that have the configureable async interval attribute specified
- IEnumerable<Tuple<ConfigurableAsyncIntervalAttribute, AsyncSchedulableCallback>> confIntervals = epType.GetMethods()
+ //Get all methods that have the configureable async interval attribute specified
+ IEnumerable<Tuple<ConfigurableAsyncIntervalAttribute, AsyncSchedulableCallback>> confIntervals = epType.GetMethods()
.Where(m => m.GetCustomAttribute<ConfigurableAsyncIntervalAttribute>() != null)
.Select(m => new Tuple<ConfigurableAsyncIntervalAttribute, AsyncSchedulableCallback>
(m.GetCustomAttribute<ConfigurableAsyncIntervalAttribute>()!, m.CreateDelegate<AsyncSchedulableCallback>(endpointInstance)));
- //If the endpoint has a local config, then use it to find the interval
- if (endpointLocalConfig != null)
- {
+ //If the endpoint has a local config, then use it to find the interval
+ if (endpointLocalConfig != null)
+ {
- //Schedule event handlers on the current plugin
- foreach (Tuple<ConfigurableAsyncIntervalAttribute, AsyncSchedulableCallback> interval in confIntervals)
+ //Schedule event handlers on the current plugin
+ foreach (Tuple<ConfigurableAsyncIntervalAttribute, AsyncSchedulableCallback> interval in confIntervals)
+ {
+ int value = endpointLocalConfig[interval.Item1.IntervalPropertyName].GetInt32();
+ //Get the timeout from its resolution variable
+ TimeSpan timeout = interval.Item1.Resolution switch
{
- int value = endpointLocalConfig[interval.Item1.IntervalPropertyName].GetInt32();
- //Get the timeout from its resolution variable
- TimeSpan timeout = interval.Item1.Resolution switch
- {
- IntervalResultionType.Seconds => TimeSpan.FromSeconds(value),
- IntervalResultionType.Minutes => TimeSpan.FromMinutes(value),
- IntervalResultionType.Hours => TimeSpan.FromHours(value),
- _ => TimeSpan.FromMilliseconds(value),
- };
- //Schedule
- registered.Add(plugin.ScheduleInterval(interval.Item2, timeout));
- }
+ IntervalResultionType.Seconds => TimeSpan.FromSeconds(value),
+ IntervalResultionType.Minutes => TimeSpan.FromMinutes(value),
+ IntervalResultionType.Hours => TimeSpan.FromHours(value),
+ _ => TimeSpan.FromMilliseconds(value),
+ };
+ //Schedule
+ plugin.ScheduleInterval(interval.Item2, timeout);
}
+ }
- //Get all methods that have the async interval attribute specified
- IEnumerable<Tuple<AsyncIntervalAttribute, AsyncSchedulableCallback>> intervals = epType.GetMethods()
+ //Get all methods that have the async interval attribute specified
+ IEnumerable<Tuple<AsyncIntervalAttribute, AsyncSchedulableCallback>> intervals = epType.GetMethods()
.Where(m => m.GetCustomAttribute<AsyncIntervalAttribute>() != null)
.Select(m => new Tuple<AsyncIntervalAttribute, AsyncSchedulableCallback>(
m.GetCustomAttribute<AsyncIntervalAttribute>()!, m.CreateDelegate<AsyncSchedulableCallback>(endpointInstance))
);
- //Schedule event handlers on the current plugin
- foreach (Tuple<AsyncIntervalAttribute, AsyncSchedulableCallback> interval in intervals)
- {
- registered.Add(plugin.ScheduleInterval(interval.Item2, interval.Item1.Interval));
- }
- }
- catch
+ //Schedule event handlers on the current plugin
+ foreach (Tuple<AsyncIntervalAttribute, AsyncSchedulableCallback> interval in intervals)
{
- //Stop all event handles
- foreach (EventHandle evh in registered)
- {
- evh.Dispose();
- }
- throw;
+ plugin.ScheduleInterval(interval.Item2, interval.Item1.Interval);
}
}
}
diff --git a/VNLib.Plugins.Extensions.Loading/UserLoading.cs b/VNLib.Plugins.Extensions.Loading/UserLoading.cs
index 3457dc3..da090ec 100644
--- a/VNLib.Plugins.Extensions.Loading/UserLoading.cs
+++ b/VNLib.Plugins.Extensions.Loading/UserLoading.cs
@@ -23,10 +23,7 @@
*/
using System;
-using System.Linq;
-using System.Threading;
using System.Collections.Generic;
-using System.Runtime.CompilerServices;
using VNLib.Utils.Logging;
using VNLib.Utils.Extensions;
@@ -42,8 +39,7 @@ namespace VNLib.Plugins.Extensions.Loading.Users
public const string USER_CUSTOM_ASSEMBLY = "user_custom_asm";
public const string DEFAULT_USER_ASM = "VNLib.Plugins.Essentials.Users.dll";
public const string ONLOAD_METHOD_NAME = "OnPluginLoading";
-
- private static readonly ConditionalWeakTable<PluginBase, Lazy<IUserManager>> UsersTable = new();
+
/// <summary>
/// Gets or loads the plugin's ambient <see cref="IUserManager"/>, with the specified user-table name,
@@ -57,52 +53,41 @@ namespace VNLib.Plugins.Extensions.Loading.Users
{
plugin.ThrowIfUnloaded();
//Get stored or load
- return UsersTable.GetValue(plugin, LoadUsers).Value;
+ return LoadingExtensions.GetOrCreateSingleton(plugin, LoadUsers);
}
- private static Lazy<IUserManager> LoadUsers(PluginBase pbase)
+ private static IUserManager LoadUsers(PluginBase pbase)
{
- //lazy callack
- IUserManager LoadManager()
- {
- //Try to load a custom user assembly for exporting IUserManager
- string? customAsm = pbase.PluginConfig.GetPropString(USER_CUSTOM_ASSEMBLY);
- //See if host config defined the path
- customAsm ??= pbase.HostConfig.GetPropString(USER_CUSTOM_ASSEMBLY);
- //Finally default
- customAsm ??= DEFAULT_USER_ASM;
-
- //Try to load a custom assembly
- AssemblyLoader<IUserManager> loader = pbase.LoadAssembly<IUserManager>(customAsm);
- try
- {
- //Get the runtime type
- Type runtimeType = loader.Resource.GetType();
-
- //Get the onplugin load method
- Action<object>? onLoadMethod = runtimeType.GetMethods()
- .Where(static p => p.IsPublic && !p.IsAbstract && ONLOAD_METHOD_NAME.Equals(p.Name, StringComparison.Ordinal))
- .Select(p => p.CreateDelegate<Action<object>>(loader.Resource))
- .FirstOrDefault();
+ //Try to load a custom user assembly for exporting IUserManager
+ string? customAsm = pbase.PluginConfig.GetPropString(USER_CUSTOM_ASSEMBLY);
+ //See if host config defined the path
+ customAsm ??= pbase.HostConfig.GetPropString(USER_CUSTOM_ASSEMBLY);
+ //Finally default
+ customAsm ??= DEFAULT_USER_ASM;
- //Call the onplugin load method
- onLoadMethod?.Invoke(pbase);
+ //Try to load a custom assembly
+ AssemblyLoader<IUserManager> loader = pbase.LoadAssembly<IUserManager>(customAsm);
+ try
+ {
+ //Try to get the onload method
+ Action<object>? onLoadMethod = loader.TryGetMethod<Action<object>>(ONLOAD_METHOD_NAME);
- if (pbase.IsDebug())
- {
- pbase.Log.Verbose("Loading user manager from assembly {name}", runtimeType.AssemblyQualifiedName);
- }
+ //Call the onplugin load method
+ onLoadMethod?.Invoke(pbase);
- //Return the loaded instance (may raise exception)
- return loader.Resource;
- }
- catch
+ if (pbase.IsDebug())
{
- loader.Dispose();
- throw;
+ pbase.Log.Verbose("Loading user manager from assembly {name}", loader.Resource.GetType().AssemblyQualifiedName);
}
+
+ //Return the loaded instance (may raise exception)
+ return loader.Resource;
+ }
+ catch
+ {
+ loader.Dispose();
+ throw;
}
- return new Lazy<IUserManager>(LoadManager, LazyThreadSafetyMode.PublicationOnly);
}
}
} \ No newline at end of file
diff --git a/VNLib.Plugins.Extensions.Loading/VNLib.Plugins.Extensions.Loading.csproj b/VNLib.Plugins.Extensions.Loading/VNLib.Plugins.Extensions.Loading.csproj
index 43eefc2..15bb15e 100644
--- a/VNLib.Plugins.Extensions.Loading/VNLib.Plugins.Extensions.Loading.csproj
+++ b/VNLib.Plugins.Extensions.Loading/VNLib.Plugins.Extensions.Loading.csproj
@@ -8,6 +8,8 @@
<GenerateDocumentationFile>True</GenerateDocumentationFile>
<Nullable>enable</Nullable>
+ <SignAssembly>True</SignAssembly>
+ <AssemblyOriginatorKeyFile>\\vaughnnugent.com\Internal\Folder Redirection\vman\Documents\Programming\Software\StrongNameingKey.snk</AssemblyOriginatorKeyFile>
</PropertyGroup>
<PropertyGroup>
@@ -30,7 +32,7 @@
</ItemGroup>
<ItemGroup>
- <ProjectReference Include="..\..\..\VNLib\Essentials\VNLib.Plugins.Essentials.csproj" />
+ <ProjectReference Include="..\..\..\VNLib\Essentials\src\VNLib.Plugins.Essentials.csproj" />
<ProjectReference Include="..\..\PluginBase\VNLib.Plugins.PluginBase.csproj" />
</ItemGroup>
diff --git a/VNLib.Plugins.Extensions.Loading/VaultSecrets.cs b/VNLib.Plugins.Extensions.Loading/VaultSecrets.cs
index 468600f..cd67903 100644
--- a/VNLib.Plugins.Extensions.Loading/VaultSecrets.cs
+++ b/VNLib.Plugins.Extensions.Loading/VaultSecrets.cs
@@ -26,10 +26,8 @@ using System;
using System.Linq;
using System.Text;
using System.Text.Json;
-using System.Threading;
using System.Threading.Tasks;
using System.Collections.Generic;
-using System.Runtime.CompilerServices;
using System.Security.Cryptography.X509Certificates;
using VaultSharp;
@@ -64,8 +62,6 @@ namespace VNLib.Plugins.Extensions.Loading
public const string VAULT_URL_SCHEME = "vault://";
- private static readonly ConditionalWeakTable<PluginBase, Lazy<IVaultClient?>> _vaults = new();
-
/// <summary>
/// <para>
/// Gets a secret from the "secrets" element.
@@ -135,7 +131,7 @@ namespace VNLib.Plugins.Extensions.Loading
async Task<SecretResult?> execute()
{
//Try load client
- IVaultClient? client = _vaults.GetValue(plugin, TryGetVaultLoader).Value;
+ IVaultClient? client = plugin.GetVault();
_ = client ?? throw new KeyNotFoundException("Vault client not found");
//run read async
@@ -209,7 +205,7 @@ namespace VNLib.Plugins.Extensions.Loading
async Task<X509Certificate?> execute()
{
//Try load client
- IVaultClient? client = _vaults.GetValue(plugin, TryGetVaultLoader).Value;
+ IVaultClient? client = plugin.GetVault();
_ = client ?? throw new KeyNotFoundException("Vault client not found");
@@ -239,7 +235,7 @@ namespace VNLib.Plugins.Extensions.Loading
/// <returns>The ambient <see cref="IVaultClient"/> if loaded, null otherwise</returns>
/// <exception cref="KeyNotFoundException"></exception>
/// <exception cref="ObjectDisposedException"></exception>
- public static IVaultClient? GetVault(this PluginBase plugin) => _vaults.GetValue(plugin, TryGetVaultLoader).Value;
+ public static IVaultClient? GetVault(this PluginBase plugin) => LoadingExtensions.GetOrCreateSingleton(plugin, TryGetVaultLoader);
private static string? TryGetSecretInternal(PluginBase plugin, string secretName)
{
@@ -283,47 +279,41 @@ namespace VNLib.Plugins.Extensions.Loading
return conf != null && conf.TryGetValue(secretName, out JsonElement el) ? el.GetString() : null;
}
- private static Lazy<IVaultClient?> TryGetVaultLoader(PluginBase pbase)
+ private static IVaultClient? TryGetVaultLoader(PluginBase pbase)
{
- //Local func to load the vault client
- IVaultClient? LoadVault()
+ //Get vault config
+ IReadOnlyDictionary<string, JsonElement>? conf = pbase.TryGetConfig(VAULT_OBJECT_NAME);
+
+ if (conf == null)
{
- //Get vault config
- IReadOnlyDictionary<string, JsonElement>? conf = pbase.TryGetConfig(VAULT_OBJECT_NAME);
+ return null;
+ }
- if(conf == null)
- {
- return null;
- }
+ //try get servre address creds from config
+ string? serverAddress = conf[VAULT_URL_KEY].GetString() ?? throw new KeyNotFoundException($"Failed to load the key {VAULT_URL_KEY} from object {VAULT_OBJECT_NAME}");
- //try get servre address creds from config
- string? serverAddress = conf[VAULT_URL_KEY].GetString() ?? throw new KeyNotFoundException($"Failed to load the key {VAULT_URL_KEY} from object {VAULT_OBJECT_NAME}");
+ IAuthMethodInfo authMethod;
- IAuthMethodInfo authMethod;
+ //Get authentication method from config
+ if (conf.TryGetValue(VAULT_TOKEN_KEY, out JsonElement tokenEl))
+ {
+ //Init token
+ authMethod = new TokenAuthMethodInfo(tokenEl.GetString());
+ }
+ else if (conf.TryGetValue(VAULT_ROLE_KEY, out JsonElement roleEl) && conf.TryGetValue(VAULT_SECRET_KEY, out JsonElement secretEl))
+ {
+ authMethod = new AppRoleAuthMethodInfo(roleEl.GetString(), secretEl.GetString());
+ }
+ else
+ {
+ throw new KeyNotFoundException($"Failed to load the vault authentication method from {VAULT_OBJECT_NAME}");
+ }
- //Get authentication method from config
- if (conf.TryGetValue(VAULT_TOKEN_KEY, out JsonElement tokenEl))
- {
- //Init token
- authMethod = new TokenAuthMethodInfo(tokenEl.GetString());
- }
- else if(conf.TryGetValue(VAULT_ROLE_KEY, out JsonElement roleEl) && conf.TryGetValue(VAULT_SECRET_KEY, out JsonElement secretEl))
- {
- authMethod = new AppRoleAuthMethodInfo(roleEl.GetString(), secretEl.GetString());
- }
- else
- {
- throw new KeyNotFoundException($"Failed to load the vault authentication method from {VAULT_OBJECT_NAME}");
- }
+ //Settings
+ VaultClientSettings settings = new(serverAddress, authMethod);
- //Settings
- VaultClientSettings settings = new(serverAddress, authMethod);
-
- //create vault client
- return new VaultClient(settings);
- }
- //init lazy
- return new (LoadVault, LazyThreadSafetyMode.PublicationOnly);
+ //create vault client
+ return new VaultClient(settings);
}
/// <summary>
@@ -355,6 +345,20 @@ namespace VNLib.Plugins.Extensions.Loading
}
/// <summary>
+ /// Converts the secret recovery task to
+ /// </summary>
+ /// <param name="secret"></param>
+ /// <returns>A task whos result the base64 decoded secret as a byte[]</returns>
+ /// <exception cref="ArgumentNullException"></exception>
+ /// <exception cref="InternalBufferTooSmallException"></exception>
+ public static async Task<byte[]?> ToBase64Bytes(this Task<SecretResult?> secret)
+ {
+ _ = secret ?? throw new ArgumentNullException(nameof(secret));
+ using SecretResult? sec = await secret.ConfigureAwait(false);
+ return sec?.GetFromBase64();
+ }
+
+ /// <summary>
/// Recovers a certificate from a PEM encoded secret
/// </summary>
/// <param name="secret"></param>
@@ -431,5 +435,19 @@ namespace VNLib.Plugins.Extensions.Loading
int count = Encoding.UTF8.GetBytes(secret.Result, buffer.Span);
return new ReadOnlyJsonWebKey(buffer.Span[..count]);
}
+
+ /// <summary>
+ /// Gets a task that resolves a <see cref="ReadOnlyJsonWebKey"/>
+ /// from a <see cref="SecretResult"/> task
+ /// </summary>
+ /// <param name="secret"></param>
+ /// <returns>The <see cref="ReadOnlyJsonWebKey"/> from the secret, or null if the secret was not found</returns>
+ /// <exception cref="ArgumentNullException"></exception>
+ public static async Task<ReadOnlyJsonWebKey?> ToJsonWebKey(this Task<SecretResult?> secret)
+ {
+ _ = secret ?? throw new ArgumentNullException(nameof(secret));
+ using SecretResult? sec = await secret.ConfigureAwait(false);
+ return sec?.GetJsonWebKey();
+ }
}
}