Merge pull request #1726 from tgstation/NullableAtLast

Taking further steps towards full nullable reference type support
This commit is contained in:
Jordan Dominion
2023-11-26 17:04:33 -05:00
committed by GitHub
66 changed files with 455 additions and 350 deletions
+1 -1
View File
@@ -17,7 +17,7 @@
<ItemGroup>
<!-- Usage: Linting -->
<PackageReference Include="StyleCop.Analyzers" Version="1.1.118">
<PackageReference Include="StyleCop.Analyzers" Version="1.2.0-beta.507">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
+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>
@@ -6,7 +6,7 @@
public enum EngineType
{
/// <summary>
/// Build your own net dream,
/// Build your own net dream.
/// </summary>
Byond,
+4 -4
View File
@@ -36,7 +36,7 @@ namespace Tgstation.Server.Api.Models
IOError,
/// <summary>
/// The <see cref="ApiHeaders"/> failed to validate!
/// The <see cref="ApiHeaders"/> failed to validate.
/// </summary>
[Description("A header validation error occurred!")]
BadHeaders,
@@ -324,7 +324,7 @@ namespace Tgstation.Server.Api.Models
EngineNoVersionsInstalled,
/// <summary>
/// The DMAPI never validated itself
/// The DMAPI never validated itself.
/// </summary>
[Description("DMAPI validation failed! See FAQ at https://github.com/tgstation/tgstation-server/discussions/1695")]
DeploymentNeverValidated,
@@ -360,7 +360,7 @@ namespace Tgstation.Server.Api.Models
DeploymentExitCode,
/// <summary>
/// Deployment already in progress
/// Deployment already in progress.
/// </summary>
[Description("There is already a deployment operation in progress!")]
DeploymentInProgress,
@@ -600,7 +600,7 @@ namespace Tgstation.Server.Api.Models
FileUploadExpired,
/// <summary>
/// Tried to update a user to have both a <see cref="Internal.UserApiBase.Group"/> and <see cref="Internal.UserApiBase.PermissionSet"/>
/// Tried to update a user to have both a <see cref="Internal.UserApiBase.Group"/> and <see cref="Internal.UserApiBase.PermissionSet"/>.
/// </summary>
[Description("A user may not have both a permissionSet and group!")]
UserGroupAndPermissionSet,
@@ -6,7 +6,7 @@
public enum FieldPresence
{
/// <summary>
/// The field is optional
/// The field is optional.
/// </summary>
Optional,
@@ -6,17 +6,17 @@
public enum IrcPasswordType
{
/// <summary>
/// Use server authentication
/// Use server authentication.
/// </summary>
Server,
/// <summary>
/// Use PLAIN sasl authentication
/// Use PLAIN sasl authentication.
/// </summary>
Sasl,
/// <summary>
/// Use NickServ authentication
/// Use NickServ authentication.
/// </summary>
NickServ,
}
@@ -10,27 +10,27 @@ namespace Tgstation.Server.Api.Models
public enum OAuthProvider
{
/// <summary>
/// https://github.com
/// https://github.com.
/// </summary>
GitHub,
/// <summary>
/// https://discord.com
/// https://discord.com.
/// </summary>
Discord,
/// <summary>
/// https://tgstation13.org
/// https://tgstation13.org.
/// </summary>
TGForums,
/// <summary>
/// https://www.keycloak.org
/// https://www.keycloak.org.
/// </summary>
Keycloak,
/// <summary>
/// https://invisioncommunity.com/
/// https://invisioncommunity.com.
/// </summary>
InvisionCommunity,
}
@@ -11,12 +11,12 @@
Unknown,
/// <summary>
/// Remote provider is GitHub.com
/// Remote provider is GitHub.com.
/// </summary>
GitHub,
/// <summary>
/// Remote provider is GitLab.com
/// Remote provider is GitLab.com.
/// </summary>
GitLab,
}
@@ -16,6 +16,6 @@ namespace Tgstation.Server.Api.Models.Response
/// Parses the <see cref="Bearer"/> as a <see cref="JsonWebToken"/>.
/// </summary>
/// <returns>A new <see cref="JsonWebToken"/> based on <see cref="Bearer"/>.</returns>
public JsonWebToken ParseJwt() => new (Bearer);
public JsonWebToken ParseJwt() => new(Bearer);
}
}
@@ -9,7 +9,7 @@ namespace Tgstation.Server.Api.Rights
public enum AdministrationRights : ulong
{
/// <summary>
/// User has no rights
/// User has no rights.
/// </summary>
None = 0,
@@ -29,7 +29,7 @@ namespace Tgstation.Server.Api.Rights
WriteChannels = 1 << 2,
/// <summary>
/// User can change <see cref="Models.Internal.ChatBotSettings.ConnectionString"/>
/// User can change <see cref="Models.Internal.ChatBotSettings.ConnectionString"/>.
/// </summary>
WriteConnectionString = 1 << 3,
@@ -39,7 +39,7 @@ namespace Tgstation.Server.Api.Rights
ReadConnectionString = 1 << 4,
/// <summary>
/// User can read all chat bot properties except <see cref="Models.Internal.ChatBotSettings.ConnectionString"/>
/// User can read all chat bot properties except <see cref="Models.Internal.ChatBotSettings.ConnectionString"/>.
/// </summary>
Read = 1 << 5,
@@ -74,7 +74,7 @@ namespace Tgstation.Server.Api.Rights
SetStartupTimeout = 1 << 11,
/// <summary>
/// User can change <see cref="Models.Internal.DreamDaemonLaunchParameters.HealthCheckSeconds"/>
/// User can change <see cref="Models.Internal.DreamDaemonLaunchParameters.HealthCheckSeconds"/>.
/// </summary>
SetHealthCheckInterval = 1 << 12,
@@ -9,7 +9,7 @@ namespace Tgstation.Server.Api.Rights
public enum InstancePermissionSetRights : ulong
{
/// <summary>
/// User has no rights/
/// User has no rights.
/// </summary>
None = 0,
@@ -6,47 +6,47 @@
public enum RightsType : ulong
{
/// <summary>
/// <see cref="AdministrationRights"/>
/// <see cref="AdministrationRights"/>.
/// </summary>
Administration,
/// <summary>
/// <see cref="InstanceManagerRights"/>
/// <see cref="InstanceManagerRights"/>.
/// </summary>
InstanceManager,
/// <summary>
/// <see cref="RepositoryRights"/>
/// <see cref="RepositoryRights"/>.
/// </summary>
Repository,
/// <summary>
/// <see cref="EngineRights"/>
/// <see cref="EngineRights"/>.
/// </summary>
Engine,
/// <summary>
/// <see cref="DreamMakerRights"/>
/// <see cref="DreamMakerRights"/>.
/// </summary>
DreamMaker,
/// <summary>
/// <see cref="DreamDaemonRights"/>
/// <see cref="DreamDaemonRights"/>.
/// </summary>
DreamDaemon,
/// <summary>
/// <see cref="ChatBotRights"/>
/// <see cref="ChatBotRights"/>.
/// </summary>
ChatBots,
/// <summary>
/// <see cref="ConfigurationRights"/>
/// <see cref="ConfigurationRights"/>.
/// </summary>
Configuration,
/// <summary>
/// <see cref="InstancePermissionSetRights"/>
/// <see cref="InstancePermissionSetRights"/>.
/// </summary>
InstancePermissionSet,
}
+2 -6
View File
@@ -37,7 +37,7 @@ namespace Tgstation.Server.Client
/// PATCH <see cref="HttpMethod"/>.
/// </summary>
/// <remarks>HOW IS THIS NOT INCLUDED IN THE FRAMEWORK??!?!?</remarks>
static readonly HttpMethod HttpPatch = new ("PATCH");
static readonly HttpMethod HttpPatch = new("PATCH");
/// <inheritdoc />
public Uri Url { get; }
@@ -59,7 +59,7 @@ namespace Tgstation.Server.Client
/// <summary>
/// The <see cref="JsonSerializerSettings"/> to use.
/// </summary>
static readonly JsonSerializerSettings SerializerSettings = new ()
static readonly JsonSerializerSettings SerializerSettings = new()
{
ContractResolver = new CamelCasePropertyNamesContractResolver(),
Converters = new[]
@@ -126,9 +126,7 @@ namespace Tgstation.Server.Client
}
#pragma warning disable IDE0010 // Add missing cases
#pragma warning disable IDE0066 // Convert switch statement to expression
switch (response.StatusCode)
#pragma warning restore IDE0066 // Convert switch statement to expression
#pragma warning restore IDE0010 // Add missing cases
{
case HttpStatusCode.Unauthorized:
@@ -310,9 +308,7 @@ namespace Tgstation.Server.Client
using (memoryStream)
{
#pragma warning disable CA2000 // Dispose objects before losing scope
var streamContent = new StreamContent(uploadStream ?? memoryStream);
#pragma warning restore CA2000 // Dispose objects before losing scope
try
{
await RunRequest<object>(
@@ -138,7 +138,7 @@ namespace Tgstation.Server.Common.Extensions
}
catch (Exception ex)
{
exceptions ??= new (tasks.Count - i);
exceptions ??= new(tasks.Count - i);
exceptions.Add(ex);
}
@@ -4,6 +4,7 @@
<PropertyGroup>
<TargetFrameworks>$(TgsNugetNetFramework)</TargetFrameworks>
<Version>$(TgsCommonLibraryVersion)</Version>
<Nullable>enable</Nullable>
<Description>Common functions for tgstation-server.</Description>
<PackageTags>web tgstation-server tgstation ss13 byond client http</PackageTags>
<PackageReleaseNotes>$(TGS_NUGET_RELEASE_NOTES_COMMON)</PackageReleaseNotes>
@@ -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>(),
newArgs.ToArray());
}
/// <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,210 @@
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;
async void StopServiceAsync()
{
try
{
// This can call OnStop which waits on this task to complete, must be threaded off or it will deadlock
await Task.Run(stopService, cancellationToken);
}
catch (OperationCanceledException ex)
{
logger.LogDebug(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);
}
}
}
}
@@ -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 Pid, Task ChildLifetime)> 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>
+37 -12
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
@@ -89,7 +101,10 @@ namespace Tgstation.Server.Host.Watchdog
Directory.Delete(assemblyStoragePath, true);
Directory.CreateDirectory(defaultAssemblyPath);
var sourcePath = "../../../../Tgstation.Server.Host/bin/Debug/net8.0";
var sourcePath = Path.GetFullPath(
Path.Combine(
rootLocation,
"../../../../Tgstation.Server.Host/bin/Debug/net8.0"));
foreach (string dirPath in Directory.GetDirectories(sourcePath, "*", SearchOption.AllDirectories))
Directory.CreateDirectory(dirPath.Replace(sourcePath, defaultAssemblyPath, StringComparison.Ordinal));
@@ -119,9 +134,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 +182,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 +223,7 @@ namespace Tgstation.Server.Host.Watchdog
var checkerTask = signalChecker.CheckSignals(StartProcess, cts.Token);
try
{
await processTask;
await processTask!;
}
finally
{
@@ -338,10 +362,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)
{
@@ -337,7 +337,7 @@ namespace Tgstation.Server.Host.Components.Chat.Providers
{
var completionString = errorMessage == null ? "Pending" : "Failed";
Embed CreateUpdatedEmbed(string message, Color color) => new ()
Embed CreateUpdatedEmbed(string message, Color color) => new()
{
Author = embed.Author,
Colour = color,
@@ -351,7 +351,7 @@ namespace Tgstation.Server.Host.Components.Chat.Providers
dbChannel,
new List<ChannelRepresentation>
{
new ()
new()
{
RealId = id.Value,
IsAdminChannel = dbChannel.IsAdminChannel == true,
@@ -41,12 +41,12 @@ namespace Tgstation.Server.Host.Components.Engine
/// <summary>
/// The first <see cref="Version"/> of BYOND that supports the '-map-threads' parameter on DreamDaemon.
/// </summary>
static readonly Version MapThreadsVersion = new (515, 1609);
static readonly Version MapThreadsVersion = new(515, 1609);
/// <summary>
/// <see cref="SemaphoreSlim"/> for writing to files in the user's BYOND directory.
/// </summary>
static readonly SemaphoreSlim UserFilesSemaphore = new (1);
static readonly SemaphoreSlim UserFilesSemaphore = new(1);
/// <inheritdoc />
protected override EngineType TargetEngineType => EngineType.Byond;
@@ -50,7 +50,7 @@ namespace Tgstation.Server.Host.Components.Engine
/// <summary>
/// The first version of BYOND to ship with dd.exe on the Windows build.
/// </summary>
public static Version DDExeVersion => new (515, 1598);
public static Version DDExeVersion => new(515, 1598);
/// <inheritdoc />
protected override string DreamMakerName => "dm.exe";
@@ -6,92 +6,92 @@
public enum EventType
{
/// <summary>
/// Parameters: Reference name, commit sha
/// Parameters: Reference name, commit sha.
/// </summary>
[EventScript("RepoResetOrigin")]
RepoResetOrigin,
/// <summary>
/// Parameters: Checkout target
/// Parameters: Checkout target.
/// </summary>
[EventScript("RepoCheckout")]
RepoCheckout,
/// <summary>
/// No parameters
/// No parameters.
/// </summary>
[EventScript("RepoFetch")]
RepoFetch,
/// <summary>
/// Parameters: Test merge number, test merge target sha, merger message
/// Parameters: Test merge number, test merge target sha, merger message.
/// </summary>
[EventScript("RepoMergePullRequest")]
RepoAddTestMerge,
/// <summary>
/// Parameters: Absolute path to repository root
/// Parameters: Absolute path to repository root.
/// </summary>
/// <remarks>Changes made to the repository during this event will be pushed to the tracked branch if no test merges are present.</remarks>
[EventScript("PreSynchronize")]
RepoPreSynchronize,
/// <summary>
/// Parameters: Version being installed
/// Parameters: Version being installed.
/// </summary>
[EventScript("ByondInstallStart", "EngineInstallStart")]
EngineInstallStart,
/// <summary>
/// Parameters: Error string
/// Parameters: Error string.
/// </summary>
[EventScript("ByondInstallFail", "EngineInstallFail")]
EngineInstallFail,
/// <summary>
/// Parameters: Old active version, new active version
/// Parameters: Old active version, new active version.
/// </summary>
[EventScript("ByondActiveVersionChange", "EngineActiveVersionChange")]
EngineActiveVersionChange,
/// <summary>
/// After the repo is copied, before CodeModifications are applied. Parameters: Game directory path, origin commit sha, engine version string
/// After the repo is copied, before CodeModifications are applied. Parameters: Game directory path, origin commit sha, engine version string.
/// </summary>
[EventScript("PreCompile")]
CompileStart,
/// <summary>
/// No parameters
/// No parameters.
/// </summary>
[EventScript("CompileCancelled")]
CompileCancelled,
/// <summary>
/// Parameters: Game directory path, "1" if compile succeeded and api validation failed, "0" otherwise, engine version string
/// Parameters: Game directory path, "1" if compile succeeded and api validation failed, "0" otherwise, engine version string.
/// </summary>
[EventScript("CompileFailure")]
CompileFailure,
/// <summary>
/// Parameters: Game directory path, engine version string
/// Parameters: Game directory path, engine version string.
/// </summary>
[EventScript("PostCompile")]
CompileComplete,
/// <summary>
/// No parameters
/// No parameters.
/// </summary>
[EventScript("InstanceAutoUpdateStart")]
InstanceAutoUpdateStart,
/// <summary>
/// Parameters: Base sha, target sha, base reference, target reference, all conflicting files
/// Parameters: Base sha, target sha, base reference, target reference, all conflicting files.
/// </summary>
[EventScript("RepoMergeConflict")]
RepoMergeConflict,
/// <summary>
/// No parameters
/// No parameters.
/// </summary>
[EventScript("DeploymentComplete")]
DeploymentComplete,
@@ -139,31 +139,31 @@
WorldPrime,
/// <summary>
/// After DD has launched. Not the same as WatchdogLaunch. Parameters: PID of DreamDaemon
/// After DD has launched. Not the same as WatchdogLaunch. Parameters: PID of DreamDaemon.
/// </summary>
[EventScript("DreamDaemonLaunch")]
DreamDaemonLaunch,
/// <summary>
/// After a single submodule update is performed. Parameters: Updated submodule name
/// After a single submodule update is performed. Parameters: Updated submodule name.
/// </summary>
[EventScript("RepoSubmoduleUpdate")]
RepoSubmoduleUpdate,
/// <summary>
/// After CodeModifications are applied, before DreamMaker is run. Parameters: Game directory path, origin commit sha, engine version string
/// After CodeModifications are applied, before DreamMaker is run. Parameters: Game directory path, origin commit sha, engine version string.
/// </summary>
[EventScript("PreDreamMaker")]
PreDreamMaker,
/// <summary>
/// Whenever a deployment folder is deleted from disk. Parameters: Game directory path
/// Whenever a deployment folder is deleted from disk. Parameters: Game directory path.
/// </summary>
[EventScript("DeploymentCleanup")]
DeploymentCleanup,
/// <summary>
/// Whenever a deployment is about to be used by the game server. May fire multiple times per deployment. Parameters: Game directory path
/// Whenever a deployment is about to be used by the game server. May fire multiple times per deployment. Parameters: Game directory path.
/// </summary>
[EventScript("DeploymentActivation")]
DeploymentActivation,
@@ -16,7 +16,7 @@
Startup,
/// <summary>
/// DreamDaemon notifying the server is primed
/// DreamDaemon notifying the server is primed.
/// </summary>
Prime,
@@ -57,7 +57,7 @@ namespace Tgstation.Server.Host.Components.Interop
/// <summary>
/// <see cref="JsonSerializerSettings"/> for use when communicating with the DMAPI.
/// </summary>
public static readonly JsonSerializerSettings SerializerSettings = new ()
public static readonly JsonSerializerSettings SerializerSettings = new()
{
ContractResolver = new DefaultContractResolver
{
@@ -88,7 +88,7 @@ namespace Tgstation.Server.Host.Components.Interop.Topic
/// <param name="newInstanceName">The value of <see cref="NewInstanceName"/>.</param>
/// <returns>The created <see cref="TopicParameters"/>.</returns>
public static TopicParameters CreateInstanceRenamedTopicParameters(string newInstanceName)
=> new (
=> new(
newInstanceName ?? throw new ArgumentNullException(nameof(newInstanceName)),
TopicCommandType.InstanceRenamed);
@@ -98,7 +98,7 @@ namespace Tgstation.Server.Host.Components.Interop.Topic
/// <param name="broadcastMessage">The value of <see cref="BroadcastMessage"/>.</param>
/// <returns>The created <see cref="TopicParameters"/>.</returns>
public static TopicParameters CreateBroadcastParameters(string broadcastMessage)
=> new (
=> new(
broadcastMessage ?? throw new ArgumentNullException(nameof(broadcastMessage)),
TopicCommandType.Broadcast);
@@ -68,7 +68,7 @@ namespace Tgstation.Server.Host.Components.Repository
public string Reference => libGitRepo.Head.FriendlyName;
/// <inheritdoc />
public Uri Origin => new (libGitRepo.Network.Remotes.First().Url);
public Uri Origin => new(libGitRepo.Network.Remotes.First().Url);
/// <summary>
/// The <see cref="LibGit2Sharp.IRepository"/> for the <see cref="Repository"/>.
@@ -6,32 +6,32 @@
enum ApiValidationStatus
{
/// <summary>
/// The DMAPI never contacted the server for validation
/// The DMAPI never contacted the server for validation.
/// </summary>
NeverValidated,
/// <summary>
/// The server was contacted for validation but it was never requested
/// The server was contacted for validation but it was never requested.
/// </summary>
UnaskedValidationRequest,
/// <summary>
/// The validation request was malformed
/// The validation request was malformed.
/// </summary>
BadValidationRequest,
/// <summary>
/// Valid API. The game must be run with a minimum security level of <see cref="Api.Models.DreamDaemonSecurity.Safe"/>
/// Valid API. The game must be run with a minimum security level of <see cref="Api.Models.DreamDaemonSecurity.Safe"/>.
/// </summary>
RequiresSafe,
/// <summary>
/// Valid API. The game must be run with a security level of <see cref="Api.Models.DreamDaemonSecurity.Trusted"/>
/// Valid API. The game must be run with a security level of <see cref="Api.Models.DreamDaemonSecurity.Trusted"/>.
/// </summary>
RequiresTrusted,
/// <summary>
/// Valid API. The game must be run with a minimum security level of <see cref="Api.Models.DreamDaemonSecurity.Ultrasafe"/>
/// Valid API. The game must be run with a minimum security level of <see cref="Api.Models.DreamDaemonSecurity.Ultrasafe"/>.
/// </summary>
RequiresUltrasafe,
@@ -6,17 +6,17 @@
public enum RebootState : int
{
/// <summary>
/// Run DreamDaemon's normal reboot process
/// Run DreamDaemon's normal reboot process.
/// </summary>
Normal = 0,
/// <summary>
/// Shutdown DreamDaemon
/// Shutdown DreamDaemon.
/// </summary>
Shutdown = 1,
/// <summary>
/// Restart the DreamDaemon process
/// Restart the DreamDaemon process.
/// </summary>
Restart = 2,
}
@@ -614,7 +614,7 @@ namespace Tgstation.Server.Host.Components.Session
DreamDaemonSecurity securityLevel,
DreamDaemonVisibility visibility,
bool apiValidateOnly)
=> new (
=> new(
chatTrackingContext,
dmbProvider,
assemblyInformationProvider.Version,
@@ -6,22 +6,22 @@
enum MonitorAction
{
/// <summary>
/// The monitor should continue as normal
/// The monitor should continue as normal.
/// </summary>
Continue,
/// <summary>
/// Skips the next call to HandleMonitorWakeup action
/// Skips the next call to HandleMonitorWakeup action.
/// </summary>
Skip,
/// <summary>
/// The monitor should kill and restart both servers
/// The monitor should kill and restart both servers.
/// </summary>
Restart,
/// <summary>
/// The monitor should stop checking actions for this iteration and continue its loop
/// The monitor should stop checking actions for this iteration and continue its loop.
/// </summary>
Break,
@@ -6,22 +6,22 @@
enum MonitorActivationReason
{
/// <summary>
/// The active server crashed or exited
/// The active server crashed or exited.
/// </summary>
ActiveServerCrashed,
/// <summary>
/// The active server called /world/Reboot()
/// The active server called /world/Reboot().
/// </summary>
ActiveServerRebooted,
/// <summary>
/// A new .dmb was deployed
/// A new .dmb was deployed.
/// </summary>
NewDmbAvailable,
/// <summary>
/// Server launch parameters were changed
/// Server launch parameters were changed.
/// </summary>
ActiveLaunchParametersUpdated,
@@ -6,27 +6,27 @@
public enum DatabaseType
{
/// <summary>
/// Use Microsoft SQL Server
/// Use Microsoft SQL Server.
/// </summary>
SqlServer,
/// <summary>
/// Use MySQL
/// Use MySQL.
/// </summary>
MySql,
/// <summary>
/// Use MariaDB
/// Use MariaDB.
/// </summary>
MariaDB,
/// <summary>
/// Use Sqlite
/// Use Sqlite.
/// </summary>
Sqlite,
/// <summary>
/// Use PostgresSql
/// Use PostgresSql.
/// </summary>
PostgresSql,
}
@@ -10,7 +10,7 @@ namespace Tgstation.Server.Host.Extensions
/// <summary>
/// A <see cref="TaskCompletionSource"/> that never completes.
/// </summary>
static readonly TaskCompletionSource InfiniteTaskCompletionSource = new ();
static readonly TaskCompletionSource InfiniteTaskCompletionSource = new();
/// <summary>
/// Gets a <see cref="Task"/> that never completes.
@@ -102,7 +102,7 @@ namespace Tgstation.Server.Host.IO
/// </summary>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> for the operation.</param>
/// <returns>A <see cref="Task{TResult}"/> resulting in <see cref="buffer"/> and its <see cref="Stream.Length"/>.</returns>
async ValueTask<(MemoryStream, long)> GetResultInternal(CancellationToken cancellationToken)
async ValueTask<(MemoryStream Stream, long StreamLength)> GetResultInternal(CancellationToken cancellationToken)
{
if (!buffered)
using (await SemaphoreSlimContext.Lock(semaphore, cancellationToken))
@@ -115,15 +115,15 @@ namespace Tgstation.Server.Host.IO
await input.CopyToAsync(localBuffer, cancellationToken);
localBuffer.Seek(0, SeekOrigin.Begin);
buffered = true;
return (localBuffer, localBuffer.Length);
return (Stream: localBuffer, StreamLength: localBuffer.Length);
}
lock (semaphore)
{
var localBuffer = buffer ?? throw new ObjectDisposedException(nameof(BufferedFileStreamProvider));
return (
localBuffer,
localBuffer.Length);
Stream: localBuffer,
StreamLength: localBuffer.Length);
}
}
}
@@ -323,7 +323,7 @@ namespace Tgstation.Server.Host.IO
TaskScheduler.Current);
/// <inheritdoc />
public FileStream GetFileStream(string path, bool shareWrite) => new (
public FileStream GetFileStream(string path, bool shareWrite) => new(
ResolvePath(path),
FileMode.Open,
FileAccess.Read,
@@ -82,7 +82,7 @@ namespace Tgstation.Server.Host.Models
}
/// <inheritdoc />
public CompileJobResponse ToApi() => new ()
public CompileJobResponse ToApi() => new()
{
DirectoryName = DirectoryName,
DmeName = DmeName,
+1 -1
View File
@@ -55,7 +55,7 @@ namespace Tgstation.Server.Host.Models
public ICollection<Job> Jobs { get; set; }
/// <inheritdoc />
public InstanceResponse ToApi() => new ()
public InstanceResponse ToApi() => new()
{
AutoUpdateInterval = AutoUpdateInterval,
ConfigurationType = ConfigurationType,
+3 -3
View File
@@ -42,7 +42,7 @@ namespace Tgstation.Server.Host.Models
/// <returns>A new <see cref="Job"/> ready to be registered with the <see cref="Jobs.IJobService"/>.</returns>
public static Job Create<TRight>(JobCode code, User startedBy, Api.Models.Instance instance, TRight cancelRight)
where TRight : Enum
=> new (
=> new(
code,
startedBy,
instance,
@@ -57,7 +57,7 @@ namespace Tgstation.Server.Host.Models
/// <param name="instance">The <see cref="Api.Models.Instance"/> used to generate the value of <see cref="Instance"/>.</param>
/// <returns>A new <see cref="Job"/> ready to be registered with the <see cref="Jobs.IJobService"/>.</returns>
public static Job Create(JobCode code, User startedBy, Api.Models.Instance instance)
=> new (
=> new(
code,
startedBy,
instance,
@@ -109,7 +109,7 @@ namespace Tgstation.Server.Host.Models
}
/// <inheritdoc />
public JobResponse ToApi() => new ()
public JobResponse ToApi() => new()
{
Id = Id,
JobCode = JobCode.Value,
@@ -28,7 +28,7 @@ namespace Tgstation.Server.Host.Models
/// </summary>
/// <param name="showUsers">If <see cref="UserGroupResponse.Users"/> should be populated.</param>
/// <returns>A new <see cref="UserGroupResponse"/>.</returns>
public UserGroupResponse ToApi(bool showUsers) => new ()
public UserGroupResponse ToApi(bool showUsers) => new()
{
Id = Id,
Name = Name,
@@ -17,10 +17,10 @@ namespace Tgstation.Server.Host.Security.OAuth
public override OAuthProvider Provider => OAuthProvider.Discord;
/// <inheritdoc />
protected override Uri TokenUrl => new ("https://discord.com/api/oauth2/token");
protected override Uri TokenUrl => new("https://discord.com/api/oauth2/token");
/// <inheritdoc />
protected override Uri UserInformationUrl => new ("https://discord.com/api/users/@me");
protected override Uri UserInformationUrl => new("https://discord.com/api/users/@me");
/// <summary>
/// Initializes a new instance of the <see cref="DiscordOAuthValidator"/> class.
@@ -37,7 +37,7 @@ namespace Tgstation.Server.Host.Security.OAuth
}
/// <inheritdoc />
protected override OAuthTokenRequest CreateTokenRequest(string code) => new (OAuthConfiguration, code, "identify");
protected override OAuthTokenRequest CreateTokenRequest(string code) => new(OAuthConfiguration, code, "identify");
/// <inheritdoc />
protected override string DecodeTokenPayload(dynamic responseJson) => responseJson.access_token;
@@ -55,7 +55,7 @@ namespace Tgstation.Server.Host.Security.OAuth
/// Gets <see cref="JsonSerializerSettings"/> that should be used.
/// </summary>
/// <returns>A new <see cref="JsonSerializerSettings"/> <see cref="object"/>.</returns>
protected static JsonSerializerSettings SerializerSettings() => new ()
protected static JsonSerializerSettings SerializerSettings() => new()
{
ContractResolver = new DefaultContractResolver
{
@@ -141,7 +141,7 @@ namespace Tgstation.Server.Host.Security.OAuth
/// <inheritdoc />
public OAuthProviderInfo GetProviderInfo()
=> new ()
=> new()
{
ClientId = OAuthConfiguration.ClientId,
RedirectUri = OAuthConfiguration.RedirectUrl,
@@ -85,7 +85,7 @@ namespace Tgstation.Server.Host.Security.OAuth
/// <inheritdoc />
public OAuthProviderInfo GetProviderInfo()
=> new ()
=> new()
{
ClientId = oAuthConfiguration.ClientId,
RedirectUri = oAuthConfiguration.RedirectUrl,
@@ -17,10 +17,10 @@ namespace Tgstation.Server.Host.Security.OAuth
public override OAuthProvider Provider => OAuthProvider.InvisionCommunity;
/// <inheritdoc />
protected override Uri TokenUrl => new ($"{OAuthConfiguration.ServerUrl}/oauth/token/"); // This needs the trailing slash or it doesnt get the token. Do not remove.
protected override Uri TokenUrl => new($"{OAuthConfiguration.ServerUrl}/oauth/token/"); // This needs the trailing slash or it doesnt get the token. Do not remove.
/// <inheritdoc />
protected override Uri UserInformationUrl => new ($"{OAuthConfiguration.ServerUrl}/api/core/me");
protected override Uri UserInformationUrl => new($"{OAuthConfiguration.ServerUrl}/api/core/me");
/// <summary>
/// Initializes a new instance of the <see cref="InvisionCommunityOAuthValidator"/> class.
@@ -37,7 +37,7 @@ namespace Tgstation.Server.Host.Security.OAuth
}
/// <inheritdoc />
protected override OAuthTokenRequest CreateTokenRequest(string code) => new (OAuthConfiguration, code, "profile");
protected override OAuthTokenRequest CreateTokenRequest(string code) => new(OAuthConfiguration, code, "profile");
/// <inheritdoc />
protected override string DecodeTokenPayload(dynamic responseJson) => responseJson.access_token;
@@ -17,10 +17,10 @@ namespace Tgstation.Server.Host.Security.OAuth
public override OAuthProvider Provider => OAuthProvider.Keycloak;
/// <inheritdoc />
protected override Uri TokenUrl => new ($"{BaseProtocolPath}/token");
protected override Uri TokenUrl => new($"{BaseProtocolPath}/token");
/// <inheritdoc />
protected override Uri UserInformationUrl => new ($"{BaseProtocolPath}/userinfo");
protected override Uri UserInformationUrl => new($"{BaseProtocolPath}/userinfo");
/// <summary>
/// Base path to the server's OAuth endpoint.
@@ -42,7 +42,7 @@ namespace Tgstation.Server.Host.Security.OAuth
}
/// <inheritdoc />
protected override OAuthTokenRequest CreateTokenRequest(string code) => new (OAuthConfiguration, code, "openid");
protected override OAuthTokenRequest CreateTokenRequest(string code) => new(OAuthConfiguration, code, "openid");
/// <inheritdoc />
protected override string DecodeTokenPayload(dynamic responseJson) => responseJson.access_token;
@@ -17,10 +17,10 @@ namespace Tgstation.Server.Host.Security.OAuth
public override OAuthProvider Provider => OAuthProvider.TGForums;
/// <inheritdoc />
protected override Uri TokenUrl => new ("https://tgstation13.org/phpBB/app.php/tgapi/oauth/token");
protected override Uri TokenUrl => new("https://tgstation13.org/phpBB/app.php/tgapi/oauth/token");
/// <inheritdoc />
protected override Uri UserInformationUrl => new ("https://tgstation13.org/phpBB/app.php/tgapi/user/me");
protected override Uri UserInformationUrl => new("https://tgstation13.org/phpBB/app.php/tgapi/user/me");
/// <summary>
/// Initializes a new instance of the <see cref="TGForumsOAuthValidator"/> class.
@@ -46,6 +46,6 @@ namespace Tgstation.Server.Host.Security.OAuth
protected override string DecodeUserInformationPayload(dynamic responseJson) => responseJson.phpbb_username;
/// <inheritdoc />
protected override OAuthTokenRequest CreateTokenRequest(string code) => new (OAuthConfiguration, code, "user");
protected override OAuthTokenRequest CreateTokenRequest(string code) => new(OAuthConfiguration, code, "user");
}
}
@@ -66,7 +66,7 @@ namespace Tgstation.Server.Host.Swarm
/// </summary>
static SwarmConstants()
{
SerializerSettings = new ()
SerializerSettings = new()
{
ContractResolver = new DefaultContractResolver
{
+10 -10
View File
@@ -109,7 +109,7 @@ namespace Tgstation.Server.Host.Swarm
/// <summary>
/// <see cref="Dictionary{TKey, TValue}"/> of <see cref="Api.Models.Internal.SwarmServer.Identifier"/>s to registration <see cref="Guid"/>s and when they were created.
/// </summary>
readonly Dictionary<string, (Guid, DateTimeOffset)> registrationIdsAndTimes;
readonly Dictionary<string, (Guid RegistrationId, DateTimeOffset RegisteredAt)> registrationIdsAndTimes;
/// <summary>
/// If the current server is the swarm controller.
@@ -193,7 +193,7 @@ namespace Tgstation.Server.Host.Swarm
serverHealthCheckCancellationTokenSource = new CancellationTokenSource();
forceHealthCheckTcs = new TaskCompletionSource();
if (swarmController)
registrationIdsAndTimes = new ();
registrationIdsAndTimes = new();
swarmServers = new List<SwarmServerResponse>
{
@@ -520,7 +520,7 @@ namespace Tgstation.Server.Host.Swarm
{
if (swarmController)
lock (swarmServers)
return registrationIdsAndTimes.Values.Any(x => x.Item1 == registrationId);
return registrationIdsAndTimes.Values.Any(x => x.RegistrationId == registrationId);
if (registrationId != controllerRegistration)
return false;
@@ -549,9 +549,9 @@ namespace Tgstation.Server.Host.Swarm
lock (swarmServers)
{
if (registrationIdsAndTimes.Any(x => x.Value.Item1 == registrationId))
if (registrationIdsAndTimes.Any(x => x.Value.RegistrationId == registrationId))
{
var preExistingRegistrationKvp = registrationIdsAndTimes.FirstOrDefault(x => x.Value.Item1 == registrationId);
var preExistingRegistrationKvp = registrationIdsAndTimes.FirstOrDefault(x => x.Value.RegistrationId == registrationId);
if (preExistingRegistrationKvp.Key == node.Identifier)
{
logger.LogWarning("Node {nodeId} has already registered!", node.Identifier);
@@ -580,7 +580,7 @@ namespace Tgstation.Server.Host.Swarm
Identifier = node.Identifier,
Controller = false,
});
registrationIdsAndTimes.Add(node.Identifier, (registrationId, DateTimeOffset.UtcNow));
registrationIdsAndTimes.Add(node.Identifier, (RegistrationId: registrationId, DateTimeOffset.UtcNow));
}
logger.LogInformation("Registered node {nodeId} ({nodeIP}) with ID {registrationId}", node.Identifier, node.Address, registrationId);
@@ -1139,7 +1139,7 @@ namespace Tgstation.Server.Host.Swarm
currentSwarmServers
.Where(node => !node.Controller
&& registrationIdsAndTimes.TryGetValue(node.Identifier, out var registrationAndTime)
&& registrationAndTime.Item2.AddMinutes(SwarmConstants.ControllerHealthCheckIntervalMinutes) < DateTimeOffset.UtcNow)
&& registrationAndTime.RegisteredAt.AddMinutes(SwarmConstants.ControllerHealthCheckIntervalMinutes) < DateTimeOffset.UtcNow)
.Select(HealthRequestForServer));
lock (swarmServers)
@@ -1390,7 +1390,7 @@ namespace Tgstation.Server.Host.Swarm
{
lock (swarmServers)
if (registrationIdsAndTimes.TryGetValue(swarmServer.Identifier, out var registrationIdAndTime))
request.Headers.Add(SwarmConstants.RegistrationIdHeader, registrationIdAndTime.Item1.ToString());
request.Headers.Add(SwarmConstants.RegistrationIdHeader, registrationIdAndTime.RegistrationId.ToString());
}
else if (controllerRegistration.HasValue)
request.Headers.Add(SwarmConstants.RegistrationIdHeader, controllerRegistration.Value.ToString());
@@ -1505,14 +1505,14 @@ namespace Tgstation.Server.Host.Swarm
lock (swarmServers)
{
var exists = registrationIdsAndTimes.Any(x => x.Value.Item1 == registrationId);
var exists = registrationIdsAndTimes.Any(x => x.Value.RegistrationId == registrationId);
if (!exists)
{
logger.LogWarning("A node that was to be looked up ({registrationId}) disappeared from our records!", registrationId);
return null;
}
return registrationIdsAndTimes.First(x => x.Value.Item1 == registrationId).Key;
return registrationIdsAndTimes.First(x => x.Value.RegistrationId == registrationId).Key;
}
}
}
@@ -26,7 +26,7 @@ namespace Tgstation.Server.Host.System
public string VersionString { get; }
/// <inheritdoc />
public ProductInfoHeaderValue ProductInfoHeaderValue => new (
public ProductInfoHeaderValue ProductInfoHeaderValue => new(
VersionPrefix,
Version.ToString());
@@ -17,7 +17,7 @@ namespace Tgstation.Server.Host.System
/// <summary>
/// <see cref="ReaderWriterLockSlim"/> for <see cref="WithProcessLaunchExclusivity(Action)"/>.
/// </summary>
static readonly ReaderWriterLockSlim ExclusiveProcessLaunchLock = new ();
static readonly ReaderWriterLockSlim ExclusiveProcessLaunchLock = new();
/// <summary>
/// The <see cref="IProcessFeatures"/> for the <see cref="ProcessExecutor"/>.
@@ -74,7 +74,7 @@ namespace Tgstation.Server.Host.System
try
{
var pointerChildHandlesList = GCHandle.ToIntPtr(gcChildhandlesList);
NativeMethods.EnumWindowProc childProc = new (EnumWindow);
NativeMethods.EnumWindowProc childProc = new(EnumWindow);
NativeMethods.EnumChildWindows(mainWindow, childProc, pointerChildHandlesList);
}
finally
@@ -113,11 +113,12 @@ namespace Tgstation.Server.Host.System
process.Lifetime.ContinueWith(
x =>
{
logger.LogTrace("Unregistering process {0}...", process.Id);
lock (registeredProcesses)
registeredProcesses.Remove(process);
}, TaskScheduler.Current);
{
logger.LogTrace("Unregistering process {pid}...", process.Id);
lock (registeredProcesses)
registeredProcesses.Remove(process);
},
TaskScheduler.Current);
}
/// <inheritdoc />
@@ -240,7 +240,7 @@ namespace Tgstation.Server.Host.Transfer
/// Creates a new <see cref="FileTicketResponse"/>.
/// </summary>
/// <returns>A new <see cref="FileTicketResponse"/>.</returns>
FileTicketResponse CreateTicket() => new ()
FileTicketResponse CreateTicket() => new()
{
FileTicket = cryptographySuite.GetSecureString(),
};
@@ -43,7 +43,7 @@ namespace Tgstation.Server.Host.Utils.GitHub
/// <summary>
/// Cache of created <see cref="GitHubClient"/>s and last used times, keyed by access token.
/// </summary>
readonly Dictionary<string, (GitHubClient, DateTimeOffset)> clientCache;
readonly Dictionary<string, (GitHubClient Client, DateTimeOffset LastUsed)> clientCache;
/// <summary>
/// Initializes a new instance of the <see cref="GitHubClientFactory"/> class.
@@ -105,15 +105,15 @@ namespace Tgstation.Server.Host.Utils.GitHub
if (accessToken != null)
client.Credentials = new Credentials(accessToken);
clientCache.Add(cacheKey, (client, now));
clientCache.Add(cacheKey, (Client: client, LastUsed: now));
lastUsed = null;
}
else
{
logger.LogTrace("Cache hit for GitHubClient");
client = tuple.Item1;
lastUsed = tuple.Item2;
tuple.Item2 = now;
client = tuple.Client;
lastUsed = tuple.LastUsed;
tuple.LastUsed = now;
}
// Prune the cache
@@ -125,7 +125,7 @@ namespace Tgstation.Server.Host.Utils.GitHub
continue; // save the hash lookup
tuple = clientCache[key];
if (tuple.Item2 <= purgeAfter)
if (tuple.LastUsed <= purgeAfter)
{
clientCache.Remove(key);
++purgeCount;
@@ -58,7 +58,7 @@ namespace Tgstation.Server.Host.Utils.GitHub
/// <param name="gitHubClient">The <see cref="IGitHubClient"/> for the <see cref="GitHubService"/>.</param>
/// <returns>A new <see cref="GitHubService"/>.</returns>
GitHubService CreateServiceImpl(IGitHubClient gitHubClient)
=> new (
=> new(
gitHubClient,
loggerFactory.CreateLogger<GitHubService>(),
updatesConfiguration);
@@ -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);
}
}
}