1 using Microsoft.EntityFrameworkCore;
2 using Microsoft.Extensions.Logging;
6 using System.Collections.Generic;
9 using System.Threading.Tasks;
28 #pragma warning disable CA1506 // TODO: Decomplexify 38 Logger.LogTrace(
"Running set to {0}", running);
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; }
188 Api.Models.Instance instance,
191 Chat = chat ??
throw new ArgumentNullException(nameof(chat));
192 SessionControllerFactory = sessionControllerFactory ??
throw new ArgumentNullException(nameof(sessionControllerFactory));
193 DmbFactory = dmbFactory ??
throw new ArgumentNullException(nameof(dmbFactory));
194 this.reattachInfoHandler = reattachInfoHandler ??
throw new ArgumentNullException(nameof(reattachInfoHandler));
195 this.databaseContextFactory = databaseContextFactory ??
throw new ArgumentNullException(nameof(databaseContextFactory));
196 this.jobManager = jobManager ??
throw new ArgumentNullException(nameof(jobManager));
197 AsyncDelayer = asyncDelayer ??
throw new ArgumentNullException(nameof(asyncDelayer));
198 Logger = logger ??
throw new ArgumentNullException(nameof(logger));
199 ActiveLaunchParameters = initialLaunchParameters ??
throw new ArgumentNullException(nameof(initialLaunchParameters));
200 this.instance = instance ??
throw new ArgumentNullException(nameof(instance));
201 this.autoStart = autoStart;
203 if (serverControl == null)
204 throw new ArgumentNullException(nameof(serverControl));
208 ActiveLaunchParameters = initialLaunchParameters;
209 releaseServers =
false;
210 ActiveParametersUpdated =
new TaskCompletionSource<object>();
211 controllerDisposeLock =
new object();
216 Semaphore =
new SemaphoreSlim(1);
220 restartRegistration.Dispose();
228 Logger.LogTrace(
"Disposing...");
230 restartRegistration.Dispose();
231 DisposeAndNullControllers();
232 monitorCts?.Dispose();
243 async Task
TerminateNoLock(
bool graceful,
bool announce, CancellationToken cancellationToken)
249 var eventTask = HandleEvent(releaseServers ?
EventType.WatchdogDetach :
EventType.WatchdogShutdown, null, cancellationToken);
251 var chatTask = announce ? Chat.SendWatchdogMessage(
"Shutting down...",
false, cancellationToken) : Task.CompletedTask;
253 await eventTask.ConfigureAwait(
false);
255 await StopMonitor().ConfigureAwait(
false);
257 DisposeAndNullControllers();
259 LastLaunchParameters = null;
261 await chatTask.ConfigureAwait(
false);
266 var toKill = GetActiveController();
269 await toKill.SetRebootState(Session.RebootState.Shutdown, cancellationToken).ConfigureAwait(
false);
270 Logger.LogTrace(
"Graceful termination requested");
273 Logger.LogTrace(
"Could not gracefully terminate as there is no active controller!");
284 Logger.LogTrace(
"Sending heartbeat to active server...");
287 var shouldShutdown = activeServer.
RebootState == Session.RebootState.Shutdown;
288 if (response == null)
290 switch (++heartbeatsMissed)
293 Logger.LogDebug(
"DEFCON 4: DreamDaemon missed first heartbeat!");
296 var message2 =
"DEFCON 3: DreamDaemon has missed 2 heartbeats!";
297 Logger.LogInformation(message2);
298 await Chat.SendWatchdogMessage(message2,
true, cancellationToken).ConfigureAwait(
false);
301 var actionToTake = shouldShutdown
304 var message3 = $
"DEFCON 2: DreamDaemon has missed 3 heartbeats! If it does not respond to the next one, the watchdog will {actionToTake}!";
305 Logger.LogWarning(message3);
306 await Chat.SendWatchdogMessage(message3,
false, cancellationToken).ConfigureAwait(
false);
309 var actionTaken = shouldShutdown
310 ?
"Shutting down due to graceful termination request" 312 var message4 = $
"DEFCON 1: Four heartbeats have been missed! {actionTaken}...";
313 Logger.LogWarning(message4);
314 DisposeAndNullControllers();
315 await Chat.SendWatchdogMessage(message4,
false, cancellationToken).ConfigureAwait(
false);
318 Logger.LogError(
"Invalid heartbeats missed count: {0}", heartbeatsMissed);
323 heartbeatsMissed = 0;
338 Logger.LogTrace(
"Begin LaunchImplNoLock");
348 if (startMonitor && await StopMonitor().ConfigureAwait(
false))
349 chatTask = Chat.SendWatchdogMessage(
"Automatic retry sequence cancelled by manual launch. Restarting...",
false, cancellationToken);
351 chatTask = Chat.SendWatchdogMessage(reattachInfo == null ?
"Launching..." :
"Reattaching...",
false, cancellationToken);
353 chatTask = Task.CompletedTask;
356 LastLaunchParameters = ActiveLaunchParameters;
357 heartbeatsMissed = 0;
360 var recursiveCallToHappen =
false;
363 await InitControllers(() => recursiveCallToHappen =
true, chatTask, reattachInfo, cancellationToken).ConfigureAwait(
false);
364 if (recursiveCallToHappen)
367 await chatTask.ConfigureAwait(
false);
369 Logger.LogInformation(
"Launched servers successfully");
374 monitorCts =
new CancellationTokenSource();
375 monitorTask = MonitorLifetimes(monitorCts.Token);
381 if (!recursiveCallToHappen && !cancellationToken.IsCancellationRequested)
383 var originalChatTask = chatTask;
384 async Task ChainChatTaskWithErrorMessage()
386 await originalChatTask.ConfigureAwait(
false);
387 await Chat.SendWatchdogMessage(
"Startup failed!",
false, cancellationToken).ConfigureAwait(
false);
390 chatTask = ChainChatTaskWithErrorMessage();
391 Logger.LogWarning(
"Failed to start watchdog: {0}", e.ToString());
401 await chatTask.ConfigureAwait(
false);
403 catch (OperationCanceledException) { }
413 Logger.LogTrace(
"StopMonitor");
414 if (monitorTask == null)
416 var wasRunning = !monitorTask.IsCompleted;
418 await monitorTask.ConfigureAwait(
false);
419 Logger.LogTrace(
"Stopped Monitor");
420 monitorCts.Dispose();
435 var launchResult = await controller.
LaunchResult.WithToken(cancellationToken).ConfigureAwait(
false);
438 if (launchResult.ExitCode.HasValue)
441 new JobException($
"{serverName} failed to start: {launchResult}"));
442 if (!launchResult.StartupTime.HasValue)
445 new JobException($
"{serverName} timed out on startup: {ActiveLaunchParameters.StartupTimeout.Value}s"));
451 protected abstract void DisposeAndNullControllersImpl();
458 Logger.LogTrace(
"DisposeAndNullControllers");
459 lock (controllerDisposeLock)
460 DisposeAndNullControllersImpl();
481 protected abstract IReadOnlyDictionary<MonitorActivationReason, Task> GetMonitoredServerTasks(
MonitorState monitorState);
490 protected abstract Task HandleMonitorWakeup(
493 CancellationToken cancellationToken);
500 private async Task<MonitorState>
MonitorRestart(CancellationToken cancellationToken)
502 Logger.LogTrace(
"Monitor restart!");
503 DisposeAndNullControllers();
505 var chatTask = Task.CompletedTask;
506 for (var retryAttempts = 1; ; ++retryAttempts)
508 Exception launchException = null;
513 await LaunchNoLock(
false,
false, null, cancellationToken).ConfigureAwait(
false);
516 Logger.LogDebug(
"Relaunch successful, resetting monitor state...");
520 catch (OperationCanceledException)
529 await chatTask.ConfigureAwait(
false);
532 if (launchException == null)
533 Logger.LogWarning(
"Failed to automatically restart the watchdog! Attempt: {0}", retryAttempts);
535 Logger.LogWarning(
"Failed to automatically restart the watchdog! Attempt: {0}, Exception: {1}", retryAttempts, launchException);
536 var retryDelay = Math.Min(
538 Math.Pow(2, retryAttempts)),
539 TimeSpan.FromHours(1).Seconds);
540 chatTask = Chat.SendWatchdogMessage(
541 $
"Failed to restart (Attempt: {retryAttempts}), retrying in {retryDelay}",
546 TimeSpan.FromSeconds(retryDelay),
549 .ConfigureAwait(
false);
561 Logger.LogTrace(
"Entered MonitorLifetimes");
562 using var _ = cancellationToken.Register(() => Logger.LogTrace(
"Monitor cancellationToken triggered"));
569 using (LogContext.PushProperty(
"Monitor", iteration))
572 Logger.LogTrace(
"Iteration {0} of monitor loop", iteration);
575 var serverTasks = GetMonitoredServerTasks(monitorState);
576 if (serverTasks.Count != 5)
577 throw new InvalidOperationException(
"Expected 5 monitored server tasks!");
585 Task activeLaunchParametersChanged = ActiveParametersUpdated.Task;
588 var heartbeatSeconds = ActiveLaunchParameters.HeartbeatSeconds.Value;
589 var heartbeat = heartbeatSeconds == 0
590 ? Extensions.TaskExtensions.InfiniteTask()
591 : Task.Delay(TimeSpan.FromSeconds(heartbeatSeconds));
594 var cancelTcs =
new TaskCompletionSource<object>();
595 var toWaitOn = Task.WhenAny(
596 activeServerLifetime,
598 inactiveServerLifetime,
599 inactiveServerReboot,
600 inactiveStartupComplete,
604 activeLaunchParametersChanged);
607 using (cancellationToken.Register(() => cancelTcs.SetCanceled()))
608 await toWaitOn.ConfigureAwait(
false);
610 cancellationToken.ThrowIfCancellationRequested();
611 Logger.LogTrace(
"Monitor activated");
623 var taskCompleted = task?.IsCompleted ==
true;
627 else if (taskCompleted)
629 activationReason = testActivationReason;
637 var anyActivation = CheckActivationReason(ref activeServerLifetime,
MonitorActivationReason.ActiveServerCrashed)
643 || CheckActivationReason(ref activeLaunchParametersChanged,
MonitorActivationReason.ActiveLaunchParametersUpdated)
647 moreActivationsToProcess =
false;
650 Logger.LogTrace(
"Reason: {0}", activationReason);
652 monitorState.
NextAction = await HandleHeartbeat(
655 .ConfigureAwait(
false);
657 await HandleMonitorWakeup(
661 .ConfigureAwait(
false);
666 Logger.LogTrace(
"Next monitor action is to {0}", monitorState.
NextAction);
670 monitorState = await MonitorRestart(cancellationToken).ConfigureAwait(
false);
672 catch (OperationCanceledException)
681 "Monitor crashed! Iteration: {0}, Monitor State: {1}, Exception: {2}",
683 JsonConvert.SerializeObject(monitorState),
689 var chatTask = Chat.SendWatchdogMessage(
690 $
"Monitor crashed, this should NEVER happen! Please report this, full details in logs! {nextActionMessage}. Error: {e.Message}",
697 monitorState = await MonitorRestart(cancellationToken).ConfigureAwait(
false);
699 await chatTask.ConfigureAwait(
false);
702 catch (OperationCanceledException)
705 Logger.LogDebug(
"Monitor cancelled");
709 Logger.LogTrace(
"Detaching servers...");
710 releasedReattachInformation = CreateReattachInformation();
714 DisposeAndNullControllers();
716 Logger.LogTrace(
"Monitor exiting...");
727 protected abstract Task InitControllers(Action callBeforeRecurse, Task chatTask,
DualReattachInformation reattachInfo, CancellationToken cancellationToken);
735 ActiveLaunchParameters = launchParameters;
736 if (match || !Running)
739 ActiveParametersUpdated.TrySetResult(null);
740 ActiveParametersUpdated =
new TaskCompletionSource<object>();
745 public async Task
HandleEvent(
EventType eventType, IEnumerable<string> parameters, CancellationToken cancellationToken)
747 var activeServer = GetActiveController();
750 if (activeServer == null)
754 var result = await activeServer.SendCommand(
757 .ConfigureAwait(
false);
759 if (result?.InteropResponse?.ChatResponses != null)
761 result.InteropResponse.ChatResponses.Select(
762 x => Chat.SendMessage(
765 .Select(channelIdString =>
767 if (UInt64.TryParse(channelIdString, out var channelId))
768 return (ulong?)channelId;
772 .Where(nullableChannelId => nullableChannelId.HasValue)
773 .Select(nullableChannelId => nullableChannelId.Value),
775 .ConfigureAwait(
false);
784 return "ERROR: Server offline!";
786 var commandObject =
new ChatCommand(sender, commandName, arguments);
790 var activeServer = GetActiveController();
791 var commandResult = await activeServer.SendCommand(command, cancellationToken).ConfigureAwait(
false);
793 if (commandResult == null)
794 return "TGS: Bad topic exchange!";
796 if (commandResult.InteropResponse == null)
797 return "TGS: Bad topic response!";
799 return commandResult.InteropResponse.CommandResponseMessage ??
800 "TGS: Command processed but no DMAPI response returned!";
805 public async Task
Launch(CancellationToken cancellationToken)
808 await LaunchNoLock(
true,
true, null, cancellationToken).ConfigureAwait(
false);
818 var toClear = GetActiveController();
820 await toClear.SetRebootState(Session.RebootState.Normal, cancellationToken).ConfigureAwait(
false);
825 public async Task
Restart(
bool graceful, CancellationToken cancellationToken)
830 Logger.LogTrace(
"Begin Restart. Graceful: {0}", graceful);
838 chatTask = Chat.SendWatchdogMessage(
"Manual restart triggered...",
false, cancellationToken);
839 await TerminateNoLock(
false,
false, cancellationToken).ConfigureAwait(
false);
842 chatTask = Task.CompletedTask;
843 await LaunchNoLock(
true, !Running, null, cancellationToken).ConfigureAwait(
false);
844 await chatTask.ConfigureAwait(
false);
847 var toReboot = GetActiveController();
849 && !await toReboot.SetRebootState(Session.RebootState.Restart, cancellationToken).ConfigureAwait(
false))
850 Logger.LogWarning(
"Unable to send reboot state change event!");
855 public async Task
StartAsync(CancellationToken cancellationToken)
857 var reattachInfo = await reattachInfoHandler.Load(cancellationToken).ConfigureAwait(
false);
858 if (!autoStart && reattachInfo == null)
861 long? adminUserId = null;
863 await databaseContextFactory.UseContext(
864 async db => adminUserId = await db
867 .Where(x => x.CanonicalName == Models.User.CanonicalizeName(Api.Models.User.AdminName))
869 .FirstAsync(cancellationToken)
870 .ConfigureAwait(
false))
871 .ConfigureAwait(
false);
872 var job =
new Models.Job
874 StartedBy =
new Models.User
876 Id = adminUserId.Value
882 Description = $
"Instance startup watchdog {(reattachInfo != null ? "reattach
" : "launch
")}",
886 await jobManager.RegisterOperation(job, async (j, databaseContextFactory, progressFunction, ct) =>
889 await LaunchNoLock(
true,
true, reattachInfo, ct).ConfigureAwait(
false);
890 }, cancellationToken).ConfigureAwait(
false);
894 public async Task
StopAsync(CancellationToken cancellationToken)
896 await TerminateNoLock(
false, !releaseServers, cancellationToken).ConfigureAwait(
false);
897 if (releasedReattachInformation != null)
899 await reattachInfoHandler.Save(releasedReattachInformation, cancellationToken).ConfigureAwait(
false);
900 releasedReattachInformation = null;
901 releaseServers =
false;
906 public async Task
Terminate(
bool graceful, CancellationToken cancellationToken)
909 await TerminateNoLock(graceful, !releaseServers, cancellationToken).ConfigureAwait(
false);
913 public async Task
HandleRestart(Version updateVersion, CancellationToken cancellationToken)
915 releaseServers =
true;
917 await Chat.SendWatchdogMessage(
"Detaching...",
false, cancellationToken).ConfigureAwait(
false);
921 public abstract Task
InstanceRenamed(
string newInstanceName, CancellationToken cancellationToken);
readonly bool autoStart
If the WatchdogBase should LaunchNoLock(bool, bool, DualReattachInformation, CancellationToken) in St...
async Task Launch(CancellationToken cancellationToken)
Start the IWatchdog
Base class for IWatchdogs.
ErrorCode
Types of ErrorMessages that the API may return.
MonitorAction NextAction
The next MonitorAction to take in WatchdogBase.MonitorLifetimes(global::System.Threading.CancellationToken)
RightsType
The type of rights a model uses
Use server authentication
Parameters for a topic request.
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
WatchdogBase(IChatManager chat, ISessionControllerFactory sessionControllerFactory, IDmbFactory dmbFactory, IReattachInfoHandler reattachInfoHandler, IDatabaseContextFactory databaseContextFactory, IJobManager jobManager, IServerControl serverControl, IAsyncDelayer asyncDelayer, ILogger logger, DreamDaemonLaunchParameters initialLaunchParameters, Api.Models.Instance instance, bool autoStart)
Initializes a new instance of the WatchdogBase .
async Task< MonitorState > MonitorRestart(CancellationToken cancellationToken)
Attempt to restart the monitor from scratch.
async Task TerminateNoLock(bool graceful, bool announce, CancellationToken cancellationToken)
Implementation of Terminate(bool, CancellationToken). Does not lock Semaphore
async Task HandleEvent(EventType eventType, IEnumerable< string > parameters, CancellationToken cancellationToken)
Handle a given eventType
EventType
Types of events. Mirror in tgs.dm
For waiting asynchronously
async Task Restart(bool graceful, CancellationToken cancellationToken)
Restarts the watchdog
readonly IJobManager jobManager
The IJobManager for the WatchdogBase.
bool CanApplyWithoutReboot(DreamDaemonLaunchParameters otherParameters)
Check if we match a given set of otherParameters . StartupTimeout is excluded.
readonly IReattachInfoHandler reattachInfoHandler
The IReattachInfoHandler for the WatchdogBase
For managing connected chat services
The owning instance was renamed.
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
async Task< MonitorAction > HandleHeartbeat(ISessionController activeServer, CancellationToken cancellationToken)
Handles a watchdog heartbeat.
Operation exceptions thrown from the context of a Models.Job
RebootState RebootState
The current RebootState
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
Task< CombinedTopicResponse > SendCommand(TopicParameters parameters, CancellationToken cancellationToken)
Sends a command to DreamDaemon through /world/Topic()
bool running
Backing field for Running.
ISessionController ActiveServer
The active ISessionController
Factory for IDmbProviders
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 triggers a graceful restart ...
async Task LaunchNoLock(bool startMonitor, bool announce, DualReattachInformation reattachInfo, CancellationToken cancellationToken)
Launches the watchdog.
bool releaseServers
If the servers should be released instead of shutdown
Handles communication with a DreamDaemon IProcess
Manages the runtime of Jobs
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...
Handles saving and loading DualReattachInformation
DualReattachInformation releasedReattachInformation
Used when detaching servers.
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
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
The (absolute) state of the ExperimentalWatchdog
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 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...