Merge branch 'dev' into 1458-AddMapThreadsParam

This commit is contained in:
Jordan Dominion
2023-07-04 18:01:35 -04:00
166 changed files with 3322 additions and 1076 deletions
+1
View File
@@ -8,6 +8,7 @@ coverage:
comment:
layout: "header, diff, changes"
ignore:
- "build"
- "src/Tgstation.Server.Host/Database/Design"
- "src/Tgstation.Server.Host/Database/Migrations/MySqlDatabaseContextModelSnapshot.cs"
- "src/Tgstation.Server.Host/Database/Migrations/PostgresSqlDatabaseContextModelSnapshot.cs"
+1
View File
@@ -23,6 +23,7 @@ build/**
!build/ControlPanelVersion.props
!build/Common.props
!build/NugetCommon.props
!build/uac_elevation_manifest.xml
docs
src/DMAPI
src/Tgstation.Server.Host/ClientApp
+8
View File
@@ -12,6 +12,14 @@ indent_size = 4
indent_style = space
indent_size = 2
[*.wxs]
indent_style = space
indent_size = 2
[*.wixproj]
indent_style = space
indent_size = 2
[*.dm]
indent_style = tab
indent_size = 4
+11 -2
View File
@@ -34,9 +34,13 @@ You can of course, as always, ask for help at [#coderbus](irc://irc.rizon.net/co
### Development Environment
You need the Dotnet 6.0 SDK and npm>=v5.7 (in your PATH) to compile the server. In order to build the service version you also need a .NET 4.7.1 build chain
You need the .NET 6.0 SDK and npm>=v5.7 (in your PATH) to compile the server.
The recommended IDE is Visual Studio 2019 which has installation options for both of these.
The recommended IDE is Visual Studio 2022 or VSCode.
In order to build the service version and/or the Windows installer you need a to run on Windows.
In addition, the installer project uses the Wix v4 Toolset which will cause an error on loading the .sln in Visual Studio if the [HeatWave for VS2022 Extension](https://marketplace.visualstudio.com/items?itemName=FireGiant.FireGiantHeatWaveDev17) is not installed.
In order to run the integration tests you must have the following environment variables set. To run them more accurately, include the optional ones.
- `TGS_TEST_DATABASE_TYPE`: `MySql`, `MariaDB`, `PostgresSql`, or `SqlServer`.
@@ -70,6 +74,8 @@ If you don't plan on deploying TGS, the following secrets can be omitted:
- Secret `DOCKER_USERNAME`: Login username for Docker image push.
- Secret `DOCKER_PASSWORD`: Login password for Docker image push.
- Secret `NUGET_API_KEY`: Nuget.org API Key for client libraries push.
- Secret `CODE_SIGNING_BASE64`: Base64 string of a .pfx file containing a Windows code-signing certificate.
- Secret `CODE_SIGNING_PASSWORD`: Password for importing the above .pfx.
### Know your Code
@@ -146,10 +152,13 @@ void Hello()
{
if (!thing1)
return;
if (thing2)
return;
if (thing3 != 30)
return;
do stuff
}
```
@@ -16,7 +16,7 @@ concurrency:
jobs:
approve-pr-if-dominion-is-author:
name: Approve PR if Dominion is Author
if: github.event.pull_request.user.login == 'Cyberboss' && !github.event.pull_request.draft && (github.event.pull_request.base.repo.owner.login == 'tgstation' || github.event.pull_request.base.repo.owner.login == 'Cyberboss')
if: github.event.pull_request.user.login == 'Cyberboss' && !github.event.pull_request.draft && !contains(github.event.pull_request.body, '- [ ]') && (github.event.pull_request.base.repo.owner.login == 'tgstation' || github.event.pull_request.base.repo.owner.login == 'Cyberboss')
runs-on: ubuntu-latest
steps:
- name: GitHub API Call
@@ -1,4 +1,21 @@
name: 'CI'
# THE MAIN BIG CHUNGUS
# Does CI on push/PR/cron. Deployments on push when triggered
# - Validates Documentation
# - Builds C# and DMAPI
# - Tests everything on massive matrix
# - Packages
# - Tests package installs/services/uninstalls
# - Properly versions everything based on build/Version.props
# - Checks commit tags for deployment intents
# - Deploys DreamMaker API zip [DMDeploy] (dev/master)
# - Deploys Nuget Packages [NugetDeploy] (dev/master)
# - Deploys HTTP API swagger.json [APIDeploy] (dev/master)
# - Deploys tgstation-server [TGSDeploy] (master)
# - GitHub Releases: https://github.com/tgstation/tgstation-server/releases
# - Docker: https://hub.docker.com/r/tgstation/server
# - apt repo: https://github.com/tgstation/tgstation-ppa
# - winget: https://github.com/microsoft/winget-pkgs/tree/master/manifests/t/Tgstation/Server
name: 'CI Pipeline'
on:
schedule:
@@ -21,6 +38,9 @@ env:
TGS_DOTNET_VERSION: 6.0.x
TGS_TEST_GITHUB_TOKEN: ${{ secrets.LIVE_TESTS_TOKEN }}
TGS_RELEASE_NOTES_TOKEN: ${{ secrets.DEV_PUSH_TOKEN }}
WINGET_PUSH_TOKEN: ${{ secrets.DEV_PUSH_TOKEN }}
PACKAGING_KEYGRIP: ${{ vars.PACKAGING_KEYGRIP }}
PACKAGING_PRIVATE_KEY_PASSPHRASE: ${{ secrets.PACKAGING_PRIVATE_KEY_PASSPHRASE }}
CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }}
concurrency:
@@ -50,7 +70,7 @@ jobs:
run: exit 1
start-ci-run-gate:
name: Start CI Run Gate
name: CI Start Gate
needs: security-checkpoint
runs-on: ubuntu-latest
if: "!(cancelled() || failure()) && (needs.security-checkpoint.result == 'success' || (needs.security-checkpoint.result == 'skipped' && (github.event_name == 'push' || github.event_name == 'schedule' || (github.event.pull_request.head.repo.id == github.event.pull_request.base.repo.id && github.event_name != 'pull_request_target'))))"
@@ -230,11 +250,14 @@ jobs:
with:
ref: "refs/pull/${{ github.event.number }}/merge"
- name: Restore
run: dotnet restore
- name: Build
run: dotnet build -c ${{ matrix.configuration }}NoService
run: dotnet build -c ${{ matrix.configuration }}NoWindows
- name: Run Unit Tests
run: sudo dotnet test --no-build --logger GitHubActions --filter FullyQualifiedName!~TestLiveServer -c ${{ matrix.configuration }}NoService --collect:"XPlat Code Coverage" --settings build/coverlet.runsettings --results-directory ./TestResults tgstation-server.sln
run: sudo dotnet test --no-build --logger GitHubActions --filter FullyQualifiedName!~TestLiveServer -c ${{ matrix.configuration }}NoWindows --collect:"XPlat Code Coverage" --settings build/coverlet.runsettings --results-directory ./TestResults tgstation-server.sln
- name: Store Code Coverage
uses: actions/upload-artifact@v3
@@ -270,11 +293,14 @@ jobs:
with:
ref: "refs/pull/${{ github.event.number }}/merge"
- name: Restore
run: dotnet restore
- name: Build
run: dotnet build -c ${{ matrix.configuration }}
run: dotnet build -c ${{ matrix.configuration }}NoWix
- name: Run Unit Tests
run: dotnet test --no-build --logger GitHubActions --filter FullyQualifiedName!~TestLiveServer -c ${{ matrix.configuration }} --collect:"XPlat Code Coverage" --settings build/coverlet.runsettings --results-directory ./TestResults tgstation-server.sln
run: dotnet test --no-build --logger GitHubActions --filter FullyQualifiedName!~TestLiveServer -c ${{ matrix.configuration }}NoWix --collect:"XPlat Code Coverage" --settings build/coverlet.runsettings --results-directory ./TestResults tgstation-server.sln
- name: Store Code Coverage
uses: actions/upload-artifact@v3
@@ -372,6 +398,9 @@ jobs:
with:
ref: "refs/pull/${{ github.event.number }}/merge"
- name: Restore
run: dotnet restore
- name: Build
run: dotnet build -c ${{ matrix.configuration }} tests/Tgstation.Server.Tests/Tgstation.Server.Tests.csproj
@@ -409,7 +438,6 @@ jobs:
cd ../Tgstation.Server.Host
dotnet publish -c ${{ matrix.configuration }} --no-build -o ../../Artifacts/Service/lib/Default
mv ../../Artifacts/Service/lib/Default/appsettings.yml ../../Artifacts/Service/appsettings.yml
rm ../../Artifacts/Service/lib/Default/Tgstation.Server.Host.exe
- name: Store Server Service
if: ${{ matrix.configuration == 'Release' && matrix.watchdog-type == 'Basic' }}
@@ -418,6 +446,21 @@ jobs:
name: ServerService
path: Artifacts/Service/
- name: Install Code Signing Certificate
if: ${{ matrix.configuration == 'Release' && matrix.watchdog-type == 'Basic' }}
shell: powershell
run: |
$pfxBytes = [convert]::FromBase64String("${{ secrets.CODE_SIGNING_BASE64 }}")
[IO.File]::WriteAllBytes("tg_codesigning.pfx", $pfxBytes)
$certPassword = ConvertTo-SecureString -String "${{ secrets.CODE_SIGNING_PASSWORD }}" -Force -AsPlainText
Import-PfxCertificate -FilePath tg_codesigning.pfx -Cert Cert:\CurrentUser\My -Password $certPassword
rm tg_codesigning.pfx
- name: Test Sign Service .exe
if: ${{ matrix.configuration == 'Release' && matrix.watchdog-type == 'Basic' }}
shell: powershell
run: Set-AuthenticodeSignature Artifacts/Service/Tgstation.Server.Host.Service.exe -Certificate (Get-ChildItem Cert:\CurrentUser\My | Where-Object { $_.Thumbprint -eq "${{ vars.CODE_SIGNING_THUMBPRINT }}" }) -TimestampServer "http://timestamp.digicert.com"
linux-integration-tests:
name: Linux Live Tests
needs: dmapi-build
@@ -524,8 +567,11 @@ jobs:
with:
ref: "refs/pull/${{ github.event.number }}/merge"
- name: Restore
run: dotnet restore
- name: Build
run: dotnet build -c ${{ matrix.configuration }}NoService tests/Tgstation.Server.Tests/Tgstation.Server.Tests.csproj
run: dotnet build -c ${{ matrix.configuration }}NoWindows tests/Tgstation.Server.Tests/Tgstation.Server.Tests.csproj
- name: Cache BYOND .zips
uses: actions/cache@v3
@@ -538,7 +584,7 @@ jobs:
run: |
cd tests/Tgstation.Server.Tests
sleep 10
dotnet test -c ${{ matrix.configuration }}NoService --filter FullyQualifiedName~TestLiveServer --logger GitHubActions --no-build --collect:"XPlat Code Coverage" --settings ../../build/coverlet.runsettings --results-directory ../../TestResults
dotnet test -c ${{ matrix.configuration }}NoWindows --filter FullyQualifiedName~TestLiveServer --logger GitHubActions --no-build --collect:"XPlat Code Coverage" --settings ../../build/coverlet.runsettings --results-directory ../../TestResults
- name: Store Code Coverage
uses: actions/upload-artifact@v3
@@ -552,17 +598,15 @@ jobs:
cd src/Tgstation.Server.Host.Console
dotnet publish -c ${{ matrix.configuration }} -o ../../Artifacts/Console
cd ../Tgstation.Server.Host
dotnet publish -c ${{ matrix.configuration }}NoService --no-build -o ../../Artifacts/Console/lib/Default
dotnet publish -c ${{ matrix.configuration }}NoWindows --no-build -o ../../Artifacts/Console/lib/Default
mv ../../Artifacts/Console/lib/Default/appsettings.yml ../../Artifacts/Console/appsettings.yml
rm ../../Artifacts/Console/lib/Default/Tgstation.Server.Host
- name: Package Server Update Package
if: ${{ matrix.configuration == 'Release' && matrix.watchdog-type == 'System' && matrix.database-type == 'PostgresSql' }}
run: |
cd src/Tgstation.Server.Host
dotnet publish -c ${{ matrix.configuration }}NoService --no-build -o ../../Artifacts/ServerUpdate
dotnet publish -c ${{ matrix.configuration }}NoWindows --no-build -o ../../Artifacts/ServerUpdate
rm ../../Artifacts/ServerUpdate/appsettings.yml
rm ../../Artifacts/ServerUpdate/Tgstation.Server.Host
- name: Store Server Console
if: ${{ matrix.configuration == 'Release' && matrix.watchdog-type == 'System' && matrix.database-type == 'MariaDB' }}
@@ -863,30 +907,31 @@ jobs:
fail_ci_if_error: true
build-deb:
name: Build .deb Package
name: Build .deb Package # Can't do i386 due to https://github.com/dotnet/core/issues/4595
needs: start-ci-run-gate
runs-on: ubuntu-latest
if: "!(cancelled() || failure()) && needs.start-ci-run-gate.result == 'success'"
strategy:
fail-fast: false
matrix:
arch: [ 'amd64' ] # Can't do i386 due to https://github.com/dotnet/core/issues/4595
steps:
- name: Import GPG Key
run: |
sudo dpkg --add-architecture i386
sudo apt-get update
sudo apt-get install -y gnupg2
sudo apt-get install -y -o APT::Immediate-Configure=0 libstdc++6:i386 libgcc-s1:i386 gnupg2
echo "${{ secrets.PACKAGING_PRIVATE_KEY }}" > private.pgp
echo ${{ secrets.PACKAGING_PRIVATE_KEY_PASSPHRASE }} | gpg --batch --yes --passphrase-fd 0 --import private.pgp
rm private.pgp
- name: Checkout
- name: Checkout (Branch)
uses: actions/checkout@v3
if: github.event_name == 'push' || github.event_name == 'schedule'
- name: Checkout (PR Merge)
uses: actions/checkout@v3
if: github.event_name != 'push' && github.event_name != 'schedule'
with:
ref: "refs/pull/${{ github.event.number }}/merge"
- name: Execute Build Script
env:
PACKAGING_KEYGRIP: ${{ vars.PACKAGING_KEYGRIP }}
PACKAGING_PRIVATE_KEY_PASSPHRASE: ${{ secrets.PACKAGING_PRIVATE_KEY_PASSPHRASE }}
run: sudo -E build/package/deb/build_package.sh
- name: Parse TGS version
@@ -899,6 +944,32 @@ jobs:
gpg --verify tgstation-server_${{ env.TGS_VERSION }}-1_amd64.changes
gpg --verify tgstation-server_${{ env.TGS_VERSION }}-1_amd64.buildinfo
- name: Test Install
run: |
sudo mkdir /etc/tgstation-server
sudo cp build/package/appsettings.GitHub.yml /etc/tgstation-server/appsettings.Production.yml
sudo apt-get install -y ./tgstation-server_${{ env.TGS_VERSION }}-1_amd64.deb
sudo ls -al /etc/tgstation-server
sudo cat /etc/tgstation-server/appsettings.Production.yml
sudo cat /etc/tgstation-server/appsettings.yml
ls -al /opt/tgstation-server
cat /opt/tgstation-server/lib/Default/Tgstation.Server.Host.deps.json
cat /usr/bin/tgs-configure
- name: Test Service
run: |
systemctl status tgstation-server
- name: Test Uninstall # Wait 10s for service to initialize
shell: bash
run: |
sleep 10
sudo apt-get remove -y tgstation-server
if [[ -d "/opt/tgstation-server" ]]; then
ls -al /opt/tgstation-server
exit 2
fi
- name: Create Packaging Archive
run: tar cfJ tgstation-server-v${{ env.TGS_VERSION }}.debian.packaging.tar.xz tgstation-server_*
@@ -908,11 +979,186 @@ jobs:
name: packaging-debian
path: tgstation-server-v${{ env.TGS_VERSION }}.debian.packaging.tar.xz
deployment-gate:
name: Deployment Gate
needs: [ dox-build, docker-build, build-deb, validate-openapi-spec, upload-code-coverage ]
build-msi:
name: Build Windows Installer .exe
needs: start-ci-run-gate
runs-on: windows-latest
if: "!(cancelled() || failure()) && needs.start-ci-run-gate.result == 'success'"
steps:
- name: Install winget
uses: Cyberboss/install-winget@v1
with:
GITHUB_TOKEN: ${{ env.WINGET_PUSH_TOKEN }}
- name: Setup dotnet
uses: actions/setup-dotnet@v2
with:
dotnet-version: ${{ env.TGS_DOTNET_VERSION }}
- name: Checkout (Branch)
uses: actions/checkout@v3
if: github.event_name == 'push' || github.event_name == 'schedule'
- name: Checkout (PR Merge)
uses: actions/checkout@v3
if: github.event_name != 'push' && github.event_name != 'schedule'
with:
ref: "refs/pull/${{ github.event.number }}/merge"
- name: Restore Wix dotnet Tool
run: |
cd build/package/winget
dotnet tool restore
- name: Validate winget Manifest
run: winget validate --manifest build/package/winget/manifest
- name: Restore
run: dotnet restore
- name: Build Host
run: dotnet build -c Release src/Tgstation.Server.Host/Tgstation.Server.Host.csproj
- name: Build Service
run: dotnet build -c Release src/Tgstation.Server.Host.Service/Tgstation.Server.Host.Service.csproj
- name: Prepare Artifacts
shell: powershell
run: build/package/winget/prepare_installer_input_artifacts.ps1
- name: Build Installer .exe
run: |
cd build/package/winget/Tgstation.Server.Host.Service.Wix.Bundle
dotnet build -c Release
- name: Install Code Signing Certificate
shell: powershell
run: |
$pfxBytes = [convert]::FromBase64String("${{ secrets.CODE_SIGNING_BASE64 }}")
[IO.File]::WriteAllBytes("tg_codesigning.pfx", $pfxBytes)
$certPassword = ConvertTo-SecureString -String "${{ secrets.CODE_SIGNING_PASSWORD }}" -Force -AsPlainText
Import-PfxCertificate -FilePath tg_codesigning.pfx -Cert Cert:\CurrentUser\My -Password $certPassword
rm tg_codesigning.pfx
- name: Sign Installer .exe for Testing # https://wixtoolset.org/docs/tools/signing/
shell: powershell
run: |
cd build/package/winget
dotnet wix burn detach Tgstation.Server.Host.Service.Wix.Bundle/bin/Release/tgstation-server-installer.exe -engine burnengine.exe
Set-AuthenticodeSignature burnengine.exe -Certificate (Get-ChildItem Cert:\CurrentUser\My | Where-Object { $_.Thumbprint -eq "${{ vars.CODE_SIGNING_THUMBPRINT }}" }) -TimestampServer "http://timestamp.digicert.com"
dotnet wix burn reattach Tgstation.Server.Host.Service.Wix.Bundle/bin/Release/tgstation-server-installer.exe -engine burnengine.exe -o test-installer.exe
Set-AuthenticodeSignature test-installer.exe -Certificate (Get-ChildItem Cert:\CurrentUser\My | Where-Object { $_.Thumbprint -eq "${{ vars.CODE_SIGNING_THUMBPRINT }}" }) -TimestampServer "http://timestamp.digicert.com"
- name: Test Install # Sanity checks the .deps.json exists, which the installation is broken without
shell: powershell # If it's missing, I found that <PrivateAssets> in <PackageReference> elements were the problem
run: |
mkdir C:/ProgramData/tgstation-server
cp build/package/appsettings.GitHub.yml C:/ProgramData/tgstation-server/appsettings.Production.yml
$file = [System.IO.Path]::GetFullPath("build/package/winget/test-installer.exe")
$log = [System.IO.Path]::GetFullPath("install.log")
$procMain = Start-Process -FilePath $file "/install /silent /log `"$log`"" -NoNewWindow -PassThru -Wait
if (Test-Path -Path $log -PathType Leaf) {
Get-Content $log
}
$installCode = $procMain.ExitCode
if($installCode -ne 0) {
Write-Host "ERROR INSTALLER EXIT CODE $installCode"
exit 3
}
if (-Not (Test-Path -Path "C:/Program Files (x86)/tgstation-server/lib/Default/Tgstation.Server.Host.deps.json" -PathType Leaf)) {
exit 2
}
if (-Not (Test-Path -Path "C:/ProgramData/tgstation-server/appsettings.yml" -PathType Leaf)) {
exit 4
}
- name: Test Service
shell: powershell
run: |
$service=Get-Service -Name tgstation-server -ErrorAction SilentlyContinue
if ($service.Length -eq 0) {
exit 3
}
if ($service[0].Status -ne "Running") {
exit 4
}
- name: Test Uninstall # Sanity checks the .deps.json exists, which the installation is broken without
shell: powershell
run: |
$file = [System.IO.Path]::GetFullPath("build/package/winget/test-installer.exe")
$log = [System.IO.Path]::GetFullPath("uninstall.log")
$procMain = Start-Process -FilePath $file "/uninstall /silent /log `"$log`"" -NoNewWindow -PassThru -Wait
if (Test-Path -Path $log -PathType Leaf) {
Get-Content $log
}
$installCode = $procMain.ExitCode
if($installCode -ne 0) {
Write-Host "ERROR INSTALLER EXIT CODE $installCode"
exit 3
}
$service=Get-Service -Name tgstation-server -ErrorAction SilentlyContinue
if ($service.Length -gt 0) {
echo $service
exit 2
}
- name: Upload Unsigned Installer .exe
uses: actions/upload-artifact@v3
with:
name: packaging-preview-windows
path: build/package/winget/Tgstation.Server.Host.Service.Wix.Bundle/bin/Release/tgstation-server-installer.exe
check-winget-pr-template:
name: Check winget-pkgs Pull Request Template is up to date
needs: start-ci-run-gate
if: "!(cancelled() || failure()) && needs.start-ci-run-gate.result == 'success'"
runs-on: ubuntu-latest
if: "!(cancelled() || failure()) && needs.dox-build.result == 'success' && needs.docker-build.result == 'success' && needs.build-deb.result == 'success' && needs.validate-openapi-spec.result == 'success' && needs.upload-code-coverage.result == 'success' && github.event_name == 'push'"
steps:
- name: Setup dotnet
uses: actions/setup-dotnet@v2
with:
dotnet-version: ${{ env.TGS_DOTNET_VERSION }}
- name: Retrieve Latest winget-pkgs PULL_REQUEST_TEMPLATE commit SHA from GitHub API
id: get-sha
run: |
curl -L -u "${{ vars.DEV_PUSH_USERNAME }}:${{ secrets.DEV_PUSH_TOKEN }}" -H "Accept: application/vnd.github.everest-preview+json" -H "Content-Type: application/json" -o commits.json https://api.github.com/repos/microsoft/winget-pkgs/commits?path=.github/PULL_REQUEST_TEMPLATE.md
echo "pr_template_sha=$(cat commits.json | jq '.[0].sha')" >> $GITHUB_OUTPUT
- name: Checkout (Branch)
uses: actions/checkout@v3
if: github.event_name == 'push' || github.event_name == 'schedule'
- name: Checkout (PR Merge)
uses: actions/checkout@v3
if: github.event_name != 'push' && github.event_name != 'schedule'
with:
ref: "refs/pull/${{ github.event.number }}/merge"
- name: Restore
run: dotnet restore
- name: Build ReleaseNotes
run: dotnet build -c Release tools/ReleaseNotes/ReleaseNotes.csproj
- name: Run ReleaseNotes Check
run: dotnet run -c Release --no-build --project tools/ReleaseNotes --winget-template-check ${{ steps.get-sha.outputs.pr_template_sha }}
ci-completion-gate: # This job exists so there isn't a moving target for branch protections
name: CI Completion Gate
needs: [ dox-build, docker-build, build-deb, build-msi, validate-openapi-spec, upload-code-coverage, check-winget-pr-template ]
runs-on: ubuntu-latest
if: "!(cancelled() || failure()) && needs.dox-build.result == 'success' && needs.docker-build.result == 'success' && needs.build-deb.result == 'success' && needs.build-msi.result == 'success' && needs.validate-openapi-spec.result == 'success' && needs.upload-code-coverage.result == 'success' && needs.check-winget-pr-template.result == 'success'"
steps:
- name: GitHub Requires at Least One Step for a Job
run: exit 0
deployment-gate:
name: Deployment Start Gate
needs: ci-completion-gate
runs-on: ubuntu-latest
if: "!(cancelled() || failure()) && needs.ci-completion-gate.result == 'success' && github.event_name == 'push'"
steps:
- name: GitHub Requires at Least One Step for a Job
run: exit 0
@@ -1054,8 +1300,14 @@ jobs:
- name: Checkout
uses: actions/checkout@v3
- name: Restore
run: dotnet restore
- name: Build ReleaseNotes
run: dotnet build -c Release tools/ReleaseNotes/ReleaseNotes.csproj
- name: Run ReleaseNotes with --ensure-release
run: dotnet run -c Release --project tools/ReleaseNotes --ensure-release
run: dotnet run -c Release --no-build --project tools/ReleaseNotes --ensure-release
deploy-tgs:
name: Deploy TGS
@@ -1071,6 +1323,36 @@ jobs:
- name: Checkout
uses: actions/checkout@v3
- name: Restore
run: dotnet restore
- name: Restore Wix dotnet Tool
run: |
cd build/package/winget
dotnet tool restore
- name: Build ReleaseNotes
run: dotnet build -c Release tools/ReleaseNotes/ReleaseNotes.csproj
# We need to rebuild the installer.exe so it can be properly signed
- name: Restore
run: dotnet restore
- name: Build Host
run: dotnet build -c Release src/Tgstation.Server.Host/Tgstation.Server.Host.csproj
- name: Build Service
run: dotnet build -c Release src/Tgstation.Server.Host.Service/Tgstation.Server.Host.Service.csproj
- name: Prepare Artifacts
shell: powershell
run: build/package/winget/prepare_installer_input_artifacts.ps1
- name: Build Installer .exe
run: |
cd build/package/winget/Tgstation.Server.Host.Service.Wix.Bundle
dotnet build -c Release
- name: Parse TGS version
shell: powershell
run: |
@@ -1108,6 +1390,28 @@ jobs:
name: packaging-debian
path: packaging-debian
- name: Install Code Signing Certificate
shell: powershell
run: |
$pfxBytes = [convert]::FromBase64String("${{ secrets.CODE_SIGNING_BASE64 }}")
[IO.File]::WriteAllBytes("tg_codesigning.pfx", $pfxBytes)
$certPassword = ConvertTo-SecureString -String "${{ secrets.CODE_SIGNING_PASSWORD }}" -Force -AsPlainText
Import-PfxCertificate -FilePath tg_codesigning.pfx -Cert Cert:\CurrentUser\My -Password $certPassword
rm tg_codesigning.pfx
- name: Sign Installer .exe # https://wixtoolset.org/docs/tools/signing/
shell: powershell
run: |
cd build/package/winget
dotnet wix burn detach Tgstation.Server.Host.Service.Wix.Bundle/bin/Release/tgstation-server-installer.exe -engine burnengine.exe
Set-AuthenticodeSignature burnengine.exe -Certificate (Get-ChildItem Cert:\CurrentUser\My | Where-Object { $_.Thumbprint -eq "${{ vars.CODE_SIGNING_THUMBPRINT }}" }) -TimestampServer "http://timestamp.digicert.com"
dotnet wix burn reattach Tgstation.Server.Host.Service.Wix.Bundle/bin/Release/tgstation-server-installer.exe -engine burnengine.exe -o tgstation-server-installer.exe
Set-AuthenticodeSignature tgstation-server-installer.exe -Certificate (Get-ChildItem Cert:\CurrentUser\My | Where-Object { $_.Thumbprint -eq "${{ vars.CODE_SIGNING_THUMBPRINT }}" }) -TimestampServer "http://timestamp.digicert.com"
- name: Sign Service .exe
shell: powershell
run: Set-AuthenticodeSignature ServerService/Tgstation.Server.Host.Service.exe -Certificate (Get-ChildItem Cert:\CurrentUser\My | Where-Object { $_.Thumbprint -eq "${{ vars.CODE_SIGNING_THUMBPRINT }}" }) -TimestampServer "http://timestamp.digicert.com"
- name: Zip Artifacts
shell: powershell
run: |
@@ -1118,7 +1422,7 @@ jobs:
&"C:/Program Files/7-Zip/7z.exe" a ServerUpdatePackage.zip ./ServerUpdatePackage/* -tzip
- name: Generate Release Notes
run: dotnet run -c Release --project tools/ReleaseNotes ${{ env.TGS_VERSION }}
run: dotnet run -c Release --no-build --project tools/ReleaseNotes ${{ env.TGS_VERSION }}
- name: Create GitHub Release
uses: actions/create-release@v1
@@ -1191,6 +1495,16 @@ jobs:
asset_name: tgstation-server-v${{ env.TGS_VERSION }}.debian.packaging.tar.xz
asset_content_type: application/x-tar
- name: Upload Installer .exe
uses: actions/upload-release-asset@v1
env:
GITHUB_TOKEN: ${{ secrets.DEV_PUSH_TOKEN }}
with:
upload_url: ${{ steps.create_release.outputs.upload_url }}
asset_path: ./build/package/winget/tgstation-server-installer.exe
asset_name: tgstation-server-installer.exe
asset_content_type: application/octet-stream
deploy-docker:
name: Deploy TGS (Docker)
needs: deploy-tgs
@@ -1232,4 +1546,39 @@ jobs:
- name: Trigger tgstation-ppa workflow
run: |
curl -XPOST -u "${{ vars.DEV_PUSH_USERNAME }}:${{ secrets.DEV_PUSH_TOKEN }}" -H "Accept: application/vnd.github.everest-preview+json" -H "Content-Type: application/json" https://api.github.com/repos/tgstation/tgstation-ppa/actions/workflows/add_tgs_version.yml/dispatches --data '{"ref":"main","inputs":{"tgs_semver": "${{ env.TGS_VERSION }}"}}'
curl -XPOST -u "${{ vars.DEV_PUSH_USERNAME }}:${{ secrets.DEV_PUSH_TOKEN }}" -H "Accept: application/vnd.github.everest-preview+json" -H "Content-Type: application/json" https://api.github.com/repos/tgstation/tgstation-ppa/actions/workflows/add_tgs_version.yml/dispatches --data '{"ref":"main","inputs":{"tgs_semver": "${{ env.TGS_VERSION }}"}
deploy-winget:
name: Deploy TGS (winget)
needs: deploy-tgs
if: "!(cancelled() || failure()) && needs.deploy-tgs.result == 'success'"
runs-on: windows-latest
steps:
- name: Setup dotnet
uses: actions/setup-dotnet@v2
with:
dotnet-version: ${{ env.TGS_DOTNET_VERSION }}
- name: Install winget
uses: Cyberboss/install-winget@v1
with:
GITHUB_TOKEN: ${{ env.WINGET_PUSH_TOKEN }}
- name: Install wingetcreate
run: winget install wingetcreate --disable-interactivity --accept-source-agreements
- name: Checkout
uses: actions/checkout@v3
- name: Build ReleaseNotes
run: dotnet build -c Release tools/ReleaseNotes
- name: Execute Push Script
shell: powershell
run: build/package/winget/push_manifest.ps1
- name: Run ReleaseNotes with --link-winget
shell: powershell
run: |
Sleep 15
dotnet run -c Release --no-build --project tools/ReleaseNotes --link-winget ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}
+1 -1
View File
@@ -35,7 +35,7 @@ jobs:
languages: csharp
- name: Build
run: dotnet build -c ReleaseNoService
run: dotnet build -c ReleaseNoWindows
- name: Perform CodeQL Analysis
uses: github/codeql-action/analyze@v2
+1
View File
@@ -5,6 +5,7 @@ _ReSharper.*
packages/
artifacts/
.vs/
.vscode/
*.user
*.suo
*.userprefs
-9
View File
@@ -1,9 +0,0 @@
{
"recommendations": [
"gbasood.byond-dm-language-support",
"ms-vscode.csharp",
"k--kato.docomment",
"peterjausovec.vscode-docker",
"bbenoist.doxygen"
]
}
-21
View File
@@ -1,21 +0,0 @@
{
// Use IntelliSense to find out which attributes exist for C# debugging
// Use hover for the description of the existing attributes
// For further information visit https://github.com/OmniSharp/omnisharp-vscode/blob/master/debugger-launchjson.md
"version": "0.2.0",
"configurations": [
{
"name": "Debug Server",
"type": "coreclr",
"request": "launch",
"preLaunchTask": "build",
// If you have changed target frameworks, make sure to update the program path.
"program": "${workspaceFolder}/src/Tgstation.Server.Host/bin/Debug/net6.0/Tgstation.Server.Host.dll",
"args": [],
"cwd": "${workspaceFolder}/src/Tgstation.Server.Host",
// For more information about the 'console' field, see https://github.com/OmniSharp/omnisharp-vscode/blob/master/debugger-launchjson.md#console-terminal-window
"console": "externalTerminal",
"stopAtEntry": false
}
]
}
-33
View File
@@ -1,33 +0,0 @@
{
"version": "2.0.0",
"tasks": [
{
"label": "build",
"command": "dotnet",
"type": "process",
"args": [
"msbuild",
"-p:Configuration=DebugNoService"
],
"problemMatcher": "$msCompile",
"group": {
"kind": "build",
"isDefault": true
}
},
{
"label": "test",
"command": "dotnet",
"type": "process",
"args": [
"test",
"-p:Configuration=DebugNoService"
],
"problemMatcher": "$msCompile",
"group": {
"kind": "test",
"isDefault": true
}
}
]
}
+127 -15
View File
@@ -18,40 +18,138 @@ Older server versions can be found in the V# branches of this repository. Note t
### Pre-Requisites
- [ASP .NET Core Runtime (>= v6.0)](https://dotnet.microsoft.com/download/dotnet/6.0) (Choose the option to `Run Server Apps` for your system) If you plan to install tgstation-server as a Windows service, you should also ensure that your .NET Framework runtime version is >= v4.7.2 (Download can be found on same page). Ensure that the `dotnet` executable file is in your system's `PATH` variable (or that of the user's that will be running the server).
- A [MariaDB](https://downloads.mariadb.org/), MySQL, [PostgresSQL](https://www.postgresql.org/download/), or [Microsoft SQL Server](https://www.microsoft.com/en-us/download/details.aspx?id=55994) database engine is required
tgstation-server needs a relational database to store it's data.
If you're just a hobbyist server host, you can probably get away with using SQLite for this. SQLite is bundled with TGS and simply requires you to specify where on your machine you want to store the data.
_HOWEVER_
SQLite is not a battle-ready relational database. It doesn't scale well for any use case. TGS *strongly* recommends you use one of its supported standalone databases. Setting one of these up is more involved but worth the effort.
The supported standalone databases are:
- [MariaDB](https://downloads.mariadb.org/) _- NOTE: If you plan on hosting SpaceStation 13, this is the database most codebases support, making it an ideal choice_
- [PostgresSQL](https://www.postgresql.org/download/)
- [Microsoft SQL Server](https://www.microsoft.com/en-us/download/details.aspx?id=55994)
- MySQL
TGS will require either:
- No pre-existing database WITH schema creation permissions.
or
- Exclusive access to a database schema that TGS has full control over.
### Installation
1. [Download the latest release .zip](https://github.com/tgstation/tgstation-server/releases/latest). The `ServerService` package will only work on Windows. Choose `ServerConsole` if that is not your target OS or you prefer not to use the Windows service.
2. Extract the .zip file to where you want the server to run from. Note the account running the server must have write and delete access to the `lib` subdirectory.
Follow the instructions for your OS below.
#### Windows
If you wish to install the TGS as a service, run `Tgstation.Server.Host.Service.exe`. It should prompt you to install it. Click `Yes` and accept a potential UAC elevation prompt and the setup wizard should run.
###### Note about Digital Signatures
Note that the Windows Service and installer executables requires administrative privileges. These are digitally signed against the Root CA managed by [Jordan Dominion](https://github.com/Cyberboss). Consider installing the certificate into your `Trusted Root Authorities` store for cleaner UAC prompts. The certificate can be downloaded [here](https://file.house/zpFb.cer), please validate the thumbprint is `70176acf7ffa2898fa5b5cd6e38b43b38ea5d07f` before installing.
##### winget (Windows 10 or later)
[winget](https://github.com/microsoft/winget-cli) installed is the easiest way to install the latest version of tgstation-server (provided Microsoft has approved the most recent package manifest).
Check if you have `winget` by running the following command.
```
winget --version
```
If it returns an error that means you don't have winget. You can easily install it by running the following commands in an administrative Windows Powershell instance:
```
Import-Module Appx
Invoke-WebRequest -Uri https://www.nuget.org/api/v2/package/Microsoft.UI.Xaml/2.7.3 -OutFile .\microsoft.ui.xaml.2.7.3.zip
Expand-Archive .\microsoft.ui.xaml.2.7.3.zip
Add-AppxPackage .\microsoft.ui.xaml.2.7.3\tools\AppX\x64\Release\Microsoft.UI.Xaml.2.7.appx
Add-AppxPackage -Path "https://aka.ms/Microsoft.VCLibs.x64.14.00.Desktop.appx"
Add-AppxPackage -Path "https://github.com/microsoft/winget-cli/releases/latest/download/Microsoft.DesktopAppInstaller_8wekyb3d8bbwe.msixbundle"
Remove-Item .\microsoft.ui.xaml.2.7.3\ -r
Remove-Item .\microsoft.ui.xaml.2.7.3.zip
```
Once winget is installed, simply run the following commands, accepting any prompts that may appear:
```ps
winget install tgstation-server
```
The required dotnet runtime may be installed as a pre-requisite.
Note: If you use the `-h` or `--disable-interactivity` winget arguments, you will need to either pre-configure TGS or configure and start the `tgstation-server` service after installing. A shortcut will be placed on your desktop and in your start menu to assist with this.
##### Installer
[Download the latest release's tgstation-server-installer.exe](https://github.com/tgstation/tgstation-server/releases/latest). Executing it will take you through the process of installing and configuring your server. The required dotnet runtime may be installed as a pre-requisite.
Note: If you use the `/silent` or `/passive` arguments to the installer, you will need to either pre-configure TGS or configure and start the `tgstation-server` service after installing. A shortcut will be placed on your desktop and in your start menu to assist with this.
##### Manual
If you don't have it installed already, download and install the [ASP .NET Core Runtime Hosting Bundle (>= v6.0)](https://dotnet.microsoft.com/download/dotnet/6.0). Ensure that the `dotnet` executable file is in your system's `PATH` variable (or that of the user's that will be running the server). You can test this by opening a command prompt and running `dotnet --list-runtimes`.
[Download the latest release .zip](https://github.com/tgstation/tgstation-server/releases/latest). Typically, you want the `ServerService.zip` package in order to run TGS as a Windows service. Choose `ServerConsole.zip` if you prefer to use a command line daemon.
Extract the .zip file to where you want the server to run from. Note the account running the server must have write, execute, and delete access to the `lib` subdirectory.
If you wish to install the TGS as a service, run `Tgstation.Server.Host.Service.exe`. It should prompt you to install it. Click `Yes` and the setup wizard should run.
Should you want a clean start, be sure to first uninstall the service by running `Tgstation.Server.Host.Service.exe -u` from the command line.
If using the console version, run ./tgs.bat in the root of the installation directory. Ctrl+C will close the server, terminating all live game instances.
If using the console version, run `./tgs.bat` in the root of the installation directory. Ctrl+C will close the server, terminating all live game instances.
#### Linux (Native)
#### Linux
Installing natively is the recommended way to run tgstation-server on Linux.
##### Ubuntu
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 \
&& sudo apt update \
&& sudo apt install -y software-properties-common \
&& sudo apt-key adv --keyserver hkp://keyserver.ubuntu.com:80 --recv B6FD15EE7ED77676EAEAF910EEEDC8280A307527 \
&& sudo add-apt-repository -y "deb https://tgstation.github.io/tgstation-ppa/debian unstable main" \
&& sudo apt update \
&& sudo apt install -y tgstation-server \
&& sudo tgs-configure \
&& sudo systemctl start tgstation-server
```
##### Debian
The `aspnetcore-runtime-6.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
The following dependencies are required.
- aspnetcore-runtime-6.0 (Note, not all supported distros have this package, see the links above for official Microsoft installation instructions)
- libc6-i386
- libstdc++6:i386
- gdb (for using gcore to create core dumps)
- gcc-multilib (Only on 64-bit systems)
- gdb (for using gcore to create core dumps)
If you have SystemD, we recommend installing the service unit [here](./build/tgstation-server.service). It assumes TGS is installed into `/opt/tgstation-server` but feel free to adjust it to your needs. Note that the server will need to have it's configuration file setup before running with SystemD.
[Download the latest release .zip](https://github.com/tgstation/tgstation-server/releases/latest). Choose `ServerConsole`.
If you have SystemD installed, we recommend installing the service unit [here](./build/tgstation-server.service). It assumes TGS is installed into `/opt/tgstation-server` and you will be using the but feel free to adjust it to your needs. Note that the server will need to have it's configuration file setup before running with SystemD.
Alternatively, to launch the server in the current shell, run `./tgs.sh` in the root of the installation directory. The process will run in a blocking fashion. SIGQUIT will close the server, terminating all live game instances.
#### Docker (Linux)
##### Docker
tgstation-server supports running in a docker container. The official image repository is located at https://hub.docker.com/r/tgstation/server. It can also be built locally by running `docker build . -f build/Dockerfile -t <your tag name>` in the repository root.
@@ -213,11 +311,13 @@ Note that the live detach for DreamDaemon servers is only supported for updates
For the Windows service version stop the `tgstation-server` service
For the SystemD managed service, use `systemctl stop tgstation-server`. DO NOT USE `systemctl kill` as this can create orphaned processes while leaving TGS running.
For the console version press `Ctrl+C` or send a SIGQUIT to the ORIGINAL dotnet process
For the docker version run `docker stop <your container name>`
### Updating
### Updating the Game
## Integrating
@@ -371,15 +471,27 @@ Instances can be either part of a swarm or not. Once in the database they cannot
tgstation-server is controlled via a RESTful HTTP json API. Documentation on this API can be found [here](https://tgstation.github.io/tgstation-server/api.html). This section serves to document the concepts of the server. The API is versioned separately from the release version. A specification for it can be found in the api-vX.X.X git releases/tags.
### Updating
### Updating TGS
TGS can self update without stopping your DreamDaemon servers. Releases made to this repository are bound by a contract that allows changes of the runtime assemblies without stopping your servers. Database migrations are automatically applied as well. Because of this REVERTING TO LOWER VERSIONS IS NOT OFFICIALLY SUPPORTED, do so at your own risk (check changes made to `/src/Tgstation.Server.Host/Models/Migrations`).
TGS can self update without stopping your DreamDaemon servers. Releases made to this repository are bound by a contract that allows changes of the runtime assemblies without stopping your servers. Database migrations are automatically applied as well. Reverting to lower versions works but only works so far back in time, do so at your own risk (check changes made to `/src/Tgstation.Server.Host/Models/Migrations`).
Major version updates may require additional action on the part of the user (apart from the configuration changes).
#### Linux Notes
If TGS was installed via a package manager, using the TGS self updater will cause the version to change without notifying said package manager. This is not necessarily a problem if you're okay with the deviation.
To avoid this, use the package manager to update TGS. It is just as seamless as the self-updater.
##### apt
```sh
sudo apt update && sudo apt upgrade -y
```
#### Notifications
If a server update is available, it will be indicated in the response from the GET /Administration endpoint. For more active notifications, you can subscribe to [this GitHub discussion](https://github.com/tgstation/tgstation-server/discussions/1322).
If a server update is available, it will be indicated in the response from the GET /Administration endpoint and shown as a green exclamation mark in the webpanel navbar. For more active notifications, you can subscribe to [this GitHub discussion](https://github.com/tgstation/tgstation-server/discussions/1322).
### Users
+1 -1
View File
@@ -2,7 +2,7 @@
<Import Project="Version.props" />
<PropertyGroup>
<TgsNetVersion>net6.0</TgsNetVersion>
<TgsFrameworkVersion>net$(TgsNetMajorVersion).0</TgsFrameworkVersion>
<TgsNugetNetVersion>netstandard2.0</TgsNugetNetVersion>
<LangVersion>latest</LangVersion>
<DebugType>Full</DebugType>
+2
View File
@@ -7,6 +7,7 @@
<Company>/tg/station 13</Company>
<PackageProjectUrl>https://tgstation.github.io/tgstation-server</PackageProjectUrl>
<PackageLicenseFile>LICENSE</PackageLicenseFile>
<PackageReadmeFile>README.md</PackageReadmeFile>
<PackageIcon>tgs.png</PackageIcon>
<RepositoryType>Git</RepositoryType>
<RepositoryUrl>https://github.com/tgstation/tgstation-server</RepositoryUrl>
@@ -17,6 +18,7 @@
</PropertyGroup>
<ItemGroup>
<None Include="README.md" Pack="true" PackagePath="" />
<None Include="../../LICENSE" Pack="true" PackagePath="" />
<None Include="../../build/tgs.png" Pack="true" PackagePath="" />
</ItemGroup>
+5 -1
View File
@@ -13,6 +13,10 @@
<TgsInteropVersion>5.6.1</TgsInteropVersion>
<TgsHostWatchdogVersion>1.3.0</TgsHostWatchdogVersion>
<TgsContainerScriptVersion>1.2.1</TgsContainerScriptVersion>
<TgsMigratorVersion>1.0.1</TgsMigratorVersion>
<TgsMigratorVersion>1.0.2</TgsMigratorVersion>
<TgsNetMajorVersion>6</TgsNetMajorVersion>
<!-- Update this frequently with dotnet runtime patches. MAJOR MUST MATCH ABOVE! -->
<!-- You also need to update the .Bundle project's ExePackagePayload hash and file size after changing it -->
<TgsRedistUrl>https://dotnetcli.azureedge.net/dotnet/aspnetcore/Runtime/6.0.19/dotnet-hosting-6.0.19-win.exe</TgsRedistUrl>
</PropertyGroup>
</Project>
+26
View File
@@ -0,0 +1,26 @@
# Barebones config file used for testing service installs in GitHub actions
Database:
DatabaseType: Sqlite
ConnectionString: Data Source=/tgs.sqlite3;Mode=ReadWriteCreate
General:
ConfigVersion: 4.6.0
ApiPort: 5000
GitHubAccessToken:
SetupWizardMode: Never
ByondTopicTimeout: 5000
RestartTimeoutMinutes: 1
ShutdownTimeoutMinutes: 300
UseBasicWatchdog: false
HostApiDocumentation: true
SkipAddingByondFirewallException: false
DeploymentDirectoryCopyTasksPerCore:
MinimumPasswordLength: 15
InstanceLimit: 10
UserLimit: 100
UserGroupLimit: 25
ValidInstancePaths:
FileLogging:
Directory:
Disable: false
LogLevel: Trace
MicrosoftLogLevel: Warning
+2
View File
@@ -0,0 +1,2 @@
# tgstation-server configuration file
# See appsettings.yml or README.md for details on individual configuration options
+4 -6
View File
@@ -1,9 +1,7 @@
#!/usr/bin/make -f
install:
mkdir -p $(DESTDIR)/opt/tgstation-server
cp -r artifacts/* $(DESTDIR)/opt/tgstation-server
uninstall:
mkdir -p $(DESTDIR)/opt/tgstation-server
cp -r artifacts/* $(DESTDIR)/opt/tgstation-server/
build/package/deb/install_artifacts.sh "$(DESTDIR)"
install -D build/package/appsettings.Initial.yml "$(DESTDIR)/etc/tgstation-server/appsettings.Production.yml"
install src/Tgstation.Server.Host/appsettings.yml "$(DESTDIR)/etc/tgstation-server/appsettings.yml"
install -D build/package/deb/tgs-configure "$(DESTDIR)/usr/bin/tgs-configure"
+1
View File
@@ -22,6 +22,7 @@ Depends:
libstdc++6 [i386],
gcc-multilib [amd64],
Recommends:
libsystemd0,
gdb,
Description: A production scale tool for BYOND server management
This is a toolset to manage production BYOND servers. It includes the ability to update the server without having to stop or shutdown the server (the update will take effect on a "reboot" of the server), the ability to start the server and restart it if it crashes, as well as systems for managing code and game files, and locally merging GitHub Pull Requests for test deployments.
+1 -1
View File
@@ -7,4 +7,4 @@ Files:
*
Copyright:
2023 Jordan Dominion <Cyberboss@users.noreply.github.com>
License: AGPL-3
License: AGPL-3.0
+17 -5
View File
@@ -1,7 +1,19 @@
#!/bin/bash
pushd /opt/tgstation-server
dotnet /opt/tgstation-server/lib/Default/Tgstation.Server.Host.dll General:SetupWizardMode=Only
popd
#!/bin/sh -e
#DEBHELPER#
if [ "$1" = "configure" ]; then
chmod 600 /etc/tgstation-server
deb-systemd-helper stop 'tgstation-server.service' >/dev/null || true
echo " _ _ _ _ "
echo " | |_ __ _ ___| |_ __ _| |_(_) ___ _ __ ___ ___ _ ____ _____ _ __ "
echo " | __/ _\` / __| __/ _\` | __| |/ _ \\| '_ \\ _____/ __|/ _ \\ '__\\ \\ / / _ \\ '__|"
echo " | || (_| \\__ \\ || (_| | |_| | (_) | | | |_____\\__ \\ __/ | \\ V / __/ | "
echo " \\__\\__, |___/\\__\\__,_|\\__|_|\\___/|_| |_| |___/\\___|_| \\_/ \\___|_| "
echo " |___/ "
echo "tgstation-server is now installed but must first be configured"
echo "Run 'sudo tgs-configure' to interactively configure your server"
echo "Alternatively, edit '/etc/tgstation-server/appsettings.Production.yml' to your desired specifications"
echo "Once complete, run 'sudo systemctl start tgstation-server' to start the service"
fi
+1 -1
View File
@@ -1,4 +1,4 @@
#!/bin/bash
#!/bin/sh -e
#DEBHELPER#
+3 -4
View File
@@ -7,14 +7,13 @@ export DH_VERBOSE = 1
override_dh_auto_clean:
rm -rf artifacts
dotnet clean -c ReleaseNoService
dotnet clean -c ReleaseNoWindows
override_dh_auto_build:
dotnet restore
cd src/Tgstation.Server.Host.Console && dotnet publish -c Release -o ../../artifacts
cd src/Tgstation.Server.Host && dotnet publish -c Release -o ../../artifacts/lib/Default
mv artifacts/lib/Default/appsettings.yml artifacts/appsettings.yml
rm artifacts/lib/Default/Tgstation.Server.Host
rm artifacts/lib/Default/appsettings.yml
override_dh_auto_install:
cp build/package/deb/MakeInstall ./Makefile
@@ -25,4 +24,4 @@ override_dh_strip:
override_dh_shlibdeps:
override_dh_installsystemd:
dh_installsystemd -v --name=tgstation-server --restart-after-upgrade
dh_installsystemd -v --restart-after-upgrade
+3
View File
@@ -0,0 +1,3 @@
#!/usr/bin/sh -e
cd artifacts && for f in $(find * -type f); do install -D "$f" "$1/opt/tgstation-server/$f"; done
+5
View File
@@ -0,0 +1,5 @@
#!/bin/sh
cd /opt/tgstation-server
export General__SetupWizardMode=Only
exec /usr/bin/dotnet /opt/tgstation-server/lib/Default/Tgstation.Server.Host.dll
@@ -0,0 +1,12 @@
{
"version": 1,
"isRoot": true,
"tools": {
"wix": {
"version": "4.0.1",
"commands": [
"wix"
]
}
}
}
@@ -0,0 +1,25 @@
<Wix xmlns="http://wixtoolset.org/schemas/v4/wxs" xmlns:bal="http://wixtoolset.org/schemas/v4/wxs/bal" xmlns:netfx="http://wixtoolset.org/schemas/v4/wxs/netfx">
<Bundle Name="tgstation-server" Manufacturer="/tg/station 13" Version="$(var.ProductVersion)" AboutUrl="https://github.com/tgstation/tgstation-server/blob/tgstation-server-v$(var.ProductVersion)/LICENSE" IconSourceFile="../../../tgs.ico" HelpUrl="https://github.com/tgstation/tgstation-server/discussions/categories/q-a" UpdateUrl="https://github.com/tgstation/tgstation-server/releases/latest" UpgradeCode="542535f4-49ad-45c4-9b96-6d8235ed8b87">
<BootstrapperApplication>
<bal:WixStandardBootstrapperApplication LicenseUrl="https://github.com/tgstation/tgstation/tree/tgstation-server-v$(var.ProductVersion)" Theme="hyperlinkLicense" LogoFile="../../../tgs.ico" ShowVersion="yes" SuppressOptionsUI="yes" />
</BootstrapperApplication>
<netfx:DotNetCoreSearch RuntimeType="aspnet" Platform="x64" MajorVersion="$(var.NetMajorVersion)" Variable="AspNetCorex64Status" />
<netfx:DotNetCoreSearch RuntimeType="aspnet" Platform="x86" MajorVersion="$(var.NetMajorVersion)" Variable="AspNetCorex86Status" />
<netfx:DotNetCoreSearch RuntimeType="core" Platform="x64" MajorVersion="$(var.NetMajorVersion)" Variable="NetCorex64Status" />
<netfx:DotNetCoreSearch RuntimeType="core" Platform="x86" MajorVersion="$(var.NetMajorVersion)" Variable="NetCorex86Status" />
<WixVariable Id="AspNetCoreDetectCondition" Value="(NetCorex64Status AND AspNetCorex64Status) OR (NetCorex86Status AND AspNetCorex86Status)" />
<Chain>
<ExePackage PerMachine="yes" DetectCondition="!(wix.AspNetCoreDetectCondition)" Vital="yes" Permanent="yes" Protocol="burn" InstallArguments="/install /quiet /norestart /log [HOSTING_BUNDLE_LOG_PATH]" RepairArguments="/repair /quiet /norestart /log [HOSTING_BUNDLE_LOG_PATH]" UninstallArguments="/uninstall /quiet /norestart /log [HOSTING_BUNDLE_LOG_PATH]" Cache="remove" LogPathVariable="HOSTING_BUNDLE_LOG_PATH" RepairCondition="FALSE">
<ExePackagePayload Name="Microsoft.DotNet.HostingBundle.$(var.NetMajorVersion)" DownloadUrl="$(var.AspNetRedistUrl)" Hash="FADC48A76561A7ABD5CC599DB737AFC2B1638013C95E29CFC8955CACA1D57004BD9078C6EF9696BECFD75BAA2983FDC2FF4382C9EBDF833A11316C2DEC218884" Size="71391856" Compressed="no" />
</ExePackage>
<MsiPackage SourceFile="$(var.Tgstation.Server.Host.Service.Wix.TargetPath)">
<MsiProperty Name="WIX_BOOTSTRAPPER_UILEVEL" Value="[WixBundleUILevel]" />
</MsiPackage>
</Chain>
</Bundle>
</Wix>
@@ -0,0 +1,29 @@
<Project Sdk="WixToolset.Sdk/4.0.1" xmlns:bal="http://schemas.microsoft.com/wix/BalExtension">
<Import Project="../../../Common.props" />
<PropertyGroup>
<DefineConstants>ProductVersion=$(TgsCoreVersion);NetMajorVersion=$(TgsNetMajorVersion);AspNetRedistUrl=$(TgsRedistUrl)</DefineConstants>
<OutputType>Bundle</OutputType>
<Platforms>x86</Platforms>
<OutputName>tgstation-server-installer</OutputName>
<!-- For debugging, this is done externally by CI -->
<!--
<SignOutput>true</SignOutput>
<CertificateThumbprint>A478CB55A37A135913F6C2A19A45FD8E1A9F3633</CertificateThumbprint>
-->
</PropertyGroup>
<Target Name="SignBundleEngine">
<Message Importance="high" Text="SignBundleEngine: @(SignBundleEngine)" />
<Exec Command='powershell.exe NonInteractive ExecutionPolicy Unrestricted Command "Set-AuthenticodeSignature %(SignBundleEngine.FullPath) -Certificate (Get-ChildItem Cert:\CurrentUser\My | Where-Object { $_.Thumbprint -eq \"$(CertificateThumbprint)\" }) -TimestampServer http://timestamp.digicert.com"' />
</Target>
<Target Name="SignBundle">
<Message Importance="high" Text="SignBundle: @(SignBundle)" />
<Exec Command='powershell.exe NonInteractive ExecutionPolicy Unrestricted Command "Set-AuthenticodeSignature %(SignBundle.FullPath) -Certificate (Get-ChildItem Cert:\CurrentUser\My | Where-Object { $_.Thumbprint -eq \"$(CertificateThumbprint)\" }) -TimestampServer http://timestamp.digicert.com"' />
</Target>
<ItemGroup>
<PackageReference Include="WixToolset.Bal.wixext" Version="4.0.1" />
<PackageReference Include="WixToolset.Netfx.wixext" Version="4.0.1" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\Tgstation.Server.Host.Service.Wix\Tgstation.Server.Host.Service.Wix.wixproj" />
</ItemGroup>
</Project>
@@ -0,0 +1,7 @@
<?xml version="1.0" encoding="utf-8" ?>
<configuration>
<startup useLegacyV2RuntimeActivationPolicy="true">
<supportedRuntime version="v4.0" />
<supportedRuntime version="v2.0.50727" />
</startup>
</configuration>
@@ -0,0 +1,76 @@
namespace Tgstation.Server.Host.Service.Wix.SafeShutdown
{
using System;
using System.ServiceProcess;
using Tgstation.Server.Host.Common;
using WixToolset.Dtf.WindowsInstaller;
/// <summary>
/// Extension methods for the .msi installer.
/// </summary>
public static class InstallationExtensions
{
/// <summary>
/// Attempts to detach stop the existing tgstation-server service if it exists.
/// </summary>
/// <param name="session">The installer <see cref="Session"/>.</param>
/// <returns>The <see cref="ActionResult"/> of the custom action.</returns>
[CustomAction]
public static ActionResult DetachStopTgsServiceIfRunning(Session session)
{
if (session == null)
throw new ArgumentNullException(nameof(session));
try
{
session.Log("Begin DetachStopTgsServiceIfRunning");
ServiceController serviceController = null;
session.Log($"Searching for {Constants.CanonicalPackageName} service...");
foreach (var controller in ServiceController.GetServices())
if (controller.ServiceName == Constants.CanonicalPackageName)
{
serviceController = controller;
break;
}
else
controller.Dispose();
using (serviceController)
{
if (serviceController == null || serviceController.Status != ServiceControllerStatus.Running)
{
session.Log($"{Constants.CanonicalPackageName} service not found. Continuing.");
return ActionResult.Success;
}
var commandId = PipeCommands.GetCommandId(
PipeCommands.CommandDetachingShutdown)
.Value;
session.Log($"{Constants.CanonicalPackageName} service found. Sending command \"{PipeCommands.CommandDetachingShutdown}\" ({commandId})...");
serviceController.ExecuteCommand(commandId);
session.Log($"Command sent. Waiting for {Constants.CanonicalPackageName} service to stop...");
serviceController.WaitForStatus(ServiceControllerStatus.Stopped, TimeSpan.FromMinutes(1));
var stopped = serviceController.Status == ServiceControllerStatus.Stopped;
session.Log($"{Constants.CanonicalPackageName} stopped {(stopped ? String.Empty : "un")}successfully.");
return stopped
? ActionResult.Success
: ActionResult.NotExecuted;
}
}
catch (Exception ex)
{
session.Log($"Exception in DetachStopTgsServiceIfRunning:{Environment.NewLine}{ex}");
return ActionResult.Failure;
}
}
}
}
@@ -0,0 +1,27 @@
<Project Sdk="Microsoft.NET.Sdk">
<Import Project="../../../Common.props" />
<PropertyGroup>
<TargetFramework>net2.0</TargetFramework>
<Version>$(TgsCoreVersion)</Version>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="WixToolset.Dtf.CustomAction" Version="4.0.1" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\..\..\..\src\Tgstation.Server.Host.Common\Tgstation.Server.Host.Common.csproj">
<Private>True</Private>
</ProjectReference>
</ItemGroup>
<ItemGroup>
<Content Include="CustomAction.config" CopyToOutputDirectory="PreserveNewest" />
</ItemGroup>
<ItemGroup>
<Reference Include="System.ServiceProcess" />
</ItemGroup>
</Project>
@@ -0,0 +1,11 @@
<!--
This file contains the declaration of all the localizable strings.
-->
<WixLocalization xmlns="http://wixtoolset.org/schemas/v4/wxl" Culture="en-US">
<String Id="DowngradeError" Value="A newer version of [ProductName] is already installed." />
<String Id="MainServerFeatureDescription" Value="The primary application for running [ProductName]." />
<String Id="ServerServiceDescription" Value="[ProductName] running as a Windows service." />
<String Id="ConfigureRestartDescription" Value="Runs the tgstation-server setup wizard and restarts the service (Does not stop DreamDaemon processes)." />
<String Id="ConfigureRestartName" Value="Configure &amp; Restart tgstation-server" />
</WixLocalization>
@@ -0,0 +1,87 @@
<Wix xmlns="http://wixtoolset.org/schemas/v4/wxs" xmlns:util="http://wixtoolset.org/schemas/v4/wxs/util">
<Package Name="tgstation-server" Manufacturer="/tg/station 13" Version="$(var.ProductVersion)" UpgradeCode="36912c45-5953-4f16-ad7d-17a150ea3587" Scope="perMachine">
<MajorUpgrade DowngradeErrorMessage="!(loc.DowngradeError)" />
<MediaTemplate EmbedCab="yes" />
<Binary Id="InstallerExtensionsBinary" SourceFile="$(var.Tgstation.Server.Host.Service.Wix.Extensions.TargetName).CA.dll" />
<CustomAction Id="DetachStopTgsServiceIfRunningAction" BinaryRef="InstallerExtensionsBinary" DllEntry="DetachStopTgsServiceIfRunning" Execute="deferred" />
<CustomAction Id="RunTgsConfigure" Execute="deferred" FileRef="ServiceExecutableFile" ExeCommand="-c -p=--appsettings-base-path=[APPLICATIONDATADIRECTORY]" />
<Icon Id="tgs.ico" SourceFile="../../../tgs.ico" />
<SetProperty Id="WIX_BOOTSTRAPPER_UILEVEL" Value="[UILevel]" After="CostFinalize" Condition="WIX_BOOTSTRAPPER_UILEVEL=&quot;&quot;" />
<InstallExecuteSequence>
<Custom Action="DetachStopTgsServiceIfRunningAction" Condition="UPGRADINGPRODUCTCODE OR REINSTALL" Before="StopServices" />
<Custom Action="RunTgsConfigure" Before="StartServices" Condition="(NOT REMOVE~=&quot;ALL&quot;) AND (NOT PRODUCTIONAPPSETTINGSPRESENT) AND ((WIX_BOOTSTRAPPER_UILEVEL >= 4) OR (UILevel >= 4))" />
<StartServices Condition="(NOT REMOVE~=&quot;ALL&quot;) AND ((WIX_BOOTSTRAPPER_UILEVEL >= 4) OR (UILevel >= 4) OR PRODUCTIONAPPSETTINGSPRESENT)"/>
</InstallExecuteSequence>
<Feature Id="MainServerFeature" AllowAbsent="no" Description="!(loc.MainServerFeatureDescription)">
<ComponentGroupRef Id="InitialHostComponentGroup"/>
<ComponentGroupRef Id="ServiceHostWatchdogComponentGroup"/>
<ComponentRef Id="StoreApplicationDataDirectoryPathComponent"/>
<ComponentRef Id="ServiceHostWatchdogExeComponent"/>
<ComponentRef Id="PurgeLibDirectoryComponent"/>
<ComponentRef Id="BaseAppSettingsComponent"/>
<ComponentRef Id="ProductionAppSettingsComponent"/>
</Feature>
<Property Id="LIBDIRECTORY">
<RegistrySearch Key="SOFTWARE\tgstation\tgstation-server" Root="HKLM" Type="raw" Id="StoredLibDirectory" Name="LibDirectory" />
</Property>
<Property Id="APPLICATIONDATADIRECTORY">
<RegistrySearch Key="SOFTWARE\tgstation\tgstation-server" Root="HKLM" Type="raw" Id="StoredProgramDataDirectory" Name="ProgramDataDirectory" />
</Property>
<Property Id="PRODUCTIONAPPSETTINGSPRESENT">
<DirectorySearch Path="[CommonAppDataFolder]" Depth="0" Id="ProgramDataDirectorySearcher">
<DirectorySearch Path="!(bind.Property.ProductName)" Depth="0" Id="ApplicationDataDirectorySearcher">
<FileSearch Name="appsettings.Production.yml" />
</DirectorySearch>
</DirectorySearch>
</Property>
<StandardDirectory Id="ProgramFiles6432Folder">
<Directory Id="ApplicationDirectory" Name="!(bind.Property.ProductName)">
<Directory Id="LIBDIRECTORY" Name="lib">
<Directory Id="DefaultLibDirectory" Name="Default" />
<Component Id="PurgeLibDirectoryComponent" Guid="38E323E2-0404-4ACB-ACDD-80F5525538FB">
<RegistryValue Root="HKLM" Key="SOFTWARE\tgstation\tgstation-server" Name="LibDirectory" Type="string" Value="[LIBDIRECTORY]" KeyPath="yes" />
<util:RemoveFolderEx Id="PurgeLibDirectory" On="both" Property="LIBDIRECTORY" />
</Component>
</Directory>
<Component Id="ServiceHostWatchdogExeComponent" Guid="9430D56D-EEBF-4026-9EC1-693668A6EC7C" >
<File Id="ServiceExecutableFile" Source="Tgstation.Server.Host.Service.exe" KeyPath="yes" />
<Shortcut Advertise="yes" Arguments="-c -x -r -p=--appsettings-base-path=[APPLICATIONDATADIRECTORY]" Description="!(loc.ConfigureRestartDescription)" Name="!(loc.ConfigureRestartName)" Directory="DesktopFolder" Icon="tgs.ico" />
<Shortcut Advertise="yes" Arguments="-c -x -r -p=--appsettings-base-path=[APPLICATIONDATADIRECTORY]" Description="!(loc.ConfigureRestartDescription)" Name="!(loc.ConfigureRestartName)" Directory="StartMenuFolder" Icon="tgs.ico" />
<ServiceInstall Name="!(bind.Property.ProductName)" DisplayName="!(bind.Property.ProductName)" Description="!(loc.ServerServiceDescription)" Start="auto" Type="ownProcess" ErrorControl="normal" Account="LocalSystem" Vital="yes" Interactive="no" Arguments="-p=--appsettings-base-path=[APPLICATIONDATADIRECTORY]">
<ServiceDependency Id="Tcpip" />
<ServiceDependency Id="Dhcp" />
<ServiceDependency Id="Dnscache" />
</ServiceInstall>
<ServiceControl Name="!(bind.Property.ProductName)" Remove="uninstall" Start="install" Stop="uninstall" Wait="yes" />
</Component>
</Directory>
</StandardDirectory>
<StandardDirectory Id="CommonAppDataFolder">
<Directory Id="APPLICATIONDATADIRECTORY" Name="!(bind.Property.ProductName)">
<Component Id="StoreApplicationDataDirectoryPathComponent" Guid="86B0EB79-DAC9-4325-8965-42089ABED375">
<RegistryValue Root="HKLM" Key="SOFTWARE\tgstation\tgstation-server" Name="ProgramDataDirectory" Type="string" Value="[APPLICATIONDATADIRECTORY]" KeyPath="yes" />
</Component>
<Component Id="BaseAppSettingsComponent" Guid="" NeverOverwrite="yes">
<File Source="../../../../src/Tgstation.Server.Host/appsettings.yml" />
</Component>
<Component Id="ProductionAppSettingsComponent" Guid="" NeverOverwrite="yes">
<File Source="../../appsettings.Initial.yml" Name="appsettings.Production.yml">
<PermissionEx Sddl="D:PAI(A;;FA;;;SY)(A;;FA;;;BA)" />
</File>
</Component>
</Directory>
</StandardDirectory>
</Package>
</Wix>
@@ -0,0 +1,34 @@
<Project Sdk="WixToolset.Sdk/4.0.1">
<Import Project="../../../Common.props" />
<PropertyGroup>
<DefineConstants>ProductVersion=$(TgsCoreVersion)</DefineConstants>
<HarvestDirectorySuppressSpecificWarnings>5150;5151</HarvestDirectorySuppressSpecificWarnings>
<Platforms>x86</Platforms>
<OutputName>tgstation-server</OutputName>
</PropertyGroup>
<ItemGroup>
<BindPath Include="../Tgstation.Server.Host.Service.Wix.Extensions/$(Configuration)/net472" />
<BindPath Include="../../../../artifacts" />
<BindPath Include="../../../../artifacts/Tgstation.Server.Host" />
<BindPath Include="../../../../artifacts/Tgstation.Server.Host.Service" />
<HarvestDirectory Include="../../../../artifacts/Tgstation.Server.Host">
<ComponentGroupName>InitialHostComponentGroup</ComponentGroupName>
<DirectoryRefId>DefaultLibDirectory</DirectoryRefId>
<SuppressCom>true</SuppressCom>
<SuppressRegistry>true</SuppressRegistry>
<SuppressRootDirectory>true</SuppressRootDirectory>
</HarvestDirectory>
<HarvestDirectory Include="../../../../artifacts/Tgstation.Server.Host.Service">
<ComponentGroupName>ServiceHostWatchdogComponentGroup</ComponentGroupName>
<DirectoryRefId>ApplicationDirectory</DirectoryRefId>
<SuppressRootDirectory>true</SuppressRootDirectory>
</HarvestDirectory>
</ItemGroup>
<ItemGroup>
<PackageReference Include="WixToolset.Heat" Version="4.0.1" />
<PackageReference Include="WixToolset.Util.wixext" Version="4.0.1" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\Tgstation.Server.Host.Service.Wix.Extensions\Tgstation.Server.Host.Service.Wix.Extensions.csproj" />
</ItemGroup>
</Project>
@@ -0,0 +1,31 @@
# Created using wingetcreate 1.2.6.0
# yaml-language-server: $schema=https://aka.ms/winget-manifest.installer.1.4.0.schema.json
PackageIdentifier: Tgstation.Server
PackageVersion: 0.22.475 # Do not change. Set before publish by push_manifest.ps1
Installers:
- InstallerUrl: https://github.com/tgstation/tgstation-server/releases/download/tgstation-server-v0.22.475/tgstation-server-installer.exe # Do not change. Set before publish by push_manifest.ps1
InstallerLocale: en-US
Architecture: x86
InstallerType: burn
Scope: machine
InstallerSha256: CF0D4D2FD042098D826A226A05A9DAA5C792318CF48FD334F7FC06AF6A8A23B1 # Do not change. Set before publish by push_manifest.ps1
InstallModes:
- interactive
- silent
- silentWithProgress
ProductCode: '{D24887FA-3228-4509-B5F3-4E07E349F278}'
UnsupportedOSArchitectures:
- arm
- arm64
ElevationRequirement: elevatesSelf
AppsAndFeaturesEntries:
- DisplayName: tgstation-server
DisplayVersion: 0.22.475 # Do not change. Set before publish by push_manifest.ps1
Publisher: /tg/station 13
Dependencies:
PackageDependencies:
- PackageIdentifier: Microsoft.DotNet.AspNetCore.6
ReleaseDate: 2023-06-24 # Do not change. Set before publish by push_manifest.ps1
ManifestType: installer
ManifestVersion: 1.4.0
@@ -0,0 +1,21 @@
# Created using wingetcreate 1.2.6.0
# yaml-language-server: $schema=https://aka.ms/winget-manifest.defaultLocale.1.4.0.schema.json
PackageIdentifier: Tgstation.Server
PackageVersion: 0.22.475 # Do not change. Set before publish by push_manifest.ps1
PackageLocale: en-US
Publisher: /tg/station 13
PublisherUrl: https://github.com/tgstation/tgstation-server
PublisherSupportUrl: https://github.com/tgstation/tgstation-server/discussions/categories/q-a
Author: Dominion/Cyberboss
PackageName: tgstation-server
License: AGPL-3.0
ShortDescription: A production scale tool for BYOND server management
Moniker: tgstation-server
Documentations:
- DocumentLabel: README.md
DocumentUrl: https://github.com/tgstation/tgstation-server/blob/tgstation-server-v0.22.475/README.md # Do not change. Set before publish by push_manifest.ps1
ReleaseNotesUrl: https://github.com/tgstation/tgstation-server/releases/tag/tgstation-server-v0.22.475
PurchaseUrl: https://github.com/sponsors/Cyberboss
ManifestType: defaultLocale
ManifestVersion: 1.4.0
@@ -0,0 +1,8 @@
# Created using wingetcreate 1.2.6.0
# yaml-language-server: $schema=https://aka.ms/winget-manifest.version.1.4.0.schema.json
PackageIdentifier: Tgstation.Server
PackageVersion: 0.22.475 # Do not change. Set before publish by push_manifest.ps1
DefaultLocale: en-US
ManifestType: version
ManifestVersion: 1.4.0
@@ -0,0 +1,45 @@
# Note: This script requires that Tgstation.Server.Host and Tgstation.Server.Host.Service be built in Release configuration beforehand
$ErrorActionPreference="stop"
$startDirectory=$pwd
try
{
Remove-Item -Recurse -Force artifacts -ErrorAction SilentlyContinue
Remove-Item -Recurse -Force build/package/winget/Tgstation.Server.Host.Service.Wix/bin -ErrorAction SilentlyContinue
Remove-Item -Recurse -Force build/package/winget/Tgstation.Server.Host.Service.Wix/obj -ErrorAction SilentlyContinue
Remove-Item -Recurse -Force build/package/winget/Tgstation.Server.Host.Service.Wix.Bundle/bin -ErrorAction SilentlyContinue
Remove-Item -Recurse -Force build/package/winget/Tgstation.Server.Host.Service.Wix.Bundle/obj -ErrorAction SilentlyContinue
[XML]$versionXML = Get-Content build/Version.props -ErrorAction Stop
$redistUrl = $versionXML.Project.PropertyGroup.TgsRedistUrl
mkdir artifacts
$previousProgressPreference = $ProgressPreference
$ProgressPreference = 'SilentlyContinue'
try
{
Invoke-WebRequest -Uri $redistUrl -OutFile artifacts/hosting-bundle.exe
} finally {
$ProgressPreference = $previousProgressPreference
}
cd src/Tgstation.Server.Host
dotnet publish -c Release --no-build -o ../../artifacts/Tgstation.Server.Host
if (-Not $?) {
exit $lastexitcode
}
cd ../Tgstation.Server.Host.Service
dotnet publish -c Release --no-build -o ../../artifacts/Tgstation.Server.Host.Service
if (-Not $?) {
exit $lastexitcode
}
mv ../../artifacts/Tgstation.Server.Host.Service/Tgstation.Server.Host.Service.exe ../../artifacts/
}
finally
{
cd $startDirectory
}
+44
View File
@@ -0,0 +1,44 @@
$ErrorActionPreference="Stop"
[XML]$versionXML = Get-Content build/Version.props -ErrorAction Stop
$tgsVersion = $versionXML.Project.PropertyGroup.TgsCoreVersion
mkdir artifacts
$previousProgressPreference = $ProgressPreference
$ProgressPreference = 'SilentlyContinue'
try
{
Invoke-WebRequest -Uri "https://github.com/tgstation/tgstation-server/releases/download/tgstation-server-v$tgsVersion/tgstation-server-installer.exe" -OutFile "artifacts/tgstation-server-installer.exe"
} finally {
$ProgressPreference = $previousProgressPreference
}
$installerHash = Get-FileHash -Path "artifacts/tgstation-server-installer.exe" -ErrorAction Stop # SHA256 is the default
cd build/package/winget/manifest
try
{
$devHash = 'CF0D4D2FD042098D826A226A05A9DAA5C792318CF48FD334F7FC06AF6A8A23B1'
$devVersion = '0.22.475'
$devReleaseDate = '2023-06-24'
$releaseDate = Get-Date -format "yyyy-MM-dd"
(Get-Content Tgstation.Server.installer.yaml -ErrorAction Stop).Replace($devHash, $installerHash.Hash).Replace($devReleaseDate, $releaseDate).Replace($devVersion, $tgsVersion) | Set-Content Tgstation.Server.installer.yaml -ErrorAction Stop
(Get-Content Tgstation.Server.locale.en-US.yaml -ErrorAction Stop).Replace($devVersion, $tgsVersion) | Set-Content Tgstation.Server.locale.en-US.yaml -ErrorAction Stop
(Get-Content Tgstation.Server.yaml -ErrorAction Stop).Replace($devVersion, $tgsVersion) | Set-Content Tgstation.Server.yaml -ErrorAction Stop
winget validate --manifest .
if (-Not $?) {
exit $lastexitcode
}
wingetcreate submit -t $Env:WINGET_PUSH_TOKEN .
if (-Not $?) {
exit $lastexitcode
}
}
finally
{
cd ../../../..
}
-1
View File
@@ -14,7 +14,6 @@ if [ ! -f $PROD_CONFIG ]; then
fi
echo "$PROD_CONFIG not detected! Creating empty and running setup wizard..."
# Important, config reloading doesn't work with symlinks
echo "{}" > $PROD_CONFIG
fi
+8 -3
View File
@@ -7,13 +7,18 @@ After=postgresql.service
After=mssql-server.service
[Service]
ExecStart=/bin/bash /opt/tgstation-server/tgs.sh General:SetupWizardMode=Never
Type=notify-reload
NotifyAccess=all
WorkingDirectory=/opt/tgstation-server
ExecStart=/usr/bin/dotnet Tgstation.Server.Host.Console.dll --appsettings-base-path=/etc/tgstation-server --General:SetupWizardMode=Never --Internal:UsingSystemD=true
TimeoutStartSec=600
Restart=Always
KillMode=process
ReloadSignal=SIGUSR2
RestartKillSignal=SIGUSR2
AmbientCapabilities=CAP_SYS_NICE
StandardOutput=null
StandardError=null
WatchdogSec=60
WatchdogSignal=SIGTERM
[Install]
WantedBy=multi-user.target
@@ -1,11 +1,11 @@
<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<assembly xmlns="urn:schemas-microsoft-com:asm.v1" manifestVersion="1.0">
<trustInfo xmlns="urn:schemas-microsoft-com:asm.v2">
<security>
<requestedPrivileges xmlns="urn:schemas-microsoft-com:asm.v3">
<requestedExecutionLevel level="highestAvailable" uiAccess="false"/>
</requestedPrivileges>
</security>
</trustInfo>
</assembly>
<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<assembly xmlns="urn:schemas-microsoft-com:asm.v1" manifestVersion="1.0">
<trustInfo xmlns="urn:schemas-microsoft-com:asm.v2">
<security>
<requestedPrivileges xmlns="urn:schemas-microsoft-com:asm.v3">
<requestedExecutionLevel level="highestAvailable" uiAccess="false"/>
</requestedPrivileges>
</security>
</trustInfo>
</assembly>
+2 -35
View File
@@ -3,41 +3,6 @@
@tableofcontents
@section new_features New since TGS3 (Rewrite)
- Agnostic HTTP API: The replaces the WCF service calls used in TGS3. This helps avoid Windows vendor lock-in and get away from the SOAP API that literally no one understood (not even me). With it, it's much easier to expose TGS to the internet, all you need is a HTTPS reverse proxy in front of it. A rundown of the new API exists here: https://tgstation.github.io/tgstation-server/api.html.
- Granular Access Controls: Windows users are no longer (required to be) the basis for authentication to the server. We now have database-backed users as a login option. These use a combined Basic/JWT authentication scheme with industry standard password hashing and salting. Users are fully customizable and can be given granular access to every bit of the server via the new permissions system. From changing the BYOND version, to test merging a PR, to restarting the server, every action may now be granted or revoked on a per user basis.
- Limitation: Users can be disabled but not deleted.
- Proper Long Running Operation Support: Server actions take a long time, from a git pull to a DreamMaker compile. TGS now internally allows for them to be run in parallel with each other and provides an audit record via the database. This is an improvement over the old system where connections had to be held open for the duration of operations.
- Database Backend: TGS requires an SQL database to operate. This allows for much better concurrency and is just overall much cleaner than the old single json file storage blob per instance.
- Limitation: There is a one-to-one relationship with a TGS server and a database. **DO NOT SHARE TGS DATABASES OUTSIDE OF SWARM MODE**.
- Linux/Docker Support: TGS is Linux and docker compatible. (Note this does not mean that rust-g and BSQL work out of the box, they must be compiled using event scripts like PreCompile.sh).
- Limitation: TGS has a dependency on the native library libgit2 which is known to cause issues on Linux. The binaries distributed with TGS are kept up to date with the upstream repository, but out of the box Linux support can't be assured in every environment. Docker is guaranteed to always work, however. See the repository for the distributed binary here: https://github.com/libgit2/libgit2sharp.nativebinaries.
- Limitation: System based logins are not supported on Linux. https://github.com/tgstation/tgstation-server/issues/709
- Incredibly Detailed Logging: Various log levels exist now (Trace/Debug/Info/Warning/Error/Critical) and are sanely output to a rolling file on the host. Significant improvement over having to use the Windows event viewer with TGS3. Until such a point where bugs stop copping up I'd recommend Trace logging for the main log level.
- Historical Deployment Data: Every time code is compiled the following data is logged and stored.
- The User that initiated it.
- When it was started.
- When it finished.
- All revision information including local/remote SHAs, test merged pull requests and their SHAs.
- The BYOND version used.
- The DMAPI version in the binary
- Multiple chat bots per instance: Up to 65535 as a matter of fact (who knows why I chose that number?)!
- Automatic Chat Bot Reconnection Intervals: Set in minutes.
- Better Error State Handling: The Server and Watchdog aren't your momma's boys anymore. Every error state will be automatically resolved or reported with recommended actions.
- Watchdog Health Checks: An interval in seconds can now be set at which TGS will send /world/Topic() packets to DreamDaemon. If four of these are missed, the server will be rebooted. No more endless @Key Holder pings in discord (and I can finally unmute the /tg/ guild)! This feature can be disabled.
- Better DMAPI: No longer requires injecting a .NET runtime .dll into the DreamDaemon process. DD -> TGS communication is now handled securely via BYOND's native /world/Export() API ("But Cyberboss, BYOND only supports GET requests." Who said anything about respecting HTTP standards when dealing with BYOND?).
- Safe/Ultrasafe Security Support: Thanks to the new DMAPI, the ultrasafe and safe security levels may be used without running into BYOND's limitations. But no one really cares...
- Private/Invisible visibility Support: Stored per instance.
- Self Upgrading: To upgrade TGS3 you needed to download and run the installer. This was pretty seamless, but it's now even better in versions >=4 as the command to upgrade can be given straight to the API. At that point the server will handle downloading the update, detaching running DreamDaemon instances, restarting with the new version, and reattaching to them. Easier than ever patch delivery.
- Multi-server co-operation: TGS installed on multiple machines can share the same database by using Swarm Mode. This mode keeps TGS and account details in sync on all machines.
- OAuth 2.0 Support: Integrate with your favorite 3rd party authentication providers
- *Gasp* TESTING: TGS currently has over 60% code coverage in automated unit and full stack integration tests. I aim to have that number ever increasing to prevent trivial mistakes. Big improvement over V3 which had... literally none...
Along with these features, nearly every single V3 feature has been included and possibly improved in some fashion. This includes stuff like Windows accounts for logins, and using ACLs for static file handling. The following exceptions exist but are planned for future updates:
- Process memory/CPU diagnostic data is not generated: https://github.com/tgstation/tgstation-server/issues/611
- Direct server announcements are no longer present but may be readded upon request.
@section features_list Comprehensive Feature List
tgstation-server is a BYOND server managment suite. It includes all the following features
@@ -58,6 +23,8 @@ tgstation-server is a BYOND server managment suite. It includes all the followin
- Supports Elasticsearch ingesting
- Able to self update with user input
- Updates are sourced from our (or anyone else's if configured to do so) GitHub releases or uploaded directly by the user.
- Can also self update using official installation packages.
- Able to shutdown and restart without interrupting DreamDaemon processes.
- Swarm system to coordinate servers across systems
- Database backend supporting several providers
- Microsoft SQL Server
-6
View File
@@ -1,6 +0,0 @@
{
"msbuild": {
"Configuration": "DebugNoService",
"EnablePackageAutoRestore" : true
}
}
+2 -1
View File
@@ -12,6 +12,7 @@ using Microsoft.Net.Http.Headers;
using Tgstation.Server.Api.Models;
using Tgstation.Server.Api.Properties;
using Tgstation.Server.Common.Extensions;
namespace Tgstation.Server.Api
{
@@ -51,7 +52,7 @@ namespace Tgstation.Server.Api
public const string OAuthAuthenticationScheme = "OAuth";
/// <summary>
/// Added to <see cref="MediaTypeNames.Application"/> in netstandard2.1. Can't use because of Tgstation.Server.Migrator.
/// Added to <see cref="MediaTypeNames.Application"/> in netstandard2.1. Can't use because of lack of .NET Framework support.
/// </summary>
public const string ApplicationJsonMime = "application/json";
+2 -6
View File
@@ -1,7 +1,3 @@
# TGS API Definitions
# tgstation-server API Definitions
This assembly defines models and routes for communicating with TGS.
- Data models are stored in the [Models](./Models) directory.
- Rights enums are stored in the [Rights](./Rights) directory.
- API routes are defined in the [Routes.cs](./Routes.cs) file.
This assembly defines HTTP headers, default credentials, models, rights, and routes for communicating with the [tgstation-server](http://github.com/tgstation/tgstation-server) API.
@@ -48,4 +48,8 @@
<ItemGroup>
<AdditionalFiles Include="../../build/stylecop.json" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\Tgstation.Server.Common\Tgstation.Server.Common.csproj" />
</ItemGroup>
</Project>
+28
View File
@@ -0,0 +1,28 @@
# tgstation-server Client Library
This library is used for accessing [tgstation-server](https://github.com/tgstation/tgstation-server) instances via .NET code.
## Examples
### Connecting to a Server:
```cs
using System.Threading;
using System.Threading.Tasks;
using Tgstation.Server.Api;
using Tgstation.Server.Client;
...
async ValueTask<IServerClient> CreateClientWithDefaultCredentials(CancellationToken cancellationToken)
{
return await clientFactory.CreateFromLogin(
url,
DefaultCredentials.AdminUserName,
DefaultCredentials.DefaultAdminUserPassword,
cancellationToken: cancellationToken);
}
```
@@ -27,7 +27,6 @@
<ItemGroup>
<ProjectReference Include="..\Tgstation.Server.Api\Tgstation.Server.Api.csproj" />
<ProjectReference Include="..\Tgstation.Server.Common\Tgstation.Server.Common.csproj" />
</ItemGroup>
<ItemGroup>
@@ -1,6 +1,6 @@
using System;
namespace Tgstation.Server.Api
namespace Tgstation.Server.Common.Extensions
{
/// <summary>
/// Extensions for the <see cref="Version"/> class.
+3
View File
@@ -0,0 +1,3 @@
# tgstation-server Common functionalty
This library contains common functions used by both the [tgstation-server](https://github.com/tgstation/tgstation-server) host and client.
@@ -0,0 +1,13 @@
namespace Tgstation.Server.Host.Common
{
/// <summary>
/// Constant values.
/// </summary>
public static class Constants
{
/// <summary>
/// The name of the project.
/// </summary>
public const string CanonicalPackageName = "tgstation-server";
}
}
@@ -0,0 +1,49 @@
namespace Tgstation.Server.Host.Common
{
/// <summary>
/// Values able to be passed via the update file path.
/// </summary>
public static class PipeCommands
{
/// <summary>
/// Stops the server ASAP, shutting down any running instances.
/// </summary>
public const string CommandStop = "stop";
/// <summary>
/// Stops the server eventually, waiting for the games in any running instances to reboot.
/// </summary>
public const string CommandGracefulShutdown = "graceful";
/// <summary>
/// Stops the server ASAP, detaching the watchdog for any running instances.
/// </summary>
public const string CommandDetachingShutdown = "detach";
#if NET6_0_OR_GREATER
/// <summary>
/// All of the <see cref="PipeCommands"/> represented as a <see cref="System.Collections.Generic.IReadOnlyList{T}"/>.
/// </summary>
public static System.Collections.Generic.IReadOnlyList<string> AllCommands { get; } = new[]
{
CommandStop,
CommandGracefulShutdown,
CommandDetachingShutdown,
};
#endif
/// <summary>
/// Gets the <see cref="int"/> value of a given <paramref name="command"/>.
/// </summary>
/// <param name="command">The <see cref="PipeCommands"/>.</param>
/// <returns>The <see cref="int"/> value of the command or <see langword="null"/> if it was unrecognized.</returns>
public static int? GetCommandId(string command)
=> command switch
{
CommandStop => 128, // Windows only allows commands 128-256: https://stackoverflow.com/a/62858106
CommandGracefulShutdown => 129,
CommandDetachingShutdown => 130,
_ => null,
};
}
}
@@ -2,7 +2,7 @@
<Import Project="../../build/Common.props" />
<PropertyGroup>
<TargetFramework>netstandard2.0</TargetFramework>
<TargetFrameworks>$(TgsFrameworkVersion);net2.0</TargetFrameworks>
<Version>$(TgsCoreVersion)</Version>
<IsPackable>false</IsPackable>
<EnableNETAnalyzers>true</EnableNETAnalyzers>
@@ -0,0 +1,103 @@
using System;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.Extensions.Logging;
using Mono.Unix;
using Mono.Unix.Native;
using Tgstation.Server.Host.Watchdog;
namespace Tgstation.Server.Host.Console
{
/// <summary>
/// <see cref="ISignalChecker"/> for checking POSIX signals.
/// </summary>
sealed class PosixSignalChecker : ISignalChecker
{
/// <summary>
/// The <see cref="ILogger"/> for the <see cref="PosixSignalChecker"/>.
/// </summary>
readonly ILogger<PosixSignalChecker> logger;
/// <summary>
/// Initializes a new instance of the <see cref="PosixSignalChecker"/> class.
/// </summary>
/// <param name="logger">The value of <see cref="logger"/>.</param>
public PosixSignalChecker(ILogger<PosixSignalChecker> logger)
{
this.logger = logger ?? throw new ArgumentNullException(nameof(logger));
}
/// <inheritdoc />
public async Task CheckSignals(Func<string, (int, Task)> startChild, CancellationToken cancellationToken)
{
var (childPid, _) = startChild?.Invoke(null) ?? throw new ArgumentNullException(nameof(startChild));
var signalTcs = new TaskCompletionSource<Signum>();
async Task<Signum?> CheckSignal(Signum signum)
{
try
{
using var unixSignal = new UnixSignal(signum);
if (!unixSignal.IsSet)
{
logger.LogTrace("Waiting for {signum}...", signum);
while (!unixSignal.IsSet)
await Task.Delay(TimeSpan.FromMilliseconds(250), cancellationToken);
logger.LogTrace("{signum} received!", signum);
}
else
logger.LogDebug("{signum} has already been sent", signum);
signalTcs.TrySetResult(signum);
}
catch (OperationCanceledException)
{
}
return signum;
}
var tasks = new[]
{
CheckSignal(Signum.SIGUSR1),
CheckSignal(Signum.SIGUSR2),
};
var completedTask = await Task.WhenAny(tasks);
if (cancellationToken.IsCancellationRequested)
{
await Task.WhenAll(tasks);
return;
}
var signalReceived = await completedTask;
logger.LogInformation("Received {signalReceived}, forwarding to main TGS process!", signalReceived);
var result = Syscall.kill(childPid, signalReceived.Value);
if (result != 0)
logger.LogWarning(
new UnixIOException(Stdlib.GetLastError()),
"Failed to forward {signalReceived}!",
signalReceived);
// forward the other signal if necessary
await Task.WhenAll(tasks);
if (cancellationToken.IsCancellationRequested)
return;
var otherTask = tasks[0] == completedTask
? tasks[1]
: tasks[0];
signalReceived = await otherTask;
logger.LogInformation("Received {signalReceived}, forwarding to main TGS process!", signalReceived);
result = Syscall.kill(childPid, signalReceived.Value);
if (result != 0)
logger.LogWarning(
new UnixIOException(Stdlib.GetLastError()),
"Failed to forward {signalReceived}!",
signalReceived);
}
}
}
+25 -5
View File
@@ -1,10 +1,11 @@
using System;
using System.Collections.Generic;
using System.Reflection;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.Extensions.Logging;
using Tgstation.Server.Common.Extensions;
using Tgstation.Server.Host.Watchdog;
namespace Tgstation.Server.Host.Console
@@ -32,17 +33,28 @@ namespace Tgstation.Server.Host.Console
/// </summary>
/// <param name="args">The arguments for the <see cref="Program"/>.</param>
/// <returns>A <see cref="Task"/> representing the running operation.</returns>
internal static async Task Main(string[] args)
internal static async Task<int> Main(string[] args)
{
using var loggerFactory = LoggerFactory.Create(builder => builder.AddConsole());
System.Console.Title = $"tgstation-server Host Watchdog v{Assembly.GetExecutingAssembly().GetName().Version.Semver()}";
var arguments = new List<string>(args);
var trace = arguments.Remove("--trace-host-watchdog");
var debug = arguments.Remove("--debug-host-watchdog");
using var loggerFactory = LoggerFactory.Create(builder =>
{
if (trace)
builder.SetMinimumLevel(LogLevel.Trace);
else if (debug)
builder.SetMinimumLevel(LogLevel.Debug);
builder.AddConsole();
});
if (trace && debug)
{
loggerFactory.CreateLogger(nameof(Program)).LogCritical("Please specify only 1 of --trace-host-watchdog or --debug-host-watchdog!");
return;
return 2;
}
using var cts = new CancellationTokenSource();
@@ -55,7 +67,15 @@ namespace Tgstation.Server.Host.Console
b.Cancel = true;
cts.Cancel();
};
await WatchdogFactory.CreateWatchdog(loggerFactory).RunAsync(false, arguments.ToArray(), cts.Token);
var watchdog = WatchdogFactory.CreateWatchdog(
new PosixSignalChecker(
loggerFactory.CreateLogger<PosixSignalChecker>()),
loggerFactory);
return await watchdog.RunAsync(false, arguments.ToArray(), cts.Token)
? 0
: 1;
}
finally
{
@@ -3,12 +3,14 @@
<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>$(TgsNetVersion)</TargetFramework>
<TargetFramework>$(TgsFrameworkVersion)</TargetFramework>
<Version>$(TgsCoreVersion)</Version>
<UseAppHost>false</UseAppHost>
<IsPackable>false</IsPackable>
<EnableNETAnalyzers>true</EnableNETAnalyzers>
<CodeAnalysisRuleSet>../../build/analyzers.ruleset</CodeAnalysisRuleSet>
<DocumentationFile>bin/$(Configuration)/$(TargetFramework)/$(AssemblyName).xml</DocumentationFile>
<ApplicationManifest>../../build/uac_elevation_manifest.xml</ApplicationManifest>
</PropertyGroup>
<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Release|AnyCPU'">
@@ -47,5 +49,6 @@
<ItemGroup>
<AdditionalFiles Include="../../build/stylecop.json" />
<AdditionalFiles Include="../../build/uac_elevation_manifest.xml" />
</ItemGroup>
</Project>
@@ -1,2 +1,3 @@
@echo off
dotnet %~dp0Tgstation.Server.Host.Console.dll %*
+1 -1
View File
@@ -1,4 +1,4 @@
#!/bin/sh
#!/bin/sh -e
script_full_path=$(dirname "$0")
exec dotnet "$script_full_path/Tgstation.Server.Host.Console.dll" "$@"
@@ -1,11 +0,0 @@
[assembly: System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Usage", "CA2232:MarkWindowsFormsEntryPointsWithStaThread", Justification = "No don't, it breaks shit")]
// This file is used by Code Analysis to maintain SuppressMessage
// attributes that are applied to this project.
// Project-level suppressions either have no target or are given
// a specific target and scoped to a namespace, type, member, etc.
//
// To add a suppression to this file, right-click the message in the
// Code Analysis results, point to "Suppress Message", and click
// "In Suppression File".
// You do not need to add suppressions to this file manually.
@@ -0,0 +1,44 @@
using System.Runtime.InteropServices;
namespace Tgstation.Server.Host.Service
{
/// <summary>
/// Native methods used by the code.
/// </summary>
static class NativeMethods
{
/// <summary>
/// See https://learn.microsoft.com/en-us/windows/win32/api/winuser/nf-winuser-messagebox.
/// </summary>
public enum MessageBoxButtons : uint
{
/// <summary>
/// The message box contains two push buttons: Yes and No.
/// </summary>
YesNo = 0x00000004,
}
/// <summary>
/// The result of a call to <see cref="MessageBox(HandleRef, string, string, MessageBoxButtons)"/>.
/// </summary>
public enum DialogResult : int
{
/// <summary>
/// The Yes button was selected.
/// </summary>
Yes = 6,
}
/// <summary>
/// Displays a modal dialog box that contains a system icon, a set of buttons, and a brief application-specific message, such as status or error information. The message box returns an integer value that indicates which button the user clicked.
/// </summary>
/// <param name="hWnd">A handle to the owner window of the message box to be created. If this parameter is NULL, the message box has no owner window.</param>
/// <param name="text">The message to be displayed. If the string consists of more than one line, you can separate the lines using a carriage return and/or linefeed character between each line.</param>
/// <param name="caption">The dialog box title. If this parameter is NULL, the default title is Error.</param>
/// <param name="type">The <see cref="MessageBoxButtons"/>.</param>
/// <returns>The resulting <see cref="DialogResult"/>.</returns>
/// <remarks>See https://learn.microsoft.com/en-us/windows/win32/api/winuser/nf-winuser-messagebox.</remarks>
[DllImport("user32.dll", CharSet = CharSet.Unicode)]
public static extern DialogResult MessageBox(HandleRef hWnd, string text, string caption, MessageBoxButtons type);
}
}
@@ -0,0 +1,21 @@
using System;
using System.Threading;
using System.Threading.Tasks;
using Tgstation.Server.Host.Watchdog;
namespace Tgstation.Server.Host.Service
{
/// <summary>
/// No-op <see cref="ISignalChecker"/>.
/// </summary>
sealed class NoopSignalChecker : ISignalChecker
{
/// <inheritdoc />
public Task CheckSignals(Func<string, (int, Task)> startChild, CancellationToken cancellationToken)
{
startChild(null);
return Task.CompletedTask;
}
}
}
+184 -106
View File
@@ -1,20 +1,17 @@
using System;
using System.Collections.Specialized;
using System.Configuration.Install;
using System.Diagnostics;
using System.Globalization;
using System.Linq;
using System.IO;
using System.Reflection;
using System.Security.Principal;
using System.ServiceProcess;
using System.Threading;
using System.Threading.Tasks;
using System.Windows.Forms;
using McMaster.Extensions.CommandLineUtils;
using Microsoft.Extensions.Hosting.WindowsServices;
using Microsoft.Extensions.Logging;
using Tgstation.Server.Host.Common;
using Tgstation.Server.Host.Watchdog;
namespace Tgstation.Server.Host.Service
@@ -34,43 +31,50 @@ namespace Tgstation.Server.Host.Service
/// <summary>
/// The --uninstall or -u option.
/// </summary>
[Option(ShortName = "u")]
[Option(ShortName = "u", Description = "Uninstalls ANY installed tgstation-server service >=v4.0.0")]
public bool Uninstall { get; }
/// <summary>
/// The --detach or -x option. Valid only with <see cref="Install"/>.
/// </summary>
[Option(ShortName = "x", Description = "If the service has to stop, detach any running DreamDaemon processes beforehand. Only supported on versions >=5.13.0")]
public bool Detach { get; }
/// <summary>
/// The --restart or -r option.
/// </summary>
[Option(ShortName = "r", Description = "Stop and restart the tgstation-server service")]
public bool Restart { get; }
/// <summary>
/// The --install or -i option.
/// </summary>
[Option(ShortName = "i")]
[Option(ShortName = "i", Description = "Installs this executable as the tgstation-server Windows service")]
public bool Install { get; set; }
/// <summary>
/// The --force or -f option.
/// </summary>
[Option(ShortName = "f", Description = "Automatically agree to uninstall prompts")]
public bool Force { get; set; }
/// <summary>
/// The --silent or -s option.
/// </summary>
[Option(ShortName = "s", Description = "Suppresses console output from the host watchdog")]
public bool Silent { get; set; }
/// <summary>
/// The --configure or -c option.
/// </summary>
[Option(ShortName = "c")]
[Option(ShortName = "c", Description = "Runs the TGS setup wizard")]
public bool Configure { get; set; }
/// <summary>
/// The --trace or -t option. Enables trace logs.
/// The --passthroughargs or -p option.
/// </summary>
[Option(ShortName = "t")]
public bool Trace { get; set; }
/// <summary>
/// The --debug or -d option. Enables debug logs.
/// </summary>
[Option(ShortName = "d")]
public bool Debug { get; set; }
/// <summary>
/// Check if the running user is a system administrator.
/// </summary>
/// <returns><see langword="true"/> if the running user is a system administrator, <see langword="false"/> otherwise.</returns>
static bool IsAdministrator()
{
var user = WindowsIdentity.GetCurrent();
var principal = new WindowsPrincipal(user);
return principal.IsInRole(WindowsBuiltInRole.Administrator);
}
[Option(ShortName = "p", Description = "Arguments passed to main host process")]
public string PassthroughArgs { get; set; }
/// <summary>
/// Entrypoint for the application.
@@ -80,47 +84,30 @@ namespace Tgstation.Server.Host.Service
static Task<int> Main(string[] args) => CommandLineApplication.ExecuteAsync<Program>(args);
/// <summary>
/// Attempt to install the TGS Service.
/// Runs sc.exe to either uninstall a given <paramref name="serviceToUninstall"/> or install the running <see cref="ServerService"/>.
/// </summary>
static void RunServiceInstall()
/// <param name="serviceToUninstall">The name of a service to uninstall.</param>
/// <returns>A <see cref="ValueTask"/> representing the running operation.</returns>
static async ValueTask InvokeSC(string serviceToUninstall)
{
// First check if the service already exists
if (Environment.UserInteractive)
foreach (ServiceController sc in ServiceController.GetServices())
if (sc.ServiceName == "tgstation-server" || sc.ServiceName == "tgstation-server-4")
{
DialogResult result = MessageBox.Show($"You already have another TGS service installed ({sc.ServiceName}). Would you like to uninstall it now? Pressing \"No\" will cancel this install.", "TGS Service", MessageBoxButtons.YesNo);
if (result != DialogResult.Yes)
return; // is this needed after exit?
using var process = new Process();
process.StartInfo.FileName = "C:/Windows/System32/sc.exe";
// Stop it first to give it some cleanup time
if (sc.Status == ServiceControllerStatus.Running)
{
sc.Stop();
sc.WaitForStatus(ServiceControllerStatus.Stopped);
}
var fullPathToAssembly = Path.GetFullPath(
Assembly.GetExecutingAssembly().Location);
// And remove it
using var serviceInstaller = new ServiceInstaller();
serviceInstaller.Context = new InstallContext($"old-{sc.ServiceName}-uninstall.log", null);
serviceInstaller.ServiceName = sc.ServiceName;
serviceInstaller.Uninstall(null);
}
var assemblyDirectory = Path.GetDirectoryName(fullPathToAssembly);
var assemblyNameWithoutExtension = Path.GetFileNameWithoutExtension(fullPathToAssembly);
var exePath = Path.Combine(assemblyDirectory, $"{assemblyNameWithoutExtension}.exe");
using var processInstaller = new ServiceProcessInstaller();
using var installer = new ServiceInstaller();
processInstaller.Account = ServiceAccount.LocalSystem;
process.StartInfo.Arguments = serviceToUninstall == null
? $"create tgstation-server binPath=\"{exePath}\" start=auto depend=Tcpip/Dhcp/Dnscache"
: $"delete {serviceToUninstall}";
process.StartInfo.CreateNoWindow = false;
installer.Context = new InstallContext("tgs-install.log", new string[] { String.Format(CultureInfo.InvariantCulture, "/assemblypath={0}", Assembly.GetEntryAssembly().Location) });
installer.Description = "/tg/station 13 server running as a windows service";
installer.DisplayName = "/tg/station server";
installer.StartType = ServiceStartMode.Automatic;
installer.ServicesDependedOn = new string[] { "Tcpip", "Dhcp", "Dnscache" };
installer.ServiceName = ServerService.Name;
installer.Parent = processInstaller;
process.Start();
var state = new ListDictionary();
installer.Install(state);
await process.WaitForExitAsync();
}
/// <summary>
@@ -129,67 +116,158 @@ namespace Tgstation.Server.Host.Service
/// <returns>A <see cref="Task"/> representing the running operation.</returns>
public async Task OnExecuteAsync()
{
if (Environment.UserInteractive)
{
if (!Install && !Uninstall && !Configure)
var standardRun = !Install && !Uninstall && !Configure;
if (standardRun)
if (!Silent && !WindowsServiceHelpers.IsWindowsService())
{
var result = MessageBox.Show("You are running the TGS windows service executable directly. It should only be run by the service control manager. Would you like to install and configure the service in this location?", "TGS Service", MessageBoxButtons.YesNo);
if (result != DialogResult.Yes)
var result = NativeMethods.MessageBox(
default,
"You are running the TGS windows service executable directly. It should only be run by the service control manager. Would you like to install and configure the service in this location?",
"TGS Service",
NativeMethods.MessageBoxButtons.YesNo);
if (result != NativeMethods.DialogResult.Yes)
return;
Install = true;
Configure = true;
}
if (!IsAdministrator())
{
// try to restart as admin
// its windows, first arg is .exe name guaranteed
var exe = Environment.GetCommandLineArgs().First();
var startInfo = new ProcessStartInfo
else
using (var service = new ServerService(WatchdogFactory, GetPassthroughArgs(), LogLevel.Trace))
{
UseShellExecute = true,
Verb = "runas",
Arguments = String.Format(CultureInfo.InvariantCulture, "{0} {1}", Install ? "-i" : Uninstall ? "-u" : String.Empty, Configure ? "-c" : String.Empty),
FileName = exe,
WorkingDirectory = Environment.CurrentDirectory,
};
using (Process.Start(startInfo))
service.Run();
return;
}
if (Configure)
await RunConfigure(CancellationToken.None); // DCT: None available
bool stopped = false;
if (Uninstall)
{
foreach (ServiceController sc in ServiceController.GetServices())
{
bool match;
using (sc)
{
match = sc.ServiceName == ServerService.Name;
if (match)
RestartService(sc);
}
if (match)
{
await InvokeSC(ServerService.Name);
break;
}
}
stopped = true;
}
if (Install)
{
if (Uninstall)
return; // oh no, it's retarded...
stopped |= await RunServiceInstall();
RunServiceInstall();
if (Restart)
foreach (ServiceController sc in ServiceController.GetServices())
using (sc)
if (sc.ServiceName == ServerService.Name)
{
if (!stopped)
RestartService(sc);
if (Configure)
{
Console.WriteLine("For this first run we'll launch the console runner so you may use the setup wizard.");
Console.WriteLine("If it starts successfully, feel free to close it and then start the service from the Windows control panel.");
}
}
else if (Uninstall)
using (var installer = new ServiceInstaller())
{
installer.Context = new InstallContext("tgs-uninstall.log", null);
installer.ServiceName = ServerService.Name;
installer.Uninstall(null);
}
else if (!Configure)
sc.Start();
break;
}
}
/// <summary>
/// Attempt to install the TGS Service.
/// </summary>
/// <returns>A <see cref="ValueTask{TResult}"/> resulting in <see langword="true"/> if the service was stopped or detached as a result, <see langword="false"/> otherwise.</returns>
async ValueTask<bool> RunServiceInstall()
{
// First check if the service already exists
bool serviceStopped = false;
if (Force || !WindowsServiceHelpers.IsWindowsService())
foreach (ServiceController sc in ServiceController.GetServices())
using (sc)
{
var serviceName = sc.ServiceName;
if (serviceName == ServerService.Name || serviceName == "tgstation-server-4")
{
NativeMethods.DialogResult result = !Force
? NativeMethods.MessageBox(
default,
$"You already have another TGS service installed ({sc.ServiceName}). Would you like to uninstall it now? Pressing \"No\" will cancel this install.",
"TGS Service",
NativeMethods.MessageBoxButtons.YesNo)
: NativeMethods.DialogResult.Yes;
if (result != NativeMethods.DialogResult.Yes)
return false; // is this needed after exit?
// Stop it first to give it some cleanup time
RestartService(sc);
// And remove it
await InvokeSC(sc.ServiceName);
}
}
await InvokeSC(null);
return serviceStopped;
}
/// <summary>
/// Restarts a service using a given <paramref name="serviceController"/>.
/// </summary>
/// <param name="serviceController">The <see cref="ServiceController"/> for the service to restart.</param>
void RestartService(ServiceController serviceController)
{
if (serviceController.Status != ServiceControllerStatus.Running)
return;
var stop = !Detach;
if (!stop)
{
using var service = new ServerService(WatchdogFactory, Trace ? LogLevel.Trace : Debug ? LogLevel.Debug : LogLevel.Information);
ServiceBase.Run(service);
serviceController.ExecuteCommand(
PipeCommands.GetCommandId(
PipeCommands.CommandDetachingShutdown)
.Value);
serviceController.WaitForStatus(ServiceControllerStatus.Stopped, TimeSpan.FromSeconds(30));
if (serviceController.Status != ServiceControllerStatus.Stopped)
stop = true;
}
if (Configure)
if (stop)
{
using var loggerFactory = LoggerFactory.Create(builder => builder.AddConsole());
await WatchdogFactory.CreateWatchdog(loggerFactory)
.RunAsync(true, Array.Empty<string>(), CancellationToken.None); // DCT: None available
serviceController.Stop();
serviceController.WaitForStatus(ServiceControllerStatus.Stopped);
}
}
/// <summary>
/// Runs the host application with the setup wizard.
/// </summary>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> for the operation.</param>
/// <returns>A <see cref="Task"/> representing the running operation.</returns>
async Task RunConfigure(CancellationToken cancellationToken)
{
using var loggerFactory = LoggerFactory.Create(builder =>
{
if (!Silent)
builder.AddConsole();
});
var watchdog = WatchdogFactory.CreateWatchdog(new NoopSignalChecker(), loggerFactory);
await watchdog.RunAsync(true, GetPassthroughArgs(), cancellationToken);
}
/// <summary>
/// Format <see cref="PassthroughArgs"/> into an <see cref="Array"/>.
/// </summary>
/// <returns><see cref="PassthroughArgs"/> formatted as a <see cref="string"/> <see cref="Array"/>.</returns>
string[] GetPassthroughArgs() => PassthroughArgs?.Split(' ', StringSplitOptions.RemoveEmptyEntries) ?? Array.Empty<string>();
}
}
@@ -1,13 +1,18 @@
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Globalization;
using System.IO;
using System.IO.Pipes;
using System.Linq;
using System.ServiceProcess;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Logging.EventLog;
using Tgstation.Server.Host.Common;
using Tgstation.Server.Host.Watchdog;
namespace Tgstation.Server.Host.Service
@@ -15,12 +20,12 @@ namespace Tgstation.Server.Host.Service
/// <summary>
/// Represents a <see cref="IWatchdog"/> as a <see cref="ServiceBase"/>.
/// </summary>
sealed class ServerService : ServiceBase
sealed class ServerService : ServiceBase, ISignalChecker
{
/// <summary>
/// The canonical windows service name.
/// </summary>
public const string Name = "tgstation-server";
public const string Name = Constants.CanonicalPackageName;
/// <summary>
/// The <see cref="IWatchdog"/> for the <see cref="ServerService"/>.
@@ -28,17 +33,27 @@ namespace Tgstation.Server.Host.Service
readonly IWatchdogFactory watchdogFactory;
/// <summary>
/// The minimum <see cref="Microsoft.Extensions.Logging.LogLevel"/> for the <see cref="EventLog"/>.
/// The <see cref="Array"/> of command line arguments the service was invoked with.
/// </summary>
readonly string[] commandLineArguments;
/// <summary>
/// The minimum <see cref="LogLevel"/> for the <see cref="EventLog"/>.
/// </summary>
readonly LogLevel minimumLogLevel;
/// <summary>
/// The <see cref="ILoggerFactory"/> used by the service.
/// The <see cref="ILoggerFactory"/> used by the <see cref="ServerService"/>.
/// </summary>
ILoggerFactory loggerFactory;
/// <summary>
/// The <see cref="Task"/> that represents the running service.
/// The <see cref="ILogger"/> for the <see cref="ServerService"/>.
/// </summary>
ILogger<ServerService> logger;
/// <summary>
/// The <see cref="Task"/> that represents the running <see cref="ServerService"/>.
/// </summary>
Task watchdogTask;
@@ -47,26 +62,71 @@ namespace Tgstation.Server.Host.Service
/// </summary>
CancellationTokenSource cancellationTokenSource;
/// <summary>
/// The <see cref="AnonymousPipeServerStream"/> the server process is using.
/// </summary>
AnonymousPipeServerStream pipeServer;
/// <summary>
/// Initializes a new instance of the <see cref="ServerService"/> class.
/// </summary>
/// <param name="watchdogFactory">The value of <see cref="watchdogFactory"/>.</param>
/// <param name="minimumLogLevel">The minimum <see cref="Microsoft.Extensions.Logging.LogLevel"/> to record in the event log.</param>
public ServerService(IWatchdogFactory watchdogFactory, LogLevel minimumLogLevel)
/// <param name="commandLineArguments">The value of <see cref="commandLineArguments"/>.</param>
/// <param name="minimumLogLevel">The minimum <see cref="LogLevel"/> to record in the event log.</param>
public ServerService(IWatchdogFactory watchdogFactory, string[] commandLineArguments, LogLevel minimumLogLevel)
{
this.watchdogFactory = watchdogFactory ?? throw new ArgumentNullException(nameof(watchdogFactory));
this.commandLineArguments = commandLineArguments ?? throw new ArgumentNullException(nameof(commandLineArguments));
this.minimumLogLevel = minimumLogLevel;
ServiceName = Name;
}
/// <inheritdoc />
public async Task CheckSignals(Func<string, (int, Task)> startChildAndGetPid, CancellationToken cancellationToken)
{
using (pipeServer = new AnonymousPipeServerStream(PipeDirection.Out, HandleInheritability.Inheritable))
{
var (_, lifetimeTask) = startChildAndGetPid($"--Internal:CommandPipe={pipeServer.GetClientHandleAsString()}");
pipeServer.DisposeLocalCopyOfClientHandle();
await lifetimeTask;
}
}
/// <summary>
/// Executes the <see cref="ServerService"/>.
/// </summary>
public void Run() => Run(this);
/// <inheritdoc />
protected override void Dispose(bool disposing)
{
loggerFactory?.Dispose();
cancellationTokenSource?.Dispose();
if (disposing)
{
loggerFactory?.Dispose();
cancellationTokenSource?.Dispose();
pipeServer?.Dispose();
}
base.Dispose(disposing);
}
/// <inheritdoc />
protected override void OnCustomCommand(int command)
{
var commandsToCheck = PipeCommands.AllCommands;
foreach (var stringCommand in commandsToCheck)
{
var commandId = PipeCommands.GetCommandId(stringCommand);
if (command == commandId)
{
SendCommandToUpdatePath(stringCommand);
return;
}
}
logger.LogWarning("Received unknown service command: {command}", command);
}
/// <inheritdoc />
protected override void OnStart(string[] args)
{
@@ -79,14 +139,24 @@ namespace Tgstation.Server.Host.Service
SourceName = EventLog.Source,
Filter = (message, logLevel) => logLevel >= minimumLogLevel,
}));
logger = loggerFactory.CreateLogger<ServerService>();
}
var watchdog = watchdogFactory.CreateWatchdog(loggerFactory);
var watchdog = watchdogFactory.CreateWatchdog(this, loggerFactory);
cancellationTokenSource?.Dispose();
cancellationTokenSource = new CancellationTokenSource();
watchdogTask = RunWatchdog(watchdog, args, cancellationTokenSource.Token);
var newArgs = new List<string>(commandLineArguments.Length + args.Length + 1)
{
"--General:SetupWizardMode=Never",
};
newArgs.AddRange(commandLineArguments);
newArgs.AddRange(args);
watchdogTask = RunWatchdog(watchdog, newArgs.ToArray(), cancellationTokenSource.Token);
}
/// <inheritdoc />
@@ -105,24 +175,60 @@ namespace Tgstation.Server.Host.Service
/// <returns>A <see cref="Task"/> representing the running operation.</returns>
async Task RunWatchdog(IWatchdog watchdog, string[] args, CancellationToken cancellationToken)
{
await watchdog.RunAsync(false, args, cancellationTokenSource.Token);
await watchdog.RunAsync(false, args, cancellationToken);
void StopServiceAsync()
async void StopServiceAsync()
{
try
{
Task.Run(Stop, cancellationToken);
await Task.Run(Stop, cancellationToken); // DCT intentional
}
catch (OperationCanceledException)
catch (OperationCanceledException ex)
{
logger.LogTrace(ex, "Stopping service cancelled!");
}
catch (Exception e)
catch (Exception ex)
{
EventLog.WriteEntry(String.Format(CultureInfo.InvariantCulture, "Error stopping service! Exception: {0}", e));
logger.LogError(ex, "Error stopping service!");
}
}
StopServiceAsync();
}
/// <summary>
/// Sends a command to the main server process.
/// </summary>
/// <param name="command">One of the <see cref="PipeCommands"/>.</param>
void SendCommandToUpdatePath(string command)
{
var localPipeServer = pipeServer;
if (localPipeServer == null)
{
logger.LogWarning("Unable to send command \"{command}\" to main server process. Is the service running?", command);
return;
}
logger.LogDebug("Send command: {command}", command);
try
{
var encoding = Encoding.UTF8;
using var streamWriter = new StreamWriter(
localPipeServer,
encoding,
PipeCommands
.AllCommands
.Select(
command => encoding.GetByteCount(
command + Environment.NewLine))
.Max(),
true);
streamWriter.WriteLine(command);
}
catch (Exception ex)
{
logger.LogError(ex, "Error attempting to send command \"{command}\"", command);
}
}
}
}
@@ -2,13 +2,16 @@
<Import Project="../../build/Common.props" />
<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>net472</TargetFramework>
<RuntimeIdentifier>win</RuntimeIdentifier>
<OutputType>WinExe</OutputType>
<TargetFramework>$(TgsFrameworkVersion)-windows</TargetFramework>
<Version>$(TgsCoreVersion)</Version>
<EnableNETAnalyzers>true</EnableNETAnalyzers>
<!-- DO NOT ENABLE THIS, It makes the service require the desktop runtime instead of the ASP NET Core Hosting Bundle -->
<UseWindowsForms>false</UseWindowsForms>
<CodeAnalysisRuleSet>../../build/analyzers.ruleset</CodeAnalysisRuleSet>
<DocumentationFile>bin/$(Configuration)/$(TargetFramework)/$(RuntimeIdentifier)/$(AssemblyName).xml</DocumentationFile>
<ApplicationIcon>../../build/tgs.ico</ApplicationIcon>
<ApplicationManifest>../../build/uac_elevation_manifest.xml</ApplicationManifest>
</PropertyGroup>
<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Release|AnyCPU'">
@@ -23,6 +26,7 @@
<ItemGroup>
<!-- Usage: Command line argument support -->
<PackageReference Include="McMaster.Extensions.CommandLineUtils" Version="4.0.2" />
<PackageReference Include="Microsoft.Extensions.Hosting.WindowsServices" Version="7.0.1" />
<!-- Usage: Windows event log logging plugin -->
<PackageReference Include="Microsoft.Extensions.Logging.EventLog" Version="7.0.0" />
<!-- Usage: Console logging plugin -->
@@ -34,6 +38,7 @@
</PackageReference>
<!-- Usage: OS identification -->
<PackageReference Include="System.Runtime.InteropServices" Version="4.3.0" />
<PackageReference Include="System.ServiceProcess.ServiceController" Version="7.0.1" />
</ItemGroup>
<ItemGroup>
@@ -42,11 +47,7 @@
<ItemGroup>
<AdditionalFiles Include="../../build/stylecop.json" />
<AdditionalFiles Include="../../build/uac_elevation_manifest.xml" />
</ItemGroup>
<ItemGroup>
<Reference Include="System.Configuration.Install" />
<Reference Include="System.ServiceProcess" />
<Reference Include="System.Windows.Forms" />
</ItemGroup>
</Project>
@@ -0,0 +1,20 @@
using System;
using System.Threading;
using System.Threading.Tasks;
namespace Tgstation.Server.Host.Watchdog
{
/// <summary>
/// For relaying signals received to the host process.
/// </summary>
public interface ISignalChecker
{
/// <summary>
/// Relays signals received to the host process.
/// </summary>
/// <param name="startChild">An <see cref="Func{TResult}"/> to start the main process. It accepts an optional additional command line argument as a paramter and returns it's <see cref="System.Diagnostics.Process.Id"/> and lifetime <see cref="Task"/>.</param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> for the operation.</param>
/// <returns>A <see cref="Task"/> representing the running operation.</returns>
Task CheckSignals(Func<string, (int, Task)> startChild, CancellationToken cancellationToken);
}
}
@@ -14,7 +14,7 @@ namespace Tgstation.Server.Host.Watchdog
/// <param name="runConfigure">If the <see cref="IWatchdog"/> should just run the host configuration wizard and exit.</param>
/// <param name="args">The arguments for the <see cref="IWatchdog"/>.</param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> for the operation.</param>
/// <returns>A <see cref="Task"/> representing the running operation.</returns>
Task RunAsync(bool runConfigure, string[] args, CancellationToken cancellationToken);
/// <returns>A <see cref="Task{TResult}"/> resulting in <see langword="true"/> if there were no errors, <see langword="false"/> otherwise.</returns>
Task<bool> RunAsync(bool runConfigure, string[] args, CancellationToken cancellationToken);
}
}
@@ -10,8 +10,9 @@ namespace Tgstation.Server.Host.Watchdog
/// <summary>
/// Create a <see cref="IWatchdog"/>.
/// </summary>
/// <param name="signalChecker">The <see cref="ISignalChecker"/> to use for relaying signals.</param>
/// <param name="loggerFactory">The <see cref="ILoggerFactory"/> to use for error reporting.</param>
/// <returns>A new <see cref="IWatchdog"/>.</returns>
IWatchdog CreateWatchdog(ILoggerFactory loggerFactory);
IWatchdog CreateWatchdog(ISignalChecker signalChecker, ILoggerFactory loggerFactory);
}
}
@@ -2,7 +2,7 @@
<Import Project="../../build/Common.props" />
<PropertyGroup>
<TargetFramework>netstandard2.0</TargetFramework>
<TargetFramework>$(TgsFrameworkVersion)</TargetFramework>
<DebugType>Full</DebugType>
<AddSyntheticProjectReferencesForSolutionDependencies>false</AddSyntheticProjectReferencesForSolutionDependencies>
<Version>$(TgsHostWatchdogVersion)</Version>
@@ -44,6 +44,7 @@
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\Tgstation.Server.Common\Tgstation.Server.Common.csproj" />
<ProjectReference Include="..\Tgstation.Server.Host.Common\Tgstation.Server.Host.Common.csproj" />
</ItemGroup>
</Project>
+74 -81
View File
@@ -10,10 +10,7 @@ using System.Threading;
using System.Threading.Tasks;
using Microsoft.Extensions.Logging;
using Mono.Unix;
using Mono.Unix.Native;
using Tgstation.Server.Common.Extensions;
using Tgstation.Server.Host.Common;
namespace Tgstation.Server.Host.Watchdog
@@ -22,6 +19,11 @@ namespace Tgstation.Server.Host.Watchdog
/// <remarks>This <see langword="class"/> is a HACK but it works. Try not to break it if you wish to change it. Remember, this code doesn't get updated with the rest of the server.</remarks>
sealed class Watchdog : IWatchdog
{
/// <summary>
/// The <see cref="ISignalChecker"/> for the <see cref="Watchdog"/>.
/// </summary>
readonly ISignalChecker signalChecker;
/// <summary>
/// The <see cref="ILogger"/> for the <see cref="Watchdog"/>.
/// </summary>
@@ -30,16 +32,18 @@ namespace Tgstation.Server.Host.Watchdog
/// <summary>
/// Initializes a new instance of the <see cref="Watchdog"/> class.
/// </summary>
/// <param name="signalChecker">The value of <see cref="signalChecker"/>.</param>
/// <param name="logger">The value of <see cref="logger"/>.</param>
public Watchdog(ILogger<Watchdog> logger)
public Watchdog(ISignalChecker signalChecker, ILogger<Watchdog> logger)
{
this.signalChecker = signalChecker ?? throw new ArgumentNullException(nameof(signalChecker));
this.logger = logger ?? throw new ArgumentNullException(nameof(logger));
}
/// <inheritdoc />
#pragma warning disable CA1502 // TODO: Decomplexify
#pragma warning disable CA1506
public async Task RunAsync(bool runConfigure, string[] args, CancellationToken cancellationToken)
public async Task<bool> RunAsync(bool runConfigure, string[] args, CancellationToken cancellationToken)
{
logger.LogInformation("Host watchdog starting...");
int currentProcessId;
@@ -55,7 +59,7 @@ namespace Tgstation.Server.Host.Watchdog
if (dotnetPath == default)
{
logger.LogCritical("Unable to locate dotnet executable in PATH! Please ensure the .NET Core runtime is installed and is in your PATH!");
return;
return false;
}
logger.LogInformation("Detected dotnet executable at {dotnetPath}", dotnetPath);
@@ -71,15 +75,16 @@ namespace Tgstation.Server.Host.Watchdog
{
// VS special tactics
// just copy the shit where it belongs
Directory.Delete(assemblyStoragePath, true);
if (Directory.Exists(assemblyStoragePath))
Directory.Delete(assemblyStoragePath, true);
Directory.CreateDirectory(defaultAssemblyPath);
var sourcePath = "../../../../Tgstation.Server.Host/bin/Debug/net6.0";
foreach (string dirPath in Directory.GetDirectories(sourcePath, "*", SearchOption.AllDirectories))
Directory.CreateDirectory(dirPath.Replace(sourcePath, defaultAssemblyPath));
Directory.CreateDirectory(dirPath.Replace(sourcePath, defaultAssemblyPath, StringComparison.Ordinal));
foreach (string newPath in Directory.GetFiles(sourcePath, "*.*", SearchOption.AllDirectories))
File.Copy(newPath, newPath.Replace(sourcePath, defaultAssemblyPath), true);
File.Copy(newPath, newPath.Replace(sourcePath, defaultAssemblyPath, StringComparison.Ordinal), true);
const string AppSettingsYaml = "appsettings.yml";
var rootYaml = Path.Combine(rootLocation, AppSettingsYaml);
@@ -92,19 +97,19 @@ namespace Tgstation.Server.Host.Watchdog
var assemblyName = String.Join(".", nameof(Tgstation), nameof(Server), nameof(Host), "dll");
var assemblyPath = Path.Combine(defaultAssemblyPath, assemblyName);
if (assemblyPath.Contains("\""))
if (assemblyPath.Contains('"', StringComparison.Ordinal))
{
logger.LogCritical("Running from paths with \"'s in the name is not supported!");
return;
return false;
}
if (!File.Exists(assemblyPath))
{
logger.LogCritical("Unable to locate host assembly!");
return;
return false;
}
var watchdogVersion = executingAssembly.GetName().Version.ToString();
var watchdogVersion = executingAssembly.GetName().Version.Semver().ToString();
while (!cancellationToken.IsCancellationRequested)
using (logger.BeginScope("Host invocation"))
@@ -128,8 +133,8 @@ namespace Tgstation.Server.Host.Watchdog
if (runConfigure)
{
logger.LogInformation("Running configuration check and wizard if necessary...");
arguments.Add("General:SetupWizardMode=Only");
logger.LogInformation("Running configuration check and wizard...");
arguments.Add("--General:SetupWizardMode=Only");
}
arguments.AddRange(args);
@@ -145,13 +150,20 @@ namespace Tgstation.Server.Host.Watchdog
};
process.EnableRaisingEvents = true;
logger.LogInformation("Launching host...");
var killedHostProcess = false;
try
{
process.Start();
var childPid = process.Id;
var processTask = tcs.Task;
(int, Task) StartProcess(string additionalArg)
{
if (additionalArg != null)
process.StartInfo.Arguments += $" {additionalArg}";
logger.LogInformation("Launching host with arguments: {arguments}", process.StartInfo.Arguments);
process.Start();
return (process.Id, processTask);
}
using (var processCts = new CancellationTokenSource())
using (processCts.Token.Register(() => tcs.TrySetResult(null)))
@@ -178,62 +190,17 @@ namespace Tgstation.Server.Host.Watchdog
}
}))
{
var processTask = tcs.Task;
while (!processTask.IsCompleted)
using var cts = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken);
var checkerTask = signalChecker.CheckSignals(StartProcess, cts.Token);
try
{
var signalTcs = new TaskCompletionSource<Signum>();
using var cts = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken);
async Task CheckSignal(Signum signum)
{
if (isWindows)
return;
try
{
using var unixSignal = new UnixSignal(signum);
if (!unixSignal.IsSet)
{
logger.LogTrace("Waiting for {signum}...", signum);
while (!unixSignal.IsSet)
await Task.Delay(TimeSpan.FromMilliseconds(250), cts.Token);
logger.LogTrace("{signum} received!", signum);
}
else
logger.LogDebug("{signum} has already been sent", signum);
signalTcs.TrySetResult(signum);
}
catch (OperationCanceledException)
{
}
}
var checkerTask = Task.WhenAll(
CheckSignal(Signum.SIGUSR1),
CheckSignal(Signum.SIGUSR2));
try
{
var signalTask = signalTcs.Task;
var completedTask = await Task.WhenAny(processTask, signalTask);
if (completedTask == signalTask)
{
var signalReceived = await signalTask;
logger.LogInformation("Received {signalReceived}, forwarding to main TGS process!", signalReceived);
var result = Syscall.kill(childPid, signalReceived);
if (result != 0)
logger.LogWarning(
new UnixIOException(Stdlib.GetLastError()),
"Failed to forward {signalReceived}!",
signalReceived);
}
}
finally
{
cts.Cancel();
await checkerTask;
}
await processTask;
}
finally
{
cts.Cancel();
await checkerTask;
}
}
}
@@ -252,8 +219,19 @@ namespace Tgstation.Server.Host.Watchdog
process.WaitForExit();
}
}
catch (InvalidOperationException)
catch (InvalidOperationException ex2)
{
logger.LogWarning(ex2, "Error killing host process!");
}
try
{
if (File.Exists(updateDirectory))
File.Delete(updateDirectory);
}
catch (Exception ex2)
{
logger.LogWarning(ex2, "Error deleting comms file!");
}
logger.LogInformation("Host exited!");
@@ -261,14 +239,14 @@ namespace Tgstation.Server.Host.Watchdog
if (runConfigure)
{
logger.LogInformation("Exiting due to configuration check...");
return;
logger.LogInformation("Exiting due to configure intent...");
return true;
}
switch ((HostExitCode)process.ExitCode)
{
case HostExitCode.CompleteExecution:
return;
return true;
case HostExitCode.RestartRequested:
if (!cancellationToken.IsCancellationRequested)
logger.LogInformation("Watchdog will restart host..."); // just a restart
@@ -364,11 +342,14 @@ namespace Tgstation.Server.Host.Watchdog
catch (Exception ex)
{
logger.LogCritical(ex, "Host watchdog error!");
return false;
}
finally
{
logger.LogInformation("Host watchdog exiting...");
}
return true;
}
#pragma warning restore CA1502
#pragma warning restore CA1506
@@ -388,10 +369,22 @@ namespace Tgstation.Server.Host.Watchdog
if (isWindows)
{
exeName += ".exe";
enumerator = paths;
enumerator = new List<string>(paths)
{
"C:/Program Files/dotnet",
"C:/Program Files (x86)/dotnet",
};
}
else
enumerator = paths.Select(x => x.Split(':')).SelectMany(x => x);
enumerator = paths
.Select(x => x.Split(':'))
.SelectMany(x => x)
.Concat(new List<string>(2)
{
"/usr/bin",
"/usr/share/bin",
"/usr/local/share/dotnet",
});
enumerator = enumerator.Select(x => Path.Combine(x, exeName));
@@ -8,6 +8,10 @@ namespace Tgstation.Server.Host.Watchdog
public sealed class WatchdogFactory : IWatchdogFactory
{
/// <inheritdoc />
public IWatchdog CreateWatchdog(ILoggerFactory loggerFactory) => new Watchdog(loggerFactory?.CreateLogger<Watchdog>() ?? throw new ArgumentNullException(nameof(loggerFactory)));
public IWatchdog CreateWatchdog(
ISignalChecker signalChecker,
ILoggerFactory loggerFactory) => new Watchdog(
signalChecker ?? throw new ArgumentNullException(nameof(signalChecker)),
loggerFactory?.CreateLogger<Watchdog>() ?? throw new ArgumentNullException(nameof(loggerFactory)));
}
}
@@ -165,6 +165,7 @@ namespace Tgstation.Server.Host.Components.Byond
ActiveVersion?.ToString(),
stringVersion,
},
false,
cancellationToken);
ActiveVersion = version;
@@ -234,7 +235,7 @@ namespace Tgstation.Server.Host.Components.Byond
await Task.WhenAny(
containerTask,
activeVersionUpdate)
.WithToken(cancellationToken);
.WaitAsync(cancellationToken);
if (containerTask.IsCompleted)
logger.LogTrace("All BYOND locks for {version} are gone", version);
@@ -301,26 +302,28 @@ namespace Tgstation.Server.Host.Components.Byond
var activeVersionBytesTask = GetActiveVersion();
using (await SemaphoreSlimContext.Lock(UserFilesSemaphore, cancellationToken))
{
// Create local cfg directory in case it doesn't exist
var localCfgDirectory = ioManager.ConcatPath(
byondInstaller.PathToUserByondFolder,
CfgDirectoryName);
await ioManager.CreateDirectory(
localCfgDirectory,
cancellationToken);
// Delete trusted.txt so it doesn't grow too large
var trustedFilePath =
ioManager.ConcatPath(
var byondDir = byondInstaller.PathToUserByondFolder;
if (byondDir != null)
using (await SemaphoreSlimContext.Lock(UserFilesSemaphore, cancellationToken))
{
// Create local cfg directory in case it doesn't exist
var localCfgDirectory = ioManager.ConcatPath(
byondDir,
CfgDirectoryName);
await ioManager.CreateDirectory(
localCfgDirectory,
TrustedDmbFileName);
logger.LogTrace("Deleting trusted .dmbs file {trustedFilePath}", trustedFilePath);
await ioManager.DeleteFile(
trustedFilePath,
cancellationToken);
}
cancellationToken);
// Delete trusted.txt so it doesn't grow too large
var trustedFilePath =
ioManager.ConcatPath(
localCfgDirectory,
TrustedDmbFileName);
logger.LogTrace("Deleting trusted .dmbs file {trustedFilePath}", trustedFilePath);
await ioManager.DeleteFile(
trustedFilePath,
cancellationToken);
}
await ioManager.CreateDirectory(DefaultIOManager.CurrentDirectory, cancellationToken);
var directories = await ioManager.GetDirectories(DefaultIOManager.CurrentDirectory, cancellationToken);
@@ -452,7 +455,7 @@ namespace Tgstation.Server.Host.Components.Byond
if (neededForLock && !installation.InstallationTask.IsCompleted)
logger.LogWarning("The required BYOND version ({version}) is not readily available! We will have to wait for it to install.", version);
await installation.InstallationTask.WithToken(cancellationToken);
await installation.InstallationTask.WaitAsync(cancellationToken);
return installLock;
}
@@ -475,7 +478,7 @@ namespace Tgstation.Server.Host.Components.Byond
progressReporter.StageName = "Running event";
var versionString = version.ToString();
await eventConsumer.HandleEvent(EventType.ByondInstallStart, new List<string> { versionString }, cancellationToken);
await eventConsumer.HandleEvent(EventType.ByondInstallStart, new List<string> { versionString }, false, cancellationToken);
await InstallVersionFiles(progressReporter, version, customVersionStream, cancellationToken);
@@ -484,7 +487,7 @@ namespace Tgstation.Server.Host.Components.Byond
catch (Exception ex)
{
if (ex is not OperationCanceledException)
await eventConsumer.HandleEvent(EventType.ByondInstallFail, new List<string> { ex.Message }, cancellationToken);
await eventConsumer.HandleEvent(EventType.ByondInstallFail, new List<string> { ex.Message }, false, cancellationToken);
lock (installedVersions)
installedVersions.Remove(version);
@@ -619,8 +622,15 @@ namespace Tgstation.Server.Host.Components.Byond
/// <returns>A <see cref="Task"/> representing the running operation.</returns>
async Task TrustDmbPath(string fullDmbPath, CancellationToken cancellationToken)
{
var byondDir = byondInstaller.PathToUserByondFolder;
if (String.IsNullOrWhiteSpace(byondDir))
{
logger.LogTrace("No relevant user BYOND directory to install a \"{fileName}\" in", TrustedDmbFileName);
return;
}
var trustedFilePath = ioManager.ConcatPath(
byondInstaller.PathToUserByondFolder,
byondDir,
CfgDirectoryName,
TrustedDmbFileName);
@@ -636,9 +646,7 @@ namespace Tgstation.Server.Host.Components.Byond
trustedFileText = $"{trustedFileText.Trim()}{Environment.NewLine}";
}
else
{
trustedFileText = String.Empty;
}
if (trustedFileText.Contains(fullDmbPath, StringComparison.Ordinal))
return;
@@ -107,7 +107,11 @@ namespace Tgstation.Server.Host.Components.Byond
generalConfiguration = generalConfigurationOptions?.Value ?? throw new ArgumentNullException(nameof(generalConfigurationOptions));
sessionConfiguration = sessionConfigurationOptions?.Value ?? throw new ArgumentNullException(nameof(sessionConfigurationOptions));
PathToUserByondFolder = IOManager.ResolvePath(IOManager.ConcatPath(Environment.GetFolderPath(Environment.SpecialFolder.MyDocuments), "BYOND"));
var documentsDirectory = Environment.GetFolderPath(Environment.SpecialFolder.MyDocuments);
if (String.IsNullOrWhiteSpace(documentsDirectory))
PathToUserByondFolder = null; // happens with the service account
else
PathToUserByondFolder = IOManager.ResolvePath(IOManager.ConcatPath(documentsDirectory, "BYOND"));
semaphore = new SemaphoreSlim(1);
installedDirectX = false;
@@ -482,7 +482,7 @@ namespace Tgstation.Server.Host.Components.Chat
if (waitingForInitialConnection)
{
logger.LogTrace("Waiting for initial chat bot connections before updating tracking contexts...");
await initialProviderConnectionsTask.WithToken(cancellationToken);
await initialProviderConnectionsTask.WaitAsync(cancellationToken);
}
List<Task> tasks;
@@ -932,7 +932,7 @@ namespace Tgstation.Server.Host.Components.Chat
if (messageTasks.Count == 0)
{
logger.LogTrace("No providers active, pausing messsage monitoring...");
await updatedTask.WithToken(cancellationToken);
await updatedTask.WaitAsync(cancellationToken);
logger.LogTrace("Resuming message monitoring...");
continue;
}
@@ -1073,7 +1073,7 @@ namespace Tgstation.Server.Host.Components.Chat
{
var cancellationToken = handlerCts.Token;
if (waitForConnections)
await initialProviderConnectionsTask.WithToken(cancellationToken);
await initialProviderConnectionsTask.WaitAsync(cancellationToken);
await SendMessage(
channelIdsFactory(),
@@ -377,7 +377,7 @@ namespace Tgstation.Server.Host.Components.Chat.Providers
cancellationToken,
DefaultIOManager.BlockingTaskCreationOptions,
TaskScheduler.Current)
.WithToken(cancellationToken);
.WaitAsync(cancellationToken);
cancellationToken.ThrowIfCancellationRequested();
@@ -584,7 +584,7 @@ namespace Tgstation.Server.Host.Components.Chat.Providers
cancellationToken,
TaskCreationOptions.None,
TaskScheduler.Current)
.WithToken(cancellationToken);
.WaitAsync(cancellationToken);
/// <summary>
/// Run SASL authentication on <see cref="client"/>.
@@ -149,7 +149,7 @@ namespace Tgstation.Server.Host.Components.Chat.Providers
{
while (true)
{
await nextMessage.Task.WithToken(cancellationToken);
await nextMessage.Task.WaitAsync(cancellationToken);
lock (messageQueue)
if (messageQueue.Count > 0)
{
@@ -448,7 +448,7 @@ namespace Tgstation.Server.Host.Components.Deployment
async Task DeleteCompileJobContent(string directory, CancellationToken cancellationToken)
{
// Then call the cleanup event, waiting here first
await eventConsumer.HandleEvent(EventType.DeploymentCleanup, new List<string> { ioManager.ResolvePath(directory) }, cancellationToken);
await eventConsumer.HandleEvent(EventType.DeploymentCleanup, new List<string> { ioManager.ResolvePath(directory) }, true, cancellationToken);
await ioManager.DeleteDirectory(directory, cancellationToken);
}
}
@@ -386,7 +386,7 @@ namespace Tgstation.Server.Host.Components.Deployment
repoName,
cancellationToken);
var eventTask = eventConsumer.HandleEvent(EventType.DeploymentComplete, Enumerable.Empty<string>(), cancellationToken);
var eventTask = eventConsumer.HandleEvent(EventType.DeploymentComplete, Enumerable.Empty<string>(), false, cancellationToken);
try
{
@@ -537,7 +537,7 @@ namespace Tgstation.Server.Host.Components.Deployment
{
// DCT: Cancellation token is for job, delaying here is fine
progressReporter.StageName = "Running CompileCancelled event";
await eventConsumer.HandleEvent(EventType.CompileCancelled, Enumerable.Empty<string>(), CancellationToken.None);
await eventConsumer.HandleEvent(EventType.CompileCancelled, Enumerable.Empty<string>(), true, CancellationToken.None);
throw;
}
finally
@@ -594,6 +594,7 @@ namespace Tgstation.Server.Host.Components.Deployment
repoOrigin.ToString(),
$"{byondLock.Version.Major}.{byondLock.Version.Minor}",
},
true,
cancellationToken);
// determine the dme
@@ -632,6 +633,7 @@ namespace Tgstation.Server.Host.Components.Deployment
repoOrigin.ToString(),
$"{byondLock.Version.Major}.{byondLock.Version.Minor}",
},
true,
cancellationToken);
// run compiler
@@ -672,6 +674,7 @@ namespace Tgstation.Server.Host.Components.Deployment
exitCode == 0 ? "1" : "0",
byondVersion.ToString(),
},
true,
cancellationToken);
throw;
}
@@ -684,6 +687,7 @@ namespace Tgstation.Server.Host.Components.Deployment
resolvedOutputDirectory,
byondVersion.ToString(),
},
true,
cancellationToken);
logger.LogTrace("Applying static game file symlinks...");
@@ -803,10 +807,10 @@ namespace Tgstation.Server.Host.Components.Deployment
using (var provider = new TemporaryDmbProvider(ioManager.ResolvePath(job.DirectoryName.ToString()), String.Concat(job.DmeName, DmbExtension), job))
await using (var controller = await sessionControllerFactory.LaunchNew(provider, byondLock, launchParameters, true, cancellationToken))
{
var launchResult = await controller.LaunchResult.WithToken(cancellationToken);
var launchResult = await controller.LaunchResult.WaitAsync(cancellationToken);
if (launchResult.StartupTime.HasValue)
await controller.Lifetime.WithToken(cancellationToken);
await controller.Lifetime.WaitAsync(cancellationToken);
if (!controller.Lifetime.IsCompleted)
await controller.DisposeAsync();
@@ -958,7 +962,7 @@ namespace Tgstation.Server.Host.Components.Deployment
try
{
// DCT: None available
await eventConsumer.HandleEvent(EventType.DeploymentCleanup, new List<string> { jobPath }, CancellationToken.None);
await eventConsumer.HandleEvent(EventType.DeploymentCleanup, new List<string> { jobPath }, true, CancellationToken.None);
await ioManager.DeleteDirectory(jobPath, CancellationToken.None);
}
catch (Exception e)
@@ -127,9 +127,9 @@ namespace Tgstation.Server.Host.Components.Deployment.Remote
Logger.LogTrace("In-progress deployment status created");
}
catch (ApiException ex)
catch (Exception ex) when (ex is not OperationCanceledException)
{
Logger.LogWarning(ex, "Unable to create deployment!");
Logger.LogWarning(ex, "Unable to create GitHub deployment!");
}
}
@@ -138,7 +138,7 @@ namespace Tgstation.Server.Host.Components.Deployment.Remote
compileJob.GitHubRepoId = await repositoryIdTask;
Logger.LogTrace("Set GitHub ID as {gitHubRepoId}", compileJob.GitHubRepoId);
}
catch (RateLimitExceededException ex) when (!repositorySettings.CreateGitHubDeployments.Value)
catch (Exception ex) when (ex is not OperationCanceledException)
{
Logger.LogWarning(ex, "Unable to set compile job repository ID!");
}
@@ -252,9 +252,9 @@ namespace Tgstation.Server.Host.Components.Deployment.Remote
{
await gitHubService.CommentOnIssue(remoteRepositoryOwner, remoteRepositoryName, comment, testMergeNumber, cancellationToken);
}
catch (ApiException e)
catch (Exception ex) when (ex is not OperationCanceledException)
{
Logger.LogWarning(e, "Error posting GitHub comment!");
Logger.LogWarning(ex, "Error posting GitHub comment!");
}
}
@@ -336,14 +336,21 @@ namespace Tgstation.Server.Host.Components.Deployment.Remote
var gitHubService = gitHubServiceFactory.CreateService(gitHubAccessToken);
await gitHubService.CreateDeploymentStatus(
new NewDeploymentStatus(deploymentState)
{
Description = description,
},
compileJob.GitHubRepoId.Value,
compileJob.GitHubDeploymentId.Value,
cancellationToken);
try
{
await gitHubService.CreateDeploymentStatus(
new NewDeploymentStatus(deploymentState)
{
Description = description,
},
compileJob.GitHubRepoId.Value,
compileJob.GitHubDeploymentId.Value,
cancellationToken);
}
catch (Exception ex) when (ex is not OperationCanceledException)
{
Logger.LogWarning(ex, "Error updating GitHub deployment!");
}
}
}
}
@@ -11,7 +11,6 @@ using GitLabApiClient.Models.Notes.Requests;
using Microsoft.Extensions.Logging;
using Tgstation.Server.Host.Components.Repository;
using Tgstation.Server.Host.Extensions;
using Tgstation.Server.Host.Models;
namespace Tgstation.Server.Host.Components.Deployment.Remote
@@ -59,7 +58,7 @@ namespace Tgstation.Server.Host.Components.Deployment.Remote
.GetAsync(
$"{repository.RemoteRepositoryOwner}/{repository.RemoteRepositoryName}",
x.TestMerge.Number)
.WithToken(cancellationToken));
.WaitAsync(cancellationToken));
try
{
await Task.WhenAll(tasks);
@@ -120,7 +119,7 @@ namespace Tgstation.Server.Host.Components.Deployment.Remote
CancellationToken cancellationToken) => Task.CompletedTask;
/// <inheritdoc />
protected override Task CommentOnTestMergeSource(
protected override async Task CommentOnTestMergeSource(
RepositorySettings repositorySettings,
string remoteRepositoryOwner,
string remoteRepositoryName,
@@ -132,13 +131,20 @@ namespace Tgstation.Server.Host.Components.Deployment.Remote
? new GitLabClient(GitLabRemoteFeatures.GitLabUrl, repositorySettings.AccessToken)
: new GitLabClient(GitLabRemoteFeatures.GitLabUrl);
return client
.MergeRequests
.CreateNoteAsync(
$"{remoteRepositoryOwner}/{remoteRepositoryName}",
testMergeNumber,
new CreateMergeRequestNoteRequest(comment))
.WithToken(cancellationToken);
try
{
await client
.MergeRequests
.CreateNoteAsync(
$"{remoteRepositoryOwner}/{remoteRepositoryName}",
testMergeNumber,
new CreateMergeRequestNoteRequest(comment))
.WaitAsync(cancellationToken);
}
catch (Exception ex) when (ex is not OperationCanceledException)
{
Logger.LogWarning(ex, "Error posting GitHub comment!");
}
}
/// <inheritdoc />
@@ -31,15 +31,15 @@ namespace Tgstation.Server.Host.Components.Events
}
/// <inheritdoc />
public async Task HandleEvent(EventType eventType, IEnumerable<string> parameters, CancellationToken cancellationToken)
public async Task HandleEvent(EventType eventType, IEnumerable<string> parameters, bool deploymentPipeline, CancellationToken cancellationToken)
{
ArgumentNullException.ThrowIfNull(parameters);
if (watchdog == null)
throw new InvalidOperationException("EventConsumer used without watchdog set!");
var scriptTask = configuration.HandleEvent(eventType, parameters, cancellationToken);
await watchdog.HandleEvent(eventType, parameters, cancellationToken);
var scriptTask = configuration.HandleEvent(eventType, parameters, deploymentPipeline, cancellationToken);
await watchdog.HandleEvent(eventType, parameters, deploymentPipeline, cancellationToken);
await scriptTask;
}
@@ -49,11 +49,10 @@ namespace Tgstation.Server.Host.Components.Events
/// <param name="watchdog">The value of <see cref="watchdog"/>.</param>
public void SetWatchdog(IWatchdog watchdog)
{
#pragma warning disable IDE0016 // Use 'throw' expression
ArgumentNullException.ThrowIfNull(watchdog);
#pragma warning restore IDE0016 // Use 'throw' expression
if (this.watchdog != null)
throw new InvalidOperationException("watchdog already set!");
this.watchdog = watchdog;
}
}
@@ -14,8 +14,9 @@ namespace Tgstation.Server.Host.Components.Events
/// </summary>
/// <param name="eventType">The <see cref="EventType"/>.</param>
/// <param name="parameters">An <see cref="IEnumerable{T}"/> of <see cref="string"/> parameters for <paramref name="eventType"/>.</param>
/// <param name="deploymentPipeline">If this event is part of the deployment pipeline.</param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> for the operation.</param>
/// <returns>A <see cref="Task"/> representing the running operation.</returns>
Task HandleEvent(EventType eventType, IEnumerable<string> parameters, CancellationToken cancellationToken);
Task HandleEvent(EventType eventType, IEnumerable<string> parameters, bool deploymentPipeline, CancellationToken cancellationToken);
}
}
@@ -308,9 +308,10 @@ namespace Tgstation.Server.Host.Components
// the main point of auto update is to pull the remote
await repo.FetchOrigin(
NextProgressReporter("Fetch Origin"),
repositorySettings.AccessUser,
repositorySettings.AccessToken,
NextProgressReporter("Fetch Origin"),
true,
cancellationToken);
var hasDbChanges = false;
@@ -382,9 +383,10 @@ namespace Tgstation.Server.Host.Components
await UpdateRevInfo(repo.Head, false, null);
var result = await repo.MergeOrigin(
NextProgressReporter("Merge Origin"),
repositorySettings.CommitterName,
repositorySettings.CommitterEmail,
NextProgressReporter("Merge Origin"),
true,
cancellationToken);
var preserveTestMerges = repositorySettings.AutoUpdatesKeepTestMerges.Value;
@@ -430,10 +432,11 @@ namespace Tgstation.Server.Host.Components
const string StageName = "Resetting to origin...";
logger.LogTrace(StageName);
await repo.ResetToOrigin(
NextProgressReporter(StageName),
repositorySettings.AccessUser,
repositorySettings.AccessToken,
repositorySettings.UpdateSubmodules.Value,
NextProgressReporter(StageName),
true,
cancellationToken);
var currentHead = repo.Head;
@@ -453,12 +456,13 @@ namespace Tgstation.Server.Host.Components
if (repositorySettings.AutoUpdatesSynchronize.Value && startSha != repo.Head && (shouldSyncTracked || repositorySettings.PushTestMergeCommits.Value))
{
var pushedOrigin = await repo.Sychronize(
NextProgressReporter("Synchronize"),
repositorySettings.AccessUser,
repositorySettings.AccessToken,
repositorySettings.CommitterName,
repositorySettings.CommitterEmail,
NextProgressReporter("Synchronize"),
shouldSyncTracked,
true,
cancellationToken);
var currentHead = repo.Head;
if (currentHead != currentRevInfo.CommitSha)
@@ -494,7 +498,7 @@ namespace Tgstation.Server.Host.Components
{
await asyncDelayer.Delay(TimeSpan.FromMinutes(minutes > Int32.MaxValue ? Int32.MaxValue : minutes), cancellationToken);
logger.LogInformation("Beginning auto update...");
await eventConsumer.HandleEvent(EventType.InstanceAutoUpdateStart, Enumerable.Empty<string>(), cancellationToken);
await eventConsumer.HandleEvent(EventType.InstanceAutoUpdateStart, Enumerable.Empty<string>(), true, cancellationToken);
try
{
var repositoryUpdateJob = new Job
@@ -281,7 +281,8 @@ namespace Tgstation.Server.Host.Components
platformIdentifier,
fileTransferService,
loggerFactory.CreateLogger<StaticFiles.Configuration>(),
generalConfiguration);
generalConfiguration,
sessionConfiguration);
var eventConsumer = new EventConsumer(configuration);
var repoManager = new RepositoryManager(
repositoryFactory,
@@ -87,6 +87,11 @@ namespace Tgstation.Server.Host.Components
/// </summary>
readonly ISwarmServiceController swarmServiceController;
/// <summary>
/// The <see cref="IConsole"/> for the <see cref="InstanceManager"/>.
/// </summary>
readonly IConsole console;
/// <summary>
/// The <see cref="ILogger"/> for the <see cref="InstanceManager"/>.
/// </summary>
@@ -132,6 +137,11 @@ namespace Tgstation.Server.Host.Components
/// </summary>
readonly CancellationTokenSource shutdownCancellationTokenSource;
/// <summary>
/// The original <see cref="IConsole.Title"/> of <see cref="console"/>.
/// </summary>
readonly string originalConsoleTitle;
/// <summary>
/// The <see cref="Task"/> returned by <see cref="Initialize(CancellationToken)"/>.
/// </summary>
@@ -155,6 +165,7 @@ namespace Tgstation.Server.Host.Components
/// <param name="asyncDelayer">The value of <see cref="asyncDelayer"/>.</param>
/// <param name="serverPortProvider">The value of <see cref="serverPortProvider"/>.</param>
/// <param name="swarmServiceController">The value of <see cref="swarmServiceController"/>.</param>
/// <param name="console">The value of <see cref="console"/>.</param>
/// <param name="generalConfigurationOptions">The <see cref="IOptions{TOptions}"/> containing the value of <see cref="generalConfiguration"/>.</param>
/// <param name="swarmConfigurationOptions">The <see cref="IOptions{TOptions}"/> containing the value of <see cref="swarmConfiguration"/>.</param>
/// <param name="logger">The value of <see cref="logger"/>.</param>
@@ -169,6 +180,7 @@ namespace Tgstation.Server.Host.Components
IAsyncDelayer asyncDelayer,
IServerPortProvider serverPortProvider,
ISwarmServiceController swarmServiceController,
IConsole console,
IOptions<GeneralConfiguration> generalConfigurationOptions,
IOptions<SwarmConfiguration> swarmConfigurationOptions,
ILogger<InstanceManager> logger)
@@ -183,10 +195,13 @@ namespace Tgstation.Server.Host.Components
this.asyncDelayer = asyncDelayer ?? throw new ArgumentNullException(nameof(asyncDelayer));
this.serverPortProvider = serverPortProvider ?? throw new ArgumentNullException(nameof(serverPortProvider));
this.swarmServiceController = swarmServiceController ?? throw new ArgumentNullException(nameof(swarmServiceController));
this.console = console ?? throw new ArgumentNullException(nameof(console));
generalConfiguration = generalConfigurationOptions?.Value ?? throw new ArgumentNullException(nameof(generalConfigurationOptions));
swarmConfiguration = swarmConfigurationOptions?.Value ?? throw new ArgumentNullException(nameof(swarmConfigurationOptions));
this.logger = logger ?? throw new ArgumentNullException(nameof(logger));
originalConsoleTitle = console.Title;
instances = new Dictionary<long, ReferenceCountingContainer<IInstance, InstanceWrapper>>();
bridgeHandlers = new Dictionary<string, IBridgeHandler>();
readyTcs = new TaskCompletionSource();
@@ -323,7 +338,7 @@ namespace Tgstation.Server.Host.Components
try
{
await container.OnZeroReferences.WithToken(cancellationToken);
await container.OnZeroReferences.WaitAsync(cancellationToken);
// we are the one responsible for cancelling his jobs
var tasks = new List<Task>();
@@ -426,42 +441,50 @@ namespace Tgstation.Server.Host.Components
/// <inheritdoc />
public async Task StopAsync(CancellationToken cancellationToken)
{
using (cancellationToken.Register(shutdownCancellationTokenSource.Cancel))
try
{
logger.LogDebug("Stopping instance manager...");
if (!startupTask.IsCompleted)
try
{
using (cancellationToken.Register(shutdownCancellationTokenSource.Cancel))
try
{
logger.LogTrace("Interrupting startup task...");
startupCancellationTokenSource.Cancel();
await startupTask;
logger.LogDebug("Stopping instance manager...");
if (!startupTask.IsCompleted)
{
logger.LogTrace("Interrupting startup task...");
startupCancellationTokenSource.Cancel();
await startupTask;
}
var instanceFactoryStopTask = instanceFactory.StopAsync(cancellationToken);
await jobService.StopAsync(cancellationToken);
async Task OfflineInstanceImmediate(IInstance instance, CancellationToken cancellationToken)
{
try
{
await instance.StopAsync(cancellationToken);
}
catch (Exception ex)
{
logger.LogError(ex, "Instance shutdown exception!");
}
}
await Task.WhenAll(instances.Select(x => OfflineInstanceImmediate(x.Value.Instance, cancellationToken)));
await instanceFactoryStopTask;
await swarmServiceController.Shutdown(cancellationToken);
}
var instanceFactoryStopTask = instanceFactory.StopAsync(cancellationToken);
await jobService.StopAsync(cancellationToken);
async Task OfflineInstanceImmediate(IInstance instance, CancellationToken cancellationToken)
finally
{
try
{
await instance.StopAsync(cancellationToken);
}
catch (Exception ex)
{
logger.LogError(ex, "Instance shutdown exception!");
}
if (originalConsoleTitle != null)
console.Title = originalConsoleTitle;
}
await Task.WhenAll(instances.Select(x => OfflineInstanceImmediate(x.Value.Instance, cancellationToken)));
await instanceFactoryStopTask;
await swarmServiceController.Shutdown(cancellationToken);
}
catch (Exception ex)
{
logger.LogCritical(ex, "Instance manager stop exception!");
}
}
catch (Exception ex)
{
logger.LogCritical(ex, "Instance manager stop exception!");
}
}
/// <inheritdoc />
@@ -535,6 +558,7 @@ namespace Tgstation.Server.Host.Components
try
{
logger.LogInformation("{versionString}", assemblyInformationProvider.VersionString);
console.Title = assemblyInformationProvider.VersionString;
CheckSystemCompatibility();
@@ -66,7 +66,7 @@ namespace Tgstation.Server.Host.Components.Repository
var mr = await client
.MergeRequests
.GetAsync($"{RemoteRepositoryOwner}/{RemoteRepositoryName}", parameters.Number)
.WithToken(cancellationToken);
.WaitAsync(cancellationToken);
var revisionToUse = parameters.TargetCommitSha == null
|| mr.Sha.StartsWith(parameters.TargetCommitSha, StringComparison.OrdinalIgnoreCase)
@@ -83,31 +83,35 @@ namespace Tgstation.Server.Host.Components.Repository
/// <summary>
/// Fetch commits from the origin repository.
/// </summary>
/// <param name="progressReporter">The <see cref="JobProgressReporter"/> to report progress of the operation.</param>
/// <param name="username">The username to fetch from the origin repository.</param>
/// <param name="password">The password to fetch from the origin repository.</param>
/// <param name="progressReporter">The <see cref="JobProgressReporter"/> to report progress of the operation.</param>
/// <param name="deploymentPipeline">If any events created should be marked as part of the deployment pipeline.</param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> for the operation.</param>
/// <returns>A <see cref="Task"/> representing the running operation.</returns>
Task FetchOrigin(
JobProgressReporter progressReporter,
string username,
string password,
JobProgressReporter progressReporter,
bool deploymentPipeline,
CancellationToken cancellationToken);
/// <summary>
/// Requires the current HEAD to be a tracked reference. Hard resets the reference to what it tracks on the origin repository.
/// </summary>
/// <param name="progressReporter">The <see cref="JobProgressReporter"/> to report progress of the operation.</param>
/// <param name="username">The username used for fetching from submodule repositories.</param>
/// <param name="password">The password used for fetching from submodule repositories.</param>
/// <param name="updateSubmodules">If a submodule update should be attempted after the merge.</param>
/// <param name="progressReporter">The <see cref="JobProgressReporter"/> to report progress of the operation.</param>
/// <param name="deploymentPipeline">If any events created should be marked as part of the deployment pipeline.</param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> for the operation.</param>
/// <returns>A <see cref="Task{TResult}"/> resulting in the SHA of the new HEAD.</returns>
Task ResetToOrigin(
JobProgressReporter progressReporter,
string username,
string password,
bool updateSubmodules,
JobProgressReporter progressReporter,
bool deploymentPipeline,
CancellationToken cancellationToken);
/// <summary>
@@ -122,31 +126,39 @@ namespace Tgstation.Server.Host.Components.Repository
/// <summary>
/// Requires the current HEAD to be a tracked reference. Merges the reference to what it tracks on the origin repository.
/// </summary>
/// <param name="progressReporter">The <see cref="JobProgressReporter"/> to report progress of the operation.</param>
/// <param name="committerName">The name of the merge committer.</param>
/// <param name="committerEmail">The e-mail of the merge committer.</param>
/// <param name="progressReporter">The <see cref="JobProgressReporter"/> to report progress of the operation.</param>
/// <param name="deploymentPipeline">If any events created should be marked as part of the deployment pipeline.</param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> for the operation.</param>
/// <returns>A <see cref="Task{TResult}"/> resulting in a <see cref="Nullable{T}"/> <see cref="bool"/> representing the merge result that is <see langword="true"/> after a fast forward, <see langword="false"/> on a merge or up to date, <see langword="null"/> on a conflict.</returns>
Task<bool?> MergeOrigin(string committerName, string committerEmail, JobProgressReporter progressReporter, CancellationToken cancellationToken);
Task<bool?> MergeOrigin(
JobProgressReporter progressReporter,
string committerName,
string committerEmail,
bool deploymentPipeline,
CancellationToken cancellationToken);
/// <summary>
/// Runs the synchronize event script and attempts to push any changes made to the <see cref="IRepository"/> if on a tracked branch.
/// </summary>
/// <param name="progressReporter">The <see cref="JobProgressReporter"/> to report progress of the operation.</param>
/// <param name="username">The username to fetch from the origin repository.</param>
/// <param name="password">The password to fetch from the origin repository.</param>
/// <param name="committerName">The name of the potential committer.</param>
/// <param name="committerEmail">The e-mail of the potential committer.</param>
/// <param name="progressReporter">The <see cref="JobProgressReporter"/> to report progress of the operation.</param>
/// <param name="synchronizeTrackedBranch">If the synchronizations should be made to the tracked reference as opposed to a temporary branch.</param>
/// <param name="deploymentPipeline">If any events created should be marked as part of the deployment pipeline.</param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> for the operation.</param>
/// <returns>A <see cref="Task{TResult}"/> resulting in <see langword="true"/> if commits were pushed to the tracked origin reference, <see langword="false"/> otherwise.</returns>
Task<bool> Sychronize(
JobProgressReporter progressReporter,
string username,
string password,
string committerName,
string committerEmail,
JobProgressReporter progressReporter,
bool synchronizeTrackedBranch,
bool deploymentPipeline,
CancellationToken cancellationToken);
/// <summary>
@@ -333,6 +333,7 @@ namespace Tgstation.Server.Host.Components.Repository
await eventConsumer.HandleEvent(
EventType.RepoMergeConflict,
arguments,
false,
cancellationToken);
return new TestMergeResult
{
@@ -359,6 +360,7 @@ namespace Tgstation.Server.Host.Components.Repository
progressReporter.CreateSection("Update Submodules", progressFactor),
username,
password,
false,
cancellationToken);
}
}
@@ -371,6 +373,7 @@ namespace Tgstation.Server.Host.Components.Repository
testMergeParameters.TargetCommitSha,
testMergeParameters.Comment,
},
false,
cancellationToken);
return new TestMergeResult
@@ -392,7 +395,7 @@ namespace Tgstation.Server.Host.Components.Repository
ArgumentNullException.ThrowIfNull(committish);
ArgumentNullException.ThrowIfNull(progressReporter);
logger.LogDebug("Checkout object: {committish}...", committish);
await eventConsumer.HandleEvent(EventType.RepoCheckout, new List<string> { committish }, cancellationToken);
await eventConsumer.HandleEvent(EventType.RepoCheckout, new List<string> { committish }, false, cancellationToken);
await Task.Factory.StartNew(
() =>
{
@@ -411,15 +414,21 @@ namespace Tgstation.Server.Host.Components.Repository
progressReporter.CreateSection(null, 1.0 / 3),
username,
password,
false,
cancellationToken);
}
/// <inheritdoc />
public async Task FetchOrigin(string username, string password, JobProgressReporter progressReporter, CancellationToken cancellationToken)
public async Task FetchOrigin(
JobProgressReporter progressReporter,
string username,
string password,
bool deploymentPipeline,
CancellationToken cancellationToken)
{
ArgumentNullException.ThrowIfNull(progressReporter);
logger.LogDebug("Fetch origin...");
await eventConsumer.HandleEvent(EventType.RepoFetch, Enumerable.Empty<string>(), cancellationToken);
await eventConsumer.HandleEvent(EventType.RepoFetch, Enumerable.Empty<string>(), deploymentPipeline, cancellationToken);
await Task.Factory.StartNew(
() =>
{
@@ -458,10 +467,11 @@ namespace Tgstation.Server.Host.Components.Repository
/// <inheritdoc />
public async Task ResetToOrigin(
JobProgressReporter progressReporter,
string username,
string password,
bool updateSubmodules,
JobProgressReporter progressReporter,
bool deploymentPipeline,
CancellationToken cancellationToken)
{
ArgumentNullException.ThrowIfNull(progressReporter);
@@ -469,7 +479,7 @@ namespace Tgstation.Server.Host.Components.Repository
throw new JobException(ErrorCode.RepoReferenceRequired);
logger.LogTrace("Reset to origin...");
var trackedBranch = libGitRepo.Head.TrackedBranch;
await eventConsumer.HandleEvent(EventType.RepoResetOrigin, new List<string> { trackedBranch.FriendlyName, trackedBranch.Tip.Sha }, cancellationToken);
await eventConsumer.HandleEvent(EventType.RepoResetOrigin, new List<string> { trackedBranch.FriendlyName, trackedBranch.Tip.Sha }, deploymentPipeline, cancellationToken);
await ResetToSha(
trackedBranch.Tip.Sha,
progressReporter.CreateSection(null, updateSubmodules ? 2.0 / 3 : 1.0),
@@ -480,6 +490,7 @@ namespace Tgstation.Server.Host.Components.Repository
progressReporter.CreateSection(null, 1.0 / 3),
username,
password,
deploymentPipeline,
cancellationToken);
}
@@ -547,9 +558,10 @@ namespace Tgstation.Server.Host.Components.Repository
/// <inheritdoc />
public async Task<bool?> MergeOrigin(
JobProgressReporter progressReporter,
string committerName,
string committerEmail,
JobProgressReporter progressReporter,
bool deploymentPipeline,
CancellationToken cancellationToken)
{
ArgumentNullException.ThrowIfNull(progressReporter);
@@ -606,7 +618,17 @@ namespace Tgstation.Server.Host.Components.Repository
if (result.Status == MergeStatus.Conflicts)
{
await eventConsumer.HandleEvent(EventType.RepoMergeConflict, new List<string> { oldTip.Sha, trackedBranch.Tip.Sha, oldHead.FriendlyName ?? UnknownReference, trackedBranch.FriendlyName }, cancellationToken);
await eventConsumer.HandleEvent(
EventType.RepoMergeConflict,
new List<string>
{
oldTip.Sha,
trackedBranch.Tip.Sha,
oldHead.FriendlyName ?? UnknownReference,
trackedBranch.FriendlyName,
},
deploymentPipeline,
cancellationToken);
return null;
}
@@ -615,12 +637,13 @@ namespace Tgstation.Server.Host.Components.Repository
/// <inheritdoc />
public async Task<bool> Sychronize(
JobProgressReporter progressReporter,
string username,
string password,
string committerName,
string committerEmail,
JobProgressReporter progressReporter,
bool synchronizeTrackedBranch,
bool deploymentPipeline,
CancellationToken cancellationToken)
{
ArgumentNullException.ThrowIfNull(committerName);
@@ -661,6 +684,7 @@ namespace Tgstation.Server.Host.Components.Repository
{
ioMananger.ResolvePath(),
},
deploymentPipeline,
cancellationToken);
}
finally
@@ -964,9 +988,15 @@ namespace Tgstation.Server.Host.Components.Repository
/// <param name="progressReporter"><see cref="JobProgressReporter"/> of the operation.</param>
/// <param name="username">The username for the <see cref="credentialsProvider"/>.</param>
/// <param name="password">The password for the <see cref="credentialsProvider"/>.</param>
/// <param name="deploymentPipeline">If any events created should be marked as part of the deployment pipeline.</param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> for the operation.</param>
/// <returns>A <see cref="Task"/> representing the running operation.</returns>
async Task UpdateSubmodules(JobProgressReporter progressReporter, string username, string password, CancellationToken cancellationToken)
async Task UpdateSubmodules(
JobProgressReporter progressReporter,
string username,
string password,
bool deploymentPipeline,
CancellationToken cancellationToken)
{
var submoduleCount = libGitRepo.Submodules.Count();
if (submoduleCount == 0)
@@ -1032,7 +1062,11 @@ namespace Tgstation.Server.Host.Components.Repository
}
}
await eventConsumer.HandleEvent(EventType.RepoSubmoduleUpdate, new List<string> { submodule.Name }, cancellationToken);
await eventConsumer.HandleEvent(
EventType.RepoSubmoduleUpdate,
new List<string> { submodule.Name },
deploymentPipeline,
cancellationToken);
}
}
@@ -152,11 +152,10 @@ namespace Tgstation.Server.Host.Components.Repository
CancellationToken cancellationToken)
#pragma warning restore CA1502, CA1506
{
var repoManager = instance.RepositoryManager;
using var repo = await repoManager.LoadRepository(cancellationToken);
if (repo == null)
throw new JobException(ErrorCode.RepoMissing);
_ = job; // shuts up an IDE warning
var repoManager = instance.RepositoryManager;
using var repo = await repoManager.LoadRepository(cancellationToken) ?? throw new JobException(ErrorCode.RepoMissing);
var modelHasShaOrReference = model.CheckoutSha != null || model.Reference != null;
var startReference = repo.Reference;
@@ -252,11 +251,21 @@ namespace Tgstation.Server.Host.Components.Repository
{
if (!repo.Tracking)
throw new JobException(ErrorCode.RepoReferenceRequired);
await repo.FetchOrigin(currentModel.AccessUser, currentModel.AccessToken, NextProgressReporter("Fetch Origin"), cancellationToken);
await repo.FetchOrigin(
NextProgressReporter("Fetch Origin"),
currentModel.AccessUser,
currentModel.AccessToken,
false,
cancellationToken);
if (!modelHasShaOrReference)
{
var fastForward = await repo.MergeOrigin(committerName, currentModel.CommitterEmail, NextProgressReporter("Merge Origin"), cancellationToken);
var fastForward = await repo.MergeOrigin(
NextProgressReporter("Merge Origin"),
committerName,
currentModel.CommitterEmail,
false,
cancellationToken);
if (!fastForward.HasValue)
throw new JobException(ErrorCode.RepoMergeConflict);
lastRevisionInfo.OriginCommitSha = await repo.GetOriginSha(cancellationToken);
@@ -264,12 +273,13 @@ namespace Tgstation.Server.Host.Components.Repository
if (fastForward.Value)
{
await repo.Sychronize(
NextProgressReporter("Sychronize"),
currentModel.AccessUser,
currentModel.AccessToken,
currentModel.CommitterName,
currentModel.CommitterEmail,
NextProgressReporter("Sychronize"),
true,
false,
cancellationToken);
postUpdateSha = repo.Head;
}
@@ -315,18 +325,20 @@ namespace Tgstation.Server.Host.Components.Repository
if (!repo.Tracking)
throw new JobException(ErrorCode.RepoReferenceNotTracking);
await repo.ResetToOrigin(
NextProgressReporter("Reset to Origin"),
currentModel.AccessUser,
currentModel.AccessToken,
updateSubmodules,
NextProgressReporter("Reset to Origin"),
false,
cancellationToken);
await repo.Sychronize(
NextProgressReporter("Synchronize"),
currentModel.AccessUser,
currentModel.AccessToken,
currentModel.CommitterName,
currentModel.CommitterEmail,
NextProgressReporter("Synchronize"),
true,
false,
cancellationToken);
await CallLoadRevInfo();
@@ -473,7 +485,7 @@ namespace Tgstation.Server.Host.Components.Repository
if (revInfoWereLookingFor != null)
{
// goteem
logger.LogDebug("Reusing existing SHA {0}...", revInfoWereLookingFor.CommitSha);
logger.LogDebug("Reusing existing SHA {sha}...", revInfoWereLookingFor.CommitSha);
await repo.ResetToSha(revInfoWereLookingFor.CommitSha, NextProgressReporter($"Reset to {revInfoWereLookingFor.CommitSha[..7]}"), cancellationToken);
lastRevisionInfo = revInfoWereLookingFor;
}
@@ -535,11 +547,12 @@ namespace Tgstation.Server.Host.Components.Repository
if (currentModel.PushTestMergeCommits.Value && (startSha != currentHead || (postUpdateSha != null && postUpdateSha != currentHead)))
{
await repo.Sychronize(
NextProgressReporter("Synchronize"),
currentModel.AccessUser,
currentModel.AccessToken,
currentModel.CommitterName,
currentModel.CommitterEmail,
NextProgressReporter("Synchronize"),
false,
false,
cancellationToken);
await UpdateRevInfo();
@@ -13,15 +13,14 @@ using Newtonsoft.Json;
using Serilog.Context;
using Tgstation.Server.Api;
using Tgstation.Server.Api.Models;
using Tgstation.Server.Common.Extensions;
using Tgstation.Server.Host.Components.Byond;
using Tgstation.Server.Host.Components.Chat;
using Tgstation.Server.Host.Components.Deployment;
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;
@@ -82,17 +81,15 @@ namespace Tgstation.Server.Host.Components.Session
get => rebootGate;
set
{
var tcs = new TaskCompletionSource();
Task toAwait = null;
var tcs = new TaskCompletionSource<Task>();
async Task Wrap()
{
await tcs.Task;
var toAwait = await tcs.Task;
await toAwait;
await value;
}
toAwait = Interlocked.Exchange(ref rebootGate, Wrap());
tcs.SetResult();
tcs.SetResult(Interlocked.Exchange(ref rebootGate, Wrap()));
}
}
@@ -779,7 +776,7 @@ namespace Tgstation.Server.Host.Components.Session
}
Interlocked.Exchange(ref rebootTcs, new TaskCompletionSource()).SetResult();
await RebootGate.WithToken(cancellationToken);
await RebootGate.WaitAsync(cancellationToken);
}
finally
{
@@ -10,9 +10,9 @@ using Byond.TopicSender;
using Microsoft.Extensions.Logging;
using Tgstation.Server.Api;
using Tgstation.Server.Api.Models;
using Tgstation.Server.Api.Models.Internal;
using Tgstation.Server.Common.Extensions;
using Tgstation.Server.Host.Components.Byond;
using Tgstation.Server.Host.Components.Chat;
using Tgstation.Server.Host.Components.Deployment;
@@ -552,6 +552,7 @@ namespace Tgstation.Server.Host.Components.Session
{
process.Id.ToString(CultureInfo.InvariantCulture),
},
false,
cancellationToken);
return process;
@@ -128,6 +128,11 @@ namespace Tgstation.Server.Host.Components.StaticFiles
/// </summary>
readonly GeneralConfiguration generalConfiguration;
/// <summary>
/// The <see cref="SessionConfiguration"/> for <see cref="Configuration"/>.
/// </summary>
readonly SessionConfiguration sessionConfiguration;
/// <summary>
/// The <see cref="SemaphoreSlim"/> for <see cref="Configuration"/>. Also used as a <see langword="lock"/> <see cref="object"/>.
/// </summary>
@@ -155,6 +160,7 @@ namespace Tgstation.Server.Host.Components.StaticFiles
/// <param name="fileTransferService">The value of <see cref="fileTransferService"/>.</param>
/// <param name="logger">The value of <see cref="logger"/>.</param>
/// <param name="generalConfiguration">The value of <see cref="generalConfiguration"/>.</param>
/// <param name="sessionConfiguration">The value of <see cref="sessionConfiguration"/>.</param>
public Configuration(
IIOManager ioManager,
ISynchronousIOManager synchronousIOManager,
@@ -164,7 +170,8 @@ namespace Tgstation.Server.Host.Components.StaticFiles
IPlatformIdentifier platformIdentifier,
IFileTransferTicketProvider fileTransferService,
ILogger<Configuration> logger,
GeneralConfiguration generalConfiguration)
GeneralConfiguration generalConfiguration,
SessionConfiguration sessionConfiguration)
{
this.ioManager = ioManager ?? throw new ArgumentNullException(nameof(ioManager));
this.synchronousIOManager = synchronousIOManager ?? throw new ArgumentNullException(nameof(synchronousIOManager));
@@ -175,6 +182,7 @@ namespace Tgstation.Server.Host.Components.StaticFiles
this.fileTransferService = fileTransferService ?? throw new ArgumentNullException(nameof(fileTransferService));
this.logger = logger ?? throw new ArgumentNullException(nameof(logger));
this.generalConfiguration = generalConfiguration ?? throw new ArgumentNullException(nameof(generalConfiguration));
this.sessionConfiguration = sessionConfiguration ?? throw new ArgumentNullException(nameof(sessionConfiguration));
semaphore = new SemaphoreSlim(1);
disposeCts = new CancellationTokenSource();
@@ -592,7 +600,7 @@ namespace Tgstation.Server.Host.Components.StaticFiles
public Task StopAsync(CancellationToken cancellationToken) => EnsureDirectories(cancellationToken);
/// <inheritdoc />
public async Task HandleEvent(EventType eventType, IEnumerable<string> parameters, CancellationToken cancellationToken)
public async Task HandleEvent(EventType eventType, IEnumerable<string> parameters, bool deploymentPipeline, CancellationToken cancellationToken)
{
ArgumentNullException.ThrowIfNull(parameters);
@@ -639,6 +647,9 @@ namespace Tgstation.Server.Host.Components.StaticFiles
noShellExecute: true))
using (cancellationToken.Register(() => script.Terminate()))
{
if (sessionConfiguration.LowPriorityDeploymentProcesses)
script.AdjustPriority(false);
var exitCode = await script.Lifetime;
cancellationToken.ThrowIfCancellationRequested();
var scriptOutput = await script.GetCombinedOutput(cancellationToken);
@@ -112,7 +112,7 @@ namespace Tgstation.Server.Host.Components.Watchdog
var eventType = Server.TerminationWasRequested
? EventType.WorldEndProcess
: EventType.WatchdogCrash;
await HandleEvent(eventType, Enumerable.Empty<string>(), false, cancellationToken);
await HandleEventImpl(eventType, Enumerable.Empty<string>(), false, cancellationToken);
var exitWord = Server.TerminationWasRequested ? "exited" : "crashed";
if (Server.RebootState == Session.RebootState.Shutdown)
@@ -143,7 +143,7 @@ namespace Tgstation.Server.Host.Components.Watchdog
gracefulRebootRequired = false;
Server.ResetRebootState();
var eventTask = HandleEvent(EventType.WorldReboot, Enumerable.Empty<string>(), false, cancellationToken);
var eventTask = HandleEventImpl(EventType.WorldReboot, Enumerable.Empty<string>(), false, cancellationToken);
try
{
switch (rebootState)
@@ -174,7 +174,7 @@ namespace Tgstation.Server.Host.Components.Watchdog
await HandleNewDmbAvailable(cancellationToken);
break;
case MonitorActivationReason.ActiveServerPrimed:
await HandleEvent(EventType.WorldPrime, Enumerable.Empty<string>(), false, cancellationToken);
await HandleEventImpl(EventType.WorldPrime, Enumerable.Empty<string>(), false, cancellationToken);
break;
case MonitorActivationReason.ActiveServerStartup:
break; // unused in BasicWatchdog
@@ -411,7 +411,7 @@ namespace Tgstation.Server.Host.Components.Watchdog
if (Status != WatchdogStatus.Offline)
{
Logger.LogDebug("Waiting for server to gracefully shut down.");
await monitorTask.WithToken(cancellationToken);
await monitorTask.WaitAsync(cancellationToken);
}
else
Logger.LogTrace("Graceful shutdown requested but server is already offline.");
@@ -449,7 +449,7 @@ namespace Tgstation.Server.Host.Components.Watchdog
}
/// <inheritdoc />
async Task IEventConsumer.HandleEvent(EventType eventType, IEnumerable<string> parameters, CancellationToken cancellationToken)
async Task IEventConsumer.HandleEvent(EventType eventType, IEnumerable<string> parameters, bool deploymentPipeline, CancellationToken cancellationToken)
{
ArgumentNullException.ThrowIfNull(parameters);
@@ -509,7 +509,7 @@ namespace Tgstation.Server.Host.Components.Watchdog
? "Launching..."
: "Reattaching..."); // simple announce
if (reattachInfo == null)
eventTask = HandleEvent(EventType.WatchdogLaunch, Enumerable.Empty<string>(), false, cancellationToken);
eventTask = HandleEventImpl(EventType.WatchdogLaunch, Enumerable.Empty<string>(), false, cancellationToken);
}
// since neither server is running, this is safe to do
@@ -589,7 +589,7 @@ namespace Tgstation.Server.Host.Components.Watchdog
/// <returns>A <see cref="Task"/> representing the running operation.</returns>
protected async Task CheckLaunchResult(ISessionController controller, string serverName, CancellationToken cancellationToken)
{
var launchResult = await controller.LaunchResult.WithToken(cancellationToken);
var launchResult = await controller.LaunchResult.WaitAsync(cancellationToken);
// Dead sessions won't trigger this
if (launchResult.ExitCode.HasValue) // you killed us ray...
@@ -693,13 +693,13 @@ namespace Tgstation.Server.Host.Components.Watchdog
/// <param name="relayToSession">If the event should be sent to DreamDaemon.</param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> for the operation.</param>
/// <returns>A <see cref="Task"/> representing the running operation.</returns>
protected async Task HandleEvent(EventType eventType, IEnumerable<string> parameters, bool relayToSession, CancellationToken cancellationToken)
protected async Task HandleEventImpl(EventType eventType, IEnumerable<string> parameters, bool relayToSession, CancellationToken cancellationToken)
{
try
{
var sessionEventTask = relayToSession ? ((IEventConsumer)this).HandleEvent(eventType, parameters, cancellationToken) : Task.CompletedTask;
var sessionEventTask = relayToSession ? ((IEventConsumer)this).HandleEvent(eventType, parameters, false, cancellationToken) : Task.CompletedTask;
await Task.WhenAll(
eventConsumer.HandleEvent(eventType, parameters, cancellationToken),
eventConsumer.HandleEvent(eventType, parameters, false, cancellationToken),
sessionEventTask);
}
catch (JobException ex)
@@ -872,7 +872,7 @@ namespace Tgstation.Server.Host.Components.Watchdog
serverPrimed);
// wait for something to happen
await toWaitOn.WithToken(cancellationToken);
await toWaitOn.WaitAsync(cancellationToken);
cancellationToken.ThrowIfCancellationRequested();
Logger.LogTrace("Monitor activated");
@@ -1005,7 +1005,7 @@ namespace Tgstation.Server.Host.Components.Watchdog
return;
if (!graceful)
{
var eventTask = HandleEvent(
var eventTask = HandleEventImpl(
releaseServers
? EventType.WatchdogDetach
: EventType.WatchdogShutdown,
@@ -34,11 +34,6 @@ namespace Tgstation.Server.Host.Configuration
/// </summary>
public bool DropDatabase { get; set; }
/// <summary>
/// Used to indicate that the database is being loaded to generate migrations. Should not be used in production!.
/// </summary>
public bool DesignTime { get; set; }
/// <summary>
/// The <see cref="string"/> form of the <see cref="global::System.Version"/> of the target server.
/// </summary>
@@ -10,21 +10,6 @@
/// </summary>
public const string Section = "Elasticsearch";
/// <summary>
/// Default value of <see cref="Host"/>.
/// </summary>
const string DefaultHost = "http://127.0.0.1:9200"; // localhost
/// <summary>
/// Default value of <see cref="Username"/>.
/// </summary>
const string DefaultUsername = "my_username";
/// <summary>
/// Default value of <see cref="Password"/>.
/// </summary>
const string DefaultPassword = "my_password";
/// <summary>
/// Do we want to enable elasticsearch or not?.
/// </summary>
@@ -33,16 +18,16 @@
/// <summary>
/// The host of the elasticsearch endpoint.
/// </summary>
public string Host { get; set; } = DefaultHost;
public string Host { get; set; }
/// <summary>
/// Username for elasticsearch.
/// </summary>
public string Username { get; set; } = DefaultUsername;
public string Username { get; set; }
/// <summary>
/// Password for elasticsearch.
/// </summary>
public string Password { get; set; } = DefaultPassword;
public string Password { get; set; }
}
}
@@ -75,7 +75,8 @@ namespace Tgstation.Server.Host.Configuration
? Directory
: ioManager.ConcatPath(
directoryToUse,
assemblyInformationProvider.VersionPrefix);
assemblyInformationProvider.VersionPrefix,
"logs");
}
}
}
@@ -0,0 +1,28 @@
namespace Tgstation.Server.Host.Configuration
{
/// <summary>
/// Unstable configuration options used internally by TGS.
/// </summary>
public sealed class InternalConfiguration
{
/// <summary>
/// The key for the <see cref="Microsoft.Extensions.Configuration.IConfigurationSection"/> the <see cref="InternalConfiguration"/> resides in.
/// </summary>
public const string Section = "Internal";
/// <summary>
/// The name of the pipe opened by the host watchdog, if any.
/// </summary>
public string CommandPipe { get; set; }
/// <summary>
/// If the server is running under SystemD.
/// </summary>
public bool UsingSystemD { get; set; }
/// <summary>
/// The base path for the app settings configuration files.
/// </summary>
public string AppSettingsBasePath { get; set; }
}
}
@@ -41,9 +41,9 @@ namespace Tgstation.Server.Host.Controllers
const string OctokitException = "Bad GitHub API response, check configuration!";
/// <summary>
/// The <see cref="IGitHubService"/> for the <see cref="AdministrationController"/>.
/// The <see cref="IGitHubServiceFactory"/> for the <see cref="AdministrationController"/>.
/// </summary>
readonly IGitHubService gitHubService;
readonly IGitHubServiceFactory gitHubServiceFactory;
/// <summary>
/// The <see cref="IServerControl"/> for the <see cref="AdministrationController"/>.
@@ -85,7 +85,7 @@ namespace Tgstation.Server.Host.Controllers
/// </summary>
/// <param name="databaseContext">The <see cref="IDatabaseContext"/> for the <see cref="ApiController"/>.</param>
/// <param name="authenticationContextFactory">The <see cref="IAuthenticationContextFactory"/> for the <see cref="ApiController"/>.</param>
/// <param name="gitHubService">The value of <see cref="gitHubService"/>.</param>
/// <param name="gitHubServiceFactory">The value of <see cref="gitHubServiceFactory"/>.</param>
/// <param name="serverControl">The value of <see cref="serverControl"/>.</param>
/// <param name="serverUpdateInitiator">The value of <see cref="serverUpdateInitiator"/>.</param>
/// <param name="assemblyInformationProvider">The value of <see cref="assemblyInformationProvider"/>.</param>
@@ -97,7 +97,7 @@ namespace Tgstation.Server.Host.Controllers
public AdministrationController(
IDatabaseContext databaseContext,
IAuthenticationContextFactory authenticationContextFactory,
IGitHubService gitHubService,
IGitHubServiceFactory gitHubServiceFactory,
IServerControl serverControl,
IServerUpdateInitiator serverUpdateInitiator,
IAssemblyInformationProvider assemblyInformationProvider,
@@ -112,7 +112,7 @@ namespace Tgstation.Server.Host.Controllers
logger,
true)
{
this.gitHubService = gitHubService ?? throw new ArgumentNullException(nameof(gitHubService));
this.gitHubServiceFactory = gitHubServiceFactory ?? throw new ArgumentNullException(nameof(gitHubServiceFactory));
this.serverControl = serverControl ?? throw new ArgumentNullException(nameof(serverControl));
this.serverUpdateInitiator = serverUpdateInitiator ?? throw new ArgumentNullException(nameof(serverUpdateInitiator));
this.assemblyInformationProvider = assemblyInformationProvider ?? throw new ArgumentNullException(nameof(assemblyInformationProvider));
@@ -143,6 +143,7 @@ namespace Tgstation.Server.Host.Controllers
Uri repoUrl = null;
try
{
var gitHubService = gitHubServiceFactory.CreateService();
var repositoryUrlTask = gitHubService.GetUpdatesRepositoryUrl(cancellationToken);
var releases = await gitHubService.GetTgsReleases(cancellationToken);
@@ -22,6 +22,7 @@ using Serilog.Context;
using Tgstation.Server.Api;
using Tgstation.Server.Api.Models;
using Tgstation.Server.Api.Models.Response;
using Tgstation.Server.Common.Extensions;
using Tgstation.Server.Host.Database;
using Tgstation.Server.Host.Extensions;
using Tgstation.Server.Host.Models;

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