mirror of
https://github.com/tgstation/tgstation-server.git
synced 2026-08-26 06:27:19 +01:00
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:
@@ -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>
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
|
||||
+54
-42
@@ -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);
|
||||
}
|
||||
|
||||
+26
-20
@@ -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
|
||||
|
||||
+4
-4
@@ -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
Reference in New Issue
Block a user