tgstation-server 6.8.0
The /tg/station 13 server suite
Loading...
Searching...
No Matches
Server.cs
Go to the documentation of this file.
1using System;
2using System.Collections.Generic;
3using System.IO;
4using System.Linq;
5using System.Threading;
6using System.Threading.Tasks;
7
8using Microsoft.Extensions.DependencyInjection;
9using Microsoft.Extensions.Hosting;
10using Microsoft.Extensions.Logging;
11using Microsoft.Extensions.Options;
12
17
19{
22 {
24 public bool RestartRequested { get; private set; }
25
27 public bool UpdateInProgress { get; private set; }
28
30 public bool WatchdogPresent =>
31#if WATCHDOG_FREE_RESTART
32 true;
33#else
34 updatePath != null;
35#endif
36
40 internal IHost? Host { get; private set; }
41
45 readonly IHostBuilder hostBuilder;
46
50 readonly List<IRestartHandler> restartHandlers;
51
55 readonly string? updatePath;
56
60 readonly object restartLock;
61
65 ILogger<Server>? logger;
66
71
75 CancellationTokenSource? cancellationTokenSource;
76
81
86
91
96
102 public Server(IHostBuilder hostBuilder, string? updatePath)
103 {
104 this.hostBuilder = hostBuilder ?? throw new ArgumentNullException(nameof(hostBuilder));
105 this.updatePath = updatePath;
106
107 hostBuilder.ConfigureServices(serviceCollection => serviceCollection.AddSingleton<IServerControl>(this));
108
109 restartHandlers = new List<IRestartHandler>();
110 restartLock = new object();
111 logger = null;
112 }
113
115 public async ValueTask Run(CancellationToken cancellationToken)
116 {
117 var updateDirectory = updatePath != null ? Path.GetDirectoryName(updatePath) : null;
118 using (cancellationTokenSource = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken))
119 using (var fsWatcher = updateDirectory != null ? new FileSystemWatcher(updateDirectory) : null)
120 {
121 if (fsWatcher != null)
122 {
123 // If ever there is a NECESSARY update to the Host Watchdog, change this to use a pipe
124 // I don't know why I'm only realizing this in 2023 when this is 2019 code
125 // As it stands, FSWatchers use async I/O on Windows and block a new thread on Linux
126 // That's an acceptable, if saddening, resource loss for now
127 fsWatcher.Created += WatchForShutdownFileCreation;
128 fsWatcher.EnableRaisingEvents = true;
129 }
130
131 try
132 {
133 using (Host = hostBuilder.Build())
134 {
135 logger = Host.Services.GetRequiredService<ILogger<Server>>();
136 try
137 {
138 using (cancellationToken.Register(() => logger.LogInformation("Server termination requested!")))
139 {
140 var generalConfigurationOptions = Host.Services.GetRequiredService<IOptions<GeneralConfiguration>>();
141 generalConfiguration = generalConfigurationOptions.Value;
142 await Host.RunAsync(cancellationTokenSource.Token);
143 }
144
145 if (updateTask != null)
146 await updateTask;
147 }
148 catch (OperationCanceledException ex)
149 {
150 logger.LogDebug(ex, "Server run cancelled!");
151 }
152 catch (Exception ex)
153 {
155 throw;
156 }
157 finally
158 {
159 logger = null;
160 }
161 }
162 }
163 finally
164 {
165 Host = null;
166 }
167 }
168
170 }
171
173 public bool TryStartUpdate(IServerUpdateExecutor updateExecutor, Version newVersion)
174 {
175 ArgumentNullException.ThrowIfNull(updateExecutor);
176 ArgumentNullException.ThrowIfNull(newVersion);
177
178 CheckSanity(true);
179
180 if (updatePath == null)
181 throw new InvalidOperationException("Tried to start update when server was initialized without an updatePath set!");
182
183 var logger = this.logger!;
184 logger.LogTrace("Begin ApplyUpdate...");
185
186 CancellationToken criticalCancellationToken;
187 lock (restartLock)
188 {
190 {
191 logger.LogDebug("Aborted update due to concurrency conflict!");
192 return false;
193 }
194
195 if (cancellationTokenSource == null)
196 throw new InvalidOperationException("Tried to update a non-running Server!");
197
198 criticalCancellationToken = cancellationTokenSource.Token;
199 UpdateInProgress = true;
200 }
201
202 async Task RunUpdate()
203 {
204 if (await updateExecutor.ExecuteUpdate(updatePath, criticalCancellationToken, criticalCancellationToken))
205 {
206 logger.LogTrace("Update complete!");
207 await RestartImpl(newVersion, null, true, true);
208 }
209 else if (terminateIfUpdateFails)
210 {
211 logger.LogTrace("Stopping host due to termination request...");
213 }
214 else
215 {
216 logger.LogTrace("Update failed!");
217 UpdateInProgress = false;
218 }
219 }
220
221 updateTask = RunUpdate();
222 return true;
223 }
224
227 {
228 ArgumentNullException.ThrowIfNull(handler);
229
230 CheckSanity(false);
231
232 var logger = this.logger!;
233 lock (restartLock)
235 {
236 logger.LogTrace("Registering restart handler {handlerImplementationName}...", handler);
237 restartHandlers.Add(handler);
238 return new RestartRegistration(
239 new DisposeInvoker(() =>
240 {
241 lock (restartLock)
243 restartHandlers.Remove(handler);
244 }));
245 }
246
247 logger.LogWarning("Restart handler {handlerImplementationName} register after a shutdown had begun!", handler);
248 return new RestartRegistration(null);
249 }
250
252 public ValueTask Restart() => RestartImpl(null, null, true, true);
253
255 public ValueTask GracefulShutdown(bool detach) => RestartImpl(null, null, false, detach);
256
258 public ValueTask Die(Exception? exception)
259 {
260 if (exception != null)
261 return RestartImpl(null, exception, false, true);
262
264 return ValueTask.CompletedTask;
265 }
266
271 void CheckSanity(bool checkWatchdog)
272 {
273 if (checkWatchdog && !WatchdogPresent && propagatedException == null)
274 throw new InvalidOperationException("Server restarts are not supported");
275
276 if (cancellationTokenSource == null || logger == null)
277 throw new InvalidOperationException("Tried to control a non-running Server!");
278 }
279
285 {
286 if (propagatedException == null)
287 return;
288
289 if (otherException != null)
290 throw new AggregateException(propagatedException, otherException);
291
293 }
294
303 async ValueTask RestartImpl(Version? newVersion, Exception? exception, bool requireWatchdog, bool completeAsap)
304 {
305 CheckSanity(requireWatchdog);
306
307 // if the watchdog isn't required and there's no issue, this is just a graceful shutdown
308 bool isGracefulShutdown = !requireWatchdog && exception == null;
309 var logger = this.logger!;
310 logger.LogTrace(
311 "Begin {restartType}...",
312 isGracefulShutdown
313 ? completeAsap
314 ? "semi-graceful shutdown"
315 : "graceful shutdown"
316 : "restart");
317
318 lock (restartLock)
319 {
320 if ((UpdateInProgress && newVersion == null) || shutdownInProgress)
321 {
322 logger.LogTrace("Aborted restart due to concurrency conflict!");
323 return;
324 }
325
326 RestartRequested = !isGracefulShutdown;
327 propagatedException ??= exception;
328 }
329
330 if (exception == null)
331 {
332 var giveHandlersTimeToWaitAround = isGracefulShutdown && !completeAsap;
333 logger.LogInformation("Stopping server...");
334 using var cts = new CancellationTokenSource(
335 TimeSpan.FromMinutes(
336 giveHandlersTimeToWaitAround
339 var cancellationToken = cts.Token;
340 try
341 {
342 ValueTask eventsTask;
343 lock (restartLock)
344 eventsTask = ValueTaskExtensions.WhenAll(
346 .Select(
347 x => x.HandleRestart(newVersion, giveHandlersTimeToWaitAround, cancellationToken))
348 .ToList());
349
350 logger.LogTrace("Joining restart handlers...");
351 await eventsTask;
352 }
353 catch (OperationCanceledException ex)
354 {
355 if (isGracefulShutdown)
356 logger.LogWarning(ex, "Graceful shutdown timeout hit! Existing DreamDaemon processes will be terminated!");
357 else
358 logger.LogError(
359 ex,
360 "Restart timeout hit! Existing DreamDaemon processes will be lost and must be killed manually before being restarted with TGS!");
361 }
362 catch (Exception e)
363 {
364 logger.LogError(e, "Restart handlers error!");
365 }
366 }
367
369 }
370
376 void WatchForShutdownFileCreation(object sender, FileSystemEventArgs eventArgs)
377 {
378 logger?.LogTrace("FileSystemWatcher triggered.");
379
380 // TODO: Refactor this to not use System.IO function here.
381 if (eventArgs.FullPath == Path.GetFullPath(updatePath!) && File.Exists(eventArgs.FullPath))
382 {
383 logger?.LogInformation("Host watchdog appears to be requesting server termination!");
384 lock (restartLock)
385 {
386 if (!UpdateInProgress)
387 {
389 return;
390 }
391
393 }
394
395 logger?.LogInformation("An update is in progress, we will wait for that to complete...");
396 }
397 }
398
403 {
404 shutdownInProgress = true;
405 logger!.LogDebug("Stopping host...");
406 cancellationTokenSource!.Cancel();
407 }
408 }
409}
Extension methods for the ValueTask and ValueTask<TResult> classes.
static async ValueTask WhenAll(IEnumerable< ValueTask > tasks)
Fully await a given list of tasks .
uint ShutdownTimeoutMinutes
The timeout minutes for gracefully stopping the server.
uint RestartTimeoutMinutes
The timeout minutes for restarting the server.
bool WatchdogPresent
true if live updates are supported, false. TryStartUpdate(IServerUpdateExecutor, Version) and Restart...
Definition: Server.cs:30
IRestartRegistration RegisterForRestart(IRestartHandler handler)
Register a given handler to run before stopping the server for a restart. A new IRestartRegistration...
Definition: Server.cs:226
ValueTask Die(Exception? exception)
Kill the server with a fatal exception. A Task representing the running operation.
Definition: Server.cs:258
bool UpdateInProgress
Whether or not the server is currently updating.
Definition: Server.cs:27
async ValueTask Run(CancellationToken cancellationToken)
Runs the IServer. A ValueTask representing the running operation.
Definition: Server.cs:115
bool TryStartUpdate(IServerUpdateExecutor updateExecutor, Version newVersion)
Attempt to update with a given updateExecutor . true if the update started successfully,...
Definition: Server.cs:173
Task? updateTask
The Task that is used for asynchronously updating the server.
Definition: Server.cs:85
async ValueTask RestartImpl(Version? newVersion, Exception? exception, bool requireWatchdog, bool completeAsap)
Implements Restart().
Definition: Server.cs:303
void CheckSanity(bool checkWatchdog)
Throws an InvalidOperationException if the IServerControl cannot be used.
Definition: Server.cs:271
ValueTask GracefulShutdown(bool detach)
Gracefully shutsdown the Host. A ValueTask representing the running operation.
readonly List< IRestartHandler > restartHandlers
The IRestartHandlers to run when the Server restarts.
Definition: Server.cs:50
CancellationTokenSource? cancellationTokenSource
The cancellationTokenSource for the Server.
Definition: Server.cs:75
ValueTask Restart()
Restarts the Host. A ValueTask representing the running operation.
bool terminateIfUpdateFails
If there is an update in progress and this flag is set, it should stop the server immediately if it f...
Definition: Server.cs:95
readonly? string updatePath
The absolute path to install updates to.
Definition: Server.cs:55
ILogger< Server >? logger
The ILogger for the Server.
Definition: Server.cs:65
void WatchForShutdownFileCreation(object sender, FileSystemEventArgs eventArgs)
Event handler for the updatePath's FileSystemWatcher. Triggers shutdown if requested by host watchdog...
Definition: Server.cs:376
readonly IHostBuilder hostBuilder
The IHostBuilder for the Server.
Definition: Server.cs:45
Exception? propagatedException
The Exception to propagate when the server terminates.
Definition: Server.cs:80
GeneralConfiguration? generalConfiguration
The GeneralConfiguration for the Server.
Definition: Server.cs:70
readonly object restartLock
lock object for certain restart related operations.
Definition: Server.cs:60
bool shutdownInProgress
If the server is being shut down or restarted.
Definition: Server.cs:90
Server(IHostBuilder hostBuilder, string? updatePath)
Initializes a new instance of the Server class.
Definition: Server.cs:102
bool RestartRequested
If the IServer should restart.
Definition: Server.cs:24
void StopServerImmediate()
Fires off the cancellationTokenSource without any checks, shutting down everything.
Definition: Server.cs:402
void CheckExceptionPropagation(Exception? otherException)
Re-throw propagatedException if it exists.
Definition: Server.cs:284
Runs a given disposeAction on Dispose.
Represents the lifetime of a IRestartHandler registration.
Represents a service that may take an updated Host assembly and run it, stopping the current assembly...
ValueTask< bool > ExecuteUpdate(string updatePath, CancellationToken cancellationToken, CancellationToken criticalCancellationToken)
Executes a pending server update by extracting the new server to a given updatePath .
Represents the host.
Definition: IServer.cs:10