Merge pull request #1742 from tgstation/TheGreatNullening

Add support for nullable reference types in `Tgstation.Server.Host`. Use the RobustToolbox watchdog API to shutdown OpenDream
This commit is contained in:
Jordan Dominion
2023-12-24 08:46:45 -05:00
committed by GitHub
355 changed files with 7345 additions and 2226 deletions
+1 -1
View File
@@ -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: |
@@ -38,7 +38,7 @@ namespace Tgstation.Server.Api.Models
public int? CustomIteration { get; set; }
/// <summary>
/// Parses a stringified <see cref="EngineVersion"/>.
/// Attempts to parse a stringified <see cref="EngineVersion"/>.
/// </summary>
/// <param name="input">The input <see cref="string"/>.</param>
/// <param name="engineVersion">The output <see cref="EngineVersion"/>.</param>
@@ -110,6 +110,23 @@ namespace Tgstation.Server.Api.Models
return true;
}
/// <summary>
/// Parses a stringified <see cref="EngineVersion"/>.
/// </summary>
/// <param name="input">The input <see cref="string"/>.</param>
/// <returns>The output <see cref="EngineVersion"/>.</returns>
/// <exception cref="InvalidOperationException">If the <paramref name="input"/> is not a valid stringified <see cref="EngineVersion"/>.</exception>
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}");
}
/// <summary>
/// Initializes a new instance of the <see cref="EngineVersion"/> class.
/// </summary>
+3 -3
View File
@@ -150,10 +150,10 @@ namespace Tgstation.Server.Api.Models
InstanceLimitReached,
/// <summary>
/// Attempted to create an <see cref="Instance"/> with a whitespace <see cref="NamedEntity.Name"/>.
/// Attempted to create an <see cref="Instance"/> with a whitespace <see cref="NamedEntity.Name"/> or <see cref="Instance.Path"/>.
/// </summary>
[Description("Instance names cannot be whitespace!")]
InstanceWhitespaceName,
[Description("Instance names and paths cannot be whitespace!")]
InstanceWhitespaceNameOrPath,
/// <summary>
/// The <see cref="ApiHeaders.InstanceIdHeader"/> header was required but not set.
+1 -1
View File
@@ -16,7 +16,7 @@ namespace Tgstation.Server.Api.Models
/// <summary>
/// When the instance is being moved.
/// </summary>
[Description("Instance move")]
[Description("Move instance")]
Move,
/// <summary>
@@ -20,7 +20,7 @@ namespace Tgstation.Server.Api.Models.Response
/// <param name="newVersion">The value of <see cref="NewVersion"/>.</param>
/// <param name="fileTicket">The optional value of <see cref="FileTicketResponse.FileTicket"/>.</param>
[JsonConstructor]
public ServerUpdateResponse(Version newVersion, string fileTicket)
public ServerUpdateResponse(Version newVersion, string? fileTicket)
{
NewVersion = newVersion ?? throw new ArgumentNullException(nameof(newVersion));
FileTicket = fileTicket;
@@ -1,4 +1,6 @@
namespace Tgstation.Server.Host.Components.Chat
using System;
namespace Tgstation.Server.Host.Components.Chat
{
/// <summary>
/// Represents a mapping of a <see cref="ChannelRepresentation.RealId"/>.
@@ -38,6 +40,15 @@
/// <summary>
/// The <see cref="ChannelRepresentation"/> with the mapped Id.
/// </summary>
public ChannelRepresentation Channel { get; set; }
public ChannelRepresentation Channel { get; }
/// <summary>
/// Initializes a new instance of the <see cref="ChannelMapping"/> class.
/// </summary>
/// <param name="channel">The value of <see cref="Channel"/>.</param>
public ChannelMapping(ChannelRepresentation channel)
{
Channel = channel ?? throw new ArgumentNullException(nameof(channel));
}
}
}
@@ -14,7 +14,7 @@ namespace Tgstation.Server.Host.Components.Chat
/// <summary>
/// Backing field for <see cref="RealId"/>. Represented as a <see cref="string"/> to avoid BYOND percision loss.
/// </summary>
public string Id { get; set; }
public string Id { get; private set; }
/// <summary>
/// The <see cref="Providers.IProvider"/> channel Id.
@@ -30,12 +30,12 @@ namespace Tgstation.Server.Host.Components.Chat
/// <summary>
/// The user friendly name of the <see cref="ChannelRepresentation"/>.
/// </summary>
public string FriendlyName { get; set; }
public string FriendlyName { get; }
/// <summary>
/// The name of the connection the <see cref="ChannelRepresentation"/> belongs to.
/// </summary>
public string ConnectionName { get; set; }
public string ConnectionName { get; }
/// <summary>
/// If this is considered a channel for admin commands.
@@ -45,16 +45,30 @@ namespace Tgstation.Server.Host.Components.Chat
/// <summary>
/// If this is a 1-to-1 chat channel.
/// </summary>
public bool IsPrivateChannel { get; set; }
public bool IsPrivateChannel { get; init; }
/// <summary>
/// For user use.
/// </summary>
public string Tag { get; set; }
public string? Tag { get; set; }
/// <summary>
/// If this channel supports embeds.
/// </summary>
public bool EmbedsSupported { get; set; }
public bool EmbedsSupported { get; init; }
/// <summary>
/// Initializes a new instance of the <see cref="ChannelRepresentation"/> class.
/// </summary>
/// <param name="connectionName">The value of <see cref="ConnectionName"/>.</param>
/// <param name="friendlyName">The value of <see cref="FriendlyName"/>.</param>
/// <param name="id">The value of <see cref="RealId"/>/<see cref="Id"/>.</param>
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;
}
}
}
@@ -24,8 +24,7 @@ using Tgstation.Server.Host.Utils;
namespace Tgstation.Server.Host.Components.Chat
{
/// <inheritdoc cref="IChatManager" />
// TODO: Decomplexify
#pragma warning disable CA1506
#pragma warning disable CA1506 // TODO: Decomplexify
sealed class ChatManager : IChatManager, IRestartHandler
{
/// <summary>
@@ -69,7 +68,7 @@ namespace Tgstation.Server.Host.Components.Chat
readonly Dictionary<long, IProvider> providers;
/// <summary>
/// Map of <see cref="SemaphoreSlim"/>s used to guard concurrent access to <see cref="ChangeChannels(long, IEnumerable{Models.ChatChannel}, CancellationToken)"/>, keyed by <see cref="ChatBotSettings"/> <see cref="Api.Models.EntityId.Id"/>.
/// Map of <see cref="SemaphoreSlim"/>s used to guard concurrent access to <see cref="ChangeChannels(long, IEnumerable{Models.ChatChannel}, CancellationToken)"/>, keyed by <see cref="ChatBotSettings"/> <see cref="EntityId.Id"/>.
/// </summary>
readonly ConcurrentDictionary<long, SemaphoreSlim> changeChannelSemaphores;
@@ -101,17 +100,17 @@ namespace Tgstation.Server.Host.Components.Chat
/// <summary>
/// The <see cref="ICustomCommandHandler"/> for the <see cref="ChangeChannels(long, IEnumerable{Models.ChatChannel}, CancellationToken)"/>.
/// </summary>
ICustomCommandHandler customCommandHandler;
ICustomCommandHandler? customCommandHandler;
/// <summary>
/// The <see cref="Task"/> that monitors incoming chat messages.
/// </summary>
Task chatHandler;
Task? chatHandler;
/// <summary>
/// A <see cref="Task"/> that represents the <see cref="IProvider"/>s initial connection.
/// </summary>
Task initialProviderConnectionsTask;
Task? initialProviderConnectionsTask;
/// <summary>
/// A <see cref="Task"/> 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
}
/// <inheritdoc />
public Func<string, string, Action<bool>> QueueDeploymentMessage(
public Func<string?, string, Action<bool>> QueueDeploymentMessage(
Models.RevisionInformation revisionInformation,
EngineVersion engineVersion,
DateTimeOffset? estimatedCompletionTime,
string gitHubOwner,
string gitHubRepo,
string? gitHubOwner,
string? gitHubRepo,
bool localCommitPushed)
{
List<ulong> 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<Func<string, string, ValueTask<Func<bool, ValueTask>>>>();
var callbacks = new List<Func<string?, string, ValueTask<Func<bool, ValueTask>>>>();
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<bool, Task> finalUpdateAction = null;
async Task CallbackTask(string errorMessage, string dreamMakerOutput)
Func<bool, Task>? 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
}
/// <inheritdoc />
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
/// <param name="removeProvider">If the provider should be removed from <see cref="providers"/> and <see cref="trackingContexts"/> should be update.</param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> for the operation.</param>
/// <returns>A <see cref="ValueTask{TResult}"/> resulting in the <see cref="IProvider"/> being removed if it exists, <see langword="null"/> otherwise.</returns>
async ValueTask<IProvider> RemoveProviderChannels(long connectionId, bool removeProvider, CancellationToken cancellationToken)
async ValueTask<IProvider?> 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<Models.ChatChannel> channelsToMap;
IEnumerable<Models.ChatChannel>? 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
/// <param name="cancellationToken">The <see cref="CancellationToken"/> for the operation.</param>
/// <returns>A <see cref="ValueTask"/> representing the running operation.</returns>
#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<ICommand, IChatTrackingContext> GetCommand()
Tuple<ICommand, IChatTrackingContext?>? GetCommand()
{
if (!builtinCommands.TryGetValue(command, out var handler))
return trackingContexts
.Where(trackingContext => trackingContext.Active)
.SelectMany(trackingContext => trackingContext.CustomCommands.Select(customCommand => Tuple.Create<ICommand, IChatTrackingContext>(customCommand, trackingContext)))
.SelectMany(trackingContext => trackingContext.CustomCommands.Select(customCommand => Tuple.Create<ICommand, IChatTrackingContext?>(customCommand, trackingContext)))
.Where(tuple => tuple.Item1.Name.Equals(command, StringComparison.OrdinalIgnoreCase))
.FirstOrDefault();
return Tuple.Create<ICommand, IChatTrackingContext>(handler, null);
return Tuple.Create<ICommand, IChatTrackingContext?>(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<IProvider, Task<Message>>();
var messageTasks = new Dictionary<IProvider, Task<Message?>>();
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
/// <param name="message">The <see cref="MessageContent"/> to send.</param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> for the operation.</param>
/// <returns>A <see cref="Task"/> representing the running operation.</returns>
ValueTask SendMessage(IEnumerable<ulong> channelIds, Message replyTo, MessageContent message, CancellationToken cancellationToken)
ValueTask SendMessage(IEnumerable<ulong> 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(),
@@ -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<ChatManager>(),
initialChatBots.Where(x => x.Enabled.Value));
initialChatBots.Where(x => x.Require(y => y.Enabled)));
}
}
@@ -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
{
/// <inheritdoc />
sealed class ChatTrackingContext : IChatTrackingContext
sealed class ChatTrackingContext : DisposeInvoker, IChatTrackingContext
{
/// <inheritdoc />
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<ChatTrackingContext> logger;
/// <summary>
/// <see langword="lock"/> <see cref="object"/> for modifying <see cref="onDispose"/>, <see cref="channelSink"/>, and <see cref="Channels"/>.
/// <see langword="lock"/> <see cref="object"/> for modifying <see cref="Channels"/> and calling <see cref="IChannelSink.UpdateChannels(IEnumerable{ChannelRepresentation}, CancellationToken)"/>.
/// </summary>
readonly object synchronizationLock;
/// <summary>
/// The <see cref="IChannelSink"/> if any.
/// </summary>
volatile IChannelSink? channelSink;
/// <summary>
/// Backing field for <see cref="CustomCommands"/>.
/// </summary>
IReadOnlyCollection<CustomCommand> customCommands;
/// <summary>
/// The <see cref="IChannelSink"/> if any.
/// </summary>
IChannelSink channelSink;
/// <summary>
/// The <see cref="Action"/> to run when <see cref="Dispose"/>d.
/// </summary>
Action onDispose;
/// <summary>
/// Backing field for <see cref="Active"/>.
/// </summary>
@@ -91,45 +87,31 @@ namespace Tgstation.Server.Host.Components.Chat
/// <param name="customCommandHandler">The value of <see cref="customCommandHandler"/>.</param>
/// <param name="initialChannels">The initial value of <see cref="Channels"/>.</param>
/// <param name="logger">The value of <see cref="logger"/>.</param>
/// <param name="onDispose">The value of <see cref="onDispose"/>.</param>
/// <param name="disposeAction">The <see cref="IDisposable.Dispose"/> action for the <see cref="DisposeInvoker"/>.</param>
public ChatTrackingContext(
ICustomCommandHandler customCommandHandler,
IEnumerable<ChannelRepresentation> initialChannels,
ILogger<ChatTrackingContext> 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<CustomCommand>();
}
/// <inheritdoc />
public void Dispose()
{
lock (synchronizationLock)
{
onDispose?.Invoke();
onDispose = null;
}
}
/// <inheritdoc />
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!");
}
/// <inheritdoc />
@@ -13,7 +13,7 @@ namespace Tgstation.Server.Host.Components.Chat
/// <summary>
/// Backing field for <see cref="RealId"/>. Represented as a <see cref="string"/> to avoid BYOND percision loss.
/// </summary>
public string Id { get; set; }
public string Id { get; private set; }
/// <summary>
/// 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);
}
/// <summary>
/// The friendly name of the user.
/// </summary>
public string FriendlyName { get; set; }
public string FriendlyName { get; }
/// <summary>
/// The text to mention the user.
/// </summary>
public string Mention { get; set; }
public string Mention { get; }
/// <summary>
/// The <see cref="ChannelRepresentation"/> the user spoke from.
/// </summary>
public ChannelRepresentation Channel { get; set; }
public ChannelRepresentation Channel { get; }
/// <summary>
/// Initializes a new instance of the <see cref="ChatUser"/> class.
/// </summary>
/// <param name="channel">The value of <see cref="Channel"/>.</param>
/// <param name="friendlyName">The value of <see cref="FriendlyName"/>.</param>
/// <param name="mention">The value of <see cref="Mention"/>.</param>
/// <param name="realId">The value of <see cref="RealId"/>.</param>
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;
}
}
}
@@ -46,7 +46,7 @@ namespace Tgstation.Server.Host.Components.Chat.Commands
/// <summary>
/// The <see cref="IWatchdog"/> for the <see cref="CommandFactory"/>.
/// </summary>
IWatchdog watchdog;
IWatchdog? watchdog;
/// <summary>
/// Initializes a new instance of the <see cref="CommandFactory"/> class.
@@ -12,18 +12,31 @@ namespace Tgstation.Server.Host.Components.Chat.Commands
public sealed class CustomCommand : ICommand
{
/// <inheritdoc />
public string Name { get; set; }
public string Name { get; }
/// <inheritdoc />
public string HelpText { get; set; }
public string HelpText { get; }
/// <inheritdoc />
public bool AdminOnly { get; set; }
public bool AdminOnly { get; }
/// <summary>
/// The <see cref="ICustomCommandHandler"/> for the <see cref="CustomCommand"/>.
/// </summary>
ICustomCommandHandler handler;
ICustomCommandHandler? handler;
/// <summary>
/// Initializes a new instance of the <see cref="CustomCommand"/> class.
/// </summary>
/// <param name="name">The value of <see cref="Name"/>.</param>
/// <param name="helpText">The value of <see cref="HelpText"/>.</param>
/// <param name="adminOnly">The value of <see cref="AdminOnly"/>.</param>
public CustomCommand(string name, string helpText, bool adminOnly)
{
Name = name ?? throw new ArgumentNullException(nameof(name));
HelpText = helpText ?? throw new ArgumentNullException(nameof(helpText));
AdminOnly = adminOnly;
}
/// <summary>
/// Set a new <paramref name="handler"/>.
@@ -48,7 +48,7 @@ namespace Tgstation.Server.Host.Components.Chat.Commands
/// <inheritdoc />
public ValueTask<MessageContent> 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}"),
};
@@ -81,7 +81,7 @@ namespace Tgstation.Server.Host.Components.Chat.Commands
#pragma warning disable CA1506
public async ValueTask<MessageContent> Invoke(string arguments, ChatUser user, CancellationToken cancellationToken)
{
IEnumerable<Models.TestMerge> results = null;
IEnumerable<Models.TestMerge> 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<Models.TestMerge>();
results = compileJobToUse?.RevisionInformation.ActiveTestMerges?.Select(x => x.TestMerge).ToList() ?? Enumerable.Empty<Models.TestMerge>();
}
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
@@ -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
@@ -67,12 +67,12 @@ namespace Tgstation.Server.Host.Components.Chat
/// <param name="gitHubRepo">The repository GitHub name, if any.</param>
/// <param name="localCommitPushed"><see langword="true"/> if the local deployment commit was pushed to the remote repository.</param>
/// <returns>A <see cref="Func{T1, T2, TResult}"/> to call to update the message at the deployment's conclusion. Parameters: Error message if any, DreamMaker output if any. Returns an <see cref="Action"/> to call to mark the deployment as active/inactive. Parameter: If the deployment is being activated or inactivated.</returns>
Func<string, string, Action<bool>> QueueDeploymentMessage(
Func<string?, string, Action<bool>> QueueDeploymentMessage(
Models.RevisionInformation revisionInformation,
Api.Models.EngineVersion engineVersion,
DateTimeOffset? estimatedCompletionTime,
string gitHubOwner,
string gitHubRepo,
string? gitHubOwner,
string? gitHubRepo,
bool localCommitPushed);
/// <summary>
@@ -12,5 +12,19 @@ namespace Tgstation.Server.Host.Components.Chat.Providers
/// The <see cref="IMessageReference"/> of the source <see cref="Message"/>.
/// </summary>
public Optional<IMessageReference> MessageReference { get; set; }
/// <summary>
/// Initializes a new instance of the <see cref="DiscordMessage"/> class.
/// </summary>
/// <param name="user">The value of <see cref="Message.User"/>.</param>
/// <param name="content">The value of <see cref="Message.Content"/>.</param>
/// <param name="messageReference">The value of <see cref="MessageReference"/>.</param>
public DiscordMessage(ChatUser user, string content, Optional<IMessageReference> messageReference)
: base(
user,
content)
{
MessageReference = messageReference;
}
}
}
@@ -58,13 +58,13 @@ namespace Tgstation.Server.Host.Components.Chat.Providers
/// <summary>
/// The <see cref="ChannelType"/>s supported by the <see cref="DiscordProvider"/> for mapping.
/// </summary>
static readonly ChannelType[] SupportedGuildChannelTypes = new[]
{
static readonly ChannelType[] SupportedGuildChannelTypes =
[
ChannelType.GuildText,
ChannelType.GuildAnnouncement,
ChannelType.PrivateThread,
ChannelType.PublicThread,
};
];
/// <summary>
/// The <see cref="IAssemblyInformationProvider"/> for the <see cref="DiscordProvider"/>.
@@ -104,17 +104,17 @@ namespace Tgstation.Server.Host.Components.Chat.Providers
/// <summary>
/// The <see cref="CancellationTokenSource"/> for the <see cref="gatewayTask"/>.
/// </summary>
CancellationTokenSource gatewayCts;
CancellationTokenSource? gatewayCts;
/// <summary>
/// The <see cref="TaskCompletionSource"/> for the initial gateway connection event.
/// </summary>
TaskCompletionSource gatewayReadyTcs;
TaskCompletionSource? gatewayReadyTcs;
/// <summary>
/// The <see cref="Task"/> representing the lifetime of the client.
/// </summary>
Task<Result> gatewayTask;
Task<Result>? gatewayTask;
/// <summary>
/// The bot's <see cref="Snowflake"/>.
@@ -157,8 +157,8 @@ namespace Tgstation.Server.Host.Components.Chat.Providers
mappedChannels = new List<ulong>();
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
}
/// <inheritdoc />
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<IDiscordRestChannelAPI>();
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
}
/// <inheritdoc />
public override async ValueTask<Func<string, string, ValueTask<Func<bool, ValueTask>>>> SendUpdateMessage(
public override async ValueTask<Func<string?, string, ValueTask<Func<bool, 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<IEmbedAuthor> 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<IDiscordRestChannelAPI>();
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<IDiscordRestGuildAPI>();
var guildTasks = new ConcurrentDictionary<Snowflake, Task<Result<IGuild>>>();
async ValueTask<Tuple<Models.ChatChannel, IEnumerable<ChannelRepresentation>>> GetModelChannelFromDBChannel(Models.ChatChannel channelFromDB)
async ValueTask<Tuple<Models.ChatChannel, IEnumerable<ChannelRepresentation>>?> 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<Tuple<Models.ChatChannel, IEnumerable<ChannelRepresentation>>>() // 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<Tuple<Models.ChatChannel, IEnumerable<ChannelRepresentation>>> 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<ChannelRepresentation?>(
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<Models.ChatChannel, IEnumerable<ChannelRepresentation>>(
channelIdZeroModel,
unmappedTasks.Select(x => x.Result).Where(x => x != null).ToList());
unmappedTasks
.Select(x => x.Result)
.Where(x => x != null)
.Cast<ChannelRepresentation>() // 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<Models.ChatChannel, IEnumerable<ChannelRepresentation>>(enumerator.Select(x => new KeyValuePair<Models.ChatChannel, IEnumerable<ChannelRepresentation>>(x.Item1, x.Item2)));
return new Dictionary<Models.ChatChannel, IEnumerable<ChannelRepresentation>>(list.Select(x => new KeyValuePair<Models.ChatChannel, IEnumerable<ChannelRepresentation>>(x.Item1, x.Item2)));
}
/// <summary>
@@ -881,46 +896,55 @@ namespace Tgstation.Server.Host.Components.Chat.Providers
List<IEmbedField> 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<IEmbedField>
{
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<RevInfoTestMerge>())
.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
/// <param name="embed">The <see cref="ChatEmbed"/> to convert.</param>
/// <returns>The parameter for sending a single <see cref="IEmbed"/>.</returns>
#pragma warning disable CA1502
Optional<IReadOnlyList<IEmbed>> ConvertEmbed(ChatEmbed embed)
Optional<IReadOnlyList<IEmbed>> ConvertEmbed(ChatEmbed? embed)
{
if (embed == null)
return default;
@@ -955,7 +979,7 @@ namespace Tgstation.Server.Host.Components.Chat.Providers
embed.Author = null;
}
List<IEmbedField> fields = null;
List<IEmbedField>? fields = null;
if (embed.Fields != null)
{
fields = new List<IEmbedField>();
@@ -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<bool>),
});
@@ -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<string>),
ProxyIconUrl = embed.Author.ProxyIconUrl ?? default(Optional<string>),
@@ -1037,14 +1061,14 @@ namespace Tgstation.Server.Host.Components.Chat.Providers
Description = embed.Description ?? default(Optional<string>),
Fields = fields ?? default(Optional<IReadOnlyList<IEmbedField>>),
Footer = embed.Footer != null
? new EmbedFooter(embed.Footer.Text)
? (Optional<IEmbedFooter>)new EmbedFooter(embed.Footer.Text!)
{
IconUrl = embed.Footer.IconUrl ?? default(Optional<string>),
ProxyIconUrl = embed.Footer.ProxyIconUrl ?? default(Optional<string>),
}
: default,
Image = embed.Image != null
? new EmbedImage(embed.Image.Url)
? new EmbedImage(embed.Image.Url!)
{
Width = embed.Image.Width ?? default(Optional<int>),
Height = embed.Image.Height ?? default(Optional<int>),
@@ -1059,7 +1083,7 @@ namespace Tgstation.Server.Host.Components.Chat.Providers
}
: default(Optional<IEmbedProvider>),
Thumbnail = embed.Thumbnail != null
? new EmbedThumbnail(embed.Thumbnail.Url)
? new EmbedThumbnail(embed.Thumbnail.Url!)
{
Width = embed.Thumbnail.Width ?? default(Optional<int>),
Height = embed.Thumbnail.Height ?? default(Optional<int>),
@@ -45,7 +45,7 @@ namespace Tgstation.Server.Host.Components.Chat.Providers
/// <param name="cancellationToken">The <see cref="CancellationToken"/> for the operation.</param>
/// <returns>A <see cref="Task{TResult}"/> resulting in the next available <see cref="Message"/> or <see langword="null"/> if the <see cref="IProvider"/> needed to reconnect.</returns>
/// <remarks>Note that private messages will come in the form of <see cref="ChannelRepresentation"/>s not returned in <see cref="MapChannels(IEnumerable{ChatChannel}, CancellationToken)"/>.</remarks>
Task<Message> NextMessage(CancellationToken cancellationToken);
Task<Message?> NextMessage(CancellationToken cancellationToken);
/// <summary>
/// Gracefully disconnects the provider. Permanently stops the reconnection timer.
@@ -65,12 +65,12 @@ namespace Tgstation.Server.Host.Components.Chat.Providers
/// <summary>
/// Send a message to the <see cref="IProvider"/>.
/// </summary>
/// <param name="replyTo">The <see cref="Message"/> to reply to.</param>
/// <param name="replyTo">The optional <see cref="Message"/> to reply to.</param>
/// <param name="message">The <see cref="MessageContent"/>.</param>
/// <param name="channelId">The <see cref="ChannelRepresentation.RealId"/> to send to.</param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> for the operation.</param>
/// <returns>A <see cref="ValueTask"/> representing the running operation.</returns>
ValueTask SendMessage(Message replyTo, MessageContent message, ulong channelId, CancellationToken cancellationToken);
ValueTask SendMessage(Message? replyTo, MessageContent message, ulong channelId, CancellationToken cancellationToken);
/// <summary>
/// Set the interval at which the provider starts jobs to try to reconnect.
@@ -92,12 +92,12 @@ namespace Tgstation.Server.Host.Components.Chat.Providers
/// <param name="localCommitPushed"><see langword="true"/> if the local deployment commit was pushed to the remote repository.</param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> for the operation.</param>
/// <returns>A <see cref="ValueTask{TResult}"/> resulting in a <see cref="Func{T1, T2, TResult}"/> 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.</returns>
ValueTask<Func<string, string, ValueTask<Func<bool, ValueTask>>>> SendUpdateMessage(
ValueTask<Func<string?, string, ValueTask<Func<bool, 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);
@@ -75,23 +75,23 @@ namespace Tgstation.Server.Host.Components.Chat.Providers
/// <summary>
/// Map of <see cref="ChannelRepresentation.RealId"/>s to channel names.
/// </summary>
readonly Dictionary<ulong, string> channelIdMap;
readonly Dictionary<ulong, string?> channelIdMap;
/// <summary>
/// Map of <see cref="ChannelRepresentation.RealId"/>s to query users.
/// </summary>
readonly Dictionary<ulong, string> queryChannelIdMap;
/// <summary>
/// The <see cref="ValueTask"/> used for <see cref="IrcConnection.Listen(bool)"/>.
/// </summary>
Task? listenTask;
/// <summary>
/// Id counter for <see cref="channelIdMap"/>.
/// </summary>
ulong channelIdCounter;
/// <summary>
/// The <see cref="ValueTask"/> used for <see cref="IrcConnection.Listen(bool)"/>.
/// </summary>
Task listenTask;
/// <summary>
/// If we are disconnecting.
/// </summary>
@@ -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<ulong, string>();
channelIdMap = new Dictionary<ulong, string?>();
queryChannelIdMap = new Dictionary<ulong, string>();
channelIdCounter = 1;
}
@@ -164,7 +164,7 @@ namespace Tgstation.Server.Host.Components.Chat.Providers
}
/// <inheritdoc />
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
}
/// <inheritdoc />
public override async ValueTask<Func<string, string, ValueTask<Func<bool, ValueTask>>>> SendUpdateMessage(
public override async ValueTask<Func<string?, string, ValueTask<Func<bool, 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<ChannelRepresentation>
{
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<ulong, string> dicToCheck)
ulong MapAndGetChannelId(Dictionary<ulong, string?> 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<ulong, string?>(queryChannelIdMap
.Cast<KeyValuePair<ulong, string?>>())); // 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);
}
@@ -1,4 +1,6 @@
namespace Tgstation.Server.Host.Components.Chat.Providers
using System;
namespace Tgstation.Server.Host.Components.Chat.Providers
{
/// <summary>
/// Represents a message received by a <see cref="IProvider"/>.
@@ -8,11 +10,22 @@
/// <summary>
/// The text of the message.
/// </summary>
public string Content { get; set; }
public string Content { get; }
/// <summary>
/// The <see cref="ChatUser"/> who sent the <see cref="Message"/>.
/// </summary>
public ChatUser User { get; set; }
public ChatUser User { get; }
/// <summary>
/// Initializes a new instance of the <see cref="Message"/> class.
/// </summary>
/// <param name="user">The value of <see cref="User"/>.</param>
/// <param name="content">The value of <see cref="Content"/>.</param>
public Message(ChatUser user, string content)
{
User = user ?? throw new ArgumentNullException(nameof(user));
Content = content ?? throw new ArgumentNullException(nameof(content));
}
}
}
@@ -42,7 +42,7 @@ namespace Tgstation.Server.Host.Components.Chat.Providers
/// <summary>
/// <see cref="Queue{T}"/> of received <see cref="Message"/>s.
/// </summary>
readonly Queue<Message> messageQueue;
readonly Queue<Message?> messageQueue;
/// <summary>
/// The backing <see cref="TaskCompletionSource"/> for <see cref="InitialConnectionJob"/>.
@@ -62,12 +62,12 @@ namespace Tgstation.Server.Host.Components.Chat.Providers
/// <summary>
/// The auto reconnect <see cref="Task"/>.
/// </summary>
Task reconnectTask;
Task? reconnectTask;
/// <summary>
/// <see cref="CancellationTokenSource"/> for <see cref="reconnectTask"/>.
/// </summary>
CancellationTokenSource reconnectCts;
CancellationTokenSource? reconnectCts;
/// <summary>
/// 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<Message>();
if (chatBot.Instance == null)
throw new ArgumentException("chatBot must have Instance!", nameof(chatBot));
messageQueue = new Queue<Message?>();
nextMessage = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously);
initialConnectionTcs = new TaskCompletionSource();
reconnectTaskLock = new object();
@@ -157,7 +160,7 @@ namespace Tgstation.Server.Host.Components.Chat.Providers
}
/// <inheritdoc />
public async Task<Message> NextMessage(CancellationToken cancellationToken)
public async Task<Message?> NextMessage(CancellationToken cancellationToken)
{
while (true)
{
@@ -191,15 +194,15 @@ namespace Tgstation.Server.Host.Components.Chat.Providers
}
/// <inheritdoc />
public abstract ValueTask SendMessage(Message replyTo, MessageContent message, ulong channelId, CancellationToken cancellationToken);
public abstract ValueTask SendMessage(Message? replyTo, MessageContent message, ulong channelId, CancellationToken cancellationToken);
/// <inheritdoc />
public abstract ValueTask<Func<string, string, ValueTask<Func<bool, ValueTask>>>> SendUpdateMessage(
public abstract ValueTask<Func<string?, string, ValueTask<Func<bool, 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 <paramref name="message"/> for <see cref="NextMessage(CancellationToken)"/>.
/// </summary>
/// <param name="message">The <see cref="Message"/> to queue. A value of <see langword="null"/> indicates the channel mappings are out of date.</param>
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(
@@ -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
}
/// <inheritdoc />
[MemberNotNullWhen(true, nameof(nextDmbProvider))]
public bool DmbAvailable => nextDmbProvider != null;
/// <summary>
@@ -74,7 +77,7 @@ namespace Tgstation.Server.Host.Components.Deployment
/// <summary>
/// Map of <see cref="CompileJob.JobId"/>s to locks on them.
/// </summary>
readonly IDictionary<long, int> jobLockCounts;
readonly Dictionary<long, int> jobLockCounts;
/// <summary>
/// <see cref="TaskCompletionSource"/> resulting in the latest <see cref="DmbProvider"/> yet to exist.
@@ -89,7 +92,7 @@ namespace Tgstation.Server.Host.Components.Deployment
/// <summary>
/// The latest <see cref="DmbProvider"/>.
/// </summary>
IDmbProvider nextDmbProvider;
IDmbProvider? nextDmbProvider;
/// <summary>
/// If the <see cref="DmbFactory"/> is "started" via <see cref="IComponentService"/>.
@@ -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
/// <inheritdoc />
public async ValueTask LoadCompileJob(CompileJob job, Action<bool> activationAction, CancellationToken cancellationToken)
public async ValueTask LoadCompileJob(CompileJob job, Action<bool>? 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
/// <inheritdoc />
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
/// <inheritdoc />
#pragma warning disable CA1506 // TODO: Decomplexify
public async ValueTask<IDmbProvider> FromCompileJob(CompileJob compileJob, CancellationToken cancellationToken)
public async ValueTask<IDmbProvider?> 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<string> jobUidsToNotErase = null;
List<string>? 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<string>();
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
/// <inheritdoc />
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);
}
}
/// <summary>
@@ -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
{
/// <inheritdoc />
public override string Directory => ioManager.ResolvePath(CompileJob.DirectoryName.ToString() + directoryAppend);
public override string Directory => ioManager.ResolvePath(CompileJob.DirectoryName!.Value.ToString() + directoryAppend);
/// <inheritdoc />
public override Models.CompileJob CompileJob { get; }
@@ -32,7 +33,7 @@ namespace Tgstation.Server.Host.Components.Deployment
/// <summary>
/// The <see cref="Action"/> to run when <see cref="DisposeAsync"/> is called.
/// </summary>
Action onDispose;
DisposeInvoker? onDispose;
/// <summary>
/// Initializes a new instance of the <see cref="DmbProvider"/> class.
@@ -42,7 +43,7 @@ namespace Tgstation.Server.Host.Components.Deployment
/// <param name="ioManager">The value of <see cref="ioManager"/>.</param>
/// <param name="onDispose">The value of <see cref="onDispose"/>.</param>
/// <param name="directoryAppend">The optional value of <see cref="directoryAppend"/>.</param>
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
/// <inheritdoc />
public override ValueTask DisposeAsync()
{
onDispose?.Invoke();
onDispose?.Dispose();
return ValueTask.CompletedTask;
}
@@ -13,11 +13,11 @@ namespace Tgstation.Server.Host.Components.Deployment
/// <inheritdoc />
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}"),
});
/// <inheritdoc />
@@ -113,12 +113,12 @@ namespace Tgstation.Server.Host.Components.Deployment
/// <summary>
/// The active callback from <see cref="IChatManager.QueueDeploymentMessage"/>.
/// </summary>
Func<string, string, Action<bool>> currentChatCallback;
Func<string?, string, Action<bool>>? currentChatCallback;
/// <summary>
/// Cached for <see cref="currentChatCallback"/>.
/// </summary>
string currentDreamMakerOutput;
string? currentDreamMakerOutput;
/// <summary>
/// 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
/// <summary>
/// Run the compile implementation.
/// </summary>
/// <param name="job">The currently running <see cref="Job"/>.</param>
/// <param name="revisionInformation">The <see cref="RevisionInformation"/>.</param>
/// <param name="dreamMakerSettings">The <see cref="Api.Models.Internal.DreamMakerSettings"/>.</param>
/// <param name="launchParameters">The <see cref="DreamDaemonLaunchParameters"/>.</param>
@@ -456,6 +458,7 @@ namespace Tgstation.Server.Host.Components.Deployment
/// <param name="cancellationToken">The <see cref="CancellationToken"/> for the operation.</param>
/// <returns>A <see cref="ValueTask{TResult}"/> resulting in the completed <see cref="CompileJob"/>.</returns>
async ValueTask<Models.CompileJob> 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);
}
}
}
@@ -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
/// <param name="cancellationToken">The <see cref="CancellationToken"/> for the operation.</param>
/// <returns>A <see cref="IEnumerable{T}"/> of <see cref="Task"/>s representing the running operations. The first <see cref="Task"/> returned is always the necessary call to <see cref="IIOManager.CreateDirectory(string, CancellationToken)"/>.</returns>
/// <remarks>I genuinely don't know how this will work with symlinked files. Waiting for the issue report I guess.</remarks>
IEnumerable<Task> MirrorDirectoryImpl(string src, string dest, SemaphoreSlim semaphore, CancellationToken cancellationToken)
IEnumerable<Task> 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);
@@ -15,9 +15,9 @@ namespace Tgstation.Server.Host.Components.Deployment
/// Load a new <paramref name="job"/> into the <see cref="ICompileJobSink"/>.
/// </summary>
/// <param name="job">The <see cref="CompileJob"/> to load.</param>
/// <param name="activationAction">An <see cref="Action{T1}"/> to be called when the <see cref="CompileJob"/> becomes active or is discarded with <see langword="true"/> or <see langword="false"/> respectively.</param>
/// <param name="activationAction">An optional <see cref="Action{T1}"/> to be called when the <see cref="CompileJob"/> becomes active or is discarded with <see langword="true"/> or <see langword="false"/> respectively.</param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> for the operation.</param>
/// <returns>A <see cref="ValueTask"/> representing the running operation.</returns>
ValueTask LoadCompileJob(CompileJob job, Action<bool> activationAction, CancellationToken cancellationToken);
ValueTask LoadCompileJob(CompileJob job, Action<bool>? activationAction, CancellationToken cancellationToken);
}
}
@@ -23,7 +23,7 @@ namespace Tgstation.Server.Host.Components.Deployment
bool DmbAvailable { get; }
/// <summary>
/// Gets the next <see cref="IDmbProvider"/>.
/// Gets the next <see cref="IDmbProvider"/>. <see cref="DmbAvailable"/> is a precondition.
/// </summary>
/// <param name="lockCount">The amount of locks to give the resulting <see cref="IDmbProvider"/>. It's <see cref="IDisposable.Dispose"/> must be called this many times to properly clean the job.</param>
/// <returns>A new <see cref="IDmbProvider"/>.</returns>
@@ -35,7 +35,7 @@ namespace Tgstation.Server.Host.Components.Deployment
/// <param name="compileJob">The <see cref="CompileJob"/> to make the <see cref="IDmbProvider"/> for.</param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> for the operation.</param>
/// <returns>A <see cref="ValueTask{TResult}"/> resulting in a new <see cref="IDmbProvider"/> representing the <see cref="CompileJob"/> on success, <see langword="null"/> on failure.</returns>
ValueTask<IDmbProvider> FromCompileJob(CompileJob compileJob, CancellationToken cancellationToken);
ValueTask<IDmbProvider?> FromCompileJob(CompileJob compileJob, CancellationToken cancellationToken);
/// <summary>
/// Deletes all compile jobs that are inactive in the Game folder.
@@ -10,7 +10,7 @@ namespace Tgstation.Server.Host.Components.Deployment
/// <summary>
/// Gets the latest <see cref="CompileJob"/>.
/// </summary>
/// <returns>The latest <see cref="CompileJob"/>.</returns>
CompileJob LatestCompileJob();
/// <returns>The latest <see cref="CompileJob"/> or <see langword="null"/> if none are available.</returns>
CompileJob? LatestCompileJob();
}
}
@@ -52,18 +52,23 @@ namespace Tgstation.Server.Host.Components.Deployment.Remote
/// <inheritdoc />
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<ValueTask>(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);
/// <inheritdoc />
public ValueTask StageDeployment(CompileJob compileJob, Action<bool> activationCallback, CancellationToken cancellationToken)
public ValueTask StageDeployment(CompileJob compileJob, Action<bool>? 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);
}
@@ -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<string>(),
},
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<TestMerge>();
@@ -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<PullRequest> 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><summary>Details</summary>{0}{0}##### Server Instance{0}{5}{1}{0}{0}##### Revision{0}Origin: {6}{0}Pull Request: {2}{0}Server: {7}{3}{8}{0}</details>",
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);
/// <summary>
@@ -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
@@ -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<TestMerge>();
@@ -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<MergeRequest> 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
@@ -34,7 +34,7 @@ namespace Tgstation.Server.Host.Components.Deployment.Remote
/// <returns>A <see cref="ValueTask"/> representing the running operation.</returns>
ValueTask StageDeployment(
CompileJob compileJob,
Action<bool> activationCallback,
Action<bool>? activationCallback,
CancellationToken cancellationToken);
/// <summary>
@@ -66,18 +66,18 @@ namespace Tgstation.Server.Host.Components.Deployment.Remote
/// Post deployment comments to the test merge ticket.
/// </summary>
/// <param name="compileJob">The deployed <see cref="CompileJob"/>.</param>
/// <param name="previousRevisionInformation">The <see cref="RevisionInformation"/> of the previous deployment.</param>
/// <param name="previousRevisionInformation">The optional <see cref="RevisionInformation"/> of the previous deployment.</param>
/// <param name="repositorySettings">The <see cref="RepositorySettings"/>.</param>
/// <param name="repoOwner">The GitHub repostiory owner.</param>
/// <param name="repoName">The GitHub repostiory name.</param>
/// <param name="repoOwner">The remote repostiory owner.</param>
/// <param name="repoName">The remote repostiory name.</param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> for the operation.</param>
/// <returns>A <see cref="ValueTask"/> representing the running operation.</returns>
ValueTask PostDeploymentComments(
CompileJob compileJob,
RevisionInformation previousRevisionInformation,
RevisionInformation? previousRevisionInformation,
RepositorySettings repositorySettings,
string repoOwner,
string repoName,
string? repoOwner,
string? repoName,
CancellationToken cancellationToken);
/// <summary>
@@ -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<string, string> 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);
@@ -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
}
/// <inheritdoc />
public override async ValueTask<IEngineInstallationData> DownloadVersion(EngineVersion version, JobProgressReporter progressReporter, CancellationToken cancellationToken)
public override async ValueTask<IEngineInstallationData> 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 <see cref="Uri"/> pointing to the location of the download for a given <paramref name="version"/>.
/// </summary>
/// <param name="version">The <see cref="EngineVersion"/> to create a <see cref="Uri"/> for.</param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> for the operation.</param>
/// <returns>A <see cref="ValueTask{TResult}"/> resulting in a new <see cref="Uri"/> pointing to the version download location.</returns>
ValueTask<Uri> GetDownloadZipUrl(EngineVersion version, CancellationToken cancellationToken)
/// <returns>A <see cref="Uri"/> pointing to the version download location.</returns>
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);
}
}
}
@@ -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
{
/// <summary>
/// The <see cref="IReadOnlyDictionary{TKey, TValue}"/> mapping <see cref="EngineType"/>s to their appropriate <see cref="IEngineInstaller"/>.
/// The <see cref="FrozenDictionary{TKey, TValue}"/> mapping <see cref="EngineType"/>s to their appropriate <see cref="IEngineInstaller"/>.
/// </summary>
readonly IReadOnlyDictionary<EngineType, IEngineInstaller> delegatedInstallers;
readonly FrozenDictionary<EngineType, IEngineInstaller> delegatedInstallers;
/// <summary>
/// Initializes a new instance of the <see cref="DelegatingEngineInstaller"/> class.
/// </summary>
/// <param name="delegatedInstallers">The value of <see cref="delegatedInstallers"/>.</param>
public DelegatingEngineInstaller(IReadOnlyDictionary<EngineType, IEngineInstaller> delegatedInstallers)
public DelegatingEngineInstaller(FrozenDictionary<EngineType, IEngineInstaller> 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));
/// <inheritdoc />
public ValueTask<IEngineInstallationData> DownloadVersion(EngineVersion version, JobProgressReporter jobProgressReporter, CancellationToken cancellationToken)
public ValueTask<IEngineInstallationData> DownloadVersion(EngineVersion version, JobProgressReporter? jobProgressReporter, CancellationToken cancellationToken)
=> DelegateCall(version, installer => installer.DownloadVersion(version, jobProgressReporter, cancellationToken));
/// <inheritdoc />
@@ -62,7 +62,7 @@ namespace Tgstation.Server.Host.Components.Engine
TReturn DelegateCall<TReturn>(EngineVersion version, Func<IEngineInstaller, TReturn> call)
{
ArgumentNullException.ThrowIfNull(version);
return call(delegatedInstallers[version.Engine.Value]);
return call(delegatedInstallers[version.Engine!.Value]);
}
}
}
@@ -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
{
/// <inheritdoc cref="IEngineExecutableLock" />
sealed class EngineExecutableLock : ReferenceCounter<IEngineInstallation>, IEngineExecutableLock
class EngineExecutableLock : ReferenceCounter<IEngineInstallation>, IEngineExecutableLock
{
/// <inheritdoc />
public EngineVersion Version => Instance.Version;
@@ -40,7 +44,7 @@ namespace Tgstation.Server.Host.Components.Engine
IDmbProvider dmbProvider,
IReadOnlyDictionary<string, string> parameters,
DreamDaemonLaunchParameters launchParameters,
string logFilePath)
string? logFilePath)
=> Instance.FormatServerArguments(
dmbProvider,
parameters,
@@ -49,5 +53,14 @@ namespace Tgstation.Server.Host.Components.Engine
/// <inheritdoc />
public string FormatCompilerArguments(string dmePath) => Instance.FormatCompilerArguments(dmePath);
/// <inheritdoc />
public ValueTask StopServerProcess(ILogger logger, IProcess process, string accessIdentifier, ushort port, CancellationToken cancellationToken)
=> Instance.StopServerProcess(
logger,
process,
accessIdentifier,
port,
cancellationToken);
}
}
@@ -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);
/// <inheritdoc />
public abstract string FormatServerArguments(IDmbProvider dmbProvider, IReadOnlyDictionary<string, string> parameters, DreamDaemonLaunchParameters launchParameters, string logFilePath);
public abstract string FormatServerArguments(
IDmbProvider dmbProvider,
IReadOnlyDictionary<string, string> parameters,
DreamDaemonLaunchParameters launchParameters,
string? logFilePath);
/// <inheritdoc />
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;
}
}
}
@@ -52,7 +52,7 @@ namespace Tgstation.Server.Host.Components.Engine
public abstract ValueTask UpgradeInstallation(EngineVersion version, string path, CancellationToken cancellationToken);
/// <inheritdoc />
public abstract ValueTask<IEngineInstallationData> DownloadVersion(EngineVersion version, JobProgressReporter jobProgressReporter, CancellationToken cancellationToken);
public abstract ValueTask<IEngineInstallationData> DownloadVersion(EngineVersion version, JobProgressReporter? jobProgressReporter, CancellationToken cancellationToken);
/// <inheritdoc />
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}");
}
}
@@ -32,7 +32,7 @@ namespace Tgstation.Server.Host.Components.Engine
const string ActiveVersionFileName = "ActiveVersion.txt";
/// <inheritdoc />
public EngineVersion ActiveVersion { get; private set; }
public EngineVersion? ActiveVersion { get; private set; }
/// <inheritdoc />
public IReadOnlyList<EngineVersion> InstalledVersions
@@ -118,9 +118,9 @@ namespace Tgstation.Server.Host.Components.Engine
/// <inheritdoc />
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<string>
new List<string?>
{
ActiveVersion?.ToString(),
stringVersion,
@@ -160,7 +160,7 @@ namespace Tgstation.Server.Host.Components.Engine
}
/// <inheritdoc />
public async ValueTask<IEngineExecutableLock> UseExecutables(EngineVersion requiredVersion, string trustDmbFullPath, CancellationToken cancellationToken)
public async ValueTask<IEngineExecutableLock> 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<IEngineInstallation, EngineExecutableLock> 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
/// <inheritdoc />
public async Task StartAsync(CancellationToken cancellationToken)
{
async ValueTask<byte[]> GetActiveVersion()
async ValueTask<byte[]?> 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
/// </summary>
/// <param name="progressReporter">The optional <see cref="JobProgressReporter"/> for the operation.</param>
/// <param name="version">The <see cref="EngineVersion"/> to install.</param>
/// <param name="customVersionStream">Custom zip file <see cref="Stream"/> to use. Will cause a <see cref="Version.Build"/> number to be added.</param>
/// <param name="customVersionStream">Optional custom zip file <see cref="Stream"/> to use. Will cause a <see cref="Version.Build"/> number to be added.</param>
/// <param name="neededForLock">If this BYOND version is required as part of a locking operation.</param>
/// <param name="allowInstallation">If an installation should be performed if the <paramref name="version"/> is not installed. If <see langword="false"/> and an installation is required an <see cref="InvalidOperationException"/> will be thrown.</param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> for the operation.</param>
/// <returns>A <see cref="ValueTask{TResult}"/> resulting in the <see cref="EngineExecutableLock"/>.</returns>
async ValueTask<EngineExecutableLock> 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<IEngineInstallation, EngineExecutableLock> 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
/// <param name="customVersionStream">Custom zip file <see cref="Stream"/> to use. Will cause a <see cref="Version.Build"/> number to be added.</param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> for the operation.</param>
/// <returns>A <see cref="ValueTask"/> representing the running operation.</returns>
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
@@ -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 <paramref name="launchParameters"/>.
/// </summary>
/// <param name="dmbProvider">The <see cref="IDmbProvider"/>.</param>
/// <param name="parameters">The map of parameter <see cref="string"/>s as a <see cref="IReadOnlyDictionary{TKey, TValue}"/>. Should NOT include the <see cref="DreamDaemonLaunchParameters.AdditionalParameters"/> of <paramref name="launchParameters"/>.</param>
/// <param name="parameters">The map of parameter <see cref="string"/>s as a <see cref="IReadOnlyDictionary{TKey, TValue}"/>. MUST include <see cref="Interop.DMApiConstants.ParamAccessIdentifier"/>. Should NOT include the <see cref="DreamDaemonLaunchParameters.AdditionalParameters"/> of <paramref name="launchParameters"/>.</param>
/// <param name="launchParameters">The <see cref="DreamDaemonLaunchParameters"/>.</param>
/// <param name="logFilePath">The full path to the log file, if any.</param>
/// <returns>The formatted arguments <see cref="string"/>.</returns>
@@ -59,7 +63,7 @@ namespace Tgstation.Server.Host.Components.Engine
IDmbProvider dmbProvider,
IReadOnlyDictionary<string, string> parameters,
DreamDaemonLaunchParameters launchParameters,
string logFilePath);
string? logFilePath);
/// <summary>
/// Return the command line arguments for compiling a given <paramref name="dmePath"/> if compilation is necessary.
@@ -67,5 +71,16 @@ namespace Tgstation.Server.Host.Components.Engine
/// <param name="dmePath">The full path to the .dme to compile.</param>
/// <returns>The formatted arguments <see cref="string"/>.</returns>
string FormatCompilerArguments(string dmePath);
/// <summary>
/// Kills a given engine server <paramref name="process"/>.
/// </summary>
/// <param name="logger">The <see cref="ILogger"/> to write to.</param>
/// <param name="process">The <see cref="IProcess"/> to be terminated.</param>
/// <param name="accessIdentifier">The <see cref="Interop.DMApiParameters.AccessIdentifier"/> of the session.</param>
/// <param name="port">The port the server is running on.</param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> for the operation.</param>
/// <returns>A <see cref="ValueTask"/> representing the running operation.</returns>
ValueTask StopServerProcess(ILogger logger, IProcess process, string accessIdentifier, ushort port, CancellationToken cancellationToken);
}
}
@@ -27,7 +27,7 @@ namespace Tgstation.Server.Host.Components.Engine
/// <param name="jobProgressReporter">The optional <see cref="JobProgressReporter"/> for the operation.</param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> for the operation.</param>
/// <returns>A <see cref="ValueTask{TResult}"/> resulting in the <see cref="IEngineInstallationData"/> for the download.</returns>
ValueTask<IEngineInstallationData> DownloadVersion(EngineVersion version, JobProgressReporter jobProgressReporter, CancellationToken cancellationToken);
ValueTask<IEngineInstallationData> DownloadVersion(EngineVersion version, JobProgressReporter? jobProgressReporter, CancellationToken cancellationToken);
/// <summary>
/// Does actions necessary to get an extracted installation working.
@@ -18,7 +18,7 @@ namespace Tgstation.Server.Host.Components.Engine
/// <summary>
/// The currently active <see cref="EngineVersion"/>.
/// </summary>
EngineVersion ActiveVersion { get; }
EngineVersion? ActiveVersion { get; }
/// <summary>
/// The installed <see cref="EngineVersion"/>s.
@@ -35,9 +35,9 @@ namespace Tgstation.Server.Host.Components.Engine
/// <param name="cancellationToken">The <see cref="CancellationToken"/> for the operation.</param>
/// <returns>A <see cref="ValueTask"/> representing the running operation.</returns>
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
/// <param name="cancellationToken">The <see cref="CancellationToken"/> for the operation.</param>
/// <returns>A <see cref="ValueTask{TResult}"/> resulting in the requested <see cref="IEngineExecutableLock"/>.</returns>
ValueTask<IEngineExecutableLock> UseExecutables(
EngineVersion requiredVersion,
string trustDmbFullPath,
EngineVersion? requiredVersion,
string? trustDmbFullPath,
CancellationToken cancellationToken);
}
}
@@ -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
/// </summary>
readonly IIOManager ioManager;
/// <summary>
/// The <see cref="IAsyncDelayer"/> for the <see cref="OpenDreamInstallation"/>.
/// </summary>
readonly IAsyncDelayer asyncDelayer;
/// <summary>
/// The <see cref="IAbstractHttpClientFactory"/> for the <see cref="OpenDreamInstallation"/>.
/// </summary>
readonly IAbstractHttpClientFactory httpClientFactory;
/// <summary>
/// Initializes a new instance of the <see cref="OpenDreamInstallation"/> class.
/// </summary>
/// <param name="ioManager">The value of <see cref="ioManager"/>.</param>
/// <param name="asyncDelayer">The value of <see cref="asyncDelayer"/>.</param>
/// <param name="httpClientFactory">The value of <see cref="httpClientFactory"/>.</param>
/// <param name="serverExePath">The value of <see cref="ServerExePath"/>.</param>
/// <param name="compilerExePath">The value of <see cref="CompilerExePath"/>.</param>
/// <param name="installationTask">The value of <see cref="InstallationTask"/>.</param>
/// <param name="version">The value of <see cref="Version"/>.</param>
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<string, string> 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;
}
/// <inheritdoc />
public override string FormatCompilerArguments(string dmePath)
=> $"--suppress-unimplemented --notices-enabled \"{dmePath ?? throw new ArgumentNullException(nameof(dmePath))}\"";
/// <inheritdoc />
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);
}
}
}
@@ -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
/// </summary>
protected IProcessExecutor ProcessExecutor { get; }
/// <summary>
/// The <see cref="GeneralConfiguration"/> for the <see cref="OpenDreamInstaller"/>.
/// </summary>
protected GeneralConfiguration GeneralConfiguration { get; }
/// <summary>
/// The <see cref="Configuration.SessionConfiguration"/> for the <see cref="OpenDreamInstaller"/>.
/// </summary>
protected SessionConfiguration SessionConfiguration { get; }
/// <summary>
/// The <see cref="IPlatformIdentifier"/> for the <see cref="OpenDreamInstaller"/>.
/// </summary>
@@ -61,9 +73,14 @@ namespace Tgstation.Server.Host.Components.Engine
readonly IRepositoryManager repositoryManager;
/// <summary>
/// The <see cref="GeneralConfiguration"/> for the <see cref="OpenDreamInstaller"/>.
/// The <see cref="IAsyncDelayer"/> for the <see cref="OpenDreamInstaller"/>.
/// </summary>
protected GeneralConfiguration GeneralConfiguration { get; }
readonly IAsyncDelayer asyncDelayer;
/// <summary>
/// The <see cref="IAbstractHttpClientFactory"/> for the <see cref="OpenDreamInstaller"/>.
/// </summary>
readonly IAbstractHttpClientFactory httpClientFactory;
/// <summary>
/// Initializes a new instance of the <see cref="OpenDreamInstaller"/> class.
@@ -73,20 +90,29 @@ namespace Tgstation.Server.Host.Components.Engine
/// <param name="platformIdentifier">The value of <see cref="platformIdentifier"/>.</param>
/// <param name="processExecutor">The value of <see cref="ProcessExecutor"/>.</param>
/// <param name="repositoryManager">The value of <see cref="repositoryManager"/>.</param>
/// <param name="asyncDelayer">The value of <see cref="asyncDelayer"/>.</param>
/// <param name="httpClientFactory">The value of <see cref="httpClientFactory"/>.</param>
/// <param name="generalConfigurationOptions">The <see cref="IOptions{TOptions}"/> containing value of <see cref="GeneralConfiguration"/>.</param>
/// <param name="sessionConfigurationOptions">The <see cref="IOptions{TOptions}"/> containing value of <see cref="SessionConfiguration"/>.</param>
public OpenDreamInstaller(
IIOManager ioManager,
ILogger<OpenDreamInstaller> logger,
IPlatformIdentifier platformIdentifier,
IProcessExecutor processExecutor,
IRepositoryManager repositoryManager,
IOptions<GeneralConfiguration> generalConfigurationOptions)
IAsyncDelayer asyncDelayer,
IAbstractHttpClientFactory httpClientFactory,
IOptions<GeneralConfiguration> generalConfigurationOptions,
IOptions<SessionConfiguration> 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));
}
/// <inheritdoc />
@@ -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
}
/// <inheritdoc />
public override async ValueTask<IEngineInstallationData> DownloadVersion(EngineVersion version, JobProgressReporter jobProgressReporter, CancellationToken cancellationToken)
public override async ValueTask<IEngineInstallationData> 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);
@@ -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(
@@ -71,6 +71,11 @@ namespace Tgstation.Server.Host.Components.Engine
/// </summary>
readonly GeneralConfiguration generalConfiguration;
/// <summary>
/// The <see cref="SessionConfiguration"/> for the <see cref="WindowsByondInstaller"/>.
/// </summary>
readonly SessionConfiguration sessionConfiguration;
/// <summary>
/// The <see cref="SemaphoreSlim"/> for the <see cref="WindowsByondInstaller"/>.
/// </summary>
@@ -86,6 +91,7 @@ namespace Tgstation.Server.Host.Components.Engine
/// </summary>
/// <param name="processExecutor">The value of <see cref="processExecutor"/>.</param>
/// <param name="generalConfigurationOptions">The <see cref="IOptions{TOptions}"/> containing the value of <see cref="generalConfiguration"/>.</param>
/// <param name="sessionConfigurationOptions">The <see cref="IOptions{TOptions}"/> containing the value of <see cref="sessionConfiguration"/>.</param>
/// <param name="ioManager">The <see cref="IIOManager"/> for the <see cref="ByondInstallerBase"/>.</param>
/// <param name="fileDownloader">The <see cref="IFileDownloader"/> for the <see cref="ByondInstallerBase"/>.</param>
/// <param name="logger">The <see cref="ILogger"/> for the <see cref="ByondInstallerBase"/>.</param>
@@ -94,11 +100,13 @@ namespace Tgstation.Server.Host.Components.Engine
IIOManager ioManager,
IFileDownloader fileDownloader,
IOptions<GeneralConfiguration> generalConfigurationOptions,
IOptions<SessionConfiguration> sessionConfigurationOptions,
ILogger<WindowsByondInstaller> 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<ValueTask>(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
/// <returns>A <see cref="ValueTask"/> representing the running operation.</returns>
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)
@@ -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
/// <param name="platformIdentifier">The <see cref="IPlatformIdentifier"/> for the <see cref="OpenDreamInstaller"/>.</param>
/// <param name="processExecutor">The <see cref="IProcessExecutor"/> for the <see cref="OpenDreamInstaller"/>.</param>
/// <param name="repositoryManager">The <see cref="IRepositoryManager"/> for the <see cref="OpenDreamInstaller"/>.</param>
/// <param name="asyncDelayer">The <see cref="IAsyncDelayer"/> for the <see cref="OpenDreamInstaller"/>.</param>
/// <param name="httpClientFactory">The <see cref="IAbstractHttpClientFactory"/> for the <see cref="OpenDreamInstaller"/>.</param>
/// <param name="generalConfigurationOptions">The <see cref="IOptions{TOptions}"/> of <see cref="GeneralConfiguration"/> for the <see cref="OpenDreamInstaller"/>.</param>
/// <param name="sessionConfigurationOptions">The <see cref="IOptions{TOptions}"/> of <see cref="SessionConfiguration"/> for the <see cref="OpenDreamInstaller"/>.</param>
/// <param name="linkFactory">The value of <see cref="linkFactory"/>.</param>
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<GeneralConfiguration> generalConfigurationOptions,
IOptions<SessionConfiguration> 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)
@@ -19,7 +19,7 @@ namespace Tgstation.Server.Host.Components.Events
/// <summary>
/// The <see cref="IWatchdog"/> for the <see cref="EventConsumer"/>.
/// </summary>
IWatchdog watchdog;
IWatchdog? watchdog;
/// <summary>
/// Initializes a new instance of the <see cref="EventConsumer"/> class.
@@ -31,7 +31,7 @@ namespace Tgstation.Server.Host.Components.Events
}
/// <inheritdoc />
public async ValueTask HandleEvent(EventType eventType, IEnumerable<string> parameters, bool deploymentPipeline, CancellationToken cancellationToken)
public async ValueTask HandleEvent(EventType eventType, IEnumerable<string?> 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;
}
}
}
@@ -17,6 +17,6 @@ namespace Tgstation.Server.Host.Components.Events
/// <param name="deploymentPipeline">If this event is part of the deployment pipeline.</param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> for the operation.</param>
/// <returns>A <see cref="ValueTask"/> representing the running operation.</returns>
ValueTask HandleEvent(EventType eventType, IEnumerable<string> parameters, bool deploymentPipeline, CancellationToken cancellationToken);
ValueTask HandleEvent(EventType eventType, IEnumerable<string?> parameters, bool deploymentPipeline, CancellationToken cancellationToken);
}
}
@@ -10,7 +10,7 @@ namespace Tgstation.Server.Host.Components.Events
sealed class NoopEventConsumer : IEventConsumer
{
/// <inheritdoc />
public ValueTask HandleEvent(EventType eventType, IEnumerable<string> parameters, bool deploymentPipeline, CancellationToken cancellationToken)
public ValueTask HandleEvent(EventType eventType, IEnumerable<string?> parameters, bool deploymentPipeline, CancellationToken cancellationToken)
=> ValueTask.CompletedTask;
}
}
@@ -10,6 +10,6 @@
/// </summary>
/// <param name="instance">The <see cref="Instance"/> to get the <see cref="IInstanceCore"/> for.</param>
/// <returns>The <see cref="IInstanceCore"/> if it is online, <see langword="null"/> otherwise.</returns>
IInstanceCore GetInstance(Models.Instance instance);
IInstanceCore? GetInstance(Models.Instance instance);
}
}
@@ -19,6 +19,6 @@ namespace Tgstation.Server.Host.Components
/// </summary>
/// <param name="metadata">The <see cref="Models.Instance"/> of the desired <see cref="IInstance"/>.</param>
/// <returns>The <see cref="IInstance"/> associated with the given <paramref name="metadata"/> if it is online, <see langword="null"/> otherwise.</returns>
IInstanceReference GetInstanceReference(Api.Models.Instance metadata);
IInstanceReference? GetInstanceReference(Api.Models.Instance metadata);
}
}
@@ -93,12 +93,12 @@ namespace Tgstation.Server.Host.Components
/// <summary>
/// The auto update <see cref="Task"/>.
/// </summary>
Task timerTask;
Task? timerTask;
/// <summary>
/// <see cref="CancellationTokenSource"/> for <see cref="timerTask"/>.
/// </summary>
CancellationTokenSource timerCts;
CancellationTokenSource? timerCts;
/// <summary>
/// Initializes a new instance of the <see cref="Instance"/> 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
}
/// <inheritdoc />
public CompileJob LatestCompileJob() => dmbFactory.LatestCompileJob();
public CompileJob? LatestCompileJob() => dmbFactory.LatestCompileJob();
/// <summary>
/// The <see cref="JobEntrypoint"/> for updating the repository.
@@ -266,7 +266,7 @@ namespace Tgstation.Server.Host.Components
/// <returns>A <see cref="ValueTask"/> representing the running operation.</returns>
#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<TestMerge> updatedTestMerges)
RevisionInformation? currentRevInfo = null;
Models.Instance? attachedInstance = null;
async ValueTask UpdateRevInfo(string currentHead, bool onOrigin, IEnumerable<TestMerge>? 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)
{
@@ -150,7 +150,7 @@ namespace Tgstation.Server.Host.Components
/// </summary>
/// <param name="instanceIOManager">The instance's <see cref="IIOManager"/>.</param>
/// <returns>The <see cref="IIOManager"/> for the instance's "Game" directory.</returns>
static IIOManager CreateGameIOManager(IIOManager instanceIOManager) => new ResolvingIOManager(instanceIOManager, "Game");
static ResolvingIOManager CreateGameIOManager(IIOManager instanceIOManager) => new(instanceIOManager, "Game");
#pragma warning disable CA1502 // TODO: Decomplexify
/// <summary>
@@ -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
/// </summary>
/// <param name="metadata">The <see cref="Models.Instance"/>.</param>
/// <returns>The <see cref="IIOManager"/> for the <paramref name="metadata"/>.</returns>
IIOManager CreateInstanceIOManager(Models.Instance metadata) => new ResolvingIOManager(ioManager, metadata.Path);
ResolvingIOManager CreateInstanceIOManager(Models.Instance metadata) => new(ioManager, metadata.Path!);
}
}
@@ -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<string, IBridgeHandler> bridgeHandlers;
/// <summary>
/// <see cref="SemaphoreSlim"/> used to guard calls to <see cref="OnlineInstance(Models.Instance, CancellationToken)"/> and <see cref="OfflineInstance(Models.Instance, Models.User, CancellationToken)"/>.
/// <see cref="SemaphoreSlim"/> used to guard calls to <see cref="OnlineInstance(Models.Instance, CancellationToken)"/> and <see cref="OfflineInstance(Models.Instance, User, CancellationToken)"/>.
/// </summary>
readonly SemaphoreSlim instanceStateChangeSemaphore;
@@ -147,12 +148,12 @@ namespace Tgstation.Server.Host.Components
/// <summary>
/// The original <see cref="IConsole.Title"/> of <see cref="console"/>.
/// </summary>
readonly string originalConsoleTitle;
readonly string? originalConsoleTitle;
/// <summary>
/// The <see cref="Task"/> returned by <see cref="Initialize(CancellationToken)"/>.
/// </summary>
Task startupTask;
Task? startupTask;
/// <summary>
/// If the <see cref="InstanceManager"/> has been <see cref="DisposeAsync"/>'d.
@@ -241,13 +242,13 @@ namespace Tgstation.Server.Host.Components
}
/// <inheritdoc />
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
}
/// <inheritdoc />
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<IInstance, InstanceWrapper> container;
ReferenceCountingContainer<IInstance, InstanceWrapper>? 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<Models.Job>>();
ValueTask<Job?[]> 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<IInstance, InstanceWrapper>(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
}
/// <inheritdoc />
public async ValueTask<BridgeResponse> ProcessBridgeRequest(BridgeParameters parameters, CancellationToken cancellationToken)
public async ValueTask<BridgeResponse?> 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
}
/// <inheritdoc />
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<Models.Instance> dbInstances = null;
List<Models.Instance>? 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)
{
@@ -58,6 +58,6 @@ namespace Tgstation.Server.Host.Components
public ValueTask SetAutoUpdateInterval(uint newInterval) => Instance.SetAutoUpdateInterval(newInterval);
/// <inheritdoc />
public CompileJob LatestCompileJob() => Instance.LatestCompileJob();
public CompileJob? LatestCompileJob() => Instance.LatestCompileJob();
}
}
@@ -24,12 +24,12 @@ namespace Tgstation.Server.Host.Components.Interop.Bridge
/// <summary>
/// The DMAPI <see cref="global::System.Version"/> for <see cref="BridgeCommandType.Startup"/> requests.
/// </summary>
public Version Version { get; set; }
public Version? Version { get; set; }
/// <summary>
/// The DMAPI <see cref="CustomCommand"/>s for <see cref="BridgeCommandType.Startup"/> requests.
/// </summary>
public ICollection<CustomCommand> CustomCommands { get; set; }
public ICollection<CustomCommand>? CustomCommands { get; set; }
/// <summary>
/// The minimum required <see cref="DreamDaemonSecurity"/> level for <see cref="BridgeCommandType.Startup"/> requests.
@@ -39,16 +39,25 @@ namespace Tgstation.Server.Host.Components.Interop.Bridge
/// <summary>
/// The <see cref="Interop.ChatMessage"/> for <see cref="BridgeCommandType.ChatSend"/> requests.
/// </summary>
public ChatMessage ChatMessage { get; set; }
public ChatMessage? ChatMessage { get; set; }
/// <summary>
/// The <see cref="ChunkData"/> for <see cref="BridgeCommandType.Chunk"/> requests.
/// </summary>
public ChunkData Chunk { get; set; }
public ChunkData? Chunk { get; set; }
/// <summary>
/// The port that should be used to send world topics, if not the default.
/// </summary>
public ushort? TopicPort { get; set; }
/// <summary>
/// Initializes a new instance of the <see cref="BridgeParameters"/> class.
/// </summary>
/// <param name="accessIdentifier">The access identifier for the <see cref="DMApiParameters"/>.</param>
public BridgeParameters(string accessIdentifier)
: base(accessIdentifier)
{
}
}
}
@@ -1,38 +1,19 @@
using System;
using Tgstation.Server.Host.Utils;
namespace Tgstation.Server.Host.Components.Interop.Bridge
{
/// <inheritdoc />
sealed class BridgeRegistration : IBridgeRegistration
sealed class BridgeRegistration : DisposeInvoker, IBridgeRegistration
{
/// <summary>
/// <see langword="lock"/> <see cref="object"/> for accessing <see cref="onDispose"/>.
/// </summary>
readonly object lockObject;
/// <summary>
/// <see cref="Action"/> to run when <see cref="Dispose"/>d.
/// </summary>
Action onDispose;
/// <summary>
/// Initializes a new instance of the <see cref="BridgeRegistration"/> class.
/// </summary>
/// <param name="onDispose">The value of <see cref="onDispose"/>.</param>
public BridgeRegistration(Action onDispose)
/// <param name="disposeAction">The <see cref="IDisposable.Dispose"/> action for the <see cref="DisposeInvoker"/>.</param>
public BridgeRegistration(Action disposeAction)
: base(disposeAction)
{
this.onDispose = onDispose ?? throw new ArgumentNullException(nameof(onDispose));
lockObject = new object();
}
/// <inheritdoc />
public void Dispose()
{
lock (lockObject)
{
onDispose?.Invoke();
onDispose = null;
}
}
}
}
@@ -15,11 +15,11 @@ namespace Tgstation.Server.Host.Components.Interop.Bridge
/// <summary>
/// The <see cref="Bridge.RuntimeInformation"/> for <see cref="BridgeCommandType.Startup"/> requests.
/// </summary>
public RuntimeInformation RuntimeInformation { get; set; }
public RuntimeInformation? RuntimeInformation { get; set; }
/// <summary>
/// The <see cref="ChunkData.SequenceId"/>s missing from a chunked request.
/// </summary>
public IReadOnlyCollection<uint> MissingChunks { get; set; }
public IReadOnlyCollection<uint>? MissingChunks { get; set; }
}
}
@@ -14,6 +14,6 @@ namespace Tgstation.Server.Host.Components.Interop.Bridge
/// <param name="parameters">The <see cref="BridgeParameters"/> to handle.</param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> for the operation.</param>
/// <returns>A <see cref="ValueTask{TResult}"/> resulting in the <see cref="BridgeResponse"/> for the request or <see langword="null"/> if the request could not be dispatched.</returns>
ValueTask<BridgeResponse> ProcessBridgeRequest(BridgeParameters parameters, CancellationToken cancellationToken);
ValueTask<BridgeResponse?> ProcessBridgeRequest(BridgeParameters parameters, CancellationToken cancellationToken);
}
}
@@ -91,11 +91,11 @@ namespace Tgstation.Server.Host.Components.Interop.Bridge
OriginCommitSha = dmbProvider.CompileJob.RevisionInformation.OriginCommitSha,
};
TestMerges = (IReadOnlyCollection<TestMergeInformation>)dmbProvider
TestMerges = (IReadOnlyCollection<TestMergeInformation>?)dmbProvider
.CompileJob
.RevisionInformation
.ActiveTestMerges?
.Select(x => x.TestMerge)
.ActiveTestMerges
?.Select(x => x.TestMerge)
.Select(x => new TestMergeInformation(x, Revision))
.ToList()
?? Array.Empty<TestMergeInformation>();
@@ -18,10 +18,10 @@ namespace Tgstation.Server.Host.Components.Interop.Bridge
/// <summary>
/// Backing field for <see cref="TargetCommitSha"/> needed to continue to support DMAPI 5.
/// </summary>
public string PullRequestRevision { get; set; }
public string? PullRequestRevision { get; set; }
/// <inheritdoc />
public override string TargetCommitSha
public override string? TargetCommitSha
{
get => PullRequestRevision;
set => PullRequestRevision = value;
@@ -10,63 +10,63 @@ namespace Tgstation.Server.Host.Components.Interop
/// <summary>
/// The title of the embed.
/// </summary>
public string Title { get; set; }
public string? Title { get; set; }
/// <summary>
/// The description of the embed.
/// </summary>
public string Description { get; set; }
public string? Description { get; set; }
/// <summary>
/// The URL of the embed.
/// </summary>
#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
/// <summary>
/// The ISO 8601 timestamp of the embed.
/// </summary>
public string Timestamp { get; set; }
public string? Timestamp { get; set; }
/// <summary>
/// The colour of the embed in the format hex "#AARRGGBB".
/// </summary>
public string Colour { get; set; }
public string? Colour { get; set; }
/// <summary>
/// The <see cref="ChatEmbedFooter"/>.
/// </summary>
public ChatEmbedFooter Footer { get; set; }
public ChatEmbedFooter? Footer { get; set; }
/// <summary>
/// The <see cref="ChatEmbedMedia"/> for an image.
/// </summary>
public ChatEmbedMedia Image { get; set; }
public ChatEmbedMedia? Image { get; set; }
/// <summary>
/// The <see cref="ChatEmbedMedia"/> for a thumbnail.
/// </summary>
public ChatEmbedMedia Thumbnail { get; set; }
public ChatEmbedMedia? Thumbnail { get; set; }
/// <summary>
/// The <see cref="ChatEmbedMedia"/> for a video.
/// </summary>
public ChatEmbedMedia Video { get; set; }
public ChatEmbedMedia? Video { get; set; }
/// <summary>
/// The <see cref="ChatEmbedProvider"/>.
/// </summary>
public ChatEmbedProvider Provider { get; set; }
public ChatEmbedProvider? Provider { get; set; }
/// <summary>
/// The <see cref="ChatEmbedAuthor"/>.
/// </summary>
public ChatEmbedAuthor Author { get; set; }
public ChatEmbedAuthor? Author { get; set; }
/// <summary>
/// The <see cref="ChatEmbedField"/>s.
/// </summary>
public ICollection<ChatEmbedField> Fields { get; set; }
public ICollection<ChatEmbedField>? Fields { get; set; }
}
}
@@ -9,12 +9,12 @@
/// Gets the icon URL of the author.
/// </summary>
#pragma warning disable CA1056 // Uri properties should not be strings
public string IconUrl { get; set; }
public string? IconUrl { get; set; }
/// <summary>
/// Gets the proxied icon URL of the thumbnail.
/// </summary>
public string ProxyIconUrl { get; set; }
public string? ProxyIconUrl { get; set; }
#pragma warning restore CA1056 // Uri properties should not be strings
}
}
@@ -8,12 +8,12 @@
/// <summary>
/// Gets the name of the field.
/// </summary>
public string Name { get; set; }
public string? Name { get; set; }
/// <summary>
/// Gets the value of the field.
/// </summary>
public string Value { get; set; }
public string? Value { get; set; }
/// <summary>
/// Gets a value indicating whether the field should display inline.
@@ -8,18 +8,18 @@
/// <summary>
/// Gets the text of the footer.
/// </summary>
public string Text { get; set; }
public string? Text { get; set; }
/// <summary>
/// Gets the URL of the footer icon. Only supports http(s) and attachments.
/// </summary>
#pragma warning disable CA1056 // Uri properties should not be strings
public string IconUrl { get; set; }
public string? IconUrl { get; set; }
/// <summary>
/// Gets the proxied icon URL.
/// </summary>
public string ProxyIconUrl { get; set; }
public string? ProxyIconUrl { get; set; }
#pragma warning restore CA1056 // Uri properties should not be strings
}
}
@@ -9,12 +9,12 @@
/// Gets the source URL of the media. Only supports http(s) and attachments.
/// </summary>
#pragma warning disable CA1056 // Uri properties should not be strings
public string Url { get; set; }
public string? Url { get; set; }
/// <summary>
/// Gets the proxied URL of the media.
/// </summary>
public string ProxyUrl { get; set; }
public string? ProxyUrl { get; set; }
#pragma warning restore CA1056 // Uri properties should not be strings
/// <summary>
@@ -8,13 +8,13 @@
/// <summary>
/// Gets the name of the provider.
/// </summary>
public string Name { get; set; }
public string? Name { get; set; }
/// <summary>
/// Gets the URL of the provider.
/// </summary>
#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
}
}
@@ -10,6 +10,6 @@ namespace Tgstation.Server.Host.Components.Interop
/// <summary>
/// The <see cref="ICollection{T}"/> of <see cref="Chat.ChannelRepresentation.Id"/>s to sent the <see cref="MessageContent"/> to. Must be safe to parse as <see cref="ulong"/>s.
/// </summary>
public ICollection<string> ChannelIds { get; set; }
public ICollection<string>? ChannelIds { get; set; }
}
}
@@ -14,6 +14,6 @@
/// <summary>
/// The partial JSON payload of the chunk.
/// </summary>
public string Payload { get; set; }
public string? Payload { get; set; }
}
}
@@ -14,6 +14,11 @@ namespace Tgstation.Server.Host.Components.Interop
/// </summary>
abstract class Chunker
{
/// <summary>
/// The <see cref="ILogger"/> for the <see cref="Chunker"/>.
/// </summary>
protected ILogger<Chunker> Logger { get; }
/// <summary>
/// Gets a payload ID for use in a new <see cref="ChunkSetInfo"/>.
/// </summary>
@@ -39,11 +44,6 @@ namespace Tgstation.Server.Host.Components.Interop
/// </summary>
uint highestSeenPayloadId;
/// <summary>
/// The <see cref="ILogger"/> for the <see cref="Chunker"/>.
/// </summary>
protected ILogger<Chunker> Logger { get; }
/// <summary>
/// Initializes a new instance of the <see cref="Chunker"/> class.
/// </summary>
@@ -64,11 +64,12 @@ namespace Tgstation.Server.Host.Components.Interop
/// <param name="chunk">The <see cref="ChunkData"/>.</param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> for the operation.</param>
/// <returns>A <see cref="ValueTask{TResult}"/> resulting in the <typeparamref name="TResponse"/> for the chunked request.</returns>
protected async ValueTask<TResponse> ProcessChunk<TCommunication, TResponse>(
Func<TCommunication, CancellationToken, ValueTask<TResponse>> completionCallback,
Func<string, TResponse> chunkErrorCallback,
ChunkData chunk,
protected async ValueTask<TResponse?> ProcessChunk<TCommunication, TResponse>(
Func<TCommunication, CancellationToken, ValueTask<TResponse?>> completionCallback,
Func<string, TResponse?> 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);
}
}
@@ -40,12 +40,12 @@ namespace Tgstation.Server.Host.Components.Interop
public const uint MaximumBridgeRequestLength = 8198;
/// <summary>
/// The maximum length in bytes of a <see cref="global::Byond.TopicSender.ITopicClient"/> payload.
/// The maximum length in bytes of a <see cref="Byond.TopicSender.ITopicClient"/> payload.
/// </summary>
public const uint MaximumTopicRequestLength = 65528;
/// <summary>
/// The maximum length in bytes of a <see cref="global::Byond.TopicSender.ITopicClient"/> response.
/// The maximum length in bytes of a <see cref="Byond.TopicSender.ITopicClient"/> response.
/// </summary>
public const uint MaximumTopicResponseLength = 65529;
@@ -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
/// </summary>
[Required]
public string AccessIdentifier { get; set; }
/// <summary>
/// Initializes a new instance of the <see cref="DMApiParameters"/> class.
/// </summary>
/// <param name="accessIdentifier">The value of <see cref="AccessIdentifier"/>.</param>
public DMApiParameters(string accessIdentifier)
{
AccessIdentifier = accessIdentifier ?? throw new ArgumentNullException(nameof(accessIdentifier));
}
/// <summary>
/// Initializes a new instance of the <see cref="DMApiParameters"/> class.
/// </summary>
/// <remarks>For use by EFCore only.</remarks>
protected DMApiParameters()
{
AccessIdentifier = null!;
}
}
}
@@ -8,6 +8,6 @@
/// <summary>
/// Any errors in the client's parameters.
/// </summary>
public string ErrorMessage { get; set; }
public string? ErrorMessage { get; set; }
}
}
@@ -10,6 +10,6 @@ namespace Tgstation.Server.Host.Components.Interop
/// <summary>
/// The <see cref="ChunkData.SequenceId"/>s missing from a chunked request.
/// </summary>
IReadOnlyCollection<uint> MissingChunks { get; set; }
IReadOnlyCollection<uint>? MissingChunks { get; set; }
}
}
@@ -8,11 +8,11 @@
/// <summary>
/// The message <see cref="string"/>.
/// </summary>
public string Text { get; set; }
public string? Text { get; set; }
/// <summary>
/// The <see cref="ChatEmbed"/>.
/// </summary>
public ChatEmbed Embed { get; set; }
public ChatEmbed? Embed { get; set; }
}
}
@@ -8,7 +8,7 @@ namespace Tgstation.Server.Host.Components.Interop.Topic
sealed class ChunkedTopicParameters : TopicParameters, IMissingPayloadsCommunication, IChunkPayloadId
{
/// <inheritdoc />
public IReadOnlyCollection<uint> MissingChunks { get; set; }
public IReadOnlyCollection<uint>? MissingChunks { get; set; }
/// <inheritdoc />
public uint? PayloadId { get; set; }
@@ -20,14 +20,14 @@ namespace Tgstation.Server.Host.Components.Interop.Topic
/// <summary>
/// The set of parameters.
/// </summary>
public IReadOnlyCollection<string> Parameters { get; }
public IReadOnlyCollection<string?> Parameters { get; }
/// <summary>
/// Initializes a new instance of the <see cref="EventNotification"/> class.
/// </summary>
/// <param name="eventType">The value of <see cref="Type"/>.</param>
/// <param name="parameters">The <see cref="IEnumerable{T}"/> that forms the value of <see cref="Parameters"/>.</param>
public EventNotification(EventType eventType, IEnumerable<string> parameters = null)
public EventNotification(EventType eventType, IEnumerable<string?> parameters)
{
Type = eventType;
Parameters = parameters?.ToList() ?? throw new ArgumentNullException(nameof(parameters));
@@ -20,12 +20,12 @@ namespace Tgstation.Server.Host.Components.Interop.Topic
/// <summary>
/// The <see cref="Topic.ChatCommand"/> for <see cref="TopicCommandType.ChatCommand"/> requests.
/// </summary>
public ChatCommand ChatCommand { get; }
public ChatCommand? ChatCommand { get; }
/// <summary>
/// The <see cref="Topic.EventNotification"/> for <see cref="TopicCommandType.EventNotification"/> requests.
/// </summary>
public EventNotification EventNotification { get; }
public EventNotification? EventNotification { get; }
/// <summary>
/// The new port for <see cref="TopicCommandType.ChangePort"/> or <see cref="TopicCommandType.ServerPortUpdate"/> requests.
@@ -40,27 +40,27 @@ namespace Tgstation.Server.Host.Components.Interop.Topic
/// <summary>
/// The new <see cref="Api.Models.NamedEntity.Name"/> for <see cref="TopicCommandType.InstanceRenamed"/> requests.
/// </summary>
public string NewInstanceName { get; }
public string? NewInstanceName { get; }
/// <summary>
/// The message to broadcast for <see cref="TopicCommandType.Broadcast"/> requests.
/// </summary>
public string BroadcastMessage { get; }
public string? BroadcastMessage { get; }
/// <summary>
/// The <see cref="Interop.ChatUpdate"/> for <see cref="TopicCommandType.ChatChannelsUpdate"/> requests.
/// </summary>
public ChatUpdate ChatUpdate { get; }
public ChatUpdate? ChatUpdate { get; }
/// <summary>
/// The new server <see cref="Version"/> after a reattach.
/// </summary>
public Version NewServerVersion { get; }
public Version? NewServerVersion { get; }
/// <summary>
/// The <see cref="ChunkData"/> for a partial request.
/// </summary>
public ChunkData Chunk { get; }
public ChunkData? Chunk { get; }
/// <summary>
/// Whether or not the <see cref="TopicParameters"/> constitute a priority request.
@@ -188,6 +188,7 @@ namespace Tgstation.Server.Host.Components.Interop.Topic
/// </summary>
/// <param name="commandType">The value of <see cref="CommandType"/>.</param>
protected TopicParameters(TopicCommandType commandType)
: base(String.Empty) // access identifier gets set before send
{
CommandType = commandType;
}
@@ -12,29 +12,29 @@ namespace Tgstation.Server.Host.Components.Interop.Topic
/// <summary>
/// The text to reply with as the result of a <see cref="TopicCommandType.ChatCommand"/> request, if any. Deprecated circa Interop 5.4.0.
/// </summary>
public string CommandResponseMessage { get; set; }
public string? CommandResponseMessage { get; set; }
/// <summary>
/// The <see cref="ChatMessage"/> response from a <see cref="ChatCommand"/>. Added in Interop 5.4.0.
/// </summary>
public ChatMessage CommandResponse { get; set; }
public ChatMessage? CommandResponse { get; set; }
/// <summary>
/// The <see cref="ChatMessage"/>s to send as the result of a <see cref="TopicCommandType.EventNotification"/> request, if any.
/// </summary>
public ICollection<ChatMessage> ChatResponses { get; set; }
public ICollection<ChatMessage>? ChatResponses { get; set; }
/// <summary>
/// The DMAPI <see cref="CustomCommand"/>s for <see cref="TopicCommandType.ServerRestarted"/> requests.
/// </summary>
public ICollection<CustomCommand> CustomCommands { get; set; }
public ICollection<CustomCommand>? CustomCommands { get; set; }
/// <summary>
/// The <see cref="ChunkData"/> for a partial response.
/// </summary>
public ChunkData Chunk { get; set; }
public ChunkData? Chunk { get; set; }
/// <inheritdoc />
public IReadOnlyCollection<uint> MissingChunks { get; set; }
public IReadOnlyCollection<uint>? MissingChunks { get; set; }
}
}
@@ -21,10 +21,10 @@ namespace Tgstation.Server.Host.Components.Repository
public RemoteGitProvider? RemoteGitProvider => Api.Models.RemoteGitProvider.Unknown;
/// <inheritdoc />
public string RemoteRepositoryOwner => null;
public string? RemoteRepositoryOwner => null;
/// <inheritdoc />
public string RemoteRepositoryName => null;
public string? RemoteRepositoryName => null;
/// <inheritdoc />
public ValueTask<Models.TestMerge> GetTestMerge(
@@ -25,12 +25,6 @@ namespace Tgstation.Server.Host.Components.Repository
/// <inheritdoc />
public override RemoteGitProvider? RemoteGitProvider => Api.Models.RemoteGitProvider.GitHub;
/// <inheritdoc />
public override string RemoteRepositoryOwner { get; }
/// <inheritdoc />
public override string RemoteRepositoryName { get; }
/// <summary>
/// The <see cref="IGitHubServiceFactory"/> for the <see cref="GitHubRemoteFeatures"/>.
/// </summary>
@@ -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];
}
/// <inheritdoc />
@@ -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);
@@ -30,12 +30,6 @@ namespace Tgstation.Server.Host.Components.Repository
/// <inheritdoc />
public override RemoteGitProvider? RemoteGitProvider => Api.Models.RemoteGitProvider.GitLab;
/// <inheritdoc />
public override string RemoteRepositoryOwner { get; }
/// <inheritdoc />
public override string RemoteRepositoryName { get; }
/// <summary>
/// Initializes a new instance of the <see cref="GitLabRemoteFeatures"/> class.
/// </summary>
@@ -44,10 +38,6 @@ namespace Tgstation.Server.Host.Components.Repository
public GitLabRemoteFeatures(ILogger<GitLabRemoteFeatures> 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];
}
/// <inheritdoc />
@@ -25,10 +25,10 @@ namespace Tgstation.Server.Host.Components.Repository
public abstract RemoteGitProvider? RemoteGitProvider { get; }
/// <inheritdoc />
public abstract string RemoteRepositoryOwner { get; }
public string RemoteRepositoryOwner { get; }
/// <inheritdoc />
public abstract string RemoteRepositoryName { get; }
public string RemoteRepositoryName { get; }
/// <summary>
/// The <see cref="ILogger"/> for the <see cref="GitRemoteFeaturesBase"/>.
@@ -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<TestMergeParameters, Models.TestMerge>();
}
@@ -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;
@@ -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;
}
}
@@ -16,7 +16,7 @@ namespace Tgstation.Server.Host.Components.Repository
/// <param name="username">The optional username to use in the <see cref="CredentialsHandler"/>.</param>
/// <param name="password">The optional password to use in the <see cref="CredentialsHandler"/>.</param>
/// <returns>A new <see cref="CredentialsHandler"/>.</returns>
CredentialsHandler GenerateCredentialsHandler(string username, string password);
CredentialsHandler GenerateCredentialsHandler(string? username, string? password);
/// <summary>
/// Rethrow the authentication failure message as a <see cref="JobException"/> if it is one.
@@ -44,18 +44,18 @@ namespace Tgstation.Server.Host.Components.Repository
/// Checks out a given <paramref name="committish"/>.
/// </summary>
/// <param name="committish">The sha or reference to checkout.</param>
/// <param name="username">The username used for fetching from submodule repositories.</param>
/// <param name="password">The password used for fetching from submodule repositories.</param>
/// <param name="username">The optional username used for fetching from submodule repositories.</param>
/// <param name="password">The optional password used for fetching from submodule repositories.</param>
/// <param name="updateSubmodules">If a submodule update should be attempted after the merge.</param>
/// <param name="progressReporter">The optional <see cref="JobProgressReporter"/> to report progress of the operation.</param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> for the operation.</param>
/// <returns>A <see cref="ValueTask"/> representing the running operation.</returns>
ValueTask CheckoutObject(
string committish,
string username,
string password,
string? username,
string? password,
bool updateSubmodules,
JobProgressReporter progressReporter,
JobProgressReporter? progressReporter,
CancellationToken cancellationToken);
/// <summary>
@@ -64,8 +64,8 @@ namespace Tgstation.Server.Host.Components.Repository
/// <param name="testMergeParameters">The <see cref="TestMergeParameters"/> of the pull request.</param>
/// <param name="committerName">The name of the merge committer.</param>
/// <param name="committerEmail">The e-mail of the merge committer.</param>
/// <param name="username">The username used to fetch from the origin and submodule repositories.</param>
/// <param name="password">The password used to fetch from the origin and submodule repositories.</param>
/// <param name="username">The optional username used to fetch from the origin and submodule repositories.</param>
/// <param name="password">The optional password used to fetch from the origin and submodule repositories.</param>
/// <param name="updateSubmodules">If a submodule update should be attempted after the merge.</param>
/// <param name="progressReporter">The <see cref="JobProgressReporter"/> to report progress of the operation.</param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> for the operation.</param>
@@ -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.
/// </summary>
/// <param name="progressReporter">The optional <see cref="JobProgressReporter"/> to report progress of the operation.</param>
/// <param name="username">The username to fetch from the origin repository.</param>
/// <param name="password">The password to fetch from the origin repository.</param>
/// <param name="username">The optional username to fetch from the origin repository.</param>
/// <param name="password">The optional password to fetch from the origin repository.</param>
/// <param name="deploymentPipeline">If any events created should be marked as part of the deployment pipeline.</param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> for the operation.</param>
/// <returns>A <see cref="ValueTask"/> representing the running operation.</returns>
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.
/// </summary>
/// <param name="progressReporter">The <see cref="JobProgressReporter"/> to report progress of the operation.</param>
/// <param name="username">The username used for fetching from submodule repositories.</param>
/// <param name="password">The password used for fetching from submodule repositories.</param>
/// <param name="username">The optional username used for fetching from submodule repositories.</param>
/// <param name="password">The optional password used for fetching from submodule repositories.</param>
/// <param name="updateSubmodules">If a submodule update should be attempted after the merge.</param>
/// <param name="deploymentPipeline">If any events created should be marked as part of the deployment pipeline.</param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> for the operation.</param>
/// <returns>A <see cref="ValueTask{TResult}"/> resulting in the SHA of the new HEAD.</returns>
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
/// <param name="deploymentPipeline">If any events created should be marked as part of the deployment pipeline.</param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> for the operation.</param>
/// <returns>A <see cref="ValueTask{TResult}"/> resulting in <see langword="true"/> if commits were pushed to the tracked origin reference, <see langword="false"/> otherwise.</returns>
ValueTask<bool> Sychronize(
ValueTask<bool> Synchronize(
JobProgressReporter progressReporter,
string username,
string password,
string? username,
string? password,
string committerName,
string committerEmail,
bool synchronizeTrackedBranch,
@@ -26,25 +26,25 @@ namespace Tgstation.Server.Host.Components.Repository
/// </summary>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> for the operation.</param>
/// <returns>A <see cref="ValueTask{TResult}"/> resulting in the loaded <see cref="IRepository"/> if it exists, <see langword="null"/> otherwise.</returns>
ValueTask<IRepository> LoadRepository(CancellationToken cancellationToken);
ValueTask<IRepository?> LoadRepository(CancellationToken cancellationToken);
/// <summary>
/// Clone the repository at <paramref name="url"/>.
/// </summary>
/// <param name="url">The <see cref="Uri"/> of the remote repository to clone.</param>
/// <param name="initialBranch">The branch to clone.</param>
/// <param name="username">The username to clone from <paramref name="url"/>.</param>
/// <param name="password">The password to clone from <paramref name="url"/>.</param>
/// <param name="initialBranch">The optional branch to clone.</param>
/// <param name="username">The optional username to clone from <paramref name="url"/>.</param>
/// <param name="password">The optional password to clone from <paramref name="url"/>.</param>
/// <param name="progressReporter">The optional <see cref="JobProgressReporter"/> for progress of the clone.</param>
/// <param name="recurseSubmodules">If submodules should be recusively cloned and initialized.</param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> for the operation.</param>
/// <returns>A <see cref="ValueTask{TResult}"/> resulting i the newly cloned <see cref="IRepository"/>, <see langword="null"/> if one already exists.</returns>
ValueTask<IRepository> CloneRepository(
ValueTask<IRepository?> CloneRepository(
Uri url,
string initialBranch,
string username,
string password,
JobProgressReporter progressReporter,
string? initialBranch,
string? username,
string? password,
JobProgressReporter? progressReporter,
bool recurseSubmodules,
CancellationToken cancellationToken);
@@ -80,7 +80,7 @@ namespace Tgstation.Server.Host.Components.Repository
TaskScheduler.Current);
/// <inheritdoc />
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);
@@ -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
{
/// <inheritdoc />
#pragma warning disable CA1506 // TODO: Decomplexify
sealed class Repository : IRepository
sealed class Repository : DisposeInvoker, IRepository
{
/// <summary>
/// The default username for committers.
@@ -53,10 +54,10 @@ namespace Tgstation.Server.Host.Components.Repository
public RemoteGitProvider? RemoteGitProvider => gitRemoteFeatures.RemoteGitProvider;
/// <inheritdoc />
public string RemoteRepositoryOwner => gitRemoteFeatures.RemoteRepositoryOwner;
public string? RemoteRepositoryOwner => gitRemoteFeatures.RemoteRepositoryOwner;
/// <inheritdoc />
public string RemoteRepositoryName => gitRemoteFeatures.RemoteRepositoryName;
public string? RemoteRepositoryName => gitRemoteFeatures.RemoteRepositoryName;
/// <inheritdoc />
public bool Tracking => Reference != null && libGitRepo.Head.IsTracking;
@@ -115,16 +116,6 @@ namespace Tgstation.Server.Host.Components.Repository
/// </summary>
readonly GeneralConfiguration generalConfiguration;
/// <summary>
/// <see cref="Action"/> to be taken when <see cref="Dispose"/> is called.
/// </summary>
readonly Action onDispose;
/// <summary>
/// If the <see cref="Repository"/> was disposed.
/// </summary>
bool disposed;
/// <summary>
/// Initializes a new instance of the <see cref="Repository"/> class.
/// </summary>
@@ -137,7 +128,7 @@ namespace Tgstation.Server.Host.Components.Repository
/// <param name="gitRemoteFeaturesFactory">The <see cref="IGitRemoteFeaturesFactory"/> to provide the value of <see cref="gitRemoteFeatures"/>.</param>
/// <param name="logger">The value of <see cref="logger"/>.</param>
/// <param name="generalConfiguration">The value of <see cref="generalConfiguration"/>.</param>
/// <param name="onDispose">The value if <see cref="onDispose"/>.</param>
/// <param name="disposeAction">The <see cref="IDisposable.Dispose"/> action for the <see cref="DisposeInvoker"/>.</param>
public Repository(
LibGit2Sharp.IRepository libGitRepo,
ILibGit2Commands commands,
@@ -148,7 +139,8 @@ namespace Tgstation.Server.Host.Components.Repository
IGitRemoteFeaturesFactory gitRemoteFeaturesFactory,
ILogger<Repository> 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);
}
/// <inheritdoc />
public void Dispose()
{
lock (onDispose)
{
if (disposed)
return;
disposed = true;
}
logger.LogTrace("Disposing...");
libGitRepo.Dispose();
onDispose();
}
/// <inheritdoc />
#pragma warning disable CA1506 // TODO: Decomplexify
public async ValueTask<TestMergeResult> 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<string> conflictedPaths = null;
List<string>? 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<string>
{
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<string>
new List<string?>
{
testMergeParameters.Number.ToString(CultureInfo.InvariantCulture),
testMergeParameters.TargetCommitSha,
testMergeParameters.TargetCommitSha!,
testMergeParameters.Comment,
},
false,
@@ -384,10 +359,10 @@ namespace Tgstation.Server.Host.Components.Repository
/// <inheritdoc />
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
/// <inheritdoc />
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
/// <inheritdoc />
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<string>
{
oldTip.Sha,
trackedBranch.Tip.Sha,
trackedBranch!.Tip.Sha,
oldHead.FriendlyName ?? UnknownReference,
trackedBranch.FriendlyName,
},
@@ -636,10 +611,10 @@ namespace Tgstation.Server.Host.Components.Repository
}
/// <inheritdoc />
public async ValueTask<bool> Sychronize(
public async ValueTask<bool> 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);
/// <inheritdoc />
protected override void DisposeImpl()
{
logger.LogTrace("Disposing...");
libGitRepo.Dispose();
base.DisposeImpl();
}
/// <summary>
/// Runs a blocking force checkout to <paramref name="committish"/>.
/// </summary>
/// <param name="committish">The committish to checkout.</param>
/// <param name="progressReporter">The optional <see cref="JobProgressReporter"/> for the operation.</param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> for the operation.</param>
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 <see cref="Submodule"/>s in the <see cref="libGitRepo"/>.
/// </summary>
/// <param name="progressReporter">Optional <see cref="JobProgressReporter"/> of the operation.</param>
/// <param name="username">The username for the <see cref="credentialsProvider"/>.</param>
/// <param name="password">The password for the <see cref="credentialsProvider"/>.</param>
/// <param name="username">The optional username for the <see cref="credentialsProvider"/>.</param>
/// <param name="password">The optional password for the <see cref="credentialsProvider"/>.</param>
/// <param name="deploymentPipeline">If any events created should be marked as part of the deployment pipeline.</param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> for the operation.</param>
/// <returns>A <see cref="ValueTask"/> representing the running operation.</returns>
async ValueTask UpdateSubmodules(
JobProgressReporter progressReporter,
string username,
string password,
JobProgressReporter? progressReporter,
string? username,
string? password,
bool deploymentPipeline,
CancellationToken cancellationToken)
{
@@ -118,12 +118,12 @@ namespace Tgstation.Server.Host.Components.Repository
}
/// <inheritdoc />
public async ValueTask<IRepository> CloneRepository(
public async ValueTask<IRepository?> 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
}
/// <inheritdoc />
public async ValueTask<IRepository> LoadRepository(CancellationToken cancellationToken)
public async ValueTask<IRepository?> LoadRepository(CancellationToken cancellationToken)
{
logger.LogTrace("Begin LoadRepository...");
lock (semaphore)
@@ -85,16 +85,18 @@ namespace Tgstation.Server.Host.Components.Repository
IDatabaseContext databaseContext,
ILogger logger,
Models.Instance instance,
string lastOriginCommitSha,
Action<Models.RevisionInformation> revInfoSink,
string? lastOriginCommitSha,
Action<Models.RevisionInformation>? revInfoSink,
CancellationToken cancellationToken)
{
var repoSha = repository.Head;
IQueryable<Models.RevisionInformation> ApplyQuery(IQueryable<Models.RevisionInformation> 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
/// <returns>A <see cref="ValueTask"/> representing the running operation.</returns>
#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<Models.RevisionInformation> dbPull = null;
List<Models.RevisionInformation>? 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<long>();
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);
@@ -17,6 +17,6 @@ namespace Tgstation.Server.Host.Components.Repository
/// <summary>
/// List of conflicting file paths relative to the repository root. Only present if <see cref="Status"/> is <see cref="MergeStatus.Conflicts"/>.
/// </summary>
public IReadOnlyList<string> ConflictingFiles { get; init; }
public IReadOnlyList<string>? ConflictingFiles { get; init; }
}
}
@@ -5,26 +5,26 @@ using Tgstation.Server.Host.Components.Interop.Topic;
namespace Tgstation.Server.Host.Components.Session
{
/// <summary>
/// Combines a <see cref="global::Byond.TopicSender.TopicResponse"/> with a <see cref="TopicResponse"/>.
/// Combines a <see cref="Byond.TopicSender.TopicResponse"/> with a <see cref="TopicResponse"/>.
/// </summary>
sealed class CombinedTopicResponse
{
/// <summary>
/// The raw <see cref="global::Byond.TopicSender.TopicResponse"/>.
/// The raw <see cref="Byond.TopicSender.TopicResponse"/>.
/// </summary>
public global::Byond.TopicSender.TopicResponse ByondTopicResponse { get; }
public Byond.TopicSender.TopicResponse ByondTopicResponse { get; }
/// <summary>
/// The interop <see cref="TopicResponse"/>, if any.
/// </summary>
public TopicResponse InteropResponse { get; }
public TopicResponse? InteropResponse { get; }
/// <summary>
/// Initializes a new instance of the <see cref="CombinedTopicResponse"/> class.
/// </summary>
/// <param name="byondTopicResponse">The value of <see cref="ByondTopicResponse"/>.</param>
/// <param name="interopResponse">The optional value of <see cref="InteropResponse"/>.</param>
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;
@@ -32,7 +32,7 @@ namespace Tgstation.Server.Host.Components.Session
/// <summary>
/// The DMAPI <see cref="Version"/>.
/// </summary>
Version DMApiVersion { get; }
Version? DMApiVersion { get; }
/// <summary>
/// Gets the <see cref="CompileJob"/> associated with the <see cref="ISessionController"/>.
@@ -96,7 +96,7 @@ namespace Tgstation.Server.Host.Components.Session
/// <param name="parameters">The <see cref="TopicParameters"/> to send.</param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> for the operation.</param>
/// <returns>A <see cref="ValueTask{TResult}"/> resulting in the <see cref="TopicResponse"/> of /world/Topic().</returns>
ValueTask<TopicResponse> SendCommand(TopicParameters parameters, CancellationToken cancellationToken);
ValueTask<TopicResponse?> SendCommand(TopicParameters parameters, CancellationToken cancellationToken);
/// <summary>
/// Attempts to change the current <see cref="RebootState"/> to <paramref name="newRebootState"/>.
@@ -16,14 +16,14 @@ namespace Tgstation.Server.Host.Components.Session
/// Create a <see cref="ISessionController"/> from a freshly launch DreamDaemon instance.
/// </summary>
/// <param name="dmbProvider">The <see cref="IDmbProvider"/> to use.</param>
/// <param name="currentByondLock">The current <see cref="IEngineExecutableLock"/> if any.</param>
/// <param name="currentByondLock">The current <see cref="IEngineExecutableLock"/>. if any.</param>
/// <param name="launchParameters">The <see cref="DreamDaemonLaunchParameters"/> to use. <see cref="DreamDaemonLaunchParameters.SecurityLevel"/> will be updated with the minumum required security level for the launch.</param>
/// <param name="apiValidate">If the <see cref="ISessionController"/> should only validate the DMAPI then exit.</param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> for the operation.</param>
/// <returns>A <see cref="ValueTask{TResult}"/> resulting in a new <see cref="ISessionController"/>.</returns>
ValueTask<ISessionController> LaunchNew(
IDmbProvider dmbProvider,
IEngineExecutableLock currentByondLock,
IEngineExecutableLock? currentByondLock,
DreamDaemonLaunchParameters launchParameters,
bool apiValidate,
CancellationToken cancellationToken);
@@ -34,7 +34,7 @@ namespace Tgstation.Server.Host.Components.Session
/// <param name="reattachInformation">The <see cref="ReattachInformation"/> to use.</param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> for the operation.</param>
/// <returns>A <see cref="ValueTask{TResult}"/> resulting in a new <see cref="ISessionController"/> on success or <see langword="null"/> on failure to reattach.</returns>
ValueTask<ISessionController> Reattach(
ValueTask<ISessionController?> Reattach(
ReattachInformation reattachInformation,
CancellationToken cancellationToken);
}
@@ -29,7 +29,7 @@ namespace Tgstation.Server.Host.Components.Session
/// </summary>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> for the operation.</param>
/// <returns>A <see cref="ValueTask{TResult}"/> resulting in the stored <see cref="ReattachInformation"/> if any.</returns>
ValueTask<ReattachInformation> Load(CancellationToken cancellationToken);
ValueTask<ReattachInformation?> Load(CancellationToken cancellationToken);
/// <summary>
/// Clear any stored <see cref="ReattachInformation"/>.

Some files were not shown because too many files have changed in this diff Show More