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 IOptionsMonitor<GeneralConfiguration>? generalConfigurationOptions;
75
79 ILogger<Server>? logger;
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 generalConfigurationOptions = Host.Services.GetRequiredService<IOptionsMonitor<GeneralConfiguration>>();
155 await Host.RunAsync(cancellationTokenSource.Token);
156 }
157
158 if (updateTask != null)
159 await updateTask;
160 }
161 catch (OperationCanceledException ex)
162 {
163 logger.LogDebug(ex, "Server run cancelled!");
164 }
165 catch (Exception ex)
166 {
168 throw;
169 }
170 finally
171 {
172 logger = null;
173 }
174 }
175 }
176 finally
177 {
178 Host = null;
179 }
180 }
181
183 }
184
186 public bool TryStartUpdate(IServerUpdateExecutor updateExecutor, Version newVersion)
187 {
188 ArgumentNullException.ThrowIfNull(updateExecutor);
189 ArgumentNullException.ThrowIfNull(newVersion);
190
191 CheckSanity(true);
192
193 if (updatePath == null)
194 throw new InvalidOperationException("Tried to start update when server was initialized without an updatePath set!");
195
196 var logger = this.logger!;
197 logger.LogTrace("Begin ApplyUpdate...");
198
199 CancellationToken criticalCancellationToken;
200 lock (restartLock)
201 {
203 {
204 logger.LogDebug("Aborted update due to concurrency conflict!");
205 return false;
206 }
207
208 if (cancellationTokenSource == null)
209 throw new InvalidOperationException("Tried to update a non-running Server!");
210
211 criticalCancellationToken = cancellationTokenSource.Token;
212 UpdateInProgress = true;
213 }
214
215 async Task RunUpdate()
216 {
217 var updateExecutedSuccessfully = false;
218 try
219 {
220 updateExecutedSuccessfully = await updateExecutor.ExecuteUpdate(updatePath, criticalCancellationToken, criticalCancellationToken);
221 }
222 catch (OperationCanceledException ex)
223 {
224 logger.LogDebug(ex, "Update cancelled!");
225 UpdateInProgress = false;
226 }
227 catch (Exception ex)
228 {
229 logger.LogError(ex, "Update errored!");
230 UpdateInProgress = false;
231 }
232
233 if (updateExecutedSuccessfully)
234 {
235 logger.LogTrace("Update complete!");
236 await RestartImpl(newVersion, null, true, true);
237 }
238 else if (terminateIfUpdateFails)
239 {
240 logger.LogTrace("Stopping host due to termination request...");
242 }
243 else
244 {
245 logger.LogTrace("Update failed!");
246 UpdateInProgress = false;
247 }
248 }
249
250 updateTask = RunUpdate();
251 return true;
252 }
253
256 {
257 ArgumentNullException.ThrowIfNull(handler);
258
259 CheckSanity(false);
260
261 var logger = this.logger!;
262 lock (restartLock)
264 {
265 logger.LogTrace("Registering restart handler {handlerImplementationName}...", handler);
266 restartHandlers.Add(handler);
267 return new RestartRegistration(
268 new DisposeInvoker(() =>
269 {
270 lock (restartLock)
272 restartHandlers.Remove(handler);
273 }));
274 }
275
276 logger.LogWarning("Restart handler {handlerImplementationName} register after a shutdown had begun!", handler);
277 return new RestartRegistration(null);
278 }
279
281 public ValueTask Restart() => RestartImpl(null, null, true, true);
282
284 public ValueTask GracefulShutdown(bool detach) => RestartImpl(null, null, false, detach);
285
287 public ValueTask Die(Exception? exception)
288 {
289 if (exception != null)
290 return RestartImpl(null, exception, false, true);
291
293 return ValueTask.CompletedTask;
294 }
295
302 async ValueTask<bool> DumpGraphQLSchemaIfRequested(IServiceProvider services, CancellationToken cancellationToken)
303 {
304 var internalConfigurationOptions = services.GetRequiredService<IOptions<InternalConfiguration>>();
305 var apiDumpPath = internalConfigurationOptions.Value.DumpGraphQLApiPath;
306 if (String.IsNullOrWhiteSpace(apiDumpPath))
307 return false;
308
309 logger!.LogInformation("Dumping GraphQL API spec to {path} and exiting...", apiDumpPath);
310
311 // https://github.com/ChilliCream/graphql-platform/discussions/5885
312 var resolver = services.GetRequiredService<IRequestExecutorResolver>();
313 var executor = await resolver.GetRequestExecutorAsync(cancellationToken: cancellationToken);
314 var sdl = executor.Schema.Print();
315
316 var ioManager = services.GetRequiredService<IIOManager>();
317 await ioManager.WriteAllBytes(apiDumpPath, Encoding.UTF8.GetBytes(sdl), cancellationToken);
318 return true;
319 }
320
325 void CheckSanity(bool checkWatchdog)
326 {
327 if (checkWatchdog && !WatchdogPresent && propagatedException == null)
328 throw new InvalidOperationException("Server restarts are not supported");
329
330 if (cancellationTokenSource == null || logger == null)
331 throw new InvalidOperationException("Tried to control a non-running Server!");
332 }
333
339 {
340 if (propagatedException == null)
341 return;
342
343 if (otherException != null)
344 throw new AggregateException(propagatedException, otherException);
345
347 }
348
357 async ValueTask RestartImpl(Version? newVersion, Exception? exception, bool requireWatchdog, bool completeAsap)
358 {
359 CheckSanity(requireWatchdog);
360
361 // if the watchdog isn't required and there's no issue, this is just a graceful shutdown
362 bool isGracefulShutdown = !requireWatchdog && exception == null;
363 var logger = this.logger!;
364 logger.LogTrace(
365 "Begin {restartType}...",
366 isGracefulShutdown
367 ? completeAsap
368 ? "semi-graceful shutdown"
369 : "graceful shutdown"
370 : "restart");
371
372 lock (restartLock)
373 {
374 if ((UpdateInProgress && newVersion == null) || shutdownInProgress)
375 {
376 logger.LogTrace("Aborted restart due to concurrency conflict!");
377 return;
378 }
379
380 RestartRequested = !isGracefulShutdown;
381 propagatedException ??= exception;
382 }
383
384 if (exception == null)
385 {
386 var giveHandlersTimeToWaitAround = isGracefulShutdown && !completeAsap;
387 logger.LogInformation("Stopping server...");
388 using var cts = new CancellationTokenSource(
389 TimeSpan.FromMinutes(
390 giveHandlersTimeToWaitAround
391 ? generalConfigurationOptions!.CurrentValue.ShutdownTimeoutMinutes
392 : generalConfigurationOptions!.CurrentValue.RestartTimeoutMinutes));
393 var cancellationToken = cts.Token;
394 try
395 {
396 ValueTask eventsTask;
397 lock (restartLock)
398 eventsTask = ValueTaskExtensions.WhenAll(
400 .Select(
401 x => x.HandleRestart(newVersion, giveHandlersTimeToWaitAround, cancellationToken))
402 .ToList());
403
404 logger.LogTrace("Joining restart handlers...");
405 await eventsTask;
406 }
407 catch (OperationCanceledException ex)
408 {
409 if (isGracefulShutdown)
410 logger.LogWarning(ex, "Graceful shutdown timeout hit! Existing DreamDaemon processes will be terminated!");
411 else
412 logger.LogError(
413 ex,
414 "Restart timeout hit! Existing DreamDaemon processes will be lost and must be killed manually before being restarted with TGS!");
415 }
416 catch (Exception e)
417 {
418 logger.LogError(e, "Restart handlers error!");
419 }
420 }
421
423 }
424
430 async void WatchForShutdownFileCreation(object sender, FileSystemEventArgs eventArgs)
431 {
432 logger?.LogTrace("FileSystemWatcher triggered.");
433
434 // DCT: None available
435 if (eventArgs.FullPath == ioManager.ResolvePath(updatePath!) && await ioManager.FileExists(eventArgs.FullPath, CancellationToken.None))
436 {
437 logger?.LogInformation("Host watchdog appears to be requesting server termination!");
438 lock (restartLock)
439 {
440 if (!UpdateInProgress)
441 {
443 return;
444 }
445
447 }
448
449 logger?.LogInformation("An update is in progress, we will wait for that to complete...");
450 }
451 }
452
457 {
458 shutdownInProgress = true;
459 logger!.LogDebug("Stopping host...");
460 cancellationTokenSource!.Cancel();
461 }
462 }
463}
Extension methods for the ValueTask and ValueTask<TResult> classes.
static async ValueTask WhenAll(IEnumerable< ValueTask > tasks)
Fully await a given list of tasks .
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:255
ValueTask Die(Exception? exception)
Kill the server with a fatal exception.A Task representing the running operation.
Definition Server.cs:287
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:186
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:357
void CheckSanity(bool checkWatchdog)
Throws an InvalidOperationException if the IServerControl cannot be used.
Definition Server.cs:325
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:430
readonly IIOManager ioManager
The IIOManager to use.
Definition Server.cs:49
ValueTask Restart()
Restarts the Host.A ValueTask representing the running operation.
IOptionsMonitor< GeneralConfiguration >? generalConfigurationOptions
The IOptionsMonitor<TOptions> of GeneralConfiguration for the Server.
Definition Server.cs:74
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:79
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
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:302
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:456
void CheckExceptionPropagation(Exception? otherException)
Re-throw propagatedException if it exists.
Definition Server.cs:338
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