diff --git a/src/Tgstation.Server.Host/Components/Chat/Chat.cs b/src/Tgstation.Server.Host/Components/Chat/Chat.cs index 64c763c22e..456b882168 100644 --- a/src/Tgstation.Server.Host/Components/Chat/Chat.cs +++ b/src/Tgstation.Server.Host/Components/Chat/Chat.cs @@ -9,12 +9,13 @@ using System.Threading.Tasks; using Tgstation.Server.Api.Models.Internal; using Tgstation.Server.Host.Components.Chat.Commands; using Tgstation.Server.Host.Components.Chat.Providers; +using Tgstation.Server.Host.Core; using Tgstation.Server.Host.IO; namespace Tgstation.Server.Host.Components.Chat { /// - sealed class Chat : IChat + sealed class Chat : IChat, IRestartHandler { const string CommonMention = "!tgs"; @@ -33,6 +34,11 @@ namespace Tgstation.Server.Host.Components.Chat /// readonly ICommandFactory commandFactory; + /// + /// The for the + /// + readonly IRestartRegistration restartRegistration; + /// /// The for the /// @@ -100,15 +106,20 @@ namespace Tgstation.Server.Host.Components.Chat /// The value of /// The value of /// The value of + /// The to populate with /// The used to populate - public Chat(IProviderFactory providerFactory, IIOManager ioManager, ICommandFactory commandFactory, ILogger logger, IEnumerable initialChatBots) + public Chat(IProviderFactory providerFactory, IIOManager ioManager, ICommandFactory commandFactory, IServerControl serverControl, ILogger logger, IEnumerable initialChatBots) { this.providerFactory = providerFactory ?? throw new ArgumentNullException(nameof(providerFactory)); this.ioManager = ioManager ?? throw new ArgumentNullException(nameof(ioManager)); this.commandFactory = commandFactory ?? throw new ArgumentNullException(nameof(commandFactory)); + if (serverControl == null) + throw new ArgumentNullException(nameof(serverControl)); this.logger = logger ?? throw new ArgumentNullException(nameof(logger)); this.initialChatBots = initialChatBots?.ToList() ?? throw new ArgumentNullException(nameof(initialChatBots)); + restartRegistration = serverControl.RegisterForRestart(this); + builtinCommands = new Dictionary(); providers = new Dictionary(); mappedChannels = new Dictionary(); @@ -121,6 +132,7 @@ namespace Tgstation.Server.Host.Components.Chat /// public void Dispose() { + restartRegistration.Dispose(); handlerCts.Dispose(); foreach (var I in providers) I.Value.Dispose(); @@ -563,8 +575,9 @@ namespace Tgstation.Server.Host.Components.Chat } /// - public Task SendBroadcast(string message, CancellationToken cancellationToken) + public Task HandleRestart(Version updateVersion, CancellationToken cancellationToken) { + var message = updateVersion == null ? "TGS: Restart requested..." : String.Format(CultureInfo.InvariantCulture, "TGS: Updating to version {0}...", updateVersion); List wdChannels; lock (mappedChannels) //so it doesn't change while we're using it wdChannels = mappedChannels.Select(x => x.Key).ToList(); diff --git a/src/Tgstation.Server.Host/Components/Chat/ChatFactory.cs b/src/Tgstation.Server.Host/Components/Chat/ChatFactory.cs index 4fc1329766..8b3db001ce 100644 --- a/src/Tgstation.Server.Host/Components/Chat/ChatFactory.cs +++ b/src/Tgstation.Server.Host/Components/Chat/ChatFactory.cs @@ -2,6 +2,7 @@ using System; using System.Collections.Generic; using Tgstation.Server.Host.Components.Chat.Commands; +using Tgstation.Server.Host.Core; using Tgstation.Server.Host.IO; namespace Tgstation.Server.Host.Components.Chat @@ -9,42 +10,35 @@ namespace Tgstation.Server.Host.Components.Chat /// sealed class ChatFactory : IChatFactory { - /// - /// The for the - /// - readonly IIOManager ioManager; - /// /// The for the /// readonly ILoggerFactory loggerFactory; - /// - /// The for the - /// - readonly ICommandFactory commandFactory; - /// /// The for the /// readonly IProviderFactory providerFactory; + /// + /// The for the + /// + readonly IServerControl serverControl; + /// /// Construct a /// - /// The value of /// The value of - /// The value of /// The value of - public ChatFactory(IIOManager ioManager, ILoggerFactory loggerFactory, ICommandFactory commandFactory, IProviderFactory providerFactory) + /// The value of + public ChatFactory(ILoggerFactory loggerFactory, IProviderFactory providerFactory, IServerControl serverControl) { - this.ioManager = ioManager ?? throw new ArgumentNullException(nameof(ioManager)); this.loggerFactory = loggerFactory ?? throw new ArgumentNullException(nameof(loggerFactory)); - this.commandFactory = commandFactory ?? throw new ArgumentNullException(nameof(commandFactory)); this.providerFactory = providerFactory ?? throw new ArgumentNullException(nameof(providerFactory)); + this.serverControl = serverControl ?? throw new ArgumentNullException(nameof(serverControl)); } /// - public IChat CreateChat(IEnumerable initialChatBots) => new Chat(providerFactory, ioManager, commandFactory, loggerFactory.CreateLogger(), initialChatBots); + public IChat CreateChat(IIOManager ioManager, ICommandFactory commandFactory, IEnumerable initialChatBots) => new Chat(providerFactory, ioManager, commandFactory, serverControl, loggerFactory.CreateLogger(), initialChatBots); } } diff --git a/src/Tgstation.Server.Host/Components/Chat/IChat.cs b/src/Tgstation.Server.Host/Components/Chat/IChat.cs index fb8a1166df..59ecc05587 100644 --- a/src/Tgstation.Server.Host/Components/Chat/IChat.cs +++ b/src/Tgstation.Server.Host/Components/Chat/IChat.cs @@ -75,14 +75,6 @@ namespace Tgstation.Server.Host.Components.Chat /// A representing the running operation Task SendUpdateMessage(string message, CancellationToken cancellationToken); - /// - /// Send a chat to all channels - /// - /// The message being sent - /// The for the operation - /// A representing the running operation - Task SendBroadcast(string message, CancellationToken cancellationToken); - /// /// Start tracking json files for commands and channels /// diff --git a/src/Tgstation.Server.Host/Components/Chat/IChatFactory.cs b/src/Tgstation.Server.Host/Components/Chat/IChatFactory.cs index 6950552eb7..a47c2c13d2 100644 --- a/src/Tgstation.Server.Host/Components/Chat/IChatFactory.cs +++ b/src/Tgstation.Server.Host/Components/Chat/IChatFactory.cs @@ -1,4 +1,6 @@ using System.Collections.Generic; +using Tgstation.Server.Host.Components.Chat.Commands; +using Tgstation.Server.Host.IO; namespace Tgstation.Server.Host.Components.Chat { @@ -10,8 +12,10 @@ namespace Tgstation.Server.Host.Components.Chat /// /// Create a /// + /// The for the + /// The for the /// The initial for the /// A new - IChat CreateChat(IEnumerable initialChatBots); + IChat CreateChat(IIOManager ioManager, ICommandFactory commandFactory, IEnumerable initialChatBots); } } diff --git a/src/Tgstation.Server.Host/Components/InstanceFactory.cs b/src/Tgstation.Server.Host/Components/InstanceFactory.cs index c9494cbd0c..a66f643e36 100644 --- a/src/Tgstation.Server.Host/Components/InstanceFactory.cs +++ b/src/Tgstation.Server.Host/Components/InstanceFactory.cs @@ -43,11 +43,6 @@ namespace Tgstation.Server.Host.Components /// readonly IByondTopicSender byondTopicSender; - /// - /// The for the - /// - readonly IServerControl serverUpdater; - /// /// The for the /// @@ -71,7 +66,7 @@ namespace Tgstation.Server.Host.Components /// /// The for the /// - readonly IProviderFactory providerFactory; + readonly IChatFactory chatFactory; /// /// The for the @@ -106,30 +101,28 @@ namespace Tgstation.Server.Host.Components /// The value of /// The value of /// The value of - /// The value of /// The value of /// The value of /// The value of /// The value of - /// The value of + /// The value of /// The value of /// The value of /// The value of /// The value of /// The value of - public InstanceFactory(IIOManager ioManager, IDatabaseContextFactory databaseContextFactory, IApplication application, ILoggerFactory loggerFactory, IByondTopicSender byondTopicSender, IServerControl serverUpdater, ICryptographySuite cryptographySuite, ISynchronousIOManager synchronousIOManager, ISymlinkFactory symlinkFactory, IByondInstaller byondInstaller, IProviderFactory providerFactory, IProcessExecutor processExecutor, IPostWriteHandler postWriteHandler, IWatchdogFactory watchdogFactory, IJobManager jobManager, ICredentialsProvider credentialsProvider) + public InstanceFactory(IIOManager ioManager, IDatabaseContextFactory databaseContextFactory, IApplication application, ILoggerFactory loggerFactory, IByondTopicSender byondTopicSender, ICryptographySuite cryptographySuite, ISynchronousIOManager synchronousIOManager, ISymlinkFactory symlinkFactory, IByondInstaller byondInstaller, IChatFactory chatFactory, IProcessExecutor processExecutor, IPostWriteHandler postWriteHandler, IWatchdogFactory watchdogFactory, IJobManager jobManager, ICredentialsProvider credentialsProvider) { this.ioManager = ioManager ?? throw new ArgumentNullException(nameof(ioManager)); this.databaseContextFactory = databaseContextFactory ?? throw new ArgumentNullException(nameof(databaseContextFactory)); this.application = application ?? throw new ArgumentNullException(nameof(application)); this.loggerFactory = loggerFactory ?? throw new ArgumentNullException(nameof(loggerFactory)); this.byondTopicSender = byondTopicSender ?? throw new ArgumentNullException(nameof(byondTopicSender)); - this.serverUpdater = serverUpdater ?? throw new ArgumentNullException(nameof(serverUpdater)); this.cryptographySuite = cryptographySuite ?? throw new ArgumentNullException(nameof(cryptographySuite)); this.synchronousIOManager = synchronousIOManager ?? throw new ArgumentNullException(nameof(synchronousIOManager)); this.symlinkFactory = symlinkFactory ?? throw new ArgumentNullException(nameof(symlinkFactory)); this.byondInstaller = byondInstaller ?? throw new ArgumentNullException(nameof(byondInstaller)); - this.providerFactory = providerFactory ?? throw new ArgumentNullException(nameof(providerFactory)); + this.chatFactory = chatFactory ?? throw new ArgumentNullException(nameof(chatFactory)); this.processExecutor = processExecutor ?? throw new ArgumentNullException(nameof(processExecutor)); this.postWriteHandler = postWriteHandler ?? throw new ArgumentNullException(nameof(postWriteHandler)); this.watchdogFactory = watchdogFactory ?? throw new ArgumentNullException(nameof(watchdogFactory)); @@ -161,9 +154,8 @@ namespace Tgstation.Server.Host.Components var byond = new ByondManager(byondIOManager, byondInstaller, loggerFactory.CreateLogger()); var commandFactory = new CommandFactory(application, byond, repoManager, databaseContextFactory, metadata); - var chatFactory = new ChatFactory(instanceIoManager, loggerFactory, commandFactory, providerFactory); - var chat = chatFactory.CreateChat(metadata.ChatSettings); + var chat = chatFactory.CreateChat(instanceIoManager, commandFactory, metadata.ChatSettings); try { var sessionControllerFactory = new SessionControllerFactory(processExecutor, byond, byondTopicSender, cryptographySuite, application, gameIoManager, chat, loggerFactory, metadata.CloneMetadata()); diff --git a/src/Tgstation.Server.Host/Components/InstanceManager.cs b/src/Tgstation.Server.Host/Components/InstanceManager.cs index 1165c0c390..00b4e42e3c 100644 --- a/src/Tgstation.Server.Host/Components/InstanceManager.cs +++ b/src/Tgstation.Server.Host/Components/InstanceManager.cs @@ -49,16 +49,6 @@ namespace Tgstation.Server.Host.Components /// readonly Dictionary instances; - /// - /// of s to finish in - /// - readonly List shutdownTasks; - - /// - /// Used as a temporary for - /// - readonly CancellationTokenSource shutdownCancellationTokenSource; - /// /// Construct an /// @@ -67,31 +57,17 @@ namespace Tgstation.Server.Host.Components /// The value of /// The value of /// The value of - /// The for the /// The value of - public InstanceManager(IInstanceFactory instanceFactory, IIOManager ioManager, IDatabaseContextFactory databaseContextFactory, IApplication application, IJobManager jobManager, IServerControl serverControl, ILogger logger) + public InstanceManager(IInstanceFactory instanceFactory, IIOManager ioManager, IDatabaseContextFactory databaseContextFactory, IApplication application, IJobManager jobManager, ILogger logger) { this.instanceFactory = instanceFactory ?? throw new ArgumentNullException(nameof(instanceFactory)); this.ioManager = ioManager ?? throw new ArgumentNullException(nameof(ioManager)); this.databaseContextFactory = databaseContextFactory ?? throw new ArgumentNullException(nameof(databaseContextFactory)); this.application = application ?? throw new ArgumentNullException(nameof(application)); this.jobManager = jobManager ?? throw new ArgumentNullException(nameof(jobManager)); - - if (serverControl == null) - throw new ArgumentNullException(nameof(serverControl)); - - shutdownCancellationTokenSource = new CancellationTokenSource(); - var cancellationToken = shutdownCancellationTokenSource.Token; - serverControl.RegisterForRestart(() => - { - lock (this) - shutdownTasks.AddRange(instances.Select(x => x.Value.Chat.SendBroadcast("TGS: Restart requested...", cancellationToken))); - }); - this.logger = logger ?? throw new ArgumentNullException(nameof(logger)); instances = new Dictionary(); - shutdownTasks = new List(); } /// @@ -99,7 +75,6 @@ namespace Tgstation.Server.Host.Components { foreach (var I in instances) I.Value.Dispose(); - shutdownCancellationTokenSource.Dispose(); } /// @@ -237,11 +212,7 @@ namespace Tgstation.Server.Host.Components public async Task StopAsync(CancellationToken cancellationToken) { await jobManager.StopAsync(cancellationToken).ConfigureAwait(false); - - using (cancellationToken.Register(() => shutdownCancellationTokenSource.Cancel())) - await Task.WhenAll(shutdownTasks).ConfigureAwait(false); await Task.WhenAll(instances.Select(x => x.Value.StopAsync(cancellationToken))).ConfigureAwait(false); - await instanceFactory.StopAsync(cancellationToken).ConfigureAwait(false); } } diff --git a/src/Tgstation.Server.Host/Components/Watchdog/Watchdog.cs b/src/Tgstation.Server.Host/Components/Watchdog/Watchdog.cs index 2975471eeb..1baed19018 100644 --- a/src/Tgstation.Server.Host/Components/Watchdog/Watchdog.cs +++ b/src/Tgstation.Server.Host/Components/Watchdog/Watchdog.cs @@ -20,7 +20,7 @@ using Tgstation.Server.Host.Core; namespace Tgstation.Server.Host.Components.Watchdog { /// - sealed class Watchdog : IWatchdog, ICustomCommandHandler + sealed class Watchdog : IWatchdog, ICustomCommandHandler, IRestartHandler { /// /// The time in seconds to wait from starting to start . Does not take responsiveness into account @@ -93,6 +93,11 @@ namespace Tgstation.Server.Host.Components.Watchdog /// readonly IJobManager jobManager; + /// + /// The for the + /// + readonly IRestartRegistration restartRegistration; + /// /// The for the /// @@ -143,17 +148,17 @@ namespace Tgstation.Server.Host.Components.Watchdog /// The value of /// The value of /// The value of - /// The for the /// The value of /// The value of /// The value of /// The value of /// The value of /// The value of + /// The to populate with /// The initial value of . May be modified /// The value of /// The value of - public Watchdog(IChat chat, ISessionControllerFactory sessionControllerFactory, IDmbFactory dmbFactory, IServerControl serverUpdater, ILogger logger, IReattachInfoHandler reattachInfoHandler, IDatabaseContextFactory databaseContextFactory, IByondTopicSender byondTopicSender, IEventConsumer eventConsumer, IJobManager jobManager, DreamDaemonLaunchParameters initialLaunchParameters, Api.Models.Instance instance, bool autoStart) + public Watchdog(IChat chat, ISessionControllerFactory sessionControllerFactory, IDmbFactory dmbFactory, ILogger logger, IReattachInfoHandler reattachInfoHandler, IDatabaseContextFactory databaseContextFactory, IByondTopicSender byondTopicSender, IEventConsumer eventConsumer, IJobManager jobManager, IServerControl serverControl, DreamDaemonLaunchParameters initialLaunchParameters, Api.Models.Instance instance, bool autoStart) { this.chat = chat ?? throw new ArgumentNullException(nameof(chat)); this.sessionControllerFactory = sessionControllerFactory ?? throw new ArgumentNullException(nameof(sessionControllerFactory)); @@ -167,10 +172,10 @@ namespace Tgstation.Server.Host.Components.Watchdog this.instance = instance ?? throw new ArgumentNullException(nameof(instance)); this.autoStart = autoStart; - if (serverUpdater == null) - throw new ArgumentNullException(nameof(serverUpdater)); + if (serverControl == null) + throw new ArgumentNullException(nameof(serverControl)); - serverUpdater.RegisterForRestart(() => releaseServers = true); + restartRegistration = serverControl.RegisterForRestart(this); chat.RegisterCommandHandler(this); @@ -186,6 +191,7 @@ namespace Tgstation.Server.Host.Components.Watchdog { DisposeAndNullControllers(); semaphore.Dispose(); + restartRegistration.Dispose(); } /// @@ -641,7 +647,7 @@ namespace Tgstation.Server.Host.Components.Watchdog if (startMonitor && await StopMonitor().ConfigureAwait(false)) chatTask = chat.SendWatchdogMessage("Automatic retry sequence cancelled by manual launch. Restarting...", cancellationToken); else if (announce) - chatTask = chat.SendWatchdogMessage("Starting...", cancellationToken); + chatTask = chat.SendWatchdogMessage(doReattach ? "Reattaching..." : "Starting...", cancellationToken); else chatTask = Task.CompletedTask; //start both servers @@ -925,5 +931,13 @@ namespace Tgstation.Server.Host.Components.Watchdog return await activeServer.SendCommand(command, cancellationToken).ConfigureAwait(false) ?? "ERROR: Bad topic exchange!"; } } + + /// + public async Task HandleRestart(Version updateVersion, CancellationToken cancellationToken) + { + releaseServers = true; + if (Running) + await chat.SendWatchdogMessage("Detaching...", cancellationToken).ConfigureAwait(false); + } } } diff --git a/src/Tgstation.Server.Host/Components/Watchdog/WatchdogFactory.cs b/src/Tgstation.Server.Host/Components/Watchdog/WatchdogFactory.cs index b200d8f93b..b2d33d838d 100644 --- a/src/Tgstation.Server.Host/Components/Watchdog/WatchdogFactory.cs +++ b/src/Tgstation.Server.Host/Components/Watchdog/WatchdogFactory.cs @@ -14,7 +14,7 @@ namespace Tgstation.Server.Host.Components.Watchdog /// /// The for the /// - readonly IServerControl serverUpdater; + readonly IServerControl serverControl; /// /// The for the @@ -39,14 +39,14 @@ namespace Tgstation.Server.Host.Components.Watchdog /// /// Construct a /// - /// The value of + /// The value of /// The value of /// The value of /// The value of /// The value of - public WatchdogFactory(IServerControl serverUpdater, ILoggerFactory loggerFactory, IDatabaseContextFactory databaseContextFactory, IByondTopicSender byondTopicSender, IJobManager jobManager) + public WatchdogFactory(IServerControl serverControl, ILoggerFactory loggerFactory, IDatabaseContextFactory databaseContextFactory, IByondTopicSender byondTopicSender, IJobManager jobManager) { - this.serverUpdater = serverUpdater ?? throw new ArgumentNullException(nameof(serverUpdater)); + this.serverControl = serverControl ?? throw new ArgumentNullException(nameof(serverControl)); this.loggerFactory = loggerFactory ?? throw new ArgumentNullException(nameof(loggerFactory)); this.databaseContextFactory = databaseContextFactory ?? throw new ArgumentNullException(nameof(databaseContextFactory)); this.byondTopicSender = byondTopicSender ?? throw new ArgumentNullException(nameof(byondTopicSender)); @@ -54,6 +54,6 @@ namespace Tgstation.Server.Host.Components.Watchdog } /// - public IWatchdog CreateWatchdog(IChat chat, IDmbFactory dmbFactory, IReattachInfoHandler reattachInfoHandler, IEventConsumer eventConsumer, ISessionControllerFactory sessionControllerFactory, Api.Models.Instance instance, DreamDaemonSettings settings) => new Watchdog(chat, sessionControllerFactory, dmbFactory, serverUpdater, loggerFactory.CreateLogger(), reattachInfoHandler, databaseContextFactory, byondTopicSender, eventConsumer, jobManager, settings, instance, settings.AutoStart.Value); + public IWatchdog CreateWatchdog(IChat chat, IDmbFactory dmbFactory, IReattachInfoHandler reattachInfoHandler, IEventConsumer eventConsumer, ISessionControllerFactory sessionControllerFactory, Api.Models.Instance instance, DreamDaemonSettings settings) => new Watchdog(chat, sessionControllerFactory, dmbFactory, loggerFactory.CreateLogger(), reattachInfoHandler, databaseContextFactory, byondTopicSender, eventConsumer, jobManager, serverControl, settings, instance, settings.AutoStart.Value); } } diff --git a/src/Tgstation.Server.Host/Controllers/AdministrationController.cs b/src/Tgstation.Server.Host/Controllers/AdministrationController.cs index 553163da07..fdb40c2462 100644 --- a/src/Tgstation.Server.Host/Controllers/AdministrationController.cs +++ b/src/Tgstation.Server.Host/Controllers/AdministrationController.cs @@ -180,7 +180,7 @@ namespace Tgstation.Server.Host.Controllers try { Logger.LogDebug("Extracting server update..."); - if (!await serverUpdater.ApplyUpdate(assetBytes, ioManager, cancellationToken).ConfigureAwait(false)) + if (!await serverUpdater.ApplyUpdate(version, assetBytes, ioManager, cancellationToken).ConfigureAwait(false)) return UnprocessableEntity(new ErrorMessage { Message = RestartNotSupportedException @@ -201,23 +201,23 @@ namespace Tgstation.Server.Host.Controllers /// [HttpDelete] [TgsAuthorize(AdministrationRights.RestartHost)] - public Task Delete() + public async Task Delete() { try { - var result = serverUpdater.Restart(); + 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 Task.FromResult(result ? (IActionResult)Ok() : UnprocessableEntity(new ErrorMessage + return result ? (IActionResult)Ok() : UnprocessableEntity(new ErrorMessage { Message = RestartNotSupportedException - })); + }); } catch (InvalidOperationException) { - return Task.FromResult(StatusCode((int)HttpStatusCode.ServiceUnavailable)); + return StatusCode((int)HttpStatusCode.ServiceUnavailable); } } } diff --git a/src/Tgstation.Server.Host/Core/Application.cs b/src/Tgstation.Server.Host/Core/Application.cs index 9ee1c8eca9..e82a7d9147 100644 --- a/src/Tgstation.Server.Host/Core/Application.cs +++ b/src/Tgstation.Server.Host/Core/Application.cs @@ -215,6 +215,7 @@ namespace Tgstation.Server.Host.Core SendTimeout = 5000 }); + services.AddSingleton(); services.AddSingleton(); services.AddSingleton(); diff --git a/src/Tgstation.Server.Host/Core/IRestartHandler.cs b/src/Tgstation.Server.Host/Core/IRestartHandler.cs new file mode 100644 index 0000000000..f3b0c3afca --- /dev/null +++ b/src/Tgstation.Server.Host/Core/IRestartHandler.cs @@ -0,0 +1,20 @@ +using System; +using System.Threading; +using System.Threading.Tasks; + +namespace Tgstation.Server.Host.Core +{ + /// + /// Handler for server restarts + /// + public interface IRestartHandler + { + /// + /// Handle a restart of the server + /// + /// The being updated to, if not being changed + /// The for the operation + /// A representing the running operation + Task HandleRestart(Version updateVersion, CancellationToken cancellationToken); + } +} diff --git a/src/Tgstation.Server.Host/Core/IRestartRegistration.cs b/src/Tgstation.Server.Host/Core/IRestartRegistration.cs new file mode 100644 index 0000000000..78aa00ffc5 --- /dev/null +++ b/src/Tgstation.Server.Host/Core/IRestartRegistration.cs @@ -0,0 +1,11 @@ +using System; + +namespace Tgstation.Server.Host.Core +{ + /// + /// Represents the lifetime of a registration + /// + public interface IRestartRegistration : IDisposable + { + } +} \ No newline at end of file diff --git a/src/Tgstation.Server.Host/Core/IServerControl.cs b/src/Tgstation.Server.Host/Core/IServerControl.cs index dc97c4bb56..4c20880dd7 100644 --- a/src/Tgstation.Server.Host/Core/IServerControl.cs +++ b/src/Tgstation.Server.Host/Core/IServerControl.cs @@ -13,22 +13,24 @@ namespace Tgstation.Server.Host.Core /// /// 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 for the operation /// The for the operation /// A resulting in if live updates are supported, otherwise - Task ApplyUpdate(byte[] updateZipData, IIOManager ioManager, CancellationToken cancellationToken); + Task ApplyUpdate(Version version, byte[] updateZipData, IIOManager ioManager, CancellationToken cancellationToken); /// - /// Register a given to run before stopping the server for a restart + /// Register a given to run before stopping the server for a restart /// - /// The to run - void RegisterForRestart(Action action); + /// The to register + /// A new representing the scope of the registration + IRestartRegistration RegisterForRestart(IRestartHandler handler); /// /// Restarts the /// - /// if live restarts are supported, otherwise - bool Restart(); + /// A resulting in if live restarts are supported, otherwise + Task Restart(); } } diff --git a/src/Tgstation.Server.Host/Core/RestartRegistration.cs b/src/Tgstation.Server.Host/Core/RestartRegistration.cs new file mode 100644 index 0000000000..8df2cf3ad2 --- /dev/null +++ b/src/Tgstation.Server.Host/Core/RestartRegistration.cs @@ -0,0 +1,26 @@ +using System; +using Tgstation.Server.Host.Core; + +namespace Tgstation.Server.Host.Core +{ + /// + sealed class RestartRegistration : IRestartRegistration + { + /// + /// The + /// + readonly Action onDispose; + + /// + /// Construct a + /// + /// The value of + public RestartRegistration(Action onDispose) + { + this.onDispose = onDispose ?? throw new ArgumentNullException(nameof(onDispose)); + } + + /// + public void Dispose() => onDispose(); + } +} \ No newline at end of file diff --git a/src/Tgstation.Server.Host/Server.cs b/src/Tgstation.Server.Host/Server.cs index 7bc3192638..18a5749478 100644 --- a/src/Tgstation.Server.Host/Server.cs +++ b/src/Tgstation.Server.Host/Server.cs @@ -1,8 +1,10 @@ using Microsoft.AspNetCore.Hosting; using Microsoft.Extensions.DependencyInjection; using System; +using System.Collections.Generic; using System.IO; using System.Diagnostics.CodeAnalysis; +using System.Linq; using System.Threading; using System.Threading.Tasks; using Tgstation.Server.Host.Core; @@ -30,6 +32,11 @@ namespace Tgstation.Server.Host /// 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 @@ -51,6 +58,7 @@ namespace Tgstation.Server.Host this.webHostBuilder = webHostBuilder ?? throw new ArgumentNullException(nameof(webHostBuilder)); this.updatePath = updatePath; + restartHandlers = new List(); semaphore = new SemaphoreSlim(1); updated = false; RestartRequested = false; @@ -85,8 +93,15 @@ namespace Tgstation.Server.Host } /// - public async Task ApplyUpdate(byte[] updateZipData, IIOManager ioManager, CancellationToken cancellationToken) + public async Task ApplyUpdate(Version version, byte[] updateZipData, IIOManager ioManager, CancellationToken cancellationToken) { + if (version == null) + throw new ArgumentNullException(nameof(version)); + if (updateZipData == null) + throw new ArgumentNullException(nameof(updateZipData)); + if (ioManager == null) + throw new ArgumentNullException(nameof(ioManager)); + if (updatePath == null) return false; using (await SemaphoreSlimContext.Lock(semaphore, cancellationToken).ConfigureAwait(false)) @@ -98,44 +113,82 @@ namespace Tgstation.Server.Host { await ioManager.ZipToDirectory(updatePath, updateZipData, cancellationToken).ConfigureAwait(false); } - catch + catch (Exception e) { + updated = false; try { //important to not leave this directory around if possible await ioManager.DeleteDirectory(updatePath, default).ConfigureAwait(false); } - catch { } - updated = false; + catch (Exception e2) + { + throw new AggregateException(e, e2); + } throw; } - Restart(); + await Restart(version).ConfigureAwait(false); return true; } } /// - public void RegisterForRestart(Action action) + public IRestartRegistration RegisterForRestart(IRestartHandler handler) { - if (action == null) - throw new ArgumentNullException(nameof(action)); + 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!"); - cancellationTokenSource.Token.Register(() => - { - if (RestartRequested) - action(); - }); + lock (this) + if (!RestartRequested) + { + restartHandlers.Add(handler); + return new RestartRegistration(() => + { + lock (this) + if(!RestartRequested) + restartHandlers.Remove(handler); + }); + } + return new RestartRegistration(() => { }); } /// - public bool Restart() + public Task Restart() => Restart(null); + + /// + /// Implements + /// + /// The of any potential updates being applied + /// + async Task Restart(Version newVersion) { if (updatePath == null) return false; if (cancellationTokenSource == null) throw new InvalidOperationException("Tried to restart a non-running Server!"); - RestartRequested = true; + lock (this) + { + if (RestartRequested) + return true; + RestartRequested = true; + } + + using (var cts = new CancellationTokenSource()) + { + var cancellationToken = cts.Token; + var eventsTask = Task.WhenAll(restartHandlers.Select(x => x.HandleRestart(newVersion, cancellationToken))); + //YA GOT 10 SECONDS + var expiryTask = Task.Delay(TimeSpan.FromSeconds(10)); + await Task.WhenAny(eventsTask, expiryTask).ConfigureAwait(false); + cts.Cancel(); + try + { + await eventsTask.ConfigureAwait(false); + } + catch (OperationCanceledException) { } + } + cancellationTokenSource.Cancel(); return true; }