diff --git a/src/Tgstation.Server.Host/Components/EventConsumer.cs b/src/Tgstation.Server.Host/Components/EventConsumer.cs new file mode 100644 index 0000000000..83e1c5ae69 --- /dev/null +++ b/src/Tgstation.Server.Host/Components/EventConsumer.cs @@ -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 +{ + /// + sealed class EventConsumer : IEventConsumer + { + /// + /// The for the + /// + readonly IConfiguration configuration; + + /// + /// The for the + /// + IWatchdog watchdog; + + /// + /// Construct an + /// + /// The value of + public EventConsumer(IConfiguration configuration) + { + this.configuration = configuration ?? throw new ArgumentNullException(nameof(configuration)); + } + + /// + public async Task HandleEvent(EventType eventType, IEnumerable 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); + } + + /// + /// Set the for the + /// + /// The value of + public void SetWatchdog(IWatchdog watchdog) + { + if (watchdog != null) + throw new InvalidOperationException("watchdog already set!"); + this.watchdog = watchdog; + } + } +} diff --git a/src/Tgstation.Server.Host/Components/IEventConsumer.cs b/src/Tgstation.Server.Host/Components/IEventConsumer.cs index 986f9f2213..55945548c2 100644 --- a/src/Tgstation.Server.Host/Components/IEventConsumer.cs +++ b/src/Tgstation.Server.Host/Components/IEventConsumer.cs @@ -15,7 +15,7 @@ namespace Tgstation.Server.Host.Components /// The /// The parameters for /// The for the operation - /// A representing the running operation - Task HandleEvent(EventType eventType, IEnumerable parameters, CancellationToken cancellationToken); + /// A resulting in if more should run, otherwise + Task HandleEvent(EventType eventType, IEnumerable parameters, CancellationToken cancellationToken); } } diff --git a/src/Tgstation.Server.Host/Components/InstanceFactory.cs b/src/Tgstation.Server.Host/Components/InstanceFactory.cs index 9bb2a2b73d..f6c402c1fa 100644 --- a/src/Tgstation.Server.Host/Components/InstanceFactory.cs +++ b/src/Tgstation.Server.Host/Components/InstanceFactory.cs @@ -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 /// readonly IProviderFactory providerFactory; + /// + /// The for the + /// + readonly IScriptExecutor scriptExecutor; + /// /// Construct an /// @@ -97,7 +103,8 @@ namespace Tgstation.Server.Host.Components /// The value of /// The value of /// The value of - 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) + /// The value of + 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)); } /// @@ -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()); + 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()); - var configuration = new StaticFiles.Configuration(configurationIoManager, synchronousIOManager, symlinkFactory, loggerFactory.CreateLogger()); 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()); + var dreamMaker = new DreamMaker(byond, ioManager, configuration, sessionControllerFactory, dmbFactory, application, eventConsumer, loggerFactory.CreateLogger()); return new Instance(metadata.CloneMetadata(), repoManager, byond, dreamMaker, watchdog, chat, configuration, dmbFactory, databaseContextFactory, dmbFactory); } diff --git a/src/Tgstation.Server.Host/Components/StaticFiles/Configuration.cs b/src/Tgstation.Server.Host/Components/StaticFiles/Configuration.cs index d014ccd90d..6442c22e7c 100644 --- a/src/Tgstation.Server.Host/Components/StaticFiles/Configuration.cs +++ b/src/Tgstation.Server.Host/Components/StaticFiles/Configuration.cs @@ -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 EventTypeScriptFileNameMap = new Dictionary + { + }; + + static readonly string SystemScriptFileExtension = RuntimeInformation.IsOSPlatform(OSPlatform.Windows) ? ".bat" : ".sh"; + /// /// The for /// @@ -38,6 +45,11 @@ namespace Tgstation.Server.Host.Components.StaticFiles /// readonly ISymlinkFactory symlinkFactory; + /// + /// The for + /// + readonly IScriptExecutor scriptExecutor; + /// /// The for /// @@ -49,12 +61,14 @@ namespace Tgstation.Server.Host.Components.StaticFiles /// The value of /// The value of /// The value of + /// The value of /// The value of - public Configuration(IIOManager ioManager, ISynchronousIOManager synchronousIOManager, ISymlinkFactory symlinkFactory, ILogger logger) + public Configuration(IIOManager ioManager, ISynchronousIOManager synchronousIOManager, ISymlinkFactory symlinkFactory, IScriptExecutor scriptExecutor, ILogger 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 /// public Task StopAsync(CancellationToken cancellationToken) => EnsureDirectories(cancellationToken); + + /// + public async Task HandleEvent(EventType eventType, IEnumerable 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; + } } } diff --git a/src/Tgstation.Server.Host/Components/StaticFiles/IConfiguration.cs b/src/Tgstation.Server.Host/Components/StaticFiles/IConfiguration.cs index 433e532413..883568558e 100644 --- a/src/Tgstation.Server.Host/Components/StaticFiles/IConfiguration.cs +++ b/src/Tgstation.Server.Host/Components/StaticFiles/IConfiguration.cs @@ -10,7 +10,7 @@ namespace Tgstation.Server.Host.Components.StaticFiles /// /// For managing the Configuration directory /// - public interface IConfiguration : IHostedService + public interface IConfiguration : IHostedService, IEventConsumer { /// /// Copies all files in the CodeModifications directory to diff --git a/src/Tgstation.Server.Host/Components/StaticFiles/IScriptExecutor.cs b/src/Tgstation.Server.Host/Components/StaticFiles/IScriptExecutor.cs new file mode 100644 index 0000000000..a9e26ff72f --- /dev/null +++ b/src/Tgstation.Server.Host/Components/StaticFiles/IScriptExecutor.cs @@ -0,0 +1,21 @@ +using System.Collections.Generic; +using System.Threading; +using System.Threading.Tasks; + +namespace Tgstation.Server.Host.Components.StaticFiles +{ + /// + /// For executing system shell scripts + /// + interface IScriptExecutor + { + /// + /// Execute a shell script and get the result + /// + /// The absolute path to the script + /// Command line parameters for the script + /// The for the operation + /// A resulting in the or if an error occurred + Task ExecuteScript(string scriptPath, IEnumerable parameters, CancellationToken cancellationToken); + } +} diff --git a/src/Tgstation.Server.Host/Components/StaticFiles/ScriptExecutor.cs b/src/Tgstation.Server.Host/Components/StaticFiles/ScriptExecutor.cs new file mode 100644 index 0000000000..7aa7409d4c --- /dev/null +++ b/src/Tgstation.Server.Host/Components/StaticFiles/ScriptExecutor.cs @@ -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 +{ + /// + sealed class ScriptExecutor : IScriptExecutor + { + /// + /// The for the + /// + readonly IIOManager ioManager; + + /// + /// The for the + /// + readonly ILogger logger; + + /// + /// Construct a + /// + /// The value of + /// The value of + public ScriptExecutor(IIOManager ioManager, ILogger logger) + { + this.ioManager = ioManager ?? throw new ArgumentNullException(nameof(ioManager)); + this.logger = logger ?? throw new ArgumentNullException(nameof(logger)); + } + + /// + public async Task ExecuteScript(string scriptPath, IEnumerable 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(); + 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; + } + } + } +} diff --git a/src/Tgstation.Server.Host/Components/Watchdog/Watchdog.cs b/src/Tgstation.Server.Host/Components/Watchdog/Watchdog.cs index 4a5249e4e8..c27db4fed0 100644 --- a/src/Tgstation.Server.Host/Components/Watchdog/Watchdog.cs +++ b/src/Tgstation.Server.Host/Components/Watchdog/Watchdog.cs @@ -76,6 +76,11 @@ namespace Tgstation.Server.Host.Components.Watchdog /// readonly IByondTopicSender byondTopicSender; + /// + /// The for the + /// + readonly IEventConsumer eventConsumer; + /// /// The for the /// @@ -134,7 +139,8 @@ namespace Tgstation.Server.Host.Components.Watchdog /// The initial value of /// The value of /// The value of - public Watchdog(IChat chat, ISessionControllerFactory sessionControllerFactory, IDmbFactory dmbFactory, IServerUpdater serverUpdater, ILogger logger, IReattachInfoHandler reattachInfoHandler, IDatabaseContextFactory databaseContextFactory, IByondTopicSender byondTopicSender, DreamDaemonLaunchParameters initialLaunchParameters, Api.Models.Instance instance, bool autoStart) + /// The value of + public Watchdog(IChat chat, ISessionControllerFactory sessionControllerFactory, IDmbFactory dmbFactory, IServerUpdater serverUpdater, ILogger 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 } /// - public async Task HandleEvent(EventType eventType, IEnumerable parameters, CancellationToken cancellationToken) + public async Task HandleEvent(EventType eventType, IEnumerable 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 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; } /// diff --git a/src/Tgstation.Server.Host/Components/Watchdog/WatchdogFactory.cs b/src/Tgstation.Server.Host/Components/Watchdog/WatchdogFactory.cs index 2df5df1d9e..2cf4f407af 100644 --- a/src/Tgstation.Server.Host/Components/Watchdog/WatchdogFactory.cs +++ b/src/Tgstation.Server.Host/Components/Watchdog/WatchdogFactory.cs @@ -46,11 +46,16 @@ namespace Tgstation.Server.Host.Components.Watchdog /// readonly IByondTopicSender byondTopicSender; + /// + /// The for the + /// + readonly IEventConsumer eventConsumer; + /// /// The for the /// readonly Api.Models.Instance instance; - + /// /// Construct a @@ -62,8 +67,9 @@ namespace Tgstation.Server.Host.Components.Watchdog /// The value of /// The value of /// The value of + /// The value of /// The value of - 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)); } /// - public IWatchdog CreateWatchdog(IDmbFactory dmbFactory, DreamDaemonSettings settings) => new Watchdog(chat, sessionControllerFactory, dmbFactory, serverUpdater, loggerFactory.CreateLogger(), reattachInfoHandler, databaseContextFactory, byondTopicSender, settings, instance, settings.AutoStart.Value); + public IWatchdog CreateWatchdog(IDmbFactory dmbFactory, DreamDaemonSettings settings) => new Watchdog(chat, sessionControllerFactory, dmbFactory, serverUpdater, loggerFactory.CreateLogger(), reattachInfoHandler, databaseContextFactory, byondTopicSender, eventConsumer, settings, instance, settings.AutoStart.Value); } }