mirror of
https://github.com/tgstation/tgstation-server.git
synced 2026-08-27 07:04:57 +01:00
Interop rewrite WIP
This commit is contained in:
@@ -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.
|
||||
|
||||
@@ -8,3 +8,5 @@
|
||||
#endif
|
||||
#include "v4\api.dm"
|
||||
#include "v4\commands.dm"
|
||||
#include "v5\api.dm"
|
||||
#include "v5\commands.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
|
||||
@@ -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.
|
||||
*/
|
||||
@@ -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.
|
||||
*/
|
||||
@@ -31,5 +31,10 @@ namespace Tgstation.Server.Api.Models.Internal
|
||||
/// </summary>
|
||||
[Required]
|
||||
public DreamDaemonSecurity? MinimumSecurityLevel { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The DMAPI <see cref="Version"/>.
|
||||
/// </summary>
|
||||
public Version DMApiVersion { get; set;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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<List<CustomCommand>>(resultJson, new JsonSerializerSettings
|
||||
{
|
||||
ContractResolver = new DefaultContractResolver
|
||||
{
|
||||
NamingStrategy = new SnakeCaseNamingStrategy()
|
||||
}
|
||||
});
|
||||
var result = JsonConvert.DeserializeObject<List<CustomCommand>>(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);
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
|
||||
|
||||
@@ -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
|
||||
/// <summary>
|
||||
/// Create an <see cref="IInstance"/>
|
||||
/// </summary>
|
||||
/// <param name="bridgeRegistrar">The <see cref="IBridgeRegistrar"/> to use.</param>
|
||||
/// <param name="metadata">The <see cref="Models.Instance"/></param>
|
||||
/// <returns>A new <see cref="IInstance"/></returns>
|
||||
IInstance CreateInstance(Models.Instance metadata);
|
||||
IInstance CreateInstance(IBridgeRegistrar bridgeRegistrar, Models.Instance metadata);
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
/// <summary>
|
||||
/// For managing <see cref="IInstance"/>s
|
||||
/// </summary>
|
||||
public interface IInstanceManager
|
||||
public interface IInstanceManager : IBridgeHandlerBase
|
||||
{
|
||||
/// <summary>
|
||||
/// Get the <see cref="IInstance"/> associated with given <paramref name="metadata"/>
|
||||
|
||||
@@ -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
|
||||
|
||||
/// <inheritdoc />
|
||||
#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<DmbFactory>(), metadata.CloneMetadata());
|
||||
try
|
||||
|
||||
@@ -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
|
||||
{
|
||||
/// <inheritdoc />
|
||||
sealed class InstanceManager : IInstanceManager, IRestartHandler, IHostedService, IDisposable
|
||||
sealed class InstanceManager : IInstanceManager, IRestartHandler, IHostedService, IBridgeRegistrar, IDisposable
|
||||
{
|
||||
/// <summary>
|
||||
/// The <see cref="IInstanceFactory"/> for the <see cref="InstanceManager"/>
|
||||
@@ -48,11 +49,6 @@ namespace Tgstation.Server.Host.Components
|
||||
/// </summary>
|
||||
readonly IServerControl serverControl;
|
||||
|
||||
/// <summary>
|
||||
/// The <see cref="IPlatformIdentifier"/> for the <see cref="InstanceManager"/>
|
||||
/// </summary>
|
||||
readonly IPlatformIdentifier platformIdentifier;
|
||||
|
||||
/// <summary>
|
||||
/// The <see cref="ISystemIdentityFactory"/> for the <see cref="InstanceManager"/>
|
||||
/// </summary>
|
||||
@@ -66,7 +62,12 @@ namespace Tgstation.Server.Host.Components
|
||||
/// <summary>
|
||||
/// Map of <see cref="Api.Models.Instance.Id"/>s to respective <see cref="IInstance"/>s
|
||||
/// </summary>
|
||||
readonly Dictionary<long, IInstance> instances;
|
||||
readonly IDictionary<long, IInstance> instances;
|
||||
|
||||
/// <summary>
|
||||
/// Map of <see cref="IBridgeHandler.AccessIdentifier"/>s to their respective <see cref="IBridgeHandler"/>s.
|
||||
/// </summary>
|
||||
readonly IDictionary<string, IBridgeHandler> bridgeHandlers;
|
||||
|
||||
/// <summary>
|
||||
/// Used in <see cref="StopAsync(CancellationToken)"/> to determine if database downgrades must be made
|
||||
@@ -87,7 +88,6 @@ namespace Tgstation.Server.Host.Components
|
||||
/// <param name="application">The value of <see cref="application"/></param>
|
||||
/// <param name="jobManager">The value of <see cref="jobManager"/></param>
|
||||
/// <param name="serverControl">The value of <see cref="serverControl"/></param>
|
||||
/// <param name="platformIdentifier">The value of <see cref="platformIdentifier"/>.</param>
|
||||
/// <param name="systemIdentityFactory">The value of <see cref="systemIdentityFactory"/>.</param>
|
||||
/// <param name="logger">The value of <see cref="logger"/></param>
|
||||
public InstanceManager(
|
||||
@@ -97,7 +97,6 @@ namespace Tgstation.Server.Host.Components
|
||||
IApplication application,
|
||||
IJobManager jobManager,
|
||||
IServerControl serverControl,
|
||||
IPlatformIdentifier platformIdentifier,
|
||||
ISystemIdentityFactory systemIdentityFactory,
|
||||
ILogger<InstanceManager> 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<long, IInstance>();
|
||||
bridgeHandlers = new Dictionary<string, IBridgeHandler>();
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
@@ -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!");
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public async Task<BridgeResponse> 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);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
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);
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,12 @@
|
||||
namespace Tgstation.Server.Host.Components.Interop.Bridge
|
||||
{
|
||||
public enum BridgeCommandType
|
||||
{
|
||||
NewPort,
|
||||
Validate,
|
||||
Prime,
|
||||
Reboot,
|
||||
Kill,
|
||||
ChatSend
|
||||
}
|
||||
}
|
||||
@@ -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; }
|
||||
}
|
||||
}
|
||||
@@ -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; }
|
||||
}
|
||||
}
|
||||
@@ -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<ulong> ChannelIds { get; set; }
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,38 @@
|
||||
using System;
|
||||
|
||||
namespace Tgstation.Server.Host.Components.Interop
|
||||
{
|
||||
/// <inheritdoc />
|
||||
sealed class BridgeRegistration : IBridgeRegistration
|
||||
{
|
||||
/// <summary>
|
||||
/// <see langword="lock"/> <see cref="object"/> for accessing <see cref="onDispose"/>.
|
||||
/// </summary>
|
||||
readonly object lockObject;
|
||||
|
||||
/// <summary>
|
||||
/// <see cref="Action"/> to run when <see cref="Dispose"/>d.
|
||||
/// </summary>
|
||||
Action onDispose;
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="BridgeRegistration"/> <see langword="class"/>.
|
||||
/// </summary>
|
||||
/// <param name="onDispose">The value of <see cref="onDispose"/>.</param>
|
||||
public BridgeRegistration(Action onDispose)
|
||||
{
|
||||
this.onDispose = onDispose ?? throw new ArgumentNullException(nameof(onDispose));
|
||||
lockObject = new object();
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public void Dispose()
|
||||
{
|
||||
lock(lockObject)
|
||||
{
|
||||
onDispose?.Invoke();
|
||||
onDispose = null;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,25 +0,0 @@
|
||||
using Tgstation.Server.Host.Components.Chat;
|
||||
|
||||
namespace Tgstation.Server.Host.Components.Interop
|
||||
{
|
||||
/// <summary>
|
||||
/// Represents a chat command to be handled by DD
|
||||
/// </summary>
|
||||
sealed class ChatCommand
|
||||
{
|
||||
/// <summary>
|
||||
/// The command name
|
||||
/// </summary>
|
||||
public string Command { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The command params
|
||||
/// </summary>
|
||||
public string Params { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The <see cref="Chat.User"/> that sent the command
|
||||
/// </summary>
|
||||
public User User { get; set; }
|
||||
}
|
||||
}
|
||||
@@ -1,20 +0,0 @@
|
||||
using System.Collections.Generic;
|
||||
|
||||
namespace Tgstation.Server.Host.Components.Interop
|
||||
{
|
||||
/// <summary>
|
||||
/// Represents a command from DD
|
||||
/// </summary>
|
||||
sealed class CommCommand
|
||||
{
|
||||
/// <summary>
|
||||
/// The dictionary of the <see cref="CommCommand"/>
|
||||
/// </summary>
|
||||
public IReadOnlyDictionary<string, object> Parameters { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The raw JSON of the <see cref="CommCommand"/>
|
||||
/// </summary>
|
||||
public string RawJson { get; set; }
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
{
|
||||
/// <inheritdoc />
|
||||
sealed class CommContext : ICommContext
|
||||
{
|
||||
/// <summary>
|
||||
/// The <see cref="IIOManager"/> for the <see cref="CommContext"/>
|
||||
/// </summary>
|
||||
readonly IIOManager ioManager;
|
||||
|
||||
/// <summary>
|
||||
/// The <see cref="ILogger"/> for the <see cref="CommContext"/>
|
||||
/// </summary>
|
||||
readonly ILogger<CommContext> logger;
|
||||
|
||||
/// <summary>
|
||||
/// The <see cref="FileSystemWatcher"/> for the <see cref="CommContext"/>
|
||||
/// </summary>
|
||||
readonly FileSystemWatcher fileSystemWatcher;
|
||||
|
||||
/// <summary>
|
||||
/// The <see cref="CancellationTokenSource"/> for the <see cref="CommContext"/>
|
||||
/// </summary>
|
||||
readonly CancellationTokenSource cancellationTokenSource;
|
||||
|
||||
/// <summary>
|
||||
/// The <see cref="CancellationToken"/> for the <see cref="CommContext"/>
|
||||
/// </summary>
|
||||
readonly CancellationToken cancellationToken;
|
||||
|
||||
/// <summary>
|
||||
/// The <see cref="ICommHandler"/> for the <see cref="CommContext"/>
|
||||
/// </summary>
|
||||
ICommHandler handler;
|
||||
|
||||
/// <summary>
|
||||
/// If the <see cref="CommContext"/> has been disposed
|
||||
/// </summary>
|
||||
bool disposed;
|
||||
|
||||
/// <summary>
|
||||
/// Construct an <see cref="CommContext"/>
|
||||
/// </summary>
|
||||
/// <param name="ioManager">The value of <see cref="ioManager"/></param>
|
||||
/// <param name="logger">The value of <see cref="logger"/></param>
|
||||
/// <param name="directory">The path to watch</param>
|
||||
/// <param name="filter">The filter to watch for</param>
|
||||
public CommContext(IIOManager ioManager, ILogger<CommContext> 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;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public void Dispose()
|
||||
{
|
||||
if (disposed)
|
||||
return;
|
||||
disposed = true;
|
||||
fileSystemWatcher.Dispose();
|
||||
cancellationTokenSource.Cancel();
|
||||
cancellationTokenSource.Dispose();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Runs when the <see cref="fileSystemWatcher"/> triggers
|
||||
/// </summary>
|
||||
/// <param name="sender">The sender of the event</param>
|
||||
/// <param name="e">The <see cref="FileSystemEventArgs"/></param>
|
||||
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<IReadOnlyDictionary<string, object>>(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);
|
||||
}
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public void RegisterHandler(ICommHandler handler)
|
||||
{
|
||||
if (this.handler != null)
|
||||
throw new InvalidOperationException("RegisterHandler already called!");
|
||||
this.handler = handler ?? throw new ArgumentNullException(nameof(handler));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,93 +0,0 @@
|
||||
namespace Tgstation.Server.Host.Components.Interop
|
||||
{
|
||||
/// <summary>
|
||||
/// Constants used for communication with the DMAPI
|
||||
/// </summary>
|
||||
static class Constants
|
||||
{
|
||||
/// <summary>
|
||||
/// Identifies a TGS execution. The server version
|
||||
/// </summary>
|
||||
public const string DMParamHostVersion = "server_service_version";
|
||||
|
||||
/// <summary>
|
||||
/// Path to the <see cref="JsonFile"/>
|
||||
/// </summary>
|
||||
public const string DMParamInfoJson = "tgs_json";
|
||||
|
||||
/// <summary>
|
||||
/// The <see cref="JsonFile.AccessIdentifier"/>
|
||||
/// </summary>
|
||||
public const string DMInteropAccessIdentifier = "tgs_tok";
|
||||
|
||||
/// <summary>
|
||||
/// Generic OK response
|
||||
/// </summary>
|
||||
public const string DMResponseSuccess = "tgs_succ";
|
||||
|
||||
/// <summary>
|
||||
/// Change port
|
||||
/// </summary>
|
||||
public const string DMTopicChangePort = "tgs_port";
|
||||
|
||||
/// <summary>
|
||||
/// Change reboot mode
|
||||
/// </summary>
|
||||
public const string DMTopicChangeReboot = "tgs_rmode";
|
||||
|
||||
/// <summary>
|
||||
/// Chat command
|
||||
/// </summary>
|
||||
public const string DMTopicChatCommand = "tgs_chat_comm";
|
||||
|
||||
/// <summary>
|
||||
/// Notify of an <see cref="EventType"/>
|
||||
/// </summary>
|
||||
public const string DMTopicEvent = "tgs_event";
|
||||
|
||||
/// <summary>
|
||||
/// Response to an interop export from DM
|
||||
/// </summary>
|
||||
public const string DMTopicInteropResponse = "tgs_interop";
|
||||
|
||||
/// <summary>
|
||||
/// Set port command
|
||||
/// </summary>
|
||||
public const string DMCommandNewPort = "tgs_new_port";
|
||||
|
||||
/// <summary>
|
||||
/// API validation command
|
||||
/// </summary>
|
||||
public const string DMCommandApiValidate = "tgs_validate";
|
||||
|
||||
/// <summary>
|
||||
/// Server primed command
|
||||
/// </summary>
|
||||
public const string DMCommandServerPrimed = "tgs_prime";
|
||||
|
||||
/// <summary>
|
||||
/// World reboot command
|
||||
/// </summary>
|
||||
public const string DMCommandWorldReboot = "tgs_reboot";
|
||||
|
||||
/// <summary>
|
||||
/// Terminate process command
|
||||
/// </summary>
|
||||
public const string DMCommandEndProcess = "tgs_kill";
|
||||
|
||||
/// <summary>
|
||||
/// Chat send command
|
||||
/// </summary>
|
||||
public const string DMCommandChat = "tgs_chat_send";
|
||||
|
||||
/// <summary>
|
||||
/// Topic command parameter
|
||||
/// </summary>
|
||||
public const string DMParameterCommand = "tgs_com";
|
||||
|
||||
/// <summary>
|
||||
/// Command data
|
||||
/// </summary>
|
||||
public const string DMParameterData = "tgs_data";
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,50 @@
|
||||
using Newtonsoft.Json;
|
||||
using Newtonsoft.Json.Converters;
|
||||
using Newtonsoft.Json.Serialization;
|
||||
using System;
|
||||
|
||||
namespace Tgstation.Server.Host.Components.Interop
|
||||
{
|
||||
/// <summary>
|
||||
/// Constants used for communication with the DMAPI
|
||||
/// </summary>
|
||||
static class DMApiConstants
|
||||
{
|
||||
/// <summary>
|
||||
/// Identifies a DMAPI execution with the version as the value.
|
||||
/// </summary>
|
||||
public const string ParamApiVersion = "server_service_version";
|
||||
|
||||
/// <summary>
|
||||
/// Identifies the path to the <see cref="Runtime.RuntimeInformation"/> file.
|
||||
/// </summary>
|
||||
public const string ParamDeploymentInformationFile = "tgs_json";
|
||||
|
||||
/// <summary>
|
||||
/// Parameter json is encoded in for topic requests.
|
||||
/// </summary>
|
||||
public const string TopicData = "data";
|
||||
|
||||
/// <summary>
|
||||
/// The DMAPI <see cref="Version"/> being used.
|
||||
/// </summary>
|
||||
public static readonly Version Version = new Version(5, 0, 0);
|
||||
|
||||
/// <summary>
|
||||
/// <see cref="JsonSerializerSettings"/> for use when communicating with the DMAPI.
|
||||
/// </summary>
|
||||
public static readonly JsonSerializerSettings SerializerSettings = new JsonSerializerSettings
|
||||
{
|
||||
ContractResolver = new DefaultContractResolver
|
||||
{
|
||||
NamingStrategy = new CamelCaseNamingStrategy()
|
||||
},
|
||||
Converters = new[]
|
||||
{
|
||||
new VersionConverter()
|
||||
},
|
||||
DefaultValueHandling = DefaultValueHandling.Ignore,
|
||||
ReferenceLoopHandling = ReferenceLoopHandling.Ignore
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
namespace Tgstation.Server.Host.Components.Interop
|
||||
{
|
||||
public class DMApiParameters
|
||||
{
|
||||
public string AccessIdentifier { get; set; }
|
||||
}
|
||||
}
|
||||
@@ -1,20 +0,0 @@
|
||||
using System.Collections.Generic;
|
||||
|
||||
namespace Tgstation.Server.Host.Components.Interop
|
||||
{
|
||||
/// <summary>
|
||||
/// For notifying DD of <see cref="EventType"/>s
|
||||
/// </summary>
|
||||
sealed class EventNotification
|
||||
{
|
||||
/// <summary>
|
||||
/// The <see cref="EventType"/>
|
||||
/// </summary>
|
||||
public EventType Type { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The event parameters
|
||||
/// </summary>
|
||||
public IEnumerable<string> Parameters { get; set; }
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace Tgstation.Server.Host.Components.Interop
|
||||
{
|
||||
/// <inheritdoc />
|
||||
interface IBridgeHandler : IBridgeHandlerBase
|
||||
{
|
||||
/// <summary>
|
||||
/// The <see cref="RuntimeInformation.AccessIdentifier"/> for the <see cref="IBridgeHandler"/>.
|
||||
/// </summary>
|
||||
string AccessIdentifier { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Called when the owning <see cref="Instance"/> is renamed.
|
||||
/// </summary>
|
||||
/// <param name="newInstanceName">The new <see cref="Api.Models.Instance.Name"/>.</param>
|
||||
/// <param name="cancellationToken">The <see cref="CancellationToken"/> for the operation.</param>
|
||||
/// <returns>A <see cref="Task"/> representing the running operation.</returns>
|
||||
Task InstanceRenamed(string newInstanceName, CancellationToken cancellationToken);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using Tgstation.Server.Host.Components.Interop.Bridge;
|
||||
|
||||
namespace Tgstation.Server.Host.Components.Interop
|
||||
{
|
||||
/// <summary>
|
||||
/// Handler for <see cref="BridgeParameters"/>.
|
||||
/// </summary>
|
||||
public interface IBridgeHandlerBase
|
||||
{
|
||||
/// <summary>
|
||||
/// Handle a set of bridge <paramref name="parameters"/>.
|
||||
/// </summary>
|
||||
/// <param name="parameters">The <see cref="BridgeParameters"/> to handle.</param>
|
||||
/// <param name="cancellationToken">The <see cref="CancellationToken"/> for the operation.</param>
|
||||
/// <returns>A <see cref="Task"/> representing the running operation.</returns>
|
||||
Task<BridgeResponse> ProcessBridgeRequest(BridgeParameters parameters, CancellationToken cancellationToken);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
namespace Tgstation.Server.Host.Components.Interop
|
||||
{
|
||||
/// <summary>
|
||||
/// Registers <see cref="IBridgeHandler"/>s.
|
||||
/// </summary>
|
||||
interface IBridgeRegistrar
|
||||
{
|
||||
/// <summary>
|
||||
/// Register a given <paramref name="bridgeHandler"/>.
|
||||
/// </summary>
|
||||
/// <param name="bridgeHandler">The <see cref="IBridgeHandler"/> to register.</param>
|
||||
/// <returns>A representative <see cref="IBridgeRegistration"/>.</returns>
|
||||
IBridgeRegistration RegisterHandler(IBridgeHandler bridgeHandler);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
using System;
|
||||
|
||||
namespace Tgstation.Server.Host.Components.Interop
|
||||
{
|
||||
/// <summary>
|
||||
/// Represents a registration of an interop session.
|
||||
/// </summary>
|
||||
interface IBridgeRegistration : IDisposable
|
||||
{
|
||||
}
|
||||
}
|
||||
@@ -1,16 +0,0 @@
|
||||
using System;
|
||||
|
||||
namespace Tgstation.Server.Host.Components.Interop
|
||||
{
|
||||
/// <summary>
|
||||
/// Represents a registration of an interop session
|
||||
/// </summary>
|
||||
interface ICommContext : IDisposable
|
||||
{
|
||||
/// <summary>
|
||||
/// Register a <paramref name="handler"/> with the <see cref="ICommContext"/>
|
||||
/// </summary>
|
||||
/// <param name="handler">The <see cref="ICommHandler"/> to register</param>
|
||||
void RegisterHandler(ICommHandler handler);
|
||||
}
|
||||
}
|
||||
@@ -1,19 +0,0 @@
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace Tgstation.Server.Host.Components.Interop
|
||||
{
|
||||
/// <summary>
|
||||
/// Handles <see cref="CommCommand"/>s
|
||||
/// </summary>
|
||||
interface ICommHandler
|
||||
{
|
||||
/// <summary>
|
||||
/// Handle a <paramref name="command"/>
|
||||
/// </summary>
|
||||
/// <param name="command">The <see cref="CommCommand"/> to handle</param>
|
||||
/// <param name="cancellationToken">The <see cref="CancellationToken"/> for the operation</param>
|
||||
/// <returns>A <see cref="Task"/> representing the running operation</returns>
|
||||
Task HandleInterop(CommCommand command, CancellationToken cancellationToken);
|
||||
}
|
||||
}
|
||||
@@ -1,56 +0,0 @@
|
||||
using System.Collections.Generic;
|
||||
using Tgstation.Server.Api.Models;
|
||||
|
||||
namespace Tgstation.Server.Host.Components.Interop
|
||||
{
|
||||
/// <summary>
|
||||
/// Representation of the initial json passed to DreamDaemon
|
||||
/// </summary>
|
||||
sealed class JsonFile
|
||||
{
|
||||
/// <summary>
|
||||
/// The code used by the server to authenticate command Topics
|
||||
/// </summary>
|
||||
public string AccessIdentifier { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// If DD should just respond if it's API is working and then exit
|
||||
/// </summary>
|
||||
public bool ApiValidateOnly { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The <see cref="Api.Models.Instance.Name"/> of the owner at the time of launch
|
||||
/// </summary>
|
||||
public string InstanceName { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// JSON file name that contains current active chat channel information
|
||||
/// </summary>
|
||||
public string ChatChannelsJson { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// JSON file DD should write to with available chat commands
|
||||
/// </summary>
|
||||
public string ChatCommandsJson { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// JSON file DD should write to to send commands to the server
|
||||
/// </summary>
|
||||
public string ServerCommandsJson { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The <see cref="Api.Models.Internal.RevisionInformation"/> of the launch
|
||||
/// </summary>
|
||||
public Api.Models.Internal.RevisionInformation Revision { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The <see cref="DreamDaemonSecurity"/> level of the launch
|
||||
/// </summary>
|
||||
public DreamDaemonSecurity SecurityLevel { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The <see cref="TestMerge"/>s in the launch
|
||||
/// </summary>
|
||||
public List<TestMerge> TestMerges { get; } = new List<TestMerge>();
|
||||
}
|
||||
}
|
||||
+7
-14
@@ -1,12 +1,12 @@
|
||||
using System;
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
|
||||
namespace Tgstation.Server.Host.Components.Interop
|
||||
namespace Tgstation.Server.Host.Components.Interop.Runtime
|
||||
{
|
||||
/// <summary>
|
||||
/// Information used in for reattaching and interop
|
||||
/// </summary>
|
||||
public class JsonSubFileList
|
||||
public class RuntimeFileList
|
||||
{
|
||||
/// <summary>
|
||||
/// Path to the chat commands json file
|
||||
@@ -21,27 +21,20 @@ namespace Tgstation.Server.Host.Components.Interop
|
||||
public string ChatChannelsJson { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Path to the server commands json file
|
||||
/// Construct an <see cref="RuntimeFileList"/>
|
||||
/// </summary>
|
||||
[Required]
|
||||
public string ServerCommandsJson { get; set; }
|
||||
protected RuntimeFileList() { }
|
||||
|
||||
/// <summary>
|
||||
/// Construct an <see cref="JsonSubFileList"/>
|
||||
/// Construct an <see cref="RuntimeFileList"/> from a <paramref name="copy"/>
|
||||
/// </summary>
|
||||
protected JsonSubFileList() { }
|
||||
|
||||
/// <summary>
|
||||
/// Construct an <see cref="JsonSubFileList"/> from a <paramref name="copy"/>
|
||||
/// </summary>
|
||||
/// <param name="copy">An <see cref="JsonSubFileList"/> to copy</param>
|
||||
public JsonSubFileList(JsonSubFileList copy)
|
||||
/// <param name="copy">An <see cref="RuntimeFileList"/> to copy</param>
|
||||
public RuntimeFileList(RuntimeFileList copy)
|
||||
{
|
||||
if (copy == null)
|
||||
throw new ArgumentNullException(nameof(copy));
|
||||
ChatChannelsJson = copy.ChatChannelsJson;
|
||||
ChatCommandsJson = copy.ChatCommandsJson;
|
||||
ServerCommandsJson = copy.ServerCommandsJson;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
{
|
||||
/// <summary>
|
||||
/// Representation of the initial json passed to DreamDaemon
|
||||
/// </summary>
|
||||
sealed class RuntimeInformation : RuntimeFileList
|
||||
{
|
||||
/// <summary>
|
||||
/// The code used by the server to authenticate command Topics
|
||||
/// </summary>
|
||||
public string AccessIdentifier { get; }
|
||||
|
||||
/// <summary>
|
||||
/// The <see cref="IApplication.Version"/>.
|
||||
/// </summary>
|
||||
public Version ServerVersion { get; }
|
||||
|
||||
/// <summary>
|
||||
/// The port the HTTP server is running on
|
||||
/// </summary>
|
||||
public ushort ServerPort { get; }
|
||||
|
||||
/// <summary>
|
||||
/// If DD should just respond if it's API is working and then exit.
|
||||
/// </summary>
|
||||
public bool ApiValidateOnly { get; }
|
||||
|
||||
/// <summary>
|
||||
/// The <see cref="Api.Models.Instance.Name"/> of the owner at the time of launch
|
||||
/// </summary>
|
||||
public string InstanceName { get; }
|
||||
|
||||
/// <summary>
|
||||
/// The <see cref="Api.Models.Internal.RevisionInformation"/> of the launch
|
||||
/// </summary>
|
||||
public Api.Models.Internal.RevisionInformation Revision { get; }
|
||||
|
||||
/// <summary>
|
||||
/// The <see cref="DreamDaemonSecurity"/> level of the launch
|
||||
/// </summary>
|
||||
public DreamDaemonSecurity SecurityLevel { get; }
|
||||
|
||||
/// <summary>
|
||||
/// The <see cref="RuntimeTestMerge"/>s in the launch
|
||||
/// </summary>
|
||||
public IReadOnlyCollection<RuntimeTestMerge> TestMerges { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="RuntimeInformation"/> <see langword="class"/>.
|
||||
/// </summary>
|
||||
/// <param name="application">The <see cref="IApplication"/> to use.</param>
|
||||
/// <param name="cryptographySuite">The <see cref="ICryptographySuite"/> to use.</param>
|
||||
/// <param name="testMerges">An <see cref="IEnumerable{T}"/> used to construct the value of <see cref="TestMerges"/>.</param>
|
||||
/// <param name="instance">The <see cref="Instance"/> used to set <see cref="InstanceName"/>.</param>
|
||||
/// <param name="revision">The value of <see cref="RevisionInformation"/>.</param>
|
||||
/// <param name="channelsJson">The value of <see cref="RuntimeFileList.ChatChannelsJson"/>.</param>
|
||||
/// <param name="commandsJson">The value of <see cref="RuntimeFileList.ChatCommandsJson"/>.</param>
|
||||
/// <param name="securityLevel">The value of <see cref="SecurityLevel"/>.</param>
|
||||
public RuntimeInformation(
|
||||
IApplication application,
|
||||
ICryptographySuite cryptographySuite,
|
||||
IServerPortProvider portProvider,
|
||||
IEnumerable<RuntimeTestMerge> 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
+5
-5
@@ -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
|
||||
{
|
||||
/// <summary>
|
||||
/// This model mirrors /datum/tgs_revision_information/test_merge
|
||||
/// </summary>
|
||||
sealed class TestMerge : TestMergeBase
|
||||
sealed class RuntimeTestMerge : TestMergeBase
|
||||
{
|
||||
/// <summary>
|
||||
/// 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; }
|
||||
|
||||
/// <summary>
|
||||
/// The <see cref="RevisionInformation"/> of the <see cref="TestMerge"/>
|
||||
/// The <see cref="RevisionInformation"/> of the <see cref="RuntimeTestMerge"/>
|
||||
/// </summary>
|
||||
public RevisionInformation Revision { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Construct a <see cref="TestMerge"/>
|
||||
/// Construct a <see cref="RuntimeTestMerge"/>
|
||||
/// </summary>
|
||||
/// <param name="testMerge">The <see cref="Models.TestMerge"/> to build from</param>
|
||||
/// <param name="revision">The value of <see cref="Revision"/></param>
|
||||
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));
|
||||
@@ -0,0 +1,39 @@
|
||||
using System;
|
||||
using Tgstation.Server.Host.Components.Chat;
|
||||
|
||||
namespace Tgstation.Server.Host.Components.Interop.Topic
|
||||
{
|
||||
/// <summary>
|
||||
/// Represents a chat command to be handled by DD
|
||||
/// </summary>
|
||||
sealed class ChatCommand
|
||||
{
|
||||
/// <summary>
|
||||
/// The command name
|
||||
/// </summary>
|
||||
public string Command { get; }
|
||||
|
||||
/// <summary>
|
||||
/// The command params
|
||||
/// </summary>
|
||||
public string Params { get; }
|
||||
|
||||
/// <summary>
|
||||
/// The <see cref="Chat.User"/> that sent the command
|
||||
/// </summary>
|
||||
public User User { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="ChatCommand"/> <see langword="class"/>.
|
||||
/// </summary>
|
||||
/// <param name="user">The value of <see cref="User"/>.</param>
|
||||
/// <param name="command">The value of <see cref="Command"/>.</param>
|
||||
/// <param name="parameters">The value of <see cref="Parames"/>.</param>
|
||||
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));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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<object> Parameters { get; }
|
||||
|
||||
public EventNotification(EventType eventType, IEnumerable<object> parameters = null)
|
||||
{
|
||||
EventType = eventType;
|
||||
Parameters = parameters?.ToList();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
namespace Tgstation.Server.Host.Components.Interop.Topic
|
||||
{
|
||||
/// <summary>
|
||||
/// The type of topic command being sent.
|
||||
/// </summary>
|
||||
enum TopicCommandType
|
||||
{
|
||||
/// <summary>
|
||||
/// Invoking a custom chat command.
|
||||
/// </summary>
|
||||
ChatCommand,
|
||||
|
||||
/// <summary>
|
||||
/// Notification of a TGS event.
|
||||
/// </summary>
|
||||
Event,
|
||||
|
||||
/// <summary>
|
||||
/// Port change request.
|
||||
/// </summary>
|
||||
ChangePort,
|
||||
|
||||
/// <summary>
|
||||
/// Reboot state change request.
|
||||
/// </summary>
|
||||
ChangeRebootState,
|
||||
|
||||
/// <summary>
|
||||
/// The owning instance was renamed.
|
||||
/// </summary>
|
||||
InstanceRenamed
|
||||
}
|
||||
}
|
||||
@@ -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));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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<Response> ChatResponses { get; set; }
|
||||
}
|
||||
}
|
||||
@@ -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,
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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
|
||||
/// <inheritdoc />
|
||||
public Task<int> Lifetime { get; }
|
||||
|
||||
/// <inheritdoc />
|
||||
public Version DMApiVersion => throw new NotSupportedException();
|
||||
|
||||
/// <summary>
|
||||
/// If the <see cref="DeadSessionController"/> was <see cref="Dispose"/>d
|
||||
/// </summary>
|
||||
@@ -87,7 +91,7 @@ namespace Tgstation.Server.Host.Components.Watchdog
|
||||
public void ResetRebootState() => throw new NotSupportedException();
|
||||
|
||||
/// <inheritdoc />
|
||||
public Task<string> SendCommand(string command, CancellationToken cancellationToken) => throw new NotSupportedException();
|
||||
public Task<TopicResponse> SendCommand(TopicParameters parameters, CancellationToken cancellationToken) => throw new NotSupportedException();
|
||||
|
||||
/// <inheritdoc />
|
||||
public void SetHighPriority() => throw new NotSupportedException();
|
||||
|
||||
@@ -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<ISessionController> 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);
|
||||
|
||||
|
||||
@@ -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
|
||||
/// </summary>
|
||||
ApiValidationStatus ApiValidationStatus { get; }
|
||||
|
||||
/// <summary>
|
||||
/// The DMAPI <see cref="Version"/>.
|
||||
/// </summary>
|
||||
Version DMApiVersion { get; }
|
||||
|
||||
/// <summary>
|
||||
/// The <see cref="IDmbProvider"/> being used
|
||||
/// </summary>
|
||||
@@ -64,10 +71,10 @@ namespace Tgstation.Server.Host.Components.Watchdog
|
||||
/// <summary>
|
||||
/// Sends a command to DreamDaemon through /world/Topic()
|
||||
/// </summary>
|
||||
/// <param name="command">The sanitized command to send</param>
|
||||
/// <param name="parameters">The <see cref="TopicParameters"/> to send.</param>
|
||||
/// <param name="cancellationToken">The <see cref="CancellationToken"/> for the operation</param>
|
||||
/// <returns>A <see cref="Task{TResult}"/> resulting in the result of /world/Topic()</returns>
|
||||
Task<string> SendCommand(string command, CancellationToken cancellationToken);
|
||||
/// <returns>A <see cref="Task{TResult}"/> resulting in the <see cref="TopicResponse"/> of /world/Topic()</returns>
|
||||
Task<TopicResponse> SendCommand(TopicParameters parameters, CancellationToken cancellationToken);
|
||||
|
||||
/// <summary>
|
||||
/// Causes the world to start listening on a <paramref name="newPort"/>
|
||||
|
||||
@@ -14,15 +14,22 @@ namespace Tgstation.Server.Host.Components.Watchdog
|
||||
/// <summary>
|
||||
/// Create a <see cref="ISessionController"/> from a freshly launch DreamDaemon instance
|
||||
/// </summary>
|
||||
/// <param name="launchParameters">The <see cref="DreamDaemonLaunchParameters"/> to use. <see cref="DreamDaemonLaunchParameters.SecurityLevel"/> will be updated with the minumum required security level for the launch</param>
|
||||
/// <param name="dmbProvider">The <see cref="IDmbProvider"/> to use</param>
|
||||
/// <param name="currentByondLock">The current <see cref="IByondExecutableLock"/> if any</param>
|
||||
/// <param name="launchParameters">The <see cref="DreamDaemonLaunchParameters"/> to use. <see cref="DreamDaemonLaunchParameters.SecurityLevel"/> will be updated with the minumum required security level for the launch.</param>
|
||||
/// <param name="primaryPort">If the <see cref="DreamDaemonLaunchParameters.PrimaryPort"/> of <paramref name="launchParameters"/> should be used</param>
|
||||
/// <param name="primaryDirectory">If the <see cref="IDmbProvider.PrimaryDirectory"/> of <paramref name="dmbProvider"/> should be used</param>
|
||||
/// <param name="apiValidate">If the <see cref="ISessionController"/> should only validate the DMAPI then exit</param>
|
||||
/// <param name="cancellationToken">The <see cref="CancellationToken"/> for the operation</param>
|
||||
/// <returns>A <see cref="Task{TResult}"/> resulting in a new <see cref="ISessionController"/></returns>
|
||||
Task<ISessionController> LaunchNew(DreamDaemonLaunchParameters launchParameters, IDmbProvider dmbProvider, IByondExecutableLock currentByondLock, bool primaryPort, bool primaryDirectory, bool apiValidate, CancellationToken cancellationToken);
|
||||
Task<ISessionController> LaunchNew(
|
||||
IDmbProvider dmbProvider,
|
||||
IByondExecutableLock currentByondLock,
|
||||
DreamDaemonLaunchParameters launchParameters,
|
||||
bool primaryPort,
|
||||
bool primaryDirectory,
|
||||
bool apiValidate,
|
||||
CancellationToken cancellationToken);
|
||||
|
||||
/// <summary>
|
||||
/// Create a <see cref="ISessionController"/> from an existing DreamDaemon instance
|
||||
@@ -30,7 +37,9 @@ namespace Tgstation.Server.Host.Components.Watchdog
|
||||
/// <param name="reattachInformation">The <see cref="ReattachInformation"/> to use</param>
|
||||
/// <param name="cancellationToken">The <see cref="CancellationToken"/> for the operation</param>
|
||||
/// <returns>A <see cref="Task{TResult}"/> resulting in a new <see cref="ISessionController"/> on success or <see langword="null"/> on failure to reattach</returns>
|
||||
Task<ISessionController> Reattach(ReattachInformation reattachInformation, CancellationToken cancellationToken);
|
||||
Task<ISessionController> Reattach(
|
||||
ReattachInformation reattachInformation,
|
||||
CancellationToken cancellationToken);
|
||||
|
||||
/// <summary>
|
||||
/// Creates a <see cref="ISessionController"/> that appears to have started and died with exit code -1
|
||||
|
||||
@@ -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
|
||||
{
|
||||
/// <inheritdoc />
|
||||
sealed class SessionController : ISessionController, ICommHandler
|
||||
sealed class SessionController : ISessionController, IBridgeHandler
|
||||
{
|
||||
/// <summary>
|
||||
/// The DMAPI version being used.
|
||||
/// </summary>
|
||||
public static readonly Version DMApiVersion = new Version(5, 0, 0);
|
||||
/// <inheritdoc />
|
||||
public string AccessIdentifier => reattachInformation.AccessIdentifier;
|
||||
|
||||
/// <inheritdoc />
|
||||
public bool IsPrimary
|
||||
@@ -78,6 +76,9 @@ namespace Tgstation.Server.Host.Components.Watchdog
|
||||
}
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public Version DMApiVersion { get; private set; }
|
||||
|
||||
/// <inheritdoc />
|
||||
public bool ClosePortOnReboot { get; set; }
|
||||
|
||||
@@ -104,9 +105,9 @@ namespace Tgstation.Server.Host.Components.Watchdog
|
||||
readonly IByondTopicSender byondTopicSender;
|
||||
|
||||
/// <summary>
|
||||
/// The <see cref="ICommContext"/> for the <see cref="SessionController"/>
|
||||
/// The <see cref="IBridgeRegistration"/> for the <see cref="SessionController"/>
|
||||
/// </summary>
|
||||
readonly ICommContext interopContext;
|
||||
readonly IBridgeRegistration bridgeRegistration;
|
||||
|
||||
/// <summary>
|
||||
/// The <see cref="IProcess"/> for the <see cref="SessionController"/>
|
||||
@@ -180,7 +181,7 @@ namespace Tgstation.Server.Host.Components.Watchdog
|
||||
/// <param name="process">The value of <see cref="process"/></param>
|
||||
/// <param name="byondLock">The value of <see cref="byondLock"/></param>
|
||||
/// <param name="byondTopicSender">The value of <see cref="byondTopicSender"/></param>
|
||||
/// <param name="interopContext">The value of <see cref="interopContext"/></param>
|
||||
/// <param name="bridgeRegistrar">The <see cref="IBridgeRegistrar"/> used to populate <see cref="bridgeRegistration"/>.</param>
|
||||
/// <param name="chat">The value of <see cref="chat"/></param>
|
||||
/// <param name="chatJsonTrackingContext">The value of <see cref="chatJsonTrackingContext"/></param>
|
||||
/// <param name="logger">The value of <see cref="logger"/></param>
|
||||
@@ -192,7 +193,7 @@ namespace Tgstation.Server.Host.Components.Watchdog
|
||||
IByondExecutableLock byondLock,
|
||||
IByondTopicSender byondTopicSender,
|
||||
IJsonTrackingContext chatJsonTrackingContext,
|
||||
ICommContext interopContext,
|
||||
IBridgeRegistrar bridgeRegistrar,
|
||||
IChat chat,
|
||||
ILogger<SessionController> 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
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
#pragma warning disable CA1502 // TODO: Decomplexify
|
||||
public async Task HandleInterop(CommCommand command, CancellationToken cancellationToken)
|
||||
public async Task<BridgeResponse> 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<Response>(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<string, ushort> { { 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<DreamDaemonSecurity>(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<string, int> { { 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<object>();
|
||||
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<object>();
|
||||
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
|
||||
|
||||
/// <summary>
|
||||
/// Throws an <see cref="ObjectDisposedException"/> if <see cref="Dispose(bool)"/> has been called
|
||||
@@ -459,35 +464,48 @@ namespace Tgstation.Server.Host.Components.Watchdog
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public Task<string> SendCommand(string command, CancellationToken cancellationToken) => SendCommand(command, null, cancellationToken);
|
||||
|
||||
async Task<string> SendCommand(string command, ushort? overridePort, CancellationToken cancellationToken)
|
||||
public async Task<TopicResponse> 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<TopicResponse>(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;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
@@ -510,11 +529,14 @@ namespace Tgstation.Server.Host.Components.Watchdog
|
||||
|
||||
async Task<bool> 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;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
@@ -563,13 +590,19 @@ namespace Tgstation.Server.Host.Components.Watchdog
|
||||
/// <inheritdoc />
|
||||
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;
|
||||
/// <inheritdoc />
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
/// </summary>
|
||||
readonly IPlatformIdentifier platformIdentifier;
|
||||
|
||||
/// <summary>
|
||||
/// The <see cref="IBridgeRegistrar"/> for the <see cref="SessionControllerFactory"/>.
|
||||
/// </summary>
|
||||
readonly IBridgeRegistrar bridgeRegistrar;
|
||||
|
||||
/// <summary>
|
||||
/// The <see cref="ILoggerFactory"/> for the <see cref="SessionControllerFactory"/>
|
||||
/// </summary>
|
||||
@@ -112,6 +119,7 @@ namespace Tgstation.Server.Host.Components.Watchdog
|
||||
/// <param name="chat">The value of <see cref="chat"/></param>
|
||||
/// <param name="networkPromptReaper">The value of <see cref="networkPromptReaper"/></param>
|
||||
/// <param name="platformIdentifier">The value of <see cref="platformIdentifier"/></param>
|
||||
/// <param name="bridgeRegistrar">The value of <see cref="bridgeRegistrar"/>.</param>
|
||||
/// <param name="loggerFactory">The value of <see cref="loggerFactory"/></param>
|
||||
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));
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
#pragma warning disable CA1506 // TODO: Decomplexify
|
||||
public async Task<ISessionController> LaunchNew(DreamDaemonLaunchParameters launchParameters, IDmbProvider dmbProvider, IByondExecutableLock currentByondLock, bool primaryPort, bool primaryDirectory, bool apiValidate, CancellationToken cancellationToken)
|
||||
public async Task<ISessionController> 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<CommContext>(), 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<SessionController>(), launchParameters.SecurityLevel, launchParameters.StartupTimeout);
|
||||
},
|
||||
process,
|
||||
byondLock,
|
||||
byondTopicSender,
|
||||
chatJsonTrackingContext,
|
||||
bridgeRegistrar,
|
||||
chat,
|
||||
loggerFactory.CreateLogger<SessionController>(),
|
||||
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
|
||||
|
||||
/// <inheritdoc />
|
||||
public async Task<ISessionController> Reattach(ReattachInformation reattachInformation, CancellationToken cancellationToken)
|
||||
public async Task<ISessionController> 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<CommContext>(), 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<SessionController>(),
|
||||
null,
|
||||
null);
|
||||
|
||||
if (process != null)
|
||||
try
|
||||
{
|
||||
networkPromptReaper.RegisterProcess(process);
|
||||
result = new SessionController(reattachInformation, process, byondLock, byondTopicSender, chatJsonTrackingContext, context, chat, loggerFactory.CreateLogger<SessionController>(), 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
|
||||
{
|
||||
|
||||
@@ -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<Response> responses;
|
||||
try
|
||||
{
|
||||
responses = JsonConvert.DeserializeObject<List<Response>>(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!");
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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
|
||||
{
|
||||
/// <summary>
|
||||
/// <see cref="Controller"/> for recieving DMAPI requests from DreamDaemon.
|
||||
/// </summary>
|
||||
[Route("Bridge")]
|
||||
[Produces(ApiHeaders.ApplicationJson)]
|
||||
public class BridgeController : Controller
|
||||
{
|
||||
/// <summary>
|
||||
/// The <see cref="IInstanceManager"/> for the <see cref="BridgeController"/>
|
||||
/// </summary>
|
||||
readonly IInstanceManager instanceManager;
|
||||
|
||||
/// <summary>
|
||||
/// The <see cref="ILogger"/> for the <see cref="BridgeController"/>
|
||||
/// </summary>
|
||||
readonly ILogger<BridgeController> logger;
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="BridgeController"/> <see langword="class"/>.
|
||||
/// </summary>
|
||||
/// <param name="instanceManager">The value of <see cref="instanceManager"/>.</param>
|
||||
/// <param name="logger">The value of <see cref="logger"/>.</param>
|
||||
public BridgeController(IInstanceManager instanceManager, ILogger<BridgeController> logger)
|
||||
{
|
||||
this.instanceManager = instanceManager ?? throw new ArgumentNullException(nameof(instanceManager));
|
||||
this.logger = logger ?? throw new ArgumentNullException(nameof(logger));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Processes a bridge request.
|
||||
/// </summary>
|
||||
/// <param name="data">JSON encoded <see cref="BridgeParameters"/>.</param>
|
||||
/// <param name="cancellationToken">The <see cref="CancellationToken"/> for the operation</param>
|
||||
/// <returns>A <see cref="Task{TResult}"/> resulting in the <see cref="IActionResult"/> for the operation.</returns>
|
||||
[HttpGet]
|
||||
public async Task<IActionResult> Process([FromQuery]string data, CancellationToken cancellationToken)
|
||||
{
|
||||
if (String.IsNullOrWhiteSpace(data))
|
||||
return BadRequest();
|
||||
|
||||
BridgeParameters request;
|
||||
try
|
||||
{
|
||||
request = JsonConvert.DeserializeObject<BridgeParameters>(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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
/// <summary>
|
||||
/// Base class for <see cref="ReattachInformation"/>
|
||||
/// </summary>
|
||||
public abstract class ReattachInformationBase : JsonSubFileList
|
||||
public abstract class ReattachInformationBase : RuntimeFileList
|
||||
{
|
||||
/// <summary>
|
||||
/// Used to identify and authenticate the DreamDaemon instance
|
||||
|
||||
@@ -109,7 +109,7 @@ namespace Tgstation.Server.Host.Components.Watchdog.Tests
|
||||
var sessionsToVerify = new List<Mock<ISessionController>>();
|
||||
|
||||
var cancellationToken = cts.Token;
|
||||
mockSessionControllerFactory.Setup(x => x.LaunchNew(mockLaunchParameters, mDmbP, null, It.IsAny<bool>(), It.IsAny<bool>(), false, cancellationToken)).Returns(() =>
|
||||
mockSessionControllerFactory.Setup(x => x.LaunchNew(mDmbP, null, mockLaunchParameters, It.IsAny<bool>(), It.IsAny<bool>(), false, cancellationToken)).Returns(() =>
|
||||
{
|
||||
var mockSession = new Mock<ISessionController>();
|
||||
mockSession.SetupGet(x => x.Lifetime).Returns(infiniteTask).Verifiable();
|
||||
|
||||
@@ -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]
|
||||
|
||||
Reference in New Issue
Block a user