tgstation-server  4.4.0
The /tg/station 13 server suite
WatchdogBase.cs
Go to the documentation of this file.
1 using Microsoft.EntityFrameworkCore;
2 using Microsoft.Extensions.Logging;
3 using Serilog.Context;
4 using System;
5 using System.Collections.Generic;
6 using System.Linq;
7 using System.Threading;
8 using System.Threading.Tasks;
20 using Tgstation.Server.Host.IO;
22 
23 namespace Tgstation.Server.Host.Components.Watchdog
24 {
28  #pragma warning disable CA1506 // TODO: Decomplexify
30  {
32  public WatchdogStatus Status
33  {
34  get => status;
35  set
36  {
37  status = value;
38  Logger.LogTrace("Status set to {0}", status);
39  }
40  }
41 
43  public abstract bool AlphaIsActive { get; }
44 
46  public abstract Models.CompileJob ActiveCompileJob { get; }
47 
49  public DreamDaemonLaunchParameters ActiveLaunchParameters { get; protected set; }
50 
52  public DreamDaemonLaunchParameters LastLaunchParameters { get; protected set; }
53 
55  public abstract RebootState? RebootState { get; }
56 
60  protected TaskCompletionSource<object> ActiveParametersUpdated { get; set; }
61 
65  protected SemaphoreSlim Semaphore { get; }
66 
70  protected ILogger Logger { get; }
71 
75  protected IChatManager Chat { get; }
76 
81 
85  protected IDmbFactory DmbFactory { get; }
86 
90  protected IAsyncDelayer AsyncDelayer { get; }
91 
95  readonly Api.Models.Instance instance;
96 
101 
106 
111 
116 
121 
126 
130  readonly object controllerDisposeLock;
131 
135  readonly bool autoStart;
136 
141 
145  CancellationTokenSource monitorCts;
146 
151 
156 
161 
166 
170  bool disposed;
171 
189  protected WatchdogBase(
190  IChatManager chat,
191  ISessionControllerFactory sessionControllerFactory,
192  IDmbFactory dmbFactory,
193  ISessionPersistor sessionPersistor,
194  IDatabaseContextFactory databaseContextFactory,
195  IJobManager jobManager,
196  IServerControl serverControl,
197  IAsyncDelayer asyncDelayer,
198  IIOManager diagnosticsIOManager,
199  IEventConsumer eventConsumer,
200  ILogger logger,
201  DreamDaemonLaunchParameters initialLaunchParameters,
202  Api.Models.Instance instance,
203  bool autoStart)
204  {
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;
218 
219  if (serverControl == null)
220  throw new ArgumentNullException(nameof(serverControl));
221 
222  chat.RegisterCommandHandler(this);
223 
224  ActiveLaunchParameters = initialLaunchParameters;
225  releaseServers = false;
226  ActiveParametersUpdated = new TaskCompletionSource<object>();
227  controllerDisposeLock = new object();
228 
229  restartRegistration = serverControl.RegisterForRestart(this);
230  try
231  {
232  Semaphore = new SemaphoreSlim(1);
233  }
234  catch
235  {
236  restartRegistration.Dispose();
237  throw;
238  }
239 
240  Logger.LogTrace("Created watchdog");
241  }
242 
244  public void Dispose()
245  {
246  Logger.LogTrace("Disposing...");
247  Semaphore.Dispose();
248  restartRegistration.Dispose();
249  DisposeAndNullControllers();
250  monitorCts?.Dispose();
251  disposed = true;
252  }
253 
261  async Task TerminateNoLock(bool graceful, bool announce, CancellationToken cancellationToken)
262  {
263  if (Status == WatchdogStatus.Offline)
264  return;
265  if (!graceful)
266  {
267  var eventTask = eventConsumer.HandleEvent(releaseServers ? EventType.WatchdogDetach : EventType.WatchdogShutdown, null, cancellationToken);
268 
269  var chatTask = announce ? Chat.SendWatchdogMessage("Shutting down...", false, cancellationToken) : Task.CompletedTask;
270 
271  await eventTask.ConfigureAwait(false);
272 
273  await StopMonitor().ConfigureAwait(false);
274 
275  DisposeAndNullControllers();
276 
277  LastLaunchParameters = null;
278 
279  await chatTask.ConfigureAwait(false);
280  return;
281  }
282 
283  // merely set the reboot state
284  var toKill = GetActiveController();
285  if (toKill != null)
286  {
287  await toKill.SetRebootState(Session.RebootState.Shutdown, cancellationToken).ConfigureAwait(false);
288  Logger.LogTrace("Graceful termination requested");
289  }
290  else
291  Logger.LogTrace("Could not gracefully terminate as there is no active controller!");
292  }
293 
299  async Task<MonitorAction> HandleHeartbeat(CancellationToken cancellationToken)
300  {
301  Logger.LogTrace("Sending heartbeat to active server...");
302  var activeServer = GetActiveController();
303  var response = await activeServer.SendCommand(new TopicParameters(), cancellationToken).ConfigureAwait(false);
304 
305  var shouldShutdown = activeServer.RebootState == Session.RebootState.Shutdown;
306  if (response == null)
307  {
308  switch (++heartbeatsMissed)
309  {
310  case 1:
311  Logger.LogDebug("DEFCON 4: DreamDaemon missed first heartbeat!");
312  break;
313  case 2:
314  var message2 = "DEFCON 3: DreamDaemon has missed 2 heartbeats!";
315  Logger.LogInformation(message2);
316  await Chat.SendWatchdogMessage(message2, true, cancellationToken).ConfigureAwait(false);
317  break;
318  case 3:
319  var actionToTake = shouldShutdown
320  ? "shutdown"
321  : "be restarted";
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);
325  break;
326  case 4:
327  var actionTaken = shouldShutdown
328  ? "Shutting down due to graceful termination request"
329  : "Restarting";
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();
334  return shouldShutdown ? MonitorAction.Exit : MonitorAction.Restart;
335  default:
336  Logger.LogError("Invalid heartbeats missed count: {0}", heartbeatsMissed);
337  break;
338  }
339  }
340  else
341  heartbeatsMissed = 0;
342 
343  return MonitorAction.Continue;
344  }
345 
355  protected async Task LaunchNoLock(
356  bool startMonitor,
357  bool announce,
358  bool announceFailure,
359  ReattachInformation reattachInfo,
360  CancellationToken cancellationToken)
361  {
362  Logger.LogTrace("Begin LaunchImplNoLock");
363 
364  if (reattachInfo == null && !DmbFactory.DmbAvailable)
365  throw new JobException(ErrorCode.WatchdogCompileJobCorrupted);
366 
367  // 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
368  Task announceTask;
369  if (announce)
370  {
371  announceTask = Chat.SendWatchdogMessage(reattachInfo == null ? "Launching..." : "Reattaching...", false, cancellationToken); // simple announce
372  if (reattachInfo == null)
373  announceTask = Task.WhenAll(
374  eventConsumer.HandleEvent(EventType.WatchdogLaunch, Enumerable.Empty<string>(), cancellationToken),
375  announceTask);
376  }
377  else
378  announceTask = Task.CompletedTask; // no announce
379 
380  // since neither server is running, this is safe to do
381  LastLaunchParameters = ActiveLaunchParameters;
382  heartbeatsMissed = 0;
383 
384  try
385  {
386  await InitControllers(announceTask, reattachInfo, cancellationToken).ConfigureAwait(false);
387  }
388  catch (OperationCanceledException)
389  {
390  Logger.LogTrace("Controller initialization canceled!");
391  throw;
392  }
393  catch (Exception e)
394  {
395  // don't try to send chat tasks or warning logs if were suppressing exceptions or cancelled
396  var originalChatTask = announceTask;
397  async Task ChainChatTaskWithErrorMessage()
398  {
399  await originalChatTask.ConfigureAwait(false);
400  if (announceFailure)
401  await Chat.SendWatchdogMessage("Startup failed!", false, cancellationToken).ConfigureAwait(false);
402  }
403 
404  announceTask = ChainChatTaskWithErrorMessage();
405  Logger.LogWarning("Failed to start watchdog: {0}", e.ToString());
406  throw;
407  }
408  finally
409  {
410  // finish the chat task that's in flight
411  try
412  {
413  await announceTask.ConfigureAwait(false);
414  }
415  catch (OperationCanceledException)
416  {
417  Logger.LogTrace("Announcement task canceled!");
418  }
419  }
420 
421  Logger.LogInformation("Controller(s) initialized successfully");
422 
423  if (startMonitor)
424  {
425  monitorCts = new CancellationTokenSource();
426  monitorTask = MonitorLifetimes(monitorCts.Token);
427  }
428  }
429 
434  protected async Task<bool> StopMonitor()
435  {
436  Logger.LogTrace("StopMonitor");
437  if (monitorTask == null)
438  return false;
439  var wasRunning = !monitorTask.IsCompleted;
440  monitorCts.Cancel();
441  await monitorTask.ConfigureAwait(false);
442  Logger.LogTrace("Stopped Monitor");
443  monitorCts.Dispose();
444  monitorTask = null;
445  monitorCts = null;
446  return wasRunning;
447  }
448 
456  protected async Task CheckLaunchResult(ISessionController controller, string serverName, CancellationToken cancellationToken)
457  {
458  var launchResult = await controller.LaunchResult.WithToken(cancellationToken).ConfigureAwait(false);
459 
460  // Dead sessions won't trigger this
461  if (launchResult.ExitCode.HasValue) // you killed us ray...
462  throw new JobException(
463  ErrorCode.WatchdogStartupFailed,
464  new JobException($"{serverName} failed to start: {launchResult}"));
465  if (!launchResult.StartupTime.HasValue)
466  throw new JobException(
467  ErrorCode.WatchdogStartupTimeout,
468  new JobException($"{serverName} timed out on startup: {ActiveLaunchParameters.StartupTimeout.Value}s"));
469  }
470 
477  protected async Task ReattachFailure(Task chatTask, CancellationToken cancellationToken)
478  {
479  // we lost the server, just restart entirely
480  DisposeAndNullControllers();
481  const string FailReattachMessage = "Unable to properly reattach to server! Restarting watchdog...";
482  Logger.LogWarning(FailReattachMessage);
483 
484  async Task ChainChatTask()
485  {
486  await chatTask.ConfigureAwait(false);
487  await Chat.SendWatchdogMessage(FailReattachMessage, false, cancellationToken).ConfigureAwait(false);
488  }
489 
490  await InitControllers(ChainChatTask(), null, cancellationToken).ConfigureAwait(false);
491  }
492 
496  protected abstract void DisposeAndNullControllersImpl();
497 
501  protected void DisposeAndNullControllers()
502  {
503  Logger.LogTrace("DisposeAndNullControllers");
504  lock (controllerDisposeLock)
505  DisposeAndNullControllersImpl();
506  }
507 
512  protected abstract ISessionController GetActiveController();
513 
520  protected abstract Task<MonitorAction> HandleMonitorWakeup(
521  MonitorActivationReason activationReason,
522  CancellationToken cancellationToken);
523 
529  private async Task MonitorRestart(CancellationToken cancellationToken)
530  {
531  Logger.LogTrace("Monitor restart!");
532  DisposeAndNullControllers();
533 
534  var chatTask = Task.CompletedTask;
535  for (var retryAttempts = 1; ; ++retryAttempts)
536  {
537  Status = WatchdogStatus.Restoring;
538  Exception launchException;
539  using (await SemaphoreSlimContext.Lock(Semaphore, cancellationToken).ConfigureAwait(false))
540  try
541  {
542  // use LaunchImplNoLock without announcements or restarting the monitor
543  await LaunchNoLock(false, false, false, null, cancellationToken).ConfigureAwait(false);
544  Status = WatchdogStatus.Online;
545  Logger.LogDebug("Relaunch successful, resuming monitor...");
546  return;
547  }
548  catch (OperationCanceledException)
549  {
550  throw;
551  }
552  catch (Exception e)
553  {
554  launchException = e;
555  }
556  finally
557  {
558  await chatTask.ConfigureAwait(false);
559  }
560 
561  Logger.LogWarning("Failed to automatically restart the watchdog! Attempt: {0}, Exception: {1}", retryAttempts, launchException);
562  Status = WatchdogStatus.DelayedRestart;
563 
564  var retryDelay = Math.Min(
565  Convert.ToInt32(
566  Math.Pow(2, retryAttempts)),
567  TimeSpan.FromHours(1).TotalSeconds); // max of one hour, increasing by a power of 2 each time
568 
569  chatTask = Chat.SendWatchdogMessage(
570  $"Failed to restart (Attempt: {retryAttempts}), retrying in {retryDelay}s...",
571  false,
572  cancellationToken);
573 
574  await Task.WhenAll(
576  TimeSpan.FromSeconds(retryDelay),
577  cancellationToken),
578  chatTask)
579  .ConfigureAwait(false);
580  }
581  }
582 
588  private async Task MonitorLifetimes(CancellationToken cancellationToken)
589  {
590  Logger.LogTrace("Entered MonitorLifetimes");
591  Status = WatchdogStatus.Online;
592  using var _ = cancellationToken.Register(() => Logger.LogTrace("Monitor cancellationToken triggered"));
593 
594  // this function is responsible for calling HandlerMonitorWakeup when necessary and manitaining the MonitorState
595  try
596  {
597  MonitorAction nextAction = MonitorAction.Continue;
598  for (ulong iteration = 1; nextAction != MonitorAction.Exit; ++iteration)
599  using (LogContext.PushProperty("Monitor", iteration))
600  try
601  {
602  Logger.LogTrace("Iteration {0} of monitor loop", iteration);
603  nextAction = MonitorAction.Continue;
604 
605  var controller = GetActiveController();
606  Task activeServerLifetime = controller.Lifetime;
607  var activeServerReboot = controller.OnReboot;
608 
609  Task activeLaunchParametersChanged = ActiveParametersUpdated.Task;
610  var newDmbAvailable = DmbFactory.OnNewerDmb;
611 
612  var heartbeatSeconds = ActiveLaunchParameters.HeartbeatSeconds.Value;
613  var heartbeat = heartbeatSeconds == 0
614  || !controller.DMApiAvailable
615  ? Extensions.TaskExtensions.InfiniteTask()
616  : Task.Delay(TimeSpan.FromSeconds(heartbeatSeconds));
617 
618  // cancel waiting if requested
619  var cancelTcs = new TaskCompletionSource<object>();
620  var toWaitOn = Task.WhenAny(
621  activeServerLifetime,
622  activeServerReboot,
623  heartbeat,
624  newDmbAvailable,
625  cancelTcs.Task,
626  activeLaunchParametersChanged);
627 
628  // wait for something to happen
629  using (cancellationToken.Register(() => cancelTcs.SetCanceled()))
630  await toWaitOn.ConfigureAwait(false);
631 
632  cancellationToken.ThrowIfCancellationRequested();
633  Logger.LogTrace("Monitor activated");
634 
635  // always run HandleMonitorWakeup from the context of the semaphore lock
636  using (await SemaphoreSlimContext.Lock(Semaphore, cancellationToken).ConfigureAwait(false))
637  {
638  // Set this sooner so chat sends don't hold us up
639  if (activeServerLifetime.IsCompleted)
640  Status = WatchdogStatus.Restoring;
641 
642  // multiple things may have happened, handle them one at a time
643  for (var moreActivationsToProcess = true; moreActivationsToProcess && (nextAction == MonitorAction.Continue || nextAction == MonitorAction.Skip);)
644  {
645  MonitorActivationReason activationReason = default; // this will always be assigned before being used
646 
647  bool CheckActivationReason(ref Task task, MonitorActivationReason testActivationReason)
648  {
649  var taskCompleted = task?.IsCompleted == true;
650  task = null;
651  if (nextAction == MonitorAction.Skip)
652  nextAction = MonitorAction.Continue;
653  else if (taskCompleted)
654  {
655  activationReason = testActivationReason;
656  return true;
657  }
658 
659  return false;
660  }
661 
662  // process the tasks in this order and call HandlerMonitorWakup for each depending on the new monitorState
663  var anyActivation = CheckActivationReason(ref activeServerLifetime, MonitorActivationReason.ActiveServerCrashed)
664  || CheckActivationReason(ref activeServerReboot, MonitorActivationReason.ActiveServerRebooted)
665  || CheckActivationReason(ref newDmbAvailable, MonitorActivationReason.NewDmbAvailable)
666  || CheckActivationReason(ref activeLaunchParametersChanged, MonitorActivationReason.ActiveLaunchParametersUpdated)
667  || CheckActivationReason(ref heartbeat, MonitorActivationReason.Heartbeat);
668 
669  if (!anyActivation)
670  moreActivationsToProcess = false;
671  else
672  {
673  Logger.LogTrace("Reason: {0}", activationReason);
674  if (activationReason == MonitorActivationReason.Heartbeat)
675  nextAction = await HandleHeartbeat(
676  cancellationToken)
677  .ConfigureAwait(false);
678  else
679  nextAction = await HandleMonitorWakeup(
680  activationReason,
681  cancellationToken)
682  .ConfigureAwait(false);
683  }
684  }
685  }
686 
687  Logger.LogTrace("Next monitor action is to {0}", nextAction);
688 
689  // Restart if requested
690  if (nextAction == MonitorAction.Restart)
691  {
692  await MonitorRestart(cancellationToken).ConfigureAwait(false);
693  nextAction = MonitorAction.Continue;
694  }
695  }
696  catch (OperationCanceledException)
697  {
698  // let this bubble, other exceptions caught below
699  throw;
700  }
701  catch (Exception e)
702  {
703  // really, this should NEVER happen
704  Logger.LogError(
705  "Monitor crashed! Iteration: {0}, Exception: {1}",
706  iteration,
707  e);
708 
709  var nextActionMessage = nextAction != MonitorAction.Exit
710  ? "Recovering"
711  : "Shutting down";
712  var chatTask = Chat.SendWatchdogMessage(
713  $"Monitor crashed, this should NEVER happen! Please report this, full details in logs! {nextActionMessage}. Error: {e.Message}",
714  false,
715  cancellationToken);
716 
717  if (disposed)
718  nextAction = MonitorAction.Exit;
719  else if (nextAction != MonitorAction.Exit)
720  {
721  if (GetActiveController()?.Lifetime.IsCompleted != true)
722  await MonitorRestart(cancellationToken).ConfigureAwait(false);
723  else
724  Logger.LogDebug("Server seems to be okay, not restarting");
725  nextAction = MonitorAction.Continue;
726  }
727 
728  await chatTask.ConfigureAwait(false);
729  }
730  }
731  catch (OperationCanceledException)
732  {
733  // stop signal
734  Logger.LogDebug("Monitor cancelled");
735 
736  if (releaseServers)
737  {
738  Logger.LogTrace("Detaching servers...");
739  releasedReattachInformation = GetActiveController().Release();
740  }
741  }
742 
743  DisposeAndNullControllers();
744  Status = WatchdogStatus.Offline;
745 
746  Logger.LogTrace("Monitor exiting...");
747  }
748 
756  protected abstract Task InitControllers(Task chatTask, ReattachInformation reattachInfo, CancellationToken cancellationToken);
757 
759  public async Task ChangeSettings(DreamDaemonLaunchParameters launchParameters, CancellationToken cancellationToken)
760  {
761  using (await SemaphoreSlimContext.Lock(Semaphore, cancellationToken).ConfigureAwait(false))
762  {
763  bool match = launchParameters.CanApplyWithoutReboot(ActiveLaunchParameters);
764  ActiveLaunchParameters = launchParameters;
765  if (match || Status == WatchdogStatus.Offline)
766  return;
767 
768  ActiveParametersUpdated.TrySetResult(null); // queue an update
769  ActiveParametersUpdated = new TaskCompletionSource<object>();
770  }
771  }
772 
774  async Task IEventConsumer.HandleEvent(EventType eventType, IEnumerable<string> parameters, CancellationToken cancellationToken)
775  {
776  // Method explicitly implemented to prevent accidental calls when this.eventConsumer should be used.
777  var activeServer = GetActiveController();
778 
779  // Server may have ended
780  if (activeServer == null)
781  return;
782 
783  var notification = new EventNotification(eventType, parameters);
784  var result = await activeServer.SendCommand(
785  new TopicParameters(notification),
786  cancellationToken)
787  .ConfigureAwait(false);
788 
789  if (result?.InteropResponse?.ChatResponses != null)
790  await Task.WhenAll(
791  result.InteropResponse.ChatResponses.Select(
792  x => Chat.SendMessage(
793  x.Text,
794  x.ChannelIds
795  .Select(channelIdString =>
796  {
797  if (UInt64.TryParse(channelIdString, out var channelId))
798  return (ulong?)channelId;
799 
800  return null;
801  })
802  .Where(nullableChannelId => nullableChannelId.HasValue)
803  .Select(nullableChannelId => nullableChannelId.Value),
804  cancellationToken)))
805  .ConfigureAwait(false);
806  }
807 
809  public async Task<string> HandleChatCommand(string commandName, string arguments, ChatUser sender, CancellationToken cancellationToken)
810  {
811  using (await SemaphoreSlimContext.Lock(Semaphore, cancellationToken).ConfigureAwait(false))
812  {
813  if (Status == WatchdogStatus.Offline)
814  return "TGS: Server offline!";
815 
816  var commandObject = new ChatCommand(sender, commandName, arguments);
817 
818  var command = new TopicParameters(commandObject);
819 
820  var activeServer = GetActiveController();
821  var commandResult = await activeServer.SendCommand(command, cancellationToken).ConfigureAwait(false);
822 
823  if (commandResult == null)
824  return "TGS: Bad topic exchange!";
825 
826  if (commandResult.InteropResponse == null)
827  return "TGS: Bad topic response!";
828 
829  return commandResult.InteropResponse.CommandResponseMessage ??
830  "TGS: Command processed but no DMAPI response returned!";
831  }
832  }
833 
835  public async Task Launch(CancellationToken cancellationToken)
836  {
837  if (Status != WatchdogStatus.Offline)
838  throw new JobException(ErrorCode.WatchdogRunning);
839  using (await SemaphoreSlimContext.Lock(Semaphore, cancellationToken).ConfigureAwait(false))
840  await LaunchNoLock(true, true, true, null, cancellationToken).ConfigureAwait(false);
841  }
842 
844  public virtual async Task ResetRebootState(CancellationToken cancellationToken)
845  {
846  using (await SemaphoreSlimContext.Lock(Semaphore, cancellationToken).ConfigureAwait(false))
847  {
848  if (Status == WatchdogStatus.Offline)
849  return;
850  var toClear = GetActiveController();
851  if (toClear != null)
852  await toClear.SetRebootState(Session.RebootState.Normal, cancellationToken).ConfigureAwait(false);
853  }
854  }
855 
857  public async Task Restart(bool graceful, CancellationToken cancellationToken)
858  {
859  if (Status == WatchdogStatus.Offline)
860  throw new JobException(ErrorCode.WatchdogNotRunning);
861 
862  Logger.LogTrace("Begin Restart. Graceful: {0}", graceful);
863  using (await SemaphoreSlimContext.Lock(Semaphore, cancellationToken).ConfigureAwait(false))
864  {
865  if (!graceful)
866  {
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);
871  return;
872  }
873 
874  var toReboot = GetActiveController();
875  if (toReboot != null
876  && !await toReboot.SetRebootState(Session.RebootState.Restart, cancellationToken).ConfigureAwait(false))
877  Logger.LogWarning("Unable to send reboot state change event!");
878  }
879  }
880 
882  public async Task StartAsync(CancellationToken cancellationToken)
883  {
884  var reattachInfo = await sessionPersistor.Load(cancellationToken).ConfigureAwait(false);
885  if (!autoStart && reattachInfo == null)
886  return;
887 
888  long? adminUserId = null;
889 
890  await databaseContextFactory.UseContext(
891  async db => adminUserId = await db
892  .Users
893  .AsQueryable()
894  .Where(x => x.CanonicalName == Models.User.CanonicalizeName(Api.Models.User.AdminName))
895  .Select(x => x.Id)
896  .FirstAsync(cancellationToken)
897  .ConfigureAwait(false))
898  .ConfigureAwait(false);
899  var job = new Models.Job
900  {
901  StartedBy = new Models.User
902  {
903  Id = adminUserId.Value
904  },
905  Instance = new Models.Instance
906  {
907  Id = instance.Id
908  },
909  Description = $"Instance startup watchdog {(reattachInfo != null ? "reattach" : "launch")}",
910  CancelRight = (ulong)DreamDaemonRights.Shutdown,
911  CancelRightsType = RightsType.DreamDaemon
912  };
913  await jobManager.RegisterOperation(job, async (j, databaseContextFactory, progressFunction, ct) =>
914  {
915  using (await SemaphoreSlimContext.Lock(Semaphore, ct).ConfigureAwait(false))
916  await LaunchNoLock(true, true, true, reattachInfo, ct).ConfigureAwait(false);
917  }, cancellationToken).ConfigureAwait(false);
918  }
919 
921  public async Task StopAsync(CancellationToken cancellationToken)
922  {
923  await TerminateNoLock(false, !releaseServers, cancellationToken).ConfigureAwait(false);
924  if (releasedReattachInformation != null)
925  {
926  try
927  {
928  await sessionPersistor.Save(releasedReattachInformation, cancellationToken).ConfigureAwait(false);
929  }
930  catch (Exception ex)
931  {
932  Logger.LogCritical(
933  "Failed to persist session reattach information! To repair this, DreamDaemon will need to be manully stopped and then relaunched with TGS. Exception: {0}",
934  ex);
935  }
936 
937  releasedReattachInformation = null;
938  releaseServers = false;
939  }
940  }
941 
943  public async Task Terminate(bool graceful, CancellationToken cancellationToken)
944  {
945  using (await SemaphoreSlimContext.Lock(Semaphore, cancellationToken).ConfigureAwait(false))
946  await TerminateNoLock(graceful, !releaseServers, cancellationToken).ConfigureAwait(false);
947  }
948 
950  public async Task HandleRestart(Version updateVersion, CancellationToken cancellationToken)
951  {
952  releaseServers = true;
953  if (Status == WatchdogStatus.Online)
954  await Chat.SendWatchdogMessage("Detaching...", false, cancellationToken).ConfigureAwait(false);
955  else
956  Logger.LogTrace("Not sending detach chat message as status is: {0}", Status);
957  }
958 
960  public abstract Task InstanceRenamed(string newInstanceName, CancellationToken cancellationToken);
961 
963  public async Task CreateDump(CancellationToken cancellationToken)
964  {
965  const string DumpDirectory = "ProcessDumps";
966  await diagnosticsIOManager.CreateDirectory(DumpDirectory, cancellationToken).ConfigureAwait(false);
967 
968  var dumpFileName = diagnosticsIOManager.ResolvePath(
969  diagnosticsIOManager.ConcatPath(
970  DumpDirectory,
971  $"DreamDaemon-{DateTimeOffset.Now.ToFileStamp()}.dmp"));
972 
973  var session = GetActiveController();
974  if (session?.Lifetime.IsCompleted != false)
975  throw new JobException(ErrorCode.DreamDaemonOffline);
976 
977  Logger.LogInformation("Dumping session to {0}...", dumpFileName);
978  await session.CreateDump(dumpFileName, cancellationToken).ConfigureAwait(false);
979  }
980  }
981 }
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
ErrorCode
Types of ErrorMessages that the API may return.
Definition: ErrorCode.cs:10
RightsType
The type of rights a model uses
Definition: RightsType.cs:6
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
Definition: ChatCommand.cs:9
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
Definition: Instance.cs:104
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
Parameters necessary for duplicating a ISessionController session
EventType
Types of events. Mirror in tgs.dm
Definition: EventType.cs:6
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
Definition: IChatManager.cs:13
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
Definition: RebootState.cs:6
CancellationTokenSource monitorCts
The CancellationTokenSource for the monitor loop
Operation exceptions thrown from the context of a Models.Job
Definition: JobException.cs:9
virtual async Task ResetRebootState(CancellationToken cancellationToken)
Cancels pending graceful actions
void DisposeAndNullControllers()
Wrapper for DisposeAndNullControllersImpl under a locked context.
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.
Consumes EventTypes and takes the appropriate actions
Represents a tgs_chat_user datum
Definition: ChatUser.cs:10
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
Definition: IJobManager.cs:13
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
Definition: IIOManager.cs:11
readonly Api.Models.Instance instance
The Api.Models.Instance for the WatchdogBase.
Definition: WatchdogBase.cs:95
async Task< bool > StopMonitor()
Stops MonitorLifetimes(CancellationToken). Doesn&#39;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
Definition: MonitorAction.cs:6
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.
bool disposed
If the WatchdogBase has been Disposed.
Runs and monitors the twin server controllers
Definition: IWatchdog.cs:15
Represents a service that may take an updated Host assembly and run it, stopping the current assembly...