diff --git a/src/Tgstation.Server.Host/Controllers/AdministrationController.cs b/src/Tgstation.Server.Host/Controllers/AdministrationController.cs index fdb40c2462..a7ed6593d4 100644 --- a/src/Tgstation.Server.Host/Controllers/AdministrationController.cs +++ b/src/Tgstation.Server.Host/Controllers/AdministrationController.cs @@ -150,12 +150,19 @@ namespace Tgstation.Server.Host.Controllers if (model.NewVersion.Major != application.Version.Major) return BadRequest(new ErrorMessage { Message = "Cannot update to a different suite version!" }); - Logger.LogInformation("Updating to version {0}...", model.NewVersion); + if(!serverUpdater.WatchdogPresent) + return UnprocessableEntity(new ErrorMessage + { + Message = RestartNotSupportedException + }); + + Logger.LogDebug("Looking for GitHub releases version {0}...", model.NewVersion); IEnumerable releases; try { var gitHubClient = GetGitHubClient(); releases = (await gitHubClient.Repository.Release.GetAll(updatesConfiguration.GitHubRepositoryId).ConfigureAwait(false)).Where(x => x.TagName.StartsWith(updatesConfiguration.GitTagPrefix, StringComparison.InvariantCulture)); + cancellationToken.ThrowIfCancellationRequested(); } catch (RateLimitExceededException e) { @@ -174,24 +181,12 @@ namespace Tgstation.Server.Host.Controllers var asset = release.Assets.Where(x => x.Name == updatesConfiguration.UpdatePackageAssetName).FirstOrDefault(); if (asset == default) continue; - - Logger.LogDebug("Downloading update package..."); - var assetBytes = await ioManager.DownloadFile(new Uri(asset.BrowserDownloadUrl), cancellationToken).ConfigureAwait(false); - try - { - Logger.LogDebug("Extracting server update..."); - if (!await serverUpdater.ApplyUpdate(version, assetBytes, ioManager, cancellationToken).ConfigureAwait(false)) - return UnprocessableEntity(new ErrorMessage - { - Message = RestartNotSupportedException - }); //unprocessable entity - } - catch (InvalidOperationException) - { - Logger.LogDebug("Unable to find github release version!"); - return StatusCode((int)HttpStatusCode.ServiceUnavailable); //we were beat to the punch, really shouldn't happen but heat death of the universe and what not - } - Logger.LogInformation("Update staged! Restarting host..."); + + if (!serverUpdater.ApplyUpdate(version, new Uri(asset.BrowserDownloadUrl), ioManager)) + return Conflict(new ErrorMessage + { + Message = "An update operation is already in progress!" + }); return Accepted(); //gtfo of here before all the cancellation tokens fire } @@ -205,15 +200,16 @@ namespace Tgstation.Server.Host.Controllers { try { - var result = await serverUpdater.Restart().ConfigureAwait(false); - if (result) - Logger.LogInformation("Restarting host by request..."); - else - Logger.LogDebug("Restart request failed due to lack of host watchdog!"); - return result ? (IActionResult)Ok() : UnprocessableEntity(new ErrorMessage + if (!serverUpdater.WatchdogPresent) { - Message = RestartNotSupportedException - }); + Logger.LogDebug("Restart request failed due to lack of host watchdog!"); + return UnprocessableEntity(new ErrorMessage + { + Message = RestartNotSupportedException + }); + } + await serverUpdater.Restart().ConfigureAwait(false); + return Ok(); } catch (InvalidOperationException) { diff --git a/src/Tgstation.Server.Host/Core/Application.cs b/src/Tgstation.Server.Host/Core/Application.cs index ef13f01292..1288324842 100644 --- a/src/Tgstation.Server.Host/Core/Application.cs +++ b/src/Tgstation.Server.Host/Core/Application.cs @@ -317,7 +317,8 @@ namespace Tgstation.Server.Host.Core logger.LogInformation(VersionString); //attempt to restart the server if the configuration changes - ChangeToken.OnChange(configuration.GetReloadToken, () => serverControl.Restart()); + if(serverControl.WatchdogPresent) + ChangeToken.OnChange(configuration.GetReloadToken, () => serverControl.Restart()); //now setup the HTTP request pipeline diff --git a/src/Tgstation.Server.Host/Core/IServerControl.cs b/src/Tgstation.Server.Host/Core/IServerControl.cs index 4c20880dd7..8aefd00d48 100644 --- a/src/Tgstation.Server.Host/Core/IServerControl.cs +++ b/src/Tgstation.Server.Host/Core/IServerControl.cs @@ -10,15 +10,19 @@ namespace Tgstation.Server.Host.Core /// public interface IServerControl { + /// + /// if live updates are supported, . and will fail if this is + /// + bool WatchdogPresent { get; } + /// /// Run a new assembly and stop the current one. This will likely trigger all active s /// /// The the is updating to - /// The s of the .zip file that contains the new assembly + /// The that points to the .zip file that contains the new assembly /// The for the operation - /// The for the operation - /// A resulting in if live updates are supported, otherwise - Task ApplyUpdate(Version version, byte[] updateZipData, IIOManager ioManager, CancellationToken cancellationToken); + /// if the update started successfully, if there was another update in progress + bool ApplyUpdate(Version version, Uri updateZipUrl, IIOManager ioManager); /// /// Register a given to run before stopping the server for a restart @@ -30,7 +34,7 @@ namespace Tgstation.Server.Host.Core /// /// Restarts the /// - /// A resulting in if live restarts are supported, otherwise - Task Restart(); + /// A representing the running operation + Task Restart(); } } diff --git a/src/Tgstation.Server.Host/IServer.cs b/src/Tgstation.Server.Host/IServer.cs index 531b3b8afc..44aa540901 100644 --- a/src/Tgstation.Server.Host/IServer.cs +++ b/src/Tgstation.Server.Host/IServer.cs @@ -1,5 +1,4 @@ -using System; -using System.Threading; +using System.Threading; using System.Threading.Tasks; namespace Tgstation.Server.Host @@ -7,7 +6,7 @@ namespace Tgstation.Server.Host /// /// Represents the host /// - public interface IServer : IDisposable + public interface IServer { /// /// If the should restart diff --git a/src/Tgstation.Server.Host/Program.cs b/src/Tgstation.Server.Host/Program.cs index 03dbd4d2cb..d7c28a061f 100644 --- a/src/Tgstation.Server.Host/Program.cs +++ b/src/Tgstation.Server.Host/Program.cs @@ -38,32 +38,30 @@ namespace Tgstation.Server.Host updatePath = null; try { - using (var server = serverFactory.CreateServer(listArgs.ToArray(), updatePath)) + var server = serverFactory.CreateServer(listArgs.ToArray(), updatePath); + try { - try + using (var cts = new CancellationTokenSource()) { - using (var cts = new CancellationTokenSource()) + void AppDomainHandler(object a, EventArgs b) => cts.Cancel(); + AppDomain.CurrentDomain.ProcessExit += AppDomainHandler; + try { - void AppDomainHandler(object a, EventArgs b) => cts.Cancel(); - AppDomain.CurrentDomain.ProcessExit += AppDomainHandler; - try + Console.CancelKeyPress += (a, b) => { - Console.CancelKeyPress += (a, b) => - { - b.Cancel = true; - cts.Cancel(); - }; - await server.RunAsync(cts.Token).ConfigureAwait(false); - } - finally - { - AppDomain.CurrentDomain.ProcessExit -= AppDomainHandler; - } + b.Cancel = true; + cts.Cancel(); + }; + await server.RunAsync(cts.Token).ConfigureAwait(false); + } + finally + { + AppDomain.CurrentDomain.ProcessExit -= AppDomainHandler; } } - catch (OperationCanceledException) { } - return server.RestartRequested ? 1 : 0; } + catch (OperationCanceledException) { } + return server.RestartRequested ? 1 : 0; } catch (Exception e) { diff --git a/src/Tgstation.Server.Host/Server.cs b/src/Tgstation.Server.Host/Server.cs index 3404787028..1786fd6a19 100644 --- a/src/Tgstation.Server.Host/Server.cs +++ b/src/Tgstation.Server.Host/Server.cs @@ -1,5 +1,6 @@ using Microsoft.AspNetCore.Hosting; using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Logging; using System; using System.Collections.Generic; using System.IO; @@ -12,41 +13,46 @@ using Tgstation.Server.Host.IO; namespace Tgstation.Server.Host { /// +#pragma warning disable CA1001 // Types that own disposable fields should be disposable sealed class Server : IServer, IServerControl +#pragma warning restore CA1001 // Types that own disposable fields should be disposable { /// public bool RestartRequested { get; private set; } + /// + public bool WatchdogPresent => updatePath != null; + /// /// The for the /// readonly IWebHostBuilder webHostBuilder; - /// - /// The for the - /// - readonly SemaphoreSlim semaphore; - - /// - /// The absolute path to install updates to - /// - readonly string updatePath; - /// /// The s to run when the restarts /// readonly List restartHandlers; /// - /// If a server update has been applied + /// The absolute path to install updates to /// - bool updated; + readonly string updatePath; + + /// + /// The for the + /// + ILogger logger; /// /// The for the /// CancellationTokenSource cancellationTokenSource; + /// + /// If a server update has been or is being applied + /// + bool updating; + /// /// Construct a /// @@ -58,13 +64,20 @@ namespace Tgstation.Server.Host this.updatePath = updatePath; restartHandlers = new List(); - semaphore = new SemaphoreSlim(1); - updated = false; - RestartRequested = false; } - /// - public void Dispose() => semaphore.Dispose(); + /// + /// Throws an if the cannot be used + /// + /// If should be checked + void CheckSanity(bool checkWatchdog) + { + if (checkWatchdog && !WatchdogPresent) + throw new InvalidOperationException("Server restarts are not supported"); + + if (cancellationTokenSource == null || logger == null) + throw new InvalidOperationException("Tried to control a non-running Server!"); + } /// public async Task RunAsync(CancellationToken cancellationToken) @@ -83,51 +96,91 @@ namespace Tgstation.Server.Host } using (var webHost = webHostBuilder .UseStartup() - .ConfigureServices((serviceCollection) => serviceCollection.AddSingleton(this)) + .ConfigureServices(serviceCollection => serviceCollection.AddSingleton(this)) .Build() ) + { + logger = webHost.Services.GetRequiredService>(); await webHost.RunAsync(cancellationTokenSource.Token).ConfigureAwait(false); + } } } /// - public async Task ApplyUpdate(Version version, byte[] updateZipData, IIOManager ioManager, CancellationToken cancellationToken) + public bool ApplyUpdate(Version version, Uri updateZipUrl, IIOManager ioManager) { if (version == null) throw new ArgumentNullException(nameof(version)); - if (updateZipData == null) - throw new ArgumentNullException(nameof(updateZipData)); + if (updateZipUrl == null) + throw new ArgumentNullException(nameof(updateZipUrl)); if (ioManager == null) throw new ArgumentNullException(nameof(ioManager)); - if (updatePath == null) - return false; - using (await SemaphoreSlimContext.Lock(semaphore, cancellationToken).ConfigureAwait(false)) + CheckSanity(true); + + logger.LogTrace("Begin ApplyUpdate..."); + + lock (this) + { + if (updating || RestartRequested) + { + logger.LogTrace("Aborted due to concurrency conflict!"); + return false; + } + updating = true; + } + + async void RunUpdate() { - if (updated) - throw new InvalidOperationException("ApplyUpdate has already been called!"); - updated = true; try { - await ioManager.ZipToDirectory(updatePath, updateZipData, cancellationToken).ConfigureAwait(false); + logger.LogInformation("Updating server to version {0} ({1})...", version, updateZipUrl); + + if (cancellationTokenSource == null) + throw new InvalidOperationException("Tried to update a non-running Server!"); + var cancellationToken = cancellationTokenSource.Token; + + logger.LogTrace("Downloading zip package..."); + var updateZipData = await ioManager.DownloadFile(updateZipUrl, cancellationToken).ConfigureAwait(false); + + try + { + logger.LogTrace("Exctracting zip package to {0}...", updatePath); + await ioManager.ZipToDirectory(updatePath, updateZipData, cancellationToken).ConfigureAwait(false); + } + catch (Exception e) + { + updating = false; + try + { + //important to not leave this directory around if possible + await ioManager.DeleteDirectory(updatePath, default).ConfigureAwait(false); + } + catch (Exception e2) + { + throw new AggregateException(e, e2); + } + throw; + } + + await Restart(version).ConfigureAwait(false); + } + catch (OperationCanceledException) + { + logger.LogInformation("Server update cancelled!"); } catch (Exception e) { - updated = false; - try - { - //important to not leave this directory around if possible - await ioManager.DeleteDirectory(updatePath, default).ConfigureAwait(false); - } - catch (Exception e2) - { - throw new AggregateException(e, e2); - } - throw; + logger.LogError("Error updating server! Exception: {0}", e); + } + finally + { + updating = false; } - await Restart(version).ConfigureAwait(false); - return true; } + + RunUpdate(); + return true; } /// @@ -135,16 +188,18 @@ namespace Tgstation.Server.Host { if (handler == null) throw new ArgumentNullException(nameof(handler)); - if (cancellationTokenSource == null) - throw new InvalidOperationException("Tried to register an update action on a non-running Server!"); + + CheckSanity(false); + lock (this) if (!RestartRequested) { + logger.LogTrace("Registering restart handler {0}...", handler); restartHandlers.Add(handler); return new RestartRegistration(() => { lock (this) - if(!RestartRequested) + if (!RestartRequested) restartHandlers.Remove(handler); }); } @@ -152,43 +207,54 @@ namespace Tgstation.Server.Host } /// - public Task Restart() => Restart(null); + public Task Restart() => Restart(null); /// /// Implements /// /// The of any potential updates being applied /// - async Task Restart(Version newVersion) + async Task Restart(Version newVersion) { - if (updatePath == null) - return false; - if (cancellationTokenSource == null) - throw new InvalidOperationException("Tried to restart a non-running Server!"); + CheckSanity(true); + + logger.LogTrace("Begin Restart..."); + lock (this) { - if (RestartRequested) - return true; + if ((updating && newVersion == null) || RestartRequested) + { + logger.LogTrace("Aborted due to concurrency conflict!"); + return; + } RestartRequested = true; } + logger.LogInformation("Restarting server..."); + using (var cts = new CancellationTokenSource()) { + logger.LogTrace("Running restart handlers..."); var cancellationToken = cts.Token; var eventsTask = Task.WhenAll(restartHandlers.Select(x => x.HandleRestart(newVersion, cancellationToken)).ToList()); //YA GOT 10 SECONDS var expiryTask = Task.Delay(TimeSpan.FromSeconds(10)); await Task.WhenAny(eventsTask, expiryTask).ConfigureAwait(false); + logger.LogTrace("Joining restart handlers..."); cts.Cancel(); try { await eventsTask.ConfigureAwait(false); } catch (OperationCanceledException) { } + catch (Exception e) + { + logger.LogError("Restart handlers error! Exception: {0}", e); + } } + logger.LogTrace("Stopping host..."); cancellationTokenSource.Cancel(); - return true; } } }