From c5fff694d79d997d7caf517ed4c7c6efb0e5a664 Mon Sep 17 00:00:00 2001 From: Jordan Dominion Date: Tue, 14 Nov 2023 23:11:33 -0500 Subject: [PATCH] Make the host watchdog use nullable references --- build/Version.props | 2 +- .../PosixSignalChecker.cs | 2 +- src/Tgstation.Server.Host.Console/Program.cs | 4 +- .../Tgstation.Server.Host.Console.csproj | 1 + src/Tgstation.Server.Host.Service/Program.cs | 13 +- .../ServerService.cs | 201 +++--------------- .../ServiceLifetime.cs | 200 +++++++++++++++++ .../Tgstation.Server.Host.Service.csproj | 1 + .../ISignalChecker.cs | 4 +- .../IWatchdog.cs | 4 +- .../NoopSignalChecker.cs | 6 +- .../Tgstation.Server.Host.Watchdog.csproj | 1 + .../Watchdog.cs | 44 +++- .../TestServerService.cs | 17 +- 14 files changed, 297 insertions(+), 203 deletions(-) create mode 100644 src/Tgstation.Server.Host.Service/ServiceLifetime.cs diff --git a/build/Version.props b/build/Version.props index f75f23d239..c27dc59c94 100644 --- a/build/Version.props +++ b/build/Version.props @@ -11,7 +11,7 @@ 15.0.0 7.0.0 5.7.0 - 1.4.0 + 1.4.1 1.2.1 2.0.0 netstandard2.0 diff --git a/src/Tgstation.Server.Host.Console/PosixSignalChecker.cs b/src/Tgstation.Server.Host.Console/PosixSignalChecker.cs index e037b5936f..b18ece3c03 100644 --- a/src/Tgstation.Server.Host.Console/PosixSignalChecker.cs +++ b/src/Tgstation.Server.Host.Console/PosixSignalChecker.cs @@ -31,7 +31,7 @@ namespace Tgstation.Server.Host.Console } /// - public async ValueTask CheckSignals(Func startChild, CancellationToken cancellationToken) + public async ValueTask CheckSignals(Func startChild, CancellationToken cancellationToken) { var (childPid, _) = startChild?.Invoke(null) ?? throw new ArgumentNullException(nameof(startChild)); var signalTcs = new TaskCompletionSource(); diff --git a/src/Tgstation.Server.Host.Console/Program.cs b/src/Tgstation.Server.Host.Console/Program.cs index e142de81be..c8383865af 100644 --- a/src/Tgstation.Server.Host.Console/Program.cs +++ b/src/Tgstation.Server.Host.Console/Program.cs @@ -38,7 +38,7 @@ namespace Tgstation.Server.Host.Console /// A representing the running operation. internal static async Task 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(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 { diff --git a/src/Tgstation.Server.Host.Console/Tgstation.Server.Host.Console.csproj b/src/Tgstation.Server.Host.Console/Tgstation.Server.Host.Console.csproj index 92902912bb..3cbf1c1a16 100644 --- a/src/Tgstation.Server.Host.Console/Tgstation.Server.Host.Console.csproj +++ b/src/Tgstation.Server.Host.Console/Tgstation.Server.Host.Console.csproj @@ -5,6 +5,7 @@ Exe $(TgsFrameworkVersion) $(TgsCoreVersion) + enable false ../../build/uac_elevation_manifest.xml diff --git a/src/Tgstation.Server.Host.Service/Program.cs b/src/Tgstation.Server.Host.Service/Program.cs index 9e6deaeb63..c2f72b0901 100644 --- a/src/Tgstation.Server.Host.Service/Program.cs +++ b/src/Tgstation.Server.Host.Service/Program.cs @@ -77,7 +77,7 @@ namespace Tgstation.Server.Host.Service /// The --passthroughargs or -p option. /// [Option(ShortName = "p", Description = "Arguments passed to main host process")] - public string PassthroughArgs { get; set; } + public string? PassthroughArgs { get; set; } /// /// Entrypoint for the application. @@ -157,7 +157,7 @@ namespace Tgstation.Server.Host.Service /// Runs sc.exe to either uninstall a given or install the running . /// /// The name of a service to uninstall. - 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; diff --git a/src/Tgstation.Server.Host.Service/ServerService.cs b/src/Tgstation.Server.Host.Service/ServerService.cs index 8f5554304f..56491187fb 100644 --- a/src/Tgstation.Server.Host.Service/ServerService.cs +++ b/src/Tgstation.Server.Host.Service/ServerService.cs @@ -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 as a . /// [SupportedOSPlatform("windows")] - sealed class ServerService : ServiceBase, ISignalChecker + sealed class ServerService : ServiceBase { /// /// The canonical windows service name. @@ -35,45 +29,22 @@ namespace Tgstation.Server.Host.Service /// readonly IWatchdogFactory watchdogFactory; + /// + /// The used by the . + /// + readonly Lazy loggerFactory; + /// /// The of command line arguments the service was invoked with. /// readonly string[] commandLineArguments; /// - /// The minimum for the . + /// The active . /// - readonly LogLevel minimumLogLevel; - - /// - /// The used by the . - /// - ILoggerFactory loggerFactory; - - /// - /// The for the . - /// - ILogger logger; - - /// - /// The that represents the running . - /// - Task watchdogTask; - - /// - /// The for the . - /// - CancellationTokenSource cancellationTokenSource; - - /// - /// The for sending to the server process. - /// - AnonymousPipeServerStream commandPipeServer; - - /// - /// The for receiving the . - /// - AnonymousPipeServerStream readyPipeServer; +#pragma warning disable CA2213 // Disposable fields should be disposed + volatile ServiceLifetime? serviceLifetime; +#pragma warning restore CA2213 // Disposable fields should be disposed /// /// Initializes a new instance of the 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; - } - /// - public async ValueTask CheckSignals(Func 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(() => 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, + }))); } /// @@ -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); } /// - 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); /// 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(); - } - - var watchdog = watchdogFactory.CreateWatchdog(this, loggerFactory); - - cancellationTokenSource?.Dispose(); - cancellationTokenSource = new CancellationTokenSource(); - var newArgs = new List(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(), + args); } /// protected override void OnStop() { - cancellationTokenSource.Cancel(); - watchdogTask.GetAwaiter().GetResult(); - } - - /// - /// Executes the , stopping the service if it exits. - /// - /// The to run. - /// The arguments for the . - /// The for the operation. - /// A representing the running operation. - 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(); - } - - /// - /// Sends a command to the main server process. - /// - /// One of the . - 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(); } } } diff --git a/src/Tgstation.Server.Host.Service/ServiceLifetime.cs b/src/Tgstation.Server.Host.Service/ServiceLifetime.cs new file mode 100644 index 0000000000..efc058fbaa --- /dev/null +++ b/src/Tgstation.Server.Host.Service/ServiceLifetime.cs @@ -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 +{ + /// + /// Represents the lifetime of the service. + /// + sealed class ServiceLifetime : ISignalChecker, IAsyncDisposable + { + /// + /// The for the . + /// + readonly ILogger logger; + + /// + /// The that represents the running . + /// + readonly Task watchdogTask; + + /// + /// The for the . + /// + readonly CancellationTokenSource cancellationTokenSource; + + /// + /// The for sending to the server process. + /// + AnonymousPipeServerStream? commandPipeServer; + + /// + /// The for receiving the . + /// + AnonymousPipeServerStream? readyPipeServer; + + /// + /// Initializes a new instance of the class. + /// + /// An to manually stop the service. + /// A taking a and returning the to run. + /// The value of . + /// The arguments for the . + public ServiceLifetime(Action stopService, Func watchdogFactory, ILogger 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); + } + + /// + public async ValueTask DisposeAsync() + { + cancellationTokenSource.Cancel(); + await watchdogTask; + cancellationTokenSource.Dispose(); + + if (commandPipeServer != null) + await commandPipeServer.DisposeAsync(); + + if (readyPipeServer != null) + await readyPipeServer.DisposeAsync(); + } + + /// + public async ValueTask CheckSignals(Func 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; + } + } + + /// + /// Handle a custom service . + /// + /// The command sent to the service. + 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); + } + + /// + /// Executes the , stopping the service if it exits. + /// + /// An to manually stop the service. + /// The to run. + /// The arguments for the . + /// The for the operation. + /// A representing the running operation. + 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!"); + } + } + + /// + /// Sends a command to the main server process. + /// + /// One of the . + 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); + } + } + } +} diff --git a/src/Tgstation.Server.Host.Service/Tgstation.Server.Host.Service.csproj b/src/Tgstation.Server.Host.Service/Tgstation.Server.Host.Service.csproj index aa98536f38..54710f6b47 100644 --- a/src/Tgstation.Server.Host.Service/Tgstation.Server.Host.Service.csproj +++ b/src/Tgstation.Server.Host.Service/Tgstation.Server.Host.Service.csproj @@ -4,6 +4,7 @@ WinExe win-x86;win-x64 + enable $(TgsFrameworkVersion) $(TgsCoreVersion) diff --git a/src/Tgstation.Server.Host.Watchdog/ISignalChecker.cs b/src/Tgstation.Server.Host.Watchdog/ISignalChecker.cs index b23295dd0f..2b8d42ee80 100644 --- a/src/Tgstation.Server.Host.Watchdog/ISignalChecker.cs +++ b/src/Tgstation.Server.Host.Watchdog/ISignalChecker.cs @@ -12,9 +12,9 @@ namespace Tgstation.Server.Host.Watchdog /// /// 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 . + /// An to start the main process. It accepts an optional additional command line argument as a paramter and returns it's and lifetime . Must be called. /// The for the operation. /// A representing the running operation. - ValueTask CheckSignals(Func startChild, CancellationToken cancellationToken); + ValueTask CheckSignals(Func startChildAndGetPid, CancellationToken cancellationToken); } } diff --git a/src/Tgstation.Server.Host.Watchdog/IWatchdog.cs b/src/Tgstation.Server.Host.Watchdog/IWatchdog.cs index dadff2c41b..c9f9e89da9 100644 --- a/src/Tgstation.Server.Host.Watchdog/IWatchdog.cs +++ b/src/Tgstation.Server.Host.Watchdog/IWatchdog.cs @@ -10,9 +10,9 @@ namespace Tgstation.Server.Host.Watchdog public interface IWatchdog { /// - /// Gets the current version of the host process. Set once begins and doesn't immediately return . + /// Gets a resulting in the current version of the host process. Guaranteed to complete once begins and doesn't immediately return . /// - Version InitialHostVersion { get; } + Task InitialHostVersion { get; } /// /// Run the . diff --git a/src/Tgstation.Server.Host.Watchdog/NoopSignalChecker.cs b/src/Tgstation.Server.Host.Watchdog/NoopSignalChecker.cs index fa5dd56f94..270cf63380 100644 --- a/src/Tgstation.Server.Host.Watchdog/NoopSignalChecker.cs +++ b/src/Tgstation.Server.Host.Watchdog/NoopSignalChecker.cs @@ -10,10 +10,10 @@ namespace Tgstation.Server.Host.Watchdog public sealed class NoopSignalChecker : ISignalChecker { /// - public ValueTask CheckSignals(Func startChild, CancellationToken cancellationToken) + public ValueTask CheckSignals(Func startChildAndGetPid, CancellationToken cancellationToken) { - ArgumentNullException.ThrowIfNull(startChild); - startChild(null); + ArgumentNullException.ThrowIfNull(startChildAndGetPid); + startChildAndGetPid(null); return ValueTask.CompletedTask; } } diff --git a/src/Tgstation.Server.Host.Watchdog/Tgstation.Server.Host.Watchdog.csproj b/src/Tgstation.Server.Host.Watchdog/Tgstation.Server.Host.Watchdog.csproj index ae3b89cdb2..04f187c08e 100644 --- a/src/Tgstation.Server.Host.Watchdog/Tgstation.Server.Host.Watchdog.csproj +++ b/src/Tgstation.Server.Host.Watchdog/Tgstation.Server.Host.Watchdog.csproj @@ -3,6 +3,7 @@ $(TgsFrameworkVersion) + enable false $(TgsHostWatchdogVersion) diff --git a/src/Tgstation.Server.Host.Watchdog/Watchdog.cs b/src/Tgstation.Server.Host.Watchdog/Watchdog.cs index bab5e9a07b..a574a6ab6b 100644 --- a/src/Tgstation.Server.Host.Watchdog/Watchdog.cs +++ b/src/Tgstation.Server.Host.Watchdog/Watchdog.cs @@ -20,7 +20,7 @@ namespace Tgstation.Server.Host.Watchdog sealed class Watchdog : IWatchdog { /// - public Version InitialHostVersion { get; private set; } + public Task InitialHostVersion => initialHostVersionTcs.Task; /// /// The for the . @@ -32,6 +32,11 @@ namespace Tgstation.Server.Host.Watchdog /// readonly ILogger logger; + /// + /// Backing for . + /// + readonly TaskCompletionSource initialHostVersionTcs; + /// /// Initializes a new instance of the class. /// @@ -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(); } /// @@ -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) { diff --git a/tests/Tgstation.Server.Host.Service.Tests/TestServerService.cs b/tests/Tgstation.Server.Host.Service.Tests/TestServerService.cs index 855870d4c8..6faf32f95e 100644 --- a/tests/Tgstation.Server.Host.Service.Tests/TestServerService.cs +++ b/tests/Tgstation.Server.Host.Service.Tests/TestServerService.cs @@ -39,15 +39,25 @@ namespace Tgstation.Server.Host.Service.Tests var childStarted = false; ISignalChecker signalChecker = null; - mockWatchdog.Setup(x => x.RunAsync(false, It.IsNotNull(), It.IsAny())).Callback((bool x, string[] _, CancellationToken token) => + var hostVersionTcs = new TaskCompletionSource(); + var hostLifetimeTcs = new TaskCompletionSource(); + + mockWatchdog.Setup(x => x.RunAsync(false, It.IsNotNull(), It.IsAny())).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(); mockWatchdogFactory.Setup(x => x.CreateWatchdog(It.IsNotNull(), It.IsNotNull())) @@ -68,6 +78,7 @@ namespace Tgstation.Server.Host.Service.Tests mockWatchdogFactory.VerifyAll(); Assert.IsTrue(signalCheckerTask.IsCompleted); + Assert.IsTrue(cancellationToken.IsCancellationRequested); } } }