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
diff --git a/.github/workflows/ci-pipeline.yml b/.github/workflows/ci-pipeline.yml
index 171a10eeab..3a5e143e6b 100644
--- a/.github/workflows/ci-pipeline.yml
+++ b/.github/workflows/ci-pipeline.yml
@@ -366,7 +366,7 @@ jobs:
fail-fast: false
matrix:
database-type: [ 'SqlServer', 'Sqlite', 'PostgresSql', 'MariaDB', 'MySql' ]
- watchdog-type: [ 'Basic', 'System' ]
+ watchdog-type: [ 'Basic', 'Advanced' ]
configuration: [ 'Debug', 'Release' ]
runs-on: windows-latest
steps:
@@ -383,7 +383,7 @@ jobs:
dotnet-version: '${{ env.TGS_DOTNET_VERSION }}.0.x'
- name: Set TGS_TEST_DUMP_API_SPEC
- if: ${{ matrix.configuration == 'Release' && matrix.watchdog-type == 'System' && matrix.database-type == 'SqlServer' }}
+ if: ${{ matrix.configuration == 'Release' && matrix.watchdog-type == 'Advanced' && matrix.database-type == 'SqlServer' }}
run: echo "TGS_TEST_DUMP_API_SPEC=yes" >> $Env:GITHUB_ENV
- name: Set General__UseBasicWatchdog
@@ -474,7 +474,7 @@ jobs:
path: ./TestResults/
- name: Store OpenAPI Spec
- if: ${{ matrix.configuration == 'Release' && matrix.watchdog-type == 'System' && matrix.database-type == 'SqlServer' }}
+ if: ${{ matrix.configuration == 'Release' && matrix.watchdog-type == 'Advanced' && matrix.database-type == 'SqlServer' }}
uses: actions/upload-artifact@v3
with:
name: openapi-spec
@@ -563,7 +563,7 @@ jobs:
fail-fast: false
matrix:
database-type: [ 'Sqlite', 'PostgresSql', 'MariaDB', 'MySql' ]
- watchdog-type: [ 'Basic', 'System' ]
+ watchdog-type: [ 'Basic', 'Advanced' ]
configuration: [ 'Debug', 'Release' ]
runs-on: ubuntu-latest
steps:
@@ -646,7 +646,7 @@ jobs:
path: ./TestResults/
- name: Package Server Console
- if: ${{ matrix.configuration == 'Release' && matrix.watchdog-type == 'System' && matrix.database-type == 'MariaDB' }}
+ if: ${{ matrix.configuration == 'Release' && matrix.watchdog-type == 'Advanced' && matrix.database-type == 'MariaDB' }}
run: |
cd src/Tgstation.Server.Host.Console
dotnet publish -c ${{ matrix.configuration }} -o ../../artifacts/Console
@@ -658,7 +658,7 @@ jobs:
build/RemoveUnsupportedRuntimes.sh artifacts/Console
- name: Package Server Update Package
- if: ${{ matrix.configuration == 'Release' && matrix.watchdog-type == 'System' && matrix.database-type == 'PostgresSql' }}
+ if: ${{ matrix.configuration == 'Release' && matrix.watchdog-type == 'Advanced' && matrix.database-type == 'PostgresSql' }}
run: |
cd src/Tgstation.Server.Host
dotnet publish -c ${{ matrix.configuration }}NoWindows --no-build -o ../../artifacts/ServerUpdate
@@ -667,14 +667,14 @@ jobs:
build/RemoveUnsupportedRuntimes.sh artifacts/ServerUpdate
- name: Store Server Console
- if: ${{ matrix.configuration == 'Release' && matrix.watchdog-type == 'System' && matrix.database-type == 'MariaDB' }}
+ if: ${{ matrix.configuration == 'Release' && matrix.watchdog-type == 'Advanced' && matrix.database-type == 'MariaDB' }}
uses: actions/upload-artifact@v3
with:
name: ServerConsole
path: artifacts/Console/
- name: Store Server Update Package
- if: ${{ matrix.configuration == 'Release' && matrix.watchdog-type == 'System' && matrix.database-type == 'PostgresSql' }}
+ if: ${{ matrix.configuration == 'Release' && matrix.watchdog-type == 'Advanced' && matrix.database-type == 'PostgresSql' }}
uses: actions/upload-artifact@v3
with:
name: ServerUpdatePackage
@@ -736,100 +736,100 @@ jobs:
name: linux-unit-test-coverage-Release
path: ./code_coverage/unit_tests/linux_unit_tests_release
- - name: Retrieve Linux Integration Test Coverage (Release, System, Sqlite)
+ - name: Retrieve Linux Integration Test Coverage (Release, Advanced, Sqlite)
uses: actions/download-artifact@v3
with:
- name: linux-integration-test-coverage-Release-System-Sqlite
+ name: linux-integration-test-coverage-Release-Advanced-Sqlite
path: ./code_coverage/integration_tests/linux_integration_tests_release_system_sqlite
- - name: Retrieve Linux Integration Test Coverage (Release, System, PostgresSql)
+ - name: Retrieve Linux Integration Test Coverage (Release, Advanced, PostgresSql)
uses: actions/download-artifact@v3
with:
- name: linux-integration-test-coverage-Release-System-PostgresSql
+ name: linux-integration-test-coverage-Release-Advanced-PostgresSql
path: ./code_coverage/integration_tests/linux_integration_tests_release_system_mariadb
- - name: Retrieve Linux Integration Test Coverage (Release, System, MariaDB)
+ - name: Retrieve Linux Integration Test Coverage (Release, Advanced, MariaDB)
uses: actions/download-artifact@v3
with:
- name: linux-integration-test-coverage-Release-System-MariaDB
+ name: linux-integration-test-coverage-Release-Advanced-MariaDB
path: ./code_coverage/integration_tests/linux_integration_tests_release_system_mysql
- - name: Retrieve Linux Integration Test Coverage (Release, System, MySql)
+ - name: Retrieve Linux Integration Test Coverage (Release, Advanced, MySql)
uses: actions/download-artifact@v3
with:
- name: linux-integration-test-coverage-Release-System-MySql
+ name: linux-integration-test-coverage-Release-Advanced-MySql
path: ./code_coverage/integration_tests/linux_integration_tests_release_system_mysql
- name: Retrieve Linux Integration Test Coverage (Release, Basic, Sqlite)
uses: actions/download-artifact@v3
with:
- name: linux-integration-test-coverage-Release-System-Sqlite
+ name: linux-integration-test-coverage-Release-Advanced-Sqlite
path: ./code_coverage/integration_tests/linux_integration_tests_release_basic_sqlite
- name: Retrieve Linux Integration Test Coverage (Release, Basic, PostgresSql)
uses: actions/download-artifact@v3
with:
- name: linux-integration-test-coverage-Release-System-PostgresSql
+ name: linux-integration-test-coverage-Release-Advanced-PostgresSql
path: ./code_coverage/integration_tests/linux_integration_tests_release_basic_mariadb
- name: Retrieve Linux Integration Test Coverage (Release, Basic, MariaDB)
uses: actions/download-artifact@v3
with:
- name: linux-integration-test-coverage-Release-System-MariaDB
+ name: linux-integration-test-coverage-Release-Advanced-MariaDB
path: ./code_coverage/integration_tests/linux_integration_tests_release_basic_mysql
- name: Retrieve Linux Integration Test Coverage (Release, Basic, MySql)
uses: actions/download-artifact@v3
with:
- name: linux-integration-test-coverage-Release-System-MySql
+ name: linux-integration-test-coverage-Release-Advanced-MySql
path: ./code_coverage/integration_tests/linux_integration_tests_release_basic_mysql
- - name: Retrieve Linux Integration Test Coverage (Debug, System, Sqlite)
+ - name: Retrieve Linux Integration Test Coverage (Debug, Advanced, Sqlite)
uses: actions/download-artifact@v3
with:
- name: linux-integration-test-coverage-Debug-System-Sqlite
+ name: linux-integration-test-coverage-Debug-Advanced-Sqlite
path: ./code_coverage/integration_tests/linux_integration_tests_debug_system_sqlite
- - name: Retrieve Linux Integration Test Coverage (Debug, System, PostgresSql)
+ - name: Retrieve Linux Integration Test Coverage (Debug, Advanced, PostgresSql)
uses: actions/download-artifact@v3
with:
- name: linux-integration-test-coverage-Debug-System-PostgresSql
+ name: linux-integration-test-coverage-Debug-Advanced-PostgresSql
path: ./code_coverage/integration_tests/linux_integration_tests_debug_system_mariadb
- - name: Retrieve Linux Integration Test Coverage (Debug, System, MariaDB)
+ - name: Retrieve Linux Integration Test Coverage (Debug, Advanced, MariaDB)
uses: actions/download-artifact@v3
with:
- name: linux-integration-test-coverage-Debug-System-MariaDB
+ name: linux-integration-test-coverage-Debug-Advanced-MariaDB
path: ./code_coverage/integration_tests/linux_integration_tests_debug_system_mysql
- - name: Retrieve Linux Integration Test Coverage (Debug, System, MySql)
+ - name: Retrieve Linux Integration Test Coverage (Debug, Advanced, MySql)
uses: actions/download-artifact@v3
with:
- name: linux-integration-test-coverage-Debug-System-MySql
+ name: linux-integration-test-coverage-Debug-Advanced-MySql
path: ./code_coverage/integration_tests/linux_integration_tests_debug_system_mysql
- name: Retrieve Linux Integration Test Coverage (Debug, Basic, Sqlite)
uses: actions/download-artifact@v3
with:
- name: linux-integration-test-coverage-Debug-System-Sqlite
+ name: linux-integration-test-coverage-Debug-Advanced-Sqlite
path: ./code_coverage/integration_tests/linux_integration_tests_debug_basic_sqlite
- name: Retrieve Linux Integration Test Coverage (Debug, Basic, PostgresSql)
uses: actions/download-artifact@v3
with:
- name: linux-integration-test-coverage-Debug-System-PostgresSql
+ name: linux-integration-test-coverage-Debug-Advanced-PostgresSql
path: ./code_coverage/integration_tests/linux_integration_tests_debug_basic_mariadb
- name: Retrieve Linux Integration Test Coverage (Debug, Basic, MariaDB)
uses: actions/download-artifact@v3
with:
- name: linux-integration-test-coverage-Debug-System-MariaDB
+ name: linux-integration-test-coverage-Debug-Advanced-MariaDB
path: ./code_coverage/integration_tests/linux_integration_tests_debug_basic_mysql
- name: Retrieve Linux Integration Test Coverage (Debug, Basic, MySql)
uses: actions/download-artifact@v3
with:
- name: linux-integration-test-coverage-Debug-System-MySql
+ name: linux-integration-test-coverage-Debug-Advanced-MySql
path: ./code_coverage/integration_tests/linux_integration_tests_debug_basic_mysql
- name: Retrieve Windows Unit Test Coverage (Release)
@@ -850,16 +850,16 @@ jobs:
name: windows-integration-test-coverage-Release-Basic-SqlServer
path: ./code_coverage/integration_tests/windows_integration_tests_release_basic_sqlserver
- - name: Retrieve Windows Integration Test Coverage (Debug, System, SqlServer)
+ - name: Retrieve Windows Integration Test Coverage (Debug, Advanced, SqlServer)
uses: actions/download-artifact@v3
with:
- name: windows-integration-test-coverage-Debug-System-SqlServer
+ name: windows-integration-test-coverage-Debug-Advanced-SqlServer
path: ./code_coverage/integration_tests/windows_integration_tests_debug_system_sqlserver
- - name: Retrieve Windows Integration Test Coverage (Release, System, SqlServer)
+ - name: Retrieve Windows Integration Test Coverage (Release, Advanced, SqlServer)
uses: actions/download-artifact@v3
with:
- name: windows-integration-test-coverage-Release-System-SqlServer
+ name: windows-integration-test-coverage-Release-Advanced-SqlServer
path: ./code_coverage/integration_tests/windows_integration_tests_release_system_sqlserver
- name: Retrieve Windows Integration Test Coverage (Debug, Basic, MariaDB)
@@ -874,16 +874,16 @@ jobs:
name: windows-integration-test-coverage-Release-Basic-MariaDB
path: ./code_coverage/integration_tests/windows_integration_tests_release_basic_mariadb
- - name: Retrieve Windows Integration Test Coverage (Debug, System, MariaDB)
+ - name: Retrieve Windows Integration Test Coverage (Debug, Advanced, MariaDB)
uses: actions/download-artifact@v3
with:
- name: windows-integration-test-coverage-Debug-System-MariaDB
+ name: windows-integration-test-coverage-Debug-Advanced-MariaDB
path: ./code_coverage/integration_tests/windows_integration_tests_debug_system_mariadb
- - name: Retrieve Windows Integration Test Coverage (Release, System, MariaDB)
+ - name: Retrieve Windows Integration Test Coverage (Release, Advanced, MariaDB)
uses: actions/download-artifact@v3
with:
- name: windows-integration-test-coverage-Release-System-MariaDB
+ name: windows-integration-test-coverage-Release-Advanced-MariaDB
path: ./code_coverage/integration_tests/windows_integration_tests_release_system_mariadb
- name: Retrieve Windows Integration Test Coverage (Debug, Basic, MySql)
@@ -898,16 +898,16 @@ jobs:
name: windows-integration-test-coverage-Release-Basic-MySql
path: ./code_coverage/integration_tests/windows_integration_tests_release_basic_mysql
- - name: Retrieve Windows Integration Test Coverage (Debug, System, MySql)
+ - name: Retrieve Windows Integration Test Coverage (Debug, Advanced, MySql)
uses: actions/download-artifact@v3
with:
- name: windows-integration-test-coverage-Debug-System-MySql
+ name: windows-integration-test-coverage-Debug-Advanced-MySql
path: ./code_coverage/integration_tests/windows_integration_tests_debug_system_mysql
- - name: Retrieve Windows Integration Test Coverage (Release, System, MySql)
+ - name: Retrieve Windows Integration Test Coverage (Release, Advanced, MySql)
uses: actions/download-artifact@v3
with:
- name: windows-integration-test-coverage-Release-System-MySql
+ name: windows-integration-test-coverage-Release-Advanced-MySql
path: ./code_coverage/integration_tests/windows_integration_tests_release_system_mysql
- name: Retrieve Windows Integration Test Coverage (Debug, Basic, PostgresSql)
@@ -922,16 +922,16 @@ jobs:
name: windows-integration-test-coverage-Release-Basic-PostgresSql
path: ./code_coverage/integration_tests/windows_integration_tests_release_basic_postgressql
- - name: Retrieve Windows Integration Test Coverage (Debug, System, PostgresSql)
+ - name: Retrieve Windows Integration Test Coverage (Debug, Advanced, PostgresSql)
uses: actions/download-artifact@v3
with:
- name: windows-integration-test-coverage-Debug-System-PostgresSql
+ name: windows-integration-test-coverage-Debug-Advanced-PostgresSql
path: ./code_coverage/integration_tests/windows_integration_tests_debug_system_postgressql
- - name: Retrieve Windows Integration Test Coverage (Release, System, PostgresSql)
+ - name: Retrieve Windows Integration Test Coverage (Release, Advanced, PostgresSql)
uses: actions/download-artifact@v3
with:
- name: windows-integration-test-coverage-Release-System-PostgresSql
+ name: windows-integration-test-coverage-Release-Advanced-PostgresSql
path: ./code_coverage/integration_tests/windows_integration_tests_release_system_postgressql
- name: Retrieve Windows Integration Test Coverage (Debug, Basic, Sqlite)
@@ -946,16 +946,16 @@ jobs:
name: windows-integration-test-coverage-Release-Basic-Sqlite
path: ./code_coverage/integration_tests/windows_integration_tests_release_basic_sqlite
- - name: Retrieve Windows Integration Test Coverage (Debug, System, Sqlite)
+ - name: Retrieve Windows Integration Test Coverage (Debug, Advanced, Sqlite)
uses: actions/download-artifact@v3
with:
- name: windows-integration-test-coverage-Debug-System-Sqlite
+ name: windows-integration-test-coverage-Debug-Advanced-Sqlite
path: ./code_coverage/integration_tests/windows_integration_tests_debug_system_sqlite
- - name: Retrieve Windows Integration Test Coverage (Release, System, Sqlite)
+ - name: Retrieve Windows Integration Test Coverage (Release, Advanced, Sqlite)
uses: actions/download-artifact@v3
with:
- name: windows-integration-test-coverage-Release-System-Sqlite
+ name: windows-integration-test-coverage-Release-Advanced-Sqlite
path: ./code_coverage/integration_tests/windows_integration_tests_release_system_sqlite
- name: Upload Coverage to CodeCov
@@ -1774,5 +1774,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 }}
diff --git a/README.md b/README.md
index 4d6d1b192c..a288af1e99 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 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/proxy
+See https://caddyserver.com/docs/caddyfile/directives/reverse_proxy
#### NGINX (Reccommended for Linux)
@@ -570,7 +571,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 +634,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/build/Version.props b/build/Version.props
index 0e1866162a..5a1e5a5347 100644
--- a/build/Version.props
+++ b/build/Version.props
@@ -9,8 +9,8 @@
7.0.0
12.0.0
14.0.0
- 6.5.3
- 5.6.1
+ 6.6.0
+ 5.6.2
1.4.0
1.2.1
2.0.0
diff --git a/src/DMAPI/tgs.dm b/src/DMAPI/tgs.dm
index 6187a67825..9825cd118b 100644
--- a/src/DMAPI/tgs.dm
+++ b/src/DMAPI/tgs.dm
@@ -1,6 +1,6 @@
// tgstation-server DMAPI
-#define TGS_DMAPI_VERSION "6.5.3"
+#define TGS_DMAPI_VERSION "6.6.0"
// All functions and datums outside this document are subject to change with any version and should not be relied on.
@@ -129,6 +129,13 @@
/// DreamDaemon Ultrasafe security level.
#define TGS_SECURITY_ULTRASAFE 2
+/// DreamDaemon public visibility level.
+#define TGS_VISIBILITY_PUBLIC 0
+/// DreamDaemon private visibility level.
+#define TGS_VISIBILITY_PRIVATE 1
+/// DreamDaemon invisible visibility level.
+#define TGS_VISIBILITY_INVISIBLE 2
+
//REQUIRED HOOKS
/**
@@ -458,6 +465,10 @@
/world/proc/TgsSecurityLevel()
return
+/// Returns the current BYOND visibility level as a TGS_VISIBILITY_ define if TGS is present, null otherwise. Requires TGS to be using interop API version 5 or higher otherwise the string "___unimplemented" wil be returned. This function may sleep if the call to [/world/proc/TgsNew] is sleeping!
+/world/proc/TgsVisibility()
+ return
+
/// Returns a list of active [/datum/tgs_revision_information/test_merge]s if TGS is present, null otherwise. This function may sleep if the call to [/world/proc/TgsNew] is sleeping!
/world/proc/TgsTestMerges()
return
diff --git a/src/DMAPI/tgs/core/core.dm b/src/DMAPI/tgs/core/core.dm
index 41a0473394..b9a9f27a28 100644
--- a/src/DMAPI/tgs/core/core.dm
+++ b/src/DMAPI/tgs/core/core.dm
@@ -153,4 +153,9 @@
/world/TgsSecurityLevel()
var/datum/tgs_api/api = TGS_READ_GLOBAL(tgs)
if(api)
- api.SecurityLevel()
+ return api.SecurityLevel()
+
+/world/TgsVisibility()
+ var/datum/tgs_api/api = TGS_READ_GLOBAL(tgs)
+ if(api)
+ return api.Visibility()
diff --git a/src/DMAPI/tgs/core/datum.dm b/src/DMAPI/tgs/core/datum.dm
index 68b0330fe8..93377079aa 100644
--- a/src/DMAPI/tgs/core/datum.dm
+++ b/src/DMAPI/tgs/core/datum.dm
@@ -57,3 +57,6 @@ TGS_PROTECT_DATUM(/datum/tgs_api)
/datum/tgs_api/proc/SecurityLevel()
return TGS_UNIMPLEMENTED
+
+/datum/tgs_api/proc/Visibility()
+ return TGS_UNIMPLEMENTED
diff --git a/src/DMAPI/tgs/v5/__interop_version.dm b/src/DMAPI/tgs/v5/__interop_version.dm
index 5d3d491a73..1b52b31d6a 100644
--- a/src/DMAPI/tgs/v5/__interop_version.dm
+++ b/src/DMAPI/tgs/v5/__interop_version.dm
@@ -1 +1 @@
-"5.6.1"
+"5.6.2"
diff --git a/src/DMAPI/tgs/v5/_defines.dm b/src/DMAPI/tgs/v5/_defines.dm
index f973338daa..bdcd4e4dd5 100644
--- a/src/DMAPI/tgs/v5/_defines.dm
+++ b/src/DMAPI/tgs/v5/_defines.dm
@@ -48,6 +48,7 @@
#define DMAPI5_RUNTIME_INFORMATION_REVISION "revision"
#define DMAPI5_RUNTIME_INFORMATION_TEST_MERGES "testMerges"
#define DMAPI5_RUNTIME_INFORMATION_SECURITY_LEVEL "securityLevel"
+#define DMAPI5_RUNTIME_INFORMATION_VISIBILITY "visibility"
#define DMAPI5_CHAT_UPDATE_CHANNELS "channels"
diff --git a/src/DMAPI/tgs/v5/api.dm b/src/DMAPI/tgs/v5/api.dm
index 34cc43f876..45250efc46 100644
--- a/src/DMAPI/tgs/v5/api.dm
+++ b/src/DMAPI/tgs/v5/api.dm
@@ -4,6 +4,7 @@
var/instance_name
var/security_level
+ var/visibility
var/reboot_mode = TGS_REBOOT_MODE_NORMAL
@@ -54,6 +55,7 @@
version = new /datum/tgs_version(runtime_information[DMAPI5_RUNTIME_INFORMATION_SERVER_VERSION])
security_level = runtime_information[DMAPI5_RUNTIME_INFORMATION_SECURITY_LEVEL]
+ visibility = runtime_information[DMAPI5_RUNTIME_INFORMATION_VISIBILITY]
instance_name = runtime_information[DMAPI5_RUNTIME_INFORMATION_INSTANCE_NAME]
var/list/revisionData = runtime_information[DMAPI5_RUNTIME_INFORMATION_REVISION]
@@ -252,3 +254,7 @@
/datum/tgs_api/v5/SecurityLevel()
RequireInitialBridgeResponse()
return security_level
+
+/datum/tgs_api/v5/Visibility()
+ RequireInitialBridgeResponse()
+ return visibility
diff --git a/src/DMAPI/tgs/v5/undefs.dm b/src/DMAPI/tgs/v5/undefs.dm
index c679737dfc..f163adaaaf 100644
--- a/src/DMAPI/tgs/v5/undefs.dm
+++ b/src/DMAPI/tgs/v5/undefs.dm
@@ -48,6 +48,7 @@
#undef DMAPI5_RUNTIME_INFORMATION_REVISION
#undef DMAPI5_RUNTIME_INFORMATION_TEST_MERGES
#undef DMAPI5_RUNTIME_INFORMATION_SECURITY_LEVEL
+#undef DMAPI5_RUNTIME_INFORMATION_VISIBILITY
#undef DMAPI5_CHAT_UPDATE_CHANNELS
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)
diff --git a/src/Tgstation.Server.Host/Components/Deployment/DmbFactory.cs b/src/Tgstation.Server.Host/Components/Deployment/DmbFactory.cs
index b4e333a41d..31bef5dfdb 100644
--- a/src/Tgstation.Server.Host/Components/Deployment/DmbFactory.cs
+++ b/src/Tgstation.Server.Host/Components/Deployment/DmbFactory.cs
@@ -149,15 +149,18 @@ namespace Tgstation.Server.Host.Components.Deployment
cancellationToken);
}
+ ValueTask dmbDisposeTask;
lock (jobLockCounts)
{
- nextDmbProvider?.Dispose();
+ dmbDisposeTask = nextDmbProvider?.DisposeAsync() ?? ValueTask.CompletedTask;
nextDmbProvider = newProvider;
// Oh god dammit
var temp = Interlocked.Exchange(ref newerDmbTcs, new TaskCompletionSource());
temp.SetResult();
}
+
+ await dmbDisposeTask;
}
///
@@ -321,7 +324,7 @@ namespace Tgstation.Server.Host.Components.Deployment
finally
{
if (!providerSubmitted)
- newProvider.Dispose();
+ await newProvider.DisposeAsync();
}
}
#pragma warning restore CA1506
diff --git a/src/Tgstation.Server.Host/Components/Deployment/DmbProvider.cs b/src/Tgstation.Server.Host/Components/Deployment/DmbProvider.cs
index f3711ede4f..770409f790 100644
--- a/src/Tgstation.Server.Host/Components/Deployment/DmbProvider.cs
+++ b/src/Tgstation.Server.Host/Components/Deployment/DmbProvider.cs
@@ -1,4 +1,5 @@
using System;
+using System.Threading.Tasks;
using Tgstation.Server.Host.IO;
using Tgstation.Server.Host.Models;
@@ -30,7 +31,7 @@ namespace Tgstation.Server.Host.Components.Deployment
readonly string directoryAppend;
///
- /// The to run when is called.
+ /// The to run when is called.
///
Action onDispose;
@@ -50,7 +51,11 @@ namespace Tgstation.Server.Host.Components.Deployment
}
///
- public void Dispose() => onDispose?.Invoke();
+ public ValueTask DisposeAsync()
+ {
+ onDispose?.Invoke();
+ return ValueTask.CompletedTask;
+ }
///
public void KeepAlive() => onDispose = null;
diff --git a/src/Tgstation.Server.Host/Components/Deployment/DreamMaker.cs b/src/Tgstation.Server.Host/Components/Deployment/DreamMaker.cs
index 30f6a555df..c561e25939 100644
--- a/src/Tgstation.Server.Host/Components/Deployment/DreamMaker.cs
+++ b/src/Tgstation.Server.Host/Components/Deployment/DreamMaker.cs
@@ -803,7 +803,7 @@ namespace Tgstation.Server.Host.Components.Deployment
job.MinimumSecurityLevel = securityLevel; // needed for the TempDmbProvider
ApiValidationStatus validationStatus;
- using (var provider = new TemporaryDmbProvider(ioManager.ResolvePath(job.DirectoryName.ToString()), String.Concat(job.DmeName, DmbExtension), job))
+ await using (var provider = new TemporaryDmbProvider(ioManager.ResolvePath(job.DirectoryName.ToString()), String.Concat(job.DmeName, DmbExtension), job))
await using (var controller = await sessionControllerFactory.LaunchNew(provider, byondLock, launchParameters, true, cancellationToken))
{
var launchResult = await controller.LaunchResult.WaitAsync(cancellationToken);
diff --git a/src/Tgstation.Server.Host/Components/Deployment/HardLinkDmbProvider.cs b/src/Tgstation.Server.Host/Components/Deployment/HardLinkDmbProvider.cs
new file mode 100644
index 0000000000..568786e340
--- /dev/null
+++ b/src/Tgstation.Server.Host/Components/Deployment/HardLinkDmbProvider.cs
@@ -0,0 +1,273 @@
+using System;
+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;
+using Tgstation.Server.Host.Utils;
+
+namespace Tgstation.Server.Host.Components.Deployment
+{
+ ///
+ /// A that uses hard links.
+ ///
+ [UnsupportedOSPlatform("windows")]
+ sealed class HardLinkDmbProvider : SwappableDmbProvider
+ {
+ ///
+ /// The for .
+ ///
+ readonly CancellationTokenSource cancellationTokenSource;
+
+ ///
+ /// The representing the base provider mirroring operation.
+ ///
+ readonly Task mirroringTask;
+
+ ///
+ /// The for the .
+ ///
+ readonly ILogger logger;
+
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ /// The for the .
+ /// The for the .
+ /// The for the .
+ /// The value of .
+ /// The for the .
+ public HardLinkDmbProvider(
+ IDmbProvider baseProvider,
+ IIOManager ioManager,
+ IFilesystemLinkFactory linkFactory,
+ ILogger logger,
+ GeneralConfiguration generalConfiguration)
+ : base(
+ baseProvider,
+ ioManager,
+ linkFactory)
+ {
+ this.logger = logger ?? throw new ArgumentNullException(nameof(logger));
+ cancellationTokenSource = new CancellationTokenSource();
+ try
+ {
+ mirroringTask = MirrorSourceDirectory(generalConfiguration.GetCopyDirectoryTaskThrottle(), cancellationTokenSource.Token);
+ }
+ catch
+ {
+ cancellationTokenSource.Dispose();
+ throw;
+ }
+ }
+
+ ///
+ public override async ValueTask DisposeAsync()
+ {
+ cancellationTokenSource.Cancel();
+ cancellationTokenSource.Dispose();
+ try
+ {
+ await mirroringTask;
+ }
+ catch (OperationCanceledException ex)
+ {
+ logger.LogDebug(ex, "Mirroring task cancelled!");
+ }
+
+ await base.DisposeAsync();
+ }
+
+ ///
+ public override Task FinishActivationPreparation(CancellationToken cancellationToken)
+ {
+ if (!mirroringTask.IsCompleted)
+ logger.LogTrace("Waiting for mirroring to complete...");
+
+ return mirroringTask.WaitAsync(cancellationToken);
+ }
+
+ ///
+ protected override async ValueTask DoSwap(CancellationToken cancellationToken)
+ {
+ logger.LogTrace("Begin DoSwap, mirroring task complete: {complete}...", mirroringTask.IsCompleted);
+ var mirroredDir = await mirroringTask.WaitAsync(cancellationToken);
+ var goAheadTcs = new TaskCompletionSource();
+
+ // I feel dirty...
+ async void DisposeOfOldDirectory()
+ {
+ var directoryMoved = false;
+ 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)
+ {
+ logger.LogDebug(ex, "Live directory appears to not exist");
+ if (!directoryMoved)
+ goAheadTcs.SetResult();
+ }
+ catch (Exception ex)
+ {
+ logger.LogWarning(ex, "Failed to delete hard linked directory: {disposePath}", disposePath);
+ if (!directoryMoved)
+ goAheadTcs.SetException(ex);
+ }
+ }
+
+ DisposeOfOldDirectory();
+ await goAheadTcs.Task;
+ logger.LogTrace("Moving mirror directory {path} to Live...", mirroredDir);
+ await IOManager.MoveDirectory(mirroredDir, LiveGameDirectory, cancellationToken);
+ logger.LogTrace("Swap complete!");
+ }
+
+ ///
+ /// Mirror the .
+ ///
+ /// The optional maximum number of simultaneous tasks allowed to execute.
+ /// The for the operation.
+ /// A resulting in the full path to the mirrored directory.
+ async Task MirrorSourceDirectory(int? taskThrottle, CancellationToken cancellationToken)
+ {
+ var stopwatch = Stopwatch.StartNew();
+ var mirrorGuid = Guid.NewGuid();
+ logger.LogDebug("Starting to mirror {sourceDir} as hard links to {mirrorGuid}...", CompileJob.DirectoryName, mirrorGuid);
+ if (taskThrottle.HasValue && taskThrottle < 1)
+ throw new ArgumentOutOfRangeException(nameof(taskThrottle), taskThrottle, "taskThrottle must be at least 1!");
+
+ var src = IOManager.ResolvePath(CompileJob.DirectoryName.ToString());
+ var dest = IOManager.ResolvePath(mirrorGuid.ToString());
+
+ using var semaphore = taskThrottle.HasValue ? new SemaphoreSlim(taskThrottle.Value) : null;
+ await Task.WhenAll(MirrorDirectoryImpl(src, dest, semaphore, cancellationToken));
+ stopwatch.Stop();
+
+ logger.LogDebug(
+ "Finished mirror of {sourceDir} to {mirrorGuid} in {seconds}s...",
+ CompileJob.DirectoryName,
+ mirrorGuid,
+ stopwatch.Elapsed.TotalSeconds.ToString("0.##", CultureInfo.InvariantCulture));
+
+ return dest;
+ }
+
+ ///
+ /// Recursively create tasks to create a hard link directory mirror of to .
+ ///
+ /// The source directory path.
+ /// The destination directory path.
+ /// Optional used to limit degree of parallelism.
+ /// The for the operation.
+ /// A of s representing the running operations. The first returned is always the necessary call to .
+ /// I genuinely don't know how this will work with symlinked files. Waiting for the issue report I guess.
+ IEnumerable MirrorDirectoryImpl(string src, string dest, SemaphoreSlim semaphore, CancellationToken cancellationToken)
+ {
+ 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.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, mirroredName, semaphore, cancellationToken))
+ {
+ if (subdirCreationTask == null)
+ {
+ subdirCreationTask = copyTask;
+ yield return subdirCreationTask;
+ }
+ else if (!checkingSubdirCreationTask)
+ yield return copyTask;
+
+ checkingSubdirCreationTask = false;
+ }
+ }
+
+ foreach (var fileInfo in dir.EnumerateFiles())
+ {
+ if (subdirCreationTask == null)
+ {
+ subdirCreationTask = IOManager.CreateDirectory(dest, cancellationToken);
+ yield return subdirCreationTask;
+ }
+
+ var sourceFile = fileInfo.FullName;
+ var destFile = IOManager.ConcatPath(dest, fileInfo.Name);
+
+ async Task LinkThisFile()
+ {
+ await subdirCreationTask.WaitAsync(cancellationToken);
+ using var lockContext = semaphore != null
+ ? await SemaphoreSlimContext.Lock(semaphore, cancellationToken)
+ : null;
+
+ 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/Deployment/IDmbProvider.cs b/src/Tgstation.Server.Host/Components/Deployment/IDmbProvider.cs
index efc773a919..c1c477ffff 100644
--- a/src/Tgstation.Server.Host/Components/Deployment/IDmbProvider.cs
+++ b/src/Tgstation.Server.Host/Components/Deployment/IDmbProvider.cs
@@ -7,7 +7,7 @@ namespace Tgstation.Server.Host.Components.Deployment
///
/// Provides absolute paths to the latest compiled .dmbs.
///
- public interface IDmbProvider : IDisposable
+ public interface IDmbProvider : IAsyncDisposable
{
///
/// The file name of the .dmb.
diff --git a/src/Tgstation.Server.Host/Components/Deployment/SwappableDmbProvider.cs b/src/Tgstation.Server.Host/Components/Deployment/SwappableDmbProvider.cs
index b3d8505e65..7279297bf5 100644
--- a/src/Tgstation.Server.Host/Components/Deployment/SwappableDmbProvider.cs
+++ b/src/Tgstation.Server.Host/Components/Deployment/SwappableDmbProvider.cs
@@ -8,23 +8,23 @@ using Tgstation.Server.Host.Models;
namespace Tgstation.Server.Host.Components.Deployment
{
///
- /// A that uses symlinks.
+ /// A that uses filesystem links to change directory structure underneath the server process.
///
- sealed class SwappableDmbProvider : IDmbProvider
+ abstract class SwappableDmbProvider : IDmbProvider
{
///
- /// The directory where the is symlinked to.
+ /// The directory where the is symlinked to.
///
public const string LiveGameDirectory = "Live";
///
- public string DmbName => baseProvider.DmbName;
+ public string DmbName => BaseProvider.DmbName;
///
- public string Directory => ioManager.ResolvePath(LiveGameDirectory);
+ public string Directory => IOManager.ResolvePath(LiveGameDirectory);
///
- public CompileJob CompileJob => baseProvider.CompileJob;
+ public CompileJob CompileJob => BaseProvider.CompileJob;
///
/// If has been run.
@@ -34,17 +34,17 @@ namespace Tgstation.Server.Host.Components.Deployment
///
/// The we are swapping for.
///
- readonly IDmbProvider baseProvider;
+ protected IDmbProvider BaseProvider { get; }
///
/// The to use.
///
- readonly IIOManager ioManager;
+ protected IIOManager IOManager { get; }
///
- /// The to use.
+ /// The to use.
///
- readonly ISymlinkFactory symlinkFactory;
+ protected IFilesystemLinkFactory LinkFactory { get; }
///
/// Backing field for .
@@ -54,41 +54,47 @@ namespace Tgstation.Server.Host.Components.Deployment
///
/// Initializes a new instance of the class.
///
- /// The value of .
- /// The value of .
- /// The value of .
- public SwappableDmbProvider(IDmbProvider baseProvider, IIOManager ioManager, ISymlinkFactory symlinkFactory)
+ /// The value of .
+ /// The value of .
+ /// The value of .
+ public SwappableDmbProvider(IDmbProvider baseProvider, IIOManager ioManager, IFilesystemLinkFactory symlinkFactory)
{
- this.baseProvider = baseProvider ?? throw new ArgumentNullException(nameof(baseProvider));
- this.ioManager = ioManager ?? throw new ArgumentNullException(nameof(ioManager));
- this.symlinkFactory = symlinkFactory ?? throw new ArgumentNullException(nameof(symlinkFactory));
+ BaseProvider = baseProvider ?? throw new ArgumentNullException(nameof(baseProvider));
+ IOManager = ioManager ?? throw new ArgumentNullException(nameof(ioManager));
+ LinkFactory = symlinkFactory ?? throw new ArgumentNullException(nameof(symlinkFactory));
}
///
- public void Dispose() => baseProvider.Dispose();
+ public virtual ValueTask DisposeAsync() => BaseProvider.DisposeAsync();
///
- public void KeepAlive() => baseProvider.KeepAlive();
+ public void KeepAlive() => BaseProvider.KeepAlive();
///
/// Make the active by replacing the live link with our .
///
/// The for the operation.
/// A representing the running operation.
- public async ValueTask MakeActive(CancellationToken cancellationToken)
+ public ValueTask MakeActive(CancellationToken cancellationToken)
{
if (Interlocked.Exchange(ref swapped, 1) != 0)
throw new InvalidOperationException("Already swapped!");
- if (symlinkFactory.SymlinkedDirectoriesAreDeletedAsFiles)
- await ioManager.DeleteFile(LiveGameDirectory, cancellationToken);
- else
- await ioManager.DeleteDirectory(LiveGameDirectory, cancellationToken);
-
- await symlinkFactory.CreateSymbolicLink(
- ioManager.ResolvePath(baseProvider.Directory),
- ioManager.ResolvePath(LiveGameDirectory),
- cancellationToken);
+ return DoSwap(cancellationToken);
}
+
+ ///
+ /// Should be . before calling to ensure the is ready to instantly swap. Can be called multiple times.
+ ///
+ /// The for the operation.
+ /// A representing the preparation process.
+ public abstract Task FinishActivationPreparation(CancellationToken cancellationToken);
+
+ ///
+ /// Perform the swapping action.
+ ///
+ /// The for the operation.
+ /// A representing the running operation.
+ protected abstract ValueTask DoSwap(CancellationToken cancellationToken);
}
}
diff --git a/src/Tgstation.Server.Host/Components/Deployment/SymlinkDmbProvider.cs b/src/Tgstation.Server.Host/Components/Deployment/SymlinkDmbProvider.cs
new file mode 100644
index 0000000000..edb570bcb8
--- /dev/null
+++ b/src/Tgstation.Server.Host/Components/Deployment/SymlinkDmbProvider.cs
@@ -0,0 +1,44 @@
+using System.Threading;
+using System.Threading.Tasks;
+
+using Tgstation.Server.Host.IO;
+
+namespace Tgstation.Server.Host.Components.Deployment
+{
+ ///
+ /// A that uses symlinks.
+ ///
+ sealed class SymlinkDmbProvider : SwappableDmbProvider
+ {
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ /// The for the .
+ /// The for the .
+ /// The for the .
+ public SymlinkDmbProvider(
+ IDmbProvider baseProvider,
+ IIOManager ioManager,
+ IFilesystemLinkFactory linkFactory)
+ : base(baseProvider, ioManager, linkFactory)
+ {
+ }
+
+ ///
+ public override Task FinishActivationPreparation(CancellationToken cancellationToken) => Task.CompletedTask;
+
+ ///
+ protected override async ValueTask DoSwap(CancellationToken cancellationToken)
+ {
+ if (LinkFactory.SymlinkedDirectoriesAreDeletedAsFiles)
+ await IOManager.DeleteFile(LiveGameDirectory, cancellationToken);
+ else
+ await IOManager.DeleteDirectory(LiveGameDirectory, cancellationToken);
+
+ await LinkFactory.CreateSymbolicLink(
+ IOManager.ResolvePath(BaseProvider.Directory),
+ IOManager.ResolvePath(LiveGameDirectory),
+ cancellationToken);
+ }
+ }
+}
diff --git a/src/Tgstation.Server.Host/Components/Deployment/TemporaryDmbProvider.cs b/src/Tgstation.Server.Host/Components/Deployment/TemporaryDmbProvider.cs
index 0bf982cfa3..4476d9d2c3 100644
--- a/src/Tgstation.Server.Host/Components/Deployment/TemporaryDmbProvider.cs
+++ b/src/Tgstation.Server.Host/Components/Deployment/TemporaryDmbProvider.cs
@@ -1,4 +1,5 @@
using System;
+using System.Threading.Tasks;
using Tgstation.Server.Host.Models;
@@ -32,9 +33,7 @@ namespace Tgstation.Server.Host.Components.Deployment
}
///
- public void Dispose()
- {
- }
+ public ValueTask DisposeAsync() => ValueTask.CompletedTask;
///
public void KeepAlive() => throw new NotSupportedException();
diff --git a/src/Tgstation.Server.Host/Components/InstanceFactory.cs b/src/Tgstation.Server.Host/Components/InstanceFactory.cs
index 6d9d7a8b61..b3817ab86c 100644
--- a/src/Tgstation.Server.Host/Components/InstanceFactory.cs
+++ b/src/Tgstation.Server.Host/Components/InstanceFactory.cs
@@ -66,9 +66,9 @@ namespace Tgstation.Server.Host.Components
readonly ISynchronousIOManager synchronousIOManager;
///
- /// The for the .
+ /// The for the .
///
- readonly ISymlinkFactory symlinkFactory;
+ readonly IFilesystemLinkFactory linkFactory;
///
/// The for the .
@@ -173,7 +173,7 @@ namespace Tgstation.Server.Host.Components
/// The value of .
/// The value of .
/// The value of .
- /// The value of .
+ /// The value of .
/// The value of .
/// The value of .
/// The value of .
@@ -199,7 +199,7 @@ namespace Tgstation.Server.Host.Components
ITopicClientFactory topicClientFactory,
ICryptographySuite cryptographySuite,
ISynchronousIOManager synchronousIOManager,
- ISymlinkFactory symlinkFactory,
+ IFilesystemLinkFactory linkFactory,
IByondInstaller byondInstaller,
IChatManagerFactory chatFactory,
IProcessExecutor processExecutor,
@@ -225,7 +225,7 @@ namespace Tgstation.Server.Host.Components
this.topicClientFactory = topicClientFactory ?? throw new ArgumentNullException(nameof(topicClientFactory));
this.cryptographySuite = cryptographySuite ?? throw new ArgumentNullException(nameof(cryptographySuite));
this.synchronousIOManager = synchronousIOManager ?? throw new ArgumentNullException(nameof(synchronousIOManager));
- this.symlinkFactory = symlinkFactory ?? throw new ArgumentNullException(nameof(symlinkFactory));
+ this.linkFactory = linkFactory ?? throw new ArgumentNullException(nameof(linkFactory));
this.byondInstaller = byondInstaller ?? throw new ArgumentNullException(nameof(byondInstaller));
this.chatFactory = chatFactory ?? throw new ArgumentNullException(nameof(chatFactory));
this.processExecutor = processExecutor ?? throw new ArgumentNullException(nameof(processExecutor));
@@ -275,7 +275,7 @@ namespace Tgstation.Server.Host.Components
var configuration = new StaticFiles.Configuration(
configurationIoManager,
synchronousIOManager,
- symlinkFactory,
+ linkFactory,
processExecutor,
postWriteHandler,
platformIdentifier,
diff --git a/src/Tgstation.Server.Host/Components/Interop/Bridge/RuntimeInformation.cs b/src/Tgstation.Server.Host/Components/Interop/Bridge/RuntimeInformation.cs
index 5d0d13b7fa..b7d05b67d7 100644
--- a/src/Tgstation.Server.Host/Components/Interop/Bridge/RuntimeInformation.cs
+++ b/src/Tgstation.Server.Host/Components/Interop/Bridge/RuntimeInformation.cs
@@ -2,6 +2,8 @@
using System.Collections.Generic;
using System.Linq;
+using Newtonsoft.Json;
+
using Tgstation.Server.Api.Models;
using Tgstation.Server.Host.Components.Chat;
using Tgstation.Server.Host.Components.Deployment;
@@ -42,12 +44,14 @@ namespace Tgstation.Server.Host.Components.Interop.Bridge
///
/// The level of the launch.
///
- public DreamDaemonSecurity? SecurityLevel { get; }
+ [JsonProperty(DefaultValueHandling = DefaultValueHandling.Include)]
+ public DreamDaemonSecurity SecurityLevel { get; }
///
/// The level of the launch.
///
- public DreamDaemonVisibility? Visibility { get; }
+ [JsonProperty(DefaultValueHandling = DefaultValueHandling.Include)]
+ public DreamDaemonVisibility Visibility { get; }
///
/// The s in the launch.
@@ -70,8 +74,8 @@ namespace Tgstation.Server.Host.Components.Interop.Bridge
IDmbProvider dmbProvider,
Version serverVersion,
string instanceName,
- DreamDaemonSecurity? securityLevel,
- DreamDaemonVisibility? visibility,
+ DreamDaemonSecurity securityLevel,
+ DreamDaemonVisibility visibility,
ushort serverPort,
bool apiValidateOnly)
: base(chatTrackingContext?.Channels ?? throw new ArgumentNullException(nameof(chatTrackingContext)))
diff --git a/src/Tgstation.Server.Host/Components/Session/ISessionController.cs b/src/Tgstation.Server.Host/Components/Session/ISessionController.cs
index a42186c66b..72e66c2066 100644
--- a/src/Tgstation.Server.Host/Components/Session/ISessionController.cs
+++ b/src/Tgstation.Server.Host/Components/Session/ISessionController.cs
@@ -128,7 +128,7 @@ namespace Tgstation.Server.Host.Components.Session
/// Replace the in use with a given , disposing the old one.
///
/// The new .
- /// An to be disposed once certain that the original is no longer in use.
- IDisposable ReplaceDmbProvider(IDmbProvider newProvider);
+ /// An to be disposed once certain that the original is no longer in use.
+ IAsyncDisposable ReplaceDmbProvider(IDmbProvider newProvider);
}
}
diff --git a/src/Tgstation.Server.Host/Components/Session/ReattachInformation.cs b/src/Tgstation.Server.Host/Components/Session/ReattachInformation.cs
index 2c9007f4f3..6c429d2d0a 100644
--- a/src/Tgstation.Server.Host/Components/Session/ReattachInformation.cs
+++ b/src/Tgstation.Server.Host/Components/Session/ReattachInformation.cs
@@ -76,13 +76,11 @@ namespace Tgstation.Server.Host.Components.Session
Dmb = dmb ?? throw new ArgumentNullException(nameof(dmb));
ProcessId = process?.Id ?? throw new ArgumentNullException(nameof(process));
RuntimeInformation = runtimeInformation ?? throw new ArgumentNullException(nameof(runtimeInformation));
- if (!runtimeInformation.SecurityLevel.HasValue)
- throw new ArgumentException("runtimeInformation must have a valid SecurityLevel!", nameof(runtimeInformation));
AccessIdentifier = accessIdentifier ?? throw new ArgumentNullException(nameof(accessIdentifier));
- LaunchSecurityLevel = runtimeInformation.SecurityLevel.Value;
- LaunchVisibility = runtimeInformation.Visibility.Value;
+ LaunchSecurityLevel = runtimeInformation.SecurityLevel;
+ LaunchVisibility = runtimeInformation.Visibility;
Port = port;
runtimeInformationLock = new object();
diff --git a/src/Tgstation.Server.Host/Components/Session/SessionController.cs b/src/Tgstation.Server.Host/Components/Session/SessionController.cs
index d3749f0e9f..78160b352d 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.
///
@@ -192,6 +197,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().
///
@@ -239,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,
@@ -271,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;
@@ -291,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);
@@ -307,6 +319,9 @@ namespace Tgstation.Server.Host.Components.Session
{
var exitCode = await process.Lifetime;
await postLifetimeCallback();
+ if (postValidationShutdownTask != null)
+ await postValidationShutdownTask;
+
return exitCode;
}
@@ -350,8 +365,13 @@ namespace Tgstation.Server.Host.Components.Session
await process.DisposeAsync();
byondLock.Dispose();
bridgeRegistration?.Dispose();
- ReattachInformation.Dmb.Dispose();
- ReattachInformation.InitialDmb?.Dispose();
+ var regularDmbDisposeTask = ReattachInformation.Dmb.DisposeAsync();
+ var initialDmb = ReattachInformation.InitialDmb;
+ if (initialDmb != null)
+ await initialDmb.DisposeAsync();
+
+ await regularDmbDisposeTask;
+
chatTrackingContext.Dispose();
reattachTopicCts.Dispose();
@@ -552,7 +572,7 @@ namespace Tgstation.Server.Host.Components.Session
public void Resume() => process.Resume();
///
- public IDisposable ReplaceDmbProvider(IDmbProvider dmbProvider)
+ public IAsyncDisposable ReplaceDmbProvider(IDmbProvider dmbProvider)
{
var oldDmb = ReattachInformation.Dmb;
ReattachInformation.Dmb = dmbProvider ?? throw new ArgumentNullException(nameof(dmbProvider));
@@ -655,12 +675,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 ValueTask ProcessBridgeCommand(BridgeParameters parameters, CancellationToken cancellationToken)
{
var response = new BridgeResponse();
@@ -735,6 +783,22 @@ 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();
+ var firstValidationRequest = Interlocked.CompareExchange(ref postValidationShutdownTask, PostValidationShutdown(proceedTcs.Task), null) == null;
+ proceedTcs.SetResult(firstValidationRequest);
+
+ if (!firstValidationRequest)
+ return BridgeError("Startup bridge request was repeated!");
+ }
+#else
+ postValidationShutdownTask = Task.CompletedTask;
+#endif
+
if (parameters.Version == null)
return BridgeError("Missing dmApiVersion field!");
@@ -808,6 +872,7 @@ namespace Tgstation.Server.Host.Components.Session
return response;
}
+#pragma warning restore CA1502
///
/// Log and return a for a given .
diff --git a/src/Tgstation.Server.Host/Components/Session/SessionControllerFactory.cs b/src/Tgstation.Server.Host/Components/Session/SessionControllerFactory.cs
index 3bc79939c7..6d48b990fe 100644
--- a/src/Tgstation.Server.Host/Components/Session/SessionControllerFactory.cs
+++ b/src/Tgstation.Server.Host/Components/Session/SessionControllerFactory.cs
@@ -337,7 +337,8 @@ namespace Tgstation.Server.Host.Components.Session
var runtimeInformation = CreateRuntimeInformation(
dmbProvider,
chatTrackingContext,
- launchParameters,
+ launchParameters.SecurityLevel.Value,
+ launchParameters.Visibility.Value,
apiValidate);
var reattachInformation = new ReattachInformation(
@@ -432,7 +433,8 @@ namespace Tgstation.Server.Host.Components.Session
var runtimeInformation = CreateRuntimeInformation(
reattachInformation.Dmb,
chatTrackingContext,
- null,
+ reattachInformation.LaunchSecurityLevel,
+ reattachInformation.LaunchVisibility,
false);
reattachInformation.SetRuntimeInformation(runtimeInformation);
@@ -628,21 +630,23 @@ namespace Tgstation.Server.Host.Components.Session
///
/// The .
/// The .
- /// The if any.
+ /// The the server was launched with.
+ /// The the server was launched with.
/// The value of .
/// A new class.
RuntimeInformation CreateRuntimeInformation(
IDmbProvider dmbProvider,
IChatTrackingContext chatTrackingContext,
- DreamDaemonLaunchParameters launchParameters,
+ DreamDaemonSecurity securityLevel,
+ DreamDaemonVisibility visibility,
bool apiValidateOnly)
=> new (
chatTrackingContext,
dmbProvider,
assemblyInformationProvider.Version,
instance.Name,
- launchParameters?.SecurityLevel,
- launchParameters?.Visibility,
+ securityLevel,
+ visibility,
serverPortProvider.HttpApiPort,
apiValidateOnly);
diff --git a/src/Tgstation.Server.Host/Components/StaticFiles/Configuration.cs b/src/Tgstation.Server.Host/Components/StaticFiles/Configuration.cs
index 2b19803126..380e0e02ea 100644
--- a/src/Tgstation.Server.Host/Components/StaticFiles/Configuration.cs
+++ b/src/Tgstation.Server.Host/Components/StaticFiles/Configuration.cs
@@ -95,9 +95,9 @@ namespace Tgstation.Server.Host.Components.StaticFiles
readonly ISynchronousIOManager synchronousIOManager;
///
- /// The for .
+ /// The for .
///
- readonly ISymlinkFactory symlinkFactory;
+ readonly IFilesystemLinkFactory linkFactory;
///
/// The for .
@@ -154,7 +154,7 @@ namespace Tgstation.Server.Host.Components.StaticFiles
///
/// The value of .
/// The value of .
- /// The value of .
+ /// The value of .
/// The value of .
/// The value of .
/// The value of .
@@ -165,7 +165,7 @@ namespace Tgstation.Server.Host.Components.StaticFiles
public Configuration(
IIOManager ioManager,
ISynchronousIOManager synchronousIOManager,
- ISymlinkFactory symlinkFactory,
+ IFilesystemLinkFactory linkFactory,
IProcessExecutor processExecutor,
IPostWriteHandler postWriteHandler,
IPlatformIdentifier platformIdentifier,
@@ -176,7 +176,7 @@ namespace Tgstation.Server.Host.Components.StaticFiles
{
this.ioManager = ioManager ?? throw new ArgumentNullException(nameof(ioManager));
this.synchronousIOManager = synchronousIOManager ?? throw new ArgumentNullException(nameof(synchronousIOManager));
- this.symlinkFactory = symlinkFactory ?? throw new ArgumentNullException(nameof(symlinkFactory));
+ this.linkFactory = linkFactory ?? throw new ArgumentNullException(nameof(linkFactory));
this.processExecutor = processExecutor ?? throw new ArgumentNullException(nameof(processExecutor));
this.postWriteHandler = postWriteHandler ?? throw new ArgumentNullException(nameof(postWriteHandler));
this.platformIdentifier = platformIdentifier ?? throw new ArgumentNullException(nameof(platformIdentifier));
@@ -432,7 +432,7 @@ namespace Tgstation.Server.Host.Components.StaticFiles
var fileExists = await fileExistsTask;
if (fileExists)
await ioManager.DeleteFile(destPath, cancellationToken);
- await symlinkFactory.CreateSymbolicLink(ioManager.ResolvePath(file), ioManager.ResolvePath(destPath), cancellationToken);
+ await linkFactory.CreateSymbolicLink(ioManager.ResolvePath(file), ioManager.ResolvePath(destPath), cancellationToken);
}));
}
diff --git a/src/Tgstation.Server.Host/Components/Watchdog/AdvancedWatchdog.cs b/src/Tgstation.Server.Host/Components/Watchdog/AdvancedWatchdog.cs
new file mode 100644
index 0000000000..35d3e508e7
--- /dev/null
+++ b/src/Tgstation.Server.Host/Components/Watchdog/AdvancedWatchdog.cs
@@ -0,0 +1,424 @@
+using System;
+using System.Collections.Generic;
+using System.Diagnostics;
+using System.Threading;
+using System.Threading.Tasks;
+
+using Microsoft.Extensions.Logging;
+
+using Tgstation.Server.Api.Models.Internal;
+using Tgstation.Server.Host.Components.Chat;
+using Tgstation.Server.Host.Components.Deployment;
+using Tgstation.Server.Host.Components.Deployment.Remote;
+using Tgstation.Server.Host.Components.Events;
+using Tgstation.Server.Host.Components.Session;
+using Tgstation.Server.Host.Core;
+using Tgstation.Server.Host.IO;
+using Tgstation.Server.Host.Jobs;
+using Tgstation.Server.Host.Utils;
+
+namespace Tgstation.Server.Host.Components.Watchdog
+{
+ ///
+ /// A that, instead of killing servers for updates, uses the wonders of filesystem links to swap out changes without killing the server process.
+ ///
+ abstract class AdvancedWatchdog : BasicWatchdog
+ {
+ ///
+ /// The for .
+ ///
+ protected SwappableDmbProvider ActiveSwappable { get; private set; }
+
+ ///
+ /// The for the pointing to the Game directory.
+ ///
+ protected IIOManager GameIOManager { get; }
+
+ ///
+ /// The for the .
+ ///
+ protected IFilesystemLinkFactory LinkFactory { get; }
+
+ ///
+ /// of s that are waiting to clean up old deployments.
+ ///
+ readonly List deploymentCleanupTasks;
+
+ ///
+ /// The active for .
+ ///
+ SwappableDmbProvider pendingSwappable;
+
+ ///
+ /// The representing the cleanup of an unused .
+ ///
+ volatile TaskCompletionSource deploymentCleanupGate;
+
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ /// The for the .
+ /// The for the .
+ /// The for the .
+ /// The for the .
+ /// The for the .
+ /// The for the .
+ /// The for the .
+ /// The for the .
+ /// The for the .
+ /// The for the .
+ /// The value of .
+ /// The value of .
+ /// The for the .
+ /// The for the .
+ /// The for the .
+ /// The autostart value for the .
+ public AdvancedWatchdog(
+ IChatManager chat,
+ ISessionControllerFactory sessionControllerFactory,
+ IDmbFactory dmbFactory,
+ ISessionPersistor sessionPersistor,
+ IJobManager jobManager,
+ IServerControl serverControl,
+ IAsyncDelayer asyncDelayer,
+ IIOManager diagnosticsIOManager,
+ IEventConsumer eventConsumer,
+ IRemoteDeploymentManagerFactory remoteDeploymentManagerFactory,
+ IIOManager gameIOManager,
+ IFilesystemLinkFactory linkFactory,
+ ILogger logger,
+ DreamDaemonLaunchParameters initialLaunchParameters,
+ Api.Models.Instance instance,
+ bool autoStart)
+ : base(
+ chat,
+ sessionControllerFactory,
+ dmbFactory,
+ sessionPersistor,
+ jobManager,
+ serverControl,
+ asyncDelayer,
+ diagnosticsIOManager,
+ eventConsumer,
+ remoteDeploymentManagerFactory,
+ logger,
+ initialLaunchParameters,
+ instance,
+ autoStart)
+ {
+ try
+ {
+ GameIOManager = gameIOManager ?? throw new ArgumentNullException(nameof(gameIOManager));
+ LinkFactory = linkFactory ?? throw new ArgumentNullException(nameof(linkFactory));
+
+ deploymentCleanupTasks = new List();
+ }
+ catch
+ {
+ // Async dispose is for if we have controllers running, not the case here
+ var disposeTask = DisposeAsync();
+ Debug.Assert(disposeTask.IsCompleted, "This should always be true during construction!");
+ disposeTask.GetAwaiter().GetResult();
+
+ throw;
+ }
+ }
+
+ ///
+ protected sealed override async ValueTask DisposeAndNullControllersImpl()
+ {
+ await base.DisposeAndNullControllersImpl();
+
+ // If we reach this point, we can guarantee PrepServerForLaunch will be called before starting again.
+ ActiveSwappable = null;
+ await (pendingSwappable?.DisposeAsync() ?? ValueTask.CompletedTask);
+ pendingSwappable = null;
+
+ await DrainDeploymentCleanupTasks(true);
+ }
+
+ ///
+ protected sealed override async ValueTask HandleNormalReboot(CancellationToken cancellationToken)
+ {
+ if (pendingSwappable != null)
+ {
+ ValueTask RunPrequel() => BeforeApplyDmb(pendingSwappable.CompileJob, cancellationToken);
+
+ var needToSwap = !pendingSwappable.Swapped;
+ if (needToSwap)
+ {
+ // IMPORTANT: THE SESSIONCONTROLLER SHOULD STILL BE PROCESSING THE BRIDGE REQUEST SO WE KNOW DD IS SLEEPING
+ // OTHERWISE, IT COULD RETURN TO /world/Reboot() TOO EARLY AND LOAD THE WRONG .DMB
+ if (!Server.ProcessingRebootBridgeRequest)
+ {
+ // integration test logging will catch this
+ Logger.LogError(
+ "The reboot bridge request completed before the watchdog could suspend the server! This can lead to buggy DreamDaemon behaviour and should be reported! To ensure stability, we will need to hard reboot the server");
+ await RunPrequel();
+ return MonitorAction.Restart;
+ }
+
+ // DCT: Not necessary
+ if (!pendingSwappable.FinishActivationPreparation(CancellationToken.None).IsCompleted)
+ {
+ // rare pokemon
+ Logger.LogInformation("Deployed .dme is not ready to swap, delaying until next reboot!");
+ Chat.QueueWatchdogMessage("The pending deployment was not ready to be activated this reboot. It will be applied at the next one.");
+ return MonitorAction.Continue;
+ }
+ }
+
+ var updateTask = RunPrequel();
+ if (needToSwap)
+ await PerformDmbSwap(pendingSwappable, cancellationToken);
+
+ var currentCompileJobId = Server.ReattachInformation.Dmb.CompileJob.Id;
+
+ await DrainDeploymentCleanupTasks(false);
+
+ IAsyncDisposable lingeringDeployment;
+ var localDeploymentCleanupGate = new TaskCompletionSource();
+ async Task CleanupLingeringDeployment()
+ {
+ var lingeringDeploymentExpirySeconds = ActiveLaunchParameters.StartupTimeout.Value;
+ Logger.LogDebug(
+ "Holding old deployment {compileJobId} for up to {expiry} seconds...",
+ currentCompileJobId,
+ lingeringDeploymentExpirySeconds);
+
+ var timeout = AsyncDelayer.Delay(TimeSpan.FromSeconds(lingeringDeploymentExpirySeconds), cancellationToken);
+
+ var completedTask = await Task.WhenAny(
+ localDeploymentCleanupGate.Task,
+ timeout);
+
+ var timedOut = completedTask == timeout;
+ Logger.Log(
+ timedOut
+ ? LogLevel.Warning
+ : LogLevel.Trace,
+ "Releasing old deployment {compileJobId}{afterTimeout}",
+ currentCompileJobId,
+ timedOut
+ ? " due to timeout!"
+ : "...");
+
+ await lingeringDeployment.DisposeAsync();
+ }
+
+ var oldDeploymentCleanupGate = Interlocked.Exchange(ref deploymentCleanupGate, localDeploymentCleanupGate);
+ oldDeploymentCleanupGate?.TrySetResult();
+
+ Logger.LogTrace("Replacing activeSwappable with pendingSwappable...");
+
+ lock (deploymentCleanupTasks)
+ {
+ lingeringDeployment = Server.ReplaceDmbProvider(pendingSwappable);
+ deploymentCleanupTasks.Add(
+ CleanupLingeringDeployment());
+ }
+
+ ActiveSwappable = pendingSwappable;
+ pendingSwappable = null;
+
+ await SessionPersistor.Save(Server.ReattachInformation, cancellationToken);
+ await updateTask;
+ }
+ else
+ Logger.LogTrace("Nothing to do as pendingSwappable is null.");
+
+ return MonitorAction.Continue;
+ }
+
+ ///
+ protected sealed override async ValueTask HandleNewDmbAvailable(CancellationToken cancellationToken)
+ {
+ IDmbProvider compileJobProvider = DmbFactory.LockNextDmb(1);
+ bool canSeamlesslySwap = true;
+
+ if (compileJobProvider.CompileJob.ByondVersion != ActiveCompileJob.ByondVersion)
+ {
+ // have to do a graceful restart
+ Logger.LogDebug(
+ "Not swapping to new compile job {0} as it uses a different BYOND version ({1}) than what is currently active {2}. Queueing graceful restart instead...",
+ compileJobProvider.CompileJob.Id,
+ compileJobProvider.CompileJob.ByondVersion,
+ ActiveCompileJob.ByondVersion);
+ canSeamlesslySwap = false;
+ }
+
+ if (compileJobProvider.CompileJob.DmeName != ActiveCompileJob.DmeName)
+ {
+ Logger.LogDebug(
+ "Not swapping to new compile job {0} as it uses a different .dmb name ({1}) than what is currently active {2}. Queueing graceful restart instead...",
+ compileJobProvider.CompileJob.Id,
+ compileJobProvider.CompileJob.DmeName,
+ ActiveCompileJob.DmeName);
+ canSeamlesslySwap = false;
+ }
+
+ if (!canSeamlesslySwap)
+ {
+ await compileJobProvider.DisposeAsync();
+ await base.HandleNewDmbAvailable(cancellationToken);
+ return;
+ }
+
+ SwappableDmbProvider swappableProvider = null;
+ try
+ {
+ swappableProvider = CreateSwappableDmbProvider(compileJobProvider);
+ if (ActiveCompileJob.DMApiVersion == null)
+ {
+ Logger.LogWarning("Active compile job has no DMAPI! Commencing immediate .dmb swap. Note this behavior is known to be buggy in some DM code contexts. See https://github.com/tgstation/tgstation-server/issues/1550");
+ await PerformDmbSwap(swappableProvider, cancellationToken);
+ }
+ }
+ catch (Exception ex)
+ {
+ Logger.LogError(ex, "Exception while swapping");
+ IDmbProvider providerToDispose = swappableProvider ?? compileJobProvider;
+ await providerToDispose.DisposeAsync();
+ throw;
+ }
+
+ await (pendingSwappable?.DisposeAsync() ?? ValueTask.CompletedTask);
+ pendingSwappable = swappableProvider;
+ }
+
+ ///
+ protected sealed override async ValueTask PrepServerForLaunch(IDmbProvider dmbToUse, CancellationToken cancellationToken)
+ {
+ if (ActiveSwappable != null)
+ throw new InvalidOperationException("Expected activeSwappable to be null!");
+ if (pendingSwappable != null)
+ throw new InvalidOperationException("Expected pendingSwappable to be null!");
+
+ Logger.LogTrace("Prep for server launch");
+
+ ActiveSwappable = CreateSwappableDmbProvider(dmbToUse);
+ try
+ {
+ await InitialLink(cancellationToken);
+ }
+ catch (Exception ex)
+ {
+ // We won't worry about disposing activeSwappable here as we can't dispose dmbToUse here.
+ Logger.LogTrace(ex, "Initial link error, nulling ActiveSwappable");
+ ActiveSwappable = null;
+ throw;
+ }
+
+ return ActiveSwappable;
+ }
+
+ ///
+ /// Set the for the .
+ ///
+ /// The for the operation.
+ /// A representing the running operation.
+ protected abstract ValueTask ApplyInitialDmb(CancellationToken cancellationToken);
+
+ ///
+ /// Create a for a given .
+ ///
+ /// The to create a for.
+ /// A new .
+ protected abstract SwappableDmbProvider CreateSwappableDmbProvider(IDmbProvider dmbProvider);
+
+ ///
+ protected override async ValueTask SessionStartupPersist(CancellationToken cancellationToken)
+ {
+ await ApplyInitialDmb(cancellationToken);
+ await base.SessionStartupPersist(cancellationToken);
+ }
+
+ ///
+ protected override async ValueTask HandleMonitorWakeup(MonitorActivationReason reason, CancellationToken cancellationToken)
+ {
+ var result = await base.HandleMonitorWakeup(reason, cancellationToken);
+ if (reason == MonitorActivationReason.ActiveServerStartup)
+ await DrainDeploymentCleanupTasks(false);
+
+ return result;
+ }
+
+ ///
+ /// Create the initial link to the live game directory using .
+ ///
+ /// The for the operation.
+ /// A representing the running operation.
+ async ValueTask InitialLink(CancellationToken cancellationToken)
+ {
+ await ActiveSwappable.FinishActivationPreparation(cancellationToken);
+ Logger.LogTrace("Linking compile job...");
+ await ActiveSwappable.MakeActive(cancellationToken);
+ }
+
+ ///
+ /// Suspends the and calls on a .
+ ///
+ /// The to activate.
+ /// The for the operation.
+ /// A representing the running operation.
+ async ValueTask PerformDmbSwap(SwappableDmbProvider newProvider, CancellationToken cancellationToken)
+ {
+ Logger.LogDebug("Swapping to compile job {id}...", newProvider.CompileJob.Id);
+
+ await newProvider.FinishActivationPreparation(cancellationToken);
+
+ var suspended = false;
+ var server = Server;
+ try
+ {
+ server.Suspend();
+ suspended = true;
+ }
+ catch (Exception ex)
+ {
+ Logger.LogWarning(ex, "Exception while suspending server!");
+ }
+
+ try
+ {
+ await newProvider.MakeActive(cancellationToken);
+ }
+ finally
+ {
+ // Let this throw hard if it fails
+ if (suspended)
+ server.Resume();
+ }
+ }
+
+ ///
+ /// Asynchronously drain .
+ ///
+ /// If , all s will be ed. Otherwise, only s with set will be ed.
+ /// A representing the running operation.
+ Task DrainDeploymentCleanupTasks(bool blocking)
+ {
+ Logger.LogTrace("DrainDeploymentCleanupTasks...");
+ var localDeploymentCleanupGate = Interlocked.Exchange(ref deploymentCleanupGate, null);
+ localDeploymentCleanupGate?.TrySetResult();
+
+ List localDeploymentCleanupTasks;
+ lock (deploymentCleanupTasks)
+ {
+ var totalActiveTasks = deploymentCleanupTasks.Count;
+ localDeploymentCleanupTasks = new List(totalActiveTasks);
+ for (var i = totalActiveTasks - 1; i >= 0; --i)
+ {
+ var currentTask = deploymentCleanupTasks[i];
+ if (!blocking && !currentTask.IsCompleted)
+ continue;
+
+ localDeploymentCleanupTasks.Add(currentTask);
+ deploymentCleanupTasks.RemoveAt(i);
+ }
+ }
+
+ return Task.WhenAll(localDeploymentCleanupTasks);
+ }
+ }
+}
diff --git a/src/Tgstation.Server.Host/Components/Watchdog/BasicWatchdog.cs b/src/Tgstation.Server.Host/Components/Watchdog/BasicWatchdog.cs
index fa9159527d..815f87fa9b 100644
--- a/src/Tgstation.Server.Host/Components/Watchdog/BasicWatchdog.cs
+++ b/src/Tgstation.Server.Host/Components/Watchdog/BasicWatchdog.cs
@@ -268,7 +268,7 @@ namespace Tgstation.Server.Host.Components.Watchdog
// server didn't get control of this dmb
if (dmbToUse != null && !serverWasActive)
- dmbToUse.Dispose();
+ await dmbToUse.DisposeAsync();
throw;
}
diff --git a/src/Tgstation.Server.Host/Components/Watchdog/PosixWatchdog.cs b/src/Tgstation.Server.Host/Components/Watchdog/PosixWatchdog.cs
index 841f903677..27874178af 100644
--- a/src/Tgstation.Server.Host/Components/Watchdog/PosixWatchdog.cs
+++ b/src/Tgstation.Server.Host/Components/Watchdog/PosixWatchdog.cs
@@ -1,4 +1,6 @@
-using System.Threading;
+using System;
+using System.Runtime.Versioning;
+using System.Threading;
using System.Threading.Tasks;
using Microsoft.Extensions.Logging;
@@ -9,6 +11,7 @@ using Tgstation.Server.Host.Components.Deployment;
using Tgstation.Server.Host.Components.Deployment.Remote;
using Tgstation.Server.Host.Components.Events;
using Tgstation.Server.Host.Components.Session;
+using Tgstation.Server.Host.Configuration;
using Tgstation.Server.Host.Core;
using Tgstation.Server.Host.IO;
using Tgstation.Server.Host.Jobs;
@@ -17,10 +20,16 @@ using Tgstation.Server.Host.Utils;
namespace Tgstation.Server.Host.Components.Watchdog
{
///
- /// A variant of the that works on POSIX systems.
+ /// A variant of the that works on POSIX systems.
///
- sealed class PosixWatchdog : WindowsWatchdog
+ [UnsupportedOSPlatform("windows")]
+ sealed class PosixWatchdog : AdvancedWatchdog
{
+ ///
+ /// The for the .
+ ///
+ readonly GeneralConfiguration generalConfiguration;
+
///
/// Initializes a new instance of the class.
///
@@ -34,11 +43,12 @@ namespace Tgstation.Server.Host.Components.Watchdog
/// The for the .
/// The for the .
/// The for the .
- /// The pointing to the game directory for the ..
- /// The for the .
+ /// The pointing to the game directory for the ..
+ /// The for the .
/// The for the .
/// The for the .
/// The for the .
+ /// The value of .
/// The autostart value for the .
public PosixWatchdog(
IChatManager chat,
@@ -52,10 +62,11 @@ namespace Tgstation.Server.Host.Components.Watchdog
IEventConsumer eventConsumer,
IRemoteDeploymentManagerFactory remoteDeploymentManagerFactory,
IIOManager gameIOManager,
- ISymlinkFactory symlinkFactory,
+ IFilesystemLinkFactory linkFactory,
ILogger logger,
DreamDaemonLaunchParameters initialLaunchParameters,
Api.Models.Instance instance,
+ GeneralConfiguration generalConfiguration,
bool autoStart)
: base(
chat,
@@ -69,19 +80,21 @@ namespace Tgstation.Server.Host.Components.Watchdog
eventConsumer,
remoteDeploymentManagerFactory,
gameIOManager,
- symlinkFactory,
+ linkFactory,
logger,
initialLaunchParameters,
instance,
autoStart)
{
+ this.generalConfiguration = generalConfiguration ?? throw new ArgumentNullException(nameof(generalConfiguration));
}
///
protected override ValueTask ApplyInitialDmb(CancellationToken cancellationToken)
- {
- // not necessary to hold initial .dmb on Linux because of based inode deletes
- return ValueTask.CompletedTask;
- }
+ => ValueTask.CompletedTask; // not necessary to hold initial .dmb on Linux because of based inode deletes
+
+ ///
+ protected override SwappableDmbProvider CreateSwappableDmbProvider(IDmbProvider dmbProvider)
+ => new HardLinkDmbProvider(dmbProvider, GameIOManager, LinkFactory, Logger, generalConfiguration);
}
}
diff --git a/src/Tgstation.Server.Host/Components/Watchdog/PosixWatchdogFactory.cs b/src/Tgstation.Server.Host/Components/Watchdog/PosixWatchdogFactory.cs
index 6774c73cb5..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
{
///
@@ -29,21 +31,21 @@ namespace Tgstation.Server.Host.Components.Watchdog
/// The for the .
/// The for the .
/// The for the .
- /// The for the .
+ /// The for the .
/// The for for the .
public PosixWatchdogFactory(
IServerControl serverControl,
ILoggerFactory loggerFactory,
IJobManager jobManager,
IAsyncDelayer asyncDelayer,
- ISymlinkFactory symlinkFactory,
+ IFilesystemLinkFactory linkFactory,
IOptions generalConfigurationOptions)
: base(
serverControl,
loggerFactory,
jobManager,
asyncDelayer,
- symlinkFactory,
+ linkFactory,
generalConfigurationOptions)
{
}
@@ -72,10 +74,11 @@ namespace Tgstation.Server.Host.Components.Watchdog
eventConsumer,
remoteDeploymentManagerFactory,
gameIOManager,
- SymlinkFactory,
+ LinkFactory,
LoggerFactory.CreateLogger(),
settings,
instance,
+ GeneralConfiguration,
settings.AutoStart ?? throw new ArgumentNullException(nameof(settings)));
}
}
diff --git a/src/Tgstation.Server.Host/Components/Watchdog/WindowsWatchdog.cs b/src/Tgstation.Server.Host/Components/Watchdog/WindowsWatchdog.cs
index cdc5307352..8180630ea4 100644
--- a/src/Tgstation.Server.Host/Components/Watchdog/WindowsWatchdog.cs
+++ b/src/Tgstation.Server.Host/Components/Watchdog/WindowsWatchdog.cs
@@ -1,7 +1,4 @@
-using System;
-using System.Collections.Generic;
-using System.Diagnostics;
-using System.Threading;
+using System.Threading;
using System.Threading.Tasks;
using Microsoft.Extensions.Logging;
@@ -20,40 +17,10 @@ using Tgstation.Server.Host.Utils;
namespace Tgstation.Server.Host.Components.Watchdog
{
///
- /// A that, instead of killing servers for updates, uses the wonders of symlinks to swap out changes without killing DreamDaemon.
+ /// A variant of the that works on Windows systems.
///
- class WindowsWatchdog : BasicWatchdog
+ sealed class WindowsWatchdog : AdvancedWatchdog
{
- ///
- /// The for .
- ///
- protected SwappableDmbProvider ActiveSwappable { get; private set; }
-
- ///
- /// The for the pointing to the Game directory.
- ///
- protected IIOManager GameIOManager { get; }
-
- ///
- /// The for the .
- ///
- readonly ISymlinkFactory symlinkFactory;
-
- ///
- /// of s that are waiting to clean up old deployments.
- ///
- readonly List deploymentCleanupTasks;
-
- ///
- /// The active for .
- ///
- SwappableDmbProvider pendingSwappable;
-
- ///
- /// The representing the cleanup of an unused .
- ///
- volatile TaskCompletionSource deploymentCleanupGate;
-
///
/// Initializes a new instance of the class.
///
@@ -67,8 +34,8 @@ namespace Tgstation.Server.Host.Components.Watchdog
/// The for the .
/// The for the .
/// The for the .
- /// The value of .
- /// The value of .
+ /// The pointing to the game directory for the ..
+ /// The for the .
/// The for the .
/// The for the .
/// The for the .
@@ -85,321 +52,39 @@ namespace Tgstation.Server.Host.Components.Watchdog
IEventConsumer eventConsumer,
IRemoteDeploymentManagerFactory remoteDeploymentManagerFactory,
IIOManager gameIOManager,
- ISymlinkFactory symlinkFactory,
+ IFilesystemLinkFactory linkFactory,
ILogger logger,
DreamDaemonLaunchParameters initialLaunchParameters,
Api.Models.Instance instance,
bool autoStart)
: base(
- chat,
- sessionControllerFactory,
- dmbFactory,
- sessionPersistor,
- jobManager,
- serverControl,
- asyncDelayer,
- diagnosticsIOManager,
- eventConsumer,
- remoteDeploymentManagerFactory,
- logger,
- initialLaunchParameters,
- instance,
- autoStart)
+ chat,
+ sessionControllerFactory,
+ dmbFactory,
+ sessionPersistor,
+ jobManager,
+ serverControl,
+ asyncDelayer,
+ diagnosticsIOManager,
+ eventConsumer,
+ remoteDeploymentManagerFactory,
+ gameIOManager,
+ linkFactory,
+ logger,
+ initialLaunchParameters,
+ instance,
+ autoStart)
{
- try
- {
- GameIOManager = gameIOManager ?? throw new ArgumentNullException(nameof(gameIOManager));
- this.symlinkFactory = symlinkFactory ?? throw new ArgumentNullException(nameof(symlinkFactory));
-
- deploymentCleanupTasks = new List();
- }
- catch
- {
- // Async dispose is for if we have controllers running, not the case here
- var disposeTask = DisposeAsync();
- Debug.Assert(disposeTask.IsCompleted, "This should always be true during construction!");
- disposeTask.GetAwaiter().GetResult();
-
- throw;
- }
}
///
- protected override async ValueTask DisposeAndNullControllersImpl()
- {
- await base.DisposeAndNullControllersImpl();
-
- // If we reach this point, we can guarantee PrepServerForLaunch will be called before starting again.
- ActiveSwappable = null;
- pendingSwappable?.Dispose();
- pendingSwappable = null;
-
- await DrainDeploymentCleanupTasks(true);
- }
-
- ///
- protected override async ValueTask HandleNormalReboot(CancellationToken cancellationToken)
- {
- if (pendingSwappable != null)
- {
- var updateTask = BeforeApplyDmb(pendingSwappable.CompileJob, cancellationToken);
-
- if (!pendingSwappable.Swapped)
- {
- // IMPORTANT: THE SESSIONCONTROLLER SHOULD STILL BE PROCESSING THE BRIDGE REQUEST SO WE KNOW DD IS SLEEPING
- // OTHERWISE, IT COULD RETURN TO /world/Reboot() TOO EARLY AND LOAD THE WRONG .DMB
- if (!Server.ProcessingRebootBridgeRequest)
- {
- // integration test logging will catch this
- Logger.LogError(
- "The reboot bridge request completed before the watchdog could suspend the server! This can lead to buggy DreamDaemon behaviour and should be reported! To ensure stability, we will need to hard reboot the server");
- await updateTask;
- return MonitorAction.Restart;
- }
-
- await PerformDmbSwap(pendingSwappable, cancellationToken);
- }
-
- var currentCompileJobId = Server.ReattachInformation.Dmb.CompileJob.Id;
-
- await DrainDeploymentCleanupTasks(false);
-
- IDisposable lingeringDeployment;
- var localDeploymentCleanupGate = new TaskCompletionSource();
- async Task CleanupLingeringDeployment()
- {
- var lingeringDeploymentExpirySeconds = ActiveLaunchParameters.StartupTimeout.Value;
- Logger.LogDebug(
- "Holding old deployment {compileJobId} for up to {expiry} seconds...",
- currentCompileJobId,
- lingeringDeploymentExpirySeconds);
-
- var timeout = AsyncDelayer.Delay(TimeSpan.FromSeconds(lingeringDeploymentExpirySeconds), cancellationToken);
-
- var completedTask = await Task.WhenAny(
- localDeploymentCleanupGate.Task,
- timeout);
-
- var timedOut = completedTask == timeout;
- Logger.Log(
- timedOut
- ? LogLevel.Warning
- : LogLevel.Trace,
- "Releasing old deployment {compileJobId}{afterTimeout}",
- currentCompileJobId,
- timedOut
- ? " due to timeout!"
- : "...");
-
- lingeringDeployment.Dispose();
- }
-
- var oldDeploymentCleanupGate = Interlocked.Exchange(ref deploymentCleanupGate, localDeploymentCleanupGate);
- oldDeploymentCleanupGate?.TrySetResult();
-
- Logger.LogTrace("Replacing activeSwappable with pendingSwappable...");
-
- lock (deploymentCleanupTasks)
- {
- lingeringDeployment = Server.ReplaceDmbProvider(pendingSwappable);
- deploymentCleanupTasks.Add(
- CleanupLingeringDeployment());
- }
-
- ActiveSwappable = pendingSwappable;
- pendingSwappable = null;
-
- await SessionPersistor.Save(Server.ReattachInformation, cancellationToken);
- await updateTask;
- }
- else
- Logger.LogTrace("Nothing to do as pendingSwappable is null.");
-
- return MonitorAction.Continue;
- }
-
- ///
- protected override async ValueTask HandleNewDmbAvailable(CancellationToken cancellationToken)
- {
- IDmbProvider compileJobProvider = DmbFactory.LockNextDmb(1);
- bool canSeamlesslySwap = true;
-
- if (compileJobProvider.CompileJob.ByondVersion != ActiveCompileJob.ByondVersion)
- {
- // have to do a graceful restart
- Logger.LogDebug(
- "Not swapping to new compile job {0} as it uses a different BYOND version ({1}) than what is currently active {2}. Queueing graceful restart instead...",
- compileJobProvider.CompileJob.Id,
- compileJobProvider.CompileJob.ByondVersion,
- ActiveCompileJob.ByondVersion);
- canSeamlesslySwap = false;
- }
-
- if (compileJobProvider.CompileJob.DmeName != ActiveCompileJob.DmeName)
- {
- Logger.LogDebug(
- "Not swapping to new compile job {0} as it uses a different .dmb name ({1}) than what is currently active {2}. Queueing graceful restart instead...",
- compileJobProvider.CompileJob.Id,
- compileJobProvider.CompileJob.DmeName,
- ActiveCompileJob.DmeName);
- canSeamlesslySwap = false;
- }
-
- if (!canSeamlesslySwap)
- {
- compileJobProvider.Dispose();
- await base.HandleNewDmbAvailable(cancellationToken);
- return;
- }
-
- SwappableDmbProvider windowsProvider = null;
- try
- {
- windowsProvider = new SwappableDmbProvider(compileJobProvider, GameIOManager, symlinkFactory);
- if (ActiveCompileJob.DMApiVersion == null)
- {
- Logger.LogWarning("Active compile job has no DMAPI! Commencing immediate .dmb swap. Note this behavior is known to be buggy in some DM code contexts. See https://github.com/tgstation/tgstation-server/issues/1550");
- await PerformDmbSwap(windowsProvider, cancellationToken);
- }
- }
- catch (Exception ex)
- {
- Logger.LogError(ex, "Exception while swapping");
- IDmbProvider providerToDispose = windowsProvider ?? compileJobProvider;
- providerToDispose.Dispose();
- throw;
- }
-
- pendingSwappable?.Dispose();
- pendingSwappable = windowsProvider;
- }
-
- ///
- protected sealed override async ValueTask PrepServerForLaunch(IDmbProvider dmbToUse, CancellationToken cancellationToken)
- {
- if (ActiveSwappable != null)
- throw new InvalidOperationException("Expected activeSwappable to be null!");
- if (pendingSwappable != null)
- throw new InvalidOperationException("Expected pendingSwappable to be null!");
-
- Logger.LogTrace("Prep for server launch");
-
- ActiveSwappable = new SwappableDmbProvider(dmbToUse, GameIOManager, symlinkFactory);
- try
- {
- await InitialLink(cancellationToken);
- }
- catch (Exception ex)
- {
- // We won't worry about disposing activeSwappable here as we can't dispose dmbToUse here.
- Logger.LogTrace(ex, "Initial link error, nulling ActiveSwappable");
- ActiveSwappable = null;
- throw;
- }
-
- return ActiveSwappable;
- }
-
- ///
- /// Set the for the .
- ///
- /// The for the operation.
- /// A representing the running operation.
- protected virtual async ValueTask ApplyInitialDmb(CancellationToken cancellationToken)
+ protected override async ValueTask ApplyInitialDmb(CancellationToken cancellationToken)
{
Server.ReattachInformation.InitialDmb = await DmbFactory.FromCompileJob(Server.CompileJob, cancellationToken);
}
///
- protected override async ValueTask SessionStartupPersist(CancellationToken cancellationToken)
- {
- await ApplyInitialDmb(cancellationToken);
- await base.SessionStartupPersist(cancellationToken);
- }
-
- ///
- protected override async ValueTask HandleMonitorWakeup(MonitorActivationReason reason, CancellationToken cancellationToken)
- {
- var result = await base.HandleMonitorWakeup(reason, cancellationToken);
- if (reason == MonitorActivationReason.ActiveServerStartup)
- await DrainDeploymentCleanupTasks(false);
-
- return result;
- }
-
- ///
- /// Create the initial link to the live game directory using .
- ///
- /// The for the operation.
- /// A representing the running operation.
- ValueTask InitialLink(CancellationToken cancellationToken)
- {
- Logger.LogTrace("Symlinking compile job...");
- return ActiveSwappable.MakeActive(cancellationToken);
- }
-
- ///
- /// Suspends the and calls on a .
- ///
- /// The to activate.
- /// The for the operation.
- /// A representing the running operation.
- async ValueTask PerformDmbSwap(SwappableDmbProvider newProvider, CancellationToken cancellationToken)
- {
- Logger.LogDebug("Swapping to compile job {id}...", newProvider.CompileJob.Id);
-
- var suspended = false;
- var server = Server;
- try
- {
- server.Suspend();
- suspended = true;
- }
- catch (Exception ex)
- {
- Logger.LogWarning(ex, "Exception while suspending server!");
- }
-
- try
- {
- await newProvider.MakeActive(cancellationToken);
- }
- finally
- {
- // Let this throw hard if it fails
- if (suspended)
- server.Resume();
- }
- }
-
- ///
- /// Asynchronously drain .
- ///
- /// If , all s will be ed. Otherwise, only s with set will be ed.
- /// A representing the running operation.
- Task DrainDeploymentCleanupTasks(bool blocking)
- {
- Logger.LogTrace("DrainDeploymentCleanupTasks...");
- var localDeploymentCleanupGate = Interlocked.Exchange(ref deploymentCleanupGate, null);
- localDeploymentCleanupGate?.TrySetResult();
-
- List localDeploymentCleanupTasks;
- lock (deploymentCleanupTasks)
- {
- var totalActiveTasks = deploymentCleanupTasks.Count;
- localDeploymentCleanupTasks = new List(totalActiveTasks);
- for (var i = totalActiveTasks - 1; i >= 0; --i)
- {
- var currentTask = deploymentCleanupTasks[i];
- if (!blocking && !currentTask.IsCompleted)
- continue;
-
- localDeploymentCleanupTasks.Add(currentTask);
- deploymentCleanupTasks.RemoveAt(i);
- }
- }
-
- return Task.WhenAll(localDeploymentCleanupTasks);
- }
+ protected override SwappableDmbProvider CreateSwappableDmbProvider(IDmbProvider dmbProvider)
+ => new SymlinkDmbProvider(dmbProvider, GameIOManager, LinkFactory);
}
}
diff --git a/src/Tgstation.Server.Host/Components/Watchdog/WindowsWatchdogFactory.cs b/src/Tgstation.Server.Host/Components/Watchdog/WindowsWatchdogFactory.cs
index 7909be59af..89ed6b145d 100644
--- a/src/Tgstation.Server.Host/Components/Watchdog/WindowsWatchdogFactory.cs
+++ b/src/Tgstation.Server.Host/Components/Watchdog/WindowsWatchdogFactory.cs
@@ -18,14 +18,14 @@ using Tgstation.Server.Host.Utils;
namespace Tgstation.Server.Host.Components.Watchdog
{
///
- /// for creating s.
+ /// for creating s.
///
class WindowsWatchdogFactory : WatchdogFactory
{
///
- /// The for the .
+ /// The for the .
///
- protected ISymlinkFactory SymlinkFactory { get; }
+ protected IFilesystemLinkFactory LinkFactory { get; }
///
/// Initializes a new instance of the class.
@@ -34,14 +34,14 @@ namespace Tgstation.Server.Host.Components.Watchdog
/// The for the .
/// The for the .
/// The for the .
- /// The value of .
+ /// The value of .
/// The for for the .
public WindowsWatchdogFactory(
IServerControl serverControl,
ILoggerFactory loggerFactory,
IJobManager jobManager,
IAsyncDelayer asyncDelayer,
- ISymlinkFactory symlinkFactory,
+ IFilesystemLinkFactory symlinkFactory,
IOptions generalConfigurationOptions)
: base(
serverControl,
@@ -50,7 +50,7 @@ namespace Tgstation.Server.Host.Components.Watchdog
asyncDelayer,
generalConfigurationOptions)
{
- SymlinkFactory = symlinkFactory ?? throw new ArgumentNullException(nameof(symlinkFactory));
+ LinkFactory = symlinkFactory ?? throw new ArgumentNullException(nameof(symlinkFactory));
}
///
@@ -77,7 +77,7 @@ namespace Tgstation.Server.Host.Components.Watchdog
eventConsumer,
remoteDeploymentManagerFactory,
gameIOManager,
- SymlinkFactory,
+ LinkFactory,
LoggerFactory.CreateLogger(),
settings,
instance,
diff --git a/src/Tgstation.Server.Host/Core/Application.cs b/src/Tgstation.Server.Host/Core/Application.cs
index da0a804468..d5902db2f4 100644
--- a/src/Tgstation.Server.Host/Core/Application.cs
+++ b/src/Tgstation.Server.Host/Core/Application.cs
@@ -331,7 +331,7 @@ namespace Tgstation.Server.Host.Core
{
AddWatchdog(services, postSetupServices);
services.AddSingleton();
- services.AddSingleton();
+ services.AddSingleton();
services.AddSingleton();
services.AddSingleton();
services.AddSingleton();
@@ -344,7 +344,7 @@ namespace Tgstation.Server.Host.Core
{
AddWatchdog(services, postSetupServices);
services.AddSingleton();
- services.AddSingleton();
+ services.AddSingleton();
services.AddSingleton();
services.AddSingleton();
diff --git a/src/Tgstation.Server.Host/IO/ISymlinkFactory.cs b/src/Tgstation.Server.Host/IO/IFilesystemLinkFactory.cs
similarity index 65%
rename from src/Tgstation.Server.Host/IO/ISymlinkFactory.cs
rename to src/Tgstation.Server.Host/IO/IFilesystemLinkFactory.cs
index 1adbd99d97..3cf0622801 100644
--- a/src/Tgstation.Server.Host/IO/ISymlinkFactory.cs
+++ b/src/Tgstation.Server.Host/IO/IFilesystemLinkFactory.cs
@@ -6,7 +6,7 @@ namespace Tgstation.Server.Host.IO
///
/// For creating filesystem symbolic links.
///
- interface ISymlinkFactory
+ interface IFilesystemLinkFactory
{
///
/// If directory symlinks must be deleted as files would in the current environment.
@@ -22,5 +22,14 @@ namespace Tgstation.Server.Host.IO
/// The for the operation.
/// A representing the running operation.
Task CreateSymbolicLink(string targetPath, string linkPath, CancellationToken cancellationToken);
+
+ ///
+ /// Creates a hard link.
+ ///
+ /// The path to the hard target.
+ /// The path to the link.
+ /// The for the operation.
+ /// A representing the running operation.
+ Task CreateHardLink(string targetPath, string linkPath, CancellationToken cancellationToken);
}
}
diff --git a/src/Tgstation.Server.Host/IO/PosixSymlinkFactory.cs b/src/Tgstation.Server.Host/IO/PosixFilesystemLinkFactory.cs
similarity index 58%
rename from src/Tgstation.Server.Host/IO/PosixSymlinkFactory.cs
rename to src/Tgstation.Server.Host/IO/PosixFilesystemLinkFactory.cs
index ddbcbb86fb..b80af9e122 100644
--- a/src/Tgstation.Server.Host/IO/PosixSymlinkFactory.cs
+++ b/src/Tgstation.Server.Host/IO/PosixFilesystemLinkFactory.cs
@@ -8,13 +8,29 @@ using Mono.Unix;
namespace Tgstation.Server.Host.IO
{
///
- /// for posix systems.
+ /// for POSIX systems.
///
- sealed class PosixSymlinkFactory : ISymlinkFactory
+ sealed class PosixFilesystemLinkFactory : IFilesystemLinkFactory
{
///
public bool SymlinkedDirectoriesAreDeletedAsFiles => true;
+ ///
+ public Task CreateHardLink(string targetPath, string linkPath, CancellationToken cancellationToken) => Task.Factory.StartNew(
+ () =>
+ {
+ ArgumentNullException.ThrowIfNull(targetPath);
+ ArgumentNullException.ThrowIfNull(linkPath);
+
+ cancellationToken.ThrowIfCancellationRequested();
+ var fsInfo = new UnixFileInfo(targetPath);
+ cancellationToken.ThrowIfCancellationRequested();
+ fsInfo.CreateLink(linkPath);
+ },
+ cancellationToken,
+ DefaultIOManager.BlockingTaskCreationOptions,
+ TaskScheduler.Current);
+
///
public Task CreateSymbolicLink(string targetPath, string linkPath, CancellationToken cancellationToken) => Task.Factory.StartNew(
() =>
diff --git a/src/Tgstation.Server.Host/IO/WindowsSymlinkFactory.cs b/src/Tgstation.Server.Host/IO/WindowsFilesystemLinkFactory.cs
similarity index 83%
rename from src/Tgstation.Server.Host/IO/WindowsSymlinkFactory.cs
rename to src/Tgstation.Server.Host/IO/WindowsFilesystemLinkFactory.cs
index 18abde6b8f..1513f4e166 100644
--- a/src/Tgstation.Server.Host/IO/WindowsSymlinkFactory.cs
+++ b/src/Tgstation.Server.Host/IO/WindowsFilesystemLinkFactory.cs
@@ -9,13 +9,17 @@ using Tgstation.Server.Host.System;
namespace Tgstation.Server.Host.IO
{
///
- /// for windows systems.
+ /// for windows systems.
///
- sealed class WindowsSymlinkFactory : ISymlinkFactory
+ sealed class WindowsFilesystemLinkFactory : IFilesystemLinkFactory
{
///
public bool SymlinkedDirectoriesAreDeletedAsFiles => false;
+ ///
+ public Task CreateHardLink(string targetPath, string linkPath, CancellationToken cancellationToken)
+ => throw new NotSupportedException();
+
///
public Task CreateSymbolicLink(string targetPath, string linkPath, CancellationToken cancellationToken) => Task.Factory.StartNew(
() =>
diff --git a/src/Tgstation.Server.Host/Models/ReattachInformationBase.cs b/src/Tgstation.Server.Host/Models/ReattachInformationBase.cs
index 76dfbfda36..241fae5141 100644
--- a/src/Tgstation.Server.Host/Models/ReattachInformationBase.cs
+++ b/src/Tgstation.Server.Host/Models/ReattachInformationBase.cs
@@ -1,5 +1,4 @@
using System;
-using System.ComponentModel.DataAnnotations;
using System.Globalization;
using Tgstation.Server.Api.Models;
@@ -31,14 +30,12 @@ namespace Tgstation.Server.Host.Models
///
/// The level DreamDaemon was launched with.
///
- [Required]
- public DreamDaemonSecurity? LaunchSecurityLevel { get; set; }
+ public DreamDaemonSecurity LaunchSecurityLevel { get; set; }
///
/// The DreamDaemon was launched with.
///
- [Required]
- public DreamDaemonVisibility? LaunchVisibility { get; set; }
+ public DreamDaemonVisibility LaunchVisibility { get; set; }
///
/// Initializes a new instance of the class.
diff --git a/src/Tgstation.Server.Host/Utils/GitHub/GitHubClientFactory.cs b/src/Tgstation.Server.Host/Utils/GitHub/GitHubClientFactory.cs
index 47a67728ab..7595a70a43 100644
--- a/src/Tgstation.Server.Host/Utils/GitHub/GitHubClientFactory.cs
+++ b/src/Tgstation.Server.Host/Utils/GitHub/GitHubClientFactory.cs
@@ -153,7 +153,7 @@ namespace Tgstation.Server.Host.Utils.GitHub
rateLimitInfo.Reset.ToString("o"));
else
logger.LogDebug(
- "Requested GitHub client has {remainingRequests} requests remaining after the usage {lastUse}. Limit resets at {resetTime}",
+ "Requested GitHub client has {remainingRequests} requests remaining after the usage at {lastUse}. Limit resets at {resetTime}",
rateLimitInfo.Remaining,
lastUsed,
rateLimitInfo.Reset.ToString("o"));
diff --git a/tests/DMAPI/LongRunning/Test.dm b/tests/DMAPI/LongRunning/Test.dm
index bed428eceb..1d008a3de9 100644
--- a/tests/DMAPI/LongRunning/Test.dm
+++ b/tests/DMAPI/LongRunning/Test.dm
@@ -11,11 +11,49 @@
dab()
TgsNew(new /datum/tgs_event_handler/impl, TGS_SECURITY_SAFE)
+ var/sec = TgsSecurityLevel()
+ if(isnull(sec))
+ FailTest("TGS Security level was null!")
+
+ log << "Running in security level: [sec]"
+
+ var/vis = TgsVisibility()
+ if(isnull(vis))
+ FailTest("TGS Visibility was null!")
+
+ log << "Running in visibility: [vis]"
+
if(params["expect_chat_channels"])
var/list/channels = TgsChatChannelInfo()
if(!length(channels))
FailTest("Expected some chat channels!")
+ var/test_str = "aljsdhfjahsfkjnsalkjdfhskljdackmcnvxkljhvkjsdanv,jdshlkufhklasjeFDhfjkalhdkjlfhalksfdjh"
+ var/res_contents = file2text('resource.txt') // we need a .rsc to be generated
+
+ if(!findtext(res_contents, test_str))
+ FailTest("Failed to resource? Did not find magic: [res_contents]")
+
+ 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.dme b/tests/DMAPI/LongRunning/long_running_test.dme
index b927014455..322587647d 100644
--- a/tests/DMAPI/LongRunning/long_running_test.dme
+++ b/tests/DMAPI/LongRunning/long_running_test.dme
@@ -13,5 +13,8 @@
// BEGIN_INCLUDE
#include "Config.dm"
#include "../test_prelude.dm"
+#ifndef DME_NAME
+#define DME_NAME "long_running_test"
+#endif
#include "Test.dm"
// END_INCLUDE
diff --git a/tests/DMAPI/LongRunning/long_running_test_copy.dme b/tests/DMAPI/LongRunning/long_running_test_copy.dme
index b4da8e0d96..e443c4b331 100644
--- a/tests/DMAPI/LongRunning/long_running_test_copy.dme
+++ b/tests/DMAPI/LongRunning/long_running_test_copy.dme
@@ -11,5 +11,6 @@
// END_PREFERENCES
// BEGIN_INCLUDE
+#define DME_NAME "long_running_test_copy"
#include "long_running_test.dme"
// END_INCLUDE
diff --git a/tests/DMAPI/LongRunning/long_running_test_rooted.dme b/tests/DMAPI/LongRunning/long_running_test_rooted.dme
new file mode 100644
index 0000000000..a2ff783588
--- /dev/null
+++ b/tests/DMAPI/LongRunning/long_running_test_rooted.dme
@@ -0,0 +1,19 @@
+// Hand crafted DME, will not work if saved with DreamMaker
+
+// BEGIN_INTERNALS
+// END_INTERNALS
+
+// BEGIN_FILE_DIR
+#define FILE_DIR .
+// END_FILE_DIR
+
+// BEGIN_PREFERENCES
+// 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"
+// END_INCLUDE
diff --git a/tests/DMAPI/LongRunning/resource.txt b/tests/DMAPI/LongRunning/resource.txt
new file mode 100644
index 0000000000..94b848fe2a
--- /dev/null
+++ b/tests/DMAPI/LongRunning/resource.txt
@@ -0,0 +1,8 @@
+
+ if(!length(channels))
+ FailTest("Expected some chat channels!")
+
+ var/test_str = "aljsdhfjahsfkjnsalkjdfhskljdackmcnvxkljhvkjsdanv,jdshlkufhklasjeFDhfjkalhdkjlfhalksfdjh"
+ var/self_contents = file2text(file('Test.dm')) // we need a .rsc to be generated
+
+ if(!(test_str in self_contents))
diff --git a/tests/Tgstation.Server.Host.Tests/Components/StaticFiles/TestConfiguration.cs b/tests/Tgstation.Server.Host.Tests/Components/StaticFiles/TestConfiguration.cs
index 68241988d1..ff3bd0b5b8 100644
--- a/tests/Tgstation.Server.Host.Tests/Components/StaticFiles/TestConfiguration.cs
+++ b/tests/Tgstation.Server.Host.Tests/Components/StaticFiles/TestConfiguration.cs
@@ -47,7 +47,7 @@ namespace Tgstation.Server.Host.Components.StaticFiles.Tests
var configuration = new Configuration(
ioManager,
new SynchronousIOManager(),
- Mock.Of(),
+ Mock.Of(),
Mock.Of(),
Mock.Of(),
Mock.Of(),
diff --git a/tests/Tgstation.Server.Host.Tests/IO/TestSymlinkFactory.cs b/tests/Tgstation.Server.Host.Tests/IO/TestFilesystemLinkFactory.cs
similarity index 52%
rename from tests/Tgstation.Server.Host.Tests/IO/TestSymlinkFactory.cs
rename to tests/Tgstation.Server.Host.Tests/IO/TestFilesystemLinkFactory.cs
index 27646a9dc7..dbf2aaf2a8 100644
--- a/tests/Tgstation.Server.Host.Tests/IO/TestSymlinkFactory.cs
+++ b/tests/Tgstation.Server.Host.Tests/IO/TestFilesystemLinkFactory.cs
@@ -1,24 +1,26 @@
-using Microsoft.VisualStudio.TestTools.UnitTesting;
-using System;
+using System;
using System.IO;
using System.Runtime.InteropServices;
using System.Security.Principal;
+using System.Threading;
using System.Threading.Tasks;
+using Microsoft.VisualStudio.TestTools.UnitTesting;
+
namespace Tgstation.Server.Host.IO.Tests
{
[TestClass]
- public sealed class TestSymlinkFactory
+ public sealed class TestFilesystemLinkFactory
{
- static ISymlinkFactory symlinkFactory;
+ static IFilesystemLinkFactory linkFactory;
[ClassInitialize]
public static void SelectFactory(TestContext _)
{
if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows))
- symlinkFactory = new WindowsSymlinkFactory();
+ linkFactory = new WindowsFilesystemLinkFactory();
else
- symlinkFactory = new PosixSymlinkFactory();
+ linkFactory = new PosixFilesystemLinkFactory();
}
public static bool HasPermissionToMakeSymlinks()
@@ -30,6 +32,57 @@ namespace Tgstation.Server.Host.IO.Tests
return principal.IsInRole(WindowsBuiltInRole.Administrator);
}
+ [TestMethod]
+ public async Task TestHardLinkWorks()
+ {
+ string f2 = null;
+ var f1 = Path.GetTempFileName();
+ try
+ {
+ f2 = f1 + ".linked";
+ const string Text = "Hello world";
+ File.WriteAllText(f1, Text);
+
+ if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows))
+ {
+ await Assert.ThrowsExceptionAsync(() => linkFactory.CreateHardLink(f1, f2, CancellationToken.None));
+ Assert.Inconclusive("Windows does not support hardlinks");
+ }
+
+ await Assert.ThrowsExceptionAsync(() => linkFactory.CreateHardLink(null, null, CancellationToken.None));
+ await Assert.ThrowsExceptionAsync(() => linkFactory.CreateHardLink(f1, null, CancellationToken.None));
+
+ await linkFactory.CreateHardLink(f1, f2, default);
+ Assert.IsTrue(File.Exists(f2));
+
+ var f2Contents = File.ReadAllText(f2);
+ Assert.AreEqual(Text, f2Contents);
+
+ const string NewText = "asdf";
+ File.WriteAllText(f1, NewText);
+
+ f2Contents = File.ReadAllText(f2);
+ Assert.AreEqual(NewText, f2Contents);
+
+ const string NewText2 = "fdsa";
+ File.WriteAllText(f2, NewText2);
+
+ var f1Contents = File.ReadAllText(f1);
+ Assert.AreEqual(NewText2, f1Contents);
+
+ File.Delete(f1);
+ Assert.IsFalse(File.Exists(f1));
+ Assert.IsTrue(File.Exists(f2));
+ f2Contents = File.ReadAllText(f2);
+ Assert.AreEqual(NewText2, f2Contents);
+ }
+ finally
+ {
+ File.Delete(f2);
+ File.Delete(f1);
+ }
+ }
+
[TestMethod]
public async Task TestFileWorks()
{
@@ -43,10 +96,10 @@ namespace Tgstation.Server.Host.IO.Tests
f2 = f1 + ".linked";
File.WriteAllText(f1, Text);
- await Assert.ThrowsExceptionAsync(() => symlinkFactory.CreateSymbolicLink(null, null, default));
- await Assert.ThrowsExceptionAsync(() => symlinkFactory.CreateSymbolicLink(f1, null, default));
+ await Assert.ThrowsExceptionAsync(() => linkFactory.CreateSymbolicLink(null, null, default));
+ await Assert.ThrowsExceptionAsync(() => linkFactory.CreateSymbolicLink(f1, null, default));
- await symlinkFactory.CreateSymbolicLink(f1, f2, default);
+ await linkFactory.CreateSymbolicLink(f1, f2, default);
Assert.IsTrue(File.Exists(f2));
var f2Contents = File.ReadAllText(f2);
@@ -76,10 +129,10 @@ namespace Tgstation.Server.Host.IO.Tests
var p1 = Path.Combine(f1, FileName);
File.WriteAllText(p1, Text);
- await Assert.ThrowsExceptionAsync(() => symlinkFactory.CreateSymbolicLink(null, null, default));
- await Assert.ThrowsExceptionAsync(() => symlinkFactory.CreateSymbolicLink(f1, null, default));
+ await Assert.ThrowsExceptionAsync(() => linkFactory.CreateSymbolicLink(null, null, default));
+ await Assert.ThrowsExceptionAsync(() => linkFactory.CreateSymbolicLink(f1, null, default));
- await symlinkFactory.CreateSymbolicLink(f1, f2, default);
+ await linkFactory.CreateSymbolicLink(f1, f2, default);
var p2 = Path.Combine(f2, FileName);
Assert.IsTrue(File.Exists(p2));
@@ -104,7 +157,7 @@ namespace Tgstation.Server.Host.IO.Tests
try
{
- await symlinkFactory.CreateSymbolicLink(BadPath, BadPath, default);
+ await linkFactory.CreateSymbolicLink(BadPath, BadPath, default);
Assert.Fail("No exception thrown!");
}
catch { }
diff --git a/tests/Tgstation.Server.Host.Tests/IO/TestIOManager.cs b/tests/Tgstation.Server.Host.Tests/IO/TestIOManager.cs
index f8b7a9b7c9..8cd5424a4f 100644
--- a/tests/Tgstation.Server.Host.Tests/IO/TestIOManager.cs
+++ b/tests/Tgstation.Server.Host.Tests/IO/TestIOManager.cs
@@ -1,11 +1,12 @@
-using Microsoft.VisualStudio.TestTools.UnitTesting;
-
-using Remora.Discord.API.Objects;
-
-using System;
+using System;
using System.IO;
+using System.Linq;
+using System.Text;
+using System.Threading;
using System.Threading.Tasks;
+using Microsoft.VisualStudio.TestTools.UnitTesting;
+
using Tgstation.Server.Host.System;
namespace Tgstation.Server.Host.IO.Tests
@@ -38,6 +39,53 @@ namespace Tgstation.Server.Host.IO.Tests
}
}
+ [TestMethod]
+ public async Task TestDeleteDirectoryWithSymlinkInsideDoesntRecurse()
+ {
+ var linkFactory = (IFilesystemLinkFactory)(new PlatformIdentifier().IsWindows
+ ? new WindowsFilesystemLinkFactory()
+ : new PosixFilesystemLinkFactory());
+
+ var tempPath = Path.GetTempFileName();
+ File.Delete(tempPath);
+ Directory.CreateDirectory(tempPath);
+ try
+ {
+ var targetDir = ioManager.ConcatPath(tempPath, "targetdir");
+ await ioManager.CreateDirectory(targetDir, CancellationToken.None);
+ var fileInTargetDir = ioManager.ConcatPath(targetDir, "test1.txt");
+
+ var expectedBytes = Encoding.UTF8.GetBytes("I want to live");
+ await ioManager.WriteAllBytes(fileInTargetDir, expectedBytes, CancellationToken.None);
+
+ var testDir = ioManager.ConcatPath(tempPath, "testdir");
+ await ioManager.CreateDirectory(testDir, CancellationToken.None);
+ var symlinkedFile = ioManager.ConcatPath(testDir, "test1.txt");
+ var symlinkedDir = ioManager.ConcatPath(testDir, "linkedDir");
+
+ await linkFactory.CreateSymbolicLink(targetDir, symlinkedDir, CancellationToken.None);
+ await linkFactory.CreateSymbolicLink(fileInTargetDir, symlinkedFile, CancellationToken.None);
+
+ Assert.IsTrue(await ioManager.DirectoryExists(symlinkedDir, CancellationToken.None));
+ Assert.IsTrue(await ioManager.FileExists(symlinkedFile, CancellationToken.None));
+ Assert.IsTrue(await ioManager.FileExists(ioManager.ConcatPath(symlinkedDir, "test1.txt"), CancellationToken.None));
+ Assert.IsTrue(await ioManager.FileExists(fileInTargetDir, CancellationToken.None));
+
+ await ioManager.DeleteDirectory(testDir, CancellationToken.None);
+
+ Assert.IsFalse(await ioManager.DirectoryExists(symlinkedDir, CancellationToken.None));
+ Assert.IsFalse(await ioManager.FileExists(symlinkedFile, CancellationToken.None));
+ Assert.IsFalse(await ioManager.FileExists(ioManager.ConcatPath(symlinkedDir, "test1.txt"), CancellationToken.None));
+ Assert.IsTrue(await ioManager.FileExists(fileInTargetDir, CancellationToken.None));
+ Assert.IsTrue(expectedBytes.SequenceEqual(await ioManager.ReadAllBytes(fileInTargetDir, CancellationToken.None)));
+ }
+ catch
+ {
+ Directory.Delete(tempPath, true);
+ throw;
+ }
+ }
+
[TestMethod]
public async Task TestFileExists()
{
diff --git a/tests/Tgstation.Server.Host.Tests/System/TestSymlinkFactory.cs b/tests/Tgstation.Server.Host.Tests/System/TestSymlinkFactory.cs
index 8e488f11b0..4d3c5be749 100644
--- a/tests/Tgstation.Server.Host.Tests/System/TestSymlinkFactory.cs
+++ b/tests/Tgstation.Server.Host.Tests/System/TestSymlinkFactory.cs
@@ -12,9 +12,9 @@ namespace Tgstation.Server.Host.System.Tests
[TestClass]
public sealed class TestSymlinkFactory
{
- readonly ISymlinkFactory factory = new PlatformIdentifier().IsWindows
- ? new WindowsSymlinkFactory()
- : new PosixSymlinkFactory();
+ readonly IFilesystemLinkFactory factory = new PlatformIdentifier().IsWindows
+ ? new WindowsFilesystemLinkFactory()
+ : new PosixFilesystemLinkFactory();
[TestMethod]
public async Task TestSymlinks()
diff --git a/tests/Tgstation.Server.Tests/Live/Instance/ConfigurationTest.cs b/tests/Tgstation.Server.Tests/Live/Instance/ConfigurationTest.cs
index 56a879c98d..333b2489d7 100644
--- a/tests/Tgstation.Server.Tests/Live/Instance/ConfigurationTest.cs
+++ b/tests/Tgstation.Server.Tests/Live/Instance/ConfigurationTest.cs
@@ -84,19 +84,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 ValueTask SetupDMApiTests(CancellationToken cancellationToken)
+ public ValueTask SetupDMApiTests(bool includingRoot, CancellationToken cancellationToken)
{
// just use an I/O manager here
var ioManager = new DefaultIOManager();
+
+ async ValueTask 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 ValueTaskExtensions.WhenAll(
ioManager.CopyDirectory(
Enumerable.Empty(),
@@ -105,6 +126,15 @@ namespace Tgstation.Server.Tests.Live.Instance
ioManager.ConcatPath(instance.Path, "Repository", "tests", "DMAPI"),
null,
cancellationToken),
+ includingRoot
+ ? ioManager.CopyFile(
+ "../../../../DMAPI/LongRunning/long_running_test_rooted.dme",
+ ioManager.ConcatPath(instance.Path, "Repository", "long_running_test_rooted.dme"),
+ cancellationToken)
+ : ValueTask.CompletedTask,
+ includingRoot
+ ? TestStaticFileAndDir()
+ : ValueTask.CompletedTask,
ioManager.CopyDirectory(
Enumerable.Empty(),
null,
@@ -176,8 +206,8 @@ namespace Tgstation.Server.Tests.Live.Instance
}
public Task RunPreWatchdog(CancellationToken cancellationToken) => Task.WhenAll(
+ SetupDMApiTests(false, cancellationToken).AsTask(),
SequencedApiTests(cancellationToken),
- SetupDMApiTests(cancellationToken).AsTask(),
TestPregeneratedFilesExist(cancellationToken));
}
}
diff --git a/tests/Tgstation.Server.Tests/Live/Instance/InstanceTest.cs b/tests/Tgstation.Server.Tests/Live/Instance/InstanceTest.cs
index ed0edee01e..657849d25f 100644
--- a/tests/Tgstation.Server.Tests/Live/Instance/InstanceTest.cs
+++ b/tests/Tgstation.Server.Tests/Live/Instance/InstanceTest.cs
@@ -43,6 +43,7 @@ namespace Tgstation.Server.Tests.Live.Instance
ushort ddPort,
bool highPrioDD,
bool lowPrioDeployment,
+ bool usingBasicWatchdog,
CancellationToken cancellationToken)
{
var byondTest = new ByondTest(instanceClient.Byond, instanceClient.Jobs, fileDownloader, instanceClient.Metadata);
@@ -63,10 +64,11 @@ namespace Tgstation.Server.Tests.Live.Instance
await chatTask;
await dmTask;
+ await configTest.SetupDMApiTests(true, cancellationToken);
await byondTask;
await new WatchdogTest(
- await ByondTest.GetEdgeVersion(fileDownloader, cancellationToken), instanceClient, instanceManager, serverPort, highPrioDD, ddPort).Run(cancellationToken);
+ await ByondTest.GetEdgeVersion(fileDownloader, cancellationToken), instanceClient, instanceManager, serverPort, highPrioDD, ddPort, usingBasicWatchdog).Run(cancellationToken);
}
public async Task RunCompatTests(
@@ -75,6 +77,7 @@ namespace Tgstation.Server.Tests.Live.Instance
ushort dmPort,
ushort ddPort,
bool highPrioDD,
+ bool usingBasicWatchdog,
CancellationToken cancellationToken)
{
System.Console.WriteLine($"COMPAT TEST START: {compatVersion}");
@@ -172,7 +175,7 @@ namespace Tgstation.Server.Tests.Live.Instance
dmUpdateRequest.AsTask(),
cloneRequest.AsTask());
- var configSetupTask = new ConfigurationTest(instanceClient.Configuration, instanceClient.Metadata).SetupDMApiTests(cancellationToken);
+ var configSetupTask = new ConfigurationTest(instanceClient.Configuration, instanceClient.Metadata).SetupDMApiTests(true, cancellationToken);
if (TestingUtils.RunningInGitHubActions
|| String.IsNullOrWhiteSpace(Environment.GetEnvironmentVariable("TGS_TEST_GITHUB_TOKEN"))
@@ -188,7 +191,7 @@ namespace Tgstation.Server.Tests.Live.Instance
await configSetupTask;
- await new WatchdogTest(compatVersion, instanceClient, instanceManager, serverPort, highPrioDD, ddPort).Run(cancellationToken);
+ await new WatchdogTest(compatVersion, instanceClient, instanceManager, serverPort, highPrioDD, ddPort, usingBasicWatchdog).Run(cancellationToken);
await instanceManagerClient.Update(new InstanceUpdateRequest
{
diff --git a/tests/Tgstation.Server.Tests/Live/Instance/WatchdogTest.cs b/tests/Tgstation.Server.Tests/Live/Instance/WatchdogTest.cs
index f0c89002e8..7da4e2bbe4 100644
--- a/tests/Tgstation.Server.Tests/Live/Instance/WatchdogTest.cs
+++ b/tests/Tgstation.Server.Tests/Live/Instance/WatchdogTest.cs
@@ -3,17 +3,22 @@
using Microsoft.Extensions.Logging;
using Microsoft.VisualStudio.TestTools.UnitTesting;
+using Mono.Unix;
+using Mono.Unix.Native;
+
using Moq;
using Newtonsoft.Json;
using System;
+using System.Collections.Generic;
using System.Globalization;
using System.IO;
using System.Linq;
using System.Net;
using System.Net.Sockets;
using System.Runtime.InteropServices;
+using System.Text;
using System.Threading;
using System.Threading.Tasks;
@@ -58,10 +63,11 @@ namespace Tgstation.Server.Tests.Live.Instance
readonly bool highPrioDD;
readonly TopicClient topicClient;
readonly Version testVersion;
+ readonly bool usingBasicWatchdog;
bool ranTimeoutTest = false;
- public WatchdogTest(Version testVersion, IInstanceClient instanceClient, InstanceManager instanceManager, ushort serverPort, bool highPrioDD, ushort ddPort)
+ public WatchdogTest(Version testVersion, IInstanceClient instanceClient, InstanceManager instanceManager, ushort serverPort, bool highPrioDD, ushort ddPort, bool usingBasicWatchdog)
: base(instanceClient.Jobs)
{
this.instanceClient = instanceClient ?? throw new ArgumentNullException(nameof(instanceClient));
@@ -70,8 +76,9 @@ namespace Tgstation.Server.Tests.Live.Instance
this.highPrioDD = highPrioDD;
this.ddPort = ddPort;
this.testVersion = testVersion ?? throw new ArgumentNullException(nameof(testVersion));
+ this.usingBasicWatchdog = usingBasicWatchdog;
- this.topicClient = new(new SocketParameters
+ topicClient = new(new SocketParameters
{
SendTimeout = TimeSpan.FromSeconds(30),
ReceiveTimeout = TimeSpan.FromSeconds(30),
@@ -107,6 +114,7 @@ namespace Tgstation.Server.Tests.Live.Instance
Port = ddPort,
MapThreads = 2,
LogOutput = false,
+ AdditionalParameters = "expect_chat_channels=1&expect_static_files=1"
}, cancellationToken).AsTask(),
CheckByondVersions(),
ApiAssert.ThrowsException(() => instanceClient.DreamDaemon.Update(new DreamDaemonRequest
@@ -149,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);
@@ -183,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);
@@ -470,6 +533,47 @@ namespace Tgstation.Server.Tests.Live.Instance
Assert.AreEqual(string.Empty, daemonStatus.AdditionalParameters);
}
+ void TestLinuxIsntBeingFuckingCheekyAboutFilePaths(DreamDaemonResponse currentStatus, CompileJobResponse previousStatus)
+ {
+ if (new PlatformIdentifier().IsWindows || usingBasicWatchdog)
+ return;
+
+ Assert.IsNotNull(currentStatus.ActiveCompileJob);
+ Assert.IsTrue(currentStatus.ActiveCompileJob.DmeName.Contains("long_running_test"));
+ Assert.AreEqual(WatchdogStatus.Online, currentStatus.Status);
+
+ var procs = TestLiveServer.GetDDProcessesOnPort(currentStatus.Port.Value);
+ Assert.AreEqual(1, procs.Count);
+ var failingLinks = new List();
+ using var proc = procs[0];
+ var pid = proc.Id;
+ var foundLivePath = false;
+ var allPaths = new List();
+ foreach (var fd in Directory.EnumerateFiles($"/proc/{pid}/fd"))
+ {
+ var sb = new StringBuilder(UInt16.MaxValue);
+ if (Syscall.readlink(fd, sb) == -1)
+ throw new UnixIOException(Stdlib.GetLastError());
+
+ var path = sb.ToString();
+
+ allPaths.Add($"Path: {path}");
+ if (path.Contains($"Game/{previousStatus.DirectoryName}"))
+ failingLinks.Add($"Found fd {fd} resolving to previous absolute path game dir path: {path}");
+
+ if (path.Contains($"Game/{currentStatus.ActiveCompileJob.DirectoryName}"))
+ failingLinks.Add($"Found fd {fd} resolving to current absolute path game dir path: {path}");
+
+ if (path.Contains($"Game/Live"))
+ foundLivePath = true;
+ }
+
+ if (!foundLivePath)
+ failingLinks.Add($"Failed to find a path containing the 'Live' directory!");
+
+ Assert.IsTrue(failingLinks.Count == 0, String.Join(Environment.NewLine, failingLinks.Concat(allPaths)));
+ }
+
async Task RunHealthCheckTest(bool checkDump, CancellationToken cancellationToken)
{
System.Console.WriteLine("TEST: WATCHDOG HEALTH CHECK TEST");
@@ -900,6 +1004,8 @@ namespace Tgstation.Server.Tests.Live.Instance
Assert.AreNotEqual(initialCompileJob.Id, daemonStatus.ActiveCompileJob.Id);
Assert.IsNull(daemonStatus.StagedCompileJob);
+ TestLinuxIsntBeingFuckingCheekyAboutFilePaths(daemonStatus, initialCompileJob);
+
await instanceClient.DreamDaemon.Shutdown(cancellationToken);
daemonStatus = await instanceClient.DreamDaemon.Read(cancellationToken);
@@ -1100,7 +1206,7 @@ namespace Tgstation.Server.Tests.Live.Instance
var refreshed = await instanceClient.DreamMaker.Update(new DreamMakerRequest
{
ApiValidationSecurityLevel = deploymentSecurity,
- ProjectName = $"tests/DMAPI/{dmeName}",
+ ProjectName = dmeName.Contains("rooted") ? dmeName : $"tests/DMAPI/{dmeName}",
RequireDMApiValidation = requireApi,
Timeout = TimeSpan.FromMilliseconds(1),
}, cancellationToken);
diff --git a/tests/Tgstation.Server.Tests/Live/LiveTestingServer.cs b/tests/Tgstation.Server.Tests/Live/LiveTestingServer.cs
index 0f725cf11d..5f78b526b1 100644
--- a/tests/Tgstation.Server.Tests/Live/LiveTestingServer.cs
+++ b/tests/Tgstation.Server.Tests/Live/LiveTestingServer.cs
@@ -63,6 +63,8 @@ namespace Tgstation.Server.Tests.Live
public bool HighPriorityDreamDaemon { get; }
public bool LowPriorityDeployments { get; }
+ public bool UsingBasicWatchdog { get; }
+
public bool RestartRequested => RealServer.RestartRequested;
readonly List args;
@@ -89,6 +91,8 @@ namespace Tgstation.Server.Tests.Live
var gitHubAccessToken = Environment.GetEnvironmentVariable("TGS_TEST_GITHUB_TOKEN");
var dumpOpenAPISpecPathEnvVar = Environment.GetEnvironmentVariable("TGS_TEST_DUMP_API_SPEC");
+ UsingBasicWatchdog = Boolean.TryParse(Environment.GetEnvironmentVariable("General__UseBasicWatchdog"), out var result) && result;
+
if (String.IsNullOrEmpty(DatabaseType))
Assert.Inconclusive("No database type configured in env var TGS_TEST_DATABASE_TYPE!");
diff --git a/tests/Tgstation.Server.Tests/Live/TestLiveServer.cs b/tests/Tgstation.Server.Tests/Live/TestLiveServer.cs
index 260a9c457d..0d8fec8488 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);
@@ -1326,8 +1323,6 @@ namespace Tgstation.Server.Tests.Live
async Task RunInstanceTests()
{
- // Some earlier linux BYOND versions have a critical bug where replacing the directory in non-basic watchdogs causes the DreamDaemon cwd to change
- var canRunCompatTests = new PlatformIdentifier().IsWindows;
var compatTests = FailFast(
instanceTest
.RunCompatTests(
@@ -1338,6 +1333,7 @@ namespace Tgstation.Server.Tests.Live
compatDMPort,
compatDDPort,
server.HighPriorityDreamDaemon,
+ server.UsingBasicWatchdog,
cancellationToken));
if (TestingUtils.RunningInGitHubActions) // they only have 2 cores, can't handle intense parallelization
@@ -1351,6 +1347,7 @@ namespace Tgstation.Server.Tests.Live
mainDDPort,
server.HighPriorityDreamDaemon,
server.LowPriorityDeployments,
+ server.UsingBasicWatchdog,
cancellationToken));
await compatTests;
@@ -1469,7 +1466,7 @@ namespace Tgstation.Server.Tests.Live
await WatchdogTest.TellWorldToReboot2(instanceClient, WatchdogTest.StaticTopicClient, mainDDPort, cancellationToken);
dd = await instanceClient.DreamDaemon.Read(cancellationToken);
- Assert.AreEqual(WatchdogStatus.Online, dd.Status.Value);
+ Assert.AreEqual(WatchdogStatus.Online, dd.Status.Value); // if this assert fails, you likely have to crack open the debugger and read test_fail_reason.txt manually
Assert.IsNull(dd.StagedCompileJob);
Assert.AreEqual(initialStaged, dd.ActiveCompileJob.Id);
@@ -1528,7 +1525,7 @@ namespace Tgstation.Server.Tests.Live
Assert.AreEqual(WatchdogStatus.Online, dd.Status.Value);
var compileJob = await instanceClient.DreamMaker.Compile(cancellationToken);
- var wdt = new WatchdogTest(edgeByond, instanceClient, GetInstanceManager(), (ushort)server.Url.Port, server.HighPriorityDreamDaemon, mainDDPort);
+ var wdt = new WatchdogTest(edgeByond, instanceClient, GetInstanceManager(), (ushort)server.Url.Port, server.HighPriorityDreamDaemon, mainDDPort, server.UsingBasicWatchdog);
await wdt.WaitForJob(compileJob, 30, false, null, cancellationToken);
dd = await instanceClient.DreamDaemon.Read(cancellationToken);
@@ -1575,7 +1572,7 @@ namespace Tgstation.Server.Tests.Live
Assert.AreEqual(WatchdogStatus.Online, currentDD.Status);
Assert.AreEqual(expectedStaged, currentDD.StagedCompileJob.Job.Id.Value);
- var wdt = new WatchdogTest(edgeByond, instanceClient, GetInstanceManager(), (ushort)server.Url.Port, server.HighPriorityDreamDaemon, mainDDPort);
+ var wdt = new WatchdogTest(edgeByond, instanceClient, GetInstanceManager(), (ushort)server.Url.Port, server.HighPriorityDreamDaemon, mainDDPort, server.UsingBasicWatchdog);
currentDD = await wdt.TellWorldToReboot(cancellationToken);
Assert.AreEqual(expectedStaged, currentDD.ActiveCompileJob.Job.Id.Value);
Assert.IsNull(currentDD.StagedCompileJob);
diff --git a/tgstation-server.sln b/tgstation-server.sln
index 6d183435b8..c1cde46821 100644
--- a/tgstation-server.sln
+++ b/tgstation-server.sln
@@ -157,6 +157,8 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "LongRunning", "LongRunning"
tests\DMAPI\LongRunning\Config.dm = tests\DMAPI\LongRunning\Config.dm
tests\DMAPI\LongRunning\long_running_test.dme = tests\DMAPI\LongRunning\long_running_test.dme
tests\DMAPI\LongRunning\long_running_test_copy.dme = tests\DMAPI\LongRunning\long_running_test_copy.dme
+ tests\DMAPI\LongRunning\long_running_test_rooted.dme = tests\DMAPI\LongRunning\long_running_test_rooted.dme
+ tests\DMAPI\LongRunning\resource.txt = tests\DMAPI\LongRunning\resource.txt
tests\DMAPI\LongRunning\Test.dm = tests\DMAPI\LongRunning\Test.dm
EndProjectSection
EndProject