Make the host watchdog use nullable references

This commit is contained in:
Jordan Dominion
2023-11-23 16:49:59 -05:00
parent a0ee1b35a4
commit c5fff694d7
14 changed files with 297 additions and 203 deletions
+1 -1
View File
@@ -11,7 +11,7 @@
<TgsClientVersion>15.0.0</TgsClientVersion>
<TgsDmapiVersion>7.0.0</TgsDmapiVersion>
<TgsInteropVersion>5.7.0</TgsInteropVersion>
<TgsHostWatchdogVersion>1.4.0</TgsHostWatchdogVersion>
<TgsHostWatchdogVersion>1.4.1</TgsHostWatchdogVersion>
<TgsContainerScriptVersion>1.2.1</TgsContainerScriptVersion>
<TgsMigratorVersion>2.0.0</TgsMigratorVersion>
<TgsNugetNetFramework>netstandard2.0</TgsNugetNetFramework>
@@ -31,7 +31,7 @@ namespace Tgstation.Server.Host.Console
}
/// <inheritdoc />
public async ValueTask CheckSignals(Func<string, (int, Task)> startChild, CancellationToken cancellationToken)
public async ValueTask CheckSignals(Func<string?, (int, Task)> startChild, CancellationToken cancellationToken)
{
var (childPid, _) = startChild?.Invoke(null) ?? throw new ArgumentNullException(nameof(startChild));
var signalTcs = new TaskCompletionSource<Signum>();
+2 -2
View File
@@ -38,7 +38,7 @@ namespace Tgstation.Server.Host.Console
/// <returns>A <see cref="Task"/> representing the running operation.</returns>
internal static async Task<int> Main(string[] args)
{
System.Console.Title = $"{Constants.CanonicalPackageName} Host Watchdog v{Assembly.GetExecutingAssembly().GetName().Version.Semver()}";
System.Console.Title = $"{Constants.CanonicalPackageName} Host Watchdog v{Assembly.GetExecutingAssembly().GetName().Version?.Semver()}";
var arguments = new List<string>(args);
var trace = arguments.Remove("--trace-host-watchdog");
@@ -61,7 +61,7 @@ namespace Tgstation.Server.Host.Console
}
using var cts = new CancellationTokenSource();
void AppDomainHandler(object a, EventArgs b) => cts.Cancel();
void AppDomainHandler(object? a, EventArgs b) => cts.Cancel();
AppDomain.CurrentDomain.ProcessExit += AppDomainHandler;
try
{
@@ -5,6 +5,7 @@
<OutputType>Exe</OutputType>
<TargetFramework>$(TgsFrameworkVersion)</TargetFramework>
<Version>$(TgsCoreVersion)</Version>
<Nullable>enable</Nullable>
<UseAppHost>false</UseAppHost>
<ApplicationManifest>../../build/uac_elevation_manifest.xml</ApplicationManifest>
</PropertyGroup>
+7 -6
View File
@@ -77,7 +77,7 @@ namespace Tgstation.Server.Host.Service
/// The --passthroughargs or -p option.
/// </summary>
[Option(ShortName = "p", Description = "Arguments passed to main host process")]
public string PassthroughArgs { get; set; }
public string? PassthroughArgs { get; set; }
/// <summary>
/// Entrypoint for the application.
@@ -157,7 +157,7 @@ namespace Tgstation.Server.Host.Service
/// Runs sc.exe to either uninstall a given <paramref name="serviceToUninstall"/> or install the running <see cref="ServerService"/>.
/// </summary>
/// <param name="serviceToUninstall">The name of a service to uninstall.</param>
void InvokeSC(string serviceToUninstall)
void InvokeSC(string? serviceToUninstall)
{
using var installer = new ServiceInstaller();
if (serviceToUninstall != null)
@@ -172,6 +172,9 @@ namespace Tgstation.Server.Host.Service
Assembly.GetExecutingAssembly().Location);
var assemblyDirectory = Path.GetDirectoryName(fullPathToAssembly);
if (assemblyDirectory == null)
throw new InvalidOperationException($"Failed to resolve directory name of {assemblyDirectory}");
var assemblyNameWithoutExtension = Path.GetFileNameWithoutExtension(fullPathToAssembly);
var exePath = Path.Combine(assemblyDirectory, $"{assemblyNameWithoutExtension}.exe");
@@ -260,10 +263,8 @@ namespace Tgstation.Server.Host.Service
var stop = !Detach;
if (!stop)
{
serviceController.ExecuteCommand(
PipeCommands.GetServiceCommandId(
PipeCommands.CommandDetachingShutdown)
.Value);
var serviceControllerCommand = PipeCommands.GetServiceCommandId(PipeCommands.CommandDetachingShutdown);
serviceController.ExecuteCommand(serviceControllerCommand!.Value);
serviceController.WaitForStatus(ServiceControllerStatus.Stopped, TimeSpan.FromSeconds(30));
if (serviceController.Status != ServiceControllerStatus.Stopped)
stop = true;
@@ -1,20 +1,14 @@
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.IO;
using System.IO.Pipes;
using System.Linq;
using System.Runtime.Versioning;
using System.ServiceProcess;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Logging.EventLog;
using Tgstation.Server.Common;
using Tgstation.Server.Host.Common;
using Tgstation.Server.Host.Watchdog;
namespace Tgstation.Server.Host.Service
@@ -23,7 +17,7 @@ namespace Tgstation.Server.Host.Service
/// Represents a <see cref="IWatchdog"/> as a <see cref="ServiceBase"/>.
/// </summary>
[SupportedOSPlatform("windows")]
sealed class ServerService : ServiceBase, ISignalChecker
sealed class ServerService : ServiceBase
{
/// <summary>
/// The canonical windows service name.
@@ -35,45 +29,22 @@ namespace Tgstation.Server.Host.Service
/// </summary>
readonly IWatchdogFactory watchdogFactory;
/// <summary>
/// The <see cref="Lazy{T}"/> <see cref="ILoggerFactory"/> used by the <see cref="ServerService"/>.
/// </summary>
readonly Lazy<ILoggerFactory> loggerFactory;
/// <summary>
/// The <see cref="Array"/> of command line arguments the service was invoked with.
/// </summary>
readonly string[] commandLineArguments;
/// <summary>
/// The minimum <see cref="LogLevel"/> for the <see cref="EventLog"/>.
/// The active <see cref="ServiceLifetime"/>.
/// </summary>
readonly LogLevel minimumLogLevel;
/// <summary>
/// The <see cref="ILoggerFactory"/> used by the <see cref="ServerService"/>.
/// </summary>
ILoggerFactory loggerFactory;
/// <summary>
/// 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;
/// <summary>
/// The <see cref="cancellationTokenSource"/> for the <see cref="ServerService"/>.
/// </summary>
CancellationTokenSource cancellationTokenSource;
/// <summary>
/// The <see cref="AnonymousPipeServerStream"/> for sending <see cref="PipeCommands"/> to the server process.
/// </summary>
AnonymousPipeServerStream commandPipeServer;
/// <summary>
/// The <see cref="AnonymousPipeServerStream"/> for receiving the <see cref="PipeCommands.CommandStartupComplete"/>.
/// </summary>
AnonymousPipeServerStream readyPipeServer;
#pragma warning disable CA2213 // Disposable fields should be disposed
volatile ServiceLifetime? serviceLifetime;
#pragma warning restore CA2213 // Disposable fields should be disposed
/// <summary>
/// Initializes a new instance of the <see cref="ServerService"/> class.
@@ -85,21 +56,15 @@ namespace Tgstation.Server.Host.Service
{
this.watchdogFactory = watchdogFactory ?? throw new ArgumentNullException(nameof(watchdogFactory));
this.commandLineArguments = commandLineArguments ?? throw new ArgumentNullException(nameof(commandLineArguments));
this.minimumLogLevel = minimumLogLevel;
ServiceName = Name;
}
/// <inheritdoc />
public async ValueTask CheckSignals(Func<string, (int, Task)> startChildAndGetPid, CancellationToken cancellationToken)
{
await using (commandPipeServer = new AnonymousPipeServerStream(PipeDirection.Out, HandleInheritability.Inheritable))
await using (readyPipeServer = new AnonymousPipeServerStream(PipeDirection.In, HandleInheritability.Inheritable))
ServiceName = Name;
loggerFactory = new Lazy<ILoggerFactory>(() => LoggerFactory.Create(builder => builder.AddEventLog(new EventLogSettings
{
var (_, lifetimeTask) = startChildAndGetPid($"--Internal:CommandPipe={commandPipeServer.GetClientHandleAsString()} --Internal:ReadyPipe={readyPipeServer.GetClientHandleAsString()}");
commandPipeServer.DisposeLocalCopyOfClientHandle();
readyPipeServer.DisposeLocalCopyOfClientHandle();
await lifetimeTask;
}
LogName = EventLog.Log,
MachineName = EventLog.MachineName,
SourceName = EventLog.Source,
Filter = (message, logLevel) => logLevel >= minimumLogLevel,
})));
}
/// <summary>
@@ -112,53 +77,21 @@ namespace Tgstation.Server.Host.Service
{
if (disposing)
{
loggerFactory?.Dispose();
cancellationTokenSource?.Dispose();
commandPipeServer?.Dispose();
readyPipeServer?.Dispose();
OnStop();
if (loggerFactory.IsValueCreated)
loggerFactory.Value.Dispose();
}
base.Dispose(disposing);
}
/// <inheritdoc />
protected override void OnCustomCommand(int command)
{
var commandsToCheck = PipeCommands.AllCommands;
foreach (var stringCommand in commandsToCheck)
{
var commandId = PipeCommands.GetServiceCommandId(stringCommand);
if (command == commandId)
{
SendCommandToHostThroughPipe(stringCommand);
return;
}
}
logger.LogWarning("Received unknown service command: {command}", command);
}
protected override void OnCustomCommand(int command) => serviceLifetime!.HandleCustomCommand(command);
/// <inheritdoc />
protected override void OnStart(string[] args)
{
if (loggerFactory == null)
{
loggerFactory = LoggerFactory.Create(builder => builder.AddEventLog(new EventLogSettings
{
LogName = EventLog.Log,
MachineName = EventLog.MachineName,
SourceName = EventLog.Source,
Filter = (message, logLevel) => logLevel >= minimumLogLevel,
}));
logger = loggerFactory.CreateLogger<ServerService>();
}
var watchdog = watchdogFactory.CreateWatchdog(this, loggerFactory);
cancellationTokenSource?.Dispose();
cancellationTokenSource = new CancellationTokenSource();
var newArgs = new List<string>(commandLineArguments.Length + args.Length + 1)
{
"--General:SetupWizardMode=Never",
@@ -167,94 +100,18 @@ namespace Tgstation.Server.Host.Service
newArgs.AddRange(commandLineArguments);
newArgs.AddRange(args);
watchdogTask = RunWatchdog(watchdog, newArgs.ToArray(), cancellationTokenSource.Token);
if (!watchdogTask.IsCompleted && watchdog.InitialHostVersion >= new Version(5, 14, 0))
{
logger.LogInformation("Waiting for host to finish starting...");
using var streamReader = new StreamReader(
readyPipeServer,
Encoding.UTF8,
leaveOpen: true);
var line = streamReader.ReadLine(); // Intentionally blocking service startup
logger.LogDebug("Pipe read: {line}", line);
}
// Maybe we'll use this pipe more in the future, but for now leaving it open is just a resource waste
readyPipeServer.Dispose();
serviceLifetime = new ServiceLifetime(
Stop,
signalChecker => watchdogFactory.CreateWatchdog(signalChecker, loggerFactory.Value),
loggerFactory.Value.CreateLogger<ServiceLifetime>(),
args);
}
/// <inheritdoc />
protected override void OnStop()
{
cancellationTokenSource.Cancel();
watchdogTask.GetAwaiter().GetResult();
}
/// <summary>
/// Executes the <paramref name="watchdog"/>, stopping the service if it exits.
/// </summary>
/// <param name="watchdog">The <see cref="IWatchdog"/> to run.</param>
/// <param name="args">The arguments for the <paramref name="watchdog"/>.</param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> for the operation.</param>
/// <returns>A <see cref="Task"/> representing the running operation.</returns>
async Task RunWatchdog(IWatchdog watchdog, string[] args, CancellationToken cancellationToken)
{
await watchdog.RunAsync(false, args, cancellationToken);
async void StopServiceAsync()
{
try
{
await Task.Run(Stop, cancellationToken); // DCT intentional
}
catch (OperationCanceledException ex)
{
logger.LogTrace(ex, "Stopping service cancelled!");
}
catch (Exception ex)
{
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 SendCommandToHostThroughPipe(string command)
{
var localPipeServer = commandPipeServer;
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
{
var encoding = Encoding.UTF8;
using var streamWriter = new StreamWriter(
localPipeServer,
encoding,
PipeCommands
.AllCommands
.Select(
command => encoding.GetByteCount(
command + Environment.NewLine))
.Max(),
true);
streamWriter.WriteLine(command);
}
catch (Exception ex)
{
logger.LogError(ex, "Error attempting to send command \"{command}\"", command);
}
var oldLifetime = Interlocked.Exchange(ref serviceLifetime, null);
oldLifetime?.DisposeAsync().GetAwaiter().GetResult();
}
}
}
@@ -0,0 +1,200 @@
using System;
using System.IO;
using System.IO.Pipes;
using System.Linq;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.Extensions.Logging;
using Tgstation.Server.Host.Common;
using Tgstation.Server.Host.Watchdog;
namespace Tgstation.Server.Host.Service
{
/// <summary>
/// Represents the lifetime of the service.
/// </summary>
sealed class ServiceLifetime : ISignalChecker, IAsyncDisposable
{
/// <summary>
/// The <see cref="ILogger"/> for the <see cref="ServerService"/>.
/// </summary>
readonly ILogger<ServiceLifetime> logger;
/// <summary>
/// The <see cref="Task"/> that represents the running <see cref="ServerService"/>.
/// </summary>
readonly Task watchdogTask;
/// <summary>
/// The <see cref="cancellationTokenSource"/> for the <see cref="ServerService"/>.
/// </summary>
readonly CancellationTokenSource cancellationTokenSource;
/// <summary>
/// The <see cref="AnonymousPipeServerStream"/> for sending <see cref="PipeCommands"/> to the server process.
/// </summary>
AnonymousPipeServerStream? commandPipeServer;
/// <summary>
/// The <see cref="AnonymousPipeServerStream"/> for receiving the <see cref="PipeCommands.CommandStartupComplete"/>.
/// </summary>
AnonymousPipeServerStream? readyPipeServer;
/// <summary>
/// Initializes a new instance of the <see cref="ServiceLifetime"/> class.
/// </summary>
/// <param name="stopService">An <see cref="Action"/> to manually stop the service.</param>
/// <param name="watchdogFactory">A <see cref="Func{T, TResult}"/> taking a <see cref="ISignalChecker"/> and returning the <see cref="IWatchdog"/> to run.</param>
/// <param name="logger">The value of <see cref="logger"/>.</param>
/// <param name="args">The arguments for the <see cref="IWatchdog"/>.</param>
public ServiceLifetime(Action stopService, Func<ISignalChecker, IWatchdog> watchdogFactory, ILogger<ServiceLifetime> logger, string[] args)
{
ArgumentNullException.ThrowIfNull(stopService);
ArgumentNullException.ThrowIfNull(watchdogFactory);
this.logger = logger ?? throw new ArgumentNullException(nameof(logger));
ArgumentNullException.ThrowIfNull(args);
cancellationTokenSource = new CancellationTokenSource();
watchdogTask = RunWatchdog(
stopService,
watchdogFactory(this),
args,
cancellationTokenSource.Token);
}
/// <inheritdoc />
public async ValueTask DisposeAsync()
{
cancellationTokenSource.Cancel();
await watchdogTask;
cancellationTokenSource.Dispose();
if (commandPipeServer != null)
await commandPipeServer.DisposeAsync();
if (readyPipeServer != null)
await readyPipeServer.DisposeAsync();
}
/// <inheritdoc />
public async ValueTask CheckSignals(Func<string, (int, Task)> startChildAndGetPid, CancellationToken cancellationToken)
{
try
{
await using (commandPipeServer = new AnonymousPipeServerStream(PipeDirection.Out, HandleInheritability.Inheritable))
await using (readyPipeServer = new AnonymousPipeServerStream(PipeDirection.In, HandleInheritability.Inheritable))
{
var (_, lifetimeTask) = startChildAndGetPid($"--Internal:CommandPipe={commandPipeServer.GetClientHandleAsString()} --Internal:ReadyPipe={readyPipeServer.GetClientHandleAsString()}");
commandPipeServer.DisposeLocalCopyOfClientHandle();
readyPipeServer.DisposeLocalCopyOfClientHandle();
await lifetimeTask;
}
}
finally
{
readyPipeServer = null;
commandPipeServer = null;
}
}
/// <summary>
/// Handle a custom service <paramref name="command"/>.
/// </summary>
/// <param name="command">The <see cref="int"/> command sent to the service.</param>
public void HandleCustomCommand(int command)
{
var commandsToCheck = PipeCommands.AllCommands;
foreach (var stringCommand in commandsToCheck)
{
var commandId = PipeCommands.GetServiceCommandId(stringCommand);
if (command == commandId)
{
SendCommandToHostThroughPipe(stringCommand);
return;
}
}
logger.LogWarning("Received unknown service command: {command}", command);
}
/// <summary>
/// Executes the <paramref name="watchdog"/>, stopping the service if it exits.
/// </summary>
/// <param name="stopService">An <see cref="Action"/> to manually stop the service.</param>
/// <param name="watchdog">The <see cref="IWatchdog"/> to run.</param>
/// <param name="args">The arguments for the <paramref name="watchdog"/>.</param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> for the operation.</param>
/// <returns>A <see cref="Task"/> representing the running operation.</returns>
async Task RunWatchdog(Action stopService, IWatchdog watchdog, string[] args, CancellationToken cancellationToken)
{
var localWatchdogTask = watchdog.RunAsync(false, args, cancellationToken);
if (!localWatchdogTask.IsCompleted && (await watchdog.InitialHostVersion) >= new Version(5, 14, 0))
if (readyPipeServer != null)
{
logger.LogInformation("Waiting for host to finish starting...");
using var streamReader = new StreamReader(
readyPipeServer,
Encoding.UTF8,
leaveOpen: true);
var line = streamReader.ReadLine(); // Intentionally blocking service startup
logger.LogDebug("Pipe read: {line}", line);
// Maybe we'll use this pipe more in the future, but for now leaving it open is just a resource waste
readyPipeServer.Dispose();
}
else
logger.LogError("Watchdog started and ready pipe was not initialized!");
await localWatchdogTask;
try
{
stopService();
}
catch (Exception ex)
{
logger.LogError(ex, "Error stopping service!");
}
}
/// <summary>
/// Sends a command to the main server process.
/// </summary>
/// <param name="command">One of the <see cref="PipeCommands"/>.</param>
void SendCommandToHostThroughPipe(string command)
{
var localPipeServer = commandPipeServer;
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
{
var encoding = Encoding.UTF8;
using var streamWriter = new StreamWriter(
localPipeServer,
encoding,
PipeCommands
.AllCommands
.Select(
command => encoding.GetByteCount(
command + Environment.NewLine))
.Max(),
true);
streamWriter.WriteLine(command);
}
catch (Exception ex)
{
logger.LogError(ex, "Error attempting to send command \"{command}\"", command);
}
}
}
}
@@ -4,6 +4,7 @@
<PropertyGroup>
<OutputType>WinExe</OutputType>
<RuntimeIdentifiers>win-x86;win-x64</RuntimeIdentifiers>
<Nullable>enable</Nullable>
<!-- DO NOT ADD THE -windows SUFFIX, It makes the service require the desktop runtime instead of the ASP NET Core Hosting Bundle -->
<TargetFramework>$(TgsFrameworkVersion)</TargetFramework>
<Version>$(TgsCoreVersion)</Version>
@@ -12,9 +12,9 @@ namespace Tgstation.Server.Host.Watchdog
/// <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="startChildAndGetPid">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"/>. Must be called.</param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> for the operation.</param>
/// <returns>A <see cref="ValueTask"/> representing the running operation.</returns>
ValueTask CheckSignals(Func<string, (int, Task)> startChild, CancellationToken cancellationToken);
ValueTask CheckSignals(Func<string?, (int, Task)> startChildAndGetPid, CancellationToken cancellationToken);
}
}
@@ -10,9 +10,9 @@ namespace Tgstation.Server.Host.Watchdog
public interface IWatchdog
{
/// <summary>
/// Gets the current version of the host process. Set once <see cref="RunAsync(bool, string[], CancellationToken)"/> begins and doesn't immediately return <see langword="false"/>.
/// Gets a <see cref="Task{TResult}"/> resulting in the current version of the host process. Guaranteed to complete once <see cref="RunAsync(bool, string[], CancellationToken)"/> begins and doesn't immediately return <see langword="false"/>.
/// </summary>
Version InitialHostVersion { get; }
Task<Version> InitialHostVersion { get; }
/// <summary>
/// Run the <see cref="IWatchdog"/>.
@@ -10,10 +10,10 @@ namespace Tgstation.Server.Host.Watchdog
public sealed class NoopSignalChecker : ISignalChecker
{
/// <inheritdoc />
public ValueTask CheckSignals(Func<string, (int, Task)> startChild, CancellationToken cancellationToken)
public ValueTask CheckSignals(Func<string?, (int, Task)> startChildAndGetPid, CancellationToken cancellationToken)
{
ArgumentNullException.ThrowIfNull(startChild);
startChild(null);
ArgumentNullException.ThrowIfNull(startChildAndGetPid);
startChildAndGetPid(null);
return ValueTask.CompletedTask;
}
}
@@ -3,6 +3,7 @@
<PropertyGroup>
<TargetFramework>$(TgsFrameworkVersion)</TargetFramework>
<Nullable>enable</Nullable>
<AddSyntheticProjectReferencesForSolutionDependencies>false</AddSyntheticProjectReferencesForSolutionDependencies>
<Version>$(TgsHostWatchdogVersion)</Version>
</PropertyGroup>
+33 -11
View File
@@ -20,7 +20,7 @@ namespace Tgstation.Server.Host.Watchdog
sealed class Watchdog : IWatchdog
{
/// <inheritdoc />
public Version InitialHostVersion { get; private set; }
public Task<Version> InitialHostVersion => initialHostVersionTcs.Task;
/// <summary>
/// The <see cref="ISignalChecker"/> for the <see cref="Watchdog"/>.
@@ -32,6 +32,11 @@ namespace Tgstation.Server.Host.Watchdog
/// </summary>
readonly ILogger<Watchdog> logger;
/// <summary>
/// Backing <see cref="TaskCompletionSource{TResult}"/> for <see cref="InitialHostVersion"/>.
/// </summary>
readonly TaskCompletionSource<Version> initialHostVersionTcs;
/// <summary>
/// Initializes a new instance of the <see cref="Watchdog"/> class.
/// </summary>
@@ -41,6 +46,8 @@ namespace Tgstation.Server.Host.Watchdog
{
this.signalChecker = signalChecker ?? throw new ArgumentNullException(nameof(signalChecker));
this.logger = logger ?? throw new ArgumentNullException(nameof(logger));
initialHostVersionTcs = new TaskCompletionSource<Version>();
}
/// <inheritdoc />
@@ -54,7 +61,7 @@ namespace Tgstation.Server.Host.Watchdog
currentProcessId = currentProc.Id;
logger.LogDebug("PID: {pid}", currentProcessId);
string updateDirectory = null;
string? updateDirectory = null;
try
{
var isWindows = RuntimeInformation.IsOSPlatform(OSPlatform.Windows);
@@ -76,6 +83,11 @@ namespace Tgstation.Server.Host.Watchdog
var executingAssembly = Assembly.GetExecutingAssembly();
var rootLocation = Path.GetDirectoryName(executingAssembly.Location);
if (rootLocation == null)
{
logger.LogCritical("Failed to get the directory name of the executing assembly: {location}", executingAssembly.Location);
return false;
}
var assemblyStoragePath = Path.Combine(rootLocation, "lib"); // always always next to watchdog
@@ -119,9 +131,18 @@ namespace Tgstation.Server.Host.Watchdog
return false;
}
InitialHostVersion = Version.Parse(FileVersionInfo.GetVersionInfo(assemblyPath).FileVersion);
var fileVersion = FileVersionInfo.GetVersionInfo(assemblyPath).FileVersion;
if (fileVersion == null)
{
logger.LogCritical("Failed to parse version info from {assemblyPath}!", assemblyPath);
return false;
}
var watchdogVersion = executingAssembly.GetName().Version.Semver().ToString();
initialHostVersionTcs.SetResult(
Version.Parse(
fileVersion));
var watchdogVersion = executingAssembly.GetName().Version?.Semver().ToString();
while (!cancellationToken.IsCancellationRequested)
using (logger.BeginScope("Host invocation"))
@@ -158,8 +179,8 @@ namespace Tgstation.Server.Host.Watchdog
var killedHostProcess = false;
try
{
Task processTask = null;
(int, Task) StartProcess(string additionalArg)
Task? processTask = null;
(int, Task) StartProcess(string? additionalArg)
{
if (additionalArg != null)
process.StartInfo.Arguments += $" {additionalArg}";
@@ -199,7 +220,7 @@ namespace Tgstation.Server.Host.Watchdog
var checkerTask = signalChecker.CheckSignals(StartProcess, cts.Token);
try
{
await processTask;
await processTask!;
}
finally
{
@@ -338,10 +359,11 @@ namespace Tgstation.Server.Host.Watchdog
catch (OperationCanceledException ex)
{
logger.LogDebug(ex, "Exiting due to cancellation...");
if (!Directory.Exists(updateDirectory))
File.Delete(updateDirectory);
else
Directory.Delete(updateDirectory, true);
if (updateDirectory != null)
if (!Directory.Exists(updateDirectory))
File.Delete(updateDirectory);
else
Directory.Delete(updateDirectory, true);
}
catch (Exception ex)
{
@@ -39,15 +39,25 @@ namespace Tgstation.Server.Host.Service.Tests
var childStarted = false;
ISignalChecker signalChecker = null;
mockWatchdog.Setup(x => x.RunAsync(false, It.IsNotNull<string[]>(), It.IsAny<CancellationToken>())).Callback((bool x, string[] _, CancellationToken token) =>
var hostVersionTcs = new TaskCompletionSource<Version>();
var hostLifetimeTcs = new TaskCompletionSource<bool>();
mockWatchdog.Setup(x => x.RunAsync(false, It.IsNotNull<string[]>(), It.IsAny<CancellationToken>())).Returns(async (bool x, string[] _, CancellationToken token) =>
{
hostVersionTcs.SetResult(typeof(ServerService).Assembly.GetName().Version);
cancellationToken = token;
cancellationToken.Register(() => hostLifetimeTcs.SetResult(true));
signalCheckerTask = signalChecker.CheckSignals(additionalArgs =>
{
childStarted = true;
return (123, Task.CompletedTask);
return (123, hostLifetimeTcs.Task);
}, cancellationToken).AsTask();
}).ReturnsAsync(true).Verifiable();
await signalCheckerTask;
return true;
}).Verifiable();
mockWatchdog.SetupGet(x => x.InitialHostVersion).Returns(hostVersionTcs.Task);
var mockWatchdogFactory = new Mock<IWatchdogFactory>();
mockWatchdogFactory.Setup(x => x.CreateWatchdog(It.IsNotNull<ISignalChecker>(), It.IsNotNull<ILoggerFactory>()))
@@ -68,6 +78,7 @@ namespace Tgstation.Server.Host.Service.Tests
mockWatchdogFactory.VerifyAll();
Assert.IsTrue(signalCheckerTask.IsCompleted);
Assert.IsTrue(cancellationToken.IsCancellationRequested);
}
}
}