mirror of
https://github.com/tgstation/tgstation-server.git
synced 2026-07-14 17:43:19 +01:00
Merge branch 'V6' into TheGreatNullening
This commit is contained in:
@@ -2,6 +2,7 @@
|
||||
# Does CI on push/PR/cron. Deployments on push when triggered
|
||||
# - Validates Documentation
|
||||
# - Builds C# and DMAPI
|
||||
# - Runs CodeQL Anaylsis
|
||||
# - Tests everything on massive matrix
|
||||
# - Packages
|
||||
# - Tests package installs/services/uninstalls
|
||||
@@ -19,7 +20,7 @@ name: 'CI Pipeline'
|
||||
|
||||
on:
|
||||
schedule:
|
||||
- cron: 0 23 * * *
|
||||
- cron: 0 9 * * *
|
||||
push:
|
||||
branches:
|
||||
- dev
|
||||
@@ -136,11 +137,11 @@ jobs:
|
||||
exit 0
|
||||
|
||||
- name: Checkout (Branch)
|
||||
uses: actions/checkout@v3
|
||||
uses: actions/checkout@v4
|
||||
if: github.event_name == 'push' || github.event_name == 'schedule'
|
||||
|
||||
- name: Checkout (PR Merge)
|
||||
uses: actions/checkout@v3
|
||||
uses: actions/checkout@v4
|
||||
if: github.event_name != 'push' && github.event_name != 'schedule'
|
||||
with:
|
||||
ref: "refs/pull/${{ github.event.number }}/merge"
|
||||
@@ -182,11 +183,11 @@ jobs:
|
||||
dotnet-quality: ${{ env.TGS_DOTNET_QUALITY }}
|
||||
|
||||
- name: Checkout (Branch)
|
||||
uses: actions/checkout@v3
|
||||
uses: actions/checkout@v4
|
||||
if: github.event_name == 'push' || github.event_name == 'schedule'
|
||||
|
||||
- name: Checkout (PR Merge)
|
||||
uses: actions/checkout@v3
|
||||
uses: actions/checkout@v4
|
||||
if: github.event_name != 'push' && github.event_name != 'schedule'
|
||||
with:
|
||||
ref: "refs/pull/${{ github.event.number }}/merge"
|
||||
@@ -222,11 +223,11 @@ jobs:
|
||||
dotnet-quality: ${{ env.TGS_DOTNET_QUALITY }}
|
||||
|
||||
- name: Checkout (Branch)
|
||||
uses: actions/checkout@v3
|
||||
uses: actions/checkout@v4
|
||||
if: github.event_name == 'push' || github.event_name == 'schedule'
|
||||
|
||||
- name: Checkout (PR Merge)
|
||||
uses: actions/checkout@v3
|
||||
uses: actions/checkout@v4
|
||||
if: github.event_name != 'push' && github.event_name != 'schedule'
|
||||
with:
|
||||
ref: "refs/pull/${{ github.event.number }}/merge"
|
||||
@@ -286,11 +287,11 @@ jobs:
|
||||
if: (!(cancelled() || failure()) && needs.start-ci-run-gate.result == 'success')
|
||||
steps:
|
||||
- name: Checkout (Branch)
|
||||
uses: actions/checkout@v3
|
||||
uses: actions/checkout@v4
|
||||
if: github.event_name == 'push' || github.event_name == 'schedule'
|
||||
|
||||
- name: Checkout (PR Merge)
|
||||
uses: actions/checkout@v3
|
||||
uses: actions/checkout@v4
|
||||
if: github.event_name != 'push' && github.event_name != 'schedule'
|
||||
with:
|
||||
ref: "refs/pull/${{ github.event.number }}/merge"
|
||||
@@ -324,11 +325,11 @@ jobs:
|
||||
dotnet-quality: ${{ env.TGS_DOTNET_QUALITY }}
|
||||
|
||||
- name: Checkout (Branch)
|
||||
uses: actions/checkout@v3
|
||||
uses: actions/checkout@v4
|
||||
if: github.event_name == 'push' || github.event_name == 'schedule'
|
||||
|
||||
- name: Checkout (PR Merge)
|
||||
uses: actions/checkout@v3
|
||||
uses: actions/checkout@v4
|
||||
if: github.event_name != 'push' && github.event_name != 'schedule'
|
||||
with:
|
||||
ref: "refs/pull/${{ github.event.number }}/merge"
|
||||
@@ -375,11 +376,11 @@ jobs:
|
||||
dotnet-quality: ${{ env.TGS_DOTNET_QUALITY }}
|
||||
|
||||
- name: Checkout (Branch)
|
||||
uses: actions/checkout@v3
|
||||
uses: actions/checkout@v4
|
||||
if: github.event_name == 'push' || github.event_name == 'schedule'
|
||||
|
||||
- name: Checkout (PR Merge)
|
||||
uses: actions/checkout@v3
|
||||
uses: actions/checkout@v4
|
||||
if: github.event_name != 'push' && github.event_name != 'schedule'
|
||||
with:
|
||||
ref: "refs/pull/${{ github.event.number }}/merge"
|
||||
@@ -490,11 +491,11 @@ jobs:
|
||||
echo "TGS_TEST_DATABASE_TYPE=SqlServer" >> $GITHUB_ENV
|
||||
|
||||
- name: Checkout (Branch)
|
||||
uses: actions/checkout@v3
|
||||
uses: actions/checkout@v4
|
||||
if: github.event_name == 'push' || github.event_name == 'schedule'
|
||||
|
||||
- name: Checkout (PR Merge)
|
||||
uses: actions/checkout@v3
|
||||
uses: actions/checkout@v4
|
||||
if: github.event_name != 'push' && github.event_name != 'schedule'
|
||||
with:
|
||||
ref: "refs/pull/${{ github.event.number }}/merge"
|
||||
@@ -531,11 +532,19 @@ jobs:
|
||||
fi
|
||||
|
||||
- name: Store Live Tests Output
|
||||
if: ${{ steps.live-tests.outputs.succeeded == 'YES' }}
|
||||
uses: actions/upload-artifact@v3
|
||||
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
|
||||
with:
|
||||
name: errored-windows-test-logs-${{ matrix.configuration }}-${{ matrix.watchdog-type }}-${{ matrix.database-type }}
|
||||
path: ./test_output.txt
|
||||
|
||||
- name: Fail if Live Tests Failed
|
||||
if: ${{ steps.live-tests.outputs.succeeded != 'YES' }}
|
||||
run: exit 1
|
||||
@@ -593,14 +602,14 @@ jobs:
|
||||
if: (!(cancelled() || failure()) && needs.dmapi-build.result == 'success' && needs.opendream-build.result == 'success')
|
||||
services: # We start all dbs here so we can just code the stuff once
|
||||
mssql:
|
||||
image: mcr.microsoft.com/mssql/server:2019-latest
|
||||
image: ${{ (matrix.database-type == 'SqlServer') && 'mcr.microsoft.com/mssql/server:2019-latest' || '' }}
|
||||
env:
|
||||
SA_PASSWORD: myPassword
|
||||
ACCEPT_EULA: 'Y'
|
||||
ports:
|
||||
- 1433:1433
|
||||
postgres:
|
||||
image: cyberboss/postgres-max-connections # Fork of _/postgres:latest with max_connections=500 becuase GitHub actions service containers have no way to set command lines. Rebuilds with updates.
|
||||
image: ${{ (matrix.database-type == 'PostgresSql') && 'cyberboss/postgres-max-connections' || '' }} # Fork of _/postgres:latest with max_connections=500 becuase GitHub actions service containers have no way to set command lines. Rebuilds with updates.
|
||||
ports:
|
||||
- 5432:5432
|
||||
env:
|
||||
@@ -611,7 +620,7 @@ jobs:
|
||||
--health-timeout 5s
|
||||
--health-retries 5
|
||||
mariadb:
|
||||
image: mariadb
|
||||
image: ${{ (matrix.database-type == 'MariaDB') && 'mariadb' || '' }}
|
||||
ports:
|
||||
- 3306:3306
|
||||
env:
|
||||
@@ -622,7 +631,7 @@ jobs:
|
||||
--health-timeout=2s
|
||||
--health-retries=3
|
||||
mysql:
|
||||
image: mysql:5.7.31
|
||||
image: ${{ (matrix.database-type == 'MySql') && 'mysql:5.7.31' || '' }}
|
||||
ports:
|
||||
- 3307:3306
|
||||
env:
|
||||
@@ -687,11 +696,11 @@ jobs:
|
||||
run: echo "General__UseBasicWatchdog=true" >> $GITHUB_ENV
|
||||
|
||||
- name: Checkout (Branch)
|
||||
uses: actions/checkout@v3
|
||||
uses: actions/checkout@v4
|
||||
if: github.event_name == 'push' || github.event_name == 'schedule'
|
||||
|
||||
- name: Checkout (PR Merge)
|
||||
uses: actions/checkout@v3
|
||||
uses: actions/checkout@v4
|
||||
if: github.event_name != 'push' && github.event_name != 'schedule'
|
||||
with:
|
||||
ref: "refs/pull/${{ github.event.number }}/merge"
|
||||
@@ -766,11 +775,11 @@ jobs:
|
||||
run: npm i -g ibm-openapi-validator@0.51.3
|
||||
|
||||
- name: Checkout (Branch)
|
||||
uses: actions/checkout@v3
|
||||
uses: actions/checkout@v4
|
||||
if: github.event_name == 'push' || github.event_name == 'schedule'
|
||||
|
||||
- name: Checkout (PR Merge)
|
||||
uses: actions/checkout@v3
|
||||
uses: actions/checkout@v4
|
||||
if: github.event_name != 'push' && github.event_name != 'schedule'
|
||||
with:
|
||||
ref: "refs/pull/${{ github.event.number }}/merge"
|
||||
@@ -791,11 +800,11 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout (Branch)
|
||||
uses: actions/checkout@v3
|
||||
uses: actions/checkout@v4
|
||||
if: github.event_name == 'push' || github.event_name == 'schedule'
|
||||
|
||||
- name: Checkout (PR Merge)
|
||||
uses: actions/checkout@v3
|
||||
uses: actions/checkout@v4
|
||||
if: github.event_name != 'push' && github.event_name != 'schedule'
|
||||
with:
|
||||
ref: "refs/pull/${{ github.event.number }}/merge"
|
||||
@@ -1079,11 +1088,11 @@ jobs:
|
||||
echo "New dotnet path should be $DOTNET_PATH"
|
||||
|
||||
- name: Checkout (Branch)
|
||||
uses: actions/checkout@v3
|
||||
uses: actions/checkout@v4
|
||||
if: github.event_name == 'push' || github.event_name == 'schedule'
|
||||
|
||||
- name: Checkout (PR Merge)
|
||||
uses: actions/checkout@v3
|
||||
uses: actions/checkout@v4
|
||||
if: github.event_name != 'push' && github.event_name != 'schedule'
|
||||
with:
|
||||
ref: "refs/pull/${{ github.event.number }}/merge"
|
||||
@@ -1162,11 +1171,11 @@ jobs:
|
||||
dotnet-quality: ${{ env.TGS_DOTNET_QUALITY }}
|
||||
|
||||
- name: Checkout (Branch)
|
||||
uses: actions/checkout@v3
|
||||
uses: actions/checkout@v4
|
||||
if: github.event_name == 'push' || github.event_name == 'schedule'
|
||||
|
||||
- name: Checkout (PR Merge)
|
||||
uses: actions/checkout@v3
|
||||
uses: actions/checkout@v4
|
||||
if: github.event_name != 'push' && github.event_name != 'schedule'
|
||||
with:
|
||||
ref: "refs/pull/${{ github.event.number }}/merge"
|
||||
@@ -1294,11 +1303,11 @@ jobs:
|
||||
echo "pr_template_sha=$(cat commits.json | jq '.[0].sha')" >> $GITHUB_OUTPUT
|
||||
|
||||
- name: Checkout (Branch)
|
||||
uses: actions/checkout@v3
|
||||
uses: actions/checkout@v4
|
||||
if: github.event_name == 'push' || github.event_name == 'schedule'
|
||||
|
||||
- name: Checkout (PR Merge)
|
||||
uses: actions/checkout@v3
|
||||
uses: actions/checkout@v4
|
||||
if: github.event_name != 'push' && github.event_name != 'schedule'
|
||||
with:
|
||||
ref: "refs/pull/${{ github.event.number }}/merge"
|
||||
@@ -1343,7 +1352,7 @@ jobs:
|
||||
dotnet-quality: ${{ env.TGS_DOTNET_QUALITY }}
|
||||
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v3
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Restore
|
||||
run: dotnet restore
|
||||
@@ -1407,7 +1416,7 @@ jobs:
|
||||
dotnet-quality: ${{ env.TGS_DOTNET_QUALITY }}
|
||||
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v3
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Restore
|
||||
run: dotnet restore
|
||||
@@ -1470,7 +1479,7 @@ jobs:
|
||||
dotnet-quality: ${{ env.TGS_DOTNET_QUALITY }}
|
||||
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v3
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Restore
|
||||
run: dotnet restore
|
||||
@@ -1521,7 +1530,7 @@ jobs:
|
||||
dotnet-quality: ${{ env.TGS_DOTNET_QUALITY }}
|
||||
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v3
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Restore
|
||||
run: dotnet restore
|
||||
@@ -1545,7 +1554,7 @@ jobs:
|
||||
dotnet-quality: ${{ env.TGS_DOTNET_QUALITY }}
|
||||
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v3
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Restore
|
||||
run: dotnet restore
|
||||
@@ -1757,7 +1766,7 @@ jobs:
|
||||
dotnet-quality: ${{ env.TGS_DOTNET_QUALITY }}
|
||||
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v3
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Restore
|
||||
run: dotnet restore
|
||||
@@ -1796,7 +1805,7 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v3
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Parse TGS version
|
||||
run: |
|
||||
@@ -1820,7 +1829,7 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v3
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Parse TGS version
|
||||
run: |
|
||||
@@ -1853,7 +1862,7 @@ jobs:
|
||||
run: winget install wingetcreate --version 1.2.8.0 --disable-interactivity --accept-source-agreements # Pinned due to breaking every other version
|
||||
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v3
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Build ReleaseNotes
|
||||
run: dotnet build -c Release -p:TGS_HOST_NO_WEBPANEL=true tools/Tgstation.Server.ReleaseNotes
|
||||
|
||||
@@ -1,8 +1,6 @@
|
||||
name: 'Code Scanning'
|
||||
|
||||
on:
|
||||
schedule:
|
||||
- cron: 0 23 * * 1
|
||||
push:
|
||||
branches:
|
||||
- dev
|
||||
@@ -39,7 +37,7 @@ jobs:
|
||||
dotnet-quality: ${{ env.TGS_DOTNET_QUALITY }}
|
||||
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v3
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Initialize CodeQL
|
||||
uses: github/codeql-action/init@v2
|
||||
|
||||
@@ -12,7 +12,7 @@ This is a toolset to manage production BYOND servers. It includes the ability to
|
||||
|
||||
### Pre-Requisites
|
||||
|
||||
_Note: If you opt to use the Windows installer, all pre-requisites (including MariaDB) are provided out of the box._
|
||||
_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._
|
||||
|
||||
tgstation-server needs a relational database to store it's data.
|
||||
|
||||
@@ -101,9 +101,19 @@ If using the console version, run `./tgs.bat` in the root of the installation di
|
||||
|
||||
Installing natively is the recommended way to run tgstation-server on Linux.
|
||||
|
||||
##### Ubuntu
|
||||
##### Ubuntu/Debian Package
|
||||
|
||||
Install TGS and all it's dependencies via our apt repository, interactively configure it, and start the service with this one-liner:
|
||||
You first need to add the appropriate Microsoft package repository for your distribution
|
||||
|
||||
Refer to the Microsoft website for steps for
|
||||
|
||||
- [Ubuntu](https://learn.microsoft.com/en-us/dotnet/core/install/linux-ubuntu#register-the-microsoft-package-repository)
|
||||
- [Debian 12](https://learn.microsoft.com/en-us/dotnet/core/install/linux-debian#debian-12)
|
||||
- [Debian 11](https://learn.microsoft.com/en-us/dotnet/core/install/linux-debian#debian-11)
|
||||
- [Debian 10](https://learn.microsoft.com/en-us/dotnet/core/install/linux-debian#debian-10)
|
||||
- [Other Distros](https://learn.microsoft.com/en-us/dotnet/core/install/linux-scripted-manual#manual-install)
|
||||
|
||||
After that, install TGS and all it's dependencies via our apt repository, interactively configure it, and start the service with this one-liner:
|
||||
|
||||
```sh
|
||||
sudo dpkg --add-architecture i386 \
|
||||
@@ -119,25 +129,11 @@ sudo dpkg --add-architecture i386 \
|
||||
|
||||
The service will execute as the newly created user: `tgstation-server`.
|
||||
|
||||
##### Debian
|
||||
|
||||
The `aspnetcore-runtime-8.0` package isn't yet available on mainline Debian and must be [installed from Microsoft](https://learn.microsoft.com/en-us/dotnet/core/install/linux-debian) first. Use the following one-liner to add their packages repository.
|
||||
|
||||
```sh
|
||||
curl -L https://packages.microsoft.com/config/debian/11/packages-microsoft-prod.deb -o packages-microsoft-prod.deb \
|
||||
&& sudo dpkg -i packages-microsoft-prod.deb \
|
||||
&& rm packages-microsoft-prod.deb
|
||||
```
|
||||
|
||||
After that, run the same command as the Ubuntu installation.
|
||||
|
||||
_Support for more distros coming soon_
|
||||
|
||||
##### Manual
|
||||
##### Manual Setup
|
||||
|
||||
The following dependencies are required.
|
||||
|
||||
- aspnetcore-runtime-8.0 (Note, not all supported distros have this package, see the links above for official Microsoft installation instructions)
|
||||
- aspnetcore-runtime-8.0 (See Prerequisites under the `Ubuntu/Debian Package` section)
|
||||
- libc6-i386
|
||||
- libstdc++6:i386
|
||||
- gcc-multilib (Only on 64-bit systems)
|
||||
@@ -185,6 +181,21 @@ Note that automatic configuration reloading is currently not supported in the co
|
||||
|
||||
If using manual configuration, before starting your container make sure the aforementioned `appsettings.Production.yml` is setup properly. See below
|
||||
|
||||
#### OpenDream
|
||||
|
||||
In order for TGS to use [OpenDream](https://github.com/OpenDreamProject/OpenDream), it requires the full .NET SDK to build whichever version your servers target. Whatever that is, it must be available using the `dotnet` command for whichever user runs TGS.
|
||||
|
||||
OpenDream currently requires [.NET SDK 7.0](https://dotnet.microsoft.com/en-us/download/dotnet/7.0) at the time of this writing. You must install this manually.
|
||||
|
||||
On Linux, as long as OpenDream and TGS do not use the same .NET major version, you cannot achieve this with the package manager as they will conflict. The 7.0 SDK can be added to an 8.0 runtime installation via the following steps.
|
||||
|
||||
1. Install `tgstation-server` using any of the above methods.
|
||||
1. [Download the Linux SDK binaries](https://dotnet.microsoft.com/en-us/download/dotnet/7.0) for your selected architecture.
|
||||
1. Extract everything EXCEPT the `dotnet` executable, `LICENSE.txt``, and `ThirdPartyNotices.txt` in the `.tar.gz` on top of the existing installation directory `/usr/share/dotnet/`
|
||||
1. Run `sudo chown -R root /usr/share/dotnet`
|
||||
|
||||
You should now be able to run the `dotnet --list-sdks` command and see an entry for `7.0.XXX [/usr/share/dotnet/sdk]`.
|
||||
|
||||
### Configuring
|
||||
|
||||
The first time you run TGS you should be prompted with a configuration wizard which will guide you through setting up your `appsettings.Production.yml`
|
||||
|
||||
@@ -25,7 +25,7 @@
|
||||
|
||||
<ItemGroup>
|
||||
<!-- Usage: Sourcelink -->
|
||||
<PackageReference Include="Microsoft.SourceLink.GitHub" Version="1.1.1" PrivateAssets="All" />
|
||||
<PackageReference Include="Microsoft.SourceLink.GitHub" Version="8.0.0" PrivateAssets="All" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
|
||||
+14
-9
@@ -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" />
|
||||
@@ -88,7 +88,6 @@
|
||||
<Rule Id="CA1414" Action="Warning" />
|
||||
<Rule Id="CA1415" Action="Warning" />
|
||||
<Rule Id="CA1500" Action="Warning" />
|
||||
<Rule Id="CA1501" Action="None" />
|
||||
<Rule Id="CA1502" Action="Warning" />
|
||||
<Rule Id="CA1504" Action="Warning" />
|
||||
<Rule Id="CA1505" Action="Warning" />
|
||||
@@ -666,6 +665,12 @@
|
||||
<Rule Id="CS8019" Action="Warning" />
|
||||
<Rule Id="CS8020" Action="Warning" />
|
||||
<Rule Id="CS8040" Action="Warning" />
|
||||
<Rule Id="CS9179" Action="None" />
|
||||
</Rules>
|
||||
<Rules AnalyzerId="Microsoft.CodeAnalysis.CSharp.CodeStyle" RuleNamespace="Microsoft.CodeAnalysis.CSharp.CodeStyle">
|
||||
<Rule Id="IDE0028" Action="None" />
|
||||
<Rule Id="IDE0290" Action="None" />
|
||||
<Rule Id="IDE0301" Action="None" />
|
||||
</Rules>
|
||||
<Rules AnalyzerId="Microsoft.CodeAnalysis.CSharp.EditorFeatures" RuleNamespace="Microsoft.CodeAnalysis.CSharp.EditorFeatures">
|
||||
<Rule Id="IDE0032" Action="Warning" />
|
||||
@@ -715,8 +720,8 @@
|
||||
<Rule Id="IDE0026WithoutSuggestion" Action="Warning" />
|
||||
<Rule Id="IDE0027" Action="Warning" />
|
||||
<Rule Id="IDE0027WithoutSuggestion" Action="Warning" />
|
||||
<Rule Id="IDE0028" Action="Info" />
|
||||
<Rule Id="IDE0028WithoutSuggestion" Action="Info" />
|
||||
<Rule Id="IDE0028" Action="None" />
|
||||
<Rule Id="IDE0028WithoutSuggestion" Action="None" />
|
||||
<Rule Id="IDE0029" Action="Warning" />
|
||||
<Rule Id="IDE0029WithoutSuggestion" Action="Warning" />
|
||||
<Rule Id="IDE0030" Action="Warning" />
|
||||
@@ -751,6 +756,8 @@
|
||||
<Rule Id="IDE0048WithoutSuggestion" Action="Warning" />
|
||||
<Rule Id="IDE0049" Action="None" />
|
||||
<Rule Id="IDE0049WithoutSuggestion" Action="None" />
|
||||
<Rule Id="IDE0290" Action="None" />
|
||||
<Rule Id="IDE0301" Action="None" />
|
||||
<Rule Id="IDE1005" Action="Warning" />
|
||||
<Rule Id="IDE1005WithoutSuggestion" Action="Warning" />
|
||||
<Rule Id="IDE1006" Action="Warning" />
|
||||
@@ -952,8 +959,8 @@
|
||||
<Rule Id="IDE0015" Action="Warning" />
|
||||
<Rule Id="IDE0017" Action="Warning" />
|
||||
<Rule Id="IDE0017WithoutSuggestion" Action="Warning" />
|
||||
<Rule Id="IDE0028" Action="Info" />
|
||||
<Rule Id="IDE0028WithoutSuggestion" Action="Info" />
|
||||
<Rule Id="IDE0028" Action="None" />
|
||||
<Rule Id="IDE0028WithoutSuggestion" Action="None" />
|
||||
<Rule Id="IDE0029" Action="Warning" />
|
||||
<Rule Id="IDE0029WithoutSuggestion" Action="Warning" />
|
||||
<Rule Id="IDE0030" Action="Warning" />
|
||||
@@ -1026,8 +1033,6 @@
|
||||
<Rule Id="SA1101" Action="None" />
|
||||
<Rule Id="SA1118" Action="None" />
|
||||
<Rule Id="SA1121" Action="None" />
|
||||
<Rule Id="SA1127" Action="Warning" />
|
||||
<Rule Id="SA1128" Action="Warning" />
|
||||
<Rule Id="SA1201" Action="None" />
|
||||
<Rule Id="SA1400" Action="None" />
|
||||
<Rule Id="SA1412" Action="Warning" />
|
||||
@@ -1043,4 +1048,4 @@
|
||||
<Rules AnalyzerId="Text.CSharp.Analyzers" RuleNamespace="Text.CSharp.Analyzers">
|
||||
<Rule Id="CA1704" Action="Warning" />
|
||||
</Rules>
|
||||
</RuleSet>
|
||||
</RuleSet>
|
||||
@@ -0,0 +1,25 @@
|
||||
using System;
|
||||
|
||||
using Tgstation.Server.Api.Models;
|
||||
|
||||
namespace Tgstation.Server.Api.Extensions
|
||||
{
|
||||
/// <summary>
|
||||
/// Extension methods for the <see cref="JobCode"/> <see langword="enum"/>.
|
||||
/// </summary>
|
||||
public static class JobCodeExtensions
|
||||
{
|
||||
/// <summary>
|
||||
/// If a given <paramref name="jobCode"/> can be triggered by TGS startup.
|
||||
/// </summary>
|
||||
/// <param name="jobCode">The <see cref="JobCode"/>.</param>
|
||||
/// <returns><see langword="true"/> if the <see cref="JobCode"/> can trigger before startup, <see langword="false"/> otherwise.</returns>
|
||||
public static bool IsServerStartupJob(this JobCode jobCode)
|
||||
=> jobCode switch
|
||||
{
|
||||
JobCode.Unknown or JobCode.Move or JobCode.RepositoryClone or JobCode.RepositoryUpdate or JobCode.RepositoryAutoUpdate or JobCode.RepositoryDelete or JobCode.EngineOfficialInstall or JobCode.EngineCustomInstall or JobCode.EngineDelete or JobCode.Deployment or JobCode.AutomaticDeployment or JobCode.WatchdogLaunch or JobCode.WatchdogRestart or JobCode.WatchdogDump => false,
|
||||
JobCode.StartupWatchdogLaunch or JobCode.StartupWatchdogReattach or JobCode.ReconnectChatBot => true,
|
||||
_ => throw new InvalidOperationException($"Invalid JobCode: {jobCode}"),
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -7,7 +7,6 @@
|
||||
<Description>Client library for tgstation-server.</Description>
|
||||
<PackageTags>json web api tgstation-server tgstation ss13 byond client http</PackageTags>
|
||||
<PackageReleaseNotes>$(TGS_NUGET_RELEASE_NOTES_CLIENT)</PackageReleaseNotes>
|
||||
<NoWarn>NU5104</NoWarn>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
|
||||
@@ -179,7 +179,9 @@ namespace Tgstation.Server.Host.Service
|
||||
var exePath = Path.Combine(assemblyDirectory, $"{assemblyNameWithoutExtension}.exe");
|
||||
|
||||
var programDataDirectory = Path.Combine(
|
||||
Environment.GetFolderPath(Environment.SpecialFolder.CommonApplicationData),
|
||||
Environment.GetFolderPath(
|
||||
Environment.SpecialFolder.CommonApplicationData,
|
||||
Environment.SpecialFolderOption.DoNotVerify),
|
||||
Server.Common.Constants.CanonicalPackageName);
|
||||
|
||||
using var processInstaller = new ServiceProcessInstaller();
|
||||
|
||||
@@ -16,7 +16,7 @@ namespace Tgstation.Server.Host.Components.Chat
|
||||
/// <inheritdoc />
|
||||
public bool Active
|
||||
{
|
||||
get => active;
|
||||
get => active && onDispose != null;
|
||||
set
|
||||
{
|
||||
if (active == value)
|
||||
|
||||
@@ -54,7 +54,7 @@ namespace Tgstation.Server.Host.Components.Engine
|
||||
protected override EngineType TargetEngineType => EngineType.Byond;
|
||||
|
||||
/// <summary>
|
||||
/// Bath to the system user's local BYOND folder.
|
||||
/// Path to the system user's local BYOND folder.
|
||||
/// </summary>
|
||||
protected abstract string PathToUserFolder { get; }
|
||||
|
||||
@@ -159,12 +159,6 @@ namespace Tgstation.Server.Host.Components.Engine
|
||||
ArgumentNullException.ThrowIfNull(fullDmbPath);
|
||||
|
||||
var byondDir = PathToUserFolder;
|
||||
if (String.IsNullOrWhiteSpace(byondDir))
|
||||
{
|
||||
Logger.LogTrace("No relevant user BYOND directory to install a \"{fileName}\" in", TrustedDmbFileName);
|
||||
return;
|
||||
}
|
||||
|
||||
var cfgDir = IOManager.ConcatPath(
|
||||
byondDir,
|
||||
CfgDirectoryName);
|
||||
|
||||
@@ -232,18 +232,28 @@ namespace Tgstation.Server.Host.Components.Engine
|
||||
shortenedPath,
|
||||
$"run -c Release --project OpenDreamPackageTool -- --tgs -o {shortenedDeployPath}",
|
||||
null,
|
||||
true,
|
||||
true);
|
||||
!GeneralConfiguration.OpenDreamSuppressInstallOutput,
|
||||
!GeneralConfiguration.OpenDreamSuppressInstallOutput);
|
||||
using (cancellationToken.Register(() => buildProcess.Terminate()))
|
||||
buildExitCode = await buildProcess.Lifetime;
|
||||
|
||||
Logger.LogTrace("OD build complete, waiting for output...");
|
||||
string output;
|
||||
|
||||
if (!GeneralConfiguration.OpenDreamSuppressInstallOutput)
|
||||
{
|
||||
var buildOutputTask = buildProcess.GetCombinedOutput(cancellationToken);
|
||||
if (!buildOutputTask.IsCompleted)
|
||||
Logger.LogTrace("OD build complete, waiting for output...");
|
||||
output = await buildOutputTask;
|
||||
}
|
||||
else
|
||||
output = "<Build output suppressed by configuration due to not being immediately available>";
|
||||
|
||||
Logger.LogDebug(
|
||||
"OpenDream build exited with code {exitCode}:{newLine}{output}",
|
||||
buildExitCode,
|
||||
Environment.NewLine,
|
||||
await buildProcess.GetCombinedOutput(cancellationToken));
|
||||
output);
|
||||
},
|
||||
sourcePath,
|
||||
cancellationToken);
|
||||
@@ -278,7 +288,9 @@ namespace Tgstation.Server.Host.Components.Engine
|
||||
cancellationToken)));
|
||||
}
|
||||
|
||||
await ValueTaskExtensions.WhenAll(MoveDirs(), MoveFiles());
|
||||
var dirsMoveTask = MoveDirs();
|
||||
var outputFilesMoveTask = MoveFiles();
|
||||
await ValueTaskExtensions.WhenAll(dirsMoveTask, outputFilesMoveTask);
|
||||
await IOManager.DeleteDirectory(sourcePath, cancellationToken);
|
||||
}
|
||||
|
||||
|
||||
@@ -67,7 +67,8 @@ namespace Tgstation.Server.Host.Components.Engine
|
||||
PathToUserFolder = IOManager.ResolvePath(
|
||||
IOManager.ConcatPath(
|
||||
Environment.GetFolderPath(
|
||||
Environment.SpecialFolder.UserProfile),
|
||||
Environment.SpecialFolder.UserProfile,
|
||||
Environment.SpecialFolderOption.DoNotVerify),
|
||||
"./byond/cache"));
|
||||
}
|
||||
|
||||
|
||||
@@ -102,11 +102,12 @@ namespace Tgstation.Server.Host.Components.Engine
|
||||
this.processExecutor = processExecutor ?? throw new ArgumentNullException(nameof(processExecutor));
|
||||
generalConfiguration = generalConfigurationOptions?.Value ?? throw new ArgumentNullException(nameof(generalConfigurationOptions));
|
||||
|
||||
var documentsDirectory = Environment.GetFolderPath(Environment.SpecialFolder.MyDocuments);
|
||||
if (String.IsNullOrWhiteSpace(documentsDirectory))
|
||||
PathToUserFolder = null; // happens with the service account
|
||||
else
|
||||
PathToUserFolder = IOManager.ResolvePath(IOManager.ConcatPath(documentsDirectory, "BYOND"));
|
||||
var documentsDirectory = Environment.GetFolderPath(
|
||||
Environment.SpecialFolder.MyDocuments,
|
||||
Environment.SpecialFolderOption.DoNotVerify);
|
||||
|
||||
PathToUserFolder = IOManager.ResolvePath(
|
||||
IOManager.ConcatPath(documentsDirectory, "BYOND"));
|
||||
|
||||
semaphore = new SemaphoreSlim(1);
|
||||
installedDirectX = false;
|
||||
|
||||
@@ -58,15 +58,18 @@ namespace Tgstation.Server.Host.Components.Engine
|
||||
|
||||
/// <inheritdoc />
|
||||
public override ValueTask Install(EngineVersion version, string installPath, CancellationToken cancellationToken)
|
||||
=> ValueTaskExtensions.WhenAll(
|
||||
base.Install(
|
||||
version,
|
||||
installPath,
|
||||
cancellationToken),
|
||||
AddServerFirewallException(
|
||||
version,
|
||||
installPath,
|
||||
cancellationToken));
|
||||
{
|
||||
var installTask = base.Install(
|
||||
version,
|
||||
installPath,
|
||||
cancellationToken);
|
||||
var firewallTask = AddServerFirewallException(
|
||||
version,
|
||||
installPath,
|
||||
cancellationToken);
|
||||
|
||||
return ValueTaskExtensions.WhenAll(installTask, firewallTask);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override async ValueTask HandleExtremelyLongPathOperation(Func<string, ValueTask> shortenedPathOperation, string originalPath, CancellationToken cancellationToken)
|
||||
@@ -104,7 +107,7 @@ namespace Tgstation.Server.Host.Components.Engine
|
||||
// I really wish we could add the instance name here but
|
||||
// 1. It'd make IByondInstaller need to be transient per-instance and WindowsByondInstaller relys on being a singleton for its DX installer call
|
||||
// 2. The instance could be renamed, so it'd have to be an unfriendly ID anyway.
|
||||
var ruleName = $"TGS DreamDaemon {version}";
|
||||
var ruleName = $"TGS OpenDream {version}";
|
||||
|
||||
exitCode = await WindowsFirewallHelper.AddFirewallException(
|
||||
ProcessExecutor,
|
||||
|
||||
@@ -59,15 +59,15 @@ namespace Tgstation.Server.Host.Components.Interop
|
||||
/// <summary>
|
||||
/// Process a given <paramref name="chunk"/>.
|
||||
/// </summary>
|
||||
/// <typeparam name="TCommnication">The <see cref="Type"/> of communication that was chunked.</typeparam>
|
||||
/// <typeparam name="TCommunication">The <see cref="Type"/> of communication that was chunked.</typeparam>
|
||||
/// <typeparam name="TResponse">The <see cref="Type"/> of <see cref="IMissingPayloadsCommunication"/> expected.</typeparam>
|
||||
/// <param name="completionCallback">The callback that receives the completed <typeparamref name="TCommnication"/>.</param>
|
||||
/// <param name="completionCallback">The callback that receives the completed <typeparamref name="TCommunication"/>.</param>
|
||||
/// <param name="chunkErrorCallback">The callback that generates a <typeparamref name="TResponse"/> for a given error.</param>
|
||||
/// <param name="chunk">The <see cref="ChunkData"/>.</param>
|
||||
/// <param name="cancellationToken">The <see cref="CancellationToken"/> for the operation.</param>
|
||||
/// <returns>A <see cref="ValueTask{TResult}"/> resulting in the <typeparamref name="TResponse"/> for the chunked request.</returns>
|
||||
protected async ValueTask<TResponse> ProcessChunk<TCommnication, TResponse>(
|
||||
Func<TCommnication, CancellationToken, ValueTask<TResponse>> completionCallback,
|
||||
protected async ValueTask<TResponse> ProcessChunk<TCommunication, TResponse>(
|
||||
Func<TCommunication, CancellationToken, ValueTask<TResponse>> completionCallback,
|
||||
Func<string, TResponse> chunkErrorCallback,
|
||||
ChunkData chunk,
|
||||
CancellationToken cancellationToken)
|
||||
@@ -130,11 +130,11 @@ namespace Tgstation.Server.Host.Components.Interop
|
||||
chunkSets.Remove(requestInfo.PayloadId.Value);
|
||||
}
|
||||
|
||||
TCommnication completedCommunication;
|
||||
TCommunication completedCommunication;
|
||||
var fullCommunicationJson = String.Concat(payloads);
|
||||
try
|
||||
{
|
||||
completedCommunication = JsonConvert.DeserializeObject<TCommnication>(fullCommunicationJson, DMApiConstants.SerializerSettings);
|
||||
completedCommunication = JsonConvert.DeserializeObject<TCommunication>(fullCommunicationJson, DMApiConstants.SerializerSettings);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
|
||||
@@ -249,20 +249,16 @@ namespace Tgstation.Server.Host.Components.Repository
|
||||
libGitRepo,
|
||||
refSpecList,
|
||||
remote,
|
||||
new FetchOptions
|
||||
{
|
||||
Prune = true,
|
||||
OnProgress = (a) => !cancellationToken.IsCancellationRequested,
|
||||
OnTransferProgress = TransferProgressHandler(
|
||||
progressReporter.CreateSection($"Fetch {refSpec}", progressFactor),
|
||||
cancellationToken),
|
||||
OnUpdateTips = (a, b, c) => !cancellationToken.IsCancellationRequested,
|
||||
CredentialsProvider = credentialsProvider.GenerateCredentialsHandler(username, password),
|
||||
},
|
||||
new FetchOptions().Hydrate(
|
||||
logger,
|
||||
progressReporter.CreateSection($"Fetch {refSpec}", progressFactor),
|
||||
credentialsProvider.GenerateCredentialsHandler(username, password),
|
||||
cancellationToken),
|
||||
logMessage);
|
||||
}
|
||||
catch (UserCancelledException)
|
||||
catch (UserCancelledException ex)
|
||||
{
|
||||
logger.LogTrace(ex, "Suppressing fetch cancel exception");
|
||||
}
|
||||
catch (LibGit2SharpException ex)
|
||||
{
|
||||
@@ -441,14 +437,12 @@ namespace Tgstation.Server.Host.Components.Repository
|
||||
var fetchOptions = new FetchOptions
|
||||
{
|
||||
Prune = true,
|
||||
OnProgress = (a) => !cancellationToken.IsCancellationRequested,
|
||||
OnUpdateTips = (a, b, c) => !cancellationToken.IsCancellationRequested,
|
||||
CredentialsProvider = credentialsProvider.GenerateCredentialsHandler(username, password),
|
||||
TagFetchMode = TagFetchMode.All,
|
||||
};
|
||||
|
||||
if (progressReporter != null)
|
||||
fetchOptions.OnTransferProgress = TransferProgressHandler(progressReporter.CreateSection("Fetch Origin", 1.0), cancellationToken);
|
||||
}.Hydrate(
|
||||
logger,
|
||||
progressReporter?.CreateSection("Fetch Origin", 1.0),
|
||||
credentialsProvider.GenerateCredentialsHandler(username, password),
|
||||
cancellationToken);
|
||||
|
||||
commands.Fetch(
|
||||
libGitRepo,
|
||||
@@ -1043,19 +1037,18 @@ namespace Tgstation.Server.Host.Components.Repository
|
||||
var submoduleUpdateOptions = new SubmoduleUpdateOptions
|
||||
{
|
||||
Init = true,
|
||||
OnProgress = output => !cancellationToken.IsCancellationRequested,
|
||||
OnUpdateTips = (a, b, c) => !cancellationToken.IsCancellationRequested,
|
||||
CredentialsProvider = credentialsProvider.GenerateCredentialsHandler(username, password),
|
||||
OnCheckoutNotify = (_, _) => !cancellationToken.IsCancellationRequested,
|
||||
};
|
||||
|
||||
submoduleUpdateOptions.FetchOptions.Hydrate(
|
||||
logger,
|
||||
progressReporter?.CreateSection($"Fetch submodule {submodule.Name}", factor),
|
||||
credentialsProvider.GenerateCredentialsHandler(username, password),
|
||||
cancellationToken);
|
||||
|
||||
if (progressReporter != null)
|
||||
{
|
||||
submoduleUpdateOptions.OnTransferProgress = TransferProgressHandler(
|
||||
progressReporter.CreateSection($"Fetch submodule {submodule.Name}", factor),
|
||||
cancellationToken);
|
||||
submoduleUpdateOptions.OnCheckoutProgress = CheckoutProgressHandler(
|
||||
progressReporter.CreateSection($"Checkout submodule {submodule.Name}", factor));
|
||||
}
|
||||
|
||||
logger.LogDebug("Updating submodule {submoduleName}...", submodule.Name);
|
||||
Task RawSubModuleUpdate() => Task.Factory.StartNew(
|
||||
@@ -1133,37 +1126,6 @@ namespace Tgstation.Server.Host.Components.Repository
|
||||
|
||||
progressReporter.ReportProgress(percentage);
|
||||
};
|
||||
|
||||
/// <summary>
|
||||
/// Generate a <see cref="LibGit2Sharp.Handlers.TransferProgressHandler"/> from a given <paramref name="progressReporter"/> and <paramref name="cancellationToken"/>.
|
||||
/// </summary>
|
||||
/// <param name="progressReporter">The <see cref="JobProgressReporter"/> of the operation.</param>
|
||||
/// <param name="cancellationToken">The <see cref="CancellationToken"/> for the operation.</param>
|
||||
/// <returns>A new <see cref="LibGit2Sharp.Handlers.TransferProgressHandler"/> based on <paramref name="progressReporter"/>.</returns>
|
||||
TransferProgressHandler TransferProgressHandler(JobProgressReporter progressReporter, CancellationToken cancellationToken) => (transferProgress) =>
|
||||
{
|
||||
double? percentage;
|
||||
var totalObjectsToProcess = transferProgress.TotalObjects * 2;
|
||||
var processedObjects = transferProgress.IndexedObjects + transferProgress.ReceivedObjects;
|
||||
if (totalObjectsToProcess < processedObjects || totalObjectsToProcess == 0)
|
||||
percentage = null;
|
||||
else
|
||||
{
|
||||
percentage = (double)processedObjects / totalObjectsToProcess;
|
||||
if (percentage < 0)
|
||||
percentage = null;
|
||||
}
|
||||
|
||||
if (percentage == null)
|
||||
logger.LogDebug(
|
||||
"Bad transfer progress values (Please tell Cyberboss)! Indexed: {indexed}, Received: {received}, Total: {total}",
|
||||
transferProgress.IndexedObjects,
|
||||
transferProgress.ReceivedObjects,
|
||||
transferProgress.TotalObjects);
|
||||
|
||||
progressReporter.ReportProgress(percentage);
|
||||
return !cancellationToken.IsCancellationRequested;
|
||||
};
|
||||
}
|
||||
#pragma warning restore CA1506
|
||||
}
|
||||
|
||||
@@ -9,6 +9,7 @@ using Microsoft.Extensions.Logging;
|
||||
using Tgstation.Server.Api.Models;
|
||||
using Tgstation.Server.Host.Components.Events;
|
||||
using Tgstation.Server.Host.Configuration;
|
||||
using Tgstation.Server.Host.Extensions;
|
||||
using Tgstation.Server.Host.IO;
|
||||
using Tgstation.Server.Host.Jobs;
|
||||
using Tgstation.Server.Host.Utils;
|
||||
@@ -129,8 +130,6 @@ namespace Tgstation.Server.Host.Components.Repository
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(url);
|
||||
|
||||
logger.LogInformation("Begin clone {url} (Branch: {initialBranch})", url, initialBranch);
|
||||
lock (semaphore)
|
||||
{
|
||||
if (CloneInProgress)
|
||||
@@ -138,12 +137,14 @@ namespace Tgstation.Server.Host.Components.Repository
|
||||
CloneInProgress = true;
|
||||
}
|
||||
|
||||
var repositoryPath = ioManager.ResolvePath();
|
||||
logger.LogInformation("Begin clone {url} to {path} (Branch: {initialBranch})", url, repositoryPath, initialBranch);
|
||||
|
||||
try
|
||||
{
|
||||
using (await SemaphoreSlimContext.Lock(semaphore, cancellationToken))
|
||||
{
|
||||
logger.LogTrace("Semaphore acquired for clone");
|
||||
var repositoryPath = ioManager.ResolvePath();
|
||||
if (!await ioManager.DirectoryExists(repositoryPath, cancellationToken))
|
||||
try
|
||||
{
|
||||
@@ -151,20 +152,7 @@ namespace Tgstation.Server.Host.Components.Repository
|
||||
var checkoutProgressReporter = progressReporter?.CreateSection(null, 0.25f);
|
||||
var cloneOptions = new CloneOptions
|
||||
{
|
||||
OnProgress = (a) => !cancellationToken.IsCancellationRequested,
|
||||
OnTransferProgress = (a) =>
|
||||
{
|
||||
if (cloneProgressReporter != null)
|
||||
{
|
||||
var percentage = ((double)a.IndexedObjects + a.ReceivedObjects) / (a.TotalObjects * 2);
|
||||
cloneProgressReporter.ReportProgress(percentage);
|
||||
}
|
||||
|
||||
return !cancellationToken.IsCancellationRequested;
|
||||
},
|
||||
RecurseSubmodules = recurseSubmodules,
|
||||
OnUpdateTips = (a, b, c) => !cancellationToken.IsCancellationRequested,
|
||||
RepositoryOperationStarting = (a) => !cancellationToken.IsCancellationRequested,
|
||||
OnCheckoutProgress = (path, completed, remaining) =>
|
||||
{
|
||||
if (checkoutProgressReporter == null)
|
||||
@@ -174,9 +162,14 @@ namespace Tgstation.Server.Host.Components.Repository
|
||||
checkoutProgressReporter.ReportProgress(percentage);
|
||||
},
|
||||
BranchName = initialBranch,
|
||||
CredentialsProvider = repositoryFactory.GenerateCredentialsHandler(username, password),
|
||||
};
|
||||
|
||||
cloneOptions.FetchOptions.Hydrate(
|
||||
logger,
|
||||
cloneProgressReporter,
|
||||
repositoryFactory.GenerateCredentialsHandler(username, password),
|
||||
cancellationToken);
|
||||
|
||||
await repositoryFactory.Clone(
|
||||
url,
|
||||
cloneOptions,
|
||||
|
||||
@@ -2,7 +2,6 @@
|
||||
using System.Collections.Generic;
|
||||
using System.Globalization;
|
||||
using System.Linq;
|
||||
using System.Net;
|
||||
using System.Text;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
@@ -22,6 +21,7 @@ using Tgstation.Server.Host.Components.Engine;
|
||||
using Tgstation.Server.Host.Components.Interop;
|
||||
using Tgstation.Server.Host.Components.Interop.Bridge;
|
||||
using Tgstation.Server.Host.Components.Interop.Topic;
|
||||
using Tgstation.Server.Host.Extensions;
|
||||
using Tgstation.Server.Host.System;
|
||||
using Tgstation.Server.Host.Utils;
|
||||
|
||||
@@ -110,6 +110,11 @@ namespace Tgstation.Server.Host.Components.Session
|
||||
/// </summary>
|
||||
public ReattachInformation ReattachInformation { get; }
|
||||
|
||||
/// <summary>
|
||||
/// The <see cref="FifoSemaphore"/> used to prevent concurrent calls into /world/Topic().
|
||||
/// </summary>
|
||||
public FifoSemaphore TopicSendSemaphore { get; }
|
||||
|
||||
/// <summary>
|
||||
/// The <see cref="Byond.TopicSender.ITopicClient"/> for the <see cref="SessionController"/>.
|
||||
/// </summary>
|
||||
@@ -150,11 +155,6 @@ namespace Tgstation.Server.Host.Components.Session
|
||||
/// </summary>
|
||||
readonly TaskCompletionSource initialBridgeRequestTcs;
|
||||
|
||||
/// <summary>
|
||||
/// The <see cref="FifoSemaphore"/> used to prevent concurrent calls into /world/Topic().
|
||||
/// </summary>
|
||||
readonly FifoSemaphore topicSendSemaphore;
|
||||
|
||||
/// <summary>
|
||||
/// The <see cref="Instance"/> metadata.
|
||||
/// </summary>
|
||||
@@ -287,7 +287,7 @@ namespace Tgstation.Server.Host.Components.Session
|
||||
initialBridgeRequestTcs = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously);
|
||||
reattachTopicCts = new CancellationTokenSource();
|
||||
|
||||
topicSendSemaphore = new FifoSemaphore();
|
||||
TopicSendSemaphore = new FifoSemaphore();
|
||||
synchronizationLock = new object();
|
||||
|
||||
if (apiValidationSession || DMApiAvailable)
|
||||
@@ -339,9 +339,8 @@ namespace Tgstation.Server.Host.Components.Session
|
||||
|
||||
Logger.LogTrace("Disposing...");
|
||||
|
||||
// yield then acquire the topic semaphore to prevent new calls from starting
|
||||
await Task.Yield();
|
||||
(await topicSendSemaphore.Lock(CancellationToken.None)).Dispose(); // DCT: None available
|
||||
reattachTopicCts.Cancel();
|
||||
var semaphoreLockTask = TopicSendSemaphore.Lock(CancellationToken.None); // DCT: None available
|
||||
|
||||
if (!released)
|
||||
{
|
||||
@@ -365,7 +364,8 @@ namespace Tgstation.Server.Host.Components.Session
|
||||
if (!released)
|
||||
await Lifetime; // finish the async callback
|
||||
|
||||
topicSendSemaphore.Dispose();
|
||||
(await semaphoreLockTask).Dispose();
|
||||
TopicSendSemaphore.Dispose();
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
@@ -886,35 +886,15 @@ namespace Tgstation.Server.Host.Components.Session
|
||||
}
|
||||
|
||||
var targetPort = ReattachInformation.Port;
|
||||
Byond.TopicSender.TopicResponse byondResponse = null;
|
||||
var firstSend = true;
|
||||
|
||||
using (await topicSendSemaphore.Lock(cancellationToken))
|
||||
{
|
||||
const int PrioritySendAttempts = 5;
|
||||
var endpoint = new IPEndPoint(IPAddress.Loopback, targetPort);
|
||||
for (var i = PrioritySendAttempts - 1; i >= 0 && (priority || firstSend); --i)
|
||||
try
|
||||
{
|
||||
firstSend = false;
|
||||
|
||||
Logger.LogTrace("Begin topic request");
|
||||
byondResponse = await byondTopicSender.SendTopic(
|
||||
endpoint,
|
||||
queryString,
|
||||
cancellationToken);
|
||||
|
||||
Logger.LogTrace("End topic request");
|
||||
break;
|
||||
}
|
||||
catch (Exception ex) when (ex is not OperationCanceledException)
|
||||
{
|
||||
Logger.LogWarning(ex, "SendTopic exception!{retryDetails}", priority ? $" {i} attempts remaining." : String.Empty);
|
||||
|
||||
if (priority && i > 0)
|
||||
await asyncDelayer.Delay(TimeSpan.FromSeconds(2), cancellationToken);
|
||||
}
|
||||
}
|
||||
Byond.TopicSender.TopicResponse byondResponse;
|
||||
using (await TopicSendSemaphore.Lock(cancellationToken))
|
||||
byondResponse = await byondTopicSender.SendWithOptionalPriority(
|
||||
asyncDelayer,
|
||||
Logger,
|
||||
queryString,
|
||||
targetPort,
|
||||
priority,
|
||||
cancellationToken);
|
||||
|
||||
if (byondResponse == null)
|
||||
{
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
using System;
|
||||
|
||||
using Byond.TopicSender;
|
||||
|
||||
using Microsoft.Extensions.Logging;
|
||||
|
||||
#nullable disable
|
||||
|
||||
@@ -73,7 +73,7 @@ namespace Tgstation.Server.Host.Components.StaticFiles
|
||||
/// <summary>
|
||||
/// Map of <see cref="EventType"/>s to the filename of the event scripts they trigger.
|
||||
/// </summary>
|
||||
static readonly IReadOnlyDictionary<EventType, IReadOnlyList<string>> EventTypeScriptFileNameMap = new Dictionary<EventType, IReadOnlyList<string>>(
|
||||
public static IReadOnlyDictionary<EventType, IReadOnlyList<string>> EventTypeScriptFileNameMap { get; } = new Dictionary<EventType, IReadOnlyList<string>>(
|
||||
Enum.GetValues(typeof(EventType))
|
||||
.Cast<EventType>()
|
||||
.Select(
|
||||
@@ -300,8 +300,7 @@ namespace Tgstation.Server.Host.Components.StaticFiles
|
||||
string GetFileSha()
|
||||
{
|
||||
var content = synchronousIOManager.ReadFile(path);
|
||||
using var sha1 = SHA1.Create();
|
||||
return String.Join(String.Empty, sha1.ComputeHash(content).Select(b => b.ToString("x2", CultureInfo.InvariantCulture)));
|
||||
return String.Join(String.Empty, SHA1.HashData(content).Select(b => b.ToString("x2", CultureInfo.InvariantCulture)));
|
||||
}
|
||||
|
||||
var originalSha = GetFileSha();
|
||||
@@ -408,12 +407,10 @@ namespace Tgstation.Server.Host.Components.StaticFiles
|
||||
var fileName = ioManager.GetFileName(file);
|
||||
|
||||
// need to normalize
|
||||
bool ignored;
|
||||
if (platformIdentifier.IsWindows)
|
||||
ignored = ignoreFiles.Any(y => fileName.ToUpperInvariant() == y.ToUpperInvariant());
|
||||
else
|
||||
ignored = ignoreFiles.Any(y => fileName == y);
|
||||
|
||||
var fileComparison = platformIdentifier.IsWindows
|
||||
? StringComparison.OrdinalIgnoreCase
|
||||
: StringComparison.Ordinal;
|
||||
var ignored = ignoreFiles.Any(y => fileName.Equals(y, fileComparison));
|
||||
if (ignored)
|
||||
{
|
||||
logger.LogTrace("Ignoring static file {fileName}...", fileName);
|
||||
@@ -444,12 +441,14 @@ namespace Tgstation.Server.Host.Components.StaticFiles
|
||||
using (var reader = new StringReader(ignoreFileText))
|
||||
{
|
||||
cancellationToken.ThrowIfCancellationRequested();
|
||||
var line = await reader.ReadLineAsync();
|
||||
var line = await reader.ReadLineAsync(cancellationToken);
|
||||
if (!String.IsNullOrEmpty(line))
|
||||
ignoreFiles.Add(line);
|
||||
}
|
||||
|
||||
await ValueTaskExtensions.WhenAll(SymlinkBase(true), SymlinkBase(false));
|
||||
var filesSymlinkTask = SymlinkBase(true);
|
||||
var dirsSymlinkTask = SymlinkBase(false);
|
||||
await ValueTaskExtensions.WhenAll(filesSymlinkTask, dirsSymlinkTask);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -617,7 +616,7 @@ namespace Tgstation.Server.Host.Components.StaticFiles
|
||||
scriptName => x.StartsWith(scriptName, StringComparison.Ordinal)))
|
||||
.ToList();
|
||||
|
||||
if (!scriptFiles.Any())
|
||||
if (scriptFiles.Count == 0)
|
||||
{
|
||||
logger.LogTrace("No event scripts starting with \"{scriptName}\" detected", String.Join("\" or \"", scriptNames));
|
||||
return;
|
||||
@@ -712,19 +711,19 @@ namespace Tgstation.Server.Host.Components.StaticFiles
|
||||
return;
|
||||
|
||||
await ioManager.CreateDirectory(CodeModificationsSubdirectory, cancellationToken);
|
||||
await ValueTaskExtensions.WhenAll(
|
||||
ioManager.WriteAllBytes(
|
||||
ioManager.ConcatPath(
|
||||
CodeModificationsSubdirectory,
|
||||
CodeModificationsHeadFile),
|
||||
Encoding.UTF8.GetBytes(DefaultHeadInclude),
|
||||
cancellationToken),
|
||||
ioManager.WriteAllBytes(
|
||||
ioManager.ConcatPath(
|
||||
CodeModificationsSubdirectory,
|
||||
CodeModificationsTailFile),
|
||||
Encoding.UTF8.GetBytes(DefaultTailInclude),
|
||||
cancellationToken));
|
||||
var headWriteTask = ioManager.WriteAllBytes(
|
||||
ioManager.ConcatPath(
|
||||
CodeModificationsSubdirectory,
|
||||
CodeModificationsHeadFile),
|
||||
Encoding.UTF8.GetBytes(DefaultHeadInclude),
|
||||
cancellationToken);
|
||||
var tailWriteTask = ioManager.WriteAllBytes(
|
||||
ioManager.ConcatPath(
|
||||
CodeModificationsSubdirectory,
|
||||
CodeModificationsTailFile),
|
||||
Encoding.UTF8.GetBytes(DefaultTailInclude),
|
||||
cancellationToken);
|
||||
await ValueTaskExtensions.WhenAll(headWriteTask, tailWriteTask);
|
||||
}
|
||||
|
||||
return Task.WhenAll(
|
||||
|
||||
@@ -397,6 +397,7 @@ namespace Tgstation.Server.Host.Components.Watchdog
|
||||
|
||||
try
|
||||
{
|
||||
Logger.LogTrace("Making new provider {id} active...", newProvider.CompileJob.Id);
|
||||
await newProvider.MakeActive(cancellationToken);
|
||||
}
|
||||
finally
|
||||
|
||||
@@ -64,8 +64,8 @@ namespace Tgstation.Server.Host.Components.Watchdog
|
||||
/// </summary>
|
||||
/// <param name="launchParameters">The new <see cref="DreamDaemonLaunchParameters"/>. May be modified.</param>
|
||||
/// <param name="cancellationToken">The <see cref="CancellationToken"/> for the operation.</param>
|
||||
/// <returns>A <see cref="ValueTask"/> representing the running operation.</returns>
|
||||
ValueTask ChangeSettings(DreamDaemonLaunchParameters launchParameters, CancellationToken cancellationToken);
|
||||
/// <returns>A <see cref="ValueTask{TResult}"/> resulting in <see langword="true"/> if a reboot is required, <see langword="false"/> otherwise.</returns>
|
||||
ValueTask<bool> ChangeSettings(DreamDaemonLaunchParameters launchParameters, CancellationToken cancellationToken);
|
||||
|
||||
/// <summary>
|
||||
/// Restarts the watchdog.
|
||||
|
||||
@@ -269,18 +269,20 @@ namespace Tgstation.Server.Host.Components.Watchdog
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public async ValueTask ChangeSettings(DreamDaemonLaunchParameters launchParameters, CancellationToken cancellationToken)
|
||||
public async ValueTask<bool> ChangeSettings(DreamDaemonLaunchParameters launchParameters, CancellationToken cancellationToken)
|
||||
{
|
||||
using (await SemaphoreSlimContext.Lock(synchronizationSemaphore, cancellationToken))
|
||||
{
|
||||
bool match = launchParameters.CanApplyWithoutReboot(ActiveLaunchParameters);
|
||||
ActiveLaunchParameters = launchParameters;
|
||||
if (match || Status == WatchdogStatus.Offline)
|
||||
return;
|
||||
if (match || Status == WatchdogStatus.Offline || Status == WatchdogStatus.DelayedRestart)
|
||||
return false;
|
||||
|
||||
var oldTcs = Interlocked.Exchange(ref activeParametersUpdated, new TaskCompletionSource());
|
||||
oldTcs.SetResult();
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
@@ -446,20 +448,8 @@ namespace Tgstation.Server.Host.Components.Watchdog
|
||||
/// <inheritdoc />
|
||||
public async ValueTask CreateDump(CancellationToken cancellationToken)
|
||||
{
|
||||
const string DumpDirectory = "ProcessDumps";
|
||||
await diagnosticsIOManager.CreateDirectory(DumpDirectory, cancellationToken);
|
||||
|
||||
var dumpFileName = diagnosticsIOManager.ResolvePath(
|
||||
diagnosticsIOManager.ConcatPath(
|
||||
DumpDirectory,
|
||||
$"DreamDaemon-{DateTimeOffset.UtcNow.ToFileStamp()}.dmp"));
|
||||
|
||||
var session = GetActiveController();
|
||||
if (session?.Lifetime.IsCompleted != false)
|
||||
throw new JobException(ErrorCode.GameServerOffline);
|
||||
|
||||
Logger.LogInformation("Dumping session to {dumpFileName}...", dumpFileName);
|
||||
await session.CreateDump(dumpFileName, cancellationToken);
|
||||
using (await SemaphoreSlimContext.Lock(synchronizationSemaphore, cancellationToken))
|
||||
await CreateDumpNoLock(cancellationToken);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
@@ -760,8 +750,9 @@ namespace Tgstation.Server.Host.Components.Watchdog
|
||||
try
|
||||
{
|
||||
var sessionEventTask = relayToSession ? ((IEventConsumer)this).HandleEvent(eventType, parameters, false, cancellationToken) : ValueTask.CompletedTask;
|
||||
var eventConsumerTask = eventConsumer.HandleEvent(eventType, parameters, false, cancellationToken);
|
||||
await ValueTaskExtensions.WhenAll(
|
||||
eventConsumer.HandleEvent(eventType, parameters, false, cancellationToken),
|
||||
eventConsumerTask,
|
||||
sessionEventTask);
|
||||
}
|
||||
catch (JobException ex)
|
||||
@@ -1144,7 +1135,7 @@ namespace Tgstation.Server.Host.Components.Watchdog
|
||||
Logger.LogDebug("DumpOnHealthCheckRestart enabled.");
|
||||
try
|
||||
{
|
||||
await CreateDump(cancellationToken);
|
||||
await CreateDumpNoLock(cancellationToken);
|
||||
}
|
||||
catch (JobException ex)
|
||||
{
|
||||
@@ -1194,5 +1185,34 @@ namespace Tgstation.Server.Host.Components.Watchdog
|
||||
.Where(nullableChannelId => nullableChannelId.HasValue)
|
||||
.Select(nullableChannelId => nullableChannelId.Value));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Attempt to create a process dump for the game server. Requires a lock on <see cref="synchronizationSemaphore"/>.
|
||||
/// </summary>
|
||||
/// <param name="cancellationToken">The <see cref="CancellationToken"/> for the operation.</param>
|
||||
/// <returns>A <see cref="ValueTask"/> representing the running operation.</returns>
|
||||
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);
|
||||
|
||||
Logger.LogInformation("Dumping session to {dumpFileName}...", dumpFileName);
|
||||
await session.CreateDump(dumpFileName, cancellationToken);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -62,7 +62,9 @@ namespace Tgstation.Server.Host.Configuration
|
||||
|
||||
return platformIdentifier.IsWindows
|
||||
? ioManager.ConcatPath(
|
||||
Environment.GetFolderPath(Environment.SpecialFolder.CommonApplicationData),
|
||||
Environment.GetFolderPath(
|
||||
Environment.SpecialFolder.CommonApplicationData,
|
||||
Environment.SpecialFolderOption.DoNotVerify),
|
||||
assemblyInformationProvider.VersionPrefix,
|
||||
"logs")
|
||||
: ioManager.ConcatPath(
|
||||
|
||||
@@ -145,6 +145,11 @@ namespace Tgstation.Server.Host.Configuration
|
||||
/// </summary>
|
||||
public string OpenDreamGitTagPrefix { get; set; } = DefaultOpenDreamGitTagPrefix;
|
||||
|
||||
/// <summary>
|
||||
/// If the dotnet output of creating an OpenDream installation should be suppressed. Known to cause issues in CI.
|
||||
/// </summary>
|
||||
public bool OpenDreamSuppressInstallOutput { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="GeneralConfiguration"/> class.
|
||||
/// </summary>
|
||||
|
||||
@@ -7,7 +7,6 @@ using System.Threading.Tasks;
|
||||
using Microsoft.AspNetCore.Hosting;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.AspNetCore.Mvc.Filters;
|
||||
using Microsoft.AspNetCore.StaticFiles;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Microsoft.Extensions.Options;
|
||||
using Microsoft.Extensions.Primitives;
|
||||
@@ -15,6 +14,7 @@ using Microsoft.Net.Http.Headers;
|
||||
|
||||
using Tgstation.Server.Api;
|
||||
using Tgstation.Server.Host.Configuration;
|
||||
using Tgstation.Server.Host.Extensions;
|
||||
|
||||
namespace Tgstation.Server.Host.Controllers
|
||||
{
|
||||
@@ -136,22 +136,11 @@ namespace Tgstation.Server.Host.Controllers
|
||||
if (Request.Headers.ContainsKey(FetchChannelVaryHeader))
|
||||
return GetChannelJson();
|
||||
|
||||
var fileInfo = hostEnvironment.WebRootFileProvider.GetFileInfo(appRoute);
|
||||
if (fileInfo.Exists)
|
||||
{
|
||||
logger.LogTrace("Serving static file \"{filename}\"...", appRoute);
|
||||
var contentTypeProvider = new FileExtensionContentTypeProvider();
|
||||
if (!contentTypeProvider.TryGetContentType(fileInfo.Name, out var contentType))
|
||||
contentType = MediaTypeNames.Application.Octet;
|
||||
else if (contentType == MediaTypeNames.Application.Json)
|
||||
Response.Headers.Add(
|
||||
HeaderNames.CacheControl,
|
||||
new StringValues(new[] { "public", "max-age=31536000", "immutable" }));
|
||||
var foundFile = this.TryServeFile(hostEnvironment, logger, appRoute);
|
||||
if (foundFile != null)
|
||||
return foundFile;
|
||||
|
||||
return File(appRoute, contentType);
|
||||
}
|
||||
else
|
||||
logger.LogTrace("Requested static file \"{filename}\" does not exist! Redirecting to index...", appRoute);
|
||||
logger.LogTrace("Requested static file \"{filename}\" does not exist! Redirecting to index...", appRoute);
|
||||
|
||||
return File("index.html", MediaTypeNames.Text.Html);
|
||||
}
|
||||
|
||||
@@ -108,7 +108,7 @@ namespace Tgstation.Server.Host.Controllers
|
||||
[TgsAuthorize(DreamDaemonRights.ReadMetadata | DreamDaemonRights.ReadRevision)]
|
||||
[ProducesResponseType(typeof(DreamDaemonResponse), 200)]
|
||||
[ProducesResponseType(typeof(ErrorMessageResponse), 410)]
|
||||
public ValueTask<IActionResult> Read(CancellationToken cancellationToken) => ReadImpl(null, cancellationToken);
|
||||
public ValueTask<IActionResult> Read(CancellationToken cancellationToken) => ReadImpl(null, false, cancellationToken);
|
||||
|
||||
/// <summary>
|
||||
/// Stops the Watchdog if it's running.
|
||||
@@ -233,7 +233,7 @@ namespace Tgstation.Server.Host.Controllers
|
||||
|
||||
// run this second because current may be modified by it
|
||||
// slight race condition with request cancellation, but I CANNOT be assed right now
|
||||
await watchdog.ChangeSettings(current, cancellationToken);
|
||||
var rebootRequired = await watchdog.ChangeSettings(current, cancellationToken);
|
||||
|
||||
var rebootState = watchdog.RebootState;
|
||||
var oldSoftRestart = rebootState == RebootState.Restart;
|
||||
@@ -245,7 +245,7 @@ namespace Tgstation.Server.Host.Controllers
|
||||
else if ((oldSoftRestart && model.SoftRestart == false) || (oldSoftShutdown && model.SoftShutdown == false))
|
||||
await watchdog.ResetRebootState(cancellationToken);
|
||||
|
||||
return await ReadImpl(current, cancellationToken);
|
||||
return await ReadImpl(current, rebootRequired, cancellationToken);
|
||||
});
|
||||
}
|
||||
#pragma warning restore CA1506
|
||||
@@ -307,9 +307,10 @@ namespace Tgstation.Server.Host.Controllers
|
||||
/// Implementation of <see cref="Read(CancellationToken)"/>.
|
||||
/// </summary>
|
||||
/// <param name="settings">The <see cref="DreamDaemonSettings"/> to operate on if any.</param>
|
||||
/// <param name="knownForcedReboot">If there was a settings change made that forced a switch to <see cref="RebootState.Restart"/>.</param>
|
||||
/// <param name="cancellationToken">The <see cref="CancellationToken"/> for the operation.</param>
|
||||
/// <returns>A <see cref="ValueTask{TResult}"/> resulting in the <see cref="IActionResult"/> of the operation.</returns>
|
||||
ValueTask<IActionResult> ReadImpl(DreamDaemonSettings settings, CancellationToken cancellationToken)
|
||||
ValueTask<IActionResult> ReadImpl(DreamDaemonSettings settings, bool knownForcedReboot, CancellationToken cancellationToken)
|
||||
=> WithComponentInstance(async instance =>
|
||||
{
|
||||
var dd = instance.Watchdog;
|
||||
@@ -362,6 +363,10 @@ namespace Tgstation.Server.Host.Controllers
|
||||
result.Visibility = settings.Visibility.Value;
|
||||
result.SoftRestart = rstate == RebootState.Restart;
|
||||
result.SoftShutdown = rstate == RebootState.Shutdown;
|
||||
|
||||
if (rstate == RebootState.Normal && knownForcedReboot)
|
||||
result.SoftRestart = true;
|
||||
|
||||
result.StartupTimeout = settings.StartupTimeout.Value;
|
||||
result.HealthCheckSeconds = settings.HealthCheckSeconds.Value;
|
||||
result.DumpOnHealthCheckRestart = settings.DumpOnHealthCheckRestart.Value;
|
||||
|
||||
@@ -2,10 +2,13 @@
|
||||
using System.Collections.Generic;
|
||||
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.Hosting;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Microsoft.Extensions.Options;
|
||||
|
||||
using Tgstation.Server.Host.Configuration;
|
||||
using Tgstation.Server.Host.Extensions;
|
||||
using Tgstation.Server.Host.System;
|
||||
using Tgstation.Server.Host.Utils;
|
||||
|
||||
@@ -21,14 +24,14 @@ namespace Tgstation.Server.Host.Controllers
|
||||
public sealed class RootController : Controller
|
||||
{
|
||||
/// <summary>
|
||||
/// The route to the TGS logo .svg in the <see cref="Microsoft.AspNetCore.Hosting.IWebHostEnvironment.WebRootPath"/> on Windows.
|
||||
/// The name of the TGS logo .svg in the <see cref="IWebHostEnvironment.WebRootPath"/> on Windows.
|
||||
/// </summary>
|
||||
public const string ProjectLogoSvgRouteWindows = "/0176d5d8b7d307f158e0.svg";
|
||||
const string LogoSvgWindowsName = "0176d5d8b7d307f158e0";
|
||||
|
||||
/// <summary>
|
||||
/// The route to the TGS logo .svg in the <see cref="Microsoft.AspNetCore.Hosting.IWebHostEnvironment.WebRootPath"/> on Linux.
|
||||
/// The name of the TGS logo .svg in the <see cref="IWebHostEnvironment.WebRootPath"/> on Linux.
|
||||
/// </summary>
|
||||
public const string ProjectLogoSvgRouteLinux = "/b5616c99bf2052a6bbd7.svg";
|
||||
const string LogoSvgLinuxName = "b5616c99bf2052a6bbd7";
|
||||
|
||||
/// <summary>
|
||||
/// The <see cref="IAssemblyInformationProvider"/> for the <see cref="RootController"/>.
|
||||
@@ -40,6 +43,16 @@ namespace Tgstation.Server.Host.Controllers
|
||||
/// </summary>
|
||||
readonly IPlatformIdentifier platformIdentifier;
|
||||
|
||||
/// <summary>
|
||||
/// THe <see cref="IWebHostEnvironment"/> for the <see cref="RootController"/>.
|
||||
/// </summary>
|
||||
readonly IWebHostEnvironment hostEnvironment;
|
||||
|
||||
/// <summary>
|
||||
/// The <see cref="ILogger"/> for the <see cref="RootController"/>.
|
||||
/// </summary>
|
||||
readonly ILogger<RootController> logger;
|
||||
|
||||
/// <summary>
|
||||
/// The <see cref="GeneralConfiguration"/> for the <see cref="RootController"/>.
|
||||
/// </summary>
|
||||
@@ -55,16 +68,22 @@ namespace Tgstation.Server.Host.Controllers
|
||||
/// </summary>
|
||||
/// <param name="assemblyInformationProvider">The value of <see cref="assemblyInformationProvider"/>.</param>
|
||||
/// <param name="platformIdentifier">The value of <see cref="platformIdentifier"/>.</param>
|
||||
/// <param name="hostEnvironment">The value of <see cref="hostEnvironment"/>.</param>
|
||||
/// <param name="logger">The value of <see cref="logger"/>.</param>
|
||||
/// <param name="generalConfigurationOptions">The <see cref="IOptions{TOptions}"/> containing the value of <see cref="generalConfiguration"/>.</param>
|
||||
/// <param name="controlPanelConfigurationOptions">The <see cref="IOptions{TOptions}"/> containing the value of <see cref="controlPanelConfiguration"/>.</param>
|
||||
public RootController(
|
||||
IAssemblyInformationProvider assemblyInformationProvider,
|
||||
IPlatformIdentifier platformIdentifier,
|
||||
IWebHostEnvironment hostEnvironment,
|
||||
ILogger<RootController> logger,
|
||||
IOptions<GeneralConfiguration> generalConfigurationOptions,
|
||||
IOptions<ControlPanelConfiguration> controlPanelConfigurationOptions)
|
||||
{
|
||||
this.assemblyInformationProvider = assemblyInformationProvider ?? throw new ArgumentNullException(nameof(assemblyInformationProvider));
|
||||
this.platformIdentifier = platformIdentifier ?? throw new ArgumentNullException(nameof(platformIdentifier));
|
||||
this.hostEnvironment = hostEnvironment ?? throw new ArgumentNullException(nameof(hostEnvironment));
|
||||
this.logger = logger ?? throw new ArgumentNullException(nameof(logger));
|
||||
generalConfiguration = generalConfigurationOptions?.Value ?? throw new ArgumentNullException(nameof(generalConfigurationOptions));
|
||||
controlPanelConfiguration = controlPanelConfigurationOptions?.Value ?? throw new ArgumentNullException(nameof(controlPanelConfigurationOptions));
|
||||
}
|
||||
@@ -100,13 +119,24 @@ namespace Tgstation.Server.Host.Controllers
|
||||
var model = new
|
||||
{
|
||||
Links = links,
|
||||
Svg = platformIdentifier.IsWindows // these are different because of motherfucking line endings -_-
|
||||
? ProjectLogoSvgRouteWindows
|
||||
: ProjectLogoSvgRouteLinux,
|
||||
Title = assemblyInformationProvider.VersionString,
|
||||
};
|
||||
|
||||
return View(model);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Retrieve the logo .svg for the webpanel.
|
||||
/// </summary>
|
||||
/// <returns>The appropriate <see cref="IActionResult"/>.</returns>
|
||||
[HttpGet("logo.svg")]
|
||||
public IActionResult GetLogo()
|
||||
{
|
||||
var logoFileName = platformIdentifier.IsWindows // these are different because of motherfucking line endings -_-
|
||||
? LogoSvgWindowsName
|
||||
: LogoSvgLinuxName;
|
||||
|
||||
return (IActionResult)this.TryServeFile(hostEnvironment, logger, $"{logoFileName}.svg") ?? NotFound();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -367,7 +367,9 @@ namespace Tgstation.Server.Host.Core
|
||||
|
||||
// only global repo manager should be for the OD repo
|
||||
var openDreamRepositoryDirectory = ioManager.ConcatPath(
|
||||
Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData),
|
||||
Environment.GetFolderPath(
|
||||
Environment.SpecialFolder.LocalApplicationData,
|
||||
Environment.SpecialFolderOption.DoNotVerify),
|
||||
assemblyInformationProvider.VersionPrefix,
|
||||
"OpenDreamRepository");
|
||||
services.AddSingleton(
|
||||
|
||||
@@ -245,11 +245,8 @@ namespace Tgstation.Server.Host.Database
|
||||
const string ConfigureMethodName = nameof(SqlServerDatabaseContext.ConfigureWith);
|
||||
var configureFunction = typeof(TDatabaseContext).GetMethod(
|
||||
ConfigureMethodName,
|
||||
BindingFlags.Public | BindingFlags.Static);
|
||||
|
||||
if (configureFunction == null)
|
||||
throw new InvalidOperationException($"Context type {typeof(TDatabaseContext).FullName} missing static {ConfigureMethodName} function!");
|
||||
|
||||
BindingFlags.Public | BindingFlags.Static)
|
||||
?? throw new InvalidOperationException($"Context type {typeof(TDatabaseContext).FullName} missing static {ConfigureMethodName} function!");
|
||||
return (optionsBuilder, config) => configureFunction.Invoke(null, new object[] { optionsBuilder, config });
|
||||
}
|
||||
|
||||
|
||||
@@ -1,7 +1,13 @@
|
||||
using System;
|
||||
using System.Net;
|
||||
using System.Net.Mime;
|
||||
|
||||
using Microsoft.AspNetCore.Hosting;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.AspNetCore.StaticFiles;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Microsoft.Extensions.Primitives;
|
||||
using Microsoft.Net.Http.Headers;
|
||||
|
||||
using Tgstation.Server.Api.Models;
|
||||
using Tgstation.Server.Api.Models.Response;
|
||||
@@ -30,5 +36,38 @@ namespace Tgstation.Server.Host.Extensions
|
||||
/// <returns>A <see cref="StatusCodeResult"/> with the given <paramref name="statusCode"/>.</returns>
|
||||
public static ObjectResult StatusCode(this ControllerBase controller, HttpStatusCode statusCode, object? errorMessage)
|
||||
=> controller?.StatusCode((int)statusCode, errorMessage) ?? throw new ArgumentNullException(nameof(controller));
|
||||
|
||||
/// <summary>
|
||||
/// Try to serve a given file <paramref name="path"/>.
|
||||
/// </summary>
|
||||
/// <param name="controller">The <see cref="ControllerBase"/>.</param>
|
||||
/// <param name="hostEnvironment">The <see cref="IWebHostEnvironment"/>.</param>
|
||||
/// <param name="logger">The <see cref="ILogger"/>.</param>
|
||||
/// <param name="path">The path to the file in the 'wwwroot'.</param>
|
||||
/// <returns>A <see cref="VirtualFileResult"/> if the file was found. <see langword="null"/> otherwise.</returns>
|
||||
public static VirtualFileResult? TryServeFile(this ControllerBase controller, IWebHostEnvironment hostEnvironment, ILogger logger, string path)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(controller);
|
||||
ArgumentNullException.ThrowIfNull(hostEnvironment);
|
||||
ArgumentNullException.ThrowIfNull(logger);
|
||||
ArgumentNullException.ThrowIfNull(path);
|
||||
|
||||
var fileInfo = hostEnvironment.WebRootFileProvider.GetFileInfo(path);
|
||||
if (fileInfo.Exists)
|
||||
{
|
||||
logger.LogTrace("Serving static file \"{filename}\"...", path);
|
||||
var contentTypeProvider = new FileExtensionContentTypeProvider();
|
||||
if (!contentTypeProvider.TryGetContentType(fileInfo.Name, out var contentType))
|
||||
contentType = MediaTypeNames.Application.Octet;
|
||||
else if (contentType == MediaTypeNames.Application.Json)
|
||||
controller.Response.Headers.Add(
|
||||
HeaderNames.CacheControl,
|
||||
new StringValues(new[] { "public", "max-age=31536000", "immutable" }));
|
||||
|
||||
return controller.File(path, contentType);
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,91 @@
|
||||
using System;
|
||||
using System.Threading;
|
||||
|
||||
using LibGit2Sharp;
|
||||
using LibGit2Sharp.Handlers;
|
||||
|
||||
using Microsoft.Extensions.Logging;
|
||||
|
||||
using Tgstation.Server.Host.Jobs;
|
||||
|
||||
namespace Tgstation.Server.Host.Extensions
|
||||
{
|
||||
/// <summary>
|
||||
/// Extension methods for <see cref="FetchOptions"/>.
|
||||
/// </summary>
|
||||
static class FetchOptionsExtensions
|
||||
{
|
||||
/// <summary>
|
||||
/// Hydrate a given set of <paramref name="fetchOptions"/>.
|
||||
/// </summary>
|
||||
/// <param name="fetchOptions">The <see cref="FetchOptions"/> to hydrate.</param>
|
||||
/// <param name="logger">The <see cref="ILogger"/> for the operation.</param>
|
||||
/// <param name="progressReporter">The optional <see cref="JobProgressReporter"/>.</param>
|
||||
/// <param name="credentialsHandler">The optional <see cref="CredentialsHandler"/>.</param>
|
||||
/// <param name="cancellationToken">The <see cref="CancellationToken"/> for the operation.</param>
|
||||
/// <returns>The hydrated <paramref name="fetchOptions"/>.</returns>
|
||||
public static FetchOptions Hydrate(
|
||||
this FetchOptions fetchOptions,
|
||||
ILogger logger,
|
||||
JobProgressReporter progressReporter,
|
||||
CredentialsHandler credentialsHandler,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(fetchOptions);
|
||||
ArgumentNullException.ThrowIfNull(logger);
|
||||
|
||||
fetchOptions.OnProgress = _ => !cancellationToken.IsCancellationRequested;
|
||||
fetchOptions.OnTransferProgress = transferProgress =>
|
||||
{
|
||||
if (progressReporter != null)
|
||||
{
|
||||
var percentage = ((double)transferProgress.IndexedObjects + transferProgress.ReceivedObjects) / (transferProgress.TotalObjects * 2);
|
||||
progressReporter.ReportProgress(percentage);
|
||||
}
|
||||
|
||||
return !cancellationToken.IsCancellationRequested;
|
||||
};
|
||||
fetchOptions.OnUpdateTips = (_, _, _) => !cancellationToken.IsCancellationRequested;
|
||||
fetchOptions.CredentialsProvider = credentialsHandler;
|
||||
fetchOptions.RepositoryOperationStarting = _ => !cancellationToken.IsCancellationRequested;
|
||||
fetchOptions.OnTransferProgress = TransferProgressHandler(
|
||||
logger,
|
||||
progressReporter,
|
||||
cancellationToken);
|
||||
|
||||
return fetchOptions;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Generate a <see cref="LibGit2Sharp.Handlers.TransferProgressHandler"/> from a given <paramref name="progressReporter"/> and <paramref name="cancellationToken"/>.
|
||||
/// </summary>
|
||||
/// <param name="logger">The <see cref="ILogger"/> for the operation.</param>
|
||||
/// <param name="progressReporter">The optional <see cref="JobProgressReporter"/> of the operation.</param>
|
||||
/// <param name="cancellationToken">The <see cref="CancellationToken"/> for the operation.</param>
|
||||
/// <returns>A new <see cref="LibGit2Sharp.Handlers.TransferProgressHandler"/> based on <paramref name="progressReporter"/>.</returns>
|
||||
static TransferProgressHandler TransferProgressHandler(ILogger logger, JobProgressReporter progressReporter, CancellationToken cancellationToken) => transferProgress =>
|
||||
{
|
||||
double? percentage;
|
||||
var totalObjectsToProcess = transferProgress.TotalObjects * 2;
|
||||
var processedObjects = transferProgress.IndexedObjects + transferProgress.ReceivedObjects;
|
||||
if (totalObjectsToProcess < processedObjects || totalObjectsToProcess == 0)
|
||||
percentage = null;
|
||||
else
|
||||
{
|
||||
percentage = (double)processedObjects / totalObjectsToProcess;
|
||||
if (percentage < 0)
|
||||
percentage = null;
|
||||
}
|
||||
|
||||
if (percentage == null)
|
||||
logger.LogDebug(
|
||||
"Bad transfer progress values (Please tell Cyberboss)! Indexed: {indexed}, Received: {received}, Total: {total}",
|
||||
transferProgress.IndexedObjects,
|
||||
transferProgress.ReceivedObjects,
|
||||
transferProgress.TotalObjects);
|
||||
|
||||
progressReporter?.ReportProgress(percentage);
|
||||
return !cancellationToken.IsCancellationRequested;
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,68 @@
|
||||
using System;
|
||||
using System.Net;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
using Byond.TopicSender;
|
||||
|
||||
using Microsoft.Extensions.Logging;
|
||||
|
||||
using Tgstation.Server.Host.Utils;
|
||||
|
||||
namespace Tgstation.Server.Host.Extensions
|
||||
{
|
||||
/// <summary>
|
||||
/// Extension methods for <see cref="ITopicClient"/>.
|
||||
/// </summary>
|
||||
static class TopicClientExtensions
|
||||
{
|
||||
/// <summary>
|
||||
/// Send a <paramref name="queryString"/> with optional repeated priority.
|
||||
/// </summary>
|
||||
/// <param name="topicClient">The <see cref="ITopicClient"/> to send with.</param>
|
||||
/// <param name="delayer">The <see cref="IAsyncDelayer"/> to use for delayed retries if an error occurs.</param>
|
||||
/// <param name="logger">The <see cref="ILogger"/> to write to.</param>
|
||||
/// <param name="queryString">The <see cref="string"/> to send.</param>
|
||||
/// <param name="port">The local port to send the topic to.</param>
|
||||
/// <param name="priority">If priority retries should be used.</param>
|
||||
/// <param name="cancellationToken">The <see cref="CancellationToken"/> for the operation.</param>
|
||||
/// <returns>A <see cref="ValueTask{TResult}"/> resulting in the <see cref="TopicResponse"/> on success, <see langword="null"/> on failure.</returns>
|
||||
public static async ValueTask<TopicResponse?> SendWithOptionalPriority(
|
||||
this ITopicClient topicClient,
|
||||
IAsyncDelayer delayer,
|
||||
ILogger logger,
|
||||
string queryString,
|
||||
ushort port,
|
||||
bool priority,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
const int PrioritySendAttempts = 5;
|
||||
var endpoint = new IPEndPoint(IPAddress.Loopback, port);
|
||||
var firstSend = true;
|
||||
|
||||
for (var i = PrioritySendAttempts - 1; i >= 0 && (priority || firstSend); --i)
|
||||
try
|
||||
{
|
||||
firstSend = false;
|
||||
|
||||
logger.LogTrace("Begin topic request");
|
||||
var byondResponse = await topicClient.SendTopic(
|
||||
endpoint,
|
||||
queryString,
|
||||
cancellationToken);
|
||||
|
||||
logger.LogTrace("End topic request");
|
||||
return byondResponse;
|
||||
}
|
||||
catch (Exception ex) when (ex is not OperationCanceledException)
|
||||
{
|
||||
logger.LogWarning(ex, "SendTopic exception!{retryDetails}", priority ? $" {i} attempts remaining." : String.Empty);
|
||||
|
||||
if (priority && i > 0)
|
||||
await delayer.Delay(TimeSpan.FromSeconds(2), cancellationToken);
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -11,6 +11,7 @@ using Microsoft.Extensions.Logging;
|
||||
|
||||
using Serilog.Context;
|
||||
|
||||
using Tgstation.Server.Api.Extensions;
|
||||
using Tgstation.Server.Api.Hubs;
|
||||
using Tgstation.Server.Api.Models.Response;
|
||||
using Tgstation.Server.Common.Extensions;
|
||||
@@ -447,7 +448,12 @@ namespace Tgstation.Server.Host.Jobs
|
||||
}
|
||||
}
|
||||
|
||||
var instanceCoreProvider = await activationTcs.Task.WaitAsync(cancellationToken);
|
||||
var activationTask = activationTcs.Task;
|
||||
|
||||
Debug.Assert(activationTask.IsCompleted || job.Require(x => x.JobCode).IsServerStartupJob(), "Non-server startup job registered before activation!");
|
||||
|
||||
var instanceCoreProvider = await activationTask.WaitAsync(cancellationToken);
|
||||
|
||||
QueueHubUpdate(job.ToApi(), false);
|
||||
|
||||
logger.LogTrace("Starting job...");
|
||||
|
||||
@@ -7,9 +7,15 @@
|
||||
"ASPNETCORE_ENVIRONMENT": "Development"
|
||||
}
|
||||
},
|
||||
"Docker": {
|
||||
"commandName": "Docker",
|
||||
"publishAllPorts": true
|
||||
"WSL": {
|
||||
"commandName": "WSL2",
|
||||
"distributionName": "Ubuntu",
|
||||
"environmentVariables": {
|
||||
"DOTNET_ENVIRONMENT": "Development",
|
||||
"ASPNETCORE_ENVIRONMENT": "Development",
|
||||
"Database__ConnectionString": "Data Source=192.168.2.16,1433;Initial Catalog=TGS_Linux;User Id=tgs_debug;Password=asdf;Encrypt=False;Application Name=tgstation-server",
|
||||
"General__ValidInstancePaths__0": "/home/dominion/tgs_debug_pen"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -603,6 +603,8 @@ namespace Tgstation.Server.Host.Setup
|
||||
};
|
||||
|
||||
CreateTestConnection(csb.ConnectionString);
|
||||
|
||||
csb.Mode = SqliteOpenMode.ReadWriteCreate;
|
||||
databaseConfiguration.ConnectionString = csb.ConnectionString;
|
||||
}
|
||||
|
||||
|
||||
@@ -23,13 +23,12 @@ namespace Tgstation.Server.Host.System
|
||||
/// Get the stderr and stdout output of the <see cref="IProcess"/>.
|
||||
/// </summary>
|
||||
/// <param name="cancellationToken">The <see cref="CancellationToken"/> for the operation.</param>
|
||||
/// <returns>A <see cref="ValueTask{TResult}"/> resulting in the stderr and stdout output of the <see cref="IProcess"/>.</returns>
|
||||
/// <returns>A <see cref="Task{TResult}"/> resulting in the stderr and stdout output of the <see cref="IProcess"/>.</returns>
|
||||
/// <remarks>
|
||||
/// To guarantee that all data is received from the <see cref="IProcess"/> when redirecting streams to a file
|
||||
/// the result of this function must be <see langword="await"/>ed before <see cref="IAsyncDisposable.DisposeAsync"/> is called.
|
||||
/// May call <see cref="IAsyncDisposable.DisposeAsync"/> internally if the process has exited.
|
||||
/// </remarks>
|
||||
ValueTask<string?> GetCombinedOutput(CancellationToken cancellationToken);
|
||||
Task<string?> GetCombinedOutput(CancellationToken cancellationToken);
|
||||
|
||||
/// <summary>
|
||||
/// Asycnhronously terminates the process.
|
||||
|
||||
@@ -7,7 +7,6 @@ using Microsoft.Extensions.Logging;
|
||||
using Microsoft.Win32.SafeHandles;
|
||||
|
||||
using Tgstation.Server.Host.IO;
|
||||
using Tgstation.Server.Host.Utils;
|
||||
|
||||
namespace Tgstation.Server.Host.System
|
||||
{
|
||||
@@ -28,11 +27,6 @@ namespace Tgstation.Server.Host.System
|
||||
/// </summary>
|
||||
readonly IProcessFeatures processFeatures;
|
||||
|
||||
/// <summary>
|
||||
/// The <see cref="IAsyncDelayer"/> for the <see cref="Process"/>.
|
||||
/// </summary>
|
||||
readonly IAsyncDelayer asyncDelayer;
|
||||
|
||||
/// <summary>
|
||||
/// The <see cref="ILogger"/> for the <see cref="Process"/>.
|
||||
/// </summary>
|
||||
@@ -68,7 +62,6 @@ namespace Tgstation.Server.Host.System
|
||||
/// Initializes a new instance of the <see cref="Process"/> class.
|
||||
/// </summary>
|
||||
/// <param name="processFeatures">The value of <see cref="processFeatures"/>.</param>
|
||||
/// <param name="asyncDelayer">The value of <see cref="asyncDelayer"/>.</param>
|
||||
/// <param name="handle">The value of <see cref="handle"/>.</param>
|
||||
/// <param name="readerCts">The override value of <see cref="cancellationTokenSource"/>.</param>
|
||||
/// <param name="readTask">The value of <see cref="readTask"/>.</param>
|
||||
@@ -76,7 +69,6 @@ namespace Tgstation.Server.Host.System
|
||||
/// <param name="preExisting">If <paramref name="handle"/> was NOT just created.</param>
|
||||
public Process(
|
||||
IProcessFeatures processFeatures,
|
||||
IAsyncDelayer asyncDelayer,
|
||||
global::System.Diagnostics.Process handle,
|
||||
CancellationTokenSource? readerCts,
|
||||
Task<string?>? readTask,
|
||||
@@ -92,7 +84,6 @@ namespace Tgstation.Server.Host.System
|
||||
cancellationTokenSource = readerCts ?? new CancellationTokenSource();
|
||||
|
||||
this.processFeatures = processFeatures ?? throw new ArgumentNullException(nameof(processFeatures));
|
||||
this.asyncDelayer = asyncDelayer ?? throw new ArgumentNullException(nameof(asyncDelayer));
|
||||
|
||||
this.readTask = readTask;
|
||||
|
||||
@@ -128,7 +119,7 @@ namespace Tgstation.Server.Host.System
|
||||
/// <inheritdoc />
|
||||
public async ValueTask DisposeAsync()
|
||||
{
|
||||
if (Interlocked.Exchange(ref disposed, 1) == 1)
|
||||
if (Interlocked.Exchange(ref disposed, 1) != 0)
|
||||
return;
|
||||
|
||||
logger.LogTrace("Disposing PID {pid}...", Id);
|
||||
@@ -144,30 +135,18 @@ namespace Tgstation.Server.Host.System
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public async ValueTask<string?> GetCombinedOutput(CancellationToken cancellationToken)
|
||||
public Task<string?> GetCombinedOutput(CancellationToken cancellationToken)
|
||||
{
|
||||
if (readTask == null)
|
||||
throw new InvalidOperationException("Output/Error stream reading was not enabled!");
|
||||
|
||||
// workaround for https://github.com/dotnet/runtime/issues/28583 (?)
|
||||
if (handle.HasExited)
|
||||
{
|
||||
handle.WaitForExit();
|
||||
await Task.WhenAny(readTask, asyncDelayer.Delay(TimeSpan.FromSeconds(30), cancellationToken));
|
||||
|
||||
if (!readTask.IsCompleted)
|
||||
{
|
||||
logger.LogWarning("Detected process output read hang on PID {pid}! Closing handle as a workaround...", Id);
|
||||
await DisposeAsync();
|
||||
}
|
||||
}
|
||||
|
||||
return await readTask.WaitAsync(cancellationToken);
|
||||
return readTask.WaitAsync(cancellationToken);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public void Terminate()
|
||||
{
|
||||
CheckDisposed();
|
||||
if (handle.HasExited)
|
||||
{
|
||||
logger.LogTrace("PID {pid} already exited", Id);
|
||||
@@ -190,6 +169,7 @@ namespace Tgstation.Server.Host.System
|
||||
/// <inheritdoc />
|
||||
public void AdjustPriority(bool higher)
|
||||
{
|
||||
CheckDisposed();
|
||||
var targetPriority = higher ? ProcessPriorityClass.AboveNormal : ProcessPriorityClass.BelowNormal;
|
||||
try
|
||||
{
|
||||
@@ -205,6 +185,7 @@ namespace Tgstation.Server.Host.System
|
||||
/// <inheritdoc />
|
||||
public void Suspend()
|
||||
{
|
||||
CheckDisposed();
|
||||
try
|
||||
{
|
||||
processFeatures.SuspendProcess(handle);
|
||||
@@ -220,6 +201,7 @@ namespace Tgstation.Server.Host.System
|
||||
/// <inheritdoc />
|
||||
public void Resume()
|
||||
{
|
||||
CheckDisposed();
|
||||
try
|
||||
{
|
||||
processFeatures.ResumeProcess(handle);
|
||||
@@ -235,6 +217,7 @@ namespace Tgstation.Server.Host.System
|
||||
/// <inheritdoc />
|
||||
public string GetExecutingUsername()
|
||||
{
|
||||
CheckDisposed();
|
||||
var result = processFeatures.GetExecutingUsername(handle);
|
||||
logger.LogTrace("PID {pid} Username: {username}", Id, result);
|
||||
return result;
|
||||
@@ -244,6 +227,7 @@ namespace Tgstation.Server.Host.System
|
||||
public ValueTask CreateDump(string outputFile, CancellationToken cancellationToken)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(outputFile);
|
||||
CheckDisposed();
|
||||
|
||||
logger.LogTrace("Dumping PID {pid} to {dumpFilePath}...", Id, outputFile);
|
||||
return processFeatures.CreateDump(handle, outputFile, cancellationToken);
|
||||
@@ -255,18 +239,29 @@ namespace Tgstation.Server.Host.System
|
||||
/// <returns>A <see cref="Task{TResult}"/> resulting in the <see cref="global::System.Diagnostics.Process.ExitCode"/> or <see langword="null"/> if the process was detached.</returns>
|
||||
async Task<int?> WrapLifetimeTask()
|
||||
{
|
||||
bool hasExited;
|
||||
try
|
||||
{
|
||||
await handle.WaitForExitAsync(cancellationTokenSource.Token);
|
||||
var exitCode = handle.ExitCode;
|
||||
logger.LogTrace("PID {pid} exited with code {exitCode}", Id, exitCode);
|
||||
return exitCode;
|
||||
hasExited = true;
|
||||
}
|
||||
catch (OperationCanceledException ex)
|
||||
{
|
||||
logger.LogTrace(ex, "Process lifetime task cancelled!");
|
||||
return null;
|
||||
hasExited = handle.HasExited;
|
||||
}
|
||||
|
||||
if (!hasExited)
|
||||
return null;
|
||||
|
||||
var exitCode = handle.ExitCode;
|
||||
logger.LogTrace("PID {pid} exited with code {exitCode}", Id, exitCode);
|
||||
return exitCode;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Throws an <see cref="ObjectDisposedException"/> if a method of the <see cref="Process"/> was called after <see cref="DisposeAsync"/>.
|
||||
/// </summary>
|
||||
void CheckDisposed() => ObjectDisposedException.ThrowIf(disposed != 0, this);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,13 +1,14 @@
|
||||
using System;
|
||||
using System.Diagnostics;
|
||||
using System.IO;
|
||||
using System.Text;
|
||||
using System.Threading;
|
||||
using System.Threading.Channels;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
using Microsoft.Extensions.Logging;
|
||||
|
||||
using Tgstation.Server.Host.IO;
|
||||
using Tgstation.Server.Host.Utils;
|
||||
|
||||
namespace Tgstation.Server.Host.System
|
||||
{
|
||||
@@ -24,11 +25,6 @@ namespace Tgstation.Server.Host.System
|
||||
/// </summary>
|
||||
readonly IProcessFeatures processFeatures;
|
||||
|
||||
/// <summary>
|
||||
/// The <see cref="IAsyncDelayer"/> for the <see cref="ProcessExecutor"/>.
|
||||
/// </summary>
|
||||
readonly IAsyncDelayer asyncDelayer;
|
||||
|
||||
/// <summary>
|
||||
/// The <see cref="IIOManager"/> for the <see cref="ProcessExecutor"/>.
|
||||
/// </summary>
|
||||
@@ -65,19 +61,16 @@ namespace Tgstation.Server.Host.System
|
||||
/// Initializes a new instance of the <see cref="ProcessExecutor"/> class.
|
||||
/// </summary>
|
||||
/// <param name="processFeatures">The value of <see cref="processFeatures"/>.</param>
|
||||
/// <param name="asyncDelayer">The value of <see cref="asyncDelayer"/>.</param>
|
||||
/// <param name="ioManager">The value of <see cref="ioManager"/>.</param>
|
||||
/// <param name="logger">The value of <see cref="logger"/>.</param>
|
||||
/// <param name="loggerFactory">The value of <see cref="loggerFactory"/>.</param>
|
||||
public ProcessExecutor(
|
||||
IProcessFeatures processFeatures,
|
||||
IAsyncDelayer asyncDelayer,
|
||||
IIOManager ioManager,
|
||||
ILogger<ProcessExecutor> logger,
|
||||
ILoggerFactory loggerFactory)
|
||||
{
|
||||
this.processFeatures = processFeatures ?? throw new ArgumentNullException(nameof(processFeatures));
|
||||
this.asyncDelayer = asyncDelayer ?? throw new ArgumentNullException(nameof(asyncDelayer));
|
||||
this.ioManager = ioManager ?? throw new ArgumentNullException(nameof(ioManager));
|
||||
this.logger = logger ?? throw new ArgumentNullException(nameof(logger));
|
||||
this.loggerFactory = loggerFactory ?? throw new ArgumentNullException(nameof(loggerFactory));
|
||||
@@ -122,16 +115,19 @@ namespace Tgstation.Server.Host.System
|
||||
ArgumentNullException.ThrowIfNull(workingDirectory);
|
||||
ArgumentNullException.ThrowIfNull(arguments);
|
||||
|
||||
if (!noShellExecute && readStandardHandles)
|
||||
throw new InvalidOperationException("Requesting output/error reading requires noShellExecute to be true!");
|
||||
|
||||
logger.LogDebug(
|
||||
noShellExecute
|
||||
? "Launching process in {workingDirectory}: {exe} {arguments}"
|
||||
: "Shell launching process in {workingDirectory}: {exe} {arguments}",
|
||||
if (noShellExecute)
|
||||
logger.LogDebug(
|
||||
"Launching process in {workingDirectory}: {exe} {arguments}",
|
||||
workingDirectory,
|
||||
fileName,
|
||||
arguments);
|
||||
else
|
||||
logger.LogDebug(
|
||||
"Shell launching process in {workingDirectory}: {exe} {arguments}",
|
||||
workingDirectory,
|
||||
fileName,
|
||||
arguments);
|
||||
|
||||
var handle = new global::System.Diagnostics.Process();
|
||||
try
|
||||
{
|
||||
@@ -145,17 +141,15 @@ namespace Tgstation.Server.Host.System
|
||||
CancellationTokenSource? disposeCts = null;
|
||||
try
|
||||
{
|
||||
TaskCompletionSource? processStartTcs = null;
|
||||
TaskCompletionSource<int>? processStartTcs = null;
|
||||
if (readStandardHandles)
|
||||
{
|
||||
processStartTcs = new TaskCompletionSource();
|
||||
handle.StartInfo.RedirectStandardOutput = true;
|
||||
handle.StartInfo.RedirectStandardError = true;
|
||||
|
||||
processStartTcs = new TaskCompletionSource<int>();
|
||||
disposeCts = new CancellationTokenSource();
|
||||
readTask = ConsumeReaders(handle, processStartTcs.Task, fileRedirect, disposeCts.Token);
|
||||
}
|
||||
|
||||
int pid;
|
||||
try
|
||||
{
|
||||
ExclusiveProcessLaunchLock.EnterReadLock();
|
||||
@@ -168,7 +162,8 @@ namespace Tgstation.Server.Host.System
|
||||
ExclusiveProcessLaunchLock.ExitReadLock();
|
||||
}
|
||||
|
||||
processStartTcs?.SetResult();
|
||||
pid = handle.Id;
|
||||
processStartTcs?.SetResult(pid);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
@@ -178,7 +173,6 @@ namespace Tgstation.Server.Host.System
|
||||
|
||||
var process = new Process(
|
||||
processFeatures,
|
||||
asyncDelayer,
|
||||
handle,
|
||||
disposeCts,
|
||||
readTask,
|
||||
@@ -225,76 +219,102 @@ namespace Tgstation.Server.Host.System
|
||||
/// Consume the stdout/stderr streams into a <see cref="Task"/>.
|
||||
/// </summary>
|
||||
/// <param name="handle">The <see cref="global::System.Diagnostics.Process"/>.</param>
|
||||
/// <param name="startTask">The <see cref="Task"/> that completes when <paramref name="handle"/> starts.</param>
|
||||
/// <param name="startupAndPid">The <see cref="Task{TResult}"/> resulting in the <see cref="global::System.Diagnostics.Process.Id"/> of the started process.</param>
|
||||
/// <param name="fileRedirect">The optional path to redirect the streams to.</param>
|
||||
/// <param name="disposeToken">The <see cref="CancellationToken"/> that triggers when the <see cref="Process"/> is disposed.</param>
|
||||
/// <param name="cancellationToken">The <see cref="CancellationToken"/> for the operation.</param>
|
||||
/// <returns>A <see cref="Task{TResult}"/> resulting in the program's output/error text if <paramref name="fileRedirect"/> is <see langword="null"/>, <see langword="null"/> otherwise.</returns>
|
||||
async Task<string?> ConsumeReaders(global::System.Diagnostics.Process handle, Task startTask, string? fileRedirect, CancellationToken disposeToken)
|
||||
async Task<string?> ConsumeReaders(global::System.Diagnostics.Process handle, Task<int> startupAndPid, string? fileRedirect, CancellationToken cancellationToken)
|
||||
{
|
||||
await startTask;
|
||||
handle.StartInfo.RedirectStandardOutput = true;
|
||||
handle.StartInfo.RedirectStandardError = true;
|
||||
|
||||
var pid = handle.Id;
|
||||
logger.LogTrace("Starting read for PID {pid}...", pid);
|
||||
bool writingToFile;
|
||||
await using var fileStream = (writingToFile = fileRedirect != null) ? ioManager.CreateAsyncSequentialWriteStream(fileRedirect) : null;
|
||||
await using var fileWriter = fileStream != null ? new StreamWriter(fileStream) : null;
|
||||
|
||||
// once we obtain these handles we're responsible for them
|
||||
using var stdOutHandle = handle.StandardOutput;
|
||||
using var stdErrHandle = handle.StandardError;
|
||||
Task<string?>? outputReadTask = null, errorReadTask = null;
|
||||
bool outputOpen = true, errorOpen = true;
|
||||
async Task<string?> GetNextLine()
|
||||
{
|
||||
if (outputOpen && outputReadTask == null)
|
||||
outputReadTask = stdOutHandle.ReadLineAsync(disposeToken).AsTask();
|
||||
var stringBuilder = fileStream == null ? new StringBuilder() : null;
|
||||
|
||||
if (errorOpen && errorReadTask == null)
|
||||
errorReadTask = stdErrHandle.ReadLineAsync(disposeToken).AsTask();
|
||||
|
||||
var completedTask = await Task.WhenAny(outputReadTask ?? errorReadTask!, errorReadTask ?? outputReadTask!);
|
||||
var line = await completedTask.WaitAsync(disposeToken);
|
||||
if (completedTask == outputReadTask)
|
||||
var dataChannel = Channel.CreateUnbounded<string>(
|
||||
new UnboundedChannelOptions
|
||||
{
|
||||
outputReadTask = null;
|
||||
if (line == null)
|
||||
outputOpen = false;
|
||||
AllowSynchronousContinuations = !writingToFile,
|
||||
SingleReader = true,
|
||||
SingleWriter = false,
|
||||
});
|
||||
|
||||
var handlesOpen = 2;
|
||||
async void DataReceivedHandler(object sender, DataReceivedEventArgs eventArgs)
|
||||
{
|
||||
var line = eventArgs.Data;
|
||||
if (line == null)
|
||||
{
|
||||
var handlesRemaining = Interlocked.Decrement(ref handlesOpen);
|
||||
if (handlesRemaining == 0)
|
||||
dataChannel.Writer.Complete();
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
await dataChannel.Writer.WriteAsync(line, cancellationToken);
|
||||
}
|
||||
catch (OperationCanceledException ex)
|
||||
{
|
||||
logger.LogWarning(ex, "Handle channel write interrupted!");
|
||||
}
|
||||
}
|
||||
|
||||
handle.OutputDataReceived += DataReceivedHandler;
|
||||
handle.ErrorDataReceived += DataReceivedHandler;
|
||||
|
||||
async ValueTask OutputWriter()
|
||||
{
|
||||
var enumerable = dataChannel.Reader.ReadAllAsync(cancellationToken);
|
||||
if (writingToFile)
|
||||
{
|
||||
var enumerator = enumerable.GetAsyncEnumerator(cancellationToken);
|
||||
var nextEnumeration = enumerator.MoveNextAsync();
|
||||
while (await nextEnumeration)
|
||||
{
|
||||
var text = enumerator.Current;
|
||||
nextEnumeration = enumerator.MoveNextAsync();
|
||||
await fileWriter!.WriteLineAsync(text.AsMemory(), cancellationToken);
|
||||
|
||||
if (!nextEnumeration.IsCompleted)
|
||||
await fileWriter.FlushAsync(cancellationToken);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
errorReadTask = null;
|
||||
if (line == null)
|
||||
errorOpen = false;
|
||||
}
|
||||
|
||||
if (line == null && (errorOpen || outputOpen))
|
||||
return await GetNextLine();
|
||||
|
||||
return line;
|
||||
}
|
||||
|
||||
await using var fileStream = fileRedirect != null ? ioManager.CreateAsyncSequentialWriteStream(fileRedirect) : null;
|
||||
await using var writer = fileStream != null ? new StreamWriter(fileStream) : null;
|
||||
|
||||
string? text;
|
||||
var stringBuilder = fileStream == null ? new StringBuilder() : null;
|
||||
try
|
||||
{
|
||||
while ((text = await GetNextLine()) != null)
|
||||
{
|
||||
if (fileStream != null)
|
||||
{
|
||||
await writer!.WriteLineAsync(text.AsMemory(), disposeToken);
|
||||
await writer.FlushAsync(disposeToken);
|
||||
}
|
||||
else
|
||||
await foreach (var text in enumerable)
|
||||
stringBuilder!.AppendLine(text);
|
||||
}
|
||||
|
||||
logger.LogTrace("Finished read for PID {pid}", pid);
|
||||
}
|
||||
catch (OperationCanceledException ex)
|
||||
|
||||
var pid = await startupAndPid;
|
||||
logger.LogTrace("Starting read for PID {pid}...", pid);
|
||||
|
||||
using (cancellationToken.Register(() => dataChannel.Writer.TryComplete()))
|
||||
{
|
||||
logger.LogWarning(ex, "PID {pid} stream reading interrupted!", pid);
|
||||
if (fileStream != null)
|
||||
await writer!.WriteLineAsync("-- Process detached, log truncated. This is likely due a to TGS restart --");
|
||||
handle.BeginOutputReadLine();
|
||||
using (cancellationToken.Register(handle.CancelOutputRead))
|
||||
{
|
||||
handle.BeginErrorReadLine();
|
||||
using (cancellationToken.Register(handle.CancelErrorRead))
|
||||
{
|
||||
try
|
||||
{
|
||||
await OutputWriter();
|
||||
|
||||
logger.LogTrace("Finished read for PID {pid}", pid);
|
||||
}
|
||||
catch (OperationCanceledException ex)
|
||||
{
|
||||
logger.LogWarning(ex, "PID {pid} stream reading interrupted!", pid);
|
||||
if (writingToFile)
|
||||
await fileWriter!.WriteLineAsync("-- Process detached, log truncated. This is likely due a to TGS restart --");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return stringBuilder?.ToString();
|
||||
@@ -312,7 +332,6 @@ namespace Tgstation.Server.Host.System
|
||||
var pid = handle.Id;
|
||||
return new Process(
|
||||
processFeatures,
|
||||
asyncDelayer,
|
||||
handle,
|
||||
null,
|
||||
null,
|
||||
|
||||
@@ -64,7 +64,7 @@
|
||||
<!-- Usage: Concise throw statements for native Win32 errors -->
|
||||
<PackageReference Include="BetterWin32Errors" Version="0.2.0" />
|
||||
<!-- Usage: Interop with BYOND's /world/Topic -->
|
||||
<PackageReference Include="Byond.TopicSender" Version="7.0.8" />
|
||||
<PackageReference Include="Byond.TopicSender" Version="8.0.0" />
|
||||
<!-- Usage: 503'ing request pipeline until server is finished initializing -->
|
||||
<PackageReference Include="Cyberboss.AspNetCore.AsyncInitializer" Version="1.2.0" />
|
||||
<!-- Usage: IRC interop -->
|
||||
@@ -74,7 +74,7 @@
|
||||
<!-- Usage: GitLab interop -->
|
||||
<PackageReference Include="GitLabApiClient" Version="1.8.0" />
|
||||
<!-- Usage: git interop -->
|
||||
<PackageReference Include="LibGit2Sharp" Version="0.28.0" />
|
||||
<PackageReference Include="LibGit2Sharp" Version="0.29.0" />
|
||||
<!-- Usage: JWT injection into HTTP pipeline -->
|
||||
<PackageReference Include="Microsoft.AspNetCore.Authentication.JwtBearer" Version="8.0.0" />
|
||||
<!-- Usage: Support ""legacy"" Newotonsoft.Json in HTTP pipeline. The rest of our codebase uses Newtonsoft. -->
|
||||
@@ -96,7 +96,7 @@
|
||||
<!-- Usage: YAML config plugin -->
|
||||
<PackageReference Include="NetEscapades.Configuration.Yaml" Version="3.1.0" />
|
||||
<!-- Usage: PostgresSQL ORM plugin -->
|
||||
<PackageReference Include="Npgsql.EntityFrameworkCore.PostgreSQL" Version="8.0.0-rc.2" />
|
||||
<PackageReference Include="Npgsql.EntityFrameworkCore.PostgreSQL" Version="8.0.0" />
|
||||
<!-- Usage: GitHub.com interop -->
|
||||
<PackageReference Include="Octokit" Version="9.0.0" />
|
||||
<!-- Usage: MYSQL/MariaDB ORM plugin -->
|
||||
@@ -108,7 +108,7 @@
|
||||
<!-- Usage: Async logging plugin -->
|
||||
<PackageReference Include="Serilog.Sinks.Async" Version="1.5.0" />
|
||||
<!-- Usage: Console logging plugin -->
|
||||
<PackageReference Include="Serilog.Sinks.Console" Version="5.0.0" />
|
||||
<PackageReference Include="Serilog.Sinks.Console" Version="5.0.1" />
|
||||
<!-- Usage: Elasticsearch logging plugin -->
|
||||
<PackageReference Include="Serilog.Sinks.Elasticsearch" Version="9.0.3" />
|
||||
<!-- Usage: File logging plugin -->
|
||||
@@ -126,7 +126,7 @@
|
||||
<!-- Usage: Temporary resolution to compatibility issues with EFCore 7 and .NET 8 -->
|
||||
<PackageReference Include="System.Security.Permissions" Version="8.0.0" />
|
||||
<!-- Usage: .DeleteAsync() support for IQueryable<T>s -->
|
||||
<PackageReference Include="Z.EntityFramework.Plus.EFCore" Version="8.101.0" />
|
||||
<PackageReference Include="Z.EntityFramework.Plus.EFCore" Version="8.101.1.2" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
@{
|
||||
var svgPath = Model.Svg;
|
||||
var title = Model.Title;
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
@@ -13,7 +12,7 @@
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<img src="@svgPath"/>
|
||||
<img src="logo.svg"/>
|
||||
@{
|
||||
if (Model.Links != null)
|
||||
foreach (KeyValuePair<string, string> kvp in Model.Links)
|
||||
|
||||
@@ -18,6 +18,7 @@ General:
|
||||
DeploymentDirectoryCopyTasksPerCore: 100 # Maximum number of concurrent file copy operations PER available CPU core
|
||||
OpenDreamGitUrl: https://github.com/OpenDreamProject/OpenDream # The repository to retrieve OpenDream from
|
||||
OpenDreamGitTagPrefix: v # The prefix to the OpenDream semver as tags appear in the git repository
|
||||
OpenDreamSuppressInstallOutput: false # Suppress the dotnet output of creating an OpenDream installation. Known to cause hangs in CI.
|
||||
Session:
|
||||
HighPriorityLiveDreamDaemon: false # If DreamDaemon instances should run as higher priority processes
|
||||
LowPriorityDeploymentProcesses: true # If TGS Deployments should run as lower priority processes
|
||||
|
||||
@@ -6,9 +6,11 @@
|
||||
log << "Initial value of sleep_offline: [sleep_offline]"
|
||||
sleep_offline = FALSE
|
||||
|
||||
// Intentionally slow down startup for testing purposes
|
||||
for(var/i in 1 to 10000000)
|
||||
dab()
|
||||
if(params["slow_start"])
|
||||
// Intentionally slow down startup for health check testing purposes
|
||||
for(var/i in 1 to 10000000)
|
||||
dab()
|
||||
|
||||
TgsNew(new /datum/tgs_event_handler/impl, TGS_SECURITY_SAFE)
|
||||
|
||||
var/sec = TgsSecurityLevel()
|
||||
@@ -189,7 +191,7 @@ var/run_bridge_test
|
||||
var/its_sad = data["im_out_of_memes"]
|
||||
if(its_sad)
|
||||
TestLegacyBridge()
|
||||
return "yeah gimmie a sec"
|
||||
return "all gucci"
|
||||
|
||||
TgsChatBroadcast(new /datum/tgs_message_content("Received non-tgs topic: `[T]`"))
|
||||
|
||||
@@ -252,9 +254,9 @@ var/received_health_check = FALSE
|
||||
|
||||
/proc/RebootAsync()
|
||||
set waitfor = FALSE
|
||||
world.TgsChatBroadcast(new /datum/tgs_message_content("Rebooting after 3 seconds"));
|
||||
world.TgsChatBroadcast(new /datum/tgs_message_content("Rebooting after 1 seconds"));
|
||||
world.log << "About to sleep. sleep_offline: [world.sleep_offline]"
|
||||
sleep(30)
|
||||
sleep(10)
|
||||
world.log << "Done sleep, calling Reboot"
|
||||
world.Reboot()
|
||||
|
||||
@@ -362,10 +364,6 @@ var/suppress_bridge_spam = FALSE
|
||||
api.access_identifier = old_ai
|
||||
|
||||
/proc/TestLegacyBridge()
|
||||
set waitfor = FALSE
|
||||
|
||||
sleep(10)
|
||||
|
||||
var/datum/tgs_api/v5/api = TGS_READ_GLOBAL(tgs)
|
||||
if(api.interop_version.suite != 5)
|
||||
FailTest("Legacy bridge test not required anymore?")
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
using LibGit2Sharp;
|
||||
using LibGit2Sharp;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Microsoft.VisualStudio.TestTools.UnitTesting;
|
||||
using Moq;
|
||||
@@ -39,12 +39,11 @@ namespace Tgstation.Server.Host.Components.Repository.Tests
|
||||
try
|
||||
{
|
||||
var factory = CreateFactory();
|
||||
var cloneOpts = new CloneOptions();
|
||||
cloneOpts.FetchOptions.CredentialsProvider = factory.GenerateCredentialsHandler(null, null);
|
||||
await factory.Clone(
|
||||
new Uri("https://github.com/Cyberboss/Test"),
|
||||
new CloneOptions
|
||||
{
|
||||
CredentialsProvider = factory.GenerateCredentialsHandler(null, null)
|
||||
},
|
||||
cloneOpts,
|
||||
tempDir,
|
||||
default);
|
||||
|
||||
|
||||
@@ -54,13 +54,12 @@ namespace Tgstation.Server.Host.System.Tests
|
||||
builder.SetMinimumLevel(LogLevel.Trace);
|
||||
});
|
||||
|
||||
IProcessExecutor processExecutor = null;
|
||||
ProcessExecutor processExecutor = null;
|
||||
processExecutor = new ProcessExecutor(
|
||||
new PosixProcessFeatures(
|
||||
new Lazy<IProcessExecutor>(() => processExecutor),
|
||||
new DefaultIOManager(),
|
||||
loggerFactory.CreateLogger<PosixProcessFeatures>()),
|
||||
new AsyncDelayer(),
|
||||
Mock.Of<IIOManager>(),
|
||||
loggerFactory.CreateLogger<ProcessExecutor>(),
|
||||
loggerFactory);
|
||||
|
||||
@@ -94,7 +94,9 @@ namespace Tgstation.Server.Tests
|
||||
// actions is supposed to cache BYOND for us
|
||||
|
||||
var dir = Path.Combine(
|
||||
Environment.GetFolderPath(Environment.SpecialFolder.UserProfile),
|
||||
Environment.GetFolderPath(
|
||||
Environment.SpecialFolder.UserProfile,
|
||||
Environment.SpecialFolderOption.DoNotVerify),
|
||||
"byond-zips-cache",
|
||||
windows ? "windows" : "linux");
|
||||
path = Path.Combine(
|
||||
|
||||
@@ -44,6 +44,8 @@ namespace Tgstation.Server.Tests.Live
|
||||
|
||||
ulong channelIdAllocator;
|
||||
|
||||
public static Task MessageGuard = Task.CompletedTask;
|
||||
|
||||
static IAsyncDelayer CreateMockDelayer()
|
||||
{
|
||||
// at time of writing, this is used exclusively for the reconnection interval which works in minutes
|
||||
@@ -219,6 +221,8 @@ namespace Tgstation.Server.Tests.Live
|
||||
var delay = random.Next(0, 10000);
|
||||
await Task.Delay(delay, cancellationToken);
|
||||
|
||||
await MessageGuard;
|
||||
|
||||
// %5 chance to disconnect randomly
|
||||
if (enableRandomDisconnections != 0 && random.Next(0, 100) > 95)
|
||||
connected = false;
|
||||
|
||||
@@ -3,6 +3,7 @@ using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
using Microsoft.VisualStudio.TestTools.UnitTesting;
|
||||
|
||||
using Tgstation.Server.Api.Models;
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
using System;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
using Microsoft.VisualStudio.TestTools.UnitTesting;
|
||||
|
||||
using Tgstation.Server.Api.Models;
|
||||
@@ -138,10 +139,10 @@ namespace Tgstation.Server.Tests.Live.Instance
|
||||
|
||||
var updatedDD = await dreamDaemonClient.Update(new DreamDaemonRequest
|
||||
{
|
||||
StartupTimeout = 15,
|
||||
StartupTimeout = 30,
|
||||
Port = ddPort
|
||||
}, cancellationToken);
|
||||
Assert.AreEqual(15U, updatedDD.StartupTimeout);
|
||||
Assert.AreEqual(30U, updatedDD.StartupTimeout);
|
||||
Assert.AreEqual(ddPort, updatedDD.Port);
|
||||
|
||||
async Task<JobResponse> CompileAfterByondInstall()
|
||||
|
||||
@@ -140,7 +140,7 @@ namespace Tgstation.Server.Tests.Live.Instance
|
||||
case EngineType.Byond:
|
||||
return 30;
|
||||
case EngineType.OpenDream:
|
||||
return 300;
|
||||
return 500;
|
||||
default:
|
||||
throw new InvalidOperationException($"Unknown engine type: {testVersion.Engine.Value}");
|
||||
}
|
||||
|
||||
@@ -45,11 +45,11 @@ namespace Tgstation.Server.Tests.Live.Instance
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
var testVersion = await EngineTest.GetEdgeVersion(EngineType.Byond, fileDownloader, cancellationToken);
|
||||
var engineTest = new EngineTest(instanceClient.Engine, instanceClient.Jobs, fileDownloader, instanceClient.Metadata, testVersion.Engine.Value);
|
||||
var chatTest = new ChatTest(instanceClient.ChatBots, instanceManagerClient, instanceClient.Jobs, instanceClient.Metadata);
|
||||
await using var engineTest = new EngineTest(instanceClient.Engine, instanceClient.Jobs, fileDownloader, instanceClient.Metadata, testVersion.Engine.Value);
|
||||
await using var chatTest = new ChatTest(instanceClient.ChatBots, instanceManagerClient, instanceClient.Jobs, instanceClient.Metadata);
|
||||
var configTest = new ConfigurationTest(instanceClient.Configuration, instanceClient.Metadata);
|
||||
var repoTest = new RepositoryTest(instanceClient.Repository, instanceClient.Jobs);
|
||||
var dmTest = new DeploymentTest(instanceClient, instanceClient.Jobs, dmPort, ddPort, lowPrioDeployment, testVersion.Engine.Value);
|
||||
await using var repoTest = new RepositoryTest(instanceClient.Repository, instanceClient.Jobs);
|
||||
await using var dmTest = new DeploymentTest(instanceClient, instanceClient.Jobs, dmPort, ddPort, lowPrioDeployment, testVersion.Engine.Value);
|
||||
|
||||
var byondTask = engineTest.Run(cancellationToken, out var firstInstall);
|
||||
var chatTask = chatTest.RunPreWatchdog(cancellationToken);
|
||||
@@ -66,15 +66,15 @@ namespace Tgstation.Server.Tests.Live.Instance
|
||||
await configTest.SetupDMApiTests(true, cancellationToken);
|
||||
await byondTask;
|
||||
|
||||
await new WatchdogTest(
|
||||
await using var wdt = new WatchdogTest(
|
||||
testVersion,
|
||||
instanceClient,
|
||||
instanceManager,
|
||||
serverPort,
|
||||
highPrioDD,
|
||||
ddPort,
|
||||
usingBasicWatchdog)
|
||||
.Run(cancellationToken);
|
||||
usingBasicWatchdog);
|
||||
await wdt.Run(cancellationToken);
|
||||
}
|
||||
|
||||
public static async ValueTask<IEngineInstallationData> DownloadEngineVersion(
|
||||
@@ -85,7 +85,9 @@ namespace Tgstation.Server.Tests.Live.Instance
|
||||
{
|
||||
var ioManager = new DefaultIOManager();
|
||||
var odRepoDir = ioManager.ConcatPath(
|
||||
Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData),
|
||||
Environment.GetFolderPath(
|
||||
Environment.SpecialFolder.LocalApplicationData,
|
||||
Environment.SpecialFolderOption.DoNotVerify),
|
||||
new AssemblyInformationProvider().VersionPrefix,
|
||||
"OpenDreamRepository");
|
||||
var odRepoIoManager = new ResolvingIOManager(ioManager, odRepoDir);
|
||||
@@ -197,7 +199,7 @@ namespace Tgstation.Server.Tests.Live.Instance
|
||||
ReconnectionInterval = 1,
|
||||
}, cancellationToken);
|
||||
|
||||
var jrt = new JobsRequiredTest(instanceClient.Jobs);
|
||||
await using var jrt = new JobsRequiredTest(instanceClient.Jobs);
|
||||
|
||||
EngineInstallResponse installJob2;
|
||||
await using (var stableBytesMs = await TestingUtils.ExtractMemoryStreamFromInstallationData(
|
||||
@@ -219,7 +221,6 @@ namespace Tgstation.Server.Tests.Live.Instance
|
||||
await chatRequest;
|
||||
await Task.Yield();
|
||||
|
||||
|
||||
await Task.WhenAll(
|
||||
jrt.WaitForJob(installJob2.InstallJob, EngineTest.EngineInstallationTimeout(compatVersion) + 30, false, null, cancellationToken),
|
||||
jrt.WaitForJob(cloneRequest.Result.ActiveJob, 60, false, null, cancellationToken),
|
||||
@@ -257,7 +258,8 @@ namespace Tgstation.Server.Tests.Live.Instance
|
||||
|
||||
await configSetupTask;
|
||||
|
||||
await new WatchdogTest(compatVersion, instanceClient, instanceManager, serverPort, highPrioDD, ddPort, usingBasicWatchdog).Run(cancellationToken);
|
||||
await using var wdt = new WatchdogTest(compatVersion, instanceClient, instanceManager, serverPort, highPrioDD, ddPort, usingBasicWatchdog);
|
||||
await wdt.Run(cancellationToken);
|
||||
|
||||
await instanceManagerClient.Update(new InstanceUpdateRequest
|
||||
{
|
||||
|
||||
@@ -8,6 +8,7 @@ using System.Threading.Tasks;
|
||||
using Microsoft.AspNetCore.SignalR.Client;
|
||||
using Microsoft.VisualStudio.TestTools.UnitTesting;
|
||||
|
||||
using Tgstation.Server.Api.Extensions;
|
||||
using Tgstation.Server.Api.Hubs;
|
||||
using Tgstation.Server.Api.Models;
|
||||
using Tgstation.Server.Api.Models.Request;
|
||||
@@ -66,7 +67,7 @@ namespace Tgstation.Server.Tests.Live.Instance
|
||||
}
|
||||
catch(Exception ex)
|
||||
{
|
||||
finishTcs.SetException(ex);
|
||||
finishTcs.TrySetException(ex);
|
||||
}
|
||||
|
||||
return Task.CompletedTask;
|
||||
@@ -84,7 +85,7 @@ namespace Tgstation.Server.Tests.Live.Instance
|
||||
}
|
||||
}
|
||||
|
||||
public async Task Run(CancellationToken cancellationToken)
|
||||
public async Task<Task> Run(CancellationToken cancellationToken)
|
||||
{
|
||||
var neverReceiver = new ShouldNeverReceiveUpdates()
|
||||
{
|
||||
@@ -98,16 +99,33 @@ namespace Tgstation.Server.Tests.Live.Instance
|
||||
},
|
||||
};
|
||||
|
||||
await using (permedConn = (HubConnection)await permedUser.SubscribeToJobUpdates(
|
||||
permedConn = (HubConnection)await permedUser.SubscribeToJobUpdates(
|
||||
this,
|
||||
null,
|
||||
null,
|
||||
cancellationToken))
|
||||
await using (permlessConn = (HubConnection)await permlessUser.SubscribeToJobUpdates(
|
||||
neverReceiver,
|
||||
null,
|
||||
null,
|
||||
cancellationToken))
|
||||
cancellationToken);
|
||||
|
||||
try
|
||||
{
|
||||
permlessConn = (HubConnection)await permlessUser.SubscribeToJobUpdates(
|
||||
neverReceiver,
|
||||
null,
|
||||
null,
|
||||
cancellationToken);
|
||||
}
|
||||
catch
|
||||
{
|
||||
await permedConn.DisposeAsync();
|
||||
throw;
|
||||
}
|
||||
|
||||
return FinishAsync(cancellationToken);
|
||||
}
|
||||
|
||||
async Task FinishAsync(CancellationToken cancellationToken)
|
||||
{
|
||||
await using (permedConn)
|
||||
await using (permlessConn)
|
||||
{
|
||||
Console.WriteLine($"Initial conn1: {permedConn.ConnectionId}");
|
||||
Console.WriteLine($"Initial conn2: {permlessConn.ConnectionId}");
|
||||
@@ -138,7 +156,10 @@ namespace Tgstation.Server.Tests.Live.Instance
|
||||
Online = true,
|
||||
}, cancellationToken);
|
||||
|
||||
var jobs = await permedUser.Instances.CreateClient(instance).Jobs.List(null, cancellationToken);
|
||||
var jobs = await permedUser.Instances.CreateClient(instance).Jobs.List(new PaginationSettings
|
||||
{
|
||||
PageSize = 100
|
||||
}, cancellationToken);
|
||||
if (wasOffline)
|
||||
await permedUser.Instances.Update(new InstanceUpdateRequest
|
||||
{
|
||||
@@ -153,6 +174,13 @@ namespace Tgstation.Server.Tests.Live.Instance
|
||||
.Select(CheckInstance);
|
||||
|
||||
var allJobs = (await ValueTaskExtensions.WhenAll(allJobsTask, allInstances.Count)).SelectMany(x => x).ToList();
|
||||
|
||||
var groups = allJobs.GroupBy(x => x.Id.Value).ToList();
|
||||
var uniqueAllJobs = groups.Select(x => x.First()).ToList();
|
||||
|
||||
static string JobListFormatter(IEnumerable<JobResponse> jobs) => String.Join(Environment.NewLine, jobs.Select(x => $"- I:{x.InstanceId}|JID:{x.Id}|JC:{x.JobCode}|Desc:{x.Description}"));
|
||||
Assert.AreEqual(allJobs.Count, uniqueAllJobs.Count, $"Duplicated Jobs:{Environment.NewLine}{JobListFormatter(groups.Where(x => x.Count() > 1).SelectMany(x => x))}");
|
||||
|
||||
var missableMissedJobs = 0;
|
||||
foreach (var job in allJobs)
|
||||
{
|
||||
@@ -185,18 +213,24 @@ namespace Tgstation.Server.Tests.Live.Instance
|
||||
}
|
||||
else
|
||||
{
|
||||
var wasMissableJob = job.JobCode == JobCode.ReconnectChatBot
|
||||
|| job.JobCode == JobCode.StartupWatchdogLaunch
|
||||
|| job.JobCode == JobCode.StartupWatchdogReattach;
|
||||
Assert.IsTrue(wasMissableJob);
|
||||
var wasMissableJob = job.JobCode.Value.IsServerStartupJob();
|
||||
Assert.IsTrue(wasMissableJob, $"Found unexpected missed job: #{job.Id.Value} - {job.JobCode} - {job.Description}");
|
||||
++missableMissedJobs;
|
||||
}
|
||||
}
|
||||
|
||||
var jobsSeenByHubButNotInAllJobs = seenJobs.Values.Where(x => !allJobs.Any(y => y.Id.Value == x.Id.Value)).ToList();
|
||||
|
||||
// some instances may be detached, but our cache remains
|
||||
var accountedJobs = allJobs.Count - missableMissedJobs;
|
||||
var accountedSeenJobs = seenJobs.Where(x => allInstances.Any(i => i.Id.Value == x.Value.InstanceId)).ToList();
|
||||
Assert.AreEqual(accountedJobs, accountedSeenJobs.Count, $"Mismatch in seen jobs:{Environment.NewLine}{String.Join(Environment.NewLine, allJobs.Where(x => !seenJobs.Any(y => y.Key == x.Id.Value)).Select(x => $"- I:{x.InstanceId}|JID:{x.Id}|JC:{x.JobCode}|Desc:{x.Description}"))}");
|
||||
var errorMessage = $"Mismatch in seen jobs:{Environment.NewLine}Not seen in seen:{Environment.NewLine}{JobListFormatter(allJobs.Where(x => !seenJobs.Any(y => y.Key == x.Id.Value)))}{Environment.NewLine}Seen not in all:{Environment.NewLine}{JobListFormatter(jobsSeenByHubButNotInAllJobs)}{Environment.NewLine}Current Instances: {String.Join(", ", allInstances.Select(i => i.Id.Value))}";
|
||||
Assert.AreEqual(
|
||||
accountedJobs,
|
||||
seenJobs.Count - jobsSeenByHubButNotInAllJobs.Count,
|
||||
errorMessage);
|
||||
Assert.IsTrue(
|
||||
jobsSeenByHubButNotInAllJobs.All(job => job.JobCode.Value == JobCode.Move),
|
||||
errorMessage);
|
||||
Assert.IsTrue(accountedJobs <= seenJobs.Count);
|
||||
Assert.AreNotEqual(0, permlessSeenJobs.Count);
|
||||
Assert.IsTrue(permlessSeenJobs.Count < seenJobs.Count);
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
using System;
|
||||
using System.Collections.Concurrent;
|
||||
using System.Reflection;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
@@ -13,16 +14,72 @@ using Tgstation.Server.Client.Components;
|
||||
|
||||
namespace Tgstation.Server.Tests.Live.Instance
|
||||
{
|
||||
class JobsRequiredTest
|
||||
class JobsRequiredTest : IAsyncDisposable
|
||||
{
|
||||
protected IJobsClient JobsClient { get; }
|
||||
|
||||
readonly IApiClient apiClient;
|
||||
|
||||
IAsyncDisposable hubConnection;
|
||||
readonly Task hubConnectionTask;
|
||||
readonly CancellationTokenSource cancellationTokenSource;
|
||||
|
||||
readonly ConcurrentDictionary<long, TaskCompletionSource<JobResponse>> registry;
|
||||
|
||||
public JobsRequiredTest(IJobsClient jobsClient)
|
||||
{
|
||||
JobsClient = jobsClient ?? throw new ArgumentNullException(nameof(jobsClient));
|
||||
apiClient = (IApiClient)jobsClient.GetType().GetProperty("ApiClient", BindingFlags.NonPublic | BindingFlags.Instance).GetValue(jobsClient);
|
||||
registry = new ConcurrentDictionary<long, TaskCompletionSource<JobResponse>>();
|
||||
|
||||
cancellationTokenSource = new CancellationTokenSource();
|
||||
hubConnectionTask = CreateHubConnection();
|
||||
}
|
||||
|
||||
async Task CreateHubConnection()
|
||||
{
|
||||
var receiver = new JobReceiver
|
||||
{
|
||||
Callback = job => Register(job),
|
||||
};
|
||||
|
||||
hubConnection = await apiClient.CreateHubConnection<IJobsHub>(receiver, null, null, cancellationTokenSource.Token);
|
||||
}
|
||||
|
||||
public async ValueTask DisposeAsync()
|
||||
{
|
||||
cancellationTokenSource.Cancel();
|
||||
cancellationTokenSource.Dispose();
|
||||
try
|
||||
{
|
||||
await hubConnectionTask;
|
||||
}
|
||||
catch (OperationCanceledException)
|
||||
{
|
||||
}
|
||||
|
||||
if (hubConnection != null)
|
||||
await hubConnection.DisposeAsync();
|
||||
}
|
||||
|
||||
Task<JobResponse> Register(JobResponse updatedJob)
|
||||
{
|
||||
var tcs = registry.AddOrUpdate(updatedJob.Id.Value,
|
||||
_ =>
|
||||
{
|
||||
var tcs = new TaskCompletionSource<JobResponse>();
|
||||
if (updatedJob.StoppedAt.HasValue)
|
||||
tcs.SetResult(updatedJob);
|
||||
return tcs;
|
||||
},
|
||||
(_, oldTcs) =>
|
||||
{
|
||||
if (updatedJob.StoppedAt.HasValue)
|
||||
oldTcs.TrySetResult(updatedJob);
|
||||
return oldTcs;
|
||||
});
|
||||
|
||||
return tcs.Task;
|
||||
}
|
||||
|
||||
class JobReceiver : IJobsHub
|
||||
@@ -42,47 +99,21 @@ namespace Tgstation.Server.Tests.Live.Instance
|
||||
Assert.IsNotNull(originalJob.JobCode);
|
||||
|
||||
var job = originalJob;
|
||||
var registryTask = Register(job);
|
||||
await Task.WhenAny(
|
||||
registryTask,
|
||||
Task.Delay(TimeSpan.FromSeconds(timeout), cancellationToken));
|
||||
|
||||
if (!registryTask.IsCompleted)
|
||||
// one last get in case SignalR dropped the ball
|
||||
job = await JobsClient.GetId(job, cancellationToken);
|
||||
else
|
||||
job = await registryTask;
|
||||
|
||||
if (!job.StoppedAt.HasValue)
|
||||
{
|
||||
var tcs = new TaskCompletionSource();
|
||||
var receiver = new JobReceiver
|
||||
{
|
||||
Callback = updatedJob =>
|
||||
{
|
||||
if (updatedJob.Id != job.Id)
|
||||
return;
|
||||
|
||||
job = updatedJob;
|
||||
if (updatedJob.StoppedAt.HasValue)
|
||||
tcs.TrySetResult();
|
||||
},
|
||||
};
|
||||
|
||||
JobResponse firstCheck;
|
||||
await using (var hubConnection = await apiClient.CreateHubConnection<IJobsHub>(receiver, null, null, cancellationToken))
|
||||
{
|
||||
// initial GET after connecting
|
||||
firstCheck = await JobsClient.GetId(job, cancellationToken);
|
||||
if (!firstCheck.StoppedAt.HasValue)
|
||||
{
|
||||
firstCheck = null;
|
||||
await Task.WhenAny(
|
||||
tcs.Task,
|
||||
Task.Delay(TimeSpan.FromSeconds(timeout), cancellationToken));
|
||||
}
|
||||
}
|
||||
|
||||
if (firstCheck != null)
|
||||
job = firstCheck;
|
||||
else if (!job.StoppedAt.HasValue)
|
||||
// one last get in case SignalR dropped the ball
|
||||
job = await JobsClient.GetId(job, cancellationToken);
|
||||
|
||||
if (!job.StoppedAt.HasValue)
|
||||
{
|
||||
await JobsClient.Cancel(job, cancellationToken);
|
||||
Assert.Fail($"Job ID {job.Id} \"{job.Description}\" timed out!");
|
||||
}
|
||||
await JobsClient.Cancel(job, cancellationToken);
|
||||
Assert.Fail($"Job ID {job.Id} \"{job.Description}\" timed out!");
|
||||
}
|
||||
|
||||
if (expectFailure.HasValue && expectFailure.Value ^ job.ExceptionDetails != null)
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
using Byond.TopicSender;
|
||||
|
||||
using Microsoft.AspNetCore.DataProtection;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Microsoft.VisualStudio.TestTools.UnitTesting;
|
||||
|
||||
@@ -19,6 +18,7 @@ using System.Linq;
|
||||
using System.Net;
|
||||
using System.Net.Sockets;
|
||||
using System.Reflection;
|
||||
using System.Runtime.CompilerServices;
|
||||
using System.Runtime.InteropServices;
|
||||
using System.Text;
|
||||
using System.Threading;
|
||||
@@ -68,6 +68,7 @@ namespace Tgstation.Server.Tests.Live.Instance
|
||||
readonly bool watchdogRestartsProcess;
|
||||
|
||||
bool ranTimeoutTest = false;
|
||||
const string BaseAdditionalParameters = "expect_chat_channels=1&expect_static_files=1";
|
||||
|
||||
public WatchdogTest(EngineVersion testVersion, IInstanceClient instanceClient, InstanceManager instanceManager, ushort serverPort, bool highPrioDD, ushort ddPort, bool watchdogRestartsProcess)
|
||||
: base(instanceClient.Jobs)
|
||||
@@ -88,8 +89,20 @@ namespace Tgstation.Server.Tests.Live.Instance
|
||||
DisconnectTimeout = TimeSpan.FromSeconds(30)
|
||||
}, loggerFactory.CreateLogger($"WatchdogTest.TopicClient.{instanceClient.Metadata.Name}"));
|
||||
}
|
||||
|
||||
public async Task Run(CancellationToken cancellationToken)
|
||||
{
|
||||
try
|
||||
{
|
||||
await RunInt(cancellationToken);
|
||||
}
|
||||
catch
|
||||
{
|
||||
System.Console.WriteLine($"WATCHDOG TEST FAILING INSTANCE ID {instanceClient.Metadata.Id.Value}");
|
||||
throw;
|
||||
}
|
||||
}
|
||||
|
||||
async Task RunInt(CancellationToken cancellationToken)
|
||||
{
|
||||
System.Console.WriteLine($"TEST: START WATCHDOG TESTS {instanceClient.Metadata.Name}");
|
||||
|
||||
@@ -120,12 +133,12 @@ namespace Tgstation.Server.Tests.Live.Instance
|
||||
// Increase startup timeout, disable heartbeats, enable map threads because we've tested without for years
|
||||
instanceClient.DreamDaemon.Update(new DreamDaemonRequest
|
||||
{
|
||||
StartupTimeout = 15,
|
||||
StartupTimeout = 30,
|
||||
HealthCheckSeconds = 0,
|
||||
Port = ddPort,
|
||||
MapThreads = 2,
|
||||
LogOutput = false,
|
||||
AdditionalParameters = "expect_chat_channels=1&expect_static_files=1"
|
||||
AdditionalParameters = BaseAdditionalParameters
|
||||
}, cancellationToken).AsTask(),
|
||||
CheckByondVersions(),
|
||||
ApiAssert.ThrowsException<ApiConflictException, DreamDaemonResponse>(() => instanceClient.DreamDaemon.Update(new DreamDaemonRequest
|
||||
@@ -152,9 +165,21 @@ namespace Tgstation.Server.Tests.Live.Instance
|
||||
await RunLongRunningTestThenUpdateWithNewDme(cancellationToken);
|
||||
await RunLongRunningTestThenUpdateWithByondVersionSwitch(cancellationToken);
|
||||
|
||||
// no chatty bullshit while we test health checks
|
||||
var tcs = new TaskCompletionSource();
|
||||
var oldTask = Interlocked.Exchange(ref DummyChatProvider.MessageGuard, tcs.Task);
|
||||
|
||||
await RunHealthCheckTest(true, cancellationToken);
|
||||
await RunHealthCheckTest(false, cancellationToken);
|
||||
|
||||
async void Cleanup()
|
||||
{
|
||||
await oldTask;
|
||||
tcs.SetResult();
|
||||
}
|
||||
|
||||
Cleanup();
|
||||
|
||||
await InteropTestsForLongRunningDme(cancellationToken);
|
||||
|
||||
await instanceClient.DreamDaemon.Update(new DreamDaemonRequest
|
||||
@@ -176,7 +201,6 @@ namespace Tgstation.Server.Tests.Live.Instance
|
||||
var ddUpdateTask = instanceClient.DreamDaemon.Update(new DreamDaemonRequest
|
||||
{
|
||||
SecurityLevel = useTrusted ? DreamDaemonSecurity.Trusted : DreamDaemonSecurity.Safe,
|
||||
AdditionalParameters = "expect_chat_channels=1&expect_static_files=1",
|
||||
}, cancellationToken);
|
||||
var currentStatus = await DeployTestDme("long_running_test_rooted", DreamDaemonSecurity.Trusted, true, cancellationToken);
|
||||
await ddUpdateTask;
|
||||
@@ -197,7 +221,9 @@ namespace Tgstation.Server.Tests.Live.Instance
|
||||
|
||||
// reimplement TellWorldToReboot because it expects a new deployment and we don't care
|
||||
System.Console.WriteLine("TEST: Hack world reboot topic...");
|
||||
var result = await topicClient.SendTopic(IPAddress.Loopback, "tgs_integration_test_special_tactics=1", FindTopicPort(), cancellationToken);
|
||||
var result = await SendTestTopic(
|
||||
"tgs_integration_test_special_tactics=1",
|
||||
cancellationToken);
|
||||
Assert.AreEqual("ack", result.StringData);
|
||||
|
||||
using var tempCts = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken);
|
||||
@@ -223,13 +249,38 @@ namespace Tgstation.Server.Tests.Live.Instance
|
||||
await RunTest(false);
|
||||
}
|
||||
|
||||
ValueTask<TopicResponse> SendTestTopic(string queryString, CancellationToken cancellationToken)
|
||||
=> SendTestTopic(queryString, topicClient, instanceManager.GetInstanceReference(instanceClient.Metadata), FindTopicPort(), cancellationToken);
|
||||
|
||||
public static async ValueTask<TopicResponse> SendTestTopic(string queryString, ITopicClient topicClient, IInstanceReference instanceReference, ushort topicPort, CancellationToken cancellationToken)
|
||||
{
|
||||
using (instanceReference)
|
||||
{
|
||||
using var loggerFactory = LoggerFactory.Create(builder =>
|
||||
{
|
||||
builder.AddConsole();
|
||||
builder.SetMinimumLevel(LogLevel.Trace);
|
||||
});
|
||||
|
||||
var watchdog = instanceReference?.Watchdog;
|
||||
var session = (SessionController)watchdog?.GetType().GetMethod("GetActiveController", BindingFlags.Instance | BindingFlags.NonPublic)?.Invoke(watchdog, null);
|
||||
|
||||
using (session != null
|
||||
? await session.TopicSendSemaphore.Lock(cancellationToken)
|
||||
: null)
|
||||
return await topicClient.SendWithOptionalPriority(
|
||||
new AsyncDelayer(),
|
||||
loggerFactory.CreateLogger<WatchdogTest>(),
|
||||
queryString,
|
||||
topicPort,
|
||||
true,
|
||||
cancellationToken);
|
||||
}
|
||||
}
|
||||
|
||||
async ValueTask BroadcastTest(CancellationToken cancellationToken)
|
||||
{
|
||||
var topicRequestResult = await topicClient.SendTopic(
|
||||
IPAddress.Loopback,
|
||||
$"tgs_integration_test_tactics_broadcast=1",
|
||||
FindTopicPort(),
|
||||
cancellationToken);
|
||||
var topicRequestResult = await SendTestTopic("tgs_integration_test_tactics_broadcast=1", cancellationToken);
|
||||
|
||||
Assert.IsNotNull(topicRequestResult);
|
||||
Assert.AreEqual("!!NULL!!", topicRequestResult.StringData);
|
||||
@@ -240,10 +291,8 @@ namespace Tgstation.Server.Tests.Live.Instance
|
||||
BroadcastMessage = TestBroadcastMessage,
|
||||
}, cancellationToken);
|
||||
|
||||
topicRequestResult = await topicClient.SendTopic(
|
||||
IPAddress.Loopback,
|
||||
$"tgs_integration_test_tactics_broadcast=1",
|
||||
FindTopicPort(),
|
||||
topicRequestResult = await SendTestTopic(
|
||||
"tgs_integration_test_tactics_broadcast=1",
|
||||
cancellationToken);
|
||||
|
||||
Assert.IsNotNull(topicRequestResult);
|
||||
@@ -294,34 +343,34 @@ namespace Tgstation.Server.Tests.Live.Instance
|
||||
|
||||
async ValueTask RegressionTest1550(CancellationToken cancellationToken)
|
||||
{
|
||||
// Previous test, StartAndLeaveRunning, has SoftRestart set. We don't want that.
|
||||
var restartJob = await instanceClient.DreamDaemon.Restart(cancellationToken);
|
||||
await WaitForJob(restartJob, 10, false, null, cancellationToken);
|
||||
|
||||
// we need to cycle deployments twice because TGS holds the initial deployment
|
||||
var currentStatus = await DeployTestDme("LongRunning/long_running_test", DreamDaemonSecurity.Trusted, true, cancellationToken);
|
||||
|
||||
Assert.AreEqual(WatchdogStatus.Online, currentStatus.Status);
|
||||
Assert.IsNotNull(currentStatus.StagedCompileJob);
|
||||
ValidateSessionId(currentStatus, false);
|
||||
ValidateSessionId(currentStatus, true);
|
||||
var expectedStaged = currentStatus.StagedCompileJob;
|
||||
Assert.AreNotEqual(expectedStaged.Id, currentStatus.ActiveCompileJob.Id);
|
||||
|
||||
Assert.AreEqual(watchdogRestartsProcess, currentStatus.SoftRestart);
|
||||
Assert.IsFalse(currentStatus.SoftShutdown.Value);
|
||||
Assert.IsFalse(currentStatus.SoftShutdown);
|
||||
|
||||
currentStatus = await TellWorldToReboot(true, cancellationToken);
|
||||
ValidateSessionId(currentStatus, watchdogRestartsProcess);
|
||||
ValidateSessionId(currentStatus, true);
|
||||
Assert.AreEqual(expectedStaged.Id, currentStatus.ActiveCompileJob.Id);
|
||||
|
||||
await Task.Delay(TimeSpan.FromSeconds(3), cancellationToken);
|
||||
|
||||
var topicRequestResult = await topicClient.SendTopic(
|
||||
IPAddress.Loopback,
|
||||
$"shadow_wizard_money_gang=1",
|
||||
FindTopicPort(),
|
||||
var topicRequestResult = await SendTestTopic(
|
||||
"shadow_wizard_money_gang=1",
|
||||
cancellationToken);
|
||||
|
||||
Assert.IsNotNull(topicRequestResult);
|
||||
Assert.AreEqual("we love casting spells", topicRequestResult.StringData);
|
||||
|
||||
currentStatus = await DeployTestDme("LongRunning/long_running_test", DreamDaemonSecurity.Trusted, true, cancellationToken);
|
||||
Assert.AreEqual(watchdogRestartsProcess, currentStatus.SoftRestart);
|
||||
ValidateSessionId(currentStatus, false);
|
||||
|
||||
Assert.AreEqual(WatchdogStatus.Online, currentStatus.Status);
|
||||
@@ -443,10 +492,8 @@ namespace Tgstation.Server.Tests.Live.Instance
|
||||
async Task SendChatOverloadCommand(CancellationToken cancellationToken)
|
||||
{
|
||||
// for the code coverage really...
|
||||
var topicRequestResult = await topicClient.SendTopic(
|
||||
IPAddress.Loopback,
|
||||
$"tgs_integration_test_tactics5=1",
|
||||
FindTopicPort(),
|
||||
var topicRequestResult = await SendTestTopic(
|
||||
"tgs_integration_test_tactics5=1",
|
||||
cancellationToken);
|
||||
|
||||
Assert.IsNotNull(topicRequestResult);
|
||||
@@ -511,11 +558,8 @@ namespace Tgstation.Server.Tests.Live.Instance
|
||||
|
||||
Assert.IsTrue(job.ErrorCode == ErrorCode.GameServerOffline || job.ErrorCode == ErrorCode.GCoreFailure, $"{job.ErrorCode}: {job.ExceptionDetails}");
|
||||
|
||||
await Task.Delay(TimeSpan.FromSeconds(20), cancellationToken);
|
||||
|
||||
var ddStatus = await instanceClient.DreamDaemon.Read(cancellationToken);
|
||||
ValidateSessionId(ddStatus, true);
|
||||
Assert.AreEqual(WatchdogStatus.Online, ddStatus.Status.Value);
|
||||
var restartJob2 = await instanceClient.DreamDaemon.Restart(cancellationToken);
|
||||
await WaitForJob(restartJob2, 20, false, null, cancellationToken);
|
||||
}
|
||||
|
||||
async Task TestDMApiFreeDeploy(CancellationToken cancellationToken)
|
||||
@@ -537,7 +581,7 @@ namespace Tgstation.Server.Tests.Live.Instance
|
||||
|
||||
Assert.AreEqual(WatchdogStatus.Online, daemonStatus.Status.Value);
|
||||
ValidateSessionId(daemonStatus, true);
|
||||
CheckDDPriority();
|
||||
await CheckDDPriority();
|
||||
Assert.AreEqual(false, daemonStatus.SoftRestart);
|
||||
Assert.AreEqual(false, daemonStatus.SoftShutdown);
|
||||
Assert.AreEqual(string.Empty, daemonStatus.AdditionalParameters);
|
||||
@@ -608,11 +652,11 @@ namespace Tgstation.Server.Tests.Live.Instance
|
||||
|
||||
Assert.AreEqual(WatchdogStatus.Online, daemonStatus.Status.Value);
|
||||
ValidateSessionId(daemonStatus, true);
|
||||
CheckDDPriority();
|
||||
await CheckDDPriority();
|
||||
Assert.AreEqual(false, daemonStatus.SoftRestart);
|
||||
Assert.AreEqual(false, daemonStatus.SoftShutdown);
|
||||
|
||||
await GracefulWatchdogShutdown(60, cancellationToken);
|
||||
await GracefulWatchdogShutdown(cancellationToken);
|
||||
|
||||
daemonStatus = await instanceClient.DreamDaemon.Read(cancellationToken);
|
||||
Assert.AreEqual(WatchdogStatus.Offline, daemonStatus.Status.Value);
|
||||
@@ -662,7 +706,9 @@ namespace Tgstation.Server.Tests.Live.Instance
|
||||
var pid = proc.Id;
|
||||
var foundLivePath = false;
|
||||
var allPaths = new List<string>();
|
||||
foreach (var fd in Directory.EnumerateFiles($"/proc/{pid}/fd"))
|
||||
|
||||
Assert.IsFalse(proc.HasExited);
|
||||
foreach (var fd in Directory.GetFiles($"/proc/{pid}/fd"))
|
||||
{
|
||||
var sb = new StringBuilder(UInt16.MaxValue);
|
||||
if (Syscall.readlink(fd, sb) == -1)
|
||||
@@ -691,26 +737,21 @@ namespace Tgstation.Server.Tests.Live.Instance
|
||||
{
|
||||
System.Console.WriteLine("TEST: WATCHDOG HEALTH CHECK TEST");
|
||||
|
||||
// Check reverse mapping
|
||||
var status = await instanceClient.DreamDaemon.Update(new DreamDaemonRequest
|
||||
{
|
||||
DumpOnHealthCheckRestart = !checkDump,
|
||||
}, cancellationToken);
|
||||
|
||||
// enable health checks
|
||||
status = await instanceClient.DreamDaemon.Update(new DreamDaemonRequest
|
||||
var status = await instanceClient.DreamDaemon.Update(new DreamDaemonRequest
|
||||
{
|
||||
HealthCheckSeconds = 1,
|
||||
DumpOnHealthCheckRestart = checkDump,
|
||||
}, cancellationToken);
|
||||
|
||||
Assert.AreEqual(checkDump, status.DumpOnHealthCheckRestart);
|
||||
Assert.AreEqual(1U, status.HealthCheckSeconds.Value);
|
||||
|
||||
var startJob = await StartDD(cancellationToken);
|
||||
|
||||
await WaitForJob(startJob, 40, false, null, cancellationToken);
|
||||
|
||||
CheckDDPriority();
|
||||
await CheckDDPriority();
|
||||
|
||||
// lock on to DD and pause it so it can't health check
|
||||
var ddProcs = TestLiveServer.GetEngineServerProcessesOnPort(testVersion.Engine.Value, ddPort).Where(x => !x.HasExited).ToList();
|
||||
@@ -723,7 +764,6 @@ namespace Tgstation.Server.Tests.Live.Instance
|
||||
RuntimeInformation.IsOSPlatform(OSPlatform.Windows)
|
||||
? new WindowsProcessFeatures(Mock.Of<ILogger<WindowsProcessFeatures>>())
|
||||
: new PosixProcessFeatures(new Lazy<IProcessExecutor>(() => executor), Mock.Of<IIOManager>(), Mock.Of<ILogger<PosixProcessFeatures>>()),
|
||||
Mock.Of<IAsyncDelayer>(),
|
||||
Mock.Of<IIOManager>(),
|
||||
Mock.Of<ILogger<ProcessExecutor>>(),
|
||||
LoggerFactory.Create(x => { }));
|
||||
@@ -731,20 +771,18 @@ namespace Tgstation.Server.Tests.Live.Instance
|
||||
.GetProcess(ddProc.Id);
|
||||
|
||||
// Ensure it's responding to health checks
|
||||
await Task.WhenAny(Task.Delay(20000, cancellationToken), ourProcessHandler.Lifetime);
|
||||
await Task.WhenAny(Task.Delay(6000, cancellationToken), ourProcessHandler.Lifetime);
|
||||
Assert.IsFalse(ddProc.HasExited);
|
||||
|
||||
// check DD agrees
|
||||
var topicRequestResult = await topicClient.SendTopic(
|
||||
IPAddress.Loopback,
|
||||
$"tgs_integration_test_tactics8=1",
|
||||
FindTopicPort(),
|
||||
var topicRequestResult = await SendTestTopic(
|
||||
"tgs_integration_test_tactics8=1",
|
||||
cancellationToken);
|
||||
|
||||
Assert.IsNotNull(topicRequestResult);
|
||||
Assert.AreEqual(TopicResponseType.StringResponse, topicRequestResult.ResponseType);
|
||||
Assert.IsNotNull(topicRequestResult.StringData);
|
||||
Assert.AreEqual(topicRequestResult.StringData, "received health check");
|
||||
Assert.AreEqual("received health check", topicRequestResult.StringData);
|
||||
|
||||
var ddStatus = await instanceClient.DreamDaemon.Update(new DreamDaemonRequest
|
||||
{
|
||||
@@ -752,14 +790,16 @@ namespace Tgstation.Server.Tests.Live.Instance
|
||||
}, cancellationToken);
|
||||
ValidateSessionId(ddStatus, true);
|
||||
|
||||
global::System.Console.WriteLine($"WATCHDOG TEST {instanceClient.Metadata.Id}: COMMENCE PROCESS SUSPEND FOR HEALTH CHECK DEATH PID {ourProcessHandler.Id}.");
|
||||
ourProcessHandler.Suspend();
|
||||
global::System.Console.WriteLine($"WATCHDOG TEST {instanceClient.Metadata.Id}: FINISH PROCESS SUSPEND FOR HEALTH CHECK DEATH. WAITING FOR LIFETIME {ourProcessHandler.Id}.");
|
||||
|
||||
await Task.WhenAny(ourProcessHandler.Lifetime, Task.Delay(TimeSpan.FromMinutes(1), cancellationToken));
|
||||
await Task.WhenAny(ourProcessHandler.Lifetime, Task.Delay(TimeSpan.FromMinutes(4), cancellationToken));
|
||||
Assert.IsTrue(ourProcessHandler.Lifetime.IsCompleted);
|
||||
|
||||
var timeout = 60;
|
||||
var timeout = 20;
|
||||
do
|
||||
{
|
||||
await Task.Delay(TimeSpan.FromSeconds(1), cancellationToken);
|
||||
ddStatus = await instanceClient.DreamDaemon.Read(cancellationToken);
|
||||
Assert.AreEqual(1U, ddStatus.HealthCheckSeconds.Value);
|
||||
if (ddStatus.Status.Value == WatchdogStatus.Offline)
|
||||
@@ -770,6 +810,8 @@ namespace Tgstation.Server.Tests.Live.Instance
|
||||
|
||||
if (--timeout == 0)
|
||||
Assert.Fail("DreamDaemon didn't shutdown within the timeout!");
|
||||
|
||||
await Task.Delay(TimeSpan.FromSeconds(3), cancellationToken);
|
||||
}
|
||||
while (timeout > 0);
|
||||
|
||||
@@ -816,7 +858,6 @@ namespace Tgstation.Server.Tests.Live.Instance
|
||||
throw;
|
||||
}
|
||||
}
|
||||
await Task.Delay(TimeSpan.FromSeconds(3), cts.Token);
|
||||
|
||||
return await instanceClient.DreamDaemon.Start(cancellationToken);
|
||||
}
|
||||
@@ -846,7 +887,9 @@ namespace Tgstation.Server.Tests.Live.Instance
|
||||
|
||||
System.Console.WriteLine("TEST: Sending Bridge tests topic...");
|
||||
|
||||
var bridgeTestTopicResult = await topicClient.SendTopic(IPAddress.Loopback, $"tgs_integration_test_tactics2={accessIdentifier}", FindTopicPort(), cancellationToken);
|
||||
var bridgeTestTopicResult = await SendTestTopic(
|
||||
$"tgs_integration_test_tactics2={accessIdentifier}",
|
||||
cancellationToken);
|
||||
Assert.AreEqual("ack2", bridgeTestTopicResult.StringData);
|
||||
|
||||
await bridgeTestsTcs.Task.WaitAsync(cancellationToken);
|
||||
@@ -855,7 +898,7 @@ namespace Tgstation.Server.Tests.Live.Instance
|
||||
BridgeController.LogContent = true;
|
||||
|
||||
// Time for DD to revert the bridge access identifier change
|
||||
await Task.Delay(TimeSpan.FromSeconds(3), cancellationToken);
|
||||
await Task.Delay(TimeSpan.FromSeconds(1), cancellationToken);
|
||||
}
|
||||
|
||||
async Task ValidateTopicLimits(CancellationToken cancellationToken)
|
||||
@@ -896,10 +939,8 @@ namespace Tgstation.Server.Tests.Live.Instance
|
||||
try
|
||||
{
|
||||
System.Console.WriteLine($"Topic send limit test S:{currentSize}...");
|
||||
topicRequestResult = await topicClient.SendTopic(
|
||||
IPAddress.Loopback,
|
||||
topicRequestResult = await SendTestTopic(
|
||||
$"tgs_integration_test_tactics3={topicClient.SanitizeString(JsonConvert.SerializeObject(topic, DMApiConstants.SerializerSettings))}",
|
||||
FindTopicPort(),
|
||||
cancellationToken);
|
||||
}
|
||||
catch (ArgumentOutOfRangeException)
|
||||
@@ -911,7 +952,11 @@ namespace Tgstation.Server.Tests.Live.Instance
|
||||
|| topicRequestResult.StringData != "pass")
|
||||
{
|
||||
if (topicRequestResult != null)
|
||||
{
|
||||
Assert.AreEqual(TopicResponseType.StringResponse, topicRequestResult.ResponseType, $"String data is: {topicRequestResult.StringData ?? "<<NULL>>"}");
|
||||
Assert.AreEqual("fail", topicRequestResult.StringData);
|
||||
}
|
||||
|
||||
if (currentSize == lastSize + 1)
|
||||
break;
|
||||
baseSize = lastSize;
|
||||
@@ -937,10 +982,8 @@ namespace Tgstation.Server.Tests.Live.Instance
|
||||
{
|
||||
var currentSize = baseSize + (int)Math.Pow(2, nextPow);
|
||||
System.Console.WriteLine($"Topic recieve limit test S:{currentSize}...");
|
||||
var topicRequestResult = await topicClient.SendTopic(
|
||||
IPAddress.Loopback,
|
||||
var topicRequestResult = await SendTestTopic(
|
||||
$"tgs_integration_test_tactics4={topicClient.SanitizeString(currentSize.ToString())}",
|
||||
FindTopicPort(),
|
||||
cancellationToken);
|
||||
|
||||
if (topicRequestResult.ResponseType != TopicResponseType.StringResponse
|
||||
@@ -1066,8 +1109,9 @@ namespace Tgstation.Server.Tests.Live.Instance
|
||||
Assert.AreEqual("Footer text", embedsResponse.Embed.Footer?.Text);
|
||||
}
|
||||
|
||||
void CheckDDPriority()
|
||||
async ValueTask CheckDDPriority()
|
||||
{
|
||||
await Task.Yield();
|
||||
var allProcesses = TestLiveServer.GetEngineServerProcessesOnPort(testVersion.Engine.Value, ddPort).Where(x => !x.HasExited).ToList();
|
||||
if (allProcesses.Count == 0)
|
||||
Assert.Fail("Expected engine server to be running here");
|
||||
@@ -1107,7 +1151,7 @@ namespace Tgstation.Server.Tests.Live.Instance
|
||||
|
||||
Assert.AreEqual(WatchdogStatus.Online, daemonStatus.Status.Value);
|
||||
ValidateSessionId(daemonStatus, true);
|
||||
CheckDDPriority();
|
||||
await CheckDDPriority();
|
||||
|
||||
Assert.AreEqual(initialCompileJob.Id, daemonStatus.ActiveCompileJob.Id);
|
||||
var newerCompileJob = daemonStatus.StagedCompileJob;
|
||||
@@ -1155,7 +1199,7 @@ namespace Tgstation.Server.Tests.Live.Instance
|
||||
ValidateSessionId(daemonStatus, true);
|
||||
Assert.AreEqual(WatchdogStatus.Online, daemonStatus.Status.Value);
|
||||
Assert.AreEqual(true, daemonStatus.SoftRestart);
|
||||
CheckDDPriority();
|
||||
await CheckDDPriority();
|
||||
|
||||
Assert.AreEqual(initialCompileJob.Id, daemonStatus.ActiveCompileJob.Id);
|
||||
var newerCompileJob = daemonStatus.StagedCompileJob;
|
||||
@@ -1193,7 +1237,7 @@ namespace Tgstation.Server.Tests.Live.Instance
|
||||
|
||||
await WaitForJob(startJob, 70, false, null, cancellationToken);
|
||||
|
||||
CheckDDPriority();
|
||||
await CheckDDPriority();
|
||||
|
||||
var byondInstallJobTask = instanceClient.Engine.SetActiveVersion(
|
||||
new EngineVersionRequest
|
||||
@@ -1247,22 +1291,28 @@ namespace Tgstation.Server.Tests.Live.Instance
|
||||
public async Task StartAndLeaveRunning(CancellationToken cancellationToken)
|
||||
{
|
||||
System.Console.WriteLine("TEST: WATCHDOG STARTING ENDLESS");
|
||||
await DeployTestDme("LongRunning/long_running_test", DreamDaemonSecurity.Trusted, true, cancellationToken);
|
||||
|
||||
var startJob = await StartDD(cancellationToken);
|
||||
|
||||
await WaitForJob(startJob, 40, false, null, cancellationToken);
|
||||
|
||||
var daemonStatus = await instanceClient.DreamDaemon.Read(cancellationToken);
|
||||
var daemonStatus = await instanceClient.DreamDaemon.Update(new DreamDaemonRequest
|
||||
{
|
||||
AdditionalParameters = "slow_start=1",
|
||||
},
|
||||
cancellationToken);
|
||||
|
||||
Assert.AreEqual(WatchdogStatus.Online, daemonStatus.Status.Value);
|
||||
CheckDDPriority();
|
||||
Assert.AreEqual(WatchdogStatus.Online, daemonStatus.Status);
|
||||
Assert.IsTrue(daemonStatus.SoftRestart);
|
||||
await CheckDDPriority();
|
||||
Assert.AreEqual(ddPort, daemonStatus.CurrentPort);
|
||||
|
||||
// Try killing the DD process to ensure it gets set to the restoring state
|
||||
bool firstTime = true;
|
||||
do
|
||||
{
|
||||
if(!firstTime)
|
||||
Assert.IsFalse(daemonStatus.SoftRestart);
|
||||
|
||||
ValidateSessionId(daemonStatus, true);
|
||||
KillDD(firstTime);
|
||||
firstTime = false;
|
||||
@@ -1270,7 +1320,7 @@ namespace Tgstation.Server.Tests.Live.Instance
|
||||
daemonStatus = await instanceClient.DreamDaemon.Read(cancellationToken);
|
||||
}
|
||||
while (daemonStatus.Status == WatchdogStatus.Online);
|
||||
Assert.AreEqual(WatchdogStatus.Restoring, daemonStatus.Status.Value);
|
||||
Assert.AreEqual(WatchdogStatus.Restoring, daemonStatus.Status);
|
||||
|
||||
// Kill it again
|
||||
do
|
||||
@@ -1279,13 +1329,18 @@ namespace Tgstation.Server.Tests.Live.Instance
|
||||
daemonStatus = await instanceClient.DreamDaemon.Read(cancellationToken);
|
||||
}
|
||||
while (daemonStatus.Status == WatchdogStatus.Online || daemonStatus.Status == WatchdogStatus.Restoring);
|
||||
Assert.AreEqual(WatchdogStatus.DelayedRestart, daemonStatus.Status.Value);
|
||||
Assert.AreEqual(WatchdogStatus.DelayedRestart, daemonStatus.Status);
|
||||
|
||||
await Task.Delay(TimeSpan.FromSeconds(15), cancellationToken);
|
||||
|
||||
daemonStatus = await instanceClient.DreamDaemon.Read(cancellationToken);
|
||||
daemonStatus = await instanceClient.DreamDaemon.Update(new DreamDaemonRequest
|
||||
{
|
||||
AdditionalParameters = String.Empty,
|
||||
},
|
||||
cancellationToken);
|
||||
ValidateSessionId(daemonStatus, true);
|
||||
Assert.AreEqual(WatchdogStatus.Online, daemonStatus.Status.Value);
|
||||
Assert.AreEqual(WatchdogStatus.Online, daemonStatus.Status);
|
||||
Assert.IsTrue(daemonStatus.SoftRestart);
|
||||
await CheckDMApiFail(daemonStatus.ActiveCompileJob, cancellationToken);
|
||||
}
|
||||
|
||||
@@ -1302,17 +1357,17 @@ namespace Tgstation.Server.Tests.Live.Instance
|
||||
return ddProc != null;
|
||||
}
|
||||
|
||||
public Task<DreamDaemonResponse> TellWorldToReboot(bool waitForOnlineIfRestoring, CancellationToken cancellationToken)
|
||||
=> TellWorldToReboot2(instanceClient, topicClient, FindTopicPort(), waitForOnlineIfRestoring ? true : testVersion.Engine.Value == EngineType.OpenDream, cancellationToken);
|
||||
public static async Task<DreamDaemonResponse> TellWorldToReboot2(IInstanceClient instanceClient, ITopicClient topicClient, ushort topicPort, bool waitForOnlineIfRestoring, CancellationToken cancellationToken)
|
||||
public Task<DreamDaemonResponse> TellWorldToReboot(bool waitForOnlineIfRestoring, CancellationToken cancellationToken, [CallerLineNumber]int source = 0)
|
||||
=> TellWorldToReboot2(instanceClient, instanceManager, topicClient, FindTopicPort(), waitForOnlineIfRestoring || testVersion.Engine.Value == EngineType.OpenDream, cancellationToken, source);
|
||||
public static async Task<DreamDaemonResponse> TellWorldToReboot2(IInstanceClient instanceClient, IInstanceManager instanceManager, ITopicClient topicClient, ushort topicPort, bool waitForOnlineIfRestoring, CancellationToken cancellationToken, [CallerLineNumber]int source = 0, [CallerFilePath]string path = null)
|
||||
{
|
||||
var daemonStatus = await instanceClient.DreamDaemon.Read(cancellationToken);
|
||||
Assert.IsNotNull(daemonStatus.StagedCompileJob);
|
||||
var initialCompileJob = daemonStatus.ActiveCompileJob;
|
||||
var initialSession = daemonStatus.ActiveCompileJob;
|
||||
|
||||
System.Console.WriteLine("TEST: Sending world reboot topic...");
|
||||
System.Console.WriteLine($"TEST: Sending world reboot topic @ {path}#L{source}");
|
||||
|
||||
var result = await topicClient.SendTopic(IPAddress.Loopback, "tgs_integration_test_special_tactics=1", topicPort, cancellationToken);
|
||||
var result = await SendTestTopic("tgs_integration_test_special_tactics=1", topicClient, instanceManager.GetInstanceReference(instanceClient.Metadata), topicPort, cancellationToken);
|
||||
Assert.AreEqual("ack", result.StringData);
|
||||
|
||||
using var tempCts = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken);
|
||||
@@ -1326,11 +1381,9 @@ namespace Tgstation.Server.Tests.Live.Instance
|
||||
await Task.Delay(TimeSpan.FromSeconds(1), tempToken);
|
||||
daemonStatus = await instanceClient.DreamDaemon.Read(tempToken);
|
||||
}
|
||||
while (initialCompileJob.Id == daemonStatus.ActiveCompileJob.Id);
|
||||
while (initialSession.Id == daemonStatus.ActiveCompileJob.Id);
|
||||
}
|
||||
|
||||
await Task.Delay(TimeSpan.FromSeconds(3), cancellationToken);
|
||||
|
||||
if (waitForOnlineIfRestoring && daemonStatus.Status == WatchdogStatus.Restoring)
|
||||
{
|
||||
do
|
||||
@@ -1351,7 +1404,7 @@ namespace Tgstation.Server.Tests.Live.Instance
|
||||
ApiValidationSecurityLevel = deploymentSecurity,
|
||||
ProjectName = dmeName.Contains("rooted") ? dmeName : $"tests/DMAPI/{dmeName}",
|
||||
RequireDMApiValidation = requireApi,
|
||||
Timeout = TimeSpan.FromMilliseconds(1),
|
||||
Timeout = !ranTimeoutTest ? TimeSpan.FromMilliseconds(1) : TimeSpan.FromMinutes(5),
|
||||
}, cancellationToken);
|
||||
|
||||
JobResponse compileJobJob;
|
||||
@@ -1364,29 +1417,33 @@ namespace Tgstation.Server.Tests.Live.Instance
|
||||
compileJobJob = await instanceClient.DreamMaker.Compile(cancellationToken);
|
||||
|
||||
await WaitForJob(compileJobJob, 90, true, ErrorCode.DeploymentTimeout, cancellationToken);
|
||||
|
||||
await instanceClient.DreamMaker.Update(new DreamMakerRequest
|
||||
{
|
||||
Timeout = TimeSpan.FromMinutes(5),
|
||||
}, cancellationToken);
|
||||
ranTimeoutTest = true;
|
||||
}
|
||||
|
||||
await instanceClient.DreamMaker.Update(new DreamMakerRequest
|
||||
{
|
||||
Timeout = TimeSpan.FromMinutes(5),
|
||||
}, cancellationToken);
|
||||
|
||||
compileJobJob = await instanceClient.DreamMaker.Compile(cancellationToken);
|
||||
await WaitForJob(compileJobJob, 90, false, null, cancellationToken);
|
||||
|
||||
// annoying but, with signalR instant job updates, this running task can get queued before the task that processes the watchdog's monitor activation
|
||||
for (var i = 0; i < 10; ++i)
|
||||
await Task.Yield();
|
||||
|
||||
var ddInfo = await instanceClient.DreamDaemon.Read(cancellationToken);
|
||||
var targetJob = ddInfo.StagedCompileJob ?? ddInfo.ActiveCompileJob;
|
||||
Assert.IsNotNull(targetJob);
|
||||
if (requireApi)
|
||||
{
|
||||
var targetJob = ddInfo.StagedCompileJob ?? ddInfo.ActiveCompileJob;
|
||||
Assert.IsNotNull(targetJob);
|
||||
Assert.IsNotNull(targetJob.DMApiVersion);
|
||||
}
|
||||
else
|
||||
Assert.IsNull(targetJob.DMApiVersion);
|
||||
|
||||
return ddInfo;
|
||||
}
|
||||
|
||||
async Task GracefulWatchdogShutdown(uint timeout, CancellationToken cancellationToken)
|
||||
async Task GracefulWatchdogShutdown(CancellationToken cancellationToken)
|
||||
{
|
||||
await instanceClient.DreamDaemon.Update(new DreamDaemonRequest
|
||||
{
|
||||
@@ -1396,9 +1453,10 @@ namespace Tgstation.Server.Tests.Live.Instance
|
||||
var newStatus = await instanceClient.DreamDaemon.Read(cancellationToken);
|
||||
Assert.IsTrue(newStatus.SoftShutdown.Value || newStatus.Status.Value == WatchdogStatus.Offline);
|
||||
|
||||
var timeout = 20;
|
||||
do
|
||||
{
|
||||
await Task.Delay(TimeSpan.FromSeconds(1), cancellationToken);
|
||||
await Task.Delay(TimeSpan.FromSeconds(3), cancellationToken);
|
||||
var ddStatus = await instanceClient.DreamDaemon.Read(cancellationToken);
|
||||
if (ddStatus.Status.Value == WatchdogStatus.Offline)
|
||||
break;
|
||||
@@ -1444,9 +1502,12 @@ namespace Tgstation.Server.Tests.Live.Instance
|
||||
|
||||
async ValueTask TestLegacyBridgeEndpoint(CancellationToken cancellationToken)
|
||||
{
|
||||
var result = await topicClient.SendTopic(IPAddress.Loopback, "im_out_of_memes=1", FindTopicPort(), cancellationToken);
|
||||
Assert.AreEqual("yeah gimmie a sec", result.StringData);
|
||||
await Task.Delay(TimeSpan.FromSeconds(10), cancellationToken);
|
||||
System.Console.WriteLine("TEST: TestLegacyBridgeEndpoint");
|
||||
var result = await SendTestTopic(
|
||||
"im_out_of_memes=1",
|
||||
cancellationToken);
|
||||
Assert.IsNotNull(result);
|
||||
Assert.AreEqual("all gucci", result.StringData);
|
||||
await CheckDMApiFail((await instanceClient.DreamDaemon.Read(cancellationToken)).ActiveCompileJob, cancellationToken);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -153,6 +153,8 @@ namespace Tgstation.Server.Tests.Live
|
||||
$"Session:LowPriorityDeploymentProcesses={LowPriorityDeployments}",
|
||||
$"General:SkipAddingByondFirewallException={!TestingUtils.RunningInGitHubActions}",
|
||||
$"General:OpenDreamGitUrl={OpenDreamUrl}",
|
||||
$"Security:TokenExpiryMinutes=120", // timeouts are useless for us
|
||||
$"General:OpenDreamSuppressInstallOutput={TestingUtils.RunningInGitHubActions}",
|
||||
};
|
||||
|
||||
swarmArgs = new List<string>();
|
||||
|
||||
@@ -28,6 +28,7 @@ using Newtonsoft.Json;
|
||||
using Npgsql;
|
||||
|
||||
using Tgstation.Server.Api;
|
||||
using Tgstation.Server.Api.Extensions;
|
||||
using Tgstation.Server.Api.Models;
|
||||
using Tgstation.Server.Api.Models.Request;
|
||||
using Tgstation.Server.Api.Models.Response;
|
||||
@@ -53,12 +54,12 @@ namespace Tgstation.Server.Tests.Live
|
||||
{
|
||||
public static readonly Version TestUpdateVersion = new(5, 11, 0);
|
||||
|
||||
static readonly ushort mainDDPort = FreeTcpPort();
|
||||
static readonly ushort mainDMPort = FreeTcpPort(mainDDPort);
|
||||
static readonly ushort compatDMPort = FreeTcpPort(mainDDPort, mainDMPort);
|
||||
static readonly ushort compatDDPort = FreeTcpPort(mainDDPort, mainDMPort, compatDMPort);
|
||||
static readonly ushort odDMPort = FreeTcpPort(mainDDPort, mainDMPort, compatDMPort, compatDDPort);
|
||||
static readonly ushort odDDPort = FreeTcpPort(mainDDPort, mainDMPort, compatDMPort, compatDDPort, odDMPort);
|
||||
static readonly Lazy<ushort> odDMPort = new Lazy<ushort>(() => FreeTcpPort());
|
||||
static readonly Lazy<ushort> odDDPort = new Lazy<ushort>(() => FreeTcpPort(odDMPort.Value));
|
||||
static readonly Lazy<ushort> compatDMPort = new Lazy<ushort>(() => FreeTcpPort(odDDPort.Value, odDMPort.Value));
|
||||
static readonly Lazy<ushort> compatDDPort = new Lazy<ushort>(() => FreeTcpPort(odDDPort.Value, odDMPort.Value, compatDMPort.Value));
|
||||
static readonly Lazy<ushort> mainDDPort = new Lazy<ushort>(() => FreeTcpPort(odDDPort.Value, odDMPort.Value, compatDMPort.Value, compatDDPort.Value));
|
||||
static readonly Lazy<ushort> mainDMPort = new Lazy<ushort>(() => FreeTcpPort(odDDPort.Value, odDMPort.Value, compatDMPort.Value, compatDDPort.Value, mainDDPort.Value));
|
||||
|
||||
readonly ServerClientFactory clientFactory = new (new ProductHeaderValue(Assembly.GetExecutingAssembly().GetName().Name, Assembly.GetExecutingAssembly().GetName().Version.ToString()));
|
||||
|
||||
@@ -146,8 +147,12 @@ namespace Tgstation.Server.Tests.Live
|
||||
|
||||
static ushort FreeTcpPort(params ushort[] usedPorts)
|
||||
{
|
||||
var portList = new ushort[] { 42069, 42070, 42071, 42072, 42073, 42074 };
|
||||
return portList.First(x => !usedPorts.Contains(x));
|
||||
/*
|
||||
ushort result;
|
||||
var listeners = new List<TcpListener>();
|
||||
|
||||
try
|
||||
{
|
||||
do
|
||||
@@ -166,7 +171,7 @@ namespace Tgstation.Server.Tests.Live
|
||||
|
||||
result = (ushort)((IPEndPoint)l.LocalEndpoint).Port;
|
||||
}
|
||||
while (usedPorts.Contains(result));
|
||||
while (usedPorts.Contains(result) || result < 10000);
|
||||
}
|
||||
finally
|
||||
{
|
||||
@@ -176,6 +181,7 @@ namespace Tgstation.Server.Tests.Live
|
||||
}
|
||||
}
|
||||
return result;
|
||||
*/
|
||||
}
|
||||
|
||||
[ClassInitialize]
|
||||
@@ -1049,7 +1055,7 @@ namespace Tgstation.Server.Tests.Live
|
||||
|
||||
var ioManager = new Host.IO.DefaultIOManager();
|
||||
var repoPath = ioManager.ConcatPath(instance.Path, "Repository");
|
||||
var jobsTest = new JobsRequiredTest(instanceClient.Jobs);
|
||||
await using var jobsTest = new JobsRequiredTest(instanceClient.Jobs);
|
||||
var postWriteHandler = (Host.IO.IPostWriteHandler)(new PlatformIdentifier().IsWindows
|
||||
? new Host.IO.WindowsPostWriteHandler()
|
||||
: new Host.IO.PosixPostWriteHandler(loggerFactory.CreateLogger<Host.IO.PosixPostWriteHandler>()));
|
||||
@@ -1080,7 +1086,6 @@ namespace Tgstation.Server.Tests.Live
|
||||
new PlatformIdentifier().IsWindows
|
||||
? new WindowsProcessFeatures(loggerFactory.CreateLogger<WindowsProcessFeatures>())
|
||||
: new PosixProcessFeatures(new Lazy<IProcessExecutor>(() => processExecutor), ioManager, loggerFactory.CreateLogger<PosixProcessFeatures>()),
|
||||
new AsyncDelayer(),
|
||||
ioManager,
|
||||
loggerFactory.CreateLogger<ProcessExecutor>(),
|
||||
loggerFactory);
|
||||
@@ -1213,7 +1218,7 @@ namespace Tgstation.Server.Tests.Live
|
||||
[TestMethod]
|
||||
public Task TestOpenDreamExclusiveTgsOperation()
|
||||
{
|
||||
if (String.IsNullOrWhiteSpace(Environment.GetEnvironmentVariable("TGS_TEST_OD_EXCLUSIVE")))
|
||||
if (Environment.GetEnvironmentVariable("TGS_TEST_OD_EXCLUSIVE") != "true")
|
||||
Assert.Inconclusive("This test is covered by TestStandardTgsOperation");
|
||||
|
||||
return TestStandardTgsOperation(true);
|
||||
@@ -1395,11 +1400,11 @@ namespace Tgstation.Server.Tests.Live
|
||||
InstanceResponse odInstance, compatInstance;
|
||||
if (!openDreamOnly)
|
||||
{
|
||||
jobsHubTestTask = FailFast(await jobsHubTest.Run(cancellationToken)); // returns Task<Task>
|
||||
var rootTest = FailFast(RawRequestTests.Run(clientFactory, firstAdminClient, cancellationToken));
|
||||
var adminTest = FailFast(new AdministrationTest(firstAdminClient.Administration).Run(cancellationToken));
|
||||
var usersTest = FailFast(new UsersTest(firstAdminClient).Run(cancellationToken));
|
||||
|
||||
jobsHubTestTask = FailFast(jobsHubTest.Run(cancellationToken));
|
||||
var instanceManagerTest = new InstanceManagerTest(firstAdminClient, server.Directory);
|
||||
var compatInstanceTask = instanceManagerTest.CreateTestInstance("CompatTestsInstance", cancellationToken);
|
||||
var odInstanceTask = instanceManagerTest.CreateTestInstance("OdTestsInstance", cancellationToken);
|
||||
@@ -1457,8 +1462,8 @@ namespace Tgstation.Server.Tests.Live
|
||||
await edgeODVersionTask,
|
||||
server.OpenDreamUrl,
|
||||
firstAdminClient.Instances.CreateClient(odInstance),
|
||||
odDMPort,
|
||||
odDDPort,
|
||||
odDMPort.Value,
|
||||
odDDPort.Value,
|
||||
server.HighPriorityDreamDaemon,
|
||||
server.UsingBasicWatchdog,
|
||||
cancellationToken);
|
||||
@@ -1484,8 +1489,8 @@ namespace Tgstation.Server.Tests.Live
|
||||
},
|
||||
server.OpenDreamUrl,
|
||||
firstAdminClient.Instances.CreateClient(compatInstance),
|
||||
compatDMPort,
|
||||
compatDDPort,
|
||||
compatDMPort.Value,
|
||||
compatDDPort.Value,
|
||||
server.HighPriorityDreamDaemon,
|
||||
server.UsingBasicWatchdog,
|
||||
cancellationToken));
|
||||
@@ -1497,8 +1502,8 @@ namespace Tgstation.Server.Tests.Live
|
||||
instanceTest
|
||||
.RunTests(
|
||||
instanceClient,
|
||||
mainDMPort,
|
||||
mainDDPort,
|
||||
mainDMPort.Value,
|
||||
mainDDPort.Value,
|
||||
server.HighPriorityDreamDaemon,
|
||||
server.LowPriorityDeployments,
|
||||
server.UsingBasicWatchdog,
|
||||
@@ -1532,10 +1537,11 @@ namespace Tgstation.Server.Tests.Live
|
||||
|
||||
// test the reattach message queueing
|
||||
// for the code coverage really...
|
||||
var topicRequestResult = await WatchdogTest.StaticTopicClient.SendTopic(
|
||||
IPAddress.Loopback,
|
||||
$"tgs_integration_test_tactics6=1",
|
||||
mainDDPort,
|
||||
var topicRequestResult = await WatchdogTest.SendTestTopic(
|
||||
"tgs_integration_test_tactics6=1",
|
||||
WatchdogTest.StaticTopicClient,
|
||||
null,
|
||||
mainDDPort.Value,
|
||||
cancellationToken);
|
||||
|
||||
Assert.IsNotNull(topicRequestResult);
|
||||
@@ -1590,7 +1596,7 @@ namespace Tgstation.Server.Tests.Live
|
||||
.ToList();
|
||||
}
|
||||
|
||||
var jrt = new JobsRequiredTest(instanceClient.Jobs);
|
||||
await using var jrt = new JobsRequiredTest(instanceClient.Jobs);
|
||||
foreach (var job in jobs)
|
||||
{
|
||||
Assert.IsTrue(job.StartedAt.Value >= preStartupTime);
|
||||
@@ -1608,10 +1614,11 @@ namespace Tgstation.Server.Tests.Live
|
||||
var chatReadTask = instanceClient.ChatBots.List(null, cancellationToken);
|
||||
|
||||
// Check the DMAPI got the channels again https://github.com/tgstation/tgstation-server/issues/1490
|
||||
topicRequestResult = await WatchdogTest.StaticTopicClient.SendTopic(
|
||||
IPAddress.Loopback,
|
||||
$"tgs_integration_test_tactics7=1",
|
||||
mainDDPort,
|
||||
topicRequestResult = await WatchdogTest.SendTestTopic(
|
||||
"tgs_integration_test_tactics7=1",
|
||||
WatchdogTest.StaticTopicClient,
|
||||
GetInstanceManager().GetInstanceReference(instanceClient.Metadata),
|
||||
mainDDPort.Value,
|
||||
cancellationToken);
|
||||
|
||||
Assert.IsNotNull(topicRequestResult);
|
||||
@@ -1622,7 +1629,13 @@ namespace Tgstation.Server.Tests.Live
|
||||
|
||||
Assert.AreEqual(connectedChannelCount, topicRequestResult.FloatData.Value);
|
||||
|
||||
dd = await WatchdogTest.TellWorldToReboot2(instanceClient, WatchdogTest.StaticTopicClient, mainDDPort, true, cancellationToken);
|
||||
dd = await WatchdogTest.TellWorldToReboot2(
|
||||
instanceClient,
|
||||
GetInstanceManager(),
|
||||
WatchdogTest.StaticTopicClient,
|
||||
mainDDPort.Value,
|
||||
true,
|
||||
cancellationToken);
|
||||
|
||||
Assert.AreEqual(WatchdogStatus.Online, dd.Status.Value); // if this assert fails, you likely have to crack open the debugger and read test_fail_reason.txt manually
|
||||
Assert.IsNull(dd.StagedCompileJob);
|
||||
@@ -1649,22 +1662,17 @@ namespace Tgstation.Server.Tests.Live
|
||||
{
|
||||
var jobs = await instanceClient.Jobs.ListActive(null, cancellationToken);
|
||||
if (jobs.Count == 0)
|
||||
{
|
||||
var entities = await instanceClient.Jobs.List(null, cancellationToken);
|
||||
var getTasks = entities
|
||||
.Select(e => instanceClient.Jobs.GetId(e, cancellationToken))
|
||||
.ToList();
|
||||
|
||||
jobs = (await ValueTaskExtensions.WhenAll(getTasks))
|
||||
jobs = (await instanceClient.Jobs.List(null, cancellationToken))
|
||||
.Where(x => x.StartedAt.Value > preStartupTime)
|
||||
.ToList();
|
||||
}
|
||||
.ToList();
|
||||
else
|
||||
jobs = jobs.Where(x => x.JobCode.Value.IsServerStartupJob()).ToList();
|
||||
|
||||
var jrt = new JobsRequiredTest(instanceClient.Jobs);
|
||||
await using var jrt = new JobsRequiredTest(instanceClient.Jobs);
|
||||
foreach (var job in jobs)
|
||||
{
|
||||
Assert.IsTrue(job.StartedAt.Value >= preStartupTime);
|
||||
await jrt.WaitForJob(job, 140, job.Description.Contains("Reconnect chat bot") ? null : false, null, cancellationToken);
|
||||
await jrt.WaitForJob(job, 140, job.JobCode == JobCode.ReconnectChatBot ? null : false, null, cancellationToken);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1675,16 +1683,16 @@ namespace Tgstation.Server.Tests.Live
|
||||
var edgeVersion = await EngineTest.GetEdgeVersion(EngineType.Byond, fileDownloader, cancellationToken);
|
||||
await using (var adminClient = await CreateAdminClient(server.ApiUrl, cancellationToken))
|
||||
{
|
||||
await jobsHubTest.WaitForReconnect(cancellationToken);
|
||||
var instanceClient = adminClient.Instances.CreateClient(instance);
|
||||
await WaitForInitialJobs(instanceClient);
|
||||
await jobsHubTest.WaitForReconnect(cancellationToken);
|
||||
|
||||
var dd = await instanceClient.DreamDaemon.Read(cancellationToken);
|
||||
|
||||
Assert.AreEqual(WatchdogStatus.Online, dd.Status.Value);
|
||||
|
||||
var compileJob = await instanceClient.DreamMaker.Compile(cancellationToken);
|
||||
var wdt = new WatchdogTest(edgeVersion, instanceClient, GetInstanceManager(), (ushort)server.ApiUrl.Port, server.HighPriorityDreamDaemon, mainDDPort, server.UsingBasicWatchdog);
|
||||
await using var wdt = new WatchdogTest(edgeVersion, instanceClient, GetInstanceManager(), (ushort)server.ApiUrl.Port, server.HighPriorityDreamDaemon, mainDDPort.Value, server.UsingBasicWatchdog);
|
||||
await wdt.WaitForJob(compileJob, 30, false, null, cancellationToken);
|
||||
|
||||
dd = await instanceClient.DreamDaemon.Read(cancellationToken);
|
||||
@@ -1718,22 +1726,24 @@ namespace Tgstation.Server.Tests.Live
|
||||
serverTask = server.Run(cancellationToken).AsTask();
|
||||
await using (var adminClient = await CreateAdminClient(server.ApiUrl, cancellationToken))
|
||||
{
|
||||
await jobsHubTest.WaitForReconnect(cancellationToken);
|
||||
var instanceClient = adminClient.Instances.CreateClient(instance);
|
||||
await WaitForInitialJobs(instanceClient);
|
||||
await jobsHubTest.WaitForReconnect(cancellationToken);
|
||||
|
||||
var currentDD = await instanceClient.DreamDaemon.Read(cancellationToken);
|
||||
Assert.AreEqual(expectedCompileJobId, currentDD.ActiveCompileJob.Id.Value);
|
||||
Assert.AreEqual(WatchdogStatus.Online, currentDD.Status);
|
||||
Assert.AreEqual(expectedStaged, currentDD.StagedCompileJob.Job.Id.Value);
|
||||
|
||||
var wdt = new WatchdogTest(edgeVersion, instanceClient, GetInstanceManager(), (ushort)server.ApiUrl.Port, server.HighPriorityDreamDaemon, mainDDPort, server.UsingBasicWatchdog);
|
||||
currentDD = await wdt.TellWorldToReboot(true, cancellationToken);
|
||||
await using var wdt = new WatchdogTest(edgeVersion, instanceClient, GetInstanceManager(), (ushort)server.ApiUrl.Port, server.HighPriorityDreamDaemon, mainDDPort.Value, server.UsingBasicWatchdog);
|
||||
currentDD = await wdt.TellWorldToReboot(false, cancellationToken);
|
||||
Assert.AreEqual(expectedStaged, currentDD.ActiveCompileJob.Job.Id.Value);
|
||||
Assert.IsNull(currentDD.StagedCompileJob);
|
||||
|
||||
var repoTest = new RepositoryTest(instanceClient.Repository, instanceClient.Jobs).RunPostTest(cancellationToken);
|
||||
await new ChatTest(instanceClient.ChatBots, adminClient.Instances, instanceClient.Jobs, instance).RunPostTest(cancellationToken);
|
||||
await using var repoTestObj = new RepositoryTest(instanceClient.Repository, instanceClient.Jobs);
|
||||
var repoTest = repoTestObj.RunPostTest(cancellationToken);
|
||||
await using var chatTestObj = new ChatTest(instanceClient.ChatBots, adminClient.Instances, instanceClient.Jobs, instance);
|
||||
await chatTestObj.RunPostTest(cancellationToken);
|
||||
await repoTest;
|
||||
|
||||
await DummyChatProvider.RandomDisconnections(false, cancellationToken);
|
||||
|
||||
@@ -24,7 +24,6 @@ namespace Tgstation.Server.Tests
|
||||
var platformIdentifier = new PlatformIdentifier();
|
||||
var processExecutor = new ProcessExecutor(
|
||||
Mock.Of<IProcessFeatures>(),
|
||||
Mock.Of<IAsyncDelayer>(),
|
||||
new DefaultIOManager(),
|
||||
Mock.Of<ILogger<ProcessExecutor>>(),
|
||||
loggerFactory);
|
||||
@@ -45,40 +44,47 @@ namespace Tgstation.Server.Tests
|
||||
[TestMethod]
|
||||
public async Task TestScriptExecutionWithFileOutput()
|
||||
{
|
||||
using var loggerFactory = LoggerFactory.Create(x => { });
|
||||
using var loggerFactory = LoggerFactory.Create(builder =>
|
||||
{
|
||||
builder.AddConsole();
|
||||
builder.SetMinimumLevel(LogLevel.Trace);
|
||||
});
|
||||
var platformIdentifier = new PlatformIdentifier();
|
||||
var processExecutor = new ProcessExecutor(
|
||||
Mock.Of<IProcessFeatures>(),
|
||||
Mock.Of<IAsyncDelayer>(),
|
||||
new DefaultIOManager(),
|
||||
Mock.Of<ILogger<ProcessExecutor>>(),
|
||||
loggerFactory.CreateLogger<ProcessExecutor>(),
|
||||
loggerFactory);
|
||||
|
||||
var tempFile = Path.GetTempFileName();
|
||||
File.Delete(tempFile);
|
||||
try
|
||||
{
|
||||
await using (var process = processExecutor.LaunchProcess("test." + platformIdentifier.ScriptFileExtension, ".", string.Empty, tempFile, true, true))
|
||||
{
|
||||
using var cts = new CancellationTokenSource();
|
||||
cts.CancelAfter(3000);
|
||||
var exitCode = await process.Lifetime.WaitAsync(cts.Token);
|
||||
|
||||
await process.GetCombinedOutput(cts.Token);
|
||||
|
||||
Assert.AreEqual(0, exitCode);
|
||||
}
|
||||
|
||||
Assert.IsTrue(File.Exists(tempFile));
|
||||
var result = File.ReadAllText(tempFile).Trim();
|
||||
|
||||
// no guarantees about order
|
||||
Assert.IsTrue(result.Contains("Hello World!"));
|
||||
Assert.IsTrue(result.Contains("Hello Error!"));
|
||||
}
|
||||
finally
|
||||
// run on a loop to spot the hang
|
||||
for (var i = 0; i < 1000; ++i)
|
||||
{
|
||||
var tempFile = Path.GetTempFileName();
|
||||
File.Delete(tempFile);
|
||||
try
|
||||
{
|
||||
await using (var process = processExecutor.LaunchProcess("test." + platformIdentifier.ScriptFileExtension, ".", string.Empty, tempFile, true, true))
|
||||
{
|
||||
using var cts = new CancellationTokenSource();
|
||||
cts.CancelAfter(3000);
|
||||
var exitCode = await process.Lifetime.WaitAsync(cts.Token);
|
||||
|
||||
await process.GetCombinedOutput(cts.Token);
|
||||
|
||||
Assert.AreEqual(0, exitCode);
|
||||
}
|
||||
|
||||
Assert.IsTrue(File.Exists(tempFile), $"Could not find temp file: {tempFile}");
|
||||
var result = File.ReadAllText(tempFile).Trim();
|
||||
|
||||
// no guarantees about order
|
||||
Assert.IsTrue(result.Contains("Hello World!"), $"Result: {result}");
|
||||
Assert.IsTrue(result.Contains("Hello Error!"), $"Result: {result}");
|
||||
}
|
||||
finally
|
||||
{
|
||||
File.Delete(tempFile);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -208,7 +208,6 @@ namespace Tgstation.Server.Tests
|
||||
new Lazy<IProcessExecutor>(() => null),
|
||||
Mock.Of<IIOManager>(),
|
||||
loggerFactory.CreateLogger<PosixProcessFeatures>()),
|
||||
Mock.Of<IAsyncDelayer>(),
|
||||
Mock.Of<IIOManager>(),
|
||||
loggerFactory.CreateLogger<ProcessExecutor>(),
|
||||
loggerFactory);
|
||||
@@ -416,11 +415,13 @@ namespace Tgstation.Server.Tests
|
||||
if (!Directory.Exists(directory))
|
||||
Assert.Inconclusive("Webpanel not built?");
|
||||
|
||||
var logo = new PlatformIdentifier().IsWindows
|
||||
? RootController.ProjectLogoSvgRouteWindows
|
||||
: RootController.ProjectLogoSvgRouteLinux;
|
||||
static string GetConstField(string name) => (string)typeof(RootController).GetField(name, BindingFlags.NonPublic | BindingFlags.Static).GetValue(null);
|
||||
|
||||
var path = $"../../../../../src/Tgstation.Server.Host/wwwroot{logo}";
|
||||
var logo = new PlatformIdentifier().IsWindows
|
||||
? GetConstField("LogoSvgWindowsName")
|
||||
: GetConstField("LogoSvgLinuxName");
|
||||
|
||||
var path = $"../../../../../src/Tgstation.Server.Host/wwwroot/{logo}.svg";
|
||||
Assert.IsTrue(File.Exists(path));
|
||||
|
||||
var content = await File.ReadAllBytesAsync(path);
|
||||
@@ -462,8 +463,8 @@ namespace Tgstation.Server.Tests
|
||||
EngineVersion engineVersion,
|
||||
Stream byondBytes,
|
||||
ByondInstallerBase byondInstaller,
|
||||
IIOManager ioManager,
|
||||
IProcessExecutor processExecutor,
|
||||
DefaultIOManager ioManager,
|
||||
ProcessExecutor processExecutor,
|
||||
string tempPath)
|
||||
{
|
||||
using (byondBytes)
|
||||
|
||||
Reference in New Issue
Block a user