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}"