Lift out event handlers. Static event handlers

This commit is contained in:
Jordan Brown
2018-07-29 18:55:23 -04:00
parent dd394d9795
commit 70e800ebcd
9 changed files with 223 additions and 16 deletions
@@ -0,0 +1,54 @@
using System;
using System.Collections.Generic;
using System.Threading;
using System.Threading.Tasks;
using Tgstation.Server.Host.Components.StaticFiles;
using Tgstation.Server.Host.Components.Watchdog;
namespace Tgstation.Server.Host.Components
{
/// <inheritdoc />
sealed class EventConsumer : IEventConsumer
{
/// <summary>
/// The <see cref="IConfiguration"/> for the <see cref="EventConsumer"/>
/// </summary>
readonly IConfiguration configuration;
/// <summary>
/// The <see cref="IWatchdog"/> for the
/// </summary>
IWatchdog watchdog;
/// <summary>
/// Construct an <see cref="IEventConsumer"/>
/// </summary>
/// <param name="configuration">The value of <see cref="configuration"/></param>
public EventConsumer(IConfiguration configuration)
{
this.configuration = configuration ?? throw new ArgumentNullException(nameof(configuration));
}
/// <inheritdoc />
public async Task<bool> HandleEvent(EventType eventType, IEnumerable<string> parameters, CancellationToken cancellationToken)
{
if (watchdog == null)
throw new InvalidOperationException("EventConsumer used without watchdog set!");
if (!await configuration.HandleEvent(eventType, parameters, cancellationToken).ConfigureAwait(false))
return false;
return await watchdog.HandleEvent(eventType, parameters, cancellationToken).ConfigureAwait(false);
}
/// <summary>
/// Set the <paramref name="watchdog"/> for the <see cref="EventConsumer"/>
/// </summary>
/// <param name="watchdog">The value of <see cref="watchdog"/></param>
public void SetWatchdog(IWatchdog watchdog)
{
if (watchdog != null)
throw new InvalidOperationException("watchdog already set!");
this.watchdog = watchdog;
}
}
}
@@ -15,7 +15,7 @@ namespace Tgstation.Server.Host.Components
/// <param name="eventType">The <see cref="EventType"/></param>
/// <param name="parameters">The parameters for <paramref name="eventType"/></param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> for the operation</param>
/// <returns>A <see cref="Task"/> representing the running operation</returns>
Task HandleEvent(EventType eventType, IEnumerable<string> parameters, CancellationToken cancellationToken);
/// <returns>A <see cref="Task{TResult}"/> resulting in <see langword="true"/> if more <see cref="IEventConsumer"/> should run, <see langword="false"/> otherwise</returns>
Task<bool> HandleEvent(EventType eventType, IEnumerable<string> parameters, CancellationToken cancellationToken);
}
}
@@ -6,6 +6,7 @@ using Tgstation.Server.Host.Components.Chat;
using Tgstation.Server.Host.Components.Chat.Commands;
using Tgstation.Server.Host.Components.Compiler;
using Tgstation.Server.Host.Components.Repository;
using Tgstation.Server.Host.Components.StaticFiles;
using Tgstation.Server.Host.Components.Watchdog;
using Tgstation.Server.Host.Core;
using Tgstation.Server.Host.IO;
@@ -81,6 +82,11 @@ namespace Tgstation.Server.Host.Components
/// </summary>
readonly IProviderFactory providerFactory;
/// <summary>
/// The <see cref="IScriptExecutor"/> for the <see cref="InstanceFactory"/>
/// </summary>
readonly IScriptExecutor scriptExecutor;
/// <summary>
/// Construct an <see cref="InstanceFactory"/>
/// </summary>
@@ -97,7 +103,8 @@ namespace Tgstation.Server.Host.Components
/// <param name="symlinkFactory">The value of <see cref="symlinkFactory"/></param>
/// <param name="byondInstaller">The value of <see cref="byondInstaller"/></param>
/// <param name="providerFactory">The value of <see cref="providerFactory"/></param>
public InstanceFactory(IIOManager ioManager, IDatabaseContextFactory databaseContextFactory, IApplication application, ILoggerFactory loggerFactory, IByondTopicSender byondTopicSender, IServerUpdater serverUpdater, ICryptographySuite cryptographySuite, IExecutor executor, ICommandFactory commandFactory, ISynchronousIOManager synchronousIOManager, ISymlinkFactory symlinkFactory, IByondInstaller byondInstaller, IProviderFactory providerFactory)
/// <param name="scriptExecutor">The value of <see cref="scriptExecutor"/></param>
public InstanceFactory(IIOManager ioManager, IDatabaseContextFactory databaseContextFactory, IApplication application, ILoggerFactory loggerFactory, IByondTopicSender byondTopicSender, IServerUpdater serverUpdater, ICryptographySuite cryptographySuite, IExecutor executor, ICommandFactory commandFactory, ISynchronousIOManager synchronousIOManager, ISymlinkFactory symlinkFactory, IByondInstaller byondInstaller, IProviderFactory providerFactory, IScriptExecutor scriptExecutor)
{
this.ioManager = ioManager ?? throw new ArgumentNullException(nameof(ioManager));
this.databaseContextFactory = databaseContextFactory ?? throw new ArgumentNullException(nameof(databaseContextFactory));
@@ -112,6 +119,7 @@ namespace Tgstation.Server.Host.Components
this.symlinkFactory = symlinkFactory ?? throw new ArgumentNullException(nameof(symlinkFactory));
this.byondInstaller = byondInstaller ?? throw new ArgumentNullException(nameof(byondInstaller));
this.providerFactory = providerFactory ?? throw new ArgumentNullException(nameof(providerFactory));
this.scriptExecutor = scriptExecutor ?? throw new ArgumentNullException(nameof(scriptExecutor));
}
/// <inheritdoc />
@@ -126,6 +134,9 @@ namespace Tgstation.Server.Host.Components
var gameIoManager = new ResolvingIOManager(instanceIoManager, "Game");
var configurationIoManager = new ResolvingIOManager(instanceIoManager, "Configuration");
var configuration = new StaticFiles.Configuration(configurationIoManager, synchronousIOManager, symlinkFactory, scriptExecutor, loggerFactory.CreateLogger<StaticFiles.Configuration>());
var eventConsumer = new EventConsumer(configuration);
var dmbFactory = new DmbFactory(databaseContextFactory, gameIoManager, metadata.CloneMetadata());
try
{
@@ -136,18 +147,18 @@ namespace Tgstation.Server.Host.Components
try
{
var byond = new ByondManager(byondIOManager, byondInstaller, loggerFactory.CreateLogger<ByondManager>());
var configuration = new StaticFiles.Configuration(configurationIoManager, synchronousIOManager, symlinkFactory, loggerFactory.CreateLogger<StaticFiles.Configuration>());
var chat = chatFactory.CreateChat(metadata.ChatSettings);
try
{
var sessionControllerFactory = new SessionControllerFactory(executor, byond, byondTopicSender, interopRegistrar, cryptographySuite, application, gameIoManager, chat, loggerFactory, metadata.CloneMetadata());
var reattachInfoHandler = new ReattachInfoHandler(databaseContextFactory, dmbFactory, metadata.CloneMetadata());
var watchdogFactory = new WatchdogFactory(chat, sessionControllerFactory, serverUpdater, loggerFactory, reattachInfoHandler, databaseContextFactory, byondTopicSender, metadata.CloneMetadata());
var watchdogFactory = new WatchdogFactory(chat, sessionControllerFactory, serverUpdater, loggerFactory, reattachInfoHandler, databaseContextFactory, byondTopicSender, eventConsumer, metadata.CloneMetadata());
var watchdog = watchdogFactory.CreateWatchdog(dmbFactory, metadata.DreamDaemonSettings);
eventConsumer.SetWatchdog(watchdog);
try
{
var dreamMaker = new DreamMaker(byond, ioManager, configuration, sessionControllerFactory, dmbFactory, application, watchdog, loggerFactory.CreateLogger<DreamMaker>());
var dreamMaker = new DreamMaker(byond, ioManager, configuration, sessionControllerFactory, dmbFactory, application, eventConsumer, loggerFactory.CreateLogger<DreamMaker>());
return new Instance(metadata.CloneMetadata(), repoManager, byond, dreamMaker, watchdog, chat, configuration, dmbFactory, databaseContextFactory, dmbFactory);
}
@@ -4,6 +4,7 @@ using System.Collections.Generic;
using System.Globalization;
using System.IO;
using System.Linq;
using System.Runtime.InteropServices;
using System.Security.Cryptography;
using System.Threading;
using System.Threading.Tasks;
@@ -23,6 +24,12 @@ namespace Tgstation.Server.Host.Components.StaticFiles
const string CodeModificationsHeadFile = "HeadInclude.dm";
const string CodeModificationsTailFile = "TailInclude.dm";
static readonly IReadOnlyDictionary<EventType, string> EventTypeScriptFileNameMap = new Dictionary<EventType, string>
{
};
static readonly string SystemScriptFileExtension = RuntimeInformation.IsOSPlatform(OSPlatform.Windows) ? ".bat" : ".sh";
/// <summary>
/// The <see cref="IIOManager"/> for <see cref="Configuration"/>
/// </summary>
@@ -38,6 +45,11 @@ namespace Tgstation.Server.Host.Components.StaticFiles
/// </summary>
readonly ISymlinkFactory symlinkFactory;
/// <summary>
/// The <see cref="IScriptExecutor"/> for <see cref="Configuration"/>
/// </summary>
readonly IScriptExecutor scriptExecutor;
/// <summary>
/// The <see cref="ILogger"/> for <see cref="Configuration"/>
/// </summary>
@@ -49,12 +61,14 @@ namespace Tgstation.Server.Host.Components.StaticFiles
/// <param name="ioManager">The value of <see cref="ioManager"/></param>
/// <param name="synchronousIOManager">The value of <see cref="synchronousIOManager"/></param>
/// <param name="symlinkFactory">The value of <see cref="symlinkFactory"/></param>
/// <param name="scriptExecutor">The value of <see cref="scriptExecutor"/></param>
/// <param name="logger">The value of <see cref="logger"/></param>
public Configuration(IIOManager ioManager, ISynchronousIOManager synchronousIOManager, ISymlinkFactory symlinkFactory, ILogger<Configuration> logger)
public Configuration(IIOManager ioManager, ISynchronousIOManager synchronousIOManager, ISymlinkFactory symlinkFactory, IScriptExecutor scriptExecutor, ILogger<Configuration> logger)
{
this.ioManager = ioManager ?? throw new ArgumentNullException(nameof(ioManager));
this.synchronousIOManager = synchronousIOManager ?? throw new ArgumentNullException(nameof(synchronousIOManager));
this.symlinkFactory = symlinkFactory ?? throw new ArgumentNullException(nameof(symlinkFactory));
this.scriptExecutor = scriptExecutor ?? throw new ArgumentNullException(nameof(scriptExecutor));
this.logger = logger ?? throw new ArgumentNullException(nameof(logger));
}
@@ -253,5 +267,21 @@ namespace Tgstation.Server.Host.Components.StaticFiles
/// <inheritdoc />
public Task StopAsync(CancellationToken cancellationToken) => EnsureDirectories(cancellationToken);
/// <inheritdoc />
public async Task<bool> HandleEvent(EventType eventType, IEnumerable<string> parameters, CancellationToken cancellationToken)
{
await EnsureDirectories(cancellationToken).ConfigureAwait(false);
if (!EventTypeScriptFileNameMap.TryGetValue(eventType, out var scriptName))
return true;
var files = await ioManager.GetFilesWithExtension(EventScriptsSubdirectory, SystemScriptFileExtension, cancellationToken).ConfigureAwait(false);
var resolvedScriptsDir = ioManager.ResolvePath(EventScriptsSubdirectory);
foreach (var I in files.Where(x => x.StartsWith(scriptName, StringComparison.Ordinal)))
if ((await scriptExecutor.ExecuteScript(ioManager.ConcatPath(resolvedScriptsDir, I), parameters, cancellationToken).ConfigureAwait(false)) != 0)
return false;
return true;
}
}
}
@@ -10,7 +10,7 @@ namespace Tgstation.Server.Host.Components.StaticFiles
/// <summary>
/// For managing the Configuration directory
/// </summary>
public interface IConfiguration : IHostedService
public interface IConfiguration : IHostedService, IEventConsumer
{
/// <summary>
/// Copies all files in the CodeModifications directory to <paramref name="destination"/>
@@ -0,0 +1,21 @@
using System.Collections.Generic;
using System.Threading;
using System.Threading.Tasks;
namespace Tgstation.Server.Host.Components.StaticFiles
{
/// <summary>
/// For executing system shell scripts
/// </summary>
interface IScriptExecutor
{
/// <summary>
/// Execute a shell script and get the result
/// </summary>
/// <param name="scriptPath">The absolute path to the script</param>
/// <param name="parameters">Command line parameters for the script</param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> for the operation</param>
/// <returns>A <see cref="Task{TResult}"/> resulting in the <see cref="System.Diagnostics.Process.ExitCode"/> or <see langword="null"/> if an error occurred</returns>
Task<int?> ExecuteScript(string scriptPath, IEnumerable<string> parameters, CancellationToken cancellationToken);
}
}
@@ -0,0 +1,75 @@
using Microsoft.Extensions.Logging;
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Threading;
using System.Threading.Tasks;
using Tgstation.Server.Host.IO;
namespace Tgstation.Server.Host.Components.StaticFiles
{
/// <inheritdoc />
sealed class ScriptExecutor : IScriptExecutor
{
/// <summary>
/// The <see cref="IIOManager"/> for the <see cref="ScriptExecutor"/>
/// </summary>
readonly IIOManager ioManager;
/// <summary>
/// The <see cref="ILogger"/> for the <see cref="ScriptExecutor"/>
/// </summary>
readonly ILogger<ScriptExecutor> logger;
/// <summary>
/// Construct a <see cref="ScriptExecutor"/>
/// </summary>
/// <param name="ioManager">The value of <see cref="ioManager"/></param>
/// <param name="logger">The value of <see cref="logger"/></param>
public ScriptExecutor(IIOManager ioManager, ILogger<ScriptExecutor> logger)
{
this.ioManager = ioManager ?? throw new ArgumentNullException(nameof(ioManager));
this.logger = logger ?? throw new ArgumentNullException(nameof(logger));
}
/// <inheritdoc />
public async Task<int?> ExecuteScript(string scriptPath, IEnumerable<string> parameters, CancellationToken cancellationToken)
{
try
{
using (var process = new Process())
{
process.StartInfo.FileName = scriptPath;
process.StartInfo.WorkingDirectory = ioManager.GetDirectoryName(scriptPath);
process.StartInfo.Arguments = String.Join(" ", parameters);
process.EnableRaisingEvents = true;
var tcs = new TaskCompletionSource<object>();
process.Exited += (a, b) => tcs.SetResult(null);
try
{
process.Start();
using (cancellationToken.Register(() => tcs.SetCanceled()))
await tcs.Task.ConfigureAwait(false);
}
catch (InvalidOperationException)
{
try
{
process.Kill();
process.WaitForExit();
}
catch (InvalidOperationException) { }
}
return process.ExitCode;
}
}
catch (Exception e)
{
logger.LogWarning("Error running shell script {0}! Exception: {1}", scriptPath, e);
return null;
}
}
}
}
@@ -76,6 +76,11 @@ namespace Tgstation.Server.Host.Components.Watchdog
/// </summary>
readonly IByondTopicSender byondTopicSender;
/// <summary>
/// The <see cref="IEventConsumer"/> for the <see cref="Watchdog"/>
/// </summary>
readonly IEventConsumer eventConsumer;
/// <summary>
/// The <see cref="SemaphoreSlim"/> for the <see cref="Watchdog"/>
/// </summary>
@@ -134,7 +139,8 @@ namespace Tgstation.Server.Host.Components.Watchdog
/// <param name="initialLaunchParameters">The initial value of <see cref="ActiveLaunchParameters"/></param>
/// <param name="instance">The value of <see cref="instance"/></param>
/// <param name="autoStart">The value of <see cref="autoStart"/></param>
public Watchdog(IChat chat, ISessionControllerFactory sessionControllerFactory, IDmbFactory dmbFactory, IServerUpdater serverUpdater, ILogger<Watchdog> logger, IReattachInfoHandler reattachInfoHandler, IDatabaseContextFactory databaseContextFactory, IByondTopicSender byondTopicSender, DreamDaemonLaunchParameters initialLaunchParameters, Api.Models.Instance instance, bool autoStart)
/// <param name="eventConsumer">The value of <see cref="eventConsumer"/></param>
public Watchdog(IChat chat, ISessionControllerFactory sessionControllerFactory, IDmbFactory dmbFactory, IServerUpdater serverUpdater, ILogger<Watchdog> logger, IReattachInfoHandler reattachInfoHandler, IDatabaseContextFactory databaseContextFactory, IByondTopicSender byondTopicSender, IEventConsumer eventConsumer, DreamDaemonLaunchParameters initialLaunchParameters, Api.Models.Instance instance, bool autoStart)
{
this.chat = chat ?? throw new ArgumentNullException(nameof(chat));
this.sessionControllerFactory = sessionControllerFactory ?? throw new ArgumentNullException(nameof(sessionControllerFactory));
@@ -143,6 +149,7 @@ namespace Tgstation.Server.Host.Components.Watchdog
this.reattachInfoHandler = reattachInfoHandler ?? throw new ArgumentNullException(nameof(reattachInfoHandler));
this.databaseContextFactory = databaseContextFactory ?? throw new ArgumentNullException(nameof(databaseContextFactory));
this.byondTopicSender = byondTopicSender ?? throw new ArgumentNullException(nameof(byondTopicSender));
this.eventConsumer = eventConsumer ?? throw new ArgumentNullException(nameof(eventConsumer));
this.instance = instance ?? throw new ArgumentNullException(nameof(instance));
this.autoStart = autoStart;
@@ -775,13 +782,13 @@ namespace Tgstation.Server.Host.Components.Watchdog
}
/// <inheritdoc />
public async Task HandleEvent(EventType eventType, IEnumerable<string> parameters, CancellationToken cancellationToken)
public async Task<bool> HandleEvent(EventType eventType, IEnumerable<string> parameters, CancellationToken cancellationToken)
{
string results;
using (await SemaphoreSlimContext.Lock(semaphore, cancellationToken).ConfigureAwait(false))
{
if (!Running)
return;
return true;
var builder = new StringBuilder(InteropConstants.DMTopicEvent);
foreach (var I in parameters)
@@ -795,7 +802,7 @@ namespace Tgstation.Server.Host.Components.Watchdog
}
if (results == null)
return;
return true;
List<Response> responses;
try
@@ -805,10 +812,12 @@ namespace Tgstation.Server.Host.Components.Watchdog
catch
{
logger.LogInformation("Recieved invalid response from DD when parsing event {0}:{1}{2}", eventType, Environment.NewLine, results);
return;
return true;
}
await Task.WhenAll(responses.Select(x => chat.SendMessage(x.Message, x.ChannelIds, cancellationToken))).ConfigureAwait(false);
return true;
}
/// <inheritdoc />
@@ -46,11 +46,16 @@ namespace Tgstation.Server.Host.Components.Watchdog
/// </summary>
readonly IByondTopicSender byondTopicSender;
/// <summary>
/// The <see cref="IEventConsumer"/> for the <see cref="WatchdogFactory"/>
/// </summary>
readonly IEventConsumer eventConsumer;
/// <summary>
/// The <see cref="Api.Models.Instance"/> for the <see cref="WatchdogFactory"/>
/// </summary>
readonly Api.Models.Instance instance;
/// <summary>
/// Construct a <see cref="WatchdogFactory"/>
@@ -62,8 +67,9 @@ namespace Tgstation.Server.Host.Components.Watchdog
/// <param name="reattachInfoHandler">The value of <see cref="reattachInfoHandler"/></param>
/// <param name="databaseContextFactory">The value of <see cref="databaseContextFactory"/></param>
/// <param name="byondTopicSender">The value of <see cref="byondTopicSender"/></param>
/// <param name="eventConsumer">The value of <see cref="eventConsumer"/></param>
/// <param name="instance">The value of <see cref="instance"/></param>
public WatchdogFactory(IChat chat, ISessionControllerFactory sessionControllerFactory, IServerUpdater serverUpdater, ILoggerFactory loggerFactory, IReattachInfoHandler reattachInfoHandler, IDatabaseContextFactory databaseContextFactory, IByondTopicSender byondTopicSender, Api.Models.Instance instance)
public WatchdogFactory(IChat chat, ISessionControllerFactory sessionControllerFactory, IServerUpdater serverUpdater, ILoggerFactory loggerFactory, IReattachInfoHandler reattachInfoHandler, IDatabaseContextFactory databaseContextFactory, IByondTopicSender byondTopicSender, IEventConsumer eventConsumer, Api.Models.Instance instance)
{
this.chat = chat ?? throw new ArgumentNullException(nameof(chat));
this.sessionControllerFactory = sessionControllerFactory ?? throw new ArgumentNullException(nameof(sessionControllerFactory));
@@ -72,10 +78,11 @@ namespace Tgstation.Server.Host.Components.Watchdog
this.reattachInfoHandler = reattachInfoHandler ?? throw new ArgumentNullException(nameof(reattachInfoHandler));
this.databaseContextFactory = databaseContextFactory ?? throw new ArgumentNullException(nameof(databaseContextFactory));
this.byondTopicSender = byondTopicSender ?? throw new ArgumentNullException(nameof(byondTopicSender));
this.eventConsumer = eventConsumer ?? throw new ArgumentNullException(nameof(eventConsumer));
this.instance = instance ?? throw new ArgumentNullException(nameof(instance));
}
/// <inheritdoc />
public IWatchdog CreateWatchdog(IDmbFactory dmbFactory, DreamDaemonSettings settings) => new Watchdog(chat, sessionControllerFactory, dmbFactory, serverUpdater, loggerFactory.CreateLogger<Watchdog>(), reattachInfoHandler, databaseContextFactory, byondTopicSender, settings, instance, settings.AutoStart.Value);
public IWatchdog CreateWatchdog(IDmbFactory dmbFactory, DreamDaemonSettings settings) => new Watchdog(chat, sessionControllerFactory, dmbFactory, serverUpdater, loggerFactory.CreateLogger<Watchdog>(), reattachInfoHandler, databaseContextFactory, byondTopicSender, eventConsumer, settings, instance, settings.AutoStart.Value);
}
}