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
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();