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 ad4db13d02..e053af0bcc 100644
--- a/src/Tgstation.Server.Host/Components/Watchdog/WatchdogBase.cs
+++ b/src/Tgstation.Server.Host/Components/Watchdog/WatchdogBase.cs
@@ -381,8 +381,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/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