tgstation-server 6.11.1
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 if (await updateExecutor.ExecuteUpdate(updatePath, criticalCancellationToken, criticalCancellationToken))
212 {
213 logger.LogTrace("Update complete!");
214 await RestartImpl(newVersion, null, true, true);
215 }
216 else if (terminateIfUpdateFails)
217 {
218 logger.LogTrace("Stopping host due to termination request...");
220 }
221 else
222 {
223 logger.LogTrace("Update failed!");
224 UpdateInProgress = false;
225 }
226 }
227
228 updateTask = RunUpdate();
229 return true;
230 }
231
234 {
235 ArgumentNullException.ThrowIfNull(handler);
236
237 CheckSanity(false);
238
239 var logger = this.logger!;
240 lock (restartLock)
242 {
243 logger.LogTrace("Registering restart handler {handlerImplementationName}...", handler);
244 restartHandlers.Add(handler);
245 return new RestartRegistration(
246 new DisposeInvoker(() =>
247 {
248 lock (restartLock)
250 restartHandlers.Remove(handler);
251 }));
252 }
253
254 logger.LogWarning("Restart handler {handlerImplementationName} register after a shutdown had begun!", handler);
255 return new RestartRegistration(null);
256 }
257
259 public ValueTask Restart() => RestartImpl(null, null, true, true);
260
262 public ValueTask GracefulShutdown(bool detach) => RestartImpl(null, null, false, detach);
263
265 public ValueTask Die(Exception? exception)
266 {
267 if (exception != null)
268 return RestartImpl(null, exception, false, true);
269
271 return ValueTask.CompletedTask;
272 }
273
280 async ValueTask<bool> DumpGraphQLSchemaIfRequested(IServiceProvider services, CancellationToken cancellationToken)
281 {
282 var internalConfigurationOptions = services.GetRequiredService<IOptions<InternalConfiguration>>();
283 var apiDumpPath = internalConfigurationOptions.Value.DumpGraphQLApiPath;
284 if (String.IsNullOrWhiteSpace(apiDumpPath))
285 return false;
286
287 logger!.LogInformation("Dumping GraphQL API spec to {path} and exiting...", apiDumpPath);
288
289 // https://github.com/ChilliCream/graphql-platform/discussions/5885
290 var resolver = services.GetRequiredService<IRequestExecutorResolver>();
291 var executor = await resolver.GetRequestExecutorAsync(cancellationToken: cancellationToken);
292 var sdl = executor.Schema.Print();
293
294 var ioManager = services.GetRequiredService<IIOManager>();
295 await ioManager.WriteAllBytes(apiDumpPath, Encoding.UTF8.GetBytes(sdl), cancellationToken);
296 return true;
297 }
298
303 void CheckSanity(bool checkWatchdog)
304 {
305 if (checkWatchdog && !WatchdogPresent && propagatedException == null)
306 throw new InvalidOperationException("Server restarts are not supported");
307
308 if (cancellationTokenSource == null || logger == null)
309 throw new InvalidOperationException("Tried to control a non-running Server!");
310 }
311
317 {
318 if (propagatedException == null)
319 return;
320
321 if (otherException != null)
322 throw new AggregateException(propagatedException, otherException);
323
325 }
326
335 async ValueTask RestartImpl(Version? newVersion, Exception? exception, bool requireWatchdog, bool completeAsap)
336 {
337 CheckSanity(requireWatchdog);
338
339 // if the watchdog isn't required and there's no issue, this is just a graceful shutdown
340 bool isGracefulShutdown = !requireWatchdog && exception == null;
341 var logger = this.logger!;
342 logger.LogTrace(
343 "Begin {restartType}...",
344 isGracefulShutdown
345 ? completeAsap
346 ? "semi-graceful shutdown"
347 : "graceful shutdown"
348 : "restart");
349
350 lock (restartLock)
351 {
352 if ((UpdateInProgress && newVersion == null) || shutdownInProgress)
353 {
354 logger.LogTrace("Aborted restart due to concurrency conflict!");
355 return;
356 }
357
358 RestartRequested = !isGracefulShutdown;
359 propagatedException ??= exception;
360 }
361
362 if (exception == null)
363 {
364 var giveHandlersTimeToWaitAround = isGracefulShutdown && !completeAsap;
365 logger.LogInformation("Stopping server...");
366 using var cts = new CancellationTokenSource(
367 TimeSpan.FromMinutes(
368 giveHandlersTimeToWaitAround
371 var cancellationToken = cts.Token;
372 try
373 {
374 ValueTask eventsTask;
375 lock (restartLock)
376 eventsTask = ValueTaskExtensions.WhenAll(
378 .Select(
379 x => x.HandleRestart(newVersion, giveHandlersTimeToWaitAround, cancellationToken))
380 .ToList());
381
382 logger.LogTrace("Joining restart handlers...");
383 await eventsTask;
384 }
385 catch (OperationCanceledException ex)
386 {
387 if (isGracefulShutdown)
388 logger.LogWarning(ex, "Graceful shutdown timeout hit! Existing DreamDaemon processes will be terminated!");
389 else
390 logger.LogError(
391 ex,
392 "Restart timeout hit! Existing DreamDaemon processes will be lost and must be killed manually before being restarted with TGS!");
393 }
394 catch (Exception e)
395 {
396 logger.LogError(e, "Restart handlers error!");
397 }
398 }
399
401 }
402
408 void WatchForShutdownFileCreation(object sender, FileSystemEventArgs eventArgs)
409 {
410 logger?.LogTrace("FileSystemWatcher triggered.");
411
412 // TODO: Refactor this to not use System.IO function here.
413 if (eventArgs.FullPath == Path.GetFullPath(updatePath!) && File.Exists(eventArgs.FullPath))
414 {
415 logger?.LogInformation("Host watchdog appears to be requesting server termination!");
416 lock (restartLock)
417 {
418 if (!UpdateInProgress)
419 {
421 return;
422 }
423
425 }
426
427 logger?.LogInformation("An update is in progress, we will wait for that to complete...");
428 }
429 }
430
435 {
436 shutdownInProgress = true;
437 logger!.LogDebug("Stopping host...");
438 cancellationTokenSource!.Cancel();
439 }
440 }
441}
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:233
ValueTask Die(Exception? exception)
Kill the server with a fatal exception.A Task representing the running operation.
Definition Server.cs:265
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:335
void CheckSanity(bool checkWatchdog)
Throws an InvalidOperationException if the IServerControl cannot be used.
Definition Server.cs:303
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:408
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:280
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:434
void CheckExceptionPropagation(Exception? otherException)
Re-throw propagatedException if it exists.
Definition Server.cs:316
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