tgstation-server
The /tg/station 13 server suite
Server.cs
Go to the documentation of this file.
1 using Microsoft.AspNetCore.Hosting;
2 using Microsoft.Extensions.DependencyInjection;
3 using Microsoft.Extensions.Logging;
4 using System;
5 using System.Collections.Generic;
6 using System.IO;
7 using System.Linq;
8 using System.Threading;
9 using System.Threading.Tasks;
11 using Tgstation.Server.Host.IO;
12 
13 namespace Tgstation.Server.Host
14 {
16 #pragma warning disable CA1001 // Types that own disposable fields should be disposable
17  sealed class Server : IServer, IServerControl
18 #pragma warning restore CA1001 // Types that own disposable fields should be disposable
19  {
21  public bool RestartRequested { get; private set; }
22 
24  public bool WatchdogPresent => updatePath != null;
25 
29  readonly IWebHostBuilder webHostBuilder;
30 
34  readonly List<IRestartHandler> restartHandlers;
35 
39  readonly string updatePath;
40 
44  ILogger<Server> logger;
45 
49  CancellationTokenSource cancellationTokenSource;
50 
55 
59  bool updating;
60 
66  public Server(IWebHostBuilder webHostBuilder, string updatePath)
67  {
68  this.webHostBuilder = webHostBuilder ?? throw new ArgumentNullException(nameof(webHostBuilder));
69  this.updatePath = updatePath;
70 
71  webHostBuilder.ConfigureServices(serviceCollection => serviceCollection.AddSingleton<IServerControl>(this));
72 
73  restartHandlers = new List<IRestartHandler>();
74  }
75 
80  void CheckSanity(bool checkWatchdog)
81  {
82  if (checkWatchdog && !WatchdogPresent && propagatedException == null)
83  throw new InvalidOperationException("Server restarts are not supported");
84 
85  if (cancellationTokenSource == null || logger == null)
86  throw new InvalidOperationException("Tried to control a non-running Server!");
87  }
88 
93  {
94  if (propagatedException != null)
95  throw propagatedException;
96  }
97 
99  public async Task RunAsync(CancellationToken cancellationToken)
100  {
101  using (cancellationTokenSource = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken))
102  using (var fsWatcher = updatePath != null ? new FileSystemWatcher(Path.GetDirectoryName(updatePath)) : null)
103  {
104  if (fsWatcher != null)
105  {
106  fsWatcher.Created += (a, b) =>
107  {
108  if (b.FullPath == updatePath && File.Exists(b.FullPath))
109  cancellationTokenSource.Cancel();
110  };
111  fsWatcher.EnableRaisingEvents = true;
112  }
113 
114  using (var webHost = webHostBuilder.Build())
115  try
116  {
117  logger = webHost.Services.GetRequiredService<ILogger<Server>>();
118  await webHost.RunAsync(cancellationTokenSource.Token).ConfigureAwait(false);
119  }
120  catch (OperationCanceledException)
121  {
122  CheckExceptionPropagation();
123  throw;
124  }
125  }
126  CheckExceptionPropagation();
127  }
128 
130  public bool ApplyUpdate(Version version, Uri updateZipUrl, IIOManager ioManager)
131  {
132  if (version == null)
133  throw new ArgumentNullException(nameof(version));
134  if (updateZipUrl == null)
135  throw new ArgumentNullException(nameof(updateZipUrl));
136  if (ioManager == null)
137  throw new ArgumentNullException(nameof(ioManager));
138 
139  CheckSanity(true);
140 
141  logger.LogTrace("Begin ApplyUpdate...");
142 
143  lock (this)
144  {
145  if (updating || RestartRequested)
146  {
147  logger.LogTrace("Aborted due to concurrency conflict!");
148  return false;
149  }
150  updating = true;
151  }
152 
153  async void RunUpdate()
154  {
155  try
156  {
157  logger.LogInformation("Updating server to version {0} ({1})...", version, updateZipUrl);
158 
159  if (cancellationTokenSource == null)
160  throw new InvalidOperationException("Tried to update a non-running Server!");
161  var cancellationToken = cancellationTokenSource.Token;
162 
163  logger.LogTrace("Downloading zip package...");
164  var updateZipData = await ioManager.DownloadFile(updateZipUrl, cancellationToken).ConfigureAwait(false);
165 
166  try
167  {
168  logger.LogTrace("Exctracting zip package to {0}...", updatePath);
169  await ioManager.ZipToDirectory(updatePath, updateZipData, cancellationToken).ConfigureAwait(false);
170  }
171  catch (Exception e)
172  {
173  updating = false;
174  try
175  {
176  //important to not leave this directory around if possible
177  await ioManager.DeleteDirectory(updatePath, default).ConfigureAwait(false);
178  }
179  catch (Exception e2)
180  {
181  throw new AggregateException(e, e2);
182  }
183  throw;
184  }
185 
186  await Restart(version, null).ConfigureAwait(false);
187  }
188  catch (OperationCanceledException)
189  {
190  logger.LogInformation("Server update cancelled!");
191  }
192  catch (Exception e)
193  {
194  logger.LogError("Error updating server! Exception: {0}", e);
195  }
196  finally
197  {
198  updating = false;
199  }
200  }
201 
202  RunUpdate();
203  return true;
204  }
205 
208  {
209  if (handler == null)
210  throw new ArgumentNullException(nameof(handler));
211 
212  CheckSanity(false);
213 
214  lock (this)
215  if (!RestartRequested)
216  {
217  logger.LogTrace("Registering restart handler {0}...", handler);
218  restartHandlers.Add(handler);
219  return new RestartRegistration(() =>
220  {
221  lock (this)
222  if (!RestartRequested)
223  restartHandlers.Remove(handler);
224  });
225  }
226  return new RestartRegistration(() => { });
227  }
228 
230  public Task Restart() => Restart(null, null);
231 
238  async Task Restart(Version newVersion, Exception exception)
239  {
240  CheckSanity(true);
241 
242  logger.LogTrace("Begin Restart...");
243 
244  lock (this)
245  {
246  if ((updating && newVersion == null) || RestartRequested)
247  {
248  logger.LogTrace("Aborted due to concurrency conflict!");
249  return;
250  }
251  RestartRequested = true;
252  propagatedException = exception;
253  }
254 
255  if (exception == null)
256  using (var cts = new CancellationTokenSource())
257  {
258  logger.LogInformation("Restarting server...");
259  var cancellationToken = cts.Token;
260  var eventsTask = Task.WhenAll(restartHandlers.Select(x => x.HandleRestart(newVersion, cancellationToken)).ToList());
261  //YA GOT 10 SECONDS
262  var expiryTask = Task.Delay(TimeSpan.FromSeconds(10));
263  await Task.WhenAny(eventsTask, expiryTask).ConfigureAwait(false);
264  logger.LogTrace("Joining restart handlers...");
265  cts.Cancel();
266  try
267  {
268  await eventsTask.ConfigureAwait(false);
269  }
270  catch (OperationCanceledException) { }
271  catch (Exception e)
272  {
273  logger.LogError("Restart handlers error! Exception: {0}", e);
274  }
275  }
276 
277  logger.LogTrace("Stopping host...");
278  cancellationTokenSource.Cancel();
279  }
280 
282  public Task Die(Exception exception) => Restart(null, exception);
283  }
284 }
Task DeleteDirectory(string path, CancellationToken cancellationToken)
Recursively delete a directory
ILogger< Server > logger
The ILogger for the Server
Definition: Server.cs:44
async Task RunAsync(CancellationToken cancellationToken)
Runs the IServer
Definition: Server.cs:99
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:130
readonly List< IRestartHandler > restartHandlers
The IRestartHandlers to run when the Server restarts
Definition: Server.cs:34
async Task Restart(Version newVersion, Exception exception)
Implements Restart()
Definition: Server.cs:238
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:207
Task ZipToDirectory(string path, byte[] zipFileBytes, CancellationToken cancellationToken)
Extract a set of zipFileBytes to a given path
void CheckExceptionPropagation()
Re-throw propagatedException if it exists
Definition: Server.cs:92
readonly string updatePath
The absolute path to install updates to
Definition: Server.cs:39
Represents the host
Definition: IServer.cs:9
Server(IWebHostBuilder webHostBuilder, string updatePath)
Construct a Server
Definition: Server.cs:66
CancellationTokenSource cancellationTokenSource
The cancellationTokenSource for the Server
Definition: Server.cs:49
bool updating
If a server update has been or is being applied
Definition: Server.cs:59
Exception propagatedException
The Exception to propagate when the server terminates
Definition: Server.cs:54
Interface for using filesystems
Definition: IIOManager.cs:11
Represents the lifetime of a IRestartHandler registration
readonly IWebHostBuilder webHostBuilder
The IWebHostBuilder for the Server
Definition: Server.cs:29
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:80