// Copyright Epic Games, Inc. All Rights Reserved.
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Reflection;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
namespace HordeServer.Plugins
{
///
/// Concrete implementation of
///
public class PluginCollection : IPluginCollection
{
// Plugin which is compiled into the application or already loaded
class StaticPlugin : IPlugin
{
public PluginName Name => _metadata.Name;
public IPluginMetadata Metadata => _metadata;
readonly IPluginMetadata _metadata;
readonly ILoadedPlugin _loadedPlugin;
public StaticPlugin(IPluginMetadata metadata, ILoadedPlugin loadedPlugin)
{
_metadata = metadata;
_loadedPlugin = loadedPlugin;
}
public ILoadedPlugin Load()
=> _loadedPlugin;
}
[DebuggerDisplay("{Name}")]
class LoadedPlugin : ILoadedPlugin
where TServerConfig : PluginServerConfig, new()
where TGlobalConfig : class, IPluginConfig, new()
where TStartup : class, IPluginStartup
{
readonly IPluginMetadata _metadata;
public PluginName Name => _metadata.Name;
public IPluginMetadata Metadata => _metadata;
public Assembly Assembly => typeof(TStartup).Assembly;
public Type ServerConfigType => typeof(TServerConfig);
public Type GlobalConfigType => typeof(TGlobalConfig);
public LoadedPlugin(IPluginMetadata metadata)
=> _metadata = metadata;
public void ConfigureServices(IConfiguration config, IServerInfo serverInfo, IServiceCollection serviceCollection)
{
serviceCollection.Configure(config);
serviceCollection.AddPluginConfig(Name);
TStartup startup = CreateStartup(config, serverInfo);
serviceCollection.AddSingleton(startup);
serviceCollection.AddSingleton(startup);
startup.ConfigureServices(serviceCollection);
}
static TStartup CreateStartup(IConfiguration configuration, IServerInfo serverInfo)
{
ConstructorInfo? chosenConstructor = null;
ParameterInfo[]? chosenConstructorParams = null;
ConstructorInfo[] constructors = typeof(TStartup).GetConstructors(BindingFlags.Public | BindingFlags.Instance);
foreach (ConstructorInfo constructor in constructors)
{
ParameterInfo[] parameters = constructor.GetParameters();
if (IsValidConstructor(parameters))
{
if (chosenConstructorParams == null || parameters.Length > chosenConstructorParams.Length)
{
chosenConstructor = constructor;
chosenConstructorParams = parameters;
}
}
}
TStartup startup;
if (chosenConstructor == null || chosenConstructorParams == null)
{
startup = Activator.CreateInstance();
}
else
{
object[] arguments = new object[chosenConstructorParams.Length];
for (int idx = 0; idx < chosenConstructorParams.Length; idx++)
{
ParameterInfo parameter = chosenConstructorParams[idx];
if (parameter.ParameterType == typeof(IConfiguration))
{
arguments[idx] = configuration;
}
else if (parameter.ParameterType == typeof(IServerInfo))
{
arguments[idx] = serverInfo;
}
else if (parameter.ParameterType == typeof(TServerConfig))
{
arguments[idx] = new TServerConfig();
configuration.Bind(arguments[idx]);
}
else
{
throw new NotImplementedException();
}
}
startup = (TStartup)chosenConstructor.Invoke(arguments);
}
return startup;
}
static bool IsValidConstructor(ParameterInfo[] parameters)
{
foreach (ParameterInfo parameter in parameters)
{
if (parameter.ParameterType != typeof(IServerInfo)
&& parameter.ParameterType != typeof(TServerConfig))
{
return false;
}
}
return true;
}
public ILoadedPlugin Load()
=> this;
}
///
public IReadOnlyList Plugins => _plugins;
///
public IReadOnlyList LoadedPlugins => _loadedPlugins;
readonly List _plugins = new List();
readonly List _loadedPlugins = new List();
readonly HashSet _loadedPluginNames = new HashSet();
///
/// Adds a plugin with the given startup class
///
public ILoadedPlugin Add(Type startupType)
{
PluginAttribute attr = startupType.GetCustomAttribute()
?? throw new InvalidOperationException($"Cannot add {startupType.Name} as a plugin. No {nameof(PluginAttribute)} was found.");
IPluginMetadata metadata = new PluginMetadata { Name = new PluginName(attr.Name), DependsOn = [..attr.DependsOn] };
if (!_loadedPluginNames.Add(metadata.Name))
{
throw new InvalidOperationException($"An implementation of the {metadata.Name} plugin has already been added");
}
Type pluginType = typeof(LoadedPlugin<,,>).MakeGenericType(attr.ServerConfigType ?? typeof(PluginServerConfig), attr.GlobalConfigType ?? typeof(EmptyPluginConfig), startupType);
ILoadedPlugin loadedPlugin = (ILoadedPlugin)Activator.CreateInstance(pluginType, metadata)!;
_loadedPlugins.Add(loadedPlugin);
return loadedPlugin;
}
private enum DependencyNodeState
{
None,
InProgress,
Done,
}
private class DependencyNode(ILoadedPlugin plugin, IReadOnlyList dependsOn)
{
public readonly ILoadedPlugin Plugin = plugin;
public readonly IReadOnlyList DependsOn = dependsOn;
public DependencyNodeState State { get; set; } = DependencyNodeState.None;
}
private static void DependencyNodeVisit(DependencyNode current, Dictionary nodes, List result)
{
if (current.State == DependencyNodeState.Done)
{
return;
}
if (current.State == DependencyNodeState.InProgress)
{
throw new InvalidOperationException($"Cycle found in plugins for '{current.Plugin.Name}' during dependency sort");
}
current.State = DependencyNodeState.InProgress;
foreach (PluginName dependsOn in current.DependsOn)
{
if (nodes.TryGetValue(dependsOn, out DependencyNode? next))
{
DependencyNodeVisit(next, nodes, result);
}
}
current.State = DependencyNodeState.Done;
result.Add(current.Plugin);
}
///
/// Returns a topological sorted sequence of plugins by plugins they depend upon
///
public static IReadOnlyList GetTopologicalSort(IReadOnlyList plugins)
{
List result = [];
Dictionary nodes = new();
foreach (ILoadedPlugin plugin in plugins)
{
List dependsOn = [];
foreach (string pluginName in plugin.Metadata.DependsOn)
{
dependsOn.Add(new PluginName(pluginName));
}
DependencyNode node = new(plugin, dependsOn);
nodes.Add(plugin.Name, node);
}
foreach (DependencyNode node in nodes.Values)
{
DependencyNodeVisit(node, nodes, result);
}
return result;
}
///
/// Adds a plugin with the given startup class
///
/// Type of the startup class
public ILoadedPlugin Add() where T : class, IPluginStartup
=> Add(typeof(T));
}
}