tgstation-server 6.19.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.Text;
6using System.Threading;
7using System.Threading.Tasks;
8
9using HotChocolate.Execution;
10
11using Microsoft.Extensions.DependencyInjection;
12using Microsoft.Extensions.Hosting;
13using Microsoft.Extensions.Logging;
14using Microsoft.Extensions.Options;
15
21
23{
26 {
28 public bool RestartRequested { get; private set; }
29
31 public bool UpdateInProgress { get; private set; }
32
34 public bool WatchdogPresent =>
35#if WATCHDOG_FREE_RESTART
36 true;
37#else
38 updatePath != null;
39#endif
40
44 internal IHost? Host { get; private set; }
45
50
54 readonly IHostBuilder hostBuilder;
55
59 readonly List<IRestartHandler> restartHandlers;
60
64 readonly string? updatePath;
65
69 readonly object restartLock;
70
74 ILogger<Server>? logger;
75
80
84 CancellationTokenSource? cancellationTokenSource;
85
90
95
100
105
112 public Server(IIOManager ioManager, IHostBuilder hostBuilder, string? updatePath)
113 {
114 this.ioManager = ioManager ?? throw new ArgumentNullException(nameof(ioManager));
115 this.hostBuilder = hostBuilder ?? throw new ArgumentNullException(nameof(hostBuilder));
116 this.updatePath = updatePath;
117
118 hostBuilder.ConfigureServices(serviceCollection => serviceCollection.AddSingleton<IServerControl>(this));
119
120 restartHandlers = new List<IRestartHandler>();
121 restartLock = new object();
122 logger = null;
123 }
124
126 public async ValueTask Run(CancellationToken cancellationToken)
127 {
128 var updateDirectory = updatePath != null ? ioManager.GetDirectoryName(updatePath) : null;
129 using (cancellationTokenSource = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken))
130 using (var fsWatcher = updateDirectory != null ? new FileSystemWatcher(updateDirectory) : null)
131 {
132 if (fsWatcher != null)
133 {
134 // If ever there is a NECESSARY update to the Host Watchdog, change this to use a pipe
135 // I don't know why I'm only realizing this in 2023 when this is 2019 code
136 // As it stands, FSWatchers use async I/O on Windows and block a new thread on Linux
137 // That's an acceptable, if saddening, resource loss for now
138 fsWatcher.Created += WatchForShutdownFileCreation;
139 fsWatcher.EnableRaisingEvents = true;
140 }
141
142 try
143 {
144 using (Host = hostBuilder.Build())
145 {
146 logger = Host.Services.GetRequiredService<ILogger<Server>>();
147 try
148 {
149 using (cancellationToken.Register(() => logger.LogInformation("Server termination requested!")))
150 {
151 if (await DumpGraphQLSchemaIfRequested(Host.Services, cancellationToken))
152 return;
153
154 var generalConfigurationOptions = Host.Services.GetRequiredService<IOptions<GeneralConfiguration>>();
155 generalConfiguration = generalConfigurationOptions.Value;
156 await Host.RunAsync(cancellationTokenSource.Token);
157 }
158
159 if (updateTask != null)
160 await updateTask;
161 }
162 catch (OperationCanceledException ex)
163 {
164 logger.LogDebug(ex, "Server run cancelled!");
165 }
166 catch (Exception ex)
167 {
169 throw;
170 }
171 finally
172 {
173 logger = null;
174 }
175 }
176 }
177 finally
178 {
179 Host = null;
180 }
181 }
182
184 }
185
187 public bool TryStartUpdate(IServerUpdateExecutor updateExecutor, Version newVersion)
188 {
189 ArgumentNullException.ThrowIfNull(updateExecutor);
190 ArgumentNullException.ThrowIfNull(newVersion);
191
192 CheckSanity(true);
193
194 if (updatePath == null)
195 throw new InvalidOperationException("Tried to start update when server was initialized without an updatePath set!");
196
197 var logger = this.logger!;
198 logger.LogTrace("Begin ApplyUpdate...");
199
200 CancellationToken criticalCancellationToken;
201 lock (restartLock)
202 {
204 {
205 logger.LogDebug("Aborted update due to concurrency conflict!");
206 return false;
207 }
208
209 if (cancellationTokenSource == null)
210 throw new InvalidOperationException("Tried to update a non-running Server!");
211
212 criticalCancellationToken = cancellationTokenSource.Token;
213 UpdateInProgress = true;
214 }
215
216 async Task RunUpdate()
217 {
218 var updateExecutedSuccessfully = false;
219 try
220 {
221 updateExecutedSuccessfully = await updateExecutor.ExecuteUpdate(updatePath, criticalCancellationToken, criticalCancellationToken);
222 }
223 catch (OperationCanceledException ex)
224 {
225 logger.LogDebug(ex, "Update cancelled!");
226 UpdateInProgress = false;
227 }
228 catch (Exception ex)
229 {
230 logger.LogError(ex, "Update errored!");
231 UpdateInProgress = false;
232 }
233
234 if (updateExecutedSuccessfully)
235 {
236 logger.LogTrace("Update complete!");
237 await RestartImpl(newVersion, null, true, true);
238 }
239 else if (terminateIfUpdateFails)
240 {
241 logger.LogTrace("Stopping host due to termination request...");
243 }
244 else
245 {
246 logger.LogTrace("Update failed!");
247 UpdateInProgress = false;
248 }
249 }
250
251 updateTask = RunUpdate();
252 return true;
253 }
254
257 {
258 ArgumentNullException.ThrowIfNull(handler);
259
260 CheckSanity(false);
261
262 var logger = this.logger!;
263 lock (restartLock)
265 {
266 logger.LogTrace("Registering restart handler {handlerImplementationName}...", handler);
267 restartHandlers.Add(handler);
268 return new RestartRegistration(
269 new DisposeInvoker(() =>
270 {
271 lock (restartLock)
273 restartHandlers.Remove(handler);
274 }));
275 }
276
277 logger.LogWarning("Restart handler {handlerImplementationName} register after a shutdown had begun!", handler);
278 return new RestartRegistration(null);
279 }
280
282 public ValueTask Restart() => RestartImpl(null, null, true, true);
283
285 public ValueTask GracefulShutdown(bool detach) => RestartImpl(null, null, false, detach);
286
288 public ValueTask Die(Exception? exception)
289 {
290 if (exception != null)
291 return RestartImpl(null, exception, false, true);
292
294 return ValueTask.CompletedTask;
295 }
296
303 async ValueTask<bool> DumpGraphQLSchemaIfRequested(IServiceProvider services, CancellationToken cancellationToken)
304 {
305 var internalConfigurationOptions = services.GetRequiredService<IOptions<InternalConfiguration>>();
306 var apiDumpPath = internalConfigurationOptions.Value.DumpGraphQLApiPath;
307 if (String.IsNullOrWhiteSpace(apiDumpPath))
308 return false;
309
310 logger!.LogInformation("Dumping GraphQL API spec to {path} and exiting...", apiDumpPath);
311
312 // https://github.com/ChilliCream/graphql-platform/discussions/5885
313 var resolver = services.GetRequiredService<IRequestExecutorResolver>();
314 var executor = await resolver.GetRequestExecutorAsync(cancellationToken: cancellationToken);
315 var sdl = executor.Schema.Print();
316
317 var ioManager = services.GetRequiredService<IIOManager>();
318 await ioManager.WriteAllBytes(apiDumpPath, Encoding.UTF8.GetBytes(sdl), cancellationToken);
319 return true;
320 }
321
326 void CheckSanity(bool checkWatchdog)
327 {
328 if (checkWatchdog && !WatchdogPresent && propagatedException == null)
329 throw new InvalidOperationException("Server restarts are not supported");
330
331 if (cancellationTokenSource == null || logger == null)
332 throw new InvalidOperationException("Tried to control a non-running Server!");
333 }
334
340 {
341 if (propagatedException == null)
342 return;
343
344 if (otherException != null)
345 throw new AggregateException(propagatedException, otherException);
346
348 }
349
358 async ValueTask RestartImpl(Version? newVersion, Exception? exception, bool requireWatchdog, bool completeAsap)
359 {
360 CheckSanity(requireWatchdog);
361
362 // if the watchdog isn't required and there's no issue, this is just a graceful shutdown
363 bool isGracefulShutdown = !requireWatchdog && exception == null;
364 var logger = this.logger!;
365 logger.LogTrace(
366 "Begin {restartType}...",
367 isGracefulShutdown
368 ? completeAsap
369 ? "semi-graceful shutdown"
370 : "graceful shutdown"
371 : "restart");
372
373 lock (restartLock)
374 {
375 if ((UpdateInProgress && newVersion == null) || shutdownInProgress)
376 {
377 logger.LogTrace("Aborted restart due to concurrency conflict!");
378 return;
379 }
380
381 RestartRequested = !isGracefulShutdown;
382 propagatedException ??= exception;
383 }
384
385 if (exception == null)
386 {
387 var giveHandlersTimeToWaitAround = isGracefulShutdown && !completeAsap;
388 logger.LogInformation("Stopping server...");
389 using var cts = new CancellationTokenSource(
390 TimeSpan.FromMinutes(
391 giveHandlersTimeToWaitAround
394 var cancellationToken = cts.Token;
395 try
396 {
397 ValueTask eventsTask;
398 lock (restartLock)
399 eventsTask = ValueTaskExtensions.WhenAll(
401 .Select(
402 x => x.HandleRestart(newVersion, giveHandlersTimeToWaitAround, cancellationToken))
403 .ToList());
404
405 logger.LogTrace("Joining restart handlers...");
406 await eventsTask;
407 }
408 catch (OperationCanceledException ex)
409 {
410 if (isGracefulShutdown)
411 logger.LogWarning(ex, "Graceful shutdown timeout hit! Existing DreamDaemon processes will be terminated!");
412 else
413 logger.LogError(
414 ex,
415 "Restart timeout hit! Existing DreamDaemon processes will be lost and must be killed manually before being restarted with TGS!");
416 }
417 catch (Exception e)
418 {
419 logger.LogError(e, "Restart handlers error!");
420 }
421 }
422
424 }
425
431 async void WatchForShutdownFileCreation(object sender, FileSystemEventArgs eventArgs)
432 {
433 logger?.LogTrace("FileSystemWatcher triggered.");
434
435 // DCT: None available
436 if (eventArgs.FullPath == ioManager.ResolvePath(updatePath!) && await ioManager.FileExists(eventArgs.FullPath, CancellationToken.None))
437 {
438 logger?.LogInformation("Host watchdog appears to be requesting server termination!");
439 lock (restartLock)
440 {
441 if (!UpdateInProgress)
442 {
444 return;
445 }
446
448 }
449
450 logger?.LogInformation("An update is in progress, we will wait for that to complete...");
451 }
452 }
453
458 {
459 shutdownInProgress = true;
460 logger!.LogDebug("Stopping host...");
461 cancellationTokenSource!.Cancel();
462 }
463 }
464}
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:34
IRestartRegistration RegisterForRestart(IRestartHandler handler)
Register a given handler to run before stopping the server for a restart.A new IRestartRegistration ...
Definition Server.cs:256
ValueTask Die(Exception? exception)
Kill the server with a fatal exception.A Task representing the running operation.
Definition Server.cs:288
bool UpdateInProgress
Whether or not the server is currently updating.
Definition Server.cs:31
async ValueTask Run(CancellationToken cancellationToken)
Runs the IServer.A ValueTask representing the running operation.
Definition Server.cs:126
bool TryStartUpdate(IServerUpdateExecutor updateExecutor, Version newVersion)
Attempt to update with a given updateExecutor .true if the update started successfully,...
Definition Server.cs:187
Task? updateTask
The Task that is used for asynchronously updating the server.
Definition Server.cs:94
async ValueTask RestartImpl(Version? newVersion, Exception? exception, bool requireWatchdog, bool completeAsap)
Implements Restart().
Definition Server.cs:358
void CheckSanity(bool checkWatchdog)
Throws an InvalidOperationException if the IServerControl cannot be used.
Definition Server.cs:326
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:59
CancellationTokenSource? cancellationTokenSource
The cancellationTokenSource for the Server.
Definition Server.cs:84
async void WatchForShutdownFileCreation(object sender, FileSystemEventArgs eventArgs)
Event handler for the updatePath's FileSystemWatcher. Triggers shutdown if requested by host watchdog...
Definition Server.cs:431
readonly IIOManager ioManager
The IIOManager to use.
Definition Server.cs:49
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:104
readonly? string updatePath
The absolute path to install updates to.
Definition Server.cs:64
ILogger< Server >? logger
The ILogger for the Server.
Definition Server.cs:74
readonly IHostBuilder hostBuilder
The IHostBuilder for the Server.
Definition Server.cs:54
Exception? propagatedException
The Exception to propagate when the server terminates.
Definition Server.cs:89
GeneralConfiguration? generalConfiguration
The GeneralConfiguration for the Server.
Definition Server.cs:79
readonly object restartLock
lock object for certain restart related operations.
Definition Server.cs:69
bool shutdownInProgress
If the server is being shut down or restarted.
Definition Server.cs:99
Server(IIOManager ioManager, IHostBuilder hostBuilder, string? updatePath)
Initializes a new instance of the Server class.
Definition Server.cs:112
async ValueTask< bool > DumpGraphQLSchemaIfRequested(IServiceProvider services, CancellationToken cancellationToken)
Checks if InternalConfiguration.DumpGraphQLApiPath is set and dumps the GraphQL API Schema to it if s...
Definition Server.cs:303
bool RestartRequested
If the IServer should restart.
Definition Server.cs:28
void StopServerImmediate()
Fires off the cancellationTokenSource without any checks, shutting down everything.
Definition Server.cs:457
void CheckExceptionPropagation(Exception? otherException)
Re-throw propagatedException if it exists.
Definition Server.cs:339
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 .
Interface for using filesystems.
Definition IIOManager.cs:14
string ResolvePath()
Retrieve the full path of the current working directory.
string GetDirectoryName(string path)
Gets the directory portion of a given path .
ValueTask WriteAllBytes(string path, byte[] contents, CancellationToken cancellationToken)
Writes some contents to a file at path overwriting previous content.
Task< bool > FileExists(string path, CancellationToken cancellationToken)
Check that the file at path exists.
Represents the host.
Definition IServer.cs:10