diff --git a/src/Tgstation.Server.Host/Components/Instance.cs b/src/Tgstation.Server.Host/Components/Instance.cs
index 899310152a..5353b9d6f8 100644
--- a/src/Tgstation.Server.Host/Components/Instance.cs
+++ b/src/Tgstation.Server.Host/Components/Instance.cs
@@ -372,7 +372,7 @@ namespace Tgstation.Server.Host.Components
var startSha = repo.Head;
if (!repo.Tracking)
{
- logger.LogTrace("Aborting repo update, not tracking origin!");
+ logger.LogTrace("Aborting repo update, active ref not tracking any remote branch!");
deploySha = startSha;
return;
}
diff --git a/src/Tgstation.Server.Host/Components/Watchdog/BasicWatchdog.cs b/src/Tgstation.Server.Host/Components/Watchdog/BasicWatchdog.cs
new file mode 100644
index 0000000000..2f9f9f5b8f
--- /dev/null
+++ b/src/Tgstation.Server.Host/Components/Watchdog/BasicWatchdog.cs
@@ -0,0 +1,359 @@
+using Byond.TopicSender;
+using Microsoft.Extensions.Logging;
+using System;
+using System.Globalization;
+using System.Threading;
+using System.Threading.Tasks;
+using Tgstation.Server.Api.Models.Internal;
+using Tgstation.Server.Host.Components.Chat;
+using Tgstation.Server.Host.Components.Compiler;
+using Tgstation.Server.Host.Core;
+
+namespace Tgstation.Server.Host.Components.Watchdog
+{
+ ///
+ /// A that manages one server.
+ ///
+ sealed class BasicWatchdog : WatchdogBase
+ {
+ ///
+ public override bool AlphaIsActive => true;
+
+ ///
+ public override Models.CompileJob ActiveCompileJob => server?.Dmb.CompileJob;
+
+ ///
+ public override RebootState? RebootState => server?.RebootState;
+
+ ///
+ /// The single .
+ ///
+ ISessionController server;
+
+ ///
+ /// Initializes a new instance of the .
+ ///
+ /// The for the .
+ /// The for the .
+ /// The for the .
+ /// The for the .
+ /// The for the .
+ /// The for the .
+ /// The for the .
+ /// The for the .
+ /// The for the .
+ /// The for the .
+ /// The for the .
+ /// The for the .
+ /// The for the .
+ /// The autostart value for the .
+ public BasicWatchdog(
+ IChat chat,
+ ISessionControllerFactory sessionControllerFactory,
+ IDmbFactory dmbFactory,
+ IReattachInfoHandler reattachInfoHandler,
+ IDatabaseContextFactory databaseContextFactory,
+ IByondTopicSender byondTopicSender,
+ IEventConsumer eventConsumer,
+ IJobManager jobManager,
+ IServerControl serverControl,
+ IAsyncDelayer asyncDelayer,
+ ILogger logger,
+ DreamDaemonLaunchParameters initialLaunchParameters,
+ Api.Models.Instance instance,
+ bool autoStart)
+ : base(
+ chat,
+ sessionControllerFactory,
+ dmbFactory,
+ reattachInfoHandler,
+ databaseContextFactory,
+ byondTopicSender,
+ eventConsumer,
+ jobManager,
+ serverControl,
+ asyncDelayer,
+ logger,
+ initialLaunchParameters,
+ instance,
+ autoStart)
+ { }
+
+ async Task HandleMonitorWakeup(MonitorActivationReason reason, CancellationToken cancellationToken)
+ {
+ switch (reason)
+ {
+ case MonitorActivationReason.ActiveServerCrashed:
+ string exitWord = server.TerminationWasRequested ? "exited" : "crashed";
+ if (server.RebootState == Watchdog.RebootState.Shutdown)
+ {
+ // the time for graceful shutdown is now
+ await Chat.SendWatchdogMessage(String.Format(CultureInfo.InvariantCulture, "Server {0}! Exiting due to graceful termination request...", exitWord), cancellationToken).ConfigureAwait(false);
+ DisposeAndNullControllers();
+ return MonitorAction.Exit;
+ }
+
+ await Chat.SendWatchdogMessage(String.Format(CultureInfo.InvariantCulture, "Server {0}! Rebooting...", exitWord), cancellationToken).ConfigureAwait(false);
+ return MonitorAction.Restart;
+ case MonitorActivationReason.ActiveServerRebooted:
+ var rebootState = server.RebootState;
+ server.ResetRebootState();
+
+ switch (rebootState)
+ {
+ case Watchdog.RebootState.Normal:
+ bool dmbUpdatePending = ActiveLaunchParameters != LastLaunchParameters;
+ return dmbUpdatePending ? MonitorAction.Restart : MonitorAction.Continue;
+ case Watchdog.RebootState.Restart:
+ return MonitorAction.Restart;
+ case Watchdog.RebootState.Shutdown:
+ // graceful shutdown time
+ await Chat.SendWatchdogMessage("Active server rebooted! Exiting due to graceful termination request...", cancellationToken).ConfigureAwait(false);
+ DisposeAndNullControllers();
+ return MonitorAction.Exit;
+ default:
+ throw new InvalidOperationException($"Invalid reboot state: {rebootState}");
+ }
+
+ case MonitorActivationReason.ActiveLaunchParametersUpdated:
+ case MonitorActivationReason.NewDmbAvailable:
+ await server.SetRebootState(Watchdog.RebootState.Restart, cancellationToken).ConfigureAwait(false);
+ return MonitorAction.Continue;
+ case MonitorActivationReason.InactiveServerCrashed:
+ case MonitorActivationReason.InactiveServerRebooted:
+ case MonitorActivationReason.InactiveServerStartupComplete:
+ throw new NotSupportedException($"Unsupported activation reason: {reason}");
+ default:
+ throw new InvalidOperationException($"Invalid activation reason: {reason}");
+ }
+ }
+
+ ///
+ protected override WatchdogReattachInformation CreateReattachInformation()
+ => new WatchdogReattachInformation
+ {
+ AlphaIsActive = true,
+ Alpha = server?.Release()
+ };
+
+ ///
+ protected override void DisposeAndNullControllers()
+ {
+ server?.Dispose();
+ server = null;
+ Running = false;
+ }
+
+ ///
+ protected override ISessionController GetActiveController() => server;
+
+ ///
+ protected override async Task InitControllers(Action callBeforeRecurse, Task chatTask, WatchdogReattachInformation reattachInfo, CancellationToken cancellationToken)
+ {
+ // good ole sanity, should never fucking trigger but i don't trust myself even though I should
+ // TODO: Unit test this instead?
+ if (server != null)
+ throw new InvalidOperationException("Entered LaunchNoLock with server not being null!");
+
+ // don't need a new dmb if reattaching
+ var doesntNeedNewDmb = reattachInfo?.Alpha != null && reattachInfo?.Bravo != null;
+ var dmbToUse = doesntNeedNewDmb ? null : DmbFactory.LockNextDmb(1);
+
+ var serverToReattach = reattachInfo?.Alpha ?? reattachInfo?.Bravo;
+ var serverToKill = reattachInfo?.Bravo ?? reattachInfo?.Alpha;
+
+ // vice versa
+ if (reattachInfo?.AlphaIsActive == false)
+ {
+ var temp = serverToReattach;
+ serverToReattach = serverToKill;
+ serverToKill = temp;
+ }
+
+ // if this try catches something, both servers are killed
+ bool inactiveServerWasKilled = false;
+ try
+ {
+ // start the alpha server task, either by launch a new process or attaching to an existing one
+ // The tasks returned are mainly for writing interop files to the directories among other things and should generally never fail
+ // The tasks pertaining to server startup times are in the ISessionControllers
+ Task serverLaunchTask, inactiveReattachTask;
+ if (!doesntNeedNewDmb)
+ serverLaunchTask = SessionControllerFactory.LaunchNew(ActiveLaunchParameters, dmbToUse, null, true, true, false, cancellationToken);
+ else
+ serverLaunchTask = SessionControllerFactory.Reattach(serverToReattach, cancellationToken);
+
+ bool thereIsAnInactiveServerToKill = serverToKill != null;
+ if (thereIsAnInactiveServerToKill)
+ inactiveReattachTask = SessionControllerFactory.Reattach(serverToKill, cancellationToken);
+ else
+ inactiveReattachTask = Task.FromResult(null);
+
+ // retrieve the session controller
+ server = await serverLaunchTask.ConfigureAwait(false);
+
+ // failed reattaches will return null
+ server?.SetHighPriority();
+
+ var inactiveServerController = await inactiveReattachTask.ConfigureAwait(false);
+ inactiveServerController?.Dispose();
+ inactiveServerWasKilled = inactiveServerController != null;
+
+ // possiblity of null servers due to failed reattaches
+ if (server == null)
+ {
+ callBeforeRecurse();
+ await NotifyOfFailedReattach(thereIsAnInactiveServerToKill && !inactiveServerWasKilled, cancellationToken).ConfigureAwait(false);
+ return;
+ }
+
+ await CheckLaunchResult(server, "Server", cancellationToken).ConfigureAwait(false);
+
+ server.EnableCustomChatCommands();
+ }
+ catch
+ {
+ // kill the controllers
+ bool serverWasActive = server != null;
+ DisposeAndNullControllers();
+
+ // server didn't get control of this dmb
+ if (dmbToUse != null && !serverWasActive)
+ dmbToUse.Dispose();
+
+ if (serverToKill != null && !inactiveServerWasKilled)
+ serverToKill.Dmb.Dispose();
+ throw;
+ }
+ }
+
+ ///
+ protected override async Task MonitorLifetimes(CancellationToken cancellationToken)
+ {
+ Logger.LogTrace("Entered MonitorLifetimes");
+
+ // this function is responsible for calling HandlerMonitorWakeup when necessary and manitaining the MonitorState
+ var iteration = 1;
+ for (MonitorAction nextAction = MonitorAction.Continue; nextAction != MonitorAction.Exit; ++iteration)
+ {
+ // always start out with continue
+ nextAction = MonitorAction.Continue;
+
+ // dump some info to the logs
+ Logger.LogDebug("Iteration {0} of monitor loop", iteration);
+ try
+ {
+ Logger.LogDebug("Server Compile Job ID: {0}", server.Dmb.CompileJob.Id);
+
+ // load the activation tasks into local variables
+ Task activeServerLifetime = server.Lifetime;
+ var activeServerReboot = server.OnReboot;
+ Task activeLaunchParametersChanged = ActiveParametersUpdated.Task;
+ var newDmbAvailable = DmbFactory.OnNewerDmb;
+
+ // cancel waiting if requested
+ var cancelTcs = new TaskCompletionSource