From 61eeb3b5c009231583507dec6406d15378509621 Mon Sep 17 00:00:00 2001 From: AeonLucid Date: Sat, 24 Oct 2020 06:02:52 +0200 Subject: [PATCH] Improve PluginLoader and Enable/Disable plugins --- src/Impostor.Api/Plugins/IPlugin.cs | 4 -- src/Impostor.Api/Plugins/IPluginStartup.cs | 12 ++++ .../Plugins/ImpostorPluginAttribute.cs | 24 ++++++++ src/Impostor.Api/Plugins/PluginBase.cs | 10 ---- src/Impostor.Plugins.Debugger/DebugPlugin.cs | 32 ++--------- .../DebugPluginStartup.cs | 35 ++++++++++++ src/Impostor.Plugins.Example/ExamplePlugin.cs | 33 +++++++++++ .../Impostor.Plugins.Example.csproj | 11 ++++ src/Impostor.Server/Impostor.Server.csproj | 2 +- .../Plugins/PluginInformation.cs | 38 +++++++++++++ src/Impostor.Server/Plugins/PluginLoader.cs | 56 +++++++++++++++---- .../Plugins/PluginLoaderException.cs | 25 +++++++++ .../Plugins/PluginLoaderService.cs | 53 ++++++++++++++++++ src/Impostor.Server/Program.cs | 4 +- src/Impostor.sln | 11 ++++ 15 files changed, 296 insertions(+), 54 deletions(-) create mode 100644 src/Impostor.Api/Plugins/IPluginStartup.cs create mode 100644 src/Impostor.Api/Plugins/ImpostorPluginAttribute.cs create mode 100644 src/Impostor.Plugins.Debugger/DebugPluginStartup.cs create mode 100644 src/Impostor.Plugins.Example/ExamplePlugin.cs create mode 100644 src/Impostor.Plugins.Example/Impostor.Plugins.Example.csproj create mode 100644 src/Impostor.Server/Plugins/PluginInformation.cs create mode 100644 src/Impostor.Server/Plugins/PluginLoaderException.cs create mode 100644 src/Impostor.Server/Plugins/PluginLoaderService.cs diff --git a/src/Impostor.Api/Plugins/IPlugin.cs b/src/Impostor.Api/Plugins/IPlugin.cs index f66f112..a4c3a55 100644 --- a/src/Impostor.Api/Plugins/IPlugin.cs +++ b/src/Impostor.Api/Plugins/IPlugin.cs @@ -12,9 +12,5 @@ namespace Impostor.Api.Plugins ValueTask DisableAsync(); ValueTask ReloadAsync(); - - void ConfigureHost(IHostBuilder host); - - void ConfigureServices(IServiceCollection services); } } \ No newline at end of file diff --git a/src/Impostor.Api/Plugins/IPluginStartup.cs b/src/Impostor.Api/Plugins/IPluginStartup.cs new file mode 100644 index 0000000..aa6a35f --- /dev/null +++ b/src/Impostor.Api/Plugins/IPluginStartup.cs @@ -0,0 +1,12 @@ +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Hosting; + +namespace Impostor.Api.Plugins +{ + public interface IPluginStartup + { + void ConfigureHost(IHostBuilder host); + + void ConfigureServices(IServiceCollection services); + } +} \ No newline at end of file diff --git a/src/Impostor.Api/Plugins/ImpostorPluginAttribute.cs b/src/Impostor.Api/Plugins/ImpostorPluginAttribute.cs new file mode 100644 index 0000000..b31bd47 --- /dev/null +++ b/src/Impostor.Api/Plugins/ImpostorPluginAttribute.cs @@ -0,0 +1,24 @@ +using System; + +namespace Impostor.Api.Plugins +{ + [AttributeUsage(AttributeTargets.Class)] + public class ImpostorPluginAttribute : Attribute + { + public ImpostorPluginAttribute(string package, string name, string author, string version) + { + Package = package; + Name = name; + Author = author; + Version = version; + } + + public string Package { get; } + + public string Name { get; } + + public string Author { get; } + + public string Version { get; } + } +} \ No newline at end of file diff --git a/src/Impostor.Api/Plugins/PluginBase.cs b/src/Impostor.Api/Plugins/PluginBase.cs index cf64190..0384363 100644 --- a/src/Impostor.Api/Plugins/PluginBase.cs +++ b/src/Impostor.Api/Plugins/PluginBase.cs @@ -1,6 +1,4 @@ using System.Threading.Tasks; -using Microsoft.Extensions.DependencyInjection; -using Microsoft.Extensions.Hosting; namespace Impostor.Api.Plugins { @@ -20,13 +18,5 @@ namespace Impostor.Api.Plugins { return default; } - - public virtual void ConfigureHost(IHostBuilder host) - { - } - - public virtual void ConfigureServices(IServiceCollection services) - { - } } } \ No newline at end of file diff --git a/src/Impostor.Plugins.Debugger/DebugPlugin.cs b/src/Impostor.Plugins.Debugger/DebugPlugin.cs index 3521a65..8f57e89 100644 --- a/src/Impostor.Plugins.Debugger/DebugPlugin.cs +++ b/src/Impostor.Plugins.Debugger/DebugPlugin.cs @@ -1,35 +1,13 @@ using Impostor.Api.Plugins; -using Microsoft.AspNetCore.Builder; -using Microsoft.AspNetCore.Hosting; -using Microsoft.Extensions.DependencyInjection; -using Microsoft.Extensions.Hosting; namespace Impostor.Plugins.Debugger { + [ImpostorPlugin( + package: "gg.impostor.debugger", + name: "Debugger", + author: "Gerard", + version: "1.0.0")] public class DebugPlugin : PluginBase { - public override void ConfigureServices(IServiceCollection services) - { - services.AddRazorPages(); - services.AddServerSideBlazor(); - } - - public override void ConfigureHost(IHostBuilder host) - { - host.ConfigureWebHostDefaults(webBuilder => - { - webBuilder.Configure(app => - { - app.UseStaticFiles(); - app.UseRouting(); - - app.UseEndpoints(endpoints => - { - endpoints.MapBlazorHub(); - endpoints.MapFallbackToPage("/_Host"); - }); - }); - }); - } } } \ No newline at end of file diff --git a/src/Impostor.Plugins.Debugger/DebugPluginStartup.cs b/src/Impostor.Plugins.Debugger/DebugPluginStartup.cs new file mode 100644 index 0000000..cc36ce6 --- /dev/null +++ b/src/Impostor.Plugins.Debugger/DebugPluginStartup.cs @@ -0,0 +1,35 @@ +using Impostor.Api.Plugins; +using Microsoft.AspNetCore.Builder; +using Microsoft.AspNetCore.Hosting; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Hosting; + +namespace Impostor.Plugins.Debugger +{ + public class DebugPluginStartup : IPluginStartup + { + public void ConfigureServices(IServiceCollection services) + { + services.AddRazorPages(); + services.AddServerSideBlazor(); + } + + public void ConfigureHost(IHostBuilder host) + { + host.ConfigureWebHostDefaults(webBuilder => + { + webBuilder.Configure(app => + { + app.UseStaticFiles(); + app.UseRouting(); + + app.UseEndpoints(endpoints => + { + endpoints.MapBlazorHub(); + endpoints.MapFallbackToPage("/_Host"); + }); + }); + }); + } + } +} \ No newline at end of file diff --git a/src/Impostor.Plugins.Example/ExamplePlugin.cs b/src/Impostor.Plugins.Example/ExamplePlugin.cs new file mode 100644 index 0000000..c55039c --- /dev/null +++ b/src/Impostor.Plugins.Example/ExamplePlugin.cs @@ -0,0 +1,33 @@ +using System.Threading.Tasks; +using Impostor.Api.Plugins; +using Microsoft.Extensions.Logging; + +namespace Impostor.Plugins.Example +{ + [ImpostorPlugin( + package: "gg.impostor.example", + name: "Example", + author: "AeonLucid", + version: "1.0.0")] + public class ExamplePlugin : PluginBase + { + private readonly ILogger _logger; + + public ExamplePlugin(ILogger logger) + { + _logger = logger; + } + + public override ValueTask EnableAsync() + { + _logger.LogInformation("Hooray."); + return default; + } + + public override ValueTask DisableAsync() + { + _logger.LogInformation("Boooh."); + return default; + } + } +} \ No newline at end of file diff --git a/src/Impostor.Plugins.Example/Impostor.Plugins.Example.csproj b/src/Impostor.Plugins.Example/Impostor.Plugins.Example.csproj new file mode 100644 index 0000000..4d93f83 --- /dev/null +++ b/src/Impostor.Plugins.Example/Impostor.Plugins.Example.csproj @@ -0,0 +1,11 @@ + + + + netstandard2.1 + + + + + + + diff --git a/src/Impostor.Server/Impostor.Server.csproj b/src/Impostor.Server/Impostor.Server.csproj index cbcbf74..5f69bf1 100644 --- a/src/Impostor.Server/Impostor.Server.csproj +++ b/src/Impostor.Server/Impostor.Server.csproj @@ -44,7 +44,7 @@ PreserveNewest true - + Never PreserveNewest true diff --git a/src/Impostor.Server/Plugins/PluginInformation.cs b/src/Impostor.Server/Plugins/PluginInformation.cs new file mode 100644 index 0000000..e6a5b6c --- /dev/null +++ b/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(); + + 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/src/Impostor.Server/Plugins/PluginLoader.cs b/src/Impostor.Server/Plugins/PluginLoader.cs index 2b3985d..9705567 100644 --- a/src/Impostor.Server/Plugins/PluginLoader.cs +++ b/src/Impostor.Server/Plugins/PluginLoader.cs @@ -5,6 +5,7 @@ using System.Linq; using System.Reflection; using System.Runtime.Loader; using Impostor.Api.Plugins; +using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.FileSystemGlobbing; using Microsoft.Extensions.Hosting; @@ -50,23 +51,58 @@ namespace Impostor.Server.Plugins .Select(a => context.LoadFromAssemblyName(a.AssemblyName)) .ToList(); - var plugins = assemblies - .SelectMany(a => a.GetTypes()) - .Where(t => typeof(IPlugin).IsAssignableFrom(t) && t.IsClass && !t.IsAbstract) - .Select(Activator.CreateInstance) - .Cast() - .ToList(); - foreach (var plugin in plugins) + // Find all plugins. + var plugins = new List(); + + 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) + { + throw new PluginLoaderException("A plugin may only define zero or one IPluginStartup implementation."); + } + + // Find plugin. + var plugin = assembly + .GetTypes() + .Where(t => typeof(IPlugin).IsAssignableFrom(t) + && t.IsClass + && !t.IsAbstract + && t.GetCustomAttribute() != null) + .ToList(); + + if (plugin.Count != 1) + { + throw new PluginLoaderException("A plugin must define exactly one IPlugin or PluginBase implementation."); + } + + // Save plugin. + plugins.Add(new PluginInformation( + pluginStartup + .Select(Activator.CreateInstance) + .Cast() + .FirstOrDefault(), + plugin.First())); + } + + foreach (var plugin in plugins.Where(plugin => plugin.Startup != null)) { - plugin.ConfigureHost(builder); + plugin.Startup.ConfigureHost(builder); } builder.ConfigureServices(services => { - foreach (var plugin in plugins) + services.AddHostedService(provider => ActivatorUtilities.CreateInstance(provider, plugins)); + + foreach (var plugin in plugins.Where(plugin => plugin.Startup != null)) { - plugin.ConfigureServices(services); + plugin.Startup.ConfigureServices(services); } }); diff --git a/src/Impostor.Server/Plugins/PluginLoaderException.cs b/src/Impostor.Server/Plugins/PluginLoaderException.cs new file mode 100644 index 0000000..64424a1 --- /dev/null +++ b/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/src/Impostor.Server/Plugins/PluginLoaderService.cs b/src/Impostor.Server/Plugins/PluginLoaderService.cs new file mode 100644 index 0000000..7ba4b77 --- /dev/null +++ b/src/Impostor.Server/Plugins/PluginLoaderService.cs @@ -0,0 +1,53 @@ +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 _logger; + private readonly IServiceProvider _serviceProvider; + private readonly List _plugins; + + public PluginLoaderService(ILogger logger, IServiceProvider serviceProvider, List plugins) + { + _logger = logger; + _serviceProvider = serviceProvider; + _plugins = plugins; + } + + public async Task StartAsync(CancellationToken cancellationToken) + { + 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(); + } + } + + 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 diff --git a/src/Impostor.Server/Program.cs b/src/Impostor.Server/Program.cs index 9c49224..8f0fc26 100644 --- a/src/Impostor.Server/Program.cs +++ b/src/Impostor.Server/Program.cs @@ -173,9 +173,9 @@ namespace Impostor.Server services.AddSingleton(); services.AddHostedService(); }) - .UsePluginLoader(pluginConfig) + .UseSerilog() .UseConsoleLifetime() - .UseSerilog(); + .UsePluginLoader(pluginConfig); } } } \ No newline at end of file diff --git a/src/Impostor.sln b/src/Impostor.sln index c04c378..dcd5255 100644 --- a/src/Impostor.sln +++ b/src/Impostor.sln @@ -35,6 +35,8 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Impostor.Client.App", "Impo EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Impostor.Tools.ServerReplay", "Impostor.Tools.ServerReplay\Impostor.Tools.ServerReplay.csproj", "{4DB56ADD-6D3D-4D0E-A047-9D7E7D40EF99}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Impostor.Plugins.Example", "Impostor.Plugins.Example\Impostor.Plugins.Example.csproj", "{70F97BB7-12A1-4C7A-B2A5-B962D14B813E}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -135,6 +137,14 @@ Global {4DB56ADD-6D3D-4D0E-A047-9D7E7D40EF99}.Release|Any CPU.Build.0 = Release|Any CPU {4DB56ADD-6D3D-4D0E-A047-9D7E7D40EF99}.Release|x86.ActiveCfg = Release|Any CPU {4DB56ADD-6D3D-4D0E-A047-9D7E7D40EF99}.Release|x86.Build.0 = Release|Any CPU + {70F97BB7-12A1-4C7A-B2A5-B962D14B813E}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {70F97BB7-12A1-4C7A-B2A5-B962D14B813E}.Debug|Any CPU.Build.0 = Debug|Any CPU + {70F97BB7-12A1-4C7A-B2A5-B962D14B813E}.Debug|x86.ActiveCfg = Debug|Any CPU + {70F97BB7-12A1-4C7A-B2A5-B962D14B813E}.Debug|x86.Build.0 = Debug|Any CPU + {70F97BB7-12A1-4C7A-B2A5-B962D14B813E}.Release|Any CPU.ActiveCfg = Release|Any CPU + {70F97BB7-12A1-4C7A-B2A5-B962D14B813E}.Release|Any CPU.Build.0 = Release|Any CPU + {70F97BB7-12A1-4C7A-B2A5-B962D14B813E}.Release|x86.ActiveCfg = Release|Any CPU + {70F97BB7-12A1-4C7A-B2A5-B962D14B813E}.Release|x86.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE @@ -151,5 +161,6 @@ Global {BE44C4A8-A202-4B63-A3BA-AC0AB5E7A0EB} = {9F1919B0-915B-4749-9944-697DF7E7F67F} {3DF86F12-7099-44F6-B98B-A148213D60B1} = {9F1919B0-915B-4749-9944-697DF7E7F67F} {4DB56ADD-6D3D-4D0E-A047-9D7E7D40EF99} = {56DD9707-D811-4056-9E2C-8A9CC2479B07} + {70F97BB7-12A1-4C7A-B2A5-B962D14B813E} = {36AA9913-E6EA-4A6C-90E6-2FD3CC2E3124} EndGlobalSection EndGlobal -- 2.39.5