diff --git a/.dockerignore b/.dockerignore
index 4a54e2617a..0558f70123 100644
--- a/.dockerignore
+++ b/.dockerignore
@@ -21,6 +21,8 @@ build/**
!build/stylecop.json
!build/Version.props
!build/ControlPanelVersion.props
+!build/Common.props
+!build/NugetCommon.props
docs
src/DMAPI
src/Tgstation.Server.Host/ClientApp
diff --git a/.github/CONTRIBUTING.md b/.github/CONTRIBUTING.md
index 79ee3f7ac3..4348930e99 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
@@ -173,6 +174,8 @@ Whenever you make a change to a model schema that must be reflected in the datab
We have a script to do this.
+Warning: You may need to temporarily set valid MySql credentials in [MySqlDesignTimeDbContextFactory.cs](../src/Tgstation.Server.Host/Database/Design/MySqlDesignTimeDbContextFactory.cs) for migrations to generate properly. I have no idea why. Be careful not to commit the change.
+
1. Run `build/GenerateMigrations.sh NameOfMigration` from the project root.
1. You should now have MY/MS/SL/PG migration files generated in `/src/Tgstation.Server.Host/Models/Migrations`. Fix compiler warnings in the generated files. Ensure all classes are in the Tgstation.Server.Host.Database.Migrations namespace.
1. Manually review what each migration does.
diff --git a/.github/workflows/ci-suite.yml b/.github/workflows/ci-suite.yml
index 005ca7b1e6..217b2f4f01 100644
--- a/.github/workflows/ci-suite.yml
+++ b/.github/workflows/ci-suite.yml
@@ -19,10 +19,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 }}
@@ -233,6 +229,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
@@ -273,6 +272,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
@@ -310,11 +312,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
@@ -331,11 +333,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)
uses: actions/checkout@v3
@@ -363,7 +367,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/Common.props b/build/Common.props
new file mode 100644
index 0000000000..95ade51555
--- /dev/null
+++ b/build/Common.props
@@ -0,0 +1,10 @@
+
+
+
+
+ net6.0
+ netstandard2.0
+ latest
+ Full
+
+
diff --git a/build/Dockerfile b/build/Dockerfile
index c5baa2f3ff..716790bada 100644
--- a/build/Dockerfile
+++ b/build/Dockerfile
@@ -25,6 +25,8 @@ RUN npm install -g yarn
# Build web control panel
WORKDIR /repo/build
+COPY build/Common.props Common.props
+COPY build/NugetCommon.props NugetCommon.props
COPY build/Version.props Version.props
COPY build/ControlPanelVersion.props ControlPanelVersion.props
diff --git a/build/NugetCommon.props b/build/NugetCommon.props
new file mode 100644
index 0000000000..d844ff4b1a
--- /dev/null
+++ b/build/NugetCommon.props
@@ -0,0 +1,23 @@
+
+
+
+
+ true
+ Cyberboss/Dominion
+ /tg/station 13
+ https://tgstation.github.io/tgstation-server
+ LICENSE
+ tgs.png
+ Git
+ https://github.com/tgstation/tgstation-server
+ 2018-2023
+ true
+ snupkg
+ enable
+
+
+
+
+
+
+
diff --git a/build/Version.props b/build/Version.props
index 7929f4741d..b7a3d895b6 100644
--- a/build/Version.props
+++ b/build/Version.props
@@ -3,16 +3,15 @@
- 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
1.0.1
- net6.0
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.Api/Models/Internal/ChatChannelBase.cs b/src/Tgstation.Server.Api/Models/Internal/ChatChannelBase.cs
index 87da5746ae..eee19f244f 100644
--- a/src/Tgstation.Server.Api/Models/Internal/ChatChannelBase.cs
+++ b/src/Tgstation.Server.Api/Models/Internal/ChatChannelBase.cs
@@ -25,6 +25,12 @@ namespace Tgstation.Server.Api.Models.Internal
[Required]
public bool? IsUpdatesChannel { get; set; }
+ ///
+ /// If the received system messages.
+ ///
+ [Required]
+ public bool? IsSystemChannel { get; set; }
+
///
/// A custom tag users can define to group channels together.
///
diff --git a/src/Tgstation.Server.Api/Tgstation.Server.Api.csproj b/src/Tgstation.Server.Api/Tgstation.Server.Api.csproj
index 21a2efd47d..cb44534a9c 100644
--- a/src/Tgstation.Server.Api/Tgstation.Server.Api.csproj
+++ b/src/Tgstation.Server.Api/Tgstation.Server.Api.csproj
@@ -1,29 +1,15 @@
-
+
- netstandard2.0
- Full
+ $(TgsNugetNetVersion)
$(TgsApiLibraryVersion)
- true
- Cyberboss/Dominion
- /tg/station 13
API definitions for tgstation-server.
- https://tgstation.github.io/tgstation-server
- LICENSE
- tgs.png
- Git
- https://github.com/tgstation/tgstation-server
- 2018-2023
json web api tgstation-server tgstation ss13 byond http
Minor documentation correction.
- true
- snupkg
- ../../build/analyzers.ruleset
- latest
- enable
- bin\$(Configuration)\netstandard2.0\Tgstation.Server.Api.xml
true
+ ../../build/analyzers.ruleset
+ bin/$(Configuration)/$(TargetFramework)/$(AssemblyName).xml
CA1028
@@ -61,7 +47,5 @@
-
-
diff --git a/src/Tgstation.Server.Client/Tgstation.Server.Client.csproj b/src/Tgstation.Server.Client/Tgstation.Server.Client.csproj
index bd1874f5d5..07e99c9dd4 100644
--- a/src/Tgstation.Server.Client/Tgstation.Server.Client.csproj
+++ b/src/Tgstation.Server.Client/Tgstation.Server.Client.csproj
@@ -1,29 +1,15 @@
-
+
- netstandard2.0
- Full
+ $(TgsNugetNetVersion)
$(TgsClientVersion)
- true
- Cyberboss/Dominion
- /tg/station 13
Client library for tgstation-server.
- https://tgstation.github.io/tgstation-server
- LICENSE
- tgs.png
- Git
- https://github.com/tgstation/tgstation-server
- 2018-2023
json web api tgstation-server tgstation ss13 byond client http
Added missing Dispose() call to the StringContents for requests with bodies and added missing ConfigureAwait(false) to async call.
- true
- snupkg
- ../../build/analyzers.ruleset
- latest
- enable
- bin\$(Configuration)\netstandard2.0\Tgstation.Server.Client.xml
true
+ ../../build/analyzers.ruleset
+ bin/$(Configuration)/$(TargetFramework)/$(AssemblyName).xml
@@ -46,7 +32,5 @@
-
-
diff --git a/src/Tgstation.Server.Common/Tgstation.Server.Common.csproj b/src/Tgstation.Server.Common/Tgstation.Server.Common.csproj
index ad8396a76c..582e4741cc 100644
--- a/src/Tgstation.Server.Common/Tgstation.Server.Common.csproj
+++ b/src/Tgstation.Server.Common/Tgstation.Server.Common.csproj
@@ -1,29 +1,15 @@
-
+
- netstandard2.0
- Full
+ $(TgsNugetNetVersion)
$(TgsCoreVersion)
- true
- Cyberboss/Dominion
- /tg/station 13
Common functions for tgstation-server.
- https://tgstation.github.io/tgstation-server
- LICENSE
- tgs.png
- Git
- https://github.com/tgstation/tgstation-server
- 2018-2023
web tgstation-server tgstation ss13 byond client http
Initial release.
- true
- snupkg
- ../../build/analyzers.ruleset
- latest
- enable
- bin\$(Configuration)\netstandard2.0\Tgstation.Server.Client.xml
true
+ ../../build/analyzers.ruleset
+ bin/$(Configuration)/$(TargetFramework)/$(AssemblyName).xml
@@ -41,7 +27,5 @@
-
-
diff --git a/src/Tgstation.Server.Host.Common/Tgstation.Server.Host.Common.csproj b/src/Tgstation.Server.Host.Common/Tgstation.Server.Host.Common.csproj
index 35c6319a38..dbe6263f1d 100644
--- a/src/Tgstation.Server.Host.Common/Tgstation.Server.Host.Common.csproj
+++ b/src/Tgstation.Server.Host.Common/Tgstation.Server.Host.Common.csproj
@@ -1,15 +1,13 @@
-
+
netstandard2.0
- Full
$(TgsCoreVersion)
- ../../build/analyzers.ruleset
- latest
false
true
- bin\$(Configuration)\netstandard2.0\Tgstation.Server.Host.Shared.xml
+ ../../build/analyzers.ruleset
+ bin/$(Configuration)/$(TargetFramework)/$(AssemblyName).xml
diff --git a/src/Tgstation.Server.Host.Console/Tgstation.Server.Host.Console.csproj b/src/Tgstation.Server.Host.Console/Tgstation.Server.Host.Console.csproj
index 045d8ce9eb..4d433b2c54 100644
--- a/src/Tgstation.Server.Host.Console/Tgstation.Server.Host.Console.csproj
+++ b/src/Tgstation.Server.Host.Console/Tgstation.Server.Host.Console.csproj
@@ -1,15 +1,14 @@
-
+
Exe
- net6.0
- Full
+ $(TgsNetVersion)
$(TgsCoreVersion)
- ../../build/analyzers.ruleset
- latest
false
true
+ ../../build/analyzers.ruleset
+ bin/$(Configuration)/$(TargetFramework)/$(AssemblyName).xml
diff --git a/src/Tgstation.Server.Host.Service/Tgstation.Server.Host.Service.csproj b/src/Tgstation.Server.Host.Service/Tgstation.Server.Host.Service.csproj
index 5499f045f7..6f496e9c5e 100644
--- a/src/Tgstation.Server.Host.Service/Tgstation.Server.Host.Service.csproj
+++ b/src/Tgstation.Server.Host.Service/Tgstation.Server.Host.Service.csproj
@@ -1,16 +1,14 @@
-
+
Exe
net472
win
- Full
$(TgsCoreVersion)
- ../../build/analyzers.ruleset
- 7.3
- bin\Debug\Tgstation.Server.Host.Console.xml
true
+ ../../build/analyzers.ruleset
+ bin/$(Configuration)/$(TargetFramework)/$(RuntimeIdentifier)/$(AssemblyName).xml
diff --git a/src/Tgstation.Server.Host.Watchdog/Tgstation.Server.Host.Watchdog.csproj b/src/Tgstation.Server.Host.Watchdog/Tgstation.Server.Host.Watchdog.csproj
index e8b69ace2b..4fb825d496 100644
--- a/src/Tgstation.Server.Host.Watchdog/Tgstation.Server.Host.Watchdog.csproj
+++ b/src/Tgstation.Server.Host.Watchdog/Tgstation.Server.Host.Watchdog.csproj
@@ -1,16 +1,15 @@
-
+
netstandard2.0
Full
false
$(TgsHostWatchdogVersion)
- ../../build/analyzers.ruleset
- latest
false
true
- bin\$(Configuration)\netstandard2.0\Tgstation.Server.Host.Watchdog.xml
+ ../../build/analyzers.ruleset
+ bin/$(Configuration)/$(TargetFramework)/$(AssemblyName).xml
diff --git a/src/Tgstation.Server.Host/.config/dotnet-tools.json b/src/Tgstation.Server.Host/.config/dotnet-tools.json
index b8b93fc6c0..5bc20db4c2 100644
--- a/src/Tgstation.Server.Host/.config/dotnet-tools.json
+++ b/src/Tgstation.Server.Host/.config/dotnet-tools.json
@@ -3,7 +3,7 @@
"isRoot": true,
"tools": {
"dotnet-ef": {
- "version": "6.0.15",
+ "version": "6.0.16",
"commands": [
"dotnet-ef"
]
diff --git a/src/Tgstation.Server.Host/Components/Chat/ChannelMapping.cs b/src/Tgstation.Server.Host/Components/Chat/ChannelMapping.cs
index 333706e796..d7430d7c69 100644
--- a/src/Tgstation.Server.Host/Components/Chat/ChannelMapping.cs
+++ b/src/Tgstation.Server.Host/Components/Chat/ChannelMapping.cs
@@ -30,6 +30,11 @@
///
public bool IsAdminChannel { get; set; }
+ ///
+ /// If the is a system messages channel.
+ ///
+ public bool IsSystemChannel { get; set; }
+
///
/// The with the mapped Id.
///
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..99829dac91 100644
--- a/src/Tgstation.Server.Host/Components/Chat/ChatManager.cs
+++ b/src/Tgstation.Server.Host/Components/Chat/ChatManager.cs
@@ -207,6 +207,7 @@ namespace Tgstation.Server.Host.Components.Chat
IrcChannel = apiModel.IrcChannel,
IsAdminChannel = apiModel.IsAdminChannel,
IsUpdatesChannel = apiModel.IsUpdatesChannel,
+ IsSystemChannel = apiModel.IsSystemChannel,
IsWatchdogChannel = apiModel.IsWatchdogChannel,
Tag = apiModel.Tag,
})
@@ -220,6 +221,7 @@ namespace Tgstation.Server.Host.Components.Chat
IsWatchdogChannel = kvp.Key.IsWatchdogChannel == true,
IsUpdatesChannel = kvp.Key.IsUpdatesChannel == true,
IsAdminChannel = kvp.Key.IsAdminChannel == true,
+ IsSystemChannel = kvp.Key.IsSystemChannel == true,
ProviderChannelId = channelRepresentation.RealId,
ProviderId = connectionId,
Channel = channelRepresentation,
@@ -232,7 +234,6 @@ namespace Tgstation.Server.Host.Components.Chat
channelIdCounter += (ulong)results.Count;
}
- Task trackingContextUpdateTask;
lock (mappedChannels)
{
lock (providers)
@@ -245,16 +246,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 +331,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 +399,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 +415,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 +473,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 +525,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);
@@ -514,7 +549,7 @@ namespace Tgstation.Server.Host.Components.Chat
List wdChannels;
lock (mappedChannels) // so it doesn't change while we're using it
wdChannels = mappedChannels
- .Where(x => !x.Value.Channel.IsPrivateChannel)
+ .Where(x => !x.Value.IsSystemChannel)
.Select(x => x.Key)
.ToList();
@@ -906,6 +941,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 +956,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 +967,6 @@ namespace Tgstation.Server.Host.Components.Chat
}
activeProcessingTask = WrapProcessMessage();
-
- messageTasks.Remove(completedMessageTaskKvp.Key);
}
}
}
@@ -957,12 +996,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 +1045,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/Session/TopicClientFactory.cs b/src/Tgstation.Server.Host/Components/Session/TopicClientFactory.cs
index 559ffd0400..df0289a380 100644
--- a/src/Tgstation.Server.Host/Components/Session/TopicClientFactory.cs
+++ b/src/Tgstation.Server.Host/Components/Session/TopicClientFactory.cs
@@ -19,7 +19,12 @@ namespace Tgstation.Server.Host.Components.Session
/// The value of .
public TopicClientFactory(ILogger logger)
{
- this.logger = logger ?? throw new ArgumentNullException(nameof(logger));
+ if (logger == null)
+ throw new ArgumentNullException(nameof(logger));
+
+ // Don't want the debug logs Topic client spits out either, they're too verbose
+ if (logger.IsEnabled(LogLevel.Trace))
+ this.logger = logger;
}
///
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..5d5f8c998b 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,11 +384,13 @@ 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)
- ;
+ cancellationToken);
}
///
@@ -422,7 +424,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 +476,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 +506,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 +523,7 @@ namespace Tgstation.Server.Host.Components.Watchdog
try
{
- await InitController(announceTask, reattachInfo, cancellationToken);
+ await InitController(eventTask, reattachInfo, cancellationToken);
}
catch (OperationCanceledException ex)
{
@@ -536,15 +533,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 +549,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 +620,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 +724,6 @@ namespace Tgstation.Server.Host.Components.Watchdog
await DisposeAndNullControllers(cancellationToken);
- var chatTask = Task.CompletedTask;
for (var retryAttempts = 1; ; ++retryAttempts)
{
Status = WatchdogStatus.Restoring;
@@ -745,10 +741,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 +750,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 +951,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 +964,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 +1008,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 +1052,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 +1060,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 +1072,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..d4651d1877 100644
--- a/src/Tgstation.Server.Host/Controllers/ChatController.cs
+++ b/src/Tgstation.Server.Host/Controllers/ChatController.cs
@@ -71,6 +71,7 @@ namespace Tgstation.Server.Host.Controllers
IsAdminChannel = api.IsAdminChannel ?? false,
IsWatchdogChannel = api.IsWatchdogChannel ?? false,
IsUpdatesChannel = api.IsUpdatesChannel ?? false,
+ IsSystemChannel = api.IsSystemChannel ?? false,
Tag = api.Tag,
};
@@ -115,8 +116,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/Database/DatabaseContext.cs b/src/Tgstation.Server.Host/Database/DatabaseContext.cs
index bf77cbd332..2614ad2329 100644
--- a/src/Tgstation.Server.Host/Database/DatabaseContext.cs
+++ b/src/Tgstation.Server.Host/Database/DatabaseContext.cs
@@ -379,22 +379,22 @@ namespace Tgstation.Server.Host.Database
///
/// Used by unit tests to remind us to setup the correct MSSQL migration downgrades.
///
- internal static readonly Type MSLatestMigration = typeof(MSAddReattachInfoInitialCompileJob);
+ internal static readonly Type MSLatestMigration = typeof(MSAddSystemChannels);
///
/// Used by unit tests to remind us to setup the correct MYSQL migration downgrades.
///
- internal static readonly Type MYLatestMigration = typeof(MYAddReattachInfoInitialCompileJob);
+ internal static readonly Type MYLatestMigration = typeof(MYAddSystemChannels);
///
/// Used by unit tests to remind us to setup the correct PostgresSQL migration downgrades.
///
- internal static readonly Type PGLatestMigration = typeof(PGAddReattachInfoInitialCompileJob);
+ internal static readonly Type PGLatestMigration = typeof(PGAddSystemChannels);
///
/// Used by unit tests to remind us to setup the correct SQLite migration downgrades.
///
- internal static readonly Type SLLatestMigration = typeof(SLAddReattachInfoInitialCompileJob);
+ internal static readonly Type SLLatestMigration = typeof(SLAddSystemChannels);
///
#pragma warning disable CA1502 // Cyclomatic complexity
@@ -425,6 +425,15 @@ namespace Tgstation.Server.Host.Database
string BadDatabaseType() => throw new ArgumentException($"Invalid DatabaseType: {currentDatabaseType}", nameof(currentDatabaseType));
+ if (targetVersion < new Version(5, 13, 0))
+ targetMigration = currentDatabaseType switch
+ {
+ DatabaseType.MySql => nameof(MYAddReattachInfoInitialCompileJob),
+ DatabaseType.PostgresSql => nameof(PGAddReattachInfoInitialCompileJob),
+ DatabaseType.SqlServer => nameof(MSAddReattachInfoInitialCompileJob),
+ DatabaseType.Sqlite => nameof(SLAddReattachInfoInitialCompileJob),
+ _ => BadDatabaseType(),
+ };
if (targetVersion < new Version(5, 7, 3))
targetMigration = currentDatabaseType switch
{
diff --git a/src/Tgstation.Server.Host/Database/Design/MySqlDesignTimeDbContextFactory.cs b/src/Tgstation.Server.Host/Database/Design/MySqlDesignTimeDbContextFactory.cs
index cf758ef363..f67028efff 100644
--- a/src/Tgstation.Server.Host/Database/Design/MySqlDesignTimeDbContextFactory.cs
+++ b/src/Tgstation.Server.Host/Database/Design/MySqlDesignTimeDbContextFactory.cs
@@ -14,6 +14,6 @@ namespace Tgstation.Server.Host.Database.Design
=> new MySqlDatabaseContext(
DesignTimeDbContextFactoryHelpers.CreateDatabaseContextOptions(
DatabaseType.MariaDB,
- "Server=127.0.0.1;User Id=root;Password=fake;Database=TGS_Design"));
+ "Server=127.0.0.1;User Id=root;Password=zdxfOOTlQFnklwzytzCj;Database=TGS_Design"));
}
}
diff --git a/src/Tgstation.Server.Host/Database/Migrations/20230520203236_MSAddSystemChannels.Designer.cs b/src/Tgstation.Server.Host/Database/Migrations/20230520203236_MSAddSystemChannels.Designer.cs
new file mode 100644
index 0000000000..4fa9364f35
--- /dev/null
+++ b/src/Tgstation.Server.Host/Database/Migrations/20230520203236_MSAddSystemChannels.Designer.cs
@@ -0,0 +1,1069 @@
+//
+using System;
+
+using Microsoft.EntityFrameworkCore;
+using Microsoft.EntityFrameworkCore.Infrastructure;
+using Microsoft.EntityFrameworkCore.Migrations;
+
+#nullable disable
+
+namespace Tgstation.Server.Host.Database.Migrations
+{
+ [DbContext(typeof(SqlServerDatabaseContext))]
+ [Migration("20230520203236_MSAddSystemChannels")]
+ partial class MSAddSystemChannels
+ {
+ ///
+ protected override void BuildTargetModel(ModelBuilder modelBuilder)
+ {
+#pragma warning disable 612, 618
+ modelBuilder
+ .HasAnnotation("ProductVersion", "6.0.16")
+ .HasAnnotation("Relational:MaxIdentifierLength", 128);
+
+ SqlServerModelBuilderExtensions.UseIdentityColumns(modelBuilder, 1L, 1);
+
+ modelBuilder.Entity("Tgstation.Server.Host.Models.ChatBot", b =>
+ {
+ b.Property("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("bigint");
+
+ SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id"), 1L, 1);
+
+ b.Property("ChannelLimit")
+ .HasColumnType("int");
+
+ b.Property("ConnectionString")
+ .IsRequired()
+ .HasMaxLength(10000)
+ .HasColumnType("nvarchar(max)");
+
+ b.Property("Enabled")
+ .HasColumnType("bit");
+
+ b.Property("InstanceId")
+ .HasColumnType("bigint");
+
+ b.Property("Name")
+ .IsRequired()
+ .HasMaxLength(100)
+ .HasColumnType("nvarchar(100)");
+
+ b.Property("Provider")
+ .HasColumnType("int");
+
+ b.Property("ReconnectionInterval")
+ .HasColumnType("bigint");
+
+ b.HasKey("Id");
+
+ b.HasIndex("InstanceId", "Name")
+ .IsUnique();
+
+ b.ToTable("ChatBots");
+ });
+
+ modelBuilder.Entity("Tgstation.Server.Host.Models.ChatChannel", b =>
+ {
+ b.Property("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("bigint");
+
+ SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id"), 1L, 1);
+
+ b.Property("ChatSettingsId")
+ .HasColumnType("bigint");
+
+ b.Property("DiscordChannelId")
+ .HasColumnType("decimal(20,0)");
+
+ b.Property("IrcChannel")
+ .HasMaxLength(100)
+ .HasColumnType("nvarchar(100)");
+
+ b.Property("IsAdminChannel")
+ .IsRequired()
+ .HasColumnType("bit");
+
+ b.Property("IsSystemChannel")
+ .IsRequired()
+ .HasColumnType("bit");
+
+ b.Property("IsUpdatesChannel")
+ .IsRequired()
+ .HasColumnType("bit");
+
+ b.Property("IsWatchdogChannel")
+ .IsRequired()
+ .HasColumnType("bit");
+
+ b.Property("Tag")
+ .HasMaxLength(10000)
+ .HasColumnType("nvarchar(max)");
+
+ b.HasKey("Id");
+
+ b.HasIndex("ChatSettingsId", "DiscordChannelId")
+ .IsUnique()
+ .HasFilter("[DiscordChannelId] IS NOT NULL");
+
+ b.HasIndex("ChatSettingsId", "IrcChannel")
+ .IsUnique()
+ .HasFilter("[IrcChannel] IS NOT NULL");
+
+ b.ToTable("ChatChannels");
+ });
+
+ modelBuilder.Entity("Tgstation.Server.Host.Models.CompileJob", b =>
+ {
+ b.Property("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("bigint");
+
+ SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id"), 1L, 1);
+
+ b.Property("ByondVersion")
+ .IsRequired()
+ .HasColumnType("nvarchar(max)");
+
+ b.Property("DMApiMajorVersion")
+ .HasColumnType("int");
+
+ b.Property("DMApiMinorVersion")
+ .HasColumnType("int");
+
+ b.Property("DMApiPatchVersion")
+ .HasColumnType("int");
+
+ b.Property("DirectoryName")
+ .IsRequired()
+ .HasColumnType("uniqueidentifier");
+
+ b.Property("DmeName")
+ .IsRequired()
+ .HasColumnType("nvarchar(max)");
+
+ b.Property("GitHubDeploymentId")
+ .HasColumnType("int");
+
+ b.Property("GitHubRepoId")
+ .HasColumnType("bigint");
+
+ b.Property("JobId")
+ .HasColumnType("bigint");
+
+ b.Property("MinimumSecurityLevel")
+ .HasColumnType("int");
+
+ b.Property("Output")
+ .IsRequired()
+ .HasColumnType("nvarchar(max)");
+
+ b.Property("RepositoryOrigin")
+ .HasColumnType("nvarchar(max)");
+
+ b.Property("RevisionInformationId")
+ .HasColumnType("bigint");
+
+ b.HasKey("Id");
+
+ b.HasIndex("DirectoryName");
+
+ b.HasIndex("JobId")
+ .IsUnique();
+
+ b.HasIndex("RevisionInformationId");
+
+ b.ToTable("CompileJobs");
+ });
+
+ modelBuilder.Entity("Tgstation.Server.Host.Models.DreamDaemonSettings", b =>
+ {
+ b.Property("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("bigint");
+
+ SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id"), 1L, 1);
+
+ b.Property("AdditionalParameters")
+ .IsRequired()
+ .HasMaxLength(10000)
+ .HasColumnType("nvarchar(max)");
+
+ b.Property("AllowWebClient")
+ .IsRequired()
+ .HasColumnType("bit");
+
+ b.Property("AutoStart")
+ .IsRequired()
+ .HasColumnType("bit");
+
+ b.Property("DumpOnHeartbeatRestart")
+ .IsRequired()
+ .HasColumnType("bit");
+
+ b.Property("HeartbeatSeconds")
+ .HasColumnType("bigint");
+
+ b.Property("InstanceId")
+ .HasColumnType("bigint");
+
+ b.Property("LogOutput")
+ .IsRequired()
+ .HasColumnType("bit");
+
+ b.Property("Port")
+ .HasColumnType("int");
+
+ b.Property("SecurityLevel")
+ .HasColumnType("int");
+
+ b.Property("StartProfiler")
+ .IsRequired()
+ .HasColumnType("bit");
+
+ b.Property("StartupTimeout")
+ .HasColumnType("bigint");
+
+ b.Property("TopicRequestTimeout")
+ .HasColumnType("bigint");
+
+ b.Property("Visibility")
+ .HasColumnType("int");
+
+ b.HasKey("Id");
+
+ b.HasIndex("InstanceId")
+ .IsUnique();
+
+ b.ToTable("DreamDaemonSettings");
+ });
+
+ modelBuilder.Entity("Tgstation.Server.Host.Models.DreamMakerSettings", b =>
+ {
+ b.Property("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("bigint");
+
+ SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id"), 1L, 1);
+
+ b.Property("ApiValidationPort")
+ .HasColumnType("int");
+
+ b.Property("ApiValidationSecurityLevel")
+ .HasColumnType("int");
+
+ b.Property("InstanceId")
+ .HasColumnType("bigint");
+
+ b.Property("ProjectName")
+ .HasMaxLength(10000)
+ .HasColumnType("nvarchar(max)");
+
+ b.Property("RequireDMApiValidation")
+ .IsRequired()
+ .HasColumnType("bit");
+
+ b.Property("Timeout")
+ .IsRequired()
+ .HasColumnType("time");
+
+ b.HasKey("Id");
+
+ b.HasIndex("InstanceId")
+ .IsUnique();
+
+ b.ToTable("DreamMakerSettings");
+ });
+
+ modelBuilder.Entity("Tgstation.Server.Host.Models.Instance", b =>
+ {
+ b.Property("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("bigint");
+
+ SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id"), 1L, 1);
+
+ b.Property("AutoUpdateInterval")
+ .HasColumnType("bigint");
+
+ b.Property("ChatBotLimit")
+ .HasColumnType("int");
+
+ b.Property("ConfigurationType")
+ .HasColumnType("int");
+
+ b.Property("Name")
+ .IsRequired()
+ .HasMaxLength(100)
+ .HasColumnType("nvarchar(100)");
+
+ b.Property("Online")
+ .IsRequired()
+ .HasColumnType("bit");
+
+ b.Property("Path")
+ .IsRequired()
+ .HasColumnType("nvarchar(450)");
+
+ b.Property("SwarmIdentifer")
+ .HasColumnType("nvarchar(450)");
+
+ b.HasKey("Id");
+
+ b.HasIndex("Path", "SwarmIdentifer")
+ .IsUnique()
+ .HasFilter("[SwarmIdentifer] IS NOT NULL");
+
+ b.ToTable("Instances");
+ });
+
+ modelBuilder.Entity("Tgstation.Server.Host.Models.InstancePermissionSet", b =>
+ {
+ b.Property("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("bigint");
+
+ SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id"), 1L, 1);
+
+ b.Property("ByondRights")
+ .HasColumnType("decimal(20,0)");
+
+ b.Property("ChatBotRights")
+ .HasColumnType("decimal(20,0)");
+
+ b.Property("ConfigurationRights")
+ .HasColumnType("decimal(20,0)");
+
+ b.Property("DreamDaemonRights")
+ .HasColumnType("decimal(20,0)");
+
+ b.Property("DreamMakerRights")
+ .HasColumnType("decimal(20,0)");
+
+ b.Property("InstanceId")
+ .HasColumnType("bigint");
+
+ b.Property("InstancePermissionSetRights")
+ .HasColumnType("decimal(20,0)");
+
+ b.Property("PermissionSetId")
+ .HasColumnType("bigint");
+
+ b.Property("RepositoryRights")
+ .HasColumnType("decimal(20,0)");
+
+ b.HasKey("Id");
+
+ b.HasIndex("InstanceId");
+
+ b.HasIndex("PermissionSetId", "InstanceId")
+ .IsUnique();
+
+ b.ToTable("InstancePermissionSets");
+ });
+
+ modelBuilder.Entity("Tgstation.Server.Host.Models.Job", b =>
+ {
+ b.Property("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("bigint");
+
+ SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id"), 1L, 1);
+
+ b.Property("CancelRight")
+ .HasColumnType("decimal(20,0)");
+
+ b.Property("CancelRightsType")
+ .HasColumnType("decimal(20,0)");
+
+ b.Property("Cancelled")
+ .IsRequired()
+ .HasColumnType("bit");
+
+ b.Property("CancelledById")
+ .HasColumnType("bigint");
+
+ b.Property("Description")
+ .IsRequired()
+ .HasColumnType("nvarchar(max)");
+
+ b.Property("ErrorCode")
+ .HasColumnType("bigint");
+
+ b.Property("ExceptionDetails")
+ .HasColumnType("nvarchar(max)");
+
+ b.Property("InstanceId")
+ .HasColumnType("bigint");
+
+ b.Property("StartedAt")
+ .IsRequired()
+ .HasColumnType("datetimeoffset");
+
+ b.Property("StartedById")
+ .HasColumnType("bigint");
+
+ b.Property("StoppedAt")
+ .HasColumnType("datetimeoffset");
+
+ b.HasKey("Id");
+
+ b.HasIndex("CancelledById");
+
+ b.HasIndex("InstanceId");
+
+ b.HasIndex("StartedById");
+
+ b.ToTable("Jobs");
+ });
+
+ modelBuilder.Entity("Tgstation.Server.Host.Models.OAuthConnection", b =>
+ {
+ b.Property("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("bigint");
+
+ SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id"), 1L, 1);
+
+ b.Property("ExternalUserId")
+ .IsRequired()
+ .HasMaxLength(100)
+ .HasColumnType("nvarchar(100)");
+
+ b.Property("Provider")
+ .HasColumnType("int");
+
+ b.Property("UserId")
+ .HasColumnType("bigint");
+
+ b.HasKey("Id");
+
+ b.HasIndex("UserId");
+
+ b.HasIndex("Provider", "ExternalUserId")
+ .IsUnique();
+
+ b.ToTable("OAuthConnections");
+ });
+
+ modelBuilder.Entity("Tgstation.Server.Host.Models.PermissionSet", b =>
+ {
+ b.Property("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("bigint");
+
+ SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id"), 1L, 1);
+
+ b.Property("AdministrationRights")
+ .HasColumnType("decimal(20,0)");
+
+ b.Property("GroupId")
+ .HasColumnType("bigint");
+
+ b.Property("InstanceManagerRights")
+ .HasColumnType("decimal(20,0)");
+
+ b.Property("UserId")
+ .HasColumnType("bigint");
+
+ b.HasKey("Id");
+
+ b.HasIndex("GroupId")
+ .IsUnique()
+ .HasFilter("[GroupId] IS NOT NULL");
+
+ b.HasIndex("UserId")
+ .IsUnique()
+ .HasFilter("[UserId] IS NOT NULL");
+
+ b.ToTable("PermissionSets");
+ });
+
+ modelBuilder.Entity("Tgstation.Server.Host.Models.ReattachInformation", b =>
+ {
+ b.Property("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("bigint");
+
+ SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id"), 1L, 1);
+
+ b.Property("AccessIdentifier")
+ .IsRequired()
+ .HasColumnType("nvarchar(max)");
+
+ b.Property("CompileJobId")
+ .HasColumnType("bigint");
+
+ b.Property("InitialCompileJobId")
+ .HasColumnType("bigint");
+
+ b.Property("LaunchSecurityLevel")
+ .HasColumnType("int");
+
+ b.Property("LaunchVisibility")
+ .HasColumnType("int");
+
+ b.Property("Port")
+ .HasColumnType("int");
+
+ b.Property("ProcessId")
+ .HasColumnType("int");
+
+ b.Property("RebootState")
+ .HasColumnType("int");
+
+ b.HasKey("Id");
+
+ b.HasIndex("CompileJobId");
+
+ b.HasIndex("InitialCompileJobId");
+
+ b.ToTable("ReattachInformations");
+ });
+
+ modelBuilder.Entity("Tgstation.Server.Host.Models.RepositorySettings", b =>
+ {
+ b.Property("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("bigint");
+
+ SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id"), 1L, 1);
+
+ b.Property("AccessToken")
+ .HasMaxLength(10000)
+ .HasColumnType("nvarchar(max)");
+
+ b.Property("AccessUser")
+ .HasMaxLength(10000)
+ .HasColumnType("nvarchar(max)");
+
+ b.Property("AutoUpdatesKeepTestMerges")
+ .IsRequired()
+ .HasColumnType("bit");
+
+ b.Property("AutoUpdatesSynchronize")
+ .IsRequired()
+ .HasColumnType("bit");
+
+ b.Property("CommitterEmail")
+ .IsRequired()
+ .HasMaxLength(10000)
+ .HasColumnType("nvarchar(max)");
+
+ b.Property("CommitterName")
+ .IsRequired()
+ .HasMaxLength(10000)
+ .HasColumnType("nvarchar(max)");
+
+ b.Property("CreateGitHubDeployments")
+ .IsRequired()
+ .HasColumnType("bit");
+
+ b.Property("InstanceId")
+ .HasColumnType("bigint");
+
+ b.Property("PostTestMergeComment")
+ .IsRequired()
+ .HasColumnType("bit");
+
+ b.Property("PushTestMergeCommits")
+ .IsRequired()
+ .HasColumnType("bit");
+
+ b.Property("ShowTestMergeCommitters")
+ .IsRequired()
+ .HasColumnType("bit");
+
+ b.Property("UpdateSubmodules")
+ .IsRequired()
+ .HasColumnType("bit");
+
+ b.HasKey("Id");
+
+ b.HasIndex("InstanceId")
+ .IsUnique();
+
+ b.ToTable("RepositorySettings");
+ });
+
+ modelBuilder.Entity("Tgstation.Server.Host.Models.RevInfoTestMerge", b =>
+ {
+ b.Property("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("bigint");
+
+ SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id"), 1L, 1);
+
+ b.Property("RevisionInformationId")
+ .HasColumnType("bigint");
+
+ b.Property("TestMergeId")
+ .HasColumnType("bigint");
+
+ b.HasKey("Id");
+
+ b.HasIndex("RevisionInformationId");
+
+ b.HasIndex("TestMergeId");
+
+ b.ToTable("RevInfoTestMerges");
+ });
+
+ modelBuilder.Entity("Tgstation.Server.Host.Models.RevisionInformation", b =>
+ {
+ b.Property("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("bigint");
+
+ SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id"), 1L, 1);
+
+ b.Property("CommitSha")
+ .IsRequired()
+ .HasMaxLength(40)
+ .HasColumnType("nvarchar(40)");
+
+ b.Property("InstanceId")
+ .HasColumnType("bigint");
+
+ b.Property("OriginCommitSha")
+ .IsRequired()
+ .HasMaxLength(40)
+ .HasColumnType("nvarchar(40)");
+
+ b.Property("Timestamp")
+ .HasColumnType("datetimeoffset");
+
+ b.HasKey("Id");
+
+ b.HasIndex("InstanceId", "CommitSha")
+ .IsUnique();
+
+ b.ToTable("RevisionInformations");
+ });
+
+ modelBuilder.Entity("Tgstation.Server.Host.Models.TestMerge", b =>
+ {
+ b.Property("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("bigint");
+
+ SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id"), 1L, 1);
+
+ b.Property("Author")
+ .IsRequired()
+ .HasColumnType("nvarchar(max)");
+
+ b.Property("BodyAtMerge")
+ .IsRequired()
+ .HasColumnType("nvarchar(max)");
+
+ b.Property("Comment")
+ .HasMaxLength(10000)
+ .HasColumnType("nvarchar(max)");
+
+ b.Property("MergedAt")
+ .HasColumnType("datetimeoffset");
+
+ b.Property("MergedById")
+ .HasColumnType("bigint");
+
+ b.Property("Number")
+ .HasColumnType("int");
+
+ b.Property("PrimaryRevisionInformationId")
+ .IsRequired()
+ .HasColumnType("bigint");
+
+ b.Property("TargetCommitSha")
+ .IsRequired()
+ .HasMaxLength(40)
+ .HasColumnType("nvarchar(40)");
+
+ b.Property("TitleAtMerge")
+ .IsRequired()
+ .HasColumnType("nvarchar(max)");
+
+ b.Property("Url")
+ .IsRequired()
+ .HasColumnType("nvarchar(max)");
+
+ b.HasKey("Id");
+
+ b.HasIndex("MergedById");
+
+ b.HasIndex("PrimaryRevisionInformationId")
+ .IsUnique();
+
+ b.ToTable("TestMerges");
+ });
+
+ modelBuilder.Entity("Tgstation.Server.Host.Models.User", b =>
+ {
+ b.Property("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("bigint");
+
+ SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id"), 1L, 1);
+
+ b.Property("CanonicalName")
+ .IsRequired()
+ .HasMaxLength(100)
+ .HasColumnType("nvarchar(100)");
+
+ b.Property("CreatedAt")
+ .IsRequired()
+ .HasColumnType("datetimeoffset");
+
+ b.Property("CreatedById")
+ .HasColumnType("bigint");
+
+ b.Property("Enabled")
+ .IsRequired()
+ .HasColumnType("bit");
+
+ b.Property("GroupId")
+ .HasColumnType("bigint");
+
+ b.Property("LastPasswordUpdate")
+ .HasColumnType("datetimeoffset");
+
+ b.Property("Name")
+ .IsRequired()
+ .HasMaxLength(100)
+ .HasColumnType("nvarchar(100)");
+
+ b.Property("PasswordHash")
+ .HasColumnType("nvarchar(max)");
+
+ b.Property("SystemIdentifier")
+ .HasMaxLength(100)
+ .HasColumnType("nvarchar(100)");
+
+ b.HasKey("Id");
+
+ b.HasIndex("CanonicalName")
+ .IsUnique();
+
+ b.HasIndex("CreatedById");
+
+ b.HasIndex("GroupId");
+
+ b.HasIndex("SystemIdentifier")
+ .IsUnique()
+ .HasFilter("[SystemIdentifier] IS NOT NULL");
+
+ b.ToTable("Users");
+ });
+
+ modelBuilder.Entity("Tgstation.Server.Host.Models.UserGroup", b =>
+ {
+ b.Property("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("bigint");
+
+ SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id"), 1L, 1);
+
+ b.Property("Name")
+ .IsRequired()
+ .HasMaxLength(100)
+ .HasColumnType("nvarchar(100)");
+
+ b.HasKey("Id");
+
+ b.HasIndex("Name")
+ .IsUnique();
+
+ b.ToTable("Groups");
+ });
+
+ modelBuilder.Entity("Tgstation.Server.Host.Models.ChatBot", b =>
+ {
+ b.HasOne("Tgstation.Server.Host.Models.Instance", "Instance")
+ .WithMany("ChatSettings")
+ .HasForeignKey("InstanceId")
+ .OnDelete(DeleteBehavior.Cascade)
+ .IsRequired();
+
+ b.Navigation("Instance");
+ });
+
+ modelBuilder.Entity("Tgstation.Server.Host.Models.ChatChannel", b =>
+ {
+ b.HasOne("Tgstation.Server.Host.Models.ChatBot", "ChatSettings")
+ .WithMany("Channels")
+ .HasForeignKey("ChatSettingsId")
+ .OnDelete(DeleteBehavior.Cascade)
+ .IsRequired();
+
+ b.Navigation("ChatSettings");
+ });
+
+ modelBuilder.Entity("Tgstation.Server.Host.Models.CompileJob", b =>
+ {
+ b.HasOne("Tgstation.Server.Host.Models.Job", "Job")
+ .WithOne()
+ .HasForeignKey("Tgstation.Server.Host.Models.CompileJob", "JobId")
+ .OnDelete(DeleteBehavior.Cascade)
+ .IsRequired();
+
+ b.HasOne("Tgstation.Server.Host.Models.RevisionInformation", "RevisionInformation")
+ .WithMany("CompileJobs")
+ .HasForeignKey("RevisionInformationId")
+ .OnDelete(DeleteBehavior.ClientNoAction)
+ .IsRequired();
+
+ b.Navigation("Job");
+
+ b.Navigation("RevisionInformation");
+ });
+
+ modelBuilder.Entity("Tgstation.Server.Host.Models.DreamDaemonSettings", b =>
+ {
+ b.HasOne("Tgstation.Server.Host.Models.Instance", "Instance")
+ .WithOne("DreamDaemonSettings")
+ .HasForeignKey("Tgstation.Server.Host.Models.DreamDaemonSettings", "InstanceId")
+ .OnDelete(DeleteBehavior.Cascade)
+ .IsRequired();
+
+ b.Navigation("Instance");
+ });
+
+ modelBuilder.Entity("Tgstation.Server.Host.Models.DreamMakerSettings", b =>
+ {
+ b.HasOne("Tgstation.Server.Host.Models.Instance", "Instance")
+ .WithOne("DreamMakerSettings")
+ .HasForeignKey("Tgstation.Server.Host.Models.DreamMakerSettings", "InstanceId")
+ .OnDelete(DeleteBehavior.Cascade)
+ .IsRequired();
+
+ b.Navigation("Instance");
+ });
+
+ modelBuilder.Entity("Tgstation.Server.Host.Models.InstancePermissionSet", b =>
+ {
+ b.HasOne("Tgstation.Server.Host.Models.Instance", "Instance")
+ .WithMany("InstancePermissionSets")
+ .HasForeignKey("InstanceId")
+ .OnDelete(DeleteBehavior.Cascade)
+ .IsRequired();
+
+ b.HasOne("Tgstation.Server.Host.Models.PermissionSet", "PermissionSet")
+ .WithMany("InstancePermissionSets")
+ .HasForeignKey("PermissionSetId")
+ .OnDelete(DeleteBehavior.Cascade)
+ .IsRequired();
+
+ b.Navigation("Instance");
+
+ b.Navigation("PermissionSet");
+ });
+
+ modelBuilder.Entity("Tgstation.Server.Host.Models.Job", b =>
+ {
+ b.HasOne("Tgstation.Server.Host.Models.User", "CancelledBy")
+ .WithMany()
+ .HasForeignKey("CancelledById");
+
+ b.HasOne("Tgstation.Server.Host.Models.Instance", "Instance")
+ .WithMany("Jobs")
+ .HasForeignKey("InstanceId")
+ .OnDelete(DeleteBehavior.Cascade)
+ .IsRequired();
+
+ b.HasOne("Tgstation.Server.Host.Models.User", "StartedBy")
+ .WithMany()
+ .HasForeignKey("StartedById")
+ .OnDelete(DeleteBehavior.Cascade)
+ .IsRequired();
+
+ b.Navigation("CancelledBy");
+
+ b.Navigation("Instance");
+
+ b.Navigation("StartedBy");
+ });
+
+ modelBuilder.Entity("Tgstation.Server.Host.Models.OAuthConnection", b =>
+ {
+ b.HasOne("Tgstation.Server.Host.Models.User", "User")
+ .WithMany("OAuthConnections")
+ .HasForeignKey("UserId")
+ .OnDelete(DeleteBehavior.Cascade);
+
+ b.Navigation("User");
+ });
+
+ modelBuilder.Entity("Tgstation.Server.Host.Models.PermissionSet", b =>
+ {
+ b.HasOne("Tgstation.Server.Host.Models.UserGroup", "Group")
+ .WithOne("PermissionSet")
+ .HasForeignKey("Tgstation.Server.Host.Models.PermissionSet", "GroupId")
+ .OnDelete(DeleteBehavior.Cascade);
+
+ b.HasOne("Tgstation.Server.Host.Models.User", "User")
+ .WithOne("PermissionSet")
+ .HasForeignKey("Tgstation.Server.Host.Models.PermissionSet", "UserId")
+ .OnDelete(DeleteBehavior.Cascade);
+
+ b.Navigation("Group");
+
+ b.Navigation("User");
+ });
+
+ modelBuilder.Entity("Tgstation.Server.Host.Models.ReattachInformation", b =>
+ {
+ b.HasOne("Tgstation.Server.Host.Models.CompileJob", "CompileJob")
+ .WithMany()
+ .HasForeignKey("CompileJobId")
+ .OnDelete(DeleteBehavior.Cascade)
+ .IsRequired();
+
+ b.HasOne("Tgstation.Server.Host.Models.CompileJob", "InitialCompileJob")
+ .WithMany()
+ .HasForeignKey("InitialCompileJobId");
+
+ b.Navigation("CompileJob");
+
+ b.Navigation("InitialCompileJob");
+ });
+
+ modelBuilder.Entity("Tgstation.Server.Host.Models.RepositorySettings", b =>
+ {
+ b.HasOne("Tgstation.Server.Host.Models.Instance", "Instance")
+ .WithOne("RepositorySettings")
+ .HasForeignKey("Tgstation.Server.Host.Models.RepositorySettings", "InstanceId")
+ .OnDelete(DeleteBehavior.Cascade)
+ .IsRequired();
+
+ b.Navigation("Instance");
+ });
+
+ modelBuilder.Entity("Tgstation.Server.Host.Models.RevInfoTestMerge", b =>
+ {
+ b.HasOne("Tgstation.Server.Host.Models.RevisionInformation", "RevisionInformation")
+ .WithMany("ActiveTestMerges")
+ .HasForeignKey("RevisionInformationId")
+ .OnDelete(DeleteBehavior.Cascade)
+ .IsRequired();
+
+ b.HasOne("Tgstation.Server.Host.Models.TestMerge", "TestMerge")
+ .WithMany("RevisonInformations")
+ .HasForeignKey("TestMergeId")
+ .OnDelete(DeleteBehavior.ClientNoAction)
+ .IsRequired();
+
+ b.Navigation("RevisionInformation");
+
+ b.Navigation("TestMerge");
+ });
+
+ modelBuilder.Entity("Tgstation.Server.Host.Models.RevisionInformation", b =>
+ {
+ b.HasOne("Tgstation.Server.Host.Models.Instance", "Instance")
+ .WithMany("RevisionInformations")
+ .HasForeignKey("InstanceId")
+ .OnDelete(DeleteBehavior.Cascade)
+ .IsRequired();
+
+ b.Navigation("Instance");
+ });
+
+ modelBuilder.Entity("Tgstation.Server.Host.Models.TestMerge", b =>
+ {
+ b.HasOne("Tgstation.Server.Host.Models.User", "MergedBy")
+ .WithMany("TestMerges")
+ .HasForeignKey("MergedById")
+ .OnDelete(DeleteBehavior.Restrict)
+ .IsRequired();
+
+ b.HasOne("Tgstation.Server.Host.Models.RevisionInformation", "PrimaryRevisionInformation")
+ .WithOne("PrimaryTestMerge")
+ .HasForeignKey("Tgstation.Server.Host.Models.TestMerge", "PrimaryRevisionInformationId")
+ .OnDelete(DeleteBehavior.Cascade)
+ .IsRequired();
+
+ b.Navigation("MergedBy");
+
+ b.Navigation("PrimaryRevisionInformation");
+ });
+
+ modelBuilder.Entity("Tgstation.Server.Host.Models.User", b =>
+ {
+ b.HasOne("Tgstation.Server.Host.Models.User", "CreatedBy")
+ .WithMany("CreatedUsers")
+ .HasForeignKey("CreatedById");
+
+ b.HasOne("Tgstation.Server.Host.Models.UserGroup", "Group")
+ .WithMany("Users")
+ .HasForeignKey("GroupId");
+
+ b.Navigation("CreatedBy");
+
+ b.Navigation("Group");
+ });
+
+ modelBuilder.Entity("Tgstation.Server.Host.Models.ChatBot", b =>
+ {
+ b.Navigation("Channels");
+ });
+
+ modelBuilder.Entity("Tgstation.Server.Host.Models.Instance", b =>
+ {
+ b.Navigation("ChatSettings");
+
+ b.Navigation("DreamDaemonSettings");
+
+ b.Navigation("DreamMakerSettings");
+
+ b.Navigation("InstancePermissionSets");
+
+ b.Navigation("Jobs");
+
+ b.Navigation("RepositorySettings");
+
+ b.Navigation("RevisionInformations");
+ });
+
+ modelBuilder.Entity("Tgstation.Server.Host.Models.PermissionSet", b =>
+ {
+ b.Navigation("InstancePermissionSets");
+ });
+
+ modelBuilder.Entity("Tgstation.Server.Host.Models.RevisionInformation", b =>
+ {
+ b.Navigation("ActiveTestMerges");
+
+ b.Navigation("CompileJobs");
+
+ b.Navigation("PrimaryTestMerge");
+ });
+
+ modelBuilder.Entity("Tgstation.Server.Host.Models.TestMerge", b =>
+ {
+ b.Navigation("RevisonInformations");
+ });
+
+ modelBuilder.Entity("Tgstation.Server.Host.Models.User", b =>
+ {
+ b.Navigation("CreatedUsers");
+
+ b.Navigation("OAuthConnections");
+
+ b.Navigation("PermissionSet");
+
+ b.Navigation("TestMerges");
+ });
+
+ modelBuilder.Entity("Tgstation.Server.Host.Models.UserGroup", b =>
+ {
+ b.Navigation("PermissionSet")
+ .IsRequired();
+
+ b.Navigation("Users");
+ });
+#pragma warning restore 612, 618
+ }
+ }
+}
diff --git a/src/Tgstation.Server.Host/Database/Migrations/20230520203236_MSAddSystemChannels.cs b/src/Tgstation.Server.Host/Database/Migrations/20230520203236_MSAddSystemChannels.cs
new file mode 100644
index 0000000000..b2ead80c4d
--- /dev/null
+++ b/src/Tgstation.Server.Host/Database/Migrations/20230520203236_MSAddSystemChannels.cs
@@ -0,0 +1,39 @@
+using System;
+
+using Microsoft.EntityFrameworkCore.Migrations;
+
+#nullable disable
+
+namespace Tgstation.Server.Host.Database.Migrations
+{
+ ///
+ /// Adds the IsSystemChannel chat channel option for MSSQL.
+ ///
+ public partial class MSAddSystemChannels : Migration
+ {
+ ///
+ protected override void Up(MigrationBuilder migrationBuilder)
+ {
+ if (migrationBuilder == null)
+ throw new ArgumentNullException(nameof(migrationBuilder));
+
+ migrationBuilder.AddColumn(
+ name: "IsSystemChannel",
+ table: "ChatChannels",
+ type: "bit",
+ nullable: false,
+ defaultValue: true);
+ }
+
+ ///
+ protected override void Down(MigrationBuilder migrationBuilder)
+ {
+ if (migrationBuilder == null)
+ throw new ArgumentNullException(nameof(migrationBuilder));
+
+ migrationBuilder.DropColumn(
+ name: "IsSystemChannel",
+ table: "ChatChannels");
+ }
+ }
+}
diff --git a/src/Tgstation.Server.Host/Database/Migrations/20230520203305_MYAddSystemChannels.Designer.cs b/src/Tgstation.Server.Host/Database/Migrations/20230520203305_MYAddSystemChannels.Designer.cs
new file mode 100644
index 0000000000..3f1e7b3249
--- /dev/null
+++ b/src/Tgstation.Server.Host/Database/Migrations/20230520203305_MYAddSystemChannels.Designer.cs
@@ -0,0 +1,1102 @@
+//
+using System;
+
+using Microsoft.EntityFrameworkCore;
+using Microsoft.EntityFrameworkCore.Infrastructure;
+using Microsoft.EntityFrameworkCore.Migrations;
+
+#nullable disable
+
+namespace Tgstation.Server.Host.Database.Migrations
+{
+ [DbContext(typeof(MySqlDatabaseContext))]
+ [Migration("20230520203305_MYAddSystemChannels")]
+ partial class MYAddSystemChannels
+ {
+ ///
+ protected override void BuildTargetModel(ModelBuilder modelBuilder)
+ {
+#pragma warning disable 612, 618
+ modelBuilder
+ .HasAnnotation("ProductVersion", "6.0.16")
+ .HasAnnotation("Relational:MaxIdentifierLength", 64);
+
+ modelBuilder.Entity("Tgstation.Server.Host.Models.ChatBot", b =>
+ {
+ b.Property("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("bigint");
+
+ b.Property("ChannelLimit")
+ .IsRequired()
+ .HasColumnType("smallint unsigned");
+
+ b.Property("ConnectionString")
+ .IsRequired()
+ .HasMaxLength(10000)
+ .HasColumnType("longtext");
+
+ MySqlPropertyBuilderExtensions.HasCharSet(b.Property("ConnectionString"), "utf8mb4");
+
+ b.Property("Enabled")
+ .HasColumnType("tinyint(1)");
+
+ b.Property("InstanceId")
+ .HasColumnType("bigint");
+
+ b.Property("Name")
+ .IsRequired()
+ .HasMaxLength(100)
+ .HasColumnType("varchar(100)");
+
+ b.Property("Provider")
+ .HasColumnType("int");
+
+ b.Property("ReconnectionInterval")
+ .IsRequired()
+ .HasColumnType("int unsigned");
+
+ b.HasKey("Id");
+
+ b.HasIndex("InstanceId", "Name")
+ .IsUnique();
+
+ b.ToTable("ChatBots");
+ });
+
+ modelBuilder.Entity("Tgstation.Server.Host.Models.ChatChannel", b =>
+ {
+ b.Property("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("bigint");
+
+ b.Property("ChatSettingsId")
+ .HasColumnType("bigint");
+
+ b.Property("DiscordChannelId")
+ .HasColumnType("bigint unsigned");
+
+ b.Property("IrcChannel")
+ .HasMaxLength(100)
+ .HasColumnType("varchar(100)");
+
+ MySqlPropertyBuilderExtensions.HasCharSet(b.Property("IrcChannel"), "utf8mb4");
+
+ b.Property("IsAdminChannel")
+ .IsRequired()
+ .HasColumnType("tinyint(1)");
+
+ b.Property("IsSystemChannel")
+ .IsRequired()
+ .HasColumnType("tinyint(1)");
+
+ b.Property("IsUpdatesChannel")
+ .IsRequired()
+ .HasColumnType("tinyint(1)");
+
+ b.Property("IsWatchdogChannel")
+ .IsRequired()
+ .HasColumnType("tinyint(1)");
+
+ b.Property("Tag")
+ .HasMaxLength(10000)
+ .HasColumnType("longtext");
+
+ MySqlPropertyBuilderExtensions.HasCharSet(b.Property("Tag"), "utf8mb4");
+
+ b.HasKey("Id");
+
+ b.HasIndex("ChatSettingsId", "DiscordChannelId")
+ .IsUnique();
+
+ b.HasIndex("ChatSettingsId", "IrcChannel")
+ .IsUnique();
+
+ b.ToTable("ChatChannels");
+ });
+
+ modelBuilder.Entity("Tgstation.Server.Host.Models.CompileJob", b =>
+ {
+ b.Property("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("bigint");
+
+ b.Property("ByondVersion")
+ .IsRequired()
+ .HasColumnType("longtext");
+
+ MySqlPropertyBuilderExtensions.HasCharSet(b.Property("ByondVersion"), "utf8mb4");
+
+ b.Property("DMApiMajorVersion")
+ .HasColumnType("int");
+
+ b.Property("DMApiMinorVersion")
+ .HasColumnType("int");
+
+ b.Property("DMApiPatchVersion")
+ .HasColumnType("int");
+
+ b.Property("DirectoryName")
+ .IsRequired()
+ .HasColumnType("char(36)");
+
+ b.Property("DmeName")
+ .IsRequired()
+ .HasColumnType("longtext");
+
+ MySqlPropertyBuilderExtensions.HasCharSet(b.Property("DmeName"), "utf8mb4");
+
+ b.Property("GitHubDeploymentId")
+ .HasColumnType("int");
+
+ b.Property("GitHubRepoId")
+ .HasColumnType("bigint");
+
+ b.Property("JobId")
+ .HasColumnType("bigint");
+
+ b.Property("MinimumSecurityLevel")
+ .HasColumnType("int");
+
+ b.Property("Output")
+ .IsRequired()
+ .HasColumnType("longtext");
+
+ MySqlPropertyBuilderExtensions.HasCharSet(b.Property("Output"), "utf8mb4");
+
+ b.Property("RepositoryOrigin")
+ .HasColumnType("longtext");
+
+ MySqlPropertyBuilderExtensions.HasCharSet(b.Property("RepositoryOrigin"), "utf8mb4");
+
+ b.Property("RevisionInformationId")
+ .HasColumnType("bigint");
+
+ b.HasKey("Id");
+
+ b.HasIndex("DirectoryName");
+
+ b.HasIndex("JobId")
+ .IsUnique();
+
+ b.HasIndex("RevisionInformationId");
+
+ b.ToTable("CompileJobs");
+ });
+
+ modelBuilder.Entity("Tgstation.Server.Host.Models.DreamDaemonSettings", b =>
+ {
+ b.Property