1 using Microsoft.AspNetCore.Hosting;
2 using Microsoft.Extensions.DependencyInjection;
3 using Microsoft.Extensions.Logging;
5 using System.Collections.Generic;
9 using System.Threading.Tasks;
16 #pragma warning disable CA1001 // Types that own disposable fields should be disposable 18 #pragma warning restore CA1001 // Types that own disposable fields should be disposable 21 public bool RestartRequested {
get;
private set; }
24 public bool WatchdogPresent => updatePath != null;
66 public Server(IWebHostBuilder webHostBuilder,
string updatePath)
68 this.webHostBuilder = webHostBuilder ??
throw new ArgumentNullException(nameof(webHostBuilder));
69 this.updatePath = updatePath;
71 webHostBuilder.ConfigureServices(serviceCollection => serviceCollection.AddSingleton<
IServerControl>(
this));
73 restartHandlers =
new List<IRestartHandler>();
82 if (checkWatchdog && !WatchdogPresent && propagatedException == null)
83 throw new InvalidOperationException(
"Server restarts are not supported");
85 if (cancellationTokenSource == null || logger == null)
86 throw new InvalidOperationException(
"Tried to control a non-running Server!");
94 if (propagatedException != null)
95 throw propagatedException;
99 public async Task
RunAsync(CancellationToken cancellationToken)
101 using (cancellationTokenSource = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken))
102 using (var fsWatcher = updatePath != null ?
new FileSystemWatcher(Path.GetDirectoryName(updatePath)) : null)
104 if (fsWatcher != null)
106 fsWatcher.Created += (a, b) =>
108 if (b.FullPath == updatePath && File.Exists(b.FullPath))
109 cancellationTokenSource.Cancel();
111 fsWatcher.EnableRaisingEvents =
true;
114 using (var webHost = webHostBuilder.Build())
117 logger = webHost.Services.GetRequiredService<ILogger<Server>>();
118 await webHost.RunAsync(cancellationTokenSource.Token).ConfigureAwait(
false);
120 catch (OperationCanceledException)
122 CheckExceptionPropagation();
126 CheckExceptionPropagation();
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));
141 logger.LogTrace(
"Begin ApplyUpdate...");
145 if (updating || RestartRequested)
147 logger.LogTrace(
"Aborted due to concurrency conflict!");
153 async
void RunUpdate()
157 logger.LogInformation(
"Updating server to version {0} ({1})...", version, updateZipUrl);
159 if (cancellationTokenSource == null)
160 throw new InvalidOperationException(
"Tried to update a non-running Server!");
161 var cancellationToken = cancellationTokenSource.Token;
163 logger.LogTrace(
"Downloading zip package...");
164 var updateZipData = await ioManager.
DownloadFile(updateZipUrl, cancellationToken).ConfigureAwait(
false);
168 logger.LogTrace(
"Exctracting zip package to {0}...", updatePath);
169 await ioManager.
ZipToDirectory(updatePath, updateZipData, cancellationToken).ConfigureAwait(
false);
177 await ioManager.
DeleteDirectory(updatePath,
default).ConfigureAwait(
false);
181 throw new AggregateException(e, e2);
186 await Restart(version, null).ConfigureAwait(
false);
188 catch (OperationCanceledException)
190 logger.LogInformation(
"Server update cancelled!");
194 logger.LogError(
"Error updating server! Exception: {0}", e);
210 throw new ArgumentNullException(nameof(handler));
215 if (!RestartRequested)
217 logger.LogTrace(
"Registering restart handler {0}...", handler);
218 restartHandlers.Add(handler);
222 if (!RestartRequested)
223 restartHandlers.Remove(handler);
230 public Task Restart() => Restart(null, null);
238 async Task
Restart(Version newVersion, Exception exception)
242 logger.LogTrace(
"Begin Restart...");
246 if ((updating && newVersion == null) || RestartRequested)
248 logger.LogTrace(
"Aborted due to concurrency conflict!");
251 RestartRequested =
true;
252 propagatedException = exception;
255 if (exception == null)
256 using (var cts =
new CancellationTokenSource())
258 logger.LogInformation(
"Restarting server...");
259 var cancellationToken = cts.Token;
260 var eventsTask = Task.WhenAll(restartHandlers.Select(x => x.HandleRestart(newVersion, cancellationToken)).ToList());
262 var expiryTask = Task.Delay(TimeSpan.FromSeconds(10));
263 await Task.WhenAny(eventsTask, expiryTask).ConfigureAwait(
false);
264 logger.LogTrace(
"Joining restart handlers...");
268 await eventsTask.ConfigureAwait(
false);
270 catch (OperationCanceledException) { }
273 logger.LogError(
"Restart handlers error! Exception: {0}", e);
277 logger.LogTrace(
"Stopping host...");
278 cancellationTokenSource.Cancel();
282 public Task Die(Exception exception) => Restart(null, exception);
Task DeleteDirectory(string path, CancellationToken cancellationToken)
Recursively delete a directory
ILogger< Server > logger
The ILogger for the Server
async Task RunAsync(CancellationToken cancellationToken)
Runs the IServer
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...
readonly List< IRestartHandler > restartHandlers
The IRestartHandlers to run when the Server restarts
async Task Restart(Version newVersion, Exception exception)
Implements Restart()
Use server authentication
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
Task ZipToDirectory(string path, byte[] zipFileBytes, CancellationToken cancellationToken)
Extract a set of zipFileBytes to a given path
Handler for server restarts
void CheckExceptionPropagation()
Re-throw propagatedException if it exists
readonly string updatePath
The absolute path to install updates to
Server(IWebHostBuilder webHostBuilder, string updatePath)
Construct a Server
CancellationTokenSource cancellationTokenSource
The cancellationTokenSource for the Server
bool updating
If a server update has been or is being applied
Exception propagatedException
The Exception to propagate when the server terminates
Interface for using filesystems
Represents the lifetime of a IRestartHandler registration
readonly IWebHostBuilder webHostBuilder
The IWebHostBuilder for 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