From 73bb7d1fff7bd77722feee8d7ef70cff62d88291 Mon Sep 17 00:00:00 2001 From: Pieter-Jan Briers Date: Fri, 5 Apr 2024 23:45:48 +0200 Subject: [PATCH] Add system to stop game server gracefully without restart Intended use is manually taking down game servers for maintenance purposes. --- .../ServerManagement/IServerInstance.cs | 27 +++++++++++- .../ServerManagement/ServerInstance.Actor.cs | 44 ++++++++++++++++++- .../ServerManagement/ServerInstance.cs | 5 +++ .../Controllers/InstanceController.cs | 14 +++++- 4 files changed, 86 insertions(+), 4 deletions(-) diff --git a/SS14.Watchdog/Components/ServerManagement/IServerInstance.cs b/SS14.Watchdog/Components/ServerManagement/IServerInstance.cs index e55f08b..0a8db7e 100644 --- a/SS14.Watchdog/Components/ServerManagement/IServerInstance.cs +++ b/SS14.Watchdog/Components/ServerManagement/IServerInstance.cs @@ -61,5 +61,30 @@ public interface IServerInstance Task ForceShutdownServerAsync(CancellationToken cancel = default); Task DoRestartCommandAsync(CancellationToken cancel = default); + + /// + /// Instruct that the server instance should be stopped gracefully. + /// It will not be restarted automatically after shutdown. + /// + /// + /// The server will be asked to gracefully shut down via the /update end point. + /// + Task DoStopCommandAsync(ServerInstanceStopCommand stopCommand, CancellationToken cancel = default); + } + + /// + /// Information about a stop command sent to a server instance. + /// + /// + public sealed class ServerInstanceStopCommand + { + public ServerInstanceStopReason StopReason; + } + + /// + public enum ServerInstanceStopReason + { + Unknown, + Maintenance, } -} \ No newline at end of file +} diff --git a/SS14.Watchdog/Components/ServerManagement/ServerInstance.Actor.cs b/SS14.Watchdog/Components/ServerManagement/ServerInstance.Actor.cs index 29ef151..b6a2a4b 100644 --- a/SS14.Watchdog/Components/ServerManagement/ServerInstance.Actor.cs +++ b/SS14.Watchdog/Components/ServerManagement/ServerInstance.Actor.cs @@ -40,6 +40,8 @@ public sealed partial class ServerInstance private int _serverTimeoutNumber; private int _startNumber; + // Server got an explicit stop command, will not be automatically restarted. + private bool _stopped; public async Task StartAsync(string baseServerAddress, CancellationToken cancel) { @@ -134,6 +136,9 @@ private async Task RunCommand(Command command, CancellationToken cancel) case CommandRestart: await RunCommandRestart(cancel); break; + case CommandStop stop: + await RunCommandStop(stop.StopCommand, cancel); + break; case CommandServerPing ping: await RunCommandServerPing(ping, cancel); break; @@ -150,6 +155,12 @@ private async Task RunCommand(Command command, CancellationToken cancel) private async Task RunCommandRestart(CancellationToken cancel) { + if (_stopped) + { + _logger.LogDebug("Clearing stopped flag due to manual server restart"); + _stopped = false; + } + if (_runningServer == null) { _loadFailCount = 0; @@ -161,6 +172,18 @@ private async Task RunCommandRestart(CancellationToken cancel) await ForceShutdownServerAsync(cancel); } + private async Task RunCommandStop(ServerInstanceStopCommand stopCommand, CancellationToken cancel) + { + // TODO: use stopCommand to indicate more extensive error message to error. + + _stopped = true; + if (IsRunning) + { + _logger.LogTrace("Server is running, sending fake update notification to make it stop"); + await SendUpdateNotificationAsync(cancel); + } + } + private async Task RunCommandUpdateAvailable(CommandUpdateAvailable command, CancellationToken cancel) { _updateOnRestart = command.UpdateAvailable; @@ -171,6 +194,10 @@ private async Task RunCommandUpdateAvailable(CommandUpdateAvailable command, Can _logger.LogTrace("Server is running, sending update notification."); await SendUpdateNotificationAsync(cancel); } + else if (_stopped) + { + _logger.LogInformation("Not restarting server for update as it was manually stopped."); + } else if (_startupFailUpdateWait) { _startupFailUpdateWait = false; @@ -247,8 +274,16 @@ private async Task RunCommandServerExit(CommandServerExit exit, CancellationToke _loadFailCount = 0; } - _logger.LogInformation("{Key}: Restarting server after exit...", Key); - await StartServer(cancel); + if (!_stopped) + { + _logger.LogInformation("{Key}: Restarting server after exit...", Key); + await StartServer(cancel); + } + else + { + _logger.LogInformation("{Key}: Not restarting server as it was manually stopped.", Key); + _notificationManager.SendNotification($"Server `{Key}` has exited after manual stop request."); + } } private async Task RunCommandStart(CancellationToken cancel) @@ -421,6 +456,11 @@ private sealed record CommandStart : Command; /// private sealed record CommandRestart : Command; + /// + /// Command to stop the server gracefully, without restarting it afterwards. + /// + private sealed record CommandStop(ServerInstanceStopCommand StopCommand) : Command; + /// /// The server has failed to ping back in time, grab the axe! /// diff --git a/SS14.Watchdog/Components/ServerManagement/ServerInstance.cs b/SS14.Watchdog/Components/ServerManagement/ServerInstance.cs index 52d3de0..6bf8af3 100644 --- a/SS14.Watchdog/Components/ServerManagement/ServerInstance.cs +++ b/SS14.Watchdog/Components/ServerManagement/ServerInstance.cs @@ -353,6 +353,11 @@ public async Task DoRestartCommandAsync(CancellationToken cancel = default) await _commandQueue.Writer.WriteAsync(new CommandRestart(), cancel); } + public async Task DoStopCommandAsync(ServerInstanceStopCommand stopCommand, CancellationToken cancel = default) + { + await _commandQueue.Writer.WriteAsync(new CommandStop(stopCommand), cancel); + } + public async Task ForceShutdownServerAsync(CancellationToken cancel = default) { var proc = _runningServer; diff --git a/SS14.Watchdog/Controllers/InstanceController.cs b/SS14.Watchdog/Controllers/InstanceController.cs index 85aa940..3a2356b 100644 --- a/SS14.Watchdog/Controllers/InstanceController.cs +++ b/SS14.Watchdog/Controllers/InstanceController.cs @@ -30,6 +30,18 @@ public async Task Restart([FromHeader(Name = "Authorization")] st return Ok(); } + [HttpPost("stop")] + public async Task Stop([FromHeader(Name = "Authorization")] string authorization, string key) + { + if (!TryAuthorize(authorization, key, out var failure, out var instance)) + { + return failure; + } + + await instance.DoStopCommandAsync(new ServerInstanceStopCommand()); + return Ok(); + } + [HttpPost("update")] public IActionResult Update([FromHeader(Name = "Authorization")] string authorization, string key) { @@ -79,4 +91,4 @@ public bool TryAuthorize(string authorization, return true; } } -} \ No newline at end of file +}