Merge pull request #1790 from tgstation/v630 [TGSDeploy]

v6.3.0 Release
This commit is contained in:
Jordan Dominion
2024-02-22 22:32:36 -05:00
committed by GitHub
58 changed files with 614 additions and 220 deletions
+1 -1
View File
@@ -127,7 +127,7 @@ sudo dpkg --add-architecture i386 \
&& sudo systemctl start tgstation-server
```
The service will execute as the newly created user: `tgstation-server`.
The service will execute as the newly created user: `tgstation-server`. You should, ideally, store your instances somewhere under `/home/tgstation-server`.
##### Manual Setup
+3 -3
View File
@@ -13,14 +13,14 @@
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
<!-- Usage: Hard to say what exactly this is for, but not including it removes the test icon and breaks vstest.console.exe for some reason -->
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.8.0" Condition="'$(TgsTestNoSdk)' != 'true'" />
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.9.0" Condition="'$(TgsTestNoSdk)' != 'true'" />
<!-- Usage: Dependency mocking for tests -->
<!-- Pinned: Be VERY careful about updating https://github.com/moq/moq/issues/1372 -->
<PackageReference Include="Moq" Version="4.20.70" />
<!-- Usage: MSTest execution -->
<PackageReference Include="MSTest.TestAdapter" Version="3.2.0" />
<PackageReference Include="MSTest.TestAdapter" Version="3.2.1" />
<!-- Usage: MSTest asserts etc... -->
<PackageReference Include="MSTest.TestFramework" Version="3.2.0" />
<PackageReference Include="MSTest.TestFramework" Version="3.2.1" />
</ItemGroup>
</Project>
+7 -7
View File
@@ -3,21 +3,21 @@
<!-- Integration tests will ensure they match across the board -->
<Import Project="WebpanelVersion.props" />
<PropertyGroup>
<TgsCoreVersion>6.2.0</TgsCoreVersion>
<TgsCoreVersion>6.3.0</TgsCoreVersion>
<TgsConfigVersion>5.1.0</TgsConfigVersion>
<TgsApiVersion>10.1.0</TgsApiVersion>
<TgsApiVersion>10.2.0</TgsApiVersion>
<TgsCommonLibraryVersion>7.0.0</TgsCommonLibraryVersion>
<TgsApiLibraryVersion>13.1.0</TgsApiLibraryVersion>
<TgsClientVersion>15.1.0</TgsClientVersion>
<TgsDmapiVersion>7.0.2</TgsDmapiVersion>
<TgsInteropVersion>5.8.0</TgsInteropVersion>
<TgsApiLibraryVersion>13.2.0</TgsApiLibraryVersion>
<TgsClientVersion>15.2.0</TgsClientVersion>
<TgsDmapiVersion>7.1.0</TgsDmapiVersion>
<TgsInteropVersion>5.9.0</TgsInteropVersion>
<TgsHostWatchdogVersion>1.4.1</TgsHostWatchdogVersion>
<TgsContainerScriptVersion>1.2.1</TgsContainerScriptVersion>
<TgsMigratorVersion>2.0.0</TgsMigratorVersion>
<TgsNugetNetFramework>netstandard2.0</TgsNugetNetFramework>
<TgsNetMajorVersion>8</TgsNetMajorVersion>
<!-- Update this frequently with dotnet runtime patches. MAJOR MUST MATCH ABOVE! -->
<TgsDotnetRedistUrl>https://download.visualstudio.microsoft.com/download/pr/016c6447-764a-4210-a260-bf7a2880d5c0/a5746437a3862d7803284ae8c2290200/dotnet-hosting-8.0.1-win.exe</TgsDotnetRedistUrl>
<TgsDotnetRedistUrl>https://download.visualstudio.microsoft.com/download/pr/98ff0a08-a283-428f-8e54-19841d97154c/8c7d5f9600eadf264f04c82c813b7aab/dotnet-hosting-8.0.2-win.exe</TgsDotnetRedistUrl>
<TgsMariaDBRedistVersion>10.11.6</TgsMariaDBRedistVersion>
<TgsYarnVersion>1.22.21</TgsYarnVersion>
</PropertyGroup>
+1 -1
View File
@@ -1,6 +1,6 @@
<Project xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
<PropertyGroup>
<!-- This is in it's own file to help incremental building, changing it causes a complete rebuild of the web panel -->
<TgsWebpanelVersion>5.5.0</TgsWebpanelVersion>
<TgsWebpanelVersion>5.6.0</TgsWebpanelVersion>
</PropertyGroup>
</Project>
+1 -1
View File
@@ -1,7 +1,7 @@
#!/bin/sh -e
if [ -z "$2" ]; then
adduser --system tgstation-server
adduser --system --home /home/tgstation-server tgstation-server
mkdir -m 754 -p /var/log/tgstation-server
chown -R tgstation-server /etc/tgstation-server
chown -R tgstation-server /opt/tgstation-server/lib
@@ -3,7 +3,7 @@
"isRoot": true,
"tools": {
"wix": {
"version": "4.0.2",
"version": "4.0.4",
"commands": [
"wix"
]
@@ -1,4 +1,4 @@
<Project Sdk="WixToolset.Sdk/4.0.2" xmlns:bal="http://schemas.microsoft.com/wix/BalExtension">
<Project Sdk="WixToolset.Sdk/4.0.4" xmlns:bal="http://schemas.microsoft.com/wix/BalExtension">
<Import Project="../../../Common.props" />
<PropertyGroup>
<DefineConstants>ProductVersion=$(TgsCoreVersion);NetMajorVersion=$(TgsNetMajorVersion);DotnetRedistUrl=$(TgsDotnetRedistUrl);MariaDBRedistUrl=https://github.com/tgstation/tgstation-server/releases/download/tgstation-server-v$(TgsCoreVersion)/mariadb-$(TgsMariaDBRedistVersion)-winx64.msi</DefineConstants>
@@ -24,8 +24,8 @@
<Content Include="Theme.xml" />
</ItemGroup>
<ItemGroup>
<PackageReference Include="WixToolset.Bal.wixext" Version="4.0.2" />
<PackageReference Include="WixToolset.Netfx.wixext" Version="4.0.2" />
<PackageReference Include="WixToolset.Bal.wixext" Version="4.0.4" />
<PackageReference Include="WixToolset.Netfx.wixext" Version="4.0.4" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\Tgstation.Server.Host.Service.Wix\Tgstation.Server.Host.Service.Wix.wixproj" />
@@ -7,7 +7,7 @@
</PropertyGroup>
<ItemGroup>
<PackageReference Include="WixToolset.Dtf.CustomAction" Version="4.0.2" />
<PackageReference Include="WixToolset.Dtf.CustomAction" Version="4.0.4" />
</ItemGroup>
<ItemGroup>
@@ -1,4 +1,4 @@
<Project Sdk="WixToolset.Sdk/4.0.2">
<Project Sdk="WixToolset.Sdk/4.0.4">
<Import Project="../../../Common.props" />
<PropertyGroup>
<DefineConstants>ProductVersion=$(TgsCoreVersion)</DefineConstants>
@@ -25,8 +25,8 @@
</HarvestDirectory>
</ItemGroup>
<ItemGroup>
<PackageReference Include="WixToolset.Heat" Version="4.0.2" />
<PackageReference Include="WixToolset.Util.wixext" Version="4.0.2" />
<PackageReference Include="WixToolset.Heat" Version="4.0.4" />
<PackageReference Include="WixToolset.Util.wixext" Version="4.0.4" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\Tgstation.Server.Host.Service.Wix.Extensions\Tgstation.Server.Host.Service.Wix.Extensions.csproj" />
@@ -24,4 +24,4 @@ Installers:
Publisher: /tg/station 13
ReleaseDate: 2023-06-24 # Do not change. Set before publish by push_manifest.ps1
ManifestType: installer
ManifestVersion: 1.5.0
ManifestVersion: 1.6.0
@@ -18,4 +18,4 @@ Documentations:
ReleaseNotesUrl: https://github.com/tgstation/tgstation-server/releases/tag/tgstation-server-v0.22.475
PurchaseUrl: https://github.com/sponsors/Cyberboss
ManifestType: defaultLocale
ManifestVersion: 1.5.0
ManifestVersion: 1.6.0
@@ -5,4 +5,4 @@ PackageIdentifier: Tgstation.Server
PackageVersion: 0.22.475 # Do not change. Set before publish by push_manifest.ps1
DefaultLocale: en-US
ManifestType: version
ManifestVersion: 1.5.0
ManifestVersion: 1.6.0
+18 -1
View File
@@ -1,6 +1,6 @@
// tgstation-server DMAPI
#define TGS_DMAPI_VERSION "7.0.2"
#define TGS_DMAPI_VERSION "7.1.0"
// All functions and datums outside this document are subject to change with any version and should not be relied on.
@@ -50,6 +50,13 @@
#endif
#ifndef TGS_FILE2TEXT_NATIVE
#ifdef file2text
#error Your codebase is re-defining the BYOND proc file2text. The DMAPI requires the native version to read the result of world.Export(). You can fix this by adding "#define TGS_FILE2TEXT_NATIVE file2text" before your override of file2text to allow the DMAPI to use the native version. This will only be used for world.Export(), not regular file accesses
#endif
#define TGS_FILE2TEXT_NATIVE file2text
#endif
// EVENT CODES
/// Before a reboot mode change, extras parameters are the current and new reboot mode enums.
@@ -489,6 +496,16 @@
/// Returns a list of connected [/datum/tgs_chat_channel]s if TGS is present, null otherwise. This function may sleep if the call to [/world/proc/TgsNew] is sleeping!
/world/proc/TgsChatChannelInfo()
return
/**
* Trigger an event in TGS. Requires TGS version >= 6.3.0. Returns [TRUE] if the event was triggered successfully, [FALSE] otherwise. This function may sleep!
*
* event_name - The name of the event to trigger
* parameters - Optional list of string parameters to pass as arguments to the event script. The first parameter passed to a script will always be the running game's directory followed by these parameters.
* wait_for_completion - If set, this function will not return until the event has run to completion.
*/
/world/proc/TgsTriggerEvent(event_name, list/parameters, wait_for_completion = FALSE)
return
/*
The MIT License
+8
View File
@@ -166,3 +166,11 @@
var/datum/tgs_api/api = TGS_READ_GLOBAL(tgs)
if(api)
return api.Visibility()
/world/TgsTriggerEvent(event_name, list/parameters, wait_for_completion = FALSE)
var/datum/tgs_api/api = TGS_READ_GLOBAL(tgs)
if(api)
if(!istype(parameters, /list))
parameters = list()
return api.TriggerEvent(event_name, parameters, wait_for_completion)
+4 -1
View File
@@ -17,7 +17,7 @@ TGS_DEFINE_AND_SET_GLOBAL(tgs, null)
world.sleep_offline = FALSE // https://www.byond.com/forum/post/2894866
del(world)
world.sleep_offline = FALSE // just in case, this is BYOND after all...
sleep(1)
sleep(world.tick_lag)
TGS_DEBUG_LOG("BYOND DIDN'T TERMINATE THE WORLD!!! TICK IS: [world.time], sleep_offline: [world.sleep_offline]")
/datum/tgs_api/latest
@@ -69,3 +69,6 @@ TGS_PROTECT_DATUM(/datum/tgs_api)
/datum/tgs_api/proc/Visibility()
return TGS_UNIMPLEMENTED
/datum/tgs_api/proc/TriggerEvent(event_name, list/parameters, wait_for_completion)
return FALSE
+3 -3
View File
@@ -181,7 +181,7 @@
var/json = json_encode(data)
while(requesting_new_port && !override_requesting_new_port)
sleep(1)
sleep(world.tick_lag)
//we need some port open at this point to facilitate return communication
if(!world.port)
@@ -209,7 +209,7 @@
requesting_new_port = FALSE
while(export_lock)
sleep(1)
sleep(world.tick_lag)
export_lock = TRUE
last_interop_response = null
@@ -217,7 +217,7 @@
text2file(json, server_commands_json_path)
for(var/I = 0; I < EXPORT_TIMEOUT_DS && !last_interop_response; ++I)
sleep(1)
sleep(world.tick_lag)
if(!last_interop_response)
TGS_ERROR_LOG("Failed to get export result for: [json]")
+1 -1
View File
@@ -1 +1 @@
"5.8.0"
"5.9.0"
+9
View File
@@ -14,6 +14,7 @@
#define DMAPI5_BRIDGE_COMMAND_KILL 4
#define DMAPI5_BRIDGE_COMMAND_CHAT_SEND 5
#define DMAPI5_BRIDGE_COMMAND_CHUNK 6
#define DMAPI5_BRIDGE_COMMAND_EVENT 7
#define DMAPI5_PARAMETER_ACCESS_IDENTIFIER "accessIdentifier"
#define DMAPI5_PARAMETER_CUSTOM_COMMANDS "customCommands"
@@ -34,6 +35,7 @@
#define DMAPI5_BRIDGE_PARAMETER_VERSION "version"
#define DMAPI5_BRIDGE_PARAMETER_CHAT_MESSAGE "chatMessage"
#define DMAPI5_BRIDGE_PARAMETER_MINIMUM_SECURITY_LEVEL "minimumSecurityLevel"
#define DMAPI5_BRIDGE_PARAMETER_EVENT_INVOCATION "eventInvocation"
#define DMAPI5_BRIDGE_RESPONSE_NEW_PORT "newPort"
#define DMAPI5_BRIDGE_RESPONSE_RUNTIME_INFORMATION "runtimeInformation"
@@ -81,6 +83,7 @@
#define DMAPI5_TOPIC_COMMAND_SEND_CHUNK 9
#define DMAPI5_TOPIC_COMMAND_RECEIVE_CHUNK 10
#define DMAPI5_TOPIC_COMMAND_RECEIVE_BROADCAST 11
#define DMAPI5_TOPIC_COMMAND_COMPLETE_EVENT 12
#define DMAPI5_TOPIC_PARAMETER_COMMAND_TYPE "commandType"
#define DMAPI5_TOPIC_PARAMETER_CHAT_COMMAND "chatCommand"
@@ -116,3 +119,9 @@
#define DMAPI5_CUSTOM_CHAT_COMMAND_NAME "name"
#define DMAPI5_CUSTOM_CHAT_COMMAND_HELP_TEXT "helpText"
#define DMAPI5_CUSTOM_CHAT_COMMAND_ADMIN_ONLY "adminOnly"
#define DMAPI5_EVENT_ID "eventId"
#define DMAPI5_EVENT_INVOCATION_NAME "eventName"
#define DMAPI5_EVENT_INVOCATION_PARAMETERS "parameters"
#define DMAPI5_EVENT_INVOCATION_NOTIFY_COMPLETION "notifyCompletion"
+37 -1
View File
@@ -27,6 +27,8 @@
var/chunked_requests = 0
var/list/chunked_topics = list()
var/list/pending_events = list()
var/detached = FALSE
/datum/tgs_api/v5/New()
@@ -125,7 +127,7 @@
TGS_DEBUG_LOG("RequireInitialBridgeResponse: Starting sleep")
logged = TRUE
sleep(1)
sleep(world.tick_lag)
TGS_DEBUG_LOG("RequireInitialBridgeResponse: Passed")
@@ -249,6 +251,40 @@
WaitForReattach(TRUE)
return chat_channels.Copy()
/datum/tgs_api/v5/TriggerEvent(event_name, list/parameters, wait_for_completion)
RequireInitialBridgeResponse()
WaitForReattach(TRUE)
if(interop_version.minor < 9)
TGS_WARNING_LOG("Interop version too low for custom events!")
return FALSE
var/str_parameters = list()
for(var/i in parameters)
str_parameters += "[i]"
var/list/response = Bridge(DMAPI5_BRIDGE_COMMAND_EVENT, list(DMAPI5_BRIDGE_PARAMETER_EVENT_INVOCATION = list(DMAPI5_EVENT_INVOCATION_NAME = event_name, DMAPI5_EVENT_INVOCATION_PARAMETERS = str_parameters, DMAPI5_EVENT_INVOCATION_NOTIFY_COMPLETION = wait_for_completion)))
if(!response)
return FALSE
var/event_id = response[DMAPI5_EVENT_ID]
if(!event_id)
return FALSE
TGS_DEBUG_LOG("Created event ID: [event_id]")
if(!wait_for_completion)
return TRUE
TGS_DEBUG_LOG("Waiting for completion of event ID: [event_id]")
while(!pending_events[event_id])
sleep(world.tick_lag)
TGS_DEBUG_LOG("Completed wait on event ID: [event_id]")
pending_events -= event_id
return TRUE
/datum/tgs_api/v5/proc/DecodeChannels(chat_update_json)
TGS_DEBUG_LOG("DecodeChannels()")
var/list/chat_channels_json = chat_update_json[DMAPI5_CHAT_UPDATE_CHANNELS]
+5 -2
View File
@@ -65,7 +65,7 @@
if(detached)
// Wait up to one minute
for(var/i in 1 to 600)
sleep(1)
sleep(world.tick_lag)
if(!detached && (!require_channels || length(chat_channels)))
break
@@ -77,8 +77,11 @@
/datum/tgs_api/v5/proc/PerformBridgeRequest(bridge_request)
WaitForReattach(FALSE)
TGS_DEBUG_LOG("Bridge request start")
// This is an infinite sleep until we get a response
var/export_response = world.Export(bridge_request)
TGS_DEBUG_LOG("Bridge request complete")
if(!export_response)
TGS_ERROR_LOG("Failed bridge request: [bridge_request]")
return
@@ -88,7 +91,7 @@
TGS_ERROR_LOG("Failed bridge request, missing content!")
return
var/response_json = file2text(content)
var/response_json = TGS_FILE2TEXT_NATIVE(content)
if(!response_json)
TGS_ERROR_LOG("Failed bridge request, failed to load content!")
return
+12
View File
@@ -176,6 +176,9 @@
var/list/reattach_response = TopicResponse(error_message)
reattach_response[DMAPI5_PARAMETER_CUSTOM_COMMANDS] = ListCustomCommands()
reattach_response[DMAPI5_PARAMETER_TOPIC_PORT] = GetTopicPort()
pending_events.Cut()
return reattach_response
if(DMAPI5_TOPIC_COMMAND_SEND_CHUNK)
@@ -276,6 +279,15 @@
TGS_WORLD_ANNOUNCE(message)
return TopicResponse()
if(DMAPI5_TOPIC_COMMAND_COMPLETE_EVENT)
var/event_id = topic_parameters[DMAPI5_EVENT_ID]
if (!istext(event_id))
return TopicResponse("Invalid or missing [DMAPI5_EVENT_ID]")
TGS_DEBUG_LOG("Completing event ID [event_id]...")
pending_events[event_id] = TRUE
return TopicResponse()
return TopicResponse("Unknown command: [command]")
/datum/tgs_api/v5/proc/WorldBroadcast(message)
+9
View File
@@ -14,6 +14,7 @@
#undef DMAPI5_BRIDGE_COMMAND_KILL
#undef DMAPI5_BRIDGE_COMMAND_CHAT_SEND
#undef DMAPI5_BRIDGE_COMMAND_CHUNK
#undef DMAPI5_BRIDGE_COMMAND_EVENT
#undef DMAPI5_PARAMETER_ACCESS_IDENTIFIER
#undef DMAPI5_PARAMETER_CUSTOM_COMMANDS
@@ -34,6 +35,7 @@
#undef DMAPI5_BRIDGE_PARAMETER_VERSION
#undef DMAPI5_BRIDGE_PARAMETER_CHAT_MESSAGE
#undef DMAPI5_BRIDGE_PARAMETER_MINIMUM_SECURITY_LEVEL
#undef DMAPI5_BRIDGE_PARAMETER_EVENT_INVOCATION
#undef DMAPI5_BRIDGE_RESPONSE_NEW_PORT
#undef DMAPI5_BRIDGE_RESPONSE_RUNTIME_INFORMATION
@@ -81,6 +83,7 @@
#undef DMAPI5_TOPIC_COMMAND_SEND_CHUNK
#undef DMAPI5_TOPIC_COMMAND_RECEIVE_CHUNK
#undef DMAPI5_TOPIC_COMMAND_RECEIVE_BROADCAST
#undef DMAPI5_TOPIC_COMMAND_COMPLETE_EVENT
#undef DMAPI5_TOPIC_PARAMETER_COMMAND_TYPE
#undef DMAPI5_TOPIC_PARAMETER_CHAT_COMMAND
@@ -116,3 +119,9 @@
#undef DMAPI5_CUSTOM_CHAT_COMMAND_NAME
#undef DMAPI5_CUSTOM_CHAT_COMMAND_HELP_TEXT
#undef DMAPI5_CUSTOM_CHAT_COMMAND_ADMIN_ONLY
#undef DMAPI5_EVENT_ID
#undef DMAPI5_EVENT_INVOCATION_NAME
#undef DMAPI5_EVENT_INVOCATION_PARAMETERS
#undef DMAPI5_EVENT_INVOCATION_NOTIFY_COMPLETION
@@ -646,5 +646,11 @@ namespace Tgstation.Server.Api.Models
/// </summary>
[Description("The specified OpenDream version is too old!")]
OpenDreamTooOld,
/// <summary>
/// Failed dotnet diagnostics dump.
/// </summary>
[Description("Could not create dump as dotnet diagnostics threw an exception!")]
DotnetDiagnosticsFailure,
}
}
@@ -27,7 +27,7 @@
<!-- Usage: HTTP constants reference -->
<PackageReference Include="Microsoft.AspNetCore.Http.Extensions" Version="2.2.0" />
<!-- Usage: Decoding the 'nbf' property of JWTs -->
<PackageReference Include="Microsoft.IdentityModel.JsonWebTokens" Version="7.3.0" />
<PackageReference Include="Microsoft.IdentityModel.JsonWebTokens" Version="7.3.1" />
<!-- Usage: Primary JSON library -->
<PackageReference Include="Newtonsoft.Json" Version="13.0.3" />
<!-- Usage: Data model annotating -->
@@ -11,9 +11,9 @@
<ItemGroup>
<!-- Usage: Connecting to SignalR hubs in API -->
<PackageReference Include="Microsoft.AspNetCore.SignalR.Client" Version="8.0.1" />
<PackageReference Include="Microsoft.AspNetCore.SignalR.Client" Version="8.0.2" />
<!-- Usage: Using target JSON serializer for API -->
<PackageReference Include="Microsoft.AspNetCore.SignalR.Protocols.NewtonsoftJson" Version="8.0.1" />
<PackageReference Include="Microsoft.AspNetCore.SignalR.Protocols.NewtonsoftJson" Version="8.0.2" />
</ItemGroup>
<ItemGroup>
@@ -3,7 +3,7 @@
"isRoot": true,
"tools": {
"dotnet-ef": {
"version": "8.0.1",
"version": "8.0.2",
"commands": [
"dotnet-ef"
]
@@ -424,21 +424,22 @@ namespace Tgstation.Server.Host.Components.Deployment
/// <param name="job">The <see cref="CompileJob"/> to clean.</param>
void CleanRegisteredCompileJob(CompileJob job)
{
async Task HandleCleanup()
Task HandleCleanup()
{
// First kill the GitHub deployment
var remoteDeploymentManager = remoteDeploymentManagerFactory.CreateRemoteDeploymentManager(metadata, job);
// DCT: None available
var deploymentJob = remoteDeploymentManager.MarkInactive(job, CancellationToken.None);
var deleteTask = DeleteCompileJobContent(job.DirectoryName!.Value.ToString(), cleanupCts.Token);
var otherTask = cleanupTask;
async Task WrapThrowableTasks()
{
try
{
// First kill the GitHub deployment
var remoteDeploymentManager = remoteDeploymentManagerFactory.CreateRemoteDeploymentManager(metadata, job);
// DCT: None available
var deploymentJob = remoteDeploymentManager.MarkInactive(job, CancellationToken.None);
var deleteTask = DeleteCompileJobContent(job.DirectoryName!.Value.ToString(), cleanupCts.Token);
await ValueTaskExtensions.WhenAll(deleteTask, deploymentJob);
}
catch (Exception ex)
@@ -447,7 +448,7 @@ namespace Tgstation.Server.Host.Components.Deployment
}
}
await Task.WhenAll(otherTask, WrapThrowableTasks());
return Task.WhenAll(otherTask, WrapThrowableTasks());
}
lock (jobLockCounts)
@@ -15,7 +15,7 @@ namespace Tgstation.Server.Host.Components.Deployment
string DmbName { get; }
/// <summary>
/// The primary game directory with a trailing directory separator.
/// The primary game directory.
/// </summary>
string Directory { get; }
@@ -1,6 +1,7 @@
using System;
using System.Collections.Generic;
using System.Globalization;
using System.Text;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
@@ -9,7 +10,6 @@ using Microsoft.Extensions.Logging;
using Tgstation.Server.Api.Models;
using Tgstation.Server.Host.IO;
using Tgstation.Server.Host.Jobs;
using Tgstation.Server.Host.Utils;
namespace Tgstation.Server.Host.Components.Engine
{
@@ -23,31 +23,21 @@ namespace Tgstation.Server.Host.Components.Engine
/// </summary>
protected const string ByondBinPath = "byond/bin";
/// <summary>
/// The path to the cfg directory.
/// </summary>
protected const string CfgDirectoryName = "cfg";
/// <summary>
/// The name of BYOND's cache directory.
/// </summary>
const string CacheDirectoryName = "cache";
/// <summary>
/// The path to the cfg directory.
/// </summary>
const string CfgDirectoryName = "cfg";
/// <summary>
/// The name of the list of trusted .dmb files in the user's BYOND cfg directory.
/// </summary>
const string TrustedDmbFileName = "trusted.txt";
/// <summary>
/// The first <see cref="Version"/> of BYOND that supports the '-map-threads' parameter on DreamDaemon.
/// </summary>
static readonly Version MapThreadsVersion = new(515, 1609);
/// <summary>
/// <see cref="SemaphoreSlim"/> for writing to files in the user's BYOND directory.
/// </summary>
static readonly SemaphoreSlim UserFilesSemaphore = new(1);
/// <inheritdoc />
protected override EngineType TargetEngineType => EngineType.Byond;
@@ -144,18 +134,10 @@ namespace Tgstation.Server.Host.Components.Engine
localCfgDirectory,
cancellationToken);
// Delete trusted.txt so it doesn't grow too large
var trustedFilePath =
IOManager.ConcatPath(
localCfgDirectory,
TrustedDmbFileName);
var additionalCleanTasks = AdditionalCacheCleanFilePaths(localCfgDirectory)
.Select(path => IOManager.DeleteFile(path, cancellationToken));
Logger.LogTrace("Deleting trusted .dmbs file {trustedFilePath}", trustedFilePath);
var trustedDmbDeleteTask = IOManager.DeleteFile(
trustedFilePath,
cancellationToken);
await Task.WhenAll(cacheCleanTask, cfgCreateTask, trustedDmbDeleteTask);
await Task.WhenAll(cacheCleanTask, cfgCreateTask, Task.WhenAll(additionalCleanTasks));
}
catch (Exception ex) when (ex is not OperationCanceledException)
{
@@ -163,49 +145,6 @@ namespace Tgstation.Server.Host.Components.Engine
}
}
/// <inheritdoc />
public override async ValueTask TrustDmbPath(EngineVersion version, string fullDmbPath, CancellationToken cancellationToken)
{
ArgumentNullException.ThrowIfNull(version);
ArgumentNullException.ThrowIfNull(fullDmbPath);
var byondDir = PathToUserFolder;
var cfgDir = IOManager.ConcatPath(
byondDir,
CfgDirectoryName);
var trustedFilePath = IOManager.ConcatPath(
cfgDir,
TrustedDmbFileName);
Logger.LogDebug("Adding .dmb ({dmbPath}) to {trustedFilePath}", fullDmbPath, trustedFilePath);
using (await SemaphoreSlimContext.Lock(UserFilesSemaphore, cancellationToken))
{
string trustedFileText;
var filePreviouslyExisted = await IOManager.FileExists(trustedFilePath, cancellationToken);
if (filePreviouslyExisted)
{
var trustedFileBytes = await IOManager.ReadAllBytes(trustedFilePath, cancellationToken);
trustedFileText = Encoding.UTF8.GetString(trustedFileBytes);
trustedFileText = $"{trustedFileText.Trim()}{Environment.NewLine}";
}
else
trustedFileText = String.Empty;
if (trustedFileText.Contains(fullDmbPath, StringComparison.Ordinal))
return;
trustedFileText = $"{trustedFileText}{fullDmbPath}{Environment.NewLine}";
var newTrustedFileBytes = Encoding.UTF8.GetBytes(trustedFileText);
if (!filePreviouslyExisted)
await IOManager.CreateDirectory(cfgDir, cancellationToken);
await IOManager.WriteAllBytes(trustedFilePath, newTrustedFileBytes, cancellationToken);
}
}
/// <inheritdoc />
public override async ValueTask<IEngineInstallationData> DownloadVersion(EngineVersion version, JobProgressReporter? progressReporter, CancellationToken cancellationToken)
{
@@ -240,6 +179,13 @@ namespace Tgstation.Server.Host.Components.Engine
/// <returns>The file name of the DreamDaemon executable.</returns>
protected abstract string GetDreamDaemonName(Version byondVersion, out bool supportsCli);
/// <summary>
/// List off additional file paths in the <paramref name="configDirectory"/> to delete.
/// </summary>
/// <param name="configDirectory">The full path to the relevant <see cref="CfgDirectoryName"/>.</param>
/// <returns>An <see cref="IEnumerable{T}"/> of paths in <paramref name="configDirectory"/> to clean.</returns>
protected virtual IEnumerable<string> AdditionalCacheCleanFilePaths(string configDirectory) => Enumerable.Empty<string>();
/// <summary>
/// Create a <see cref="Uri"/> pointing to the location of the download for a given <paramref name="version"/>.
/// </summary>
@@ -67,7 +67,7 @@ namespace Tgstation.Server.Host.Components.Engine
Environment.GetFolderPath(
Environment.SpecialFolder.UserProfile,
Environment.SpecialFolderOption.DoNotVerify),
"./byond/cache"));
"./.byond/cache"));
}
/// <inheritdoc />
@@ -119,6 +119,16 @@ namespace Tgstation.Server.Host.Components.Engine
return ValueTask.CompletedTask;
}
/// <inheritdoc />
public override ValueTask TrustDmbPath(EngineVersion version, string fullDmbPath, CancellationToken cancellationToken)
{
ArgumentNullException.ThrowIfNull(version);
ArgumentNullException.ThrowIfNull(fullDmbPath);
Logger.LogTrace("No need to trust .dmb path \"{path}\" on POSIX", fullDmbPath);
return ValueTask.CompletedTask;
}
/// <inheritdoc />
protected override string GetDreamDaemonName(Version byondVersion, out bool supportsCli)
{
@@ -47,6 +47,16 @@ namespace Tgstation.Server.Host.Components.Engine
/// </summary>
const string TgsFirewalledDDFile = "TGSFirewalledDD";
/// <summary>
/// The name of the list of trusted .dmb files in the user's BYOND cfg directory.
/// </summary>
const string TrustedDmbFileName = "trusted.txt";
/// <summary>
/// <see cref="SemaphoreSlim"/> for writing to files in the user's BYOND directory.
/// </summary>
static readonly SemaphoreSlim UserFilesSemaphore = new(1, 1);
/// <summary>
/// The first version of BYOND to ship with dd.exe on the Windows build.
/// </summary>
@@ -168,6 +178,49 @@ namespace Tgstation.Server.Host.Components.Engine
await AddDreamDaemonToFirewall(version, path, true, cancellationToken);
}
/// <inheritdoc />
public override async ValueTask TrustDmbPath(EngineVersion version, string fullDmbPath, CancellationToken cancellationToken)
{
ArgumentNullException.ThrowIfNull(version);
ArgumentNullException.ThrowIfNull(fullDmbPath);
var byondDir = PathToUserFolder;
var cfgDir = IOManager.ConcatPath(
byondDir,
CfgDirectoryName);
var trustedFilePath = IOManager.ConcatPath(
cfgDir,
TrustedDmbFileName);
Logger.LogDebug("Adding .dmb ({dmbPath}) to {trustedFilePath}", fullDmbPath, trustedFilePath);
using (await SemaphoreSlimContext.Lock(UserFilesSemaphore, cancellationToken))
{
string trustedFileText;
var filePreviouslyExisted = await IOManager.FileExists(trustedFilePath, cancellationToken);
if (filePreviouslyExisted)
{
var trustedFileBytes = await IOManager.ReadAllBytes(trustedFilePath, cancellationToken);
trustedFileText = Encoding.UTF8.GetString(trustedFileBytes);
trustedFileText = $"{trustedFileText.Trim()}{Environment.NewLine}";
}
else
trustedFileText = String.Empty;
if (trustedFileText.Contains(fullDmbPath, StringComparison.Ordinal))
return;
trustedFileText = $"{trustedFileText}{fullDmbPath}{Environment.NewLine}";
var newTrustedFileBytes = Encoding.UTF8.GetBytes(trustedFileText);
if (!filePreviouslyExisted)
await IOManager.CreateDirectory(cfgDir, cancellationToken);
await IOManager.WriteAllBytes(trustedFilePath, newTrustedFileBytes, cancellationToken);
}
}
/// <inheritdoc />
protected override string GetDreamDaemonName(Version byondVersion, out bool supportsCli)
{
@@ -175,6 +228,19 @@ namespace Tgstation.Server.Host.Components.Engine
return supportsCli ? "dd.exe" : "dreamdaemon.exe";
}
/// <inheritdoc />
protected override IEnumerable<string> AdditionalCacheCleanFilePaths(string configDirectory)
{
// Delete trusted.txt so it doesn't grow too large
var trustedFilePath =
IOManager.ConcatPath(
configDirectory,
TrustedDmbFileName);
Logger.LogTrace("Deleting trusted .dmbs file {trustedFilePath}", trustedFilePath);
yield return trustedFilePath;
}
/// <summary>
/// Creates the BYOND cfg file that prevents the trusted mode dialog from appearing when launching DreamDaemon.
/// </summary>
@@ -30,6 +30,10 @@ namespace Tgstation.Server.Host.Components.Events
this.configuration = configuration ?? throw new ArgumentNullException(nameof(configuration));
}
/// <inheritdoc />
public ValueTask? HandleCustomEvent(string eventName, IEnumerable<string?> parameters, CancellationToken cancellationToken)
=> configuration.HandleCustomEvent(eventName, parameters, cancellationToken);
/// <inheritdoc />
public async ValueTask HandleEvent(EventType eventType, IEnumerable<string?> parameters, bool deploymentPipeline, CancellationToken cancellationToken)
{
@@ -1,5 +1,4 @@
using System;
using System.Collections.Generic;
namespace Tgstation.Server.Host.Components.Events
{
@@ -12,7 +11,7 @@ namespace Tgstation.Server.Host.Components.Events
/// <summary>
/// The name and order of the scripts the event script the <see cref="EventType"/> runs.
/// </summary>
public IReadOnlyList<string> ScriptNames { get; }
public string[] ScriptNames { get; }
/// <summary>
/// Initializes a new instance of the <see cref="EventScriptAttribute"/> class.
@@ -18,5 +18,14 @@ namespace Tgstation.Server.Host.Components.Events
/// <param name="cancellationToken">The <see cref="CancellationToken"/> for the operation.</param>
/// <returns>A <see cref="ValueTask"/> representing the running operation.</returns>
ValueTask HandleEvent(EventType eventType, IEnumerable<string?> parameters, bool deploymentPipeline, CancellationToken cancellationToken);
/// <summary>
/// Handles a given custom event.
/// </summary>
/// <param name="eventName">The name of the event.</param>
/// <param name="parameters">An <see cref="IEnumerable{T}"/> of <see cref="string"/> parameters for the event.</param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> for the operation.</param>
/// <returns>A <see cref="ValueTask"/> representing the running operation if the event was triggered successfully, <see langword="null"/> if it matched a TGS event and wasn't executed.</returns>
ValueTask? HandleCustomEvent(string eventName, IEnumerable<string?> parameters, CancellationToken cancellationToken);
}
}
@@ -12,5 +12,9 @@ namespace Tgstation.Server.Host.Components.Events
/// <inheritdoc />
public ValueTask HandleEvent(EventType eventType, IEnumerable<string?> parameters, bool deploymentPipeline, CancellationToken cancellationToken)
=> ValueTask.CompletedTask;
/// <inheritdoc />
public ValueTask? HandleCustomEvent(string eventName, IEnumerable<string?> parameters, CancellationToken cancellationToken)
=> ValueTask.CompletedTask;
}
}
@@ -39,5 +39,10 @@
/// DreamDaemon attempting to send a longer bridge message.
/// </summary>
Chunk,
/// <summary>
/// DreamDaemon requesting a custom event to be triggered.
/// </summary>
Event,
}
}
@@ -51,6 +51,11 @@ namespace Tgstation.Server.Host.Components.Interop.Bridge
/// </summary>
public ushort? TopicPort { get; set; }
/// <summary>
/// The <see cref="Bridge.CustomEventInvocation"/> being triggered.
/// </summary>
public CustomEventInvocation? EventInvocation { get; set; }
/// <summary>
/// Initializes a new instance of the <see cref="BridgeParameters"/> class.
/// </summary>
@@ -21,5 +21,10 @@ namespace Tgstation.Server.Host.Components.Interop.Bridge
/// The <see cref="ChunkData.SequenceId"/>s missing from a chunked request.
/// </summary>
public IReadOnlyCollection<uint>? MissingChunks { get; set; }
/// <summary>
/// The triggered event ID for <see cref="BridgeCommandType.Event"/> requests.
/// </summary>
public string? EventId { get; set; }
}
}
@@ -0,0 +1,25 @@
using System.Collections.Generic;
namespace Tgstation.Server.Host.Components.Interop.Bridge
{
/// <summary>
/// Parameters for invoking a custom event.
/// </summary>
public sealed class CustomEventInvocation
{
/// <summary>
/// The name of the event being invoked.
/// </summary>
public string? EventName { get; set; }
/// <summary>
/// The parameters for the invoked event.
/// </summary>
public ICollection<string?>? Parameters { get; set; }
/// <summary>
/// If the DMAPI should be notified when the event compeletes.
/// </summary>
public bool? NotifyCompletion { get; set; }
}
}
@@ -15,12 +15,12 @@ namespace Tgstation.Server.Host.Components.Interop.Topic
/// The <see cref="EventType"/> triggered.
/// </summary>
/// <remarks>Nullable to prevent ignoring when serializing.</remarks>
public EventType? Type { get; }
public EventType Type { get; }
/// <summary>
/// The set of parameters.
/// </summary>
public IReadOnlyCollection<string?> Parameters { get; }
public IReadOnlyCollection<string?>? Parameters { get; }
/// <summary>
/// Initializes a new instance of the <see cref="EventNotification"/> class.
@@ -67,5 +67,10 @@ namespace Tgstation.Server.Host.Components.Interop.Topic
/// Sending a broadcast message.
/// </summary>
Broadcast,
/// <summary>
/// Notifying about the completion of a custom event.
/// </summary>
CompleteEvent,
}
}
@@ -62,6 +62,11 @@ namespace Tgstation.Server.Host.Components.Interop.Topic
/// </summary>
public ChunkData? Chunk { get; }
/// <summary>
/// The completed custom event ID.
/// </summary>
public string? EventId { get; set; }
/// <summary>
/// Whether or not the <see cref="TopicParameters"/> constitute a priority request.
/// </summary>
@@ -74,6 +79,7 @@ namespace Tgstation.Server.Host.Components.Interop.Topic
or TopicCommandType.InstanceRenamed
or TopicCommandType.ChatChannelsUpdate
or TopicCommandType.Broadcast
or TopicCommandType.CompleteEvent
or TopicCommandType.ServerRestarted => true,
TopicCommandType.ChatCommand
or TopicCommandType.HealthCheck
@@ -174,6 +180,16 @@ namespace Tgstation.Server.Host.Components.Interop.Topic
Chunk = chunk ?? throw new ArgumentNullException(nameof(chunk));
}
/// <summary>
/// Initializes a new instance of the <see cref="TopicParameters"/> class.
/// </summary>
/// <param name="eventId">The <see cref="Guid"/> containig the value of <see cref="EventId"/>.</param>
public TopicParameters(Guid eventId)
: this(TopicCommandType.CompleteEvent)
{
EventId = eventId.ToString();
}
/// <summary>
/// Initializes a new instance of the <see cref="TopicParameters"/> class.
/// </summary>
@@ -19,6 +19,7 @@ using Tgstation.Server.Host.Components.Chat;
using Tgstation.Server.Host.Components.Chat.Commands;
using Tgstation.Server.Host.Components.Deployment;
using Tgstation.Server.Host.Components.Engine;
using Tgstation.Server.Host.Components.Events;
using Tgstation.Server.Host.Components.Interop;
using Tgstation.Server.Host.Components.Interop.Bridge;
using Tgstation.Server.Host.Components.Interop.Topic;
@@ -159,6 +160,11 @@ namespace Tgstation.Server.Host.Components.Session
/// </summary>
readonly IDotnetDumpService dotnetDumpService;
/// <summary>
/// The <see cref="IEventConsumer"/> for the <see cref="SessionController"/>.
/// </summary>
readonly IEventConsumer eventConsumer;
/// <summary>
/// The <see cref="TaskCompletionSource"/> that completes when DD makes it's first bridge request.
/// </summary>
@@ -170,9 +176,9 @@ namespace Tgstation.Server.Host.Components.Session
readonly Api.Models.Instance metadata;
/// <summary>
/// A <see cref="CancellationTokenSource"/> used for the topic send operation made on reattaching.
/// A <see cref="CancellationTokenSource"/> used for tasks that should not exceed the lifetime of the session.
/// </summary>
readonly CancellationTokenSource reattachTopicCts;
readonly CancellationTokenSource sessionDurationCts;
/// <summary>
/// <see langword="lock"/> <see cref="object"/> for port updates and <see cref="disposed"/>.
@@ -204,6 +210,11 @@ namespace Tgstation.Server.Host.Components.Session
/// </summary>
volatile Task rebootGate;
/// <summary>
/// The <see cref="Task"/> representing calls to <see cref="TriggerCustomEvent(CustomEventInvocation?)"/>.
/// </summary>
volatile Task customEventProcessingTask;
/// <summary>
/// <see cref="Task"/> for shutting down the server if it is taking too long after validation.
/// </summary>
@@ -248,6 +259,7 @@ namespace Tgstation.Server.Host.Components.Session
/// <param name="assemblyInformationProvider">The <see cref="IAssemblyInformationProvider"/> for the <see cref="SessionController"/>.</param>
/// <param name="asyncDelayer">The value of <see cref="asyncDelayer"/>.</param>
/// <param name="dotnetDumpService">The value of <see cref="dotnetDumpService"/>.</param>
/// <param name="eventConsumer">The value of <see cref="eventConsumer"/>.</param>
/// <param name="logger">The value of <see cref="Chunker.Logger"/>.</param>
/// <param name="postLifetimeCallback">The <see cref="Func{TResult}"/> returning a <see cref="ValueTask"/> to be run after the <paramref name="process"/> ends.</param>
/// <param name="startupTimeout">The optional time to wait before failing the <see cref="LaunchResult"/>.</param>
@@ -265,6 +277,7 @@ namespace Tgstation.Server.Host.Components.Session
IAssemblyInformationProvider assemblyInformationProvider,
IAsyncDelayer asyncDelayer,
IDotnetDumpService dotnetDumpService,
IEventConsumer eventConsumer,
ILogger<SessionController> logger,
Func<ValueTask> postLifetimeCallback,
uint? startupTimeout,
@@ -285,6 +298,7 @@ namespace Tgstation.Server.Host.Components.Session
this.asyncDelayer = asyncDelayer ?? throw new ArgumentNullException(nameof(asyncDelayer));
this.dotnetDumpService = dotnetDumpService ?? throw new ArgumentNullException(nameof(dotnetDumpService));
this.eventConsumer = eventConsumer ?? throw new ArgumentNullException(nameof(eventConsumer));
apiValidationSession = apiValidate;
@@ -297,12 +311,13 @@ namespace Tgstation.Server.Host.Components.Session
primeTcs = new TaskCompletionSource();
rebootGate = Task.CompletedTask;
customEventProcessingTask = Task.CompletedTask;
// Run this asynchronously because we want to try to avoid any effects sending topics to the server while the initial bridge request is processing
// It MAY be the source of a DD crash. See this gist https://gist.github.com/Cyberboss/7776bbeff3a957d76affe0eae95c9f14
// Worth further investigation as to if that sequence of events is a reliable crash vector and opening a BYOND bug if it is
initialBridgeRequestTcs = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously);
reattachTopicCts = new CancellationTokenSource();
sessionDurationCts = new CancellationTokenSource();
TopicSendSemaphore = new FifoSemaphore();
synchronizationLock = new object();
@@ -356,7 +371,7 @@ namespace Tgstation.Server.Host.Components.Session
Logger.LogTrace("Disposing...");
reattachTopicCts.Cancel();
sessionDurationCts.Cancel();
var cancellationToken = CancellationToken.None; // DCT: None available
var semaphoreLockTask = TopicSendSemaphore.Lock(cancellationToken);
@@ -381,13 +396,15 @@ namespace Tgstation.Server.Host.Components.Session
await regularDmbDisposeTask;
chatTrackingContext.Dispose();
reattachTopicCts.Dispose();
sessionDurationCts.Dispose();
if (!released)
await Lifetime; // finish the async callback
(await semaphoreLockTask).Dispose();
TopicSendSemaphore.Dispose();
await customEventProcessingTask;
}
/// <inheritdoc />
@@ -547,7 +564,7 @@ namespace Tgstation.Server.Host.Components.Session
assemblyInformationProvider.Version,
ReattachInformation.RuntimeInformation!.ServerPort),
true,
reattachTopicCts.Token);
sessionDurationCts.Token);
if (reattachResponse != null)
{
@@ -735,6 +752,8 @@ namespace Tgstation.Server.Host.Components.Session
break;
case BridgeCommandType.Chunk:
return await ProcessChunk<BridgeParameters, BridgeResponse>(ProcessBridgeCommand, BridgeError, parameters.Chunk, cancellationToken);
case BridgeCommandType.Event:
return TriggerCustomEvent(parameters.EventInvocation);
case null:
return BridgeError("Missing commandType!");
default:
@@ -1102,5 +1121,81 @@ namespace Tgstation.Server.Host.Components.Session
return fullResponse;
}
/// <summary>
/// Trigger a custom event from a given <paramref name="invocation"/>.
/// </summary>
/// <param name="invocation">The <see cref="CustomEventInvocation"/>.</param>
/// <returns>An appropriate <see cref="BridgeResponse"/>.</returns>
BridgeResponse TriggerCustomEvent(CustomEventInvocation? invocation)
{
if (invocation == null)
return BridgeError("Missing eventInvocation!");
var eventName = invocation.EventName;
if (eventName == null)
return BridgeError("Missing eventName!");
var notifyCompletion = invocation.NotifyCompletion;
if (!notifyCompletion.HasValue)
return BridgeError("Missing notifyCompletion!");
var eventParams = new List<string>
{
ReattachInformation.Dmb.Directory,
};
eventParams.AddRange(invocation
.Parameters?
.Where(param => param != null)
.Cast<string>()
?? Enumerable.Empty<string>());
var eventId = Guid.NewGuid();
Logger.LogInformation("Triggering custom event \"{eventName}\": {eventId}", eventName, eventId);
var cancellationToken = sessionDurationCts.Token;
ValueTask? eventTask = eventConsumer.HandleCustomEvent(eventName, eventParams, cancellationToken);
async Task ProcessEvent()
{
try
{
await eventTask.Value;
if (notifyCompletion.Value)
await SendCommand(
new TopicParameters(eventId),
cancellationToken);
else
Logger.LogTrace("Finished custom event {eventId}, not sending notification.", eventId);
}
catch (OperationCanceledException ex)
{
Logger.LogDebug(ex, "Custom event invocation {eventId} aborted!", eventId);
}
catch (Exception ex)
{
Logger.LogWarning(ex, "Custom event invocation {eventId} errored!", eventId);
}
}
if (!eventTask.HasValue)
return BridgeError("Event refused to execute due to matching a TGS event!");
lock (sessionDurationCts)
{
var previousEventProcessingTask = customEventProcessingTask;
var eventProcessingTask = ProcessEvent();
customEventProcessingTask = Task.WhenAll(customEventProcessingTask, eventProcessingTask);
}
return new BridgeResponse
{
EventId = notifyCompletion.Value
? eventId.ToString()
: null,
};
}
}
}
@@ -355,6 +355,7 @@ namespace Tgstation.Server.Host.Components.Session
assemblyInformationProvider,
asyncDelayer,
dotnetDumpService,
eventConsumer,
loggerFactory.CreateLogger<SessionController>(),
() => LogDDOutput(
process,
@@ -446,6 +447,7 @@ namespace Tgstation.Server.Host.Components.Session
assemblyInformationProvider,
asyncDelayer,
dotnetDumpService,
eventConsumer,
loggerFactory.CreateLogger<SessionController>(),
() => ValueTask.CompletedTask,
null,
@@ -71,11 +71,11 @@ namespace Tgstation.Server.Host.Components.StaticFiles
/// <summary>
/// Map of <see cref="EventType"/>s to the filename of the event scripts they trigger.
/// </summary>
public static IReadOnlyDictionary<EventType, IReadOnlyList<string>> EventTypeScriptFileNameMap { get; } = new Dictionary<EventType, IReadOnlyList<string>>(
public static IReadOnlyDictionary<EventType, string[]> EventTypeScriptFileNameMap { get; } = new Dictionary<EventType, string[]>(
Enum.GetValues(typeof(EventType))
.Cast<EventType>()
.Select(
eventType => new KeyValuePair<EventType, IReadOnlyList<string>>(
eventType => new KeyValuePair<EventType, string[]>(
eventType,
typeof(EventType)
.GetField(eventType.ToString())!
@@ -600,70 +600,39 @@ namespace Tgstation.Server.Host.Components.StaticFiles
public Task StopAsync(CancellationToken cancellationToken) => EnsureDirectories(cancellationToken);
/// <inheritdoc />
public async ValueTask HandleEvent(EventType eventType, IEnumerable<string?> parameters, bool deploymentPipeline, CancellationToken cancellationToken)
public ValueTask HandleEvent(EventType eventType, IEnumerable<string?> parameters, bool deploymentPipeline, CancellationToken cancellationToken)
{
ArgumentNullException.ThrowIfNull(parameters);
await EnsureDirectories(cancellationToken);
if (!EventTypeScriptFileNameMap.TryGetValue(eventType, out var scriptNames))
return;
// always execute in serial
using (await SemaphoreSlimContext.Lock(semaphore, cancellationToken))
{
var files = await ioManager.GetFilesWithExtension(EventScriptsSubdirectory, platformIdentifier.ScriptFileExtension, false, cancellationToken);
var resolvedScriptsDir = ioManager.ResolvePath(EventScriptsSubdirectory);
var scriptFiles = files
.Select(x => ioManager.GetFileName(x))
.Where(x => scriptNames.Any(
scriptName => x.StartsWith(scriptName, StringComparison.Ordinal)))
.ToList();
if (scriptFiles.Count == 0)
{
logger.LogTrace("No event scripts starting with \"{scriptName}\" detected", String.Join("\" or \"", scriptNames));
return;
}
foreach (var scriptFile in scriptFiles)
{
logger.LogTrace("Running event script {scriptFile}...", scriptFile);
await using (var script = processExecutor.LaunchProcess(
ioManager.ConcatPath(resolvedScriptsDir, scriptFile),
resolvedScriptsDir,
String.Join(
' ',
parameters.Select(arg =>
{
if (arg == null)
return "(NULL)";
if (!arg.Contains(' ', StringComparison.Ordinal))
return arg;
arg = arg.Replace("\"", "\\\"", StringComparison.Ordinal);
return $"\"{arg}\"";
})),
readStandardHandles: true,
noShellExecute: true))
using (cancellationToken.Register(() => script.Terminate()))
{
if (sessionConfiguration.LowPriorityDeploymentProcesses)
script.AdjustPriority(false);
var exitCode = await script.Lifetime;
cancellationToken.ThrowIfCancellationRequested();
var scriptOutput = await script.GetCombinedOutput(cancellationToken);
if (exitCode != 0)
throw new JobException($"Script {scriptFile} exited with code {exitCode}:{Environment.NewLine}{scriptOutput}");
else
logger.LogDebug("Script output:{newLine}{scriptOutput}", Environment.NewLine, scriptOutput);
}
}
logger.LogTrace("No event script for event {event}!", eventType);
return ValueTask.CompletedTask;
}
return ExecuteEventScripts(parameters, deploymentPipeline, cancellationToken, scriptNames);
}
/// <inheritdoc />
public ValueTask? HandleCustomEvent(string scriptName, IEnumerable<string?> parameters, CancellationToken cancellationToken)
{
var scriptNameIsTgsEventName = EventTypeScriptFileNameMap
.Values
.SelectMany(scriptNames => scriptNames)
.Any(tgsScriptName => tgsScriptName.Equals(
scriptName,
platformIdentifier.IsWindows
? StringComparison.OrdinalIgnoreCase
: StringComparison.Ordinal));
if (scriptNameIsTgsEventName)
{
logger.LogWarning("DMAPI attempted to execute TGS reserved event: {eventName}", scriptName);
return null;
}
#pragma warning disable CA2012 // Use ValueTasks correctly
return ExecuteEventScripts(parameters, false, cancellationToken, scriptName);
#pragma warning restore CA2012 // Use ValueTasks correctly
}
/// <inheritdoc />
@@ -758,5 +727,74 @@ namespace Tgstation.Server.Host.Components.StaticFiles
throw new InvalidOperationException("Attempted to access file outside of configuration manager!");
return resolved;
}
/// <summary>
/// Execute a set of given <paramref name="scriptNames"/>.
/// </summary>
/// <param name="parameters">An <see cref="IEnumerable{T}"/> of <see cref="string"/> parameters for the <paramref name="scriptNames"/>.</param>
/// <param name="deploymentPipeline">If this event is part of the deployment pipeline.</param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> for the operation.</param>
/// <param name="scriptNames">The names of the scripts to execute.</param>
/// <returns>A <see cref="ValueTask"/> representing the running operation.</returns>
async ValueTask ExecuteEventScripts(IEnumerable<string?> parameters, bool deploymentPipeline, CancellationToken cancellationToken, params string[] scriptNames)
{
await EnsureDirectories(cancellationToken);
// always execute in serial
using (await SemaphoreSlimContext.Lock(semaphore, cancellationToken))
{
var files = await ioManager.GetFilesWithExtension(EventScriptsSubdirectory, platformIdentifier.ScriptFileExtension, false, cancellationToken);
var resolvedScriptsDir = ioManager.ResolvePath(EventScriptsSubdirectory);
var scriptFiles = files
.Select(x => ioManager.GetFileName(x))
.Where(x => scriptNames.Any(
scriptName => x.StartsWith(scriptName, StringComparison.Ordinal)))
.ToList();
if (scriptFiles.Count == 0)
{
logger.LogTrace("No event scripts starting with \"{scriptName}\" detected", String.Join("\" or \"", scriptNames));
return;
}
foreach (var scriptFile in scriptFiles)
{
logger.LogTrace("Running event script {scriptFile}...", scriptFile);
await using (var script = processExecutor.LaunchProcess(
ioManager.ConcatPath(resolvedScriptsDir, scriptFile),
resolvedScriptsDir,
String.Join(
' ',
parameters.Select(arg =>
{
if (arg == null)
return "(NULL)";
if (!arg.Contains(' ', StringComparison.Ordinal))
return arg;
arg = arg.Replace("\"", "\\\"", StringComparison.Ordinal);
return $"\"{arg}\"";
})),
readStandardHandles: true,
noShellExecute: true))
using (cancellationToken.Register(() => script.Terminate()))
{
if (sessionConfiguration.LowPriorityDeploymentProcesses && deploymentPipeline)
script.AdjustPriority(false);
var exitCode = await script.Lifetime;
cancellationToken.ThrowIfCancellationRequested();
var scriptOutput = await script.GetCombinedOutput(cancellationToken);
if (exitCode != 0)
throw new JobException($"Script {scriptFile} exited with code {exitCode}:{Environment.NewLine}{scriptOutput}");
else
logger.LogDebug("Script output:{newLine}{scriptOutput}", Environment.NewLine, scriptOutput);
}
}
}
}
}
}
@@ -507,6 +507,10 @@ namespace Tgstation.Server.Host.Components.Watchdog
HandleChatResponses(result);
}
/// <inheritdoc />
ValueTask? IEventConsumer.HandleCustomEvent(string eventName, IEnumerable<string?> parameters, CancellationToken cancellationToken)
=> throw new NotSupportedException("Watchdogs do not support custom events!");
/// <summary>
/// Starts all <see cref="ISessionController"/>s.
/// </summary>
@@ -5,6 +5,9 @@ using System.Threading.Tasks;
using Microsoft.Diagnostics.NETCore.Client;
using Microsoft.Extensions.Logging;
using Tgstation.Server.Api.Models;
using Tgstation.Server.Host.Jobs;
namespace Tgstation.Server.Host.System
{
/// <inheritdoc />
@@ -42,13 +45,20 @@ namespace Tgstation.Server.Host.System
var pid = process.Id;
logger.LogDebug("dotnet-dump requested for PID {pid}...", pid);
var client = new DiagnosticsClient(pid);
await client.WriteDumpAsync(
minidump
? DumpType.Normal
: DumpType.Full,
outputFile,
false,
cts.Token);
try
{
await client.WriteDumpAsync(
minidump
? DumpType.Normal
: DumpType.Full,
outputFile,
false,
cts.Token);
}
catch (Exception ex)
{
throw new JobException(ErrorCode.GCoreFailure, ex);
}
}
}
}
@@ -78,35 +78,35 @@
<!-- Usage: git interop -->
<PackageReference Include="LibGit2Sharp" Version="0.29.0" />
<!-- Usage: JWT injection into HTTP pipeline -->
<PackageReference Include="Microsoft.AspNetCore.Authentication.JwtBearer" Version="8.0.1" />
<PackageReference Include="Microsoft.AspNetCore.Authentication.JwtBearer" Version="8.0.2" />
<!-- Usage: Support ""legacy"" Newotonsoft.Json in HTTP pipeline. The rest of our codebase uses Newtonsoft. -->
<PackageReference Include="Microsoft.AspNetCore.Mvc.NewtonsoftJson" Version="8.0.1" />
<PackageReference Include="Microsoft.AspNetCore.Mvc.NewtonsoftJson" Version="8.0.2" />
<!-- Usage: Using target JSON serializer for API -->
<PackageReference Include="Microsoft.AspNetCore.SignalR.Protocols.NewtonsoftJson" Version="8.0.1" />
<PackageReference Include="Microsoft.AspNetCore.SignalR.Protocols.NewtonsoftJson" Version="8.0.2" />
<!-- Usage: Generating dumps of dotnet engine processes -->
<PackageReference Include="Microsoft.Diagnostics.NETCore.Client" Version="0.2.505301" />
<PackageReference Include="Microsoft.Diagnostics.NETCore.Client" Version="0.2.510501" />
<!-- Usage: Database ORM -->
<PackageReference Include="Microsoft.EntityFrameworkCore" Version="8.0.1" />
<PackageReference Include="Microsoft.EntityFrameworkCore" Version="8.0.2" />
<!-- Usage: Automatic migration generation using command line -->
<PackageReference Include="Microsoft.EntityFrameworkCore.Design" Version="8.0.1">
<PackageReference Include="Microsoft.EntityFrameworkCore.Design" Version="8.0.2">
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
<!-- Usage: Sqlite ORM plugin -->
<PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite" Version="8.0.1" />
<PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite" Version="8.0.2" />
<!-- Usage: MSSQL ORM plugin -->
<PackageReference Include="Microsoft.EntityFrameworkCore.SqlServer" Version="8.0.1" />
<PackageReference Include="Microsoft.EntityFrameworkCore.SqlServer" Version="8.0.2" />
<!-- Usage: POSIX support for syscalls, signals, and symlinks -->
<PackageReference Include="Mono.Posix.NETStandard" Version="1.0.0" />
<!-- Usage: YAML config plugin -->
<PackageReference Include="NetEscapades.Configuration.Yaml" Version="3.1.0" />
<!-- Usage: PostgresSQL ORM plugin -->
<PackageReference Include="Npgsql.EntityFrameworkCore.PostgreSQL" Version="8.0.0" />
<PackageReference Include="Npgsql.EntityFrameworkCore.PostgreSQL" Version="8.0.2" />
<!-- Usage: GitHub.com interop -->
<PackageReference Include="Octokit" Version="9.1.2" />
<!-- Usage: MYSQL/MariaDB ORM plugin -->
<PackageReference Include="Pomelo.EntityFrameworkCore.MySql" Version="8.0.0-beta.2" />
<PackageReference Include="Pomelo.EntityFrameworkCore.MySql" Version="8.0.0" />
<!-- Usage: Discord interop -->
<PackageReference Include="Remora.Discord" Version="2023.5.0" />
<PackageReference Include="Remora.Discord" Version="2024.1.0" />
<!-- Usage: Rich logger builder -->
<PackageReference Include="Serilog.Extensions.Logging" Version="8.0.0" />
<!-- Usage: Async logging plugin -->
@@ -128,7 +128,7 @@
<!-- Usage: Temporary resolution to compatibility issues with EFCore 7 and .NET 8 -->
<PackageReference Include="System.Security.Permissions" Version="8.0.0" />
<!-- Usage: .DeleteAsync() support for IQueryable<T>s -->
<PackageReference Include="Z.EntityFramework.Plus.EFCore" Version="8.101.2.1" />
<PackageReference Include="Z.EntityFramework.Plus.EFCore" Version="8.102.1" />
</ItemGroup>
<ItemGroup>
+21
View File
@@ -15,10 +15,31 @@
sleep(50)
world.TgsTargetedChatBroadcast("Sample admin-only message", TRUE)
world.log << "params check"
var/list/world_params = world.params
if(!("test" in world_params) || world_params["test"] != "bababooey")
FailTest("Expected parameter test=bababooey but did not receive", "test_fail_reason.txt")
world.log << "file check 1"
fdel("test_event_output.txt")
var/test_data = "nwfiuurhfu"
world.TgsTriggerEvent("test_event", list(test_data), TRUE)
if(!fexists("test_event_output.txt"))
FailTest("Expected test_event_output.txt to exist here", "test_fail_reason.txt")
var/test_contents = copytext(file2text("test_event_output.txt"), 1, length(test_data) + 1)
if(test_contents != test_data)
FailTest("Expected test_event_output.txt to contain [test_data] here. Got [test_contents]", "test_fail_reason.txt")
world.log << "file check 1"
fdel("test_event_output.txt")
var/start_time = world.timeofday
world.TgsTriggerEvent("test_event", list("asdf"), FALSE)
if((world.timeofday - start_time) <= 50 && fexists("test_event_output.txt"))
FailTest("Expected test_event_output.txt to not exist here", "test_fail_reason.txt")
world.log << "sleep2"
sleep(150)
world.log << "Terminating..."
@@ -0,0 +1,5 @@
echo "Running test_event script"
C:\Windows\System32\WindowsPowerShell\v1.0\powershell.exe -ExecutionPolicy Bypass -Command "Start-Sleep -Seconds 5"
cd %1
cd tests\DMAPI\BasicOperation
echo %2 > test_event_output.txt
+13
View File
@@ -0,0 +1,13 @@
#!/bin/bash
set -e
echo "Running test_event script - $1 - $2"
sleep 5
cd $1
cd tests/DMAPI/BasicOperation
echo $2 > test_event_output.txt
+3 -3
View File
@@ -34,7 +34,7 @@
if(!res)
FailTest("Failed to resource!")
var/res_contents = file2text(res) // we need a .rsc to be generated
var/res_contents = TGS_FILE2TEXT_NATIVE(res) // we need a .rsc to be generated
if(!res_contents)
FailTest("Failed to resource? No contents!")
@@ -203,7 +203,7 @@ var/run_bridge_test
DetachedChatMessageQueuingP2()
/proc/DetachedChatMessageQueuingP2()
sleep(1)
sleep(world.tick_lag)
DetachedChatMessageQueuingP3()
/proc/DetachedChatMessageQueuingP3()
@@ -240,7 +240,7 @@ var/received_health_check = FALSE
DelayCheckDetach()
/proc/DelayCheckDetach()
sleep(1)
sleep(world.tick_lag)
// hack hack, calling world.TgsChatChannelInfo() will try to delay until the channels come back
var/datum/tgs_api/v5/api = TGS_READ_GLOBAL(tgs)
if(length(api.chat_channels))
+1 -1
View File
@@ -32,4 +32,4 @@
text2file(reason, "test_fail_reason.txt")
world.log << "Terminating..."
del(world)
sleep(1) // https://www.byond.com/forum/post/2894866
sleep(world.tick_lag) // https://www.byond.com/forum/post/2894866
@@ -6,7 +6,7 @@
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Microsoft.EntityFrameworkCore.InMemory" Version="8.0.1" />
<PackageReference Include="Microsoft.EntityFrameworkCore.InMemory" Version="8.0.2" />
</ItemGroup>
<ItemGroup>
@@ -118,7 +118,7 @@ namespace Tgstation.Server.Tests.Live.Instance
await using var memoryStream2 = new MemoryStream(Encoding.UTF8.GetBytes("bbb"));
await configurationClient.Write(staticFile2, memoryStream2, cancellationToken);
async ValueTask UploadScript(string scriptId)
async ValueTask UploadScript(string scriptId, bool basic)
{
var shellScriptExtension = new PlatformIdentifier().IsWindows ? ".bat" : ".sh";
var scriptName = $"{scriptId}{shellScriptExtension}";
@@ -127,15 +127,16 @@ namespace Tgstation.Server.Tests.Live.Instance
Path = $"/EventScripts/{scriptName}"
};
await using var readStream = ioManager.GetFileStream($"../../../../DMAPI/LongRunning/{scriptName}", false);
await using var readStream = ioManager.GetFileStream($"../../../../DMAPI/{(basic ? "BasicOperation" : "LongRunning")}/{scriptName}", false);
await configurationClient.Write(
resourcingScript,
readStream,
cancellationToken);
}
await UploadScript("PreCompile-GenerateRandomResource");
await UploadScript("EngineActiveVersionChange-SetupEnv");
await UploadScript("PreCompile-GenerateRandomResource", false);
await UploadScript("EngineActiveVersionChange-SetupEnv", false);
await UploadScript("test_event-qwer", true);
}
return ValueTaskExtensions.WhenAll(
@@ -1485,7 +1485,7 @@ namespace Tgstation.Server.Tests.Live.Instance
var newStatus = await instanceClient.DreamDaemon.Read(cancellationToken);
Assert.IsTrue(newStatus.SoftShutdown.Value || newStatus.Status.Value == WatchdogStatus.Offline);
var timeout = 20;
var timeout = 40;
do
{
await Task.Delay(TimeSpan.FromSeconds(3), cancellationToken);
+2
View File
@@ -176,6 +176,8 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "BasicOperation", "BasicOper
tests\DMAPI\BasicOperation\basic operation_test.dme = tests\DMAPI\BasicOperation\basic operation_test.dme
tests\DMAPI\BasicOperation\Config.dm = tests\DMAPI\BasicOperation\Config.dm
tests\DMAPI\BasicOperation\Test.dm = tests\DMAPI\BasicOperation\Test.dm
tests\DMAPI\BasicOperation\test_event-qwer.sh = tests\DMAPI\BasicOperation\test_event-qwer.sh
tests\DMAPI\BasicOperation\test_event-qwer.bat = tests\DMAPI\BasicOperation\test_event-qwer.bat
EndProjectSection
EndProject
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "BuildFail", "BuildFail", "{103C61AB-67D6-46FE-AA47-CC633B88EE0F}"
@@ -788,7 +788,7 @@ namespace Tgstation.Server.ReleaseNotes
var versionsPropertyGroup = project.Elements().First(x => x.Name == xmlNamespace + "PropertyGroup");
var coreVersion = Version.Parse(versionsPropertyGroup.Element(xmlNamespace + "TgsCoreVersion").Value);
const string BodyForPRSha = "184dccf9de3e3e4abe289a46648af42017ad6f09";
const string BodyForPRSha = "bec143988b4b8ddeb586ed97aaf0647803110d98";
var prBody = $@"# Automated Pull Request
This pull request was generated by our [deployment pipeline]({actionUrl}) as a result of the release of [tgstation-server-v{coreVersion}](https://github.com/tgstation/tgstation-server/releases/tag/tgstation-server-v{coreVersion}). Validation was performed as part of the process.
@@ -803,7 +803,7 @@ The user account that created this pull request is available to correct any issu
- Validation is performed as a prerequisite to deployment.
- [x] Have you tested your manifest locally with `winget install --manifest <path>`?
- Manifest installation and uninstallation is performed as a prerequisite to deployment.
- [x] Does your manifest conform to the [1.5 schema](https://github.com/microsoft/winget-pkgs/tree/master/doc/manifest/schema/1.5.0)?
- [x] Does your manifest conform to the [1.6 schema](https://github.com/microsoft/winget-pkgs/tree/master/doc/manifest/schema/1.6.0)?
###### Microsoft Reviewers: [Open in CodeFlow](https://microsoft.github.io/open-pr/?codeflow=https://github.com/microsoft/winget-pkgs/pull/$PR_NUMBER_SUBST$)";