diff --git a/build/Version.props b/build/Version.props index 63918ebbe0..9ef2360ec9 100644 --- a/build/Version.props +++ b/build/Version.props @@ -3,7 +3,7 @@ - 5.8.0 + 5.9.0 4.5.0 9.9.0 10.3.0 diff --git a/src/DMAPI/tgs/v5/api.dm b/src/DMAPI/tgs/v5/api.dm index 4bf593f35d..4d08ec194d 100644 --- a/src/DMAPI/tgs/v5/api.dm +++ b/src/DMAPI/tgs/v5/api.dm @@ -101,8 +101,7 @@ var/list/response = list() if(error_message) response[DMAPI5_RESPONSE_ERROR_MESSAGE] = error_message - return json_encode(response) - return "{}" + return response /datum/tgs_api/v5/OnTopic(T) RequireInitialBridgeResponse() @@ -127,17 +126,20 @@ if(!isnum(command)) return TopicResponse("Failed to decode [DMAPI5_TOPIC_PARAMETER_COMMAND_TYPE] from: [json]!") + var/result = ProcessTopicCommand(command, topic_parameters) + if(!length(result)) + return "{}" // quirk of json_encode is an empty list returns "[]" + + return json_encode(result) + +/datum/tgs_api/v5/proc/ProcessTopicCommand(command, list/topic_parameters) switch(command) if(DMAPI5_TOPIC_COMMAND_CHAT_COMMAND) intercepted_message_queue = list() - var/result = HandleCustomCommand(topic_parameters[DMAPI5_TOPIC_PARAMETER_CHAT_COMMAND]) + var/list/result = HandleCustomCommand(topic_parameters[DMAPI5_TOPIC_PARAMETER_CHAT_COMMAND]) if(!result) result = TopicResponse("Error running chat command!") - //TODO: make this not need the decode/encode. - if (length(intercepted_message_queue)) - var/list/result_array = json_decode(result) - result_array[DMAPI5_TOPIC_RESPONSE_CHAT_RESPONSES] = intercepted_message_queue - result = json_encode(result_array) + result[DMAPI5_TOPIC_RESPONSE_CHAT_RESPONSES] = intercepted_message_queue intercepted_message_queue = null return result if(DMAPI5_TOPIC_COMMAND_EVENT_NOTIFICATION) @@ -161,10 +163,10 @@ if(event_handler != null) event_handler.HandleEvent(arglist(event_call)) - var/list/response = list() + var/list/response = TopicResponse() response[DMAPI5_TOPIC_RESPONSE_CHAT_RESPONSES] = intercepted_message_queue intercepted_message_queue = null - return json_encode(response) + return response if(DMAPI5_TOPIC_COMMAND_CHANGE_PORT) var/new_port = topic_parameters[DMAPI5_TOPIC_PARAMETER_NEW_PORT] if (!isnum(new_port) || !(new_port > 0)) @@ -236,7 +238,9 @@ version = new_version - return json_encode(list(DMAPI5_RESPONSE_ERROR_MESSAGE = error_message, DMAPI5_PARAMETER_CUSTOM_COMMANDS = ListCustomCommands())) + var/list/reattach_response = TopicResponse(error_message) + reattach_response[DMAPI5_PARAMETER_CUSTOM_COMMANDS] = ListCustomCommands() + return reattach_response return TopicResponse("Unknown command: [command]") diff --git a/src/DMAPI/tgs/v5/commands.dm b/src/DMAPI/tgs/v5/commands.dm index 71ede42c3b..a832c81f17 100644 --- a/src/DMAPI/tgs/v5/commands.dm +++ b/src/DMAPI/tgs/v5/commands.dm @@ -36,10 +36,10 @@ var/datum/tgs_message_content/response = sc.Run(u, params) response = UpgradeDeprecatedCommandResponse(response, command) - var/list/topic_response = list() + var/list/topic_response = TopicResponse() topic_response[DMAPI5_TOPIC_RESPONSE_COMMAND_RESPONSE_MESSAGE] = response?.text topic_response[DMAPI5_TOPIC_RESPONSE_COMMAND_RESPONSE] = response?._interop_serialize() - return json_encode(topic_response) + return topic_response return TopicResponse("Unknown custom chat command: [command]!") // Common proc b/c it's used by the V3/V4 APIs diff --git a/src/Tgstation.Server.Api/Tgstation.Server.Api.csproj b/src/Tgstation.Server.Api/Tgstation.Server.Api.csproj index f079f488db..c67ed86f87 100644 --- a/src/Tgstation.Server.Api/Tgstation.Server.Api.csproj +++ b/src/Tgstation.Server.Api/Tgstation.Server.Api.csproj @@ -46,12 +46,16 @@ + + + all runtime; build; native; contentfiles; analyzers; buildtransitive + diff --git a/src/Tgstation.Server.Client/Tgstation.Server.Client.csproj b/src/Tgstation.Server.Client/Tgstation.Server.Client.csproj index 6ef0426a5b..9ea8c721cd 100644 --- a/src/Tgstation.Server.Client/Tgstation.Server.Client.csproj +++ b/src/Tgstation.Server.Client/Tgstation.Server.Client.csproj @@ -1,4 +1,4 @@ - + @@ -32,6 +32,7 @@ + all runtime; build; native; contentfiles; analyzers; buildtransitive diff --git a/src/Tgstation.Server.Host.Console/Tgstation.Server.Host.Console.csproj b/src/Tgstation.Server.Host.Console/Tgstation.Server.Host.Console.csproj index 9ab96c2f7b..045d8ce9eb 100644 --- a/src/Tgstation.Server.Host.Console/Tgstation.Server.Host.Console.csproj +++ b/src/Tgstation.Server.Host.Console/Tgstation.Server.Host.Console.csproj @@ -24,8 +24,9 @@ - + + all runtime; build; native; contentfiles; analyzers diff --git a/src/Tgstation.Server.Host.Service/Tgstation.Server.Host.Service.csproj b/src/Tgstation.Server.Host.Service/Tgstation.Server.Host.Service.csproj index 23ad2e5d76..5499f045f7 100644 --- a/src/Tgstation.Server.Host.Service/Tgstation.Server.Host.Service.csproj +++ b/src/Tgstation.Server.Host.Service/Tgstation.Server.Host.Service.csproj @@ -23,17 +23,18 @@ + - - all - runtime; build; native; contentfiles; analyzers; buildtransitive - + + + all runtime; build; native; contentfiles; analyzers; buildtransitive + diff --git a/src/Tgstation.Server.Host.Watchdog/Tgstation.Server.Host.Watchdog.csproj b/src/Tgstation.Server.Host.Watchdog/Tgstation.Server.Host.Watchdog.csproj index 763e986d9d..972487c7dd 100644 --- a/src/Tgstation.Server.Host.Watchdog/Tgstation.Server.Host.Watchdog.csproj +++ b/src/Tgstation.Server.Host.Watchdog/Tgstation.Server.Host.Watchdog.csproj @@ -24,12 +24,9 @@ - - all - runtime; build; native; contentfiles; analyzers; buildtransitive - - + + all runtime; build; native; contentfiles; analyzers; buildtransitive diff --git a/src/Tgstation.Server.Host/Components/Byond/ByondManager.cs b/src/Tgstation.Server.Host/Components/Byond/ByondManager.cs index 76f5ebce25..eab96c95d8 100644 --- a/src/Tgstation.Server.Host/Components/Byond/ByondManager.cs +++ b/src/Tgstation.Server.Host/Components/Byond/ByondManager.cs @@ -150,15 +150,13 @@ namespace Tgstation.Server.Host.Components.Byond /// public async Task UseExecutables(Version requiredVersion, CancellationToken cancellationToken) { - var versionToUse = requiredVersion ?? ActiveVersion; - if (versionToUse == null) - throw new JobException(ErrorCode.ByondNoVersionsInstalled); + var versionToUse = requiredVersion ?? ActiveVersion ?? throw new JobException(ErrorCode.ByondNoVersionsInstalled); await InstallVersion(versionToUse, null, cancellationToken); var versionKey = VersionKey(versionToUse, true); var binPathForVersion = ioManager.ConcatPath(versionKey, BinPath); - logger.LogTrace("Creating ByondExecutableLock lock for version {0}", versionToUse); + logger.LogTrace("Creating ByondExecutableLock lock for version {versionToUse}", versionToUse); return new ByondExecutableLock( ioManager, semaphore, @@ -203,7 +201,7 @@ namespace Tgstation.Server.Host.Components.Byond ioManager.ConcatPath( localCfgDirectory, TrustedDmbFileName); - logger.LogTrace("Deleting trusted .dmbs file {0}", trustedFilePath); + logger.LogTrace("Deleting trusted .dmbs file {trustedFilePath}", trustedFilePath); await ioManager.DeleteFile( trustedFilePath, cancellationToken); @@ -219,7 +217,7 @@ namespace Tgstation.Server.Host.Components.Byond var versionFile = ioManager.ConcatPath(path, VersionFileName); if (!await ioManager.FileExists(versionFile, cancellationToken)) { - logger.LogInformation("Cleaning unparsable version path: {0}", ioManager.ResolvePath(path)); + logger.LogInformation("Cleaning unparsable version path: {versionPath}", ioManager.ResolvePath(path)); await ioManager.DeleteDirectory(path, cancellationToken); // cleanup return; } @@ -232,7 +230,7 @@ namespace Tgstation.Server.Host.Components.Byond lock (installedVersions) if (!installedVersions.ContainsKey(key)) { - logger.LogDebug("Adding detected BYOND version {0}...", key); + logger.LogDebug("Adding detected BYOND version {versionKey}...", key); installedVersions.Add(key, Task.CompletedTask); installedVersionPaths.Add(ioManager.ResolvePath(key), version); return; @@ -276,7 +274,7 @@ namespace Tgstation.Server.Host.Components.Byond /// A representing the running operation. async Task InstallVersion(Version version, Stream customVersionStream, CancellationToken cancellationToken) { - var ourTcs = new TaskCompletionSource(); + var ourTcs = new TaskCompletionSource(); Task inProgressTask; string versionKey; bool installed; @@ -308,11 +306,11 @@ namespace Tgstation.Server.Host.Components.Byond } if (customVersionStream != null) - logger.LogInformation("Installing custom BYOND version as {0}...", versionKey); + logger.LogInformation("Installing custom BYOND version as {versionKey}...", versionKey); else if (version.Build > 0) throw new JobException(ErrorCode.ByondNonExistentCustomVersion); else - logger.LogDebug("Requested BYOND version {0} not currently installed. Doing so now...", versionKey); + logger.LogDebug("Requested BYOND version {versionKey} not currently installed. Doing so now...", versionKey); // okay up to us to install it then try @@ -342,7 +340,7 @@ namespace Tgstation.Server.Host.Components.Byond using (downloadedStream) { await directoryCleanupTask; - logger.LogTrace("Extracting downloaded BYOND zip to {0}...", extractPath); + logger.LogTrace("Extracting downloaded BYOND zip to {extractPath}...", extractPath); await ioManager.ZipToDirectory(extractPath, versionZipStream, cancellationToken); } @@ -370,11 +368,11 @@ namespace Tgstation.Server.Host.Components.Byond throw; } - ourTcs.SetResult(null); + ourTcs.SetResult(); } catch (Exception e) { - if (!(e is OperationCanceledException)) + if (e is not OperationCanceledException) await eventConsumer.HandleEvent(EventType.ByondInstallFail, new List { e.Message }, cancellationToken); lock (installedVersions) installedVersions.Remove(versionKey); diff --git a/src/Tgstation.Server.Host/Components/Chat/ChatManager.cs b/src/Tgstation.Server.Host/Components/Chat/ChatManager.cs index d57a900ef1..7b204b0b05 100644 --- a/src/Tgstation.Server.Host/Components/Chat/ChatManager.cs +++ b/src/Tgstation.Server.Host/Components/Chat/ChatManager.cs @@ -109,9 +109,9 @@ namespace Tgstation.Server.Host.Components.Chat Task messageSendTask; /// - /// The that completes when s change. + /// The that completes when s change. /// - TaskCompletionSource connectionsUpdated; + TaskCompletionSource connectionsUpdated; /// /// Used for remapping s. @@ -157,7 +157,7 @@ namespace Tgstation.Server.Host.Components.Chat mappedChannels = new Dictionary(); trackingContexts = new List(); handlerCts = new CancellationTokenSource(); - connectionsUpdated = new TaskCompletionSource(); + connectionsUpdated = new TaskCompletionSource(); messageSendTask = Task.CompletedTask; channelIdCounter = 1; @@ -293,8 +293,8 @@ namespace Tgstation.Server.Host.Components.Chat { // same thread shennanigans var oldOne = connectionsUpdated; - connectionsUpdated = new TaskCompletionSource(); - oldOne.SetResult(null); + connectionsUpdated = new TaskCompletionSource(); + oldOne.SetResult(); } var reconnectionUpdateTask = provider?.SetReconnectInterval( @@ -483,6 +483,8 @@ namespace Tgstation.Server.Host.Components.Chat logger.LogTrace("DeleteConnection {connectionId}", connectionId); var provider = await RemoveProviderChannels(connectionId, true, cancellationToken); if (provider != null) + { + var startTime = DateTimeOffset.UtcNow; try { await provider.Disconnect(cancellationToken); @@ -490,7 +492,11 @@ namespace Tgstation.Server.Host.Components.Chat finally { await provider.DisposeAsync(); + var duration = DateTimeOffset.UtcNow - startTime; + if (duration.TotalSeconds > 3) + logger.LogWarning("Disconnecting a {providerType} took {totalSeconds}s!", provider.GetType().Name, duration.TotalSeconds); } + } else logger.LogTrace("DeleteConnection: ID {connectionId} doesn't exist!", connectionId); } diff --git a/src/Tgstation.Server.Host/Components/Chat/Providers/DiscordProvider.cs b/src/Tgstation.Server.Host/Components/Chat/Providers/DiscordProvider.cs index dfddd9d326..bd29609528 100644 --- a/src/Tgstation.Server.Host/Components/Chat/Providers/DiscordProvider.cs +++ b/src/Tgstation.Server.Host/Components/Chat/Providers/DiscordProvider.cs @@ -88,9 +88,9 @@ namespace Tgstation.Server.Host.Components.Chat.Providers CancellationTokenSource gatewayCts; /// - /// The for the initial gateway connection event. + /// The for the initial gateway connection event. /// - TaskCompletionSource gatewayReadyTcs; + TaskCompletionSource gatewayReadyTcs; /// /// The representing the lifetime of the client. @@ -145,14 +145,14 @@ namespace Tgstation.Server.Host.Components.Chat.Providers new EmbedField( "Local Commit", localCommitPushed && gitHub - ? $"[{revisionInformation.CommitSha.Substring(0, 7)}](https://github.com/{gitHubOwner}/{gitHubRepo}/commit/{revisionInformation.CommitSha})" - : revisionInformation.CommitSha.Substring(0, 7), + ? $"[{revisionInformation.CommitSha[..7]}](https://github.com/{gitHubOwner}/{gitHubRepo}/commit/{revisionInformation.CommitSha})" + : revisionInformation.CommitSha[..7], true), new EmbedField( "Branch Commit", gitHub - ? $"[{revisionInformation.OriginCommitSha.Substring(0, 7)}](https://github.com/{gitHubOwner}/{gitHubRepo}/commit/{revisionInformation.OriginCommitSha})" - : revisionInformation.OriginCommitSha.Substring(0, 7), + ? $"[{revisionInformation.OriginCommitSha[..7]}](https://github.com/{gitHubOwner}/{gitHubRepo}/commit/{revisionInformation.OriginCommitSha})" + : revisionInformation.OriginCommitSha[..7], true), }; @@ -160,7 +160,7 @@ namespace Tgstation.Server.Host.Components.Chat.Providers .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.Substring(0, 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; @@ -253,7 +253,7 @@ namespace Tgstation.Server.Host.Components.Chat.Providers if (!result.IsSuccess) Logger.LogWarning( - "Failed to send to channel {0}: {1}", + "Failed to send to channel {channelId}: {error}", channelId, result.Error); } @@ -267,7 +267,7 @@ namespace Tgstation.Server.Host.Components.Chat.Providers if (!currentGuildsResponse.IsSuccess) { Logger.LogWarning( - "Error retrieving current discord guilds: {0}", + "Error retrieving current discord guilds: {error}", currentGuildsResponse.Error.Message); return; } @@ -559,7 +559,7 @@ namespace Tgstation.Server.Host.Components.Chat.Providers throw new ArgumentNullException(nameof(readyEvent)); Logger.LogTrace("Gatway ready. Version: {version}", readyEvent.Version); - gatewayReadyTcs?.TrySetResult(null); + gatewayReadyTcs?.TrySetResult(); return Task.FromResult(Result.FromSuccess()); } @@ -580,7 +580,7 @@ namespace Tgstation.Server.Host.Components.Chat.Providers var gatewayClient = serviceProvider.GetRequiredService(); Task localGatewayTask; - gatewayReadyTcs = new TaskCompletionSource(); + gatewayReadyTcs = new TaskCompletionSource(); using var gatewayConnectionAbortRegistration = cancellationToken.Register(() => gatewayReadyTcs.TrySetCanceled()); gatewayCancellationToken.Register(() => Logger.LogTrace("Stopping gateway client...")); @@ -758,10 +758,10 @@ namespace Tgstation.Server.Host.Components.Chat.Providers if (embed == null) return default; - List embedErrors = new List(); + var embedErrors = new List(); Optional colour = default; if (embed.Colour != null) - if (Int32.TryParse(embed.Colour.Substring(1), NumberStyles.HexNumber, CultureInfo.InvariantCulture, out var argb)) + if (Int32.TryParse(embed.Colour[1..], NumberStyles.HexNumber, CultureInfo.InvariantCulture, out var argb)) colour = Color.FromArgb(argb); else embedErrors.Add( diff --git a/src/Tgstation.Server.Host/Components/Chat/Providers/IrcProvider.cs b/src/Tgstation.Server.Host/Components/Chat/Providers/IrcProvider.cs index 32eeb51dac..5d08910ed9 100644 --- a/src/Tgstation.Server.Host/Components/Chat/Providers/IrcProvider.cs +++ b/src/Tgstation.Server.Host/Components/Chat/Providers/IrcProvider.cs @@ -168,8 +168,7 @@ namespace Tgstation.Server.Host.Components.Chat.Providers // IRC doesn't allow newlines // Explicitly ignore embeds var messageText = message.Text; - if (messageText == null) - messageText = $"Embed Only: {JsonConvert.SerializeObject(message.Embed)}"; + messageText ??= $"Embed Only: {JsonConvert.SerializeObject(message.Embed)}"; messageText = String.Concat( messageText @@ -380,7 +379,7 @@ namespace Tgstation.Server.Host.Components.Chat.Providers Logger.LogTrace("Processing initial messages..."); await NonBlockingListen(cancellationToken); - var nickCheckCompleteTcs = new TaskCompletionSource(); + var nickCheckCompleteTcs = new TaskCompletionSource(); using (cancellationToken.Register(() => nickCheckCompleteTcs.TrySetCanceled())) { listenTask = Task.Factory.StartNew( @@ -399,7 +398,7 @@ namespace Tgstation.Server.Host.Components.Chat.Providers client.RfcNick(nickname); } - nickCheckCompleteTcs.TrySetResult(null); + nickCheckCompleteTcs.TrySetResult(); Logger.LogTrace("Starting blocking listen..."); try diff --git a/src/Tgstation.Server.Host/Components/Chat/Providers/Provider.cs b/src/Tgstation.Server.Host/Components/Chat/Providers/Provider.cs index eb7ac3ca35..0422d24e51 100644 --- a/src/Tgstation.Server.Host/Components/Chat/Providers/Provider.cs +++ b/src/Tgstation.Server.Host/Components/Chat/Providers/Provider.cs @@ -40,9 +40,9 @@ namespace Tgstation.Server.Host.Components.Chat.Providers readonly Queue messageQueue; /// - /// The backing for . + /// The backing for . /// - readonly TaskCompletionSource initialConnectionTcs; + readonly TaskCompletionSource initialConnectionTcs; /// /// Used for synchronizing access to and . @@ -50,9 +50,9 @@ namespace Tgstation.Server.Host.Components.Chat.Providers readonly object reconnectTaskLock; /// - /// that completes while isn't empty. + /// that completes while isn't empty. /// - TaskCompletionSource nextMessage; + TaskCompletionSource nextMessage; /// /// The auto reconnect . @@ -77,8 +77,8 @@ namespace Tgstation.Server.Host.Components.Chat.Providers ChatBot = chatBot ?? throw new ArgumentNullException(nameof(chatBot)); messageQueue = new Queue(); - nextMessage = new TaskCompletionSource(); - initialConnectionTcs = new TaskCompletionSource(); + nextMessage = new TaskCompletionSource(); + initialConnectionTcs = new TaskCompletionSource(); reconnectTaskLock = new object(); logger.LogTrace("Created."); @@ -115,7 +115,7 @@ namespace Tgstation.Server.Host.Components.Chat.Providers } /// - public void InitialMappingComplete() => initialConnectionTcs.TrySetResult(null); + public void InitialMappingComplete() => initialConnectionTcs.TrySetResult(); /// public async Task>> MapChannels(IEnumerable channels, CancellationToken cancellationToken) @@ -126,7 +126,7 @@ namespace Tgstation.Server.Host.Components.Chat.Providers } catch { - initialConnectionTcs.TrySetResult(null); + initialConnectionTcs.TrySetResult(); throw; } } @@ -142,7 +142,7 @@ namespace Tgstation.Server.Host.Components.Chat.Providers { var result = messageQueue.Dequeue(); if (messageQueue.Count == 0) - nextMessage = new TaskCompletionSource(); + nextMessage = new TaskCompletionSource(); return result; } } @@ -215,7 +215,7 @@ namespace Tgstation.Server.Host.Components.Chat.Providers lock (messageQueue) { messageQueue.Enqueue(message); - nextMessage.TrySetResult(null); + nextMessage.TrySetResult(); } } @@ -290,7 +290,7 @@ namespace Tgstation.Server.Host.Components.Chat.Providers } catch { - initialConnectionTcs.TrySetResult(null); + initialConnectionTcs.TrySetResult(); throw; } }, diff --git a/src/Tgstation.Server.Host/Components/Deployment/DmbFactory.cs b/src/Tgstation.Server.Host/Components/Deployment/DmbFactory.cs index 67e59737cb..1e01982bee 100644 --- a/src/Tgstation.Server.Host/Components/Deployment/DmbFactory.cs +++ b/src/Tgstation.Server.Host/Components/Deployment/DmbFactory.cs @@ -80,9 +80,9 @@ namespace Tgstation.Server.Host.Components.Deployment Task cleanupTask; /// - /// resulting in the latest yet to exist. + /// resulting in the latest yet to exist. /// - TaskCompletionSource newerDmbTcs; + TaskCompletionSource newerDmbTcs; /// /// The latest . @@ -119,7 +119,7 @@ namespace Tgstation.Server.Host.Components.Deployment this.metadata = metadata ?? throw new ArgumentNullException(nameof(metadata)); cleanupTask = Task.CompletedTask; - newerDmbTcs = new TaskCompletionSource(); + newerDmbTcs = new TaskCompletionSource(); cleanupCts = new CancellationTokenSource(); jobLockCounts = new Dictionary(); } @@ -156,8 +156,8 @@ namespace Tgstation.Server.Host.Components.Deployment // Oh god dammit var temp = newerDmbTcs; - newerDmbTcs = new TaskCompletionSource(); - temp.SetResult(nextDmbProvider); + newerDmbTcs = new TaskCompletionSource(); + temp.SetResult(); } } diff --git a/src/Tgstation.Server.Host/Components/IInstanceFactory.cs b/src/Tgstation.Server.Host/Components/IInstanceFactory.cs index 8f3c7d2ab4..0c37b40778 100644 --- a/src/Tgstation.Server.Host/Components/IInstanceFactory.cs +++ b/src/Tgstation.Server.Host/Components/IInstanceFactory.cs @@ -3,6 +3,7 @@ using Microsoft.Extensions.Hosting; using Tgstation.Server.Host.Components.Interop.Bridge; +using Tgstation.Server.Host.IO; namespace Tgstation.Server.Host.Components { @@ -18,5 +19,12 @@ namespace Tgstation.Server.Host.Components /// The . /// A resulting in a new . Task CreateInstance(IBridgeRegistrar bridgeRegistrar, Models.Instance metadata); + + /// + /// Create an that resolves to the "Game" directory of the defined by . + /// + /// The . + /// The for the instance's "Game" directory. + IIOManager CreateGameIOManager(Models.Instance metadata); } } diff --git a/src/Tgstation.Server.Host/Components/InstanceContainer.cs b/src/Tgstation.Server.Host/Components/InstanceContainer.cs index 9cb320cce5..6a8c49500e 100644 --- a/src/Tgstation.Server.Host/Components/InstanceContainer.cs +++ b/src/Tgstation.Server.Host/Components/InstanceContainer.cs @@ -35,9 +35,9 @@ namespace Tgstation.Server.Host.Components readonly object referenceCountLock; /// - /// Backing for . + /// Backing for . /// - TaskCompletionSource onZeroReferencesTcs; + TaskCompletionSource onZeroReferencesTcs; /// /// Count of active s. @@ -64,7 +64,7 @@ namespace Tgstation.Server.Host.Components lock (referenceCountLock) { if (referenceCount++ == 0) - onZeroReferencesTcs = new TaskCompletionSource(); + onZeroReferencesTcs = new TaskCompletionSource(); try { @@ -72,7 +72,7 @@ namespace Tgstation.Server.Host.Components { lock (referenceCountLock) if (--referenceCount == 0) - onZeroReferencesTcs.SetResult(null); + onZeroReferencesTcs.SetResult(); }); } catch diff --git a/src/Tgstation.Server.Host/Components/InstanceFactory.cs b/src/Tgstation.Server.Host/Components/InstanceFactory.cs index 5a78706831..c9b891f087 100644 --- a/src/Tgstation.Server.Host/Components/InstanceFactory.cs +++ b/src/Tgstation.Server.Host/Components/InstanceFactory.cs @@ -144,6 +144,13 @@ namespace Tgstation.Server.Host.Components /// readonly SessionConfiguration sessionConfiguration; + /// + /// Create the pointing to the "Game" directory of a given . + /// + /// The instance's . + /// The for the instance's "Game" directory. + static IIOManager CreateGameIOManager(IIOManager instanceIOManager) => new ResolvingIOManager(instanceIOManager, "Game"); + #pragma warning disable CA1502 // TODO: Decomplexify /// /// Initializes a new instance of the class. @@ -222,17 +229,32 @@ namespace Tgstation.Server.Host.Components } #pragma warning restore CA1502 + /// + public IIOManager CreateGameIOManager(Models.Instance metadata) + { + if (metadata == null) + throw new ArgumentNullException(nameof(metadata)); + + var instanceIoManager = CreateInstanceIOManager(metadata); + return CreateGameIOManager(instanceIoManager); + } + /// #pragma warning disable CA1506 // TODO: Decomplexify public async Task CreateInstance(IBridgeRegistrar bridgeRegistrar, Models.Instance metadata) { + if (bridgeRegistrar == null) + throw new ArgumentNullException(nameof(bridgeRegistrar)); + if (metadata == null) + throw new ArgumentNullException(nameof(metadata)); + // Create the ioManager for the instance - var instanceIoManager = new ResolvingIOManager(ioManager, metadata.Path); + var instanceIoManager = CreateInstanceIOManager(metadata); // various other ioManagers var repoIoManager = new ResolvingIOManager(instanceIoManager, "Repository"); var byondIOManager = new ResolvingIOManager(instanceIoManager, "Byond"); - var gameIoManager = new ResolvingIOManager(instanceIoManager, "Game"); + var gameIoManager = CreateGameIOManager(instanceIoManager); var diagnosticsIOManager = new ResolvingIOManager(instanceIoManager, "Diagnostics"); var configurationIoManager = new ResolvingIOManager(instanceIoManager, "Configuration"); @@ -305,7 +327,7 @@ namespace Tgstation.Server.Host.Components sessionControllerFactory, gameIoManager, diagnosticsIOManager, - eventConsumer, + configuration, // watchdog doesn't need itself as an event consumer remoteDeploymentManagerFactory, metadata, metadata.DreamDaemonSettings); @@ -386,5 +408,12 @@ namespace Tgstation.Server.Host.Components /// Test that the is functional. /// void CheckSystemCompatibility() => repositoryFactory.CreateInMemory(); + + /// + /// Create the for a given set of instance . + /// + /// The . + /// The for the . + IIOManager CreateInstanceIOManager(Models.Instance metadata) => new ResolvingIOManager(ioManager, metadata.Path); } } diff --git a/src/Tgstation.Server.Host/Components/InstanceManager.cs b/src/Tgstation.Server.Host/Components/InstanceManager.cs index ac0b1d230b..5c53a622c3 100644 --- a/src/Tgstation.Server.Host/Components/InstanceManager.cs +++ b/src/Tgstation.Server.Host/Components/InstanceManager.cs @@ -117,9 +117,9 @@ namespace Tgstation.Server.Host.Components readonly SwarmConfiguration swarmConfiguration; /// - /// The for . + /// The for . /// - readonly TaskCompletionSource readyTcs; + readonly TaskCompletionSource readyTcs; /// /// If the has been 'd. @@ -173,7 +173,7 @@ namespace Tgstation.Server.Host.Components instances = new Dictionary(); bridgeHandlers = new Dictionary(); - readyTcs = new TaskCompletionSource(); + readyTcs = new TaskCompletionSource(); instanceStateChangeSemaphore = new SemaphoreSlim(1); } @@ -218,6 +218,8 @@ namespace Tgstation.Server.Host.Components { if (oldPath == null) throw new ArgumentNullException(nameof(oldPath)); + + using var lockContext = await SemaphoreSlimContext.Lock(instanceStateChangeSemaphore, cancellationToken); using var instanceReferenceCheck = GetInstanceReference(instance); if (instanceReferenceCheck != null) throw new InvalidOperationException("Cannot move an online instance!"); @@ -225,6 +227,10 @@ namespace Tgstation.Server.Host.Components try { await ioManager.MoveDirectory(oldPath, newPath, cancellationToken); + + // Delete the Game directory to clear out broken symlinks + var instanceGameIOManager = instanceFactory.CreateGameIOManager(instance); + await instanceGameIOManager.DeleteDirectory(".", cancellationToken); } catch (Exception ex) { @@ -307,23 +313,21 @@ namespace Tgstation.Server.Host.Components // we are the one responsible for cancelling his jobs var tasks = new List(); - await databaseContextFactory.UseContext(async db => - { - var jobs = db - .Jobs - .AsQueryable() - .Where(x => x.Instance.Id == metadata.Id) - .Select(x => new Models.Job - { - Id = x.Id, - }); - await jobs.ForEachAsync( - job => + await databaseContextFactory.UseContext( + async db => { - lock (tasks) + var jobs = await db + .Jobs + .AsQueryable() + .Where(x => x.Instance.Id == metadata.Id && !x.StoppedAt.HasValue) + .Select(x => new Models.Job + { + Id = x.Id, + }) + .ToListAsync(cancellationToken); + foreach (var job in jobs) tasks.Add(jobManager.CancelJob(job, user, true, cancellationToken)); - }, cancellationToken); - }); + }); await Task.WhenAll(tasks); @@ -430,7 +434,7 @@ namespace Tgstation.Server.Host.Components jobManager.Activate(); logger.LogInformation("Server ready!"); - readyTcs.SetResult(null); + readyTcs.SetResult(); } catch (OperationCanceledException ex) { diff --git a/src/Tgstation.Server.Host/Components/Session/SessionController.cs b/src/Tgstation.Server.Host/Components/Session/SessionController.cs index 0f76b5993f..adcc944cd3 100644 --- a/src/Tgstation.Server.Host/Components/Session/SessionController.cs +++ b/src/Tgstation.Server.Host/Components/Session/SessionController.cs @@ -76,9 +76,9 @@ namespace Tgstation.Server.Host.Components.Session public ReattachInformation ReattachInformation { get; } /// - /// The that completes when DD makes it's first bridge request. + /// The that completes when DD makes it's first bridge request. /// - readonly TaskCompletionSource initialBridgeRequestTcs; + readonly TaskCompletionSource initialBridgeRequestTcs; /// /// The metadata. @@ -141,14 +141,14 @@ namespace Tgstation.Server.Host.Components.Session ushort? nextPort; /// - /// The that completes when DD tells us about a reboot. + /// The that completes when DD tells us about a reboot. /// - TaskCompletionSource rebootTcs; + TaskCompletionSource rebootTcs; /// - /// The that completes when DD tells us it's primed. + /// The that completes when DD tells us it's primed. /// - TaskCompletionSource primeTcs; + TaskCompletionSource primeTcs; /// /// If we know DreamDaemon currently has it's port closed. @@ -219,9 +219,9 @@ namespace Tgstation.Server.Host.Components.Session apiValidationStatus = ApiValidationStatus.NeverValidated; released = false; - rebootTcs = new TaskCompletionSource(); - primeTcs = new TaskCompletionSource(); - initialBridgeRequestTcs = new TaskCompletionSource(); + rebootTcs = new TaskCompletionSource(); + primeTcs = new TaskCompletionSource(); + initialBridgeRequestTcs = new TaskCompletionSource(); reattachTopicCts = new CancellationTokenSource(); synchronizationLock = new object(); @@ -298,7 +298,7 @@ namespace Tgstation.Server.Host.Components.Session using (LogContext.PushProperty("Instance", metadata.Id)) { logger.LogTrace("Handling bridge request..."); - initialBridgeRequestTcs.TrySetResult(null); + initialBridgeRequestTcs.TrySetResult(); var response = new BridgeResponse(); switch (parameters.CommandType) @@ -333,8 +333,8 @@ namespace Tgstation.Server.Host.Components.Session break; case BridgeCommandType.Prime: var oldPrimeTcs = primeTcs; - primeTcs = new TaskCompletionSource(); - oldPrimeTcs.SetResult(null); + primeTcs = new TaskCompletionSource(); + oldPrimeTcs.SetResult(); break; case BridgeCommandType.Kill: logger.LogInformation("Bridge requested process termination!"); @@ -425,8 +425,8 @@ namespace Tgstation.Server.Host.Components.Session } var oldRebootTcs = rebootTcs; - rebootTcs = new TaskCompletionSource(); - oldRebootTcs.SetResult(null); + rebootTcs = new TaskCompletionSource(); + oldRebootTcs.SetResult(); break; case null: response.ErrorMessage = "Missing commandType!"; diff --git a/src/Tgstation.Server.Host/Components/Watchdog/WatchdogBase.cs b/src/Tgstation.Server.Host/Components/Watchdog/WatchdogBase.cs index 2bbff38173..4d9330aaeb 100644 --- a/src/Tgstation.Server.Host/Components/Watchdog/WatchdogBase.cs +++ b/src/Tgstation.Server.Host/Components/Watchdog/WatchdogBase.cs @@ -57,9 +57,9 @@ namespace Tgstation.Server.Host.Components.Watchdog public abstract RebootState? RebootState { get; } /// - /// that completes when are changed and we are running. + /// that completes when are changed and we are running. /// - protected TaskCompletionSource ActiveParametersUpdated { get; set; } + protected TaskCompletionSource ActiveParametersUpdated { get; set; } /// /// The for the . @@ -220,7 +220,7 @@ namespace Tgstation.Server.Host.Components.Watchdog ActiveLaunchParameters = initialLaunchParameters; releaseServers = false; - ActiveParametersUpdated = new TaskCompletionSource(); + ActiveParametersUpdated = new TaskCompletionSource(); restartRegistration = serverControl.RegisterForRestart(this); try @@ -261,8 +261,8 @@ namespace Tgstation.Server.Host.Components.Watchdog if (match || Status == WatchdogStatus.Offline) return; - ActiveParametersUpdated.TrySetResult(null); // queue an update - ActiveParametersUpdated = new TaskCompletionSource(); + ActiveParametersUpdated.TrySetResult(); // queue an update + ActiveParametersUpdated = new TaskCompletionSource(); } } @@ -872,7 +872,7 @@ namespace Tgstation.Server.Host.Components.Watchdog cancellationToken); // cancel waiting if requested - var cancelTcs = new TaskCompletionSource(); + var cancelTcs = new TaskCompletionSource(); var toWaitOn = Task.WhenAny( activeServerLifetime, activeServerReboot, diff --git a/src/Tgstation.Server.Host/Core/Application.cs b/src/Tgstation.Server.Host/Core/Application.cs index 960e3c3fed..f96b2e5670 100644 --- a/src/Tgstation.Server.Host/Core/Application.cs +++ b/src/Tgstation.Server.Host/Core/Application.cs @@ -3,7 +3,6 @@ using System.Collections.Generic; using System.Globalization; using System.IdentityModel.Tokens.Jwt; using System.Linq; -using System.Threading.Tasks; using Cyberboss.AspNetCore.AsyncInitializer; using Microsoft.AspNetCore.Authentication.JwtBearer; @@ -420,9 +419,7 @@ namespace Tgstation.Server.Host.Core // 503 requests made while the application is starting applicationBuilder.UseAsyncInitialization(async (cancellationToken) => { - var tcs = new TaskCompletionSource(); - using (cancellationToken.Register(() => tcs.SetCanceled())) - await Task.WhenAny(tcs.Task, instanceManager.Ready); + await instanceManager.Ready.WithToken(cancellationToken); }); // suppress OperationCancelledExceptions, they are just aborted HTTP requests diff --git a/src/Tgstation.Server.Host/Extensions/ServiceCollectionExtensions.cs b/src/Tgstation.Server.Host/Extensions/ServiceCollectionExtensions.cs index 04c168dd7a..8e88c63ebf 100644 --- a/src/Tgstation.Server.Host/Extensions/ServiceCollectionExtensions.cs +++ b/src/Tgstation.Server.Host/Extensions/ServiceCollectionExtensions.cs @@ -42,10 +42,8 @@ namespace Tgstation.Server.Host.Extensions const string SectionFieldName = nameof(GeneralConfiguration.Section); var configType = typeof(TConfig); - var sectionField = configType.GetField(SectionFieldName); - if (sectionField == null) - throw new InvalidOperationException(String.Format(CultureInfo.InvariantCulture, "{0} has no {1} field!", configType, SectionFieldName)); - + var sectionField = configType.GetField(SectionFieldName) ?? throw new InvalidOperationException( + String.Format(CultureInfo.InvariantCulture, "{0} has no {1} field!", configType, SectionFieldName)); var stringType = typeof(string); if (sectionField.FieldType != stringType) throw new InvalidOperationException(String.Format(CultureInfo.InvariantCulture, "{0} has invalid {1} field type, must be {2}!", configType, SectionFieldName, stringType)); diff --git a/src/Tgstation.Server.Host/Extensions/TaskExtensions.cs b/src/Tgstation.Server.Host/Extensions/TaskExtensions.cs index 42cc4d843d..a8ee467d1e 100644 --- a/src/Tgstation.Server.Host/Extensions/TaskExtensions.cs +++ b/src/Tgstation.Server.Host/Extensions/TaskExtensions.cs @@ -9,6 +9,11 @@ namespace Tgstation.Server.Host.Extensions /// static class TaskExtensions { + /// + /// A that never completes. + /// + static readonly TaskCompletionSource InfiniteTaskCompletionSource = new (); + /// /// Create a that can be awaited while respecting a given . /// @@ -41,7 +46,7 @@ namespace Tgstation.Server.Host.Extensions if (task == null) throw new ArgumentNullException(nameof(task)); - var cancelTcs = new TaskCompletionSource(); + var cancelTcs = new TaskCompletionSource(); using (cancellationToken.Register(() => cancelTcs.SetCanceled())) await Task.WhenAny(task, cancelTcs.Task); cancellationToken.ThrowIfCancellationRequested(); @@ -53,6 +58,6 @@ namespace Tgstation.Server.Host.Extensions /// Creates a that never completes. /// /// A never ending . - public static Task InfiniteTask() => new TaskCompletionSource().Task; + public static Task InfiniteTask() => InfiniteTaskCompletionSource.Task; } } diff --git a/src/Tgstation.Server.Host/Jobs/JobManager.cs b/src/Tgstation.Server.Host/Jobs/JobManager.cs index 77fd1d1211..f40bdaa13a 100644 --- a/src/Tgstation.Server.Host/Jobs/JobManager.cs +++ b/src/Tgstation.Server.Host/Jobs/JobManager.cs @@ -44,9 +44,9 @@ namespace Tgstation.Server.Host.Jobs readonly Dictionary jobs; /// - /// to delay starting jobs until the server is ready. + /// to delay starting jobs until the server is ready. /// - readonly TaskCompletionSource activationTcs; + readonly TaskCompletionSource activationTcs; /// /// for various operations. @@ -76,7 +76,7 @@ namespace Tgstation.Server.Host.Jobs this.loggerFactory = loggerFactory ?? throw new ArgumentNullException(nameof(loggerFactory)); this.logger = logger ?? throw new ArgumentNullException(nameof(logger)); jobs = new Dictionary(); - activationTcs = new TaskCompletionSource(); + activationTcs = new TaskCompletionSource(); synchronizationLock = new object(); addCancelLock = new object(); } @@ -268,7 +268,7 @@ namespace Tgstation.Server.Host.Jobs public void Activate() { logger.LogTrace("Activating job manager..."); - activationTcs.SetResult(null); + activationTcs.SetResult(); } /// diff --git a/src/Tgstation.Server.Host/Server.cs b/src/Tgstation.Server.Host/Server.cs index f4e6c195b7..0a3ca3b007 100644 --- a/src/Tgstation.Server.Host/Server.cs +++ b/src/Tgstation.Server.Host/Server.cs @@ -34,6 +34,11 @@ namespace Tgstation.Server.Host updatePath != null; #endif + /// + /// The of the running server. + /// + internal IHost Host { get; private set; } + /// /// The for the . /// @@ -119,26 +124,35 @@ namespace Tgstation.Server.Host fsWatcher.EnableRaisingEvents = true; } - using var host = hostBuilder.Build(); try { - swarmService = host.Services.GetRequiredService(); - logger = host.Services.GetRequiredService>(); - using (cancellationToken.Register(() => logger.LogInformation("Server termination requested!"))) + using (Host = hostBuilder.Build()) { - var generalConfigurationOptions = host.Services.GetRequiredService>(); - generalConfiguration = generalConfigurationOptions.Value; - await host.RunAsync(cancellationTokenSource.Token); + try + { + swarmService = Host.Services.GetRequiredService(); + logger = Host.Services.GetRequiredService>(); + using (cancellationToken.Register(() => logger.LogInformation("Server termination requested!"))) + { + var generalConfigurationOptions = Host.Services.GetRequiredService>(); + generalConfiguration = generalConfigurationOptions.Value; + await Host.RunAsync(cancellationTokenSource.Token); + } + } + catch (OperationCanceledException ex) + { + logger?.LogDebug(ex, "Server run cancelled!"); + } + catch (Exception ex) + { + CheckExceptionPropagation(ex); + throw; + } } } - catch (OperationCanceledException ex) + finally { - logger?.LogDebug(ex, "Server run cancelled!"); - } - catch (Exception ex) - { - CheckExceptionPropagation(ex); - throw; + Host = null; } } diff --git a/src/Tgstation.Server.Host/Setup/SetupWizard.cs b/src/Tgstation.Server.Host/Setup/SetupWizard.cs index 53d31df891..b446cd81b2 100644 --- a/src/Tgstation.Server.Host/Setup/SetupWizard.cs +++ b/src/Tgstation.Server.Host/Setup/SetupWizard.cs @@ -80,9 +80,9 @@ namespace Tgstation.Server.Host.Setup readonly GeneralConfiguration generalConfiguration; /// - /// A that will complete when the is reloaded. + /// A that will complete when the is reloaded. /// - TaskCompletionSource reloadTcs; + TaskCompletionSource reloadTcs; /// /// Initializes a new instance of the class. @@ -125,7 +125,7 @@ namespace Tgstation.Server.Host.Setup configuration .GetReloadToken() .RegisterChangeCallback( - state => reloadTcs?.TrySetResult(null), + state => reloadTcs?.TrySetResult(), null); } @@ -942,7 +942,7 @@ namespace Tgstation.Server.Host.Setup var configBytes = Encoding.UTF8.GetBytes(serializedYaml); - reloadTcs = new TaskCompletionSource(); + reloadTcs = new TaskCompletionSource(); try { diff --git a/src/Tgstation.Server.Host/Swarm/SwarmService.cs b/src/Tgstation.Server.Host/Swarm/SwarmService.cs index 7831d29253..ca584b9cfb 100644 --- a/src/Tgstation.Server.Host/Swarm/SwarmService.cs +++ b/src/Tgstation.Server.Host/Swarm/SwarmService.cs @@ -51,7 +51,7 @@ namespace Tgstation.Server.Host.Swarm /// /// See for the swarm system. /// - static readonly JsonSerializerSettings SerializerSettings = new JsonSerializerSettings + static readonly JsonSerializerSettings SerializerSettings = new () { ContractResolver = new DefaultContractResolver { @@ -142,9 +142,9 @@ namespace Tgstation.Server.Host.Swarm readonly bool swarmController; /// - /// A that is used to force a health check. + /// A that is used to force a health check. /// - TaskCompletionSource forceHealthCheckTcs; + TaskCompletionSource forceHealthCheckTcs; /// /// The that is used to proceed with committing an update. @@ -233,7 +233,7 @@ namespace Tgstation.Server.Host.Swarm if (SwarmMode) { serverHealthCheckCancellationTokenSource = new CancellationTokenSource(); - forceHealthCheckTcs = new TaskCompletionSource(); + forceHealthCheckTcs = new TaskCompletionSource(); if (swarmController) registrationIds = new Dictionary(); @@ -917,7 +917,7 @@ namespace Tgstation.Server.Host.Swarm response.EnsureSuccessStatusCode(); return; } - catch (Exception ex) when (!(ex is OperationCanceledException)) + catch (Exception ex) when (ex is not OperationCanceledException) { logger.LogWarning( ex, @@ -963,8 +963,8 @@ namespace Tgstation.Server.Host.Swarm bool TriggerHealthCheck() { var currentTcs = forceHealthCheckTcs; - forceHealthCheckTcs = new TaskCompletionSource(); - return currentTcs.TrySetResult(null); + forceHealthCheckTcs = new TaskCompletionSource(); + return currentTcs.TrySetResult(); } /// @@ -1068,7 +1068,7 @@ namespace Tgstation.Server.Host.Swarm logger.LogWarning("Error registering with swarm controller: HTTP {0}", response.StatusCode); try { - var responseData = await response.Content.ReadAsStringAsync(); + var responseData = await response.Content.ReadAsStringAsync(cancellationToken); if (!String.IsNullOrWhiteSpace(responseData)) logger.LogDebug("Response:{0}{1}", Environment.NewLine, responseData); } @@ -1114,7 +1114,7 @@ namespace Tgstation.Server.Host.Swarm using var response = await httpClient.SendAsync(request, cancellationToken); response.EnsureSuccessStatusCode(); } - catch (Exception ex) when (!(ex is OperationCanceledException)) + catch (Exception ex) when (ex is not OperationCanceledException) { logger.LogWarning(ex, "Error during swarm server list update for node '{0}'! Unregistering...", swarmServer.Identifier); @@ -1268,7 +1268,7 @@ namespace Tgstation.Server.Host.Swarm else await HealthCheckController(cancellationToken); } - catch (Exception ex) when (!(ex is OperationCanceledException)) + catch (Exception ex) when (ex is not OperationCanceledException) { logger.LogError(ex, "Health check error!"); } diff --git a/src/Tgstation.Server.Host/Tgstation.Server.Host.csproj b/src/Tgstation.Server.Host/Tgstation.Server.Host.csproj index 438b92c003..ee0278d141 100644 --- a/src/Tgstation.Server.Host/Tgstation.Server.Host.csproj +++ b/src/Tgstation.Server.Host/Tgstation.Server.Host.csproj @@ -61,45 +61,79 @@ + + + + + + + + + + + + all runtime; build; native; contentfiles; analyzers; buildtransitive + + - + + + + - + + + + + + + + + + + all runtime; build; native; contentfiles; analyzers; buildtransitive + + + + - + + + + diff --git a/src/Tgstation.Server.Host/Transfer/FileUploadProvider.cs b/src/Tgstation.Server.Host/Transfer/FileUploadProvider.cs index 298c22bc1b..cb4961fcbd 100644 --- a/src/Tgstation.Server.Host/Transfer/FileUploadProvider.cs +++ b/src/Tgstation.Server.Host/Transfer/FileUploadProvider.cs @@ -29,9 +29,9 @@ namespace Tgstation.Server.Host.Transfer readonly TaskCompletionSource taskCompletionSource; /// - /// The that completes in or when is called. + /// The that completes in or when is called. /// - readonly TaskCompletionSource completionTcs; + readonly TaskCompletionSource completionTcs; /// /// If synchronous IO is required. Uses a as a backend if set. @@ -54,7 +54,7 @@ namespace Tgstation.Server.Host.Transfer ticketExpiryCts = new CancellationTokenSource(); taskCompletionSource = new TaskCompletionSource(); - completionTcs = new TaskCompletionSource(); + completionTcs = new TaskCompletionSource(); this.requireSynchronousIO = requireSynchronousIO; } @@ -62,7 +62,7 @@ namespace Tgstation.Server.Host.Transfer public void Dispose() { ticketExpiryCts.Dispose(); - completionTcs.TrySetResult(null); + completionTcs.TrySetResult(); } /// @@ -125,7 +125,7 @@ namespace Tgstation.Server.Host.Transfer throw new InvalidOperationException("ErrorMessage already set!"); this.errorMessage = errorMessage; - completionTcs.TrySetResult(null); + completionTcs.TrySetResult(); } } } diff --git a/tests/Tgstation.Server.Host.Tests.Signals/Program.cs b/tests/Tgstation.Server.Host.Tests.Signals/Program.cs index 95d91ef80d..16aa655fab 100644 --- a/tests/Tgstation.Server.Host.Tests.Signals/Program.cs +++ b/tests/Tgstation.Server.Host.Tests.Signals/Program.cs @@ -18,10 +18,10 @@ namespace Tgstation.Server.Host.Tests.Signals { var mockServerControl = new Mock(); - var tcs = new TaskCompletionSource(); + var tcs = new TaskCompletionSource(); mockServerControl .Setup(x => x.GracefulShutdown()) - .Callback(() => tcs.SetResult(null)) + .Callback(() => tcs.SetResult()) .Returns(Task.CompletedTask); var mockAsyncDelayer = new Mock(); diff --git a/tests/Tgstation.Server.Host.Tests/IO/TestIOManager.cs b/tests/Tgstation.Server.Host.Tests/IO/TestIOManager.cs index 942643b2c2..d18400d728 100644 --- a/tests/Tgstation.Server.Host.Tests/IO/TestIOManager.cs +++ b/tests/Tgstation.Server.Host.Tests/IO/TestIOManager.cs @@ -20,13 +20,17 @@ namespace Tgstation.Server.Host.IO.Tests Directory.CreateDirectory(tempPath); try { + await File.WriteAllTextAsync(Path.Combine(tempPath, "file.txt"), "asdf"); + var subDir = Path.Combine(tempPath, "subdir"); + Directory.CreateDirectory(subDir); + await File.WriteAllTextAsync(Path.Combine(subDir, "file2.txt"), "fdsa"); await ioManager.DeleteDirectory(tempPath, default); Assert.IsFalse(Directory.Exists(tempPath)); } catch { - Directory.Delete(tempPath); + Directory.Delete(tempPath, true); throw; } } diff --git a/tests/Tgstation.Server.Host.Tests/Jobs/TestJobHandler.cs b/tests/Tgstation.Server.Host.Tests/Jobs/TestJobHandler.cs index d0b129f869..3f8e8663d8 100644 --- a/tests/Tgstation.Server.Host.Tests/Jobs/TestJobHandler.cs +++ b/tests/Tgstation.Server.Host.Tests/Jobs/TestJobHandler.cs @@ -35,14 +35,14 @@ namespace Tgstation.Server.Host.Jobs.Tests //test with a cancelled cts using (var cts = new CancellationTokenSource()) { - var tcs = new TaskCompletionSource(); + var tcs = new TaskCompletionSource(); currentWaitTask = tcs.Task; cts.Cancel(); using var handler = new JobHandler(TestJob); await Assert.ThrowsExceptionAsync(() => handler.Wait(cts.Token)); handler.Start(); await Assert.ThrowsExceptionAsync(() => handler.Wait(cts.Token)); - tcs.SetResult(null); + tcs.SetResult(); await handler.Wait(default); } Assert.IsFalse(cancelled); @@ -65,14 +65,14 @@ namespace Tgstation.Server.Host.Jobs.Tests [TestMethod] public async Task TestCancellation() { - var tcs = new TaskCompletionSource(); + var tcs = new TaskCompletionSource(); currentWaitTask = tcs.Task; cancelled = false; using (var handler = new JobHandler(TestJob)) { handler.Start(); handler.Cancel(); - tcs.SetResult(null); + tcs.SetResult(); await handler.Wait(default); } Assert.IsTrue(cancelled); diff --git a/tests/Tgstation.Server.Tests/Instance/InstanceTest.cs b/tests/Tgstation.Server.Tests/Instance/InstanceTest.cs index 0c20e778a4..ebd46b7bea 100644 --- a/tests/Tgstation.Server.Tests/Instance/InstanceTest.cs +++ b/tests/Tgstation.Server.Tests/Instance/InstanceTest.cs @@ -4,6 +4,7 @@ using System.Threading.Tasks; using Tgstation.Server.Client; using Tgstation.Server.Client.Components; +using Tgstation.Server.Host.Components; namespace Tgstation.Server.Tests.Instance { @@ -11,11 +12,13 @@ namespace Tgstation.Server.Tests.Instance { readonly IInstanceClient instanceClient; readonly IInstanceManagerClient instanceManagerClient; + readonly IInstanceManager instanceManager; - public InstanceTest(IInstanceClient instanceClient, IInstanceManagerClient instanceManagerClient) + public InstanceTest(IInstanceClient instanceClient, IInstanceManagerClient instanceManagerClient, IInstanceManager instanceManager) { this.instanceClient = instanceClient ?? throw new ArgumentNullException(nameof(instanceClient)); this.instanceManagerClient = instanceManagerClient ?? throw new ArgumentNullException(nameof(instanceManagerClient)); + this.instanceManager = instanceManager ?? throw new ArgumentNullException(nameof(instanceManager)); } public async Task RunTests(CancellationToken cancellationToken) @@ -35,7 +38,7 @@ namespace Tgstation.Server.Tests.Instance await configTest.Run(cancellationToken); await chatTests; await repoTests; - await new WatchdogTest(instanceClient).Run(cancellationToken); + await new WatchdogTest(instanceClient, instanceManager).Run(cancellationToken); } } } diff --git a/tests/Tgstation.Server.Tests/Instance/WatchdogTest.cs b/tests/Tgstation.Server.Tests/Instance/WatchdogTest.cs index 1c350fda37..4bbb52d663 100644 --- a/tests/Tgstation.Server.Tests/Instance/WatchdogTest.cs +++ b/tests/Tgstation.Server.Tests/Instance/WatchdogTest.cs @@ -1,8 +1,11 @@ using Byond.TopicSender; + using Microsoft.Extensions.Logging; using Microsoft.VisualStudio.TestTools.UnitTesting; using Moq; + using System; +using System.Globalization; using System.IO; using System.Linq; using System.Net; @@ -17,7 +20,9 @@ using Tgstation.Server.Api.Models.Request; using Tgstation.Server.Api.Models.Response; using Tgstation.Server.Client; using Tgstation.Server.Client.Components; +using Tgstation.Server.Host.Components; using Tgstation.Server.Host.Components.Interop; +using Tgstation.Server.Host.Components.Watchdog; using Tgstation.Server.Host.Extensions; using Tgstation.Server.Host.IO; using Tgstation.Server.Host.System; @@ -27,13 +32,15 @@ namespace Tgstation.Server.Tests.Instance sealed class WatchdogTest : JobsRequiredTest { readonly IInstanceClient instanceClient; + readonly IInstanceManager instanceManager; bool ranTimeoutTest = false; - public WatchdogTest(IInstanceClient instanceClient) + public WatchdogTest(IInstanceClient instanceClient, IInstanceManager instanceManager) : base(instanceClient.Jobs) { this.instanceClient = instanceClient ?? throw new ArgumentNullException(nameof(instanceClient)); + this.instanceManager = instanceManager ?? throw new ArgumentNullException(nameof(instanceManager)); } public async Task Run(CancellationToken cancellationToken) @@ -66,6 +73,9 @@ namespace Tgstation.Server.Tests.Instance await TestDMApiFreeDeploy(cancellationToken); await RunLongRunningTestThenUpdate(cancellationToken); + + await GhettoChatCommandTest(cancellationToken); + await RunLongRunningTestThenUpdateWithNewDme(cancellationToken); await RunLongRunningTestThenUpdateWithByondVersionSwitch(cancellationToken); @@ -91,11 +101,11 @@ namespace Tgstation.Server.Tests.Instance File.Delete(dumpFiles.Single()); KillDD(true); - TaskCompletionSource jobTcs = new TaskCompletionSource(); - TaskCompletionSource killTaskStarted = new TaskCompletionSource(); + var jobTcs = new TaskCompletionSource(); + var killTaskStarted = new TaskCompletionSource(); var killTask = Task.Run(() => { - killTaskStarted.SetResult(null); + killTaskStarted.SetResult(); while (!jobTcs.Task.IsCompleted) KillDD(false); }, cancellationToken); @@ -109,7 +119,7 @@ namespace Tgstation.Server.Tests.Instance } finally { - jobTcs.SetResult(null); + jobTcs.SetResult(); await killTask; } Assert.IsTrue(job.ErrorCode == ErrorCode.DreamDaemonOffline || job.ErrorCode == ErrorCode.GCoreFailure, $"{job.ErrorCode}: {job.ExceptionDetails}"); @@ -318,6 +328,66 @@ namespace Tgstation.Server.Tests.Instance return await instanceClient.DreamDaemon.Start(cancellationToken); } + async Task GhettoChatCommandTest(CancellationToken cancellationToken) + { + var startJob = await StartDD(cancellationToken); + + await WaitForJob(startJob, 40, false, null, cancellationToken); + + // oh god, oh fuck, blackbox testing + MessageContent response; + var startTime = DateTimeOffset.UtcNow - TimeSpan.FromSeconds(5); + using (var instanceReference = instanceManager.GetInstanceReference(instanceClient.Metadata)) + { + response = await ((BasicWatchdog)instanceReference.Watchdog).HandleChatCommand( + "embeds_test", + String.Empty, + new Host.Components.Chat.ChatUser + { + Channel = new Host.Components.Chat.ChannelRepresentation + { + IsAdminChannel = true, + ConnectionName = "test_connection", + EmbedsSupported = true, + FriendlyName = "Test Connection", + Id = "test_channel_id", + IsPrivateChannel = false, + }, + FriendlyName = "Test Sender", + Id = "test_user_id", + Mention = "test_user_mention", + RealId = 1234, + }, + cancellationToken); + } + + var endTime = DateTimeOffset.UtcNow + TimeSpan.FromSeconds(5); + + Assert.IsNotNull(response); + Assert.AreEqual("Embed support test2", response.Text); + Assert.AreEqual("desc", response.Embed.Description); + Assert.AreEqual("title", response.Embed.Title); + Assert.AreEqual("#0000FF", response.Embed.Colour); + Assert.AreEqual("Dominion", response.Embed.Author?.Name); + Assert.AreEqual("https://github.com/Cyberboss", response.Embed.Author.Url); + Assert.IsTrue(DateTimeOffset.TryParse(response.Embed.Timestamp, CultureInfo.InvariantCulture, DateTimeStyles.AssumeLocal, out var timestamp)); + Assert.IsTrue(startTime < timestamp && endTime > timestamp); + Assert.AreEqual("https://github.com/tgstation/tgstation-server", response.Embed.Url); + Assert.AreEqual(3, response.Embed.Fields?.Count); + Assert.AreEqual("field1", response.Embed.Fields.ElementAt(0).Name); + Assert.AreEqual("value1", response.Embed.Fields.ElementAt(0).Value); + Assert.IsNull(response.Embed.Fields.ElementAt(0).IsInline); + Assert.AreEqual("field2", response.Embed.Fields.ElementAt(1).Name); + Assert.AreEqual("value2", response.Embed.Fields.ElementAt(1).Value); + Assert.IsTrue(response.Embed.Fields.ElementAt(1).IsInline); + Assert.AreEqual("field3", response.Embed.Fields.ElementAt(2).Name); + Assert.AreEqual("value3", response.Embed.Fields.ElementAt(2).Value); + Assert.IsTrue(response.Embed.Fields.ElementAt(2).IsInline); + Assert.AreEqual("Footer text", response.Embed.Footer?.Text); + + await instanceClient.DreamDaemon.Shutdown(cancellationToken); + } + async Task RunLongRunningTestThenUpdate(CancellationToken cancellationToken) { global::System.Console.WriteLine("TEST: WATCHDOG LONG RUNNING WITH UPDATE TEST"); diff --git a/tests/Tgstation.Server.Tests/IntegrationTest.cs b/tests/Tgstation.Server.Tests/IntegrationTest.cs index f45fef48bc..f14acea7be 100644 --- a/tests/Tgstation.Server.Tests/IntegrationTest.cs +++ b/tests/Tgstation.Server.Tests/IntegrationTest.cs @@ -27,6 +27,8 @@ using Tgstation.Server.Api.Models.Response; using Tgstation.Server.Api.Rights; using Tgstation.Server.Client; using Tgstation.Server.Client.Components; +using Tgstation.Server.Host; +using Tgstation.Server.Host.Components; using Tgstation.Server.Host.Components.Events; using Tgstation.Server.Host.Components.Repository; using Tgstation.Server.Host.Configuration; @@ -827,6 +829,8 @@ namespace Tgstation.Server.Tests TerminateAllDDs(); + IInstanceManager GetInstanceManager() => ((Host.Server)server.RealServer).Host.Services.GetRequiredService(); + // main run var serverTask = server.Run(cancellationToken); @@ -874,7 +878,7 @@ namespace Tgstation.Server.Tests Assert.IsTrue(Directory.Exists(instanceClient.Metadata.Path)); - var instanceTests = FailFast(new InstanceTest(instanceClient, adminClient.Instances).RunTests(cancellationToken)); + var instanceTests = FailFast(new InstanceTest(instanceClient, adminClient.Instances, GetInstanceManager()).RunTests(cancellationToken)); await Task.WhenAll(rootTest, adminTest, instanceTests, usersTest); @@ -994,7 +998,7 @@ namespace Tgstation.Server.Tests Assert.AreEqual(WatchdogStatus.Online, dd.Status.Value); var compileJob = await instanceClient.DreamMaker.Compile(cancellationToken); - var wdt = new WatchdogTest(instanceClient); + var wdt = new WatchdogTest(instanceClient, GetInstanceManager()); await wdt.WaitForJob(compileJob, 30, false, null, cancellationToken); dd = await instanceClient.DreamDaemon.Read(cancellationToken); @@ -1041,7 +1045,7 @@ namespace Tgstation.Server.Tests Assert.AreEqual(WatchdogStatus.Online, currentDD.Status); Assert.AreEqual(expectedStaged, currentDD.StagedCompileJob.Job.Id.Value); - var wdt = new WatchdogTest(instanceClient); + var wdt = new WatchdogTest(instanceClient, GetInstanceManager()); currentDD = await wdt.TellWorldToReboot(cancellationToken); Assert.AreEqual(expectedStaged, currentDD.ActiveCompileJob.Job.Id.Value); Assert.IsNull(currentDD.StagedCompileJob); diff --git a/tests/Tgstation.Server.Tests/TestingServer.cs b/tests/Tgstation.Server.Tests/TestingServer.cs index 3490255754..971a90cd04 100644 --- a/tests/Tgstation.Server.Tests/TestingServer.cs +++ b/tests/Tgstation.Server.Tests/TestingServer.cs @@ -27,11 +27,11 @@ namespace Tgstation.Server.Tests public bool DumpOpenApiSpecpath { get; } - public bool RestartRequested => realServer.RestartRequested; + public bool RestartRequested => RealServer.RestartRequested; string[] args; - IServer realServer; + public IServer RealServer { get; private set; } public TestingServer(SwarmConfiguration swarmConfiguration, bool enableOAuth, ushort port = 5010) { @@ -154,8 +154,8 @@ namespace Tgstation.Server.Tests public async Task Run(CancellationToken cancellationToken) { Console.WriteLine("TEST SERVER START"); - var firstRun = realServer == null; - realServer = await Application + var firstRun = RealServer == null; + RealServer = await Application .CreateDefaultServerFactory() .CreateServer( args, @@ -169,7 +169,7 @@ namespace Tgstation.Server.Tests args = tmp.ToArray(); } - await realServer.Run(cancellationToken); + await RealServer.Run(cancellationToken); Console.WriteLine("TEST SERVER END"); } }