mirror of
https://github.com/tgstation/tgstation-server.git
synced 2026-08-28 23:52:36 +01:00
Make the host watchdog use nullable references
This commit is contained in:
+1
-1
@@ -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>();
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user