diff options
17 files changed, 781 insertions, 355 deletions
@@ -360,4 +360,5 @@ MigrationBackup/ .ionide/ # Fody - auto-generated XML schema -FodyWeavers.xsd
\ No newline at end of file +FodyWeavers.xsd +*.licenseheader diff --git a/VNLib.Plugins.Extensions.Data/VNLib.Plugins.Extensions.Data.csproj b/VNLib.Plugins.Extensions.Data/VNLib.Plugins.Extensions.Data.csproj index acca4b3..b229e64 100644 --- a/VNLib.Plugins.Extensions.Data/VNLib.Plugins.Extensions.Data.csproj +++ b/VNLib.Plugins.Extensions.Data/VNLib.Plugins.Extensions.Data.csproj @@ -16,6 +16,8 @@ <PackageProjectUrl>https://www.vaughnnugent.com/resources</PackageProjectUrl> <Version>1.0.1.1</Version> <Nullable>enable</Nullable> + <SignAssembly>True</SignAssembly> + <AssemblyOriginatorKeyFile>\\vaughnnugent.com\Internal\Folder Redirection\vman\Documents\Programming\Software\StrongNameingKey.snk</AssemblyOriginatorKeyFile> </PropertyGroup> <PropertyGroup> diff --git a/VNLib.Plugins.Extensions.Loading.Sql/SqlDbConnectionLoader.cs b/VNLib.Plugins.Extensions.Loading.Sql/SqlDbConnectionLoader.cs index eef79f0..c230cf5 100644 --- a/VNLib.Plugins.Extensions.Loading.Sql/SqlDbConnectionLoader.cs +++ b/VNLib.Plugins.Extensions.Loading.Sql/SqlDbConnectionLoader.cs @@ -25,7 +25,6 @@ using System; using System.Text.Json; using System.Data.Common; -using System.Runtime.CompilerServices; using MySqlConnector; @@ -45,9 +44,6 @@ namespace VNLib.Plugins.Extensions.Loading.Sql { public const string SQL_CONFIG_KEY = "sql"; public const string DB_PASSWORD_KEY = "db_password"; - - private static readonly ConditionalWeakTable<PluginBase, Func<DbConnection>> LazyDbFuncTable = new(); - private static readonly ConditionalWeakTable<PluginBase, DbContextOptions> LazyCtxTable = new(); /// <summary> @@ -61,7 +57,7 @@ namespace VNLib.Plugins.Extensions.Loading.Sql { plugin.ThrowIfUnloaded(); //Get or load - return LazyDbFuncTable.GetValue(plugin, FactoryLoader); + return LoadingExtensions.GetOrCreateSingleton(plugin, FactoryLoader); } private static Func<DbConnection> FactoryLoader(PluginBase plugin) @@ -136,7 +132,7 @@ namespace VNLib.Plugins.Extensions.Loading.Sql public static DbContextOptions GetContextOptions(this PluginBase plugin) { plugin.ThrowIfUnloaded(); - return LazyCtxTable.GetValue(plugin, GetDbOptionsLoader); + return LoadingExtensions.GetOrCreateSingleton(plugin, GetDbOptionsLoader); } private static DbContextOptions GetDbOptionsLoader(PluginBase plugin) diff --git a/VNLib.Plugins.Extensions.Loading.Sql/VNLib.Plugins.Extensions.Loading.Sql.csproj b/VNLib.Plugins.Extensions.Loading.Sql/VNLib.Plugins.Extensions.Loading.Sql.csproj index c6ab306..ea876c1 100644 --- a/VNLib.Plugins.Extensions.Loading.Sql/VNLib.Plugins.Extensions.Loading.Sql.csproj +++ b/VNLib.Plugins.Extensions.Loading.Sql/VNLib.Plugins.Extensions.Loading.Sql.csproj @@ -9,6 +9,8 @@ <PackageProjectUrl>https://www.vaughnnugent.com/resources</PackageProjectUrl> <Version>1.0.1.1</Version> <GenerateDocumentationFile>True</GenerateDocumentationFile> + <SignAssembly>True</SignAssembly> + <AssemblyOriginatorKeyFile>\\vaughnnugent.com\Internal\Folder Redirection\vman\Documents\Programming\Software\StrongNameingKey.snk</AssemblyOriginatorKeyFile> </PropertyGroup> <PropertyGroup> 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(); + } } } diff --git a/VNLib.Plugins.Extensions.VNCache/VNCacheExtensions.cs b/VNLib.Plugins.Extensions.VNCache/VNCacheExtensions.cs new file mode 100644 index 0000000..98898ef --- /dev/null +++ b/VNLib.Plugins.Extensions.VNCache/VNCacheExtensions.cs @@ -0,0 +1,108 @@ +/* +* Copyright (c) 2022 Vaughn Nugent +* +* Library: VNLib +* Package: VNLib.Plugins.Extensions.VNCache +* File: VNCacheExtensions.cs +* +* VNCacheExtensions.cs is part of VNLib.Plugins.Extensions.VNCache which is part of the larger +* VNLib collection of libraries and utilities. +* +* VNLib.Plugins.Extensions.VNCache is free software: you can redistribute it and/or modify +* it under the terms of the GNU Affero General Public License as +* published by the Free Software Foundation, either version 3 of the +* License, or (at your option) any later version. +* +* VNLib.Plugins.Extensions.VNCache is distributed in the hope that it will be useful, +* but WITHOUT ANY WARRANTY; without even the implied warranty of +* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +* GNU Affero General Public License for more details. +* +* You should have received a copy of the GNU Affero General Public License +* along with this program. If not, see https://www.gnu.org/licenses/. +*/ + +using System.Text.Json; + +using VNLib.Utils.Logging; +using VNLib.Data.Caching; +using VNLib.Data.Caching.Extensions; +using VNLib.Plugins.Extensions.Loading; + +namespace VNLib.Plugins.Extensions.VNCache +{ + /// <summary> + /// Contains extension methods for aquiring a Plugin managed + /// global cache provider. + /// </summary> + public static class VNCacheExtensions + { + + /// <summary> + /// Loads the shared cache provider for the current plugin + /// </summary> + /// <param name="pbase"></param> + /// <returns>The shared <see cref="IGlobalCacheProvider"/> </returns> + /// <remarks> + /// The returned instance, background work, logging, and its lifetime + /// are managed by the current plugin. Beware when calling this method + /// network connections may be spawend and managed in the background by + /// this library. + /// </remarks> + public static IGlobalCacheProvider GetGlobalCache(this PluginBase pbase) + => LoadingExtensions.GetOrCreateSingleton(pbase, LoadCacheClient); + + private static IGlobalCacheProvider LoadCacheClient(PluginBase pbase) + { + //pbase.Log.Verbose("Loading global cache provider for {pc}, with {hc}", pbase.GetHashCode(), LoadingExtensions.HashCode); + + //Get config for client + IReadOnlyDictionary<string, JsonElement> config = pbase.GetConfigForType<VnCacheClient>(); + + //Init client + ILogProvider? debugLog = pbase.IsDebug() ? pbase.Log : null; + VnCacheClient client = new(debugLog); + + //Begin cache connections by scheduling a task on the plugin's scheduler + _ = pbase.DeferTask(() => RunClientAsync(pbase, config, client), 250); + + return client; + } + + private static async Task RunClientAsync(PluginBase pbase, IReadOnlyDictionary<string, JsonElement> config, VnCacheClient client) + { + ILogProvider Log = pbase.Log; + + try + { + //Try loading config + await client.LoadConfigAsync(pbase, config); + + Log.Verbose("VNCache client configration loaded successfully"); + + //Run and wait for exit + await client.RunAsync(Log, pbase.UnloadToken); + } + catch (OperationCanceledException) + { } + catch (KeyNotFoundException e) + { + Log.Error("Missing required configuration variable for VnCache client: {0}", e.Message); + } + catch (FBMServerNegiationException fne) + { + Log.Error("Failed to negotiate connection with cache server {reason}", fne.Message); + } + catch (Exception ex) + { + Log.Error(ex, "Cache client error occured in session provider"); + } + finally + { + client.Dispose(); + } + + Log.Information("Cache client exited"); + } + } +} diff --git a/VNLib.Plugins.Extensions.VNCache/VNLib.Plugins.Extensions.VNCache.csproj b/VNLib.Plugins.Extensions.VNCache/VNLib.Plugins.Extensions.VNCache.csproj new file mode 100644 index 0000000..f913366 --- /dev/null +++ b/VNLib.Plugins.Extensions.VNCache/VNLib.Plugins.Extensions.VNCache.csproj @@ -0,0 +1,26 @@ +<Project Sdk="Microsoft.NET.Sdk"> + + <PropertyGroup> + <TargetFramework>net6.0</TargetFramework> + <ImplicitUsings>enable</ImplicitUsings> + <Nullable>enable</Nullable> + <GenerateDocumentationFile>True</GenerateDocumentationFile> + <Version>1.0.1.1</Version> + <Authors>Vaughn Nugent</Authors> + <Copyright>Copyright © 2022 Vaughn Nugent</Copyright> + <PackageProjectUrl>https://www.vaughnnugent.com/resources</PackageProjectUrl> + <CopyLocalLockFileAssemblies>true</CopyLocalLockFileAssemblies> + <SignAssembly>True</SignAssembly> + <AssemblyOriginatorKeyFile>\\vaughnnugent.com\Internal\Folder Redirection\vman\Documents\Programming\Software\StrongNameingKey.snk</AssemblyOriginatorKeyFile> + </PropertyGroup> + + <ItemGroup> + <ProjectReference Include="..\..\..\VNLib\Hashing\src\VNLib.Hashing.Portable.csproj" /> + <ProjectReference Include="..\..\..\VNLib\VNLib.Net.Messaging.FBM\src\VNLib.Net.Messaging.FBM.csproj" /> + <ProjectReference Include="..\..\DataCaching\VNLib.Data.Caching.Extensions\VNLib.Data.Caching.Extensions.csproj" /> + <ProjectReference Include="..\..\DataCaching\VNLib.Data.Caching\VNLib.Data.Caching.csproj" /> + <ProjectReference Include="..\..\PluginBase\VNLib.Plugins.PluginBase.csproj" /> + <ProjectReference Include="..\VNLib.Plugins.Extensions.Loading\VNLib.Plugins.Extensions.Loading.csproj" /> + </ItemGroup> + +</Project> diff --git a/VNLib.Plugins.Extensions.VNCache/VnCacheClient.cs b/VNLib.Plugins.Extensions.VNCache/VnCacheClient.cs new file mode 100644 index 0000000..26e9217 --- /dev/null +++ b/VNLib.Plugins.Extensions.VNCache/VnCacheClient.cs @@ -0,0 +1,229 @@ +/* +* Copyright (c) 2022 Vaughn Nugent +* +* Library: VNLib +* Package: VNLib.Plugins.Extensions.VNCache +* File: VnCacheClient.cs +* +* VnCacheClient.cs is part of VNLib.Plugins.Extensions.VNCache which is part of the larger +* VNLib collection of libraries and utilities. +* +* VNLib.Plugins.Extensions.VNCache is free software: you can redistribute it and/or modify +* it under the terms of the GNU Affero General Public License as +* published by the Free Software Foundation, either version 3 of the +* License, or (at your option) any later version. +* +* VNLib.Plugins.Extensions.VNCache is distributed in the hope that it will be useful, +* but WITHOUT ANY WARRANTY; without even the implied warranty of +* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +* GNU Affero General Public License for more details. +* +* You should have received a copy of the GNU Affero General Public License +* along with this program. If not, see https://www.gnu.org/licenses/. +*/ + +using System.Text.Json; +using System.Net.Sockets; +using System.Net.WebSockets; +using System.Security.Cryptography; + +using VNLib.Utils; +using VNLib.Utils.Memory; +using VNLib.Utils.Logging; +using VNLib.Utils.Extensions; +using VNLib.Hashing.IdentityUtility; +using VNLib.Data.Caching; +using VNLib.Data.Caching.Extensions; +using VNLib.Net.Messaging.FBM.Client; +using VNLib.Plugins.Extensions.Loading; + + +namespace VNLib.Plugins.Extensions.VNCache +{ + /// <summary> + /// A wrapper to simplify a shared global cache client + /// </summary> + [ConfigurationName("vncache")] + internal sealed class VnCacheClient : VnDisposeable, IGlobalCacheProvider + { + FBMClient? _client; + + private TimeSpan RetryInterval; + + private readonly ILogProvider? DebugLog; + private readonly IUnmangedHeap? ClientHeap; + + /// <summary> + /// Initializes an emtpy client wrapper that still requires + /// configuration loading + /// </summary> + /// <param name="debugLog">An optional debugging log</param> + /// <param name="heap">An optional <see cref="IUnmangedHeap"/> for <see cref="FBMClient"/> buffers</param> + public VnCacheClient(ILogProvider? debugLog, IUnmangedHeap? heap = null) + { + DebugLog = debugLog; + //Default to 10 seconds + RetryInterval = TimeSpan.FromSeconds(10); + + ClientHeap = heap; + } + + ///<inheritdoc/> + protected override void Free() + { + _client?.Dispose(); + _client = null; + } + + + /// <summary> + /// Loads required configuration variables from the config store and + /// intializes the interal client + /// </summary> + /// <param name="pbase"></param> + /// <param name="config">A dictionary of configuration varables</param> + /// <exception cref="KeyNotFoundException"></exception> + public async Task LoadConfigAsync(PluginBase pbase, IReadOnlyDictionary<string, JsonElement> config) + { + int maxMessageSize = config["max_message_size"].GetInt32(); + string? brokerAddress = config["broker_address"].GetString() ?? throw new KeyNotFoundException("Missing required configuration variable broker_address"); + + //Get keys async + Task<ReadOnlyJsonWebKey?> clientPrivTask = pbase.TryGetSecretAsync("client_private_key").ToJsonWebKey(); + Task<ReadOnlyJsonWebKey?> brokerPubTask = pbase.TryGetSecretAsync("broker_public_key").ToJsonWebKey(); + Task<ReadOnlyJsonWebKey?> cachePubTask = pbase.TryGetSecretAsync("cache_public_key").ToJsonWebKey(); + + //Wait for all tasks to complete + _ = await Task.WhenAll(clientPrivTask, brokerPubTask, cachePubTask); + + ReadOnlyJsonWebKey clientPriv = await clientPrivTask ?? throw new KeyNotFoundException("Missing required secret client_private_key"); + ReadOnlyJsonWebKey brokerPub = await brokerPubTask ?? throw new KeyNotFoundException("Missing required secret broker_public_key"); + ReadOnlyJsonWebKey cachePub = await cachePubTask ?? throw new KeyNotFoundException("Missing required secret cache_public_key"); + + RetryInterval = config["retry_interval_sec"].GetTimeSpan(TimeParseType.Seconds); + + Uri brokerUri = new(brokerAddress); + + //Init the client with default settings + FBMClientConfig conf = FBMDataCacheExtensions.GetDefaultConfig(ClientHeap ?? Memory.Shared, maxMessageSize, DebugLog); + + _client = new(conf); + + //Add the configuration to the client + _client.GetCacheConfiguration() + .WithBroker(brokerUri) + .WithVerificationKey(cachePub) + .WithSigningCertificate(clientPriv) + .WithBrokerVerificationKey(brokerPub) + .WithTls(brokerUri.Scheme == Uri.UriSchemeHttps); + } + + /// <summary> + /// Discovers nodes in the configured cluster and connects to a random node + /// </summary> + /// <param name="Log">A <see cref="ILogProvider"/> to write log events to</param> + /// <param name="cancellationToken">A token to cancel the operation</param> + /// <returns>A task that completes when the operation has been cancelled or an unrecoverable error occured</returns> + /// <exception cref="InvalidOperationException"></exception> + /// <exception cref="OperationCanceledException"></exception> + public async Task RunAsync(ILogProvider Log, CancellationToken cancellationToken) + { + _ = _client ?? throw new InvalidOperationException("Client configuration not loaded, cannot connect to cache servers"); + + while (true) + { + //Load the server list + ActiveServer[]? servers; + while (true) + { + try + { + Log.Debug("Discovering cluster nodes in broker"); + //Get server list + servers = await _client.DiscoverCacheNodesAsync(cancellationToken); + break; + } + catch (HttpRequestException re) when (re.InnerException is SocketException) + { + Log.Warn("Broker server is unreachable"); + } + catch (Exception ex) + { + Log.Warn("Failed to get server list from broker, reason {r}", ex.Message); + } + + //Gen random ms delay + int randomMsDelay = RandomNumberGenerator.GetInt32(1000, 2000); + await Task.Delay(randomMsDelay, cancellationToken); + } + + if (servers?.Length == 0) + { + Log.Warn("No cluster nodes found, retrying"); + await Task.Delay(RetryInterval, cancellationToken); + continue; + } + + try + { + Log.Debug("Connecting to random cache server"); + + //Connect to a random server + ActiveServer selected = await _client.ConnectToRandomCacheAsync(cancellationToken); + Log.Debug("Connected to cache server {s}", selected.ServerId); + + //Set connection status flag + IsConnected = true; + + //Wait for disconnect + await _client.WaitForExitAsync(cancellationToken); + + Log.Debug("Cache server disconnected"); + } + catch (WebSocketException wse) + { + Log.Warn("Failed to connect to cache server {reason}", wse.Message); + continue; + } + catch (HttpRequestException he) when (he.InnerException is SocketException) + { + Log.Debug("Failed to connect to random cache server server"); + //Continue next loop + continue; + } + finally + { + IsConnected = false; + } + } + } + + + ///<inheritdoc/> + public bool IsConnected { get; private set; } + + ///<inheritdoc/> + public Task<T?> GetAsync<T>(string key, CancellationToken cancellation) + { + return !IsConnected + ? throw new InvalidOperationException("The underlying client is not connected to a cache node") + : _client!.GetObjectAsync<T>(key, cancellation); + } + + ///<inheritdoc/> + Task IGlobalCacheProvider.AddOrUpdateAsync<T>(string key, string? newKey, T value, CancellationToken cancellation) + { + return !IsConnected + ? throw new InvalidOperationException("The underlying client is not connected to a cache node") + : _client!.AddOrUpdateObjectAsync(key, newKey, value, cancellation); + } + + ///<inheritdoc/> + Task IGlobalCacheProvider.DeleteAsync(string key, CancellationToken cancellation) + { + return !IsConnected + ? throw new InvalidOperationException("The underlying client is not connected to a cache node") + : _client!.DeleteObjectAsync(key, cancellation); + } + } +}
\ No newline at end of file diff --git a/VNLib.Plugins.Extensions.Validation/VNLib.Plugins.Extensions.Validation.csproj b/VNLib.Plugins.Extensions.Validation/VNLib.Plugins.Extensions.Validation.csproj index ea34a6c..ef10ecf 100644 --- a/VNLib.Plugins.Extensions.Validation/VNLib.Plugins.Extensions.Validation.csproj +++ b/VNLib.Plugins.Extensions.Validation/VNLib.Plugins.Extensions.Validation.csproj @@ -6,6 +6,8 @@ <Copyright>Copyright © 2022 Vaughn Nugent</Copyright> <PackageProjectUrl>https://www.vaughnnugent.com/resources</PackageProjectUrl> <Version>1.0.1.1</Version> + <SignAssembly>True</SignAssembly> + <AssemblyOriginatorKeyFile>\\vaughnnugent.com\Internal\Folder Redirection\vman\Documents\Programming\Software\StrongNameingKey.snk</AssemblyOriginatorKeyFile> </PropertyGroup> <PropertyGroup> diff --git a/VNLib.Plugins.Extensions.Validation/ValidatorExtensions.cs b/VNLib.Plugins.Extensions.Validation/ValidatorExtensions.cs index e415873..63b4f07 100644 --- a/VNLib.Plugins.Extensions.Validation/ValidatorExtensions.cs +++ b/VNLib.Plugins.Extensions.Validation/ValidatorExtensions.cs @@ -82,12 +82,12 @@ namespace VNLib.Plugins.Extensions.Validation /// <returns></returns> public static IRuleBuilderOptions<T, string> EmptyPhoneNumber<T>(this IRuleBuilder<T, string> builder) { - return builder.Must(static phone => !(phone?.Length).HasValue || PhoneRegex.IsMatch(phone)) + return builder.Must(static phone => phone == null || phone.Length == 0 || PhoneRegex.IsMatch(phone)) .WithMessage("{PropertyValue} is not a valid phone number."); } /// <summary> - /// Checks a string against <see cref="Statics.SpecialCharacters"/>. + /// Checks a string against <see cref="SpecialCharactersRegx"/>. /// If the string is null or empty, it is allowed. /// </summary> /// <typeparam name="T"></typeparam> |