diff --git a/build/package/winget/Tgstation.Server.Host.Service.Configure/Program.cs b/build/package/winget/Tgstation.Server.Host.Service.Configure/Program.cs
index 13b2ae64c3..9d5c5bb53b 100644
--- a/build/package/winget/Tgstation.Server.Host.Service.Configure/Program.cs
+++ b/build/package/winget/Tgstation.Server.Host.Service.Configure/Program.cs
@@ -13,7 +13,7 @@ var installDir = Path.GetDirectoryName(
process.StartInfo.WorkingDirectory = installDir;
process.StartInfo.FileName = Path.Combine(
- process.StartInfo.WorkingDirectory,
+ installDir,
"Tgstation.Server.Host.Service.exe");
/*
@@ -44,11 +44,22 @@ var uninstall = args
var shortcut = uiLevel == 42069;
+bool startService;
+if (shortcut)
+{
+ Console.WriteLine();
+ Console.WriteLine("This program will configure tgstation-server. Would you like to restart the service after doing so?");
+ Console.Write("A restart is required to apply changes. It will not kill your DreamDaemon instances provided you are running a version >=5.13.0 (Y/n): ");
+ var keyInfo = Console.ReadKey();
+ startService = keyInfo.Key == ConsoleKey.Enter || keyInfo.Key == ConsoleKey.Y;
+ Console.WriteLine();
+}
+else
+ startService = interactive;
+
process.StartInfo.Arguments = uninstall
? "-u"
- : shortcut
- ? "-c"
- : $"-i -f {(interactive ? "-c" : String.Empty)} {(silent ? "-s" : String.Empty)}";
+ : $"-i -f -x{(startService ? " -r" : String.Empty)}{((interactive || shortcut) ? " -c" : String.Empty)}{(silent ? " -s" : String.Empty)}";
process.Start();
@@ -65,7 +76,7 @@ if (uninstall)
if ((shortcut || (interactive && !uninstall)) && process.ExitCode == 0)
{
// try and lockdown the appsettings file
- var fileInfo = new FileInfo("appsettings.Production.yml");
+ var fileInfo = new FileInfo(Path.Combine(installDir, "appsettings.Production.yml"));
//get security access
FileSecurity fs = fileInfo.GetAccessControl();
@@ -75,46 +86,16 @@ if ((shortcut || (interactive && !uninstall)) && process.ExitCode == 0)
// Explicitly grant admins and SYSTEM
fs.AddAccessRule(new FileSystemAccessRule("Administrators", FileSystemRights.FullControl, AccessControlType.Allow));
- fs.AddAccessRule(new FileSystemAccessRule("SYSTEM", FileSystemRights.Read, AccessControlType.Allow));
+ fs.AddAccessRule(new FileSystemAccessRule("NT AUTHORITY\\SYSTEM", FileSystemRights.Read, AccessControlType.Allow));
fileInfo.SetAccessControl(fs);
+}
- var startServer = interactive;
- if (shortcut)
- {
- Console.WriteLine();
- Console.Write("tgstation-server is now configured. Would you like to (re)start the service? (Y/n): ");
- var keyInfo = Console.ReadKey();
- Console.WriteLine();
- if (keyInfo.Key == ConsoleKey.Y || keyInfo.Key == ConsoleKey.Enter)
- {
- using (var stopService = new Process())
- {
- stopService.StartInfo.FileName = "sc";
- stopService.StartInfo.Arguments = "stop tgstation-server";
- stopService.Start();
- await stopService.WaitForExitAsync(CancellationToken.None); // DCT: None available
- }
-
- startServer = true;
- }
- }
-
- if (startServer)
- {
- using var startService = new Process();
- startService.StartInfo.FileName = "sc";
- startService.StartInfo.Arguments = "start tgstation-server";
- startService.Start();
- await startService.WaitForExitAsync(CancellationToken.None); // DCT: None available
- }
-
- if (shortcut)
- {
- Console.WriteLine("Press any key to exit this program...");
- Console.ReadKey();
- Console.WriteLine();
- }
+if (shortcut)
+{
+ Console.WriteLine("Press any key to exit this program...");
+ Console.ReadKey();
+ Console.WriteLine();
}
// returning non-zero can make the installation uninstallable, no thanks
diff --git a/src/Tgstation.Server.Host.Common/PipeCommands.cs b/src/Tgstation.Server.Host.Common/PipeCommands.cs
new file mode 100644
index 0000000000..10390cbfbc
--- /dev/null
+++ b/src/Tgstation.Server.Host.Common/PipeCommands.cs
@@ -0,0 +1,35 @@
+using System.Collections.Generic;
+
+namespace Tgstation.Server.Host.Common
+{
+ ///
+ /// Values able to be passed via the update file path.
+ ///
+ public static class PipeCommands
+ {
+ ///
+ /// Stops the server ASAP, shutting down any running instances.
+ ///
+ public const string CommandStop = "stop";
+
+ ///
+ /// Stops the server eventually, waiting for the games in any running instances to reboot.
+ ///
+ public const string CommandGracefulShutdown = "graceful";
+
+ ///
+ /// Stops the server ASAP, detaching the watchdog for any running instances.
+ ///
+ public const string CommandDetachingShutdown = "detach";
+
+ ///
+ /// All of the represented as a .
+ ///
+ public static IReadOnlyList AllCommands { get; } = new[]
+ {
+ CommandStop,
+ CommandGracefulShutdown,
+ CommandDetachingShutdown,
+ };
+ }
+}
diff --git a/src/Tgstation.Server.Host.Watchdog/SignalChecker.cs b/src/Tgstation.Server.Host.Console/PosixSignalChecker.cs
similarity index 70%
rename from src/Tgstation.Server.Host.Watchdog/SignalChecker.cs
rename to src/Tgstation.Server.Host.Console/PosixSignalChecker.cs
index 3be780fb39..a2ffceb69f 100644
--- a/src/Tgstation.Server.Host.Watchdog/SignalChecker.cs
+++ b/src/Tgstation.Server.Host.Console/PosixSignalChecker.cs
@@ -7,22 +7,33 @@ using Microsoft.Extensions.Logging;
using Mono.Unix;
using Mono.Unix.Native;
-namespace Tgstation.Server.Host.Watchdog
+using Tgstation.Server.Host.Watchdog;
+
+namespace Tgstation.Server.Host.Console
{
///
- /// Helper for checking POSIX signals.
+ /// for checking POSIX signals.
///
- static class SignalChecker
+ sealed class PosixSignalChecker : ISignalChecker
{
///
- /// Forwards certain signals to a given .
+ /// The for the .
///
- /// The to write to.
- /// The of the process to forward signals to.
- /// The for the operation.
- /// A representing the running operation.
- public static async Task CheckSignals(ILogger logger, int childPid, CancellationToken cancellationToken)
+ readonly ILogger logger;
+
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ /// The value of .
+ public PosixSignalChecker(ILogger logger)
{
+ this.logger = logger ?? throw new ArgumentNullException(nameof(logger));
+ }
+
+ ///
+ public async Task CheckSignals(Func startChild, CancellationToken cancellationToken)
+ {
+ var (childPid, _) = startChild?.Invoke(null) ?? throw new ArgumentNullException(nameof(startChild));
var signalTcs = new TaskCompletionSource();
async Task CheckSignal(Signum signum)
{
diff --git a/src/Tgstation.Server.Host.Console/Program.cs b/src/Tgstation.Server.Host.Console/Program.cs
index c1c9c4dbc7..1a3c02cd72 100644
--- a/src/Tgstation.Server.Host.Console/Program.cs
+++ b/src/Tgstation.Server.Host.Console/Program.cs
@@ -32,17 +32,26 @@ namespace Tgstation.Server.Host.Console
///
/// The arguments for the .
/// A representing the running operation.
- internal static async Task Main(string[] args)
+ internal static async Task Main(string[] args)
{
- using var loggerFactory = LoggerFactory.Create(builder => builder.AddConsole());
var arguments = new List(args);
var trace = arguments.Remove("--trace-host-watchdog");
var debug = arguments.Remove("--debug-host-watchdog");
+ using var loggerFactory = LoggerFactory.Create(builder =>
+ {
+ if (trace)
+ builder.SetMinimumLevel(LogLevel.Trace);
+ else if (debug)
+ builder.SetMinimumLevel(LogLevel.Debug);
+
+ builder.AddConsole();
+ });
+
if (trace && debug)
{
loggerFactory.CreateLogger(nameof(Program)).LogCritical("Please specify only 1 of --trace-host-watchdog or --debug-host-watchdog!");
- return;
+ return 2;
}
using var cts = new CancellationTokenSource();
@@ -55,7 +64,15 @@ namespace Tgstation.Server.Host.Console
b.Cancel = true;
cts.Cancel();
};
- await WatchdogFactory.CreateWatchdog(loggerFactory).RunAsync(false, arguments.ToArray(), cts.Token);
+
+ var watchdog = WatchdogFactory.CreateWatchdog(
+ new PosixSignalChecker(
+ loggerFactory.CreateLogger()),
+ loggerFactory);
+
+ return await watchdog.RunAsync(false, arguments.ToArray(), cts.Token)
+ ? 0
+ : 1;
}
finally
{
diff --git a/src/Tgstation.Server.Host.Service/NoopSignalChecker.cs b/src/Tgstation.Server.Host.Service/NoopSignalChecker.cs
new file mode 100644
index 0000000000..485d8f152e
--- /dev/null
+++ b/src/Tgstation.Server.Host.Service/NoopSignalChecker.cs
@@ -0,0 +1,21 @@
+using System;
+using System.Threading;
+using System.Threading.Tasks;
+
+using Tgstation.Server.Host.Watchdog;
+
+namespace Tgstation.Server.Host.Service
+{
+ ///
+ /// No-op .
+ ///
+ sealed class NoopSignalChecker : ISignalChecker
+ {
+ ///
+ public Task CheckSignals(Func startChild, CancellationToken cancellationToken)
+ {
+ startChild(null);
+ return Task.CompletedTask;
+ }
+ }
+}
diff --git a/src/Tgstation.Server.Host.Service/Program.cs b/src/Tgstation.Server.Host.Service/Program.cs
index db02337785..f2f78bc9a8 100644
--- a/src/Tgstation.Server.Host.Service/Program.cs
+++ b/src/Tgstation.Server.Host.Service/Program.cs
@@ -15,6 +15,7 @@ using McMaster.Extensions.CommandLineUtils;
using Microsoft.Extensions.Logging;
+using Tgstation.Server.Host.Common;
using Tgstation.Server.Host.Watchdog;
namespace Tgstation.Server.Host.Service
@@ -37,6 +38,18 @@ namespace Tgstation.Server.Host.Service
[Option(ShortName = "u")]
public bool Uninstall { get; }
+ ///
+ /// The --detach or -x option. Valid only with .
+ ///
+ [Option(ShortName = "x")]
+ public bool Detach { get; }
+
+ ///
+ /// The --resume or -r option.
+ ///
+ [Option(ShortName = "r")]
+ public bool Resume { get; }
+
///
/// The --install or -i option.
///
@@ -97,9 +110,11 @@ namespace Tgstation.Server.Host.Service
/// A representing the running operation.
public async Task OnExecuteAsync()
{
+ var standardRun = !Install && !Uninstall && !Configure;
+
if (Environment.UserInteractive)
{
- if (!Install && !Uninstall && !Configure)
+ if (standardRun)
{
var result = MessageBox.Show("You are running the TGS windows service executable directly. It should only be run by the service control manager. Would you like to install and configure the service in this location?", "TGS Service", MessageBoxButtons.YesNo);
if (result != DialogResult.Yes)
@@ -112,12 +127,13 @@ namespace Tgstation.Server.Host.Service
{
// try to restart as admin
// its windows, first arg is .exe name guaranteed
- var exe = Environment.GetCommandLineArgs().First();
+ var args = Environment.GetCommandLineArgs();
+ var exe = args.First();
var startInfo = new ProcessStartInfo
{
UseShellExecute = true,
Verb = "runas",
- Arguments = $"{(Install ? "-i" : Uninstall ? "-u" : String.Empty)} {(Configure ? "-c" : String.Empty)} {(Force ? "-f" : String.Empty)}",
+ Arguments = String.Join(" ", args.Skip(1)),
FileName = exe,
WorkingDirectory = Environment.CurrentDirectory,
};
@@ -126,17 +142,10 @@ namespace Tgstation.Server.Host.Service
}
}
- if (Install)
- {
- if (Uninstall)
- return; // oh no, it's retarded...
+ if (Configure)
+ await RunConfigure(CancellationToken.None); // DCT: None available
- RunServiceInstall();
-
- if (Configure && !Silent)
- Console.WriteLine("For this first run we'll launch the console runner so you may use the setup wizard.");
- }
- else if (Uninstall)
+ if (Uninstall)
using (var installer = new ServiceInstaller())
{
installer.Context = new InstallContext("tgs-uninstall.log", null);
@@ -146,36 +155,56 @@ namespace Tgstation.Server.Host.Service
installer.ServiceName = ServerService.Name;
installer.Uninstall(null);
}
- else if (!Configure)
+
+ if (Install)
+ {
+ RunServiceInstall();
+ }
+
+ if (standardRun)
{
using var service = new ServerService(WatchdogFactory, Trace ? LogLevel.Trace : Debug ? LogLevel.Debug : LogLevel.Information);
ServiceBase.Run(service);
}
- if (Configure)
- await RunConfigure(CancellationToken.None); // DCT: None available
+ if (Resume)
+ foreach (ServiceController sc in ServiceController.GetServices())
+ if (sc.ServiceName == ServerService.Name)
+ {
+ sc.Start();
+ break;
+ }
}
///
/// Attempt to install the TGS Service.
///
- void RunServiceInstall()
+ /// if the service was stopped or detached as a result, otherwise.
+ bool RunServiceInstall()
{
// First check if the service already exists
+ bool serviceStopped = false;
if (Force || Environment.UserInteractive)
foreach (ServiceController sc in ServiceController.GetServices())
- if (sc.ServiceName == "tgstation-server" || sc.ServiceName == "tgstation-server-4")
+ if (sc.ServiceName == ServerService.Name || sc.ServiceName == "tgstation-server-4")
{
DialogResult result = !Force
? MessageBox.Show($"You already have another TGS service installed ({sc.ServiceName}). Would you like to uninstall it now? Pressing \"No\" will cancel this install.", "TGS Service", MessageBoxButtons.YesNo)
: DialogResult.Yes;
if (result != DialogResult.Yes)
- return; // is this needed after exit?
+ return false; // is this needed after exit?
// Stop it first to give it some cleanup time
if (sc.Status == ServiceControllerStatus.Running)
{
- sc.Stop();
+ if (Detach)
+ sc.ExecuteCommand(
+ ServerService.GetCommand(
+ PipeCommands.CommandDetachingShutdown)
+ .Value);
+ else
+ sc.Stop();
+
sc.WaitForStatus(ServiceControllerStatus.Stopped);
}
@@ -188,6 +217,8 @@ namespace Tgstation.Server.Host.Service
serviceInstaller.ServiceName = sc.ServiceName;
serviceInstaller.Uninstall(null);
+
+ serviceStopped = true;
}
using var processInstaller = new ServiceProcessInstaller();
@@ -198,7 +229,7 @@ namespace Tgstation.Server.Host.Service
if (Silent)
installer.Context.Parameters["LogToConsole"] = false.ToString();
installer.Description = "tgstation-server running as a windows service";
- installer.DisplayName = "tgstation-server";
+ installer.DisplayName = ServerService.Name;
installer.StartType = ServiceStartMode.Automatic;
installer.ServicesDependedOn = new string[] { "Tcpip", "Dhcp", "Dnscache" };
installer.ServiceName = ServerService.Name;
@@ -206,6 +237,8 @@ namespace Tgstation.Server.Host.Service
var state = new ListDictionary();
installer.Install(state);
+
+ return serviceStopped;
}
///
@@ -220,8 +253,9 @@ namespace Tgstation.Server.Host.Service
if (!Silent)
builder.AddConsole();
});
- await WatchdogFactory.CreateWatchdog(loggerFactory)
- .RunAsync(true, Array.Empty(), cancellationToken);
+
+ var watchdog = WatchdogFactory.CreateWatchdog(new NoopSignalChecker(), loggerFactory);
+ await watchdog.RunAsync(true, Array.Empty(), cancellationToken);
}
}
}
diff --git a/src/Tgstation.Server.Host.Service/ServerService.cs b/src/Tgstation.Server.Host.Service/ServerService.cs
index 775946d42c..b5a7434c68 100644
--- a/src/Tgstation.Server.Host.Service/ServerService.cs
+++ b/src/Tgstation.Server.Host.Service/ServerService.cs
@@ -1,6 +1,7 @@
using System;
using System.Diagnostics;
-using System.Globalization;
+using System.IO;
+using System.IO.Pipes;
using System.ServiceProcess;
using System.Threading;
using System.Threading.Tasks;
@@ -8,6 +9,7 @@ using System.Threading.Tasks;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Logging.EventLog;
+using Tgstation.Server.Host.Common;
using Tgstation.Server.Host.Watchdog;
namespace Tgstation.Server.Host.Service
@@ -15,7 +17,7 @@ namespace Tgstation.Server.Host.Service
///
/// Represents a as a .
///
- sealed class ServerService : ServiceBase
+ sealed class ServerService : ServiceBase, ISignalChecker
{
///
/// The canonical windows service name.
@@ -33,12 +35,17 @@ namespace Tgstation.Server.Host.Service
readonly LogLevel minimumLogLevel;
///
- /// The used by the service.
+ /// The used by the .
///
ILoggerFactory loggerFactory;
///
- /// The that represents the running service.
+ /// The for the .
+ ///
+ ILogger logger;
+
+ ///
+ /// The that represents the running .
///
Task watchdogTask;
@@ -47,6 +54,25 @@ namespace Tgstation.Server.Host.Service
///
CancellationTokenSource cancellationTokenSource;
+ ///
+ /// The the server process is using.
+ ///
+ AnonymousPipeServerStream pipeServer;
+
+ ///
+ /// Gets the value of a given .
+ ///
+ /// The .
+ /// The value of the command or if it was unrecognized.
+ public static int? GetCommand(string command)
+ => command switch
+ {
+ PipeCommands.CommandStop => 128, // Windows only allows commands 128-256: https://stackoverflow.com/a/62858106
+ PipeCommands.CommandGracefulShutdown => 129,
+ PipeCommands.CommandDetachingShutdown => 130,
+ _ => null,
+ };
+
///
/// Initializes a new instance of the class.
///
@@ -59,14 +85,47 @@ namespace Tgstation.Server.Host.Service
ServiceName = Name;
}
+ ///
+ public async Task CheckSignals(Func startChildAndGetPid, CancellationToken cancellationToken)
+ {
+ using (pipeServer = new AnonymousPipeServerStream(PipeDirection.Out, HandleInheritability.Inheritable))
+ {
+ var (_, lifetimeTask) = startChildAndGetPid($"--Internal:CommandPipe={pipeServer.GetClientHandleAsString()}");
+ pipeServer.DisposeLocalCopyOfClientHandle();
+ await lifetimeTask;
+ }
+ }
+
///
protected override void Dispose(bool disposing)
{
- loggerFactory?.Dispose();
- cancellationTokenSource?.Dispose();
+ if (disposing)
+ {
+ loggerFactory?.Dispose();
+ cancellationTokenSource?.Dispose();
+ pipeServer?.Dispose();
+ }
+
base.Dispose(disposing);
}
+ ///
+ protected override void OnCustomCommand(int command)
+ {
+ var commandsToCheck = PipeCommands.AllCommands;
+ foreach (var stringCommand in commandsToCheck)
+ {
+ var commandId = GetCommand(stringCommand);
+ if (command == commandId)
+ {
+ SendCommandToUpdatePath(stringCommand);
+ return;
+ }
+ }
+
+ logger.LogWarning("Received unknown service command: {command}", command);
+ }
+
///
protected override void OnStart(string[] args)
{
@@ -79,9 +138,11 @@ namespace Tgstation.Server.Host.Service
SourceName = EventLog.Source,
Filter = (message, logLevel) => logLevel >= minimumLogLevel,
}));
+
+ logger = loggerFactory.CreateLogger();
}
- var watchdog = watchdogFactory.CreateWatchdog(loggerFactory);
+ var watchdog = watchdogFactory.CreateWatchdog(this, loggerFactory);
cancellationTokenSource?.Dispose();
cancellationTokenSource = new CancellationTokenSource();
@@ -105,24 +166,50 @@ namespace Tgstation.Server.Host.Service
/// A representing the running operation.
async Task RunWatchdog(IWatchdog watchdog, string[] args, CancellationToken cancellationToken)
{
- await watchdog.RunAsync(false, args, cancellationTokenSource.Token);
+ await watchdog.RunAsync(false, args, cancellationToken);
- void StopServiceAsync()
+ async void StopServiceAsync()
{
try
{
- Task.Run(Stop, cancellationToken);
+ await Task.Run(Stop, cancellationToken); // DCT intentional
}
- catch (OperationCanceledException)
+ catch (OperationCanceledException ex)
{
+ logger.LogTrace(ex, "Stopping service cancelled!");
}
- catch (Exception e)
+ catch (Exception ex)
{
- EventLog.WriteEntry(String.Format(CultureInfo.InvariantCulture, "Error stopping service! Exception: {0}", e));
+ logger.LogError(ex, "Error stopping service!");
}
}
StopServiceAsync();
}
+
+ ///
+ /// Sends a command to the main server process.
+ ///
+ /// One of the .
+ void SendCommandToUpdatePath(string command)
+ {
+ var localPipeServer = pipeServer;
+ if (localPipeServer == null)
+ {
+ logger.LogWarning("Unable to send command \"{command}\" to main server process. Is the service running?", command);
+ return;
+ }
+
+ logger.LogDebug("Send command: {command}", command);
+ try
+ {
+ using var streamWriter = new StreamWriter(localPipeServer);
+ streamWriter.WriteLine(command);
+ }
+ catch (Exception ex)
+ {
+ logger.LogError(ex, "Error attempting to send command \"{command}\"", command);
+ }
+ }
}
}
diff --git a/src/Tgstation.Server.Host.Watchdog/ISignalChecker.cs b/src/Tgstation.Server.Host.Watchdog/ISignalChecker.cs
new file mode 100644
index 0000000000..8ab3b06df6
--- /dev/null
+++ b/src/Tgstation.Server.Host.Watchdog/ISignalChecker.cs
@@ -0,0 +1,20 @@
+using System;
+using System.Threading;
+using System.Threading.Tasks;
+
+namespace Tgstation.Server.Host.Watchdog
+{
+ ///
+ /// For relaying signals received to the host process.
+ ///
+ public interface ISignalChecker
+ {
+ ///
+ /// Relays signals received to the host process.
+ ///
+ /// An to start the main process. It accepts an optional additional command line argument as a paramter and returns it's and lifetime .
+ /// The for the operation.
+ /// A representing the running operation.
+ Task CheckSignals(Func startChild, CancellationToken cancellationToken);
+ }
+}
diff --git a/src/Tgstation.Server.Host.Watchdog/IWatchdog.cs b/src/Tgstation.Server.Host.Watchdog/IWatchdog.cs
index cb587dfddc..e99f09e31b 100644
--- a/src/Tgstation.Server.Host.Watchdog/IWatchdog.cs
+++ b/src/Tgstation.Server.Host.Watchdog/IWatchdog.cs
@@ -14,7 +14,7 @@ namespace Tgstation.Server.Host.Watchdog
/// If the should just run the host configuration wizard and exit.
/// The arguments for the .
/// The for the operation.
- /// A representing the running operation.
- Task RunAsync(bool runConfigure, string[] args, CancellationToken cancellationToken);
+ /// A resulting in if there were no errors, otherwise.
+ Task RunAsync(bool runConfigure, string[] args, CancellationToken cancellationToken);
}
}
diff --git a/src/Tgstation.Server.Host.Watchdog/IWatchdogFactory.cs b/src/Tgstation.Server.Host.Watchdog/IWatchdogFactory.cs
index c4dfe2bd00..3569139fdd 100644
--- a/src/Tgstation.Server.Host.Watchdog/IWatchdogFactory.cs
+++ b/src/Tgstation.Server.Host.Watchdog/IWatchdogFactory.cs
@@ -10,8 +10,9 @@ namespace Tgstation.Server.Host.Watchdog
///
/// Create a .
///
+ /// The to use for relaying signals.
/// The to use for error reporting.
/// A new .
- IWatchdog CreateWatchdog(ILoggerFactory loggerFactory);
+ IWatchdog CreateWatchdog(ISignalChecker signalChecker, ILoggerFactory loggerFactory);
}
}
diff --git a/src/Tgstation.Server.Host.Watchdog/Watchdog.cs b/src/Tgstation.Server.Host.Watchdog/Watchdog.cs
index 1afd2cad85..26bac6d658 100644
--- a/src/Tgstation.Server.Host.Watchdog/Watchdog.cs
+++ b/src/Tgstation.Server.Host.Watchdog/Watchdog.cs
@@ -19,6 +19,11 @@ namespace Tgstation.Server.Host.Watchdog
/// This is a HACK but it works. Try not to break it if you wish to change it. Remember, this code doesn't get updated with the rest of the server.
sealed class Watchdog : IWatchdog
{
+ ///
+ /// The for the .
+ ///
+ readonly ISignalChecker signalChecker;
+
///
/// The for the .
///
@@ -27,16 +32,18 @@ namespace Tgstation.Server.Host.Watchdog
///
/// Initializes a new instance of the class.
///
+ /// The value of .
/// The value of .
- public Watchdog(ILogger logger)
+ public Watchdog(ISignalChecker signalChecker, ILogger logger)
{
+ this.signalChecker = signalChecker ?? throw new ArgumentNullException(nameof(signalChecker));
this.logger = logger ?? throw new ArgumentNullException(nameof(logger));
}
///
#pragma warning disable CA1502 // TODO: Decomplexify
#pragma warning disable CA1506
- public async Task RunAsync(bool runConfigure, string[] args, CancellationToken cancellationToken)
+ public async Task RunAsync(bool runConfigure, string[] args, CancellationToken cancellationToken)
{
logger.LogInformation("Host watchdog starting...");
int currentProcessId;
@@ -52,7 +59,7 @@ namespace Tgstation.Server.Host.Watchdog
if (dotnetPath == default)
{
logger.LogCritical("Unable to locate dotnet executable in PATH! Please ensure the .NET Core runtime is installed and is in your PATH!");
- return;
+ return false;
}
logger.LogInformation("Detected dotnet executable at {dotnetPath}", dotnetPath);
@@ -93,13 +100,13 @@ namespace Tgstation.Server.Host.Watchdog
if (assemblyPath.Contains("\""))
{
logger.LogCritical("Running from paths with \"'s in the name is not supported!");
- return;
+ return false;
}
if (!File.Exists(assemblyPath))
{
logger.LogCritical("Unable to locate host assembly!");
- return;
+ return false;
}
var watchdogVersion = executingAssembly.GetName().Version.ToString();
@@ -148,8 +155,15 @@ namespace Tgstation.Server.Host.Watchdog
var killedHostProcess = false;
try
{
- process.Start();
- var childPid = process.Id;
+ var processTask = tcs.Task;
+ (int, Task) StartProcess(string additionalArg)
+ {
+ if (additionalArg != null)
+ process.StartInfo.Arguments += $" {additionalArg}";
+
+ process.Start();
+ return (process.Id, processTask);
+ }
using (var processCts = new CancellationTokenSource())
using (processCts.Token.Register(() => tcs.TrySetResult(null)))
@@ -176,12 +190,9 @@ namespace Tgstation.Server.Host.Watchdog
}
}))
{
- var processTask = tcs.Task;
using var cts = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken);
- var checkerTask = isWindows
- ? Task.CompletedTask
- : SignalChecker.CheckSignals(logger, childPid, cts.Token);
+ var checkerTask = signalChecker.CheckSignals(StartProcess, cts.Token);
try
{
await processTask;
@@ -228,14 +239,14 @@ namespace Tgstation.Server.Host.Watchdog
if (runConfigure)
{
- logger.LogInformation("Exiting due to configuration check...");
- return;
+ logger.LogInformation("Exiting due to configure intent...");
+ return true;
}
switch ((HostExitCode)process.ExitCode)
{
case HostExitCode.CompleteExecution:
- return;
+ return true;
case HostExitCode.RestartRequested:
if (!cancellationToken.IsCancellationRequested)
logger.LogInformation("Watchdog will restart host..."); // just a restart
@@ -331,11 +342,14 @@ namespace Tgstation.Server.Host.Watchdog
catch (Exception ex)
{
logger.LogCritical(ex, "Host watchdog error!");
+ return false;
}
finally
{
logger.LogInformation("Host watchdog exiting...");
}
+
+ return true;
}
#pragma warning restore CA1502
#pragma warning restore CA1506
diff --git a/src/Tgstation.Server.Host.Watchdog/WatchdogFactory.cs b/src/Tgstation.Server.Host.Watchdog/WatchdogFactory.cs
index 083d89eafb..4b2fc89147 100644
--- a/src/Tgstation.Server.Host.Watchdog/WatchdogFactory.cs
+++ b/src/Tgstation.Server.Host.Watchdog/WatchdogFactory.cs
@@ -8,6 +8,10 @@ namespace Tgstation.Server.Host.Watchdog
public sealed class WatchdogFactory : IWatchdogFactory
{
///
- public IWatchdog CreateWatchdog(ILoggerFactory loggerFactory) => new Watchdog(loggerFactory?.CreateLogger() ?? throw new ArgumentNullException(nameof(loggerFactory)));
+ public IWatchdog CreateWatchdog(
+ ISignalChecker signalChecker,
+ ILoggerFactory loggerFactory) => new Watchdog(
+ signalChecker ?? throw new ArgumentNullException(nameof(signalChecker)),
+ loggerFactory?.CreateLogger() ?? throw new ArgumentNullException(nameof(loggerFactory)));
}
}
diff --git a/src/Tgstation.Server.Host/Configuration/InternalConfiguration.cs b/src/Tgstation.Server.Host/Configuration/InternalConfiguration.cs
new file mode 100644
index 0000000000..81bc785e16
--- /dev/null
+++ b/src/Tgstation.Server.Host/Configuration/InternalConfiguration.cs
@@ -0,0 +1,18 @@
+namespace Tgstation.Server.Host.Configuration
+{
+ ///
+ /// Unstable configuration options used internally by TGS.
+ ///
+ public sealed class InternalConfiguration
+ {
+ ///
+ /// The key for the the resides in.
+ ///
+ public const string Section = "Internal";
+
+ ///
+ /// The name of the pipe opened by the host watchdog, if any.
+ ///
+ public string CommandPipe { get; set; }
+ }
+}
diff --git a/src/Tgstation.Server.Host/Core/Application.cs b/src/Tgstation.Server.Host/Core/Application.cs
index ef312928a0..efd9da1238 100644
--- a/src/Tgstation.Server.Host/Core/Application.cs
+++ b/src/Tgstation.Server.Host/Core/Application.cs
@@ -132,6 +132,7 @@ namespace Tgstation.Server.Host.Core
services.UseStandardConfig(Configuration);
services.UseStandardConfig(Configuration);
services.UseStandardConfig(Configuration);
+ services.UseStandardConfig(Configuration);
// enable options which give us config reloading
services.AddOptions();
@@ -335,10 +336,8 @@ namespace Tgstation.Server.Host.Core
services.AddSingleton(x => new Lazy(() => x.GetRequiredService(), true));
services.AddSingleton();
- services.AddSingleton();
-
- services.AddSingleton();
- services.AddSingleton(x => x.GetRequiredService());
+ services.AddHostedService();
+ services.AddHostedService();
}
// configure file transfer services
@@ -368,10 +367,11 @@ namespace Tgstation.Server.Host.Core
// configure misc services
services.AddSingleton();
services.AddSingleton();
- services.AddFileDownloader();
services.AddSingleton();
services.AddSingleton();
+ services.AddHostedService();
+ services.AddFileDownloader();
services.AddGitHub();
// configure root services
diff --git a/src/Tgstation.Server.Host/Core/CommandPipeReader.cs b/src/Tgstation.Server.Host/Core/CommandPipeReader.cs
new file mode 100644
index 0000000000..a1afa80cbe
--- /dev/null
+++ b/src/Tgstation.Server.Host/Core/CommandPipeReader.cs
@@ -0,0 +1,109 @@
+using System;
+using System.IO;
+using System.IO.Pipes;
+using System.Threading;
+using System.Threading.Tasks;
+
+using Microsoft.Extensions.Hosting;
+using Microsoft.Extensions.Logging;
+using Microsoft.Extensions.Options;
+
+using Tgstation.Server.Host.Common;
+using Tgstation.Server.Host.Configuration;
+using Tgstation.Server.Host.Extensions;
+
+namespace Tgstation.Server.Host.Core
+{
+ ///
+ /// Reads from the command pipe opened by the host watchdog.
+ ///
+ sealed class CommandPipeReader : BackgroundService
+ {
+ ///
+ /// The for the .
+ ///
+ readonly IServerControl serverControl;
+
+ ///
+ /// The for the .
+ ///
+ readonly ILogger logger;
+
+ ///
+ /// The for the .
+ ///
+ readonly InternalConfiguration internalConfiguration;
+
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ /// The value of .
+ /// The containing the value of .
+ /// The value of .
+ public CommandPipeReader(
+ IServerControl serverControl,
+ IOptions internalConfigurationOptions,
+ ILogger logger)
+ {
+ this.serverControl = serverControl ?? throw new ArgumentNullException(nameof(serverControl));
+ internalConfiguration = internalConfigurationOptions?.Value ?? throw new ArgumentNullException(nameof(internalConfigurationOptions));
+ this.logger = logger ?? throw new ArgumentNullException(nameof(logger));
+ }
+
+ ///
+ protected override async Task ExecuteAsync(CancellationToken cancellationToken)
+ {
+ logger.LogTrace("Starting...");
+
+ var pipeName = internalConfiguration.CommandPipe;
+ if (string.IsNullOrWhiteSpace(pipeName))
+ {
+ logger.LogDebug("No command pipe name specified in configuration");
+ return;
+ }
+
+ try
+ {
+ await using var pipeClient = new AnonymousPipeClientStream(PipeDirection.In, pipeName);
+ using var streamReader = new StreamReader(pipeClient, leaveOpen: true);
+ while (!cancellationToken.IsCancellationRequested)
+ {
+ logger.LogTrace("Waiting to read command line...");
+ var line = await streamReader.ReadLineAsync().WithToken(cancellationToken);
+
+ logger?.LogInformation("Received pipe command: {command}", line);
+ switch (line)
+ {
+ case PipeCommands.CommandStop:
+ await serverControl.Die(null);
+ break;
+ case PipeCommands.CommandGracefulShutdown:
+ await serverControl.GracefulShutdown(false);
+ break;
+ case PipeCommands.CommandDetachingShutdown:
+ await serverControl.GracefulShutdown(true);
+ break;
+ case null:
+ logger.LogDebug("Read null from pipe");
+ return;
+ default:
+ logger?.LogWarning("Unrecognized pipe command: {command}", line);
+ break;
+ }
+ }
+ }
+ catch (OperationCanceledException ex)
+ {
+ logger?.LogTrace(ex, "Command read task cancelled!");
+ }
+ catch (Exception ex)
+ {
+ logger?.LogError(ex, "Command read task errored!");
+ }
+ finally
+ {
+ logger?.LogTrace("Command read task exiting...");
+ }
+ }
+ }
+}
diff --git a/src/Tgstation.Server.Host/Server.cs b/src/Tgstation.Server.Host/Server.cs
index 1a42d25b40..b76891e3ec 100644
--- a/src/Tgstation.Server.Host/Server.cs
+++ b/src/Tgstation.Server.Host/Server.cs
@@ -114,8 +114,12 @@ namespace Tgstation.Server.Host
using (cancellationTokenSource = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken))
using (var fsWatcher = updatePath != null ? new FileSystemWatcher(Path.GetDirectoryName(updatePath)) : null)
{
- if (fsWatcher != null)
+ if (updatePath != null)
{
+ // If ever there is a NECESSARY update to the Host Watchdog, change this to use a pipe
+ // I don't know why I'm only realizing this in 2023 when this is 2019 code
+ // As it stands, FSWatchers use async I/O on Windows and block a new thread on Linux
+ // That's an acceptable, if saddening, resource loss for now
fsWatcher.Created += WatchForShutdownFileCreation;
fsWatcher.EnableRaisingEvents = true;
}
@@ -241,7 +245,14 @@ namespace Tgstation.Server.Host
public Task GracefulShutdown(bool detach) => RestartImpl(null, null, false, detach);
///
- public Task Die(Exception exception) => RestartImpl(null, exception, false, true);
+ public Task Die(Exception exception)
+ {
+ if (exception != null)
+ return RestartImpl(null, exception, false, true);
+
+ StopServerImmediate();
+ return Task.CompletedTask;
+ }
///
/// Throws an if the cannot be used.
@@ -377,7 +388,7 @@ namespace Tgstation.Server.Host
void StopServerImmediate()
{
shutdownInProgress = true;
- logger.LogTrace("Stopping host...");
+ logger.LogDebug("Stopping host...");
cancellationTokenSource.Cancel();
}
}
diff --git a/tests/Tgstation.Server.Host.Console.Tests/TestProgram.cs b/tests/Tgstation.Server.Host.Console.Tests/TestProgram.cs
index edb19af15e..0fc337843c 100644
--- a/tests/Tgstation.Server.Host.Console.Tests/TestProgram.cs
+++ b/tests/Tgstation.Server.Host.Console.Tests/TestProgram.cs
@@ -1,4 +1,4 @@
-using Microsoft.Extensions.Logging;
+using Microsoft.Extensions.Logging;
using Microsoft.VisualStudio.TestTools.UnitTesting;
using Moq;
using System;
@@ -17,9 +17,9 @@ namespace Tgstation.Server.Host.Console.Tests
{
var mockServer = new Mock();
var args = Array.Empty();
- mockServer.Setup(x => x.RunAsync(false, args, It.IsAny())).Returns(Task.CompletedTask).Verifiable();
+ mockServer.Setup(x => x.RunAsync(false, args, It.IsAny())).Returns(Task.FromResult(true)).Verifiable();
var mockServerFactory = new Mock();
- mockServerFactory.Setup(x => x.CreateWatchdog(It.IsAny())).Returns(mockServer.Object).Verifiable();
+ mockServerFactory.Setup(x => x.CreateWatchdog(It.IsNotNull(), It.IsNotNull())).Returns(mockServer.Object).Verifiable();
Program.WatchdogFactory = mockServerFactory.Object;
await Program.Main(args);
mockServer.VerifyAll();
diff --git a/tests/Tgstation.Server.Host.Service.Tests/TestServerService.cs b/tests/Tgstation.Server.Host.Service.Tests/TestServerService.cs
index 03846f15a6..13fb19381e 100644
--- a/tests/Tgstation.Server.Host.Service.Tests/TestServerService.cs
+++ b/tests/Tgstation.Server.Host.Service.Tests/TestServerService.cs
@@ -34,9 +34,9 @@ namespace Tgstation.Server.Host.Service.Tests
var mockWatchdog = new Mock();
var args = Array.Empty();
CancellationToken cancellationToken;
- mockWatchdog.Setup(x => x.RunAsync(false, args, It.IsAny())).Callback((bool x, string[] _, CancellationToken token) => cancellationToken = token).Returns(Task.CompletedTask).Verifiable();
+ mockWatchdog.Setup(x => x.RunAsync(false, args, It.IsAny())).Callback((bool x, string[] _, CancellationToken token) => cancellationToken = token).Returns(Task.FromResult(true)).Verifiable();
var mockWatchdogFactory = new Mock();
- mockWatchdogFactory.Setup(x => x.CreateWatchdog(It.IsNotNull())).Returns(mockWatchdog.Object).Verifiable();
+ mockWatchdogFactory.Setup(x => x.CreateWatchdog(It.IsNotNull(), It.IsNotNull())).Returns(mockWatchdog.Object).Verifiable();
using (var service = new ServerService(mockWatchdogFactory.Object, default))
{
diff --git a/tests/Tgstation.Server.Host.Watchdog.Tests/TestWatchdog.cs b/tests/Tgstation.Server.Host.Watchdog.Tests/TestWatchdog.cs
index 67c3eb9289..0ba2e31764 100644
--- a/tests/Tgstation.Server.Host.Watchdog.Tests/TestWatchdog.cs
+++ b/tests/Tgstation.Server.Host.Watchdog.Tests/TestWatchdog.cs
@@ -13,9 +13,11 @@ namespace Tgstation.Server.Host.Watchdog.Tests
[TestMethod]
public void TestConstruction()
{
- Assert.ThrowsException(() => new Watchdog(null));
- var mockLogger = new LoggerFactory().CreateLogger();
- var wd = new Watchdog(mockLogger);
+ Assert.ThrowsException(() => new Watchdog(null, null));
+ var mockSignalChecker = Mock.Of();
+ Assert.ThrowsException(() => new Watchdog(mockSignalChecker, null));
+ var mockLogger = Mock.Of>();
+ var wd = new Watchdog(mockSignalChecker, mockLogger);
}
}
}
diff --git a/tests/Tgstation.Server.Host.Watchdog.Tests/TestWatchdogFactory.cs b/tests/Tgstation.Server.Host.Watchdog.Tests/TestWatchdogFactory.cs
index afc5f24c62..f3bededdba 100644
--- a/tests/Tgstation.Server.Host.Watchdog.Tests/TestWatchdogFactory.cs
+++ b/tests/Tgstation.Server.Host.Watchdog.Tests/TestWatchdogFactory.cs
@@ -1,6 +1,8 @@
using Microsoft.Extensions.Logging;
using Microsoft.VisualStudio.TestTools.UnitTesting;
+using Moq;
+
namespace Tgstation.Server.Host.Watchdog.Tests
{
///
@@ -13,7 +15,10 @@ namespace Tgstation.Server.Host.Watchdog.Tests
public void TestCreateWatchdog()
{
var factory = new WatchdogFactory();
- Assert.IsNotNull(factory.CreateWatchdog(new LoggerFactory()));
+ Assert.IsNotNull(
+ factory.CreateWatchdog(
+ Mock.Of(),
+ Mock.Of()));
}
}
}