From ec99d0c948733ea379065e0ae37ab7702a1e4727 Mon Sep 17 00:00:00 2001 From: vnugent Date: Thu, 9 Mar 2023 01:48:39 -0500 Subject: Omega cache, session, and account provider complete overhaul --- .../src/Storage/LWStorageContext.cs | 9 +- .../src/Storage/LWStorageDescriptor.cs | 9 +- .../src/Storage/LWStorageManager.cs | 5 +- .../src/VNLib.Plugins.Extensions.Data.csproj | 17 +- .../src/SqlDbConnectionLoader.cs | 5 +- .../VNLib.Plugins.Extensions.Loading.Sql.csproj | 19 +- .../src/AssemblyLoader.cs | 103 +++--- .../src/ConfigrationValidationException.cs | 42 +++ .../src/Configuration/ConfigScope.cs | 83 +++++ .../src/Configuration/IAsyncConfigurable.cs | 42 +++ .../src/Configuration/IConfigScope.cs | 47 +++ .../src/Configuration/IOnConfigValidation.cs | 38 +++ .../src/ConfigurationExtensions.cs | 237 ++++++++++--- .../src/Events/AsyncIntervalAttribute.cs | 44 ++- .../src/Events/EventManagment.cs | 4 +- .../src/LoadingExtensions.cs | 374 +++++++++++++++++---- .../src/RoutingExtensions.cs | 17 +- .../src/S3Config.cs | 13 +- .../src/UserLoading.cs | 93 ----- .../src/UserManager.cs | 138 ++++++++ .../src/VNLib.Plugins.Extensions.Loading.csproj | 23 +- .../src/VaultSecrets.cs | 4 +- .../src/VNLib.Plugins.Extensions.Validation.csproj | 16 +- 23 files changed, 1085 insertions(+), 297 deletions(-) create mode 100644 lib/VNLib.Plugins.Extensions.Loading/src/ConfigrationValidationException.cs create mode 100644 lib/VNLib.Plugins.Extensions.Loading/src/Configuration/ConfigScope.cs create mode 100644 lib/VNLib.Plugins.Extensions.Loading/src/Configuration/IAsyncConfigurable.cs create mode 100644 lib/VNLib.Plugins.Extensions.Loading/src/Configuration/IConfigScope.cs create mode 100644 lib/VNLib.Plugins.Extensions.Loading/src/Configuration/IOnConfigValidation.cs delete mode 100644 lib/VNLib.Plugins.Extensions.Loading/src/UserLoading.cs create mode 100644 lib/VNLib.Plugins.Extensions.Loading/src/UserManager.cs diff --git a/lib/VNLib.Plugins.Extensions.Data/src/Storage/LWStorageContext.cs b/lib/VNLib.Plugins.Extensions.Data/src/Storage/LWStorageContext.cs index dcde80a..fabd54e 100644 --- a/lib/VNLib.Plugins.Extensions.Data/src/Storage/LWStorageContext.cs +++ b/lib/VNLib.Plugins.Extensions.Data/src/Storage/LWStorageContext.cs @@ -1,5 +1,5 @@ /* -* Copyright (c) 2022 Vaughn Nugent +* Copyright (c) 2023 Vaughn Nugent * * Library: VNLib * Package: VNLib.Plugins.Extensions.Data @@ -22,6 +22,8 @@ * along with this program. If not, see https://www.gnu.org/licenses/. */ +using System; + using Microsoft.EntityFrameworkCore; namespace VNLib.Plugins.Extensions.Data.Storage @@ -40,9 +42,8 @@ namespace VNLib.Plugins.Extensions.Data.Storage protected override void OnModelCreating(ModelBuilder modelBuilder) { - //Set table name - modelBuilder.Entity() - .ToTable(TableName); + if(TableName != null) + throw new NotImplementedException("Table/relational package requires development not yet implemented"); } } } \ No newline at end of file diff --git a/lib/VNLib.Plugins.Extensions.Data/src/Storage/LWStorageDescriptor.cs b/lib/VNLib.Plugins.Extensions.Data/src/Storage/LWStorageDescriptor.cs index 939d3e3..cc9bd46 100644 --- a/lib/VNLib.Plugins.Extensions.Data/src/Storage/LWStorageDescriptor.cs +++ b/lib/VNLib.Plugins.Extensions.Data/src/Storage/LWStorageDescriptor.cs @@ -1,5 +1,5 @@ /* -* Copyright (c) 2022 Vaughn Nugent +* Copyright (c) 2023 Vaughn Nugent * * Library: VNLib * Package: VNLib.Plugins.Extensions.Data @@ -33,7 +33,6 @@ using System.Text.Json.Serialization; using VNLib.Utils; using VNLib.Utils.Async; -using VNLib.Utils.Extensions; using VNLib.Utils.Memory; namespace VNLib.Plugins.Extensions.Data.Storage @@ -108,7 +107,7 @@ namespace VNLib.Plugins.Extensions.Data.Storage //Decode and deserialize the data return BrotliDecoder.TryDecompress(Entry.Data, decodeBuffer, out int written) - ? JsonSerializer.Deserialize>(Entry.Data, SerializerOptions) ?? new(StringComparer.OrdinalIgnoreCase) + ? JsonSerializer.Deserialize>(decodeBuffer.Span[..written], SerializerOptions) ?? new(StringComparer.OrdinalIgnoreCase) : throw new InvalidDataException("Failed to decompress data"); } } @@ -122,7 +121,7 @@ namespace VNLib.Plugins.Extensions.Data.Storage { Check(); //De-serialize and return object - return StringStorage.Value.TryGetValue(key, out string? val) ? val.AsJsonObject(SerializerOptions) : default; + return StringStorage.Value.TryGetValue(key, out string? val) ? JsonSerializer.Deserialize(val, SerializerOptions) : default; } /// @@ -138,7 +137,7 @@ namespace VNLib.Plugins.Extensions.Data.Storage else { //Serialize the object to a string - string value = obj.ToJsonString(SerializerOptions)!; + string value = JsonSerializer.Serialize(obj, SerializerOptions); //Attempt to store string in storage SetStringValue(key, value); } diff --git a/lib/VNLib.Plugins.Extensions.Data/src/Storage/LWStorageManager.cs b/lib/VNLib.Plugins.Extensions.Data/src/Storage/LWStorageManager.cs index 8a34ec4..eabe618 100644 --- a/lib/VNLib.Plugins.Extensions.Data/src/Storage/LWStorageManager.cs +++ b/lib/VNLib.Plugins.Extensions.Data/src/Storage/LWStorageManager.cs @@ -1,5 +1,5 @@ /* -* Copyright (c) 2022 Vaughn Nugent +* Copyright (c) 2023 Vaughn Nugent * * Library: VNLib * Package: VNLib.Plugins.Extensions.Data @@ -272,8 +272,9 @@ namespace VNLib.Plugins.Extensions.Data.Storage { throw new InvalidDataException("Failed to compress the descriptor data"); } + //Set the data - entry.Data = encBuffer.Span.ToArray(); + entry.Data = encBuffer.Span[..compressed].ToArray(); } //Update modified time entry.LastModified = DateTime.UtcNow; diff --git a/lib/VNLib.Plugins.Extensions.Data/src/VNLib.Plugins.Extensions.Data.csproj b/lib/VNLib.Plugins.Extensions.Data/src/VNLib.Plugins.Extensions.Data.csproj index 67310b6..0616048 100644 --- a/lib/VNLib.Plugins.Extensions.Data/src/VNLib.Plugins.Extensions.Data.csproj +++ b/lib/VNLib.Plugins.Extensions.Data/src/VNLib.Plugins.Extensions.Data.csproj @@ -4,20 +4,24 @@ net6.0 VNLib.Plugins.Extensions.Data VNLib.Plugins.Extensions.Data - 1.0.1.1 enable - True True - \\vaughnnugent.com\Internal\Folder Redirection\vman\Documents\Programming\Software\StrongNameingKey.snk latest-all Vaughn Nugent - Data extensions for VNLib Plugins + Vaughn Nugent + VNLib.Plugins.Extensions.Data + VNLib.Plugins.Extensions.Data + + An Essentials framework extension library that provides data structures for abstractions between data and + databases, using the EntityFrameworkCore library and design patterns. + Copyright © 2023 Vaughn Nugent - https://www.vaughnnugent.com/resources + https://www.vaughnnugent.com/resources/software/modules/VNLib.Plugins.Extensions + https://github.com/VnUgE/VNLib.Plugins.Extensions/tree/master/lib/VNLib.Plugins.Extensions.Data @@ -37,8 +41,7 @@ all runtime; build; native; contentfiles; analyzers; buildtransitive - - + diff --git a/lib/VNLib.Plugins.Extensions.Loading.Sql/src/SqlDbConnectionLoader.cs b/lib/VNLib.Plugins.Extensions.Loading.Sql/src/SqlDbConnectionLoader.cs index 9d3d2e0..c2d7726 100644 --- a/lib/VNLib.Plugins.Extensions.Loading.Sql/src/SqlDbConnectionLoader.cs +++ b/lib/VNLib.Plugins.Extensions.Loading.Sql/src/SqlDbConnectionLoader.cs @@ -1,5 +1,5 @@ /* -* Copyright (c) 2022 Vaughn Nugent +* Copyright (c) 2023 Vaughn Nugent * * Library: VNLib * Package: VNLib.Plugins.Extensions.Loading.Sql @@ -23,7 +23,6 @@ */ using System; -using System.Text.Json; using System.Data.Common; using System.Collections.Generic; @@ -63,7 +62,7 @@ namespace VNLib.Plugins.Extensions.Loading.Sql private static Func FactoryLoader(PluginBase plugin) { - IReadOnlyDictionary sqlConf = plugin.GetConfig(SQL_CONFIG_KEY); + IConfigScope sqlConf = plugin.GetConfig(SQL_CONFIG_KEY); //Get the db-type string? type = sqlConf.GetPropString("db_type"); diff --git a/lib/VNLib.Plugins.Extensions.Loading.Sql/src/VNLib.Plugins.Extensions.Loading.Sql.csproj b/lib/VNLib.Plugins.Extensions.Loading.Sql/src/VNLib.Plugins.Extensions.Loading.Sql.csproj index f944cd6..3d61b67 100644 --- a/lib/VNLib.Plugins.Extensions.Loading.Sql/src/VNLib.Plugins.Extensions.Loading.Sql.csproj +++ b/lib/VNLib.Plugins.Extensions.Loading.Sql/src/VNLib.Plugins.Extensions.Loading.Sql.csproj @@ -4,18 +4,23 @@ net6.0 VNLib.Plugins.Extensions.Loading.Sql VNLib.Plugins.Extensions.Loading.Sql - 1.0.1.1 enable - True - \\vaughnnugent.com\Internal\Folder Redirection\vman\Documents\Programming\Software\StrongNameingKey.snk latest-all True - + Vaughn Nugent + Vaughn Nugent + VNLib.Plugins.Extensions.Loading.Sql + VVNLib.Plugins.Extensions.Loading.Sql + + An Essentials framework extension library for configuring database connections using the EntityFrameworkCore + patterns and abstractions. Supports MSSql, MySql and SqlLite database connections via plugin configuration. + Copyright © 2023 Vaughn Nugent - https://www.vaughnnugent.com/resources/software + https://www.vaughnnugent.com/resources/software/modules/VNLib.Plugins.Extensions + https://github.com/VnUgE/VNLib.Plugins.Extensions/tree/master/lib/VNLib.Plugins.Extensions.Loading.Sql @@ -28,8 +33,8 @@ - - + + diff --git a/lib/VNLib.Plugins.Extensions.Loading/src/AssemblyLoader.cs b/lib/VNLib.Plugins.Extensions.Loading/src/AssemblyLoader.cs index 2827587..dcc5f59 100644 --- a/lib/VNLib.Plugins.Extensions.Loading/src/AssemblyLoader.cs +++ b/lib/VNLib.Plugins.Extensions.Loading/src/AssemblyLoader.cs @@ -1,5 +1,5 @@ /* -* Copyright (c) 2022 Vaughn Nugent +* Copyright (c) 2023 Vaughn Nugent * * Library: VNLib * Package: VNLib.Plugins.Extensions.Loading @@ -23,14 +23,14 @@ */ using System; +using System.IO; using System.Linq; using System.Threading; using System.Reflection; using System.Runtime.Loader; -using System.Collections.Generic; - -using McMaster.NETCore.Plugins; +using System.Runtime.InteropServices; +using VNLib.Utils.IO; using VNLib.Utils.Resources; namespace VNLib.Plugins.Extensions.Loading @@ -48,24 +48,55 @@ namespace VNLib.Plugins.Extensions.Loading /// The exported type to manage public sealed class AssemblyLoader : OpenResourceHandle { - private readonly PluginLoader _loader; private readonly CancellationTokenRegistration _reg; private readonly Lazy _instance; + private readonly AssemblyLoadContext _loadContext; + private readonly AssemblyDependencyResolver _resolver; + private readonly string _assemblyPath; /// /// The instance of the loaded type /// public override T Resource => _instance.Value; - private AssemblyLoader(PluginLoader loader, in CancellationToken unloadToken) + private AssemblyLoader(string assemblyPath, AssemblyLoadContext parentContext, CancellationToken unloadToken) { - _loader = loader; + _loadContext = parentContext; + _resolver = new(assemblyPath); + _assemblyPath = assemblyPath; + + //Add resolver for context + parentContext.Resolving += OnDependencyResolving; + parentContext.ResolvingUnmanagedDll += OnNativeLibraryResolving; + //Init lazy loader _instance = new(LoadAndGetExportedType, LazyThreadSafetyMode.PublicationOnly); //Register dispose _reg = unloadToken.Register(Dispose); } - + + /* + * Resolves a native libary isolated to the requested assembly, which + * should be isolated to this assembly or one of its dependencies. + */ + private IntPtr OnNativeLibraryResolving(Assembly assembly, string libname) + { + //Resolve the desired asm dependency for the current context + string? requestedDll = _resolver.ResolveUnmanagedDllToPath(libname); + + //if the dep is resolved, seach in the assembly directory for the manageed dll only + return requestedDll == null ? IntPtr.Zero : NativeLibrary.Load(requestedDll, assembly, DllImportSearchPath.AssemblyDirectory); + } + + private Assembly? OnDependencyResolving(AssemblyLoadContext context, AssemblyName asmName) + { + //Resolve the desired asm dependency for the current context + string? desiredAsm = _resolver.ResolveAssemblyToPath(asmName); + + //If the asm exists in the dir, load it + return desiredAsm == null ? null : _loadContext.LoadFromAssemblyPath(desiredAsm); + } + /// /// Loads the default assembly and gets the expected export type, /// creates a new instance, and calls its parameterless constructor @@ -74,8 +105,8 @@ namespace VNLib.Plugins.Extensions.Loading /// private T LoadAndGetExportedType() { - //Load the assembly - Assembly asm = _loader.LoadDefaultAssembly(); + //Load the assembly into the parent context + Assembly asm = _loadContext.LoadFromAssemblyPath(_assemblyPath); Type resourceType = typeof(T); @@ -109,54 +140,44 @@ namespace VNLib.Plugins.Extensions.Loading /// protected override void Free() { + //Remove resolving event handlers + _loadContext.Resolving -= OnDependencyResolving; + _loadContext.ResolvingUnmanagedDll -= OnNativeLibraryResolving; + //If the instance is disposable, call its dispose method on unload if (_instance.IsValueCreated && _instance.Value is IDisposable) { (_instance.Value as IDisposable)?.Dispose(); } - _loader.Dispose(); _reg.Dispose(); } /// - /// Creates a new assembly loader for the specified type and + /// Creates a new loader for the desired assembly. The assembly and its dependencies + /// will be loaded into the parent context, meaning all loaded types belong to the + /// current which belongs the current plugin instance. /// /// The name of the assmbly within the current plugin directory /// The plugin unload token + /// internal static AssemblyLoader Load(string assemblyName, CancellationToken unloadToken) { - Assembly executingAsm = Assembly.GetExecutingAssembly(); - AssemblyLoadContext currentCtx = AssemblyLoadContext.GetLoadContext(executingAsm) ?? throw new InvalidOperationException("Could not get default assembly load context"); - - List shared = new () + //Make sure the file exists + if (!FileOperations.FileExists(assemblyName)) { - typeof(T), - typeof(PluginBase), - }; + throw new FileNotFoundException($"The desired assembly {assemblyName} could not be found at the file path"); + } - //Share all VNLib internal libraries - shared.AddRange(currentCtx.Assemblies.Where(static s => s.FullName.Contains("VNLib", StringComparison.OrdinalIgnoreCase)).SelectMany(static s => s.GetExportedTypes())); + /* + * Dynamic assemblies are loaded directly to the exe assembly context. + * This should always be the plugin isolated context. + */ - 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.MemoryUtil).Assembly.GetName()); - }); - - return new(loader, in unloadToken); + Assembly executingAsm = Assembly.GetExecutingAssembly(); + AssemblyLoadContext currentCtx = AssemblyLoadContext.GetLoadContext(executingAsm) ?? throw new InvalidOperationException("Could not get default assembly load context"); + + return new(assemblyName, currentCtx, unloadToken); } + } } diff --git a/lib/VNLib.Plugins.Extensions.Loading/src/ConfigrationValidationException.cs b/lib/VNLib.Plugins.Extensions.Loading/src/ConfigrationValidationException.cs new file mode 100644 index 0000000..83ce558 --- /dev/null +++ b/lib/VNLib.Plugins.Extensions.Loading/src/ConfigrationValidationException.cs @@ -0,0 +1,42 @@ +/* +* Copyright (c) 2023 Vaughn Nugent +* +* Library: VNLib +* Package: VNLib.Plugins.Extensions.Loading +* File: ConfigrationValidationException.cs +* +* ConfigrationValidationException.cs is part of VNLib.Plugins.Extensions.Loading which is part of the larger +* VNLib collection of libraries and utilities. +* +* VNLib.Plugins.Extensions.Loading is free software: you can redistribute it and/or modify +* it under the terms of the GNU Affero General Public License as +* published by the Free Software Foundation, either version 3 of the +* License, or (at your option) any later version. +* +* VNLib.Plugins.Extensions.Loading is distributed in the hope that it will be useful, +* but WITHOUT ANY WARRANTY; without even the implied warranty of +* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +* GNU Affero General Public License for more details. +* +* You should have received a copy of the GNU Affero General Public License +* along with this program. If not, see https://www.gnu.org/licenses/. +*/ + +using System; + +namespace VNLib.Plugins.Extensions.Loading +{ + /// + /// An exception raised when a configuration validation exception has occured + /// + public class ConfigrationValidationException : Exception + { + public ConfigrationValidationException(string message) : base(message) + {} + + public ConfigrationValidationException(string message, Exception innerException) : base(message, innerException) + {} + public ConfigrationValidationException() + {} + } +} diff --git a/lib/VNLib.Plugins.Extensions.Loading/src/Configuration/ConfigScope.cs b/lib/VNLib.Plugins.Extensions.Loading/src/Configuration/ConfigScope.cs new file mode 100644 index 0000000..7f5c09c --- /dev/null +++ b/lib/VNLib.Plugins.Extensions.Loading/src/Configuration/ConfigScope.cs @@ -0,0 +1,83 @@ +/* +* Copyright (c) 2023 Vaughn Nugent +* +* Library: VNLib +* Package: VNLib.Plugins.Extensions.Loading +* File: ConfigScope.cs +* +* ConfigScope.cs is part of VNLib.Plugins.Extensions.Loading which is part of the larger +* VNLib collection of libraries and utilities. +* +* VNLib.Plugins.Extensions.Loading is free software: you can redistribute it and/or modify +* it under the terms of the GNU Affero General Public License as +* published by the Free Software Foundation, either version 3 of the +* License, or (at your option) any later version. +* +* VNLib.Plugins.Extensions.Loading is distributed in the hope that it will be useful, +* but WITHOUT ANY WARRANTY; without even the implied warranty of +* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +* GNU Affero General Public License for more details. +* +* You should have received a copy of the GNU Affero General Public License +* along with this program. If not, see https://www.gnu.org/licenses/. +*/ + +using System; +using System.Linq; +using System.Text.Json; +using System.Collections; +using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; + + +namespace VNLib.Plugins.Extensions.Loading +{ + internal sealed class ConfigScope: IConfigScope + { + + private readonly Lazy> _config; + + private readonly JsonElement _element; + + internal ConfigScope(JsonElement element, string scopeName) + { + _element = element; + ScopeName = scopeName; + _config = new(LoadTable); + } + + private IReadOnlyDictionary LoadTable() + { + return _element.EnumerateObject().ToDictionary(static k => k.Name, static k => k.Value); + } + + /// + public JsonElement this[string key] => _config.Value[key]; + + /// + public IEnumerable Keys => _config.Value.Keys; + + /// + public IEnumerable Values => _config.Value.Values; + + /// + public int Count => _config.Value.Count; + + /// + public string ScopeName { get; } + + /// + public bool ContainsKey(string key) => _config.Value.ContainsKey(key); + + /// + public T Deserialze() => _element.Deserialize()!; + + /// + public IEnumerator> GetEnumerator() => _config.Value.GetEnumerator(); + + /// + public bool TryGetValue(string key, [MaybeNullWhen(false)] out JsonElement value) => _config.Value.TryGetValue(key, out value); + + IEnumerator IEnumerable.GetEnumerator() => _config.Value.GetEnumerator(); + } +} diff --git a/lib/VNLib.Plugins.Extensions.Loading/src/Configuration/IAsyncConfigurable.cs b/lib/VNLib.Plugins.Extensions.Loading/src/Configuration/IAsyncConfigurable.cs new file mode 100644 index 0000000..2c51da2 --- /dev/null +++ b/lib/VNLib.Plugins.Extensions.Loading/src/Configuration/IAsyncConfigurable.cs @@ -0,0 +1,42 @@ +/* +* Copyright (c) 2023 Vaughn Nugent +* +* Library: VNLib +* Package: VNLib.Plugins.Extensions.Loading +* File: IAsyncConfigurable.cs +* +* IAsyncConfigurable.cs is part of VNLib.Plugins.Extensions.Loading which is part of the larger +* VNLib collection of libraries and utilities. +* +* VNLib.Plugins.Extensions.Loading is free software: you can redistribute it and/or modify +* it under the terms of the GNU Affero General Public License as +* published by the Free Software Foundation, either version 3 of the +* License, or (at your option) any later version. +* +* VNLib.Plugins.Extensions.Loading is distributed in the hope that it will be useful, +* but WITHOUT ANY WARRANTY; without even the implied warranty of +* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +* GNU Affero General Public License for more details. +* +* You should have received a copy of the GNU Affero General Public License +* along with this program. If not, see https://www.gnu.org/licenses/. +*/ + +using System.Threading.Tasks; + +namespace VNLib.Plugins.Extensions.Loading +{ + /// + /// Allows for asynchronous service configuration during service creation, that + /// will be observed on the plugin + /// + public interface IAsyncConfigurable + { + /// + /// Configures the service for use. Exceptions will be written to the + /// plugin's default log provider + /// + /// A task that completes when the service has been loaded successfully + Task ConfigureServiceAsync(PluginBase plugin); + } +} diff --git a/lib/VNLib.Plugins.Extensions.Loading/src/Configuration/IConfigScope.cs b/lib/VNLib.Plugins.Extensions.Loading/src/Configuration/IConfigScope.cs new file mode 100644 index 0000000..af6f181 --- /dev/null +++ b/lib/VNLib.Plugins.Extensions.Loading/src/Configuration/IConfigScope.cs @@ -0,0 +1,47 @@ +/* +* Copyright (c) 2023 Vaughn Nugent +* +* Library: VNLib +* Package: VNLib.Plugins.Extensions.Loading +* File: IConfigScope.cs +* +* IConfigScope.cs is part of VNLib.Plugins.Extensions.Loading which is part of the larger +* VNLib collection of libraries and utilities. +* +* VNLib.Plugins.Extensions.Loading is free software: you can redistribute it and/or modify +* it under the terms of the GNU Affero General Public License as +* published by the Free Software Foundation, either version 3 of the +* License, or (at your option) any later version. +* +* VNLib.Plugins.Extensions.Loading is distributed in the hope that it will be useful, +* but WITHOUT ANY WARRANTY; without even the implied warranty of +* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +* GNU Affero General Public License for more details. +* +* You should have received a copy of the GNU Affero General Public License +* along with this program. If not, see https://www.gnu.org/licenses/. +*/ + +using System.Text.Json; +using System.Collections.Generic; + +namespace VNLib.Plugins.Extensions.Loading +{ + /// + /// A top-level scoped configuration element + /// + public interface IConfigScope : IReadOnlyDictionary + { + /// + /// The root level name of the configuration element + /// + string ScopeName { get; } + + /// + /// Json deserialzes the current config scope to the desired type + /// + /// The type to deserialze the current config to + /// The instance created from the current scope + T Deserialze(); + } +} diff --git a/lib/VNLib.Plugins.Extensions.Loading/src/Configuration/IOnConfigValidation.cs b/lib/VNLib.Plugins.Extensions.Loading/src/Configuration/IOnConfigValidation.cs new file mode 100644 index 0000000..6d4641b --- /dev/null +++ b/lib/VNLib.Plugins.Extensions.Loading/src/Configuration/IOnConfigValidation.cs @@ -0,0 +1,38 @@ +/* +* Copyright (c) 2023 Vaughn Nugent +* +* Library: VNLib +* Package: VNLib.Plugins.Extensions.Loading +* File: IOnConfigValidation.cs +* +* IOnConfigValidation.cs is part of VNLib.Plugins.Extensions.Loading which is part of the larger +* VNLib collection of libraries and utilities. +* +* VNLib.Plugins.Extensions.Loading is free software: you can redistribute it and/or modify +* it under the terms of the GNU Affero General Public License as +* published by the Free Software Foundation, either version 3 of the +* License, or (at your option) any later version. +* +* VNLib.Plugins.Extensions.Loading is distributed in the hope that it will be useful, +* but WITHOUT ANY WARRANTY; without even the implied warranty of +* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +* GNU Affero General Public License for more details. +* +* You should have received a copy of the GNU Affero General Public License +* along with this program. If not, see https://www.gnu.org/licenses/. +*/ + +namespace VNLib.Plugins.Extensions.Loading +{ + /// + /// Called when a configuration deserialzation occurs, to validate + /// the configuration. + /// + public interface IOnConfigValidation + { + /// + /// Validates a json configuration during deserialzation + /// + void Validate(); + } +} diff --git a/lib/VNLib.Plugins.Extensions.Loading/src/ConfigurationExtensions.cs b/lib/VNLib.Plugins.Extensions.Loading/src/ConfigurationExtensions.cs index 2bba84a..0ae6ed6 100644 --- a/lib/VNLib.Plugins.Extensions.Loading/src/ConfigurationExtensions.cs +++ b/lib/VNLib.Plugins.Extensions.Loading/src/ConfigurationExtensions.cs @@ -1,5 +1,5 @@ /* -* Copyright (c) 2022 Vaughn Nugent +* Copyright (c) 2023 Vaughn Nugent * * Library: VNLib * Package: VNLib.Plugins.Extensions.Loading @@ -23,12 +23,10 @@ */ using System; -using System.Linq; using System.Text.Json; using System.Reflection; using System.Collections.Generic; - -using VNLib.Utils.Extensions; +using System.Diagnostics.CodeAnalysis; namespace VNLib.Plugins.Extensions.Loading { @@ -52,8 +50,15 @@ namespace VNLib.Plugins.Extensions.Loading { ConfigVarName = configVarName; } + + /// + /// When true or not configured, signals that the type requires a configuration scope + /// when loaded. When false, and configuration is not found, signals to the service loading + /// system to continue without configuration + /// + public bool Required { get; init; } = true; } - + /// /// Contains extensions for plugin configuration specifc extensions /// @@ -69,22 +74,26 @@ namespace VNLib.Plugins.Extensions.Loading /// The type to get the configuration of /// /// A of top level configuration elements for the type + /// /// - public static IReadOnlyDictionary GetConfigForType(this PluginBase plugin) + public static IConfigScope GetConfigForType(this PluginBase plugin) { Type t = typeof(T); return plugin.GetConfigForType(t); } + /// - /// Retrieves a top level configuration dictionary of elements with the specified property name, - /// from the plugin config first, or falls back to the host config file + /// Retrieves a top level configuration dictionary of elements with the specified property name. /// + /// + /// Search order: Plugin config, fall back to host config, throw if not found + /// /// /// The config property name to retrieve - /// A of top level configuration elements for the type + /// A of top level configuration elements for the type /// /// - public static IReadOnlyDictionary GetConfig(this PluginBase plugin, string propName) + public static IConfigScope GetConfig(this PluginBase plugin, string propName) { plugin.ThrowIfUnloaded(); try @@ -96,29 +105,33 @@ namespace VNLib.Plugins.Extensions.Loading el = plugin.HostConfig.GetProperty(propName); } //Get the top level config as a dictionary - return el.EnumerateObject().ToDictionary(static k => k.Name, static k => k.Value); + return new ConfigScope(el, propName); } - catch(KeyNotFoundException) + catch (KeyNotFoundException) { throw new KeyNotFoundException($"Missing required top level configuration object '{propName}', in host/plugin configuration files"); } } + /// /// Retrieves a top level configuration dictionary of elements with the specified property name, - /// from the plugin config first, or falls back to the host config file + /// or null if no configuration could be found /// + /// + /// Search order: Plugin config, fall back to host config, null not found + /// /// /// The config property name to retrieve /// A of top level configuration elements for the type /// - public static IReadOnlyDictionary? TryGetConfig(this PluginBase plugin, string propName) + public static IConfigScope? TryGetConfig(this PluginBase plugin, string propName) { plugin.ThrowIfUnloaded(); //Try to get the element from the plugin config first, or fallback to host if (plugin.PluginConfig.TryGetProperty(propName, out JsonElement el) || plugin.HostConfig.TryGetProperty(propName, out el)) { //Get the top level config as a dictionary - return el.EnumerateObject().ToDictionary(static k => k.Name, static k => k.Value); + return new ConfigScope(el, propName); } //No config found return null; @@ -130,15 +143,65 @@ namespace VNLib.Plugins.Extensions.Loading /// /// /// The type to get configuration data for - /// A of top level configuration elements for the type + /// A of top level configuration elements for the type /// - public static IReadOnlyDictionary GetConfigForType(this PluginBase plugin, Type type) + public static IConfigScope GetConfigForType(this PluginBase plugin, Type type) + { + _ = type ?? throw new ArgumentNullException(nameof(type)); + + string? configName = GetConfigNameForType(type); + + if (configName == null) + { + ThrowConfigNotFoundForType(type); + } + + return plugin.GetConfig(configName); + } + + /// + /// Gets the configuration property name for the type + /// + /// The type to get the configuration name for + /// The configuration property element name + public static string? GetConfigNameForType(Type type) { //Get config name attribute from plugin type - ConfigurationNameAttribute? configName = type.GetCustomAttribute(); - return configName?.ConfigVarName == null - ? throw new KeyNotFoundException("No configuration attribute set") - : plugin.GetConfig(configName.ConfigVarName); + return type.GetCustomAttribute()?.ConfigVarName; + } + + /// + /// Determines if the type requires a configuration element. + /// + /// The type to determine config required status + /// + /// True if the configuration is required, or false if the + /// was not declared, or is false + /// + public static bool ConfigurationRequired(Type type) + { + return type.GetCustomAttribute()?.Required ?? false; + } + + /// + /// Throws a with proper diagnostic information + /// for missing configuration for a given type + /// + /// The type to raise exception for + /// + [DoesNotReturn] + public static void ThrowConfigNotFoundForType(Type type) + { + //Try to get the config property name for the type + string? configName = GetConfigNameForType(type); + if (configName != null) + { + throw new KeyNotFoundException($"Missing required configuration key {configName} for type {type.Name}"); + } + else + { + throw new KeyNotFoundException($"Missing required configuration key for type {type.Name}"); + } } /// @@ -147,14 +210,37 @@ namespace VNLib.Plugins.Extensions.Loading /// /// The object that a configuration can be retrieved for /// The plugin containing configuration variables - /// A of top level configuration elements for the type + /// A of top level configuration elements for the type /// - public static IReadOnlyDictionary GetConfig(this PluginBase plugin, object obj) + public static IConfigScope GetConfig(this PluginBase plugin, object obj) { Type t = obj.GetType(); return plugin.GetConfigForType(t); } + /// + /// Deserialzes the configuration to the desired object and calls its + /// method. Validation exceptions + /// are wrapped in a + /// + /// + /// + /// + /// + public static T DeserialzeAndValidate(this IConfigScope scope) where T : IOnConfigValidation + { + T conf = scope.Deserialze(); + try + { + conf.Validate(); + } + catch(Exception ex) + { + throw new ConfigrationValidationException($"Configuration validation failed for type {typeof(T).Name}", ex); + } + return conf; + } + /// /// Determines if the current plugin configuration contains the require properties to initialize /// the type @@ -170,6 +256,94 @@ namespace VNLib.Plugins.Extensions.Loading return configName != null && plugin.PluginConfig.TryGetProperty(configName.ConfigVarName, out _); } + /// + /// Gets a given configuration element from the global configuration scope + /// and deserializes it into the desired type. + /// + /// If the type inherits the + /// method is invoked, and exceptions are warpped in + /// + /// + /// If the type inherits the + /// method is called by the service scheduler + /// + /// + /// The configuration type + /// + /// The deserialzed configuration element + /// + public static TConfig GetConfigElement(this PluginBase plugin) + { + //Deserialze the element + TConfig config = plugin.GetConfigForType().Deserialze(); + + //If the type is validatable, validate it + if(config is IOnConfigValidation conf) + { + try + { + conf.Validate(); + } + catch (Exception ex) + { + throw new ConfigrationValidationException($"Configuration validation failed for type {typeof(TConfig).Name}", ex); + } + } + + //If async config, load async + if(config is IAsyncConfigurable ac) + { + _ = plugin.ConfigureServiceAsync(ac); + } + + return config; + } + + /// + /// Gets a given configuration element from the global configuration scope + /// and deserializes it into the desired type. + /// + /// If the type inherits the + /// method is invoked, and exceptions are warpped in + /// + /// + /// If the type inherits the + /// method is called by the service scheduler + /// + /// + /// The configuration type + /// + /// The configuration element name override + /// The deserialzed configuration element + /// + public static TConfig GetConfigElement(this PluginBase plugin, string elementName) + { + //Deserialze the element + TConfig config = plugin.GetConfig(elementName).Deserialze(); + + //If the type is validatable, validate it + if (config is IOnConfigValidation conf) + { + try + { + conf.Validate(); + } + catch (Exception ex) + { + throw new ConfigrationValidationException($"Configuration validation failed for type {typeof(TConfig).Name}", ex); + } + } + + //If async config, load async + if (config is IAsyncConfigurable ac) + { + _ = plugin.ConfigureServiceAsync(ac); + } + + return config; + } + + /// /// Attempts to load the basic S3 configuration variables required /// for S3 client access @@ -179,21 +353,8 @@ namespace VNLib.Plugins.Extensions.Loading public static S3Config? TryGetS3Config(this PluginBase plugin) { //Try get the config - IReadOnlyDictionary? s3conf = plugin.TryGetConfig(S3_CONFIG); - if(s3conf == null) - { - return null; - } - - //Try get the elements - return new() - { - BaseBucket = s3conf.GetPropString("bucket"), - ClientId = s3conf.GetPropString("access_key"), - ServerAddress = s3conf.GetPropString("server_address"), - UseSsl = s3conf.TryGetValue("use_ssl", out JsonElement el) && el.GetBoolean(), - Region = s3conf.GetPropString("region"), - }; + IConfigScope? s3conf = plugin.TryGetConfig(S3_CONFIG); + return s3conf?.Deserialze(); } } } diff --git a/lib/VNLib.Plugins.Extensions.Loading/src/Events/AsyncIntervalAttribute.cs b/lib/VNLib.Plugins.Extensions.Loading/src/Events/AsyncIntervalAttribute.cs index e8f071e..139a3ac 100644 --- a/lib/VNLib.Plugins.Extensions.Loading/src/Events/AsyncIntervalAttribute.cs +++ b/lib/VNLib.Plugins.Extensions.Loading/src/Events/AsyncIntervalAttribute.cs @@ -1,5 +1,5 @@ /* -* Copyright (c) 2022 Vaughn Nugent +* Copyright (c) 2023 Vaughn Nugent * * Library: VNLib * Package: VNLib.Plugins.Extensions.Loading @@ -36,12 +36,46 @@ namespace VNLib.Plugins.Extensions.Loading.Events internal readonly TimeSpan Interval; /// - /// Intializes the with the specified timeout in milliseconds + /// Initializes a new with allowing + /// a configurable /// - /// The interval in milliseconds - public AsyncIntervalAttribute(int milliseconds) + public AsyncIntervalAttribute() + {} + + /// + /// Gets or sets the interval in seconds. Choose only ONE internval resolution + /// + public int Seconds + { + get => (int)Interval.TotalSeconds; + init => Interval = TimeSpan.FromSeconds(value); + } + + /// + /// Gets or sets the interval in milliseconds. Choose only ONE internval resolution + /// + public int MilliSeconds + { + get => (int)Interval.TotalMilliseconds; + init => Interval = TimeSpan.FromMilliseconds(value); + } + + /// + /// Gets or sets the interval in minutes. Choose only ONE internval resolution + /// + public int Minutes + { + get => (int)Interval.TotalMinutes; + init => Interval = TimeSpan.FromMinutes(value); + } + + /// + /// Gets or sets the interval in hours. Choose only ONE internval resolution + /// + public int Hours { - Interval = TimeSpan.FromMilliseconds(milliseconds); + get => (int)Interval.TotalMinutes; + init => Interval = TimeSpan.FromHours(value); } } } diff --git a/lib/VNLib.Plugins.Extensions.Loading/src/Events/EventManagment.cs b/lib/VNLib.Plugins.Extensions.Loading/src/Events/EventManagment.cs index bde6986..57e4a9c 100644 --- a/lib/VNLib.Plugins.Extensions.Loading/src/Events/EventManagment.cs +++ b/lib/VNLib.Plugins.Extensions.Loading/src/Events/EventManagment.cs @@ -1,5 +1,5 @@ /* -* Copyright (c) 2022 Vaughn Nugent +* Copyright (c) 2023 Vaughn Nugent * * Library: VNLib * Package: VNLib.Plugins.Extensions.Loading @@ -60,7 +60,7 @@ namespace VNLib.Plugins.Extensions.Loading.Events plugin.Log.Verbose("Interval for {t} scheduled", interval); //Run interval on plugins bg scheduler - _ = plugin.ObserveTask(() => RunIntervalOnPluginScheduler(plugin, asyncCallback, interval, immediate)); + _ = plugin.ObserveWork(() => RunIntervalOnPluginScheduler(plugin, asyncCallback, interval, immediate)); } private static async Task RunIntervalOnPluginScheduler(PluginBase plugin, AsyncSchedulableCallback callback, TimeSpan interval, bool immediate) diff --git a/lib/VNLib.Plugins.Extensions.Loading/src/LoadingExtensions.cs b/lib/VNLib.Plugins.Extensions.Loading/src/LoadingExtensions.cs index 743566d..62af9e3 100644 --- a/lib/VNLib.Plugins.Extensions.Loading/src/LoadingExtensions.cs +++ b/lib/VNLib.Plugins.Extensions.Loading/src/LoadingExtensions.cs @@ -1,5 +1,5 @@ /* -* Copyright (c) 2022 Vaughn Nugent +* Copyright (c) 2023 Vaughn Nugent * * Library: VNLib * Package: VNLib.Plugins.Extensions.Loading @@ -26,6 +26,7 @@ using System; using System.IO; using System.Linq; using System.Text.Json; +using System.Reflection; using System.Threading.Tasks; using System.Collections.Generic; using System.Runtime.CompilerServices; @@ -37,7 +38,7 @@ using VNLib.Utils.Extensions; using VNLib.Plugins.Essentials.Accounts; namespace VNLib.Plugins.Extensions.Loading -{ +{ /// /// Provides common loading (and unloading when required) extensions for plugins @@ -103,45 +104,12 @@ namespace VNLib.Plugins.Extensions.Loading /// /// /// - public static PasswordHashing GetPasswords(this PluginBase plugin) + public static IPasswordHashingProvider GetPasswords(this PluginBase plugin) { plugin.ThrowIfUnloaded(); - //Get/load the passwords one time only - return GetOrCreateSingleton(plugin, LoadPasswords); + //Check if a password configuration element is loaded, otherwise load with defaults + return plugin.GetOrCreateSingleton().Passwords; } - - private static PasswordHashing LoadPasswords(PluginBase plugin) - { - PasswordHashing Passwords; - - //Create new session provider - SecretProvider secrets = new(); - - //Load the secret in the background - secrets.LoadSecret(plugin); - - //See hashing params are defined - IReadOnlyDictionary? hashingArgs = plugin.TryGetConfig(PASSWORD_HASHING_KEY); - if (hashingArgs != null) - { - //Get hashing arguments - uint saltLen = hashingArgs["salt_len"].GetUInt32(); - uint hashLen = hashingArgs["hash_len"].GetUInt32(); - uint timeCost = hashingArgs["time_cost"].GetUInt32(); - uint memoryCost = hashingArgs["memory_cost"].GetUInt32(); - uint parallelism = hashingArgs["parallelism"].GetUInt32(); - //Load passwords - Passwords = new(secrets, (int)saltLen, timeCost, memoryCost, parallelism, hashLen); - } - else - { - //Init default password hashing - Passwords = new(secrets); - } - //return - return Passwords; - } - /// /// Loads an assembly into the current plugins AppDomain and will unload when disposed @@ -165,21 +133,21 @@ namespace VNLib.Plugins.Extensions.Loading _ = assemblyName ?? throw new ArgumentNullException(nameof(assemblyName)); //get plugin directory from config - IReadOnlyDictionary config = plugin.GetConfig("plugins"); + IConfigScope config = plugin.GetConfig("plugins"); /* * Allow an assets directory to limit the scope of the search for the desired * assembly, otherwise search all plugins directories */ - string? assetDir = config.GetPropString("assets"); + string? assetDir = config.GetPropString(PLUGIN_ASSET_KEY); assetDir ??= config["path"].GetString(); /* * This should never happen since this method can only be called from a * plugin context, which means this path was used to load the current plugin */ - _ = assetDir ?? throw new ArgumentNullException("assets", "No plugin path is defined for the current host configuration, this is likely a bug"); + _ = assetDir ?? throw new ArgumentNullException(PLUGIN_ASSET_KEY, "No plugin path is defined for the current host configuration, this is likely a bug"); //Get the first file that matches the search file string? asmFile = Directory.EnumerateFiles(assetDir, assemblyName, dirSearchOption).FirstOrDefault(); @@ -187,8 +155,7 @@ namespace VNLib.Plugins.Extensions.Loading //Load the assembly return AssemblyLoader.Load(asmFile, plugin.UnloadToken); - } - + } /// /// Determintes if the current plugin config has a debug propety set @@ -226,7 +193,7 @@ namespace VNLib.Plugins.Extensions.Loading /// An optional startup delay for the operation /// A task that completes when the deferred task completes /// - public static async Task ObserveTask(this PluginBase plugin, Func asyncTask, int delayMs = 0) + public static async Task ObserveWork(this PluginBase plugin, Func asyncTask, int delayMs = 0) { /* * Motivation: @@ -267,7 +234,6 @@ namespace VNLib.Plugins.Extensions.Loading } } - /// /// Schedules work to begin after the specified delay to be observed by the plugin while /// passing plugin specifie information. Exceptions are logged to the default plugin log @@ -278,22 +244,22 @@ namespace VNLib.Plugins.Extensions.Loading /// The task that represents the scheduled work public static Task ObserveWork(this PluginBase plugin, IAsyncBackgroundWork work, int delayMs = 0) { - return ObserveTask(plugin, () => work.DoWorkAsync(plugin.Log, plugin.UnloadToken), delayMs); + return ObserveWork(plugin, () => work.DoWorkAsync(plugin.Log, plugin.UnloadToken), delayMs); } /// /// 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 /// - /// + /// /// The method to call when the plugin is unloaded /// A task that represents the registered work /// /// - public static Task RegisterForUnload(this PluginBase pbase, Action callback) + public static Task RegisterForUnload(this PluginBase plugin, Action callback) { //Test status - pbase.ThrowIfUnloaded(); + plugin.ThrowIfUnloaded(); _ = callback ?? throw new ArgumentNullException(nameof(callback)); //Wait method @@ -307,9 +273,238 @@ namespace VNLib.Plugins.Extensions.Loading } //Registaer the task to cause the plugin to wait - return pbase.ObserveTask(() => WaitForUnload(pbase, callback)); + return plugin.ObserveWork(() => WaitForUnload(plugin, callback)); + } + + /// + /// + /// Gets or inializes a singleton service of the desired type. + /// + /// + /// If the type derrives the + /// method is called once when the instance is loaded, and observed on the plugin scheduler. + /// + /// + /// If the type derrives the + /// method is called once when the instance is loaded, and observed on the plugin scheduler. + /// + /// + /// + /// + /// + /// + /// + /// + public static T GetOrCreateSingleton(this PluginBase plugin) + { + //Add service to service continer + return GetOrCreateSingleton(plugin, CreateService); + } + + /// + /// + /// Gets or inializes a singleton service of the desired type. + /// + /// + /// If the type derrives the + /// method is called once when the instance is loaded, and observed on the plugin scheduler. + /// + /// + /// If the type derrives the + /// method is called once when the instance is loaded, and observed on the plugin scheduler. + /// + /// + /// + /// + /// Overrids the default configuration property name + /// The configured service singleton + /// + /// + /// + public static T GetOrCreateSingleton(this PluginBase plugin, string configName) + { + //Add service to service continer + return GetOrCreateSingleton(plugin, (plugin) => CreateService(plugin, configName)); + } + + /// + /// Configures the service asynchronously on the plugin's scheduler and returns a task + /// that represents the configuration work. + /// + /// The service type + /// + /// The service to configure + /// The time in milliseconds to delay the configuration task + /// A task that complets when the load operation completes + /// + public static Task ConfigureServiceAsync(this PluginBase plugin, T service, int delayMs = 0) where T : IAsyncConfigurable + { + //Register async load + return ObserveWork(plugin, () => service.ConfigureServiceAsync(plugin), delayMs); + } + + /// + /// + /// Creates and configures a new instance of the desired type and captures the configuration + /// information from the type. + /// + /// + /// If the type derrives the + /// method is called once when the instance is loaded, and observed on the plugin scheduler. + /// + /// + /// If the type derrives the + /// method is called once when the instance is loaded, and observed on the plugin scheduler. + /// + /// + /// If the type derrives the method is called once when + /// the plugin is unloaded. + /// + /// + /// The service type + /// + /// The a new instance configured service + /// + /// + /// + public static T CreateService(this PluginBase plugin) + { + if (plugin.HasConfigForType()) + { + IConfigScope config = plugin.GetConfigForType(); + return CreateService(plugin, config); + } + else + { + return CreateService(plugin, (IConfigScope?)null); + } + } + + /// + /// + /// Creates and configures a new instance of the desired type, with the configuration property name + /// + /// + /// If the type derrives the + /// method is called once when the instance is loaded, and observed on the plugin scheduler. + /// + /// + /// If the type derrives the + /// method is called once when the instance is loaded, and observed on the plugin scheduler. + /// + /// + /// The service type + /// + /// The configuration element name to pass to the new instance + /// The a new instance configured service + /// + /// + /// + public static T CreateService(this PluginBase plugin, string configName) + { + IConfigScope config = plugin.GetConfig(configName); + return CreateService(plugin, config); + } + + /// + /// + /// Creates and configures a new instance of the desired type, with the specified configuration scope + /// + /// + /// If the type derrives the + /// method is called once when the instance is loaded, and observed on the plugin scheduler. + /// + /// + /// If the type derrives the + /// method is called once when the instance is loaded, and observed on the plugin scheduler. + /// + /// + /// The service type + /// + /// The configuration scope to pass directly to the new instance + /// The a new instance configured service + /// + /// + /// + public static T CreateService(this PluginBase plugin, IConfigScope? config) + { + plugin.ThrowIfUnloaded(); + + Type serviceType = typeof(T); + + T service; + + //Determin configuration requirments + if (ConfigurationExtensions.ConfigurationRequired(serviceType) || config != null) + { + if(config == null) + { + ConfigurationExtensions.ThrowConfigNotFoundForType(serviceType); + } + + //Get the constructor for required or available config + ConstructorInfo? constructor = serviceType.GetConstructor(new Type[] { typeof(PluginBase), typeof(IConfigScope) }); + + //Make sure the constructor exists + _ = constructor ?? throw new EntryPointNotFoundException($"No constructor found for {serviceType.Name}"); + + //Call constructore + service = (T)constructor.Invoke(new object[2] { plugin, config }); + } + else + { + //Get the constructor + ConstructorInfo? constructor = serviceType.GetConstructor(new Type[] { typeof(PluginBase) }); + + //Make sure the constructor exists + _ = constructor ?? throw new EntryPointNotFoundException($"No constructor found for {serviceType.Name}"); + + //Call constructore + service = (T)constructor.Invoke(new object[1] { plugin }); + } + + Task? loading = null; + + //If the service is async configurable, configure it + if (service is IAsyncConfigurable asc) + { +#pragma warning disable CA5394 // Do not use insecure randomness + int randomDelay = Random.Shared.Next(1, 100); +#pragma warning restore CA5394 // Do not use insecure randomness + + //Register async load + loading = plugin.ConfigureServiceAsync(asc, randomDelay); + } + + //Allow background work loading + if (service is IAsyncBackgroundWork bw) + { + +#pragma warning disable CA5394 // Do not use insecure randomness + int randomDelay = Random.Shared.Next(10, 200); +#pragma warning restore CA5394 // Do not use insecure randomness + + //If the instances supports async loading, dont start work until its loaded + if(loading != null) + { + _ = loading.ContinueWith(t => ObserveWork(plugin, bw, randomDelay), TaskScheduler.Default); + } + else + { + _ = ObserveWork(plugin, bw, randomDelay); + } + } + + //register dispose cleanup + if (service is IDisposable disp) + { + _ = plugin.RegisterForUnload(disp.Dispose); + } + + return service; } + private sealed class PluginLocalCache { private readonly PluginBase _plugin; @@ -348,52 +543,91 @@ namespace VNLib.Plugins.Extensions.Loading } } - private sealed class SecretProvider : ISecretProvider + [ConfigurationName(PASSWORD_HASHING_KEY, Required = false)] + private sealed class SecretProvider : VnDisposeable, ISecretProvider, IAsyncConfigurable { private byte[]? _pepper; private Exception? _error; - /// - public int BufferSize => _error != null ? throw _error : _pepper?.Length ?? 0; + public SecretProvider(PluginBase plugin, IConfigScope config) + { + if(config.TryGetValue("args", out JsonElement el)) + { + //Convert to dict + IReadOnlyDictionary hashingArgs = el.EnumerateObject().ToDictionary(static k => k.Name, static v => v.Value); + + //Get hashing arguments + uint saltLen = hashingArgs["salt_len"].GetUInt32(); + uint hashLen = hashingArgs["hash_len"].GetUInt32(); + uint timeCost = hashingArgs["time_cost"].GetUInt32(); + uint memoryCost = hashingArgs["memory_cost"].GetUInt32(); + uint parallelism = hashingArgs["parallelism"].GetUInt32(); + //Load passwords + Passwords = new(this, (int)saltLen, timeCost, memoryCost, parallelism, hashLen); + } + else + { + Passwords = new(this); + } + } - public ERRNO GetSecret(Span buffer) + public SecretProvider(PluginBase plugin) { - if(_error != null) + Passwords = new(this); + } + + + public PasswordHashing Passwords { get; } + + /// + public int BufferSize + { + get { - throw _error; + Check(); + return _pepper!.Length; } + } + + public ERRNO GetSecret(Span buffer) + { + Check(); //Coppy pepper to buffer _pepper.CopyTo(buffer); //Return pepper length return _pepper!.Length; } - public void LoadSecret(PluginBase pbase) + protected override void Check() + { + base.Check(); + if(_error != null) + { + throw _error; + } + } + + protected override void Free() { - _ = pbase.ObserveTask(() => LoadSecretInternal(pbase)); + //Clear the pepper if set + MemoryUtil.InitializeBlock(_pepper.AsSpan()); } - private async Task LoadSecretInternal(PluginBase pbase) + public async Task ConfigureServiceAsync(PluginBase plugin) { try { //Get the pepper from secret storage - _pepper = await pbase.TryGetSecretAsync(PASSWORD_HASHING_KEY).ToBase64Bytes(); - - //Regsiter cleanup - _ = pbase.RegisterForUnload(Clear); + _pepper = await plugin.TryGetSecretAsync(PASSWORD_HASHING_KEY).ToBase64Bytes(); } - catch(Exception ex) + catch (Exception ex) { //Store exception for re-propagation _error = ex; - } - } - public void Clear() - { - //Clear the pepper if set - MemoryUtil.InitializeBlock(_pepper.AsSpan()); + //Propagate exception to system + throw; + } } } } diff --git a/lib/VNLib.Plugins.Extensions.Loading/src/RoutingExtensions.cs b/lib/VNLib.Plugins.Extensions.Loading/src/RoutingExtensions.cs index c1c6bb6..22686f0 100644 --- a/lib/VNLib.Plugins.Extensions.Loading/src/RoutingExtensions.cs +++ b/lib/VNLib.Plugins.Extensions.Loading/src/RoutingExtensions.cs @@ -1,5 +1,5 @@ /* -* Copyright (c) 2022 Vaughn Nugent +* Copyright (c) 2023 Vaughn Nugent * * Library: VNLib * Package: VNLib.Plugins.Extensions.Loading @@ -24,22 +24,22 @@ using System; using System.Linq; -using System.Text.Json; using System.Reflection; using System.Collections.Generic; using System.Runtime.CompilerServices; using VNLib.Plugins.Extensions.Loading.Events; -using System.Net; namespace VNLib.Plugins.Extensions.Loading.Routing { + /// /// Provides advanced QOL features to plugin loading /// public static class RoutingExtensions { private static readonly ConditionalWeakTable _pluginRefs = new(); + /// /// Constructs and routes the specific endpoint type for the current plugin @@ -69,13 +69,13 @@ namespace VNLib.Plugins.Extensions.Loading.Routing } else { - ConstructorInfo? constructor = endpointType.GetConstructor(new Type[] { typeof(PluginBase), typeof(IReadOnlyDictionary) }); + ConstructorInfo? constructor = endpointType.GetConstructor(new Type[] { typeof(PluginBase), typeof(IConfigScope) }); //Make sure the constructor exists _ = constructor ?? throw new EntryPointNotFoundException($"No constructor found for {endpointType.Name}"); //Get config variables for the endpoint - IReadOnlyDictionary conf = plugin.GetConfig(pluginConfigPathName); + IConfigScope conf = plugin.GetConfig(pluginConfigPathName); //Create the new endpoint and pass the plugin instance along with the configuration object endpoint = (T)constructor.Invoke(new object[] { plugin, conf }); @@ -126,10 +126,10 @@ namespace VNLib.Plugins.Extensions.Loading.Routing _ = _pluginRefs.TryGetValue(ep, out PluginBase? pBase); return pBase ?? throw new InvalidOperationException("Endpoint was not dynamically routed"); } - - private static void ScheduleIntervals(PluginBase plugin, T endpointInstance, Type epType, IReadOnlyDictionary? endpointLocalConfig) where T : IEndpoint + + private static void ScheduleIntervals(PluginBase plugin, T endpointInstance, Type epType, IConfigScope? endpointLocalConfig) where T : IEndpoint { - //Get all methods that have the configureable async interval attribute specified + //Get all methods that have the configurable async interval attribute specified IEnumerable> confIntervals = epType.GetMethods() .Where(m => m.GetCustomAttribute() != null) .Select(m => new Tuple @@ -169,5 +169,6 @@ namespace VNLib.Plugins.Extensions.Loading.Routing plugin.ScheduleInterval(interval.Item2, interval.Item1.Interval); } } + } } diff --git a/lib/VNLib.Plugins.Extensions.Loading/src/S3Config.cs b/lib/VNLib.Plugins.Extensions.Loading/src/S3Config.cs index bc42f3a..11f101f 100644 --- a/lib/VNLib.Plugins.Extensions.Loading/src/S3Config.cs +++ b/lib/VNLib.Plugins.Extensions.Loading/src/S3Config.cs @@ -1,5 +1,5 @@ /* -* Copyright (c) 2022 Vaughn Nugent +* Copyright (c) 2023 Vaughn Nugent * * Library: VNLib * Package: VNLib.Plugins.Extensions.Loading @@ -22,14 +22,25 @@ * along with this program. If not, see https://www.gnu.org/licenses/. */ +using System.Text.Json.Serialization; + namespace VNLib.Plugins.Extensions.Loading { public sealed class S3Config { + [JsonPropertyName("server_address")] public string? ServerAddress { get; init; } + + [JsonPropertyName("access_key")] public string? ClientId { get; init; } + + [JsonPropertyName("bucket")] public string? BaseBucket { get; init; } + + [JsonPropertyName("use_ssl")] public bool? UseSsl { get; init; } + + [JsonPropertyName("region")] public string? Region { get; init; } } } diff --git a/lib/VNLib.Plugins.Extensions.Loading/src/UserLoading.cs b/lib/VNLib.Plugins.Extensions.Loading/src/UserLoading.cs deleted file mode 100644 index b66bea3..0000000 --- a/lib/VNLib.Plugins.Extensions.Loading/src/UserLoading.cs +++ /dev/null @@ -1,93 +0,0 @@ -/* -* Copyright (c) 2022 Vaughn Nugent -* -* Library: VNLib -* Package: VNLib.Plugins.Extensions.Loading -* File: UserLoading.cs -* -* UserLoading.cs is part of VNLib.Plugins.Extensions.Loading which is part of the larger -* VNLib collection of libraries and utilities. -* -* VNLib.Plugins.Extensions.Loading is free software: you can redistribute it and/or modify -* it under the terms of the GNU Affero General Public License as -* published by the Free Software Foundation, either version 3 of the -* License, or (at your option) any later version. -* -* VNLib.Plugins.Extensions.Loading is distributed in the hope that it will be useful, -* but WITHOUT ANY WARRANTY; without even the implied warranty of -* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -* GNU Affero General Public License for more details. -* -* You should have received a copy of the GNU Affero General Public License -* along with this program. If not, see https://www.gnu.org/licenses/. -*/ - -using System; -using System.Collections.Generic; - -using VNLib.Utils.Logging; -using VNLib.Utils.Extensions; -using VNLib.Plugins.Essentials.Users; - -namespace VNLib.Plugins.Extensions.Loading.Users -{ - /// - /// Contains extension methods for plugins to load the "users" system - /// - public static class UserLoading - { - 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"; - - - /// - /// Gets or loads the plugin's ambient , with the specified user-table name, - /// or the default table name - /// - /// - /// The ambient for the current plugin - /// - /// - public static IUserManager GetUserManager(this PluginBase plugin) - { - plugin.ThrowIfUnloaded(); - //Get stored or load - return LoadingExtensions.GetOrCreateSingleton(plugin, LoadUsers); - } - - private static IUserManager LoadUsers(PluginBase pbase) - { - //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 loader = pbase.LoadAssembly(customAsm); - try - { - //Try to get the onload method - Action? onLoadMethod = loader.TryGetMethod>(ONLOAD_METHOD_NAME); - - //Call the onplugin load method - onLoadMethod?.Invoke(pbase); - - if (pbase.IsDebug()) - { - 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; - } - } - } -} \ No newline at end of file diff --git a/lib/VNLib.Plugins.Extensions.Loading/src/UserManager.cs b/lib/VNLib.Plugins.Extensions.Loading/src/UserManager.cs new file mode 100644 index 0000000..a3d667d --- /dev/null +++ b/lib/VNLib.Plugins.Extensions.Loading/src/UserManager.cs @@ -0,0 +1,138 @@ +/* +* Copyright (c) 2023 Vaughn Nugent +* +* Library: VNLib +* Package: VNLib.Plugins.Extensions.Loading +* File: UserManager.cs +* +* UserManager.cs is part of VNLib.Plugins.Extensions.Loading which is part of the larger +* VNLib collection of libraries and utilities. +* +* VNLib.Plugins.Extensions.Loading is free software: you can redistribute it and/or modify +* it under the terms of the GNU Affero General Public License as +* published by the Free Software Foundation, either version 3 of the +* License, or (at your option) any later version. +* +* VNLib.Plugins.Extensions.Loading is distributed in the hope that it will be useful, +* but WITHOUT ANY WARRANTY; without even the implied warranty of +* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +* GNU Affero General Public License for more details. +* +* You should have received a copy of the GNU Affero General Public License +* along with this program. If not, see https://www.gnu.org/licenses/. +*/ + +using System; +using System.Threading; +using System.Threading.Tasks; +using System.Runtime.CompilerServices; + +using VNLib.Utils; +using VNLib.Utils.Memory; +using VNLib.Utils.Logging; +using VNLib.Plugins.Essentials.Users; + +namespace VNLib.Plugins.Extensions.Loading.Users +{ + /// + /// Provides a singleton service that dynamically loads + /// a user manager for the plugin. + /// + [ConfigurationName("users", Required = false)] + public class UserManager : IUserManager + { + public const string USER_CUSTOM_ASSEMBLY = "custom_assembly"; + public const string DEFAULT_USER_ASM = "VNLib.Plugins.Essentials.Users.dll"; + public const string ONLOAD_METHOD_NAME = "OnPluginLoading"; + + private readonly IUserManager _dynamicLoader; + + public UserManager(PluginBase plugin) + { + _dynamicLoader = LoadUserAssembly(plugin, DEFAULT_USER_ASM); + } + + public UserManager(PluginBase plugin, IConfigScope config) + { + //Get the service configuration + string customAsm = config[USER_CUSTOM_ASSEMBLY].GetString() ?? DEFAULT_USER_ASM; + //Load the assembly + _dynamicLoader = LoadUserAssembly(plugin, customAsm); + } + + private static IUserManager LoadUserAssembly(PluginBase plugin, string customAsm) + { + //Try to load a custom assembly + AssemblyLoader loader = plugin.LoadAssembly(customAsm); + try + { + //Try to get the onload method + Action? onLoadMethod = loader.TryGetMethod>(ONLOAD_METHOD_NAME); + + //Call the onplugin load method + onLoadMethod?.Invoke(plugin); + + if (plugin.IsDebug()) + { + plugin.Log.Debug("Loading user manager from assembly {name}", loader.Resource.GetType().AssemblyQualifiedName); + } + + //Return the loaded instance (may raise exception) + return loader.Resource; + } + catch + { + loader.Dispose(); + throw; + } + } + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public Task CreateUserAsync(string userid, string emailAddress, ulong privilages, PrivateString passHash, CancellationToken cancellation = default) + { + return _dynamicLoader.CreateUserAsync(userid, emailAddress, privilages, passHash, cancellation); + } + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public Task GetUserAndPassFromEmailAsync(string emailAddress, CancellationToken cancellationToken = default) + { + return _dynamicLoader.GetUserAndPassFromEmailAsync(emailAddress, cancellationToken); + } + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public Task GetUserAndPassFromIDAsync(string userid, CancellationToken cancellation = default) + { + return _dynamicLoader.GetUserAndPassFromIDAsync(userid, cancellation); + } + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public Task GetUserCountAsync(CancellationToken cancellation = default) + { + return _dynamicLoader.GetUserCountAsync(cancellation); + } + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public Task GetUserFromEmailAsync(string emailAddress, CancellationToken cancellationToken = default) + { + return _dynamicLoader.GetUserFromEmailAsync(emailAddress, cancellationToken); + } + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public Task GetUserFromIDAsync(string userId, CancellationToken cancellationToken = default) + { + return _dynamicLoader.GetUserFromIDAsync(userId, cancellationToken); + } + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public Task UpdatePassAsync(IUser user, PrivateString newPass, CancellationToken cancellation = default) + { + return _dynamicLoader.UpdatePassAsync(user, newPass, cancellation); + } + } +} \ No newline at end of file diff --git a/lib/VNLib.Plugins.Extensions.Loading/src/VNLib.Plugins.Extensions.Loading.csproj b/lib/VNLib.Plugins.Extensions.Loading/src/VNLib.Plugins.Extensions.Loading.csproj index a660a7e..ca4113e 100644 --- a/lib/VNLib.Plugins.Extensions.Loading/src/VNLib.Plugins.Extensions.Loading.csproj +++ b/lib/VNLib.Plugins.Extensions.Loading/src/VNLib.Plugins.Extensions.Loading.csproj @@ -4,11 +4,8 @@ net6.0 VNLib.Plugins.Extensions.Loading VNLib.Plugins.Extensions.Loading - 1.0.1.1 True enable - True - \\vaughnnugent.com\Internal\Folder Redirection\vman\Documents\Programming\Software\StrongNameingKey.snk latest-all @@ -18,6 +15,21 @@ Vaughn Nugent + + + Vaughn Nugent + Vaughn Nugent + VNLib.Plugins.Extensions.Loading + VNLib.Plugins.Extensions.Loading + + An Essentials framework extension library for common loading/configuration/service operations. Enables rapid plugin + and service development. + + Copyright © 2023 Vaughn Nugent + https://www.vaughnnugent.com/resources/software/modules/VNLib.Plugins.Extensions + https://github.com/VnUgE/VNLib.Plugins.Extensions/tree/master/lib/VNLib.Plugins.Extensions.Loading + + all @@ -27,7 +39,6 @@ all runtime; build; native; contentfiles; analyzers; buildtransitive - @@ -37,4 +48,8 @@ + + + + diff --git a/lib/VNLib.Plugins.Extensions.Loading/src/VaultSecrets.cs b/lib/VNLib.Plugins.Extensions.Loading/src/VaultSecrets.cs index da6650a..25e30ea 100644 --- a/lib/VNLib.Plugins.Extensions.Loading/src/VaultSecrets.cs +++ b/lib/VNLib.Plugins.Extensions.Loading/src/VaultSecrets.cs @@ -1,5 +1,5 @@ /* -* Copyright (c) 2022 Vaughn Nugent +* Copyright (c) 2023 Vaughn Nugent * * Library: VNLib * Package: VNLib.Plugins.Extensions.Loading @@ -284,7 +284,7 @@ namespace VNLib.Plugins.Extensions.Loading private static IVaultClient? TryGetVaultLoader(PluginBase pbase) { //Get vault config - IReadOnlyDictionary? conf = pbase.TryGetConfig(VAULT_OBJECT_NAME); + IConfigScope? conf = pbase.TryGetConfig(VAULT_OBJECT_NAME); if (conf == null) { diff --git a/lib/VNLib.Plugins.Extensions.Validation/src/VNLib.Plugins.Extensions.Validation.csproj b/lib/VNLib.Plugins.Extensions.Validation/src/VNLib.Plugins.Extensions.Validation.csproj index 2496138..826fe47 100644 --- a/lib/VNLib.Plugins.Extensions.Validation/src/VNLib.Plugins.Extensions.Validation.csproj +++ b/lib/VNLib.Plugins.Extensions.Validation/src/VNLib.Plugins.Extensions.Validation.csproj @@ -4,21 +4,27 @@ net6.0 VNLib.Plugins.Extensions.Validation VNLib.Plugins.Extensions.Validation - 1.0.1.1 enable - True - \\vaughnnugent.com\Internal\Folder Redirection\vman\Documents\Programming\Software\StrongNameingKey.snk latest-all True Vaughn Nugent + Vaughn Nugent + VNLib.Plugins.Extensions.Validation + VNLib.Plugins.Extensions.Validation + + An Essentials framework extension library for extending FluentValidation validators for common checks + including minium password validation. + Copyright © 2023 Vaughn Nugent - https://www.vaughnnugent.com/resources + https://www.vaughnnugent.com/resources/software/modules/VNLib.Plugins.Sessions + https://github.com/VnUgE/VNLib.Plugins.Extensions/tree/master/lib/VNLib.Plugins.Extensions.Validation + all @@ -28,7 +34,7 @@ all runtime; build; native; contentfiles; analyzers; buildtransitive - + -- cgit