diff --git a/build/Version.props b/build/Version.props
index 26f8009ffe..eca4607a50 100644
--- a/build/Version.props
+++ b/build/Version.props
@@ -8,8 +8,8 @@
9.9.0
10.3.0
11.3.0
- 6.3.1
- 5.5.0
+ 6.4.0
+ 5.6.0
1.2.1
1.2.1
1.0.1
diff --git a/src/DMAPI/tgs.dm b/src/DMAPI/tgs.dm
index 80df9d1d54..855fdc4285 100644
--- a/src/DMAPI/tgs.dm
+++ b/src/DMAPI/tgs.dm
@@ -1,6 +1,6 @@
// tgstation-server DMAPI
-#define TGS_DMAPI_VERSION "6.3.1"
+#define TGS_DMAPI_VERSION "6.4.0"
// All functions and datums outside this document are subject to change with any version and should not be relied on.
diff --git a/src/DMAPI/tgs/includes.dm b/src/DMAPI/tgs/includes.dm
index bf91aa6006..23b714f9d0 100644
--- a/src/DMAPI/tgs/includes.dm
+++ b/src/DMAPI/tgs/includes.dm
@@ -14,6 +14,8 @@
#include "v5\_defines.dm"
#include "v5\api.dm"
#include "v5\bridge.dm"
+#include "v5\chunking.dm"
#include "v5\commands.dm"
#include "v5\serializers.dm"
+#include "v5\topic.dm"
#include "v5\undefs.dm"
diff --git a/src/DMAPI/tgs/v5/README.md b/src/DMAPI/tgs/v5/README.md
index 7c65ecf776..a8a0c748e7 100644
--- a/src/DMAPI/tgs/v5/README.md
+++ b/src/DMAPI/tgs/v5/README.md
@@ -5,7 +5,9 @@ This DMAPI implements bridge requests using HTTP GET requests to TGS. It has no
- [__interop_version.dm](./__interop_version.dm) contains the version of the API used between the DMAPI and TGS.
- [_defines.dm](./_defines.dm) contains constant definitions.
- [api.dm](./api.dm) contains the bulk of the API code.
-- [bridge.dm](./bridge.dm) contains the functions related to making bridge requests.
+- [bridge.dm](./bridge.dm) contains functions related to making bridge requests.
+- [chunking.dm](./chunking.dm) contains common function for splitting large raw data sets into chunks BYOND can natively process.
- [commands.dm](./commands.dm) contains functions relating to `/datum/tgs_chat_command`s.
- [serializers.dm](./serializers.dm) contains function to help convert interop `/datum`s into a JSON encodable `list()` format.
+- [topic.dm](./topic.dm) contains functions related to processing topic requests.
- [undefs.dm](./undefs.dm) Undoes the work of `_defines.dm`.
diff --git a/src/DMAPI/tgs/v5/__interop_version.dm b/src/DMAPI/tgs/v5/__interop_version.dm
index d0ac7e92ea..6ef7c86ef7 100644
--- a/src/DMAPI/tgs/v5/__interop_version.dm
+++ b/src/DMAPI/tgs/v5/__interop_version.dm
@@ -1 +1 @@
-"5.5.0"
+"5.6.0"
diff --git a/src/DMAPI/tgs/v5/_defines.dm b/src/DMAPI/tgs/v5/_defines.dm
index 580c40bca0..a3f949081f 100644
--- a/src/DMAPI/tgs/v5/_defines.dm
+++ b/src/DMAPI/tgs/v5/_defines.dm
@@ -5,6 +5,8 @@
#define DMAPI5_TOPIC_DATA "tgs_data"
#define DMAPI5_BRIDGE_REQUEST_LIMIT 8198
+#define DMAPI5_TOPIC_REQUEST_LIMIT 65529
+#define DMAPI5_TOPIC_RESPONSE_LIMIT 65528
#define DMAPI5_BRIDGE_COMMAND_PORT_UPDATE 0
#define DMAPI5_BRIDGE_COMMAND_STARTUP 1
@@ -17,6 +19,14 @@
#define DMAPI5_PARAMETER_ACCESS_IDENTIFIER "accessIdentifier"
#define DMAPI5_PARAMETER_CUSTOM_COMMANDS "customCommands"
+#define DMAPI5_CHUNK "chunk"
+#define DMAPI5_CHUNK_PAYLOAD "payload"
+#define DMAPI5_CHUNK_TOTAL "totalChunks"
+#define DMAPI5_CHUNK_SEQUENCE_ID "sequenceId"
+#define DMAPI5_CHUNK_PAYLOAD_ID "payloadId"
+
+#define DMAPI5_MISSING_CHUNKS "missingChunks"
+
#define DMAPI5_RESPONSE_ERROR_MESSAGE "errorMessage"
#define DMAPI5_BRIDGE_PARAMETER_COMMAND_TYPE "commandType"
@@ -27,7 +37,6 @@
#define DMAPI5_BRIDGE_RESPONSE_NEW_PORT "newPort"
#define DMAPI5_BRIDGE_RESPONSE_RUNTIME_INFORMATION "runtimeInformation"
-#define DMAPI5_BRIDGE_RESPONSE_MISSING_CHUNKS "missingChunks"
#define DMAPI5_CHAT_MESSAGE_CHANNEL_IDS "channelIds"
@@ -68,6 +77,8 @@
#define DMAPI5_TOPIC_COMMAND_SERVER_PORT_UPDATE 6
#define DMAPI5_TOPIC_COMMAND_HEARTBEAT 7
#define DMAPI5_TOPIC_COMMAND_WATCHDOG_REATTACH 8
+#define DMAPI5_TOPIC_COMMAND_SEND_CHUNK 9
+#define DMAPI5_TOPIC_COMMAND_RECEIVE_CHUNK 10
#define DMAPI5_TOPIC_PARAMETER_COMMAND_TYPE "commandType"
#define DMAPI5_TOPIC_PARAMETER_CHAT_COMMAND "chatCommand"
diff --git a/src/DMAPI/tgs/v5/api.dm b/src/DMAPI/tgs/v5/api.dm
index 15d5bb03e2..91f4d73990 100644
--- a/src/DMAPI/tgs/v5/api.dm
+++ b/src/DMAPI/tgs/v5/api.dm
@@ -16,7 +16,9 @@
var/list/chat_channels
var/initialized = FALSE
+
var/chunked_requests = 0
+ var/list/chunked_topics = list()
/datum/tgs_api/v5/ApiVersion()
return new /datum/tgs_version(
@@ -98,12 +100,6 @@
/datum/tgs_api/v5/OnInitializationComplete()
Bridge(DMAPI5_BRIDGE_COMMAND_PRIME)
-/datum/tgs_api/v5/proc/TopicResponse(error_message = null)
- var/list/response = list()
- if(error_message)
- response[DMAPI5_RESPONSE_ERROR_MESSAGE] = error_message
- return response
-
/datum/tgs_api/v5/OnTopic(T)
RequireInitialBridgeResponse()
var/list/params = params2list(T)
@@ -111,139 +107,11 @@
if(!json)
return FALSE // continue to /world/Topic
- var/list/topic_parameters = json_decode(json)
- if(!topic_parameters)
- return TopicResponse("Invalid topic parameters json!");
-
if(!initialized)
- TGS_WARNING_LOG("Missed topic due to not being initialized: [T]")
+ TGS_WARNING_LOG("Missed topic due to not being initialized: [json]")
return TRUE // too early to handle, but it's still our responsibility
- var/their_sCK = topic_parameters[DMAPI5_PARAMETER_ACCESS_IDENTIFIER]
- if(their_sCK != access_identifier)
- return TopicResponse("Failed to decode [DMAPI5_PARAMETER_ACCESS_IDENTIFIER] from: [json]!");
-
- var/command = topic_parameters[DMAPI5_TOPIC_PARAMETER_COMMAND_TYPE]
- if(!isnum(command))
- return TopicResponse("Failed to decode [DMAPI5_TOPIC_PARAMETER_COMMAND_TYPE] from: [json]!")
-
- var/result = ProcessTopicCommand(command, topic_parameters)
- if(!length(result))
- return "{}" // quirk of json_encode is an empty list returns "[]"
-
- return json_encode(result)
-
-/datum/tgs_api/v5/proc/ProcessTopicCommand(command, list/topic_parameters)
- switch(command)
- if(DMAPI5_TOPIC_COMMAND_CHAT_COMMAND)
- intercepted_message_queue = list()
- var/list/result = HandleCustomCommand(topic_parameters[DMAPI5_TOPIC_PARAMETER_CHAT_COMMAND])
- if(!result)
- result = TopicResponse("Error running chat command!")
- result[DMAPI5_TOPIC_RESPONSE_CHAT_RESPONSES] = intercepted_message_queue
- intercepted_message_queue = null
- return result
- if(DMAPI5_TOPIC_COMMAND_EVENT_NOTIFICATION)
- intercepted_message_queue = list()
- var/list/event_notification = topic_parameters[DMAPI5_TOPIC_PARAMETER_EVENT_NOTIFICATION]
- if(!istype(event_notification))
- return TopicResponse("Invalid [DMAPI5_TOPIC_PARAMETER_EVENT_NOTIFICATION]!")
-
- var/event_type = event_notification[DMAPI5_EVENT_NOTIFICATION_TYPE]
- if(!isnum(event_type))
- return TopicResponse("Invalid or missing [DMAPI5_EVENT_NOTIFICATION_TYPE]!")
-
- var/list/event_parameters = event_notification[DMAPI5_EVENT_NOTIFICATION_PARAMETERS]
- if(event_parameters && !istype(event_parameters))
- return TopicResponse("Invalid or missing [DMAPI5_EVENT_NOTIFICATION_PARAMETERS]!")
-
- var/list/event_call = list(event_type)
- if(event_parameters)
- event_call += event_parameters
-
- if(event_handler != null)
- event_handler.HandleEvent(arglist(event_call))
-
- var/list/response = TopicResponse()
- response[DMAPI5_TOPIC_RESPONSE_CHAT_RESPONSES] = intercepted_message_queue
- intercepted_message_queue = null
- return response
- if(DMAPI5_TOPIC_COMMAND_CHANGE_PORT)
- var/new_port = topic_parameters[DMAPI5_TOPIC_PARAMETER_NEW_PORT]
- if (!isnum(new_port) || !(new_port > 0))
- return TopicResponse("Invalid or missing [DMAPI5_TOPIC_PARAMETER_NEW_PORT]]")
-
- if(event_handler != null)
- event_handler.HandleEvent(TGS_EVENT_PORT_SWAP, new_port)
-
- //the topic still completes, miraculously
- //I honestly didn't believe byond could do it without exploding
- if(!world.OpenPort(new_port))
- return TopicResponse("Port change failed!")
-
- return TopicResponse()
- if(DMAPI5_TOPIC_COMMAND_CHANGE_REBOOT_STATE)
- var/new_reboot_mode = topic_parameters[DMAPI5_TOPIC_PARAMETER_NEW_REBOOT_STATE]
- if(!isnum(new_reboot_mode))
- return TopicResponse("Invalid or missing [DMAPI5_TOPIC_PARAMETER_NEW_REBOOT_STATE]!")
-
- if(event_handler != null)
- event_handler.HandleEvent(TGS_EVENT_REBOOT_MODE_CHANGE, reboot_mode, new_reboot_mode)
-
- reboot_mode = new_reboot_mode
- return TopicResponse()
- if(DMAPI5_TOPIC_COMMAND_INSTANCE_RENAMED)
- var/new_instance_name = topic_parameters[DMAPI5_TOPIC_PARAMETER_NEW_INSTANCE_NAME]
- if(!istext(new_instance_name))
- return TopicResponse("Invalid or missing [DMAPI5_TOPIC_PARAMETER_NEW_INSTANCE_NAME]!")
-
- if(event_handler != null)
- event_handler.HandleEvent(TGS_EVENT_INSTANCE_RENAMED, new_instance_name)
-
- instance_name = new_instance_name
- return TopicResponse()
- if(DMAPI5_TOPIC_COMMAND_CHAT_CHANNELS_UPDATE)
- var/list/chat_update_json = topic_parameters[DMAPI5_TOPIC_PARAMETER_CHAT_UPDATE]
- if(!istype(chat_update_json))
- return TopicResponse("Invalid or missing [DMAPI5_TOPIC_PARAMETER_CHAT_UPDATE]!")
-
- DecodeChannels(chat_update_json)
- return TopicResponse()
- if(DMAPI5_TOPIC_COMMAND_SERVER_PORT_UPDATE)
- var/new_port = topic_parameters[DMAPI5_TOPIC_PARAMETER_NEW_PORT]
- if (!isnum(new_port) || !(new_port > 0))
- return TopicResponse("Invalid or missing [DMAPI5_TOPIC_PARAMETER_NEW_PORT]]")
-
- server_port = new_port
- return TopicResponse()
- if(DMAPI5_TOPIC_COMMAND_HEARTBEAT)
- return TopicResponse()
- if(DMAPI5_TOPIC_COMMAND_WATCHDOG_REATTACH)
- var/new_port = topic_parameters[DMAPI5_TOPIC_PARAMETER_NEW_PORT]
- var/error_message = null
- if (new_port != null)
- if (!isnum(new_port) || !(new_port > 0))
- error_message = "Invalid [DMAPI5_TOPIC_PARAMETER_NEW_PORT]]"
- else
- server_port = new_port
-
- var/new_version_string = topic_parameters[DMAPI5_TOPIC_PARAMETER_NEW_SERVER_VERSION]
- if (!istext(new_version_string))
- if(error_message != null)
- error_message += ", "
- error_message += "Invalid or missing [DMAPI5_TOPIC_PARAMETER_NEW_SERVER_VERSION]]"
- else
- var/datum/tgs_version/new_version = new(new_version_string)
- if (event_handler)
- event_handler.HandleEvent(TGS_EVENT_WATCHDOG_REATTACH, new_version)
-
- version = new_version
-
- var/list/reattach_response = TopicResponse(error_message)
- reattach_response[DMAPI5_PARAMETER_CUSTOM_COMMANDS] = ListCustomCommands()
- return reattach_response
-
- return TopicResponse("Unknown command: [command]")
+ return ProcessTopicJson(json, TRUE)
/datum/tgs_api/v5/OnReboot()
var/list/result = Bridge(DMAPI5_BRIDGE_COMMAND_REBOOT)
diff --git a/src/DMAPI/tgs/v5/bridge.dm b/src/DMAPI/tgs/v5/bridge.dm
index d2e7bbc395..df4825f89e 100644
--- a/src/DMAPI/tgs/v5/bridge.dm
+++ b/src/DMAPI/tgs/v5/bridge.dm
@@ -10,29 +10,8 @@
var/payload_id = ++chunked_requests
var/raw_data = CreateBridgeData(command, data, FALSE)
- var/data_length = length(raw_data)
- var/chunk_count
- var/list/chunk_requests
- for(chunk_count = 2; !chunk_requests; ++chunk_count);
- var/max_chunk_size = -round(-(data_length / chunk_count))
- if(max_chunk_size > DMAPI5_BRIDGE_REQUEST_LIMIT)
- continue
-
- chunk_requests = list()
- for(var/i in 1 to chunk_count)
- var/startIndex = 1 + ((i - 1) * max_chunk_size)
- var/endIndex = min(1 + (i * max_chunk_size), data_length + 1)
- var/chunk_payload = copytext(raw_data, startIndex, endIndex)
- var/list/chunk = list("payloadId" = payload_id, "sequenceId" = (i - 1), "totalChunks" = chunk_count, payload = chunk_payload)
-
- var/chunk_request = CreateBridgeRequest(DMAPI5_BRIDGE_COMMAND_CHUNK, list("chunk" = chunk))
- if(length(chunk_request) > DMAPI5_BRIDGE_REQUEST_LIMIT)
- // Screwed by url encoding, no way to preempt it though
- chunk_requests = null
- break
-
- chunk_requests += chunk_request
+ var/list/chunk_requests = GenerateChunks(raw_data, TRUE)
var/list/response
for(var/bridge_request in chunk_requests)
@@ -41,17 +20,17 @@
// Abort
return
- var/list/missing_sequence_ids = response[DMAPI5_BRIDGE_RESPONSE_MISSING_CHUNKS]
+ var/list/missing_sequence_ids = response[DMAPI5_MISSING_CHUNKS]
if(length(missing_sequence_ids))
do
- TGS_WARNING_LOG("Server is missing some chunks of payload [payload_id]! Sending missing chunks...")
+ TGS_WARNING_LOG("Server is still missing some chunks of bridge P[payload_id]! Sending missing chunks...")
if(!istype(missing_sequence_ids))
- TGS_ERROR_LOG("Did not receive a list() for [DMAPI5_BRIDGE_RESPONSE_MISSING_CHUNKS]!")
+ TGS_ERROR_LOG("Did not receive a list() for [DMAPI5_MISSING_CHUNKS]!")
return
for(var/missing_sequence_id in missing_sequence_ids)
if(!isnum(missing_sequence_id))
- TGS_ERROR_LOG("Did not receive a num in [DMAPI5_BRIDGE_RESPONSE_MISSING_CHUNKS]!")
+ TGS_ERROR_LOG("Did not receive a num in [DMAPI5_MISSING_CHUNKS]!")
return
var/missing_chunk_request = chunk_requests[missing_sequence_id + 1]
@@ -60,7 +39,7 @@
// Abort
return
- missing_sequence_ids = response[DMAPI5_BRIDGE_RESPONSE_MISSING_CHUNKS]
+ missing_sequence_ids = response[DMAPI5_MISSING_CHUNKS]
while(length(missing_sequence_ids))
return response
diff --git a/src/DMAPI/tgs/v5/chunking.dm b/src/DMAPI/tgs/v5/chunking.dm
new file mode 100644
index 0000000000..af4cd6cc80
--- /dev/null
+++ b/src/DMAPI/tgs/v5/chunking.dm
@@ -0,0 +1,43 @@
+/datum/tgs_api/v5/proc/GenerateChunks(payload, bridge)
+ var/limit = bridge ? DMAPI5_BRIDGE_REQUEST_LIMIT : DMAPI5_TOPIC_RESPONSE_LIMIT
+
+ var/payload_id = ++chunked_requests
+ var/data_length = length(payload)
+
+ var/chunk_count
+ var/list/chunk_requests
+ for(chunk_count = 2; !chunk_requests; ++chunk_count);
+ var/max_chunk_size = -round(-(data_length / chunk_count))
+ if(max_chunk_size > limit)
+ continue
+
+ chunk_requests = list()
+ for(var/i in 1 to chunk_count)
+ var/start_index = 1 + ((i - 1) * max_chunk_size)
+ if (start_index > data_length)
+ break
+
+ var/end_index = min(1 + (i * max_chunk_size), data_length + 1)
+
+ var/chunk_payload = copytext(payload, start_index, end_index)
+
+ // sequence IDs in interop chunking are always zero indexed
+ var/list/chunk = list(DMAPI5_CHUNK_PAYLOAD_ID = payload_id, DMAPI5_CHUNK_SEQUENCE_ID = (i - 1), DMAPI5_CHUNK_TOTAL = chunk_count, DMAPI5_CHUNK_PAYLOAD = chunk_payload)
+
+ var/chunk_request = list(DMAPI5_CHUNK = chunk)
+ var/chunk_length
+ if(bridge)
+ chunk_request = CreateBridgeRequest(DMAPI5_BRIDGE_COMMAND_CHUNK, chunk_request)
+ chunk_length = length(chunk_request)
+ else
+ chunk_request = list(chunk_request) // wrap for adding to list
+ chunk_length = length(json_encode(chunk_request))
+
+ if(chunk_length > limit)
+ // Screwed by encoding, no way to preempt it though
+ chunk_requests = null
+ break
+
+ chunk_requests += chunk_request
+
+ return chunk_requests
diff --git a/src/DMAPI/tgs/v5/topic.dm b/src/DMAPI/tgs/v5/topic.dm
new file mode 100644
index 0000000000..c54ee810f7
--- /dev/null
+++ b/src/DMAPI/tgs/v5/topic.dm
@@ -0,0 +1,254 @@
+/datum/tgs_api/v5/proc/TopicResponse(error_message = null)
+ var/list/response = list()
+ if(error_message)
+ response[DMAPI5_RESPONSE_ERROR_MESSAGE] = error_message
+ return response
+
+/datum/tgs_api/v5/proc/ProcessTopicJson(json, check_access_identifier)
+ var/list/result = ProcessRawTopic(json, check_access_identifier)
+ if(!result)
+ result = TopicResponse("Runtime error!")
+ else if(!length(result))
+ return "{}" // quirk of json_encode is an empty list returns "[]"
+
+ var/response_json = json_encode(result)
+ if(length(response_json) > DMAPI5_TOPIC_RESPONSE_LIMIT)
+ // cache response chunks and send the first
+ var/list/chunks = GenerateChunks(response_json, FALSE)
+ var/payload_id = chunks[1][DMAPI5_CHUNK][DMAPI5_CHUNK_PAYLOAD_ID]
+ var/cache_key = ResponseTopicChunkCacheKey(payload_id)
+
+ chunked_topics[cache_key] = chunks
+
+ response_json = json_encode(chunks[1])
+
+ return response_json
+
+/datum/tgs_api/v5/proc/ProcessRawTopic(json, check_access_identifier)
+ var/list/topic_parameters = json_decode(json)
+ if(!topic_parameters)
+ return TopicResponse("Invalid topic parameters json: [json]!");
+
+ var/their_sCK = topic_parameters[DMAPI5_PARAMETER_ACCESS_IDENTIFIER]
+ if(check_access_identifier && their_sCK != access_identifier)
+ return TopicResponse("Failed to decode [DMAPI5_PARAMETER_ACCESS_IDENTIFIER]!")
+
+ var/command = topic_parameters[DMAPI5_TOPIC_PARAMETER_COMMAND_TYPE]
+ if(!isnum(command))
+ return TopicResponse("Failed to decode [DMAPI5_TOPIC_PARAMETER_COMMAND_TYPE]!")
+
+ return ProcessTopicCommand(command, topic_parameters)
+
+/datum/tgs_api/v5/proc/ResponseTopicChunkCacheKey(payload_id)
+ return "response[payload_id]"
+
+/datum/tgs_api/v5/proc/ProcessTopicCommand(command, list/topic_parameters)
+ switch(command)
+
+ if(DMAPI5_TOPIC_COMMAND_CHAT_COMMAND)
+ intercepted_message_queue = list()
+ var/list/result = HandleCustomCommand(topic_parameters[DMAPI5_TOPIC_PARAMETER_CHAT_COMMAND])
+ if(!result)
+ result = TopicResponse("Error running chat command!")
+ result[DMAPI5_TOPIC_RESPONSE_CHAT_RESPONSES] = intercepted_message_queue
+ intercepted_message_queue = null
+ return result
+
+ if(DMAPI5_TOPIC_COMMAND_EVENT_NOTIFICATION)
+ intercepted_message_queue = list()
+ var/list/event_notification = topic_parameters[DMAPI5_TOPIC_PARAMETER_EVENT_NOTIFICATION]
+ if(!istype(event_notification))
+ return TopicResponse("Invalid [DMAPI5_TOPIC_PARAMETER_EVENT_NOTIFICATION]!")
+
+ var/event_type = event_notification[DMAPI5_EVENT_NOTIFICATION_TYPE]
+ if(!isnum(event_type))
+ return TopicResponse("Invalid or missing [DMAPI5_EVENT_NOTIFICATION_TYPE]!")
+
+ var/list/event_parameters = event_notification[DMAPI5_EVENT_NOTIFICATION_PARAMETERS]
+ if(event_parameters && !istype(event_parameters))
+ return TopicResponse("Invalid or missing [DMAPI5_EVENT_NOTIFICATION_PARAMETERS]!")
+
+ var/list/event_call = list(event_type)
+ if(event_parameters)
+ event_call += event_parameters
+
+ if(event_handler != null)
+ event_handler.HandleEvent(arglist(event_call))
+
+ var/list/response = TopicResponse()
+ response[DMAPI5_TOPIC_RESPONSE_CHAT_RESPONSES] = intercepted_message_queue
+ intercepted_message_queue = null
+ return response
+
+ if(DMAPI5_TOPIC_COMMAND_CHANGE_PORT)
+ var/new_port = topic_parameters[DMAPI5_TOPIC_PARAMETER_NEW_PORT]
+ if (!isnum(new_port) || !(new_port > 0))
+ return TopicResponse("Invalid or missing [DMAPI5_TOPIC_PARAMETER_NEW_PORT]]")
+
+ if(event_handler != null)
+ event_handler.HandleEvent(TGS_EVENT_PORT_SWAP, new_port)
+
+ //the topic still completes, miraculously
+ //I honestly didn't believe byond could do it without exploding
+ if(!world.OpenPort(new_port))
+ return TopicResponse("Port change failed!")
+
+ return TopicResponse()
+
+ if(DMAPI5_TOPIC_COMMAND_CHANGE_REBOOT_STATE)
+ var/new_reboot_mode = topic_parameters[DMAPI5_TOPIC_PARAMETER_NEW_REBOOT_STATE]
+ if(!isnum(new_reboot_mode))
+ return TopicResponse("Invalid or missing [DMAPI5_TOPIC_PARAMETER_NEW_REBOOT_STATE]!")
+
+ if(event_handler != null)
+ event_handler.HandleEvent(TGS_EVENT_REBOOT_MODE_CHANGE, reboot_mode, new_reboot_mode)
+
+ reboot_mode = new_reboot_mode
+ return TopicResponse()
+
+ if(DMAPI5_TOPIC_COMMAND_INSTANCE_RENAMED)
+ var/new_instance_name = topic_parameters[DMAPI5_TOPIC_PARAMETER_NEW_INSTANCE_NAME]
+ if(!istext(new_instance_name))
+ return TopicResponse("Invalid or missing [DMAPI5_TOPIC_PARAMETER_NEW_INSTANCE_NAME]!")
+
+ if(event_handler != null)
+ event_handler.HandleEvent(TGS_EVENT_INSTANCE_RENAMED, new_instance_name)
+
+ instance_name = new_instance_name
+ return TopicResponse()
+
+ if(DMAPI5_TOPIC_COMMAND_CHAT_CHANNELS_UPDATE)
+ var/list/chat_update_json = topic_parameters[DMAPI5_TOPIC_PARAMETER_CHAT_UPDATE]
+ if(!istype(chat_update_json))
+ return TopicResponse("Invalid or missing [DMAPI5_TOPIC_PARAMETER_CHAT_UPDATE]!")
+
+ DecodeChannels(chat_update_json)
+ return TopicResponse()
+
+ if(DMAPI5_TOPIC_COMMAND_SERVER_PORT_UPDATE)
+ var/new_port = topic_parameters[DMAPI5_TOPIC_PARAMETER_NEW_PORT]
+ if (!isnum(new_port) || !(new_port > 0))
+ return TopicResponse("Invalid or missing [DMAPI5_TOPIC_PARAMETER_NEW_PORT]]")
+
+ server_port = new_port
+ return TopicResponse()
+
+ if(DMAPI5_TOPIC_COMMAND_HEARTBEAT)
+ return TopicResponse()
+
+ if(DMAPI5_TOPIC_COMMAND_WATCHDOG_REATTACH)
+ var/new_port = topic_parameters[DMAPI5_TOPIC_PARAMETER_NEW_PORT]
+ var/error_message = null
+ if (new_port != null)
+ if (!isnum(new_port) || !(new_port > 0))
+ error_message = "Invalid [DMAPI5_TOPIC_PARAMETER_NEW_PORT]]"
+ else
+ server_port = new_port
+
+ var/new_version_string = topic_parameters[DMAPI5_TOPIC_PARAMETER_NEW_SERVER_VERSION]
+ if (!istext(new_version_string))
+ if(error_message != null)
+ error_message += ", "
+ error_message += "Invalid or missing [DMAPI5_TOPIC_PARAMETER_NEW_SERVER_VERSION]]"
+ else
+ var/datum/tgs_version/new_version = new(new_version_string)
+ if (event_handler)
+ event_handler.HandleEvent(TGS_EVENT_WATCHDOG_REATTACH, new_version)
+
+ version = new_version
+
+ var/list/reattach_response = TopicResponse(error_message)
+ reattach_response[DMAPI5_PARAMETER_CUSTOM_COMMANDS] = ListCustomCommands()
+ return reattach_response
+
+ if(DMAPI5_TOPIC_COMMAND_SEND_CHUNK)
+ var/list/chunk = topic_parameters[DMAPI5_CHUNK]
+ if(!istype(chunk))
+ return TopicResponse("Invalid [DMAPI5_CHUNK]!")
+
+ var/payload_id = chunk[DMAPI5_CHUNK_PAYLOAD_ID]
+ if(!isnum(payload_id))
+ return TopicResponse("[DMAPI5_CHUNK_PAYLOAD_ID] is not a number!")
+
+ // Always updated the highest known payload ID
+ chunked_requests = max(chunked_requests, payload_id)
+
+ var/sequence_id = chunk[DMAPI5_CHUNK_SEQUENCE_ID]
+ if(!isnum(sequence_id))
+ return TopicResponse("[DMAPI5_CHUNK_SEQUENCE_ID] is not a number!")
+
+ var/total_chunks = chunk[DMAPI5_CHUNK_TOTAL]
+ if(!isnum(total_chunks))
+ return TopicResponse("[DMAPI5_CHUNK_TOTAL] is not a number!")
+
+ if(total_chunks == 0)
+ return TopicResponse("[DMAPI5_CHUNK_TOTAL] is zero!")
+
+ var/payload = chunk[DMAPI5_CHUNK_PAYLOAD]
+ if(!istext(payload))
+ return TopicResponse("[DMAPI5_CHUNK_PAYLOAD] is not text!")
+
+ var/cache_key = "request[payload_id]"
+ var/payloads = chunked_topics[cache_key]
+
+ if(!payloads)
+ payloads = new /list(total_chunks)
+ chunked_topics[cache_key] = payloads
+
+ if(total_chunks != length(payloads))
+ chunked_topics -= cache_key
+ return TopicResponse("Received differing total chunks for same [DMAPI5_CHUNK_PAYLOAD_ID]! Invalidating [DMAPI5_CHUNK_PAYLOAD_ID]!")
+
+ var/pre_existing_chunk = payloads[sequence_id + 1]
+ if(pre_existing_chunk && pre_existing_chunk != payload)
+ chunked_topics -= cache_key
+ return TopicResponse("Received differing payload for same [DMAPI5_CHUNK_SEQUENCE_ID]! Invalidating [DMAPI5_CHUNK_PAYLOAD_ID]!")
+
+ payloads[sequence_id + 1] = payload
+
+ var/list/missing_sequence_ids = list()
+ for(var/i in 1 to total_chunks)
+ if(!payloads[i])
+ missing_sequence_ids += i - 1
+
+ if(length(missing_sequence_ids))
+ return list(DMAPI5_MISSING_CHUNKS = missing_sequence_ids)
+
+ chunked_topics -= cache_key
+ var/full_json = jointext(payloads, "")
+
+ return ProcessRawTopic(full_json, FALSE)
+
+ if(DMAPI5_TOPIC_COMMAND_RECEIVE_CHUNK)
+ var/payload_id = topic_parameters[DMAPI5_CHUNK_PAYLOAD_ID]
+ if(!isnum(payload_id))
+ return TopicResponse("[DMAPI5_CHUNK_PAYLOAD_ID] is not a number!")
+
+ // Always updated the highest known payload ID
+ chunked_requests = max(chunked_requests, payload_id)
+
+ var/list/missing_chunks = topic_parameters[DMAPI5_MISSING_CHUNKS]
+ if(!istype(missing_chunks) || !length(missing_chunks))
+ return TopicResponse("Missing or empty [DMAPI5_MISSING_CHUNKS]!")
+
+ var/sequence_id_to_send = missing_chunks[1]
+ if(!isnum(sequence_id_to_send))
+ return TopicResponse("[DMAPI5_MISSING_CHUNKS] contained a non-number!")
+
+ var/cache_key = ResponseTopicChunkCacheKey(payload_id)
+ var/list/chunks = chunked_topics[cache_key]
+ if(!chunks)
+ return TopicResponse("Unknown response chunk set: P[payload_id]!")
+
+ // sequence IDs in interop chunking are always zero indexed
+ var/chunk_to_send = chunks[sequence_id_to_send + 1]
+ if(!chunk_to_send)
+ return TopicResponse("Sequence ID [sequence_id_to_send] is not present in response chunk P[payload_id]!")
+
+ if(length(missing_chunks) == 1)
+ // sending last chunk, purge the cache
+ chunked_topics -= cache_key
+
+ return chunk_to_send
+
+ return TopicResponse("Unknown command: [command]")
diff --git a/src/DMAPI/tgs/v5/undefs.dm b/src/DMAPI/tgs/v5/undefs.dm
index 6209945372..2e3b7ae771 100644
--- a/src/DMAPI/tgs/v5/undefs.dm
+++ b/src/DMAPI/tgs/v5/undefs.dm
@@ -4,6 +4,10 @@
#undef DMAPI5_BRIDGE_DATA
#undef DMAPI5_TOPIC_DATA
+#undef DMAPI5_BRIDGE_REQUEST_LIMIT
+#undef DMAPI5_TOPIC_REQUEST_LIMIT
+#undef DMAPI5_TOPIC_RESPONSE_LIMIT
+
#undef DMAPI5_BRIDGE_COMMAND_PORT_UPDATE
#undef DMAPI5_BRIDGE_COMMAND_STARTUP
#undef DMAPI5_BRIDGE_COMMAND_PRIME
@@ -14,6 +18,14 @@
#undef DMAPI5_PARAMETER_ACCESS_IDENTIFIER
#undef DMAPI5_PARAMETER_CUSTOM_COMMANDS
+#undef DMAPI5_CHUNK
+#undef DMAPI5_CHUNK_PAYLOAD
+#undef DMAPI5_CHUNK_TOTAL
+#undef DMAPI5_CHUNK_SEQUENCE_ID
+#undef DMAPI5_CHUNK_PAYLOAD_ID
+
+#undef DMAPI5_MISSING_CHUNKS
+
#undef DMAPI5_RESPONSE_ERROR_MESSAGE
#undef DMAPI5_BRIDGE_PARAMETER_COMMAND_TYPE
diff --git a/src/Tgstation.Server.Host/Components/Interop/Bridge/BridgeRequestChunker.cs b/src/Tgstation.Server.Host/Components/Interop/Bridge/BridgeRequestChunker.cs
deleted file mode 100644
index 9ce6d7b443..0000000000
--- a/src/Tgstation.Server.Host/Components/Interop/Bridge/BridgeRequestChunker.cs
+++ /dev/null
@@ -1,131 +0,0 @@
-using System;
-using System.Collections.Generic;
-using System.Threading;
-using System.Threading.Tasks;
-
-using Microsoft.Extensions.Logging;
-
-using Newtonsoft.Json;
-
-namespace Tgstation.Server.Host.Components.Interop.Bridge
-{
- ///
- /// Processes chunked bridge requests.
- ///
- abstract class BridgeRequestChunker : IBridgeDispatcher
- {
- ///
- /// The cache of chunked bridge requests.
- ///
- /// If the DMAPI is erroring, this can present a memory leak. Worth expiring entries if they aren't completed after some minutes.
- readonly Dictionary> bridgeChunks;
-
- ///
- /// The for the .
- ///
- protected ILogger Logger { get; }
-
- ///
- /// Initializes a new instance of the class.
- ///
- /// The value of .
- protected BridgeRequestChunker(ILogger logger)
- {
- Logger = logger ?? throw new ArgumentNullException(nameof(logger));
- bridgeChunks = new Dictionary>();
- }
-
- ///
- public abstract Task ProcessBridgeRequest(BridgeParameters parameters, CancellationToken cancellationToken);
-
- ///
- /// Process a given .
- ///
- /// The .
- /// The for the operation.
- /// A resulting in the for the chunked request.
- protected async Task ProcessBridgeChunk(ChunkData chunk, CancellationToken cancellationToken)
- {
- if (chunk == null)
- return BridgeError("Missing chunk!");
-
- ChunkedRequestInfo requestInfo;
- string[] payloads;
- lock (bridgeChunks)
- {
- if (!bridgeChunks.TryGetValue(chunk.PayloadId, out var tuple))
- {
- // first time seeing this payload
- if (chunk.TotalChunks == 0)
- return BridgeError("Receieved chunked request with 0 totalChunks!");
-
- tuple = Tuple.Create(chunk, new string[chunk.TotalChunks]);
- bridgeChunks.Add(chunk.PayloadId, tuple);
- }
-
- requestInfo = tuple.Item1;
- payloads = tuple.Item2;
-
- Logger.LogTrace("Received bridge payload chunk P{payloadId}: {sequenceId}/{totalChunks}", requestInfo.PayloadId, chunk.SequenceId + 1, requestInfo.TotalChunks);
-
- if (chunk.TotalChunks != requestInfo.TotalChunks)
- {
- bridgeChunks.Remove(requestInfo.PayloadId);
- return BridgeError("Received differing total chunks for same payloadId! Invalidating payloadId!");
- }
-
- if (payloads[chunk.SequenceId] != null && payloads[chunk.SequenceId] != chunk.Payload)
- {
- bridgeChunks.Remove(requestInfo.PayloadId);
- return BridgeError("Received differing payload for same sequenceId! Invalidating payloadId!");
- }
-
- payloads[chunk.SequenceId] = chunk.Payload;
- var missingPayloads = new List();
- for (uint i = 0; i < payloads.Length; ++i)
- if (payloads[i] == null)
- missingPayloads.Add(i);
-
- if (missingPayloads.Count > 0)
- return new BridgeResponse
- {
- MissingChunks = missingPayloads,
- };
-
- Logger.LogTrace("Received all bridge chunks for P{payloadId}, processing request...", requestInfo.PayloadId);
- bridgeChunks.Remove(requestInfo.PayloadId);
- }
-
- BridgeParameters completedRequest;
- var fullRequestJson = String.Concat(payloads);
- try
- {
- completedRequest = JsonConvert.DeserializeObject(fullRequestJson, DMApiConstants.SerializerSettings);
- }
- catch (Exception ex)
- {
- Logger.LogWarning(ex, "Bad chunked bridge request for payload {payloadId}!", requestInfo.PayloadId);
- return BridgeError("Chunked request completed with bad JSON!", false);
- }
-
- return await ProcessBridgeRequest(completedRequest, cancellationToken);
- }
-
- ///
- /// Create and logs an errored .
- ///
- /// The error message.
- /// If should be written to the .
- /// A new errored .
- protected BridgeResponse BridgeError(string message, bool log = true)
- {
- if (log)
- Logger.LogWarning("Bridge processing error: {errorMessage}", message);
-
- return new BridgeResponse
- {
- ErrorMessage = message,
- };
- }
- }
-}
diff --git a/src/Tgstation.Server.Host/Components/Interop/Bridge/BridgeResponse.cs b/src/Tgstation.Server.Host/Components/Interop/Bridge/BridgeResponse.cs
index 68c1adbea7..f7f358d612 100644
--- a/src/Tgstation.Server.Host/Components/Interop/Bridge/BridgeResponse.cs
+++ b/src/Tgstation.Server.Host/Components/Interop/Bridge/BridgeResponse.cs
@@ -5,7 +5,7 @@ namespace Tgstation.Server.Host.Components.Interop.Bridge
///
/// A response to a bridge request.
///
- public class BridgeResponse : DMApiResponse
+ public class BridgeResponse : DMApiResponse, IMissingPayloadsCommunication
{
///
/// The new port for requests.
diff --git a/src/Tgstation.Server.Host/Components/Interop/Bridge/ChunkData.cs b/src/Tgstation.Server.Host/Components/Interop/Bridge/ChunkData.cs
deleted file mode 100644
index e0c0204d2c..0000000000
--- a/src/Tgstation.Server.Host/Components/Interop/Bridge/ChunkData.cs
+++ /dev/null
@@ -1,18 +0,0 @@
-namespace Tgstation.Server.Host.Components.Interop.Bridge
-{
- ///
- /// A packet of a split serialized set of .
- ///
- public sealed class ChunkData : ChunkedRequestInfo
- {
- ///
- /// The sequence ID of the chunk.
- ///
- public uint SequenceId { get; set; }
-
- ///
- /// The partial JSON payload of the chunk.
- ///
- public string Payload { get; set; }
- }
-}
diff --git a/src/Tgstation.Server.Host/Components/Interop/Bridge/ChunkedRequestInfo.cs b/src/Tgstation.Server.Host/Components/Interop/Bridge/ChunkedRequestInfo.cs
deleted file mode 100644
index 1246f37a19..0000000000
--- a/src/Tgstation.Server.Host/Components/Interop/Bridge/ChunkedRequestInfo.cs
+++ /dev/null
@@ -1,18 +0,0 @@
-namespace Tgstation.Server.Host.Components.Interop.Bridge
-{
- ///
- /// Information about a chunked bridge request.
- ///
- public abstract class ChunkedRequestInfo
- {
- ///
- /// The ID of the full request to differentiate different chunkings.
- ///
- public uint PayloadId { get; set; }
-
- ///
- /// The total number of chunks in the request.
- ///
- public uint TotalChunks { get; set; }
- }
-}
diff --git a/src/Tgstation.Server.Host/Components/Interop/ChunkData.cs b/src/Tgstation.Server.Host/Components/Interop/ChunkData.cs
new file mode 100644
index 0000000000..ee6122d764
--- /dev/null
+++ b/src/Tgstation.Server.Host/Components/Interop/ChunkData.cs
@@ -0,0 +1,19 @@
+namespace Tgstation.Server.Host.Components.Interop
+{
+ ///
+ /// A packet of a split serialized set of data.
+ ///
+ public sealed class ChunkData : ChunkSetInfo
+ {
+ ///
+ /// The sequence ID of the chunk.
+ ///
+ /// Always zero indexed. Nullable to prevent default value omission.
+ public uint? SequenceId { get; set; }
+
+ ///
+ /// The partial JSON payload of the chunk.
+ ///
+ public string Payload { get; set; }
+ }
+}
diff --git a/src/Tgstation.Server.Host/Components/Interop/ChunkSetInfo.cs b/src/Tgstation.Server.Host/Components/Interop/ChunkSetInfo.cs
new file mode 100644
index 0000000000..6d6a23c4ae
--- /dev/null
+++ b/src/Tgstation.Server.Host/Components/Interop/ChunkSetInfo.cs
@@ -0,0 +1,16 @@
+namespace Tgstation.Server.Host.Components.Interop
+{
+ ///
+ /// Information about a chunked bridge request.
+ ///
+ public abstract class ChunkSetInfo : IChunkPayloadId
+ {
+ ///
+ public uint? PayloadId { get; set; }
+
+ ///
+ /// The total number of chunks in the request.
+ ///
+ public uint TotalChunks { get; set; }
+ }
+}
diff --git a/src/Tgstation.Server.Host/Components/Interop/Chunker.cs b/src/Tgstation.Server.Host/Components/Interop/Chunker.cs
new file mode 100644
index 0000000000..7363f3aee8
--- /dev/null
+++ b/src/Tgstation.Server.Host/Components/Interop/Chunker.cs
@@ -0,0 +1,146 @@
+using System;
+using System.Collections.Generic;
+using System.Threading;
+using System.Threading.Tasks;
+
+using Microsoft.Extensions.Logging;
+
+using Newtonsoft.Json;
+
+namespace Tgstation.Server.Host.Components.Interop
+{
+ ///
+ /// Class that deserializes chunked interop payloads.
+ ///
+ abstract class Chunker
+ {
+ ///
+ /// Gets a payload ID for use in a new .
+ ///
+ protected uint NextPayloadId
+ {
+ get
+ {
+ // 0 is special, since BYOND doesn't use it we reserve it for if we have to send a chunked topic request immediately upon reattaching
+ // Otherwise, jump ahead a bunch compared to what was last used/seen, so we don't accidentally clash
+ lock (chunkSets)
+ return highestSeenPayloadId == 0 ? 0 : highestSeenPayloadId + 20;
+ }
+ }
+
+ ///
+ /// The cache of chunked communications.
+ ///
+ /// If the DMAPI is erroring, this can present a memory leak. Worth expiring entries if they aren't completed after some minutes.
+ readonly Dictionary> chunkSets;
+
+ ///
+ /// The highest payload ID value seen.
+ ///
+ uint highestSeenPayloadId;
+
+ ///
+ /// The for the .
+ ///
+ protected ILogger Logger { get; }
+
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ /// The value of .
+ protected Chunker(ILogger logger)
+ {
+ Logger = logger ?? throw new ArgumentNullException(nameof(logger));
+ chunkSets = new Dictionary>();
+ }
+
+ ///
+ /// Process a given .
+ ///
+ /// The of communication that was chunked.
+ /// The of expected.
+ /// The callback that receives the completed .
+ /// The callback that generates a for a given error.
+ /// The .
+ /// The for the operation.
+ /// A resulting in the for the chunked request.
+ protected async Task ProcessChunk(
+ Func> completionCallback,
+ Func chunkErrorCallback,
+ ChunkData chunk,
+ CancellationToken cancellationToken)
+ where TResponse : IMissingPayloadsCommunication, new()
+ {
+ if (chunk == null)
+ return chunkErrorCallback("Missing chunk!");
+
+ if (!chunk.PayloadId.HasValue)
+ return chunkErrorCallback("Missing chunk payloadId!");
+
+ if (!chunk.SequenceId.HasValue)
+ return chunkErrorCallback("Missing chunk sequenceId!");
+
+ ChunkSetInfo requestInfo;
+ string[] payloads;
+ lock (chunkSets)
+ {
+ highestSeenPayloadId = Math.Max(chunk.PayloadId.Value, highestSeenPayloadId);
+ if (!chunkSets.TryGetValue(chunk.PayloadId.Value, out var tuple))
+ {
+ // first time seeing this payload
+ if (chunk.TotalChunks == 0)
+ return chunkErrorCallback("Receieved chunked request with 0 totalChunks!");
+
+ tuple = Tuple.Create(chunk, new string[chunk.TotalChunks]);
+ chunkSets.Add(chunk.PayloadId.Value, tuple);
+ }
+
+ requestInfo = tuple.Item1;
+ payloads = tuple.Item2;
+
+ Logger.LogTrace("Received chunk P{payloadId}: {sequenceId}/{totalChunks}", requestInfo.PayloadId, chunk.SequenceId + 1, requestInfo.TotalChunks);
+
+ if (chunk.TotalChunks != requestInfo.TotalChunks)
+ {
+ chunkSets.Remove(requestInfo.PayloadId.Value);
+ return chunkErrorCallback("Received differing total chunks for same payloadId! Invalidating payloadId!");
+ }
+
+ if (payloads[chunk.SequenceId.Value] != null && payloads[chunk.SequenceId.Value] != chunk.Payload)
+ {
+ chunkSets.Remove(requestInfo.PayloadId.Value);
+ return chunkErrorCallback("Received differing payload for same sequenceId! Invalidating payloadId!");
+ }
+
+ payloads[chunk.SequenceId.Value] = chunk.Payload;
+ var missingPayloads = new List();
+ for (var i = 0U; i < payloads.Length; ++i)
+ if (payloads[i] == null)
+ missingPayloads.Add(i);
+
+ if (missingPayloads.Count > 0)
+ return new TResponse
+ {
+ MissingChunks = missingPayloads,
+ };
+
+ Logger.LogTrace("Received all chunks for P{payloadId}, processing request...", requestInfo.PayloadId);
+ chunkSets.Remove(requestInfo.PayloadId.Value);
+ }
+
+ TCommnication completedCommunication;
+ var fullCommunicationJson = String.Concat(payloads);
+ try
+ {
+ completedCommunication = JsonConvert.DeserializeObject(fullCommunicationJson, DMApiConstants.SerializerSettings);
+ }
+ catch (Exception ex)
+ {
+ Logger.LogDebug(ex, "Bad chunked communication for payload {payloadId}!", requestInfo.PayloadId);
+ return chunkErrorCallback("Chunked request completed with bad JSON!");
+ }
+
+ return await completionCallback(completedCommunication, cancellationToken);
+ }
+ }
+}
diff --git a/src/Tgstation.Server.Host/Components/Interop/IChunkPayloadId.cs b/src/Tgstation.Server.Host/Components/Interop/IChunkPayloadId.cs
new file mode 100644
index 0000000000..173a854581
--- /dev/null
+++ b/src/Tgstation.Server.Host/Components/Interop/IChunkPayloadId.cs
@@ -0,0 +1,14 @@
+namespace Tgstation.Server.Host.Components.Interop
+{
+ ///
+ /// Represents the payload ID of a set of chunked data.
+ ///
+ public interface IChunkPayloadId
+ {
+ ///
+ /// The ID of the full request to differentiate different chunkings.
+ ///
+ /// Nullable to prevent default value omission.
+ uint? PayloadId { get; set; }
+ }
+}
diff --git a/src/Tgstation.Server.Host/Components/Interop/IMissingPayloadsCommunication.cs b/src/Tgstation.Server.Host/Components/Interop/IMissingPayloadsCommunication.cs
new file mode 100644
index 0000000000..392cd7eecb
--- /dev/null
+++ b/src/Tgstation.Server.Host/Components/Interop/IMissingPayloadsCommunication.cs
@@ -0,0 +1,15 @@
+using System.Collections.Generic;
+
+namespace Tgstation.Server.Host.Components.Interop
+{
+ ///
+ /// A communication that can be missing chunk payloads.
+ ///
+ interface IMissingPayloadsCommunication
+ {
+ ///
+ /// The s missing from a chunked request.
+ ///
+ IReadOnlyCollection MissingChunks { get; set; }
+ }
+}
diff --git a/src/Tgstation.Server.Host/Components/Interop/Topic/ChunkedTopicParameters.cs b/src/Tgstation.Server.Host/Components/Interop/Topic/ChunkedTopicParameters.cs
new file mode 100644
index 0000000000..79353a66bb
--- /dev/null
+++ b/src/Tgstation.Server.Host/Components/Interop/Topic/ChunkedTopicParameters.cs
@@ -0,0 +1,24 @@
+using System.Collections.Generic;
+
+namespace Tgstation.Server.Host.Components.Interop.Topic
+{
+ ///
+ /// for .
+ ///
+ sealed class ChunkedTopicParameters : TopicParameters, IMissingPayloadsCommunication, IChunkPayloadId
+ {
+ ///
+ public IReadOnlyCollection MissingChunks { get; set; }
+
+ ///
+ public uint? PayloadId { get; set; }
+
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ public ChunkedTopicParameters()
+ : base(TopicCommandType.ReceiveChunk)
+ {
+ }
+ }
+}
diff --git a/src/Tgstation.Server.Host/Components/Interop/Topic/TopicCommandType.cs b/src/Tgstation.Server.Host/Components/Interop/Topic/TopicCommandType.cs
index 83a71feaf3..e44e1a925e 100644
--- a/src/Tgstation.Server.Host/Components/Interop/Topic/TopicCommandType.cs
+++ b/src/Tgstation.Server.Host/Components/Interop/Topic/TopicCommandType.cs
@@ -52,5 +52,15 @@ namespace Tgstation.Server.Host.Components.Interop.Topic
/// Notify the server of a reattach and potentially new version.
///
ServerRestarted,
+
+ ///
+ /// Part of a larger topic.
+ ///
+ SendChunk,
+
+ ///
+ /// Receive additional data for a previous response.
+ ///
+ ReceiveChunk,
}
}
diff --git a/src/Tgstation.Server.Host/Components/Interop/Topic/TopicParameters.cs b/src/Tgstation.Server.Host/Components/Interop/Topic/TopicParameters.cs
index 1deaac3848..6e3e604d7c 100644
--- a/src/Tgstation.Server.Host/Components/Interop/Topic/TopicParameters.cs
+++ b/src/Tgstation.Server.Host/Components/Interop/Topic/TopicParameters.cs
@@ -7,7 +7,7 @@ namespace Tgstation.Server.Host.Components.Interop.Topic
///
/// Parameters for a topic request.
///
- sealed class TopicParameters : DMApiParameters
+ class TopicParameters : DMApiParameters
{
///
/// The .
@@ -50,6 +50,11 @@ namespace Tgstation.Server.Host.Components.Interop.Topic
///
public Version NewServerVersion { get; }
+ ///
+ /// The for a partial request.
+ ///
+ public ChunkData Chunk { get; }
+
///
/// Initializes a new instance of the class.
///
@@ -122,6 +127,16 @@ namespace Tgstation.Server.Host.Components.Interop.Topic
NewPort = serverPort;
}
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ /// The value of .
+ public TopicParameters(ChunkData chunk)
+ : this(TopicCommandType.SendChunk)
+ {
+ Chunk = chunk ?? throw new ArgumentNullException(nameof(chunk));
+ }
+
///
/// Initializes a new instance of the class.
///
@@ -135,7 +150,7 @@ namespace Tgstation.Server.Host.Components.Interop.Topic
/// Initializes a new instance of the class.
///
/// The value of .
- TopicParameters(TopicCommandType commandType)
+ protected TopicParameters(TopicCommandType commandType)
{
CommandType = commandType;
}
diff --git a/src/Tgstation.Server.Host/Components/Interop/Topic/TopicResponse.cs b/src/Tgstation.Server.Host/Components/Interop/Topic/TopicResponse.cs
index 7a67104155..f108aba543 100644
--- a/src/Tgstation.Server.Host/Components/Interop/Topic/TopicResponse.cs
+++ b/src/Tgstation.Server.Host/Components/Interop/Topic/TopicResponse.cs
@@ -7,7 +7,7 @@ namespace Tgstation.Server.Host.Components.Interop.Topic
///
/// A response to a topic request.
///
- sealed class TopicResponse : DMApiResponse
+ sealed class TopicResponse : DMApiResponse, IMissingPayloadsCommunication
{
///
/// The text to reply with as the result of a request, if any. Deprecated circa Interop 5.4.0.
@@ -28,5 +28,13 @@ namespace Tgstation.Server.Host.Components.Interop.Topic
/// The DMAPI s for requests.
///
public ICollection CustomCommands { get; set; }
+
+ ///
+ /// The for a partial response.
+ ///
+ public ChunkData Chunk { get; set; }
+
+ ///
+ public IReadOnlyCollection MissingChunks { get; set; }
}
}
diff --git a/src/Tgstation.Server.Host/Components/Session/ISessionController.cs b/src/Tgstation.Server.Host/Components/Session/ISessionController.cs
index 343e7eec37..21b45dc434 100644
--- a/src/Tgstation.Server.Host/Components/Session/ISessionController.cs
+++ b/src/Tgstation.Server.Host/Components/Session/ISessionController.cs
@@ -81,7 +81,7 @@ namespace Tgstation.Server.Host.Components.Session
/// The to send.
/// The for the operation.
/// A resulting in the of /world/Topic().
- Task SendCommand(TopicParameters parameters, CancellationToken cancellationToken);
+ Task SendCommand(TopicParameters parameters, CancellationToken cancellationToken);
///
/// Causes the world to start listening on a .
diff --git a/src/Tgstation.Server.Host/Components/Session/SessionController.cs b/src/Tgstation.Server.Host/Components/Session/SessionController.cs
index 723c8626ac..bb7f7345d8 100644
--- a/src/Tgstation.Server.Host/Components/Session/SessionController.cs
+++ b/src/Tgstation.Server.Host/Components/Session/SessionController.cs
@@ -3,12 +3,14 @@ using System.Collections.Generic;
using System.Globalization;
using System.Linq;
using System.Net;
+using System.Text;
using System.Threading;
using System.Threading.Tasks;
-using Byond.TopicSender;
using Microsoft.Extensions.Logging;
+
using Newtonsoft.Json;
+
using Serilog.Context;
using Tgstation.Server.Api;
@@ -24,7 +26,7 @@ using Tgstation.Server.Host.System;
namespace Tgstation.Server.Host.Components.Session
{
///
- sealed class SessionController : BridgeRequestChunker, ISessionController, IBridgeHandler, IChannelSink
+ sealed class SessionController : Chunker, ISessionController, IBridgeHandler, IChannelSink
{
///
public DMApiParameters DMApiParameters => ReattachInformation;
@@ -91,9 +93,9 @@ namespace Tgstation.Server.Host.Components.Session
readonly CancellationTokenSource reattachTopicCts;
///
- /// The for the .
+ /// The for the .
///
- readonly ITopicClient byondTopicSender;
+ readonly global::Byond.TopicSender.ITopicClient byondTopicSender;
///
/// The for the .
@@ -177,7 +179,7 @@ namespace Tgstation.Server.Host.Components.Session
/// The value of .
/// The value of .
/// The for the .
- /// The value of .
+ /// The value of .
/// The returning a to be run after the ends.
/// The optional time to wait before failing the .
/// If this is a reattached session.
@@ -187,7 +189,7 @@ namespace Tgstation.Server.Host.Components.Session
Api.Models.Instance metadata,
IProcess process,
IByondExecutableLock byondLock,
- ITopicClient byondTopicSender,
+ global::Byond.TopicSender.ITopicClient byondTopicSender,
IChatTrackingContext chatTrackingContext,
IBridgeRegistrar bridgeRegistrar,
IChatManager chat,
@@ -283,7 +285,7 @@ namespace Tgstation.Server.Host.Components.Session
}
///
- public override async Task ProcessBridgeRequest(BridgeParameters parameters, CancellationToken cancellationToken)
+ public async Task ProcessBridgeRequest(BridgeParameters parameters, CancellationToken cancellationToken)
{
if (parameters == null)
throw new ArgumentNullException(nameof(parameters));
@@ -319,7 +321,7 @@ namespace Tgstation.Server.Host.Components.Session
}
///
- public async Task SendCommand(TopicParameters parameters, CancellationToken cancellationToken)
+ public async Task SendCommand(TopicParameters parameters, CancellationToken cancellationToken)
{
if (parameters == null)
throw new ArgumentNullException(nameof(parameters));
@@ -338,62 +340,73 @@ namespace Tgstation.Server.Host.Components.Session
return null;
}
- parameters.AccessIdentifier = ReattachInformation.AccessIdentifier;
-
- var json = JsonConvert.SerializeObject(parameters, DMApiConstants.SerializerSettings);
- Logger.LogTrace("Topic request: {0}", json);
+ TopicResponse fullResponse = null;
try
{
- var commandString = String.Format(
- CultureInfo.InvariantCulture,
- "?{0}={1}",
- byondTopicSender.SanitizeString(DMApiConstants.TopicData),
- byondTopicSender.SanitizeString(json));
+ var combinedResponse = await SendTopicRequest(parameters, cancellationToken);
- var targetPort = ReattachInformation.Port;
+ void LogCombinedResponse()
+ {
+ if (combinedResponse != null)
+ Logger.LogTrace("Topic response: {topicString}", combinedResponse.ByondTopicResponse.StringData ?? "(NO STRING DATA)");
+ }
- var topicResponse = await byondTopicSender.SendTopic(
- new IPEndPoint(IPAddress.Loopback, targetPort),
- commandString,
- cancellationToken);
+ LogCombinedResponse();
- var topicReturn = topicResponse.StringData;
+ if (combinedResponse?.InteropResponse?.Chunk != null)
+ {
+ Logger.LogTrace("Topic response is chunked...");
- Interop.Topic.TopicResponse interopResponse = null;
- if (topicReturn != null)
- try
+ ChunkData nextChunk = combinedResponse.InteropResponse.Chunk;
+ do
{
- interopResponse = JsonConvert.DeserializeObject(topicReturn, DMApiConstants.SerializerSettings);
- if (interopResponse.ErrorMessage != null)
+ var nextRequest = await ProcessChunk(
+ (completedResponse, cancellationToken) =>
+ {
+ fullResponse = completedResponse;
+ return Task.FromResult(null);
+ },
+ error =>
+ {
+ Logger.LogWarning("Topic response chunking error: {message}", error);
+ return null;
+ },
+ combinedResponse?.InteropResponse?.Chunk,
+ cancellationToken);
+
+ if (nextRequest != null)
{
- Logger.LogWarning("Errored topic response for command {0}: {1}", parameters.CommandType, interopResponse.ErrorMessage);
+ nextRequest.PayloadId = nextChunk.PayloadId;
+ combinedResponse = await SendTopicRequest(nextRequest, cancellationToken);
+ LogCombinedResponse();
+ nextChunk = combinedResponse?.InteropResponse?.Chunk;
}
-
- Logger.LogTrace("Interop response: {0}", topicReturn);
+ else
+ nextChunk = null;
}
- catch (Exception ex)
- {
- Logger.LogWarning(ex, "Invalid interop response: {0}", topicReturn);
- }
-
- return new CombinedTopicResponse(topicResponse, interopResponse);
+ while (nextChunk != null);
+ }
+ else
+ fullResponse = combinedResponse?.InteropResponse;
}
catch (OperationCanceledException ex)
{
- Logger.LogTrace(
+ Logger.LogDebug(
ex,
- "Topic request {0}!",
+ "Topic request {cancellationType}!",
cancellationToken.IsCancellationRequested
? "aborted"
: "timed out");
cancellationToken.ThrowIfCancellationRequested();
}
- catch (Exception e)
- {
- Logger.LogWarning(e, "Send command exception!");
- }
- return null;
+ if (fullResponse?.ErrorMessage != null)
+ Logger.LogWarning(
+ "Errored topic response for command {commandType}: {errorMessage}",
+ parameters.CommandType,
+ fullResponse.ErrorMessage);
+
+ return fullResponse;
}
///
@@ -411,7 +424,7 @@ namespace Tgstation.Server.Host.Components.Session
cancellationToken)
;
- if (commandResult.InteropResponse?.ErrorMessage != null)
+ if (commandResult?.ErrorMessage != null)
return false;
ReattachInformation.Port = port;
@@ -445,7 +458,7 @@ namespace Tgstation.Server.Host.Components.Session
cancellationToken)
;
- return result?.InteropResponse != null && result.InteropResponse?.ErrorMessage == null;
+ return result?.ErrorMessage == null;
}
///
@@ -540,11 +553,11 @@ namespace Tgstation.Server.Host.Components.Session
if (reattachResponse != null)
{
- if (reattachResponse.InteropResponse?.CustomCommands != null)
- chatTrackingContext.CustomCommands = reattachResponse.InteropResponse.CustomCommands;
- else if (reattachResponse.InteropResponse != null)
+ if (reattachResponse?.CustomCommands != null)
+ chatTrackingContext.CustomCommands = reattachResponse.CustomCommands;
+ else if (reattachResponse != null)
Logger.LogWarning(
- "DMAPI v{0} isn't returning the TGS custom commands list. Functionality added in v5.2.0.",
+ "DMAPI Interop v{0} isn't returning the TGS custom commands list. Functionality added in v5.2.0.",
CompileJob.DMApiVersion.Semver());
}
}
@@ -698,7 +711,7 @@ namespace Tgstation.Server.Host.Components.Session
oldRebootTcs.SetResult();
break;
case BridgeCommandType.Chunk:
- return await ProcessBridgeChunk(parameters.Chunk, cancellationToken);
+ return await ProcessChunk(ProcessBridgeRequest, BridgeError, parameters.Chunk, cancellationToken);
case null:
return BridgeError("Missing commandType!");
default:
@@ -707,5 +720,198 @@ namespace Tgstation.Server.Host.Components.Session
return response;
}
+
+ ///
+ /// Log and return a for a given .
+ ///
+ /// The error message.
+ /// A new errored .
+ BridgeResponse BridgeError(string message)
+ {
+ Logger.LogWarning("Bridge request chunking error: {message}", message);
+ return new BridgeResponse
+ {
+ ErrorMessage = message,
+ };
+ }
+
+ ///
+ /// Send a topic request for given to DreamDaemon, chunking it if necessary.
+ ///
+ /// The to send.
+ /// The for the operation.
+ /// A resulting in the of the topic request.
+ async Task SendTopicRequest(TopicParameters parameters, CancellationToken cancellationToken)
+ {
+ parameters.AccessIdentifier = ReattachInformation.AccessIdentifier;
+
+ var fullCommandString = GenerateQueryString(parameters, out var json);
+ Logger.LogTrace("Topic request: {0}", json);
+ var fullCommandByteCount = Encoding.UTF8.GetByteCount(fullCommandString);
+ if (fullCommandByteCount <= DMApiConstants.MaximumTopicRequestLength)
+ return await SendRawTopic(fullCommandString, cancellationToken);
+
+ var interopChunkingVersion = new Version(5, 6, 0);
+ if (ReattachInformation.Dmb.CompileJob.DMApiVersion < interopChunkingVersion)
+ {
+ Logger.LogWarning(
+ "Cannot send topic request as it is exceeds the single request limit of {limitBytes}B ({actualBytes}B) and requires chunking and the current compile job's interop version must be at least {chunkingVersionRequired}!",
+ DMApiConstants.MaximumTopicRequestLength,
+ fullCommandByteCount,
+ interopChunkingVersion);
+ return null;
+ }
+
+ var payloadId = NextPayloadId;
+
+ // AccessIdentifer is just noise in a chunked request
+ parameters.AccessIdentifier = null;
+ GenerateQueryString(parameters, out json);
+
+ // yes, this straight up ignores unicode, precalculating it is useless when we don't
+ // even know if the UTF8 bytes of the url encoded chunk will fit the window until we do said encoding
+ var fullPayloadSize = (uint)json.Length;
+
+ List chunkQueryStrings = null;
+ for (var chunkCount = 2; chunkQueryStrings == null; ++chunkCount)
+ {
+ var standardChunkSize = fullPayloadSize / chunkCount;
+ var bigChunkSize = standardChunkSize + (fullPayloadSize % chunkCount);
+ if (bigChunkSize > DMApiConstants.MaximumTopicRequestLength)
+ continue;
+
+ chunkQueryStrings = new List();
+ for (var i = 0U; i < chunkCount; ++i)
+ {
+ var startIndex = i * standardChunkSize;
+ var subStringLength = Math.Min(
+ fullPayloadSize - startIndex,
+ i == chunkCount - 1
+ ? bigChunkSize
+ : standardChunkSize);
+ var chunkPayload = json.Substring((int)startIndex, (int)subStringLength);
+
+ var chunk = new ChunkData
+ {
+ Payload = chunkPayload,
+ PayloadId = payloadId,
+ SequenceId = i,
+ TotalChunks = (uint)chunkCount,
+ };
+
+ var chunkParameters = new TopicParameters(chunk)
+ {
+ AccessIdentifier = ReattachInformation.AccessIdentifier,
+ };
+
+ var chunkCommandString = GenerateQueryString(chunkParameters, out _);
+ if (Encoding.UTF8.GetByteCount(chunkCommandString) > DMApiConstants.MaximumTopicRequestLength)
+ {
+ // too long when encoded, need more chunks
+ chunkQueryStrings = null;
+ break;
+ }
+
+ chunkQueryStrings.Add(chunkCommandString);
+ }
+ }
+
+ Logger.LogTrace("Chunking topic request ({totalChunks} total)...", chunkQueryStrings.Count);
+
+ CombinedTopicResponse combinedResponse = null;
+ bool LogRequestIssue(bool possiblyFromCompletedRequest)
+ {
+ if (combinedResponse?.InteropResponse == null || combinedResponse.InteropResponse.ErrorMessage != null)
+ {
+ Logger.LogWarning(
+ "Topic request {chunkingStatus} failed!{potentialRequestError}",
+ possiblyFromCompletedRequest ? "final chunk" : "chunking",
+ combinedResponse?.InteropResponse?.ErrorMessage != null
+ ? $" Request error: {combinedResponse.InteropResponse.ErrorMessage}"
+ : String.Empty);
+ return true;
+ }
+
+ return false;
+ }
+
+ foreach (var chunkCommandString in chunkQueryStrings)
+ {
+ combinedResponse = await SendRawTopic(chunkCommandString, cancellationToken);
+ if (LogRequestIssue(chunkCommandString == chunkQueryStrings.Last()))
+ return null;
+ }
+
+ while ((combinedResponse.InteropResponse.MissingChunks?.Count ?? 0) > 0)
+ {
+ Logger.LogWarning("DD is still missing some chunks of topic request P{payloadId}! Sending missing chunks...", payloadId);
+ var lastIndex = combinedResponse.InteropResponse.MissingChunks.Last();
+ foreach (var missingChunkIndex in combinedResponse.InteropResponse.MissingChunks)
+ {
+ var chunkCommandString = chunkQueryStrings[(int)missingChunkIndex];
+ combinedResponse = await SendRawTopic(chunkCommandString, cancellationToken);
+ if (LogRequestIssue(missingChunkIndex == lastIndex))
+ return null;
+ }
+ }
+
+ return combinedResponse;
+ }
+
+ ///
+ /// Generates a query string for a given set of .
+ ///
+ /// The to serialize.
+ /// The intermediate JSON prior to URL encoding.
+ /// The query string for the given .
+ string GenerateQueryString(TopicParameters parameters, out string json)
+ {
+ json = JsonConvert.SerializeObject(parameters, DMApiConstants.SerializerSettings);
+ var commandString = String.Format(
+ CultureInfo.InvariantCulture,
+ "?{0}={1}",
+ byondTopicSender.SanitizeString(DMApiConstants.TopicData),
+ byondTopicSender.SanitizeString(json));
+ return commandString;
+ }
+
+ ///
+ /// Send a given to DreamDaemon's /world/Topic.
+ ///
+ /// The sanitized topic query string to send.
+ /// The for the operation.
+ /// A resulting in the of the topic request.
+ async Task SendRawTopic(string queryString, CancellationToken cancellationToken)
+ {
+ var targetPort = ReattachInformation.Port;
+ global::Byond.TopicSender.TopicResponse byondResponse;
+ try
+ {
+ byondResponse = await byondTopicSender.SendTopic(
+ new IPEndPoint(IPAddress.Loopback, targetPort),
+ queryString,
+ cancellationToken);
+ }
+ catch (Exception ex)
+ {
+ Logger.LogWarning(ex, "SendTopic exception!");
+ return null;
+ }
+
+ var topicReturn = byondResponse.StringData;
+
+ TopicResponse interopResponse = null;
+ if (topicReturn != null)
+ try
+ {
+ interopResponse = JsonConvert.DeserializeObject(topicReturn, DMApiConstants.SerializerSettings);
+ }
+ catch (Exception ex)
+ {
+ Logger.LogWarning(ex, "Invalid interop response: {topicReturnString}", topicReturn);
+ }
+
+ return new CombinedTopicResponse(byondResponse, interopResponse);
+ }
}
}
diff --git a/src/Tgstation.Server.Host/Components/Watchdog/WatchdogBase.cs b/src/Tgstation.Server.Host/Components/Watchdog/WatchdogBase.cs
index 4d9330aaeb..a708244fe1 100644
--- a/src/Tgstation.Server.Host/Components/Watchdog/WatchdogBase.cs
+++ b/src/Tgstation.Server.Host/Components/Watchdog/WatchdogBase.cs
@@ -290,7 +290,7 @@ namespace Tgstation.Server.Host.Components.Watchdog
Text = "TGS: Bad topic exchange!",
};
- if (commandResult.InteropResponse == null)
+ if (commandResult == null)
return new MessageContent
{
Text = "TGS: Bad topic response!",
@@ -298,8 +298,8 @@ namespace Tgstation.Server.Host.Components.Watchdog
var commandResponse = new MessageContent
{
- Text = commandResult.InteropResponse.CommandResponse?.Text ?? commandResult.InteropResponse.CommandResponseMessage,
- Embed = commandResult.InteropResponse.CommandResponse?.Embed,
+ Text = commandResult.CommandResponse?.Text ?? commandResult.CommandResponseMessage,
+ Embed = commandResult.CommandResponse?.Embed,
};
if (commandResponse.Text == null && commandResponse.Embed == null)
@@ -1125,11 +1125,11 @@ namespace Tgstation.Server.Host.Components.Watchdog
///
/// Handle any in a given topic .
///
- /// The .
- void HandleChatResponses(CombinedTopicResponse result)
+ /// The .
+ void HandleChatResponses(TopicResponse result)
{
- if (result?.InteropResponse?.ChatResponses != null)
- foreach (var response in result.InteropResponse.ChatResponses)
+ if (result?.ChatResponses != null)
+ foreach (var response in result.ChatResponses)
Chat.QueueMessage(
response,
response.ChannelIds
diff --git a/tests/Tgstation.Server.Tests/Instance/TestBridgeHandler.cs b/tests/Tgstation.Server.Tests/Instance/TestBridgeHandler.cs
index b2c07e33cf..ef751274b9 100644
--- a/tests/Tgstation.Server.Tests/Instance/TestBridgeHandler.cs
+++ b/tests/Tgstation.Server.Tests/Instance/TestBridgeHandler.cs
@@ -6,8 +6,6 @@ using System.Web;
using Microsoft.Extensions.Logging;
using Microsoft.VisualStudio.TestTools.UnitTesting;
-using Moq;
-
using Newtonsoft.Json;
using Tgstation.Server.Host.Components.Interop;
@@ -15,7 +13,7 @@ using Tgstation.Server.Host.Components.Interop.Bridge;
namespace Tgstation.Server.Tests.Instance
{
- sealed class TestBridgeHandler : BridgeRequestChunker, IBridgeHandler
+ sealed class TestBridgeHandler : Chunker, IBridgeHandler
{
class DMApiParametersImpl : DMApiParameters { }
@@ -34,6 +32,8 @@ namespace Tgstation.Server.Tests.Instance
readonly TaskCompletionSource bridgeTestsTcs;
readonly ushort serverPort;
+ bool chunksProcessed = false;
+
public TestBridgeHandler(TaskCompletionSource tcs, ILogger logger, ushort serverPort)
: base(logger)
{
@@ -41,13 +41,28 @@ namespace Tgstation.Server.Tests.Instance
this.serverPort = serverPort;
}
- public override async Task ProcessBridgeRequest(BridgeParameters parameters, CancellationToken cancellationToken)
+ public async Task ProcessBridgeRequest(BridgeParameters parameters, CancellationToken cancellationToken)
{
try
{
Assert.AreEqual(DMApiParameters.AccessIdentifier, parameters.AccessIdentifier);
if (parameters.CommandType == BridgeCommandType.Chunk)
- return await ProcessBridgeChunk(parameters.Chunk, cancellationToken);
+ return await ProcessChunk(
+ (parameters, cancellationToken) =>
+ {
+ chunksProcessed = true;
+ return ProcessBridgeRequest(parameters, cancellationToken);
+ },
+ error =>
+ {
+ bridgeTestsTcs.SetException(new Exception(error));
+ return new BridgeResponse
+ {
+ ErrorMessage = error,
+ };
+ },
+ parameters.Chunk,
+ cancellationToken);
Assert.AreEqual((BridgeCommandType)0, parameters.CommandType);
Assert.IsNotNull(parameters.ChatMessage?.Text);
@@ -57,6 +72,7 @@ namespace Tgstation.Server.Tests.Instance
Assert.IsFalse(String.IsNullOrWhiteSpace(coreMessage));
if (coreMessage == "done")
{
+ Assert.IsTrue(chunksProcessed);
Assert.AreEqual(DMApiConstants.MaximumBridgeRequestLength, lastBridgeRequestSize);
Assert.AreEqual(new string('a', (int)(DMApiConstants.MaximumBridgeRequestLength * 3)), splits[1]);
diff --git a/tgstation-server.sln b/tgstation-server.sln
index 8d53fc25c0..6128dc5188 100644
--- a/tgstation-server.sln
+++ b/tgstation-server.sln
@@ -139,9 +139,11 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "v5", "v5", "{FAEAD3B5-2EAB-
ProjectSection(SolutionItems) = preProject
src\DMAPI\tgs\v5\api.dm = src\DMAPI\tgs\v5\api.dm
src\DMAPI\tgs\v5\bridge.dm = src\DMAPI\tgs\v5\bridge.dm
+ src\DMAPI\tgs\v5\chunking.dm = src\DMAPI\tgs\v5\chunking.dm
src\DMAPI\tgs\v5\commands.dm = src\DMAPI\tgs\v5\commands.dm
src\DMAPI\tgs\v5\README.md = src\DMAPI\tgs\v5\README.md
src\DMAPI\tgs\v5\serializers.dm = src\DMAPI\tgs\v5\serializers.dm
+ src\DMAPI\tgs\v5\topic.dm = src\DMAPI\tgs\v5\topic.dm
src\DMAPI\tgs\v5\undefs.dm = src\DMAPI\tgs\v5\undefs.dm
src\DMAPI\tgs\v5\_defines.dm = src\DMAPI\tgs\v5\_defines.dm
src\DMAPI\tgs\v5\__interop_version.dm = src\DMAPI\tgs\v5\__interop_version.dm
@@ -174,13 +176,6 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "BuildFail", "BuildFail", "{
tests\DMAPI\BuildFail\Test.dm = tests\DMAPI\BuildFail\Test.dm
EndProjectSection
EndProject
-Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "workflows", "workflows", "{28CDEB8F-2B2A-47A2-985B-5E2487E8D096}"
- ProjectSection(SolutionItems) = preProject
- .github\workflows\artifact-cleanup.yml = .github\workflows\artifact-cleanup.yml
- .github\workflows\ci-suite.yml = .github\workflows\ci-suite.yml
- .github\workflows\stable-merge.yml = .github\workflows\stable-merge.yml
- EndProjectSection
-EndProject
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "ISSUE_TEMPLATE", "ISSUE_TEMPLATE", "{CFFD7992-E73A-4D1F-9D7A-C817C07B7BEB}"
ProjectSection(SolutionItems) = preProject
.github\ISSUE_TEMPLATE\bug-report.md = .github\ISSUE_TEMPLATE\bug-report.md
@@ -360,7 +355,6 @@ Global
{7B8FC2AF-1B64-4A89-8480-F0FE360DC9EC} = {82066812-6C73-4360-943B-B23F2F491261}
{F32B9514-AAD9-429D-841A-ED810FC2598C} = {82066812-6C73-4360-943B-B23F2F491261}
{103C61AB-67D6-46FE-AA47-CC633B88EE0F} = {82066812-6C73-4360-943B-B23F2F491261}
- {28CDEB8F-2B2A-47A2-985B-5E2487E8D096} = {E82104F4-F5C4-4786-ACD4-B635166CDB21}
{CFFD7992-E73A-4D1F-9D7A-C817C07B7BEB} = {E82104F4-F5C4-4786-ACD4-B635166CDB21}
{5813CC33-B16C-485D-A74D-20204DDF6542} = {316141B0-CD21-4769-A013-D53DA9B9EC09}
{CE499888-B22B-457C-891E-0EA9DC317228} = {A55C1117-5808-4AB2-BEA6-4D4A3E66A2F2}