tgstation-server 6.19.0
The /tg/station 13 server suite
Loading...
Searching...
No Matches
WatchdogBase.cs
Go to the documentation of this file.
1using System;
4using System.Linq;
7
9
10using Prometheus;
11
12using Serilog.Context;
13
30
32{
36#pragma warning disable CA1506 // TODO: Decomplexify
38 {
41
44
46 public uint? ClientCount { get; private set; }
47
49 public DateTimeOffset? LaunchTime => GetActiveController()?.LaunchTime;
50
53 {
54 get => status;
55 protected set
56 {
58 status = value;
59 Logger.LogTrace("Status set from {oldStatus} to {status}", oldStatus, status);
60 }
61 }
62
65
67 public abstract bool AlphaIsActive { get; }
68
71
73 public DreamDaemonLaunchParameters? LastLaunchParameters { get; protected set; }
74
76 public Models.CompileJob? ActiveCompileJob => GetActiveController()?.CompileJob;
77
79 public abstract RebootState? RebootState { get; }
80
85
89 protected ILogger<WatchdogBase> Logger { get; }
90
94 protected IChatManager Chat { get; }
95
100
104 protected IDmbFactory DmbFactory { get; }
105
109 protected IAsyncDelayer AsyncDelayer { get; }
110
114 protected IIOManager GameIOManager { get; }
115
120
125
130
135
140
145
150
155
160
165
170
175
179 volatile TaskCompletionSource activeParametersUpdated;
180
185
190
195
200
205
210
230 protected WatchdogBase(
231 IChatManager chat,
232 ISessionControllerFactory sessionControllerFactory,
233 IDmbFactory dmbFactory,
236 IServerControl serverControl,
237 IAsyncDelayer asyncDelayer,
241 IMetricFactory metricFactory,
242 IIOManager gameIOManager,
245 Api.Models.Instance metadata,
246 bool autoStart)
247 {
248 Chat = chat ?? throw new ArgumentNullException(nameof(chat));
249 SessionControllerFactory = sessionControllerFactory ?? throw new ArgumentNullException(nameof(sessionControllerFactory));
250 DmbFactory = dmbFactory ?? throw new ArgumentNullException(nameof(dmbFactory));
253 AsyncDelayer = asyncDelayer ?? throw new ArgumentNullException(nameof(asyncDelayer));
257 ArgumentNullException.ThrowIfNull(metricFactory);
258 GameIOManager = gameIOManager ?? throw new ArgumentNullException(nameof(gameIOManager));
259 Logger = logger ?? throw new ArgumentNullException(nameof(logger));
263
264 ArgumentNullException.ThrowIfNull(serverControl);
265
266 watchdogStatusMetric = metricFactory.CreateGauge(
267 "tgs_watchdog_status",
268 $"TGS Watchdog status: {(int)WatchdogStatus.Offline} = Offline, {(int)WatchdogStatus.Online} = Online, {(int)WatchdogStatus.Restoring} = Restoring, {(int)WatchdogStatus.DelayedRestart} = Delayed Restart");
269 cpuUsageMetric = metricFactory.CreateGauge("tgs_game_cpu_usage", "Estimated total CPU usage time for the game process from 0-1");
270 ramUsageMetric = metricFactory.CreateGauge("tgs_game_ram_usage", "Total used bytes of private memory for the game process");
271
272 chat.RegisterCommandHandler(this);
273
275 releaseServers = false;
276 activeParametersUpdated = new TaskCompletionSource();
277
278 restartRegistration = serverControl.RegisterForRestart(this);
279 try
280 {
283 }
284 catch
285 {
286 restartRegistration.Dispose();
287 synchronizationSemaphore?.Dispose();
288 throw;
289 }
290
291 Logger.LogTrace("Created watchdog");
292 }
293
296 {
297 Logger.LogTrace("Disposing...");
298 synchronizationSemaphore.Dispose();
299 restartRegistration.Dispose();
300
303 monitorCts?.Dispose();
304
305 disposed = true;
306 }
307
310 {
312 {
316 if (!currentEngine.HasValue)
317 return false;
318
319 bool match = launchParameters.CanApplyWithoutReboot(currentLaunchParameters, currentEngine.Value);
320 if (match || Status == WatchdogStatus.Offline || Status == WatchdogStatus.DelayedRestart)
321 return false;
322
323 var oldTcs = Interlocked.Exchange(ref activeParametersUpdated, new TaskCompletionSource());
324 oldTcs.SetResult();
325 }
326
327 return true;
328 }
329
332 {
334 {
338 if (Status != WatchdogStatus.Online || activeServer == null)
339 return new MessageContent
340 {
341 Text = "TGS: Server offline!",
342 };
343
345
346 if (commandResult == null)
347 return new MessageContent
348 {
349 Text = "TGS: Bad topic exchange!",
350 };
351
352 if (commandResult == null)
353 return new MessageContent
354 {
355 Text = "TGS: Bad topic response!",
356 };
357
359 {
360 Text = commandResult.CommandResponse?.Text ?? commandResult.CommandResponseMessage,
361 Embed = commandResult.CommandResponse?.Embed,
362 };
363
364 if (commandResponse.Text == null && commandResponse.Embed == null)
365 {
366 commandResponse.Text = "TGS: Command processed but no DMAPI response returned!";
367 }
368
370
371 return commandResponse;
372 }
373 }
374
376 public async ValueTask Launch(CancellationToken cancellationToken)
377 {
378 if (Status != WatchdogStatus.Offline)
379 throw new JobException(ErrorCode.WatchdogRunning);
381 await LaunchNoLock(true, true, true, null, cancellationToken);
382 }
383
385 public virtual async ValueTask ResetRebootState(CancellationToken cancellationToken)
386 {
388 {
389 if (Status == WatchdogStatus.Offline)
390 return;
392 if (toClear != null)
393 await toClear.SetRebootState(Session.RebootState.Normal, cancellationToken);
394 }
395 }
396
398 public async ValueTask Restart(bool graceful, CancellationToken cancellationToken)
399 {
400 if (Status == WatchdogStatus.Offline)
401 throw new JobException(ErrorCode.WatchdogNotRunning);
402
403 Logger.LogTrace("Begin Restart. Graceful: {gracefulFlag}", graceful);
405 {
406 if (!graceful)
407 {
408 Chat.QueueWatchdogMessage("Manual restart triggered...");
410 await LaunchNoLock(true, false, true, null, cancellationToken);
411 return;
412 }
413
415 if (toReboot != null
416 && !await toReboot.SetRebootState(Session.RebootState.Restart, cancellationToken))
417 Logger.LogWarning("Unable to send reboot state change event!");
418 }
419 }
420
422 public async Task StartAsync(CancellationToken cancellationToken)
423 {
425 var reattaching = reattachInfo != null;
426 if (!autoStart && !reattaching)
427 return;
428
429 var job = Models.Job.Create(
431 ? JobCode.StartupWatchdogReattach
432 : JobCode.StartupWatchdogLaunch,
433 null,
434 metadata,
435 DreamDaemonRights.Shutdown);
437 job,
438 async (core, databaseContextFactory, paramJob, progressFunction, ct) =>
439 {
440 if (core?.Watchdog != this)
442
444 await LaunchNoLock(true, true, true, reattachInfo, ct);
445
447 },
449 }
450
452 public async Task StopAsync(CancellationToken cancellationToken) =>
454
461
464 {
466 {
468
469 if (Status != WatchdogStatus.Offline)
470 {
471 Logger.LogDebug("Waiting for server to gracefully shut down.");
473 }
474 else
475 Logger.LogTrace("Graceful shutdown requested but server is already offline.");
476
477 return;
478 }
479
480 releaseServers = true;
481 if (Status == WatchdogStatus.Online)
482 Chat.QueueWatchdogMessage("Detaching...");
483 else
484 Logger.LogTrace("Not sending detach chat message as status is: {status}", Status);
485 }
486
488 public abstract ValueTask InstanceRenamed(string newInstanceName, CancellationToken cancellationToken);
489
496
498 public async ValueTask<bool> Broadcast(string message, CancellationToken cancellationToken)
499 {
500 ArgumentNullException.ThrowIfNull(message);
501
503 if (activeServer == null)
504 {
505 Logger.LogInformation("Attempted broadcast failed, no active server!");
506 return false;
507 }
508
509 if (!activeServer.DMApiAvailable)
510 {
511 Logger.LogInformation("Attempted broadcast failed, no DMAPI!");
512 return false;
513 }
514
515 var minimumRequiredVersion = new Version(5, 7, 0);
516 if (activeServer.DMApiVersion < minimumRequiredVersion)
517 {
518 Logger.LogInformation(
519 "Attempted broadcast failed, insufficient interop version: {interopVersion}. Requires {minimumRequiredVersion}!",
520 activeServer.DMApiVersion,
522 return false;
523 }
524
525 Logger.LogInformation("Broadcasting: {message}", message);
526
527 var response = await activeServer.SendCommand(
530
531 return response != null && response.ErrorMessage == null;
532 }
533
535 public void RunMetricsScrape()
536 {
540 cpuUsageMetric.Set(controller?.MeasureProcessorTimeDelta() ?? 0);
541 }
542
545 {
547
548 // Method explicitly implemented to prevent accidental calls when this.eventConsumer should be used.
550
551 // Server may have ended
552 if (activeServer == null)
553 return;
554
556 var result = await activeServer.SendCommand(
559
561 }
562
565 => throw new NotSupportedException("Watchdogs do not support custom events!");
566
575
586 bool startMonitor,
587 bool announce,
588 bool announceFailure,
590 CancellationToken cancellationToken)
591 {
592 Logger.LogTrace("Begin LaunchImplNoLock");
593 if (startMonitor && Status != WatchdogStatus.Offline)
594 throw new JobException(ErrorCode.WatchdogRunning);
595
596 if (reattachInfo == null && !DmbFactory.DmbAvailable)
597 throw new JobException(ErrorCode.WatchdogCompileJobCorrupted);
598
599 // this is necessary, the monitor could be in it's sleep loop trying to restart, if so cancel THAT monitor and start our own with blackjack and hookers
600 var eventTask = ValueTask.CompletedTask;
601 if (announce)
602 {
604 reattachInfo == null
605 ? "Launching..."
606 : "Reattaching..."); // simple announce
607 if (reattachInfo == null)
608 eventTask = HandleEventImpl(EventType.WatchdogLaunch, Enumerable.Empty<string>(), false, cancellationToken);
609 }
610
611 // since neither server is running, this is safe to do
614 ClientCount = null;
615
616 try
617 {
619 }
621 {
622 Logger.LogTrace(ex, "Controller initialization cancelled!");
623 throw;
624 }
625 catch (Exception e)
626 {
627 Logger.LogWarning(e, "Failed to start watchdog!");
630 {
632 if (announceFailure)
633 Chat.QueueWatchdogMessage("Startup failed!");
634 }
635
637 throw;
638 }
639 finally
640 {
641 // finish the chat task that's in flight
642 try
643 {
645 }
647 {
648 Logger.LogTrace(ex, "Announcement task canceled!");
649 }
650 }
651
652 Logger.LogInformation("Controller(s) initialized successfully");
653
654 if (startMonitor)
655 {
658 }
659 }
660
666 {
667 Logger.LogTrace("StopMonitor");
668 if (monitorTask == null)
669 return false;
670 var wasRunning = !monitorTask.IsCompleted;
671 monitorCts!.Cancel();
673 Logger.LogTrace("Stopped Monitor");
674 monitorCts.Dispose();
675 monitorTask = null;
676 monitorCts = null;
677 return wasRunning;
678 }
679
688 {
689 var launchResult = await controller.LaunchResult.WaitAsync(cancellationToken);
690
691 // Dead sessions won't trigger this
692 if (launchResult.ExitCode.HasValue) // you killed us ray...
693 throw new JobException(
694 ErrorCode.WatchdogStartupFailed,
695 new JobException($"{serverName} failed to start: {launchResult}"));
696 if (!launchResult.StartupTime.HasValue)
697 throw new JobException(
698 ErrorCode.WatchdogStartupTimeout,
699 new JobException($"{serverName} timed out on startup: {ActiveLaunchParameters.StartupTimeout!.Value}s"));
700 }
701
708 {
709 // we lost the server, just restart entirely
710 // DCT: Operation must always run
711 await DisposeAndNullControllers(CancellationToken.None);
712 ClientCount = null;
713 const string FailReattachMessage = "Unable to properly reattach to server! Restarting watchdog...";
714 Logger.LogWarning(FailReattachMessage);
715
717 await InitController(ValueTask.CompletedTask, null, cancellationToken);
718 }
719
725
741
747
756 CancellationToken cancellationToken);
757
764 protected async ValueTask BeforeApplyDmb(Models.CompileJob newCompileJob, CancellationToken cancellationToken)
765 {
767 {
768 Logger.LogTrace("Same compile job, not sending deployment event");
769 return;
770 }
771
773 metadata,
775
777 EventType.DeploymentActivation,
778 new List<string?>
779 {
780 GameIOManager.ResolvePath(newCompileJob.DirectoryName!.Value.ToString()),
781 },
782 false,
784
785 try
786 {
788 }
789 catch (Exception ex)
790 {
791 Logger.LogWarning(ex, "Failed to apply remote deployment!");
792 }
793
795 }
796
806 {
807 try
808 {
809 var sessionEventTask = relayToSession ? ((IEventConsumer)this).HandleEvent(eventType, parameters, false, cancellationToken) : ValueTask.CompletedTask;
814 }
815 catch (JobException ex)
816 {
817 Logger.LogError(ex, "Suppressing exception triggered by event!");
818 }
819 }
820
827 {
828 Logger.LogTrace("Monitor restart!");
829
831
832 for (var retryAttempts = 1; ; ++retryAttempts)
833 {
834 Status = WatchdogStatus.Restoring;
837 try
838 {
839 // use LaunchImplNoLock without announcements or restarting the monitor
840 await LaunchNoLock(false, false, false, null, cancellationToken);
841 Status = WatchdogStatus.Online;
842 Logger.LogDebug("Relaunch successful, resuming monitor...");
843 return;
844 }
846 {
848 }
849
850 Logger.LogWarning(launchException, "Failed to automatically restart the watchdog! Attempt: {attemptNumber}", retryAttempts);
851 Status = WatchdogStatus.DelayedRestart;
852
853 var retryDelay = Math.Min(
854 Convert.ToInt32(
855 Math.Pow(2, retryAttempts)),
856 TimeSpan.FromHours(1).TotalSeconds); // max of one hour, increasing by a power of 2 each time
857
859 $"Failed to restart (Attempt: {retryAttempts}), retrying in {retryDelay}s...");
860
862 TimeSpan.FromSeconds(retryDelay),
864 }
865 }
866
873 {
875
878 {
879 Logger.LogDebug("Found new CompileJob without waiting");
880 return;
881 }
882
884 }
885
891#pragma warning disable CA1502
893 {
894 Logger.LogTrace("Entered MonitorLifetimes");
895 Status = WatchdogStatus.Online;
896 using var cancellationTokenLoggingRegistration = cancellationToken.Register(() => Logger.LogTrace("Monitor cancellationToken triggered"));
897
898 // this function is responsible for calling HandlerMonitorWakeup when necessary and manitaining the MonitorState
899 try
900 {
902 Task? activeServerLifetime = null,
903 activeServerReboot = null,
904 activeServerStartup = null,
905 serverPrimed = null,
907 newDmbAvailable = null,
908 healthCheck = null;
910 var ranInitialDmbCheck = false;
911 for (ulong iteration = 1; nextAction != MonitorAction.Exit; ++iteration)
913 {
914 var nextMonitorWakeupTcs = new TaskCompletionSource();
915 try
916 {
917 Logger.LogTrace("Iteration {iteration} of monitor loop", iteration);
918 nextAction = MonitorAction.Continue;
919
921
923 {
926 {
927 if (sameController && oldTask?.IsCompleted == true)
928 return;
929
931 }
932
933 controller!.RebootGate = nextMonitorWakeupTcs.Task;
934
939
940 if (!sameController)
942
946 () =>
947 {
951 ranInitialDmbCheck = true;
952 return result;
953 });
954 }
955
956 if (controller != null)
957 {
959
964 : Task.Delay(
965 TimeSpan.FromSeconds(healthCheckSeconds),
967
968 // cancel waiting if requested
969 var toWaitOn = Task.WhenAny(
976 serverPrimed!);
977
978 // wait for something to happen
980 }
981 else
982 {
983 Logger.LogError("Controller was null on monitor wakeup! Attempting restart...");
984 nextAction = MonitorAction.Restart; // excuse me wtf?
985 }
986
987 cancellationToken.ThrowIfCancellationRequested();
988 Logger.LogTrace("Monitor activated");
989
990 // always run HandleMonitorWakeup from the context of the semaphore lock
992 {
993 // Set this sooner so chat sends don't hold us up
995 Status = WatchdogStatus.Restoring;
996
997 // multiple things may have happened, handle them one at a time
999 {
1000 MonitorActivationReason activationReason = default; // this will always be assigned before being used
1001
1003 {
1004 var taskCompleted = task?.IsCompleted == true;
1005 task = null;
1006 if (nextAction == MonitorAction.Skip)
1007 nextAction = MonitorAction.Continue;
1008 else if (taskCompleted)
1009 {
1011 return true;
1012 }
1013
1014 return false;
1015 }
1016
1017 // process the tasks in this order and call HandlerMonitorWakup for each depending on the new monitorState
1025
1027
1028 if (!anyActivation)
1030 else
1031 {
1032 Logger.LogTrace("Reason: {activationReason}", activationReason);
1033 if (activationReason == MonitorActivationReason.HealthCheck)
1036 else
1040 }
1041 }
1042 }
1043
1044 Logger.LogTrace("Next monitor action is to {nextAction}", nextAction);
1045
1046 // Restart if requested
1047 if (nextAction == MonitorAction.Restart)
1048 {
1050 nextAction = MonitorAction.Continue;
1051 }
1052 }
1054 {
1055 // really, this should NEVER happen
1056 Logger.LogError(
1057 e,
1058 "Monitor crashed! Iteration: {iteration}",
1059 iteration);
1060
1062 ? "Recovering"
1063 : "Shutting down";
1065 $"Monitor crashed, this should NEVER happen! Please report this, full details in logs! {nextActionMessage}. Error: {e.Message}");
1066
1067 if (disposed)
1069 else if (nextAction != MonitorAction.Exit)
1070 {
1071 if (GetActiveController()?.Lifetime.IsCompleted != true)
1073 else
1074 Logger.LogDebug("Server seems to be okay, not restarting");
1075 nextAction = MonitorAction.Continue;
1076 }
1077 }
1078 finally
1079 {
1080 nextMonitorWakeupTcs.SetResult();
1081 }
1082 }
1083 }
1085 {
1086 // stop signal
1087 Logger.LogDebug("Monitor cancelled");
1088
1089 if (releaseServers)
1090 {
1091 Logger.LogTrace("Detaching server...");
1093 if (controller != null)
1094 await controller.Release();
1095 else
1096 Logger.LogError("Controller was null on monitor shutdown!");
1097 }
1098 }
1099
1100 // DCT: Operation must always run
1101 await DisposeAndNullControllers(CancellationToken.None);
1102 Status = WatchdogStatus.Offline;
1103
1104 Logger.LogTrace("Monitor exiting...");
1105 }
1106#pragma warning restore CA1502
1107
1116 {
1117 if (Status == WatchdogStatus.Offline)
1118 return;
1119 if (!graceful)
1120 {
1123 ? EventType.WatchdogDetach
1124 : EventType.WatchdogShutdown,
1125 Enumerable.Empty<string>(),
1128
1129 if (announce)
1130 Chat.QueueWatchdogMessage("Shutting down...");
1131
1133
1135
1136 LastLaunchParameters = null;
1137 return;
1138 }
1139
1140 // merely set the reboot state
1142 if (toKill != null)
1143 {
1144 await toKill.SetRebootState(Session.RebootState.Shutdown, cancellationToken);
1145 Logger.LogTrace("Graceful termination requested");
1146 }
1147 else
1148 Logger.LogTrace("Could not gracefully terminate as there is no active controller!");
1149 }
1150
1157 {
1158 Logger.LogTrace("Sending health check to active server...");
1160 if (activeServer == null)
1161 return MonitorAction.Restart; // uhhhh???
1162
1164
1165 var shouldShutdown = activeServer.RebootState == Session.RebootState.Shutdown;
1166 if (response == null)
1167 {
1168 switch (++healthChecksMissed)
1169 {
1170 case 1:
1171 Logger.LogDebug("DEFCON 4: Game server missed first health check!");
1172 break;
1173 case 2:
1174 const string message2 = "DEFCON 3: Game server has missed 2 health checks!";
1175 Logger.LogInformation(message2);
1177 break;
1178 case 3:
1180 ? "shutdown"
1181 : "be restarted";
1182 const string logTemplate1 = "DEFCON 2: Game server has missed 3 health checks! If it does not respond to the next one, the watchdog will {actionToTake}!";
1183 Logger.LogWarning(logTemplate1, actionToTake);
1185 logTemplate1.Replace(
1186 "{actionToTake}",
1188 StringComparison.Ordinal));
1189 break;
1190 case 4:
1192 ? "Shutting down due to graceful termination request"
1193 : "Restarting";
1194 const string logTemplate2 = "DEFCON 1: Four health checks have been missed! {actionTaken}...";
1195 Logger.LogWarning(logTemplate2, actionTaken);
1197 logTemplate2.Replace(
1198 "{actionTaken}",
1200 StringComparison.Ordinal));
1201
1203 {
1204 Logger.LogDebug("DumpOnHealthCheckRestart enabled.");
1205 try
1206 {
1208 }
1209 catch (JobException ex)
1210 {
1211 Logger.LogWarning(ex, "Creating dump failed!");
1212 }
1213 catch (Win32Exception ex)
1214 {
1215 Logger.LogWarning(ex, "Creating dump failed!");
1216 }
1217 }
1218 else
1219 Logger.LogTrace("DumpOnHealthCheckRestart disabled.");
1220
1223 default:
1224 Logger.LogError("Invalid health checks missed count: {healthChecksMissed}", healthChecksMissed);
1225 break;
1226 }
1227 }
1228 else
1229 {
1231 ClientCount = response.ClientCount;
1232 }
1233
1234 return MonitorAction.Continue;
1235 }
1236
1242 {
1243 if (result?.ChatResponses != null)
1244 {
1246 foreach (var response in result.ChatResponses
1247 .Where(response =>
1248 {
1249 if (response.ChannelIds == null)
1250 {
1251 if (!warnedMissingChannelIds)
1252 {
1253 Logger.LogWarning("DMAPI response contains null channelIds!");
1254 warnedMissingChannelIds = true;
1255 }
1256
1257 return false;
1258 }
1259
1260 return true;
1261 }))
1263 response,
1264 response.ChannelIds!
1265 .Select(channelIdString =>
1266 {
1267 if (UInt64.TryParse(channelIdString, out var channelId))
1268 return (ulong?)channelId;
1269 else
1270 Logger.LogWarning("Could not parse chat response channel ID: {channelID}", channelIdString);
1271
1272 return null;
1273 })
1275 .Select(nullableChannelId => nullableChannelId!.Value));
1276 }
1277 }
1278
1285 {
1286 const string DumpDirectory = "ProcessDumps";
1287
1288 var session = GetActiveController();
1289 if (session?.Lifetime.IsCompleted != false)
1290 throw new JobException(ErrorCode.GameServerOffline);
1291
1292 var dumpFileExtension = session.DumpFileExtension;
1293
1294 var dumpFileNameTemplate = diagnosticsIOManager.ResolvePath(
1295 diagnosticsIOManager.ConcatPath(
1297 $"DreamDaemon-{DateTimeOffset.UtcNow.ToFileStamp()}"));
1298
1299 var dumpFileName = $"{dumpFileNameTemplate}{dumpFileExtension}";
1300 var iteration = 0;
1301 while (await diagnosticsIOManager.FileExists(dumpFileName, cancellationToken))
1302 dumpFileName = $"{dumpFileNameTemplate} ({++iteration}){dumpFileExtension}";
1303
1304 if (iteration == 0)
1305 await diagnosticsIOManager.CreateDirectory(DumpDirectory, cancellationToken);
1306
1307 if (session.Lifetime.IsCompleted)
1308 throw new JobException(ErrorCode.GameServerOffline);
1309
1310 Logger.LogInformation("Dumping session to {dumpFileName}...", dumpFileName);
1311 await session.CreateDump(dumpFileName, ActiveLaunchParameters.Minidumps!.Value, cancellationToken);
1312 }
1313 }
1314}
virtual ? long Id
The ID of the entity.
Definition EntityId.cs:14
Metadata about a server instance.
Definition Instance.cs:9
uint? HealthCheckSeconds
The number of seconds between each watchdog health check. 0 disables.
bool? DumpOnHealthCheckRestart
If a process core dump should be created prior to restarting the watchdog due to health check failure...
Extension methods for the ValueTask and ValueTask<TResult> classes.
static async ValueTask WhenAll(IEnumerable< ValueTask > tasks)
Fully await a given list of tasks .
Represents a tgs_chat_user datum.
Definition ChatUser.cs:12
bool DmbAvailable
If LockNextDmb will succeed.
Definition DmbFactory.cs:41
Task OnNewerDmb
Get a Task that completes when the result of a call to LockNextDmb will be different than the previou...
Definition DmbFactory.cs:31
async ValueTask< CompileJob?> LatestCompileJob()
Gets the latest CompileJob.A ValueTask<TResult> resulting in the latest CompileJob or null if none ar...
const string DifferentCoreExceptionMessage
Message for the InvalidOperationException if ever a job starts on a different IInstanceCore than the ...
Definition Instance.cs:36
Represents a message to send to a chat provider.
Represents a chat command to be handled by DD.
Data structure for TopicCommandType.EventNotification requests.
static TopicParameters CreateBroadcastParameters(string broadcastMessage)
Initializes a new instance of the TopicParameters class.
Parameters necessary for duplicating a ISessionController session.
async ValueTask< ReattachInformation?> Load(CancellationToken cancellationToken)
Load a saved ReattachInformation.A ValueTask<TResult> resulting in the stored ReattachInformation if ...
ValueTask Clear(CancellationToken cancellationToken)
Clear any stored ReattachInformation.A ValueTask representing the running operation.
async ValueTask HandleRestart(Version? updateVersion, bool handlerMayDelayShutdownWithExtremelyLongRunningTasks, CancellationToken cancellationToken)
Handle a restart of the server.A ValueTask representing the running operation.
async ValueTask< bool > Broadcast(string message, CancellationToken cancellationToken)
Send a broadcast message to the DMAPI.A ValueTask<TResult> resulting in true if the broadcast succee...
async ValueTask< MonitorAction > HandleHealthCheck(CancellationToken cancellationToken)
Handles a watchdog health check.
ISessionController? GetActiveController()
Get the active ISessionController.
readonly Gauge cpuUsageMetric
Active session CPU usage as a metric.
readonly IJobManager jobManager
The IJobManager for the WatchdogBase.
ILogger< WatchdogBase > Logger
The ILogger for the WatchdogBase.
bool releaseServers
If the servers should be released instead of shutdown.
async ValueTask ReattachFailure(CancellationToken cancellationToken)
Call from InitController(ValueTask, ReattachInformation, CancellationToken) when a reattach operation...
async ValueTask CreateDumpNoLock(CancellationToken cancellationToken)
Attempt to create a process dump for the game server. Requires a lock on synchronizationSemaphore.
Models.? CompileJob ActiveCompileJob
Retrieves the Models.CompileJob currently running on the server.
DreamDaemonLaunchParameters? LastLaunchParameters
The DreamDaemonLaunchParameters the active server is using.This may not be the exact same as ActiveLa...
DateTimeOffset? LaunchTime
When the current server executions was started.
WatchdogBase(IChatManager chat, ISessionControllerFactory sessionControllerFactory, IDmbFactory dmbFactory, ISessionPersistor sessionPersistor, IJobManager jobManager, IServerControl serverControl, IAsyncDelayer asyncDelayer, IIOManager diagnosticsIOManager, IEventConsumer eventConsumer, IRemoteDeploymentManagerFactory remoteDeploymentManagerFactory, IMetricFactory metricFactory, IIOManager gameIOManager, ILogger< WatchdogBase > logger, DreamDaemonLaunchParameters initialLaunchParameters, Api.Models.Instance metadata, bool autoStart)
Initializes a new instance of the WatchdogBase class.
readonly bool autoStart
If the WatchdogBase should LaunchNoLock(bool, bool, bool, ReattachInformation, CancellationToken) in ...
async ValueTask Terminate(bool graceful, CancellationToken cancellationToken)
Stops the watchdog.A ValueTask representing the running operation.
async ValueTask CheckLaunchResult(ISessionController controller, string serverName, CancellationToken cancellationToken)
Check the LaunchResult of a given controller for errors and throw a JobException if any are detected...
readonly Api.Models.Instance metadata
The Api.Models.Instance for the WatchdogBase.
bool AlphaIsActive
If the alpha server is the active server.
readonly IEventConsumer eventConsumer
The IEventConsumer that is not the WatchdogBase.
bool disposed
If the WatchdogBase has been DisposeAsync'd.
long? WorldIteration
A incrementing ID for representing current iteration of servers world (i.e. after calling /world/proc...
readonly SemaphoreSlim controllerDisposeSemaphore
SemaphoreSlim used for DisposeAndNullControllers.
WatchdogStatus Status
The current WatchdogStatus.
ValueTask< MonitorAction > HandleMonitorWakeup(MonitorActivationReason activationReason, CancellationToken cancellationToken)
Handles the actions to take when the monitor has to "wake up".
CancellationTokenSource? monitorCts
The CancellationTokenSource for the monitor loop.
Task? monitorTask
The Task running the monitor loop.
void RunMetricsScrape()
Callback to update transient metrics.
readonly IRemoteDeploymentManagerFactory remoteDeploymentManagerFactory
The IRemoteDeploymentManagerFactory for the WatchdogBase.
readonly SemaphoreSlim synchronizationSemaphore
The SemaphoreSlim for the WatchdogBase.
async ValueTask Restart(bool graceful, CancellationToken cancellationToken)
Restarts the watchdog.A ValueTask representing the running operation.
long? MemoryUsage
Gets the memory usage of the game server in bytes.
readonly IIOManager diagnosticsIOManager
The IIOManager pointing to the Diagnostics directory.
async ValueTask< MessageContent > HandleChatCommand(string commandName, string arguments, ChatUser sender, CancellationToken cancellationToken)
Handle a chat command.A ValueTask<TResult> resulting in the MessageContent text to send back.
ValueTask InitController(ValueTask eventTask, ReattachInformation? reattachInfo, CancellationToken cancellationToken)
Starts all ISessionControllers.
async ValueTask CreateDump(CancellationToken cancellationToken)
Attempt to create a process dump for DreamDaemon.A ValueTask representing the running operation.
IIOManager GameIOManager
The IIOManager for the WatchdogBase pointing to the Game directory.
DreamDaemonLaunchParameters ActiveLaunchParameters
The DreamDaemonLaunchParameters to be applied.
uint? ClientCount
Last known client count queried from the DMAPI. Requires health checks to be enabled to populate.
async ValueTask BeforeApplyDmb(Models.CompileJob newCompileJob, CancellationToken cancellationToken)
To be called before a given newCompileJob goes live.
async ValueTask< bool > StopMonitor()
Stops MonitorLifetimes(CancellationToken). Doesn't kill the servers.
async ValueTask MonitorRestart(CancellationToken cancellationToken)
Attempt to restart the monitor from scratch.
async ValueTask Launch(CancellationToken cancellationToken)
Start the IWatchdog.A ValueTask representing the running operation.
async Task InitialCheckDmbUpdated(CompileJob currentCompileJob)
Check for a new IDmbProvider.
async ValueTask LaunchNoLock(bool startMonitor, bool announce, bool announceFailure, ReattachInformation? reattachInfo, CancellationToken cancellationToken)
Launches the watchdog.
async ValueTask DisposeAndNullControllers(CancellationToken cancellationToken)
Wrapper for DisposeAndNullControllersImpl under a locked context.
ValueTask DisposeAndNullControllersImpl()
Call IDisposable.Dispose and null the fields for all ISessionControllers.
async ValueTask TerminateNoLock(bool graceful, bool announce, CancellationToken cancellationToken)
Implementation of Terminate(bool, CancellationToken). Does not lock synchronizationSemaphore.
async ValueTask< bool > ChangeSettings(DreamDaemonLaunchParameters launchParameters, CancellationToken cancellationToken)
Changes the ActiveLaunchParameters. If currently running, may trigger a graceful restart....
void HandleChatResponses(TopicResponse? result)
Handle any TopicResponse.ChatResponses in a given topic result .
virtual async ValueTask ResetRebootState(CancellationToken cancellationToken)
Cancels pending graceful actions.A ValueTask representing the running operation.
async Task MonitorLifetimes(CancellationToken cancellationToken)
The main loop of the watchdog. Ayschronously waits for events to occur and then responds to them.
long? SessionId
An incrementing ID for representing current server execution.
async Task StartAsync(CancellationToken cancellationToken)
ValueTask InstanceRenamed(string newInstanceName, CancellationToken cancellationToken)
Called when the owning Instance is renamed.A ValueTask representing the running operation.
readonly Gauge ramUsageMetric
MemoryUsage as a metric.
int healthChecksMissed
The number of hearbeats missed.
readonly Gauge watchdogStatusMetric
The Status as a metric.
IChatManager Chat
The IChatManager for the WatchdogBase.
async Task StopAsync(CancellationToken cancellationToken)
readonly IRestartRegistration restartRegistration
The IRestartRegistration for the WatchdogBase.
async ValueTask HandleEventImpl(EventType eventType, IEnumerable< string > parameters, bool relayToSession, CancellationToken cancellationToken)
Handle a given eventType without re-throwing errors.
WatchdogStatus status
Backing field for Status.
volatile TaskCompletionSource activeParametersUpdated
TaskCompletionSource that completes when ActiveLaunchParameters are changed and we are running.
Operation exceptions thrown from the context of a Models.Job.
async ValueTask Delay(TimeSpan timeSpan, CancellationToken cancellationToken)
Create a Task that completes after a given timeSpan .A ValueTask representing the running operation.
static async ValueTask< SemaphoreSlimContext > Lock(SemaphoreSlim semaphore, CancellationToken cancellationToken, ILogger? logger=null)
Asyncronously locks a semaphore .
Helpers for manipulating the Serilog.Context.LogContext.
const string WatchdogMonitorIterationContextProperty
The Serilog.Context.LogContext property name for the ID of the watchdog monitor iteration currently b...
For managing connected chat services.
void QueueWatchdogMessage(string message)
Queue a chat message to configured watchdog channels.
void RegisterCommandHandler(ICustomCommandHandler customCommandHandler)
Registers a customCommandHandler to use.
ValueTask UpdateTrackingContexts(CancellationToken cancellationToken)
Force an update with the active channels on all active IChatTrackingContexts.
void QueueMessage(MessageContent message, IEnumerable< ulong > channelIds)
Queue a chat message to a given set of channelIds .
Handles Commands.ICommands that map to those defined in a IChatTrackingContext.
IRemoteDeploymentManager CreateRemoteDeploymentManager(Api.Models.Instance metadata, RemoteGitProvider remoteGitProvider)
Creates a IRemoteDeploymentManager for a given remoteGitProvider .
Consumes EventTypes and takes the appropriate actions.
ValueTask? HandleCustomEvent(string eventName, IEnumerable< string?> parameters, CancellationToken cancellationToken)
Handles a given custom event.
ValueTask HandleEvent(EventType eventType, IEnumerable< string?> parameters, bool deploymentPipeline, CancellationToken cancellationToken)
Handle a given eventType .
Handles communication with a DreamDaemon IProcess.
Models.CompileJob CompileJob
Gets the CompileJob associated with the ISessionController.
ReattachInformation ReattachInformation
Gets the Session.ReattachInformation associated with the ISessionController.
EngineVersion EngineVersion
Gets the Api.Models.EngineVersion associated with the ISessionController.
long? StartupBridgeRequestsReceived
The number of times a startup bridge request has been received. null if DMApiAvailable is false.
Handles saving and loading ReattachInformation.
Runs and monitors the twin server controllers.
Definition IWatchdog.cs:16
Represents the lifetime of a IRestartHandler registration.
Represents a service that may take an updated Host assembly and run it, stopping the current assembly...
IRestartRegistration RegisterForRestart(IRestartHandler handler)
Register a given handler to run before stopping the server for a restart.
Interface for using filesystems.
Definition IIOManager.cs:14
Manages the runtime of Jobs.
ValueTask RegisterOperation(Job job, JobEntrypoint operation, CancellationToken cancellationToken)
Registers a given Job and begins running it.
long? MemoryUsage
Gets the process' memory usage in bytes.
DateTimeOffset? LaunchTime
When the process was started.
ErrorCode
Types of Response.ErrorMessageResponses that the API may return.
Definition ErrorCode.cs:12
JobCode
The different types of Response.JobResponse.
Definition JobCode.cs:9
WatchdogStatus
The current status of the watchdog.
@ List
User may list files if the Models.Instance allows it.
DreamDaemonRights
Rights for managing DreamDaemon.
EventType
Types of events. Mirror in tgs.dm. Prefer last listed name for script.
Definition EventType.cs:7
RebootState
Represents the action to take when /world/Reboot() is called.
Definition RebootState.cs:7
MonitorAction
The action for the monitor loop to take when control is returned to it.
MonitorActivationReason
Reasons for the monitor to wake up.