diff options
Diffstat (limited to 'lib/VNLib.Plugins.Extensions.Loading')
6 files changed, 187 insertions, 36 deletions
diff --git a/lib/VNLib.Plugins.Extensions.Loading/src/ConfigurationExtensions.cs b/lib/VNLib.Plugins.Extensions.Loading/src/ConfigurationExtensions.cs index 39bdc86..e838822 100644 --- a/lib/VNLib.Plugins.Extensions.Loading/src/ConfigurationExtensions.cs +++ b/lib/VNLib.Plugins.Extensions.Loading/src/ConfigurationExtensions.cs @@ -96,23 +96,9 @@ namespace VNLib.Plugins.Extensions.Loading /// <exception cref="ConfigurationException"></exception> /// <exception cref="ObjectDisposedException"></exception> public static IConfigScope GetConfig(this PluginBase plugin, string propName) - { - plugin.ThrowIfUnloaded(); - try - { - //Try to get the element from the plugin config first - if (!plugin.PluginConfig.TryGetProperty(propName, out JsonElement el)) - { - //Fallback to the host config - el = plugin.HostConfig.GetProperty(propName); - } - //Get the top level config as a dictionary - return new ConfigScope(el, propName); - } - catch (KeyNotFoundException) - { - throw new ConfigurationException($"Missing required top level configuration object '{propName}', in host/plugin configuration files"); - } + { + return TryGetConfig(plugin, propName) + ?? throw new ConfigurationException($"Missing required top level configuration object '{propName}', in host/plugin configuration files"); } /// <summary> @@ -206,17 +192,21 @@ namespace VNLib.Plugins.Extensions.Loading T? value = getter(el); Validate.Assert(value is not null, $"Missing required configuration property '{property}' in config {config.ScopeName}"); + //Attempt to validate if the configuration inherits the interface + TryValidateConfig(value); + return value; } /// <summary> /// Gets a required configuration property from the specified configuration scope + /// and deserializes the json type. /// </summary> /// <typeparam name="T"></typeparam> /// <param name="config"></param> /// <param name="property">The name of the property to get</param> - /// <returns>The property value</returns> + /// <returns>The property value deserialzied into the desired object</returns> /// <exception cref="ArgumentNullException"></exception> /// <exception cref="ConfigurationException"></exception> public static T GetRequiredProperty<T>(this IConfigScope config, string property) @@ -380,7 +370,6 @@ namespace VNLib.Plugins.Extensions.Loading /// <returns>True if the plugin config contains the require configuration property</returns> public static bool HasConfigForType<T>(this PluginBase plugin) => HasConfigForType(plugin, typeof(T)); - /// <summary> /// Determines if the current plugin configuration contains the require properties to initialize /// the type @@ -417,7 +406,8 @@ namespace VNLib.Plugins.Extensions.Loading public static TConfig GetConfigElement<TConfig>(this PluginBase plugin) { //Deserialze the element - TConfig config = plugin.GetConfigForType<TConfig>().Deserialze<TConfig>(); + TConfig config = plugin.GetConfigForType<TConfig>() + .Deserialze<TConfig>(); TryValidateConfig(config); @@ -450,7 +440,8 @@ namespace VNLib.Plugins.Extensions.Loading public static TConfig GetConfigElement<TConfig>(this PluginBase plugin, string elementName) { //Deserialze the element - TConfig config = plugin.GetConfig(elementName).Deserialze<TConfig>(); + TConfig config = plugin.GetConfig(elementName) + .Deserialze<TConfig>(); TryValidateConfig(config); diff --git a/lib/VNLib.Plugins.Extensions.Loading/src/LoadingExtensions.cs b/lib/VNLib.Plugins.Extensions.Loading/src/LoadingExtensions.cs index 58478d4..ca897e6 100644 --- a/lib/VNLib.Plugins.Extensions.Loading/src/LoadingExtensions.cs +++ b/lib/VNLib.Plugins.Extensions.Loading/src/LoadingExtensions.cs @@ -26,6 +26,7 @@ using System; using System.IO; using System.Linq; using System.Text.Json; +using System.Threading; using System.Reflection; using System.Runtime.Loader; using System.Threading.Tasks; @@ -303,6 +304,12 @@ namespace VNLib.Plugins.Extensions.Loading //Optional delay await Task.Delay(delayMs); + //If plugin unloads during delay, bail + if (plugin.UnloadToken.IsCancellationRequested) + { + return; + } + //Run on ts Task deferred = Task.Run(asyncTask); @@ -357,7 +364,7 @@ namespace VNLib.Plugins.Extensions.Loading static async Task WaitForUnload(PluginBase pb, Action callback) { //Wait for unload as a task on the threadpool to avoid deadlocks - await pb.UnloadToken.WaitHandle.WaitAsync() + await pb.UnloadToken.WaitHandle.NoSpinWaitAsync(Timeout.Infinite) .ConfigureAwait(false); callback(); diff --git a/lib/VNLib.Plugins.Extensions.Loading/src/Routing/EndpointLogNameAttribute.cs b/lib/VNLib.Plugins.Extensions.Loading/src/Routing/EndpointLogNameAttribute.cs new file mode 100644 index 0000000..d47be22 --- /dev/null +++ b/lib/VNLib.Plugins.Extensions.Loading/src/Routing/EndpointLogNameAttribute.cs @@ -0,0 +1,41 @@ +/* +* Copyright (c) 2024 Vaughn Nugent +* +* Library: VNLib +* Package: VNLib.Plugins.Extensions.Loading +* File: ConfigurationExtensions.cs +* +* ConfigurationExtensions.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.Routing +{ + + /// <summary> + /// Defines configurable settings for an endpoint + /// </summary> + [AttributeUsage(AttributeTargets.Class)] + public sealed class EndpointLogNameAttribute(string logName) : Attribute + { + /// <summary> + /// The name of the logging scope for the endpoint + /// </summary> + public string LogName { get; } = logName; + } +} diff --git a/lib/VNLib.Plugins.Extensions.Loading/src/Routing/EndpointPathAttribute.cs b/lib/VNLib.Plugins.Extensions.Loading/src/Routing/EndpointPathAttribute.cs new file mode 100644 index 0000000..a5ab355 --- /dev/null +++ b/lib/VNLib.Plugins.Extensions.Loading/src/Routing/EndpointPathAttribute.cs @@ -0,0 +1,40 @@ +/* +* Copyright (c) 2024 Vaughn Nugent +* +* Library: VNLib +* Package: VNLib.Plugins.Extensions.Loading +* File: ConfigurationExtensions.cs +* +* ConfigurationExtensions.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.Routing +{ + /// <summary> + /// Defines configurable settings for an endpoint + /// </summary> + [AttributeUsage(AttributeTargets.Class)] + public sealed class EndpointPathAttribute(string path) : Attribute + { + /// <summary> + /// Sets the endpoint path (or configuration template if set) + /// </summary> + public string Path { get; } = path; + } +} diff --git a/lib/VNLib.Plugins.Extensions.Loading/src/Routing/RoutingExtensions.cs b/lib/VNLib.Plugins.Extensions.Loading/src/Routing/RoutingExtensions.cs index ab7dc58..6665a75 100644 --- a/lib/VNLib.Plugins.Extensions.Loading/src/Routing/RoutingExtensions.cs +++ b/lib/VNLib.Plugins.Extensions.Loading/src/Routing/RoutingExtensions.cs @@ -27,23 +27,24 @@ using System.Reflection; using System.Threading.Tasks; using System.Collections.Frozen; using System.Collections.Generic; +using System.Text.RegularExpressions; using System.Runtime.CompilerServices; using VNLib.Net.Http; using VNLib.Utils.Logging; +using VNLib.Utils.Resources; using VNLib.Plugins.Essentials.Runtime; using VNLib.Plugins.Essentials; using VNLib.Plugins.Essentials.Endpoints; using VNLib.Plugins.Extensions.Loading.Routing.Mvc; - namespace VNLib.Plugins.Extensions.Loading.Routing { /// <summary> /// Provides advanced QOL features to plugin loading /// </summary> - public static class RoutingExtensions + public static partial class RoutingExtensions { private static readonly ConditionalWeakTable<IEndpoint, PluginBase?> _pluginRefs = new(); private static readonly ConditionalWeakTable<PluginBase, EndpointCollection> _pluginEndpoints = new(); @@ -60,11 +61,14 @@ namespace VNLib.Plugins.Extensions.Loading.Routing T endpoint = plugin.CreateService<T>(); //Route the endpoint - plugin.Route(endpoint); + Route(plugin, endpoint); //Store ref to plugin for endpoint _pluginRefs.Add(endpoint, plugin); + //Function that initalizes the endpoint's path and logging variables + InitEndpointSettings(plugin, endpoint); + return endpoint; } @@ -104,17 +108,85 @@ namespace VNLib.Plugins.Extensions.Loading.Routing { _ = _pluginRefs.TryGetValue(ep, out PluginBase? pBase); return pBase ?? throw new InvalidOperationException("Endpoint was not dynamically routed"); - } + } + + private static readonly Regex ConfigSyntaxParser = ParserRegex(); + private delegate void InitFunc(string path, ILogProvider log); + + [GeneratedRegex("{{(.*?)}}", RegexOptions.Compiled)] + private static partial Regex ParserRegex(); + + private static void InitEndpointSettings<T>(PluginBase plugin, T endpoint) where T : IEndpoint + { + //Load optional config + IConfigScope config = plugin.GetConfigForType<T>(); + + ILogProvider logger = plugin.Log; + + EndpointPathAttribute? pathAttr = typeof(T).GetCustomAttribute<EndpointPathAttribute>(); + + /* + * gets the protected function for assigning the endpoint path + * and logger instance. + */ + InitFunc? initPathAndLog = ManagedLibrary.TryGetMethod<InitFunc>(endpoint, "InitPathAndLog", BindingFlags.NonPublic); + + if (pathAttr is null || initPathAndLog is null) + { + return; + } + + string? logName = typeof(T).GetCustomAttribute<EndpointLogNameAttribute>()?.LogName; + + if (!string.IsNullOrWhiteSpace(logName)) + { + logger = plugin.Log.CreateScope(SubsituteValue(logName, config)); + } + try + { + //Invoke init function and pass in variable names + initPathAndLog( + path: SubsituteValue(pathAttr.Path, config), + logger + ); + } + catch (ConfigurationException) + { + throw; + } + catch(Exception e) + { + throw new ConfigurationException($"Failed to initalize endpoint {endpoint.GetType().Name}", e); + } + + static string SubsituteValue(string pathVar, IConfigScope? config) + { + if (config is null) + { + return pathVar; + } + + // Replace the matched pattern with the corresponding value from the dictionary + return ConfigSyntaxParser.Replace(pathVar, match => + { + string varName = match.Groups[1].Value; + + //Get the value from the config scope or return the original variable unmodified + return config.GetValueOrDefault(varName, varName); + }); + } + } private sealed class EndpointCollection : IVirtualEndpointDefinition { public List<IEndpoint> Endpoints { get; } = new(); ///<inheritdoc/> - IEnumerable<IEndpoint> IVirtualEndpointDefinition.GetEndpoints() => Endpoints; + IEnumerable<IEndpoint> IVirtualEndpointDefinition.GetEndpoints() => Endpoints; } + private delegate ValueTask<VfReturnType> EndpointWorkFunc(HttpEntity entity); sealed record class HttpControllerEndpoint(MethodInfo MethodInfo, HttpEndpointAttribute Attr) diff --git a/lib/VNLib.Plugins.Extensions.Loading/src/Secrets/HCVaultClient.cs b/lib/VNLib.Plugins.Extensions.Loading/src/Secrets/HCVaultClient.cs index 885f22f..fae22c8 100644 --- a/lib/VNLib.Plugins.Extensions.Loading/src/Secrets/HCVaultClient.cs +++ b/lib/VNLib.Plugins.Extensions.Loading/src/Secrets/HCVaultClient.cs @@ -163,7 +163,7 @@ namespace VNLib.Plugins.Extensions.Loading using HttpResponseMessage response = await _client.SendAsync(ms, HttpCompletionOption.ResponseHeadersRead); //Check if an error occured in the response - await ProcessVaultErrorResponseAsync(response); + await ProcessVaultErrorResponseAsync(secretName, response); //Read the response async using SecretResponse res = await ReadSecretResponse(response.Content); @@ -266,7 +266,7 @@ namespace VNLib.Plugins.Extensions.Loading return null; } - private static ValueTask ProcessVaultErrorResponseAsync(HttpResponseMessage response) + private static ValueTask ProcessVaultErrorResponseAsync(string secretName, HttpResponseMessage response) { if (response.IsSuccessStatusCode) { @@ -278,7 +278,7 @@ namespace VNLib.Plugins.Extensions.Loading if(!ctLen.HasValue || ctLen.Value == 0) { return ValueTask.FromException( - new HttpRequestException($"Failed to fetch secret from vault with error code {response.StatusCode}") + new HttpRequestException($"Failed to fetch secret '{secretName}' from vault with error code {response.StatusCode}") ); } @@ -300,15 +300,15 @@ namespace VNLib.Plugins.Extensions.Loading ); } - return ExceptionsFromContentAsync(response); + return ExceptionsFromContentAsync(secretName, response); - static ValueTask ExceptionFromVaultErrors(HttpStatusCode code, VaultErrorMessage? errs) + static ValueTask ExceptionFromVaultErrors(string secretName, HttpStatusCode code, VaultErrorMessage? errs) { //If the error message is null, raise an exception if (errs?.Errors is null || errs.Errors.Length == 0) { return ValueTask.FromException( - new HttpRequestException($"Failed to fetch secret from vault with error code {code}") + new HttpRequestException($"Failed to fetch secret '{secretName}' from vault with error code {code}") ); } @@ -318,17 +318,17 @@ namespace VNLib.Plugins.Extensions.Loading //Finally raise the exception with all the returned errors return ValueTask.FromException( - new HttpRequestException($"Failed to fetch secre from vault with {code}, errors:\n {errStr}") + new HttpRequestException($"Failed to fetch secret `{secretName}` from vault with {code}, errors:\n {errStr}") ); } - static async ValueTask ExceptionsFromContentAsync(HttpResponseMessage response) + static async ValueTask ExceptionsFromContentAsync(string secretName, HttpResponseMessage response) { //Read stream async and deserialize async using Stream stream = await response.Content.ReadAsStreamAsync().ConfigureAwait(false); VaultErrorMessage? errs = await JsonSerializer.DeserializeAsync<VaultErrorMessage>(stream); - await ExceptionFromVaultErrors(response.StatusCode, errs); + await ExceptionFromVaultErrors(secretName, response.StatusCode, errs); } } |