From 6f06f1e45cd95a51399a850b3bc5da74ba78a29b Mon Sep 17 00:00:00 2001 From: Jordan Dominion Date: Thu, 15 Feb 2024 17:24:36 -0500 Subject: [PATCH 01/22] One less async function --- .../Components/Deployment/DmbFactory.cs | 19 ++++++++++--------- 1 file changed, 10 insertions(+), 9 deletions(-) diff --git a/src/Tgstation.Server.Host/Components/Deployment/DmbFactory.cs b/src/Tgstation.Server.Host/Components/Deployment/DmbFactory.cs index 3b6695313b..d61b2ffc68 100644 --- a/src/Tgstation.Server.Host/Components/Deployment/DmbFactory.cs +++ b/src/Tgstation.Server.Host/Components/Deployment/DmbFactory.cs @@ -424,21 +424,22 @@ namespace Tgstation.Server.Host.Components.Deployment /// The to clean. void CleanRegisteredCompileJob(CompileJob job) { - async Task HandleCleanup() + Task HandleCleanup() { - // First kill the GitHub deployment - var remoteDeploymentManager = remoteDeploymentManagerFactory.CreateRemoteDeploymentManager(metadata, job); - - // DCT: None available - var deploymentJob = remoteDeploymentManager.MarkInactive(job, CancellationToken.None); - - var deleteTask = DeleteCompileJobContent(job.DirectoryName!.Value.ToString(), cleanupCts.Token); var otherTask = cleanupTask; async Task WrapThrowableTasks() { try { + // First kill the GitHub deployment + var remoteDeploymentManager = remoteDeploymentManagerFactory.CreateRemoteDeploymentManager(metadata, job); + + // DCT: None available + var deploymentJob = remoteDeploymentManager.MarkInactive(job, CancellationToken.None); + + var deleteTask = DeleteCompileJobContent(job.DirectoryName!.Value.ToString(), cleanupCts.Token); + await ValueTaskExtensions.WhenAll(deleteTask, deploymentJob); } catch (Exception ex) @@ -447,7 +448,7 @@ namespace Tgstation.Server.Host.Components.Deployment } } - await Task.WhenAll(otherTask, WrapThrowableTasks()); + return Task.WhenAll(otherTask, WrapThrowableTasks()); } lock (jobLockCounts) From 4500719a1cee44d7f42a16f9213a4ca182713571 Mon Sep 17 00:00:00 2001 From: Jordan Dominion Date: Thu, 15 Feb 2024 17:31:23 -0500 Subject: [PATCH 02/22] DMAPI 7.1.0: Add `TGS_FILE2TEXT_NATIVE` --- build/Version.props | 2 +- src/DMAPI/tgs.dm | 9 ++++++++- src/DMAPI/tgs/v5/bridge.dm | 2 +- tests/DMAPI/LongRunning/Test.dm | 2 +- 4 files changed, 11 insertions(+), 4 deletions(-) diff --git a/build/Version.props b/build/Version.props index b0347f6394..640f7a72fa 100644 --- a/build/Version.props +++ b/build/Version.props @@ -9,7 +9,7 @@ 7.0.0 13.1.0 15.1.0 - 7.0.2 + 7.1.0 5.8.0 1.4.1 1.2.1 diff --git a/src/DMAPI/tgs.dm b/src/DMAPI/tgs.dm index fdfec5e8ca..1d7f7d02f8 100644 --- a/src/DMAPI/tgs.dm +++ b/src/DMAPI/tgs.dm @@ -1,6 +1,6 @@ // tgstation-server DMAPI -#define TGS_DMAPI_VERSION "7.0.2" +#define TGS_DMAPI_VERSION "7.1.0" // All functions and datums outside this document are subject to change with any version and should not be relied on. @@ -50,6 +50,13 @@ #endif +#ifndef TGS_FILE2TEXT_NATIVE +#ifdef file2text +#error Your codebase is re-defining the BYOND proc file2text. The DMAPI requires the native version to read the result of world.Export(). You can fix this by adding "#define TGS_FILE2TEXT_NATIVE file2text" before your override of file2text to allow the DMAPI to use the native version. This will only be used for world.Export(), not regular file accesses +#endif +#define TGS_FILE2TEXT_NATIVE file2text +#endif + // EVENT CODES /// Before a reboot mode change, extras parameters are the current and new reboot mode enums. diff --git a/src/DMAPI/tgs/v5/bridge.dm b/src/DMAPI/tgs/v5/bridge.dm index a0ab359876..d986ec7e73 100644 --- a/src/DMAPI/tgs/v5/bridge.dm +++ b/src/DMAPI/tgs/v5/bridge.dm @@ -88,7 +88,7 @@ TGS_ERROR_LOG("Failed bridge request, missing content!") return - var/response_json = file2text(content) + var/response_json = TGS_FILE2TEXT_NATIVE(content) if(!response_json) TGS_ERROR_LOG("Failed bridge request, failed to load content!") return diff --git a/tests/DMAPI/LongRunning/Test.dm b/tests/DMAPI/LongRunning/Test.dm index 90164c7e80..f4468953a7 100644 --- a/tests/DMAPI/LongRunning/Test.dm +++ b/tests/DMAPI/LongRunning/Test.dm @@ -34,7 +34,7 @@ if(!res) FailTest("Failed to resource!") - var/res_contents = file2text(res) // we need a .rsc to be generated + var/res_contents = TGS_FILE2TEXT_NATIVE(res) // we need a .rsc to be generated if(!res_contents) FailTest("Failed to resource? No contents!") From 07c7bd573c1dd3596ca77ef699b7a94baa14ec81 Mon Sep 17 00:00:00 2001 From: Jordan Dominion Date: Thu, 15 Feb 2024 18:44:20 -0500 Subject: [PATCH 03/22] Nuget package updates. Primarily patch Tuesday --- build/TestCommon.props | 6 ++--- .../Tgstation.Server.Api.csproj | 2 +- .../Tgstation.Server.Client.csproj | 4 ++-- .../.config/dotnet-tools.json | 2 +- .../Tgstation.Server.Host.csproj | 22 +++++++++---------- .../Tgstation.Server.Host.Tests.csproj | 2 +- 6 files changed, 19 insertions(+), 19 deletions(-) diff --git a/build/TestCommon.props b/build/TestCommon.props index 9811a480b8..af63625813 100644 --- a/build/TestCommon.props +++ b/build/TestCommon.props @@ -13,14 +13,14 @@ runtime; build; native; contentfiles; analyzers; buildtransitive - + - + - + diff --git a/src/Tgstation.Server.Api/Tgstation.Server.Api.csproj b/src/Tgstation.Server.Api/Tgstation.Server.Api.csproj index 4f64dba5b1..0b93c41fec 100644 --- a/src/Tgstation.Server.Api/Tgstation.Server.Api.csproj +++ b/src/Tgstation.Server.Api/Tgstation.Server.Api.csproj @@ -27,7 +27,7 @@ - + diff --git a/src/Tgstation.Server.Client/Tgstation.Server.Client.csproj b/src/Tgstation.Server.Client/Tgstation.Server.Client.csproj index 62e15714d5..5cff1a09a8 100644 --- a/src/Tgstation.Server.Client/Tgstation.Server.Client.csproj +++ b/src/Tgstation.Server.Client/Tgstation.Server.Client.csproj @@ -11,9 +11,9 @@ - + - + diff --git a/src/Tgstation.Server.Host/.config/dotnet-tools.json b/src/Tgstation.Server.Host/.config/dotnet-tools.json index 81fe5add42..d9b689bb64 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": "8.0.1", + "version": "8.0.2", "commands": [ "dotnet-ef" ] diff --git a/src/Tgstation.Server.Host/Tgstation.Server.Host.csproj b/src/Tgstation.Server.Host/Tgstation.Server.Host.csproj index 2ea5dcd4ed..a944063eaa 100644 --- a/src/Tgstation.Server.Host/Tgstation.Server.Host.csproj +++ b/src/Tgstation.Server.Host/Tgstation.Server.Host.csproj @@ -78,23 +78,23 @@ - + - + - + - + - + - + runtime; build; native; contentfiles; analyzers; buildtransitive - + - + @@ -104,9 +104,9 @@ - + - + @@ -128,7 +128,7 @@ - + diff --git a/tests/Tgstation.Server.Host.Tests/Tgstation.Server.Host.Tests.csproj b/tests/Tgstation.Server.Host.Tests/Tgstation.Server.Host.Tests.csproj index d6f3aa8e39..87023f4cfd 100644 --- a/tests/Tgstation.Server.Host.Tests/Tgstation.Server.Host.Tests.csproj +++ b/tests/Tgstation.Server.Host.Tests/Tgstation.Server.Host.Tests.csproj @@ -6,7 +6,7 @@ - + From b2b7afe5f2872ce75d74b6d9063f6471361c1a20 Mon Sep 17 00:00:00 2001 From: Jordan Dominion Date: Thu, 15 Feb 2024 18:45:12 -0500 Subject: [PATCH 04/22] Update dotnet redistributable --- build/Version.props | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/build/Version.props b/build/Version.props index 640f7a72fa..5f87b4c3ba 100644 --- a/build/Version.props +++ b/build/Version.props @@ -17,7 +17,7 @@ netstandard2.0 8 - https://download.visualstudio.microsoft.com/download/pr/016c6447-764a-4210-a260-bf7a2880d5c0/a5746437a3862d7803284ae8c2290200/dotnet-hosting-8.0.1-win.exe + https://download.visualstudio.microsoft.com/download/pr/98ff0a08-a283-428f-8e54-19841d97154c/8c7d5f9600eadf264f04c82c813b7aab/dotnet-hosting-8.0.2-win.exe 10.11.6 1.22.21 From c17ecb84261910d57635ae0cc7a23fa60f4707e3 Mon Sep 17 00:00:00 2001 From: Jordan Dominion Date: Mon, 19 Feb 2024 16:00:14 -0500 Subject: [PATCH 05/22] Fix event scripts always running with low priority if `Session:LowPriorityDeploymentProcesses` was set --- .../Components/StaticFiles/Configuration.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Tgstation.Server.Host/Components/StaticFiles/Configuration.cs b/src/Tgstation.Server.Host/Components/StaticFiles/Configuration.cs index 8261afe918..c77fa7ab49 100644 --- a/src/Tgstation.Server.Host/Components/StaticFiles/Configuration.cs +++ b/src/Tgstation.Server.Host/Components/StaticFiles/Configuration.cs @@ -651,7 +651,7 @@ namespace Tgstation.Server.Host.Components.StaticFiles noShellExecute: true)) using (cancellationToken.Register(() => script.Terminate())) { - if (sessionConfiguration.LowPriorityDeploymentProcesses) + if (sessionConfiguration.LowPriorityDeploymentProcesses && deploymentPipeline) script.AdjustPriority(false); var exitCode = await script.Lifetime; From 0bbf7cd46a1b0c483a1a0348bd32365c695b53c1 Mon Sep 17 00:00:00 2001 From: Jordan Dominion Date: Mon, 19 Feb 2024 16:44:54 -0500 Subject: [PATCH 06/22] Add DMAPI triggerable custom events Closes #1746 --- build/Version.props | 2 +- src/DMAPI/tgs.dm | 10 ++ src/DMAPI/tgs/core/core.dm | 8 + src/DMAPI/tgs/core/datum.dm | 3 + src/DMAPI/tgs/v5/__interop_version.dm | 2 +- src/DMAPI/tgs/v5/_defines.dm | 9 + src/DMAPI/tgs/v5/api.dm | 37 ++++ src/DMAPI/tgs/v5/topic.dm | 12 ++ src/DMAPI/tgs/v5/undefs.dm | 9 + .../Components/Deployment/IDmbProvider.cs | 2 +- .../Components/Events/EventConsumer.cs | 4 + .../Components/Events/EventScriptAttribute.cs | 3 +- .../Components/Events/IEventConsumer.cs | 9 + .../Components/Events/NoopEventConsumer.cs | 4 + .../Interop/Bridge/BridgeCommandType.cs | 5 + .../Interop/Bridge/BridgeParameters.cs | 5 + .../Interop/Bridge/BridgeResponse.cs | 5 + .../Interop/Bridge/CustomEventInvocation.cs | 25 +++ .../Interop/Topic/EventNotification.cs | 4 +- .../Interop/Topic/TopicCommandType.cs | 5 + .../Interop/Topic/TopicParameters.cs | 16 ++ .../Components/Session/SessionController.cs | 107 +++++++++++- .../Session/SessionControllerFactory.cs | 2 + .../Components/StaticFiles/Configuration.cs | 158 +++++++++++------- .../Components/Watchdog/WatchdogBase.cs | 4 + tests/DMAPI/BasicOperation/Test.dm | 15 ++ .../DMAPI/BasicOperation/test_event-qwer.bat | 7 + tests/DMAPI/BasicOperation/test_event-qwer.sh | 13 ++ .../Live/Instance/ConfigurationTest.cs | 9 +- .../Live/Instance/WatchdogTest.cs | 2 +- tgstation-server.sln | 2 + 31 files changed, 420 insertions(+), 78 deletions(-) create mode 100644 src/Tgstation.Server.Host/Components/Interop/Bridge/CustomEventInvocation.cs create mode 100644 tests/DMAPI/BasicOperation/test_event-qwer.bat create mode 100755 tests/DMAPI/BasicOperation/test_event-qwer.sh diff --git a/build/Version.props b/build/Version.props index 5f87b4c3ba..d5b05ce320 100644 --- a/build/Version.props +++ b/build/Version.props @@ -10,7 +10,7 @@ 13.1.0 15.1.0 7.1.0 - 5.8.0 + 5.9.0 1.4.1 1.2.1 2.0.0 diff --git a/src/DMAPI/tgs.dm b/src/DMAPI/tgs.dm index 1d7f7d02f8..dc49d2c6f0 100644 --- a/src/DMAPI/tgs.dm +++ b/src/DMAPI/tgs.dm @@ -496,6 +496,16 @@ /// Returns a list of connected [/datum/tgs_chat_channel]s if TGS is present, null otherwise. This function may sleep if the call to [/world/proc/TgsNew] is sleeping! /world/proc/TgsChatChannelInfo() return + +/** + * Trigger an event in TGS. Requires TGS version >= 6.3.0. Returns [TRUE] if the event was triggered successfully, [FALSE] otherwise. This function may sleep! + * + * event_name - The name of the event to trigger + * parameters - Optional list of string parameters to pass as arguments to the event script. The first parameter passed to a script will always be the running game's directory followed by these parameters. + * wait_for_completion - If set, this function will not return until the event has run to completion. + */ +/world/proc/TgsTriggerEvent(event_name, list/parameters, wait_for_completion = FALSE) + return /* The MIT License diff --git a/src/DMAPI/tgs/core/core.dm b/src/DMAPI/tgs/core/core.dm index 8be96f2740..15622228e9 100644 --- a/src/DMAPI/tgs/core/core.dm +++ b/src/DMAPI/tgs/core/core.dm @@ -166,3 +166,11 @@ var/datum/tgs_api/api = TGS_READ_GLOBAL(tgs) if(api) return api.Visibility() + +/world/TgsTriggerEvent(event_name, list/parameters, wait_for_completion = FALSE) + var/datum/tgs_api/api = TGS_READ_GLOBAL(tgs) + if(api) + if(!istype(parameters, /list)) + parameters = list() + + return api.TriggerEvent(event_name, parameters, wait_for_completion) diff --git a/src/DMAPI/tgs/core/datum.dm b/src/DMAPI/tgs/core/datum.dm index 07ce3b6845..fefca3af2f 100644 --- a/src/DMAPI/tgs/core/datum.dm +++ b/src/DMAPI/tgs/core/datum.dm @@ -69,3 +69,6 @@ TGS_PROTECT_DATUM(/datum/tgs_api) /datum/tgs_api/proc/Visibility() return TGS_UNIMPLEMENTED + +/datum/tgs_api/proc/TriggerEvent(event_name, list/parameters, wait_for_completion) + return FALSE diff --git a/src/DMAPI/tgs/v5/__interop_version.dm b/src/DMAPI/tgs/v5/__interop_version.dm index 616263098f..f4806f7adb 100644 --- a/src/DMAPI/tgs/v5/__interop_version.dm +++ b/src/DMAPI/tgs/v5/__interop_version.dm @@ -1 +1 @@ -"5.8.0" +"5.9.0" diff --git a/src/DMAPI/tgs/v5/_defines.dm b/src/DMAPI/tgs/v5/_defines.dm index 1c7d67d20c..92c7a8388a 100644 --- a/src/DMAPI/tgs/v5/_defines.dm +++ b/src/DMAPI/tgs/v5/_defines.dm @@ -14,6 +14,7 @@ #define DMAPI5_BRIDGE_COMMAND_KILL 4 #define DMAPI5_BRIDGE_COMMAND_CHAT_SEND 5 #define DMAPI5_BRIDGE_COMMAND_CHUNK 6 +#define DMAPI5_BRIDGE_COMMAND_EVENT 7 #define DMAPI5_PARAMETER_ACCESS_IDENTIFIER "accessIdentifier" #define DMAPI5_PARAMETER_CUSTOM_COMMANDS "customCommands" @@ -34,6 +35,7 @@ #define DMAPI5_BRIDGE_PARAMETER_VERSION "version" #define DMAPI5_BRIDGE_PARAMETER_CHAT_MESSAGE "chatMessage" #define DMAPI5_BRIDGE_PARAMETER_MINIMUM_SECURITY_LEVEL "minimumSecurityLevel" +#define DMAPI5_BRIDGE_PARAMETER_EVENT_INVOCATION "eventInvocation" #define DMAPI5_BRIDGE_RESPONSE_NEW_PORT "newPort" #define DMAPI5_BRIDGE_RESPONSE_RUNTIME_INFORMATION "runtimeInformation" @@ -81,6 +83,7 @@ #define DMAPI5_TOPIC_COMMAND_SEND_CHUNK 9 #define DMAPI5_TOPIC_COMMAND_RECEIVE_CHUNK 10 #define DMAPI5_TOPIC_COMMAND_RECEIVE_BROADCAST 11 +#define DMAPI5_TOPIC_COMMAND_COMPLETE_EVENT 12 #define DMAPI5_TOPIC_PARAMETER_COMMAND_TYPE "commandType" #define DMAPI5_TOPIC_PARAMETER_CHAT_COMMAND "chatCommand" @@ -116,3 +119,9 @@ #define DMAPI5_CUSTOM_CHAT_COMMAND_NAME "name" #define DMAPI5_CUSTOM_CHAT_COMMAND_HELP_TEXT "helpText" #define DMAPI5_CUSTOM_CHAT_COMMAND_ADMIN_ONLY "adminOnly" + +#define DMAPI5_EVENT_ID "eventId" + +#define DMAPI5_EVENT_INVOCATION_NAME "eventName" +#define DMAPI5_EVENT_INVOCATION_PARAMETERS "parameters" +#define DMAPI5_EVENT_INVOCATION_NOTIFY_COMPLETION "notifyCompletion" diff --git a/src/DMAPI/tgs/v5/api.dm b/src/DMAPI/tgs/v5/api.dm index a5c064a8ea..32d09544ea 100644 --- a/src/DMAPI/tgs/v5/api.dm +++ b/src/DMAPI/tgs/v5/api.dm @@ -27,6 +27,8 @@ var/chunked_requests = 0 var/list/chunked_topics = list() + var/list/pending_events = list() + var/detached = FALSE /datum/tgs_api/v5/New() @@ -249,6 +251,41 @@ WaitForReattach(TRUE) return chat_channels.Copy() +/datum/tgs_api/v5/TriggerEvent(event_name, list/parameters, wait_for_completion) + RequireInitialBridgeResponse() + WaitForReattach(TRUE) + + if(interop_version.minor < 9) + TGS_WARNING_LOG("Interop version too low for custom events!") + return FALSE + + var/str_parameters = list() + for(var/i in parameters) + str_parameters += "[i]" + + var/list/response = Bridge(DMAPI5_BRIDGE_COMMAND_EVENT, list(DMAPI5_BRIDGE_PARAMETER_EVENT_INVOCATION = list(DMAPI5_EVENT_INVOCATION_NAME = event_name, DMAPI5_EVENT_INVOCATION_PARAMETERS = str_parameters, DMAPI5_EVENT_INVOCATION_NOTIFY_COMPLETION = wait_for_completion))) + if(!response) + return FALSE + + var/event_id = response[DMAPI5_EVENT_ID] + if(!event_id) + return FALSE + + TGS_DEBUG_LOG("Created event ID: [event_id]") + if(!wait_for_completion) + return TRUE + + TGS_DEBUG_LOG("Waiting for completion of event ID: [event_id]") + pending_events[event_id] = TRUE + + do + sleep(1) + while(pending_events[event_id]) + + TGS_DEBUG_LOG("Completed wait on event ID: [event_id]") + + return TRUE + /datum/tgs_api/v5/proc/DecodeChannels(chat_update_json) TGS_DEBUG_LOG("DecodeChannels()") var/list/chat_channels_json = chat_update_json[DMAPI5_CHAT_UPDATE_CHANNELS] diff --git a/src/DMAPI/tgs/v5/topic.dm b/src/DMAPI/tgs/v5/topic.dm index 05e6c4e1b2..b13f83f82c 100644 --- a/src/DMAPI/tgs/v5/topic.dm +++ b/src/DMAPI/tgs/v5/topic.dm @@ -176,6 +176,9 @@ var/list/reattach_response = TopicResponse(error_message) reattach_response[DMAPI5_PARAMETER_CUSTOM_COMMANDS] = ListCustomCommands() reattach_response[DMAPI5_PARAMETER_TOPIC_PORT] = GetTopicPort() + + pending_events.Cut() + return reattach_response if(DMAPI5_TOPIC_COMMAND_SEND_CHUNK) @@ -276,6 +279,15 @@ TGS_WORLD_ANNOUNCE(message) return TopicResponse() + if(DMAPI5_TOPIC_COMMAND_COMPLETE_EVENT) + var/event_id = topic_parameters[DMAPI5_EVENT_ID] + if (!istext(event_id)) + return TopicResponse("Invalid or missing [DMAPI5_EVENT_ID]") + + TGS_DEBUG_LOG("Completing event ID [event_id]...") + pending_events -= event_id + return TopicResponse() + return TopicResponse("Unknown command: [command]") /datum/tgs_api/v5/proc/WorldBroadcast(message) diff --git a/src/DMAPI/tgs/v5/undefs.dm b/src/DMAPI/tgs/v5/undefs.dm index d531d4b7b9..237207fdfd 100644 --- a/src/DMAPI/tgs/v5/undefs.dm +++ b/src/DMAPI/tgs/v5/undefs.dm @@ -14,6 +14,7 @@ #undef DMAPI5_BRIDGE_COMMAND_KILL #undef DMAPI5_BRIDGE_COMMAND_CHAT_SEND #undef DMAPI5_BRIDGE_COMMAND_CHUNK +#undef DMAPI5_BRIDGE_COMMAND_EVENT #undef DMAPI5_PARAMETER_ACCESS_IDENTIFIER #undef DMAPI5_PARAMETER_CUSTOM_COMMANDS @@ -34,6 +35,7 @@ #undef DMAPI5_BRIDGE_PARAMETER_VERSION #undef DMAPI5_BRIDGE_PARAMETER_CHAT_MESSAGE #undef DMAPI5_BRIDGE_PARAMETER_MINIMUM_SECURITY_LEVEL +#undef DMAPI5_BRIDGE_PARAMETER_EVENT_INVOCATION #undef DMAPI5_BRIDGE_RESPONSE_NEW_PORT #undef DMAPI5_BRIDGE_RESPONSE_RUNTIME_INFORMATION @@ -81,6 +83,7 @@ #undef DMAPI5_TOPIC_COMMAND_SEND_CHUNK #undef DMAPI5_TOPIC_COMMAND_RECEIVE_CHUNK #undef DMAPI5_TOPIC_COMMAND_RECEIVE_BROADCAST +#undef DMAPI5_TOPIC_COMMAND_COMPLETE_EVENT #undef DMAPI5_TOPIC_PARAMETER_COMMAND_TYPE #undef DMAPI5_TOPIC_PARAMETER_CHAT_COMMAND @@ -116,3 +119,9 @@ #undef DMAPI5_CUSTOM_CHAT_COMMAND_NAME #undef DMAPI5_CUSTOM_CHAT_COMMAND_HELP_TEXT #undef DMAPI5_CUSTOM_CHAT_COMMAND_ADMIN_ONLY + +#undef DMAPI5_EVENT_ID + +#undef DMAPI5_EVENT_INVOCATION_NAME +#undef DMAPI5_EVENT_INVOCATION_PARAMETERS +#undef DMAPI5_EVENT_INVOCATION_NOTIFY_COMPLETION diff --git a/src/Tgstation.Server.Host/Components/Deployment/IDmbProvider.cs b/src/Tgstation.Server.Host/Components/Deployment/IDmbProvider.cs index 820796c318..7d5f0fd6a9 100644 --- a/src/Tgstation.Server.Host/Components/Deployment/IDmbProvider.cs +++ b/src/Tgstation.Server.Host/Components/Deployment/IDmbProvider.cs @@ -15,7 +15,7 @@ namespace Tgstation.Server.Host.Components.Deployment string DmbName { get; } /// - /// The primary game directory with a trailing directory separator. + /// The primary game directory. /// string Directory { get; } diff --git a/src/Tgstation.Server.Host/Components/Events/EventConsumer.cs b/src/Tgstation.Server.Host/Components/Events/EventConsumer.cs index 559e584a27..fa98e56607 100644 --- a/src/Tgstation.Server.Host/Components/Events/EventConsumer.cs +++ b/src/Tgstation.Server.Host/Components/Events/EventConsumer.cs @@ -30,6 +30,10 @@ namespace Tgstation.Server.Host.Components.Events this.configuration = configuration ?? throw new ArgumentNullException(nameof(configuration)); } + /// + public ValueTask? HandleCustomEvent(string eventName, IEnumerable parameters, CancellationToken cancellationToken) + => configuration.HandleCustomEvent(eventName, parameters, cancellationToken); + /// public async ValueTask HandleEvent(EventType eventType, IEnumerable parameters, bool deploymentPipeline, CancellationToken cancellationToken) { diff --git a/src/Tgstation.Server.Host/Components/Events/EventScriptAttribute.cs b/src/Tgstation.Server.Host/Components/Events/EventScriptAttribute.cs index ba93d34cf9..7d772c2318 100644 --- a/src/Tgstation.Server.Host/Components/Events/EventScriptAttribute.cs +++ b/src/Tgstation.Server.Host/Components/Events/EventScriptAttribute.cs @@ -1,5 +1,4 @@ using System; -using System.Collections.Generic; namespace Tgstation.Server.Host.Components.Events { @@ -12,7 +11,7 @@ namespace Tgstation.Server.Host.Components.Events /// /// The name and order of the scripts the event script the runs. /// - public IReadOnlyList ScriptNames { get; } + public string[] ScriptNames { get; } /// /// Initializes a new instance of the class. diff --git a/src/Tgstation.Server.Host/Components/Events/IEventConsumer.cs b/src/Tgstation.Server.Host/Components/Events/IEventConsumer.cs index 4d268011f1..a12547e8c2 100644 --- a/src/Tgstation.Server.Host/Components/Events/IEventConsumer.cs +++ b/src/Tgstation.Server.Host/Components/Events/IEventConsumer.cs @@ -18,5 +18,14 @@ namespace Tgstation.Server.Host.Components.Events /// The for the operation. /// A representing the running operation. ValueTask HandleEvent(EventType eventType, IEnumerable parameters, bool deploymentPipeline, CancellationToken cancellationToken); + + /// + /// Handles a given custom event. + /// + /// The name of the event. + /// An of parameters for the event. + /// The for the operation. + /// A representing the running operation if the event was triggered successfully, if it matched a TGS event and wasn't executed. + ValueTask? HandleCustomEvent(string eventName, IEnumerable parameters, CancellationToken cancellationToken); } } diff --git a/src/Tgstation.Server.Host/Components/Events/NoopEventConsumer.cs b/src/Tgstation.Server.Host/Components/Events/NoopEventConsumer.cs index dde777572a..880a45c9ef 100644 --- a/src/Tgstation.Server.Host/Components/Events/NoopEventConsumer.cs +++ b/src/Tgstation.Server.Host/Components/Events/NoopEventConsumer.cs @@ -12,5 +12,9 @@ namespace Tgstation.Server.Host.Components.Events /// public ValueTask HandleEvent(EventType eventType, IEnumerable parameters, bool deploymentPipeline, CancellationToken cancellationToken) => ValueTask.CompletedTask; + + /// + public ValueTask? HandleCustomEvent(string eventName, IEnumerable parameters, CancellationToken cancellationToken) + => ValueTask.CompletedTask; } } diff --git a/src/Tgstation.Server.Host/Components/Interop/Bridge/BridgeCommandType.cs b/src/Tgstation.Server.Host/Components/Interop/Bridge/BridgeCommandType.cs index 8c18f74c39..287e9aceda 100644 --- a/src/Tgstation.Server.Host/Components/Interop/Bridge/BridgeCommandType.cs +++ b/src/Tgstation.Server.Host/Components/Interop/Bridge/BridgeCommandType.cs @@ -39,5 +39,10 @@ /// DreamDaemon attempting to send a longer bridge message. /// Chunk, + + /// + /// DreamDaemon requesting a custom event to be triggered. + /// + Event, } } diff --git a/src/Tgstation.Server.Host/Components/Interop/Bridge/BridgeParameters.cs b/src/Tgstation.Server.Host/Components/Interop/Bridge/BridgeParameters.cs index 1240bc81cb..228c467768 100644 --- a/src/Tgstation.Server.Host/Components/Interop/Bridge/BridgeParameters.cs +++ b/src/Tgstation.Server.Host/Components/Interop/Bridge/BridgeParameters.cs @@ -51,6 +51,11 @@ namespace Tgstation.Server.Host.Components.Interop.Bridge /// public ushort? TopicPort { get; set; } + /// + /// The being triggered. + /// + public CustomEventInvocation? EventInvocation { get; set; } + /// /// Initializes a new instance of the class. /// diff --git a/src/Tgstation.Server.Host/Components/Interop/Bridge/BridgeResponse.cs b/src/Tgstation.Server.Host/Components/Interop/Bridge/BridgeResponse.cs index 620241fafb..da3a3bb563 100644 --- a/src/Tgstation.Server.Host/Components/Interop/Bridge/BridgeResponse.cs +++ b/src/Tgstation.Server.Host/Components/Interop/Bridge/BridgeResponse.cs @@ -21,5 +21,10 @@ namespace Tgstation.Server.Host.Components.Interop.Bridge /// The s missing from a chunked request. /// public IReadOnlyCollection? MissingChunks { get; set; } + + /// + /// The triggered event ID for requests. + /// + public string? EventId { get; set; } } } diff --git a/src/Tgstation.Server.Host/Components/Interop/Bridge/CustomEventInvocation.cs b/src/Tgstation.Server.Host/Components/Interop/Bridge/CustomEventInvocation.cs new file mode 100644 index 0000000000..afb6ab66cf --- /dev/null +++ b/src/Tgstation.Server.Host/Components/Interop/Bridge/CustomEventInvocation.cs @@ -0,0 +1,25 @@ +using System.Collections.Generic; + +namespace Tgstation.Server.Host.Components.Interop.Bridge +{ + /// + /// Parameters for invoking a custom event. + /// + public sealed class CustomEventInvocation + { + /// + /// The name of the event being invoked. + /// + public string? EventName { get; set; } + + /// + /// The parameters for the invoked event. + /// + public ICollection? Parameters { get; set; } + + /// + /// If the DMAPI should be notified when the event compeletes. + /// + public bool? NotifyCompletion { get; set; } + } +} diff --git a/src/Tgstation.Server.Host/Components/Interop/Topic/EventNotification.cs b/src/Tgstation.Server.Host/Components/Interop/Topic/EventNotification.cs index 75eb3bc9f4..1284940b12 100644 --- a/src/Tgstation.Server.Host/Components/Interop/Topic/EventNotification.cs +++ b/src/Tgstation.Server.Host/Components/Interop/Topic/EventNotification.cs @@ -15,12 +15,12 @@ namespace Tgstation.Server.Host.Components.Interop.Topic /// The triggered. /// /// Nullable to prevent ignoring when serializing. - public EventType? Type { get; } + public EventType Type { get; } /// /// The set of parameters. /// - public IReadOnlyCollection Parameters { get; } + public IReadOnlyCollection? Parameters { get; } /// /// Initializes a new instance of the class. diff --git a/src/Tgstation.Server.Host/Components/Interop/Topic/TopicCommandType.cs b/src/Tgstation.Server.Host/Components/Interop/Topic/TopicCommandType.cs index 286c605d07..c7d21332c3 100644 --- a/src/Tgstation.Server.Host/Components/Interop/Topic/TopicCommandType.cs +++ b/src/Tgstation.Server.Host/Components/Interop/Topic/TopicCommandType.cs @@ -67,5 +67,10 @@ namespace Tgstation.Server.Host.Components.Interop.Topic /// Sending a broadcast message. /// Broadcast, + + /// + /// Notifying about the completion of a custom event. + /// + CompleteEvent, } } diff --git a/src/Tgstation.Server.Host/Components/Interop/Topic/TopicParameters.cs b/src/Tgstation.Server.Host/Components/Interop/Topic/TopicParameters.cs index bdc8405b02..1b2745ab6a 100644 --- a/src/Tgstation.Server.Host/Components/Interop/Topic/TopicParameters.cs +++ b/src/Tgstation.Server.Host/Components/Interop/Topic/TopicParameters.cs @@ -62,6 +62,11 @@ namespace Tgstation.Server.Host.Components.Interop.Topic /// public ChunkData? Chunk { get; } + /// + /// The completed custom event ID. + /// + public string? EventId { get; set; } + /// /// Whether or not the constitute a priority request. /// @@ -74,6 +79,7 @@ namespace Tgstation.Server.Host.Components.Interop.Topic or TopicCommandType.InstanceRenamed or TopicCommandType.ChatChannelsUpdate or TopicCommandType.Broadcast + or TopicCommandType.CompleteEvent or TopicCommandType.ServerRestarted => true, TopicCommandType.ChatCommand or TopicCommandType.HealthCheck @@ -174,6 +180,16 @@ namespace Tgstation.Server.Host.Components.Interop.Topic Chunk = chunk ?? throw new ArgumentNullException(nameof(chunk)); } + /// + /// Initializes a new instance of the class. + /// + /// The containig the value of . + public TopicParameters(Guid eventId) + : this(TopicCommandType.CompleteEvent) + { + EventId = eventId.ToString(); + } + /// /// Initializes a new instance of the class. /// diff --git a/src/Tgstation.Server.Host/Components/Session/SessionController.cs b/src/Tgstation.Server.Host/Components/Session/SessionController.cs index 110506c6e2..e32c251eb5 100644 --- a/src/Tgstation.Server.Host/Components/Session/SessionController.cs +++ b/src/Tgstation.Server.Host/Components/Session/SessionController.cs @@ -19,6 +19,7 @@ using Tgstation.Server.Host.Components.Chat; using Tgstation.Server.Host.Components.Chat.Commands; using Tgstation.Server.Host.Components.Deployment; using Tgstation.Server.Host.Components.Engine; +using Tgstation.Server.Host.Components.Events; using Tgstation.Server.Host.Components.Interop; using Tgstation.Server.Host.Components.Interop.Bridge; using Tgstation.Server.Host.Components.Interop.Topic; @@ -159,6 +160,11 @@ namespace Tgstation.Server.Host.Components.Session /// readonly IDotnetDumpService dotnetDumpService; + /// + /// The for the . + /// + readonly IEventConsumer eventConsumer; + /// /// The that completes when DD makes it's first bridge request. /// @@ -170,9 +176,9 @@ namespace Tgstation.Server.Host.Components.Session readonly Api.Models.Instance metadata; /// - /// A used for the topic send operation made on reattaching. + /// A used for tasks that should not exceed the lifetime of the session. /// - readonly CancellationTokenSource reattachTopicCts; + readonly CancellationTokenSource sessionDurationCts; /// /// for port updates and . @@ -204,6 +210,11 @@ namespace Tgstation.Server.Host.Components.Session /// volatile Task rebootGate; + /// + /// The representing calls to . + /// + volatile Task customEventProcessingTask; + /// /// for shutting down the server if it is taking too long after validation. /// @@ -248,6 +259,7 @@ namespace Tgstation.Server.Host.Components.Session /// The for the . /// The value of . /// The value of . + /// The value of . /// The value of . /// The returning a to be run after the ends. /// The optional time to wait before failing the . @@ -265,6 +277,7 @@ namespace Tgstation.Server.Host.Components.Session IAssemblyInformationProvider assemblyInformationProvider, IAsyncDelayer asyncDelayer, IDotnetDumpService dotnetDumpService, + IEventConsumer eventConsumer, ILogger logger, Func postLifetimeCallback, uint? startupTimeout, @@ -285,6 +298,7 @@ namespace Tgstation.Server.Host.Components.Session this.asyncDelayer = asyncDelayer ?? throw new ArgumentNullException(nameof(asyncDelayer)); this.dotnetDumpService = dotnetDumpService ?? throw new ArgumentNullException(nameof(dotnetDumpService)); + this.eventConsumer = eventConsumer ?? throw new ArgumentNullException(nameof(eventConsumer)); apiValidationSession = apiValidate; @@ -297,12 +311,13 @@ namespace Tgstation.Server.Host.Components.Session primeTcs = new TaskCompletionSource(); rebootGate = Task.CompletedTask; + customEventProcessingTask = Task.CompletedTask; // 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(); + sessionDurationCts = new CancellationTokenSource(); TopicSendSemaphore = new FifoSemaphore(); synchronizationLock = new object(); @@ -356,7 +371,7 @@ namespace Tgstation.Server.Host.Components.Session Logger.LogTrace("Disposing..."); - reattachTopicCts.Cancel(); + sessionDurationCts.Cancel(); var cancellationToken = CancellationToken.None; // DCT: None available var semaphoreLockTask = TopicSendSemaphore.Lock(cancellationToken); @@ -381,13 +396,15 @@ namespace Tgstation.Server.Host.Components.Session await regularDmbDisposeTask; chatTrackingContext.Dispose(); - reattachTopicCts.Dispose(); + sessionDurationCts.Dispose(); if (!released) await Lifetime; // finish the async callback (await semaphoreLockTask).Dispose(); TopicSendSemaphore.Dispose(); + + await customEventProcessingTask; } /// @@ -547,7 +564,7 @@ namespace Tgstation.Server.Host.Components.Session assemblyInformationProvider.Version, ReattachInformation.RuntimeInformation!.ServerPort), true, - reattachTopicCts.Token); + sessionDurationCts.Token); if (reattachResponse != null) { @@ -735,6 +752,8 @@ namespace Tgstation.Server.Host.Components.Session break; case BridgeCommandType.Chunk: return await ProcessChunk(ProcessBridgeCommand, BridgeError, parameters.Chunk, cancellationToken); + case BridgeCommandType.Event: + return TriggerCustomEvent(parameters.EventInvocation); case null: return BridgeError("Missing commandType!"); default: @@ -1102,5 +1121,81 @@ namespace Tgstation.Server.Host.Components.Session return fullResponse; } + + /// + /// Trigger a custom event from a given . + /// + /// The . + /// An appropriate . + BridgeResponse TriggerCustomEvent(CustomEventInvocation? invocation) + { + if (invocation == null) + return BridgeError("Missing eventInvocation!"); + + var eventName = invocation.EventName; + if (eventName == null) + return BridgeError("Missing eventName!"); + + var notifyCompletion = invocation.NotifyCompletion; + if (!notifyCompletion.HasValue) + return BridgeError("Missing notifyCompletion!"); + + var eventParams = new List + { + ReattachInformation.Dmb.Directory, + }; + + eventParams.AddRange(invocation + .Parameters? + .Where(param => param != null) + .Cast() + ?? Enumerable.Empty()); + + var eventId = Guid.NewGuid(); + Logger.LogInformation("Triggering custom event \"{eventName}\": {eventId}", eventName, eventId); + + var cancellationToken = sessionDurationCts.Token; + ValueTask? eventTask = eventConsumer.HandleCustomEvent(eventName, eventParams, cancellationToken); + + async Task ProcessEvent() + { + try + { + await eventTask.Value; + + if (notifyCompletion.Value) + await SendCommand( + new TopicParameters(eventId), + cancellationToken); + else + Logger.LogTrace("Finished custom event {eventId}, not sending notification.", eventId); + } + catch (OperationCanceledException ex) + { + Logger.LogDebug(ex, "Custom event invocation {eventId} aborted!", eventId); + } + catch (Exception ex) + { + Logger.LogWarning(ex, "Custom event invocation {eventId} errored!", eventId); + } + } + + if (!eventTask.HasValue) + return BridgeError("Event refused to execute due to matching a TGS event!"); + + lock (sessionDurationCts) + { + var previousEventProcessingTask = customEventProcessingTask; + var eventProcessingTask = ProcessEvent(); + customEventProcessingTask = Task.WhenAll(customEventProcessingTask, eventProcessingTask); + } + + return new BridgeResponse + { + EventId = notifyCompletion.Value + ? eventId.ToString() + : null, + }; + } } } diff --git a/src/Tgstation.Server.Host/Components/Session/SessionControllerFactory.cs b/src/Tgstation.Server.Host/Components/Session/SessionControllerFactory.cs index 4c7fab19b9..5e89fe8cb2 100644 --- a/src/Tgstation.Server.Host/Components/Session/SessionControllerFactory.cs +++ b/src/Tgstation.Server.Host/Components/Session/SessionControllerFactory.cs @@ -355,6 +355,7 @@ namespace Tgstation.Server.Host.Components.Session assemblyInformationProvider, asyncDelayer, dotnetDumpService, + eventConsumer, loggerFactory.CreateLogger(), () => LogDDOutput( process, @@ -446,6 +447,7 @@ namespace Tgstation.Server.Host.Components.Session assemblyInformationProvider, asyncDelayer, dotnetDumpService, + eventConsumer, loggerFactory.CreateLogger(), () => ValueTask.CompletedTask, null, diff --git a/src/Tgstation.Server.Host/Components/StaticFiles/Configuration.cs b/src/Tgstation.Server.Host/Components/StaticFiles/Configuration.cs index c77fa7ab49..f25f515a60 100644 --- a/src/Tgstation.Server.Host/Components/StaticFiles/Configuration.cs +++ b/src/Tgstation.Server.Host/Components/StaticFiles/Configuration.cs @@ -71,11 +71,11 @@ namespace Tgstation.Server.Host.Components.StaticFiles /// /// Map of s to the filename of the event scripts they trigger. /// - public static IReadOnlyDictionary> EventTypeScriptFileNameMap { get; } = new Dictionary>( + public static IReadOnlyDictionary EventTypeScriptFileNameMap { get; } = new Dictionary( Enum.GetValues(typeof(EventType)) .Cast() .Select( - eventType => new KeyValuePair>( + eventType => new KeyValuePair( eventType, typeof(EventType) .GetField(eventType.ToString())! @@ -600,70 +600,39 @@ namespace Tgstation.Server.Host.Components.StaticFiles public Task StopAsync(CancellationToken cancellationToken) => EnsureDirectories(cancellationToken); /// - public async ValueTask HandleEvent(EventType eventType, IEnumerable parameters, bool deploymentPipeline, CancellationToken cancellationToken) + public ValueTask HandleEvent(EventType eventType, IEnumerable parameters, bool deploymentPipeline, CancellationToken cancellationToken) { ArgumentNullException.ThrowIfNull(parameters); - await EnsureDirectories(cancellationToken); - if (!EventTypeScriptFileNameMap.TryGetValue(eventType, out var scriptNames)) - return; - - // always execute in serial - using (await SemaphoreSlimContext.Lock(semaphore, cancellationToken)) { - var files = await ioManager.GetFilesWithExtension(EventScriptsSubdirectory, platformIdentifier.ScriptFileExtension, false, cancellationToken); - var resolvedScriptsDir = ioManager.ResolvePath(EventScriptsSubdirectory); - - var scriptFiles = files - .Select(x => ioManager.GetFileName(x)) - .Where(x => scriptNames.Any( - scriptName => x.StartsWith(scriptName, StringComparison.Ordinal))) - .ToList(); - - if (scriptFiles.Count == 0) - { - logger.LogTrace("No event scripts starting with \"{scriptName}\" detected", String.Join("\" or \"", scriptNames)); - return; - } - - foreach (var scriptFile in scriptFiles) - { - logger.LogTrace("Running event script {scriptFile}...", scriptFile); - await using (var script = processExecutor.LaunchProcess( - ioManager.ConcatPath(resolvedScriptsDir, scriptFile), - resolvedScriptsDir, - String.Join( - ' ', - parameters.Select(arg => - { - if (arg == null) - return "(NULL)"; - - if (!arg.Contains(' ', StringComparison.Ordinal)) - return arg; - - arg = arg.Replace("\"", "\\\"", StringComparison.Ordinal); - - return $"\"{arg}\""; - })), - readStandardHandles: true, - noShellExecute: true)) - using (cancellationToken.Register(() => script.Terminate())) - { - if (sessionConfiguration.LowPriorityDeploymentProcesses && deploymentPipeline) - script.AdjustPriority(false); - - var exitCode = await script.Lifetime; - cancellationToken.ThrowIfCancellationRequested(); - var scriptOutput = await script.GetCombinedOutput(cancellationToken); - if (exitCode != 0) - throw new JobException($"Script {scriptFile} exited with code {exitCode}:{Environment.NewLine}{scriptOutput}"); - else - logger.LogDebug("Script output:{newLine}{scriptOutput}", Environment.NewLine, scriptOutput); - } - } + logger.LogTrace("No event script for event {event}!", eventType); + return ValueTask.CompletedTask; } + + return ExecuteEventScripts(parameters, deploymentPipeline, cancellationToken, scriptNames); + } + + /// + public ValueTask? HandleCustomEvent(string scriptName, IEnumerable parameters, CancellationToken cancellationToken) + { + var scriptNameIsTgsEventName = EventTypeScriptFileNameMap + .Values + .SelectMany(scriptNames => scriptNames) + .Any(tgsScriptName => tgsScriptName.Equals( + scriptName, + platformIdentifier.IsWindows + ? StringComparison.OrdinalIgnoreCase + : StringComparison.Ordinal)); + if (scriptNameIsTgsEventName) + { + logger.LogWarning("DMAPI attempted to execute TGS reserved event: {eventName}", scriptName); + return null; + } + +#pragma warning disable CA2012 // Use ValueTasks correctly + return ExecuteEventScripts(parameters, false, cancellationToken, scriptName); +#pragma warning restore CA2012 // Use ValueTasks correctly } /// @@ -758,5 +727,74 @@ namespace Tgstation.Server.Host.Components.StaticFiles throw new InvalidOperationException("Attempted to access file outside of configuration manager!"); return resolved; } + + /// + /// Execute a set of given . + /// + /// An of parameters for the . + /// If this event is part of the deployment pipeline. + /// The for the operation. + /// The names of the scripts to execute. + /// A representing the running operation. + async ValueTask ExecuteEventScripts(IEnumerable parameters, bool deploymentPipeline, CancellationToken cancellationToken, params string[] scriptNames) + { + await EnsureDirectories(cancellationToken); + + // always execute in serial + using (await SemaphoreSlimContext.Lock(semaphore, cancellationToken)) + { + var files = await ioManager.GetFilesWithExtension(EventScriptsSubdirectory, platformIdentifier.ScriptFileExtension, false, cancellationToken); + var resolvedScriptsDir = ioManager.ResolvePath(EventScriptsSubdirectory); + + var scriptFiles = files + .Select(x => ioManager.GetFileName(x)) + .Where(x => scriptNames.Any( + scriptName => x.StartsWith(scriptName, StringComparison.Ordinal))) + .ToList(); + + if (scriptFiles.Count == 0) + { + logger.LogTrace("No event scripts starting with \"{scriptName}\" detected", String.Join("\" or \"", scriptNames)); + return; + } + + foreach (var scriptFile in scriptFiles) + { + logger.LogTrace("Running event script {scriptFile}...", scriptFile); + await using (var script = processExecutor.LaunchProcess( + ioManager.ConcatPath(resolvedScriptsDir, scriptFile), + resolvedScriptsDir, + String.Join( + ' ', + parameters.Select(arg => + { + if (arg == null) + return "(NULL)"; + + if (!arg.Contains(' ', StringComparison.Ordinal)) + return arg; + + arg = arg.Replace("\"", "\\\"", StringComparison.Ordinal); + + return $"\"{arg}\""; + })), + readStandardHandles: true, + noShellExecute: true)) + using (cancellationToken.Register(() => script.Terminate())) + { + if (sessionConfiguration.LowPriorityDeploymentProcesses && deploymentPipeline) + script.AdjustPriority(false); + + var exitCode = await script.Lifetime; + cancellationToken.ThrowIfCancellationRequested(); + var scriptOutput = await script.GetCombinedOutput(cancellationToken); + if (exitCode != 0) + throw new JobException($"Script {scriptFile} exited with code {exitCode}:{Environment.NewLine}{scriptOutput}"); + else + logger.LogDebug("Script output:{newLine}{scriptOutput}", Environment.NewLine, scriptOutput); + } + } + } + } } } diff --git a/src/Tgstation.Server.Host/Components/Watchdog/WatchdogBase.cs b/src/Tgstation.Server.Host/Components/Watchdog/WatchdogBase.cs index 8e891f10c3..211249e0df 100644 --- a/src/Tgstation.Server.Host/Components/Watchdog/WatchdogBase.cs +++ b/src/Tgstation.Server.Host/Components/Watchdog/WatchdogBase.cs @@ -507,6 +507,10 @@ namespace Tgstation.Server.Host.Components.Watchdog HandleChatResponses(result); } + /// + ValueTask? IEventConsumer.HandleCustomEvent(string eventName, IEnumerable parameters, CancellationToken cancellationToken) + => throw new NotSupportedException("Watchdogs do not support custom events!"); + /// /// Starts all s. /// diff --git a/tests/DMAPI/BasicOperation/Test.dm b/tests/DMAPI/BasicOperation/Test.dm index 022088fc67..aa0455b659 100644 --- a/tests/DMAPI/BasicOperation/Test.dm +++ b/tests/DMAPI/BasicOperation/Test.dm @@ -19,6 +19,21 @@ if(!("test" in world_params) || world_params["test"] != "bababooey") FailTest("Expected parameter test=bababooey but did not receive", "test_fail_reason.txt") + fdel("test_event_output.txt") + var/test_data = "nwfiuurhfu" + world.TgsTriggerEvent("test_event", list(test_data), TRUE) + if(!fexists("test_event_output.txt")) + FailTest("Expected test_event_output.txt to exist here", "test_fail_reason.txt") + + var/test_contents = copytext(file2text("test_event_output.txt"), 1, length(test_data) + 1) + if(test_contents != test_data) + FailTest("Expected test_event_output.txt to contain [test_data] here. Got [test_contents]", "test_fail_reason.txt") + + fdel("test_event_output.txt") + world.TgsTriggerEvent("test_event", list("asdf"), FALSE) + if(fexists("test_event_output.txt")) + FailTest("Expected test_event_output.txt to not exist here", "test_fail_reason.txt") + world.log << "sleep2" sleep(150) world.log << "Terminating..." diff --git a/tests/DMAPI/BasicOperation/test_event-qwer.bat b/tests/DMAPI/BasicOperation/test_event-qwer.bat new file mode 100644 index 0000000000..ecbce0d0af --- /dev/null +++ b/tests/DMAPI/BasicOperation/test_event-qwer.bat @@ -0,0 +1,7 @@ +echo "Running test_event script" + +rem mingw has their own /usr/bin/timeout +C:\Windows\system32\timeout.exe /t 5 +cd %1 +cd tests\DMAPI\BasicOperation +echo %2 > test_event_output.txt diff --git a/tests/DMAPI/BasicOperation/test_event-qwer.sh b/tests/DMAPI/BasicOperation/test_event-qwer.sh new file mode 100755 index 0000000000..185bc88fed --- /dev/null +++ b/tests/DMAPI/BasicOperation/test_event-qwer.sh @@ -0,0 +1,13 @@ +#!/bin/bash + +set -e + +echo "Running test_event script - $1 - $2" + +sleep 5 + +cd $1 +cd tests/DMAPI/BasicOperation + +echo $2 > test_event_output.txt + diff --git a/tests/Tgstation.Server.Tests/Live/Instance/ConfigurationTest.cs b/tests/Tgstation.Server.Tests/Live/Instance/ConfigurationTest.cs index 9ef929701b..eb39799414 100644 --- a/tests/Tgstation.Server.Tests/Live/Instance/ConfigurationTest.cs +++ b/tests/Tgstation.Server.Tests/Live/Instance/ConfigurationTest.cs @@ -118,7 +118,7 @@ namespace Tgstation.Server.Tests.Live.Instance await using var memoryStream2 = new MemoryStream(Encoding.UTF8.GetBytes("bbb")); await configurationClient.Write(staticFile2, memoryStream2, cancellationToken); - async ValueTask UploadScript(string scriptId) + async ValueTask UploadScript(string scriptId, bool basic) { var shellScriptExtension = new PlatformIdentifier().IsWindows ? ".bat" : ".sh"; var scriptName = $"{scriptId}{shellScriptExtension}"; @@ -127,15 +127,16 @@ namespace Tgstation.Server.Tests.Live.Instance Path = $"/EventScripts/{scriptName}" }; - await using var readStream = ioManager.GetFileStream($"../../../../DMAPI/LongRunning/{scriptName}", false); + await using var readStream = ioManager.GetFileStream($"../../../../DMAPI/{(basic ? "BasicOperation" : "LongRunning")}/{scriptName}", false); await configurationClient.Write( resourcingScript, readStream, cancellationToken); } - await UploadScript("PreCompile-GenerateRandomResource"); - await UploadScript("EngineActiveVersionChange-SetupEnv"); + await UploadScript("PreCompile-GenerateRandomResource", false); + await UploadScript("EngineActiveVersionChange-SetupEnv", false); + await UploadScript("test_event-qwer", true); } return ValueTaskExtensions.WhenAll( diff --git a/tests/Tgstation.Server.Tests/Live/Instance/WatchdogTest.cs b/tests/Tgstation.Server.Tests/Live/Instance/WatchdogTest.cs index fd927a684a..4a453d4f51 100644 --- a/tests/Tgstation.Server.Tests/Live/Instance/WatchdogTest.cs +++ b/tests/Tgstation.Server.Tests/Live/Instance/WatchdogTest.cs @@ -1485,7 +1485,7 @@ namespace Tgstation.Server.Tests.Live.Instance var newStatus = await instanceClient.DreamDaemon.Read(cancellationToken); Assert.IsTrue(newStatus.SoftShutdown.Value || newStatus.Status.Value == WatchdogStatus.Offline); - var timeout = 20; + var timeout = 40; do { await Task.Delay(TimeSpan.FromSeconds(3), cancellationToken); diff --git a/tgstation-server.sln b/tgstation-server.sln index fead8a204d..ad10c6fa1b 100644 --- a/tgstation-server.sln +++ b/tgstation-server.sln @@ -176,6 +176,8 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "BasicOperation", "BasicOper tests\DMAPI\BasicOperation\basic operation_test.dme = tests\DMAPI\BasicOperation\basic operation_test.dme tests\DMAPI\BasicOperation\Config.dm = tests\DMAPI\BasicOperation\Config.dm tests\DMAPI\BasicOperation\Test.dm = tests\DMAPI\BasicOperation\Test.dm + tests\DMAPI\BasicOperation\test_event-qwer.sh = tests\DMAPI\BasicOperation\test_event-qwer.sh + tests\DMAPI\BasicOperation\test_event-qwer.bat = tests\DMAPI\BasicOperation\test_event-qwer.bat EndProjectSection EndProject Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "BuildFail", "BuildFail", "{103C61AB-67D6-46FE-AA47-CC633B88EE0F}" From ae3fa1785ecedfa2b5ab33843567b407936046f5 Mon Sep 17 00:00:00 2001 From: Jordan Dominion Date: Mon, 19 Feb 2024 16:47:12 -0500 Subject: [PATCH 07/22] Change all `sleep(1)`s into `sleep(world.tick_lag)`s --- src/DMAPI/tgs/core/datum.dm | 4 ++-- src/DMAPI/tgs/v4/api.dm | 6 +++--- src/DMAPI/tgs/v5/api.dm | 4 ++-- src/DMAPI/tgs/v5/bridge.dm | 2 +- tests/DMAPI/LongRunning/Test.dm | 4 ++-- tests/DMAPI/test_setup.dm | 2 +- 6 files changed, 11 insertions(+), 11 deletions(-) diff --git a/src/DMAPI/tgs/core/datum.dm b/src/DMAPI/tgs/core/datum.dm index fefca3af2f..898516f124 100644 --- a/src/DMAPI/tgs/core/datum.dm +++ b/src/DMAPI/tgs/core/datum.dm @@ -17,7 +17,7 @@ TGS_DEFINE_AND_SET_GLOBAL(tgs, null) world.sleep_offline = FALSE // https://www.byond.com/forum/post/2894866 del(world) world.sleep_offline = FALSE // just in case, this is BYOND after all... - sleep(1) + sleep(world.tick_lag) TGS_DEBUG_LOG("BYOND DIDN'T TERMINATE THE WORLD!!! TICK IS: [world.time], sleep_offline: [world.sleep_offline]") /datum/tgs_api/latest @@ -69,6 +69,6 @@ TGS_PROTECT_DATUM(/datum/tgs_api) /datum/tgs_api/proc/Visibility() return TGS_UNIMPLEMENTED - + /datum/tgs_api/proc/TriggerEvent(event_name, list/parameters, wait_for_completion) return FALSE diff --git a/src/DMAPI/tgs/v4/api.dm b/src/DMAPI/tgs/v4/api.dm index 945e2e4117..7c87922750 100644 --- a/src/DMAPI/tgs/v4/api.dm +++ b/src/DMAPI/tgs/v4/api.dm @@ -181,7 +181,7 @@ var/json = json_encode(data) while(requesting_new_port && !override_requesting_new_port) - sleep(1) + sleep(world.tick_lag) //we need some port open at this point to facilitate return communication if(!world.port) @@ -209,7 +209,7 @@ requesting_new_port = FALSE while(export_lock) - sleep(1) + sleep(world.tick_lag) export_lock = TRUE last_interop_response = null @@ -217,7 +217,7 @@ text2file(json, server_commands_json_path) for(var/I = 0; I < EXPORT_TIMEOUT_DS && !last_interop_response; ++I) - sleep(1) + sleep(world.tick_lag) if(!last_interop_response) TGS_ERROR_LOG("Failed to get export result for: [json]") diff --git a/src/DMAPI/tgs/v5/api.dm b/src/DMAPI/tgs/v5/api.dm index 32d09544ea..ffdde1ae20 100644 --- a/src/DMAPI/tgs/v5/api.dm +++ b/src/DMAPI/tgs/v5/api.dm @@ -127,7 +127,7 @@ TGS_DEBUG_LOG("RequireInitialBridgeResponse: Starting sleep") logged = TRUE - sleep(1) + sleep(world.tick_lag) TGS_DEBUG_LOG("RequireInitialBridgeResponse: Passed") @@ -279,7 +279,7 @@ pending_events[event_id] = TRUE do - sleep(1) + sleep(world.tick_lag) while(pending_events[event_id]) TGS_DEBUG_LOG("Completed wait on event ID: [event_id]") diff --git a/src/DMAPI/tgs/v5/bridge.dm b/src/DMAPI/tgs/v5/bridge.dm index d986ec7e73..763ab3e02b 100644 --- a/src/DMAPI/tgs/v5/bridge.dm +++ b/src/DMAPI/tgs/v5/bridge.dm @@ -65,7 +65,7 @@ if(detached) // Wait up to one minute for(var/i in 1 to 600) - sleep(1) + sleep(world.tick_lag) if(!detached && (!require_channels || length(chat_channels))) break diff --git a/tests/DMAPI/LongRunning/Test.dm b/tests/DMAPI/LongRunning/Test.dm index f4468953a7..2735c8e0ab 100644 --- a/tests/DMAPI/LongRunning/Test.dm +++ b/tests/DMAPI/LongRunning/Test.dm @@ -203,7 +203,7 @@ var/run_bridge_test DetachedChatMessageQueuingP2() /proc/DetachedChatMessageQueuingP2() - sleep(1) + sleep(world.tick_lag) DetachedChatMessageQueuingP3() /proc/DetachedChatMessageQueuingP3() @@ -240,7 +240,7 @@ var/received_health_check = FALSE DelayCheckDetach() /proc/DelayCheckDetach() - sleep(1) + sleep(world.tick_lag) // hack hack, calling world.TgsChatChannelInfo() will try to delay until the channels come back var/datum/tgs_api/v5/api = TGS_READ_GLOBAL(tgs) if(length(api.chat_channels)) diff --git a/tests/DMAPI/test_setup.dm b/tests/DMAPI/test_setup.dm index 56f4c03eb8..ac345230b7 100644 --- a/tests/DMAPI/test_setup.dm +++ b/tests/DMAPI/test_setup.dm @@ -32,4 +32,4 @@ text2file(reason, "test_fail_reason.txt") world.log << "Terminating..." del(world) - sleep(1) // https://www.byond.com/forum/post/2894866 + sleep(world.tick_lag) // https://www.byond.com/forum/post/2894866 From 36dff446469dfd69e9ed7e51b71eab0d4e48b9e3 Mon Sep 17 00:00:00 2001 From: Jordan Dominion Date: Mon, 19 Feb 2024 16:49:53 -0500 Subject: [PATCH 08/22] Update Postgres library --- src/Tgstation.Server.Host/Tgstation.Server.Host.csproj | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Tgstation.Server.Host/Tgstation.Server.Host.csproj b/src/Tgstation.Server.Host/Tgstation.Server.Host.csproj index a944063eaa..7ef4d1c18c 100644 --- a/src/Tgstation.Server.Host/Tgstation.Server.Host.csproj +++ b/src/Tgstation.Server.Host/Tgstation.Server.Host.csproj @@ -100,7 +100,7 @@ - + From 5e0eb1aaf6a7bd734b196ccf6696a28f20d2d140 Mon Sep 17 00:00:00 2001 From: Jordan Dominion Date: Mon, 19 Feb 2024 17:17:32 -0500 Subject: [PATCH 09/22] Address winget manifest 1.6 update --- build/package/winget/manifest/Tgstation.Server.installer.yaml | 2 +- .../winget/manifest/Tgstation.Server.locale.en-US.yaml | 2 +- build/package/winget/manifest/Tgstation.Server.yaml | 2 +- tools/Tgstation.Server.ReleaseNotes/Program.cs | 4 ++-- 4 files changed, 5 insertions(+), 5 deletions(-) diff --git a/build/package/winget/manifest/Tgstation.Server.installer.yaml b/build/package/winget/manifest/Tgstation.Server.installer.yaml index 5d0ed20324..e19c13c9fb 100644 --- a/build/package/winget/manifest/Tgstation.Server.installer.yaml +++ b/build/package/winget/manifest/Tgstation.Server.installer.yaml @@ -24,4 +24,4 @@ Installers: Publisher: /tg/station 13 ReleaseDate: 2023-06-24 # Do not change. Set before publish by push_manifest.ps1 ManifestType: installer -ManifestVersion: 1.5.0 +ManifestVersion: 1.6.0 diff --git a/build/package/winget/manifest/Tgstation.Server.locale.en-US.yaml b/build/package/winget/manifest/Tgstation.Server.locale.en-US.yaml index 54556c5d92..a08b33f691 100644 --- a/build/package/winget/manifest/Tgstation.Server.locale.en-US.yaml +++ b/build/package/winget/manifest/Tgstation.Server.locale.en-US.yaml @@ -18,4 +18,4 @@ Documentations: ReleaseNotesUrl: https://github.com/tgstation/tgstation-server/releases/tag/tgstation-server-v0.22.475 PurchaseUrl: https://github.com/sponsors/Cyberboss ManifestType: defaultLocale -ManifestVersion: 1.5.0 +ManifestVersion: 1.6.0 diff --git a/build/package/winget/manifest/Tgstation.Server.yaml b/build/package/winget/manifest/Tgstation.Server.yaml index 09972f148d..2c28c483b4 100644 --- a/build/package/winget/manifest/Tgstation.Server.yaml +++ b/build/package/winget/manifest/Tgstation.Server.yaml @@ -5,4 +5,4 @@ PackageIdentifier: Tgstation.Server PackageVersion: 0.22.475 # Do not change. Set before publish by push_manifest.ps1 DefaultLocale: en-US ManifestType: version -ManifestVersion: 1.5.0 +ManifestVersion: 1.6.0 diff --git a/tools/Tgstation.Server.ReleaseNotes/Program.cs b/tools/Tgstation.Server.ReleaseNotes/Program.cs index b8d1e86f79..6130bbc17a 100644 --- a/tools/Tgstation.Server.ReleaseNotes/Program.cs +++ b/tools/Tgstation.Server.ReleaseNotes/Program.cs @@ -788,7 +788,7 @@ namespace Tgstation.Server.ReleaseNotes var versionsPropertyGroup = project.Elements().First(x => x.Name == xmlNamespace + "PropertyGroup"); var coreVersion = Version.Parse(versionsPropertyGroup.Element(xmlNamespace + "TgsCoreVersion").Value); - const string BodyForPRSha = "184dccf9de3e3e4abe289a46648af42017ad6f09"; + const string BodyForPRSha = "bec143988b4b8ddeb586ed97aaf0647803110d98"; var prBody = $@"# Automated Pull Request This pull request was generated by our [deployment pipeline]({actionUrl}) as a result of the release of [tgstation-server-v{coreVersion}](https://github.com/tgstation/tgstation-server/releases/tag/tgstation-server-v{coreVersion}). Validation was performed as part of the process. @@ -803,7 +803,7 @@ The user account that created this pull request is available to correct any issu - Validation is performed as a prerequisite to deployment. - [x] Have you tested your manifest locally with `winget install --manifest `? - Manifest installation and uninstallation is performed as a prerequisite to deployment. -- [x] Does your manifest conform to the [1.5 schema](https://github.com/microsoft/winget-pkgs/tree/master/doc/manifest/schema/1.5.0)? +- [x] Does your manifest conform to the [1.6 schema](https://github.com/microsoft/winget-pkgs/tree/master/doc/manifest/schema/1.6.0)? ###### Microsoft Reviewers: [Open in CodeFlow](https://microsoft.github.io/open-pr/?codeflow=https://github.com/microsoft/winget-pkgs/pull/$PR_NUMBER_SUBST$)"; From e9463c192f46bb16fb749ed46a1c046edb343698 Mon Sep 17 00:00:00 2001 From: Jordan Dominion Date: Mon, 19 Feb 2024 17:19:08 -0500 Subject: [PATCH 10/22] Update to Wix 4.0.4 --- build/package/winget/.config/dotnet-tools.json | 2 +- .../Tgstation.Server.Host.Service.Wix.Bundle.wixproj | 6 +++--- .../Tgstation.Server.Host.Service.Wix.Extensions.csproj | 2 +- .../Tgstation.Server.Host.Service.Wix.wixproj | 6 +++--- 4 files changed, 8 insertions(+), 8 deletions(-) diff --git a/build/package/winget/.config/dotnet-tools.json b/build/package/winget/.config/dotnet-tools.json index 14178b0a98..b94eed2541 100644 --- a/build/package/winget/.config/dotnet-tools.json +++ b/build/package/winget/.config/dotnet-tools.json @@ -3,7 +3,7 @@ "isRoot": true, "tools": { "wix": { - "version": "4.0.2", + "version": "4.0.4", "commands": [ "wix" ] diff --git a/build/package/winget/Tgstation.Server.Host.Service.Wix.Bundle/Tgstation.Server.Host.Service.Wix.Bundle.wixproj b/build/package/winget/Tgstation.Server.Host.Service.Wix.Bundle/Tgstation.Server.Host.Service.Wix.Bundle.wixproj index 827b40f156..af8ce69036 100644 --- a/build/package/winget/Tgstation.Server.Host.Service.Wix.Bundle/Tgstation.Server.Host.Service.Wix.Bundle.wixproj +++ b/build/package/winget/Tgstation.Server.Host.Service.Wix.Bundle/Tgstation.Server.Host.Service.Wix.Bundle.wixproj @@ -1,4 +1,4 @@ - + ProductVersion=$(TgsCoreVersion);NetMajorVersion=$(TgsNetMajorVersion);DotnetRedistUrl=$(TgsDotnetRedistUrl);MariaDBRedistUrl=https://github.com/tgstation/tgstation-server/releases/download/tgstation-server-v$(TgsCoreVersion)/mariadb-$(TgsMariaDBRedistVersion)-winx64.msi @@ -24,8 +24,8 @@ - - + + diff --git a/build/package/winget/Tgstation.Server.Host.Service.Wix.Extensions/Tgstation.Server.Host.Service.Wix.Extensions.csproj b/build/package/winget/Tgstation.Server.Host.Service.Wix.Extensions/Tgstation.Server.Host.Service.Wix.Extensions.csproj index c7edd3c403..85cbe7e02f 100644 --- a/build/package/winget/Tgstation.Server.Host.Service.Wix.Extensions/Tgstation.Server.Host.Service.Wix.Extensions.csproj +++ b/build/package/winget/Tgstation.Server.Host.Service.Wix.Extensions/Tgstation.Server.Host.Service.Wix.Extensions.csproj @@ -7,7 +7,7 @@ - + diff --git a/build/package/winget/Tgstation.Server.Host.Service.Wix/Tgstation.Server.Host.Service.Wix.wixproj b/build/package/winget/Tgstation.Server.Host.Service.Wix/Tgstation.Server.Host.Service.Wix.wixproj index 14838b0531..d8ada09e94 100644 --- a/build/package/winget/Tgstation.Server.Host.Service.Wix/Tgstation.Server.Host.Service.Wix.wixproj +++ b/build/package/winget/Tgstation.Server.Host.Service.Wix/Tgstation.Server.Host.Service.Wix.wixproj @@ -1,4 +1,4 @@ - + ProductVersion=$(TgsCoreVersion) @@ -25,8 +25,8 @@ - - + + From f13502f2acc2aee1458b693af15cc9890b934a70 Mon Sep 17 00:00:00 2001 From: Jordan Dominion Date: Mon, 19 Feb 2024 17:28:49 -0500 Subject: [PATCH 11/22] Note about where to store instances on Linux --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 2efb724ec3..085f8c05db 100644 --- a/README.md +++ b/README.md @@ -127,7 +127,7 @@ sudo dpkg --add-architecture i386 \ && sudo systemctl start tgstation-server ``` -The service will execute as the newly created user: `tgstation-server`. +The service will execute as the newly created user: `tgstation-server`. You should, ideally, store your instances somewhere under `/home/tgstation-server`. ##### Manual Setup From 9747be0b59c5311108adc0c32ea133eba0d6e035 Mon Sep 17 00:00:00 2001 From: Jordan Dominion Date: Mon, 19 Feb 2024 17:56:04 -0500 Subject: [PATCH 12/22] Workaround for Debian stupidity See https://bugs.debian.org/cgi-bin/bugreport.cgi?bug=679746 --- build/package/deb/debian/postinst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/build/package/deb/debian/postinst b/build/package/deb/debian/postinst index 2b98bd204c..28f147a87e 100755 --- a/build/package/deb/debian/postinst +++ b/build/package/deb/debian/postinst @@ -1,7 +1,7 @@ #!/bin/sh -e if [ -z "$2" ]; then - adduser --system tgstation-server + adduser --system --home /home/tgstation-server tgstation-server mkdir -m 754 -p /var/log/tgstation-server chown -R tgstation-server /etc/tgstation-server chown -R tgstation-server /opt/tgstation-server/lib From 9243f70203acfdc410ab32f741cd290dd1ae5c21 Mon Sep 17 00:00:00 2001 From: Jordan Dominion Date: Mon, 19 Feb 2024 18:27:46 -0500 Subject: [PATCH 13/22] `trusted.txt` is only a Windows thing --- .../Components/Engine/ByondInstallerBase.cs | 88 ++++--------------- .../Components/Engine/PosixByondInstaller.cs | 10 +++ .../Engine/WindowsByondInstaller.cs | 66 ++++++++++++++ 3 files changed, 93 insertions(+), 71 deletions(-) diff --git a/src/Tgstation.Server.Host/Components/Engine/ByondInstallerBase.cs b/src/Tgstation.Server.Host/Components/Engine/ByondInstallerBase.cs index 81a68470e7..c7c73e3a23 100644 --- a/src/Tgstation.Server.Host/Components/Engine/ByondInstallerBase.cs +++ b/src/Tgstation.Server.Host/Components/Engine/ByondInstallerBase.cs @@ -1,6 +1,7 @@ using System; +using System.Collections.Generic; using System.Globalization; -using System.Text; +using System.Linq; using System.Threading; using System.Threading.Tasks; @@ -9,7 +10,6 @@ using Microsoft.Extensions.Logging; using Tgstation.Server.Api.Models; using Tgstation.Server.Host.IO; using Tgstation.Server.Host.Jobs; -using Tgstation.Server.Host.Utils; namespace Tgstation.Server.Host.Components.Engine { @@ -23,31 +23,21 @@ namespace Tgstation.Server.Host.Components.Engine /// protected const string ByondBinPath = "byond/bin"; + /// + /// The path to the cfg directory. + /// + protected const string CfgDirectoryName = "cfg"; + /// /// The name of BYOND's cache directory. /// const string CacheDirectoryName = "cache"; - /// - /// The path to the cfg directory. - /// - const string CfgDirectoryName = "cfg"; - - /// - /// The name of the list of trusted .dmb files in the user's BYOND cfg directory. - /// - const string TrustedDmbFileName = "trusted.txt"; - /// /// The first of BYOND that supports the '-map-threads' parameter on DreamDaemon. /// static readonly Version MapThreadsVersion = new(515, 1609); - /// - /// for writing to files in the user's BYOND directory. - /// - static readonly SemaphoreSlim UserFilesSemaphore = new(1); - /// protected override EngineType TargetEngineType => EngineType.Byond; @@ -144,18 +134,10 @@ namespace Tgstation.Server.Host.Components.Engine localCfgDirectory, cancellationToken); - // Delete trusted.txt so it doesn't grow too large - var trustedFilePath = - IOManager.ConcatPath( - localCfgDirectory, - TrustedDmbFileName); + var additionalCleanTasks = AdditionalCacheCleanFilePaths(localCfgDirectory) + .Select(path => IOManager.DeleteFile(path, cancellationToken)); - Logger.LogTrace("Deleting trusted .dmbs file {trustedFilePath}", trustedFilePath); - var trustedDmbDeleteTask = IOManager.DeleteFile( - trustedFilePath, - cancellationToken); - - await Task.WhenAll(cacheCleanTask, cfgCreateTask, trustedDmbDeleteTask); + await Task.WhenAll(cacheCleanTask, cfgCreateTask, Task.WhenAll(additionalCleanTasks)); } catch (Exception ex) when (ex is not OperationCanceledException) { @@ -163,49 +145,6 @@ namespace Tgstation.Server.Host.Components.Engine } } - /// - public override async ValueTask TrustDmbPath(EngineVersion version, string fullDmbPath, CancellationToken cancellationToken) - { - ArgumentNullException.ThrowIfNull(version); - ArgumentNullException.ThrowIfNull(fullDmbPath); - - var byondDir = PathToUserFolder; - var cfgDir = IOManager.ConcatPath( - byondDir, - CfgDirectoryName); - var trustedFilePath = IOManager.ConcatPath( - cfgDir, - TrustedDmbFileName); - - Logger.LogDebug("Adding .dmb ({dmbPath}) to {trustedFilePath}", fullDmbPath, trustedFilePath); - - using (await SemaphoreSlimContext.Lock(UserFilesSemaphore, cancellationToken)) - { - string trustedFileText; - var filePreviouslyExisted = await IOManager.FileExists(trustedFilePath, cancellationToken); - if (filePreviouslyExisted) - { - var trustedFileBytes = await IOManager.ReadAllBytes(trustedFilePath, cancellationToken); - trustedFileText = Encoding.UTF8.GetString(trustedFileBytes); - trustedFileText = $"{trustedFileText.Trim()}{Environment.NewLine}"; - } - else - trustedFileText = String.Empty; - - if (trustedFileText.Contains(fullDmbPath, StringComparison.Ordinal)) - return; - - trustedFileText = $"{trustedFileText}{fullDmbPath}{Environment.NewLine}"; - - var newTrustedFileBytes = Encoding.UTF8.GetBytes(trustedFileText); - - if (!filePreviouslyExisted) - await IOManager.CreateDirectory(cfgDir, cancellationToken); - - await IOManager.WriteAllBytes(trustedFilePath, newTrustedFileBytes, cancellationToken); - } - } - /// public override async ValueTask DownloadVersion(EngineVersion version, JobProgressReporter? progressReporter, CancellationToken cancellationToken) { @@ -240,6 +179,13 @@ namespace Tgstation.Server.Host.Components.Engine /// The file name of the DreamDaemon executable. protected abstract string GetDreamDaemonName(Version byondVersion, out bool supportsCli); + /// + /// List off additional file paths in the to delete. + /// + /// The full path to the relevant . + /// An of paths in to clean. + protected virtual IEnumerable AdditionalCacheCleanFilePaths(string configDirectory) => Enumerable.Empty(); + /// /// Create a pointing to the location of the download for a given . /// diff --git a/src/Tgstation.Server.Host/Components/Engine/PosixByondInstaller.cs b/src/Tgstation.Server.Host/Components/Engine/PosixByondInstaller.cs index ef019aa354..0723c96f5b 100644 --- a/src/Tgstation.Server.Host/Components/Engine/PosixByondInstaller.cs +++ b/src/Tgstation.Server.Host/Components/Engine/PosixByondInstaller.cs @@ -119,6 +119,16 @@ namespace Tgstation.Server.Host.Components.Engine return ValueTask.CompletedTask; } + /// + public override ValueTask TrustDmbPath(EngineVersion version, string fullDmbPath, CancellationToken cancellationToken) + { + ArgumentNullException.ThrowIfNull(version); + ArgumentNullException.ThrowIfNull(fullDmbPath); + + Logger.LogTrace("No need to trust .dmb path \"{path}\" on POSIX", fullDmbPath); + return ValueTask.CompletedTask; + } + /// protected override string GetDreamDaemonName(Version byondVersion, out bool supportsCli) { diff --git a/src/Tgstation.Server.Host/Components/Engine/WindowsByondInstaller.cs b/src/Tgstation.Server.Host/Components/Engine/WindowsByondInstaller.cs index 3b6790cee1..1534bfc500 100644 --- a/src/Tgstation.Server.Host/Components/Engine/WindowsByondInstaller.cs +++ b/src/Tgstation.Server.Host/Components/Engine/WindowsByondInstaller.cs @@ -47,6 +47,16 @@ namespace Tgstation.Server.Host.Components.Engine /// const string TgsFirewalledDDFile = "TGSFirewalledDD"; + /// + /// The name of the list of trusted .dmb files in the user's BYOND cfg directory. + /// + const string TrustedDmbFileName = "trusted.txt"; + + /// + /// for writing to files in the user's BYOND directory. + /// + static readonly SemaphoreSlim UserFilesSemaphore = new(1, 1); + /// /// The first version of BYOND to ship with dd.exe on the Windows build. /// @@ -168,6 +178,49 @@ namespace Tgstation.Server.Host.Components.Engine await AddDreamDaemonToFirewall(version, path, true, cancellationToken); } + /// + public override async ValueTask TrustDmbPath(EngineVersion version, string fullDmbPath, CancellationToken cancellationToken) + { + ArgumentNullException.ThrowIfNull(version); + ArgumentNullException.ThrowIfNull(fullDmbPath); + + var byondDir = PathToUserFolder; + var cfgDir = IOManager.ConcatPath( + byondDir, + CfgDirectoryName); + var trustedFilePath = IOManager.ConcatPath( + cfgDir, + TrustedDmbFileName); + + Logger.LogDebug("Adding .dmb ({dmbPath}) to {trustedFilePath}", fullDmbPath, trustedFilePath); + + using (await SemaphoreSlimContext.Lock(UserFilesSemaphore, cancellationToken)) + { + string trustedFileText; + var filePreviouslyExisted = await IOManager.FileExists(trustedFilePath, cancellationToken); + if (filePreviouslyExisted) + { + var trustedFileBytes = await IOManager.ReadAllBytes(trustedFilePath, cancellationToken); + trustedFileText = Encoding.UTF8.GetString(trustedFileBytes); + trustedFileText = $"{trustedFileText.Trim()}{Environment.NewLine}"; + } + else + trustedFileText = String.Empty; + + if (trustedFileText.Contains(fullDmbPath, StringComparison.Ordinal)) + return; + + trustedFileText = $"{trustedFileText}{fullDmbPath}{Environment.NewLine}"; + + var newTrustedFileBytes = Encoding.UTF8.GetBytes(trustedFileText); + + if (!filePreviouslyExisted) + await IOManager.CreateDirectory(cfgDir, cancellationToken); + + await IOManager.WriteAllBytes(trustedFilePath, newTrustedFileBytes, cancellationToken); + } + } + /// protected override string GetDreamDaemonName(Version byondVersion, out bool supportsCli) { @@ -175,6 +228,19 @@ namespace Tgstation.Server.Host.Components.Engine return supportsCli ? "dd.exe" : "dreamdaemon.exe"; } + /// + protected override IEnumerable AdditionalCacheCleanFilePaths(string configDirectory) + { + // Delete trusted.txt so it doesn't grow too large + var trustedFilePath = + IOManager.ConcatPath( + configDirectory, + TrustedDmbFileName); + + Logger.LogTrace("Deleting trusted .dmbs file {trustedFilePath}", trustedFilePath); + yield return trustedFilePath; + } + /// /// Creates the BYOND cfg file that prevents the trusted mode dialog from appearing when launching DreamDaemon. /// From e59e2ff337ae5ea903be0483c2d1be3738147db2 Mon Sep 17 00:00:00 2001 From: Jordan Dominion Date: Mon, 19 Feb 2024 18:29:47 -0500 Subject: [PATCH 14/22] Fix path to BYOND $HOME folder on Linux --- .../Components/Engine/PosixByondInstaller.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Tgstation.Server.Host/Components/Engine/PosixByondInstaller.cs b/src/Tgstation.Server.Host/Components/Engine/PosixByondInstaller.cs index 0723c96f5b..fff62b3b3f 100644 --- a/src/Tgstation.Server.Host/Components/Engine/PosixByondInstaller.cs +++ b/src/Tgstation.Server.Host/Components/Engine/PosixByondInstaller.cs @@ -67,7 +67,7 @@ namespace Tgstation.Server.Host.Components.Engine Environment.GetFolderPath( Environment.SpecialFolder.UserProfile, Environment.SpecialFolderOption.DoNotVerify), - "./byond/cache")); + "./.byond/cache")); } /// From ba41f41defa88acf9bec137ec98cd2e4282c7b45 Mon Sep 17 00:00:00 2001 From: Jordan Dominion Date: Mon, 19 Feb 2024 18:32:08 -0500 Subject: [PATCH 15/22] Bump to latest webpanel version --- build/WebpanelVersion.props | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/build/WebpanelVersion.props b/build/WebpanelVersion.props index abaf23b16d..4fed7127de 100644 --- a/build/WebpanelVersion.props +++ b/build/WebpanelVersion.props @@ -1,6 +1,6 @@ - 5.5.0 + 5.5.1 From 8bfccae678994613df1a0fa06288517a5b84c4ba Mon Sep 17 00:00:00 2001 From: Jordan Dominion Date: Mon, 19 Feb 2024 18:54:26 -0500 Subject: [PATCH 16/22] Version bump to 6.3.0 --- build/Version.props | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/build/Version.props b/build/Version.props index d5b05ce320..2f96d66f55 100644 --- a/build/Version.props +++ b/build/Version.props @@ -3,7 +3,7 @@ - 6.2.0 + 6.3.0 5.1.0 10.1.0 7.0.0 From 054c72cc1d1290e55a146c4ff85f09834efcc934 Mon Sep 17 00:00:00 2001 From: Jordan Dominion Date: Tue, 20 Feb 2024 08:43:41 -0500 Subject: [PATCH 17/22] Some debug logging --- src/DMAPI/tgs/v5/bridge.dm | 3 +++ tests/DMAPI/BasicOperation/Test.dm | 3 +++ 2 files changed, 6 insertions(+) diff --git a/src/DMAPI/tgs/v5/bridge.dm b/src/DMAPI/tgs/v5/bridge.dm index 763ab3e02b..0c5e701a32 100644 --- a/src/DMAPI/tgs/v5/bridge.dm +++ b/src/DMAPI/tgs/v5/bridge.dm @@ -77,8 +77,11 @@ /datum/tgs_api/v5/proc/PerformBridgeRequest(bridge_request) WaitForReattach(FALSE) + TGS_DEBUG_LOG("Bridge request start") // This is an infinite sleep until we get a response var/export_response = world.Export(bridge_request) + TGS_DEBUG_LOG("Bridge request complete") + if(!export_response) TGS_ERROR_LOG("Failed bridge request: [bridge_request]") return diff --git a/tests/DMAPI/BasicOperation/Test.dm b/tests/DMAPI/BasicOperation/Test.dm index aa0455b659..e0cc3d2ba4 100644 --- a/tests/DMAPI/BasicOperation/Test.dm +++ b/tests/DMAPI/BasicOperation/Test.dm @@ -15,10 +15,12 @@ sleep(50) world.TgsTargetedChatBroadcast("Sample admin-only message", TRUE) + world.log << "params check" var/list/world_params = world.params if(!("test" in world_params) || world_params["test"] != "bababooey") FailTest("Expected parameter test=bababooey but did not receive", "test_fail_reason.txt") + world.log << "file check 1" fdel("test_event_output.txt") var/test_data = "nwfiuurhfu" world.TgsTriggerEvent("test_event", list(test_data), TRUE) @@ -29,6 +31,7 @@ if(test_contents != test_data) FailTest("Expected test_event_output.txt to contain [test_data] here. Got [test_contents]", "test_fail_reason.txt") + world.log << "file check 1" fdel("test_event_output.txt") world.TgsTriggerEvent("test_event", list("asdf"), FALSE) if(fexists("test_event_output.txt")) From a43216210f1c7ecec78d25f062f52bfa6ffc02c9 Mon Sep 17 00:00:00 2001 From: Jordan Dominion Date: Tue, 20 Feb 2024 18:25:30 -0500 Subject: [PATCH 18/22] Fix race condition with DMAPI custom event creation/completion --- src/DMAPI/tgs/v5/api.dm | 5 ++--- src/DMAPI/tgs/v5/topic.dm | 2 +- 2 files changed, 3 insertions(+), 4 deletions(-) diff --git a/src/DMAPI/tgs/v5/api.dm b/src/DMAPI/tgs/v5/api.dm index ffdde1ae20..9b64931f8f 100644 --- a/src/DMAPI/tgs/v5/api.dm +++ b/src/DMAPI/tgs/v5/api.dm @@ -276,13 +276,12 @@ return TRUE TGS_DEBUG_LOG("Waiting for completion of event ID: [event_id]") - pending_events[event_id] = TRUE - do + while(!pending_events[event_id]) sleep(world.tick_lag) - while(pending_events[event_id]) TGS_DEBUG_LOG("Completed wait on event ID: [event_id]") + pending_events -= event_id return TRUE diff --git a/src/DMAPI/tgs/v5/topic.dm b/src/DMAPI/tgs/v5/topic.dm index b13f83f82c..e66edc2720 100644 --- a/src/DMAPI/tgs/v5/topic.dm +++ b/src/DMAPI/tgs/v5/topic.dm @@ -285,7 +285,7 @@ return TopicResponse("Invalid or missing [DMAPI5_EVENT_ID]") TGS_DEBUG_LOG("Completing event ID [event_id]...") - pending_events -= event_id + pending_events[event_id] = TRUE return TopicResponse() return TopicResponse("Unknown command: [command]") From 9d381df6323a04a0d83cf61db600667f9278715a Mon Sep 17 00:00:00 2001 From: Jordan Dominion Date: Wed, 21 Feb 2024 08:38:28 -0500 Subject: [PATCH 19/22] Fix errors with dotnet dump. Add new error code --- build/Version.props | 6 ++--- src/Tgstation.Server.Api/Models/ErrorCode.cs | 6 +++++ .../System/DotnetDumpService.cs | 24 +++++++++++++------ 3 files changed, 26 insertions(+), 10 deletions(-) diff --git a/build/Version.props b/build/Version.props index 2f96d66f55..c11adcf272 100644 --- a/build/Version.props +++ b/build/Version.props @@ -5,10 +5,10 @@ 6.3.0 5.1.0 - 10.1.0 + 10.2.0 7.0.0 - 13.1.0 - 15.1.0 + 13.2.0 + 15.2.0 7.1.0 5.9.0 1.4.1 diff --git a/src/Tgstation.Server.Api/Models/ErrorCode.cs b/src/Tgstation.Server.Api/Models/ErrorCode.cs index 28f39a7638..b75c837585 100644 --- a/src/Tgstation.Server.Api/Models/ErrorCode.cs +++ b/src/Tgstation.Server.Api/Models/ErrorCode.cs @@ -646,5 +646,11 @@ namespace Tgstation.Server.Api.Models /// [Description("The specified OpenDream version is too old!")] OpenDreamTooOld, + + /// + /// Failed dotnet diagnostics dump. + /// + [Description("Could not create dump as dotnet diagnostics threw an exception!")] + DotnetDiagnosticsFailure, } } diff --git a/src/Tgstation.Server.Host/System/DotnetDumpService.cs b/src/Tgstation.Server.Host/System/DotnetDumpService.cs index f813b44230..de24e5387a 100644 --- a/src/Tgstation.Server.Host/System/DotnetDumpService.cs +++ b/src/Tgstation.Server.Host/System/DotnetDumpService.cs @@ -5,6 +5,9 @@ using System.Threading.Tasks; using Microsoft.Diagnostics.NETCore.Client; using Microsoft.Extensions.Logging; +using Tgstation.Server.Api.Models; +using Tgstation.Server.Host.Jobs; + namespace Tgstation.Server.Host.System { /// @@ -42,13 +45,20 @@ namespace Tgstation.Server.Host.System var pid = process.Id; logger.LogDebug("dotnet-dump requested for PID {pid}...", pid); var client = new DiagnosticsClient(pid); - await client.WriteDumpAsync( - minidump - ? DumpType.Normal - : DumpType.Full, - outputFile, - false, - cts.Token); + try + { + await client.WriteDumpAsync( + minidump + ? DumpType.Normal + : DumpType.Full, + outputFile, + false, + cts.Token); + } + catch (Exception ex) + { + throw new JobException(ErrorCode.GCoreFailure, ex); + } } } } From a91cb620f42073713914da19e37c76ae8469e9ed Mon Sep 17 00:00:00 2001 From: Jordan Dominion Date: Wed, 21 Feb 2024 08:48:06 -0500 Subject: [PATCH 20/22] Workaround for slow GitHub runners --- tests/DMAPI/BasicOperation/Test.dm | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/tests/DMAPI/BasicOperation/Test.dm b/tests/DMAPI/BasicOperation/Test.dm index e0cc3d2ba4..6eecddfaa7 100644 --- a/tests/DMAPI/BasicOperation/Test.dm +++ b/tests/DMAPI/BasicOperation/Test.dm @@ -33,8 +33,11 @@ world.log << "file check 1" fdel("test_event_output.txt") + + var/start_time = world.timeofday world.TgsTriggerEvent("test_event", list("asdf"), FALSE) - if(fexists("test_event_output.txt")) + + if((world.timeofday - start_time) <= 50 && fexists("test_event_output.txt")) FailTest("Expected test_event_output.txt to not exist here", "test_fail_reason.txt") world.log << "sleep2" From bab8f93154ad394636511c1a7500af2a1136f50c Mon Sep 17 00:00:00 2001 From: Jordan Dominion Date: Wed, 21 Feb 2024 17:49:16 -0500 Subject: [PATCH 21/22] Use a better sleep command on Windows Works in GitHub Actions --- tests/DMAPI/BasicOperation/test_event-qwer.bat | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/tests/DMAPI/BasicOperation/test_event-qwer.bat b/tests/DMAPI/BasicOperation/test_event-qwer.bat index ecbce0d0af..7eb6a4dff9 100644 --- a/tests/DMAPI/BasicOperation/test_event-qwer.bat +++ b/tests/DMAPI/BasicOperation/test_event-qwer.bat @@ -1,7 +1,5 @@ echo "Running test_event script" - -rem mingw has their own /usr/bin/timeout -C:\Windows\system32\timeout.exe /t 5 +C:\Windows\System32\WindowsPowerShell\v1.0\powershell.exe -ExecutionPolicy Bypass -Command "Start-Sleep -Seconds 5" cd %1 cd tests\DMAPI\BasicOperation echo %2 > test_event_output.txt From fd2e875d1b430e2446d583525d102639f6523cdd Mon Sep 17 00:00:00 2001 From: Jordan Dominion Date: Thu, 22 Feb 2024 19:09:24 -0500 Subject: [PATCH 22/22] Bump webpanel version to 5.6.0 --- build/WebpanelVersion.props | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/build/WebpanelVersion.props b/build/WebpanelVersion.props index 4fed7127de..1d18faa135 100644 --- a/build/WebpanelVersion.props +++ b/build/WebpanelVersion.props @@ -1,6 +1,6 @@ - 5.5.1 + 5.6.0