Merge branch 'V6' into OpenDream

This commit is contained in:
Jordan Dominion
2023-11-16 18:09:00 -05:00
17 changed files with 219 additions and 48 deletions
+12 -11
View File
@@ -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
+1
View File
@@ -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
*/
+2
View File
@@ -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"
+16 -4
View File
@@ -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)
+4
View File
@@ -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
@@ -629,6 +629,12 @@ namespace Tgstation.Server.Api.Models
[Description("The deployment took longer than the configured timeout!")]
DeploymentTimeout,
/// <summary>
/// Sending a broadcast message failed.
/// </summary>
[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,
/// <summary>
/// Could not compile OpenDream due to a missing dotnet executable.
/// </summary>
@@ -7,5 +7,9 @@ namespace Tgstation.Server.Api.Models.Request
/// </summary>
public sealed class DreamDaemonRequest : DreamDaemonApiBase
{
/// <summary>
/// A <see cref="string"/> 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.
/// </summary>
public string? BroadcastMessage { get; set; }
}
}
@@ -112,5 +112,10 @@ namespace Tgstation.Server.Api.Rights
/// User can change <see cref="Models.Internal.DreamDaemonLaunchParameters.MapThreads"/>.
/// </summary>
SetMapThreads = 1 << 19,
/// <summary>
/// User can use <see cref="Models.Request.DreamDaemonRequest.BroadcastMessage"/>.
/// </summary>
BroadcastMessage = 1 << 20,
}
}
@@ -62,5 +62,10 @@ namespace Tgstation.Server.Host.Components.Interop.Topic
/// Receive additional data for a previous response.
/// </summary>
ReceiveChunk,
/// <summary>
/// Sending a broadcast message.
/// </summary>
Broadcast,
}
}
@@ -42,6 +42,11 @@ namespace Tgstation.Server.Host.Components.Interop.Topic
/// </summary>
public string NewInstanceName { get; }
/// <summary>
/// The message to broadcast for <see cref="TopicCommandType.Broadcast"/> requests.
/// </summary>
public string BroadcastMessage { get; }
/// <summary>
/// The <see cref="Interop.ChatUpdate"/> for <see cref="TopicCommandType.ChatChannelsUpdate"/> requests.
/// </summary>
@@ -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}"),
};
/// <summary>
/// Initializes a new instance of the <see cref="TopicParameters"/> class.
/// </summary>
/// <param name="newInstanceName">The value of <see cref="NewInstanceName"/>.</param>
/// <returns>The created <see cref="TopicParameters"/>.</returns>
public static TopicParameters CreateInstanceRenamedTopicParameters(string newInstanceName)
=> new (
newInstanceName ?? throw new ArgumentNullException(nameof(newInstanceName)),
TopicCommandType.InstanceRenamed);
/// <summary>
/// Initializes a new instance of the <see cref="TopicParameters"/> class.
/// </summary>
/// <param name="broadcastMessage">The value of <see cref="BroadcastMessage"/>.</param>
/// <returns>The created <see cref="TopicParameters"/>.</returns>
public static TopicParameters CreateBroadcastParameters(string broadcastMessage)
=> new (
broadcastMessage ?? throw new ArgumentNullException(nameof(broadcastMessage)),
TopicCommandType.Broadcast);
/// <summary>
/// Initializes a new instance of the <see cref="TopicParameters"/> class.
/// </summary>
@@ -116,16 +142,6 @@ namespace Tgstation.Server.Host.Components.Interop.Topic
NewRebootState = newRebootState;
}
/// <summary>
/// Initializes a new instance of the <see cref="TopicParameters"/> class.
/// </summary>
/// <param name="newInstanceName">The value of <see cref="NewInstanceName"/>.</param>
public TopicParameters(string newInstanceName)
: this(TopicCommandType.InstanceRenamed)
{
NewInstanceName = newInstanceName ?? throw new ArgumentNullException(nameof(newInstanceName));
}
/// <summary>
/// Initializes a new instance of the <see cref="TopicParameters"/> class.
/// </summary>
@@ -175,5 +191,28 @@ namespace Tgstation.Server.Host.Components.Interop.Topic
{
CommandType = commandType;
}
/// <summary>
/// Initializes a new instance of the <see cref="TopicParameters"/> class.
/// </summary>
/// <param name="stringCommand">The <see cref="string"/> parameter for the property designated by <paramref name="stringCommandType"/>.</param>
/// <param name="stringCommandType">The value of <see cref="CommandType"/>.</param>
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
}
}
}
@@ -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);
}
/// <inheritdoc />
@@ -88,5 +88,13 @@ namespace Tgstation.Server.Host.Components.Watchdog
/// <param name="cancellationToken">The <see cref="CancellationToken"/> for the operation.</param>
/// <returns>A <see cref="ValueTask"/> representing the running operation.</returns>
ValueTask CreateDump(CancellationToken cancellationToken);
/// <summary>
/// Send a broadcast <paramref name="message"/> to the DMAPI.
/// </summary>
/// <param name="message">The message to broadcast.</param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> for the operation.</param>
/// <returns>A <see cref="ValueTask{TResult}"/> resulting in <see langword="true"/> if the broadcast succeeded., <see langword="false"/> otherwise.</returns>
ValueTask<bool> Broadcast(string message, CancellationToken cancellationToken);
}
}
@@ -457,6 +457,43 @@ namespace Tgstation.Server.Host.Components.Watchdog
await session.CreateDump(dumpFileName, cancellationToken);
}
/// <inheritdoc />
public async ValueTask<bool> 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;
}
/// <inheritdoc />
async ValueTask IEventConsumer.HandleEvent(EventType eventType, IEnumerable<string> parameters, bool deploymentPipeline, CancellationToken cancellationToken)
{
@@ -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)
+4
View File
@@ -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")
+2 -1
View File
@@ -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
@@ -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<ConflictException, DreamDaemonResponse>(() => instanceClient.DreamDaemon.Update(new DreamDaemonRequest
{
BroadcastMessage = "ksjfdksjf",
}, cancellationToken), ErrorCode.BroadcastFailure);
await StartAndLeaveRunning(cancellationToken);
await BroadcastTest(cancellationToken);
await RegressionTest1550(cancellationToken);
await TestLegacyBridgeEndpoint(cancellationToken);