/*
* 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 System.Runtime.CompilerServices;
using VNLib.Utils;
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, IHttpPluginManager, IPluginEventListener
{
private readonly ConditionalWeakTable _managedPlugins;
private readonly ServiceDomain _dependents;
private readonly IPluginStack _stack;
private IEnumerable _livePlugins => _managedPlugins.SelectMany(static p => p.Key.Plugins);
///
/// The collection of internal controllers
///
public IEnumerable Plugins => _managedPlugins.Select(static p => p.Value);
public PluginManager(ServiceDomain dependents, IPluginStack stack)
{
_dependents = dependents;
_stack = stack;
_managedPlugins = new();
}
///
/// Configures the manager to capture and manage plugins within a plugin stack
///
///
public void LoadPlugins(ILogProvider debugLog)
{
_ = _stack ?? throw new InvalidOperationException("Plugin stack has not been set.");
//Build the plugin stack
_stack.BuildStack();
//Register for plugin events
_stack.RegsiterListener(this, this);
//Create plugin wrappers from loaded plugins
ManagedPlugin[] wrapper = _stack.Plugins.Select(p => new ManagedPlugin(p)).ToArray();
//Add all wrappers to the managed plugins table
Array.ForEach(wrapper, w => _managedPlugins.Add(w.Plugin.Controller, w));
//Init remaining controllers single-threaded
_managedPlugins.Select(p => p.Value).TryForeach(w => InitializePlugin(w.Plugin, debugLog));
//Load stage, load all multithreaded
Parallel.ForEach(wrapper, wp => LoadPlugin(wp.Plugin, debugLog));
debugLog.Information("Plugin loading completed");
}
private void InitializePlugin(RuntimePluginLoader plugin, ILogProvider debugLog)
{
string fileName = Path.GetFileName(plugin.Config.AssemblyFile);
try
{
//Initialzie plugin wrapper
plugin.InitializeController();
/*
* 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", fileName);
}
}
catch (Exception ex)
{
debugLog.Error("Exception raised during initialzation of {asm}. It has been removed from the collection\n{ex}", fileName, ex);
//Remove the plugin from the table
_managedPlugins.Remove(plugin.Controller);
//Dispose the plugin
plugin.Dispose();
}
}
private static void LoadPlugin(RuntimePluginLoader plugin, ILogProvider debugLog)
{
string fileName = Path.GetFileName(plugin.Config.AssemblyFile);
Stopwatch sw = new();
try
{
sw.Start();
//Load wrapper
plugin.LoadPlugins();
sw.Stop();
debugLog.Verbose("Loaded {pl} in {tm} ms", fileName, sw.ElapsedMilliseconds);
}
catch (Exception ex)
{
debugLog.Error("Exception raised during loading {asf}. Failed to load plugin \n{ex}", fileName, 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()
{
Check();
//Reload all plugins, causing an event cascade
_stack.ReloadAll();
}
///
public void UnloadPlugins()
{
Check();
//Unload all plugin controllers
_stack.UnloadAll();
/*
* All plugin instances must be destroyed because the
* only way they will be loaded is from their files
* again, so they must be released
*/
Free();
}
protected override void Free()
{
//Dispose all managed plugins and clear the table
_managedPlugins.TryForeach(p => p.Value.Dispose());
_managedPlugins.Clear();
}
void IPluginEventListener.OnPluginLoaded(PluginController controller, object? state)
{
//Handle service events
ManagedPlugin mp = _managedPlugins.GetValue(controller, (pc) => null!);
//Run onload method before invoking other handlers
mp.OnPluginLoaded();
//Get event listeners at event time because deps may be modified by the domain
ServiceGroup[] deps = _dependents.ServiceGroups.Select(static d => d).ToArray();
//run onload method
deps.TryForeach(d => d.OnPluginLoaded(mp));
}
void IPluginEventListener.OnPluginUnloaded(PluginController controller, object? state)
{
//Handle service events
ManagedPlugin mp = _managedPlugins.GetValue(controller, (pc) => null!);
//Run onload method before invoking other handlers
mp.OnPluginUnloaded();
//Get event listeners at event time because deps may be modified by the domain
ServiceGroup[] deps = _dependents.ServiceGroups.Select(static d => d).ToArray();
//Run unloaded method
deps.TryForeach(d => d.OnPluginUnloaded(mp));
}
}
}