Merge remote-tracking branch 'upstream/dev' into rinfo

This commit is contained in:
ZephyrTFA
2024-05-09 16:14:18 -04:00
175 changed files with 12553 additions and 748 deletions
+5 -5
View File
@@ -34,7 +34,7 @@ You can of course, as always, ask for help at [#coderbus](irc://irc.rizon.net/co
### Development Environment
You need the .NET 8.0 SDK, node>=v20, and npm>=v5.7 (in your PATH) to compile the server.
You need the .NET 8.0 SDK, node>=v20, and npm>=v5.7 (in your PATH) to compile the server. On Linux, you also need the `libgdiplus` package installed to generate icons.
The recommended IDE is Visual Studio 2022 or VSCode.
@@ -42,7 +42,8 @@ In order to build the service version and/or the Windows installer you need a to
In addition, the installer project uses the Wix v4 Toolset which will cause an error on loading the .sln in Visual Studio if the [HeatWave for VS2022 Extension](https://marketplace.visualstudio.com/items?itemName=FireGiant.FireGiantHeatWaveDev17) is not installed.
In order to run the integration tests you must have the following environment variables set. To run them more accurately, include the optional ones.
In order to run the integration tests you must have the dotnet 7.0 SDK installed to properly build the OpenDream minimum compatible version.
You must also have the following environment variables set. To run them more accurately, include the optional ones.
- `TGS_TEST_DATABASE_TYPE`: `MySql`, `MariaDB`, `PostgresSql`, or `SqlServer`.
- `TGS_TEST_CONNECTION_STRING`: To a valid database connection string. You can use the setup wizard to create one.
- (Optional) `TGS_TEST_GITHUB_TOKEN`: A GitHub personal access token with no scopes used to bypass rate limits.
@@ -59,10 +60,9 @@ In order to run the integration tests you must have the following environment va
For the full CI gambit, the following repository configuration must be set:
- Setting `Workflow Permissions` to `Read and write permissions`: Enables CodeQL uploads and GitHub Actions comments.
- Setting `Workflow Permissions` to `Read and write permissions`: Enables GitHub Actions comments.
![image](https://github.com/tgstation/tgstation-server/assets/8171642/ab17fa74-364f-4e66-b7c4-b9bb24c6a599)
- Label `CI Cleared`: To allow PRs from forks to run CI with secrets after approval.
- Variable `TGS_ENABLE_CODE_QL` to `true`: Enables CodeQL scanning in actions.
- Integration [CodeCov](https://github.com/apps/codecov): Enables CodeCov status checks.
- Secret `CODECOV_TOKEN`: A CodeCov repo token to work around https://github.com/codecov/codecov-action/issues/837.
- Secret `LIVE_TESTS_TOKEN`: A GitHub token with read access to the repository and write access to https://github.com/Cyberboss/common_core (TODO: Make the target repository here configurable). Despite it's name, it may be used across the entire test suite.
@@ -73,7 +73,7 @@ For the full CI gambit, the following repository configuration must be set:
If you don't plan on deploying TGS, the following secrets can be omitted:
- Secret `DEV_PUSH_TOKEN`: A GitHub token with write access to the repository. Enables doxygen pushes to `gh-pages` branch, and releases creation.
- Secret `DEV_PUSH_TOKEN`: A GitHub token with read/write access to the repository. Enables doxygen pushes to `gh-pages` branch, and releases creation.
- Secret `DOCKER_USERNAME`: Login username for Docker image push.
- Secret `DOCKER_PASSWORD`: Login password for Docker image push.
- Secret `NUGET_API_KEY`: Nuget.org API Key for client libraries push.
+205 -102
View File
@@ -37,7 +37,8 @@ on:
env:
TGS_DOTNET_VERSION: 8
OD_DOTNET_VERSION: 7
OD_MIN_COMPAT_DOTNET_VERSION: 7
OD_DOTNET_VERSION: 8
TGS_DOTNET_QUALITY: ga
TGS_TEST_GITHUB_TOKEN: ${{ secrets.LIVE_TESTS_TOKEN }}
TGS_RELEASE_NOTES_TOKEN: ${{ secrets.DEV_PUSH_TOKEN }}
@@ -53,20 +54,35 @@ jobs:
security-checkpoint:
name: Check CI Clearance
runs-on: ubuntu-latest
permissions:
pull-requests: write
if: github.event_name == 'pull_request_target' && github.event.pull_request.head.repo.id != github.event.pull_request.base.repo.id && github.event.pull_request.state == 'open'
steps:
- name: Comment on new Fork PR
if: github.event.action == 'opened' && !contains(github.event.pull_request.labels.*.name, 'CI Cleared')
uses: thollander/actions-comment-pull-request@v2
uses: thollander/actions-comment-pull-request@1d3973dc4b8e1399c0620d3f2b1aa5e795465308
with:
message: Thank you for contributing to tgstation-server! As this pull request is from a fork, we can't allow the CI actions which require repository secrets to run on it without approval. After a brief review to make sure you're not misusing those secrets, a maintainer will add the `CI Cleared` label to allow the CI suite to run. Maintainers, please note that any changes to workflow files will not be reflected in the CI run.
message: Thank you for contributing to ${{ github.event.pull_request.base.repo.name }}! The workflow '${{ github.workflow }}' requires repository secrets and will not run without approval. Maintainers can add the `CI Cleared` label to allow it to run. Please note that any changes to the workflow file will not be reflected in the run.
- name: "Remove Stale 'CI Cleared' Label"
if: github.event.action == 'synchronize' || github.event.action == 'reopened'
uses: actions-ecosystem/action-remove-labels@v1
uses: actions-ecosystem/action-remove-labels@2ce5d41b4b6aa8503e285553f75ed56e0a40bae0
with:
labels: CI Cleared
- name: "Add 'CI Approval Required' Label"
if: (github.event.action == 'synchronize' || github.event.action == 'reopened') || ((github.event.action == 'opened' || github.event.action == 'labeled') && !contains(github.event.pull_request.labels.*.name, 'CI Cleared'))
uses: actions-ecosystem/action-add-labels@bd52874380e3909a1ac983768df6976535ece7f8
with:
labels: CI Approval Required
github_token: ${{ github.token }}
- name: "Remove 'CI Approval Required' Label"
if: (github.event.action == 'synchronize' || github.event.action == 'reopened') || ((github.event.action == 'opened' || github.event.action == 'labeled') && !contains(github.event.pull_request.labels.*.name, 'CI Cleared'))
uses: actions-ecosystem/action-remove-labels@2ce5d41b4b6aa8503e285553f75ed56e0a40bae0
with:
labels: CI Approval Required
- name: Fail Clearance Check if PR has Unlabeled new Commits from Fork
if: (github.event.action == 'synchronize' || github.event.action == 'reopened') || ((github.event.action == 'opened' || github.event.action == 'labeled') && !contains(github.event.pull_request.labels.*.name, 'CI Cleared'))
run: exit 1
@@ -80,6 +96,44 @@ jobs:
- name: GitHub Requires at Least One Step for a Job
run: exit 0
code-scanning:
name: Code Scanning
needs: start-ci-run-gate
runs-on: ubuntu-latest
permissions:
security-events: write
actions: read
if: (!(cancelled() || failure()) && needs.start-ci-run-gate.result == 'success')
steps:
- name: Setup dotnet
uses: actions/setup-dotnet@v4
with:
dotnet-version: '${{ env.TGS_DOTNET_VERSION }}.0.x'
dotnet-quality: ${{ env.TGS_DOTNET_QUALITY }}
- name: Checkout (Branch)
uses: actions/checkout@v4
if: github.event_name == 'push' || github.event_name == 'schedule'
- name: Checkout (PR Merge)
uses: actions/checkout@v4
if: github.event_name != 'push' && github.event_name != 'schedule'
with:
ref: "refs/pull/${{ github.event.number }}/merge"
- name: Initialize CodeQL
uses: github/codeql-action/init@v3
with:
languages: csharp
- name: Build
run: dotnet build -c ReleaseNoWindows -p:TGS_HOST_NO_WEBPANEL=true
- name: Perform CodeQL Analysis
uses: github/codeql-action/analyze@v3
with:
category: "/language:csharp"
dmapi-build:
name: Build DMAPI
needs: start-ci-run-gate
@@ -97,7 +151,7 @@ jobs:
sudo apt-get install -y -o APT::Immediate-Configure=0 libc6-i386 libstdc++6:i386 libgcc-s1:i386
- name: Cache BYOND .zips
uses: actions/cache@v3
uses: actions/cache@v4
id: cache-byond
with:
path: ~/byond-zips-cache
@@ -174,11 +228,19 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Setup dotnet
uses: actions/setup-dotnet@v3
uses: actions/setup-dotnet@v4
if: matrix.committish == 'master'
with:
dotnet-version: '${{ env.OD_DOTNET_VERSION }}.0.x'
dotnet-quality: ${{ env.TGS_DOTNET_QUALITY }}
- name: Setup dotnet (min-compat)
uses: actions/setup-dotnet@v4
if: matrix.committish == 'tgs-min-compat'
with:
dotnet-version: '${{ env.OD_MIN_COMPAT_DOTNET_VERSION }}.0.x'
dotnet-quality: ${{ env.TGS_DOTNET_QUALITY }}
- name: Checkout (Branch)
uses: actions/checkout@v4
if: github.event_name == 'push' || github.event_name == 'schedule'
@@ -197,10 +259,20 @@ jobs:
git checkout ${{ matrix.committish }}
git submodule update --init --recursive
- name: Restore OpenDream
run: |
cd $HOME/OpenDream
dotnet restore
- name: Build OpenDream
run: |
cd $HOME/OpenDream/OpenDreamPackageTool
dotnet build -c Release --nologo -v q --property WarningLevel=0 /clp:ErrorsOnly
- name: Create TGS Deployment
run: |
cd $HOME/OpenDream
dotnet run -c Release --project OpenDreamPackageTool --property WarningLevel=0 -- --tgs -o tgs_deploy
dotnet run -c Release --project OpenDreamPackageTool --no-build -- --tgs -o tgs_deploy
- name: Build DMAPI
run: |
@@ -214,7 +286,7 @@ jobs:
if: (!(cancelled() || failure()) && needs.start-ci-run-gate.result == 'success')
steps:
- name: Setup dotnet
uses: actions/setup-dotnet@v3
uses: actions/setup-dotnet@v4
with:
dotnet-version: '${{ env.TGS_DOTNET_VERSION }}.0.x'
dotnet-quality: ${{ env.TGS_DOTNET_QUALITY }}
@@ -246,7 +318,7 @@ jobs:
- name: Patch Doxyfile
run: |
VERSION=$(cat "build/Version.props" | grep -oPm1 "(?<=<TgsCoreVersion>)[^<]+")
echo -e "\nPROJECT_NUMBER = $VERSION\nINPUT = .\nOUTPUT_DIRECTORY = ./doxout\nPROJECT_LOGO = ./build/tgs.ico\nHAVE_DOT=YES" >> "docs/Doxyfile"
echo -e "\nPROJECT_NUMBER = $VERSION\nINPUT = .\nOUTPUT_DIRECTORY = ./doxout\nPROJECT_LOGO = ./artifacts/tgs.ico\nHAVE_DOT=YES" >> "docs/Doxyfile"
- name: Doxygen Build
uses: mattnotmitt/doxygen-action@411df0c62acb5b96b8a93d93a7bf4b753c47ea05 # v1.9.5
@@ -313,10 +385,10 @@ jobs:
run: |
sudo dpkg --add-architecture i386
sudo apt-get update
sudo apt-get install -y -o APT::Immediate-Configure=0 libc6-i386 libstdc++6:i386 libgcc-s1:i386
sudo apt-get install -y -o APT::Immediate-Configure=0 libc6-i386 libstdc++6:i386 libgcc-s1:i386 libgdiplus
- name: Setup dotnet
uses: actions/setup-dotnet@v3
uses: actions/setup-dotnet@v4
with:
dotnet-version: '${{ env.TGS_DOTNET_VERSION }}.0.x'
dotnet-quality: ${{ env.TGS_DOTNET_QUALITY }}
@@ -338,7 +410,7 @@ jobs:
run: dotnet build -c ${{ matrix.configuration }}NoWindows
- name: Cache BYOND .zips
uses: actions/cache@v3
uses: actions/cache@v4
id: cache-byond
with:
path: ~/byond-zips-cache
@@ -348,7 +420,7 @@ jobs:
run: sudo dotnet test --no-build --logger "GitHubActions;summary.includePassedTests=true;summary.includeSkippedTests=true" --filter TestCategory!=RequiresDatabase -c ${{ matrix.configuration }}NoWindows --collect:"XPlat Code Coverage" --settings build/ci.runsettings --results-directory ./TestResults tgstation-server.sln
- name: Store Code Coverage
uses: actions/upload-artifact@v3
uses: actions/upload-artifact@v4
with:
name: linux-unit-test-coverage-${{ matrix.configuration }}
path: ./TestResults/
@@ -367,7 +439,7 @@ jobs:
runs-on: windows-latest
steps:
- name: Setup dotnet
uses: actions/setup-dotnet@v3
uses: actions/setup-dotnet@v4
with:
dotnet-version: '${{ env.TGS_DOTNET_VERSION }}.0.x'
dotnet-quality: ${{ env.TGS_DOTNET_QUALITY }}
@@ -389,7 +461,7 @@ jobs:
run: dotnet build -c ${{ matrix.configuration }}NoWix
- name: Cache BYOND .zips
uses: actions/cache@v3
uses: actions/cache@v4
id: cache-byond
with:
path: ~/byond-zips-cache
@@ -399,7 +471,7 @@ jobs:
run: dotnet test --no-build --logger "GitHubActions;summary.includePassedTests=true;summary.includeSkippedTests=true" --filter TestCategory!=RequiresDatabase -c ${{ matrix.configuration }}NoWix --collect:"XPlat Code Coverage" --settings build/ci.runsettings --results-directory ./TestResults tgstation-server.sln
- name: Store Code Coverage
uses: actions/upload-artifact@v3
uses: actions/upload-artifact@v4
with:
name: windows-unit-test-coverage-${{ matrix.configuration }}
path: ./TestResults/
@@ -424,11 +496,12 @@ jobs:
sqlcmd -l 600 -S "(localdb)\MSSQLLocalDB" -Q "SELECT @@VERSION;"
- name: Setup dotnet
uses: actions/setup-dotnet@v3
uses: actions/setup-dotnet@v4
with:
dotnet-version: |
${{ env.TGS_DOTNET_VERSION }}.0.x
${{ env.OD_DOTNET_VERSION }}.0.x
${{ env.OD_MIN_COMPAT_DOTNET_VERSION }}.0.x
dotnet-quality: ${{ env.TGS_DOTNET_QUALITY }}
- name: Set TGS_TEST_DUMP_API_SPEC
@@ -504,7 +577,7 @@ jobs:
run: dotnet build -c ${{ matrix.configuration }} tests/Tgstation.Server.Tests/Tgstation.Server.Tests.csproj
- name: Cache BYOND .zips
uses: actions/cache@v3
uses: actions/cache@v4
id: cache-byond
with:
path: ~/byond-zips-cache
@@ -530,14 +603,14 @@ jobs:
- name: Store Live Tests Output
if: ${{ steps.live-tests.outputs.succeeded == 'YES' }}
uses: actions/upload-artifact@v3
uses: actions/upload-artifact@v4
with:
name: windows-integration-test-logs-${{ matrix.configuration }}-${{ matrix.watchdog-type }}-${{ matrix.database-type }}
path: ./test_output.txt
- name: Store Errored Live Tests Output
if: ${{ steps.live-tests.outputs.succeeded != 'YES' }}
uses: actions/upload-artifact@v3
uses: actions/upload-artifact@v4
with:
name: errored-windows-test-logs-${{ matrix.configuration }}-${{ matrix.watchdog-type }}-${{ matrix.database-type }}
path: ./test_output.txt
@@ -547,20 +620,20 @@ jobs:
run: exit 1
- name: Store Code Coverage
uses: actions/upload-artifact@v3
uses: actions/upload-artifact@v4
with:
name: windows-integration-test-coverage-${{ matrix.configuration }}-${{ matrix.watchdog-type }}-${{ matrix.database-type }}
path: ./TestResults/
- name: Store OpenAPI Spec
if: ${{ matrix.configuration == 'Release' && matrix.watchdog-type == 'Advanced' && matrix.database-type == 'SqlServer' }}
uses: actions/upload-artifact@v3
uses: actions/upload-artifact@v4
with:
name: openapi-spec
path: C:/tgs_api.json
- name: Package Server Service
if: ${{ matrix.configuration == 'Release' && matrix.watchdog-type == 'Basic' }}
if: ${{ matrix.configuration == 'Release' && matrix.watchdog-type == 'Basic' && matrix.database-type == 'PostgresSql' }}
run: |
cd src/Tgstation.Server.Host.Service
dotnet publish -c ${{ matrix.configuration }} -o ../../artifacts/Service
@@ -572,14 +645,14 @@ jobs:
build/RemoveUnsupportedServiceRuntimes.ps1 artifacts/Service
- name: Store Server Service
if: ${{ matrix.configuration == 'Release' && matrix.watchdog-type == 'Basic' }}
uses: actions/upload-artifact@v3
if: ${{ matrix.configuration == 'Release' && matrix.watchdog-type == 'Basic' && matrix.database-type == 'PostgresSql' }}
uses: actions/upload-artifact@v4
with:
name: ServerService
path: artifacts/Service/
- name: Install Code Signing Certificate
if: ${{ matrix.configuration == 'Release' && matrix.watchdog-type == 'Basic' }}
if: ${{ matrix.configuration == 'Release' && matrix.watchdog-type == 'Basic' && matrix.database-type == 'PostgresSql' }}
shell: powershell
run: |
$pfxBytes = [convert]::FromBase64String("${{ secrets.CODE_SIGNING_BASE64 }}")
@@ -589,7 +662,7 @@ jobs:
rm tg_codesigning.pfx
- name: Test Sign Service .exe
if: ${{ matrix.configuration == 'Release' && matrix.watchdog-type == 'Basic' }}
if: ${{ matrix.configuration == 'Release' && matrix.watchdog-type == 'Basic' && matrix.database-type == 'PostgresSql' }}
shell: powershell
run: Set-AuthenticodeSignature artifacts/Service/Tgstation.Server.Host.Service.exe -Certificate (Get-ChildItem Cert:\CurrentUser\My | Where-Object { $_.Thumbprint -eq "${{ vars.CODE_SIGNING_THUMBPRINT }}" }) -TimestampServer "http://timestamp.digicert.com"
@@ -653,14 +726,15 @@ jobs:
run: |
sudo dpkg --add-architecture i386
sudo apt-get update
sudo apt-get install -y -o APT::Immediate-Configure=0 libc6-i386 libstdc++6:i386 gdb libgcc-s1:i386
sudo apt-get install -y -o APT::Immediate-Configure=0 libc6-i386 libstdc++6:i386 gdb libgcc-s1:i386 libgdiplus
- name: Setup dotnet
uses: actions/setup-dotnet@v3
uses: actions/setup-dotnet@v4
with:
dotnet-version: |
${{ env.TGS_DOTNET_VERSION }}.0.x
${{ env.OD_DOTNET_VERSION }}.0.x
${{ env.OD_MIN_COMPAT_DOTNET_VERSION }}.0.x
dotnet-quality: ${{ env.TGS_DOTNET_QUALITY }}
- name: Set Sqlite Connection Info
@@ -709,7 +783,7 @@ jobs:
run: dotnet build -c ${{ matrix.configuration }}NoWindows tests/Tgstation.Server.Tests/Tgstation.Server.Tests.csproj
- name: Cache BYOND .zips
uses: actions/cache@v3
uses: actions/cache@v4
id: cache-byond
with:
path: ~/byond-zips-cache
@@ -722,7 +796,7 @@ jobs:
dotnet test -c ${{ matrix.configuration }}NoWindows --filter TestCategory=RequiresDatabase --logger "GitHubActions;summary.includePassedTests=true;summary.includeSkippedTests=true" --no-build --collect:"XPlat Code Coverage" --settings ../../build/ci.runsettings --results-directory ../../TestResults
- name: Store Code Coverage
uses: actions/upload-artifact@v3
uses: actions/upload-artifact@v4
with:
name: linux-integration-test-coverage-${{ matrix.configuration }}-${{ matrix.watchdog-type }}-${{ matrix.database-type }}
path: ./TestResults/
@@ -750,14 +824,14 @@ jobs:
- name: Store Server Console
if: ${{ matrix.configuration == 'Release' && matrix.watchdog-type == 'Advanced' && matrix.database-type == 'MariaDB' }}
uses: actions/upload-artifact@v3
uses: actions/upload-artifact@v4
with:
name: ServerConsole
path: artifacts/Console/
- name: Store Server Update Package
if: ${{ matrix.configuration == 'Release' && matrix.watchdog-type == 'Advanced' && matrix.database-type == 'PostgresSql' }}
uses: actions/upload-artifact@v3
uses: actions/upload-artifact@v4
with:
name: ServerUpdatePackage
path: artifacts/ServerUpdate/
@@ -782,7 +856,7 @@ jobs:
ref: "refs/pull/${{ github.event.number }}/merge"
- name: Retrieve OpenAPI Spec
uses: actions/download-artifact@v3
uses: actions/download-artifact@v4
with:
name: openapi-spec
path: ./swagger
@@ -807,235 +881,235 @@ jobs:
ref: "refs/pull/${{ github.event.number }}/merge"
- name: Retrieve Linux Unit Test Coverage (Debug)
uses: actions/download-artifact@v3
uses: actions/download-artifact@v4
with:
name: linux-unit-test-coverage-Debug
path: ./code_coverage/unit_tests/linux_unit_tests_debug
- name: Retrieve Linux Unit Test Coverage (Release)
uses: actions/download-artifact@v3
uses: actions/download-artifact@v4
with:
name: linux-unit-test-coverage-Release
path: ./code_coverage/unit_tests/linux_unit_tests_release
- name: Retrieve Linux Integration Test Coverage (Release, Advanced, Sqlite)
uses: actions/download-artifact@v3
uses: actions/download-artifact@v4
with:
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, Advanced, PostgresSql)
uses: actions/download-artifact@v3
uses: actions/download-artifact@v4
with:
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, Advanced, MariaDB)
uses: actions/download-artifact@v3
uses: actions/download-artifact@v4
with:
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, Advanced, MySql)
uses: actions/download-artifact@v3
uses: actions/download-artifact@v4
with:
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
uses: actions/download-artifact@v4
with:
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
uses: actions/download-artifact@v4
with:
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
uses: actions/download-artifact@v4
with:
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
uses: actions/download-artifact@v4
with:
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, Advanced, Sqlite)
uses: actions/download-artifact@v3
uses: actions/download-artifact@v4
with:
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, Advanced, PostgresSql)
uses: actions/download-artifact@v3
uses: actions/download-artifact@v4
with:
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, Advanced, MariaDB)
uses: actions/download-artifact@v3
uses: actions/download-artifact@v4
with:
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, Advanced, MySql)
uses: actions/download-artifact@v3
uses: actions/download-artifact@v4
with:
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
uses: actions/download-artifact@v4
with:
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
uses: actions/download-artifact@v4
with:
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
uses: actions/download-artifact@v4
with:
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
uses: actions/download-artifact@v4
with:
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)
uses: actions/download-artifact@v3
uses: actions/download-artifact@v4
with:
name: windows-unit-test-coverage-Release
path: ./code_coverage/unit_tests/windows_unit_tests_release
- name: Retrieve Windows Integration Test Coverage (Debug, Basic, SqlServer)
uses: actions/download-artifact@v3
uses: actions/download-artifact@v4
with:
name: windows-integration-test-coverage-Debug-Basic-SqlServer
path: ./code_coverage/integration_tests/windows_integration_tests_debug_basic_sqlserver
- name: Retrieve Windows Integration Test Coverage (Release, Basic, SqlServer)
uses: actions/download-artifact@v3
uses: actions/download-artifact@v4
with:
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, Advanced, SqlServer)
uses: actions/download-artifact@v3
uses: actions/download-artifact@v4
with:
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, Advanced, SqlServer)
uses: actions/download-artifact@v3
uses: actions/download-artifact@v4
with:
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)
uses: actions/download-artifact@v3
uses: actions/download-artifact@v4
with:
name: windows-integration-test-coverage-Debug-Basic-MariaDB
path: ./code_coverage/integration_tests/windows_integration_tests_debug_basic_mariadb
- name: Retrieve Windows Integration Test Coverage (Release, Basic, MariaDB)
uses: actions/download-artifact@v3
uses: actions/download-artifact@v4
with:
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, Advanced, MariaDB)
uses: actions/download-artifact@v3
uses: actions/download-artifact@v4
with:
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, Advanced, MariaDB)
uses: actions/download-artifact@v3
uses: actions/download-artifact@v4
with:
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)
uses: actions/download-artifact@v3
uses: actions/download-artifact@v4
with:
name: windows-integration-test-coverage-Debug-Basic-MySql
path: ./code_coverage/integration_tests/windows_integration_tests_debug_basic_mysql
- name: Retrieve Windows Integration Test Coverage (Release, Basic, MySql)
uses: actions/download-artifact@v3
uses: actions/download-artifact@v4
with:
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, Advanced, MySql)
uses: actions/download-artifact@v3
uses: actions/download-artifact@v4
with:
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, Advanced, MySql)
uses: actions/download-artifact@v3
uses: actions/download-artifact@v4
with:
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)
uses: actions/download-artifact@v3
uses: actions/download-artifact@v4
with:
name: windows-integration-test-coverage-Debug-Basic-PostgresSql
path: ./code_coverage/integration_tests/windows_integration_tests_debug_basic_postgressql
- name: Retrieve Windows Integration Test Coverage (Release, Basic, PostgresSql)
uses: actions/download-artifact@v3
uses: actions/download-artifact@v4
with:
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, Advanced, PostgresSql)
uses: actions/download-artifact@v3
uses: actions/download-artifact@v4
with:
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, Advanced, PostgresSql)
uses: actions/download-artifact@v3
uses: actions/download-artifact@v4
with:
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)
uses: actions/download-artifact@v3
uses: actions/download-artifact@v4
with:
name: windows-integration-test-coverage-Debug-Basic-Sqlite
path: ./code_coverage/integration_tests/windows_integration_tests_debug_basic_sqlite
- name: Retrieve Windows Integration Test Coverage (Release, Basic, Sqlite)
uses: actions/download-artifact@v3
uses: actions/download-artifact@v4
with:
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, Advanced, Sqlite)
uses: actions/download-artifact@v3
uses: actions/download-artifact@v4
with:
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, Advanced, Sqlite)
uses: actions/download-artifact@v3
uses: actions/download-artifact@v4
with:
name: windows-integration-test-coverage-Release-Advanced-Sqlite
path: ./code_coverage/integration_tests/windows_integration_tests_release_system_sqlite
@@ -1056,7 +1130,7 @@ jobs:
run: |
sudo dpkg --add-architecture i386
sudo apt-get update
sudo apt-get install -y -o APT::Immediate-Configure=0 libstdc++6:i386 libgcc-s1:i386 gnupg2 xmlstarlet
sudo apt-get install -y -o APT::Immediate-Configure=0 libstdc++6:i386 libgcc-s1:i386 gnupg2 xmlstarlet libgdiplus
- name: Import GPG Key
if: (github.event_name == 'push' && contains(github.event.head_commit.message, '[TGSDeploy]') && (github.event.ref == 'refs/heads/master' || github.event.ref == 'refs/heads/dev'))
@@ -1072,7 +1146,7 @@ jobs:
sudo apt-get install -y dotnet-sdk-${{ env.TGS_DOTNET_VERSION }}.0
- name: Setup dotnet
uses: actions/setup-dotnet@v3
uses: actions/setup-dotnet@v4
with:
dotnet-version: '${{ env.TGS_DOTNET_VERSION }}.0.x'
dotnet-quality: ${{ env.TGS_DOTNET_QUALITY }}
@@ -1137,7 +1211,7 @@ jobs:
sleep 10
sudo apt-get remove -y tgstation-server
if [[ -d "/opt/tgstation-server" ]]; then
ls -al /opt/tgstation-server
find /opt/tgstation-server
exit 2
fi
@@ -1145,7 +1219,7 @@ jobs:
run: tar cfJ tgstation-server-v${{ env.TGS_VERSION }}.debian.packaging.tar.xz tgstation-server_*
- name: Upload Packaging Archive
uses: actions/upload-artifact@v3
uses: actions/upload-artifact@v4
with:
name: packaging-debian
path: tgstation-server-v${{ env.TGS_VERSION }}.debian.packaging.tar.xz
@@ -1162,7 +1236,7 @@ jobs:
GITHUB_TOKEN: ${{ env.WINGET_PUSH_TOKEN }}
- name: Setup dotnet
uses: actions/setup-dotnet@v3
uses: actions/setup-dotnet@v4
with:
dotnet-version: '${{ env.TGS_DOTNET_VERSION }}.0.x'
dotnet-quality: ${{ env.TGS_DOTNET_QUALITY }}
@@ -1276,7 +1350,7 @@ jobs:
}
- name: Upload Unsigned Installer .exe
uses: actions/upload-artifact@v3
uses: actions/upload-artifact@v4
with:
name: packaging-preview-windows
path: build/package/winget/Tgstation.Server.Host.Service.Wix.Bundle/bin/Release/tgstation-server-installer.exe
@@ -1288,7 +1362,7 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Setup dotnet
uses: actions/setup-dotnet@v3
uses: actions/setup-dotnet@v4
with:
dotnet-version: '${{ env.TGS_DOTNET_VERSION }}.0.x'
dotnet-quality: ${{ env.TGS_DOTNET_QUALITY }}
@@ -1320,12 +1394,39 @@ jobs:
ci-completion-gate: # This job exists so there isn't a moving target for branch protections
name: CI Completion Gate
needs: [ pages-build, docker-build, build-deb, build-msi, validate-openapi-spec, upload-code-coverage, check-winget-pr-template ]
needs: [ pages-build, docker-build, build-deb, build-msi, validate-openapi-spec, upload-code-coverage, check-winget-pr-template, code-scanning ]
runs-on: ubuntu-latest
if: (!(cancelled() || failure()) && needs.pages-build.result == 'success' && needs.docker-build.result == 'success' && needs.build-deb.result == 'success' && needs.build-msi.result == 'success' && needs.validate-openapi-spec.result == 'success' && needs.upload-code-coverage.result == 'success' && needs.check-winget-pr-template.result == 'success')
if: (!(cancelled() || failure()) && needs.pages-build.result == 'success' && needs.docker-build.result == 'success' && needs.build-deb.result == 'success' && needs.build-msi.result == 'success' && needs.validate-openapi-spec.result == 'success' && needs.upload-code-coverage.result == 'success' && needs.check-winget-pr-template.result == 'success' && needs.code-scanning.result == 'success')
steps:
- name: GitHub Requires at Least One Step for a Job
run: exit 0
- name: Setup dotnet
uses: actions/setup-dotnet@v4
with:
dotnet-version: '${{ env.TGS_DOTNET_VERSION }}.0.x'
dotnet-quality: ${{ env.TGS_DOTNET_QUALITY }}
- name: Checkout (Branch)
uses: actions/checkout@v4
if: github.event_name == 'push' || github.event_name == 'schedule'
- name: Checkout (PR Merge)
uses: actions/checkout@v4
if: github.event_name != 'push' && github.event_name != 'schedule'
with:
ref: "refs/pull/${{ github.event.number }}/merge"
- name: Restore
run: dotnet restore
- name: Build ReleaseNotes
run: dotnet build -c Release -p:TGS_HOST_NO_WEBPANEL=true tools/Tgstation.Server.ReleaseNotes/Tgstation.Server.ReleaseNotes.csproj
- name: Run ReleaseNotes Create CI Completion Check (PR HEAD)
if: github.event_name != 'push' && github.event_name != 'schedule'
run: dotnet run -c Release --no-build --project tools/Tgstation.Server.ReleaseNotes --ci-completion-check ${{ github.event.pull_request.head.sha }} ${{ secrets.TGS_CI_GITHUB_APP_TOKEN_BASE64 }}
- name: Run ReleaseNotes Create CI Completion Check (Branch)
if: github.event_name == 'push' || github.event_name == 'schedule'
run: dotnet run -c Release --no-build --project tools/Tgstation.Server.ReleaseNotes --ci-completion-check ${{ github.sha }} ${{ secrets.TGS_CI_GITHUB_APP_TOKEN_BASE64 }}
deployment-gate:
name: Deployment Start Gate
@@ -1343,7 +1444,7 @@ jobs:
if: (!(cancelled() || failure()) && needs.deployment-gate.result == 'success' && contains(github.event.head_commit.message, '[APIDeploy]'))
steps:
- name: Setup dotnet
uses: actions/setup-dotnet@v3
uses: actions/setup-dotnet@v4
with:
dotnet-version: '${{ env.TGS_DOTNET_VERSION }}.0.x'
dotnet-quality: ${{ env.TGS_DOTNET_QUALITY }}
@@ -1365,7 +1466,7 @@ jobs:
echo "TGS_API_VERSION=$apiVersion" | Out-File -FilePath $Env:GITHUB_ENV -Encoding utf8 -Append
- name: Retrieve OpenAPI Spec
uses: actions/download-artifact@v3
uses: actions/download-artifact@v4
with:
name: openapi-spec
path: swagger
@@ -1407,7 +1508,7 @@ jobs:
if: (!(cancelled() || failure()) && needs.deployment-gate.result == 'success' && contains(github.event.head_commit.message, '[DMDeploy]'))
steps:
- name: Setup dotnet
uses: actions/setup-dotnet@v3
uses: actions/setup-dotnet@v4
with:
dotnet-version: '${{ env.TGS_DOTNET_VERSION }}.0.x'
dotnet-quality: ${{ env.TGS_DOTNET_QUALITY }}
@@ -1470,7 +1571,7 @@ jobs:
if: (!(cancelled() || failure()) && needs.deployment-gate.result == 'success' && contains(github.event.head_commit.message, '[NugetDeploy]'))
steps:
- name: Setup dotnet
uses: actions/setup-dotnet@v3
uses: actions/setup-dotnet@v4
with:
dotnet-version: '${{ env.TGS_DOTNET_VERSION }}.0.x'
dotnet-quality: ${{ env.TGS_DOTNET_QUALITY }}
@@ -1521,7 +1622,7 @@ jobs:
if: (!(cancelled() || failure()) && (needs.deploy-dm.result == 'success' || needs.deploy-http.result == 'success') && !contains(github.event.head_commit.message, '[TGSDeploy]'))
steps:
- name: Setup dotnet
uses: actions/setup-dotnet@v3
uses: actions/setup-dotnet@v4
with:
dotnet-version: '${{ env.TGS_DOTNET_VERSION }}.0.x'
dotnet-quality: ${{ env.TGS_DOTNET_QUALITY }}
@@ -1545,7 +1646,7 @@ jobs:
if: (!(cancelled() || failure()) && needs.deployment-gate.result == 'success' && github.event.ref == 'refs/heads/master' && contains(github.event.head_commit.message, '[TGSDeploy]'))
steps:
- name: Setup dotnet
uses: actions/setup-dotnet@v3
uses: actions/setup-dotnet@v4
with:
dotnet-version: '${{ env.TGS_DOTNET_VERSION }}.0.x'
dotnet-quality: ${{ env.TGS_DOTNET_QUALITY }}
@@ -1591,37 +1692,37 @@ jobs:
echo "MARIADB_VERSION=$mariaDBVerison" | Out-File -FilePath $Env:GITHUB_ENV -Encoding utf8 -Append
- name: Upload .msi
uses: actions/upload-artifact@v3
uses: actions/upload-artifact@v4
with:
name: packaging-windows-raw-msi
path: build/package/winget/Tgstation.Server.Host.Service.Wix/bin/Release/en-US/tgstation-server.msi
- name: Retrieve Server Service
uses: actions/download-artifact@v3
uses: actions/download-artifact@v4
with:
name: ServerService
path: ServerService
- name: Retrieve Server Console
uses: actions/download-artifact@v3
uses: actions/download-artifact@v4
with:
name: ServerConsole
path: ServerConsole
- name: Retrieve Server Update Package
uses: actions/download-artifact@v3
uses: actions/download-artifact@v4
with:
name: ServerUpdatePackage
path: ServerUpdatePackage
- name: Retrieve OpenAPI Spec
uses: actions/download-artifact@v3
uses: actions/download-artifact@v4
with:
name: openapi-spec
path: swagger
- name: Retrieve Debian Packaging Archive
uses: actions/download-artifact@v3
uses: actions/download-artifact@v4
with:
name: packaging-debian
path: packaging-debian
@@ -1757,7 +1858,7 @@ jobs:
if: (!(cancelled() || failure()) && needs.deploy-tgs.result == 'success')
steps:
- name: Setup dotnet
uses: actions/setup-dotnet@v3
uses: actions/setup-dotnet@v4
with:
dotnet-version: '${{ env.TGS_DOTNET_VERSION }}.0.x'
dotnet-quality: ${{ env.TGS_DOTNET_QUALITY }}
@@ -1845,7 +1946,7 @@ jobs:
runs-on: windows-latest
steps:
- name: Setup dotnet
uses: actions/setup-dotnet@v3
uses: actions/setup-dotnet@v4
with:
dotnet-version: '${{ env.TGS_DOTNET_VERSION }}.0.x'
dotnet-quality: ${{ env.TGS_DOTNET_QUALITY }}
@@ -1865,7 +1966,7 @@ jobs:
run: dotnet build -c Release -p:TGS_HOST_NO_WEBPANEL=true tools/Tgstation.Server.ReleaseNotes
- name: Retrieve Server Service
uses: actions/download-artifact@v3
uses: actions/download-artifact@v4
with:
name: packaging-windows-raw-msi
path: artifacts
@@ -1874,8 +1975,10 @@ jobs:
shell: powershell
run: build/package/winget/push_manifest.ps1
- name: Delay 10m to allow MS bot to update PR
shell: powershell
run: Sleep 600
- name: Run ReleaseNotes with --link-winget
shell: powershell
run: |
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 }}
run: dotnet run -c Release --no-build --project tools/Tgstation.Server.ReleaseNotes --link-winget ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}
-51
View File
@@ -1,51 +0,0 @@
name: 'Code Scanning'
on:
push:
branches:
- dev
- master
pull_request:
branches:
- dev
- master
env:
TGS_DOTNET_VERSION: 8
TGS_DOTNET_QUALITY: ga
concurrency:
group: "code-scanning-${{ github.head_ref || github.run_id }}-${{ github.event_name }}"
cancel-in-progress: true
jobs:
analyze:
name: Code Scanning
runs-on: ubuntu-latest
permissions:
actions: read
contents: read
security-events: write
if: ${{ vars.TGS_ENABLE_CODE_QL }} == 'true'
steps:
- name: Setup dotnet
uses: actions/setup-dotnet@v3
with:
dotnet-version: '${{ env.TGS_DOTNET_VERSION }}.0.x'
dotnet-quality: ${{ env.TGS_DOTNET_QUALITY }}
- name: Checkout
uses: actions/checkout@v4
- name: Initialize CodeQL
uses: github/codeql-action/init@v2
with:
languages: csharp
- name: Build
run: dotnet build -c ReleaseNoWindows -p:TGS_HOST_NO_WEBPANEL=true
- name: Perform CodeQL Analysis
uses: github/codeql-action/analyze@v2
with:
category: "/language:csharp"
+21
View File
@@ -0,0 +1,21 @@
name: Rerun Flaky Live Tests
on:
workflow_run:
workflows: [CI Pipeline]
types:
- completed
jobs:
rerun_flaky_tests:
name: Rerun Flaky Tests
runs-on: ubuntu-latest
if: ${{ github.event.workflow_run.conclusion == 'failure' && github.event.workflow_run.run_attempt == 1 }}
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Rerun flaky tests
uses: actions/github-script@v6
with:
script: |
const { rerunFlakyTests } = await import('${{ github.workspace }}/.github/workflows/scripts/rerunFlakyTests.js')
await rerunFlakyTests({ github, context })
+3
View File
@@ -0,0 +1,3 @@
{
"type": "module"
}
@@ -0,0 +1,55 @@
// Only check jobs that start with these.
// Helps make sure we don't restart something like which is not known to be flaky.
const CONSIDERED_JOBS = [
"Windows Live Tests",
"Linux Live Tests",
"Build .deb Package"
];
async function getFailedJobsForRun(github, context, workflowRunId, runAttempt) {
const jobs = await github.paginate(
github.rest.actions.listJobsForWorkflowRunAttempt,
{
owner: context.repo.owner,
repo: context.repo.repo,
run_id: workflowRunId,
attempt_number: runAttempt
},
response => {
return response.data;
});
return jobs
.filter((job) => job.conclusion === "failure");
}
export async function rerunFlakyTests({ github, context }) {
const failingJobs = await getFailedJobsForRun(
github,
context,
context.payload.workflow_run.id,
context.payload.workflow_run.run_attempt
);
if (failingJobs.length > 1) {
console.log("Multiple jobs failing. PROBABLY not flaky, not rerunning.");
return;
}
const filteredFailingJobs = failingJobs.filter((job) => {
console.log(`Failing job: ${job.name}`)
return CONSIDERED_JOBS.some((title) => job.name.startsWith(title));
});
if (filteredFailingJobs.length === 0) {
console.log("Failing jobs are NOT designated flaky. Not rerunning.");
return;
}
console.log(`Rerunning job: ${filteredFailingJobs[0].name}`);
github.rest.actions.reRunWorkflowFailedJobs({
owner: context.repo.owner,
repo: context.repo.repo,
run_id: context.payload.workflow_run.id,
});
}
+9 -7
View File
@@ -1,21 +1,23 @@
name: 'Master Merge'
on:
push:
branches:
- master
workflow_run:
workflows: [CI Pipeline]
types:
- completed
jobs:
master-merge:
runs-on: ubuntu-latest
if: ${{ github.event.workflow_run.conclusion == 'success' }}
steps:
- name: Checkout
uses: actions/checkout@v1
uses: actions/checkout@v4
with:
fetch-depth: 0
- name: Merge master into dev
uses: robotology/gh-action-nightly-merge@22f5e45d028f22837d617fa07512925457eec184 #v1.3.3
uses: robotology/gh-action-nightly-merge@14b4a4cf358f7479aa708bee05cf8a794d6a2516 #v1.5.0
with:
stable_branch: 'master'
development_branch: 'dev'
+22 -13
View File
@@ -6,13 +6,17 @@
[![forthebadge](http://forthebadge.com/images/badges/made-with-c-sharp.svg)](http://forthebadge.com) [![forinfinityandbyond](https://user-images.githubusercontent.com/5211576/29499758-4efff304-85e6-11e7-8267-62919c3688a9.gif)](https://www.reddit.com/r/SS13/comments/5oplxp/what_is_the_main_problem_with_byond_as_an_engine/dclbu1a) [![forthebadge](http://forthebadge.com/images/badges/built-with-love.svg)](http://forthebadge.com)
This is a toolset to manage production BYOND servers. It includes the ability to update the server without having to stop or shutdown the server (the update will take effect on a "reboot" of the server), the ability to start the server and restart it if it crashes, as well as systems for managing code and game files, and locally merging GitHub Pull Requests for test deployments.
This is a toolset to manage production DreamMaker servers. It includes the ability to update the server without having to stop or shutdown the server (the update will take effect on a "reboot" of the server), the ability to start the server and restart it if it crashes, as well as systems for managing code and game files, and locally merging GitHub Pull Requests for test deployments.
## Setup
### Pre-Requisites
_Note: If you opt to use the Windows installer, all pre-requisites for running BYOND servers (including MariaDB) are provided out of the box. If you wish to use OpenDream you will need to install the required dotnet SDK manually._
_Note: If you opt to use the Windows installer, most pre-requisites for running BYOND servers (including MariaDB) are provided out of the box._
_If you are running on a Windows Server OS. You **might** need to install the [x86 Visual C++ 2015 Runtime](https://aka.ms/vs/17/release/vc_redist.x86.exe) to run BYOND._
_If you wish to use OpenDream you will need to install the required dotnet SDK manually._
tgstation-server needs a relational database to store it's data.
@@ -127,7 +131,7 @@ sudo dpkg --add-architecture i386 \
&& sudo systemctl start tgstation-server
```
The service will execute as the newly created user: `tgstation-server`.
The service will execute as the newly created user: `tgstation-server`. You should, ideally, store your instances somewhere under `/home/tgstation-server`.
##### Manual Setup
@@ -157,6 +161,7 @@ docker run \
--network="host" \ # Not recommended, eases networking setup if your sql server is on the same machine
--name="tgs" \ # Name for the container
--cap-add=sys_nice \ # Recommended, allows TGS to lower the niceness of child processes if it sees fit
--cap-add=sys_resource \ # Recommended, allows TGS to not be killed by the OOM killer before its child processes
--init \ #Highly recommended, reaps potential zombie processes
-p 5000:5000 \ # Port bridge for accessing TGS, you can change this if you need
-p 0.0.0.0:<public game port>:<public game port> \ # Port bridge for accessing DreamDaemon
@@ -407,15 +412,15 @@ _NOTE: Your reverse proxy setup may interfere with SSE (Server-Sent Events) whic
#### IIS (Reccommended for Windows)
1. Acquire an HTTPS certificate. The easiet free way for Windows is [win-acme](https://github.com/PKISharp/win-acme) (requires you to set up the website first)
2. Install the [Web Platform Installer](https://www.microsoft.com/web/downloads/platform.aspx)
3. Open the web platform installer in the IIS Manager and install the Application Request Routing 3.0 module
4. Create a new website, bind it to HTTPS only with your chosen certificate and exposed port. The physical path won't matter since it won't be used. Use `Require Server Name Indication` if you want to limit requests to a specific URL prefix. Do not use the same port as the one TGS is running on.
5. Close and reopen the IIS Manager
5. Open the site and navigate to the `URL Rewrite` module
6. In the `Actions` Pane on the right click `Add Rule(s)...`
7. For the rule template, select `Reverse Proxy` under `Inbound and Outbound Rules` and click `OK`
8. You may get a prompt about enabling proxy functionality. Click `OK`
9. In the window that appears set the `Inbound Rules` textbox to the URL of your tgstation-server i.e. `http://localhost:5000`. Ensure `Enable SSL Offloading` is checked, then click `OK`
1. Install the [URL Rewrite Module](https://www.iis.net/downloads/microsoft/url-rewrite)
1. Install the [Application Request Routing Module](https://www.iis.net/downloads/microsoft/application-request-routing)
1. Create a new website, bind it to HTTPS only with your chosen certificate and exposed port. The physical path won't matter since it won't be used. Use `Require Server Name Indication` if you want to limit requests to a specific URL prefix. Do not use the same port as the one TGS is running on.
1. Close and reopen the IIS Manager
1. Open the site and navigate to the `URL Rewrite` module
1. In the `Actions` Pane on the right click `Add Rule(s)...`
1. For the rule template, select `Reverse Proxy` under `Inbound and Outbound Rules` and click `OK`
1. You may get a prompt about enabling proxy functionality. Click `OK`
1. In the window that appears set the `Inbound Rules` textbox to the URL of your tgstation-server i.e. `localhost:5000`. Ensure `Enable SSL Offloading` is checked, then click `OK`
#### Caddy (Reccommended for Linux, or those unfamilar with configuring NGINX or Apache)
@@ -539,7 +544,11 @@ Manual operations on the repository while an instance is running may lead to git
#### Byond
The `Byond` folder contains installations of [BYOND](https://www.byond.com/) versions. The version which is used by your game code can be changed on a whim (Note that only versions >= 511.1385 have been thouroughly tested. Lower versions should work but if one doesn't function, please open an issue report) and the server will take care of installing it.
The `Byond` folder contains installations of [BYOND](https://www.byond.com/) or [OpenDream](https://github.com/OpenDreamProject/OpenDream) versions. The version which is used by your game code can be changed on a whim (Note that only versions >= 511.1385 have been thouroughly tested. Lower versions should work but if one doesn't function, please open an issue report) and the server will take care of installing it.
##### Environment Variables
You can specify additional environment variables to launch your server/compiler with by adding `server.env`/`compiler.env` to your engine installation directory (i.e. `<instance>/Byond/515.1530/server.env`). These are [.env](https://hexdocs.pm/dotenvy/dotenv-file-format.html) files.
#### Compiler
+1
View File
@@ -16,6 +16,7 @@ RUN . $NVM_DIR/nvm.sh \
&& apt-get update \
&& apt-get install -y \
dos2unix \
libgdiplus \
&& rm -rf /var/lib/apt/lists/*
# Build web control panel
+1 -1
View File
@@ -17,7 +17,7 @@
<ItemGroup>
<!-- Usage: Linting -->
<PackageReference Include="StyleCop.Analyzers" Version="1.2.0-beta.507">
<PackageReference Include="StyleCop.Analyzers" Version="1.2.0-beta.556">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
+4 -4
View File
@@ -3,7 +3,7 @@
<ItemGroup>
<!-- Usage: Code coverage collection -->
<PackageReference Include="coverlet.collector" Version="6.0.0">
<PackageReference Include="coverlet.collector" Version="6.0.2">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
@@ -13,14 +13,14 @@
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
<!-- Usage: Hard to say what exactly this is for, but not including it removes the test icon and breaks vstest.console.exe for some reason -->
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.8.0" Condition="'$(TgsTestNoSdk)' != 'true'" />
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.9.0" Condition="'$(TgsTestNoSdk)' != 'true'" />
<!-- Usage: Dependency mocking for tests -->
<!-- Pinned: Be VERY careful about updating https://github.com/moq/moq/issues/1372 -->
<PackageReference Include="Moq" Version="4.20.70" />
<!-- Usage: MSTest execution -->
<PackageReference Include="MSTest.TestAdapter" Version="3.1.1" />
<PackageReference Include="MSTest.TestAdapter" Version="3.3.1" />
<!-- Usage: MSTest asserts etc... -->
<PackageReference Include="MSTest.TestFramework" Version="3.1.1" />
<PackageReference Include="MSTest.TestFramework" Version="3.3.1" />
</ItemGroup>
</Project>
+7 -7
View File
@@ -3,21 +3,21 @@
<!-- Integration tests will ensure they match across the board -->
<Import Project="WebpanelVersion.props" />
<PropertyGroup>
<TgsCoreVersion>6.1.2</TgsCoreVersion>
<TgsCoreVersion>6.5.0</TgsCoreVersion>
<TgsConfigVersion>5.1.0</TgsConfigVersion>
<TgsApiVersion>10.0.0</TgsApiVersion>
<TgsApiVersion>10.3.0</TgsApiVersion>
<TgsCommonLibraryVersion>7.0.0</TgsCommonLibraryVersion>
<TgsApiLibraryVersion>13.0.1</TgsApiLibraryVersion>
<TgsClientVersion>15.0.1</TgsClientVersion>
<TgsDmapiVersion>7.0.3</TgsDmapiVersion>
<TgsInteropVersion>5.8.0</TgsInteropVersion>
<TgsApiLibraryVersion>13.3.0</TgsApiLibraryVersion>
<TgsClientVersion>15.3.0</TgsClientVersion>
<TgsDmapiVersion>7.1.3</TgsDmapiVersion>
<TgsInteropVersion>5.9.0</TgsInteropVersion>
<TgsHostWatchdogVersion>1.4.1</TgsHostWatchdogVersion>
<TgsContainerScriptVersion>1.2.1</TgsContainerScriptVersion>
<TgsMigratorVersion>2.0.0</TgsMigratorVersion>
<TgsNugetNetFramework>netstandard2.0</TgsNugetNetFramework>
<TgsNetMajorVersion>8</TgsNetMajorVersion>
<!-- Update this frequently with dotnet runtime patches. MAJOR MUST MATCH ABOVE! -->
<TgsDotnetRedistUrl>https://download.visualstudio.microsoft.com/download/pr/2a7ae819-fbc4-4611-a1ba-f3b072d4ea25/32f3b931550f7b315d9827d564202eeb/dotnet-hosting-8.0.0-win.exe</TgsDotnetRedistUrl>
<TgsDotnetRedistUrl>https://download.visualstudio.microsoft.com/download/pr/00397fee-1bd9-44ef-899b-4504b26e6e96/ab9c73409659f3238d33faee304a8b7c/dotnet-hosting-8.0.4-win.exe</TgsDotnetRedistUrl>
<TgsMariaDBRedistVersion>10.11.6</TgsMariaDBRedistVersion>
<TgsYarnVersion>1.22.21</TgsYarnVersion>
</PropertyGroup>
+1 -1
View File
@@ -1,6 +1,6 @@
<Project xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
<PropertyGroup>
<!-- This is in it's own file to help incremental building, changing it causes a complete rebuild of the web panel -->
<TgsWebpanelVersion>5.4.2</TgsWebpanelVersion>
<TgsWebpanelVersion>5.7.1</TgsWebpanelVersion>
</PropertyGroup>
</Project>
+3 -3
View File
@@ -1,4 +1,4 @@
<?xml version="1.0" encoding="utf-8"?>
<?xml version="1.0" encoding="utf-8"?>
<RuleSet Name="myrules" Description="My rule set" ToolsVersion="17.0">
<Rules AnalyzerId="AsyncUsageAnalyzers" RuleNamespace="AsyncUsageAnalyzers">
<Rule Id="UseConfigureAwait" Action="Warning" />
@@ -6,7 +6,7 @@
<Rules AnalyzerId="Microsoft.Analyzers.ManagedCodeAnalysis" RuleNamespace="Microsoft.Rules.Managed">
<Rule Id="CA1000" Action="Warning" />
<Rule Id="CA1001" Action="Warning" />
<Rule Id="CA1002" Action="Warning" />
<Rule Id="CA1002" Action="None" />
<Rule Id="CA1003" Action="Warning" />
<Rule Id="CA1004" Action="Warning" />
<Rule Id="CA1005" Action="Warning" />
@@ -1048,4 +1048,4 @@
<Rules AnalyzerId="Text.CSharp.Analyzers" RuleNamespace="Text.CSharp.Analyzers">
<Rule Id="CA1704" Action="Warning" />
</Rules>
</RuleSet>
</RuleSet>
+2 -1
View File
@@ -19,7 +19,8 @@ apt-get install -y \
ca-certificates \
curl \
gnupg \
xmlstarlet
xmlstarlet \
libgdiplus
declare repo_version=$(if command -v lsb_release &> /dev/null; then lsb_release -r -s; else grep -oP '(?<=^VERSION_ID=).+' /etc/os-release | tr -d '"'; fi)
curl -L https://packages.microsoft.com/config/ubuntu/$repo_version/packages-microsoft-prod.deb -o packages-microsoft-prod.deb
@@ -0,0 +1,18 @@
.TH TGS-CONFIGURE 1
.SH NAME
tgs-configure \- tgstation-server interactive configuration file generator
.SH SYNOPSIS
.B tgs-configure
.SH DESCRIPTION
.B tgs-configure
.SH OPTIONS
The
.B tgs-configure
command does not take any options.
modifies the tgstation-server configuration file stored in /etc/tgstation-server/appsettings.Production.yml.
.SH SEE ALSO
tgstation-server(7)
.SH BUGS
See issue tracker at https://github.com/tgstation/tgstation-server/issues.
.SH AUTHOR
Jordan Dominion (Cyberboss@users.noreply.github.com)
@@ -0,0 +1,11 @@
.TH TGSTATION-SERVER 7
.SH NAME
tgstation-server \- A production scale tool for DreamMaker server management
.SH DESCRIPTION
This is a toolset to manage production DreamMaker servers. It includes the ability to update the server without having to stop or shutdown the server (the update will take effect on a "reboot" of the server), the ability to start the server and restart it if it crashes, as well as systems for managing code and game files, and locally merging GitHub Pull Requests for test deployments.
.SH SEE ALSO
tgs-configure(1)
.SH BUGS
See issue tracker at https://github.com/tgstation/tgstation-server/issues.
.SH AUTHOR
Jordan Dominion (Cyberboss@users.noreply.github.com)
+2
View File
@@ -0,0 +1,2 @@
debian/man/tgs-configure.1
debian/man/tgstation-server.7
+1 -1
View File
@@ -1,7 +1,7 @@
#!/bin/sh -e
if [ -z "$2" ]; then
adduser --system tgstation-server
adduser --system --home /home/tgstation-server tgstation-server
mkdir -m 754 -p /var/log/tgstation-server
chown -R tgstation-server /etc/tgstation-server
chown -R tgstation-server /opt/tgstation-server/lib
@@ -3,7 +3,7 @@
"isRoot": true,
"tools": {
"wix": {
"version": "4.0.2",
"version": "4.0.4",
"commands": [
"wix"
]
@@ -1,4 +1,4 @@
<Project Sdk="WixToolset.Sdk/4.0.2" xmlns:bal="http://schemas.microsoft.com/wix/BalExtension">
<Project Sdk="WixToolset.Sdk/4.0.4" xmlns:bal="http://schemas.microsoft.com/wix/BalExtension">
<Import Project="../../../Common.props" />
<PropertyGroup>
<DefineConstants>ProductVersion=$(TgsCoreVersion);NetMajorVersion=$(TgsNetMajorVersion);DotnetRedistUrl=$(TgsDotnetRedistUrl);MariaDBRedistUrl=https://github.com/tgstation/tgstation-server/releases/download/tgstation-server-v$(TgsCoreVersion)/mariadb-$(TgsMariaDBRedistVersion)-winx64.msi</DefineConstants>
@@ -24,8 +24,8 @@
<Content Include="Theme.xml" />
</ItemGroup>
<ItemGroup>
<PackageReference Include="WixToolset.Bal.wixext" Version="4.0.2" />
<PackageReference Include="WixToolset.Netfx.wixext" Version="4.0.2" />
<PackageReference Include="WixToolset.Bal.wixext" Version="4.0.4" />
<PackageReference Include="WixToolset.Netfx.wixext" Version="4.0.4" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\Tgstation.Server.Host.Service.Wix\Tgstation.Server.Host.Service.Wix.wixproj" />
@@ -7,7 +7,7 @@
</PropertyGroup>
<ItemGroup>
<PackageReference Include="WixToolset.Dtf.CustomAction" Version="4.0.2" />
<PackageReference Include="WixToolset.Dtf.CustomAction" Version="4.0.4" />
</ItemGroup>
<ItemGroup>
@@ -1,4 +1,4 @@
<Project Sdk="WixToolset.Sdk/4.0.2">
<Project Sdk="WixToolset.Sdk/4.0.4">
<Import Project="../../../Common.props" />
<PropertyGroup>
<DefineConstants>ProductVersion=$(TgsCoreVersion)</DefineConstants>
@@ -25,8 +25,8 @@
</HarvestDirectory>
</ItemGroup>
<ItemGroup>
<PackageReference Include="WixToolset.Heat" Version="4.0.2" />
<PackageReference Include="WixToolset.Util.wixext" Version="4.0.2" />
<PackageReference Include="WixToolset.Heat" Version="4.0.4" />
<PackageReference Include="WixToolset.Util.wixext" Version="4.0.4" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\Tgstation.Server.Host.Service.Wix.Extensions\Tgstation.Server.Host.Service.Wix.Extensions.csproj" />
@@ -22,9 +22,6 @@ Installers:
AppsAndFeaturesEntries:
- DisplayName: tgstation-server
Publisher: /tg/station 13
Dependencies:
PackageDependencies:
- PackageIdentifier: Microsoft.DotNet.HostingBundle.8
ReleaseDate: 2023-06-24 # Do not change. Set before publish by push_manifest.ps1
ManifestType: installer
ManifestVersion: 1.5.0
ManifestVersion: 1.6.0
@@ -18,4 +18,4 @@ Documentations:
ReleaseNotesUrl: https://github.com/tgstation/tgstation-server/releases/tag/tgstation-server-v0.22.475
PurchaseUrl: https://github.com/sponsors/Cyberboss
ManifestType: defaultLocale
ManifestVersion: 1.5.0
ManifestVersion: 1.6.0
@@ -5,4 +5,4 @@ PackageIdentifier: Tgstation.Server
PackageVersion: 0.22.475 # Do not change. Set before publish by push_manifest.ps1
DefaultLocale: en-US
ManifestType: version
ManifestVersion: 1.5.0
ManifestVersion: 1.6.0
+24 -2
View File
@@ -1,6 +1,6 @@
// tgstation-server DMAPI
#define TGS_DMAPI_VERSION "7.0.3"
#define TGS_DMAPI_VERSION "7.1.3"
// All functions and datums outside this document are subject to change with any version and should not be relied on.
@@ -50,6 +50,13 @@
#endif
#ifndef TGS_FILE2TEXT_NATIVE
#ifdef file2text
#error Your codebase is re-defining the BYOND proc file2text. The DMAPI requires the native version to read the result of world.Export(). You can fix this by adding "#define TGS_FILE2TEXT_NATIVE file2text" before your override of file2text to allow the DMAPI to use the native version. This will only be used for world.Export(), not regular file accesses
#endif
#define TGS_FILE2TEXT_NATIVE file2text
#endif
// EVENT CODES
/// Before a reboot mode change, extras parameters are the current and new reboot mode enums.
@@ -305,6 +312,7 @@
var/datum/tgs_chat_embed/structure/embed
/datum/tgs_message_content/New(text)
..()
if(!istext(text))
TGS_ERROR_LOG("[/datum/tgs_message_content] created with no text!")
text = null
@@ -347,6 +355,7 @@
var/proxy_url
/datum/tgs_chat_embed/media/New(url)
..()
if(!istext(url))
CRASH("[/datum/tgs_chat_embed/media] created with no url!")
@@ -360,6 +369,7 @@
var/proxy_icon_url
/datum/tgs_chat_embed/footer/New(text)
..()
if(!istext(text))
CRASH("[/datum/tgs_chat_embed/footer] created with no text!")
@@ -376,6 +386,7 @@
var/proxy_icon_url
/datum/tgs_chat_embed/provider/author/New(name)
..()
if(!istext(name))
CRASH("[/datum/tgs_chat_embed/provider/author] created with no name!")
@@ -388,6 +399,7 @@
var/is_inline
/datum/tgs_chat_embed/field/New(name, value)
..()
if(!istext(name))
CRASH("[/datum/tgs_chat_embed/field] created with no name!")
@@ -490,10 +502,20 @@
/world/proc/TgsChatChannelInfo()
return
/**
* Trigger an event in TGS. Requires TGS version >= 6.3.0. Returns [TRUE] if the event was triggered successfully, [FALSE] otherwise. This function may sleep!
*
* event_name - The name of the event to trigger
* parameters - Optional list of string parameters to pass as arguments to the event script. The first parameter passed to a script will always be the running game's directory followed by these parameters.
* wait_for_completion - If set, this function will not return until the event has run to completion.
*/
/world/proc/TgsTriggerEvent(event_name, list/parameters, wait_for_completion = FALSE)
return
/*
The MIT License
Copyright (c) 2017-2023 Jordan Brown
Copyright (c) 2017-2024 Jordan Brown
Permission is hereby granted, free of charge,
to any person obtaining a copy of this software and
+1 -1
View File
@@ -1,6 +1,6 @@
The MIT License
Copyright (c) 2017-2023 Jordan Brown
Copyright (c) 2017-2024 Jordan Brown
Permission is hereby granted, free of charge,
to any person obtaining a copy of this software and
+8
View File
@@ -166,3 +166,11 @@
var/datum/tgs_api/api = TGS_READ_GLOBAL(tgs)
if(api)
return api.Visibility()
/world/TgsTriggerEvent(event_name, list/parameters, wait_for_completion = FALSE)
var/datum/tgs_api/api = TGS_READ_GLOBAL(tgs)
if(api)
if(!istype(parameters, /list))
parameters = list()
return api.TriggerEvent(event_name, parameters, wait_for_completion)
+5 -2
View File
@@ -7,7 +7,7 @@ TGS_DEFINE_AND_SET_GLOBAL(tgs, null)
var/list/warned_deprecated_command_runs
/datum/tgs_api/New(datum/tgs_event_handler/event_handler, datum/tgs_version/version)
. = ..()
..()
src.event_handler = event_handler
src.version = version
@@ -17,7 +17,7 @@ TGS_DEFINE_AND_SET_GLOBAL(tgs, null)
world.sleep_offline = FALSE // https://www.byond.com/forum/post/2894866
del(world)
world.sleep_offline = FALSE // just in case, this is BYOND after all...
sleep(1)
sleep(world.tick_lag)
TGS_DEBUG_LOG("BYOND DIDN'T TERMINATE THE WORLD!!! TICK IS: [world.time], sleep_offline: [world.sleep_offline]")
/datum/tgs_api/latest
@@ -69,3 +69,6 @@ TGS_PROTECT_DATUM(/datum/tgs_api)
/datum/tgs_api/proc/Visibility()
return TGS_UNIMPLEMENTED
/datum/tgs_api/proc/TriggerEvent(event_name, list/parameters, wait_for_completion)
return FALSE
+1
View File
@@ -1,4 +1,5 @@
/datum/tgs_version/New(raw_parameter)
..()
src.raw_parameter = raw_parameter
deprefixed_parameter = replacetext(raw_parameter, "/tg/station 13 Server v", "")
var/list/version_bits = splittext(deprefixed_parameter, ".")
+3 -3
View File
@@ -181,7 +181,7 @@
var/json = json_encode(data)
while(requesting_new_port && !override_requesting_new_port)
sleep(1)
sleep(world.tick_lag)
//we need some port open at this point to facilitate return communication
if(!world.port)
@@ -209,7 +209,7 @@
requesting_new_port = FALSE
while(export_lock)
sleep(1)
sleep(world.tick_lag)
export_lock = TRUE
last_interop_response = null
@@ -217,7 +217,7 @@
text2file(json, server_commands_json_path)
for(var/I = 0; I < EXPORT_TIMEOUT_DS && !last_interop_response; ++I)
sleep(1)
sleep(world.tick_lag)
if(!last_interop_response)
TGS_ERROR_LOG("Failed to get export result for: [json]")
+1 -1
View File
@@ -1 +1 @@
"5.8.0"
"5.9.0"
+9
View File
@@ -14,6 +14,7 @@
#define DMAPI5_BRIDGE_COMMAND_KILL 4
#define DMAPI5_BRIDGE_COMMAND_CHAT_SEND 5
#define DMAPI5_BRIDGE_COMMAND_CHUNK 6
#define DMAPI5_BRIDGE_COMMAND_EVENT 7
#define DMAPI5_PARAMETER_ACCESS_IDENTIFIER "accessIdentifier"
#define DMAPI5_PARAMETER_CUSTOM_COMMANDS "customCommands"
@@ -34,6 +35,7 @@
#define DMAPI5_BRIDGE_PARAMETER_VERSION "version"
#define DMAPI5_BRIDGE_PARAMETER_CHAT_MESSAGE "chatMessage"
#define DMAPI5_BRIDGE_PARAMETER_MINIMUM_SECURITY_LEVEL "minimumSecurityLevel"
#define DMAPI5_BRIDGE_PARAMETER_EVENT_INVOCATION "eventInvocation"
#define DMAPI5_BRIDGE_RESPONSE_NEW_PORT "newPort"
#define DMAPI5_BRIDGE_RESPONSE_RUNTIME_INFORMATION "runtimeInformation"
@@ -81,6 +83,7 @@
#define DMAPI5_TOPIC_COMMAND_SEND_CHUNK 9
#define DMAPI5_TOPIC_COMMAND_RECEIVE_CHUNK 10
#define DMAPI5_TOPIC_COMMAND_RECEIVE_BROADCAST 11
#define DMAPI5_TOPIC_COMMAND_COMPLETE_EVENT 12
#define DMAPI5_TOPIC_PARAMETER_COMMAND_TYPE "commandType"
#define DMAPI5_TOPIC_PARAMETER_CHAT_COMMAND "chatCommand"
@@ -116,3 +119,9 @@
#define DMAPI5_CUSTOM_CHAT_COMMAND_NAME "name"
#define DMAPI5_CUSTOM_CHAT_COMMAND_HELP_TEXT "helpText"
#define DMAPI5_CUSTOM_CHAT_COMMAND_ADMIN_ONLY "adminOnly"
#define DMAPI5_EVENT_ID "eventId"
#define DMAPI5_EVENT_INVOCATION_NAME "eventName"
#define DMAPI5_EVENT_INVOCATION_PARAMETERS "parameters"
#define DMAPI5_EVENT_INVOCATION_NOTIFY_COMPLETION "notifyCompletion"
+41 -1
View File
@@ -27,6 +27,8 @@
var/chunked_requests = 0
var/list/chunked_topics = list()
var/list/pending_events = list()
var/detached = FALSE
/datum/tgs_api/v5/New()
@@ -46,6 +48,10 @@
var/datum/tgs_version/api_version = ApiVersion()
version = null // we want this to be the TGS version, not the interop version
// sleep once to prevent an issue where world.Export on the first tick can hang indefinitely
sleep(world.tick_lag)
var/list/bridge_response = Bridge(DMAPI5_BRIDGE_COMMAND_STARTUP, list(DMAPI5_BRIDGE_PARAMETER_MINIMUM_SECURITY_LEVEL = minimum_required_security_level, DMAPI5_BRIDGE_PARAMETER_VERSION = api_version.raw_parameter, DMAPI5_PARAMETER_CUSTOM_COMMANDS = ListCustomCommands(), DMAPI5_PARAMETER_TOPIC_PORT = GetTopicPort()))
if(!istype(bridge_response))
TGS_ERROR_LOG("Failed initial bridge request!")
@@ -125,7 +131,7 @@
TGS_DEBUG_LOG("RequireInitialBridgeResponse: Starting sleep")
logged = TRUE
sleep(1)
sleep(world.tick_lag)
TGS_DEBUG_LOG("RequireInitialBridgeResponse: Passed")
@@ -249,6 +255,40 @@
WaitForReattach(TRUE)
return chat_channels.Copy()
/datum/tgs_api/v5/TriggerEvent(event_name, list/parameters, wait_for_completion)
RequireInitialBridgeResponse()
WaitForReattach(TRUE)
if(interop_version.minor < 9)
TGS_WARNING_LOG("Interop version too low for custom events!")
return FALSE
var/str_parameters = list()
for(var/i in parameters)
str_parameters += "[i]"
var/list/response = Bridge(DMAPI5_BRIDGE_COMMAND_EVENT, list(DMAPI5_BRIDGE_PARAMETER_EVENT_INVOCATION = list(DMAPI5_EVENT_INVOCATION_NAME = event_name, DMAPI5_EVENT_INVOCATION_PARAMETERS = str_parameters, DMAPI5_EVENT_INVOCATION_NOTIFY_COMPLETION = wait_for_completion)))
if(!response)
return FALSE
var/event_id = response[DMAPI5_EVENT_ID]
if(!event_id)
return FALSE
TGS_DEBUG_LOG("Created event ID: [event_id]")
if(!wait_for_completion)
return TRUE
TGS_DEBUG_LOG("Waiting for completion of event ID: [event_id]")
while(!pending_events[event_id])
sleep(world.tick_lag)
TGS_DEBUG_LOG("Completed wait on event ID: [event_id]")
pending_events -= event_id
return TRUE
/datum/tgs_api/v5/proc/DecodeChannels(chat_update_json)
TGS_DEBUG_LOG("DecodeChannels()")
var/list/chat_channels_json = chat_update_json[DMAPI5_CHAT_UPDATE_CHANNELS]
+5 -2
View File
@@ -65,7 +65,7 @@
if(detached)
// Wait up to one minute
for(var/i in 1 to 600)
sleep(1)
sleep(world.tick_lag)
if(!detached && (!require_channels || length(chat_channels)))
break
@@ -77,8 +77,11 @@
/datum/tgs_api/v5/proc/PerformBridgeRequest(bridge_request)
WaitForReattach(FALSE)
TGS_DEBUG_LOG("Bridge request start")
// This is an infinite sleep until we get a response
var/export_response = world.Export(bridge_request)
TGS_DEBUG_LOG("Bridge request complete")
if(!export_response)
TGS_ERROR_LOG("Failed bridge request: [bridge_request]")
return
@@ -88,7 +91,7 @@
TGS_ERROR_LOG("Failed bridge request, missing content!")
return
var/response_json = file2text(content)
var/response_json = TGS_FILE2TEXT_NATIVE(content)
if(!response_json)
TGS_ERROR_LOG("Failed bridge request, failed to load content!")
return
+13
View File
@@ -176,6 +176,10 @@
var/list/reattach_response = TopicResponse(error_message)
reattach_response[DMAPI5_PARAMETER_CUSTOM_COMMANDS] = ListCustomCommands()
reattach_response[DMAPI5_PARAMETER_TOPIC_PORT] = GetTopicPort()
for(var/eventId in pending_events)
pending_events[eventId] = TRUE
return reattach_response
if(DMAPI5_TOPIC_COMMAND_SEND_CHUNK)
@@ -276,6 +280,15 @@
TGS_WORLD_ANNOUNCE(message)
return TopicResponse()
if(DMAPI5_TOPIC_COMMAND_COMPLETE_EVENT)
var/event_id = topic_parameters[DMAPI5_EVENT_ID]
if (!istext(event_id))
return TopicResponse("Invalid or missing [DMAPI5_EVENT_ID]")
TGS_DEBUG_LOG("Completing event ID [event_id]...")
pending_events[event_id] = TRUE
return TopicResponse()
return TopicResponse("Unknown command: [command]")
/datum/tgs_api/v5/proc/WorldBroadcast(message)
+9
View File
@@ -14,6 +14,7 @@
#undef DMAPI5_BRIDGE_COMMAND_KILL
#undef DMAPI5_BRIDGE_COMMAND_CHAT_SEND
#undef DMAPI5_BRIDGE_COMMAND_CHUNK
#undef DMAPI5_BRIDGE_COMMAND_EVENT
#undef DMAPI5_PARAMETER_ACCESS_IDENTIFIER
#undef DMAPI5_PARAMETER_CUSTOM_COMMANDS
@@ -34,6 +35,7 @@
#undef DMAPI5_BRIDGE_PARAMETER_VERSION
#undef DMAPI5_BRIDGE_PARAMETER_CHAT_MESSAGE
#undef DMAPI5_BRIDGE_PARAMETER_MINIMUM_SECURITY_LEVEL
#undef DMAPI5_BRIDGE_PARAMETER_EVENT_INVOCATION
#undef DMAPI5_BRIDGE_RESPONSE_NEW_PORT
#undef DMAPI5_BRIDGE_RESPONSE_RUNTIME_INFORMATION
@@ -81,6 +83,7 @@
#undef DMAPI5_TOPIC_COMMAND_SEND_CHUNK
#undef DMAPI5_TOPIC_COMMAND_RECEIVE_CHUNK
#undef DMAPI5_TOPIC_COMMAND_RECEIVE_BROADCAST
#undef DMAPI5_TOPIC_COMMAND_COMPLETE_EVENT
#undef DMAPI5_TOPIC_PARAMETER_COMMAND_TYPE
#undef DMAPI5_TOPIC_PARAMETER_CHAT_COMMAND
@@ -116,3 +119,9 @@
#undef DMAPI5_CUSTOM_CHAT_COMMAND_NAME
#undef DMAPI5_CUSTOM_CHAT_COMMAND_HELP_TEXT
#undef DMAPI5_CUSTOM_CHAT_COMMAND_ADMIN_ONLY
#undef DMAPI5_EVENT_ID
#undef DMAPI5_EVENT_INVOCATION_NAME
#undef DMAPI5_EVENT_INVOCATION_PARAMETERS
#undef DMAPI5_EVENT_INVOCATION_NOTIFY_COMPLETION
@@ -646,5 +646,11 @@ namespace Tgstation.Server.Api.Models
/// </summary>
[Description("The specified OpenDream version is too old!")]
OpenDreamTooOld,
/// <summary>
/// Failed dotnet diagnostics dump.
/// </summary>
[Description("Could not create dump as dotnet diagnostics threw an exception!")]
DotnetDiagnosticsFailure,
}
}
@@ -98,6 +98,13 @@ namespace Tgstation.Server.Api.Models.Internal
[ResponseOptions]
public uint? MapThreads { get; set; }
/// <summary>
/// If minidumps should be taken instead of full dumps.
/// </summary>
[Required]
[ResponseOptions]
public bool? Minidumps { get; set; }
/// <summary>
/// Check if we match a given set of <paramref name="otherParameters"/>. <see cref="StartupTimeout"/> is excluded.
/// </summary>
@@ -116,7 +123,7 @@ namespace Tgstation.Server.Api.Models.Internal
&& AdditionalParameters == otherParameters.AdditionalParameters
&& StartProfiler == otherParameters.StartProfiler
&& LogOutput == otherParameters.LogOutput
&& MapThreads == otherParameters.MapThreads; // We intentionally don't check StartupTimeout, health check seconds, or health check dump as they don't matter in terms of the watchdog
&& MapThreads == otherParameters.MapThreads; // We intentionally don't check StartupTimeout, Minidumps, health check seconds, or health check dump as they don't matter in terms of the watchdog
}
}
}
@@ -39,5 +39,12 @@ namespace Tgstation.Server.Api.Models.Internal
/// </summary>
[Required]
public TimeSpan? Timeout { get; set; }
/// <summary>
/// Additional arguments added to the compiler command line.
/// </summary>
[StringLength(Limits.MaximumStringLength)]
[ResponseOptions]
public string? CompilerAdditionalArguments { get; set; }
}
}
@@ -117,5 +117,10 @@ namespace Tgstation.Server.Api.Rights
/// User can use <see cref="Models.Request.DreamDaemonRequest.BroadcastMessage"/>.
/// </summary>
BroadcastMessage = 1 << 20,
/// <summary>
/// User can use <see cref="Models.Internal.DreamDaemonLaunchParameters.Minidumps"/>.
/// </summary>
SetMinidumps = 1 << 21,
}
}
@@ -57,5 +57,10 @@ namespace Tgstation.Server.Api.Rights
/// User may modify <see cref="Models.Internal.DreamMakerSettings.Timeout"/>.
/// </summary>
SetTimeout = 1 << 8,
/// <summary>
/// User may modify <see cref="Models.Internal.DreamMakerSettings.CompilerAdditionalArguments"/>.
/// </summary>
SetCompilerArguments = 1 << 9,
}
}
@@ -27,7 +27,7 @@
<!-- Usage: HTTP constants reference -->
<PackageReference Include="Microsoft.AspNetCore.Http.Extensions" Version="2.2.0" />
<!-- Usage: Decoding the 'nbf' property of JWTs -->
<PackageReference Include="Microsoft.IdentityModel.JsonWebTokens" Version="7.1.2" />
<PackageReference Include="Microsoft.IdentityModel.JsonWebTokens" Version="7.5.1" />
<!-- Usage: Primary JSON library -->
<PackageReference Include="Newtonsoft.Json" Version="13.0.3" />
<!-- Usage: Data model annotating -->
@@ -11,9 +11,9 @@
<ItemGroup>
<!-- Usage: Connecting to SignalR hubs in API -->
<PackageReference Include="Microsoft.AspNetCore.SignalR.Client" Version="8.0.1" />
<PackageReference Include="Microsoft.AspNetCore.SignalR.Client" Version="8.0.4" />
<!-- Usage: Using target JSON serializer for API -->
<PackageReference Include="Microsoft.AspNetCore.SignalR.Protocols.NewtonsoftJson" Version="8.0.1" />
<PackageReference Include="Microsoft.AspNetCore.SignalR.Protocols.NewtonsoftJson" Version="8.0.4" />
</ItemGroup>
<ItemGroup>
@@ -15,21 +15,13 @@
<PackageReference Include="System.Threading.Tasks.Extensions" Version="4.5.4" />
</ItemGroup>
<!-- This is here because I know we have node as a build dep so this just works -->
<Target Name="IconGeneration" BeforeTargets="ResolveAssemblyReferences" Inputs="build_logo.js;../../build/logo.svg" Outputs="../../artifacts/tgs.ico;../../artifacts/tgs.png">
<Message Text="Restoring yarn packages..." Importance="high" />
<Exec Command="npx --yes yarn@$(TgsYarnVersion) add svg-to-ico@1.0.14 svg2img@1.0.0-beta.2" />
<Target Name="IconGeneration" BeforeTargets="ResolveAssemblyReferences" Inputs="../../build/logo.svg" Outputs="../../artifacts/tgs.ico;../../artifacts/tgs.png;../../tools/Tgstation.Server.LogoGenerator/Program.cs">
<Message Text="Generating icons from SVG..." Importance="high" />
<Exec Command="node ./build_logo.js" />
<Exec Command="dotnet run --project tools/Tgstation.Server.LogoGenerator -c $(Configuration)" WorkingDirectory="../.." />
</Target>
<Target Name="IconClean" AfterTargets="Clean">
<Message Text="Cleaning icons..." Importance="high" />
<Delete Files="../../artifacts/tgs.ico;../../artifacts/tgs.png" />
</Target>
<Target Name="IconNodeCleanup" AfterTargets="IconGeneration;IconClean">
<RemoveDir Directories="node_modules" />
<Delete Files="package.json;yarn.lock;logo_bg_white.svg;yarn-error.log" />
</Target>
</Project>
-48
View File
@@ -1,48 +0,0 @@
// Prereq packages: svg-to-ico@1.0.14 svg2img@1.0.0-beta.2
// Usage: node ./build_logo.js
// Generates ../../artifacts/tgs.ico and ../../artifacts/tgs.ico
const svg_to_img = require("svg-to-ico");
const svg2img = require('svg2img');
const fs = require('fs');
const { exit } = require("process");
if (!fs.existsSync("../../artifacts")) {
fs.mkdirSync("../../artifacts",'0777', true);
}
const svg_bytes = fs.readFileSync("../../build/logo.svg");
const svg = svg_bytes.toString();
const white_bg_svg = svg
.replace("<!-- DO NOT CHANGE THIS LINE, UNCOMMENTING IT ENABLES THE WHITE BACKGROUND FOR THE .ICO--><!--", "")
.replace("SCRIPT_REPLACE_TOKEN", "");
fs.writeFileSync("logo_white_bg.svg", white_bg_svg);
svg_to_img({
input_name: "logo_white_bg.svg",
output_name: "../../artifacts/tgs.ico",
sizes: [ 160 ]
}).then(() => {
fs.unlinkSync("logo_white_bg.svg");
svg2img(
"../../build/logo.svg",
{
resvg: {
fitTo: {
mode: 'width', // or height
value: 64,
}
}
},
function(error, buffer) {
if(error) {
console.error(`PNG conversion failed: ${error}`);
exit(2);
}
fs.writeFileSync("../../artifacts/tgs.png", buffer);
});
}).catch((error) => {
fs.unlinkSync("logo_white_bg.svg");
console.error(`ICO conversion failed: ${error}`);
exit(1);
});
@@ -18,7 +18,7 @@
<!-- Usage: Installing service programatically -->
<PackageReference Include="Core.System.ServiceProcess" Version="2.0.1" />
<!-- Usage: Command line argument support -->
<PackageReference Include="McMaster.Extensions.CommandLineUtils" Version="4.1.0" />
<PackageReference Include="McMaster.Extensions.CommandLineUtils" Version="4.1.1" />
<!-- Usage: Identifies when we are running in the context of the Windows SCM -->
<PackageReference Include="Microsoft.Extensions.Hosting.WindowsServices" Version="8.0.0" />
<!-- Usage: Windows event log logging plugin -->
@@ -3,7 +3,7 @@
"isRoot": true,
"tools": {
"dotnet-ef": {
"version": "8.0.0",
"version": "8.0.4",
"commands": [
"dotnet-ef"
]
@@ -839,7 +839,7 @@ namespace Tgstation.Server.Host.Components.Chat
splits.RemoveAt(0);
var arguments = String.Join(" ", splits);
Tuple<ICommand, IChatTrackingContext?>? GetCommand()
Tuple<ICommand, IChatTrackingContext?>? GetCommand(string command)
{
if (!builtinCommands.TryGetValue(command, out var handler))
return trackingContexts
@@ -867,7 +867,7 @@ namespace Tgstation.Server.Host.Components.Chat
}
else
{
var helpTuple = GetCommand();
var helpTuple = GetCommand(splits[0]);
if (helpTuple != default)
{
var (helpHandler, _) = helpTuple;
@@ -881,7 +881,7 @@ namespace Tgstation.Server.Host.Components.Chat
return;
}
var tuple = GetCommand();
var tuple = GetCommand(command);
if (tuple == default)
{
@@ -320,7 +320,7 @@ namespace Tgstation.Server.Host.Components.Chat.Providers
Optional<IEmbedAuthor> author = new EmbedAuthor(assemblyInformationProvider.VersionPrefix)
{
Url = "https://github.com/tgstation/tgstation-server",
IconUrl = "https://cdn.discordapp.com/attachments/1114451486374637629/1151650846019432448/tgs.png",
IconUrl = "https://cdn.discordapp.com/attachments/1114451486374637629/1151650846019432448/tgs.png", // 404's in browsers but works in Discord
};
var embed = new Embed
{
@@ -3,6 +3,7 @@ using System.Collections.Generic;
using System.Diagnostics.CodeAnalysis;
using System.IO;
using System.Linq;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
@@ -178,7 +179,8 @@ namespace Tgstation.Server.Host.Components.Deployment
{
var jobId = nextDmbProvider.CompileJob.Require(x => x.Id);
var incremented = jobLockCounts[jobId] += lockCount;
logger.LogTrace("Compile job {jobId} lock count now: {lockCount}", jobId, incremented);
logger.LogTrace("Compile job {jobId} lock increased by: {increment}", jobId, lockCount);
LogLockCounts();
return nextDmbProvider;
}
}
@@ -325,14 +327,18 @@ namespace Tgstation.Server.Host.Components.Deployment
if (!jobLockCounts.TryGetValue(compileJobId, out int value))
{
value = 1;
logger.LogTrace("Initializing lock count for compile job {id}", compileJobId);
jobLockCounts.Add(compileJobId, 1);
}
else
{
logger.LogTrace("FromCompileJob already had a jobLockCounts entry for {id}. Incrementing lock count to {value}.", compileJobId, value);
jobLockCounts[compileJobId] = ++value;
}
providerSubmitted = true;
logger.LogTrace("Compile job {id} lock count now: {lockCount}", compileJobId, value);
LogLockCounts();
return newProvider;
}
}
@@ -385,7 +391,7 @@ namespace Tgstation.Server.Host.Components.Deployment
await ioManager.CreateDirectory(gameDirectory, cancellationToken);
var directories = await ioManager.GetDirectories(gameDirectory, cancellationToken);
int deleting = 0;
var tasks = directories.Select(async x =>
var tasks = directories.Select<string, ValueTask>(async x =>
{
var nameOnly = ioManager.GetFileName(x);
if (jobUidsToNotErase.Contains(nameOnly))
@@ -396,17 +402,13 @@ namespace Tgstation.Server.Host.Components.Deployment
++deleting;
await DeleteCompileJobContent(x, cancellationToken);
}
catch (OperationCanceledException)
{
throw;
}
catch (Exception e)
catch (Exception e) when (e is not OperationCanceledException)
{
logger.LogWarning(e, "Error deleting directory {dirName}!", x);
}
}).ToList();
if (deleting > 0)
await Task.WhenAll(tasks);
await ValueTaskExtensions.WhenAll(tasks);
}
#pragma warning restore CA1506
@@ -424,30 +426,31 @@ namespace Tgstation.Server.Host.Components.Deployment
/// <param name="job">The <see cref="CompileJob"/> to clean.</param>
void CleanRegisteredCompileJob(CompileJob job)
{
async Task HandleCleanup()
Task HandleCleanup()
{
// First kill the GitHub deployment
var remoteDeploymentManager = remoteDeploymentManagerFactory.CreateRemoteDeploymentManager(metadata, job);
// DCT: None available
var deploymentJob = remoteDeploymentManager.MarkInactive(job, CancellationToken.None);
var deleteTask = DeleteCompileJobContent(job.DirectoryName!.Value.ToString(), cleanupCts.Token);
var otherTask = cleanupTask;
async Task WrapThrowableTasks()
{
try
{
// First kill the GitHub deployment
var remoteDeploymentManager = remoteDeploymentManagerFactory.CreateRemoteDeploymentManager(metadata, job);
var cancellationToken = cleanupCts.Token;
var deploymentJob = remoteDeploymentManager.MarkInactive(job, cancellationToken);
var deleteTask = DeleteCompileJobContent(job.DirectoryName!.Value.ToString(), cancellationToken);
await ValueTaskExtensions.WhenAll(deleteTask, deploymentJob);
}
catch (Exception ex)
catch (Exception ex) when (ex is not OperationCanceledException)
{
logger.LogWarning(ex, "Error cleaning up compile job {jobGuid}!", job.DirectoryName);
}
}
await Task.WhenAll(otherTask, WrapThrowableTasks());
return Task.WhenAll(otherTask, WrapThrowableTasks());
}
lock (jobLockCounts)
@@ -467,6 +470,8 @@ namespace Tgstation.Server.Host.Components.Deployment
}
else
logger.LogError("Extra Dispose of DmbProvider for CompileJob {compileJobId}!", jobId);
LogLockCounts();
}
}
@@ -482,5 +487,30 @@ namespace Tgstation.Server.Host.Components.Deployment
await eventConsumer.HandleEvent(EventType.DeploymentCleanup, new List<string> { ioManager.ResolvePath(directory) }, true, cancellationToken);
await ioManager.DeleteDirectory(directory, cancellationToken);
}
/// <summary>
/// Log out the current lock counts to Trace.
/// </summary>
/// <remarks><see cref="jobLockCounts"/> must be locked before calling this function.</remarks>
void LogLockCounts()
{
if (jobLockCounts.Count == 0)
{
logger.LogWarning("No compile jobs registered!");
return;
}
var builder = new StringBuilder();
foreach (var jobId in jobLockCounts.Keys)
{
builder.AppendLine();
builder.Append("\t- ");
builder.Append(jobId);
builder.Append(": ");
builder.Append(jobLockCounts[jobId]);
}
logger.LogTrace("Compile Job Lock Counts:{details}", builder.ToString());
}
}
}
@@ -571,6 +571,7 @@ namespace Tgstation.Server.Host.Components.Deployment
progressReporter.StageName = "Copying repository";
var resolvedOutputDirectory = ioManager.ResolvePath(outputDirectory);
var repoOrigin = repository.Origin;
var repoReference = repository.Reference;
using (repository)
await repository.CopyTo(resolvedOutputDirectory, cancellationToken);
@@ -585,6 +586,7 @@ namespace Tgstation.Server.Host.Components.Deployment
resolvedOutputDirectory,
repoOrigin.ToString(),
engineLock.Version.ToString(),
repoReference,
},
true,
cancellationToken);
@@ -630,7 +632,7 @@ namespace Tgstation.Server.Host.Components.Deployment
// run compiler
progressReporter.StageName = "Running Compiler";
var compileSuceeded = await RunDreamMaker(engineLock, job, cancellationToken);
var compileSuceeded = await RunDreamMaker(engineLock, job, dreamMakerSettings.CompilerAdditionalArguments, cancellationToken);
// Session takes ownership of the lock and Disposes it so save this for later
var engineVersion = engineLock.Version;
@@ -848,17 +850,25 @@ namespace Tgstation.Server.Host.Components.Deployment
/// </summary>
/// <param name="engineLock">The <see cref="IEngineExecutableLock"/> to use.</param>
/// <param name="job">The <see cref="CompileJob"/> for the operation.</param>
/// <param name="additionalCompilerArguments">Additional arguments to be added to the compiler.</param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> for the operation.</param>
/// <returns>A <see cref="ValueTask{TResult}"/> resulting in <see langword="true"/> if compilation succeeded, <see langword="false"/> otherwise.</returns>
async ValueTask<bool> RunDreamMaker(IEngineExecutableLock engineLock, Models.CompileJob job, CancellationToken cancellationToken)
async ValueTask<bool> RunDreamMaker(
IEngineExecutableLock engineLock,
Models.CompileJob job,
string? additionalCompilerArguments,
CancellationToken cancellationToken)
{
var arguments = engineLock.FormatCompilerArguments($"{job.DmeName}.{DmeExtension}");
var environment = await engineLock.LoadEnv(logger, true, cancellationToken);
var arguments = engineLock.FormatCompilerArguments($"{job.DmeName}.{DmeExtension}", additionalCompilerArguments);
await using var dm = processExecutor.LaunchProcess(
await using var dm = await processExecutor.LaunchProcess(
engineLock.CompilerExePath,
ioManager.ResolvePath(
job.DirectoryName!.Value.ToString()),
arguments,
cancellationToken,
environment,
readStandardHandles: true,
noShellExecute: true);
@@ -46,12 +46,14 @@ namespace Tgstation.Server.Host.Components.Deployment
/// <param name="linkFactory">The <see cref="IFilesystemLinkFactory"/> for the <see cref="SwappableDmbProvider"/>.</param>
/// <param name="logger">The value of <see cref="logger"/>.</param>
/// <param name="generalConfiguration">The <see cref="GeneralConfiguration"/> for the <see cref="HardLinkDmbProvider"/>.</param>
/// <param name="securityLevel">The launch <see cref="DreamDaemonSecurity"/> level.</param>
public HardLinkDmbProvider(
IDmbProvider baseProvider,
IIOManager ioManager,
IFilesystemLinkFactory linkFactory,
ILogger logger,
GeneralConfiguration generalConfiguration)
GeneralConfiguration generalConfiguration,
DreamDaemonSecurity securityLevel)
: base(
baseProvider,
ioManager,
@@ -61,7 +63,7 @@ namespace Tgstation.Server.Host.Components.Deployment
cancellationTokenSource = new CancellationTokenSource();
try
{
mirroringTask = MirrorSourceDirectory(generalConfiguration.GetCopyDirectoryTaskThrottle(), cancellationTokenSource.Token);
mirroringTask = MirrorSourceDirectory(generalConfiguration.GetCopyDirectoryTaskThrottle(), securityLevel, cancellationTokenSource.Token);
}
catch
{
@@ -143,9 +145,10 @@ namespace Tgstation.Server.Host.Components.Deployment
/// Mirror the <see cref="Models.CompileJob"/>.
/// </summary>
/// <param name="taskThrottle">The optional maximum number of simultaneous tasks allowed to execute.</param>
/// <param name="securityLevel">The launch <see cref="DreamDaemonSecurity"/> level.</param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> for the operation.</param>
/// <returns>A <see cref="Task{TResult}"/> resulting in the full path to the mirrored directory.</returns>
async Task<string> MirrorSourceDirectory(int? taskThrottle, CancellationToken cancellationToken)
async Task<string> MirrorSourceDirectory(int? taskThrottle, DreamDaemonSecurity securityLevel, CancellationToken cancellationToken)
{
var stopwatch = Stopwatch.StartNew();
var mirrorGuid = Guid.NewGuid();
@@ -157,7 +160,12 @@ namespace Tgstation.Server.Host.Components.Deployment
var dest = IOManager.ResolvePath(mirrorGuid.ToString());
using var semaphore = taskThrottle.HasValue ? new SemaphoreSlim(taskThrottle.Value) : null;
await Task.WhenAll(MirrorDirectoryImpl(src, dest, semaphore, cancellationToken));
await Task.WhenAll(MirrorDirectoryImpl(
src,
dest,
semaphore,
securityLevel,
cancellationToken));
stopwatch.Stop();
logger.LogDebug(
@@ -175,14 +183,15 @@ namespace Tgstation.Server.Host.Components.Deployment
/// <param name="src">The source directory path.</param>
/// <param name="dest">The destination directory path.</param>
/// <param name="semaphore">Optional <see cref="SemaphoreSlim"/> used to limit degree of parallelism.</param>
/// <param name="securityLevel">The launch <see cref="DreamDaemonSecurity"/> level.</param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> for the operation.</param>
/// <returns>A <see cref="IEnumerable{T}"/> of <see cref="Task"/>s representing the running operations. The first <see cref="Task"/> returned is always the necessary call to <see cref="IIOManager.CreateDirectory(string, CancellationToken)"/>.</returns>
/// <remarks>I genuinely don't know how this will work with symlinked files. Waiting for the issue report I guess.</remarks>
IEnumerable<Task> MirrorDirectoryImpl(string src, string dest, SemaphoreSlim? semaphore, CancellationToken cancellationToken)
IEnumerable<Task> MirrorDirectoryImpl(string src, string dest, SemaphoreSlim? semaphore, DreamDaemonSecurity securityLevel, CancellationToken cancellationToken)
{
var dir = new DirectoryInfo(src);
Task? subdirCreationTask = null;
var dreamDaemonWillAcceptOutOfDirectorySymlinks = CompileJob.MinimumSecurityLevel == DreamDaemonSecurity.Trusted;
var dreamDaemonWillAcceptOutOfDirectorySymlinks = securityLevel == DreamDaemonSecurity.Trusted;
foreach (var subDirectory in dir.EnumerateDirectories())
{
var mirroredName = Path.Combine(dest, subDirectory.Name);
@@ -216,7 +225,7 @@ namespace Tgstation.Server.Host.Components.Deployment
logger.LogDebug("Recreating symlinked directory {name} as hard links...", subDirectory.Name);
var checkingSubdirCreationTask = true;
foreach (var copyTask in MirrorDirectoryImpl(subDirectory.FullName, mirroredName, semaphore, cancellationToken))
foreach (var copyTask in MirrorDirectoryImpl(subDirectory.FullName, mirroredName, semaphore, securityLevel, cancellationToken))
{
if (subdirCreationTask == null)
{
@@ -15,7 +15,7 @@ namespace Tgstation.Server.Host.Components.Deployment
string DmbName { get; }
/// <summary>
/// The primary game directory with a trailing directory separator.
/// The primary game directory.
/// </summary>
string Directory { get; }
@@ -87,7 +87,7 @@ namespace Tgstation.Server.Host.Components.Deployment
}
/// <summary>
/// Should be <see langword="await"/>. before calling <see cref="MakeActive(CancellationToken)"/> to ensure the <see cref="SwappableDmbProvider"/> is ready to instantly swap. Can be called multiple times.
/// Should be <see langword="await"/>ed. before calling <see cref="MakeActive(CancellationToken)"/> to ensure the <see cref="SwappableDmbProvider"/> is ready to instantly swap. Can be called multiple times.
/// </summary>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> for the operation.</param>
/// <returns>A <see cref="Task"/> representing the preparation process.</returns>
@@ -6,6 +6,7 @@ using System.Threading.Tasks;
using Tgstation.Server.Api.Models;
using Tgstation.Server.Api.Models.Internal;
using Tgstation.Server.Host.Components.Deployment;
using Tgstation.Server.Host.IO;
namespace Tgstation.Server.Host.Components.Engine
{
@@ -32,6 +33,9 @@ namespace Tgstation.Server.Host.Components.Engine
/// <inheritdoc />
public override bool PreferFileLogging => false;
/// <inheritdoc />
public override bool UseDotnetDump => false;
/// <inheritdoc />
public override Task InstallationTask { get; }
@@ -75,6 +79,7 @@ namespace Tgstation.Server.Host.Components.Engine
/// <summary>
/// Initializes a new instance of the <see cref="ByondInstallation"/> class.
/// </summary>
/// <param name="installationIOManager">The <see cref="IIOManager"/> for the <see cref="EngineInstallationBase"/>.</param>
/// <param name="installationTask">The value of <see cref="InstallationTask"/>.</param>
/// <param name="version">The value of <see cref="Version"/>.</param>
/// <param name="dreamDaemonPath">The value of <see cref="ServerExePath"/>.</param>
@@ -82,12 +87,14 @@ namespace Tgstation.Server.Host.Components.Engine
/// <param name="supportsCli">If a CLI application is being used.</param>
/// <param name="supportsMapThreads">The value of <see cref="supportsMapThreads"/>.</param>
public ByondInstallation(
IIOManager installationIOManager,
Task installationTask,
EngineVersion version,
string dreamDaemonPath,
string dreamMakerPath,
bool supportsCli,
bool supportsMapThreads)
: base(installationIOManager)
{
InstallationTask = installationTask ?? throw new ArgumentNullException(nameof(installationTask));
ArgumentNullException.ThrowIfNull(version);
@@ -140,7 +147,14 @@ namespace Tgstation.Server.Host.Components.Engine
}
/// <inheritdoc />
public override string FormatCompilerArguments(string dmePath)
=> $"-clean \"{dmePath ?? throw new ArgumentNullException(nameof(dmePath))}\"";
public override string FormatCompilerArguments(string dmePath, string? additionalArguments)
{
if (String.IsNullOrWhiteSpace(additionalArguments))
additionalArguments = String.Empty;
else
additionalArguments = $"{additionalArguments.Trim()} ";
return $"-clean {additionalArguments}\"{dmePath ?? throw new ArgumentNullException(nameof(dmePath))}\"";
}
}
}
@@ -1,6 +1,7 @@
using System;
using System.Collections.Generic;
using System.Globalization;
using System.Text;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
@@ -9,7 +10,6 @@ using Microsoft.Extensions.Logging;
using Tgstation.Server.Api.Models;
using Tgstation.Server.Host.IO;
using Tgstation.Server.Host.Jobs;
using Tgstation.Server.Host.Utils;
namespace Tgstation.Server.Host.Components.Engine
{
@@ -23,31 +23,21 @@ namespace Tgstation.Server.Host.Components.Engine
/// </summary>
protected const string ByondBinPath = "byond/bin";
/// <summary>
/// The path to the cfg directory.
/// </summary>
protected const string CfgDirectoryName = "cfg";
/// <summary>
/// The name of BYOND's cache directory.
/// </summary>
const string CacheDirectoryName = "cache";
/// <summary>
/// The path to the cfg directory.
/// </summary>
const string CfgDirectoryName = "cfg";
/// <summary>
/// The name of the list of trusted .dmb files in the user's BYOND cfg directory.
/// </summary>
const string TrustedDmbFileName = "trusted.txt";
/// <summary>
/// The first <see cref="Version"/> of BYOND that supports the '-map-threads' parameter on DreamDaemon.
/// </summary>
static readonly Version MapThreadsVersion = new(515, 1609);
/// <summary>
/// <see cref="SemaphoreSlim"/> for writing to files in the user's BYOND directory.
/// </summary>
static readonly SemaphoreSlim UserFilesSemaphore = new(1);
/// <inheritdoc />
protected override EngineType TargetEngineType => EngineType.Byond;
@@ -88,21 +78,22 @@ namespace Tgstation.Server.Host.Components.Engine
{
CheckVersionValidity(version);
var binPathForVersion = IOManager.ConcatPath(path, ByondBinPath);
var installationIOManager = new ResolvingIOManager(IOManager, path);
var supportsMapThreads = version.Version >= MapThreadsVersion;
return new ByondInstallation(
installationIOManager,
installationTask,
version,
IOManager.ResolvePath(
IOManager.ConcatPath(
binPathForVersion,
installationIOManager.ResolvePath(
installationIOManager.ConcatPath(
ByondBinPath,
GetDreamDaemonName(
version.Version!,
out var supportsCli))),
IOManager.ResolvePath(
IOManager.ConcatPath(
binPathForVersion,
installationIOManager.ResolvePath(
installationIOManager.ConcatPath(
ByondBinPath,
DreamMakerName)),
supportsCli,
supportsMapThreads);
@@ -143,18 +134,10 @@ namespace Tgstation.Server.Host.Components.Engine
localCfgDirectory,
cancellationToken);
// Delete trusted.txt so it doesn't grow too large
var trustedFilePath =
IOManager.ConcatPath(
localCfgDirectory,
TrustedDmbFileName);
var additionalCleanTasks = AdditionalCacheCleanFilePaths(localCfgDirectory)
.Select(path => IOManager.DeleteFile(path, cancellationToken));
Logger.LogTrace("Deleting trusted .dmbs file {trustedFilePath}", trustedFilePath);
var trustedDmbDeleteTask = IOManager.DeleteFile(
trustedFilePath,
cancellationToken);
await Task.WhenAll(cacheCleanTask, cfgCreateTask, trustedDmbDeleteTask);
await Task.WhenAll(cacheCleanTask, cfgCreateTask, Task.WhenAll(additionalCleanTasks));
}
catch (Exception ex) when (ex is not OperationCanceledException)
{
@@ -162,49 +145,6 @@ namespace Tgstation.Server.Host.Components.Engine
}
}
/// <inheritdoc />
public override async ValueTask TrustDmbPath(EngineVersion version, string fullDmbPath, CancellationToken cancellationToken)
{
ArgumentNullException.ThrowIfNull(version);
ArgumentNullException.ThrowIfNull(fullDmbPath);
var byondDir = PathToUserFolder;
var cfgDir = IOManager.ConcatPath(
byondDir,
CfgDirectoryName);
var trustedFilePath = IOManager.ConcatPath(
cfgDir,
TrustedDmbFileName);
Logger.LogDebug("Adding .dmb ({dmbPath}) to {trustedFilePath}", fullDmbPath, trustedFilePath);
using (await SemaphoreSlimContext.Lock(UserFilesSemaphore, cancellationToken))
{
string trustedFileText;
var filePreviouslyExisted = await IOManager.FileExists(trustedFilePath, cancellationToken);
if (filePreviouslyExisted)
{
var trustedFileBytes = await IOManager.ReadAllBytes(trustedFilePath, cancellationToken);
trustedFileText = Encoding.UTF8.GetString(trustedFileBytes);
trustedFileText = $"{trustedFileText.Trim()}{Environment.NewLine}";
}
else
trustedFileText = String.Empty;
if (trustedFileText.Contains(fullDmbPath, StringComparison.Ordinal))
return;
trustedFileText = $"{trustedFileText}{fullDmbPath}{Environment.NewLine}";
var newTrustedFileBytes = Encoding.UTF8.GetBytes(trustedFileText);
if (!filePreviouslyExisted)
await IOManager.CreateDirectory(cfgDir, cancellationToken);
await IOManager.WriteAllBytes(trustedFilePath, newTrustedFileBytes, cancellationToken);
}
}
/// <inheritdoc />
public override async ValueTask<IEngineInstallationData> DownloadVersion(EngineVersion version, JobProgressReporter? progressReporter, CancellationToken cancellationToken)
{
@@ -239,6 +179,13 @@ namespace Tgstation.Server.Host.Components.Engine
/// <returns>The file name of the DreamDaemon executable.</returns>
protected abstract string GetDreamDaemonName(Version byondVersion, out bool supportsCli);
/// <summary>
/// List off additional file paths in the <paramref name="configDirectory"/> to delete.
/// </summary>
/// <param name="configDirectory">The full path to the relevant <see cref="CfgDirectoryName"/>.</param>
/// <returns>An <see cref="IEnumerable{T}"/> of paths in <paramref name="configDirectory"/> to clean.</returns>
protected virtual IEnumerable<string> AdditionalCacheCleanFilePaths(string configDirectory) => Enumerable.Empty<string>();
/// <summary>
/// Create a <see cref="Uri"/> pointing to the location of the download for a given <paramref name="version"/>.
/// </summary>
@@ -41,8 +41,8 @@ namespace Tgstation.Server.Host.Components.Engine
=> DelegateCall(version, installer => installer.DownloadVersion(version, jobProgressReporter, cancellationToken));
/// <inheritdoc />
public ValueTask Install(EngineVersion version, string path, CancellationToken cancellationToken)
=> DelegateCall(version, installer => installer.Install(version, path, cancellationToken));
public ValueTask Install(EngineVersion version, string path, bool deploymentPipelineProcesses, CancellationToken cancellationToken)
=> DelegateCall(version, installer => installer.Install(version, path, deploymentPipelineProcesses, cancellationToken));
/// <inheritdoc />
public ValueTask TrustDmbPath(EngineVersion version, string fullDmbPath, CancellationToken cancellationToken)
@@ -36,6 +36,9 @@ namespace Tgstation.Server.Host.Components.Engine
/// <inheritdoc />
public Task InstallationTask => Instance.InstallationTask;
/// <inheritdoc />
public bool UseDotnetDump => Instance.UseDotnetDump;
/// <inheritdoc />
public void DoNotDeleteThisSession() => DangerousDropReference();
@@ -52,7 +55,7 @@ namespace Tgstation.Server.Host.Components.Engine
logFilePath);
/// <inheritdoc />
public string FormatCompilerArguments(string dmePath) => Instance.FormatCompilerArguments(dmePath);
public string FormatCompilerArguments(string dmePath, string? additionalArguments) => Instance.FormatCompilerArguments(dmePath, additionalArguments);
/// <inheritdoc />
public ValueTask StopServerProcess(ILogger logger, IProcess process, string accessIdentifier, ushort port, CancellationToken cancellationToken)
@@ -62,5 +65,9 @@ namespace Tgstation.Server.Host.Components.Engine
accessIdentifier,
port,
cancellationToken);
/// <inheritdoc />
public ValueTask<Dictionary<string, string>?> LoadEnv(ILogger logger, bool forCompiler, CancellationToken cancellationToken)
=> Instance.LoadEnv(logger, forCompiler, cancellationToken);
}
}
@@ -1,15 +1,19 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
using System.Web;
using DotEnv.Core;
using Microsoft.Extensions.Logging;
using Tgstation.Server.Api.Models;
using Tgstation.Server.Api.Models.Internal;
using Tgstation.Server.Host.Components.Deployment;
using Tgstation.Server.Host.IO;
using Tgstation.Server.Host.System;
namespace Tgstation.Server.Host.Components.Engine
@@ -35,9 +39,17 @@ namespace Tgstation.Server.Host.Components.Engine
/// <inheritdoc />
public abstract bool PromptsForNetworkAccess { get; }
/// <inheritdoc />
public abstract bool UseDotnetDump { get; }
/// <inheritdoc />
public abstract Task InstallationTask { get; }
/// <summary>
/// The <see cref="IIOManager"/> pointing to the installation directory.
/// </summary>
protected IIOManager InstallationIOManager { get; }
/// <summary>
/// Encode given parameters for passing as world.params on the command line.
/// </summary>
@@ -56,8 +68,17 @@ namespace Tgstation.Server.Host.Components.Engine
return parametersString;
}
/// <summary>
/// Initializes a new instance of the <see cref="EngineInstallationBase"/> class.
/// </summary>
/// <param name="installationIOManager">The value of <see cref="InstallationIOManager"/>.</param>
public EngineInstallationBase(IIOManager installationIOManager)
{
InstallationIOManager = installationIOManager ?? throw new ArgumentNullException(nameof(installationIOManager));
}
/// <inheritdoc />
public abstract string FormatCompilerArguments(string dmePath);
public abstract string FormatCompilerArguments(string dmePath, string? additionalArguments);
/// <inheritdoc />
public abstract string FormatServerArguments(
@@ -69,10 +90,45 @@ namespace Tgstation.Server.Host.Components.Engine
/// <inheritdoc />
public virtual async ValueTask StopServerProcess(ILogger logger, IProcess process, string accessIdentifier, ushort port, CancellationToken cancellationToken)
{
ArgumentNullException.ThrowIfNull(logger);
cancellationToken.ThrowIfCancellationRequested();
logger.LogTrace("Terminating engine server process...");
process.Terminate();
await process.Lifetime;
}
/// <inheritdoc />
public async ValueTask<Dictionary<string, string>?> LoadEnv(ILogger logger, bool forCompiler, CancellationToken cancellationToken)
{
ArgumentNullException.ThrowIfNull(logger);
var envFile = forCompiler
? "compiler.env"
: "server.env";
if (!await InstallationIOManager.FileExists(envFile, cancellationToken))
{
logger.LogTrace("No {envFile} present in engine installation {version}", envFile, Version);
return null;
}
logger.LogDebug("Loading {envFile} for engine installation {version}...", envFile, Version);
var fileBytes = await InstallationIOManager.ReadAllBytes(envFile, cancellationToken);
var fileContents = Encoding.UTF8.GetString(fileBytes);
var parser = new EnvParser();
try
{
var variables = parser.Parse(fileContents);
return variables.ToDictionary();
}
catch (Exception ex)
{
logger.LogWarning(ex, "Unable to parse {envFile}!", envFile);
return null;
}
}
}
}
@@ -46,7 +46,7 @@ namespace Tgstation.Server.Host.Components.Engine
public abstract Task CleanCache(CancellationToken cancellationToken);
/// <inheritdoc />
public abstract ValueTask Install(EngineVersion version, string path, CancellationToken cancellationToken);
public abstract ValueTask Install(EngineVersion version, string path, bool deploymentPipelineProcesses, CancellationToken cancellationToken);
/// <inheritdoc />
public abstract ValueTask UpgradeInstallation(EngineVersion version, string path, CancellationToken cancellationToken);
@@ -438,6 +438,7 @@ namespace Tgstation.Server.Host.Components.Engine
installLock = installationContainer.AddReference();
}
var deploymentPipelineProcesses = !neededForLock;
try
{
if (installedOrInstalling)
@@ -471,18 +472,18 @@ namespace Tgstation.Server.Host.Components.Engine
progressReporter.StageName = "Running event";
var versionString = version.ToString();
await eventConsumer.HandleEvent(EventType.EngineInstallStart, new List<string> { versionString }, false, cancellationToken);
await eventConsumer.HandleEvent(EventType.EngineInstallStart, new List<string> { versionString }, deploymentPipelineProcesses, cancellationToken);
await InstallVersionFiles(progressReporter, version, customVersionStream, cancellationToken);
await InstallVersionFiles(progressReporter, version, customVersionStream, deploymentPipelineProcesses, cancellationToken);
ourTcs.SetResult();
await eventConsumer.HandleEvent(EventType.EngineInstallComplete, new List<string> { versionString }, false, cancellationToken);
await eventConsumer.HandleEvent(EventType.EngineInstallComplete, new List<string> { versionString }, deploymentPipelineProcesses, cancellationToken);
}
catch (Exception ex)
{
if (ex is not OperationCanceledException)
await eventConsumer.HandleEvent(EventType.EngineInstallFail, new List<string> { ex.Message }, false, cancellationToken);
await eventConsumer.HandleEvent(EventType.EngineInstallFail, new List<string> { ex.Message }, deploymentPipelineProcesses, cancellationToken);
lock (installedVersions)
installedVersions.Remove(version);
@@ -506,9 +507,15 @@ namespace Tgstation.Server.Host.Components.Engine
/// <param name="progressReporter">The optional <see cref="JobProgressReporter"/> for the operation.</param>
/// <param name="version">The <see cref="EngineVersion"/> being installed with the <see cref="Version.Build"/> number set if appropriate.</param>
/// <param name="customVersionStream">Custom zip file <see cref="Stream"/> to use. Will cause a <see cref="Version.Build"/> number to be added.</param>
/// <param name="deploymentPipelineProcesses">If processes should be launched as part of the deployment pipeline.</param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> for the operation.</param>
/// <returns>A <see cref="ValueTask"/> representing the running operation.</returns>
async ValueTask InstallVersionFiles(JobProgressReporter? progressReporter, EngineVersion version, Stream? customVersionStream, CancellationToken cancellationToken)
async ValueTask InstallVersionFiles(
JobProgressReporter? progressReporter,
EngineVersion version,
Stream? customVersionStream,
bool deploymentPipelineProcesses,
CancellationToken cancellationToken)
{
var installFullPath = ioManager.ResolvePath(version.ToString());
async ValueTask DirectoryCleanup()
@@ -554,7 +561,7 @@ namespace Tgstation.Server.Host.Components.Engine
if (progressReporter != null)
progressReporter.StageName = "Running installation actions";
await engineInstaller.Install(version, installFullPath, cancellationToken);
await engineInstaller.Install(version, installFullPath, deploymentPipelineProcesses, cancellationToken);
if (progressReporter != null)
progressReporter.StageName = "Writing version file";
@@ -46,6 +46,11 @@ namespace Tgstation.Server.Host.Components.Engine
/// </summary>
bool PreferFileLogging { get; }
/// <summary>
/// If dotnet-dump should be used to create process dumps for this installation.
/// </summary>
bool UseDotnetDump { get; }
/// <summary>
/// The <see cref="Task"/> that completes when the BYOND version finished installing.
/// </summary>
@@ -69,8 +74,9 @@ namespace Tgstation.Server.Host.Components.Engine
/// Return the command line arguments for compiling a given <paramref name="dmePath"/> if compilation is necessary.
/// </summary>
/// <param name="dmePath">The full path to the .dme to compile.</param>
/// <param name="additionalArguments">Optional additional arguments provided to the compiler.</param>
/// <returns>The formatted arguments <see cref="string"/>.</returns>
string FormatCompilerArguments(string dmePath);
string FormatCompilerArguments(string dmePath, string? additionalArguments);
/// <summary>
/// Kills a given engine server <paramref name="process"/>.
@@ -82,5 +88,14 @@ namespace Tgstation.Server.Host.Components.Engine
/// <param name="cancellationToken">The <see cref="CancellationToken"/> for the operation.</param>
/// <returns>A <see cref="ValueTask"/> representing the running operation.</returns>
ValueTask StopServerProcess(ILogger logger, IProcess process, string accessIdentifier, ushort port, CancellationToken cancellationToken);
/// <summary>
/// Loads the environment settings for either the server or compiler.
/// </summary>
/// <param name="logger">The <see cref="ILogger"/> to write to.</param>
/// <param name="forCompiler">If <see langword="false"/> server.env will be loaded. If <see langword="true"/> compiler.env will be loaded.</param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> for the operation.</param>
/// <returns>A <see cref="ValueTask{TResult}"/> resulting in the environment <see cref="Dictionary{TKey, TValue}"/> or <see langword="null"/> if the target environment file doesn't exist.</returns>
ValueTask<Dictionary<string, string>?> LoadEnv(ILogger logger, bool forCompiler, CancellationToken cancellationToken);
}
}
@@ -34,9 +34,10 @@ namespace Tgstation.Server.Host.Components.Engine
/// </summary>
/// <param name="version">The <see cref="EngineVersion"/> being installed.</param>
/// <param name="path">The path to the installation.</param>
/// <param name="deploymentPipelineProcesses">If the operation should consider processes it launches to be part of the deployment pipeline.</param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> for the operation.</param>
/// <returns>A <see cref="ValueTask"/> representing the running operation.</returns>
ValueTask Install(EngineVersion version, string path, CancellationToken cancellationToken);
ValueTask Install(EngineVersion version, string path, bool deploymentPipelineProcesses, CancellationToken cancellationToken);
/// <summary>
/// Does actions necessary to get upgrade a version installed by a previous version of TGS.
@@ -45,12 +45,10 @@ namespace Tgstation.Server.Host.Components.Engine
public override bool PreferFileLogging => true;
/// <inheritdoc />
public override Task InstallationTask { get; }
public override bool UseDotnetDump => true;
/// <summary>
/// The <see cref="IIOManager"/> for the <see cref="OpenDreamInstallation"/>.
/// </summary>
readonly IIOManager ioManager;
/// <inheritdoc />
public override Task InstallationTask { get; }
/// <summary>
/// The <see cref="IAsyncDelayer"/> for the <see cref="OpenDreamInstallation"/>.
@@ -65,7 +63,7 @@ namespace Tgstation.Server.Host.Components.Engine
/// <summary>
/// Initializes a new instance of the <see cref="OpenDreamInstallation"/> class.
/// </summary>
/// <param name="ioManager">The value of <see cref="ioManager"/>.</param>
/// <param name="installationIOManager">The <see cref="IIOManager"/> for the <see cref="EngineInstallationBase"/>.</param>
/// <param name="asyncDelayer">The value of <see cref="asyncDelayer"/>.</param>
/// <param name="httpClientFactory">The value of <see cref="httpClientFactory"/>.</param>
/// <param name="serverExePath">The value of <see cref="ServerExePath"/>.</param>
@@ -73,15 +71,15 @@ namespace Tgstation.Server.Host.Components.Engine
/// <param name="installationTask">The value of <see cref="InstallationTask"/>.</param>
/// <param name="version">The value of <see cref="Version"/>.</param>
public OpenDreamInstallation(
IIOManager ioManager,
IIOManager installationIOManager,
IAsyncDelayer asyncDelayer,
IAbstractHttpClientFactory httpClientFactory,
string serverExePath,
string compilerExePath,
Task installationTask,
EngineVersion version)
: base(installationIOManager)
{
this.ioManager = ioManager ?? throw new ArgumentNullException(nameof(ioManager));
this.asyncDelayer = asyncDelayer ?? throw new ArgumentNullException(nameof(asyncDelayer));
this.httpClientFactory = httpClientFactory ?? throw new ArgumentNullException(nameof(httpClientFactory));
ServerExePath = serverExePath ?? throw new ArgumentNullException(nameof(serverExePath));
@@ -109,13 +107,20 @@ namespace Tgstation.Server.Host.Components.Engine
var parametersString = EncodeParameters(parameters, launchParameters);
var arguments = $"--cvar {(logFilePath != null ? $"log.path=\"{ioManager.GetDirectoryName(logFilePath)}\" --cvar log.format=\"{ioManager.GetFileName(logFilePath)}\"" : "log.enabled=false")} --cvar watchdog.token={accessIdentifier} --cvar log.runtimelog=false --cvar net.port={launchParameters.Port!.Value} --cvar opendream.topic_port=0 --cvar opendream.world_params=\"{parametersString}\" --cvar opendream.json_path=\"./{dmbProvider.DmbName}\"";
var arguments = $"--cvar {(logFilePath != null ? $"log.path=\"{InstallationIOManager.GetDirectoryName(logFilePath)}\" --cvar log.format=\"{InstallationIOManager.GetFileName(logFilePath)}\"" : "log.enabled=false")} --cvar watchdog.token={accessIdentifier} --cvar log.runtimelog=false --cvar net.port={launchParameters.Port!.Value} --cvar opendream.topic_port=0 --cvar opendream.world_params=\"{parametersString}\" --cvar opendream.json_path=\"./{dmbProvider.DmbName}\"";
return arguments;
}
/// <inheritdoc />
public override string FormatCompilerArguments(string dmePath)
=> $"--suppress-unimplemented --notices-enabled \"{dmePath ?? throw new ArgumentNullException(nameof(dmePath))}\"";
public override string FormatCompilerArguments(string dmePath, string? additionalArguments)
{
if (String.IsNullOrWhiteSpace(additionalArguments))
additionalArguments = String.Empty;
else
additionalArguments = $"{additionalArguments.Trim()} ";
return $"--suppress-unimplemented --notices-enabled {additionalArguments}\"{dmePath ?? throw new ArgumentNullException(nameof(dmePath))}\"";
}
/// <inheritdoc />
public override async ValueTask StopServerProcess(
@@ -125,6 +130,8 @@ namespace Tgstation.Server.Host.Components.Engine
ushort port,
CancellationToken cancellationToken)
{
ArgumentNullException.ThrowIfNull(logger);
const int MaximumTerminationSeconds = 5;
logger.LogTrace("Attempting Robust.Server graceful exit (Timeout: {seconds}s)...", MaximumTerminationSeconds);
@@ -9,7 +9,6 @@ using Microsoft.Extensions.Options;
using Tgstation.Server.Api.Models;
using Tgstation.Server.Common.Extensions;
using Tgstation.Server.Common.Http;
using Tgstation.Server.Host.Common;
using Tgstation.Server.Host.Components.Repository;
using Tgstation.Server.Host.Configuration;
using Tgstation.Server.Host.IO;
@@ -124,7 +123,7 @@ namespace Tgstation.Server.Host.Components.Engine
CheckVersionValidity(version);
GetExecutablePaths(path, out var serverExePath, out var compilerExePath);
return new OpenDreamInstallation(
IOManager,
new ResolvingIOManager(IOManager, path),
asyncDelayer,
httpClientFactory,
serverExePath,
@@ -193,7 +192,7 @@ namespace Tgstation.Server.Host.Components.Engine
}
/// <inheritdoc />
public override async ValueTask Install(EngineVersion version, string installPath, CancellationToken cancellationToken)
public override async ValueTask Install(EngineVersion version, string installPath, bool deploymentPipelineProcesses, CancellationToken cancellationToken)
{
CheckVersionValidity(version);
ArgumentNullException.ThrowIfNull(installPath);
@@ -232,36 +231,27 @@ namespace Tgstation.Server.Host.Components.Engine
await Task.WhenAll(dirsMoveTasks.Concat(filesMoveTask));
}
var dotnetPaths = DotnetHelper.GetPotentialDotnetPaths(platformIdentifier.IsWindows)
.ToList();
var tasks = dotnetPaths
.Select(path => IOManager.FileExists(path, cancellationToken))
.ToList();
await Task.WhenAll(tasks);
var selectedPathIndex = tasks.FindIndex(pathValidTask => pathValidTask.Result);
if (selectedPathIndex == -1)
var dotnetPath = await DotnetHelper.GetDotnetPath(platformIdentifier, IOManager, cancellationToken);
if (dotnetPath == null)
throw new JobException(ErrorCode.OpenDreamCantFindDotnet);
var dotnetPath = dotnetPaths[selectedPathIndex];
const string DeployDir = "tgs_deploy";
int? buildExitCode = null;
await HandleExtremelyLongPathOperation(
async shortenedPath =>
{
var shortenedDeployPath = IOManager.ConcatPath(shortenedPath, DeployDir);
await using var buildProcess = ProcessExecutor.LaunchProcess(
await using var buildProcess = await ProcessExecutor.LaunchProcess(
dotnetPath,
shortenedPath,
$"run -c Release --project OpenDreamPackageTool -- --tgs -o {shortenedDeployPath}",
cancellationToken,
null,
null,
!GeneralConfiguration.OpenDreamSuppressInstallOutput,
!GeneralConfiguration.OpenDreamSuppressInstallOutput);
if (SessionConfiguration.LowPriorityDeploymentProcesses)
if (deploymentPipelineProcesses && SessionConfiguration.LowPriorityDeploymentProcesses)
buildProcess.AdjustPriority(false);
using (cancellationToken.Register(() => buildProcess.Terminate()))
@@ -67,11 +67,11 @@ namespace Tgstation.Server.Host.Components.Engine
Environment.GetFolderPath(
Environment.SpecialFolder.UserProfile,
Environment.SpecialFolderOption.DoNotVerify),
"./byond/cache"));
"./.byond/cache"));
}
/// <inheritdoc />
public override ValueTask Install(EngineVersion version, string path, CancellationToken cancellationToken)
public override ValueTask Install(EngineVersion version, string path, bool deploymentPipelineProcesses, CancellationToken cancellationToken)
{
CheckVersionValidity(version);
ArgumentNullException.ThrowIfNull(path);
@@ -119,6 +119,16 @@ namespace Tgstation.Server.Host.Components.Engine
return ValueTask.CompletedTask;
}
/// <inheritdoc />
public override ValueTask TrustDmbPath(EngineVersion version, string fullDmbPath, CancellationToken cancellationToken)
{
ArgumentNullException.ThrowIfNull(version);
ArgumentNullException.ThrowIfNull(fullDmbPath);
Logger.LogTrace("No need to trust .dmb path \"{path}\" on POSIX", fullDmbPath);
return ValueTask.CompletedTask;
}
/// <inheritdoc />
protected override string GetDreamDaemonName(Version byondVersion, out bool supportsCli)
{
@@ -47,6 +47,16 @@ namespace Tgstation.Server.Host.Components.Engine
/// </summary>
const string TgsFirewalledDDFile = "TGSFirewalledDD";
/// <summary>
/// The name of the list of trusted .dmb files in the user's BYOND cfg directory.
/// </summary>
const string TrustedDmbFileName = "trusted.txt";
/// <summary>
/// <see cref="SemaphoreSlim"/> for writing to files in the user's BYOND directory.
/// </summary>
static readonly SemaphoreSlim UserFilesSemaphore = new(1, 1);
/// <summary>
/// The first version of BYOND to ship with dd.exe on the Windows build.
/// </summary>
@@ -127,7 +137,7 @@ namespace Tgstation.Server.Host.Components.Engine
public void Dispose() => semaphore.Dispose();
/// <inheritdoc />
public override ValueTask Install(EngineVersion version, string path, CancellationToken cancellationToken)
public override ValueTask Install(EngineVersion version, string path, bool deploymentPipelineProcesses, CancellationToken cancellationToken)
{
CheckVersionValidity(version);
ArgumentNullException.ThrowIfNull(path);
@@ -142,7 +152,7 @@ namespace Tgstation.Server.Host.Components.Engine
if (!generalConfiguration.SkipAddingByondFirewallException)
{
var firewallTask = AddDreamDaemonToFirewall(version, path, cancellationToken);
var firewallTask = AddDreamDaemonToFirewall(version, path, deploymentPipelineProcesses, cancellationToken);
tasks.Add(firewallTask);
}
@@ -165,7 +175,50 @@ namespace Tgstation.Server.Host.Components.Engine
return;
Logger.LogInformation("BYOND Version {version} needs dd.exe added to firewall", version);
await AddDreamDaemonToFirewall(version, path, cancellationToken);
await AddDreamDaemonToFirewall(version, path, true, cancellationToken);
}
/// <inheritdoc />
public override async ValueTask TrustDmbPath(EngineVersion version, string fullDmbPath, CancellationToken cancellationToken)
{
ArgumentNullException.ThrowIfNull(version);
ArgumentNullException.ThrowIfNull(fullDmbPath);
var byondDir = PathToUserFolder;
var cfgDir = IOManager.ConcatPath(
byondDir,
CfgDirectoryName);
var trustedFilePath = IOManager.ConcatPath(
cfgDir,
TrustedDmbFileName);
Logger.LogDebug("Adding .dmb ({dmbPath}) to {trustedFilePath}", fullDmbPath, trustedFilePath);
using (await SemaphoreSlimContext.Lock(UserFilesSemaphore, cancellationToken))
{
string trustedFileText;
var filePreviouslyExisted = await IOManager.FileExists(trustedFilePath, cancellationToken);
if (filePreviouslyExisted)
{
var trustedFileBytes = await IOManager.ReadAllBytes(trustedFilePath, cancellationToken);
trustedFileText = Encoding.UTF8.GetString(trustedFileBytes);
trustedFileText = $"{trustedFileText.Trim()}{Environment.NewLine}";
}
else
trustedFileText = String.Empty;
if (trustedFileText.Contains(fullDmbPath, StringComparison.Ordinal))
return;
trustedFileText = $"{trustedFileText}{fullDmbPath}{Environment.NewLine}";
var newTrustedFileBytes = Encoding.UTF8.GetBytes(trustedFileText);
if (!filePreviouslyExisted)
await IOManager.CreateDirectory(cfgDir, cancellationToken);
await IOManager.WriteAllBytes(trustedFilePath, newTrustedFileBytes, cancellationToken);
}
}
/// <inheritdoc />
@@ -175,6 +228,19 @@ namespace Tgstation.Server.Host.Components.Engine
return supportsCli ? "dd.exe" : "dreamdaemon.exe";
}
/// <inheritdoc />
protected override IEnumerable<string> AdditionalCacheCleanFilePaths(string configDirectory)
{
// Delete trusted.txt so it doesn't grow too large
var trustedFilePath =
IOManager.ConcatPath(
configDirectory,
TrustedDmbFileName);
Logger.LogTrace("Deleting trusted .dmbs file {trustedFilePath}", trustedFilePath);
yield return trustedFilePath;
}
/// <summary>
/// Creates the BYOND cfg file that prevents the trusted mode dialog from appearing when launching DreamDaemon.
/// </summary>
@@ -217,10 +283,11 @@ namespace Tgstation.Server.Host.Components.Engine
try
{
// noShellExecute because we aren't doing runas shennanigans
await using var directXInstaller = processExecutor.LaunchProcess(
await using var directXInstaller = await processExecutor.LaunchProcess(
IOManager.ConcatPath(rbdx, "DXSETUP.exe"),
rbdx,
"/silent",
cancellationToken,
noShellExecute: true);
int exitCode;
@@ -243,9 +310,10 @@ namespace Tgstation.Server.Host.Components.Engine
/// </summary>
/// <param name="version">The BYOND <see cref="EngineVersion"/>.</param>
/// <param name="path">The path to the BYOND installation.</param>
/// <param name="deploymentPipelineProcesses">If the operation is part of the deployment pipeline.</param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> for the operation.</param>
/// <returns>A <see cref="ValueTask"/> representing the running operation.</returns>
async ValueTask AddDreamDaemonToFirewall(EngineVersion version, string path, CancellationToken cancellationToken)
async ValueTask AddDreamDaemonToFirewall(EngineVersion version, string path, bool deploymentPipelineProcesses, CancellationToken cancellationToken)
{
var dreamDaemonName = GetDreamDaemonName(version.Version!, out var usesDDExe);
@@ -268,7 +336,7 @@ namespace Tgstation.Server.Host.Components.Engine
Logger,
ruleName,
dreamDaemonPath,
sessionConfiguration.LowPriorityDeploymentProcesses,
deploymentPipelineProcesses && sessionConfiguration.LowPriorityDeploymentProcesses,
cancellationToken);
}
catch (Exception ex)
@@ -66,15 +66,17 @@ namespace Tgstation.Server.Host.Components.Engine
}
/// <inheritdoc />
public override ValueTask Install(EngineVersion version, string installPath, CancellationToken cancellationToken)
public override ValueTask Install(EngineVersion version, string installPath, bool deploymentPipelineProcesses, CancellationToken cancellationToken)
{
var installTask = base.Install(
version,
installPath,
deploymentPipelineProcesses,
cancellationToken);
var firewallTask = AddServerFirewallException(
version,
installPath,
deploymentPipelineProcesses,
cancellationToken);
return ValueTaskExtensions.WhenAll(installTask, firewallTask);
@@ -101,9 +103,10 @@ namespace Tgstation.Server.Host.Components.Engine
/// </summary>
/// <param name="version">The BYOND <see cref="EngineVersion"/>.</param>
/// <param name="path">The path to the BYOND installation.</param>
/// <param name="deploymentPipelineProcesses">If the operation is part of the deployment pipeline.</param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> for the operation.</param>
/// <returns>A <see cref="ValueTask"/> representing the running operation.</returns>
async ValueTask AddServerFirewallException(EngineVersion version, string path, CancellationToken cancellationToken)
async ValueTask AddServerFirewallException(EngineVersion version, string path, bool deploymentPipelineProcesses, CancellationToken cancellationToken)
{
if (GeneralConfiguration.SkipAddingByondFirewallException)
return;
@@ -123,7 +126,7 @@ namespace Tgstation.Server.Host.Components.Engine
Logger,
ruleName,
serverExePath,
SessionConfiguration.LowPriorityDeploymentProcesses,
deploymentPipelineProcesses && SessionConfiguration.LowPriorityDeploymentProcesses,
cancellationToken);
}
catch (Exception ex)
@@ -30,6 +30,10 @@ namespace Tgstation.Server.Host.Components.Events
this.configuration = configuration ?? throw new ArgumentNullException(nameof(configuration));
}
/// <inheritdoc />
public ValueTask? HandleCustomEvent(string eventName, IEnumerable<string?> parameters, CancellationToken cancellationToken)
=> configuration.HandleCustomEvent(eventName, parameters, cancellationToken);
/// <inheritdoc />
public async ValueTask HandleEvent(EventType eventType, IEnumerable<string?> parameters, bool deploymentPipeline, CancellationToken cancellationToken)
{
@@ -1,5 +1,4 @@
using System;
using System.Collections.Generic;
namespace Tgstation.Server.Host.Components.Events
{
@@ -12,7 +11,7 @@ namespace Tgstation.Server.Host.Components.Events
/// <summary>
/// The name and order of the scripts the event script the <see cref="EventType"/> runs.
/// </summary>
public IReadOnlyList<string> ScriptNames { get; }
public string[] ScriptNames { get; }
/// <summary>
/// Initializes a new instance of the <see cref="EventScriptAttribute"/> class.
@@ -55,7 +55,7 @@
EngineActiveVersionChange,
/// <summary>
/// After the repo is copied, before CodeModifications are applied. Parameters: Game directory path, origin commit sha, engine version string.
/// After the repo is copied, before CodeModifications are applied. Parameters: Game directory path, origin commit sha, engine version string, repository reference (or "(no branch)" if there is no reference).
/// </summary>
[EventScript("PreCompile")]
CompileStart,
@@ -18,5 +18,14 @@ namespace Tgstation.Server.Host.Components.Events
/// <param name="cancellationToken">The <see cref="CancellationToken"/> for the operation.</param>
/// <returns>A <see cref="ValueTask"/> representing the running operation.</returns>
ValueTask HandleEvent(EventType eventType, IEnumerable<string?> parameters, bool deploymentPipeline, CancellationToken cancellationToken);
/// <summary>
/// Handles a given custom event.
/// </summary>
/// <param name="eventName">The name of the event.</param>
/// <param name="parameters">An <see cref="IEnumerable{T}"/> of <see cref="string"/> parameters for the event.</param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> for the operation.</param>
/// <returns>A <see cref="ValueTask"/> representing the running operation if the event was triggered successfully, <see langword="null"/> if it matched a TGS event and wasn't executed.</returns>
ValueTask? HandleCustomEvent(string eventName, IEnumerable<string?> parameters, CancellationToken cancellationToken);
}
}
@@ -12,5 +12,9 @@ namespace Tgstation.Server.Host.Components.Events
/// <inheritdoc />
public ValueTask HandleEvent(EventType eventType, IEnumerable<string?> parameters, bool deploymentPipeline, CancellationToken cancellationToken)
=> ValueTask.CompletedTask;
/// <inheritdoc />
public ValueTask? HandleCustomEvent(string eventName, IEnumerable<string?> parameters, CancellationToken cancellationToken)
=> ValueTask.CompletedTask;
}
}
@@ -135,6 +135,11 @@ namespace Tgstation.Server.Host.Components
/// </summary>
readonly IAsyncDelayer asyncDelayer;
/// <summary>
/// The <see cref="IDotnetDumpService"/> for the <see cref="InstanceFactory"/>.
/// </summary>
readonly IDotnetDumpService dotnetDumpService;
/// <summary>
/// The <see cref="GeneralConfiguration"/> for the <see cref="InstanceFactory"/>.
/// </summary>
@@ -177,6 +182,7 @@ namespace Tgstation.Server.Host.Components
/// <param name="fileTransferService">The value of <see cref="fileTransferService"/>.</param>
/// <param name="remoteDeploymentManagerFactory">The value of <see cref="remoteDeploymentManagerFactory"/>.</param>
/// <param name="asyncDelayer">The value of <see cref="asyncDelayer"/>.</param>
/// <param name="dotnetDumpService">The value of <see cref="dotnetDumpService"/>.</param>
/// <param name="generalConfigurationOptions">The <see cref="IOptions{TOptions}"/> containing the value of <see cref="generalConfiguration"/>.</param>
/// <param name="sessionConfigurationOptions">The <see cref="IOptions{TOptions}"/> containing the value of <see cref="sessionConfiguration"/>.</param>
public InstanceFactory(
@@ -201,6 +207,7 @@ namespace Tgstation.Server.Host.Components
IFileTransferTicketProvider fileTransferService,
IRemoteDeploymentManagerFactory remoteDeploymentManagerFactory,
IAsyncDelayer asyncDelayer,
IDotnetDumpService dotnetDumpService,
IOptions<GeneralConfiguration> generalConfigurationOptions,
IOptions<SessionConfiguration> sessionConfigurationOptions)
{
@@ -225,6 +232,7 @@ namespace Tgstation.Server.Host.Components
this.fileTransferService = fileTransferService ?? throw new ArgumentNullException(nameof(fileTransferService));
this.remoteDeploymentManagerFactory = remoteDeploymentManagerFactory ?? throw new ArgumentNullException(nameof(remoteDeploymentManagerFactory));
this.asyncDelayer = asyncDelayer ?? throw new ArgumentNullException(nameof(asyncDelayer));
this.dotnetDumpService = dotnetDumpService ?? throw new ArgumentNullException(nameof(dotnetDumpService));
generalConfiguration = generalConfigurationOptions?.Value ?? throw new ArgumentNullException(nameof(generalConfigurationOptions));
sessionConfiguration = sessionConfigurationOptions?.Value ?? throw new ArgumentNullException(nameof(sessionConfigurationOptions));
}
@@ -271,7 +279,11 @@ namespace Tgstation.Server.Host.Components
var repoManager = repositoryManagerFactory.CreateRepositoryManager(repoIoManager, eventConsumer);
try
{
var engineManager = new EngineManager(byondIOManager, engineInstaller, eventConsumer, loggerFactory.CreateLogger<EngineManager>());
var engineManager = new EngineManager(
byondIOManager,
engineInstaller,
eventConsumer,
loggerFactory.CreateLogger<EngineManager>());
var dmbFactory = new DmbFactory(
databaseContextFactory,
@@ -309,6 +321,7 @@ namespace Tgstation.Server.Host.Components
serverPortProvider,
eventConsumer,
asyncDelayer,
dotnetDumpService,
loggerFactory,
loggerFactory.CreateLogger<SessionControllerFactory>(),
sessionConfiguration,
@@ -39,5 +39,10 @@
/// DreamDaemon attempting to send a longer bridge message.
/// </summary>
Chunk,
/// <summary>
/// DreamDaemon requesting a custom event to be triggered.
/// </summary>
Event,
}
}
@@ -51,6 +51,11 @@ namespace Tgstation.Server.Host.Components.Interop.Bridge
/// </summary>
public ushort? TopicPort { get; set; }
/// <summary>
/// The <see cref="Bridge.CustomEventInvocation"/> being triggered.
/// </summary>
public CustomEventInvocation? EventInvocation { get; set; }
/// <summary>
/// Initializes a new instance of the <see cref="BridgeParameters"/> class.
/// </summary>
@@ -21,5 +21,10 @@ namespace Tgstation.Server.Host.Components.Interop.Bridge
/// The <see cref="ChunkData.SequenceId"/>s missing from a chunked request.
/// </summary>
public IReadOnlyCollection<uint>? MissingChunks { get; set; }
/// <summary>
/// The triggered event ID for <see cref="BridgeCommandType.Event"/> requests.
/// </summary>
public string? EventId { get; set; }
}
}
@@ -0,0 +1,25 @@
using System.Collections.Generic;
namespace Tgstation.Server.Host.Components.Interop.Bridge
{
/// <summary>
/// Parameters for invoking a custom event.
/// </summary>
public sealed class CustomEventInvocation
{
/// <summary>
/// The name of the event being invoked.
/// </summary>
public string? EventName { get; set; }
/// <summary>
/// The parameters for the invoked event.
/// </summary>
public ICollection<string?>? Parameters { get; set; }
/// <summary>
/// If the DMAPI should be notified when the event compeletes.
/// </summary>
public bool? NotifyCompletion { get; set; }
}
}
@@ -15,12 +15,12 @@ namespace Tgstation.Server.Host.Components.Interop.Topic
/// The <see cref="EventType"/> triggered.
/// </summary>
/// <remarks>Nullable to prevent ignoring when serializing.</remarks>
public EventType? Type { get; }
public EventType Type { get; }
/// <summary>
/// The set of parameters.
/// </summary>
public IReadOnlyCollection<string?> Parameters { get; }
public IReadOnlyCollection<string?>? Parameters { get; }
/// <summary>
/// Initializes a new instance of the <see cref="EventNotification"/> class.
@@ -67,5 +67,10 @@ namespace Tgstation.Server.Host.Components.Interop.Topic
/// Sending a broadcast message.
/// </summary>
Broadcast,
/// <summary>
/// Notifying about the completion of a custom event.
/// </summary>
CompleteEvent,
}
}
@@ -62,6 +62,11 @@ namespace Tgstation.Server.Host.Components.Interop.Topic
/// </summary>
public ChunkData? Chunk { get; }
/// <summary>
/// The completed custom event ID.
/// </summary>
public string? EventId { get; set; }
/// <summary>
/// Whether or not the <see cref="TopicParameters"/> constitute a priority request.
/// </summary>
@@ -74,6 +79,7 @@ namespace Tgstation.Server.Host.Components.Interop.Topic
or TopicCommandType.InstanceRenamed
or TopicCommandType.ChatChannelsUpdate
or TopicCommandType.Broadcast
or TopicCommandType.CompleteEvent
or TopicCommandType.ServerRestarted => true,
TopicCommandType.ChatCommand
or TopicCommandType.HealthCheck
@@ -174,6 +180,16 @@ namespace Tgstation.Server.Host.Components.Interop.Topic
Chunk = chunk ?? throw new ArgumentNullException(nameof(chunk));
}
/// <summary>
/// Initializes a new instance of the <see cref="TopicParameters"/> class.
/// </summary>
/// <param name="eventId">The <see cref="Guid"/> containig the value of <see cref="EventId"/>.</param>
public TopicParameters(Guid eventId)
: this(TopicCommandType.CompleteEvent)
{
EventId = eventId.ToString();
}
/// <summary>
/// Initializes a new instance of the <see cref="TopicParameters"/> class.
/// </summary>
@@ -93,7 +93,7 @@ namespace Tgstation.Server.Host.Components.Repository
Comment = parameters.Comment,
Number = parameters.Number,
TargetCommitSha = revisionToUse,
Url = pr?.HtmlUrl ?? errorMessage,
Url = pr?.HtmlUrl ?? $"https://github.com/{RemoteRepositoryOwner}/{RemoteRepositoryName}/pull/{parameters.Number}",
};
return testMerge;
@@ -85,7 +85,7 @@ namespace Tgstation.Server.Host.Components.Repository
Comment = parameters.Comment,
Number = parameters.Number,
TargetCommitSha = parameters.TargetCommitSha,
Url = ex.Message,
Url = $"https://gitlab.com/{RemoteRepositoryOwner}/{RemoteRepositoryName}/-/merge_requests/{parameters.Number}",
};
}
}
@@ -46,6 +46,11 @@ namespace Tgstation.Server.Host.Components.Repository
/// </summary>
public const string RemoteTemporaryBranchName = "___TGSTempBranch";
/// <summary>
/// The value of <see cref="Reference"/> when not on a reference.
/// </summary>
public const string NoReference = "(no branch)";
/// <summary>
/// Used when a reference cannot be determined.
/// </summary>
@@ -22,7 +22,7 @@ namespace Tgstation.Server.Host.Components.Session
/// <summary>
/// If the DreamDaemon instance sent a.
/// </summary>
bool TerminationWasRequested { get; }
bool TerminationWasIntentional { get; }
/// <summary>
/// The DMAPI <see cref="Session.ApiValidationStatus"/>.
@@ -84,6 +84,11 @@ namespace Tgstation.Server.Host.Components.Session
/// </summary>
bool DMApiAvailable { get; }
/// <summary>
/// The file extension to use for process dumps created from this session.
/// </summary>
string DumpFileExtension { get; }
/// <summary>
/// Releases the <see cref="IProcess"/> without terminating it. Also calls <see cref="IDisposable.Dispose"/>.
/// </summary>
@@ -19,6 +19,7 @@ using Tgstation.Server.Host.Components.Chat;
using Tgstation.Server.Host.Components.Chat.Commands;
using Tgstation.Server.Host.Components.Deployment;
using Tgstation.Server.Host.Components.Engine;
using Tgstation.Server.Host.Components.Events;
using Tgstation.Server.Host.Components.Interop;
using Tgstation.Server.Host.Components.Interop.Bridge;
using Tgstation.Server.Host.Components.Interop.Topic;
@@ -63,7 +64,7 @@ namespace Tgstation.Server.Host.Components.Session
public Version? DMApiVersion { get; private set; }
/// <inheritdoc />
public bool TerminationWasRequested { get; private set; }
public bool TerminationWasIntentional => terminationWasIntentional || (Lifetime.IsCompleted && Lifetime.Result == 0);
/// <inheritdoc />
public Task<LaunchResult> LaunchResult { get; }
@@ -104,6 +105,11 @@ namespace Tgstation.Server.Host.Components.Session
/// <inheritdoc />
public bool ProcessingRebootBridgeRequest => rebootBridgeRequestsProcessing > 0;
/// <inheritdoc />
public string DumpFileExtension => engineLock.UseDotnetDump
? ".net.dmp"
: ".dmp";
/// <summary>
/// The up to date <see cref="Session.ReattachInformation"/>.
/// </summary>
@@ -149,6 +155,16 @@ namespace Tgstation.Server.Host.Components.Session
/// </summary>
readonly IAsyncDelayer asyncDelayer;
/// <summary>
/// The <see cref="IDotnetDumpService"/> for the <see cref="SessionController"/>.
/// </summary>
readonly IDotnetDumpService dotnetDumpService;
/// <summary>
/// The <see cref="IEventConsumer"/> for the <see cref="SessionController"/>.
/// </summary>
readonly IEventConsumer eventConsumer;
/// <summary>
/// The <see cref="TaskCompletionSource"/> that completes when DD makes it's first bridge request.
/// </summary>
@@ -160,9 +176,9 @@ namespace Tgstation.Server.Host.Components.Session
readonly Api.Models.Instance metadata;
/// <summary>
/// A <see cref="CancellationTokenSource"/> used for the topic send operation made on reattaching.
/// A <see cref="CancellationTokenSource"/> used for tasks that should not exceed the lifetime of the session.
/// </summary>
readonly CancellationTokenSource reattachTopicCts;
readonly CancellationTokenSource sessionDurationCts;
/// <summary>
/// <see langword="lock"/> <see cref="object"/> for port updates and <see cref="disposed"/>.
@@ -194,6 +210,11 @@ namespace Tgstation.Server.Host.Components.Session
/// </summary>
volatile Task rebootGate;
/// <summary>
/// The <see cref="Task"/> representing calls to <see cref="TriggerCustomEvent(CustomEventInvocation?)"/>.
/// </summary>
volatile Task customEventProcessingTask;
/// <summary>
/// <see cref="Task"/> for shutting down the server if it is taking too long after validation.
/// </summary>
@@ -219,6 +240,11 @@ namespace Tgstation.Server.Host.Components.Session
/// </summary>
bool released;
/// <summary>
/// Backing field for overriding <see cref="TerminationWasIntentional"/>.
/// </summary>
bool terminationWasIntentional;
/// <summary>
/// Initializes a new instance of the <see cref="SessionController"/> class.
/// </summary>
@@ -231,7 +257,9 @@ namespace Tgstation.Server.Host.Components.Session
/// <param name="chat">The value of <see cref="chat"/>.</param>
/// <param name="chatTrackingContext">The value of <see cref="chatTrackingContext"/>.</param>
/// <param name="assemblyInformationProvider">The <see cref="IAssemblyInformationProvider"/> for the <see cref="SessionController"/>.</param>
/// <param name="asyncDelayer">The <see cref="IAsyncDelayer"/> for the <see cref="SessionController"/>.</param>
/// <param name="asyncDelayer">The value of <see cref="asyncDelayer"/>.</param>
/// <param name="dotnetDumpService">The value of <see cref="dotnetDumpService"/>.</param>
/// <param name="eventConsumer">The value of <see cref="eventConsumer"/>.</param>
/// <param name="logger">The value of <see cref="Chunker.Logger"/>.</param>
/// <param name="postLifetimeCallback">The <see cref="Func{TResult}"/> returning a <see cref="ValueTask"/> to be run after the <paramref name="process"/> ends.</param>
/// <param name="startupTimeout">The optional time to wait before failing the <see cref="LaunchResult"/>.</param>
@@ -248,6 +276,8 @@ namespace Tgstation.Server.Host.Components.Session
IChatManager chat,
IAssemblyInformationProvider assemblyInformationProvider,
IAsyncDelayer asyncDelayer,
IDotnetDumpService dotnetDumpService,
IEventConsumer eventConsumer,
ILogger<SessionController> logger,
Func<ValueTask> postLifetimeCallback,
uint? startupTimeout,
@@ -267,6 +297,8 @@ namespace Tgstation.Server.Host.Components.Session
ArgumentNullException.ThrowIfNull(assemblyInformationProvider);
this.asyncDelayer = asyncDelayer ?? throw new ArgumentNullException(nameof(asyncDelayer));
this.dotnetDumpService = dotnetDumpService ?? throw new ArgumentNullException(nameof(dotnetDumpService));
this.eventConsumer = eventConsumer ?? throw new ArgumentNullException(nameof(eventConsumer));
apiValidationSession = apiValidate;
@@ -279,12 +311,13 @@ namespace Tgstation.Server.Host.Components.Session
primeTcs = new TaskCompletionSource();
rebootGate = Task.CompletedTask;
customEventProcessingTask = Task.CompletedTask;
// Run this asynchronously because we want to try to avoid any effects sending topics to the server while the initial bridge request is processing
// It MAY be the source of a DD crash. See this gist https://gist.github.com/Cyberboss/7776bbeff3a957d76affe0eae95c9f14
// Worth further investigation as to if that sequence of events is a reliable crash vector and opening a BYOND bug if it is
initialBridgeRequestTcs = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously);
reattachTopicCts = new CancellationTokenSource();
sessionDurationCts = new CancellationTokenSource();
TopicSendSemaphore = new FifoSemaphore();
synchronizationLock = new object();
@@ -338,7 +371,7 @@ namespace Tgstation.Server.Host.Components.Session
Logger.LogTrace("Disposing...");
reattachTopicCts.Cancel();
sessionDurationCts.Cancel();
var cancellationToken = CancellationToken.None; // DCT: None available
var semaphoreLockTask = TopicSendSemaphore.Lock(cancellationToken);
@@ -363,13 +396,15 @@ namespace Tgstation.Server.Host.Components.Session
await regularDmbDisposeTask;
chatTrackingContext.Dispose();
reattachTopicCts.Dispose();
sessionDurationCts.Dispose();
if (!released)
await Lifetime; // finish the async callback
(await semaphoreLockTask).Dispose();
TopicSendSemaphore.Dispose();
await customEventProcessingTask;
}
/// <inheritdoc />
@@ -469,7 +504,13 @@ namespace Tgstation.Server.Host.Components.Session
cancellationToken);
/// <inheritdoc />
public ValueTask CreateDump(string outputFile, CancellationToken cancellationToken) => process.CreateDump(outputFile, cancellationToken);
public ValueTask CreateDump(string outputFile, bool minidump, CancellationToken cancellationToken)
{
if (engineLock.UseDotnetDump)
return dotnetDumpService.Dump(process, outputFile, minidump, cancellationToken);
return process.CreateDump(outputFile, minidump, cancellationToken);
}
/// <summary>
/// The <see cref="Task{TResult}"/> for <see cref="LaunchResult"/>.
@@ -523,7 +564,7 @@ namespace Tgstation.Server.Host.Components.Session
assemblyInformationProvider.Version,
ReattachInformation.RuntimeInformation!.ServerPort),
true,
reattachTopicCts.Token);
sessionDurationCts.Token);
if (reattachResponse != null)
{
@@ -622,7 +663,7 @@ namespace Tgstation.Server.Host.Components.Session
case BridgeCommandType.Kill:
Logger.LogInformation("Bridge requested process termination!");
chatTrackingContext.Active = false;
TerminationWasRequested = true;
terminationWasIntentional = true;
process.Terminate();
break;
case BridgeCommandType.DeprecatedPortUpdate:
@@ -711,6 +752,8 @@ namespace Tgstation.Server.Host.Components.Session
break;
case BridgeCommandType.Chunk:
return await ProcessChunk<BridgeParameters, BridgeResponse>(ProcessBridgeCommand, BridgeError, parameters.Chunk, cancellationToken);
case BridgeCommandType.Event:
return TriggerCustomEvent(parameters.EventInvocation);
case null:
return BridgeError("Missing commandType!");
default:
@@ -1078,5 +1121,81 @@ namespace Tgstation.Server.Host.Components.Session
return fullResponse;
}
/// <summary>
/// Trigger a custom event from a given <paramref name="invocation"/>.
/// </summary>
/// <param name="invocation">The <see cref="CustomEventInvocation"/>.</param>
/// <returns>An appropriate <see cref="BridgeResponse"/>.</returns>
BridgeResponse TriggerCustomEvent(CustomEventInvocation? invocation)
{
if (invocation == null)
return BridgeError("Missing eventInvocation!");
var eventName = invocation.EventName;
if (eventName == null)
return BridgeError("Missing eventName!");
var notifyCompletion = invocation.NotifyCompletion;
if (!notifyCompletion.HasValue)
return BridgeError("Missing notifyCompletion!");
var eventParams = new List<string>
{
ReattachInformation.Dmb.Directory,
};
eventParams.AddRange(invocation
.Parameters?
.Where(param => param != null)
.Cast<string>()
?? Enumerable.Empty<string>());
var eventId = Guid.NewGuid();
Logger.LogInformation("Triggering custom event \"{eventName}\": {eventId}", eventName, eventId);
var cancellationToken = sessionDurationCts.Token;
ValueTask? eventTask = eventConsumer.HandleCustomEvent(eventName, eventParams, cancellationToken);
async Task ProcessEvent()
{
try
{
await eventTask.Value;
if (notifyCompletion.Value)
await SendCommand(
new TopicParameters(eventId),
cancellationToken);
else
Logger.LogTrace("Finished custom event {eventId}, not sending notification.", eventId);
}
catch (OperationCanceledException ex)
{
Logger.LogDebug(ex, "Custom event invocation {eventId} aborted!", eventId);
}
catch (Exception ex)
{
Logger.LogWarning(ex, "Custom event invocation {eventId} errored!", eventId);
}
}
if (!eventTask.HasValue)
return BridgeError("Event refused to execute due to matching a TGS event!");
lock (sessionDurationCts)
{
var previousEventProcessingTask = customEventProcessingTask;
var eventProcessingTask = ProcessEvent();
customEventProcessingTask = Task.WhenAll(customEventProcessingTask, eventProcessingTask);
}
return new BridgeResponse
{
EventId = notifyCompletion.Value
? eventId.ToString()
: null,
};
}
}
}
@@ -106,6 +106,11 @@ namespace Tgstation.Server.Host.Components.Session
/// </summary>
readonly IAsyncDelayer asyncDelayer;
/// <summary>
/// The <see cref="IDotnetDumpService"/> for the <see cref="SessionControllerFactory"/>.
/// </summary>
readonly IDotnetDumpService dotnetDumpService;
/// <summary>
/// The <see cref="ILoggerFactory"/> for the <see cref="SessionControllerFactory"/>.
/// </summary>
@@ -178,6 +183,7 @@ namespace Tgstation.Server.Host.Components.Session
/// <param name="serverPortProvider">The value of <see cref="serverPortProvider"/>.</param>
/// <param name="eventConsumer">The value of <see cref="eventConsumer"/>.</param>
/// <param name="asyncDelayer">The value of <see cref="asyncDelayer"/>.</param>
/// <param name="dotnetDumpService">The value of <see cref="dotnetDumpService"/>.</param>
/// <param name="loggerFactory">The value of <see cref="loggerFactory"/>.</param>
/// <param name="logger">The value of <see cref="logger"/>.</param>
/// <param name="sessionConfiguration">The value of <see cref="sessionConfiguration"/>.</param>
@@ -196,6 +202,7 @@ namespace Tgstation.Server.Host.Components.Session
IServerPortProvider serverPortProvider,
IEventConsumer eventConsumer,
IAsyncDelayer asyncDelayer,
IDotnetDumpService dotnetDumpService,
ILoggerFactory loggerFactory,
ILogger<SessionControllerFactory> logger,
SessionConfiguration sessionConfiguration,
@@ -215,6 +222,7 @@ namespace Tgstation.Server.Host.Components.Session
this.serverPortProvider = serverPortProvider ?? throw new ArgumentNullException(nameof(serverPortProvider));
this.eventConsumer = eventConsumer ?? throw new ArgumentNullException(nameof(eventConsumer));
this.asyncDelayer = asyncDelayer ?? throw new ArgumentNullException(nameof(asyncDelayer));
this.dotnetDumpService = dotnetDumpService ?? throw new ArgumentNullException(nameof(dotnetDumpService));
this.loggerFactory = loggerFactory ?? throw new ArgumentNullException(nameof(loggerFactory));
this.logger = logger ?? throw new ArgumentNullException(nameof(logger));
this.sessionConfiguration = sessionConfiguration ?? throw new ArgumentNullException(nameof(sessionConfiguration));
@@ -346,6 +354,8 @@ namespace Tgstation.Server.Host.Components.Session
chat,
assemblyInformationProvider,
asyncDelayer,
dotnetDumpService,
eventConsumer,
loggerFactory.CreateLogger<SessionController>(),
() => LogDDOutput(
process,
@@ -436,6 +446,8 @@ namespace Tgstation.Server.Host.Components.Session
chat,
assemblyInformationProvider,
asyncDelayer,
dotnetDumpService,
eventConsumer,
loggerFactory.CreateLogger<SessionController>(),
() => ValueTask.CompletedTask,
null,
@@ -490,6 +502,7 @@ namespace Tgstation.Server.Host.Components.Session
CancellationToken cancellationToken)
{
// important to run on all ports to allow port changing
var environment = await engineLock.LoadEnv(logger, false, cancellationToken);
var arguments = engineLock.FormatServerArguments(
dmbProvider,
new Dictionary<string, string>
@@ -503,10 +516,12 @@ namespace Tgstation.Server.Host.Components.Session
? logFilePath
: null);
var process = processExecutor.LaunchProcess(
var process = await processExecutor.LaunchProcess(
engineLock.ServerExePath,
dmbProvider.Directory,
arguments,
cancellationToken,
environment,
logFilePath,
engineLock.HasStandardOutput,
true);
@@ -71,11 +71,11 @@ namespace Tgstation.Server.Host.Components.StaticFiles
/// <summary>
/// Map of <see cref="EventType"/>s to the filename of the event scripts they trigger.
/// </summary>
public static IReadOnlyDictionary<EventType, IReadOnlyList<string>> EventTypeScriptFileNameMap { get; } = new Dictionary<EventType, IReadOnlyList<string>>(
public static IReadOnlyDictionary<EventType, string[]> EventTypeScriptFileNameMap { get; } = new Dictionary<EventType, string[]>(
Enum.GetValues(typeof(EventType))
.Cast<EventType>()
.Select(
eventType => new KeyValuePair<EventType, IReadOnlyList<string>>(
eventType => new KeyValuePair<EventType, string[]>(
eventType,
typeof(EventType)
.GetField(eventType.ToString())!
@@ -600,70 +600,39 @@ namespace Tgstation.Server.Host.Components.StaticFiles
public Task StopAsync(CancellationToken cancellationToken) => EnsureDirectories(cancellationToken);
/// <inheritdoc />
public async ValueTask HandleEvent(EventType eventType, IEnumerable<string?> parameters, bool deploymentPipeline, CancellationToken cancellationToken)
public ValueTask HandleEvent(EventType eventType, IEnumerable<string?> parameters, bool deploymentPipeline, CancellationToken cancellationToken)
{
ArgumentNullException.ThrowIfNull(parameters);
await EnsureDirectories(cancellationToken);
if (!EventTypeScriptFileNameMap.TryGetValue(eventType, out var scriptNames))
return;
// always execute in serial
using (await SemaphoreSlimContext.Lock(semaphore, cancellationToken))
{
var files = await ioManager.GetFilesWithExtension(EventScriptsSubdirectory, platformIdentifier.ScriptFileExtension, false, cancellationToken);
var resolvedScriptsDir = ioManager.ResolvePath(EventScriptsSubdirectory);
var scriptFiles = files
.Select(x => ioManager.GetFileName(x))
.Where(x => scriptNames.Any(
scriptName => x.StartsWith(scriptName, StringComparison.Ordinal)))
.ToList();
if (scriptFiles.Count == 0)
{
logger.LogTrace("No event scripts starting with \"{scriptName}\" detected", String.Join("\" or \"", scriptNames));
return;
}
foreach (var scriptFile in scriptFiles)
{
logger.LogTrace("Running event script {scriptFile}...", scriptFile);
await using (var script = processExecutor.LaunchProcess(
ioManager.ConcatPath(resolvedScriptsDir, scriptFile),
resolvedScriptsDir,
String.Join(
' ',
parameters.Select(arg =>
{
if (arg == null)
return "(NULL)";
if (!arg.Contains(' ', StringComparison.Ordinal))
return arg;
arg = arg.Replace("\"", "\\\"", StringComparison.Ordinal);
return $"\"{arg}\"";
})),
readStandardHandles: true,
noShellExecute: true))
using (cancellationToken.Register(() => script.Terminate()))
{
if (sessionConfiguration.LowPriorityDeploymentProcesses)
script.AdjustPriority(false);
var exitCode = await script.Lifetime;
cancellationToken.ThrowIfCancellationRequested();
var scriptOutput = await script.GetCombinedOutput(cancellationToken);
if (exitCode != 0)
throw new JobException($"Script {scriptFile} exited with code {exitCode}:{Environment.NewLine}{scriptOutput}");
else
logger.LogDebug("Script output:{newLine}{scriptOutput}", Environment.NewLine, scriptOutput);
}
}
logger.LogTrace("No event script for event {event}!", eventType);
return ValueTask.CompletedTask;
}
return ExecuteEventScripts(parameters, deploymentPipeline, cancellationToken, scriptNames);
}
/// <inheritdoc />
public ValueTask? HandleCustomEvent(string scriptName, IEnumerable<string?> parameters, CancellationToken cancellationToken)
{
var scriptNameIsTgsEventName = EventTypeScriptFileNameMap
.Values
.SelectMany(scriptNames => scriptNames)
.Any(tgsScriptName => tgsScriptName.Equals(
scriptName,
platformIdentifier.IsWindows
? StringComparison.OrdinalIgnoreCase
: StringComparison.Ordinal));
if (scriptNameIsTgsEventName)
{
logger.LogWarning("DMAPI attempted to execute TGS reserved event: {eventName}", scriptName);
return null;
}
#pragma warning disable CA2012 // Use ValueTasks correctly
return ExecuteEventScripts(parameters, false, cancellationToken, scriptName);
#pragma warning restore CA2012 // Use ValueTasks correctly
}
/// <inheritdoc />
@@ -758,5 +727,75 @@ namespace Tgstation.Server.Host.Components.StaticFiles
throw new InvalidOperationException("Attempted to access file outside of configuration manager!");
return resolved;
}
/// <summary>
/// Execute a set of given <paramref name="scriptNames"/>.
/// </summary>
/// <param name="parameters">An <see cref="IEnumerable{T}"/> of <see cref="string"/> parameters for the <paramref name="scriptNames"/>.</param>
/// <param name="deploymentPipeline">If this event is part of the deployment pipeline.</param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> for the operation.</param>
/// <param name="scriptNames">The names of the scripts to execute.</param>
/// <returns>A <see cref="ValueTask"/> representing the running operation.</returns>
async ValueTask ExecuteEventScripts(IEnumerable<string?> parameters, bool deploymentPipeline, CancellationToken cancellationToken, params string[] scriptNames)
{
await EnsureDirectories(cancellationToken);
// always execute in serial
using (await SemaphoreSlimContext.Lock(semaphore, cancellationToken))
{
var files = await ioManager.GetFilesWithExtension(EventScriptsSubdirectory, platformIdentifier.ScriptFileExtension, false, cancellationToken);
var resolvedScriptsDir = ioManager.ResolvePath(EventScriptsSubdirectory);
var scriptFiles = files
.Select(x => ioManager.GetFileName(x))
.Where(x => scriptNames.Any(
scriptName => x.StartsWith(scriptName, StringComparison.Ordinal)))
.ToList();
if (scriptFiles.Count == 0)
{
logger.LogTrace("No event scripts starting with \"{scriptName}\" detected", String.Join("\" or \"", scriptNames));
return;
}
foreach (var scriptFile in scriptFiles)
{
logger.LogTrace("Running event script {scriptFile}...", scriptFile);
await using (var script = await processExecutor.LaunchProcess(
ioManager.ConcatPath(resolvedScriptsDir, scriptFile),
resolvedScriptsDir,
String.Join(
' ',
parameters.Select(arg =>
{
if (arg == null)
return "(NULL)";
if (!arg.Contains(' ', StringComparison.Ordinal))
return arg;
arg = arg.Replace("\"", "\\\"", StringComparison.Ordinal);
return $"\"{arg}\"";
})),
cancellationToken,
readStandardHandles: true,
noShellExecute: true))
using (cancellationToken.Register(() => script.Terminate()))
{
if (sessionConfiguration.LowPriorityDeploymentProcesses && deploymentPipeline)
script.AdjustPriority(false);
var exitCode = await script.Lifetime;
cancellationToken.ThrowIfCancellationRequested();
var scriptOutput = await script.GetCombinedOutput(cancellationToken);
if (exitCode != 0)
throw new JobException($"Script {scriptFile} exited with code {exitCode}:{Environment.NewLine}{scriptOutput}");
else
logger.LogDebug("Script output:{newLine}{scriptOutput}", Environment.NewLine, scriptOutput);
}
}
}
}
}
}
@@ -127,8 +127,12 @@ namespace Tgstation.Server.Host.Components.Watchdog
// If we reach this point, we can guarantee PrepServerForLaunch will be called before starting again.
ActiveSwappable = null;
await (pendingSwappable?.DisposeAsync() ?? ValueTask.CompletedTask);
pendingSwappable = null;
if (pendingSwappable != null)
{
await pendingSwappable.DisposeAsync();
pendingSwappable = null;
}
await DrainDeploymentCleanupTasks(true);
}
@@ -138,8 +142,6 @@ namespace Tgstation.Server.Host.Components.Watchdog
{
if (pendingSwappable != null)
{
ValueTask RunPrequel() => BeforeApplyDmb(pendingSwappable.CompileJob, cancellationToken);
var needToSwap = !pendingSwappable.Swapped;
var controller = Server!;
if (needToSwap)
@@ -151,7 +153,6 @@ namespace Tgstation.Server.Host.Components.Watchdog
// 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;
}
@@ -165,7 +166,7 @@ namespace Tgstation.Server.Host.Components.Watchdog
}
}
var updateTask = RunPrequel();
var updateTask = BeforeApplyDmb(pendingSwappable.CompileJob, cancellationToken);
if (needToSwap)
await PerformDmbSwap(pendingSwappable, cancellationToken);
@@ -183,7 +184,8 @@ namespace Tgstation.Server.Host.Components.Watchdog
currentCompileJobId,
lingeringDeploymentExpirySeconds);
var timeout = AsyncDelayer.Delay(TimeSpan.FromSeconds(lingeringDeploymentExpirySeconds), cancellationToken);
// DCT: A cancel firing here can result in us leaving a dmbprovider undisposed, localDeploymentCleanupGate will always fire in that case
var timeout = AsyncDelayer.Delay(TimeSpan.FromSeconds(lingeringDeploymentExpirySeconds), CancellationToken.None);
var completedTask = await Task.WhenAny(
localDeploymentCleanupGate.Task,
@@ -113,12 +113,12 @@ namespace Tgstation.Server.Host.Components.Watchdog
switch (reason)
{
case MonitorActivationReason.ActiveServerCrashed:
var eventType = controller.TerminationWasRequested || (await controller.Lifetime) == 0
var eventType = controller.TerminationWasIntentional
? EventType.WorldEndProcess
: EventType.WatchdogCrash;
await HandleEventImpl(eventType, Enumerable.Empty<string>(), false, cancellationToken);
var exitWord = controller.TerminationWasRequested ? "exited" : "crashed";
var exitWord = controller.TerminationWasIntentional ? "exited" : "crashed";
if (controller.RebootState == Session.RebootState.Shutdown)
{
// the time for graceful shutdown is now
@@ -95,6 +95,6 @@ namespace Tgstation.Server.Host.Components.Watchdog
/// <inheritdoc />
protected override SwappableDmbProvider CreateSwappableDmbProvider(IDmbProvider dmbProvider)
=> new HardLinkDmbProvider(dmbProvider, GameIOManager, LinkFactory, Logger, generalConfiguration);
=> new HardLinkDmbProvider(dmbProvider, GameIOManager, LinkFactory, Logger, generalConfiguration, ActiveLaunchParameters.SecurityLevel!.Value);
}
}
@@ -507,6 +507,10 @@ namespace Tgstation.Server.Host.Components.Watchdog
HandleChatResponses(result);
}
/// <inheritdoc />
ValueTask? IEventConsumer.HandleCustomEvent(string eventName, IEnumerable<string?> parameters, CancellationToken cancellationToken)
=> throw new NotSupportedException("Watchdogs do not support custom events!");
/// <summary>
/// Starts all <see cref="ISessionController"/>s.
/// </summary>
@@ -1225,25 +1229,31 @@ namespace Tgstation.Server.Host.Components.Watchdog
async ValueTask CreateDumpNoLock(CancellationToken cancellationToken)
{
const string DumpDirectory = "ProcessDumps";
var dumpFileNameTemplate = diagnosticsIOManager.ResolvePath(
diagnosticsIOManager.ConcatPath(
DumpDirectory,
$"DreamDaemon-{DateTimeOffset.UtcNow.ToFileStamp()}.dmp"));
var dumpFileName = dumpFileNameTemplate;
var iteration = 0;
while (await diagnosticsIOManager.FileExists(dumpFileName, cancellationToken))
dumpFileName = $"{dumpFileNameTemplate} ({++iteration})";
if (iteration == 0)
await diagnosticsIOManager.CreateDirectory(DumpDirectory, cancellationToken);
var session = GetActiveController();
if (session?.Lifetime.IsCompleted != false)
throw new JobException(ErrorCode.GameServerOffline);
var dumpFileExtension = session.DumpFileExtension;
var dumpFileNameTemplate = diagnosticsIOManager.ResolvePath(
diagnosticsIOManager.ConcatPath(
DumpDirectory,
$"DreamDaemon-{DateTimeOffset.UtcNow.ToFileStamp()}"));
var dumpFileName = $"{dumpFileNameTemplate}{dumpFileExtension}";
var iteration = 0;
while (await diagnosticsIOManager.FileExists(dumpFileName, cancellationToken))
dumpFileName = $"{dumpFileNameTemplate} ({++iteration}){dumpFileExtension}";
if (iteration == 0)
await diagnosticsIOManager.CreateDirectory(DumpDirectory, cancellationToken);
if (session.Lifetime.IsCompleted)
throw new JobException(ErrorCode.GameServerOffline);
Logger.LogInformation("Dumping session to {dumpFileName}...", dumpFileName);
await session.CreateDump(dumpFileName, cancellationToken);
await session.CreateDump(dumpFileName, ActiveLaunchParameters.Minidumps!.Value, cancellationToken);
}
}
}
@@ -187,6 +187,9 @@ namespace Tgstation.Server.Host.Configuration
else if (this.GetCopyDirectoryTaskThrottle() < 1)
throw new InvalidOperationException(
$"{nameof(DeploymentDirectoryCopyTasksPerCore)} is too large for the CPU core count of {Environment.ProcessorCount} and overflows a 32-bit signed integer. Please lower the value!");
if (ByondTopicTimeout <= 1000)
logger.LogWarning("The timeout for sending BYOND topics is very low ({ms}ms). Topic calls may fail to complete at all!", ByondTopicTimeout);
}
}
}
@@ -357,7 +357,7 @@ namespace Tgstation.Server.Host.Controllers
var identExpiry = token.ParseJwt().ValidTo;
identExpiry += tokenFactory.ValidationParameters.ClockSkew;
identExpiry += TimeSpan.FromSeconds(15);
identityCache.CacheSystemIdentity(user, systemIdentity!, identExpiry);
await identityCache.CacheSystemIdentity(user, systemIdentity!, identExpiry);
}
Logger.LogDebug("Successfully logged in user {userId}!", user.Id);
@@ -30,6 +30,11 @@ namespace Tgstation.Server.Host.Controllers
/// </summary>
public const string ControlPanelRoute = "/app";
/// <summary>
/// The route to the control panel channel .json.
/// </summary>
public const string ChannelJsonRoute = "channel.json";
/// <summary>
/// Header for forcing channel.json to be fetched.
/// </summary>
@@ -70,7 +75,7 @@ namespace Tgstation.Server.Host.Controllers
/// Returns the <see cref="ControlPanelConfiguration.Channel"/>.
/// </summary>
/// <returns>A <see cref="JsonResult"/> with the <see cref="ControlPanelConfiguration.Channel"/>.</returns>
[Route("channel.json")]
[Route(ChannelJsonRoute)]
[HttpGet]
public IActionResult GetChannelJson()
{
@@ -149,7 +149,8 @@ namespace Tgstation.Server.Host.Controllers
| DreamDaemonRights.SetProfiler
| DreamDaemonRights.SetLogOutput
| DreamDaemonRights.SetMapThreads
| DreamDaemonRights.BroadcastMessage)]
| DreamDaemonRights.BroadcastMessage
| DreamDaemonRights.SetMinidumps)]
[ProducesResponseType(typeof(DreamDaemonResponse), 200)]
[ProducesResponseType(typeof(ErrorMessageResponse), 410)]
#pragma warning disable CA1502 // TODO: Decomplexify
@@ -222,7 +223,8 @@ namespace Tgstation.Server.Host.Controllers
|| CheckModified(x => x.AdditionalParameters, DreamDaemonRights.SetAdditionalParameters)
|| CheckModified(x => x.StartProfiler, DreamDaemonRights.SetProfiler)
|| CheckModified(x => x.LogOutput, DreamDaemonRights.SetLogOutput)
|| CheckModified(x => x.MapThreads, DreamDaemonRights.SetMapThreads))
|| CheckModified(x => x.MapThreads, DreamDaemonRights.SetMapThreads)
|| CheckModified(x => x.Minidumps, DreamDaemonRights.SetMinidumps))
return Forbid();
return await WithComponentInstance(
@@ -379,6 +381,7 @@ namespace Tgstation.Server.Host.Controllers
result.StartProfiler = settings.StartProfiler;
result.LogOutput = settings.LogOutput;
result.MapThreads = settings.MapThreads;
result.Minidumps = settings.Minidumps;
}
if (revision)
@@ -171,7 +171,8 @@ namespace Tgstation.Server.Host.Controllers
| DreamMakerRights.SetApiValidationPort
| DreamMakerRights.SetSecurityLevel
| DreamMakerRights.SetApiValidationRequirement
| DreamMakerRights.SetTimeout)]
| DreamMakerRights.SetTimeout
| DreamMakerRights.SetCompilerArguments)]
[ProducesResponseType(typeof(DreamMakerResponse), 200)]
[ProducesResponseType(204)]
[ProducesResponseType(typeof(ErrorMessageResponse), 410)]
@@ -196,7 +197,8 @@ namespace Tgstation.Server.Host.Controllers
{
if (!dreamMakerRights.HasFlag(DreamMakerRights.SetDme))
return Forbid();
if (model.ProjectName.Length == 0)
if (model.ProjectName.Length == 0) // can't use isnullorwhitespace because linux memes
hostModel.ProjectName = null;
else
hostModel.ProjectName = model.ProjectName;
@@ -230,6 +232,7 @@ namespace Tgstation.Server.Host.Controllers
{
if (!dreamMakerRights.HasFlag(DreamMakerRights.SetSecurityLevel))
return Forbid();
hostModel.ApiValidationSecurityLevel = model.ApiValidationSecurityLevel;
}
@@ -237,6 +240,7 @@ namespace Tgstation.Server.Host.Controllers
{
if (!dreamMakerRights.HasFlag(DreamMakerRights.SetApiValidationRequirement))
return Forbid();
hostModel.RequireDMApiValidation = model.RequireDMApiValidation;
}
@@ -244,9 +248,22 @@ namespace Tgstation.Server.Host.Controllers
{
if (!dreamMakerRights.HasFlag(DreamMakerRights.SetTimeout))
return Forbid();
hostModel.Timeout = model.Timeout;
}
if (model.CompilerAdditionalArguments != null)
{
if (!dreamMakerRights.HasFlag(DreamMakerRights.SetCompilerArguments))
return Forbid();
var sanitizedArguments = model.CompilerAdditionalArguments.Trim();
if (sanitizedArguments.Length == 0)
hostModel.CompilerAdditionalArguments = null;
else
hostModel.CompilerAdditionalArguments = sanitizedArguments;
}
await DatabaseContext.Save(cancellationToken);
if (!dreamMakerRights.HasFlag(DreamMakerRights.Read))
@@ -732,6 +732,7 @@ namespace Tgstation.Server.Host.Controllers
StartProfiler = false,
LogOutput = false,
MapThreads = 0,
Minidumps = true,
},
DreamMakerSettings = new DreamMakerSettings
{
@@ -739,6 +740,7 @@ namespace Tgstation.Server.Host.Controllers
ApiValidationSecurityLevel = DreamDaemonSecurity.Safe,
RequireDMApiValidation = true,
Timeout = TimeSpan.FromHours(1),
CompilerAdditionalArguments = null,
},
Name = initialSettings.Name,
Online = false,

Some files were not shown because too many files have changed in this diff Show More