diff --git a/.github/workflows/ci-pipeline.yml b/.github/workflows/ci-pipeline.yml index 11a821405e..e271a1d338 100644 --- a/.github/workflows/ci-pipeline.yml +++ b/.github/workflows/ci-pipeline.yml @@ -514,20 +514,21 @@ jobs: - name: Run Live Tests # Logging here is weird because printing massive amounts of text on Windows runners is SLOW AS SHIT!!! id: live-tests + shell: bash run: | cd tests/Tgstation.Server.Tests - Start-Sleep -Seconds 10 - $ErrorActionPreference="SilentlyContinue" - $test_output = dotnet test -c ${{ matrix.configuration }} --no-build --filter TestCategory=RequiresDatabase --logger "GitHubActions;summary.includePassedTests=true;summary.includeSkippedTests=true" --collect:"XPlat Code Coverage" --settings ../../build/ci.runsettings --results-directory ../../TestResults - $succeeded = $? - $ErrorActionPreference="Stop" + sleep 10 + set +e + test_output=$(dotnet test -c ${{ matrix.configuration }} --no-build --filter TestCategory=RequiresDatabase --logger "GitHubActions;summary.includePassedTests=true;summary.includeSkippedTests=true" --collect:"XPlat Code Coverage" --settings ../../build/ci.runsettings --results-directory ../../TestResults) + succeeded=$? + set -e cd ../.. - $test_output | Out-File -FilePath ./test_output.txt - if (-Not $succeeded) { - echo "succeeded=NO" >> $env:GITHUB_OUTPUT - } else { - echo "succeeded=YES" >> $env:GITHUB_OUTPUT - } + echo $test_output > ./test_output.txt + if [[ $retVal -ne 0 ]]; then + echo "succeeded=NO" >> $GITHUB_OUTPUT + else + echo "succeeded=YES" >> $GITHUB_OUTPUT + fi - name: Store Live Tests Output uses: actions/upload-artifact@v3 diff --git a/docs/Features.dox b/docs/Features.dox index 725b26d947..64eca5a3f7 100644 --- a/docs/Features.dox +++ b/docs/Features.dox @@ -87,4 +87,5 @@ tgstation-server is a BYOND server managment suite. It includes all the followin - Functions with all 3 DreamDaemon security levels - Provides notifications of TGS side events - Allows specifying the .dmb's minimum required security level + - Broadcast messages to clients */ diff --git a/src/DMAPI/tgs/v5/_defines.dm b/src/DMAPI/tgs/v5/_defines.dm index b4d3cc4108..1c7d67d20c 100644 --- a/src/DMAPI/tgs/v5/_defines.dm +++ b/src/DMAPI/tgs/v5/_defines.dm @@ -80,6 +80,7 @@ #define DMAPI5_TOPIC_COMMAND_WATCHDOG_REATTACH 8 #define DMAPI5_TOPIC_COMMAND_SEND_CHUNK 9 #define DMAPI5_TOPIC_COMMAND_RECEIVE_CHUNK 10 +#define DMAPI5_TOPIC_COMMAND_RECEIVE_BROADCAST 11 #define DMAPI5_TOPIC_PARAMETER_COMMAND_TYPE "commandType" #define DMAPI5_TOPIC_PARAMETER_CHAT_COMMAND "chatCommand" @@ -89,6 +90,7 @@ #define DMAPI5_TOPIC_PARAMETER_NEW_INSTANCE_NAME "newInstanceName" #define DMAPI5_TOPIC_PARAMETER_CHAT_UPDATE "chatUpdate" #define DMAPI5_TOPIC_PARAMETER_NEW_SERVER_VERSION "newServerVersion" +#define DMAPI5_TOPIC_PARAMETER_BROADCAST_MESSAGE "broadcastMessage" #define DMAPI5_TOPIC_RESPONSE_COMMAND_RESPONSE "commandResponse" #define DMAPI5_TOPIC_RESPONSE_COMMAND_RESPONSE_MESSAGE "commandResponseMessage" diff --git a/src/DMAPI/tgs/v5/topic.dm b/src/DMAPI/tgs/v5/topic.dm index 40ab80e465..05e6c4e1b2 100644 --- a/src/DMAPI/tgs/v5/topic.dm +++ b/src/DMAPI/tgs/v5/topic.dm @@ -94,7 +94,7 @@ if(DMAPI5_TOPIC_COMMAND_CHANGE_PORT) var/new_port = topic_parameters[DMAPI5_TOPIC_PARAMETER_NEW_PORT] if (!isnum(new_port) || !(new_port > 0)) - return TopicResponse("Invalid or missing [DMAPI5_TOPIC_PARAMETER_NEW_PORT]]") + return TopicResponse("Invalid or missing [DMAPI5_TOPIC_PARAMETER_NEW_PORT]") if(event_handler != null) event_handler.HandleEvent(TGS_EVENT_PORT_SWAP, new_port) @@ -141,7 +141,7 @@ if(DMAPI5_TOPIC_COMMAND_SERVER_PORT_UPDATE) var/new_port = topic_parameters[DMAPI5_TOPIC_PARAMETER_NEW_PORT] if (!isnum(new_port) || !(new_port > 0)) - return TopicResponse("Invalid or missing [DMAPI5_TOPIC_PARAMETER_NEW_PORT]]") + return TopicResponse("Invalid or missing [DMAPI5_TOPIC_PARAMETER_NEW_PORT]") server_port = new_port return TopicResponse() @@ -157,7 +157,7 @@ var/error_message = null if (new_port != null) if (!isnum(new_port) || !(new_port > 0)) - error_message = "Invalid [DMAPI5_TOPIC_PARAMETER_NEW_PORT]]" + error_message = "Invalid [DMAPI5_TOPIC_PARAMETER_NEW_PORT]" else server_port = new_port @@ -165,7 +165,7 @@ if (!istext(new_version_string)) if(error_message != null) error_message += ", " - error_message += "Invalid or missing [DMAPI5_TOPIC_PARAMETER_NEW_SERVER_VERSION]]" + error_message += "Invalid or missing [DMAPI5_TOPIC_PARAMETER_NEW_SERVER_VERSION]" else var/datum/tgs_version/new_version = new(new_version_string) if (event_handler) @@ -268,4 +268,16 @@ return chunk_to_send + if(DMAPI5_TOPIC_COMMAND_RECEIVE_BROADCAST) + var/message = topic_parameters[DMAPI5_TOPIC_PARAMETER_BROADCAST_MESSAGE] + if (!istext(message)) + return TopicResponse("Invalid or missing [DMAPI5_TOPIC_PARAMETER_BROADCAST_MESSAGE]") + + TGS_WORLD_ANNOUNCE(message) + return TopicResponse() + return TopicResponse("Unknown command: [command]") + +/datum/tgs_api/v5/proc/WorldBroadcast(message) + set waitfor = FALSE + TGS_WORLD_ANNOUNCE(message) diff --git a/src/DMAPI/tgs/v5/undefs.dm b/src/DMAPI/tgs/v5/undefs.dm index fbf7b23d59..d531d4b7b9 100644 --- a/src/DMAPI/tgs/v5/undefs.dm +++ b/src/DMAPI/tgs/v5/undefs.dm @@ -78,6 +78,9 @@ #undef DMAPI5_TOPIC_COMMAND_SERVER_PORT_UPDATE #undef DMAPI5_TOPIC_COMMAND_HEALTHCHECK #undef DMAPI5_TOPIC_COMMAND_WATCHDOG_REATTACH +#undef DMAPI5_TOPIC_COMMAND_SEND_CHUNK +#undef DMAPI5_TOPIC_COMMAND_RECEIVE_CHUNK +#undef DMAPI5_TOPIC_COMMAND_RECEIVE_BROADCAST #undef DMAPI5_TOPIC_PARAMETER_COMMAND_TYPE #undef DMAPI5_TOPIC_PARAMETER_CHAT_COMMAND @@ -87,6 +90,7 @@ #undef DMAPI5_TOPIC_PARAMETER_NEW_INSTANCE_NAME #undef DMAPI5_TOPIC_PARAMETER_CHAT_UPDATE #undef DMAPI5_TOPIC_PARAMETER_NEW_SERVER_VERSION +#undef DMAPI5_TOPIC_PARAMETER_BROADCAST_MESSAGE #undef DMAPI5_TOPIC_RESPONSE_COMMAND_RESPONSE #undef DMAPI5_TOPIC_RESPONSE_COMMAND_RESPONSE_MESSAGE diff --git a/src/Tgstation.Server.Api/Models/ErrorCode.cs b/src/Tgstation.Server.Api/Models/ErrorCode.cs index d002d80e5c..52de54d77e 100644 --- a/src/Tgstation.Server.Api/Models/ErrorCode.cs +++ b/src/Tgstation.Server.Api/Models/ErrorCode.cs @@ -629,6 +629,12 @@ namespace Tgstation.Server.Api.Models [Description("The deployment took longer than the configured timeout!")] DeploymentTimeout, + /// + /// Sending a broadcast message failed. + /// + [Description("Could not send broadcast to the DMAPI. This can happen either due to there being an insufficient DMAPI version, a communication failure, or the server being offline.")] + BroadcastFailure, + /// /// Could not compile OpenDream due to a missing dotnet executable. /// diff --git a/src/Tgstation.Server.Api/Models/Request/DreamDaemonRequest.cs b/src/Tgstation.Server.Api/Models/Request/DreamDaemonRequest.cs index 0398d69935..0e4c51e993 100644 --- a/src/Tgstation.Server.Api/Models/Request/DreamDaemonRequest.cs +++ b/src/Tgstation.Server.Api/Models/Request/DreamDaemonRequest.cs @@ -7,5 +7,9 @@ namespace Tgstation.Server.Api.Models.Request /// public sealed class DreamDaemonRequest : DreamDaemonApiBase { + /// + /// A to send to the running server's DMAPI for broadcasting. How this is displayed is up to how the DMAPI is integrated in the codebase. Requires interop version >=5.7.0. + /// + public string? BroadcastMessage { get; set; } } } diff --git a/src/Tgstation.Server.Api/Rights/DreamDaemonRights.cs b/src/Tgstation.Server.Api/Rights/DreamDaemonRights.cs index b13002bf9c..378783bf1e 100644 --- a/src/Tgstation.Server.Api/Rights/DreamDaemonRights.cs +++ b/src/Tgstation.Server.Api/Rights/DreamDaemonRights.cs @@ -112,5 +112,10 @@ namespace Tgstation.Server.Api.Rights /// User can change . /// SetMapThreads = 1 << 19, + + /// + /// User can use . + /// + BroadcastMessage = 1 << 20, } } diff --git a/src/Tgstation.Server.Host/Components/Interop/Topic/TopicCommandType.cs b/src/Tgstation.Server.Host/Components/Interop/Topic/TopicCommandType.cs index 29be2c9a10..286c605d07 100644 --- a/src/Tgstation.Server.Host/Components/Interop/Topic/TopicCommandType.cs +++ b/src/Tgstation.Server.Host/Components/Interop/Topic/TopicCommandType.cs @@ -62,5 +62,10 @@ namespace Tgstation.Server.Host.Components.Interop.Topic /// Receive additional data for a previous response. /// ReceiveChunk, + + /// + /// Sending a broadcast message. + /// + Broadcast, } } diff --git a/src/Tgstation.Server.Host/Components/Interop/Topic/TopicParameters.cs b/src/Tgstation.Server.Host/Components/Interop/Topic/TopicParameters.cs index 3da7e1c34c..fff5453b0d 100644 --- a/src/Tgstation.Server.Host/Components/Interop/Topic/TopicParameters.cs +++ b/src/Tgstation.Server.Host/Components/Interop/Topic/TopicParameters.cs @@ -42,6 +42,11 @@ namespace Tgstation.Server.Host.Components.Interop.Topic /// public string NewInstanceName { get; } + /// + /// The message to broadcast for requests. + /// + public string BroadcastMessage { get; } + /// /// The for requests. /// @@ -68,6 +73,7 @@ namespace Tgstation.Server.Host.Components.Interop.Topic or TopicCommandType.ChangeRebootState or TopicCommandType.InstanceRenamed or TopicCommandType.ChatChannelsUpdate + or TopicCommandType.Broadcast or TopicCommandType.ServerRestarted => true, TopicCommandType.ChatCommand or TopicCommandType.HealthCheck @@ -76,6 +82,26 @@ namespace Tgstation.Server.Host.Components.Interop.Topic _ => throw new InvalidOperationException($"Invalid value for {nameof(CommandType)}: {CommandType}"), }; + /// + /// Initializes a new instance of the class. + /// + /// The value of . + /// The created . + public static TopicParameters CreateInstanceRenamedTopicParameters(string newInstanceName) + => new ( + newInstanceName ?? throw new ArgumentNullException(nameof(newInstanceName)), + TopicCommandType.InstanceRenamed); + + /// + /// Initializes a new instance of the class. + /// + /// The value of . + /// The created . + public static TopicParameters CreateBroadcastParameters(string broadcastMessage) + => new ( + broadcastMessage ?? throw new ArgumentNullException(nameof(broadcastMessage)), + TopicCommandType.Broadcast); + /// /// Initializes a new instance of the class. /// @@ -116,16 +142,6 @@ namespace Tgstation.Server.Host.Components.Interop.Topic NewRebootState = newRebootState; } - /// - /// Initializes a new instance of the class. - /// - /// The value of . - public TopicParameters(string newInstanceName) - : this(TopicCommandType.InstanceRenamed) - { - NewInstanceName = newInstanceName ?? throw new ArgumentNullException(nameof(newInstanceName)); - } - /// /// Initializes a new instance of the class. /// @@ -175,5 +191,28 @@ namespace Tgstation.Server.Host.Components.Interop.Topic { CommandType = commandType; } + + /// + /// Initializes a new instance of the class. + /// + /// The parameter for the property designated by . + /// The value of . + TopicParameters(string stringCommand, TopicCommandType stringCommandType) + : this(stringCommandType) + { +#pragma warning disable IDE0010 // Add missing cases + switch (stringCommandType) + { + case TopicCommandType.InstanceRenamed: + NewInstanceName = stringCommand; + break; + case TopicCommandType.Broadcast: + BroadcastMessage = stringCommand; + break; + default: + throw new InvalidOperationException($"Invalid string TopicCommandType: {stringCommandType}"); + } +#pragma warning restore IDE0010 // Add missing cases + } } } diff --git a/src/Tgstation.Server.Host/Components/Session/SessionController.cs b/src/Tgstation.Server.Host/Components/Session/SessionController.cs index 2f046830d0..73c9d82341 100644 --- a/src/Tgstation.Server.Host/Components/Session/SessionController.cs +++ b/src/Tgstation.Server.Host/Components/Session/SessionController.cs @@ -534,7 +534,9 @@ namespace Tgstation.Server.Host.Components.Session public async ValueTask InstanceRenamed(string newInstanceName, CancellationToken cancellationToken) { ReattachInformation.RuntimeInformation.InstanceName = newInstanceName; - await SendCommand(new TopicParameters(newInstanceName), cancellationToken); + await SendCommand( + TopicParameters.CreateInstanceRenamedTopicParameters(newInstanceName), + cancellationToken); } /// diff --git a/src/Tgstation.Server.Host/Components/Watchdog/IWatchdog.cs b/src/Tgstation.Server.Host/Components/Watchdog/IWatchdog.cs index 360d3fcfc3..2d8d0f5d24 100644 --- a/src/Tgstation.Server.Host/Components/Watchdog/IWatchdog.cs +++ b/src/Tgstation.Server.Host/Components/Watchdog/IWatchdog.cs @@ -88,5 +88,13 @@ namespace Tgstation.Server.Host.Components.Watchdog /// The for the operation. /// A representing the running operation. ValueTask CreateDump(CancellationToken cancellationToken); + + /// + /// Send a broadcast to the DMAPI. + /// + /// The message to broadcast. + /// The for the operation. + /// A resulting in if the broadcast succeeded., otherwise. + ValueTask Broadcast(string message, CancellationToken cancellationToken); } } diff --git a/src/Tgstation.Server.Host/Components/Watchdog/WatchdogBase.cs b/src/Tgstation.Server.Host/Components/Watchdog/WatchdogBase.cs index 97fad794f3..6048e66f61 100644 --- a/src/Tgstation.Server.Host/Components/Watchdog/WatchdogBase.cs +++ b/src/Tgstation.Server.Host/Components/Watchdog/WatchdogBase.cs @@ -457,6 +457,43 @@ namespace Tgstation.Server.Host.Components.Watchdog await session.CreateDump(dumpFileName, cancellationToken); } + /// + public async ValueTask Broadcast(string message, CancellationToken cancellationToken) + { + ArgumentNullException.ThrowIfNull(message); + + var activeServer = GetActiveController(); + if (activeServer == null) + { + Logger.LogInformation("Attempted broadcast failed, no active server!"); + return false; + } + + if (!activeServer.DMApiAvailable) + { + Logger.LogInformation("Attempted broadcast failed, no DMAPI!"); + return false; + } + + var minimumRequiredVersion = new Version(5, 7, 0); + if (activeServer.DMApiVersion < minimumRequiredVersion) + { + Logger.LogInformation( + "Attempted broadcast failed, insufficient interop version: {interopVersion}. Requires {minimumRequiredVersion}!", + activeServer.DMApiVersion, + minimumRequiredVersion); + return false; + } + + Logger.LogInformation("Broadcasting: {message}", message); + + var response = await activeServer.SendCommand( + TopicParameters.CreateBroadcastParameters(message), + cancellationToken); + + return response != null && response.ErrorMessage == null; + } + /// async ValueTask IEventConsumer.HandleEvent(EventType eventType, IEnumerable parameters, bool deploymentPipeline, CancellationToken cancellationToken) { diff --git a/src/Tgstation.Server.Host/Controllers/DreamDaemonController.cs b/src/Tgstation.Server.Host/Controllers/DreamDaemonController.cs index ea39745845..fb53f5ab33 100644 --- a/src/Tgstation.Server.Host/Controllers/DreamDaemonController.cs +++ b/src/Tgstation.Server.Host/Controllers/DreamDaemonController.cs @@ -148,7 +148,8 @@ namespace Tgstation.Server.Host.Controllers | DreamDaemonRights.SetVisibility | DreamDaemonRights.SetProfiler | DreamDaemonRights.SetLogOutput - | DreamDaemonRights.SetMapThreads)] + | DreamDaemonRights.SetMapThreads + | DreamDaemonRights.BroadcastMessage)] [ProducesResponseType(typeof(DreamDaemonResponse), 200)] [ProducesResponseType(typeof(ErrorMessageResponse), 410)] #pragma warning disable CA1502 // TODO: Decomplexify @@ -208,36 +209,41 @@ namespace Tgstation.Server.Host.Controllers return false; } + if (CheckModified(x => x.AllowWebClient, DreamDaemonRights.SetWebClient) + || CheckModified(x => x.AutoStart, DreamDaemonRights.SetAutoStart) + || CheckModified(x => x.Port, DreamDaemonRights.SetPort) + || CheckModified(x => x.SecurityLevel, DreamDaemonRights.SetSecurity) + || CheckModified(x => x.Visibility, DreamDaemonRights.SetVisibility) + || (model.SoftRestart.HasValue && !AuthenticationContext.InstancePermissionSet.DreamDaemonRights.Value.HasFlag(DreamDaemonRights.SoftRestart)) + || (model.SoftShutdown.HasValue && !AuthenticationContext.InstancePermissionSet.DreamDaemonRights.Value.HasFlag(DreamDaemonRights.SoftShutdown)) + || (model.BroadcastMessage != null && !AuthenticationContext.InstancePermissionSet.DreamDaemonRights.Value.HasFlag(DreamDaemonRights.BroadcastMessage)) + || CheckModified(x => x.StartupTimeout, DreamDaemonRights.SetStartupTimeout) + || CheckModified(x => x.HealthCheckSeconds, DreamDaemonRights.SetHealthCheckInterval) + || CheckModified(x => x.DumpOnHealthCheckRestart, DreamDaemonRights.CreateDump) + || CheckModified(x => x.TopicRequestTimeout, DreamDaemonRights.SetTopicTimeout) + || CheckModified(x => x.AdditionalParameters, DreamDaemonRights.SetAdditionalParameters) + || CheckModified(x => x.StartProfiler, DreamDaemonRights.SetProfiler) + || CheckModified(x => x.LogOutput, DreamDaemonRights.SetLogOutput) + || CheckModified(x => x.MapThreads, DreamDaemonRights.SetMapThreads)) + return Forbid(); + return await WithComponentInstance( async instance => { var watchdog = instance.Watchdog; - var rebootState = watchdog.RebootState; - var oldSoftRestart = rebootState == RebootState.Restart; - var oldSoftShutdown = rebootState == RebootState.Shutdown; - - if (CheckModified(x => x.AllowWebClient, DreamDaemonRights.SetWebClient) - || CheckModified(x => x.AutoStart, DreamDaemonRights.SetAutoStart) - || CheckModified(x => x.Port, DreamDaemonRights.SetPort) - || CheckModified(x => x.SecurityLevel, DreamDaemonRights.SetSecurity) - || CheckModified(x => x.Visibility, DreamDaemonRights.SetVisibility) - || (model.SoftRestart.HasValue && !AuthenticationContext.InstancePermissionSet.DreamDaemonRights.Value.HasFlag(DreamDaemonRights.SoftRestart)) - || (model.SoftShutdown.HasValue && !AuthenticationContext.InstancePermissionSet.DreamDaemonRights.Value.HasFlag(DreamDaemonRights.SoftShutdown)) - || CheckModified(x => x.StartupTimeout, DreamDaemonRights.SetStartupTimeout) - || CheckModified(x => x.HealthCheckSeconds, DreamDaemonRights.SetHealthCheckInterval) - || CheckModified(x => x.DumpOnHealthCheckRestart, DreamDaemonRights.CreateDump) - || CheckModified(x => x.TopicRequestTimeout, DreamDaemonRights.SetTopicTimeout) - || CheckModified(x => x.AdditionalParameters, DreamDaemonRights.SetAdditionalParameters) - || CheckModified(x => x.StartProfiler, DreamDaemonRights.SetProfiler) - || CheckModified(x => x.LogOutput, DreamDaemonRights.SetLogOutput) - || CheckModified(x => x.MapThreads, DreamDaemonRights.SetMapThreads)) - return Forbid(); + if (model.BroadcastMessage != null + && !await watchdog.Broadcast(model.BroadcastMessage, cancellationToken)) + return Conflict(new ErrorMessageResponse(ErrorCode.BroadcastFailure)); await DatabaseContext.Save(cancellationToken); // run this second because current may be modified by it + // slight race condition with request cancellation, but I CANNOT be assed right now await watchdog.ChangeSettings(current, cancellationToken); + var rebootState = watchdog.RebootState; + var oldSoftRestart = rebootState == RebootState.Restart; + var oldSoftShutdown = rebootState == RebootState.Shutdown; if (!oldSoftRestart && model.SoftRestart == true && watchdog.Status == WatchdogStatus.Online) await watchdog.Restart(true, cancellationToken); else if (!oldSoftShutdown && model.SoftShutdown == true) diff --git a/tests/DMAPI/LongRunning/Test.dm b/tests/DMAPI/LongRunning/Test.dm index 0a80378b7e..2ee16cc2de 100644 --- a/tests/DMAPI/LongRunning/Test.dm +++ b/tests/DMAPI/LongRunning/Test.dm @@ -176,6 +176,10 @@ var/run_bridge_test if(tactics8) return received_health_check ? "received health check" : "did not receive health check" + var/tactics_broadcast = data["tgs_integration_test_tactics_broadcast"] + if(tactics_broadcast) + return last_tgs_broadcast || "!!NULL!!" + var/legalize_nuclear_bombs = data["shadow_wizard_money_gang"] if(legalize_nuclear_bombs) text2file("I expect this to remain here for a while", "kajigger.txt") diff --git a/tests/DMAPI/test_prelude.dm b/tests/DMAPI/test_prelude.dm index efb84d46ae..6cb666e372 100644 --- a/tests/DMAPI/test_prelude.dm +++ b/tests/DMAPI/test_prelude.dm @@ -3,7 +3,8 @@ #define TGS_READ_GLOBAL(Name) global.##Name #define TGS_WRITE_GLOBAL(Name, Value) global.##Name = ##Value #define TGS_PROTECT_DATUM(Path) -#define TGS_WORLD_ANNOUNCE(message) world << ##message +var/last_tgs_broadcast +#define TGS_WORLD_ANNOUNCE(message) if(TRUE) { var/__tgs_announce_message_local = ##message; world << __tgs_announce_message_local; last_tgs_broadcast = __tgs_announce_message_local; } #define TGS_WARNING_LOG(message) world.log << "Warn: [##message]" #define TGS_NOTIFY_ADMINS(event) #define TGS_CLIENT_COUNT 0 diff --git a/tests/Tgstation.Server.Tests/Live/Instance/WatchdogTest.cs b/tests/Tgstation.Server.Tests/Live/Instance/WatchdogTest.cs index 5e3a7f8eb0..fcc38a52c3 100644 --- a/tests/Tgstation.Server.Tests/Live/Instance/WatchdogTest.cs +++ b/tests/Tgstation.Server.Tests/Live/Instance/WatchdogTest.cs @@ -224,12 +224,46 @@ namespace Tgstation.Server.Tests.Live.Instance await RunTest(false); } + async ValueTask BroadcastTest(CancellationToken cancellationToken) + { + var topicRequestResult = await topicClient.SendTopic( + IPAddress.Loopback, + $"tgs_integration_test_tactics_broadcast=1", + FindTopicPort(), + cancellationToken); + + Assert.IsNotNull(topicRequestResult); + Assert.AreEqual("!!NULL!!", topicRequestResult.StringData); + + const string TestBroadcastMessage = "TGS: THIS IS A TEST OF THE EMERGENCY BROADCAST SYSTEM!"; + await instanceClient.DreamDaemon.Update(new DreamDaemonRequest + { + BroadcastMessage = TestBroadcastMessage, + }, cancellationToken); + + topicRequestResult = await topicClient.SendTopic( + IPAddress.Loopback, + $"tgs_integration_test_tactics_broadcast=1", + FindTopicPort(), + cancellationToken); + + Assert.IsNotNull(topicRequestResult); + Assert.AreEqual(TestBroadcastMessage, topicRequestResult.StringData); + } + async Task InteropTestsForLongRunningDme(CancellationToken cancellationToken) { await RegressionTest1686(cancellationToken); + await ApiAssert.ThrowsException(() => instanceClient.DreamDaemon.Update(new DreamDaemonRequest + { + BroadcastMessage = "ksjfdksjf", + }, cancellationToken), ErrorCode.BroadcastFailure); + await StartAndLeaveRunning(cancellationToken); + await BroadcastTest(cancellationToken); + await RegressionTest1550(cancellationToken); await TestLegacyBridgeEndpoint(cancellationToken);