diff --git a/.github/workflows/ci-pipeline.yml b/.github/workflows/ci-pipeline.yml index 1efe65423b..483a030d50 100644 --- a/.github/workflows/ci-pipeline.yml +++ b/.github/workflows/ci-pipeline.yml @@ -203,7 +203,7 @@ jobs: - name: Create TGS Deployment run: | cd $HOME/OpenDream - dotnet run -c Release --project OpenDreamPackageTool -- --tgs -o tgs_deploy + dotnet run -c Release --project OpenDreamPackageTool --property WarningLevel=0 -- --tgs -o tgs_deploy - name: Build DMAPI run: | diff --git a/src/Tgstation.Server.Api/Models/EngineVersion.cs b/src/Tgstation.Server.Api/Models/EngineVersion.cs index d4681faa60..adaa90bcbf 100644 --- a/src/Tgstation.Server.Api/Models/EngineVersion.cs +++ b/src/Tgstation.Server.Api/Models/EngineVersion.cs @@ -38,7 +38,7 @@ namespace Tgstation.Server.Api.Models public int? CustomIteration { get; set; } /// - /// Parses a stringified . + /// Attempts to parse a stringified . /// /// The input . /// The output . @@ -110,6 +110,23 @@ namespace Tgstation.Server.Api.Models return true; } + /// + /// Parses a stringified . + /// + /// The input . + /// The output . + /// If the is not a valid stringified . + public static EngineVersion Parse(string input) + { + if (input == null) + throw new ArgumentNullException(nameof(input)); + + if (TryParse(input, out var engineVersion)) + return engineVersion!; + + throw new InvalidOperationException($"Invalid engine version: {input}"); + } + /// /// Initializes a new instance of the class. /// diff --git a/src/Tgstation.Server.Api/Models/ErrorCode.cs b/src/Tgstation.Server.Api/Models/ErrorCode.cs index 886816346a..28f39a7638 100644 --- a/src/Tgstation.Server.Api/Models/ErrorCode.cs +++ b/src/Tgstation.Server.Api/Models/ErrorCode.cs @@ -150,10 +150,10 @@ namespace Tgstation.Server.Api.Models InstanceLimitReached, /// - /// Attempted to create an with a whitespace . + /// Attempted to create an with a whitespace or . /// - [Description("Instance names cannot be whitespace!")] - InstanceWhitespaceName, + [Description("Instance names and paths cannot be whitespace!")] + InstanceWhitespaceNameOrPath, /// /// The header was required but not set. diff --git a/src/Tgstation.Server.Api/Models/JobCode.cs b/src/Tgstation.Server.Api/Models/JobCode.cs index e82600b302..be5db8d5a6 100644 --- a/src/Tgstation.Server.Api/Models/JobCode.cs +++ b/src/Tgstation.Server.Api/Models/JobCode.cs @@ -16,7 +16,7 @@ namespace Tgstation.Server.Api.Models /// /// When the instance is being moved. /// - [Description("Instance move")] + [Description("Move instance")] Move, /// diff --git a/src/Tgstation.Server.Api/Models/Response/ServerUpdateResponse.cs b/src/Tgstation.Server.Api/Models/Response/ServerUpdateResponse.cs index 3578e6cd0a..48c9b99e6a 100644 --- a/src/Tgstation.Server.Api/Models/Response/ServerUpdateResponse.cs +++ b/src/Tgstation.Server.Api/Models/Response/ServerUpdateResponse.cs @@ -20,7 +20,7 @@ namespace Tgstation.Server.Api.Models.Response /// The value of . /// The optional value of . [JsonConstructor] - public ServerUpdateResponse(Version newVersion, string fileTicket) + public ServerUpdateResponse(Version newVersion, string? fileTicket) { NewVersion = newVersion ?? throw new ArgumentNullException(nameof(newVersion)); FileTicket = fileTicket; diff --git a/src/Tgstation.Server.Host/Components/Chat/ChannelMapping.cs b/src/Tgstation.Server.Host/Components/Chat/ChannelMapping.cs index d7430d7c69..19581b09bb 100644 --- a/src/Tgstation.Server.Host/Components/Chat/ChannelMapping.cs +++ b/src/Tgstation.Server.Host/Components/Chat/ChannelMapping.cs @@ -1,4 +1,6 @@ -namespace Tgstation.Server.Host.Components.Chat +using System; + +namespace Tgstation.Server.Host.Components.Chat { /// /// Represents a mapping of a . @@ -38,6 +40,15 @@ /// /// The with the mapped Id. /// - public ChannelRepresentation Channel { get; set; } + public ChannelRepresentation Channel { get; } + + /// + /// Initializes a new instance of the class. + /// + /// The value of . + public ChannelMapping(ChannelRepresentation channel) + { + Channel = channel ?? throw new ArgumentNullException(nameof(channel)); + } } } diff --git a/src/Tgstation.Server.Host/Components/Chat/ChannelRepresentation.cs b/src/Tgstation.Server.Host/Components/Chat/ChannelRepresentation.cs index a24f416ec1..b1d4aa3ad2 100644 --- a/src/Tgstation.Server.Host/Components/Chat/ChannelRepresentation.cs +++ b/src/Tgstation.Server.Host/Components/Chat/ChannelRepresentation.cs @@ -14,7 +14,7 @@ namespace Tgstation.Server.Host.Components.Chat /// /// Backing field for . Represented as a to avoid BYOND percision loss. /// - public string Id { get; set; } + public string Id { get; private set; } /// /// The channel Id. @@ -30,12 +30,12 @@ namespace Tgstation.Server.Host.Components.Chat /// /// The user friendly name of the . /// - public string FriendlyName { get; set; } + public string FriendlyName { get; } /// /// The name of the connection the belongs to. /// - public string ConnectionName { get; set; } + public string ConnectionName { get; } /// /// If this is considered a channel for admin commands. @@ -45,16 +45,30 @@ namespace Tgstation.Server.Host.Components.Chat /// /// If this is a 1-to-1 chat channel. /// - public bool IsPrivateChannel { get; set; } + public bool IsPrivateChannel { get; init; } /// /// For user use. /// - public string Tag { get; set; } + public string? Tag { get; set; } /// /// If this channel supports embeds. /// - public bool EmbedsSupported { get; set; } + public bool EmbedsSupported { get; init; } + + /// + /// Initializes a new instance of the class. + /// + /// The value of . + /// The value of . + /// The value of /. + public ChannelRepresentation(string connectionName, string friendlyName, ulong id) + { + ConnectionName = connectionName ?? throw new ArgumentNullException(nameof(connectionName)); + FriendlyName = friendlyName ?? throw new ArgumentNullException(nameof(friendlyName)); + Id = null!; + RealId = id; + } } } diff --git a/src/Tgstation.Server.Host/Components/Chat/ChatManager.cs b/src/Tgstation.Server.Host/Components/Chat/ChatManager.cs index 7e3daa6646..d53e997b25 100644 --- a/src/Tgstation.Server.Host/Components/Chat/ChatManager.cs +++ b/src/Tgstation.Server.Host/Components/Chat/ChatManager.cs @@ -24,8 +24,7 @@ using Tgstation.Server.Host.Utils; namespace Tgstation.Server.Host.Components.Chat { /// - // TODO: Decomplexify -#pragma warning disable CA1506 +#pragma warning disable CA1506 // TODO: Decomplexify sealed class ChatManager : IChatManager, IRestartHandler { /// @@ -69,7 +68,7 @@ namespace Tgstation.Server.Host.Components.Chat readonly Dictionary providers; /// - /// Map of s used to guard concurrent access to , keyed by . + /// Map of s used to guard concurrent access to , keyed by . /// readonly ConcurrentDictionary changeChannelSemaphores; @@ -101,17 +100,17 @@ namespace Tgstation.Server.Host.Components.Chat /// /// The for the . /// - ICustomCommandHandler customCommandHandler; + ICustomCommandHandler? customCommandHandler; /// /// The that monitors incoming chat messages. /// - Task chatHandler; + Task? chatHandler; /// /// A that represents the s initial connection. /// - Task initialProviderConnectionsTask; + Task? initialProviderConnectionsTask; /// /// A that represents all sent messages. @@ -234,7 +233,7 @@ namespace Tgstation.Server.Host.Components.Chat var newMappings = results.SelectMany( kvp => kvp.Value.Select( - channelRepresentation => new ChannelMapping + channelRepresentation => new ChannelMapping(channelRepresentation) { IsWatchdogChannel = kvp.Key.IsWatchdogChannel == true, IsUpdatesChannel = kvp.Key.IsUpdatesChannel == true, @@ -242,7 +241,6 @@ namespace Tgstation.Server.Host.Components.Chat IsSystemChannel = kvp.Key.IsSystemChannel == true, ProviderChannelId = channelRepresentation.RealId, ProviderId = connectionId, - Channel = channelRepresentation, })); ulong baseId; @@ -255,7 +253,7 @@ namespace Tgstation.Server.Host.Components.Chat lock (mappedChannels) { lock (providers) - if (!providers.TryGetValue(connectionId, out IProvider verify) || verify != provider) // aborted again + if (!providers.TryGetValue(connectionId, out var verify) || verify != provider) // aborted again return; foreach (var newMapping in newMappings) { @@ -269,7 +267,7 @@ namespace Tgstation.Server.Host.Components.Chat // we only want to update contexts if everything at startup has connected once already // otherwise we could send an incomplete channel set to the DMAPI, which will then spout all its queued messages into it instead of all relevant chatbots // The watchdog can call this if it needs to after starting up - if (initialProviderConnectionsTask.IsCompleted) + if (initialProviderConnectionsTask!.IsCompleted) await UpdateTrackingContexts(cancellationToken); } finally @@ -287,23 +285,25 @@ namespace Tgstation.Server.Host.Components.Chat logger.LogTrace("ChangeSettings..."); Task disconnectTask; - IProvider provider = null; + IProvider? provider = null; + var newSettingsId = Models.ModelExtensions.Require(newSettings, x => x.Id); + var newSettingsEnabled = Models.ModelExtensions.Require(newSettings, x => x.Enabled); lock (providers) { // raw settings changes forces a rebuild of the provider - if (providers.ContainsKey(newSettings.Id.Value)) - disconnectTask = DeleteConnection(newSettings.Id.Value, cancellationToken); + if (providers.ContainsKey(newSettingsId)) + disconnectTask = DeleteConnection(newSettingsId, cancellationToken); else disconnectTask = Task.CompletedTask; - if (newSettings.Enabled.Value) + if (newSettingsEnabled) { provider = providerFactory.CreateProvider(newSettings); - providers.Add(newSettings.Id.Value, provider); + providers.Add(newSettingsId, provider); } } lock (mappedChannels) - foreach (var oldMappedChannelId in mappedChannels.Where(x => x.Value.ProviderId == newSettings.Id).Select(x => x.Key).ToList()) + foreach (var oldMappedChannelId in mappedChannels.Where(x => x.Value.ProviderId == newSettingsId).Select(x => x.Key).ToList()) mappedChannels.Remove(oldMappedChannelId); await disconnectTask; @@ -317,8 +317,8 @@ namespace Tgstation.Server.Host.Components.Chat } var reconnectionUpdateTask = provider?.SetReconnectInterval( - newSettings.ReconnectionInterval.Value, - newSettings.Enabled.Value) + Models.ModelExtensions.Require(newSettings, x => x.ReconnectionInterval), + newSettingsEnabled) ?? Task.CompletedTask; lock (activeChatBots) { @@ -326,7 +326,7 @@ namespace Tgstation.Server.Host.Components.Chat if (originalChatBot != null) activeChatBots.Remove(originalChatBot); - activeChatBots.Add(new Models.ChatBot + activeChatBots.Add(new Models.ChatBot(newSettings.Channels) { Id = newSettings.Id, ConnectionString = newSettings.ConnectionString, @@ -334,7 +334,6 @@ namespace Tgstation.Server.Host.Components.Chat Name = newSettings.Name, ReconnectionInterval = newSettings.ReconnectionInterval, Provider = newSettings.Provider, - Channels = newSettings.Channels, }); } @@ -357,7 +356,7 @@ namespace Tgstation.Server.Host.Components.Chat message = String.Format(CultureInfo.InvariantCulture, "WD: {0}", message); - if (!initialProviderConnectionsTask.IsCompleted) + if (!initialProviderConnectionsTask!.IsCompleted) logger.LogTrace("Waiting for initial provider connections before sending watchdog message..."); // Reimplementing QueueMessage @@ -376,12 +375,12 @@ namespace Tgstation.Server.Host.Components.Chat } /// - public Func> QueueDeploymentMessage( + public Func> QueueDeploymentMessage( Models.RevisionInformation revisionInformation, EngineVersion engineVersion, DateTimeOffset? estimatedCompletionTime, - string gitHubOwner, - string gitHubRepo, + string? gitHubOwner, + string? gitHubRepo, bool localCommitPushed) { List wdChannels; @@ -390,17 +389,17 @@ namespace Tgstation.Server.Host.Components.Chat logger.LogTrace("Sending deployment message for RevisionInformation: {revisionInfoId}", revisionInformation.Id); - var callbacks = new List>>>(); + var callbacks = new List>>>(); var task = Task.WhenAll( wdChannels.Select( async x => { - ChannelMapping channelMapping; + ChannelMapping? channelMapping; lock (mappedChannels) if (!mappedChannels.TryGetValue(x, out channelMapping)) return; - IProvider provider; + IProvider? provider; lock (providers) if (!providers.TryGetValue(channelMapping.ProviderId, out provider)) return; @@ -431,8 +430,8 @@ namespace Tgstation.Server.Host.Components.Chat AddMessageTask(task); Task callbackTask; - Func finalUpdateAction = null; - async Task CallbackTask(string errorMessage, string dreamMakerOutput) + Func? finalUpdateAction = null; + async Task CallbackTask(string? errorMessage, string dreamMakerOutput) { await task; var callbackResults = await ValueTaskExtensions.WhenAll( @@ -457,7 +456,7 @@ namespace Tgstation.Server.Host.Components.Chat return; } - AddMessageTask(finalUpdateAction(active)); + AddMessageTask(finalUpdateAction!(active)); } return (errorMessage, dreamMakerOutput) => @@ -495,7 +494,7 @@ namespace Tgstation.Server.Host.Components.Chat if (customCommandHandler == null) throw new InvalidOperationException("RegisterCommandHandler() hasn't been called!"); - IChatTrackingContext context = null; + IChatTrackingContext context = null!; lock (mappedChannels) context = new ChatTrackingContext( customCommandHandler, @@ -524,7 +523,7 @@ namespace Tgstation.Server.Host.Components.Chat await channelSink.UpdateChannels(channels, cancellationToken); } - var waitingForInitialConnection = !initialProviderConnectionsTask.IsCompleted; + var waitingForInitialConnection = !initialProviderConnectionsTask!.IsCompleted; if (waitingForInitialConnection) { logger.LogTrace("Waiting for initial chat bot connections before updating tracking contexts..."); @@ -562,7 +561,7 @@ namespace Tgstation.Server.Host.Components.Chat ? semaphore : null) using (hasSemaphore - ? await SemaphoreSlimContext.Lock(semaphore, cancellationToken) + ? await SemaphoreSlimContext.Lock(semaphore!, cancellationToken) : null) { var provider = await RemoveProviderChannels(connectionId, true, cancellationToken); @@ -589,7 +588,7 @@ namespace Tgstation.Server.Host.Components.Chat } /// - public ValueTask HandleRestart(Version updateVersion, bool handlerMayDelayShutdownWithExtremelyLongRunningTasks, CancellationToken cancellationToken) + public ValueTask HandleRestart(Version? updateVersion, bool handlerMayDelayShutdownWithExtremelyLongRunningTasks, CancellationToken cancellationToken) { var message = updateVersion == null ? $"TGS: {(handlerMayDelayShutdownWithExtremelyLongRunningTasks ? "Graceful shutdown" : "Going down")}..." @@ -618,10 +617,10 @@ namespace Tgstation.Server.Host.Components.Chat /// If the provider should be removed from and should be update. /// The for the operation. /// A resulting in the being removed if it exists, otherwise. - async ValueTask RemoveProviderChannels(long connectionId, bool removeProvider, CancellationToken cancellationToken) + async ValueTask RemoveProviderChannels(long connectionId, bool removeProvider, CancellationToken cancellationToken) { logger.LogTrace("RemoveProviderChannels {connectionId}...", connectionId); - IProvider provider; + IProvider? provider; lock (providers) { if (!providers.TryGetValue(connectionId, out provider)) @@ -663,7 +662,7 @@ namespace Tgstation.Server.Host.Components.Chat async ValueTask RemapProvider(IProvider provider, CancellationToken cancellationToken) { logger.LogTrace("Remapping channels for provider reconnection..."); - IEnumerable channelsToMap; + IEnumerable? channelsToMap; long providerId; lock (providers) providerId = providers.Where(x => x.Value == provider).Select(x => x.Key).First(); @@ -684,7 +683,7 @@ namespace Tgstation.Server.Host.Components.Chat /// The for the operation. /// A representing the running operation. #pragma warning disable CA1502 - async ValueTask ProcessMessage(IProvider provider, Message message, bool recursed, CancellationToken cancellationToken) + async ValueTask ProcessMessage(IProvider provider, Message? message, bool recursed, CancellationToken cancellationToken) #pragma warning restore CA1502 { if (!provider.Connected) @@ -765,11 +764,10 @@ namespace Tgstation.Server.Host.Components.Chat message.User.Channel.ConnectionName, message.User.FriendlyName, newId); - mappedChannels.Add(newId, new ChannelMapping + mappedChannels.Add(newId, new ChannelMapping(message.User.Channel) { ProviderChannelId = message.User.Channel.RealId, ProviderId = providerId, - Channel = message.User.Channel, }); logger.LogTrace( @@ -799,7 +797,7 @@ namespace Tgstation.Server.Host.Components.Chat var mappingChannelRepresentation = mappedChannel.Value.Value.Channel; - message.User.Channel.Id = mappingChannelRepresentation.Id; + message.User.Channel.RealId = mappingChannelRepresentation.RealId; message.User.Channel.Tag = mappingChannelRepresentation.Tag; message.User.Channel.IsAdminChannel = mappingChannelRepresentation.IsAdminChannel; } @@ -813,11 +811,9 @@ namespace Tgstation.Server.Host.Components.Chat if (address.Length > 1 && (address.Last() == ':' || address.Last() == ',')) address = address[0..^1]; - address = address.ToUpperInvariant(); - var addressed = - address == CommonMention.ToUpperInvariant() - || address == provider.BotMention.ToUpperInvariant(); + address.Equals(CommonMention, StringComparison.OrdinalIgnoreCase) + || address.Equals(provider.BotMention, StringComparison.OrdinalIgnoreCase); // no mention if (!addressed && !message.User.Channel.IsPrivateChannel) @@ -843,16 +839,16 @@ namespace Tgstation.Server.Host.Components.Chat splits.RemoveAt(0); var arguments = String.Join(" ", splits); - Tuple GetCommand() + Tuple? GetCommand() { if (!builtinCommands.TryGetValue(command, out var handler)) return trackingContexts .Where(trackingContext => trackingContext.Active) - .SelectMany(trackingContext => trackingContext.CustomCommands.Select(customCommand => Tuple.Create(customCommand, trackingContext))) + .SelectMany(trackingContext => trackingContext.CustomCommands.Select(customCommand => Tuple.Create(customCommand, trackingContext))) .Where(tuple => tuple.Item1.Name.Equals(command, StringComparison.OrdinalIgnoreCase)) .FirstOrDefault(); - return Tuple.Create(handler, null); + return Tuple.Create(handler, null); } const string UnknownCommandMessage = "TGS: Unknown command! Type '?' or 'help' for available commands."; @@ -935,11 +931,11 @@ namespace Tgstation.Server.Host.Components.Chat async Task MonitorMessages(CancellationToken cancellationToken) { logger.LogTrace("Starting processing loop..."); - var messageTasks = new Dictionary>(); + var messageTasks = new Dictionary>(); ValueTask activeProcessingTask = ValueTask.CompletedTask; try { - Task updatedTask = null; + Task? updatedTask = null; while (!cancellationToken.IsCancellationRequested) { if (updatedTask?.IsCompleted != false) @@ -1025,7 +1021,7 @@ namespace Tgstation.Server.Host.Components.Chat /// The to send. /// The for the operation. /// A representing the running operation. - ValueTask SendMessage(IEnumerable channelIds, Message replyTo, MessageContent message, CancellationToken cancellationToken) + ValueTask SendMessage(IEnumerable channelIds, Message? replyTo, MessageContent message, CancellationToken cancellationToken) { var channelIdsList = channelIds.ToList(); @@ -1035,17 +1031,17 @@ namespace Tgstation.Server.Host.Components.Chat message.Embed != null ? " (with embed)" : String.Empty, String.Join(", ", channelIdsList)); - if (!channelIdsList.Any()) + if (channelIdsList.Count == 0) return ValueTask.CompletedTask; return ValueTaskExtensions.WhenAll( channelIdsList.Select(x => { - ChannelMapping channelMapping; + ChannelMapping? channelMapping; lock (mappedChannels) if (!mappedChannels.TryGetValue(x, out channelMapping)) return ValueTask.CompletedTask; - IProvider provider; + IProvider? provider; lock (providers) if (!providers.TryGetValue(channelMapping.ProviderId, out provider)) return ValueTask.CompletedTask; @@ -1102,7 +1098,7 @@ namespace Tgstation.Server.Host.Components.Chat { var cancellationToken = handlerCts.Token; if (waitForConnections) - await initialProviderConnectionsTask.WaitAsync(cancellationToken); + await initialProviderConnectionsTask!.WaitAsync(cancellationToken); await SendMessage( channelIdsFactory(), diff --git a/src/Tgstation.Server.Host/Components/Chat/ChatManagerFactory.cs b/src/Tgstation.Server.Host/Components/Chat/ChatManagerFactory.cs index 12427da6be..a1697a9711 100644 --- a/src/Tgstation.Server.Host/Components/Chat/ChatManagerFactory.cs +++ b/src/Tgstation.Server.Host/Components/Chat/ChatManagerFactory.cs @@ -7,6 +7,7 @@ using Microsoft.Extensions.Logging; using Tgstation.Server.Host.Components.Chat.Commands; using Tgstation.Server.Host.Components.Chat.Providers; using Tgstation.Server.Host.Core; +using Tgstation.Server.Host.Models; namespace Tgstation.Server.Host.Components.Chat { @@ -54,6 +55,6 @@ namespace Tgstation.Server.Host.Components.Chat serverControl, loggerFactory, loggerFactory.CreateLogger(), - initialChatBots.Where(x => x.Enabled.Value)); + initialChatBots.Where(x => x.Require(y => y.Enabled))); } } diff --git a/src/Tgstation.Server.Host/Components/Chat/ChatTrackingContext.cs b/src/Tgstation.Server.Host/Components/Chat/ChatTrackingContext.cs index 03c9ee2e54..02b6675323 100644 --- a/src/Tgstation.Server.Host/Components/Chat/ChatTrackingContext.cs +++ b/src/Tgstation.Server.Host/Components/Chat/ChatTrackingContext.cs @@ -7,16 +7,17 @@ using System.Threading.Tasks; using Microsoft.Extensions.Logging; using Tgstation.Server.Host.Components.Chat.Commands; +using Tgstation.Server.Host.Utils; namespace Tgstation.Server.Host.Components.Chat { /// - sealed class ChatTrackingContext : IChatTrackingContext + sealed class ChatTrackingContext : DisposeInvoker, IChatTrackingContext { /// public bool Active { - get => active && onDispose != null; + get => active && !IsDisposed; set { if (active == value) @@ -61,25 +62,20 @@ namespace Tgstation.Server.Host.Components.Chat readonly ILogger logger; /// - /// for modifying , , and . + /// for modifying and calling . /// readonly object synchronizationLock; + /// + /// The if any. + /// + volatile IChannelSink? channelSink; + /// /// Backing field for . /// IReadOnlyCollection customCommands; - /// - /// The if any. - /// - IChannelSink channelSink; - - /// - /// The to run when d. - /// - Action onDispose; - /// /// Backing field for . /// @@ -91,45 +87,31 @@ namespace Tgstation.Server.Host.Components.Chat /// The value of . /// The initial value of . /// The value of . - /// The value of . + /// The action for the . public ChatTrackingContext( ICustomCommandHandler customCommandHandler, IEnumerable initialChannels, ILogger logger, - Action onDispose) + Action disposeAction) + : base(disposeAction) { this.customCommandHandler = customCommandHandler ?? throw new ArgumentNullException(nameof(customCommandHandler)); Channels = initialChannels?.ToList() ?? throw new ArgumentNullException(nameof(initialChannels)); this.logger = logger ?? throw new ArgumentNullException(nameof(logger)); - this.onDispose = onDispose ?? throw new ArgumentNullException(nameof(onDispose)); synchronizationLock = new object(); Active = true; customCommands = Array.Empty(); } - /// - public void Dispose() - { - lock (synchronizationLock) - { - onDispose?.Invoke(); - onDispose = null; - } - } - /// public void SetChannelSink(IChannelSink channelSink) { ArgumentNullException.ThrowIfNull(channelSink); - lock (synchronizationLock) - { - if (this.channelSink != null) - throw new InvalidOperationException("channelSink already set!"); - - this.channelSink = channelSink; - } + var originalValue = Interlocked.CompareExchange(ref this.channelSink, channelSink, null); + if (originalValue != null) + throw new InvalidOperationException("channelSink already set!"); } /// diff --git a/src/Tgstation.Server.Host/Components/Chat/ChatUser.cs b/src/Tgstation.Server.Host/Components/Chat/ChatUser.cs index 6de07109ea..69757279b0 100644 --- a/src/Tgstation.Server.Host/Components/Chat/ChatUser.cs +++ b/src/Tgstation.Server.Host/Components/Chat/ChatUser.cs @@ -13,7 +13,7 @@ namespace Tgstation.Server.Host.Components.Chat /// /// Backing field for . Represented as a to avoid BYOND percision loss. /// - public string Id { get; set; } + public string Id { get; private set; } /// /// The internal user id. @@ -21,23 +21,40 @@ namespace Tgstation.Server.Host.Components.Chat [JsonIgnore] public ulong RealId { - get => UInt64.Parse(Id, CultureInfo.InvariantCulture); - set => Id = value.ToString(CultureInfo.InvariantCulture); + get => UInt64.Parse(Id!, CultureInfo.InvariantCulture); + private set => Id = value.ToString(CultureInfo.InvariantCulture); } /// /// The friendly name of the user. /// - public string FriendlyName { get; set; } + public string FriendlyName { get; } /// /// The text to mention the user. /// - public string Mention { get; set; } + public string Mention { get; } /// /// The the user spoke from. /// - public ChannelRepresentation Channel { get; set; } + public ChannelRepresentation Channel { get; } + + /// + /// Initializes a new instance of the class. + /// + /// The value of . + /// The value of . + /// The value of . + /// The value of . + public ChatUser(ChannelRepresentation channel, string friendlyName, string mention, ulong realId) + { + Channel = channel ?? throw new ArgumentNullException(nameof(channel)); + FriendlyName = friendlyName ?? throw new ArgumentNullException(nameof(friendlyName)); + Mention = mention ?? throw new ArgumentNullException(nameof(mention)); + + Id = null!; + RealId = realId; + } } } diff --git a/src/Tgstation.Server.Host/Components/Chat/Commands/CommandFactory.cs b/src/Tgstation.Server.Host/Components/Chat/Commands/CommandFactory.cs index d4fda66603..a575ec9c87 100644 --- a/src/Tgstation.Server.Host/Components/Chat/Commands/CommandFactory.cs +++ b/src/Tgstation.Server.Host/Components/Chat/Commands/CommandFactory.cs @@ -46,7 +46,7 @@ namespace Tgstation.Server.Host.Components.Chat.Commands /// /// The for the . /// - IWatchdog watchdog; + IWatchdog? watchdog; /// /// Initializes a new instance of the class. diff --git a/src/Tgstation.Server.Host/Components/Chat/Commands/CustomCommand.cs b/src/Tgstation.Server.Host/Components/Chat/Commands/CustomCommand.cs index 617b1dba48..9492e925d7 100644 --- a/src/Tgstation.Server.Host/Components/Chat/Commands/CustomCommand.cs +++ b/src/Tgstation.Server.Host/Components/Chat/Commands/CustomCommand.cs @@ -12,18 +12,31 @@ namespace Tgstation.Server.Host.Components.Chat.Commands public sealed class CustomCommand : ICommand { /// - public string Name { get; set; } + public string Name { get; } /// - public string HelpText { get; set; } + public string HelpText { get; } /// - public bool AdminOnly { get; set; } + public bool AdminOnly { get; } /// /// The for the . /// - ICustomCommandHandler handler; + ICustomCommandHandler? handler; + + /// + /// Initializes a new instance of the class. + /// + /// The value of . + /// The value of . + /// The value of . + public CustomCommand(string name, string helpText, bool adminOnly) + { + Name = name ?? throw new ArgumentNullException(nameof(name)); + HelpText = helpText ?? throw new ArgumentNullException(nameof(helpText)); + AdminOnly = adminOnly; + } /// /// Set a new . diff --git a/src/Tgstation.Server.Host/Components/Chat/Commands/EngineCommand.cs b/src/Tgstation.Server.Host/Components/Chat/Commands/EngineCommand.cs index 9e92f3f02f..c0bf80ea59 100644 --- a/src/Tgstation.Server.Host/Components/Chat/Commands/EngineCommand.cs +++ b/src/Tgstation.Server.Host/Components/Chat/Commands/EngineCommand.cs @@ -48,7 +48,7 @@ namespace Tgstation.Server.Host.Components.Chat.Commands /// public ValueTask Invoke(string arguments, ChatUser user, CancellationToken cancellationToken) { - EngineVersion engineVersion; + EngineVersion? engineVersion; if (arguments.Split(' ').Any(x => x.Equals("--active", StringComparison.OrdinalIgnoreCase))) engineVersion = engineManager.ActiveVersion; else @@ -67,8 +67,7 @@ namespace Tgstation.Server.Host.Components.Chat.Commands Text = "None!", }); - if (!EngineVersion.TryParse(watchdog.ActiveCompileJob.EngineVersion, out engineVersion)) - throw new InvalidOperationException($"Invalid engine version: {watchdog.ActiveCompileJob.EngineVersion}"); + engineVersion = EngineVersion.Parse(watchdog.ActiveCompileJob.EngineVersion); } string text; @@ -76,10 +75,10 @@ namespace Tgstation.Server.Host.Components.Chat.Commands text = "None!"; else { - text = engineVersion.Engine.Value switch + text = engineVersion.Engine!.Value switch { EngineType.OpenDream => $"OpenDream: {engineVersion.SourceSHA}", - EngineType.Byond => $"BYOND {engineVersion.Version.Major}.{engineVersion.Version.Minor}", + EngineType.Byond => $"BYOND {engineVersion.Version!.Major}.{engineVersion.Version.Minor}", _ => throw new InvalidOperationException($"Invalid EngineType: {engineVersion.Engine.Value}"), }; diff --git a/src/Tgstation.Server.Host/Components/Chat/Commands/PullRequestsCommand.cs b/src/Tgstation.Server.Host/Components/Chat/Commands/PullRequestsCommand.cs index 39a99bcac4..3fd8c9e64b 100644 --- a/src/Tgstation.Server.Host/Components/Chat/Commands/PullRequestsCommand.cs +++ b/src/Tgstation.Server.Host/Components/Chat/Commands/PullRequestsCommand.cs @@ -81,7 +81,7 @@ namespace Tgstation.Server.Host.Components.Chat.Commands #pragma warning disable CA1506 public async ValueTask Invoke(string arguments, ChatUser user, CancellationToken cancellationToken) { - IEnumerable results = null; + IEnumerable results; var splits = arguments.Split(' '); var hasRepo = splits.Any(x => x.Equals("--repo", StringComparison.OrdinalIgnoreCase)); var hasStaged = splits.Any(x => x.Equals("--staged", StringComparison.OrdinalIgnoreCase)); @@ -112,12 +112,13 @@ namespace Tgstation.Server.Host.Components.Chat.Commands head = repo.Head; } + results = null!; await databaseContextFactory.UseContext( async db => results = await db .RevisionInformations .AsQueryable() - .Where(x => x.Instance.Id == instance.Id && x.CommitSha == head) - .SelectMany(x => x.ActiveTestMerges) + .Where(x => x.Instance!.Id == instance.Id && x.CommitSha == head) + .SelectMany(x => x.ActiveTestMerges!) .Select(x => x.TestMerge) .Select(x => new Models.TestMerge { @@ -143,7 +144,7 @@ namespace Tgstation.Server.Host.Components.Chat.Commands compileJobToUse = null; } - results = compileJobToUse?.RevisionInformation.ActiveTestMerges.Select(x => x.TestMerge).ToList() ?? Enumerable.Empty(); + results = compileJobToUse?.RevisionInformation.ActiveTestMerges?.Select(x => x.TestMerge).ToList() ?? Enumerable.Empty(); } return new MessageContent @@ -153,7 +154,7 @@ namespace Tgstation.Server.Host.Components.Chat.Commands : String.Join( ", ", results.Select( - x => $"#{x.Number} at {x.TargetCommitSha[..7]}")), + x => $"#{x.Number} at {x.TargetCommitSha![..7]}")), }; } #pragma warning restore CA1506 diff --git a/src/Tgstation.Server.Host/Components/Chat/Commands/RevisionCommand.cs b/src/Tgstation.Server.Host/Components/Chat/Commands/RevisionCommand.cs index aceff96435..39ed128d59 100644 --- a/src/Tgstation.Server.Host/Components/Chat/Commands/RevisionCommand.cs +++ b/src/Tgstation.Server.Host/Components/Chat/Commands/RevisionCommand.cs @@ -75,7 +75,7 @@ namespace Tgstation.Server.Host.Components.Chat.Commands { Text = "Server offline!", }; - result = watchdog.ActiveCompileJob?.RevisionInformation.OriginCommitSha; + result = watchdog.ActiveCompileJob?.RevisionInformation.OriginCommitSha!; } return new MessageContent diff --git a/src/Tgstation.Server.Host/Components/Chat/IChatManager.cs b/src/Tgstation.Server.Host/Components/Chat/IChatManager.cs index ae4aefb8fb..8a0aa33fe4 100644 --- a/src/Tgstation.Server.Host/Components/Chat/IChatManager.cs +++ b/src/Tgstation.Server.Host/Components/Chat/IChatManager.cs @@ -67,12 +67,12 @@ namespace Tgstation.Server.Host.Components.Chat /// The repository GitHub name, if any. /// if the local deployment commit was pushed to the remote repository. /// A to call to update the message at the deployment's conclusion. Parameters: Error message if any, DreamMaker output if any. Returns an to call to mark the deployment as active/inactive. Parameter: If the deployment is being activated or inactivated. - Func> QueueDeploymentMessage( + Func> QueueDeploymentMessage( Models.RevisionInformation revisionInformation, Api.Models.EngineVersion engineVersion, DateTimeOffset? estimatedCompletionTime, - string gitHubOwner, - string gitHubRepo, + string? gitHubOwner, + string? gitHubRepo, bool localCommitPushed); /// diff --git a/src/Tgstation.Server.Host/Components/Chat/Providers/DiscordMessage.cs b/src/Tgstation.Server.Host/Components/Chat/Providers/DiscordMessage.cs index b0b3fbb56e..565d78d654 100644 --- a/src/Tgstation.Server.Host/Components/Chat/Providers/DiscordMessage.cs +++ b/src/Tgstation.Server.Host/Components/Chat/Providers/DiscordMessage.cs @@ -12,5 +12,19 @@ namespace Tgstation.Server.Host.Components.Chat.Providers /// The of the source . /// public Optional MessageReference { get; set; } + + /// + /// Initializes a new instance of the class. + /// + /// The value of . + /// The value of . + /// The value of . + public DiscordMessage(ChatUser user, string content, Optional messageReference) + : base( + user, + content) + { + MessageReference = messageReference; + } } } diff --git a/src/Tgstation.Server.Host/Components/Chat/Providers/DiscordProvider.cs b/src/Tgstation.Server.Host/Components/Chat/Providers/DiscordProvider.cs index bda2228065..6f7ad59c5a 100644 --- a/src/Tgstation.Server.Host/Components/Chat/Providers/DiscordProvider.cs +++ b/src/Tgstation.Server.Host/Components/Chat/Providers/DiscordProvider.cs @@ -58,13 +58,13 @@ namespace Tgstation.Server.Host.Components.Chat.Providers /// /// The s supported by the for mapping. /// - static readonly ChannelType[] SupportedGuildChannelTypes = new[] - { + static readonly ChannelType[] SupportedGuildChannelTypes = + [ ChannelType.GuildText, ChannelType.GuildAnnouncement, ChannelType.PrivateThread, ChannelType.PublicThread, - }; + ]; /// /// The for the . @@ -104,17 +104,17 @@ namespace Tgstation.Server.Host.Components.Chat.Providers /// /// The for the . /// - CancellationTokenSource gatewayCts; + CancellationTokenSource? gatewayCts; /// /// The for the initial gateway connection event. /// - TaskCompletionSource gatewayReadyTcs; + TaskCompletionSource? gatewayReadyTcs; /// /// The representing the lifetime of the client. /// - Task gatewayTask; + Task? gatewayTask; /// /// The bot's . @@ -157,8 +157,8 @@ namespace Tgstation.Server.Host.Components.Chat.Providers mappedChannels = new List(); connectDisconnectLock = new object(); - var csb = new DiscordConnectionStringBuilder(chatBot.ConnectionString); - var botToken = csb.BotToken; + var csb = new DiscordConnectionStringBuilder(chatBot.ConnectionString!); + var botToken = csb.BotToken!; outputDisplayType = csb.DMOutputDisplay; deploymentBranding = csb.DeploymentBranding; @@ -194,7 +194,7 @@ namespace Tgstation.Server.Host.Components.Chat.Providers } /// - public override async ValueTask SendMessage(Message replyTo, MessageContent message, ulong channelId, CancellationToken cancellationToken) + public override async ValueTask SendMessage(Message? replyTo, MessageContent message, ulong channelId, CancellationToken cancellationToken) { ArgumentNullException.ThrowIfNull(message); @@ -218,6 +218,22 @@ namespace Tgstation.Server.Host.Components.Chat.Providers var channelsClient = serviceProvider.GetRequiredService(); async ValueTask SendToChannel(Snowflake channelId) { + if (message.Text == null) + { + Logger.LogWarning( + "Failed to send to channel {channelId}: Message was null!", + channelId); + + await channelsClient.CreateMessageAsync( + channelId, + "TGS: Could not send message to Discord. Message was `null`!", + messageReference: replyToReference, + allowedMentions: allowedMentions, + ct: cancellationToken); + + return; + } + var result = await channelsClient.CreateMessageAsync( channelId, message.Text, @@ -285,25 +301,23 @@ namespace Tgstation.Server.Host.Components.Chat.Providers } /// - public override async ValueTask>>> SendUpdateMessage( + public override async ValueTask>>> SendUpdateMessage( Models.RevisionInformation revisionInformation, EngineVersion engineVersion, DateTimeOffset? estimatedCompletionTime, - string gitHubOwner, - string gitHubRepo, + string? gitHubOwner, + string? gitHubRepo, ulong channelId, bool localCommitPushed, CancellationToken cancellationToken) { ArgumentNullException.ThrowIfNull(revisionInformation); ArgumentNullException.ThrowIfNull(engineVersion); - ArgumentNullException.ThrowIfNull(gitHubOwner); - ArgumentNullException.ThrowIfNull(gitHubRepo); localCommitPushed |= revisionInformation.CommitSha == revisionInformation.OriginCommitSha; var fields = BuildUpdateEmbedFields(revisionInformation, engineVersion, gitHubOwner, gitHubRepo, localCommitPushed); - var author = new EmbedAuthor(assemblyInformationProvider.VersionPrefix) + Optional author = new EmbedAuthor(assemblyInformationProvider.VersionPrefix) { Url = "https://github.com/tgstation/tgstation-server", IconUrl = "https://cdn.discordapp.com/attachments/1114451486374637629/1151650846019432448/tgs.png", @@ -323,7 +337,7 @@ namespace Tgstation.Server.Host.Components.Chat.Providers Logger.LogTrace("Attempting to post deploy embed to channel {channelId}...", channelId); var channelsClient = serviceProvider.GetRequiredService(); - var prefix = GetEngineCompilerPrefix(engineVersion.Engine.Value); + var prefix = GetEngineCompilerPrefix(engineVersion.Engine!.Value); var messageResponse = await channelsClient.CreateMessageAsync( new Snowflake(channelId), $"{prefix}: Deployment in progress...", @@ -386,7 +400,7 @@ namespace Tgstation.Server.Host.Components.Chat.Providers var updatedMessageText = errorMessage == null ? $"{prefix}: Deployment pending reboot..." : $"{prefix}: Deployment failed!"; - IMessage updatedMessage = null; + IMessage? updatedMessage = null; async ValueTask CreateUpdatedMessage() { var createUpdatedMessageResponse = await channelsClient.CreateMessageAsync( @@ -532,27 +546,23 @@ namespace Tgstation.Server.Host.Components.Chat.Providers messageGuildResponse.LogFormat()); } - var result = new DiscordMessage - { - MessageReference = messageReference, - Content = content, - User = new ChatUser - { - RealId = messageCreateEvent.Author.ID.Value, - Channel = new ChannelRepresentation + var result = new DiscordMessage( + new ChatUser( + new ChannelRepresentation( + pm ? messageCreateEvent.Author.Username : guildName, + channelResponse.Entity.Name.Value!, + messageCreateEvent.ChannelID.Value) { - RealId = messageCreateEvent.ChannelID.Value, IsPrivateChannel = pm, - ConnectionName = pm ? messageCreateEvent.Author.Username : guildName, - FriendlyName = channelResponse.Entity.Name.Value, EmbedsSupported = true, // isAdmin and Tag populated by manager }, - FriendlyName = messageCreateEvent.Author.Username, - Mention = NormalizeMentions($"<@{messageCreateEvent.Author.ID}>"), - }, - }; + messageCreateEvent.Author.Username, + NormalizeMentions($"<@{messageCreateEvent.Author.ID}>"), + messageCreateEvent.Author.ID.Value), + content, + messageReference); EnqueueMessage(result); return Result.FromSuccess(); @@ -635,8 +645,8 @@ namespace Tgstation.Server.Host.Components.Chat.Providers CancellationTokenSource localGatewayCts; lock (connectDisconnectLock) { - localGatewayTask = gatewayTask; - localGatewayCts = gatewayCts; + localGatewayTask = gatewayTask!; + localGatewayCts = gatewayCts!; gatewayTask = null; gatewayCts = null; if (localGatewayTask == null) @@ -662,7 +672,7 @@ namespace Tgstation.Server.Host.Components.Chat.Providers var guildsClient = serviceProvider.GetRequiredService(); var guildTasks = new ConcurrentDictionary>>(); - async ValueTask>> GetModelChannelFromDBChannel(Models.ChatChannel channelFromDB) + async ValueTask>?> GetModelChannelFromDBChannel(Models.ChatChannel channelFromDB) { if (!channelFromDB.DiscordChannelId.HasValue) throw new InvalidOperationException("ChatChannel missing DiscordChannelId!"); @@ -719,12 +729,12 @@ namespace Tgstation.Server.Host.Components.Chat.Providers var connectionName = guildsResponse.Entity.Name; - var channelModel = new ChannelRepresentation + var channelModel = new ChannelRepresentation( + guildsResponse.Entity.Name, + discordChannelResponse.Entity.Name.Value!, + channelId) { - RealId = channelId, IsAdminChannel = channelFromDB.IsAdminChannel == true, - ConnectionName = guildsResponse.Entity.Name, - FriendlyName = discordChannelResponse.Entity.Name.Value, IsPrivateChannel = false, Tag = channelFromDB.Tag, EmbedsSupported = true, @@ -742,8 +752,9 @@ namespace Tgstation.Server.Host.Components.Chat.Providers var channelTuples = await ValueTaskExtensions.WhenAll(tasks.ToList()); - var enumerator = channelTuples + var list = channelTuples .Where(x => x != null) + .Cast>>() // NRT my beloathed .ToList(); var channelIdZeroModel = channels.FirstOrDefault(x => x.DiscordChannelId == 0); @@ -752,7 +763,7 @@ namespace Tgstation.Server.Host.Components.Chat.Providers Logger.LogInformation("Mapping ALL additional accessible text channels"); var allAccessibleChannels = await GetAllAccessibleTextChannels(cancellationToken); var unmappedTextChannels = allAccessibleChannels - .Where(x => !tasks.Any(task => task.Result != null && new Snowflake(task.Result.Item1.DiscordChannelId.Value) == x.ID)); + .Where(x => !tasks.Any(task => task.Result != null && new Snowflake(task.Result.Item1.DiscordChannelId!.Value) == x.ID)); async ValueTask>> CreateMappingsForUnmappedChannels() { @@ -773,32 +784,36 @@ namespace Tgstation.Server.Host.Components.Chat.Providers .ToList(); // Add catch-all channel - unmappedTasks.Add(Task.FromResult( - new ChannelRepresentation + unmappedTasks.Add(Task.FromResult( + new ChannelRepresentation( + "(Unknown Discord Guilds)", + "(Unknown Discord Channels)", + 0) { - IsAdminChannel = channelIdZeroModel.IsAdminChannel.Value, - ConnectionName = "(Unknown Discord Guilds)", + IsAdminChannel = channelIdZeroModel.IsAdminChannel!.Value, EmbedsSupported = true, - FriendlyName = "(Unknown Discord Channels)", - RealId = 0, Tag = channelIdZeroModel.Tag, })); await Task.WhenAll(unmappedTasks); return Tuple.Create>( channelIdZeroModel, - unmappedTasks.Select(x => x.Result).Where(x => x != null).ToList()); + unmappedTasks + .Select(x => x.Result) + .Where(x => x != null) + .Cast() // NRT my beloathed + .ToList()); } var task = CreateMappingsForUnmappedChannels(); var tuple = await task; - enumerator.Add(tuple); + list.Add(tuple); } lock (mappedChannels) { mappedChannels.Clear(); - mappedChannels.AddRange(enumerator.SelectMany(x => x.Item2).Select(x => x.RealId)); + mappedChannels.AddRange(list.SelectMany(x => x.Item2).Select(x => x.RealId)); } if (remapRequired) @@ -807,7 +822,7 @@ namespace Tgstation.Server.Host.Components.Chat.Providers EnqueueMessage(null); } - return new Dictionary>(enumerator.Select(x => new KeyValuePair>(x.Item1, x.Item2))); + return new Dictionary>(list.Select(x => new KeyValuePair>(x.Item1, x.Item2))); } /// @@ -881,46 +896,55 @@ namespace Tgstation.Server.Host.Components.Chat.Providers List BuildUpdateEmbedFields( Models.RevisionInformation revisionInformation, EngineVersion engineVersion, - string gitHubOwner, - string gitHubRepo, + string? gitHubOwner, + string? gitHubRepo, bool localCommitPushed) { bool gitHub = gitHubOwner != null && gitHubRepo != null; - var engineField = engineVersion.Engine.Value switch + var engineField = engineVersion.Engine!.Value switch { EngineType.Byond => new EmbedField( "BYOND Version", - $"{engineVersion.Version.Major}.{engineVersion.Version.Minor}{(engineVersion.CustomIteration.HasValue ? $".{engineVersion.CustomIteration.Value}" : String.Empty)}", + $"{engineVersion.Version!.Major}.{engineVersion.Version.Minor}{(engineVersion.CustomIteration.HasValue ? $".{engineVersion.CustomIteration.Value}" : String.Empty)}", true), EngineType.OpenDream => new EmbedField( "OpenDream Version", - $"[{engineVersion.SourceSHA[..7]}]({generalConfiguration.OpenDreamGitUrl}/commit/{engineVersion.SourceSHA})", + $"[{engineVersion.SourceSHA![..7]}]({generalConfiguration.OpenDreamGitUrl}/commit/{engineVersion.SourceSHA})", true), _ => throw new InvalidOperationException($"Invaild EngineType: {engineVersion.Engine.Value}"), }; + var revisionSha = revisionInformation.CommitSha!; + var revisionOriginSha = revisionInformation.OriginCommitSha!; var fields = new List { engineField, + }; + + if (gitHubOwner == null || gitHubRepo == null) + return fields; + + fields.Add( new EmbedField( "Local Commit", localCommitPushed && gitHub - ? $"[{revisionInformation.CommitSha[..7]}](https://github.com/{gitHubOwner}/{gitHubRepo}/commit/{revisionInformation.CommitSha})" - : revisionInformation.CommitSha[..7], - true), + ? $"[{revisionSha[..7]}](https://github.com/{gitHubOwner}/{gitHubRepo}/commit/{revisionSha})" + : revisionSha[..7], + true)); + + fields.Add( new EmbedField( "Branch Commit", gitHub - ? $"[{revisionInformation.OriginCommitSha[..7]}](https://github.com/{gitHubOwner}/{gitHubRepo}/commit/{revisionInformation.OriginCommitSha})" - : revisionInformation.OriginCommitSha[..7], - true), - }; + ? $"[{revisionOriginSha[..7]}](https://github.com/{gitHubOwner}/{gitHubRepo}/commit/{revisionOriginSha})" + : revisionOriginSha[..7], + true)); fields.AddRange((revisionInformation.ActiveTestMerges ?? Enumerable.Empty()) .Select(x => x.TestMerge) .Select(x => new EmbedField( $"#{x.Number}", - $"[{x.TitleAtMerge}]({x.Url}) by _[@{x.Author}](https://github.com/{x.Author})_{Environment.NewLine}Commit: [{x.TargetCommitSha[..7]}](https://github.com/{gitHubOwner}/{gitHubRepo}/commit/{x.TargetCommitSha}){(String.IsNullOrWhiteSpace(x.Comment) ? String.Empty : $"{Environment.NewLine}_**{x.Comment}**_")}", + $"[{x.TitleAtMerge}]({x.Url}) by _[@{x.Author}](https://github.com/{x.Author})_{Environment.NewLine}Commit: [{x.TargetCommitSha![..7]}](https://github.com/{gitHubOwner}/{gitHubRepo}/commit/{x.TargetCommitSha}){(String.IsNullOrWhiteSpace(x.Comment) ? String.Empty : $"{Environment.NewLine}_**{x.Comment}**_")}", false))); return fields; @@ -932,7 +956,7 @@ namespace Tgstation.Server.Host.Components.Chat.Providers /// The to convert. /// The parameter for sending a single . #pragma warning disable CA1502 - Optional> ConvertEmbed(ChatEmbed embed) + Optional> ConvertEmbed(ChatEmbed? embed) { if (embed == null) return default; @@ -955,7 +979,7 @@ namespace Tgstation.Server.Host.Components.Chat.Providers embed.Author = null; } - List fields = null; + List? fields = null; if (embed.Fields != null) { fields = new List(); @@ -987,7 +1011,7 @@ namespace Tgstation.Server.Host.Components.Chat.Providers if (invalid) continue; - fields.Add(new EmbedField(field.Name, field.Value) + fields.Add(new EmbedField(field.Name!, field.Value!) { IsInline = field.IsInline ?? default(Optional), }); @@ -1026,7 +1050,7 @@ namespace Tgstation.Server.Host.Components.Chat.Providers var discordEmbed = new Embed { Author = embed.Author != null - ? new EmbedAuthor(embed.Author.Name) + ? new EmbedAuthor(embed.Author.Name!) { IconUrl = embed.Author.IconUrl ?? default(Optional), ProxyIconUrl = embed.Author.ProxyIconUrl ?? default(Optional), @@ -1037,14 +1061,14 @@ namespace Tgstation.Server.Host.Components.Chat.Providers Description = embed.Description ?? default(Optional), Fields = fields ?? default(Optional>), Footer = embed.Footer != null - ? new EmbedFooter(embed.Footer.Text) + ? (Optional)new EmbedFooter(embed.Footer.Text!) { IconUrl = embed.Footer.IconUrl ?? default(Optional), ProxyIconUrl = embed.Footer.ProxyIconUrl ?? default(Optional), } : default, Image = embed.Image != null - ? new EmbedImage(embed.Image.Url) + ? new EmbedImage(embed.Image.Url!) { Width = embed.Image.Width ?? default(Optional), Height = embed.Image.Height ?? default(Optional), @@ -1059,7 +1083,7 @@ namespace Tgstation.Server.Host.Components.Chat.Providers } : default(Optional), Thumbnail = embed.Thumbnail != null - ? new EmbedThumbnail(embed.Thumbnail.Url) + ? new EmbedThumbnail(embed.Thumbnail.Url!) { Width = embed.Thumbnail.Width ?? default(Optional), Height = embed.Thumbnail.Height ?? default(Optional), diff --git a/src/Tgstation.Server.Host/Components/Chat/Providers/IProvider.cs b/src/Tgstation.Server.Host/Components/Chat/Providers/IProvider.cs index 22028aefc7..845892d9e0 100644 --- a/src/Tgstation.Server.Host/Components/Chat/Providers/IProvider.cs +++ b/src/Tgstation.Server.Host/Components/Chat/Providers/IProvider.cs @@ -45,7 +45,7 @@ namespace Tgstation.Server.Host.Components.Chat.Providers /// The for the operation. /// A resulting in the next available or if the needed to reconnect. /// Note that private messages will come in the form of s not returned in . - Task NextMessage(CancellationToken cancellationToken); + Task NextMessage(CancellationToken cancellationToken); /// /// Gracefully disconnects the provider. Permanently stops the reconnection timer. @@ -65,12 +65,12 @@ namespace Tgstation.Server.Host.Components.Chat.Providers /// /// Send a message to the . /// - /// The to reply to. + /// The optional to reply to. /// The . /// The to send to. /// The for the operation. /// A representing the running operation. - ValueTask SendMessage(Message replyTo, MessageContent message, ulong channelId, CancellationToken cancellationToken); + ValueTask SendMessage(Message? replyTo, MessageContent message, ulong channelId, CancellationToken cancellationToken); /// /// Set the interval at which the provider starts jobs to try to reconnect. @@ -92,12 +92,12 @@ namespace Tgstation.Server.Host.Components.Chat.Providers /// if the local deployment commit was pushed to the remote repository. /// The for the operation. /// A resulting in a to call to update the message at the deployment's conclusion. Parameters: Error message if any, DreamMaker output if any. Returns another callback which should be called to mark the deployment as active. - ValueTask>>> SendUpdateMessage( + ValueTask>>> SendUpdateMessage( Models.RevisionInformation revisionInformation, Api.Models.EngineVersion engineVersion, DateTimeOffset? estimatedCompletionTime, - string gitHubOwner, - string gitHubRepo, + string? gitHubOwner, + string? gitHubRepo, ulong channelId, bool localCommitPushed, CancellationToken cancellationToken); diff --git a/src/Tgstation.Server.Host/Components/Chat/Providers/IrcProvider.cs b/src/Tgstation.Server.Host/Components/Chat/Providers/IrcProvider.cs index e4fe37a93e..ad83a80b18 100644 --- a/src/Tgstation.Server.Host/Components/Chat/Providers/IrcProvider.cs +++ b/src/Tgstation.Server.Host/Components/Chat/Providers/IrcProvider.cs @@ -75,23 +75,23 @@ namespace Tgstation.Server.Host.Components.Chat.Providers /// /// Map of s to channel names. /// - readonly Dictionary channelIdMap; + readonly Dictionary channelIdMap; /// /// Map of s to query users. /// readonly Dictionary queryChannelIdMap; + /// + /// The used for . + /// + Task? listenTask; + /// /// Id counter for . /// ulong channelIdCounter; - /// - /// The used for . - /// - Task listenTask; - /// /// If we are disconnecting. /// @@ -119,11 +119,11 @@ namespace Tgstation.Server.Host.Components.Chat.Providers if (builder == null || !builder.Valid || builder is not IrcConnectionStringBuilder ircBuilder) throw new InvalidOperationException("Invalid ChatConnectionStringBuilder!"); - address = ircBuilder.Address; - port = ircBuilder.Port.Value; - nickname = ircBuilder.Nickname; + address = ircBuilder.Address!; + port = ircBuilder.Port!.Value; + nickname = ircBuilder.Nickname!; - password = ircBuilder.Password; + password = ircBuilder.Password!; passwordType = ircBuilder.PasswordType; client = new IrcFeatures @@ -138,7 +138,7 @@ namespace Tgstation.Server.Host.Components.Chat.Providers ActiveChannelSyncing = true, AutoNickHandling = true, CtcpVersion = assemblyInformationProvider.VersionString, - UseSsl = ircBuilder.UseSsl.Value, + UseSsl = ircBuilder.UseSsl!.Value, }; if (ircBuilder.UseSsl.Value) client.ValidateServerCertificate = true; // dunno if it defaults to that or what @@ -149,7 +149,7 @@ namespace Tgstation.Server.Host.Components.Chat.Providers /*client.OnReadLine += (sender, e) => Logger.LogTrace("READ: {line}", e.Line); client.OnWriteLine += (sender, e) => Logger.LogTrace("WRITE: {line}", e.Line);*/ - channelIdMap = new Dictionary(); + channelIdMap = new Dictionary(); queryChannelIdMap = new Dictionary(); channelIdCounter = 1; } @@ -164,7 +164,7 @@ namespace Tgstation.Server.Host.Components.Chat.Providers } /// - public override async ValueTask SendMessage(Message replyTo, MessageContent message, ulong channelId, CancellationToken cancellationToken) + public override async ValueTask SendMessage(Message? replyTo, MessageContent message, ulong channelId, CancellationToken cancellationToken) { ArgumentNullException.ThrowIfNull(message); @@ -218,12 +218,12 @@ namespace Tgstation.Server.Host.Components.Chat.Providers } /// - public override async ValueTask>>> SendUpdateMessage( + public override async ValueTask>>> SendUpdateMessage( Models.RevisionInformation revisionInformation, EngineVersion engineVersion, DateTimeOffset? estimatedCompletionTime, - string gitHubOwner, - string gitHubRepo, + string? gitHubOwner, + string? gitHubRepo, ulong channelId, bool localCommitPushed, CancellationToken cancellationToken) @@ -233,7 +233,7 @@ namespace Tgstation.Server.Host.Components.Chat.Providers ArgumentNullException.ThrowIfNull(gitHubOwner); ArgumentNullException.ThrowIfNull(gitHubRepo); - var commitInsert = revisionInformation.CommitSha[..7]; + var commitInsert = revisionInformation.CommitSha![..7]; string remoteCommitInsert; if (revisionInformation.CommitSha == revisionInformation.OriginCommitSha) { @@ -241,7 +241,7 @@ namespace Tgstation.Server.Host.Components.Chat.Providers remoteCommitInsert = String.Empty; } else - remoteCommitInsert = String.Format(CultureInfo.InvariantCulture, ". Remote commit: ^{0}", revisionInformation.OriginCommitSha[..7]); + remoteCommitInsert = String.Format(CultureInfo.InvariantCulture, ". Remote commit: ^{0}", revisionInformation.OriginCommitSha![..7]); var testmergeInsert = (revisionInformation.ActiveTestMerges?.Count ?? 0) == 0 ? String.Empty @@ -251,17 +251,17 @@ namespace Tgstation.Server.Host.Components.Chat.Providers String.Join( ", ", revisionInformation - .ActiveTestMerges + .ActiveTestMerges! .Select(x => x.TestMerge) .Select(x => { - var result = String.Format(CultureInfo.InvariantCulture, "#{0} at {1}", x.Number, x.TargetCommitSha[..7]); + var result = String.Format(CultureInfo.InvariantCulture, "#{0} at {1}", x.Number, x.TargetCommitSha![..7]); if (x.Comment != null) result += String.Format(CultureInfo.InvariantCulture, " ({0})", x.Comment); return result; }))); - var prefix = GetEngineCompilerPrefix(engineVersion.Engine.Value); + var prefix = GetEngineCompilerPrefix(engineVersion.Engine!.Value); await SendMessage( null, new MessageContent @@ -351,14 +351,11 @@ namespace Tgstation.Server.Host.Components.Chat.Providers dbChannel, new List { - new() + new(address, channelName, id!.Value) { - RealId = id.Value, - IsAdminChannel = dbChannel.IsAdminChannel == true, - ConnectionName = address, - FriendlyName = channelIdMap[id.Value], - IsPrivateChannel = false, Tag = dbChannel.Tag, + IsAdminChannel = dbChannel.IsAdminChannel == true, + IsPrivateChannel = false, EmbedsSupported = false, }, }); @@ -503,7 +500,7 @@ namespace Tgstation.Server.Host.Components.Chat.Providers var username = e.Data.Nick; var channelName = isPrivate ? username : e.Data.Channel; - ulong MapAndGetChannelId(Dictionary dicToCheck) + ulong MapAndGetChannelId(Dictionary dicToCheck) { ulong? resultId = null; if (!dicToCheck.Any(x => @@ -520,36 +517,31 @@ namespace Tgstation.Server.Host.Components.Chat.Providers channelIdMap.Add(resultId.Value, null); } - return resultId.Value; + return resultId!.Value; } ulong userId, channelId; lock (client) { - userId = MapAndGetChannelId(queryChannelIdMap); + userId = MapAndGetChannelId(new Dictionary(queryChannelIdMap + .Cast>())); // NRT my beloathed channelId = isPrivate ? userId : MapAndGetChannelId(channelIdMap); } - var message = new Message - { - Content = e.Data.Message, - User = new ChatUser - { - Channel = new ChannelRepresentation + var channelFriendlyName = isPrivate ? String.Format(CultureInfo.InvariantCulture, "PM: {0}", channelName) : channelName; + var message = new Message( + new ChatUser( + new ChannelRepresentation(address, channelFriendlyName, channelId) { - ConnectionName = address, - FriendlyName = isPrivate ? String.Format(CultureInfo.InvariantCulture, "PM: {0}", channelName) : channelName, - RealId = channelId, IsPrivateChannel = isPrivate, EmbedsSupported = false, // isAdmin and Tag populated by manager }, - FriendlyName = username, - RealId = userId, - Mention = username, - }, - }; + username, + username, + userId), + e.Data.Message); EnqueueMessage(message); } diff --git a/src/Tgstation.Server.Host/Components/Chat/Providers/Message.cs b/src/Tgstation.Server.Host/Components/Chat/Providers/Message.cs index 9510113f62..c9084657fc 100644 --- a/src/Tgstation.Server.Host/Components/Chat/Providers/Message.cs +++ b/src/Tgstation.Server.Host/Components/Chat/Providers/Message.cs @@ -1,4 +1,6 @@ -namespace Tgstation.Server.Host.Components.Chat.Providers +using System; + +namespace Tgstation.Server.Host.Components.Chat.Providers { /// /// Represents a message received by a . @@ -8,11 +10,22 @@ /// /// The text of the message. /// - public string Content { get; set; } + public string Content { get; } /// /// The who sent the . /// - public ChatUser User { get; set; } + public ChatUser User { get; } + + /// + /// Initializes a new instance of the class. + /// + /// The value of . + /// The value of . + public Message(ChatUser user, string content) + { + User = user ?? throw new ArgumentNullException(nameof(user)); + Content = content ?? throw new ArgumentNullException(nameof(content)); + } } } diff --git a/src/Tgstation.Server.Host/Components/Chat/Providers/Provider.cs b/src/Tgstation.Server.Host/Components/Chat/Providers/Provider.cs index 7da3b29930..6be6b71521 100644 --- a/src/Tgstation.Server.Host/Components/Chat/Providers/Provider.cs +++ b/src/Tgstation.Server.Host/Components/Chat/Providers/Provider.cs @@ -42,7 +42,7 @@ namespace Tgstation.Server.Host.Components.Chat.Providers /// /// of received s. /// - readonly Queue messageQueue; + readonly Queue messageQueue; /// /// The backing for . @@ -62,12 +62,12 @@ namespace Tgstation.Server.Host.Components.Chat.Providers /// /// The auto reconnect . /// - Task reconnectTask; + Task? reconnectTask; /// /// for . /// - CancellationTokenSource reconnectCts; + CancellationTokenSource? reconnectCts; /// /// Get the prefix for messages about deployments. @@ -96,7 +96,10 @@ namespace Tgstation.Server.Host.Components.Chat.Providers Logger = logger ?? throw new ArgumentNullException(nameof(logger)); ChatBot = chatBot ?? throw new ArgumentNullException(nameof(chatBot)); - messageQueue = new Queue(); + if (chatBot.Instance == null) + throw new ArgumentException("chatBot must have Instance!", nameof(chatBot)); + + messageQueue = new Queue(); nextMessage = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously); initialConnectionTcs = new TaskCompletionSource(); reconnectTaskLock = new object(); @@ -157,7 +160,7 @@ namespace Tgstation.Server.Host.Components.Chat.Providers } /// - public async Task NextMessage(CancellationToken cancellationToken) + public async Task NextMessage(CancellationToken cancellationToken) { while (true) { @@ -191,15 +194,15 @@ namespace Tgstation.Server.Host.Components.Chat.Providers } /// - public abstract ValueTask SendMessage(Message replyTo, MessageContent message, ulong channelId, CancellationToken cancellationToken); + public abstract ValueTask SendMessage(Message? replyTo, MessageContent message, ulong channelId, CancellationToken cancellationToken); /// - public abstract ValueTask>>> SendUpdateMessage( + public abstract ValueTask>>> SendUpdateMessage( RevisionInformation revisionInformation, Api.Models.EngineVersion engineVersion, DateTimeOffset? estimatedCompletionTime, - string gitHubOwner, - string gitHubRepo, + string? gitHubOwner, + string? gitHubRepo, ulong channelId, bool localCommitPushed, CancellationToken cancellationToken); @@ -232,7 +235,7 @@ namespace Tgstation.Server.Host.Components.Chat.Providers /// Queues a for . /// /// The to queue. A value of indicates the channel mappings are out of date. - protected void EnqueueMessage(Message message) + protected void EnqueueMessage(Message? message) { if (message == null) Logger.LogTrace("Requesting channel remap..."); @@ -257,7 +260,7 @@ namespace Tgstation.Server.Host.Components.Chat.Providers reconnectCts.Cancel(); reconnectCts.Dispose(); reconnectCts = null; - var reconnectTask = this.reconnectTask; + var reconnectTask = this.reconnectTask!; this.reconnectTask = null; return reconnectTask; } @@ -286,7 +289,7 @@ namespace Tgstation.Server.Host.Components.Chat.Providers connectNow = false; if (!Connected) { - var job = Job.Create(Api.Models.JobCode.ReconnectChatBot, null, ChatBot.Instance, ChatBotRights.WriteEnabled); + var job = Job.Create(Api.Models.JobCode.ReconnectChatBot, null, ChatBot.Instance!, ChatBotRights.WriteEnabled); job.Description += $": {ChatBot.Name}"; await jobManager.RegisterOperation( diff --git a/src/Tgstation.Server.Host/Components/Deployment/DmbFactory.cs b/src/Tgstation.Server.Host/Components/Deployment/DmbFactory.cs index de36092c6b..3b6695313b 100644 --- a/src/Tgstation.Server.Host/Components/Deployment/DmbFactory.cs +++ b/src/Tgstation.Server.Host/Components/Deployment/DmbFactory.cs @@ -1,5 +1,6 @@ using System; using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; using System.IO; using System.Linq; using System.Threading; @@ -15,6 +16,7 @@ using Tgstation.Server.Host.Components.Events; using Tgstation.Server.Host.Database; using Tgstation.Server.Host.IO; using Tgstation.Server.Host.Models; +using Tgstation.Server.Host.Utils; namespace Tgstation.Server.Host.Components.Deployment { @@ -34,6 +36,7 @@ namespace Tgstation.Server.Host.Components.Deployment } /// + [MemberNotNullWhen(true, nameof(nextDmbProvider))] public bool DmbAvailable => nextDmbProvider != null; /// @@ -74,7 +77,7 @@ namespace Tgstation.Server.Host.Components.Deployment /// /// Map of s to locks on them. /// - readonly IDictionary jobLockCounts; + readonly Dictionary jobLockCounts; /// /// resulting in the latest yet to exist. @@ -89,7 +92,7 @@ namespace Tgstation.Server.Host.Components.Deployment /// /// The latest . /// - IDmbProvider nextDmbProvider; + IDmbProvider? nextDmbProvider; /// /// If the is "started" via . @@ -130,7 +133,7 @@ namespace Tgstation.Server.Host.Components.Deployment public void Dispose() => cleanupCts.Dispose(); // we don't dispose nextDmbProvider here, since it might be the only thing we have /// - public async ValueTask LoadCompileJob(CompileJob job, Action activationAction, CancellationToken cancellationToken) + public async ValueTask LoadCompileJob(CompileJob job, Action? activationAction, CancellationToken cancellationToken) { ArgumentNullException.ThrowIfNull(job); @@ -173,8 +176,8 @@ namespace Tgstation.Server.Host.Components.Deployment throw new ArgumentOutOfRangeException(nameof(lockCount), lockCount, "lockCount must be greater than or equal to 0!"); lock (jobLockCounts) { - var jobId = nextDmbProvider.CompileJob.Id; - var incremented = jobLockCounts[jobId.Value] += lockCount; + var jobId = nextDmbProvider.CompileJob.Require(x => x.Id); + var incremented = jobLockCounts[jobId] += lockCount; logger.LogTrace("Compile job {jobId} lock count now: {lockCount}", jobId, incremented); return nextDmbProvider; } @@ -183,16 +186,15 @@ namespace Tgstation.Server.Host.Components.Deployment /// public async Task StartAsync(CancellationToken cancellationToken) { - CompileJob cj = null; - await databaseContextFactory.UseContext(async (db) => - { - cj = await db - .CompileJobs - .AsQueryable() - .Where(x => x.Job.Instance.Id == metadata.Id) - .OrderByDescending(x => x.Job.StoppedAt) - .FirstOrDefaultAsync(cancellationToken); - }); + CompileJob? cj = null; + await databaseContextFactory.UseContext( + async (db) => + cj = await db + .CompileJobs + .AsQueryable() + .Where(x => x.Job.Instance!.Id == metadata.Id) + .OrderByDescending(x => x.Job.StoppedAt) + .FirstOrDefaultAsync(cancellationToken)); try { @@ -227,35 +229,39 @@ namespace Tgstation.Server.Host.Components.Deployment /// #pragma warning disable CA1506 // TODO: Decomplexify - public async ValueTask FromCompileJob(CompileJob compileJob, CancellationToken cancellationToken) + public async ValueTask FromCompileJob(CompileJob compileJob, CancellationToken cancellationToken) { ArgumentNullException.ThrowIfNull(compileJob); // ensure we have the entire metadata tree - logger.LogTrace("Loading compile job {id}...", compileJob.Id); + var compileJobId = compileJob.Require(x => x.Id); + logger.LogTrace("Loading compile job {id}...", compileJobId); await databaseContextFactory.UseContext( async db => compileJob = await db .CompileJobs .AsQueryable() - .Where(x => x.Id == compileJob.Id) - .Include(x => x.Job) + .Where(x => x!.Id == compileJobId) + .Include(x => x.Job!) .ThenInclude(x => x.StartedBy) - .Include(x => x.Job) + .Include(x => x.Job!) .ThenInclude(x => x.Instance) - .Include(x => x.RevisionInformation) - .ThenInclude(x => x.PrimaryTestMerge) - .ThenInclude(x => x.MergedBy) - .Include(x => x.RevisionInformation) - .ThenInclude(x => x.ActiveTestMerges) - .ThenInclude(x => x.TestMerge) - .ThenInclude(x => x.MergedBy) + .Include(x => x.RevisionInformation!) + .ThenInclude(x => x.PrimaryTestMerge!) + .ThenInclude(x => x.MergedBy) + .Include(x => x.RevisionInformation!) + .ThenInclude(x => x.ActiveTestMerges!) + .ThenInclude(x => x.TestMerge!) + .ThenInclude(x => x.MergedBy) .FirstAsync(cancellationToken)); // can't wait to see that query - if (!EngineVersion.TryParse(compileJob.EngineVersion, out var engineVersion)) + EngineVersion engineVersion; + if (!EngineVersion.TryParse(compileJob.EngineVersion, out var engineVersionNullable)) { - logger.LogWarning("Error loading compile job, bad engine version: {0}", compileJob.EngineVersion); + logger.LogWarning("Error loading compile job, bad engine version: {engineVersion}", compileJob.EngineVersion); return null; // omae wa mou shinderu } + else + engineVersion = engineVersionNullable!; if (!compileJob.Job.StoppedAt.HasValue) { @@ -274,7 +280,7 @@ namespace Tgstation.Server.Host.Components.Deployment CleanRegisteredCompileJob(compileJob); } - var newProvider = new DmbProvider(compileJob, engineVersion, ioManager, CleanupAction); + var newProvider = new DmbProvider(compileJob, engineVersion, ioManager, new DisposeInvoker(CleanupAction)); try { const string LegacyADirectoryName = "A"; @@ -311,22 +317,22 @@ namespace Tgstation.Server.Host.Components.Deployment // rebuild the provider because it's using the legacy style directories // Don't dispose it logger.LogDebug("Creating legacy two folder .dmb provider targeting {aDirName} directory...", LegacyADirectoryName); - newProvider = new DmbProvider(compileJob, engineVersion, ioManager, CleanupAction, Path.DirectorySeparatorChar + LegacyADirectoryName); + newProvider = new DmbProvider(compileJob, engineVersion, ioManager, new DisposeInvoker(CleanupAction), Path.DirectorySeparatorChar + LegacyADirectoryName); } lock (jobLockCounts) { - if (!jobLockCounts.TryGetValue(compileJob.Id.Value, out int value)) + if (!jobLockCounts.TryGetValue(compileJobId, out int value)) { value = 1; - jobLockCounts.Add(compileJob.Id.Value, 1); + jobLockCounts.Add(compileJobId, 1); } else - jobLockCounts[compileJob.Id.Value] = ++value; + jobLockCounts[compileJobId] = ++value; providerSubmitted = true; - logger.LogTrace("Compile job {id} lock count now: {lockCount}", compileJob.Id, value); + logger.LogTrace("Compile job {id} lock count now: {lockCount}", compileJobId, value); return newProvider; } } @@ -348,10 +354,10 @@ namespace Tgstation.Server.Host.Components.Deployment lock (jobLockCounts) jobIdsToSkip = jobLockCounts.Keys.ToList(); - List jobUidsToNotErase = null; + List? jobUidsToNotErase = null; // find the uids of locked directories - if (jobIdsToSkip.Any()) + if (jobIdsToSkip.Count > 0) { await databaseContextFactory.UseContext(async db => { @@ -359,9 +365,9 @@ namespace Tgstation.Server.Host.Components.Deployment .CompileJobs .AsQueryable() .Where( - x => x.Job.Instance.Id == metadata.Id - && jobIdsToSkip.Contains(x.Id.Value)) - .Select(x => x.DirectoryName.Value) + x => x.Job.Instance!.Id == metadata.Id + && jobIdsToSkip.Contains(x.Id!.Value)) + .Select(x => x.DirectoryName!.Value) .ToListAsync(cancellationToken)) .Select(x => x.ToString()) .ToList(); @@ -370,7 +376,7 @@ namespace Tgstation.Server.Host.Components.Deployment else jobUidsToNotErase = new List(); - jobUidsToNotErase.Add(SwappableDmbProvider.LiveGameDirectory); + jobUidsToNotErase!.Add(SwappableDmbProvider.LiveGameDirectory); logger.LogTrace("We will not clean the following directories: {directoriesToNotClean}", String.Join(", ", jobUidsToNotErase)); @@ -405,7 +411,7 @@ namespace Tgstation.Server.Host.Components.Deployment #pragma warning restore CA1506 /// - public CompileJob LatestCompileJob() + public CompileJob? LatestCompileJob() { if (!DmbAvailable) return null; @@ -426,7 +432,7 @@ namespace Tgstation.Server.Host.Components.Deployment // DCT: None available var deploymentJob = remoteDeploymentManager.MarkInactive(job, CancellationToken.None); - var deleteTask = DeleteCompileJobContent(job.DirectoryName.ToString(), cleanupCts.Token); + var deleteTask = DeleteCompileJobContent(job.DirectoryName!.Value.ToString(), cleanupCts.Token); var otherTask = cleanupTask; async Task WrapThrowableTasks() @@ -445,20 +451,23 @@ namespace Tgstation.Server.Host.Components.Deployment } lock (jobLockCounts) - if (jobLockCounts.TryGetValue(job.Id.Value, out var currentVal)) + { + var jobId = job.Require(x => x.Id); + if (jobLockCounts.TryGetValue(jobId, out var currentVal)) if (currentVal == 1) { - jobLockCounts.Remove(job.Id.Value); - logger.LogDebug("Cleaning lock-free compile job {id} => {dirName}", job.Id, job.DirectoryName); + jobLockCounts.Remove(jobId); + logger.LogDebug("Cleaning lock-free compile job {id} => {dirName}", jobId, job.DirectoryName); cleanupTask = HandleCleanup(); } else { - var decremented = --jobLockCounts[job.Id.Value]; - logger.LogTrace("Compile job {id} lock count now: {lockCount}", job.Id, decremented); + var decremented = --jobLockCounts[jobId]; + logger.LogTrace("Compile job {id} lock count now: {lockCount}", jobId, decremented); } else - logger.LogError("Extra Dispose of DmbProvider for CompileJob {compileJobId}!", job.Id); + logger.LogError("Extra Dispose of DmbProvider for CompileJob {compileJobId}!", jobId); + } } /// diff --git a/src/Tgstation.Server.Host/Components/Deployment/DmbProvider.cs b/src/Tgstation.Server.Host/Components/Deployment/DmbProvider.cs index 37c44d9fab..be47aeb6b3 100644 --- a/src/Tgstation.Server.Host/Components/Deployment/DmbProvider.cs +++ b/src/Tgstation.Server.Host/Components/Deployment/DmbProvider.cs @@ -4,6 +4,7 @@ using System.Threading.Tasks; using Tgstation.Server.Api.Models; using Tgstation.Server.Api.Models.Internal; using Tgstation.Server.Host.IO; +using Tgstation.Server.Host.Utils; namespace Tgstation.Server.Host.Components.Deployment { @@ -11,7 +12,7 @@ namespace Tgstation.Server.Host.Components.Deployment sealed class DmbProvider : DmbProviderBase, IDmbProvider { /// - public override string Directory => ioManager.ResolvePath(CompileJob.DirectoryName.ToString() + directoryAppend); + public override string Directory => ioManager.ResolvePath(CompileJob.DirectoryName!.Value.ToString() + directoryAppend); /// public override Models.CompileJob CompileJob { get; } @@ -32,7 +33,7 @@ namespace Tgstation.Server.Host.Components.Deployment /// /// The to run when is called. /// - Action onDispose; + DisposeInvoker? onDispose; /// /// Initializes a new instance of the class. @@ -42,7 +43,7 @@ namespace Tgstation.Server.Host.Components.Deployment /// The value of . /// The value of . /// The optional value of . - public DmbProvider(Models.CompileJob compileJob, EngineVersion engineVersion, IIOManager ioManager, Action onDispose, string directoryAppend = null) + public DmbProvider(Models.CompileJob compileJob, EngineVersion engineVersion, IIOManager ioManager, DisposeInvoker onDispose, string? directoryAppend = null) { CompileJob = compileJob ?? throw new ArgumentNullException(nameof(compileJob)); EngineVersion = engineVersion ?? throw new ArgumentNullException(nameof(engineVersion)); @@ -54,7 +55,7 @@ namespace Tgstation.Server.Host.Components.Deployment /// public override ValueTask DisposeAsync() { - onDispose?.Invoke(); + onDispose?.Dispose(); return ValueTask.CompletedTask; } diff --git a/src/Tgstation.Server.Host/Components/Deployment/DmbProviderBase.cs b/src/Tgstation.Server.Host/Components/Deployment/DmbProviderBase.cs index 5f1248463d..5929b74167 100644 --- a/src/Tgstation.Server.Host/Components/Deployment/DmbProviderBase.cs +++ b/src/Tgstation.Server.Host/Components/Deployment/DmbProviderBase.cs @@ -13,11 +13,11 @@ namespace Tgstation.Server.Host.Components.Deployment /// public string DmbName => String.Concat( CompileJob.DmeName, - EngineVersion.Engine.Value switch + EngineVersion.Engine switch { EngineType.Byond => ".dmb", EngineType.OpenDream => ".json", - _ => throw new InvalidOperationException($"Invalid EngineType: {EngineVersion.Engine.Value}"), + _ => throw new InvalidOperationException($"Invalid EngineType: {EngineVersion.Engine}"), }); /// diff --git a/src/Tgstation.Server.Host/Components/Deployment/DreamMaker.cs b/src/Tgstation.Server.Host/Components/Deployment/DreamMaker.cs index 3a8a256fd1..f72d848082 100644 --- a/src/Tgstation.Server.Host/Components/Deployment/DreamMaker.cs +++ b/src/Tgstation.Server.Host/Components/Deployment/DreamMaker.cs @@ -113,12 +113,12 @@ namespace Tgstation.Server.Host.Components.Deployment /// /// The active callback from . /// - Func> currentChatCallback; + Func>? currentChatCallback; /// /// Cached for . /// - string currentDreamMakerOutput; + string? currentDreamMakerOutput; /// /// If a compile job is running. @@ -207,18 +207,18 @@ namespace Tgstation.Server.Host.Components.Deployment currentChatCallback = null; currentDreamMakerOutput = null; - Models.CompileJob compileJob = null; + Models.CompileJob? compileJob = null; try { - string repoOwner = null; - string repoName = null; + string? repoOwner = null; + string? repoName = null; TimeSpan? averageSpan = null; - Models.RepositorySettings repositorySettings = null; - Models.DreamDaemonSettings ddSettings = null; - Models.DreamMakerSettings dreamMakerSettings = null; - IRepository repo = null; - IRemoteDeploymentManager remoteDeploymentManager = null; - Models.RevisionInformation revInfo = null; + Models.RepositorySettings? repositorySettings = null; + Models.DreamDaemonSettings? ddSettings = null; + Models.DreamMakerSettings? dreamMakerSettings = null; + IRepository? repo = null; + IRemoteDeploymentManager? remoteDeploymentManager = null; + Models.RevisionInformation? revInfo = null; await databaseContextFactory.UseContext( async databaseContext => { @@ -268,7 +268,7 @@ namespace Tgstation.Server.Host.Components.Deployment throw new JobException(ErrorCode.RepoMissing); remoteDeploymentManager = remoteDeploymentManagerFactory - .CreateRemoteDeploymentManager(metadata, repo.RemoteGitProvider.Value); + .CreateRemoteDeploymentManager(metadata, repo.RemoteGitProvider!.Value); var repoSha = repo.Head; repoOwner = repo.RemoteRepositoryOwner; @@ -276,13 +276,13 @@ namespace Tgstation.Server.Host.Components.Deployment revInfo = await databaseContext .RevisionInformations .AsQueryable() - .Where(x => x.CommitSha == repoSha && x.Instance.Id == metadata.Id) - .Include(x => x.ActiveTestMerges) - .ThenInclude(x => x.TestMerge) - .ThenInclude(x => x.MergedBy) + .Where(x => x.CommitSha == repoSha && x.InstanceId == metadata.Id) + .Include(x => x.ActiveTestMerges!) + .ThenInclude(x => x.TestMerge!) + .ThenInclude(x => x.MergedBy) .FirstOrDefaultAsync(cancellationToken); - if (revInfo == default) + if (revInfo == null) { revInfo = new Models.RevisionInformation { @@ -310,16 +310,17 @@ namespace Tgstation.Server.Host.Components.Deployment }); var likelyPushedTestMergeCommit = - repositorySettings.PushTestMergeCommits.Value + repositorySettings!.PushTestMergeCommits!.Value && repositorySettings.AccessToken != null && repositorySettings.AccessUser != null; using (repo) compileJob = await Compile( - revInfo, - dreamMakerSettings, - ddSettings, - repo, - remoteDeploymentManager, + job, + revInfo!, + dreamMakerSettings!, + ddSettings!, + repo!, + remoteDeploymentManager!, progressReporter, averageSpan, likelyPushedTestMergeCommit, @@ -332,11 +333,11 @@ namespace Tgstation.Server.Host.Components.Deployment async databaseContext => { var fullJob = compileJob.Job; - compileJob.Job = new Models.Job(job.Id.Value); + compileJob.Job = new Models.Job(job.Require(x => x.Id)); var fullRevInfo = compileJob.RevisionInformation; compileJob.RevisionInformation = new Models.RevisionInformation { - Id = revInfo.Id, + Id = revInfo!.Id, }; databaseContext.Jobs.Attach(compileJob.Job); @@ -348,7 +349,7 @@ namespace Tgstation.Server.Host.Components.Deployment logger.LogTrace("Created CompileJob {compileJobId}", compileJob.Id); try { - var chatNotificationAction = currentChatCallback(null, compileJob.Output); + var chatNotificationAction = currentChatCallback!(null, compileJob.Output!); await compileJobConsumer.LoadCompileJob(compileJob, chatNotificationAction, cancellationToken); } catch @@ -367,11 +368,11 @@ namespace Tgstation.Server.Host.Components.Deployment } catch (Exception ex) { - await CleanupFailedCompile(compileJob, remoteDeploymentManager, ex); + await CleanupFailedCompile(compileJob, remoteDeploymentManager!, ex); throw; } - var commentsTask = remoteDeploymentManager.PostDeploymentComments( + var commentsTask = remoteDeploymentManager!.PostDeploymentComments( compileJob, activeCompileJob?.RevisionInformation, repositorySettings, @@ -398,7 +399,7 @@ namespace Tgstation.Server.Host.Components.Deployment { currentChatCallback?.Invoke( FormatExceptionForUsers(ex), - currentDreamMakerOutput); + currentDreamMakerOutput!); throw; } @@ -420,13 +421,13 @@ namespace Tgstation.Server.Host.Components.Deployment var previousCompileJobs = await databaseContext .CompileJobs .AsQueryable() - .Where(x => x.Job.Instance.Id == metadata.Id) + .Where(x => x.Job.Instance!.Id == metadata.Id) .OrderByDescending(x => x.Job.StoppedAt) .Take(10) .Select(x => new { - x.Job.StoppedAt, - x.Job.StartedAt, + StoppedAt = x.Job.StoppedAt!.Value, + StartedAt = x.Job.StartedAt!.Value, }) .ToListAsync(cancellationToken); @@ -435,7 +436,7 @@ namespace Tgstation.Server.Host.Components.Deployment { var totalSpan = TimeSpan.Zero; foreach (var previousCompileJob in previousCompileJobs) - totalSpan += previousCompileJob.StoppedAt.Value - previousCompileJob.StartedAt.Value; + totalSpan += previousCompileJob.StoppedAt - previousCompileJob.StartedAt; averageSpan = totalSpan / previousCompileJobs.Count; } @@ -445,6 +446,7 @@ namespace Tgstation.Server.Host.Components.Deployment /// /// Run the compile implementation. /// + /// The currently running . /// The . /// The . /// The . @@ -456,6 +458,7 @@ namespace Tgstation.Server.Host.Components.Deployment /// The for the operation. /// A resulting in the completed . async ValueTask Compile( + Models.Job job, Models.RevisionInformation revisionInformation, Api.Models.Internal.DreamMakerSettings dreamMakerSettings, DreamDaemonLaunchParameters launchParameters, @@ -483,22 +486,20 @@ namespace Tgstation.Server.Host.Components.Deployment repository.RemoteRepositoryName, localCommitExistsOnRemote); - var job = new Models.CompileJob + var compileJob = new Models.CompileJob(job, revisionInformation, engineLock.Version.ToString()) { DirectoryName = Guid.NewGuid(), DmeName = dreamMakerSettings.ProjectName, - RevisionInformation = revisionInformation, - EngineVersion = engineLock.Version.ToString(), RepositoryOrigin = repository.Origin.ToString(), }; progressReporter.StageName = "Creating remote deployment notification"; await remoteDeploymentManager.StartDeployment( repository, - job, + compileJob, cancellationToken); - logger.LogTrace("Deployment will timeout at {timeoutTime}", DateTimeOffset.UtcNow + dreamMakerSettings.Timeout.Value); + logger.LogTrace("Deployment will timeout at {timeoutTime}", DateTimeOffset.UtcNow + dreamMakerSettings.Timeout!.Value); using var timeoutTokenSource = new CancellationTokenSource(dreamMakerSettings.Timeout.Value); var timeoutToken = timeoutTokenSource.Token; using (timeoutToken.Register(() => logger.LogWarning("Deployment timed out!"))) @@ -508,7 +509,7 @@ namespace Tgstation.Server.Host.Components.Deployment { await RunCompileJob( progressReporter, - job, + compileJob, dreamMakerSettings, launchParameters, engineLock, @@ -522,7 +523,7 @@ namespace Tgstation.Server.Host.Components.Deployment } } - return job; + return compileJob; } catch (OperationCanceledException) { @@ -560,7 +561,7 @@ namespace Tgstation.Server.Host.Components.Deployment IRemoteDeploymentManager remoteDeploymentManager, CancellationToken cancellationToken) { - var outputDirectory = job.DirectoryName.ToString(); + var outputDirectory = job.DirectoryName!.Value.ToString(); logger.LogTrace("Compile output GUID: {dirGuid}", outputDirectory); try @@ -644,13 +645,13 @@ namespace Tgstation.Server.Host.Components.Deployment progressReporter.StageName = "Validating DMAPI"; await VerifyApi( - launchParameters.StartupTimeout.Value, - dreamMakerSettings.ApiValidationSecurityLevel.Value, + launchParameters.StartupTimeout!.Value, + dreamMakerSettings.ApiValidationSecurityLevel!.Value, job, engineLock, - dreamMakerSettings.ApiValidationPort.Value, - dreamMakerSettings.RequireDMApiValidation.Value, - launchParameters.LogOutput.Value, + dreamMakerSettings.ApiValidationPort!.Value, + dreamMakerSettings.RequireDMApiValidation!.Value, + launchParameters.LogOutput!.Value, cancellationToken); } catch (JobException) @@ -796,7 +797,7 @@ namespace Tgstation.Server.Host.Components.Deployment ApiValidationStatus validationStatus; await using (var provider = new TemporaryDmbProvider( - ioManager.ResolvePath(job.DirectoryName.ToString()), + ioManager.ResolvePath(job.DirectoryName!.Value.ToString()), job, engineLock.Version)) await using (var controller = await sessionControllerFactory.LaunchNew(provider, engineLock, launchParameters, true, cancellationToken)) @@ -856,7 +857,7 @@ namespace Tgstation.Server.Host.Components.Deployment await using var dm = processExecutor.LaunchProcess( engineLock.CompilerExePath, ioManager.ResolvePath( - job.DirectoryName.ToString()), + job.DirectoryName!.Value.ToString()), arguments, readStandardHandles: true, noShellExecute: true); @@ -886,14 +887,15 @@ namespace Tgstation.Server.Host.Components.Deployment async ValueTask ModifyDme(Models.CompileJob job, CancellationToken cancellationToken) { var dmeFileName = String.Join('.', job.DmeName, DmeExtension); - var dmePath = ioManager.ConcatPath(job.DirectoryName.ToString(), dmeFileName); + var stringDirectoryName = job.DirectoryName!.Value.ToString(); + var dmePath = ioManager.ConcatPath(stringDirectoryName, dmeFileName); var dmeReadTask = ioManager.ReadAllBytes(dmePath, cancellationToken); var dmeModificationsTask = configuration.CopyDMFilesTo( dmeFileName, ioManager.ResolvePath( ioManager.ConcatPath( - job.DirectoryName.ToString(), + stringDirectoryName, ioManager.GetDirectoryName(dmeFileName))), cancellationToken); @@ -952,7 +954,7 @@ namespace Tgstation.Server.Host.Components.Deployment async ValueTask CleanDir() { logger.LogTrace("Cleaning compile directory..."); - var jobPath = job.DirectoryName.ToString(); + var jobPath = job.DirectoryName!.Value.ToString(); try { // DCT: None available @@ -965,13 +967,16 @@ namespace Tgstation.Server.Host.Components.Deployment } } - // DCT: None available + var dirCleanTask = CleanDir(); + + var failRemoteDeployTask = remoteDeploymentManager.FailDeployment( + job, + FormatExceptionForUsers(exception), + CancellationToken.None); // DCT: None available + return ValueTaskExtensions.WhenAll( - CleanDir(), - remoteDeploymentManager.FailDeployment( - job, - FormatExceptionForUsers(exception), - CancellationToken.None)); + dirCleanTask, + failRemoteDeployTask); } } } diff --git a/src/Tgstation.Server.Host/Components/Deployment/HardLinkDmbProvider.cs b/src/Tgstation.Server.Host/Components/Deployment/HardLinkDmbProvider.cs index 568786e340..98a8361e34 100644 --- a/src/Tgstation.Server.Host/Components/Deployment/HardLinkDmbProvider.cs +++ b/src/Tgstation.Server.Host/Components/Deployment/HardLinkDmbProvider.cs @@ -153,7 +153,7 @@ namespace Tgstation.Server.Host.Components.Deployment if (taskThrottle.HasValue && taskThrottle < 1) throw new ArgumentOutOfRangeException(nameof(taskThrottle), taskThrottle, "taskThrottle must be at least 1!"); - var src = IOManager.ResolvePath(CompileJob.DirectoryName.ToString()); + var src = IOManager.ResolvePath(CompileJob.DirectoryName!.Value.ToString()); var dest = IOManager.ResolvePath(mirrorGuid.ToString()); using var semaphore = taskThrottle.HasValue ? new SemaphoreSlim(taskThrottle.Value) : null; @@ -178,10 +178,10 @@ namespace Tgstation.Server.Host.Components.Deployment /// The for the operation. /// A of s representing the running operations. The first returned is always the necessary call to . /// I genuinely don't know how this will work with symlinked files. Waiting for the issue report I guess. - IEnumerable MirrorDirectoryImpl(string src, string dest, SemaphoreSlim semaphore, CancellationToken cancellationToken) + IEnumerable MirrorDirectoryImpl(string src, string dest, SemaphoreSlim? semaphore, CancellationToken cancellationToken) { var dir = new DirectoryInfo(src); - Task subdirCreationTask = null; + Task? subdirCreationTask = null; var dreamDaemonWillAcceptOutOfDirectorySymlinks = CompileJob.MinimumSecurityLevel == DreamDaemonSecurity.Trusted; foreach (var subDirectory in dir.EnumerateDirectories()) { @@ -191,7 +191,8 @@ namespace Tgstation.Server.Host.Components.Deployment if (subDirectory.Attributes.HasFlag(FileAttributes.ReparsePoint)) if (dreamDaemonWillAcceptOutOfDirectorySymlinks) { - var target = subDirectory.ResolveLinkTarget(false); + var target = subDirectory.ResolveLinkTarget(false) + ?? throw new InvalidOperationException($"\"{subDirectory.FullName}\" was incorrectly identified as a symlinked directory!"); logger.LogDebug("Recreating directory {name} as symlink to {target}", subDirectory.Name, target); if (subdirCreationTask == null) { @@ -250,7 +251,9 @@ namespace Tgstation.Server.Host.Components.Deployment if (fileInfo.Attributes.HasFlag(FileAttributes.ReparsePoint)) { // AHHHHHHHHHHHHH - var target = fileInfo.ResolveLinkTarget(!dreamDaemonWillAcceptOutOfDirectorySymlinks); + var target = fileInfo.ResolveLinkTarget(!dreamDaemonWillAcceptOutOfDirectorySymlinks) + ?? throw new InvalidOperationException($"\"{fileInfo.FullName}\" was incorrectly identified as a symlinked file!"); + if (dreamDaemonWillAcceptOutOfDirectorySymlinks) { logger.LogDebug("Recreating symlinked file {name} as symlink to {target}", fileInfo.Name, target.FullName); diff --git a/src/Tgstation.Server.Host/Components/Deployment/ICompileJobSink.cs b/src/Tgstation.Server.Host/Components/Deployment/ICompileJobSink.cs index 49d4eae165..cf18fc0412 100644 --- a/src/Tgstation.Server.Host/Components/Deployment/ICompileJobSink.cs +++ b/src/Tgstation.Server.Host/Components/Deployment/ICompileJobSink.cs @@ -15,9 +15,9 @@ namespace Tgstation.Server.Host.Components.Deployment /// Load a new into the . /// /// The to load. - /// An to be called when the becomes active or is discarded with or respectively. + /// An optional to be called when the becomes active or is discarded with or respectively. /// The for the operation. /// A representing the running operation. - ValueTask LoadCompileJob(CompileJob job, Action activationAction, CancellationToken cancellationToken); + ValueTask LoadCompileJob(CompileJob job, Action? activationAction, CancellationToken cancellationToken); } } diff --git a/src/Tgstation.Server.Host/Components/Deployment/IDmbFactory.cs b/src/Tgstation.Server.Host/Components/Deployment/IDmbFactory.cs index 6ff04b0a00..08d1a782b5 100644 --- a/src/Tgstation.Server.Host/Components/Deployment/IDmbFactory.cs +++ b/src/Tgstation.Server.Host/Components/Deployment/IDmbFactory.cs @@ -23,7 +23,7 @@ namespace Tgstation.Server.Host.Components.Deployment bool DmbAvailable { get; } /// - /// Gets the next . + /// Gets the next . is a precondition. /// /// The amount of locks to give the resulting . It's must be called this many times to properly clean the job. /// A new . @@ -35,7 +35,7 @@ namespace Tgstation.Server.Host.Components.Deployment /// The to make the for. /// The for the operation. /// A resulting in a new representing the on success, on failure. - ValueTask FromCompileJob(CompileJob compileJob, CancellationToken cancellationToken); + ValueTask FromCompileJob(CompileJob compileJob, CancellationToken cancellationToken); /// /// Deletes all compile jobs that are inactive in the Game folder. diff --git a/src/Tgstation.Server.Host/Components/Deployment/ILatestCompileJobProvider.cs b/src/Tgstation.Server.Host/Components/Deployment/ILatestCompileJobProvider.cs index 38a90c8dc6..021f62602d 100644 --- a/src/Tgstation.Server.Host/Components/Deployment/ILatestCompileJobProvider.cs +++ b/src/Tgstation.Server.Host/Components/Deployment/ILatestCompileJobProvider.cs @@ -10,7 +10,7 @@ namespace Tgstation.Server.Host.Components.Deployment /// /// Gets the latest . /// - /// The latest . - CompileJob LatestCompileJob(); + /// The latest or if none are available. + CompileJob? LatestCompileJob(); } } diff --git a/src/Tgstation.Server.Host/Components/Deployment/Remote/BaseRemoteDeploymentManager.cs b/src/Tgstation.Server.Host/Components/Deployment/Remote/BaseRemoteDeploymentManager.cs index db48c659ba..0f8c86732f 100644 --- a/src/Tgstation.Server.Host/Components/Deployment/Remote/BaseRemoteDeploymentManager.cs +++ b/src/Tgstation.Server.Host/Components/Deployment/Remote/BaseRemoteDeploymentManager.cs @@ -52,18 +52,23 @@ namespace Tgstation.Server.Host.Components.Deployment.Remote /// public async ValueTask PostDeploymentComments( CompileJob compileJob, - RevisionInformation previousRevisionInformation, + RevisionInformation? previousRevisionInformation, RepositorySettings repositorySettings, - string repoOwner, - string repoName, + string? repoOwner, + string? repoName, CancellationToken cancellationToken) { - if (repositorySettings?.AccessToken == null) + ArgumentNullException.ThrowIfNull(compileJob); + ArgumentNullException.ThrowIfNull(repositorySettings); + ArgumentNullException.ThrowIfNull(repoOwner); + ArgumentNullException.ThrowIfNull(repoName); + + if (repositorySettings.AccessToken == null) return; var deployedRevisionInformation = compileJob.RevisionInformation; if ((previousRevisionInformation != null && previousRevisionInformation.CommitSha == deployedRevisionInformation.CommitSha) - || !repositorySettings.PostTestMergeComment.Value) + || !repositorySettings.PostTestMergeComment!.Value) return; previousRevisionInformation ??= new RevisionInformation(); @@ -94,7 +99,7 @@ namespace Tgstation.Server.Host.Components.Deployment.Remote .Any(y => y.TestMerge.Number == x.Number)) .ToList(); - if (!addedTestMerges.Any() && !removedTestMerges.Any() && !updatedTestMerges.Any()) + if (addedTestMerges.Count == 0 && removedTestMerges.Count == 0 && updatedTestMerges.Count == 0) return; Logger.LogTrace( @@ -105,48 +110,54 @@ namespace Tgstation.Server.Host.Components.Deployment.Remote var tasks = new List(addedTestMerges.Count + updatedTestMerges.Count + removedTestMerges.Count); foreach (var addedTestMerge in addedTestMerges) - tasks.Add( - CommentOnTestMergeSource( + { + var addCommentTask = CommentOnTestMergeSource( + repositorySettings, + repoOwner, + repoName, + FormatTestMerge( repositorySettings, + compileJob, + addedTestMerge, repoOwner, repoName, - FormatTestMerge( - repositorySettings, - compileJob, - addedTestMerge, - repoOwner, - repoName, - false), - addedTestMerge.Number, - cancellationToken)); + false), + addedTestMerge.Number, + cancellationToken); + tasks.Add(addCommentTask); + } foreach (var removedTestMerge in removedTestMerges) - tasks.Add( - CommentOnTestMergeSource( - repositorySettings, - repoOwner, - repoName, - "#### Test Merge Removed", - removedTestMerge.Number, - cancellationToken)); + { + var removeCommentTask = CommentOnTestMergeSource( + repositorySettings, + repoOwner, + repoName, + "#### Test Merge Removed", + removedTestMerge.Number, + cancellationToken); + tasks.Add(removeCommentTask); + } foreach (var updatedTestMerge in updatedTestMerges) - tasks.Add( - CommentOnTestMergeSource( + { + var updateCommentTask = CommentOnTestMergeSource( + repositorySettings, + repoOwner, + repoName, + FormatTestMerge( repositorySettings, + compileJob, + updatedTestMerge, repoOwner, repoName, - FormatTestMerge( - repositorySettings, - compileJob, - updatedTestMerge, - repoOwner, - repoName, - true), - updatedTestMerge.Number, - cancellationToken)); + true), + updatedTestMerge.Number, + cancellationToken); + tasks.Add(updateCommentTask); + } - if (tasks.Any()) + if (tasks.Count > 0) await ValueTaskExtensions.WhenAll(tasks); } @@ -155,7 +166,7 @@ namespace Tgstation.Server.Host.Components.Deployment.Remote { ArgumentNullException.ThrowIfNull(compileJob); - if (activationCallbacks.TryGetValue(compileJob.Id.Value, out var activationCallback)) + if (activationCallbacks.TryGetValue(compileJob.Require(x => x.Id), out var activationCallback)) activationCallback(true); return ApplyDeploymentImpl(compileJob, cancellationToken); @@ -169,7 +180,7 @@ namespace Tgstation.Server.Host.Components.Deployment.Remote { ArgumentNullException.ThrowIfNull(compileJob); - if (activationCallbacks.TryRemove(compileJob.Id.Value, out var activationCallback)) + if (activationCallbacks.TryRemove(compileJob.Require(x => x.Id), out var activationCallback)) activationCallback(false); return MarkInactiveImpl(compileJob, cancellationToken); @@ -183,12 +194,13 @@ namespace Tgstation.Server.Host.Components.Deployment.Remote CancellationToken cancellationToken); /// - public ValueTask StageDeployment(CompileJob compileJob, Action activationCallback, CancellationToken cancellationToken) + public ValueTask StageDeployment(CompileJob compileJob, Action? activationCallback, CancellationToken cancellationToken) { ArgumentNullException.ThrowIfNull(compileJob); - if (activationCallback != null && !activationCallbacks.TryAdd(compileJob.Id.Value, activationCallback)) - Logger.LogError("activationCallbacks conflicted on CompileJob #{id}!", compileJob.Id.Value); + var compileJobId = compileJob.Require(x => x.Id); + if (activationCallback != null && !activationCallbacks.TryAdd(compileJobId, activationCallback)) + Logger.LogError("activationCallbacks conflicted on CompileJob #{id}!", compileJobId); return StageDeploymentImpl(compileJob, cancellationToken); } diff --git a/src/Tgstation.Server.Host/Components/Deployment/Remote/GitHubRemoteDeploymentManager.cs b/src/Tgstation.Server.Host/Components/Deployment/Remote/GitHubRemoteDeploymentManager.cs index 751bb33646..4e37e7d008 100644 --- a/src/Tgstation.Server.Host/Components/Deployment/Remote/GitHubRemoteDeploymentManager.cs +++ b/src/Tgstation.Server.Host/Components/Deployment/Remote/GitHubRemoteDeploymentManager.cs @@ -65,7 +65,7 @@ namespace Tgstation.Server.Host.Components.Deployment.Remote Logger.LogTrace("Starting deployment..."); - RepositorySettings repositorySettings = null; + RepositorySettings? repositorySettings = null; await databaseContextFactory.UseContext( async databaseContext => repositorySettings = await databaseContext @@ -74,12 +74,12 @@ namespace Tgstation.Server.Host.Components.Deployment.Remote .Where(x => x.InstanceId == Metadata.Id) .FirstAsync(cancellationToken)); - var instanceAuthenticated = repositorySettings.AccessToken != null; - IAuthenticatedGitHubService authenticatedGitHubService; + var instanceAuthenticated = repositorySettings!.AccessToken != null; + IAuthenticatedGitHubService? authenticatedGitHubService; IGitHubService gitHubService; if (instanceAuthenticated) { - authenticatedGitHubService = gitHubServiceFactory.CreateService(repositorySettings.AccessToken); + authenticatedGitHubService = gitHubServiceFactory.CreateService(repositorySettings.AccessToken!); gitHubService = authenticatedGitHubService; } else @@ -88,12 +88,14 @@ namespace Tgstation.Server.Host.Components.Deployment.Remote gitHubService = gitHubServiceFactory.CreateService(); } + var repoOwner = remoteInformation.RemoteRepositoryOwner!; + var repoName = remoteInformation.RemoteRepositoryName!; var repositoryIdTask = gitHubService.GetRepositoryId( - remoteInformation.RemoteRepositoryOwner, - remoteInformation.RemoteRepositoryName, + repoOwner, + repoName, cancellationToken); - if (!repositorySettings.CreateGitHubDeployments.Value) + if (!repositorySettings.CreateGitHubDeployments!.Value) Logger.LogTrace("Not creating deployment"); else if (!instanceAuthenticated) Logger.LogWarning("Can't create GitHub deployment as no access token is set for repository!"); @@ -102,7 +104,7 @@ namespace Tgstation.Server.Host.Components.Deployment.Remote Logger.LogTrace("Creating deployment..."); try { - compileJob.GitHubDeploymentId = await authenticatedGitHubService.CreateDeployment( + compileJob.GitHubDeploymentId = await authenticatedGitHubService!.CreateDeployment( new NewDeployment(compileJob.RevisionInformation.CommitSha) { AutoMerge = false, @@ -111,8 +113,8 @@ namespace Tgstation.Server.Host.Components.Deployment.Remote ProductionEnvironment = true, RequiredContexts = new Collection(), }, - remoteInformation.RemoteRepositoryOwner, - remoteInformation.RemoteRepositoryName, + repoOwner, + repoName, cancellationToken); Logger.LogDebug("Created deployment ID {deploymentId}", compileJob.GitHubDeploymentId); @@ -123,8 +125,8 @@ namespace Tgstation.Server.Host.Components.Deployment.Remote Description = "The project is being deployed", AutoInactive = false, }, - remoteInformation.RemoteRepositoryOwner, - remoteInformation.RemoteRepositoryName, + repoOwner, + repoName, compileJob.GitHubDeploymentId.Value, cancellationToken); @@ -166,7 +168,7 @@ namespace Tgstation.Server.Host.Components.Deployment.Remote ArgumentNullException.ThrowIfNull(repositorySettings); ArgumentNullException.ThrowIfNull(revisionInformation); - if (revisionInformation.ActiveTestMerges?.Any() != true) + if ((revisionInformation.ActiveTestMerges?.Count > 0) != true) { Logger.LogTrace("No test merges to remove."); return Array.Empty(); @@ -178,7 +180,11 @@ namespace Tgstation.Server.Host.Components.Deployment.Remote var tasks = revisionInformation .ActiveTestMerges - .Select(x => gitHubService.GetPullRequest(repository.RemoteRepositoryOwner, repository.RemoteRepositoryName, x.TestMerge.Number, cancellationToken)); + .Select(x => gitHubService.GetPullRequest( + repository.RemoteRepositoryOwner!, + repository.RemoteRepositoryName!, + x.TestMerge.Number, + cancellationToken)); try { await Task.WhenAll(tasks); @@ -190,7 +196,7 @@ namespace Tgstation.Server.Host.Components.Deployment.Remote var newList = revisionInformation.ActiveTestMerges.Select(x => x.TestMerge).ToList(); - PullRequest lastMerged = null; + PullRequest? lastMerged = null; async ValueTask CheckRemovePR(Task task) { var pr = await task; @@ -249,7 +255,7 @@ namespace Tgstation.Server.Host.Components.Deployment.Remote int testMergeNumber, CancellationToken cancellationToken) { - var gitHubService = gitHubServiceFactory.CreateService(repositorySettings.AccessToken); + var gitHubService = gitHubServiceFactory.CreateService(repositorySettings.AccessToken!); try { @@ -272,12 +278,12 @@ namespace Tgstation.Server.Host.Components.Deployment.Remote CultureInfo.InvariantCulture, "#### Test Merge {4}{0}{0}
Details{0}{0}##### Server Instance{0}{5}{1}{0}{0}##### Revision{0}Origin: {6}{0}Pull Request: {2}{0}Server: {7}{3}{8}{0}
", Environment.NewLine, - repositorySettings.ShowTestMergeCommitters.Value + repositorySettings.ShowTestMergeCommitters!.Value ? String.Format( CultureInfo.InvariantCulture, "{0}{0}##### Merged By{0}{1}", Environment.NewLine, - testMerge.MergedBy.Name) + testMerge.MergedBy!.Name) : String.Empty, testMerge.TargetCommitSha, testMerge.Comment != null @@ -292,7 +298,7 @@ namespace Tgstation.Server.Host.Components.Deployment.Remote compileJob.RevisionInformation.OriginCommitSha, compileJob.RevisionInformation.CommitSha, compileJob.GitHubDeploymentId.HasValue - ? $"{Environment.NewLine}[GitHub Deployments](https://github.com/{remoteRepositoryOwner}/{remoteRepositoryName}/deployments/activity_log?environment=TGS%3A+{Metadata.Name.Replace(" ", "+", StringComparison.Ordinal)})" + ? $"{Environment.NewLine}[GitHub Deployments](https://github.com/{remoteRepositoryOwner}/{remoteRepositoryName}/deployments/activity_log?environment=TGS%3A+{Metadata.Name!.Replace(" ", "+", StringComparison.Ordinal)})" : String.Empty); /// @@ -319,7 +325,7 @@ namespace Tgstation.Server.Host.Components.Deployment.Remote Logger.LogTrace("Updating deployment {gitHubDeploymentId} to {deploymentState}...", compileJob.GitHubDeploymentId.Value, deploymentState); - string gitHubAccessToken = null; + string? gitHubAccessToken = null; await databaseContextFactory.UseContext( async databaseContext => gitHubAccessToken = await databaseContext diff --git a/src/Tgstation.Server.Host/Components/Deployment/Remote/GitLabRemoteDeploymentManager.cs b/src/Tgstation.Server.Host/Components/Deployment/Remote/GitLabRemoteDeploymentManager.cs index 2686764cca..9d0adeaf37 100644 --- a/src/Tgstation.Server.Host/Components/Deployment/Remote/GitLabRemoteDeploymentManager.cs +++ b/src/Tgstation.Server.Host/Components/Deployment/Remote/GitLabRemoteDeploymentManager.cs @@ -46,7 +46,7 @@ namespace Tgstation.Server.Host.Components.Deployment.Remote ArgumentNullException.ThrowIfNull(repositorySettings); ArgumentNullException.ThrowIfNull(revisionInformation); - if (revisionInformation.ActiveTestMerges?.Any() != true) + if ((revisionInformation.ActiveTestMerges?.Count > 0) != true) { Logger.LogTrace("No test merges to remove."); return Array.Empty(); @@ -75,7 +75,7 @@ namespace Tgstation.Server.Host.Components.Deployment.Remote var newList = revisionInformation.ActiveTestMerges.Select(x => x.TestMerge).ToList(); - MergeRequest lastMerged = null; + MergeRequest? lastMerged = null; async ValueTask CheckRemoveMR(Task task) { var mergeRequest = await task; @@ -162,12 +162,12 @@ namespace Tgstation.Server.Host.Components.Deployment.Remote CultureInfo.InvariantCulture, "#### Test Merge {4}{0}{0}##### Server Instance{0}{5}{1}{0}{0}##### Revision{0}Origin: {6}{0}Merge Request: {2}{0}Server: {7}{3}", Environment.NewLine, - repositorySettings.ShowTestMergeCommitters.Value + repositorySettings.ShowTestMergeCommitters!.Value ? String.Format( CultureInfo.InvariantCulture, "{0}{0}##### Merged By{0}{1}", Environment.NewLine, - testMerge.MergedBy.Name) + testMerge.MergedBy!.Name) : String.Empty, testMerge.TargetCommitSha, testMerge.Comment != null diff --git a/src/Tgstation.Server.Host/Components/Deployment/Remote/IRemoteDeploymentManager.cs b/src/Tgstation.Server.Host/Components/Deployment/Remote/IRemoteDeploymentManager.cs index 42b154ac7e..d8017a6686 100644 --- a/src/Tgstation.Server.Host/Components/Deployment/Remote/IRemoteDeploymentManager.cs +++ b/src/Tgstation.Server.Host/Components/Deployment/Remote/IRemoteDeploymentManager.cs @@ -34,7 +34,7 @@ namespace Tgstation.Server.Host.Components.Deployment.Remote /// A representing the running operation. ValueTask StageDeployment( CompileJob compileJob, - Action activationCallback, + Action? activationCallback, CancellationToken cancellationToken); /// @@ -66,18 +66,18 @@ namespace Tgstation.Server.Host.Components.Deployment.Remote /// Post deployment comments to the test merge ticket. /// /// The deployed . - /// The of the previous deployment. + /// The optional of the previous deployment. /// The . - /// The GitHub repostiory owner. - /// The GitHub repostiory name. + /// The remote repostiory owner. + /// The remote repostiory name. /// The for the operation. /// A representing the running operation. ValueTask PostDeploymentComments( CompileJob compileJob, - RevisionInformation previousRevisionInformation, + RevisionInformation? previousRevisionInformation, RepositorySettings repositorySettings, - string repoOwner, - string repoName, + string? repoOwner, + string? repoName, CancellationToken cancellationToken); /// diff --git a/src/Tgstation.Server.Host/Components/Engine/ByondInstallation.cs b/src/Tgstation.Server.Host/Components/Engine/ByondInstallation.cs index ba730a7c7f..fce9b2c5bc 100644 --- a/src/Tgstation.Server.Host/Components/Engine/ByondInstallation.cs +++ b/src/Tgstation.Server.Host/Components/Engine/ByondInstallation.cs @@ -92,8 +92,8 @@ namespace Tgstation.Server.Host.Components.Engine InstallationTask = installationTask ?? throw new ArgumentNullException(nameof(installationTask)); ArgumentNullException.ThrowIfNull(version); - if (version.Engine.Value != EngineType.Byond) - throw new ArgumentException($"Invalid EngineType: {version.Engine.Value}", nameof(version)); + if (version.Engine != EngineType.Byond) + throw new ArgumentException($"Invalid EngineType: {version.Engine}", nameof(version)); Version = version ?? throw new ArgumentNullException(nameof(version)); ServerExePath = dreamDaemonPath ?? throw new ArgumentNullException(nameof(dreamDaemonPath)); @@ -108,7 +108,7 @@ namespace Tgstation.Server.Host.Components.Engine IDmbProvider dmbProvider, IReadOnlyDictionary parameters, DreamDaemonLaunchParameters launchParameters, - string logFilePath) + string? logFilePath) { ArgumentNullException.ThrowIfNull(dmbProvider); ArgumentNullException.ThrowIfNull(parameters); @@ -120,19 +120,19 @@ namespace Tgstation.Server.Host.Components.Engine CultureInfo.InvariantCulture, "{0} -port {1} -ports 1-65535 {2}-close -verbose -{3} -{4}{5}{6}{7} -params \"{8}\"", dmbProvider.DmbName, - launchParameters.Port.Value, - launchParameters.AllowWebClient.Value + launchParameters.Port!.Value, + launchParameters.AllowWebClient!.Value ? "-webclient " : String.Empty, - SecurityWord(launchParameters.SecurityLevel.Value), - VisibilityWord(launchParameters.Visibility.Value), + SecurityWord(launchParameters.SecurityLevel!.Value), + VisibilityWord(launchParameters.Visibility!.Value), logFilePath != null ? $" -logself -log {logFilePath}" : String.Empty, // DD doesn't output anything if -logself is set??? - launchParameters.StartProfiler.Value + launchParameters.StartProfiler!.Value ? " -profile" : String.Empty, - supportsMapThreads && launchParameters.MapThreads.Value != 0 + supportsMapThreads && launchParameters.MapThreads!.Value != 0 ? $" -map-threads {launchParameters.MapThreads.Value}" : String.Empty, parametersString); diff --git a/src/Tgstation.Server.Host/Components/Engine/ByondInstallerBase.cs b/src/Tgstation.Server.Host/Components/Engine/ByondInstallerBase.cs index f02239969f..02691acfc0 100644 --- a/src/Tgstation.Server.Host/Components/Engine/ByondInstallerBase.cs +++ b/src/Tgstation.Server.Host/Components/Engine/ByondInstallerBase.cs @@ -98,7 +98,7 @@ namespace Tgstation.Server.Host.Components.Engine IOManager.ConcatPath( binPathForVersion, GetDreamDaemonName( - version.Version, + version.Version!, out var supportsCli))), IOManager.ResolvePath( IOManager.ConcatPath( @@ -194,11 +194,11 @@ namespace Tgstation.Server.Host.Components.Engine } /// - public override async ValueTask DownloadVersion(EngineVersion version, JobProgressReporter progressReporter, CancellationToken cancellationToken) + public override async ValueTask DownloadVersion(EngineVersion version, JobProgressReporter? progressReporter, CancellationToken cancellationToken) { CheckVersionValidity(version); - var url = await GetDownloadZipUrl(version, cancellationToken); + var url = GetDownloadZipUrl(version); Logger.LogTrace("Downloading {engineType} version {version} from {url}...", TargetEngineType, version, url); await using var download = fileDownloader.DownloadFile(url, null); @@ -231,13 +231,12 @@ namespace Tgstation.Server.Host.Components.Engine /// Create a pointing to the location of the download for a given . /// /// The to create a for. - /// The for the operation. - /// A resulting in a new pointing to the version download location. - ValueTask GetDownloadZipUrl(EngineVersion version, CancellationToken cancellationToken) + /// A pointing to the version download location. + Uri GetDownloadZipUrl(EngineVersion version) { CheckVersionValidity(version); - var url = String.Format(CultureInfo.InvariantCulture, ByondRevisionsUrlTemplate, version.Version.Major, version.Version.Minor); - return ValueTask.FromResult(new Uri(url)); + var url = String.Format(CultureInfo.InvariantCulture, ByondRevisionsUrlTemplate, version.Version!.Major, version.Version.Minor); + return new Uri(url); } } } diff --git a/src/Tgstation.Server.Host/Components/Engine/DelegatingEngineInstaller.cs b/src/Tgstation.Server.Host/Components/Engine/DelegatingEngineInstaller.cs index 75393af5b7..1157dff565 100644 --- a/src/Tgstation.Server.Host/Components/Engine/DelegatingEngineInstaller.cs +++ b/src/Tgstation.Server.Host/Components/Engine/DelegatingEngineInstaller.cs @@ -1,5 +1,5 @@ using System; -using System.Collections.Generic; +using System.Collections.Frozen; using System.Linq; using System.Threading; using System.Threading.Tasks; @@ -15,15 +15,15 @@ namespace Tgstation.Server.Host.Components.Engine sealed class DelegatingEngineInstaller : IEngineInstaller { /// - /// The mapping s to their appropriate . + /// The mapping s to their appropriate . /// - readonly IReadOnlyDictionary delegatedInstallers; + readonly FrozenDictionary delegatedInstallers; /// /// Initializes a new instance of the class. /// /// The value of . - public DelegatingEngineInstaller(IReadOnlyDictionary delegatedInstallers) + public DelegatingEngineInstaller(FrozenDictionary delegatedInstallers) { this.delegatedInstallers = delegatedInstallers ?? throw new ArgumentNullException(nameof(delegatedInstallers)); } @@ -37,7 +37,7 @@ namespace Tgstation.Server.Host.Components.Engine => DelegateCall(version, installer => installer.CreateInstallation(version, path, installationTask)); /// - public ValueTask DownloadVersion(EngineVersion version, JobProgressReporter jobProgressReporter, CancellationToken cancellationToken) + public ValueTask DownloadVersion(EngineVersion version, JobProgressReporter? jobProgressReporter, CancellationToken cancellationToken) => DelegateCall(version, installer => installer.DownloadVersion(version, jobProgressReporter, cancellationToken)); /// @@ -62,7 +62,7 @@ namespace Tgstation.Server.Host.Components.Engine TReturn DelegateCall(EngineVersion version, Func call) { ArgumentNullException.ThrowIfNull(version); - return call(delegatedInstallers[version.Engine.Value]); + return call(delegatedInstallers[version.Engine!.Value]); } } } diff --git a/src/Tgstation.Server.Host/Components/Engine/EngineExecutableLock.cs b/src/Tgstation.Server.Host/Components/Engine/EngineExecutableLock.cs index 70614f13e1..5d7a1d7fef 100644 --- a/src/Tgstation.Server.Host/Components/Engine/EngineExecutableLock.cs +++ b/src/Tgstation.Server.Host/Components/Engine/EngineExecutableLock.cs @@ -1,15 +1,19 @@ using System.Collections.Generic; +using System.Threading; using System.Threading.Tasks; +using Microsoft.Extensions.Logging; + using Tgstation.Server.Api.Models; using Tgstation.Server.Api.Models.Internal; using Tgstation.Server.Host.Components.Deployment; +using Tgstation.Server.Host.System; using Tgstation.Server.Host.Utils; namespace Tgstation.Server.Host.Components.Engine { /// - sealed class EngineExecutableLock : ReferenceCounter, IEngineExecutableLock + class EngineExecutableLock : ReferenceCounter, IEngineExecutableLock { /// public EngineVersion Version => Instance.Version; @@ -40,7 +44,7 @@ namespace Tgstation.Server.Host.Components.Engine IDmbProvider dmbProvider, IReadOnlyDictionary parameters, DreamDaemonLaunchParameters launchParameters, - string logFilePath) + string? logFilePath) => Instance.FormatServerArguments( dmbProvider, parameters, @@ -49,5 +53,14 @@ namespace Tgstation.Server.Host.Components.Engine /// public string FormatCompilerArguments(string dmePath) => Instance.FormatCompilerArguments(dmePath); + + /// + public ValueTask StopServerProcess(ILogger logger, IProcess process, string accessIdentifier, ushort port, CancellationToken cancellationToken) + => Instance.StopServerProcess( + logger, + process, + accessIdentifier, + port, + cancellationToken); } } diff --git a/src/Tgstation.Server.Host/Components/Engine/EngineInstallationBase.cs b/src/Tgstation.Server.Host/Components/Engine/EngineInstallationBase.cs index fb607d3601..5edf44609f 100644 --- a/src/Tgstation.Server.Host/Components/Engine/EngineInstallationBase.cs +++ b/src/Tgstation.Server.Host/Components/Engine/EngineInstallationBase.cs @@ -1,12 +1,16 @@ using System; using System.Collections.Generic; using System.Linq; +using System.Threading; using System.Threading.Tasks; using System.Web; +using Microsoft.Extensions.Logging; + using Tgstation.Server.Api.Models; using Tgstation.Server.Api.Models.Internal; using Tgstation.Server.Host.Components.Deployment; +using Tgstation.Server.Host.System; namespace Tgstation.Server.Host.Components.Engine { @@ -56,6 +60,19 @@ namespace Tgstation.Server.Host.Components.Engine public abstract string FormatCompilerArguments(string dmePath); /// - public abstract string FormatServerArguments(IDmbProvider dmbProvider, IReadOnlyDictionary parameters, DreamDaemonLaunchParameters launchParameters, string logFilePath); + public abstract string FormatServerArguments( + IDmbProvider dmbProvider, + IReadOnlyDictionary parameters, + DreamDaemonLaunchParameters launchParameters, + string? logFilePath); + + /// + public virtual async ValueTask StopServerProcess(ILogger logger, IProcess process, string accessIdentifier, ushort port, CancellationToken cancellationToken) + { + cancellationToken.ThrowIfCancellationRequested(); + logger.LogTrace("Terminating engine server process..."); + process.Terminate(); + await process.Lifetime; + } } } diff --git a/src/Tgstation.Server.Host/Components/Engine/EngineInstallerBase.cs b/src/Tgstation.Server.Host/Components/Engine/EngineInstallerBase.cs index e281c3f48f..7c25c0a13c 100644 --- a/src/Tgstation.Server.Host/Components/Engine/EngineInstallerBase.cs +++ b/src/Tgstation.Server.Host/Components/Engine/EngineInstallerBase.cs @@ -52,7 +52,7 @@ namespace Tgstation.Server.Host.Components.Engine public abstract ValueTask UpgradeInstallation(EngineVersion version, string path, CancellationToken cancellationToken); /// - public abstract ValueTask DownloadVersion(EngineVersion version, JobProgressReporter jobProgressReporter, CancellationToken cancellationToken); + public abstract ValueTask DownloadVersion(EngineVersion version, JobProgressReporter? jobProgressReporter, CancellationToken cancellationToken); /// public abstract ValueTask TrustDmbPath(EngineVersion version, string fullDmbPath, CancellationToken cancellationToken); @@ -64,7 +64,7 @@ namespace Tgstation.Server.Host.Components.Engine protected void CheckVersionValidity(EngineVersion version) { ArgumentNullException.ThrowIfNull(version); - if (version.Engine.Value != TargetEngineType) + if (version.Engine!.Value != TargetEngineType) throw new InvalidOperationException($"Non-{TargetEngineType} engine specified: {version.Engine.Value}"); } } diff --git a/src/Tgstation.Server.Host/Components/Engine/EngineManager.cs b/src/Tgstation.Server.Host/Components/Engine/EngineManager.cs index 1205120411..529eec0fb8 100644 --- a/src/Tgstation.Server.Host/Components/Engine/EngineManager.cs +++ b/src/Tgstation.Server.Host/Components/Engine/EngineManager.cs @@ -32,7 +32,7 @@ namespace Tgstation.Server.Host.Components.Engine const string ActiveVersionFileName = "ActiveVersion.txt"; /// - public EngineVersion ActiveVersion { get; private set; } + public EngineVersion? ActiveVersion { get; private set; } /// public IReadOnlyList InstalledVersions @@ -118,9 +118,9 @@ namespace Tgstation.Server.Host.Components.Engine /// public async ValueTask ChangeVersion( - JobProgressReporter progressReporter, + JobProgressReporter? progressReporter, EngineVersion version, - Stream customVersionStream, + Stream? customVersionStream, bool allowInstallation, CancellationToken cancellationToken) { @@ -143,7 +143,7 @@ namespace Tgstation.Server.Host.Components.Engine await ioManager.WriteAllBytes(ActiveVersionFileName, Encoding.UTF8.GetBytes(stringVersion), cancellationToken); await eventConsumer.HandleEvent( EventType.EngineActiveVersionChange, - new List + new List { ActiveVersion?.ToString(), stringVersion, @@ -160,7 +160,7 @@ namespace Tgstation.Server.Host.Components.Engine } /// - public async ValueTask UseExecutables(EngineVersion requiredVersion, string trustDmbFullPath, CancellationToken cancellationToken) + public async ValueTask UseExecutables(EngineVersion? requiredVersion, string? trustDmbFullPath, CancellationToken cancellationToken) { logger.LogTrace( "Acquiring lock on BYOND version {version}...", @@ -196,19 +196,21 @@ namespace Tgstation.Server.Host.Components.Engine logger.LogTrace("DeleteVersion {version}", version); - if (version.Equals(ActiveVersion)) + var activeVersion = ActiveVersion; + if (activeVersion != null && version.Equals(activeVersion)) throw new JobException(ErrorCode.EngineCannotDeleteActiveVersion); ReferenceCountingContainer container; logger.LogTrace("Waiting to acquire installedVersions lock..."); lock (installedVersions) { - if (!installedVersions.TryGetValue(version, out container)) + if (!installedVersions.TryGetValue(version, out var containerNullable)) { logger.LogTrace("Version {version} already deleted.", version); return; } + container = containerNullable; logger.LogTrace("Installation container acquired for deletion"); } @@ -236,7 +238,8 @@ namespace Tgstation.Server.Host.Components.Engine using (await SemaphoreSlimContext.Lock(changeDeleteSemaphore, cancellationToken)) { // check again because it could have become the active version. - if (version.Equals(ActiveVersion)) + activeVersion = ActiveVersion; + if (activeVersion != null && version.Equals(activeVersion)) throw new JobException(ErrorCode.EngineCannotDeleteActiveVersion); bool proceed; @@ -294,7 +297,7 @@ namespace Tgstation.Server.Host.Components.Engine /// public async Task StartAsync(CancellationToken cancellationToken) { - async ValueTask GetActiveVersion() + async ValueTask GetActiveVersion() { var activeVersionFileExists = await ioManager.FileExists(ActiveVersionFileName, cancellationToken); return !activeVersionFileExists ? null : await ioManager.ReadAllBytes(ActiveVersionFileName, cancellationToken); @@ -319,12 +322,15 @@ namespace Tgstation.Server.Host.Components.Engine var bytes = await ioManager.ReadAllBytes(versionFile, cancellationToken); var text = Encoding.UTF8.GetString(bytes); - if (!EngineVersion.TryParse(text, out var version)) + EngineVersion version; + if (!EngineVersion.TryParse(text, out var versionNullable)) { logger.LogWarning("Cleaning path with unparsable version file: {versionPath}", ioManager.ResolvePath(path)); await ioManager.DeleteDirectory(path, cancellationToken); // cleanup return; } + else + version = versionNullable!; try { @@ -360,11 +366,11 @@ namespace Tgstation.Server.Host.Components.Engine { var activeVersionString = Encoding.UTF8.GetString(activeVersionBytes); - EngineVersion activeVersion; + EngineVersion? activeVersion; bool hasRequestedActiveVersion; lock (installedVersions) hasRequestedActiveVersion = EngineVersion.TryParse(activeVersionString, out activeVersion) - && installedVersions.ContainsKey(activeVersion); + && installedVersions.ContainsKey(activeVersion!); if (hasRequestedActiveVersion) ActiveVersion = activeVersion; // not setting TCS because there's no need during init @@ -384,15 +390,15 @@ namespace Tgstation.Server.Host.Components.Engine /// /// The optional for the operation. /// The to install. - /// Custom zip file to use. Will cause a number to be added. + /// Optional custom zip file to use. Will cause a number to be added. /// If this BYOND version is required as part of a locking operation. /// If an installation should be performed if the is not installed. If and an installation is required an will be thrown. /// The for the operation. /// A resulting in the . async ValueTask AssertAndLockVersion( - JobProgressReporter progressReporter, + JobProgressReporter? progressReporter, EngineVersion version, - Stream customVersionStream, + Stream? customVersionStream, bool neededForLock, bool allowInstallation, CancellationToken cancellationToken) @@ -413,7 +419,8 @@ namespace Tgstation.Server.Host.Components.Engine while (installedVersions.ContainsKey(version)); } - installedOrInstalling = installedVersions.TryGetValue(version, out var installationContainer); + installedOrInstalling = installedVersions.TryGetValue(version, out var installationContainerNullable); + ReferenceCountingContainer installationContainer; if (!installedOrInstalling) { if (!allowInstallation) @@ -424,6 +431,8 @@ namespace Tgstation.Server.Host.Components.Engine ioManager.ResolvePath(version.ToString()), ourTcs.Task); } + else + installationContainer = installationContainerNullable!; installation = installationContainer.Instance; installLock = installationContainer.AddReference(); @@ -497,7 +506,7 @@ namespace Tgstation.Server.Host.Components.Engine /// Custom zip file to use. Will cause a number to be added. /// The for the operation. /// A representing the running operation. - async ValueTask InstallVersionFiles(JobProgressReporter progressReporter, EngineVersion version, Stream customVersionStream, CancellationToken cancellationToken) + async ValueTask InstallVersionFiles(JobProgressReporter? progressReporter, EngineVersion version, Stream? customVersionStream, CancellationToken cancellationToken) { var installFullPath = ioManager.ResolvePath(version.ToString()); async ValueTask DirectoryCleanup() @@ -517,7 +526,7 @@ namespace Tgstation.Server.Host.Components.Engine engineInstallationData = await engineInstaller.DownloadVersion(version, progressReporter, cancellationToken); - progressReporter.ReportProgress(null); + progressReporter?.ReportProgress(null); } else #pragma warning disable CA2000 // Dispose objects before losing scope, false positive diff --git a/src/Tgstation.Server.Host/Components/Engine/IEngineInstallation.cs b/src/Tgstation.Server.Host/Components/Engine/IEngineInstallation.cs index f9aecca33b..ff2e4155f3 100644 --- a/src/Tgstation.Server.Host/Components/Engine/IEngineInstallation.cs +++ b/src/Tgstation.Server.Host/Components/Engine/IEngineInstallation.cs @@ -1,9 +1,13 @@ using System.Collections.Generic; +using System.Threading; using System.Threading.Tasks; +using Microsoft.Extensions.Logging; + using Tgstation.Server.Api.Models; using Tgstation.Server.Api.Models.Internal; using Tgstation.Server.Host.Components.Deployment; +using Tgstation.Server.Host.System; namespace Tgstation.Server.Host.Components.Engine { @@ -51,7 +55,7 @@ namespace Tgstation.Server.Host.Components.Engine /// Return the command line arguments for launching with given . ///
/// The . - /// The map of parameter s as a . Should NOT include the of . + /// The map of parameter s as a . MUST include . Should NOT include the of . /// The . /// The full path to the log file, if any. /// The formatted arguments . @@ -59,7 +63,7 @@ namespace Tgstation.Server.Host.Components.Engine IDmbProvider dmbProvider, IReadOnlyDictionary parameters, DreamDaemonLaunchParameters launchParameters, - string logFilePath); + string? logFilePath); /// /// Return the command line arguments for compiling a given if compilation is necessary. @@ -67,5 +71,16 @@ namespace Tgstation.Server.Host.Components.Engine /// The full path to the .dme to compile. /// The formatted arguments . string FormatCompilerArguments(string dmePath); + + /// + /// Kills a given engine server . + /// + /// The to write to. + /// The to be terminated. + /// The of the session. + /// The port the server is running on. + /// The for the operation. + /// A representing the running operation. + ValueTask StopServerProcess(ILogger logger, IProcess process, string accessIdentifier, ushort port, CancellationToken cancellationToken); } } diff --git a/src/Tgstation.Server.Host/Components/Engine/IEngineInstaller.cs b/src/Tgstation.Server.Host/Components/Engine/IEngineInstaller.cs index 2360202584..7169ffe4b5 100644 --- a/src/Tgstation.Server.Host/Components/Engine/IEngineInstaller.cs +++ b/src/Tgstation.Server.Host/Components/Engine/IEngineInstaller.cs @@ -27,7 +27,7 @@ namespace Tgstation.Server.Host.Components.Engine /// The optional for the operation. /// The for the operation. /// A resulting in the for the download. - ValueTask DownloadVersion(EngineVersion version, JobProgressReporter jobProgressReporter, CancellationToken cancellationToken); + ValueTask DownloadVersion(EngineVersion version, JobProgressReporter? jobProgressReporter, CancellationToken cancellationToken); /// /// Does actions necessary to get an extracted installation working. diff --git a/src/Tgstation.Server.Host/Components/Engine/IEngineManager.cs b/src/Tgstation.Server.Host/Components/Engine/IEngineManager.cs index bc272b1038..18af1469e6 100644 --- a/src/Tgstation.Server.Host/Components/Engine/IEngineManager.cs +++ b/src/Tgstation.Server.Host/Components/Engine/IEngineManager.cs @@ -18,7 +18,7 @@ namespace Tgstation.Server.Host.Components.Engine /// /// The currently active . /// - EngineVersion ActiveVersion { get; } + EngineVersion? ActiveVersion { get; } /// /// The installed s. @@ -35,9 +35,9 @@ namespace Tgstation.Server.Host.Components.Engine /// The for the operation. /// A representing the running operation. ValueTask ChangeVersion( - JobProgressReporter progressReporter, + JobProgressReporter? progressReporter, EngineVersion version, - Stream customVersionStream, + Stream? customVersionStream, bool allowInstallation, CancellationToken cancellationToken); @@ -58,8 +58,8 @@ namespace Tgstation.Server.Host.Components.Engine /// The for the operation. /// A resulting in the requested . ValueTask UseExecutables( - EngineVersion requiredVersion, - string trustDmbFullPath, + EngineVersion? requiredVersion, + string? trustDmbFullPath, CancellationToken cancellationToken); } } diff --git a/src/Tgstation.Server.Host/Components/Engine/OpenDreamInstallation.cs b/src/Tgstation.Server.Host/Components/Engine/OpenDreamInstallation.cs index 254861e69a..ab9eb923d8 100644 --- a/src/Tgstation.Server.Host/Components/Engine/OpenDreamInstallation.cs +++ b/src/Tgstation.Server.Host/Components/Engine/OpenDreamInstallation.cs @@ -1,11 +1,23 @@ using System; using System.Collections.Generic; +using System.Diagnostics; +using System.Net.Http; +using System.Net.Http.Headers; +using System.Net.Mime; +using System.Text; +using System.Threading; using System.Threading.Tasks; +using Microsoft.Extensions.Logging; + using Tgstation.Server.Api.Models; using Tgstation.Server.Api.Models.Internal; +using Tgstation.Server.Common.Http; using Tgstation.Server.Host.Components.Deployment; +using Tgstation.Server.Host.Components.Interop; using Tgstation.Server.Host.IO; +using Tgstation.Server.Host.System; +using Tgstation.Server.Host.Utils; namespace Tgstation.Server.Host.Components.Engine { @@ -40,28 +52,44 @@ namespace Tgstation.Server.Host.Components.Engine /// readonly IIOManager ioManager; + /// + /// The for the . + /// + readonly IAsyncDelayer asyncDelayer; + + /// + /// The for the . + /// + readonly IAbstractHttpClientFactory httpClientFactory; + /// /// Initializes a new instance of the class. /// /// The value of . + /// The value of . + /// The value of . /// The value of . /// The value of . /// The value of . /// The value of . public OpenDreamInstallation( IIOManager ioManager, + IAsyncDelayer asyncDelayer, + IAbstractHttpClientFactory httpClientFactory, string serverExePath, string compilerExePath, Task installationTask, EngineVersion version) { this.ioManager = ioManager ?? throw new ArgumentNullException(nameof(ioManager)); + this.asyncDelayer = asyncDelayer ?? throw new ArgumentNullException(nameof(asyncDelayer)); + this.httpClientFactory = httpClientFactory ?? throw new ArgumentNullException(nameof(httpClientFactory)); ServerExePath = serverExePath ?? throw new ArgumentNullException(nameof(serverExePath)); CompilerExePath = compilerExePath ?? throw new ArgumentNullException(nameof(compilerExePath)); InstallationTask = installationTask ?? throw new ArgumentNullException(nameof(installationTask)); Version = version ?? throw new ArgumentNullException(nameof(version)); - if (version.Engine.Value != EngineType.OpenDream) + if (version.Engine!.Value != EngineType.OpenDream) throw new ArgumentException($"Invalid EngineType: {version.Engine.Value}", nameof(version)); } @@ -70,21 +98,86 @@ namespace Tgstation.Server.Host.Components.Engine IDmbProvider dmbProvider, IReadOnlyDictionary parameters, DreamDaemonLaunchParameters launchParameters, - string logFilePath) + string? logFilePath) { ArgumentNullException.ThrowIfNull(dmbProvider); ArgumentNullException.ThrowIfNull(parameters); ArgumentNullException.ThrowIfNull(launchParameters); + if (!parameters.TryGetValue(DMApiConstants.ParamAccessIdentifier, out var accessIdentifier)) + throw new ArgumentException($"parameters must have \"{DMApiConstants.ParamAccessIdentifier}\" set!", nameof(parameters)); + var parametersString = EncodeParameters(parameters, launchParameters); - var loggingEnabled = logFilePath != null; - var arguments = $"--cvar {(loggingEnabled ? $"log.path=\"{ioManager.GetDirectoryName(logFilePath)}\" --cvar log.format=\"{ioManager.GetFileName(logFilePath)}\"" : "log.enabled=false")} --cvar log.runtimelog=false --cvar net.port={launchParameters.Port.Value} --cvar opendream.topic_port=0 --cvar opendream.world_params=\"{parametersString}\" --cvar opendream.json_path=\"./{dmbProvider.DmbName}\""; + var arguments = $"--cvar {(logFilePath != null ? $"log.path=\"{ioManager.GetDirectoryName(logFilePath)}\" --cvar log.format=\"{ioManager.GetFileName(logFilePath)}\"" : "log.enabled=false")} --cvar watchdog.token={accessIdentifier} --cvar log.runtimelog=false --cvar net.port={launchParameters.Port!.Value} --cvar opendream.topic_port=0 --cvar opendream.world_params=\"{parametersString}\" --cvar opendream.json_path=\"./{dmbProvider.DmbName}\""; return arguments; } /// public override string FormatCompilerArguments(string dmePath) => $"--suppress-unimplemented --notices-enabled \"{dmePath ?? throw new ArgumentNullException(nameof(dmePath))}\""; + + /// + public override async ValueTask StopServerProcess( + ILogger logger, + IProcess process, + string accessIdentifier, + ushort port, + CancellationToken cancellationToken) + { + const int MaximumTerminationSeconds = 5; + + logger.LogTrace("Attempting Robust.Server graceful exit (Timeout: {seconds}s)...", MaximumTerminationSeconds); + var timeout = asyncDelayer.Delay(TimeSpan.FromSeconds(MaximumTerminationSeconds), cancellationToken); + var lifetime = process.Lifetime; + + var stopwatch = Stopwatch.StartNew(); + try + { + using var httpClient = httpClientFactory.CreateClient(); + using var request = new HttpRequestMessage(); + request.Headers.Add("WatchdogToken", accessIdentifier); + request.RequestUri = new Uri($"http://localhost:{port}/shutdown"); + request.Content = new StringContent( + "{\"Reason\":\"TGS session termination\"}", + Encoding.UTF8, + new MediaTypeHeaderValue(MediaTypeNames.Application.Json)); + request.Method = HttpMethod.Post; + + var responseTask = httpClient.SendAsync(request, HttpCompletionOption.ResponseHeadersRead, cancellationToken); + try + { + await Task.WhenAny(timeout, lifetime, responseTask); + if (responseTask.IsCompleted) + { + using var response = await responseTask; + if (response.IsSuccessStatusCode) + { + logger.LogDebug("Robust.Server responded to the shutdown command successfully ({requestMs}ms). Waiting for exit...", stopwatch.ElapsedMilliseconds); + await Task.WhenAny(timeout, lifetime); + } + } + + if (!lifetime.IsCompleted) + logger.LogWarning("Robust.Server graceful exit timed out!"); + } + catch (Exception ex) when (ex is not OperationCanceledException) + { + logger.LogDebug(ex, "Unable to send graceful exit request to Robust.Server watchdog API!"); + } + + if (lifetime.IsCompleted) + { + logger.LogTrace("Robust.Server exited without termination"); + return; + } + } + finally + { + logger.LogTrace("Robust.Server graceful shutdown attempt took {totalMs}ms", stopwatch.ElapsedMilliseconds); + } + + await base.StopServerProcess(logger, process, accessIdentifier, port, cancellationToken); + } } } diff --git a/src/Tgstation.Server.Host/Components/Engine/OpenDreamInstaller.cs b/src/Tgstation.Server.Host/Components/Engine/OpenDreamInstaller.cs index b8ffc98334..a2b74e1445 100644 --- a/src/Tgstation.Server.Host/Components/Engine/OpenDreamInstaller.cs +++ b/src/Tgstation.Server.Host/Components/Engine/OpenDreamInstaller.cs @@ -8,12 +8,14 @@ using Microsoft.Extensions.Options; using Tgstation.Server.Api.Models; using Tgstation.Server.Common.Extensions; +using Tgstation.Server.Common.Http; using Tgstation.Server.Host.Common; using Tgstation.Server.Host.Components.Repository; using Tgstation.Server.Host.Configuration; using Tgstation.Server.Host.IO; using Tgstation.Server.Host.Jobs; using Tgstation.Server.Host.System; +using Tgstation.Server.Host.Utils; namespace Tgstation.Server.Host.Components.Engine { @@ -50,6 +52,16 @@ namespace Tgstation.Server.Host.Components.Engine /// protected IProcessExecutor ProcessExecutor { get; } + /// + /// The for the . + /// + protected GeneralConfiguration GeneralConfiguration { get; } + + /// + /// The for the . + /// + protected SessionConfiguration SessionConfiguration { get; } + /// /// The for the . /// @@ -61,9 +73,14 @@ namespace Tgstation.Server.Host.Components.Engine readonly IRepositoryManager repositoryManager; /// - /// The for the . + /// The for the . /// - protected GeneralConfiguration GeneralConfiguration { get; } + readonly IAsyncDelayer asyncDelayer; + + /// + /// The for the . + /// + readonly IAbstractHttpClientFactory httpClientFactory; /// /// Initializes a new instance of the class. @@ -73,20 +90,29 @@ namespace Tgstation.Server.Host.Components.Engine /// The value of . /// The value of . /// The value of . + /// The value of . + /// The value of . /// The containing value of . + /// The containing value of . public OpenDreamInstaller( IIOManager ioManager, ILogger logger, IPlatformIdentifier platformIdentifier, IProcessExecutor processExecutor, IRepositoryManager repositoryManager, - IOptions generalConfigurationOptions) + IAsyncDelayer asyncDelayer, + IAbstractHttpClientFactory httpClientFactory, + IOptions generalConfigurationOptions, + IOptions sessionConfigurationOptions) : base(ioManager, logger) { this.platformIdentifier = platformIdentifier ?? throw new ArgumentNullException(nameof(platformIdentifier)); ProcessExecutor = processExecutor ?? throw new ArgumentNullException(nameof(processExecutor)); this.repositoryManager = repositoryManager ?? throw new ArgumentNullException(nameof(repositoryManager)); + this.asyncDelayer = asyncDelayer ?? throw new ArgumentNullException(nameof(asyncDelayer)); + this.httpClientFactory = httpClientFactory ?? throw new ArgumentNullException(nameof(httpClientFactory)); GeneralConfiguration = generalConfigurationOptions?.Value ?? throw new ArgumentNullException(nameof(generalConfigurationOptions)); + SessionConfiguration = sessionConfigurationOptions?.Value ?? throw new ArgumentNullException(nameof(sessionConfigurationOptions)); } /// @@ -99,6 +125,8 @@ namespace Tgstation.Server.Host.Components.Engine GetExecutablePaths(path, out var serverExePath, out var compilerExePath); return new OpenDreamInstallation( IOManager, + asyncDelayer, + httpClientFactory, serverExePath, compilerExePath, installationTask, @@ -106,7 +134,7 @@ namespace Tgstation.Server.Host.Components.Engine } /// - public override async ValueTask DownloadVersion(EngineVersion version, JobProgressReporter jobProgressReporter, CancellationToken cancellationToken) + public override async ValueTask DownloadVersion(EngineVersion version, JobProgressReporter? jobProgressReporter, CancellationToken cancellationToken) { CheckVersionValidity(version); @@ -131,7 +159,7 @@ namespace Tgstation.Server.Host.Components.Engine Logger.LogTrace("OD repo seems to already exist, attempting load and fetch..."); repo = await repositoryManager.LoadRepository(cancellationToken); - await repo.FetchOrigin( + await repo!.FetchOrigin( progressSection1, null, null, @@ -142,7 +170,7 @@ namespace Tgstation.Server.Host.Components.Engine var progressSection2 = jobProgressReporter?.CreateSection("Checking out OpenDream version", 0.5f); var committish = version.SourceSHA - ?? $"{GeneralConfiguration.OpenDreamGitTagPrefix}{version.Version.Semver()}"; + ?? $"{GeneralConfiguration.OpenDreamGitTagPrefix}{version.Version!.Semver()}"; await repo.CheckoutObject( committish, @@ -232,11 +260,14 @@ namespace Tgstation.Server.Host.Components.Engine null, !GeneralConfiguration.OpenDreamSuppressInstallOutput, !GeneralConfiguration.OpenDreamSuppressInstallOutput); + + if (SessionConfiguration.LowPriorityDeploymentProcesses) + buildProcess.AdjustPriority(false); + using (cancellationToken.Register(() => buildProcess.Terminate())) buildExitCode = await buildProcess.Lifetime; - string output; - + string? output; if (!GeneralConfiguration.OpenDreamSuppressInstallOutput) { var buildOutputTask = buildProcess.GetCombinedOutput(cancellationToken); diff --git a/src/Tgstation.Server.Host/Components/Engine/PosixByondInstaller.cs b/src/Tgstation.Server.Host/Components/Engine/PosixByondInstaller.cs index f3c9c561b1..037cacd437 100644 --- a/src/Tgstation.Server.Host/Components/Engine/PosixByondInstaller.cs +++ b/src/Tgstation.Server.Host/Components/Engine/PosixByondInstaller.cs @@ -93,7 +93,7 @@ namespace Tgstation.Server.Host.Components.Engine var basePath = IOManager.ConcatPath(path, ByondBinPath); var ddTask = WriteAndMakeExecutable( - IOManager.ConcatPath(basePath, GetDreamDaemonName(version.Version, out _)), + IOManager.ConcatPath(basePath, GetDreamDaemonName(version.Version!, out _)), dreamDaemonScript); var dmTask = WriteAndMakeExecutable( diff --git a/src/Tgstation.Server.Host/Components/Engine/WindowsByondInstaller.cs b/src/Tgstation.Server.Host/Components/Engine/WindowsByondInstaller.cs index 0f40d3f538..87424718d5 100644 --- a/src/Tgstation.Server.Host/Components/Engine/WindowsByondInstaller.cs +++ b/src/Tgstation.Server.Host/Components/Engine/WindowsByondInstaller.cs @@ -71,6 +71,11 @@ namespace Tgstation.Server.Host.Components.Engine /// readonly GeneralConfiguration generalConfiguration; + /// + /// The for the . + /// + readonly SessionConfiguration sessionConfiguration; + /// /// The for the . /// @@ -86,6 +91,7 @@ namespace Tgstation.Server.Host.Components.Engine /// /// The value of . /// The containing the value of . + /// The containing the value of . /// The for the . /// The for the . /// The for the . @@ -94,11 +100,13 @@ namespace Tgstation.Server.Host.Components.Engine IIOManager ioManager, IFileDownloader fileDownloader, IOptions generalConfigurationOptions, + IOptions sessionConfigurationOptions, ILogger logger) : base(ioManager, logger, fileDownloader) { this.processExecutor = processExecutor ?? throw new ArgumentNullException(nameof(processExecutor)); generalConfiguration = generalConfigurationOptions?.Value ?? throw new ArgumentNullException(nameof(generalConfigurationOptions)); + sessionConfiguration = sessionConfigurationOptions?.Value ?? throw new ArgumentNullException(nameof(sessionConfigurationOptions)); var documentsDirectory = Environment.GetFolderPath( Environment.SpecialFolder.MyDocuments, @@ -120,14 +128,19 @@ namespace Tgstation.Server.Host.Components.Engine CheckVersionValidity(version); ArgumentNullException.ThrowIfNull(path); + var noPromptTrustedTask = SetNoPromptTrusted(path, cancellationToken); + var installDirectXTask = InstallDirectX(path, cancellationToken); var tasks = new List(3) { - SetNoPromptTrusted(path, cancellationToken), - InstallDirectX(path, cancellationToken), + noPromptTrustedTask, + installDirectXTask, }; if (!generalConfiguration.SkipAddingByondFirewallException) - tasks.Add(AddDreamDaemonToFirewall(version, path, cancellationToken)); + { + var firewallTask = AddDreamDaemonToFirewall(version, path, cancellationToken); + tasks.Add(firewallTask); + } return ValueTaskExtensions.WhenAll(tasks); } @@ -230,7 +243,7 @@ namespace Tgstation.Server.Host.Components.Engine /// A representing the running operation. async ValueTask AddDreamDaemonToFirewall(EngineVersion version, string path, CancellationToken cancellationToken) { - var dreamDaemonName = GetDreamDaemonName(version.Version, out var usesDDExe); + var dreamDaemonName = GetDreamDaemonName(version.Version!, out var usesDDExe); var dreamDaemonPath = IOManager.ResolvePath( IOManager.ConcatPath( @@ -251,6 +264,7 @@ namespace Tgstation.Server.Host.Components.Engine Logger, ruleName, dreamDaemonPath, + sessionConfiguration.LowPriorityDeploymentProcesses, cancellationToken); } catch (Exception ex) diff --git a/src/Tgstation.Server.Host/Components/Engine/WindowsOpenDreamInstaller.cs b/src/Tgstation.Server.Host/Components/Engine/WindowsOpenDreamInstaller.cs index c97ca10418..25968446fe 100644 --- a/src/Tgstation.Server.Host/Components/Engine/WindowsOpenDreamInstaller.cs +++ b/src/Tgstation.Server.Host/Components/Engine/WindowsOpenDreamInstaller.cs @@ -7,11 +7,13 @@ using Microsoft.Extensions.Options; using Tgstation.Server.Api.Models; using Tgstation.Server.Common.Extensions; +using Tgstation.Server.Common.Http; using Tgstation.Server.Host.Components.Repository; using Tgstation.Server.Host.Configuration; using Tgstation.Server.Host.IO; using Tgstation.Server.Host.Jobs; using Tgstation.Server.Host.System; +using Tgstation.Server.Host.Utils; namespace Tgstation.Server.Host.Components.Engine { @@ -33,7 +35,10 @@ namespace Tgstation.Server.Host.Components.Engine /// The for the . /// The for the . /// The for the . + /// The for the . + /// The for the . /// The of for the . + /// The of for the . /// The value of . public WindowsOpenDreamInstaller( IIOManager ioManager, @@ -41,7 +46,10 @@ namespace Tgstation.Server.Host.Components.Engine IPlatformIdentifier platformIdentifier, IProcessExecutor processExecutor, IRepositoryManager repositoryManager, + IAsyncDelayer asyncDelayer, + IAbstractHttpClientFactory httpClientFactory, IOptions generalConfigurationOptions, + IOptions sessionConfigurationOptions, IFilesystemLinkFactory linkFactory) : base( ioManager, @@ -49,7 +57,10 @@ namespace Tgstation.Server.Host.Components.Engine platformIdentifier, processExecutor, repositoryManager, - generalConfigurationOptions) + asyncDelayer, + httpClientFactory, + generalConfigurationOptions, + sessionConfigurationOptions) { this.linkFactory = linkFactory ?? throw new ArgumentNullException(nameof(linkFactory)); } @@ -112,6 +123,7 @@ namespace Tgstation.Server.Host.Components.Engine Logger, ruleName, serverExePath, + SessionConfiguration.LowPriorityDeploymentProcesses, cancellationToken); } catch (Exception ex) diff --git a/src/Tgstation.Server.Host/Components/Events/EventConsumer.cs b/src/Tgstation.Server.Host/Components/Events/EventConsumer.cs index 81a0d34585..559e584a27 100644 --- a/src/Tgstation.Server.Host/Components/Events/EventConsumer.cs +++ b/src/Tgstation.Server.Host/Components/Events/EventConsumer.cs @@ -19,7 +19,7 @@ namespace Tgstation.Server.Host.Components.Events /// /// The for the . /// - IWatchdog watchdog; + IWatchdog? watchdog; /// /// Initializes a new instance of the class. @@ -31,7 +31,7 @@ namespace Tgstation.Server.Host.Components.Events } /// - public async ValueTask HandleEvent(EventType eventType, IEnumerable parameters, bool deploymentPipeline, CancellationToken cancellationToken) + public async ValueTask HandleEvent(EventType eventType, IEnumerable parameters, bool deploymentPipeline, CancellationToken cancellationToken) { ArgumentNullException.ThrowIfNull(parameters); @@ -50,10 +50,9 @@ namespace Tgstation.Server.Host.Components.Events public void SetWatchdog(IWatchdog watchdog) { ArgumentNullException.ThrowIfNull(watchdog); - if (this.watchdog != null) + var oldWatchdog = Interlocked.CompareExchange(ref this.watchdog, watchdog, null); + if (oldWatchdog != null) throw new InvalidOperationException("watchdog already set!"); - - this.watchdog = watchdog; } } } diff --git a/src/Tgstation.Server.Host/Components/Events/IEventConsumer.cs b/src/Tgstation.Server.Host/Components/Events/IEventConsumer.cs index 7fddbb8950..4d268011f1 100644 --- a/src/Tgstation.Server.Host/Components/Events/IEventConsumer.cs +++ b/src/Tgstation.Server.Host/Components/Events/IEventConsumer.cs @@ -17,6 +17,6 @@ namespace Tgstation.Server.Host.Components.Events /// If this event is part of the deployment pipeline. /// The for the operation. /// A representing the running operation. - ValueTask HandleEvent(EventType eventType, IEnumerable parameters, bool deploymentPipeline, CancellationToken cancellationToken); + ValueTask HandleEvent(EventType eventType, IEnumerable parameters, bool deploymentPipeline, CancellationToken cancellationToken); } } diff --git a/src/Tgstation.Server.Host/Components/Events/NoopEventConsumer.cs b/src/Tgstation.Server.Host/Components/Events/NoopEventConsumer.cs index a8b07e4d7e..dde777572a 100644 --- a/src/Tgstation.Server.Host/Components/Events/NoopEventConsumer.cs +++ b/src/Tgstation.Server.Host/Components/Events/NoopEventConsumer.cs @@ -10,7 +10,7 @@ namespace Tgstation.Server.Host.Components.Events sealed class NoopEventConsumer : IEventConsumer { /// - public ValueTask HandleEvent(EventType eventType, IEnumerable parameters, bool deploymentPipeline, CancellationToken cancellationToken) + public ValueTask HandleEvent(EventType eventType, IEnumerable parameters, bool deploymentPipeline, CancellationToken cancellationToken) => ValueTask.CompletedTask; } } diff --git a/src/Tgstation.Server.Host/Components/IInstanceCoreProvider.cs b/src/Tgstation.Server.Host/Components/IInstanceCoreProvider.cs index 7e6b0b8dc5..ab1db54e91 100644 --- a/src/Tgstation.Server.Host/Components/IInstanceCoreProvider.cs +++ b/src/Tgstation.Server.Host/Components/IInstanceCoreProvider.cs @@ -10,6 +10,6 @@ /// /// The to get the for. /// The if it is online, otherwise. - IInstanceCore GetInstance(Models.Instance instance); + IInstanceCore? GetInstance(Models.Instance instance); } } diff --git a/src/Tgstation.Server.Host/Components/IInstanceManager.cs b/src/Tgstation.Server.Host/Components/IInstanceManager.cs index 3174686efe..516b2a8f30 100644 --- a/src/Tgstation.Server.Host/Components/IInstanceManager.cs +++ b/src/Tgstation.Server.Host/Components/IInstanceManager.cs @@ -19,6 +19,6 @@ namespace Tgstation.Server.Host.Components ///
/// The of the desired . /// The associated with the given if it is online, otherwise. - IInstanceReference GetInstanceReference(Api.Models.Instance metadata); + IInstanceReference? GetInstanceReference(Api.Models.Instance metadata); } } diff --git a/src/Tgstation.Server.Host/Components/Instance.cs b/src/Tgstation.Server.Host/Components/Instance.cs index 7176e8f050..a8b9a40721 100644 --- a/src/Tgstation.Server.Host/Components/Instance.cs +++ b/src/Tgstation.Server.Host/Components/Instance.cs @@ -93,12 +93,12 @@ namespace Tgstation.Server.Host.Components /// /// The auto update . /// - Task timerTask; + Task? timerTask; /// /// for . /// - CancellationTokenSource timerCts; + CancellationTokenSource? timerCts; /// /// Initializes a new instance of the class. @@ -183,7 +183,7 @@ namespace Tgstation.Server.Host.Components using (LogContext.PushProperty(SerilogContextHelper.InstanceIdContextProperty, metadata.Id)) { await Task.WhenAll( - SetAutoUpdateInterval(metadata.AutoUpdateInterval.Value).AsTask(), + SetAutoUpdateInterval(metadata.Require(x => x.AutoUpdateInterval)).AsTask(), Configuration.StartAsync(cancellationToken), EngineManager.StartAsync(cancellationToken), Chat.StartAsync(cancellationToken), @@ -221,7 +221,7 @@ namespace Tgstation.Server.Host.Components if (timerTask != null) { logger.LogTrace("Cancelling auto-update task"); - timerCts.Cancel(); + timerCts!.Cancel(); timerCts.Dispose(); toWait = timerTask; timerTask = null; @@ -253,7 +253,7 @@ namespace Tgstation.Server.Host.Components } /// - public CompileJob LatestCompileJob() => dmbFactory.LatestCompileJob(); + public CompileJob? LatestCompileJob() => dmbFactory.LatestCompileJob(); /// /// The for updating the repository. @@ -266,7 +266,7 @@ namespace Tgstation.Server.Host.Components /// A representing the running operation. #pragma warning disable CA1502 // Cyclomatic complexity ValueTask RepositoryAutoUpdateJob( - IInstanceCore core, + IInstanceCore? core, IDatabaseContextFactory databaseContextFactory, Job job, JobProgressReporter progressReporter, @@ -315,18 +315,18 @@ namespace Tgstation.Server.Host.Components cancellationToken); var hasDbChanges = false; - RevisionInformation currentRevInfo = null; - Models.Instance attachedInstance = null; - async ValueTask UpdateRevInfo(string currentHead, bool onOrigin, IEnumerable updatedTestMerges) + RevisionInformation? currentRevInfo = null; + Models.Instance? attachedInstance = null; + async ValueTask UpdateRevInfo(string currentHead, bool onOrigin, IEnumerable? updatedTestMerges) { if (currentRevInfo == null) { logger.LogTrace("Loading revision info for commit {sha}...", startSha[..7]); currentRevInfo = await databaseContext - .RevisionInformations + .RevisionInformations .AsQueryable() - .Where(x => x.CommitSha == startSha && x.Instance.Id == metadata.Id) - .Include(x => x.ActiveTestMerges) + .Where(x => x.CommitSha == startSha && x.InstanceId == metadata.Id) + .Include(x => x.ActiveTestMerges!) .ThenInclude(x => x.TestMerge) .FirstOrDefaultAsync(cancellationToken); } @@ -364,12 +364,9 @@ namespace Tgstation.Server.Host.Components if (!onOrigin) { - var testMerges = updatedTestMerges ?? oldRevInfo.ActiveTestMerges.Select(x => x.TestMerge); + var testMerges = updatedTestMerges ?? oldRevInfo!.ActiveTestMerges!.Select(x => x.TestMerge); var revInfoTestMerges = testMerges.Select( - testMerge => new RevInfoTestMerge - { - TestMerge = testMerge, - }) + testMerge => new RevInfoTestMerge(testMerge, currentRevInfo)) .ToList(); currentRevInfo.ActiveTestMerges = revInfoTestMerges; @@ -382,21 +379,21 @@ namespace Tgstation.Server.Host.Components // build current commit data if it's missing await UpdateRevInfo(repo.Head, false, null); - var preserveTestMerges = repositorySettings.AutoUpdatesKeepTestMerges.Value; + var preserveTestMerges = repositorySettings.AutoUpdatesKeepTestMerges!.Value; var remoteDeploymentManager = remoteDeploymentManagerFactory.CreateRemoteDeploymentManager( metadata, - repo.RemoteGitProvider.Value); + repo.RemoteGitProvider!.Value); var updatedTestMerges = await remoteDeploymentManager.RemoveMergedTestMerges( repo, repositorySettings, - currentRevInfo, + currentRevInfo!, cancellationToken); var result = await repo.MergeOrigin( NextProgressReporter("Merge Origin"), - repositorySettings.CommitterName, - repositorySettings.CommitterEmail, + repositorySettings.CommitterName!, + repositorySettings.CommitterEmail!, true, cancellationToken); @@ -435,7 +432,7 @@ namespace Tgstation.Server.Host.Components NextProgressReporter(StageName), repositorySettings.AccessUser, repositorySettings.AccessToken, - repositorySettings.UpdateSubmodules.Value, + repositorySettings.UpdateSubmodules!.Value, true, cancellationToken); @@ -443,7 +440,7 @@ namespace Tgstation.Server.Host.Components currentRevInfo = await databaseContext.RevisionInformations .AsQueryable() - .Where(x => x.CommitSha == currentHead && x.Instance.Id == metadata.Id) + .Where(x => x.CommitSha == currentHead && x.InstanceId == metadata.Id) .FirstOrDefaultAsync(cancellationToken); if (currentHead != startSha && currentRevInfo == default) @@ -453,19 +450,19 @@ namespace Tgstation.Server.Host.Components } // synch if necessary - if (repositorySettings.AutoUpdatesSynchronize.Value && startSha != repo.Head && (shouldSyncTracked || repositorySettings.PushTestMergeCommits.Value)) + if (repositorySettings.AutoUpdatesSynchronize!.Value && startSha != repo.Head && (shouldSyncTracked || repositorySettings.PushTestMergeCommits!.Value)) { - var pushedOrigin = await repo.Sychronize( + var pushedOrigin = await repo.Synchronize( NextProgressReporter("Synchronize"), repositorySettings.AccessUser, repositorySettings.AccessToken, - repositorySettings.CommitterName, - repositorySettings.CommitterEmail, + repositorySettings.CommitterName!, + repositorySettings.CommitterEmail!, shouldSyncTracked, true, cancellationToken); var currentHead = repo.Head; - if (currentHead != currentRevInfo.CommitSha) + if (currentHead != currentRevInfo!.CommitSha) await UpdateRevInfo(currentHead, pushedOrigin, null); } @@ -517,6 +514,9 @@ namespace Tgstation.Server.Host.Components Job compileProcessJob; using (var repo = await RepositoryManager.LoadRepository(cancellationToken)) { + if (repo == null) + throw new JobException(Api.Models.ErrorCode.RepoMissing); + var deploySha = repo.Head; if (deploySha == null) { diff --git a/src/Tgstation.Server.Host/Components/InstanceFactory.cs b/src/Tgstation.Server.Host/Components/InstanceFactory.cs index a2e9dc6ff5..2b24bcb6cb 100644 --- a/src/Tgstation.Server.Host/Components/InstanceFactory.cs +++ b/src/Tgstation.Server.Host/Components/InstanceFactory.cs @@ -150,7 +150,7 @@ namespace Tgstation.Server.Host.Components /// /// The instance's . /// The for the instance's "Game" directory. - static IIOManager CreateGameIOManager(IIOManager instanceIOManager) => new ResolvingIOManager(instanceIOManager, "Game"); + static ResolvingIOManager CreateGameIOManager(IIOManager instanceIOManager) => new(instanceIOManager, "Game"); #pragma warning disable CA1502 // TODO: Decomplexify /// @@ -324,13 +324,13 @@ namespace Tgstation.Server.Host.Components configuration, // watchdog doesn't need itself as an event consumer remoteDeploymentManagerFactory, metadata, - metadata.DreamDaemonSettings); + metadata.DreamDaemonSettings!); try { eventConsumer.SetWatchdog(watchdog); commandFactory.SetWatchdog(watchdog); - Instance instance = null; + Instance? instance = null; var dreamMaker = new DreamMaker( engineManager, gameIoManager, @@ -404,6 +404,6 @@ namespace Tgstation.Server.Host.Components /// /// The . /// The for the . - IIOManager CreateInstanceIOManager(Models.Instance metadata) => new ResolvingIOManager(ioManager, metadata.Path); + ResolvingIOManager CreateInstanceIOManager(Models.Instance metadata) => new(ioManager, metadata.Path!); } } diff --git a/src/Tgstation.Server.Host/Components/InstanceManager.cs b/src/Tgstation.Server.Host/Components/InstanceManager.cs index 0959e9ca01..613b9d7800 100644 --- a/src/Tgstation.Server.Host/Components/InstanceManager.cs +++ b/src/Tgstation.Server.Host/Components/InstanceManager.cs @@ -21,6 +21,7 @@ using Tgstation.Server.Host.Database; using Tgstation.Server.Host.Extensions; using Tgstation.Server.Host.IO; using Tgstation.Server.Host.Jobs; +using Tgstation.Server.Host.Models; using Tgstation.Server.Host.Security; using Tgstation.Server.Host.Swarm; using Tgstation.Server.Host.System; @@ -115,7 +116,7 @@ namespace Tgstation.Server.Host.Components readonly Dictionary bridgeHandlers; /// - /// used to guard calls to and . + /// used to guard calls to and . /// readonly SemaphoreSlim instanceStateChangeSemaphore; @@ -147,12 +148,12 @@ namespace Tgstation.Server.Host.Components /// /// The original of . /// - readonly string originalConsoleTitle; + readonly string? originalConsoleTitle; /// /// The returned by . /// - Task startupTask; + Task? startupTask; /// /// If the has been 'd. @@ -241,13 +242,13 @@ namespace Tgstation.Server.Host.Components } /// - public IInstanceReference GetInstanceReference(Api.Models.Instance metadata) + public IInstanceReference? GetInstanceReference(Api.Models.Instance metadata) { ArgumentNullException.ThrowIfNull(metadata); lock (instances) { - if (!instances.TryGetValue(metadata.Id.Value, out var instance)) + if (!instances.TryGetValue(metadata.Require(x => x.Id), out var instance)) return null; return instance.AddReference(); @@ -263,7 +264,7 @@ namespace Tgstation.Server.Host.Components using var instanceReferenceCheck = GetInstanceReference(instance); if (instanceReferenceCheck != null) throw new InvalidOperationException("Cannot move an online instance!"); - var newPath = instance.Path; + var newPath = instance.Path!; try { await ioManager.MoveDirectory(oldPath, newPath, cancellationToken); @@ -326,22 +327,23 @@ namespace Tgstation.Server.Host.Components } /// - public async ValueTask OfflineInstance(Models.Instance metadata, Models.User user, CancellationToken cancellationToken) + public async ValueTask OfflineInstance(Models.Instance metadata, User user, CancellationToken cancellationToken) { ArgumentNullException.ThrowIfNull(metadata); using (await SemaphoreSlimContext.Lock(instanceStateChangeSemaphore, cancellationToken)) { - ReferenceCountingContainer container; + ReferenceCountingContainer? container; + var instanceId = metadata.Require(x => x.Id); lock (instances) { - if (!instances.TryGetValue(metadata.Id.Value, out container)) + if (!instances.TryGetValue(instanceId, out container)) { logger.LogDebug("Not offlining removed instance {instanceId}", metadata.Id); return; } - instances.Remove(metadata.Id.Value); + instances.Remove(instanceId); } logger.LogInformation("Offlining instance ID {instanceId}", metadata.Id); @@ -351,27 +353,29 @@ namespace Tgstation.Server.Host.Components await container.OnZeroReferences.WaitAsync(cancellationToken); // we are the one responsible for cancelling his jobs - var tasks = new List>(); + ValueTask groupedTask = default; await databaseContextFactory.UseContext( async db => { var jobs = await db .Jobs .AsQueryable() - .Where(x => x.Instance.Id == metadata.Id && !x.StoppedAt.HasValue) - .Select(x => new Models.Job(x.Id.Value)) + .Where(x => x.Instance!.Id == metadata.Id && !x.StoppedAt.HasValue) + .Select(x => new Job(x.Id!.Value)) .ToListAsync(cancellationToken); - foreach (var job in jobs) - tasks.Add(jobService.CancelJob(job, user, true, cancellationToken)); + + groupedTask = ValueTaskExtensions.WhenAll( + jobs.Select(job => jobService.CancelJob(job, user, true, cancellationToken)), + jobs.Count); }); - await ValueTaskExtensions.WhenAll(tasks); + await groupedTask; } catch { // not too late to change your mind lock (instances) - instances.Add(metadata.Id.Value, container); + instances.Add(instanceId, container); throw; } @@ -393,9 +397,10 @@ namespace Tgstation.Server.Host.Components { ArgumentNullException.ThrowIfNull(metadata); + var instanceId = metadata.Require(x => x.Id); using var lockContext = await SemaphoreSlimContext.Lock(instanceStateChangeSemaphore, cancellationToken); lock (instances) - if (instances.ContainsKey(metadata.Id.Value)) + if (instances.ContainsKey(instanceId)) { logger.LogDebug("Aborting instance creation due to it seemingly already being online"); return; @@ -411,7 +416,7 @@ namespace Tgstation.Server.Host.Components { lock (instances) instances.Add( - metadata.Id.Value, + instanceId, new ReferenceCountingContainer(instance)); } catch (Exception ex) @@ -453,6 +458,12 @@ namespace Tgstation.Server.Host.Components using (cancellationToken.Register(shutdownCancellationTokenSource.Cancel)) try { + if (startupTask == null) + { + logger.LogWarning("InstanceManager was never started!"); + return; + } + logger.LogDebug("Stopping instance manager..."); if (!startupTask.IsCompleted) @@ -485,7 +496,7 @@ namespace Tgstation.Server.Host.Components finally { if (originalConsoleTitle != null) - console.Title = originalConsoleTitle; + console.SetTitle(originalConsoleTitle); } } catch (Exception ex) @@ -495,18 +506,25 @@ namespace Tgstation.Server.Host.Components } /// - public async ValueTask ProcessBridgeRequest(BridgeParameters parameters, CancellationToken cancellationToken) + public async ValueTask ProcessBridgeRequest(BridgeParameters parameters, CancellationToken cancellationToken) { ArgumentNullException.ThrowIfNull(parameters); - IBridgeHandler bridgeHandler = null; + var accessIdentifier = parameters.AccessIdentifier; + if (accessIdentifier == null) + { + logger.LogWarning("Received invalid bridge request with null access identifier!"); + return null; + } + + IBridgeHandler? bridgeHandler = null; for (var i = 0; bridgeHandler == null && i < 30; ++i) { // There's a miniscule time period where we could potentially receive a bridge request and not have the registration ready when we launch DD // This is a stopgap Task delayTask = Task.CompletedTask; lock (bridgeHandlers) - if (!bridgeHandlers.TryGetValue(parameters.AccessIdentifier, out bridgeHandler)) + if (!bridgeHandlers.TryGetValue(accessIdentifier, out bridgeHandler)) delayTask = asyncDelayer.Delay(TimeSpan.FromMilliseconds(100), cancellationToken); await delayTask; @@ -514,9 +532,9 @@ namespace Tgstation.Server.Host.Components if (bridgeHandler == null) lock (bridgeHandlers) - if (!bridgeHandlers.TryGetValue(parameters.AccessIdentifier, out bridgeHandler)) + if (!bridgeHandlers.TryGetValue(accessIdentifier, out bridgeHandler)) { - logger.LogWarning("Received invalid bridge request with access identifier: {accessIdentifier}", parameters.AccessIdentifier); + logger.LogWarning("Received invalid bridge request with access identifier: {accessIdentifier}", accessIdentifier); return null; } @@ -528,7 +546,8 @@ namespace Tgstation.Server.Host.Components { ArgumentNullException.ThrowIfNull(bridgeHandler); - var accessIdentifier = bridgeHandler.DMApiParameters.AccessIdentifier; + var accessIdentifier = bridgeHandler.DMApiParameters.AccessIdentifier + ?? throw new InvalidOperationException("Attempted bridge registration with null AccessIdentifier!"); lock (bridgeHandlers) { bridgeHandlers.Add(accessIdentifier, bridgeHandler); @@ -546,11 +565,11 @@ namespace Tgstation.Server.Host.Components } /// - public IInstanceCore GetInstance(Models.Instance metadata) + public IInstanceCore? GetInstance(Models.Instance metadata) { lock (instances) { - instances.TryGetValue(metadata.Id.Value, out var container); + instances.TryGetValue(metadata.Require(x => x.Id), out var container); return container?.Instance; } } @@ -565,7 +584,7 @@ namespace Tgstation.Server.Host.Components try { logger.LogInformation("{versionString}", assemblyInformationProvider.VersionString); - console.Title = assemblyInformationProvider.VersionString; + console.SetTitle(assemblyInformationProvider.VersionString); CheckSystemCompatibility(); @@ -574,13 +593,13 @@ namespace Tgstation.Server.Host.Components await InitializeSwarm(cancellationToken); - List dbInstances = null; + List? dbInstances = null; async ValueTask EnumerateInstances(IDatabaseContext databaseContext) => dbInstances = await databaseContext .Instances .AsQueryable() - .Where(x => x.Online.Value && x.SwarmIdentifer == swarmConfiguration.Identifier) + .Where(x => x.Online!.Value && x.SwarmIdentifer == swarmConfiguration.Identifier) .Include(x => x.RepositorySettings) .Include(x => x.ChatSettings) .ThenInclude(x => x.Channels) @@ -594,7 +613,7 @@ namespace Tgstation.Server.Host.Components await Task.WhenAll(instanceEnumeration.AsTask(), factoryStartup, jobManagerStartup); - var instanceOnliningTasks = dbInstances.Select( + var instanceOnliningTasks = dbInstances!.Select( async metadata => { try @@ -609,10 +628,11 @@ namespace Tgstation.Server.Host.Components await Task.WhenAll(instanceOnliningTasks); - jobService.Activate(this); - logger.LogInformation("Server ready!"); readyTcs.SetResult(); + + // this needs to happen after the HTTP API opens with readyTcs otherwise it can race and cause failed bridge requests with 503's + jobService.Activate(this); } catch (OperationCanceledException ex) { diff --git a/src/Tgstation.Server.Host/Components/InstanceWrapper.cs b/src/Tgstation.Server.Host/Components/InstanceWrapper.cs index 5c525d9869..ba2c84d688 100644 --- a/src/Tgstation.Server.Host/Components/InstanceWrapper.cs +++ b/src/Tgstation.Server.Host/Components/InstanceWrapper.cs @@ -58,6 +58,6 @@ namespace Tgstation.Server.Host.Components public ValueTask SetAutoUpdateInterval(uint newInterval) => Instance.SetAutoUpdateInterval(newInterval); /// - public CompileJob LatestCompileJob() => Instance.LatestCompileJob(); + public CompileJob? LatestCompileJob() => Instance.LatestCompileJob(); } } diff --git a/src/Tgstation.Server.Host/Components/Interop/Bridge/BridgeParameters.cs b/src/Tgstation.Server.Host/Components/Interop/Bridge/BridgeParameters.cs index c1c9213063..1240bc81cb 100644 --- a/src/Tgstation.Server.Host/Components/Interop/Bridge/BridgeParameters.cs +++ b/src/Tgstation.Server.Host/Components/Interop/Bridge/BridgeParameters.cs @@ -24,12 +24,12 @@ namespace Tgstation.Server.Host.Components.Interop.Bridge /// /// The DMAPI for requests. /// - public Version Version { get; set; } + public Version? Version { get; set; } /// /// The DMAPI s for requests. /// - public ICollection CustomCommands { get; set; } + public ICollection? CustomCommands { get; set; } /// /// The minimum required level for requests. @@ -39,16 +39,25 @@ namespace Tgstation.Server.Host.Components.Interop.Bridge /// /// The for requests. /// - public ChatMessage ChatMessage { get; set; } + public ChatMessage? ChatMessage { get; set; } /// /// The for requests. /// - public ChunkData Chunk { get; set; } + public ChunkData? Chunk { get; set; } /// /// The port that should be used to send world topics, if not the default. /// public ushort? TopicPort { get; set; } + + /// + /// Initializes a new instance of the class. + /// + /// The access identifier for the . + public BridgeParameters(string accessIdentifier) + : base(accessIdentifier) + { + } } } diff --git a/src/Tgstation.Server.Host/Components/Interop/Bridge/BridgeRegistration.cs b/src/Tgstation.Server.Host/Components/Interop/Bridge/BridgeRegistration.cs index ade7d5adfe..1ba9673ee6 100644 --- a/src/Tgstation.Server.Host/Components/Interop/Bridge/BridgeRegistration.cs +++ b/src/Tgstation.Server.Host/Components/Interop/Bridge/BridgeRegistration.cs @@ -1,38 +1,19 @@ using System; +using Tgstation.Server.Host.Utils; + namespace Tgstation.Server.Host.Components.Interop.Bridge { /// - sealed class BridgeRegistration : IBridgeRegistration + sealed class BridgeRegistration : DisposeInvoker, IBridgeRegistration { - /// - /// for accessing . - /// - readonly object lockObject; - - /// - /// to run when d. - /// - Action onDispose; - /// /// Initializes a new instance of the class. /// - /// The value of . - public BridgeRegistration(Action onDispose) + /// The action for the . + public BridgeRegistration(Action disposeAction) + : base(disposeAction) { - this.onDispose = onDispose ?? throw new ArgumentNullException(nameof(onDispose)); - lockObject = new object(); - } - - /// - public void Dispose() - { - lock (lockObject) - { - onDispose?.Invoke(); - onDispose = null; - } } } } diff --git a/src/Tgstation.Server.Host/Components/Interop/Bridge/BridgeResponse.cs b/src/Tgstation.Server.Host/Components/Interop/Bridge/BridgeResponse.cs index f7f358d612..620241fafb 100644 --- a/src/Tgstation.Server.Host/Components/Interop/Bridge/BridgeResponse.cs +++ b/src/Tgstation.Server.Host/Components/Interop/Bridge/BridgeResponse.cs @@ -15,11 +15,11 @@ namespace Tgstation.Server.Host.Components.Interop.Bridge /// /// The for requests. /// - public RuntimeInformation RuntimeInformation { get; set; } + public RuntimeInformation? RuntimeInformation { get; set; } /// /// The s missing from a chunked request. /// - public IReadOnlyCollection MissingChunks { get; set; } + public IReadOnlyCollection? MissingChunks { get; set; } } } diff --git a/src/Tgstation.Server.Host/Components/Interop/Bridge/IBridgeDispatcher.cs b/src/Tgstation.Server.Host/Components/Interop/Bridge/IBridgeDispatcher.cs index 82de2d6631..8035583442 100644 --- a/src/Tgstation.Server.Host/Components/Interop/Bridge/IBridgeDispatcher.cs +++ b/src/Tgstation.Server.Host/Components/Interop/Bridge/IBridgeDispatcher.cs @@ -14,6 +14,6 @@ namespace Tgstation.Server.Host.Components.Interop.Bridge /// The to handle. /// The for the operation. /// A resulting in the for the request or if the request could not be dispatched. - ValueTask ProcessBridgeRequest(BridgeParameters parameters, CancellationToken cancellationToken); + ValueTask ProcessBridgeRequest(BridgeParameters parameters, CancellationToken cancellationToken); } } diff --git a/src/Tgstation.Server.Host/Components/Interop/Bridge/RuntimeInformation.cs b/src/Tgstation.Server.Host/Components/Interop/Bridge/RuntimeInformation.cs index b7d05b67d7..a011dad75a 100644 --- a/src/Tgstation.Server.Host/Components/Interop/Bridge/RuntimeInformation.cs +++ b/src/Tgstation.Server.Host/Components/Interop/Bridge/RuntimeInformation.cs @@ -91,11 +91,11 @@ namespace Tgstation.Server.Host.Components.Interop.Bridge OriginCommitSha = dmbProvider.CompileJob.RevisionInformation.OriginCommitSha, }; - TestMerges = (IReadOnlyCollection)dmbProvider + TestMerges = (IReadOnlyCollection?)dmbProvider .CompileJob .RevisionInformation - .ActiveTestMerges? - .Select(x => x.TestMerge) + .ActiveTestMerges + ?.Select(x => x.TestMerge) .Select(x => new TestMergeInformation(x, Revision)) .ToList() ?? Array.Empty(); diff --git a/src/Tgstation.Server.Host/Components/Interop/Bridge/TestMergeInformation.cs b/src/Tgstation.Server.Host/Components/Interop/Bridge/TestMergeInformation.cs index 1270af68cf..4537c56bd9 100644 --- a/src/Tgstation.Server.Host/Components/Interop/Bridge/TestMergeInformation.cs +++ b/src/Tgstation.Server.Host/Components/Interop/Bridge/TestMergeInformation.cs @@ -18,10 +18,10 @@ namespace Tgstation.Server.Host.Components.Interop.Bridge /// /// Backing field for needed to continue to support DMAPI 5. /// - public string PullRequestRevision { get; set; } + public string? PullRequestRevision { get; set; } /// - public override string TargetCommitSha + public override string? TargetCommitSha { get => PullRequestRevision; set => PullRequestRevision = value; diff --git a/src/Tgstation.Server.Host/Components/Interop/ChatEmbed.cs b/src/Tgstation.Server.Host/Components/Interop/ChatEmbed.cs index da3518ec2a..41563191f3 100644 --- a/src/Tgstation.Server.Host/Components/Interop/ChatEmbed.cs +++ b/src/Tgstation.Server.Host/Components/Interop/ChatEmbed.cs @@ -10,63 +10,63 @@ namespace Tgstation.Server.Host.Components.Interop /// /// The title of the embed. /// - public string Title { get; set; } + public string? Title { get; set; } /// /// The description of the embed. /// - public string Description { get; set; } + public string? Description { get; set; } /// /// The URL of the embed. /// #pragma warning disable CA1056 // Uri properties should not be strings - public string Url { get; set; } + public string? Url { get; set; } #pragma warning restore CA1056 // Uri properties should not be strings /// /// The ISO 8601 timestamp of the embed. /// - public string Timestamp { get; set; } + public string? Timestamp { get; set; } /// /// The colour of the embed in the format hex "#AARRGGBB". /// - public string Colour { get; set; } + public string? Colour { get; set; } /// /// The . /// - public ChatEmbedFooter Footer { get; set; } + public ChatEmbedFooter? Footer { get; set; } /// /// The for an image. /// - public ChatEmbedMedia Image { get; set; } + public ChatEmbedMedia? Image { get; set; } /// /// The for a thumbnail. /// - public ChatEmbedMedia Thumbnail { get; set; } + public ChatEmbedMedia? Thumbnail { get; set; } /// /// The for a video. /// - public ChatEmbedMedia Video { get; set; } + public ChatEmbedMedia? Video { get; set; } /// /// The . /// - public ChatEmbedProvider Provider { get; set; } + public ChatEmbedProvider? Provider { get; set; } /// /// The . /// - public ChatEmbedAuthor Author { get; set; } + public ChatEmbedAuthor? Author { get; set; } /// /// The s. /// - public ICollection Fields { get; set; } + public ICollection? Fields { get; set; } } } diff --git a/src/Tgstation.Server.Host/Components/Interop/ChatEmbedAuthor.cs b/src/Tgstation.Server.Host/Components/Interop/ChatEmbedAuthor.cs index b79d38be5b..d136b895b0 100644 --- a/src/Tgstation.Server.Host/Components/Interop/ChatEmbedAuthor.cs +++ b/src/Tgstation.Server.Host/Components/Interop/ChatEmbedAuthor.cs @@ -9,12 +9,12 @@ /// Gets the icon URL of the author. /// #pragma warning disable CA1056 // Uri properties should not be strings - public string IconUrl { get; set; } + public string? IconUrl { get; set; } /// /// Gets the proxied icon URL of the thumbnail. /// - public string ProxyIconUrl { get; set; } + public string? ProxyIconUrl { get; set; } #pragma warning restore CA1056 // Uri properties should not be strings } } diff --git a/src/Tgstation.Server.Host/Components/Interop/ChatEmbedField.cs b/src/Tgstation.Server.Host/Components/Interop/ChatEmbedField.cs index 9d3ca0b864..70b3c37c9f 100644 --- a/src/Tgstation.Server.Host/Components/Interop/ChatEmbedField.cs +++ b/src/Tgstation.Server.Host/Components/Interop/ChatEmbedField.cs @@ -8,12 +8,12 @@ /// /// Gets the name of the field. /// - public string Name { get; set; } + public string? Name { get; set; } /// /// Gets the value of the field. /// - public string Value { get; set; } + public string? Value { get; set; } /// /// Gets a value indicating whether the field should display inline. diff --git a/src/Tgstation.Server.Host/Components/Interop/ChatEmbedFooter.cs b/src/Tgstation.Server.Host/Components/Interop/ChatEmbedFooter.cs index 0df7a2c5d9..57d45ec7eb 100644 --- a/src/Tgstation.Server.Host/Components/Interop/ChatEmbedFooter.cs +++ b/src/Tgstation.Server.Host/Components/Interop/ChatEmbedFooter.cs @@ -8,18 +8,18 @@ /// /// Gets the text of the footer. /// - public string Text { get; set; } + public string? Text { get; set; } /// /// Gets the URL of the footer icon. Only supports http(s) and attachments. /// #pragma warning disable CA1056 // Uri properties should not be strings - public string IconUrl { get; set; } + public string? IconUrl { get; set; } /// /// Gets the proxied icon URL. /// - public string ProxyIconUrl { get; set; } + public string? ProxyIconUrl { get; set; } #pragma warning restore CA1056 // Uri properties should not be strings } } diff --git a/src/Tgstation.Server.Host/Components/Interop/ChatEmbedMedia.cs b/src/Tgstation.Server.Host/Components/Interop/ChatEmbedMedia.cs index 1cad4c4349..c031a01e92 100644 --- a/src/Tgstation.Server.Host/Components/Interop/ChatEmbedMedia.cs +++ b/src/Tgstation.Server.Host/Components/Interop/ChatEmbedMedia.cs @@ -9,12 +9,12 @@ /// Gets the source URL of the media. Only supports http(s) and attachments. /// #pragma warning disable CA1056 // Uri properties should not be strings - public string Url { get; set; } + public string? Url { get; set; } /// /// Gets the proxied URL of the media. /// - public string ProxyUrl { get; set; } + public string? ProxyUrl { get; set; } #pragma warning restore CA1056 // Uri properties should not be strings /// diff --git a/src/Tgstation.Server.Host/Components/Interop/ChatEmbedProvider.cs b/src/Tgstation.Server.Host/Components/Interop/ChatEmbedProvider.cs index 386a269a7d..bc4dba30d7 100644 --- a/src/Tgstation.Server.Host/Components/Interop/ChatEmbedProvider.cs +++ b/src/Tgstation.Server.Host/Components/Interop/ChatEmbedProvider.cs @@ -8,13 +8,13 @@ /// /// Gets the name of the provider. /// - public string Name { get; set; } + public string? Name { get; set; } /// /// Gets the URL of the provider. /// #pragma warning disable CA1056 // Uri properties should not be strings - public string Url { get; set; } + public string? Url { get; set; } #pragma warning restore CA1056 // Uri properties should not be strings } } diff --git a/src/Tgstation.Server.Host/Components/Interop/ChatMessage.cs b/src/Tgstation.Server.Host/Components/Interop/ChatMessage.cs index 3d8900e427..c84e44e6cc 100644 --- a/src/Tgstation.Server.Host/Components/Interop/ChatMessage.cs +++ b/src/Tgstation.Server.Host/Components/Interop/ChatMessage.cs @@ -10,6 +10,6 @@ namespace Tgstation.Server.Host.Components.Interop /// /// The of s to sent the to. Must be safe to parse as s. /// - public ICollection ChannelIds { get; set; } + public ICollection? ChannelIds { get; set; } } } diff --git a/src/Tgstation.Server.Host/Components/Interop/ChunkData.cs b/src/Tgstation.Server.Host/Components/Interop/ChunkData.cs index ee6122d764..409179b660 100644 --- a/src/Tgstation.Server.Host/Components/Interop/ChunkData.cs +++ b/src/Tgstation.Server.Host/Components/Interop/ChunkData.cs @@ -14,6 +14,6 @@ /// /// The partial JSON payload of the chunk. /// - public string Payload { get; set; } + public string? Payload { get; set; } } } diff --git a/src/Tgstation.Server.Host/Components/Interop/Chunker.cs b/src/Tgstation.Server.Host/Components/Interop/Chunker.cs index 85f6de0d6c..e13cfd00ba 100644 --- a/src/Tgstation.Server.Host/Components/Interop/Chunker.cs +++ b/src/Tgstation.Server.Host/Components/Interop/Chunker.cs @@ -14,6 +14,11 @@ namespace Tgstation.Server.Host.Components.Interop /// abstract class Chunker { + /// + /// The for the . + /// + protected ILogger Logger { get; } + /// /// Gets a payload ID for use in a new . /// @@ -39,11 +44,6 @@ namespace Tgstation.Server.Host.Components.Interop /// uint highestSeenPayloadId; - /// - /// The for the . - /// - protected ILogger Logger { get; } - /// /// Initializes a new instance of the class. /// @@ -64,11 +64,12 @@ namespace Tgstation.Server.Host.Components.Interop /// The . /// The for the operation. /// A resulting in the for the chunked request. - protected async ValueTask ProcessChunk( - Func> completionCallback, - Func chunkErrorCallback, - ChunkData chunk, + protected async ValueTask ProcessChunk( + Func> completionCallback, + Func chunkErrorCallback, + ChunkData? chunk, CancellationToken cancellationToken) + where TCommunication : class where TResponse : IMissingPayloadsCommunication, new() { if (chunk == null) @@ -80,6 +81,9 @@ namespace Tgstation.Server.Host.Components.Interop if (!chunk.SequenceId.HasValue) return chunkErrorCallback("Missing chunk sequenceId!"); + if (chunk.Payload == null) + return chunkErrorCallback("Missing chunk payload!"); + ChunkSetInfo requestInfo; string[] payloads; lock (chunkSets) @@ -102,13 +106,13 @@ namespace Tgstation.Server.Host.Components.Interop if (chunk.TotalChunks != requestInfo.TotalChunks) { - chunkSets.Remove(requestInfo.PayloadId.Value); + chunkSets.Remove(requestInfo.PayloadId!.Value); return chunkErrorCallback("Received differing total chunks for same payloadId! Invalidating payloadId!"); } if (payloads[chunk.SequenceId.Value] != null && payloads[chunk.SequenceId.Value] != chunk.Payload) { - chunkSets.Remove(requestInfo.PayloadId.Value); + chunkSets.Remove(requestInfo.PayloadId!.Value); return chunkErrorCallback("Received differing payload for same sequenceId! Invalidating payloadId!"); } @@ -125,10 +129,10 @@ namespace Tgstation.Server.Host.Components.Interop }; Logger.LogTrace("Received all chunks for P{payloadId}, processing request...", requestInfo.PayloadId); - chunkSets.Remove(requestInfo.PayloadId.Value); + chunkSets.Remove(requestInfo.PayloadId!.Value); } - TCommunication completedCommunication; + TCommunication? completedCommunication; var fullCommunicationJson = String.Concat(payloads); try { @@ -137,9 +141,12 @@ namespace Tgstation.Server.Host.Components.Interop catch (Exception ex) { Logger.LogDebug(ex, "Bad chunked communication for payload {payloadId}!", requestInfo.PayloadId); - return chunkErrorCallback("Chunked request completed with bad JSON!"); + completedCommunication = null; } + if (completedCommunication == null) + return chunkErrorCallback("Chunked request completed with bad JSON!"); + return await completionCallback(completedCommunication, cancellationToken); } } diff --git a/src/Tgstation.Server.Host/Components/Interop/DMApiConstants.cs b/src/Tgstation.Server.Host/Components/Interop/DMApiConstants.cs index 2a59892180..7386e85669 100644 --- a/src/Tgstation.Server.Host/Components/Interop/DMApiConstants.cs +++ b/src/Tgstation.Server.Host/Components/Interop/DMApiConstants.cs @@ -40,12 +40,12 @@ namespace Tgstation.Server.Host.Components.Interop public const uint MaximumBridgeRequestLength = 8198; /// - /// The maximum length in bytes of a payload. + /// The maximum length in bytes of a payload. /// public const uint MaximumTopicRequestLength = 65528; /// - /// The maximum length in bytes of a response. + /// The maximum length in bytes of a response. /// public const uint MaximumTopicResponseLength = 65529; diff --git a/src/Tgstation.Server.Host/Components/Interop/DMApiParameters.cs b/src/Tgstation.Server.Host/Components/Interop/DMApiParameters.cs index a6faad3292..64b774aeb5 100644 --- a/src/Tgstation.Server.Host/Components/Interop/DMApiParameters.cs +++ b/src/Tgstation.Server.Host/Components/Interop/DMApiParameters.cs @@ -1,4 +1,5 @@ -using System.ComponentModel.DataAnnotations; +using System; +using System.ComponentModel.DataAnnotations; namespace Tgstation.Server.Host.Components.Interop { @@ -12,5 +13,23 @@ namespace Tgstation.Server.Host.Components.Interop /// [Required] public string AccessIdentifier { get; set; } + + /// + /// Initializes a new instance of the class. + /// + /// The value of . + public DMApiParameters(string accessIdentifier) + { + AccessIdentifier = accessIdentifier ?? throw new ArgumentNullException(nameof(accessIdentifier)); + } + + /// + /// Initializes a new instance of the class. + /// + /// For use by EFCore only. + protected DMApiParameters() + { + AccessIdentifier = null!; + } } } diff --git a/src/Tgstation.Server.Host/Components/Interop/DMApiResponse.cs b/src/Tgstation.Server.Host/Components/Interop/DMApiResponse.cs index bb1adf4925..e6c7502063 100644 --- a/src/Tgstation.Server.Host/Components/Interop/DMApiResponse.cs +++ b/src/Tgstation.Server.Host/Components/Interop/DMApiResponse.cs @@ -8,6 +8,6 @@ /// /// Any errors in the client's parameters. /// - public string ErrorMessage { get; set; } + public string? ErrorMessage { get; set; } } } diff --git a/src/Tgstation.Server.Host/Components/Interop/IMissingPayloadsCommunication.cs b/src/Tgstation.Server.Host/Components/Interop/IMissingPayloadsCommunication.cs index 392cd7eecb..7adf7d9cce 100644 --- a/src/Tgstation.Server.Host/Components/Interop/IMissingPayloadsCommunication.cs +++ b/src/Tgstation.Server.Host/Components/Interop/IMissingPayloadsCommunication.cs @@ -10,6 +10,6 @@ namespace Tgstation.Server.Host.Components.Interop /// /// The s missing from a chunked request. /// - IReadOnlyCollection MissingChunks { get; set; } + IReadOnlyCollection? MissingChunks { get; set; } } } diff --git a/src/Tgstation.Server.Host/Components/Interop/MessageContent.cs b/src/Tgstation.Server.Host/Components/Interop/MessageContent.cs index 6d484a411b..6b04fb10ae 100644 --- a/src/Tgstation.Server.Host/Components/Interop/MessageContent.cs +++ b/src/Tgstation.Server.Host/Components/Interop/MessageContent.cs @@ -8,11 +8,11 @@ /// /// The message . /// - public string Text { get; set; } + public string? Text { get; set; } /// /// The . /// - public ChatEmbed Embed { get; set; } + public ChatEmbed? Embed { get; set; } } } diff --git a/src/Tgstation.Server.Host/Components/Interop/Topic/ChunkedTopicParameters.cs b/src/Tgstation.Server.Host/Components/Interop/Topic/ChunkedTopicParameters.cs index 79353a66bb..0940281ece 100644 --- a/src/Tgstation.Server.Host/Components/Interop/Topic/ChunkedTopicParameters.cs +++ b/src/Tgstation.Server.Host/Components/Interop/Topic/ChunkedTopicParameters.cs @@ -8,7 +8,7 @@ namespace Tgstation.Server.Host.Components.Interop.Topic sealed class ChunkedTopicParameters : TopicParameters, IMissingPayloadsCommunication, IChunkPayloadId { /// - public IReadOnlyCollection MissingChunks { get; set; } + public IReadOnlyCollection? MissingChunks { get; set; } /// public uint? PayloadId { get; set; } diff --git a/src/Tgstation.Server.Host/Components/Interop/Topic/EventNotification.cs b/src/Tgstation.Server.Host/Components/Interop/Topic/EventNotification.cs index 54a8d496a5..75eb3bc9f4 100644 --- a/src/Tgstation.Server.Host/Components/Interop/Topic/EventNotification.cs +++ b/src/Tgstation.Server.Host/Components/Interop/Topic/EventNotification.cs @@ -20,14 +20,14 @@ namespace Tgstation.Server.Host.Components.Interop.Topic /// /// The set of parameters. /// - public IReadOnlyCollection Parameters { get; } + public IReadOnlyCollection Parameters { get; } /// /// Initializes a new instance of the class. /// /// The value of . /// The that forms the value of . - public EventNotification(EventType eventType, IEnumerable parameters = null) + public EventNotification(EventType eventType, IEnumerable parameters) { Type = eventType; Parameters = parameters?.ToList() ?? throw new ArgumentNullException(nameof(parameters)); diff --git a/src/Tgstation.Server.Host/Components/Interop/Topic/TopicParameters.cs b/src/Tgstation.Server.Host/Components/Interop/Topic/TopicParameters.cs index 65c060b412..bdc8405b02 100644 --- a/src/Tgstation.Server.Host/Components/Interop/Topic/TopicParameters.cs +++ b/src/Tgstation.Server.Host/Components/Interop/Topic/TopicParameters.cs @@ -20,12 +20,12 @@ namespace Tgstation.Server.Host.Components.Interop.Topic /// /// The for requests. /// - public ChatCommand ChatCommand { get; } + public ChatCommand? ChatCommand { get; } /// /// The for requests. /// - public EventNotification EventNotification { get; } + public EventNotification? EventNotification { get; } /// /// The new port for or requests. @@ -40,27 +40,27 @@ namespace Tgstation.Server.Host.Components.Interop.Topic /// /// The new for requests. /// - public string NewInstanceName { get; } + public string? NewInstanceName { get; } /// /// The message to broadcast for requests. /// - public string BroadcastMessage { get; } + public string? BroadcastMessage { get; } /// /// The for requests. /// - public ChatUpdate ChatUpdate { get; } + public ChatUpdate? ChatUpdate { get; } /// /// The new server after a reattach. /// - public Version NewServerVersion { get; } + public Version? NewServerVersion { get; } /// /// The for a partial request. /// - public ChunkData Chunk { get; } + public ChunkData? Chunk { get; } /// /// Whether or not the constitute a priority request. @@ -188,6 +188,7 @@ namespace Tgstation.Server.Host.Components.Interop.Topic /// /// The value of . protected TopicParameters(TopicCommandType commandType) + : base(String.Empty) // access identifier gets set before send { CommandType = commandType; } diff --git a/src/Tgstation.Server.Host/Components/Interop/Topic/TopicResponse.cs b/src/Tgstation.Server.Host/Components/Interop/Topic/TopicResponse.cs index f108aba543..12db6f3ef1 100644 --- a/src/Tgstation.Server.Host/Components/Interop/Topic/TopicResponse.cs +++ b/src/Tgstation.Server.Host/Components/Interop/Topic/TopicResponse.cs @@ -12,29 +12,29 @@ namespace Tgstation.Server.Host.Components.Interop.Topic /// /// The text to reply with as the result of a request, if any. Deprecated circa Interop 5.4.0. /// - public string CommandResponseMessage { get; set; } + public string? CommandResponseMessage { get; set; } /// /// The response from a . Added in Interop 5.4.0. /// - public ChatMessage CommandResponse { get; set; } + public ChatMessage? CommandResponse { get; set; } /// /// The s to send as the result of a request, if any. /// - public ICollection ChatResponses { get; set; } + public ICollection? ChatResponses { get; set; } /// /// The DMAPI s for requests. /// - public ICollection CustomCommands { get; set; } + public ICollection? CustomCommands { get; set; } /// /// The for a partial response. /// - public ChunkData Chunk { get; set; } + public ChunkData? Chunk { get; set; } /// - public IReadOnlyCollection MissingChunks { get; set; } + public IReadOnlyCollection? MissingChunks { get; set; } } } diff --git a/src/Tgstation.Server.Host/Components/Repository/DefaultGitRemoteFeatures.cs b/src/Tgstation.Server.Host/Components/Repository/DefaultGitRemoteFeatures.cs index ead4007720..05d9408a67 100644 --- a/src/Tgstation.Server.Host/Components/Repository/DefaultGitRemoteFeatures.cs +++ b/src/Tgstation.Server.Host/Components/Repository/DefaultGitRemoteFeatures.cs @@ -21,10 +21,10 @@ namespace Tgstation.Server.Host.Components.Repository public RemoteGitProvider? RemoteGitProvider => Api.Models.RemoteGitProvider.Unknown; /// - public string RemoteRepositoryOwner => null; + public string? RemoteRepositoryOwner => null; /// - public string RemoteRepositoryName => null; + public string? RemoteRepositoryName => null; /// public ValueTask GetTestMerge( diff --git a/src/Tgstation.Server.Host/Components/Repository/GitHubRemoteFeatures.cs b/src/Tgstation.Server.Host/Components/Repository/GitHubRemoteFeatures.cs index 6e5d5c4ec0..7a21ebde2b 100644 --- a/src/Tgstation.Server.Host/Components/Repository/GitHubRemoteFeatures.cs +++ b/src/Tgstation.Server.Host/Components/Repository/GitHubRemoteFeatures.cs @@ -25,12 +25,6 @@ namespace Tgstation.Server.Host.Components.Repository /// public override RemoteGitProvider? RemoteGitProvider => Api.Models.RemoteGitProvider.GitHub; - /// - public override string RemoteRepositoryOwner { get; } - - /// - public override string RemoteRepositoryName { get; } - /// /// The for the . /// @@ -46,13 +40,6 @@ namespace Tgstation.Server.Host.Components.Repository : base(logger, remoteUrl) { this.gitHubServiceFactory = gitHubServiceFactory ?? throw new ArgumentNullException(nameof(gitHubServiceFactory)); - - ArgumentNullException.ThrowIfNull(remoteUrl); - - RemoteRepositoryOwner = remoteUrl.Segments[1].TrimEnd('/'); - RemoteRepositoryName = remoteUrl.Segments[2].TrimEnd('/'); - if (RemoteRepositoryName.EndsWith(".git", StringComparison.OrdinalIgnoreCase)) - RemoteRepositoryName = RemoteRepositoryName[0..^4]; } /// @@ -65,9 +52,9 @@ namespace Tgstation.Server.Host.Components.Repository ? gitHubServiceFactory.CreateService(repositorySettings.AccessToken) : gitHubServiceFactory.CreateService(); - PullRequest pr = null; - ApiException exception = null; - string errorMessage = null; + PullRequest? pr = null; + ApiException? exception = null; + string? errorMessage = null; try { pr = await gitHubService.GetPullRequest(RemoteRepositoryOwner, RemoteRepositoryName, parameters.Number, cancellationToken); diff --git a/src/Tgstation.Server.Host/Components/Repository/GitLabRemoteFeatures.cs b/src/Tgstation.Server.Host/Components/Repository/GitLabRemoteFeatures.cs index def1adbaaf..9e468b06e6 100644 --- a/src/Tgstation.Server.Host/Components/Repository/GitLabRemoteFeatures.cs +++ b/src/Tgstation.Server.Host/Components/Repository/GitLabRemoteFeatures.cs @@ -30,12 +30,6 @@ namespace Tgstation.Server.Host.Components.Repository /// public override RemoteGitProvider? RemoteGitProvider => Api.Models.RemoteGitProvider.GitLab; - /// - public override string RemoteRepositoryOwner { get; } - - /// - public override string RemoteRepositoryName { get; } - /// /// Initializes a new instance of the class. /// @@ -44,10 +38,6 @@ namespace Tgstation.Server.Host.Components.Repository public GitLabRemoteFeatures(ILogger logger, Uri remoteUrl) : base(logger, remoteUrl) { - RemoteRepositoryOwner = remoteUrl.Segments[1].TrimEnd('/'); - RemoteRepositoryName = remoteUrl.Segments[2].TrimEnd('/'); - if (RemoteRepositoryName.EndsWith(".git", StringComparison.OrdinalIgnoreCase)) - RemoteRepositoryName = RemoteRepositoryName[0..^4]; } /// diff --git a/src/Tgstation.Server.Host/Components/Repository/GitRemoteFeaturesBase.cs b/src/Tgstation.Server.Host/Components/Repository/GitRemoteFeaturesBase.cs index 1b1c5337af..d42e928792 100644 --- a/src/Tgstation.Server.Host/Components/Repository/GitRemoteFeaturesBase.cs +++ b/src/Tgstation.Server.Host/Components/Repository/GitRemoteFeaturesBase.cs @@ -25,10 +25,10 @@ namespace Tgstation.Server.Host.Components.Repository public abstract RemoteGitProvider? RemoteGitProvider { get; } /// - public abstract string RemoteRepositoryOwner { get; } + public string RemoteRepositoryOwner { get; } /// - public abstract string RemoteRepositoryName { get; } + public string RemoteRepositoryName { get; } /// /// The for the . @@ -50,6 +50,11 @@ namespace Tgstation.Server.Host.Components.Repository Logger = logger ?? throw new ArgumentNullException(nameof(logger)); ArgumentNullException.ThrowIfNull(remoteUrl); + RemoteRepositoryOwner = remoteUrl.Segments[1].TrimEnd('/'); + RemoteRepositoryName = remoteUrl.Segments[2].TrimEnd('/'); + if (RemoteRepositoryName.EndsWith(".git", StringComparison.OrdinalIgnoreCase)) + RemoteRepositoryName = RemoteRepositoryName[0..^4]; + cachedLookups = new Dictionary(); } @@ -62,18 +67,18 @@ namespace Tgstation.Server.Host.Components.Repository ArgumentNullException.ThrowIfNull(parameters); ArgumentNullException.ThrowIfNull(repositorySettings); - Models.TestMerge result; + Models.TestMerge? result; lock (cachedLookups) if (cachedLookups.TryGetValue(parameters, out result)) - Logger.LogTrace("Using cache for test merge #{0}", parameters.Number); + Logger.LogTrace("Using cache for test merge #{testMergeNumber}", parameters.Number); if (result == null) { - Logger.LogTrace("Retrieving metadata for test merge #{0}...", parameters.Number); + Logger.LogTrace("Retrieving metadata for test merge #{testMergeNumber}...", parameters.Number); result = await GetTestMergeImpl(parameters, repositorySettings, cancellationToken); lock (cachedLookups) if (!cachedLookups.TryAdd(parameters, result)) - Logger.LogError("Race condition on adding test merge #{0}!", parameters.Number); + Logger.LogError("Race condition on adding test merge #{testMergeNumber}!", parameters.Number); } return result; diff --git a/src/Tgstation.Server.Host/Components/Repository/GitRemoteFeaturesFactory.cs b/src/Tgstation.Server.Host/Components/Repository/GitRemoteFeaturesFactory.cs index ab5dcb45ec..70694d0b94 100644 --- a/src/Tgstation.Server.Host/Components/Repository/GitRemoteFeaturesFactory.cs +++ b/src/Tgstation.Server.Host/Components/Repository/GitRemoteFeaturesFactory.cs @@ -78,7 +78,7 @@ namespace Tgstation.Server.Host.Components.Repository case "GIT.GITLAB.COM": return RemoteGitProvider.GitLab; default: - logger.LogTrace("Unknown git remote: {0}", origin); + logger.LogDebug("Unknown git remote: {origin}", origin); return RemoteGitProvider.Unknown; } } diff --git a/src/Tgstation.Server.Host/Components/Repository/ICredentialsProvider.cs b/src/Tgstation.Server.Host/Components/Repository/ICredentialsProvider.cs index cef08354c6..1324c35320 100644 --- a/src/Tgstation.Server.Host/Components/Repository/ICredentialsProvider.cs +++ b/src/Tgstation.Server.Host/Components/Repository/ICredentialsProvider.cs @@ -16,7 +16,7 @@ namespace Tgstation.Server.Host.Components.Repository /// The optional username to use in the . /// The optional password to use in the . /// A new . - CredentialsHandler GenerateCredentialsHandler(string username, string password); + CredentialsHandler GenerateCredentialsHandler(string? username, string? password); /// /// Rethrow the authentication failure message as a if it is one. diff --git a/src/Tgstation.Server.Host/Components/Repository/IRepository.cs b/src/Tgstation.Server.Host/Components/Repository/IRepository.cs index 0482eaaa90..65bcd1d61e 100644 --- a/src/Tgstation.Server.Host/Components/Repository/IRepository.cs +++ b/src/Tgstation.Server.Host/Components/Repository/IRepository.cs @@ -44,18 +44,18 @@ namespace Tgstation.Server.Host.Components.Repository /// Checks out a given . /// /// The sha or reference to checkout. - /// The username used for fetching from submodule repositories. - /// The password used for fetching from submodule repositories. + /// The optional username used for fetching from submodule repositories. + /// The optional password used for fetching from submodule repositories. /// If a submodule update should be attempted after the merge. /// The optional to report progress of the operation. /// The for the operation. /// A representing the running operation. ValueTask CheckoutObject( string committish, - string username, - string password, + string? username, + string? password, bool updateSubmodules, - JobProgressReporter progressReporter, + JobProgressReporter? progressReporter, CancellationToken cancellationToken); /// @@ -64,8 +64,8 @@ namespace Tgstation.Server.Host.Components.Repository /// The of the pull request. /// The name of the merge committer. /// The e-mail of the merge committer. - /// The username used to fetch from the origin and submodule repositories. - /// The password used to fetch from the origin and submodule repositories. + /// The optional username used to fetch from the origin and submodule repositories. + /// The optional password used to fetch from the origin and submodule repositories. /// If a submodule update should be attempted after the merge. /// The to report progress of the operation. /// The for the operation. @@ -74,8 +74,8 @@ namespace Tgstation.Server.Host.Components.Repository TestMergeParameters testMergeParameters, string committerName, string committerEmail, - string username, - string password, + string? username, + string? password, bool updateSubmodules, JobProgressReporter progressReporter, CancellationToken cancellationToken); @@ -84,15 +84,15 @@ namespace Tgstation.Server.Host.Components.Repository /// Fetch commits from the origin repository. /// /// The optional to report progress of the operation. - /// The username to fetch from the origin repository. - /// The password to fetch from the origin repository. + /// The optional username to fetch from the origin repository. + /// The optional password to fetch from the origin repository. /// If any events created should be marked as part of the deployment pipeline. /// The for the operation. /// A representing the running operation. ValueTask FetchOrigin( - JobProgressReporter progressReporter, - string username, - string password, + JobProgressReporter? progressReporter, + string? username, + string? password, bool deploymentPipeline, CancellationToken cancellationToken); @@ -100,16 +100,16 @@ namespace Tgstation.Server.Host.Components.Repository /// Requires the current HEAD to be a tracked reference. Hard resets the reference to what it tracks on the origin repository. /// /// The to report progress of the operation. - /// The username used for fetching from submodule repositories. - /// The password used for fetching from submodule repositories. + /// The optional username used for fetching from submodule repositories. + /// The optional password used for fetching from submodule repositories. /// If a submodule update should be attempted after the merge. /// If any events created should be marked as part of the deployment pipeline. /// The for the operation. /// A resulting in the SHA of the new HEAD. ValueTask ResetToOrigin( JobProgressReporter progressReporter, - string username, - string password, + string? username, + string? password, bool updateSubmodules, bool deploymentPipeline, CancellationToken cancellationToken); @@ -151,10 +151,10 @@ namespace Tgstation.Server.Host.Components.Repository /// If any events created should be marked as part of the deployment pipeline. /// The for the operation. /// A resulting in if commits were pushed to the tracked origin reference, otherwise. - ValueTask Sychronize( + ValueTask Synchronize( JobProgressReporter progressReporter, - string username, - string password, + string? username, + string? password, string committerName, string committerEmail, bool synchronizeTrackedBranch, diff --git a/src/Tgstation.Server.Host/Components/Repository/IRepositoryManager.cs b/src/Tgstation.Server.Host/Components/Repository/IRepositoryManager.cs index d602730e15..596301611a 100644 --- a/src/Tgstation.Server.Host/Components/Repository/IRepositoryManager.cs +++ b/src/Tgstation.Server.Host/Components/Repository/IRepositoryManager.cs @@ -26,25 +26,25 @@ namespace Tgstation.Server.Host.Components.Repository /// /// The for the operation. /// A resulting in the loaded if it exists, otherwise. - ValueTask LoadRepository(CancellationToken cancellationToken); + ValueTask LoadRepository(CancellationToken cancellationToken); /// /// Clone the repository at . /// /// The of the remote repository to clone. - /// The branch to clone. - /// The username to clone from . - /// The password to clone from . + /// The optional branch to clone. + /// The optional username to clone from . + /// The optional password to clone from . /// The optional for progress of the clone. /// If submodules should be recusively cloned and initialized. /// The for the operation. /// A resulting i the newly cloned , if one already exists. - ValueTask CloneRepository( + ValueTask CloneRepository( Uri url, - string initialBranch, - string username, - string password, - JobProgressReporter progressReporter, + string? initialBranch, + string? username, + string? password, + JobProgressReporter? progressReporter, bool recurseSubmodules, CancellationToken cancellationToken); diff --git a/src/Tgstation.Server.Host/Components/Repository/LibGit2RepositoryFactory.cs b/src/Tgstation.Server.Host/Components/Repository/LibGit2RepositoryFactory.cs index 83011a61b1..418758ebe7 100644 --- a/src/Tgstation.Server.Host/Components/Repository/LibGit2RepositoryFactory.cs +++ b/src/Tgstation.Server.Host/Components/Repository/LibGit2RepositoryFactory.cs @@ -80,7 +80,7 @@ namespace Tgstation.Server.Host.Components.Repository TaskScheduler.Current); /// - public CredentialsHandler GenerateCredentialsHandler(string username, string password) => (a, b, supportedCredentialTypes) => + public CredentialsHandler GenerateCredentialsHandler(string? username, string? password) => (a, b, supportedCredentialTypes) => { var hasCreds = username != null; var supportsUserPass = supportedCredentialTypes.HasFlag(SupportedCredentialTypes.UsernamePassword); diff --git a/src/Tgstation.Server.Host/Components/Repository/Repository.cs b/src/Tgstation.Server.Host/Components/Repository/Repository.cs index 125ceded30..5518c1a44d 100644 --- a/src/Tgstation.Server.Host/Components/Repository/Repository.cs +++ b/src/Tgstation.Server.Host/Components/Repository/Repository.cs @@ -17,12 +17,13 @@ using Tgstation.Server.Host.Configuration; using Tgstation.Server.Host.Extensions; using Tgstation.Server.Host.IO; using Tgstation.Server.Host.Jobs; +using Tgstation.Server.Host.Utils; namespace Tgstation.Server.Host.Components.Repository { /// #pragma warning disable CA1506 // TODO: Decomplexify - sealed class Repository : IRepository + sealed class Repository : DisposeInvoker, IRepository { /// /// The default username for committers. @@ -53,10 +54,10 @@ namespace Tgstation.Server.Host.Components.Repository public RemoteGitProvider? RemoteGitProvider => gitRemoteFeatures.RemoteGitProvider; /// - public string RemoteRepositoryOwner => gitRemoteFeatures.RemoteRepositoryOwner; + public string? RemoteRepositoryOwner => gitRemoteFeatures.RemoteRepositoryOwner; /// - public string RemoteRepositoryName => gitRemoteFeatures.RemoteRepositoryName; + public string? RemoteRepositoryName => gitRemoteFeatures.RemoteRepositoryName; /// public bool Tracking => Reference != null && libGitRepo.Head.IsTracking; @@ -115,16 +116,6 @@ namespace Tgstation.Server.Host.Components.Repository /// readonly GeneralConfiguration generalConfiguration; - /// - /// to be taken when is called. - /// - readonly Action onDispose; - - /// - /// If the was disposed. - /// - bool disposed; - /// /// Initializes a new instance of the class. /// @@ -137,7 +128,7 @@ namespace Tgstation.Server.Host.Components.Repository /// The to provide the value of . /// The value of . /// The value of . - /// The value if . + /// The action for the . public Repository( LibGit2Sharp.IRepository libGitRepo, ILibGit2Commands commands, @@ -148,7 +139,8 @@ namespace Tgstation.Server.Host.Components.Repository IGitRemoteFeaturesFactory gitRemoteFeaturesFactory, ILogger logger, GeneralConfiguration generalConfiguration, - Action onDispose) + Action disposeAction) + : base(disposeAction) { this.libGitRepo = libGitRepo ?? throw new ArgumentNullException(nameof(libGitRepo)); this.commands = commands ?? throw new ArgumentNullException(nameof(commands)); @@ -160,35 +152,18 @@ namespace Tgstation.Server.Host.Components.Repository this.logger = logger ?? throw new ArgumentNullException(nameof(logger)); this.generalConfiguration = generalConfiguration ?? throw new ArgumentNullException(nameof(generalConfiguration)); - this.onDispose = onDispose ?? throw new ArgumentNullException(nameof(onDispose)); gitRemoteFeatures = gitRemoteFeaturesFactory.CreateGitRemoteFeatures(this); } - /// - public void Dispose() - { - lock (onDispose) - { - if (disposed) - return; - - disposed = true; - } - - logger.LogTrace("Disposing..."); - libGitRepo.Dispose(); - onDispose(); - } - /// #pragma warning disable CA1506 // TODO: Decomplexify public async ValueTask AddTestMerge( TestMergeParameters testMergeParameters, string committerName, string committerEmail, - string username, - string password, + string? username, + string? password, bool updateSubmodules, JobProgressReporter progressReporter, CancellationToken cancellationToken) @@ -227,12 +202,12 @@ namespace Tgstation.Server.Host.Components.Repository var originalCommit = libGitRepo.Head; - MergeResult result = null; + MergeResult? result = null; var progressFactor = 1.0 / (updateSubmodules ? 3 : 2); var sig = new Signature(new Identity(committerName, committerEmail), DateTimeOffset.UtcNow); - List conflictedPaths = null; + List? conflictedPaths = null; await Task.Factory.StartNew( () => { @@ -316,17 +291,17 @@ namespace Tgstation.Server.Host.Components.Repository DefaultIOManager.BlockingTaskCreationOptions, TaskScheduler.Current); - if (result.Status == MergeStatus.Conflicts) + if (result!.Status == MergeStatus.Conflicts) { var arguments = new List { originalCommit.Tip.Sha, - testMergeParameters.TargetCommitSha, + testMergeParameters.TargetCommitSha!, originalCommit.FriendlyName ?? UnknownReference, testMergeBranchName, }; - arguments.AddRange(conflictedPaths); + arguments.AddRange(conflictedPaths!); await eventConsumer.HandleEvent( EventType.RepoMergeConflict, @@ -365,10 +340,10 @@ namespace Tgstation.Server.Host.Components.Repository await eventConsumer.HandleEvent( EventType.RepoAddTestMerge, - new List + new List { testMergeParameters.Number.ToString(CultureInfo.InvariantCulture), - testMergeParameters.TargetCommitSha, + testMergeParameters.TargetCommitSha!, testMergeParameters.Comment, }, false, @@ -384,10 +359,10 @@ namespace Tgstation.Server.Host.Components.Repository /// public async ValueTask CheckoutObject( string committish, - string username, - string password, + string? username, + string? password, bool updateSubmodules, - JobProgressReporter progressReporter, + JobProgressReporter? progressReporter, CancellationToken cancellationToken) { ArgumentNullException.ThrowIfNull(committish); @@ -418,9 +393,9 @@ namespace Tgstation.Server.Host.Components.Repository /// public async ValueTask FetchOrigin( - JobProgressReporter progressReporter, - string username, - string password, + JobProgressReporter? progressReporter, + string? username, + string? password, bool deploymentPipeline, CancellationToken cancellationToken) { @@ -468,8 +443,8 @@ namespace Tgstation.Server.Host.Components.Repository /// public async ValueTask ResetToOrigin( JobProgressReporter progressReporter, - string username, - string password, + string? username, + string? password, bool updateSubmodules, bool deploymentPipeline, CancellationToken cancellationToken) @@ -566,8 +541,8 @@ namespace Tgstation.Server.Host.Components.Repository { ArgumentNullException.ThrowIfNull(progressReporter); - MergeResult result = null; - Branch trackedBranch = null; + MergeResult? result = null; + Branch? trackedBranch = null; var oldHead = libGitRepo.Head; var oldTip = oldHead.Tip; @@ -616,14 +591,14 @@ namespace Tgstation.Server.Host.Components.Repository DefaultIOManager.BlockingTaskCreationOptions, TaskScheduler.Current); - if (result.Status == MergeStatus.Conflicts) + if (result!.Status == MergeStatus.Conflicts) { await eventConsumer.HandleEvent( EventType.RepoMergeConflict, new List { oldTip.Sha, - trackedBranch.Tip.Sha, + trackedBranch!.Tip.Sha, oldHead.FriendlyName ?? UnknownReference, trackedBranch.FriendlyName, }, @@ -636,10 +611,10 @@ namespace Tgstation.Server.Host.Components.Repository } /// - public async ValueTask Sychronize( + public async ValueTask Synchronize( JobProgressReporter progressReporter, - string username, - string password, + string? username, + string? password, string committerName, string committerEmail, bool synchronizeTrackedBranch, @@ -867,13 +842,21 @@ namespace Tgstation.Server.Host.Components.Repository DefaultIOManager.BlockingTaskCreationOptions, TaskScheduler.Current); + /// + protected override void DisposeImpl() + { + logger.LogTrace("Disposing..."); + libGitRepo.Dispose(); + base.DisposeImpl(); + } + /// /// Runs a blocking force checkout to . /// /// The committish to checkout. /// The optional for the operation. /// The for the operation. - void RawCheckout(string committish, JobProgressReporter progressReporter, CancellationToken cancellationToken) + void RawCheckout(string committish, JobProgressReporter? progressReporter, CancellationToken cancellationToken) { logger.LogTrace("Checkout: {committish}", committish); @@ -1008,15 +991,15 @@ namespace Tgstation.Server.Host.Components.Repository /// Recusively update all s in the . ///
/// Optional of the operation. - /// The username for the . - /// The password for the . + /// The optional username for the . + /// The optional password for the . /// If any events created should be marked as part of the deployment pipeline. /// The for the operation. /// A representing the running operation. async ValueTask UpdateSubmodules( - JobProgressReporter progressReporter, - string username, - string password, + JobProgressReporter? progressReporter, + string? username, + string? password, bool deploymentPipeline, CancellationToken cancellationToken) { diff --git a/src/Tgstation.Server.Host/Components/Repository/RepositoryManager.cs b/src/Tgstation.Server.Host/Components/Repository/RepositoryManager.cs index 8ce9b60318..727d171e4e 100644 --- a/src/Tgstation.Server.Host/Components/Repository/RepositoryManager.cs +++ b/src/Tgstation.Server.Host/Components/Repository/RepositoryManager.cs @@ -118,12 +118,12 @@ namespace Tgstation.Server.Host.Components.Repository } /// - public async ValueTask CloneRepository( + public async ValueTask CloneRepository( Uri url, - string initialBranch, - string username, - string password, - JobProgressReporter progressReporter, + string? initialBranch, + string? username, + string? password, + JobProgressReporter? progressReporter, bool recurseSubmodules, CancellationToken cancellationToken) { @@ -208,7 +208,7 @@ namespace Tgstation.Server.Host.Components.Repository } /// - public async ValueTask LoadRepository(CancellationToken cancellationToken) + public async ValueTask LoadRepository(CancellationToken cancellationToken) { logger.LogTrace("Begin LoadRepository..."); lock (semaphore) diff --git a/src/Tgstation.Server.Host/Components/Repository/RepositoryUpdateService.cs b/src/Tgstation.Server.Host/Components/Repository/RepositoryUpdateService.cs index 418dc9462d..a7aca78c4b 100644 --- a/src/Tgstation.Server.Host/Components/Repository/RepositoryUpdateService.cs +++ b/src/Tgstation.Server.Host/Components/Repository/RepositoryUpdateService.cs @@ -85,16 +85,18 @@ namespace Tgstation.Server.Host.Components.Repository IDatabaseContext databaseContext, ILogger logger, Models.Instance instance, - string lastOriginCommitSha, - Action revInfoSink, + string? lastOriginCommitSha, + Action? revInfoSink, CancellationToken cancellationToken) { var repoSha = repository.Head; IQueryable ApplyQuery(IQueryable query) => query - .Where(x => x.CommitSha == repoSha && x.Instance.Id == instance.Id) + .Where(x => x.CommitSha == repoSha && x.InstanceId == instance.Id) .Include(x => x.CompileJobs) - .Include(x => x.ActiveTestMerges).ThenInclude(x => x.TestMerge).ThenInclude(x => x.MergedBy); + .Include(x => x.ActiveTestMerges!) + .ThenInclude(x => x.TestMerge) + .ThenInclude(x => x.MergedBy); var revisionInfo = await ApplyQuery(databaseContext.RevisionInformations).FirstOrDefaultAsync(cancellationToken); @@ -103,7 +105,7 @@ namespace Tgstation.Server.Host.Components.Repository revisionInfo = databaseContext .RevisionInformations .Local - .Where(x => x.CommitSha == repoSha && x.Instance.Id == instance.Id) + .Where(x => x.CommitSha == repoSha && x.InstanceId == instance.Id) .FirstOrDefault(); var needsDbUpdate = revisionInfo == default; @@ -123,7 +125,7 @@ namespace Tgstation.Server.Host.Components.Repository databaseContext.RevisionInformations.Add(revisionInfo); } - revisionInfo.OriginCommitSha ??= lastOriginCommitSha; + revisionInfo!.OriginCommitSha ??= lastOriginCommitSha; if (revisionInfo.OriginCommitSha == null) { revisionInfo.OriginCommitSha = repoSha; @@ -145,13 +147,15 @@ namespace Tgstation.Server.Host.Components.Repository /// A representing the running operation. #pragma warning disable CA1502, CA1506 // TODO: Decomplexify public async ValueTask RepositoryUpdateJob( - IInstanceCore instance, + IInstanceCore? instance, IDatabaseContextFactory databaseContextFactory, Job job, JobProgressReporter progressReporter, CancellationToken cancellationToken) #pragma warning restore CA1502, CA1506 { + ArgumentNullException.ThrowIfNull(instance); + _ = job; // shuts up an IDE warning var repoManager = instance.RepositoryManager; @@ -160,23 +164,23 @@ namespace Tgstation.Server.Host.Components.Repository var startReference = repo.Reference; var startSha = repo.Head; - string postUpdateSha = null; + string? postUpdateSha = null; var newTestMerges = model.NewTestMerges != null && model.NewTestMerges.Count > 0; if (newTestMerges && repo.RemoteGitProvider == RemoteGitProvider.Unknown) throw new JobException(ErrorCode.RepoUnsupportedTestMergeRemote); - var committerName = currentModel.ShowTestMergeCommitters.Value + var committerName = (currentModel.ShowTestMergeCommitters!.Value ? initiatingUser.Name - : currentModel.CommitterName; + : currentModel.CommitterName)!; var hardResettingToOriginReference = model.UpdateFromOrigin == true && model.Reference != null; var numSteps = (model.NewTestMerges?.Count ?? 0) + (model.UpdateFromOrigin == true ? 1 : 0) + (!modelHasShaOrReference ? 2 : (hardResettingToOriginReference ? 3 : 1)); var progressFactor = 1.0 / numSteps; - JobProgressReporter NextProgressReporter(string stage) + JobProgressReporter NextProgressReporter(string? stage) { return progressReporter.CreateSection(stage, progressFactor); } @@ -184,19 +188,19 @@ namespace Tgstation.Server.Host.Components.Repository progressReporter.ReportProgress(0); // get a base line for where we are - Models.RevisionInformation lastRevisionInfo = null; + Models.RevisionInformation? lastRevisionInfo = null; var attachedInstance = new Models.Instance { Id = instanceId, }; - ValueTask CallLoadRevInfo(Models.TestMerge testMergeToAdd = null, string lastOriginCommitSha = null) => databaseContextFactory + ValueTask CallLoadRevInfo(Models.TestMerge? testMergeToAdd = null, string? lastOriginCommitSha = null) => databaseContextFactory .UseContext( async databaseContext => { databaseContext.Instances.Attach(attachedInstance); - var previousRevInfo = lastRevisionInfo; + var previousRevInfo = lastRevisionInfo!; var needsUpdate = await LoadRevisionInformation( repo, databaseContext, @@ -223,13 +227,11 @@ namespace Tgstation.Server.Host.Components.Repository testMergeToAdd.MergedBy = mergedBy; testMergeToAdd.MergedAt = DateTimeOffset.UtcNow; - foreach (var activeTestMerge in previousRevInfo.ActiveTestMerges) - lastRevisionInfo.ActiveTestMerges.Add(activeTestMerge); + var activeTestMerges = lastRevisionInfo!.ActiveTestMerges!; + foreach (var activeTestMerge in previousRevInfo.ActiveTestMerges!) + activeTestMerges.Add(activeTestMerge); - lastRevisionInfo.ActiveTestMerges.Add(new RevInfoTestMerge - { - TestMerge = testMergeToAdd, - }); + activeTestMerges.Add(new RevInfoTestMerge(testMergeToAdd, lastRevisionInfo)); lastRevisionInfo.PrimaryTestMerge = testMergeToAdd; needsUpdate = true; @@ -242,7 +244,7 @@ namespace Tgstation.Server.Host.Components.Repository await CallLoadRevInfo(); // apply new rev info, tracking applied test merges - ValueTask UpdateRevInfo(Models.TestMerge testMergeToAdd = null) => CallLoadRevInfo(testMergeToAdd, lastRevisionInfo.OriginCommitSha); + ValueTask UpdateRevInfo(Models.TestMerge? testMergeToAdd = null) => CallLoadRevInfo(testMergeToAdd, lastRevisionInfo!.OriginCommitSha); try { @@ -263,21 +265,21 @@ namespace Tgstation.Server.Host.Components.Repository var fastForward = await repo.MergeOrigin( NextProgressReporter("Merge Origin"), committerName, - currentModel.CommitterEmail, + currentModel.CommitterEmail!, false, cancellationToken); if (!fastForward.HasValue) throw new JobException(ErrorCode.RepoMergeConflict); - lastRevisionInfo.OriginCommitSha = await repo.GetOriginSha(cancellationToken); + lastRevisionInfo!.OriginCommitSha = await repo.GetOriginSha(cancellationToken); await UpdateRevInfo(); if (fastForward.Value) { - await repo.Sychronize( + await repo.Synchronize( NextProgressReporter("Sychronize"), currentModel.AccessUser, currentModel.AccessToken, - currentModel.CommitterName, - currentModel.CommitterEmail, + currentModel.CommitterName!, + currentModel.CommitterEmail!, true, false, cancellationToken); @@ -288,7 +290,7 @@ namespace Tgstation.Server.Host.Components.Repository } } - var updateSubmodules = currentModel.UpdateSubmodules.Value; + var updateSubmodules = currentModel.UpdateSubmodules!.Value; // checkout/hard reset if (modelHasShaOrReference) @@ -302,7 +304,7 @@ namespace Tgstation.Server.Host.Components.Repository if (validCheckoutSha || validCheckoutReference) { - var committish = model.CheckoutSha ?? model.Reference; + var committish = model.CheckoutSha ?? model.Reference!; var isSha = await repo.IsSha(committish, cancellationToken); if ((isSha && model.Reference != null) || (!isSha && model.CheckoutSha != null)) @@ -331,12 +333,12 @@ namespace Tgstation.Server.Host.Components.Repository updateSubmodules, false, cancellationToken); - await repo.Sychronize( + await repo.Synchronize( NextProgressReporter("Synchronize"), currentModel.AccessUser, currentModel.AccessToken, - currentModel.CommitterName, - currentModel.CommitterEmail, + currentModel.CommitterName!, + currentModel.CommitterEmail!, true, false, cancellationToken); @@ -344,7 +346,7 @@ namespace Tgstation.Server.Host.Components.Repository // repo head is on origin so force this // will update the db if necessary - lastRevisionInfo.OriginCommitSha = repo.Head; + lastRevisionInfo!.OriginCommitSha = repo.Head; } } @@ -355,19 +357,20 @@ namespace Tgstation.Server.Host.Components.Repository throw new JobException(ErrorCode.RepoTestMergeInvalidRemote); // bit of sanitization - foreach (var newTestMergeWithoutTargetCommitSha in model.NewTestMerges.Where(x => String.IsNullOrWhiteSpace(x.TargetCommitSha))) + var newTestMergeModels = model.NewTestMerges!; + foreach (var newTestMergeWithoutTargetCommitSha in newTestMergeModels.Where(x => String.IsNullOrWhiteSpace(x.TargetCommitSha))) newTestMergeWithoutTargetCommitSha.TargetCommitSha = null; var repoOwner = repo.RemoteRepositoryOwner; var repoName = repo.RemoteRepositoryName; // optimization: if we've already merged these exact same commits in this fashion before, just find the rev info for it and check it out - Models.RevisionInformation revInfoWereLookingFor = null; + Models.RevisionInformation? revInfoWereLookingFor = null; bool needToApplyRemainingPrs = true; - if (lastRevisionInfo.OriginCommitSha == lastRevisionInfo.CommitSha) + if (lastRevisionInfo!.OriginCommitSha == lastRevisionInfo.CommitSha) { bool cantSearch = false; - foreach (var newTestMerge in model.NewTestMerges) + foreach (var newTestMerge in newTestMergeModels) { if (newTestMerge.TargetCommitSha != null) #pragma warning disable CA1308 // Normalize strings to uppercase @@ -391,56 +394,58 @@ namespace Tgstation.Server.Host.Components.Repository if (!cantSearch) { - List dbPull = null; + List? dbPull = null; await databaseContextFactory.UseContext( async databaseContext => dbPull = await databaseContext.RevisionInformations .AsQueryable() - .Where(x => x.Instance.Id == instanceId - && x.OriginCommitSha == lastRevisionInfo.OriginCommitSha - && x.ActiveTestMerges.Count <= model.NewTestMerges.Count - && x.ActiveTestMerges.Count > 0) - .Include(x => x.ActiveTestMerges) - .ThenInclude(x => x.TestMerge) + .Where(x => x.InstanceId == instanceId + && x.OriginCommitSha == lastRevisionInfo.OriginCommitSha + && x.ActiveTestMerges!.Count <= newTestMergeModels.Count + && x.ActiveTestMerges!.Count > 0) + .Include(x => x.ActiveTestMerges!) + .ThenInclude(x => x.TestMerge) .ToListAsync(cancellationToken)); // split here cause this bit has to be done locally - revInfoWereLookingFor = dbPull - .Where(x => x.ActiveTestMerges.Count == model.NewTestMerges.Count - && x.ActiveTestMerges.Select(y => y.TestMerge) - .All(y => model.NewTestMerges.Any(z => - y.Number == z.Number - && y.TargetCommitSha.StartsWith(z.TargetCommitSha, StringComparison.Ordinal) - && (y.Comment?.Trim().ToUpperInvariant() == z.Comment?.Trim().ToUpperInvariant() || z.Comment == null)))) + revInfoWereLookingFor = dbPull! + .Where(x => x.ActiveTestMerges!.Count == newTestMergeModels.Count + && x.ActiveTestMerges + .Select(y => y.TestMerge) + .All(y => newTestMergeModels + .Any(z => + y.Number == z.Number + && y.TargetCommitSha!.StartsWith(z.TargetCommitSha!, StringComparison.Ordinal) + && (y.Comment?.Trim().ToUpperInvariant() == z.Comment?.Trim().ToUpperInvariant() || z.Comment == null)))) .FirstOrDefault(); - if (revInfoWereLookingFor == default && model.NewTestMerges.Count > 1) + if (revInfoWereLookingFor == default && newTestMergeModels.Count > 1) { // okay try to add at least SOME prs we've seen before - var listedNewTestMerges = model.NewTestMerges.ToList(); + var listedNewTestMerges = newTestMergeModels.ToList(); var appliedTestMergeIds = new List(); - Models.RevisionInformation lastGoodRevInfo = null; + Models.RevisionInformation? lastGoodRevInfo = null; do { foreach (var newTestMergeParameters in listedNewTestMerges) { - revInfoWereLookingFor = dbPull + revInfoWereLookingFor = dbPull! .Where(testRevInfo => { if (testRevInfo.PrimaryTestMerge == null) return false; - var testMergeMatch = model.NewTestMerges.Any(testTestMerge => + var testMergeMatch = newTestMergeModels.Any(testTestMerge => { var numberMatch = testRevInfo.PrimaryTestMerge.Number == testTestMerge.Number; if (!numberMatch) return false; - var shaMatch = testRevInfo.PrimaryTestMerge.TargetCommitSha.StartsWith( - testTestMerge.TargetCommitSha, + var shaMatch = testRevInfo.PrimaryTestMerge.TargetCommitSha!.StartsWith( + testTestMerge.TargetCommitSha!, StringComparison.Ordinal); if (!shaMatch) return false; @@ -453,7 +458,7 @@ namespace Tgstation.Server.Host.Components.Repository return false; var previousTestMergesMatch = testRevInfo - .ActiveTestMerges + .ActiveTestMerges! .Select(previousRevInfoTestMerge => previousRevInfoTestMerge.TestMerge) .All(previousTestMerge => appliedTestMergeIds.Contains(previousTestMerge.Id)); @@ -464,7 +469,7 @@ namespace Tgstation.Server.Host.Components.Repository if (revInfoWereLookingFor != null) { lastGoodRevInfo = revInfoWereLookingFor; - appliedTestMergeIds.Add(revInfoWereLookingFor.PrimaryTestMerge.Id); + appliedTestMergeIds.Add(revInfoWereLookingFor.PrimaryTestMerge!.Id); listedNewTestMerges.Remove(newTestMergeParameters); break; } @@ -485,16 +490,17 @@ namespace Tgstation.Server.Host.Components.Repository if (revInfoWereLookingFor != null) { // goteem - logger.LogDebug("Reusing existing SHA {sha}...", revInfoWereLookingFor.CommitSha); - await repo.ResetToSha(revInfoWereLookingFor.CommitSha, NextProgressReporter($"Reset to {revInfoWereLookingFor.CommitSha[..7]}"), cancellationToken); + var commitSha = revInfoWereLookingFor.CommitSha!; + logger.LogDebug("Reusing existing SHA {sha}...", commitSha); + await repo.ResetToSha(commitSha, NextProgressReporter($"Reset to {commitSha[..7]}"), cancellationToken); lastRevisionInfo = revInfoWereLookingFor; } if (needToApplyRemainingPrs) { - foreach (var newTestMerge in model.NewTestMerges) + foreach (var newTestMerge in newTestMergeModels) { - if (lastRevisionInfo.ActiveTestMerges.Any(x => x.TestMerge.Number == newTestMerge.Number)) + if (lastRevisionInfo.ActiveTestMerges!.Any(x => x.TestMerge.Number == newTestMerge.Number)) throw new JobException(ErrorCode.RepoDuplicateTestMerge); var fullTestMergeTask = repo.GetTestMerge(newTestMerge, currentModel, cancellationToken); @@ -502,7 +508,7 @@ namespace Tgstation.Server.Host.Components.Repository var mergeResult = await repo.AddTestMerge( newTestMerge, committerName, - currentModel.CommitterEmail, + currentModel.CommitterEmail!, currentModel.AccessUser, currentModel.AccessToken, updateSubmodules, @@ -513,7 +519,7 @@ namespace Tgstation.Server.Host.Components.Repository throw new JobException( ErrorCode.RepoTestMergeConflict, new JobException( - $"Test Merge #{newTestMerge.Number} at {newTestMerge.TargetCommitSha[..7]} conflicted! Conflicting files:{Environment.NewLine}{String.Join(Environment.NewLine, mergeResult.ConflictingFiles.Select(file => $"\t- /{file}"))}")); + $"Test Merge #{newTestMerge.Number} at {newTestMerge.TargetCommitSha![..7]} conflicted! Conflicting files:{Environment.NewLine}{String.Join(Environment.NewLine, mergeResult.ConflictingFiles!.Select(file => $"\t- /{file}"))}")); Models.TestMerge fullTestMerge; try @@ -544,14 +550,14 @@ namespace Tgstation.Server.Host.Components.Repository } var currentHead = repo.Head; - if (currentModel.PushTestMergeCommits.Value && (startSha != currentHead || (postUpdateSha != null && postUpdateSha != currentHead))) + if (currentModel.PushTestMergeCommits!.Value && (startSha != currentHead || (postUpdateSha != null && postUpdateSha != currentHead))) { - await repo.Sychronize( + await repo.Synchronize( NextProgressReporter("Synchronize"), currentModel.AccessUser, currentModel.AccessToken, - currentModel.CommitterName, - currentModel.CommitterEmail, + currentModel.CommitterName!, + currentModel.CommitterEmail!, false, false, cancellationToken); diff --git a/src/Tgstation.Server.Host/Components/Repository/TestMergeResult.cs b/src/Tgstation.Server.Host/Components/Repository/TestMergeResult.cs index 387a6baad1..2505a491d8 100644 --- a/src/Tgstation.Server.Host/Components/Repository/TestMergeResult.cs +++ b/src/Tgstation.Server.Host/Components/Repository/TestMergeResult.cs @@ -17,6 +17,6 @@ namespace Tgstation.Server.Host.Components.Repository /// /// List of conflicting file paths relative to the repository root. Only present if is . /// - public IReadOnlyList ConflictingFiles { get; init; } + public IReadOnlyList? ConflictingFiles { get; init; } } } diff --git a/src/Tgstation.Server.Host/Components/Session/CombinedTopicResponse.cs b/src/Tgstation.Server.Host/Components/Session/CombinedTopicResponse.cs index 417934f04a..ad22b6204b 100644 --- a/src/Tgstation.Server.Host/Components/Session/CombinedTopicResponse.cs +++ b/src/Tgstation.Server.Host/Components/Session/CombinedTopicResponse.cs @@ -5,26 +5,26 @@ using Tgstation.Server.Host.Components.Interop.Topic; namespace Tgstation.Server.Host.Components.Session { /// - /// Combines a with a . + /// Combines a with a . /// sealed class CombinedTopicResponse { /// - /// The raw . + /// The raw . /// - public global::Byond.TopicSender.TopicResponse ByondTopicResponse { get; } + public Byond.TopicSender.TopicResponse ByondTopicResponse { get; } /// /// The interop , if any. /// - public TopicResponse InteropResponse { get; } + public TopicResponse? InteropResponse { get; } /// /// Initializes a new instance of the class. /// /// The value of . /// The optional value of . - public CombinedTopicResponse(global::Byond.TopicSender.TopicResponse byondTopicResponse, TopicResponse interopResponse) + public CombinedTopicResponse(Byond.TopicSender.TopicResponse byondTopicResponse, TopicResponse? interopResponse) { ByondTopicResponse = byondTopicResponse ?? throw new ArgumentNullException(nameof(byondTopicResponse)); InteropResponse = interopResponse; diff --git a/src/Tgstation.Server.Host/Components/Session/ISessionController.cs b/src/Tgstation.Server.Host/Components/Session/ISessionController.cs index 2769aafa60..ccdaa448c4 100644 --- a/src/Tgstation.Server.Host/Components/Session/ISessionController.cs +++ b/src/Tgstation.Server.Host/Components/Session/ISessionController.cs @@ -32,7 +32,7 @@ namespace Tgstation.Server.Host.Components.Session /// /// The DMAPI . /// - Version DMApiVersion { get; } + Version? DMApiVersion { get; } /// /// Gets the associated with the . @@ -96,7 +96,7 @@ namespace Tgstation.Server.Host.Components.Session /// The to send. /// The for the operation. /// A resulting in the of /world/Topic(). - ValueTask SendCommand(TopicParameters parameters, CancellationToken cancellationToken); + ValueTask SendCommand(TopicParameters parameters, CancellationToken cancellationToken); /// /// Attempts to change the current to . diff --git a/src/Tgstation.Server.Host/Components/Session/ISessionControllerFactory.cs b/src/Tgstation.Server.Host/Components/Session/ISessionControllerFactory.cs index 26cc5e4a63..7058d3f4e4 100644 --- a/src/Tgstation.Server.Host/Components/Session/ISessionControllerFactory.cs +++ b/src/Tgstation.Server.Host/Components/Session/ISessionControllerFactory.cs @@ -16,14 +16,14 @@ namespace Tgstation.Server.Host.Components.Session /// Create a from a freshly launch DreamDaemon instance. /// /// The to use. - /// The current if any. + /// The current . if any. /// The to use. will be updated with the minumum required security level for the launch. /// If the should only validate the DMAPI then exit. /// The for the operation. /// A resulting in a new . ValueTask LaunchNew( IDmbProvider dmbProvider, - IEngineExecutableLock currentByondLock, + IEngineExecutableLock? currentByondLock, DreamDaemonLaunchParameters launchParameters, bool apiValidate, CancellationToken cancellationToken); @@ -34,7 +34,7 @@ namespace Tgstation.Server.Host.Components.Session /// The to use. /// The for the operation. /// A resulting in a new on success or on failure to reattach. - ValueTask Reattach( + ValueTask Reattach( ReattachInformation reattachInformation, CancellationToken cancellationToken); } diff --git a/src/Tgstation.Server.Host/Components/Session/ISessionPersistor.cs b/src/Tgstation.Server.Host/Components/Session/ISessionPersistor.cs index 5b1f1d7200..9d3806c317 100644 --- a/src/Tgstation.Server.Host/Components/Session/ISessionPersistor.cs +++ b/src/Tgstation.Server.Host/Components/Session/ISessionPersistor.cs @@ -29,7 +29,7 @@ namespace Tgstation.Server.Host.Components.Session /// /// The for the operation. /// A resulting in the stored if any. - ValueTask Load(CancellationToken cancellationToken); + ValueTask Load(CancellationToken cancellationToken); /// /// Clear any stored . diff --git a/src/Tgstation.Server.Host/Components/Session/LaunchResult.cs b/src/Tgstation.Server.Host/Components/Session/LaunchResult.cs index fd5785fd41..eb44b6e55f 100644 --- a/src/Tgstation.Server.Host/Components/Session/LaunchResult.cs +++ b/src/Tgstation.Server.Host/Components/Session/LaunchResult.cs @@ -11,12 +11,12 @@ namespace Tgstation.Server.Host.Components.Session /// /// The time it took for to return or the initial bridge request to process. If the startup timed out. /// - public TimeSpan? StartupTime { get; set; } + public TimeSpan? StartupTime { get; init; } /// /// The if it exited. /// - public int? ExitCode { get; set; } + public int? ExitCode { get; init; } /// public override string ToString() => String.Format(CultureInfo.InvariantCulture, "Exit Code: {0}, Time {1}ms", ExitCode, StartupTime?.TotalMilliseconds); diff --git a/src/Tgstation.Server.Host/Components/Session/ReattachInformation.cs b/src/Tgstation.Server.Host/Components/Session/ReattachInformation.cs index 6c429d2d0a..df2ce141d4 100644 --- a/src/Tgstation.Server.Host/Components/Session/ReattachInformation.cs +++ b/src/Tgstation.Server.Host/Components/Session/ReattachInformation.cs @@ -20,12 +20,12 @@ namespace Tgstation.Server.Host.Components.Session /// /// The initially used to launch DreamDaemon. Should be a different than . Should not be set if persisting the initial isn't necessary. /// - public IDmbProvider InitialDmb { get; set; } + public IDmbProvider? InitialDmb { get; set; } /// /// The for the DMAPI. /// - public RuntimeInformation RuntimeInformation { get; private set; } + public RuntimeInformation? RuntimeInformation { get; private set; } /// /// The which indicates when topic requests should timeout. @@ -47,7 +47,7 @@ namespace Tgstation.Server.Host.Components.Session public ReattachInformation( Models.ReattachInformation copy, IDmbProvider dmb, - IDmbProvider initialDmb, + IDmbProvider? initialDmb, TimeSpan topicRequestTimeout) : base(copy) { @@ -72,13 +72,12 @@ namespace Tgstation.Server.Host.Components.Session RuntimeInformation runtimeInformation, string accessIdentifier, ushort port) + : base(accessIdentifier) { Dmb = dmb ?? throw new ArgumentNullException(nameof(dmb)); ProcessId = process?.Id ?? throw new ArgumentNullException(nameof(process)); RuntimeInformation = runtimeInformation ?? throw new ArgumentNullException(nameof(runtimeInformation)); - AccessIdentifier = accessIdentifier ?? throw new ArgumentNullException(nameof(accessIdentifier)); - LaunchSecurityLevel = runtimeInformation.SecurityLevel; LaunchVisibility = runtimeInformation.Visibility; Port = port; diff --git a/src/Tgstation.Server.Host/Components/Session/SessionController.cs b/src/Tgstation.Server.Host/Components/Session/SessionController.cs index 2e57410eab..ad9bdf1728 100644 --- a/src/Tgstation.Server.Host/Components/Session/SessionController.cs +++ b/src/Tgstation.Server.Host/Components/Session/SessionController.cs @@ -16,6 +16,7 @@ using Tgstation.Server.Api.Models; using Tgstation.Server.Api.Models.Internal; using Tgstation.Server.Common.Extensions; using Tgstation.Server.Host.Components.Chat; +using Tgstation.Server.Host.Components.Chat.Commands; using Tgstation.Server.Host.Components.Deployment; using Tgstation.Server.Host.Components.Engine; using Tgstation.Server.Host.Components.Interop; @@ -59,7 +60,7 @@ namespace Tgstation.Server.Host.Components.Session public RebootState RebootState => ReattachInformation.RebootState; /// - public Version DMApiVersion { get; private set; } + public Version? DMApiVersion { get; private set; } /// public bool TerminationWasRequested { get; private set; } @@ -121,7 +122,7 @@ namespace Tgstation.Server.Host.Components.Session /// /// The for the . /// - readonly IBridgeRegistration bridgeRegistration; + readonly IBridgeRegistration? bridgeRegistration; /// /// The for the . @@ -131,7 +132,7 @@ namespace Tgstation.Server.Host.Components.Session /// /// The for the . /// - readonly IEngineExecutableLock byondLock; + readonly IEngineExecutableLock engineLock; /// /// The for the . @@ -196,7 +197,7 @@ namespace Tgstation.Server.Host.Components.Session /// /// for shutting down the server if it is taking too long after validation. /// - volatile Task postValidationShutdownTask; + volatile Task? postValidationShutdownTask; /// /// The number of currently active calls to from TgsReboot(). @@ -224,7 +225,7 @@ namespace Tgstation.Server.Host.Components.Session /// The value of . /// The owning . /// The value of . - /// The value of . + /// The value of . /// The value of . /// The used to populate . /// The value of . @@ -240,7 +241,7 @@ namespace Tgstation.Server.Host.Components.Session ReattachInformation reattachInformation, Api.Models.Instance metadata, IProcess process, - IEngineExecutableLock byondLock, + IEngineExecutableLock engineLock, Byond.TopicSender.ITopicClient byondTopicSender, IChatTrackingContext chatTrackingContext, IBridgeRegistrar bridgeRegistrar, @@ -257,7 +258,7 @@ namespace Tgstation.Server.Host.Components.Session ReattachInformation = reattachInformation ?? throw new ArgumentNullException(nameof(reattachInformation)); this.metadata = metadata ?? throw new ArgumentNullException(nameof(metadata)); this.process = process ?? throw new ArgumentNullException(nameof(process)); - this.byondLock = byondLock ?? throw new ArgumentNullException(nameof(byondLock)); + this.engineLock = engineLock ?? throw new ArgumentNullException(nameof(engineLock)); this.byondTopicSender = byondTopicSender ?? throw new ArgumentNullException(nameof(byondTopicSender)); this.chatTrackingContext = chatTrackingContext ?? throw new ArgumentNullException(nameof(chatTrackingContext)); ArgumentNullException.ThrowIfNull(bridgeRegistrar); @@ -338,16 +339,21 @@ namespace Tgstation.Server.Host.Components.Session Logger.LogTrace("Disposing..."); reattachTopicCts.Cancel(); - var semaphoreLockTask = TopicSendSemaphore.Lock(CancellationToken.None); // DCT: None available + var cancellationToken = CancellationToken.None; // DCT: None available + var semaphoreLockTask = TopicSendSemaphore.Lock(cancellationToken); if (!released) { - process.Terminate(); - await process.Lifetime; + await engineLock.StopServerProcess( + Logger, + process, + ReattachInformation.AccessIdentifier, + ReattachInformation.Port, + cancellationToken); } await process.DisposeAsync(); - byondLock.Dispose(); + engineLock.Dispose(); bridgeRegistration?.Dispose(); var regularDmbDisposeTask = ReattachInformation.Dmb.DisposeAsync(); var initialDmb = ReattachInformation.InitialDmb; @@ -367,7 +373,7 @@ namespace Tgstation.Server.Host.Components.Session } /// - public async ValueTask ProcessBridgeRequest(BridgeParameters parameters, CancellationToken cancellationToken) + public async ValueTask ProcessBridgeRequest(BridgeParameters parameters, CancellationToken cancellationToken) { ArgumentNullException.ThrowIfNull(parameters); @@ -393,13 +399,13 @@ namespace Tgstation.Server.Host.Components.Session ReattachInformation.Dmb.KeepAlive(); ReattachInformation.InitialDmb?.KeepAlive(); - byondLock.DoNotDeleteThisSession(); + engineLock.DoNotDeleteThisSession(); released = true; return DisposeAsync(); } /// - public ValueTask SendCommand(TopicParameters parameters, CancellationToken cancellationToken) + public ValueTask SendCommand(TopicParameters parameters, CancellationToken cancellationToken) => SendCommand(parameters, false, cancellationToken); /// @@ -430,10 +436,10 @@ namespace Tgstation.Server.Host.Components.Session public void AdjustPriority(bool higher) => process.AdjustPriority(higher); /// - public void Suspend() => process.Suspend(); + public void SuspendProcess() => process.SuspendProcess(); /// - public void Resume() => process.Resume(); + public void ResumeProcess() => process.ResumeProcess(); /// public IAsyncDisposable ReplaceDmbProvider(IDmbProvider dmbProvider) @@ -446,7 +452,10 @@ namespace Tgstation.Server.Host.Components.Session /// public async ValueTask InstanceRenamed(string newInstanceName, CancellationToken cancellationToken) { - ReattachInformation.RuntimeInformation.InstanceName = newInstanceName; + var runtimeInformation = ReattachInformation.RuntimeInformation; + if (runtimeInformation != null) + runtimeInformation.InstanceName = newInstanceName; + await SendCommand( TopicParameters.CreateInstanceRenamedTopicParameters(newInstanceName), cancellationToken); @@ -512,7 +521,7 @@ namespace Tgstation.Server.Host.Components.Session var reattachResponse = await SendCommand( new TopicParameters( assemblyInformationProvider.Version, - ReattachInformation.RuntimeInformation.ServerPort), + ReattachInformation.RuntimeInformation!.ServerPort), true, reattachTopicCts.Token); @@ -526,7 +535,7 @@ namespace Tgstation.Server.Host.Components.Session ? LogLevel.Warning : LogLevel.Debug, "DMAPI Interop v{interopVersion} isn't returning the TGS custom commands list. Functionality added in v5.2.0.", - CompileJob.DMApiVersion.Semver()); + CompileJob.DMApiVersion!.Semver()); } } @@ -574,7 +583,7 @@ namespace Tgstation.Server.Host.Components.Session /// The for the operation. /// A resulting in the for the request or if the request could not be dispatched. #pragma warning disable CA1502 // TODO: Decomplexify - async ValueTask ProcessBridgeCommand(BridgeParameters parameters, CancellationToken cancellationToken) + async ValueTask ProcessBridgeCommand(BridgeParameters parameters, CancellationToken cancellationToken) { var response = new BridgeResponse(); switch (parameters.CommandType) @@ -638,7 +647,7 @@ namespace Tgstation.Server.Host.Components.Session // TODO: When OD figures out how to unite port and topic_port, set an upper version bound on OD for this check if (DMApiVersion.Major != DMApiConstants.InteropVersion.Major - || (EngineVersion.Engine.Value == EngineType.OpenDream && DMApiVersion < new Version(5, 7))) + || (EngineVersion.Engine == EngineType.OpenDream && DMApiVersion < new Version(5, 7))) { apiValidationStatus = ApiValidationStatus.Incompatible; return BridgeError("Incompatible dmApiVersion!"); @@ -663,10 +672,11 @@ namespace Tgstation.Server.Host.Components.Session Logger.LogTrace("ApiValidationStatus set to {apiValidationStatus}", apiValidationStatus); + // we create new runtime info here because of potential .Dmb changes (i think. i forget...) response.RuntimeInformation = new RuntimeInformation( chatTrackingContext, ReattachInformation.Dmb, - ReattachInformation.RuntimeInformation.ServerVersion, + ReattachInformation.RuntimeInformation!.ServerVersion, ReattachInformation.RuntimeInformation.InstanceName, ReattachInformation.RuntimeInformation.SecurityLevel, ReattachInformation.RuntimeInformation.Visibility, @@ -677,11 +687,11 @@ namespace Tgstation.Server.Host.Components.Session { var newTopicPort = parameters.TopicPort.Value; Logger.LogInformation("Server is requesting use of port {topicPort} for topic communications", newTopicPort); - ReattachInformation.Port = newTopicPort; + ReattachInformation.TopicPort = newTopicPort; } // Load custom commands - chatTrackingContext.CustomCommands = parameters.CustomCommands; + chatTrackingContext.CustomCommands = parameters.CustomCommands ?? Array.Empty(); chatTrackingContext.Active = true; Interlocked.Exchange(ref startupTcs, new TaskCompletionSource()).SetResult(); break; @@ -731,7 +741,7 @@ namespace Tgstation.Server.Host.Components.Session /// The to send. /// The for the operation. /// A resulting in the of the topic request. - async ValueTask SendTopicRequest(TopicParameters parameters, CancellationToken cancellationToken) + async ValueTask SendTopicRequest(TopicParameters parameters, CancellationToken cancellationToken) { parameters.AccessIdentifier = ReattachInformation.AccessIdentifier; @@ -757,14 +767,14 @@ namespace Tgstation.Server.Host.Components.Session var payloadId = NextPayloadId; // AccessIdentifer is just noise in a chunked request - parameters.AccessIdentifier = null; + parameters.AccessIdentifier = null!; GenerateQueryString(parameters, out json); // yes, this straight up ignores unicode, precalculating it is useless when we don't // even know if the UTF8 bytes of the url encoded chunk will fit the window until we do said encoding var fullPayloadSize = (uint)json.Length; - List chunkQueryStrings = null; + List? chunkQueryStrings = null; for (var chunkCount = 2; chunkQueryStrings == null; ++chunkCount) { var standardChunkSize = fullPayloadSize / chunkCount; @@ -810,7 +820,7 @@ namespace Tgstation.Server.Host.Components.Session Logger.LogTrace("Chunking topic request ({totalChunks} total)...", chunkQueryStrings.Count); - CombinedTopicResponse combinedResponse = null; + CombinedTopicResponse? combinedResponse = null; bool LogRequestIssue(bool possiblyFromCompletedRequest) { if (combinedResponse?.InteropResponse == null || combinedResponse.InteropResponse.ErrorMessage != null) @@ -834,11 +844,12 @@ namespace Tgstation.Server.Host.Components.Session return null; } - while ((combinedResponse.InteropResponse.MissingChunks?.Count ?? 0) > 0) + while ((combinedResponse?.InteropResponse?.MissingChunks?.Count ?? 0) > 0) { Logger.LogWarning("DD is still missing some chunks of topic request P{payloadId}! Sending missing chunks...", payloadId); - var lastIndex = combinedResponse.InteropResponse.MissingChunks.Last(); - foreach (var missingChunkIndex in combinedResponse.InteropResponse.MissingChunks) + var missingChunks = combinedResponse!.InteropResponse!.MissingChunks!; + var lastIndex = missingChunks.Last(); + foreach (var missingChunkIndex in missingChunks) { var chunkCommandString = chunkQueryStrings[(int)missingChunkIndex]; combinedResponse = await SendRawTopic(chunkCommandString, topicPriority, cancellationToken); @@ -874,7 +885,7 @@ namespace Tgstation.Server.Host.Components.Session /// If this is a priority message. If so, the topic will make 5 attempts to send unless BYOND reboots or exits. /// The for the operation. /// A resulting in the of the topic request. - async ValueTask SendRawTopic(string queryString, bool priority, CancellationToken cancellationToken) + async ValueTask SendRawTopic(string queryString, bool priority, CancellationToken cancellationToken) { if (disposed) { @@ -883,8 +894,8 @@ namespace Tgstation.Server.Host.Components.Session return null; } - var targetPort = ReattachInformation.Port; - Byond.TopicSender.TopicResponse byondResponse; + var targetPort = ReattachInformation.TopicPort ?? ReattachInformation.Port; + Byond.TopicSender.TopicResponse? byondResponse; using (await TopicSendSemaphore.Lock(cancellationToken)) byondResponse = await byondTopicSender.SendWithOptionalPriority( asyncDelayer, @@ -906,7 +917,7 @@ namespace Tgstation.Server.Host.Components.Session var topicReturn = byondResponse.StringData; - TopicResponse interopResponse = null; + TopicResponse? interopResponse = null; if (topicReturn != null) try { @@ -927,7 +938,7 @@ namespace Tgstation.Server.Host.Components.Session /// If waiting for the should be bypassed. /// The for the operation. /// A resulting in the of /world/Topic(). - async ValueTask SendCommand(TopicParameters parameters, bool bypassLaunchResult, CancellationToken cancellationToken) + async ValueTask SendCommand(TopicParameters parameters, bool bypassLaunchResult, CancellationToken cancellationToken) { ArgumentNullException.ThrowIfNull(parameters); @@ -989,7 +1000,7 @@ namespace Tgstation.Server.Host.Components.Session } } - TopicResponse fullResponse = null; + TopicResponse? fullResponse = null; var lifetimeWatchingTask = CancelIfLifetimeElapses(); try { @@ -1007,14 +1018,14 @@ namespace Tgstation.Server.Host.Components.Session { Logger.LogTrace("Topic response is chunked..."); - ChunkData nextChunk = combinedResponse.InteropResponse.Chunk; + ChunkData? nextChunk = combinedResponse.InteropResponse.Chunk; do { var nextRequest = await ProcessChunk( (completedResponse, _) => { fullResponse = completedResponse; - return ValueTask.FromResult(null); + return ValueTask.FromResult(null); }, error => { diff --git a/src/Tgstation.Server.Host/Components/Session/SessionControllerFactory.cs b/src/Tgstation.Server.Host/Components/Session/SessionControllerFactory.cs index a54f9f8874..70bed0f5ba 100644 --- a/src/Tgstation.Server.Host/Components/Session/SessionControllerFactory.cs +++ b/src/Tgstation.Server.Host/Components/Session/SessionControllerFactory.cs @@ -225,7 +225,7 @@ namespace Tgstation.Server.Host.Components.Session #pragma warning disable CA1506 // TODO: Decomplexify public async ValueTask LaunchNew( IDmbProvider dmbProvider, - IEngineExecutableLock currentByondLock, + IEngineExecutableLock? currentByondLock, DreamDaemonLaunchParameters launchParameters, bool apiValidate, CancellationToken cancellationToken) @@ -268,17 +268,17 @@ namespace Tgstation.Server.Host.Components.Session dmbProvider.CompileJob.Id); // mad this isn't abstracted but whatever - var engineType = dmbProvider.EngineVersion.Engine.Value; + var engineType = dmbProvider.EngineVersion.Engine!.Value; if (engineType == EngineType.Byond) await CheckPagerIsNotRunning(); await PortBindTest(launchParameters.Port.Value, engineType, cancellationToken); - string outputFilePath = null; + string? outputFilePath = null; var preserveLogFile = true; var hasStandardOutput = engineLock.HasStandardOutput; - if (launchParameters.LogOutput.Value) + if (launchParameters.LogOutput!.Value) { var now = DateTimeOffset.UtcNow; var dateDirectory = diagnosticsIOManager.ConcatPath(DreamDaemonLogsPath, now.ToString("yyyy-MM-dd", CultureInfo.InvariantCulture)); @@ -320,8 +320,8 @@ namespace Tgstation.Server.Host.Components.Session var runtimeInformation = CreateRuntimeInformation( dmbProvider, chatTrackingContext, - launchParameters.SecurityLevel.Value, - launchParameters.Visibility.Value, + launchParameters.SecurityLevel!.Value, + launchParameters.Visibility!.Value, apiValidate); var reattachInformation = new ReattachInformation( @@ -333,7 +333,7 @@ namespace Tgstation.Server.Host.Components.Session var byondTopicSender = topicClientFactory.CreateTopicClient( TimeSpan.FromMilliseconds( - launchParameters.TopicRequestTimeout.Value)); + launchParameters.TopicRequestTimeout!.Value)); var sessionController = new SessionController( reattachInformation, @@ -385,7 +385,7 @@ namespace Tgstation.Server.Host.Components.Session #pragma warning restore CA1506 /// - public async ValueTask Reattach( + public async ValueTask Reattach( ReattachInformation reattachInformation, CancellationToken cancellationToken) { @@ -450,19 +450,21 @@ namespace Tgstation.Server.Host.Components.Session } catch { - chatTrackingContext.Dispose(); + chatTrackingContext?.Dispose(); throw; } } catch { - await process.DisposeAsync(); + if (process != null) + await process.DisposeAsync(); + throw; } } catch { - engineLock.Dispose(); + engineLock?.Dispose(); throw; } } @@ -474,7 +476,7 @@ namespace Tgstation.Server.Host.Components.Session /// The . /// The . /// The secure string to use for the session. - /// The full path to log DreamDaemon output to. + /// The optional full path to log DreamDaemon output to. /// If we are only validating the DMAPI then exiting. /// The for the operation. /// A resulting in the DreamDaemon . @@ -483,7 +485,7 @@ namespace Tgstation.Server.Host.Components.Session IEngineExecutableLock engineLock, DreamDaemonLaunchParameters launchParameters, string accessIdentifier, - string logFilePath, + string? logFilePath, bool apiValidate, CancellationToken cancellationToken) { @@ -555,19 +557,19 @@ namespace Tgstation.Server.Host.Components.Session /// If , will be deleted. /// The for the operation. /// A representing the running operation. - async ValueTask LogDDOutput(IProcess process, string outputFilePath, bool cliSupported, bool preserveFile, CancellationToken cancellationToken) + async ValueTask LogDDOutput(IProcess process, string? outputFilePath, bool cliSupported, bool preserveFile, CancellationToken cancellationToken) { try { - string ddOutput = null; + string? ddOutput = null; if (cliSupported) - ddOutput = await process.GetCombinedOutput(cancellationToken); + ddOutput = (await process.GetCombinedOutput(cancellationToken))!; if (ddOutput == null) try { var dreamDaemonLogBytes = await gameIOManager.ReadAllBytes( - outputFilePath, + outputFilePath!, cancellationToken); ddOutput = Encoding.UTF8.GetString(dreamDaemonLogBytes); @@ -578,7 +580,7 @@ namespace Tgstation.Server.Host.Components.Session try { logger.LogTrace("Deleting temporary log file {path}...", outputFilePath); - await gameIOManager.DeleteFile(outputFilePath, cancellationToken); + await gameIOManager.DeleteFile(outputFilePath!, cancellationToken); } catch (Exception ex) { @@ -618,7 +620,7 @@ namespace Tgstation.Server.Host.Components.Session chatTrackingContext, dmbProvider, assemblyInformationProvider.Version, - instance.Name, + instance.Name!, securityLevel, visibility, serverPortProvider.HttpApiPort, diff --git a/src/Tgstation.Server.Host/Components/Session/SessionPersistor.cs b/src/Tgstation.Server.Host/Components/Session/SessionPersistor.cs index e932fa6ec7..a916ea792b 100644 --- a/src/Tgstation.Server.Host/Components/Session/SessionPersistor.cs +++ b/src/Tgstation.Server.Host/Components/Session/SessionPersistor.cs @@ -8,6 +8,7 @@ using Microsoft.Extensions.Logging; using Tgstation.Server.Host.Components.Deployment; using Tgstation.Server.Host.Database; +using Tgstation.Server.Host.Models; using Tgstation.Server.Host.System; using Z.EntityFramework.Plus; @@ -73,11 +74,10 @@ namespace Tgstation.Server.Host.Components.Session await ClearImpl(db, false, cancellationToken); - var dbReattachInfo = new Models.ReattachInformation + var dbReattachInfo = new Models.ReattachInformation(reattachInformation.AccessIdentifier) { - AccessIdentifier = reattachInformation.AccessIdentifier, - CompileJobId = reattachInformation.Dmb.CompileJob.Id.Value, - InitialCompileJobId = reattachInformation.InitialDmb?.CompileJob.Id.Value, + CompileJobId = reattachInformation.Dmb.CompileJob.Require(x => x.Id), + InitialCompileJobId = reattachInformation.InitialDmb?.CompileJob.Require(x => x.Id), Port = reattachInformation.Port, ProcessId = reattachInformation.ProcessId, RebootState = reattachInformation.RebootState, @@ -88,7 +88,7 @@ namespace Tgstation.Server.Host.Components.Session db.ReattachInformations.Add(dbReattachInfo); await db.Save(cancellationToken); - reattachInformation.Id = dbReattachInfo.Id.Value; + reattachInformation.Id = dbReattachInfo.Id!.Value; logger.LogDebug("Saved reattach information: {info}", reattachInformation); }); @@ -101,7 +101,7 @@ namespace Tgstation.Server.Host.Components.Session logger.LogTrace("Updating reattach information: {info}...", reattachInformation); - var dbReattachInfo = new Models.ReattachInformation + var dbReattachInfo = new Models.ReattachInformation(String.Empty) { Id = reattachInformation.Id.Value, }; @@ -109,8 +109,8 @@ namespace Tgstation.Server.Host.Components.Session db.ReattachInformations.Attach(dbReattachInfo); dbReattachInfo.AccessIdentifier = reattachInformation.AccessIdentifier; - dbReattachInfo.CompileJobId = reattachInformation.Dmb.CompileJob.Id.Value; - dbReattachInfo.InitialCompileJobId = reattachInformation.InitialDmb?.CompileJob.Id.Value; + dbReattachInfo.CompileJobId = reattachInformation.Dmb.CompileJob.Require(x => x.Id); + dbReattachInfo.InitialCompileJobId = reattachInformation.InitialDmb?.CompileJob.Require(x => x.Id); dbReattachInfo.Port = reattachInformation.Port; dbReattachInfo.ProcessId = reattachInformation.ProcessId; dbReattachInfo.RebootState = reattachInformation.RebootState; @@ -123,9 +123,9 @@ namespace Tgstation.Server.Host.Components.Session }); /// - public async ValueTask Load(CancellationToken cancellationToken) + public async ValueTask Load(CancellationToken cancellationToken) { - Models.ReattachInformation result = null; + Models.ReattachInformation? result = null; TimeSpan? topicTimeout = null; async ValueTask KillProcess(Models.ReattachInformation reattachInfo) @@ -159,19 +159,19 @@ namespace Tgstation.Server.Host.Components.Session var dbReattachInfos = await db .ReattachInformations .AsQueryable() - .Where(x => x.CompileJob.Job.Instance.Id == metadata.Id) + .Where(x => x.CompileJob!.Job.Instance!.Id == metadata.Id) .Include(x => x.CompileJob) .Include(x => x.InitialCompileJob) .ToListAsync(cancellationToken); result = dbReattachInfos.FirstOrDefault(); - if (result == default) + if (result == null) return; var timeoutMilliseconds = await db .Instances .AsQueryable() .Where(x => x.Id == metadata.Id) - .Select(x => x.DreamDaemonSettings.TopicRequestTimeout) + .Select(x => x.DreamDaemonSettings!.TopicRequestTimeout) .FirstOrDefaultAsync(cancellationToken); if (timeoutMilliseconds == default) @@ -206,7 +206,7 @@ namespace Tgstation.Server.Host.Components.Session return null; } - var dmb = await dmbFactory.FromCompileJob(result.CompileJob, cancellationToken); + var dmb = await dmbFactory.FromCompileJob(result!.CompileJob!, cancellationToken); if (dmb == null) { logger.LogError("Unable to reattach! Could not load .dmb!"); @@ -224,7 +224,7 @@ namespace Tgstation.Server.Host.Components.Session return null; } - IDmbProvider initialDmb = null; + IDmbProvider? initialDmb = null; if (result.InitialCompileJob != null) { logger.LogTrace("Loading initial compile job..."); @@ -265,7 +265,7 @@ namespace Tgstation.Server.Host.Components.Session var baseQuery = databaseContext .ReattachInformations .AsQueryable() - .Where(x => x.CompileJob.Job.Instance.Id == metadata.Id); + .Where(x => x.CompileJob!.Job.Instance!.Id == metadata.Id); if (instant) await baseQuery diff --git a/src/Tgstation.Server.Host/Components/Session/TopicClientFactory.cs b/src/Tgstation.Server.Host/Components/Session/TopicClientFactory.cs index 29768e7604..3ed153a641 100644 --- a/src/Tgstation.Server.Host/Components/Session/TopicClientFactory.cs +++ b/src/Tgstation.Server.Host/Components/Session/TopicClientFactory.cs @@ -12,7 +12,7 @@ namespace Tgstation.Server.Host.Components.Session /// /// The for created s. /// - readonly ILogger logger; + readonly ILogger? logger; /// /// Initializes a new instance of the class. diff --git a/src/Tgstation.Server.Host/Components/StaticFiles/Configuration.cs b/src/Tgstation.Server.Host/Components/StaticFiles/Configuration.cs index 5e871c2551..8261afe918 100644 --- a/src/Tgstation.Server.Host/Components/StaticFiles/Configuration.cs +++ b/src/Tgstation.Server.Host/Components/StaticFiles/Configuration.cs @@ -78,7 +78,7 @@ namespace Tgstation.Server.Host.Components.StaticFiles eventType => new KeyValuePair>( eventType, typeof(EventType) - .GetField(eventType.ToString()) + .GetField(eventType.ToString())! .GetCustomAttributes(false) .OfType() .First() @@ -199,7 +199,7 @@ namespace Tgstation.Server.Host.Components.StaticFiles } /// - public async ValueTask CopyDMFilesTo(string dmeFile, string destination, CancellationToken cancellationToken) + public async ValueTask CopyDMFilesTo(string dmeFile, string destination, CancellationToken cancellationToken) { using (await SemaphoreSlimContext.Lock(semaphore, cancellationToken)) { @@ -232,12 +232,19 @@ namespace Tgstation.Server.Host.Components.StaticFiles static string IncludeLine(string filePath) => String.Format(CultureInfo.InvariantCulture, "#include \"{0}\"", filePath); - return new ServerSideModifications(headFileExistsTask.Result ? IncludeLine(CodeModificationsHeadFile) : null, tailFileExistsTask.Result ? IncludeLine(CodeModificationsTailFile) : null, false); + return new ServerSideModifications( + headFileExistsTask.Result + ? IncludeLine(CodeModificationsHeadFile) + : null, + tailFileExistsTask.Result + ? IncludeLine(CodeModificationsTailFile) + : null, + false); } } /// - public async ValueTask> ListDirectory(string configurationRelativePath, ISystemIdentity systemIdentity, CancellationToken cancellationToken) + public async ValueTask?> ListDirectory(string? configurationRelativePath, ISystemIdentity? systemIdentity, CancellationToken cancellationToken) { await EnsureDirectories(cancellationToken); var path = ValidateConfigRelativePath(configurationRelativePath); @@ -284,12 +291,12 @@ namespace Tgstation.Server.Host.Components.StaticFiles } /// - public async ValueTask Read(string configurationRelativePath, ISystemIdentity systemIdentity, CancellationToken cancellationToken) + public async ValueTask Read(string configurationRelativePath, ISystemIdentity? systemIdentity, CancellationToken cancellationToken) { await EnsureDirectories(cancellationToken); var path = ValidateConfigRelativePath(configurationRelativePath); - ConfigurationFileResponse result = null; + ConfigurationFileResponse? result = null; void ReadImpl() { @@ -319,7 +326,7 @@ namespace Tgstation.Server.Host.Components.StaticFiles }, async cancellationToken => { - FileStream result = null; + FileStream? result = null; void GetFileStream() { result = ioManager.GetFileStream(path, false); @@ -330,7 +337,7 @@ namespace Tgstation.Server.Host.Components.StaticFiles else await systemIdentity.RunImpersonated(GetFileStream, cancellationToken); - return result; + return result!; }, path, false)); @@ -451,12 +458,12 @@ namespace Tgstation.Server.Host.Components.StaticFiles } /// - public async ValueTask Write(string configurationRelativePath, ISystemIdentity systemIdentity, string previousHash, CancellationToken cancellationToken) + public async ValueTask Write(string configurationRelativePath, ISystemIdentity? systemIdentity, string? previousHash, CancellationToken cancellationToken) { await EnsureDirectories(cancellationToken); var path = ValidateConfigRelativePath(configurationRelativePath); - ConfigurationFileResponse result = null; + ConfigurationFileResponse? result = null; void WriteImpl() { @@ -561,7 +568,7 @@ namespace Tgstation.Server.Host.Components.StaticFiles } /// - public async ValueTask CreateDirectory(string configurationRelativePath, ISystemIdentity systemIdentity, CancellationToken cancellationToken) + public async ValueTask CreateDirectory(string configurationRelativePath, ISystemIdentity? systemIdentity, CancellationToken cancellationToken) { await EnsureDirectories(cancellationToken); var path = ValidateConfigRelativePath(configurationRelativePath); @@ -583,7 +590,7 @@ namespace Tgstation.Server.Host.Components.StaticFiles await systemIdentity.RunImpersonated(DoCreate, cancellationToken); } - return result.Value; + return result!.Value; } /// @@ -593,7 +600,7 @@ namespace Tgstation.Server.Host.Components.StaticFiles public Task StopAsync(CancellationToken cancellationToken) => EnsureDirectories(cancellationToken); /// - public async ValueTask HandleEvent(EventType eventType, IEnumerable parameters, bool deploymentPipeline, CancellationToken cancellationToken) + public async ValueTask HandleEvent(EventType eventType, IEnumerable parameters, bool deploymentPipeline, CancellationToken cancellationToken) { ArgumentNullException.ThrowIfNull(parameters); @@ -630,6 +637,9 @@ namespace Tgstation.Server.Host.Components.StaticFiles ' ', parameters.Select(arg => { + if (arg == null) + return "(NULL)"; + if (!arg.Contains(' ', StringComparison.Ordinal)) return arg; @@ -657,7 +667,7 @@ namespace Tgstation.Server.Host.Components.StaticFiles } /// - public async ValueTask DeleteDirectory(string configurationRelativePath, ISystemIdentity systemIdentity, CancellationToken cancellationToken) + public async ValueTask DeleteDirectory(string configurationRelativePath, ISystemIdentity? systemIdentity, CancellationToken cancellationToken) { await EnsureDirectories(cancellationToken); var path = ValidateConfigRelativePath(configurationRelativePath); @@ -735,16 +745,16 @@ namespace Tgstation.Server.Host.Components.StaticFiles /// /// A relative path in the instance's configuration directory. /// The full on-disk path of . - string ValidateConfigRelativePath(string configurationRelativePath) + string ValidateConfigRelativePath(string? configurationRelativePath) { var nullOrEmptyCheck = String.IsNullOrEmpty(configurationRelativePath); if (nullOrEmptyCheck) configurationRelativePath = DefaultIOManager.CurrentDirectory; - if (configurationRelativePath[0] == Path.DirectorySeparatorChar || configurationRelativePath[0] == Path.AltDirectorySeparatorChar) + if (configurationRelativePath![0] == Path.DirectorySeparatorChar || configurationRelativePath[0] == Path.AltDirectorySeparatorChar) configurationRelativePath = DefaultIOManager.CurrentDirectory + configurationRelativePath; var resolved = ioManager.ResolvePath(configurationRelativePath); var local = !nullOrEmptyCheck ? ioManager.ResolvePath() : null; - if (!nullOrEmptyCheck && resolved.Length < local.Length) // .. fuccbois + if (!nullOrEmptyCheck && resolved.Length < local!.Length) // .. fuccbois throw new InvalidOperationException("Attempted to access file outside of configuration manager!"); return resolved; } diff --git a/src/Tgstation.Server.Host/Components/StaticFiles/IConfiguration.cs b/src/Tgstation.Server.Host/Components/StaticFiles/IConfiguration.cs index 59546a044e..d156df54cb 100644 --- a/src/Tgstation.Server.Host/Components/StaticFiles/IConfiguration.cs +++ b/src/Tgstation.Server.Host/Components/StaticFiles/IConfiguration.cs @@ -22,7 +22,7 @@ namespace Tgstation.Server.Host.Components.StaticFiles /// Path to the destination folder. /// The for the operation. /// A resulting in the if any. - ValueTask CopyDMFilesTo(string dmeFile, string destination, CancellationToken cancellationToken); + ValueTask CopyDMFilesTo(string dmeFile, string destination, CancellationToken cancellationToken); /// /// Symlinks all directories in the GameData directory to . @@ -39,7 +39,7 @@ namespace Tgstation.Server.Host.Components.StaticFiles /// The for the operation. If , the operation will be performed as the user of the . /// The for the operation. /// A resulting in an of the s for the items in the directory. and will both be . will be returned if the operation failed due to access contention. - ValueTask> ListDirectory(string configurationRelativePath, ISystemIdentity systemIdentity, CancellationToken cancellationToken); + ValueTask?> ListDirectory(string? configurationRelativePath, ISystemIdentity? systemIdentity, CancellationToken cancellationToken); /// /// Reads a given . @@ -48,7 +48,7 @@ namespace Tgstation.Server.Host.Components.StaticFiles /// The for the operation. If , the operation will be performed as the user of the . /// The for the operation. /// A resulting in the of the file. will be returned if the operation failed due to access contention. - ValueTask Read(string configurationRelativePath, ISystemIdentity systemIdentity, CancellationToken cancellationToken); + ValueTask Read(string configurationRelativePath, ISystemIdentity? systemIdentity, CancellationToken cancellationToken); /// /// Create an empty directory at . @@ -57,7 +57,7 @@ namespace Tgstation.Server.Host.Components.StaticFiles /// The for the operation. If , the operation will be performed as the user of the . /// The for the operation. Usage may result in partial writes. /// A resulting in if the directory already existed, otherwise. will be returned if the operation failed due to access contention. - ValueTask CreateDirectory(string configurationRelativePath, ISystemIdentity systemIdentity, CancellationToken cancellationToken); + ValueTask CreateDirectory(string configurationRelativePath, ISystemIdentity? systemIdentity, CancellationToken cancellationToken); /// /// Attempt to delete an empty directory at . @@ -66,7 +66,7 @@ namespace Tgstation.Server.Host.Components.StaticFiles /// The for the operation. If , the operation will be performed as the user of the . /// The for the operation. /// A resulting in if the directory was empty and deleted, otherwise. will be returned if the operation failed due to access contention. - ValueTask DeleteDirectory(string configurationRelativePath, ISystemIdentity systemIdentity, CancellationToken cancellationToken); + ValueTask DeleteDirectory(string configurationRelativePath, ISystemIdentity? systemIdentity, CancellationToken cancellationToken); /// /// Writes to a given . @@ -76,6 +76,6 @@ namespace Tgstation.Server.Host.Components.StaticFiles /// The hash any existing file must match in order for the write to succeed. /// The for the operation. Usage may result in partial writes. /// A resulting in the updated and associated writing . will be returned if the operation failed due to access contention. - ValueTask Write(string configurationRelativePath, ISystemIdentity systemIdentity, string previousHash, CancellationToken cancellationToken); + ValueTask Write(string configurationRelativePath, ISystemIdentity? systemIdentity, string? previousHash, CancellationToken cancellationToken); } } diff --git a/src/Tgstation.Server.Host/Components/StaticFiles/ServerSideModifications.cs b/src/Tgstation.Server.Host/Components/StaticFiles/ServerSideModifications.cs index 6b7e358f0b..e28920a06e 100644 --- a/src/Tgstation.Server.Host/Components/StaticFiles/ServerSideModifications.cs +++ b/src/Tgstation.Server.Host/Components/StaticFiles/ServerSideModifications.cs @@ -13,12 +13,12 @@ /// /// The #include line which should be added to the beginning of the .dme if any. /// - public string HeadIncludeLine { get; } + public string? HeadIncludeLine { get; } /// /// The #include line which should be added to the end of the .dme if any. /// - public string TailIncludeLine { get; } + public string? TailIncludeLine { get; } /// /// Initializes a new instance of the class. @@ -26,7 +26,7 @@ /// The value of . /// The value of . /// The value of . - public ServerSideModifications(string headIncludeLine, string tailIncludeLine, bool totalDmeOverwrite) + public ServerSideModifications(string? headIncludeLine, string? tailIncludeLine, bool totalDmeOverwrite) { HeadIncludeLine = headIncludeLine; TailIncludeLine = tailIncludeLine; diff --git a/src/Tgstation.Server.Host/Components/Watchdog/AdvancedWatchdog.cs b/src/Tgstation.Server.Host/Components/Watchdog/AdvancedWatchdog.cs index 4495a99fe3..884541e9fb 100644 --- a/src/Tgstation.Server.Host/Components/Watchdog/AdvancedWatchdog.cs +++ b/src/Tgstation.Server.Host/Components/Watchdog/AdvancedWatchdog.cs @@ -28,7 +28,7 @@ namespace Tgstation.Server.Host.Components.Watchdog /// /// The for . /// - protected SwappableDmbProvider ActiveSwappable { get; private set; } + protected SwappableDmbProvider? ActiveSwappable { get; private set; } /// /// The for the . @@ -43,12 +43,12 @@ namespace Tgstation.Server.Host.Components.Watchdog /// /// The active for . /// - SwappableDmbProvider pendingSwappable; + SwappableDmbProvider? pendingSwappable; /// /// The representing the cleanup of an unused . /// - volatile TaskCompletionSource deploymentCleanupGate; + volatile TaskCompletionSource? deploymentCleanupGate; /// /// Initializes a new instance of the class. @@ -141,11 +141,12 @@ namespace Tgstation.Server.Host.Components.Watchdog ValueTask RunPrequel() => BeforeApplyDmb(pendingSwappable.CompileJob, cancellationToken); var needToSwap = !pendingSwappable.Swapped; + var controller = Server!; if (needToSwap) { // IMPORTANT: THE SESSIONCONTROLLER SHOULD STILL BE PROCESSING THE BRIDGE REQUEST SO WE KNOW DD IS SLEEPING // OTHERWISE, IT COULD RETURN TO /world/Reboot() TOO EARLY AND LOAD THE WRONG .DMB - if (!Server.ProcessingRebootBridgeRequest) + if (!controller.ProcessingRebootBridgeRequest) { // integration test logging will catch this Logger.LogError( @@ -168,7 +169,7 @@ namespace Tgstation.Server.Host.Components.Watchdog if (needToSwap) await PerformDmbSwap(pendingSwappable, cancellationToken); - var currentCompileJobId = Server.ReattachInformation.Dmb.CompileJob.Id; + var currentCompileJobId = controller.ReattachInformation.Dmb.CompileJob.Id; await DrainDeploymentCleanupTasks(false); @@ -176,7 +177,7 @@ namespace Tgstation.Server.Host.Components.Watchdog var localDeploymentCleanupGate = new TaskCompletionSource(); async Task CleanupLingeringDeployment() { - var lingeringDeploymentExpirySeconds = ActiveLaunchParameters.StartupTimeout.Value; + var lingeringDeploymentExpirySeconds = ActiveLaunchParameters.StartupTimeout!.Value; Logger.LogDebug( "Holding old deployment {compileJobId} for up to {expiry} seconds...", currentCompileJobId, @@ -209,7 +210,7 @@ namespace Tgstation.Server.Host.Components.Watchdog lock (deploymentCleanupTasks) { - lingeringDeployment = Server.ReplaceDmbProvider(pendingSwappable); + lingeringDeployment = controller.ReplaceDmbProvider(pendingSwappable); deploymentCleanupTasks.Add( CleanupLingeringDeployment()); } @@ -217,7 +218,7 @@ namespace Tgstation.Server.Host.Components.Watchdog ActiveSwappable = pendingSwappable; pendingSwappable = null; - await SessionPersistor.Update(Server.ReattachInformation, cancellationToken); + await SessionPersistor.Update(controller.ReattachInformation, cancellationToken); await updateTask; } else @@ -232,7 +233,7 @@ namespace Tgstation.Server.Host.Components.Watchdog IDmbProvider compileJobProvider = DmbFactory.LockNextDmb(1); bool canSeamlesslySwap = CanUseSwappableDmbProvider(compileJobProvider); if (canSeamlesslySwap) - if (compileJobProvider.CompileJob.EngineVersion != ActiveCompileJob.EngineVersion) + if (compileJobProvider.CompileJob.EngineVersion != ActiveCompileJob!.EngineVersion) { // have to do a graceful restart Logger.LogDebug( @@ -260,11 +261,11 @@ namespace Tgstation.Server.Host.Components.Watchdog return; } - SwappableDmbProvider swappableProvider = null; + SwappableDmbProvider? swappableProvider = null; try { swappableProvider = CreateSwappableDmbProvider(compileJobProvider); - if (ActiveCompileJob.DMApiVersion == null) + if (ActiveCompileJob!.DMApiVersion == null) { Logger.LogWarning("Active compile job has no DMAPI! Commencing immediate .dmb swap. Note this behavior is known to be buggy in some DM code contexts. See https://github.com/tgstation/tgstation-server/issues/1550"); await PerformDmbSwap(swappableProvider, cancellationToken); @@ -348,9 +349,9 @@ namespace Tgstation.Server.Host.Components.Watchdog /// if swapping is possible, otherwise. bool CanUseSwappableDmbProvider(IDmbProvider dmbProvider) { - if (dmbProvider.EngineVersion.Engine.Value != EngineType.Byond) + if (dmbProvider.EngineVersion.Engine != EngineType.Byond) { - Logger.LogDebug("Not using SwappableDmbProvider for engine type {engineType}", dmbProvider.EngineVersion.Engine.Value); + Logger.LogDebug("Not using SwappableDmbProvider for engine type {engineType}", dmbProvider.EngineVersion.Engine); return false; } @@ -364,7 +365,7 @@ namespace Tgstation.Server.Host.Components.Watchdog /// A representing the running operation. async ValueTask InitialLink(CancellationToken cancellationToken) { - await ActiveSwappable.FinishActivationPreparation(cancellationToken); + await ActiveSwappable!.FinishActivationPreparation(cancellationToken); Logger.LogTrace("Linking compile job..."); await ActiveSwappable.MakeActive(cancellationToken); } @@ -382,10 +383,10 @@ namespace Tgstation.Server.Host.Components.Watchdog await newProvider.FinishActivationPreparation(cancellationToken); var suspended = false; - var server = Server; + var server = Server!; try { - server.Suspend(); + server.SuspendProcess(); suspended = true; } catch (Exception ex) @@ -402,7 +403,7 @@ namespace Tgstation.Server.Host.Components.Watchdog { // Let this throw hard if it fails if (suspended) - server.Resume(); + server.ResumeProcess(); } } diff --git a/src/Tgstation.Server.Host/Components/Watchdog/BasicWatchdog.cs b/src/Tgstation.Server.Host/Components/Watchdog/BasicWatchdog.cs index 5beaab473c..10b6a7b19c 100644 --- a/src/Tgstation.Server.Host/Components/Watchdog/BasicWatchdog.cs +++ b/src/Tgstation.Server.Host/Components/Watchdog/BasicWatchdog.cs @@ -33,7 +33,7 @@ namespace Tgstation.Server.Host.Components.Watchdog /// /// The single . /// - protected ISessionController Server { get; private set; } + protected ISessionController? Server { get; private set; } /// /// If the server is set to gracefully reboot due to a pending dmb or settings change. @@ -109,16 +109,17 @@ namespace Tgstation.Server.Host.Components.Watchdog /// protected override async ValueTask HandleMonitorWakeup(MonitorActivationReason reason, CancellationToken cancellationToken) { + var controller = Server!; switch (reason) { case MonitorActivationReason.ActiveServerCrashed: - var eventType = Server.TerminationWasRequested + var eventType = controller.TerminationWasRequested ? EventType.WorldEndProcess : EventType.WatchdogCrash; await HandleEventImpl(eventType, Enumerable.Empty(), false, cancellationToken); - var exitWord = Server.TerminationWasRequested ? "exited" : "crashed"; - if (Server.RebootState == Session.RebootState.Shutdown) + var exitWord = controller.TerminationWasRequested ? "exited" : "crashed"; + if (controller.RebootState == Session.RebootState.Shutdown) { // the time for graceful shutdown is now Chat.QueueWatchdogMessage( @@ -136,7 +137,7 @@ namespace Tgstation.Server.Host.Components.Watchdog exitWord)); return MonitorAction.Restart; case MonitorActivationReason.ActiveServerRebooted: - var rebootState = Server.RebootState; + var rebootState = controller.RebootState; if (gracefulRebootRequired && rebootState == Session.RebootState.Normal) { Logger.LogError("Watchdog reached normal reboot state with gracefulRebootRequired set!"); @@ -144,7 +145,7 @@ namespace Tgstation.Server.Host.Components.Watchdog } gracefulRebootRequired = false; - Server.ResetRebootState(); + controller.ResetRebootState(); var eventTask = HandleEventImpl(EventType.WorldReboot, Enumerable.Empty(), false, cancellationToken); try @@ -170,7 +171,7 @@ namespace Tgstation.Server.Host.Components.Watchdog } case MonitorActivationReason.ActiveLaunchParametersUpdated: - await Server.SetRebootState(Session.RebootState.Restart, cancellationToken); + await controller.SetRebootState(Session.RebootState.Restart, cancellationToken); gracefulRebootRequired = true; break; case MonitorActivationReason.NewDmbAvailable: @@ -202,12 +203,12 @@ namespace Tgstation.Server.Host.Components.Watchdog } /// - protected sealed override ISessionController GetActiveController() => Server; + protected sealed override ISessionController? GetActiveController() => Server; /// protected override async ValueTask InitController( ValueTask eventTask, - ReattachInformation reattachInfo, + ReattachInformation? reattachInfo, CancellationToken cancellationToken) { // don't need a new dmb if reattaching @@ -220,15 +221,14 @@ namespace Tgstation.Server.Host.Components.Watchdog // start the alpha server task, either by launch a new process or attaching to an existing one // The tasks returned are mainly for writing interop files to the directories among other things and should generally never fail // The tasks pertaining to server startup times are in the ISessionControllers - ValueTask serverLaunchTask; if (!reattachInProgress) { - Logger.LogTrace("Initializing controller with CompileJob {compileJobId}...", dmbToUse.CompileJob.Id); + Logger.LogTrace("Initializing controller with CompileJob {compileJobId}...", dmbToUse!.CompileJob.Id); await BeforeApplyDmb(dmbToUse.CompileJob, cancellationToken); dmbToUse = await PrepServerForLaunch(dmbToUse, cancellationToken); await eventTask; - serverLaunchTask = SessionControllerFactory.LaunchNew( + Server = await SessionControllerFactory.LaunchNew( dmbToUse, null, ActiveLaunchParameters, @@ -238,12 +238,9 @@ namespace Tgstation.Server.Host.Components.Watchdog else { await eventTask; - serverLaunchTask = SessionControllerFactory.Reattach(reattachInfo, cancellationToken); + Server = await SessionControllerFactory.Reattach(reattachInfo!, cancellationToken); } - // retrieve the session controller - Server = await serverLaunchTask; - // possiblity of null servers due to failed reattaches if (Server == null) { @@ -291,7 +288,7 @@ namespace Tgstation.Server.Host.Components.Watchdog /// The for the operation. /// A representing the running operation. protected virtual ValueTask SessionStartupPersist(CancellationToken cancellationToken) - => SessionPersistor.Save(Server.ReattachInformation, cancellationToken); + => SessionPersistor.Save(Server!.ReattachInformation, cancellationToken); /// /// Handler for when the is . @@ -309,7 +306,7 @@ namespace Tgstation.Server.Host.Components.Watchdog protected virtual async ValueTask HandleNewDmbAvailable(CancellationToken cancellationToken) { gracefulRebootRequired = true; - if (Server.CompileJob.DMApiVersion == null) + if (Server!.CompileJob.DMApiVersion == null) { Chat.QueueWatchdogMessage( "A new deployment has been made but cannot be applied automatically as the currently running server has no DMAPI. Please manually reboot the server to apply the update."); diff --git a/src/Tgstation.Server.Host/Components/Watchdog/IWatchdog.cs b/src/Tgstation.Server.Host/Components/Watchdog/IWatchdog.cs index 7600dc9a36..96c6c9ccf9 100644 --- a/src/Tgstation.Server.Host/Components/Watchdog/IWatchdog.cs +++ b/src/Tgstation.Server.Host/Components/Watchdog/IWatchdog.cs @@ -32,7 +32,7 @@ namespace Tgstation.Server.Host.Components.Watchdog /// /// Retrieves the currently running on the server. /// - Models.CompileJob ActiveCompileJob { get; } + Models.CompileJob? ActiveCompileJob { get; } /// /// The to be applied. @@ -43,7 +43,7 @@ namespace Tgstation.Server.Host.Components.Watchdog /// The the active server is using. /// /// This may not be the exact same as but still be associated with the same session. - DreamDaemonLaunchParameters LastLaunchParameters { get; } + DreamDaemonLaunchParameters? LastLaunchParameters { get; } /// /// The of the active server. diff --git a/src/Tgstation.Server.Host/Components/Watchdog/WatchdogBase.cs b/src/Tgstation.Server.Host/Components/Watchdog/WatchdogBase.cs index 4fb3e1677f..e922322995 100644 --- a/src/Tgstation.Server.Host/Components/Watchdog/WatchdogBase.cs +++ b/src/Tgstation.Server.Host/Components/Watchdog/WatchdogBase.cs @@ -56,10 +56,10 @@ namespace Tgstation.Server.Host.Components.Watchdog public DreamDaemonLaunchParameters ActiveLaunchParameters { get; protected set; } /// - public DreamDaemonLaunchParameters LastLaunchParameters { get; protected set; } + public DreamDaemonLaunchParameters? LastLaunchParameters { get; protected set; } /// - public Models.CompileJob ActiveCompileJob => GetActiveController()?.CompileJob; + public Models.CompileJob? ActiveCompileJob => GetActiveController()?.CompileJob; /// public abstract RebootState? RebootState { get; } @@ -152,12 +152,12 @@ namespace Tgstation.Server.Host.Components.Watchdog /// /// The for the monitor loop. /// - CancellationTokenSource monitorCts; + CancellationTokenSource? monitorCts; /// /// The running the monitor loop. /// - Task monitorTask; + Task? monitorTask; /// /// Backing field for . @@ -393,7 +393,7 @@ namespace Tgstation.Server.Host.Components.Watchdog job, async (core, databaseContextFactory, paramJob, progressFunction, ct) => { - if (core.Watchdog != this) + if (core?.Watchdog != this) throw new InvalidOperationException(Instance.DifferentCoreExceptionMessage); using (await SemaphoreSlimContext.Lock(synchronizationSemaphore, ct)) @@ -416,7 +416,7 @@ namespace Tgstation.Server.Host.Components.Watchdog } /// - public async ValueTask HandleRestart(Version updateVersion, bool handlerMayDelayShutdownWithExtremelyLongRunningTasks, CancellationToken cancellationToken) + public async ValueTask HandleRestart(Version? updateVersion, bool handlerMayDelayShutdownWithExtremelyLongRunningTasks, CancellationToken cancellationToken) { if (handlerMayDelayShutdownWithExtremelyLongRunningTasks) { @@ -425,7 +425,7 @@ namespace Tgstation.Server.Host.Components.Watchdog if (Status != WatchdogStatus.Offline) { Logger.LogDebug("Waiting for server to gracefully shut down."); - await monitorTask.WaitAsync(cancellationToken); + await monitorTask!.WaitAsync(cancellationToken); } else Logger.LogTrace("Graceful shutdown requested but server is already offline."); @@ -488,7 +488,7 @@ namespace Tgstation.Server.Host.Components.Watchdog } /// - async ValueTask IEventConsumer.HandleEvent(EventType eventType, IEnumerable parameters, bool deploymentPipeline, CancellationToken cancellationToken) + async ValueTask IEventConsumer.HandleEvent(EventType eventType, IEnumerable parameters, bool deploymentPipeline, CancellationToken cancellationToken) { ArgumentNullException.ThrowIfNull(parameters); @@ -514,7 +514,7 @@ namespace Tgstation.Server.Host.Components.Watchdog /// to use, if any. /// The for the operation. /// A representing the running operation. - protected abstract ValueTask InitController(ValueTask eventTask, ReattachInformation reattachInfo, CancellationToken cancellationToken); + protected abstract ValueTask InitController(ValueTask eventTask, ReattachInformation? reattachInfo, CancellationToken cancellationToken); /// /// Launches the watchdog. @@ -529,7 +529,7 @@ namespace Tgstation.Server.Host.Components.Watchdog bool startMonitor, bool announce, bool announceFailure, - ReattachInformation reattachInfo, + ReattachInformation? reattachInfo, CancellationToken cancellationToken) { Logger.LogTrace("Begin LaunchImplNoLock"); @@ -610,7 +610,7 @@ namespace Tgstation.Server.Host.Components.Watchdog if (monitorTask == null) return false; var wasRunning = !monitorTask.IsCompleted; - monitorCts.Cancel(); + monitorCts!.Cancel(); await monitorTask; Logger.LogTrace("Stopped Monitor"); monitorCts.Dispose(); @@ -638,7 +638,7 @@ namespace Tgstation.Server.Host.Components.Watchdog if (!launchResult.StartupTime.HasValue) throw new JobException( ErrorCode.WatchdogStartupTimeout, - new JobException($"{serverName} timed out on startup: {ActiveLaunchParameters.StartupTimeout.Value}s")); + new JobException($"{serverName} timed out on startup: {ActiveLaunchParameters.StartupTimeout!.Value}s")); } /// @@ -683,8 +683,8 @@ namespace Tgstation.Server.Host.Components.Watchdog /// /// Get the active . /// - /// The active . - protected abstract ISessionController GetActiveController(); + /// The active , if any. + protected abstract ISessionController? GetActiveController(); /// /// Handles the actions to take when the monitor has to "wake up". @@ -716,9 +716,9 @@ namespace Tgstation.Server.Host.Components.Watchdog var eventTask = eventConsumer.HandleEvent( EventType.DeploymentActivation, - new List + new List { - GameIOManager.ResolvePath(newCompileJob.DirectoryName.ToString()), + GameIOManager.ResolvePath(newCompileJob.DirectoryName!.Value.ToString()), }, false, cancellationToken); @@ -843,13 +843,14 @@ namespace Tgstation.Server.Host.Components.Watchdog try { MonitorAction nextAction = MonitorAction.Continue; - Task activeServerLifetime = null, + Task? activeServerLifetime = null, activeServerReboot = null, activeServerStartup = null, serverPrimed = null, activeLaunchParametersChanged = null, - newDmbAvailable = null; - ISessionController lastController = null; + newDmbAvailable = null, + healthCheck = null; + ISessionController? lastController = null; var ranInitialDmbCheck = false; for (ulong iteration = 1; nextAction != MonitorAction.Exit; ++iteration) using (LogContext.PushProperty(SerilogContextHelper.WatchdogMonitorIterationContextProperty, iteration)) @@ -865,7 +866,7 @@ namespace Tgstation.Server.Host.Components.Watchdog void UpdateMonitoredTasks() { var sameController = lastController == controller; - void TryUpdateTask(ref Task oldTask, Func newTaskFactory) + void TryUpdateTask(ref Task? oldTask, Func newTaskFactory) { if (sameController && oldTask?.IsCompleted == true) return; @@ -873,7 +874,7 @@ namespace Tgstation.Server.Host.Components.Watchdog oldTask = newTaskFactory(); } - controller.RebootGate = nextMonitorWakeupTcs.Task; + controller!.RebootGate = nextMonitorWakeupTcs.Task; TryUpdateTask(ref activeServerLifetime, () => controller.Lifetime); TryUpdateTask(ref activeServerReboot, () => controller.OnReboot); @@ -896,28 +897,36 @@ namespace Tgstation.Server.Host.Components.Watchdog }); } - UpdateMonitoredTasks(); + if (controller != null) + { + UpdateMonitoredTasks(); - var healthCheckSeconds = ActiveLaunchParameters.HealthCheckSeconds.Value; - var healthCheck = healthCheckSeconds == 0 - || !controller.DMApiAvailable - ? Extensions.TaskExtensions.InfiniteTask - : Task.Delay( - TimeSpan.FromSeconds(healthCheckSeconds), - cancellationToken); + var healthCheckSeconds = ActiveLaunchParameters.HealthCheckSeconds!.Value; + healthCheck = healthCheckSeconds == 0 + || !controller.DMApiAvailable + ? Extensions.TaskExtensions.InfiniteTask + : Task.Delay( + TimeSpan.FromSeconds(healthCheckSeconds), + cancellationToken); - // cancel waiting if requested - var toWaitOn = Task.WhenAny( - activeServerLifetime, - activeServerReboot, - activeServerStartup, - healthCheck, - newDmbAvailable, - activeLaunchParametersChanged, - serverPrimed); + // cancel waiting if requested + var toWaitOn = Task.WhenAny( + activeServerLifetime!, + activeServerReboot!, + activeServerStartup!, + healthCheck, + newDmbAvailable!, + activeLaunchParametersChanged!, + serverPrimed!); - // wait for something to happen - await toWaitOn.WaitAsync(cancellationToken); + // wait for something to happen + await toWaitOn.WaitAsync(cancellationToken); + } + else + { + Logger.LogError("Controller was null on monitor wakeup! Attempting restart..."); + nextAction = MonitorAction.Restart; // excuse me wtf? + } cancellationToken.ThrowIfCancellationRequested(); Logger.LogTrace("Monitor activated"); @@ -926,7 +935,7 @@ namespace Tgstation.Server.Host.Components.Watchdog using (await SemaphoreSlimContext.Lock(synchronizationSemaphore, cancellationToken)) { // Set this sooner so chat sends don't hold us up - if (activeServerLifetime.IsCompleted) + if (activeServerLifetime!.IsCompleted) Status = WatchdogStatus.Restoring; // multiple things may have happened, handle them one at a time @@ -934,7 +943,7 @@ namespace Tgstation.Server.Host.Components.Watchdog { MonitorActivationReason activationReason = default; // this will always be assigned before being used - bool CheckActivationReason(ref Task task, MonitorActivationReason testActivationReason) + bool CheckActivationReason(ref Task? task, MonitorActivationReason testActivationReason) { var taskCompleted = task?.IsCompleted == true; task = null; @@ -1025,7 +1034,10 @@ namespace Tgstation.Server.Host.Components.Watchdog { Logger.LogTrace("Detaching server..."); var controller = GetActiveController(); - await controller.Release(); + if (controller != null) + await controller.Release(); + else + Logger.LogError("Controller was null on monitor shutdown!"); } } @@ -1089,6 +1101,9 @@ namespace Tgstation.Server.Host.Components.Watchdog { Logger.LogTrace("Sending health check to active server..."); var activeServer = GetActiveController(); + if (activeServer == null) + return MonitorAction.Restart; // uhhhh??? + var response = await activeServer.SendCommand(new TopicParameters(), cancellationToken); var shouldShutdown = activeServer.RebootState == Session.RebootState.Shutdown; @@ -1128,7 +1143,7 @@ namespace Tgstation.Server.Host.Components.Watchdog actionTaken, StringComparison.Ordinal)); - if (ActiveLaunchParameters.DumpOnHealthCheckRestart.Value) + if (ActiveLaunchParameters.DumpOnHealthCheckRestart!.Value) { Logger.LogDebug("DumpOnHealthCheckRestart enabled."); try @@ -1164,13 +1179,30 @@ namespace Tgstation.Server.Host.Components.Watchdog /// Handle any in a given topic . /// /// The . - void HandleChatResponses(TopicResponse result) + void HandleChatResponses(TopicResponse? result) { if (result?.ChatResponses != null) - foreach (var response in result.ChatResponses) + { + var warnedMissingChannelIds = false; + foreach (var response in result.ChatResponses + .Where(response => + { + if (response.ChannelIds == null) + { + if (!warnedMissingChannelIds) + { + Logger.LogWarning("DMAPI response contains null channelIds!"); + warnedMissingChannelIds = true; + } + + return false; + } + + return true; + })) Chat.QueueMessage( response, - response.ChannelIds + response.ChannelIds! .Select(channelIdString => { if (UInt64.TryParse(channelIdString, out var channelId)) @@ -1181,7 +1213,8 @@ namespace Tgstation.Server.Host.Components.Watchdog return null; }) .Where(nullableChannelId => nullableChannelId.HasValue) - .Select(nullableChannelId => nullableChannelId.Value)); + .Select(nullableChannelId => nullableChannelId!.Value)); + } } /// diff --git a/src/Tgstation.Server.Host/Components/Watchdog/WindowsWatchdog.cs b/src/Tgstation.Server.Host/Components/Watchdog/WindowsWatchdog.cs index cd8f0b4525..e75ba74511 100644 --- a/src/Tgstation.Server.Host/Components/Watchdog/WindowsWatchdog.cs +++ b/src/Tgstation.Server.Host/Components/Watchdog/WindowsWatchdog.cs @@ -81,9 +81,9 @@ namespace Tgstation.Server.Host.Components.Watchdog /// protected override async ValueTask ApplyInitialDmb(CancellationToken cancellationToken) { - if (Server.EngineVersion.Engine.Value != EngineType.Byond) + if (Server!.EngineVersion.Engine != EngineType.Byond) { - Logger.LogTrace("Not setting InitialDmb for engine type {engineType}", Server.EngineVersion.Engine.Value); + Logger.LogTrace("Not setting InitialDmb for engine type {engineType}", Server.EngineVersion.Engine); return; } diff --git a/src/Tgstation.Server.Host/Configuration/ControlPanelConfiguration.cs b/src/Tgstation.Server.Host/Configuration/ControlPanelConfiguration.cs index 19dfa9cace..12da65e312 100644 --- a/src/Tgstation.Server.Host/Configuration/ControlPanelConfiguration.cs +++ b/src/Tgstation.Server.Host/Configuration/ControlPanelConfiguration.cs @@ -36,16 +36,16 @@ namespace Tgstation.Server.Host.Configuration /// /// The channel to retrieve the webpanel from. "local" uses the bundled version. /// - public string Channel { get; set; } + public string? Channel { get; set; } /// /// The public path to the TGS control panel from a wider network. /// - public string PublicPath { get; set; } + public string? PublicPath { get; set; } /// /// Origins allowed for CORS requests. /// - public ICollection AllowedOrigins { get; set; } + public ICollection? AllowedOrigins { get; set; } } } diff --git a/src/Tgstation.Server.Host/Configuration/DatabaseConfiguration.cs b/src/Tgstation.Server.Host/Configuration/DatabaseConfiguration.cs index d5adce7577..ea699cd4ad 100644 --- a/src/Tgstation.Server.Host/Configuration/DatabaseConfiguration.cs +++ b/src/Tgstation.Server.Host/Configuration/DatabaseConfiguration.cs @@ -27,7 +27,7 @@ namespace Tgstation.Server.Host.Configuration /// /// The connection string for the database. /// - public string ConnectionString { get; set; } + public string? ConnectionString { get; set; } /// /// If the database should be deleted on application startup. Should not be used in production!. @@ -37,6 +37,6 @@ namespace Tgstation.Server.Host.Configuration /// /// The form of the of the target server. /// - public string ServerVersion { get; set; } + public string? ServerVersion { get; set; } } } diff --git a/src/Tgstation.Server.Host/Configuration/ElasticsearchConfiguration.cs b/src/Tgstation.Server.Host/Configuration/ElasticsearchConfiguration.cs index 8c45f2e9f5..a99f3b03e8 100644 --- a/src/Tgstation.Server.Host/Configuration/ElasticsearchConfiguration.cs +++ b/src/Tgstation.Server.Host/Configuration/ElasticsearchConfiguration.cs @@ -1,7 +1,7 @@ using System; namespace Tgstation.Server.Host.Configuration - { +{ /// /// Configuration options pertaining to elasticsearch log storage. /// @@ -20,16 +20,16 @@ namespace Tgstation.Server.Host.Configuration /// /// The host of the elasticsearch endpoint. /// - public Uri Host { get; set; } + public Uri? Host { get; set; } /// /// Username for elasticsearch. /// - public string Username { get; set; } + public string? Username { get; set; } /// /// Password for elasticsearch. /// - public string Password { get; set; } + public string? Password { get; set; } } } diff --git a/src/Tgstation.Server.Host/Configuration/FileLoggingConfiguration.cs b/src/Tgstation.Server.Host/Configuration/FileLoggingConfiguration.cs index c317fc1b97..534daf8ebb 100644 --- a/src/Tgstation.Server.Host/Configuration/FileLoggingConfiguration.cs +++ b/src/Tgstation.Server.Host/Configuration/FileLoggingConfiguration.cs @@ -22,7 +22,7 @@ namespace Tgstation.Server.Host.Configuration /// /// Where log files are stored. /// - public string Directory { get; set; } + public string? Directory { get; set; } /// /// If file logging is disabled. diff --git a/src/Tgstation.Server.Host/Configuration/GeneralConfiguration.cs b/src/Tgstation.Server.Host/Configuration/GeneralConfiguration.cs index 8e2fe59579..ae084860df 100644 --- a/src/Tgstation.Server.Host/Configuration/GeneralConfiguration.cs +++ b/src/Tgstation.Server.Host/Configuration/GeneralConfiguration.cs @@ -81,7 +81,7 @@ namespace Tgstation.Server.Host.Configuration /// /// The the file says it is. /// - public Version ConfigVersion { get; set; } + public Version? ConfigVersion { get; set; } /// /// The port the TGS API listens on. @@ -91,7 +91,7 @@ namespace Tgstation.Server.Host.Configuration /// /// A GitHub personal access token to use for bypassing rate limits on requests. Requires no scopes. /// - public string GitHubAccessToken { get; set; } + public string? GitHubAccessToken { get; set; } /// /// The . diff --git a/src/Tgstation.Server.Host/Configuration/InternalConfiguration.cs b/src/Tgstation.Server.Host/Configuration/InternalConfiguration.cs index 6bf30bafdc..58f2ad4720 100644 --- a/src/Tgstation.Server.Host/Configuration/InternalConfiguration.cs +++ b/src/Tgstation.Server.Host/Configuration/InternalConfiguration.cs @@ -13,12 +13,12 @@ /// /// The name of the pipe opened by the host watchdog for sending commands, if any. /// - public string CommandPipe { get; set; } + public string? CommandPipe { get; set; } /// /// The name of the pipe opened by the host watchdog for receiving commands, if any. /// - public string ReadyPipe { get; set; } + public string? ReadyPipe { get; set; } /// /// If the server is running under SystemD. @@ -28,7 +28,7 @@ /// /// The base path for the app settings configuration files. /// - public string AppSettingsBasePath { get; set; } + public string AppSettingsBasePath { get; set; } = "UNINITIALIZED"; // this is set in a hacky way in ServerFactory /// /// Coerce the to select . @@ -38,6 +38,6 @@ /// /// Generate default configuration using the given default password. /// - public string MariaDBDefaultRootPassword { get; set; } + public string? MariaDBDefaultRootPassword { get; set; } } } diff --git a/src/Tgstation.Server.Host/Configuration/OAuthConfiguration.cs b/src/Tgstation.Server.Host/Configuration/OAuthConfiguration.cs index 1818a15d8b..2d4c781102 100644 --- a/src/Tgstation.Server.Host/Configuration/OAuthConfiguration.cs +++ b/src/Tgstation.Server.Host/Configuration/OAuthConfiguration.cs @@ -10,16 +10,16 @@ namespace Tgstation.Server.Host.Configuration /// /// The client redirect URL. Not used by all providers. /// - public Uri ServerUrl { get; set; } + public Uri? ServerUrl { get; set; } /// /// The authentication server URL. Not used by all providers. /// - public Uri RedirectUrl { get; set; } + public Uri? RedirectUrl { get; set; } /// /// User information URL override. Not supported by the provider. /// - public Uri UserInformationUrlOverride { get; set; } + public Uri? UserInformationUrlOverride { get; set; } } } diff --git a/src/Tgstation.Server.Host/Configuration/OAuthConfigurationBase.cs b/src/Tgstation.Server.Host/Configuration/OAuthConfigurationBase.cs index d2d7668072..a170cd235f 100644 --- a/src/Tgstation.Server.Host/Configuration/OAuthConfigurationBase.cs +++ b/src/Tgstation.Server.Host/Configuration/OAuthConfigurationBase.cs @@ -10,12 +10,12 @@ namespace Tgstation.Server.Host.Configuration /// /// The client ID. /// - public string ClientId { get; set; } + public string? ClientId { get; set; } /// /// The client secret. /// - public string ClientSecret { get; set; } + public string? ClientSecret { get; set; } /// /// Initializes a new instance of the class. diff --git a/src/Tgstation.Server.Host/Configuration/SecurityConfiguration.cs b/src/Tgstation.Server.Host/Configuration/SecurityConfiguration.cs index a6073538cd..35585612c0 100644 --- a/src/Tgstation.Server.Host/Configuration/SecurityConfiguration.cs +++ b/src/Tgstation.Server.Host/Configuration/SecurityConfiguration.cs @@ -60,12 +60,12 @@ namespace Tgstation.Server.Host.Configuration /// /// A custom token signing key. Overrides . /// - public string CustomTokenSigningKeyBase64 { get; set; } + public string? CustomTokenSigningKeyBase64 { get; set; } /// /// OAuth provider settings. /// - public IDictionary OAuth + public IDictionary? OAuth { get => oAuth; set @@ -73,10 +73,10 @@ namespace Tgstation.Server.Host.Configuration // Workaround for https://github.com/dotnet/runtime/issues/89547 var publicProperties = typeof(OAuthConfiguration) .GetProperties() - .Where(property => property.CanWrite && property.SetMethod.IsPublic) + .Where(property => property.CanWrite && property.SetMethod!.IsPublic) .ToList(); oAuth = value - .Where( + ?.Where( kvp => !publicProperties.All( prop => prop.GetValue(kvp.Value) == prop.PropertyType.GetDefaultValue())) .ToDictionary(kvp => kvp.Key, kvp => kvp.Value); @@ -86,6 +86,6 @@ namespace Tgstation.Server.Host.Configuration /// /// Backing field for . /// - IDictionary oAuth; + IDictionary? oAuth; } } diff --git a/src/Tgstation.Server.Host/Configuration/SwarmConfiguration.cs b/src/Tgstation.Server.Host/Configuration/SwarmConfiguration.cs index 38c709ec39..12c9cf25a0 100644 --- a/src/Tgstation.Server.Host/Configuration/SwarmConfiguration.cs +++ b/src/Tgstation.Server.Host/Configuration/SwarmConfiguration.cs @@ -1,6 +1,7 @@ using System; using Tgstation.Server.Api.Models.Internal; + using YamlDotNet.Serialization; namespace Tgstation.Server.Host.Configuration @@ -17,7 +18,7 @@ namespace Tgstation.Server.Host.Configuration /// [YamlMember(SerializeAs = typeof(string))] - public override Uri Address + public override Uri? Address { get => base.Address; set => base.Address = value; @@ -25,7 +26,7 @@ namespace Tgstation.Server.Host.Configuration /// [YamlMember(SerializeAs = typeof(string))] - public override Uri PublicAddress + public override Uri? PublicAddress { get => base.PublicAddress; set => base.PublicAddress = value; @@ -35,12 +36,12 @@ namespace Tgstation.Server.Host.Configuration /// The of the swarm controller. If , the current server is considered the controller. /// [YamlMember(SerializeAs = typeof(string))] - public Uri ControllerAddress { get; set; } + public Uri? ControllerAddress { get; set; } /// /// The private key used for swarm communication. /// - public string PrivateKey { get; set; } + public string? PrivateKey { get; set; } /// /// The number of nodes in addition to the controller required to be connected a server swarm before performing an update. diff --git a/src/Tgstation.Server.Host/Configuration/UpdatesConfiguration.cs b/src/Tgstation.Server.Host/Configuration/UpdatesConfiguration.cs index 1e340c84ca..af475412ee 100644 --- a/src/Tgstation.Server.Host/Configuration/UpdatesConfiguration.cs +++ b/src/Tgstation.Server.Host/Configuration/UpdatesConfiguration.cs @@ -35,11 +35,11 @@ namespace Tgstation.Server.Host.Configuration /// /// Prefix before the of TGS published in git tags. /// - public string GitTagPrefix { get; set; } = DefaultGitTagPrefix; + public string? GitTagPrefix { get; set; } = DefaultGitTagPrefix; /// /// Asset package containing the new assembly in zip form. /// - public string UpdatePackageAssetName { get; set; } = DefaultUpdatePackageAssetName; + public string? UpdatePackageAssetName { get; set; } = DefaultUpdatePackageAssetName; } } diff --git a/src/Tgstation.Server.Host/Controllers/AdministrationController.cs b/src/Tgstation.Server.Host/Controllers/AdministrationController.cs index c1da69a69d..2ccc4f4c8b 100644 --- a/src/Tgstation.Server.Host/Controllers/AdministrationController.cs +++ b/src/Tgstation.Server.Host/Controllers/AdministrationController.cs @@ -144,8 +144,8 @@ namespace Tgstation.Server.Host.Controllers { try { - Version greatestVersion = null; - Uri repoUrl = null; + Version? greatestVersion = null; + Uri? repoUrl = null; try { var gitHubService = gitHubServiceFactory.CreateService(); @@ -213,10 +213,10 @@ namespace Tgstation.Server.Host.Controllers var attemptingUpload = model.UploadZip == true; if (attemptingUpload) { - if (!AuthenticationContext.PermissionSet.AdministrationRights.Value.HasFlag(AdministrationRights.UploadVersion)) + if (!AuthenticationContext.PermissionSet.AdministrationRights!.Value.HasFlag(AdministrationRights.UploadVersion)) return Forbid(); } - else if (!AuthenticationContext.PermissionSet.AdministrationRights.Value.HasFlag(AdministrationRights.ChangeVersion)) + else if (!AuthenticationContext.PermissionSet.AdministrationRights!.Value.HasFlag(AdministrationRights.ChangeVersion)) return Forbid(); if (model.NewVersion == null) @@ -377,7 +377,7 @@ namespace Tgstation.Server.Host.Controllers /// A resulting in the of the request. async ValueTask AttemptInitiateUpdate(Version newVersion, bool attemptingUpload, CancellationToken cancellationToken) { - IFileUploadTicket uploadTicket = attemptingUpload + IFileUploadTicket? uploadTicket = attemptingUpload ? fileTransferService.CreateUpload(FileUploadStreamKind.None) : null; @@ -391,7 +391,7 @@ namespace Tgstation.Server.Host.Controllers catch { if (attemptingUpload) - await uploadTicket.DisposeAsync(); + await uploadTicket!.DisposeAsync(); throw; } diff --git a/src/Tgstation.Server.Host/Controllers/ApiController.cs b/src/Tgstation.Server.Host/Controllers/ApiController.cs index ffa4e84960..bad2e6c10f 100644 --- a/src/Tgstation.Server.Host/Controllers/ApiController.cs +++ b/src/Tgstation.Server.Host/Controllers/ApiController.cs @@ -48,7 +48,7 @@ namespace Tgstation.Server.Host.Controllers /// /// The for the operation. /// - protected ApiHeaders ApiHeaders => ApiHeadersProvider.ApiHeaders; + protected ApiHeaders? ApiHeaders => ApiHeadersProvider.ApiHeaders; /// /// The containing value of . @@ -73,7 +73,7 @@ namespace Tgstation.Server.Host.Controllers /// /// The for the operation. /// - protected Models.Instance Instance { get; } + protected Models.Instance? Instance { get; } /// /// If are required. @@ -100,13 +100,13 @@ namespace Tgstation.Server.Host.Controllers ApiHeadersProvider = apiHeadersProvider ?? throw new ArgumentNullException(nameof(apiHeadersProvider)); Logger = logger ?? throw new ArgumentNullException(nameof(logger)); - Instance = AuthenticationContext?.InstancePermissionSet?.Instance; + Instance = AuthenticationContext.InstancePermissionSet?.Instance; this.requireHeaders = requireHeaders; } /// #pragma warning disable CA1506 // TODO: Decomplexify - protected override async ValueTask HookExecuteAction(Func executeAction, CancellationToken cancellationToken) + protected override async ValueTask HookExecuteAction(Func executeAction, CancellationToken cancellationToken) { ArgumentNullException.ThrowIfNull(executeAction); @@ -114,7 +114,7 @@ namespace Tgstation.Server.Host.Controllers if (ApiHeaders == null) { if (requireHeaders) - return HeadersIssue(ApiHeadersProvider.HeadersException); + return HeadersIssue(ApiHeadersProvider.HeadersException!); } var errorCase = await ValidateRequest(cancellationToken); @@ -124,7 +124,7 @@ namespace Tgstation.Server.Host.Controllers if (ModelState?.IsValid == false) { var errorMessages = ModelState - .SelectMany(x => x.Value.Errors) + .SelectMany(x => x.Value!.Errors) .Select(x => x.ErrorMessage) // We use RequiredAttributes purely for preventing properties from becoming nullable in the databases @@ -238,8 +238,8 @@ namespace Tgstation.Server.Host.Controllers /// /// The for the operation. /// A resulting in an appropriate on validation failure, otherwise. - protected virtual ValueTask ValidateRequest(CancellationToken cancellationToken) - => ValueTask.FromResult(null); + protected virtual ValueTask ValidateRequest(CancellationToken cancellationToken) + => ValueTask.FromResult(null); /// /// Response for missing/Invalid headers. @@ -267,14 +267,14 @@ namespace Tgstation.Server.Host.Controllers /// /// The of model being generated and returned. /// A resulting in a resulting in the generated . - /// A to transform the s after being queried. + /// Optional to transform the s after being queried. /// The requested page from the query. /// The requested page size from the query. /// The for the operation. /// A resulting in the for the operation. protected ValueTask Paginated( Func>> queryGenerator, - Func resultTransformer, + Func? resultTransformer, int? pageQuery, int? pageSizeQuery, CancellationToken cancellationToken) => PaginatedImpl( @@ -297,7 +297,7 @@ namespace Tgstation.Server.Host.Controllers /// A resulting in the for the operation. protected ValueTask Paginated( Func>> queryGenerator, - Func resultTransformer, + Func? resultTransformer, int? pageQuery, int? pageSizeQuery, CancellationToken cancellationToken) @@ -322,7 +322,7 @@ namespace Tgstation.Server.Host.Controllers /// A resulting in the for the operation. async ValueTask PaginatedImpl( Func>> queryGenerator, - Func resultTransformer, + Func? resultTransformer, int? pageQuery, int? pageSizeQuery, CancellationToken cancellationToken) @@ -342,7 +342,7 @@ namespace Tgstation.Server.Host.Controllers var page = pageQuery ?? 1; var paginationResult = await queryGenerator(); - if (paginationResult.EarlyOut != null) + if (!paginationResult.Valid) return paginationResult.EarlyOut; var queriedResults = paginationResult diff --git a/src/Tgstation.Server.Host/Controllers/ApiControllerBase.cs b/src/Tgstation.Server.Host/Controllers/ApiControllerBase.cs index 9a8b119808..748dd81096 100644 --- a/src/Tgstation.Server.Host/Controllers/ApiControllerBase.cs +++ b/src/Tgstation.Server.Host/Controllers/ApiControllerBase.cs @@ -37,7 +37,7 @@ namespace Tgstation.Server.Host.Controllers /// A that should be invoked and its response awaited to continue normal execution of the request. Should NOT be called if this method returns a non- value. /// The for the operation. /// A resulting in an that, if not , is executed. - protected virtual async ValueTask HookExecuteAction(Func executeAction, CancellationToken cancellationToken) + protected virtual async ValueTask HookExecuteAction(Func executeAction, CancellationToken cancellationToken) { ArgumentNullException.ThrowIfNull(executeAction); diff --git a/src/Tgstation.Server.Host/Controllers/ApiRootController.cs b/src/Tgstation.Server.Host/Controllers/ApiRootController.cs index fbfc1d4465..fa6b3e0c4b 100644 --- a/src/Tgstation.Server.Host/Controllers/ApiRootController.cs +++ b/src/Tgstation.Server.Host/Controllers/ApiRootController.cs @@ -85,11 +85,6 @@ namespace Tgstation.Server.Host.Controllers /// readonly GeneralConfiguration generalConfiguration; - /// - /// The for the . - /// - readonly ControlPanelConfiguration controlPanelConfiguration; - /// /// Initializes a new instance of the class. /// @@ -105,7 +100,6 @@ namespace Tgstation.Server.Host.Controllers /// The value of . /// The value of . /// The containing the value of . - /// The containing the value of . /// The for the . /// The for the . public ApiRootController( @@ -121,7 +115,6 @@ namespace Tgstation.Server.Host.Controllers ISwarmService swarmService, IServerControl serverControl, IOptions generalConfigurationOptions, - IOptions controlPanelConfigurationOptions, ILogger logger, IApiHeadersProvider apiHeadersProvider) : base( @@ -141,7 +134,6 @@ namespace Tgstation.Server.Host.Controllers this.swarmService = swarmService ?? throw new ArgumentNullException(nameof(swarmService)); this.serverControl = serverControl ?? throw new ArgumentNullException(nameof(serverControl)); generalConfiguration = generalConfigurationOptions?.Value ?? throw new ArgumentNullException(nameof(generalConfigurationOptions)); - controlPanelConfiguration = controlPanelConfigurationOptions?.Value ?? throw new ArgumentNullException(nameof(controlPanelConfigurationOptions)); } /// @@ -170,7 +162,7 @@ namespace Tgstation.Server.Host.Controllers return HeadersIssue(ex); } - failIfUnauthed = Request.Headers.Authorization.Any(); + failIfUnauthed = Request.Headers.Authorization.Count > 0; } else failIfUnauthed = ApiHeaders.Token != null; @@ -213,7 +205,7 @@ namespace Tgstation.Server.Host.Controllers if (ApiHeaders == null) { Response.Headers.Add(HeaderNames.WWWAuthenticate, new StringValues($"basic realm=\"Create TGS {ApiHeaders.BearerAuthenticationScheme} token\"")); - return HeadersIssue(ApiHeadersProvider.HeadersException); + return HeadersIssue(ApiHeadersProvider.HeadersException!); } if (ApiHeaders.IsTokenAuthentication) @@ -221,12 +213,12 @@ namespace Tgstation.Server.Host.Controllers var oAuthLogin = ApiHeaders.OAuthProvider.HasValue; - ISystemIdentity systemIdentity = null; + ISystemIdentity? systemIdentity = null; if (!oAuthLogin) try { // trust the system over the database because a user's name can change while still having the same SID - systemIdentity = await systemIdentityFactory.CreateSystemIdentity(ApiHeaders.Username, ApiHeaders.Password, cancellationToken); + systemIdentity = await systemIdentityFactory.CreateSystemIdentity(ApiHeaders.Username!, ApiHeaders.Password!, cancellationToken); } catch (NotImplementedException) { @@ -239,8 +231,8 @@ namespace Tgstation.Server.Host.Controllers IQueryable query = DatabaseContext.Users.AsQueryable(); if (oAuthLogin) { - var oAuthProvider = ApiHeaders.OAuthProvider.Value; - string externalUserId; + var oAuthProvider = ApiHeaders.OAuthProvider!.Value; + string? externalUserId; try { var validator = oAuthProviders @@ -263,13 +255,13 @@ namespace Tgstation.Server.Host.Controllers return Unauthorized(); query = query.Where( - x => x.OAuthConnections.Any( + x => x.OAuthConnections!.Any( y => y.Provider == oAuthProvider && y.ExternalUserId == externalUserId)); } else { - var canonicalUserName = Models.User.CanonicalizeName(ApiHeaders.Username); + var canonicalUserName = Models.User.CanonicalizeName(ApiHeaders.Username!); if (canonicalUserName == Models.User.CanonicalizeName(Models.User.TgsSystemUserName)) return Unauthorized(); @@ -311,7 +303,7 @@ namespace Tgstation.Server.Host.Controllers if (!usingSystemIdentity) { // DB User password check and update - if (!isLikelyDbUser || !cryptographySuite.CheckUserPassword(user, ApiHeaders.Password)) + if (!isLikelyDbUser || !cryptographySuite.CheckUserPassword(user, ApiHeaders.Password!)) return Unauthorized(); if (user.PasswordHash != originalHash) { @@ -327,7 +319,7 @@ namespace Tgstation.Server.Host.Controllers } else { - var usernameMismatch = systemIdentity.Username != user.Name; + var usernameMismatch = systemIdentity!.Username != user.Name; if (isLikelyDbUser || usernameMismatch) { DatabaseContext.Users.Attach(user); @@ -352,7 +344,7 @@ namespace Tgstation.Server.Host.Controllers } // Now that the bookeeping is done, tell them to fuck off if necessary - if (!user.Enabled.Value) + if (!user.Enabled!.Value) { Logger.LogTrace("Not logging in disabled user {userId}.", user.Id); return Forbid(); @@ -365,7 +357,7 @@ namespace Tgstation.Server.Host.Controllers var identExpiry = token.ParseJwt().ValidTo; identExpiry += tokenFactory.ValidationParameters.ClockSkew; identExpiry += TimeSpan.FromSeconds(15); - identityCache.CacheSystemIdentity(user, systemIdentity, identExpiry); + identityCache.CacheSystemIdentity(user, systemIdentity!, identExpiry); } Logger.LogDebug("Successfully logged in user {userId}!", user.Id); diff --git a/src/Tgstation.Server.Host/Controllers/BridgeController.cs b/src/Tgstation.Server.Host/Controllers/BridgeController.cs index f23d91aab9..4b1a1fac3e 100644 --- a/src/Tgstation.Server.Host/Controllers/BridgeController.cs +++ b/src/Tgstation.Server.Host/Controllers/BridgeController.cs @@ -89,7 +89,7 @@ namespace Tgstation.Server.Host.Controllers { // Nothing to see here var remoteIP = Request.HttpContext.Connection.RemoteIpAddress; - if (!IPAddress.IsLoopback(remoteIP)) + if (remoteIP == null || !IPAddress.IsLoopback(remoteIP)) { logger.LogTrace("Rejecting remote bridge request from {remoteIP}", remoteIP); return Forbid(); @@ -97,10 +97,10 @@ namespace Tgstation.Server.Host.Controllers using (LogContext.PushProperty(SerilogContextHelper.BridgeRequestIterationContextProperty, Interlocked.Increment(ref requestsProcessed))) { - var request = new BridgeParameters(); + BridgeParameters? request; try { - JsonConvert.PopulateObject(data, request, DMApiConstants.SerializerSettings); + request = JsonConvert.DeserializeObject(data, DMApiConstants.SerializerSettings); } catch (Exception ex) { @@ -109,6 +109,13 @@ namespace Tgstation.Server.Host.Controllers return BadRequest(); } + if (request == null) + { + if (LogContent) + logger.LogWarning("Error deserializing bridge request: {badJson}", data); + return BadRequest(); + } + if (LogContent) logger.LogTrace("Bridge Request: {json}", data); diff --git a/src/Tgstation.Server.Host/Controllers/ChatController.cs b/src/Tgstation.Server.Host/Controllers/ChatController.cs index 967971eb24..9ea7f4f5a7 100644 --- a/src/Tgstation.Server.Host/Controllers/ChatController.cs +++ b/src/Tgstation.Server.Host/Controllers/ChatController.cs @@ -25,6 +25,7 @@ using Tgstation.Server.Host.Extensions; using Tgstation.Server.Host.Models; using Tgstation.Server.Host.Security; using Tgstation.Server.Host.Utils; + using Z.EntityFramework.Plus; namespace Tgstation.Server.Host.Controllers @@ -118,20 +119,20 @@ namespace Tgstation.Server.Host.Controllers .Where(x => x.InstanceId == Instance.Id) .CountAsync(cancellationToken); - if (countOfExistingBotsInInstance >= Instance.ChatBotLimit.Value) + if (countOfExistingBotsInInstance >= Instance.ChatBotLimit!.Value) return Conflict(new ErrorMessageResponse(ErrorCode.ChatBotMax)); model.Enabled ??= false; model.ReconnectionInterval ??= 1; // try to update das db first - var dbModel = new ChatBot + var newChannels = model.Channels?.Select(x => ConvertApiChatChannel(x, model.Provider!.Value)).ToList() ?? new List(); // important that this isn't null + var dbModel = new ChatBot(newChannels) { Name = model.Name, ConnectionString = model.ConnectionString, Enabled = model.Enabled, - Channels = model.Channels?.Select(x => ConvertApiChatChannel(x, model.Provider.Value)).ToList() ?? new List(), // important that this isn't null - InstanceId = Instance.Id.Value, + InstanceId = Instance.Id!.Value, Provider = model.Provider, ReconnectionInterval = model.ReconnectionInterval, ChannelLimit = model.ChannelLimit, @@ -140,7 +141,7 @@ namespace Tgstation.Server.Host.Controllers DatabaseContext.ChatBots.Add(dbModel); await DatabaseContext.Save(cancellationToken); - return await WithComponentInstance( + return await WithComponentInstanceNullable( async instance => { try @@ -149,7 +150,7 @@ namespace Tgstation.Server.Host.Controllers await instance.Chat.ChangeSettings(dbModel, cancellationToken); if (dbModel.Channels.Count > 0) - await instance.Chat.ChangeChannels(dbModel.Id.Value, dbModel.Channels, cancellationToken); + await instance.Chat.ChangeChannels(dbModel.Id!.Value, dbModel.Channels, cancellationToken); } catch { @@ -158,7 +159,7 @@ namespace Tgstation.Server.Host.Controllers // DCTx2: Operations must always run await DatabaseContext.Save(default); - await instance.Chat.DeleteConnection(dbModel.Id.Value, default); + await instance.Chat.DeleteConnection(dbModel.Id!.Value, default); throw; } @@ -179,7 +180,7 @@ namespace Tgstation.Server.Host.Controllers [TgsAuthorize(ChatBotRights.Delete)] [ProducesResponseType(204)] public async ValueTask Delete(long id, CancellationToken cancellationToken) - => await WithComponentInstance( + => await WithComponentInstanceNullable( async instance => { await Task.WhenAll( @@ -280,7 +281,7 @@ namespace Tgstation.Server.Host.Controllers { ArgumentNullException.ThrowIfNull(model); - var earlyOut = StandardModelChecks(model, false); + IActionResult? earlyOut = StandardModelChecks(model, false); if (earlyOut != null) return earlyOut; @@ -295,7 +296,7 @@ namespace Tgstation.Server.Host.Controllers if (current == default) return this.Gone(); - if ((model.Channels?.Count ?? current.Channels.Count) > (model.ChannelLimit ?? current.ChannelLimit.Value)) + if ((model.Channels?.Count ?? current.Channels!.Count) > (model.ChannelLimit ?? current.ChannelLimit!.Value)) { // 400 or 409 depends on if the client sent both var errorMessage = new ErrorMessageResponse(ErrorCode.ChatBotMaxChannels); @@ -338,28 +339,28 @@ namespace Tgstation.Server.Host.Controllers var hasChannels = model.Channels != null; if (hasChannels || (model.Provider.HasValue && model.Provider != oldProvider)) { - DatabaseContext.ChatChannels.RemoveRange(current.Channels); + DatabaseContext.ChatChannels.RemoveRange(current.Channels!); if (hasChannels) { - var dbChannels = model.Channels.Select(x => ConvertApiChatChannel(x, model.Provider ?? current.Provider.Value)).ToList(); + var dbChannels = model.Channels!.Select(x => ConvertApiChatChannel(x, model.Provider ?? current.Provider!.Value)).ToList(); DatabaseContext.ChatChannels.AddRange(dbChannels); current.Channels = dbChannels; } else - current.Channels.Clear(); + current.Channels!.Clear(); } await DatabaseContext.Save(cancellationToken); - earlyOut = await WithComponentInstance( + earlyOut = await WithComponentInstanceNullable( async instance => { var chat = instance.Chat; if (anySettingsModified) await chat.ChangeSettings(current, cancellationToken); // have to rebuild the thing first - if ((model.Channels != null || anySettingsModified) && current.Enabled.Value) - await chat.ChangeChannels(current.Id.Value, current.Channels, cancellationToken); + if ((model.Channels != null || anySettingsModified) && current.Enabled!.Value) + await chat.ChangeChannels(current.Id!.Value, current.Channels, cancellationToken); return null; }); @@ -381,8 +382,8 @@ namespace Tgstation.Server.Host.Controllers /// /// The to validate. /// If the is being created. - /// An to respond with or . - IActionResult StandardModelChecks(ChatBotApiBase model, bool forCreation) + /// An to respond with or . + BadRequestObjectResult? StandardModelChecks(ChatBotApiBase model, bool forCreation) { if (model.ReconnectionInterval == 0) throw new InvalidOperationException("RecconnectionInterval cannot be zero!"); diff --git a/src/Tgstation.Server.Host/Controllers/ComponentInterfacingController.cs b/src/Tgstation.Server.Host/Controllers/ComponentInterfacingController.cs index eef1af8b18..f04e620d06 100644 --- a/src/Tgstation.Server.Host/Controllers/ComponentInterfacingController.cs +++ b/src/Tgstation.Server.Host/Controllers/ComponentInterfacingController.cs @@ -10,6 +10,7 @@ using Tgstation.Server.Api.Models; using Tgstation.Server.Api.Models.Response; using Tgstation.Server.Host.Components; using Tgstation.Server.Host.Database; +using Tgstation.Server.Host.Models; using Tgstation.Server.Host.Security; using Tgstation.Server.Host.Utils; @@ -63,21 +64,22 @@ namespace Tgstation.Server.Host.Controllers } /// - protected override async ValueTask ValidateRequest(CancellationToken cancellationToken) + protected override async ValueTask ValidateRequest(CancellationToken cancellationToken) { if (!useInstanceRequestHeader) return null; - if (!ApiHeaders.InstanceId.HasValue) + if (!ApiHeaders!.InstanceId.HasValue) return BadRequest(new ErrorMessageResponse(ErrorCode.InstanceHeaderRequired)); if (AuthenticationContext.InstancePermissionSet == null) return Forbid(); - if (ValidateInstanceOnlineStatus(Instance)) + var instance = Instance!; + if (ValidateInstanceOnlineStatus(instance)) await DatabaseContext.Save(cancellationToken); - using var instanceReferenceCheck = instanceManager.GetInstanceReference(Instance); + using var instanceReferenceCheck = instanceManager.GetInstanceReference(instance); if (instanceReferenceCheck == null) return Conflict(new ErrorMessageResponse(ErrorCode.InstanceOffline)); @@ -97,7 +99,7 @@ namespace Tgstation.Server.Host.Controllers using (var instanceReferenceCheck = instanceManager.GetInstanceReference(metadata)) online = instanceReferenceCheck != null; - if (metadata.Online.Value == online) + if (metadata.Require(x => x.Online) == online) return false; const string OfflineWord = "offline"; @@ -120,19 +122,29 @@ namespace Tgstation.Server.Host.Controllers /// The to grab. If , will be used. /// A resulting in the that should be returned. /// The context of should be as small as possible so as to avoid race conditions. This function can return a if the requested instance was offline. - protected async ValueTask WithComponentInstance(Func> action, Models.Instance instance = null) + protected async ValueTask WithComponentInstanceNullable(Func> action, Models.Instance? instance = null) { ArgumentNullException.ThrowIfNull(action); - instance ??= Instance; + instance ??= Instance ?? throw new InvalidOperationException("ComponentInterfacingController has no Instance!"); using var instanceReference = instanceManager.GetInstanceReference(instance); - using (LogContext.PushProperty(SerilogContextHelper.InstanceReferenceContextProperty, instanceReference.Uid)) + using (LogContext.PushProperty(SerilogContextHelper.InstanceReferenceContextProperty, instanceReference?.Uid)) { if (instanceReference == null) return Conflict(new ErrorMessageResponse(ErrorCode.InstanceOffline)); return await action(instanceReference); } } + + /// + /// Run a given with the relevant . + /// + /// A accepting the and returning a with the . + /// The to grab. If , will be used. + /// A resulting in the that should be returned. + /// The context of should be as small as possible so as to avoid race conditions. This function can return a if the requested instance was offline. + protected async ValueTask WithComponentInstance(Func> action, Models.Instance? instance = null) + => (await WithComponentInstanceNullable(async core => await action(core), instance))!; } } diff --git a/src/Tgstation.Server.Host/Controllers/ConfigurationController.cs b/src/Tgstation.Server.Host/Controllers/ConfigurationController.cs index 37b7e89ed1..8232be095c 100644 --- a/src/Tgstation.Server.Host/Controllers/ConfigurationController.cs +++ b/src/Tgstation.Server.Host/Controllers/ConfigurationController.cs @@ -73,7 +73,7 @@ namespace Tgstation.Server.Host.Controllers public async ValueTask Update([FromBody] ConfigurationFileRequest model, CancellationToken cancellationToken) { ArgumentNullException.ThrowIfNull(model); - if (ForbidDueToModeConflicts(model.Path, out var systemIdentity)) + if (ForbidDueToModeConflicts(model.Path!, out var systemIdentity)) return Forbid(); try @@ -84,7 +84,7 @@ namespace Tgstation.Server.Host.Controllers var newFile = await instance .Configuration .Write( - model.Path, + model.Path!, systemIdentity, model.LastReadHash, cancellationToken); @@ -164,7 +164,7 @@ namespace Tgstation.Server.Host.Controllers [ProducesResponseType(typeof(ErrorMessageResponse), 409)] [ProducesResponseType(typeof(ErrorMessageResponse), 410)] public ValueTask Directory( - string directoryPath, + string? directoryPath, [FromQuery] int? page, [FromQuery] int? pageSize, CancellationToken cancellationToken) @@ -240,6 +240,9 @@ namespace Tgstation.Server.Host.Controllers { ArgumentNullException.ThrowIfNull(model); + if (model.Path == null) + return BadRequest(new ErrorMessageResponse(ErrorCode.ModelValidationFailure)); + if (ForbidDueToModeConflicts(model.Path, out var systemIdentity)) return Forbid(); @@ -338,7 +341,7 @@ namespace Tgstation.Server.Host.Controllers /// The path to validate if any. /// The to use when calling into . /// if a should be returned, otherwise. - bool ForbidDueToModeConflicts(string path, out ISystemIdentity systemIdentityToUse) + bool ForbidDueToModeConflicts(string? path, out ISystemIdentity? systemIdentityToUse) { if (Instance.ConfigurationType == ConfigurationType.Disallowed || (Instance.ConfigurationType == ConfigurationType.SystemIdentityWrite && AuthenticationContext.SystemIdentity == null) diff --git a/src/Tgstation.Server.Host/Controllers/DreamDaemonController.cs b/src/Tgstation.Server.Host/Controllers/DreamDaemonController.cs index ca84dac467..069b272959 100644 --- a/src/Tgstation.Server.Host/Controllers/DreamDaemonController.cs +++ b/src/Tgstation.Server.Host/Controllers/DreamDaemonController.cs @@ -90,7 +90,7 @@ namespace Tgstation.Server.Host.Controllers var job = Job.Create(JobCode.WatchdogLaunch, AuthenticationContext.User, Instance, DreamDaemonRights.Shutdown); await jobManager.RegisterOperation( job, - (core, databaseContextFactory, paramJob, progressHandler, innerCt) => core.Watchdog.Launch(innerCt), + (core, databaseContextFactory, paramJob, progressHandler, innerCt) => core!.Watchdog.Launch(innerCt), cancellationToken); return Accepted(job.ToApi()); }); @@ -172,7 +172,7 @@ namespace Tgstation.Server.Host.Controllers if (current == default) return this.Gone(); - if (model.Port.HasValue && model.Port.Value != current.Port.Value) + if (model.Port.HasValue && model.Port.Value != current.Port!.Value) { var verifiedPort = await portAllocator .GetAvailablePort( @@ -201,14 +201,15 @@ namespace Tgstation.Server.Host.Controllers return false; } + var ddRights = InstancePermissionSet.DreamDaemonRights!.Value; if (CheckModified(x => x.AllowWebClient, DreamDaemonRights.SetWebClient) || CheckModified(x => x.AutoStart, DreamDaemonRights.SetAutoStart) || CheckModified(x => x.Port, DreamDaemonRights.SetPort) || CheckModified(x => x.SecurityLevel, DreamDaemonRights.SetSecurity) || CheckModified(x => x.Visibility, DreamDaemonRights.SetVisibility) - || (model.SoftRestart.HasValue && !AuthenticationContext.InstancePermissionSet.DreamDaemonRights.Value.HasFlag(DreamDaemonRights.SoftRestart)) - || (model.SoftShutdown.HasValue && !AuthenticationContext.InstancePermissionSet.DreamDaemonRights.Value.HasFlag(DreamDaemonRights.SoftShutdown)) - || (!String.IsNullOrWhiteSpace(model.BroadcastMessage) && !AuthenticationContext.InstancePermissionSet.DreamDaemonRights.Value.HasFlag(DreamDaemonRights.BroadcastMessage)) + || (model.SoftRestart.HasValue && !ddRights.HasFlag(DreamDaemonRights.SoftRestart)) + || (model.SoftShutdown.HasValue && !ddRights.HasFlag(DreamDaemonRights.SoftShutdown)) + || (!String.IsNullOrWhiteSpace(model.BroadcastMessage) && !ddRights.HasFlag(DreamDaemonRights.BroadcastMessage)) || CheckModified(x => x.StartupTimeout, DreamDaemonRights.SetStartupTimeout) || CheckModified(x => x.HealthCheckSeconds, DreamDaemonRights.SetHealthCheckInterval) || CheckModified(x => x.DumpOnHealthCheckRestart, DreamDaemonRights.CreateDump) @@ -270,7 +271,7 @@ namespace Tgstation.Server.Host.Controllers await jobManager.RegisterOperation( job, - (core, paramJob, databaseContextFactory, progressReporter, ct) => core.Watchdog.Restart(false, ct), + (core, paramJob, databaseContextFactory, progressReporter, ct) => core!.Watchdog.Restart(false, ct), cancellationToken); return Accepted(job.ToApi()); }); @@ -296,7 +297,7 @@ namespace Tgstation.Server.Host.Controllers await jobManager.RegisterOperation( job, - (core, databaseContextFactory, paramJob, progressReporter, ct) => core.Watchdog.CreateDump(ct), + (core, databaseContextFactory, paramJob, progressReporter, ct) => core!.Watchdog.CreateDump(ct), cancellationToken); return Accepted(job.ToApi()); }); @@ -308,7 +309,7 @@ namespace Tgstation.Server.Host.Controllers /// If there was a settings change made that forced a switch to . /// The for the operation. /// A resulting in the of the operation. - ValueTask ReadImpl(DreamDaemonSettings settings, bool knownForcedReboot, CancellationToken cancellationToken) + ValueTask ReadImpl(DreamDaemonSettings? settings, bool knownForcedReboot, CancellationToken cancellationToken) => WithComponentInstance(async instance => { var dd = instance.Watchdog; @@ -322,7 +323,7 @@ namespace Tgstation.Server.Host.Controllers .Instances .AsQueryable() .Where(x => x.Id == Instance.Id) - .Select(x => x.DreamDaemonSettings) + .Select(x => x.DreamDaemonSettings!) .FirstOrDefaultAsync(cancellationToken); if (settings == default) return this.Gone(); @@ -334,13 +335,13 @@ namespace Tgstation.Server.Host.Controllers var alphaActive = dd.AlphaIsActive; var llp = dd.LastLaunchParameters; var rstate = dd.RebootState; - result.AutoStart = settings.AutoStart.Value; - result.CurrentPort = llp?.Port.Value; - result.CurrentSecurity = llp?.SecurityLevel.Value; - result.CurrentVisibility = llp?.Visibility.Value; - result.CurrentAllowWebclient = llp?.AllowWebClient.Value; - result.Port = settings.Port.Value; - result.AllowWebClient = settings.AllowWebClient.Value; + result.AutoStart = settings.AutoStart!.Value; + result.CurrentPort = llp?.Port!.Value; + result.CurrentSecurity = llp?.SecurityLevel!.Value; + result.CurrentVisibility = llp?.Visibility!.Value; + result.CurrentAllowWebclient = llp?.AllowWebClient!.Value; + result.Port = settings.Port!.Value; + result.AllowWebClient = settings.AllowWebClient!.Value; var firstIteration = true; do @@ -357,18 +358,18 @@ namespace Tgstation.Server.Host.Controllers } while (result.Status == WatchdogStatus.Online && !result.SessionId.HasValue); // this is the one invalid combo, it's not that racy - result.SecurityLevel = settings.SecurityLevel.Value; - result.Visibility = settings.Visibility.Value; + result.SecurityLevel = settings.SecurityLevel!.Value; + result.Visibility = settings.Visibility!.Value; result.SoftRestart = rstate == RebootState.Restart; result.SoftShutdown = rstate == RebootState.Shutdown; if (rstate == RebootState.Normal && knownForcedReboot) result.SoftRestart = true; - result.StartupTimeout = settings.StartupTimeout.Value; - result.HealthCheckSeconds = settings.HealthCheckSeconds.Value; - result.DumpOnHealthCheckRestart = settings.DumpOnHealthCheckRestart.Value; - result.TopicRequestTimeout = settings.TopicRequestTimeout.Value; + result.StartupTimeout = settings.StartupTimeout!.Value; + result.HealthCheckSeconds = settings.HealthCheckSeconds!.Value; + result.DumpOnHealthCheckRestart = settings.DumpOnHealthCheckRestart!.Value; + result.TopicRequestTimeout = settings.TopicRequestTimeout!.Value; result.AdditionalParameters = settings.AdditionalParameters; result.StartProfiler = settings.StartProfiler; result.LogOutput = settings.LogOutput; diff --git a/src/Tgstation.Server.Host/Controllers/DreamMakerController.cs b/src/Tgstation.Server.Host/Controllers/DreamMakerController.cs index 714713763f..419bc9c90d 100644 --- a/src/Tgstation.Server.Host/Controllers/DreamMakerController.cs +++ b/src/Tgstation.Server.Host/Controllers/DreamMakerController.cs @@ -27,7 +27,6 @@ namespace Tgstation.Server.Host.Controllers /// for managing the deployment system. /// [Route(Routes.DreamMaker)] -#pragma warning disable CA1506 // TODO: Decomplexify public sealed class DreamMakerController : InstanceRequiredController { /// @@ -85,6 +84,10 @@ namespace Tgstation.Server.Host.Controllers .AsQueryable() .Where(x => x.InstanceId == Instance.Id) .FirstOrDefaultAsync(cancellationToken); + + if (dreamMakerSettings == null) + return this.Gone(); + return Json(dreamMakerSettings.ToApi()); } @@ -148,7 +151,7 @@ namespace Tgstation.Server.Host.Controllers await jobManager.RegisterOperation( job, (core, databaseContextFactory, paramJob, progressReporter, jobCancellationToken) - => core.DreamMaker.DeploymentProcess(paramJob, databaseContextFactory, progressReporter, jobCancellationToken), + => core!.DreamMaker.DeploymentProcess(paramJob, databaseContextFactory, progressReporter, jobCancellationToken), cancellationToken); return Accepted(job.ToApi()); } @@ -187,9 +190,10 @@ namespace Tgstation.Server.Host.Controllers if (hostModel == null) return this.Gone(); + var dreamMakerRights = InstancePermissionSet.DreamMakerRights!.Value; if (model.ProjectName != null) { - if (!AuthenticationContext.InstancePermissionSet.DreamMakerRights.Value.HasFlag(DreamMakerRights.SetDme)) + if (!dreamMakerRights.HasFlag(DreamMakerRights.SetDme)) return Forbid(); if (model.ProjectName.Length == 0) hostModel.ProjectName = null; @@ -199,10 +203,10 @@ namespace Tgstation.Server.Host.Controllers if (model.ApiValidationPort.HasValue) { - if (!AuthenticationContext.InstancePermissionSet.DreamMakerRights.Value.HasFlag(DreamMakerRights.SetApiValidationPort)) + if (!dreamMakerRights.HasFlag(DreamMakerRights.SetApiValidationPort)) return Forbid(); - if (model.ApiValidationPort.Value != hostModel.ApiValidationPort.Value) + if (model.ApiValidationPort.Value != hostModel.ApiValidationPort!.Value) { var verifiedPort = await portAllocator .GetAvailablePort( @@ -218,28 +222,28 @@ namespace Tgstation.Server.Host.Controllers if (model.ApiValidationSecurityLevel.HasValue) { - if (!AuthenticationContext.InstancePermissionSet.DreamMakerRights.Value.HasFlag(DreamMakerRights.SetSecurityLevel)) + if (!dreamMakerRights.HasFlag(DreamMakerRights.SetSecurityLevel)) return Forbid(); hostModel.ApiValidationSecurityLevel = model.ApiValidationSecurityLevel; } if (model.RequireDMApiValidation.HasValue) { - if (!AuthenticationContext.InstancePermissionSet.DreamMakerRights.Value.HasFlag(DreamMakerRights.SetApiValidationRequirement)) + if (!dreamMakerRights.HasFlag(DreamMakerRights.SetApiValidationRequirement)) return Forbid(); hostModel.RequireDMApiValidation = model.RequireDMApiValidation; } if (model.Timeout.HasValue) { - if (!AuthenticationContext.InstancePermissionSet.DreamMakerRights.Value.HasFlag(DreamMakerRights.SetTimeout)) + if (!dreamMakerRights.HasFlag(DreamMakerRights.SetTimeout)) return Forbid(); hostModel.Timeout = model.Timeout; } await DatabaseContext.Save(cancellationToken); - if ((AuthenticationContext.GetRight(RightsType.DreamMaker) & (ulong)DreamMakerRights.Read) == 0) + if (!dreamMakerRights.HasFlag(DreamMakerRights.Read)) return NoContent(); return await Read(cancellationToken); @@ -252,17 +256,17 @@ namespace Tgstation.Server.Host.Controllers IQueryable BaseCompileJobsQuery() => DatabaseContext .CompileJobs .AsQueryable() - .Include(x => x.Job) + .Include(x => x.Job!) .ThenInclude(x => x.StartedBy) - .Include(x => x.Job) + .Include(x => x.Job!) .ThenInclude(x => x.Instance) - .Include(x => x.RevisionInformation) - .ThenInclude(x => x.PrimaryTestMerge) + .Include(x => x.RevisionInformation!) + .ThenInclude(x => x.PrimaryTestMerge!) .ThenInclude(x => x.MergedBy) .Include(x => x.RevisionInformation) - .ThenInclude(x => x.ActiveTestMerges) - .ThenInclude(x => x.TestMerge) - .ThenInclude(x => x.MergedBy) - .Where(x => x.Job.Instance.Id == Instance.Id); + .ThenInclude(x => x.ActiveTestMerges!) + .ThenInclude(x => x!.TestMerge) + .ThenInclude(x => x!.MergedBy) + .Where(x => x.Job.Instance!.Id == Instance.Id); } } diff --git a/src/Tgstation.Server.Host/Controllers/EngineController.cs b/src/Tgstation.Server.Host/Controllers/EngineController.cs index 75c7ef289b..c1a20fe99c 100644 --- a/src/Tgstation.Server.Host/Controllers/EngineController.cs +++ b/src/Tgstation.Server.Host/Controllers/EngineController.cs @@ -120,7 +120,7 @@ namespace Tgstation.Server.Host.Controllers EngineVersion = x, }) .AsQueryable() - .OrderBy(x => x.EngineVersion.ToString()))), + .OrderBy(x => x.EngineVersion!.ToString()))), null, page, pageSize, @@ -155,16 +155,16 @@ namespace Tgstation.Server.Host.Controllers var uploadingZip = model.UploadCustomZip == true; - var userByondRights = AuthenticationContext.InstancePermissionSet.EngineRights.Value; - var isByondEngine = model.EngineVersion.Engine.Value == EngineType.Byond; + var engineRights = InstancePermissionSet.EngineRights!.Value; + var isByondEngine = model.EngineVersion!.Engine!.Value == EngineType.Byond; var officialPerm = isByondEngine ? EngineRights.InstallOfficialOrChangeActiveByondVersion : EngineRights.InstallOfficialOrChangeActiveOpenDreamVersion; var customPerm = isByondEngine ? EngineRights.InstallCustomByondVersion : EngineRights.InstallCustomOpenDreamVersion; - if ((!userByondRights.HasFlag(officialPerm) && !uploadingZip) - || (!userByondRights.HasFlag(customPerm) && uploadingZip)) + if ((!engineRights.HasFlag(officialPerm) && !uploadingZip) + || (!engineRights.HasFlag(customPerm) && uploadingZip)) return Forbid(); // remove cruff fields @@ -217,7 +217,7 @@ namespace Tgstation.Server.Host.Controllers EngineRights.CancelInstall); job.Description += $" {model.EngineVersion}"; - IFileUploadTicket fileUploadTicket = null; + IFileUploadTicket? fileUploadTicket = null; if (uploadingZip) fileUploadTicket = fileTransferService.CreateUpload(FileUploadStreamKind.None); @@ -227,7 +227,7 @@ namespace Tgstation.Server.Host.Controllers job, async (core, databaseContextFactory, paramJob, progressHandler, jobCancellationToken) => { - MemoryStream zipFileStream = null; + MemoryStream? zipFileStream = null; if (fileUploadTicket != null) await using (fileUploadTicket) { @@ -245,7 +245,7 @@ namespace Tgstation.Server.Host.Controllers } await using (zipFileStream) - await core.EngineManager.ChangeVersion( + await core!.EngineManager.ChangeVersion( progressHandler, model.EngineVersion, zipFileStream, @@ -291,18 +291,19 @@ namespace Tgstation.Server.Host.Controllers if (earlyOut != null) return earlyOut; - var notInstalledResponse = await WithComponentInstance( + var engineVersion = model.EngineVersion!; + var notInstalledResponse = await WithComponentInstanceNullable( instance => { var byondManager = instance.EngineManager; - - if (model.EngineVersion.Equals(byondManager.ActiveVersion)) - return ValueTask.FromResult( + var activeVersion = byondManager.ActiveVersion; + if (activeVersion != null && engineVersion.Equals(activeVersion)) + return ValueTask.FromResult( Conflict(new ErrorMessageResponse(ErrorCode.EngineCannotDeleteActiveVersion))); - var versionNotInstalled = !byondManager.InstalledVersions.Any(x => x.Equals(model.EngineVersion)); + var versionNotInstalled = !byondManager.InstalledVersions.Any(x => x.Equals(engineVersion)); - return ValueTask.FromResult( + return ValueTask.FromResult( versionNotInstalled ? this.Gone() : null); @@ -311,24 +312,24 @@ namespace Tgstation.Server.Host.Controllers if (notInstalledResponse != null) return notInstalledResponse; - var isByondVersion = model.EngineVersion.Engine.Value == EngineType.Byond; + var isByondVersion = engineVersion.Engine!.Value == EngineType.Byond; // run the install through the job manager var cancelRight = isByondVersion - ? model.EngineVersion.CustomIteration.HasValue + ? engineVersion.CustomIteration.HasValue ? EngineRights.InstallCustomByondVersion : EngineRights.InstallOfficialOrChangeActiveByondVersion - : model.EngineVersion.CustomIteration.HasValue + : engineVersion.CustomIteration.HasValue ? EngineRights.InstallOfficialOrChangeActiveOpenDreamVersion : EngineRights.InstallCustomOpenDreamVersion; var job = Models.Job.Create(JobCode.EngineDelete, AuthenticationContext.User, Instance, cancelRight); - job.Description += $" {model.EngineVersion}"; + job.Description += $" {engineVersion}"; await jobManager.RegisterOperation( job, (instanceCore, databaseContextFactory, job, progressReporter, jobCancellationToken) - => instanceCore.EngineManager.DeleteVersion(progressReporter, model.EngineVersion, jobCancellationToken), + => instanceCore!.EngineManager.DeleteVersion(progressReporter, engineVersion, jobCancellationToken), cancellationToken); var apiResponse = job.ToApi(); @@ -339,8 +340,8 @@ namespace Tgstation.Server.Host.Controllers /// Validate and normalize a given . /// /// The to validate and normalize. - /// The to return, if any. - BadRequestObjectResult ValidateEngineVersion(EngineVersion version) + /// The to return, if any. otherwise. + BadRequestObjectResult? ValidateEngineVersion(EngineVersion? version) { if (version == null || !version.Engine.HasValue) return BadRequest(new ErrorMessageResponse(ErrorCode.ModelValidationFailure)); @@ -357,7 +358,7 @@ namespace Tgstation.Server.Host.Controllers if (isByond) { - version.Version = NormalizeByondVersion(version.Version); + version.Version = NormalizeByondVersion(version.Version!); if (version.Version.Build != -1) return BadRequest(new ErrorMessageResponse(ErrorCode.ModelValidationFailure)); } diff --git a/src/Tgstation.Server.Host/Controllers/InstanceController.cs b/src/Tgstation.Server.Host/Controllers/InstanceController.cs index d63396daf9..ded9313475 100644 --- a/src/Tgstation.Server.Host/Controllers/InstanceController.cs +++ b/src/Tgstation.Server.Host/Controllers/InstanceController.cs @@ -1,5 +1,6 @@ using System; using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; using System.IO; using System.Linq; using System.Linq.Expressions; @@ -43,11 +44,6 @@ namespace Tgstation.Server.Host.Controllers /// public const string InstanceAttachFileName = "TGS4_ALLOW_INSTANCE_ATTACH"; - /// - /// Prefix for move s. - /// - const string MoveInstanceJobPrefix = "Move instance ID "; - /// /// The for the . /// @@ -145,8 +141,8 @@ namespace Tgstation.Server.Host.Controllers { ArgumentNullException.ThrowIfNull(model); - if (String.IsNullOrWhiteSpace(model.Name)) - return BadRequest(new ErrorMessageResponse(ErrorCode.InstanceWhitespaceName)); + if (String.IsNullOrWhiteSpace(model.Name) || String.IsNullOrWhiteSpace(model.Path)) + return BadRequest(new ErrorMessageResponse(ErrorCode.InstanceWhitespaceNameOrPath)); var unNormalizedPath = model.Path; var targetInstancePath = NormalizePath(unNormalizedPath); @@ -170,7 +166,7 @@ namespace Tgstation.Server.Host.Controllers return Conflict(new ErrorMessageResponse(ErrorCode.InstanceAtConflictingPath)); // Validate it's not a child of any other instance - IActionResult earlyOut = null; + IActionResult? earlyOut = null; ulong countOfOtherInstances = 0; using (var cts = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken)) { @@ -190,7 +186,7 @@ namespace Tgstation.Server.Host.Controllers { if (++countOfOtherInstances >= generalConfiguration.InstanceLimit) earlyOut ??= Conflict(new ErrorMessageResponse(ErrorCode.InstanceLimitReached)); - else if (InstanceIsChildOf(otherInstance.Path)) + else if (InstanceIsChildOf(otherInstance.Path!)) earlyOut ??= Conflict(new ErrorMessageResponse(ErrorCode.InstanceAtConflictingPath)); if (earlyOut != null && !newCancellationToken.IsCancellationRequested) @@ -306,15 +302,16 @@ namespace Tgstation.Server.Host.Controllers .FirstOrDefaultAsync(cancellationToken); if (originalModel == default) return this.Gone(); - if (originalModel.Online.Value) + if (originalModel.Online!.Value) return Conflict(new ErrorMessageResponse(ErrorCode.InstanceDetachOnline)); DatabaseContext.Instances.Remove(originalModel); - var attachFileName = ioManager.ConcatPath(originalModel.Path, InstanceAttachFileName); + var originalPath = originalModel.Path!; + var attachFileName = ioManager.ConcatPath(originalPath, InstanceAttachFileName); try { - if (await ioManager.DirectoryExists(originalModel.Path, cancellationToken)) + if (await ioManager.DirectoryExists(originalPath, cancellationToken)) await ioManager.WriteAllBytes(attachFileName, Array.Empty(), cancellationToken); } catch (OperationCanceledException) @@ -353,14 +350,15 @@ namespace Tgstation.Server.Host.Controllers .Where(x => x.Id == model.Id && x.SwarmIdentifer == swarmConfiguration.Identifier); var moveJob = await InstanceQuery() - .SelectMany(x => x.Jobs). - Where(x => !x.StoppedAt.HasValue && x.Description.StartsWith(MoveInstanceJobPrefix)) - .Select(x => new Job(x.Id.Value)).FirstOrDefaultAsync(cancellationToken); + .SelectMany(x => x.Jobs) + .Where(x => !x.StoppedAt.HasValue && x.JobCode == JobCode.Move) + .Select(x => new Job(x.Id!.Value)) + .FirstOrDefaultAsync(cancellationToken); - if (moveJob != default) + if (moveJob != null) { // don't allow them to cancel it if they can't start it. - if (!AuthenticationContext.PermissionSet.InstanceManagerRights.Value.HasFlag(InstanceManagerRights.Relocate)) + if (!AuthenticationContext.PermissionSet.InstanceManagerRights!.Value.HasFlag(InstanceManagerRights.Relocate)) return Forbid(); await jobManager.CancelJob(moveJob, AuthenticationContext.User, true, cancellationToken); // cancel it now } @@ -393,8 +391,9 @@ namespace Tgstation.Server.Host.Controllers return false; } - string originalModelPath = null; - string rawPath = null; + string? originalModelPath = null; + string? rawPath = null; + var originalOnline = originalModel.Online!.Value; if (model.Path != null) { rawPath = NormalizePath(model.Path); @@ -403,7 +402,7 @@ namespace Tgstation.Server.Host.Controllers { if (!userRights.HasFlag(InstanceManagerRights.Relocate)) return Forbid(); - if (originalModel.Online.Value && model.Online != true) + if (originalOnline && model.Online != true) return Conflict(new ErrorMessageResponse(ErrorCode.InstanceRelocateOnline)); var dirExistsTask = ioManager.DirectoryExists(model.Path, cancellationToken); @@ -415,8 +414,7 @@ namespace Tgstation.Server.Host.Controllers } } - var oldAutoUpdateInterval = originalModel.AutoUpdateInterval.Value; - var originalOnline = originalModel.Online.Value; + var oldAutoUpdateInterval = originalModel.AutoUpdateInterval!.Value; var renamed = model.Name != null && originalModel.Name != model.Name; if (CheckModified(x => x.AutoUpdateInterval, InstanceManagerRights.SetAutoUpdate) @@ -443,16 +441,16 @@ namespace Tgstation.Server.Host.Controllers if (renamed) { // ignoring retval because we don't care if it's offline - await WithComponentInstance( + await WithComponentInstanceNullable( async componentInstance => { - await componentInstance.InstanceRenamed(originalModel.Name, cancellationToken); + await componentInstance.InstanceRenamed(originalModel.Name!, cancellationToken); return null; }, originalModel); } - var oldAutoStart = originalModel.DreamDaemonSettings.AutoStart; + var oldAutoStart = originalModel.DreamDaemonSettings!.AutoStart; try { if (originalOnline && model.Online == false) @@ -487,14 +485,14 @@ namespace Tgstation.Server.Host.Controllers var moving = originalModelPath != null; if (moving) { - var description = $"{MoveInstanceJobPrefix}{originalModel.Id} from {originalModelPath} to {rawPath}"; + var description = $"Move instance ID {originalModel.Id} from {originalModelPath} to {rawPath}"; var job = Job.Create(JobCode.Move, AuthenticationContext.User, originalModel, InstanceManagerRights.Relocate); job.Description = description; await jobManager.RegisterOperation( job, (core, databaseContextFactory, paramJob, progressHandler, ct) // core will be null here since the instance is offline - => InstanceOperations.MoveInstance(originalModel, originalModelPath, ct), + => InstanceOperations.MoveInstance(originalModel, originalModelPath!, ct), cancellationToken); api.MoveJob = job.ToApi(); } @@ -502,7 +500,7 @@ namespace Tgstation.Server.Host.Controllers if (model.AutoUpdateInterval.HasValue && oldAutoUpdateInterval != model.AutoUpdateInterval) { // ignoring retval because we don't care if it's offline - await WithComponentInstance( + await WithComponentInstanceNullable( async componentInstance => { await componentInstance.SetAutoUpdateInterval(model.AutoUpdateInterval.Value); @@ -538,9 +536,9 @@ namespace Tgstation.Server.Host.Controllers .Instances .AsQueryable() .Where(x => x.SwarmIdentifer == swarmConfiguration.Identifier); - if (!AuthenticationContext.PermissionSet.InstanceManagerRights.Value.HasFlag(InstanceManagerRights.List)) + if (!AuthenticationContext.PermissionSet.InstanceManagerRights!.Value.HasFlag(InstanceManagerRights.List)) query = query - .Where(x => x.InstancePermissionSets.Any(y => y.PermissionSetId == AuthenticationContext.PermissionSet.Id.Value)) + .Where(x => x.InstancePermissionSets.Any(y => y.PermissionSetId == AuthenticationContext.PermissionSet.Id)) .Where(x => x.InstancePermissionSets.Any(instanceUser => instanceUser.EngineRights != EngineRights.None || instanceUser.ChatBotRights != ChatBotRights.None || @@ -555,8 +553,9 @@ namespace Tgstation.Server.Host.Controllers var moveJobs = await GetBaseQuery() .SelectMany(x => x.Jobs) - .Where(x => !x.StoppedAt.HasValue && x.Description.StartsWith(MoveInstanceJobPrefix)) - .Include(x => x.StartedBy).ThenInclude(x => x.CreatedBy) + .Where(x => !x.StoppedAt.HasValue && x.JobCode == JobCode.Move) + .Include(x => x.StartedBy!) + .ThenInclude(x => x.CreatedBy) .Include(x => x.Instance) .ToListAsync(cancellationToken); @@ -569,7 +568,7 @@ namespace Tgstation.Server.Host.Controllers async instance => { needsUpdate |= ValidateInstanceOnlineStatus(instance); - instance.MoveJob = moveJobs.FirstOrDefault(x => x.Instance.Id == instance.Id)?.ToApi(); + instance.MoveJob = moveJobs.FirstOrDefault(x => x.Instance!.Id == instance.Id)?.ToApi(); await CheckAccessible(instance, cancellationToken); }, page, @@ -596,7 +595,7 @@ namespace Tgstation.Server.Host.Controllers [ProducesResponseType(typeof(ErrorMessageResponse), 410)] public async ValueTask GetId(long id, CancellationToken cancellationToken) { - var cantList = !AuthenticationContext.PermissionSet.InstanceManagerRights.Value.HasFlag(InstanceManagerRights.List); + var cantList = !AuthenticationContext.PermissionSet.InstanceManagerRights!.Value.HasFlag(InstanceManagerRights.List); IQueryable QueryForUser() { var query = DatabaseContext @@ -617,8 +616,8 @@ namespace Tgstation.Server.Host.Controllers if (ValidateInstanceOnlineStatus(instance)) await DatabaseContext.Save(cancellationToken); - if (cantList && !instance.InstancePermissionSets.Any(instanceUser => instanceUser.PermissionSetId == AuthenticationContext.PermissionSet.Id.Value && - (instanceUser.RepositoryRights != RepositoryRights.None || + if (cantList && !instance.InstancePermissionSets.Any(instanceUser => instanceUser.PermissionSetId == AuthenticationContext.PermissionSet.Require(x => x.Id) + && (instanceUser.RepositoryRights != RepositoryRights.None || instanceUser.EngineRights != EngineRights.None || instanceUser.ChatBotRights != ChatBotRights.None || instanceUser.ConfigurationRights != ConfigurationRights.None || @@ -631,8 +630,8 @@ namespace Tgstation.Server.Host.Controllers var moveJob = await QueryForUser() .SelectMany(x => x.Jobs) - .Where(x => !x.StoppedAt.HasValue && x.Description.StartsWith(MoveInstanceJobPrefix)) - .Include(x => x.StartedBy) + .Where(x => !x.StoppedAt.HasValue && x.JobCode == JobCode.Move) + .Include(x => x.StartedBy!) .ThenInclude(x => x.CreatedBy) .Include(x => x.Instance) .FirstOrDefaultAsync(cancellationToken); @@ -662,7 +661,7 @@ namespace Tgstation.Server.Host.Controllers // ensure the current user has write privilege on the instance var usersInstancePermissionSet = await BaseQuery() .SelectMany(x => x.InstancePermissionSets) - .Where(x => x.PermissionSetId == AuthenticationContext.PermissionSet.Id.Value) + .Where(x => x.PermissionSetId == AuthenticationContext.PermissionSet.Id) .FirstOrDefaultAsync(cancellationToken); if (usersInstancePermissionSet == default) { @@ -691,7 +690,7 @@ namespace Tgstation.Server.Host.Controllers /// The . /// The for the operation. /// A resulting in the new or if ports could not be allocated. - async ValueTask CreateDefaultInstance(InstanceCreateRequest initialSettings, CancellationToken cancellationToken) + async ValueTask CreateDefaultInstance(InstanceCreateRequest initialSettings, CancellationToken cancellationToken) { var ddPort = await portAllocator.GetAvailablePort(1024, false, cancellationToken); if (!ddPort.HasValue) @@ -771,12 +770,12 @@ namespace Tgstation.Server.Host.Controllers /// /// An optional existing to update. /// or a new with full rights. - InstancePermissionSet InstanceAdminPermissionSet(InstancePermissionSet permissionSetToModify) + InstancePermissionSet InstanceAdminPermissionSet(InstancePermissionSet? permissionSetToModify) { permissionSetToModify ??= new InstancePermissionSet() { PermissionSet = AuthenticationContext.PermissionSet, - PermissionSetId = AuthenticationContext.PermissionSet.Id.Value, + PermissionSetId = AuthenticationContext.PermissionSet.Require(x => x.Id), }; permissionSetToModify.EngineRights = RightsHelper.AllRights(); permissionSetToModify.ChatBotRights = RightsHelper.AllRights(); @@ -793,7 +792,8 @@ namespace Tgstation.Server.Host.Controllers /// /// The path to normalize. /// The normalized . - string NormalizePath(string path) + [return: NotNullIfNotNull(nameof(path))] + string? NormalizePath(string? path) { if (path == null) return null; diff --git a/src/Tgstation.Server.Host/Controllers/InstancePermissionSetController.cs b/src/Tgstation.Server.Host/Controllers/InstancePermissionSetController.cs index 13defd8048..d86f0b7877 100644 --- a/src/Tgstation.Server.Host/Controllers/InstancePermissionSetController.cs +++ b/src/Tgstation.Server.Host/Controllers/InstancePermissionSetController.cs @@ -115,7 +115,7 @@ namespace Tgstation.Server.Host.Controllers RepositoryRights = RightsHelper.Clamp(model.RepositoryRights ?? RepositoryRights.None), InstancePermissionSetRights = RightsHelper.Clamp(model.InstancePermissionSetRights ?? InstancePermissionSetRights.None), PermissionSetId = model.PermissionSetId, - InstanceId = Instance.Id.Value, + InstanceId = Instance.Require(x => x.Id), }; DatabaseContext.InstancePermissionSets.Add(dbUser); @@ -156,16 +156,16 @@ namespace Tgstation.Server.Host.Controllers if (originalPermissionSet == null) return this.Gone(); - originalPermissionSet.EngineRights = RightsHelper.Clamp(model.EngineRights ?? originalPermissionSet.EngineRights.Value); - originalPermissionSet.RepositoryRights = RightsHelper.Clamp(model.RepositoryRights ?? originalPermissionSet.RepositoryRights.Value); - originalPermissionSet.InstancePermissionSetRights = RightsHelper.Clamp(model.InstancePermissionSetRights ?? originalPermissionSet.InstancePermissionSetRights.Value); - originalPermissionSet.ChatBotRights = RightsHelper.Clamp(model.ChatBotRights ?? originalPermissionSet.ChatBotRights.Value); - originalPermissionSet.ConfigurationRights = RightsHelper.Clamp(model.ConfigurationRights ?? originalPermissionSet.ConfigurationRights.Value); - originalPermissionSet.DreamDaemonRights = RightsHelper.Clamp(model.DreamDaemonRights ?? originalPermissionSet.DreamDaemonRights.Value); - originalPermissionSet.DreamMakerRights = RightsHelper.Clamp(model.DreamMakerRights ?? originalPermissionSet.DreamMakerRights.Value); + originalPermissionSet.EngineRights = RightsHelper.Clamp(model.EngineRights ?? originalPermissionSet.EngineRights!.Value); + originalPermissionSet.RepositoryRights = RightsHelper.Clamp(model.RepositoryRights ?? originalPermissionSet.RepositoryRights!.Value); + originalPermissionSet.InstancePermissionSetRights = RightsHelper.Clamp(model.InstancePermissionSetRights ?? originalPermissionSet.InstancePermissionSetRights!.Value); + originalPermissionSet.ChatBotRights = RightsHelper.Clamp(model.ChatBotRights ?? originalPermissionSet.ChatBotRights!.Value); + originalPermissionSet.ConfigurationRights = RightsHelper.Clamp(model.ConfigurationRights ?? originalPermissionSet.ConfigurationRights!.Value); + originalPermissionSet.DreamDaemonRights = RightsHelper.Clamp(model.DreamDaemonRights ?? originalPermissionSet.DreamDaemonRights!.Value); + originalPermissionSet.DreamMakerRights = RightsHelper.Clamp(model.DreamMakerRights ?? originalPermissionSet.DreamMakerRights!.Value); await DatabaseContext.Save(cancellationToken); - var showFullPermissionSet = originalPermissionSet.PermissionSetId == AuthenticationContext.PermissionSet.Id.Value + var showFullPermissionSet = originalPermissionSet.PermissionSetId == AuthenticationContext.PermissionSet.Require(x => x.Id) || (AuthenticationContext.GetRight(RightsType.InstancePermissionSet) & (ulong)InstancePermissionSetRights.Read) != 0; return Json( showFullPermissionSet @@ -184,7 +184,7 @@ namespace Tgstation.Server.Host.Controllers [HttpGet] [TgsAuthorize] [ProducesResponseType(typeof(InstancePermissionSetResponse), 200)] - public IActionResult Read() => Json(AuthenticationContext.InstancePermissionSet.ToApi()); + public IActionResult Read() => Json(InstancePermissionSet.ToApi()); /// /// Lists s for the instance. diff --git a/src/Tgstation.Server.Host/Controllers/InstanceRequiredController.cs b/src/Tgstation.Server.Host/Controllers/InstanceRequiredController.cs index cdf3f0e139..f3a6ff9bdc 100644 --- a/src/Tgstation.Server.Host/Controllers/InstanceRequiredController.cs +++ b/src/Tgstation.Server.Host/Controllers/InstanceRequiredController.cs @@ -12,6 +12,16 @@ namespace Tgstation.Server.Host.Controllers /// public abstract class InstanceRequiredController : ComponentInterfacingController { + /// + /// The . + /// + protected new Models.Instance Instance => base.Instance!; + + /// + /// The for the request. + /// + protected Models.InstancePermissionSet InstancePermissionSet => AuthenticationContext.InstancePermissionSet!; + /// /// Initializes a new instance of the class. /// diff --git a/src/Tgstation.Server.Host/Controllers/JobController.cs b/src/Tgstation.Server.Host/Controllers/JobController.cs index 521a560a0b..2b9f1b15f1 100644 --- a/src/Tgstation.Server.Host/Controllers/JobController.cs +++ b/src/Tgstation.Server.Host/Controllers/JobController.cs @@ -79,7 +79,7 @@ namespace Tgstation.Server.Host.Controllers .Include(x => x.StartedBy) .Include(x => x.CancelledBy) .Include(x => x.Instance) - .Where(x => x.Instance.Id == Instance.Id && !x.StoppedAt.HasValue) + .Where(x => x.Instance!.Id == Instance.Id && !x.StoppedAt.HasValue) .OrderByDescending(x => x.StartedAt))), AddJobProgressResponseTransformer, page, @@ -107,7 +107,7 @@ namespace Tgstation.Server.Host.Controllers .Include(x => x.StartedBy) .Include(x => x.CancelledBy) .Include(x => x.Instance) - .Where(x => x.Instance.Id == Instance.Id) + .Where(x => x.Instance!.Id == Instance.Id) .OrderByDescending(x => x.StartedAt))), AddJobProgressResponseTransformer, page, @@ -135,7 +135,7 @@ namespace Tgstation.Server.Host.Controllers .AsQueryable() .Include(x => x.StartedBy) .Include(x => x.Instance) - .Where(x => x.Id == id && x.Instance.Id == Instance.Id) + .Where(x => x.Id == id && x.Instance!.Id == Instance.Id) .FirstOrDefaultAsync(cancellationToken); if (job == default) return NotFound(); @@ -167,7 +167,7 @@ namespace Tgstation.Server.Host.Controllers var job = await DatabaseContext .Jobs .AsQueryable() - .Where(x => x.Id == id && x.Instance.Id == Instance.Id) + .Where(x => x.Id == id && x.Instance!.Id == Instance.Id) .Include(x => x.StartedBy) .Include(x => x.CancelledBy) .Include(x => x.Instance) diff --git a/src/Tgstation.Server.Host/Controllers/RepositoryController.cs b/src/Tgstation.Server.Host/Controllers/RepositoryController.cs index 6bc447a044..5d4d686aaf 100644 --- a/src/Tgstation.Server.Host/Controllers/RepositoryController.cs +++ b/src/Tgstation.Server.Host/Controllers/RepositoryController.cs @@ -151,7 +151,7 @@ namespace Tgstation.Server.Host.Controllers job, async (core, databaseContextFactory, paramJob, progressReporter, ct) => { - var repoManager = core.RepositoryManager; + var repoManager = core!.RepositoryManager; using var repos = await repoManager.CloneRepository( origin, cloneBranch, @@ -211,13 +211,13 @@ namespace Tgstation.Server.Host.Controllers await DatabaseContext.Save(cancellationToken); - Logger.LogInformation("Instance {instanceId} repository delete initiated by user {userId}", Instance.Id, AuthenticationContext.User.Id.Value); + Logger.LogInformation("Instance {instanceId} repository delete initiated by user {userId}", Instance.Id, AuthenticationContext.User.Require(x => x.Id)); var job = Job.Create(JobCode.RepositoryDelete, AuthenticationContext.User, Instance); var api = currentModel.ToApi(); await jobManager.RegisterOperation( job, - (core, databaseContextFactory, paramJob, progressReporter, ct) => core.RepositoryManager.DeleteRepository(ct), + (core, databaseContextFactory, paramJob, progressReporter, ct) => core!.RepositoryManager.DeleteRepository(ct), cancellationToken); api.ActiveJob = job.ToApi(); return Accepted(api); @@ -376,23 +376,23 @@ namespace Tgstation.Server.Host.Controllers var api = canRead ? currentModel.ToApi() : new RepositoryResponse(); if (canRead) { - var earlyOut = await WithComponentInstance( - async instance => - { - var repoManager = instance.RepositoryManager; - if (repoManager.CloneInProgress) - return Conflict(new ErrorMessageResponse(ErrorCode.RepoCloning)); + var earlyOut = await WithComponentInstanceNullable( + async instance => + { + var repoManager = instance.RepositoryManager; + if (repoManager.CloneInProgress) + return Conflict(new ErrorMessageResponse(ErrorCode.RepoCloning)); - if (repoManager.InUse) - return Conflict(new ErrorMessageResponse(ErrorCode.RepoBusy)); + if (repoManager.InUse) + return Conflict(new ErrorMessageResponse(ErrorCode.RepoBusy)); - using var repo = await repoManager.LoadRepository(cancellationToken); - if (repo == null) - return Conflict(new ErrorMessageResponse(ErrorCode.RepoMissing)); - await PopulateApi(api, repo, DatabaseContext, Instance, cancellationToken); + using var repo = await repoManager.LoadRepository(cancellationToken); + if (repo == null) + return Conflict(new ErrorMessageResponse(ErrorCode.RepoMissing)); + await PopulateApi(api, repo, DatabaseContext, Instance, cancellationToken); - return null; - }); + return null; + }); if (earlyOut != null) return earlyOut; @@ -402,7 +402,7 @@ namespace Tgstation.Server.Host.Controllers await DatabaseContext.Save(cancellationToken); // format the job description - string description = null; + string? description = null; if (model.UpdateFromOrigin == true) if (model.Reference != null) description = String.Format(CultureInfo.InvariantCulture, "Fetch and hard reset repository to origin/{0}", model.Reference); @@ -422,7 +422,7 @@ namespace Tgstation.Server.Host.Controllers : "T", String.Join( ", ", - model.NewTestMerges.Select( + model.NewTestMerges!.Select( x => String.Format( CultureInfo.InvariantCulture, "#{0}{1}", @@ -448,7 +448,7 @@ namespace Tgstation.Server.Host.Controllers currentModel, AuthenticationContext.User, loggerFactory.CreateLogger(), - Instance.Id.Value); + Instance.Require(x => x.Id)); // Time to access git, do it in a job await jobManager.RegisterOperation( @@ -484,16 +484,14 @@ namespace Tgstation.Server.Host.Controllers apiResponse.Reference = repository.Reference; // rev info stuff - Models.RevisionInformation revisionInfo = null; var needsDbUpdate = await RepositoryUpdateService.LoadRevisionInformation( repository, databaseContext, Logger, instance, null, - newRevInfo => revisionInfo = newRevInfo, + newRevInfo => apiResponse.RevisionInformation = newRevInfo.ToApi(), cancellationToken); - apiResponse.RevisionInformation = revisionInfo.ToApi(); return needsDbUpdate; } } diff --git a/src/Tgstation.Server.Host/Controllers/Results/LimitedStreamResultExecutor.cs b/src/Tgstation.Server.Host/Controllers/Results/LimitedStreamResultExecutor.cs index 3178db355b..c3d3b7d372 100644 --- a/src/Tgstation.Server.Host/Controllers/Results/LimitedStreamResultExecutor.cs +++ b/src/Tgstation.Server.Host/Controllers/Results/LimitedStreamResultExecutor.cs @@ -53,7 +53,7 @@ namespace Tgstation.Server.Host.Controllers.Results } else { - stream.Seek(range.From.Value, SeekOrigin.Begin); + stream.Seek(range.From ?? 0, SeekOrigin.Begin); await StreamCopyOperation.CopyToAsync( stream, outputStream, diff --git a/src/Tgstation.Server.Host/Controllers/Results/PaginatableResult.cs b/src/Tgstation.Server.Host/Controllers/Results/PaginatableResult.cs index b936d0f3e6..5d4f80d023 100644 --- a/src/Tgstation.Server.Host/Controllers/Results/PaginatableResult.cs +++ b/src/Tgstation.Server.Host/Controllers/Results/PaginatableResult.cs @@ -1,4 +1,5 @@ using System; +using System.Diagnostics.CodeAnalysis; using System.Linq; using Microsoft.AspNetCore.Mvc; @@ -11,15 +12,22 @@ namespace Tgstation.Server.Host.Controllers.Results /// The of model intended to be returned. public sealed class PaginatableResult { + /// + /// Whether or not the is valid. + /// + [MemberNotNullWhen(true, nameof(Results))] + [MemberNotNullWhen(false, nameof(EarlyOut))] + public bool Valid => EarlyOut == null; + /// /// The results. /// - public IOrderedQueryable Results { get; } + public IOrderedQueryable? Results { get; } /// /// An to return immediately. /// - public IActionResult EarlyOut { get; } + public IActionResult? EarlyOut { get; } /// /// Initializes a new instance of the class. diff --git a/src/Tgstation.Server.Host/Controllers/RootController.cs b/src/Tgstation.Server.Host/Controllers/RootController.cs index 3ccca028fe..9ae6e7e942 100644 --- a/src/Tgstation.Server.Host/Controllers/RootController.cs +++ b/src/Tgstation.Server.Host/Controllers/RootController.cs @@ -104,7 +104,7 @@ namespace Tgstation.Server.Host.Controllers else return Redirect(ApiDocumentationRoute); - Dictionary links; + Dictionary? links; if (panelEnabled) links = new Dictionary() { @@ -134,7 +134,7 @@ namespace Tgstation.Server.Host.Controllers ? LogoSvgWindowsName : LogoSvgLinuxName; - return (IActionResult)this.TryServeFile(hostEnvironment, logger, $"{logoFileName}.svg") ?? NotFound(); + return (IActionResult?)this.TryServeFile(hostEnvironment, logger, $"{logoFileName}.svg") ?? NotFound(); } } } diff --git a/src/Tgstation.Server.Host/Controllers/SwarmController.cs b/src/Tgstation.Server.Host/Controllers/SwarmController.cs index 2512eda8dd..be171b19d8 100644 --- a/src/Tgstation.Server.Host/Controllers/SwarmController.cs +++ b/src/Tgstation.Server.Host/Controllers/SwarmController.cs @@ -32,7 +32,7 @@ namespace Tgstation.Server.Host.Controllers /// /// Get the current registration from the . /// - internal Guid RequestRegistrationId => Guid.Parse(Request.Headers[SwarmConstants.RegistrationIdHeader].First()); + internal Guid RequestRegistrationId => Guid.Parse(Request.Headers[SwarmConstants.RegistrationIdHeader].First()!); /// /// The for the . @@ -150,6 +150,9 @@ namespace Tgstation.Server.Host.Controllers { ArgumentNullException.ThrowIfNull(serversUpdateRequest); + if (serversUpdateRequest.SwarmServers == null) + return BadRequest(); + if (!ValidateRegistration()) return Forbid(); @@ -210,7 +213,7 @@ namespace Tgstation.Server.Host.Controllers } /// - protected override async ValueTask HookExecuteAction(Func executeAction, CancellationToken cancellationToken) + protected override async ValueTask HookExecuteAction(Func executeAction, CancellationToken cancellationToken) { using (LogContext.PushProperty(SerilogContextHelper.RequestPathContextProperty, $"{Request.Method} {Request.Path}")) { @@ -241,7 +244,7 @@ namespace Tgstation.Server.Host.Controllers if (ModelState?.IsValid == false) { var errorMessages = ModelState - .SelectMany(x => x.Value.Errors) + .SelectMany(x => x.Value!.Errors) .Select(x => x.ErrorMessage); logger.LogDebug( diff --git a/src/Tgstation.Server.Host/Controllers/UserController.cs b/src/Tgstation.Server.Host/Controllers/UserController.cs index 08c61e05fe..68bccfa169 100644 --- a/src/Tgstation.Server.Host/Controllers/UserController.cs +++ b/src/Tgstation.Server.Host/Controllers/UserController.cs @@ -103,7 +103,7 @@ namespace Tgstation.Server.Host.Controllers return BadRequest(new ErrorMessageResponse(ErrorCode.ModelValidationFailure)); if ((model.Password != null && model.SystemIdentifier != null) - || (model.Password == null && model.SystemIdentifier == null && model.OAuthConnections?.Any() != true)) + || (model.Password == null && model.SystemIdentifier == null && (model.OAuthConnections?.Count > 0) != true)) return BadRequest(new ErrorMessageResponse(ErrorCode.UserMismatchPasswordSid)); if (model.Group != null && model.PermissionSet != null) @@ -144,14 +144,14 @@ namespace Tgstation.Server.Host.Controllers { return RequiresPosixSystemIdentity(ex); } - else if (!(model.Password?.Length == 0 && model.OAuthConnections?.Any() == true)) + else if (!(model.Password?.Length == 0 && (model.OAuthConnections?.Count > 0) == true)) { - var result = TrySetPassword(dbUser, model.Password, true); + var result = TrySetPassword(dbUser, model.Password!, true); if (result != null) return result; } - dbUser.CanonicalName = Models.User.CanonicalizeName(dbUser.Name); + dbUser.CanonicalName = Models.User.CanonicalizeName(dbUser.Name!); DatabaseContext.Users.Add(dbUser); @@ -204,7 +204,7 @@ namespace Tgstation.Server.Host.Controllers .Where(x => x.Id == model.Id) .Include(x => x.CreatedBy) .Include(x => x.OAuthConnections) - .Include(x => x.Group) + .Include(x => x.Group!) .ThenInclude(x => x.PermissionSet) .Include(x => x.PermissionSet) .FirstOrDefaultAsync(cancellationToken); @@ -254,7 +254,7 @@ namespace Tgstation.Server.Host.Controllers bool userWasDisabled; if (model.Enabled.HasValue) { - userWasDisabled = originalUser.Enabled.Value && !model.Enabled.Value; + userWasDisabled = originalUser.Require(x => x.Enabled) && !model.Enabled.Value; if (userWasDisabled) originalUser.LastPasswordUpdate = DateTimeOffset.UtcNow; @@ -264,7 +264,7 @@ namespace Tgstation.Server.Host.Controllers userWasDisabled = false; if (model.OAuthConnections != null - && (model.OAuthConnections.Count != originalUser.OAuthConnections.Count + && (model.OAuthConnections.Count != originalUser.OAuthConnections!.Count || !model.OAuthConnections.All(x => originalUser.OAuthConnections.Any(y => y.Provider == x.Provider && y.ExternalUserId == x.ExternalUserId)))) { if (originalUser.CanonicalName == Models.User.CanonicalizeName(DefaultCredentials.AdminUserName)) @@ -372,7 +372,7 @@ namespace Tgstation.Server.Host.Controllers .Include(x => x.CreatedBy) .Include(x => x.PermissionSet) .Include(x => x.OAuthConnections) - .Include(x => x.Group) + .Include(x => x.Group!) .ThenInclude(x => x.PermissionSet) .OrderBy(x => x.Id))), null, @@ -405,7 +405,7 @@ namespace Tgstation.Server.Host.Controllers .Where(x => x.Id == id) .Include(x => x.CreatedBy) .Include(x => x.OAuthConnections) - .Include(x => x.Group) + .Include(x => x.Group!) .ThenInclude(x => x.PermissionSet) .Include(x => x.PermissionSet) .FirstOrDefaultAsync(cancellationToken); @@ -426,8 +426,8 @@ namespace Tgstation.Server.Host.Controllers /// A resulting in a new on success, if the requested did not exist. async ValueTask CreateNewUserFromModel(Api.Models.Internal.UserApiBase model, CancellationToken cancellationToken) { - Models.PermissionSet permissionSet = null; - UserGroup group = null; + Models.PermissionSet? permissionSet = null; + UserGroup? group = null; if (model.Group != null) group = await DatabaseContext .Groups @@ -469,7 +469,7 @@ namespace Tgstation.Server.Host.Controllers /// The to check. /// If this is a new . /// if is valid, a otherwise. - BadRequestObjectResult CheckValidName(UserUpdateRequest model, bool newUser) + BadRequestObjectResult? CheckValidName(UserUpdateRequest model, bool newUser) { var userInvalidWithNullName = newUser && model.Name == null && model.SystemIdentifier == null; if (userInvalidWithNullName || (model.Name != null && String.IsNullOrWhiteSpace(model.Name))) @@ -488,7 +488,7 @@ namespace Tgstation.Server.Host.Controllers /// The new password. /// If this is for a new . /// on success, if is too short. - BadRequestObjectResult TrySetPassword(User dbUser, string newPassword, bool newUser) + BadRequestObjectResult? TrySetPassword(User dbUser, string newPassword, bool newUser) { newPassword ??= String.Empty; if (newPassword.Length < generalConfiguration.MinimumPasswordLength) diff --git a/src/Tgstation.Server.Host/Controllers/UserGroupController.cs b/src/Tgstation.Server.Host/Controllers/UserGroupController.cs index 3f2c5ea3a2..b6f48b8197 100644 --- a/src/Tgstation.Server.Host/Controllers/UserGroupController.cs +++ b/src/Tgstation.Server.Host/Controllers/UserGroupController.cs @@ -131,7 +131,7 @@ namespace Tgstation.Server.Host.Controllers if (model.PermissionSet != null) { - currentGroup.PermissionSet.AdministrationRights = model.PermissionSet.AdministrationRights ?? currentGroup.PermissionSet.AdministrationRights; + currentGroup.PermissionSet!.AdministrationRights = model.PermissionSet.AdministrationRights ?? currentGroup.PermissionSet.AdministrationRights; currentGroup.PermissionSet.InstanceManagerRights = model.PermissionSet.InstanceManagerRights ?? currentGroup.PermissionSet.InstanceManagerRights; } @@ -139,7 +139,7 @@ namespace Tgstation.Server.Host.Controllers await DatabaseContext.Save(cancellationToken); - if (!AuthenticationContext.PermissionSet.AdministrationRights.Value.HasFlag(AdministrationRights.ReadUsers)) + if (!AuthenticationContext.PermissionSet.AdministrationRights!.Value.HasFlag(AdministrationRights.ReadUsers)) return Json(new UserGroupResponse { Id = currentGroup.Id, @@ -220,7 +220,7 @@ namespace Tgstation.Server.Host.Controllers var numDeleted = await DatabaseContext .Groups .AsQueryable() - .Where(x => x.Id == id && x.Users.Count == 0) + .Where(x => x.Id == id && x.Users!.Count == 0) .DeleteAsync(cancellationToken); if (numDeleted > 0) diff --git a/src/Tgstation.Server.Host/Core/Application.cs b/src/Tgstation.Server.Host/Core/Application.cs index 10f2728465..3b80d9da38 100644 --- a/src/Tgstation.Server.Host/Core/Application.cs +++ b/src/Tgstation.Server.Host/Core/Application.cs @@ -1,7 +1,7 @@ using System; +using System.Collections.Frozen; using System.Collections.Generic; using System.Globalization; -using System.Linq; using System.Threading.Tasks; using Cyberboss.AspNetCore.AsyncInitializer; @@ -79,7 +79,7 @@ namespace Tgstation.Server.Host.Core /// /// The for the . /// - ITokenFactory tokenFactory; + ITokenFactory? tokenFactory; /// /// Create the default . @@ -381,12 +381,13 @@ namespace Tgstation.Server.Host.Core openDreamRepositoryDirectory), new NoopEventConsumer())); - services.AddSingleton>( + services.AddSingleton( serviceProvider => new Dictionary { { EngineType.Byond, serviceProvider.GetRequiredService() }, { EngineType.OpenDream, serviceProvider.GetRequiredService() }, - }); + } + .ToFrozenDictionary()); services.AddSingleton(); if (postSetupServices.InternalConfiguration.UsingSystemD) @@ -537,7 +538,7 @@ namespace Tgstation.Server.Host.Core applicationBuilder.UseRouting(); // Set up CORS based on configuration if necessary - Action corsBuilder = null; + Action? corsBuilder = null; if (controlPanelConfiguration.AllowAnyOrigin) { logger.LogTrace("Access-Control-Allow-Origin: *"); @@ -546,7 +547,7 @@ namespace Tgstation.Server.Host.Core else if (controlPanelConfiguration.AllowedOrigins?.Count > 0) { logger.LogTrace("Access-Control-Allow-Origin: {allowedOrigins}", String.Join(',', controlPanelConfiguration.AllowedOrigins)); - corsBuilder = builder => builder.WithOrigins(controlPanelConfiguration.AllowedOrigins.ToArray()); + corsBuilder = builder => builder.WithOrigins([.. controlPanelConfiguration.AllowedOrigins]); } var originalBuilder = corsBuilder; @@ -621,9 +622,9 @@ namespace Tgstation.Server.Host.Core // return provider.GetRequiredService().CurrentAuthenticationContext // But M$ said // https://stackoverflow.com/questions/56792917/scoped-services-in-asp-net-core-with-signalr-hubs - services.AddScoped(provider => provider + services.AddScoped(provider => (provider .GetRequiredService() - .HttpContext + .HttpContext ?? throw new InvalidOperationException($"Unable to resolve {nameof(IAuthenticationContext)} due to no HttpContext being available!")) .RequestServices .GetRequiredService() .CurrentAuthenticationContext); diff --git a/src/Tgstation.Server.Host/Core/CommandPipeManager.cs b/src/Tgstation.Server.Host/Core/CommandPipeManager.cs index d86e8ed69c..6a5c6325c6 100644 --- a/src/Tgstation.Server.Host/Core/CommandPipeManager.cs +++ b/src/Tgstation.Server.Host/Core/CommandPipeManager.cs @@ -65,22 +65,24 @@ namespace Tgstation.Server.Host.Core logger.LogTrace("Starting..."); // grab both pipes asap so we can close them on error - var supportsPipeCommands = !String.IsNullOrWhiteSpace(internalConfiguration.CommandPipe); + var commandPipe = internalConfiguration.CommandPipe; + var supportsPipeCommands = !String.IsNullOrWhiteSpace(commandPipe); await using var commandPipeClient = supportsPipeCommands ? new AnonymousPipeClientStream( PipeDirection.In, - internalConfiguration.CommandPipe) + commandPipe!) : null; if (!supportsPipeCommands) logger.LogDebug("No command pipe name specified in configuration"); - var supportsReadyNotification = !String.IsNullOrWhiteSpace(internalConfiguration.ReadyPipe); + var readyPipe = internalConfiguration.ReadyPipe; + var supportsReadyNotification = !String.IsNullOrWhiteSpace(readyPipe); if (supportsReadyNotification) { await using var readyPipeClient = new AnonymousPipeClientStream( PipeDirection.Out, - internalConfiguration.ReadyPipe); + readyPipe!); logger.LogTrace("Waiting to send ready notification..."); await instanceManager.Ready.WaitAsync(cancellationToken); @@ -96,13 +98,13 @@ namespace Tgstation.Server.Host.Core try { - using var streamReader = new StreamReader(commandPipeClient, Encoding.UTF8, leaveOpen: true); + using var streamReader = new StreamReader(commandPipeClient!, Encoding.UTF8, leaveOpen: true); while (!cancellationToken.IsCancellationRequested) { logger.LogTrace("Waiting to read command line..."); var line = await streamReader.ReadLineAsync(cancellationToken); - logger?.LogInformation("Received pipe command: {command}", line); + logger.LogInformation("Received pipe command: {command}", line); switch (line) { case PipeCommands.CommandStop: @@ -118,22 +120,22 @@ namespace Tgstation.Server.Host.Core logger.LogError("Read null from pipe!"); return; default: - logger?.LogWarning("Unrecognized pipe command: {command}", line); + logger.LogWarning("Unrecognized pipe command: {command}", line); break; } } } catch (OperationCanceledException ex) { - logger?.LogTrace(ex, "Command read task cancelled!"); + logger.LogTrace(ex, "Command read task cancelled!"); } catch (Exception ex) { - logger?.LogError(ex, "Command read task errored!"); + logger.LogError(ex, "Command read task errored!"); } finally { - logger?.LogTrace("Command read task exiting..."); + logger.LogTrace("Command read task exiting..."); } } } diff --git a/src/Tgstation.Server.Host/Core/IRestartHandler.cs b/src/Tgstation.Server.Host/Core/IRestartHandler.cs index e1c8a771ef..b663ae5d22 100644 --- a/src/Tgstation.Server.Host/Core/IRestartHandler.cs +++ b/src/Tgstation.Server.Host/Core/IRestartHandler.cs @@ -16,6 +16,6 @@ namespace Tgstation.Server.Host.Core /// If the should aim to complete the returned from this function ASAP. /// The for the operation. /// A representing the running operation. - ValueTask HandleRestart(Version updateVersion, bool handlerMayDelayShutdownWithExtremelyLongRunningTasks, CancellationToken cancellationToken); + ValueTask HandleRestart(Version? updateVersion, bool handlerMayDelayShutdownWithExtremelyLongRunningTasks, CancellationToken cancellationToken); } } diff --git a/src/Tgstation.Server.Host/Core/IServerControl.cs b/src/Tgstation.Server.Host/Core/IServerControl.cs index d864080db6..92bbfe75f5 100644 --- a/src/Tgstation.Server.Host/Core/IServerControl.cs +++ b/src/Tgstation.Server.Host/Core/IServerControl.cs @@ -51,6 +51,6 @@ namespace Tgstation.Server.Host.Core /// /// The to propagate to the watchdog if any. /// A representing the running operation. - ValueTask Die(Exception exception); + ValueTask Die(Exception? exception); } } diff --git a/src/Tgstation.Server.Host/Core/IServerUpdateInitiator.cs b/src/Tgstation.Server.Host/Core/IServerUpdateInitiator.cs index 8b290bdcd2..35aaa4037b 100644 --- a/src/Tgstation.Server.Host/Core/IServerUpdateInitiator.cs +++ b/src/Tgstation.Server.Host/Core/IServerUpdateInitiator.cs @@ -18,6 +18,6 @@ namespace Tgstation.Server.Host.Core /// The TGS to update to. /// The for the operation. /// A resulting in the . - ValueTask InitiateUpdate(IFileStreamProvider fileStreamProvider, Version version, CancellationToken cancellationToken); + ValueTask InitiateUpdate(IFileStreamProvider? fileStreamProvider, Version version, CancellationToken cancellationToken); } } diff --git a/src/Tgstation.Server.Host/Core/IServerUpdater.cs b/src/Tgstation.Server.Host/Core/IServerUpdater.cs index 6485ed4713..4a7a7c0519 100644 --- a/src/Tgstation.Server.Host/Core/IServerUpdater.cs +++ b/src/Tgstation.Server.Host/Core/IServerUpdater.cs @@ -20,6 +20,6 @@ namespace Tgstation.Server.Host.Core /// The TGS to update to. /// The for the operation. /// A resulting in the . - ValueTask BeginUpdate(ISwarmService swarmService, IFileStreamProvider fileStreamProvider, Version version, CancellationToken cancellationToken); + ValueTask BeginUpdate(ISwarmService swarmService, IFileStreamProvider? fileStreamProvider, Version version, CancellationToken cancellationToken); } } diff --git a/src/Tgstation.Server.Host/Core/RestartRegistration.cs b/src/Tgstation.Server.Host/Core/RestartRegistration.cs index 025c274e07..2cd24e307d 100644 --- a/src/Tgstation.Server.Host/Core/RestartRegistration.cs +++ b/src/Tgstation.Server.Host/Core/RestartRegistration.cs @@ -1,4 +1,4 @@ -using System; +using Tgstation.Server.Host.Utils; namespace Tgstation.Server.Host.Core { @@ -6,20 +6,20 @@ namespace Tgstation.Server.Host.Core sealed class RestartRegistration : IRestartRegistration { /// - /// The . + /// The . /// - readonly Action onDispose; + readonly DisposeInvoker? disposeInvoker; /// /// Initializes a new instance of the class. /// - /// The value of . - public RestartRegistration(Action onDispose) + /// The value of . + public RestartRegistration(DisposeInvoker? disposeInvoker) { - this.onDispose = onDispose; + this.disposeInvoker = disposeInvoker; } /// - public void Dispose() => onDispose?.Invoke(); + public void Dispose() => disposeInvoker?.Dispose(); } } diff --git a/src/Tgstation.Server.Host/Core/ServerPortProivder.cs b/src/Tgstation.Server.Host/Core/ServerPortProivder.cs index e4c4927c53..a3f39a4ab8 100644 --- a/src/Tgstation.Server.Host/Core/ServerPortProivder.cs +++ b/src/Tgstation.Server.Host/Core/ServerPortProivder.cs @@ -34,18 +34,17 @@ namespace Tgstation.Server.Host.Core generalConfiguration = generalConfigurationOptions?.Value ?? throw new ArgumentNullException(nameof(generalConfigurationOptions)); ArgumentNullException.ThrowIfNull(configuration); + var usingDefaultPort = generalConfiguration.ApiPort == default; + if (!usingDefaultPort) + return; + var httpEndpoint = configuration .GetSection("Kestrel") .GetSection("EndPoints") .GetSection("Http") .GetSection("Url") - .Value; - - if (generalConfiguration.ApiPort == default && httpEndpoint == null) - throw new InvalidOperationException("Missing required configuration option General:ApiPort!"); - - if (generalConfiguration.ApiPort != default) - return; + .Value + ?? throw new InvalidOperationException("Missing required configuration option General:ApiPort!"); logger.LogWarning("The \"Kestrel\" configuration section is deprecated! Please set your API port using the \"General:ApiPort\" configuration option!"); diff --git a/src/Tgstation.Server.Host/Core/ServerUpdateInitiator.cs b/src/Tgstation.Server.Host/Core/ServerUpdateInitiator.cs index f7fad73122..fc1dc084cc 100644 --- a/src/Tgstation.Server.Host/Core/ServerUpdateInitiator.cs +++ b/src/Tgstation.Server.Host/Core/ServerUpdateInitiator.cs @@ -32,7 +32,7 @@ namespace Tgstation.Server.Host.Core } /// - public ValueTask InitiateUpdate(IFileStreamProvider fileStreamProvider, Version version, CancellationToken cancellationToken) + public ValueTask InitiateUpdate(IFileStreamProvider? fileStreamProvider, Version version, CancellationToken cancellationToken) => serverUpdater.BeginUpdate(swarmService, fileStreamProvider, version, cancellationToken); } } diff --git a/src/Tgstation.Server.Host/Core/ServerUpdater.cs b/src/Tgstation.Server.Host/Core/ServerUpdater.cs index aacf44fd19..8c9d968168 100644 --- a/src/Tgstation.Server.Host/Core/ServerUpdater.cs +++ b/src/Tgstation.Server.Host/Core/ServerUpdater.cs @@ -59,7 +59,7 @@ namespace Tgstation.Server.Host.Core /// /// for an in-progress update operation. /// - ServerUpdateOperation serverUpdateOperation; + ServerUpdateOperation? serverUpdateOperation; /// /// Initializes a new instance of the class. @@ -92,7 +92,7 @@ namespace Tgstation.Server.Host.Core } /// - public async ValueTask BeginUpdate(ISwarmService swarmService, IFileStreamProvider fileStreamProvider, Version version, CancellationToken cancellationToken) + public async ValueTask BeginUpdate(ISwarmService swarmService, IFileStreamProvider? fileStreamProvider, Version version, CancellationToken cancellationToken) { ArgumentNullException.ThrowIfNull(swarmService); @@ -201,7 +201,7 @@ namespace Tgstation.Server.Host.Core { try { - await serverUpdateOperation.SwarmService.AbortUpdate(); + await serverUpdateOperation!.SwarmService.AbortUpdate(); } catch (Exception e2) { @@ -214,11 +214,11 @@ namespace Tgstation.Server.Host.Core /// /// The directory the server update is initially extracted to. /// The for the operation. - /// A resulting in containing a new based on the of and if it needs to be kept active until the swarm commit. + /// A resulting in containing a new based on the of and if it needs to be kept active until the swarm commit. If , the update failed to prepare. /// Requires to be populated. - async ValueTask> PrepareUpdateClearStagingAndBufferStream(string stagingDirectory, CancellationToken cancellationToken) + async ValueTask?> PrepareUpdateClearStagingAndBufferStream(string stagingDirectory, CancellationToken cancellationToken) { - await using var fileStreamProvider = serverUpdateOperation.FileStreamProvider; + await using var fileStreamProvider = serverUpdateOperation!.FileStreamProvider; var bufferedStream = new BufferedFileStreamProvider( await fileStreamProvider.GetResult(cancellationToken)); @@ -276,12 +276,12 @@ namespace Tgstation.Server.Host.Core /// A resulting in the . async ValueTask BeginUpdateImpl( ISwarmService swarmService, - IFileStreamProvider fileStreamProvider, + IFileStreamProvider? fileStreamProvider, Version newVersion, bool recursed, CancellationToken cancellationToken) { - ServerUpdateOperation ourUpdateOperation = null; + ServerUpdateOperation? ourUpdateOperation = null; try { if (fileStreamProvider == null) diff --git a/src/Tgstation.Server.Host/Database/DatabaseContext.cs b/src/Tgstation.Server.Host/Database/DatabaseContext.cs index 3dd14560f6..978a73076b 100644 --- a/src/Tgstation.Server.Host/Database/DatabaseContext.cs +++ b/src/Tgstation.Server.Host/Database/DatabaseContext.cs @@ -257,21 +257,21 @@ namespace Tgstation.Server.Host.Database protected DatabaseContext(DbContextOptions dbContextOptions) : base(dbContextOptions) { - usersCollection = new DatabaseCollection(Users); - instancesCollection = new DatabaseCollection(Instances); - instancePermissionSetsCollection = new DatabaseCollection(InstancePermissionSets); - compileJobsCollection = new DatabaseCollection(CompileJobs); - repositorySettingsCollection = new DatabaseCollection(RepositorySettings); - dreamMakerSettingsCollection = new DatabaseCollection(DreamMakerSettings); - dreamDaemonSettingsCollection = new DatabaseCollection(DreamDaemonSettings); - chatBotsCollection = new DatabaseCollection(ChatBots); - chatChannelsCollection = new DatabaseCollection(ChatChannels); - revisionInformationsCollection = new DatabaseCollection(RevisionInformations); - jobsCollection = new DatabaseCollection(Jobs); - reattachInformationsCollection = new DatabaseCollection(ReattachInformations); - oAuthConnections = new DatabaseCollection(OAuthConnections); - groups = new DatabaseCollection(Groups); - permissionSets = new DatabaseCollection(PermissionSets); + usersCollection = new DatabaseCollection(Users!); + instancesCollection = new DatabaseCollection(Instances!); + instancePermissionSetsCollection = new DatabaseCollection(InstancePermissionSets!); + compileJobsCollection = new DatabaseCollection(CompileJobs!); + repositorySettingsCollection = new DatabaseCollection(RepositorySettings!); + dreamMakerSettingsCollection = new DatabaseCollection(DreamMakerSettings!); + dreamDaemonSettingsCollection = new DatabaseCollection(DreamDaemonSettings!); + chatBotsCollection = new DatabaseCollection(ChatBots!); + chatChannelsCollection = new DatabaseCollection(ChatChannels!); + revisionInformationsCollection = new DatabaseCollection(RevisionInformations!); + jobsCollection = new DatabaseCollection(Jobs!); + reattachInformationsCollection = new DatabaseCollection(ReattachInformations!); + oAuthConnections = new DatabaseCollection(OAuthConnections!); + groups = new DatabaseCollection(Groups!); + permissionSets = new DatabaseCollection(PermissionSets!); } /// @@ -375,22 +375,22 @@ namespace Tgstation.Server.Host.Database /// /// Used by unit tests to remind us to setup the correct MSSQL migration downgrades. /// - internal static readonly Type MSLatestMigration = typeof(MSRenameByondColumnsToEngine); + internal static readonly Type MSLatestMigration = typeof(MSAddTopicPort); /// /// Used by unit tests to remind us to setup the correct MYSQL migration downgrades. /// - internal static readonly Type MYLatestMigration = typeof(MYRenameByondColumnsToEngine); + internal static readonly Type MYLatestMigration = typeof(MYAddTopicPort); /// /// Used by unit tests to remind us to setup the correct PostgresSQL migration downgrades. /// - internal static readonly Type PGLatestMigration = typeof(PGRenameByondColumnsToEngine); + internal static readonly Type PGLatestMigration = typeof(PGAddTopicPort); /// /// Used by unit tests to remind us to setup the correct SQLite migration downgrades. /// - internal static readonly Type SLLatestMigration = typeof(SLRenameByondColumnsToEngine); + internal static readonly Type SLLatestMigration = typeof(SLAddTopicPort); /// #pragma warning disable CA1502 // Cyclomatic complexity @@ -415,7 +415,7 @@ namespace Tgstation.Server.Host.Database throw new NotSupportedException("Cannot migrate below version 4.1.0!"); // Update this with new migrations as they are made - string targetMigration = null; + string? targetMigration = null; string BadDatabaseType() => throw new ArgumentException($"Invalid DatabaseType: {currentDatabaseType}", nameof(currentDatabaseType)); diff --git a/src/Tgstation.Server.Host/Database/DatabaseSeeder.cs b/src/Tgstation.Server.Host/Database/DatabaseSeeder.cs index 19efc59b47..c88c7a5e0d 100644 --- a/src/Tgstation.Server.Host/Database/DatabaseSeeder.cs +++ b/src/Tgstation.Server.Host/Database/DatabaseSeeder.cs @@ -57,7 +57,7 @@ namespace Tgstation.Server.Host.Database /// The to add a system to. /// An existing , if any. /// The created system . - static User SeedSystemUser(IDatabaseContext databaseContext, User tgsUser = null) + static User SeedSystemUser(IDatabaseContext databaseContext, User? tgsUser = null) { bool alreadyExists = tgsUser != null; tgsUser ??= new User() @@ -231,7 +231,7 @@ namespace Tgstation.Server.Host.Database .AsQueryable() .ToListAsync(cancellationToken); foreach (var instance in allInstances) - instance.Path = instance.Path.Replace('\\', '/'); + instance.Path = instance.Path!.Replace('\\', '/'); } if (generalConfiguration.ByondTopicTimeout != 0) @@ -303,7 +303,7 @@ namespace Tgstation.Server.Host.Database /// The to use. /// The for the operation. /// A resulting in the admin or . If , must be called on . - async ValueTask GetAdminUser(IDatabaseContext databaseContext, CancellationToken cancellationToken) + async ValueTask GetAdminUser(IDatabaseContext databaseContext, CancellationToken cancellationToken) { var admin = await databaseContext .Users diff --git a/src/Tgstation.Server.Host/Database/Design/DesignTimeDbContextFactoryHelpers.cs b/src/Tgstation.Server.Host/Database/Design/DesignTimeDbContextFactoryHelpers.cs index 54d79bddc3..126dfbef4a 100644 --- a/src/Tgstation.Server.Host/Database/Design/DesignTimeDbContextFactoryHelpers.cs +++ b/src/Tgstation.Server.Host/Database/Design/DesignTimeDbContextFactoryHelpers.cs @@ -21,7 +21,7 @@ namespace Tgstation.Server.Host.Database.Design public static DbContextOptions CreateDatabaseContextOptions( DatabaseType databaseType, string connectionString, - string serverVersion = null) + string? serverVersion = null) where TDatabaseContext : DatabaseContext { var dbConfig = new DatabaseConfiguration diff --git a/src/Tgstation.Server.Host/Database/Migrations/20230331220749_MSAddDreamDaemonLogOutput.Designer.cs b/src/Tgstation.Server.Host/Database/Migrations/20230331220749_MSAddDreamDaemonLogOutput.Designer.cs index 02be0acad1..8bc0ff72d4 100644 --- a/src/Tgstation.Server.Host/Database/Migrations/20230331220749_MSAddDreamDaemonLogOutput.Designer.cs +++ b/src/Tgstation.Server.Host/Database/Migrations/20230331220749_MSAddDreamDaemonLogOutput.Designer.cs @@ -5,8 +5,6 @@ using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore.Infrastructure; using Microsoft.EntityFrameworkCore.Migrations; -#nullable disable - namespace Tgstation.Server.Host.Database.Migrations { [DbContext(typeof(SqlServerDatabaseContext))] diff --git a/src/Tgstation.Server.Host/Database/Migrations/20230331220749_MSAddDreamDaemonLogOutput.cs b/src/Tgstation.Server.Host/Database/Migrations/20230331220749_MSAddDreamDaemonLogOutput.cs index bc8e3d1880..b2d86c3ae7 100644 --- a/src/Tgstation.Server.Host/Database/Migrations/20230331220749_MSAddDreamDaemonLogOutput.cs +++ b/src/Tgstation.Server.Host/Database/Migrations/20230331220749_MSAddDreamDaemonLogOutput.cs @@ -2,8 +2,6 @@ using Microsoft.EntityFrameworkCore.Migrations; -#nullable disable - namespace Tgstation.Server.Host.Database.Migrations { /// diff --git a/src/Tgstation.Server.Host/Database/Migrations/20230331221032_PGAddDreamDaemonLogOutput.Designer.cs b/src/Tgstation.Server.Host/Database/Migrations/20230331221032_PGAddDreamDaemonLogOutput.Designer.cs index c1bb3e23c5..adca6dbfa7 100644 --- a/src/Tgstation.Server.Host/Database/Migrations/20230331221032_PGAddDreamDaemonLogOutput.Designer.cs +++ b/src/Tgstation.Server.Host/Database/Migrations/20230331221032_PGAddDreamDaemonLogOutput.Designer.cs @@ -5,8 +5,6 @@ using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore.Infrastructure; using Microsoft.EntityFrameworkCore.Migrations; -#nullable disable - namespace Tgstation.Server.Host.Database.Migrations { [DbContext(typeof(PostgresSqlDatabaseContext))] diff --git a/src/Tgstation.Server.Host/Database/Migrations/20230331221032_PGAddDreamDaemonLogOutput.cs b/src/Tgstation.Server.Host/Database/Migrations/20230331221032_PGAddDreamDaemonLogOutput.cs index 1828f867bb..cded945a22 100644 --- a/src/Tgstation.Server.Host/Database/Migrations/20230331221032_PGAddDreamDaemonLogOutput.cs +++ b/src/Tgstation.Server.Host/Database/Migrations/20230331221032_PGAddDreamDaemonLogOutput.cs @@ -2,8 +2,6 @@ using Microsoft.EntityFrameworkCore.Migrations; -#nullable disable - namespace Tgstation.Server.Host.Database.Migrations { /// diff --git a/src/Tgstation.Server.Host/Database/Migrations/20230331221156_SLAddDreamDaemonLogOutput.Designer.cs b/src/Tgstation.Server.Host/Database/Migrations/20230331221156_SLAddDreamDaemonLogOutput.Designer.cs index 1e7c30621f..2610389f7e 100644 --- a/src/Tgstation.Server.Host/Database/Migrations/20230331221156_SLAddDreamDaemonLogOutput.Designer.cs +++ b/src/Tgstation.Server.Host/Database/Migrations/20230331221156_SLAddDreamDaemonLogOutput.Designer.cs @@ -5,8 +5,6 @@ using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore.Infrastructure; using Microsoft.EntityFrameworkCore.Migrations; -#nullable disable - namespace Tgstation.Server.Host.Database.Migrations { [DbContext(typeof(SqliteDatabaseContext))] diff --git a/src/Tgstation.Server.Host/Database/Migrations/20230331221156_SLAddDreamDaemonLogOutput.cs b/src/Tgstation.Server.Host/Database/Migrations/20230331221156_SLAddDreamDaemonLogOutput.cs index 587dbf9aa2..f574ac0195 100644 --- a/src/Tgstation.Server.Host/Database/Migrations/20230331221156_SLAddDreamDaemonLogOutput.cs +++ b/src/Tgstation.Server.Host/Database/Migrations/20230331221156_SLAddDreamDaemonLogOutput.cs @@ -2,8 +2,6 @@ using Microsoft.EntityFrameworkCore.Migrations; -#nullable disable - namespace Tgstation.Server.Host.Database.Migrations { /// diff --git a/src/Tgstation.Server.Host/Database/Migrations/20230401210715_MYAddDreamDaemonLogOutput.Designer.cs b/src/Tgstation.Server.Host/Database/Migrations/20230401210715_MYAddDreamDaemonLogOutput.Designer.cs index 69acf576b1..aa0b1d70c1 100644 --- a/src/Tgstation.Server.Host/Database/Migrations/20230401210715_MYAddDreamDaemonLogOutput.Designer.cs +++ b/src/Tgstation.Server.Host/Database/Migrations/20230401210715_MYAddDreamDaemonLogOutput.Designer.cs @@ -5,8 +5,6 @@ using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore.Infrastructure; using Microsoft.EntityFrameworkCore.Migrations; -#nullable disable - namespace Tgstation.Server.Host.Database.Migrations { [DbContext(typeof(MySqlDatabaseContext))] diff --git a/src/Tgstation.Server.Host/Database/Migrations/20230401210715_MYAddDreamDaemonLogOutput.cs b/src/Tgstation.Server.Host/Database/Migrations/20230401210715_MYAddDreamDaemonLogOutput.cs index 2b023f5fa2..18fc203d23 100644 --- a/src/Tgstation.Server.Host/Database/Migrations/20230401210715_MYAddDreamDaemonLogOutput.cs +++ b/src/Tgstation.Server.Host/Database/Migrations/20230401210715_MYAddDreamDaemonLogOutput.cs @@ -2,8 +2,6 @@ using Microsoft.EntityFrameworkCore.Migrations; -#nullable disable - namespace Tgstation.Server.Host.Database.Migrations { /// diff --git a/src/Tgstation.Server.Host/Database/Migrations/20230403050623_MSAddReattachInfoInitialCompileJob.Designer.cs b/src/Tgstation.Server.Host/Database/Migrations/20230403050623_MSAddReattachInfoInitialCompileJob.Designer.cs index f749992580..d1c93465d6 100644 --- a/src/Tgstation.Server.Host/Database/Migrations/20230403050623_MSAddReattachInfoInitialCompileJob.Designer.cs +++ b/src/Tgstation.Server.Host/Database/Migrations/20230403050623_MSAddReattachInfoInitialCompileJob.Designer.cs @@ -5,8 +5,6 @@ using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore.Infrastructure; using Microsoft.EntityFrameworkCore.Migrations; -#nullable disable - namespace Tgstation.Server.Host.Database.Migrations { [DbContext(typeof(SqlServerDatabaseContext))] diff --git a/src/Tgstation.Server.Host/Database/Migrations/20230403050623_MSAddReattachInfoInitialCompileJob.cs b/src/Tgstation.Server.Host/Database/Migrations/20230403050623_MSAddReattachInfoInitialCompileJob.cs index 2c378335d7..2fa0030143 100644 --- a/src/Tgstation.Server.Host/Database/Migrations/20230403050623_MSAddReattachInfoInitialCompileJob.cs +++ b/src/Tgstation.Server.Host/Database/Migrations/20230403050623_MSAddReattachInfoInitialCompileJob.cs @@ -2,8 +2,6 @@ using Microsoft.EntityFrameworkCore.Migrations; -#nullable disable - namespace Tgstation.Server.Host.Database.Migrations { /// diff --git a/src/Tgstation.Server.Host/Database/Migrations/20230403050737_MYAddReattachInfoInitialCompileJob.Designer.cs b/src/Tgstation.Server.Host/Database/Migrations/20230403050737_MYAddReattachInfoInitialCompileJob.Designer.cs index a53708cfc0..e5a7f0400b 100644 --- a/src/Tgstation.Server.Host/Database/Migrations/20230403050737_MYAddReattachInfoInitialCompileJob.Designer.cs +++ b/src/Tgstation.Server.Host/Database/Migrations/20230403050737_MYAddReattachInfoInitialCompileJob.Designer.cs @@ -5,8 +5,6 @@ using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore.Infrastructure; using Microsoft.EntityFrameworkCore.Migrations; -#nullable disable - namespace Tgstation.Server.Host.Database.Migrations { [DbContext(typeof(MySqlDatabaseContext))] diff --git a/src/Tgstation.Server.Host/Database/Migrations/20230403050737_MYAddReattachInfoInitialCompileJob.cs b/src/Tgstation.Server.Host/Database/Migrations/20230403050737_MYAddReattachInfoInitialCompileJob.cs index da962107e1..3dc01170b7 100644 --- a/src/Tgstation.Server.Host/Database/Migrations/20230403050737_MYAddReattachInfoInitialCompileJob.cs +++ b/src/Tgstation.Server.Host/Database/Migrations/20230403050737_MYAddReattachInfoInitialCompileJob.cs @@ -2,8 +2,6 @@ using Microsoft.EntityFrameworkCore.Migrations; -#nullable disable - namespace Tgstation.Server.Host.Database.Migrations { /// diff --git a/src/Tgstation.Server.Host/Database/Migrations/20230403050832_PGAddReattachInfoInitialCompileJob.Designer.cs b/src/Tgstation.Server.Host/Database/Migrations/20230403050832_PGAddReattachInfoInitialCompileJob.Designer.cs index 8e4962a1e0..3129b863f9 100644 --- a/src/Tgstation.Server.Host/Database/Migrations/20230403050832_PGAddReattachInfoInitialCompileJob.Designer.cs +++ b/src/Tgstation.Server.Host/Database/Migrations/20230403050832_PGAddReattachInfoInitialCompileJob.Designer.cs @@ -5,8 +5,6 @@ using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore.Infrastructure; using Microsoft.EntityFrameworkCore.Migrations; -#nullable disable - namespace Tgstation.Server.Host.Database.Migrations { [DbContext(typeof(PostgresSqlDatabaseContext))] diff --git a/src/Tgstation.Server.Host/Database/Migrations/20230403050832_PGAddReattachInfoInitialCompileJob.cs b/src/Tgstation.Server.Host/Database/Migrations/20230403050832_PGAddReattachInfoInitialCompileJob.cs index 14d08913ec..bab6bbc9c1 100644 --- a/src/Tgstation.Server.Host/Database/Migrations/20230403050832_PGAddReattachInfoInitialCompileJob.cs +++ b/src/Tgstation.Server.Host/Database/Migrations/20230403050832_PGAddReattachInfoInitialCompileJob.cs @@ -2,8 +2,6 @@ using Microsoft.EntityFrameworkCore.Migrations; -#nullable disable - namespace Tgstation.Server.Host.Database.Migrations { /// diff --git a/src/Tgstation.Server.Host/Database/Migrations/20230403050941_SLAddReattachInfoInitialCompileJob.Designer.cs b/src/Tgstation.Server.Host/Database/Migrations/20230403050941_SLAddReattachInfoInitialCompileJob.Designer.cs index 8330e52cb1..3ac8984309 100644 --- a/src/Tgstation.Server.Host/Database/Migrations/20230403050941_SLAddReattachInfoInitialCompileJob.Designer.cs +++ b/src/Tgstation.Server.Host/Database/Migrations/20230403050941_SLAddReattachInfoInitialCompileJob.Designer.cs @@ -5,8 +5,6 @@ using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore.Infrastructure; using Microsoft.EntityFrameworkCore.Migrations; -#nullable disable - namespace Tgstation.Server.Host.Database.Migrations { [DbContext(typeof(SqliteDatabaseContext))] diff --git a/src/Tgstation.Server.Host/Database/Migrations/20230403050941_SLAddReattachInfoInitialCompileJob.cs b/src/Tgstation.Server.Host/Database/Migrations/20230403050941_SLAddReattachInfoInitialCompileJob.cs index 630411174d..91f1ea6bb8 100644 --- a/src/Tgstation.Server.Host/Database/Migrations/20230403050941_SLAddReattachInfoInitialCompileJob.cs +++ b/src/Tgstation.Server.Host/Database/Migrations/20230403050941_SLAddReattachInfoInitialCompileJob.cs @@ -2,8 +2,6 @@ using Microsoft.EntityFrameworkCore.Migrations; -#nullable disable - namespace Tgstation.Server.Host.Database.Migrations { /// diff --git a/src/Tgstation.Server.Host/Database/Migrations/20230520203236_MSAddSystemChannels.Designer.cs b/src/Tgstation.Server.Host/Database/Migrations/20230520203236_MSAddSystemChannels.Designer.cs index 4fa9364f35..23b628d9c8 100644 --- a/src/Tgstation.Server.Host/Database/Migrations/20230520203236_MSAddSystemChannels.Designer.cs +++ b/src/Tgstation.Server.Host/Database/Migrations/20230520203236_MSAddSystemChannels.Designer.cs @@ -5,8 +5,6 @@ using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore.Infrastructure; using Microsoft.EntityFrameworkCore.Migrations; -#nullable disable - namespace Tgstation.Server.Host.Database.Migrations { [DbContext(typeof(SqlServerDatabaseContext))] diff --git a/src/Tgstation.Server.Host/Database/Migrations/20230520203236_MSAddSystemChannels.cs b/src/Tgstation.Server.Host/Database/Migrations/20230520203236_MSAddSystemChannels.cs index 410d150c1e..3ab2299fe5 100644 --- a/src/Tgstation.Server.Host/Database/Migrations/20230520203236_MSAddSystemChannels.cs +++ b/src/Tgstation.Server.Host/Database/Migrations/20230520203236_MSAddSystemChannels.cs @@ -2,8 +2,6 @@ using Microsoft.EntityFrameworkCore.Migrations; -#nullable disable - namespace Tgstation.Server.Host.Database.Migrations { /// diff --git a/src/Tgstation.Server.Host/Database/Migrations/20230520203305_MYAddSystemChannels.Designer.cs b/src/Tgstation.Server.Host/Database/Migrations/20230520203305_MYAddSystemChannels.Designer.cs index 3f1e7b3249..2a7dbd32d8 100644 --- a/src/Tgstation.Server.Host/Database/Migrations/20230520203305_MYAddSystemChannels.Designer.cs +++ b/src/Tgstation.Server.Host/Database/Migrations/20230520203305_MYAddSystemChannels.Designer.cs @@ -5,8 +5,6 @@ using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore.Infrastructure; using Microsoft.EntityFrameworkCore.Migrations; -#nullable disable - namespace Tgstation.Server.Host.Database.Migrations { [DbContext(typeof(MySqlDatabaseContext))] diff --git a/src/Tgstation.Server.Host/Database/Migrations/20230520203305_MYAddSystemChannels.cs b/src/Tgstation.Server.Host/Database/Migrations/20230520203305_MYAddSystemChannels.cs index ec249f3e44..691e439fdc 100644 --- a/src/Tgstation.Server.Host/Database/Migrations/20230520203305_MYAddSystemChannels.cs +++ b/src/Tgstation.Server.Host/Database/Migrations/20230520203305_MYAddSystemChannels.cs @@ -2,8 +2,6 @@ using Microsoft.EntityFrameworkCore.Migrations; -#nullable disable - namespace Tgstation.Server.Host.Database.Migrations { /// diff --git a/src/Tgstation.Server.Host/Database/Migrations/20230520203332_PGAddSystemChannels.Designer.cs b/src/Tgstation.Server.Host/Database/Migrations/20230520203332_PGAddSystemChannels.Designer.cs index 613cd8034e..cecc00d99f 100644 --- a/src/Tgstation.Server.Host/Database/Migrations/20230520203332_PGAddSystemChannels.Designer.cs +++ b/src/Tgstation.Server.Host/Database/Migrations/20230520203332_PGAddSystemChannels.Designer.cs @@ -5,8 +5,6 @@ using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore.Infrastructure; using Microsoft.EntityFrameworkCore.Migrations; -#nullable disable - namespace Tgstation.Server.Host.Database.Migrations { [DbContext(typeof(PostgresSqlDatabaseContext))] diff --git a/src/Tgstation.Server.Host/Database/Migrations/20230520203332_PGAddSystemChannels.cs b/src/Tgstation.Server.Host/Database/Migrations/20230520203332_PGAddSystemChannels.cs index 60bf1c5229..17df0a48ef 100644 --- a/src/Tgstation.Server.Host/Database/Migrations/20230520203332_PGAddSystemChannels.cs +++ b/src/Tgstation.Server.Host/Database/Migrations/20230520203332_PGAddSystemChannels.cs @@ -2,8 +2,6 @@ using Microsoft.EntityFrameworkCore.Migrations; -#nullable disable - namespace Tgstation.Server.Host.Database.Migrations { /// diff --git a/src/Tgstation.Server.Host/Database/Migrations/20230520203402_SLAddSystemChannels.Designer.cs b/src/Tgstation.Server.Host/Database/Migrations/20230520203402_SLAddSystemChannels.Designer.cs index fbe9ae470e..2fbf158718 100644 --- a/src/Tgstation.Server.Host/Database/Migrations/20230520203402_SLAddSystemChannels.Designer.cs +++ b/src/Tgstation.Server.Host/Database/Migrations/20230520203402_SLAddSystemChannels.Designer.cs @@ -5,8 +5,6 @@ using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore.Infrastructure; using Microsoft.EntityFrameworkCore.Migrations; -#nullable disable - namespace Tgstation.Server.Host.Database.Migrations { [DbContext(typeof(SqliteDatabaseContext))] diff --git a/src/Tgstation.Server.Host/Database/Migrations/20230520203402_SLAddSystemChannels.cs b/src/Tgstation.Server.Host/Database/Migrations/20230520203402_SLAddSystemChannels.cs index 4dc47740e8..95dc26a1b8 100644 --- a/src/Tgstation.Server.Host/Database/Migrations/20230520203402_SLAddSystemChannels.cs +++ b/src/Tgstation.Server.Host/Database/Migrations/20230520203402_SLAddSystemChannels.cs @@ -2,8 +2,6 @@ using Microsoft.EntityFrameworkCore.Migrations; -#nullable disable - namespace Tgstation.Server.Host.Database.Migrations { /// diff --git a/src/Tgstation.Server.Host/Database/Migrations/20230614053739_MSRenameHeartbeatsToHealthChecks.Designer.cs b/src/Tgstation.Server.Host/Database/Migrations/20230614053739_MSRenameHeartbeatsToHealthChecks.Designer.cs index 0b7c9281fb..af40476708 100644 --- a/src/Tgstation.Server.Host/Database/Migrations/20230614053739_MSRenameHeartbeatsToHealthChecks.Designer.cs +++ b/src/Tgstation.Server.Host/Database/Migrations/20230614053739_MSRenameHeartbeatsToHealthChecks.Designer.cs @@ -5,8 +5,6 @@ using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore.Infrastructure; using Microsoft.EntityFrameworkCore.Migrations; -#nullable disable - namespace Tgstation.Server.Host.Database.Migrations { [DbContext(typeof(SqlServerDatabaseContext))] diff --git a/src/Tgstation.Server.Host/Database/Migrations/20230614053739_MSRenameHeartbeatsToHealthChecks.cs b/src/Tgstation.Server.Host/Database/Migrations/20230614053739_MSRenameHeartbeatsToHealthChecks.cs index 37dbc14816..20f53e2d0a 100644 --- a/src/Tgstation.Server.Host/Database/Migrations/20230614053739_MSRenameHeartbeatsToHealthChecks.cs +++ b/src/Tgstation.Server.Host/Database/Migrations/20230614053739_MSRenameHeartbeatsToHealthChecks.cs @@ -2,8 +2,6 @@ using Microsoft.EntityFrameworkCore.Migrations; -#nullable disable - namespace Tgstation.Server.Host.Database.Migrations { /// diff --git a/src/Tgstation.Server.Host/Database/Migrations/20230614053927_MYRenameHeartbeatsToHealthChecks.Designer.cs b/src/Tgstation.Server.Host/Database/Migrations/20230614053927_MYRenameHeartbeatsToHealthChecks.Designer.cs index 78f17b9159..ce9233fc4e 100644 --- a/src/Tgstation.Server.Host/Database/Migrations/20230614053927_MYRenameHeartbeatsToHealthChecks.Designer.cs +++ b/src/Tgstation.Server.Host/Database/Migrations/20230614053927_MYRenameHeartbeatsToHealthChecks.Designer.cs @@ -5,8 +5,6 @@ using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore.Infrastructure; using Microsoft.EntityFrameworkCore.Migrations; -#nullable disable - namespace Tgstation.Server.Host.Database.Migrations { [DbContext(typeof(MySqlDatabaseContext))] diff --git a/src/Tgstation.Server.Host/Database/Migrations/20230614053927_MYRenameHeartbeatsToHealthChecks.cs b/src/Tgstation.Server.Host/Database/Migrations/20230614053927_MYRenameHeartbeatsToHealthChecks.cs index 37d18bf1fb..25fb3f21c8 100644 --- a/src/Tgstation.Server.Host/Database/Migrations/20230614053927_MYRenameHeartbeatsToHealthChecks.cs +++ b/src/Tgstation.Server.Host/Database/Migrations/20230614053927_MYRenameHeartbeatsToHealthChecks.cs @@ -2,8 +2,6 @@ using Microsoft.EntityFrameworkCore.Migrations; -#nullable disable - namespace Tgstation.Server.Host.Database.Migrations { /// diff --git a/src/Tgstation.Server.Host/Database/Migrations/20230614054432_PGRenameHeartbeatsToHealthChecks.Designer.cs b/src/Tgstation.Server.Host/Database/Migrations/20230614054432_PGRenameHeartbeatsToHealthChecks.Designer.cs index 650e56fe66..849210b585 100644 --- a/src/Tgstation.Server.Host/Database/Migrations/20230614054432_PGRenameHeartbeatsToHealthChecks.Designer.cs +++ b/src/Tgstation.Server.Host/Database/Migrations/20230614054432_PGRenameHeartbeatsToHealthChecks.Designer.cs @@ -5,8 +5,6 @@ using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore.Infrastructure; using Microsoft.EntityFrameworkCore.Migrations; -#nullable disable - namespace Tgstation.Server.Host.Database.Migrations { [DbContext(typeof(PostgresSqlDatabaseContext))] diff --git a/src/Tgstation.Server.Host/Database/Migrations/20230614054432_PGRenameHeartbeatsToHealthChecks.cs b/src/Tgstation.Server.Host/Database/Migrations/20230614054432_PGRenameHeartbeatsToHealthChecks.cs index 5ccae3445b..75e99990d9 100644 --- a/src/Tgstation.Server.Host/Database/Migrations/20230614054432_PGRenameHeartbeatsToHealthChecks.cs +++ b/src/Tgstation.Server.Host/Database/Migrations/20230614054432_PGRenameHeartbeatsToHealthChecks.cs @@ -2,8 +2,6 @@ using Microsoft.EntityFrameworkCore.Migrations; -#nullable disable - namespace Tgstation.Server.Host.Database.Migrations { /// diff --git a/src/Tgstation.Server.Host/Database/Migrations/20230614054537_SLRenameHeartbeatsToHealthChecks.Designer.cs b/src/Tgstation.Server.Host/Database/Migrations/20230614054537_SLRenameHeartbeatsToHealthChecks.Designer.cs index 9180386ff1..9ffc8607f9 100644 --- a/src/Tgstation.Server.Host/Database/Migrations/20230614054537_SLRenameHeartbeatsToHealthChecks.Designer.cs +++ b/src/Tgstation.Server.Host/Database/Migrations/20230614054537_SLRenameHeartbeatsToHealthChecks.Designer.cs @@ -5,8 +5,6 @@ using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore.Infrastructure; using Microsoft.EntityFrameworkCore.Migrations; -#nullable disable - namespace Tgstation.Server.Host.Database.Migrations { [DbContext(typeof(SqliteDatabaseContext))] diff --git a/src/Tgstation.Server.Host/Database/Migrations/20230614054537_SLRenameHeartbeatsToHealthChecks.cs b/src/Tgstation.Server.Host/Database/Migrations/20230614054537_SLRenameHeartbeatsToHealthChecks.cs index dc88c94012..8326dca934 100644 --- a/src/Tgstation.Server.Host/Database/Migrations/20230614054537_SLRenameHeartbeatsToHealthChecks.cs +++ b/src/Tgstation.Server.Host/Database/Migrations/20230614054537_SLRenameHeartbeatsToHealthChecks.cs @@ -2,8 +2,6 @@ using Microsoft.EntityFrameworkCore.Migrations; -#nullable disable - namespace Tgstation.Server.Host.Database.Migrations { /// diff --git a/src/Tgstation.Server.Host/Database/Migrations/20230622020600_MSAddMapThreads.Designer.cs b/src/Tgstation.Server.Host/Database/Migrations/20230622020600_MSAddMapThreads.Designer.cs index 09c3484802..8910b644ac 100644 --- a/src/Tgstation.Server.Host/Database/Migrations/20230622020600_MSAddMapThreads.Designer.cs +++ b/src/Tgstation.Server.Host/Database/Migrations/20230622020600_MSAddMapThreads.Designer.cs @@ -5,8 +5,6 @@ using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore.Infrastructure; using Microsoft.EntityFrameworkCore.Migrations; -#nullable disable - namespace Tgstation.Server.Host.Database.Migrations { [DbContext(typeof(SqlServerDatabaseContext))] diff --git a/src/Tgstation.Server.Host/Database/Migrations/20230622020600_MSAddMapThreads.cs b/src/Tgstation.Server.Host/Database/Migrations/20230622020600_MSAddMapThreads.cs index dab10dc6af..2bfc981d34 100644 --- a/src/Tgstation.Server.Host/Database/Migrations/20230622020600_MSAddMapThreads.cs +++ b/src/Tgstation.Server.Host/Database/Migrations/20230622020600_MSAddMapThreads.cs @@ -2,8 +2,6 @@ using Microsoft.EntityFrameworkCore.Migrations; -#nullable disable - namespace Tgstation.Server.Host.Database.Migrations { /// diff --git a/src/Tgstation.Server.Host/Database/Migrations/20230622020623_MYAddMapThreads.Designer.cs b/src/Tgstation.Server.Host/Database/Migrations/20230622020623_MYAddMapThreads.Designer.cs index f7039334cf..b9fe9c7cdd 100644 --- a/src/Tgstation.Server.Host/Database/Migrations/20230622020623_MYAddMapThreads.Designer.cs +++ b/src/Tgstation.Server.Host/Database/Migrations/20230622020623_MYAddMapThreads.Designer.cs @@ -5,8 +5,6 @@ using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore.Infrastructure; using Microsoft.EntityFrameworkCore.Migrations; -#nullable disable - namespace Tgstation.Server.Host.Database.Migrations { [DbContext(typeof(MySqlDatabaseContext))] diff --git a/src/Tgstation.Server.Host/Database/Migrations/20230622020623_MYAddMapThreads.cs b/src/Tgstation.Server.Host/Database/Migrations/20230622020623_MYAddMapThreads.cs index 1f83abc44d..aa52cc0d6b 100644 --- a/src/Tgstation.Server.Host/Database/Migrations/20230622020623_MYAddMapThreads.cs +++ b/src/Tgstation.Server.Host/Database/Migrations/20230622020623_MYAddMapThreads.cs @@ -2,8 +2,6 @@ using Microsoft.EntityFrameworkCore.Migrations; -#nullable disable - namespace Tgstation.Server.Host.Database.Migrations { /// diff --git a/src/Tgstation.Server.Host/Database/Migrations/20230622020647_PGAddMapThreads.Designer.cs b/src/Tgstation.Server.Host/Database/Migrations/20230622020647_PGAddMapThreads.Designer.cs index 6ef71f98ee..37c731af70 100644 --- a/src/Tgstation.Server.Host/Database/Migrations/20230622020647_PGAddMapThreads.Designer.cs +++ b/src/Tgstation.Server.Host/Database/Migrations/20230622020647_PGAddMapThreads.Designer.cs @@ -5,8 +5,6 @@ using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore.Infrastructure; using Microsoft.EntityFrameworkCore.Migrations; -#nullable disable - namespace Tgstation.Server.Host.Database.Migrations { [DbContext(typeof(PostgresSqlDatabaseContext))] diff --git a/src/Tgstation.Server.Host/Database/Migrations/20230622020647_PGAddMapThreads.cs b/src/Tgstation.Server.Host/Database/Migrations/20230622020647_PGAddMapThreads.cs index 1ff154f448..1231427158 100644 --- a/src/Tgstation.Server.Host/Database/Migrations/20230622020647_PGAddMapThreads.cs +++ b/src/Tgstation.Server.Host/Database/Migrations/20230622020647_PGAddMapThreads.cs @@ -2,8 +2,6 @@ using Microsoft.EntityFrameworkCore.Migrations; -#nullable disable - namespace Tgstation.Server.Host.Database.Migrations { /// diff --git a/src/Tgstation.Server.Host/Database/Migrations/20230622020712_SLAddMapThreads.Designer.cs b/src/Tgstation.Server.Host/Database/Migrations/20230622020712_SLAddMapThreads.Designer.cs index 0db22004a6..23859c77be 100644 --- a/src/Tgstation.Server.Host/Database/Migrations/20230622020712_SLAddMapThreads.Designer.cs +++ b/src/Tgstation.Server.Host/Database/Migrations/20230622020712_SLAddMapThreads.Designer.cs @@ -5,8 +5,6 @@ using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore.Infrastructure; using Microsoft.EntityFrameworkCore.Migrations; -#nullable disable - namespace Tgstation.Server.Host.Database.Migrations { [DbContext(typeof(SqliteDatabaseContext))] diff --git a/src/Tgstation.Server.Host/Database/Migrations/20230622020712_SLAddMapThreads.cs b/src/Tgstation.Server.Host/Database/Migrations/20230622020712_SLAddMapThreads.cs index 218c9d7d4b..8909a495d2 100644 --- a/src/Tgstation.Server.Host/Database/Migrations/20230622020712_SLAddMapThreads.cs +++ b/src/Tgstation.Server.Host/Database/Migrations/20230622020712_SLAddMapThreads.cs @@ -2,8 +2,6 @@ using Microsoft.EntityFrameworkCore.Migrations; -#nullable disable - namespace Tgstation.Server.Host.Database.Migrations { /// diff --git a/src/Tgstation.Server.Host/Database/Migrations/20231105004801_MSAddJobCodes.Designer.cs b/src/Tgstation.Server.Host/Database/Migrations/20231105004801_MSAddJobCodes.Designer.cs index 556f6d1009..3d2f9959ab 100644 --- a/src/Tgstation.Server.Host/Database/Migrations/20231105004801_MSAddJobCodes.Designer.cs +++ b/src/Tgstation.Server.Host/Database/Migrations/20231105004801_MSAddJobCodes.Designer.cs @@ -5,8 +5,6 @@ using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore.Infrastructure; using Microsoft.EntityFrameworkCore.Migrations; -#nullable disable - namespace Tgstation.Server.Host.Database.Migrations { [DbContext(typeof(SqlServerDatabaseContext))] diff --git a/src/Tgstation.Server.Host/Database/Migrations/20231105004801_MSAddJobCodes.cs b/src/Tgstation.Server.Host/Database/Migrations/20231105004801_MSAddJobCodes.cs index 87636a060f..4a6606334a 100644 --- a/src/Tgstation.Server.Host/Database/Migrations/20231105004801_MSAddJobCodes.cs +++ b/src/Tgstation.Server.Host/Database/Migrations/20231105004801_MSAddJobCodes.cs @@ -2,8 +2,6 @@ using Microsoft.EntityFrameworkCore.Migrations; -#nullable disable - namespace Tgstation.Server.Host.Database.Migrations { /// diff --git a/src/Tgstation.Server.Host/Database/Migrations/20231105004808_MYAddJobCodes.Designer.cs b/src/Tgstation.Server.Host/Database/Migrations/20231105004808_MYAddJobCodes.Designer.cs index c5f5fb94dc..197b244173 100644 --- a/src/Tgstation.Server.Host/Database/Migrations/20231105004808_MYAddJobCodes.Designer.cs +++ b/src/Tgstation.Server.Host/Database/Migrations/20231105004808_MYAddJobCodes.Designer.cs @@ -5,8 +5,6 @@ using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore.Infrastructure; using Microsoft.EntityFrameworkCore.Migrations; -#nullable disable - namespace Tgstation.Server.Host.Database.Migrations { [DbContext(typeof(MySqlDatabaseContext))] diff --git a/src/Tgstation.Server.Host/Database/Migrations/20231105004808_MYAddJobCodes.cs b/src/Tgstation.Server.Host/Database/Migrations/20231105004808_MYAddJobCodes.cs index 61eb53b6a5..4afb902183 100644 --- a/src/Tgstation.Server.Host/Database/Migrations/20231105004808_MYAddJobCodes.cs +++ b/src/Tgstation.Server.Host/Database/Migrations/20231105004808_MYAddJobCodes.cs @@ -2,8 +2,6 @@ using Microsoft.EntityFrameworkCore.Migrations; -#nullable disable - namespace Tgstation.Server.Host.Database.Migrations { /// diff --git a/src/Tgstation.Server.Host/Database/Migrations/20231105004814_PGAddJobCodes.Designer.cs b/src/Tgstation.Server.Host/Database/Migrations/20231105004814_PGAddJobCodes.Designer.cs index 6ad0149338..501367d6fc 100644 --- a/src/Tgstation.Server.Host/Database/Migrations/20231105004814_PGAddJobCodes.Designer.cs +++ b/src/Tgstation.Server.Host/Database/Migrations/20231105004814_PGAddJobCodes.Designer.cs @@ -5,8 +5,6 @@ using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore.Infrastructure; using Microsoft.EntityFrameworkCore.Migrations; -#nullable disable - namespace Tgstation.Server.Host.Database.Migrations { [DbContext(typeof(PostgresSqlDatabaseContext))] diff --git a/src/Tgstation.Server.Host/Database/Migrations/20231105004814_PGAddJobCodes.cs b/src/Tgstation.Server.Host/Database/Migrations/20231105004814_PGAddJobCodes.cs index 5bd1b863f6..b11ba1b584 100644 --- a/src/Tgstation.Server.Host/Database/Migrations/20231105004814_PGAddJobCodes.cs +++ b/src/Tgstation.Server.Host/Database/Migrations/20231105004814_PGAddJobCodes.cs @@ -2,8 +2,6 @@ using Microsoft.EntityFrameworkCore.Migrations; -#nullable disable - namespace Tgstation.Server.Host.Database.Migrations { /// diff --git a/src/Tgstation.Server.Host/Database/Migrations/20231105004820_SLAddJobCodes.Designer.cs b/src/Tgstation.Server.Host/Database/Migrations/20231105004820_SLAddJobCodes.Designer.cs index 718743a339..7fb555cc8b 100644 --- a/src/Tgstation.Server.Host/Database/Migrations/20231105004820_SLAddJobCodes.Designer.cs +++ b/src/Tgstation.Server.Host/Database/Migrations/20231105004820_SLAddJobCodes.Designer.cs @@ -5,8 +5,6 @@ using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore.Infrastructure; using Microsoft.EntityFrameworkCore.Migrations; -#nullable disable - namespace Tgstation.Server.Host.Database.Migrations { [DbContext(typeof(SqliteDatabaseContext))] diff --git a/src/Tgstation.Server.Host/Database/Migrations/20231105004820_SLAddJobCodes.cs b/src/Tgstation.Server.Host/Database/Migrations/20231105004820_SLAddJobCodes.cs index 76a2be02ee..77076a8361 100644 --- a/src/Tgstation.Server.Host/Database/Migrations/20231105004820_SLAddJobCodes.cs +++ b/src/Tgstation.Server.Host/Database/Migrations/20231105004820_SLAddJobCodes.cs @@ -2,8 +2,6 @@ using Microsoft.EntityFrameworkCore.Migrations; -#nullable disable - namespace Tgstation.Server.Host.Database.Migrations { /// diff --git a/src/Tgstation.Server.Host/Database/Migrations/20231108004349_MSRenameByondColumnsToEngine.Designer.cs b/src/Tgstation.Server.Host/Database/Migrations/20231108004349_MSRenameByondColumnsToEngine.Designer.cs index 65b94a6fc7..99cdc64c2e 100644 --- a/src/Tgstation.Server.Host/Database/Migrations/20231108004349_MSRenameByondColumnsToEngine.Designer.cs +++ b/src/Tgstation.Server.Host/Database/Migrations/20231108004349_MSRenameByondColumnsToEngine.Designer.cs @@ -5,8 +5,6 @@ using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore.Infrastructure; using Microsoft.EntityFrameworkCore.Migrations; -#nullable disable - namespace Tgstation.Server.Host.Database.Migrations { [DbContext(typeof(SqlServerDatabaseContext))] diff --git a/src/Tgstation.Server.Host/Database/Migrations/20231108004349_MSRenameByondColumnsToEngine.cs b/src/Tgstation.Server.Host/Database/Migrations/20231108004349_MSRenameByondColumnsToEngine.cs index b142ab007b..cdf156ec28 100644 --- a/src/Tgstation.Server.Host/Database/Migrations/20231108004349_MSRenameByondColumnsToEngine.cs +++ b/src/Tgstation.Server.Host/Database/Migrations/20231108004349_MSRenameByondColumnsToEngine.cs @@ -2,8 +2,6 @@ using Microsoft.EntityFrameworkCore.Migrations; -#nullable disable - namespace Tgstation.Server.Host.Database.Migrations { /// diff --git a/src/Tgstation.Server.Host/Database/Migrations/20231108004356_MYRenameByondColumnsToEngine.Designer.cs b/src/Tgstation.Server.Host/Database/Migrations/20231108004356_MYRenameByondColumnsToEngine.Designer.cs index ff843c5a04..841a10d43d 100644 --- a/src/Tgstation.Server.Host/Database/Migrations/20231108004356_MYRenameByondColumnsToEngine.Designer.cs +++ b/src/Tgstation.Server.Host/Database/Migrations/20231108004356_MYRenameByondColumnsToEngine.Designer.cs @@ -5,8 +5,6 @@ using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore.Infrastructure; using Microsoft.EntityFrameworkCore.Migrations; -#nullable disable - namespace Tgstation.Server.Host.Database.Migrations { [DbContext(typeof(MySqlDatabaseContext))] diff --git a/src/Tgstation.Server.Host/Database/Migrations/20231108004356_MYRenameByondColumnsToEngine.cs b/src/Tgstation.Server.Host/Database/Migrations/20231108004356_MYRenameByondColumnsToEngine.cs index 844b027d16..00a5d1fe1c 100644 --- a/src/Tgstation.Server.Host/Database/Migrations/20231108004356_MYRenameByondColumnsToEngine.cs +++ b/src/Tgstation.Server.Host/Database/Migrations/20231108004356_MYRenameByondColumnsToEngine.cs @@ -2,8 +2,6 @@ using Microsoft.EntityFrameworkCore.Migrations; -#nullable disable - namespace Tgstation.Server.Host.Database.Migrations { /// diff --git a/src/Tgstation.Server.Host/Database/Migrations/20231108004402_PGRenameByondColumnsToEngine.Designer.cs b/src/Tgstation.Server.Host/Database/Migrations/20231108004402_PGRenameByondColumnsToEngine.Designer.cs index 2689c8ae29..7f755e8876 100644 --- a/src/Tgstation.Server.Host/Database/Migrations/20231108004402_PGRenameByondColumnsToEngine.Designer.cs +++ b/src/Tgstation.Server.Host/Database/Migrations/20231108004402_PGRenameByondColumnsToEngine.Designer.cs @@ -5,8 +5,6 @@ using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore.Infrastructure; using Microsoft.EntityFrameworkCore.Migrations; -#nullable disable - namespace Tgstation.Server.Host.Database.Migrations { [DbContext(typeof(PostgresSqlDatabaseContext))] diff --git a/src/Tgstation.Server.Host/Database/Migrations/20231108004402_PGRenameByondColumnsToEngine.cs b/src/Tgstation.Server.Host/Database/Migrations/20231108004402_PGRenameByondColumnsToEngine.cs index 32a4a0957e..4766b285e8 100644 --- a/src/Tgstation.Server.Host/Database/Migrations/20231108004402_PGRenameByondColumnsToEngine.cs +++ b/src/Tgstation.Server.Host/Database/Migrations/20231108004402_PGRenameByondColumnsToEngine.cs @@ -2,8 +2,6 @@ using Microsoft.EntityFrameworkCore.Migrations; -#nullable disable - namespace Tgstation.Server.Host.Database.Migrations { /// diff --git a/src/Tgstation.Server.Host/Database/Migrations/20231108004409_SLRenameByondColumnsToEngine.Designer.cs b/src/Tgstation.Server.Host/Database/Migrations/20231108004409_SLRenameByondColumnsToEngine.Designer.cs index f34889c73c..dae9036d07 100644 --- a/src/Tgstation.Server.Host/Database/Migrations/20231108004409_SLRenameByondColumnsToEngine.Designer.cs +++ b/src/Tgstation.Server.Host/Database/Migrations/20231108004409_SLRenameByondColumnsToEngine.Designer.cs @@ -5,8 +5,6 @@ using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore.Infrastructure; using Microsoft.EntityFrameworkCore.Migrations; -#nullable disable - namespace Tgstation.Server.Host.Database.Migrations { [DbContext(typeof(SqliteDatabaseContext))] diff --git a/src/Tgstation.Server.Host/Database/Migrations/20231108004409_SLRenameByondColumnsToEngine.cs b/src/Tgstation.Server.Host/Database/Migrations/20231108004409_SLRenameByondColumnsToEngine.cs index 755fc8d06e..3b95c6433c 100644 --- a/src/Tgstation.Server.Host/Database/Migrations/20231108004409_SLRenameByondColumnsToEngine.cs +++ b/src/Tgstation.Server.Host/Database/Migrations/20231108004409_SLRenameByondColumnsToEngine.cs @@ -2,8 +2,6 @@ using Microsoft.EntityFrameworkCore.Migrations; -#nullable disable - namespace Tgstation.Server.Host.Database.Migrations { /// diff --git a/src/Tgstation.Server.Host/Database/Migrations/20231220032508_MSAddTopicPort.Designer.cs b/src/Tgstation.Server.Host/Database/Migrations/20231220032508_MSAddTopicPort.Designer.cs new file mode 100644 index 0000000000..39d9629c7c --- /dev/null +++ b/src/Tgstation.Server.Host/Database/Migrations/20231220032508_MSAddTopicPort.Designer.cs @@ -0,0 +1,1076 @@ +// +using System; + +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Migrations; + +namespace Tgstation.Server.Host.Database.Migrations +{ + [DbContext(typeof(SqlServerDatabaseContext))] + [Migration("20231220032508_MSAddTopicPort")] + partial class MSAddTopicPort + { + /// + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("ProductVersion", "8.0.0") + .HasAnnotation("Relational:MaxIdentifierLength", 128); + + SqlServerModelBuilderExtensions.UseIdentityColumns(modelBuilder); + + modelBuilder.Entity("Tgstation.Server.Host.Models.ChatBot", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("ChannelLimit") + .HasColumnType("int"); + + b.Property("ConnectionString") + .IsRequired() + .HasMaxLength(10000) + .HasColumnType("nvarchar(max)"); + + b.Property("Enabled") + .HasColumnType("bit"); + + b.Property("InstanceId") + .HasColumnType("bigint"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("nvarchar(100)"); + + b.Property("Provider") + .HasColumnType("int"); + + b.Property("ReconnectionInterval") + .HasColumnType("bigint"); + + b.HasKey("Id"); + + b.HasIndex("InstanceId", "Name") + .IsUnique(); + + b.ToTable("ChatBots"); + }); + + modelBuilder.Entity("Tgstation.Server.Host.Models.ChatChannel", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("ChatSettingsId") + .HasColumnType("bigint"); + + b.Property("DiscordChannelId") + .HasColumnType("decimal(20,0)"); + + b.Property("IrcChannel") + .HasMaxLength(100) + .HasColumnType("nvarchar(100)"); + + b.Property("IsAdminChannel") + .IsRequired() + .HasColumnType("bit"); + + b.Property("IsSystemChannel") + .IsRequired() + .HasColumnType("bit"); + + b.Property("IsUpdatesChannel") + .IsRequired() + .HasColumnType("bit"); + + b.Property("IsWatchdogChannel") + .IsRequired() + .HasColumnType("bit"); + + b.Property("Tag") + .HasMaxLength(10000) + .HasColumnType("nvarchar(max)"); + + b.HasKey("Id"); + + b.HasIndex("ChatSettingsId", "DiscordChannelId") + .IsUnique() + .HasFilter("[DiscordChannelId] IS NOT NULL"); + + b.HasIndex("ChatSettingsId", "IrcChannel") + .IsUnique() + .HasFilter("[IrcChannel] IS NOT NULL"); + + b.ToTable("ChatChannels"); + }); + + modelBuilder.Entity("Tgstation.Server.Host.Models.CompileJob", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("DMApiMajorVersion") + .HasColumnType("int"); + + b.Property("DMApiMinorVersion") + .HasColumnType("int"); + + b.Property("DMApiPatchVersion") + .HasColumnType("int"); + + b.Property("DirectoryName") + .IsRequired() + .HasColumnType("uniqueidentifier"); + + b.Property("DmeName") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("EngineVersion") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("GitHubDeploymentId") + .HasColumnType("int"); + + b.Property("GitHubRepoId") + .HasColumnType("bigint"); + + b.Property("JobId") + .HasColumnType("bigint"); + + b.Property("MinimumSecurityLevel") + .HasColumnType("int"); + + b.Property("Output") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("RepositoryOrigin") + .HasColumnType("nvarchar(max)"); + + b.Property("RevisionInformationId") + .HasColumnType("bigint"); + + b.HasKey("Id"); + + b.HasIndex("DirectoryName"); + + b.HasIndex("JobId") + .IsUnique(); + + b.HasIndex("RevisionInformationId"); + + b.ToTable("CompileJobs"); + }); + + modelBuilder.Entity("Tgstation.Server.Host.Models.DreamDaemonSettings", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("AdditionalParameters") + .IsRequired() + .HasMaxLength(10000) + .HasColumnType("nvarchar(max)"); + + b.Property("AllowWebClient") + .IsRequired() + .HasColumnType("bit"); + + b.Property("AutoStart") + .IsRequired() + .HasColumnType("bit"); + + b.Property("DumpOnHealthCheckRestart") + .IsRequired() + .HasColumnType("bit"); + + b.Property("HealthCheckSeconds") + .HasColumnType("bigint"); + + b.Property("InstanceId") + .HasColumnType("bigint"); + + b.Property("LogOutput") + .IsRequired() + .HasColumnType("bit"); + + b.Property("MapThreads") + .HasColumnType("bigint"); + + b.Property("Port") + .HasColumnType("int"); + + b.Property("SecurityLevel") + .HasColumnType("int"); + + b.Property("StartProfiler") + .IsRequired() + .HasColumnType("bit"); + + b.Property("StartupTimeout") + .HasColumnType("bigint"); + + b.Property("TopicRequestTimeout") + .HasColumnType("bigint"); + + b.Property("Visibility") + .HasColumnType("int"); + + b.HasKey("Id"); + + b.HasIndex("InstanceId") + .IsUnique(); + + b.ToTable("DreamDaemonSettings"); + }); + + modelBuilder.Entity("Tgstation.Server.Host.Models.DreamMakerSettings", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("ApiValidationPort") + .HasColumnType("int"); + + b.Property("ApiValidationSecurityLevel") + .HasColumnType("int"); + + b.Property("InstanceId") + .HasColumnType("bigint"); + + b.Property("ProjectName") + .HasMaxLength(10000) + .HasColumnType("nvarchar(max)"); + + b.Property("RequireDMApiValidation") + .IsRequired() + .HasColumnType("bit"); + + b.Property("Timeout") + .IsRequired() + .HasColumnType("time"); + + b.HasKey("Id"); + + b.HasIndex("InstanceId") + .IsUnique(); + + b.ToTable("DreamMakerSettings"); + }); + + modelBuilder.Entity("Tgstation.Server.Host.Models.Instance", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("AutoUpdateInterval") + .HasColumnType("bigint"); + + b.Property("ChatBotLimit") + .HasColumnType("int"); + + b.Property("ConfigurationType") + .HasColumnType("int"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("nvarchar(100)"); + + b.Property("Online") + .IsRequired() + .HasColumnType("bit"); + + b.Property("Path") + .IsRequired() + .HasColumnType("nvarchar(450)"); + + b.Property("SwarmIdentifer") + .HasColumnType("nvarchar(450)"); + + b.HasKey("Id"); + + b.HasIndex("Path", "SwarmIdentifer") + .IsUnique() + .HasFilter("[SwarmIdentifer] IS NOT NULL"); + + b.ToTable("Instances"); + }); + + modelBuilder.Entity("Tgstation.Server.Host.Models.InstancePermissionSet", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("ChatBotRights") + .HasColumnType("decimal(20,0)"); + + b.Property("ConfigurationRights") + .HasColumnType("decimal(20,0)"); + + b.Property("DreamDaemonRights") + .HasColumnType("decimal(20,0)"); + + b.Property("DreamMakerRights") + .HasColumnType("decimal(20,0)"); + + b.Property("EngineRights") + .HasColumnType("decimal(20,0)"); + + b.Property("InstanceId") + .HasColumnType("bigint"); + + b.Property("InstancePermissionSetRights") + .HasColumnType("decimal(20,0)"); + + b.Property("PermissionSetId") + .HasColumnType("bigint"); + + b.Property("RepositoryRights") + .HasColumnType("decimal(20,0)"); + + b.HasKey("Id"); + + b.HasIndex("InstanceId"); + + b.HasIndex("PermissionSetId", "InstanceId") + .IsUnique(); + + b.ToTable("InstancePermissionSets"); + }); + + modelBuilder.Entity("Tgstation.Server.Host.Models.Job", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("CancelRight") + .HasColumnType("decimal(20,0)"); + + b.Property("CancelRightsType") + .HasColumnType("decimal(20,0)"); + + b.Property("Cancelled") + .IsRequired() + .HasColumnType("bit"); + + b.Property("CancelledById") + .HasColumnType("bigint"); + + b.Property("Description") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("ErrorCode") + .HasColumnType("bigint"); + + b.Property("ExceptionDetails") + .HasColumnType("nvarchar(max)"); + + b.Property("InstanceId") + .HasColumnType("bigint"); + + b.Property("JobCode") + .HasColumnType("tinyint"); + + b.Property("StartedAt") + .IsRequired() + .HasColumnType("datetimeoffset"); + + b.Property("StartedById") + .HasColumnType("bigint"); + + b.Property("StoppedAt") + .HasColumnType("datetimeoffset"); + + b.HasKey("Id"); + + b.HasIndex("CancelledById"); + + b.HasIndex("InstanceId"); + + b.HasIndex("StartedById"); + + b.ToTable("Jobs"); + }); + + modelBuilder.Entity("Tgstation.Server.Host.Models.OAuthConnection", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("ExternalUserId") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("nvarchar(100)"); + + b.Property("Provider") + .HasColumnType("int"); + + b.Property("UserId") + .HasColumnType("bigint"); + + b.HasKey("Id"); + + b.HasIndex("UserId"); + + b.HasIndex("Provider", "ExternalUserId") + .IsUnique(); + + b.ToTable("OAuthConnections"); + }); + + modelBuilder.Entity("Tgstation.Server.Host.Models.PermissionSet", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("AdministrationRights") + .HasColumnType("decimal(20,0)"); + + b.Property("GroupId") + .HasColumnType("bigint"); + + b.Property("InstanceManagerRights") + .HasColumnType("decimal(20,0)"); + + b.Property("UserId") + .HasColumnType("bigint"); + + b.HasKey("Id"); + + b.HasIndex("GroupId") + .IsUnique() + .HasFilter("[GroupId] IS NOT NULL"); + + b.HasIndex("UserId") + .IsUnique() + .HasFilter("[UserId] IS NOT NULL"); + + b.ToTable("PermissionSets"); + }); + + modelBuilder.Entity("Tgstation.Server.Host.Models.ReattachInformation", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("AccessIdentifier") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("CompileJobId") + .HasColumnType("bigint"); + + b.Property("InitialCompileJobId") + .HasColumnType("bigint"); + + b.Property("LaunchSecurityLevel") + .HasColumnType("int"); + + b.Property("LaunchVisibility") + .HasColumnType("int"); + + b.Property("Port") + .HasColumnType("int"); + + b.Property("ProcessId") + .HasColumnType("int"); + + b.Property("RebootState") + .HasColumnType("int"); + + b.Property("TopicPort") + .HasColumnType("int"); + + b.HasKey("Id"); + + b.HasIndex("CompileJobId"); + + b.HasIndex("InitialCompileJobId"); + + b.ToTable("ReattachInformations"); + }); + + modelBuilder.Entity("Tgstation.Server.Host.Models.RepositorySettings", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("AccessToken") + .HasMaxLength(10000) + .HasColumnType("nvarchar(max)"); + + b.Property("AccessUser") + .HasMaxLength(10000) + .HasColumnType("nvarchar(max)"); + + b.Property("AutoUpdatesKeepTestMerges") + .IsRequired() + .HasColumnType("bit"); + + b.Property("AutoUpdatesSynchronize") + .IsRequired() + .HasColumnType("bit"); + + b.Property("CommitterEmail") + .IsRequired() + .HasMaxLength(10000) + .HasColumnType("nvarchar(max)"); + + b.Property("CommitterName") + .IsRequired() + .HasMaxLength(10000) + .HasColumnType("nvarchar(max)"); + + b.Property("CreateGitHubDeployments") + .IsRequired() + .HasColumnType("bit"); + + b.Property("InstanceId") + .HasColumnType("bigint"); + + b.Property("PostTestMergeComment") + .IsRequired() + .HasColumnType("bit"); + + b.Property("PushTestMergeCommits") + .IsRequired() + .HasColumnType("bit"); + + b.Property("ShowTestMergeCommitters") + .IsRequired() + .HasColumnType("bit"); + + b.Property("UpdateSubmodules") + .IsRequired() + .HasColumnType("bit"); + + b.HasKey("Id"); + + b.HasIndex("InstanceId") + .IsUnique(); + + b.ToTable("RepositorySettings"); + }); + + modelBuilder.Entity("Tgstation.Server.Host.Models.RevInfoTestMerge", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("RevisionInformationId") + .HasColumnType("bigint"); + + b.Property("TestMergeId") + .HasColumnType("bigint"); + + b.HasKey("Id"); + + b.HasIndex("RevisionInformationId"); + + b.HasIndex("TestMergeId"); + + b.ToTable("RevInfoTestMerges"); + }); + + modelBuilder.Entity("Tgstation.Server.Host.Models.RevisionInformation", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("CommitSha") + .IsRequired() + .HasMaxLength(40) + .HasColumnType("nvarchar(40)"); + + b.Property("InstanceId") + .HasColumnType("bigint"); + + b.Property("OriginCommitSha") + .IsRequired() + .HasMaxLength(40) + .HasColumnType("nvarchar(40)"); + + b.Property("Timestamp") + .HasColumnType("datetimeoffset"); + + b.HasKey("Id"); + + b.HasIndex("InstanceId", "CommitSha") + .IsUnique(); + + b.ToTable("RevisionInformations"); + }); + + modelBuilder.Entity("Tgstation.Server.Host.Models.TestMerge", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("Author") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("BodyAtMerge") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("Comment") + .HasMaxLength(10000) + .HasColumnType("nvarchar(max)"); + + b.Property("MergedAt") + .HasColumnType("datetimeoffset"); + + b.Property("MergedById") + .HasColumnType("bigint"); + + b.Property("Number") + .HasColumnType("int"); + + b.Property("PrimaryRevisionInformationId") + .IsRequired() + .HasColumnType("bigint"); + + b.Property("TargetCommitSha") + .IsRequired() + .HasMaxLength(40) + .HasColumnType("nvarchar(40)"); + + b.Property("TitleAtMerge") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("Url") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.HasKey("Id"); + + b.HasIndex("MergedById"); + + b.HasIndex("PrimaryRevisionInformationId") + .IsUnique(); + + b.ToTable("TestMerges"); + }); + + modelBuilder.Entity("Tgstation.Server.Host.Models.User", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("CanonicalName") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("nvarchar(100)"); + + b.Property("CreatedAt") + .IsRequired() + .HasColumnType("datetimeoffset"); + + b.Property("CreatedById") + .HasColumnType("bigint"); + + b.Property("Enabled") + .IsRequired() + .HasColumnType("bit"); + + b.Property("GroupId") + .HasColumnType("bigint"); + + b.Property("LastPasswordUpdate") + .HasColumnType("datetimeoffset"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("nvarchar(100)"); + + b.Property("PasswordHash") + .HasColumnType("nvarchar(max)"); + + b.Property("SystemIdentifier") + .HasMaxLength(100) + .HasColumnType("nvarchar(100)"); + + b.HasKey("Id"); + + b.HasIndex("CanonicalName") + .IsUnique(); + + b.HasIndex("CreatedById"); + + b.HasIndex("GroupId"); + + b.HasIndex("SystemIdentifier") + .IsUnique() + .HasFilter("[SystemIdentifier] IS NOT NULL"); + + b.ToTable("Users"); + }); + + modelBuilder.Entity("Tgstation.Server.Host.Models.UserGroup", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("Name") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("nvarchar(100)"); + + b.HasKey("Id"); + + b.HasIndex("Name") + .IsUnique(); + + b.ToTable("Groups"); + }); + + modelBuilder.Entity("Tgstation.Server.Host.Models.ChatBot", b => + { + b.HasOne("Tgstation.Server.Host.Models.Instance", "Instance") + .WithMany("ChatSettings") + .HasForeignKey("InstanceId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Instance"); + }); + + modelBuilder.Entity("Tgstation.Server.Host.Models.ChatChannel", b => + { + b.HasOne("Tgstation.Server.Host.Models.ChatBot", "ChatSettings") + .WithMany("Channels") + .HasForeignKey("ChatSettingsId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("ChatSettings"); + }); + + modelBuilder.Entity("Tgstation.Server.Host.Models.CompileJob", b => + { + b.HasOne("Tgstation.Server.Host.Models.Job", "Job") + .WithOne() + .HasForeignKey("Tgstation.Server.Host.Models.CompileJob", "JobId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Tgstation.Server.Host.Models.RevisionInformation", "RevisionInformation") + .WithMany("CompileJobs") + .HasForeignKey("RevisionInformationId") + .OnDelete(DeleteBehavior.ClientNoAction) + .IsRequired(); + + b.Navigation("Job"); + + b.Navigation("RevisionInformation"); + }); + + modelBuilder.Entity("Tgstation.Server.Host.Models.DreamDaemonSettings", b => + { + b.HasOne("Tgstation.Server.Host.Models.Instance", "Instance") + .WithOne("DreamDaemonSettings") + .HasForeignKey("Tgstation.Server.Host.Models.DreamDaemonSettings", "InstanceId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Instance"); + }); + + modelBuilder.Entity("Tgstation.Server.Host.Models.DreamMakerSettings", b => + { + b.HasOne("Tgstation.Server.Host.Models.Instance", "Instance") + .WithOne("DreamMakerSettings") + .HasForeignKey("Tgstation.Server.Host.Models.DreamMakerSettings", "InstanceId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Instance"); + }); + + modelBuilder.Entity("Tgstation.Server.Host.Models.InstancePermissionSet", b => + { + b.HasOne("Tgstation.Server.Host.Models.Instance", "Instance") + .WithMany("InstancePermissionSets") + .HasForeignKey("InstanceId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Tgstation.Server.Host.Models.PermissionSet", "PermissionSet") + .WithMany("InstancePermissionSets") + .HasForeignKey("PermissionSetId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Instance"); + + b.Navigation("PermissionSet"); + }); + + modelBuilder.Entity("Tgstation.Server.Host.Models.Job", b => + { + b.HasOne("Tgstation.Server.Host.Models.User", "CancelledBy") + .WithMany() + .HasForeignKey("CancelledById"); + + b.HasOne("Tgstation.Server.Host.Models.Instance", "Instance") + .WithMany("Jobs") + .HasForeignKey("InstanceId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Tgstation.Server.Host.Models.User", "StartedBy") + .WithMany() + .HasForeignKey("StartedById") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("CancelledBy"); + + b.Navigation("Instance"); + + b.Navigation("StartedBy"); + }); + + modelBuilder.Entity("Tgstation.Server.Host.Models.OAuthConnection", b => + { + b.HasOne("Tgstation.Server.Host.Models.User", "User") + .WithMany("OAuthConnections") + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Tgstation.Server.Host.Models.PermissionSet", b => + { + b.HasOne("Tgstation.Server.Host.Models.UserGroup", "Group") + .WithOne("PermissionSet") + .HasForeignKey("Tgstation.Server.Host.Models.PermissionSet", "GroupId") + .OnDelete(DeleteBehavior.Cascade); + + b.HasOne("Tgstation.Server.Host.Models.User", "User") + .WithOne("PermissionSet") + .HasForeignKey("Tgstation.Server.Host.Models.PermissionSet", "UserId") + .OnDelete(DeleteBehavior.Cascade); + + b.Navigation("Group"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Tgstation.Server.Host.Models.ReattachInformation", b => + { + b.HasOne("Tgstation.Server.Host.Models.CompileJob", "CompileJob") + .WithMany() + .HasForeignKey("CompileJobId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Tgstation.Server.Host.Models.CompileJob", "InitialCompileJob") + .WithMany() + .HasForeignKey("InitialCompileJobId"); + + b.Navigation("CompileJob"); + + b.Navigation("InitialCompileJob"); + }); + + modelBuilder.Entity("Tgstation.Server.Host.Models.RepositorySettings", b => + { + b.HasOne("Tgstation.Server.Host.Models.Instance", "Instance") + .WithOne("RepositorySettings") + .HasForeignKey("Tgstation.Server.Host.Models.RepositorySettings", "InstanceId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Instance"); + }); + + modelBuilder.Entity("Tgstation.Server.Host.Models.RevInfoTestMerge", b => + { + b.HasOne("Tgstation.Server.Host.Models.RevisionInformation", "RevisionInformation") + .WithMany("ActiveTestMerges") + .HasForeignKey("RevisionInformationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Tgstation.Server.Host.Models.TestMerge", "TestMerge") + .WithMany("RevisonInformations") + .HasForeignKey("TestMergeId") + .OnDelete(DeleteBehavior.ClientNoAction) + .IsRequired(); + + b.Navigation("RevisionInformation"); + + b.Navigation("TestMerge"); + }); + + modelBuilder.Entity("Tgstation.Server.Host.Models.RevisionInformation", b => + { + b.HasOne("Tgstation.Server.Host.Models.Instance", "Instance") + .WithMany("RevisionInformations") + .HasForeignKey("InstanceId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Instance"); + }); + + modelBuilder.Entity("Tgstation.Server.Host.Models.TestMerge", b => + { + b.HasOne("Tgstation.Server.Host.Models.User", "MergedBy") + .WithMany("TestMerges") + .HasForeignKey("MergedById") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("Tgstation.Server.Host.Models.RevisionInformation", "PrimaryRevisionInformation") + .WithOne("PrimaryTestMerge") + .HasForeignKey("Tgstation.Server.Host.Models.TestMerge", "PrimaryRevisionInformationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("MergedBy"); + + b.Navigation("PrimaryRevisionInformation"); + }); + + modelBuilder.Entity("Tgstation.Server.Host.Models.User", b => + { + b.HasOne("Tgstation.Server.Host.Models.User", "CreatedBy") + .WithMany("CreatedUsers") + .HasForeignKey("CreatedById"); + + b.HasOne("Tgstation.Server.Host.Models.UserGroup", "Group") + .WithMany("Users") + .HasForeignKey("GroupId"); + + b.Navigation("CreatedBy"); + + b.Navigation("Group"); + }); + + modelBuilder.Entity("Tgstation.Server.Host.Models.ChatBot", b => + { + b.Navigation("Channels"); + }); + + modelBuilder.Entity("Tgstation.Server.Host.Models.Instance", b => + { + b.Navigation("ChatSettings"); + + b.Navigation("DreamDaemonSettings"); + + b.Navigation("DreamMakerSettings"); + + b.Navigation("InstancePermissionSets"); + + b.Navigation("Jobs"); + + b.Navigation("RepositorySettings"); + + b.Navigation("RevisionInformations"); + }); + + modelBuilder.Entity("Tgstation.Server.Host.Models.PermissionSet", b => + { + b.Navigation("InstancePermissionSets"); + }); + + modelBuilder.Entity("Tgstation.Server.Host.Models.RevisionInformation", b => + { + b.Navigation("ActiveTestMerges"); + + b.Navigation("CompileJobs"); + + b.Navigation("PrimaryTestMerge"); + }); + + modelBuilder.Entity("Tgstation.Server.Host.Models.TestMerge", b => + { + b.Navigation("RevisonInformations"); + }); + + modelBuilder.Entity("Tgstation.Server.Host.Models.User", b => + { + b.Navigation("CreatedUsers"); + + b.Navigation("OAuthConnections"); + + b.Navigation("PermissionSet"); + + b.Navigation("TestMerges"); + }); + + modelBuilder.Entity("Tgstation.Server.Host.Models.UserGroup", b => + { + b.Navigation("PermissionSet") + .IsRequired(); + + b.Navigation("Users"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/src/Tgstation.Server.Host/Database/Migrations/20231220032508_MSAddTopicPort.cs b/src/Tgstation.Server.Host/Database/Migrations/20231220032508_MSAddTopicPort.cs new file mode 100644 index 0000000000..2c1754e5f1 --- /dev/null +++ b/src/Tgstation.Server.Host/Database/Migrations/20231220032508_MSAddTopicPort.cs @@ -0,0 +1,32 @@ +using System; + +using Microsoft.EntityFrameworkCore.Migrations; + +namespace Tgstation.Server.Host.Database.Migrations +{ + /// + public partial class MSAddTopicPort : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + ArgumentNullException.ThrowIfNull(migrationBuilder); + + migrationBuilder.AddColumn( + name: "TopicPort", + table: "ReattachInformations", + type: "int", + nullable: true); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + ArgumentNullException.ThrowIfNull(migrationBuilder); + + migrationBuilder.DropColumn( + name: "TopicPort", + table: "ReattachInformations"); + } + } +} diff --git a/src/Tgstation.Server.Host/Database/Migrations/20231220032515_MYAddTopicPort.Designer.cs b/src/Tgstation.Server.Host/Database/Migrations/20231220032515_MYAddTopicPort.Designer.cs new file mode 100644 index 0000000000..593d5f4db2 --- /dev/null +++ b/src/Tgstation.Server.Host/Database/Migrations/20231220032515_MYAddTopicPort.Designer.cs @@ -0,0 +1,1110 @@ +// +using System; + +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Migrations; + +namespace Tgstation.Server.Host.Database.Migrations +{ + [DbContext(typeof(MySqlDatabaseContext))] + [Migration("20231220032515_MYAddTopicPort")] + partial class MYAddTopicPort + { + /// + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("ProductVersion", "8.0.0") + .HasAnnotation("Relational:MaxIdentifierLength", 64); + + modelBuilder.Entity("Tgstation.Server.Host.Models.ChatBot", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + b.Property("ChannelLimit") + .IsRequired() + .HasColumnType("smallint unsigned"); + + b.Property("ConnectionString") + .IsRequired() + .HasMaxLength(10000) + .HasColumnType("longtext"); + + MySqlPropertyBuilderExtensions.HasCharSet(b.Property("ConnectionString"), "utf8mb4"); + + b.Property("Enabled") + .HasColumnType("tinyint(1)"); + + b.Property("InstanceId") + .HasColumnType("bigint"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("varchar(100)"); + + b.Property("Provider") + .HasColumnType("int"); + + b.Property("ReconnectionInterval") + .IsRequired() + .HasColumnType("int unsigned"); + + b.HasKey("Id"); + + b.HasIndex("InstanceId", "Name") + .IsUnique(); + + b.ToTable("ChatBots"); + }); + + modelBuilder.Entity("Tgstation.Server.Host.Models.ChatChannel", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + b.Property("ChatSettingsId") + .HasColumnType("bigint"); + + b.Property("DiscordChannelId") + .HasColumnType("bigint unsigned"); + + b.Property("IrcChannel") + .HasMaxLength(100) + .HasColumnType("varchar(100)"); + + MySqlPropertyBuilderExtensions.HasCharSet(b.Property("IrcChannel"), "utf8mb4"); + + b.Property("IsAdminChannel") + .IsRequired() + .HasColumnType("tinyint(1)"); + + b.Property("IsSystemChannel") + .IsRequired() + .HasColumnType("tinyint(1)"); + + b.Property("IsUpdatesChannel") + .IsRequired() + .HasColumnType("tinyint(1)"); + + b.Property("IsWatchdogChannel") + .IsRequired() + .HasColumnType("tinyint(1)"); + + b.Property("Tag") + .HasMaxLength(10000) + .HasColumnType("longtext"); + + MySqlPropertyBuilderExtensions.HasCharSet(b.Property("Tag"), "utf8mb4"); + + b.HasKey("Id"); + + b.HasIndex("ChatSettingsId", "DiscordChannelId") + .IsUnique(); + + b.HasIndex("ChatSettingsId", "IrcChannel") + .IsUnique(); + + b.ToTable("ChatChannels"); + }); + + modelBuilder.Entity("Tgstation.Server.Host.Models.CompileJob", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + b.Property("DMApiMajorVersion") + .HasColumnType("int"); + + b.Property("DMApiMinorVersion") + .HasColumnType("int"); + + b.Property("DMApiPatchVersion") + .HasColumnType("int"); + + b.Property("DirectoryName") + .IsRequired() + .HasColumnType("char(36)"); + + b.Property("DmeName") + .IsRequired() + .HasColumnType("longtext"); + + MySqlPropertyBuilderExtensions.HasCharSet(b.Property("DmeName"), "utf8mb4"); + + b.Property("EngineVersion") + .IsRequired() + .HasColumnType("longtext"); + + MySqlPropertyBuilderExtensions.HasCharSet(b.Property("EngineVersion"), "utf8mb4"); + + b.Property("GitHubDeploymentId") + .HasColumnType("int"); + + b.Property("GitHubRepoId") + .HasColumnType("bigint"); + + b.Property("JobId") + .HasColumnType("bigint"); + + b.Property("MinimumSecurityLevel") + .HasColumnType("int"); + + b.Property("Output") + .IsRequired() + .HasColumnType("longtext"); + + MySqlPropertyBuilderExtensions.HasCharSet(b.Property("Output"), "utf8mb4"); + + b.Property("RepositoryOrigin") + .HasColumnType("longtext"); + + MySqlPropertyBuilderExtensions.HasCharSet(b.Property("RepositoryOrigin"), "utf8mb4"); + + b.Property("RevisionInformationId") + .HasColumnType("bigint"); + + b.HasKey("Id"); + + b.HasIndex("DirectoryName"); + + b.HasIndex("JobId") + .IsUnique(); + + b.HasIndex("RevisionInformationId"); + + b.ToTable("CompileJobs"); + }); + + modelBuilder.Entity("Tgstation.Server.Host.Models.DreamDaemonSettings", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + b.Property("AdditionalParameters") + .IsRequired() + .HasMaxLength(10000) + .HasColumnType("longtext"); + + MySqlPropertyBuilderExtensions.HasCharSet(b.Property("AdditionalParameters"), "utf8mb4"); + + b.Property("AllowWebClient") + .IsRequired() + .HasColumnType("tinyint(1)"); + + b.Property("AutoStart") + .IsRequired() + .HasColumnType("tinyint(1)"); + + b.Property("DumpOnHealthCheckRestart") + .IsRequired() + .HasColumnType("tinyint(1)"); + + b.Property("HealthCheckSeconds") + .IsRequired() + .HasColumnType("int unsigned"); + + b.Property("InstanceId") + .HasColumnType("bigint"); + + b.Property("LogOutput") + .IsRequired() + .HasColumnType("tinyint(1)"); + + b.Property("MapThreads") + .IsRequired() + .HasColumnType("int unsigned"); + + b.Property("Port") + .IsRequired() + .HasColumnType("smallint unsigned"); + + b.Property("SecurityLevel") + .HasColumnType("int"); + + b.Property("StartProfiler") + .IsRequired() + .HasColumnType("tinyint(1)"); + + b.Property("StartupTimeout") + .IsRequired() + .HasColumnType("int unsigned"); + + b.Property("TopicRequestTimeout") + .IsRequired() + .HasColumnType("int unsigned"); + + b.Property("Visibility") + .HasColumnType("int"); + + b.HasKey("Id"); + + b.HasIndex("InstanceId") + .IsUnique(); + + b.ToTable("DreamDaemonSettings"); + }); + + modelBuilder.Entity("Tgstation.Server.Host.Models.DreamMakerSettings", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + b.Property("ApiValidationPort") + .IsRequired() + .HasColumnType("smallint unsigned"); + + b.Property("ApiValidationSecurityLevel") + .HasColumnType("int"); + + b.Property("InstanceId") + .HasColumnType("bigint"); + + b.Property("ProjectName") + .HasMaxLength(10000) + .HasColumnType("longtext"); + + MySqlPropertyBuilderExtensions.HasCharSet(b.Property("ProjectName"), "utf8mb4"); + + b.Property("RequireDMApiValidation") + .IsRequired() + .HasColumnType("tinyint(1)"); + + b.Property("Timeout") + .IsRequired() + .HasColumnType("time(6)"); + + b.HasKey("Id"); + + b.HasIndex("InstanceId") + .IsUnique(); + + b.ToTable("DreamMakerSettings"); + }); + + modelBuilder.Entity("Tgstation.Server.Host.Models.Instance", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + b.Property("AutoUpdateInterval") + .IsRequired() + .HasColumnType("int unsigned"); + + b.Property("ChatBotLimit") + .IsRequired() + .HasColumnType("smallint unsigned"); + + b.Property("ConfigurationType") + .HasColumnType("int"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("varchar(100)"); + + MySqlPropertyBuilderExtensions.HasCharSet(b.Property("Name"), "utf8mb4"); + + b.Property("Online") + .IsRequired() + .HasColumnType("tinyint(1)"); + + b.Property("Path") + .IsRequired() + .HasColumnType("varchar(255)"); + + MySqlPropertyBuilderExtensions.HasCharSet(b.Property("Path"), "utf8mb4"); + + b.Property("SwarmIdentifer") + .HasColumnType("varchar(255)"); + + MySqlPropertyBuilderExtensions.HasCharSet(b.Property("SwarmIdentifer"), "utf8mb4"); + + b.HasKey("Id"); + + b.HasIndex("Path", "SwarmIdentifer") + .IsUnique(); + + b.ToTable("Instances"); + }); + + modelBuilder.Entity("Tgstation.Server.Host.Models.InstancePermissionSet", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + b.Property("ChatBotRights") + .HasColumnType("bigint unsigned"); + + b.Property("ConfigurationRights") + .HasColumnType("bigint unsigned"); + + b.Property("DreamDaemonRights") + .HasColumnType("bigint unsigned"); + + b.Property("DreamMakerRights") + .HasColumnType("bigint unsigned"); + + b.Property("EngineRights") + .HasColumnType("bigint unsigned"); + + b.Property("InstanceId") + .HasColumnType("bigint"); + + b.Property("InstancePermissionSetRights") + .HasColumnType("bigint unsigned"); + + b.Property("PermissionSetId") + .HasColumnType("bigint"); + + b.Property("RepositoryRights") + .HasColumnType("bigint unsigned"); + + b.HasKey("Id"); + + b.HasIndex("InstanceId"); + + b.HasIndex("PermissionSetId", "InstanceId") + .IsUnique(); + + b.ToTable("InstancePermissionSets"); + }); + + modelBuilder.Entity("Tgstation.Server.Host.Models.Job", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + b.Property("CancelRight") + .HasColumnType("bigint unsigned"); + + b.Property("CancelRightsType") + .HasColumnType("bigint unsigned"); + + b.Property("Cancelled") + .IsRequired() + .HasColumnType("tinyint(1)"); + + b.Property("CancelledById") + .HasColumnType("bigint"); + + b.Property("Description") + .IsRequired() + .HasColumnType("longtext"); + + MySqlPropertyBuilderExtensions.HasCharSet(b.Property("Description"), "utf8mb4"); + + b.Property("ErrorCode") + .HasColumnType("int unsigned"); + + b.Property("ExceptionDetails") + .HasColumnType("longtext"); + + MySqlPropertyBuilderExtensions.HasCharSet(b.Property("ExceptionDetails"), "utf8mb4"); + + b.Property("InstanceId") + .HasColumnType("bigint"); + + b.Property("JobCode") + .HasColumnType("tinyint unsigned"); + + b.Property("StartedAt") + .IsRequired() + .HasColumnType("datetime(6)"); + + b.Property("StartedById") + .HasColumnType("bigint"); + + b.Property("StoppedAt") + .HasColumnType("datetime(6)"); + + b.HasKey("Id"); + + b.HasIndex("CancelledById"); + + b.HasIndex("InstanceId"); + + b.HasIndex("StartedById"); + + b.ToTable("Jobs"); + }); + + modelBuilder.Entity("Tgstation.Server.Host.Models.OAuthConnection", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + b.Property("ExternalUserId") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("varchar(100)"); + + MySqlPropertyBuilderExtensions.HasCharSet(b.Property("ExternalUserId"), "utf8mb4"); + + b.Property("Provider") + .HasColumnType("int"); + + b.Property("UserId") + .HasColumnType("bigint"); + + b.HasKey("Id"); + + b.HasIndex("UserId"); + + b.HasIndex("Provider", "ExternalUserId") + .IsUnique(); + + b.ToTable("OAuthConnections"); + }); + + modelBuilder.Entity("Tgstation.Server.Host.Models.PermissionSet", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + b.Property("AdministrationRights") + .HasColumnType("bigint unsigned"); + + b.Property("GroupId") + .HasColumnType("bigint"); + + b.Property("InstanceManagerRights") + .HasColumnType("bigint unsigned"); + + b.Property("UserId") + .HasColumnType("bigint"); + + b.HasKey("Id"); + + b.HasIndex("GroupId") + .IsUnique(); + + b.HasIndex("UserId") + .IsUnique(); + + b.ToTable("PermissionSets"); + }); + + modelBuilder.Entity("Tgstation.Server.Host.Models.ReattachInformation", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + b.Property("AccessIdentifier") + .IsRequired() + .HasColumnType("longtext"); + + MySqlPropertyBuilderExtensions.HasCharSet(b.Property("AccessIdentifier"), "utf8mb4"); + + b.Property("CompileJobId") + .HasColumnType("bigint"); + + b.Property("InitialCompileJobId") + .HasColumnType("bigint"); + + b.Property("LaunchSecurityLevel") + .HasColumnType("int"); + + b.Property("LaunchVisibility") + .HasColumnType("int"); + + b.Property("Port") + .HasColumnType("smallint unsigned"); + + b.Property("ProcessId") + .HasColumnType("int"); + + b.Property("RebootState") + .HasColumnType("int"); + + b.Property("TopicPort") + .HasColumnType("smallint unsigned"); + + b.HasKey("Id"); + + b.HasIndex("CompileJobId"); + + b.HasIndex("InitialCompileJobId"); + + b.ToTable("ReattachInformations"); + }); + + modelBuilder.Entity("Tgstation.Server.Host.Models.RepositorySettings", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + b.Property("AccessToken") + .HasMaxLength(10000) + .HasColumnType("longtext"); + + MySqlPropertyBuilderExtensions.HasCharSet(b.Property("AccessToken"), "utf8mb4"); + + b.Property("AccessUser") + .HasMaxLength(10000) + .HasColumnType("longtext"); + + MySqlPropertyBuilderExtensions.HasCharSet(b.Property("AccessUser"), "utf8mb4"); + + b.Property("AutoUpdatesKeepTestMerges") + .IsRequired() + .HasColumnType("tinyint(1)"); + + b.Property("AutoUpdatesSynchronize") + .IsRequired() + .HasColumnType("tinyint(1)"); + + b.Property("CommitterEmail") + .IsRequired() + .HasMaxLength(10000) + .HasColumnType("longtext"); + + MySqlPropertyBuilderExtensions.HasCharSet(b.Property("CommitterEmail"), "utf8mb4"); + + b.Property("CommitterName") + .IsRequired() + .HasMaxLength(10000) + .HasColumnType("longtext"); + + MySqlPropertyBuilderExtensions.HasCharSet(b.Property("CommitterName"), "utf8mb4"); + + b.Property("CreateGitHubDeployments") + .IsRequired() + .HasColumnType("tinyint(1)"); + + b.Property("InstanceId") + .HasColumnType("bigint"); + + b.Property("PostTestMergeComment") + .IsRequired() + .HasColumnType("tinyint(1)"); + + b.Property("PushTestMergeCommits") + .IsRequired() + .HasColumnType("tinyint(1)"); + + b.Property("ShowTestMergeCommitters") + .IsRequired() + .HasColumnType("tinyint(1)"); + + b.Property("UpdateSubmodules") + .IsRequired() + .HasColumnType("tinyint(1)"); + + b.HasKey("Id"); + + b.HasIndex("InstanceId") + .IsUnique(); + + b.ToTable("RepositorySettings"); + }); + + modelBuilder.Entity("Tgstation.Server.Host.Models.RevInfoTestMerge", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + b.Property("RevisionInformationId") + .HasColumnType("bigint"); + + b.Property("TestMergeId") + .HasColumnType("bigint"); + + b.HasKey("Id"); + + b.HasIndex("RevisionInformationId"); + + b.HasIndex("TestMergeId"); + + b.ToTable("RevInfoTestMerges"); + }); + + modelBuilder.Entity("Tgstation.Server.Host.Models.RevisionInformation", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + b.Property("CommitSha") + .IsRequired() + .HasMaxLength(40) + .HasColumnType("varchar(40)"); + + MySqlPropertyBuilderExtensions.HasCharSet(b.Property("CommitSha"), "utf8mb4"); + + b.Property("InstanceId") + .HasColumnType("bigint"); + + b.Property("OriginCommitSha") + .IsRequired() + .HasMaxLength(40) + .HasColumnType("varchar(40)"); + + MySqlPropertyBuilderExtensions.HasCharSet(b.Property("OriginCommitSha"), "utf8mb4"); + + b.Property("Timestamp") + .HasColumnType("datetime(6)"); + + b.HasKey("Id"); + + b.HasIndex("InstanceId", "CommitSha") + .IsUnique(); + + b.ToTable("RevisionInformations"); + }); + + modelBuilder.Entity("Tgstation.Server.Host.Models.TestMerge", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + b.Property("Author") + .IsRequired() + .HasColumnType("longtext"); + + MySqlPropertyBuilderExtensions.HasCharSet(b.Property("Author"), "utf8mb4"); + + b.Property("BodyAtMerge") + .IsRequired() + .HasColumnType("longtext"); + + MySqlPropertyBuilderExtensions.HasCharSet(b.Property("BodyAtMerge"), "utf8mb4"); + + b.Property("Comment") + .HasMaxLength(10000) + .HasColumnType("longtext"); + + MySqlPropertyBuilderExtensions.HasCharSet(b.Property("Comment"), "utf8mb4"); + + b.Property("MergedAt") + .HasColumnType("datetime(6)"); + + b.Property("MergedById") + .HasColumnType("bigint"); + + b.Property("Number") + .HasColumnType("int"); + + b.Property("PrimaryRevisionInformationId") + .IsRequired() + .HasColumnType("bigint"); + + b.Property("TargetCommitSha") + .IsRequired() + .HasMaxLength(40) + .HasColumnType("varchar(40)"); + + MySqlPropertyBuilderExtensions.HasCharSet(b.Property("TargetCommitSha"), "utf8mb4"); + + b.Property("TitleAtMerge") + .IsRequired() + .HasColumnType("longtext"); + + MySqlPropertyBuilderExtensions.HasCharSet(b.Property("TitleAtMerge"), "utf8mb4"); + + b.Property("Url") + .IsRequired() + .HasColumnType("longtext"); + + MySqlPropertyBuilderExtensions.HasCharSet(b.Property("Url"), "utf8mb4"); + + b.HasKey("Id"); + + b.HasIndex("MergedById"); + + b.HasIndex("PrimaryRevisionInformationId") + .IsUnique(); + + b.ToTable("TestMerges"); + }); + + modelBuilder.Entity("Tgstation.Server.Host.Models.User", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + b.Property("CanonicalName") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("varchar(100)"); + + MySqlPropertyBuilderExtensions.HasCharSet(b.Property("CanonicalName"), "utf8mb4"); + + b.Property("CreatedAt") + .IsRequired() + .HasColumnType("datetime(6)"); + + b.Property("CreatedById") + .HasColumnType("bigint"); + + b.Property("Enabled") + .IsRequired() + .HasColumnType("tinyint(1)"); + + b.Property("GroupId") + .HasColumnType("bigint"); + + b.Property("LastPasswordUpdate") + .HasColumnType("datetime(6)"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("varchar(100)"); + + MySqlPropertyBuilderExtensions.HasCharSet(b.Property("Name"), "utf8mb4"); + + b.Property("PasswordHash") + .HasColumnType("longtext"); + + MySqlPropertyBuilderExtensions.HasCharSet(b.Property("PasswordHash"), "utf8mb4"); + + b.Property("SystemIdentifier") + .HasMaxLength(100) + .HasColumnType("varchar(100)"); + + MySqlPropertyBuilderExtensions.HasCharSet(b.Property("SystemIdentifier"), "utf8mb4"); + + b.HasKey("Id"); + + b.HasIndex("CanonicalName") + .IsUnique(); + + b.HasIndex("CreatedById"); + + b.HasIndex("GroupId"); + + b.HasIndex("SystemIdentifier") + .IsUnique(); + + b.ToTable("Users"); + }); + + modelBuilder.Entity("Tgstation.Server.Host.Models.UserGroup", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("varchar(100)"); + + MySqlPropertyBuilderExtensions.HasCharSet(b.Property("Name"), "utf8mb4"); + + b.HasKey("Id"); + + b.HasIndex("Name") + .IsUnique(); + + b.ToTable("Groups"); + }); + + modelBuilder.Entity("Tgstation.Server.Host.Models.ChatBot", b => + { + b.HasOne("Tgstation.Server.Host.Models.Instance", "Instance") + .WithMany("ChatSettings") + .HasForeignKey("InstanceId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Instance"); + }); + + modelBuilder.Entity("Tgstation.Server.Host.Models.ChatChannel", b => + { + b.HasOne("Tgstation.Server.Host.Models.ChatBot", "ChatSettings") + .WithMany("Channels") + .HasForeignKey("ChatSettingsId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("ChatSettings"); + }); + + modelBuilder.Entity("Tgstation.Server.Host.Models.CompileJob", b => + { + b.HasOne("Tgstation.Server.Host.Models.Job", "Job") + .WithOne() + .HasForeignKey("Tgstation.Server.Host.Models.CompileJob", "JobId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Tgstation.Server.Host.Models.RevisionInformation", "RevisionInformation") + .WithMany("CompileJobs") + .HasForeignKey("RevisionInformationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Job"); + + b.Navigation("RevisionInformation"); + }); + + modelBuilder.Entity("Tgstation.Server.Host.Models.DreamDaemonSettings", b => + { + b.HasOne("Tgstation.Server.Host.Models.Instance", "Instance") + .WithOne("DreamDaemonSettings") + .HasForeignKey("Tgstation.Server.Host.Models.DreamDaemonSettings", "InstanceId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Instance"); + }); + + modelBuilder.Entity("Tgstation.Server.Host.Models.DreamMakerSettings", b => + { + b.HasOne("Tgstation.Server.Host.Models.Instance", "Instance") + .WithOne("DreamMakerSettings") + .HasForeignKey("Tgstation.Server.Host.Models.DreamMakerSettings", "InstanceId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Instance"); + }); + + modelBuilder.Entity("Tgstation.Server.Host.Models.InstancePermissionSet", b => + { + b.HasOne("Tgstation.Server.Host.Models.Instance", "Instance") + .WithMany("InstancePermissionSets") + .HasForeignKey("InstanceId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Tgstation.Server.Host.Models.PermissionSet", "PermissionSet") + .WithMany("InstancePermissionSets") + .HasForeignKey("PermissionSetId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Instance"); + + b.Navigation("PermissionSet"); + }); + + modelBuilder.Entity("Tgstation.Server.Host.Models.Job", b => + { + b.HasOne("Tgstation.Server.Host.Models.User", "CancelledBy") + .WithMany() + .HasForeignKey("CancelledById"); + + b.HasOne("Tgstation.Server.Host.Models.Instance", "Instance") + .WithMany("Jobs") + .HasForeignKey("InstanceId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Tgstation.Server.Host.Models.User", "StartedBy") + .WithMany() + .HasForeignKey("StartedById") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("CancelledBy"); + + b.Navigation("Instance"); + + b.Navigation("StartedBy"); + }); + + modelBuilder.Entity("Tgstation.Server.Host.Models.OAuthConnection", b => + { + b.HasOne("Tgstation.Server.Host.Models.User", "User") + .WithMany("OAuthConnections") + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Tgstation.Server.Host.Models.PermissionSet", b => + { + b.HasOne("Tgstation.Server.Host.Models.UserGroup", "Group") + .WithOne("PermissionSet") + .HasForeignKey("Tgstation.Server.Host.Models.PermissionSet", "GroupId") + .OnDelete(DeleteBehavior.Cascade); + + b.HasOne("Tgstation.Server.Host.Models.User", "User") + .WithOne("PermissionSet") + .HasForeignKey("Tgstation.Server.Host.Models.PermissionSet", "UserId") + .OnDelete(DeleteBehavior.Cascade); + + b.Navigation("Group"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Tgstation.Server.Host.Models.ReattachInformation", b => + { + b.HasOne("Tgstation.Server.Host.Models.CompileJob", "CompileJob") + .WithMany() + .HasForeignKey("CompileJobId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Tgstation.Server.Host.Models.CompileJob", "InitialCompileJob") + .WithMany() + .HasForeignKey("InitialCompileJobId"); + + b.Navigation("CompileJob"); + + b.Navigation("InitialCompileJob"); + }); + + modelBuilder.Entity("Tgstation.Server.Host.Models.RepositorySettings", b => + { + b.HasOne("Tgstation.Server.Host.Models.Instance", "Instance") + .WithOne("RepositorySettings") + .HasForeignKey("Tgstation.Server.Host.Models.RepositorySettings", "InstanceId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Instance"); + }); + + modelBuilder.Entity("Tgstation.Server.Host.Models.RevInfoTestMerge", b => + { + b.HasOne("Tgstation.Server.Host.Models.RevisionInformation", "RevisionInformation") + .WithMany("ActiveTestMerges") + .HasForeignKey("RevisionInformationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Tgstation.Server.Host.Models.TestMerge", "TestMerge") + .WithMany("RevisonInformations") + .HasForeignKey("TestMergeId") + .OnDelete(DeleteBehavior.ClientNoAction) + .IsRequired(); + + b.Navigation("RevisionInformation"); + + b.Navigation("TestMerge"); + }); + + modelBuilder.Entity("Tgstation.Server.Host.Models.RevisionInformation", b => + { + b.HasOne("Tgstation.Server.Host.Models.Instance", "Instance") + .WithMany("RevisionInformations") + .HasForeignKey("InstanceId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Instance"); + }); + + modelBuilder.Entity("Tgstation.Server.Host.Models.TestMerge", b => + { + b.HasOne("Tgstation.Server.Host.Models.User", "MergedBy") + .WithMany("TestMerges") + .HasForeignKey("MergedById") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("Tgstation.Server.Host.Models.RevisionInformation", "PrimaryRevisionInformation") + .WithOne("PrimaryTestMerge") + .HasForeignKey("Tgstation.Server.Host.Models.TestMerge", "PrimaryRevisionInformationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("MergedBy"); + + b.Navigation("PrimaryRevisionInformation"); + }); + + modelBuilder.Entity("Tgstation.Server.Host.Models.User", b => + { + b.HasOne("Tgstation.Server.Host.Models.User", "CreatedBy") + .WithMany("CreatedUsers") + .HasForeignKey("CreatedById"); + + b.HasOne("Tgstation.Server.Host.Models.UserGroup", "Group") + .WithMany("Users") + .HasForeignKey("GroupId"); + + b.Navigation("CreatedBy"); + + b.Navigation("Group"); + }); + + modelBuilder.Entity("Tgstation.Server.Host.Models.ChatBot", b => + { + b.Navigation("Channels"); + }); + + modelBuilder.Entity("Tgstation.Server.Host.Models.Instance", b => + { + b.Navigation("ChatSettings"); + + b.Navigation("DreamDaemonSettings"); + + b.Navigation("DreamMakerSettings"); + + b.Navigation("InstancePermissionSets"); + + b.Navigation("Jobs"); + + b.Navigation("RepositorySettings"); + + b.Navigation("RevisionInformations"); + }); + + modelBuilder.Entity("Tgstation.Server.Host.Models.PermissionSet", b => + { + b.Navigation("InstancePermissionSets"); + }); + + modelBuilder.Entity("Tgstation.Server.Host.Models.RevisionInformation", b => + { + b.Navigation("ActiveTestMerges"); + + b.Navigation("CompileJobs"); + + b.Navigation("PrimaryTestMerge"); + }); + + modelBuilder.Entity("Tgstation.Server.Host.Models.TestMerge", b => + { + b.Navigation("RevisonInformations"); + }); + + modelBuilder.Entity("Tgstation.Server.Host.Models.User", b => + { + b.Navigation("CreatedUsers"); + + b.Navigation("OAuthConnections"); + + b.Navigation("PermissionSet"); + + b.Navigation("TestMerges"); + }); + + modelBuilder.Entity("Tgstation.Server.Host.Models.UserGroup", b => + { + b.Navigation("PermissionSet") + .IsRequired(); + + b.Navigation("Users"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/src/Tgstation.Server.Host/Database/Migrations/20231220032515_MYAddTopicPort.cs b/src/Tgstation.Server.Host/Database/Migrations/20231220032515_MYAddTopicPort.cs new file mode 100644 index 0000000000..1630786ccf --- /dev/null +++ b/src/Tgstation.Server.Host/Database/Migrations/20231220032515_MYAddTopicPort.cs @@ -0,0 +1,32 @@ +using System; + +using Microsoft.EntityFrameworkCore.Migrations; + +namespace Tgstation.Server.Host.Database.Migrations +{ + /// + public partial class MYAddTopicPort : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + ArgumentNullException.ThrowIfNull(migrationBuilder); + + migrationBuilder.AddColumn( + name: "TopicPort", + table: "ReattachInformations", + type: "smallint unsigned", + nullable: true); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + ArgumentNullException.ThrowIfNull(migrationBuilder); + + migrationBuilder.DropColumn( + name: "TopicPort", + table: "ReattachInformations"); + } + } +} diff --git a/src/Tgstation.Server.Host/Database/Migrations/20231220032521_PGAddTopicPort.Designer.cs b/src/Tgstation.Server.Host/Database/Migrations/20231220032521_PGAddTopicPort.Designer.cs new file mode 100644 index 0000000000..6f31c5e109 --- /dev/null +++ b/src/Tgstation.Server.Host/Database/Migrations/20231220032521_PGAddTopicPort.Designer.cs @@ -0,0 +1,1070 @@ +// +using System; + +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Migrations; + +namespace Tgstation.Server.Host.Database.Migrations +{ + [DbContext(typeof(PostgresSqlDatabaseContext))] + [Migration("20231220032521_PGAddTopicPort")] + partial class PGAddTopicPort + { + /// + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("ProductVersion", "8.0.0") + .HasAnnotation("Relational:MaxIdentifierLength", 63); + + NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder); + + modelBuilder.Entity("Tgstation.Server.Host.Models.ChatBot", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("ChannelLimit") + .HasColumnType("integer"); + + b.Property("ConnectionString") + .IsRequired() + .HasMaxLength(10000) + .HasColumnType("character varying(10000)"); + + b.Property("Enabled") + .HasColumnType("boolean"); + + b.Property("InstanceId") + .HasColumnType("bigint"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("character varying(100)"); + + b.Property("Provider") + .HasColumnType("integer"); + + b.Property("ReconnectionInterval") + .HasColumnType("bigint"); + + b.HasKey("Id"); + + b.HasIndex("InstanceId", "Name") + .IsUnique(); + + b.ToTable("ChatBots"); + }); + + modelBuilder.Entity("Tgstation.Server.Host.Models.ChatChannel", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("ChatSettingsId") + .HasColumnType("bigint"); + + b.Property("DiscordChannelId") + .HasColumnType("numeric(20,0)"); + + b.Property("IrcChannel") + .HasMaxLength(100) + .HasColumnType("character varying(100)"); + + b.Property("IsAdminChannel") + .IsRequired() + .HasColumnType("boolean"); + + b.Property("IsSystemChannel") + .IsRequired() + .HasColumnType("boolean"); + + b.Property("IsUpdatesChannel") + .IsRequired() + .HasColumnType("boolean"); + + b.Property("IsWatchdogChannel") + .IsRequired() + .HasColumnType("boolean"); + + b.Property("Tag") + .HasMaxLength(10000) + .HasColumnType("character varying(10000)"); + + b.HasKey("Id"); + + b.HasIndex("ChatSettingsId", "DiscordChannelId") + .IsUnique(); + + b.HasIndex("ChatSettingsId", "IrcChannel") + .IsUnique(); + + b.ToTable("ChatChannels"); + }); + + modelBuilder.Entity("Tgstation.Server.Host.Models.CompileJob", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("DMApiMajorVersion") + .HasColumnType("integer"); + + b.Property("DMApiMinorVersion") + .HasColumnType("integer"); + + b.Property("DMApiPatchVersion") + .HasColumnType("integer"); + + b.Property("DirectoryName") + .IsRequired() + .HasColumnType("uuid"); + + b.Property("DmeName") + .IsRequired() + .HasColumnType("text"); + + b.Property("EngineVersion") + .IsRequired() + .HasColumnType("text"); + + b.Property("GitHubDeploymentId") + .HasColumnType("integer"); + + b.Property("GitHubRepoId") + .HasColumnType("bigint"); + + b.Property("JobId") + .HasColumnType("bigint"); + + b.Property("MinimumSecurityLevel") + .HasColumnType("integer"); + + b.Property("Output") + .IsRequired() + .HasColumnType("text"); + + b.Property("RepositoryOrigin") + .HasColumnType("text"); + + b.Property("RevisionInformationId") + .HasColumnType("bigint"); + + b.HasKey("Id"); + + b.HasIndex("DirectoryName"); + + b.HasIndex("JobId") + .IsUnique(); + + b.HasIndex("RevisionInformationId"); + + b.ToTable("CompileJobs"); + }); + + modelBuilder.Entity("Tgstation.Server.Host.Models.DreamDaemonSettings", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("AdditionalParameters") + .IsRequired() + .HasMaxLength(10000) + .HasColumnType("character varying(10000)"); + + b.Property("AllowWebClient") + .IsRequired() + .HasColumnType("boolean"); + + b.Property("AutoStart") + .IsRequired() + .HasColumnType("boolean"); + + b.Property("DumpOnHealthCheckRestart") + .IsRequired() + .HasColumnType("boolean"); + + b.Property("HealthCheckSeconds") + .HasColumnType("bigint"); + + b.Property("InstanceId") + .HasColumnType("bigint"); + + b.Property("LogOutput") + .IsRequired() + .HasColumnType("boolean"); + + b.Property("MapThreads") + .HasColumnType("bigint"); + + b.Property("Port") + .HasColumnType("integer"); + + b.Property("SecurityLevel") + .HasColumnType("integer"); + + b.Property("StartProfiler") + .IsRequired() + .HasColumnType("boolean"); + + b.Property("StartupTimeout") + .HasColumnType("bigint"); + + b.Property("TopicRequestTimeout") + .HasColumnType("bigint"); + + b.Property("Visibility") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.HasIndex("InstanceId") + .IsUnique(); + + b.ToTable("DreamDaemonSettings"); + }); + + modelBuilder.Entity("Tgstation.Server.Host.Models.DreamMakerSettings", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("ApiValidationPort") + .HasColumnType("integer"); + + b.Property("ApiValidationSecurityLevel") + .HasColumnType("integer"); + + b.Property("InstanceId") + .HasColumnType("bigint"); + + b.Property("ProjectName") + .HasMaxLength(10000) + .HasColumnType("character varying(10000)"); + + b.Property("RequireDMApiValidation") + .IsRequired() + .HasColumnType("boolean"); + + b.Property("Timeout") + .IsRequired() + .HasColumnType("interval"); + + b.HasKey("Id"); + + b.HasIndex("InstanceId") + .IsUnique(); + + b.ToTable("DreamMakerSettings"); + }); + + modelBuilder.Entity("Tgstation.Server.Host.Models.Instance", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("AutoUpdateInterval") + .HasColumnType("bigint"); + + b.Property("ChatBotLimit") + .HasColumnType("integer"); + + b.Property("ConfigurationType") + .HasColumnType("integer"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("character varying(100)"); + + b.Property("Online") + .IsRequired() + .HasColumnType("boolean"); + + b.Property("Path") + .IsRequired() + .HasColumnType("text"); + + b.Property("SwarmIdentifer") + .HasColumnType("text"); + + b.HasKey("Id"); + + b.HasIndex("Path", "SwarmIdentifer") + .IsUnique(); + + b.ToTable("Instances"); + }); + + modelBuilder.Entity("Tgstation.Server.Host.Models.InstancePermissionSet", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("ChatBotRights") + .HasColumnType("numeric(20,0)"); + + b.Property("ConfigurationRights") + .HasColumnType("numeric(20,0)"); + + b.Property("DreamDaemonRights") + .HasColumnType("numeric(20,0)"); + + b.Property("DreamMakerRights") + .HasColumnType("numeric(20,0)"); + + b.Property("EngineRights") + .HasColumnType("numeric(20,0)"); + + b.Property("InstanceId") + .HasColumnType("bigint"); + + b.Property("InstancePermissionSetRights") + .HasColumnType("numeric(20,0)"); + + b.Property("PermissionSetId") + .HasColumnType("bigint"); + + b.Property("RepositoryRights") + .HasColumnType("numeric(20,0)"); + + b.HasKey("Id"); + + b.HasIndex("InstanceId"); + + b.HasIndex("PermissionSetId", "InstanceId") + .IsUnique(); + + b.ToTable("InstancePermissionSets"); + }); + + modelBuilder.Entity("Tgstation.Server.Host.Models.Job", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("CancelRight") + .HasColumnType("numeric(20,0)"); + + b.Property("CancelRightsType") + .HasColumnType("numeric(20,0)"); + + b.Property("Cancelled") + .IsRequired() + .HasColumnType("boolean"); + + b.Property("CancelledById") + .HasColumnType("bigint"); + + b.Property("Description") + .IsRequired() + .HasColumnType("text"); + + b.Property("ErrorCode") + .HasColumnType("bigint"); + + b.Property("ExceptionDetails") + .HasColumnType("text"); + + b.Property("InstanceId") + .HasColumnType("bigint"); + + b.Property("JobCode") + .HasColumnType("smallint"); + + b.Property("StartedAt") + .IsRequired() + .HasColumnType("timestamp with time zone"); + + b.Property("StartedById") + .HasColumnType("bigint"); + + b.Property("StoppedAt") + .HasColumnType("timestamp with time zone"); + + b.HasKey("Id"); + + b.HasIndex("CancelledById"); + + b.HasIndex("InstanceId"); + + b.HasIndex("StartedById"); + + b.ToTable("Jobs"); + }); + + modelBuilder.Entity("Tgstation.Server.Host.Models.OAuthConnection", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("ExternalUserId") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("character varying(100)"); + + b.Property("Provider") + .HasColumnType("integer"); + + b.Property("UserId") + .HasColumnType("bigint"); + + b.HasKey("Id"); + + b.HasIndex("UserId"); + + b.HasIndex("Provider", "ExternalUserId") + .IsUnique(); + + b.ToTable("OAuthConnections"); + }); + + modelBuilder.Entity("Tgstation.Server.Host.Models.PermissionSet", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("AdministrationRights") + .HasColumnType("numeric(20,0)"); + + b.Property("GroupId") + .HasColumnType("bigint"); + + b.Property("InstanceManagerRights") + .HasColumnType("numeric(20,0)"); + + b.Property("UserId") + .HasColumnType("bigint"); + + b.HasKey("Id"); + + b.HasIndex("GroupId") + .IsUnique(); + + b.HasIndex("UserId") + .IsUnique(); + + b.ToTable("PermissionSets"); + }); + + modelBuilder.Entity("Tgstation.Server.Host.Models.ReattachInformation", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("AccessIdentifier") + .IsRequired() + .HasColumnType("text"); + + b.Property("CompileJobId") + .HasColumnType("bigint"); + + b.Property("InitialCompileJobId") + .HasColumnType("bigint"); + + b.Property("LaunchSecurityLevel") + .HasColumnType("integer"); + + b.Property("LaunchVisibility") + .HasColumnType("integer"); + + b.Property("Port") + .HasColumnType("integer"); + + b.Property("ProcessId") + .HasColumnType("integer"); + + b.Property("RebootState") + .HasColumnType("integer"); + + b.Property("TopicPort") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.HasIndex("CompileJobId"); + + b.HasIndex("InitialCompileJobId"); + + b.ToTable("ReattachInformations"); + }); + + modelBuilder.Entity("Tgstation.Server.Host.Models.RepositorySettings", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("AccessToken") + .HasMaxLength(10000) + .HasColumnType("character varying(10000)"); + + b.Property("AccessUser") + .HasMaxLength(10000) + .HasColumnType("character varying(10000)"); + + b.Property("AutoUpdatesKeepTestMerges") + .IsRequired() + .HasColumnType("boolean"); + + b.Property("AutoUpdatesSynchronize") + .IsRequired() + .HasColumnType("boolean"); + + b.Property("CommitterEmail") + .IsRequired() + .HasMaxLength(10000) + .HasColumnType("character varying(10000)"); + + b.Property("CommitterName") + .IsRequired() + .HasMaxLength(10000) + .HasColumnType("character varying(10000)"); + + b.Property("CreateGitHubDeployments") + .IsRequired() + .HasColumnType("boolean"); + + b.Property("InstanceId") + .HasColumnType("bigint"); + + b.Property("PostTestMergeComment") + .IsRequired() + .HasColumnType("boolean"); + + b.Property("PushTestMergeCommits") + .IsRequired() + .HasColumnType("boolean"); + + b.Property("ShowTestMergeCommitters") + .IsRequired() + .HasColumnType("boolean"); + + b.Property("UpdateSubmodules") + .IsRequired() + .HasColumnType("boolean"); + + b.HasKey("Id"); + + b.HasIndex("InstanceId") + .IsUnique(); + + b.ToTable("RepositorySettings"); + }); + + modelBuilder.Entity("Tgstation.Server.Host.Models.RevInfoTestMerge", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("RevisionInformationId") + .HasColumnType("bigint"); + + b.Property("TestMergeId") + .HasColumnType("bigint"); + + b.HasKey("Id"); + + b.HasIndex("RevisionInformationId"); + + b.HasIndex("TestMergeId"); + + b.ToTable("RevInfoTestMerges"); + }); + + modelBuilder.Entity("Tgstation.Server.Host.Models.RevisionInformation", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("CommitSha") + .IsRequired() + .HasMaxLength(40) + .HasColumnType("character varying(40)"); + + b.Property("InstanceId") + .HasColumnType("bigint"); + + b.Property("OriginCommitSha") + .IsRequired() + .HasMaxLength(40) + .HasColumnType("character varying(40)"); + + b.Property("Timestamp") + .HasColumnType("timestamp with time zone"); + + b.HasKey("Id"); + + b.HasIndex("InstanceId", "CommitSha") + .IsUnique(); + + b.ToTable("RevisionInformations"); + }); + + modelBuilder.Entity("Tgstation.Server.Host.Models.TestMerge", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("Author") + .IsRequired() + .HasColumnType("text"); + + b.Property("BodyAtMerge") + .IsRequired() + .HasColumnType("text"); + + b.Property("Comment") + .HasMaxLength(10000) + .HasColumnType("character varying(10000)"); + + b.Property("MergedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("MergedById") + .HasColumnType("bigint"); + + b.Property("Number") + .HasColumnType("integer"); + + b.Property("PrimaryRevisionInformationId") + .IsRequired() + .HasColumnType("bigint"); + + b.Property("TargetCommitSha") + .IsRequired() + .HasMaxLength(40) + .HasColumnType("character varying(40)"); + + b.Property("TitleAtMerge") + .IsRequired() + .HasColumnType("text"); + + b.Property("Url") + .IsRequired() + .HasColumnType("text"); + + b.HasKey("Id"); + + b.HasIndex("MergedById"); + + b.HasIndex("PrimaryRevisionInformationId") + .IsUnique(); + + b.ToTable("TestMerges"); + }); + + modelBuilder.Entity("Tgstation.Server.Host.Models.User", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("CanonicalName") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("character varying(100)"); + + b.Property("CreatedAt") + .IsRequired() + .HasColumnType("timestamp with time zone"); + + b.Property("CreatedById") + .HasColumnType("bigint"); + + b.Property("Enabled") + .IsRequired() + .HasColumnType("boolean"); + + b.Property("GroupId") + .HasColumnType("bigint"); + + b.Property("LastPasswordUpdate") + .HasColumnType("timestamp with time zone"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("character varying(100)"); + + b.Property("PasswordHash") + .HasColumnType("text"); + + b.Property("SystemIdentifier") + .HasMaxLength(100) + .HasColumnType("character varying(100)"); + + b.HasKey("Id"); + + b.HasIndex("CanonicalName") + .IsUnique(); + + b.HasIndex("CreatedById"); + + b.HasIndex("GroupId"); + + b.HasIndex("SystemIdentifier") + .IsUnique(); + + b.ToTable("Users"); + }); + + modelBuilder.Entity("Tgstation.Server.Host.Models.UserGroup", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("Name") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("character varying(100)"); + + b.HasKey("Id"); + + b.HasIndex("Name") + .IsUnique(); + + b.ToTable("Groups"); + }); + + modelBuilder.Entity("Tgstation.Server.Host.Models.ChatBot", b => + { + b.HasOne("Tgstation.Server.Host.Models.Instance", "Instance") + .WithMany("ChatSettings") + .HasForeignKey("InstanceId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Instance"); + }); + + modelBuilder.Entity("Tgstation.Server.Host.Models.ChatChannel", b => + { + b.HasOne("Tgstation.Server.Host.Models.ChatBot", "ChatSettings") + .WithMany("Channels") + .HasForeignKey("ChatSettingsId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("ChatSettings"); + }); + + modelBuilder.Entity("Tgstation.Server.Host.Models.CompileJob", b => + { + b.HasOne("Tgstation.Server.Host.Models.Job", "Job") + .WithOne() + .HasForeignKey("Tgstation.Server.Host.Models.CompileJob", "JobId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Tgstation.Server.Host.Models.RevisionInformation", "RevisionInformation") + .WithMany("CompileJobs") + .HasForeignKey("RevisionInformationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Job"); + + b.Navigation("RevisionInformation"); + }); + + modelBuilder.Entity("Tgstation.Server.Host.Models.DreamDaemonSettings", b => + { + b.HasOne("Tgstation.Server.Host.Models.Instance", "Instance") + .WithOne("DreamDaemonSettings") + .HasForeignKey("Tgstation.Server.Host.Models.DreamDaemonSettings", "InstanceId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Instance"); + }); + + modelBuilder.Entity("Tgstation.Server.Host.Models.DreamMakerSettings", b => + { + b.HasOne("Tgstation.Server.Host.Models.Instance", "Instance") + .WithOne("DreamMakerSettings") + .HasForeignKey("Tgstation.Server.Host.Models.DreamMakerSettings", "InstanceId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Instance"); + }); + + modelBuilder.Entity("Tgstation.Server.Host.Models.InstancePermissionSet", b => + { + b.HasOne("Tgstation.Server.Host.Models.Instance", "Instance") + .WithMany("InstancePermissionSets") + .HasForeignKey("InstanceId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Tgstation.Server.Host.Models.PermissionSet", "PermissionSet") + .WithMany("InstancePermissionSets") + .HasForeignKey("PermissionSetId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Instance"); + + b.Navigation("PermissionSet"); + }); + + modelBuilder.Entity("Tgstation.Server.Host.Models.Job", b => + { + b.HasOne("Tgstation.Server.Host.Models.User", "CancelledBy") + .WithMany() + .HasForeignKey("CancelledById"); + + b.HasOne("Tgstation.Server.Host.Models.Instance", "Instance") + .WithMany("Jobs") + .HasForeignKey("InstanceId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Tgstation.Server.Host.Models.User", "StartedBy") + .WithMany() + .HasForeignKey("StartedById") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("CancelledBy"); + + b.Navigation("Instance"); + + b.Navigation("StartedBy"); + }); + + modelBuilder.Entity("Tgstation.Server.Host.Models.OAuthConnection", b => + { + b.HasOne("Tgstation.Server.Host.Models.User", "User") + .WithMany("OAuthConnections") + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Tgstation.Server.Host.Models.PermissionSet", b => + { + b.HasOne("Tgstation.Server.Host.Models.UserGroup", "Group") + .WithOne("PermissionSet") + .HasForeignKey("Tgstation.Server.Host.Models.PermissionSet", "GroupId") + .OnDelete(DeleteBehavior.Cascade); + + b.HasOne("Tgstation.Server.Host.Models.User", "User") + .WithOne("PermissionSet") + .HasForeignKey("Tgstation.Server.Host.Models.PermissionSet", "UserId") + .OnDelete(DeleteBehavior.Cascade); + + b.Navigation("Group"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Tgstation.Server.Host.Models.ReattachInformation", b => + { + b.HasOne("Tgstation.Server.Host.Models.CompileJob", "CompileJob") + .WithMany() + .HasForeignKey("CompileJobId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Tgstation.Server.Host.Models.CompileJob", "InitialCompileJob") + .WithMany() + .HasForeignKey("InitialCompileJobId"); + + b.Navigation("CompileJob"); + + b.Navigation("InitialCompileJob"); + }); + + modelBuilder.Entity("Tgstation.Server.Host.Models.RepositorySettings", b => + { + b.HasOne("Tgstation.Server.Host.Models.Instance", "Instance") + .WithOne("RepositorySettings") + .HasForeignKey("Tgstation.Server.Host.Models.RepositorySettings", "InstanceId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Instance"); + }); + + modelBuilder.Entity("Tgstation.Server.Host.Models.RevInfoTestMerge", b => + { + b.HasOne("Tgstation.Server.Host.Models.RevisionInformation", "RevisionInformation") + .WithMany("ActiveTestMerges") + .HasForeignKey("RevisionInformationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Tgstation.Server.Host.Models.TestMerge", "TestMerge") + .WithMany("RevisonInformations") + .HasForeignKey("TestMergeId") + .OnDelete(DeleteBehavior.ClientNoAction) + .IsRequired(); + + b.Navigation("RevisionInformation"); + + b.Navigation("TestMerge"); + }); + + modelBuilder.Entity("Tgstation.Server.Host.Models.RevisionInformation", b => + { + b.HasOne("Tgstation.Server.Host.Models.Instance", "Instance") + .WithMany("RevisionInformations") + .HasForeignKey("InstanceId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Instance"); + }); + + modelBuilder.Entity("Tgstation.Server.Host.Models.TestMerge", b => + { + b.HasOne("Tgstation.Server.Host.Models.User", "MergedBy") + .WithMany("TestMerges") + .HasForeignKey("MergedById") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("Tgstation.Server.Host.Models.RevisionInformation", "PrimaryRevisionInformation") + .WithOne("PrimaryTestMerge") + .HasForeignKey("Tgstation.Server.Host.Models.TestMerge", "PrimaryRevisionInformationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("MergedBy"); + + b.Navigation("PrimaryRevisionInformation"); + }); + + modelBuilder.Entity("Tgstation.Server.Host.Models.User", b => + { + b.HasOne("Tgstation.Server.Host.Models.User", "CreatedBy") + .WithMany("CreatedUsers") + .HasForeignKey("CreatedById"); + + b.HasOne("Tgstation.Server.Host.Models.UserGroup", "Group") + .WithMany("Users") + .HasForeignKey("GroupId"); + + b.Navigation("CreatedBy"); + + b.Navigation("Group"); + }); + + modelBuilder.Entity("Tgstation.Server.Host.Models.ChatBot", b => + { + b.Navigation("Channels"); + }); + + modelBuilder.Entity("Tgstation.Server.Host.Models.Instance", b => + { + b.Navigation("ChatSettings"); + + b.Navigation("DreamDaemonSettings"); + + b.Navigation("DreamMakerSettings"); + + b.Navigation("InstancePermissionSets"); + + b.Navigation("Jobs"); + + b.Navigation("RepositorySettings"); + + b.Navigation("RevisionInformations"); + }); + + modelBuilder.Entity("Tgstation.Server.Host.Models.PermissionSet", b => + { + b.Navigation("InstancePermissionSets"); + }); + + modelBuilder.Entity("Tgstation.Server.Host.Models.RevisionInformation", b => + { + b.Navigation("ActiveTestMerges"); + + b.Navigation("CompileJobs"); + + b.Navigation("PrimaryTestMerge"); + }); + + modelBuilder.Entity("Tgstation.Server.Host.Models.TestMerge", b => + { + b.Navigation("RevisonInformations"); + }); + + modelBuilder.Entity("Tgstation.Server.Host.Models.User", b => + { + b.Navigation("CreatedUsers"); + + b.Navigation("OAuthConnections"); + + b.Navigation("PermissionSet"); + + b.Navigation("TestMerges"); + }); + + modelBuilder.Entity("Tgstation.Server.Host.Models.UserGroup", b => + { + b.Navigation("PermissionSet") + .IsRequired(); + + b.Navigation("Users"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/src/Tgstation.Server.Host/Database/Migrations/20231220032521_PGAddTopicPort.cs b/src/Tgstation.Server.Host/Database/Migrations/20231220032521_PGAddTopicPort.cs new file mode 100644 index 0000000000..1994a0b3f9 --- /dev/null +++ b/src/Tgstation.Server.Host/Database/Migrations/20231220032521_PGAddTopicPort.cs @@ -0,0 +1,32 @@ +using System; + +using Microsoft.EntityFrameworkCore.Migrations; + +namespace Tgstation.Server.Host.Database.Migrations +{ + /// + public partial class PGAddTopicPort : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + ArgumentNullException.ThrowIfNull(migrationBuilder); + + migrationBuilder.AddColumn( + name: "TopicPort", + table: "ReattachInformations", + type: "integer", + nullable: true); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + ArgumentNullException.ThrowIfNull(migrationBuilder); + + migrationBuilder.DropColumn( + name: "TopicPort", + table: "ReattachInformations"); + } + } +} diff --git a/src/Tgstation.Server.Host/Database/Migrations/20231220032528_SLAddTopicPort.Designer.cs b/src/Tgstation.Server.Host/Database/Migrations/20231220032528_SLAddTopicPort.Designer.cs new file mode 100644 index 0000000000..1352af4855 --- /dev/null +++ b/src/Tgstation.Server.Host/Database/Migrations/20231220032528_SLAddTopicPort.Designer.cs @@ -0,0 +1,1042 @@ +// +using System; + +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Migrations; + +namespace Tgstation.Server.Host.Database.Migrations +{ + [DbContext(typeof(SqliteDatabaseContext))] + [Migration("20231220032528_SLAddTopicPort")] + partial class SLAddTopicPort + { + /// + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder.HasAnnotation("ProductVersion", "8.0.0"); + + modelBuilder.Entity("Tgstation.Server.Host.Models.ChatBot", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("ChannelLimit") + .IsRequired() + .HasColumnType("INTEGER"); + + b.Property("ConnectionString") + .IsRequired() + .HasMaxLength(10000) + .HasColumnType("TEXT"); + + b.Property("Enabled") + .HasColumnType("INTEGER"); + + b.Property("InstanceId") + .HasColumnType("INTEGER"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("TEXT"); + + b.Property("Provider") + .HasColumnType("INTEGER"); + + b.Property("ReconnectionInterval") + .IsRequired() + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("InstanceId", "Name") + .IsUnique(); + + b.ToTable("ChatBots"); + }); + + modelBuilder.Entity("Tgstation.Server.Host.Models.ChatChannel", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("ChatSettingsId") + .HasColumnType("INTEGER"); + + b.Property("DiscordChannelId") + .HasColumnType("INTEGER"); + + b.Property("IrcChannel") + .HasMaxLength(100) + .HasColumnType("TEXT"); + + b.Property("IsAdminChannel") + .IsRequired() + .HasColumnType("INTEGER"); + + b.Property("IsSystemChannel") + .IsRequired() + .HasColumnType("INTEGER"); + + b.Property("IsUpdatesChannel") + .IsRequired() + .HasColumnType("INTEGER"); + + b.Property("IsWatchdogChannel") + .IsRequired() + .HasColumnType("INTEGER"); + + b.Property("Tag") + .HasMaxLength(10000) + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("ChatSettingsId", "DiscordChannelId") + .IsUnique(); + + b.HasIndex("ChatSettingsId", "IrcChannel") + .IsUnique(); + + b.ToTable("ChatChannels"); + }); + + modelBuilder.Entity("Tgstation.Server.Host.Models.CompileJob", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("DMApiMajorVersion") + .HasColumnType("INTEGER"); + + b.Property("DMApiMinorVersion") + .HasColumnType("INTEGER"); + + b.Property("DMApiPatchVersion") + .HasColumnType("INTEGER"); + + b.Property("DirectoryName") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("DmeName") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("EngineVersion") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("GitHubDeploymentId") + .HasColumnType("INTEGER"); + + b.Property("GitHubRepoId") + .HasColumnType("INTEGER"); + + b.Property("JobId") + .HasColumnType("INTEGER"); + + b.Property("MinimumSecurityLevel") + .HasColumnType("INTEGER"); + + b.Property("Output") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("RepositoryOrigin") + .HasColumnType("TEXT"); + + b.Property("RevisionInformationId") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("DirectoryName"); + + b.HasIndex("JobId") + .IsUnique(); + + b.HasIndex("RevisionInformationId"); + + b.ToTable("CompileJobs"); + }); + + modelBuilder.Entity("Tgstation.Server.Host.Models.DreamDaemonSettings", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("AdditionalParameters") + .IsRequired() + .HasMaxLength(10000) + .HasColumnType("TEXT"); + + b.Property("AllowWebClient") + .IsRequired() + .HasColumnType("INTEGER"); + + b.Property("AutoStart") + .IsRequired() + .HasColumnType("INTEGER"); + + b.Property("DumpOnHealthCheckRestart") + .IsRequired() + .HasColumnType("INTEGER"); + + b.Property("HealthCheckSeconds") + .IsRequired() + .HasColumnType("INTEGER"); + + b.Property("InstanceId") + .HasColumnType("INTEGER"); + + b.Property("LogOutput") + .IsRequired() + .HasColumnType("INTEGER"); + + b.Property("MapThreads") + .IsRequired() + .HasColumnType("INTEGER"); + + b.Property("Port") + .IsRequired() + .HasColumnType("INTEGER"); + + b.Property("SecurityLevel") + .HasColumnType("INTEGER"); + + b.Property("StartProfiler") + .IsRequired() + .HasColumnType("INTEGER"); + + b.Property("StartupTimeout") + .IsRequired() + .HasColumnType("INTEGER"); + + b.Property("TopicRequestTimeout") + .IsRequired() + .HasColumnType("INTEGER"); + + b.Property("Visibility") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("InstanceId") + .IsUnique(); + + b.ToTable("DreamDaemonSettings"); + }); + + modelBuilder.Entity("Tgstation.Server.Host.Models.DreamMakerSettings", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("ApiValidationPort") + .IsRequired() + .HasColumnType("INTEGER"); + + b.Property("ApiValidationSecurityLevel") + .HasColumnType("INTEGER"); + + b.Property("InstanceId") + .HasColumnType("INTEGER"); + + b.Property("ProjectName") + .HasMaxLength(10000) + .HasColumnType("TEXT"); + + b.Property("RequireDMApiValidation") + .IsRequired() + .HasColumnType("INTEGER"); + + b.Property("Timeout") + .IsRequired() + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("InstanceId") + .IsUnique(); + + b.ToTable("DreamMakerSettings"); + }); + + modelBuilder.Entity("Tgstation.Server.Host.Models.Instance", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("AutoUpdateInterval") + .IsRequired() + .HasColumnType("INTEGER"); + + b.Property("ChatBotLimit") + .IsRequired() + .HasColumnType("INTEGER"); + + b.Property("ConfigurationType") + .HasColumnType("INTEGER"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("TEXT"); + + b.Property("Online") + .IsRequired() + .HasColumnType("INTEGER"); + + b.Property("Path") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("SwarmIdentifer") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("Path", "SwarmIdentifer") + .IsUnique(); + + b.ToTable("Instances"); + }); + + modelBuilder.Entity("Tgstation.Server.Host.Models.InstancePermissionSet", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("ChatBotRights") + .HasColumnType("INTEGER"); + + b.Property("ConfigurationRights") + .HasColumnType("INTEGER"); + + b.Property("DreamDaemonRights") + .HasColumnType("INTEGER"); + + b.Property("DreamMakerRights") + .HasColumnType("INTEGER"); + + b.Property("EngineRights") + .HasColumnType("INTEGER"); + + b.Property("InstanceId") + .HasColumnType("INTEGER"); + + b.Property("InstancePermissionSetRights") + .HasColumnType("INTEGER"); + + b.Property("PermissionSetId") + .HasColumnType("INTEGER"); + + b.Property("RepositoryRights") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("InstanceId"); + + b.HasIndex("PermissionSetId", "InstanceId") + .IsUnique(); + + b.ToTable("InstancePermissionSets"); + }); + + modelBuilder.Entity("Tgstation.Server.Host.Models.Job", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("CancelRight") + .HasColumnType("INTEGER"); + + b.Property("CancelRightsType") + .HasColumnType("INTEGER"); + + b.Property("Cancelled") + .IsRequired() + .HasColumnType("INTEGER"); + + b.Property("CancelledById") + .HasColumnType("INTEGER"); + + b.Property("Description") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("ErrorCode") + .HasColumnType("INTEGER"); + + b.Property("ExceptionDetails") + .HasColumnType("TEXT"); + + b.Property("InstanceId") + .HasColumnType("INTEGER"); + + b.Property("JobCode") + .HasColumnType("INTEGER"); + + b.Property("StartedAt") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("StartedById") + .HasColumnType("INTEGER"); + + b.Property("StoppedAt") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("CancelledById"); + + b.HasIndex("InstanceId"); + + b.HasIndex("StartedById"); + + b.ToTable("Jobs"); + }); + + modelBuilder.Entity("Tgstation.Server.Host.Models.OAuthConnection", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("ExternalUserId") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("TEXT"); + + b.Property("Provider") + .HasColumnType("INTEGER"); + + b.Property("UserId") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("UserId"); + + b.HasIndex("Provider", "ExternalUserId") + .IsUnique(); + + b.ToTable("OAuthConnections"); + }); + + modelBuilder.Entity("Tgstation.Server.Host.Models.PermissionSet", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("AdministrationRights") + .HasColumnType("INTEGER"); + + b.Property("GroupId") + .HasColumnType("INTEGER"); + + b.Property("InstanceManagerRights") + .HasColumnType("INTEGER"); + + b.Property("UserId") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("GroupId") + .IsUnique(); + + b.HasIndex("UserId") + .IsUnique(); + + b.ToTable("PermissionSets"); + }); + + modelBuilder.Entity("Tgstation.Server.Host.Models.ReattachInformation", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("AccessIdentifier") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("CompileJobId") + .HasColumnType("INTEGER"); + + b.Property("InitialCompileJobId") + .HasColumnType("INTEGER"); + + b.Property("LaunchSecurityLevel") + .HasColumnType("INTEGER"); + + b.Property("LaunchVisibility") + .HasColumnType("INTEGER"); + + b.Property("Port") + .HasColumnType("INTEGER"); + + b.Property("ProcessId") + .HasColumnType("INTEGER"); + + b.Property("RebootState") + .HasColumnType("INTEGER"); + + b.Property("TopicPort") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("CompileJobId"); + + b.HasIndex("InitialCompileJobId"); + + b.ToTable("ReattachInformations"); + }); + + modelBuilder.Entity("Tgstation.Server.Host.Models.RepositorySettings", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("AccessToken") + .HasMaxLength(10000) + .HasColumnType("TEXT"); + + b.Property("AccessUser") + .HasMaxLength(10000) + .HasColumnType("TEXT"); + + b.Property("AutoUpdatesKeepTestMerges") + .IsRequired() + .HasColumnType("INTEGER"); + + b.Property("AutoUpdatesSynchronize") + .IsRequired() + .HasColumnType("INTEGER"); + + b.Property("CommitterEmail") + .IsRequired() + .HasMaxLength(10000) + .HasColumnType("TEXT"); + + b.Property("CommitterName") + .IsRequired() + .HasMaxLength(10000) + .HasColumnType("TEXT"); + + b.Property("CreateGitHubDeployments") + .IsRequired() + .HasColumnType("INTEGER"); + + b.Property("InstanceId") + .HasColumnType("INTEGER"); + + b.Property("PostTestMergeComment") + .IsRequired() + .HasColumnType("INTEGER"); + + b.Property("PushTestMergeCommits") + .IsRequired() + .HasColumnType("INTEGER"); + + b.Property("ShowTestMergeCommitters") + .IsRequired() + .HasColumnType("INTEGER"); + + b.Property("UpdateSubmodules") + .IsRequired() + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("InstanceId") + .IsUnique(); + + b.ToTable("RepositorySettings"); + }); + + modelBuilder.Entity("Tgstation.Server.Host.Models.RevInfoTestMerge", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("RevisionInformationId") + .HasColumnType("INTEGER"); + + b.Property("TestMergeId") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("RevisionInformationId"); + + b.HasIndex("TestMergeId"); + + b.ToTable("RevInfoTestMerges"); + }); + + modelBuilder.Entity("Tgstation.Server.Host.Models.RevisionInformation", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("CommitSha") + .IsRequired() + .HasMaxLength(40) + .HasColumnType("TEXT"); + + b.Property("InstanceId") + .HasColumnType("INTEGER"); + + b.Property("OriginCommitSha") + .IsRequired() + .HasMaxLength(40) + .HasColumnType("TEXT"); + + b.Property("Timestamp") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("InstanceId", "CommitSha") + .IsUnique(); + + b.ToTable("RevisionInformations"); + }); + + modelBuilder.Entity("Tgstation.Server.Host.Models.TestMerge", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("Author") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("BodyAtMerge") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("Comment") + .HasMaxLength(10000) + .HasColumnType("TEXT"); + + b.Property("MergedAt") + .HasColumnType("TEXT"); + + b.Property("MergedById") + .HasColumnType("INTEGER"); + + b.Property("Number") + .HasColumnType("INTEGER"); + + b.Property("PrimaryRevisionInformationId") + .IsRequired() + .HasColumnType("INTEGER"); + + b.Property("TargetCommitSha") + .IsRequired() + .HasMaxLength(40) + .HasColumnType("TEXT"); + + b.Property("TitleAtMerge") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("Url") + .IsRequired() + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("MergedById"); + + b.HasIndex("PrimaryRevisionInformationId") + .IsUnique(); + + b.ToTable("TestMerges"); + }); + + modelBuilder.Entity("Tgstation.Server.Host.Models.User", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("CanonicalName") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("TEXT"); + + b.Property("CreatedAt") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("CreatedById") + .HasColumnType("INTEGER"); + + b.Property("Enabled") + .IsRequired() + .HasColumnType("INTEGER"); + + b.Property("GroupId") + .HasColumnType("INTEGER"); + + b.Property("LastPasswordUpdate") + .HasColumnType("TEXT"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("TEXT"); + + b.Property("PasswordHash") + .HasColumnType("TEXT"); + + b.Property("SystemIdentifier") + .HasMaxLength(100) + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("CanonicalName") + .IsUnique(); + + b.HasIndex("CreatedById"); + + b.HasIndex("GroupId"); + + b.HasIndex("SystemIdentifier") + .IsUnique(); + + b.ToTable("Users"); + }); + + modelBuilder.Entity("Tgstation.Server.Host.Models.UserGroup", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("Name") + .IsUnique(); + + b.ToTable("Groups"); + }); + + modelBuilder.Entity("Tgstation.Server.Host.Models.ChatBot", b => + { + b.HasOne("Tgstation.Server.Host.Models.Instance", "Instance") + .WithMany("ChatSettings") + .HasForeignKey("InstanceId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Instance"); + }); + + modelBuilder.Entity("Tgstation.Server.Host.Models.ChatChannel", b => + { + b.HasOne("Tgstation.Server.Host.Models.ChatBot", "ChatSettings") + .WithMany("Channels") + .HasForeignKey("ChatSettingsId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("ChatSettings"); + }); + + modelBuilder.Entity("Tgstation.Server.Host.Models.CompileJob", b => + { + b.HasOne("Tgstation.Server.Host.Models.Job", "Job") + .WithOne() + .HasForeignKey("Tgstation.Server.Host.Models.CompileJob", "JobId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Tgstation.Server.Host.Models.RevisionInformation", "RevisionInformation") + .WithMany("CompileJobs") + .HasForeignKey("RevisionInformationId") + .OnDelete(DeleteBehavior.ClientNoAction) + .IsRequired(); + + b.Navigation("Job"); + + b.Navigation("RevisionInformation"); + }); + + modelBuilder.Entity("Tgstation.Server.Host.Models.DreamDaemonSettings", b => + { + b.HasOne("Tgstation.Server.Host.Models.Instance", "Instance") + .WithOne("DreamDaemonSettings") + .HasForeignKey("Tgstation.Server.Host.Models.DreamDaemonSettings", "InstanceId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Instance"); + }); + + modelBuilder.Entity("Tgstation.Server.Host.Models.DreamMakerSettings", b => + { + b.HasOne("Tgstation.Server.Host.Models.Instance", "Instance") + .WithOne("DreamMakerSettings") + .HasForeignKey("Tgstation.Server.Host.Models.DreamMakerSettings", "InstanceId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Instance"); + }); + + modelBuilder.Entity("Tgstation.Server.Host.Models.InstancePermissionSet", b => + { + b.HasOne("Tgstation.Server.Host.Models.Instance", "Instance") + .WithMany("InstancePermissionSets") + .HasForeignKey("InstanceId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Tgstation.Server.Host.Models.PermissionSet", "PermissionSet") + .WithMany("InstancePermissionSets") + .HasForeignKey("PermissionSetId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Instance"); + + b.Navigation("PermissionSet"); + }); + + modelBuilder.Entity("Tgstation.Server.Host.Models.Job", b => + { + b.HasOne("Tgstation.Server.Host.Models.User", "CancelledBy") + .WithMany() + .HasForeignKey("CancelledById"); + + b.HasOne("Tgstation.Server.Host.Models.Instance", "Instance") + .WithMany("Jobs") + .HasForeignKey("InstanceId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Tgstation.Server.Host.Models.User", "StartedBy") + .WithMany() + .HasForeignKey("StartedById") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("CancelledBy"); + + b.Navigation("Instance"); + + b.Navigation("StartedBy"); + }); + + modelBuilder.Entity("Tgstation.Server.Host.Models.OAuthConnection", b => + { + b.HasOne("Tgstation.Server.Host.Models.User", "User") + .WithMany("OAuthConnections") + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Tgstation.Server.Host.Models.PermissionSet", b => + { + b.HasOne("Tgstation.Server.Host.Models.UserGroup", "Group") + .WithOne("PermissionSet") + .HasForeignKey("Tgstation.Server.Host.Models.PermissionSet", "GroupId") + .OnDelete(DeleteBehavior.Cascade); + + b.HasOne("Tgstation.Server.Host.Models.User", "User") + .WithOne("PermissionSet") + .HasForeignKey("Tgstation.Server.Host.Models.PermissionSet", "UserId") + .OnDelete(DeleteBehavior.Cascade); + + b.Navigation("Group"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Tgstation.Server.Host.Models.ReattachInformation", b => + { + b.HasOne("Tgstation.Server.Host.Models.CompileJob", "CompileJob") + .WithMany() + .HasForeignKey("CompileJobId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Tgstation.Server.Host.Models.CompileJob", "InitialCompileJob") + .WithMany() + .HasForeignKey("InitialCompileJobId"); + + b.Navigation("CompileJob"); + + b.Navigation("InitialCompileJob"); + }); + + modelBuilder.Entity("Tgstation.Server.Host.Models.RepositorySettings", b => + { + b.HasOne("Tgstation.Server.Host.Models.Instance", "Instance") + .WithOne("RepositorySettings") + .HasForeignKey("Tgstation.Server.Host.Models.RepositorySettings", "InstanceId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Instance"); + }); + + modelBuilder.Entity("Tgstation.Server.Host.Models.RevInfoTestMerge", b => + { + b.HasOne("Tgstation.Server.Host.Models.RevisionInformation", "RevisionInformation") + .WithMany("ActiveTestMerges") + .HasForeignKey("RevisionInformationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Tgstation.Server.Host.Models.TestMerge", "TestMerge") + .WithMany("RevisonInformations") + .HasForeignKey("TestMergeId") + .OnDelete(DeleteBehavior.ClientNoAction) + .IsRequired(); + + b.Navigation("RevisionInformation"); + + b.Navigation("TestMerge"); + }); + + modelBuilder.Entity("Tgstation.Server.Host.Models.RevisionInformation", b => + { + b.HasOne("Tgstation.Server.Host.Models.Instance", "Instance") + .WithMany("RevisionInformations") + .HasForeignKey("InstanceId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Instance"); + }); + + modelBuilder.Entity("Tgstation.Server.Host.Models.TestMerge", b => + { + b.HasOne("Tgstation.Server.Host.Models.User", "MergedBy") + .WithMany("TestMerges") + .HasForeignKey("MergedById") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("Tgstation.Server.Host.Models.RevisionInformation", "PrimaryRevisionInformation") + .WithOne("PrimaryTestMerge") + .HasForeignKey("Tgstation.Server.Host.Models.TestMerge", "PrimaryRevisionInformationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("MergedBy"); + + b.Navigation("PrimaryRevisionInformation"); + }); + + modelBuilder.Entity("Tgstation.Server.Host.Models.User", b => + { + b.HasOne("Tgstation.Server.Host.Models.User", "CreatedBy") + .WithMany("CreatedUsers") + .HasForeignKey("CreatedById"); + + b.HasOne("Tgstation.Server.Host.Models.UserGroup", "Group") + .WithMany("Users") + .HasForeignKey("GroupId"); + + b.Navigation("CreatedBy"); + + b.Navigation("Group"); + }); + + modelBuilder.Entity("Tgstation.Server.Host.Models.ChatBot", b => + { + b.Navigation("Channels"); + }); + + modelBuilder.Entity("Tgstation.Server.Host.Models.Instance", b => + { + b.Navigation("ChatSettings"); + + b.Navigation("DreamDaemonSettings"); + + b.Navigation("DreamMakerSettings"); + + b.Navigation("InstancePermissionSets"); + + b.Navigation("Jobs"); + + b.Navigation("RepositorySettings"); + + b.Navigation("RevisionInformations"); + }); + + modelBuilder.Entity("Tgstation.Server.Host.Models.PermissionSet", b => + { + b.Navigation("InstancePermissionSets"); + }); + + modelBuilder.Entity("Tgstation.Server.Host.Models.RevisionInformation", b => + { + b.Navigation("ActiveTestMerges"); + + b.Navigation("CompileJobs"); + + b.Navigation("PrimaryTestMerge"); + }); + + modelBuilder.Entity("Tgstation.Server.Host.Models.TestMerge", b => + { + b.Navigation("RevisonInformations"); + }); + + modelBuilder.Entity("Tgstation.Server.Host.Models.User", b => + { + b.Navigation("CreatedUsers"); + + b.Navigation("OAuthConnections"); + + b.Navigation("PermissionSet"); + + b.Navigation("TestMerges"); + }); + + modelBuilder.Entity("Tgstation.Server.Host.Models.UserGroup", b => + { + b.Navigation("PermissionSet") + .IsRequired(); + + b.Navigation("Users"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/src/Tgstation.Server.Host/Database/Migrations/20231220032528_SLAddTopicPort.cs b/src/Tgstation.Server.Host/Database/Migrations/20231220032528_SLAddTopicPort.cs new file mode 100644 index 0000000000..7ea822bd2a --- /dev/null +++ b/src/Tgstation.Server.Host/Database/Migrations/20231220032528_SLAddTopicPort.cs @@ -0,0 +1,32 @@ +using System; + +using Microsoft.EntityFrameworkCore.Migrations; + +namespace Tgstation.Server.Host.Database.Migrations +{ + /// + public partial class SLAddTopicPort : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + ArgumentNullException.ThrowIfNull(migrationBuilder); + + migrationBuilder.AddColumn( + name: "TopicPort", + table: "ReattachInformations", + type: "INTEGER", + nullable: true); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + ArgumentNullException.ThrowIfNull(migrationBuilder); + + migrationBuilder.DropColumn( + name: "TopicPort", + table: "ReattachInformations"); + } + } +} diff --git a/src/Tgstation.Server.Host/Database/Migrations/MySqlDatabaseContextModelSnapshot.cs b/src/Tgstation.Server.Host/Database/Migrations/MySqlDatabaseContextModelSnapshot.cs index 713ce53b72..1f42bd3856 100644 --- a/src/Tgstation.Server.Host/Database/Migrations/MySqlDatabaseContextModelSnapshot.cs +++ b/src/Tgstation.Server.Host/Database/Migrations/MySqlDatabaseContextModelSnapshot.cs @@ -4,8 +4,6 @@ using System; using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore.Infrastructure; -#nullable disable - namespace Tgstation.Server.Host.Database.Migrations { [DbContext(typeof(MySqlDatabaseContext))] @@ -15,7 +13,7 @@ namespace Tgstation.Server.Host.Database.Migrations { #pragma warning disable 612, 618 modelBuilder - .HasAnnotation("ProductVersion", "8.0.0-rc.1.23419.6") + .HasAnnotation("ProductVersion", "8.0.0") .HasAnnotation("Relational:MaxIdentifierLength", 64); modelBuilder.Entity("Tgstation.Server.Host.Models.ChatBot", b => @@ -118,13 +116,6 @@ namespace Tgstation.Server.Host.Database.Migrations .ValueGeneratedOnAdd() .HasColumnType("bigint"); - b.Property("ByondVersion") - .IsRequired() - .HasColumnType("longtext") - .HasColumnName("EngineVersion"); - - MySqlPropertyBuilderExtensions.HasCharSet(b.Property("ByondVersion"), "utf8mb4"); - b.Property("DMApiMajorVersion") .HasColumnType("int"); @@ -144,6 +135,12 @@ namespace Tgstation.Server.Host.Database.Migrations MySqlPropertyBuilderExtensions.HasCharSet(b.Property("DmeName"), "utf8mb4"); + b.Property("EngineVersion") + .IsRequired() + .HasColumnType("longtext"); + + MySqlPropertyBuilderExtensions.HasCharSet(b.Property("EngineVersion"), "utf8mb4"); + b.Property("GitHubDeploymentId") .HasColumnType("int"); @@ -500,7 +497,7 @@ namespace Tgstation.Server.Host.Database.Migrations modelBuilder.Entity("Tgstation.Server.Host.Models.ReattachInformation", b => { - b.Property("Id") + b.Property("Id") .ValueGeneratedOnAdd() .HasColumnType("bigint"); @@ -531,6 +528,9 @@ namespace Tgstation.Server.Host.Database.Migrations b.Property("RebootState") .HasColumnType("int"); + b.Property("TopicPort") + .HasColumnType("smallint unsigned"); + b.HasKey("Id"); b.HasIndex("CompileJobId"); diff --git a/src/Tgstation.Server.Host/Database/Migrations/PostgresSqlDatabaseContextModelSnapshot.cs b/src/Tgstation.Server.Host/Database/Migrations/PostgresSqlDatabaseContextModelSnapshot.cs index 5e872a384a..631cf53bc9 100644 --- a/src/Tgstation.Server.Host/Database/Migrations/PostgresSqlDatabaseContextModelSnapshot.cs +++ b/src/Tgstation.Server.Host/Database/Migrations/PostgresSqlDatabaseContextModelSnapshot.cs @@ -4,8 +4,6 @@ using System; using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore.Infrastructure; -#nullable disable - namespace Tgstation.Server.Host.Database.Migrations { [DbContext(typeof(PostgresSqlDatabaseContext))] @@ -15,7 +13,7 @@ namespace Tgstation.Server.Host.Database.Migrations { #pragma warning disable 612, 618 modelBuilder - .HasAnnotation("ProductVersion", "8.0.0-rc.1.23419.6") + .HasAnnotation("ProductVersion", "8.0.0") .HasAnnotation("Relational:MaxIdentifierLength", 63); NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder); @@ -118,11 +116,6 @@ namespace Tgstation.Server.Host.Database.Migrations NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); - b.Property("ByondVersion") - .IsRequired() - .HasColumnType("text") - .HasColumnName("EngineVersion"); - b.Property("DMApiMajorVersion") .HasColumnType("integer"); @@ -140,6 +133,10 @@ namespace Tgstation.Server.Host.Database.Migrations .IsRequired() .HasColumnType("text"); + b.Property("EngineVersion") + .IsRequired() + .HasColumnType("text"); + b.Property("GitHubDeploymentId") .HasColumnType("integer"); @@ -482,11 +479,11 @@ namespace Tgstation.Server.Host.Database.Migrations modelBuilder.Entity("Tgstation.Server.Host.Models.ReattachInformation", b => { - b.Property("Id") + b.Property("Id") .ValueGeneratedOnAdd() .HasColumnType("bigint"); - NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); b.Property("AccessIdentifier") .IsRequired() @@ -513,6 +510,9 @@ namespace Tgstation.Server.Host.Database.Migrations b.Property("RebootState") .HasColumnType("integer"); + b.Property("TopicPort") + .HasColumnType("integer"); + b.HasKey("Id"); b.HasIndex("CompileJobId"); diff --git a/src/Tgstation.Server.Host/Database/Migrations/SqlServerDatabaseContextModelSnapshot.cs b/src/Tgstation.Server.Host/Database/Migrations/SqlServerDatabaseContextModelSnapshot.cs index cce9549902..1b1bb5276c 100644 --- a/src/Tgstation.Server.Host/Database/Migrations/SqlServerDatabaseContextModelSnapshot.cs +++ b/src/Tgstation.Server.Host/Database/Migrations/SqlServerDatabaseContextModelSnapshot.cs @@ -4,8 +4,6 @@ using System; using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore.Infrastructure; -#nullable disable - namespace Tgstation.Server.Host.Database.Migrations { [DbContext(typeof(SqlServerDatabaseContext))] @@ -15,7 +13,7 @@ namespace Tgstation.Server.Host.Database.Migrations { #pragma warning disable 612, 618 modelBuilder - .HasAnnotation("ProductVersion", "8.0.0-rc.1.23419.6") + .HasAnnotation("ProductVersion", "8.0.0") .HasAnnotation("Relational:MaxIdentifierLength", 128); SqlServerModelBuilderExtensions.UseIdentityColumns(modelBuilder); @@ -120,11 +118,6 @@ namespace Tgstation.Server.Host.Database.Migrations SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); - b.Property("ByondVersion") - .IsRequired() - .HasColumnType("nvarchar(max)") - .HasColumnName("EngineVersion"); - b.Property("DMApiMajorVersion") .HasColumnType("int"); @@ -142,6 +135,10 @@ namespace Tgstation.Server.Host.Database.Migrations .IsRequired() .HasColumnType("nvarchar(max)"); + b.Property("EngineVersion") + .IsRequired() + .HasColumnType("nvarchar(max)"); + b.Property("GitHubDeploymentId") .HasColumnType("int"); @@ -487,11 +484,11 @@ namespace Tgstation.Server.Host.Database.Migrations modelBuilder.Entity("Tgstation.Server.Host.Models.ReattachInformation", b => { - b.Property("Id") + b.Property("Id") .ValueGeneratedOnAdd() .HasColumnType("bigint"); - SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); b.Property("AccessIdentifier") .IsRequired() @@ -518,6 +515,9 @@ namespace Tgstation.Server.Host.Database.Migrations b.Property("RebootState") .HasColumnType("int"); + b.Property("TopicPort") + .HasColumnType("int"); + b.HasKey("Id"); b.HasIndex("CompileJobId"); diff --git a/src/Tgstation.Server.Host/Database/Migrations/SqliteDatabaseContextModelSnapshot.cs b/src/Tgstation.Server.Host/Database/Migrations/SqliteDatabaseContextModelSnapshot.cs index 3c0504bde5..29d7469c65 100644 --- a/src/Tgstation.Server.Host/Database/Migrations/SqliteDatabaseContextModelSnapshot.cs +++ b/src/Tgstation.Server.Host/Database/Migrations/SqliteDatabaseContextModelSnapshot.cs @@ -4,8 +4,6 @@ using System; using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore.Infrastructure; -#nullable disable - namespace Tgstation.Server.Host.Database.Migrations { [DbContext(typeof(SqliteDatabaseContext))] @@ -14,7 +12,7 @@ namespace Tgstation.Server.Host.Database.Migrations protected override void BuildModel(ModelBuilder modelBuilder) { #pragma warning disable 612, 618 - modelBuilder.HasAnnotation("ProductVersion", "8.0.0-rc.1.23419.6"); + modelBuilder.HasAnnotation("ProductVersion", "8.0.0"); modelBuilder.Entity("Tgstation.Server.Host.Models.ChatBot", b => { @@ -110,11 +108,6 @@ namespace Tgstation.Server.Host.Database.Migrations .ValueGeneratedOnAdd() .HasColumnType("INTEGER"); - b.Property("ByondVersion") - .IsRequired() - .HasColumnType("TEXT") - .HasColumnName("EngineVersion"); - b.Property("DMApiMajorVersion") .HasColumnType("INTEGER"); @@ -132,6 +125,10 @@ namespace Tgstation.Server.Host.Database.Migrations .IsRequired() .HasColumnType("TEXT"); + b.Property("EngineVersion") + .IsRequired() + .HasColumnType("TEXT"); + b.Property("GitHubDeploymentId") .HasColumnType("INTEGER"); @@ -468,7 +465,7 @@ namespace Tgstation.Server.Host.Database.Migrations modelBuilder.Entity("Tgstation.Server.Host.Models.ReattachInformation", b => { - b.Property("Id") + b.Property("Id") .ValueGeneratedOnAdd() .HasColumnType("INTEGER"); @@ -497,6 +494,9 @@ namespace Tgstation.Server.Host.Database.Migrations b.Property("RebootState") .HasColumnType("INTEGER"); + b.Property("TopicPort") + .HasColumnType("INTEGER"); + b.HasKey("Id"); b.HasIndex("CompileJobId"); diff --git a/src/Tgstation.Server.Host/Extensions/ChatChannelExtensions.cs b/src/Tgstation.Server.Host/Extensions/ChatChannelExtensions.cs index edfb7bb426..3b77bd0b1f 100644 --- a/src/Tgstation.Server.Host/Extensions/ChatChannelExtensions.cs +++ b/src/Tgstation.Server.Host/Extensions/ChatChannelExtensions.cs @@ -23,7 +23,7 @@ namespace Tgstation.Server.Host.Extensions /// /// The to retrieve information from. /// The IRC channel key stored in the if it exists, otherwise. - public static string GetIrcChannelKey(this ChatChannel chatChannel) + public static string? GetIrcChannelKey(this ChatChannel chatChannel) { var splits = GetIrcChannelSplits(chatChannel); if (splits.Count < 2) diff --git a/src/Tgstation.Server.Host/Extensions/ControllerBaseExtensions.cs b/src/Tgstation.Server.Host/Extensions/ControllerBaseExtensions.cs index 2d1d8119a0..818b0c44b5 100644 --- a/src/Tgstation.Server.Host/Extensions/ControllerBaseExtensions.cs +++ b/src/Tgstation.Server.Host/Extensions/ControllerBaseExtensions.cs @@ -34,7 +34,7 @@ namespace Tgstation.Server.Host.Extensions /// The . /// The accompanying payload. /// A with the given . - public static ObjectResult StatusCode(this ControllerBase controller, HttpStatusCode statusCode, object errorMessage) + public static ObjectResult StatusCode(this ControllerBase controller, HttpStatusCode statusCode, object? errorMessage) => controller?.StatusCode((int)statusCode, errorMessage) ?? throw new ArgumentNullException(nameof(controller)); /// @@ -45,7 +45,7 @@ namespace Tgstation.Server.Host.Extensions /// The . /// The path to the file in the 'wwwroot'. /// A if the file was found. otherwise. - public static VirtualFileResult TryServeFile(this ControllerBase controller, IWebHostEnvironment hostEnvironment, ILogger logger, string path) + public static VirtualFileResult? TryServeFile(this ControllerBase controller, IWebHostEnvironment hostEnvironment, ILogger logger, string path) { ArgumentNullException.ThrowIfNull(controller); ArgumentNullException.ThrowIfNull(hostEnvironment); diff --git a/src/Tgstation.Server.Host/Extensions/Converters/BoolConverter.cs b/src/Tgstation.Server.Host/Extensions/Converters/BoolConverter.cs index ece3052282..7812072f4a 100644 --- a/src/Tgstation.Server.Host/Extensions/Converters/BoolConverter.cs +++ b/src/Tgstation.Server.Host/Extensions/Converters/BoolConverter.cs @@ -10,10 +10,18 @@ namespace Tgstation.Server.Host.Extensions.Converters sealed class BoolConverter : JsonConverter { /// - public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer) => writer.WriteValue(((bool)value) ? 1 : 0); + public override void WriteJson(JsonWriter? writer, object? value, JsonSerializer serializer) + { + ArgumentNullException.ThrowIfNull(writer); + writer.WriteValue(((bool)value!) ? 1 : 0); + } /// - public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer) => reader.Value.ToString() == "1"; + public override object? ReadJson(JsonReader? reader, Type? objectType, object? existingValue, JsonSerializer serializer) + { + ArgumentNullException.ThrowIfNull(reader); + return reader.Value!.ToString() == "1"; + } /// public override bool CanConvert(Type objectType) => objectType == typeof(bool); diff --git a/src/Tgstation.Server.Host/Extensions/Converters/VersionConverter.cs b/src/Tgstation.Server.Host/Extensions/Converters/VersionConverter.cs index 0373dc5e20..48c593d72f 100644 --- a/src/Tgstation.Server.Host/Extensions/Converters/VersionConverter.cs +++ b/src/Tgstation.Server.Host/Extensions/Converters/VersionConverter.cs @@ -21,7 +21,7 @@ namespace Tgstation.Server.Host.Extensions.Converters /// The to check. /// If the method should if validation fails. /// if is a , otherwise. - static bool CheckSupportsType(Type type, bool validate) + static bool CheckSupportsType(Type? type, bool validate) { ArgumentNullException.ThrowIfNull(type); @@ -33,7 +33,7 @@ namespace Tgstation.Server.Host.Extensions.Converters } /// - public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer) + public override void WriteJson(JsonWriter? writer, object? value, JsonSerializer serializer) { ArgumentNullException.ThrowIfNull(writer); @@ -52,7 +52,7 @@ namespace Tgstation.Server.Host.Extensions.Converters } /// - public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer) + public override object? ReadJson(JsonReader? reader, Type? objectType, object? existingValue, JsonSerializer serializer) { ArgumentNullException.ThrowIfNull(reader); @@ -65,7 +65,7 @@ namespace Tgstation.Server.Host.Extensions.Converters { try { - var v = global::System.Version.Parse((string)reader.Value); + var v = global::System.Version.Parse((string)reader.Value!); return v.Semver(); } catch (Exception ex) @@ -88,7 +88,7 @@ namespace Tgstation.Server.Host.Extensions.Converters public object ReadYaml(IParser parser, Type type) => throw new NotSupportedException("Deserialization not supported!"); // The default implementation is fine at handling this /// - public void WriteYaml(IEmitter emitter, object value, Type type) + public void WriteYaml(IEmitter? emitter, object? value, Type type) { ArgumentNullException.ThrowIfNull(emitter); diff --git a/src/Tgstation.Server.Host/Extensions/FetchOptionsExtensions.cs b/src/Tgstation.Server.Host/Extensions/FetchOptionsExtensions.cs index e1b47d644a..6d71a68edf 100644 --- a/src/Tgstation.Server.Host/Extensions/FetchOptionsExtensions.cs +++ b/src/Tgstation.Server.Host/Extensions/FetchOptionsExtensions.cs @@ -27,7 +27,7 @@ namespace Tgstation.Server.Host.Extensions public static FetchOptions Hydrate( this FetchOptions fetchOptions, ILogger logger, - JobProgressReporter progressReporter, + JobProgressReporter? progressReporter, CredentialsHandler credentialsHandler, CancellationToken cancellationToken) { @@ -63,7 +63,7 @@ namespace Tgstation.Server.Host.Extensions /// The optional of the operation. /// The for the operation. /// A new based on . - static TransferProgressHandler TransferProgressHandler(ILogger logger, JobProgressReporter progressReporter, CancellationToken cancellationToken) => transferProgress => + static TransferProgressHandler TransferProgressHandler(ILogger logger, JobProgressReporter? progressReporter, CancellationToken cancellationToken) => transferProgress => { double? percentage; var totalObjectsToProcess = transferProgress.TotalObjects * 2; diff --git a/src/Tgstation.Server.Host/Extensions/FileTransferStreamHandlerExtensions.cs b/src/Tgstation.Server.Host/Extensions/FileTransferStreamHandlerExtensions.cs index a7e84abac1..5bae10e79b 100644 --- a/src/Tgstation.Server.Host/Extensions/FileTransferStreamHandlerExtensions.cs +++ b/src/Tgstation.Server.Host/Extensions/FileTransferStreamHandlerExtensions.cs @@ -67,7 +67,9 @@ namespace Tgstation.Server.Host.Extensions } catch { - await stream.DisposeAsync(); + if (stream != null) + await stream.DisposeAsync(); + throw; } } diff --git a/src/Tgstation.Server.Host/Extensions/ModelBuilderExtensions.cs b/src/Tgstation.Server.Host/Extensions/ModelBuilderExtensions.cs index f503f2c717..0db5100952 100644 --- a/src/Tgstation.Server.Host/Extensions/ModelBuilderExtensions.cs +++ b/src/Tgstation.Server.Host/Extensions/ModelBuilderExtensions.cs @@ -23,7 +23,7 @@ namespace Tgstation.Server.Host.Extensions /// . public static ModelBuilder MapMySqlTextField( this ModelBuilder modelBuilder, - Expression> expression) + Expression> expression) where TEntity : class { var property = modelBuilder @@ -47,7 +47,7 @@ namespace Tgstation.Server.Host.Extensions /// The entity. /// The accessing the relevant property. /// The pointed to by . - static PropertyInfo GetPropertyFromExpression(Expression> expression) + static PropertyInfo GetPropertyFromExpression(Expression> expression) { MemberExpression memberExpression; diff --git a/src/Tgstation.Server.Host/Extensions/ResultExtensions.cs b/src/Tgstation.Server.Host/Extensions/ResultExtensions.cs index 5d4370734f..f1157f2286 100644 --- a/src/Tgstation.Server.Host/Extensions/ResultExtensions.cs +++ b/src/Tgstation.Server.Host/Extensions/ResultExtensions.cs @@ -116,7 +116,7 @@ namespace Tgstation.Server.Host.Extensions /// /// The of . /// The to mutate. - static void FormatErrorDetails(IEnumerable errorDetails, StringBuilder stringBuilder) + static void FormatErrorDetails(IEnumerable? errorDetails, StringBuilder stringBuilder) { if (errorDetails == null) return; diff --git a/src/Tgstation.Server.Host/Extensions/ServiceCollectionExtensions.cs b/src/Tgstation.Server.Host/Extensions/ServiceCollectionExtensions.cs index c90433f1ff..cc7eb227a9 100644 --- a/src/Tgstation.Server.Host/Extensions/ServiceCollectionExtensions.cs +++ b/src/Tgstation.Server.Host/Extensions/ServiceCollectionExtensions.cs @@ -28,22 +28,22 @@ namespace Tgstation.Server.Host.Extensions /// /// The implementation used in calls to . /// - static Type chatProviderFactoryType; + static Type? chatProviderFactoryType; /// /// The implementation used in calls to . /// - static Type gitHubServiceFactoryType; + static Type? gitHubServiceFactoryType; /// /// The implementation used in calls to . /// - static Type fileDownloaderType; + static Type? fileDownloaderType; /// /// A for an additional to use. /// - static ServiceDescriptor additionalLoggerProvider; + static ServiceDescriptor? additionalLoggerProvider; /// /// Initializes static members of the class. @@ -92,7 +92,7 @@ namespace Tgstation.Server.Host.Extensions { ArgumentNullException.ThrowIfNull(serviceCollection); - serviceCollection.AddSingleton(typeof(IFileDownloader), fileDownloaderType); + serviceCollection.AddSingleton(typeof(IFileDownloader), fileDownloaderType ?? throw new InvalidOperationException("fileDownloaderType not set!")); return serviceCollection; } @@ -107,7 +107,7 @@ namespace Tgstation.Server.Host.Extensions ArgumentNullException.ThrowIfNull(serviceCollection); serviceCollection.AddSingleton(); - serviceCollection.AddSingleton(typeof(IGitHubServiceFactory), gitHubServiceFactoryType); + serviceCollection.AddSingleton(typeof(IGitHubServiceFactory), gitHubServiceFactoryType ?? throw new InvalidOperationException("gitHubServiceFactoryType not set!")); return serviceCollection; } @@ -133,7 +133,7 @@ namespace Tgstation.Server.Host.Extensions { ArgumentNullException.ThrowIfNull(serviceCollection); - return serviceCollection.AddSingleton(typeof(IProviderFactory), chatProviderFactoryType); + return serviceCollection.AddSingleton(typeof(IProviderFactory), chatProviderFactoryType ?? throw new InvalidOperationException("chatProviderFactoryType not set!")); } /// @@ -158,7 +158,7 @@ namespace Tgstation.Server.Host.Extensions if (sectionField.FieldType != stringType) throw new InvalidOperationException(String.Format(CultureInfo.InvariantCulture, "{0} has invalid {1} field type, must be {2}!", configType, SectionFieldName, stringType)); - var sectionName = (string)sectionField.GetValue(null); + var sectionName = (string)sectionField.GetValue(null)!; return serviceCollection.Configure(configuration.GetSection(sectionName)); } @@ -176,10 +176,10 @@ namespace Tgstation.Server.Host.Extensions public static IServiceCollection SetupLogging( this IServiceCollection serviceCollection, Action configurationAction, - Action sinkConfigurationAction = null, - ElasticsearchSinkOptions elasticsearchSinkOptions = null, - InternalConfiguration internalConfiguration = null, - FileLoggingConfiguration fileLoggingConfiguration = null) + Action? sinkConfigurationAction = null, + ElasticsearchSinkOptions? elasticsearchSinkOptions = null, + InternalConfiguration? internalConfiguration = null, + FileLoggingConfiguration? fileLoggingConfiguration = null) { if (internalConfiguration != null) ArgumentNullException.ThrowIfNull(fileLoggingConfiguration); @@ -203,7 +203,7 @@ namespace Tgstation.Server.Host.Extensions + SerilogContextHelper.Template + "){NewLine} {Message:lj}{NewLine}{Exception}"; - if (!((internalConfiguration?.UsingSystemD ?? false) && !fileLoggingConfiguration.Disable)) + if (!((internalConfiguration?.UsingSystemD ?? false) && !(fileLoggingConfiguration?.Disable ?? false))) sinkConfiguration.Console(outputTemplate: template, formatProvider: CultureInfo.InvariantCulture); sinkConfigurationAction?.Invoke(sinkConfiguration); }); diff --git a/src/Tgstation.Server.Host/Extensions/TopicClientExtensions.cs b/src/Tgstation.Server.Host/Extensions/TopicClientExtensions.cs index 07cd38bd1f..3a7fb56403 100644 --- a/src/Tgstation.Server.Host/Extensions/TopicClientExtensions.cs +++ b/src/Tgstation.Server.Host/Extensions/TopicClientExtensions.cs @@ -27,7 +27,7 @@ namespace Tgstation.Server.Host.Extensions /// If priority retries should be used. /// The for the operation. /// A resulting in the on success, on failure. - public static async ValueTask SendWithOptionalPriority( + public static async ValueTask SendWithOptionalPriority( this ITopicClient topicClient, IAsyncDelayer delayer, ILogger logger, diff --git a/src/Tgstation.Server.Host/IO/BufferedFileStreamProvider.cs b/src/Tgstation.Server.Host/IO/BufferedFileStreamProvider.cs index c324869860..d1310029df 100644 --- a/src/Tgstation.Server.Host/IO/BufferedFileStreamProvider.cs +++ b/src/Tgstation.Server.Host/IO/BufferedFileStreamProvider.cs @@ -28,7 +28,7 @@ namespace Tgstation.Server.Host.IO /// /// The backing . /// - volatile MemoryStream buffer; + volatile MemoryStream? buffer; /// /// If has been populated. @@ -58,7 +58,7 @@ namespace Tgstation.Server.Host.IO /// public async ValueTask DisposeAsync() { - MemoryStream localBuffer; + MemoryStream? localBuffer; lock (semaphore) { localBuffer = buffer; diff --git a/src/Tgstation.Server.Host/IO/Console.cs b/src/Tgstation.Server.Host/IO/Console.cs index fdf06eea52..6a94540bb0 100644 --- a/src/Tgstation.Server.Host/IO/Console.cs +++ b/src/Tgstation.Server.Host/IO/Console.cs @@ -13,13 +13,9 @@ namespace Tgstation.Server.Host.IO sealed class Console : IConsole, IDisposable { /// - public string Title - { - get => platformIdentifier.IsWindows - ? global::System.Console.Title - : null; - set => global::System.Console.Title = value; - } + public string? Title => platformIdentifier.IsWindows + ? global::System.Console.Title + : null; /// public bool Available => Environment.UserInteractive; @@ -89,7 +85,8 @@ namespace Tgstation.Server.Host.IO // TODO: Make this better: https://stackoverflow.com/questions/9479573/how-to-interrupt-console-readline CheckAvailable(); if (!usePasswordChar) - return global::System.Console.ReadLine(); + return global::System.Console.ReadLine() + ?? throw new InvalidOperationException("Console input has been closed!"); var passwordBuilder = new StringBuilder(); do @@ -124,7 +121,7 @@ namespace Tgstation.Server.Host.IO .WaitAsync(cancellationToken); /// - public Task WriteAsync(string text, bool newLine, CancellationToken cancellationToken) => Task.Factory.StartNew( + public Task WriteAsync(string? text, bool newLine, CancellationToken cancellationToken) => Task.Factory.StartNew( () => { CheckAvailable(); @@ -143,6 +140,13 @@ namespace Tgstation.Server.Host.IO DefaultIOManager.BlockingTaskCreationOptions, TaskScheduler.Current); + /// + public void SetTitle(string newTitle) + { + ArgumentNullException.ThrowIfNull(newTitle); + global::System.Console.Title = newTitle; + } + /// /// Assert that the is available, throwing an otherwise. /// diff --git a/src/Tgstation.Server.Host/IO/DefaultIOManager.cs b/src/Tgstation.Server.Host/IO/DefaultIOManager.cs index 4841980dd3..cdae64ce44 100644 --- a/src/Tgstation.Server.Host/IO/DefaultIOManager.cs +++ b/src/Tgstation.Server.Host/IO/DefaultIOManager.cs @@ -69,8 +69,8 @@ namespace Tgstation.Server.Host.IO /// public async ValueTask CopyDirectory( - IEnumerable ignore, - Func postCopyCallback, + IEnumerable? ignore, + Func? postCopyCallback, string src, string dest, int? taskThrottle, @@ -140,7 +140,8 @@ namespace Tgstation.Server.Host.IO public Task DirectoryExists(string path, CancellationToken cancellationToken) => Task.Factory.StartNew(() => Directory.Exists(ResolvePath(path)), cancellationToken, BlockingTaskCreationOptions, TaskScheduler.Current); /// - public string GetDirectoryName(string path) => Path.GetDirectoryName(path ?? throw new ArgumentNullException(nameof(path))); + public string GetDirectoryName(string path) => Path.GetDirectoryName(path ?? throw new ArgumentNullException(nameof(path))) + ?? throw new InvalidOperationException($"Null was returned. Path ({path}) must be rooted. This is not supported!"); /// public string GetFileName(string path) => Path.GetFileName(path ?? throw new ArgumentNullException(nameof(path))); @@ -302,11 +303,10 @@ namespace Tgstation.Server.Host.IO /// public bool PathContainsParentAccess(string path) => path ?.Split( - new[] - { + [ Path.DirectorySeparatorChar, Path.AltDirectorySeparatorChar, - }) + ]) .Any(x => x == "..") ?? throw new ArgumentNullException(nameof(path)); @@ -336,7 +336,7 @@ namespace Tgstation.Server.Host.IO /// /// The source directory path. /// The destination directory path. - /// Files and folders to ignore at the root level. + /// Optional files and folders to ignore at the root level. /// The optional callback called for each source/dest file pair post copy. /// Optional used to limit degree of parallelism. /// The for the operation. @@ -344,13 +344,13 @@ namespace Tgstation.Server.Host.IO IEnumerable CopyDirectoryImpl( string src, string dest, - IEnumerable ignore, - Func postCopyCallback, - SemaphoreSlim semaphore, + IEnumerable? ignore, + Func? postCopyCallback, + SemaphoreSlim? semaphore, CancellationToken cancellationToken) { var dir = new DirectoryInfo(src); - Task subdirCreationTask = null; + Task? subdirCreationTask = null; foreach (var subDirectory in dir.EnumerateDirectories()) { if (ignore != null && ignore.Contains(subDirectory.Name)) diff --git a/src/Tgstation.Server.Host/IO/FileDownloader.cs b/src/Tgstation.Server.Host/IO/FileDownloader.cs index 1aa554e647..30efdb6718 100644 --- a/src/Tgstation.Server.Host/IO/FileDownloader.cs +++ b/src/Tgstation.Server.Host/IO/FileDownloader.cs @@ -34,7 +34,7 @@ namespace Tgstation.Server.Host.IO } /// - public IFileStreamProvider DownloadFile(Uri url, string bearerToken) + public IFileStreamProvider DownloadFile(Uri url, string? bearerToken) { ArgumentNullException.ThrowIfNull(url); diff --git a/src/Tgstation.Server.Host/IO/IConsole.cs b/src/Tgstation.Server.Host/IO/IConsole.cs index 605f3e421d..ee1e948891 100644 --- a/src/Tgstation.Server.Host/IO/IConsole.cs +++ b/src/Tgstation.Server.Host/IO/IConsole.cs @@ -11,7 +11,7 @@ namespace Tgstation.Server.Host.IO /// /// Gets or sets the window's title. Can return if getting the console title is not supported. /// - string Title { get; set; } + string? Title { get; } /// /// If the is visible to the user. @@ -30,7 +30,7 @@ namespace Tgstation.Server.Host.IO /// If there should be a new line after the . /// The for the operation. /// A representing the running operation. - Task WriteAsync(string text, bool newLine, CancellationToken cancellationToken); + Task WriteAsync(string? text, bool newLine, CancellationToken cancellationToken); /// /// Wait for a key press on the . @@ -46,5 +46,11 @@ namespace Tgstation.Server.Host.IO /// The for the operation. /// A resulting in the read by the . Task ReadLineAsync(bool usePasswordChar, CancellationToken cancellationToken); + + /// + /// Sets a console window. + /// + /// The new . + void SetTitle(string newTitle); } } diff --git a/src/Tgstation.Server.Host/IO/IFileDownloader.cs b/src/Tgstation.Server.Host/IO/IFileDownloader.cs index 5a2c989170..446700e55f 100644 --- a/src/Tgstation.Server.Host/IO/IFileDownloader.cs +++ b/src/Tgstation.Server.Host/IO/IFileDownloader.cs @@ -13,6 +13,6 @@ namespace Tgstation.Server.Host.IO /// The URL to download. /// Optional to use as the "Bearer" value in the optional "Authorization" header for the request. /// A new for the downloaded file. - IFileStreamProvider DownloadFile(Uri url, string bearerToken); + IFileStreamProvider DownloadFile(Uri url, string? bearerToken); } } diff --git a/src/Tgstation.Server.Host/IO/IFileStreamProvider.cs b/src/Tgstation.Server.Host/IO/IFileStreamProvider.cs index e27f779314..6b1670194d 100644 --- a/src/Tgstation.Server.Host/IO/IFileStreamProvider.cs +++ b/src/Tgstation.Server.Host/IO/IFileStreamProvider.cs @@ -14,7 +14,7 @@ namespace Tgstation.Server.Host.IO /// Gets the provided . May be called multiple times, though cancelling any may cause all calls to be cancelled. All calls yield the same reference. /// /// The for the operation. - /// A resulting in the provided on success, if it could not be provided. + /// A resulting in the provided . /// The resulting is owned by the and is short lived unless otherwise specified. It should be buffered if it needs use outside the lifetime of the . ValueTask GetResult(CancellationToken cancellationToken); } diff --git a/src/Tgstation.Server.Host/IO/IIOManager.cs b/src/Tgstation.Server.Host/IO/IIOManager.cs index 0b249b23a2..d88ec3ec17 100644 --- a/src/Tgstation.Server.Host/IO/IIOManager.cs +++ b/src/Tgstation.Server.Host/IO/IIOManager.cs @@ -56,8 +56,8 @@ namespace Tgstation.Server.Host.IO /// The for the operation. /// A representing the running operation. ValueTask CopyDirectory( - IEnumerable ignore, - Func postCopyCallback, + IEnumerable? ignore, + Func? postCopyCallback, string src, string dest, int? taskThrottle, @@ -133,6 +133,7 @@ namespace Tgstation.Server.Host.IO /// /// A path to check. /// The directory portion of the given . + /// If is rooted. string GetDirectoryName(string path); /// diff --git a/src/Tgstation.Server.Host/IO/ISeekableFileStreamProvider.cs b/src/Tgstation.Server.Host/IO/ISeekableFileStreamProvider.cs index 7a6b76eb0d..42cfc86953 100644 --- a/src/Tgstation.Server.Host/IO/ISeekableFileStreamProvider.cs +++ b/src/Tgstation.Server.Host/IO/ISeekableFileStreamProvider.cs @@ -18,7 +18,7 @@ namespace Tgstation.Server.Host.IO /// Gets the provided . May be called multiple times, though cancelling any may cause all calls to be cancelled. /// /// The for the operation. - /// A resulting in the provided on success, if it could not be provided. + /// A resulting in the provided on success. ValueTask GetOwnedResult(CancellationToken cancellationToken); } } diff --git a/src/Tgstation.Server.Host/IO/ISynchronousIOManager.cs b/src/Tgstation.Server.Host/IO/ISynchronousIOManager.cs index 36f90140aa..16e0ea370e 100644 --- a/src/Tgstation.Server.Host/IO/ISynchronousIOManager.cs +++ b/src/Tgstation.Server.Host/IO/ISynchronousIOManager.cs @@ -55,7 +55,7 @@ namespace Tgstation.Server.Host.IO /// The function only succeeds if this parameter matches the SHA-1 hash of the contents of the current file. Contains the SHA1 of the file on disk once the function returns. /// The for the operation. /// on success, if the operation failed due to not matching the file's contents. - bool WriteFileChecked(string path, Stream data, ref string sha1InOut, CancellationToken cancellationToken); + bool WriteFileChecked(string path, Stream data, ref string? sha1InOut, CancellationToken cancellationToken); /// /// Checks if a given is a directory. diff --git a/src/Tgstation.Server.Host/IO/RequestFileStreamProvider.cs b/src/Tgstation.Server.Host/IO/RequestFileStreamProvider.cs index c0c100cfcf..12461e4e7f 100644 --- a/src/Tgstation.Server.Host/IO/RequestFileStreamProvider.cs +++ b/src/Tgstation.Server.Host/IO/RequestFileStreamProvider.cs @@ -31,7 +31,7 @@ namespace Tgstation.Server.Host.IO /// /// The resulting in the downloaded . /// - Task downloadTask; + Task? downloadTask; /// /// If has been called. @@ -54,7 +54,7 @@ namespace Tgstation.Server.Host.IO /// public async ValueTask DisposeAsync() { - Task localDownloadTask; + Task? localDownloadTask; lock (downloadCts) { if (disposed) diff --git a/src/Tgstation.Server.Host/IO/SynchronousIOManager.cs b/src/Tgstation.Server.Host/IO/SynchronousIOManager.cs index a629ddcff4..4dbf3d833b 100644 --- a/src/Tgstation.Server.Host/IO/SynchronousIOManager.cs +++ b/src/Tgstation.Server.Host/IO/SynchronousIOManager.cs @@ -72,14 +72,13 @@ namespace Tgstation.Server.Host.IO } /// - public bool WriteFileChecked(string path, Stream data, ref string sha1InOut, CancellationToken cancellationToken) + public bool WriteFileChecked(string path, Stream data, ref string? sha1InOut, CancellationToken cancellationToken) { ArgumentNullException.ThrowIfNull(path); ArgumentNullException.ThrowIfNull(data); cancellationToken.ThrowIfCancellationRequested(); - var directory = Path.GetDirectoryName(path); - + var directory = Path.GetDirectoryName(path) ?? throw new ArgumentException("path cannot be rooted!", nameof(path)); Directory.CreateDirectory(directory); var newFile = !File.Exists(path); @@ -97,7 +96,7 @@ namespace Tgstation.Server.Host.IO // suppressed due to only using for consistency checks using (var sha1 = SHA1.Create()) { - string GetSha1(Stream dataToHash) + string? GetSha1(Stream dataToHash) { if (dataToHash == null) return null; diff --git a/src/Tgstation.Server.Host/IO/WindowsFilesystemLinkFactory.cs b/src/Tgstation.Server.Host/IO/WindowsFilesystemLinkFactory.cs index 1513f4e166..6636838ab4 100644 --- a/src/Tgstation.Server.Host/IO/WindowsFilesystemLinkFactory.cs +++ b/src/Tgstation.Server.Host/IO/WindowsFilesystemLinkFactory.cs @@ -4,6 +4,7 @@ using System.Threading; using System.Threading.Tasks; using BetterWin32Errors; + using Tgstation.Server.Host.System; namespace Tgstation.Server.Host.IO diff --git a/src/Tgstation.Server.Host/IServerFactory.cs b/src/Tgstation.Server.Host/IServerFactory.cs index 46f9510523..b205d6b62e 100644 --- a/src/Tgstation.Server.Host/IServerFactory.cs +++ b/src/Tgstation.Server.Host/IServerFactory.cs @@ -22,6 +22,6 @@ namespace Tgstation.Server.Host /// The directory in which to install server updates. /// The for the operation. /// A resulting in a new if it should be run, otherwise. - ValueTask CreateServer(string[] args, string updatePath, CancellationToken cancellationToken); + ValueTask CreateServer(string[] args, string? updatePath, CancellationToken cancellationToken); } } diff --git a/src/Tgstation.Server.Host/Jobs/IJobManager.cs b/src/Tgstation.Server.Host/Jobs/IJobManager.cs index 80ef200859..9f582b34ab 100644 --- a/src/Tgstation.Server.Host/Jobs/IJobManager.cs +++ b/src/Tgstation.Server.Host/Jobs/IJobManager.cs @@ -34,7 +34,7 @@ namespace Tgstation.Server.Host.Jobs /// A that will cancel the . /// The for the operation. /// A representing the . Results in if the completed without errors, if errors occurred, or if the job isn't registered. - ValueTask WaitForJobCompletion(Job job, User canceller, CancellationToken jobCancellationToken, CancellationToken cancellationToken); + ValueTask WaitForJobCompletion(Job job, User? canceller, CancellationToken jobCancellationToken, CancellationToken cancellationToken); /// /// Cancels a give . @@ -44,6 +44,6 @@ namespace Tgstation.Server.Host.Jobs /// If the operation should wait until the job exits before completing. /// The for the operation. /// A resulting in the updated if it was cancelled, if it couldn't be found. - ValueTask CancelJob(Job job, User user, bool blocking, CancellationToken cancellationToken); + ValueTask CancelJob(Job job, User? user, bool blocking, CancellationToken cancellationToken); } } diff --git a/src/Tgstation.Server.Host/Jobs/JobEntrypoint.cs b/src/Tgstation.Server.Host/Jobs/JobEntrypoint.cs index 79e71b5048..82e81a647c 100644 --- a/src/Tgstation.Server.Host/Jobs/JobEntrypoint.cs +++ b/src/Tgstation.Server.Host/Jobs/JobEntrypoint.cs @@ -17,7 +17,7 @@ namespace Tgstation.Server.Host.Jobs /// The for the operation. /// A representing the running operation. public delegate ValueTask JobEntrypoint( - IInstanceCore instance, + IInstanceCore? instance, IDatabaseContextFactory databaseContextFactory, Job job, JobProgressReporter progressReporter, diff --git a/src/Tgstation.Server.Host/Jobs/JobHandler.cs b/src/Tgstation.Server.Host/Jobs/JobHandler.cs index 9d9c692c07..04c3b84510 100644 --- a/src/Tgstation.Server.Host/Jobs/JobHandler.cs +++ b/src/Tgstation.Server.Host/Jobs/JobHandler.cs @@ -22,7 +22,7 @@ namespace Tgstation.Server.Host.Jobs /// /// The stage of the job. /// - public string Stage { get; set; } + public string? Stage { get; set; } /// /// The for . @@ -37,7 +37,7 @@ namespace Tgstation.Server.Host.Jobs /// /// The being run. /// - Task task; + Task? task; /// /// Initializes a new instance of the class. diff --git a/src/Tgstation.Server.Host/Jobs/JobProgressReporter.cs b/src/Tgstation.Server.Host/Jobs/JobProgressReporter.cs index 4d23385e63..753f8cf519 100644 --- a/src/Tgstation.Server.Host/Jobs/JobProgressReporter.cs +++ b/src/Tgstation.Server.Host/Jobs/JobProgressReporter.cs @@ -14,7 +14,7 @@ namespace Tgstation.Server.Host.Jobs /// /// The name of the current stage. /// - public string StageName + public string? StageName { get => stageName; set @@ -35,12 +35,12 @@ namespace Tgstation.Server.Host.Jobs /// /// Progress reporter callback taking a description of what the job is currently doing and the (optional) progress of the job on a scale from 0.0-1.0. /// - readonly Action callback; + readonly Action callback; /// /// Backing field for . /// - string stageName; + string? stageName; /// /// The last progress value pushed into the . @@ -58,7 +58,7 @@ namespace Tgstation.Server.Host.Jobs /// The value of . /// The value of . /// The value of . - public JobProgressReporter(ILogger logger, string stageName, Action callback) + public JobProgressReporter(ILogger logger, string? stageName, Action callback) { this.logger = logger ?? throw new ArgumentNullException(nameof(logger)); this.callback = callback ?? throw new ArgumentNullException(nameof(callback)); @@ -97,7 +97,7 @@ namespace Tgstation.Server.Host.Jobs /// The 0.0f-1.0f percentage of the current 's percentage should be given to the section. /// A new that is a subsection of this one. /// A should only have one active child at a time. - public JobProgressReporter CreateSection(string newStageName, double percentage) + public JobProgressReporter CreateSection(string? newStageName, double percentage) { if (percentage > 1 || percentage < 0) { diff --git a/src/Tgstation.Server.Host/Jobs/JobService.cs b/src/Tgstation.Server.Host/Jobs/JobService.cs index abdb26170c..ea2782a821 100644 --- a/src/Tgstation.Server.Host/Jobs/JobService.cs +++ b/src/Tgstation.Server.Host/Jobs/JobService.cs @@ -120,25 +120,22 @@ namespace Tgstation.Server.Host.Jobs ArgumentNullException.ThrowIfNull(job); ArgumentNullException.ThrowIfNull(operation); + if (job.StartedBy != null && job.StartedBy.Name == null) + throw new InvalidOperationException("StartedBy User associated with job does not have a Name!"); + + if (job.Instance == null) + throw new InvalidOperationException("No Instance associated with job!"); + job.StartedAt = DateTimeOffset.UtcNow; job.Cancelled = false; - if (job.StartedBy != null) - { - if (!job.StartedBy.Id.HasValue) - throw new InvalidOperationException("StartedBy User associated with job does not have an Id!"); - - if (job.StartedBy.Name == null) - throw new InvalidOperationException("StartedBy User associated with job does not have a Name!"); - } - var originalStartedBy = job.StartedBy; await databaseContextFactory.UseContext( async databaseContext => { job.Instance = new Models.Instance { - Id = job.Instance.Id.Value, + Id = job.Instance.Require(x => x.Id), }; databaseContext.Instances.Attach(job.Instance); @@ -148,14 +145,14 @@ namespace Tgstation.Server.Host.Jobs .GetTgsUser( dbUser => new User { - Id = dbUser.Id.Value, + Id = dbUser.Id!.Value, Name = dbUser.Name, }, cancellationToken); job.StartedBy = new User { - Id = originalStartedBy.Id.Value, + Id = originalStartedBy.Require(x => x.Id), }; databaseContext.Users.Attach(job.StartedBy); @@ -176,7 +173,7 @@ namespace Tgstation.Server.Host.Jobs bool jobShouldStart; lock (synchronizationLock) { - jobs.Add(job.Id.Value, jobHandler); + jobs.Add(job.Require(x => x.Id), jobHandler); jobShouldStart = !noMoreJobsShouldStart; } @@ -200,7 +197,7 @@ namespace Tgstation.Server.Host.Jobs .Jobs .AsQueryable() .Where(y => !y.StoppedAt.HasValue) - .Select(y => y.Id.Value) + .Select(y => y.Id!.Value) .ToListAsync(cancellationToken); if (badJobIds.Count > 0) { @@ -223,7 +220,7 @@ namespace Tgstation.Server.Host.Jobs /// public Task StopAsync(CancellationToken cancellationToken) { - List> joinTasks; + List> joinTasks; lock (addCancelLock) lock (synchronizationLock) { @@ -240,24 +237,25 @@ namespace Tgstation.Server.Host.Jobs } /// - public async ValueTask CancelJob(Job job, User user, bool blocking, CancellationToken cancellationToken) + public async ValueTask CancelJob(Job job, User? user, bool blocking, CancellationToken cancellationToken) { ArgumentNullException.ThrowIfNull(job); - JobHandler handler; + var jid = job.Require(x => x.Id); + JobHandler? handler; lock (addCancelLock) { lock (synchronizationLock) - if (!jobs.TryGetValue(job.Id.Value, out handler)) + if (!jobs.TryGetValue(jid, out handler)) return null; - logger.LogDebug("Cancelling job ID {jobId}...", job.Id.Value); + logger.LogDebug("Cancelling job ID {jobId}...", jid); handler.Cancel(); // this will ensure the db update is only done once } await databaseContextFactory.UseContext(async databaseContext => { - var updatedJob = new Job(job.Id.Value); + var updatedJob = new Job(jid); databaseContext.Jobs.Attach(updatedJob); var attachedUser = user == null ? await databaseContext @@ -265,12 +263,12 @@ namespace Tgstation.Server.Host.Jobs .GetTgsUser( dbUser => new User { - Id = dbUser.Id.Value, + Id = dbUser.Id!.Value, }, cancellationToken) : new User { - Id = user.Id.Value, + Id = user.Require(x => x.Id), }; databaseContext.Users.Attach(attachedUser); @@ -297,7 +295,7 @@ namespace Tgstation.Server.Host.Jobs ArgumentNullException.ThrowIfNull(apiResponse); lock (synchronizationLock) { - if (!jobs.TryGetValue(apiResponse.Id.Value, out var handler)) + if (!jobs.TryGetValue(apiResponse.Require(x => x.Id), out var handler)) return; apiResponse.Progress = handler.Progress; apiResponse.Stage = handler.Stage; @@ -305,18 +303,18 @@ namespace Tgstation.Server.Host.Jobs } /// - public async ValueTask WaitForJobCompletion(Job job, User canceller, CancellationToken jobCancellationToken, CancellationToken cancellationToken) + public async ValueTask WaitForJobCompletion(Job job, User? canceller, CancellationToken jobCancellationToken, CancellationToken cancellationToken) { ArgumentNullException.ThrowIfNull(job); if (!cancellationToken.CanBeCanceled) throw new ArgumentException("A cancellable CancellationToken should be provided!", nameof(cancellationToken)); - JobHandler handler; + JobHandler? handler; bool noMoreJobsShouldStart; lock (synchronizationLock) { - if (!jobs.TryGetValue(job.Id.Value, out handler)) + if (!jobs.TryGetValue(job.Require(x => x.Id), out handler)) return null; noMoreJobsShouldStart = this.noMoreJobsShouldStart; @@ -325,7 +323,7 @@ namespace Tgstation.Server.Host.Jobs if (noMoreJobsShouldStart && !handler.Started) await Extensions.TaskExtensions.InfiniteTask.WaitAsync(cancellationToken); - var cancelTask = ValueTask.FromResult(null); + var cancelTask = ValueTask.FromResult(null); bool result; using (jobCancellationToken.Register(() => cancelTask = CancelJob(job, canceller, true, cancellationToken))) result = await handler.Wait(cancellationToken); @@ -363,17 +361,18 @@ namespace Tgstation.Server.Host.Jobs async Task RunJob(Job job, JobEntrypoint operation, CancellationToken cancellationToken) #pragma warning restore CA1506 { - using (LogContext.PushProperty(SerilogContextHelper.JobIdContextProperty, job.Id)) + var jid = job.Require(x => x.Id); + using (LogContext.PushProperty(SerilogContextHelper.JobIdContextProperty, jid)) try { - void LogException(Exception ex) => logger.LogDebug(ex, "Job {jobId} exited with error!", job.Id); + void LogException(Exception ex) => logger.LogDebug(ex, "Job {jobId} exited with error!", jid); var hubUpdatesTask = Task.CompletedTask; var result = false; var firstLogHappened = false; var hubGroupName = JobsHub.HubGroupName(job); - Stopwatch stopwatch = null; + Stopwatch? stopwatch = null; void QueueHubUpdate(JobResponse update, bool final) { void NextUpdate(bool bypassRate) @@ -385,7 +384,7 @@ namespace Tgstation.Server.Host.Jobs if (!firstLogHappened) { - logger.LogTrace("Sending updates for job {id} to hub group {group}", update.Id.Value, hubGroupName); + logger.LogTrace("Sending updates for job {id} to hub group {group}", jid, hubGroupName); firstLogHappened = true; } @@ -396,7 +395,7 @@ namespace Tgstation.Server.Host.Jobs .ReceiveJobUpdate(update, CancellationToken.None); } - Stopwatch enteredLock = null; + Stopwatch? enteredLock = null; try { if (!bypassRate && stopwatch != null) @@ -417,19 +416,18 @@ namespace Tgstation.Server.Host.Jobs } } - var jobId = update.Id.Value; lock (hubUpdateActions) if (final) - hubUpdateActions.Remove(jobId); + hubUpdateActions.Remove(jid); else - hubUpdateActions[jobId] = () => NextUpdate(true); + hubUpdateActions[jid] = () => NextUpdate(true); NextUpdate(false); } try { - void UpdateProgress(string stage, double? progress) + void UpdateProgress(string? stage, double? progress) { if (progress.HasValue && (progress.Value < 0 || progress.Value > 1)) @@ -441,7 +439,7 @@ namespace Tgstation.Server.Host.Jobs int? newProgress = progress.HasValue ? (int)Math.Floor(progress.Value * 100) : null; lock (synchronizationLock) - if (jobs.TryGetValue(job.Id.Value, out var handler)) + if (jobs.TryGetValue(jid, out var handler)) { handler.Stage = stage; handler.Progress = newProgress; @@ -455,7 +453,7 @@ namespace Tgstation.Server.Host.Jobs var activationTask = activationTcs.Task; - Debug.Assert(activationTask.IsCompleted || job.JobCode.Value.IsServerStartupJob(), "Non-server startup job registered before activation!"); + Debug.Assert(activationTask.IsCompleted || job.Require(x => x.JobCode).IsServerStartupJob(), "Non-server startup job registered before activation!"); var instanceCoreProvider = await activationTask.WaitAsync(cancellationToken); @@ -463,7 +461,7 @@ namespace Tgstation.Server.Host.Jobs logger.LogTrace("Starting job..."); await operation( - instanceCoreProvider.GetInstance(job.Instance), + instanceCoreProvider.GetInstance(job.Instance!), databaseContextFactory, job, new JobProgressReporter( @@ -496,7 +494,7 @@ namespace Tgstation.Server.Host.Jobs { await databaseContextFactory.UseContext(async databaseContext => { - var attachedJob = new Job(job.Id.Value); + var attachedJob = new Job(jid); databaseContext.Jobs.Attach(attachedJob); attachedJob.StoppedAt = DateTimeOffset.UtcNow; @@ -521,7 +519,7 @@ namespace Tgstation.Server.Host.Jobs .Include(x => x.Instance) .Include(x => x.StartedBy) .Include(x => x.CancelledBy) - .Where(dbJob => dbJob.Id == job.Id.Value) + .Where(dbJob => dbJob.Id == jid) .FirstAsync(CancellationToken.None); QueueHubUpdate(finalJob.ToApi(), true); }); @@ -529,7 +527,7 @@ namespace Tgstation.Server.Host.Jobs catch { lock (hubUpdateActions) - hubUpdateActions.Remove(job.Id.Value); + hubUpdateActions.Remove(jid); throw; } @@ -549,8 +547,8 @@ namespace Tgstation.Server.Host.Jobs { lock (synchronizationLock) { - var handler = jobs[job.Id.Value]; - jobs.Remove(job.Id.Value); + var handler = jobs[jid]; + jobs.Remove(jid); handler.Dispose(); } } diff --git a/src/Tgstation.Server.Host/Jobs/JobsHub.cs b/src/Tgstation.Server.Host/Jobs/JobsHub.cs index b74e19d22b..d11fdf7913 100644 --- a/src/Tgstation.Server.Host/Jobs/JobsHub.cs +++ b/src/Tgstation.Server.Host/Jobs/JobsHub.cs @@ -34,7 +34,7 @@ namespace Tgstation.Server.Host.Jobs if (job.Instance == null) throw new InvalidOperationException("job.Instance was null!"); - return HubGroupName(job.Instance.Id.Value); + return HubGroupName(job.Instance.Require(x => x.Id)); } /// diff --git a/src/Tgstation.Server.Host/Jobs/JobsHubGroupMapper.cs b/src/Tgstation.Server.Host/Jobs/JobsHubGroupMapper.cs index 2e2a5da93d..a961fbaef9 100644 --- a/src/Tgstation.Server.Host/Jobs/JobsHubGroupMapper.cs +++ b/src/Tgstation.Server.Host/Jobs/JobsHubGroupMapper.cs @@ -67,7 +67,7 @@ namespace Tgstation.Server.Host.Jobs public ValueTask InstancePermissionSetCreated(InstancePermissionSet instancePermissionSet, CancellationToken cancellationToken) { ArgumentNullException.ThrowIfNull(instancePermissionSet); - var permissionSetId = instancePermissionSet.PermissionSet.Id ?? instancePermissionSet.PermissionSetId; + var permissionSetId = instancePermissionSet.PermissionSetId; logger.LogTrace("InstancePermissionSetCreated"); return RefreshHubGroups( @@ -117,20 +117,24 @@ namespace Tgstation.Server.Host.Jobs { ArgumentNullException.ThrowIfNull(authenticationContext); - logger.LogTrace("MapConnectionGroups UID: {uid}", authenticationContext.User.Id.Value); + var pid = authenticationContext.PermissionSet.Require(x => x.Id); + logger.LogTrace( + "MapConnectionGroups UID: {uid} PID: {pid}", + authenticationContext.User.Require(x => x.Id), + pid); - List permedInstanceIds = null; + List? permedInstanceIds = null; await databaseContextFactory.UseContext( async databaseContext => permedInstanceIds = await databaseContext .InstancePermissionSets .AsQueryable() - .Where(ips => ips.PermissionSetId == authenticationContext.PermissionSet.Id.Value) + .Where(ips => ips.PermissionSetId == pid) .Select(ips => ips.InstanceId) .ToListAsync(cancellationToken)); await mappingFunc( - permedInstanceIds.Select( + permedInstanceIds!.Select( JobsHub.HubGroupName)); jobsHubUpdater.QueueActiveJobUpdates(); @@ -149,12 +153,12 @@ namespace Tgstation.Server.Host.Jobs logger.LogTrace("RefreshHubGroups"); var permissionSetUsers = await databaseContext .Users - .Where(x => x.PermissionSet.Id == permissionSetId) + .Where(x => x.PermissionSet!.Id == permissionSetId) .ToListAsync(cancellationToken); var allInstanceIds = await databaseContext .Instances .Select( - instance => instance.Id.Value) + instance => instance.Id!.Value) .ToListAsync(cancellationToken); var permissionSetAccessibleInstanceIds = await databaseContext .InstancePermissionSets diff --git a/src/Tgstation.Server.Host/Models/ChatBot.cs b/src/Tgstation.Server.Host/Models/ChatBot.cs index f98f4a43c3..caf1f0c645 100644 --- a/src/Tgstation.Server.Host/Models/ChatBot.cs +++ b/src/Tgstation.Server.Host/Models/ChatBot.cs @@ -1,4 +1,5 @@ -using System.Collections.Generic; +using System; +using System.Collections.Generic; using System.ComponentModel.DataAnnotations; using System.Linq; @@ -23,17 +24,34 @@ namespace Tgstation.Server.Host.Models /// The parent . /// [Required] - public Instance Instance { get; set; } + public Instance? Instance { get; set; } /// /// See . /// public ICollection Channels { get; set; } - /// - public ChatBotResponse ToApi() => new ChatBotResponse + /// + /// Initializes a new instance of the class. + /// + public ChatBot() + : this(new List()) { - Channels = Channels.Select(x => x.ToApi(Provider.Value)).ToList(), + } + + /// + /// Initializes a new instance of the class. + /// + /// The value of . + public ChatBot(ICollection channels) + { + Channels = channels ?? throw new ArgumentNullException(nameof(channels)); + } + + /// + public ChatBotResponse ToApi() => new() + { + Channels = Channels.Select(x => x.ToApi(this.Require(x => x.Provider))).ToList(), ConnectionString = ConnectionString, Enabled = Enabled, Provider = Provider, diff --git a/src/Tgstation.Server.Host/Models/ChatChannel.cs b/src/Tgstation.Server.Host/Models/ChatChannel.cs index a4cbea0952..9263d2f9a6 100644 --- a/src/Tgstation.Server.Host/Models/ChatChannel.cs +++ b/src/Tgstation.Server.Host/Models/ChatChannel.cs @@ -23,7 +23,7 @@ namespace Tgstation.Server.Host.Models /// The IRC channel name. /// [StringLength(Limits.MaximumIndexableStringLength, MinimumLength = 1)] - public string IrcChannel { get; set; } + public string? IrcChannel { get; set; } /// /// The Discord channel snowflake. @@ -33,16 +33,16 @@ namespace Tgstation.Server.Host.Models /// /// The . /// - public ChatBot ChatSettings { get; set; } + public ChatBot? ChatSettings { get; set; } /// /// Convert to a . /// /// The channel's . /// The converted . - public Api.Models.ChatChannel ToApi(ChatProvider chatProvider) => new Api.Models.ChatChannel + public Api.Models.ChatChannel ToApi(ChatProvider chatProvider) => new() { - ChannelData = chatProvider == ChatProvider.Discord ? DiscordChannelId.Value.ToString(CultureInfo.InvariantCulture) : IrcChannel, + ChannelData = chatProvider == ChatProvider.Discord ? DiscordChannelId!.Value.ToString(CultureInfo.InvariantCulture) : IrcChannel, IsAdminChannel = IsAdminChannel, IsWatchdogChannel = IsWatchdogChannel, IsUpdatesChannel = IsUpdatesChannel, diff --git a/src/Tgstation.Server.Host/Models/CompileJob.cs b/src/Tgstation.Server.Host/Models/CompileJob.cs index 59bc7c4d01..0f75b830e7 100644 --- a/src/Tgstation.Server.Host/Models/CompileJob.cs +++ b/src/Tgstation.Server.Host/Models/CompileJob.cs @@ -50,7 +50,7 @@ namespace Tgstation.Server.Host.Models /// /// The origin of the repository the compile job was built from. /// - public string RepositoryOrigin { get; set; } + public string? RepositoryOrigin { get; set; } /// /// The source GitHub repository the deployment came from if any. @@ -63,14 +63,14 @@ namespace Tgstation.Server.Host.Models public int? GitHubDeploymentId { get; set; } /// - public override Version DMApiVersion + public override Version? DMApiVersion { get { if (!DMApiMajorVersion.HasValue) return null; - return new Version(DMApiMajorVersion.Value, DMApiMinorVersion.Value, DMApiPatchVersion.Value); + return new Version(DMApiMajorVersion.Value, DMApiMinorVersion!.Value, DMApiPatchVersion!.Value); } set @@ -81,6 +81,47 @@ namespace Tgstation.Server.Host.Models } } + /// + /// Initializes a new instance of the class. + /// + [Obsolete("For use by EFCore only", true)] + public CompileJob() + : this(null!, null!, null!, false) + { + } + + /// + /// Initializes a new instance of the class. + /// + /// The value of . + /// The value of . + /// The value of . + public CompileJob(Job job, RevisionInformation revisionInformation, string engineVersion) + : this(job, revisionInformation, engineVersion, true) + { + } + + /// + /// Initializes a new instance of the class. + /// + /// The value of . + /// The value of . + /// The value of . + /// If , , and should be checked for nulls. + CompileJob(Job job, RevisionInformation revisionInformation, string engineVersion, bool nullChecks) + { + if (nullChecks) + { + ArgumentNullException.ThrowIfNull(job); + ArgumentNullException.ThrowIfNull(revisionInformation); + ArgumentNullException.ThrowIfNull(engineVersion); + } + + Job = job; + RevisionInformation = revisionInformation; + EngineVersion = engineVersion; + } + /// public CompileJobResponse ToApi() => new() { diff --git a/src/Tgstation.Server.Host/Models/DreamDaemonSettings.cs b/src/Tgstation.Server.Host/Models/DreamDaemonSettings.cs index 5a26391015..0da7f22079 100644 --- a/src/Tgstation.Server.Host/Models/DreamDaemonSettings.cs +++ b/src/Tgstation.Server.Host/Models/DreamDaemonSettings.cs @@ -19,6 +19,6 @@ namespace Tgstation.Server.Host.Models /// The parent . /// [Required] - public Instance Instance { get; set; } + public Instance? Instance { get; set; } } } diff --git a/src/Tgstation.Server.Host/Models/DreamMakerSettings.cs b/src/Tgstation.Server.Host/Models/DreamMakerSettings.cs index 3a55c433d0..72f5a99f29 100644 --- a/src/Tgstation.Server.Host/Models/DreamMakerSettings.cs +++ b/src/Tgstation.Server.Host/Models/DreamMakerSettings.cs @@ -21,10 +21,10 @@ namespace Tgstation.Server.Host.Models /// The parent . /// [Required] - public Instance Instance { get; set; } + public Instance? Instance { get; set; } /// - public DreamMakerResponse ToApi() => new DreamMakerResponse + public DreamMakerResponse ToApi() => new() { ProjectName = ProjectName, ApiValidationPort = ApiValidationPort, diff --git a/src/Tgstation.Server.Host/Models/Instance.cs b/src/Tgstation.Server.Host/Models/Instance.cs index 82cfd5c707..a96470e9fa 100644 --- a/src/Tgstation.Server.Host/Models/Instance.cs +++ b/src/Tgstation.Server.Host/Models/Instance.cs @@ -17,22 +17,22 @@ namespace Tgstation.Server.Host.Models /// /// The for the . /// - public DreamMakerSettings DreamMakerSettings { get; set; } + public DreamMakerSettings? DreamMakerSettings { get; set; } /// /// The for the . /// - public DreamDaemonSettings DreamDaemonSettings { get; set; } + public DreamDaemonSettings? DreamDaemonSettings { get; set; } /// /// The for the . /// - public RepositorySettings RepositorySettings { get; set; } + public RepositorySettings? RepositorySettings { get; set; } /// /// The of the the server in the swarm this instance belongs to. /// - public string SwarmIdentifer { get; set; } + public string? SwarmIdentifer { get; set; } /// /// The s in the . @@ -54,6 +54,17 @@ namespace Tgstation.Server.Host.Models /// public ICollection Jobs { get; set; } + /// + /// Initializes a new instance of the class. + /// + public Instance() + { + InstancePermissionSets = new List(); + ChatSettings = new List(); + RevisionInformations = new List(); + Jobs = new List(); + } + /// public InstanceResponse ToApi() => new() { diff --git a/src/Tgstation.Server.Host/Models/InstancePermissionSet.cs b/src/Tgstation.Server.Host/Models/InstancePermissionSet.cs index 623e920734..8db1f17fba 100644 --- a/src/Tgstation.Server.Host/Models/InstancePermissionSet.cs +++ b/src/Tgstation.Server.Host/Models/InstancePermissionSet.cs @@ -21,16 +21,16 @@ namespace Tgstation.Server.Host.Models /// The the belongs to. /// [Required] - public Instance Instance { get; set; } + public Instance? Instance { get; set; } /// /// The the belongs to. /// [Required] - public PermissionSet PermissionSet { get; set; } + public PermissionSet? PermissionSet { get; set; } /// - public InstancePermissionSetResponse ToApi() => new InstancePermissionSetResponse + public InstancePermissionSetResponse ToApi() => new() { EngineRights = EngineRights, ChatBotRights = ChatBotRights, diff --git a/src/Tgstation.Server.Host/Models/Job.cs b/src/Tgstation.Server.Host/Models/Job.cs index 674330ee15..85327de2e2 100644 --- a/src/Tgstation.Server.Host/Models/Job.cs +++ b/src/Tgstation.Server.Host/Models/Job.cs @@ -18,18 +18,18 @@ namespace Tgstation.Server.Host.Models /// See . /// [Required] - public User StartedBy { get; set; } + public User? StartedBy { get; set; } /// /// See . /// - public User CancelledBy { get; set; } + public User? CancelledBy { get; set; } /// /// The the job belongs to if any. /// [Required] - public Instance Instance { get; set; } + public Instance? Instance { get; set; } /// /// Creates a new job for registering in the . @@ -40,7 +40,7 @@ namespace Tgstation.Server.Host.Models /// The used to generate the value of . /// The value of . will be derived from this. /// A new ready to be registered with the . - public static Job Create(JobCode code, User startedBy, Api.Models.Instance instance, TRight cancelRight) + public static Job Create(JobCode code, User? startedBy, Api.Models.Instance instance, TRight cancelRight) where TRight : Enum => new( code, @@ -56,7 +56,7 @@ namespace Tgstation.Server.Host.Models /// The value of . If , the user will be used. /// The used to generate the value of . /// A new ready to be registered with the . - public static Job Create(JobCode code, User startedBy, Api.Models.Instance instance) + public static Job Create(JobCode code, User? startedBy, Api.Models.Instance instance) => new( code, startedBy, @@ -89,7 +89,7 @@ namespace Tgstation.Server.Host.Models /// The value of . /// The value of . /// The value of . - Job(JobCode code, User startedBy, Api.Models.Instance instance, RightsType? cancelRightsType, ulong? cancelRight) + Job(JobCode code, User? startedBy, Api.Models.Instance instance, RightsType? cancelRightsType, ulong? cancelRight) { StartedBy = startedBy; ArgumentNullException.ThrowIfNull(instance); @@ -98,7 +98,7 @@ namespace Tgstation.Server.Host.Models Id = instance.Id ?? throw new InvalidOperationException("Instance associated with job does not have an Id!"), }; Description = typeof(JobCode) - .GetField(code.ToString()) + .GetField(code.ToString())! .GetCustomAttributes(false) .OfType() .First() @@ -112,8 +112,8 @@ namespace Tgstation.Server.Host.Models public JobResponse ToApi() => new() { Id = Id, - JobCode = JobCode.Value, - InstanceId = Instance.Id.Value, + JobCode = this.Require(x => x.JobCode), + InstanceId = (Instance ?? throw new InvalidOperationException("Instance needs to be set!")).Require(x => x.Id), StartedAt = StartedAt, StoppedAt = StoppedAt, Cancelled = Cancelled, @@ -123,7 +123,7 @@ namespace Tgstation.Server.Host.Models Description = Description, ExceptionDetails = ExceptionDetails, ErrorCode = ErrorCode, - StartedBy = StartedBy.CreateUserName(), + StartedBy = (StartedBy ?? throw new InvalidOperationException("StartedBy needs to be set!")).CreateUserName(), }; } } diff --git a/src/Tgstation.Server.Host/Models/ModelExtensions.cs b/src/Tgstation.Server.Host/Models/ModelExtensions.cs new file mode 100644 index 0000000000..0c88a1ab07 --- /dev/null +++ b/src/Tgstation.Server.Host/Models/ModelExtensions.cs @@ -0,0 +1,40 @@ +using System; +using System.Linq.Expressions; +using System.Reflection; + +using Tgstation.Server.Api.Models; + +namespace Tgstation.Server.Host.Models +{ + /// + /// Extensions for . + /// + static class ModelExtensions + { + /// + /// Require a given property of a given be non-. + /// + /// The of the being accessed. + /// The of the property being accessed. + /// The . + /// The access . + /// The value of in . + /// When in is . + public static TProperty Require(this TModel model, Expression> accessor) + where TModel : EntityId + where TProperty : struct + { + ArgumentNullException.ThrowIfNull(model); + ArgumentNullException.ThrowIfNull(accessor); + + var memberSelectorExpression = (MemberExpression)accessor.Body; + var property = (PropertyInfo)memberSelectorExpression.Member; + + var nullableValue = (TProperty?)property.GetValue(model); + if (!nullableValue.HasValue) + throw new InvalidOperationException($"Expected {model.GetType().Name}.{property.Name} to be set here!"); + + return nullableValue.Value; + } + } +} diff --git a/src/Tgstation.Server.Host/Models/OAuthConnection.cs b/src/Tgstation.Server.Host/Models/OAuthConnection.cs index e72476c134..d622b358c8 100644 --- a/src/Tgstation.Server.Host/Models/OAuthConnection.cs +++ b/src/Tgstation.Server.Host/Models/OAuthConnection.cs @@ -11,10 +11,10 @@ /// /// The owning . /// - public User User { get; set; } + public User? User { get; set; } /// - public Api.Models.OAuthConnection ToApi() => new Api.Models.OAuthConnection + public Api.Models.OAuthConnection ToApi() => new() { Provider = Provider, ExternalUserId = ExternalUserId, diff --git a/src/Tgstation.Server.Host/Models/PermissionSet.cs b/src/Tgstation.Server.Host/Models/PermissionSet.cs index 021a8239f2..2c18a2ae1f 100644 --- a/src/Tgstation.Server.Host/Models/PermissionSet.cs +++ b/src/Tgstation.Server.Host/Models/PermissionSet.cs @@ -18,23 +18,23 @@ namespace Tgstation.Server.Host.Models /// /// The the belongs to, if it is for a . /// - public User User { get; set; } + public User? User { get; set; } /// /// The the belongs to, if it is for a . /// - public UserGroup Group { get; set; } + public UserGroup? Group { get; set; } /// /// The s associated with the . /// - public ICollection InstancePermissionSets { get; set; } + public ICollection? InstancePermissionSets { get; set; } /// /// Convert the to it's API form. /// /// A new . - public Api.Models.PermissionSet ToApi() => new Api.Models.PermissionSet + public Api.Models.PermissionSet ToApi() => new() { Id = Id, AdministrationRights = AdministrationRights, diff --git a/src/Tgstation.Server.Host/Models/ReattachInformation.cs b/src/Tgstation.Server.Host/Models/ReattachInformation.cs index d4d1520d08..49360cf3e5 100644 --- a/src/Tgstation.Server.Host/Models/ReattachInformation.cs +++ b/src/Tgstation.Server.Host/Models/ReattachInformation.cs @@ -1,4 +1,5 @@ -using System.ComponentModel.DataAnnotations; +using System; +using System.ComponentModel.DataAnnotations; namespace Tgstation.Server.Host.Models { @@ -11,7 +12,7 @@ namespace Tgstation.Server.Host.Models /// The for the . /// [Required] - public CompileJob CompileJob { get; set; } + public CompileJob? CompileJob { get; set; } /// /// The of . @@ -21,11 +22,28 @@ namespace Tgstation.Server.Host.Models /// /// The the server was initially launched with in the case of Windows. /// - public CompileJob InitialCompileJob { get; set; } + public CompileJob? InitialCompileJob { get; set; } /// /// The of . /// public long? InitialCompileJobId { get; set; } + + /// + /// Initializes a new instance of the class. + /// + [Obsolete("For use by EFCore only", true)] + public ReattachInformation() + { + } + + /// + /// Initializes a new instance of the class. + /// + /// The access identifier for the . + public ReattachInformation(string accessIdentifier) + : base(accessIdentifier) + { + } } } diff --git a/src/Tgstation.Server.Host/Models/ReattachInformationBase.cs b/src/Tgstation.Server.Host/Models/ReattachInformationBase.cs index b233f0c017..fc6d07a8f3 100644 --- a/src/Tgstation.Server.Host/Models/ReattachInformationBase.cs +++ b/src/Tgstation.Server.Host/Models/ReattachInformationBase.cs @@ -22,10 +22,15 @@ namespace Tgstation.Server.Host.Models public int ProcessId { get; set; } /// - /// The port DreamDaemon was last listening on. + /// The port the game server was last listening on. /// public ushort Port { get; set; } + /// + /// The port the game server was last listening on for topics. + /// + public ushort? TopicPort { get; set; } + /// /// The current DreamDaemon reboot state. /// @@ -44,20 +49,32 @@ namespace Tgstation.Server.Host.Models /// /// Initializes a new instance of the class. /// + /// For use by EFCore only. protected ReattachInformationBase() { } + /// + /// Initializes a new instance of the class. + /// + /// The access identifier for the . + protected ReattachInformationBase(string accessIdentifier) + : base(accessIdentifier) + { + } + /// /// Initializes a new instance of the class. /// /// The to copy values from. protected ReattachInformationBase(ReattachInformationBase copy) + : base(copy == null + ? throw new ArgumentNullException(nameof(copy)) + : copy.AccessIdentifier) { - ArgumentNullException.ThrowIfNull(copy); Id = copy.Id; - AccessIdentifier = copy.AccessIdentifier; Port = copy.Port; + TopicPort = copy.TopicPort; ProcessId = copy.ProcessId; RebootState = copy.RebootState; LaunchSecurityLevel = copy.LaunchSecurityLevel; diff --git a/src/Tgstation.Server.Host/Models/RepositorySettings.cs b/src/Tgstation.Server.Host/Models/RepositorySettings.cs index b62f235d5e..e8fb7f6019 100644 --- a/src/Tgstation.Server.Host/Models/RepositorySettings.cs +++ b/src/Tgstation.Server.Host/Models/RepositorySettings.cs @@ -21,10 +21,10 @@ namespace Tgstation.Server.Host.Models /// The parent . /// [Required] - public Instance Instance { get; set; } + public Instance? Instance { get; set; } /// - public RepositoryResponse ToApi() => new RepositoryResponse + public RepositoryResponse ToApi() => new() { // AccessToken = AccessToken, // never show this AccessUser = AccessUser, diff --git a/src/Tgstation.Server.Host/Models/RevInfoTestMerge.cs b/src/Tgstation.Server.Host/Models/RevInfoTestMerge.cs index 4328910d0c..bed334b0bc 100644 --- a/src/Tgstation.Server.Host/Models/RevInfoTestMerge.cs +++ b/src/Tgstation.Server.Host/Models/RevInfoTestMerge.cs @@ -1,4 +1,5 @@ -using System.ComponentModel.DataAnnotations; +using System; +using System.ComponentModel.DataAnnotations; namespace Tgstation.Server.Host.Models { @@ -23,5 +24,26 @@ namespace Tgstation.Server.Host.Models /// [Required] public RevisionInformation RevisionInformation { get; set; } + + /// + /// Initializes a new instance of the class. + /// + [Obsolete("For use by EFCore only", true)] + public RevInfoTestMerge() + { + TestMerge = null!; + RevisionInformation = null!; + } + + /// + /// Initializes a new instance of the class. + /// + /// The value of . + /// The value of . + public RevInfoTestMerge(TestMerge testMerge, RevisionInformation revisionInformation) + { + TestMerge = testMerge ?? throw new ArgumentNullException(nameof(testMerge)); + RevisionInformation = revisionInformation ?? throw new ArgumentNullException(nameof(revisionInformation)); + } } } diff --git a/src/Tgstation.Server.Host/Models/RevisionInformation.cs b/src/Tgstation.Server.Host/Models/RevisionInformation.cs index 290e118ae9..eb99816ab6 100644 --- a/src/Tgstation.Server.Host/Models/RevisionInformation.cs +++ b/src/Tgstation.Server.Host/Models/RevisionInformation.cs @@ -1,4 +1,5 @@ -using System.Collections.Generic; +using System; +using System.Collections.Generic; using System.ComponentModel.DataAnnotations; using System.Linq; @@ -21,32 +22,32 @@ namespace Tgstation.Server.Host.Models /// The the belongs to. /// [Required] - public Instance Instance { get; set; } + public Instance? Instance { get; set; } /// /// See . /// - public TestMerge PrimaryTestMerge { get; set; } + public TestMerge? PrimaryTestMerge { get; set; } /// /// See . /// - public ICollection ActiveTestMerges { get; set; } + public ICollection? ActiveTestMerges { get; set; } /// /// See s made from this . /// - public ICollection CompileJobs { get; set; } + public ICollection? CompileJobs { get; set; } /// - public Api.Models.RevisionInformation ToApi() => new Api.Models.RevisionInformation + public Api.Models.RevisionInformation ToApi() => new() { CommitSha = CommitSha, Timestamp = Timestamp, OriginCommitSha = OriginCommitSha, PrimaryTestMerge = PrimaryTestMerge?.ToApi(), - ActiveTestMerges = ActiveTestMerges.Select(x => x.TestMerge.ToApi()).ToList(), - CompileJobs = CompileJobs.Select(x => new Api.Models.EntityId + ActiveTestMerges = (ActiveTestMerges ?? throw new InvalidOperationException("ActiveTestMerges must be set!")).Select(x => x.TestMerge.ToApi()).ToList(), + CompileJobs = (CompileJobs ?? throw new InvalidOperationException("CompileJobs must be set!")).Select(x => new Api.Models.EntityId { Id = x.Id, }).ToList(), diff --git a/src/Tgstation.Server.Host/Models/TestMerge.cs b/src/Tgstation.Server.Host/Models/TestMerge.cs index 43d7f35b28..3641640378 100644 --- a/src/Tgstation.Server.Host/Models/TestMerge.cs +++ b/src/Tgstation.Server.Host/Models/TestMerge.cs @@ -1,4 +1,5 @@ -using System.Collections.Generic; +using System; +using System.Collections.Generic; using System.ComponentModel.DataAnnotations; namespace Tgstation.Server.Host.Models @@ -10,13 +11,13 @@ namespace Tgstation.Server.Host.Models /// See . /// [Required] - public User MergedBy { get; set; } + public User? MergedBy { get; set; } /// /// The initial the was merged with. /// [Required] - public RevisionInformation PrimaryRevisionInformation { get; set; } + public RevisionInformation? PrimaryRevisionInformation { get; set; } /// /// Foreign key for . @@ -26,10 +27,10 @@ namespace Tgstation.Server.Host.Models /// /// All the for the . /// - public ICollection RevisonInformations { get; set; } + public ICollection? RevisonInformations { get; set; } /// - public Api.Models.TestMerge ToApi() => new Api.Models.TestMerge + public Api.Models.TestMerge ToApi() => new() { Author = Author, BodyAtMerge = BodyAtMerge, @@ -37,7 +38,7 @@ namespace Tgstation.Server.Host.Models TitleAtMerge = TitleAtMerge, Comment = Comment, Id = Id, - MergedBy = MergedBy.CreateUserName(), + MergedBy = (MergedBy ?? throw new InvalidOperationException("MergedBy must be set!")).CreateUserName(), Number = Number, TargetCommitSha = TargetCommitSha, Url = Url, diff --git a/src/Tgstation.Server.Host/Models/User.cs b/src/Tgstation.Server.Host/Models/User.cs index 0d275c7d18..0939dd8042 100644 --- a/src/Tgstation.Server.Host/Models/User.cs +++ b/src/Tgstation.Server.Host/Models/User.cs @@ -19,17 +19,17 @@ namespace Tgstation.Server.Host.Models /// /// The hash of the user's password. /// - public string PasswordHash { get; set; } + public string? PasswordHash { get; set; } /// /// See . /// - public User CreatedBy { get; set; } + public User? CreatedBy { get; set; } /// /// The the belongs to, if any. /// - public UserGroup Group { get; set; } + public UserGroup? Group { get; set; } /// /// The ID of the 's . @@ -39,14 +39,14 @@ namespace Tgstation.Server.Host.Models /// /// The the has, if any. /// - public PermissionSet PermissionSet { get; set; } + public PermissionSet? PermissionSet { get; set; } /// /// The uppercase invariant of . /// [Required] [StringLength(Limits.MaximumIndexableStringLength, MinimumLength = 1)] - public string CanonicalName { get; set; } + public string? CanonicalName { get; set; } /// /// When was last changed. @@ -56,17 +56,17 @@ namespace Tgstation.Server.Host.Models /// /// s created by this . /// - public ICollection CreatedUsers { get; set; } + public ICollection? CreatedUsers { get; set; } /// /// The s made by the . /// - public ICollection TestMerges { get; set; } + public ICollection? TestMerges { get; set; } /// /// The s made by the . /// - public ICollection OAuthConnections { get; set; } + public ICollection? OAuthConnections { get; set; } /// /// Change a into a . diff --git a/src/Tgstation.Server.Host/Models/UserGroup.cs b/src/Tgstation.Server.Host/Models/UserGroup.cs index 63f351c329..4451c1a073 100644 --- a/src/Tgstation.Server.Host/Models/UserGroup.cs +++ b/src/Tgstation.Server.Host/Models/UserGroup.cs @@ -1,4 +1,5 @@ -using System.Collections.Generic; +using System; +using System.Collections.Generic; using System.ComponentModel.DataAnnotations; using System.Linq; @@ -16,12 +17,12 @@ namespace Tgstation.Server.Host.Models /// The the has. /// [Required] - public PermissionSet PermissionSet { get; set; } + public PermissionSet? PermissionSet { get; set; } /// /// The s the has. /// - public ICollection Users { get; set; } + public ICollection? Users { get; set; } /// /// Convert the to it's API form. @@ -32,7 +33,7 @@ namespace Tgstation.Server.Host.Models { Id = Id, Name = Name, - PermissionSet = PermissionSet.ToApi(), + PermissionSet = (PermissionSet ?? throw new InvalidOperationException("PermissionSet must be set!")).ToApi(), Users = showUsers ? Users ?.Select(x => x.CreateUserName()) diff --git a/src/Tgstation.Server.Host/Program.cs b/src/Tgstation.Server.Host/Program.cs index e372c0563d..b23633b7df 100644 --- a/src/Tgstation.Server.Host/Program.cs +++ b/src/Tgstation.Server.Host/Program.cs @@ -44,7 +44,7 @@ namespace Tgstation.Server.Host public static async Task Main(string[] args) { // first arg is 100% always the update path, starting it otherwise is solely for debugging purposes - string updatePath = null; + string? updatePath = null; if (args.Length > 0) { var listArgs = new List(args); @@ -77,13 +77,13 @@ namespace Tgstation.Server.Host /// The command line arguments, minus the . /// The path to extract server updates to be applied to. /// A resulting in the . - internal async ValueTask Main(string[] args, string updatePath) + internal async ValueTask Main(string[] args, string? updatePath) { try { using var shutdownNotifier = new ProgramShutdownTokenSource(); var cancellationToken = shutdownNotifier.Token; - IServer server; + IServer? server; try { server = await ServerFactory.CreateServer( diff --git a/src/Tgstation.Server.Host/Properties/MasterVersionsAttribute.cs b/src/Tgstation.Server.Host/Properties/MasterVersionsAttribute.cs index d6461713bb..d77bcd349b 100644 --- a/src/Tgstation.Server.Host/Properties/MasterVersionsAttribute.cs +++ b/src/Tgstation.Server.Host/Properties/MasterVersionsAttribute.cs @@ -14,7 +14,7 @@ namespace Tgstation.Server.Host.Properties /// public static MasterVersionsAttribute Instance => Assembly .GetExecutingAssembly() - .GetCustomAttribute(); + .GetCustomAttribute()!; /// /// The of the version built. diff --git a/src/Tgstation.Server.Host/Security/AuthenticationContext.cs b/src/Tgstation.Server.Host/Security/AuthenticationContext.cs index 5ed4dafe3f..53bf23c2a2 100644 --- a/src/Tgstation.Server.Host/Security/AuthenticationContext.cs +++ b/src/Tgstation.Server.Host/Security/AuthenticationContext.cs @@ -13,16 +13,26 @@ namespace Tgstation.Server.Host.Security public bool Valid { get; private set; } /// - public User User { get; private set; } + public User User => user ?? throw new InvalidOperationException("AuthenticationContext is invalid!"); /// - public PermissionSet PermissionSet { get; private set; } + public PermissionSet PermissionSet => permissionSet ?? throw new InvalidOperationException("AuthenticationContext is invalid!"); /// - public InstancePermissionSet InstancePermissionSet { get; private set; } + public InstancePermissionSet? InstancePermissionSet { get; private set; } /// - public ISystemIdentity SystemIdentity { get; private set; } + public ISystemIdentity? SystemIdentity { get; private set; } + + /// + /// Backing field for . + /// + User? user; + + /// + /// Backing field for . + /// + PermissionSet? permissionSet; /// /// Initializes a new instance of the class. @@ -40,13 +50,13 @@ namespace Tgstation.Server.Host.Security /// The value of . /// The value of . /// The value of . - public void Initialize(ISystemIdentity systemIdentity, User user, InstancePermissionSet instanceUser) + public void Initialize(ISystemIdentity? systemIdentity, User user, InstancePermissionSet? instanceUser) { - User = user ?? throw new ArgumentNullException(nameof(user)); + this.user = user ?? throw new ArgumentNullException(nameof(user)); if (systemIdentity == null && User.SystemIdentifier != null) throw new ArgumentNullException(nameof(systemIdentity)); - PermissionSet = user.PermissionSet - ?? user.Group.PermissionSet + permissionSet = user.PermissionSet + ?? user.Group!.PermissionSet ?? throw new ArgumentException("No PermissionSet provider", nameof(user)); InstancePermissionSet = instanceUser; SystemIdentity = systemIdentity; @@ -72,16 +82,14 @@ namespace Tgstation.Server.Host.Security var nullableType = typeof(Nullable<>); var nullableRightsType = nullableType.MakeGenericType(rightsEnum); - var prop = typeToCheck.GetProperties().Where(x => x.PropertyType == nullableRightsType).First(); + var prop = typeToCheck.GetProperties().Where(x => x.PropertyType == nullableRightsType && x.CanRead).First(); - var right = prop.GetMethod.Invoke( + var right = prop.GetMethod!.Invoke( isInstance ? InstancePermissionSet : PermissionSet, - Array.Empty()); - - if (right == null) - throw new InvalidOperationException("A user right was null!"); + Array.Empty()) + ?? throw new InvalidOperationException("A user right was null!"); return (ulong)right; } diff --git a/src/Tgstation.Server.Host/Security/AuthenticationContextAuthorizationFilter.cs b/src/Tgstation.Server.Host/Security/AuthenticationContextAuthorizationFilter.cs index 562f49f443..b009f599ad 100644 --- a/src/Tgstation.Server.Host/Security/AuthenticationContextAuthorizationFilter.cs +++ b/src/Tgstation.Server.Host/Security/AuthenticationContextAuthorizationFilter.cs @@ -5,6 +5,8 @@ using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Mvc.Filters; using Microsoft.Extensions.Logging; +using Tgstation.Server.Host.Models; + namespace Tgstation.Server.Host.Security { /// @@ -43,7 +45,7 @@ namespace Tgstation.Server.Host.Security return; } - if (authenticationContext.User.Enabled.Value) + if (authenticationContext.User.Require(x => x.Enabled)) return; logger.LogTrace("authenticationContext is for a disabled user!"); diff --git a/src/Tgstation.Server.Host/Security/AuthenticationContextClaimsTransformation.cs b/src/Tgstation.Server.Host/Security/AuthenticationContextClaimsTransformation.cs index 4f02bb0b0d..c5b1fe0ce2 100644 --- a/src/Tgstation.Server.Host/Security/AuthenticationContextClaimsTransformation.cs +++ b/src/Tgstation.Server.Host/Security/AuthenticationContextClaimsTransformation.cs @@ -28,7 +28,7 @@ namespace Tgstation.Server.Host.Security /// /// The for the . /// - readonly ApiHeaders apiHeaders; + readonly ApiHeaders? apiHeaders; /// /// Initializes a new instance of the class. diff --git a/src/Tgstation.Server.Host/Security/AuthenticationContextFactory.cs b/src/Tgstation.Server.Host/Security/AuthenticationContextFactory.cs index f39daaa989..80a9f1df92 100644 --- a/src/Tgstation.Server.Host/Security/AuthenticationContextFactory.cs +++ b/src/Tgstation.Server.Host/Security/AuthenticationContextFactory.cs @@ -88,7 +88,7 @@ namespace Tgstation.Server.Host.Security .Include(x => x.CreatedBy) .Include(x => x.PermissionSet) .Include(x => x.Group) - .ThenInclude(x => x.PermissionSet) + .ThenInclude(x => x!.PermissionSet) .Include(x => x.OAuthConnections) .FirstOrDefaultAsync(cancellationToken); if (user == default) @@ -97,7 +97,7 @@ namespace Tgstation.Server.Host.Security return currentAuthenticationContext; } - ISystemIdentity systemIdentity; + ISystemIdentity? systemIdentity; if (user.SystemIdentifier != null) systemIdentity = identityCache.LoadCachedIdentity(user); else @@ -111,15 +111,15 @@ namespace Tgstation.Server.Host.Security systemIdentity = null; } - var userPermissionSet = user.PermissionSet ?? user.Group.PermissionSet; + var userPermissionSet = user.PermissionSet ?? user.Group!.PermissionSet; try { - InstancePermissionSet instancePermissionSet = null; + InstancePermissionSet? instancePermissionSet = null; if (instanceId.HasValue) { instancePermissionSet = await databaseContext.InstancePermissionSets .AsQueryable() - .Where(x => x.PermissionSetId == userPermissionSet.Id && x.InstanceId == instanceId && x.Instance.SwarmIdentifer == swarmConfiguration.Identifier) + .Where(x => x.PermissionSetId == userPermissionSet!.Id && x.InstanceId == instanceId && x.Instance!.SwarmIdentifer == swarmConfiguration.Identifier) .Include(x => x.Instance) .FirstOrDefaultAsync(cancellationToken); diff --git a/src/Tgstation.Server.Host/Security/AuthorizationContextHubFilter.cs b/src/Tgstation.Server.Host/Security/AuthorizationContextHubFilter.cs index 38360cab9b..3eefac4f2e 100644 --- a/src/Tgstation.Server.Host/Security/AuthorizationContextHubFilter.cs +++ b/src/Tgstation.Server.Host/Security/AuthorizationContextHubFilter.cs @@ -5,6 +5,8 @@ using System.Threading.Tasks; using Microsoft.AspNetCore.SignalR; using Microsoft.Extensions.Logging; +using Tgstation.Server.Host.Models; + namespace Tgstation.Server.Host.Security { /// @@ -44,7 +46,7 @@ namespace Tgstation.Server.Host.Security } /// - public async ValueTask InvokeMethodAsync(HubInvocationContext invocationContext, Func> next) + public async ValueTask InvokeMethodAsync(HubInvocationContext invocationContext, Func> next) { ArgumentNullException.ThrowIfNull(invocationContext); if (ValidateAuthenticationContext(invocationContext.Hub)) @@ -62,7 +64,7 @@ namespace Tgstation.Server.Host.Security { if (!authenticationContext.Valid) logger.LogTrace("The token for connection {connectionId} is no longer authenticated! Aborting...", hub.Context.ConnectionId); - else if (!authenticationContext.User.Enabled.Value) + else if (!authenticationContext.User.Require(x => x.Enabled)) logger.LogTrace("The token for connection {connectionId} is no longer authorized! Aborting...", hub.Context.ConnectionId); else return true; @@ -73,8 +75,8 @@ namespace Tgstation.Server.Host.Security prop => prop.PropertyType.IsConstructedGenericType && prop.Name == nameof(hub.Clients)); var clients = typedClientsProperty.GetValue(hub); - var callerProperty = clients.GetType().GetProperty(nameof(hub.Clients.Caller)); - var caller = callerProperty.GetValue(clients); + var callerProperty = clients!.GetType().GetProperty(nameof(hub.Clients.Caller)); + var caller = callerProperty!.GetValue(clients); hub.Context.Abort(); return false; diff --git a/src/Tgstation.Server.Host/Security/CryptographySuite.cs b/src/Tgstation.Server.Host/Security/CryptographySuite.cs index d98e61c303..6a401e7575 100644 --- a/src/Tgstation.Server.Host/Security/CryptographySuite.cs +++ b/src/Tgstation.Server.Host/Security/CryptographySuite.cs @@ -55,6 +55,9 @@ namespace Tgstation.Server.Host.Security ArgumentNullException.ThrowIfNull(user); ArgumentNullException.ThrowIfNull(password); + if (user.PasswordHash == null) + throw new ArgumentException("user must have PasswordHash!", nameof(user)); + var result = passwordHasher.VerifyHashedPassword(user, user.PasswordHash, password); switch (result) { diff --git a/src/Tgstation.Server.Host/Security/IAuthenticationContext.cs b/src/Tgstation.Server.Host/Security/IAuthenticationContext.cs index 36abee2b2c..8277a6601d 100644 --- a/src/Tgstation.Server.Host/Security/IAuthenticationContext.cs +++ b/src/Tgstation.Server.Host/Security/IAuthenticationContext.cs @@ -26,7 +26,7 @@ namespace Tgstation.Server.Host.Security /// /// The 's effective if applicable. /// - InstancePermissionSet InstancePermissionSet { get; } + InstancePermissionSet? InstancePermissionSet { get; } /// /// Get the value of a given . @@ -38,6 +38,6 @@ namespace Tgstation.Server.Host.Security /// /// The of if applicable. /// - ISystemIdentity SystemIdentity { get; } + ISystemIdentity? SystemIdentity { get; } } } diff --git a/src/Tgstation.Server.Host/Security/IIdentityCache.cs b/src/Tgstation.Server.Host/Security/IIdentityCache.cs index 0832189f83..75010f2c64 100644 --- a/src/Tgstation.Server.Host/Security/IIdentityCache.cs +++ b/src/Tgstation.Server.Host/Security/IIdentityCache.cs @@ -21,7 +21,7 @@ namespace Tgstation.Server.Host.Security /// Attempt to load a cached . /// /// The the belongs to. - /// The cached or if it doesn't exist or expired. + /// The cached . ISystemIdentity LoadCachedIdentity(User user); } } diff --git a/src/Tgstation.Server.Host/Security/ISystemIdentityFactory.cs b/src/Tgstation.Server.Host/Security/ISystemIdentityFactory.cs index 2568861e53..e2e245e38e 100644 --- a/src/Tgstation.Server.Host/Security/ISystemIdentityFactory.cs +++ b/src/Tgstation.Server.Host/Security/ISystemIdentityFactory.cs @@ -22,7 +22,7 @@ namespace Tgstation.Server.Host.Security /// The user to create a for. /// The for the operation. /// A resulting in a new based on the given or if the has no . - Task CreateSystemIdentity(User user, CancellationToken cancellationToken); + Task CreateSystemIdentity(User user, CancellationToken cancellationToken); /// /// Create a for a given username and password. @@ -30,7 +30,7 @@ namespace Tgstation.Server.Host.Security /// The username of the user. /// The password of the user. /// The for the operation. - /// A resulting in a new based on the given credentials. - Task CreateSystemIdentity(string username, string password, CancellationToken cancellationToken); + /// A resulting in a new based on the given credentials on success, on failure. + Task CreateSystemIdentity(string username, string password, CancellationToken cancellationToken); } } diff --git a/src/Tgstation.Server.Host/Security/IdentityCache.cs b/src/Tgstation.Server.Host/Security/IdentityCache.cs index 0db031aba2..c42ffab3f6 100644 --- a/src/Tgstation.Server.Host/Security/IdentityCache.cs +++ b/src/Tgstation.Server.Host/Security/IdentityCache.cs @@ -54,12 +54,14 @@ namespace Tgstation.Server.Host.Security ArgumentNullException.ThrowIfNull(user); ArgumentNullException.ThrowIfNull(systemIdentity); + var uid = user.Require(x => x.Id); + var sysId = systemIdentity.Uid; + lock (cachedIdentities) { - var uid = systemIdentity.Uid; - logger.LogDebug("Caching system identity {0} of user {1}", uid, user.Id); + logger.LogDebug("Caching system identity {sysId} of user {uid}", sysId, uid); - if (cachedIdentities.TryGetValue(user.Id.Value, out var identCache)) + if (cachedIdentities.TryGetValue(uid, out var identCache)) { logger.LogTrace("Expiring previously cached identity..."); identCache.Dispose(); // also clears it out @@ -70,12 +72,12 @@ namespace Tgstation.Server.Host.Security asyncDelayer, () => { - logger.LogDebug("Expiring system identity cache for user {0}", user.Id); + logger.LogDebug("Expiring system identity cache for user {uid}", uid); lock (cachedIdentities) - cachedIdentities.Remove(user.Id.Value); + cachedIdentities.Remove(uid); }, expiry); - cachedIdentities.Add(user.Id.Value, identCache); + cachedIdentities.Add(uid, identCache); } } @@ -83,9 +85,11 @@ namespace Tgstation.Server.Host.Security public ISystemIdentity LoadCachedIdentity(User user) { ArgumentNullException.ThrowIfNull(user); + var uid = user.Require(x => x.Id); lock (cachedIdentities) - if (cachedIdentities.TryGetValue(user.Id.Value, out var identity)) + if (cachedIdentities.TryGetValue(uid, out var identity)) return identity.SystemIdentity.Clone(); + throw new InvalidOperationException("Cached system identity has expired!"); } } diff --git a/src/Tgstation.Server.Host/Security/OAuth/GenericOAuthValidator.cs b/src/Tgstation.Server.Host/Security/OAuth/GenericOAuthValidator.cs index 668a96240f..5963023b0f 100644 --- a/src/Tgstation.Server.Host/Security/OAuth/GenericOAuthValidator.cs +++ b/src/Tgstation.Server.Host/Security/OAuth/GenericOAuthValidator.cs @@ -80,11 +80,11 @@ namespace Tgstation.Server.Host.Security.OAuth } /// - public async ValueTask ValidateResponseCode(string code, CancellationToken cancellationToken) + public async ValueTask ValidateResponseCode(string code, CancellationToken cancellationToken) { using var httpClient = CreateHttpClient(); - string tokenResponsePayload = null; - string userInformationPayload = null; + string? tokenResponsePayload = null; + string? userInformationPayload = null; try { Logger.LogTrace("Validating response code..."); @@ -97,7 +97,7 @@ namespace Tgstation.Server.Host.Security.OAuth tokenRequestPayload, SerializerSettings()); - var tokenRequestDictionary = JsonConvert.DeserializeObject>(tokenRequestJson); + var tokenRequestDictionary = JsonConvert.DeserializeObject>(tokenRequestJson)!; tokenRequest.Content = new FormUrlEncodedContent(tokenRequestDictionary); using var tokenResponse = await httpClient.SendAsync(tokenRequest, HttpCompletionOption.ResponseHeadersRead, cancellationToken); diff --git a/src/Tgstation.Server.Host/Security/OAuth/GitHubOAuthValidator.cs b/src/Tgstation.Server.Host/Security/OAuth/GitHubOAuthValidator.cs index 567887b06f..3f3180f9da 100644 --- a/src/Tgstation.Server.Host/Security/OAuth/GitHubOAuthValidator.cs +++ b/src/Tgstation.Server.Host/Security/OAuth/GitHubOAuthValidator.cs @@ -52,7 +52,7 @@ namespace Tgstation.Server.Host.Security.OAuth } /// - public async ValueTask ValidateResponseCode(string code, CancellationToken cancellationToken) + public async ValueTask ValidateResponseCode(string code, CancellationToken cancellationToken) { ArgumentNullException.ThrowIfNull(code); diff --git a/src/Tgstation.Server.Host/Security/OAuth/IOAuthProviders.cs b/src/Tgstation.Server.Host/Security/OAuth/IOAuthProviders.cs index c8b8a7b161..2da6651c04 100644 --- a/src/Tgstation.Server.Host/Security/OAuth/IOAuthProviders.cs +++ b/src/Tgstation.Server.Host/Security/OAuth/IOAuthProviders.cs @@ -14,7 +14,7 @@ namespace Tgstation.Server.Host.Security.OAuth /// /// The to get the validator for. /// The for . - IOAuthValidator GetValidator(OAuthProvider oAuthProvider); + IOAuthValidator? GetValidator(OAuthProvider oAuthProvider); /// /// Gets a of the provider client IDs. diff --git a/src/Tgstation.Server.Host/Security/OAuth/IOAuthValidator.cs b/src/Tgstation.Server.Host/Security/OAuth/IOAuthValidator.cs index 7f3ca57287..472a7ff091 100644 --- a/src/Tgstation.Server.Host/Security/OAuth/IOAuthValidator.cs +++ b/src/Tgstation.Server.Host/Security/OAuth/IOAuthValidator.cs @@ -27,6 +27,6 @@ namespace Tgstation.Server.Host.Security.OAuth /// The OAuth response string from web application. /// The for the operation. /// A resulting in if authentication failed, if a rate limit occurred, and the validated otherwise. - ValueTask ValidateResponseCode(string code, CancellationToken cancellationToken); + ValueTask ValidateResponseCode(string code, CancellationToken cancellationToken); } } diff --git a/src/Tgstation.Server.Host/Security/OAuth/InvisionCommunityOAuthValidator.cs b/src/Tgstation.Server.Host/Security/OAuth/InvisionCommunityOAuthValidator.cs index a0c8cdd0e2..0e912bd368 100644 --- a/src/Tgstation.Server.Host/Security/OAuth/InvisionCommunityOAuthValidator.cs +++ b/src/Tgstation.Server.Host/Security/OAuth/InvisionCommunityOAuthValidator.cs @@ -12,7 +12,7 @@ namespace Tgstation.Server.Host.Security.OAuth /// OAuth validator for Invision Community (selfhosted). /// sealed class InvisionCommunityOAuthValidator : GenericOAuthValidator - { + { /// public override OAuthProvider Provider => OAuthProvider.InvisionCommunity; diff --git a/src/Tgstation.Server.Host/Security/OAuth/OAuthProviders.cs b/src/Tgstation.Server.Host/Security/OAuth/OAuthProviders.cs index 7a562668ee..9d28ab1c82 100644 --- a/src/Tgstation.Server.Host/Security/OAuth/OAuthProviders.cs +++ b/src/Tgstation.Server.Host/Security/OAuth/OAuthProviders.cs @@ -80,7 +80,7 @@ namespace Tgstation.Server.Host.Security.OAuth } /// - public IOAuthValidator GetValidator(OAuthProvider oAuthProvider) => validators.FirstOrDefault(x => x.Provider == oAuthProvider); + public IOAuthValidator? GetValidator(OAuthProvider oAuthProvider) => validators.FirstOrDefault(x => x.Provider == oAuthProvider); /// public Dictionary ProviderInfos() diff --git a/src/Tgstation.Server.Host/Security/OAuth/OAuthTokenRequest.cs b/src/Tgstation.Server.Host/Security/OAuth/OAuthTokenRequest.cs index 9256092f10..a3955ceaff 100644 --- a/src/Tgstation.Server.Host/Security/OAuth/OAuthTokenRequest.cs +++ b/src/Tgstation.Server.Host/Security/OAuth/OAuthTokenRequest.cs @@ -22,7 +22,7 @@ namespace Tgstation.Server.Host.Security.OAuth /// /// The OAuth redirect URI. /// - public Uri RedirectUri { get; } + public Uri? RedirectUri { get; } /// /// The OAuth grant type. diff --git a/src/Tgstation.Server.Host/Security/PosixSystemIdentityFactory.cs b/src/Tgstation.Server.Host/Security/PosixSystemIdentityFactory.cs index e85882665b..062a42747f 100644 --- a/src/Tgstation.Server.Host/Security/PosixSystemIdentityFactory.cs +++ b/src/Tgstation.Server.Host/Security/PosixSystemIdentityFactory.cs @@ -16,9 +16,9 @@ namespace Tgstation.Server.Host.Security public ISystemIdentity GetCurrent() => new PosixSystemIdentity(); /// - public Task CreateSystemIdentity(User user, CancellationToken cancellationToken) => throw new NotImplementedException(); + public Task CreateSystemIdentity(User user, CancellationToken cancellationToken) => throw new NotImplementedException(); /// - public Task CreateSystemIdentity(string username, string password, CancellationToken cancellationToken) => throw new NotImplementedException(); + public Task CreateSystemIdentity(string username, string password, CancellationToken cancellationToken) => throw new NotImplementedException(); } } diff --git a/src/Tgstation.Server.Host/Security/TokenFactory.cs b/src/Tgstation.Server.Host/Security/TokenFactory.cs index cc18c3298a..fcc5990c2f 100644 --- a/src/Tgstation.Server.Host/Security/TokenFactory.cs +++ b/src/Tgstation.Server.Host/Security/TokenFactory.cs @@ -10,6 +10,7 @@ using Microsoft.IdentityModel.Tokens; using Tgstation.Server.Api.Models.Response; using Tgstation.Server.Host.Configuration; +using Tgstation.Server.Host.Models; using Tgstation.Server.Host.System; namespace Tgstation.Server.Host.Security @@ -82,10 +83,11 @@ namespace Tgstation.Server.Host.Security } /// - public TokenResponse CreateToken(Models.User user, bool oAuth) + public TokenResponse CreateToken(User user, bool oAuth) { ArgumentNullException.ThrowIfNull(user); + var uid = user.Require(x => x.Id); var now = DateTimeOffset.UtcNow; var nowUnix = now.ToUnixTimeSeconds(); @@ -113,7 +115,7 @@ namespace Tgstation.Server.Host.Security Enumerable.Empty(), new Dictionary { - { JwtRegisteredClaimNames.Sub, user.Id.Value.ToString(CultureInfo.InvariantCulture) }, + { JwtRegisteredClaimNames.Sub, uid.ToString(CultureInfo.InvariantCulture) }, }, notBefore.UtcDateTime, expiry.UtcDateTime, diff --git a/src/Tgstation.Server.Host/Security/WindowsSystemIdentity.cs b/src/Tgstation.Server.Host/Security/WindowsSystemIdentity.cs index 28b6bbcec7..e15b51557d 100644 --- a/src/Tgstation.Server.Host/Security/WindowsSystemIdentity.cs +++ b/src/Tgstation.Server.Host/Security/WindowsSystemIdentity.cs @@ -16,10 +16,10 @@ namespace Tgstation.Server.Host.Security sealed class WindowsSystemIdentity : ISystemIdentity { /// - public string Uid => (userPrincipal?.Sid ?? identity.User).ToString(); + public string Uid => (userPrincipal?.Sid ?? identity!.User!).ToString(); // we kno user isn't null because it can only be the case when anonymous (checked in this constructor) /// - public string Username => userPrincipal?.Name ?? identity.Name; + public string Username => userPrincipal?.Name ?? identity!.Name; /// public bool CanCreateSymlinks => canCreateSymlinks ?? throw new NotSupportedException(); @@ -27,12 +27,12 @@ namespace Tgstation.Server.Host.Security /// /// The for the . /// - readonly WindowsIdentity identity; + readonly WindowsIdentity? identity; /// /// The for the . /// - readonly UserPrincipal userPrincipal; + readonly UserPrincipal? userPrincipal; /// /// Backing field for . @@ -46,6 +46,9 @@ namespace Tgstation.Server.Host.Security public WindowsSystemIdentity(WindowsIdentity identity) { this.identity = identity ?? throw new ArgumentNullException(nameof(identity)); + if (identity.IsAnonymous) + throw new InvalidOperationException($"Cannot use anonymous {nameof(WindowsIdentity)} as a {nameof(WindowsSystemIdentity)}!"); + canCreateSymlinks = new WindowsPrincipal(identity).IsInRole(WindowsBuiltInRole.Administrator); } @@ -65,7 +68,7 @@ namespace Tgstation.Server.Host.Security identity.Dispose(); else { - var context = userPrincipal.Context; + var context = userPrincipal!.Context; userPrincipal.Dispose(); context.Dispose(); } diff --git a/src/Tgstation.Server.Host/Security/WindowsSystemIdentityFactory.cs b/src/Tgstation.Server.Host/Security/WindowsSystemIdentityFactory.cs index b677a56413..ca47a87192 100644 --- a/src/Tgstation.Server.Host/Security/WindowsSystemIdentityFactory.cs +++ b/src/Tgstation.Server.Host/Security/WindowsSystemIdentityFactory.cs @@ -1,4 +1,5 @@ using System; +using System.Diagnostics.CodeAnalysis; using System.DirectoryServices.AccountManagement; using System.Runtime.Versioning; using System.Security.Principal; @@ -31,7 +32,7 @@ namespace Tgstation.Server.Host.Security /// The input . /// The output username. /// The output domain name. May be . - static void GetUserAndDomainName(string input, out string username, out string domainName) + static void GetUserAndDomainName(string input, out string username, out string? domainName) { var splits = input.Split('\\'); username = splits.Length > 1 ? splits[1] : splits[0]; @@ -51,7 +52,7 @@ namespace Tgstation.Server.Host.Security public ISystemIdentity GetCurrent() => new WindowsSystemIdentity(WindowsIdentity.GetCurrent()); /// - public Task CreateSystemIdentity(User user, CancellationToken cancellationToken) => Task.Factory.StartNew( + public Task CreateSystemIdentity(User user, CancellationToken cancellationToken) => Task.Factory.StartNew( () => { ArgumentNullException.ThrowIfNull(user); @@ -59,13 +60,12 @@ namespace Tgstation.Server.Host.Security if (user.SystemIdentifier == null) throw new InvalidOperationException("User's SystemIdentifier must not be null!"); - PrincipalContext pc = null; - UserPrincipal principal = null; - + PrincipalContext? pc = null; GetUserAndDomainName(user.SystemIdentifier, out _, out var domainName); - bool TryGetPrincipalFromContextType(ContextType contextType) + bool TryGetPrincipalFromContextType(ContextType contextType, [NotNullWhen(true)] out UserPrincipal? principal) { + principal = null; try { pc = domainName != null @@ -98,7 +98,7 @@ namespace Tgstation.Server.Host.Security return principal != null; } - if (!TryGetPrincipalFromContextType(ContextType.Machine) && !TryGetPrincipalFromContextType(ContextType.Domain)) + if (!TryGetPrincipalFromContextType(ContextType.Machine, out var principal) && !TryGetPrincipalFromContextType(ContextType.Domain, out principal)) return null; return (ISystemIdentity)new WindowsSystemIdentity(principal); }, @@ -107,7 +107,7 @@ namespace Tgstation.Server.Host.Security TaskScheduler.Current); /// - public Task CreateSystemIdentity(string username, string password, CancellationToken cancellationToken) => Task.Factory.StartNew( + public Task CreateSystemIdentity(string username, string password, CancellationToken cancellationToken) => Task.Factory.StartNew( () => { ArgumentNullException.ThrowIfNull(username); diff --git a/src/Tgstation.Server.Host/Server.cs b/src/Tgstation.Server.Host/Server.cs index e6ea8c04b1..751cd2d453 100644 --- a/src/Tgstation.Server.Host/Server.cs +++ b/src/Tgstation.Server.Host/Server.cs @@ -13,6 +13,7 @@ using Microsoft.Extensions.Options; using Tgstation.Server.Common.Extensions; using Tgstation.Server.Host.Configuration; using Tgstation.Server.Host.Core; +using Tgstation.Server.Host.Utils; namespace Tgstation.Server.Host { @@ -36,7 +37,7 @@ namespace Tgstation.Server.Host /// /// The of the running server. /// - internal IHost Host { get; private set; } + internal IHost? Host { get; private set; } /// /// The for the . @@ -51,7 +52,7 @@ namespace Tgstation.Server.Host /// /// The absolute path to install updates to. /// - readonly string updatePath; + readonly string? updatePath; /// /// for certain restart related operations. @@ -61,27 +62,27 @@ namespace Tgstation.Server.Host /// /// The for the . /// - ILogger logger; + ILogger? logger; /// /// The for the . /// - GeneralConfiguration generalConfiguration; + GeneralConfiguration? generalConfiguration; /// /// The for the . /// - CancellationTokenSource cancellationTokenSource; + CancellationTokenSource? cancellationTokenSource; /// /// The to propagate when the server terminates. /// - Exception propagatedException; + Exception? propagatedException; /// /// The that is used for asynchronously updating the server. /// - Task updateTask; + Task? updateTask; /// /// If the server is being shut down or restarted. @@ -98,7 +99,7 @@ namespace Tgstation.Server.Host /// /// The value of . /// The value of . - public Server(IHostBuilder hostBuilder, string updatePath) + public Server(IHostBuilder hostBuilder, string? updatePath) { this.hostBuilder = hostBuilder ?? throw new ArgumentNullException(nameof(hostBuilder)); this.updatePath = updatePath; @@ -107,15 +108,17 @@ namespace Tgstation.Server.Host restartHandlers = new List(); restartLock = new object(); + logger = null; } /// public async ValueTask Run(CancellationToken cancellationToken) { + var updateDirectory = updatePath != null ? Path.GetDirectoryName(updatePath) : null; using (cancellationTokenSource = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken)) - using (var fsWatcher = updatePath != null ? new FileSystemWatcher(Path.GetDirectoryName(updatePath)) : null) + using (var fsWatcher = updateDirectory != null ? new FileSystemWatcher(updateDirectory) : null) { - if (updatePath != null) + if (fsWatcher != null) { // If ever there is a NECESSARY update to the Host Watchdog, change this to use a pipe // I don't know why I'm only realizing this in 2023 when this is 2019 code @@ -128,9 +131,10 @@ namespace Tgstation.Server.Host try { using (Host = hostBuilder.Build()) + { + logger = Host.Services.GetRequiredService>(); try { - logger = Host.Services.GetRequiredService>(); using (cancellationToken.Register(() => logger.LogInformation("Server termination requested!"))) { var generalConfigurationOptions = Host.Services.GetRequiredService>(); @@ -154,6 +158,7 @@ namespace Tgstation.Server.Host { logger = null; } + } } finally { @@ -172,6 +177,10 @@ namespace Tgstation.Server.Host CheckSanity(true); + if (updatePath == null) + throw new InvalidOperationException("Tried to start update when server was initialized without an updatePath set!"); + + var logger = this.logger!; logger.LogTrace("Begin ApplyUpdate..."); CancellationToken criticalCancellationToken; @@ -220,17 +229,19 @@ namespace Tgstation.Server.Host CheckSanity(false); + var logger = this.logger!; lock (restartLock) if (!shutdownInProgress) { logger.LogTrace("Registering restart handler {handlerImplementationName}...", handler); restartHandlers.Add(handler); - return new RestartRegistration(() => - { - lock (restartLock) - if (!shutdownInProgress) - restartHandlers.Remove(handler); - }); + return new RestartRegistration( + new DisposeInvoker(() => + { + lock (restartLock) + if (!shutdownInProgress) + restartHandlers.Remove(handler); + })); } logger.LogWarning("Restart handler {handlerImplementationName} register after a shutdown had begun!", handler); @@ -244,7 +255,7 @@ namespace Tgstation.Server.Host public ValueTask GracefulShutdown(bool detach) => RestartImpl(null, null, false, detach); /// - public ValueTask Die(Exception exception) + public ValueTask Die(Exception? exception) { if (exception != null) return RestartImpl(null, exception, false, true); @@ -270,7 +281,7 @@ namespace Tgstation.Server.Host /// Re-throw if it exists. /// /// An existing that should be thrown as well, but not by itself. - void CheckExceptionPropagation(Exception otherException) + void CheckExceptionPropagation(Exception? otherException) { if (propagatedException == null) return; @@ -289,12 +300,13 @@ namespace Tgstation.Server.Host /// If the host watchdog is required for this "restart". /// If the restart should wait for extremely long running tasks to complete (Like the current DreamDaemon world). /// A representing the running operation. - async ValueTask RestartImpl(Version newVersion, Exception exception, bool requireWatchdog, bool completeAsap) + async ValueTask RestartImpl(Version? newVersion, Exception? exception, bool requireWatchdog, bool completeAsap) { CheckSanity(requireWatchdog); // if the watchdog isn't required and there's no issue, this is just a graceful shutdown bool isGracefulShutdown = !requireWatchdog && exception == null; + var logger = this.logger!; logger.LogTrace( "Begin {restartType}...", isGracefulShutdown @@ -322,8 +334,8 @@ namespace Tgstation.Server.Host using var cts = new CancellationTokenSource( TimeSpan.FromMinutes( giveHandlersTimeToWaitAround - ? generalConfiguration.ShutdownTimeoutMinutes - : generalConfiguration.RestartTimeoutMinutes)); + ? generalConfiguration!.ShutdownTimeoutMinutes + : generalConfiguration!.RestartTimeoutMinutes)); var cancellationToken = cts.Token; try { @@ -366,7 +378,7 @@ namespace Tgstation.Server.Host logger?.LogTrace("FileSystemWatcher triggered."); // TODO: Refactor this to not use System.IO function here. - if (eventArgs.FullPath == Path.GetFullPath(updatePath) && File.Exists(eventArgs.FullPath)) + if (eventArgs.FullPath == Path.GetFullPath(updatePath!) && File.Exists(eventArgs.FullPath)) { logger?.LogInformation("Host watchdog appears to be requesting server termination!"); lock (restartLock) @@ -390,8 +402,8 @@ namespace Tgstation.Server.Host void StopServerImmediate() { shutdownInProgress = true; - logger.LogDebug("Stopping host..."); - cancellationTokenSource.Cancel(); + logger!.LogDebug("Stopping host..."); + cancellationTokenSource!.Cancel(); } } } diff --git a/src/Tgstation.Server.Host/ServerFactory.cs b/src/Tgstation.Server.Host/ServerFactory.cs index a466a97fb5..f92298da10 100644 --- a/src/Tgstation.Server.Host/ServerFactory.cs +++ b/src/Tgstation.Server.Host/ServerFactory.cs @@ -51,7 +51,7 @@ namespace Tgstation.Server.Host /// // TODO: Decomplexify #pragma warning disable CA1506 - public async ValueTask CreateServer(string[] args, string updatePath, CancellationToken cancellationToken) + public async ValueTask CreateServer(string[] args, string? updatePath, CancellationToken cancellationToken) { ArgumentNullException.ThrowIfNull(args); @@ -96,7 +96,8 @@ namespace Tgstation.Server.Host #if !NET8_0 #error Validate this monstrosity works on current .NET #endif - IConfigurationSource cmdLineConfig, baseYmlConfig, environmentYmlConfig; + IConfigurationSource? cmdLineConfig; + IConfigurationSource baseYmlConfig, environmentYmlConfig; if (args.Length == 0) { cmdLineConfig = null; diff --git a/src/Tgstation.Server.Host/Setup/SetupWizard.cs b/src/Tgstation.Server.Host/Setup/SetupWizard.cs index 79f30c22b9..c8007f16b5 100644 --- a/src/Tgstation.Server.Host/Setup/SetupWizard.cs +++ b/src/Tgstation.Server.Host/Setup/SetupWizard.cs @@ -220,9 +220,12 @@ namespace Tgstation.Server.Host.Setup await console.WriteAsync($"Checking {databaseConfiguration.DatabaseType} version...", true, cancellationToken); using var command = testConnection.CreateCommand(); command.CommandText = "SELECT VERSION()"; - var fullVersion = (string)await command.ExecuteScalarAsync(cancellationToken); + var fullVersion = (string?)await command.ExecuteScalarAsync(cancellationToken); await console.WriteAsync(String.Format(CultureInfo.InvariantCulture, "Found {0}", fullVersion), true, cancellationToken); + if (fullVersion == null) + throw new InvalidOperationException($"\"{command.CommandText}\" returned null!"); + if (databaseConfiguration.DatabaseType == DatabaseType.PostgresSql) { var splits = fullVersion.Split(' '); @@ -291,7 +294,7 @@ namespace Tgstation.Server.Host.Setup /// The path to the potential SQLite database file. /// The for the operation. /// A resulting in the SQLite database path to store in the configuration. - async ValueTask ValidateNonExistantSqliteDBName(string databaseName, CancellationToken cancellationToken) + async ValueTask ValidateNonExistantSqliteDBName(string databaseName, CancellationToken cancellationToken) { var dbPathIsRooted = Path.IsPathRooted(databaseName); var resolvedPath = ioManager.ResolvePath( @@ -406,12 +409,12 @@ namespace Tgstation.Server.Host.Setup DatabaseType = await PromptDatabaseType(firstTime, cancellationToken), }; - string serverAddress = null; + string? serverAddress = null; ushort? serverPort = null; var definitelyLocalMariaDB = firstTime && internalConfiguration.MariaDBSetup; var isSqliteDB = databaseConfiguration.DatabaseType == DatabaseType.Sqlite; - IPHostEntry serverAddressEntry = null; + IPHostEntry? serverAddressEntry = null; if (!isSqliteDB) do { @@ -468,7 +471,7 @@ namespace Tgstation.Server.Host.Setup await console.WriteAsync(null, true, cancellationToken); await console.WriteAsync($"Enter the database {(isSqliteDB ? "file path" : "name")} ({(definitelyLocalMariaDB ? "leave blank for \"tgs\")" : "Can be from previous installation. Otherwise, should not exist")}): ", false, cancellationToken); - string databaseName; + string? databaseName; bool dbExists = false; do { @@ -510,8 +513,8 @@ namespace Tgstation.Server.Host.Setup await console.WriteAsync(null, true, cancellationToken); - string username = null; - string password = null; + string? username = null; + string? password = null; if (!isSqliteDB) if (!useWinAuth) { @@ -885,7 +888,7 @@ namespace Tgstation.Server.Host.Setup /// /// The for the operation. /// A resulting in the new . - async ValueTask ConfigureSwarm(CancellationToken cancellationToken) + async ValueTask ConfigureSwarm(CancellationToken cancellationToken) { var enable = await PromptYesNo("Enable swarm mode?", false, cancellationToken); if (!enable) @@ -902,7 +905,7 @@ namespace Tgstation.Server.Host.Setup async ValueTask ParseAddress(string question) { var first = true; - Uri address; + Uri? address; do { if (first) @@ -933,7 +936,7 @@ namespace Tgstation.Server.Host.Setup while (String.IsNullOrWhiteSpace(privateKey)); var controller = await PromptYesNo("Is this server the swarm's controller? (y/n): ", null, cancellationToken); - Uri controllerAddress = null; + Uri? controllerAddress = null; if (!controller) controllerAddress = await ParseAddress("Enter the swarm controller's HTTP(S) address: "); @@ -965,15 +968,15 @@ namespace Tgstation.Server.Host.Setup ushort? hostingPort, DatabaseConfiguration databaseConfiguration, GeneralConfiguration newGeneralConfiguration, - FileLoggingConfiguration fileLoggingConfiguration, - ElasticsearchConfiguration elasticsearchConfiguration, + FileLoggingConfiguration? fileLoggingConfiguration, + ElasticsearchConfiguration? elasticsearchConfiguration, ControlPanelConfiguration controlPanelConfiguration, - SwarmConfiguration swarmConfiguration, + SwarmConfiguration? swarmConfiguration, CancellationToken cancellationToken) { newGeneralConfiguration.ApiPort = hostingPort ?? GeneralConfiguration.DefaultApiPort; newGeneralConfiguration.ConfigVersion = GeneralConfiguration.CurrentConfigVersion; - var map = new Dictionary() + var map = new Dictionary() { { DatabaseConfiguration.Section, databaseConfiguration }, { GeneralConfiguration.Section, newGeneralConfiguration }, @@ -1097,14 +1100,14 @@ namespace Tgstation.Server.Host.Setup } Task finalTask = Task.CompletedTask; - string originalConsoleTitle = null; + string? originalConsoleTitle = null; void SetConsoleTitle() { if (originalConsoleTitle != null) return; originalConsoleTitle = console.Title; - console.Title = $"{assemblyInformationProvider.VersionString} Setup Wizard"; + console.SetTitle($"{assemblyInformationProvider.VersionString} Setup Wizard"); } // Link passed cancellationToken with cancel key press @@ -1194,7 +1197,7 @@ namespace Tgstation.Server.Host.Setup { await finalTask; if (originalConsoleTitle != null) - console.Title = originalConsoleTitle; + console.SetTitle(originalConsoleTitle); } } } diff --git a/src/Tgstation.Server.Host/Swarm/ISwarmService.cs b/src/Tgstation.Server.Host/Swarm/ISwarmService.cs index 9b86389540..8e827eb131 100644 --- a/src/Tgstation.Server.Host/Swarm/ISwarmService.cs +++ b/src/Tgstation.Server.Host/Swarm/ISwarmService.cs @@ -38,6 +38,6 @@ namespace Tgstation.Server.Host.Swarm /// Gets the list of s in the swarm, including the current one. /// /// A of s in the swarm. If the server is not part of a swarm, will be returned. - ICollection GetSwarmServers(); + ICollection? GetSwarmServers(); } } diff --git a/src/Tgstation.Server.Host/Swarm/SwarmRegistrationRequest.cs b/src/Tgstation.Server.Host/Swarm/SwarmRegistrationRequest.cs index cad1cebe27..229d4d28d2 100644 --- a/src/Tgstation.Server.Host/Swarm/SwarmRegistrationRequest.cs +++ b/src/Tgstation.Server.Host/Swarm/SwarmRegistrationRequest.cs @@ -14,6 +14,15 @@ namespace Tgstation.Server.Host.Swarm /// The TGS of the sending server. /// [Required] - public Version ServerVersion { get; set; } + public Version ServerVersion { get; } + + /// + /// Initializes a new instance of the class. + /// + /// The value of . + public SwarmRegistrationRequest(Version serverVersion) + { + ServerVersion = serverVersion ?? throw new ArgumentNullException(nameof(serverVersion)); + } } } diff --git a/src/Tgstation.Server.Host/Swarm/SwarmServersUpdateRequest.cs b/src/Tgstation.Server.Host/Swarm/SwarmServersUpdateRequest.cs index 61b60cc024..06fb6260bb 100644 --- a/src/Tgstation.Server.Host/Swarm/SwarmServersUpdateRequest.cs +++ b/src/Tgstation.Server.Host/Swarm/SwarmServersUpdateRequest.cs @@ -14,6 +14,6 @@ namespace Tgstation.Server.Host.Swarm /// The of updated s. /// [Required] - public ICollection SwarmServers { get; set; } + public ICollection? SwarmServers { get; set; } } } diff --git a/src/Tgstation.Server.Host/Swarm/SwarmService.cs b/src/Tgstation.Server.Host/Swarm/SwarmService.cs index dc4dfa2251..0ff0131c72 100644 --- a/src/Tgstation.Server.Host/Swarm/SwarmService.cs +++ b/src/Tgstation.Server.Host/Swarm/SwarmService.cs @@ -1,5 +1,6 @@ using System; using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; using System.Linq; using System.Net; using System.Net.Http; @@ -49,6 +50,7 @@ namespace Tgstation.Server.Host.Swarm /// /// If the swarm system is enabled. /// + [MemberNotNullWhen(true, nameof(serverHealthCheckTask), nameof(forceHealthCheckTcs), nameof(serverHealthCheckCancellationTokenSource), nameof(swarmServers))] bool SwarmMode => swarmConfiguration.PrivateKey != null; /// @@ -99,17 +101,17 @@ namespace Tgstation.Server.Host.Swarm /// /// The for . /// - readonly CancellationTokenSource serverHealthCheckCancellationTokenSource; + readonly CancellationTokenSource? serverHealthCheckCancellationTokenSource; /// /// of connected s. /// - readonly List swarmServers; + readonly List? swarmServers; /// /// of s to registration s and when they were created. /// - readonly Dictionary registrationIdsAndTimes; + readonly Dictionary? registrationIdsAndTimes; /// /// If the current server is the swarm controller. @@ -119,17 +121,17 @@ namespace Tgstation.Server.Host.Swarm /// /// A that is currently in progress. /// - volatile SwarmUpdateOperation updateOperation; + volatile SwarmUpdateOperation? updateOperation; /// /// A that is used to force a health check. /// - volatile TaskCompletionSource forceHealthCheckTcs; + volatile TaskCompletionSource? forceHealthCheckTcs; /// /// The for the . /// - Task serverHealthCheckTask; + Task? serverHealthCheckTask; /// /// The registration provided by the swarm controller. @@ -183,18 +185,17 @@ namespace Tgstation.Server.Host.Swarm { if (swarmConfiguration.Address == null) throw new InvalidOperationException("Swarm configuration missing Address!"); + if (String.IsNullOrWhiteSpace(swarmConfiguration.Identifier)) throw new InvalidOperationException("Swarm configuration missing Identifier!"); - } - swarmController = !SwarmMode || swarmConfiguration.ControllerAddress == null; - if (SwarmMode) - { - serverHealthCheckCancellationTokenSource = new CancellationTokenSource(); - forceHealthCheckTcs = new TaskCompletionSource(); + swarmController = swarmConfiguration.ControllerAddress == null; if (swarmController) registrationIdsAndTimes = new(); + serverHealthCheckCancellationTokenSource = new CancellationTokenSource(); + forceHealthCheckTcs = new TaskCompletionSource(); + swarmServers = new List { new SwarmServerResponse @@ -206,6 +207,8 @@ namespace Tgstation.Server.Host.Swarm }, }; } + else + swarmController = true; } /// @@ -346,7 +349,7 @@ namespace Tgstation.Server.Host.Swarm } /// - public ICollection GetSwarmServers() + public ICollection? GetSwarmServers() { if (!SwarmMode) return null; @@ -424,7 +427,7 @@ namespace Tgstation.Server.Host.Swarm { logger.LogTrace("Begin Shutdown"); - async ValueTask SendUnregistrationRequest(SwarmServerResponse swarmServer) + async ValueTask SendUnregistrationRequest(SwarmServerResponse? swarmServer) { using var httpClient = httpClientFactory.CreateClient(); using var request = PrepareSwarmRequest( @@ -443,13 +446,13 @@ namespace Tgstation.Server.Host.Swarm logger.LogWarning( ex, "Error unregistering {nodeType}!", - swarmController + swarmServer != null ? $"node {swarmServer.Identifier}" : "from controller"); } } - if (serverHealthCheckTask != null) + if (SwarmMode && serverHealthCheckTask != null) { serverHealthCheckCancellationTokenSource.Cancel(); await serverHealthCheckTask; @@ -489,7 +492,7 @@ namespace Tgstation.Server.Host.Swarm .Select(SendUnregistrationRequest) .ToList()); swarmServers.RemoveRange(1, swarmServers.Count - 1); - registrationIdsAndTimes.Clear(); + registrationIdsAndTimes!.Clear(); } await task; @@ -504,6 +507,9 @@ namespace Tgstation.Server.Host.Swarm { ArgumentNullException.ThrowIfNull(swarmServers); + if (!SwarmMode) + throw new InvalidOperationException("Swarm mode not enabled!"); + if (swarmController) throw new InvalidOperationException("Cannot UpdateSwarmServersList on swarm controller!"); @@ -518,9 +524,12 @@ namespace Tgstation.Server.Host.Swarm /// public bool ValidateRegistration(Guid registrationId) { + if (!SwarmMode) + throw new InvalidOperationException("Swarm mode not enabled!"); + if (swarmController) lock (swarmServers) - return registrationIdsAndTimes.Values.Any(x => x.RegistrationId == registrationId); + return registrationIdsAndTimes!.Values.Any(x => x.RegistrationId == registrationId); if (registrationId != controllerRegistration) return false; @@ -540,6 +549,9 @@ namespace Tgstation.Server.Host.Swarm if (node.Address == null) throw new ArgumentException("Node missing Address!", nameof(node)); + if (!SwarmMode) + throw new InvalidOperationException("Swarm mode not enabled!"); + if (!swarmController) throw new InvalidOperationException("Cannot RegisterNode on swarm node!"); @@ -547,6 +559,7 @@ namespace Tgstation.Server.Host.Swarm await AbortUpdate(); + var registrationIdsAndTimes = this.registrationIdsAndTimes!; lock (swarmServers) { if (registrationIdsAndTimes.Any(x => x.Value.RegistrationId == registrationId)) @@ -640,6 +653,9 @@ namespace Tgstation.Server.Host.Swarm /// public async ValueTask UnregisterNode(Guid registrationId, CancellationToken cancellationToken) { + if (!SwarmMode) + throw new InvalidOperationException("Swarm mode not enabled!"); + logger.LogTrace("UnregisterNode {registrationId}", registrationId); await AbortUpdate(); @@ -661,7 +677,7 @@ namespace Tgstation.Server.Host.Swarm lock (swarmServers) { swarmServers.RemoveAll(x => x.Identifier == nodeIdentifier); - registrationIdsAndTimes.Remove(nodeIdentifier); + registrationIdsAndTimes!.Remove(nodeIdentifier); } MarkServersDirty(); @@ -708,7 +724,7 @@ namespace Tgstation.Server.Host.Swarm Address = swarmConfiguration.ControllerAddress, }); - lock (swarmServers) + lock (swarmServers!) return ValueTaskExtensions.WhenAll( swarmServers .Where(x => !x.Controller) @@ -758,16 +774,16 @@ namespace Tgstation.Server.Host.Swarm /// The . Must always have populated. If is , it must be fully populated. /// The for the operation. /// A resulting in the . - async ValueTask PrepareUpdateImpl(ISeekableFileStreamProvider initiatorProvider, SwarmUpdateRequest updateRequest, CancellationToken cancellationToken) + async ValueTask PrepareUpdateImpl(ISeekableFileStreamProvider? initiatorProvider, SwarmUpdateRequest updateRequest, CancellationToken cancellationToken) { + var version = updateRequest.UpdateVersion!; if (!SwarmMode) { // we still need an active update operation for the TargetVersion - updateOperation = new SwarmUpdateOperation(updateRequest.UpdateVersion); + updateOperation = new SwarmUpdateOperation(version); return SwarmPrepareResult.SuccessProviderNotRequired; } - var version = updateRequest.UpdateVersion; var initiator = initiatorProvider != null; logger.LogTrace("PrepareUpdateImpl {version}...", version); @@ -775,7 +791,7 @@ namespace Tgstation.Server.Host.Swarm SwarmUpdateOperation localUpdateOperation; try { - SwarmServerResponse sourceNode = null; + SwarmServerResponse? sourceNode = null; List currentNodes; lock (swarmServers) { @@ -812,7 +828,7 @@ namespace Tgstation.Server.Host.Swarm if (!swarmController && initiator) { - var downloadTickets = await CreateDownloadTickets(initiatorProvider, currentNodes, cancellationToken); + var downloadTickets = await CreateDownloadTickets(initiatorProvider!, currentNodes, cancellationToken); // condition of initiator logger.LogInformation("Forwarding update request to swarm controller..."); using var httpClient = httpClientFactory.CreateClient(); @@ -852,7 +868,13 @@ namespace Tgstation.Server.Host.Swarm return SwarmPrepareResult.Failure; } - if (!updateRequest.DownloadTickets.TryGetValue(swarmConfiguration.Identifier, out var ticket)) + if (updateRequest.DownloadTickets == null) + { + logger.LogError("Missing download tickets in update request!"); + return SwarmPrepareResult.Failure; + } + + if (!updateRequest.DownloadTickets.TryGetValue(swarmConfiguration.Identifier!, out var ticket)) { logger.Log( swarmController @@ -922,7 +944,7 @@ namespace Tgstation.Server.Host.Swarm /// The for the operation. /// A resulting in the . async ValueTask ControllerDistributedPrepareUpdate( - ISeekableFileStreamProvider initiatorProvider, + ISeekableFileStreamProvider? initiatorProvider, SwarmUpdateRequest updateRequest, SwarmUpdateOperation currentUpdateOperation, CancellationToken cancellationToken) @@ -954,7 +976,7 @@ namespace Tgstation.Server.Host.Swarm } // The initiator node obviously doesn't create a ticket for itself - else if (!weAreInitiator && updateRequest.DownloadTickets.Count != currentUpdateOperation.InvolvedServers.Count - 1) + else if (!weAreInitiator && updateRequest.DownloadTickets!.Count != currentUpdateOperation.InvolvedServers.Count - 1) { logger.LogWarning( "Aborting update, {receivedTickets} download tickets were provided but there are {nodesToUpdate} nodes in the swarm that require the package!", @@ -965,8 +987,8 @@ namespace Tgstation.Server.Host.Swarm } var downloadTicketDictionary = weAreInitiator - ? await CreateDownloadTickets(initiatorProvider, currentUpdateOperation.InvolvedServers, cancellationToken) - : updateRequest.DownloadTickets; + ? await CreateDownloadTickets(initiatorProvider!, currentUpdateOperation.InvolvedServers, cancellationToken) + : updateRequest.DownloadTickets!; var sourceNode = weAreInitiator ? swarmConfiguration.Identifier @@ -982,18 +1004,20 @@ namespace Tgstation.Server.Host.Swarm .Select(node => { // only send the necessary ticket to each node from the controller - Dictionary localTicketDictionary; - if (!downloadTicketDictionary.TryGetValue(node.Identifier, out var ticket) - && node.Identifier != sourceNode) + Dictionary? localTicketDictionary; + var nodeId = node.Identifier!; + if (nodeId == sourceNode) + localTicketDictionary = null; + else if (!downloadTicketDictionary.TryGetValue(nodeId, out var ticket)) { - logger.LogError("Missing download ticket for node {missingNodeId}!", node.Identifier); + logger.LogError("Missing download ticket for node {missingNodeId}!", nodeId); anyFailed = true; return null; } else localTicketDictionary = new Dictionary { - { node.Identifier, ticket }, + { nodeId, ticket }, }; var request = new SwarmUpdateRequest @@ -1013,7 +1037,7 @@ namespace Tgstation.Server.Host.Swarm var tasks = updateRequests .Select(async tuple => { - var node = tuple.Item1; + var node = tuple!.Item1; var body = tuple.Item2; using var request = PrepareSwarmRequest( @@ -1086,7 +1110,7 @@ namespace Tgstation.Server.Host.Swarm var downloadTickets = new Dictionary(serversRequiringTickets.Count); foreach (var node in serversRequiringTickets) downloadTickets.Add( - node.Identifier, + node.Identifier!, transferService.CreateDownload(downloadProvider)); await streamRetrievalTask; @@ -1103,9 +1127,10 @@ namespace Tgstation.Server.Host.Swarm using var httpClient = httpClientFactory.CreateClient(); List currentSwarmServers; - lock (swarmServers) + lock (swarmServers!) currentSwarmServers = swarmServers.ToList(); + var registrationIdsAndTimes = this.registrationIdsAndTimes!; async ValueTask HealthRequestForServer(SwarmServerResponse swarmServer) { using var request = PrepareSwarmRequest( @@ -1131,14 +1156,14 @@ namespace Tgstation.Server.Host.Swarm lock (swarmServers) { swarmServers.Remove(swarmServer); - registrationIdsAndTimes.Remove(swarmServer.Identifier); + registrationIdsAndTimes.Remove(swarmServer.Identifier!); } } await ValueTaskExtensions.WhenAll( currentSwarmServers .Where(node => !node.Controller - && registrationIdsAndTimes.TryGetValue(node.Identifier, out var registrationAndTime) + && registrationIdsAndTimes.TryGetValue(node.Identifier!, out var registrationAndTime) && registrationAndTime.RegisteredAt.AddMinutes(SwarmConstants.ControllerHealthCheckIntervalMinutes) < DateTimeOffset.UtcNow) .Select(HealthRequestForServer)); @@ -1167,7 +1192,7 @@ namespace Tgstation.Server.Host.Swarm bool TriggerHealthCheck() { var currentTcs = Interlocked.Exchange(ref forceHealthCheckTcs, new TaskCompletionSource()); - return currentTcs.TrySetResult(); + return currentTcs!.TrySetResult(); } /// @@ -1242,9 +1267,8 @@ namespace Tgstation.Server.Host.Swarm null, HttpMethod.Post, SwarmConstants.RegisterRoute, - new SwarmRegistrationRequest + new SwarmRegistrationRequest(assemblyInformationProvider.Version) { - ServerVersion = assemblyInformationProvider.Version, Identifier = swarmConfiguration.Identifier, Address = swarmConfiguration.Address, PublicAddress = swarmConfiguration.PublicAddress, @@ -1297,7 +1321,7 @@ namespace Tgstation.Server.Host.Swarm async ValueTask SendUpdatedServerListToNodes(CancellationToken cancellationToken) { List currentSwarmServers; - lock (swarmServers) + lock (swarmServers!) { serversDirty = false; currentSwarmServers = swarmServers.ToList(); @@ -1335,7 +1359,7 @@ namespace Tgstation.Server.Host.Swarm lock (swarmServers) { swarmServers.Remove(swarmServer); - registrationIdsAndTimes.Remove(swarmServer.Identifier); + registrationIdsAndTimes!.Remove(swarmServer.Identifier!); } } } @@ -1350,17 +1374,17 @@ namespace Tgstation.Server.Host.Swarm /// /// Prepares a for swarm communication. /// - /// The the message is for, if null will be sent to swarm controller. + /// The the message is for. Must have and set. If , will be sent to swarm controller. /// The . /// The route on to use. /// The body if any. /// An optional override to the . /// A new . HttpRequestMessage PrepareSwarmRequest( - SwarmServerResponse swarmServer, + SwarmServerResponse? swarmServer, HttpMethod httpMethod, string route, - object body, + object? body, Guid? registrationIdOverride = null) { swarmServer ??= new SwarmServerResponse @@ -1373,7 +1397,7 @@ namespace Tgstation.Server.Host.Swarm "{method} {route} to swarm server {nodeIdOrAddress}", httpMethod, fullRoute, - swarmServer.Identifier ?? swarmServer.Address.ToString()); + swarmServer.Identifier ?? swarmServer.Address!.ToString()); var request = new HttpRequestMessage( httpMethod, @@ -1388,8 +1412,8 @@ namespace Tgstation.Server.Host.Swarm request.Headers.Add(SwarmConstants.RegistrationIdHeader, registrationIdOverride.Value.ToString()); else if (swarmController) { - lock (swarmServers) - if (registrationIdsAndTimes.TryGetValue(swarmServer.Identifier, out var registrationIdAndTime)) + lock (swarmServers!) + if (registrationIdsAndTimes!.TryGetValue(swarmServer.Identifier!, out var registrationIdAndTime)) request.Headers.Add(SwarmConstants.RegistrationIdHeader, registrationIdAndTime.RegistrationId.ToString()); } else if (controllerRegistration.HasValue) @@ -1420,7 +1444,7 @@ namespace Tgstation.Server.Host.Swarm logger.LogTrace("Starting HealthCheckLoop..."); try { - var nextForceHealthCheckTask = forceHealthCheckTcs.Task; + var nextForceHealthCheckTask = forceHealthCheckTcs!.Task; while (!cancellationToken.IsCancellationRequested) { TimeSpan delay; @@ -1498,13 +1522,14 @@ namespace Tgstation.Server.Host.Swarm /// /// The registration . /// The registered or if it does not exist. - string NodeIdentifierFromRegistration(Guid registrationId) + string? NodeIdentifierFromRegistration(Guid registrationId) { if (!swarmController) throw new InvalidOperationException("NodeIdentifierFromRegistration on node!"); - lock (swarmServers) + lock (swarmServers!) { + var registrationIdsAndTimes = this.registrationIdsAndTimes!; var exists = registrationIdsAndTimes.Any(x => x.Value.RegistrationId == registrationId); if (!exists) { diff --git a/src/Tgstation.Server.Host/Swarm/SwarmUpdateOperation.cs b/src/Tgstation.Server.Host/Swarm/SwarmUpdateOperation.cs index 01b9f28d18..67d65686ca 100644 --- a/src/Tgstation.Server.Host/Swarm/SwarmUpdateOperation.cs +++ b/src/Tgstation.Server.Host/Swarm/SwarmUpdateOperation.cs @@ -31,7 +31,7 @@ namespace Tgstation.Server.Host.Swarm /// /// Backing field for . /// - readonly IReadOnlyList initialInvolvedServers; + readonly IReadOnlyList? initialInvolvedServers; /// /// The backing for . @@ -41,7 +41,7 @@ namespace Tgstation.Server.Host.Swarm /// /// of that need to send a ready-commit to the controller before the commit can happen. /// - readonly HashSet nodesThatNeedToBeReadyToCommit; + readonly HashSet? nodesThatNeedToBeReadyToCommit; /// /// Initializes a new instance of the class. @@ -58,7 +58,7 @@ namespace Tgstation.Server.Host.Swarm /// Initializes a new instance of the class. /// /// The value of . - /// An of the controller's current nodes as s. + /// An of the controller's current nodes as s. Must have and set. /// This is the variant for use by the controller. public SwarmUpdateOperation(Version targetVersion, IEnumerable currentNodes) : this(targetVersion) @@ -66,7 +66,7 @@ namespace Tgstation.Server.Host.Swarm initialInvolvedServers = currentNodes?.ToList() ?? throw new ArgumentNullException(nameof(currentNodes)); nodesThatNeedToBeReadyToCommit = initialInvolvedServers .Where(node => !node.Controller) - .Select(node => node.Identifier) + .Select(node => node.Identifier!) .ToHashSet(); } diff --git a/src/Tgstation.Server.Host/Swarm/SwarmUpdateRequest.cs b/src/Tgstation.Server.Host/Swarm/SwarmUpdateRequest.cs index 1da49cea3c..958a8f5afb 100644 --- a/src/Tgstation.Server.Host/Swarm/SwarmUpdateRequest.cs +++ b/src/Tgstation.Server.Host/Swarm/SwarmUpdateRequest.cs @@ -15,17 +15,17 @@ namespace Tgstation.Server.Host.Swarm /// The TGS to update to. /// [Required] - public Version UpdateVersion { get; init; } + public Version? UpdateVersion { get; init; } /// /// The of the node to download the update package from. /// [Required] - public string SourceNode { get; init; } + public string? SourceNode { get; init; } /// /// The map of s to s for retrieving the update package from the initiating server. /// - public Dictionary DownloadTickets { get; init; } + public Dictionary? DownloadTickets { get; init; } } } diff --git a/src/Tgstation.Server.Host/System/AssemblyInformationProvider.cs b/src/Tgstation.Server.Host/System/AssemblyInformationProvider.cs index 88d1a3a627..475eb49862 100644 --- a/src/Tgstation.Server.Host/System/AssemblyInformationProvider.cs +++ b/src/Tgstation.Server.Host/System/AssemblyInformationProvider.cs @@ -38,7 +38,7 @@ namespace Tgstation.Server.Host.System Assembly assembly = Assembly.GetExecutingAssembly(); Path = assembly.Location; AssemblyName = assembly.GetName(); - Version = AssemblyName.Version.Semver(); + Version = AssemblyName.Version!.Semver(); VersionString = String.Concat(VersionPrefix, "-v", Version); } } diff --git a/src/Tgstation.Server.Host/System/IProcess.cs b/src/Tgstation.Server.Host/System/IProcess.cs index dc9a1d5eac..b0783cbd7e 100644 --- a/src/Tgstation.Server.Host/System/IProcess.cs +++ b/src/Tgstation.Server.Host/System/IProcess.cs @@ -7,7 +7,7 @@ namespace Tgstation.Server.Host.System /// /// Abstraction over a . /// - interface IProcess : IProcessBase, IAsyncDisposable + public interface IProcess : IProcessBase, IAsyncDisposable { /// /// The ' ID. @@ -28,7 +28,7 @@ namespace Tgstation.Server.Host.System /// To guarantee that all data is received from the when redirecting streams to a file /// the result of this function must be ed before is called. /// - Task GetCombinedOutput(CancellationToken cancellationToken); + Task GetCombinedOutput(CancellationToken cancellationToken); /// /// Asycnhronously terminates the process. diff --git a/src/Tgstation.Server.Host/System/IProcessBase.cs b/src/Tgstation.Server.Host/System/IProcessBase.cs index 8fc0bd4484..d7a20f43b7 100644 --- a/src/Tgstation.Server.Host/System/IProcessBase.cs +++ b/src/Tgstation.Server.Host/System/IProcessBase.cs @@ -6,7 +6,7 @@ namespace Tgstation.Server.Host.System /// /// Represents process lifetime. /// - interface IProcessBase + public interface IProcessBase { /// /// The resulting in the exit code of the process or if the process was detached. @@ -22,12 +22,12 @@ namespace Tgstation.Server.Host.System /// /// Suspends the process. /// - void Suspend(); + void SuspendProcess(); /// /// Resumes the process. /// - void Resume(); + void ResumeProcess(); /// /// Create a dump file of the process. diff --git a/src/Tgstation.Server.Host/System/IProcessExecutor.cs b/src/Tgstation.Server.Host/System/IProcessExecutor.cs index 81593dbe37..aae807eec2 100644 --- a/src/Tgstation.Server.Host/System/IProcessExecutor.cs +++ b/src/Tgstation.Server.Host/System/IProcessExecutor.cs @@ -18,8 +18,8 @@ IProcess LaunchProcess( string fileName, string workingDirectory, - string arguments = null, - string fileRedirect = null, + string arguments, + string? fileRedirect = null, bool readStandardHandles = false, bool noShellExecute = false); @@ -34,13 +34,13 @@ /// /// The . /// The represented by on success, on failure. - IProcess GetProcess(int id); + IProcess? GetProcess(int id); /// /// Get a with a given . /// /// The name of the process executable without the extension. /// The represented by on success, on failure. - IProcess GetProcessByName(string name); + IProcess? GetProcessByName(string name); } } diff --git a/src/Tgstation.Server.Host/System/NativeMethods.cs b/src/Tgstation.Server.Host/System/NativeMethods.cs index dc5046d133..ffba29cc9c 100644 --- a/src/Tgstation.Server.Host/System/NativeMethods.cs +++ b/src/Tgstation.Server.Host/System/NativeMethods.cs @@ -54,7 +54,7 @@ namespace Tgstation.Server.Host.System /// See https://docs.microsoft.com/en-us/windows/desktop/api/winuser/nf-winuser-findwindoww. /// [DllImport("user32.dll", CharSet = CharSet.Unicode)] - public static extern IntPtr FindWindow(string lpClassName, string lpWindowName); + public static extern IntPtr FindWindow(string? lpClassName, string lpWindowName); /// /// See https://docs.microsoft.com/en-us/windows/desktop/api/winuser/nf-winuser-sendmessage. @@ -83,7 +83,7 @@ namespace Tgstation.Server.Host.System /// See https://msdn.microsoft.com/en-us/library/windows/desktop/aa378184(v=vs.85).aspx. /// [DllImport("advapi32.dll", SetLastError = true, CharSet = CharSet.Unicode)] - public static extern bool LogonUser(string lpszUsername, string lpszDomain, string lpszPassword, int dwLogonType, int dwLogonProvider, out IntPtr phToken); + public static extern bool LogonUser(string lpszUsername, string? lpszDomain, string lpszPassword, int dwLogonType, int dwLogonProvider, out IntPtr phToken); /// /// See https://docs.microsoft.com/en-us/windows/desktop/api/winbase/nf-winbase-createsymboliclinkw. diff --git a/src/Tgstation.Server.Host/System/PosixProcessFeatures.cs b/src/Tgstation.Server.Host/System/PosixProcessFeatures.cs index b45c845fa4..8077577aaf 100644 --- a/src/Tgstation.Server.Host/System/PosixProcessFeatures.cs +++ b/src/Tgstation.Server.Host/System/PosixProcessFeatures.cs @@ -86,7 +86,7 @@ namespace Tgstation.Server.Host.System throw new JobException(ErrorCode.GameServerOffline, ex); } - string output; + string? output; int exitCode; await using (var gcoreProc = lazyLoadedProcessExecutor.Value.LaunchProcess( GCorePath, diff --git a/src/Tgstation.Server.Host/System/PosixSignalHandler.cs b/src/Tgstation.Server.Host/System/PosixSignalHandler.cs index 693324d679..eff4f6c7c2 100644 --- a/src/Tgstation.Server.Host/System/PosixSignalHandler.cs +++ b/src/Tgstation.Server.Host/System/PosixSignalHandler.cs @@ -45,7 +45,7 @@ namespace Tgstation.Server.Host.System /// /// The thread used to check the signal. See http://docs.go-mono.com/?link=T%3aMono.Unix.UnixSignal. /// - Task signalCheckerTask; + Task? signalCheckerTask; /// /// Initializes a new instance of the class. diff --git a/src/Tgstation.Server.Host/System/Process.cs b/src/Tgstation.Server.Host/System/Process.cs index 266e220708..4362083247 100644 --- a/src/Tgstation.Server.Host/System/Process.cs +++ b/src/Tgstation.Server.Host/System/Process.cs @@ -51,7 +51,7 @@ namespace Tgstation.Server.Host.System /// /// The resulting in the process' standard output/error text. /// - readonly Task readTask; + readonly Task? readTask; /// /// If the was disposed. @@ -70,8 +70,8 @@ namespace Tgstation.Server.Host.System public Process( IProcessFeatures processFeatures, global::System.Diagnostics.Process handle, - CancellationTokenSource readerCts, - Task readTask, + CancellationTokenSource? readerCts, + Task? readTask, ILogger logger, bool preExisting) { @@ -135,7 +135,7 @@ namespace Tgstation.Server.Host.System } /// - public Task GetCombinedOutput(CancellationToken cancellationToken) + public Task GetCombinedOutput(CancellationToken cancellationToken) { if (readTask == null) throw new InvalidOperationException("Output/Error stream reading was not enabled!"); @@ -183,7 +183,7 @@ namespace Tgstation.Server.Host.System } /// - public void Suspend() + public void SuspendProcess() { CheckDisposed(); try @@ -199,7 +199,7 @@ namespace Tgstation.Server.Host.System } /// - public void Resume() + public void ResumeProcess() { CheckDisposed(); try diff --git a/src/Tgstation.Server.Host/System/ProcessExecutor.cs b/src/Tgstation.Server.Host/System/ProcessExecutor.cs index 8df021f795..7820ca11b3 100644 --- a/src/Tgstation.Server.Host/System/ProcessExecutor.cs +++ b/src/Tgstation.Server.Host/System/ProcessExecutor.cs @@ -77,7 +77,7 @@ namespace Tgstation.Server.Host.System } /// - public IProcess GetProcess(int id) + public IProcess? GetProcess(int id) { logger.LogDebug("Attaching to process {pid}...", id); global::System.Diagnostics.Process handle; @@ -107,7 +107,7 @@ namespace Tgstation.Server.Host.System string fileName, string workingDirectory, string arguments, - string fileRedirect, + string? fileRedirect, bool readStandardHandles, bool noShellExecute) { @@ -137,11 +137,11 @@ namespace Tgstation.Server.Host.System handle.StartInfo.UseShellExecute = !noShellExecute; - Task readTask = null; - CancellationTokenSource disposeCts = null; + Task? readTask = null; + CancellationTokenSource? disposeCts = null; try { - TaskCompletionSource processStartTcs = null; + TaskCompletionSource? processStartTcs = null; if (readStandardHandles) { processStartTcs = new TaskCompletionSource(); @@ -195,11 +195,11 @@ namespace Tgstation.Server.Host.System } /// - public IProcess GetProcessByName(string name) + public IProcess? GetProcessByName(string name) { logger.LogTrace("GetProcessByName: {processName}...", name ?? throw new ArgumentNullException(nameof(name))); var procs = global::System.Diagnostics.Process.GetProcessesByName(name); - global::System.Diagnostics.Process handle = null; + global::System.Diagnostics.Process? handle = null; foreach (var proc in procs) if (handle == null) handle = proc; @@ -223,14 +223,14 @@ namespace Tgstation.Server.Host.System /// The optional path to redirect the streams to. /// The for the operation. /// A resulting in the program's output/error text if is , otherwise. - async Task ConsumeReaders(global::System.Diagnostics.Process handle, Task startupAndPid, string fileRedirect, CancellationToken cancellationToken) + async Task ConsumeReaders(global::System.Diagnostics.Process handle, Task startupAndPid, string? fileRedirect, CancellationToken cancellationToken) { handle.StartInfo.RedirectStandardOutput = true; handle.StartInfo.RedirectStandardError = true; bool writingToFile; - await using var fileStream = (writingToFile = fileRedirect != null) ? ioManager.CreateAsyncSequentialWriteStream(fileRedirect) : null; - await using var writer = fileStream != null ? new StreamWriter(fileStream) : null; + await using var fileStream = (writingToFile = fileRedirect != null) ? ioManager.CreateAsyncSequentialWriteStream(fileRedirect!) : null; + await using var fileWriter = fileStream != null ? new StreamWriter(fileStream) : null; var stringBuilder = fileStream == null ? new StringBuilder() : null; @@ -279,15 +279,15 @@ namespace Tgstation.Server.Host.System { var text = enumerator.Current; nextEnumeration = enumerator.MoveNextAsync(); - await writer.WriteLineAsync(text.AsMemory(), cancellationToken); + await fileWriter!.WriteLineAsync(text.AsMemory(), cancellationToken); if (!nextEnumeration.IsCompleted) - await writer.FlushAsync(cancellationToken); + await fileWriter.FlushAsync(cancellationToken); } } else await foreach (var text in enumerable) - stringBuilder.AppendLine(text); + stringBuilder!.AppendLine(text); } var pid = await startupAndPid; @@ -310,8 +310,8 @@ namespace Tgstation.Server.Host.System catch (OperationCanceledException ex) { logger.LogWarning(ex, "PID {pid} stream reading interrupted!", pid); - if (fileStream != null) - await writer.WriteLineAsync("-- Process detached, log truncated. This is likely due a to TGS restart --"); + if (writingToFile) + await fileWriter!.WriteLineAsync("-- Process detached, log truncated. This is likely due a to TGS restart --"); } } } diff --git a/src/Tgstation.Server.Host/System/ProgramShutdownTokenSource.cs b/src/Tgstation.Server.Host/System/ProgramShutdownTokenSource.cs index fb68be4ac9..0b9a77f7a6 100644 --- a/src/Tgstation.Server.Host/System/ProgramShutdownTokenSource.cs +++ b/src/Tgstation.Server.Host/System/ProgramShutdownTokenSource.cs @@ -16,7 +16,7 @@ namespace Tgstation.Server.Host.System /// /// The for the . /// - CancellationTokenSource cancellationTokenSource; + CancellationTokenSource? cancellationTokenSource; /// /// Gets the . diff --git a/src/Tgstation.Server.Host/System/SystemDManager.cs b/src/Tgstation.Server.Host/System/SystemDManager.cs index 28fb4e4d25..7b0542fcad 100644 --- a/src/Tgstation.Server.Host/System/SystemDManager.cs +++ b/src/Tgstation.Server.Host/System/SystemDManager.cs @@ -86,7 +86,7 @@ namespace Tgstation.Server.Host.System } /// - public ValueTask HandleRestart(Version updateVersion, bool handlerMayDelayShutdownWithExtremelyLongRunningTasks, CancellationToken cancellationToken) + public ValueTask HandleRestart(Version? updateVersion, bool handlerMayDelayShutdownWithExtremelyLongRunningTasks, CancellationToken cancellationToken) { // If this is set, we know a gracefule SHUTDOWN was requested restartInProgress = !handlerMayDelayShutdownWithExtremelyLongRunningTasks; diff --git a/src/Tgstation.Server.Host/System/WindowsFirewallHelper.cs b/src/Tgstation.Server.Host/System/WindowsFirewallHelper.cs index 74964a5569..fff1521092 100644 --- a/src/Tgstation.Server.Host/System/WindowsFirewallHelper.cs +++ b/src/Tgstation.Server.Host/System/WindowsFirewallHelper.cs @@ -18,6 +18,7 @@ namespace Tgstation.Server.Host.System /// The to write to. /// The name of the rule in Windows Firewall. /// The path to the .exe to add a firewall exception for. + /// If the "netsh.exe" process should be run with lower process priority. /// The for the operation. /// A resulting in the exit code of the call to netsh.exe. public static async ValueTask AddFirewallException( @@ -25,6 +26,7 @@ namespace Tgstation.Server.Host.System ILogger logger, string exceptionName, string exePath, + bool lowPriority, CancellationToken cancellationToken) { logger.LogInformation("Adding Windows Firewall exception for {path}...", exePath); @@ -36,6 +38,9 @@ namespace Tgstation.Server.Host.System readStandardHandles: true, noShellExecute: true); + if (lowPriority) + netshProcess.AdjustPriority(false); + int exitCode; using (cancellationToken.Register(() => netshProcess.Terminate())) exitCode = (await netshProcess.Lifetime).Value; diff --git a/src/Tgstation.Server.Host/Tgstation.Server.Host.csproj b/src/Tgstation.Server.Host/Tgstation.Server.Host.csproj index a47c87fbd2..733400da3d 100644 --- a/src/Tgstation.Server.Host/Tgstation.Server.Host.csproj +++ b/src/Tgstation.Server.Host/Tgstation.Server.Host.csproj @@ -9,6 +9,7 @@ API1000;ASP0019 ClientApp/node_modules ClientApp/node_modules/.install-stamp + enable Linux ..\.. ../../build/uac_elevation_manifest.xml @@ -63,7 +64,7 @@ - + @@ -125,7 +126,7 @@ - + diff --git a/src/Tgstation.Server.Host/Transfer/FileDownloadProvider.cs b/src/Tgstation.Server.Host/Transfer/FileDownloadProvider.cs index 603e099c07..1b0428665a 100644 --- a/src/Tgstation.Server.Host/Transfer/FileDownloadProvider.cs +++ b/src/Tgstation.Server.Host/Transfer/FileDownloadProvider.cs @@ -20,7 +20,7 @@ namespace Tgstation.Server.Host.Transfer /// /// A to specially provide a returning the of the file download. The caller will own the resulting . /// - public Func> StreamProvider { get; } + public Func>? StreamProvider { get; } /// /// The full path to the file on disk to download. @@ -41,7 +41,7 @@ namespace Tgstation.Server.Host.Transfer /// The value of . public FileDownloadProvider( Func activationCallback, - Func> streamProvider, + Func>? streamProvider, string filePath, bool shareWrite) { diff --git a/src/Tgstation.Server.Host/Transfer/FileTransferService.cs b/src/Tgstation.Server.Host/Transfer/FileTransferService.cs index fc0201b48d..bb1cafa171 100644 --- a/src/Tgstation.Server.Host/Transfer/FileTransferService.cs +++ b/src/Tgstation.Server.Host/Transfer/FileTransferService.cs @@ -69,6 +69,11 @@ namespace Tgstation.Server.Host.Transfer /// Task expireTask; + /// + /// If the is disposed. + /// + bool disposed; + /// /// Initializes a new instance of the class. /// @@ -101,12 +106,13 @@ namespace Tgstation.Server.Host.Transfer { Task toAwait; lock (synchronizationLock) - if (expireTask != null) + if (!disposed) { disposeCts.Cancel(); disposeCts.Dispose(); + disposed = true; toAwait = expireTask; - expireTask = null; + expireTask = Task.CompletedTask; } else toAwait = Task.CompletedTask; @@ -118,72 +124,86 @@ namespace Tgstation.Server.Host.Transfer public FileTicketResponse CreateDownload(FileDownloadProvider downloadProvider) { ArgumentNullException.ThrowIfNull(downloadProvider); + ObjectDisposedException.ThrowIf(disposed, this); logger.LogDebug("Creating download ticket for path {filePath}", downloadProvider.FilePath); - var ticketResult = CreateTicket(); + var ticket = cryptographySuite.GetSecureString(); lock (downloadTickets) - downloadTickets.Add(ticketResult.FileTicket, downloadProvider); + downloadTickets.Add(ticket, downloadProvider); QueueExpiry(() => { lock (downloadTickets) - if (downloadTickets.Remove(ticketResult.FileTicket)) - logger.LogTrace("Expired download ticket {ticket}...", ticketResult.FileTicket); + if (downloadTickets.Remove(ticket)) + logger.LogTrace("Expired download ticket {ticket}...", ticket); }); - logger.LogTrace("Created download ticket {ticket}", ticketResult.FileTicket); + logger.LogTrace("Created download ticket {ticket}", ticket); - return ticketResult; + return new FileTicketResponse + { + FileTicket = ticket, + }; } /// public IFileUploadTicket CreateUpload(FileUploadStreamKind streamKind) { + ObjectDisposedException.ThrowIf(disposed, this); + logger.LogDebug("Creating upload ticket..."); - var uploadTicket = new FileUploadProvider(CreateTicket(), streamKind); + var ticket = cryptographySuite.GetSecureString(); + var uploadTicket = new FileUploadProvider( + new FileTicketResponse + { + FileTicket = ticket, + }, + streamKind); lock (uploadTickets) - uploadTickets.Add(uploadTicket.Ticket.FileTicket, uploadTicket); + uploadTickets.Add(ticket, uploadTicket); QueueExpiry(() => { lock (uploadTickets) - if (uploadTickets.Remove(uploadTicket.Ticket.FileTicket)) - logger.LogTrace("Expired upload ticket {ticket}...", uploadTicket.Ticket.FileTicket); + if (uploadTickets.Remove(ticket)) + logger.LogTrace("Expired upload ticket {ticket}...", ticket); else return; uploadTicket.Expire(); }); - logger.LogTrace("Created upload ticket {ticket}", uploadTicket.Ticket.FileTicket); + logger.LogTrace("Created upload ticket {ticket}", ticket); return uploadTicket; } /// - public async ValueTask> RetrieveDownloadStream(FileTicketResponse ticket, CancellationToken cancellationToken) + public async ValueTask> RetrieveDownloadStream(FileTicketResponse ticketResponse, CancellationToken cancellationToken) { - ArgumentNullException.ThrowIfNull(ticket); + ArgumentNullException.ThrowIfNull(ticketResponse); + ObjectDisposedException.ThrowIf(disposed, this); - FileDownloadProvider downloadProvider; + var ticket = ticketResponse.FileTicket ?? throw new InvalidOperationException("ticketResponse must have FileTicket!"); + FileDownloadProvider? downloadProvider; lock (downloadTickets) { - if (!downloadTickets.TryGetValue(ticket.FileTicket, out downloadProvider)) + if (!downloadTickets.TryGetValue(ticket, out downloadProvider)) { - logger.LogTrace("Download ticket {ticket} not found!", ticket.FileTicket); - return Tuple.Create(null, null); + logger.LogTrace("Download ticket {ticket} not found!", ticket); + return Tuple.Create(null, null); } - downloadTickets.Remove(ticket.FileTicket); + downloadTickets.Remove(ticket); } var errorCode = downloadProvider.ActivationCallback(); if (errorCode.HasValue) { - logger.LogDebug("Download ticket {ticket} failed activation!", ticket.FileTicket); - return Tuple.Create(null, new ErrorMessageResponse(errorCode.Value)); + logger.LogDebug("Download ticket {ticket} failed activation!", ticket); + return Tuple.Create(null, new ErrorMessageResponse(errorCode.Value)); } Stream stream; @@ -196,7 +216,7 @@ namespace Tgstation.Server.Host.Transfer } catch (IOException ex) { - return Tuple.Create( + return Tuple.Create( null, new ErrorMessageResponse(ErrorCode.IOError) { @@ -206,8 +226,8 @@ namespace Tgstation.Server.Host.Transfer try { - logger.LogTrace("Ticket {ticket} downloading...", ticket.FileTicket); - return Tuple.Create(stream, null); + logger.LogTrace("Ticket {ticket} downloading...", ticket); + return Tuple.Create(stream, null); } catch { @@ -217,34 +237,27 @@ namespace Tgstation.Server.Host.Transfer } /// - public async ValueTask SetUploadStream(FileTicketResponse ticket, Stream stream, CancellationToken cancellationToken) + public async ValueTask SetUploadStream(FileTicketResponse ticketResponse, Stream stream, CancellationToken cancellationToken) { - ArgumentNullException.ThrowIfNull(ticket); + ArgumentNullException.ThrowIfNull(ticketResponse); + ObjectDisposedException.ThrowIf(disposed, this); - FileUploadProvider uploadProvider; + var ticket = ticketResponse.FileTicket ?? throw new InvalidOperationException("ticketResponse must have FileTicket!"); + FileUploadProvider? uploadProvider; lock (uploadTickets) { - if (!uploadTickets.TryGetValue(ticket.FileTicket, out uploadProvider)) + if (!uploadTickets.TryGetValue(ticket, out uploadProvider)) { - logger.LogTrace("Upload ticket {ticket} not found!", ticket.FileTicket); + logger.LogTrace("Upload ticket {ticket} not found!", ticket); return new ErrorMessageResponse(ErrorCode.ResourceNotPresent); } - uploadTickets.Remove(ticket.FileTicket); + uploadTickets.Remove(ticket); } return await uploadProvider.Completion(stream, cancellationToken); } - /// - /// Creates a new . - /// - /// A new . - FileTicketResponse CreateTicket() => new() - { - FileTicket = cryptographySuite.GetSecureString(), - }; - /// /// Queue an to run after . /// diff --git a/src/Tgstation.Server.Host/Transfer/FileUploadProvider.cs b/src/Tgstation.Server.Host/Transfer/FileUploadProvider.cs index 9fe8250da5..0dd709dcc8 100644 --- a/src/Tgstation.Server.Host/Transfer/FileUploadProvider.cs +++ b/src/Tgstation.Server.Host/Transfer/FileUploadProvider.cs @@ -25,7 +25,7 @@ namespace Tgstation.Server.Host.Transfer /// /// The for the . /// - readonly TaskCompletionSource streamTcs; + readonly TaskCompletionSource streamTcs; /// /// The that completes in or when is called. @@ -40,7 +40,7 @@ namespace Tgstation.Server.Host.Transfer /// /// The that occurred while processing the upload if any. /// - ErrorMessageResponse errorMessage; + ErrorMessageResponse? errorMessage; /// /// Initializes a new instance of the class. @@ -52,7 +52,7 @@ namespace Tgstation.Server.Host.Transfer Ticket = ticket ?? throw new ArgumentNullException(nameof(ticket)); ticketExpiryCts = new CancellationTokenSource(); - streamTcs = new TaskCompletionSource(); + streamTcs = new TaskCompletionSource(); completionTcs = new TaskCompletionSource(); this.streamKind = streamKind; } @@ -65,14 +65,6 @@ namespace Tgstation.Server.Host.Transfer return ValueTask.CompletedTask; } - /// - public async ValueTask GetResult(CancellationToken cancellationToken) - { - using (cancellationToken.Register(() => streamTcs.TrySetCanceled(cancellationToken))) - using (ticketExpiryCts.Token.Register(() => streamTcs.TrySetResult(null))) - return await streamTcs.Task; - } - /// /// Expire the . /// @@ -88,14 +80,14 @@ namespace Tgstation.Server.Host.Transfer /// The containing uploaded data. /// The for the operation. /// A resulting in , otherwise. - public async ValueTask Completion(Stream stream, CancellationToken cancellationToken) + public async ValueTask Completion(Stream stream, CancellationToken cancellationToken) { ArgumentNullException.ThrowIfNull(stream); if (ticketExpiryCts.IsCancellationRequested) return new ErrorMessageResponse(ErrorCode.ResourceNotPresent); - Stream bufferedStream = null; + Stream? bufferedStream = null; try { switch (streamKind) @@ -131,7 +123,7 @@ namespace Tgstation.Server.Host.Transfer } /// - public void SetError(ErrorCode errorCode, string additionalData) + public void SetError(ErrorCode errorCode, string? additionalData) { if (errorMessage != null) throw new InvalidOperationException("Error already set!"); @@ -142,5 +134,18 @@ namespace Tgstation.Server.Host.Transfer }; completionTcs.TrySetResult(); } + + /// + public async ValueTask GetResult(CancellationToken cancellationToken) + => await ((IFileUploadTicket)this).GetResult(cancellationToken) + ?? throw new InvalidOperationException("Upload ticket expired!"); + + /// + async ValueTask IFileUploadTicket.GetResult(CancellationToken cancellationToken) + { + using (cancellationToken.Register(() => streamTcs.TrySetCanceled(cancellationToken))) + using (ticketExpiryCts.Token.Register(() => streamTcs.TrySetResult(null))) + return await streamTcs.Task; + } } } diff --git a/src/Tgstation.Server.Host/Transfer/IFileTransferStreamHandler.cs b/src/Tgstation.Server.Host/Transfer/IFileTransferStreamHandler.cs index 75603c8684..45aafc7cd7 100644 --- a/src/Tgstation.Server.Host/Transfer/IFileTransferStreamHandler.cs +++ b/src/Tgstation.Server.Host/Transfer/IFileTransferStreamHandler.cs @@ -13,20 +13,20 @@ namespace Tgstation.Server.Host.Transfer public interface IFileTransferStreamHandler { /// - /// Sets the for a given associated with a pending upload. + /// Sets the for a given associated with a pending upload. /// - /// The . + /// The . /// The with uploaded data. /// The for the operation. /// A resulting in if the upload completed successfully, otherwise. - ValueTask SetUploadStream(FileTicketResponse ticket, Stream stream, CancellationToken cancellationToken); + ValueTask SetUploadStream(FileTicketResponse ticketResponse, Stream stream, CancellationToken cancellationToken); /// - /// Gets the the for a given associated with a pending download. + /// Gets the the for a given associated with a pending download. /// - /// The . + /// The . /// The for the operation. /// A resulting in a containing either a containing the data to download or an to return. - ValueTask> RetrieveDownloadStream(FileTicketResponse ticket, CancellationToken cancellationToken); + ValueTask> RetrieveDownloadStream(FileTicketResponse ticketResponse, CancellationToken cancellationToken); } } diff --git a/src/Tgstation.Server.Host/Transfer/IFileUploadTicket.cs b/src/Tgstation.Server.Host/Transfer/IFileUploadTicket.cs index c1bd6e5725..7e1eabf813 100644 --- a/src/Tgstation.Server.Host/Transfer/IFileUploadTicket.cs +++ b/src/Tgstation.Server.Host/Transfer/IFileUploadTicket.cs @@ -1,4 +1,8 @@ -using Tgstation.Server.Api.Models; +using System.IO; +using System.Threading; +using System.Threading.Tasks; + +using Tgstation.Server.Api.Models; using Tgstation.Server.Api.Models.Response; using Tgstation.Server.Host.IO; @@ -19,6 +23,14 @@ namespace Tgstation.Server.Host.Transfer /// /// The to set. /// Any additional information that can be provided about the error. - void SetError(ErrorCode errorCode, string additionalData); + void SetError(ErrorCode errorCode, string? additionalData); + + /// + /// Gets the provided . May be called multiple times, though cancelling any may cause all calls to be cancelled. All calls yield the same reference. + /// + /// The for the operation. + /// A resulting in the provided on success, if the upload expired. + /// The resulting is owned by the and is short lived unless otherwise specified. It should be buffered if it needs use outside the lifetime of the . + new ValueTask GetResult(CancellationToken cancellationToken); } } diff --git a/src/Tgstation.Server.Host/Utils/ApiHeadersProvider.cs b/src/Tgstation.Server.Host/Utils/ApiHeadersProvider.cs index 56aa288619..1805d337c1 100644 --- a/src/Tgstation.Server.Host/Utils/ApiHeadersProvider.cs +++ b/src/Tgstation.Server.Host/Utils/ApiHeadersProvider.cs @@ -10,12 +10,12 @@ namespace Tgstation.Server.Host.Utils sealed class ApiHeadersProvider : IApiHeadersProvider { /// - public ApiHeaders ApiHeaders => attemptedApiHeadersCreation + public ApiHeaders? ApiHeaders => attemptedApiHeadersCreation ? apiHeaders : CreateApiHeaders(false); /// - public HeadersException HeadersException { get; private set; } + public HeadersException? HeadersException { get; private set; } /// /// The for the . @@ -25,7 +25,7 @@ namespace Tgstation.Server.Host.Utils /// /// Backing field for . /// - ApiHeaders apiHeaders; + ApiHeaders? apiHeaders; /// /// If populating was previously attempted. @@ -42,14 +42,14 @@ namespace Tgstation.Server.Host.Utils } /// - public ApiHeaders CreateAuthlessHeaders() => CreateApiHeaders(true); + public ApiHeaders CreateAuthlessHeaders() => CreateApiHeaders(true)!; /// /// Attempt to parse from the , optionally populating the properties. /// /// If the error should be ignored and / should not be populated. /// A newly parsed or if was set and the parse failed. - ApiHeaders CreateApiHeaders(bool authless) + ApiHeaders? CreateApiHeaders(bool authless) { if (httpContextAccessor.HttpContext == null) throw new InvalidOperationException("httpContextAccessor has no HttpContext!"); diff --git a/src/Tgstation.Server.Host/Utils/DisposeInvoker.cs b/src/Tgstation.Server.Host/Utils/DisposeInvoker.cs new file mode 100644 index 0000000000..b90d01318b --- /dev/null +++ b/src/Tgstation.Server.Host/Utils/DisposeInvoker.cs @@ -0,0 +1,49 @@ +using System; +using System.Threading; + +namespace Tgstation.Server.Host.Utils +{ + /// + /// Runs a given on . + /// + class DisposeInvoker : IDisposable + { + /// + /// If was called. + /// + public bool IsDisposed => disposeRan != 0; + + /// + /// The to run on . + /// + readonly Action disposeAction; + + /// + /// An representation of a indicating if has ran. + /// + volatile int disposeRan; + + /// + /// Initializes a new instance of the class. + /// + /// The value of . + public DisposeInvoker(Action disposeAction) + { + this.disposeAction = disposeAction ?? throw new ArgumentNullException(nameof(disposeAction)); + } + + /// + public void Dispose() + { + if (Interlocked.Exchange(ref disposeRan, 1) != 0) + return; + + DisposeImpl(); + } + + /// + /// Implementation of run after reentrancy check. + /// + protected virtual void DisposeImpl() => disposeAction(); + } +} diff --git a/src/Tgstation.Server.Host/Utils/FifoSemaphore.cs b/src/Tgstation.Server.Host/Utils/FifoSemaphore.cs index 7a4e200c9c..0bb696680a 100644 --- a/src/Tgstation.Server.Host/Utils/FifoSemaphore.cs +++ b/src/Tgstation.Server.Host/Utils/FifoSemaphore.cs @@ -51,7 +51,7 @@ namespace Tgstation.Server.Host.Utils /// A resulting in the locked . public async ValueTask Lock(CancellationToken cancellationToken) { - FifoSemaphoreTicket ticket = null; + FifoSemaphoreTicket? ticket = null; using (cancellationToken.Register( () => { @@ -63,7 +63,7 @@ namespace Tgstation.Server.Host.Utils var context = await SemaphoreSlimContext.Lock(semaphore, cancellationToken); try { - FifoSemaphoreTicket peekedTicket = null; + FifoSemaphoreTicket? peekedTicket = null; while (ticketQueue.Count > 0) { peekedTicket = ticketQueue.Peek(); diff --git a/src/Tgstation.Server.Host/Utils/GitHub/GitHubClientFactory.cs b/src/Tgstation.Server.Host/Utils/GitHub/GitHubClientFactory.cs index 38fb72ffd6..e479388b79 100644 --- a/src/Tgstation.Server.Host/Utils/GitHub/GitHubClientFactory.cs +++ b/src/Tgstation.Server.Host/Utils/GitHub/GitHubClientFactory.cs @@ -76,7 +76,7 @@ namespace Tgstation.Server.Host.Utils.GitHub /// /// Optional access token to use as credentials. /// The for the given . - GitHubClient GetOrCreateClient(string accessToken) + GitHubClient GetOrCreateClient(string? accessToken) { GitHubClient client; bool cacheHit; @@ -97,10 +97,11 @@ namespace Tgstation.Server.Host.Utils.GitHub var now = DateTimeOffset.UtcNow; if (!cacheHit) { + var product = assemblyInformationProvider.ProductInfoHeaderValue.Product!; client = new GitHubClient( new ProductHeaderValue( - assemblyInformationProvider.ProductInfoHeaderValue.Product.Name, - assemblyInformationProvider.ProductInfoHeaderValue.Product.Version)); + product.Name, + product.Version)); if (accessToken != null) client.Credentials = new Credentials(accessToken); diff --git a/src/Tgstation.Server.Host/Utils/GitHub/GitHubService.cs b/src/Tgstation.Server.Host/Utils/GitHub/GitHubService.cs index 7429112712..a56c956ebf 100644 --- a/src/Tgstation.Server.Host/Utils/GitHub/GitHubService.cs +++ b/src/Tgstation.Server.Host/Utils/GitHub/GitHubService.cs @@ -80,36 +80,38 @@ namespace Tgstation.Server.Host.Utils.GitHub .GetAll(updatesConfiguration.GitHubRepositoryId) .WaitAsync(cancellationToken); + var gitPrefix = updatesConfiguration.GitTagPrefix ?? String.Empty; + logger.LogTrace("{totalReleases} total releases", allReleases.Count); - var releases = allReleases - .Where(release => + var releases = allReleases! + .Where(release => + { + if (!release.PublishedAt.HasValue) { - if (!release.PublishedAt.HasValue) - { - logger.LogDebug("Release tag without PublishedAt: {releaseTag}", release.TagName); - return false; - } + logger.LogDebug("Release tag without PublishedAt: {releaseTag}", release.TagName); + return false; + } - if (!release.TagName.StartsWith(updatesConfiguration.GitTagPrefix, StringComparison.InvariantCulture)) - return false; + if (!release.TagName.StartsWith(gitPrefix, StringComparison.InvariantCulture)) + return false; - return true; - }) - .GroupBy(release => + return true; + }) + .GroupBy(release => + { + if (!Version.TryParse(release.TagName.Replace(gitPrefix, String.Empty, StringComparison.Ordinal), out var version)) { - if (!Version.TryParse(release.TagName.Replace(updatesConfiguration.GitTagPrefix, String.Empty, StringComparison.Ordinal), out var version)) - { - logger.LogDebug("Unparsable release tag: {releaseTag}", release.TagName); - return null; - } + logger.LogDebug("Unparsable release tag: {releaseTag}", release.TagName); + return null; + } - return version; - }) - .Where(grouping => grouping.Key != null) + return version; + }) + .Where(grouping => grouping.Key != null) - // GitHub can return the same result twice or some other nonsense - .Select(grouping => Tuple.Create(grouping.Key, grouping.OrderBy(x => x.PublishedAt.Value).First())) - .ToDictionary(tuple => tuple.Item1, tuple => tuple.Item2); + // GitHub can return the same result twice or some other nonsense + .Select(grouping => Tuple.Create(grouping.Key!, grouping.OrderBy(x => x.PublishedAt ?? DateTimeOffset.MinValue).First())) + .ToDictionary(tuple => tuple.Item1, tuple => tuple.Item2); logger.LogTrace("{parsedReleases} parsed releases", releases.Count); return releases; diff --git a/src/Tgstation.Server.Host/Utils/IApiHeadersProvider.cs b/src/Tgstation.Server.Host/Utils/IApiHeadersProvider.cs index 7de65834d3..bf3aa714bf 100644 --- a/src/Tgstation.Server.Host/Utils/IApiHeadersProvider.cs +++ b/src/Tgstation.Server.Host/Utils/IApiHeadersProvider.cs @@ -10,12 +10,12 @@ namespace Tgstation.Server.Host.Utils /// /// The created , if any. /// - ApiHeaders ApiHeaders { get; } + ApiHeaders? ApiHeaders { get; } /// /// The thrown when attempting to parse the if any. /// - HeadersException HeadersException { get; } + HeadersException? HeadersException { get; } /// /// Attempt to create without checking for the presence of an header. diff --git a/src/Tgstation.Server.Host/Utils/OpenApiEnumVarNamesExtension.cs b/src/Tgstation.Server.Host/Utils/OpenApiEnumVarNamesExtension.cs index a81beb8339..6953cc6480 100644 --- a/src/Tgstation.Server.Host/Utils/OpenApiEnumVarNamesExtension.cs +++ b/src/Tgstation.Server.Host/Utils/OpenApiEnumVarNamesExtension.cs @@ -49,8 +49,8 @@ namespace Tgstation.Server.Host.Utils writer.WriteStartArray(); foreach (var enumValue in Enum.GetValues(enumType)) { - var enumName = enumValue.ToString(); - var field = enumType.GetField(enumName); + var enumName = enumValue.ToString()!; + var field = enumType.GetField(enumName)!; if (field.IsDefined(typeof(ObsoleteAttribute), false)) enumName = $"DEPRECATED_{enumName}"; diff --git a/src/Tgstation.Server.Host/Utils/PortAllocator.cs b/src/Tgstation.Server.Host/Utils/PortAllocator.cs index 310cfbfe3d..1aa096ab0b 100644 --- a/src/Tgstation.Server.Host/Utils/PortAllocator.cs +++ b/src/Tgstation.Server.Host/Utils/PortAllocator.cs @@ -74,14 +74,14 @@ namespace Tgstation.Server.Host.Utils var ddPorts = await databaseContext .DreamDaemonSettings .AsQueryable() - .Where(x => x.Instance.SwarmIdentifer == swarmConfiguration.Identifier) + .Where(x => x.Instance!.SwarmIdentifer == swarmConfiguration.Identifier) .Select(x => x.Port) .ToListAsync(cancellationToken); var dmPorts = await databaseContext .DreamMakerSettings .AsQueryable() - .Where(x => x.Instance.SwarmIdentifer == swarmConfiguration.Identifier) + .Where(x => x.Instance!.SwarmIdentifer == swarmConfiguration.Identifier) .Select(x => x.ApiValidationPort) .ToListAsync(cancellationToken); diff --git a/src/Tgstation.Server.Host/Utils/ReferenceCounter.cs b/src/Tgstation.Server.Host/Utils/ReferenceCounter.cs index 4351c65db6..6377c0295f 100644 --- a/src/Tgstation.Server.Host/Utils/ReferenceCounter.cs +++ b/src/Tgstation.Server.Host/Utils/ReferenceCounter.cs @@ -22,12 +22,12 @@ namespace Tgstation.Server.Host.Utils /// /// Backing field for . /// - TInstance actualInstance; + TInstance? actualInstance; /// /// The to take when is called. /// - Action referenceCleanupAction; + Action? referenceCleanupAction; /// /// If the was initialized. diff --git a/src/Tgstation.Server.Host/Utils/ReferenceCountingContainer.cs b/src/Tgstation.Server.Host/Utils/ReferenceCountingContainer.cs index 121411bd1d..798fc2d16b 100644 --- a/src/Tgstation.Server.Host/Utils/ReferenceCountingContainer.cs +++ b/src/Tgstation.Server.Host/Utils/ReferenceCountingContainer.cs @@ -28,7 +28,7 @@ namespace Tgstation.Server.Host.Utils { if (referenceCount == 0) return Task.CompletedTask; - return onZeroReferencesTcs.Task; + return onZeroReferencesTcs!.Task; } } } @@ -41,7 +41,7 @@ namespace Tgstation.Server.Host.Utils /// /// Backing for . /// - TaskCompletionSource onZeroReferencesTcs; + TaskCompletionSource? onZeroReferencesTcs; /// /// Count of active s. @@ -77,7 +77,7 @@ namespace Tgstation.Server.Host.Utils { lock (referenceCountLock) if (--referenceCount == 0) - onZeroReferencesTcs.SetResult(); + onZeroReferencesTcs!.SetResult(); }); return reference; } diff --git a/src/Tgstation.Server.Host/Utils/SemaphoreSlimContext.cs b/src/Tgstation.Server.Host/Utils/SemaphoreSlimContext.cs index 25f070cc4b..e9a40d7c68 100644 --- a/src/Tgstation.Server.Host/Utils/SemaphoreSlimContext.cs +++ b/src/Tgstation.Server.Host/Utils/SemaphoreSlimContext.cs @@ -28,7 +28,7 @@ namespace Tgstation.Server.Host.Utils /// The to lock. /// The result of the lock attempt. /// A for the lock on success, or if it was not acquired. - public static SemaphoreSlimContext TryLock(SemaphoreSlim semaphore, out bool locked) + public static SemaphoreSlimContext? TryLock(SemaphoreSlim semaphore, out bool locked) { ArgumentNullException.ThrowIfNull(semaphore); locked = semaphore.Wait(TimeSpan.Zero); diff --git a/src/Tgstation.Server.Host/Utils/SignalR/ComprehensiveHubContext.cs b/src/Tgstation.Server.Host/Utils/SignalR/ComprehensiveHubContext.cs index 1f09be3571..d9c32143ce 100644 --- a/src/Tgstation.Server.Host/Utils/SignalR/ComprehensiveHubContext.cs +++ b/src/Tgstation.Server.Host/Utils/SignalR/ComprehensiveHubContext.cs @@ -44,7 +44,7 @@ namespace Tgstation.Server.Host.Utils.SignalR readonly ConcurrentDictionary> userConnections; /// - public event Func, Task>, CancellationToken, ValueTask> OnConnectionMapGroups; + public event Func, Task>, CancellationToken, ValueTask>? OnConnectionMapGroups; /// /// Initializes a new instance of the class. @@ -65,7 +65,7 @@ namespace Tgstation.Server.Host.Utils.SignalR public List UserConnectionIds(User user) { ArgumentNullException.ThrowIfNull(user); - var connectionIds = userConnections.GetOrAdd(user.Id.Value, _ => new Dictionary()); + var connectionIds = userConnections.GetOrAdd(user.Require(x => x.Id), _ => new Dictionary()); lock (connectionIds) return connectionIds.Keys.ToList(); } @@ -76,7 +76,7 @@ namespace Tgstation.Server.Host.Utils.SignalR ArgumentNullException.ThrowIfNull(authenticationContext); ArgumentNullException.ThrowIfNull(hub); - var userId = authenticationContext.User.Id.Value; + var userId = authenticationContext.User.Require(x => x.Id); var context = hub.Context; logger.LogTrace( "Mapping user {userId} to hub connection ID: {connectionId}", @@ -129,11 +129,12 @@ namespace Tgstation.Server.Host.Utils.SignalR public void AbortUnauthedConnections(User user) { ArgumentNullException.ThrowIfNull(user); - logger.LogTrace("NotifyAndAbortUnauthedConnections. UID {userId}", user.Id.Value); + var uid = user.Require(x => x.Id); + logger.LogTrace("NotifyAndAbortUnauthedConnections. UID {userId}", uid); - List connections = null; + List? connections = null; userConnections.AddOrUpdate( - user.Id.Value, + uid, _ => new Dictionary(), (_, old) => { @@ -146,7 +147,7 @@ namespace Tgstation.Server.Host.Utils.SignalR return old; }); - foreach (var context in connections) + foreach (var context in connections!) context.Abort(); } } diff --git a/src/Tgstation.Server.Host/Utils/SignalR/ConnectionMappingHub.cs b/src/Tgstation.Server.Host/Utils/SignalR/ConnectionMappingHub.cs index 05e2eb8742..80e89089fa 100644 --- a/src/Tgstation.Server.Host/Utils/SignalR/ConnectionMappingHub.cs +++ b/src/Tgstation.Server.Host/Utils/SignalR/ConnectionMappingHub.cs @@ -50,7 +50,7 @@ namespace Tgstation.Server.Host.Utils.SignalR /// [AllowAnonymous] - public override Task OnDisconnectedAsync(Exception exception) + public override Task OnDisconnectedAsync(Exception? exception) { connectionMapper.UserDisconnected(Context.ConnectionId); return base.OnDisconnectedAsync(exception); diff --git a/src/Tgstation.Server.Host/Utils/SwaggerConfiguration.cs b/src/Tgstation.Server.Host/Utils/SwaggerConfiguration.cs index 8f8b0195bd..a700ccc9cb 100644 --- a/src/Tgstation.Server.Host/Utils/SwaggerConfiguration.cs +++ b/src/Tgstation.Server.Host/Utils/SwaggerConfiguration.cs @@ -345,7 +345,7 @@ namespace Tgstation.Server.Host.Utils ArgumentNullException.ThrowIfNull(operation); ArgumentNullException.ThrowIfNull(context); - operation.OperationId = $"{context.MethodInfo.DeclaringType.Name}.{context.MethodInfo.Name}"; + operation.OperationId = $"{context.MethodInfo.DeclaringType!.Name}.{context.MethodInfo.Name}"; var authAttributes = context .MethodInfo diff --git a/tests/Tgstation.Server.Api.Tests/Models/Internal/TestEngineVersion.cs b/tests/Tgstation.Server.Api.Tests/Models/Internal/TestEngineVersion.cs index a426927b01..f13f09f3ff 100644 --- a/tests/Tgstation.Server.Api.Tests/Models/Internal/TestEngineVersion.cs +++ b/tests/Tgstation.Server.Api.Tests/Models/Internal/TestEngineVersion.cs @@ -11,28 +11,36 @@ namespace Tgstation.Server.Api.Models.Internal.Tests public void TestParsing() { Assert.IsTrue(EngineVersion.TryParse("OpenDream-6894ba0702c1764d333eb52aa0cc211d62e2cb1c-1", out var version)); + Assert.IsNotNull(version); Assert.AreEqual(EngineType.OpenDream, version.Engine); Assert.AreEqual("6894ba0702c1764d333eb52aa0cc211d62e2cb1c", version.SourceSHA); Assert.IsNull(version.Version); Assert.AreEqual(1, version.CustomIteration); Assert.IsTrue(EngineVersion.TryParse("OpenDream-6894ba0702c1764d333eb52aa0cc211d62e2cb1c", out version)); + Assert.IsNotNull(version); Assert.AreEqual(EngineType.OpenDream, version.Engine); Assert.AreEqual("6894ba0702c1764d333eb52aa0cc211d62e2cb1c", version.SourceSHA); Assert.IsNull(version.Version); Assert.IsFalse(version.CustomIteration.HasValue); Assert.IsTrue(EngineVersion.TryParse("515.1616", out version)); + Assert.IsNotNull(version); Assert.AreEqual(EngineType.Byond, version.Engine); Assert.AreEqual(new Version(515, 1616), version.Version); Assert.IsNull(version.SourceSHA); Assert.IsFalse(version.CustomIteration.HasValue); Assert.IsTrue(EngineVersion.TryParse("515.1616.12", out version)); + Assert.IsNotNull(version); Assert.AreEqual(EngineType.Byond, version.Engine); Assert.AreEqual(new Version(515, 1616), version.Version); Assert.IsNull(version.SourceSHA); Assert.AreEqual(12, version.CustomIteration); + + Assert.IsFalse(EngineVersion.TryParse("x", out version)); + Assert.IsNull(version); + Assert.ThrowsException(() => EngineVersion.Parse("x")); } } } diff --git a/tests/Tgstation.Server.Host.Tests/Components/Chat/Providers/TestDiscordProvider.cs b/tests/Tgstation.Server.Host.Tests/Components/Chat/Providers/TestDiscordProvider.cs index b21b19f0ae..58c63c6a99 100644 --- a/tests/Tgstation.Server.Host.Tests/Components/Chat/Providers/TestDiscordProvider.cs +++ b/tests/Tgstation.Server.Host.Tests/Components/Chat/Providers/TestDiscordProvider.cs @@ -5,6 +5,7 @@ using System.Threading.Tasks; using Microsoft.Extensions.Logging; using Microsoft.VisualStudio.TestTools.UnitTesting; + using Moq; using Tgstation.Server.Api.Models; @@ -30,7 +31,8 @@ namespace Tgstation.Server.Host.Components.Chat.Providers.Tests testToken1 = new ChatBot { ConnectionString = actualToken, - ReconnectionInterval = 1 + ReconnectionInterval = 1, + Instance = new Models.Instance() }; var mockSetup = new Mock(); @@ -51,6 +53,7 @@ namespace Tgstation.Server.Host.Components.Chat.Providers.Tests { ConnectionString = "fake_token", ReconnectionInterval = 1, + Instance = new Models.Instance(), }; Assert.ThrowsException(() => new DiscordProvider(null, null, null, null, null, null)); @@ -75,7 +78,8 @@ namespace Tgstation.Server.Host.Components.Chat.Providers.Tests await using var provider = new DiscordProvider(mockJobManager, Mock.Of(), mockLogger.Object, Mock.Of(), new ChatBot { ReconnectionInterval = 1, - ConnectionString = "asdf" + ConnectionString = "asdf", + Instance = new Models.Instance(), }, new GeneralConfiguration()); await Assert.ThrowsExceptionAsync(async () => await InvokeConnect(provider)); Assert.IsFalse(provider.Connected); diff --git a/tests/Tgstation.Server.Host.Tests/Components/Chat/Providers/TestIrcProvider.cs b/tests/Tgstation.Server.Host.Tests/Components/Chat/Providers/TestIrcProvider.cs index 9e8565955e..3f1e050219 100644 --- a/tests/Tgstation.Server.Host.Tests/Components/Chat/Providers/TestIrcProvider.cs +++ b/tests/Tgstation.Server.Host.Tests/Components/Chat/Providers/TestIrcProvider.cs @@ -2,12 +2,11 @@ using System.Reflection; using System.Threading; using System.Threading.Tasks; -using System.Xml.Linq; using Microsoft.Extensions.Logging; using Microsoft.VisualStudio.TestTools.UnitTesting; + using Moq; -using Serilog.Parsing; using Tgstation.Server.Api.Models; using Tgstation.Server.Host.Jobs; @@ -36,6 +35,7 @@ namespace Tgstation.Server.Host.Components.Chat.Providers.Tests var mockBot = new ChatBot { Name = "test", + Instance = new Models.Instance(), Provider = ChatProvider.Irc }; @@ -82,6 +82,7 @@ namespace Tgstation.Server.Host.Components.Chat.Providers.Tests { ConnectionString = actualToken, Provider = ChatProvider.Irc, + Instance = new Models.Instance(), }); Assert.IsFalse(provider.Connected); await InvokeConnect(provider); diff --git a/tests/Tgstation.Server.Host.Tests/Components/Engine/TestOpenDreamInstaller.cs b/tests/Tgstation.Server.Host.Tests/Components/Engine/TestOpenDreamInstaller.cs index 6b8024ddac..2a62fea29a 100644 --- a/tests/Tgstation.Server.Host.Tests/Components/Engine/TestOpenDreamInstaller.cs +++ b/tests/Tgstation.Server.Host.Tests/Components/Engine/TestOpenDreamInstaller.cs @@ -9,10 +9,12 @@ using Moq; using Tgstation.Server.Api.Models; using Tgstation.Server.Api.Models.Internal; +using Tgstation.Server.Common.Http; using Tgstation.Server.Host.Components.Repository; using Tgstation.Server.Host.Configuration; using Tgstation.Server.Host.IO; using Tgstation.Server.Host.System; +using Tgstation.Server.Host.Utils; namespace Tgstation.Server.Host.Components.Engine.Tests { @@ -35,8 +37,11 @@ namespace Tgstation.Server.Host.Components.Engine.Tests { var mockGeneralConfigOptions = new Mock>(); var generalConfig = new GeneralConfiguration(); + var mockSessionConfigOptions = new Mock>(); + var sessionConfig = new SessionConfiguration(); Assert.IsNotNull(generalConfig.OpenDreamGitUrl); mockGeneralConfigOptions.SetupGet(x => x.Value).Returns(generalConfig); + mockSessionConfigOptions.SetupGet(x => x.Value).Returns(sessionConfig); var cloneAttempts = 0; var mockRepository = new Mock(); @@ -51,7 +56,7 @@ namespace Tgstation.Server.Host.Components.Engine.Tests true, It.IsAny())) .Callback(() => ++cloneAttempts) - .Returns(ValueTask.FromResult(needsClone ? mockRepository.Object : null)) + .ReturnsAsync(needsClone ? mockRepository.Object : null) .Verifiable(Times.Exactly(1)); mockRepositoryManager.Setup(x => x.LoadRepository( @@ -69,7 +74,10 @@ namespace Tgstation.Server.Host.Components.Engine.Tests Mock.Of(), Mock.Of(), mockRepositoryManager.Object, - mockGeneralConfigOptions.Object); + Mock.Of(), + Mock.Of(), + mockGeneralConfigOptions.Object, + mockSessionConfigOptions.Object); var data = await installer.DownloadVersion( new EngineVersion diff --git a/tests/Tgstation.Server.Tests/Live/DummyChatProvider.cs b/tests/Tgstation.Server.Tests/Live/DummyChatProvider.cs index 14cc51dd4d..872e59e6e6 100644 --- a/tests/Tgstation.Server.Tests/Live/DummyChatProvider.cs +++ b/tests/Tgstation.Server.Tests/Live/DummyChatProvider.cs @@ -55,10 +55,10 @@ namespace Tgstation.Server.Tests.Live return mock.Object; } - public static Task RandomDisconnections(bool enabled, CancellationToken cancellationToken) + public static void RandomDisconnections(bool enabled) { // we just don't do random disconnections when live testing these days, too many potential issue vectors like thread exhaustion on actions runners - return Task.CompletedTask; + enableRandomDisconnections = enabled ? 1 : 0; } public DummyChatProvider( @@ -190,14 +190,14 @@ namespace Tgstation.Server.Tests.Live else channelId = (ulong)channel.IrcChannel.GetHashCode(); - var entry = new ChannelRepresentation + var entry = new ChannelRepresentation( + $"Connection_{channelId}", + $"(Friendly) Channel_ID_{channelId}", + channelId) { IsAdminChannel = channel.IsAdminChannel.Value, - ConnectionName = $"Connection_{channelId}", EmbedsSupported = ChatBot.Provider.Value != Api.Models.ChatProvider.Irc, - FriendlyName = $"(Friendly) Channel_ID_{channelId}", IsPrivateChannel = false, - RealId = channelId, Tag = channel.Tag, }; @@ -257,12 +257,9 @@ namespace Tgstation.Server.Tests.Live } while (knownChannels.ContainsKey(channelId)); - channel = new ChannelRepresentation + channel = new ChannelRepresentation($"{username}_Connection", $"{username}_Channel", channelId) { - RealId = channelId, IsPrivateChannel = true, - ConnectionName = $"{username}_Connection", - FriendlyName = $"{username}_Channel", EmbedsSupported = ChatBot.Provider.Value != Api.Models.ChatProvider.Irc, // isAdmin and Tag populated by manager @@ -282,13 +279,11 @@ namespace Tgstation.Server.Tests.Live channel = enumerator[index].Value; } - var sender = new ChatUser - { - Channel = CloneChannel(channel), - FriendlyName = username, - RealId = i + 50000, - Mention = $"@{username}", - }; + var sender = new ChatUser( + CloneChannel(channel), + username, + $"@{username}", + i + 50000); var dice = random.Next(0, 100); string content; @@ -318,13 +313,8 @@ namespace Tgstation.Server.Tests.Live else content = $"{content} embeds_test"; // NEVER send the response_overload_test, it causes so much havoc in CI and we test it manually - EnqueueMessage(new Message - { - Content = content, - User = sender, - }); + EnqueueMessage(new Message(sender, content)); } - } catch (OperationCanceledException) { diff --git a/tests/Tgstation.Server.Tests/Live/DummyChatProviderFactory.cs b/tests/Tgstation.Server.Tests/Live/DummyChatProviderFactory.cs index dec859f6af..fe6746cb73 100644 --- a/tests/Tgstation.Server.Tests/Live/DummyChatProviderFactory.cs +++ b/tests/Tgstation.Server.Tests/Live/DummyChatProviderFactory.cs @@ -55,7 +55,7 @@ namespace Tgstation.Server.Tests.Live ? new Random().Next() : 22475; - logger.LogInformation("Random seed: {0}", randomSeed); + logger.LogInformation("Random seed: {randomSeed}", randomSeed); var baseRng = new Random(randomSeed); seededRng = new Dictionary{ diff --git a/tests/Tgstation.Server.Tests/Live/Instance/EngineTest.cs b/tests/Tgstation.Server.Tests/Live/Instance/EngineTest.cs index 57c4255130..a2f8249544 100644 --- a/tests/Tgstation.Server.Tests/Live/Instance/EngineTest.cs +++ b/tests/Tgstation.Server.Tests/Live/Instance/EngineTest.cs @@ -134,17 +134,12 @@ namespace Tgstation.Server.Tests.Live.Instance cancellationToken), ErrorCode.ModelValidationFailure); public static int EngineInstallationTimeout(EngineVersion testVersion) - { - switch (testVersion.Engine.Value) + => testVersion.Engine.Value switch { - case EngineType.Byond: - return 30; - case EngineType.OpenDream: - return 500; - default: - throw new InvalidOperationException($"Unknown engine type: {testVersion.Engine.Value}"); - } - } + EngineType.Byond => 30, + EngineType.OpenDream => 500, + _ => throw new InvalidOperationException($"Unknown engine type: {testVersion.Engine.Value}"), + }; int EngineInstallationTimeout() => EngineInstallationTimeout(testVersion); @@ -302,6 +297,7 @@ namespace Tgstation.Server.Tests.Live.Instance Mock.Of(), fileDownloader, generalConfigOptionsMock.Object, + sessionConfigOptionsMock.Object, Mock.Of>()) : new PosixByondInstaller( Mock.Of(), diff --git a/tests/Tgstation.Server.Tests/Live/Instance/InstanceTest.cs b/tests/Tgstation.Server.Tests/Live/Instance/InstanceTest.cs index a50d4a6504..bbe349ca5c 100644 --- a/tests/Tgstation.Server.Tests/Live/Instance/InstanceTest.cs +++ b/tests/Tgstation.Server.Tests/Live/Instance/InstanceTest.cs @@ -17,6 +17,7 @@ using Tgstation.Server.Api.Models.Request; using Tgstation.Server.Api.Models.Response; using Tgstation.Server.Client; using Tgstation.Server.Client.Components; +using Tgstation.Server.Common.Http; using Tgstation.Server.Host.Components; using Tgstation.Server.Host.Components.Engine; using Tgstation.Server.Host.Components.Events; @@ -25,6 +26,7 @@ using Tgstation.Server.Host.Configuration; using Tgstation.Server.Host.IO; using Tgstation.Server.Host.Jobs; using Tgstation.Server.Host.System; +using Tgstation.Server.Host.Utils; namespace Tgstation.Server.Tests.Live.Instance { @@ -116,13 +118,17 @@ namespace Tgstation.Server.Tests.Live.Instance Mock.Of>(), Mock.Of>(), genConfig), - mockOptions.Object) + Mock.Of(), + Mock.Of(), + mockOptions.Object, + Options.Create(new SessionConfiguration())) : new PlatformIdentifier().IsWindows ? new WindowsByondInstaller( Mock.Of(), Mock.Of(), fileDownloader, Options.Create(genConfig), + Options.Create(new SessionConfiguration()), Mock.Of>()) : new PosixByondInstaller( Mock.Of(), diff --git a/tests/Tgstation.Server.Tests/Live/Instance/JobsHubTests.cs b/tests/Tgstation.Server.Tests/Live/Instance/JobsHubTests.cs index 84985b6a9b..b3149556e5 100644 --- a/tests/Tgstation.Server.Tests/Live/Instance/JobsHubTests.cs +++ b/tests/Tgstation.Server.Tests/Live/Instance/JobsHubTests.cs @@ -44,7 +44,7 @@ namespace Tgstation.Server.Tests.Live.Instance finishTcs = new TaskCompletionSource(); seenJobs = new ConcurrentDictionary(); - permlessSeenJobs = new HashSet(); + permlessSeenJobs = []; } public Task ReceiveJobUpdate(JobResponse job, CancellationToken cancellationToken) @@ -199,7 +199,7 @@ namespace Tgstation.Server.Tests.Live.Instance } static DateTimeOffset PerformDBTruncation(DateTimeOffset original) - => new DateTimeOffset( + => new( original.Ticks - (original.Ticks % TimeSpan.TicksPerSecond), original.Offset); @@ -234,7 +234,7 @@ namespace Tgstation.Server.Tests.Live.Instance Assert.IsTrue(accountedJobs <= seenJobs.Count); Assert.AreNotEqual(0, permlessSeenJobs.Count); Assert.IsTrue(permlessSeenJobs.Count < seenJobs.Count); - Assert.IsTrue(permlessSeenJobs.All(id => seenJobs.ContainsKey(id))); + Assert.IsTrue(permlessSeenJobs.All(id => seenJobs.ContainsKey(id)), $"Saw permless job(s) that wasn't seen:{Environment.NewLine}{JobListFormatter(permlessSeenJobs.Where(id => !seenJobs.ContainsKey(id)).Select(id => allJobs.First(x => x.Id == id)))}"); await using var conn3 = (HubConnection)await permedUser.SubscribeToJobUpdates( this, diff --git a/tests/Tgstation.Server.Tests/Live/Instance/TestBridgeHandler.cs b/tests/Tgstation.Server.Tests/Live/Instance/TestBridgeHandler.cs index 4276067b19..e00421e295 100644 --- a/tests/Tgstation.Server.Tests/Live/Instance/TestBridgeHandler.cs +++ b/tests/Tgstation.Server.Tests/Live/Instance/TestBridgeHandler.cs @@ -15,17 +15,20 @@ namespace Tgstation.Server.Tests.Live.Instance { sealed class TestBridgeHandler : Chunker, IBridgeHandler { - class DMApiParametersImpl : DMApiParameters { } + class DMApiParametersImpl : DMApiParameters + { + public DMApiParametersImpl(string accessIdentifier) + : base(accessIdentifier) + { + } + } class BridgeResponseHack : BridgeResponse { public string IntegrationHack { get; set; } } - public DMApiParameters DMApiParameters => new DMApiParametersImpl - { - AccessIdentifier = accessIdentifier - }; + public DMApiParameters DMApiParameters => new DMApiParametersImpl(accessIdentifier); long lastBridgeRequestSize = 0; diff --git a/tests/Tgstation.Server.Tests/Live/Instance/WatchdogTest.cs b/tests/Tgstation.Server.Tests/Live/Instance/WatchdogTest.cs index c2fa31ba31..678ef10fc7 100644 --- a/tests/Tgstation.Server.Tests/Live/Instance/WatchdogTest.cs +++ b/tests/Tgstation.Server.Tests/Live/Instance/WatchdogTest.cs @@ -158,7 +158,7 @@ namespace Tgstation.Server.Tests.Live.Instance await TestDMApiFreeDeploy(cancellationToken); // long running test likes consistency with the channels - await DummyChatProvider.RandomDisconnections(false, cancellationToken); + DummyChatProvider.RandomDisconnections(false); await RunLongRunningTestThenUpdate(cancellationToken); @@ -771,7 +771,7 @@ namespace Tgstation.Server.Tests.Live.Instance .GetProcess(ddProc.Id); // Ensure it's responding to health checks - await Task.WhenAny(Task.Delay(6000, cancellationToken), ourProcessHandler.Lifetime); + await Task.WhenAny(Task.Delay(7000, cancellationToken), ourProcessHandler.Lifetime); Assert.IsFalse(ddProc.HasExited); // check DD agrees @@ -791,7 +791,7 @@ namespace Tgstation.Server.Tests.Live.Instance ValidateSessionId(ddStatus, true); global::System.Console.WriteLine($"WATCHDOG TEST {instanceClient.Metadata.Id}: COMMENCE PROCESS SUSPEND FOR HEALTH CHECK DEATH PID {ourProcessHandler.Id}."); - ourProcessHandler.Suspend(); + ourProcessHandler.SuspendProcess(); global::System.Console.WriteLine($"WATCHDOG TEST {instanceClient.Metadata.Id}: FINISH PROCESS SUSPEND FOR HEALTH CHECK DEATH. WAITING FOR LIFETIME {ourProcessHandler.Id}."); await Task.WhenAny(ourProcessHandler.Lifetime, Task.Delay(TimeSpan.FromMinutes(4), cancellationToken)); @@ -1013,7 +1013,7 @@ namespace Tgstation.Server.Tests.Live.Instance Assert.IsNotNull(sessionObj); var session = (ISessionController)sessionObj; - return session.ReattachInformation.Port; + return session.ReattachInformation.TopicPort ?? session.ReattachInformation.Port; } // - Uses instance manager concrete @@ -1024,22 +1024,16 @@ namespace Tgstation.Server.Tests.Live.Instance var startTime = DateTimeOffset.UtcNow - TimeSpan.FromSeconds(5); using (var instanceReference = instanceManager.GetInstanceReference(instanceClient.Metadata)) { - var mockChatUser = new ChatUser - { - Channel = new ChannelRepresentation + var mockChatUser = new ChatUser( + new ChannelRepresentation("test_connection", "Test Connection", 42) { IsAdminChannel = true, - ConnectionName = "test_connection", EmbedsSupported = true, - FriendlyName = "Test Connection", - Id = "test_channel_id", IsPrivateChannel = false, }, - FriendlyName = "Test Sender", - Id = "test_user_id", - Mention = "test_user_mention", - RealId = 1234, - }; + "Test Sender", + "test_user_mention", + 1234); var embedsResponseTask = ((WatchdogBase)instanceReference.Watchdog).HandleChatCommand( "embeds_test", diff --git a/tests/Tgstation.Server.Tests/Live/TestLiveServer.cs b/tests/Tgstation.Server.Tests/Live/TestLiveServer.cs index 6edee102ea..9ac95ce865 100644 --- a/tests/Tgstation.Server.Tests/Live/TestLiveServer.cs +++ b/tests/Tgstation.Server.Tests/Live/TestLiveServer.cs @@ -54,12 +54,12 @@ namespace Tgstation.Server.Tests.Live { public static readonly Version TestUpdateVersion = new(5, 11, 0); - static readonly Lazy odDMPort = new Lazy(() => FreeTcpPort()); - static readonly Lazy odDDPort = new Lazy(() => FreeTcpPort(odDMPort.Value)); - static readonly Lazy compatDMPort = new Lazy(() => FreeTcpPort(odDDPort.Value, odDMPort.Value)); - static readonly Lazy compatDDPort = new Lazy(() => FreeTcpPort(odDDPort.Value, odDMPort.Value, compatDMPort.Value)); - static readonly Lazy mainDDPort = new Lazy(() => FreeTcpPort(odDDPort.Value, odDMPort.Value, compatDMPort.Value, compatDDPort.Value)); - static readonly Lazy mainDMPort = new Lazy(() => FreeTcpPort(odDDPort.Value, odDMPort.Value, compatDMPort.Value, compatDDPort.Value, mainDDPort.Value)); + static readonly Lazy odDMPort = new(() => FreeTcpPort()); + static readonly Lazy odDDPort = new(() => FreeTcpPort(odDMPort.Value)); + static readonly Lazy compatDMPort = new(() => FreeTcpPort(odDDPort.Value, odDMPort.Value)); + static readonly Lazy compatDDPort = new(() => FreeTcpPort(odDDPort.Value, odDMPort.Value, compatDMPort.Value)); + static readonly Lazy mainDDPort = new(() => FreeTcpPort(odDDPort.Value, odDMPort.Value, compatDMPort.Value, compatDDPort.Value)); + static readonly Lazy mainDMPort = new(() => FreeTcpPort(odDDPort.Value, odDMPort.Value, compatDMPort.Value, compatDDPort.Value, mainDDPort.Value)); readonly ServerClientFactory clientFactory = new (new ProductHeaderValue(Assembly.GetExecutingAssembly().GetName().Name, Assembly.GetExecutingAssembly().GetName().Version.ToString())); @@ -147,9 +147,6 @@ namespace Tgstation.Server.Tests.Live static ushort FreeTcpPort(params ushort[] usedPorts) { - var portList = new ushort[] { 42069, 42070, 42071, 42072, 42073, 42074 }; - return portList.First(x => !usedPorts.Contains(x)); - /* ushort result; var listeners = new List(); @@ -171,7 +168,7 @@ namespace Tgstation.Server.Tests.Live result = (ushort)((IPEndPoint)l.LocalEndpoint).Port; } - while (usedPorts.Contains(result) || result < 10000); + while (usedPorts.Contains(result) || result < 20000); } finally { @@ -180,8 +177,9 @@ namespace Tgstation.Server.Tests.Live l.Stop(); } } + + Console.WriteLine($"Allocated port: {result}"); return result; - */ } [ClassInitialize] @@ -196,7 +194,7 @@ namespace Tgstation.Server.Tests.Live await CachingFileDownloader.InitializeAndInjectForLiveTests(default); - await DummyChatProvider.RandomDisconnections(true, default); + DummyChatProvider.RandomDisconnections(true); ServerClientFactory.ApiClientFactory = new RateLimitRetryingApiClientFactory(); var connectionString = Environment.GetEnvironmentVariable("TGS_TEST_CONNECTION_STRING"); @@ -1746,7 +1744,7 @@ namespace Tgstation.Server.Tests.Live await chatTestObj.RunPostTest(cancellationToken); await repoTest; - await DummyChatProvider.RandomDisconnections(false, cancellationToken); + DummyChatProvider.RandomDisconnections(false); jobsHubTest.CompleteNow(); await jobsHubTestTask; diff --git a/tests/Tgstation.Server.Tests/TestVersions.cs b/tests/Tgstation.Server.Tests/TestVersions.cs index 34dad9e006..7b52b79773 100644 --- a/tests/Tgstation.Server.Tests/TestVersions.cs +++ b/tests/Tgstation.Server.Tests/TestVersions.cs @@ -126,6 +126,7 @@ namespace Tgstation.Server.Tests Mock.Of(), new CachingFileDownloader(Mock.Of>()), mockGeneralConfigurationOptions.Object, + mockSessionConfigurationOptions.Object, Mock.Of>()); const string ArchiveEntryPath = "byond/bin/dd.exe"; @@ -193,6 +194,7 @@ namespace Tgstation.Server.Tests Mock.Of(), fileDownloader, mockGeneralConfigurationOptions.Object, + mockSessionConfigurationOptions.Object, loggerFactory.CreateLogger()) : new PosixByondInstaller( new PosixPostWriteHandler(loggerFactory.CreateLogger()), @@ -492,7 +494,7 @@ namespace Tgstation.Server.Tests var shouldSupportMapThreads = engineVersion.Version >= MapThreadsVersion(); - await File.WriteAllBytesAsync("fake.dmb", Array.Empty(), CancellationToken.None); + await File.WriteAllBytesAsync("fake.dmb", [], CancellationToken.None); try {