tgstation-server  4.3.2
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 => updatePath != null;
25 
29  readonly IHostBuilder hostBuilder;
30 
34  readonly List<IRestartHandler> restartHandlers;
35 
39  readonly string updatePath;
40 
44  readonly object restartLock;
45 
49  ILogger<Server> logger;
50 
55 
59  CancellationTokenSource cancellationTokenSource;
60 
65 
69  bool updating;
70 
76  public Server(IHostBuilder hostBuilder, string updatePath)
77  {
78  this.hostBuilder = hostBuilder ?? throw new ArgumentNullException(nameof(hostBuilder));
79  this.updatePath = updatePath;
80 
81  hostBuilder.ConfigureServices(serviceCollection => serviceCollection.AddSingleton<IServerControl>(this));
82 
83  restartHandlers = new List<IRestartHandler>();
84  restartLock = new object();
85  }
86 
91  void CheckSanity(bool checkWatchdog)
92  {
93  if (checkWatchdog && !WatchdogPresent && propagatedException == null)
94  throw new InvalidOperationException("Server restarts are not supported");
95 
96  if (cancellationTokenSource == null || logger == null)
97  throw new InvalidOperationException("Tried to control a non-running Server!");
98  }
99 
104  void CheckExceptionPropagation(Exception otherException)
105  {
106  if (propagatedException == null)
107  return;
108 
109  if (otherException != null)
110  throw new AggregateException(propagatedException, otherException);
111 
112  throw propagatedException;
113  }
114 
116  public async Task Run(CancellationToken cancellationToken)
117  {
118  using (cancellationTokenSource = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken))
119  using (var fsWatcher = updatePath != null ? new FileSystemWatcher(Path.GetDirectoryName(updatePath)) : null)
120  {
121  if (fsWatcher != null)
122  {
123  fsWatcher.Created += (a, b) =>
124  {
125  if (b.FullPath == updatePath && File.Exists(b.FullPath))
126  {
127  if (logger != null)
128  logger.LogInformation("Host watchdog appears to be requesting server termination!");
129  cancellationTokenSource.Cancel();
130  }
131  };
132  fsWatcher.EnableRaisingEvents = true;
133  }
134 
135  using var host = hostBuilder.Build();
136  try
137  {
138  logger = host.Services.GetRequiredService<ILogger<Server>>();
139  using (cancellationToken.Register(() => logger.LogInformation("Server termination requested!")))
140  {
141  var generalConfigurationOptions = host.Services.GetRequiredService<IOptions<GeneralConfiguration>>();
142  generalConfiguration = generalConfigurationOptions.Value;
143  await host.RunAsync(cancellationTokenSource.Token).ConfigureAwait(false);
144  }
145  }
146  catch (Exception ex)
147  {
148  CheckExceptionPropagation(ex);
149  throw;
150  }
151  }
152 
153  CheckExceptionPropagation(null);
154  }
155 
157  public bool ApplyUpdate(Version version, Uri updateZipUrl, IIOManager ioManager)
158  {
159  if (version == null)
160  throw new ArgumentNullException(nameof(version));
161  if (updateZipUrl == null)
162  throw new ArgumentNullException(nameof(updateZipUrl));
163  if (ioManager == null)
164  throw new ArgumentNullException(nameof(ioManager));
165 
166  CheckSanity(true);
167 
168  logger.LogTrace("Begin ApplyUpdate...");
169 
170  lock (restartLock)
171  {
172  if (updating || RestartRequested)
173  {
174  logger.LogTrace("Aborted due to concurrency conflict!");
175  return false;
176  }
177 
178  updating = true;
179  }
180 
181  async void RunUpdate()
182  {
183  try
184  {
185  logger.LogInformation("Updating server to version {0} ({1})...", version, updateZipUrl);
186 
187  if (cancellationTokenSource == null)
188  throw new InvalidOperationException("Tried to update a non-running Server!");
189  var cancellationToken = cancellationTokenSource.Token;
190 
191  logger.LogTrace("Downloading zip package...");
192  var updateZipData = await ioManager.DownloadFile(updateZipUrl, cancellationToken).ConfigureAwait(false);
193 
194  try
195  {
196  logger.LogTrace("Exctracting zip package to {0}...", updatePath);
197  await ioManager.ZipToDirectory(updatePath, updateZipData, cancellationToken).ConfigureAwait(false);
198  }
199  catch (Exception e)
200  {
201  updating = false;
202  try
203  {
204  // important to not leave this directory around if possible
205  await ioManager.DeleteDirectory(updatePath, default).ConfigureAwait(false);
206  }
207  catch (Exception e2)
208  {
209  throw new AggregateException(e, e2);
210  }
211 
212  throw;
213  }
214 
215  await Restart(version, null, true).ConfigureAwait(false);
216  }
217  catch (OperationCanceledException)
218  {
219  logger.LogInformation("Server update cancelled!");
220  }
221  catch (Exception e)
222  {
223  logger.LogError("Error updating server! Exception: {0}", e);
224  }
225  finally
226  {
227  updating = false;
228  }
229  }
230 
231  RunUpdate();
232  return true;
233  }
234 
237  {
238  if (handler == null)
239  throw new ArgumentNullException(nameof(handler));
240 
241  CheckSanity(false);
242 
243  lock (restartLock)
244  if (!RestartRequested)
245  {
246  logger.LogTrace("Registering restart handler {0}...", handler);
247  restartHandlers.Add(handler);
248  return new RestartRegistration(() =>
249  {
250  lock (restartLock)
251  if (!RestartRequested)
252  restartHandlers.Remove(handler);
253  });
254  }
255 
256  return new RestartRegistration(() => { });
257  }
258 
260  public Task Restart() => Restart(null, null, true);
261 
269  async Task Restart(Version newVersion, Exception exception, bool requireWatchdog)
270  {
271  CheckSanity(requireWatchdog);
272 
273  logger.LogTrace("Begin Restart...");
274 
275  lock (restartLock)
276  {
277  if ((updating && newVersion == null) || RestartRequested)
278  {
279  logger.LogTrace("Aborted due to concurrency conflict!");
280  return;
281  }
282 
283  RestartRequested = true;
284  propagatedException = exception;
285  }
286 
287  if (exception == null)
288  using (var cts = new CancellationTokenSource())
289  {
290  logger.LogInformation("Restarting server...");
291  var cancellationToken = cts.Token;
292  var eventsTask = Task.WhenAll(restartHandlers.Select(x => x.HandleRestart(newVersion, cancellationToken)).ToList());
293 
294  var expiryTask = Task.Delay(TimeSpan.FromMilliseconds(generalConfiguration.RestartTimeout));
295  await Task.WhenAny(eventsTask, expiryTask).ConfigureAwait(false);
296  logger.LogTrace("Joining restart handlers...");
297  cts.Cancel();
298  try
299  {
300  await eventsTask.ConfigureAwait(false);
301  }
302  catch (OperationCanceledException)
303  {
304  logger.LogError("Restart timeout hit! Existing DreamDaemon processes will be lost and must be killed manually before being restarted with TGS!");
305  }
306  catch (Exception e)
307  {
308  logger.LogError("Restart handlers error! Exception: {0}", e);
309  }
310  }
311 
312  logger.LogTrace("Stopping host...");
313  cancellationTokenSource.Cancel();
314  }
315 
317  public Task Die(Exception exception) => Restart(null, exception, false);
318  }
319 }
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:49
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:157
readonly List< IRestartHandler > restartHandlers
The IRestartHandlers to run when the Server restarts
Definition: Server.cs:34
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:236
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:269
async Task Run(CancellationToken cancellationToken)
Runs the IServer
Definition: Server.cs:116
readonly object restartLock
object for certain restart related operations.
Definition: Server.cs:44
GeneralConfiguration generalConfiguration
The GeneralConfiguration for the Server
Definition: Server.cs:54
void CheckExceptionPropagation(Exception otherException)
Re-throw propagatedException if it exists
Definition: Server.cs:104
int RestartTimeout
The timeout milliseconds for restarting the server
readonly IHostBuilder hostBuilder
The IHostBuilder for the Server
Definition: Server.cs:29
readonly string updatePath
The absolute path to install updates to
Definition: Server.cs:39
Represents the host
Definition: IServer.cs:9
Server(IHostBuilder hostBuilder, string updatePath)
Construct a Server
Definition: Server.cs:76
CancellationTokenSource cancellationTokenSource
The cancellationTokenSource for the Server
Definition: Server.cs:59
bool updating
If a server update has been or is being applied
Definition: Server.cs:69
Exception propagatedException
The Exception to propagate when the server terminates
Definition: Server.cs:64
Interface for using filesystems
Definition: IIOManager.cs:11
Represents the lifetime of a IRestartHandler registration
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:91