aboutsummaryrefslogtreecommitdiff
path: root/lib/VNLib.Plugins.Extensions.Loading/src/LoadingExtensions.cs
diff options
context:
space:
mode:
Diffstat (limited to 'lib/VNLib.Plugins.Extensions.Loading/src/LoadingExtensions.cs')
-rw-r--r--lib/VNLib.Plugins.Extensions.Loading/src/LoadingExtensions.cs334
1 files changed, 334 insertions, 0 deletions
diff --git a/lib/VNLib.Plugins.Extensions.Loading/src/LoadingExtensions.cs b/lib/VNLib.Plugins.Extensions.Loading/src/LoadingExtensions.cs
new file mode 100644
index 0000000..c23f5e2
--- /dev/null
+++ b/lib/VNLib.Plugins.Extensions.Loading/src/LoadingExtensions.cs
@@ -0,0 +1,334 @@
+/*
+* Copyright (c) 2022 Vaughn Nugent
+*
+* Library: VNLib
+* Package: VNLib.Plugins.Extensions.Loading
+* File: LoadingExtensions.cs
+*
+* LoadingExtensions.cs is part of VNLib.Plugins.Extensions.Loading which is part of the larger
+* VNLib collection of libraries and utilities.
+*
+* VNLib.Plugins.Extensions.Loading is free software: you can redistribute it and/or modify
+* it under the terms of the GNU General Public License as published
+* by the Free Software Foundation, either version 2 of the License,
+* or (at your option) any later version.
+*
+* VNLib.Plugins.Extensions.Loading is distributed in the hope that it will be useful,
+* but WITHOUT ANY WARRANTY; without even the implied warranty of
+* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+* General Public License for more details.
+*
+* You should have received a copy of the GNU General Public License
+* along with VNLib.Plugins.Extensions.Loading. If not, see http://www.gnu.org/licenses/.
+*/
+
+using System;
+using System.IO;
+using System.Linq;
+using System.Text.Json;
+using System.Threading.Tasks;
+using System.Collections.Generic;
+using System.Security.Cryptography;
+using System.Runtime.CompilerServices;
+
+using VNLib.Utils;
+using VNLib.Utils.Logging;
+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
+ /// </summary>
+ public static class LoadingExtensions
+ {
+ public const string DEBUG_CONFIG_KEY = "debug";
+ public const string SECRETS_CONFIG_KEY = "secrets";
+ public const string PASSWORD_HASHING_KEY = "passwords";
+
+ /*
+ * Plugin local cache used for storing singletons for a plugin instance
+ */
+ private static readonly ConditionalWeakTable<PluginBase, PluginLocalCache> _localCache = new();
+
+ /// <summary>
+ /// Gets a previously cached service singleton for the desired plugin
+ /// </summary>
+ /// <param name="serviceType">The service instance type</param>
+ /// <param name="plugin">The plugin to obtain or build the singleton for</param>
+ /// <param name="serviceFactory">The method to produce the singleton</param>
+ /// <returns>The cached or newly created singleton</returns>
+ public static object GetOrCreateSingleton(PluginBase plugin, Type serviceType, Func<PluginBase, object> serviceFactory)
+ {
+ Lazy<object>? service;
+ //Get local cache
+ PluginLocalCache pc = _localCache.GetValue(plugin, PluginLocalCache.Create);
+ //Hold lock while get/set the singleton
+ lock (pc.SyncRoot)
+ {
+ //Check if service already exists
+ service = pc.GetService(serviceType);
+ //publish the service if it isnt loaded yet
+ service ??= pc.AddService(serviceType, serviceFactory);
+ }
+ //Deferred load of the service
+ return service.Value;
+ }
+
+ /// <summary>
+ /// Gets a previously cached service singleton for the desired plugin
+ /// or creates a new singleton instance for the plugin
+ /// </summary>
+ /// <typeparam name="T"></typeparam>
+ /// <param name="plugin">The plugin to obtain or build the singleton for</param>
+ /// <param name="serviceFactory">The method to produce the singleton</param>
+ /// <returns>The cached or newly created singleton</returns>
+ public static T GetOrCreateSingleton<T>(PluginBase plugin, Func<PluginBase, T> serviceFactory)
+ => (T)GetOrCreateSingleton(plugin, typeof(T), p => serviceFactory(p)!);
+
+
+ /// <summary>
+ /// Gets the plugins ambient <see cref="PasswordHashing"/> if loaded, or loads it if required. This class will
+ /// be unloaded when the plugin us unloaded.
+ /// </summary>
+ /// <param name="plugin"></param>
+ /// <returns>The ambient <see cref="PasswordHashing"/></returns>
+ /// <exception cref="OverflowException"></exception>
+ /// <exception cref="KeyNotFoundException"></exception>
+ /// <exception cref="ObjectDisposedException"></exception>
+ public static PasswordHashing GetPasswords(this PluginBase plugin)
+ {
+ plugin.ThrowIfUnloaded();
+ //Get/load the passwords one time only
+ return GetOrCreateSingleton(plugin, LoadPasswords);
+ }
+
+ private static PasswordHashing LoadPasswords(PluginBase plugin)
+ {
+ PasswordHashing Passwords;
+ //Get the global password system secret (pepper)
+ byte[] pepper = plugin.TryGetSecretAsync(PASSWORD_HASHING_KEY)
+ .ToBase64Bytes().Result ?? throw new KeyNotFoundException($"Missing required key '{PASSWORD_HASHING_KEY}' in secrets");
+
+ ERRNO cb(Span<byte> buffer)
+ {
+ //No longer valid peper if plugin is unloaded as its set to zero, so we need to protect it
+ plugin.ThrowIfUnloaded();
+
+ pepper.CopyTo(buffer);
+ return pepper.Length;
+ }
+
+ //See hashing params are defined
+ IReadOnlyDictionary<string, JsonElement>? hashingArgs = plugin.TryGetConfig(PASSWORD_HASHING_KEY);
+ if (hashingArgs != null)
+ {
+ //Get hashing arguments
+ uint saltLen = hashingArgs["salt_len"].GetUInt32();
+ uint hashLen = hashingArgs["hash_len"].GetUInt32();
+ uint timeCost = hashingArgs["time_cost"].GetUInt32();
+ uint memoryCost = hashingArgs["memory_cost"].GetUInt32();
+ uint parallelism = hashingArgs["parallelism"].GetUInt32();
+ //Load passwords
+ Passwords = new(cb, pepper.Length, (int)saltLen, timeCost, memoryCost, parallelism, hashLen);
+ }
+ else
+ {
+ //Init default password hashing
+ Passwords = new(cb, pepper.Length);
+ }
+
+ //Register event to cleanup the password class
+ _ = plugin.RegisterForUnload(() =>
+ {
+ //Zero the pepper
+ CryptographicOperations.ZeroMemory(pepper);
+ });
+ //return
+ return Passwords;
+ }
+
+
+ /// <summary>
+ /// Loads an assembly into the current plugins AppDomain and will unload when disposed
+ /// or the plugin is unloaded from the host application.
+ /// </summary>
+ /// <typeparam name="T">The desired exported type to load from the assembly</typeparam>
+ /// <param name="plugin"></param>
+ /// <param name="assemblyName">The name of the assembly (ex: 'file.dll') to search for</param>
+ /// <param name="dirSearchOption">Directory/file search option</param>
+ /// <returns>The <see cref="AssemblyLoader{T}"/> managing the loaded assmbly in the current AppDomain</returns>
+ /// <exception cref="ArgumentNullException"></exception>
+ /// <exception cref="FileNotFoundException"></exception>
+ /// <exception cref="EntryPointNotFoundException"></exception>
+ public static AssemblyLoader<T> LoadAssembly<T>(this PluginBase plugin, string assemblyName, SearchOption dirSearchOption = SearchOption.AllDirectories)
+ {
+ plugin.ThrowIfUnloaded();
+ _ = assemblyName ?? throw new ArgumentNullException(nameof(assemblyName));
+
+ //get plugin directory from config
+ IReadOnlyDictionary<string, JsonElement> config = plugin.GetConfig("plugins");
+ string? pluginsBaseDir = config["path"].GetString();
+
+ /*
+ * This should never happen since this method can only be called from a
+ * plugin context, which means this path was used to load the current plugin
+ */
+ _ = pluginsBaseDir ?? throw new ArgumentNullException("path", "No plugin path is defined for the current host configuration, this is likely a bug");
+
+ //Get the first file that matches the search file
+ string? asmFile = Directory.EnumerateFiles(pluginsBaseDir, assemblyName, dirSearchOption).FirstOrDefault();
+ _ = asmFile ?? throw new FileNotFoundException($"Failed to load custom assembly {assemblyName} from plugin directory");
+
+ //Load the assembly
+ return AssemblyLoader<T>.Load(asmFile, plugin.UnloadToken);
+ }
+
+
+ /// <summary>
+ /// Determintes if the current plugin config has a debug propety set
+ /// </summary>
+ /// <param name="plugin"></param>
+ /// <returns>True if debug mode is enabled, false otherwise</returns>
+ /// <exception cref="ObjectDisposedException"></exception>
+ public static bool IsDebug(this PluginBase plugin)
+ {
+ plugin.ThrowIfUnloaded();
+ //Check for debug element
+ return plugin.PluginConfig.TryGetProperty(DEBUG_CONFIG_KEY, out JsonElement dbgEl) && dbgEl.GetBoolean();
+ }
+
+ /// <summary>
+ /// Internal exception helper to raise <see cref="ObjectDisposedException"/> if the plugin has been unlaoded
+ /// </summary>
+ /// <param name="plugin"></param>
+ /// <exception cref="ObjectDisposedException"></exception>
+ public static void ThrowIfUnloaded(this PluginBase plugin)
+ {
+ //See if the plugin was unlaoded
+ if (plugin.UnloadToken.IsCancellationRequested)
+ {
+ throw new ObjectDisposedException("The plugin has been unloaded");
+ }
+ }
+
+ /// <summary>
+ /// Schedules an asynchronous callback function to run and its results will be observed
+ /// when the operation completes, or when the plugin is unloading
+ /// </summary>
+ /// <param name="plugin"></param>
+ /// <param name="asyncTask">The asynchronous operation to observe</param>
+ /// <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 DeferTask(this PluginBase plugin, Func<Task> asyncTask, int delayMs = 0)
+ {
+ /*
+ * Motivation:
+ * Sometimes during plugin loading, a plugin may want to asynchronously load
+ * data, where the results are not required to be observed during loading, but
+ * should not be pending after the plugin is unloaded, as the assembly may be
+ * unloaded and referrences collected by the GC.
+ *
+ * So we can use the plugin's unload cancellation token to observe the results
+ * of a pending async operation
+ */
+
+ //Test status
+ plugin.ThrowIfUnloaded();
+
+ //Optional delay
+ await Task.Delay(delayMs);
+
+ //Run on ts
+ Task deferred = Task.Run(asyncTask);
+
+ //Add task to deferred list
+ plugin.ObserveTask(deferred);
+ try
+ {
+ //Await the task results
+ await deferred.ConfigureAwait(false);
+ }
+ catch(Exception ex)
+ {
+ //Log errors
+ plugin.Log.Error(ex, "Error occured while observing deferred task");
+ }
+ finally
+ {
+ //Remove task when complete
+ plugin.RemoveObservedTask(deferred);
+ }
+ }
+
+ /// <summary>
+ /// Registers an event to occur when the plugin is unloaded on a background thread
+ /// and will cause the Plugin.Unload() method to block until the event completes
+ /// </summary>
+ /// <param name="pbase"></param>
+ /// <param name="callback">The method to call when the plugin is unloaded</param>
+ /// <returns>A task that represents the registered work</returns>
+ /// <exception cref="ArgumentNullException"></exception>
+ /// <exception cref="ObjectDisposedException"></exception>
+ public static Task RegisterForUnload(this PluginBase pbase, Action callback)
+ {
+ //Test status
+ pbase.ThrowIfUnloaded();
+ _ = callback ?? throw new ArgumentNullException(nameof(callback));
+
+ //Wait method
+ static async Task WaitForUnload(PluginBase pb, Action callback)
+ {
+ //Wait for unload as a task on the threadpool to avoid deadlocks
+ await pb.UnloadToken.WaitHandle.WaitAsync()
+ .ConfigureAwait(false);
+
+ callback();
+ }
+
+ //Registaer the task to cause the plugin to wait
+ return pbase.DeferTask(() => WaitForUnload(pbase, callback));
+ }
+
+
+ private sealed class PluginLocalCache
+ {
+ private readonly PluginBase _plugin;
+
+ private readonly Dictionary<Type, Lazy<object>> _store;
+
+ public object SyncRoot { get; } = new();
+
+ private PluginLocalCache(PluginBase plugin)
+ {
+ _plugin = plugin;
+ _store = new();
+ //Register cleanup on unload
+ _ = _plugin.RegisterForUnload(() => _store.Clear());
+ }
+
+ public static PluginLocalCache Create(PluginBase plugin) => new(plugin);
+
+
+ public Lazy<object>? GetService(Type serviceType)
+ {
+ Lazy<object>? t = _store.Where(t => t.Key.IsAssignableTo(serviceType))
+ .Select(static tk => tk.Value)
+ .FirstOrDefault();
+ return t;
+ }
+
+ public Lazy<object> AddService(Type serviceType, Func<PluginBase, object> factory)
+ {
+ //Get lazy loader to invoke factory outside of cache lock
+ Lazy<object> lazyFactory = new(() => factory(_plugin), true);
+ //Store lazy factory
+ _store.Add(serviceType, lazyFactory);
+ //Pass the lazy factory back
+ return lazyFactory;
+ }
+ }
+ }
+}