tgstation-server 5.12.7
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
15
17{
20 {
22 public bool RestartRequested { get; private set; }
23
25 public bool UpdateInProgress { get; private set; }
26
28 public bool WatchdogPresent =>
29#if WATCHDOG_FREE_RESTART
30 true;
31#else
32 updatePath != null;
33#endif
34
38 internal IHost Host { get; private set; }
39
43 readonly IHostBuilder hostBuilder;
44
48 readonly List<IRestartHandler> restartHandlers;
49
53 readonly string updatePath;
54
58 readonly object restartLock;
59
63 ILogger<Server> logger;
64
69
73 CancellationTokenSource cancellationTokenSource;
74
79
84
89
94
100 public Server(IHostBuilder hostBuilder, string updatePath)
101 {
102 this.hostBuilder = hostBuilder ?? throw new ArgumentNullException(nameof(hostBuilder));
103 this.updatePath = updatePath;
104
105 hostBuilder.ConfigureServices(serviceCollection => serviceCollection.AddSingleton<IServerControl>(this));
106
107 restartHandlers = new List<IRestartHandler>();
108 restartLock = new object();
109 }
110
112 public async Task Run(CancellationToken cancellationToken)
113 {
114 using (cancellationTokenSource = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken))
115 using (var fsWatcher = updatePath != null ? new FileSystemWatcher(Path.GetDirectoryName(updatePath)) : null)
116 {
117 if (fsWatcher != null)
118 {
119 fsWatcher.Created += WatchForShutdownFileCreation;
120 fsWatcher.EnableRaisingEvents = true;
121 }
122
123 try
124 {
125 using (Host = hostBuilder.Build())
126 {
127 try
128 {
129 logger = Host.Services.GetRequiredService<ILogger<Server>>();
130 using (cancellationToken.Register(() => logger.LogInformation("Server termination requested!")))
131 {
132 var generalConfigurationOptions = Host.Services.GetRequiredService<IOptions<GeneralConfiguration>>();
133 generalConfiguration = generalConfigurationOptions.Value;
134 await Host.RunAsync(cancellationTokenSource.Token);
135 }
136
137 if (updateTask != null)
138 await updateTask;
139 }
140 catch (OperationCanceledException ex)
141 {
142 logger.LogDebug(ex, "Server run cancelled!");
143 }
144 catch (Exception ex)
145 {
147 throw;
148 }
149 finally
150 {
151 logger = null;
152 }
153 }
154 }
155 finally
156 {
157 Host = null;
158 }
159 }
160
162 }
163
165 public bool TryStartUpdate(IServerUpdateExecutor updateExecutor, Version newVersion)
166 {
167 ArgumentNullException.ThrowIfNull(updateExecutor);
168 ArgumentNullException.ThrowIfNull(newVersion);
169
170 CheckSanity(true);
171
172 logger.LogTrace("Begin ApplyUpdate...");
173
174 CancellationToken criticalCancellationToken;
175 lock (restartLock)
176 {
178 {
179 logger.LogDebug("Aborted update due to concurrency conflict!");
180 return false;
181 }
182
183 if (cancellationTokenSource == null)
184 throw new InvalidOperationException("Tried to update a non-running Server!");
185
186 criticalCancellationToken = cancellationTokenSource.Token;
187 UpdateInProgress = true;
188 }
189
190 async Task RunUpdate()
191 {
192 if (await updateExecutor.ExecuteUpdate(updatePath, criticalCancellationToken, criticalCancellationToken))
193 {
194 logger.LogTrace("Update complete!");
195 await RestartImpl(newVersion, null, true, true);
196 }
197 else if (terminateIfUpdateFails)
198 {
199 logger.LogTrace("Stopping host due to termination request...");
201 }
202 else
203 {
204 logger.LogTrace("Update failed!");
205 UpdateInProgress = false;
206 }
207 }
208
209 updateTask = RunUpdate();
210 return true;
211 }
212
215 {
216 ArgumentNullException.ThrowIfNull(handler);
217
218 CheckSanity(false);
219
220 lock (restartLock)
222 {
223 logger.LogTrace("Registering restart handler {handlerImplementationName}...", handler);
224 restartHandlers.Add(handler);
225 return new RestartRegistration(() =>
226 {
227 lock (restartLock)
229 restartHandlers.Remove(handler);
230 });
231 }
232
233 logger.LogWarning("Restart handler {handlerImplementationName} register after a shutdown had begun!", handler);
234 return new RestartRegistration(null);
235 }
236
238 public Task Restart() => RestartImpl(null, null, true, true);
239
241 public Task GracefulShutdown(bool detach) => RestartImpl(null, null, false, detach);
242
244 public Task Die(Exception exception) => RestartImpl(null, exception, false, true);
245
250 void CheckSanity(bool checkWatchdog)
251 {
252 if (checkWatchdog && !WatchdogPresent && propagatedException == null)
253 throw new InvalidOperationException("Server restarts are not supported");
254
255 if (cancellationTokenSource == null || logger == null)
256 throw new InvalidOperationException("Tried to control a non-running Server!");
257 }
258
263 void CheckExceptionPropagation(Exception otherException)
264 {
265 if (propagatedException == null)
266 return;
267
268 if (otherException != null)
269 throw new AggregateException(propagatedException, otherException);
270
272 }
273
282 async Task RestartImpl(Version newVersion, Exception exception, bool requireWatchdog, bool completeAsap)
283 {
284 CheckSanity(requireWatchdog);
285
286 // if the watchdog isn't required and there's no issue, this is just a graceful shutdown
287 bool isGracefulShutdown = !requireWatchdog && exception == null;
288 logger.LogTrace(
289 "Begin {restartType}...",
290 isGracefulShutdown
291 ? completeAsap
292 ? "semi-graceful shutdown"
293 : "graceful shutdown"
294 : "restart");
295
296 lock (restartLock)
297 {
298 if ((UpdateInProgress && newVersion == null) || shutdownInProgress)
299 {
300 logger.LogTrace("Aborted restart due to concurrency conflict!");
301 return;
302 }
303
304 RestartRequested = !isGracefulShutdown;
305 propagatedException ??= exception;
306 }
307
308 if (exception == null)
309 {
310 var giveHandlersTimeToWaitAround = isGracefulShutdown && !completeAsap;
311 logger.LogInformation("Stopping server...");
312 using var cts = new CancellationTokenSource(
313 TimeSpan.FromMinutes(
314 giveHandlersTimeToWaitAround
317 var cancellationToken = cts.Token;
318 try
319 {
320 var eventsTask = Task.WhenAll(
321 restartHandlers.Select(
322 x => x.HandleRestart(newVersion, giveHandlersTimeToWaitAround, cancellationToken))
323 .ToList());
324
325 logger.LogTrace("Joining restart handlers...");
326 await eventsTask;
327 }
328 catch (OperationCanceledException ex)
329 {
330 if (isGracefulShutdown)
331 logger.LogWarning(ex, "Graceful shutdown timeout hit! Existing DreamDaemon processes will be terminated!");
332 else
333 logger.LogError(
334 ex,
335 "Restart timeout hit! Existing DreamDaemon processes will be lost and must be killed manually before being restarted with TGS!");
336 }
337 catch (Exception e)
338 {
339 logger.LogError(e, "Restart handlers error!");
340 }
341 }
342
344 }
345
351 void WatchForShutdownFileCreation(object sender, FileSystemEventArgs eventArgs)
352 {
353 logger?.LogTrace("FileSystemWatcher triggered.");
354
355 // TODO: Refactor this to not use System.IO function here.
356 if (eventArgs.FullPath == Path.GetFullPath(updatePath) && File.Exists(eventArgs.FullPath))
357 {
358 logger?.LogInformation("Host watchdog appears to be requesting server termination!");
359 lock (restartLock)
360 {
361 if (!UpdateInProgress)
362 {
364 return;
365 }
366
368 }
369
370 logger?.LogInformation("An update is in progress, we will wait for that to complete...");
371 }
372 }
373
378 {
379 shutdownInProgress = true;
380 logger.LogTrace("Stopping host...");
382 }
383 }
384}
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:28
Task updateTask
The Task that is used for asynchronously updating the server.
Definition: Server.cs:83
IRestartRegistration RegisterForRestart(IRestartHandler handler)
Register a given handler to run before stopping the server for a restart. A new IRestartRegistration...
Definition: Server.cs:214
bool UpdateInProgress
Whether or not the server is currently updating.
Definition: Server.cs:25
bool TryStartUpdate(IServerUpdateExecutor updateExecutor, Version newVersion)
Attempt to update with a given updateExecutor . true if the update started successfully,...
Definition: Server.cs:165
GeneralConfiguration generalConfiguration
The GeneralConfiguration for the Server.
Definition: Server.cs:68
readonly string updatePath
The absolute path to install updates to.
Definition: Server.cs:53
void CheckSanity(bool checkWatchdog)
Throws an InvalidOperationException if the IServerControl cannot be used.
Definition: Server.cs:250
async Task Run(CancellationToken cancellationToken)
Runs the IServer. A Task representing the running operation.
Definition: Server.cs:112
Exception propagatedException
The Exception to propagate when the server terminates.
Definition: Server.cs:78
readonly List< IRestartHandler > restartHandlers
The IRestartHandlers to run when the Server restarts.
Definition: Server.cs:48
CancellationTokenSource cancellationTokenSource
The cancellationTokenSource for the Server.
Definition: Server.cs:73
async Task RestartImpl(Version newVersion, Exception exception, bool requireWatchdog, bool completeAsap)
Implements Restart().
Definition: Server.cs:282
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:93
void WatchForShutdownFileCreation(object sender, FileSystemEventArgs eventArgs)
Event handler for the updatePath's FileSystemWatcher. Triggers shutdown if requested by host watchdog...
Definition: Server.cs:351
Task Restart()
Restarts the Host. A Task representing the running operation.
readonly IHostBuilder hostBuilder
The IHostBuilder for the Server.
Definition: Server.cs:43
void CheckExceptionPropagation(Exception otherException)
Re-throw propagatedException if it exists.
Definition: Server.cs:263
readonly object restartLock
lock object for certain restart related operations.
Definition: Server.cs:58
bool shutdownInProgress
If the server is being shut down or restarted.
Definition: Server.cs:88
Task GracefulShutdown(bool detach)
Gracefully shutsdown the Host. A Task representing the running operation.
bool RestartRequested
If the IServer should restart.
Definition: Server.cs:22
Task Die(Exception exception)
Kill the server with a fatal exception. A Task representing the running operation.
ILogger< Server > logger
The ILogger for the Server.
Definition: Server.cs:63
Server(IHostBuilder hostBuilder, string updatePath)
Initializes a new instance of the Server class.
Definition: Server.cs:100
void StopServerImmediate()
Fires off the cancellationTokenSource without any checks, shutting down everything.
Definition: Server.cs:377
Represents the lifetime of a IRestartHandler registration.
Represents a service that may take an updated Host assembly and run it, stopping the current assembly...
Task< 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