tgstation-server  4.3.2
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 Newtonsoft.Json;
4 using Serilog.Context;
5 using System;
6 using System.Collections.Generic;
7 using System.Linq;
8 using System.Threading;
9 using System.Threading.Tasks;
22 
23 namespace Tgstation.Server.Host.Components.Watchdog
24 {
28  #pragma warning disable CA1506 // TODO: Decomplexify
30  {
32  public bool Running
33  {
34  get => running;
35  set
36  {
37  running = value;
38  Logger.LogTrace("Running set to {0}", running);
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 
120  readonly object controllerDisposeLock;
121 
125  readonly bool autoStart;
126 
131 
135  CancellationTokenSource monitorCts;
136 
141 
146 
151 
155  bool running;
156 
160  bool disposed;
161 
177  protected WatchdogBase(
178  IChatManager chat,
179  ISessionControllerFactory sessionControllerFactory,
180  IDmbFactory dmbFactory,
181  IReattachInfoHandler reattachInfoHandler,
182  IDatabaseContextFactory databaseContextFactory,
183  IJobManager jobManager,
184  IServerControl serverControl,
185  IAsyncDelayer asyncDelayer,
186  ILogger logger,
187  DreamDaemonLaunchParameters initialLaunchParameters,
188  Api.Models.Instance instance,
189  bool autoStart)
190  {
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;
202 
203  if (serverControl == null)
204  throw new ArgumentNullException(nameof(serverControl));
205 
206  chat.RegisterCommandHandler(this);
207 
208  ActiveLaunchParameters = initialLaunchParameters;
209  releaseServers = false;
210  ActiveParametersUpdated = new TaskCompletionSource<object>();
211  controllerDisposeLock = new object();
212 
213  restartRegistration = serverControl.RegisterForRestart(this);
214  try
215  {
216  Semaphore = new SemaphoreSlim(1);
217  }
218  catch
219  {
220  restartRegistration.Dispose();
221  throw;
222  }
223  }
224 
226  public void Dispose()
227  {
228  Logger.LogTrace("Disposing...");
229  Semaphore.Dispose();
230  restartRegistration.Dispose();
231  DisposeAndNullControllers();
232  monitorCts?.Dispose();
233  disposed = true;
234  }
235 
243  async Task TerminateNoLock(bool graceful, bool announce, CancellationToken cancellationToken)
244  {
245  if (!Running)
246  return;
247  if (!graceful)
248  {
249  var eventTask = HandleEvent(releaseServers ? EventType.WatchdogDetach : EventType.WatchdogShutdown, null, cancellationToken);
250 
251  var chatTask = announce ? Chat.SendWatchdogMessage("Shutting down...", false, cancellationToken) : Task.CompletedTask;
252 
253  await eventTask.ConfigureAwait(false);
254 
255  await StopMonitor().ConfigureAwait(false);
256 
257  DisposeAndNullControllers();
258 
259  LastLaunchParameters = null;
260 
261  await chatTask.ConfigureAwait(false);
262  return;
263  }
264 
265  // merely set the reboot state
266  var toKill = GetActiveController();
267  if (toKill != null)
268  {
269  await toKill.SetRebootState(Session.RebootState.Shutdown, cancellationToken).ConfigureAwait(false);
270  Logger.LogTrace("Graceful termination requested");
271  }
272  else
273  Logger.LogTrace("Could not gracefully terminate as there is no active controller!");
274  }
275 
282  async Task<MonitorAction> HandleHeartbeat(ISessionController activeServer, CancellationToken cancellationToken)
283  {
284  Logger.LogTrace("Sending heartbeat to active server...");
285  var response = await activeServer.SendCommand(new TopicParameters(), cancellationToken).ConfigureAwait(false);
286 
287  var shouldShutdown = activeServer.RebootState == Session.RebootState.Shutdown;
288  if (response == null)
289  {
290  switch (++heartbeatsMissed)
291  {
292  case 1:
293  Logger.LogDebug("DEFCON 4: DreamDaemon missed first heartbeat!");
294  break;
295  case 2:
296  var message2 = "DEFCON 3: DreamDaemon has missed 2 heartbeats!";
297  Logger.LogInformation(message2);
298  await Chat.SendWatchdogMessage(message2, true, cancellationToken).ConfigureAwait(false);
299  break;
300  case 3:
301  var actionToTake = shouldShutdown
302  ? "shutdown"
303  : "be restarted";
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);
307  break;
308  case 4:
309  var actionTaken = shouldShutdown
310  ? "Shutting down due to graceful termination request"
311  : "Restarting";
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);
316  return shouldShutdown ? MonitorAction.Exit : MonitorAction.Restart;
317  default:
318  Logger.LogError("Invalid heartbeats missed count: {0}", heartbeatsMissed);
319  break;
320  }
321  }
322  else
323  heartbeatsMissed = 0;
324 
325  return MonitorAction.Continue;
326  }
327 
336  protected async Task LaunchNoLock(bool startMonitor, bool announce, DualReattachInformation reattachInfo, CancellationToken cancellationToken)
337  {
338  Logger.LogTrace("Begin LaunchImplNoLock");
339 
340  if (Running)
341  throw new JobException(ErrorCode.WatchdogRunning);
342 
343  if (reattachInfo == null && !DmbFactory.DmbAvailable)
344  throw new JobException(ErrorCode.WatchdogCompileJobCorrupted);
345 
346  // 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
347  Task chatTask;
348  if (startMonitor && await StopMonitor().ConfigureAwait(false))
349  chatTask = Chat.SendWatchdogMessage("Automatic retry sequence cancelled by manual launch. Restarting...", false, cancellationToken);
350  else if (announce)
351  chatTask = Chat.SendWatchdogMessage(reattachInfo == null ? "Launching..." : "Reattaching...", false, cancellationToken); // simple announce
352  else
353  chatTask = Task.CompletedTask; // no announce
354 
355  // since neither server is running, this is safe to do
356  LastLaunchParameters = ActiveLaunchParameters;
357  heartbeatsMissed = 0;
358 
359  // for when we call ourself and want to not catch thrown exceptions
360  var recursiveCallToHappen = false;
361  try
362  {
363  await InitControllers(() => recursiveCallToHappen = true, chatTask, reattachInfo, cancellationToken).ConfigureAwait(false);
364  if (recursiveCallToHappen)
365  return;
366 
367  await chatTask.ConfigureAwait(false);
368 
369  Logger.LogInformation("Launched servers successfully");
370  Running = true;
371 
372  if (startMonitor)
373  {
374  monitorCts = new CancellationTokenSource();
375  monitorTask = MonitorLifetimes(monitorCts.Token);
376  }
377  }
378  catch (Exception e)
379  {
380  // don't try to send chat tasks or warning logs if were suppressing exceptions or cancelled
381  if (!recursiveCallToHappen && !cancellationToken.IsCancellationRequested)
382  {
383  var originalChatTask = chatTask;
384  async Task ChainChatTaskWithErrorMessage()
385  {
386  await originalChatTask.ConfigureAwait(false);
387  await Chat.SendWatchdogMessage("Startup failed!", false, cancellationToken).ConfigureAwait(false);
388  }
389 
390  chatTask = ChainChatTaskWithErrorMessage();
391  Logger.LogWarning("Failed to start watchdog: {0}", e.ToString());
392  }
393 
394  throw;
395  }
396  finally
397  {
398  // finish the chat task that's in flight
399  try
400  {
401  await chatTask.ConfigureAwait(false);
402  }
403  catch (OperationCanceledException) { }
404  }
405  }
406 
411  protected async Task<bool> StopMonitor()
412  {
413  Logger.LogTrace("StopMonitor");
414  if (monitorTask == null)
415  return false;
416  var wasRunning = !monitorTask.IsCompleted;
417  monitorCts.Cancel();
418  await monitorTask.ConfigureAwait(false);
419  Logger.LogTrace("Stopped Monitor");
420  monitorCts.Dispose();
421  monitorTask = null;
422  monitorCts = null;
423  return wasRunning;
424  }
425 
433  protected async Task CheckLaunchResult(ISessionController controller, string serverName, CancellationToken cancellationToken)
434  {
435  var launchResult = await controller.LaunchResult.WithToken(cancellationToken).ConfigureAwait(false);
436 
437  // Dead sessions won't trigger this
438  if (launchResult.ExitCode.HasValue) // you killed us ray...
439  throw new JobException(
440  ErrorCode.WatchdogStartupFailed,
441  new JobException($"{serverName} failed to start: {launchResult}"));
442  if (!launchResult.StartupTime.HasValue)
443  throw new JobException(
444  ErrorCode.WatchdogStartupTimeout,
445  new JobException($"{serverName} timed out on startup: {ActiveLaunchParameters.StartupTimeout.Value}s"));
446  }
447 
451  protected abstract void DisposeAndNullControllersImpl();
452 
456  protected void DisposeAndNullControllers()
457  {
458  Logger.LogTrace("DisposeAndNullControllers");
459  lock (controllerDisposeLock)
460  DisposeAndNullControllersImpl();
461  }
462 
467  protected abstract ISessionController GetActiveController();
468 
473  protected abstract DualReattachInformation CreateReattachInformation();
474 
481  protected abstract IReadOnlyDictionary<MonitorActivationReason, Task> GetMonitoredServerTasks(MonitorState monitorState);
482 
490  protected abstract Task HandleMonitorWakeup(
491  MonitorActivationReason activationReason,
492  MonitorState monitorState,
493  CancellationToken cancellationToken);
494 
500  private async Task<MonitorState> MonitorRestart(CancellationToken cancellationToken)
501  {
502  Logger.LogTrace("Monitor restart!");
503  DisposeAndNullControllers();
504 
505  var chatTask = Task.CompletedTask;
506  for (var retryAttempts = 1; ; ++retryAttempts)
507  {
508  Exception launchException = null;
509  using (await SemaphoreSlimContext.Lock(Semaphore, cancellationToken).ConfigureAwait(false))
510  try
511  {
512  // use LaunchImplNoLock without announcements or restarting the monitor
513  await LaunchNoLock(false, false, null, cancellationToken).ConfigureAwait(false);
514  if (Running)
515  {
516  Logger.LogDebug("Relaunch successful, resetting monitor state...");
517  return new MonitorState();
518  }
519  }
520  catch (OperationCanceledException)
521  {
522  throw;
523  }
524  catch (Exception e)
525  {
526  launchException = e;
527  }
528 
529  await chatTask.ConfigureAwait(false);
530  if (!Running)
531  {
532  if (launchException == null)
533  Logger.LogWarning("Failed to automatically restart the watchdog! Attempt: {0}", retryAttempts);
534  else
535  Logger.LogWarning("Failed to automatically restart the watchdog! Attempt: {0}, Exception: {1}", retryAttempts, launchException);
536  var retryDelay = Math.Min(
537  Convert.ToInt32(
538  Math.Pow(2, retryAttempts)),
539  TimeSpan.FromHours(1).Seconds); // max of one hour, increasing by a power of 2 each time
540  chatTask = Chat.SendWatchdogMessage(
541  $"Failed to restart (Attempt: {retryAttempts}), retrying in {retryDelay}",
542  false,
543  cancellationToken);
544  await Task.WhenAll(
546  TimeSpan.FromSeconds(retryDelay),
547  cancellationToken),
548  chatTask)
549  .ConfigureAwait(false);
550  }
551  }
552  }
553 
559  private async Task MonitorLifetimes(CancellationToken cancellationToken)
560  {
561  Logger.LogTrace("Entered MonitorLifetimes");
562  using var _ = cancellationToken.Register(() => Logger.LogTrace("Monitor cancellationToken triggered"));
563 
564  // this function is responsible for calling HandlerMonitorWakeup when necessary and manitaining the MonitorState
565  var iteration = 1;
566  try
567  {
568  for (var monitorState = new MonitorState(); monitorState.NextAction != MonitorAction.Exit; ++iteration)
569  using (LogContext.PushProperty("Monitor", iteration))
570  try
571  {
572  Logger.LogTrace("Iteration {0} of monitor loop", iteration);
573 
574  // load the activation tasks into local variables
575  var serverTasks = GetMonitoredServerTasks(monitorState);
576  if (serverTasks.Count != 5)
577  throw new InvalidOperationException("Expected 5 monitored server tasks!");
578 
579  var activeServerLifetime = serverTasks[MonitorActivationReason.ActiveServerCrashed];
580  var activeServerReboot = serverTasks[MonitorActivationReason.ActiveServerRebooted];
581  var inactiveServerLifetime = serverTasks[MonitorActivationReason.InactiveServerCrashed];
582  var inactiveServerReboot = serverTasks[MonitorActivationReason.InactiveServerRebooted];
583  var inactiveStartupComplete = serverTasks[MonitorActivationReason.InactiveServerStartupComplete];
584 
585  Task activeLaunchParametersChanged = ActiveParametersUpdated.Task;
586  var newDmbAvailable = DmbFactory.OnNewerDmb;
587 
588  var heartbeatSeconds = ActiveLaunchParameters.HeartbeatSeconds.Value;
589  var heartbeat = heartbeatSeconds == 0
590  ? Extensions.TaskExtensions.InfiniteTask()
591  : Task.Delay(TimeSpan.FromSeconds(heartbeatSeconds));
592 
593  // cancel waiting if requested
594  var cancelTcs = new TaskCompletionSource<object>();
595  var toWaitOn = Task.WhenAny(
596  activeServerLifetime,
597  activeServerReboot,
598  inactiveServerLifetime,
599  inactiveServerReboot,
600  inactiveStartupComplete,
601  heartbeat,
602  newDmbAvailable,
603  cancelTcs.Task,
604  activeLaunchParametersChanged);
605 
606  // wait for something to happen
607  using (cancellationToken.Register(() => cancelTcs.SetCanceled()))
608  await toWaitOn.ConfigureAwait(false);
609 
610  cancellationToken.ThrowIfCancellationRequested();
611  Logger.LogTrace("Monitor activated");
612 
613  // always run HandleMonitorWakeup from the context of the semaphore lock
614  using (await SemaphoreSlimContext.Lock(Semaphore, cancellationToken).ConfigureAwait(false))
615  {
616  // multiple things may have happened, handle them one at a time
617  for (var moreActivationsToProcess = true; moreActivationsToProcess && (monitorState.NextAction == MonitorAction.Continue || monitorState.NextAction == MonitorAction.Skip);)
618  {
619  MonitorActivationReason activationReason = default; // this will always be assigned before being used
620 
621  bool CheckActivationReason(ref Task task, MonitorActivationReason testActivationReason)
622  {
623  var taskCompleted = task?.IsCompleted == true;
624  task = null;
625  if (monitorState.NextAction == MonitorAction.Skip)
626  monitorState.NextAction = MonitorAction.Continue;
627  else if (taskCompleted)
628  {
629  activationReason = testActivationReason;
630  return true;
631  }
632 
633  return false;
634  }
635 
636  // process the tasks in this order and call HandlerMonitorWakup for each depending on the new monitorState
637  var anyActivation = CheckActivationReason(ref activeServerLifetime, MonitorActivationReason.ActiveServerCrashed)
638  || CheckActivationReason(ref activeServerReboot, MonitorActivationReason.ActiveServerRebooted)
639  || CheckActivationReason(ref newDmbAvailable, MonitorActivationReason.NewDmbAvailable)
640  || CheckActivationReason(ref inactiveServerLifetime, MonitorActivationReason.InactiveServerCrashed)
641  || CheckActivationReason(ref inactiveServerReboot, MonitorActivationReason.InactiveServerRebooted)
642  || CheckActivationReason(ref inactiveStartupComplete, MonitorActivationReason.InactiveServerStartupComplete)
643  || CheckActivationReason(ref activeLaunchParametersChanged, MonitorActivationReason.ActiveLaunchParametersUpdated)
644  || CheckActivationReason(ref heartbeat, MonitorActivationReason.Heartbeat);
645 
646  if (!anyActivation)
647  moreActivationsToProcess = false;
648  else
649  {
650  Logger.LogTrace("Reason: {0}", activationReason);
651  if (activationReason == MonitorActivationReason.Heartbeat)
652  monitorState.NextAction = await HandleHeartbeat(
653  monitorState.ActiveServer,
654  cancellationToken)
655  .ConfigureAwait(false);
656  else
657  await HandleMonitorWakeup(
658  activationReason,
659  monitorState,
660  cancellationToken)
661  .ConfigureAwait(false);
662  }
663  }
664  }
665 
666  Logger.LogTrace("Next monitor action is to {0}", monitorState.NextAction);
667 
668  // Restart if requested
669  if (monitorState.NextAction == MonitorAction.Restart)
670  monitorState = await MonitorRestart(cancellationToken).ConfigureAwait(false);
671  }
672  catch (OperationCanceledException)
673  {
674  // let this bubble, other exceptions caught below
675  throw;
676  }
677  catch (Exception e)
678  {
679  // really, this should NEVER happen
680  Logger.LogError(
681  "Monitor crashed! Iteration: {0}, Monitor State: {1}, Exception: {2}",
682  iteration,
683  JsonConvert.SerializeObject(monitorState),
684  e);
685 
686  var nextActionMessage = monitorState.NextAction != MonitorAction.Exit
687  ? "Restarting"
688  : "Shutting down";
689  var chatTask = Chat.SendWatchdogMessage(
690  $"Monitor crashed, this should NEVER happen! Please report this, full details in logs! {nextActionMessage}. Error: {e.Message}",
691  false,
692  cancellationToken);
693 
694  if (disposed)
695  monitorState.NextAction = MonitorAction.Exit;
696  else if (monitorState.NextAction != MonitorAction.Exit)
697  monitorState = await MonitorRestart(cancellationToken).ConfigureAwait(false);
698 
699  await chatTask.ConfigureAwait(false);
700  }
701  }
702  catch (OperationCanceledException)
703  {
704  // stop signal
705  Logger.LogDebug("Monitor cancelled");
706 
707  if (releaseServers)
708  {
709  Logger.LogTrace("Detaching servers...");
710  releasedReattachInformation = CreateReattachInformation();
711  }
712  }
713 
714  DisposeAndNullControllers();
715 
716  Logger.LogTrace("Monitor exiting...");
717  }
718 
727  protected abstract Task InitControllers(Action callBeforeRecurse, Task chatTask, DualReattachInformation reattachInfo, CancellationToken cancellationToken);
728 
730  public async Task ChangeSettings(DreamDaemonLaunchParameters launchParameters, CancellationToken cancellationToken)
731  {
732  using (await SemaphoreSlimContext.Lock(Semaphore, cancellationToken).ConfigureAwait(false))
733  {
734  bool match = launchParameters.CanApplyWithoutReboot(ActiveLaunchParameters);
735  ActiveLaunchParameters = launchParameters;
736  if (match || !Running)
737  return;
738 
739  ActiveParametersUpdated.TrySetResult(null); // queue an update
740  ActiveParametersUpdated = new TaskCompletionSource<object>();
741  }
742  }
743 
745  public async Task HandleEvent(EventType eventType, IEnumerable<string> parameters, CancellationToken cancellationToken)
746  {
747  var activeServer = GetActiveController();
748 
749  // Server may have ended
750  if (activeServer == null)
751  return;
752 
753  var notification = new EventNotification(eventType, parameters);
754  var result = await activeServer.SendCommand(
755  new TopicParameters(notification),
756  cancellationToken)
757  .ConfigureAwait(false);
758 
759  if (result?.InteropResponse?.ChatResponses != null)
760  await Task.WhenAll(
761  result.InteropResponse.ChatResponses.Select(
762  x => Chat.SendMessage(
763  x.Text,
764  x.ChannelIds
765  .Select(channelIdString =>
766  {
767  if (UInt64.TryParse(channelIdString, out var channelId))
768  return (ulong?)channelId;
769 
770  return null;
771  })
772  .Where(nullableChannelId => nullableChannelId.HasValue)
773  .Select(nullableChannelId => nullableChannelId.Value),
774  cancellationToken)))
775  .ConfigureAwait(false);
776  }
777 
779  public async Task<string> HandleChatCommand(string commandName, string arguments, ChatUser sender, CancellationToken cancellationToken)
780  {
781  using (await SemaphoreSlimContext.Lock(Semaphore, cancellationToken).ConfigureAwait(false))
782  {
783  if (!Running)
784  return "ERROR: Server offline!";
785 
786  var commandObject = new ChatCommand(sender, commandName, arguments);
787 
788  var command = new TopicParameters(commandObject);
789 
790  var activeServer = GetActiveController();
791  var commandResult = await activeServer.SendCommand(command, cancellationToken).ConfigureAwait(false);
792 
793  if (commandResult == null)
794  return "TGS: Bad topic exchange!";
795 
796  if (commandResult.InteropResponse == null)
797  return "TGS: Bad topic response!";
798 
799  return commandResult.InteropResponse.CommandResponseMessage ??
800  "TGS: Command processed but no DMAPI response returned!";
801  }
802  }
803 
805  public async Task Launch(CancellationToken cancellationToken)
806  {
807  using (await SemaphoreSlimContext.Lock(Semaphore, cancellationToken).ConfigureAwait(false))
808  await LaunchNoLock(true, true, null, cancellationToken).ConfigureAwait(false);
809  }
810 
812  public virtual async Task ResetRebootState(CancellationToken cancellationToken)
813  {
814  using (await SemaphoreSlimContext.Lock(Semaphore, cancellationToken).ConfigureAwait(false))
815  {
816  if (!Running)
817  return;
818  var toClear = GetActiveController();
819  if (toClear != null)
820  await toClear.SetRebootState(Session.RebootState.Normal, cancellationToken).ConfigureAwait(false);
821  }
822  }
823 
825  public async Task Restart(bool graceful, CancellationToken cancellationToken)
826  {
827  if (!Running)
828  throw new JobException(ErrorCode.WatchdogRunning);
829 
830  Logger.LogTrace("Begin Restart. Graceful: {0}", graceful);
831  using (await SemaphoreSlimContext.Lock(Semaphore, cancellationToken).ConfigureAwait(false))
832  {
833  if (!graceful)
834  {
835  Task chatTask;
836  if (Running)
837  {
838  chatTask = Chat.SendWatchdogMessage("Manual restart triggered...", false, cancellationToken);
839  await TerminateNoLock(false, false, cancellationToken).ConfigureAwait(false);
840  }
841  else
842  chatTask = Task.CompletedTask;
843  await LaunchNoLock(true, !Running, null, cancellationToken).ConfigureAwait(false);
844  await chatTask.ConfigureAwait(false);
845  }
846 
847  var toReboot = GetActiveController();
848  if (toReboot != null
849  && !await toReboot.SetRebootState(Session.RebootState.Restart, cancellationToken).ConfigureAwait(false))
850  Logger.LogWarning("Unable to send reboot state change event!");
851  }
852  }
853 
855  public async Task StartAsync(CancellationToken cancellationToken)
856  {
857  var reattachInfo = await reattachInfoHandler.Load(cancellationToken).ConfigureAwait(false);
858  if (!autoStart && reattachInfo == null)
859  return;
860 
861  long? adminUserId = null;
862 
863  await databaseContextFactory.UseContext(
864  async db => adminUserId = await db
865  .Users
866  .AsQueryable()
867  .Where(x => x.CanonicalName == Models.User.CanonicalizeName(Api.Models.User.AdminName))
868  .Select(x => x.Id)
869  .FirstAsync(cancellationToken)
870  .ConfigureAwait(false))
871  .ConfigureAwait(false);
872  var job = new Models.Job
873  {
874  StartedBy = new Models.User
875  {
876  Id = adminUserId.Value
877  },
878  Instance = new Models.Instance
879  {
880  Id = instance.Id
881  },
882  Description = $"Instance startup watchdog {(reattachInfo != null ? "reattach" : "launch")}",
883  CancelRight = (ulong)DreamDaemonRights.Shutdown,
884  CancelRightsType = RightsType.DreamDaemon
885  };
886  await jobManager.RegisterOperation(job, async (j, databaseContextFactory, progressFunction, ct) =>
887  {
888  using (await SemaphoreSlimContext.Lock(Semaphore, ct).ConfigureAwait(false))
889  await LaunchNoLock(true, true, reattachInfo, ct).ConfigureAwait(false);
890  }, cancellationToken).ConfigureAwait(false);
891  }
892 
894  public async Task StopAsync(CancellationToken cancellationToken)
895  {
896  await TerminateNoLock(false, !releaseServers, cancellationToken).ConfigureAwait(false);
897  if (releasedReattachInformation != null)
898  {
899  await reattachInfoHandler.Save(releasedReattachInformation, cancellationToken).ConfigureAwait(false);
900  releasedReattachInformation = null;
901  releaseServers = false;
902  }
903  }
904 
906  public async Task Terminate(bool graceful, CancellationToken cancellationToken)
907  {
908  using (await SemaphoreSlimContext.Lock(Semaphore, cancellationToken).ConfigureAwait(false))
909  await TerminateNoLock(graceful, !releaseServers, cancellationToken).ConfigureAwait(false);
910  }
911 
913  public async Task HandleRestart(Version updateVersion, CancellationToken cancellationToken)
914  {
915  releaseServers = true;
916  if (Running)
917  await Chat.SendWatchdogMessage("Detaching...", false, cancellationToken).ConfigureAwait(false);
918  }
919 
921  public abstract Task InstanceRenamed(string newInstanceName, CancellationToken cancellationToken);
922  }
923 }
readonly bool autoStart
If the WatchdogBase should LaunchNoLock(bool, bool, DualReattachInformation, CancellationToken) in St...
async Task Launch(CancellationToken cancellationToken)
Start the IWatchdog
ErrorCode
Types of ErrorMessages that the API may return.
Definition: ErrorCode.cs:10
MonitorAction NextAction
The next MonitorAction to take in WatchdogBase.MonitorLifetimes(global::System.Threading.CancellationToken)
Definition: MonitorState.cs:24
RightsType
The type of rights a model uses
Definition: RightsType.cs:6
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
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
Definition: EventType.cs:6
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
Definition: IChatManager.cs:13
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
async Task< MonitorAction > HandleHeartbeat(ISessionController activeServer, CancellationToken cancellationToken)
Handles a watchdog heartbeat.
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
Task< CombinedTopicResponse > SendCommand(TopicParameters parameters, CancellationToken cancellationToken)
Sends a command to DreamDaemon through /world/Topic()
ISessionController ActiveServer
The active ISessionController
Definition: MonitorState.cs:30
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 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
Definition: IJobManager.cs:13
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.
Definition: WatchdogBase.cs:95
async Task< bool > StopMonitor()
Stops MonitorLifetimes(CancellationToken). Doesn&#39;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
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
The (absolute) state of the ExperimentalWatchdog
Definition: MonitorState.cs:9
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
bool disposed
If the WatchdogBase has been Disposed.
Runs and monitors the twin server controllers
Definition: IWatchdog.cs:14
Represents a service that may take an updated Host assembly and run it, stopping the current assembly...