/* * Copyright (c) 2023 Vaughn Nugent * * Library: VNLib * Package: VNLib.Plugins.Essentials.ServiceStack * File: PluginManager.cs * * PluginManager.cs is part of VNLib.Plugins.Essentials.ServiceStack which * is part of the larger VNLib collection of libraries and utilities. * * VNLib.Plugins.Essentials.ServiceStack 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 2 of the * License, or (at your option) any later version. * * VNLib.Plugins.Essentials.ServiceStack 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.IO; using System.Linq; using System.Diagnostics; using System.Threading.Tasks; using System.Collections.Generic; using VNLib.Utils; using VNLib.Utils.IO; using VNLib.Utils.Logging; using VNLib.Utils.Extensions; using VNLib.Plugins.Runtime; namespace VNLib.Plugins.Essentials.ServiceStack { /// /// A sealed type that manages the plugin interaction layer. Manages the lifetime of plugin /// instances, exposes controls, and relays stateful plugin events. /// internal sealed class PluginManager : VnDisposeable, IPluginManager, IPluginEventListener { private const string PLUGIN_FILE_EXTENSION = ".dll"; private readonly List _plugins; private readonly IReadOnlyCollection _dependents; private IEnumerable _livePlugins => _plugins.SelectMany(static p => p.Controller.Plugins); /// /// The collection of internal controllers /// public IEnumerable Plugins => _plugins; public PluginManager(IReadOnlyCollection dependents) { _plugins = new(); _dependents = dependents; } /// /// public void LoadPlugins(IPluginLoadConfiguration config, ILogProvider appLog) { Check(); //Load all virtual file assemblies withing the plugin folder DirectoryInfo dir = new(config.PluginDir); if (!dir.Exists) { appLog.Warn("Plugin directory {dir} does not exist. No plugins were loaded", config.PluginDir); return; } appLog.Information("Loading managed plugins"); //Enumerate all dll files within this dir IEnumerable dirs = dir.EnumerateDirectories("*", SearchOption.TopDirectoryOnly); //Select only dirs with a dll that is named after the directory name IEnumerable pluginPaths = GetPluginPaths(dirs); IEnumerable pluginFileNames = pluginPaths.Select(static s => $"{Path.GetFileName(s)}\n"); appLog.Debug("Found plugin files: \n{files}", string.Concat(pluginFileNames)); /* * We need to get the assembly loader for the plugin file, then create its * RuntimePluginLoader which will be passed to the Managed plugin instance */ ManagedPlugin[] wrappers = pluginPaths.Select(pw => config.AssemblyLoaderFactory.GetLoaderForPluginFile(pw)) .Select(l => new RuntimePluginLoader(l, config.HostConfig, config.PluginErrorLog)) .Select(loader => new ManagedPlugin(loader, this)) .ToArray(); //Add to loaded plugins _plugins.AddRange(wrappers); //Load plugins InitiailzeAndLoad(appLog); } private static IEnumerable GetPluginPaths(IEnumerable dirs) { //Select only dirs with a dll that is named after the directory name return dirs.Where(static pdir => { string compined = Path.Combine(pdir.FullName, pdir.Name); string FilePath = string.Concat(compined, PLUGIN_FILE_EXTENSION); return FileOperations.FileExists(FilePath); }) //Return the name of the dll file to import .Select(static pdir => { string compined = Path.Combine(pdir.FullName, pdir.Name); return string.Concat(compined, PLUGIN_FILE_EXTENSION); }); } private void InitiailzeAndLoad(ILogProvider debugLog) { //Load all async _plugins.ToArray().TryForeach(p => InitializePlugin(p, debugLog)); //Load stage, load all multithreaded Parallel.ForEach(_plugins, p => LoadPlugin(p, debugLog)); debugLog.Information("Plugin loading completed"); } private void InitializePlugin(ManagedPlugin plugin, ILogProvider debugLog) { void LogAndRemovePlugin(Exception ex) { debugLog.Error(ex, $"Exception raised during initialzation of {plugin.PluginFileName}. It has been removed from the collection\n{ex}"); //Remove the plugin from the list while locking it lock (_plugins) { _plugins.Remove(plugin); } //Dispose the plugin plugin.Dispose(); } try { //Load wrapper plugin.InitializePlugins(); } catch (Exception ex) { LogAndRemovePlugin(ex); } } private static void LoadPlugin(ManagedPlugin plugin, ILogProvider debugLog) { Stopwatch sw = new(); try { sw.Start(); //Load wrapper plugin.LoadPlugins(); sw.Stop(); /* * If the plugin assembly does not expose any plugin types or there is an issue loading the assembly, * its types my not unify, then we should give the user feedback insead of a silent fail. */ if (!plugin.Controller.Plugins.Any()) { debugLog.Warn("No plugin instances were exposed via {ams} assembly. This may be due to an assebmly mismatch", plugin.PluginFileName); } else { debugLog.Verbose("Loaded {pl} in {tm} ms", plugin.PluginFileName, sw.ElapsedMilliseconds); } } catch (Exception ex) { debugLog.Error(ex, $"Exception raised during loading {plugin.PluginFileName}. Failed to load plugin \n{ex}"); } finally { sw.Stop(); } } /// public bool SendCommandToPlugin(string pluginName, string message, StringComparison nameComparison = StringComparison.Ordinal) { Check(); //Find the single plugin by its name LivePlugin? pl = _livePlugins.Where(p => pluginName.Equals(p.PluginName, nameComparison)).SingleOrDefault(); //Send the command return pl?.SendConsoleMessage(message) ?? false; } /// public void ForceReloadAllPlugins() { //Reload all plugin managers _plugins.TryForeach(static p => p.ReloadPlugins()); } /// public void UnloadPlugins() { //Unload all plugin controllers _plugins.TryForeach(static p => p.UnloadPlugins()); /* * All plugin instances must be destroyed because the * only way they will be loaded is from their files * again, so they must be released */ _plugins.TryForeach(static p => p.Dispose()); _plugins.Clear(); } protected override void Free() { //Cleanup on dispose if unload failed _plugins.TryForeach(static p => p.Dispose()); _plugins.Clear(); } void IPluginEventListener.OnPluginLoaded(PluginController controller, object? state) { //Get event listeners at event time because deps may be modified by the domain ServiceGroup[] deps = _dependents.Select(static d => d).ToArray(); //run onload method deps.TryForeach(d => d.OnPluginLoaded((IManagedPlugin)state!)); } void IPluginEventListener.OnPluginUnloaded(PluginController controller, object? state) { //Get event listeners at event time because deps may be modified by the domain ServiceGroup[] deps = _dependents.Select(static d => d).ToArray(); //Run unloaded method deps.TryForeach(d => d.OnPluginUnloaded((IManagedPlugin)state!)); } } }