diff options
Diffstat (limited to 'VNLib.Plugins.Extensions.Loading')
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(); + } } } |