diff --git a/.github/CONTRIBUTING.md b/.github/CONTRIBUTING.md
index 79ee3f7ac3..934067d85f 100644
--- a/.github/CONTRIBUTING.md
+++ b/.github/CONTRIBUTING.md
@@ -38,15 +38,16 @@ You need the Dotnet 6.0 SDK and npm>=v5.7 (in your PATH) to compile the server.
The recommended IDE is Visual Studio 2019 which has installation options for both of these.
-In order to run the integration tests you must have the following environment variables set:
+In order to run the integration tests you must have the following environment variables set. To run them more accurately, include the optional ones.
- `TGS_TEST_DATABASE_TYPE`: `MySql`, `MariaDB`, `PostgresSql`, or `SqlServer`.
- `TGS_TEST_CONNECTION_STRING`: To a valid database connection string. You can use the setup wizard to create one.
-- `TSG_TEST_DISCORD_TOKEN`: To a valid discord bot token.
-- `TGS_TEST_DISCORD_CHANNEL`: To a valid discord channel ID that the above bot can access.
-- `TGS_TEST_IRC_CONNECTION_STRING`: To a valid IRC connection string. See the code for [IrcConnectionStringBuilder](../src/Tgstation.Server.Api/Models/IrcConnectionStringBuilder.cs) for details.
-- `TGS_TEST_IRC_CHANNEL`: To a valid IRC channel accessible with the above connection.
- `TGS_TEST_BRANCH`: Should be either `dev` or `master` depending on what you are working off of. Used for repository tests.
- (Optional) `TGS_TEST_GITHUB_TOKEN`: A GitHub personal access token with no scopes used to bypass rate limits.
+- (Optional) The following variables are all interdependent, so if one is set they all must be.
+ - `TSG_TEST_DISCORD_TOKEN`: To a valid discord bot token.
+ - `TGS_TEST_DISCORD_CHANNEL`: To a valid discord channel ID that the above bot can access.
+ - `TGS_TEST_IRC_CONNECTION_STRING`: To a valid IRC connection string. See the code for [IrcConnectionStringBuilder](../src/Tgstation.Server.Api/Models/IrcConnectionStringBuilder.cs) for details.
+ - `TGS_TEST_IRC_CHANNEL`: To a valid IRC channel accessible with the above connection.
### Know your Code
diff --git a/.github/workflows/ci-suite.yml b/.github/workflows/ci-suite.yml
index 9253d15ddd..5b934f08da 100644
--- a/.github/workflows/ci-suite.yml
+++ b/.github/workflows/ci-suite.yml
@@ -17,10 +17,6 @@ on:
env:
TGS_DOTNET_VERSION: 6.0.x
- TGS_TEST_DISCORD_CHANNEL: ${{ secrets.DISCORD_CHANNEL_ID }}
- TGS_TEST_DISCORD_TOKEN: ${{ secrets.DISCORD_TOKEN }}
- TGS_TEST_IRC_CHANNEL: ${{ secrets.IRC_CHANNEL }}
- TGS_TEST_IRC_CONNECTION_STRING: ${{ secrets.IRC_CONNECTION_STRING }}
TGS_TEST_GITHUB_TOKEN: ${{ secrets.LIVE_TESTS_TOKEN }}
TGS_RELEASE_NOTES_TOKEN: ${{ secrets.DEV_PUSH_TOKEN }}
@@ -231,6 +227,9 @@ jobs:
fail-fast: false
matrix:
configuration: [ 'Debug', 'Release' ]
+ env:
+ TGS_TEST_DISCORD_TOKEN: ${{ secrets.DISCORD_TOKEN }}
+ TGS_TEST_IRC_CONNECTION_STRING: ${{ secrets.IRC_CONNECTION_STRING }}
runs-on: ubuntu-latest
steps:
- name: Setup dotnet
@@ -271,6 +270,9 @@ jobs:
fail-fast: false
matrix:
configuration: [ 'Debug', 'Release' ]
+ env:
+ TGS_TEST_DISCORD_TOKEN: ${{ secrets.DISCORD_TOKEN }}
+ TGS_TEST_IRC_CONNECTION_STRING: ${{ secrets.IRC_CONNECTION_STRING }}
runs-on: windows-latest
steps:
- name: Setup dotnet
@@ -308,11 +310,11 @@ jobs:
needs: dmapi-build
if: "!(cancelled() || failure()) && needs.dmapi-build.result == 'success'"
env:
- TGS_TEST_DATABASE_TYPE: SqlServer
TGS_TEST_DUMP_API_SPEC: yes
strategy:
fail-fast: false
matrix:
+ database-type: [ 'SqlServer' ]
watchdog-type: [ 'Basic', 'System' ]
configuration: [ 'Debug', 'Release' ]
runs-on: windows-2019
@@ -329,11 +331,13 @@ jobs:
if: ${{ matrix.watchdog-type == 'Basic' }}
run: echo "General__UseBasicWatchdog=true" >> $Env:GITHUB_ENV
- - name: Set TGS_TEST_CONNECTION_STRING
+ - name: Set SqlServer Connection Info
+ if: ${{ matrix.database-type == 'SqlServer' }}
shell: bash
run: |
TGS_CONNSTRING_VALUE="Server=(localdb)\MSSQLLocalDB;Integrated Security=true;Initial Catalog=TGS_${{ matrix.watchdog-type }}_${{ matrix.configuration }};Application Name=tgstation-server"
echo "TGS_TEST_CONNECTION_STRING=$(echo $TGS_CONNSTRING_VALUE)" >> $GITHUB_ENV
+ echo "TGS_TEST_DATABASE_TYPE=SqlServer" >> $GITHUB_ENV
- name: Checkout (Branch Push)
uses: actions/checkout@v3
@@ -361,7 +365,7 @@ jobs:
path: ./TestResults/
- name: Store OpenAPI Spec
- if: ${{ matrix.configuration == 'Release' && matrix.watchdog-type == 'System' }}
+ if: ${{ matrix.configuration == 'Release' && matrix.watchdog-type == 'System' && matrix.database-type == 'SqlServer' }}
uses: actions/upload-artifact@v3
with:
name: openapi-spec
diff --git a/build/Version.props b/build/Version.props
index 7929f4741d..bb6a17d416 100644
--- a/build/Version.props
+++ b/build/Version.props
@@ -3,12 +3,12 @@
- 5.12.1
+ 5.12.2
4.6.0
9.10.2
10.4.1
11.4.2
- 6.4.3
+ 6.4.4
5.6.0
1.2.2
1.2.1
diff --git a/src/DMAPI/tgs.dm b/src/DMAPI/tgs.dm
index c562224c73..ab2d565991 100644
--- a/src/DMAPI/tgs.dm
+++ b/src/DMAPI/tgs.dm
@@ -1,6 +1,6 @@
// tgstation-server DMAPI
-#define TGS_DMAPI_VERSION "6.4.3"
+#define TGS_DMAPI_VERSION "6.4.4"
// All functions and datums outside this document are subject to change with any version and should not be relied on.
diff --git a/src/DMAPI/tgs/v4/api.dm b/src/DMAPI/tgs/v4/api.dm
index 2f05c38633..b9a75c4abb 100644
--- a/src/DMAPI/tgs/v4/api.dm
+++ b/src/DMAPI/tgs/v4/api.dm
@@ -263,7 +263,12 @@
for(var/I in channels)
var/datum/tgs_chat_channel/channel = I
ids += channel.id
+
message = UpgradeDeprecatedChatMessage(message)
+
+ if (!length(channels))
+ return
+
message = list("message" = message.text, "channelIds" = ids)
if(intercepted_message_queue)
intercepted_message_queue += list(message)
@@ -276,7 +281,12 @@
var/datum/tgs_chat_channel/channel = I
if (!channel.is_private_channel && ((channel.is_admin_channel && admin_only) || (!channel.is_admin_channel && !admin_only)))
channels += channel.id
+
message = UpgradeDeprecatedChatMessage(message)
+
+ if (!length(channels))
+ return
+
message = list("message" = message.text, "channelIds" = channels)
if(intercepted_message_queue)
intercepted_message_queue += list(message)
diff --git a/src/DMAPI/tgs/v5/api.dm b/src/DMAPI/tgs/v5/api.dm
index 517240f12f..926ea10a8f 100644
--- a/src/DMAPI/tgs/v5/api.dm
+++ b/src/DMAPI/tgs/v5/api.dm
@@ -166,6 +166,10 @@
ids += channel.id
message = UpgradeDeprecatedChatMessage(message)
+
+ if (!length(channels))
+ return
+
message = message._interop_serialize()
message[DMAPI5_CHAT_MESSAGE_CHANNEL_IDS] = ids
if(intercepted_message_queue)
@@ -181,6 +185,10 @@
channels += channel.id
message = UpgradeDeprecatedChatMessage(message)
+
+ if (!length(channels))
+ return
+
message = message._interop_serialize()
message[DMAPI5_CHAT_MESSAGE_CHANNEL_IDS] = channels
if(intercepted_message_queue)
@@ -199,6 +207,7 @@
/datum/tgs_api/v5/ChatChannelInfo()
RequireInitialBridgeResponse()
+ WaitForReattach(TRUE)
return chat_channels.Copy()
/datum/tgs_api/v5/proc/DecodeChannels(chat_update_json)
diff --git a/src/DMAPI/tgs/v5/bridge.dm b/src/DMAPI/tgs/v5/bridge.dm
index b3cf775939..37f58bcdf6 100644
--- a/src/DMAPI/tgs/v5/bridge.dm
+++ b/src/DMAPI/tgs/v5/bridge.dm
@@ -59,18 +59,22 @@
var/json = json_encode(data)
return json
-/datum/tgs_api/v5/proc/PerformBridgeRequest(bridge_request)
+/datum/tgs_api/v5/proc/WaitForReattach(require_channels = FALSE)
if(detached)
// Wait up to one minute
for(var/i in 1 to 600)
sleep(1)
- if(!detached)
+ if(!detached && (!require_channels || length(chat_channels)))
break
- // dad went out for milk cigarettes 20 years ago...
+ // dad went out for milk and cigarettes 20 years ago...
+ // yes, this affects all other waiters, intentional
if(i == 600)
detached = FALSE
+/datum/tgs_api/v5/proc/PerformBridgeRequest(bridge_request)
+ WaitForReattach(FALSE)
+
// This is an infinite sleep until we get a response
var/export_response = world.Export(bridge_request)
if(!export_response)
diff --git a/src/DMAPI/tgs/v5/topic.dm b/src/DMAPI/tgs/v5/topic.dm
index 28fcc14aef..3779db6237 100644
--- a/src/DMAPI/tgs/v5/topic.dm
+++ b/src/DMAPI/tgs/v5/topic.dm
@@ -71,6 +71,7 @@
var/list/event_call = list(event_type)
if (event_type == TGS_EVENT_WATCHDOG_DETACH)
detached = TRUE
+ chat_channels.Cut() // https://github.com/tgstation/tgstation-server/issues/1490
if(event_parameters)
event_call += event_parameters
diff --git a/src/Tgstation.Server.Host/Components/Chat/ChannelRepresentation.cs b/src/Tgstation.Server.Host/Components/Chat/ChannelRepresentation.cs
index 239e68fa88..a24f416ec1 100644
--- a/src/Tgstation.Server.Host/Components/Chat/ChannelRepresentation.cs
+++ b/src/Tgstation.Server.Host/Components/Chat/ChannelRepresentation.cs
@@ -19,7 +19,7 @@ namespace Tgstation.Server.Host.Components.Chat
///
/// The channel Id.
///
- /// remaps this to an internal id using . Not sent over the DMAPI.
+ /// remaps this to an internal id using . Not sent over the DMAPI.
[JsonIgnore]
public ulong RealId
{
diff --git a/src/Tgstation.Server.Host/Components/Chat/ChatManager.cs b/src/Tgstation.Server.Host/Components/Chat/ChatManager.cs
index ddd0aff92b..5ae211107d 100644
--- a/src/Tgstation.Server.Host/Components/Chat/ChatManager.cs
+++ b/src/Tgstation.Server.Host/Components/Chat/ChatManager.cs
@@ -232,7 +232,6 @@ namespace Tgstation.Server.Host.Components.Chat
channelIdCounter += (ulong)results.Count;
}
- Task trackingContextUpdateTask;
lock (mappedChannels)
{
lock (providers)
@@ -245,16 +244,13 @@ namespace Tgstation.Server.Host.Components.Chat
mappedChannels.Add(newId, newMapping);
newMapping.Channel.RealId = newId;
}
-
- lock (trackingContexts)
- trackingContextUpdateTask = Task.WhenAll(
- trackingContexts.Select(
- x => x.UpdateChannels(
- mappedChannels.Select(y => y.Value.Channel).ToList(),
- cancellationToken)));
}
- await trackingContextUpdateTask;
+ // 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)
+ await UpdateTrackingContexts(cancellationToken);
}
finally
{
@@ -333,35 +329,33 @@ namespace Tgstation.Server.Host.Components.Chat
if (channelIds == null)
throw new ArgumentNullException(nameof(channelIds));
- var task = SendMessage(
- channelIds,
- null,
- message,
- handlerCts.Token);
- AddMessageTask(task);
+ QueueMessageInternal(message, () => channelIds, false);
}
///
- public async Task QueueWatchdogMessage(string message, CancellationToken cancellationToken)
+ public void QueueWatchdogMessage(string message)
{
- List wdChannels = null;
+ if (message == null)
+ throw new ArgumentNullException(nameof(message));
+
message = String.Format(CultureInfo.InvariantCulture, "WD: {0}", message);
if (!initialProviderConnectionsTask.IsCompleted)
logger.LogTrace("Waiting for initial provider connections before sending watchdog message...");
- await initialProviderConnectionsTask.WithToken(cancellationToken);
-
- // so it doesn't change while we're using it
- lock (mappedChannels)
- wdChannels = mappedChannels.Where(x => x.Value.IsWatchdogChannel).Select(x => x.Key).ToList();
-
- QueueMessage(
+ // Reimplementing QueueMessage
+ QueueMessageInternal(
new MessageContent
{
Text = message,
},
- wdChannels);
+ () =>
+ {
+ // so it doesn't change while we're using it
+ lock (mappedChannels)
+ return mappedChannels.Where(x => x.Value.IsWatchdogChannel).Select(x => x.Key).ToList();
+ },
+ true);
}
///
@@ -403,10 +397,10 @@ namespace Tgstation.Server.Host.Components.Chat
gitHubRepo,
channelMapping.ProviderChannelId,
localCommitPushed,
- handlerCts.Token)
- ;
+ handlerCts.Token);
- callbacks.Add(callback);
+ lock (callbacks)
+ callbacks.Add(callback);
}
catch (Exception ex)
{
@@ -419,12 +413,17 @@ namespace Tgstation.Server.Host.Components.Chat
AddMessageTask(task);
- return (errorMessage, dreamMakerOutput) => AddMessageTask(
- Task.WhenAll(
+ async Task CollateTasks(string errorMessage, string dreamMakerOutput)
+ {
+ await task;
+ await Task.WhenAll(
callbacks.Select(
x => x(
errorMessage,
- dreamMakerOutput))));
+ dreamMakerOutput)));
+ }
+
+ return (errorMessage, dreamMakerOutput) => AddMessageTask(CollateTasks(errorMessage, dreamMakerOutput));
}
///
@@ -472,6 +471,38 @@ namespace Tgstation.Server.Host.Components.Chat
return context;
}
+ ///
+ public async Task UpdateTrackingContexts(CancellationToken cancellationToken)
+ {
+ var logMessageSent = 0;
+ async Task UpdateTrackingContext(IChatTrackingContext channelSink, IEnumerable channels)
+ {
+ if (Interlocked.Exchange(ref logMessageSent, 1) == 0)
+
+ await channelSink.UpdateChannels(channels, cancellationToken);
+ }
+
+ var waitingForInitialConnection = !initialProviderConnectionsTask.IsCompleted;
+ if (waitingForInitialConnection)
+ {
+ logger.LogTrace("Waiting for initial chat bot connections before updating tracking contexts...");
+ await initialProviderConnectionsTask.WithToken(cancellationToken);
+ }
+
+ List tasks;
+ lock (mappedChannels)
+ lock (trackingContexts)
+ tasks = trackingContexts.Select(x => UpdateTrackingContext(x, mappedChannels.Select(y => y.Value.Channel))).ToList();
+
+ if (waitingForInitialConnection)
+ if (tasks.Count > 0)
+ logger.LogTrace("Updating chat tracking contexts...");
+ else
+ logger.LogTrace("No chat tracking contexts to update");
+
+ await Task.WhenAll(tasks);
+ }
+
///
public void RegisterCommandHandler(ICustomCommandHandler customCommandHandler)
{
@@ -492,13 +523,15 @@ namespace Tgstation.Server.Host.Components.Chat
{
await provider.Disconnect(cancellationToken);
}
- finally
+ catch (Exception ex)
{
- 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);
+ logger.LogError(ex, "Error disconnecting connection {connectionId}!", connectionId);
}
+
+ 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);
@@ -906,6 +939,12 @@ namespace Tgstation.Server.Host.Components.Chat
// process completed ones
foreach (var completedMessageTaskKvp in messageTasks.Where(x => x.Value.IsCompleted).ToList())
{
+ var provider = completedMessageTaskKvp.Key;
+ messageTasks.Remove(provider);
+
+ if (provider.Disposed) // valid to receive one, but don't process it
+ continue;
+
var message = await completedMessageTaskKvp.Value;
var messageNumber = Interlocked.Increment(ref messagesProcessed);
@@ -915,7 +954,7 @@ namespace Tgstation.Server.Host.Components.Chat
using (LogContext.PushProperty(SerilogContextHelper.ChatMessageIterationContextProperty, messageNumber))
try
{
- await ProcessMessage(completedMessageTaskKvp.Key, message, false, cancellationToken);
+ await ProcessMessage(provider, message, false, cancellationToken);
}
catch (Exception ex)
{
@@ -926,8 +965,6 @@ namespace Tgstation.Server.Host.Components.Chat
}
activeProcessingTask = WrapProcessMessage();
-
- messageTasks.Remove(completedMessageTaskKvp.Key);
}
}
}
@@ -957,12 +994,17 @@ namespace Tgstation.Server.Host.Components.Chat
/// A representing the running operation.
Task SendMessage(IEnumerable channelIds, Message replyTo, MessageContent message, CancellationToken cancellationToken)
{
+ channelIds = channelIds.ToList();
+
logger.LogTrace(
- "Chat send \"{message}\"{embed} to channels: {channelIdsCommaSeperated}",
+ "Chat send \"{message}\"{embed} to channels: [{channelIdsCommaSeperated}]",
message.Text,
message.Embed != null ? " (with embed)" : String.Empty,
String.Join(", ", channelIds));
+ if (!channelIds.Any())
+ return Task.CompletedTask;
+
return Task.WhenAll(
channelIds.Select(x =>
{
@@ -1001,14 +1043,42 @@ namespace Tgstation.Server.Host.Components.Chat
{
await task;
}
+ catch (OperationCanceledException ex)
+ {
+ logger.LogDebug(ex, "Async chat message cancelled!");
+ }
catch (Exception ex)
{
- logger.LogWarning(ex, "Error in asynchronous chat message!");
+ logger.LogError(ex, "Error in asynchronous chat message!");
}
}
lock (handlerCts)
messageSendTask = Wrap(messageSendTask);
}
+
+ ///
+ /// Adds a given to the send queue.
+ ///
+ /// The being sent.
+ /// A to retrieve he s of the s to send to.
+ /// If , the message send will wait for to complete before running.
+ void QueueMessageInternal(MessageContent message, Func> channelIdsFactory, bool waitForConnections)
+ {
+ async Task SendMessageTask()
+ {
+ var cancellationToken = handlerCts.Token;
+ if (waitForConnections)
+ await initialProviderConnectionsTask.WithToken(cancellationToken);
+
+ await SendMessage(
+ channelIdsFactory(),
+ null,
+ message,
+ cancellationToken);
+ }
+
+ AddMessageTask(SendMessageTask());
+ }
}
}
diff --git a/src/Tgstation.Server.Host/Components/Chat/ChatTrackingContext.cs b/src/Tgstation.Server.Host/Components/Chat/ChatTrackingContext.cs
index 8239bdb0e1..119fb5573f 100644
--- a/src/Tgstation.Server.Host/Components/Chat/ChatTrackingContext.cs
+++ b/src/Tgstation.Server.Host/Components/Chat/ChatTrackingContext.cs
@@ -21,7 +21,11 @@ namespace Tgstation.Server.Host.Components.Chat
{
if (active == value)
return;
- logger.LogTrace(value ? "Activated" : "Deactivated");
+ if (value)
+ logger.LogTrace("Activated");
+ else
+ logger.LogTrace("Deactivated");
+
active = value;
}
}
diff --git a/src/Tgstation.Server.Host/Components/Chat/IChatManager.cs b/src/Tgstation.Server.Host/Components/Chat/IChatManager.cs
index cb0265c334..59f3e2b1c5 100644
--- a/src/Tgstation.Server.Host/Components/Chat/IChatManager.cs
+++ b/src/Tgstation.Server.Host/Components/Chat/IChatManager.cs
@@ -57,9 +57,7 @@ namespace Tgstation.Server.Host.Components.Chat
/// Queue a chat to configured watchdog channels.
///
/// The message being sent.
- /// The for the operation.
- /// A representing the running operation.
- Task QueueWatchdogMessage(string message, CancellationToken cancellationToken);
+ void QueueWatchdogMessage(string message);
///
/// Send the message for a deployment to configured deployment channels.
@@ -84,5 +82,12 @@ namespace Tgstation.Server.Host.Components.Chat
///
/// A new .
IChatTrackingContext CreateTrackingContext();
+
+ ///
+ /// Force an update with the active channels on all active s.
+ ///
+ /// The for the operation.
+ /// A representing the running operation.
+ Task UpdateTrackingContexts(CancellationToken cancellationToken);
}
}
diff --git a/src/Tgstation.Server.Host/Components/Chat/Providers/DiscordProvider.cs b/src/Tgstation.Server.Host/Components/Chat/Providers/DiscordProvider.cs
index 1640d28516..b15ccc02ef 100644
--- a/src/Tgstation.Server.Host/Components/Chat/Providers/DiscordProvider.cs
+++ b/src/Tgstation.Server.Host/Components/Chat/Providers/DiscordProvider.cs
@@ -27,6 +27,7 @@ using Tgstation.Server.Host.Extensions;
using Tgstation.Server.Host.Jobs;
using Tgstation.Server.Host.Models;
using Tgstation.Server.Host.System;
+using Tgstation.Server.Host.Utils;
namespace Tgstation.Server.Host.Components.Chat.Providers
{
@@ -179,15 +180,17 @@ namespace Tgstation.Server.Host.Components.Chat.Providers
/// Initializes a new instance of the class.
///
/// The for the .
- /// The value of .
+ /// The for the .
/// The for the .
+ /// The value of .
/// The for the .
public DiscordProvider(
IJobManager jobManager,
- IAssemblyInformationProvider assemblyInformationProvider,
+ IAsyncDelayer asyncDelayer,
ILogger logger,
+ IAssemblyInformationProvider assemblyInformationProvider,
ChatBot chatBot)
- : base(jobManager, logger, chatBot)
+ : base(jobManager, asyncDelayer, logger, chatBot)
{
this.assemblyInformationProvider = assemblyInformationProvider ?? throw new ArgumentNullException(nameof(assemblyInformationProvider));
@@ -232,6 +235,9 @@ namespace Tgstation.Server.Host.Components.Chat.Providers
///
public override async Task SendMessage(Message replyTo, MessageContent message, ulong channelId, CancellationToken cancellationToken)
{
+ if (message == null)
+ throw new ArgumentNullException(nameof(message));
+
Optional replyToReference = default;
Optional allowedMentions = default;
if (replyTo != null && replyTo is DiscordMessage discordMessage)
@@ -329,6 +335,15 @@ namespace Tgstation.Server.Host.Components.Chat.Providers
bool localCommitPushed,
CancellationToken cancellationToken)
{
+ if (revisionInformation == null)
+ throw new ArgumentNullException(nameof(revisionInformation));
+ if (byondVersion == null)
+ throw new ArgumentNullException(nameof(byondVersion));
+ if (gitHubOwner == null)
+ throw new ArgumentNullException(nameof(gitHubOwner));
+ if (gitHubRepo == null)
+ throw new ArgumentNullException(nameof(gitHubRepo));
+
localCommitPushed |= revisionInformation.CommitSha == revisionInformation.OriginCommitSha;
var fields = BuildUpdateEmbedFields(revisionInformation, byondVersion, gitHubOwner, gitHubRepo, localCommitPushed);
@@ -356,8 +371,7 @@ namespace Tgstation.Server.Host.Components.Chat.Providers
new Snowflake(channelId),
"DM: Deployment in Progress...",
embeds: new List { embed },
- ct: cancellationToken)
- ;
+ ct: cancellationToken);
if (!messageResponse.IsSuccess)
Logger.LogWarning("Failed to post deploy embed to channel {channelId}: {result}", channelId, messageResponse.LogFormat());
@@ -414,8 +428,7 @@ namespace Tgstation.Server.Host.Components.Chat.Providers
new Snowflake(channelId),
updatedMessage,
embeds: new List { embed },
- ct: cancellationToken)
- ;
+ ct: cancellationToken);
if (!createUpdatedMessageResponse.IsSuccess)
Logger.LogWarning(
@@ -432,8 +445,7 @@ namespace Tgstation.Server.Host.Components.Chat.Providers
messageResponse.Entity.ID,
updatedMessage,
embeds: new List { embed },
- ct: cancellationToken)
- ;
+ ct: cancellationToken);
if (!editResponse.IsSuccess)
{
diff --git a/src/Tgstation.Server.Host/Components/Chat/Providers/IrcProvider.cs b/src/Tgstation.Server.Host/Components/Chat/Providers/IrcProvider.cs
index b0563406ea..179105d248 100644
--- a/src/Tgstation.Server.Host/Components/Chat/Providers/IrcProvider.cs
+++ b/src/Tgstation.Server.Host/Components/Chat/Providers/IrcProvider.cs
@@ -42,11 +42,6 @@ namespace Tgstation.Server.Host.Components.Chat.Providers
///
public override string BotMention => client.Nickname;
- ///
- /// The for the .
- ///
- readonly IAsyncDelayer asyncDelayer;
-
///
/// The client.
///
@@ -105,24 +100,22 @@ namespace Tgstation.Server.Host.Components.Chat.Providers
///
/// Initializes a new instance of the class.
///
- /// The for the provider.
- /// The to get the from.
- /// The value of .
+ /// The for the .
+ /// The for the .
/// The for the .
+ /// The to get the from.
/// The for the .
public IrcProvider(
IJobManager jobManager,
- IAssemblyInformationProvider assemblyInformationProvider,
IAsyncDelayer asyncDelayer,
ILogger logger,
+ IAssemblyInformationProvider assemblyInformationProvider,
Models.ChatBot chatBot)
- : base(jobManager, logger, chatBot)
+ : base(jobManager, asyncDelayer, logger, chatBot)
{
if (assemblyInformationProvider == null)
throw new ArgumentNullException(nameof(assemblyInformationProvider));
- this.asyncDelayer = asyncDelayer ?? throw new ArgumentNullException(nameof(asyncDelayer));
-
var builder = chatBot.CreateConnectionStringBuilder();
if (builder == null || !builder.Valid || builder is not IrcConnectionStringBuilder ircBuilder)
throw new InvalidOperationException("Invalid ChatConnectionStringBuilder!");
@@ -172,53 +165,59 @@ namespace Tgstation.Server.Host.Components.Chat.Providers
}
///
- public override Task SendMessage(Message replyTo, MessageContent message, ulong channelId, CancellationToken cancellationToken) => Task.Factory.StartNew(
- () =>
- {
- // IRC doesn't allow newlines
- // Explicitly ignore embeds
- var messageText = message.Text;
- messageText ??= $"Embed Only: {JsonConvert.SerializeObject(message.Embed)}";
+ public override Task SendMessage(Message replyTo, MessageContent message, ulong channelId, CancellationToken cancellationToken)
+ {
+ if (message == null)
+ throw new ArgumentNullException(nameof(message));
- messageText = String.Concat(
- messageText
- .Where(x => x != '\r')
- .Select(x => x == '\n' ? '|' : x));
-
- var channelName = channelIdMap[channelId];
- SendType sendType;
- if (channelName == null)
+ return Task.Factory.StartNew(
+ () =>
{
- channelName = queryChannelIdMap[channelId];
- sendType = SendType.Notice;
- }
- else
- sendType = SendType.Message;
+ // IRC doesn't allow newlines
+ // Explicitly ignore embeds
+ var messageText = message.Text;
+ messageText ??= $"Embed Only: {JsonConvert.SerializeObject(message.Embed)}";
- var messageSize = Encoding.UTF8.GetByteCount(messageText) + Encoding.UTF8.GetByteCount(channelName) + PreambleMessageLength;
- var messageTooLong = messageSize > MessageBytesLimit;
- if (messageTooLong)
- messageText = $"TGS: Could not send message to IRC. Line write exceeded protocol limit of {MessageBytesLimit}B.";
+ messageText = String.Concat(
+ messageText
+ .Where(x => x != '\r')
+ .Select(x => x == '\n' ? '|' : x));
- try
- {
- client.SendMessage(sendType, channelName, messageText);
- }
- catch (Exception e)
- {
- Logger.LogWarning(e, "Unable to send to channel {channelName}!", channelName);
- return;
- }
+ var channelName = channelIdMap[channelId];
+ SendType sendType;
+ if (channelName == null)
+ {
+ channelName = queryChannelIdMap[channelId];
+ sendType = SendType.Notice;
+ }
+ else
+ sendType = SendType.Message;
- if (messageTooLong)
- Logger.LogWarning(
- "Failed to send to channel {channelId}: Message size ({messageSize}B) exceeds IRC limit of 512B",
- channelId,
- messageSize);
- },
- cancellationToken,
- DefaultIOManager.BlockingTaskCreationOptions,
- TaskScheduler.Current);
+ var messageSize = Encoding.UTF8.GetByteCount(messageText) + Encoding.UTF8.GetByteCount(channelName) + PreambleMessageLength;
+ var messageTooLong = messageSize > MessageBytesLimit;
+ if (messageTooLong)
+ messageText = $"TGS: Could not send message to IRC. Line write exceeded protocol limit of {MessageBytesLimit}B.";
+
+ try
+ {
+ client.SendMessage(sendType, channelName, messageText);
+ }
+ catch (Exception e)
+ {
+ Logger.LogWarning(e, "Unable to send to channel {channelName}!", channelName);
+ return;
+ }
+
+ if (messageTooLong)
+ Logger.LogWarning(
+ "Failed to send to channel {channelId}: Message size ({messageSize}B) exceeds IRC limit of 512B",
+ channelId,
+ messageSize);
+ },
+ cancellationToken,
+ DefaultIOManager.BlockingTaskCreationOptions,
+ TaskScheduler.Current);
+ }
///
public override async Task> SendUpdateMessage(
@@ -231,6 +230,15 @@ namespace Tgstation.Server.Host.Components.Chat.Providers
bool localCommitPushed,
CancellationToken cancellationToken)
{
+ if (revisionInformation == null)
+ throw new ArgumentNullException(nameof(revisionInformation));
+ if (byondVersion == null)
+ throw new ArgumentNullException(nameof(byondVersion));
+ if (gitHubOwner == null)
+ throw new ArgumentNullException(nameof(gitHubOwner));
+ if (gitHubRepo == null)
+ throw new ArgumentNullException(nameof(gitHubRepo));
+
var commitInsert = revisionInformation.CommitSha[..7];
string remoteCommitInsert;
if (revisionInformation.CommitSha == revisionInformation.OriginCommitSha)
@@ -626,14 +634,14 @@ namespace Tgstation.Server.Host.Components.Chat.Providers
var listenTimeSpan = TimeSpan.FromMilliseconds(10);
for (; !recievedAck;
- await asyncDelayer.Delay(listenTimeSpan, timeoutToken))
+ await AsyncDelayer.Delay(listenTimeSpan, timeoutToken))
await NonBlockingListen(cancellationToken);
client.WriteLine("AUTHENTICATE PLAIN", Priority.Critical);
timeoutToken.ThrowIfCancellationRequested();
for (; !recievedPlus;
- await asyncDelayer.Delay(listenTimeSpan, timeoutToken))
+ await AsyncDelayer.Delay(listenTimeSpan, timeoutToken))
await NonBlockingListen(cancellationToken);
}
finally
@@ -699,8 +707,7 @@ namespace Tgstation.Server.Host.Components.Chat.Providers
Task.WhenAll(
disconnectTask,
listenTask ?? Task.CompletedTask),
- asyncDelayer.Delay(TimeSpan.FromSeconds(5), cancellationToken))
- ;
+ AsyncDelayer.Delay(TimeSpan.FromSeconds(5), cancellationToken));
}
}
}
diff --git a/src/Tgstation.Server.Host/Components/Chat/Providers/Provider.cs b/src/Tgstation.Server.Host/Components/Chat/Providers/Provider.cs
index 1e2bf22ff0..e13f46ade8 100644
--- a/src/Tgstation.Server.Host/Components/Chat/Providers/Provider.cs
+++ b/src/Tgstation.Server.Host/Components/Chat/Providers/Provider.cs
@@ -10,6 +10,7 @@ using Tgstation.Server.Host.Components.Interop;
using Tgstation.Server.Host.Extensions;
using Tgstation.Server.Host.Jobs;
using Tgstation.Server.Host.Models;
+using Tgstation.Server.Host.Utils;
namespace Tgstation.Server.Host.Components.Chat.Providers
{
@@ -24,6 +25,11 @@ namespace Tgstation.Server.Host.Components.Chat.Providers
///
protected ChatBot ChatBot { get; }
+ ///
+ /// The for the .
+ ///
+ protected IAsyncDelayer AsyncDelayer { get; }
+
///
/// The for the .
///
@@ -68,11 +74,13 @@ namespace Tgstation.Server.Host.Components.Chat.Providers
/// Initializes a new instance of the class.
///
/// The value of .
+ /// The value of .
/// The value of .
/// The value of .
- protected Provider(IJobManager jobManager, ILogger logger, ChatBot chatBot)
+ protected Provider(IJobManager jobManager, IAsyncDelayer asyncDelayer, ILogger logger, ChatBot chatBot)
{
this.jobManager = jobManager ?? throw new ArgumentNullException(nameof(jobManager));
+ AsyncDelayer = asyncDelayer ?? throw new ArgumentNullException(nameof(asyncDelayer));
Logger = logger ?? throw new ArgumentNullException(nameof(logger));
ChatBot = chatBot ?? throw new ArgumentNullException(nameof(chatBot));
@@ -98,6 +106,9 @@ namespace Tgstation.Server.Host.Components.Chat.Providers
{
Disposed = true;
await StopReconnectionTimer();
+
+ // queue a final message to shutdown the NextMessage Task
+ EnqueueMessage(null);
Logger.LogTrace("Disposed");
}
@@ -206,7 +217,7 @@ namespace Tgstation.Server.Host.Components.Chat.Providers
///
/// Queues a for .
///
- /// The to queue. A value of indicates the channel mappings a out of date.
+ /// The to queue. A value of indicates the channel mappings are out of date.
protected void EnqueueMessage(Message message)
{
if (message == null)
@@ -256,7 +267,7 @@ namespace Tgstation.Server.Host.Components.Chat.Providers
try
{
if (!connectNow)
- await Task.Delay(TimeSpan.FromMinutes(reconnectInterval), cancellationToken);
+ await AsyncDelayer.Delay(TimeSpan.FromMinutes(reconnectInterval), cancellationToken);
else
connectNow = false;
if (!Connected)
@@ -290,6 +301,7 @@ namespace Tgstation.Server.Host.Components.Chat.Providers
}
catch
{
+ // we set this here because otherwise there could be stuff waiting on to connect us forever
initialConnectionTcs.TrySetResult();
throw;
}
diff --git a/src/Tgstation.Server.Host/Components/Chat/Providers/ProviderFactory.cs b/src/Tgstation.Server.Host/Components/Chat/Providers/ProviderFactory.cs
index 6a7e0abd32..775db6c2e5 100644
--- a/src/Tgstation.Server.Host/Components/Chat/Providers/ProviderFactory.cs
+++ b/src/Tgstation.Server.Host/Components/Chat/Providers/ProviderFactory.cs
@@ -61,14 +61,15 @@ namespace Tgstation.Server.Host.Components.Chat.Providers
{
ChatProvider.Irc => new IrcProvider(
jobManager,
- assemblyInformationProvider,
asyncDelayer,
loggerFactory.CreateLogger(),
+ assemblyInformationProvider,
settings),
ChatProvider.Discord => new DiscordProvider(
jobManager,
- assemblyInformationProvider,
+ asyncDelayer,
loggerFactory.CreateLogger(),
+ assemblyInformationProvider,
settings),
_ => throw new InvalidOperationException(String.Format(CultureInfo.InvariantCulture, "Invalid ChatProvider: {0}", settings.Provider)),
};
diff --git a/src/Tgstation.Server.Host/Components/Deployment/DreamMaker.cs b/src/Tgstation.Server.Host/Components/Deployment/DreamMaker.cs
index ab04afebb9..4d1a37fe96 100644
--- a/src/Tgstation.Server.Host/Components/Deployment/DreamMaker.cs
+++ b/src/Tgstation.Server.Host/Components/Deployment/DreamMaker.cs
@@ -23,6 +23,7 @@ using Tgstation.Server.Host.IO;
using Tgstation.Server.Host.Jobs;
using Tgstation.Server.Host.Models;
using Tgstation.Server.Host.System;
+using Tgstation.Server.Host.Utils;
namespace Tgstation.Server.Host.Components.Deployment
{
@@ -89,6 +90,11 @@ namespace Tgstation.Server.Host.Components.Deployment
///
readonly IRemoteDeploymentManagerFactory remoteDeploymentManagerFactory;
+ ///
+ /// The for .
+ ///
+ readonly IAsyncDelayer asyncDelayer;
+
///
/// The for .
///
@@ -147,6 +153,7 @@ namespace Tgstation.Server.Host.Components.Deployment
/// The value of .
/// The value of .
/// The value of .
+ /// The value of .
/// The value of .
/// The value of .
/// The value of .
@@ -161,6 +168,7 @@ namespace Tgstation.Server.Host.Components.Deployment
ICompileJobSink compileJobConsumer,
IRepositoryManager repositoryManager,
IRemoteDeploymentManagerFactory remoteDeploymentManagerFactory,
+ IAsyncDelayer asyncDelayer,
ILogger logger,
SessionConfiguration sessionConfiguration,
Api.Models.Instance metadata)
@@ -174,6 +182,7 @@ namespace Tgstation.Server.Host.Components.Deployment
this.processExecutor = processExecutor ?? throw new ArgumentNullException(nameof(processExecutor));
this.compileJobConsumer = compileJobConsumer ?? throw new ArgumentNullException(nameof(compileJobConsumer));
this.repositoryManager = repositoryManager ?? throw new ArgumentNullException(nameof(repositoryManager));
+ this.asyncDelayer = asyncDelayer ?? throw new ArgumentNullException(nameof(asyncDelayer));
this.remoteDeploymentManagerFactory = remoteDeploymentManagerFactory ?? throw new ArgumentNullException(nameof(remoteDeploymentManagerFactory));
this.logger = logger ?? throw new ArgumentNullException(nameof(logger));
this.sessionConfiguration = sessionConfiguration ?? throw new ArgumentNullException(nameof(sessionConfiguration));
@@ -741,13 +750,13 @@ namespace Tgstation.Server.Host.Components.Deployment
var remainingSleepThisInterval = nextInterval - DateTimeOffset.UtcNow;
var nextSleepSpan = remainingSleepThisInterval < minimumSleepInterval ? minimumSleepInterval : remainingSleepThisInterval;
- await Task.Delay(nextSleepSpan, cancellationToken);
+ await asyncDelayer.Delay(nextSleepSpan, cancellationToken);
progressReporter.ReportProgress(lastReport);
}
while (DateTimeOffset.UtcNow < nextInterval);
}
else
- await Task.Delay(minimumSleepInterval, cancellationToken);
+ await asyncDelayer.Delay(minimumSleepInterval, cancellationToken);
lastReport = estimatedDuration.HasValue ? sleepInterval * (iteration + 1) / estimatedDuration.Value : null;
progressReporter.ReportProgress(lastReport);
diff --git a/src/Tgstation.Server.Host/Components/Instance.cs b/src/Tgstation.Server.Host/Components/Instance.cs
index 8d36d468b3..5c5ec4d692 100644
--- a/src/Tgstation.Server.Host/Components/Instance.cs
+++ b/src/Tgstation.Server.Host/Components/Instance.cs
@@ -70,6 +70,11 @@ namespace Tgstation.Server.Host.Components
///
readonly IRemoteDeploymentManagerFactory remoteDeploymentManagerFactory;
+ ///
+ /// The for the .
+ ///
+ readonly IAsyncDelayer asyncDelayer;
+
///
/// The for the .
///
@@ -109,6 +114,7 @@ namespace Tgstation.Server.Host.Components
/// The value of .
/// The value of .
/// The value of .
+ /// The value of .
/// The value of .
public Instance(
Api.Models.Instance metadata,
@@ -123,6 +129,7 @@ namespace Tgstation.Server.Host.Components
IJobManager jobManager,
IEventConsumer eventConsumer,
IRemoteDeploymentManagerFactory remoteDeploymentManagerFactory,
+ IAsyncDelayer asyncDelayer,
ILogger logger)
{
this.metadata = metadata ?? throw new ArgumentNullException(nameof(metadata));
@@ -136,6 +143,7 @@ namespace Tgstation.Server.Host.Components
this.jobManager = jobManager ?? throw new ArgumentNullException(nameof(jobManager));
this.eventConsumer = eventConsumer ?? throw new ArgumentNullException(nameof(eventConsumer));
this.remoteDeploymentManagerFactory = remoteDeploymentManagerFactory ?? throw new ArgumentNullException(nameof(remoteDeploymentManagerFactory));
+ this.asyncDelayer = asyncDelayer ?? throw new ArgumentNullException(nameof(asyncDelayer));
this.logger = logger ?? throw new ArgumentNullException(nameof(logger));
timerLock = new object();
@@ -488,7 +496,7 @@ namespace Tgstation.Server.Host.Components
while (true)
try
{
- await Task.Delay(TimeSpan.FromMinutes(minutes > Int32.MaxValue ? Int32.MaxValue : minutes), cancellationToken);
+ await asyncDelayer.Delay(TimeSpan.FromMinutes(minutes > Int32.MaxValue ? Int32.MaxValue : minutes), cancellationToken);
logger.LogInformation("Beginning auto update...");
await eventConsumer.HandleEvent(EventType.InstanceAutoUpdateStart, Enumerable.Empty(), cancellationToken);
try
diff --git a/src/Tgstation.Server.Host/Components/InstanceFactory.cs b/src/Tgstation.Server.Host/Components/InstanceFactory.cs
index 6f68acec65..6d78a4edd0 100644
--- a/src/Tgstation.Server.Host/Components/InstanceFactory.cs
+++ b/src/Tgstation.Server.Host/Components/InstanceFactory.cs
@@ -23,6 +23,7 @@ using Tgstation.Server.Host.Jobs;
using Tgstation.Server.Host.Security;
using Tgstation.Server.Host.System;
using Tgstation.Server.Host.Transfer;
+using Tgstation.Server.Host.Utils;
namespace Tgstation.Server.Host.Components
{
@@ -139,6 +140,11 @@ namespace Tgstation.Server.Host.Components
///
readonly IRemoteDeploymentManagerFactory remoteDeploymentManagerFactory;
+ ///
+ /// The for the .
+ ///
+ readonly IAsyncDelayer asyncDelayer;
+
///
/// The for the .
///
@@ -182,6 +188,7 @@ namespace Tgstation.Server.Host.Components
/// The value of .
/// The value of .
/// The value of .
+ /// The value of .
/// The containing the value of .
/// The containing the value of .
public InstanceFactory(
@@ -207,6 +214,7 @@ namespace Tgstation.Server.Host.Components
IFileTransferTicketProvider fileTransferService,
IGitRemoteFeaturesFactory gitRemoteFeaturesFactory,
IRemoteDeploymentManagerFactory remoteDeploymentManagerFactory,
+ IAsyncDelayer asyncDelayer,
IOptions generalConfigurationOptions,
IOptions sessionConfigurationOptions)
{
@@ -232,6 +240,7 @@ namespace Tgstation.Server.Host.Components
this.fileTransferService = fileTransferService ?? throw new ArgumentNullException(nameof(fileTransferService));
this.gitRemoteFeaturesFactory = gitRemoteFeaturesFactory ?? throw new ArgumentNullException(nameof(gitRemoteFeaturesFactory));
this.remoteDeploymentManagerFactory = remoteDeploymentManagerFactory ?? throw new ArgumentNullException(nameof(remoteDeploymentManagerFactory));
+ this.asyncDelayer = asyncDelayer ?? throw new ArgumentNullException(nameof(asyncDelayer));
generalConfiguration = generalConfigurationOptions?.Value ?? throw new ArgumentNullException(nameof(generalConfigurationOptions));
sessionConfiguration = sessionConfigurationOptions?.Value ?? throw new ArgumentNullException(nameof(sessionConfigurationOptions));
}
@@ -310,6 +319,7 @@ namespace Tgstation.Server.Host.Components
bridgeRegistrar,
serverPortProvider,
eventConsumer,
+ asyncDelayer,
loggerFactory,
loggerFactory.CreateLogger(),
sessionConfiguration,
@@ -358,6 +368,7 @@ namespace Tgstation.Server.Host.Components
dmbFactory,
repoManager,
remoteDeploymentManagerFactory,
+ asyncDelayer,
loggerFactory.CreateLogger(),
sessionConfiguration,
metadata);
@@ -374,6 +385,7 @@ namespace Tgstation.Server.Host.Components
jobManager,
eventConsumer,
remoteDeploymentManagerFactory,
+ asyncDelayer,
loggerFactory.CreateLogger());
return instance;
diff --git a/src/Tgstation.Server.Host/Components/Session/SessionController.cs b/src/Tgstation.Server.Host/Components/Session/SessionController.cs
index 82c0063193..a831a37679 100644
--- a/src/Tgstation.Server.Host/Components/Session/SessionController.cs
+++ b/src/Tgstation.Server.Host/Components/Session/SessionController.cs
@@ -180,6 +180,7 @@ namespace Tgstation.Server.Host.Components.Session
/// The value of .
/// The value of .
/// The for the .
+ /// The for the .
/// The value of .
/// The returning a to be run after the ends.
/// The optional time to wait before failing the .
@@ -195,6 +196,7 @@ namespace Tgstation.Server.Host.Components.Session
IBridgeRegistrar bridgeRegistrar,
IChatManager chat,
IAssemblyInformationProvider assemblyInformationProvider,
+ IAsyncDelayer asyncDelayer,
ILogger logger,
Func postLifetimeCallback,
uint? startupTimeout,
@@ -219,7 +221,11 @@ namespace Tgstation.Server.Host.Components.Session
rebootTcs = new TaskCompletionSource();
primeTcs = new TaskCompletionSource();
- initialBridgeRequestTcs = new TaskCompletionSource();
+
+ // Run this asynchronously because we want to try to avoid any effects sending topics to the server while the initial bridge request is processing
+ // It MAY be the source of a DD crash. See this gist https://gist.github.com/Cyberboss/7776bbeff3a957d76affe0eae95c9f14
+ // Worth further investigation as to if that sequence of events is a reliable crash vector and opening a BYOND bug if it is
+ initialBridgeRequestTcs = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously);
reattachTopicCts = new CancellationTokenSource();
synchronizationLock = new object();
@@ -246,6 +252,7 @@ namespace Tgstation.Server.Host.Components.Session
LaunchResult = GetLaunchResult(
assemblyInformationProvider,
+ asyncDelayer,
startupTimeout,
reattached,
apiValidate);
@@ -508,12 +515,14 @@ namespace Tgstation.Server.Host.Components.Session
/// The for .
///
/// The .
+ /// The .
/// The, optional, startup timeout in seconds.
/// If DreamDaemon was reattached.
/// If this is a DMAPI validation session.
/// A resulting in the for the operation.
async Task GetLaunchResult(
IAssemblyInformationProvider assemblyInformationProvider,
+ IAsyncDelayer asyncDelayer,
uint? startupTimeout,
bool reattached,
bool apiValidate)
@@ -526,7 +535,7 @@ namespace Tgstation.Server.Host.Components.Session
var toAwait = Task.WhenAny(startupTask, process.Lifetime);
if (startupTimeout.HasValue)
- toAwait = Task.WhenAny(toAwait, Task.Delay(TimeSpan.FromSeconds(startupTimeout.Value)));
+ toAwait = Task.WhenAny(toAwait, asyncDelayer.Delay(TimeSpan.FromSeconds(startupTimeout.Value), default)); // DCT: None available, task will clean up after delay
Logger.LogTrace(
"Waiting for LaunchResult based on {launchResultCompletionCause}{possibleTimeout}...",
diff --git a/src/Tgstation.Server.Host/Components/Session/SessionControllerFactory.cs b/src/Tgstation.Server.Host/Components/Session/SessionControllerFactory.cs
index f785629871..0662b381d2 100644
--- a/src/Tgstation.Server.Host/Components/Session/SessionControllerFactory.cs
+++ b/src/Tgstation.Server.Host/Components/Session/SessionControllerFactory.cs
@@ -26,6 +26,7 @@ using Tgstation.Server.Host.IO;
using Tgstation.Server.Host.Jobs;
using Tgstation.Server.Host.Security;
using Tgstation.Server.Host.System;
+using Tgstation.Server.Host.Utils;
namespace Tgstation.Server.Host.Components.Session
{
@@ -102,6 +103,11 @@ namespace Tgstation.Server.Host.Components.Session
///
readonly IEventConsumer eventConsumer;
+ ///
+ /// The for the .
+ ///
+ readonly IAsyncDelayer asyncDelayer;
+
///
/// The for the .
///
@@ -187,10 +193,11 @@ namespace Tgstation.Server.Host.Components.Session
/// The value of .
/// The value of .
/// The value of .
+ /// The value of .
+ /// The value of .
/// The value of .
/// The value of .
/// The value of .
- /// The value of .
public SessionControllerFactory(
IProcessExecutor processExecutor,
IByondManager byond,
@@ -205,6 +212,7 @@ namespace Tgstation.Server.Host.Components.Session
IBridgeRegistrar bridgeRegistrar,
IServerPortProvider serverPortProvider,
IEventConsumer eventConsumer,
+ IAsyncDelayer asyncDelayer,
ILoggerFactory loggerFactory,
ILogger logger,
SessionConfiguration sessionConfiguration,
@@ -223,6 +231,7 @@ namespace Tgstation.Server.Host.Components.Session
this.bridgeRegistrar = bridgeRegistrar ?? throw new ArgumentNullException(nameof(bridgeRegistrar));
this.serverPortProvider = serverPortProvider ?? throw new ArgumentNullException(nameof(serverPortProvider));
this.eventConsumer = eventConsumer ?? throw new ArgumentNullException(nameof(eventConsumer));
+ this.asyncDelayer = asyncDelayer ?? throw new ArgumentNullException(nameof(asyncDelayer));
this.loggerFactory = loggerFactory ?? throw new ArgumentNullException(nameof(loggerFactory));
this.logger = logger ?? throw new ArgumentNullException(nameof(logger));
this.sessionConfiguration = sessionConfiguration ?? throw new ArgumentNullException(nameof(sessionConfiguration));
@@ -341,6 +350,7 @@ namespace Tgstation.Server.Host.Components.Session
bridgeRegistrar,
chat,
assemblyInformationProvider,
+ asyncDelayer,
loggerFactory.CreateLogger(),
() => !launchParameters.LogOutput.Value
? LogDDOutput(process, outputFilePath, byondLock.SupportsCli, default) // DCT: None available
@@ -425,6 +435,7 @@ namespace Tgstation.Server.Host.Components.Session
bridgeRegistrar,
chat,
assemblyInformationProvider,
+ asyncDelayer,
loggerFactory.CreateLogger(),
() => Task.CompletedTask,
null,
diff --git a/src/Tgstation.Server.Host/Components/Watchdog/BasicWatchdog.cs b/src/Tgstation.Server.Host/Components/Watchdog/BasicWatchdog.cs
index 5445ae311c..f7bc152132 100644
--- a/src/Tgstation.Server.Host/Components/Watchdog/BasicWatchdog.cs
+++ b/src/Tgstation.Server.Host/Components/Watchdog/BasicWatchdog.cs
@@ -118,22 +118,19 @@ namespace Tgstation.Server.Host.Components.Watchdog
if (Server.RebootState == Session.RebootState.Shutdown)
{
// the time for graceful shutdown is now
- await Chat.QueueWatchdogMessage(
+ Chat.QueueWatchdogMessage(
String.Format(
CultureInfo.InvariantCulture,
"Server {0}! Shutting down due to graceful termination request...",
- exitWord),
- cancellationToken)
- ;
+ exitWord));
return MonitorAction.Exit;
}
- await Chat.QueueWatchdogMessage(
+ Chat.QueueWatchdogMessage(
String.Format(
CultureInfo.InvariantCulture,
"Server {0}! Rebooting...",
- exitWord),
- cancellationToken);
+ exitWord));
return MonitorAction.Restart;
case MonitorActivationReason.ActiveServerRebooted:
var rebootState = Server.RebootState;
@@ -156,10 +153,8 @@ namespace Tgstation.Server.Host.Components.Watchdog
return MonitorAction.Restart;
case Session.RebootState.Shutdown:
// graceful shutdown time
- await Chat.QueueWatchdogMessage(
- "Active server rebooted! Shutting down due to graceful termination request...",
- cancellationToken)
- ;
+ Chat.QueueWatchdogMessage(
+ "Active server rebooted! Shutting down due to graceful termination request...");
return MonitorAction.Exit;
default:
throw new InvalidOperationException($"Invalid reboot state: {rebootState}");
@@ -200,7 +195,7 @@ namespace Tgstation.Server.Host.Components.Watchdog
///
protected override async Task InitController(
- Task chatTask,
+ Task eventTask,
ReattachInformation reattachInfo,
CancellationToken cancellationToken)
{
@@ -221,7 +216,7 @@ namespace Tgstation.Server.Host.Components.Watchdog
await BeforeApplyDmb(dmbToUse.CompileJob, cancellationToken);
dmbToUse = await PrepServerForLaunch(dmbToUse, cancellationToken);
- await chatTask;
+ await eventTask;
serverLaunchTask = SessionControllerFactory.LaunchNew(
dmbToUse,
null,
@@ -231,7 +226,7 @@ namespace Tgstation.Server.Host.Components.Watchdog
}
else
{
- await chatTask;
+ await eventTask;
serverLaunchTask = SessionControllerFactory.Reattach(reattachInfo, cancellationToken);
}
@@ -246,7 +241,6 @@ namespace Tgstation.Server.Host.Components.Watchdog
return;
}
- // Server.AdjustPriority(true);
if (!reattachInProgress)
await SessionStartupPersist(cancellationToken);
@@ -299,14 +293,17 @@ namespace Tgstation.Server.Host.Components.Watchdog
///
/// The for the operation.
/// A representing the running operation.
- protected virtual Task HandleNewDmbAvailable(CancellationToken cancellationToken)
+ protected virtual async Task HandleNewDmbAvailable(CancellationToken cancellationToken)
{
gracefulRebootRequired = true;
if (Server.CompileJob.DMApiVersion == null)
- return Chat.QueueWatchdogMessage(
- "A new deployment has been made but cannot be applied automatically as the currently running server has no DMAPI. Please manually reboot the server to apply the update.",
- cancellationToken);
- return Server.SetRebootState(Session.RebootState.Restart, cancellationToken);
+ {
+ Chat.QueueWatchdogMessage(
+ "A new deployment has been made but cannot be applied automatically as the currently running server has no DMAPI. Please manually reboot the server to apply the update.");
+ return;
+ }
+
+ await Server.SetRebootState(Session.RebootState.Restart, cancellationToken);
}
///
diff --git a/src/Tgstation.Server.Host/Components/Watchdog/WatchdogBase.cs b/src/Tgstation.Server.Host/Components/Watchdog/WatchdogBase.cs
index e2b1a6b7dc..2c5d0ba569 100644
--- a/src/Tgstation.Server.Host/Components/Watchdog/WatchdogBase.cs
+++ b/src/Tgstation.Server.Host/Components/Watchdog/WatchdogBase.cs
@@ -347,10 +347,9 @@ namespace Tgstation.Server.Host.Components.Watchdog
{
if (!graceful)
{
- var chatTask = Chat.QueueWatchdogMessage("Manual restart triggered...", cancellationToken);
+ Chat.QueueWatchdogMessage("Manual restart triggered...");
await TerminateNoLock(false, false, cancellationToken);
await LaunchNoLock(true, false, true, null, cancellationToken);
- await chatTask;
return;
}
@@ -365,7 +364,8 @@ namespace Tgstation.Server.Host.Components.Watchdog
public async Task StartAsync(CancellationToken cancellationToken)
{
var reattachInfo = await SessionPersistor.Load(cancellationToken);
- if (!autoStart && reattachInfo == null)
+ var reattaching = reattachInfo != null;
+ if (!autoStart && !reattaching)
return;
var job = new Models.Job
@@ -374,7 +374,7 @@ namespace Tgstation.Server.Host.Components.Watchdog
{
Id = metadata.Id,
},
- Description = $"Instance startup watchdog {(reattachInfo != null ? "reattach" : "launch")}",
+ Description = $"Instance startup watchdog {(reattaching ? "reattach" : "launch")}",
CancelRight = (ulong)DreamDaemonRights.Shutdown,
CancelRightsType = RightsType.DreamDaemon,
};
@@ -384,8 +384,11 @@ namespace Tgstation.Server.Host.Components.Watchdog
{
if (core.Watchdog != this)
throw new InvalidOperationException(Instance.DifferentCoreExceptionMessage);
+
using (await SemaphoreSlimContext.Lock(synchronizationSemaphore, ct))
await LaunchNoLock(true, true, true, reattachInfo, ct);
+
+ await Chat.UpdateTrackingContexts(ct);
},
cancellationToken)
;
@@ -422,7 +425,7 @@ namespace Tgstation.Server.Host.Components.Watchdog
releaseServers = true;
if (Status == WatchdogStatus.Online)
- await Chat.QueueWatchdogMessage("Detaching...", cancellationToken);
+ Chat.QueueWatchdogMessage("Detaching...");
else
Logger.LogTrace("Not sending detach chat message as status is: {status}", Status);
}
@@ -474,11 +477,11 @@ namespace Tgstation.Server.Host.Components.Watchdog
///
/// Starts all s.
///
- /// A, possibly active, for an outgoing chat message.
+ /// A, possibly active, for an event that's running.
/// to use, if any.
/// The for the operation.
/// A representing the running operation.
- protected abstract Task InitController(Task chatTask, ReattachInformation reattachInfo, CancellationToken cancellationToken);
+ protected abstract Task InitController(Task eventTask, ReattachInformation reattachInfo, CancellationToken cancellationToken);
///
/// Launches the watchdog.
@@ -504,21 +507,16 @@ namespace Tgstation.Server.Host.Components.Watchdog
throw new JobException(ErrorCode.WatchdogCompileJobCorrupted);
// this is necessary, the monitor could be in it's sleep loop trying to restart, if so cancel THAT monitor and start our own with blackjack and hookers
- Task announceTask;
+ var eventTask = Task.CompletedTask;
if (announce)
{
- announceTask = Chat.QueueWatchdogMessage(
+ Chat.QueueWatchdogMessage(
reattachInfo == null
? "Launching..."
- : "Reattaching...",
- cancellationToken); // simple announce
+ : "Reattaching..."); // simple announce
if (reattachInfo == null)
- announceTask = Task.WhenAll(
- HandleEvent(EventType.WatchdogLaunch, Enumerable.Empty(), false, cancellationToken),
- announceTask);
+ eventTask = HandleEvent(EventType.WatchdogLaunch, Enumerable.Empty(), false, cancellationToken);
}
- else
- announceTask = Task.CompletedTask; // no announce
// since neither server is running, this is safe to do
LastLaunchParameters = ActiveLaunchParameters;
@@ -526,7 +524,7 @@ namespace Tgstation.Server.Host.Components.Watchdog
try
{
- await InitController(announceTask, reattachInfo, cancellationToken);
+ await InitController(eventTask, reattachInfo, cancellationToken);
}
catch (OperationCanceledException ex)
{
@@ -536,15 +534,15 @@ namespace Tgstation.Server.Host.Components.Watchdog
catch (Exception e)
{
Logger.LogWarning(e, "Failed to start watchdog!");
- var originalChatTask = announceTask;
- async Task ChainChatTaskWithErrorMessage()
+ var originalChatTask = eventTask;
+ async Task ChainEventTaskWithErrorMessage()
{
await originalChatTask;
if (announceFailure)
- await Chat.QueueWatchdogMessage("Startup failed!", cancellationToken);
+ Chat.QueueWatchdogMessage("Startup failed!");
}
- announceTask = ChainChatTaskWithErrorMessage();
+ eventTask = ChainEventTaskWithErrorMessage();
throw;
}
finally
@@ -552,7 +550,7 @@ namespace Tgstation.Server.Host.Components.Watchdog
// finish the chat task that's in flight
try
{
- await announceTask;
+ await eventTask;
}
catch (OperationCanceledException ex)
{
@@ -623,8 +621,8 @@ namespace Tgstation.Server.Host.Components.Watchdog
const string FailReattachMessage = "Unable to properly reattach to server! Restarting watchdog...";
Logger.LogWarning(FailReattachMessage);
- var chatTask = Chat.QueueWatchdogMessage(FailReattachMessage, cancellationToken);
- await InitController(chatTask, null, cancellationToken);
+ Chat.QueueWatchdogMessage(FailReattachMessage);
+ await InitController(Task.CompletedTask, null, cancellationToken);
}
///
@@ -727,7 +725,6 @@ namespace Tgstation.Server.Host.Components.Watchdog
await DisposeAndNullControllers(cancellationToken);
- var chatTask = Task.CompletedTask;
for (var retryAttempts = 1; ; ++retryAttempts)
{
Status = WatchdogStatus.Restoring;
@@ -745,10 +742,6 @@ namespace Tgstation.Server.Host.Components.Watchdog
{
launchException = e;
}
- finally
- {
- await chatTask;
- }
Logger.LogWarning(launchException, "Failed to automatically restart the watchdog! Attempt: {attemptNumber}", retryAttempts);
Status = WatchdogStatus.DelayedRestart;
@@ -758,16 +751,12 @@ namespace Tgstation.Server.Host.Components.Watchdog
Math.Pow(2, retryAttempts)),
TimeSpan.FromHours(1).TotalSeconds); // max of one hour, increasing by a power of 2 each time
- chatTask = Chat.QueueWatchdogMessage(
- $"Failed to restart (Attempt: {retryAttempts}), retrying in {retryDelay}s...",
- cancellationToken);
+ Chat.QueueWatchdogMessage(
+ $"Failed to restart (Attempt: {retryAttempts}), retrying in {retryDelay}s...");
- await Task.WhenAll(
- AsyncDelayer.Delay(
- TimeSpan.FromSeconds(retryDelay),
- cancellationToken),
- chatTask)
- ;
+ await AsyncDelayer.Delay(
+ TimeSpan.FromSeconds(retryDelay),
+ cancellationToken);
}
}
@@ -963,9 +952,8 @@ namespace Tgstation.Server.Host.Components.Watchdog
var nextActionMessage = nextAction != MonitorAction.Exit
? "Recovering"
: "Shutting down";
- var chatTask = Chat.QueueWatchdogMessage(
- $"Monitor crashed, this should NEVER happen! Please report this, full details in logs! {nextActionMessage}. Error: {e.Message}",
- cancellationToken);
+ Chat.QueueWatchdogMessage(
+ $"Monitor crashed, this should NEVER happen! Please report this, full details in logs! {nextActionMessage}. Error: {e.Message}");
if (disposed)
nextAction = MonitorAction.Exit;
@@ -977,8 +965,6 @@ namespace Tgstation.Server.Host.Components.Watchdog
Logger.LogDebug("Server seems to be okay, not restarting");
nextAction = MonitorAction.Continue;
}
-
- await chatTask;
}
}
catch (OperationCanceledException)
@@ -1023,15 +1009,14 @@ namespace Tgstation.Server.Host.Components.Watchdog
releaseServers,
cancellationToken);
- var chatTask = announce ? Chat.QueueWatchdogMessage("Shutting down...", cancellationToken) : Task.CompletedTask;
+ if (announce)
+ Chat.QueueWatchdogMessage("Shutting down...");
await eventTask;
await StopMonitor();
LastLaunchParameters = null;
-
- await chatTask;
return;
}
@@ -1068,7 +1053,7 @@ namespace Tgstation.Server.Host.Components.Watchdog
case 2:
const string message2 = "DEFCON 3: DreamDaemon has missed 2 heartbeats!";
Logger.LogInformation(message2);
- await Chat.QueueWatchdogMessage(message2, cancellationToken);
+ Chat.QueueWatchdogMessage(message2);
break;
case 3:
var actionToTake = shouldShutdown
@@ -1076,12 +1061,11 @@ namespace Tgstation.Server.Host.Components.Watchdog
: "be restarted";
const string logTemplate1 = "DEFCON 2: DreamDaemon has missed 3 heartbeats! If it does not respond to the next one, the watchdog will {actionToTake}!";
Logger.LogWarning(logTemplate1, actionToTake);
- await Chat.QueueWatchdogMessage(
+ Chat.QueueWatchdogMessage(
logTemplate1.Replace(
"{actionToTake}",
actionToTake,
- StringComparison.Ordinal),
- cancellationToken);
+ StringComparison.Ordinal));
break;
case 4:
var actionTaken = shouldShutdown
@@ -1089,12 +1073,11 @@ namespace Tgstation.Server.Host.Components.Watchdog
: "Restarting";
const string logTemplate2 = "DEFCON 1: Four heartbeats have been missed! {actionTaken}...";
Logger.LogWarning(logTemplate2, actionTaken);
- await Chat.QueueWatchdogMessage(
+ Chat.QueueWatchdogMessage(
logTemplate2.Replace(
"{actionTaken}",
actionTaken,
- StringComparison.Ordinal),
- cancellationToken);
+ StringComparison.Ordinal));
if (ActiveLaunchParameters.DumpOnHeartbeatRestart.Value)
{
diff --git a/src/Tgstation.Server.Host/Controllers/ChatController.cs b/src/Tgstation.Server.Host/Controllers/ChatController.cs
index d6a33f6fe8..22605192ad 100644
--- a/src/Tgstation.Server.Host/Controllers/ChatController.cs
+++ b/src/Tgstation.Server.Host/Controllers/ChatController.cs
@@ -115,8 +115,7 @@ namespace Tgstation.Server.Host.Controllers
.ChatBots
.AsQueryable()
.Where(x => x.InstanceId == Instance.Id)
- .CountAsync(cancellationToken)
- ;
+ .CountAsync(cancellationToken);
if (countOfExistingBotsInInstance >= Instance.ChatBotLimit.Value)
return Conflict(new ErrorMessageResponse(ErrorCode.ChatBotMax));
diff --git a/src/Tgstation.Server.Host/Core/Application.cs b/src/Tgstation.Server.Host/Core/Application.cs
index c2b4547e7d..fa5897a9e5 100644
--- a/src/Tgstation.Server.Host/Core/Application.cs
+++ b/src/Tgstation.Server.Host/Core/Application.cs
@@ -30,7 +30,6 @@ using Tgstation.Server.Common;
using Tgstation.Server.Host.Components;
using Tgstation.Server.Host.Components.Byond;
using Tgstation.Server.Host.Components.Chat;
-using Tgstation.Server.Host.Components.Chat.Providers;
using Tgstation.Server.Host.Components.Deployment.Remote;
using Tgstation.Server.Host.Components.Interop;
using Tgstation.Server.Host.Components.Interop.Bridge;
@@ -360,7 +359,7 @@ namespace Tgstation.Server.Host.Core
services.AddSingleton();
services.AddSingleton();
services.AddSingleton();
- services.AddSingleton();
+ services.AddChatProviderFactory();
services.AddSingleton();
services.AddSingleton();
services.AddSingleton();
diff --git a/src/Tgstation.Server.Host/Extensions/ServiceCollectionExtensions.cs b/src/Tgstation.Server.Host/Extensions/ServiceCollectionExtensions.cs
index 46bb712d39..4fcbc5d32e 100644
--- a/src/Tgstation.Server.Host/Extensions/ServiceCollectionExtensions.cs
+++ b/src/Tgstation.Server.Host/Extensions/ServiceCollectionExtensions.cs
@@ -11,6 +11,7 @@ using Serilog;
using Serilog.Configuration;
using Serilog.Sinks.Elasticsearch;
+using Tgstation.Server.Host.Components.Chat.Providers;
using Tgstation.Server.Host.Configuration;
using Tgstation.Server.Host.Utils;
@@ -21,6 +22,30 @@ namespace Tgstation.Server.Host.Extensions
///
static class ServiceCollectionExtensions
{
+ ///
+ /// The implementation used in calls to .
+ ///
+ static Type chatProviderFactoryType = typeof(ProviderFactory);
+
+ ///
+ /// Change the used as an implementation for calls to .
+ ///
+ /// The implementation to use.
+ public static void UseChatProviderFactory() where TProviderFactory : IProviderFactory
+ {
+ chatProviderFactoryType = typeof(TProviderFactory);
+ }
+
+ ///
+ /// Adds a implementation to the given .
+ ///
+ /// The to configure.
+ /// .
+ public static IServiceCollection AddChatProviderFactory(this IServiceCollection serviceCollection)
+ {
+ return serviceCollection.AddSingleton(typeof(IProviderFactory), chatProviderFactoryType);
+ }
+
///
/// Add a standard binding.
///
diff --git a/src/Tgstation.Server.Host/Server.cs b/src/Tgstation.Server.Host/Server.cs
index f4bf53ad66..de7a97cf65 100644
--- a/src/Tgstation.Server.Host/Server.cs
+++ b/src/Tgstation.Server.Host/Server.cs
@@ -314,14 +314,14 @@ namespace Tgstation.Server.Host
? generalConfiguration.ShutdownTimeoutMinutes
: generalConfiguration.RestartTimeoutMinutes));
var cancellationToken = cts.Token;
- var eventsTask = Task.WhenAll(
- restartHandlers.Select(
- x => x.HandleRestart(newVersion, isGracefulShutdown, cancellationToken))
- .ToList());
-
- logger.LogTrace("Joining restart handlers...");
try
{
+ var eventsTask = Task.WhenAll(
+ restartHandlers.Select(
+ x => x.HandleRestart(newVersion, isGracefulShutdown, cancellationToken))
+ .ToList());
+
+ logger.LogTrace("Joining restart handlers...");
await eventsTask;
}
catch (OperationCanceledException ex)
diff --git a/tests/DMAPI/ApiFree/Test.dm b/tests/DMAPI/ApiFree/Test.dm
index b7b4ae5535..a14f28aef0 100644
--- a/tests/DMAPI/ApiFree/Test.dm
+++ b/tests/DMAPI/ApiFree/Test.dm
@@ -1,7 +1,2 @@
-/world/New()
- text2file("SUCCESS", "test_success.txt")
+/world/proc/RunTest()
log << "Hello world!"
-
-/world/Error(exception)
- fdel("test_success.txt")
- text2file("Runtime Error: [exception]", "test_fail_reason.txt")
diff --git a/tests/DMAPI/ApiFree/api_free.dme b/tests/DMAPI/ApiFree/api_free.dme
index 444e2491d8..8179f9b698 100644
--- a/tests/DMAPI/ApiFree/api_free.dme
+++ b/tests/DMAPI/ApiFree/api_free.dme
@@ -11,5 +11,6 @@
// END_PREFERENCES
// BEGIN_INCLUDE
+#include "../test_setup.dm"
#include "Test.dm"
// END_INCLUDE
diff --git a/tests/DMAPI/BasicOperation/Test.dm b/tests/DMAPI/BasicOperation/Test.dm
index 42d311056c..022088fc67 100644
--- a/tests/DMAPI/BasicOperation/Test.dm
+++ b/tests/DMAPI/BasicOperation/Test.dm
@@ -1,4 +1,4 @@
-/world/New()
+/world/proc/RunTest()
text2file("SUCCESS", "test_success.txt")
log << "About to call TgsNew()"
sleep_offline = FALSE
@@ -6,10 +6,6 @@
log << "About to call StartAsync()"
StartAsync()
-/world/Error(exception)
- fdel("test_success.txt")
- text2file("Runtime Error: [exception]", "test_fail_reason.txt")
-
/proc/StartAsync()
set waitfor = FALSE
Run()
@@ -21,7 +17,7 @@
var/list/world_params = world.params
if(!("test" in world_params) || world_params["test"] != "bababooey")
- text2file("Expected parameter test=bababooey but did not receive", "test_fail_reason.txt")
+ FailTest("Expected parameter test=bababooey but did not receive", "test_fail_reason.txt")
world.log << "sleep2"
sleep(150)
diff --git a/tests/DMAPI/BasicOperation/basic_operation_test.dme b/tests/DMAPI/BasicOperation/basic_operation_test.dme
index 92781c7e16..b927014455 100644
--- a/tests/DMAPI/BasicOperation/basic_operation_test.dme
+++ b/tests/DMAPI/BasicOperation/basic_operation_test.dme
@@ -12,6 +12,6 @@
// BEGIN_INCLUDE
#include "Config.dm"
-#include "..\tgs_include.dm"
+#include "../test_prelude.dm"
#include "Test.dm"
// END_INCLUDE
diff --git a/tests/DMAPI/LongRunning/Test.dm b/tests/DMAPI/LongRunning/Test.dm
index 14b927c7e8..762add090c 100644
--- a/tests/DMAPI/LongRunning/Test.dm
+++ b/tests/DMAPI/LongRunning/Test.dm
@@ -2,32 +2,7 @@
sleep_offline = FALSE
loop_checks = FALSE
-/world/Error(exception/E, datum/e_src)
- var/list/usrinfo = null
- var/list/splitlines = splittext(E.desc, "\n")
- var/list/desclines = list()
- for(var/line in splitlines)
- if(length(line) < 3 || findtext(line, "source file:") || findtext(line, "usr.loc:"))
- continue
- if(findtext(line, "usr:"))
- if(usrinfo)
- desclines.Add(usrinfo)
- usrinfo = null
- continue // Our usr info is better, replace it
-
- if(copytext(line, 1, 3) != " ")//3 == length(" ") + 1
- desclines += (" " + line) // Pad any unpadded lines, so they look pretty
- else
- desclines += line
-
- if(usrinfo) //If this info isn't null, it hasn't been added yet
- desclines.Add(usrinfo)
-
- fdel("test_success.txt")
- text2file("Runtime Error: [E]", "test_fail_reason.txt")
-
-/world/New()
- text2file("SUCCESS", "test_success.txt")
+/world/proc/RunTest()
log << "Initial value of sleep_offline: [sleep_offline]"
sleep_offline = FALSE
@@ -35,6 +10,11 @@
for(var/i in 1 to 10000000)
dab()
TgsNew(new /datum/tgs_event_handler/impl, TGS_SECURITY_SAFE)
+
+ var/list/channels = TgsChatChannelInfo()
+ if(!length(channels))
+ FailTest("Expected some chat channels!")
+
StartAsync()
/proc/dab()
@@ -118,8 +98,7 @@ var/run_bridge_test
if(tactics4)
var/size = isnum(tactics4) ? tactics4 : text2num(tactics4)
if(!isnum(size))
- text2file("tgs_integration_test_tactics4 wasn't a number!", "test_fail_reason.txt")
- del(world)
+ FailTest("tgs_integration_test_tactics4 wasn't a number!")
var/payload = create_payload(size)
return payload
@@ -133,9 +112,19 @@ var/run_bridge_test
// Bridge response queuing
var/tactics6 = data["tgs_integration_test_tactics6"]
if(tactics6)
+ // hack hack, calling world.TgsChatChannelInfo() will try to delay until the channels come back
+ var/datum/tgs_api/v5/api = TGS_READ_GLOBAL(tgs)
+ if (length(api.chat_channels))
+ return "channels_present!"
+
DetachedChatMessageQueuing()
return "queued"
+ var/tactics7 = data["tgs_integration_test_tactics7"]
+ if(tactics7)
+ var/list/channels = TgsChatChannelInfo()
+ return "[length(channels)]"
+
TgsChatBroadcast(new /datum/tgs_message_content("Recieved non-tgs topic: `[T]`"))
return "feck"
@@ -162,6 +151,12 @@ var/run_bridge_test
/datum/tgs_event_handler/impl/HandleEvent(event_code, ...)
set waitfor = FALSE
+ if(event_code == TGS_EVENT_WATCHDOG_DETACH)
+ // hack hack, calling world.TgsChatChannelInfo() will try to delay until the channels come back
+ var/datum/tgs_api/v5/api = TGS_READ_GLOBAL(tgs)
+ if(length(api.chat_channels))
+ FailTest("Expected no chat channels after detach!")
+
world.TgsChatBroadcast(new /datum/tgs_message_content("Recieved event: `[json_encode(args)]`"))
/world/Export(url)
@@ -276,7 +271,6 @@ var/lastTgsError
// this actually gets doubled because it's in two fields for backwards compatibility, but that's fine
var/list/final_result = api.Bridge(0, list("chatMessage" = list("text" = "done:[create_payload(limit * 3)]")))
if(!final_result || lastTgsError || final_result["integrationHack"] != "ok")
- text2file("Failed to end bridge limit test! [(istype(final_result) ? json_encode(final_result): (final_result || "null"))]", "test_fail_reason.txt")
- del(world)
+ FailTest("Failed to end bridge limit test! [(istype(final_result) ? json_encode(final_result): (final_result || "null"))]")
api.access_identifier = old_ai
diff --git a/tests/DMAPI/LongRunning/long_running_test.dme b/tests/DMAPI/LongRunning/long_running_test.dme
index 92781c7e16..b927014455 100644
--- a/tests/DMAPI/LongRunning/long_running_test.dme
+++ b/tests/DMAPI/LongRunning/long_running_test.dme
@@ -12,6 +12,6 @@
// BEGIN_INCLUDE
#include "Config.dm"
-#include "..\tgs_include.dm"
+#include "../test_prelude.dm"
#include "Test.dm"
// END_INCLUDE
diff --git a/tests/DMAPI/LongRunning/long_running_test_copy.dme b/tests/DMAPI/LongRunning/long_running_test_copy.dme
index 92781c7e16..b4da8e0d96 100644
--- a/tests/DMAPI/LongRunning/long_running_test_copy.dme
+++ b/tests/DMAPI/LongRunning/long_running_test_copy.dme
@@ -11,7 +11,5 @@
// END_PREFERENCES
// BEGIN_INCLUDE
-#include "Config.dm"
-#include "..\tgs_include.dm"
-#include "Test.dm"
+#include "long_running_test.dme"
// END_INCLUDE
diff --git a/tests/DMAPI/tgs_include.dm b/tests/DMAPI/test_prelude.dm
similarity index 75%
rename from tests/DMAPI/tgs_include.dm
rename to tests/DMAPI/test_prelude.dm
index d60b655b94..18b7192ec1 100644
--- a/tests/DMAPI/tgs_include.dm
+++ b/tests/DMAPI/test_prelude.dm
@@ -1,2 +1,3 @@
#include "..\..\src\DMAPI\tgs.dm"
#include "..\..\src\DMAPI\tgs\includes.dm"
+#include "test_setup.dm"
diff --git a/tests/DMAPI/test_setup.dm b/tests/DMAPI/test_setup.dm
new file mode 100644
index 0000000000..514921d668
--- /dev/null
+++ b/tests/DMAPI/test_setup.dm
@@ -0,0 +1,32 @@
+/world/New()
+ text2file("SUCCESS", "test_success.txt")
+ world.RunTest()
+
+/world/Error(exception/E, datum/e_src)
+ var/list/usrinfo = null
+ var/list/splitlines = splittext(E.desc, "\n")
+ var/list/desclines = list()
+ for(var/line in splitlines)
+ if(length(line) < 3 || findtext(line, "source file:") || findtext(line, "usr.loc:"))
+ continue
+ if(findtext(line, "usr:"))
+ if(usrinfo)
+ desclines.Add(usrinfo)
+ usrinfo = null
+ continue // Our usr info is better, replace it
+
+ if(copytext(line, 1, 3) != " ")//3 == length(" ") + 1
+ desclines += (" " + line) // Pad any unpadded lines, so they look pretty
+ else
+ desclines += line
+
+ if(usrinfo) //If this info isn't null, it hasn't been added yet
+ desclines.Add(usrinfo)
+
+ FailTest("Runtime Error: [E]")
+
+/proc/FailTest(reason)
+ world.log << "TEST ERROR DM-SIDE: [reason]"
+ fdel("test_success.txt")
+ text2file(reason, "test_fail_reason.txt")
+ del(world)
diff --git a/tests/Tgstation.Server.Host.Tests/Components/Chat/Providers/TestDiscordProvider.cs b/tests/Tgstation.Server.Host.Tests/Components/Chat/Providers/TestDiscordProvider.cs
index 696d1cc2fd..cdc30c888e 100644
--- a/tests/Tgstation.Server.Host.Tests/Components/Chat/Providers/TestDiscordProvider.cs
+++ b/tests/Tgstation.Server.Host.Tests/Components/Chat/Providers/TestDiscordProvider.cs
@@ -7,9 +7,11 @@ using Microsoft.Extensions.Logging;
using Microsoft.VisualStudio.TestTools.UnitTesting;
using Moq;
+using Tgstation.Server.Api.Models;
using Tgstation.Server.Host.Jobs;
using Tgstation.Server.Host.Models;
using Tgstation.Server.Host.System;
+using Tgstation.Server.Host.Utils;
namespace Tgstation.Server.Host.Components.Chat.Providers.Tests
{
@@ -44,16 +46,21 @@ namespace Tgstation.Server.Host.Components.Chat.Providers.Tests
[TestMethod]
public async Task TestConstructionAndDisposal()
{
- if (testToken1 == null)
- Assert.Inconclusive("Required environment variable TGS_TEST_DISCORD_TOKEN isn't set!");
+ var bot = new ChatBot
+ {
+ ConnectionString = "fake_token",
+ ReconnectionInterval = 1,
+ };
- Assert.ThrowsException(() => new DiscordProvider(null, null, null, null));
- Assert.ThrowsException(() => new DiscordProvider(mockJobManager, null, null, null));
- var mockAss = new Mock();
- Assert.ThrowsException(() => new DiscordProvider(mockJobManager, mockAss.Object, null, null));
- var mockLogger = new Mock>();
- Assert.ThrowsException(() => new DiscordProvider(mockJobManager, null, mockLogger.Object, null));
- await new DiscordProvider(mockJobManager, mockAss.Object, mockLogger.Object, testToken1).DisposeAsync();
+ Assert.ThrowsException(() => new DiscordProvider(null, null, null, null, null));
+ Assert.ThrowsException(() => new DiscordProvider(mockJobManager, null, null, null, null));
+ var mockDel = Mock.Of();
+ Assert.ThrowsException(() => new DiscordProvider(mockJobManager, mockDel, null, null, null));
+ var mockLogger = Mock.Of>();
+ Assert.ThrowsException(() => new DiscordProvider(mockJobManager, mockDel, mockLogger, null, null));
+ var mockAss = Mock.Of();
+ Assert.ThrowsException(() => new DiscordProvider(mockJobManager, mockDel, mockLogger, mockAss, null));
+ await new DiscordProvider(mockJobManager, mockDel, mockLogger, mockAss, bot).DisposeAsync();
}
static Task InvokeConnect(IProvider provider, CancellationToken cancellationToken = default) => (Task)provider.GetType().GetMethod("Connect", BindingFlags.Instance | BindingFlags.NonPublic).Invoke(provider, new object[] { cancellationToken });
@@ -62,7 +69,7 @@ namespace Tgstation.Server.Host.Components.Chat.Providers.Tests
public async Task TestConnectWithFakeTokenFails()
{
var mockLogger = new Mock>();
- await using var provider = new DiscordProvider(mockJobManager, Mock.Of(), mockLogger.Object, new ChatBot
+ await using var provider = new DiscordProvider(mockJobManager, Mock.Of(), mockLogger.Object, Mock.Of(), new ChatBot
{
ReconnectionInterval = 1,
ConnectionString = "asdf"
@@ -77,8 +84,11 @@ namespace Tgstation.Server.Host.Components.Chat.Providers.Tests
if (testToken1 == null)
Assert.Inconclusive("Required environment variable TGS_TEST_DISCORD_TOKEN isn't set!");
+ if (!new DiscordConnectionStringBuilder(testToken1.ConnectionString).Valid)
+ Assert.Fail("TGS_TEST_DISCORD_TOKEN is not a valid Discord connection string!");
+
var mockLogger = new Mock>();
- await using var provider = new DiscordProvider(mockJobManager, Mock.Of(), mockLogger.Object, testToken1);
+ await using var provider = new DiscordProvider(mockJobManager, Mock.Of(), mockLogger.Object, Mock.Of(), testToken1);
Assert.IsFalse(provider.Connected);
await InvokeConnect(provider);
Assert.IsTrue(provider.Connected);
diff --git a/tests/Tgstation.Server.Host.Tests/Components/Chat/Providers/TestIrcProvider.cs b/tests/Tgstation.Server.Host.Tests/Components/Chat/Providers/TestIrcProvider.cs
index 2efc860a6a..b11b0ea074 100644
--- a/tests/Tgstation.Server.Host.Tests/Components/Chat/Providers/TestIrcProvider.cs
+++ b/tests/Tgstation.Server.Host.Tests/Components/Chat/Providers/TestIrcProvider.cs
@@ -1,8 +1,14 @@
+using System;
+using System.Reflection;
+using System.Threading;
+using System.Threading.Tasks;
+using System.Xml.Linq;
+
using Microsoft.Extensions.Logging;
using Microsoft.VisualStudio.TestTools.UnitTesting;
using Moq;
-using System;
-using System.Threading.Tasks;
+using Serilog.Parsing;
+
using Tgstation.Server.Api.Models;
using Tgstation.Server.Host.Jobs;
using Tgstation.Server.Host.Models;
@@ -20,12 +26,12 @@ namespace Tgstation.Server.Host.Components.Chat.Providers.Tests
Assert.ThrowsException(() => new IrcProvider(null, null, null, null, null));
var mockJobManager = new Mock();
Assert.ThrowsException(() => new IrcProvider(mockJobManager.Object, null, null, null, null));
- var mockAss = new Mock();
- Assert.ThrowsException(() => new IrcProvider(mockJobManager.Object, mockAss.Object, null, null, null));
var mockAsyncDelayer = new Mock();
- Assert.ThrowsException(() => new IrcProvider(mockJobManager.Object, mockAss.Object, mockAsyncDelayer.Object, null, null));
+ Assert.ThrowsException(() => new IrcProvider(mockJobManager.Object, mockAsyncDelayer.Object, null, null, null));
var mockLogger = new Mock>();
- Assert.ThrowsException(() => new IrcProvider(mockJobManager.Object, mockAss.Object, mockAsyncDelayer.Object, mockLogger.Object, null));
+ Assert.ThrowsException(() => new IrcProvider(mockJobManager.Object, mockAsyncDelayer.Object, mockLogger.Object, null, null));
+ var mockAss = new Mock();
+ Assert.ThrowsException(() => new IrcProvider(mockJobManager.Object, mockAsyncDelayer.Object, mockLogger.Object, mockAss.Object, null));
var mockBot = new ChatBot
{
@@ -33,7 +39,7 @@ namespace Tgstation.Server.Host.Components.Chat.Providers.Tests
Provider = ChatProvider.Irc
};
- Assert.ThrowsException(() => new IrcProvider(mockJobManager.Object, mockAss.Object, mockAsyncDelayer.Object, mockLogger.Object, mockBot));
+ Assert.ThrowsException(() => new IrcProvider(mockJobManager.Object, mockAsyncDelayer.Object, mockLogger.Object, mockAss.Object, mockBot));
mockBot.ConnectionString = new IrcConnectionStringBuilder
{
@@ -43,7 +49,46 @@ namespace Tgstation.Server.Host.Components.Chat.Providers.Tests
Port = 6667
}.ToString();
- await new IrcProvider(mockJobManager.Object, mockAss.Object, mockAsyncDelayer.Object, mockLogger.Object, mockBot).DisposeAsync();
+ await new IrcProvider(mockJobManager.Object, mockAsyncDelayer.Object, mockLogger.Object, mockAss.Object, mockBot).DisposeAsync();
+ }
+
+ static Task InvokeConnect(IProvider provider, CancellationToken cancellationToken = default) => (Task)provider.GetType().GetMethod("Connect", BindingFlags.Instance | BindingFlags.NonPublic).Invoke(provider, new object[] { cancellationToken });
+
+ [TestMethod]
+ public async Task TestConnectAndDisconnect()
+ {
+ var actualToken = Environment.GetEnvironmentVariable("TGS_TEST_IRC_CONNECTION_STRING");
+ if (actualToken == null)
+ Assert.Inconclusive("Required environment variable TGS_TEST_IRC_CONNECTION_STRING isn't set!");
+
+ if (!new IrcConnectionStringBuilder(actualToken).Valid)
+ Assert.Fail("TGS_TEST_IRC_CONNECTION_STRING is not a valid IRC connection string!");
+
+ using var loggerFactory = LoggerFactory.Create(builder =>
+ {
+ builder.AddConsole();
+ builder.SetMinimumLevel(LogLevel.Trace);
+ });
+ var mockSetup = new Mock();
+ mockSetup
+ .Setup(x => x.RegisterOperation(It.IsNotNull(), It.IsNotNull(), It.IsAny()))
+ .Callback((job, entrypoint, cancellationToken) => job.StartedBy ??= new User { })
+ .Returns(Task.CompletedTask);
+ mockSetup
+ .Setup(x => x.WaitForJobCompletion(It.IsNotNull(), It.IsAny(), It.IsAny(), It.IsAny()))
+ .Returns(Task.CompletedTask);
+ var mockJobManager = mockSetup.Object;
+ await using var provider = new IrcProvider(mockJobManager, new AsyncDelayer(), loggerFactory.CreateLogger(), Mock.Of(), new ChatBot
+ {
+ ConnectionString = actualToken,
+ Provider = ChatProvider.Irc,
+ });
+ Assert.IsFalse(provider.Connected);
+ await InvokeConnect(provider);
+ Assert.IsTrue(provider.Connected);
+
+ await provider.Disconnect(default);
+ Assert.IsFalse(provider.Connected);
}
}
}
diff --git a/tests/Tgstation.Server.Tests/Live/DummyChatProvider.cs b/tests/Tgstation.Server.Tests/Live/DummyChatProvider.cs
new file mode 100644
index 0000000000..331931cef6
--- /dev/null
+++ b/tests/Tgstation.Server.Tests/Live/DummyChatProvider.cs
@@ -0,0 +1,303 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Threading;
+using System.Threading.Tasks;
+
+using Microsoft.Extensions.Logging;
+using Microsoft.VisualStudio.TestTools.UnitTesting;
+
+using Moq;
+
+using Tgstation.Server.Host.Components.Chat;
+using Tgstation.Server.Host.Components.Chat.Commands;
+using Tgstation.Server.Host.Components.Chat.Providers;
+using Tgstation.Server.Host.Components.Interop;
+using Tgstation.Server.Host.Jobs;
+using Tgstation.Server.Host.Models;
+using Tgstation.Server.Host.Security;
+using Tgstation.Server.Host.Utils;
+
+namespace Tgstation.Server.Tests.Live
+{
+ sealed class DummyChatProvider : Provider
+ {
+ public override bool Connected => connected;
+
+ public override string BotMention => $"Dummy{ChatBot.Provider}-I-{ChatBot.InstanceId}-N-{ChatBot.Name}";
+
+ static int enableRandomDisconnections = 1;
+
+ readonly Random random; // this RNG isn't perfect as calls into this class can theoretically happen in a random order due to async
+
+ readonly IReadOnlyCollection commands;
+ readonly ICryptographySuite cryptographySuite;
+ readonly CancellationTokenSource randomMessageCts;
+ readonly Task randomMessageTask;
+
+ bool connectedOnce;
+ bool connected;
+
+ ulong channelIdAllocator;
+
+ static ILoggerFactory CreateLoggerFactoryForLogger(ILogger logger, out Mock mockLoggerFactory)
+ {
+ mockLoggerFactory = new Mock();
+ mockLoggerFactory.Setup(x => x.CreateLogger(It.IsAny())).Returns(() =>
+ {
+ var temp = logger;
+ logger = null;
+
+ Assert.IsNotNull(temp);
+ return temp;
+ })
+ .Verifiable();
+ return mockLoggerFactory.Object;
+ }
+
+ static IAsyncDelayer CreateMockDelayer()
+ {
+ // at time of writing, this is used exclusively for the reconnection interval which works in minutes
+ // shorten it to 3s
+ var mock = new Mock();
+ mock.Setup(x => x.Delay(It.IsAny(), It.IsAny())).Returns((delay, cancellationToken) => Task.Delay(TimeSpan.FromSeconds(3), cancellationToken));
+ return mock.Object;
+ }
+ public static async Task RandomDisconnections(bool enabled, CancellationToken cancellationToken)
+ {
+ if (Interlocked.Exchange(ref enableRandomDisconnections, enabled ? 1 : 0) != 0 && !enabled)
+ await Task.Delay(TimeSpan.FromSeconds(5), cancellationToken);
+ }
+
+ public DummyChatProvider(
+ IJobManager jobManager,
+ ILogger logger,
+ ChatBot chatBot,
+ ICryptographySuite cryptographySuite,
+ IReadOnlyCollection commands,
+ Random random)
+ : base(jobManager, CreateMockDelayer(), new Logger(CreateLoggerFactoryForLogger(logger, out var mockLoggerFactory)), chatBot)
+ {
+ mockLoggerFactory.VerifyAll();
+ this.cryptographySuite = cryptographySuite ?? throw new ArgumentNullException(nameof(cryptographySuite));
+ this.commands = commands ?? throw new ArgumentNullException(nameof(commands));
+ this.random = random ?? throw new ArgumentNullException(nameof(random));
+
+ // this could be random but there's no point
+ channelIdAllocator = 100000;
+ logger.LogTrace("Base channel ID {baseChannelId}", channelIdAllocator);
+
+ this.randomMessageCts = new CancellationTokenSource();
+ this.randomMessageTask = RandomMessageLoop(this.randomMessageCts.Token);
+ }
+
+ public override async ValueTask DisposeAsync()
+ {
+ Logger.LogTrace("DisposeAsync Child");
+ this.randomMessageCts.Cancel();
+ this.randomMessageCts.Dispose();
+ await this.randomMessageTask;
+ await base.DisposeAsync();
+ }
+
+ public override Task SendMessage(Message replyTo, MessageContent message, ulong channelId, CancellationToken cancellationToken)
+ {
+ if (message == null)
+ throw new ArgumentNullException(nameof(message));
+
+ Logger.LogTrace("SendMessage");
+
+ Assert.AreNotEqual(0UL, channelId);
+ Assert.IsTrue(channelId <= channelIdAllocator);
+
+ cancellationToken.ThrowIfCancellationRequested();
+
+ /* SendMessage is no-throw
+ if (random.Next(0, 100) > 70)
+ throw new Exception("Random SendMessage failure!"); */
+
+ return Task.CompletedTask;
+ }
+
+ public override Task> SendUpdateMessage(RevisionInformation revisionInformation, Version byondVersion, DateTimeOffset? estimatedCompletionTime, string gitHubOwner, string gitHubRepo, ulong channelId, bool localCommitPushed, CancellationToken cancellationToken)
+ {
+ if (revisionInformation == null)
+ throw new ArgumentNullException(nameof(revisionInformation));
+ if (byondVersion == null)
+ throw new ArgumentNullException(nameof(byondVersion));
+ if (gitHubOwner == null)
+ throw new ArgumentNullException(nameof(gitHubOwner));
+ if (gitHubRepo == null)
+ throw new ArgumentNullException(nameof(gitHubRepo));
+
+ Logger.LogTrace("SendUpdateMessage");
+
+ Assert.AreNotEqual(0UL, channelId);
+ Assert.IsTrue(channelId <= channelIdAllocator);
+
+ cancellationToken.ThrowIfCancellationRequested();
+
+ /* SendUpdateMessage is no-throw
+ if (random.Next(0, 100) > 70)
+ throw new Exception("Random SendUpdateMessage failure!"); */
+
+ return Task.FromResult>((_, _) =>
+ {
+ cancellationToken.ThrowIfCancellationRequested();
+
+ /* SendUpdateMessage callbacks are no-throw
+ if (random.Next(0, 100) > 70)
+ throw new Exception("Random SendUpdateMessage failure!"); */
+
+ return Task.CompletedTask;
+ });
+ }
+
+ protected override Task Connect(CancellationToken cancellationToken)
+ {
+ Logger.LogTrace("Connect");
+ cancellationToken.ThrowIfCancellationRequested();
+
+ // 30% chance to fail AFTER initial connection
+ if (connectedOnce && enableRandomDisconnections != 0 && random.Next(0, 100) > 70)
+ throw new Exception("Random connection failure!");
+
+ connected = true;
+ connectedOnce = true;
+ return Task.CompletedTask;
+ }
+
+ protected override Task DisconnectImpl(CancellationToken cancellationToken)
+ {
+ Logger.LogTrace("DisconnectImpl");
+ cancellationToken.ThrowIfCancellationRequested();
+ connected = false;
+
+ if (random.Next(0, 100) > 70)
+ throw new Exception("Random disconnection failure!");
+ return Task.CompletedTask;
+ }
+
+ protected override Task>> MapChannelsImpl(IEnumerable channels, CancellationToken cancellationToken)
+ {
+ channels = channels.ToList();
+ Logger.LogTrace("MapChannels: [{channels}]", String.Join(", ", channels.Select(channel => channel.IrcChannel ?? channel.DiscordChannelId?.ToString() ?? throw new InvalidOperationException("BAD CHANNEL"))));
+
+ cancellationToken.ThrowIfCancellationRequested();
+
+ /* MapChannelsImpl is no-throw
+ if (random.Next(0, 100) > 70)
+ throw new Exception("Random MapChannelsImpl failure!"); */
+
+ return Task.FromResult(
+ new Dictionary>(
+ channels.Select(
+ channel => new KeyValuePair>(
+ channel,
+ new List
+ {
+ new ChannelRepresentation
+ {
+ IsAdminChannel = channel.IsAdminChannel.Value,
+ ConnectionName = $"Connection_{channelIdAllocator + 1}",
+ EmbedsSupported = ChatBot.Provider.Value != Api.Models.ChatProvider.Irc,
+ FriendlyName = $"(Friendly) Channel_ID_{channelIdAllocator + 1}",
+ IsPrivateChannel = false,
+ RealId = ++channelIdAllocator,
+ Tag = channel.Tag,
+ }
+ }))));
+ }
+
+ async Task RandomMessageLoop(CancellationToken cancellationToken)
+ {
+ Logger.LogTrace("RandomMessageLoop");
+ try
+ {
+ for (var i = 0UL; !cancellationToken.IsCancellationRequested; ++i)
+ {
+ // random intervals under 10s
+ var delay = random.Next(0, 10000);
+ await Task.Delay(delay, cancellationToken);
+
+ if (!connected)
+ continue;
+
+ // %5 chance to disconnect randomly
+ if (enableRandomDisconnections != 0 && random.Next(0, 100) > 95)
+ connected = false;
+
+ if (channelIdAllocator >= Int32.MaxValue / 2)
+ Assert.Fail("Too many channels have been allocated!");
+
+ var isPm = channelIdAllocator == 0 || random.Next(0, 100) > 20;
+ var realId = (ulong)random.Next(1, (int)channelIdAllocator);
+
+ if (isPm)
+ realId += Int32.MaxValue / 2;
+
+ var username = $"RandomUser{i}";
+ var sender = new ChatUser
+ {
+ Channel = new ChannelRepresentation
+ {
+ RealId = realId,
+ IsPrivateChannel = isPm,
+ ConnectionName = isPm ? $"{username}_Connection" : $"Connection_{realId}",
+ FriendlyName = isPm ? $"{username}_Channel" : $"(Friendly) Channel_ID_{realId}",
+ EmbedsSupported = ChatBot.Provider.Value != Api.Models.ChatProvider.Irc,
+
+ // isAdmin and Tag populated by manager
+ },
+ FriendlyName = username,
+ RealId = i + 50000,
+ Mention = $"@{username}",
+ };
+
+ var dice = random.Next(0, 100);
+ string content;
+ // 70% chance to be random chat
+ if (dice < 70)
+ content = cryptographySuite.GetSecureString();
+ // 15% chance to be a !tgs
+ else if (dice < 85)
+ content = "!tgs";
+ // 15% chance to be a strict mention
+ else
+ content = BotMention;
+
+ // 30% chance to request help
+ if (random.Next(0, 100) > 70)
+ content = $"{content} help";
+
+ dice = random.Next(0, 100);
+
+ // 20% chance to whiff
+ if (dice > 20)
+ // 40% chance to attempt a built-in TGS command
+ if (dice < 68)
+ // equal chance for each
+ content = $"{content} {commands.ElementAt(random.Next(0, commands.Count)).Name}";
+ // 40% chance to attempt a custom chat command in long_running_test
+ else
+ // equal chance for each
+ if (random.Next(0, 100) > 50)
+ content = $"{content} embeds_test";
+ else
+ content = $"{content} response_overload_test";
+
+ EnqueueMessage(new Message
+ {
+ Content = content,
+ User = sender,
+ });
+ }
+
+ }
+ catch (OperationCanceledException)
+ {
+ Logger.LogTrace("RandomMessageLoop cancelled");
+ }
+ }
+ }
+}
diff --git a/tests/Tgstation.Server.Tests/Live/DummyChatProviderFactory.cs b/tests/Tgstation.Server.Tests/Live/DummyChatProviderFactory.cs
new file mode 100644
index 0000000000..55162d85c2
--- /dev/null
+++ b/tests/Tgstation.Server.Tests/Live/DummyChatProviderFactory.cs
@@ -0,0 +1,84 @@
+using System;
+using System.Collections.Generic;
+
+using Microsoft.Extensions.Logging;
+
+using Moq;
+
+using Tgstation.Server.Api.Models;
+using Tgstation.Server.Host.Components.Byond;
+using Tgstation.Server.Host.Components.Chat.Commands;
+using Tgstation.Server.Host.Components.Chat.Providers;
+using Tgstation.Server.Host.Components.Repository;
+using Tgstation.Server.Host.Components.Watchdog;
+using Tgstation.Server.Host.Database;
+using Tgstation.Server.Host.Jobs;
+using Tgstation.Server.Host.Models;
+using Tgstation.Server.Host.Security;
+using Tgstation.Server.Host.System;
+
+namespace Tgstation.Server.Tests.Live
+{
+ sealed class DummyChatProviderFactory : IProviderFactory
+ {
+ readonly IJobManager jobManager;
+ readonly ICryptographySuite cryptographySuite;
+ readonly ILoggerFactory loggerFactory;
+ readonly ILogger logger;
+
+ readonly IReadOnlyList commands;
+
+ readonly Dictionary seededRng;
+
+ public DummyChatProviderFactory(IJobManager jobManager, ICryptographySuite cryptographySuite, ILoggerFactory loggerFactory, ILogger logger)
+ {
+ this.jobManager = jobManager ?? throw new ArgumentNullException(nameof(jobManager));
+ this.cryptographySuite = cryptographySuite ?? throw new ArgumentNullException(nameof(cryptographySuite));
+ this.loggerFactory = loggerFactory ?? throw new ArgumentNullException(nameof(loggerFactory));
+ this.logger = logger ?? throw new ArgumentNullException(nameof(logger));
+
+ var commandFactory = new CommandFactory(
+ Mock.Of(),
+ Mock.Of(),
+ Mock.Of(),
+ Mock.Of(),
+ new Host.Models.Instance());
+
+ commandFactory.SetWatchdog(Mock.Of());
+ commands = commandFactory.GenerateCommands();
+
+ var baseRng = new Random(22475);
+ seededRng = new Dictionary{
+ { ChatProvider.Irc, new Random(baseRng.Next()) },
+ { ChatProvider.Discord, new Random(baseRng.Next()) },
+ }; // hope you get the reference
+ }
+
+ public IProvider CreateProvider(ChatBot settings)
+ {
+ logger.LogTrace("CreateProvider");
+ if (settings == null)
+ throw new ArgumentNullException(nameof(settings));
+
+ var provider = settings.Provider;
+ switch (provider)
+ {
+ case ChatProvider.Irc:
+ case ChatProvider.Discord:
+ logger.LogTrace("Creating DummyChatProvider in place of requested {providerType}Provider", settings.Provider);
+
+ // for RNG to work, chat bots need to get created in a certain order
+ // the ChatTest creates one of each provider type
+ return new DummyChatProvider(
+ jobManager,
+ loggerFactory.CreateLogger($"Dummy{settings.Provider}Provider"),
+ settings,
+ cryptographySuite,
+ commands,
+ new Random(seededRng[provider.Value].Next()));
+ default:
+ throw new InvalidOperationException($"Invalid ChatProvider: {provider}");
+ }
+ }
+ }
+}
diff --git a/tests/Tgstation.Server.Tests/Live/Instance/ChatTest.cs b/tests/Tgstation.Server.Tests/Live/Instance/ChatTest.cs
index ab8553fc22..e53f5d54bc 100644
--- a/tests/Tgstation.Server.Tests/Live/Instance/ChatTest.cs
+++ b/tests/Tgstation.Server.Tests/Live/Instance/ChatTest.cs
@@ -13,13 +13,14 @@ using Tgstation.Server.Client.Components;
namespace Tgstation.Server.Tests.Live.Instance
{
- sealed class ChatTest
+ sealed class ChatTest : JobsRequiredTest
{
readonly IChatBotsClient chatClient;
readonly IInstanceManagerClient instanceClient;
readonly Api.Models.Instance metadata;
- public ChatTest(IChatBotsClient chatClient, IInstanceManagerClient instanceClient, Api.Models.Instance metadata)
+ public ChatTest(IChatBotsClient chatClient, IInstanceManagerClient instanceClient, IJobsClient jobsClient, Api.Models.Instance metadata)
+ : base(jobsClient)
{
this.chatClient = chatClient ?? throw new ArgumentNullException(nameof(chatClient));
this.instanceClient = instanceClient ?? throw new ArgumentNullException(nameof(instanceClient));
@@ -40,9 +41,25 @@ namespace Tgstation.Server.Tests.Live.Instance
async Task RunIrc(CancellationToken cancellationToken)
{
+ var connectionString = Environment.GetEnvironmentVariable("TGS_TEST_IRC_CONNECTION_STRING");
+ if (String.IsNullOrWhiteSpace(connectionString))
+ // needs to just be valid
+ connectionString = new IrcConnectionStringBuilder
+ {
+ Address = "irc.fake.com",
+ Nickname = "irc_nick",
+ Password = "some_pw",
+ PasswordType = IrcPasswordType.Server,
+ Port = 6668,
+ UseSsl = true,
+ }.ToString();
+ else
+ // standardize
+ connectionString = new IrcConnectionStringBuilder(connectionString).ToString();
+
var firstBotReq = new ChatBotCreateRequest
{
- ConnectionString = Environment.GetEnvironmentVariable("TGS_TEST_IRC_CONNECTION_STRING"),
+ ConnectionString = connectionString,
Enabled = false,
Name = "tgs_integration_test",
Provider = ChatProvider.Irc,
@@ -65,6 +82,8 @@ namespace Tgstation.Server.Tests.Live.Instance
var retrievedBot = await chatClient.GetId(firstBot, cancellationToken);
Assert.AreEqual(firstBot.Id, retrievedBot.Id);
+ var beforeChatBotEnabled = DateTimeOffset.UtcNow;
+
var updatedBot = await chatClient.Update(new ChatBotUpdateRequest
{
Id = firstBot.Id,
@@ -73,7 +92,20 @@ namespace Tgstation.Server.Tests.Live.Instance
Assert.AreEqual(true, updatedBot.Enabled);
- var channelId = Environment.GetEnvironmentVariable("TGS_TEST_IRC_CHANNEL"); ;
+ await Task.Delay(TimeSpan.FromSeconds(5), cancellationToken);
+
+ var jobs = await JobsClient.List(null, cancellationToken);
+ var reconnectJob = jobs
+ .Where(x => x.StartedAt >= beforeChatBotEnabled && x.Description.Contains(updatedBot.Name))
+ .OrderByDescending(x => x.StartedAt)
+ .FirstOrDefault();
+
+ Assert.IsNotNull(reconnectJob);
+ await WaitForJob(reconnectJob, 60, false, null, cancellationToken);
+
+ var channelId = Environment.GetEnvironmentVariable("TGS_TEST_IRC_CHANNEL");
+ if (String.IsNullOrWhiteSpace(channelId))
+ channelId = "#botbus";
updatedBot = await chatClient.Update(new ChatBotUpdateRequest
{
@@ -110,14 +142,23 @@ namespace Tgstation.Server.Tests.Live.Instance
async Task RunDiscord(CancellationToken cancellationToken)
{
+ var connectionString = Environment.GetEnvironmentVariable("TGS_TEST_DISCORD_TOKEN");
+ if (String.IsNullOrWhiteSpace(connectionString))
+ // needs to just be valid
+ connectionString = new DiscordConnectionStringBuilder
+ {
+ BasedMeme = true,
+ BotToken = "some_token",
+ DeploymentBranding = true,
+ DMOutputDisplay = DiscordDMOutputDisplayType.Never,
+ }.ToString();
+ else
+ // standardize
+ connectionString = new DiscordConnectionStringBuilder(connectionString).ToString();
+
var firstBotReq = new ChatBotCreateRequest
{
- ConnectionString =
- new DiscordConnectionStringBuilder
- {
- BotToken = Environment.GetEnvironmentVariable("TGS_TEST_DISCORD_TOKEN"),
- DMOutputDisplay = DiscordDMOutputDisplayType.OnError
- }.ToString(),
+ ConnectionString = connectionString,
Enabled = false,
Name = "r4407",
Provider = ChatProvider.Discord,
@@ -140,6 +181,8 @@ namespace Tgstation.Server.Tests.Live.Instance
var retrievedBot = await chatClient.GetId(firstBot, cancellationToken);
Assert.AreEqual(firstBot.Id, retrievedBot.Id);
+ var beforeChatBotEnabled = DateTimeOffset.UtcNow;
+
var updatedBot = await chatClient.Update(new ChatBotUpdateRequest
{
Id = firstBot.Id,
@@ -148,7 +191,22 @@ namespace Tgstation.Server.Tests.Live.Instance
Assert.AreEqual(true, updatedBot.Enabled);
- var channelId = ulong.Parse(Environment.GetEnvironmentVariable("TGS_TEST_DISCORD_CHANNEL"));
+ await Task.Delay(TimeSpan.FromSeconds(5), cancellationToken);
+
+ var jobs = await JobsClient.List(null, cancellationToken);
+ var reconnectJob = jobs
+ .Where(x => x.StartedAt >= beforeChatBotEnabled && x.Description.Contains(updatedBot.Name))
+ .OrderByDescending(x => x.StartedAt)
+ .FirstOrDefault();
+
+ Assert.IsNotNull(reconnectJob);
+ await WaitForJob(reconnectJob, 60, false, null, cancellationToken);
+
+ var channelIdStr = Environment.GetEnvironmentVariable("TGS_TEST_DISCORD_CHANNEL");
+ if (String.IsNullOrWhiteSpace(channelIdStr))
+ channelIdStr = "487268744419344384";
+
+ var channelId = ulong.Parse(channelIdStr);
updatedBot = await chatClient.Update(new ChatBotUpdateRequest
{
diff --git a/tests/Tgstation.Server.Tests/Live/Instance/InstanceTest.cs b/tests/Tgstation.Server.Tests/Live/Instance/InstanceTest.cs
index dae2bee1d9..fd502926a9 100644
--- a/tests/Tgstation.Server.Tests/Live/Instance/InstanceTest.cs
+++ b/tests/Tgstation.Server.Tests/Live/Instance/InstanceTest.cs
@@ -26,7 +26,7 @@ namespace Tgstation.Server.Tests.Live.Instance
public async Task RunTests(CancellationToken cancellationToken)
{
var byondTest = new ByondTest(instanceClient.Byond, instanceClient.Jobs, instanceClient.Metadata);
- var chatTest = new ChatTest(instanceClient.ChatBots, instanceManagerClient, instanceClient.Metadata);
+ var chatTest = new ChatTest(instanceClient.ChatBots, instanceManagerClient, instanceClient.Jobs, instanceClient.Metadata);
var configTest = new ConfigurationTest(instanceClient.Configuration, instanceClient.Metadata);
var repoTest = new RepositoryTest(instanceClient.Repository, instanceClient.Jobs);
var dmTest = new DeploymentTest(instanceClient, instanceClient.Jobs);
diff --git a/tests/Tgstation.Server.Tests/Live/Instance/WatchdogTest.cs b/tests/Tgstation.Server.Tests/Live/Instance/WatchdogTest.cs
index 484be88225..78b287a641 100644
--- a/tests/Tgstation.Server.Tests/Live/Instance/WatchdogTest.cs
+++ b/tests/Tgstation.Server.Tests/Live/Instance/WatchdogTest.cs
@@ -79,6 +79,9 @@ namespace Tgstation.Server.Tests.Live.Instance
await TestDMApiFreeDeploy(cancellationToken);
+ // long running test likes consistency with the channels
+ await DummyChatProvider.RandomDisconnections(false, cancellationToken);
+
await RunLongRunningTestThenUpdate(cancellationToken);
await RunLongRunningTestThenUpdateWithNewDme(cancellationToken);
@@ -717,9 +720,8 @@ namespace Tgstation.Server.Tests.Live.Instance
async Task RunLongRunningTestThenUpdateWithNewDme(CancellationToken cancellationToken)
{
System.Console.WriteLine("TEST: WATCHDOG LONG RUNNING WITH NEW DME TEST");
- const string DmeName = "LongRunning/long_running_test";
- var daemonStatus = await DeployTestDme(DmeName, DreamDaemonSecurity.Trusted, true, cancellationToken);
+ var daemonStatus = await DeployTestDme("LongRunning/long_running_test", DreamDaemonSecurity.Trusted, true, cancellationToken);
var initialCompileJob = daemonStatus.ActiveCompileJob;
Assert.AreEqual(WatchdogStatus.Offline, daemonStatus.Status.Value);
@@ -732,7 +734,7 @@ namespace Tgstation.Server.Tests.Live.Instance
await WaitForJob(startJob, 40, false, null, cancellationToken);
- daemonStatus = await DeployTestDme(DmeName + "_copy", DreamDaemonSecurity.Safe, true, cancellationToken);
+ daemonStatus = await DeployTestDme("LongRunning/long_running_test_copy", DreamDaemonSecurity.Safe, true, cancellationToken);
Assert.AreEqual(WatchdogStatus.Online, daemonStatus.Status.Value);
diff --git a/tests/Tgstation.Server.Tests/Live/TestLiveServer.cs b/tests/Tgstation.Server.Tests/Live/TestLiveServer.cs
index 096b4e8b12..e39d36650e 100644
--- a/tests/Tgstation.Server.Tests/Live/TestLiveServer.cs
+++ b/tests/Tgstation.Server.Tests/Live/TestLiveServer.cs
@@ -44,7 +44,7 @@ namespace Tgstation.Server.Tests.Live
public static ushort DDPort { get; } = FreeTcpPort();
public static ushort DMPort { get; } = GetDMPort();
- readonly Version TestUpdateVersion = new Version(5, 11, 0);
+ readonly Version TestUpdateVersion = new(5, 11, 0);
readonly IServerClientFactory clientFactory = new ServerClientFactory(new ProductHeaderValue(Assembly.GetExecutingAssembly().GetName().Name, Assembly.GetExecutingAssembly().GetName().Version.ToString()));
@@ -82,6 +82,7 @@ namespace Tgstation.Server.Tests.Live
[TestInitialize]
public async Task Initialize()
{
+ await DummyChatProvider.RandomDisconnections(true, default);
ServerClientFactory.ApiClientFactory = new RateLimitRetryingApiClientFactory();
var connectionString = Environment.GetEnvironmentVariable("TGS_TEST_CONNECTION_STRING");
@@ -697,6 +698,32 @@ namespace Tgstation.Server.Tests.Live
[TestMethod]
public async Task TestStandardTgsOperation()
{
+ var discordConnectionString = Environment.GetEnvironmentVariable("TGS_TEST_DISCORD_TOKEN");
+ var ircConnectionString = Environment.GetEnvironmentVariable("TGS_TEST_IRC_CONNECTION_STRING");
+ var missingChatVarsCount = Convert.ToInt32(String.IsNullOrWhiteSpace(discordConnectionString))
+ + Convert.ToInt32(String.IsNullOrWhiteSpace(Environment.GetEnvironmentVariable("TGS_TEST_DISCORD_CHANNEL")))
+ + Convert.ToInt32(String.IsNullOrWhiteSpace(ircConnectionString))
+ + Convert.ToInt32(String.IsNullOrWhiteSpace(Environment.GetEnvironmentVariable("TGS_TEST_IRC_CHANNEL")));
+
+ const int TotalChatVars = 4;
+
+ // uncomment to force this test to run with DummyChatProviders
+ // missingChatVarsCount = TotalChatVars;
+
+ if (missingChatVarsCount != 0)
+ {
+ if (missingChatVarsCount != TotalChatVars)
+ Assert.Fail("All TGS_TEST_* chat environment variables must be present or none at all!");
+
+ ServiceCollectionExtensions.UseChatProviderFactory();
+ }
+ else
+ {
+ // prevalidate
+ Assert.IsTrue(new DiscordConnectionStringBuilder(discordConnectionString).Valid);
+ Assert.IsTrue(new IrcConnectionStringBuilder(ircConnectionString).Valid);
+ }
+
var procs = System.Diagnostics.Process.GetProcessesByName("byond");
if (procs.Any())
{
@@ -732,6 +759,7 @@ namespace Tgstation.Server.Tests.Live
using var httpClient = new HttpClient();
var webRequestTask = httpClient.GetAsync(server.Url.ToString() + "swagger/v1/swagger.json");
using var response = await webRequestTask;
+ response.EnsureSuccessStatusCode();
using var content = await response.Content.ReadAsStreamAsync();
using var output = new FileStream(@"C:\swagger.json", FileMode.Create);
await content.CopyToAsync(output);
@@ -840,6 +868,26 @@ namespace Tgstation.Server.Tests.Live
var dd = await instanceClient.DreamDaemon.Read(cancellationToken);
Assert.AreEqual(WatchdogStatus.Online, dd.Status.Value);
+ var chatReadTask = instanceClient.ChatBots.List(null, cancellationToken);
+
+ // Check the DMAPI got the channels again https://github.com/tgstation/tgstation-server/issues/1490
+ topicRequestResult = await WatchdogTest.TopicClient.SendTopic(
+ IPAddress.Loopback,
+ $"tgs_integration_test_tactics7=1",
+ DDPort,
+ cancellationToken);
+
+ Assert.IsNotNull(topicRequestResult);
+ if(!Int32.TryParse(topicRequestResult.StringData, out var channelsPresent))
+ {
+ Assert.Fail("Expected DD to send us an int!");
+ }
+
+ var currentChatBots = await chatReadTask;
+ var connectedChannelCount = currentChatBots.Where(x => x.Enabled.Value).SelectMany(x => x.Channels).Count();
+
+ Assert.AreEqual(connectedChannelCount, channelsPresent);
+
await instanceClient.DreamDaemon.Shutdown(cancellationToken);
dd = await instanceClient.DreamDaemon.Update(new DreamDaemonRequest
{
@@ -948,7 +996,7 @@ namespace Tgstation.Server.Tests.Live
Assert.IsNull(currentDD.StagedCompileJob);
var repoTest = new RepositoryTest(instanceClient.Repository, instanceClient.Jobs).RunPostTest(cancellationToken);
- await new ChatTest(instanceClient.ChatBots, adminClient.Instances, instance).RunPostTest(cancellationToken);
+ await new ChatTest(instanceClient.ChatBots, adminClient.Instances, instanceClient.Jobs, instance).RunPostTest(cancellationToken);
await repoTest;
await new InstanceManagerTest(adminClient, server.Directory).RunPostTest(cancellationToken);
diff --git a/tgstation-server.sln b/tgstation-server.sln
index 2a7bda3f3d..fa1f1bf89f 100644
--- a/tgstation-server.sln
+++ b/tgstation-server.sln
@@ -93,7 +93,8 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "src", "src", "{3BB10856-AA0
EndProject
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "tests", "tests", "{82066812-6C73-4360-943B-B23F2F491261}"
ProjectSection(SolutionItems) = preProject
- tests\DMAPI\tgs_include.dm = tests\DMAPI\tgs_include.dm
+ tests\DMAPI\test_prelude.dm = tests\DMAPI\test_prelude.dm
+ tests\DMAPI\test_setup.dm = tests\DMAPI\test_setup.dm
EndProjectSection
EndProject
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "v4", "v4", "{057FAC33-CC31-4948-91C6-B0977C335890}"
@@ -193,7 +194,7 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Tgstation.Server.Migrator.C
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Tgstation.Server.Host.Common", "src\Tgstation.Server.Host.Common\Tgstation.Server.Host.Common.csproj", "{CF3968A0-EA81-4464-B2D4-C7D40F6B5BCB}"
EndProject
-Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Tgstation.Server.Common", "src\Tgstation.Server.Common\Tgstation.Server.Common.csproj", "{70CD9A98-D31A-44A4-81D1-D02764CEEEFD}"
+Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Tgstation.Server.Common", "src\Tgstation.Server.Common\Tgstation.Server.Common.csproj", "{70CD9A98-D31A-44A4-81D1-D02764CEEEFD}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution