From c48ce8deed0103bab9365871ccf6ad56a817f0d6 Mon Sep 17 00:00:00 2001 From: Jordan Dominion Date: Sat, 21 Oct 2023 12:00:55 -0400 Subject: [PATCH 01/16] Fix deployments always timing out if DMAPI validation fails --- .../Components/Session/SessionController.cs | 44 +++++++++++++++++++ 1 file changed, 44 insertions(+) diff --git a/src/Tgstation.Server.Host/Components/Session/SessionController.cs b/src/Tgstation.Server.Host/Components/Session/SessionController.cs index 2da8c03b58..c792054b83 100644 --- a/src/Tgstation.Server.Host/Components/Session/SessionController.cs +++ b/src/Tgstation.Server.Host/Components/Session/SessionController.cs @@ -192,6 +192,11 @@ namespace Tgstation.Server.Host.Components.Session /// volatile Task rebootGate; + /// + /// for shutting down the server if it is taking too long after validation. + /// + volatile Task postValidationShutdownTask; + /// /// The number of currently active calls to from TgsReboot(). /// @@ -307,6 +312,9 @@ namespace Tgstation.Server.Host.Components.Session { var exitCode = await process.Lifetime; await postLifetimeCallback(); + if (postValidationShutdownTask != null) + await postValidationShutdownTask; + return exitCode; } @@ -655,12 +663,40 @@ namespace Tgstation.Server.Host.Components.Session throw new ObjectDisposedException(nameof(SessionController)); } + /// + /// Terminates the server after ten seconds if it does not exit. + /// + /// A that this method s before executing. If the is , this method will return immediately. + /// A representing the running operation. + async Task PostValidationShutdown(Task proceedTask) + { + Logger.LogTrace("Entered post validation terminate task."); + if (!await proceedTask) + { + Logger.LogTrace("Not running post validation terminate task for repeated bridge request."); + return; + } + + Logger.LogDebug("Server will terminated in 10s if it does not exit..."); + var delayTask = asyncDelayer.Delay(TimeSpan.FromSeconds(10), CancellationToken.None); // DCT: None available + var completedTask = await Task.WhenAny(process.Lifetime, delayTask); + if (completedTask == delayTask) + { + Logger.LogWarning("DMAPI took too long to shutdown server after validation request!"); + process.Terminate(); + apiValidationStatus = ApiValidationStatus.BadValidationRequest; + } + else + Logger.LogTrace("Server exited properly post validation."); + } + /// /// Handle a set of bridge . /// /// The to handle. /// The for the operation. /// A resulting in the for the request or if the request could not be dispatched. +#pragma warning disable CA1502 // TODO: Decomplexify async Task ProcessBridgeCommand(BridgeParameters parameters, CancellationToken cancellationToken) { var response = new BridgeResponse(); @@ -734,7 +770,14 @@ namespace Tgstation.Server.Host.Components.Session break; case BridgeCommandType.Startup: + var proceedTcs = new TaskCompletionSource(); + var firstValidationRequest = Interlocked.CompareExchange(ref postValidationShutdownTask, PostValidationShutdown(proceedTcs.Task), null) == null; + proceedTcs.SetResult(firstValidationRequest); apiValidationStatus = ApiValidationStatus.BadValidationRequest; + + if (!firstValidationRequest) + return BridgeError("Startup bridge request was repeated!"); + if (parameters.Version == null) return BridgeError("Missing dmApiVersion field!"); @@ -808,6 +851,7 @@ namespace Tgstation.Server.Host.Components.Session return response; } +#pragma warning restore CA1502 /// /// Log and return a for a given . From f124c2aaec13351521b14f2288d8e0c40ef855e8 Mon Sep 17 00:00:00 2001 From: Jordan Dominion Date: Sat, 21 Oct 2023 12:01:04 -0400 Subject: [PATCH 02/16] Add a decomplexify TODO --- src/Tgstation.Server.Api/ApiHeaders.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Tgstation.Server.Api/ApiHeaders.cs b/src/Tgstation.Server.Api/ApiHeaders.cs index 2d27908acd..7fe6edafc3 100644 --- a/src/Tgstation.Server.Api/ApiHeaders.cs +++ b/src/Tgstation.Server.Api/ApiHeaders.cs @@ -164,7 +164,7 @@ namespace Tgstation.Server.Api /// The containing the serialized . /// If a missing should be ignored. /// Thrown if the constitue invalid . -#pragma warning disable CA1502 +#pragma warning disable CA1502 // TODO: Decomplexify public ApiHeaders(RequestHeaders requestHeaders, bool ignoreMissingAuth = false) { if (requestHeaders == null) From 103adf13d1c580256414e06b415cea1a100e1d1b Mon Sep 17 00:00:00 2001 From: Jordan Dominion Date: Sun, 22 Oct 2023 09:12:28 -0400 Subject: [PATCH 03/16] Increase delay before running `--link-winget` Prevents PR content from being overwritten by the MS bot --- .github/workflows/ci-pipeline.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/ci-pipeline.yml b/.github/workflows/ci-pipeline.yml index 8e75c03afb..02945d00e4 100644 --- a/.github/workflows/ci-pipeline.yml +++ b/.github/workflows/ci-pipeline.yml @@ -1771,5 +1771,5 @@ jobs: - name: Run ReleaseNotes with --link-winget shell: powershell run: | - Sleep 15 + Sleep 600 dotnet run -c Release --no-build --project tools/Tgstation.Server.ReleaseNotes --link-winget ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }} From bf23f03b9118a1eb895e017ac3f9cc072f74f0e9 Mon Sep 17 00:00:00 2001 From: Jordan Dominion Date: Sun, 22 Oct 2023 09:17:34 -0400 Subject: [PATCH 04/16] Fix DMAPI post validate timeout applying to all sessions --- .../Components/Session/SessionController.cs | 24 +++++++++++++++---- 1 file changed, 19 insertions(+), 5 deletions(-) diff --git a/src/Tgstation.Server.Host/Components/Session/SessionController.cs b/src/Tgstation.Server.Host/Components/Session/SessionController.cs index 4672b63757..8e8a4715ec 100644 --- a/src/Tgstation.Server.Host/Components/Session/SessionController.cs +++ b/src/Tgstation.Server.Host/Components/Session/SessionController.cs @@ -167,6 +167,11 @@ namespace Tgstation.Server.Host.Components.Session /// readonly object synchronizationLock; + /// + /// If this session is meant to validate the presence of the DMAPI. + /// + readonly bool apiValidationSession; + /// /// The waits on when DreamDaemon currently has it's ports closed. /// @@ -244,7 +249,7 @@ namespace Tgstation.Server.Host.Components.Session /// The returning a to be run after the ends. /// The optional time to wait before failing the . /// If this is a reattached session. - /// If this is a DMAPI validation session. + /// The value of . public SessionController( ReattachInformation reattachInformation, Api.Models.Instance metadata, @@ -276,6 +281,8 @@ namespace Tgstation.Server.Host.Components.Session this.asyncDelayer = asyncDelayer ?? throw new ArgumentNullException(nameof(asyncDelayer)); + apiValidationSession = apiValidate; + portClosedForReboot = false; disposed = false; apiValidationStatus = ApiValidationStatus.NeverValidated; @@ -296,7 +303,7 @@ namespace Tgstation.Server.Host.Components.Session topicSendSemaphore = new FifoSemaphore(); synchronizationLock = new object(); - if (apiValidate || DMApiAvailable) + if (apiValidationSession || DMApiAvailable) { bridgeRegistration = bridgeRegistrar.RegisterHandler(this); this.chatTrackingContext.SetChannelSink(this); @@ -775,9 +782,16 @@ namespace Tgstation.Server.Host.Components.Session break; case BridgeCommandType.Startup: - var proceedTcs = new TaskCompletionSource(); - var firstValidationRequest = Interlocked.CompareExchange(ref postValidationShutdownTask, PostValidationShutdown(proceedTcs.Task), null) == null; - proceedTcs.SetResult(firstValidationRequest); + bool firstValidationRequest; + if (apiValidationSession) + { + var proceedTcs = new TaskCompletionSource(); + firstValidationRequest = Interlocked.CompareExchange(ref postValidationShutdownTask, PostValidationShutdown(proceedTcs.Task), null) == null; + proceedTcs.SetResult(firstValidationRequest); + } + else + firstValidationRequest = Interlocked.CompareExchange(ref postValidationShutdownTask, Task.CompletedTask, null) == null; + apiValidationStatus = ApiValidationStatus.BadValidationRequest; if (!firstValidationRequest) From 812d6a619cf7b75ed3dea2079883265efb949a42 Mon Sep 17 00:00:00 2001 From: Jordan Dominion Date: Sun, 22 Oct 2023 10:01:03 -0400 Subject: [PATCH 05/16] Remove the interactive test It's annoying to monitor --- tests/Tgstation.Server.Tests/Live/TestLiveServer.cs | 3 --- 1 file changed, 3 deletions(-) diff --git a/tests/Tgstation.Server.Tests/Live/TestLiveServer.cs b/tests/Tgstation.Server.Tests/Live/TestLiveServer.cs index 21ab8be844..7ae5e35c6d 100644 --- a/tests/Tgstation.Server.Tests/Live/TestLiveServer.cs +++ b/tests/Tgstation.Server.Tests/Live/TestLiveServer.cs @@ -955,9 +955,6 @@ namespace Tgstation.Server.Tests.Live new LiveTestingServer(null, false).Dispose(); } - [TestMethod] - public async Task TestTgstationInteractive() => await TestTgstation(true); - [TestMethod] public async Task TestTgstationHeadless() => await TestTgstation(false); From c820296b4ca0e994edd7266118a35497d90cb9c5 Mon Sep 17 00:00:00 2001 From: Jordan Dominion Date: Sun, 22 Oct 2023 10:19:52 -0400 Subject: [PATCH 06/16] Fix initial bridge requests not expecting reboots --- .../Components/Session/SessionController.cs | 14 +++++--------- 1 file changed, 5 insertions(+), 9 deletions(-) diff --git a/src/Tgstation.Server.Host/Components/Session/SessionController.cs b/src/Tgstation.Server.Host/Components/Session/SessionController.cs index 8e8a4715ec..42054f24fb 100644 --- a/src/Tgstation.Server.Host/Components/Session/SessionController.cs +++ b/src/Tgstation.Server.Host/Components/Session/SessionController.cs @@ -782,20 +782,16 @@ namespace Tgstation.Server.Host.Components.Session break; case BridgeCommandType.Startup: - bool firstValidationRequest; + apiValidationStatus = ApiValidationStatus.BadValidationRequest; if (apiValidationSession) { var proceedTcs = new TaskCompletionSource(); - firstValidationRequest = Interlocked.CompareExchange(ref postValidationShutdownTask, PostValidationShutdown(proceedTcs.Task), null) == null; + var firstValidationRequest = Interlocked.CompareExchange(ref postValidationShutdownTask, PostValidationShutdown(proceedTcs.Task), null) == null; proceedTcs.SetResult(firstValidationRequest); + + if (!firstValidationRequest) + return BridgeError("Startup bridge request was repeated!"); } - else - firstValidationRequest = Interlocked.CompareExchange(ref postValidationShutdownTask, Task.CompletedTask, null) == null; - - apiValidationStatus = ApiValidationStatus.BadValidationRequest; - - if (!firstValidationRequest) - return BridgeError("Startup bridge request was repeated!"); if (parameters.Version == null) return BridgeError("Missing dmApiVersion field!"); From 4fac505a93a4d98892a6fbbc251a36ce5758b794 Mon Sep 17 00:00:00 2001 From: Jordan Dominion Date: Sun, 22 Oct 2023 10:37:00 -0400 Subject: [PATCH 07/16] Version bump to 5.16.4 --- build/Version.props | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/build/Version.props b/build/Version.props index c9c9e39ca0..3f21f8ad4b 100644 --- a/build/Version.props +++ b/build/Version.props @@ -3,7 +3,7 @@ - 5.16.3 + 5.16.4 4.7.1 9.12.0 6.0.1 From f0d5286a206f4ab1d9f289dea892d3bee00d04a7 Mon Sep 17 00:00:00 2001 From: Jordan Dominion Date: Sun, 22 Oct 2023 11:13:16 -0400 Subject: [PATCH 08/16] Disable this validation until BYOND bug is fixed See https://www.byond.com/forum/post/2894866 --- .../Components/Session/SessionController.cs | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/Tgstation.Server.Host/Components/Session/SessionController.cs b/src/Tgstation.Server.Host/Components/Session/SessionController.cs index 42054f24fb..b33d3628a1 100644 --- a/src/Tgstation.Server.Host/Components/Session/SessionController.cs +++ b/src/Tgstation.Server.Host/Components/Session/SessionController.cs @@ -783,6 +783,9 @@ namespace Tgstation.Server.Host.Components.Session break; case BridgeCommandType.Startup: apiValidationStatus = ApiValidationStatus.BadValidationRequest; + + // This business is is cancelled until this BYOND bug is resolved: https://www.byond.com/forum/post/2894866 +#if FALSE if (apiValidationSession) { var proceedTcs = new TaskCompletionSource(); @@ -792,6 +795,7 @@ namespace Tgstation.Server.Host.Components.Session if (!firstValidationRequest) return BridgeError("Startup bridge request was repeated!"); } +#endif if (parameters.Version == null) return BridgeError("Missing dmApiVersion field!"); From 720c42c522cb646fed39cff6d4f61a220bad8ca6 Mon Sep 17 00:00:00 2001 From: Jordan Dominion Date: Sun, 22 Oct 2023 11:44:07 -0400 Subject: [PATCH 09/16] Test that GameStaticFiles work in live tests --- tests/DMAPI/LongRunning/Test.dm | 17 ++++++ .../LongRunning/long_running_test_rooted.dme | 2 + .../Live/Instance/ConfigurationTest.cs | 38 +++++++++--- .../Live/Instance/WatchdogTest.cs | 61 ++++++++++++++++++- 4 files changed, 108 insertions(+), 10 deletions(-) diff --git a/tests/DMAPI/LongRunning/Test.dm b/tests/DMAPI/LongRunning/Test.dm index 657fadbfd0..f605c5ad59 100644 --- a/tests/DMAPI/LongRunning/Test.dm +++ b/tests/DMAPI/LongRunning/Test.dm @@ -31,6 +31,23 @@ if(!fexists("[DME_NAME].rsc")) FailTest("Failed to create .rsc!") +#ifdef RUN_STATIC_FILE_TESTS + if(params["expect_static_files"]) + if(!fexists("test2.txt")) + FailTest("Missing test2.txt") + + var/f2content = file2text("test2.txt") + if(f2content != "bbb") + FailTest("Unexpected test2.txt content: [f2content]") + + if(!fexists("data/test.txt")) + FailTest("Missing data/test.txt") + + var/f1content = file2text("data/test.txt") + if(f1content != "aaa") + FailTest("Unexpected data/test.txt content: [f1content]") +#endif + StartAsync() /proc/dab() diff --git a/tests/DMAPI/LongRunning/long_running_test_rooted.dme b/tests/DMAPI/LongRunning/long_running_test_rooted.dme index d8e9d61c96..a2ff783588 100644 --- a/tests/DMAPI/LongRunning/long_running_test_rooted.dme +++ b/tests/DMAPI/LongRunning/long_running_test_rooted.dme @@ -11,6 +11,8 @@ // END_PREFERENCES // BEGIN_INCLUDE +#define RUN_STATIC_FILE_TESTS +#define DME_NAME "long_running_test_rooted" #include "tests/DMAPI/LongRunning/Config.dm" #include "tests/DMAPI/test_prelude.dm" #include "tests/DMAPI/LongRunning/Test.dm" diff --git a/tests/Tgstation.Server.Tests/Live/Instance/ConfigurationTest.cs b/tests/Tgstation.Server.Tests/Live/Instance/ConfigurationTest.cs index 2dea3e5b12..44c2f7246b 100644 --- a/tests/Tgstation.Server.Tests/Live/Instance/ConfigurationTest.cs +++ b/tests/Tgstation.Server.Tests/Live/Instance/ConfigurationTest.cs @@ -83,19 +83,40 @@ namespace Tgstation.Server.Tests.Live.Instance var path = Path.Combine(instance.Path, "Configuration", tmp); Assert.IsFalse(Directory.Exists(path)); - // leave a directory there to test the deployment process - var staticDir = new ConfigurationFileRequest - { - Path = "/GameStaticFiles/data" - }; - - await configurationClient.CreateDirectory(staticDir, cancellationToken); } public Task SetupDMApiTests(bool includingRoot, CancellationToken cancellationToken) { // just use an I/O manager here var ioManager = new DefaultIOManager(); + + async Task TestStaticFileAndDir() + { + // leave a file there to test the deployment process + var staticDir = new ConfigurationFileRequest + { + Path = "/GameStaticFiles/data" + }; + + await configurationClient.CreateDirectory(staticDir, cancellationToken); + + var staticFile = new ConfigurationFileRequest + { + Path = "/GameStaticFiles/data/test.txt" + }; + + await using var memoryStream = new MemoryStream(Encoding.UTF8.GetBytes("aaa")); + await configurationClient.Write(staticFile, memoryStream, cancellationToken); + + var staticFile2 = new ConfigurationFileRequest + { + Path = "/GameStaticFiles/test2.txt" + }; + + await using var memoryStream2 = new MemoryStream(Encoding.UTF8.GetBytes("bbb")); + await configurationClient.Write(staticFile2, memoryStream2, cancellationToken); + } + return Task.WhenAll( ioManager.CopyDirectory( Enumerable.Empty(), @@ -110,6 +131,9 @@ namespace Tgstation.Server.Tests.Live.Instance ioManager.ConcatPath(instance.Path, "Repository", "long_running_test_rooted.dme"), cancellationToken) : Task.CompletedTask, + includingRoot + ? TestStaticFileAndDir() + : Task.CompletedTask, ioManager.CopyDirectory( Enumerable.Empty(), null, diff --git a/tests/Tgstation.Server.Tests/Live/Instance/WatchdogTest.cs b/tests/Tgstation.Server.Tests/Live/Instance/WatchdogTest.cs index 784fce5f67..35bd51fd11 100644 --- a/tests/Tgstation.Server.Tests/Live/Instance/WatchdogTest.cs +++ b/tests/Tgstation.Server.Tests/Live/Instance/WatchdogTest.cs @@ -114,7 +114,7 @@ namespace Tgstation.Server.Tests.Live.Instance Port = ddPort, MapThreads = 2, LogOutput = false, - AdditionalParameters = "expect_chat_channels=1" + AdditionalParameters = "expect_chat_channels=1&expect_static_files=1" }, cancellationToken), CheckByondVersions(), ApiAssert.ThrowsException(() => instanceClient.DreamDaemon.Update(new DreamDaemonRequest @@ -157,8 +157,64 @@ namespace Tgstation.Server.Tests.Live.Instance System.Console.WriteLine($"TEST: END WATCHDOG TESTS {instanceClient.Metadata.Name}"); } + async ValueTask RegressionTest1686(CancellationToken cancellationToken) + { + async ValueTask RunTest(bool useTrusted) + { + System.Console.WriteLine($"TEST: RegressionTest1686 {useTrusted}..."); + var ddUpdateTask = instanceClient.DreamDaemon.Update(new DreamDaemonRequest + { + SecurityLevel = useTrusted ? DreamDaemonSecurity.Trusted : DreamDaemonSecurity.Safe, + AdditionalParameters = "expect_chat_channels=1&expect_static_files=1", + }, cancellationToken); + var currentStatus = await DeployTestDme("long_running_test_rooted", DreamDaemonSecurity.Trusted, true, cancellationToken); + await ddUpdateTask; + + Assert.AreEqual(WatchdogStatus.Offline, currentStatus.Status); + + var startJob = await StartDD(cancellationToken); + + await WaitForJob(startJob, 40, false, null, cancellationToken); + + currentStatus = await instanceClient.DreamDaemon.Update(new DreamDaemonRequest + { + SoftShutdown = true, + }, cancellationToken); + + Assert.AreEqual(WatchdogStatus.Online, currentStatus.Status); + + // reimplement TellWorldToReboot because it expects a new deployment and we don't care + System.Console.WriteLine("TEST: Hack world reboot topic..."); + var result = await topicClient.SendTopic(IPAddress.Loopback, "tgs_integration_test_special_tactics=1", ddPort, cancellationToken); + Assert.AreEqual("ack", result.StringData); + + using var tempCts = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken); + var tempToken = tempCts.Token; + using (tempToken.Register(() => System.Console.WriteLine("TEST ERROR: Timeout in RegressionTest1686!"))) + { + tempCts.CancelAfter(TimeSpan.FromMinutes(2)); + + do + { + await Task.Delay(TimeSpan.FromSeconds(1), tempToken); + currentStatus = await instanceClient.DreamDaemon.Read(tempToken); + } + while (currentStatus.Status != WatchdogStatus.Offline); + } + + await CheckDMApiFail(currentStatus.ActiveCompileJob, cancellationToken); + } + + await RunTest(true); + + if (new PlatformIdentifier().IsWindows || !usingBasicWatchdog) + await RunTest(false); + } + async Task InteropTestsForLongRunningDme(CancellationToken cancellationToken) { + await RegressionTest1686(cancellationToken); + await StartAndLeaveRunning(cancellationToken); await RegressionTest1550(cancellationToken); @@ -191,8 +247,7 @@ namespace Tgstation.Server.Tests.Live.Instance async ValueTask RegressionTest1550(CancellationToken cancellationToken) { // we need to cycle deployments twice because TGS holds the initial deployment - await DeployTestDme("LongRunning/long_running_test", DreamDaemonSecurity.Trusted, true, cancellationToken); - var currentStatus = await instanceClient.DreamDaemon.Read(cancellationToken); + var currentStatus = await DeployTestDme("LongRunning/long_running_test", DreamDaemonSecurity.Trusted, true, cancellationToken); Assert.AreEqual(WatchdogStatus.Online, currentStatus.Status); Assert.IsNotNull(currentStatus.StagedCompileJob); From 5f663b419eb4e671fff52909a53683ce826fdcef Mon Sep 17 00:00:00 2001 From: Hawk Date: Sun, 22 Oct 2023 19:02:37 +0100 Subject: [PATCH 10/16] Update Caddy Instructions Provide Caddy 2 Caddyfile. Update Caddy Documenation Link. Clarify PublicPath settings. --- README.md | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index ab7a2556b7..dcceecf82b 100644 --- a/README.md +++ b/README.md @@ -401,14 +401,15 @@ Once complete, test that your configuration worked by visiting your proxy site f #### Caddy (Reccommended for Linux, or those unfamilar with configuring NGINX or Apache) 1. Setup a basic website configuration. Instructions on how to do so are out of scope. -2. In your Caddyfile, under a server entry, add the following (replace 8080 with the port TGS is hosted on): +2. In your Caddyfile, under a server entry, add the following (replace 5000 with the port TGS is hosted on): ``` -proxy /tgs localhost:8080 { - transparent +https://your.site.here { + reverse_proxy localhost:5000 } ``` +3. For this setup, your PublicPath needs to be blank. If you have a path in PublicPath, it needs to be in "reverse_proxy PublicPathHere localhost:5000". -See https://caddyserver.com/docs/proxy +See https://caddyserver.com/docs/caddyfile/directives/reverse_proxy #### NGINX (Reccommended for Linux) From 9d674618a90e7ca5deda34039fd48a13db7538e7 Mon Sep 17 00:00:00 2001 From: Jordan Dominion Date: Sun, 22 Oct 2023 14:09:42 -0400 Subject: [PATCH 11/16] Fix `HardLinkDmbProvider` not mirroring `GameStaticFiles` and other symlinks Also update readme about conditional requirements for GameStaticFiles to function Fixes #1686 --- README.md | 22 ++++++- .../Deployment/HardLinkDmbProvider.cs | 65 ++++++++++++++++--- .../Components/Watchdog/PosixWatchdog.cs | 1 + 3 files changed, 78 insertions(+), 10 deletions(-) diff --git a/README.md b/README.md index ab7a2556b7..b7636f5c85 100644 --- a/README.md +++ b/README.md @@ -570,7 +570,26 @@ This folder can contain anything. But, when certain events occur in the instance #### GameStaticFiles -Any files and folders contained in this root level of this folder will be symbolically linked to all deployments at the time they are created. This allows persistent game data (BYOND `.sav`s or code configuration files for example) to persist across all deployments. This folder contains a .tgsignore file which can be used to prevent symlinks from being generated by entering the names of files and folders (1 per line) +Any files and folders contained in this root level of this folder will be symbolically linked to all deployments at the time they are created. This allows persistent game data (BYOND `.sav`s or code configuration files for example) to persist across all deployments. This folder contains a .tgsignore file which can be used to prevent symlinks from being generated by entering the names of files and folders (1 per line). + +This functionality has the following prerequisites: + +- You are using Windows. + +**OR** + +- Your world uses the TGS DreamMaker API. +- Your world runs with the `Trusted` security level. + +**OR** + +- You are NOT using the basic watchdog. +- The contents of the `GameStaticFiles` directory are on the same filesystem as the instance's `Game` directory. + +**OR** + +- You are using the basic watchdog. +- Your world runs with the `Trusted` security level. ### Clients @@ -614,4 +633,3 @@ Feel free to ask for help [on the discussions page](https://github.com/tgstation * The remainder of the project is licensed under [GNU AGPL v3](http://www.gnu.org/licenses/agpl-3.0.html) See the files in the `/src/DMAPI` tree for the MIT license - diff --git a/src/Tgstation.Server.Host/Components/Deployment/HardLinkDmbProvider.cs b/src/Tgstation.Server.Host/Components/Deployment/HardLinkDmbProvider.cs index cd9a3f1185..263333f6bb 100644 --- a/src/Tgstation.Server.Host/Components/Deployment/HardLinkDmbProvider.cs +++ b/src/Tgstation.Server.Host/Components/Deployment/HardLinkDmbProvider.cs @@ -3,11 +3,13 @@ using System.Collections.Generic; using System.Diagnostics; using System.Globalization; using System.IO; +using System.Runtime.Versioning; using System.Threading; using System.Threading.Tasks; using Microsoft.Extensions.Logging; +using Tgstation.Server.Api.Models; using Tgstation.Server.Host.Configuration; using Tgstation.Server.Host.Extensions; using Tgstation.Server.Host.IO; @@ -18,6 +20,7 @@ namespace Tgstation.Server.Host.Components.Deployment /// /// A that uses hard links. /// + [UnsupportedOSPlatform("windows")] sealed class HardLinkDmbProvider : SwappableDmbProvider { /// @@ -96,6 +99,7 @@ namespace Tgstation.Server.Host.Components.Deployment /// protected override async Task DoSwap(CancellationToken cancellationToken) { + logger.LogTrace("Begin DoSwap, mirroring task complete: {complete}...", mirroringTask.IsCompleted); var mirroredDir = await mirroringTask.WaitAsync(cancellationToken); var goAheadTcs = new TaskCompletionSource(); @@ -103,12 +107,15 @@ namespace Tgstation.Server.Host.Components.Deployment async void DisposeOfOldDirectory() { var directoryMoved = false; - var disposePath = Guid.NewGuid().ToString(); + var disposeGuid = Guid.NewGuid(); + var disposePath = disposeGuid.ToString(); + logger.LogTrace("Moving Live directory to {path} for deletion...", disposeGuid); try { await IOManager.MoveDirectory(LiveGameDirectory, disposePath, cancellationToken); directoryMoved = true; goAheadTcs.SetResult(); + logger.LogTrace("Deleting old Live directory {path}...", disposePath); await IOManager.DeleteDirectory(disposePath, CancellationToken.None); // DCT: We're detached at this point } catch (DirectoryNotFoundException ex) @@ -127,7 +134,9 @@ namespace Tgstation.Server.Host.Components.Deployment DisposeOfOldDirectory(); await goAheadTcs.Task; + logger.LogTrace("Moving mirror directory {path} to Live...", mirroredDir); await IOManager.MoveDirectory(mirroredDir, LiveGameDirectory, cancellationToken); + logger.LogTrace("Swap complete!"); } /// @@ -173,17 +182,40 @@ namespace Tgstation.Server.Host.Components.Deployment { var dir = new DirectoryInfo(src); Task subdirCreationTask = null; + var dreamDaemonWillAcceptOutOfDirectorySymlinks = CompileJob.MinimumSecurityLevel == DreamDaemonSecurity.Trusted; foreach (var subDirectory in dir.EnumerateDirectories()) { + var mirroredName = Path.Combine(dest, subDirectory.Name); + // check if we are a symbolic link - if (!subDirectory.Attributes.HasFlag(FileAttributes.Directory) || subDirectory.Attributes.HasFlag(FileAttributes.ReparsePoint)) - { - logger.LogTrace("Skipping symlink to {subdir}", subDirectory.Name); - continue; - } + if (subDirectory.Attributes.HasFlag(FileAttributes.ReparsePoint)) + if (dreamDaemonWillAcceptOutOfDirectorySymlinks) + { + var target = subDirectory.ResolveLinkTarget(false); + logger.LogDebug("Recreating directory {name} as symlink to {target}", subDirectory.Name, target); + if (subdirCreationTask == null) + { + subdirCreationTask = IOManager.CreateDirectory(dest, cancellationToken); + yield return subdirCreationTask; + } + + async Task CopyLink() + { + await subdirCreationTask.WaitAsync(cancellationToken); + using var lockContext = semaphore != null + ? await SemaphoreSlimContext.Lock(semaphore, cancellationToken) + : null; + await LinkFactory.CreateSymbolicLink(target.FullName, mirroredName, cancellationToken); + } + + yield return CopyLink(); + continue; + } + else + logger.LogDebug("Recreating symlinked directory {name} as hard links...", subDirectory.Name); var checkingSubdirCreationTask = true; - foreach (var copyTask in MirrorDirectoryImpl(subDirectory.FullName, Path.Combine(dest, subDirectory.Name), semaphore, cancellationToken)) + foreach (var copyTask in MirrorDirectoryImpl(subDirectory.FullName, mirroredName, semaphore, cancellationToken)) { if (subdirCreationTask == null) { @@ -214,7 +246,24 @@ namespace Tgstation.Server.Host.Components.Deployment using var lockContext = semaphore != null ? await SemaphoreSlimContext.Lock(semaphore, cancellationToken) : null; - await LinkFactory.CreateHardLink(sourceFile, destFile, cancellationToken); + + if (fileInfo.Attributes.HasFlag(FileAttributes.ReparsePoint)) + { + // AHHHHHHHHHHHHH + var target = fileInfo.ResolveLinkTarget(!dreamDaemonWillAcceptOutOfDirectorySymlinks); + if (dreamDaemonWillAcceptOutOfDirectorySymlinks) + { + logger.LogDebug("Recreating symlinked file {name} as symlink to {target}", fileInfo.Name, target.FullName); + await LinkFactory.CreateSymbolicLink(target.FullName, destFile, cancellationToken); + } + else + { + logger.LogDebug("Recreating symlinked file {name} as hard link to {target}", fileInfo.Name, target.FullName); + await LinkFactory.CreateHardLink(target.FullName, destFile, cancellationToken); + } + } + else + await LinkFactory.CreateHardLink(sourceFile, destFile, cancellationToken); } yield return LinkThisFile(); diff --git a/src/Tgstation.Server.Host/Components/Watchdog/PosixWatchdog.cs b/src/Tgstation.Server.Host/Components/Watchdog/PosixWatchdog.cs index eb563bd1fa..2fb5ad1d54 100644 --- a/src/Tgstation.Server.Host/Components/Watchdog/PosixWatchdog.cs +++ b/src/Tgstation.Server.Host/Components/Watchdog/PosixWatchdog.cs @@ -1,4 +1,5 @@ using System; +using System.Runtime.Versioning; using System.Threading; using System.Threading.Tasks; From 50072ae2227b48b7f0f638378e4087df765e01df Mon Sep 17 00:00:00 2001 From: Jordan Dominion Date: Sun, 22 Oct 2023 14:20:24 -0400 Subject: [PATCH 12/16] Run milestone check on labeling --- .github/workflows/check-pr-has-milestone.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/check-pr-has-milestone.yml b/.github/workflows/check-pr-has-milestone.yml index 954d539927..7f6ed97d2d 100644 --- a/.github/workflows/check-pr-has-milestone.yml +++ b/.github/workflows/check-pr-has-milestone.yml @@ -2,7 +2,7 @@ name: "Check PR Has Milestone" on: pull_request: - types: [ opened, edited, synchronize, reopened ] + types: [ opened, edited, synchronize, reopened, labeled ] branches: - dev - master From cc0b46205f0bf0fb663eabaf8fe74e61883bce60 Mon Sep 17 00:00:00 2001 From: Jordan Dominion Date: Sun, 22 Oct 2023 14:28:04 -0400 Subject: [PATCH 13/16] Fix warnings with OS restricted classes --- .../Components/Deployment/SymlinkDmbProvider.cs | 4 +++- .../Components/Watchdog/PosixWatchdog.cs | 1 + .../Components/Watchdog/PosixWatchdogFactory.cs | 2 ++ .../Components/Watchdog/WindowsWatchdog.cs | 4 +++- .../Components/Watchdog/WindowsWatchdogFactory.cs | 2 ++ 5 files changed, 11 insertions(+), 2 deletions(-) diff --git a/src/Tgstation.Server.Host/Components/Deployment/SymlinkDmbProvider.cs b/src/Tgstation.Server.Host/Components/Deployment/SymlinkDmbProvider.cs index 2edc58d37e..0054e7fa4b 100644 --- a/src/Tgstation.Server.Host/Components/Deployment/SymlinkDmbProvider.cs +++ b/src/Tgstation.Server.Host/Components/Deployment/SymlinkDmbProvider.cs @@ -1,4 +1,5 @@ -using System.Threading; +using System.Runtime.Versioning; +using System.Threading; using System.Threading.Tasks; using Tgstation.Server.Host.IO; @@ -8,6 +9,7 @@ namespace Tgstation.Server.Host.Components.Deployment /// /// A that uses symlinks. /// + [SupportedOSPlatform("windows")] sealed class SymlinkDmbProvider : SwappableDmbProvider { /// diff --git a/src/Tgstation.Server.Host/Components/Watchdog/PosixWatchdog.cs b/src/Tgstation.Server.Host/Components/Watchdog/PosixWatchdog.cs index 2fb5ad1d54..818dea9d7c 100644 --- a/src/Tgstation.Server.Host/Components/Watchdog/PosixWatchdog.cs +++ b/src/Tgstation.Server.Host/Components/Watchdog/PosixWatchdog.cs @@ -22,6 +22,7 @@ namespace Tgstation.Server.Host.Components.Watchdog /// /// A variant of the that works on POSIX systems. /// + [UnsupportedOSPlatform("windows")] sealed class PosixWatchdog : AdvancedWatchdog { /// diff --git a/src/Tgstation.Server.Host/Components/Watchdog/PosixWatchdogFactory.cs b/src/Tgstation.Server.Host/Components/Watchdog/PosixWatchdogFactory.cs index 64c8672262..1d0d140bc0 100644 --- a/src/Tgstation.Server.Host/Components/Watchdog/PosixWatchdogFactory.cs +++ b/src/Tgstation.Server.Host/Components/Watchdog/PosixWatchdogFactory.cs @@ -1,4 +1,5 @@ using System; +using System.Runtime.Versioning; using Microsoft.Extensions.Logging; using Microsoft.Extensions.Options; @@ -20,6 +21,7 @@ namespace Tgstation.Server.Host.Components.Watchdog /// /// for creating s. /// + [UnsupportedOSPlatform("windows")] sealed class PosixWatchdogFactory : WindowsWatchdogFactory { /// diff --git a/src/Tgstation.Server.Host/Components/Watchdog/WindowsWatchdog.cs b/src/Tgstation.Server.Host/Components/Watchdog/WindowsWatchdog.cs index 05c1f10088..8ff6bf3362 100644 --- a/src/Tgstation.Server.Host/Components/Watchdog/WindowsWatchdog.cs +++ b/src/Tgstation.Server.Host/Components/Watchdog/WindowsWatchdog.cs @@ -1,4 +1,5 @@ -using System.Threading; +using System.Runtime.Versioning; +using System.Threading; using System.Threading.Tasks; using Microsoft.Extensions.Logging; @@ -19,6 +20,7 @@ namespace Tgstation.Server.Host.Components.Watchdog /// /// A variant of the that works on Windows systems. /// + [SupportedOSPlatform("windows")] sealed class WindowsWatchdog : AdvancedWatchdog { /// diff --git a/src/Tgstation.Server.Host/Components/Watchdog/WindowsWatchdogFactory.cs b/src/Tgstation.Server.Host/Components/Watchdog/WindowsWatchdogFactory.cs index 89ed6b145d..0888294aa4 100644 --- a/src/Tgstation.Server.Host/Components/Watchdog/WindowsWatchdogFactory.cs +++ b/src/Tgstation.Server.Host/Components/Watchdog/WindowsWatchdogFactory.cs @@ -1,4 +1,5 @@ using System; +using System.Runtime.Versioning; using Microsoft.Extensions.Logging; using Microsoft.Extensions.Options; @@ -20,6 +21,7 @@ namespace Tgstation.Server.Host.Components.Watchdog /// /// for creating s. /// + [SupportedOSPlatform("windows")] class WindowsWatchdogFactory : WatchdogFactory { /// From ad2a5d2b9b131575a67ec2b709ec98fcd44f89c6 Mon Sep 17 00:00:00 2001 From: Jordan Dominion Date: Sun, 22 Oct 2023 14:30:13 -0400 Subject: [PATCH 14/16] Fix release build warnings --- .../Components/Session/SessionController.cs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/Tgstation.Server.Host/Components/Session/SessionController.cs b/src/Tgstation.Server.Host/Components/Session/SessionController.cs index b33d3628a1..1ee53dfb57 100644 --- a/src/Tgstation.Server.Host/Components/Session/SessionController.cs +++ b/src/Tgstation.Server.Host/Components/Session/SessionController.cs @@ -795,6 +795,8 @@ namespace Tgstation.Server.Host.Components.Session if (!firstValidationRequest) return BridgeError("Startup bridge request was repeated!"); } +#else + postValidationShutdownTask = Task.CompletedTask; #endif if (parameters.Version == null) From a97001231ff6b1606b54adc16f4ee19a01e04069 Mon Sep 17 00:00:00 2001 From: Hawk-v3 Date: Sun, 22 Oct 2023 19:32:55 +0100 Subject: [PATCH 15/16] Update README.md Accept correction for definition of PublicPath Co-authored-by: Jordan Dominion --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index dcceecf82b..64221f5028 100644 --- a/README.md +++ b/README.md @@ -407,7 +407,7 @@ https://your.site.here { reverse_proxy localhost:5000 } ``` -3. For this setup, your PublicPath needs to be blank. If you have a path in PublicPath, it needs to be in "reverse_proxy PublicPathHere localhost:5000". +3. For this setup, your configuration's `ControlPanel:PublicPath` needs to be blank. If you have a path in `PublicPath`, it needs to be in "reverse_proxy PublicPathHere localhost:5000". See https://caddyserver.com/docs/caddyfile/directives/reverse_proxy From 5afc74c4872ca4c49fe7990c316fecd4b0f46603 Mon Sep 17 00:00:00 2001 From: Jordan Dominion Date: Sun, 22 Oct 2023 16:01:52 -0400 Subject: [PATCH 16/16] Fix more Release build warnings Not being able to build locally in release mode because of the .NET 8 RC SDK is painful --- .../Components/Deployment/SymlinkDmbProvider.cs | 4 +--- .../Components/Watchdog/WindowsWatchdog.cs | 4 +--- .../Components/Watchdog/WindowsWatchdogFactory.cs | 2 -- 3 files changed, 2 insertions(+), 8 deletions(-) diff --git a/src/Tgstation.Server.Host/Components/Deployment/SymlinkDmbProvider.cs b/src/Tgstation.Server.Host/Components/Deployment/SymlinkDmbProvider.cs index 0054e7fa4b..2edc58d37e 100644 --- a/src/Tgstation.Server.Host/Components/Deployment/SymlinkDmbProvider.cs +++ b/src/Tgstation.Server.Host/Components/Deployment/SymlinkDmbProvider.cs @@ -1,5 +1,4 @@ -using System.Runtime.Versioning; -using System.Threading; +using System.Threading; using System.Threading.Tasks; using Tgstation.Server.Host.IO; @@ -9,7 +8,6 @@ namespace Tgstation.Server.Host.Components.Deployment /// /// A that uses symlinks. /// - [SupportedOSPlatform("windows")] sealed class SymlinkDmbProvider : SwappableDmbProvider { /// diff --git a/src/Tgstation.Server.Host/Components/Watchdog/WindowsWatchdog.cs b/src/Tgstation.Server.Host/Components/Watchdog/WindowsWatchdog.cs index 8ff6bf3362..05c1f10088 100644 --- a/src/Tgstation.Server.Host/Components/Watchdog/WindowsWatchdog.cs +++ b/src/Tgstation.Server.Host/Components/Watchdog/WindowsWatchdog.cs @@ -1,5 +1,4 @@ -using System.Runtime.Versioning; -using System.Threading; +using System.Threading; using System.Threading.Tasks; using Microsoft.Extensions.Logging; @@ -20,7 +19,6 @@ namespace Tgstation.Server.Host.Components.Watchdog /// /// A variant of the that works on Windows systems. /// - [SupportedOSPlatform("windows")] sealed class WindowsWatchdog : AdvancedWatchdog { /// diff --git a/src/Tgstation.Server.Host/Components/Watchdog/WindowsWatchdogFactory.cs b/src/Tgstation.Server.Host/Components/Watchdog/WindowsWatchdogFactory.cs index 0888294aa4..89ed6b145d 100644 --- a/src/Tgstation.Server.Host/Components/Watchdog/WindowsWatchdogFactory.cs +++ b/src/Tgstation.Server.Host/Components/Watchdog/WindowsWatchdogFactory.cs @@ -1,5 +1,4 @@ using System; -using System.Runtime.Versioning; using Microsoft.Extensions.Logging; using Microsoft.Extensions.Options; @@ -21,7 +20,6 @@ namespace Tgstation.Server.Host.Components.Watchdog /// /// for creating s. /// - [SupportedOSPlatform("windows")] class WindowsWatchdogFactory : WatchdogFactory { ///