From 67b6276ba40841ffe5223f6e4b44ed33d99aaf60 Mon Sep 17 00:00:00 2001 From: Jordan Dominion Date: Wed, 15 Nov 2023 08:58:29 -0500 Subject: [PATCH 1/3] Fix Windows Live test errors not printing --- .github/workflows/ci-pipeline.yml | 23 ++++++++++++----------- 1 file changed, 12 insertions(+), 11 deletions(-) diff --git a/.github/workflows/ci-pipeline.yml b/.github/workflows/ci-pipeline.yml index d6df684904..82252adb17 100644 --- a/.github/workflows/ci-pipeline.yml +++ b/.github/workflows/ci-pipeline.yml @@ -462,20 +462,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 From bb06a2eff208c49f36cde0818ac98b6fbabedf31 Mon Sep 17 00:00:00 2001 From: Jordan Dominion Date: Wed, 15 Nov 2023 19:41:11 -0500 Subject: [PATCH 2/3] `Forbid()` before accessing instance here --- .../Controllers/DreamDaemonController.cs | 40 +++++++++---------- 1 file changed, 20 insertions(+), 20 deletions(-) diff --git a/src/Tgstation.Server.Host/Controllers/DreamDaemonController.cs b/src/Tgstation.Server.Host/Controllers/DreamDaemonController.cs index 097e0377d3..c438b2e7fa 100644 --- a/src/Tgstation.Server.Host/Controllers/DreamDaemonController.cs +++ b/src/Tgstation.Server.Host/Controllers/DreamDaemonController.cs @@ -1,4 +1,4 @@ -using System; +using System; using System.Linq; using System.Linq.Expressions; using System.Reflection; @@ -208,6 +208,25 @@ 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)) + || 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(); + + await DatabaseContext.Save(cancellationToken); + return await WithComponentInstance( async instance => { @@ -216,25 +235,6 @@ namespace Tgstation.Server.Host.Controllers 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(); - - await DatabaseContext.Save(cancellationToken); - // run this second because current may be modified by it await watchdog.ChangeSettings(current, cancellationToken); From d36e2f59f8fa84d6786124a92a3b530eb85b946e Mon Sep 17 00:00:00 2001 From: Jordan Dominion Date: Wed, 15 Nov 2023 20:53:52 -0500 Subject: [PATCH 3/3] Add broadcasting support - Bump appropriate versions - Add DreamDaemon request field and permission bit. - Add generic failure error code. - Remove `ErrorCode` comment since they are being using in #1666. - Fix bad interop return messages. - Adjust DMAPI code to support broadcasts. - Add tests. Closes #138 --- build/Version.props | 10 ++-- docs/Features.dox | 1 + src/DMAPI/tgs.dm | 2 +- src/DMAPI/tgs/v5/__interop_version.dm | 2 +- src/DMAPI/tgs/v5/_defines.dm | 2 + src/DMAPI/tgs/v5/topic.dm | 20 +++++-- src/DMAPI/tgs/v5/undefs.dm | 4 ++ src/Tgstation.Server.Api/Models/ErrorCode.cs | 6 +- .../Models/Request/DreamDaemonRequest.cs | 4 ++ .../Rights/DreamDaemonRights.cs | 5 ++ .../Interop/Topic/TopicCommandType.cs | 5 ++ .../Interop/Topic/TopicParameters.cs | 59 +++++++++++++++---- .../Components/Session/SessionController.cs | 4 +- .../Components/Watchdog/IWatchdog.cs | 8 +++ .../Components/Watchdog/WatchdogBase.cs | 37 ++++++++++++ .../Controllers/DreamDaemonController.cs | 22 ++++--- tests/DMAPI/LongRunning/Test.dm | 4 ++ tests/DMAPI/test_prelude.dm | 3 +- .../Live/Instance/WatchdogTest.cs | 34 +++++++++++ 19 files changed, 200 insertions(+), 32 deletions(-) diff --git a/build/Version.props b/build/Version.props index 0f6cb5b904..81ddc262f7 100644 --- a/build/Version.props +++ b/build/Version.props @@ -5,12 +5,12 @@ 5.17.3 4.7.1 - 9.13.0 + 9.14.0 7.0.0 - 12.0.1 - 14.1.0 - 6.6.2 - 5.6.2 + 12.2.0 + 14.2.0 + 6.7.0 + 5.7.0 1.4.0 1.2.1 1.0.2 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.dm b/src/DMAPI/tgs.dm index 0cc106ec9c..b0e97e05e9 100644 --- a/src/DMAPI/tgs.dm +++ b/src/DMAPI/tgs.dm @@ -1,6 +1,6 @@ // tgstation-server DMAPI -#define TGS_DMAPI_VERSION "6.6.2" +#define TGS_DMAPI_VERSION "6.7.0" // 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/v5/__interop_version.dm b/src/DMAPI/tgs/v5/__interop_version.dm index 1b52b31d6a..83420d130a 100644 --- a/src/DMAPI/tgs/v5/__interop_version.dm +++ b/src/DMAPI/tgs/v5/__interop_version.dm @@ -1 +1 @@ -"5.6.2" +"5.7.0" diff --git a/src/DMAPI/tgs/v5/_defines.dm b/src/DMAPI/tgs/v5/_defines.dm index bdcd4e4dd5..48969c0c7d 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 d7d4712138..2ef0c70a97 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) @@ -267,4 +267,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 f163adaaaf..fd1ed7e4cf 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 ae2dd3341c..69f4d8dbed 100644 --- a/src/Tgstation.Server.Api/Models/ErrorCode.cs +++ b/src/Tgstation.Server.Api/Models/ErrorCode.cs @@ -629,6 +629,10 @@ namespace Tgstation.Server.Api.Models [Description("The deployment took longer than the configured timeout!")] DeploymentTimeout, - // This comment is here to remind you that there is one more unused error code above and you should use it first + /// + /// 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, } } 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 bf0d22fac7..93e05417a3 100644 --- a/src/Tgstation.Server.Host/Components/Session/SessionController.cs +++ b/src/Tgstation.Server.Host/Components/Session/SessionController.cs @@ -583,7 +583,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 025f3f27a8..a77496bae2 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 c438b2e7fa..3c946af2e0 100644 --- a/src/Tgstation.Server.Host/Controllers/DreamDaemonController.cs +++ b/src/Tgstation.Server.Host/Controllers/DreamDaemonController.cs @@ -1,4 +1,4 @@ -using System; +using System; using System.Linq; using System.Linq.Expressions; using System.Reflection; @@ -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 @@ -215,6 +216,7 @@ namespace Tgstation.Server.Host.Controllers || 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) @@ -225,19 +227,23 @@ namespace Tgstation.Server.Host.Controllers || CheckModified(x => x.MapThreads, DreamDaemonRights.SetMapThreads)) return Forbid(); - await DatabaseContext.Save(cancellationToken); - return await WithComponentInstance( async instance => { var watchdog = instance.Watchdog; + 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; - - // run this second because current may be modified by it - await watchdog.ChangeSettings(current, cancellationToken); - 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 c2e274f4d4..57b23805bc 100644 --- a/tests/DMAPI/LongRunning/Test.dm +++ b/tests/DMAPI/LongRunning/Test.dm @@ -174,6 +174,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 0a67dc8c02..b352bd28d6 100644 --- a/tests/Tgstation.Server.Tests/Live/Instance/WatchdogTest.cs +++ b/tests/Tgstation.Server.Tests/Live/Instance/WatchdogTest.cs @@ -211,12 +211,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", + ddPort, + 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", + ddPort, + 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); var deleteJobTask = TestDeleteByondInstallErrorCasesAndQueing(cancellationToken);