diff options
9 files changed, 227 insertions, 119 deletions
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 065d93f..05a6df2 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 @@ -41,7 +41,7 @@ <PrivateAssets>all</PrivateAssets> <IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets> </PackageReference> - <PackageReference Include="Microsoft.EntityFrameworkCore" Version="6.0.16" /> + <PackageReference Include="Microsoft.EntityFrameworkCore" Version="6.0.20" /> </ItemGroup> <ItemGroup> 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 6422776..abfbfb6 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 @@ -33,9 +33,9 @@ </PropertyGroup> <ItemGroup> - <PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite" Version="6.0.16" /> - <PackageReference Include="Microsoft.EntityFrameworkCore.SqlServer" Version="6.0.16" /> - <PackageReference Include="MySqlConnector" Version="2.2.6" /> + <PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite" Version="6.0.20" /> + <PackageReference Include="Microsoft.EntityFrameworkCore.SqlServer" Version="6.0.20" /> + <PackageReference Include="MySqlConnector" Version="2.2.7" /> <PackageReference Include="Pomelo.EntityFrameworkCore.MySql" Version="6.0.2" /> </ItemGroup> diff --git a/lib/VNLib.Plugins.Extensions.Loading/src/AssemblyLoader.cs b/lib/VNLib.Plugins.Extensions.Loading/src/AssemblyLoader.cs index e01b32d..cd2dbad 100644 --- a/lib/VNLib.Plugins.Extensions.Loading/src/AssemblyLoader.cs +++ b/lib/VNLib.Plugins.Extensions.Loading/src/AssemblyLoader.cs @@ -24,21 +24,20 @@ using System; using System.IO; -using System.Linq; using System.Threading; using System.Reflection; using System.Runtime.Loader; -using System.Runtime.InteropServices; using VNLib.Utils.IO; using VNLib.Utils.Resources; namespace VNLib.Plugins.Extensions.Loading { + /// <summary> /// <para> /// Represents a disposable assembly loader wrapper for - /// exporting a signle type from a loaded assembly + /// exporting a single type from a loaded assembly /// </para> /// <para> /// If the loaded type implements <see cref="IDisposable"/> the @@ -46,79 +45,26 @@ namespace VNLib.Plugins.Extensions.Loading /// </para> /// </summary> /// <typeparam name="T">The exported type to manage</typeparam> - public sealed class AssemblyLoader<T> : OpenResourceHandle<T> + public sealed class AssemblyLoader<T> : ManagedLibrary, IDisposable { private readonly CancellationTokenRegistration _reg; private readonly Lazy<T> _instance; - private readonly AssemblyLoadContext _loadContext; - private readonly AssemblyDependencyResolver _resolver; - private readonly string _assemblyPath; + private bool disposedValue; /// <summary> /// The instance of the loaded type /// </summary> - public override T Resource => _instance.Value; + public T Resource => _instance.Value; private AssemblyLoader(string assemblyPath, AssemblyLoadContext parentContext, CancellationToken unloadToken) + :base(assemblyPath, parentContext) { - _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); + //Init lazy type loader + _instance = new(LoadTypeFromAssembly<T>, 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 - /// </summary> - /// <returns>The desired type instance</returns> - /// <exception cref="EntryPointNotFoundException"></exception> - private T LoadAndGetExportedType() - { - //Load the assembly into the parent context - Assembly asm = _loadContext.LoadFromAssemblyPath(_assemblyPath); - - Type resourceType = typeof(T); - - //See if the type is exported - Type exp = (from type in asm.GetExportedTypes() - where resourceType.IsAssignableFrom(type) - select type) - .FirstOrDefault() - ?? throw new EntryPointNotFoundException($"Imported assembly does not export desired type {resourceType.FullName}"); - //Create instance - return (T)Activator.CreateInstance(exp)!; - } + /// <summary> /// Creates a method delegate for the given method name from @@ -132,24 +78,53 @@ namespace VNLib.Plugins.Extensions.Loading public TDelegate? TryGetMethod<TDelegate>(string methodName) where TDelegate : Delegate { //get the type info of the actual resource - return Resource!.GetType() + return Resource.GetType() .GetMethod(methodName, BindingFlags.Public | BindingFlags.Instance) ?.CreateDelegate<TDelegate>(Resource); } - ///<inheritdoc/> - protected override void Free() + private void Dispose(bool disposing) { - //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) + if (!disposedValue) { - (_instance.Value as IDisposable)?.Dispose(); + //Call base unload during dispose (or finalize) + OnUnload(); + + //Always cleanup registration + _reg.Dispose(); + + if (disposing) + { + //If the instance is disposable, call its dispose method on unload + if (_instance.IsValueCreated && _instance.Value is IDisposable) + { + (_instance.Value as IDisposable)?.Dispose(); + } + } + + disposedValue = true; } - _reg.Dispose(); + } + + /// <summary> + /// Cleans up any unused internals + /// </summary> + ~AssemblyLoader() + { + // Do not change this code. Put cleanup code in 'Dispose(bool disposing)' method + Dispose(disposing: false); + } + + + /// <summary> + /// Disposes the assembly loader and cleans up resources. If the <typeparamref name="T"/> + /// inherits <see cref="IDisposable"/> the intrance is disposed. + /// </summary> + public void Dispose() + { + // Do not change this code. Put cleanup code in 'Dispose(bool disposing)' method + Dispose(disposing: true); + GC.SuppressFinalize(this); } /// <summary> @@ -171,8 +146,9 @@ namespace VNLib.Plugins.Extensions.Loading throw new FileNotFoundException($"The desired assembly {assemblyName} could not be found at the file path"); } - return new(assemblyName, loadContext, unloadToken); + //Create the loader from its absolute file path + FileInfo fi = new(assemblyName); + return new(fi.FullName, loadContext, unloadToken); } - } } diff --git a/lib/VNLib.Plugins.Extensions.Loading/src/ConfigurationExtensions.cs b/lib/VNLib.Plugins.Extensions.Loading/src/ConfigurationExtensions.cs index fbe8d48..adfd997 100644 --- a/lib/VNLib.Plugins.Extensions.Loading/src/ConfigurationExtensions.cs +++ b/lib/VNLib.Plugins.Extensions.Loading/src/ConfigurationExtensions.cs @@ -134,7 +134,8 @@ namespace VNLib.Plugins.Extensions.Loading { 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)) + if (plugin.PluginConfig.TryGetProperty(propName, out JsonElement el) + || plugin.HostConfig.TryGetProperty(propName, out el)) { //Get the top level config as a dictionary return new ConfigScope(el, propName); @@ -202,7 +203,7 @@ namespace VNLib.Plugins.Extensions.Loading string? configName = GetConfigNameForType(type); if (configName != null) { - throw new KeyNotFoundException($"Missing required configuration key {configName} for type {type.Name}"); + throw new KeyNotFoundException($"Missing required configuration key '{configName}' for type {type.Name}"); } else { @@ -259,7 +260,10 @@ namespace VNLib.Plugins.Extensions.Loading Type type = typeof(T); ConfigurationNameAttribute? configName = type.GetCustomAttribute<ConfigurationNameAttribute>(); //See if the plugin contains a configuration varables - return configName != null && plugin.PluginConfig.TryGetProperty(configName.ConfigVarName, out _); + return configName != null && ( + plugin.PluginConfig.TryGetProperty(configName.ConfigVarName, out _) || + plugin.HostConfig.TryGetProperty(configName.ConfigVarName, out _) + ); } /// <summary> diff --git a/lib/VNLib.Plugins.Extensions.Loading/src/LoadingExtensions.cs b/lib/VNLib.Plugins.Extensions.Loading/src/LoadingExtensions.cs index 8f7dee8..3da015e 100644 --- a/lib/VNLib.Plugins.Extensions.Loading/src/LoadingExtensions.cs +++ b/lib/VNLib.Plugins.Extensions.Loading/src/LoadingExtensions.cs @@ -561,33 +561,40 @@ namespace VNLib.Plugins.Extensions.Loading object service; - //Determin configuration requirments - if (ConfigurationExtensions.ConfigurationRequired(serviceType) || config != null) + try { - if (config == null) + //Determin configuration requirments + if (ConfigurationExtensions.ConfigurationRequired(serviceType) || config != null) { - ConfigurationExtensions.ThrowConfigNotFoundForType(serviceType); - } + if (config == null) + { + ConfigurationExtensions.ThrowConfigNotFoundForType(serviceType); + } - //Get the constructor for required or available config - ConstructorInfo? constructor = serviceType.GetConstructor(new Type[] { typeof(PluginBase), typeof(IConfigScope) }); + //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}"); + //Make sure the constructor exists + _ = constructor ?? throw new EntryPointNotFoundException($"No constructor found for {serviceType.Name}"); - //Call constructore - service = constructor.Invoke(new object[2] { plugin, config }); - } - else - { - //Get the constructor - ConstructorInfo? constructor = serviceType.GetConstructor(new Type[] { typeof(PluginBase) }); + //Call constructore + service = 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}"); + //Make sure the constructor exists + _ = constructor ?? throw new EntryPointNotFoundException($"No constructor found for {serviceType.Name}"); - //Call constructore - service = constructor.Invoke(new object[1] { plugin }); + //Call constructore + service = constructor.Invoke(new object[1] { plugin }); + } + } + catch(TargetInvocationException te) when (te.InnerException != null) + { + throw te.InnerException; } Task? loading = null; diff --git a/lib/VNLib.Plugins.Extensions.Loading/src/LoggingExtensions.cs b/lib/VNLib.Plugins.Extensions.Loading/src/LoggingExtensions.cs new file mode 100644 index 0000000..47ecdf0 --- /dev/null +++ b/lib/VNLib.Plugins.Extensions.Loading/src/LoggingExtensions.cs @@ -0,0 +1,84 @@ +/* +* Copyright (c) 2023 Vaughn Nugent +* +* Library: VNLib +* Package: VNLib.Plugins.Extensions.Loading +* File: LoggingExtensions.cs +* +* LoggingExtensions.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 VNLib.Utils.Logging; + +namespace VNLib.Plugins.Extensions.Loading +{ + /// <summary> + /// Provides advanced QOL features for event logging + /// </summary> + public static class LoggingExtensions + { + /// <summary> + /// Creates a new <see cref="ILogProvider"/> that scopes all log messages to the specified name + /// when writing messages + /// </summary> + /// <param name="log"></param> + /// <param name="scopeName">The name of the scope to print log values to</param> + /// <returns>The scoped log provider instance</returns> + public static ILogProvider CreateScope(this ILogProvider log, string scopeName) + { + return new ScopeLogProvider(log, scopeName); + } + + private sealed record class ScopeLogProvider(ILogProvider Log, string ScopeName) : ILogProvider + { + ///<inheritdoc/> + public void Flush() => Log.Flush(); + + ///<inheritdoc/> + public object GetLogProvider() => Log.GetLogProvider(); + + ///<inheritdoc/> + public bool IsEnabled(LogLevel level) => Log.IsEnabled(level); + + ///<inheritdoc/> + public void Write(LogLevel level, string value) + { + Log.Write(level, $"[{ScopeName}]: {value}"); + } + + ///<inheritdoc/> + public void Write(LogLevel level, Exception exception, string value = "") + { + Log.Write(level, exception, $"[{ScopeName}]: {value}"); + } + + ///<inheritdoc/> + public void Write(LogLevel level, string value, params object?[] args) + { + Log.Write(level, $"[{ScopeName}]: {value}", args); + } + + ///<inheritdoc/> + public void Write(LogLevel level, string value, params ValueType[] args) + { + Log.Write(level, $"[{ScopeName}]: {value}", args); + } + } + } +} diff --git a/lib/VNLib.Plugins.Extensions.Loading/src/RoutingExtensions.cs b/lib/VNLib.Plugins.Extensions.Loading/src/RoutingExtensions.cs index 22686f0..7731bcf 100644 --- a/lib/VNLib.Plugins.Extensions.Loading/src/RoutingExtensions.cs +++ b/lib/VNLib.Plugins.Extensions.Loading/src/RoutingExtensions.cs @@ -46,9 +46,10 @@ namespace VNLib.Plugins.Extensions.Loading.Routing /// </summary> /// <typeparam name="T">The <see cref="IEndpoint"/> type</typeparam> /// <param name="plugin"></param> + /// <param name="configRequired">If true, requires the configuration exist in the config file</param> /// <param name="pluginConfigPathName">The path to the plugin sepcific configuration property</param> /// <exception cref="TargetInvocationException"></exception> - public static T Route<T>(this PluginBase plugin, string? pluginConfigPathName) where T : IEndpoint + public static T Route<T>(this PluginBase plugin, string? pluginConfigPathName, bool configRequired = true) where T : IEndpoint { Type endpointType = typeof(T); @@ -59,7 +60,7 @@ namespace VNLib.Plugins.Extensions.Loading.Routing { ConstructorInfo? constructor = endpointType.GetConstructor(new Type[] { typeof(PluginBase) }); - _ = constructor ?? throw new EntryPointNotFoundException($"No constructor found for {endpointType.Name}"); + _ = constructor ?? throw new EntryPointNotFoundException($"No constructor t(PluginBase p) found for {endpointType.Name}"); //Create the new endpoint and pass the plugin instance endpoint = (T)constructor.Invoke(new object[] { plugin }); @@ -69,19 +70,42 @@ namespace VNLib.Plugins.Extensions.Loading.Routing } else { - ConstructorInfo? constructor = endpointType.GetConstructor(new Type[] { typeof(PluginBase), typeof(IConfigScope) }); + //Try to get config but allow null if not required + IConfigScope? config = plugin.TryGetConfig(pluginConfigPathName); - //Make sure the constructor exists - _ = constructor ?? throw new EntryPointNotFoundException($"No constructor found for {endpointType.Name}"); + if(configRequired && config == null) + { + ConfigurationExtensions.ThrowConfigNotFoundForType(endpointType); + return default; + } - //Get config variables for the endpoint - IConfigScope conf = plugin.GetConfig(pluginConfigPathName); + //Choose constructor based on config + if (config != null) + { + ConstructorInfo? constructor = endpointType.GetConstructor(new Type[] { typeof(PluginBase), typeof(IConfigScope) }); - //Create the new endpoint and pass the plugin instance along with the configuration object - endpoint = (T)constructor.Invoke(new object[] { plugin, conf }); + //Make sure the constructor exists + _ = constructor ?? throw new EntryPointNotFoundException($"No constructor t(PluginBase p, IConfigScope cs) found for {endpointType.Name}"); - //Register event handlers for the endpoint - ScheduleIntervals(plugin, endpoint, endpointType, conf); + //Create the new endpoint and pass the plugin instance along with the configuration object + endpoint = (T)constructor.Invoke(new object[] { plugin, config }); + + //Register event handlers for the endpoint + ScheduleIntervals(plugin, endpoint, endpointType, config); + } + else + { + //Config does not exist, so use the default constructor + ConstructorInfo? constructor = endpointType.GetConstructor(new Type[] { typeof(PluginBase) }); + + _ = constructor ?? throw new EntryPointNotFoundException($"No constructor t(PluginBase p) found for {endpointType.Name}"); + + //Create the new endpoint and pass the plugin instance + endpoint = (T)constructor.Invoke(new object[] { plugin }); + + //Register event handlers for the endpoint + ScheduleIntervals(plugin, endpoint, endpointType, null); + } } //Route the endpoint @@ -112,7 +136,7 @@ namespace VNLib.Plugins.Extensions.Loading.Routing //Get config name attribute ConfigurationNameAttribute? configAttr = endpointType.GetCustomAttribute<ConfigurationNameAttribute>(); //Route using attribute - return plugin.Route<T>(configAttr?.ConfigVarName); + return plugin.Route<T>(configAttr?.ConfigVarName, configAttr?.Required == true); } /// <summary> diff --git a/lib/VNLib.Plugins.Extensions.Loading/src/Secrets/VaultSecrets.cs b/lib/VNLib.Plugins.Extensions.Loading/src/Secrets/VaultSecrets.cs index 711ae50..08af485 100644 --- a/lib/VNLib.Plugins.Extensions.Loading/src/Secrets/VaultSecrets.cs +++ b/lib/VNLib.Plugins.Extensions.Loading/src/Secrets/VaultSecrets.cs @@ -61,6 +61,7 @@ namespace VNLib.Plugins.Extensions.Loading public const string VAULT_URL_KEY = "url"; public const string VAULT_URL_SCHEME = "vault://"; + public const string ENV_URL_SCHEME = "env://"; /// <summary> @@ -110,12 +111,23 @@ namespace VNLib.Plugins.Extensions.Loading } //Secret is a vault path, or return the raw value - if (!rawSecret.StartsWith(VAULT_URL_SCHEME, StringComparison.OrdinalIgnoreCase)) + if (rawSecret.StartsWith(VAULT_URL_SCHEME, StringComparison.OrdinalIgnoreCase)) + { + return GetSecretFromVaultAsync(plugin, rawSecret); + } + + //See if the secret is an environment variable path + if (rawSecret.StartsWith(ENV_URL_SCHEME, StringComparison.OrdinalIgnoreCase)) { - return Task.FromResult<ISecretResult?>(new SecretResult(rawSecret.AsSpan())); + //try to get the environment variable + string envVar = rawSecret[ENV_URL_SCHEME.Length..]; + string? envVal = Environment.GetEnvironmentVariable(envVar); + + return Task.FromResult<ISecretResult?>(envVal == null ? null : new SecretResult(envVal)); } - return GetSecretFromVaultAsync(plugin, rawSecret); + //Finally, return the raw value + return Task.FromResult<ISecretResult?>(new SecretResult(rawSecret.AsSpan())); } /// <summary> @@ -197,6 +209,7 @@ namespace VNLib.Plugins.Extensions.Loading { return Task.FromResult<X509Certificate?>(new (rawSecret)); } + return GetCertFromVaultAsync(plugin, rawSecret); } 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 aced03a..1747a7d 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 @@ -34,7 +34,7 @@ <PrivateAssets>all</PrivateAssets> <IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets> </PackageReference> - <PackageReference Include="FluentValidation" Version="11.5.2" /> + <PackageReference Include="FluentValidation" Version="11.6.0" /> </ItemGroup> <ItemGroup> |