aboutsummaryrefslogtreecommitdiff
path: root/VNLib.Plugins.Extensions.Loading/LoadingExtensions.cs
diff options
context:
space:
mode:
Diffstat (limited to 'VNLib.Plugins.Extensions.Loading/LoadingExtensions.cs')
-rw-r--r--VNLib.Plugins.Extensions.Loading/LoadingExtensions.cs334
1 files changed, 0 insertions, 334 deletions
diff --git a/VNLib.Plugins.Extensions.Loading/LoadingExtensions.cs b/VNLib.Plugins.Extensions.Loading/LoadingExtensions.cs
deleted file mode 100644
index c23f5e2..0000000
--- a/VNLib.Plugins.Extensions.Loading/LoadingExtensions.cs
+++ /dev/null
@@ -1,334 +0,0 @@
-/*
-* 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;
- }
- }
- }
-}