diff --git a/.github/CONTRIBUTING.md b/.github/CONTRIBUTING.md index fd77c8cb69..91a2e52742 100644 --- a/.github/CONTRIBUTING.md +++ b/.github/CONTRIBUTING.md @@ -132,6 +132,18 @@ This prevents nesting levels from getting deeper then they need to be. * You are expected to help maintain the code that you add, meaning that if there is a problem then you are likely to be approached in order to fix any issues, runtimes, or bugs. +* Some terminology to help understand the architecture: + * An instance can be thought of as a separate server. It has a separate directory, repository, set of byond installations, etc... The only thing shared amongst instances is API surface, users, global configuration, the active tgstation-server version, and the host machine. + * API refers to the HTTP API unless otherwise specified. + * The entirety of server functionality resides in the host (Tgstation.Server.Host) project. + * A Component is a service running in tgstation-server to help with instance functionality. These can only be communicated with via the HTTP or DM APIs. + * There is a difference between Watchdog and Host Watchdog. The former monitors DreamDaemon uptime, the latter handles updating tgstation-server. + * Interop is complicated terminology wise: + * Interop: The overall process of communication between tgstation-server and DreamDaemon. + * DMAPI: The tgstation-server provided code compiled into .dmbs to provide additional functionality. + * Topic: The process of sending a message from the TGS -> DD via /world/Topic() and receiving a response. + * Bridge: The process of sending a message from DD -> TGS and receiving a response. + ## Pull Request Process There is no strict process when it comes to merging pull requests. Pull requests will sometimes take a while before they are looked at by a maintainer; the bigger the change, the more time it will take before they are accepted into the code. Every team member is a volunteer who is giving up their own time to help maintain and contribute, so please be courteous and respectful. Here are some helpful ways to make it easier for you and for the maintainers when making a pull request. diff --git a/src/DMAPI/tgs/includes.dm b/src/DMAPI/tgs/includes.dm index 247f1fda5d..23fe376bf6 100644 --- a/src/DMAPI/tgs/includes.dm +++ b/src/DMAPI/tgs/includes.dm @@ -8,3 +8,5 @@ #endif #include "v4\api.dm" #include "v4\commands.dm" +#include "v5\api.dm" +#include "v5\commands.dm" diff --git a/src/DMAPI/tgs/v5/_defines.dm b/src/DMAPI/tgs/v5/_defines.dm new file mode 100644 index 0000000000..925b5856fd --- /dev/null +++ b/src/DMAPI/tgs/v5/_defines.dm @@ -0,0 +1,19 @@ +#define DMAPI5_PARAM_DEPLOYMENT_INFORMATION_FILE "tgs_json" +#define DMAPI5_TOPIC_DATA "data" + +#define DMAPI5_BRIDGE_COMMAND_NEW_PORT 0 +#define DMAPI5_BRIDGE_COMMAND_VALIDATE 1 +#define DMAPI5_BRIDGE_COMMAND_PRIME 2 +#define DMAPI5_BRIDGE_COMMAND_REBOOT 3 +#define DMAPI5_BRIDGE_COMMAND_KILL 4 +#define DMAPI5_BRIDGE_COMMAND_CHAT_SEND 5 + +#define DMAPI5_BRIDGE_PARAMETER_COMMAND "commandType" +#define DMAPI5_BRIDGE_PARAMETER_NEW_PORT "newPort" +#define DMAPI5_BRIDGE_PARAMETER_VERSION "version" +#define DMAPI5_BRIDGE_PARAMETER_CHAT_MESSAGE "chatMessage" +#define DMAPI5_BRIDGE_PARAMETER_MINIMUM_SECURITY_LEVEL "minimumSecurityLevel" + +#define DMAPI5_BRIDGE_RESPONSE_ERROR_MESSAGE "errorMessage" +#define DMAPI5_BRIDGE_RESPONSE_ERROR_NEW_PORT "newPort" +#de \ No newline at end of file diff --git a/src/DMAPI/tgs/v5/api.dm b/src/DMAPI/tgs/v5/api.dm new file mode 100644 index 0000000000..96a037644a --- /dev/null +++ b/src/DMAPI/tgs/v5/api.dm @@ -0,0 +1,341 @@ +#define TGS4_PARAM_DEPLOYMENT_INFORMATION_FILE "tgs_json" +#define TGS4_TOPIC_DATA "data" + +#define TGS4_INTEROP_ACCESS_IDENTIFIER "tgs_tok" + +#define TGS4_RESPONSE_SUCCESS "tgs_succ" + +#define TGS4_TOPIC_CHANGE_PORT "tgs_port" +#define TGS4_TOPIC_CHANGE_REBOOT_MODE "tgs_rmode" +#define TGS4_TOPIC_CHAT_COMMAND "tgs_chat_comm" +#define TGS4_TOPIC_EVENT "tgs_event" +#define TGS4_TOPIC_INTEROP_RESPONSE "tgs_interop" + +#define TGS4_COMM_NEW_PORT "tgs_new_port" +#define TGS4_COMM_VALIDATE "tgs_validate" +#define TGS4_COMM_SERVER_PRIMED "tgs_prime" +#define TGS4_COMM_WORLD_REBOOT "tgs_reboot" +#define TGS4_COMM_END_PROCESS "tgs_kill" +#define TGS4_COMM_CHAT "tgs_chat_send" + +#define TGS4_PARAMETER_COMMAND "tgs_com" +#define TGS4_PARAMETER_DATA "tgs_data" + +#define TGS4_PORT_CRITFAIL_MESSAGE " Must exit to let watchdog reboot..." + +#define EXPORT_TIMEOUT_DS 200 + +/datum/tgs_api/v5 + var/access_identifier + var/instance_name + var/json_path + var/chat_channels_json_path + var/chat_commands_json_path + var/reboot_mode = TGS_REBOOT_MODE_NORMAL + var/security_level + + var/requesting_new_port = FALSE + + var/list/intercepted_message_queue + + var/list/custom_commands + + var/list/cached_test_merges + var/datum/tgs_revision_information/cached_revision + + var/datum/tgs_event_handler/event_handler + + var/export_lock = FALSE + +/datum/tgs_api/v4/ApiVersion() + return "5.0.0" + +/datum/tgs_api/v4/OnWorldNew(datum/tgs_event_handler/event_handler, minimum_required_security_level) + json_path = world.params[TGS4_PARAM_INFO_JSON] + if(!json_path) + TGS_ERROR_LOG("Missing [TGS4_PARAM_INFO_JSON] world parameter!") + return + var/json_file = file2text(json_path) + if(!json_file) + TGS_ERROR_LOG("Missing specified json file: [json_path]") + return + var/cached_json = json_decode(json_file) + if(!cached_json) + TGS_ERROR_LOG("Failed to decode info json: [json_file]") + return + + access_identifier = cached_json["accessIdentifier"] + server_commands_json_path = cached_json["serverCommandsJson"] + + if(cached_json["apiValidateOnly"]) + TGS_INFO_LOG("Validating API and exiting...") + Export(TGS4_COMM_VALIDATE, list(TGS4_PARAMETER_DATA = "[minimum_required_security_level]")) + del(world) + + security_level = cached_json["securityLevel"] + chat_channels_json_path = cached_json["chatChannelsJson"] + chat_commands_json_path = cached_json["chatCommandsJson"] + src.event_handler = event_handler + instance_name = cached_json["instanceName"] + + ListCustomCommands() + + var/list/revisionData = cached_json["revision"] + if(revisionData) + cached_revision = new + cached_revision.commit = revisionData["commitSha"] + cached_revision.origin_commit = revisionData["originCommitSha"] + + cached_test_merges = list() + var/list/json = cached_json["testMerges"] + for(var/entry in json) + var/datum/tgs_revision_information/test_merge/tm = new + tm.time_merged = text2num(entry["timeMerged"]) + + var/list/revInfo = entry["revision"] + if(revInfo) + tm.commit = revInfo["commitSha"] + tm.origin_commit = revInfo["originCommitSha"] + + tm.title = entry["titleAtMerge"] + tm.body = entry["bodyAtMerge"] + tm.url = entry["url"] + tm.author = entry["author"] + tm.number = entry["number"] + tm.pull_request_commit = entry["pullRequestRevision"] + tm.comment = entry["comment"] + + cached_test_merges += tm + + return TRUE + +/datum/tgs_api/v4/OnInitializationComplete() + Export(TGS4_COMM_SERVER_PRIMED) + + var/tgs4_secret_sleep_offline_sauce = 24051994 + var/old_sleep_offline = world.sleep_offline + world.sleep_offline = tgs4_secret_sleep_offline_sauce + sleep(1) + if(world.sleep_offline == tgs4_secret_sleep_offline_sauce) //if not someone changed it + world.sleep_offline = old_sleep_offline + +/datum/tgs_api/v4/OnTopic(T) + var/list/params = params2list(T) + var/their_sCK = params[TGS4_INTEROP_ACCESS_IDENTIFIER] + if(!their_sCK) + return FALSE //continue world/Topic + + if(their_sCK != access_identifier) + return "Invalid comms key!"; + + var/command = params[TGS4_PARAMETER_COMMAND] + if(!command) + return "No command!" + + . = TGS4_RESPONSE_SUCCESS + + switch(command) + if(TGS4_TOPIC_CHAT_COMMAND) + var/result = HandleCustomCommand(params[TGS4_PARAMETER_DATA]) + if(result == null) + result = "Error running chat command!" + return result + if(TGS4_TOPIC_EVENT) + intercepted_message_queue = list() + var/list/event_notification = json_decode(params[TGS4_PARAMETER_DATA]) + var/list/event_parameters = event_notification["Parameters"] + + var/list/event_call = list(event_notification["Type"]) + if(event_parameters) + event_call += event_parameters + + if(event_handler != null) + event_handler.HandleEvent(arglist(event_call)) + + . = json_encode(intercepted_message_queue) + intercepted_message_queue = null + return + if(TGS4_TOPIC_INTEROP_RESPONSE) + last_interop_response = json_decode(params[TGS4_PARAMETER_DATA]) + return + if(TGS4_TOPIC_CHANGE_PORT) + var/new_port = text2num(params[TGS4_PARAMETER_DATA]) + if (!(new_port > 0)) + return "Invalid port: [new_port]" + + //the topic still completes, miraculously + //I honestly didn't believe byond could do it + if(event_handler != null) + event_handler.HandleEvent(TGS_EVENT_PORT_SWAP, new_port) + if(!world.OpenPort(new_port)) + return "Port change failed!" + return + if(TGS4_TOPIC_CHANGE_REBOOT_MODE) + var/new_reboot_mode = text2num(params[TGS4_PARAMETER_DATA]) + if(event_handler != null) + event_handler.HandleEvent(TGS_EVENT_REBOOT_MODE_CHANGE, reboot_mode, new_reboot_mode) + reboot_mode = new_reboot_mode + return + + return "Unknown command: [command]" + +/datum/tgs_api/v4/proc/Export(command, list/data, override_requesting_new_port = FALSE) + if(!data) + data = list() + data[TGS4_PARAMETER_COMMAND] = command + var/json = json_encode(data) + + while(requesting_new_port && !override_requesting_new_port) + sleep(1) + + //we need some port open at this point to facilitate return communication + if(!world.port) + requesting_new_port = TRUE + if(!world.OpenPort(0)) //open any port + TGS_ERROR_LOG("Unable to open random port to retrieve new port![TGS4_PORT_CRITFAIL_MESSAGE]") + del(world) + + //request a new port + export_lock = FALSE + var/list/new_port_json = Export(TGS4_COMM_NEW_PORT, list(TGS4_PARAMETER_DATA = "[world.port]"), TRUE) //stringify this on purpose + + if(!new_port_json) + TGS_ERROR_LOG("No new port response from server![TGS4_PORT_CRITFAIL_MESSAGE]") + del(world) + + var/new_port = new_port_json[TGS4_PARAMETER_DATA] + if(!isnum(new_port) || new_port <= 0) + TGS_ERROR_LOG("Malformed new port json ([json_encode(new_port_json)])![TGS4_PORT_CRITFAIL_MESSAGE]") + del(world) + + if(new_port != world.port && !world.OpenPort(new_port)) + TGS_ERROR_LOG("Unable to open port [new_port]![TGS4_PORT_CRITFAIL_MESSAGE]") + del(world) + requesting_new_port = FALSE + + while(export_lock) + sleep(1) + export_lock = TRUE + + last_interop_response = null + fdel(server_commands_json_path) + text2file(json, server_commands_json_path) + + for(var/I = 0; I < EXPORT_TIMEOUT_DS && !last_interop_response; ++I) + sleep(1) + + if(!last_interop_response) + TGS_ERROR_LOG("Failed to get export result for: [json]") + else + . = last_interop_response + + export_lock = FALSE + +/datum/tgs_api/v4/OnReboot() + var/list/result = Export(TGS4_COMM_WORLD_REBOOT) + if(!result) + return + + //okay so the standard TGS4 proceedure is: right before rebooting change the port to whatever was sent to us in the above json's data parameter + + var/port = result[TGS4_PARAMETER_DATA] + if(!isnum(port)) + return //this is valid, server may just want use to reboot + + if(port == 0) + //to byond 0 means any port and "none" means close vOv + port = "none" + + if(!world.OpenPort(port)) + TGS_ERROR_LOG("Unable to set port to [port]!") + +/datum/tgs_api/v4/InstanceName() + return instance_name + +/datum/tgs_api/v4/TestMerges() + return cached_test_merges + +/datum/tgs_api/v4/EndProcess() + Export(TGS4_COMM_END_PROCESS) + +/datum/tgs_api/v4/Revision() + return cached_revision + +/datum/tgs_api/v4/ChatBroadcast(message, list/channels) + var/list/ids + if(length(channels)) + ids = list() + for(var/I in channels) + var/datum/tgs_chat_channel/channel = I + ids += channel.id + message = list("message" = message, "channelIds" = ids) + if(intercepted_message_queue) + intercepted_message_queue += list(message) + else + Export(TGS4_COMM_CHAT, message) + +/datum/tgs_api/v4/ChatTargetedBroadcast(message, admin_only) + var/list/channels = list() + for(var/I in ChatChannelInfo()) + var/datum/tgs_chat_channel/channel = I + if (!channel.is_private_channel && ((channel.is_admin_channel && admin_only) || (!channel.is_admin_channel && !admin_only))) + channels += channel.id + message = list("message" = message, "channelIds" = channels) + if(intercepted_message_queue) + intercepted_message_queue += list(message) + else + Export(TGS4_COMM_CHAT, message) + +/datum/tgs_api/v4/ChatPrivateMessage(message, datum/tgs_chat_user/user) + message = list("message" = message, "channelIds" = list(user.channel.id)) + if(intercepted_message_queue) + intercepted_message_queue += list(message) + else + Export(TGS4_COMM_CHAT, message) + +/datum/tgs_api/v4/ChatChannelInfo() + . = list() + //no caching cause tgs may change this + var/list/json = json_decode(file2text(chat_channels_json_path)) + for(var/I in json) + . += DecodeChannel(I) + +/datum/tgs_api/v4/proc/DecodeChannel(channel_json) + var/datum/tgs_chat_channel/channel = new + channel.id = channel_json["id"] + channel.friendly_name = channel_json["friendlyName"] + channel.connection_name = channel_json["connectionName"] + channel.is_admin_channel = channel_json["isAdminChannel"] + channel.is_private_channel = channel_json["isPrivateChannel"] + channel.custom_tag = channel_json["tag"] + return channel + +/datum/tgs_api/v4/SecurityLevel() + return security_level + +/* +The MIT License + +Copyright (c) 2017 Jordan Brown + +Permission is hereby granted, free of charge, +to any person obtaining a copy of this software and +associated documentation files (the "Software"), to +deal in the Software without restriction, including +without limitation the rights to use, copy, modify, +merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom +the Software is furnished to do so, +subject to the following conditions: + +The above copyright notice and this permission notice +shall be included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES +OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. +IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR +ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, +TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE +SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. +*/ diff --git a/src/DMAPI/tgs/v5/commands.dm b/src/DMAPI/tgs/v5/commands.dm new file mode 100644 index 0000000000..1d9951bc04 --- /dev/null +++ b/src/DMAPI/tgs/v5/commands.dm @@ -0,0 +1,69 @@ +/datum/tgs_api/v4/proc/ListCustomCommands() + var/results = list() + custom_commands = list() + for(var/I in typesof(/datum/tgs_chat_command) - /datum/tgs_chat_command) + var/datum/tgs_chat_command/stc = new I + var/command_name = stc.name + if(!command_name || findtext(command_name, " ") || findtext(command_name, "'") || findtext(command_name, "\"")) + TGS_ERROR_LOG("Custom command [command_name] ([I]) can't be used as it is empty or contains illegal characters!") + continue + + if(results[command_name]) + var/datum/other = custom_commands[command_name] + TGS_ERROR_LOG("Custom commands [other.type] and [I] have the same name (\"[command_name]\"), only [other.type] will be available!") + continue + results += list(list("name" = command_name, "help_text" = stc.help_text, "admin_only" = stc.admin_only)) + custom_commands[command_name] = stc + + var/commands_file = chat_commands_json_path + if(!commands_file) + return + text2file(json_encode(results), commands_file) + +/datum/tgs_api/v4/proc/HandleCustomCommand(command_json) + var/list/data = json_decode(command_json) + var/command = data["command"] + var/user = data["user"] + var/params = data["params"] + + var/datum/tgs_chat_user/u = new + u.id = user["id"] + u.friendly_name = user["friendlyName"] + u.mention = user["mention"] + u.channel = DecodeChannel(user["channel"]) + + var/datum/tgs_chat_command/sc = custom_commands[command] + if(sc) + var/result = sc.Run(u, params) + if(result == null) + result = "" + return result + return "Unknown command: [command]!" + +/* + +The MIT License + +Copyright (c) 2017 Jordan Brown + +Permission is hereby granted, free of charge, +to any person obtaining a copy of this software and +associated documentation files (the "Software"), to +deal in the Software without restriction, including +without limitation the rights to use, copy, modify, +merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom +the Software is furnished to do so, +subject to the following conditions: + +The above copyright notice and this permission notice +shall be included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES +OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. +IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR +ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, +TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE +SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. +*/ diff --git a/src/Tgstation.Server.Api/Models/Internal/CompileJob.cs b/src/Tgstation.Server.Api/Models/Internal/CompileJob.cs index 204657b883..97d9aaaf5c 100644 --- a/src/Tgstation.Server.Api/Models/Internal/CompileJob.cs +++ b/src/Tgstation.Server.Api/Models/Internal/CompileJob.cs @@ -31,5 +31,10 @@ namespace Tgstation.Server.Api.Models.Internal /// [Required] public DreamDaemonSecurity? MinimumSecurityLevel { get; set; } + + /// + /// The DMAPI . + /// + public Version DMApiVersion { get; set; } } diff --git a/src/Tgstation.Server.Host/Components/Chat/JsonTrackingContext.cs b/src/Tgstation.Server.Host/Components/Chat/JsonTrackingContext.cs index e76fdec0dd..4fa03f5570 100644 --- a/src/Tgstation.Server.Host/Components/Chat/JsonTrackingContext.cs +++ b/src/Tgstation.Server.Host/Components/Chat/JsonTrackingContext.cs @@ -7,6 +7,7 @@ using System.Text; using System.Threading; using System.Threading.Tasks; using Tgstation.Server.Host.Components.Chat.Commands; +using Tgstation.Server.Host.Components.Interop; using Tgstation.Server.Host.Core; using Tgstation.Server.Host.IO; @@ -80,13 +81,7 @@ namespace Tgstation.Server.Host.Components.Chat var resultBytes = await ioManager.ReadAllBytes(commandsPath, cancellationToken).ConfigureAwait(false); var resultJson = Encoding.UTF8.GetString(resultBytes); logger.LogTrace("Read commands JSON: {0}", resultJson); - var result = JsonConvert.DeserializeObject>(resultJson, new JsonSerializerSettings - { - ContractResolver = new DefaultContractResolver - { - NamingStrategy = new SnakeCaseNamingStrategy() - } - }); + var result = JsonConvert.DeserializeObject>(resultJson, DMApiConstants.SerializerSettings); foreach (var I in result) I.SetHandler(customCommandHandler); return result; @@ -109,10 +104,7 @@ namespace Tgstation.Server.Host.Components.Chat { using (await SemaphoreSlimContext.Lock(channelsSemaphore, cancellationToken).ConfigureAwait(false)) { - var json = JsonConvert.SerializeObject(channels, new JsonSerializerSettings - { - ContractResolver = new CamelCasePropertyNamesContractResolver() - }); + var json = JsonConvert.SerializeObject(channels, DMApiConstants.SerializerSettings); logger.LogTrace("Writing channels JSON: {0}", json); await ioManager.WriteAllBytes(channelsPath, Encoding.UTF8.GetBytes(json), cancellationToken).ConfigureAwait(false); } diff --git a/src/Tgstation.Server.Host/Components/Deployment/DreamMaker.cs b/src/Tgstation.Server.Host/Components/Deployment/DreamMaker.cs index e93de15c20..6156844e19 100644 --- a/src/Tgstation.Server.Host/Components/Deployment/DreamMaker.cs +++ b/src/Tgstation.Server.Host/Components/Deployment/DreamMaker.cs @@ -176,7 +176,7 @@ namespace Tgstation.Server.Host.Components.Deployment var timeoutAt = DateTimeOffset.Now.AddSeconds(timeout); using (var provider = new TemporaryDmbProvider(ioManager.ResolvePath(dirA), String.Concat(job.DmeName, DmbExtension), job)) - using (var controller = await sessionControllerFactory.LaunchNew(launchParameters, provider, byondLock, true, true, true, cancellationToken).ConfigureAwait(false)) + using (var controller = await sessionControllerFactory.LaunchNew(provider, byondLock, launchParameters, true, true, true, cancellationToken).ConfigureAwait(false)) { var launchResult = await controller.LaunchResult.ConfigureAwait(false); diff --git a/src/Tgstation.Server.Host/Components/IInstanceFactory.cs b/src/Tgstation.Server.Host/Components/IInstanceFactory.cs index 8095d074d1..b6f08c05d9 100644 --- a/src/Tgstation.Server.Host/Components/IInstanceFactory.cs +++ b/src/Tgstation.Server.Host/Components/IInstanceFactory.cs @@ -1,4 +1,5 @@ using Microsoft.Extensions.Hosting; +using Tgstation.Server.Host.Components.Interop; namespace Tgstation.Server.Host.Components { @@ -10,8 +11,9 @@ namespace Tgstation.Server.Host.Components /// /// Create an /// + /// The to use. /// The /// A new - IInstance CreateInstance(Models.Instance metadata); + IInstance CreateInstance(IBridgeRegistrar bridgeRegistrar, Models.Instance metadata); } } \ No newline at end of file diff --git a/src/Tgstation.Server.Host/Components/IInstanceManager.cs b/src/Tgstation.Server.Host/Components/IInstanceManager.cs index 186a2ac25a..aad177b67b 100644 --- a/src/Tgstation.Server.Host/Components/IInstanceManager.cs +++ b/src/Tgstation.Server.Host/Components/IInstanceManager.cs @@ -1,5 +1,6 @@ using System.Threading; using System.Threading.Tasks; +using Tgstation.Server.Host.Components.Interop; using Tgstation.Server.Host.Models; namespace Tgstation.Server.Host.Components @@ -7,7 +8,7 @@ namespace Tgstation.Server.Host.Components /// /// For managing s /// - public interface IInstanceManager + public interface IInstanceManager : IBridgeHandlerBase { /// /// Get the associated with given diff --git a/src/Tgstation.Server.Host/Components/InstanceFactory.cs b/src/Tgstation.Server.Host/Components/InstanceFactory.cs index c8774659b6..ed3ceffea5 100644 --- a/src/Tgstation.Server.Host/Components/InstanceFactory.cs +++ b/src/Tgstation.Server.Host/Components/InstanceFactory.cs @@ -7,6 +7,7 @@ using Tgstation.Server.Host.Components.Byond; using Tgstation.Server.Host.Components.Chat; using Tgstation.Server.Host.Components.Chat.Commands; using Tgstation.Server.Host.Components.Deployment; +using Tgstation.Server.Host.Components.Interop; using Tgstation.Server.Host.Components.Repository; using Tgstation.Server.Host.Components.Watchdog; using Tgstation.Server.Host.Core; @@ -174,7 +175,7 @@ namespace Tgstation.Server.Host.Components /// #pragma warning disable CA1506 // TODO: Decomplexify - public IInstance CreateInstance(Models.Instance metadata) + public IInstance CreateInstance(IBridgeRegistrar bridgeRegistrar, Models.Instance metadata) { // Create the ioManager for the instance var instanceIoManager = new ResolvingIOManager(ioManager, metadata.Path); @@ -203,7 +204,19 @@ namespace Tgstation.Server.Host.Components var chat = chatFactory.CreateChat(instanceIoManager, commandFactory, metadata.ChatSettings); try { - var sessionControllerFactory = new SessionControllerFactory(processExecutor, byond, byondTopicSender, cryptographySuite, application, gameIoManager, chat, networkPromptReaper, platformIdentifier, loggerFactory, metadata.CloneMetadata()); + var sessionControllerFactory = new SessionControllerFactory( + processExecutor, + byond, + byondTopicSender, + cryptographySuite, + application, + gameIoManager, + chat, + networkPromptReaper, + platformIdentifier, + bridgeRegistrar, + loggerFactory, + metadata.CloneMetadata()); var dmbFactory = new DmbFactory(databaseContextFactory, gameIoManager, loggerFactory.CreateLogger(), metadata.CloneMetadata()); try diff --git a/src/Tgstation.Server.Host/Components/InstanceManager.cs b/src/Tgstation.Server.Host/Components/InstanceManager.cs index 07bd7aa148..50f79bb661 100644 --- a/src/Tgstation.Server.Host/Components/InstanceManager.cs +++ b/src/Tgstation.Server.Host/Components/InstanceManager.cs @@ -6,17 +6,18 @@ using System.Collections.Generic; using System.Linq; using System.Threading; using System.Threading.Tasks; +using Tgstation.Server.Host.Components.Interop; +using Tgstation.Server.Host.Components.Interop.Bridge; using Tgstation.Server.Host.Core; using Tgstation.Server.Host.Database; using Tgstation.Server.Host.IO; using Tgstation.Server.Host.Jobs; using Tgstation.Server.Host.Security; -using Tgstation.Server.Host.System; namespace Tgstation.Server.Host.Components { /// - sealed class InstanceManager : IInstanceManager, IRestartHandler, IHostedService, IDisposable + sealed class InstanceManager : IInstanceManager, IRestartHandler, IHostedService, IBridgeRegistrar, IDisposable { /// /// The for the @@ -48,11 +49,6 @@ namespace Tgstation.Server.Host.Components /// readonly IServerControl serverControl; - /// - /// The for the - /// - readonly IPlatformIdentifier platformIdentifier; - /// /// The for the /// @@ -66,7 +62,12 @@ namespace Tgstation.Server.Host.Components /// /// Map of s to respective s /// - readonly Dictionary instances; + readonly IDictionary instances; + + /// + /// Map of s to their respective s. + /// + readonly IDictionary bridgeHandlers; /// /// Used in to determine if database downgrades must be made @@ -87,7 +88,6 @@ namespace Tgstation.Server.Host.Components /// The value of /// The value of /// The value of - /// The value of . /// The value of . /// The value of public InstanceManager( @@ -97,7 +97,6 @@ namespace Tgstation.Server.Host.Components IApplication application, IJobManager jobManager, IServerControl serverControl, - IPlatformIdentifier platformIdentifier, ISystemIdentityFactory systemIdentityFactory, ILogger logger) { @@ -107,13 +106,13 @@ namespace Tgstation.Server.Host.Components this.application = application ?? throw new ArgumentNullException(nameof(application)); this.jobManager = jobManager ?? throw new ArgumentNullException(nameof(jobManager)); this.serverControl = serverControl ?? throw new ArgumentNullException(nameof(serverControl)); - this.platformIdentifier = platformIdentifier ?? throw new ArgumentNullException(nameof(platformIdentifier)); this.systemIdentityFactory = systemIdentityFactory ?? throw new ArgumentNullException(nameof(systemIdentityFactory)); this.logger = logger ?? throw new ArgumentNullException(nameof(logger)); serverControl.RegisterForRestart(this); instances = new Dictionary(); + bridgeHandlers = new Dictionary(); } /// @@ -212,7 +211,7 @@ namespace Tgstation.Server.Host.Components if (metadata == null) throw new ArgumentNullException(nameof(metadata)); logger.LogInformation("Onlining instance ID {0} ({1}) at {2}", metadata.Id, metadata.Name, metadata.Path); - var instance = instanceFactory.CreateInstance(metadata); + var instance = instanceFactory.CreateInstance(this, metadata); try { lock (this) @@ -302,5 +301,39 @@ namespace Tgstation.Server.Host.Components if (!systemIdentity.CanCreateSymlinks) throw new InvalidOperationException("The user running tgstation-server cannot create symlinks! Please try running as an administrative user!"); } + + /// + public async Task ProcessBridgeRequest(BridgeParameters parameters, CancellationToken cancellationToken) + { + if (parameters == null) + throw new ArgumentNullException(nameof(parameters)); + + IBridgeHandler bridgeHandler; + lock (bridgeHandlers) + if (!bridgeHandlers.TryGetValue(parameters.AccessIdentifier, out bridgeHandler)) + { + logger.LogWarning("Recieved invalid bridge request with accees identifier: {0}", parameters.AccessIdentifier); + return null; + } + + return await bridgeHandler.ProcessBridgeRequest(parameters, cancellationToken).ConfigureAwait(false); + } + + /// + public IBridgeRegistration RegisterHandler(IBridgeHandler bridgeHandler) + { + if (bridgeHandler == null) + throw new ArgumentNullException(nameof(bridgeHandler)); + + var accessIdentifier = bridgeHandler.AccessIdentifier; + lock (bridgeHandlers) + bridgeHandlers.Add(accessIdentifier, bridgeHandler); + + return new BridgeRegistration(() => + { + lock (bridgeHandlers) + bridgeHandlers.Remove(accessIdentifier); + }); + } } } diff --git a/src/Tgstation.Server.Host/Components/Interop/Bridge/BridgeCommandType.cs b/src/Tgstation.Server.Host/Components/Interop/Bridge/BridgeCommandType.cs new file mode 100644 index 0000000000..1736fc5b8d --- /dev/null +++ b/src/Tgstation.Server.Host/Components/Interop/Bridge/BridgeCommandType.cs @@ -0,0 +1,12 @@ +namespace Tgstation.Server.Host.Components.Interop.Bridge +{ + public enum BridgeCommandType + { + NewPort, + Validate, + Prime, + Reboot, + Kill, + ChatSend + } +} diff --git a/src/Tgstation.Server.Host/Components/Interop/Bridge/BridgeParameters.cs b/src/Tgstation.Server.Host/Components/Interop/Bridge/BridgeParameters.cs new file mode 100644 index 0000000000..7d84cd8fc0 --- /dev/null +++ b/src/Tgstation.Server.Host/Components/Interop/Bridge/BridgeParameters.cs @@ -0,0 +1,18 @@ +using System; +using Tgstation.Server.Api.Models; + +namespace Tgstation.Server.Host.Components.Interop.Bridge +{ + public sealed class BridgeParameters : DMApiParameters + { + public BridgeCommandType? CommandType { get; set; } + + public ushort? NewPort { get; set; } + + public Version Version { get; set; } + + public ChatMessage ChatMessage { get; set; } + + public DreamDaemonSecurity? MinimumSecurityLevel { get; set; } + } +} diff --git a/src/Tgstation.Server.Host/Components/Interop/Bridge/BridgeResponse.cs b/src/Tgstation.Server.Host/Components/Interop/Bridge/BridgeResponse.cs new file mode 100644 index 0000000000..50c231f3e1 --- /dev/null +++ b/src/Tgstation.Server.Host/Components/Interop/Bridge/BridgeResponse.cs @@ -0,0 +1,8 @@ +namespace Tgstation.Server.Host.Components.Interop.Bridge +{ + public sealed class BridgeResponse + { + public string ErrorMessage { get; set; } + public ushort? NewPort { get; set; } + } +} diff --git a/src/Tgstation.Server.Host/Components/Interop/Bridge/ChatMessage.cs b/src/Tgstation.Server.Host/Components/Interop/Bridge/ChatMessage.cs new file mode 100644 index 0000000000..8fbe6006f5 --- /dev/null +++ b/src/Tgstation.Server.Host/Components/Interop/Bridge/ChatMessage.cs @@ -0,0 +1,11 @@ +using System.Collections.Generic; + +namespace Tgstation.Server.Host.Components.Interop.Bridge +{ + public sealed class ChatMessage + { + public string Message { get; set; } + + public ICollection ChannelIds { get; set; } + } +} \ No newline at end of file diff --git a/src/Tgstation.Server.Host/Components/Interop/BridgeRegistration.cs b/src/Tgstation.Server.Host/Components/Interop/BridgeRegistration.cs new file mode 100644 index 0000000000..54037a40f8 --- /dev/null +++ b/src/Tgstation.Server.Host/Components/Interop/BridgeRegistration.cs @@ -0,0 +1,38 @@ +using System; + +namespace Tgstation.Server.Host.Components.Interop +{ + /// + sealed class BridgeRegistration : IBridgeRegistration + { + /// + /// for accessing . + /// + readonly object lockObject; + + /// + /// to run when d. + /// + Action onDispose; + + /// + /// Initializes a new instance of the . + /// + /// The value of . + public BridgeRegistration(Action onDispose) + { + this.onDispose = onDispose ?? throw new ArgumentNullException(nameof(onDispose)); + lockObject = new object(); + } + + /// + public void Dispose() + { + lock(lockObject) + { + onDispose?.Invoke(); + onDispose = null; + } + } + } +} diff --git a/src/Tgstation.Server.Host/Components/Interop/ChatCommand.cs b/src/Tgstation.Server.Host/Components/Interop/ChatCommand.cs deleted file mode 100644 index fbad20d56e..0000000000 --- a/src/Tgstation.Server.Host/Components/Interop/ChatCommand.cs +++ /dev/null @@ -1,25 +0,0 @@ -using Tgstation.Server.Host.Components.Chat; - -namespace Tgstation.Server.Host.Components.Interop -{ - /// - /// Represents a chat command to be handled by DD - /// - sealed class ChatCommand - { - /// - /// The command name - /// - public string Command { get; set; } - - /// - /// The command params - /// - public string Params { get; set; } - - /// - /// The that sent the command - /// - public User User { get; set; } - } -} diff --git a/src/Tgstation.Server.Host/Components/Interop/CommCommand.cs b/src/Tgstation.Server.Host/Components/Interop/CommCommand.cs deleted file mode 100644 index 31b4715897..0000000000 --- a/src/Tgstation.Server.Host/Components/Interop/CommCommand.cs +++ /dev/null @@ -1,20 +0,0 @@ -using System.Collections.Generic; - -namespace Tgstation.Server.Host.Components.Interop -{ - /// - /// Represents a command from DD - /// - sealed class CommCommand - { - /// - /// The dictionary of the - /// - public IReadOnlyDictionary Parameters { get; set; } - - /// - /// The raw JSON of the - /// - public string RawJson { get; set; } - } -} \ No newline at end of file diff --git a/src/Tgstation.Server.Host/Components/Interop/CommContext.cs b/src/Tgstation.Server.Host/Components/Interop/CommContext.cs deleted file mode 100644 index 433714a64e..0000000000 --- a/src/Tgstation.Server.Host/Components/Interop/CommContext.cs +++ /dev/null @@ -1,140 +0,0 @@ -using Microsoft.Extensions.Logging; -using Newtonsoft.Json; -using System; -using System.Collections.Generic; -using System.IO; -using System.Text; -using System.Threading; -using System.Threading.Tasks; -using Tgstation.Server.Host.IO; - -namespace Tgstation.Server.Host.Components.Interop -{ - /// - sealed class CommContext : ICommContext - { - /// - /// The for the - /// - readonly IIOManager ioManager; - - /// - /// The for the - /// - readonly ILogger logger; - - /// - /// The for the - /// - readonly FileSystemWatcher fileSystemWatcher; - - /// - /// The for the - /// - readonly CancellationTokenSource cancellationTokenSource; - - /// - /// The for the - /// - readonly CancellationToken cancellationToken; - - /// - /// The for the - /// - ICommHandler handler; - - /// - /// If the has been disposed - /// - bool disposed; - - /// - /// Construct an - /// - /// The value of - /// The value of - /// The path to watch - /// The filter to watch for - public CommContext(IIOManager ioManager, ILogger logger, string directory, string filter) - { - this.ioManager = ioManager ?? throw new ArgumentNullException(nameof(ioManager)); - this.logger = logger ?? throw new ArgumentNullException(nameof(logger)); - - directory = ioManager.ResolvePath(directory) ?? throw new ArgumentNullException(nameof(directory)); - if (filter == null) - throw new ArgumentNullException(nameof(filter)); - - fileSystemWatcher = new FileSystemWatcher(directory, filter) - { - EnableRaisingEvents = true, - IncludeSubdirectories = false, - NotifyFilter = NotifyFilters.LastWrite - }; - - fileSystemWatcher.Created += HandleWrite; - fileSystemWatcher.Changed += HandleWrite; - - cancellationTokenSource = new CancellationTokenSource(); - cancellationToken = cancellationTokenSource.Token; - disposed = false; - } - - /// - public void Dispose() - { - if (disposed) - return; - disposed = true; - fileSystemWatcher.Dispose(); - cancellationTokenSource.Cancel(); - cancellationTokenSource.Dispose(); - } - - /// - /// Runs when the triggers - /// - /// The sender of the event - /// The - async void HandleWrite(object sender, FileSystemEventArgs e) // this is what async void was made for - { - try - { - var fileBytes = await ioManager.ReadAllBytes(e.FullPath, cancellationToken).ConfigureAwait(false); - var file = Encoding.UTF8.GetString(fileBytes); - - logger.LogTrace("Read interop command json: {0}", file); - - CommCommand command; - try - { - command = new CommCommand - { - Parameters = JsonConvert.DeserializeObject>(file), - RawJson = file - }; - } - catch (JsonException ex) - { - // file not fully written yet - logger.LogDebug("Suppressing json convert exception for command file write: {0}", ex); - return; - } - - await (handler?.HandleInterop(command, cancellationToken) ?? Task.CompletedTask).ConfigureAwait(false); - } - catch (OperationCanceledException) { } - catch (Exception ex) - { - logger.LogError("Exception while trying to handle command json write: {0}", ex); - } - } - - /// - public void RegisterHandler(ICommHandler handler) - { - if (this.handler != null) - throw new InvalidOperationException("RegisterHandler already called!"); - this.handler = handler ?? throw new ArgumentNullException(nameof(handler)); - } - } -} \ No newline at end of file diff --git a/src/Tgstation.Server.Host/Components/Interop/Constants.cs b/src/Tgstation.Server.Host/Components/Interop/Constants.cs deleted file mode 100644 index da5da51c91..0000000000 --- a/src/Tgstation.Server.Host/Components/Interop/Constants.cs +++ /dev/null @@ -1,93 +0,0 @@ -namespace Tgstation.Server.Host.Components.Interop -{ - /// - /// Constants used for communication with the DMAPI - /// - static class Constants - { - /// - /// Identifies a TGS execution. The server version - /// - public const string DMParamHostVersion = "server_service_version"; - - /// - /// Path to the - /// - public const string DMParamInfoJson = "tgs_json"; - - /// - /// The - /// - public const string DMInteropAccessIdentifier = "tgs_tok"; - - /// - /// Generic OK response - /// - public const string DMResponseSuccess = "tgs_succ"; - - /// - /// Change port - /// - public const string DMTopicChangePort = "tgs_port"; - - /// - /// Change reboot mode - /// - public const string DMTopicChangeReboot = "tgs_rmode"; - - /// - /// Chat command - /// - public const string DMTopicChatCommand = "tgs_chat_comm"; - - /// - /// Notify of an - /// - public const string DMTopicEvent = "tgs_event"; - - /// - /// Response to an interop export from DM - /// - public const string DMTopicInteropResponse = "tgs_interop"; - - /// - /// Set port command - /// - public const string DMCommandNewPort = "tgs_new_port"; - - /// - /// API validation command - /// - public const string DMCommandApiValidate = "tgs_validate"; - - /// - /// Server primed command - /// - public const string DMCommandServerPrimed = "tgs_prime"; - - /// - /// World reboot command - /// - public const string DMCommandWorldReboot = "tgs_reboot"; - - /// - /// Terminate process command - /// - public const string DMCommandEndProcess = "tgs_kill"; - - /// - /// Chat send command - /// - public const string DMCommandChat = "tgs_chat_send"; - - /// - /// Topic command parameter - /// - public const string DMParameterCommand = "tgs_com"; - - /// - /// Command data - /// - public const string DMParameterData = "tgs_data"; - } -} diff --git a/src/Tgstation.Server.Host/Components/Interop/DMApiConstants.cs b/src/Tgstation.Server.Host/Components/Interop/DMApiConstants.cs new file mode 100644 index 0000000000..8a4e69bfb0 --- /dev/null +++ b/src/Tgstation.Server.Host/Components/Interop/DMApiConstants.cs @@ -0,0 +1,50 @@ +using Newtonsoft.Json; +using Newtonsoft.Json.Converters; +using Newtonsoft.Json.Serialization; +using System; + +namespace Tgstation.Server.Host.Components.Interop +{ + /// + /// Constants used for communication with the DMAPI + /// + static class DMApiConstants + { + /// + /// Identifies a DMAPI execution with the version as the value. + /// + public const string ParamApiVersion = "server_service_version"; + + /// + /// Identifies the path to the file. + /// + public const string ParamDeploymentInformationFile = "tgs_json"; + + /// + /// Parameter json is encoded in for topic requests. + /// + public const string TopicData = "data"; + + /// + /// The DMAPI being used. + /// + public static readonly Version Version = new Version(5, 0, 0); + + /// + /// for use when communicating with the DMAPI. + /// + public static readonly JsonSerializerSettings SerializerSettings = new JsonSerializerSettings + { + ContractResolver = new DefaultContractResolver + { + NamingStrategy = new CamelCaseNamingStrategy() + }, + Converters = new[] + { + new VersionConverter() + }, + DefaultValueHandling = DefaultValueHandling.Ignore, + ReferenceLoopHandling = ReferenceLoopHandling.Ignore + }; + } +} diff --git a/src/Tgstation.Server.Host/Components/Interop/DMApiParameters.cs b/src/Tgstation.Server.Host/Components/Interop/DMApiParameters.cs new file mode 100644 index 0000000000..22b8785be8 --- /dev/null +++ b/src/Tgstation.Server.Host/Components/Interop/DMApiParameters.cs @@ -0,0 +1,7 @@ +namespace Tgstation.Server.Host.Components.Interop +{ + public class DMApiParameters + { + public string AccessIdentifier { get; set; } + } +} diff --git a/src/Tgstation.Server.Host/Components/Interop/EventNotification.cs b/src/Tgstation.Server.Host/Components/Interop/EventNotification.cs deleted file mode 100644 index 9c9811808d..0000000000 --- a/src/Tgstation.Server.Host/Components/Interop/EventNotification.cs +++ /dev/null @@ -1,20 +0,0 @@ -using System.Collections.Generic; - -namespace Tgstation.Server.Host.Components.Interop -{ - /// - /// For notifying DD of s - /// - sealed class EventNotification - { - /// - /// The - /// - public EventType Type { get; set; } - - /// - /// The event parameters - /// - public IEnumerable Parameters { get; set; } - } -} diff --git a/src/Tgstation.Server.Host/Components/Interop/IBridgeHandler.cs b/src/Tgstation.Server.Host/Components/Interop/IBridgeHandler.cs new file mode 100644 index 0000000000..c7f431a504 --- /dev/null +++ b/src/Tgstation.Server.Host/Components/Interop/IBridgeHandler.cs @@ -0,0 +1,22 @@ +using System.Threading; +using System.Threading.Tasks; + +namespace Tgstation.Server.Host.Components.Interop +{ + /// + interface IBridgeHandler : IBridgeHandlerBase + { + /// + /// The for the . + /// + string AccessIdentifier { get; } + + /// + /// Called when the owning is renamed. + /// + /// The new . + /// The for the operation. + /// A representing the running operation. + Task InstanceRenamed(string newInstanceName, CancellationToken cancellationToken); + } +} \ No newline at end of file diff --git a/src/Tgstation.Server.Host/Components/Interop/IBridgeHandlerBase.cs b/src/Tgstation.Server.Host/Components/Interop/IBridgeHandlerBase.cs new file mode 100644 index 0000000000..380d41faee --- /dev/null +++ b/src/Tgstation.Server.Host/Components/Interop/IBridgeHandlerBase.cs @@ -0,0 +1,20 @@ +using System.Threading; +using System.Threading.Tasks; +using Tgstation.Server.Host.Components.Interop.Bridge; + +namespace Tgstation.Server.Host.Components.Interop +{ + /// + /// Handler for . + /// + public interface IBridgeHandlerBase + { + /// + /// Handle a set of bridge . + /// + /// The to handle. + /// The for the operation. + /// A representing the running operation. + Task ProcessBridgeRequest(BridgeParameters parameters, CancellationToken cancellationToken); + } +} \ No newline at end of file diff --git a/src/Tgstation.Server.Host/Components/Interop/IBridgeRegistrar.cs b/src/Tgstation.Server.Host/Components/Interop/IBridgeRegistrar.cs new file mode 100644 index 0000000000..23a98465ba --- /dev/null +++ b/src/Tgstation.Server.Host/Components/Interop/IBridgeRegistrar.cs @@ -0,0 +1,15 @@ +namespace Tgstation.Server.Host.Components.Interop +{ + /// + /// Registers s. + /// + interface IBridgeRegistrar + { + /// + /// Register a given . + /// + /// The to register. + /// A representative . + IBridgeRegistration RegisterHandler(IBridgeHandler bridgeHandler); + } +} diff --git a/src/Tgstation.Server.Host/Components/Interop/IBridgeRegistration.cs b/src/Tgstation.Server.Host/Components/Interop/IBridgeRegistration.cs new file mode 100644 index 0000000000..2d211214e5 --- /dev/null +++ b/src/Tgstation.Server.Host/Components/Interop/IBridgeRegistration.cs @@ -0,0 +1,11 @@ +using System; + +namespace Tgstation.Server.Host.Components.Interop +{ + /// + /// Represents a registration of an interop session. + /// + interface IBridgeRegistration : IDisposable + { + } +} \ No newline at end of file diff --git a/src/Tgstation.Server.Host/Components/Interop/ICommContext.cs b/src/Tgstation.Server.Host/Components/Interop/ICommContext.cs deleted file mode 100644 index c3f034551c..0000000000 --- a/src/Tgstation.Server.Host/Components/Interop/ICommContext.cs +++ /dev/null @@ -1,16 +0,0 @@ -using System; - -namespace Tgstation.Server.Host.Components.Interop -{ - /// - /// Represents a registration of an interop session - /// - interface ICommContext : IDisposable - { - /// - /// Register a with the - /// - /// The to register - void RegisterHandler(ICommHandler handler); - } -} \ No newline at end of file diff --git a/src/Tgstation.Server.Host/Components/Interop/ICommHandler.cs b/src/Tgstation.Server.Host/Components/Interop/ICommHandler.cs deleted file mode 100644 index 53a9256bf7..0000000000 --- a/src/Tgstation.Server.Host/Components/Interop/ICommHandler.cs +++ /dev/null @@ -1,19 +0,0 @@ -using System.Threading; -using System.Threading.Tasks; - -namespace Tgstation.Server.Host.Components.Interop -{ - /// - /// Handles s - /// - interface ICommHandler - { - /// - /// Handle a - /// - /// The to handle - /// The for the operation - /// A representing the running operation - Task HandleInterop(CommCommand command, CancellationToken cancellationToken); - } -} \ No newline at end of file diff --git a/src/Tgstation.Server.Host/Components/Interop/JsonFile.cs b/src/Tgstation.Server.Host/Components/Interop/JsonFile.cs deleted file mode 100644 index da94459801..0000000000 --- a/src/Tgstation.Server.Host/Components/Interop/JsonFile.cs +++ /dev/null @@ -1,56 +0,0 @@ -using System.Collections.Generic; -using Tgstation.Server.Api.Models; - -namespace Tgstation.Server.Host.Components.Interop -{ - /// - /// Representation of the initial json passed to DreamDaemon - /// - sealed class JsonFile - { - /// - /// The code used by the server to authenticate command Topics - /// - public string AccessIdentifier { get; set; } - - /// - /// If DD should just respond if it's API is working and then exit - /// - public bool ApiValidateOnly { get; set; } - - /// - /// The of the owner at the time of launch - /// - public string InstanceName { get; set; } - - /// - /// JSON file name that contains current active chat channel information - /// - public string ChatChannelsJson { get; set; } - - /// - /// JSON file DD should write to with available chat commands - /// - public string ChatCommandsJson { get; set; } - - /// - /// JSON file DD should write to to send commands to the server - /// - public string ServerCommandsJson { get; set; } - - /// - /// The of the launch - /// - public Api.Models.Internal.RevisionInformation Revision { get; set; } - - /// - /// The level of the launch - /// - public DreamDaemonSecurity SecurityLevel { get; set; } - - /// - /// The s in the launch - /// - public List TestMerges { get; } = new List(); - } -} diff --git a/src/Tgstation.Server.Host/Components/Interop/JsonSubFileList.cs b/src/Tgstation.Server.Host/Components/Interop/Runtime/RuntimeFileList.cs similarity index 56% rename from src/Tgstation.Server.Host/Components/Interop/JsonSubFileList.cs rename to src/Tgstation.Server.Host/Components/Interop/Runtime/RuntimeFileList.cs index 2e3066cd15..aa4638914e 100644 --- a/src/Tgstation.Server.Host/Components/Interop/JsonSubFileList.cs +++ b/src/Tgstation.Server.Host/Components/Interop/Runtime/RuntimeFileList.cs @@ -1,12 +1,12 @@ using System; using System.ComponentModel.DataAnnotations; -namespace Tgstation.Server.Host.Components.Interop +namespace Tgstation.Server.Host.Components.Interop.Runtime { /// /// Information used in for reattaching and interop /// - public class JsonSubFileList + public class RuntimeFileList { /// /// Path to the chat commands json file @@ -21,27 +21,20 @@ namespace Tgstation.Server.Host.Components.Interop public string ChatChannelsJson { get; set; } /// - /// Path to the server commands json file + /// Construct an /// - [Required] - public string ServerCommandsJson { get; set; } + protected RuntimeFileList() { } /// - /// Construct an + /// Construct an from a /// - protected JsonSubFileList() { } - - /// - /// Construct an from a - /// - /// An to copy - public JsonSubFileList(JsonSubFileList copy) + /// An to copy + public RuntimeFileList(RuntimeFileList copy) { if (copy == null) throw new ArgumentNullException(nameof(copy)); ChatChannelsJson = copy.ChatChannelsJson; ChatCommandsJson = copy.ChatCommandsJson; - ServerCommandsJson = copy.ServerCommandsJson; } } } diff --git a/src/Tgstation.Server.Host/Components/Interop/Runtime/RuntimeInformation.cs b/src/Tgstation.Server.Host/Components/Interop/Runtime/RuntimeInformation.cs new file mode 100644 index 0000000000..d5be4fddac --- /dev/null +++ b/src/Tgstation.Server.Host/Components/Interop/Runtime/RuntimeInformation.cs @@ -0,0 +1,87 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using Tgstation.Server.Api.Models; +using Tgstation.Server.Host.Core; +using Tgstation.Server.Host.Security; + +namespace Tgstation.Server.Host.Components.Interop.Runtime +{ + /// + /// Representation of the initial json passed to DreamDaemon + /// + sealed class RuntimeInformation : RuntimeFileList + { + /// + /// The code used by the server to authenticate command Topics + /// + public string AccessIdentifier { get; } + + /// + /// The . + /// + public Version ServerVersion { get; } + + /// + /// The port the HTTP server is running on + /// + public ushort ServerPort { get; } + + /// + /// If DD should just respond if it's API is working and then exit. + /// + public bool ApiValidateOnly { get; } + + /// + /// The of the owner at the time of launch + /// + public string InstanceName { get; } + + /// + /// The of the launch + /// + public Api.Models.Internal.RevisionInformation Revision { get; } + + /// + /// The level of the launch + /// + public DreamDaemonSecurity SecurityLevel { get; } + + /// + /// The s in the launch + /// + public IReadOnlyCollection TestMerges { get; } + + /// + /// Initializes a new instance of the . + /// + /// The to use. + /// The to use. + /// An used to construct the value of . + /// The used to set . + /// The value of . + /// The value of . + /// The value of . + /// The value of . + public RuntimeInformation( + IApplication application, + ICryptographySuite cryptographySuite, + IServerPortProvider portProvider, + IEnumerable testMerges, + Api.Models.Instance instance, + Api.Models.Internal.RevisionInformation revision, + string channelsJson, + string commandsJson, + DreamDaemonSecurity securityLevel) + { + ServerVersion = application?.Version ?? throw new ArgumentNullException(nameof(application)); + AccessIdentifier = cryptographySuite?.GetSecureString() ?? throw new ArgumentNullException(nameof(cryptographySuite)); + TestMerges = testMerges?.ToList() ?? throw new ArgumentNullException(nameof(testMerges)); + InstanceName = instance?.Name ?? throw new ArgumentNullException(nameof(instance)); + Revision = revision ?? throw new ArgumentNullException(nameof(revision)); + ChatChannelsJson = channelsJson ?? throw new ArgumentNullException(nameof(channelsJson)); + ChatChannelsJson = commandsJson ?? throw new ArgumentNullException(nameof(commandsJson)); + SecurityLevel = securityLevel; + } + } +} diff --git a/src/Tgstation.Server.Host/Components/Interop/TestMerge.cs b/src/Tgstation.Server.Host/Components/Interop/Runtime/RuntimeTestMerge.cs similarity index 69% rename from src/Tgstation.Server.Host/Components/Interop/TestMerge.cs rename to src/Tgstation.Server.Host/Components/Interop/Runtime/RuntimeTestMerge.cs index 9e8b795b53..0d20cd35eb 100644 --- a/src/Tgstation.Server.Host/Components/Interop/TestMerge.cs +++ b/src/Tgstation.Server.Host/Components/Interop/Runtime/RuntimeTestMerge.cs @@ -2,12 +2,12 @@ using System.Globalization; using Tgstation.Server.Api.Models.Internal; -namespace Tgstation.Server.Host.Components.Interop +namespace Tgstation.Server.Host.Components.Interop.Runtime { /// /// This model mirrors /datum/tgs_revision_information/test_merge /// - sealed class TestMerge : TestMergeBase + sealed class RuntimeTestMerge : TestMergeBase { /// /// The unix time of when the test merge was applied @@ -15,16 +15,16 @@ namespace Tgstation.Server.Host.Components.Interop public string TimeMerged { get; set; } /// - /// The of the + /// The of the /// public RevisionInformation Revision { get; set; } /// - /// Construct a + /// Construct a /// /// The to build from /// The value of - public TestMerge(Models.TestMerge testMerge, RevisionInformation revision) : base(testMerge) + public RuntimeTestMerge(Models.TestMerge testMerge, RevisionInformation revision) : base(testMerge) { TimeMerged = testMerge.MergedAt.Ticks.ToString(CultureInfo.InvariantCulture); Revision = revision ?? throw new ArgumentNullException(nameof(revision)); diff --git a/src/Tgstation.Server.Host/Components/Interop/Topic/ChatCommand.cs b/src/Tgstation.Server.Host/Components/Interop/Topic/ChatCommand.cs new file mode 100644 index 0000000000..7b4115e449 --- /dev/null +++ b/src/Tgstation.Server.Host/Components/Interop/Topic/ChatCommand.cs @@ -0,0 +1,39 @@ +using System; +using Tgstation.Server.Host.Components.Chat; + +namespace Tgstation.Server.Host.Components.Interop.Topic +{ + /// + /// Represents a chat command to be handled by DD + /// + sealed class ChatCommand + { + /// + /// The command name + /// + public string Command { get; } + + /// + /// The command params + /// + public string Params { get; } + + /// + /// The that sent the command + /// + public User User { get; } + + /// + /// Initializes a new instance of the . + /// + /// The value of . + /// The value of . + /// The value of . + public ChatCommand(User user, string command, string parameters) + { + User = user ?? throw new ArgumentNullException(nameof(user)); + Command = command ?? throw new ArgumentNullException(nameof(command)); + Params = parameters ?? throw new ArgumentNullException(nameof(parameters)); + } + } +} diff --git a/src/Tgstation.Server.Host/Components/Interop/Topic/EventNotification.cs b/src/Tgstation.Server.Host/Components/Interop/Topic/EventNotification.cs new file mode 100644 index 0000000000..48a31622bb --- /dev/null +++ b/src/Tgstation.Server.Host/Components/Interop/Topic/EventNotification.cs @@ -0,0 +1,18 @@ +using System.Collections.Generic; +using System.Linq; + +namespace Tgstation.Server.Host.Components.Interop.Topic +{ + sealed class EventNotification + { + public EventType EventType { get; } + + public IReadOnlyCollection Parameters { get; } + + public EventNotification(EventType eventType, IEnumerable parameters = null) + { + EventType = eventType; + Parameters = parameters?.ToList(); + } + } +} \ No newline at end of file diff --git a/src/Tgstation.Server.Host/Components/Interop/Topic/TopicCommandType.cs b/src/Tgstation.Server.Host/Components/Interop/Topic/TopicCommandType.cs new file mode 100644 index 0000000000..f254da4d00 --- /dev/null +++ b/src/Tgstation.Server.Host/Components/Interop/Topic/TopicCommandType.cs @@ -0,0 +1,33 @@ +namespace Tgstation.Server.Host.Components.Interop.Topic +{ + /// + /// The type of topic command being sent. + /// + enum TopicCommandType + { + /// + /// Invoking a custom chat command. + /// + ChatCommand, + + /// + /// Notification of a TGS event. + /// + Event, + + /// + /// Port change request. + /// + ChangePort, + + /// + /// Reboot state change request. + /// + ChangeRebootState, + + /// + /// The owning instance was renamed. + /// + InstanceRenamed + } +} \ No newline at end of file diff --git a/src/Tgstation.Server.Host/Components/Interop/Topic/TopicParameters.cs b/src/Tgstation.Server.Host/Components/Interop/Topic/TopicParameters.cs new file mode 100644 index 0000000000..49628ecce6 --- /dev/null +++ b/src/Tgstation.Server.Host/Components/Interop/Topic/TopicParameters.cs @@ -0,0 +1,54 @@ +using System; +using Tgstation.Server.Host.Components.Watchdog; + +namespace Tgstation.Server.Host.Components.Interop.Topic +{ + sealed class TopicParameters : DMApiParameters + { + public TopicCommandType CommandType { get; } + + public ChatCommand ChatCommand { get; } + + public EventNotification EventNotification { get; } + + public ushort? NewPort { get; } + + public RebootState? NewRebootState { get; } + public string NewInstanceName { get; } + + private TopicParameters(TopicCommandType commandType) + { + CommandType = commandType; + } + + public TopicParameters(ChatCommand chatCommand) + : this(TopicCommandType.ChatCommand) + { + ChatCommand = chatCommand ?? throw new ArgumentNullException(nameof(chatCommand)); + } + + public TopicParameters(EventNotification eventNotification) + : this(TopicCommandType.Event) + { + EventNotification = eventNotification ?? throw new ArgumentNullException(nameof(eventNotification)); + } + + public TopicParameters(ushort newPort) + : this(TopicCommandType.ChangePort) + { + NewPort = newPort; + } + + public TopicParameters(RebootState newRebootState) + : this(TopicCommandType.ChangeRebootState) + { + NewRebootState = newRebootState; + } + + public TopicParameters(string newInstanceName) + : this(TopicCommandType.InstanceRenamed) + { + NewInstanceName = newInstanceName ?? throw new ArgumentNullException(nameof(newInstanceName)); + } + } +} diff --git a/src/Tgstation.Server.Host/Components/Interop/Topic/TopicResponse.cs b/src/Tgstation.Server.Host/Components/Interop/Topic/TopicResponse.cs new file mode 100644 index 0000000000..cee18a73d8 --- /dev/null +++ b/src/Tgstation.Server.Host/Components/Interop/Topic/TopicResponse.cs @@ -0,0 +1,14 @@ +using System.Collections.Generic; +using Tgstation.Server.Host.Components.Chat; + +namespace Tgstation.Server.Host.Components.Interop.Topic +{ + sealed class TopicResponse + { + public string ErrorMessage { get; set; } + + public string CommandResponse { get; set; } + + public ICollection ChatResponses { get; set; } + } +} diff --git a/src/Tgstation.Server.Host/Components/ReattachInfoHandler.cs b/src/Tgstation.Server.Host/Components/ReattachInfoHandler.cs index 299139cdfd..19840fd758 100644 --- a/src/Tgstation.Server.Host/Components/ReattachInfoHandler.cs +++ b/src/Tgstation.Server.Host/Components/ReattachInfoHandler.cs @@ -72,7 +72,6 @@ namespace Tgstation.Server.Host.Components AccessIdentifier = wdInfo.AccessIdentifier, ChatChannelsJson = wdInfo.ChatChannelsJson, ChatCommandsJson = wdInfo.ChatCommandsJson, - ServerCommandsJson = wdInfo.ServerCommandsJson, CompileJob = wdInfo.Dmb.CompileJob, IsPrimary = wdInfo.IsPrimary, Port = wdInfo.Port, diff --git a/src/Tgstation.Server.Host/Components/Watchdog/BasicWatchdog.cs b/src/Tgstation.Server.Host/Components/Watchdog/BasicWatchdog.cs index 687b497fcf..fe4bb3d147 100644 --- a/src/Tgstation.Server.Host/Components/Watchdog/BasicWatchdog.cs +++ b/src/Tgstation.Server.Host/Components/Watchdog/BasicWatchdog.cs @@ -182,7 +182,14 @@ namespace Tgstation.Server.Host.Components.Watchdog if (!doesntNeedNewDmb) { dmbToUse = await PrepServerForLaunch(dmbToUse, cancellationToken).ConfigureAwait(false); - serverLaunchTask = SessionControllerFactory.LaunchNew(ActiveLaunchParameters, dmbToUse, null, true, true, false, cancellationToken); + serverLaunchTask = SessionControllerFactory.LaunchNew( + dmbToUse, + null, + ActiveLaunchParameters, + true, + true, + false, + cancellationToken); } else serverLaunchTask = SessionControllerFactory.Reattach(serverToReattach, cancellationToken); diff --git a/src/Tgstation.Server.Host/Components/Watchdog/DeadSessionController.cs b/src/Tgstation.Server.Host/Components/Watchdog/DeadSessionController.cs index 035708a67c..2cc9cbfc8a 100644 --- a/src/Tgstation.Server.Host/Components/Watchdog/DeadSessionController.cs +++ b/src/Tgstation.Server.Host/Components/Watchdog/DeadSessionController.cs @@ -2,6 +2,7 @@ using System.Threading; using System.Threading.Tasks; using Tgstation.Server.Host.Components.Deployment; +using Tgstation.Server.Host.Components.Interop.Topic; namespace Tgstation.Server.Host.Components.Watchdog { @@ -44,6 +45,9 @@ namespace Tgstation.Server.Host.Components.Watchdog /// public Task Lifetime { get; } + /// + public Version DMApiVersion => throw new NotSupportedException(); + /// /// If the was d /// @@ -87,7 +91,7 @@ namespace Tgstation.Server.Host.Components.Watchdog public void ResetRebootState() => throw new NotSupportedException(); /// - public Task SendCommand(string command, CancellationToken cancellationToken) => throw new NotSupportedException(); + public Task SendCommand(TopicParameters parameters, CancellationToken cancellationToken) => throw new NotSupportedException(); /// public void SetHighPriority() => throw new NotSupportedException(); diff --git a/src/Tgstation.Server.Host/Components/Watchdog/ExperimentalWatchdog.cs b/src/Tgstation.Server.Host/Components/Watchdog/ExperimentalWatchdog.cs index 7bd0a1235c..63fdd48ff0 100644 --- a/src/Tgstation.Server.Host/Components/Watchdog/ExperimentalWatchdog.cs +++ b/src/Tgstation.Server.Host/Components/Watchdog/ExperimentalWatchdog.cs @@ -157,7 +157,15 @@ namespace Tgstation.Server.Host.Components.Watchdog var newDmb = DmbFactory.LockNextDmb(1); try { - monitorState.InactiveServer = await SessionControllerFactory.LaunchNew(ActiveLaunchParameters, newDmb, null, false, !monitorState.ActiveServer.IsPrimary, false, cancellationToken).ConfigureAwait(false); + monitorState.InactiveServer = await SessionControllerFactory.LaunchNew( + newDmb, + null, + ActiveLaunchParameters, + false, + !monitorState.ActiveServer.IsPrimary, + false, + cancellationToken) + .ConfigureAwait(false); monitorState.InactiveServer.SetHighPriority(); } catch (OperationCanceledException) @@ -179,7 +187,15 @@ namespace Tgstation.Server.Host.Components.Watchdog if (dmbBackup == null) // NANI!? throw new JobException("Creating backup DMB provider failed!"); // just give up, if THAT compile job is failing then the ActiveServer is gonna crash soon too or already has - monitorState.InactiveServer = await SessionControllerFactory.LaunchNew(ActiveLaunchParameters, dmbBackup, null, false, !monitorState.ActiveServer.IsPrimary, false, cancellationToken).ConfigureAwait(false); + monitorState.InactiveServer = await SessionControllerFactory.LaunchNew( + dmbBackup, + null, + ActiveLaunchParameters, + false, + !monitorState.ActiveServer.IsPrimary, + false, + cancellationToken) + .ConfigureAwait(false); monitorState.InactiveServer.SetHighPriority(); await Chat.SendWatchdogMessage("Staging newest DMB on inactive server failed: {0} Falling back to previous dmb...", cancellationToken).ConfigureAwait(false); } @@ -532,7 +548,14 @@ namespace Tgstation.Server.Host.Components.Watchdog // The tasks pertaining to server startup times are in the ISessionControllers Task alphaServerTask; if (!doesntNeedNewDmb) - alphaServerTask = SessionControllerFactory.LaunchNew(ActiveLaunchParameters, dmbToUse, null, true, true, false, cancellationToken); + alphaServerTask = SessionControllerFactory.LaunchNew( + dmbToUse, + null, + ActiveLaunchParameters, + true, + true, + false, + cancellationToken); else alphaServerTask = SessionControllerFactory.Reattach(reattachInfo.Alpha, cancellationToken); @@ -553,7 +576,15 @@ namespace Tgstation.Server.Host.Components.Watchdog // now bring bravo up if (!doesntNeedNewDmb) - bravoServer = await SessionControllerFactory.LaunchNew(ActiveLaunchParameters, dmbToUse, null, false, false, false, cancellationToken).ConfigureAwait(false); + bravoServer = await SessionControllerFactory.LaunchNew( + dmbToUse, + null, + ActiveLaunchParameters, + false, + false, + false, + cancellationToken) + .ConfigureAwait(false); else bravoServer = await SessionControllerFactory.Reattach(reattachInfo.Bravo, cancellationToken).ConfigureAwait(false); diff --git a/src/Tgstation.Server.Host/Components/Watchdog/ISessionController.cs b/src/Tgstation.Server.Host/Components/Watchdog/ISessionController.cs index b1d5e9947b..34bcd92733 100644 --- a/src/Tgstation.Server.Host/Components/Watchdog/ISessionController.cs +++ b/src/Tgstation.Server.Host/Components/Watchdog/ISessionController.cs @@ -1,6 +1,8 @@ -using System.Threading; +using System; +using System.Threading; using System.Threading.Tasks; using Tgstation.Server.Host.Components.Deployment; +using Tgstation.Server.Host.Components.Interop.Topic; using Tgstation.Server.Host.System; namespace Tgstation.Server.Host.Components.Watchdog @@ -30,6 +32,11 @@ namespace Tgstation.Server.Host.Components.Watchdog /// ApiValidationStatus ApiValidationStatus { get; } + /// + /// The DMAPI . + /// + Version DMApiVersion { get; } + /// /// The being used /// @@ -64,10 +71,10 @@ namespace Tgstation.Server.Host.Components.Watchdog /// /// Sends a command to DreamDaemon through /world/Topic() /// - /// The sanitized command to send + /// The to send. /// The for the operation - /// A resulting in the result of /world/Topic() - Task SendCommand(string command, CancellationToken cancellationToken); + /// A resulting in the of /world/Topic() + Task SendCommand(TopicParameters parameters, CancellationToken cancellationToken); /// /// Causes the world to start listening on a diff --git a/src/Tgstation.Server.Host/Components/Watchdog/ISessionControllerFactory.cs b/src/Tgstation.Server.Host/Components/Watchdog/ISessionControllerFactory.cs index 28e9d67135..54cb97fbb8 100644 --- a/src/Tgstation.Server.Host/Components/Watchdog/ISessionControllerFactory.cs +++ b/src/Tgstation.Server.Host/Components/Watchdog/ISessionControllerFactory.cs @@ -14,15 +14,22 @@ namespace Tgstation.Server.Host.Components.Watchdog /// /// Create a from a freshly launch DreamDaemon instance /// - /// The to use. will be updated with the minumum required security level for the launch /// The to use /// The current if any + /// The to use. will be updated with the minumum required security level for the launch. /// If the of should be used /// If the of should be used /// If the should only validate the DMAPI then exit /// The for the operation /// A resulting in a new - Task LaunchNew(DreamDaemonLaunchParameters launchParameters, IDmbProvider dmbProvider, IByondExecutableLock currentByondLock, bool primaryPort, bool primaryDirectory, bool apiValidate, CancellationToken cancellationToken); + Task LaunchNew( + IDmbProvider dmbProvider, + IByondExecutableLock currentByondLock, + DreamDaemonLaunchParameters launchParameters, + bool primaryPort, + bool primaryDirectory, + bool apiValidate, + CancellationToken cancellationToken); /// /// Create a from an existing DreamDaemon instance @@ -30,7 +37,9 @@ namespace Tgstation.Server.Host.Components.Watchdog /// The to use /// The for the operation /// A resulting in a new on success or on failure to reattach - Task Reattach(ReattachInformation reattachInformation, CancellationToken cancellationToken); + Task Reattach( + ReattachInformation reattachInformation, + CancellationToken cancellationToken); /// /// Creates a that appears to have started and died with exit code -1 diff --git a/src/Tgstation.Server.Host/Components/Watchdog/SessionController.cs b/src/Tgstation.Server.Host/Components/Watchdog/SessionController.cs index 4b1298c500..11e326ff83 100644 --- a/src/Tgstation.Server.Host/Components/Watchdog/SessionController.cs +++ b/src/Tgstation.Server.Host/Components/Watchdog/SessionController.cs @@ -1,9 +1,7 @@ using Byond.TopicSender; using Microsoft.Extensions.Logging; using Newtonsoft.Json; -using Newtonsoft.Json.Serialization; using System; -using System.Collections.Generic; using System.Globalization; using System.Net; using System.Threading; @@ -13,17 +11,17 @@ using Tgstation.Server.Host.Components.Byond; using Tgstation.Server.Host.Components.Chat; using Tgstation.Server.Host.Components.Deployment; using Tgstation.Server.Host.Components.Interop; +using Tgstation.Server.Host.Components.Interop.Bridge; +using Tgstation.Server.Host.Components.Interop.Topic; using Tgstation.Server.Host.System; namespace Tgstation.Server.Host.Components.Watchdog { /// - sealed class SessionController : ISessionController, ICommHandler + sealed class SessionController : ISessionController, IBridgeHandler { - /// - /// The DMAPI version being used. - /// - public static readonly Version DMApiVersion = new Version(5, 0, 0); + /// + public string AccessIdentifier => reattachInformation.AccessIdentifier; /// public bool IsPrimary @@ -78,6 +76,9 @@ namespace Tgstation.Server.Host.Components.Watchdog } } + /// + public Version DMApiVersion { get; private set; } + /// public bool ClosePortOnReboot { get; set; } @@ -104,9 +105,9 @@ namespace Tgstation.Server.Host.Components.Watchdog readonly IByondTopicSender byondTopicSender; /// - /// The for the + /// The for the /// - readonly ICommContext interopContext; + readonly IBridgeRegistration bridgeRegistration; /// /// The for the @@ -180,7 +181,7 @@ namespace Tgstation.Server.Host.Components.Watchdog /// The value of /// The value of /// The value of - /// The value of + /// The used to populate . /// The value of /// The value of /// The value of @@ -192,7 +193,7 @@ namespace Tgstation.Server.Host.Components.Watchdog IByondExecutableLock byondLock, IByondTopicSender byondTopicSender, IJsonTrackingContext chatJsonTrackingContext, - ICommContext interopContext, + IBridgeRegistrar bridgeRegistrar, IChat chat, ILogger logger, DreamDaemonSecurity? launchSecurityLevel, @@ -203,14 +204,12 @@ namespace Tgstation.Server.Host.Components.Watchdog this.byondTopicSender = byondTopicSender ?? throw new ArgumentNullException(nameof(byondTopicSender)); this.process = process ?? throw new ArgumentNullException(nameof(process)); this.byondLock = byondLock ?? throw new ArgumentNullException(nameof(byondLock)); - this.interopContext = interopContext ?? throw new ArgumentNullException(nameof(interopContext)); + bridgeRegistration = bridgeRegistrar?.RegisterHandler(this) ?? throw new ArgumentNullException(nameof(bridgeRegistrar)); this.chat = chat ?? throw new ArgumentNullException(nameof(chat)); this.logger = logger ?? throw new ArgumentNullException(nameof(logger)); this.launchSecurityLevel = launchSecurityLevel; - interopContext.RegisterHandler(this); - portClosedForReboot = false; disposed = false; apiValidationStatus = ApiValidationStatus.NeverValidated; @@ -277,7 +276,7 @@ namespace Tgstation.Server.Host.Components.Watchdog } process.Dispose(); - interopContext.Dispose(); + bridgeRegistration.Dispose(); Dmb?.Dispose(); // will be null when released chatJsonTrackingContext.Dispose(); disposed = true; @@ -296,139 +295,145 @@ namespace Tgstation.Server.Host.Components.Watchdog } /// - #pragma warning disable CA1502 // TODO: Decomplexify - public async Task HandleInterop(CommCommand command, CancellationToken cancellationToken) + public async Task ProcessBridgeRequest(BridgeParameters parameters, CancellationToken cancellationToken) { - if (command == null) - throw new ArgumentNullException(nameof(command)); + if (parameters == null) + throw new ArgumentNullException(nameof(parameters)); - var query = command.Parameters; - - object content; - Action postRespond = null; - ushort? overrideResponsePort = null; - if (query.TryGetValue(Constants.DMParameterCommand, out var method)) + var response = new BridgeResponse(); + switch (parameters.CommandType) { - content = new object(); - switch (method) - { - case Constants.DMCommandChat: - try + case BridgeCommandType.ChatSend: + if (parameters.ChatMessage == null) + return new BridgeResponse { - var message = JsonConvert.DeserializeObject(command.RawJson, new JsonSerializerSettings + ErrorMessage = "Missing chatMessage field!" + }; + + if (parameters.ChatMessage.ChannelIds == null) + return new BridgeResponse + { + ErrorMessage = "Missing channelIds field in chatMessage!" + }; + + if (parameters.ChatMessage.Message == null) + return new BridgeResponse + { + ErrorMessage = "Missing message field in chatMessage!" + }; + + await chat.SendMessage( + parameters.ChatMessage.Message, + parameters.ChatMessage.ChannelIds, + cancellationToken).ConfigureAwait(false); + break; + case BridgeCommandType.Prime: + // currently unused, maybe in the future + break; + case BridgeCommandType.Kill: + TerminationWasRequested = true; + process.Terminate(); + break; + case BridgeCommandType.NewPort: + lock (this) + { + if (!parameters.NewPort.HasValue) + { + /////UHHHH + logger.LogWarning("DreamDaemon sent new port command without providing it's own!"); + return new BridgeResponse { - ContractResolver = new CamelCasePropertyNamesContractResolver() - }); - if (message.ChannelIds == null) - throw new InvalidOperationException("Missing ChannelIds field!"); - if (message.Message == null) - throw new InvalidOperationException("Missing Message field!"); - await chat.SendMessage(message.Message, message.ChannelIds, cancellationToken).ConfigureAwait(false); - } - catch (Exception e) - { - logger.LogDebug("Exception while decoding chat message! Exception: {0}", e); - goto default; + ErrorMessage = "Missing stringified port as data parameter!" + }; } - break; - case Constants.DMCommandServerPrimed: - // currently unused, maybe in the future - break; - case Constants.DMCommandEndProcess: - TerminationWasRequested = true; - process.Terminate(); - return; - case Constants.DMCommandNewPort: - lock (this) - { - if (!query.TryGetValue(Constants.DMParameterData, out var stringPortObject) || !UInt16.TryParse(stringPortObject as string, out var currentPort)) - { - /////UHHHH - logger.LogWarning("DreamDaemon sent new port command without providing it's own!"); - content = new ErrorMessage(ErrorCode.InternalServerError) { Message = "Missing stringified port as data parameter!" }; - break; - } - - if (!nextPort.HasValue) - reattachInformation.Port = currentPort; // not ready yet, so what we'll do is accept the random port DD opened on for now and change it later when we decide to - else - { - // nextPort is ready, tell DD to switch to that - // if it fails it'll kill itself - content = new Dictionary { { Constants.DMParameterData, nextPort.Value } }; - reattachInformation.Port = nextPort.Value; - overrideResponsePort = currentPort; - nextPort = null; - - // we'll also get here from SetPort so complete that task - var tmpTcs = portAssignmentTcs; - portAssignmentTcs = null; - if (tmpTcs != null) - postRespond = () => tmpTcs.SetResult(true); - } - - portClosedForReboot = false; - } - - break; - case Constants.DMCommandApiValidate: - if (!launchSecurityLevel.HasValue) - { - logger.LogWarning("DreamDaemon requested API validation but no intial security level was passed to the session controller!"); - apiValidationStatus = ApiValidationStatus.UnaskedValidationRequest; - content = new ErrorMessage(ErrorCode.InternalServerError) { Message = "Invalid API validation request!" }; - break; - } - - if (!query.TryGetValue(Constants.DMParameterData, out var stringMinimumSecurityLevelObject) || !Enum.TryParse(stringMinimumSecurityLevelObject as string, out var minimumSecurityLevel)) - apiValidationStatus = ApiValidationStatus.BadValidationRequest; + var currentPort = parameters.NewPort.Value; + if (!nextPort.HasValue) + reattachInformation.Port = parameters.NewPort.Value; // not ready yet, so what we'll do is accept the random port DD opened on for now and change it later when we decide to else - switch (minimumSecurityLevel) - { - case DreamDaemonSecurity.Safe: - apiValidationStatus = ApiValidationStatus.RequiresSafe; - break; - case DreamDaemonSecurity.Ultrasafe: - apiValidationStatus = ApiValidationStatus.RequiresUltrasafe; - break; - case DreamDaemonSecurity.Trusted: - apiValidationStatus = ApiValidationStatus.RequiresTrusted; - break; - default: - throw new InvalidOperationException("Enum.TryParse failed to validate the DreamDaemonSecurity range!"); - } - - break; - case Constants.DMCommandWorldReboot: - if (ClosePortOnReboot) { - chatJsonTrackingContext.Active = false; - content = new Dictionary { { Constants.DMParameterData, 0 } }; - portClosedForReboot = true; + // nextPort is ready, tell DD to switch to that + // if it fails it'll kill itself + response.NewPort = nextPort.Value; + reattachInformation.Port = nextPort.Value; + nextPort = null; + + // we'll also get here from SetPort so complete that task + var tmpTcs = portAssignmentTcs; + portAssignmentTcs = null; + tmpTcs.SetResult(true); } - var oldTcs = rebootTcs; - rebootTcs = new TaskCompletionSource(); - postRespond = () => oldTcs.SetResult(null); - break; - default: - content = new ErrorMessage(ErrorCode.InternalServerError) { Message = "Requested command not supported!" }; - break; - } + portClosedForReboot = false; + } + + break; + case BridgeCommandType.Validate: + if (!launchSecurityLevel.HasValue) + { + logger.LogWarning("DreamDaemon requested API validation but no intial security level was passed to the session controller!"); + apiValidationStatus = ApiValidationStatus.UnaskedValidationRequest; + return new BridgeResponse + { + ErrorMessage = "Invalid time for an API validation request!" + }; + } + + if (parameters.Version == null) + { + return new BridgeResponse + { + ErrorMessage = "Missing dmApiVersion field!" + }; + } + + switch (parameters.MinimumSecurityLevel) + { + case DreamDaemonSecurity.Safe: + apiValidationStatus = ApiValidationStatus.RequiresSafe; + break; + case DreamDaemonSecurity.Ultrasafe: + apiValidationStatus = ApiValidationStatus.RequiresUltrasafe; + break; + case DreamDaemonSecurity.Trusted: + apiValidationStatus = ApiValidationStatus.RequiresTrusted; + break; + case null: + apiValidationStatus = ApiValidationStatus.BadValidationRequest; + return new BridgeResponse + { + ErrorMessage = "Missing minimumSecurityLevel field!" + }; + default: + return new BridgeResponse + { + ErrorMessage = "Invalid minimumSecurityLevel!" + }; + } + + break; + case BridgeCommandType.Reboot: + if (ClosePortOnReboot) + { + chatJsonTrackingContext.Active = false; + response.NewPort = 0; + portClosedForReboot = true; + } + + var oldTcs = rebootTcs; + rebootTcs = new TaskCompletionSource(); + oldTcs.SetResult(null); + break; + case null: + response.ErrorMessage = "Missing commandType!"; + break; + default: + response.ErrorMessage = "Requested commandType not supported!"; + break; } - else - content = new ErrorMessage(ErrorCode.InternalServerError) { Message = "Missing command parameter!" }; - var json = JsonConvert.SerializeObject(content); - var response = await SendCommand(String.Format(CultureInfo.InvariantCulture, "{0}&{1}={2}", byondTopicSender.SanitizeString(Constants.DMTopicInteropResponse), byondTopicSender.SanitizeString(Constants.DMParameterData), byondTopicSender.SanitizeString(json)), overrideResponsePort, cancellationToken).ConfigureAwait(false); - - if (response != Constants.DMResponseSuccess) - logger.LogWarning("Received error response while responding to interop: {0}", response); - - postRespond?.Invoke(); + return response; } - #pragma warning restore CA1502 /// /// Throws an if has been called @@ -459,35 +464,48 @@ namespace Tgstation.Server.Host.Components.Watchdog } /// - public Task SendCommand(string command, CancellationToken cancellationToken) => SendCommand(command, null, cancellationToken); - - async Task SendCommand(string command, ushort? overridePort, CancellationToken cancellationToken) + public async Task SendCommand(TopicParameters parameters, CancellationToken cancellationToken) { if (Lifetime.IsCompleted) { logger.LogWarning( - "Attempted to send a command to an inactive SessionController{1}: {0}", - command, - overridePort.HasValue ? $" (Override port: {overridePort.Value})" : String.Empty); + "Attempted to send a command to an inactive SessionController: {0}", + parameters.CommandType); return null; } + parameters.AccessIdentifier = reattachInformation.AccessIdentifier; + + var json = JsonConvert.SerializeObject(parameters, DMApiConstants.SerializerSettings); try { var commandString = String.Format(CultureInfo.InvariantCulture, - "?{0}={1}&{2}={3}", - byondTopicSender.SanitizeString(Constants.DMInteropAccessIdentifier), - byondTopicSender.SanitizeString(reattachInformation.AccessIdentifier), - byondTopicSender.SanitizeString(Constants.DMParameterCommand), - command); // intentionally don't sanitize command, that's up to the caller + "?{0}={1}", + byondTopicSender.SanitizeString(DMApiConstants.TopicData), + byondTopicSender.SanitizeString(json)); - var targetPort = overridePort ?? reattachInformation.Port; + var targetPort = reattachInformation.Port; logger.LogTrace("Export to :{0}. Query: {1}", targetPort, commandString); - return await byondTopicSender.SendTopic( + var topicReturn = await byondTopicSender.SendTopic( new IPEndPoint(IPAddress.Loopback, targetPort), commandString, cancellationToken).ConfigureAwait(false); + + try + { + var result = JsonConvert.DeserializeObject(topicReturn, DMApiConstants.SerializerSettings); + if (result.ErrorMessage != null) + { + logger.LogWarning("Errored topic response for command {0}: {1}", parameters.CommandType, result.ErrorMessage); + } + + return result; + } + catch + { + logger.LogWarning("Invalid topic response: {0}", topicReturn); + } } catch (OperationCanceledException) { @@ -495,9 +513,10 @@ namespace Tgstation.Server.Host.Components.Watchdog } catch (Exception e) { - logger.LogInformation("Send command exception:{0}{1}", Environment.NewLine, e.Message); - return null; + logger.LogWarning("Send command exception:{0}{1}", Environment.NewLine, e.Message); } + + return null; } /// @@ -510,11 +529,14 @@ namespace Tgstation.Server.Host.Components.Watchdog async Task ImmediateTopicPortChange() { - var commandResult = await SendCommand(String.Format(CultureInfo.InvariantCulture, "{0}&{1}={2}", byondTopicSender.SanitizeString(Constants.DMTopicChangePort), byondTopicSender.SanitizeString(Constants.DMParameterData), byondTopicSender.SanitizeString(port.ToString(CultureInfo.InvariantCulture))), cancellationToken).ConfigureAwait(false); + var commandResult = await SendCommand( + new TopicParameters(port), + cancellationToken) + .ConfigureAwait(false); - if (commandResult != Constants.DMResponseSuccess) + if (commandResult.ErrorMessage != null) { - logger.LogWarning("Failed port change! DD says: {0}", commandResult); + logger.LogWarning("Failed port change! DD says: {0}", commandResult.ErrorMessage); return false; } @@ -541,7 +563,12 @@ namespace Tgstation.Server.Host.Components.Watchdog if (RebootState == newRebootState) return true; reattachInformation.RebootState = newRebootState; - return await SendCommand(String.Format(CultureInfo.InvariantCulture, "{0}&{1}={2}", byondTopicSender.SanitizeString(Constants.DMTopicChangeReboot), byondTopicSender.SanitizeString(Constants.DMParameterData), (int)newRebootState), cancellationToken).ConfigureAwait(false) == Constants.DMResponseSuccess; + var result = await SendCommand( + new TopicParameters(newRebootState), + cancellationToken) + .ConfigureAwait(false); + + return result != null && result.ErrorMessage != null; } /// @@ -563,13 +590,19 @@ namespace Tgstation.Server.Host.Components.Watchdog /// public void ReplaceDmbProvider(IDmbProvider dmbProvider) { -#pragma warning disable IDE0016 // Use 'throw' expression - if (dmbProvider == null) - throw new ArgumentNullException(nameof(dmbProvider)); -#pragma warning restore IDE0016 // Use 'throw' expression + var oldDmb = reattachInformation.Dmb; + reattachInformation.Dmb = dmbProvider ?? throw new ArgumentNullException(nameof(dmbProvider)); + oldDmb.Dispose(); + } - reattachInformation.Dmb.Dispose(); - reattachInformation.Dmb = dmbProvider; + /// + public async Task InstanceRenamed(string newInstanceName, CancellationToken cancellationToken) + { + var result = await SendCommand(new TopicParameters(newInstanceName), cancellationToken).ConfigureAwait(false); + if(result == null) + logger.LogWarning("Failed to change instance name! No DD response from Topic!", result.ErrorMessage); + if (result.ErrorMessage != null) + logger.LogWarning("Failed to change reboot state! DD says: {0}", result.ErrorMessage); } } } diff --git a/src/Tgstation.Server.Host/Components/Watchdog/SessionControllerFactory.cs b/src/Tgstation.Server.Host/Components/Watchdog/SessionControllerFactory.cs index fbe01d1a4f..cbd5dc65f4 100644 --- a/src/Tgstation.Server.Host/Components/Watchdog/SessionControllerFactory.cs +++ b/src/Tgstation.Server.Host/Components/Watchdog/SessionControllerFactory.cs @@ -14,7 +14,9 @@ using Tgstation.Server.Host.Components.Byond; using Tgstation.Server.Host.Components.Chat; using Tgstation.Server.Host.Components.Deployment; using Tgstation.Server.Host.Components.Interop; +using Tgstation.Server.Host.Components.Interop.Runtime; using Tgstation.Server.Host.Core; +using Tgstation.Server.Host.Extensions; using Tgstation.Server.Host.IO; using Tgstation.Server.Host.Security; using Tgstation.Server.Host.System; @@ -69,6 +71,11 @@ namespace Tgstation.Server.Host.Components.Watchdog /// readonly IPlatformIdentifier platformIdentifier; + /// + /// The for the . + /// + readonly IBridgeRegistrar bridgeRegistrar; + /// /// The for the /// @@ -112,6 +119,7 @@ namespace Tgstation.Server.Host.Components.Watchdog /// The value of /// The value of /// The value of + /// The value of . /// The value of public SessionControllerFactory( IProcessExecutor processExecutor, @@ -123,6 +131,7 @@ namespace Tgstation.Server.Host.Components.Watchdog IChat chat, INetworkPromptReaper networkPromptReaper, IPlatformIdentifier platformIdentifier, + IBridgeRegistrar bridgeRegistrar, ILoggerFactory loggerFactory, Api.Models.Instance instance) { @@ -136,12 +145,20 @@ namespace Tgstation.Server.Host.Components.Watchdog this.chat = chat ?? throw new ArgumentNullException(nameof(chat)); this.networkPromptReaper = networkPromptReaper ?? throw new ArgumentNullException(nameof(networkPromptReaper)); this.platformIdentifier = platformIdentifier ?? throw new ArgumentNullException(nameof(platformIdentifier)); + this.bridgeRegistrar = bridgeRegistrar ?? throw new ArgumentNullException(nameof(bridgeRegistrar)); this.loggerFactory = loggerFactory ?? throw new ArgumentNullException(nameof(loggerFactory)); } /// #pragma warning disable CA1506 // TODO: Decomplexify - public async Task LaunchNew(DreamDaemonLaunchParameters launchParameters, IDmbProvider dmbProvider, IByondExecutableLock currentByondLock, bool primaryPort, bool primaryDirectory, bool apiValidate, CancellationToken cancellationToken) + public async Task LaunchNew( + IDmbProvider dmbProvider, + IByondExecutableLock currentByondLock, + DreamDaemonLaunchParameters launchParameters, + bool primaryPort, + bool primaryDirectory, + bool apiValidate, + CancellationToken cancellationToken) { var portToUse = primaryPort ? launchParameters.PrimaryPort : launchParameters.SecondaryPort; if (!portToUse.HasValue) @@ -156,9 +173,6 @@ namespace Tgstation.Server.Host.Components.Watchdog var files = await ioManager.GetFilesWithExtension(basePath, JsonPostfix, cancellationToken).ConfigureAwait(false); await Task.WhenAll(files.Select(x => ioManager.DeleteFile(x, cancellationToken))).ConfigureAwait(false); - // i changed this back from guids, hopefully i don't regret that - string JsonFile(string name) => String.Format(CultureInfo.InvariantCulture, "{0}.{1}", name, JsonPostfix); - var securityLevelToUse = launchParameters.SecurityLevel.Value; switch (dmbProvider.CompileJob.MinimumSecurityLevel) { @@ -175,32 +189,34 @@ namespace Tgstation.Server.Host.Components.Watchdog throw new InvalidOperationException(String.Format(CultureInfo.InvariantCulture, "Invalid DreamDaemonSecurity value: {0}", dmbProvider.CompileJob.MinimumSecurityLevel)); } - // setup interop files - var interopInfo = new JsonFile - { - AccessIdentifier = accessIdentifier, - ApiValidateOnly = apiValidate, - ChatChannelsJson = JsonFile("chat_channels"), - ChatCommandsJson = JsonFile("chat_commands"), - ServerCommandsJson = JsonFile("server_commands"), - InstanceName = instance.Name, - SecurityLevel = securityLevelToUse, - Revision = new Api.Models.Internal.RevisionInformation - { - CommitSha = dmbProvider.CompileJob.RevisionInformation.CommitSha, - OriginCommitSha = dmbProvider.CompileJob.RevisionInformation.OriginCommitSha - } - }; + // i changed this back from guids, hopefully i don't regret that + string JsonFile(string name) => $"tgs_{name}.{JsonPostfix}"; - interopInfo.TestMerges.AddRange(dmbProvider.CompileJob.RevisionInformation.ActiveTestMerges.Select(x => x.TestMerge).Select(x => new Interop.TestMerge(x, interopInfo.Revision))); + // setup interop files + var revisionInfo = new Api.Models.Internal.RevisionInformation + { + CommitSha = dmbProvider.CompileJob.RevisionInformation.CommitSha, + OriginCommitSha = dmbProvider.CompileJob.RevisionInformation.OriginCommitSha + }; + var testMerges = dmbProvider + .CompileJob + .RevisionInformation + .ActiveTestMerges + .Select(x => x.TestMerge) + .Select(x => new RuntimeTestMerge(x, revisionInfo)); + var interopInfo = new RuntimeInformation( + application, + cryptographySuite, + testMerges, + instance, + revisionInfo, + JsonFile("chat_channels"), + JsonFile("chat_commands"), + securityLevelToUse); var interopJsonFile = JsonFile("interop"); - var interopJson = JsonConvert.SerializeObject(interopInfo, new JsonSerializerSettings - { - ContractResolver = new CamelCasePropertyNamesContractResolver(), - ReferenceLoopHandling = ReferenceLoopHandling.Ignore - }); + var interopJson = JsonConvert.SerializeObject(interopInfo, DMApiConstants.SerializerSettings); var chatJsonTrackingTask = chat.TrackJsons(basePath, interopInfo.ChatChannelsJson, interopInfo.ChatCommandsJson, cancellationToken); @@ -212,36 +228,33 @@ namespace Tgstation.Server.Host.Components.Watchdog var byondLock = currentByondLock ?? await byond.UseExecutables(Version.Parse(dmbProvider.CompileJob.ByondVersion), cancellationToken).ConfigureAwait(false); try { - // create interop context - var context = new CommContext(ioManager, loggerFactory.CreateLogger(), basePath, interopInfo.ServerCommandsJson); + // set command line options + // more sanitization here cause it uses the same scheme + var parameters = $"{DMApiConstants.ParamApiVersion}={byondTopicSender.SanitizeString(DMApiConstants.Version.Semver())}&{byondTopicSender.SanitizeString(DMApiConstants.ParamDeploymentInformationFile)}={byondTopicSender.SanitizeString(interopJsonFile)}"; + + var visibility = apiValidate ? "invisible" : "public"; + + // important to run on all ports to allow port changing + var arguments = String.Format(CultureInfo.InvariantCulture, "{0} -port {1} -ports 1-65535 {2}-close -{3} -{5} -public -params \"{4}\"", + dmbProvider.DmbName, + primaryPort ? launchParameters.PrimaryPort : launchParameters.SecondaryPort, + launchParameters.AllowWebClient.Value ? "-webclient " : String.Empty, + SecurityWord(securityLevelToUse), + parameters, + visibility); + + // See https://github.com/tgstation/tgstation-server/issues/719 + var noShellExecute = !platformIdentifier.IsWindows; + + // launch dd + var process = processExecutor.LaunchProcess(byondLock.DreamDaemonPath, basePath, arguments, noShellExecute: noShellExecute); try { - // set command line options - // more sanitization here cause it uses the same scheme - var parameters = String.Format(CultureInfo.InvariantCulture, "{2}={0}&{3}={1}", byondTopicSender.SanitizeString(application.Version.ToString()), byondTopicSender.SanitizeString(interopJsonFile), byondTopicSender.SanitizeString(Constants.DMParamHostVersion), byondTopicSender.SanitizeString(Constants.DMParamInfoJson)); + networkPromptReaper.RegisterProcess(process); - var visibility = apiValidate ? "invisible" : "public"; - - // important to run on all ports to allow port changing - var arguments = String.Format(CultureInfo.InvariantCulture, "{0} -port {1} -ports 1-65535 {2}-close -{3} -{5} -public -params \"{4}\"", - dmbProvider.DmbName, - primaryPort ? launchParameters.PrimaryPort : launchParameters.SecondaryPort, - launchParameters.AllowWebClient.Value ? "-webclient " : String.Empty, - SecurityWord(securityLevelToUse), - parameters, - visibility); - - // See https://github.com/tgstation/tgstation-server/issues/719 - var noShellExecute = !platformIdentifier.IsWindows; - - // launch dd - var process = processExecutor.LaunchProcess(byondLock.DreamDaemonPath, basePath, arguments, noShellExecute: noShellExecute); - try - { - networkPromptReaper.RegisterProcess(process); - - // return the session controller for it - var result = new SessionController(new ReattachInformation + // return the session controller for it + var result = new SessionController( + new ReattachInformation { AccessIdentifier = accessIdentifier, Dmb = dmbProvider, @@ -250,23 +263,25 @@ namespace Tgstation.Server.Host.Components.Watchdog ProcessId = process.Id, ChatChannelsJson = interopInfo.ChatChannelsJson, ChatCommandsJson = interopInfo.ChatCommandsJson, - ServerCommandsJson = interopInfo.ServerCommandsJson, - }, process, byondLock, byondTopicSender, chatJsonTrackingContext, context, chat, loggerFactory.CreateLogger(), launchParameters.SecurityLevel, launchParameters.StartupTimeout); + }, + process, + byondLock, + byondTopicSender, + chatJsonTrackingContext, + bridgeRegistrar, + chat, + loggerFactory.CreateLogger(), + launchParameters.SecurityLevel, + launchParameters.StartupTimeout); - // writeback launch parameter's fixed security level - launchParameters.SecurityLevel = securityLevelToUse; + // writeback launch parameter's fixed security level + launchParameters.SecurityLevel = securityLevelToUse; - return result; - } - catch - { - process.Dispose(); - throw; - } + return result; } catch { - context.Dispose(); + process.Dispose(); throw; } } @@ -286,7 +301,9 @@ namespace Tgstation.Server.Host.Components.Watchdog #pragma warning restore CA1506 /// - public async Task Reattach(ReattachInformation reattachInformation, CancellationToken cancellationToken) + public async Task Reattach( + ReattachInformation reattachInformation, + CancellationToken cancellationToken) { if (reattachInformation == null) throw new ArgumentNullException(nameof(reattachInformation)); @@ -299,31 +316,31 @@ namespace Tgstation.Server.Host.Components.Watchdog var byondLock = await byond.UseExecutables(Version.Parse(reattachInformation.Dmb.CompileJob.ByondVersion), cancellationToken).ConfigureAwait(false); try { - var context = new CommContext(ioManager, loggerFactory.CreateLogger(), basePath, reattachInformation.ServerCommandsJson); - try - { - var process = processExecutor.GetProcess(reattachInformation.ProcessId); + var process = processExecutor.GetProcess(reattachInformation.ProcessId); + if (process != null) + try + { + networkPromptReaper.RegisterProcess(process); + result = new SessionController( + reattachInformation, + process, + byondLock, + byondTopicSender, + chatJsonTrackingContext, + bridgeRegistrar, + chat, + loggerFactory.CreateLogger(), + null, + null); - if (process != null) - try - { - networkPromptReaper.RegisterProcess(process); - result = new SessionController(reattachInformation, process, byondLock, byondTopicSender, chatJsonTrackingContext, context, chat, loggerFactory.CreateLogger(), null, null); - - process = null; - context = null; - byondLock = null; - chatJsonTrackingContext = null; - } - finally - { - process?.Dispose(); - } - } - finally - { - context?.Dispose(); - } + process = null; + byondLock = null; + chatJsonTrackingContext = null; + } + finally + { + process?.Dispose(); + } } finally { diff --git a/src/Tgstation.Server.Host/Components/Watchdog/WatchdogBase.cs b/src/Tgstation.Server.Host/Components/Watchdog/WatchdogBase.cs index 0a845a0bb6..96b71e5afa 100644 --- a/src/Tgstation.Server.Host/Components/Watchdog/WatchdogBase.cs +++ b/src/Tgstation.Server.Host/Components/Watchdog/WatchdogBase.cs @@ -2,13 +2,11 @@ using Microsoft.EntityFrameworkCore; using Microsoft.Extensions.Logging; using Newtonsoft.Json; -using Newtonsoft.Json.Serialization; using System; using System.Collections.Generic; using System.Diagnostics; using System.Globalization; using System.Linq; -using System.Text; using System.Threading; using System.Threading.Tasks; using Tgstation.Server.Api.Models.Internal; @@ -16,6 +14,7 @@ using Tgstation.Server.Api.Rights; using Tgstation.Server.Host.Components.Chat; using Tgstation.Server.Host.Components.Deployment; using Tgstation.Server.Host.Components.Interop; +using Tgstation.Server.Host.Components.Interop.Topic; using Tgstation.Server.Host.Core; using Tgstation.Server.Host.Database; using Tgstation.Server.Host.Extensions; @@ -432,43 +431,25 @@ namespace Tgstation.Server.Host.Components.Watchdog if (!Running) return true; - string results; + TopicResponse result; using (await SemaphoreSlimContext.Lock(Semaphore, cancellationToken).ConfigureAwait(false)) { if (!Running) return true; - var builder = new StringBuilder(Constants.DMTopicEvent); - builder.Append('&'); - var notification = new EventNotification - { - Type = eventType, - Parameters = parameters - }; - var json = JsonConvert.SerializeObject(notification); - builder.Append(byondTopicSender.SanitizeString(Constants.DMParameterData)); - builder.Append('='); - builder.Append(byondTopicSender.SanitizeString(json)); + var notification = new EventNotification(eventType, parameters); var activeServer = GetActiveController(); - results = await activeServer.SendCommand(builder.ToString(), cancellationToken).ConfigureAwait(false); + result = await activeServer.SendCommand( + new TopicParameters(notification), + cancellationToken) + .ConfigureAwait(false); } - if (results == Constants.DMResponseSuccess) + if (result?.ChatResponses == null) return true; - List responses; - try - { - responses = JsonConvert.DeserializeObject>(results); - } - catch - { - Logger.LogInformation("Recieved invalid response from DD when parsing event {0}:{1}{2}", eventType, Environment.NewLine, results); - return true; - } - - await Task.WhenAll(responses.Select(x => Chat.SendMessage(x.Message, x.ChannelIds, cancellationToken))).ConfigureAwait(false); + await Task.WhenAll(result.ChatResponses.Select(x => Chat.SendMessage(x.Message, x.ChannelIds, cancellationToken))).ConfigureAwait(false); return true; } @@ -481,22 +462,17 @@ namespace Tgstation.Server.Host.Components.Watchdog if (!Running) return "ERROR: Server offline!"; - var commandObject = new ChatCommand - { - Command = commandName, - Params = arguments, - User = sender - }; + var commandObject = new ChatCommand(sender, commandName, arguments); - var json = JsonConvert.SerializeObject(commandObject, new JsonSerializerSettings - { - ContractResolver = new CamelCasePropertyNamesContractResolver() - }); - - var command = String.Format(CultureInfo.InvariantCulture, "{0}&{1}={2}", byondTopicSender.SanitizeString(Constants.DMTopicChatCommand), byondTopicSender.SanitizeString(Constants.DMParameterData), byondTopicSender.SanitizeString(json)); + var command = new TopicParameters(commandObject); var activeServer = GetActiveController(); - return await activeServer.SendCommand(command, cancellationToken).ConfigureAwait(false) ?? "ERROR: Bad topic exchange!"; + var commandResult = await activeServer.SendCommand(command, cancellationToken).ConfigureAwait(false); + + return commandResult?.CommandResponse ?? + (commandResult == null + ? "ERROR: Bad topic exchange!" + : "ERROR: Bad DMAPI response!"); } } diff --git a/src/Tgstation.Server.Host/Controllers/BridgeController.cs b/src/Tgstation.Server.Host/Controllers/BridgeController.cs new file mode 100644 index 0000000000..b0718236e8 --- /dev/null +++ b/src/Tgstation.Server.Host/Controllers/BridgeController.cs @@ -0,0 +1,75 @@ +using Microsoft.AspNetCore.Mvc; +using Microsoft.Extensions.Logging; +using Newtonsoft.Json; +using System; +using System.Threading; +using System.Threading.Tasks; +using Tgstation.Server.Api; +using Tgstation.Server.Host.Components; +using Tgstation.Server.Host.Components.Interop; +using Tgstation.Server.Host.Components.Interop.Bridge; + +namespace Tgstation.Server.Host.Controllers +{ + /// + /// for recieving DMAPI requests from DreamDaemon. + /// + [Route("Bridge")] + [Produces(ApiHeaders.ApplicationJson)] + public class BridgeController : Controller + { + /// + /// The for the + /// + readonly IInstanceManager instanceManager; + + /// + /// The for the + /// + readonly ILogger logger; + + /// + /// Initializes a new instance of the . + /// + /// The value of . + /// The value of . + public BridgeController(IInstanceManager instanceManager, ILogger logger) + { + this.instanceManager = instanceManager ?? throw new ArgumentNullException(nameof(instanceManager)); + this.logger = logger ?? throw new ArgumentNullException(nameof(logger)); + } + + /// + /// Processes a bridge request. + /// + /// JSON encoded . + /// The for the operation + /// A resulting in the for the operation. + [HttpGet] + public async Task Process([FromQuery]string data, CancellationToken cancellationToken) + { + if (String.IsNullOrWhiteSpace(data)) + return BadRequest(); + + BridgeParameters request; + try + { + request = JsonConvert.DeserializeObject(data, DMApiConstants.SerializerSettings); + } + catch + { + logger.LogDebug("Error deserializing bridge request: {0}", data); + return BadRequest(); + } + + logger.LogTrace("Bridge Request: {0}", data); + + var response = await instanceManager.ProcessBridgeRequest(request, cancellationToken).ConfigureAwait(false); + if (response == null) + Forbid(); + + var responseJson = JsonConvert.SerializeObject(response, DMApiConstants.SerializerSettings); + return Content(responseJson, ApiHeaders.ApplicationJson); + } + } +} diff --git a/src/Tgstation.Server.Host/Models/ReattachInformationBase.cs b/src/Tgstation.Server.Host/Models/ReattachInformationBase.cs index c057dd8d7b..8ccb723d44 100644 --- a/src/Tgstation.Server.Host/Models/ReattachInformationBase.cs +++ b/src/Tgstation.Server.Host/Models/ReattachInformationBase.cs @@ -1,7 +1,7 @@ using System; using System.ComponentModel.DataAnnotations; using System.Globalization; -using Tgstation.Server.Host.Components.Interop; +using Tgstation.Server.Host.Components.Interop.Runtime; using Tgstation.Server.Host.Components.Watchdog; namespace Tgstation.Server.Host.Models @@ -9,7 +9,7 @@ namespace Tgstation.Server.Host.Models /// /// Base class for /// - public abstract class ReattachInformationBase : JsonSubFileList + public abstract class ReattachInformationBase : RuntimeFileList { /// /// Used to identify and authenticate the DreamDaemon instance diff --git a/tests/Tgstation.Server.Host.Tests/Components/Watchdog/TestExperimentalWatchdog.cs b/tests/Tgstation.Server.Host.Tests/Components/Watchdog/TestExperimentalWatchdog.cs index fbff4d209e..e405b191cb 100644 --- a/tests/Tgstation.Server.Host.Tests/Components/Watchdog/TestExperimentalWatchdog.cs +++ b/tests/Tgstation.Server.Host.Tests/Components/Watchdog/TestExperimentalWatchdog.cs @@ -109,7 +109,7 @@ namespace Tgstation.Server.Host.Components.Watchdog.Tests var sessionsToVerify = new List>(); var cancellationToken = cts.Token; - mockSessionControllerFactory.Setup(x => x.LaunchNew(mockLaunchParameters, mDmbP, null, It.IsAny(), It.IsAny(), false, cancellationToken)).Returns(() => + mockSessionControllerFactory.Setup(x => x.LaunchNew(mDmbP, null, mockLaunchParameters, It.IsAny(), It.IsAny(), false, cancellationToken)).Returns(() => { var mockSession = new Mock(); mockSession.SetupGet(x => x.Lifetime).Returns(infiniteTask).Verifiable(); diff --git a/tests/Tgstation.Server.Tests/VersionsTest.cs b/tests/Tgstation.Server.Tests/VersionsTest.cs index 77cbc3418b..0bc638dec2 100644 --- a/tests/Tgstation.Server.Tests/VersionsTest.cs +++ b/tests/Tgstation.Server.Tests/VersionsTest.cs @@ -7,6 +7,7 @@ using System.Xml.Linq; using Tgstation.Server.Api; using Tgstation.Server.Client; using Tgstation.Server.Host; +using Tgstation.Server.Host.Components.Interop; using Tgstation.Server.Host.Components.Watchdog; namespace Tgstation.Server.Tests @@ -85,7 +86,7 @@ namespace Tgstation.Server.Tests Assert.IsTrue(Version.TryParse(versionLine, out var actual)); Assert.AreEqual(expected, actual); - Assert.AreEqual(expected, SessionController.DMApiVersion); + Assert.AreEqual(expected, DMApiConstants.Version); } [TestMethod]