From fff7d1b10a9e69efd293d4fc1003ac79763d463e Mon Sep 17 00:00:00 2001 From: Cyberboss Date: Mon, 23 Apr 2018 15:04:13 -0400 Subject: [PATCH] WIP as usual --- .../Models/DreamDaemon.cs | 2 +- .../Components/DreamDaemon.cs | 276 ++++++++++++++++++ .../Components/IByond.cs | 7 + .../Components/IDreamDaemon.cs | 20 +- .../Components/IInstanceManager.cs | 2 +- .../Components/IInstanceShutdownMethod.cs | 11 + .../Components/IInterop.cs | 6 +- .../Components/InstanceManager.cs | 3 + .../Components/ServerControlEventArgs.cs | 8 +- .../Security/CryptographySuite.cs | 3 + .../Security/ICryptographySuite.cs | 6 + src/Tgstation.Server.Host/Server.cs | 4 +- 12 files changed, 336 insertions(+), 12 deletions(-) create mode 100644 src/Tgstation.Server.Host/Components/DreamDaemon.cs create mode 100644 src/Tgstation.Server.Host/Components/IInstanceShutdownMethod.cs diff --git a/src/Tgstation.Server.Api/Models/DreamDaemon.cs b/src/Tgstation.Server.Api/Models/DreamDaemon.cs index 49571a3fa3..4a332b36dd 100644 --- a/src/Tgstation.Server.Api/Models/DreamDaemon.cs +++ b/src/Tgstation.Server.Api/Models/DreamDaemon.cs @@ -18,7 +18,7 @@ namespace Tgstation.Server.Api.Models /// The current status of /// [Permissions(DenyWrite = true, ReadRight = DreamDaemonRights.ReadMetadata)] - public DreamDaemonStatus? Status { get; set; } + public bool? Running { get; set; } /// /// The current of diff --git a/src/Tgstation.Server.Host/Components/DreamDaemon.cs b/src/Tgstation.Server.Host/Components/DreamDaemon.cs new file mode 100644 index 0000000000..6ca1a8fc6b --- /dev/null +++ b/src/Tgstation.Server.Host/Components/DreamDaemon.cs @@ -0,0 +1,276 @@ +using System; +using System.Diagnostics; +using System.Globalization; +using System.Threading; +using System.Threading.Tasks; +using Tgstation.Server.Api.Models; +using Tgstation.Server.Api.Models.Internal; +using Tgstation.Server.Host.Core; +using Tgstation.Server.Host.Security; + +namespace Tgstation.Server.Host.Components +{ + /// + sealed class DreamDaemon : IDreamDaemon, IDisposable + { + /// + public bool Running { get; private set; } + + /// + public ushort? CurrentPort { get; private set; } + + /// + public string AccessToken { get; private set; } + + /// + public DreamDaemonSecurity? CurrentSecurity { get; private set; } + + /// + public bool SoftRebooting { get; private set; } + + /// + public bool SoftStopping { get; private set; } + + /// + /// The for + /// + readonly IEventConsumer eventConsumer; + /// + /// The for + /// + readonly IByond byond; + /// + /// The for + /// + readonly ICryptographySuite cryptographySuite; + /// + /// The for + /// + readonly IInterop interop; + /// + /// The for + /// + readonly IInstanceShutdownMethod instanceShutdownMethod; + readonly IIOManager ioManager; + + readonly bool autoStart; + + readonly SemaphoreSlim semaphore; + + DreamDaemonLaunchParameters currentLaunchParameters; + + CancellationTokenSource watchdogCancellationTokenSource; + Task watchdogTask; + + public DreamDaemon(IEventConsumer eventConsumer, IByond byond, ICryptographySuite cryptographySuite, IInterop interop, IInstanceShutdownMethod instanceShutdownMethod, DreamDaemonSettings initialSettings) + { + this.eventConsumer = eventConsumer ?? throw new ArgumentNullException(nameof(eventConsumer)); + this.byond = byond ?? throw new ArgumentNullException(nameof(byond)); + this.cryptographySuite = cryptographySuite ?? throw new ArgumentNullException(nameof(cryptographySuite)); + this.interop = interop ?? throw new ArgumentNullException(nameof(interop)); + this.instanceShutdownMethod = instanceShutdownMethod ?? throw new ArgumentNullException(nameof(instanceShutdownMethod)); + + interop.SetServerControlHandler(OnServerControl); + + currentLaunchParameters = initialSettings ?? throw new ArgumentNullException(nameof(initialSettings)); + autoStart = initialSettings.AutoStart; + + semaphore = new SemaphoreSlim(1); + } + + public void Dispose() + { + if (watchdogCancellationTokenSource != null) + watchdogCancellationTokenSource.Dispose(); + semaphore.Dispose(); + } + + Task OnServerControl(ServerControlEventArgs serverControlEventArgs, CancellationToken cancellationToken) + { + lock (this) + { + CancelGracefulActions(); + if (serverControlEventArgs.ProcessRestart) + return Restart(serverControlEventArgs.Graceful, cancellationToken); + if(!serverControlEventArgs.ServerReboot) + return Terminate(serverControlEventArgs.Graceful, cancellationToken); + return Task.CompletedTask; + } + } + + static string SecurityWord(DreamDaemonSecurity securityLevel) + { + switch (securityLevel) + { + case DreamDaemonSecurity.Safe: + return "safe"; + case DreamDaemonSecurity.Trusted: + return "trusted"; + case DreamDaemonSecurity.Ultrasafe: + return "ultrasafe"; + default: + throw new ArgumentOutOfRangeException(nameof(securityLevel), securityLevel, String.Format(CultureInfo.InvariantCulture, "Bad DreamDaemon security level: {0}", securityLevel)); + } + } + + async Task Watchdog(DreamDaemonLaunchParameters launchParameters, TaskCompletionSource onSuccessfulStartup, CancellationToken cancellationToken) + { + async Task RunOnce(string executablePath) + { + if (await byond.GetVersion(cancellationToken).ConfigureAwait(false) == null) + throw new InvalidOperationException("No byond version installed!"); + + await byond.ClearCache(cancellationToken).ConfigureAwait(false); + using (var proc = new Process()) + { + var accessToken = cryptographySuite.GetSecureString(); + proc.StartInfo.FileName = executablePath; + proc.StartInfo.Arguments = String.Format(CultureInfo.InvariantCulture, "{0} -port {1} {5}-close -verbose -params \"server_service={3}&server_service_version={4}&{6}={7}\" -{2} -public", DMB, launchParameters.PrimaryPort, SecurityWord(launchParameters.SecurityLevel), accessToken, Application.Version, launchParameters.AllowWebClient ? "-webclient " : String.Empty); + + proc.EnableRaisingEvents = true; + var tcs = new TaskCompletionSource(); + proc.Exited += (a, b) => tcs.SetResult(null); + + try + { + proc.Start(); + + await Task.Factory.StartNew(() => proc.WaitForInputIdle(), cancellationToken, TaskCreationOptions.LongRunning, TaskScheduler.Current).ConfigureAwait(false); + + if (onSuccessfulStartup != null) + { + onSuccessfulStartup.SetResult(null); + onSuccessfulStartup = null; + } + + try + { + using (cancellationToken.Register(() => tcs.SetCanceled())) + await tcs.Task.ConfigureAwait(false); + } + catch (OperationCanceledException) + { + return; + } + finally + { + if (!instanceShutdownMethod.GracefulShutdown) + { + proc.Kill(); + proc.WaitForExit(); + } + } + } + catch (Exception e) + { + onSuccessfulStartup?.SetException(e); + throw; + } + await eventConsumer.HandleEvent(proc.ExitCode != 0 ? EventType.DDCrash : EventType.DDExit, null, cancellationToken).ConfigureAwait(false); + } + }; + + do + { + await byond.UseExecutable(RunOnce, false, true).ConfigureAwait(false); + } while (!cancellationToken.IsCancellationRequested); + } + + public void CancelGracefulActions() + { + lock (this) + { + SoftRebooting = false; + SoftStopping = false; + } + } + + public async Task ChangeSettings(DreamDaemonLaunchParameters launchParameters, CancellationToken cancellationToken) + { + Task launchTask; + lock (this) + { + currentLaunchParameters = launchParameters; + if (!Running) + launchTask = Launch(launchParameters, cancellationToken); + else + launchTask = Restart(true, cancellationToken); + } + await launchTask.ConfigureAwait(false); + } + + public async Task Launch(DreamDaemonLaunchParameters launchParameters, CancellationToken cancellationToken) + { + await semaphore.WaitAsync(cancellationToken).ConfigureAwait(false); + try + { + if (Running) + throw new InvalidOperationException("DreamDaemon already running!"); + Running = true; + await eventConsumer.HandleEvent(EventType.DDLaunched, null, cancellationToken).ConfigureAwait(false); + watchdogCancellationTokenSource?.Dispose(); + watchdogCancellationTokenSource = new CancellationTokenSource(); + var startupTcs = new TaskCompletionSource(); + watchdogTask = Watchdog(currentLaunchParameters, startupTcs, watchdogCancellationTokenSource.Token); + await startupTcs.Task.ConfigureAwait(false); + } + finally + { + semaphore.Release(); + } + } + + + public async Task Restart(bool graceful, CancellationToken cancellationToken) + { + await semaphore.WaitAsync(cancellationToken).ConfigureAwait(false); + try + { + if (graceful) + { + if (!Running) + throw new InvalidOperationException("DreamDaemon not running!"); + if (SoftStopping) + throw new InvalidOperationException("DreamDaemon has a graceful stop queued!"); + SoftRebooting = true; + return; + } + await eventConsumer.HandleEvent(EventType.DDRestart, null, cancellationToken).ConfigureAwait(false); + if (Running) + watchdogCancellationTokenSource.Cancel(); + await Launch(currentLaunchParameters, cancellationToken).ConfigureAwait(false); + } + finally + { + semaphore.Release(); + } + } + + public Task StartAsync(CancellationToken cancellationToken) => autoStart ? Launch(currentLaunchParameters, cancellationToken) : Task.CompletedTask; + + public Task StopAsync(CancellationToken cancellationToken) => Terminate(false, cancellationToken); + + public async Task Terminate(bool graceful, CancellationToken cancellationToken) + { + await semaphore.WaitAsync(cancellationToken).ConfigureAwait(false); + try + { + if (graceful) + { + if (!Running) + throw new InvalidOperationException("DreamDaemon not running!"); + SoftRebooting = false; + SoftStopping = true; + return; + } + await eventConsumer.HandleEvent(EventType.DDTerminated, null, cancellationToken).ConfigureAwait(false); + watchdogCancellationTokenSource.Cancel(); + await watchdogTask.ConfigureAwait(false); + } + finally + { + semaphore.Release(); + } + } + } +} diff --git a/src/Tgstation.Server.Host/Components/IByond.cs b/src/Tgstation.Server.Host/Components/IByond.cs index ab3450872e..7f9d0ce98f 100644 --- a/src/Tgstation.Server.Host/Components/IByond.cs +++ b/src/Tgstation.Server.Host/Components/IByond.cs @@ -31,5 +31,12 @@ namespace Tgstation.Server.Host.Components /// Pass the path of dreamdaemon.exe to if dm.exe otherwise /// A representing the running Task UseExecutable(Func operation, bool stagedIfExists, bool dreamDaemon); + + /// + /// Clears the cache folder + /// + /// The for the operation + /// A representing the running operation + Task ClearCache(CancellationToken cancellationToken); } } \ No newline at end of file diff --git a/src/Tgstation.Server.Host/Components/IDreamDaemon.cs b/src/Tgstation.Server.Host/Components/IDreamDaemon.cs index f5cf744187..b49ac50e4c 100644 --- a/src/Tgstation.Server.Host/Components/IDreamDaemon.cs +++ b/src/Tgstation.Server.Host/Components/IDreamDaemon.cs @@ -1,6 +1,7 @@ using Microsoft.Extensions.Hosting; using System.Threading; using System.Threading.Tasks; +using Tgstation.Server.Api.Models; using Tgstation.Server.Api.Models.Internal; namespace Tgstation.Server.Host.Components @@ -15,11 +16,26 @@ namespace Tgstation.Server.Host.Components /// bool Running { get; } + /// + /// If DreamDaemon is going to soft reboot + /// + bool SoftRebooting { get; } + + /// + /// If DreamDaemon is going to soft stop + /// + bool SoftStopping { get; } + /// /// The port DreamDaemon is currently running on /// ushort? CurrentPort { get; } + /// + /// The current of + /// + DreamDaemonSecurity? CurrentSecurity { get; } + /// /// The access token used for communication with the DMAPI /// @@ -37,7 +53,9 @@ namespace Tgstation.Server.Host.Components /// Changes the if currently . Triggers a graceful restart /// /// The new - void ChangeSettings(DreamDaemonLaunchParameters launchParameters); + /// The for the operation + /// A representing the running operation + Task ChangeSettings(DreamDaemonLaunchParameters launchParameters, CancellationToken cancellationToken); /// /// Restarts DreamDaemon diff --git a/src/Tgstation.Server.Host/Components/IInstanceManager.cs b/src/Tgstation.Server.Host/Components/IInstanceManager.cs index d63384a3fd..356a2fd9c8 100644 --- a/src/Tgstation.Server.Host/Components/IInstanceManager.cs +++ b/src/Tgstation.Server.Host/Components/IInstanceManager.cs @@ -6,7 +6,7 @@ namespace Tgstation.Server.Host.Components /// /// For managing s /// - interface IInstanceManager + interface IInstanceManager : IInstanceShutdownMethod { /// /// Get the associated with given diff --git a/src/Tgstation.Server.Host/Components/IInstanceShutdownMethod.cs b/src/Tgstation.Server.Host/Components/IInstanceShutdownMethod.cs new file mode 100644 index 0000000000..14ced74d67 --- /dev/null +++ b/src/Tgstation.Server.Host/Components/IInstanceShutdownMethod.cs @@ -0,0 +1,11 @@ +using System; +using System.Collections.Generic; +using System.Text; + +namespace Tgstation.Server.Host.Components +{ + interface IInstanceShutdownMethod + { + bool GracefulShutdown { get; set; } + } +} diff --git a/src/Tgstation.Server.Host/Components/IInterop.cs b/src/Tgstation.Server.Host/Components/IInterop.cs index 7c1d0e67ad..bc42312136 100644 --- a/src/Tgstation.Server.Host/Components/IInterop.cs +++ b/src/Tgstation.Server.Host/Components/IInterop.cs @@ -9,15 +9,15 @@ namespace Tgstation.Server.Host.Components /// interface IInterop { - event EventHandler OnServerControl; - event EventHandler OnChatMessage; + void SetServerControlHandler(Func serverControlHandler); + void SetChatMessageHandler(Func chatMessageHandler); Version MinimumApiVersion { get; } Version MaximumApiVersion { get; } Task GetApiVersion(CancellationToken cancellationToken); - void SetActivePort(ushort? port); + void SetRun(ushort? port, string accessToken); Task ChatCommand(string command, string arguments, CancellationToken cancellationToken); diff --git a/src/Tgstation.Server.Host/Components/InstanceManager.cs b/src/Tgstation.Server.Host/Components/InstanceManager.cs index 92947c72a9..ae48802658 100644 --- a/src/Tgstation.Server.Host/Components/InstanceManager.cs +++ b/src/Tgstation.Server.Host/Components/InstanceManager.cs @@ -14,6 +14,9 @@ namespace Tgstation.Server.Host.Components /// sealed class InstanceManager : IInstanceManager, IHostedService { + /// + public bool GracefulShutdown { get; set; } + /// /// The for the /// diff --git a/src/Tgstation.Server.Host/Components/ServerControlEventArgs.cs b/src/Tgstation.Server.Host/Components/ServerControlEventArgs.cs index a0c12a243f..ba9b7b2bef 100644 --- a/src/Tgstation.Server.Host/Components/ServerControlEventArgs.cs +++ b/src/Tgstation.Server.Host/Components/ServerControlEventArgs.cs @@ -10,16 +10,16 @@ namespace Tgstation.Server.Host.Components /// /// If the event will result in a new run of the world /// - bool ServerReboot { get; set; } + public bool ServerReboot { get; set; } /// /// If the event will result in a restart of the process. If both this and are , the server is being terminated /// - bool ProcessRestart { get; set; } - + public bool ProcessRestart { get; set; } + /// /// If the action to be taken is graceful /// - bool Graceful { get; set; } + public bool Graceful { get; set; } } } \ No newline at end of file diff --git a/src/Tgstation.Server.Host/Security/CryptographySuite.cs b/src/Tgstation.Server.Host/Security/CryptographySuite.cs index a3c5553030..b0f68e7a5c 100644 --- a/src/Tgstation.Server.Host/Security/CryptographySuite.cs +++ b/src/Tgstation.Server.Host/Security/CryptographySuite.cs @@ -56,5 +56,8 @@ namespace Tgstation.Server.Host.Security } return true; } + + /// + public string GetSecureString() => Convert.ToBase64String(GetSecureBytes(30)); } } diff --git a/src/Tgstation.Server.Host/Security/ICryptographySuite.cs b/src/Tgstation.Server.Host/Security/ICryptographySuite.cs index b19f805871..137cbf54ff 100644 --- a/src/Tgstation.Server.Host/Security/ICryptographySuite.cs +++ b/src/Tgstation.Server.Host/Security/ICryptographySuite.cs @@ -21,5 +21,11 @@ namespace Tgstation.Server.Host.Security /// The password to check /// if matches the hash, otherwise bool CheckUserPassword(User user, string password); + + /// + /// Generates a 40-length secure ascii + /// + /// A 40-length secure ascii + string GetSecureString(); } } diff --git a/src/Tgstation.Server.Host/Server.cs b/src/Tgstation.Server.Host/Server.cs index 506424c49c..6d73783284 100644 --- a/src/Tgstation.Server.Host/Server.cs +++ b/src/Tgstation.Server.Host/Server.cs @@ -10,7 +10,7 @@ using Tgstation.Server.Host.Startup; namespace Tgstation.Server.Host { /// - sealed class Server : IServer, IServerUpdateConsumer + sealed class Server : IServer, IServerControl { /// public string UpdatePath { get; private set; } @@ -39,7 +39,7 @@ namespace Tgstation.Server.Host using (cancellationTokenSource) using (var webHost = webHostBuilder .UseStartup() - .ConfigureServices((serviceCollection) => serviceCollection.AddSingleton(this)) + .ConfigureServices((serviceCollection) => serviceCollection.AddSingleton(this)) .Build() ) await webHost.RunAsync(cancellationToken).ConfigureAwait(false);