tgstation-server 6.12.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
49 readonly IHostBuilder hostBuilder;
50
54 readonly List<IRestartHandler> restartHandlers;
55
59 readonly string? updatePath;
60
64 readonly object restartLock;
65
69 ILogger<Server>? logger;
70
75
79 CancellationTokenSource? cancellationTokenSource;
80
85
90
95
100
106 public Server(IHostBuilder hostBuilder, string? updatePath)
107 {
108 this.hostBuilder = hostBuilder ?? throw new ArgumentNullException(nameof(hostBuilder));
109 this.updatePath = updatePath;
110
111 hostBuilder.ConfigureServices(serviceCollection => serviceCollection.AddSingleton<IServerControl>(this));
112
113 restartHandlers = new List<IRestartHandler>();
114 restartLock = new object();
115 logger = null;
116 }
117
119 public async ValueTask Run(CancellationToken cancellationToken)
120 {
121 var updateDirectory = updatePath != null ? Path.GetDirectoryName(updatePath) : null;
122 using (cancellationTokenSource = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken))
123 using (var fsWatcher = updateDirectory != null ? new FileSystemWatcher(updateDirectory) : null)
124 {
125 if (fsWatcher != null)
126 {
127 // If ever there is a NECESSARY update to the Host Watchdog, change this to use a pipe
128 // I don't know why I'm only realizing this in 2023 when this is 2019 code
129 // As it stands, FSWatchers use async I/O on Windows and block a new thread on Linux
130 // That's an acceptable, if saddening, resource loss for now
131 fsWatcher.Created += WatchForShutdownFileCreation;
132 fsWatcher.EnableRaisingEvents = true;
133 }
134
135 try
136 {
137 using (Host = hostBuilder.Build())
138 {
139 logger = Host.Services.GetRequiredService<ILogger<Server>>();
140 try
141 {
142 using (cancellationToken.Register(() => logger.LogInformation("Server termination requested!")))
143 {
144 if (await DumpGraphQLSchemaIfRequested(Host.Services, cancellationToken))
145 return;
146
147 var generalConfigurationOptions = Host.Services.GetRequiredService<IOptions<GeneralConfiguration>>();
148 generalConfiguration = generalConfigurationOptions.Value;
149 await Host.RunAsync(cancellationTokenSource.Token);
150 }
151
152 if (updateTask != null)
153 await updateTask;
154 }
155 catch (OperationCanceledException ex)
156 {
157 logger.LogDebug(ex, "Server run cancelled!");
158 }
159 catch (Exception ex)
160 {
162 throw;
163 }
164 finally
165 {
166 logger = null;
167 }
168 }
169 }
170 finally
171 {
172 Host = null;
173 }
174 }
175
177 }
178
180 public bool TryStartUpdate(IServerUpdateExecutor updateExecutor, Version newVersion)
181 {
182 ArgumentNullException.ThrowIfNull(updateExecutor);
183 ArgumentNullException.ThrowIfNull(newVersion);
184
185 CheckSanity(true);
186
187 if (updatePath == null)
188 throw new InvalidOperationException("Tried to start update when server was initialized without an updatePath set!");
189
190 var logger = this.logger!;
191 logger.LogTrace("Begin ApplyUpdate...");
192
193 CancellationToken criticalCancellationToken;
194 lock (restartLock)
195 {
197 {
198 logger.LogDebug("Aborted update due to concurrency conflict!");
199 return false;
200 }
201
202 if (cancellationTokenSource == null)
203 throw new InvalidOperationException("Tried to update a non-running Server!");
204
205 criticalCancellationToken = cancellationTokenSource.Token;
206 UpdateInProgress = true;
207 }
208
209 async Task RunUpdate()
210 {
211 var updateExecutedSuccessfully = false;
212 try
213 {
214 updateExecutedSuccessfully = await updateExecutor.ExecuteUpdate(updatePath, criticalCancellationToken, criticalCancellationToken);
215 }
216 catch (OperationCanceledException ex)
217 {
218 logger.LogDebug(ex, "Update cancelled!");
219 UpdateInProgress = false;
220 }
221 catch (Exception ex)
222 {
223 logger.LogError(ex, "Update errored!");
224 UpdateInProgress = false;
225 }
226
227 if (updateExecutedSuccessfully)
228 {
229 logger.LogTrace("Update complete!");
230 await RestartImpl(newVersion, null, true, true);
231 }
232 else if (terminateIfUpdateFails)
233 {
234 logger.LogTrace("Stopping host due to termination request...");
236 }
237 else
238 {
239 logger.LogTrace("Update failed!");
240 UpdateInProgress = false;
241 }
242 }
243
244 updateTask = RunUpdate();
245 return true;
246 }
247
250 {
251 ArgumentNullException.ThrowIfNull(handler);
252
253 CheckSanity(false);
254
255 var logger = this.logger!;
256 lock (restartLock)
258 {
259 logger.LogTrace("Registering restart handler {handlerImplementationName}...", handler);
260 restartHandlers.Add(handler);
261 return new RestartRegistration(
262 new DisposeInvoker(() =>
263 {
264 lock (restartLock)
266 restartHandlers.Remove(handler);
267 }));
268 }
269
270 logger.LogWarning("Restart handler {handlerImplementationName} register after a shutdown had begun!", handler);
271 return new RestartRegistration(null);
272 }
273
275 public ValueTask Restart() => RestartImpl(null, null, true, true);
276
278 public ValueTask GracefulShutdown(bool detach) => RestartImpl(null, null, false, detach);
279
281 public ValueTask Die(Exception? exception)
282 {
283 if (exception != null)
284 return RestartImpl(null, exception, false, true);
285
287 return ValueTask.CompletedTask;
288 }
289
296 async ValueTask<bool> DumpGraphQLSchemaIfRequested(IServiceProvider services, CancellationToken cancellationToken)
297 {
298 var internalConfigurationOptions = services.GetRequiredService<IOptions<InternalConfiguration>>();
299 var apiDumpPath = internalConfigurationOptions.Value.DumpGraphQLApiPath;
300 if (String.IsNullOrWhiteSpace(apiDumpPath))
301 return false;
302
303 logger!.LogInformation("Dumping GraphQL API spec to {path} and exiting...", apiDumpPath);
304
305 // https://github.com/ChilliCream/graphql-platform/discussions/5885
306 var resolver = services.GetRequiredService<IRequestExecutorResolver>();
307 var executor = await resolver.GetRequestExecutorAsync(cancellationToken: cancellationToken);
308 var sdl = executor.Schema.Print();
309
310 var ioManager = services.GetRequiredService<IIOManager>();
311 await ioManager.WriteAllBytes(apiDumpPath, Encoding.UTF8.GetBytes(sdl), cancellationToken);
312 return true;
313 }
314
319 void CheckSanity(bool checkWatchdog)
320 {
321 if (checkWatchdog && !WatchdogPresent && propagatedException == null)
322 throw new InvalidOperationException("Server restarts are not supported");
323
324 if (cancellationTokenSource == null || logger == null)
325 throw new InvalidOperationException("Tried to control a non-running Server!");
326 }
327
333 {
334 if (propagatedException == null)
335 return;
336
337 if (otherException != null)
338 throw new AggregateException(propagatedException, otherException);
339
341 }
342
351 async ValueTask RestartImpl(Version? newVersion, Exception? exception, bool requireWatchdog, bool completeAsap)
352 {
353 CheckSanity(requireWatchdog);
354
355 // if the watchdog isn't required and there's no issue, this is just a graceful shutdown
356 bool isGracefulShutdown = !requireWatchdog && exception == null;
357 var logger = this.logger!;
358 logger.LogTrace(
359 "Begin {restartType}...",
360 isGracefulShutdown
361 ? completeAsap
362 ? "semi-graceful shutdown"
363 : "graceful shutdown"
364 : "restart");
365
366 lock (restartLock)
367 {
368 if ((UpdateInProgress && newVersion == null) || shutdownInProgress)
369 {
370 logger.LogTrace("Aborted restart due to concurrency conflict!");
371 return;
372 }
373
374 RestartRequested = !isGracefulShutdown;
375 propagatedException ??= exception;
376 }
377
378 if (exception == null)
379 {
380 var giveHandlersTimeToWaitAround = isGracefulShutdown && !completeAsap;
381 logger.LogInformation("Stopping server...");
382 using var cts = new CancellationTokenSource(
383 TimeSpan.FromMinutes(
384 giveHandlersTimeToWaitAround
387 var cancellationToken = cts.Token;
388 try
389 {
390 ValueTask eventsTask;
391 lock (restartLock)
392 eventsTask = ValueTaskExtensions.WhenAll(
394 .Select(
395 x => x.HandleRestart(newVersion, giveHandlersTimeToWaitAround, cancellationToken))
396 .ToList());
397
398 logger.LogTrace("Joining restart handlers...");
399 await eventsTask;
400 }
401 catch (OperationCanceledException ex)
402 {
403 if (isGracefulShutdown)
404 logger.LogWarning(ex, "Graceful shutdown timeout hit! Existing DreamDaemon processes will be terminated!");
405 else
406 logger.LogError(
407 ex,
408 "Restart timeout hit! Existing DreamDaemon processes will be lost and must be killed manually before being restarted with TGS!");
409 }
410 catch (Exception e)
411 {
412 logger.LogError(e, "Restart handlers error!");
413 }
414 }
415
417 }
418
424 void WatchForShutdownFileCreation(object sender, FileSystemEventArgs eventArgs)
425 {
426 logger?.LogTrace("FileSystemWatcher triggered.");
427
428 // TODO: Refactor this to not use System.IO function here.
429 if (eventArgs.FullPath == Path.GetFullPath(updatePath!) && File.Exists(eventArgs.FullPath))
430 {
431 logger?.LogInformation("Host watchdog appears to be requesting server termination!");
432 lock (restartLock)
433 {
434 if (!UpdateInProgress)
435 {
437 return;
438 }
439
441 }
442
443 logger?.LogInformation("An update is in progress, we will wait for that to complete...");
444 }
445 }
446
451 {
452 shutdownInProgress = true;
453 logger!.LogDebug("Stopping host...");
454 cancellationTokenSource!.Cancel();
455 }
456 }
457}
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:249
ValueTask Die(Exception? exception)
Kill the server with a fatal exception.A Task representing the running operation.
Definition Server.cs:281
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:119
bool TryStartUpdate(IServerUpdateExecutor updateExecutor, Version newVersion)
Attempt to update with a given updateExecutor .true if the update started successfully,...
Definition Server.cs:180
Task? updateTask
The Task that is used for asynchronously updating the server.
Definition Server.cs:89
async ValueTask RestartImpl(Version? newVersion, Exception? exception, bool requireWatchdog, bool completeAsap)
Implements Restart().
Definition Server.cs:351
void CheckSanity(bool checkWatchdog)
Throws an InvalidOperationException if the IServerControl cannot be used.
Definition Server.cs:319
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:54
CancellationTokenSource? cancellationTokenSource
The cancellationTokenSource for the Server.
Definition Server.cs:79
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:99
readonly? string updatePath
The absolute path to install updates to.
Definition Server.cs:59
ILogger< Server >? logger
The ILogger for the Server.
Definition Server.cs:69
void WatchForShutdownFileCreation(object sender, FileSystemEventArgs eventArgs)
Event handler for the updatePath's FileSystemWatcher. Triggers shutdown if requested by host watchdog...
Definition Server.cs:424
readonly IHostBuilder hostBuilder
The IHostBuilder for the Server.
Definition Server.cs:49
Exception? propagatedException
The Exception to propagate when the server terminates.
Definition Server.cs:84
GeneralConfiguration? generalConfiguration
The GeneralConfiguration for the Server.
Definition Server.cs:74
readonly object restartLock
lock object for certain restart related operations.
Definition Server.cs:64
bool shutdownInProgress
If the server is being shut down or restarted.
Definition Server.cs:94
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:296
Server(IHostBuilder hostBuilder, string? updatePath)
Initializes a new instance of the Server class.
Definition Server.cs:106
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:450
void CheckExceptionPropagation(Exception? otherException)
Re-throw propagatedException if it exists.
Definition Server.cs:332
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:13
ValueTask WriteAllBytes(string path, byte[] contents, CancellationToken cancellationToken)
Writes some contents to a file at path overwriting previous content.
Represents the host.
Definition IServer.cs:10