1 using Microsoft.EntityFrameworkCore;
2 using Microsoft.Extensions.Logging;
5 using System.Collections.Generic;
8 using System.Threading.Tasks;
28 #pragma warning disable CA1506 // TODO: Decomplexify 38 Logger.LogTrace(
"Status set to {0}", status);
43 public abstract bool AlphaIsActive {
get; }
46 public abstract Models.CompileJob ActiveCompileJob {
get; }
60 protected TaskCompletionSource<object> ActiveParametersUpdated {
get;
set; }
65 protected SemaphoreSlim Semaphore {
get; }
70 protected ILogger Logger {
get; }
202 Api.Models.Instance instance,
205 Chat = chat ??
throw new ArgumentNullException(nameof(chat));
206 SessionControllerFactory = sessionControllerFactory ??
throw new ArgumentNullException(nameof(sessionControllerFactory));
207 DmbFactory = dmbFactory ??
throw new ArgumentNullException(nameof(dmbFactory));
208 this.sessionPersistor = sessionPersistor ??
throw new ArgumentNullException(nameof(sessionPersistor));
209 this.databaseContextFactory = databaseContextFactory ??
throw new ArgumentNullException(nameof(databaseContextFactory));
210 this.jobManager = jobManager ??
throw new ArgumentNullException(nameof(jobManager));
211 AsyncDelayer = asyncDelayer ??
throw new ArgumentNullException(nameof(asyncDelayer));
212 this.diagnosticsIOManager = diagnosticsIOManager ??
throw new ArgumentNullException(nameof(diagnosticsIOManager));
213 this.eventConsumer = eventConsumer ??
throw new ArgumentNullException(nameof(eventConsumer));
214 Logger = logger ??
throw new ArgumentNullException(nameof(logger));
215 ActiveLaunchParameters = initialLaunchParameters ??
throw new ArgumentNullException(nameof(initialLaunchParameters));
216 this.instance = instance ??
throw new ArgumentNullException(nameof(instance));
217 this.autoStart = autoStart;
219 if (serverControl == null)
220 throw new ArgumentNullException(nameof(serverControl));
224 ActiveLaunchParameters = initialLaunchParameters;
225 releaseServers =
false;
226 ActiveParametersUpdated =
new TaskCompletionSource<object>();
227 controllerDisposeLock =
new object();
232 Semaphore =
new SemaphoreSlim(1);
236 restartRegistration.Dispose();
240 Logger.LogTrace(
"Created watchdog");
246 Logger.LogTrace(
"Disposing...");
248 restartRegistration.Dispose();
249 DisposeAndNullControllers();
250 monitorCts?.Dispose();
261 async Task
TerminateNoLock(
bool graceful,
bool announce, CancellationToken cancellationToken)
267 var eventTask = eventConsumer.HandleEvent(releaseServers ?
EventType.WatchdogDetach :
EventType.WatchdogShutdown, null, cancellationToken);
269 var chatTask = announce ? Chat.SendWatchdogMessage(
"Shutting down...",
false, cancellationToken) : Task.CompletedTask;
271 await eventTask.ConfigureAwait(
false);
273 await StopMonitor().ConfigureAwait(
false);
275 DisposeAndNullControllers();
277 LastLaunchParameters = null;
279 await chatTask.ConfigureAwait(
false);
284 var toKill = GetActiveController();
287 await toKill.SetRebootState(Session.RebootState.Shutdown, cancellationToken).ConfigureAwait(
false);
288 Logger.LogTrace(
"Graceful termination requested");
291 Logger.LogTrace(
"Could not gracefully terminate as there is no active controller!");
301 Logger.LogTrace(
"Sending heartbeat to active server...");
302 var activeServer = GetActiveController();
303 var response = await activeServer.SendCommand(
new TopicParameters(), cancellationToken).ConfigureAwait(
false);
305 var shouldShutdown = activeServer.RebootState == Session.RebootState.Shutdown;
306 if (response == null)
308 switch (++heartbeatsMissed)
311 Logger.LogDebug(
"DEFCON 4: DreamDaemon missed first heartbeat!");
314 var message2 =
"DEFCON 3: DreamDaemon has missed 2 heartbeats!";
315 Logger.LogInformation(message2);
316 await Chat.SendWatchdogMessage(message2,
true, cancellationToken).ConfigureAwait(
false);
319 var actionToTake = shouldShutdown
322 var message3 = $
"DEFCON 2: DreamDaemon has missed 3 heartbeats! If it does not respond to the next one, the watchdog will {actionToTake}!";
323 Logger.LogWarning(message3);
324 await Chat.SendWatchdogMessage(message3,
false, cancellationToken).ConfigureAwait(
false);
327 var actionTaken = shouldShutdown
328 ?
"Shutting down due to graceful termination request" 330 var message4 = $
"DEFCON 1: Four heartbeats have been missed! {actionTaken}...";
331 Logger.LogWarning(message4);
332 await Chat.SendWatchdogMessage(message4,
false, cancellationToken).ConfigureAwait(
false);
333 DisposeAndNullControllers();
336 Logger.LogError(
"Invalid heartbeats missed count: {0}", heartbeatsMissed);
341 heartbeatsMissed = 0;
358 bool announceFailure,
360 CancellationToken cancellationToken)
362 Logger.LogTrace(
"Begin LaunchImplNoLock");
371 announceTask = Chat.SendWatchdogMessage(reattachInfo == null ?
"Launching..." :
"Reattaching...",
false, cancellationToken);
372 if (reattachInfo == null)
373 announceTask = Task.WhenAll(
374 eventConsumer.HandleEvent(
EventType.WatchdogLaunch, Enumerable.Empty<
string>(), cancellationToken),
378 announceTask = Task.CompletedTask;
381 LastLaunchParameters = ActiveLaunchParameters;
382 heartbeatsMissed = 0;
386 await InitControllers(announceTask, reattachInfo, cancellationToken).ConfigureAwait(
false);
388 catch (OperationCanceledException)
390 Logger.LogTrace(
"Controller initialization canceled!");
396 var originalChatTask = announceTask;
397 async Task ChainChatTaskWithErrorMessage()
399 await originalChatTask.ConfigureAwait(
false);
401 await Chat.SendWatchdogMessage(
"Startup failed!",
false, cancellationToken).ConfigureAwait(
false);
404 announceTask = ChainChatTaskWithErrorMessage();
405 Logger.LogWarning(
"Failed to start watchdog: {0}", e.ToString());
413 await announceTask.ConfigureAwait(
false);
415 catch (OperationCanceledException)
417 Logger.LogTrace(
"Announcement task canceled!");
421 Logger.LogInformation(
"Controller(s) initialized successfully");
425 monitorCts =
new CancellationTokenSource();
426 monitorTask = MonitorLifetimes(monitorCts.Token);
436 Logger.LogTrace(
"StopMonitor");
437 if (monitorTask == null)
439 var wasRunning = !monitorTask.IsCompleted;
441 await monitorTask.ConfigureAwait(
false);
442 Logger.LogTrace(
"Stopped Monitor");
443 monitorCts.Dispose();
458 var launchResult = await controller.
LaunchResult.WithToken(cancellationToken).ConfigureAwait(
false);
461 if (launchResult.ExitCode.HasValue)
464 new JobException($
"{serverName} failed to start: {launchResult}"));
465 if (!launchResult.StartupTime.HasValue)
468 new JobException($
"{serverName} timed out on startup: {ActiveLaunchParameters.StartupTimeout.Value}s"));
477 protected async Task
ReattachFailure(Task chatTask, CancellationToken cancellationToken)
480 DisposeAndNullControllers();
481 const string FailReattachMessage =
"Unable to properly reattach to server! Restarting watchdog...";
482 Logger.LogWarning(FailReattachMessage);
484 async Task ChainChatTask()
486 await chatTask.ConfigureAwait(
false);
487 await Chat.SendWatchdogMessage(FailReattachMessage,
false, cancellationToken).ConfigureAwait(
false);
490 await InitControllers(ChainChatTask(), null, cancellationToken).ConfigureAwait(
false);
496 protected abstract void DisposeAndNullControllersImpl();
503 Logger.LogTrace(
"DisposeAndNullControllers");
504 lock (controllerDisposeLock)
505 DisposeAndNullControllersImpl();
520 protected abstract Task<MonitorAction> HandleMonitorWakeup(
522 CancellationToken cancellationToken);
531 Logger.LogTrace(
"Monitor restart!");
532 DisposeAndNullControllers();
534 var chatTask = Task.CompletedTask;
535 for (var retryAttempts = 1; ; ++retryAttempts)
538 Exception launchException;
543 await LaunchNoLock(
false,
false,
false, null, cancellationToken).ConfigureAwait(
false);
545 Logger.LogDebug(
"Relaunch successful, resuming monitor...");
548 catch (OperationCanceledException)
558 await chatTask.ConfigureAwait(
false);
561 Logger.LogWarning(
"Failed to automatically restart the watchdog! Attempt: {0}, Exception: {1}", retryAttempts, launchException);
564 var retryDelay = Math.Min(
566 Math.Pow(2, retryAttempts)),
567 TimeSpan.FromHours(1).TotalSeconds);
569 chatTask = Chat.SendWatchdogMessage(
570 $
"Failed to restart (Attempt: {retryAttempts}), retrying in {retryDelay}s...",
576 TimeSpan.FromSeconds(retryDelay),
579 .ConfigureAwait(
false);
590 Logger.LogTrace(
"Entered MonitorLifetimes");
592 using var _ = cancellationToken.Register(() => Logger.LogTrace(
"Monitor cancellationToken triggered"));
598 for (ulong iteration = 1; nextAction !=
MonitorAction.Exit; ++iteration)
599 using (LogContext.PushProperty(
"Monitor", iteration))
602 Logger.LogTrace(
"Iteration {0} of monitor loop", iteration);
605 var controller = GetActiveController();
606 Task activeServerLifetime = controller.Lifetime;
607 var activeServerReboot = controller.OnReboot;
609 Task activeLaunchParametersChanged = ActiveParametersUpdated.Task;
612 var heartbeatSeconds = ActiveLaunchParameters.HeartbeatSeconds.Value;
613 var heartbeat = heartbeatSeconds == 0
614 || !controller.DMApiAvailable
615 ? Extensions.TaskExtensions.InfiniteTask()
616 : Task.Delay(TimeSpan.FromSeconds(heartbeatSeconds));
619 var cancelTcs =
new TaskCompletionSource<object>();
620 var toWaitOn = Task.WhenAny(
621 activeServerLifetime,
626 activeLaunchParametersChanged);
629 using (cancellationToken.Register(() => cancelTcs.SetCanceled()))
630 await toWaitOn.ConfigureAwait(
false);
632 cancellationToken.ThrowIfCancellationRequested();
633 Logger.LogTrace(
"Monitor activated");
639 if (activeServerLifetime.IsCompleted)
643 for (var moreActivationsToProcess =
true; moreActivationsToProcess && (nextAction ==
MonitorAction.Continue || nextAction ==
MonitorAction.Skip);)
649 var taskCompleted = task?.IsCompleted ==
true;
653 else if (taskCompleted)
655 activationReason = testActivationReason;
663 var anyActivation = CheckActivationReason(ref activeServerLifetime,
MonitorActivationReason.ActiveServerCrashed)
666 || CheckActivationReason(ref activeLaunchParametersChanged,
MonitorActivationReason.ActiveLaunchParametersUpdated)
670 moreActivationsToProcess =
false;
673 Logger.LogTrace(
"Reason: {0}", activationReason);
675 nextAction = await HandleHeartbeat(
677 .ConfigureAwait(
false);
679 nextAction = await HandleMonitorWakeup(
682 .ConfigureAwait(
false);
687 Logger.LogTrace(
"Next monitor action is to {0}", nextAction);
692 await MonitorRestart(cancellationToken).ConfigureAwait(
false);
696 catch (OperationCanceledException)
705 "Monitor crashed! Iteration: {0}, Exception: {1}",
712 var chatTask = Chat.SendWatchdogMessage(
713 $
"Monitor crashed, this should NEVER happen! Please report this, full details in logs! {nextActionMessage}. Error: {e.Message}",
721 if (GetActiveController()?.Lifetime.IsCompleted !=
true)
722 await MonitorRestart(cancellationToken).ConfigureAwait(
false);
724 Logger.LogDebug(
"Server seems to be okay, not restarting");
728 await chatTask.ConfigureAwait(
false);
731 catch (OperationCanceledException)
734 Logger.LogDebug(
"Monitor cancelled");
738 Logger.LogTrace(
"Detaching servers...");
739 releasedReattachInformation = GetActiveController().
Release();
743 DisposeAndNullControllers();
746 Logger.LogTrace(
"Monitor exiting...");
756 protected abstract Task InitControllers(Task chatTask,
ReattachInformation reattachInfo, CancellationToken cancellationToken);
764 ActiveLaunchParameters = launchParameters;
768 ActiveParametersUpdated.TrySetResult(null);
769 ActiveParametersUpdated =
new TaskCompletionSource<object>();
777 var activeServer = GetActiveController();
780 if (activeServer == null)
784 var result = await activeServer.SendCommand(
787 .ConfigureAwait(
false);
789 if (result?.InteropResponse?.ChatResponses != null)
791 result.InteropResponse.ChatResponses.Select(
792 x => Chat.SendMessage(
795 .Select(channelIdString =>
797 if (UInt64.TryParse(channelIdString, out var channelId))
798 return (ulong?)channelId;
802 .Where(nullableChannelId => nullableChannelId.HasValue)
803 .Select(nullableChannelId => nullableChannelId.Value),
805 .ConfigureAwait(
false);
814 return "TGS: Server offline!";
816 var commandObject =
new ChatCommand(sender, commandName, arguments);
820 var activeServer = GetActiveController();
821 var commandResult = await activeServer.SendCommand(command, cancellationToken).ConfigureAwait(
false);
823 if (commandResult == null)
824 return "TGS: Bad topic exchange!";
826 if (commandResult.InteropResponse == null)
827 return "TGS: Bad topic response!";
829 return commandResult.InteropResponse.CommandResponseMessage ??
830 "TGS: Command processed but no DMAPI response returned!";
835 public async Task
Launch(CancellationToken cancellationToken)
840 await LaunchNoLock(
true,
true,
true, null, cancellationToken).ConfigureAwait(
false);
850 var toClear = GetActiveController();
852 await toClear.SetRebootState(Session.RebootState.Normal, cancellationToken).ConfigureAwait(
false);
857 public async Task
Restart(
bool graceful, CancellationToken cancellationToken)
862 Logger.LogTrace(
"Begin Restart. Graceful: {0}", graceful);
867 var chatTask = Chat.SendWatchdogMessage(
"Manual restart triggered...",
false, cancellationToken);
868 await TerminateNoLock(
false,
false, cancellationToken).ConfigureAwait(
false);
869 await LaunchNoLock(
true,
false,
true, null, cancellationToken).ConfigureAwait(
false);
870 await chatTask.ConfigureAwait(
false);
874 var toReboot = GetActiveController();
876 && !await toReboot.SetRebootState(Session.RebootState.Restart, cancellationToken).ConfigureAwait(
false))
877 Logger.LogWarning(
"Unable to send reboot state change event!");
882 public async Task
StartAsync(CancellationToken cancellationToken)
884 var reattachInfo = await sessionPersistor.Load(cancellationToken).ConfigureAwait(
false);
885 if (!autoStart && reattachInfo == null)
888 long? adminUserId = null;
890 await databaseContextFactory.UseContext(
891 async db => adminUserId = await db
894 .Where(x => x.CanonicalName == Models.User.CanonicalizeName(Api.Models.User.AdminName))
896 .FirstAsync(cancellationToken)
897 .ConfigureAwait(
false))
898 .ConfigureAwait(
false);
899 var job =
new Models.Job
901 StartedBy =
new Models.User
903 Id = adminUserId.Value
909 Description = $
"Instance startup watchdog {(reattachInfo != null ? "reattach
" : "launch
")}",
913 await jobManager.RegisterOperation(job, async (j, databaseContextFactory, progressFunction, ct) =>
916 await LaunchNoLock(
true,
true,
true, reattachInfo, ct).ConfigureAwait(
false);
917 }, cancellationToken).ConfigureAwait(
false);
921 public async Task
StopAsync(CancellationToken cancellationToken)
923 await TerminateNoLock(
false, !releaseServers, cancellationToken).ConfigureAwait(
false);
924 if (releasedReattachInformation != null)
928 await sessionPersistor.Save(releasedReattachInformation, cancellationToken).ConfigureAwait(
false);
933 "Failed to persist session reattach information! To repair this, DreamDaemon will need to be manully stopped and then relaunched with TGS. Exception: {0}",
937 releasedReattachInformation = null;
938 releaseServers =
false;
943 public async Task
Terminate(
bool graceful, CancellationToken cancellationToken)
946 await TerminateNoLock(graceful, !releaseServers, cancellationToken).ConfigureAwait(
false);
950 public async Task
HandleRestart(Version updateVersion, CancellationToken cancellationToken)
952 releaseServers =
true;
954 await Chat.SendWatchdogMessage(
"Detaching...",
false, cancellationToken).ConfigureAwait(
false);
956 Logger.LogTrace(
"Not sending detach chat message as status is: {0}", Status);
960 public abstract Task
InstanceRenamed(
string newInstanceName, CancellationToken cancellationToken);
963 public async Task
CreateDump(CancellationToken cancellationToken)
965 const string DumpDirectory =
"ProcessDumps";
966 await diagnosticsIOManager.CreateDirectory(DumpDirectory, cancellationToken).ConfigureAwait(
false);
968 var dumpFileName = diagnosticsIOManager.ResolvePath(
969 diagnosticsIOManager.ConcatPath(
971 $
"DreamDaemon-{DateTimeOffset.Now.ToFileStamp()}.dmp"));
973 var session = GetActiveController();
974 if (session?.Lifetime.IsCompleted !=
false)
977 Logger.LogInformation(
"Dumping session to {0}...", dumpFileName);
978 await session.CreateDump(dumpFileName, cancellationToken).ConfigureAwait(
false);
async Task LaunchNoLock(bool startMonitor, bool announce, bool announceFailure, ReattachInformation reattachInfo, CancellationToken cancellationToken)
Launches the watchdog.
readonly bool autoStart
If the WatchdogBase should LaunchNoLock(bool, bool, bool, ReattachInformation, CancellationToken) in ...
Handles saving and loading ReattachInformation.
async Task Launch(CancellationToken cancellationToken)
Start the IWatchdog
Base class for IWatchdogs.
ErrorCode
Types of ErrorMessages that the API may return.
RightsType
The type of rights a model uses
Use server authentication
Parameters for a topic request.
WatchdogBase(IChatManager chat, ISessionControllerFactory sessionControllerFactory, IDmbFactory dmbFactory, ISessionPersistor sessionPersistor, IDatabaseContextFactory databaseContextFactory, IJobManager jobManager, IServerControl serverControl, IAsyncDelayer asyncDelayer, IIOManager diagnosticsIOManager, IEventConsumer eventConsumer, ILogger logger, DreamDaemonLaunchParameters initialLaunchParameters, Api.Models.Instance instance, bool autoStart)
Initializes a new instance of the WatchdogBase .
Factory for scoping usage of IDatabaseContexts. Meant for use by Components
async Task MonitorLifetimes(CancellationToken cancellationToken)
The main loop of the watchdog. Ayschronously waits for events to occur and then responds to them...
Represents a chat command to be handled by DD
Launch settings for DreamDaemon
Instance(Api.Models.Instance metadata, IRepositoryManager repositoryManager, IByondManager byondManager, IDreamMaker dreamMaker, IWatchdog watchdog, IChatManager chat, StaticFiles.IConfiguration configuration, IDatabaseContextFactory databaseContextFactory, IDmbFactory dmbFactory, IJobManager jobManager, IEventConsumer eventConsumer, ILogger< Instance > logger)
Construct an Instance
readonly IIOManager diagnosticsIOManager
The IIOManager pointing to the Diagnostics directory.
async Task TerminateNoLock(bool graceful, bool announce, CancellationToken cancellationToken)
Implementation of Terminate(bool, CancellationToken). Does not lock Semaphore
EventType
Types of events. Mirror in tgs.dm
For waiting asynchronously
WatchdogStatus
The current status of the watchdog.
async Task Restart(bool graceful, CancellationToken cancellationToken)
Restarts the watchdog
readonly IJobManager jobManager
The IJobManager for the WatchdogBase.
async Task MonitorRestart(CancellationToken cancellationToken)
Attempt to restart the monitor from scratch.
bool CanApplyWithoutReboot(DreamDaemonLaunchParameters otherParameters)
Check if we match a given set of otherParameters . StartupTimeout is excluded.
async Task ReattachFailure(Task chatTask, CancellationToken cancellationToken)
Call from InitControllers(Task, ReattachInformation, CancellationToken) when a reattach operation fai...
For managing connected chat services
The owning instance was renamed.
WatchdogStatus status
Backing field for Status.
Task monitorTask
The Task running the monitor loop
async Task StopAsync(CancellationToken cancellationToken)
DreamDaemonRights
Rights for Models.DreamDaemon
readonly IDatabaseContextFactory databaseContextFactory
The IDatabaseContextFactory for the WatchdogBase
int heartbeatsMissed
The number of hearbeats missed.
RebootState
Represents the action to take when /world/Reboot() is called
CancellationTokenSource monitorCts
The CancellationTokenSource for the monitor loop
Handler for server restarts
Operation exceptions thrown from the context of a Models.Job
virtual async Task ResetRebootState(CancellationToken cancellationToken)
Cancels pending graceful actions
void DisposeAndNullControllers()
Wrapper for DisposeAndNullControllersImpl under a locked context.
Factory for ISessionControllers
Task Delay(TimeSpan timeSpan, CancellationToken cancellationToken)
Create a Task that completes after a given timeSpan
MonitorActivationReason
Reasons for the monitor to wake up
Data structure for TopicCommandType.EventNotification requests.
async Task< string > HandleChatCommand(string commandName, string arguments, ChatUser sender, CancellationToken cancellationToken)
Handle a chat command
ReattachInformation releasedReattachInformation
Used when detaching servers.
Factory for IDmbProviders
Consumes EventTypes and takes the appropriate actions
Represents a tgs_chat_user datum
async Task StartAsync(CancellationToken cancellationToken)
readonly object controllerDisposeLock
object used for DisposeAndNullControllers.
async Task ChangeSettings(DreamDaemonLaunchParameters launchParameters, CancellationToken cancellationToken)
Changes the ActiveLaunchParameters. If currently running, may trigger a graceful restart.
readonly ISessionPersistor sessionPersistor
The ISessionPersistor for the WatchdogBase
bool releaseServers
If the servers should be released instead of shutdown
Handles communication with a DreamDaemon IProcess
readonly IEventConsumer eventConsumer
The IEventConsumer that is not the WatchdogBase
Manages the runtime of Jobs
Task HandleEvent(EventType eventType, IEnumerable< string > parameters, CancellationToken cancellationToken)
Handle a given eventType
async Task< MonitorAction > HandleHeartbeat(CancellationToken cancellationToken)
Handles a watchdog heartbeat.
async Task Terminate(bool graceful, CancellationToken cancellationToken)
Stops the watchdog
Handles Commands.ICommands that map to those defined in a IChatTrackingContext
async Task HandleRestart(Version updateVersion, CancellationToken cancellationToken)
Handle a restart of the server
async Task CheckLaunchResult(ISessionController controller, string serverName, CancellationToken cancellationToken)
Check the LaunchResult of a given controller for errors and throw a JobException if any are detected...
Interface for using filesystems
readonly Api.Models.Instance instance
The Api.Models.Instance for the WatchdogBase.
async Task< bool > StopMonitor()
Stops MonitorLifetimes(CancellationToken). Doesn't kill the servers
Represents the lifetime of a IRestartHandler registration
ReattachInformation Release()
Releases the IProcess without terminating it. Also calls IDisposable.Dispose
MonitorAction
The action for the monitor loop to take when control is returned to it
Task< LaunchResult > LaunchResult
A Task that completes when DreamDaemon starts pumping the windows message queue after loading a ...
IRestartRegistration RegisterForRestart(IRestartHandler handler)
Register a given handler to run before stopping the server for a restart
readonly IRestartRegistration restartRegistration
The IRestartRegistration for the WatchdogBase.
static async Task< SemaphoreSlimContext > Lock(SemaphoreSlim semaphore, CancellationToken cancellationToken)
Asyncronously locks a semaphore
void RegisterCommandHandler(ICustomCommandHandler customCommandHandler)
Registers a customCommandHandler to use
async Task CreateDump(CancellationToken cancellationToken)
Attempt to create a process dump for DreamDaemon.
Async lock context helper
bool disposed
If the WatchdogBase has been Disposed.
Runs and monitors the twin server controllers
Represents a service that may take an updated Host assembly and run it, stopping the current assembly...