summaryrefslogtreecommitdiff
path: root/Impostor-dev/src/Impostor.Server/Plugins
diff options
context:
space:
mode:
Diffstat (limited to 'Impostor-dev/src/Impostor.Server/Plugins')
-rw-r--r--Impostor-dev/src/Impostor.Server/Plugins/AssemblyInformation.cs38
-rw-r--r--Impostor-dev/src/Impostor.Server/Plugins/IAssemblyInformation.cs14
-rw-r--r--Impostor-dev/src/Impostor.Server/Plugins/LoadedAssemblyInformation.cs25
-rw-r--r--Impostor-dev/src/Impostor.Server/Plugins/PluginConfig.cs11
-rw-r--r--Impostor-dev/src/Impostor.Server/Plugins/PluginInformation.cs38
-rw-r--r--Impostor-dev/src/Impostor.Server/Plugins/PluginLoader.cs147
-rw-r--r--Impostor-dev/src/Impostor.Server/Plugins/PluginLoaderException.cs25
-rw-r--r--Impostor-dev/src/Impostor.Server/Plugins/PluginLoaderService.cs60
8 files changed, 358 insertions, 0 deletions
diff --git a/Impostor-dev/src/Impostor.Server/Plugins/AssemblyInformation.cs b/Impostor-dev/src/Impostor.Server/Plugins/AssemblyInformation.cs
new file mode 100644
index 0000000..5f6aee1
--- /dev/null
+++ b/Impostor-dev/src/Impostor.Server/Plugins/AssemblyInformation.cs
@@ -0,0 +1,38 @@
+using System.IO;
+using System.Reflection;
+using System.Runtime.Loader;
+
+namespace Impostor.Server.Plugins
+{
+ public class AssemblyInformation : IAssemblyInformation
+ {
+ private Assembly _assembly;
+
+ public AssemblyInformation(AssemblyName assemblyName, string path, bool isPlugin)
+ {
+ AssemblyName = assemblyName;
+ Path = path;
+ IsPlugin = isPlugin;
+ }
+
+ public string Path { get; }
+
+ public bool IsPlugin { get; }
+
+ public AssemblyName AssemblyName { get; }
+
+ public Assembly Load(AssemblyLoadContext context)
+ {
+ if (_assembly != null)
+ {
+ return _assembly;
+ }
+
+ using var stream = File.Open(Path, FileMode.Open, FileAccess.Read, FileShare.Read);
+
+ _assembly = context.LoadFromStream(stream);
+
+ return _assembly;
+ }
+ }
+} \ No newline at end of file
diff --git a/Impostor-dev/src/Impostor.Server/Plugins/IAssemblyInformation.cs b/Impostor-dev/src/Impostor.Server/Plugins/IAssemblyInformation.cs
new file mode 100644
index 0000000..fb36e92
--- /dev/null
+++ b/Impostor-dev/src/Impostor.Server/Plugins/IAssemblyInformation.cs
@@ -0,0 +1,14 @@
+using System.Reflection;
+using System.Runtime.Loader;
+
+namespace Impostor.Server.Plugins
+{
+ public interface IAssemblyInformation
+ {
+ AssemblyName AssemblyName { get; }
+
+ bool IsPlugin { get; }
+
+ Assembly Load(AssemblyLoadContext context);
+ }
+} \ No newline at end of file
diff --git a/Impostor-dev/src/Impostor.Server/Plugins/LoadedAssemblyInformation.cs b/Impostor-dev/src/Impostor.Server/Plugins/LoadedAssemblyInformation.cs
new file mode 100644
index 0000000..720367c
--- /dev/null
+++ b/Impostor-dev/src/Impostor.Server/Plugins/LoadedAssemblyInformation.cs
@@ -0,0 +1,25 @@
+using System.Reflection;
+using System.Runtime.Loader;
+
+namespace Impostor.Server.Plugins
+{
+ public class LoadedAssemblyInformation : IAssemblyInformation
+ {
+ private readonly Assembly _assembly;
+
+ public LoadedAssemblyInformation(Assembly assembly)
+ {
+ AssemblyName = assembly.GetName();
+ _assembly = assembly;
+ }
+
+ public AssemblyName AssemblyName { get; }
+
+ public bool IsPlugin => false;
+
+ public Assembly Load(AssemblyLoadContext context)
+ {
+ return _assembly;
+ }
+ }
+} \ No newline at end of file
diff --git a/Impostor-dev/src/Impostor.Server/Plugins/PluginConfig.cs b/Impostor-dev/src/Impostor.Server/Plugins/PluginConfig.cs
new file mode 100644
index 0000000..22dc9e9
--- /dev/null
+++ b/Impostor-dev/src/Impostor.Server/Plugins/PluginConfig.cs
@@ -0,0 +1,11 @@
+using System.Collections.Generic;
+
+namespace Impostor.Server.Plugins
+{
+ public class PluginConfig
+ {
+ public List<string> Paths { get; set; } = new List<string>();
+
+ public List<string> LibraryPaths { get; set; } = new List<string>();
+ }
+} \ No newline at end of file
diff --git a/Impostor-dev/src/Impostor.Server/Plugins/PluginInformation.cs b/Impostor-dev/src/Impostor.Server/Plugins/PluginInformation.cs
new file mode 100644
index 0000000..e6a5b6c
--- /dev/null
+++ b/Impostor-dev/src/Impostor.Server/Plugins/PluginInformation.cs
@@ -0,0 +1,38 @@
+using System;
+using System.Reflection;
+using Impostor.Api.Plugins;
+
+namespace Impostor.Server.Plugins
+{
+ public class PluginInformation
+ {
+ private readonly ImpostorPluginAttribute _attribute;
+
+ public PluginInformation(IPluginStartup startup, Type pluginType)
+ {
+ _attribute = pluginType.GetCustomAttribute<ImpostorPluginAttribute>();
+
+ Startup = startup;
+ PluginType = pluginType;
+ }
+
+ public string Package => _attribute.Package;
+
+ public string Name => _attribute.Name;
+
+ public string Author => _attribute.Author;
+
+ public string Version => _attribute.Version;
+
+ public IPluginStartup Startup { get; }
+
+ public Type PluginType { get; }
+
+ public IPlugin Instance { get; set; }
+
+ public override string ToString()
+ {
+ return $"{Package} {Name} ({Version}) by {Author}";
+ }
+ }
+} \ No newline at end of file
diff --git a/Impostor-dev/src/Impostor.Server/Plugins/PluginLoader.cs b/Impostor-dev/src/Impostor.Server/Plugins/PluginLoader.cs
new file mode 100644
index 0000000..4e72886
--- /dev/null
+++ b/Impostor-dev/src/Impostor.Server/Plugins/PluginLoader.cs
@@ -0,0 +1,147 @@
+using System;
+using System.Collections.Generic;
+using System.IO;
+using System.Linq;
+using System.Reflection;
+using System.Runtime.Loader;
+using Impostor.Api.Plugins;
+using Impostor.Server.Utils;
+using Microsoft.Extensions.DependencyInjection;
+using Microsoft.Extensions.FileSystemGlobbing;
+using Microsoft.Extensions.Hosting;
+using Serilog;
+
+namespace Impostor.Server.Plugins
+{
+ public static class PluginLoader
+ {
+ private static readonly ILogger Logger = Log.ForContext(typeof(PluginLoader));
+
+ public static IHostBuilder UsePluginLoader(this IHostBuilder builder, PluginConfig config)
+ {
+ var assemblyInfos = new List<IAssemblyInformation>();
+ var context = AssemblyLoadContext.Default;
+
+ // Add the plugins and libraries.
+ var pluginPaths = new List<string>(config.Paths);
+ var libraryPaths = new List<string>(config.LibraryPaths);
+
+ var rootFolder = Directory.GetCurrentDirectory();
+
+ pluginPaths.Add(Path.Combine(rootFolder, "plugins"));
+ libraryPaths.Add(Path.Combine(rootFolder, "libraries"));
+
+ var matcher = new Matcher(StringComparison.OrdinalIgnoreCase);
+ matcher.AddInclude("*.dll");
+ matcher.AddExclude("Impostor.Api.dll");
+
+ RegisterAssemblies(pluginPaths, matcher, assemblyInfos, true);
+ RegisterAssemblies(libraryPaths, matcher, assemblyInfos, false);
+
+ // Register the resolver to the current context.
+ // TODO: Move this to a new context so we can unload/reload plugins.
+ context.Resolving += (loadContext, name) =>
+ {
+ Logger.Verbose("Loading assembly {0} v{1}", name.Name, name.Version);
+
+ // Some plugins may be referencing another Impostor.Api version and try to load it.
+ // We want to only use the one shipped with the server.
+ if (name.Name.Equals("Impostor.Api"))
+ {
+ return typeof(IPlugin).Assembly;
+ }
+
+ var info = assemblyInfos.FirstOrDefault(a => a.AssemblyName.Name == name.Name);
+
+ return info?.Load(loadContext);
+ };
+
+ // TODO: Catch uncaught exceptions.
+ var assemblies = assemblyInfos
+ .Where(a => a.IsPlugin)
+ .Select(a => context.LoadFromAssemblyName(a.AssemblyName))
+ .ToList();
+
+ // Find all plugins.
+ var plugins = new List<PluginInformation>();
+
+ foreach (var assembly in assemblies)
+ {
+ // Find plugin startup.
+ var pluginStartup = assembly
+ .GetTypes()
+ .Where(t => typeof(IPluginStartup).IsAssignableFrom(t) && t.IsClass)
+ .ToList();
+
+ if (pluginStartup.Count > 1)
+ {
+ Logger.Warning("A plugin may only define zero or one IPluginStartup implementation ({0}).", assembly);
+ continue;
+ }
+
+ // Find plugin.
+ var plugin = assembly
+ .GetTypes()
+ .Where(t => typeof(IPlugin).IsAssignableFrom(t)
+ && t.IsClass
+ && !t.IsAbstract
+ && t.GetCustomAttribute<ImpostorPluginAttribute>() != null)
+ .ToList();
+
+ if (plugin.Count != 1)
+ {
+ Logger.Warning("A plugin must define exactly one IPlugin or PluginBase implementation ({0}).", assembly);
+ continue;
+ }
+
+ // Save plugin.
+ plugins.Add(new PluginInformation(
+ pluginStartup
+ .Select(Activator.CreateInstance)
+ .Cast<IPluginStartup>()
+ .FirstOrDefault(),
+ plugin.First()));
+ }
+
+ foreach (var plugin in plugins.Where(plugin => plugin.Startup != null))
+ {
+ plugin.Startup.ConfigureHost(builder);
+ }
+
+ builder.ConfigureServices(services =>
+ {
+ services.AddHostedService(provider => ActivatorUtilities.CreateInstance<PluginLoaderService>(provider, plugins));
+
+ foreach (var plugin in plugins.Where(plugin => plugin.Startup != null))
+ {
+ plugin.Startup.ConfigureServices(services);
+ }
+ });
+
+ return builder;
+ }
+
+ private static void RegisterAssemblies(
+ IEnumerable<string> paths,
+ Matcher matcher,
+ ICollection<IAssemblyInformation> assemblyInfos,
+ bool isPlugin)
+ {
+ foreach (var path in paths.SelectMany(matcher.GetResultsInFullPath))
+ {
+ AssemblyName assemblyName;
+
+ try
+ {
+ assemblyName = AssemblyName.GetAssemblyName(path);
+ }
+ catch (BadImageFormatException)
+ {
+ continue;
+ }
+
+ assemblyInfos.Add(new AssemblyInformation(assemblyName, path, isPlugin));
+ }
+ }
+ }
+}
diff --git a/Impostor-dev/src/Impostor.Server/Plugins/PluginLoaderException.cs b/Impostor-dev/src/Impostor.Server/Plugins/PluginLoaderException.cs
new file mode 100644
index 0000000..64424a1
--- /dev/null
+++ b/Impostor-dev/src/Impostor.Server/Plugins/PluginLoaderException.cs
@@ -0,0 +1,25 @@
+using System;
+using System.Runtime.Serialization;
+using Impostor.Api;
+
+namespace Impostor.Server.Plugins
+{
+ public class PluginLoaderException : ImpostorException
+ {
+ public PluginLoaderException()
+ {
+ }
+
+ protected PluginLoaderException(SerializationInfo info, StreamingContext context) : base(info, context)
+ {
+ }
+
+ public PluginLoaderException(string? message) : base(message)
+ {
+ }
+
+ public PluginLoaderException(string? message, Exception? innerException) : base(message, innerException)
+ {
+ }
+ }
+} \ No newline at end of file
diff --git a/Impostor-dev/src/Impostor.Server/Plugins/PluginLoaderService.cs b/Impostor-dev/src/Impostor.Server/Plugins/PluginLoaderService.cs
new file mode 100644
index 0000000..0afbc22
--- /dev/null
+++ b/Impostor-dev/src/Impostor.Server/Plugins/PluginLoaderService.cs
@@ -0,0 +1,60 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Threading;
+using System.Threading.Tasks;
+using Impostor.Api.Plugins;
+using Microsoft.Extensions.DependencyInjection;
+using Microsoft.Extensions.Hosting;
+using Microsoft.Extensions.Logging;
+
+namespace Impostor.Server.Plugins
+{
+ public class PluginLoaderService : IHostedService
+ {
+ private readonly ILogger<PluginLoaderService> _logger;
+ private readonly IServiceProvider _serviceProvider;
+ private readonly List<PluginInformation> _plugins;
+
+ public PluginLoaderService(ILogger<PluginLoaderService> logger, IServiceProvider serviceProvider, List<PluginInformation> plugins)
+ {
+ _logger = logger;
+ _serviceProvider = serviceProvider;
+ _plugins = plugins;
+ }
+
+ public async Task StartAsync(CancellationToken cancellationToken)
+ {
+ _logger.LogInformation("Loading plugins.");
+
+ foreach (var plugin in _plugins)
+ {
+ _logger.LogInformation("Enabling plugin {0}.", plugin);
+
+ // Create instance and inject services.
+ plugin.Instance = (IPlugin) ActivatorUtilities.CreateInstance(_serviceProvider, plugin.PluginType);
+
+ // Enable plugin.
+ await plugin.Instance.EnableAsync();
+ }
+
+ _logger.LogInformation(
+ _plugins.Count == 1
+ ? "Loaded {0} plugin."
+ : "Loaded {0} plugins.", _plugins.Count);
+ }
+
+ public async Task StopAsync(CancellationToken cancellationToken)
+ {
+ // Disable all plugins with a valid instance set.
+ // In the case of a failed startup, some can be null.
+ foreach (var plugin in _plugins.Where(plugin => plugin.Instance != null))
+ {
+ _logger.LogInformation("Disabling plugin {0}.", plugin);
+
+ // Disable plugin.
+ await plugin.Instance.DisableAsync();
+ }
+ }
+ }
+} \ No newline at end of file