diff --git a/src/Tgstation.Server.Host/Components/Watchdog/Executor.cs b/src/Tgstation.Server.Host/Components/Watchdog/Executor.cs index e31b223d2a..c422c33730 100644 --- a/src/Tgstation.Server.Host/Components/Watchdog/Executor.cs +++ b/src/Tgstation.Server.Host/Components/Watchdog/Executor.cs @@ -45,7 +45,6 @@ namespace Tgstation.Server.Host.Components.Watchdog throw new ArgumentNullException(nameof(parameters)); var proc = new Process(); - try { proc.StartInfo.FileName = byondLock.DreamDaemonPath; diff --git a/src/Tgstation.Server.Host/Components/Watchdog/ISessionControllerFactory.cs b/src/Tgstation.Server.Host/Components/Watchdog/ISessionControllerFactory.cs index 49bcbcc383..32faa37b0e 100644 --- a/src/Tgstation.Server.Host/Components/Watchdog/ISessionControllerFactory.cs +++ b/src/Tgstation.Server.Host/Components/Watchdog/ISessionControllerFactory.cs @@ -9,8 +9,24 @@ namespace Tgstation.Server.Host.Components.Watchdog /// interface ISessionControllerFactory { + /// + /// Create a from a freshly launch DreamDaemon instance + /// + /// The to use + /// The to use + /// If the of should be used + /// If the of should be used + /// If the should only validate the DMAPI then exit + /// The for the operation + /// A resulting in a new Task LaunchNew(DreamDaemonLaunchParameters launchParameters, IDmbProvider dmbProvider, bool primaryPort, bool primaryDirectory, bool apiValidate, CancellationToken cancellationToken); + /// + /// Create a from an existing DreamDaemon instance + /// + /// The to use + /// The for the operation + /// A resulting in a new Task Reattach(ReattachInformation reattachInformation, CancellationToken cancellationToken); } } diff --git a/src/Tgstation.Server.Host/Components/Watchdog/Watchdog.cs b/src/Tgstation.Server.Host/Components/Watchdog/Watchdog.cs index 856aa8a941..1f390752eb 100644 --- a/src/Tgstation.Server.Host/Components/Watchdog/Watchdog.cs +++ b/src/Tgstation.Server.Host/Components/Watchdog/Watchdog.cs @@ -1,7 +1,10 @@ using System; +using System.Globalization; using System.Threading; using System.Threading.Tasks; using Tgstation.Server.Api.Models.Internal; +using Tgstation.Server.Host.Core; +using Tgstation.Server.Host.Models; namespace Tgstation.Server.Host.Components.Watchdog { @@ -9,11 +12,28 @@ namespace Tgstation.Server.Host.Components.Watchdog sealed class Watchdog : IWatchdog { /// - public DreamDaemonLaunchParameters LaunchParameters - { - get => launchParameters; - set => launchParameters = value ?? throw new ArgumentNullException(nameof(value)); - } + public bool Running { get; private set; } + + /// + public bool AlphaIsActive { get; private set; } + + /// + public LaunchResult LastLaunchResult { get; private set; } + + /// + public Models.CompileJob LiveCompileJob { get; private set; } + + /// + public Models.CompileJob StagedCompileJob { get; private set; } + + /// + public DreamDaemonLaunchParameters ActiveLaunchParameters { get; private set; } + + /// + public DreamDaemonLaunchParameters LastLaunchParameters { get; private set; } + + /// + public RebootState? RebootState => Running ? (RebootState?)(AlphaIsActive ? alphaServer.RebootState : bravoServer.RebootState) : null; /// /// The for the @@ -23,7 +43,7 @@ namespace Tgstation.Server.Host.Components.Watchdog /// /// The for the /// - readonly ISessionControllerFactory sessionManagerFactory; + readonly ISessionControllerFactory sessionControllerFactory; /// /// The for the @@ -40,42 +60,216 @@ namespace Tgstation.Server.Host.Components.Watchdog /// readonly IDmbFactory dmbFactory; - DreamDaemonLaunchParameters launchParameters; + + /// + /// The for the + /// + readonly SemaphoreSlim semaphore; ISessionController alphaServer; ISessionController bravoServer; - bool alphaActive; - - bool disposed; + /// + /// If the servers should be released instead of shutdown + /// + bool releaseServers; /// /// Construct a /// /// The value of - /// The value of + /// The value of /// The value of /// The value of /// The value of - public Watchdog(IByond byond, IChat chat, ISessionControllerFactory sessionManagerFactory, IDmbFactory dmbFactory, IEventConsumer eventConsumer, IInteropRegistrar interopRegistrar, DreamDaemonLaunchParameters initialLaunchParameters) + /// The for the + /// The initial value of + public Watchdog(IChat chat, ISessionControllerFactory sessionControllerFactory, IDmbFactory dmbFactory, IEventConsumer eventConsumer, IInteropRegistrar interopRegistrar, IServerUpdater serverUpdater, DreamDaemonLaunchParameters initialLaunchParameters) { this.chat = chat ?? throw new ArgumentNullException(nameof(chat)); - this.sessionManagerFactory = sessionManagerFactory ?? throw new ArgumentNullException(nameof(sessionManagerFactory)); + this.sessionControllerFactory = sessionControllerFactory ?? throw new ArgumentNullException(nameof(sessionControllerFactory)); this.dmbFactory = dmbFactory ?? throw new ArgumentNullException(nameof(dmbFactory)); this.eventConsumer = eventConsumer ?? throw new ArgumentNullException(nameof(eventConsumer)); this.interopRegistrar = interopRegistrar ?? throw new ArgumentNullException(nameof(interopRegistrar)); - LaunchParameters = initialLaunchParameters; + + if(serverUpdater == null) + throw new ArgumentNullException(nameof(serverUpdater)); + + serverUpdater.RegisterForUpdate(() => releaseServers = true); + + AlphaIsActive = true; + ActiveLaunchParameters = initialLaunchParameters; + releaseServers = false; + semaphore = new SemaphoreSlim(1); } /// public void Dispose() { - lock (this) + DisposeAndNullControllers(); + semaphore.Dispose(); + } + + + void DisposeAndNullControllers() + { + alphaServer?.Dispose(); + alphaServer = null; + bravoServer?.Dispose(); + bravoServer = null; + } + + /// + public async Task ChangeSettings(DreamDaemonLaunchParameters launchParameters, CancellationToken cancellationToken) + { + using (await SemaphoreContext.Lock(semaphore, cancellationToken).ConfigureAwait(false)) { - alphaServer?.Dispose(); - bravoServer?.Dispose(); - disposed = true; + ActiveLaunchParameters = launchParameters; + if (Running) + await RestartNoLock(true, cancellationToken).ConfigureAwait(false); + + //WHEN YOU GET BACK + //change graceful restart to immediately restart the inactive server and then relaunch it + //this func is done, don't worry } } + + /// + public async Task Launch(CancellationToken cancellationToken) + { + using (await SemaphoreContext.Lock(semaphore, cancellationToken).ConfigureAwait(false)) + using(var alphaStartCts = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken)) + { + if (Running) + return null; + //start both servers + LastLaunchParameters = ActiveLaunchParameters; + var dmbToUse = await dmbFactory.LockNextDmb(cancellationToken).ConfigureAwait(false); + Task alphaServerTask = null; + + try + { + try + { + alphaServerTask = sessionControllerFactory.LaunchNew(ActiveLaunchParameters, dmbToUse, true, true, false, alphaStartCts.Token); + //do a few seconds of delay so that any backends the servers use know that alpha came first + await Task.Delay(5000, cancellationToken).ConfigureAwait(false); + var bravoServerTask = sessionControllerFactory.LaunchNew(ActiveLaunchParameters, dmbToUse, false, false, false, cancellationToken); + bravoServer = await bravoServerTask.ConfigureAwait(false); + alphaServer = await alphaServerTask.ConfigureAwait(false); + } + catch + { + if (alphaServerTask != null) + if (alphaServerTask.Status == TaskStatus.RanToCompletion) + alphaServer = await alphaServerTask.ConfigureAwait(false); + else + { + alphaStartCts.Cancel(); + try + { + alphaServer = await alphaServerTask.ConfigureAwait(false); + } + catch { } + } + throw; + } + + async Task CheckLaunch(ISessionController controller, string serverName) + { + var launch = await controller.LaunchResult.ConfigureAwait(false); + if (launch.ExitCode.HasValue) + //you killed us ray... + throw new Exception(String.Format(CultureInfo.InvariantCulture, "{2} server failed to start: Exit Code: {0}, RAM: {1}, Runtime {3}ms", launch.ExitCode, launch.StartupTime, serverName, launch.StartupTime.TotalMilliseconds)); + return launch; + } + + var alphaLrt = CheckLaunch(alphaServer, "Alpha"); + var bravoLrt = CheckLaunch(bravoServer, "Bravo"); + //now we have two booting servers, get them up and running + var allTask = Task.WhenAll(alphaLrt, bravoLrt); + + //don't forget about the cancelationToken + var cancelTcs = new TaskCompletionSource(); + using (cancellationToken.Register(() => cancelTcs.SetCanceled())) + await Task.WhenAny(allTask, cancelTcs.Task).ConfigureAwait(false); + + //both servers are now running, alpha is the active server, huzzah + LiveCompileJob = dmbToUse.CompileJob; + LastLaunchResult = alphaLrt.Result; + StagedCompileJob = null; + AlphaIsActive = true; + Running = true; + + return new WatchdogLaunchResult + { + Alpha = alphaLrt.Result, + Bravo = bravoLrt.Result + }; + } + catch + { + DisposeAndNullControllers(); + throw; + } + } + } + + async Task RestartNoLock(bool graceful, CancellationToken cancellationToken) + { + var running = Running; + if (!graceful || !running) + { + if (running) + await Terminate(false, cancellationToken).ConfigureAwait(false); + return await Launch(cancellationToken).ConfigureAwait(false); + } + var toReboot = AlphaIsActive ? alphaServer : bravoServer; + if (toReboot != null) + await toReboot.SetRebootState(Components.Watchdog.RebootState.Restart, cancellationToken).ConfigureAwait(false); + return null; + } + + /// + public async Task Restart(bool graceful, CancellationToken cancellationToken) + { + using (await SemaphoreContext.Lock(semaphore, cancellationToken).ConfigureAwait(false)) + return await RestartNoLock(graceful, cancellationToken).ConfigureAwait(false); + } + + /// + public async Task Terminate(bool graceful, CancellationToken cancellationToken) + { + using (await SemaphoreContext.Lock(semaphore, cancellationToken).ConfigureAwait(false)) + { + if (!Running) + return; + if (!graceful) + { + DisposeAndNullControllers(); + return; + } + var toKill = AlphaIsActive ? alphaServer : bravoServer; + if (toKill != null) + await toKill.SetRebootState(Components.Watchdog.RebootState.Shutdown, cancellationToken).ConfigureAwait(false); + } + } + + /// + public Task StartAsync(CancellationToken cancellationToken) => Launch(cancellationToken); + + /// + public Task StopAsync(CancellationToken cancellationToken) + { + if (releaseServers) + { + ReattachInformation reattachInformation; + if (AlphaIsActive) + reattachInformation = alphaServer?.Release(); + else + reattachInformation = bravoServer?.Release(); + } + return Terminate(false, cancellationToken); + } } } diff --git a/src/Tgstation.Server.Host/Components/Watchdog/WatchdogFactory.cs b/src/Tgstation.Server.Host/Components/Watchdog/WatchdogFactory.cs index e11ce4cd29..dc93621c80 100644 --- a/src/Tgstation.Server.Host/Components/Watchdog/WatchdogFactory.cs +++ b/src/Tgstation.Server.Host/Components/Watchdog/WatchdogFactory.cs @@ -1,5 +1,6 @@ using System; using Tgstation.Server.Api.Models.Internal; +using Tgstation.Server.Host.Core; namespace Tgstation.Server.Host.Components.Watchdog { @@ -31,6 +32,11 @@ namespace Tgstation.Server.Host.Components.Watchdog /// readonly IInteropRegistrar interopRegistrar; + /// + /// The for the + /// + readonly IServerUpdater serverUpdater; + /// /// Construct a /// @@ -39,16 +45,17 @@ namespace Tgstation.Server.Host.Components.Watchdog /// The value of /// The value of /// The value of - public WatchdogFactory(IByond byond, IChat chat, ISessionControllerFactory sessionManagerFactory, IEventConsumer eventConsumer, IInteropRegistrar interopRegistrar) + /// The value of + public WatchdogFactory(IChat chat, ISessionControllerFactory sessionManagerFactory, IEventConsumer eventConsumer, IInteropRegistrar interopRegistrar, IServerUpdater serverUpdater) { - this.byond = byond ?? throw new ArgumentNullException(nameof(byond)); this.chat = chat ?? throw new ArgumentNullException(nameof(chat)); this.sessionManagerFactory = sessionManagerFactory ?? throw new ArgumentNullException(nameof(sessionManagerFactory)); this.eventConsumer = eventConsumer ?? throw new ArgumentNullException(nameof(eventConsumer)); this.interopRegistrar = interopRegistrar ?? throw new ArgumentNullException(nameof(interopRegistrar)); + this.serverUpdater = serverUpdater ?? throw new ArgumentNullException(nameof(serverUpdater)); } /// - public IWatchdog CreateWatchdog(IDmbFactory dmbFactory, DreamDaemonLaunchParameters launchParameters) => new Watchdog(byond, chat, sessionManagerFactory, dmbFactory, eventConsumer, interopRegistrar, launchParameters); + public IWatchdog CreateWatchdog(IDmbFactory dmbFactory, DreamDaemonLaunchParameters launchParameters) => new Watchdog(chat, sessionManagerFactory, dmbFactory, eventConsumer, interopRegistrar, serverUpdater, launchParameters); } } diff --git a/src/Tgstation.Server.Host/Core/IServerUpdateConsumer.cs b/src/Tgstation.Server.Host/Core/IServerUpdater.cs similarity index 60% rename from src/Tgstation.Server.Host/Core/IServerUpdateConsumer.cs rename to src/Tgstation.Server.Host/Core/IServerUpdater.cs index 9055b7206e..c4b43eb1b2 100644 --- a/src/Tgstation.Server.Host/Core/IServerUpdateConsumer.cs +++ b/src/Tgstation.Server.Host/Core/IServerUpdater.cs @@ -1,14 +1,22 @@ -namespace Tgstation.Server.Host.Core +using System; + +namespace Tgstation.Server.Host.Core { /// /// Represents a service that may take an updated assembly and run it, stopping the current assembly in the process /// - interface IServerUpdateConsumer + interface IServerUpdater { /// /// Run a new assembly and stop the current one. This will likely trigger all active s /// /// The path to the new assembly void ApplyUpdate(string updatePath); + + /// + /// Register a given to run before stopping the server for updates + /// + /// The to run + void RegisterForUpdate(Action action); } } diff --git a/src/Tgstation.Server.Host/Core/SemaphoreContext.cs b/src/Tgstation.Server.Host/Core/SemaphoreContext.cs new file mode 100644 index 0000000000..f2665250d8 --- /dev/null +++ b/src/Tgstation.Server.Host/Core/SemaphoreContext.cs @@ -0,0 +1,60 @@ +using System; +using System.Threading; +using System.Threading.Tasks; + +namespace Tgstation.Server.Host.Core +{ + /// + /// Async lock context helper + /// + public sealed class SemaphoreContext : IDisposable + { + /// + /// Asyncronously locks a + /// + /// The to lock + /// The for the operation + /// A resulting in the for the lock + public static async Task Lock(SemaphoreSlim semaphore, CancellationToken cancellationToken) + { + if (semaphore == null) + throw new ArgumentNullException(nameof(semaphore)); + await semaphore.WaitAsync(cancellationToken).ConfigureAwait(false); + cancellationToken.ThrowIfCancellationRequested(); + return new SemaphoreContext(semaphore); + } + + /// + /// The locked + /// + readonly SemaphoreSlim lockedSemaphore; + + /// + /// If has been called + /// + bool disposed; + + /// + /// Construct a + /// + /// The value of + SemaphoreContext(SemaphoreSlim lockedSemaphore) => this.lockedSemaphore = lockedSemaphore; + + /// + /// Finalize the + /// + ~SemaphoreContext() => Dispose(); + + /// + /// Release the lock on + /// + public void Dispose() + { + if (disposed) + return; + lockedSemaphore.Release(); + GC.SuppressFinalize(this); + disposed = true; + } + } +} diff --git a/src/Tgstation.Server.Host/Server.cs b/src/Tgstation.Server.Host/Server.cs index 506424c49c..ddf5ec2ece 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, IServerUpdater { /// public string UpdatePath { get; private set; } @@ -35,11 +35,10 @@ namespace Tgstation.Server.Host [ExcludeFromCodeCoverage] public async Task RunAsync(CancellationToken cancellationToken) { - cancellationTokenSource = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken); - using (cancellationTokenSource) + using (cancellationTokenSource = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken)) using (var webHost = webHostBuilder .UseStartup() - .ConfigureServices((serviceCollection) => serviceCollection.AddSingleton(this)) + .ConfigureServices((serviceCollection) => serviceCollection.AddSingleton(this)) .Build() ) await webHost.RunAsync(cancellationToken).ConfigureAwait(false); @@ -56,5 +55,8 @@ namespace Tgstation.Server.Host } cancellationTokenSource.Cancel(); } + + /// + public void RegisterForUpdate(Action action) => cancellationTokenSource.Token.Register(action); } } diff --git a/tests/Tgstation.Server.Host.Tests/Core/TestApplication.cs b/tests/Tgstation.Server.Host.Tests/Core/TestApplication.cs index 47ac493ecc..afd32b4923 100644 --- a/tests/Tgstation.Server.Host.Tests/Core/TestApplication.cs +++ b/tests/Tgstation.Server.Host.Tests/Core/TestApplication.cs @@ -7,7 +7,7 @@ using System.Threading.Tasks; namespace Tgstation.Server.Host.Core.Tests { [TestClass] - public sealed class TestApplication : IServerUpdateConsumer + public sealed class TestApplication : IServerUpdater { public void ApplyUpdate(string updatePath) => throw new System.NotImplementedException();