diff --git a/build/Version.props b/build/Version.props index 5ea3946d4c..156b683324 100644 --- a/build/Version.props +++ b/build/Version.props @@ -3,8 +3,8 @@ - 4.12.1 - 3.1.0 + 4.13.0 + 4.0.0 9.0.1 9.0.0 10.0.0 diff --git a/src/Tgstation.Server.Host/.config/dotnet-tools.json b/src/Tgstation.Server.Host/.config/dotnet-tools.json index 820745aaa9..383d557584 100644 --- a/src/Tgstation.Server.Host/.config/dotnet-tools.json +++ b/src/Tgstation.Server.Host/.config/dotnet-tools.json @@ -3,7 +3,7 @@ "isRoot": true, "tools": { "dotnet-ef": { - "version": "3.1.13", + "version": "3.1.16", "commands": [ "dotnet-ef" ] diff --git a/src/Tgstation.Server.Host/Components/Chat/ChatManager.cs b/src/Tgstation.Server.Host/Components/Chat/ChatManager.cs index 531a70a2e9..a56464cb72 100644 --- a/src/Tgstation.Server.Host/Components/Chat/ChatManager.cs +++ b/src/Tgstation.Server.Host/Components/Chat/ChatManager.cs @@ -493,9 +493,12 @@ namespace Tgstation.Server.Host.Components.Chat } /// - public Task HandleRestart(Version updateVersion, CancellationToken cancellationToken) + public Task HandleRestart(Version updateVersion, bool graceful, CancellationToken cancellationToken) { - var message = updateVersion == null ? "TGS: Restart requested..." : String.Format(CultureInfo.InvariantCulture, "TGS: Updating to version {0}...", updateVersion); + var message = + updateVersion == null + ? $"TGS: {(graceful ? "Graceful r" : "R")}estart requested..." + : $"TGS: Updating to version {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/Watchdog/WatchdogBase.cs b/src/Tgstation.Server.Host/Components/Watchdog/WatchdogBase.cs index 38f867557d..588d6f8156 100644 --- a/src/Tgstation.Server.Host/Components/Watchdog/WatchdogBase.cs +++ b/src/Tgstation.Server.Host/Components/Watchdog/WatchdogBase.cs @@ -404,8 +404,23 @@ namespace Tgstation.Server.Host.Components.Watchdog } /// - public async Task HandleRestart(Version updateVersion, CancellationToken cancellationToken) + public async Task HandleRestart(Version updateVersion, bool graceful, CancellationToken cancellationToken) { + if (graceful) + { + await Terminate(true, cancellationToken).ConfigureAwait(false); + + if (Status != WatchdogStatus.Offline) + { + Logger.LogTrace("Waiting for server to gracefully shut down."); + await monitorTask.WithToken(cancellationToken).ConfigureAwait(false); + } + else + Logger.LogTrace("Graceful shutdown requested but server is already offline."); + + return; + } + releaseServers = true; if (Status == WatchdogStatus.Online) await Chat.QueueWatchdogMessage("Detaching...", cancellationToken).ConfigureAwait(false); diff --git a/src/Tgstation.Server.Host/Components/Watchdog/WindowsWatchdog.cs b/src/Tgstation.Server.Host/Components/Watchdog/WindowsWatchdog.cs index f2c22f9bc2..c1919b7e41 100644 --- a/src/Tgstation.Server.Host/Components/Watchdog/WindowsWatchdog.cs +++ b/src/Tgstation.Server.Host/Components/Watchdog/WindowsWatchdog.cs @@ -105,7 +105,8 @@ namespace Tgstation.Server.Host.Components.Watchdog } catch { - _ = DisposeAsync(); + // synchronous + DisposeAsync().AsTask().Wait(); throw; } } diff --git a/src/Tgstation.Server.Host/Configuration/GeneralConfiguration.cs b/src/Tgstation.Server.Host/Configuration/GeneralConfiguration.cs index 0b2d8ae6b5..d7a1a91f06 100644 --- a/src/Tgstation.Server.Host/Configuration/GeneralConfiguration.cs +++ b/src/Tgstation.Server.Host/Configuration/GeneralConfiguration.cs @@ -51,9 +51,14 @@ namespace Tgstation.Server.Host.Configuration const uint DefaultByondTopicTimeout = 5000; /// - /// The default value for . + /// The default value for . /// - const uint DefaultRestartTimeout = 60000; + const uint DefaultRestartTimeoutMinutes = 1; + + /// + /// The default value for . + /// + const uint DefaultShutdownTimeoutMinutes = 300; /// /// The current . @@ -87,9 +92,14 @@ namespace Tgstation.Server.Host.Configuration public uint ByondTopicTimeout { get; set; } = DefaultByondTopicTimeout; /// - /// The timeout milliseconds for restarting the server. + /// The timeout minutes for restarting the server. /// - public uint RestartTimeout { get; set; } = DefaultRestartTimeout; + public uint RestartTimeoutMinutes { get; set; } = DefaultRestartTimeoutMinutes; + + /// + /// The timeout minutes for gracefully stopping the server. + /// + public uint ShutdownTimeoutMinutes { get; set; } = DefaultShutdownTimeoutMinutes; /// /// If the should be preferred. diff --git a/src/Tgstation.Server.Host/Core/Application.cs b/src/Tgstation.Server.Host/Core/Application.cs index c28dc4a6f2..087cb51702 100644 --- a/src/Tgstation.Server.Host/Core/Application.cs +++ b/src/Tgstation.Server.Host/Core/Application.cs @@ -126,7 +126,7 @@ namespace Tgstation.Server.Host.Core // Set the timeout for IHostedService.StopAsync services.Configure( - opts => opts.ShutdownTimeout = TimeSpan.FromMilliseconds(postSetupServices.GeneralConfiguration.RestartTimeout)); + opts => opts.ShutdownTimeout = TimeSpan.FromMinutes(postSetupServices.GeneralConfiguration.RestartTimeoutMinutes)); static LogEventLevel? ConvertSeriLogLevel(LogLevel logLevel) => logLevel switch @@ -321,6 +321,8 @@ namespace Tgstation.Server.Host.Core // PosixProcessFeatures also needs a IProcessExecutor for gcore services.AddSingleton(x => new Lazy(() => x.GetRequiredService(), true)); services.AddSingleton(); + + services.AddSingleton(); } // configure component/misc services diff --git a/src/Tgstation.Server.Host/Core/IRestartHandler.cs b/src/Tgstation.Server.Host/Core/IRestartHandler.cs index c2d61d14d5..e76af13583 100644 --- a/src/Tgstation.Server.Host/Core/IRestartHandler.cs +++ b/src/Tgstation.Server.Host/Core/IRestartHandler.cs @@ -13,8 +13,9 @@ namespace Tgstation.Server.Host.Core /// Handle a restart of the server. /// /// The being updated to, if not being changed. + /// If the restart handler perform no destructive actions. /// The for the operation. /// A representing the running operation. - Task HandleRestart(Version updateVersion, CancellationToken cancellationToken); + Task HandleRestart(Version updateVersion, bool graceful, CancellationToken cancellationToken); } } diff --git a/src/Tgstation.Server.Host/Core/IServerControl.cs b/src/Tgstation.Server.Host/Core/IServerControl.cs index 81a72ca4c5..aebdc2f7c1 100644 --- a/src/Tgstation.Server.Host/Core/IServerControl.cs +++ b/src/Tgstation.Server.Host/Core/IServerControl.cs @@ -43,6 +43,12 @@ namespace Tgstation.Server.Host.Core /// A representing the running operation. Task Restart(); + /// + /// Gracefully shutsdown the . + /// + /// A representing the running operation. + Task GracefulShutdown(); + /// /// Kill the server with a fatal exception. /// diff --git a/src/Tgstation.Server.Host/Properties/AssemblyInfo.cs b/src/Tgstation.Server.Host/Properties/AssemblyInfo.cs index 318d89d28f..47d191b80a 100644 --- a/src/Tgstation.Server.Host/Properties/AssemblyInfo.cs +++ b/src/Tgstation.Server.Host/Properties/AssemblyInfo.cs @@ -1,5 +1,6 @@ using System.Runtime.CompilerServices; [assembly: InternalsVisibleTo("Tgstation.Server.Host.Tests")] +[assembly: InternalsVisibleTo("Tgstation.Server.Host.Tests.Signals")] [assembly: InternalsVisibleTo("Tgstation.Server.Tests")] [assembly: InternalsVisibleTo("DynamicProxyGenAssembly2")] diff --git a/src/Tgstation.Server.Host/Server.cs b/src/Tgstation.Server.Host/Server.cs index 568ed6bd74..a3ab5d3c42 100644 --- a/src/Tgstation.Server.Host/Server.cs +++ b/src/Tgstation.Server.Host/Server.cs @@ -79,6 +79,11 @@ namespace Tgstation.Server.Host /// Exception propagatedException; + /// + /// If the server is being shut down or restarted. + /// + bool shutdownInProgress; + /// /// Initializes a new instance of the class. /// @@ -153,9 +158,9 @@ namespace Tgstation.Server.Host lock (restartLock) { - if (UpdateInProgress || RestartRequested) + if (UpdateInProgress || shutdownInProgress) { - logger.LogTrace("Aborted due to concurrency conflict!"); + logger.LogDebug("Aborted update due to concurrency conflict!"); return false; } @@ -260,14 +265,14 @@ namespace Tgstation.Server.Host CheckSanity(false); lock (restartLock) - if (!RestartRequested) + if (!shutdownInProgress) { logger.LogTrace("Registering restart handler {0}...", handler); restartHandlers.Add(handler); return new RestartRegistration(() => { lock (restartLock) - if (!RestartRequested) + if (!shutdownInProgress) restartHandlers.Remove(handler); }); } @@ -278,6 +283,9 @@ namespace Tgstation.Server.Host /// public Task Restart() => Restart(null, null, true); + /// + public Task GracefulShutdown() => Restart(null, null, false); + /// public Task Die(Exception exception) => Restart(null, exception, false); @@ -320,28 +328,39 @@ namespace Tgstation.Server.Host { CheckSanity(requireWatchdog); - logger.LogTrace("Begin Restart..."); + // if the watchdog isn't required and there's no issue, this is just a graceful shutdown + bool isGracefulShutdown = !requireWatchdog && exception == null; + logger.LogTrace( + "Begin {0}...", + isGracefulShutdown + ? "graceful shutdown" + : "restart"); lock (restartLock) { - if ((UpdateInProgress && newVersion == null) || RestartRequested) + if ((UpdateInProgress && newVersion == null) || shutdownInProgress) { - logger.LogTrace("Aborted due to concurrency conflict!"); + logger.LogTrace("Aborted restart due to concurrency conflict!"); return; } - RestartRequested = true; + shutdownInProgress = true; + RestartRequested = !isGracefulShutdown; propagatedException ??= exception; } if (exception == null) { - logger.LogInformation("Restarting server..."); - using var cts = new CancellationTokenSource(TimeSpan.FromMilliseconds(generalConfiguration.RestartTimeout)); + logger.LogInformation("Stopping server..."); + using var cts = new CancellationTokenSource( + TimeSpan.FromMinutes( + isGracefulShutdown + ? generalConfiguration.ShutdownTimeoutMinutes + : generalConfiguration.RestartTimeoutMinutes)); var cancellationToken = cts.Token; var eventsTask = Task.WhenAll( restartHandlers.Select( - x => x.HandleRestart(newVersion, cancellationToken)) + x => x.HandleRestart(newVersion, isGracefulShutdown, cancellationToken)) .ToList()); logger.LogTrace("Joining restart handlers..."); @@ -351,9 +370,12 @@ namespace Tgstation.Server.Host } catch (OperationCanceledException ex) { - logger.LogError( - ex, - "Restart timeout hit! Existing DreamDaemon processes will be lost and must be killed manually before being restarted with TGS!"); + if (isGracefulShutdown) + logger.LogWarning(ex, "Graceful shutdown timeout hit! Existing DreamDaemon processes will be terminated!"); + else + logger.LogError( + ex, + "Restart timeout hit! Existing DreamDaemon processes will be lost and must be killed manually before being restarted with TGS!"); } catch (Exception e) { diff --git a/src/Tgstation.Server.Host/ServerFactory.cs b/src/Tgstation.Server.Host/ServerFactory.cs index d4ab9fc566..05adf44883 100644 --- a/src/Tgstation.Server.Host/ServerFactory.cs +++ b/src/Tgstation.Server.Host/ServerFactory.cs @@ -103,7 +103,7 @@ namespace Tgstation.Server.Host .UseIISIntegration() .UseApplication(postSetupServices) .SuppressStatusMessages(true) - .UseShutdownTimeout(TimeSpan.FromMilliseconds(postSetupServices.GeneralConfiguration.RestartTimeout))); + .UseShutdownTimeout(TimeSpan.FromMinutes(postSetupServices.GeneralConfiguration.RestartTimeoutMinutes))); if (updatePath != null) hostBuilder.UseContentRoot( diff --git a/src/Tgstation.Server.Host/Swarm/SwarmService.cs b/src/Tgstation.Server.Host/Swarm/SwarmService.cs index ad786320aa..a7ae89fc7c 100644 --- a/src/Tgstation.Server.Host/Swarm/SwarmService.cs +++ b/src/Tgstation.Server.Host/Swarm/SwarmService.cs @@ -177,7 +177,7 @@ namespace Tgstation.Server.Host.Swarm DateTimeOffset? lastControllerHealthCheck; /// - /// If was called. + /// If was called. /// bool restarting; @@ -661,7 +661,7 @@ namespace Tgstation.Server.Host.Swarm } /// - public Task HandleRestart(Version updateVersion, CancellationToken cancellationToken) + public Task HandleRestart(Version updateVersion, bool graceful, CancellationToken cancellationToken) { restarting = true; return Task.CompletedTask; diff --git a/src/Tgstation.Server.Host/System/PosixSignalHandler.cs b/src/Tgstation.Server.Host/System/PosixSignalHandler.cs new file mode 100644 index 0000000000..8faab11703 --- /dev/null +++ b/src/Tgstation.Server.Host/System/PosixSignalHandler.cs @@ -0,0 +1,131 @@ +using System; +using System.Threading; +using System.Threading.Tasks; + +using Microsoft.Extensions.Hosting; +using Microsoft.Extensions.Logging; +using Mono.Unix; +using Mono.Unix.Native; + +using Tgstation.Server.Host.Core; + +namespace Tgstation.Server.Host.System +{ + /// + /// Handles POSIX signals. + /// + sealed class PosixSignalHandler : IHostedService, IDisposable + { + /// + /// Check for signals each time this amount of milliseconds pass. + /// + const int CheckDelayMs = 250; + + /// + /// The for the . + /// + readonly IServerControl serverControl; + + /// + /// The for the . + /// + readonly IAsyncDelayer asyncDelayer; + + /// + /// The for the . + /// + readonly ILogger logger; + + /// + /// The used to stop the . + /// + readonly CancellationTokenSource cancellationTokenSource; + + /// + /// The thread used to check the signal. See http://docs.go-mono.com/?link=T%3aMono.Unix.UnixSignal. + /// + Task signalCheckerTask; + + /// + /// Initializes a new instance of the class. + /// + /// The value of . + /// The value of . + /// The value of . + public PosixSignalHandler(IServerControl serverControl, IAsyncDelayer asyncDelayer, ILogger logger) + { + this.serverControl = serverControl ?? throw new ArgumentNullException(nameof(serverControl)); + this.asyncDelayer = asyncDelayer ?? throw new ArgumentNullException(nameof(asyncDelayer)); + this.logger = logger ?? throw new ArgumentNullException(nameof(logger)); + + cancellationTokenSource = new CancellationTokenSource(); + } + + /// + public void Dispose() => cancellationTokenSource.Dispose(); + + /// + public Task StartAsync(CancellationToken cancellationToken) + { + if (signalCheckerTask != null) + throw new InvalidOperationException("Attempted to start PosixSignalHandler twice!"); + + signalCheckerTask = SignalChecker(); + + return Task.CompletedTask; + } + + /// + public async Task StopAsync(CancellationToken cancellationToken) + { + if (signalCheckerTask?.IsCompleted != false) + return; + + logger.LogDebug("Stopping SignalCheckerThread..."); + cancellationTokenSource.Cancel(); + + logger.LogTrace("Joining SignalCheckerThread..."); + await signalCheckerTask.ConfigureAwait(false); + } + + /// + /// Thread for listening to signal. + /// + /// A representing the running operation. + async Task SignalChecker() + { + try + { + logger.LogTrace("Started SignalChecker"); + + using var unixSignal = new UnixSignal(Signum.SIGUSR1); + if (!unixSignal.IsSet) + { + logger.LogTrace("Waiting for SIGUSR1..."); + var cancellationToken = cancellationTokenSource.Token; + while (!unixSignal.IsSet) + await asyncDelayer.Delay(TimeSpan.FromMilliseconds(CheckDelayMs), cancellationToken); + + logger.LogTrace("SIGUSR1 received!"); + } + else + logger.LogDebug("SIGUSR1 has already been sent"); + + logger.LogTrace("Triggering graceful shutdown..."); + await serverControl.GracefulShutdown().ConfigureAwait(false); + } + catch (OperationCanceledException ex) + { + logger.LogDebug(ex, "SignalChecker cancelled!"); + } + catch (Exception ex) + { + logger.LogError(ex, "SignalChecker crashed!"); + } + finally + { + logger.LogTrace("Exiting SignalChecker..."); + } + } + } +} diff --git a/src/Tgstation.Server.Host/System/Process.cs b/src/Tgstation.Server.Host/System/Process.cs index a3bc854915..2ff8ccf1b4 100644 --- a/src/Tgstation.Server.Host/System/Process.cs +++ b/src/Tgstation.Server.Host/System/Process.cs @@ -134,24 +134,31 @@ namespace Tgstation.Server.Host.System { if (combinedStringBuilder == null) throw new InvalidOperationException("Output/Error stream reading was not enabled!"); - await Task.WhenAll(standardOutputTask, standardErrorTask).WithToken(cancellationToken).ConfigureAwait(false); + await Task.WhenAll( + GetStandardOutput(cancellationToken), + GetErrorOutput(cancellationToken)) + .ConfigureAwait(false); return combinedStringBuilder.ToString().TrimStart(Environment.NewLine.ToCharArray()); } /// - public Task GetErrorOutput(CancellationToken cancellationToken) + public async Task GetErrorOutput(CancellationToken cancellationToken) { if (standardErrorTask == null) throw new InvalidOperationException("Error stream reading was not enabled!"); - return standardErrorTask.WithToken(cancellationToken); + if (!standardErrorTask.IsCompleted) + logger.LogTrace("Waiting for PID {0} to close error stream...", Id); + return await standardErrorTask.WithToken(cancellationToken).ConfigureAwait(false); } /// - public Task GetStandardOutput(CancellationToken cancellationToken) + public async Task GetStandardOutput(CancellationToken cancellationToken) { if (standardOutputTask == null) throw new InvalidOperationException("Output stream reading was not enabled!"); - return standardOutputTask.WithToken(cancellationToken); + if (!standardOutputTask.IsCompleted) + logger.LogTrace("Waiting for PID {0} to close output stream...", Id); + return await standardOutputTask.WithToken(cancellationToken).ConfigureAwait(false); } /// diff --git a/src/Tgstation.Server.Host/System/ProcessExecutor.cs b/src/Tgstation.Server.Host/System/ProcessExecutor.cs index 907d056e26..17e0d18d9f 100644 --- a/src/Tgstation.Server.Host/System/ProcessExecutor.cs +++ b/src/Tgstation.Server.Host/System/ProcessExecutor.cs @@ -176,13 +176,17 @@ namespace Tgstation.Server.Host.System { combinedStringBuilder = new StringBuilder(); - async Task ConsumeReader(Func readerFunc) + async Task ConsumeReader(Func readerFunc, bool isOutputStream) { var stringBuilder = new StringBuilder(); string text; await processStartTcs.Task.ConfigureAwait(false); + var pid = handle.Id; + var streamType = isOutputStream ? "out" : "err"; + logger.LogTrace("Starting std{0} read for PID {1}...", streamType, pid); + var reader = readerFunc(); while ((text = await reader.ReadLineAsync().ConfigureAwait(false)) != null) { @@ -192,18 +196,20 @@ namespace Tgstation.Server.Host.System stringBuilder.Append(text); } + logger.LogTrace("Finished std{0} read for PID {1}", streamType, pid); + return stringBuilder.ToString(); } if (readOutput) { - outputTask = ConsumeReader(() => handle.StandardOutput); + outputTask = ConsumeReader(() => handle.StandardOutput, true); handle.StartInfo.RedirectStandardOutput = true; } if (readError) { - errorTask = ConsumeReader(() => handle.StandardError); + errorTask = ConsumeReader(() => handle.StandardError, false); handle.StartInfo.RedirectStandardError = true; } } diff --git a/src/Tgstation.Server.Host/Tgstation.Server.Host.csproj b/src/Tgstation.Server.Host/Tgstation.Server.Host.csproj index 084ee35ed9..4bc0229374 100644 --- a/src/Tgstation.Server.Host/Tgstation.Server.Host.csproj +++ b/src/Tgstation.Server.Host/Tgstation.Server.Host.csproj @@ -66,45 +66,45 @@ - + - - + + all runtime; build; native; contentfiles; analyzers; buildtransitive - - + + all runtime; build; native; contentfiles; analyzers; buildtransitive - - + + - + - + - + all runtime; build; native; contentfiles; analyzers; buildtransitive - - + + - + - + diff --git a/src/Tgstation.Server.Host/appsettings.yml b/src/Tgstation.Server.Host/appsettings.yml index a8467be26e..61040477a7 100644 --- a/src/Tgstation.Server.Host/appsettings.yml +++ b/src/Tgstation.Server.Host/appsettings.yml @@ -3,7 +3,7 @@ General: GitHubAccessToken: SetupWizardMode: AutoDetect ByondTopicTimeout: 5000 - RestartTimeout: 60000 + RestartTimeoutMinutes: 1 ApiPort: 5000 UseBasicWatchdog: false UserLimit: 100 diff --git a/tests/Tgstation.Server.Api.Tests/Tgstation.Server.Api.Tests.csproj b/tests/Tgstation.Server.Api.Tests/Tgstation.Server.Api.Tests.csproj index 5e8419667f..b2d545340b 100644 --- a/tests/Tgstation.Server.Api.Tests/Tgstation.Server.Api.Tests.csproj +++ b/tests/Tgstation.Server.Api.Tests/Tgstation.Server.Api.Tests.csproj @@ -8,14 +8,14 @@ - + all runtime; build; native; contentfiles; analyzers; buildtransitive - - - + + + diff --git a/tests/Tgstation.Server.Client.Tests/Tgstation.Server.Client.Tests.csproj b/tests/Tgstation.Server.Client.Tests/Tgstation.Server.Client.Tests.csproj index d5be2a01d8..c1b0d91af7 100644 --- a/tests/Tgstation.Server.Client.Tests/Tgstation.Server.Client.Tests.csproj +++ b/tests/Tgstation.Server.Client.Tests/Tgstation.Server.Client.Tests.csproj @@ -8,14 +8,14 @@ - + all runtime; build; native; contentfiles; analyzers; buildtransitive - + - - + + diff --git a/tests/Tgstation.Server.Host.Console.Tests/Tgstation.Server.Host.Console.Tests.csproj b/tests/Tgstation.Server.Host.Console.Tests/Tgstation.Server.Host.Console.Tests.csproj index 78de606c45..0048170d0c 100644 --- a/tests/Tgstation.Server.Host.Console.Tests/Tgstation.Server.Host.Console.Tests.csproj +++ b/tests/Tgstation.Server.Host.Console.Tests/Tgstation.Server.Host.Console.Tests.csproj @@ -8,14 +8,14 @@ - + all runtime; build; native; contentfiles; analyzers; buildtransitive - + - - + + diff --git a/tests/Tgstation.Server.Host.Service.Tests/Tgstation.Server.Host.Service.Tests.csproj b/tests/Tgstation.Server.Host.Service.Tests/Tgstation.Server.Host.Service.Tests.csproj index 5db0c44389..4223f1f549 100644 --- a/tests/Tgstation.Server.Host.Service.Tests/Tgstation.Server.Host.Service.Tests.csproj +++ b/tests/Tgstation.Server.Host.Service.Tests/Tgstation.Server.Host.Service.Tests.csproj @@ -14,13 +14,13 @@ - + all runtime; build; native; contentfiles; analyzers; buildtransitive - - + + diff --git a/tests/Tgstation.Server.Host.Tests.Signals/Program.cs b/tests/Tgstation.Server.Host.Tests.Signals/Program.cs new file mode 100644 index 0000000000..180ca55408 --- /dev/null +++ b/tests/Tgstation.Server.Host.Tests.Signals/Program.cs @@ -0,0 +1,56 @@ +using System; +using System.Threading; +using System.Threading.Tasks; + +using Microsoft.Extensions.Logging; +using Microsoft.VisualStudio.TestTools.UnitTesting; +using Mono.Unix.Native; +using Moq; + +using Tgstation.Server.Host.Core; +using Tgstation.Server.Host.Extensions; +using Tgstation.Server.Host.System; + +namespace Tgstation.Server.Host.Tests.Signals +{ + static class Program + { + static async Task Main() + { + var mockServerControl = new Mock(); + + var tcs = new TaskCompletionSource(); + mockServerControl + .Setup(x => x.GracefulShutdown()) + .Callback(() => tcs.SetResult(null)) + .Returns(Task.CompletedTask); + + var mockAsyncDelayer = new Mock(); + mockAsyncDelayer.Setup(x => x.Delay(It.IsAny(), It.IsAny())).Returns(Task.CompletedTask); + using var signalHandler = new PosixSignalHandler(mockServerControl.Object, mockAsyncDelayer.Object, Mock.Of>()); + + Assert.IsFalse(tcs.Task.IsCompleted); + + await signalHandler.StartAsync(default); + + Assert.IsFalse(tcs.Task.IsCompleted); + + await Assert.ThrowsExceptionAsync(() => signalHandler.StartAsync(default)); + + Assert.IsFalse(tcs.Task.IsCompleted); + + using var cts = new CancellationTokenSource(TimeSpan.FromSeconds(5)); + await signalHandler.StopAsync(default).WithToken(cts.Token).ConfigureAwait(false); + Assert.IsFalse(tcs.Task.IsCompleted); + + using var signalHandler2 = new PosixSignalHandler(mockServerControl.Object, mockAsyncDelayer.Object, Mock.Of>()); + await signalHandler2.StartAsync(default); + + using var cts2 = new CancellationTokenSource(TimeSpan.FromSeconds(20)); + await tcs.Task.WithToken(cts2.Token); + + using var cts3 = new CancellationTokenSource(TimeSpan.FromSeconds(5)); + await signalHandler2.StopAsync(default).WithToken(cts3.Token).ConfigureAwait(false); + } + } +} diff --git a/tests/Tgstation.Server.Host.Tests.Signals/Tgstation.Server.Host.Tests.Signals.csproj b/tests/Tgstation.Server.Host.Tests.Signals/Tgstation.Server.Host.Tests.Signals.csproj new file mode 100644 index 0000000000..f83dcb3bf1 --- /dev/null +++ b/tests/Tgstation.Server.Host.Tests.Signals/Tgstation.Server.Host.Tests.Signals.csproj @@ -0,0 +1,20 @@ + + + + netcoreapp3.1 + Exe + + false + latest + + + + + + + + + + + + diff --git a/tests/Tgstation.Server.Host.Tests/System/TestPosixSignalHandler.cs b/tests/Tgstation.Server.Host.Tests/System/TestPosixSignalHandler.cs new file mode 100644 index 0000000000..bab768b6a8 --- /dev/null +++ b/tests/Tgstation.Server.Host.Tests/System/TestPosixSignalHandler.cs @@ -0,0 +1,93 @@ +using System; +using System.IO; +using System.Reflection; +using System.Threading; +using System.Threading.Tasks; + +using Microsoft.Extensions.Logging; +using Microsoft.VisualStudio.TestTools.UnitTesting; +using Moq; + +using Tgstation.Server.Host.Core; +using Tgstation.Server.Host.IO; + +namespace Tgstation.Server.Host.System.Tests +{ + [TestClass] + public sealed class TestPosixSignalHandler + { + [TestMethod] + public void TestConstruction() + { + Assert.ThrowsException(() => new PosixSignalHandler(null, null, null)); + + var mockServerControl = Mock.Of(); + Assert.ThrowsException(() => new PosixSignalHandler(mockServerControl, null, null)); + + var mockAsyncDelayer = Mock.Of(); + Assert.ThrowsException(() => new PosixSignalHandler(mockServerControl, mockAsyncDelayer, null)); + + new PosixSignalHandler(mockServerControl, mockAsyncDelayer, Mock.Of>()).Dispose(); + } + + [TestMethod] + public async Task TestSignalListening() + { + if (new PlatformIdentifier().IsWindows) + Assert.Inconclusive("POSIX only test."); + + Assert.Inconclusive("This test fucking doesn't work (hangs on stdout/stderr processing). If this functionality breaks I will find you and devise a very temporarily traumatizing torture for you."); + + // `kill`ing the test process results in it hanging, no idea why + // we need to run it as a standard dotnet process#if DEBUG +#if DEBUG + const string CurrentConfig = "Debug"; +#else + const string CurrentConfig = "Release"; +#endif + + var pathToSignalTestApp = $"{Path.GetDirectoryName(Assembly.GetExecutingAssembly().Location)}/../../../../Tgstation.Server.Host.Tests.Signals"; + var loggerFactory = LoggerFactory.Create(builder => + { + builder.AddConsole(); + builder.SetMinimumLevel(LogLevel.Trace); + }); + + IProcessExecutor processExecutor = null; + processExecutor = new ProcessExecutor( + new PosixProcessFeatures( + new Lazy(() => processExecutor), + new DefaultIOManager(), + loggerFactory.CreateLogger()), + loggerFactory.CreateLogger(), + loggerFactory); + using var subProc = processExecutor + .LaunchProcess( + "dotnet", + pathToSignalTestApp, + $"run -c {CurrentConfig} --no-build", + true, + true, + true); + + await Task.Delay(TimeSpan.FromSeconds(10)); + + using var killProc = global::System.Diagnostics.Process.Start("kill", $"-SIGUSR1 {subProc.Id}"); + killProc.WaitForExit(); + + var exitTask = subProc.Lifetime; + + await Task.WhenAny(exitTask, Task.Delay(TimeSpan.FromSeconds(10))); + + if (!exitTask.IsCompleted) + subProc.Terminate(); + + var exitCode = await exitTask; + + using var cts = new CancellationTokenSource(TimeSpan.FromSeconds(10)); + global::System.Console.WriteLine(await subProc.GetCombinedOutput(cts.Token)); + + Assert.AreEqual(0, exitCode); + } + } +} diff --git a/tests/Tgstation.Server.Host.Tests/Tgstation.Server.Host.Tests.csproj b/tests/Tgstation.Server.Host.Tests/Tgstation.Server.Host.Tests.csproj index 478e4ad02a..849ee940f7 100644 --- a/tests/Tgstation.Server.Host.Tests/Tgstation.Server.Host.Tests.csproj +++ b/tests/Tgstation.Server.Host.Tests/Tgstation.Server.Host.Tests.csproj @@ -8,14 +8,14 @@ - + all runtime; build; native; contentfiles; analyzers; buildtransitive - + - - + + diff --git a/tests/Tgstation.Server.Host.Watchdog.Tests/Tgstation.Server.Host.Watchdog.Tests.csproj b/tests/Tgstation.Server.Host.Watchdog.Tests/Tgstation.Server.Host.Watchdog.Tests.csproj index 2bb828c98b..239f423803 100644 --- a/tests/Tgstation.Server.Host.Watchdog.Tests/Tgstation.Server.Host.Watchdog.Tests.csproj +++ b/tests/Tgstation.Server.Host.Watchdog.Tests/Tgstation.Server.Host.Watchdog.Tests.csproj @@ -1,4 +1,4 @@ - + netcoreapp3.1 @@ -14,14 +14,14 @@ - + all runtime; build; native; contentfiles; analyzers; buildtransitive - + - - + + diff --git a/tests/Tgstation.Server.Tests/IntegrationTest.cs b/tests/Tgstation.Server.Tests/IntegrationTest.cs index 24e3ebeee1..85cf120b56 100644 --- a/tests/Tgstation.Server.Tests/IntegrationTest.cs +++ b/tests/Tgstation.Server.Tests/IntegrationTest.cs @@ -115,6 +115,7 @@ namespace Tgstation.Server.Tests Assert.Inconclusive(notSupportedException.Message); } } + Assert.IsTrue(server.RestartRequested, "Server not requesting restart!"); } diff --git a/tests/Tgstation.Server.Tests/Tgstation.Server.Tests.csproj b/tests/Tgstation.Server.Tests/Tgstation.Server.Tests.csproj index df3a552398..d2ac1d3319 100644 --- a/tests/Tgstation.Server.Tests/Tgstation.Server.Tests.csproj +++ b/tests/Tgstation.Server.Tests/Tgstation.Server.Tests.csproj @@ -8,14 +8,14 @@ - + all runtime; build; native; contentfiles; analyzers; buildtransitive - + - - + + diff --git a/tgstation-server.sln b/tgstation-server.sln index 6ff1e7bb44..257b86cc4d 100644 --- a/tgstation-server.sln +++ b/tgstation-server.sln @@ -48,6 +48,9 @@ EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Tgstation.Server.Client.Tests", "tests\Tgstation.Server.Client.Tests\Tgstation.Server.Client.Tests.csproj", "{E5301AF1-4F74-4982-BF24-95F23CC5D5B2}" EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Tgstation.Server.Host.Tests", "tests\Tgstation.Server.Host.Tests\Tgstation.Server.Host.Tests.csproj", "{A3362FF6-550F-480F-859E-8EC1EB6EAB31}" + ProjectSection(ProjectDependencies) = postProject + {5813CC33-B16C-485D-A74D-20204DDF6542} = {5813CC33-B16C-485D-A74D-20204DDF6542} + EndProjectSection EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Tgstation.Server.Host.Watchdog", "src\Tgstation.Server.Host.Watchdog\Tgstation.Server.Host.Watchdog.csproj", "{5D2D682C-6BF0-439C-850B-6AB945BBEAEA}" ProjectSection(ProjectDependencies) = postProject @@ -180,6 +183,8 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "ISSUE_TEMPLATE", "ISSUE_TEM .github\ISSUE_TEMPLATE\feature_request.md = .github\ISSUE_TEMPLATE\feature_request.md EndProjectSection EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Tgstation.Server.Host.Tests.Signals", "tests\Tgstation.Server.Host.Tests.Signals\Tgstation.Server.Host.Tests.Signals.csproj", "{5813CC33-B16C-485D-A74D-20204DDF6542}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -296,6 +301,14 @@ Global {5CB51532-55F0-4255-B6E5-69ED5CCD14CD}.Release|Any CPU.Build.0 = Release|Any CPU {5CB51532-55F0-4255-B6E5-69ED5CCD14CD}.ReleaseNoService|Any CPU.ActiveCfg = Release|Any CPU {5CB51532-55F0-4255-B6E5-69ED5CCD14CD}.ReleaseNoService|Any CPU.Build.0 = Release|Any CPU + {5813CC33-B16C-485D-A74D-20204DDF6542}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {5813CC33-B16C-485D-A74D-20204DDF6542}.Debug|Any CPU.Build.0 = Debug|Any CPU + {5813CC33-B16C-485D-A74D-20204DDF6542}.DebugNoService|Any CPU.ActiveCfg = Debug|Any CPU + {5813CC33-B16C-485D-A74D-20204DDF6542}.DebugNoService|Any CPU.Build.0 = Debug|Any CPU + {5813CC33-B16C-485D-A74D-20204DDF6542}.Release|Any CPU.ActiveCfg = Release|Any CPU + {5813CC33-B16C-485D-A74D-20204DDF6542}.Release|Any CPU.Build.0 = Release|Any CPU + {5813CC33-B16C-485D-A74D-20204DDF6542}.ReleaseNoService|Any CPU.ActiveCfg = Release|Any CPU + {5813CC33-B16C-485D-A74D-20204DDF6542}.ReleaseNoService|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE @@ -322,6 +335,7 @@ Global {103C61AB-67D6-46FE-AA47-CC633B88EE0F} = {82066812-6C73-4360-943B-B23F2F491261} {28CDEB8F-2B2A-47A2-985B-5E2487E8D096} = {E82104F4-F5C4-4786-ACD4-B635166CDB21} {CFFD7992-E73A-4D1F-9D7A-C817C07B7BEB} = {E82104F4-F5C4-4786-ACD4-B635166CDB21} + {5813CC33-B16C-485D-A74D-20204DDF6542} = {316141B0-CD21-4769-A013-D53DA9B9EC09} EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {DFD36C95-3E49-41C7-ACDB-86BAF5B18A79} diff --git a/tools/ReleaseNotes/ReleaseNotes.csproj b/tools/ReleaseNotes/ReleaseNotes.csproj index aa74527956..b0fc22da50 100644 --- a/tools/ReleaseNotes/ReleaseNotes.csproj +++ b/tools/ReleaseNotes/ReleaseNotes.csproj @@ -7,7 +7,7 @@ - +