tgstation-server  4.4.0
The /tg/station 13 server suite
Server.cs
Go to the documentation of this file.
1 using Microsoft.Extensions.DependencyInjection;
2 using Microsoft.Extensions.Hosting;
3 using Microsoft.Extensions.Logging;
4 using Microsoft.Extensions.Options;
5 using System;
6 using System.Collections.Generic;
7 using System.IO;
8 using System.Linq;
9 using System.Threading;
10 using System.Threading.Tasks;
13 using Tgstation.Server.Host.IO;
14 
15 namespace Tgstation.Server.Host
16 {
18  sealed class Server : IServer, IServerControl
19  {
21  public bool RestartRequested { get; private set; }
22 
24  public bool WatchdogPresent =>
25 #if WATCHDOG_FREE_RESTART
26  true;
27 #else
28  updatePath != null;
29 #endif
30 
34  readonly IHostBuilder hostBuilder;
35 
39  readonly List<IRestartHandler> restartHandlers;
40 
44  readonly string updatePath;
45 
49  readonly object restartLock;
50 
54  ILogger<Server> logger;
55 
60 
64  CancellationTokenSource cancellationTokenSource;
65 
70 
74  bool updating;
75 
81  public Server(IHostBuilder hostBuilder, string updatePath)
82  {
83  this.hostBuilder = hostBuilder ?? throw new ArgumentNullException(nameof(hostBuilder));
84  this.updatePath = updatePath;
85 
86  hostBuilder.ConfigureServices(serviceCollection => serviceCollection.AddSingleton<IServerControl>(this));
87 
88  restartHandlers = new List<IRestartHandler>();
89  restartLock = new object();
90  }
91 
96  void CheckSanity(bool checkWatchdog)
97  {
98  if (checkWatchdog && !WatchdogPresent && propagatedException == null)
99  throw new InvalidOperationException("Server restarts are not supported");
100 
101  if (cancellationTokenSource == null || logger == null)
102  throw new InvalidOperationException("Tried to control a non-running Server!");
103  }
104 
109  void CheckExceptionPropagation(Exception otherException)
110  {
111  if (propagatedException == null)
112  return;
113 
114  if (otherException != null)
115  throw new AggregateException(propagatedException, otherException);
116 
117  throw propagatedException;
118  }
119 
121  public async Task Run(CancellationToken cancellationToken)
122  {
123  using (cancellationTokenSource = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken))
124  using (var fsWatcher = updatePath != null ? new FileSystemWatcher(Path.GetDirectoryName(updatePath)) : null)
125  {
126  if (fsWatcher != null)
127  {
128  fsWatcher.Created += (a, b) =>
129  {
130  if (b.FullPath == updatePath && File.Exists(b.FullPath))
131  {
132  if (logger != null)
133  logger.LogInformation("Host watchdog appears to be requesting server termination!");
134  cancellationTokenSource.Cancel();
135  }
136  };
137  fsWatcher.EnableRaisingEvents = true;
138  }
139 
140  using var host = hostBuilder.Build();
141  try
142  {
143  logger = host.Services.GetRequiredService<ILogger<Server>>();
144  using (cancellationToken.Register(() => logger.LogInformation("Server termination requested!")))
145  {
146  var generalConfigurationOptions = host.Services.GetRequiredService<IOptions<GeneralConfiguration>>();
147  generalConfiguration = generalConfigurationOptions.Value;
148  await host.RunAsync(cancellationTokenSource.Token).ConfigureAwait(false);
149  }
150  }
151  catch (Exception ex)
152  {
153  CheckExceptionPropagation(ex);
154  throw;
155  }
156  }
157 
158  CheckExceptionPropagation(null);
159  }
160 
162  public bool ApplyUpdate(Version version, Uri updateZipUrl, IIOManager ioManager)
163  {
164  if (version == null)
165  throw new ArgumentNullException(nameof(version));
166  if (updateZipUrl == null)
167  throw new ArgumentNullException(nameof(updateZipUrl));
168  if (ioManager == null)
169  throw new ArgumentNullException(nameof(ioManager));
170 
171  CheckSanity(true);
172 
173  logger.LogTrace("Begin ApplyUpdate...");
174 
175  lock (restartLock)
176  {
177  if (updating || RestartRequested)
178  {
179  logger.LogTrace("Aborted due to concurrency conflict!");
180  return false;
181  }
182 
183  updating = true;
184  }
185 
186  async void RunUpdate()
187  {
188  try
189  {
190  logger.LogInformation("Updating server to version {0} ({1})...", version, updateZipUrl);
191 
192  if (cancellationTokenSource == null)
193  throw new InvalidOperationException("Tried to update a non-running Server!");
194  var cancellationToken = cancellationTokenSource.Token;
195 
196  logger.LogTrace("Downloading zip package...");
197  var updateZipData = await ioManager.DownloadFile(updateZipUrl, cancellationToken).ConfigureAwait(false);
198 
199  try
200  {
201  logger.LogTrace("Exctracting zip package to {0}...", updatePath);
202  await ioManager.ZipToDirectory(updatePath, updateZipData, cancellationToken).ConfigureAwait(false);
203  }
204  catch (Exception e)
205  {
206  updating = false;
207  try
208  {
209  // important to not leave this directory around if possible
210  await ioManager.DeleteDirectory(updatePath, default).ConfigureAwait(false);
211  }
212  catch (Exception e2)
213  {
214  throw new AggregateException(e, e2);
215  }
216 
217  throw;
218  }
219 
220  await Restart(version, null, true).ConfigureAwait(false);
221  }
222  catch (OperationCanceledException)
223  {
224  logger.LogInformation("Server update cancelled!");
225  }
226  catch (Exception e)
227  {
228  logger.LogError("Error updating server! Exception: {0}", e);
229  }
230  finally
231  {
232  updating = false;
233  }
234  }
235 
236  RunUpdate();
237  return true;
238  }
239 
242  {
243  if (handler == null)
244  throw new ArgumentNullException(nameof(handler));
245 
246  CheckSanity(false);
247 
248  lock (restartLock)
249  if (!RestartRequested)
250  {
251  logger.LogTrace("Registering restart handler {0}...", handler);
252  restartHandlers.Add(handler);
253  return new RestartRegistration(() =>
254  {
255  lock (restartLock)
256  if (!RestartRequested)
257  restartHandlers.Remove(handler);
258  });
259  }
260 
261  return new RestartRegistration(() => { });
262  }
263 
265  public Task Restart() => Restart(null, null, true);
266 
274  async Task Restart(Version newVersion, Exception exception, bool requireWatchdog)
275  {
276  CheckSanity(requireWatchdog);
277 
278  logger.LogTrace("Begin Restart...");
279 
280  lock (restartLock)
281  {
282  if ((updating && newVersion == null) || RestartRequested)
283  {
284  logger.LogTrace("Aborted due to concurrency conflict!");
285  return;
286  }
287 
288  RestartRequested = true;
289  propagatedException = exception;
290  }
291 
292  if (exception == null)
293  using (var cts = new CancellationTokenSource())
294  {
295  logger.LogInformation("Restarting server...");
296  var cancellationToken = cts.Token;
297  var eventsTask = Task.WhenAll(restartHandlers.Select(x => x.HandleRestart(newVersion, cancellationToken)).ToList());
298 
299  var expiryTask = Task.Delay(TimeSpan.FromMilliseconds(generalConfiguration.RestartTimeout));
300  await Task.WhenAny(eventsTask, expiryTask).ConfigureAwait(false);
301  logger.LogTrace("Joining restart handlers...");
302  cts.Cancel();
303  try
304  {
305  await eventsTask.ConfigureAwait(false);
306  }
307  catch (OperationCanceledException)
308  {
309  logger.LogError("Restart timeout hit! Existing DreamDaemon processes will be lost and must be killed manually before being restarted with TGS!");
310  }
311  catch (Exception e)
312  {
313  logger.LogError("Restart handlers error! Exception: {0}", e);
314  }
315  }
316 
317  logger.LogTrace("Stopping host...");
318  cancellationTokenSource.Cancel();
319  }
320 
322  public Task Die(Exception exception) => Restart(null, exception, false);
323  }
324 }
Task DeleteDirectory(string path, CancellationToken cancellationToken)
Recursively delete a directory, removes and does not enter any symlinks encounterd.
ILogger< Server > logger
The ILogger for the Server
Definition: Server.cs:54
bool ApplyUpdate(Version version, Uri updateZipUrl, IIOManager ioManager)
Run a new Host assembly and stop the current one. This will likely trigger all active CancellationTok...
Definition: Server.cs:162
readonly List< IRestartHandler > restartHandlers
The IRestartHandlers to run when the Server restarts
Definition: Server.cs:39
Task< byte[]> DownloadFile(Uri url, CancellationToken cancellationToken)
Downloads a file from url
IRestartRegistration RegisterForRestart(IRestartHandler handler)
Register a given handler to run before stopping the server for a restart
Definition: Server.cs:241
Task ZipToDirectory(string path, byte[] zipFileBytes, CancellationToken cancellationToken)
Extract a set of zipFileBytes to a given path
async Task Restart(Version newVersion, Exception exception, bool requireWatchdog)
Implements Restart()
Definition: Server.cs:274
async Task Run(CancellationToken cancellationToken)
Runs the IServer
Definition: Server.cs:121
readonly object restartLock
object for certain restart related operations.
Definition: Server.cs:49
GeneralConfiguration generalConfiguration
The GeneralConfiguration for the Server
Definition: Server.cs:59
void CheckExceptionPropagation(Exception otherException)
Re-throw propagatedException if it exists
Definition: Server.cs:109
readonly IHostBuilder hostBuilder
The IHostBuilder for the Server
Definition: Server.cs:34
readonly string updatePath
The absolute path to install updates to
Definition: Server.cs:44
Represents the host
Definition: IServer.cs:9
Server(IHostBuilder hostBuilder, string updatePath)
Construct a Server
Definition: Server.cs:81
CancellationTokenSource cancellationTokenSource
The cancellationTokenSource for the Server
Definition: Server.cs:64
bool updating
If a server update has been or is being applied
Definition: Server.cs:74
Exception propagatedException
The Exception to propagate when the server terminates
Definition: Server.cs:69
Interface for using filesystems
Definition: IIOManager.cs:11
Represents the lifetime of a IRestartHandler registration
uint RestartTimeout
The timeout milliseconds for restarting the server
Represents a service that may take an updated Host assembly and run it, stopping the current assembly...
void CheckSanity(bool checkWatchdog)
Throws an InvalidOperationException if the IServerControl cannot be used
Definition: Server.cs:96