diff options
Diffstat (limited to 'Impostor-dev/src/Impostor.Server/Plugins')
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 |