mirror of
https://github.com/tgstation/tgstation-server.git
synced 2026-08-30 16:39:21 +01:00
The big topic chunking commit
- Implemented topic request and response chunking - Removed GitHub workflows from .sln as they are auto-detected - Interop version bump to 5.6.0 - DMAPI version bump to 6.4.0 - Split common chunking code into its own DMAPI/v5 file - Added DMAPI/v5 file just for all the new topic handling methods - Fixed runtime errors duing Topic calls not being reported to TGS - Fixed certain topic requests returning lists instead of JSON - Bridge and topic response chunk handling merged into `BridgeRequestChunker` now just `Chunker` - `CombinedTopicResponse` is no longer used outside of SessionController
This commit is contained in:
+2
-2
@@ -8,8 +8,8 @@
|
||||
<TgsApiVersion>9.9.0</TgsApiVersion>
|
||||
<TgsApiLibraryVersion>10.3.0</TgsApiLibraryVersion>
|
||||
<TgsClientVersion>11.3.0</TgsClientVersion>
|
||||
<TgsDmapiVersion>6.3.1</TgsDmapiVersion>
|
||||
<TgsInteropVersion>5.5.0</TgsInteropVersion>
|
||||
<TgsDmapiVersion>6.4.0</TgsDmapiVersion>
|
||||
<TgsInteropVersion>5.6.0</TgsInteropVersion>
|
||||
<TgsHostWatchdogVersion>1.2.1</TgsHostWatchdogVersion>
|
||||
<TgsContainerScriptVersion>1.2.1</TgsContainerScriptVersion>
|
||||
<TgsMigratorVersion>1.0.1</TgsMigratorVersion>
|
||||
|
||||
+1
-1
@@ -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.
|
||||
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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`.
|
||||
|
||||
@@ -1 +1 @@
|
||||
"5.5.0"
|
||||
"5.6.0"
|
||||
|
||||
@@ -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"
|
||||
|
||||
+4
-136
@@ -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)
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
@@ -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]")
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
{
|
||||
/// <summary>
|
||||
/// Processes chunked bridge requests.
|
||||
/// </summary>
|
||||
abstract class BridgeRequestChunker : IBridgeDispatcher
|
||||
{
|
||||
/// <summary>
|
||||
/// The cache of chunked bridge requests.
|
||||
/// </summary>
|
||||
/// <remarks>If the DMAPI is erroring, this can present a memory leak. Worth expiring entries if they aren't completed after some minutes.</remarks>
|
||||
readonly Dictionary<uint, Tuple<ChunkedRequestInfo, string[]>> bridgeChunks;
|
||||
|
||||
/// <summary>
|
||||
/// The <see cref="ILogger"/> for the <see cref="BridgeRequestChunker"/>.
|
||||
/// </summary>
|
||||
protected ILogger<BridgeRequestChunker> Logger { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="BridgeRequestChunker"/> class.
|
||||
/// </summary>
|
||||
/// <param name="logger">The value of <see cref="Logger"/>.</param>
|
||||
protected BridgeRequestChunker(ILogger<BridgeRequestChunker> logger)
|
||||
{
|
||||
Logger = logger ?? throw new ArgumentNullException(nameof(logger));
|
||||
bridgeChunks = new Dictionary<uint, Tuple<ChunkedRequestInfo, string[]>>();
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public abstract Task<BridgeResponse> ProcessBridgeRequest(BridgeParameters parameters, CancellationToken cancellationToken);
|
||||
|
||||
/// <summary>
|
||||
/// Process a given <paramref name="chunk"/>.
|
||||
/// </summary>
|
||||
/// <param name="chunk">The <see cref="ChunkData"/>.</param>
|
||||
/// <param name="cancellationToken">The <see cref="CancellationToken"/> for the operation.</param>
|
||||
/// <returns>A <see cref="Task{TResult}"/> resulting in the <see cref="BridgeResponse"/> for the chunked request.</returns>
|
||||
protected async Task<BridgeResponse> 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<ChunkedRequestInfo, string[]>(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<uint>();
|
||||
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<BridgeParameters>(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);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Create and logs an errored <see cref="BridgeResponse"/>.
|
||||
/// </summary>
|
||||
/// <param name="message">The error message.</param>
|
||||
/// <param name="log">If <paramref name="message"/> should be written to the <see cref="Logger"/>.</param>
|
||||
/// <returns>A new errored <see cref="BridgeResponse"/>.</returns>
|
||||
protected BridgeResponse BridgeError(string message, bool log = true)
|
||||
{
|
||||
if (log)
|
||||
Logger.LogWarning("Bridge processing error: {errorMessage}", message);
|
||||
|
||||
return new BridgeResponse
|
||||
{
|
||||
ErrorMessage = message,
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -5,7 +5,7 @@ namespace Tgstation.Server.Host.Components.Interop.Bridge
|
||||
/// <summary>
|
||||
/// A response to a bridge request.
|
||||
/// </summary>
|
||||
public class BridgeResponse : DMApiResponse
|
||||
public class BridgeResponse : DMApiResponse, IMissingPayloadsCommunication
|
||||
{
|
||||
/// <summary>
|
||||
/// The new port for <see cref="BridgeCommandType.Reboot"/> requests.
|
||||
|
||||
@@ -1,18 +0,0 @@
|
||||
namespace Tgstation.Server.Host.Components.Interop.Bridge
|
||||
{
|
||||
/// <summary>
|
||||
/// A packet of a split serialized set of <see cref="BridgeParameters"/>.
|
||||
/// </summary>
|
||||
public sealed class ChunkData : ChunkedRequestInfo
|
||||
{
|
||||
/// <summary>
|
||||
/// The sequence ID of the chunk.
|
||||
/// </summary>
|
||||
public uint SequenceId { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The partial JSON payload of the chunk.
|
||||
/// </summary>
|
||||
public string Payload { get; set; }
|
||||
}
|
||||
}
|
||||
@@ -1,18 +0,0 @@
|
||||
namespace Tgstation.Server.Host.Components.Interop.Bridge
|
||||
{
|
||||
/// <summary>
|
||||
/// Information about a chunked bridge request.
|
||||
/// </summary>
|
||||
public abstract class ChunkedRequestInfo
|
||||
{
|
||||
/// <summary>
|
||||
/// The ID of the full request to differentiate different chunkings.
|
||||
/// </summary>
|
||||
public uint PayloadId { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The total number of chunks in the request.
|
||||
/// </summary>
|
||||
public uint TotalChunks { get; set; }
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
namespace Tgstation.Server.Host.Components.Interop
|
||||
{
|
||||
/// <summary>
|
||||
/// A packet of a split serialized set of data.
|
||||
/// </summary>
|
||||
public sealed class ChunkData : ChunkSetInfo
|
||||
{
|
||||
/// <summary>
|
||||
/// The sequence ID of the chunk.
|
||||
/// </summary>
|
||||
/// <remarks>Always zero indexed. Nullable to prevent default value omission.</remarks>
|
||||
public uint? SequenceId { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The partial JSON payload of the chunk.
|
||||
/// </summary>
|
||||
public string Payload { get; set; }
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
namespace Tgstation.Server.Host.Components.Interop
|
||||
{
|
||||
/// <summary>
|
||||
/// Information about a chunked bridge request.
|
||||
/// </summary>
|
||||
public abstract class ChunkSetInfo : IChunkPayloadId
|
||||
{
|
||||
/// <inheritdoc />
|
||||
public uint? PayloadId { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The total number of chunks in the request.
|
||||
/// </summary>
|
||||
public uint TotalChunks { get; set; }
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
{
|
||||
/// <summary>
|
||||
/// Class that deserializes chunked interop payloads.
|
||||
/// </summary>
|
||||
abstract class Chunker
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets a payload ID for use in a new <see cref="ChunkSetInfo"/>.
|
||||
/// </summary>
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// The cache of chunked communications.
|
||||
/// </summary>
|
||||
/// <remarks>If the DMAPI is erroring, this can present a memory leak. Worth expiring entries if they aren't completed after some minutes.</remarks>
|
||||
readonly Dictionary<uint, Tuple<ChunkSetInfo, string[]>> chunkSets;
|
||||
|
||||
/// <summary>
|
||||
/// The highest payload ID value seen.
|
||||
/// </summary>
|
||||
uint highestSeenPayloadId;
|
||||
|
||||
/// <summary>
|
||||
/// The <see cref="ILogger"/> for the <see cref="Chunker"/>.
|
||||
/// </summary>
|
||||
protected ILogger<Chunker> Logger { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="Chunker"/> class.
|
||||
/// </summary>
|
||||
/// <param name="logger">The value of <see cref="Logger"/>.</param>
|
||||
protected Chunker(ILogger<Chunker> logger)
|
||||
{
|
||||
Logger = logger ?? throw new ArgumentNullException(nameof(logger));
|
||||
chunkSets = new Dictionary<uint, Tuple<ChunkSetInfo, string[]>>();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Process a given <paramref name="chunk"/>.
|
||||
/// </summary>
|
||||
/// <typeparam name="TCommnication">The <see cref="Type"/> of communication that was chunked.</typeparam>
|
||||
/// <typeparam name="TResponse">The <see cref="Type"/> of <see cref="IMissingPayloadsCommunication"/> expected.</typeparam>
|
||||
/// <param name="completionCallback">The callback that receives the completed <typeparamref name="TCommnication"/>.</param>
|
||||
/// <param name="chunkErrorCallback">The callback that generates a <typeparamref name="TResponse"/> for a given error.</param>
|
||||
/// <param name="chunk">The <see cref="ChunkData"/>.</param>
|
||||
/// <param name="cancellationToken">The <see cref="CancellationToken"/> for the operation.</param>
|
||||
/// <returns>A <see cref="Task{TResult}"/> resulting in the <typeparamref name="TResponse"/> for the chunked request.</returns>
|
||||
protected async Task<TResponse> ProcessChunk<TCommnication, TResponse>(
|
||||
Func<TCommnication, CancellationToken, Task<TResponse>> completionCallback,
|
||||
Func<string, TResponse> 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<ChunkSetInfo, string[]>(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<uint>();
|
||||
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<TCommnication>(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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
namespace Tgstation.Server.Host.Components.Interop
|
||||
{
|
||||
/// <summary>
|
||||
/// Represents the payload ID of a set of chunked data.
|
||||
/// </summary>
|
||||
public interface IChunkPayloadId
|
||||
{
|
||||
/// <summary>
|
||||
/// The ID of the full request to differentiate different chunkings.
|
||||
/// </summary>
|
||||
/// <remarks>Nullable to prevent default value omission.</remarks>
|
||||
uint? PayloadId { get; set; }
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
using System.Collections.Generic;
|
||||
|
||||
namespace Tgstation.Server.Host.Components.Interop
|
||||
{
|
||||
/// <summary>
|
||||
/// A communication that can be missing chunk payloads.
|
||||
/// </summary>
|
||||
interface IMissingPayloadsCommunication
|
||||
{
|
||||
/// <summary>
|
||||
/// The <see cref="ChunkData.SequenceId"/>s missing from a chunked request.
|
||||
/// </summary>
|
||||
IReadOnlyCollection<uint> MissingChunks { get; set; }
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
using System.Collections.Generic;
|
||||
|
||||
namespace Tgstation.Server.Host.Components.Interop.Topic
|
||||
{
|
||||
/// <summary>
|
||||
/// <see cref="TopicParameters"/> for <see cref="TopicCommandType.ReceiveChunk"/>.
|
||||
/// </summary>
|
||||
sealed class ChunkedTopicParameters : TopicParameters, IMissingPayloadsCommunication, IChunkPayloadId
|
||||
{
|
||||
/// <inheritdoc />
|
||||
public IReadOnlyCollection<uint> MissingChunks { get; set; }
|
||||
|
||||
/// <inheritdoc />
|
||||
public uint? PayloadId { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="ChunkedTopicParameters"/> class.
|
||||
/// </summary>
|
||||
public ChunkedTopicParameters()
|
||||
: base(TopicCommandType.ReceiveChunk)
|
||||
{
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -52,5 +52,15 @@ namespace Tgstation.Server.Host.Components.Interop.Topic
|
||||
/// Notify the server of a reattach and potentially new version.
|
||||
/// </summary>
|
||||
ServerRestarted,
|
||||
|
||||
/// <summary>
|
||||
/// Part of a larger topic.
|
||||
/// </summary>
|
||||
SendChunk,
|
||||
|
||||
/// <summary>
|
||||
/// Receive additional data for a previous response.
|
||||
/// </summary>
|
||||
ReceiveChunk,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -7,7 +7,7 @@ namespace Tgstation.Server.Host.Components.Interop.Topic
|
||||
/// <summary>
|
||||
/// Parameters for a topic request.
|
||||
/// </summary>
|
||||
sealed class TopicParameters : DMApiParameters
|
||||
class TopicParameters : DMApiParameters
|
||||
{
|
||||
/// <summary>
|
||||
/// The <see cref="TopicCommandType"/>.
|
||||
@@ -50,6 +50,11 @@ namespace Tgstation.Server.Host.Components.Interop.Topic
|
||||
/// </summary>
|
||||
public Version NewServerVersion { get; }
|
||||
|
||||
/// <summary>
|
||||
/// The <see cref="ChunkData"/> for a partial request.
|
||||
/// </summary>
|
||||
public ChunkData Chunk { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="TopicParameters"/> class.
|
||||
/// </summary>
|
||||
@@ -122,6 +127,16 @@ namespace Tgstation.Server.Host.Components.Interop.Topic
|
||||
NewPort = serverPort;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="TopicParameters"/> class.
|
||||
/// </summary>
|
||||
/// <param name="chunk">The value of <see cref="Chunk"/>.</param>
|
||||
public TopicParameters(ChunkData chunk)
|
||||
: this(TopicCommandType.SendChunk)
|
||||
{
|
||||
Chunk = chunk ?? throw new ArgumentNullException(nameof(chunk));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="TopicParameters"/> class.
|
||||
/// </summary>
|
||||
@@ -135,7 +150,7 @@ namespace Tgstation.Server.Host.Components.Interop.Topic
|
||||
/// Initializes a new instance of the <see cref="TopicParameters"/> class.
|
||||
/// </summary>
|
||||
/// <param name="commandType">The value of <see cref="CommandType"/>.</param>
|
||||
TopicParameters(TopicCommandType commandType)
|
||||
protected TopicParameters(TopicCommandType commandType)
|
||||
{
|
||||
CommandType = commandType;
|
||||
}
|
||||
|
||||
@@ -7,7 +7,7 @@ namespace Tgstation.Server.Host.Components.Interop.Topic
|
||||
/// <summary>
|
||||
/// A response to a topic request.
|
||||
/// </summary>
|
||||
sealed class TopicResponse : DMApiResponse
|
||||
sealed class TopicResponse : DMApiResponse, IMissingPayloadsCommunication
|
||||
{
|
||||
/// <summary>
|
||||
/// The text to reply with as the result of a <see cref="TopicCommandType.ChatCommand"/> request, if any. Deprecated circa Interop 5.4.0.
|
||||
@@ -28,5 +28,13 @@ namespace Tgstation.Server.Host.Components.Interop.Topic
|
||||
/// The DMAPI <see cref="CustomCommand"/>s for <see cref="TopicCommandType.ServerRestarted"/> requests.
|
||||
/// </summary>
|
||||
public ICollection<CustomCommand> CustomCommands { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The <see cref="ChunkData"/> for a partial response.
|
||||
/// </summary>
|
||||
public ChunkData Chunk { get; set; }
|
||||
|
||||
/// <inheritdoc />
|
||||
public IReadOnlyCollection<uint> MissingChunks { get; set; }
|
||||
}
|
||||
}
|
||||
|
||||
@@ -81,7 +81,7 @@ namespace Tgstation.Server.Host.Components.Session
|
||||
/// <param name="parameters">The <see cref="TopicParameters"/> to send.</param>
|
||||
/// <param name="cancellationToken">The <see cref="CancellationToken"/> for the operation.</param>
|
||||
/// <returns>A <see cref="Task{TResult}"/> resulting in the <see cref="TopicResponse"/> of /world/Topic().</returns>
|
||||
Task<CombinedTopicResponse> SendCommand(TopicParameters parameters, CancellationToken cancellationToken);
|
||||
Task<TopicResponse> SendCommand(TopicParameters parameters, CancellationToken cancellationToken);
|
||||
|
||||
/// <summary>
|
||||
/// Causes the world to start listening on a <paramref name="newPort"/>.
|
||||
|
||||
@@ -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
|
||||
{
|
||||
/// <inheritdoc />
|
||||
sealed class SessionController : BridgeRequestChunker, ISessionController, IBridgeHandler, IChannelSink
|
||||
sealed class SessionController : Chunker, ISessionController, IBridgeHandler, IChannelSink
|
||||
{
|
||||
/// <inheritdoc />
|
||||
public DMApiParameters DMApiParameters => ReattachInformation;
|
||||
@@ -91,9 +93,9 @@ namespace Tgstation.Server.Host.Components.Session
|
||||
readonly CancellationTokenSource reattachTopicCts;
|
||||
|
||||
/// <summary>
|
||||
/// The <see cref="ITopicClient"/> for the <see cref="SessionController"/>.
|
||||
/// The <see cref="global::Byond.TopicSender.ITopicClient"/> for the <see cref="SessionController"/>.
|
||||
/// </summary>
|
||||
readonly ITopicClient byondTopicSender;
|
||||
readonly global::Byond.TopicSender.ITopicClient byondTopicSender;
|
||||
|
||||
/// <summary>
|
||||
/// The <see cref="IBridgeRegistration"/> for the <see cref="SessionController"/>.
|
||||
@@ -177,7 +179,7 @@ namespace Tgstation.Server.Host.Components.Session
|
||||
/// <param name="chat">The value of <see cref="chat"/>.</param>
|
||||
/// <param name="chatTrackingContext">The value of <see cref="chatTrackingContext"/>.</param>
|
||||
/// <param name="assemblyInformationProvider">The <see cref="IAssemblyInformationProvider"/> for the <see cref="SessionController"/>.</param>
|
||||
/// <param name="logger">The value of <see cref="BridgeRequestChunker.Logger"/>.</param>
|
||||
/// <param name="logger">The value of <see cref="Chunker.Logger"/>.</param>
|
||||
/// <param name="postLifetimeCallback">The <see cref="Func{TResult}"/> returning a <see cref="Task"/> to be run after the <paramref name="process"/> ends.</param>
|
||||
/// <param name="startupTimeout">The optional time to wait before failing the <see cref="LaunchResult"/>.</param>
|
||||
/// <param name="reattached">If this is a reattached session.</param>
|
||||
@@ -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
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public override async Task<BridgeResponse> ProcessBridgeRequest(BridgeParameters parameters, CancellationToken cancellationToken)
|
||||
public async Task<BridgeResponse> ProcessBridgeRequest(BridgeParameters parameters, CancellationToken cancellationToken)
|
||||
{
|
||||
if (parameters == null)
|
||||
throw new ArgumentNullException(nameof(parameters));
|
||||
@@ -319,7 +321,7 @@ namespace Tgstation.Server.Host.Components.Session
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public async Task<CombinedTopicResponse> SendCommand(TopicParameters parameters, CancellationToken cancellationToken)
|
||||
public async Task<TopicResponse> 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<Interop.Topic.TopicResponse>(topicReturn, DMApiConstants.SerializerSettings);
|
||||
if (interopResponse.ErrorMessage != null)
|
||||
var nextRequest = await ProcessChunk<TopicResponse, ChunkedTopicParameters>(
|
||||
(completedResponse, cancellationToken) =>
|
||||
{
|
||||
fullResponse = completedResponse;
|
||||
return Task.FromResult<ChunkedTopicParameters>(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;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
@@ -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<BridgeParameters, BridgeResponse>(ProcessBridgeRequest, BridgeError, parameters.Chunk, cancellationToken);
|
||||
case null:
|
||||
return BridgeError("Missing commandType!");
|
||||
default:
|
||||
@@ -707,5 +720,198 @@ namespace Tgstation.Server.Host.Components.Session
|
||||
|
||||
return response;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Log and return a <see cref="BridgeResponse"/> for a given <paramref name="message"/>.
|
||||
/// </summary>
|
||||
/// <param name="message">The error message.</param>
|
||||
/// <returns>A new errored <see cref="BridgeResponse"/>.</returns>
|
||||
BridgeResponse BridgeError(string message)
|
||||
{
|
||||
Logger.LogWarning("Bridge request chunking error: {message}", message);
|
||||
return new BridgeResponse
|
||||
{
|
||||
ErrorMessage = message,
|
||||
};
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Send a topic request for given <paramref name="parameters"/> to DreamDaemon, chunking it if necessary.
|
||||
/// </summary>
|
||||
/// <param name="parameters">The <see cref="TopicParameters"/> to send.</param>
|
||||
/// <param name="cancellationToken">The <see cref="CancellationToken"/> for the operation.</param>
|
||||
/// <returns>A <see cref="Task{TResult}"/> resulting in the <see cref="CombinedTopicResponse"/> of the topic request.</returns>
|
||||
async Task<CombinedTopicResponse> 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<string> 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<string>();
|
||||
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;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Generates a <see cref="global::Byond.TopicSender.ITopicClient"/> query string for a given set of <paramref name="parameters"/>.
|
||||
/// </summary>
|
||||
/// <param name="parameters">The <see cref="TopicParameters"/> to serialize.</param>
|
||||
/// <param name="json">The intermediate JSON <see cref="string"/> prior to URL encoding.</param>
|
||||
/// <returns>The <see cref="global::Byond.TopicSender.ITopicClient"/> query string for the given <paramref name="parameters"/>.</returns>
|
||||
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;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Send a given <paramref name="queryString"/> to DreamDaemon's /world/Topic.
|
||||
/// </summary>
|
||||
/// <param name="queryString">The sanitized topic query string to send.</param>
|
||||
/// <param name="cancellationToken">The <see cref="CancellationToken"/> for the operation.</param>
|
||||
/// <returns>A <see cref="Task{TResult}"/> resulting in the <see cref="CombinedTopicResponse"/> of the topic request.</returns>
|
||||
async Task<CombinedTopicResponse> 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<TopicResponse>(topicReturn, DMApiConstants.SerializerSettings);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Logger.LogWarning(ex, "Invalid interop response: {topicReturnString}", topicReturn);
|
||||
}
|
||||
|
||||
return new CombinedTopicResponse(byondResponse, interopResponse);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
/// <summary>
|
||||
/// Handle any <see cref="TopicResponse.ChatResponses"/> in a given topic <paramref name="result"/>.
|
||||
/// </summary>
|
||||
/// <param name="result">The <see cref="CombinedTopicResponse"/>.</param>
|
||||
void HandleChatResponses(CombinedTopicResponse result)
|
||||
/// <param name="result">The <see cref="TopicResponse"/>.</param>
|
||||
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
|
||||
|
||||
@@ -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<TestBridgeHandler> logger, ushort serverPort)
|
||||
: base(logger)
|
||||
{
|
||||
@@ -41,13 +41,28 @@ namespace Tgstation.Server.Tests.Instance
|
||||
this.serverPort = serverPort;
|
||||
}
|
||||
|
||||
public override async Task<BridgeResponse> ProcessBridgeRequest(BridgeParameters parameters, CancellationToken cancellationToken)
|
||||
public async Task<BridgeResponse> 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<BridgeParameters, BridgeResponse>(
|
||||
(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]);
|
||||
|
||||
|
||||
@@ -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}
|
||||
|
||||
Reference in New Issue
Block a user