diff options
Diffstat (limited to 'lib/VNLib.Plugins.Extensions.Loading')
16 files changed, 1038 insertions, 264 deletions
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 /// <typeparam name="T">The exported type to manage</typeparam> public sealed class AssemblyLoader<T> : OpenResourceHandle<T> { - private readonly PluginLoader _loader; private readonly CancellationTokenRegistration _reg; private readonly Lazy<T> _instance; + private readonly AssemblyLoadContext _loadContext; + private readonly AssemblyDependencyResolver _resolver; + private readonly string _assemblyPath; /// <summary> /// The instance of the loaded type /// </summary> 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); + } + /// <summary> /// 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 /// <exception cref="EntryPointNotFoundException"></exception> 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 ///<inheritdoc/> 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(); } /// <summary> - /// 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 <see cref="AssemblyLoadContext"/> which belongs the current plugin instance. /// </summary> /// <param name="assemblyName">The name of the assmbly within the current plugin directory</param> /// <param name="unloadToken">The plugin unload token</param> + /// <exception cref="FileNotFoundException"></exception> internal static AssemblyLoader<T> 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<Type> 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 +{ + /// <summary> + /// An exception raised when a configuration validation exception has occured + /// </summary> + 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<IReadOnlyDictionary<string, JsonElement>> _config; + + private readonly JsonElement _element; + + internal ConfigScope(JsonElement element, string scopeName) + { + _element = element; + ScopeName = scopeName; + _config = new(LoadTable); + } + + private IReadOnlyDictionary<string, JsonElement> LoadTable() + { + return _element.EnumerateObject().ToDictionary(static k => k.Name, static k => k.Value); + } + + ///<inheritdoc/> + public JsonElement this[string key] => _config.Value[key]; + + ///<inheritdoc/> + public IEnumerable<string> Keys => _config.Value.Keys; + + ///<inheritdoc/> + public IEnumerable<JsonElement> Values => _config.Value.Values; + + ///<inheritdoc/> + public int Count => _config.Value.Count; + + ///<inheritdoc/> + public string ScopeName { get; } + + ///<inheritdoc/> + public bool ContainsKey(string key) => _config.Value.ContainsKey(key); + + ///<inheritdoc/> + public T Deserialze<T>() => _element.Deserialize<T>()!; + + ///<inheritdoc/> + public IEnumerator<KeyValuePair<string, JsonElement>> GetEnumerator() => _config.Value.GetEnumerator(); + + ///<inheritdoc/> + 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 +{ + /// <summary> + /// Allows for asynchronous service configuration during service creation, that + /// will be observed on the plugin + /// </summary> + public interface IAsyncConfigurable + { + /// <summary> + /// Configures the service for use. Exceptions will be written to the + /// plugin's default log provider + /// </summary> + /// <returns>A task that completes when the service has been loaded successfully</returns> + 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 +{ + /// <summary> + /// A top-level scoped configuration element + /// </summary> + public interface IConfigScope : IReadOnlyDictionary<string, JsonElement> + { + /// <summary> + /// The root level name of the configuration element + /// </summary> + string ScopeName { get; } + + /// <summary> + /// Json deserialzes the current config scope to the desired type + /// </summary> + /// <typeparam name="T">The type to deserialze the current config to</typeparam> + /// <returns>The instance created from the current scope</returns> + T Deserialze<T>(); + } +} 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 +{ + /// <summary> + /// Called when a configuration deserialzation occurs, to validate + /// the configuration. + /// </summary> + public interface IOnConfigValidation + { + /// <summary> + /// Validates a json configuration during deserialzation + /// </summary> + 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; } + + /// <summary> + /// 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 + /// </summary> + public bool Required { get; init; } = true; } - + /// <summary> /// Contains extensions for plugin configuration specifc extensions /// </summary> @@ -69,22 +74,26 @@ namespace VNLib.Plugins.Extensions.Loading /// <typeparam name="T">The type to get the configuration of</typeparam> /// <param name="plugin"></param> /// <returns>A <see cref="Dictionary{TKey, TValue}"/> of top level configuration elements for the type</returns> + /// <exception cref="KeyNotFoundException"></exception> /// <exception cref="ObjectDisposedException"></exception> - public static IReadOnlyDictionary<string, JsonElement> GetConfigForType<T>(this PluginBase plugin) + public static IConfigScope GetConfigForType<T>(this PluginBase plugin) { Type t = typeof(T); return plugin.GetConfigForType(t); } + /// <summary> - /// 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. /// </summary> + /// <remarks> + /// Search order: Plugin config, fall back to host config, throw if not found + /// </remarks> /// <param name="plugin"></param> /// <param name="propName">The config property name to retrieve</param> - /// <returns>A <see cref="Dictionary{TKey, TValue}"/> of top level configuration elements for the type</returns> + /// <returns>A <see cref="IConfigScope"/> of top level configuration elements for the type</returns> /// <exception cref="KeyNotFoundException"></exception> /// <exception cref="ObjectDisposedException"></exception> - public static IReadOnlyDictionary<string, JsonElement> 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"); } } + /// <summary> /// 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 /// </summary> + /// <remarks> + /// Search order: Plugin config, fall back to host config, null not found + /// </remarks> /// <param name="plugin"></param> /// <param name="propName">The config property name to retrieve</param> /// <returns>A <see cref="Dictionary{TKey, TValue}"/> of top level configuration elements for the type</returns> /// <exception cref="ObjectDisposedException"></exception> - public static IReadOnlyDictionary<string, JsonElement>? 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 /// </summary> /// <param name="plugin"></param> /// <param name="type">The type to get configuration data for</param> - /// <returns>A <see cref="Dictionary{TKey, TValue}"/> of top level configuration elements for the type</returns> + /// <returns>A <see cref="IConfigScope"/> of top level configuration elements for the type</returns> /// <exception cref="ObjectDisposedException"></exception> - public static IReadOnlyDictionary<string, JsonElement> 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); + } + + /// <summary> + /// Gets the configuration property name for the type + /// </summary> + /// <param name="type">The type to get the configuration name for</param> + /// <returns>The configuration property element name</returns> + public static string? GetConfigNameForType(Type type) { //Get config name attribute from plugin type - ConfigurationNameAttribute? configName = type.GetCustomAttribute<ConfigurationNameAttribute>(); - return configName?.ConfigVarName == null - ? throw new KeyNotFoundException("No configuration attribute set") - : plugin.GetConfig(configName.ConfigVarName); + return type.GetCustomAttribute<ConfigurationNameAttribute>()?.ConfigVarName; + } + + /// <summary> + /// Determines if the type requires a configuration element. + /// </summary> + /// <param name="type">The type to determine config required status</param> + /// <returns> + /// True if the configuration is required, or false if the <see cref="ConfigurationNameAttribute"/> + /// was not declared, or <see cref="ConfigurationNameAttribute.Required"/> is false + /// </returns> + public static bool ConfigurationRequired(Type type) + { + return type.GetCustomAttribute<ConfigurationNameAttribute>()?.Required ?? false; + } + + /// <summary> + /// Throws a <see cref="KeyNotFoundException"/> with proper diagnostic information + /// for missing configuration for a given type + /// </summary> + /// <param name="type">The type to raise exception for</param> + /// <exception cref="KeyNotFoundException"></exception> + [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}"); + } } /// <summary> @@ -147,15 +210,38 @@ namespace VNLib.Plugins.Extensions.Loading /// </summary> /// <param name="obj">The object that a configuration can be retrieved for</param> /// <param name="plugin">The plugin containing configuration variables</param> - /// <returns>A <see cref="Dictionary{TKey, TValue}"/> of top level configuration elements for the type</returns> + /// <returns>A <see cref="IConfigScope"/> of top level configuration elements for the type</returns> /// <exception cref="ObjectDisposedException"></exception> - public static IReadOnlyDictionary<string, JsonElement> GetConfig(this PluginBase plugin, object obj) + public static IConfigScope GetConfig(this PluginBase plugin, object obj) { Type t = obj.GetType(); return plugin.GetConfigForType(t); } /// <summary> + /// Deserialzes the configuration to the desired object and calls its + /// <see cref="IOnConfigValidation.Validate"/> method. Validation exceptions + /// are wrapped in a <see cref="ConfigrationValidationException"/> + /// </summary> + /// <typeparam name="T"></typeparam> + /// <param name="scope"></param> + /// <returns></returns> + /// <exception cref="ConfigrationValidationException"></exception> + public static T DeserialzeAndValidate<T>(this IConfigScope scope) where T : IOnConfigValidation + { + T conf = scope.Deserialze<T>(); + try + { + conf.Validate(); + } + catch(Exception ex) + { + throw new ConfigrationValidationException($"Configuration validation failed for type {typeof(T).Name}", ex); + } + return conf; + } + + /// <summary> /// Determines if the current plugin configuration contains the require properties to initialize /// the type /// </summary> @@ -171,6 +257,94 @@ namespace VNLib.Plugins.Extensions.Loading } /// <summary> + /// Gets a given configuration element from the global configuration scope + /// and deserializes it into the desired type. + /// <para> + /// If the type inherits <see cref="IOnConfigValidation"/> the <see cref="IOnConfigValidation.Validate"/> + /// method is invoked, and exceptions are warpped in <see cref="ConfigrationValidationException"/> + /// </para> + /// <para> + /// If the type inherits <see cref="IAsyncConfigurable"/> the <see cref="IAsyncConfigurable.ConfigureServiceAsync(PluginBase)"/> + /// method is called by the service scheduler + /// </para> + /// </summary> + /// <typeparam name="TConfig">The configuration type</typeparam> + /// <param name="plugin"></param> + /// <returns>The deserialzed configuration element</returns> + /// <exception cref="ConfigrationValidationException"></exception> + public static TConfig GetConfigElement<TConfig>(this PluginBase plugin) + { + //Deserialze the element + TConfig config = plugin.GetConfigForType<TConfig>().Deserialze<TConfig>(); + + //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; + } + + /// <summary> + /// Gets a given configuration element from the global configuration scope + /// and deserializes it into the desired type. + /// <para> + /// If the type inherits <see cref="IOnConfigValidation"/> the <see cref="IOnConfigValidation.Validate"/> + /// method is invoked, and exceptions are warpped in <see cref="ConfigrationValidationException"/> + /// </para> + /// <para> + /// If the type inherits <see cref="IAsyncConfigurable"/> the <see cref="IAsyncConfigurable.ConfigureServiceAsync(PluginBase)"/> + /// method is called by the service scheduler + /// </para> + /// </summary> + /// <typeparam name="TConfig">The configuration type</typeparam> + /// <param name="plugin"></param> + /// <param name="elementName">The configuration element name override</param> + /// <returns>The deserialzed configuration element</returns> + /// <exception cref="ConfigrationValidationException"></exception> + public static TConfig GetConfigElement<TConfig>(this PluginBase plugin, string elementName) + { + //Deserialze the element + TConfig config = plugin.GetConfig(elementName).Deserialze<TConfig>(); + + //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; + } + + + /// <summary> /// Attempts to load the basic S3 configuration variables required /// for S3 client access /// </summary> @@ -179,21 +353,8 @@ namespace VNLib.Plugins.Extensions.Loading public static S3Config? TryGetS3Config(this PluginBase plugin) { //Try get the config - IReadOnlyDictionary<string, JsonElement>? 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<S3Config>(); } } } 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; /// <summary> - /// Intializes the <see cref="AsyncIntervalAttribute"/> with the specified timeout in milliseconds + /// Initializes a new <see cref="AsyncIntervalAttribute"/> with allowing + /// a configurable /// </summary> - /// <param name="milliseconds">The interval in milliseconds</param> - public AsyncIntervalAttribute(int milliseconds) + public AsyncIntervalAttribute() + {} + + /// <summary> + /// Gets or sets the interval in seconds. Choose only ONE internval resolution + /// </summary> + public int Seconds + { + get => (int)Interval.TotalSeconds; + init => Interval = TimeSpan.FromSeconds(value); + } + + /// <summary> + /// Gets or sets the interval in milliseconds. Choose only ONE internval resolution + /// </summary> + public int MilliSeconds + { + get => (int)Interval.TotalMilliseconds; + init => Interval = TimeSpan.FromMilliseconds(value); + } + + /// <summary> + /// Gets or sets the interval in minutes. Choose only ONE internval resolution + /// </summary> + public int Minutes + { + get => (int)Interval.TotalMinutes; + init => Interval = TimeSpan.FromMinutes(value); + } + + /// <summary> + /// Gets or sets the interval in hours. Choose only ONE internval resolution + /// </summary> + 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 -{ +{ /// <summary> /// Provides common loading (and unloading when required) extensions for plugins @@ -103,45 +104,12 @@ namespace VNLib.Plugins.Extensions.Loading /// <exception cref="OverflowException"></exception> /// <exception cref="KeyNotFoundException"></exception> /// <exception cref="ObjectDisposedException"></exception> - public static PasswordHashing GetPasswords(this PluginBase plugin) + public static IPasswordHashingProvider GetPasswords(this PluginBase plugin) { plugin.ThrowIfUnloaded(); - //Get/load the passwords one time only - return GetOrCreateSingleton(plugin, LoadPasswords); + //Check if a password configuration element is loaded, otherwise load with defaults + return plugin.GetOrCreateSingleton<SecretProvider>().Passwords; } - - private static PasswordHashing LoadPasswords(PluginBase plugin) - { - PasswordHashing Passwords; - - //Create new session provider - SecretProvider secrets = new(); - - //Load the secret in the background - secrets.LoadSecret(plugin); - - //See hashing params are defined - IReadOnlyDictionary<string, JsonElement>? hashingArgs = plugin.TryGetConfig(PASSWORD_HASHING_KEY); - if (hashingArgs != null) - { - //Get hashing arguments - uint saltLen = hashingArgs["salt_len"].GetUInt32(); - uint hashLen = hashingArgs["hash_len"].GetUInt32(); - uint timeCost = hashingArgs["time_cost"].GetUInt32(); - uint memoryCost = hashingArgs["memory_cost"].GetUInt32(); - uint parallelism = hashingArgs["parallelism"].GetUInt32(); - //Load passwords - Passwords = new(secrets, (int)saltLen, timeCost, memoryCost, parallelism, hashLen); - } - else - { - //Init default password hashing - Passwords = new(secrets); - } - //return - return Passwords; - } - /// <summary> /// Loads an assembly into the current plugins AppDomain and will unload when disposed @@ -165,21 +133,21 @@ namespace VNLib.Plugins.Extensions.Loading _ = assemblyName ?? throw new ArgumentNullException(nameof(assemblyName)); //get plugin directory from config - IReadOnlyDictionary<string, JsonElement> config = plugin.GetConfig("plugins"); + IConfigScope config = plugin.GetConfig("plugins"); /* * Allow an assets directory to limit the scope of the search for the desired * assembly, otherwise search all plugins directories */ - string? assetDir = config.GetPropString("assets"); + string? assetDir = config.GetPropString(PLUGIN_ASSET_KEY); assetDir ??= config["path"].GetString(); /* * This should never happen since this method can only be called from a * plugin context, which means this path was used to load the current plugin */ - _ = assetDir ?? throw new ArgumentNullException("assets", "No plugin path is defined for the current host configuration, this is likely a bug"); + _ = assetDir ?? throw new ArgumentNullException(PLUGIN_ASSET_KEY, "No plugin path is defined for the current host configuration, this is likely a bug"); //Get the first file that matches the search file string? asmFile = Directory.EnumerateFiles(assetDir, assemblyName, dirSearchOption).FirstOrDefault(); @@ -187,8 +155,7 @@ namespace VNLib.Plugins.Extensions.Loading //Load the assembly return AssemblyLoader<T>.Load(asmFile, plugin.UnloadToken); - } - + } /// <summary> /// Determintes if the current plugin config has a debug propety set @@ -226,7 +193,7 @@ namespace VNLib.Plugins.Extensions.Loading /// <param name="delayMs">An optional startup delay for the operation</param> /// <returns>A task that completes when the deferred task completes </returns> /// <exception cref="ObjectDisposedException"></exception> - public static async Task ObserveTask(this PluginBase plugin, Func<Task> asyncTask, int delayMs = 0) + public static async Task ObserveWork(this PluginBase plugin, Func<Task> asyncTask, int delayMs = 0) { /* * Motivation: @@ -267,7 +234,6 @@ namespace VNLib.Plugins.Extensions.Loading } } - /// <summary> /// Schedules work to begin after the specified delay to be observed by the plugin while /// passing plugin specifie information. Exceptions are logged to the default plugin log @@ -278,22 +244,22 @@ namespace VNLib.Plugins.Extensions.Loading /// <returns>The task that represents the scheduled work</returns> public static Task ObserveWork(this PluginBase plugin, IAsyncBackgroundWork work, int delayMs = 0) { - return ObserveTask(plugin, () => work.DoWorkAsync(plugin.Log, plugin.UnloadToken), delayMs); + return ObserveWork(plugin, () => work.DoWorkAsync(plugin.Log, plugin.UnloadToken), delayMs); } /// <summary> /// Registers an event to occur when the plugin is unloaded on a background thread /// and will cause the Plugin.Unload() method to block until the event completes /// </summary> - /// <param name="pbase"></param> + /// <param name="plugin"></param> /// <param name="callback">The method to call when the plugin is unloaded</param> /// <returns>A task that represents the registered work</returns> /// <exception cref="ArgumentNullException"></exception> /// <exception cref="ObjectDisposedException"></exception> - public static Task RegisterForUnload(this PluginBase pbase, Action callback) + public static Task RegisterForUnload(this PluginBase plugin, Action callback) { //Test status - pbase.ThrowIfUnloaded(); + plugin.ThrowIfUnloaded(); _ = callback ?? throw new ArgumentNullException(nameof(callback)); //Wait method @@ -307,9 +273,238 @@ namespace VNLib.Plugins.Extensions.Loading } //Registaer the task to cause the plugin to wait - return pbase.ObserveTask(() => WaitForUnload(pbase, callback)); + return plugin.ObserveWork(() => WaitForUnload(plugin, callback)); + } + + /// <summary> + /// <para> + /// Gets or inializes a singleton service of the desired type. + /// </para> + /// <para> + /// If the type derrives <see cref="IAsyncConfigurable"/> the <see cref="IAsyncConfigurable.ConfigureServiceAsync"/> + /// method is called once when the instance is loaded, and observed on the plugin scheduler. + /// </para> + /// <para> + /// If the type derrives <see cref="IAsyncBackgroundWork"/> the <see cref="IAsyncBackgroundWork.DoWorkAsync(ILogProvider, System.Threading.CancellationToken)"/> + /// method is called once when the instance is loaded, and observed on the plugin scheduler. + /// </para> + /// </summary> + /// <typeparam name="T"></typeparam> + /// <param name="plugin"></param> + /// <returns></returns> + /// <exception cref="KeyNotFoundException"></exception> + /// <exception cref="ObjectDisposedException"></exception> + /// <exception cref="EntryPointNotFoundException"></exception> + public static T GetOrCreateSingleton<T>(this PluginBase plugin) + { + //Add service to service continer + return GetOrCreateSingleton(plugin, CreateService<T>); + } + + /// <summary> + /// <para> + /// Gets or inializes a singleton service of the desired type. + /// </para> + /// <para> + /// If the type derrives <see cref="IAsyncConfigurable"/> the <see cref="IAsyncConfigurable.ConfigureServiceAsync"/> + /// method is called once when the instance is loaded, and observed on the plugin scheduler. + /// </para> + /// <para> + /// If the type derrives <see cref="IAsyncBackgroundWork"/> the <see cref="IAsyncBackgroundWork.DoWorkAsync(ILogProvider, System.Threading.CancellationToken)"/> + /// method is called once when the instance is loaded, and observed on the plugin scheduler. + /// </para> + /// </summary> + /// <typeparam name="T"></typeparam> + /// <param name="plugin"></param> + /// <param name="configName">Overrids the default configuration property name</param> + /// <returns>The configured service singleton</returns> + /// <exception cref="KeyNotFoundException"></exception> + /// <exception cref="ObjectDisposedException"></exception> + /// <exception cref="EntryPointNotFoundException"></exception> + public static T GetOrCreateSingleton<T>(this PluginBase plugin, string configName) + { + //Add service to service continer + return GetOrCreateSingleton(plugin, (plugin) => CreateService<T>(plugin, configName)); + } + + /// <summary> + /// Configures the service asynchronously on the plugin's scheduler and returns a task + /// that represents the configuration work. + /// </summary> + /// <typeparam name="T">The service type</typeparam> + /// <param name="plugin"></param> + /// <param name="service">The service to configure</param> + /// <param name="delayMs">The time in milliseconds to delay the configuration task</param> + /// <returns>A task that complets when the load operation completes</returns> + /// <exception cref="ObjectDisposedException"></exception> + public static Task ConfigureServiceAsync<T>(this PluginBase plugin, T service, int delayMs = 0) where T : IAsyncConfigurable + { + //Register async load + return ObserveWork(plugin, () => service.ConfigureServiceAsync(plugin), delayMs); + } + + /// <summary> + /// <para> + /// Creates and configures a new instance of the desired type and captures the configuration + /// information from the type. + /// </para> + /// <para> + /// If the type derrives <see cref="IAsyncConfigurable"/> the <see cref="IAsyncConfigurable.ConfigureServiceAsync"/> + /// method is called once when the instance is loaded, and observed on the plugin scheduler. + /// </para> + /// <para> + /// If the type derrives <see cref="IAsyncBackgroundWork"/> the <see cref="IAsyncBackgroundWork.DoWorkAsync(ILogProvider, System.Threading.CancellationToken)"/> + /// method is called once when the instance is loaded, and observed on the plugin scheduler. + /// </para> + /// <para> + /// If the type derrives <see cref="IDisposable"/> the <see cref="IDisposable.Dispose"/> method is called once when + /// the plugin is unloaded. + /// </para> + /// </summary> + /// <typeparam name="T">The service type</typeparam> + /// <param name="plugin"></param> + /// <returns>The a new instance configured service</returns> + /// <exception cref="KeyNotFoundException"></exception> + /// <exception cref="ObjectDisposedException"></exception> + /// <exception cref="EntryPointNotFoundException"></exception> + public static T CreateService<T>(this PluginBase plugin) + { + if (plugin.HasConfigForType<T>()) + { + IConfigScope config = plugin.GetConfigForType<T>(); + return CreateService<T>(plugin, config); + } + else + { + return CreateService<T>(plugin, (IConfigScope?)null); + } + } + + /// <summary> + /// <para> + /// Creates and configures a new instance of the desired type, with the configuration property name + /// </para> + /// <para> + /// If the type derrives <see cref="IAsyncConfigurable"/> the <see cref="IAsyncConfigurable.ConfigureServiceAsync"/> + /// method is called once when the instance is loaded, and observed on the plugin scheduler. + /// </para> + /// <para> + /// If the type derrives <see cref="IAsyncBackgroundWork"/> the <see cref="IAsyncBackgroundWork.DoWorkAsync(ILogProvider, System.Threading.CancellationToken)"/> + /// method is called once when the instance is loaded, and observed on the plugin scheduler. + /// </para> + /// </summary> + /// <typeparam name="T">The service type</typeparam> + /// <param name="plugin"></param> + /// <param name="configName">The configuration element name to pass to the new instance</param> + /// <returns>The a new instance configured service</returns> + /// <exception cref="KeyNotFoundException"></exception> + /// <exception cref="ObjectDisposedException"></exception> + /// <exception cref="EntryPointNotFoundException"></exception> + public static T CreateService<T>(this PluginBase plugin, string configName) + { + IConfigScope config = plugin.GetConfig(configName); + return CreateService<T>(plugin, config); + } + + /// <summary> + /// <para> + /// Creates and configures a new instance of the desired type, with the specified configuration scope + /// </para> + /// <para> + /// If the type derrives <see cref="IAsyncConfigurable"/> the <see cref="IAsyncConfigurable.ConfigureServiceAsync"/> + /// method is called once when the instance is loaded, and observed on the plugin scheduler. + /// </para> + /// <para> + /// If the type derrives <see cref="IAsyncBackgroundWork"/> the <see cref="IAsyncBackgroundWork.DoWorkAsync(ILogProvider, System.Threading.CancellationToken)"/> + /// method is called once when the instance is loaded, and observed on the plugin scheduler. + /// </para> + /// </summary> + /// <typeparam name="T">The service type</typeparam> + /// <param name="plugin"></param> + /// <param name="config">The configuration scope to pass directly to the new instance</param> + /// <returns>The a new instance configured service</returns> + /// <exception cref="KeyNotFoundException"></exception> + /// <exception cref="ObjectDisposedException"></exception> + /// <exception cref="EntryPointNotFoundException"></exception> + public static T CreateService<T>(this PluginBase plugin, IConfigScope? config) + { + plugin.ThrowIfUnloaded(); + + Type serviceType = typeof(T); + + T service; + + //Determin configuration requirments + if (ConfigurationExtensions.ConfigurationRequired(serviceType) || config != null) + { + if(config == null) + { + ConfigurationExtensions.ThrowConfigNotFoundForType(serviceType); + } + + //Get the constructor for required or available config + ConstructorInfo? constructor = serviceType.GetConstructor(new Type[] { typeof(PluginBase), typeof(IConfigScope) }); + + //Make sure the constructor exists + _ = constructor ?? throw new EntryPointNotFoundException($"No constructor found for {serviceType.Name}"); + + //Call constructore + service = (T)constructor.Invoke(new object[2] { plugin, config }); + } + else + { + //Get the constructor + ConstructorInfo? constructor = serviceType.GetConstructor(new Type[] { typeof(PluginBase) }); + + //Make sure the constructor exists + _ = constructor ?? throw new EntryPointNotFoundException($"No constructor found for {serviceType.Name}"); + + //Call constructore + service = (T)constructor.Invoke(new object[1] { plugin }); + } + + Task? loading = null; + + //If the service is async configurable, configure it + if (service is IAsyncConfigurable asc) + { +#pragma warning disable CA5394 // Do not use insecure randomness + int randomDelay = Random.Shared.Next(1, 100); +#pragma warning restore CA5394 // Do not use insecure randomness + + //Register async load + loading = plugin.ConfigureServiceAsync(asc, randomDelay); + } + + //Allow background work loading + if (service is IAsyncBackgroundWork bw) + { + +#pragma warning disable CA5394 // Do not use insecure randomness + int randomDelay = Random.Shared.Next(10, 200); +#pragma warning restore CA5394 // Do not use insecure randomness + + //If the instances supports async loading, dont start work until its loaded + if(loading != null) + { + _ = loading.ContinueWith(t => ObserveWork(plugin, bw, randomDelay), TaskScheduler.Default); + } + else + { + _ = ObserveWork(plugin, bw, randomDelay); + } + } + + //register dispose cleanup + if (service is IDisposable disp) + { + _ = plugin.RegisterForUnload(disp.Dispose); + } + + return service; } + private sealed class PluginLocalCache { private readonly PluginBase _plugin; @@ -348,52 +543,91 @@ namespace VNLib.Plugins.Extensions.Loading } } - private sealed class SecretProvider : ISecretProvider + [ConfigurationName(PASSWORD_HASHING_KEY, Required = false)] + private sealed class SecretProvider : VnDisposeable, ISecretProvider, IAsyncConfigurable { private byte[]? _pepper; private Exception? _error; - ///<inheritdoc/> - public int BufferSize => _error != null ? throw _error : _pepper?.Length ?? 0; + public SecretProvider(PluginBase plugin, IConfigScope config) + { + if(config.TryGetValue("args", out JsonElement el)) + { + //Convert to dict + IReadOnlyDictionary<string, JsonElement> hashingArgs = el.EnumerateObject().ToDictionary(static k => k.Name, static v => v.Value); + + //Get hashing arguments + uint saltLen = hashingArgs["salt_len"].GetUInt32(); + uint hashLen = hashingArgs["hash_len"].GetUInt32(); + uint timeCost = hashingArgs["time_cost"].GetUInt32(); + uint memoryCost = hashingArgs["memory_cost"].GetUInt32(); + uint parallelism = hashingArgs["parallelism"].GetUInt32(); + //Load passwords + Passwords = new(this, (int)saltLen, timeCost, memoryCost, parallelism, hashLen); + } + else + { + Passwords = new(this); + } + } - public ERRNO GetSecret(Span<byte> buffer) + public SecretProvider(PluginBase plugin) { - if(_error != null) + Passwords = new(this); + } + + + public PasswordHashing Passwords { get; } + + ///<inheritdoc/> + public int BufferSize + { + get { - throw _error; + Check(); + return _pepper!.Length; } + } + + public ERRNO GetSecret(Span<byte> buffer) + { + Check(); //Coppy pepper to buffer _pepper.CopyTo(buffer); //Return pepper length return _pepper!.Length; } - public void LoadSecret(PluginBase pbase) + protected override void Check() + { + base.Check(); + if(_error != null) + { + throw _error; + } + } + + protected override void Free() { - _ = pbase.ObserveTask(() => LoadSecretInternal(pbase)); + //Clear the pepper if set + MemoryUtil.InitializeBlock(_pepper.AsSpan()); } - private async Task LoadSecretInternal(PluginBase pbase) + public async Task ConfigureServiceAsync(PluginBase plugin) { try { //Get the pepper from secret storage - _pepper = await pbase.TryGetSecretAsync(PASSWORD_HASHING_KEY).ToBase64Bytes(); - - //Regsiter cleanup - _ = pbase.RegisterForUnload(Clear); + _pepper = await plugin.TryGetSecretAsync(PASSWORD_HASHING_KEY).ToBase64Bytes(); } - catch(Exception ex) + catch (Exception ex) { //Store exception for re-propagation _error = ex; - } - } - public void Clear() - { - //Clear the pepper if set - MemoryUtil.InitializeBlock(_pepper.AsSpan()); + //Propagate exception to system + throw; + } } } } 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 { + /// <summary> /// Provides advanced QOL features to plugin loading /// </summary> public static class RoutingExtensions { private static readonly ConditionalWeakTable<IEndpoint, PluginBase?> _pluginRefs = new(); + /// <summary> /// 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<string, JsonElement>) }); + 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<string, JsonElement> 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<T>(PluginBase plugin, T endpointInstance, Type epType, IReadOnlyDictionary<string, JsonElement>? endpointLocalConfig) where T : IEndpoint + + private static void ScheduleIntervals<T>(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<Tuple<ConfigurableAsyncIntervalAttribute, AsyncSchedulableCallback>> confIntervals = epType.GetMethods() .Where(m => m.GetCustomAttribute<ConfigurableAsyncIntervalAttribute>() != null) .Select(m => new Tuple<ConfigurableAsyncIntervalAttribute, AsyncSchedulableCallback> @@ -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 -{ - /// <summary> - /// Contains extension methods for plugins to load the "users" system - /// </summary> - 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"; - - - /// <summary> - /// Gets or loads the plugin's ambient <see cref="IUserManager"/>, with the specified user-table name, - /// or the default table name - /// </summary> - /// <param name="plugin"></param> - /// <returns>The ambient <see cref="IUserManager"/> for the current plugin</returns> - /// <exception cref="KeyNotFoundException"></exception> - /// <exception cref="ObjectDisposedException"></exception> - 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<IUserManager> loader = pbase.LoadAssembly<IUserManager>(customAsm); - try - { - //Try to get the onload method - Action<object>? onLoadMethod = loader.TryGetMethod<Action<object>>(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 +{ + /// <summary> + /// Provides a singleton <see cref="IUserManager"/> service that dynamically loads + /// a user manager for the plugin. + /// </summary> + [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<IUserManager> loader = plugin.LoadAssembly<IUserManager>(customAsm); + try + { + //Try to get the onload method + Action<object>? onLoadMethod = loader.TryGetMethod<Action<object>>(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; + } + } + + ///<inheritdoc/> + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public Task<IUser> CreateUserAsync(string userid, string emailAddress, ulong privilages, PrivateString passHash, CancellationToken cancellation = default) + { + return _dynamicLoader.CreateUserAsync(userid, emailAddress, privilages, passHash, cancellation); + } + + ///<inheritdoc/> + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public Task<IUser?> GetUserAndPassFromEmailAsync(string emailAddress, CancellationToken cancellationToken = default) + { + return _dynamicLoader.GetUserAndPassFromEmailAsync(emailAddress, cancellationToken); + } + + ///<inheritdoc/> + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public Task<IUser?> GetUserAndPassFromIDAsync(string userid, CancellationToken cancellation = default) + { + return _dynamicLoader.GetUserAndPassFromIDAsync(userid, cancellation); + } + + ///<inheritdoc/> + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public Task<long> GetUserCountAsync(CancellationToken cancellation = default) + { + return _dynamicLoader.GetUserCountAsync(cancellation); + } + + ///<inheritdoc/> + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public Task<IUser?> GetUserFromEmailAsync(string emailAddress, CancellationToken cancellationToken = default) + { + return _dynamicLoader.GetUserFromEmailAsync(emailAddress, cancellationToken); + } + + ///<inheritdoc/> + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public Task<IUser?> GetUserFromIDAsync(string userId, CancellationToken cancellationToken = default) + { + return _dynamicLoader.GetUserFromIDAsync(userId, cancellationToken); + } + ///<inheritdoc/> + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public Task<ERRNO> 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 @@ <TargetFramework>net6.0</TargetFramework> <RootNamespace>VNLib.Plugins.Extensions.Loading</RootNamespace> <AssemblyName>VNLib.Plugins.Extensions.Loading</AssemblyName> - <Version>1.0.1.1</Version> <GenerateDocumentationFile>True</GenerateDocumentationFile> <Nullable>enable</Nullable> - <SignAssembly>True</SignAssembly> - <AssemblyOriginatorKeyFile>\\vaughnnugent.com\Internal\Folder Redirection\vman\Documents\Programming\Software\StrongNameingKey.snk</AssemblyOriginatorKeyFile> <AnalysisLevel>latest-all</AnalysisLevel> </PropertyGroup> @@ -18,6 +15,21 @@ <Authors>Vaughn Nugent</Authors> </PropertyGroup> + + <PropertyGroup> + <Authors>Vaughn Nugent</Authors> + <Company>Vaughn Nugent</Company> + <Product>VNLib.Plugins.Extensions.Loading</Product> + <PackageId>VNLib.Plugins.Extensions.Loading</PackageId> + <Description> + An Essentials framework extension library for common loading/configuration/service operations. Enables rapid plugin + and service development. + </Description> + <Copyright>Copyright © 2023 Vaughn Nugent</Copyright> + <PackageProjectUrl>https://www.vaughnnugent.com/resources/software/modules/VNLib.Plugins.Extensions</PackageProjectUrl> + <RepositoryUrl>https://github.com/VnUgE/VNLib.Plugins.Extensions/tree/master/lib/VNLib.Plugins.Extensions.Loading</RepositoryUrl> + </PropertyGroup> + <ItemGroup> <PackageReference Include="ErrorProne.NET.CoreAnalyzers" Version="0.1.2"> <PrivateAssets>all</PrivateAssets> @@ -27,7 +39,6 @@ <PrivateAssets>all</PrivateAssets> <IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets> </PackageReference> - <PackageReference Include="McMaster.NETCore.Plugins" Version="1.4.0" /> <PackageReference Include="VaultSharp" Version="1.12.2.1" /> </ItemGroup> @@ -37,4 +48,8 @@ <ProjectReference Include="..\..\..\..\..\core\lib\Utils\src\VNLib.Utils.csproj" /> </ItemGroup> + <ItemGroup> + <Folder Include="Routing\" /> + </ItemGroup> + </Project> 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<string, JsonElement>? conf = pbase.TryGetConfig(VAULT_OBJECT_NAME); + IConfigScope? conf = pbase.TryGetConfig(VAULT_OBJECT_NAME); if (conf == null) { |