tgstation-server  4.3.2
The /tg/station 13 server suite
BasicWatchdog.cs
Go to the documentation of this file.
1 using Microsoft.Extensions.Logging;
2 using System;
3 using System.Collections.Generic;
4 using System.Globalization;
5 using System.Threading;
6 using System.Threading.Tasks;
14 
15 namespace Tgstation.Server.Host.Components.Watchdog
16 {
21  {
23  public sealed override bool AlphaIsActive => true;
24 
26  public sealed override Models.CompileJob ActiveCompileJob => Server?.Dmb.CompileJob;
27 
29  public sealed override RebootState? RebootState => Server?.RebootState;
30 
34  protected ISessionController Server { get; private set; }
35 
40 
56  public BasicWatchdog(
57  IChatManager chat,
58  ISessionControllerFactory sessionControllerFactory,
59  IDmbFactory dmbFactory,
60  IReattachInfoHandler reattachInfoHandler,
61  IDatabaseContextFactory databaseContextFactory,
62  IJobManager jobManager,
63  IServerControl serverControl,
64  IAsyncDelayer asyncDelayer,
65  ILogger<BasicWatchdog> logger,
66  DreamDaemonLaunchParameters initialLaunchParameters,
67  Api.Models.Instance instance,
68  bool autoStart)
69  : base(
70  chat,
71  sessionControllerFactory,
72  dmbFactory,
73  reattachInfoHandler,
74  databaseContextFactory,
75  jobManager,
76  serverControl,
77  asyncDelayer,
78  logger,
79  initialLaunchParameters,
80  instance,
81  autoStart)
82  { }
83 
85  protected override IReadOnlyDictionary<MonitorActivationReason, Task> GetMonitoredServerTasks(MonitorState monitorState)
86  {
87  if (Server == null)
88  return null;
89 
90  monitorState.ActiveServer = Server;
91 
92  return new Dictionary<MonitorActivationReason, Task>
93  {
94  { MonitorActivationReason.ActiveServerCrashed, Server.Lifetime },
95  { MonitorActivationReason.ActiveServerRebooted, Server.OnReboot },
96  { MonitorActivationReason.InactiveServerCrashed, Extensions.TaskExtensions.InfiniteTask() },
97  { MonitorActivationReason.InactiveServerRebooted, Extensions.TaskExtensions.InfiniteTask() },
98  { MonitorActivationReason.InactiveServerStartupComplete, Extensions.TaskExtensions.InfiniteTask() }
99  };
100  }
101 
103  protected override async Task HandleMonitorWakeup(MonitorActivationReason reason, MonitorState monitorState, CancellationToken cancellationToken)
104  {
105  switch (reason)
106  {
107  case MonitorActivationReason.ActiveServerCrashed:
108  string exitWord = Server.TerminationWasRequested ? "exited" : "crashed";
109  if (Server.RebootState == Session.RebootState.Shutdown)
110  {
111  // the time for graceful shutdown is now
112  await Chat.SendWatchdogMessage(
113  String.Format(
114  CultureInfo.InvariantCulture,
115  "Server {0}! Shutting down due to graceful termination request...",
116  exitWord),
117  false,
118  cancellationToken)
119  .ConfigureAwait(false);
120  monitorState.NextAction = MonitorAction.Exit;
121  }
122  else
123  {
124  await Chat.SendWatchdogMessage(
125  String.Format(
126  CultureInfo.InvariantCulture,
127  "Server {0}! Rebooting...",
128  exitWord),
129  false,
130  cancellationToken)
131  .ConfigureAwait(false);
132  monitorState.NextAction = MonitorAction.Restart;
133  }
134 
135  break;
136  case MonitorActivationReason.ActiveServerRebooted:
137  var rebootState = Server.RebootState;
138  if (gracefulRebootRequired && rebootState == Session.RebootState.Normal)
139  {
140  Logger.LogError("Watchdog reached normal reboot state with gracefulRebootRequired set!");
141  rebootState = Session.RebootState.Restart;
142  }
143 
144  gracefulRebootRequired = false;
145  Server.ResetRebootState();
146 
147  switch (rebootState)
148  {
149  case Session.RebootState.Normal:
150  monitorState.NextAction = HandleNormalReboot();
151  break;
152  case Session.RebootState.Restart:
153  monitorState.NextAction = MonitorAction.Restart;
154  break;
155  case Session.RebootState.Shutdown:
156  // graceful shutdown time
157  await Chat.SendWatchdogMessage(
158  "Active server rebooted! Shutting down due to graceful termination request...",
159  false,
160  cancellationToken)
161  .ConfigureAwait(false);
162  monitorState.NextAction = MonitorAction.Exit;
163  break;
164  default:
165  throw new InvalidOperationException($"Invalid reboot state: {rebootState}");
166  }
167 
168  break;
169  case MonitorActivationReason.ActiveLaunchParametersUpdated:
170  await Server.SetRebootState(Session.RebootState.Restart, cancellationToken).ConfigureAwait(false);
171  gracefulRebootRequired = true;
172  break;
173  case MonitorActivationReason.NewDmbAvailable:
174  await HandleNewDmbAvailable(cancellationToken).ConfigureAwait(false);
175  break;
176  case MonitorActivationReason.InactiveServerCrashed:
177  case MonitorActivationReason.InactiveServerRebooted:
178  case MonitorActivationReason.InactiveServerStartupComplete:
179  throw new NotSupportedException($"Unsupported activation reason: {reason}");
180  case MonitorActivationReason.Heartbeat:
181  default:
182  throw new InvalidOperationException($"Invalid activation reason: {reason}");
183  }
184  }
185 
187  protected sealed override DualReattachInformation CreateReattachInformation()
189  {
190  AlphaIsActive = true,
191  Alpha = Server?.Release()
192  };
193 
195  protected override void DisposeAndNullControllersImpl()
196  {
197  Server?.Dispose();
198  Server = null;
199  Running = false;
200  gracefulRebootRequired = false;
201  }
202 
204  protected sealed override ISessionController GetActiveController() => Server;
205 
207  protected sealed override async Task InitControllers(Action callBeforeRecurse, Task chatTask, DualReattachInformation reattachInfo, CancellationToken cancellationToken)
208  {
209  var serverToReattach = reattachInfo?.Alpha ?? reattachInfo?.Bravo;
210  var serverToKill = reattachInfo?.Bravo ?? reattachInfo?.Alpha;
211 
212  // vice versa
213  if (serverToKill == serverToReattach)
214  serverToKill = null;
215 
216  if (reattachInfo?.AlphaIsActive == false)
217  {
218  var temp = serverToReattach;
219  serverToReattach = serverToKill;
220  serverToKill = temp;
221  }
222 
223  // don't need a new dmb if reattaching
224  var doesntNeedNewDmb = serverToReattach != null;
225  var dmbToUse = doesntNeedNewDmb ? null : DmbFactory.LockNextDmb(1);
226 
227  // if this try catches something, both servers are killed
228  bool inactiveServerWasKilled = false;
229  try
230  {
231  // start the alpha server task, either by launch a new process or attaching to an existing one
232  // The tasks returned are mainly for writing interop files to the directories among other things and should generally never fail
233  // The tasks pertaining to server startup times are in the ISessionControllers
234  Task<ISessionController> serverLaunchTask, inactiveReattachTask;
235  if (!doesntNeedNewDmb)
236  {
237  dmbToUse = await PrepServerForLaunch(dmbToUse, cancellationToken).ConfigureAwait(false);
238  serverLaunchTask = SessionControllerFactory.LaunchNew(
239  dmbToUse,
240  null,
241  ActiveLaunchParameters,
242  true,
243  true,
244  false,
245  cancellationToken);
246  }
247  else
248  serverLaunchTask = SessionControllerFactory.Reattach(serverToReattach, cancellationToken);
249 
250  bool thereIsAnInactiveServerToKill = serverToKill != null;
251  if (thereIsAnInactiveServerToKill)
252  inactiveReattachTask = SessionControllerFactory.Reattach(serverToKill, cancellationToken);
253  else
254  inactiveReattachTask = Task.FromResult<ISessionController>(null);
255 
256  // retrieve the session controller
257  Server = await serverLaunchTask.ConfigureAwait(false);
258 
259  // failed reattaches will return null
260  Server?.SetHighPriority();
261 
262  var inactiveServerController = await inactiveReattachTask.ConfigureAwait(false);
263  inactiveServerController?.Dispose();
264  inactiveServerWasKilled = inactiveServerController != null;
265 
266  // possiblity of null servers due to failed reattaches
267  if (Server == null)
268  {
269  callBeforeRecurse();
270  await NotifyOfFailedReattach(thereIsAnInactiveServerToKill && !inactiveServerWasKilled, cancellationToken).ConfigureAwait(false);
271  return;
272  }
273 
274  await CheckLaunchResult(Server, "Server", cancellationToken).ConfigureAwait(false);
275 
276  Server.EnableCustomChatCommands();
277  }
278  catch
279  {
280  // kill the controllers
281  bool serverWasActive = Server != null;
282  DisposeAndNullControllers();
283 
284  // server didn't get control of this dmb
285  if (dmbToUse != null && !serverWasActive)
286  dmbToUse.Dispose();
287 
288  if (serverToKill != null && !inactiveServerWasKilled)
289  serverToKill.Dmb.Dispose();
290  throw;
291  }
292  }
293 
298  protected virtual MonitorAction HandleNormalReboot()
299  {
300  bool dmbUpdatePending = ActiveLaunchParameters != LastLaunchParameters;
301  return dmbUpdatePending ? MonitorAction.Restart : MonitorAction.Continue;
302  }
303 
309  protected virtual Task HandleNewDmbAvailable(CancellationToken cancellationToken)
310  {
311  gracefulRebootRequired = true;
312  return Server.SetRebootState(Session.RebootState.Restart, cancellationToken);
313  }
314 
321  protected virtual Task<IDmbProvider> PrepServerForLaunch(IDmbProvider dmbToUse, CancellationToken cancellationToken) => Task.FromResult(dmbToUse);
322 
324  public override Task ResetRebootState(CancellationToken cancellationToken)
325  {
326  if (!gracefulRebootRequired)
327  return base.ResetRebootState(cancellationToken);
328 
329  return Restart(true, cancellationToken);
330  }
331 
338  async Task NotifyOfFailedReattach(bool inactiveReattachSuccess, CancellationToken cancellationToken)
339  {
340  // we lost the server, just restart entirely
341  DisposeAndNullControllers();
342  const string FailReattachMessage = "Unable to properly reattach to server! Restarting...";
343  Logger.LogWarning(FailReattachMessage);
344  Logger.LogDebug(inactiveReattachSuccess ? "Also could not reattach to inactive server!" : "Inactive server was reattached successfully!");
345  Task chatTask = Chat.SendWatchdogMessage(FailReattachMessage, false, cancellationToken);
346  await LaunchNoLock(true, false, null, cancellationToken).ConfigureAwait(false);
347  await chatTask.ConfigureAwait(false);
348  }
349 
351  public sealed override Task InstanceRenamed(string newInstanceName, CancellationToken cancellationToken)
352  => Server?.InstanceRenamed(newInstanceName, cancellationToken) ?? Task.CompletedTask;
353  }
354 }
BasicWatchdog(IChatManager chat, ISessionControllerFactory sessionControllerFactory, IDmbFactory dmbFactory, IReattachInfoHandler reattachInfoHandler, IDatabaseContextFactory databaseContextFactory, IJobManager jobManager, IServerControl serverControl, IAsyncDelayer asyncDelayer, ILogger< BasicWatchdog > logger, DreamDaemonLaunchParameters initialLaunchParameters, Api.Models.Instance instance, bool autoStart)
Initializes a new instance of the BasicWatchdog .
async Task< ISessionController > Reattach(ReattachInformation reattachInformation, CancellationToken cancellationToken)
Create a ISessionController from an existing DreamDaemon instance
void EnableCustomChatCommands()
Enables the reading of custom chat commands from the ISessionController
bool gracefulRebootRequired
If the server is set to gracefully reboot due to a pending dmb or settings change.
MonitorAction NextAction
The next MonitorAction to take in WatchdogBase.MonitorLifetimes(global::System.Threading.CancellationToken)
Definition: MonitorState.cs:24
async Task< ISessionController > LaunchNew(IDmbProvider dmbProvider, IByondExecutableLock currentByondLock, DreamDaemonLaunchParameters launchParameters, bool primaryPort, bool primaryDirectory, bool apiValidate, CancellationToken cancellationToken)
Create a ISessionController from a freshly launch DreamDaemon instance
ReattachInformation Alpha
ReattachInformation for the Alpha session
Factory for scoping usage of IDatabaseContexts. Meant for use by Components
virtual Task HandleNewDmbAvailable(CancellationToken cancellationToken)
Handler for MonitorActivationReason.NewDmbAvailable.
virtual MonitorAction HandleNormalReboot()
Handler for MonitorActivationReason.ActiveServerRebooted when the RebootState is RebootState.Normal.
ReattachInformation Bravo
ReattachInformation for the Bravo session
override void DisposeAndNullControllersImpl()
Call IDisposable.Dispose and null the fields for all ISessionControllers and set Running to ...
For managing connected chat services
Definition: IChatManager.cs:13
RebootState
Represents the action to take when /world/Reboot() is called
Definition: RebootState.cs:6
MonitorActivationReason
Reasons for the monitor to wake up
ISessionController ActiveServer
The active ISessionController
Definition: MonitorState.cs:30
override async Task HandleMonitorWakeup(MonitorActivationReason reason, MonitorState monitorState, CancellationToken cancellationToken)
Handles the actions to take when the monitor has to "wake up"
Task Restart()
Restarts the Host
void SetHighPriority()
Set&#39;s the owned global::System.Diagnostics.Process.PriorityClass to global::System.Diagnostics.ProcessPriorityClass.AboveNormal
override IReadOnlyDictionary< MonitorActivationReason, Task > GetMonitoredServerTasks(MonitorState monitorState)
Gets the tasks for the following MonitorActivationReasons: MonitorActivationReason.ActiveServerCrashed, MonitorActivationReason.ActiveServerRebooted, MonitorActivationReason.InactiveServerCrashed, MonitorActivationReason.InactiveServerRebooted, MonitorActivationReason.InactiveServerStartupComplete.
Handles communication with a DreamDaemon IProcess
Manages the runtime of Jobs
Definition: IJobManager.cs:13
Provides absolute paths to the latest compiled .dmbs
Definition: IDmbProvider.cs:9
Handles saving and loading DualReattachInformation
sealed override async Task InitControllers(Action callBeforeRecurse, Task chatTask, DualReattachInformation reattachInfo, CancellationToken cancellationToken)
Starts all ISessionControllers.
MonitorAction
The action for the monitor loop to take when control is returned to it
Definition: MonitorAction.cs:6
override Task ResetRebootState(CancellationToken cancellationToken)
Cancels pending graceful actions
IDmbProvider LockNextDmb(int lockCount)
Gets the next IDmbProvider
Definition: DmbFactory.cs:153
The (absolute) state of the ExperimentalWatchdog
Definition: MonitorState.cs:9
Represents a service that may take an updated Host assembly and run it, stopping the current assembly...
async Task NotifyOfFailedReattach(bool inactiveReattachSuccess, CancellationToken cancellationToken)
Send a chat message and log about a failed reattach operation and attempts another call to WatchdogBa...