mirror of
https://github.com/tgstation/tgstation-server.git
synced 2026-07-21 04:53:44 +01:00
Add Windows service commands 128, 129, and 130
- 128 triggers a server shutdown. - 129 and 130 are identical to SIGUSR1 and SIGUSR2 respectively, triggering a graceful shutdown and detaching shutdown respectively. - These are implemented via pipes and are preferred to using the update path going forward. Should eventually replace the param entirely at the next possibility. - New service params to detach and restart any running server as opposed to stopping entirely. - Defer all service control to Service executable from Configure app. - Console runner returns non-zero on error. - Fix setting ACL of appsettings.Production.yml.
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -0,0 +1,35 @@
|
||||
using System.Collections.Generic;
|
||||
|
||||
namespace Tgstation.Server.Host.Common
|
||||
{
|
||||
/// <summary>
|
||||
/// Values able to be passed via the update file path.
|
||||
/// </summary>
|
||||
public static class PipeCommands
|
||||
{
|
||||
/// <summary>
|
||||
/// Stops the server ASAP, shutting down any running instances.
|
||||
/// </summary>
|
||||
public const string CommandStop = "stop";
|
||||
|
||||
/// <summary>
|
||||
/// Stops the server eventually, waiting for the games in any running instances to reboot.
|
||||
/// </summary>
|
||||
public const string CommandGracefulShutdown = "graceful";
|
||||
|
||||
/// <summary>
|
||||
/// Stops the server ASAP, detaching the watchdog for any running instances.
|
||||
/// </summary>
|
||||
public const string CommandDetachingShutdown = "detach";
|
||||
|
||||
/// <summary>
|
||||
/// All of the <see cref="PipeCommands"/> represented as a <see cref="IReadOnlyCollection{T}"/>.
|
||||
/// </summary>
|
||||
public static IReadOnlyList<string> AllCommands { get; } = new[]
|
||||
{
|
||||
CommandStop,
|
||||
CommandGracefulShutdown,
|
||||
CommandDetachingShutdown,
|
||||
};
|
||||
}
|
||||
}
|
||||
+20
-9
@@ -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
|
||||
{
|
||||
/// <summary>
|
||||
/// Helper for checking POSIX signals.
|
||||
/// <see cref="ISignalChecker"/> for checking POSIX signals.
|
||||
/// </summary>
|
||||
static class SignalChecker
|
||||
sealed class PosixSignalChecker : ISignalChecker
|
||||
{
|
||||
/// <summary>
|
||||
/// Forwards certain signals to a given <paramref name="childPid"/>.
|
||||
/// The <see cref="ILogger"/> for the <see cref="PosixSignalChecker"/>.
|
||||
/// </summary>
|
||||
/// <param name="logger">The <see cref="ILogger"/> to write to.</param>
|
||||
/// <param name="childPid">The <see cref="System.Diagnostics.Process.Id"/> of the process to forward signals to.</param>
|
||||
/// <param name="cancellationToken">The <see cref="CancellationToken"/> for the operation.</param>
|
||||
/// <returns>A <see cref="Task"/> representing the running operation.</returns>
|
||||
public static async Task CheckSignals(ILogger logger, int childPid, CancellationToken cancellationToken)
|
||||
readonly ILogger<PosixSignalChecker> logger;
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="PosixSignalChecker"/> class.
|
||||
/// </summary>
|
||||
/// <param name="logger">The value of <see cref="logger"/>.</param>
|
||||
public PosixSignalChecker(ILogger<PosixSignalChecker> logger)
|
||||
{
|
||||
this.logger = logger ?? throw new ArgumentNullException(nameof(logger));
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public async Task CheckSignals(Func<string, (int, Task)> startChild, CancellationToken cancellationToken)
|
||||
{
|
||||
var (childPid, _) = startChild?.Invoke(null) ?? throw new ArgumentNullException(nameof(startChild));
|
||||
var signalTcs = new TaskCompletionSource<Signum>();
|
||||
async Task<Signum?> CheckSignal(Signum signum)
|
||||
{
|
||||
@@ -32,17 +32,26 @@ namespace Tgstation.Server.Host.Console
|
||||
/// </summary>
|
||||
/// <param name="args">The arguments for the <see cref="Program"/>.</param>
|
||||
/// <returns>A <see cref="Task"/> representing the running operation.</returns>
|
||||
internal static async Task Main(string[] args)
|
||||
internal static async Task<int> Main(string[] args)
|
||||
{
|
||||
using var loggerFactory = LoggerFactory.Create(builder => builder.AddConsole());
|
||||
var arguments = new List<string>(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<PosixSignalChecker>()),
|
||||
loggerFactory);
|
||||
|
||||
return await watchdog.RunAsync(false, arguments.ToArray(), cts.Token)
|
||||
? 0
|
||||
: 1;
|
||||
}
|
||||
finally
|
||||
{
|
||||
|
||||
@@ -0,0 +1,21 @@
|
||||
using System;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
using Tgstation.Server.Host.Watchdog;
|
||||
|
||||
namespace Tgstation.Server.Host.Service
|
||||
{
|
||||
/// <summary>
|
||||
/// No-op <see cref="ISignalChecker"/>.
|
||||
/// </summary>
|
||||
sealed class NoopSignalChecker : ISignalChecker
|
||||
{
|
||||
/// <inheritdoc />
|
||||
public Task CheckSignals(Func<string, (int, Task)> startChild, CancellationToken cancellationToken)
|
||||
{
|
||||
startChild(null);
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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; }
|
||||
|
||||
/// <summary>
|
||||
/// The --detach or -x option. Valid only with <see cref="Install"/>.
|
||||
/// </summary>
|
||||
[Option(ShortName = "x")]
|
||||
public bool Detach { get; }
|
||||
|
||||
/// <summary>
|
||||
/// The --resume or -r option.
|
||||
/// </summary>
|
||||
[Option(ShortName = "r")]
|
||||
public bool Resume { get; }
|
||||
|
||||
/// <summary>
|
||||
/// The --install or -i option.
|
||||
/// </summary>
|
||||
@@ -97,9 +110,11 @@ namespace Tgstation.Server.Host.Service
|
||||
/// <returns>A <see cref="Task"/> representing the running operation.</returns>
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Attempt to install the TGS Service.
|
||||
/// </summary>
|
||||
void RunServiceInstall()
|
||||
/// <returns><see langword="true"/> if the service was stopped or detached as a result, <see langword="false"/> otherwise.</returns>
|
||||
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;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -220,8 +253,9 @@ namespace Tgstation.Server.Host.Service
|
||||
if (!Silent)
|
||||
builder.AddConsole();
|
||||
});
|
||||
await WatchdogFactory.CreateWatchdog(loggerFactory)
|
||||
.RunAsync(true, Array.Empty<string>(), cancellationToken);
|
||||
|
||||
var watchdog = WatchdogFactory.CreateWatchdog(new NoopSignalChecker(), loggerFactory);
|
||||
await watchdog.RunAsync(true, Array.Empty<string>(), cancellationToken);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
/// <summary>
|
||||
/// Represents a <see cref="IWatchdog"/> as a <see cref="ServiceBase"/>.
|
||||
/// </summary>
|
||||
sealed class ServerService : ServiceBase
|
||||
sealed class ServerService : ServiceBase, ISignalChecker
|
||||
{
|
||||
/// <summary>
|
||||
/// The canonical windows service name.
|
||||
@@ -33,12 +35,17 @@ namespace Tgstation.Server.Host.Service
|
||||
readonly LogLevel minimumLogLevel;
|
||||
|
||||
/// <summary>
|
||||
/// The <see cref="ILoggerFactory"/> used by the service.
|
||||
/// The <see cref="ILoggerFactory"/> used by the <see cref="ServerService"/>.
|
||||
/// </summary>
|
||||
ILoggerFactory loggerFactory;
|
||||
|
||||
/// <summary>
|
||||
/// The <see cref="Task"/> that represents the running service.
|
||||
/// The <see cref="ILogger"/> for the <see cref="ServerService"/>.
|
||||
/// </summary>
|
||||
ILogger<ServerService> logger;
|
||||
|
||||
/// <summary>
|
||||
/// The <see cref="Task"/> that represents the running <see cref="ServerService"/>.
|
||||
/// </summary>
|
||||
Task watchdogTask;
|
||||
|
||||
@@ -47,6 +54,25 @@ namespace Tgstation.Server.Host.Service
|
||||
/// </summary>
|
||||
CancellationTokenSource cancellationTokenSource;
|
||||
|
||||
/// <summary>
|
||||
/// The <see cref="AnonymousPipeServerStream"/> the server process is using.
|
||||
/// </summary>
|
||||
AnonymousPipeServerStream pipeServer;
|
||||
|
||||
/// <summary>
|
||||
/// Gets the <see cref="int"/> value of a given <paramref name="command"/>.
|
||||
/// </summary>
|
||||
/// <param name="command">The <see cref="PipeCommands"/>.</param>
|
||||
/// <returns>The <see cref="int"/> value of the command or <see langword="null"/> if it was unrecognized.</returns>
|
||||
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,
|
||||
};
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="ServerService"/> class.
|
||||
/// </summary>
|
||||
@@ -59,14 +85,47 @@ namespace Tgstation.Server.Host.Service
|
||||
ServiceName = Name;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public async Task CheckSignals(Func<string, (int, Task)> startChildAndGetPid, CancellationToken cancellationToken)
|
||||
{
|
||||
using (pipeServer = new AnonymousPipeServerStream(PipeDirection.Out, HandleInheritability.Inheritable))
|
||||
{
|
||||
var (_, lifetimeTask) = startChildAndGetPid($"--Internal:CommandPipe={pipeServer.GetClientHandleAsString()}");
|
||||
pipeServer.DisposeLocalCopyOfClientHandle();
|
||||
await lifetimeTask;
|
||||
}
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override void Dispose(bool disposing)
|
||||
{
|
||||
loggerFactory?.Dispose();
|
||||
cancellationTokenSource?.Dispose();
|
||||
if (disposing)
|
||||
{
|
||||
loggerFactory?.Dispose();
|
||||
cancellationTokenSource?.Dispose();
|
||||
pipeServer?.Dispose();
|
||||
}
|
||||
|
||||
base.Dispose(disposing);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
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);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
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<ServerService>();
|
||||
}
|
||||
|
||||
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
|
||||
/// <returns>A <see cref="Task"/> representing the running operation.</returns>
|
||||
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();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Sends a command to the main server process.
|
||||
/// </summary>
|
||||
/// <param name="command">One of the <see cref="PipeCommands"/>.</param>
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,20 @@
|
||||
using System;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace Tgstation.Server.Host.Watchdog
|
||||
{
|
||||
/// <summary>
|
||||
/// For relaying signals received to the host process.
|
||||
/// </summary>
|
||||
public interface ISignalChecker
|
||||
{
|
||||
/// <summary>
|
||||
/// Relays signals received to the host process.
|
||||
/// </summary>
|
||||
/// <param name="startChild">An <see cref="Func{TResult}"/> to start the main process. It accepts an optional additional command line argument as a paramter and returns it's <see cref="System.Diagnostics.Process.Id"/> and lifetime <see cref="Task"/>.</param>
|
||||
/// <param name="cancellationToken">The <see cref="CancellationToken"/> for the operation.</param>
|
||||
/// <returns>A <see cref="Task"/> representing the running operation.</returns>
|
||||
Task CheckSignals(Func<string, (int, Task)> startChild, CancellationToken cancellationToken);
|
||||
}
|
||||
}
|
||||
@@ -14,7 +14,7 @@ namespace Tgstation.Server.Host.Watchdog
|
||||
/// <param name="runConfigure">If the <see cref="IWatchdog"/> should just run the host configuration wizard and exit.</param>
|
||||
/// <param name="args">The arguments for the <see cref="IWatchdog"/>.</param>
|
||||
/// <param name="cancellationToken">The <see cref="CancellationToken"/> for the operation.</param>
|
||||
/// <returns>A <see cref="Task"/> representing the running operation.</returns>
|
||||
Task RunAsync(bool runConfigure, string[] args, CancellationToken cancellationToken);
|
||||
/// <returns>A <see cref="Task{TResult}"/> resulting in <see langword="true"/> if there were no errors, <see langword="false"/> otherwise.</returns>
|
||||
Task<bool> RunAsync(bool runConfigure, string[] args, CancellationToken cancellationToken);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -10,8 +10,9 @@ namespace Tgstation.Server.Host.Watchdog
|
||||
/// <summary>
|
||||
/// Create a <see cref="IWatchdog"/>.
|
||||
/// </summary>
|
||||
/// <param name="signalChecker">The <see cref="ISignalChecker"/> to use for relaying signals.</param>
|
||||
/// <param name="loggerFactory">The <see cref="ILoggerFactory"/> to use for error reporting.</param>
|
||||
/// <returns>A new <see cref="IWatchdog"/>.</returns>
|
||||
IWatchdog CreateWatchdog(ILoggerFactory loggerFactory);
|
||||
IWatchdog CreateWatchdog(ISignalChecker signalChecker, ILoggerFactory loggerFactory);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -19,6 +19,11 @@ namespace Tgstation.Server.Host.Watchdog
|
||||
/// <remarks>This <see langword="class"/> 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.</remarks>
|
||||
sealed class Watchdog : IWatchdog
|
||||
{
|
||||
/// <summary>
|
||||
/// The <see cref="ISignalChecker"/> for the <see cref="Watchdog"/>.
|
||||
/// </summary>
|
||||
readonly ISignalChecker signalChecker;
|
||||
|
||||
/// <summary>
|
||||
/// The <see cref="ILogger"/> for the <see cref="Watchdog"/>.
|
||||
/// </summary>
|
||||
@@ -27,16 +32,18 @@ namespace Tgstation.Server.Host.Watchdog
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="Watchdog"/> class.
|
||||
/// </summary>
|
||||
/// <param name="signalChecker">The value of <see cref="signalChecker"/>.</param>
|
||||
/// <param name="logger">The value of <see cref="logger"/>.</param>
|
||||
public Watchdog(ILogger<Watchdog> logger)
|
||||
public Watchdog(ISignalChecker signalChecker, ILogger<Watchdog> logger)
|
||||
{
|
||||
this.signalChecker = signalChecker ?? throw new ArgumentNullException(nameof(signalChecker));
|
||||
this.logger = logger ?? throw new ArgumentNullException(nameof(logger));
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
#pragma warning disable CA1502 // TODO: Decomplexify
|
||||
#pragma warning disable CA1506
|
||||
public async Task RunAsync(bool runConfigure, string[] args, CancellationToken cancellationToken)
|
||||
public async Task<bool> 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
|
||||
|
||||
@@ -8,6 +8,10 @@ namespace Tgstation.Server.Host.Watchdog
|
||||
public sealed class WatchdogFactory : IWatchdogFactory
|
||||
{
|
||||
/// <inheritdoc />
|
||||
public IWatchdog CreateWatchdog(ILoggerFactory loggerFactory) => new Watchdog(loggerFactory?.CreateLogger<Watchdog>() ?? throw new ArgumentNullException(nameof(loggerFactory)));
|
||||
public IWatchdog CreateWatchdog(
|
||||
ISignalChecker signalChecker,
|
||||
ILoggerFactory loggerFactory) => new Watchdog(
|
||||
signalChecker ?? throw new ArgumentNullException(nameof(signalChecker)),
|
||||
loggerFactory?.CreateLogger<Watchdog>() ?? throw new ArgumentNullException(nameof(loggerFactory)));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,18 @@
|
||||
namespace Tgstation.Server.Host.Configuration
|
||||
{
|
||||
/// <summary>
|
||||
/// Unstable configuration options used internally by TGS.
|
||||
/// </summary>
|
||||
public sealed class InternalConfiguration
|
||||
{
|
||||
/// <summary>
|
||||
/// The key for the <see cref="Microsoft.Extensions.Configuration.IConfigurationSection"/> the <see cref="InternalConfiguration"/> resides in.
|
||||
/// </summary>
|
||||
public const string Section = "Internal";
|
||||
|
||||
/// <summary>
|
||||
/// The name of the pipe opened by the host watchdog, if any.
|
||||
/// </summary>
|
||||
public string CommandPipe { get; set; }
|
||||
}
|
||||
}
|
||||
@@ -132,6 +132,7 @@ namespace Tgstation.Server.Host.Core
|
||||
services.UseStandardConfig<ControlPanelConfiguration>(Configuration);
|
||||
services.UseStandardConfig<SwarmConfiguration>(Configuration);
|
||||
services.UseStandardConfig<SessionConfiguration>(Configuration);
|
||||
services.UseStandardConfig<InternalConfiguration>(Configuration);
|
||||
|
||||
// enable options which give us config reloading
|
||||
services.AddOptions();
|
||||
@@ -335,10 +336,8 @@ namespace Tgstation.Server.Host.Core
|
||||
services.AddSingleton(x => new Lazy<IProcessExecutor>(() => x.GetRequiredService<IProcessExecutor>(), true));
|
||||
services.AddSingleton<INetworkPromptReaper, PosixNetworkPromptReaper>();
|
||||
|
||||
services.AddSingleton<IHostedService, PosixSignalHandler>();
|
||||
|
||||
services.AddSingleton<SystemDManager>();
|
||||
services.AddSingleton<IHostedService>(x => x.GetRequiredService<SystemDManager>());
|
||||
services.AddHostedService<PosixSignalHandler>();
|
||||
services.AddHostedService<SystemDManager>();
|
||||
}
|
||||
|
||||
// configure file transfer services
|
||||
@@ -368,10 +367,11 @@ namespace Tgstation.Server.Host.Core
|
||||
// configure misc services
|
||||
services.AddSingleton<IProcessExecutor, ProcessExecutor>();
|
||||
services.AddSingleton<ISynchronousIOManager, SynchronousIOManager>();
|
||||
services.AddFileDownloader();
|
||||
services.AddSingleton<IServerPortProvider, ServerPortProivder>();
|
||||
services.AddSingleton<ITopicClientFactory, TopicClientFactory>();
|
||||
services.AddHostedService<CommandPipeReader>();
|
||||
|
||||
services.AddFileDownloader();
|
||||
services.AddGitHub();
|
||||
|
||||
// configure root services
|
||||
|
||||
@@ -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
|
||||
{
|
||||
/// <summary>
|
||||
/// Reads from the command pipe opened by the host watchdog.
|
||||
/// </summary>
|
||||
sealed class CommandPipeReader : BackgroundService
|
||||
{
|
||||
/// <summary>
|
||||
/// The <see cref="IServerControl"/> for the <see cref="CommandPipeReader"/>.
|
||||
/// </summary>
|
||||
readonly IServerControl serverControl;
|
||||
|
||||
/// <summary>
|
||||
/// The <see cref="ILogger"/> for the <see cref="CommandPipeReader"/>.
|
||||
/// </summary>
|
||||
readonly ILogger<CommandPipeReader> logger;
|
||||
|
||||
/// <summary>
|
||||
/// The <see cref="InternalConfiguration"/> for the <see cref="CommandPipeReader"/>.
|
||||
/// </summary>
|
||||
readonly InternalConfiguration internalConfiguration;
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="CommandPipeReader"/> class.
|
||||
/// </summary>
|
||||
/// <param name="serverControl">The value of <see cref="serverControl"/>.</param>
|
||||
/// <param name="internalConfigurationOptions">The <see cref="IOptions{TOptions}"/> containing the value of <see cref="internalConfiguration"/>.</param>
|
||||
/// <param name="logger">The value of <see cref="logger"/>.</param>
|
||||
public CommandPipeReader(
|
||||
IServerControl serverControl,
|
||||
IOptions<InternalConfiguration> internalConfigurationOptions,
|
||||
ILogger<CommandPipeReader> 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));
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
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...");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
|
||||
/// <inheritdoc />
|
||||
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;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Throws an <see cref="InvalidOperationException"/> if the <see cref="IServerControl"/> 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();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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<IWatchdog>();
|
||||
var args = Array.Empty<string>();
|
||||
mockServer.Setup(x => x.RunAsync(false, args, It.IsAny<CancellationToken>())).Returns(Task.CompletedTask).Verifiable();
|
||||
mockServer.Setup(x => x.RunAsync(false, args, It.IsAny<CancellationToken>())).Returns(Task.FromResult(true)).Verifiable();
|
||||
var mockServerFactory = new Mock<IWatchdogFactory>();
|
||||
mockServerFactory.Setup(x => x.CreateWatchdog(It.IsAny<ILoggerFactory>())).Returns(mockServer.Object).Verifiable();
|
||||
mockServerFactory.Setup(x => x.CreateWatchdog(It.IsNotNull<ISignalChecker>(), It.IsNotNull<ILoggerFactory>())).Returns(mockServer.Object).Verifiable();
|
||||
Program.WatchdogFactory = mockServerFactory.Object;
|
||||
await Program.Main(args);
|
||||
mockServer.VerifyAll();
|
||||
|
||||
@@ -34,9 +34,9 @@ namespace Tgstation.Server.Host.Service.Tests
|
||||
var mockWatchdog = new Mock<IWatchdog>();
|
||||
var args = Array.Empty<string>();
|
||||
CancellationToken cancellationToken;
|
||||
mockWatchdog.Setup(x => x.RunAsync(false, args, It.IsAny<CancellationToken>())).Callback((bool x, string[] _, CancellationToken token) => cancellationToken = token).Returns(Task.CompletedTask).Verifiable();
|
||||
mockWatchdog.Setup(x => x.RunAsync(false, args, It.IsAny<CancellationToken>())).Callback((bool x, string[] _, CancellationToken token) => cancellationToken = token).Returns(Task.FromResult(true)).Verifiable();
|
||||
var mockWatchdogFactory = new Mock<IWatchdogFactory>();
|
||||
mockWatchdogFactory.Setup(x => x.CreateWatchdog(It.IsNotNull<ILoggerFactory>())).Returns(mockWatchdog.Object).Verifiable();
|
||||
mockWatchdogFactory.Setup(x => x.CreateWatchdog(It.IsNotNull<ISignalChecker>(), It.IsNotNull<ILoggerFactory>())).Returns(mockWatchdog.Object).Verifiable();
|
||||
|
||||
using (var service = new ServerService(mockWatchdogFactory.Object, default))
|
||||
{
|
||||
|
||||
@@ -13,9 +13,11 @@ namespace Tgstation.Server.Host.Watchdog.Tests
|
||||
[TestMethod]
|
||||
public void TestConstruction()
|
||||
{
|
||||
Assert.ThrowsException<ArgumentNullException>(() => new Watchdog(null));
|
||||
var mockLogger = new LoggerFactory().CreateLogger<Watchdog>();
|
||||
var wd = new Watchdog(mockLogger);
|
||||
Assert.ThrowsException<ArgumentNullException>(() => new Watchdog(null, null));
|
||||
var mockSignalChecker = Mock.Of<ISignalChecker>();
|
||||
Assert.ThrowsException<ArgumentNullException>(() => new Watchdog(mockSignalChecker, null));
|
||||
var mockLogger = Mock.Of<ILogger<Watchdog>>();
|
||||
var wd = new Watchdog(mockSignalChecker, mockLogger);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Microsoft.VisualStudio.TestTools.UnitTesting;
|
||||
|
||||
using Moq;
|
||||
|
||||
namespace Tgstation.Server.Host.Watchdog.Tests
|
||||
{
|
||||
/// <summary>
|
||||
@@ -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<ISignalChecker>(),
|
||||
Mock.Of<ILoggerFactory>()));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user