/*
* Copyright (c) 2022 Vaughn Nugent
*
* Library: VNLib
* Package: VNLib.Plugins.Essentials.ServiceStack
* File: ServiceDomain.cs
*
* ServiceDomain.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.Net;
using System.Text.Json;
using System.Diagnostics;
using VNLib.Utils;
using VNLib.Utils.IO;
using VNLib.Utils.Extensions;
using VNLib.Utils.Logging;
using VNLib.Plugins.Runtime;
using VNLib.Plugins.Essentials.Content;
using VNLib.Plugins.Essentials.Sessions;
namespace VNLib.Plugins.Essentials.ServiceStack
{
///
/// Represents a domain of services and thier dynamically loaded plugins
/// that will be hosted by an application service stack
///
public sealed class ServiceDomain : VnDisposeable, IPluginController
{
private const string PLUGIN_FILE_EXTENSION = ".dll";
private const string DEFUALT_PLUGIN_DIR = "/plugins";
private const string PLUGINS_CONFIG_ELEMENT = "plugins";
private readonly LinkedList _serviceGroups;
private readonly LinkedList _pluginLoaders;
///
/// Enumerates all loaded plugin instances
///
public IEnumerable Plugins => _pluginLoaders.SelectMany(static s =>
s.LivePlugins.Where(static p => p.Plugin != null)
.Select(static s => s.Plugin!)
);
///
/// Gets all service groups loaded in the service manager
///
public IReadOnlyCollection ServiceGroups => _serviceGroups;
///
/// Initializes a new empty
///
public ServiceDomain()
{
_serviceGroups = new();
_pluginLoaders = new();
}
///
/// Uses the supplied callback to get a collection of virtual hosts
/// to build the current domain with
///
/// The callback method to build virtual hosts
/// A value that indicates if any virtual hosts were successfully loaded
public bool BuildDomain(Action> hostBuilder)
{
//LL to store created hosts
LinkedList hosts = new();
//build hosts
hostBuilder.Invoke(hosts);
return FromExisting(hosts);
}
///
/// Builds the domain from an existing enumeration of virtual hosts
///
/// The enumeration of virtual hosts
/// A value that indicates if any virtual hosts were successfully loaded
public bool FromExisting(IEnumerable hosts)
{
//Get service groups and pass service group list
CreateServiceGroups(_serviceGroups, hosts);
return _serviceGroups.Any();
}
private static void CreateServiceGroups(ICollection groups, IEnumerable hosts)
{
//Get distinct interfaces
IPEndPoint[] interfaces = hosts.Select(static s => s.TransportInfo.TransportEndpoint).Distinct().ToArray();
//Select hosts of the same interface to create a group from
foreach (IPEndPoint iface in interfaces)
{
IEnumerable groupHosts = hosts.Where(host => host.TransportInfo.TransportEndpoint.Equals(iface));
IServiceHost[]? overlap = groupHosts.Where(vh => groupHosts.Select(static s => s.Processor.Hostname).Count(hostname => vh.Processor.Hostname == hostname) > 1).ToArray();
foreach (IServiceHost vh in overlap)
{
throw new ArgumentException($"The hostname '{vh.Processor.Hostname}' is already in use by another virtual host");
}
//init new service group around an interface and its roots
ServiceGroup group = new(iface, groupHosts);
groups.Add(group);
}
}
///
public Task LoadPlugins(JsonDocument config, ILogProvider appLog)
{
if (!config.RootElement.TryGetProperty(PLUGINS_CONFIG_ELEMENT, out JsonElement pluginEl))
{
appLog.Information("Plugins element not defined in config, skipping plugin loading");
return Task.CompletedTask;
}
//Get the plugin directory, or set to default
string pluginDir = pluginEl.GetPropString("path") ?? Path.Combine(Directory.GetCurrentDirectory(), DEFUALT_PLUGIN_DIR);
//Get the hot reload flag
bool hotReload = pluginEl.TryGetProperty("hot_reload", out JsonElement hrel) && hrel.GetBoolean();
//Load all virtual file assemblies withing the plugin folder
DirectoryInfo dir = new(pluginDir);
if (!dir.Exists)
{
appLog.Warn("Plugin directory {dir} does not exist. No plugins were loaded", pluginDir);
return Task.CompletedTask;
}
appLog.Information("Loading plugins. Hot-reload: {en}", hotReload);
//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 = 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);
});
IEnumerable pluginFileNames = pluginPaths.Select(static s => $"{Path.GetFileName(s)}\n");
appLog.Debug("Found plugin files: \n{files}", string.Concat(pluginFileNames));
LinkedList loading = new();
object listLock = new();
foreach (string pluginPath in pluginPaths)
{
async Task Load()
{
string pluginName = Path.GetFileName(pluginPath);
RuntimePluginLoader plugin = new(pluginPath, config, appLog, hotReload, hotReload);
Stopwatch sw = new();
try
{
sw.Start();
await plugin.InitLoaderAsync();
//Listen for reload events to remove and re-add endpoints
plugin.Reloaded += OnPluginReloaded;
lock (listLock)
{
//Add to list
_pluginLoaders.AddLast(plugin);
}
sw.Stop();
appLog.Verbose("Loaded {pl} in {tm} ms", pluginName, sw.ElapsedMilliseconds);
}
catch (Exception ex)
{
appLog.Error(ex, $"Exception raised during loading {pluginName}. Failed to load plugin \n{ex}");
plugin.Dispose();
}
finally
{
sw.Stop();
}
}
loading.AddLast(Load());
}
//Continuation to add all initial plugins to the service manager
void Continuation(Task t)
{
appLog.Verbose("Plugins loaded");
//Add inital endpoints for all plugins
_pluginLoaders.TryForeach(ldr => _serviceGroups.TryForeach(sg => sg.AddOrUpdateEndpointsForPlugin(ldr)));
//Init session provider
InitSessionProvider();
//Init page router
InitPageRouter();
}
//wait for loading to completed
return Task.WhenAll(loading.ToArray()).ContinueWith(Continuation, CancellationToken.None, TaskContinuationOptions.ExecuteSynchronously, TaskScheduler.Default);
}
///
public bool SendCommandToPlugin(string pluginName, string message, StringComparison nameComparison = StringComparison.Ordinal)
{
Check();
//Find the single plugin by its name
LivePlugin? pl = _pluginLoaders.Select(p =>
p.LivePlugins.Where(lp => pluginName.Equals(lp.PluginName, nameComparison))
)
.SelectMany(static lp => lp)
.SingleOrDefault();
//Send the command
return pl?.SendConsoleMessage(message) ?? false;
}
///
public void ForceReloadAllPlugins()
{
Check();
_pluginLoaders.TryForeach(static pl => pl.ReloadPlugin());
}
///
public void UnloadAll()
{
Check();
//Unload service groups before unloading plugins
_serviceGroups.TryForeach(static sg => sg.UnloadAll());
//empty service groups
_serviceGroups.Clear();
//Unload all plugins
_pluginLoaders.TryForeach(static pl => pl.UnloadAll());
}
private void OnPluginReloaded(object? plugin, EventArgs empty)
{
//Update endpoints for the loader
RuntimePluginLoader reloaded = (plugin as RuntimePluginLoader)!;
//Update all endpoints for the plugin
_serviceGroups.TryForeach(sg => sg.AddOrUpdateEndpointsForPlugin(reloaded));
}
private void InitSessionProvider()
{
//Callback to reload provider
void onSessionProviderReloaded(ISessionProvider old, ISessionProvider current)
{
_serviceGroups.TryForeach(sg => sg.UpdateSessionProvider(current));
}
try
{
//get the loader that contains the single session provider
RuntimePluginLoader? sessionLoader = _pluginLoaders
.Where(static s => s.ExposesType())
.SingleOrDefault();
//If session provider has been supplied, load it
if (sessionLoader != null)
{
//Get the session provider from the plugin loader
ISessionProvider sp = sessionLoader.GetExposedTypeFromPlugin()!;
//Init inital provider
onSessionProviderReloaded(null!, sp);
//Register reload event
sessionLoader.RegisterListenerForSingle(onSessionProviderReloaded);
}
}
catch (InvalidOperationException)
{
throw new TypeLoadException("More than one session provider plugin was defined in the plugin directory, cannot continue");
}
}
private void InitPageRouter()
{
//Callback to reload provider
void onRouterReloaded(IPageRouter old, IPageRouter current)
{
_serviceGroups.TryForeach(sg => sg.UpdatePageRouter(current));
}
try
{
//get the loader that contains the single page router
RuntimePluginLoader? routerLoader = _pluginLoaders
.Where(static s => s.ExposesType())
.SingleOrDefault();
//If router has been supplied, load it
if (routerLoader != null)
{
//Get initial value
IPageRouter sp = routerLoader.GetExposedTypeFromPlugin()!;
//Init inital provider
onRouterReloaded(null!, sp);
//Register reload event
routerLoader.RegisterListenerForSingle(onRouterReloaded);
}
}
catch (InvalidOperationException)
{
throw new TypeLoadException("More than one page router plugin was defined in the plugin directory, cannot continue");
}
}
///
protected override void Free()
{
//Dispose loaders
_pluginLoaders.TryForeach(static pl => pl.Dispose());
_pluginLoaders.Clear();
_serviceGroups.Clear();
}
}
}