ValueTask DisableAsync();
ValueTask ReloadAsync();
-
- void ConfigureHost(IHostBuilder host);
-
- void ConfigureServices(IServiceCollection services);
}
}
\ No newline at end of file
--- /dev/null
+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
--- /dev/null
+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
using System.Threading.Tasks;
-using Microsoft.Extensions.DependencyInjection;
-using Microsoft.Extensions.Hosting;
namespace Impostor.Api.Plugins
{
{
return default;
}
-
- public virtual void ConfigureHost(IHostBuilder host)
- {
- }
-
- public virtual void ConfigureServices(IServiceCollection services)
- {
- }
}
}
\ No newline at end of file
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
--- /dev/null
+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
--- /dev/null
+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<ExamplePlugin> _logger;
+
+ public ExamplePlugin(ILogger<ExamplePlugin> 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
--- /dev/null
+<Project Sdk="Microsoft.NET.Sdk">
+
+ <PropertyGroup>
+ <TargetFramework>netstandard2.1</TargetFramework>
+ </PropertyGroup>
+
+ <ItemGroup>
+ <ProjectReference Include="..\Impostor.Api\Impostor.Api.csproj" />
+ </ItemGroup>
+
+</Project>
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
<ExcludeFromSingleFile>true</ExcludeFromSingleFile>
</Content>
- <Content Update="config.Development.json">
+ <Content Include="config.Development.json">
<CopyToPublishDirectory>Never</CopyToPublishDirectory>
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
<ExcludeFromSingleFile>true</ExcludeFromSingleFile>
--- /dev/null
+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
using System.Reflection;
using System.Runtime.Loader;
using Impostor.Api.Plugins;
+using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.FileSystemGlobbing;
using Microsoft.Extensions.Hosting;
.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<IPlugin>()
- .ToList();
- foreach (var plugin in plugins)
+ // 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)
+ {
+ 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<ImpostorPluginAttribute>() != 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<IPluginStartup>()
+ .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<PluginLoaderService>(provider, plugins));
+
+ foreach (var plugin in plugins.Where(plugin => plugin.Startup != null))
{
- plugin.ConfigureServices(services);
+ plugin.Startup.ConfigureServices(services);
}
});
--- /dev/null
+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
--- /dev/null
+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)
+ {
+ 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
services.AddSingleton<Matchmaker>();
services.AddHostedService<MatchmakerService>();
})
- .UsePluginLoader(pluginConfig)
+ .UseSerilog()
.UseConsoleLifetime()
- .UseSerilog();
+ .UsePluginLoader(pluginConfig);
}
}
}
\ No newline at end of file
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
{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
{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