diff --git a/.codecov.yml b/.codecov.yml index 86207739c5..a55ee4d34f 100644 --- a/.codecov.yml +++ b/.codecov.yml @@ -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" diff --git a/.dockerignore b/.dockerignore index 384f226cd7..0a5c0e76af 100644 --- a/.dockerignore +++ b/.dockerignore @@ -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 diff --git a/.editorconfig b/.editorconfig index 5c4a83ecd1..55e95ea69f 100644 --- a/.editorconfig +++ b/.editorconfig @@ -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 diff --git a/.github/CONTRIBUTING.md b/.github/CONTRIBUTING.md index e6c9393e81..6f4a8cdab8 100644 --- a/.github/CONTRIBUTING.md +++ b/.github/CONTRIBUTING.md @@ -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 } ``` diff --git a/.github/workflows/ci-suite.yml b/.github/workflows/ci-pipeline.yml similarity index 74% rename from .github/workflows/ci-suite.yml rename to .github/workflows/ci-pipeline.yml index 0f88ac9211..07b19f07b5 100644 --- a/.github/workflows/ci-suite.yml +++ b/.github/workflows/ci-pipeline.yml @@ -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'))))" @@ -219,11 +239,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 @@ -259,11 +282,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 @@ -379,6 +405,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 @@ -542,14 +585,17 @@ 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: Run Live Tests 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 @@ -563,18 +609,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 - rm ../../Artifacts/Console/Tgstation.Server.Host.Console - 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' }} @@ -875,30 +918,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 @@ -911,6 +955,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_* @@ -920,11 +990,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 in 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 @@ -1066,8 +1311,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 @@ -1083,6 +1334,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: | @@ -1120,6 +1401,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: | @@ -1130,7 +1433,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 @@ -1203,6 +1506,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 @@ -1244,4 +1557,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 }} diff --git a/.github/workflows/code-scanning.yml b/.github/workflows/code-scanning.yml index e69d6ed259..8e130eddf4 100644 --- a/.github/workflows/code-scanning.yml +++ b/.github/workflows/code-scanning.yml @@ -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 diff --git a/.gitignore b/.gitignore index 9d6c8caecd..56aae9a8b9 100644 --- a/.gitignore +++ b/.gitignore @@ -5,6 +5,7 @@ _ReSharper.* packages/ artifacts/ .vs/ +.vscode/ *.user *.suo *.userprefs diff --git a/.vscode/extensions.json b/.vscode/extensions.json deleted file mode 100644 index 884b701d57..0000000000 --- a/.vscode/extensions.json +++ /dev/null @@ -1,9 +0,0 @@ -{ - "recommendations": [ - "gbasood.byond-dm-language-support", - "ms-vscode.csharp", - "k--kato.docomment", - "peterjausovec.vscode-docker", - "bbenoist.doxygen" - ] -} diff --git a/.vscode/launch.json b/.vscode/launch.json deleted file mode 100644 index fadbf977fe..0000000000 --- a/.vscode/launch.json +++ /dev/null @@ -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 - } - ] -} diff --git a/.vscode/tasks.json b/.vscode/tasks.json deleted file mode 100644 index 2582679179..0000000000 --- a/.vscode/tasks.json +++ /dev/null @@ -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 - } - } - ] -} diff --git a/README.md b/README.md index bb07b4281e..b971d086e2 100644 --- a/README.md +++ b/README.md @@ -18,7 +18,25 @@ Older server versions can be found in the V# branches of this repository. Note t ### Pre-Requisites -- 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 @@ -26,17 +44,60 @@ Follow the instructions for your OS below. #### Windows -Download and install the [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 (Most modern systems have it by default. 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), you can test this by opening a command prompt and running `dotnet --list-runtimes`. +###### Note about Digital Signatures -[Download the latest release .zip](https://github.com/tgstation/tgstation-server/releases/latest). You probably want the `ServerService` package. Choose `ServerConsole` if you prefer not to use the Windows service. +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. -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. +##### winget (Windows 10 or later) -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. +[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 diff --git a/build/Common.props b/build/Common.props index 95ade51555..89b88f109b 100644 --- a/build/Common.props +++ b/build/Common.props @@ -2,7 +2,7 @@ - net6.0 + net$(TgsNetMajorVersion).0 netstandard2.0 latest Full diff --git a/build/NugetCommon.props b/build/NugetCommon.props index d844ff4b1a..886f98c028 100644 --- a/build/NugetCommon.props +++ b/build/NugetCommon.props @@ -7,6 +7,7 @@ /tg/station 13 https://tgstation.github.io/tgstation-server LICENSE + README.md tgs.png Git https://github.com/tgstation/tgstation-server @@ -17,6 +18,7 @@ + diff --git a/build/Version.props b/build/Version.props index fbd5c5c101..be345f49cd 100644 --- a/build/Version.props +++ b/build/Version.props @@ -13,6 +13,10 @@ 5.6.1 1.3.0 1.2.1 - 1.0.1 + 1.0.2 + 6 + + + https://dotnetcli.azureedge.net/dotnet/aspnetcore/Runtime/6.0.19/dotnet-hosting-6.0.19-win.exe diff --git a/build/package/appsettings.GitHub.yml b/build/package/appsettings.GitHub.yml new file mode 100644 index 0000000000..610a2ccd76 --- /dev/null +++ b/build/package/appsettings.GitHub.yml @@ -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 diff --git a/build/package/appsettings.Initial.yml b/build/package/appsettings.Initial.yml new file mode 100644 index 0000000000..1996a8a881 --- /dev/null +++ b/build/package/appsettings.Initial.yml @@ -0,0 +1,2 @@ +# tgstation-server configuration file +# See appsettings.yml or README.md for details on individual configuration options diff --git a/build/package/deb/MakeInstall b/build/package/deb/MakeInstall index ac109ab592..f0d34e6963 100755 --- a/build/package/deb/MakeInstall +++ b/build/package/deb/MakeInstall @@ -2,6 +2,6 @@ install: build/package/deb/install_artifacts.sh "$(DESTDIR)" - install -D build/package/deb/appsettings.Initial.yml "$(DESTDIR)/etc/tgstation-server/appsettings.Production.yml" - install src/Tgstation.Server.Host/appsettings.yml "$(DESTDIR)/etc/tgstation-server/appsettings.ex.yml" + 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" diff --git a/build/package/deb/appsettings.Initial.yml b/build/package/deb/appsettings.Initial.yml deleted file mode 100644 index 575df3fd32..0000000000 --- a/build/package/deb/appsettings.Initial.yml +++ /dev/null @@ -1,2 +0,0 @@ -# tgstation-server configuration file -# See /opt/tgstation-server/appsettings.yml for details on individual configuration options diff --git a/build/package/deb/debian/control b/build/package/deb/debian/control index aa670a2e2c..a4e47829c7 100644 --- a/build/package/deb/debian/control +++ b/build/package/deb/debian/control @@ -21,8 +21,8 @@ Depends: libstdc++6:i386 [amd64], libstdc++6 [i386], gcc-multilib [amd64], - libsystemd0, 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. diff --git a/build/package/deb/debian/copyright b/build/package/deb/debian/copyright index 31b11fc83b..5c4e3b7bcb 100644 --- a/build/package/deb/debian/copyright +++ b/build/package/deb/debian/copyright @@ -7,4 +7,4 @@ Files: * Copyright: 2023 Jordan Dominion -License: AGPL-3 +License: AGPL-3.0 diff --git a/build/package/deb/debian/links b/build/package/deb/debian/links deleted file mode 100644 index 8a960e0a76..0000000000 --- a/build/package/deb/debian/links +++ /dev/null @@ -1 +0,0 @@ -/etc/tgstation-server/appsettings.Production.yml /opt/tgstation-server/appsettings.Production.yml diff --git a/build/package/deb/debian/rules b/build/package/deb/debian/rules index 8e185ba247..7fd8d564fa 100755 --- a/build/package/deb/debian/rules +++ b/build/package/deb/debian/rules @@ -7,15 +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/Tgstation.Server.Host.Console + rm artifacts/lib/Default/appsettings.yml override_dh_auto_install: cp build/package/deb/MakeInstall ./Makefile diff --git a/build/package/winget/.config/dotnet-tools.json b/build/package/winget/.config/dotnet-tools.json new file mode 100644 index 0000000000..6d50916bb0 --- /dev/null +++ b/build/package/winget/.config/dotnet-tools.json @@ -0,0 +1,12 @@ +{ + "version": 1, + "isRoot": true, + "tools": { + "wix": { + "version": "4.0.1", + "commands": [ + "wix" + ] + } + } +} diff --git a/build/package/winget/Tgstation.Server.Host.Service.Wix.Bundle/Bundle.wxs b/build/package/winget/Tgstation.Server.Host.Service.Wix.Bundle/Bundle.wxs new file mode 100644 index 0000000000..62a558e1f1 --- /dev/null +++ b/build/package/winget/Tgstation.Server.Host.Service.Wix.Bundle/Bundle.wxs @@ -0,0 +1,25 @@ + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/build/package/winget/Tgstation.Server.Host.Service.Wix.Bundle/Tgstation.Server.Host.Service.Wix.Bundle.wixproj b/build/package/winget/Tgstation.Server.Host.Service.Wix.Bundle/Tgstation.Server.Host.Service.Wix.Bundle.wixproj new file mode 100644 index 0000000000..ab59841b93 --- /dev/null +++ b/build/package/winget/Tgstation.Server.Host.Service.Wix.Bundle/Tgstation.Server.Host.Service.Wix.Bundle.wixproj @@ -0,0 +1,29 @@ + + + + ProductVersion=$(TgsCoreVersion);NetMajorVersion=$(TgsNetMajorVersion);AspNetRedistUrl=$(TgsRedistUrl) + Bundle + x86 + tgstation-server-installer + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/build/package/winget/Tgstation.Server.Host.Service.Wix.Extensions/CustomAction.config b/build/package/winget/Tgstation.Server.Host.Service.Wix.Extensions/CustomAction.config new file mode 100644 index 0000000000..aa417a104d --- /dev/null +++ b/build/package/winget/Tgstation.Server.Host.Service.Wix.Extensions/CustomAction.config @@ -0,0 +1,7 @@ + + + + + + + diff --git a/build/package/winget/Tgstation.Server.Host.Service.Wix.Extensions/InstallationExtensions.cs b/build/package/winget/Tgstation.Server.Host.Service.Wix.Extensions/InstallationExtensions.cs new file mode 100644 index 0000000000..02a90bdc43 --- /dev/null +++ b/build/package/winget/Tgstation.Server.Host.Service.Wix.Extensions/InstallationExtensions.cs @@ -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; + + /// + /// Extension methods for the .msi installer. + /// + public static class InstallationExtensions + { + /// + /// Attempts to detach stop the existing tgstation-server service if it exists. + /// + /// The installer . + /// The of the custom action. + [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; + } + } + } +} diff --git a/build/package/winget/Tgstation.Server.Host.Service.Wix.Extensions/Tgstation.Server.Host.Service.Wix.Extensions.csproj b/build/package/winget/Tgstation.Server.Host.Service.Wix.Extensions/Tgstation.Server.Host.Service.Wix.Extensions.csproj new file mode 100644 index 0000000000..acadffcec1 --- /dev/null +++ b/build/package/winget/Tgstation.Server.Host.Service.Wix.Extensions/Tgstation.Server.Host.Service.Wix.Extensions.csproj @@ -0,0 +1,27 @@ + + + + + net2.0 + $(TgsCoreVersion) + + + + + + + + + True + + + + + + + + + + + + diff --git a/build/package/winget/Tgstation.Server.Host.Service.Wix/Package.en-us.wxl b/build/package/winget/Tgstation.Server.Host.Service.Wix/Package.en-us.wxl new file mode 100644 index 0000000000..b1675c9c30 --- /dev/null +++ b/build/package/winget/Tgstation.Server.Host.Service.Wix/Package.en-us.wxl @@ -0,0 +1,11 @@ + + + + + + + + + diff --git a/build/package/winget/Tgstation.Server.Host.Service.Wix/Package.wxs b/build/package/winget/Tgstation.Server.Host.Service.Wix/Package.wxs new file mode 100644 index 0000000000..e136a27cd9 --- /dev/null +++ b/build/package/winget/Tgstation.Server.Host.Service.Wix/Package.wxs @@ -0,0 +1,87 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/build/package/winget/Tgstation.Server.Host.Service.Wix/Tgstation.Server.Host.Service.Wix.wixproj b/build/package/winget/Tgstation.Server.Host.Service.Wix/Tgstation.Server.Host.Service.Wix.wixproj new file mode 100644 index 0000000000..a3aa164719 --- /dev/null +++ b/build/package/winget/Tgstation.Server.Host.Service.Wix/Tgstation.Server.Host.Service.Wix.wixproj @@ -0,0 +1,34 @@ + + + + ProductVersion=$(TgsCoreVersion) + 5150;5151 + x86 + tgstation-server + + + + + + + + InitialHostComponentGroup + DefaultLibDirectory + true + true + true + + + ServiceHostWatchdogComponentGroup + ApplicationDirectory + true + + + + + + + + + + \ No newline at end of file diff --git a/build/package/winget/manifest/Tgstation.Server.installer.yaml b/build/package/winget/manifest/Tgstation.Server.installer.yaml new file mode 100644 index 0000000000..fc2aa030bb --- /dev/null +++ b/build/package/winget/manifest/Tgstation.Server.installer.yaml @@ -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 diff --git a/build/package/winget/manifest/Tgstation.Server.locale.en-US.yaml b/build/package/winget/manifest/Tgstation.Server.locale.en-US.yaml new file mode 100644 index 0000000000..e4a446ca75 --- /dev/null +++ b/build/package/winget/manifest/Tgstation.Server.locale.en-US.yaml @@ -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 diff --git a/build/package/winget/manifest/Tgstation.Server.yaml b/build/package/winget/manifest/Tgstation.Server.yaml new file mode 100644 index 0000000000..7b7d3b8eba --- /dev/null +++ b/build/package/winget/manifest/Tgstation.Server.yaml @@ -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 diff --git a/build/package/winget/prepare_installer_input_artifacts.ps1 b/build/package/winget/prepare_installer_input_artifacts.ps1 new file mode 100644 index 0000000000..df88aca955 --- /dev/null +++ b/build/package/winget/prepare_installer_input_artifacts.ps1 @@ -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 +} diff --git a/build/package/winget/push_manifest.ps1 b/build/package/winget/push_manifest.ps1 new file mode 100644 index 0000000000..12b4135771 --- /dev/null +++ b/build/package/winget/push_manifest.ps1 @@ -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 ../../../.. +} diff --git a/build/tgstation-server.service b/build/tgstation-server.service index 837d8fb21f..4f6f9b81cd 100644 --- a/build/tgstation-server.service +++ b/build/tgstation-server.service @@ -10,15 +10,13 @@ After=mssql-server.service Type=notify-reload NotifyAccess=all WorkingDirectory=/opt/tgstation-server -ExecStart=/usr/bin/dotnet Tgstation.Server.Host.Console.dll --General:SetupWizardMode=Never +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 diff --git a/src/Tgstation.Server.Host/manifest.xml b/build/uac_elevation_manifest.xml similarity index 97% rename from src/Tgstation.Server.Host/manifest.xml rename to build/uac_elevation_manifest.xml index 5173bf8331..05ddda9c2a 100644 --- a/src/Tgstation.Server.Host/manifest.xml +++ b/build/uac_elevation_manifest.xml @@ -1,11 +1,11 @@ - - - - - - - - - - - + + + + + + + + + + + diff --git a/docs/Features.dox b/docs/Features.dox index 3c1430740f..6c1f42d3b3 100644 --- a/docs/Features.dox +++ b/docs/Features.dox @@ -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 diff --git a/omnisharp.json b/omnisharp.json deleted file mode 100644 index c214653d78..0000000000 --- a/omnisharp.json +++ /dev/null @@ -1,6 +0,0 @@ -{ - "msbuild": { - "Configuration": "DebugNoService", - "EnablePackageAutoRestore" : true - } -} diff --git a/src/Tgstation.Server.Api/ApiHeaders.cs b/src/Tgstation.Server.Api/ApiHeaders.cs index 0ad8c7a487..c8a3093c78 100644 --- a/src/Tgstation.Server.Api/ApiHeaders.cs +++ b/src/Tgstation.Server.Api/ApiHeaders.cs @@ -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"; /// - /// Added to in netstandard2.1. Can't use because of Tgstation.Server.Migrator. + /// Added to in netstandard2.1. Can't use because of lack of .NET Framework support. /// public const string ApplicationJsonMime = "application/json"; diff --git a/src/Tgstation.Server.Api/README.md b/src/Tgstation.Server.Api/README.md index bbfc100bcc..97a3631f44 100644 --- a/src/Tgstation.Server.Api/README.md +++ b/src/Tgstation.Server.Api/README.md @@ -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. diff --git a/src/Tgstation.Server.Api/Tgstation.Server.Api.csproj b/src/Tgstation.Server.Api/Tgstation.Server.Api.csproj index cb44534a9c..09a0d1bac7 100644 --- a/src/Tgstation.Server.Api/Tgstation.Server.Api.csproj +++ b/src/Tgstation.Server.Api/Tgstation.Server.Api.csproj @@ -48,4 +48,8 @@ + + + + diff --git a/src/Tgstation.Server.Client/README.md b/src/Tgstation.Server.Client/README.md new file mode 100644 index 0000000000..e958404fd9 --- /dev/null +++ b/src/Tgstation.Server.Client/README.md @@ -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 CreateClientWithDefaultCredentials(CancellationToken cancellationToken) +{ + + + return await clientFactory.CreateFromLogin( + url, + DefaultCredentials.AdminUserName, + DefaultCredentials.DefaultAdminUserPassword, + cancellationToken: cancellationToken); +} +``` diff --git a/src/Tgstation.Server.Client/Tgstation.Server.Client.csproj b/src/Tgstation.Server.Client/Tgstation.Server.Client.csproj index 07e99c9dd4..490a1c51dc 100644 --- a/src/Tgstation.Server.Client/Tgstation.Server.Client.csproj +++ b/src/Tgstation.Server.Client/Tgstation.Server.Client.csproj @@ -27,7 +27,6 @@ - diff --git a/src/Tgstation.Server.Api/VersionExtensions.cs b/src/Tgstation.Server.Common/Extensions/VersionExtensions.cs similarity index 94% rename from src/Tgstation.Server.Api/VersionExtensions.cs rename to src/Tgstation.Server.Common/Extensions/VersionExtensions.cs index 9a194f0abb..3e6b3102bd 100644 --- a/src/Tgstation.Server.Api/VersionExtensions.cs +++ b/src/Tgstation.Server.Common/Extensions/VersionExtensions.cs @@ -1,6 +1,6 @@ using System; -namespace Tgstation.Server.Api +namespace Tgstation.Server.Common.Extensions { /// /// Extensions for the class. diff --git a/src/Tgstation.Server.Common/README.md b/src/Tgstation.Server.Common/README.md new file mode 100644 index 0000000000..b6a45d16fd --- /dev/null +++ b/src/Tgstation.Server.Common/README.md @@ -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. diff --git a/src/Tgstation.Server.Host.Common/Constants.cs b/src/Tgstation.Server.Host.Common/Constants.cs new file mode 100644 index 0000000000..9d0133bb0f --- /dev/null +++ b/src/Tgstation.Server.Host.Common/Constants.cs @@ -0,0 +1,13 @@ +namespace Tgstation.Server.Host.Common +{ + /// + /// Constant values. + /// + public static class Constants + { + /// + /// The name of the project. + /// + public const string CanonicalPackageName = "tgstation-server"; + } +} diff --git a/src/Tgstation.Server.Host.Common/PipeCommands.cs b/src/Tgstation.Server.Host.Common/PipeCommands.cs new file mode 100644 index 0000000000..42a8525a46 --- /dev/null +++ b/src/Tgstation.Server.Host.Common/PipeCommands.cs @@ -0,0 +1,49 @@ +namespace Tgstation.Server.Host.Common +{ + /// + /// Values able to be passed via the update file path. + /// + public static class PipeCommands + { + /// + /// Stops the server ASAP, shutting down any running instances. + /// + public const string CommandStop = "stop"; + + /// + /// Stops the server eventually, waiting for the games in any running instances to reboot. + /// + public const string CommandGracefulShutdown = "graceful"; + + /// + /// Stops the server ASAP, detaching the watchdog for any running instances. + /// + public const string CommandDetachingShutdown = "detach"; + +#if NET6_0_OR_GREATER + /// + /// All of the represented as a . + /// + public static System.Collections.Generic.IReadOnlyList AllCommands { get; } = new[] + { + CommandStop, + CommandGracefulShutdown, + CommandDetachingShutdown, + }; +#endif + + /// + /// Gets the value of a given . + /// + /// The . + /// The value of the command or if it was unrecognized. + 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, + }; + } +} diff --git a/src/Tgstation.Server.Host.Common/Tgstation.Server.Host.Common.csproj b/src/Tgstation.Server.Host.Common/Tgstation.Server.Host.Common.csproj index dbe6263f1d..90389d89a6 100644 --- a/src/Tgstation.Server.Host.Common/Tgstation.Server.Host.Common.csproj +++ b/src/Tgstation.Server.Host.Common/Tgstation.Server.Host.Common.csproj @@ -2,7 +2,7 @@ - netstandard2.0 + $(TgsFrameworkVersion);net2.0 $(TgsCoreVersion) false true diff --git a/src/Tgstation.Server.Host.Watchdog/SignalChecker.cs b/src/Tgstation.Server.Host.Console/PosixSignalChecker.cs similarity index 70% rename from src/Tgstation.Server.Host.Watchdog/SignalChecker.cs rename to src/Tgstation.Server.Host.Console/PosixSignalChecker.cs index 3be780fb39..a2ffceb69f 100644 --- a/src/Tgstation.Server.Host.Watchdog/SignalChecker.cs +++ b/src/Tgstation.Server.Host.Console/PosixSignalChecker.cs @@ -7,22 +7,33 @@ using Microsoft.Extensions.Logging; using Mono.Unix; using Mono.Unix.Native; -namespace Tgstation.Server.Host.Watchdog +using Tgstation.Server.Host.Watchdog; + +namespace Tgstation.Server.Host.Console { /// - /// Helper for checking POSIX signals. + /// for checking POSIX signals. /// - static class SignalChecker + sealed class PosixSignalChecker : ISignalChecker { /// - /// Forwards certain signals to a given . + /// The for the . /// - /// The to write to. - /// The of the process to forward signals to. - /// The for the operation. - /// A representing the running operation. - public static async Task CheckSignals(ILogger logger, int childPid, CancellationToken cancellationToken) + readonly ILogger logger; + + /// + /// Initializes a new instance of the class. + /// + /// The value of . + public PosixSignalChecker(ILogger logger) { + this.logger = logger ?? throw new ArgumentNullException(nameof(logger)); + } + + /// + public async Task CheckSignals(Func startChild, CancellationToken cancellationToken) + { + var (childPid, _) = startChild?.Invoke(null) ?? throw new ArgumentNullException(nameof(startChild)); var signalTcs = new TaskCompletionSource(); async Task CheckSignal(Signum signum) { diff --git a/src/Tgstation.Server.Host.Console/Program.cs b/src/Tgstation.Server.Host.Console/Program.cs index c1c9c4dbc7..6ba3a851e9 100644 --- a/src/Tgstation.Server.Host.Console/Program.cs +++ b/src/Tgstation.Server.Host.Console/Program.cs @@ -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 /// /// The arguments for the . /// A representing the running operation. - internal static async Task Main(string[] args) + internal static async Task 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(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()), + loggerFactory); + + return await watchdog.RunAsync(false, arguments.ToArray(), cts.Token) + ? 0 + : 1; } finally { diff --git a/src/Tgstation.Server.Host.Console/Tgstation.Server.Host.Console.csproj b/src/Tgstation.Server.Host.Console/Tgstation.Server.Host.Console.csproj index b32fe038fd..0cdf6e82c5 100644 --- a/src/Tgstation.Server.Host.Console/Tgstation.Server.Host.Console.csproj +++ b/src/Tgstation.Server.Host.Console/Tgstation.Server.Host.Console.csproj @@ -3,12 +3,14 @@ Exe - $(TgsNetVersion) + $(TgsFrameworkVersion) $(TgsCoreVersion) + false false true ../../build/analyzers.ruleset bin/$(Configuration)/$(TargetFramework)/$(AssemblyName).xml + ../../build/uac_elevation_manifest.xml @@ -47,5 +49,6 @@ + diff --git a/src/Tgstation.Server.Host.Service/GlobalSuppressions.cs b/src/Tgstation.Server.Host.Service/GlobalSuppressions.cs deleted file mode 100644 index 7771d518bc..0000000000 --- a/src/Tgstation.Server.Host.Service/GlobalSuppressions.cs +++ /dev/null @@ -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. diff --git a/src/Tgstation.Server.Host.Service/NativeMethods.cs b/src/Tgstation.Server.Host.Service/NativeMethods.cs new file mode 100644 index 0000000000..f6a9dc739e --- /dev/null +++ b/src/Tgstation.Server.Host.Service/NativeMethods.cs @@ -0,0 +1,44 @@ +using System.Runtime.InteropServices; + +namespace Tgstation.Server.Host.Service +{ + /// + /// Native methods used by the code. + /// + static class NativeMethods + { + /// + /// See https://learn.microsoft.com/en-us/windows/win32/api/winuser/nf-winuser-messagebox. + /// + public enum MessageBoxButtons : uint + { + /// + /// The message box contains two push buttons: Yes and No. + /// + YesNo = 0x00000004, + } + + /// + /// The result of a call to . + /// + public enum DialogResult : int + { + /// + /// The Yes button was selected. + /// + Yes = 6, + } + + /// + /// 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. + /// + /// 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. + /// 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. + /// The dialog box title. If this parameter is NULL, the default title is Error. + /// The . + /// The resulting . + /// See https://learn.microsoft.com/en-us/windows/win32/api/winuser/nf-winuser-messagebox. + [DllImport("user32.dll", CharSet = CharSet.Unicode)] + public static extern DialogResult MessageBox(HandleRef hWnd, string text, string caption, MessageBoxButtons type); + } +} diff --git a/src/Tgstation.Server.Host.Service/NoopSignalChecker.cs b/src/Tgstation.Server.Host.Service/NoopSignalChecker.cs new file mode 100644 index 0000000000..485d8f152e --- /dev/null +++ b/src/Tgstation.Server.Host.Service/NoopSignalChecker.cs @@ -0,0 +1,21 @@ +using System; +using System.Threading; +using System.Threading.Tasks; + +using Tgstation.Server.Host.Watchdog; + +namespace Tgstation.Server.Host.Service +{ + /// + /// No-op . + /// + sealed class NoopSignalChecker : ISignalChecker + { + /// + public Task CheckSignals(Func startChild, CancellationToken cancellationToken) + { + startChild(null); + return Task.CompletedTask; + } + } +} diff --git a/src/Tgstation.Server.Host.Service/Program.cs b/src/Tgstation.Server.Host.Service/Program.cs index e1e6e2f992..fcd4cdb19e 100644 --- a/src/Tgstation.Server.Host.Service/Program.cs +++ b/src/Tgstation.Server.Host.Service/Program.cs @@ -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 /// /// The --uninstall or -u option. /// - [Option(ShortName = "u")] + [Option(ShortName = "u", Description = "Uninstalls ANY installed tgstation-server service >=v4.0.0")] public bool Uninstall { get; } + /// + /// The --detach or -x option. Valid only with . + /// + [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; } + + /// + /// The --restart or -r option. + /// + [Option(ShortName = "r", Description = "Stop and restart the tgstation-server service")] + public bool Restart { get; } + /// /// The --install or -i option. /// - [Option(ShortName = "i")] + [Option(ShortName = "i", Description = "Installs this executable as the tgstation-server Windows service")] public bool Install { get; set; } + /// + /// The --force or -f option. + /// + [Option(ShortName = "f", Description = "Automatically agree to uninstall prompts")] + public bool Force { get; set; } + + /// + /// The --silent or -s option. + /// + [Option(ShortName = "s", Description = "Suppresses console output from the host watchdog")] + public bool Silent { get; set; } + /// /// The --configure or -c option. /// - [Option(ShortName = "c")] + [Option(ShortName = "c", Description = "Runs the TGS setup wizard")] public bool Configure { get; set; } /// - /// The --trace or -t option. Enables trace logs. + /// The --passthroughargs or -p option. /// - [Option(ShortName = "t")] - public bool Trace { get; set; } - - /// - /// The --debug or -d option. Enables debug logs. - /// - [Option(ShortName = "d")] - public bool Debug { get; set; } - - /// - /// Check if the running user is a system administrator. - /// - /// if the running user is a system administrator, otherwise. - 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; } /// /// Entrypoint for the application. @@ -80,47 +84,30 @@ namespace Tgstation.Server.Host.Service static Task Main(string[] args) => CommandLineApplication.ExecuteAsync(args); /// - /// Attempt to install the TGS Service. + /// Runs sc.exe to either uninstall a given or install the running . /// - static void RunServiceInstall() + /// The name of a service to uninstall. + /// A representing the running operation. + 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(); } /// @@ -129,67 +116,158 @@ namespace Tgstation.Server.Host.Service /// A representing the running operation. 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; + } + } + + /// + /// Attempt to install the TGS Service. + /// + /// A resulting in if the service was stopped or detached as a result, otherwise. + async ValueTask 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; + } + + /// + /// Restarts a service using a given . + /// + /// The for the service to restart. + 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(), CancellationToken.None); // DCT: None available + serviceController.Stop(); + serviceController.WaitForStatus(ServiceControllerStatus.Stopped); } } + + /// + /// Runs the host application with the setup wizard. + /// + /// The for the operation. + /// A representing the running operation. + 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); + } + + /// + /// Format into an . + /// + /// formatted as a . + string[] GetPassthroughArgs() => PassthroughArgs?.Split(' ', StringSplitOptions.RemoveEmptyEntries) ?? Array.Empty(); } } diff --git a/src/Tgstation.Server.Host.Service/ServerService.cs b/src/Tgstation.Server.Host.Service/ServerService.cs index 775946d42c..80b0fcfd79 100644 --- a/src/Tgstation.Server.Host.Service/ServerService.cs +++ b/src/Tgstation.Server.Host.Service/ServerService.cs @@ -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 /// /// Represents a as a . /// - sealed class ServerService : ServiceBase + sealed class ServerService : ServiceBase, ISignalChecker { /// /// The canonical windows service name. /// - public const string Name = "tgstation-server"; + public const string Name = Constants.CanonicalPackageName; /// /// The for the . @@ -28,17 +33,27 @@ namespace Tgstation.Server.Host.Service readonly IWatchdogFactory watchdogFactory; /// - /// The minimum for the . + /// The of command line arguments the service was invoked with. + /// + readonly string[] commandLineArguments; + + /// + /// The minimum for the . /// readonly LogLevel minimumLogLevel; /// - /// The used by the service. + /// The used by the . /// ILoggerFactory loggerFactory; /// - /// The that represents the running service. + /// The for the . + /// + ILogger logger; + + /// + /// The that represents the running . /// Task watchdogTask; @@ -47,26 +62,71 @@ namespace Tgstation.Server.Host.Service /// CancellationTokenSource cancellationTokenSource; + /// + /// The the server process is using. + /// + AnonymousPipeServerStream pipeServer; + /// /// Initializes a new instance of the class. /// /// The value of . - /// The minimum to record in the event log. - public ServerService(IWatchdogFactory watchdogFactory, LogLevel minimumLogLevel) + /// The value of . + /// The minimum to record in the event log. + 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; } + /// + public async Task CheckSignals(Func startChildAndGetPid, CancellationToken cancellationToken) + { + using (pipeServer = new AnonymousPipeServerStream(PipeDirection.Out, HandleInheritability.Inheritable)) + { + var (_, lifetimeTask) = startChildAndGetPid($"--Internal:CommandPipe={pipeServer.GetClientHandleAsString()}"); + pipeServer.DisposeLocalCopyOfClientHandle(); + await lifetimeTask; + } + } + + /// + /// Executes the . + /// + public void Run() => Run(this); + /// protected override void Dispose(bool disposing) { - loggerFactory?.Dispose(); - cancellationTokenSource?.Dispose(); + if (disposing) + { + loggerFactory?.Dispose(); + cancellationTokenSource?.Dispose(); + pipeServer?.Dispose(); + } + base.Dispose(disposing); } + /// + 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); + } + /// 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(); } - 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(commandLineArguments.Length + args.Length + 1) + { + "--General:SetupWizardMode=Never", + }; + + newArgs.AddRange(commandLineArguments); + newArgs.AddRange(args); + + watchdogTask = RunWatchdog(watchdog, newArgs.ToArray(), cancellationTokenSource.Token); } /// @@ -105,24 +175,60 @@ namespace Tgstation.Server.Host.Service /// A representing the running operation. 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(); } + + /// + /// Sends a command to the main server process. + /// + /// One of the . + 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); + } + } } } diff --git a/src/Tgstation.Server.Host.Service/Tgstation.Server.Host.Service.csproj b/src/Tgstation.Server.Host.Service/Tgstation.Server.Host.Service.csproj index 83630ddc2e..0d5b8b429e 100644 --- a/src/Tgstation.Server.Host.Service/Tgstation.Server.Host.Service.csproj +++ b/src/Tgstation.Server.Host.Service/Tgstation.Server.Host.Service.csproj @@ -2,13 +2,16 @@ - Exe - net472 - win + WinExe + $(TgsFrameworkVersion)-windows $(TgsCoreVersion) true + + false ../../build/analyzers.ruleset bin/$(Configuration)/$(TargetFramework)/$(RuntimeIdentifier)/$(AssemblyName).xml + ../../build/tgs.ico + ../../build/uac_elevation_manifest.xml @@ -23,6 +26,7 @@ + @@ -34,6 +38,7 @@ + @@ -42,11 +47,7 @@ + - - - - - diff --git a/src/Tgstation.Server.Host.Watchdog/ISignalChecker.cs b/src/Tgstation.Server.Host.Watchdog/ISignalChecker.cs new file mode 100644 index 0000000000..8ab3b06df6 --- /dev/null +++ b/src/Tgstation.Server.Host.Watchdog/ISignalChecker.cs @@ -0,0 +1,20 @@ +using System; +using System.Threading; +using System.Threading.Tasks; + +namespace Tgstation.Server.Host.Watchdog +{ + /// + /// For relaying signals received to the host process. + /// + public interface ISignalChecker + { + /// + /// Relays signals received to the host process. + /// + /// An to start the main process. It accepts an optional additional command line argument as a paramter and returns it's and lifetime . + /// The for the operation. + /// A representing the running operation. + Task CheckSignals(Func startChild, CancellationToken cancellationToken); + } +} diff --git a/src/Tgstation.Server.Host.Watchdog/IWatchdog.cs b/src/Tgstation.Server.Host.Watchdog/IWatchdog.cs index cb587dfddc..e99f09e31b 100644 --- a/src/Tgstation.Server.Host.Watchdog/IWatchdog.cs +++ b/src/Tgstation.Server.Host.Watchdog/IWatchdog.cs @@ -14,7 +14,7 @@ namespace Tgstation.Server.Host.Watchdog /// If the should just run the host configuration wizard and exit. /// The arguments for the . /// The for the operation. - /// A representing the running operation. - Task RunAsync(bool runConfigure, string[] args, CancellationToken cancellationToken); + /// A resulting in if there were no errors, otherwise. + Task RunAsync(bool runConfigure, string[] args, CancellationToken cancellationToken); } } diff --git a/src/Tgstation.Server.Host.Watchdog/IWatchdogFactory.cs b/src/Tgstation.Server.Host.Watchdog/IWatchdogFactory.cs index c4dfe2bd00..3569139fdd 100644 --- a/src/Tgstation.Server.Host.Watchdog/IWatchdogFactory.cs +++ b/src/Tgstation.Server.Host.Watchdog/IWatchdogFactory.cs @@ -10,8 +10,9 @@ namespace Tgstation.Server.Host.Watchdog /// /// Create a . /// + /// The to use for relaying signals. /// The to use for error reporting. /// A new . - IWatchdog CreateWatchdog(ILoggerFactory loggerFactory); + IWatchdog CreateWatchdog(ISignalChecker signalChecker, ILoggerFactory loggerFactory); } } diff --git a/src/Tgstation.Server.Host.Watchdog/Tgstation.Server.Host.Watchdog.csproj b/src/Tgstation.Server.Host.Watchdog/Tgstation.Server.Host.Watchdog.csproj index 0277b02fdd..2ee844505b 100644 --- a/src/Tgstation.Server.Host.Watchdog/Tgstation.Server.Host.Watchdog.csproj +++ b/src/Tgstation.Server.Host.Watchdog/Tgstation.Server.Host.Watchdog.csproj @@ -2,7 +2,7 @@ - netstandard2.0 + $(TgsFrameworkVersion) Full false $(TgsHostWatchdogVersion) @@ -44,6 +44,7 @@ + diff --git a/src/Tgstation.Server.Host.Watchdog/Watchdog.cs b/src/Tgstation.Server.Host.Watchdog/Watchdog.cs index 1afd2cad85..fc86f1f271 100644 --- a/src/Tgstation.Server.Host.Watchdog/Watchdog.cs +++ b/src/Tgstation.Server.Host.Watchdog/Watchdog.cs @@ -10,7 +10,7 @@ using System.Threading; using System.Threading.Tasks; using Microsoft.Extensions.Logging; - +using Tgstation.Server.Common.Extensions; using Tgstation.Server.Host.Common; namespace Tgstation.Server.Host.Watchdog @@ -19,6 +19,11 @@ namespace Tgstation.Server.Host.Watchdog /// This 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. sealed class Watchdog : IWatchdog { + /// + /// The for the . + /// + readonly ISignalChecker signalChecker; + /// /// The for the . /// @@ -27,16 +32,18 @@ namespace Tgstation.Server.Host.Watchdog /// /// Initializes a new instance of the class. /// + /// The value of . /// The value of . - public Watchdog(ILogger logger) + public Watchdog(ISignalChecker signalChecker, ILogger logger) { + this.signalChecker = signalChecker ?? throw new ArgumentNullException(nameof(signalChecker)); this.logger = logger ?? throw new ArgumentNullException(nameof(logger)); } /// #pragma warning disable CA1502 // TODO: Decomplexify #pragma warning disable CA1506 - public async Task RunAsync(bool runConfigure, string[] args, CancellationToken cancellationToken) + public async Task RunAsync(bool runConfigure, string[] args, CancellationToken cancellationToken) { logger.LogInformation("Host watchdog starting..."); int currentProcessId; @@ -52,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); @@ -74,10 +81,10 @@ namespace Tgstation.Server.Host.Watchdog 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); @@ -90,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")) @@ -143,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))) @@ -176,12 +190,9 @@ namespace Tgstation.Server.Host.Watchdog } })) { - var processTask = tcs.Task; using var cts = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken); - var checkerTask = isWindows - ? Task.CompletedTask - : SignalChecker.CheckSignals(logger, childPid, cts.Token); + var checkerTask = signalChecker.CheckSignals(StartProcess, cts.Token); try { await processTask; @@ -228,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 @@ -331,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 @@ -355,10 +369,22 @@ namespace Tgstation.Server.Host.Watchdog if (isWindows) { exeName += ".exe"; - enumerator = paths; + enumerator = new List(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(2) + { + "/usr/bin", + "/usr/share/bin", + "/usr/local/share/dotnet", + }); enumerator = enumerator.Select(x => Path.Combine(x, exeName)); diff --git a/src/Tgstation.Server.Host.Watchdog/WatchdogFactory.cs b/src/Tgstation.Server.Host.Watchdog/WatchdogFactory.cs index 083d89eafb..4b2fc89147 100644 --- a/src/Tgstation.Server.Host.Watchdog/WatchdogFactory.cs +++ b/src/Tgstation.Server.Host.Watchdog/WatchdogFactory.cs @@ -8,6 +8,10 @@ namespace Tgstation.Server.Host.Watchdog public sealed class WatchdogFactory : IWatchdogFactory { /// - public IWatchdog CreateWatchdog(ILoggerFactory loggerFactory) => new Watchdog(loggerFactory?.CreateLogger() ?? throw new ArgumentNullException(nameof(loggerFactory))); + public IWatchdog CreateWatchdog( + ISignalChecker signalChecker, + ILoggerFactory loggerFactory) => new Watchdog( + signalChecker ?? throw new ArgumentNullException(nameof(signalChecker)), + loggerFactory?.CreateLogger() ?? throw new ArgumentNullException(nameof(loggerFactory))); } } diff --git a/src/Tgstation.Server.Host/Components/Byond/ByondManager.cs b/src/Tgstation.Server.Host/Components/Byond/ByondManager.cs index da057c1ead..6a70782af1 100644 --- a/src/Tgstation.Server.Host/Components/Byond/ByondManager.cs +++ b/src/Tgstation.Server.Host/Components/Byond/ByondManager.cs @@ -235,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); @@ -302,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); @@ -453,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; } @@ -616,8 +618,15 @@ namespace Tgstation.Server.Host.Components.Byond /// A representing the running operation. 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); @@ -633,9 +642,7 @@ namespace Tgstation.Server.Host.Components.Byond trustedFileText = $"{trustedFileText.Trim()}{Environment.NewLine}"; } else - { trustedFileText = String.Empty; - } if (trustedFileText.Contains(fullDmbPath, StringComparison.Ordinal)) return; diff --git a/src/Tgstation.Server.Host/Components/Byond/WindowsByondInstaller.cs b/src/Tgstation.Server.Host/Components/Byond/WindowsByondInstaller.cs index 3bd1e64db1..91f9412a34 100644 --- a/src/Tgstation.Server.Host/Components/Byond/WindowsByondInstaller.cs +++ b/src/Tgstation.Server.Host/Components/Byond/WindowsByondInstaller.cs @@ -94,7 +94,11 @@ namespace Tgstation.Server.Host.Components.Byond this.processExecutor = processExecutor ?? throw new ArgumentNullException(nameof(processExecutor)); generalConfiguration = generalConfigurationOptions?.Value ?? throw new ArgumentNullException(nameof(generalConfigurationOptions)); - 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; diff --git a/src/Tgstation.Server.Host/Components/Chat/ChatManager.cs b/src/Tgstation.Server.Host/Components/Chat/ChatManager.cs index 91cfdcade9..b8038dfe5b 100644 --- a/src/Tgstation.Server.Host/Components/Chat/ChatManager.cs +++ b/src/Tgstation.Server.Host/Components/Chat/ChatManager.cs @@ -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 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(), diff --git a/src/Tgstation.Server.Host/Components/Chat/Providers/IrcProvider.cs b/src/Tgstation.Server.Host/Components/Chat/Providers/IrcProvider.cs index 5e496dba40..30b1e86d88 100644 --- a/src/Tgstation.Server.Host/Components/Chat/Providers/IrcProvider.cs +++ b/src/Tgstation.Server.Host/Components/Chat/Providers/IrcProvider.cs @@ -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); /// /// Run SASL authentication on . diff --git a/src/Tgstation.Server.Host/Components/Chat/Providers/Provider.cs b/src/Tgstation.Server.Host/Components/Chat/Providers/Provider.cs index 1fad402354..6cc52173d9 100644 --- a/src/Tgstation.Server.Host/Components/Chat/Providers/Provider.cs +++ b/src/Tgstation.Server.Host/Components/Chat/Providers/Provider.cs @@ -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) { diff --git a/src/Tgstation.Server.Host/Components/Deployment/DreamMaker.cs b/src/Tgstation.Server.Host/Components/Deployment/DreamMaker.cs index 38ee725a01..611b03ec13 100644 --- a/src/Tgstation.Server.Host/Components/Deployment/DreamMaker.cs +++ b/src/Tgstation.Server.Host/Components/Deployment/DreamMaker.cs @@ -806,10 +806,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(); diff --git a/src/Tgstation.Server.Host/Components/Deployment/Remote/GitLabRemoteDeploymentManager.cs b/src/Tgstation.Server.Host/Components/Deployment/Remote/GitLabRemoteDeploymentManager.cs index c64b35af67..d9b8c99a08 100644 --- a/src/Tgstation.Server.Host/Components/Deployment/Remote/GitLabRemoteDeploymentManager.cs +++ b/src/Tgstation.Server.Host/Components/Deployment/Remote/GitLabRemoteDeploymentManager.cs @@ -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); @@ -140,7 +139,7 @@ namespace Tgstation.Server.Host.Components.Deployment.Remote $"{remoteRepositoryOwner}/{remoteRepositoryName}", testMergeNumber, new CreateMergeRequestNoteRequest(comment)) - .WithToken(cancellationToken); + .WaitAsync(cancellationToken); } catch (Exception ex) when (ex is not OperationCanceledException) { diff --git a/src/Tgstation.Server.Host/Components/InstanceManager.cs b/src/Tgstation.Server.Host/Components/InstanceManager.cs index b82bec8f03..706950db1b 100644 --- a/src/Tgstation.Server.Host/Components/InstanceManager.cs +++ b/src/Tgstation.Server.Host/Components/InstanceManager.cs @@ -87,6 +87,11 @@ namespace Tgstation.Server.Host.Components /// readonly ISwarmServiceController swarmServiceController; + /// + /// The for the . + /// + readonly IConsole console; + /// /// The for the . /// @@ -132,6 +137,11 @@ namespace Tgstation.Server.Host.Components /// readonly CancellationTokenSource shutdownCancellationTokenSource; + /// + /// The original of . + /// + readonly string originalConsoleTitle; + /// /// The returned by . /// @@ -155,6 +165,7 @@ namespace Tgstation.Server.Host.Components /// The value of . /// The value of . /// The value of . + /// The value of . /// The containing the value of . /// The containing the value of . /// The value of . @@ -169,6 +180,7 @@ namespace Tgstation.Server.Host.Components IAsyncDelayer asyncDelayer, IServerPortProvider serverPortProvider, ISwarmServiceController swarmServiceController, + IConsole console, IOptions generalConfigurationOptions, IOptions swarmConfigurationOptions, ILogger 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>(); bridgeHandlers = new Dictionary(); 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(); @@ -426,42 +441,50 @@ namespace Tgstation.Server.Host.Components /// 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!"); + } } /// @@ -535,6 +558,7 @@ namespace Tgstation.Server.Host.Components try { logger.LogInformation("{versionString}", assemblyInformationProvider.VersionString); + console.Title = assemblyInformationProvider.VersionString; CheckSystemCompatibility(); diff --git a/src/Tgstation.Server.Host/Components/Repository/GitLabRemoteFeatures.cs b/src/Tgstation.Server.Host/Components/Repository/GitLabRemoteFeatures.cs index fbb43a62ef..166383353f 100644 --- a/src/Tgstation.Server.Host/Components/Repository/GitLabRemoteFeatures.cs +++ b/src/Tgstation.Server.Host/Components/Repository/GitLabRemoteFeatures.cs @@ -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) diff --git a/src/Tgstation.Server.Host/Components/Session/SessionController.cs b/src/Tgstation.Server.Host/Components/Session/SessionController.cs index f3b8d481d1..70a76e6760 100644 --- a/src/Tgstation.Server.Host/Components/Session/SessionController.cs +++ b/src/Tgstation.Server.Host/Components/Session/SessionController.cs @@ -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(); 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 { diff --git a/src/Tgstation.Server.Host/Components/Session/SessionControllerFactory.cs b/src/Tgstation.Server.Host/Components/Session/SessionControllerFactory.cs index 71756c841f..55439579db 100644 --- a/src/Tgstation.Server.Host/Components/Session/SessionControllerFactory.cs +++ b/src/Tgstation.Server.Host/Components/Session/SessionControllerFactory.cs @@ -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; diff --git a/src/Tgstation.Server.Host/Components/Watchdog/WatchdogBase.cs b/src/Tgstation.Server.Host/Components/Watchdog/WatchdogBase.cs index c13ea5bfb7..a1f1091807 100644 --- a/src/Tgstation.Server.Host/Components/Watchdog/WatchdogBase.cs +++ b/src/Tgstation.Server.Host/Components/Watchdog/WatchdogBase.cs @@ -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."); @@ -589,7 +589,7 @@ namespace Tgstation.Server.Host.Components.Watchdog /// A representing the running operation. 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... @@ -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"); diff --git a/src/Tgstation.Server.Host/Configuration/DatabaseConfiguration.cs b/src/Tgstation.Server.Host/Configuration/DatabaseConfiguration.cs index e076bc03cb..d5adce7577 100644 --- a/src/Tgstation.Server.Host/Configuration/DatabaseConfiguration.cs +++ b/src/Tgstation.Server.Host/Configuration/DatabaseConfiguration.cs @@ -34,11 +34,6 @@ namespace Tgstation.Server.Host.Configuration /// public bool DropDatabase { get; set; } - /// - /// Used to indicate that the database is being loaded to generate migrations. Should not be used in production!. - /// - public bool DesignTime { get; set; } - /// /// The form of the of the target server. /// diff --git a/src/Tgstation.Server.Host/Configuration/ElasticsearchConfiguration.cs b/src/Tgstation.Server.Host/Configuration/ElasticsearchConfiguration.cs index f3e9e92234..07a3afd8e0 100644 --- a/src/Tgstation.Server.Host/Configuration/ElasticsearchConfiguration.cs +++ b/src/Tgstation.Server.Host/Configuration/ElasticsearchConfiguration.cs @@ -10,21 +10,6 @@ /// public const string Section = "Elasticsearch"; - /// - /// Default value of . - /// - const string DefaultHost = "http://127.0.0.1:9200"; // localhost - - /// - /// Default value of . - /// - const string DefaultUsername = "my_username"; - - /// - /// Default value of . - /// - const string DefaultPassword = "my_password"; - /// /// Do we want to enable elasticsearch or not?. /// @@ -33,16 +18,16 @@ /// /// The host of the elasticsearch endpoint. /// - public string Host { get; set; } = DefaultHost; + public string Host { get; set; } /// /// Username for elasticsearch. /// - public string Username { get; set; } = DefaultUsername; + public string Username { get; set; } /// /// Password for elasticsearch. /// - public string Password { get; set; } = DefaultPassword; + public string Password { get; set; } } } diff --git a/src/Tgstation.Server.Host/Configuration/FileLoggingConfiguration.cs b/src/Tgstation.Server.Host/Configuration/FileLoggingConfiguration.cs index a4cefdfde1..48b08b5b48 100644 --- a/src/Tgstation.Server.Host/Configuration/FileLoggingConfiguration.cs +++ b/src/Tgstation.Server.Host/Configuration/FileLoggingConfiguration.cs @@ -75,7 +75,8 @@ namespace Tgstation.Server.Host.Configuration ? Directory : ioManager.ConcatPath( directoryToUse, - assemblyInformationProvider.VersionPrefix); + assemblyInformationProvider.VersionPrefix, + "logs"); } } } diff --git a/src/Tgstation.Server.Host/Configuration/InternalConfiguration.cs b/src/Tgstation.Server.Host/Configuration/InternalConfiguration.cs new file mode 100644 index 0000000000..0d54f24d73 --- /dev/null +++ b/src/Tgstation.Server.Host/Configuration/InternalConfiguration.cs @@ -0,0 +1,28 @@ +namespace Tgstation.Server.Host.Configuration +{ + /// + /// Unstable configuration options used internally by TGS. + /// + public sealed class InternalConfiguration + { + /// + /// The key for the the resides in. + /// + public const string Section = "Internal"; + + /// + /// The name of the pipe opened by the host watchdog, if any. + /// + public string CommandPipe { get; set; } + + /// + /// If the server is running under SystemD. + /// + public bool UsingSystemD { get; set; } + + /// + /// The base path for the app settings configuration files. + /// + public string AppSettingsBasePath { get; set; } + } +} diff --git a/src/Tgstation.Server.Host/Controllers/AdministrationController.cs b/src/Tgstation.Server.Host/Controllers/AdministrationController.cs index 104f1736b9..636aa182a2 100644 --- a/src/Tgstation.Server.Host/Controllers/AdministrationController.cs +++ b/src/Tgstation.Server.Host/Controllers/AdministrationController.cs @@ -41,9 +41,9 @@ namespace Tgstation.Server.Host.Controllers const string OctokitException = "Bad GitHub API response, check configuration!"; /// - /// The for the . + /// The for the . /// - readonly IGitHubService gitHubService; + readonly IGitHubServiceFactory gitHubServiceFactory; /// /// The for the . @@ -85,7 +85,7 @@ namespace Tgstation.Server.Host.Controllers /// /// The for the . /// The for the . - /// The value of . + /// The value of . /// The value of . /// The value of . /// The value of . @@ -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); diff --git a/src/Tgstation.Server.Host/Controllers/ApiController.cs b/src/Tgstation.Server.Host/Controllers/ApiController.cs index f9e3868ada..b3d26b709f 100644 --- a/src/Tgstation.Server.Host/Controllers/ApiController.cs +++ b/src/Tgstation.Server.Host/Controllers/ApiController.cs @@ -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; diff --git a/src/Tgstation.Server.Host/Core/Application.cs b/src/Tgstation.Server.Host/Core/Application.cs index ef312928a0..4a8bb4a152 100644 --- a/src/Tgstation.Server.Host/Core/Application.cs +++ b/src/Tgstation.Server.Host/Core/Application.cs @@ -6,6 +6,8 @@ using System.Linq; using Cyberboss.AspNetCore.AsyncInitializer; +using Elastic.CommonSchema.Serilog; + using Microsoft.AspNetCore.Authentication.JwtBearer; using Microsoft.AspNetCore.Builder; using Microsoft.AspNetCore.Cors.Infrastructure; @@ -23,6 +25,7 @@ using Newtonsoft.Json; using Serilog; using Serilog.Events; using Serilog.Formatting.Display; +using Serilog.Sinks.Elasticsearch; using Tgstation.Server.Api; using Tgstation.Server.Common.Http; @@ -154,6 +157,7 @@ namespace Tgstation.Server.Host.Core }; var microsoftEventLevel = ConvertSeriLogLevel(postSetupServices.FileLoggingConfiguration.MicrosoftLogLevel); + var elasticsearchConfiguration = postSetupServices.ElasticsearchConfiguration; services.SetupLogging( config => { @@ -191,7 +195,30 @@ namespace Tgstation.Server.Host.Core rollingInterval: RollingInterval.Day, rollOnFileSizeLimit: true); }, - postSetupServices.ElasticsearchConfiguration); + elasticsearchConfiguration.Enable + ? new ElasticsearchSinkOptions( + new Uri( + String.IsNullOrWhiteSpace(elasticsearchConfiguration.Host) + ? throw new InvalidOperationException($"Missing {ElasticsearchConfiguration.Section}:{nameof(elasticsearchConfiguration.Host)}!") + : elasticsearchConfiguration.Host)) + { + // Yes I know this means they cannot use a self signed cert unless they also have authentication, but lets be real here + // No one is going to be doing one of those but not the other + ModifyConnectionSettings = connectionConfigration => (!String.IsNullOrWhiteSpace(elasticsearchConfiguration.Username) && !String.IsNullOrWhiteSpace(elasticsearchConfiguration.Password)) + ? connectionConfigration + .BasicAuthentication( + elasticsearchConfiguration.Username, + elasticsearchConfiguration.Password) + .ServerCertificateValidationCallback((o, certificate, chain, errors) => true) + : null, + CustomFormatter = new EcsTextFormatter(), + AutoRegisterTemplate = true, + AutoRegisterTemplateVersion = AutoRegisterTemplateVersion.ESv7, + IndexFormat = "tgs-logs", + } + : null, + postSetupServices.InternalConfiguration, + postSetupServices.FileLoggingConfiguration); // configure bearer token validation services @@ -335,12 +362,12 @@ namespace Tgstation.Server.Host.Core services.AddSingleton(x => new Lazy(() => x.GetRequiredService(), true)); services.AddSingleton(); - services.AddSingleton(); - - services.AddSingleton(); - services.AddSingleton(x => x.GetRequiredService()); + services.AddHostedService(); } + if (postSetupServices.InternalConfiguration.UsingSystemD) + services.AddHostedService(); + // configure file transfer services services.AddSingleton(); services.AddSingleton(x => x.GetRequiredService()); @@ -368,10 +395,11 @@ namespace Tgstation.Server.Host.Core // configure misc services services.AddSingleton(); services.AddSingleton(); - services.AddFileDownloader(); services.AddSingleton(); services.AddSingleton(); + services.AddHostedService(); + services.AddFileDownloader(); services.AddGitHub(); // configure root services @@ -438,7 +466,7 @@ namespace Tgstation.Server.Host.Core // 503 requests made while the application is starting applicationBuilder.UseAsyncInitialization( - (instanceManager, cancellationToken) => instanceManager.Ready.WithToken(cancellationToken)); + (instanceManager, cancellationToken) => instanceManager.Ready.WaitAsync(cancellationToken)); if (generalConfiguration.HostApiDocumentation) { diff --git a/src/Tgstation.Server.Host/Core/CommandPipeReader.cs b/src/Tgstation.Server.Host/Core/CommandPipeReader.cs new file mode 100644 index 0000000000..61bad2b474 --- /dev/null +++ b/src/Tgstation.Server.Host/Core/CommandPipeReader.cs @@ -0,0 +1,109 @@ +using System; +using System.IO; +using System.IO.Pipes; +using System.Threading; +using System.Threading.Tasks; + +using Microsoft.Extensions.Hosting; +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Options; + +using Tgstation.Server.Host.Common; +using Tgstation.Server.Host.Configuration; +using Tgstation.Server.Host.Extensions; + +namespace Tgstation.Server.Host.Core +{ + /// + /// Reads from the command pipe opened by the host watchdog. + /// + sealed class CommandPipeReader : BackgroundService + { + /// + /// The for the . + /// + readonly IServerControl serverControl; + + /// + /// The for the . + /// + readonly ILogger logger; + + /// + /// The for the . + /// + readonly InternalConfiguration internalConfiguration; + + /// + /// Initializes a new instance of the class. + /// + /// The value of . + /// The containing the value of . + /// The value of . + public CommandPipeReader( + IServerControl serverControl, + IOptions internalConfigurationOptions, + ILogger logger) + { + this.serverControl = serverControl ?? throw new ArgumentNullException(nameof(serverControl)); + internalConfiguration = internalConfigurationOptions?.Value ?? throw new ArgumentNullException(nameof(internalConfigurationOptions)); + this.logger = logger ?? throw new ArgumentNullException(nameof(logger)); + } + + /// + protected override async Task ExecuteAsync(CancellationToken cancellationToken) + { + logger.LogTrace("Starting..."); + + var pipeName = internalConfiguration.CommandPipe; + if (string.IsNullOrWhiteSpace(pipeName)) + { + logger.LogDebug("No command pipe name specified in configuration"); + return; + } + + try + { + await using var pipeClient = new AnonymousPipeClientStream(PipeDirection.In, pipeName); + using var streamReader = new StreamReader(pipeClient, leaveOpen: true); + while (!cancellationToken.IsCancellationRequested) + { + logger.LogTrace("Waiting to read command line..."); + var line = await streamReader.ReadLineAsync().WaitAsync(cancellationToken); + + logger?.LogInformation("Received pipe command: {command}", line); + switch (line) + { + case PipeCommands.CommandStop: + await serverControl.Die(null); + break; + case PipeCommands.CommandGracefulShutdown: + await serverControl.GracefulShutdown(false); + break; + case PipeCommands.CommandDetachingShutdown: + await serverControl.GracefulShutdown(true); + break; + case null: + logger.LogError("Read null from pipe!"); + return; + default: + logger?.LogWarning("Unrecognized pipe command: {command}", line); + break; + } + } + } + catch (OperationCanceledException ex) + { + logger?.LogTrace(ex, "Command read task cancelled!"); + } + catch (Exception ex) + { + logger?.LogError(ex, "Command read task errored!"); + } + finally + { + logger?.LogTrace("Command read task exiting..."); + } + } + } +} diff --git a/src/Tgstation.Server.Host/Core/ServerUpdater.cs b/src/Tgstation.Server.Host/Core/ServerUpdater.cs index d04386a9b6..570b613b70 100644 --- a/src/Tgstation.Server.Host/Core/ServerUpdater.cs +++ b/src/Tgstation.Server.Host/Core/ServerUpdater.cs @@ -17,9 +17,9 @@ namespace Tgstation.Server.Host.Core sealed class ServerUpdater : IServerUpdater, IServerUpdateExecutor { /// - /// The for the . + /// The for the . /// - readonly IGitHubService gitHubService; + readonly IGitHubServiceFactory gitHubServiceFactory; /// /// The for the . @@ -64,7 +64,7 @@ namespace Tgstation.Server.Host.Core /// /// Initializes a new instance of the class. /// - /// The value of . + /// The value of . /// The value of . /// The value of . /// The value of . @@ -72,7 +72,7 @@ namespace Tgstation.Server.Host.Core /// The containing the value of . /// The containing the value of . public ServerUpdater( - IGitHubService gitHubService, + IGitHubServiceFactory gitHubServiceFactory, IIOManager ioManager, IFileDownloader fileDownloader, IServerControl serverControl, @@ -80,7 +80,7 @@ namespace Tgstation.Server.Host.Core IOptions generalConfigurationOptions, IOptions updatesConfigurationOptions) { - this.gitHubService = gitHubService ?? throw new ArgumentNullException(nameof(gitHubService)); + this.gitHubServiceFactory = gitHubServiceFactory ?? throw new ArgumentNullException(nameof(gitHubServiceFactory)); this.ioManager = ioManager ?? throw new ArgumentNullException(nameof(ioManager)); this.fileDownloader = fileDownloader ?? throw new ArgumentNullException(nameof(fileDownloader)); this.serverControl = serverControl ?? throw new ArgumentNullException(nameof(serverControl)); @@ -288,6 +288,7 @@ namespace Tgstation.Server.Host.Core { logger.LogDebug("Looking for GitHub releases version {version}...", newVersion); + var gitHubService = gitHubServiceFactory.CreateService(); var releases = await gitHubService.GetTgsReleases(cancellationToken); foreach (var kvp in releases) { diff --git a/src/Tgstation.Server.Host/Database/Design/DesignTimeDbContextFactoryHelpers.cs b/src/Tgstation.Server.Host/Database/Design/DesignTimeDbContextFactoryHelpers.cs index a0569aafe3..54d79bddc3 100644 --- a/src/Tgstation.Server.Host/Database/Design/DesignTimeDbContextFactoryHelpers.cs +++ b/src/Tgstation.Server.Host/Database/Design/DesignTimeDbContextFactoryHelpers.cs @@ -26,7 +26,6 @@ namespace Tgstation.Server.Host.Database.Design { var dbConfig = new DatabaseConfiguration { - DesignTime = true, DatabaseType = databaseType, ConnectionString = connectionString, ServerVersion = serverVersion, diff --git a/src/Tgstation.Server.Host/Database/Design/SqliteDesignTimeDbContextFactory.cs b/src/Tgstation.Server.Host/Database/Design/SqliteDesignTimeDbContextFactory.cs index b855f8ad9c..924d8934f9 100644 --- a/src/Tgstation.Server.Host/Database/Design/SqliteDesignTimeDbContextFactory.cs +++ b/src/Tgstation.Server.Host/Database/Design/SqliteDesignTimeDbContextFactory.cs @@ -12,11 +12,11 @@ namespace Tgstation.Server.Host.Database.Design /// public SqliteDatabaseContext CreateDbContext(string[] args) { - SqliteDatabaseContext.DesignTime = true; return new SqliteDatabaseContext( DesignTimeDbContextFactoryHelpers.CreateDatabaseContextOptions( DatabaseType.Sqlite, - "Data Source=tgs_design.sqlite3;Mode=ReadWriteCreate")); + "Data Source=tgs_design.sqlite3;Mode=ReadWriteCreate"), + true); } } } diff --git a/src/Tgstation.Server.Host/Database/SqliteDatabaseContext.cs b/src/Tgstation.Server.Host/Database/SqliteDatabaseContext.cs index 0076efe2c2..98a58190d9 100644 --- a/src/Tgstation.Server.Host/Database/SqliteDatabaseContext.cs +++ b/src/Tgstation.Server.Host/Database/SqliteDatabaseContext.cs @@ -14,24 +14,41 @@ namespace Tgstation.Server.Host.Database sealed class SqliteDatabaseContext : DatabaseContext { /// - /// Static property to receive the configured value of . + /// If the database context is running in design time mode. /// - public static bool DesignTime { get; set; } + readonly bool designTime; /// /// Initializes a new instance of the class. /// /// The for the . - public SqliteDatabaseContext(DbContextOptions dbContextOptions) : base(dbContextOptions) + public SqliteDatabaseContext( + DbContextOptions dbContextOptions) + : this(dbContextOptions, false) { } + /// + /// Initializes a new instance of the class. + /// + /// The for the . + /// The value of . + internal SqliteDatabaseContext( + DbContextOptions dbContextOptions, + bool designTime) + : base(dbContextOptions) + { + this.designTime = designTime; + } + /// /// Configure the . /// /// The to configure. /// The . - public static void ConfigureWith(DbContextOptionsBuilder options, DatabaseConfiguration databaseConfiguration) + public static void ConfigureWith( + DbContextOptionsBuilder options, + DatabaseConfiguration databaseConfiguration) { ArgumentNullException.ThrowIfNull(options); ArgumentNullException.ThrowIfNull(databaseConfiguration); @@ -39,7 +56,6 @@ namespace Tgstation.Server.Host.Database if (databaseConfiguration.DatabaseType != DatabaseType.Sqlite) throw new InvalidOperationException($"Invalid DatabaseType for {nameof(SqliteDatabaseContext)}!"); - DesignTime = databaseConfiguration.DesignTime; options.UseSqlite(databaseConfiguration.ConnectionString, sqliteOptions => sqliteOptions.UseQuerySplittingBehavior(QuerySplittingBehavior.SingleQuery)); } @@ -56,7 +72,7 @@ namespace Tgstation.Server.Host.Database // use the DateTimeOffsetToBinaryConverter // Based on: https://github.com/aspnet/EntityFrameworkCore/issues/10784#issuecomment-415769754 // This only supports millisecond precision, but should be sufficient for most use cases. - if (!DesignTime) + if (!designTime) foreach (var entityType in modelBuilder.Model.GetEntityTypes()) { var properties = entityType diff --git a/src/Tgstation.Server.Host/Extensions/Converters/VersionConverter.cs b/src/Tgstation.Server.Host/Extensions/Converters/VersionConverter.cs index 83eee22114..ec08e5ae7b 100644 --- a/src/Tgstation.Server.Host/Extensions/Converters/VersionConverter.cs +++ b/src/Tgstation.Server.Host/Extensions/Converters/VersionConverter.cs @@ -2,7 +2,7 @@ using Newtonsoft.Json; -using Tgstation.Server.Api; +using Tgstation.Server.Common.Extensions; using YamlDotNet.Core; using YamlDotNet.Core.Events; diff --git a/src/Tgstation.Server.Host/Extensions/ServiceCollectionExtensions.cs b/src/Tgstation.Server.Host/Extensions/ServiceCollectionExtensions.cs index d3f70f6a28..5acfd9abb3 100644 --- a/src/Tgstation.Server.Host/Extensions/ServiceCollectionExtensions.cs +++ b/src/Tgstation.Server.Host/Extensions/ServiceCollectionExtensions.cs @@ -2,7 +2,6 @@ using System.Diagnostics; using System.Globalization; -using Elastic.CommonSchema.Serilog; using Microsoft.Extensions.Configuration; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.DependencyInjection.Extensions; @@ -105,13 +104,12 @@ namespace Tgstation.Server.Host.Extensions serviceCollection.AddSingleton(); serviceCollection.AddSingleton(typeof(IGitHubServiceFactory), gitHubServiceFactoryType); - serviceCollection.AddSingleton(x => x.GetRequiredService().CreateService()); return serviceCollection; } /// - /// Add an additional to s that call . + /// Add an additional to s that call . /// /// The of to add. public static void UseAdditionalLoggerProvider() where TLoggerProvider : class, ILoggerProvider @@ -165,14 +163,22 @@ namespace Tgstation.Server.Host.Extensions /// The to configure. /// Additional configuration for a given . /// Additional configuration for a given . - /// Configuration for a given . + /// The to use, if any. + /// The active , if any. + /// The active , if any. Must be set if is passed in. /// The updated . public static IServiceCollection SetupLogging( this IServiceCollection serviceCollection, Action configurationAction, Action sinkConfigurationAction = null, - ElasticsearchConfiguration elasticsearchConfiguration = null) - => serviceCollection.AddLogging(builder => + ElasticsearchSinkOptions elasticsearchSinkOptions = null, + InternalConfiguration internalConfiguration = null, + FileLoggingConfiguration fileLoggingConfiguration = null) + { + if (internalConfiguration != null) + ArgumentNullException.ThrowIfNull(fileLoggingConfiguration); + + return serviceCollection.AddLogging(builder => { builder.ClearProviders(); @@ -190,29 +196,14 @@ namespace Tgstation.Server.Host.Extensions var template = "[{Timestamp:HH:mm:ss}] {Level:w3}: {SourceContext:l} (" + SerilogContextHelper.Template + "){NewLine} {Message:lj}{NewLine}{Exception}"; - sinkConfiguration.Console(outputTemplate: template, formatProvider: CultureInfo.InvariantCulture); + + if (!((internalConfiguration?.UsingSystemD ?? false) && !fileLoggingConfiguration.Disable)) + sinkConfiguration.Console(outputTemplate: template, formatProvider: CultureInfo.InvariantCulture); sinkConfigurationAction?.Invoke(sinkConfiguration); }); - if (elasticsearchConfiguration != null) - { - if (elasticsearchConfiguration.Enable) - { - if (elasticsearchConfiguration.Host == null) - throw new InvalidOperationException("Elasticsearch endpoint is null!"); - - configuration.WriteTo.Elasticsearch(new ElasticsearchSinkOptions(new Uri(elasticsearchConfiguration.Host)) - { - // Yes I know this means they cannot use a self signed cert unless they also have authentication, but lets be real here - // No one is going to be doing one of thsoe but not the other - ModifyConnectionSettings = x => (!string.IsNullOrEmpty(elasticsearchConfiguration.Username) && !string.IsNullOrEmpty(elasticsearchConfiguration.Password)) ? x.BasicAuthentication(elasticsearchConfiguration.Username, elasticsearchConfiguration.Password).ServerCertificateValidationCallback((o, certificate, arg3, arg4) => { return true; }) : null, - CustomFormatter = new EcsTextFormatter(), - AutoRegisterTemplate = true, - AutoRegisterTemplateVersion = AutoRegisterTemplateVersion.ESv7, - IndexFormat = "tgs-logs", - }); - } - } + if (elasticsearchSinkOptions != null) + configuration.WriteTo.Elasticsearch(elasticsearchSinkOptions); builder.AddSerilog(configuration.CreateLogger(), true); @@ -222,6 +213,7 @@ namespace Tgstation.Server.Host.Extensions if (additionalLoggerProvider != null) builder.Services.TryAddEnumerable(additionalLoggerProvider); }); + } /// /// Set the modifiable services to their default types. diff --git a/src/Tgstation.Server.Host/Extensions/TaskExtensions.cs b/src/Tgstation.Server.Host/Extensions/TaskExtensions.cs index e2dbb8f8ff..a81619a512 100644 --- a/src/Tgstation.Server.Host/Extensions/TaskExtensions.cs +++ b/src/Tgstation.Server.Host/Extensions/TaskExtensions.cs @@ -1,6 +1,4 @@ -using System; -using System.Threading; -using System.Threading.Tasks; +using System.Threading.Tasks; namespace Tgstation.Server.Host.Extensions { @@ -18,46 +16,5 @@ namespace Tgstation.Server.Host.Extensions /// Gets a that never completes. /// public static Task InfiniteTask => InfiniteTaskCompletionSource.Task; - - /// - /// Create a that can be awaited while respecting a given . - /// - /// The to add cancel support to. - /// The for the operation. - /// A representing the running operation. - public static Task WithToken(this Task task, CancellationToken cancellationToken) - { - ArgumentNullException.ThrowIfNull(task); - - async Task Wrap() - { - await task; - return null; - } - - return Wrap().WithToken(cancellationToken); - } - - /// - /// Create a that can be awaited while respecting a given . - /// - /// The result of the . - /// The to add cancel support to. - /// The for the operation. - /// A resulting in the result of . - public static async Task WithToken(this Task task, CancellationToken cancellationToken) - { - ArgumentNullException.ThrowIfNull(task); - - var cancelTcs = new TaskCompletionSource(); - Task completedTask; - using (cancellationToken.Register(() => cancelTcs.SetCanceled(cancellationToken))) - completedTask = await Task.WhenAny(task, cancelTcs.Task); - - if (completedTask != task) - await cancelTcs.Task; - - return await task; - } } } diff --git a/src/Tgstation.Server.Host/IO/Console.cs b/src/Tgstation.Server.Host/IO/Console.cs index efe70e46a3..0643ac9cab 100644 --- a/src/Tgstation.Server.Host/IO/Console.cs +++ b/src/Tgstation.Server.Host/IO/Console.cs @@ -3,19 +3,33 @@ using System.Text; using System.Threading; using System.Threading.Tasks; -using Tgstation.Server.Host.Extensions; +using Tgstation.Server.Host.System; namespace Tgstation.Server.Host.IO { /// sealed class Console : IConsole, IDisposable { + /// + public string Title + { + get => platformIdentifier.IsWindows + ? global::System.Console.Title + : null; + set => global::System.Console.Title = value; + } + /// public bool Available => Environment.UserInteractive; /// public CancellationToken CancelKeyPress => cancelKeyCts.Token; + /// + /// The for the . + /// + readonly IPlatformIdentifier platformIdentifier; + /// /// The for . /// @@ -29,8 +43,11 @@ namespace Tgstation.Server.Host.IO /// /// Initializes a new instance of the class. /// - public Console() + /// The value of . + public Console(IPlatformIdentifier platformIdentifier) { + this.platformIdentifier = platformIdentifier ?? throw new ArgumentNullException(nameof(platformIdentifier)); + cancelKeyCts = new CancellationTokenSource(); global::System.Console.CancelKeyPress += (sender, e) => { @@ -102,7 +119,7 @@ namespace Tgstation.Server.Host.IO cancellationToken, DefaultIOManager.BlockingTaskCreationOptions, TaskScheduler.Current) - .WithToken(cancellationToken); + .WaitAsync(cancellationToken); /// public Task WriteAsync(string text, bool newLine, CancellationToken cancellationToken) => Task.Factory.StartNew( diff --git a/src/Tgstation.Server.Host/IO/DefaultIOManager.cs b/src/Tgstation.Server.Host/IO/DefaultIOManager.cs index e6e6c1ca0f..fd01d2aa38 100644 --- a/src/Tgstation.Server.Host/IO/DefaultIOManager.cs +++ b/src/Tgstation.Server.Host/IO/DefaultIOManager.cs @@ -389,7 +389,7 @@ namespace Tgstation.Server.Host.IO async Task CopyThisFile() { - await subdirCreationTask.WithToken(cancellationToken); + await subdirCreationTask.WaitAsync(cancellationToken); using var lockContext = semaphore != null ? await SemaphoreSlimContext.Lock(semaphore, cancellationToken) : null; diff --git a/src/Tgstation.Server.Host/IO/IConsole.cs b/src/Tgstation.Server.Host/IO/IConsole.cs index 8fbe368f39..605f3e421d 100644 --- a/src/Tgstation.Server.Host/IO/IConsole.cs +++ b/src/Tgstation.Server.Host/IO/IConsole.cs @@ -8,6 +8,11 @@ namespace Tgstation.Server.Host.IO /// interface IConsole { + /// + /// Gets or sets the window's title. Can return if getting the console title is not supported. + /// + string Title { get; set; } + /// /// If the is visible to the user. /// diff --git a/src/Tgstation.Server.Host/Jobs/JobHandler.cs b/src/Tgstation.Server.Host/Jobs/JobHandler.cs index 98cbe88651..1bf64380f4 100644 --- a/src/Tgstation.Server.Host/Jobs/JobHandler.cs +++ b/src/Tgstation.Server.Host/Jobs/JobHandler.cs @@ -64,7 +64,7 @@ namespace Tgstation.Server.Host.Jobs if (task == null) throw new InvalidOperationException("Job not started!"); - return task.WithToken(cancellationToken); + return task.WaitAsync(cancellationToken); } /// diff --git a/src/Tgstation.Server.Host/Jobs/JobService.cs b/src/Tgstation.Server.Host/Jobs/JobService.cs index 6f8b3901fe..e33851c41a 100644 --- a/src/Tgstation.Server.Host/Jobs/JobService.cs +++ b/src/Tgstation.Server.Host/Jobs/JobService.cs @@ -265,7 +265,7 @@ namespace Tgstation.Server.Host.Jobs } if (noMoreJobsShouldStart && !handler.Started) - await Extensions.TaskExtensions.InfiniteTask.WithToken(cancellationToken); + await Extensions.TaskExtensions.InfiniteTask.WaitAsync(cancellationToken); Task cancelTask = null; using (jobCancellationToken.Register(() => cancelTask = CancelJob(job, canceller, true, cancellationToken))) @@ -320,7 +320,7 @@ namespace Tgstation.Server.Host.Jobs } } - var instanceCoreProvider = await activationTcs.Task.WithToken(cancellationToken); + var instanceCoreProvider = await activationTcs.Task.WaitAsync(cancellationToken); logger.LogTrace("Starting job..."); await operation( diff --git a/src/Tgstation.Server.Host/Server.cs b/src/Tgstation.Server.Host/Server.cs index 1a42d25b40..b76891e3ec 100644 --- a/src/Tgstation.Server.Host/Server.cs +++ b/src/Tgstation.Server.Host/Server.cs @@ -114,8 +114,12 @@ namespace Tgstation.Server.Host using (cancellationTokenSource = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken)) using (var fsWatcher = updatePath != null ? new FileSystemWatcher(Path.GetDirectoryName(updatePath)) : null) { - if (fsWatcher != null) + if (updatePath != null) { + // If ever there is a NECESSARY update to the Host Watchdog, change this to use a pipe + // I don't know why I'm only realizing this in 2023 when this is 2019 code + // As it stands, FSWatchers use async I/O on Windows and block a new thread on Linux + // That's an acceptable, if saddening, resource loss for now fsWatcher.Created += WatchForShutdownFileCreation; fsWatcher.EnableRaisingEvents = true; } @@ -241,7 +245,14 @@ namespace Tgstation.Server.Host public Task GracefulShutdown(bool detach) => RestartImpl(null, null, false, detach); /// - public Task Die(Exception exception) => RestartImpl(null, exception, false, true); + public Task Die(Exception exception) + { + if (exception != null) + return RestartImpl(null, exception, false, true); + + StopServerImmediate(); + return Task.CompletedTask; + } /// /// Throws an if the cannot be used. @@ -377,7 +388,7 @@ namespace Tgstation.Server.Host void StopServerImmediate() { shutdownInProgress = true; - logger.LogTrace("Stopping host..."); + logger.LogDebug("Stopping host..."); cancellationTokenSource.Cancel(); } } diff --git a/src/Tgstation.Server.Host/ServerFactory.cs b/src/Tgstation.Server.Host/ServerFactory.cs index 9dddebff23..3e14cdb5a9 100644 --- a/src/Tgstation.Server.Host/ServerFactory.cs +++ b/src/Tgstation.Server.Host/ServerFactory.cs @@ -10,6 +10,7 @@ using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Hosting; using Microsoft.Extensions.Logging; +using Tgstation.Server.Host.Configuration; using Tgstation.Server.Host.Core; using Tgstation.Server.Host.Extensions; using Tgstation.Server.Host.IO; @@ -59,14 +60,27 @@ namespace Tgstation.Server.Host args[oldArgs.Length] = "--hostBuilder:reloadConfigOnChange=false"; } - var basePath = IOManager.ResolvePath(); + const string AppSettings = "appsettings"; + const string AppSettingsRelocationKey = $"--{AppSettings}-base-path="; + + var appsettingsRelativeBasePathArgument = args.FirstOrDefault(arg => arg.StartsWith(AppSettingsRelocationKey, StringComparison.Ordinal)); + string basePath; + if (appsettingsRelativeBasePathArgument != null) + basePath = IOManager.ResolvePath(appsettingsRelativeBasePathArgument[AppSettingsRelocationKey.Length..]); + else + basePath = IOManager.ResolvePath(); + + // this is a massive bloody hack but I don't know a better way to do it + // It's needed for the setup wizard + Environment.SetEnvironmentVariable($"{InternalConfiguration.Section}__{nameof(InternalConfiguration.AppSettingsBasePath)}", basePath); + IHostBuilder CreateDefaultBuilder() => Microsoft.Extensions.Hosting.Host.CreateDefaultBuilder(args) .ConfigureAppConfiguration((context, builder) => { builder.SetBasePath(basePath); - builder.AddYamlFile("appsettings.yml", optional: true, reloadOnChange: false) - .AddYamlFile($"appsettings.{context.HostingEnvironment.EnvironmentName}.yml", optional: true, reloadOnChange: false); + builder.AddYamlFile($"{AppSettings}.yml", optional: true, reloadOnChange: false) + .AddYamlFile($"{AppSettings}.{context.HostingEnvironment.EnvironmentName}.yml", optional: true, reloadOnChange: false); // reorganize the builder so our yaml configs don't override the env/cmdline configs // values obtained via debugger diff --git a/src/Tgstation.Server.Host/Setup/IPostSetupServices.cs b/src/Tgstation.Server.Host/Setup/IPostSetupServices.cs index 8f4e161179..21264d9277 100644 --- a/src/Tgstation.Server.Host/Setup/IPostSetupServices.cs +++ b/src/Tgstation.Server.Host/Setup/IPostSetupServices.cs @@ -33,6 +33,11 @@ namespace Tgstation.Server.Host.Setup /// ElasticsearchConfiguration ElasticsearchConfiguration { get; } + /// + /// The . + /// + InternalConfiguration InternalConfiguration { get; } + /// /// The . /// diff --git a/src/Tgstation.Server.Host/Setup/PostSetupServices.cs b/src/Tgstation.Server.Host/Setup/PostSetupServices.cs index 808b2030d9..c1ace340ec 100644 --- a/src/Tgstation.Server.Host/Setup/PostSetupServices.cs +++ b/src/Tgstation.Server.Host/Setup/PostSetupServices.cs @@ -14,6 +14,9 @@ namespace Tgstation.Server.Host.Setup /// public IPlatformIdentifier PlatformIdentifier { get; } + /// + public ILogger Logger { get; } + /// public GeneralConfiguration GeneralConfiguration => generalConfigurationOptions.Value; @@ -27,7 +30,7 @@ namespace Tgstation.Server.Host.Setup public FileLoggingConfiguration FileLoggingConfiguration => fileLoggingConfigurationOptions.Value; /// - public ILogger Logger { get; } + public InternalConfiguration InternalConfiguration => internalConfigurationOptions.Value; /// public ElasticsearchConfiguration ElasticsearchConfiguration => elasticsearchConfigurationOptions.Value; @@ -57,6 +60,11 @@ namespace Tgstation.Server.Host.Setup /// readonly IOptions elasticsearchConfigurationOptions; + /// + /// Backing for . + /// + readonly IOptions internalConfigurationOptions; + /// /// Initializes a new instance of the class. /// @@ -67,6 +75,7 @@ namespace Tgstation.Server.Host.Setup /// The containing the value of . /// The containing the value of . /// The containing the value of . + /// The containing the value of . public PostSetupServices( IPlatformIdentifier platformIdentifier, ILoggerFactory loggerFactory, @@ -74,7 +83,8 @@ namespace Tgstation.Server.Host.Setup IOptions databaseConfigurationOptions, IOptions securityConfigurationOptions, IOptions fileLoggingConfigurationOptions, - IOptions elasticsearchConfigurationOptions) + IOptions elasticsearchConfigurationOptions, + IOptions internalConfigurationOptions) { PlatformIdentifier = platformIdentifier ?? throw new ArgumentNullException(nameof(platformIdentifier)); ArgumentNullException.ThrowIfNull(loggerFactory); @@ -85,6 +95,7 @@ namespace Tgstation.Server.Host.Setup this.securityConfigurationOptions = securityConfigurationOptions ?? throw new ArgumentNullException(nameof(securityConfigurationOptions)); this.fileLoggingConfigurationOptions = fileLoggingConfigurationOptions ?? throw new ArgumentNullException(nameof(fileLoggingConfigurationOptions)); this.elasticsearchConfigurationOptions = elasticsearchConfigurationOptions ?? throw new ArgumentNullException(nameof(elasticsearchConfigurationOptions)); + this.internalConfigurationOptions = internalConfigurationOptions ?? throw new ArgumentNullException(nameof(internalConfigurationOptions)); } } } diff --git a/src/Tgstation.Server.Host/Setup/SetupApplication.cs b/src/Tgstation.Server.Host/Setup/SetupApplication.cs index 4c85e43a60..56c590ce7d 100644 --- a/src/Tgstation.Server.Host/Setup/SetupApplication.cs +++ b/src/Tgstation.Server.Host/Setup/SetupApplication.cs @@ -61,6 +61,7 @@ namespace Tgstation.Server.Host.Setup services.UseStandardConfig(Configuration); services.UseStandardConfig(Configuration); services.UseStandardConfig(Configuration); + services.UseStandardConfig(Configuration); ConfigureHostedService(services); } diff --git a/src/Tgstation.Server.Host/Setup/SetupWizard.cs b/src/Tgstation.Server.Host/Setup/SetupWizard.cs index 15f2b82e48..dde11140f8 100644 --- a/src/Tgstation.Server.Host/Setup/SetupWizard.cs +++ b/src/Tgstation.Server.Host/Setup/SetupWizard.cs @@ -5,6 +5,7 @@ using System.Data.SqlClient; using System.Globalization; using System.IO; using System.Linq; +using System.Net; using System.Text; using System.Text.RegularExpressions; using System.Threading; @@ -26,12 +27,13 @@ using Tgstation.Server.Host.Extensions.Converters; using Tgstation.Server.Host.IO; using Tgstation.Server.Host.System; using Tgstation.Server.Host.Utils; + using YamlDotNet.Serialization; namespace Tgstation.Server.Host.Setup { /// - sealed class SetupWizard : IHostedService + sealed class SetupWizard : BackgroundService { /// /// The for the . @@ -78,6 +80,11 @@ namespace Tgstation.Server.Host.Setup /// readonly GeneralConfiguration generalConfiguration; + /// + /// The for the . + /// + readonly InternalConfiguration internalConfiguration; + /// /// A that will complete when the is reloaded. /// @@ -96,6 +103,7 @@ namespace Tgstation.Server.Host.Setup /// The value of . /// The in use. /// The containing the value of . + /// The containing the value of . public SetupWizard( IIOManager ioManager, IConsole console, @@ -106,7 +114,8 @@ namespace Tgstation.Server.Host.Setup IAsyncDelayer asyncDelayer, IHostApplicationLifetime applicationLifetime, IConfiguration configuration, - IOptions generalConfigurationOptions) + IOptions generalConfigurationOptions, + IOptions internalConfigurationOptions) { this.ioManager = ioManager ?? throw new ArgumentNullException(nameof(ioManager)); this.console = console ?? throw new ArgumentNullException(nameof(console)); @@ -119,6 +128,7 @@ namespace Tgstation.Server.Host.Setup ArgumentNullException.ThrowIfNull(configuration); generalConfiguration = generalConfigurationOptions?.Value ?? throw new ArgumentNullException(nameof(generalConfigurationOptions)); + internalConfiguration = internalConfigurationOptions?.Value ?? throw new ArgumentNullException(nameof(internalConfigurationOptions)); configuration .GetReloadToken() @@ -128,32 +138,39 @@ namespace Tgstation.Server.Host.Setup } /// - public async Task StartAsync(CancellationToken cancellationToken) + protected override async Task ExecuteAsync(CancellationToken cancellationToken) { await CheckRunWizard(cancellationToken); applicationLifetime.StopApplication(); } - /// - public Task StopAsync(CancellationToken cancellationToken) => Task.CompletedTask; - /// /// A prompt for a yes or no value. /// /// The question . + /// The optional default response if the user doesn't enter anything. /// The for the operation. /// A resulting in if the user replied yes, otherwise. - async Task PromptYesNo(string question, CancellationToken cancellationToken) + async Task PromptYesNo(string question, bool? defaultResponse, CancellationToken cancellationToken) { do { - await console.WriteAsync(question, false, cancellationToken); + await console.WriteAsync($"{question} ({(defaultResponse == true ? 'Y' : 'y')}/{(defaultResponse == false ? 'N' : 'n')}): ", false, cancellationToken); var responseString = await console.ReadLineAsync(false, cancellationToken); - var upperResponse = responseString.ToUpperInvariant(); - if (upperResponse == "Y" || upperResponse == "YES") - return true; - else if (upperResponse == "N" || upperResponse == "NO") - return false; + if (responseString.Length == 0) + { + if (defaultResponse.HasValue) + return defaultResponse.Value; + } + else + { + var upperResponse = responseString.ToUpperInvariant(); + if (upperResponse == "Y" || upperResponse == "YES") + return true; + else if (upperResponse == "N" || upperResponse == "NO") + return false; + } + await console.WriteAsync("Invalid response!", true, cancellationToken); } while (true); @@ -289,7 +306,13 @@ namespace Tgstation.Server.Host.Setup /// A resulting in the SQLite database path to store in the configuration. async Task ValidateNonExistantSqliteDBName(string databaseName, CancellationToken cancellationToken) { - var resolvedPath = ioManager.ResolvePath(databaseName); + var dbPathIsRooted = Path.IsPathRooted(databaseName); + var resolvedPath = ioManager.ResolvePath( + dbPathIsRooted + ? databaseName + : ioManager.ConcatPath( + internalConfiguration.AppSettingsBasePath, + databaseName)); try { var directoryName = ioManager.GetDirectoryName(resolvedPath); @@ -311,12 +334,13 @@ namespace Tgstation.Server.Host.Setup return null; } - if (!Path.IsPathRooted(databaseName)) + if (!dbPathIsRooted) { - await console.WriteAsync("Note, this relative path (currently) resolves to the following:", true, cancellationToken); + await console.WriteAsync("Note, this relative path currently resolves to the following:", true, cancellationToken); await console.WriteAsync(resolvedPath, true, cancellationToken); bool writeResolved = await PromptYesNo( - "Would you like to save the relative path in the configuration? If not, the full path will be saved. (y/n): ", + "Would you like to save the relative path in the configuration? If not, the full path will be saved.", + null, cancellationToken); if (writeResolved) @@ -339,24 +363,13 @@ namespace Tgstation.Server.Host.Setup { await console.WriteAsync(String.Empty, true, cancellationToken); await console.WriteAsync( - "NOTE: It is HIGHLY reccommended that TGS runs on a complete relational database, specfically *NOT* Sqlite.", + "NOTE: If you are serious about hosting public servers, it is HIGHLY reccommended that TGS runs on a database *OTHER THAN* Sqlite.", true, cancellationToken); await console.WriteAsync( - "Sqlite, by nature cannot perform several DDL operations. Because of this future compatiblility cannot be guaranteed.", + "It is, however, the easiest option to get started with and will pose few if any problems in a single user scenario.", true, cancellationToken); - await console.WriteAsync( - "This means that you may not be able to update to the next minor version of TGS without a clean re-installation!", - true, - cancellationToken); - await console.WriteAsync( - "Please consider taking the time to set up a relational database if this is meant to be a long-standing server.", - true, - cancellationToken); - await console.WriteAsync(String.Empty, true, cancellationToken); - - await asyncDelayer.Delay(TimeSpan.FromSeconds(3), cancellationToken); } await console.WriteAsync("What SQL database type will you be using?", true, cancellationToken); @@ -405,6 +418,7 @@ namespace Tgstation.Server.Host.Setup ushort? serverPort = null; bool isSqliteDB = databaseConfiguration.DatabaseType == DatabaseType.Sqlite; + IPHostEntry serverAddressEntry = null; if (!isSqliteDB) do { @@ -430,7 +444,20 @@ namespace Tgstation.Server.Host.Setup } } - break; + try + { + if (serverAddress != null) + { + await console.WriteAsync("Attempting to resolve address...", true, cancellationToken); + serverAddressEntry = await Dns.GetHostEntryAsync(serverAddress, cancellationToken); + } + + break; + } + catch (Exception ex) + { + await console.WriteAsync($"Unable to resolve address: {ex.Message}", true, cancellationToken); + } } while (true); @@ -451,7 +478,10 @@ namespace Tgstation.Server.Host.Setup databaseName = await ValidateNonExistantSqliteDBName(databaseName, cancellationToken); } else - dbExists = await PromptYesNo("Does this database already exist? If not, we will attempt to CREATE it. (y/n): ", cancellationToken); + dbExists = await PromptYesNo( + "Does this database already exist? If not, we will attempt to CREATE it.", + null, + cancellationToken); } if (String.IsNullOrWhiteSpace(databaseName)) @@ -463,7 +493,12 @@ namespace Tgstation.Server.Host.Setup bool useWinAuth; if (databaseConfiguration.DatabaseType == DatabaseType.SqlServer && platformIdentifier.IsWindows) - useWinAuth = await PromptYesNo("Use Windows Authentication? (y/n): ", cancellationToken); + { + var defaultResponse = serverAddressEntry?.AddressList.Any(IPAddress.IsLoopback) ?? false + ? (bool?)true + : null; + useWinAuth = await PromptYesNo("Use Windows Authentication?", defaultResponse, cancellationToken); + } else useWinAuth = false; @@ -648,7 +683,7 @@ namespace Tgstation.Server.Host.Setup if (String.IsNullOrWhiteSpace(newGeneralConfiguration.GitHubAccessToken)) newGeneralConfiguration.GitHubAccessToken = null; - newGeneralConfiguration.HostApiDocumentation = await PromptYesNo("Host API Documentation? (y/n): ", cancellationToken); + newGeneralConfiguration.HostApiDocumentation = await PromptYesNo("Host API Documentation?", false, cancellationToken); return newGeneralConfiguration; } @@ -662,7 +697,7 @@ namespace Tgstation.Server.Host.Setup { var fileLoggingConfiguration = new FileLoggingConfiguration(); await console.WriteAsync(null, true, cancellationToken); - fileLoggingConfiguration.Disable = !await PromptYesNo("Enable file logging? (y/n): ", cancellationToken); + fileLoggingConfiguration.Disable = !await PromptYesNo("Enable file logging?", true, cancellationToken); if (!fileLoggingConfiguration.Disable) { @@ -747,7 +782,7 @@ namespace Tgstation.Server.Host.Setup { var elasticsearchConfiguration = new ElasticsearchConfiguration(); await console.WriteAsync(null, true, cancellationToken); - elasticsearchConfiguration.Enable = await PromptYesNo("Enable logging to an external ElasticSearch server? (y/n): ", cancellationToken); + elasticsearchConfiguration.Enable = await PromptYesNo("Enable logging to an external ElasticSearch server?", false, cancellationToken); if (elasticsearchConfiguration.Enable) { @@ -797,8 +832,11 @@ namespace Tgstation.Server.Host.Setup { var config = new ControlPanelConfiguration { - Enable = await PromptYesNo("Enable the web control panel? (y/n): ", cancellationToken), - AllowAnyOrigin = await PromptYesNo("Allow web control panels hosted elsewhere to access the server? (Access-Control-Allow-Origin: *) (y/n): ", cancellationToken), + Enable = await PromptYesNo("Enable the web control panel?", true, cancellationToken), + AllowAnyOrigin = await PromptYesNo( + "Allow web control panels hosted elsewhere to access the server? (Access-Control-Allow-Origin: *)", + true, + cancellationToken), }; if (!config.AllowAnyOrigin) @@ -822,7 +860,7 @@ namespace Tgstation.Server.Host.Setup /// A resulting in the new . async Task ConfigureSwarm(CancellationToken cancellationToken) { - var enable = await PromptYesNo("Enable swarm mode? (y/n): ", cancellationToken); + var enable = await PromptYesNo("Enable swarm mode?", false, cancellationToken); if (!enable) return null; @@ -860,7 +898,7 @@ namespace Tgstation.Server.Host.Setup } while (String.IsNullOrWhiteSpace(privateKey)); - var controller = await PromptYesNo("Is this server the swarm's controller? (y/n): ", cancellationToken); + var controller = await PromptYesNo("Is this server the swarm's controller? (y/n): ", null, cancellationToken); Uri controllerAddress = null; if (!controller) controllerAddress = await ParseAddress("Enter the swarm controller's HTTP(S) address: "); @@ -937,7 +975,10 @@ namespace Tgstation.Server.Host.Setup try { - await ioManager.WriteAllBytes(userConfigFileName, configBytes, cancellationToken); + await ioManager.WriteAllBytes( + userConfigFileName, + configBytes, + cancellationToken); // Ensure the reload if (generalConfiguration.SetupWizardMode != SetupWizardMode.Only) @@ -1021,7 +1062,9 @@ namespace Tgstation.Server.Host.Setup return; } - var userConfigFileName = String.Format(CultureInfo.InvariantCulture, "appsettings.{0}.yml", hostingEnvironment.EnvironmentName); + var userConfigFileName = ioManager.ConcatPath( + internalConfiguration.AppSettingsBasePath, + String.Format(CultureInfo.InvariantCulture, "appsettings.{0}.yml", hostingEnvironment.EnvironmentName)); async Task HandleSetupCancel() { @@ -1030,8 +1073,18 @@ namespace Tgstation.Server.Host.Setup await console.WriteAsync("Aborting setup!", true, default); } - // Link passed cancellationToken with cancel key press Task finalTask = Task.CompletedTask; + string originalConsoleTitle = null; + void SetConsoleTitle() + { + if (originalConsoleTitle != null) + return; + + originalConsoleTitle = console.Title; + console.Title = $"{assemblyInformationProvider.VersionString} Setup Wizard"; + } + + // Link passed cancellationToken with cancel key press using (var cts = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken, console.CancelKeyPress)) using ((cancellationToken = cts.Token).Register(() => finalTask = HandleSetupCancel())) try @@ -1063,15 +1116,18 @@ namespace Tgstation.Server.Host.Setup { if (forceRun) { + SetConsoleTitle(); await console.WriteAsync(String.Format(CultureInfo.InvariantCulture, "The configuration settings are requesting the setup wizard be run, but you already appear to have a configuration file ({0})!", userConfigFileName), true, cancellationToken); - forceRun = await PromptYesNo("Continue running setup wizard? (y/n): ", cancellationToken); + forceRun = await PromptYesNo("Continue running setup wizard?", false, cancellationToken); } if (!forceRun) return; } + SetConsoleTitle(); + // flush the logs to prevent console conflicts await asyncDelayer.Delay(TimeSpan.FromSeconds(1), cancellationToken); @@ -1080,6 +1136,8 @@ namespace Tgstation.Server.Host.Setup finally { await finalTask; + if (originalConsoleTitle != null) + console.Title = originalConsoleTitle; } } } diff --git a/src/Tgstation.Server.Host/Swarm/SwarmService.cs b/src/Tgstation.Server.Host/Swarm/SwarmService.cs index a41b0cba00..8c4d66122c 100644 --- a/src/Tgstation.Server.Host/Swarm/SwarmService.cs +++ b/src/Tgstation.Server.Host/Swarm/SwarmService.cs @@ -20,7 +20,6 @@ using Tgstation.Server.Common.Http; using Tgstation.Server.Host.Configuration; using Tgstation.Server.Host.Core; using Tgstation.Server.Host.Database; -using Tgstation.Server.Host.Extensions; using Tgstation.Server.Host.IO; using Tgstation.Server.Host.System; using Tgstation.Server.Host.Transfer; @@ -282,7 +281,7 @@ namespace Tgstation.Server.Host.Swarm ? asyncDelayer.Delay( TimeSpan.FromMinutes(SwarmConstants.UpdateCommitTimeoutMinutes), cancellationToken) - : Extensions.TaskExtensions.InfiniteTask.WithToken(cancellationToken); + : Extensions.TaskExtensions.InfiniteTask.WaitAsync(cancellationToken); var commitTask = Task.WhenAny(localUpdateOperation.CommitGate, timeoutTask); @@ -1159,8 +1158,7 @@ namespace Tgstation.Server.Host.Swarm /// the result of the call to . bool TriggerHealthCheck() { - var currentTcs = forceHealthCheckTcs; - forceHealthCheckTcs = new TaskCompletionSource(); + var currentTcs = Interlocked.Exchange(ref forceHealthCheckTcs, new TaskCompletionSource()); return currentTcs.TrySetResult(); } diff --git a/src/Tgstation.Server.Host/System/AssemblyInformationProvider.cs b/src/Tgstation.Server.Host/System/AssemblyInformationProvider.cs index b2462fc64a..aa035c772f 100644 --- a/src/Tgstation.Server.Host/System/AssemblyInformationProvider.cs +++ b/src/Tgstation.Server.Host/System/AssemblyInformationProvider.cs @@ -2,7 +2,8 @@ using System.Net.Http.Headers; using System.Reflection; -using Tgstation.Server.Api; +using Tgstation.Server.Common.Extensions; +using Tgstation.Server.Host.Common; namespace Tgstation.Server.Host.System { @@ -10,7 +11,7 @@ namespace Tgstation.Server.Host.System sealed class AssemblyInformationProvider : IAssemblyInformationProvider { /// - public string VersionPrefix => "tgstation-server"; + public string VersionPrefix => Constants.CanonicalPackageName; /// public Version Version { get; } diff --git a/src/Tgstation.Server.Host/System/NativeMethods.cs b/src/Tgstation.Server.Host/System/NativeMethods.cs index dacb4351bc..dc5046d133 100644 --- a/src/Tgstation.Server.Host/System/NativeMethods.cs +++ b/src/Tgstation.Server.Host/System/NativeMethods.cs @@ -5,7 +5,7 @@ using System.Text; namespace Tgstation.Server.Host.System { /// - /// Native Windows methods used by the code. + /// Native methods used by the code. /// #pragma warning disable SA1602 #pragma warning disable SA1611 diff --git a/src/Tgstation.Server.Host/System/ProcessExecutor.cs b/src/Tgstation.Server.Host/System/ProcessExecutor.cs index 2ee26f2089..af4f01c421 100644 --- a/src/Tgstation.Server.Host/System/ProcessExecutor.cs +++ b/src/Tgstation.Server.Host/System/ProcessExecutor.cs @@ -262,7 +262,7 @@ namespace Tgstation.Server.Host.System if (errorOpen && errorReadTask == null) errorReadTask = stdErrHandle.ReadLineAsync(); - var completedTask = await Task.WhenAny(outputReadTask ?? errorReadTask, errorReadTask ?? outputReadTask).WithToken(disposeToken); + var completedTask = await Task.WhenAny(outputReadTask ?? errorReadTask, errorReadTask ?? outputReadTask).WaitAsync(disposeToken); var line = await completedTask; if (completedTask == outputReadTask) { diff --git a/src/Tgstation.Server.Host/System/SystemDManager.cs b/src/Tgstation.Server.Host/System/SystemDManager.cs index 99f4ade686..d7b852ae58 100644 --- a/src/Tgstation.Server.Host/System/SystemDManager.cs +++ b/src/Tgstation.Server.Host/System/SystemDManager.cs @@ -17,7 +17,7 @@ namespace Tgstation.Server.Host.System /// /// Implements the SystemD notify service protocol. /// - sealed class SystemDManager : IHostedService, IRestartHandler, IDisposable + sealed class SystemDManager : BackgroundService, IRestartHandler, IDisposable { /// /// The sd_notify command for notifying the watchdog we are alive. @@ -44,16 +44,6 @@ namespace Tgstation.Server.Host.System /// readonly ILogger logger; - /// - /// The for . - /// - readonly CancellationTokenSource watchdogCts; - - /// - /// The main task executing in the . - /// - Task runTask; - /// /// If TGS is going to restart. /// @@ -87,22 +77,13 @@ namespace Tgstation.Server.Host.System this.logger = logger ?? throw new ArgumentNullException(nameof(logger)); restartRegistration = serverControl.RegisterForRestart(this); - try - { - watchdogCts = new CancellationTokenSource(); - } - catch - { - restartRegistration.Dispose(); - throw; - } } /// - public void Dispose() + public override void Dispose() { + base.Dispose(); restartRegistration.Dispose(); - watchdogCts.Dispose(); } /// @@ -114,36 +95,16 @@ namespace Tgstation.Server.Host.System } /// - public Task StartAsync(CancellationToken cancellationToken) + protected override async Task ExecuteAsync(CancellationToken cancellationToken) { if (SendSDNotify(SDNotifyWatchdog)) - { - logger.LogDebug("SystemD detected"); - runTask = RunAsync(watchdogCts.Token); - } - else { logger.LogDebug("SystemD not detected"); - runTask = Task.CompletedTask; + return; } - return Task.CompletedTask; - } + logger.LogDebug("SystemD detected"); - /// - public async Task StopAsync(CancellationToken cancellationToken) - { - watchdogCts.Cancel(); - await runTask.WithToken(cancellationToken); - } - - /// - /// Runs the . - /// - /// The for the operation. - /// A representing the running operation. - async Task RunAsync(CancellationToken cancellationToken) - { if (applicationLifetime.ApplicationStarted.IsCancellationRequested) throw new InvalidOperationException("RunAsync called after application started!"); @@ -167,7 +128,7 @@ namespace Tgstation.Server.Host.System try { - await instanceManager.Ready.WithToken(cancellationToken); + await instanceManager.Ready.WaitAsync(cancellationToken); CheckReady(); var watchdogUsec = Environment.GetEnvironmentVariable("WATCHDOG_USEC"); diff --git a/src/Tgstation.Server.Host/System/WindowsNetworkPromptReaper.cs b/src/Tgstation.Server.Host/System/WindowsNetworkPromptReaper.cs index ce0fa3a88b..6578457672 100644 --- a/src/Tgstation.Server.Host/System/WindowsNetworkPromptReaper.cs +++ b/src/Tgstation.Server.Host/System/WindowsNetworkPromptReaper.cs @@ -16,7 +16,7 @@ using Tgstation.Server.Host.Utils; namespace Tgstation.Server.Host.System { /// - sealed class WindowsNetworkPromptReaper : IHostedService, INetworkPromptReaper, IDisposable + sealed class WindowsNetworkPromptReaper : BackgroundService, INetworkPromptReaper { /// /// Number of times to send the button click message. Should be at least 2 or it may fail to focus the window. @@ -38,21 +38,11 @@ namespace Tgstation.Server.Host.System /// readonly ILogger logger; - /// - /// The for the . - /// - readonly CancellationTokenSource cancellationTokenSource; - /// /// The list of s registered. /// readonly List registeredProcesses; - /// - /// The representing the lifetime of the . - /// - Task runTask; - /// /// Callback for . /// @@ -106,26 +96,6 @@ namespace Tgstation.Server.Host.System this.logger = logger ?? throw new ArgumentNullException(nameof(logger)); registeredProcesses = new List(); - cancellationTokenSource = new CancellationTokenSource(); - } - - /// - public void Dispose() => cancellationTokenSource.Dispose(); - - /// - public Task StartAsync(CancellationToken cancellationToken) - { - runTask = Run(cancellationTokenSource.Token); - return Task.CompletedTask; - } - - /// - public async Task StopAsync(CancellationToken cancellationToken) - { - logger.LogTrace("Stopping network prompt reaper..."); - cancellationTokenSource.Cancel(); - await runTask; - registeredProcesses.Clear(); } /// @@ -150,12 +120,8 @@ namespace Tgstation.Server.Host.System }, TaskScheduler.Current); } - /// - /// Main loop for the . - /// - /// The for the operation. - /// A representing the running operation. - async Task Run(CancellationToken cancellationToken) + /// + protected override async Task ExecuteAsync(CancellationToken cancellationToken) { logger.LogDebug("Starting network prompt reaper..."); try @@ -221,7 +187,8 @@ namespace Tgstation.Server.Host.System } finally { - logger.LogDebug("Exiting network prompt reaper..."); + registeredProcesses.Clear(); + logger.LogTrace("Exiting network prompt reaper..."); } } } diff --git a/src/Tgstation.Server.Host/Tgstation.Server.Host.csproj b/src/Tgstation.Server.Host/Tgstation.Server.Host.csproj index a9e8b35652..1b58a03bdd 100644 --- a/src/Tgstation.Server.Host/Tgstation.Server.Host.csproj +++ b/src/Tgstation.Server.Host/Tgstation.Server.Host.csproj @@ -2,26 +2,25 @@ - $(TgsNetVersion) + $(TgsFrameworkVersion) $(TgsCoreVersion) true true + false ../../build/analyzers.ruleset bin/$(Configuration)/$(TargetFramework)/$(AssemblyName).xml API1000 - - - - true - - - - ClientApp/node_modules ClientApp/node_modules/.install-stamp Linux ..\.. - manifest.xml + ../../build/uac_elevation_manifest.xml + Debug;Release;Installer + + + + true + @@ -32,6 +31,7 @@ + @@ -86,7 +86,6 @@ - all runtime; build; native; contentfiles; analyzers; buildtransitive @@ -115,11 +114,6 @@ - - - all - runtime; build; native; contentfiles; analyzers; buildtransitive - @@ -136,22 +130,25 @@ + + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + - - - - - - - - + + + diff --git a/src/Tgstation.Server.Host/Transfer/FileTransferService.cs b/src/Tgstation.Server.Host/Transfer/FileTransferService.cs index 0b7bdb497a..eabb99e445 100644 --- a/src/Tgstation.Server.Host/Transfer/FileTransferService.cs +++ b/src/Tgstation.Server.Host/Transfer/FileTransferService.cs @@ -258,7 +258,7 @@ namespace Tgstation.Server.Host.Transfer var expireAt = DateTimeOffset.UtcNow + TimeSpan.FromMinutes(TicketValidityMinutes); try { - await oldExpireTask.WithToken(disposeCts.Token); + await oldExpireTask.WaitAsync(disposeCts.Token); var now = DateTimeOffset.UtcNow; if (now < expireAt) diff --git a/src/Tgstation.Server.Host/Transfer/FileUploadProvider.cs b/src/Tgstation.Server.Host/Transfer/FileUploadProvider.cs index 402cd6b951..af3ad3229f 100644 --- a/src/Tgstation.Server.Host/Transfer/FileUploadProvider.cs +++ b/src/Tgstation.Server.Host/Transfer/FileUploadProvider.cs @@ -126,7 +126,7 @@ namespace Tgstation.Server.Host.Transfer { streamTcs.TrySetResult(bufferedStream ?? stream); - await completionTcs.Task.WithToken(cancellationToken); + await completionTcs.Task.WaitAsync(cancellationToken); return errorMessage; } } diff --git a/src/Tgstation.Server.Host/Utils/GitHub/GitHubClientFactory.cs b/src/Tgstation.Server.Host/Utils/GitHub/GitHubClientFactory.cs index 5fbd22f2de..2e9abee967 100644 --- a/src/Tgstation.Server.Host/Utils/GitHub/GitHubClientFactory.cs +++ b/src/Tgstation.Server.Host/Utils/GitHub/GitHubClientFactory.cs @@ -96,7 +96,6 @@ namespace Tgstation.Server.Host.Utils.GitHub var now = DateTimeOffset.UtcNow; if (!cacheHit) { - logger.LogTrace("Creating new GitHubClient..."); client = new GitHubClient( new ProductHeaderValue( assemblyInformationProvider.ProductInfoHeaderValue.Product.Name, diff --git a/src/Tgstation.Server.Host/Utils/GitHub/GitHubService.cs b/src/Tgstation.Server.Host/Utils/GitHub/GitHubService.cs index 0bf56b6f9d..053510b759 100644 --- a/src/Tgstation.Server.Host/Utils/GitHub/GitHubService.cs +++ b/src/Tgstation.Server.Host/Utils/GitHub/GitHubService.cs @@ -9,7 +9,6 @@ using Microsoft.Extensions.Logging; using Octokit; using Tgstation.Server.Host.Configuration; -using Tgstation.Server.Host.Extensions; namespace Tgstation.Server.Host.Utils.GitHub { @@ -65,7 +64,7 @@ namespace Tgstation.Server.Host.Utils.GitHub { RedirectUri = oAuthConfiguration.RedirectUrl, }) - .WithToken(cancellationToken); + .WaitAsync(cancellationToken); var token = response.AccessToken; return token; @@ -79,7 +78,7 @@ namespace Tgstation.Server.Host.Utils.GitHub .Repository .Release .GetAll(updatesConfiguration.GitHubRepositoryId) - .WithToken(cancellationToken); + .WaitAsync(cancellationToken); logger.LogTrace("{totalReleases} total releases", allReleases.Count); var releases = allReleases @@ -123,7 +122,7 @@ namespace Tgstation.Server.Host.Utils.GitHub var repository = await gitHubClient .Repository .Get(updatesConfiguration.GitHubRepositoryId) - .WithToken(cancellationToken); + .WaitAsync(cancellationToken); var repoUrl = new Uri(repository.HtmlUrl); logger.LogTrace("Maps to {repostioryUrl}", repoUrl); @@ -136,7 +135,7 @@ namespace Tgstation.Server.Host.Utils.GitHub { logger.LogTrace("CreateOAuthAccessToken"); - var userDetails = await gitHubClient.User.Current().WithToken(cancellationToken); + var userDetails = await gitHubClient.User.Current().WaitAsync(cancellationToken); return userDetails.Id; } @@ -159,7 +158,7 @@ namespace Tgstation.Server.Host.Utils.GitHub repoName, issueNumber, comment) - .WithToken(cancellationToken); + .WaitAsync(cancellationToken); } /// @@ -176,7 +175,7 @@ namespace Tgstation.Server.Host.Utils.GitHub .Get( repoOwner, repoName) - .WithToken(cancellationToken); + .WaitAsync(cancellationToken); return repo.Id; } @@ -199,7 +198,7 @@ namespace Tgstation.Server.Host.Utils.GitHub repoOwner, repoName, newDeployment) - .WithToken(cancellationToken); + .WaitAsync(cancellationToken); return deployment.Id; } @@ -223,7 +222,7 @@ namespace Tgstation.Server.Host.Utils.GitHub repoName, deploymentId, newDeploymentStatus) - .WithToken(cancellationToken); + .WaitAsync(cancellationToken); } /// @@ -240,7 +239,7 @@ namespace Tgstation.Server.Host.Utils.GitHub repoId, deploymentId, newDeploymentStatus) - .WithToken(cancellationToken); + .WaitAsync(cancellationToken); } /// @@ -258,7 +257,7 @@ namespace Tgstation.Server.Host.Utils.GitHub repoOwner, repoName, pullRequestNumber) - .WithToken(cancellationToken); + .WaitAsync(cancellationToken); } } } diff --git a/src/Tgstation.Server.Host/Utils/SwaggerConfiguration.cs b/src/Tgstation.Server.Host/Utils/SwaggerConfiguration.cs index 367febec2f..3bd803b362 100644 --- a/src/Tgstation.Server.Host/Utils/SwaggerConfiguration.cs +++ b/src/Tgstation.Server.Host/Utils/SwaggerConfiguration.cs @@ -16,6 +16,7 @@ using Swashbuckle.AspNetCore.SwaggerGen; using Tgstation.Server.Api; using Tgstation.Server.Api.Models; using Tgstation.Server.Api.Models.Response; +using Tgstation.Server.Common.Extensions; using Tgstation.Server.Host.Controllers; namespace Tgstation.Server.Host.Utils diff --git a/src/Tgstation.Server.Host/appsettings.yml b/src/Tgstation.Server.Host/appsettings.yml index 10af3e04e7..2371468ca6 100644 --- a/src/Tgstation.Server.Host/appsettings.yml +++ b/src/Tgstation.Server.Host/appsettings.yml @@ -1,3 +1,5 @@ +# Base tgstation-server configuration file +# You SHOULD NOT edit this file. Instead, make changes in its override file: appsettings.Production.yml General: # ConfigVersion: # Basic semver. Differs from TGS version to version. See changelog for current version MinimumPasswordLength: 15 # Minimum TGS user password length diff --git a/tests/Tgstation.Server.Api.Tests/Tgstation.Server.Api.Tests.csproj b/tests/Tgstation.Server.Api.Tests/Tgstation.Server.Api.Tests.csproj index 4cdf2fb2a0..8443db7356 100644 --- a/tests/Tgstation.Server.Api.Tests/Tgstation.Server.Api.Tests.csproj +++ b/tests/Tgstation.Server.Api.Tests/Tgstation.Server.Api.Tests.csproj @@ -2,7 +2,7 @@ - $(TgsNetVersion) + $(TgsFrameworkVersion) diff --git a/tests/Tgstation.Server.Client.Tests/Tgstation.Server.Client.Tests.csproj b/tests/Tgstation.Server.Client.Tests/Tgstation.Server.Client.Tests.csproj index 6b82ba7671..0d2d877fe1 100644 --- a/tests/Tgstation.Server.Client.Tests/Tgstation.Server.Client.Tests.csproj +++ b/tests/Tgstation.Server.Client.Tests/Tgstation.Server.Client.Tests.csproj @@ -2,7 +2,7 @@ - $(TgsNetVersion) + $(TgsFrameworkVersion) diff --git a/tests/Tgstation.Server.Host.Console.Tests/TestProgram.cs b/tests/Tgstation.Server.Host.Console.Tests/TestProgram.cs index edb19af15e..0fc337843c 100644 --- a/tests/Tgstation.Server.Host.Console.Tests/TestProgram.cs +++ b/tests/Tgstation.Server.Host.Console.Tests/TestProgram.cs @@ -1,4 +1,4 @@ -using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Logging; using Microsoft.VisualStudio.TestTools.UnitTesting; using Moq; using System; @@ -17,9 +17,9 @@ namespace Tgstation.Server.Host.Console.Tests { var mockServer = new Mock(); var args = Array.Empty(); - mockServer.Setup(x => x.RunAsync(false, args, It.IsAny())).Returns(Task.CompletedTask).Verifiable(); + mockServer.Setup(x => x.RunAsync(false, args, It.IsAny())).Returns(Task.FromResult(true)).Verifiable(); var mockServerFactory = new Mock(); - mockServerFactory.Setup(x => x.CreateWatchdog(It.IsAny())).Returns(mockServer.Object).Verifiable(); + mockServerFactory.Setup(x => x.CreateWatchdog(It.IsNotNull(), It.IsNotNull())).Returns(mockServer.Object).Verifiable(); Program.WatchdogFactory = mockServerFactory.Object; await Program.Main(args); mockServer.VerifyAll(); diff --git a/tests/Tgstation.Server.Host.Console.Tests/Tgstation.Server.Host.Console.Tests.csproj b/tests/Tgstation.Server.Host.Console.Tests/Tgstation.Server.Host.Console.Tests.csproj index 282b1742c7..e0d44a649e 100644 --- a/tests/Tgstation.Server.Host.Console.Tests/Tgstation.Server.Host.Console.Tests.csproj +++ b/tests/Tgstation.Server.Host.Console.Tests/Tgstation.Server.Host.Console.Tests.csproj @@ -2,7 +2,7 @@ - $(TgsNetVersion) + $(TgsFrameworkVersion) diff --git a/tests/Tgstation.Server.Host.Service.Tests/TestServerService.cs b/tests/Tgstation.Server.Host.Service.Tests/TestServerService.cs index 03846f15a6..54dc719a0b 100644 --- a/tests/Tgstation.Server.Host.Service.Tests/TestServerService.cs +++ b/tests/Tgstation.Server.Host.Service.Tests/TestServerService.cs @@ -19,9 +19,10 @@ namespace Tgstation.Server.Host.Service.Tests [TestMethod] public void TestConstructionAndDisposal() { - Assert.ThrowsException(() => new ServerService(null, default)); + Assert.ThrowsException(() => new ServerService(null, null, default)); var mockWatchdogFactory = new Mock(); - new ServerService(mockWatchdogFactory.Object, default).Dispose(); + Assert.ThrowsException(() => new ServerService(mockWatchdogFactory.Object, null, default)); + new ServerService(mockWatchdogFactory.Object, Array.Empty(), default).Dispose(); } [TestMethod] @@ -34,11 +35,11 @@ namespace Tgstation.Server.Host.Service.Tests var mockWatchdog = new Mock(); var args = Array.Empty(); CancellationToken cancellationToken; - mockWatchdog.Setup(x => x.RunAsync(false, args, It.IsAny())).Callback((bool x, string[] _, CancellationToken token) => cancellationToken = token).Returns(Task.CompletedTask).Verifiable(); + mockWatchdog.Setup(x => x.RunAsync(false, It.IsNotNull(), It.IsAny())).Callback((bool x, string[] _, CancellationToken token) => cancellationToken = token).Returns(Task.FromResult(true)).Verifiable(); var mockWatchdogFactory = new Mock(); - mockWatchdogFactory.Setup(x => x.CreateWatchdog(It.IsNotNull())).Returns(mockWatchdog.Object).Verifiable(); + mockWatchdogFactory.Setup(x => x.CreateWatchdog(It.IsNotNull(), It.IsNotNull())).Returns(mockWatchdog.Object).Verifiable(); - using (var service = new ServerService(mockWatchdogFactory.Object, default)) + using (var service = new ServerService(mockWatchdogFactory.Object, Array.Empty(), default)) { onStart.Invoke(service, new object[] { args }); onStop.Invoke(service, Array.Empty()); diff --git a/tests/Tgstation.Server.Host.Service.Tests/Tgstation.Server.Host.Service.Tests.csproj b/tests/Tgstation.Server.Host.Service.Tests/Tgstation.Server.Host.Service.Tests.csproj index 721a869ca1..54971a8c9f 100644 --- a/tests/Tgstation.Server.Host.Service.Tests/Tgstation.Server.Host.Service.Tests.csproj +++ b/tests/Tgstation.Server.Host.Service.Tests/Tgstation.Server.Host.Service.Tests.csproj @@ -1,7 +1,8 @@ + - net472 + $(TgsFrameworkVersion)-windows @@ -29,8 +30,4 @@ - - - - diff --git a/tests/Tgstation.Server.Host.Tests.Signals/Program.cs b/tests/Tgstation.Server.Host.Tests.Signals/Program.cs index 995031cc4e..dc222f6331 100644 --- a/tests/Tgstation.Server.Host.Tests.Signals/Program.cs +++ b/tests/Tgstation.Server.Host.Tests.Signals/Program.cs @@ -40,17 +40,17 @@ namespace Tgstation.Server.Host.Tests.Signals Assert.IsFalse(tcs.Task.IsCompleted); using var cts = new CancellationTokenSource(TimeSpan.FromSeconds(5)); - await signalHandler.StopAsync(default).WithToken(cts.Token); + await signalHandler.StopAsync(default).WaitAsync(cts.Token); Assert.IsFalse(tcs.Task.IsCompleted); using var signalHandler2 = new PosixSignalHandler(mockServerControl.Object, mockAsyncDelayer.Object, Mock.Of>()); await signalHandler2.StartAsync(default); using var cts2 = new CancellationTokenSource(TimeSpan.FromSeconds(20)); - await tcs.Task.WithToken(cts2.Token); + await tcs.Task.WaitAsync(cts2.Token); using var cts3 = new CancellationTokenSource(TimeSpan.FromSeconds(5)); - await signalHandler2.StopAsync(default).WithToken(cts3.Token); + await signalHandler2.StopAsync(default).WaitAsync(cts3.Token); } } } diff --git a/tests/Tgstation.Server.Host.Tests.Signals/Tgstation.Server.Host.Tests.Signals.csproj b/tests/Tgstation.Server.Host.Tests.Signals/Tgstation.Server.Host.Tests.Signals.csproj index ce86279440..c67edb6003 100644 --- a/tests/Tgstation.Server.Host.Tests.Signals/Tgstation.Server.Host.Tests.Signals.csproj +++ b/tests/Tgstation.Server.Host.Tests.Signals/Tgstation.Server.Host.Tests.Signals.csproj @@ -2,7 +2,7 @@ - $(TgsNetVersion) + $(TgsFrameworkVersion) Exe diff --git a/tests/Tgstation.Server.Host.Tests/Extensions/Converters/TestVersionConverter.cs b/tests/Tgstation.Server.Host.Tests/Extensions/Converters/TestVersionConverter.cs index 8347ca19f7..deb2fc1760 100644 --- a/tests/Tgstation.Server.Host.Tests/Extensions/Converters/TestVersionConverter.cs +++ b/tests/Tgstation.Server.Host.Tests/Extensions/Converters/TestVersionConverter.cs @@ -1,7 +1,7 @@ -using Microsoft.VisualStudio.TestTools.UnitTesting; +using Microsoft.VisualStudio.TestTools.UnitTesting; using System; -using Tgstation.Server.Api; +using Tgstation.Server.Common.Extensions; using YamlDotNet.Serialization; diff --git a/tests/Tgstation.Server.Host.Tests/IO/TestConsole.cs b/tests/Tgstation.Server.Host.Tests/IO/TestConsole.cs index d093287c7a..f5878a7e2f 100644 --- a/tests/Tgstation.Server.Host.Tests/IO/TestConsole.cs +++ b/tests/Tgstation.Server.Host.Tests/IO/TestConsole.cs @@ -4,15 +4,23 @@ using System.IO; using System.Threading; using System.Threading.Tasks; +using Tgstation.Server.Host.System; + namespace Tgstation.Server.Host.IO.Tests { [TestClass] public sealed class TestConsole { + [TestMethod] + public void TestContructionThrows() + { + Assert.ThrowsException(() => new Console(null)); + } + [TestMethod] public async Task TestWriteLine() { - var console = new Console(); + var console = new Console(new PlatformIdentifier()); await Assert.ThrowsExceptionAsync(() => console.WriteAsync(null, false, default)); try { @@ -28,7 +36,7 @@ namespace Tgstation.Server.Host.IO.Tests [TestMethod] public void TestUserInteractive() { - var console = new Console(); + var console = new Console(new PlatformIdentifier()); Assert.AreEqual(Environment.UserInteractive, console.Available); } } diff --git a/tests/Tgstation.Server.Host.Tests/Setup/TestSetupWizard.cs b/tests/Tgstation.Server.Host.Tests/Setup/TestSetupWizard.cs index 841f97bfd1..76e2e26ee0 100644 --- a/tests/Tgstation.Server.Host.Tests/Setup/TestSetupWizard.cs +++ b/tests/Tgstation.Server.Host.Tests/Setup/TestSetupWizard.cs @@ -27,26 +27,28 @@ namespace Tgstation.Server.Host.Setup.Tests [TestMethod] public void TestConstructionThrows() { - Assert.ThrowsException(() => new SetupWizard(null, null, null, null, null, null, null, null, null, null)); + Assert.ThrowsException(() => new SetupWizard(null, null, null, null, null, null, null, null, null, null, null)); var mockIOManager = new Mock(); - Assert.ThrowsException(() => new SetupWizard(mockIOManager.Object, null, null, null, null, null, null, null, null, null)); + Assert.ThrowsException(() => new SetupWizard(mockIOManager.Object, null, null, null, null, null, null, null, null, null, null)); var mockConsole = new Mock(); - Assert.ThrowsException(() => new SetupWizard(mockIOManager.Object, mockConsole.Object, null, null, null, null, null, null, null, null)); + Assert.ThrowsException(() => new SetupWizard(mockIOManager.Object, mockConsole.Object, null, null, null, null, null, null, null, null, null)); var mockHostingEnvironment = new Mock(); - Assert.ThrowsException(() => new SetupWizard(mockIOManager.Object, mockConsole.Object, mockHostingEnvironment.Object, null, null, null, null, null, null, null)); + Assert.ThrowsException(() => new SetupWizard(mockIOManager.Object, mockConsole.Object, mockHostingEnvironment.Object, null, null, null, null, null, null, null, null)); var mockAssemblyInfoProvider = new Mock(); - Assert.ThrowsException(() => new SetupWizard(mockIOManager.Object, mockConsole.Object, mockHostingEnvironment.Object, mockAssemblyInfoProvider.Object, null, null, null, null, null, null)); + Assert.ThrowsException(() => new SetupWizard(mockIOManager.Object, mockConsole.Object, mockHostingEnvironment.Object, mockAssemblyInfoProvider.Object, null, null, null, null, null, null, null)); var mockDBConnectionFactory = new Mock(); - Assert.ThrowsException(() => new SetupWizard(mockIOManager.Object, mockConsole.Object, mockHostingEnvironment.Object, mockAssemblyInfoProvider.Object, mockDBConnectionFactory.Object, null, null, null, null, null)); + Assert.ThrowsException(() => new SetupWizard(mockIOManager.Object, mockConsole.Object, mockHostingEnvironment.Object, mockAssemblyInfoProvider.Object, mockDBConnectionFactory.Object, null, null, null, null, null, null)); var mockPlatformIdentifier = new Mock(); - Assert.ThrowsException(() => new SetupWizard(mockIOManager.Object, mockConsole.Object, mockHostingEnvironment.Object, mockAssemblyInfoProvider.Object, mockDBConnectionFactory.Object, mockPlatformIdentifier.Object, null, null, null, null)); + Assert.ThrowsException(() => new SetupWizard(mockIOManager.Object, mockConsole.Object, mockHostingEnvironment.Object, mockAssemblyInfoProvider.Object, mockDBConnectionFactory.Object, mockPlatformIdentifier.Object, null, null, null, null, null)); var mockAsyncDelayer = new Mock(); - Assert.ThrowsException(() => new SetupWizard(mockIOManager.Object, mockConsole.Object, mockHostingEnvironment.Object, mockAssemblyInfoProvider.Object, mockDBConnectionFactory.Object, mockPlatformIdentifier.Object, mockAsyncDelayer.Object, null, null, null)); + Assert.ThrowsException(() => new SetupWizard(mockIOManager.Object, mockConsole.Object, mockHostingEnvironment.Object, mockAssemblyInfoProvider.Object, mockDBConnectionFactory.Object, mockPlatformIdentifier.Object, mockAsyncDelayer.Object, null, null, null, null)); var mockLifetime = new Mock(); - Assert.ThrowsException(() => new SetupWizard(mockIOManager.Object, mockConsole.Object, mockHostingEnvironment.Object, mockAssemblyInfoProvider.Object, mockDBConnectionFactory.Object, mockPlatformIdentifier.Object, mockAsyncDelayer.Object, mockLifetime.Object, null, null)); + Assert.ThrowsException(() => new SetupWizard(mockIOManager.Object, mockConsole.Object, mockHostingEnvironment.Object, mockAssemblyInfoProvider.Object, mockDBConnectionFactory.Object, mockPlatformIdentifier.Object, mockAsyncDelayer.Object, mockLifetime.Object, null, null, null)); var mockConfiguration = new Mock(); mockConfiguration.Setup(x => x.GetReloadToken()).Returns(Mock.Of()); - Assert.ThrowsException(() => new SetupWizard(mockIOManager.Object, mockConsole.Object, mockHostingEnvironment.Object, mockAssemblyInfoProvider.Object, mockDBConnectionFactory.Object, mockPlatformIdentifier.Object, mockAsyncDelayer.Object, mockLifetime.Object, mockConfiguration.Object, null)); + Assert.ThrowsException(() => new SetupWizard(mockIOManager.Object, mockConsole.Object, mockHostingEnvironment.Object, mockAssemblyInfoProvider.Object, mockDBConnectionFactory.Object, mockPlatformIdentifier.Object, mockAsyncDelayer.Object, mockLifetime.Object, mockConfiguration.Object, null, null)); + var mockGeneralConfigurationOptions = Options.Create(new GeneralConfiguration()); + Assert.ThrowsException(() => new SetupWizard(mockIOManager.Object, mockConsole.Object, mockHostingEnvironment.Object, mockAssemblyInfoProvider.Object, mockDBConnectionFactory.Object, mockPlatformIdentifier.Object, mockAsyncDelayer.Object, mockLifetime.Object, mockConfiguration.Object, mockGeneralConfigurationOptions, null)); } [TestMethod] @@ -59,6 +61,7 @@ namespace Tgstation.Server.Host.Setup.Tests var mockDBConnectionFactory = new Mock(); var mockLifetime = new Mock(); var mockGeneralConfigurationOptions = new Mock>(); + var mockInternalConfigurationOptions = new Mock>(); var mockPlatformIdentifier = new Mock(); var mockAsyncDelayer = new Mock(); var mockConfiguration = new Mock(); @@ -85,21 +88,42 @@ namespace Tgstation.Server.Host.Setup.Tests }; mockGeneralConfigurationOptions.SetupGet(x => x.Value).Returns(testGeneralConfig).Verifiable(); - var wizard = new SetupWizard(mockIOManager.Object, mockConsole.Object, mockHostingEnvironment.Object, mockAssemblyInfoProvider.Object, mockDBConnectionFactory.Object, mockPlatformIdentifier.Object, mockAsyncDelayer.Object, mockLifetime.Object, mockConfiguration.Object, mockGeneralConfigurationOptions.Object); + var testInternalConfig = new InternalConfiguration + { + AppSettingsBasePath = "asdfasdfasdf" + }; + mockInternalConfigurationOptions.SetupGet(x => x.Value).Returns(testInternalConfig).Verifiable(); + var wizard = new SetupWizard( + mockIOManager.Object, + mockConsole.Object, + mockHostingEnvironment.Object, + mockAssemblyInfoProvider.Object, + mockDBConnectionFactory.Object, + mockPlatformIdentifier.Object, + mockAsyncDelayer.Object, + mockLifetime.Object, + mockConfiguration.Object, + mockGeneralConfigurationOptions.Object, + mockInternalConfigurationOptions.Object); mockPlatformIdentifier.SetupGet(x => x.IsWindows).Returns(true).Verifiable(); mockAsyncDelayer.Setup(x => x.Delay(It.IsAny(), It.IsAny())).Returns(Task.CompletedTask).Verifiable(); - await wizard.StartAsync(default); + await RunWizard(); testGeneralConfig.SetupWizardMode = SetupWizardMode.Force; - await Assert.ThrowsExceptionAsync(() => wizard.StartAsync(default)); + await Assert.ThrowsExceptionAsync(() => RunWizard()); testGeneralConfig.SetupWizardMode = SetupWizardMode.Only; - await Assert.ThrowsExceptionAsync(() => wizard.StartAsync(default)); + await Assert.ThrowsExceptionAsync(() => RunWizard()); mockConsole.SetupGet(x => x.Available).Returns(true).Verifiable(); + mockIOManager.Setup(x => x.ConcatPath(testInternalConfig.AppSettingsBasePath, It.IsNotNull())).Returns(paths => + { + Assert.AreEqual(2, paths.Length); + return $"{paths[0]}/{paths[1]}"; + }).Verifiable(); mockIOManager.Setup(x => x.FileExists(It.IsNotNull(), It.IsAny())).Returns(Task.FromResult(true)).Verifiable(); mockIOManager.Setup(x => x.ReadAllBytes(It.IsNotNull(), It.IsAny())).Returns(Task.FromResult(Encoding.UTF8.GetBytes("less profane"))).Verifiable(); mockIOManager @@ -152,14 +176,14 @@ namespace Tgstation.Server.Host.Setup.Tests "666", "FakeDBType", nameof(DatabaseType.SqlServer), - "this isn't validated", + "localhost", "nor is this", "no", //test winauth "yes", //sql server will always fail so reconfigure with maria nameof(DatabaseType.MariaDB), - "bleh", + "127.0.0.1", "blah", "NO", "user", @@ -187,7 +211,7 @@ namespace Tgstation.Server.Host.Setup.Tests String.Empty, //test MySQL errors nameof(DatabaseType.MySql), - String.Empty, + "::1", String.Empty, "DbName", "n", @@ -279,13 +303,20 @@ namespace Tgstation.Server.Host.Setup.Tests .Returns(Task.CompletedTask) .Verifiable(); - await wizard.StartAsync(default); + async Task RunWizard() + { + await wizard.StartAsync(default); + await wizard.ExecuteTask; + await wizard.StopAsync(default); + } + + await RunWizard(); //first real run - await wizard.StartAsync(default); + await RunWizard(); //second run mockIOManager.Setup(x => x.ReadAllBytes(It.IsNotNull(), It.IsAny())).Returns(Task.FromResult(Encoding.UTF8.GetBytes(String.Empty))).Verifiable(); - await wizard.StartAsync(default); + await RunWizard(); //third run testGeneralConfig.SetupWizardMode = SetupWizardMode.Autodetect; @@ -309,13 +340,14 @@ namespace Tgstation.Server.Host.Setup.Tests return Task.CompletedTask; }).Verifiable(); - await Assert.ThrowsExceptionAsync(() => wizard.StartAsync(default)); + await Assert.ThrowsExceptionAsync(() => RunWizard()); Assert.AreEqual(finalInputSequence.Count, inputPos); mockFailCommand.VerifyAll(); mockSuccessCommand.VerifyAll(); mockIOManager.VerifyAll(); mockGeneralConfigurationOptions.VerifyAll(); + mockInternalConfigurationOptions.VerifyAll(); mockConsole.VerifyAll(); mockGoodDbConnection.VerifyAll(); mockBadDbConnection.VerifyAll(); diff --git a/tests/Tgstation.Server.Host.Tests/Swarm/TestSwarmProtocol.cs b/tests/Tgstation.Server.Host.Tests/Swarm/TestSwarmProtocol.cs index be23a3bce7..80ef9bd8f0 100644 --- a/tests/Tgstation.Server.Host.Tests/Swarm/TestSwarmProtocol.cs +++ b/tests/Tgstation.Server.Host.Tests/Swarm/TestSwarmProtocol.cs @@ -1,5 +1,6 @@ using System; using System.Collections.Generic; +using System.Diagnostics; using System.IO; using System.Threading; using System.Threading.Tasks; @@ -21,6 +22,7 @@ namespace Tgstation.Server.Host.Swarm.Tests { static readonly HashSet usedPorts = new (); static ILoggerFactory loggerFactory; + static ILogger logger; static ISeekableFileStreamProvider updateFileStreamProvider; @@ -33,6 +35,8 @@ namespace Tgstation.Server.Host.Swarm.Tests builder.AddConsole(); }); + logger = loggerFactory.CreateLogger(); + var ms = new MemoryStream(new byte[] { 1, 2, 3, 4 }); updateFileStreamProvider = new BufferedFileStreamProvider(ms); @@ -122,14 +126,16 @@ namespace Tgstation.Server.Host.Swarm.Tests await using var node1 = GenNode(controller); TestableSwarmNode.Link(controller, node1); - Assert.AreEqual(SwarmRegistrationResult.Success, await controller.TryInit()); + + node1.RpcMapper.AsyncRequests = false; Assert.AreEqual(SwarmRegistrationResult.Success, await node1.TryInit()); + node1.RpcMapper.AsyncRequests = true; Assert.AreEqual(2, controller.Service.GetSwarmServers().Count); Assert.AreEqual(1, node1.Service.GetSwarmServers().Count); - await DelayMax(() => Assert.AreEqual(2, node1.Service.GetSwarmServers().Count)); + await DelayMax(() => Assert.AreEqual(2, node1.Service.GetSwarmServers().Count), 15); // node checks every 5 minutes, delays are 100ms await controller.SimulateReboot(default); Assert.AreEqual(SwarmRegistrationResult.Success, await controller.TryInit()); @@ -283,20 +289,38 @@ namespace Tgstation.Server.Host.Swarm.Tests return new TestableSwarmNode(loggerFactory, config, version); } - static async Task DelayMax(Action assertion, ulong seconds = 1) + static async Task DelayMax(Action assertion, long seconds = 1) { - for (var i = 0U; i < (seconds * 10); ++i) + var id = Guid.NewGuid(); + logger.LogInformation("Begin DelayMax {id}: {seconds}", id, seconds); + var stopwatch = Stopwatch.StartNew(); + do { try { assertion(); + logger.LogInformation("End DelayMax {id} after {milliseconds}ms", id, stopwatch.ElapsedMilliseconds); return; } - catch (AssertFailedException) { } + catch (AssertFailedException) + { + } + await Task.Delay(TimeSpan.FromMilliseconds(100)); } + while (stopwatch.ElapsedMilliseconds < (seconds * 1000)); - assertion(); + try + { + assertion(); + } + catch + { + logger.LogError("Fail DelayMax {id} after {milliseconds}ms", id, stopwatch.ElapsedMilliseconds); + throw; + } + + logger.LogInformation("End DelayMax {id} after {milliseconds}ms", id, stopwatch.ElapsedMilliseconds); } } } diff --git a/tests/Tgstation.Server.Host.Tests/Tgstation.Server.Host.Tests.csproj b/tests/Tgstation.Server.Host.Tests/Tgstation.Server.Host.Tests.csproj index b5449c75c4..5f0701e388 100644 --- a/tests/Tgstation.Server.Host.Tests/Tgstation.Server.Host.Tests.csproj +++ b/tests/Tgstation.Server.Host.Tests/Tgstation.Server.Host.Tests.csproj @@ -2,7 +2,7 @@ - $(TgsNetVersion) + $(TgsFrameworkVersion) diff --git a/tests/Tgstation.Server.Host.Watchdog.Tests/TestWatchdog.cs b/tests/Tgstation.Server.Host.Watchdog.Tests/TestWatchdog.cs index 67c3eb9289..0ba2e31764 100644 --- a/tests/Tgstation.Server.Host.Watchdog.Tests/TestWatchdog.cs +++ b/tests/Tgstation.Server.Host.Watchdog.Tests/TestWatchdog.cs @@ -13,9 +13,11 @@ namespace Tgstation.Server.Host.Watchdog.Tests [TestMethod] public void TestConstruction() { - Assert.ThrowsException(() => new Watchdog(null)); - var mockLogger = new LoggerFactory().CreateLogger(); - var wd = new Watchdog(mockLogger); + Assert.ThrowsException(() => new Watchdog(null, null)); + var mockSignalChecker = Mock.Of(); + Assert.ThrowsException(() => new Watchdog(mockSignalChecker, null)); + var mockLogger = Mock.Of>(); + var wd = new Watchdog(mockSignalChecker, mockLogger); } } } diff --git a/tests/Tgstation.Server.Host.Watchdog.Tests/TestWatchdogFactory.cs b/tests/Tgstation.Server.Host.Watchdog.Tests/TestWatchdogFactory.cs index afc5f24c62..f3bededdba 100644 --- a/tests/Tgstation.Server.Host.Watchdog.Tests/TestWatchdogFactory.cs +++ b/tests/Tgstation.Server.Host.Watchdog.Tests/TestWatchdogFactory.cs @@ -1,6 +1,8 @@ using Microsoft.Extensions.Logging; using Microsoft.VisualStudio.TestTools.UnitTesting; +using Moq; + namespace Tgstation.Server.Host.Watchdog.Tests { /// @@ -13,7 +15,10 @@ namespace Tgstation.Server.Host.Watchdog.Tests public void TestCreateWatchdog() { var factory = new WatchdogFactory(); - Assert.IsNotNull(factory.CreateWatchdog(new LoggerFactory())); + Assert.IsNotNull( + factory.CreateWatchdog( + Mock.Of(), + Mock.Of())); } } } diff --git a/tests/Tgstation.Server.Host.Watchdog.Tests/Tgstation.Server.Host.Watchdog.Tests.csproj b/tests/Tgstation.Server.Host.Watchdog.Tests/Tgstation.Server.Host.Watchdog.Tests.csproj index 21a8d65ba5..a3762c1162 100644 --- a/tests/Tgstation.Server.Host.Watchdog.Tests/Tgstation.Server.Host.Watchdog.Tests.csproj +++ b/tests/Tgstation.Server.Host.Watchdog.Tests/Tgstation.Server.Host.Watchdog.Tests.csproj @@ -2,7 +2,7 @@ - $(TgsNetVersion) + $(TgsFrameworkVersion) diff --git a/tests/Tgstation.Server.Tests/Live/DummyGitHubService.cs b/tests/Tgstation.Server.Tests/Live/DummyGitHubService.cs index c1bcd2903e..35f62867c9 100644 --- a/tests/Tgstation.Server.Tests/Live/DummyGitHubService.cs +++ b/tests/Tgstation.Server.Tests/Live/DummyGitHubService.cs @@ -45,7 +45,7 @@ namespace Tgstation.Server.Tests.Live .Repository .Release .GetAll("tgstation", "tgstation-server") - .WithToken(cancellationToken); + .WaitAsync(cancellationToken); targetRelease = releases.FirstOrDefault(release => release.TagName == $"{new UpdatesConfiguration().GitTagPrefix}{TestLiveServer.TestUpdateVersion}"); } @@ -59,7 +59,7 @@ namespace Tgstation.Server.Tests.Live testPr = await gitHubClient .PullRequest .Get("Cyberboss", "common_core", 2) - .WithToken(cancellationToken); + .WaitAsync(cancellationToken); ServiceCollectionExtensions.UseGitHubServiceFactory(); } diff --git a/tests/Tgstation.Server.Tests/Live/Instance/ByondTest.cs b/tests/Tgstation.Server.Tests/Live/Instance/ByondTest.cs index 68e89c0f9f..c4467ba34e 100644 --- a/tests/Tgstation.Server.Tests/Live/Instance/ByondTest.cs +++ b/tests/Tgstation.Server.Tests/Live/Instance/ByondTest.cs @@ -9,11 +9,11 @@ using Microsoft.VisualStudio.TestTools.UnitTesting; using Moq; -using Tgstation.Server.Api; using Tgstation.Server.Api.Models; using Tgstation.Server.Api.Models.Request; using Tgstation.Server.Client; using Tgstation.Server.Client.Components; +using Tgstation.Server.Common.Extensions; using Tgstation.Server.Host.Components.Byond; using Tgstation.Server.Host.Configuration; using Tgstation.Server.Host.IO; diff --git a/tests/Tgstation.Server.Tests/Live/Instance/WatchdogTest.cs b/tests/Tgstation.Server.Tests/Live/Instance/WatchdogTest.cs index 004b0f3e8e..2dbb2ca193 100644 --- a/tests/Tgstation.Server.Tests/Live/Instance/WatchdogTest.cs +++ b/tests/Tgstation.Server.Tests/Live/Instance/WatchdogTest.cs @@ -24,6 +24,7 @@ using Tgstation.Server.Api.Models.Request; using Tgstation.Server.Api.Models.Response; using Tgstation.Server.Client; using Tgstation.Server.Client.Components; +using Tgstation.Server.Common.Extensions; using Tgstation.Server.Host.Components; using Tgstation.Server.Host.Components.Chat; using Tgstation.Server.Host.Components.Interop; @@ -590,7 +591,7 @@ namespace Tgstation.Server.Tests.Live.Instance var bridgeTestTopicResult = await TopicClient.SendTopic(IPAddress.Loopback, "tgs_integration_test_tactics2=1", TestLiveServer.DDPort, cancellationToken); Assert.AreEqual("ack2", bridgeTestTopicResult.StringData); - await bridgeTestsTcs.Task.WithToken(cancellationToken); + await bridgeTestsTcs.Task.WaitAsync(cancellationToken); } BridgeController.LogContent = true; diff --git a/tests/Tgstation.Server.Tests/Live/RawRequestTests.cs b/tests/Tgstation.Server.Tests/Live/RawRequestTests.cs index 02884dac07..a9023401ff 100644 --- a/tests/Tgstation.Server.Tests/Live/RawRequestTests.cs +++ b/tests/Tgstation.Server.Tests/Live/RawRequestTests.cs @@ -14,6 +14,7 @@ using Tgstation.Server.Api; using Tgstation.Server.Api.Models; using Tgstation.Server.Api.Models.Response; using Tgstation.Server.Client; +using Tgstation.Server.Common.Extensions; using Tgstation.Server.Host; namespace Tgstation.Server.Tests.Live diff --git a/tests/Tgstation.Server.Tests/Live/TestLiveServer.cs b/tests/Tgstation.Server.Tests/Live/TestLiveServer.cs index 015e6fcdf1..962b39c049 100644 --- a/tests/Tgstation.Server.Tests/Live/TestLiveServer.cs +++ b/tests/Tgstation.Server.Tests/Live/TestLiveServer.cs @@ -31,6 +31,7 @@ using Tgstation.Server.Api.Models.Response; using Tgstation.Server.Api.Rights; using Tgstation.Server.Client; using Tgstation.Server.Client.Components; +using Tgstation.Server.Common.Extensions; using Tgstation.Server.Host.Components; using Tgstation.Server.Host.Configuration; using Tgstation.Server.Host.Database; @@ -1303,7 +1304,7 @@ namespace Tgstation.Server.Tests.Live serverCts.Cancel(); try { - await serverTask.WithToken(hardCancellationToken); + await serverTask.WaitAsync(hardCancellationToken); } catch (OperationCanceledException) { } diff --git a/tests/Tgstation.Server.Tests/TestSystemInteraction.cs b/tests/Tgstation.Server.Tests/TestSystemInteraction.cs index abf97749ef..a7b41aba76 100644 --- a/tests/Tgstation.Server.Tests/TestSystemInteraction.cs +++ b/tests/Tgstation.Server.Tests/TestSystemInteraction.cs @@ -30,7 +30,7 @@ namespace Tgstation.Server.Tests await using var process = await processExecutor.LaunchProcess("test." + platformIdentifier.ScriptFileExtension, ".", string.Empty, null, true, true); using var cts = new CancellationTokenSource(); cts.CancelAfter(3000); - var exitCode = await process.Lifetime.WithToken(cts.Token); + var exitCode = await process.Lifetime.WaitAsync(cts.Token); Assert.AreEqual(0, exitCode); var result = (await process.GetCombinedOutput(default)).Trim(); @@ -59,7 +59,7 @@ namespace Tgstation.Server.Tests { using var cts = new CancellationTokenSource(); cts.CancelAfter(3000); - var exitCode = await process.Lifetime.WithToken(cts.Token); + var exitCode = await process.Lifetime.WaitAsync(cts.Token); await process.GetCombinedOutput(cts.Token); diff --git a/tests/Tgstation.Server.Tests/TestVersions.cs b/tests/Tgstation.Server.Tests/TestVersions.cs index 8e206913ba..d5d0c6e881 100644 --- a/tests/Tgstation.Server.Tests/TestVersions.cs +++ b/tests/Tgstation.Server.Tests/TestVersions.cs @@ -9,6 +9,7 @@ using System.Xml.Linq; using Tgstation.Server.Api; using Tgstation.Server.Client; +using Tgstation.Server.Common.Extensions; using Tgstation.Server.Host; using Tgstation.Server.Host.Components.Interop; using Tgstation.Server.Host.Configuration; diff --git a/tests/Tgstation.Server.Tests/Tgstation.Server.Tests.csproj b/tests/Tgstation.Server.Tests/Tgstation.Server.Tests.csproj index 7f3dd2a0aa..925be86ff4 100644 --- a/tests/Tgstation.Server.Tests/Tgstation.Server.Tests.csproj +++ b/tests/Tgstation.Server.Tests/Tgstation.Server.Tests.csproj @@ -2,7 +2,7 @@ - $(TgsNetVersion) + $(TgsFrameworkVersion) diff --git a/tgstation-server.sln b/tgstation-server.sln index 8c700d5f29..7dd40adac4 100644 --- a/tgstation-server.sln +++ b/tgstation-server.sln @@ -37,6 +37,7 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "build", "build", "{6FF654E6 build\tgs.ico = build\tgs.ico build\tgs.png = build\tgs.png build\tgstation-server.service = build\tgstation-server.service + build\uac_elevation_manifest.xml = build\uac_elevation_manifest.xml build\Version.props = build\Version.props EndProjectSection EndProject @@ -201,11 +202,15 @@ EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Tgstation.Server.Common", "src\Tgstation.Server.Common\Tgstation.Server.Common.csproj", "{70CD9A98-D31A-44A4-81D1-D02764CEEEFD}" EndProject Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "package", "package", "{2648A85F-61AE-428E-95E1-66D06C7A3768}" + ProjectSection(SolutionItems) = preProject + build\package\appsettings.GitHub.yml = build\package\appsettings.GitHub.yml + build\package\appsettings.Initial.yml = build\package\appsettings.Initial.yml + EndProjectSection EndProject Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "deb", "deb", "{457A1F89-6201-4430-BCC6-2F4438A54B9E}" ProjectSection(SolutionItems) = preProject - build\package\deb\appsettings.Initial.yml = build\package\deb\appsettings.Initial.yml build\package\deb\build_package.sh = build\package\deb\build_package.sh + build\package\deb\install_artifacts.sh = build\package\deb\install_artifacts.sh build\package\deb\MakeInstall = build\package\deb\MakeInstall build\package\deb\tgs-configure = build\package\deb\tgs-configure build\package\deb\wrap_gpg.sh = build\package\deb\wrap_gpg.sh @@ -216,7 +221,7 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "debian", "debian", "{08E7C6 build\package\deb\debian\changelog = build\package\deb\debian\changelog build\package\deb\debian\control = build\package\deb\debian\control build\package\deb\debian\copyright = build\package\deb\debian\copyright - build\package\deb\debian\links = build\package\deb\debian\links + build\package\deb\install_artifacts.sh = build\package\deb\install_artifacts.sh build\package\deb\debian\postinst = build\package\deb\debian\postinst build\package\deb\debian\prerm = build\package\deb\debian\prerm build\package\deb\debian\rules = build\package\deb\debian\rules @@ -227,162 +232,284 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "upstream", "upstream", "{B4 build\package\deb\debian\upstream\metadata = build\package\deb\debian\upstream\metadata EndProjectSection EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "winget", "winget", "{506B9092-AF88-4DA2-84FD-C11646B695B0}" + ProjectSection(SolutionItems) = preProject + build\package\winget\install_banner.jpg = build\package\winget\install_banner.jpg + build\package\winget\prepare_installer_input_artifacts.ps1 = build\package\winget\prepare_installer_input_artifacts.ps1 + build\package\winget\push_manifest.ps1 = build\package\winget\push_manifest.ps1 + EndProjectSection +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "manifest", "manifest", "{3F141E03-6ABB-46A8-AA24-1B4B6814E58F}" + ProjectSection(SolutionItems) = preProject + build\package\winget\manifest\Tgstation.Server.installer.yaml = build\package\winget\manifest\Tgstation.Server.installer.yaml + build\package\winget\manifest\Tgstation.Server.locale.en-US.yaml = build\package\winget\manifest\Tgstation.Server.locale.en-US.yaml + build\package\winget\manifest\Tgstation.Server.yaml = build\package\winget\manifest\Tgstation.Server.yaml + EndProjectSection +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Tgstation.Server.Host.Service.Wix.Extensions", "build\package\winget\Tgstation.Server.Host.Service.Wix.Extensions\Tgstation.Server.Host.Service.Wix.Extensions.csproj", "{28669D98-E15D-4A62-B1D1-7C11D4CB1DE0}" +EndProject +Project("{B7DD6F7E-DEF8-4E67-B5B7-07EF123DB6F0}") = "Tgstation.Server.Host.Service.Wix", "build\package\winget\Tgstation.Server.Host.Service.Wix\Tgstation.Server.Host.Service.Wix.wixproj", "{82996E9D-5E63-4CC5-8179-612C71B4F56C}" +EndProject +Project("{B7DD6F7E-DEF8-4E67-B5B7-07EF123DB6F0}") = "Tgstation.Server.Host.Service.Wix.Bundle", "build\package\winget\Tgstation.Server.Host.Service.Wix.Bundle\Tgstation.Server.Host.Service.Wix.Bundle.wixproj", "{9E66C6F1-E235-4979-83B5-C2B3FDAD6E01}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU - DebugNoService|Any CPU = DebugNoService|Any CPU + DebugNoWindows|Any CPU = DebugNoWindows|Any CPU + DebugNoWix|Any CPU = DebugNoWix|Any CPU Release|Any CPU = Release|Any CPU - ReleaseNoService|Any CPU = ReleaseNoService|Any CPU + ReleaseNoWindows|Any CPU = ReleaseNoWindows|Any CPU + ReleaseNoWix|Any CPU = ReleaseNoWix|Any CPU EndGlobalSection GlobalSection(ProjectConfigurationPlatforms) = postSolution {2B69AD6D-2B5A-4023-8EAD-0BD1B18E028A}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {2B69AD6D-2B5A-4023-8EAD-0BD1B18E028A}.Debug|Any CPU.Build.0 = Debug|Any CPU - {2B69AD6D-2B5A-4023-8EAD-0BD1B18E028A}.DebugNoService|Any CPU.ActiveCfg = Debug|Any CPU - {2B69AD6D-2B5A-4023-8EAD-0BD1B18E028A}.DebugNoService|Any CPU.Build.0 = Debug|Any CPU + {2B69AD6D-2B5A-4023-8EAD-0BD1B18E028A}.DebugNoWindows|Any CPU.ActiveCfg = Debug|Any CPU + {2B69AD6D-2B5A-4023-8EAD-0BD1B18E028A}.DebugNoWindows|Any CPU.Build.0 = Debug|Any CPU + {2B69AD6D-2B5A-4023-8EAD-0BD1B18E028A}.DebugNoWix|Any CPU.ActiveCfg = Debug|Any CPU + {2B69AD6D-2B5A-4023-8EAD-0BD1B18E028A}.DebugNoWix|Any CPU.Build.0 = Debug|Any CPU {2B69AD6D-2B5A-4023-8EAD-0BD1B18E028A}.Release|Any CPU.ActiveCfg = Release|Any CPU {2B69AD6D-2B5A-4023-8EAD-0BD1B18E028A}.Release|Any CPU.Build.0 = Release|Any CPU - {2B69AD6D-2B5A-4023-8EAD-0BD1B18E028A}.ReleaseNoService|Any CPU.ActiveCfg = Release|Any CPU - {2B69AD6D-2B5A-4023-8EAD-0BD1B18E028A}.ReleaseNoService|Any CPU.Build.0 = Release|Any CPU + {2B69AD6D-2B5A-4023-8EAD-0BD1B18E028A}.ReleaseNoWindows|Any CPU.ActiveCfg = Release|Any CPU + {2B69AD6D-2B5A-4023-8EAD-0BD1B18E028A}.ReleaseNoWindows|Any CPU.Build.0 = Release|Any CPU + {2B69AD6D-2B5A-4023-8EAD-0BD1B18E028A}.ReleaseNoWix|Any CPU.ActiveCfg = Release|Any CPU + {2B69AD6D-2B5A-4023-8EAD-0BD1B18E028A}.ReleaseNoWix|Any CPU.Build.0 = Release|Any CPU {A09C947F-4B9B-4AEE-AB50-47055EA05F18}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {A09C947F-4B9B-4AEE-AB50-47055EA05F18}.Debug|Any CPU.Build.0 = Debug|Any CPU - {A09C947F-4B9B-4AEE-AB50-47055EA05F18}.DebugNoService|Any CPU.ActiveCfg = Debug|Any CPU - {A09C947F-4B9B-4AEE-AB50-47055EA05F18}.DebugNoService|Any CPU.Build.0 = Debug|Any CPU + {A09C947F-4B9B-4AEE-AB50-47055EA05F18}.DebugNoWindows|Any CPU.ActiveCfg = Debug|Any CPU + {A09C947F-4B9B-4AEE-AB50-47055EA05F18}.DebugNoWindows|Any CPU.Build.0 = Debug|Any CPU + {A09C947F-4B9B-4AEE-AB50-47055EA05F18}.DebugNoWix|Any CPU.ActiveCfg = Debug|Any CPU + {A09C947F-4B9B-4AEE-AB50-47055EA05F18}.DebugNoWix|Any CPU.Build.0 = Debug|Any CPU {A09C947F-4B9B-4AEE-AB50-47055EA05F18}.Release|Any CPU.ActiveCfg = Release|Any CPU {A09C947F-4B9B-4AEE-AB50-47055EA05F18}.Release|Any CPU.Build.0 = Release|Any CPU - {A09C947F-4B9B-4AEE-AB50-47055EA05F18}.ReleaseNoService|Any CPU.ActiveCfg = Release|Any CPU - {A09C947F-4B9B-4AEE-AB50-47055EA05F18}.ReleaseNoService|Any CPU.Build.0 = Release|Any CPU + {A09C947F-4B9B-4AEE-AB50-47055EA05F18}.ReleaseNoWindows|Any CPU.ActiveCfg = Release|Any CPU + {A09C947F-4B9B-4AEE-AB50-47055EA05F18}.ReleaseNoWindows|Any CPU.Build.0 = Release|Any CPU + {A09C947F-4B9B-4AEE-AB50-47055EA05F18}.ReleaseNoWix|Any CPU.ActiveCfg = Release|Any CPU + {A09C947F-4B9B-4AEE-AB50-47055EA05F18}.ReleaseNoWix|Any CPU.Build.0 = Release|Any CPU {D36B99C4-E771-42D6-A95F-1102B3E236DF}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {D36B99C4-E771-42D6-A95F-1102B3E236DF}.Debug|Any CPU.Build.0 = Debug|Any CPU - {D36B99C4-E771-42D6-A95F-1102B3E236DF}.DebugNoService|Any CPU.ActiveCfg = Debug|Any CPU - {D36B99C4-E771-42D6-A95F-1102B3E236DF}.DebugNoService|Any CPU.Build.0 = Debug|Any CPU + {D36B99C4-E771-42D6-A95F-1102B3E236DF}.DebugNoWindows|Any CPU.ActiveCfg = Debug|Any CPU + {D36B99C4-E771-42D6-A95F-1102B3E236DF}.DebugNoWindows|Any CPU.Build.0 = Debug|Any CPU + {D36B99C4-E771-42D6-A95F-1102B3E236DF}.DebugNoWix|Any CPU.ActiveCfg = Debug|Any CPU + {D36B99C4-E771-42D6-A95F-1102B3E236DF}.DebugNoWix|Any CPU.Build.0 = Debug|Any CPU {D36B99C4-E771-42D6-A95F-1102B3E236DF}.Release|Any CPU.ActiveCfg = Release|Any CPU {D36B99C4-E771-42D6-A95F-1102B3E236DF}.Release|Any CPU.Build.0 = Release|Any CPU - {D36B99C4-E771-42D6-A95F-1102B3E236DF}.ReleaseNoService|Any CPU.ActiveCfg = Release|Any CPU - {D36B99C4-E771-42D6-A95F-1102B3E236DF}.ReleaseNoService|Any CPU.Build.0 = Release|Any CPU + {D36B99C4-E771-42D6-A95F-1102B3E236DF}.ReleaseNoWindows|Any CPU.ActiveCfg = Release|Any CPU + {D36B99C4-E771-42D6-A95F-1102B3E236DF}.ReleaseNoWindows|Any CPU.Build.0 = Release|Any CPU + {D36B99C4-E771-42D6-A95F-1102B3E236DF}.ReleaseNoWix|Any CPU.ActiveCfg = Release|Any CPU + {D36B99C4-E771-42D6-A95F-1102B3E236DF}.ReleaseNoWix|Any CPU.Build.0 = Release|Any CPU {8B4A208D-A48A-4A5D-8B94-E2661138865D}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {8B4A208D-A48A-4A5D-8B94-E2661138865D}.Debug|Any CPU.Build.0 = Debug|Any CPU - {8B4A208D-A48A-4A5D-8B94-E2661138865D}.DebugNoService|Any CPU.ActiveCfg = Debug|Any CPU - {8B4A208D-A48A-4A5D-8B94-E2661138865D}.DebugNoService|Any CPU.Build.0 = Debug|Any CPU + {8B4A208D-A48A-4A5D-8B94-E2661138865D}.DebugNoWindows|Any CPU.ActiveCfg = Debug|Any CPU + {8B4A208D-A48A-4A5D-8B94-E2661138865D}.DebugNoWindows|Any CPU.Build.0 = Debug|Any CPU + {8B4A208D-A48A-4A5D-8B94-E2661138865D}.DebugNoWix|Any CPU.ActiveCfg = Debug|Any CPU + {8B4A208D-A48A-4A5D-8B94-E2661138865D}.DebugNoWix|Any CPU.Build.0 = Debug|Any CPU {8B4A208D-A48A-4A5D-8B94-E2661138865D}.Release|Any CPU.ActiveCfg = Release|Any CPU {8B4A208D-A48A-4A5D-8B94-E2661138865D}.Release|Any CPU.Build.0 = Release|Any CPU - {8B4A208D-A48A-4A5D-8B94-E2661138865D}.ReleaseNoService|Any CPU.ActiveCfg = Release|Any CPU - {8B4A208D-A48A-4A5D-8B94-E2661138865D}.ReleaseNoService|Any CPU.Build.0 = Release|Any CPU + {8B4A208D-A48A-4A5D-8B94-E2661138865D}.ReleaseNoWindows|Any CPU.ActiveCfg = Release|Any CPU + {8B4A208D-A48A-4A5D-8B94-E2661138865D}.ReleaseNoWindows|Any CPU.Build.0 = Release|Any CPU + {8B4A208D-A48A-4A5D-8B94-E2661138865D}.ReleaseNoWix|Any CPU.ActiveCfg = Release|Any CPU + {8B4A208D-A48A-4A5D-8B94-E2661138865D}.ReleaseNoWix|Any CPU.Build.0 = Release|Any CPU {29927416-3B78-49A7-A560-5CCAA638B6B4}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {29927416-3B78-49A7-A560-5CCAA638B6B4}.Debug|Any CPU.Build.0 = Debug|Any CPU - {29927416-3B78-49A7-A560-5CCAA638B6B4}.DebugNoService|Any CPU.ActiveCfg = Debug|Any CPU + {29927416-3B78-49A7-A560-5CCAA638B6B4}.DebugNoWindows|Any CPU.ActiveCfg = Debug|Any CPU + {29927416-3B78-49A7-A560-5CCAA638B6B4}.DebugNoWix|Any CPU.ActiveCfg = Debug|Any CPU + {29927416-3B78-49A7-A560-5CCAA638B6B4}.DebugNoWix|Any CPU.Build.0 = Debug|Any CPU {29927416-3B78-49A7-A560-5CCAA638B6B4}.Release|Any CPU.ActiveCfg = Release|Any CPU {29927416-3B78-49A7-A560-5CCAA638B6B4}.Release|Any CPU.Build.0 = Release|Any CPU - {29927416-3B78-49A7-A560-5CCAA638B6B4}.ReleaseNoService|Any CPU.ActiveCfg = Release|Any CPU + {29927416-3B78-49A7-A560-5CCAA638B6B4}.ReleaseNoWindows|Any CPU.ActiveCfg = Release|Any CPU + {29927416-3B78-49A7-A560-5CCAA638B6B4}.ReleaseNoWix|Any CPU.ActiveCfg = Release|Any CPU + {29927416-3B78-49A7-A560-5CCAA638B6B4}.ReleaseNoWix|Any CPU.Build.0 = Release|Any CPU {E0AC911F-7675-4A91-9499-D8A2E2390AAD}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {E0AC911F-7675-4A91-9499-D8A2E2390AAD}.Debug|Any CPU.Build.0 = Debug|Any CPU - {E0AC911F-7675-4A91-9499-D8A2E2390AAD}.DebugNoService|Any CPU.ActiveCfg = Debug|Any CPU - {E0AC911F-7675-4A91-9499-D8A2E2390AAD}.DebugNoService|Any CPU.Build.0 = Debug|Any CPU + {E0AC911F-7675-4A91-9499-D8A2E2390AAD}.DebugNoWindows|Any CPU.ActiveCfg = Debug|Any CPU + {E0AC911F-7675-4A91-9499-D8A2E2390AAD}.DebugNoWindows|Any CPU.Build.0 = Debug|Any CPU + {E0AC911F-7675-4A91-9499-D8A2E2390AAD}.DebugNoWix|Any CPU.ActiveCfg = Debug|Any CPU + {E0AC911F-7675-4A91-9499-D8A2E2390AAD}.DebugNoWix|Any CPU.Build.0 = Debug|Any CPU {E0AC911F-7675-4A91-9499-D8A2E2390AAD}.Release|Any CPU.ActiveCfg = Release|Any CPU {E0AC911F-7675-4A91-9499-D8A2E2390AAD}.Release|Any CPU.Build.0 = Release|Any CPU - {E0AC911F-7675-4A91-9499-D8A2E2390AAD}.ReleaseNoService|Any CPU.ActiveCfg = Release|Any CPU - {E0AC911F-7675-4A91-9499-D8A2E2390AAD}.ReleaseNoService|Any CPU.Build.0 = Release|Any CPU + {E0AC911F-7675-4A91-9499-D8A2E2390AAD}.ReleaseNoWindows|Any CPU.ActiveCfg = Release|Any CPU + {E0AC911F-7675-4A91-9499-D8A2E2390AAD}.ReleaseNoWindows|Any CPU.Build.0 = Release|Any CPU + {E0AC911F-7675-4A91-9499-D8A2E2390AAD}.ReleaseNoWix|Any CPU.ActiveCfg = Release|Any CPU + {E0AC911F-7675-4A91-9499-D8A2E2390AAD}.ReleaseNoWix|Any CPU.Build.0 = Release|Any CPU {BE0E8F49-334F-49D7-A04D-D82E9AB7AA36}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {BE0E8F49-334F-49D7-A04D-D82E9AB7AA36}.Debug|Any CPU.Build.0 = Debug|Any CPU - {BE0E8F49-334F-49D7-A04D-D82E9AB7AA36}.DebugNoService|Any CPU.ActiveCfg = Debug|Any CPU - {BE0E8F49-334F-49D7-A04D-D82E9AB7AA36}.DebugNoService|Any CPU.Build.0 = Debug|Any CPU + {BE0E8F49-334F-49D7-A04D-D82E9AB7AA36}.DebugNoWindows|Any CPU.ActiveCfg = Debug|Any CPU + {BE0E8F49-334F-49D7-A04D-D82E9AB7AA36}.DebugNoWindows|Any CPU.Build.0 = Debug|Any CPU + {BE0E8F49-334F-49D7-A04D-D82E9AB7AA36}.DebugNoWix|Any CPU.ActiveCfg = Debug|Any CPU + {BE0E8F49-334F-49D7-A04D-D82E9AB7AA36}.DebugNoWix|Any CPU.Build.0 = Debug|Any CPU {BE0E8F49-334F-49D7-A04D-D82E9AB7AA36}.Release|Any CPU.ActiveCfg = Release|Any CPU {BE0E8F49-334F-49D7-A04D-D82E9AB7AA36}.Release|Any CPU.Build.0 = Release|Any CPU - {BE0E8F49-334F-49D7-A04D-D82E9AB7AA36}.ReleaseNoService|Any CPU.ActiveCfg = Release|Any CPU - {BE0E8F49-334F-49D7-A04D-D82E9AB7AA36}.ReleaseNoService|Any CPU.Build.0 = Release|Any CPU + {BE0E8F49-334F-49D7-A04D-D82E9AB7AA36}.ReleaseNoWindows|Any CPU.ActiveCfg = Release|Any CPU + {BE0E8F49-334F-49D7-A04D-D82E9AB7AA36}.ReleaseNoWindows|Any CPU.Build.0 = Release|Any CPU + {BE0E8F49-334F-49D7-A04D-D82E9AB7AA36}.ReleaseNoWix|Any CPU.ActiveCfg = Release|Any CPU + {BE0E8F49-334F-49D7-A04D-D82E9AB7AA36}.ReleaseNoWix|Any CPU.Build.0 = Release|Any CPU {E5301AF1-4F74-4982-BF24-95F23CC5D5B2}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {E5301AF1-4F74-4982-BF24-95F23CC5D5B2}.Debug|Any CPU.Build.0 = Debug|Any CPU - {E5301AF1-4F74-4982-BF24-95F23CC5D5B2}.DebugNoService|Any CPU.ActiveCfg = Debug|Any CPU - {E5301AF1-4F74-4982-BF24-95F23CC5D5B2}.DebugNoService|Any CPU.Build.0 = Debug|Any CPU + {E5301AF1-4F74-4982-BF24-95F23CC5D5B2}.DebugNoWindows|Any CPU.ActiveCfg = Debug|Any CPU + {E5301AF1-4F74-4982-BF24-95F23CC5D5B2}.DebugNoWindows|Any CPU.Build.0 = Debug|Any CPU + {E5301AF1-4F74-4982-BF24-95F23CC5D5B2}.DebugNoWix|Any CPU.ActiveCfg = Debug|Any CPU + {E5301AF1-4F74-4982-BF24-95F23CC5D5B2}.DebugNoWix|Any CPU.Build.0 = Debug|Any CPU {E5301AF1-4F74-4982-BF24-95F23CC5D5B2}.Release|Any CPU.ActiveCfg = Release|Any CPU {E5301AF1-4F74-4982-BF24-95F23CC5D5B2}.Release|Any CPU.Build.0 = Release|Any CPU - {E5301AF1-4F74-4982-BF24-95F23CC5D5B2}.ReleaseNoService|Any CPU.ActiveCfg = Release|Any CPU - {E5301AF1-4F74-4982-BF24-95F23CC5D5B2}.ReleaseNoService|Any CPU.Build.0 = Release|Any CPU + {E5301AF1-4F74-4982-BF24-95F23CC5D5B2}.ReleaseNoWindows|Any CPU.ActiveCfg = Release|Any CPU + {E5301AF1-4F74-4982-BF24-95F23CC5D5B2}.ReleaseNoWindows|Any CPU.Build.0 = Release|Any CPU + {E5301AF1-4F74-4982-BF24-95F23CC5D5B2}.ReleaseNoWix|Any CPU.ActiveCfg = Release|Any CPU + {E5301AF1-4F74-4982-BF24-95F23CC5D5B2}.ReleaseNoWix|Any CPU.Build.0 = Release|Any CPU {A3362FF6-550F-480F-859E-8EC1EB6EAB31}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {A3362FF6-550F-480F-859E-8EC1EB6EAB31}.Debug|Any CPU.Build.0 = Debug|Any CPU - {A3362FF6-550F-480F-859E-8EC1EB6EAB31}.DebugNoService|Any CPU.ActiveCfg = Debug|Any CPU - {A3362FF6-550F-480F-859E-8EC1EB6EAB31}.DebugNoService|Any CPU.Build.0 = Debug|Any CPU + {A3362FF6-550F-480F-859E-8EC1EB6EAB31}.DebugNoWindows|Any CPU.ActiveCfg = Debug|Any CPU + {A3362FF6-550F-480F-859E-8EC1EB6EAB31}.DebugNoWindows|Any CPU.Build.0 = Debug|Any CPU + {A3362FF6-550F-480F-859E-8EC1EB6EAB31}.DebugNoWix|Any CPU.ActiveCfg = Debug|Any CPU + {A3362FF6-550F-480F-859E-8EC1EB6EAB31}.DebugNoWix|Any CPU.Build.0 = Debug|Any CPU {A3362FF6-550F-480F-859E-8EC1EB6EAB31}.Release|Any CPU.ActiveCfg = Release|Any CPU {A3362FF6-550F-480F-859E-8EC1EB6EAB31}.Release|Any CPU.Build.0 = Release|Any CPU - {A3362FF6-550F-480F-859E-8EC1EB6EAB31}.ReleaseNoService|Any CPU.ActiveCfg = Release|Any CPU - {A3362FF6-550F-480F-859E-8EC1EB6EAB31}.ReleaseNoService|Any CPU.Build.0 = Release|Any CPU + {A3362FF6-550F-480F-859E-8EC1EB6EAB31}.ReleaseNoWindows|Any CPU.ActiveCfg = Release|Any CPU + {A3362FF6-550F-480F-859E-8EC1EB6EAB31}.ReleaseNoWindows|Any CPU.Build.0 = Release|Any CPU + {A3362FF6-550F-480F-859E-8EC1EB6EAB31}.ReleaseNoWix|Any CPU.ActiveCfg = Release|Any CPU + {A3362FF6-550F-480F-859E-8EC1EB6EAB31}.ReleaseNoWix|Any CPU.Build.0 = Release|Any CPU {5D2D682C-6BF0-439C-850B-6AB945BBEAEA}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {5D2D682C-6BF0-439C-850B-6AB945BBEAEA}.Debug|Any CPU.Build.0 = Debug|Any CPU - {5D2D682C-6BF0-439C-850B-6AB945BBEAEA}.DebugNoService|Any CPU.ActiveCfg = Debug|Any CPU - {5D2D682C-6BF0-439C-850B-6AB945BBEAEA}.DebugNoService|Any CPU.Build.0 = Debug|Any CPU + {5D2D682C-6BF0-439C-850B-6AB945BBEAEA}.DebugNoWindows|Any CPU.ActiveCfg = Debug|Any CPU + {5D2D682C-6BF0-439C-850B-6AB945BBEAEA}.DebugNoWindows|Any CPU.Build.0 = Debug|Any CPU + {5D2D682C-6BF0-439C-850B-6AB945BBEAEA}.DebugNoWix|Any CPU.ActiveCfg = Debug|Any CPU + {5D2D682C-6BF0-439C-850B-6AB945BBEAEA}.DebugNoWix|Any CPU.Build.0 = Debug|Any CPU {5D2D682C-6BF0-439C-850B-6AB945BBEAEA}.Release|Any CPU.ActiveCfg = Release|Any CPU {5D2D682C-6BF0-439C-850B-6AB945BBEAEA}.Release|Any CPU.Build.0 = Release|Any CPU - {5D2D682C-6BF0-439C-850B-6AB945BBEAEA}.ReleaseNoService|Any CPU.ActiveCfg = Release|Any CPU - {5D2D682C-6BF0-439C-850B-6AB945BBEAEA}.ReleaseNoService|Any CPU.Build.0 = Release|Any CPU + {5D2D682C-6BF0-439C-850B-6AB945BBEAEA}.ReleaseNoWindows|Any CPU.ActiveCfg = Release|Any CPU + {5D2D682C-6BF0-439C-850B-6AB945BBEAEA}.ReleaseNoWindows|Any CPU.Build.0 = Release|Any CPU + {5D2D682C-6BF0-439C-850B-6AB945BBEAEA}.ReleaseNoWix|Any CPU.ActiveCfg = Release|Any CPU + {5D2D682C-6BF0-439C-850B-6AB945BBEAEA}.ReleaseNoWix|Any CPU.Build.0 = Release|Any CPU {7500F776-4384-4B5F-A8D8-22461CAD108B}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {7500F776-4384-4B5F-A8D8-22461CAD108B}.Debug|Any CPU.Build.0 = Debug|Any CPU - {7500F776-4384-4B5F-A8D8-22461CAD108B}.DebugNoService|Any CPU.ActiveCfg = Debug|Any CPU - {7500F776-4384-4B5F-A8D8-22461CAD108B}.DebugNoService|Any CPU.Build.0 = Debug|Any CPU + {7500F776-4384-4B5F-A8D8-22461CAD108B}.DebugNoWindows|Any CPU.ActiveCfg = Debug|Any CPU + {7500F776-4384-4B5F-A8D8-22461CAD108B}.DebugNoWindows|Any CPU.Build.0 = Debug|Any CPU + {7500F776-4384-4B5F-A8D8-22461CAD108B}.DebugNoWix|Any CPU.ActiveCfg = Debug|Any CPU + {7500F776-4384-4B5F-A8D8-22461CAD108B}.DebugNoWix|Any CPU.Build.0 = Debug|Any CPU {7500F776-4384-4B5F-A8D8-22461CAD108B}.Release|Any CPU.ActiveCfg = Release|Any CPU {7500F776-4384-4B5F-A8D8-22461CAD108B}.Release|Any CPU.Build.0 = Release|Any CPU - {7500F776-4384-4B5F-A8D8-22461CAD108B}.ReleaseNoService|Any CPU.ActiveCfg = Release|Any CPU - {7500F776-4384-4B5F-A8D8-22461CAD108B}.ReleaseNoService|Any CPU.Build.0 = Release|Any CPU + {7500F776-4384-4B5F-A8D8-22461CAD108B}.ReleaseNoWindows|Any CPU.ActiveCfg = Release|Any CPU + {7500F776-4384-4B5F-A8D8-22461CAD108B}.ReleaseNoWindows|Any CPU.Build.0 = Release|Any CPU + {7500F776-4384-4B5F-A8D8-22461CAD108B}.ReleaseNoWix|Any CPU.ActiveCfg = Release|Any CPU + {7500F776-4384-4B5F-A8D8-22461CAD108B}.ReleaseNoWix|Any CPU.Build.0 = Release|Any CPU {AA80A190-52E2-4BE3-BFEB-1F148D9E9007}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {AA80A190-52E2-4BE3-BFEB-1F148D9E9007}.Debug|Any CPU.Build.0 = Debug|Any CPU - {AA80A190-52E2-4BE3-BFEB-1F148D9E9007}.DebugNoService|Any CPU.ActiveCfg = Debug|Any CPU + {AA80A190-52E2-4BE3-BFEB-1F148D9E9007}.DebugNoWindows|Any CPU.ActiveCfg = Debug|Any CPU + {AA80A190-52E2-4BE3-BFEB-1F148D9E9007}.DebugNoWix|Any CPU.ActiveCfg = Debug|Any CPU + {AA80A190-52E2-4BE3-BFEB-1F148D9E9007}.DebugNoWix|Any CPU.Build.0 = Debug|Any CPU {AA80A190-52E2-4BE3-BFEB-1F148D9E9007}.Release|Any CPU.ActiveCfg = Release|Any CPU {AA80A190-52E2-4BE3-BFEB-1F148D9E9007}.Release|Any CPU.Build.0 = Release|Any CPU - {AA80A190-52E2-4BE3-BFEB-1F148D9E9007}.ReleaseNoService|Any CPU.ActiveCfg = Release|Any CPU + {AA80A190-52E2-4BE3-BFEB-1F148D9E9007}.ReleaseNoWindows|Any CPU.ActiveCfg = Release|Any CPU + {AA80A190-52E2-4BE3-BFEB-1F148D9E9007}.ReleaseNoWix|Any CPU.ActiveCfg = Release|Any CPU + {AA80A190-52E2-4BE3-BFEB-1F148D9E9007}.ReleaseNoWix|Any CPU.Build.0 = Release|Any CPU {09056964-1C74-445A-96EC-33F6DFC07916}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {09056964-1C74-445A-96EC-33F6DFC07916}.Debug|Any CPU.Build.0 = Debug|Any CPU - {09056964-1C74-445A-96EC-33F6DFC07916}.DebugNoService|Any CPU.ActiveCfg = Debug|Any CPU - {09056964-1C74-445A-96EC-33F6DFC07916}.DebugNoService|Any CPU.Build.0 = Debug|Any CPU + {09056964-1C74-445A-96EC-33F6DFC07916}.DebugNoWindows|Any CPU.ActiveCfg = Debug|Any CPU + {09056964-1C74-445A-96EC-33F6DFC07916}.DebugNoWindows|Any CPU.Build.0 = Debug|Any CPU + {09056964-1C74-445A-96EC-33F6DFC07916}.DebugNoWix|Any CPU.ActiveCfg = Debug|Any CPU + {09056964-1C74-445A-96EC-33F6DFC07916}.DebugNoWix|Any CPU.Build.0 = Debug|Any CPU {09056964-1C74-445A-96EC-33F6DFC07916}.Release|Any CPU.ActiveCfg = Release|Any CPU {09056964-1C74-445A-96EC-33F6DFC07916}.Release|Any CPU.Build.0 = Release|Any CPU - {09056964-1C74-445A-96EC-33F6DFC07916}.ReleaseNoService|Any CPU.ActiveCfg = Release|Any CPU - {09056964-1C74-445A-96EC-33F6DFC07916}.ReleaseNoService|Any CPU.Build.0 = Release|Any CPU + {09056964-1C74-445A-96EC-33F6DFC07916}.ReleaseNoWindows|Any CPU.ActiveCfg = Release|Any CPU + {09056964-1C74-445A-96EC-33F6DFC07916}.ReleaseNoWindows|Any CPU.Build.0 = Release|Any CPU + {09056964-1C74-445A-96EC-33F6DFC07916}.ReleaseNoWix|Any CPU.ActiveCfg = Release|Any CPU + {09056964-1C74-445A-96EC-33F6DFC07916}.ReleaseNoWix|Any CPU.Build.0 = Release|Any CPU {5CB51532-55F0-4255-B6E5-69ED5CCD14CD}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {5CB51532-55F0-4255-B6E5-69ED5CCD14CD}.Debug|Any CPU.Build.0 = Debug|Any CPU - {5CB51532-55F0-4255-B6E5-69ED5CCD14CD}.DebugNoService|Any CPU.ActiveCfg = Debug|Any CPU - {5CB51532-55F0-4255-B6E5-69ED5CCD14CD}.DebugNoService|Any CPU.Build.0 = Debug|Any CPU + {5CB51532-55F0-4255-B6E5-69ED5CCD14CD}.DebugNoWindows|Any CPU.ActiveCfg = Debug|Any CPU + {5CB51532-55F0-4255-B6E5-69ED5CCD14CD}.DebugNoWindows|Any CPU.Build.0 = Debug|Any CPU + {5CB51532-55F0-4255-B6E5-69ED5CCD14CD}.DebugNoWix|Any CPU.ActiveCfg = Debug|Any CPU + {5CB51532-55F0-4255-B6E5-69ED5CCD14CD}.DebugNoWix|Any CPU.Build.0 = Debug|Any CPU {5CB51532-55F0-4255-B6E5-69ED5CCD14CD}.Release|Any CPU.ActiveCfg = Release|Any CPU {5CB51532-55F0-4255-B6E5-69ED5CCD14CD}.Release|Any CPU.Build.0 = Release|Any CPU - {5CB51532-55F0-4255-B6E5-69ED5CCD14CD}.ReleaseNoService|Any CPU.ActiveCfg = Release|Any CPU - {5CB51532-55F0-4255-B6E5-69ED5CCD14CD}.ReleaseNoService|Any CPU.Build.0 = Release|Any CPU + {5CB51532-55F0-4255-B6E5-69ED5CCD14CD}.ReleaseNoWindows|Any CPU.ActiveCfg = Release|Any CPU + {5CB51532-55F0-4255-B6E5-69ED5CCD14CD}.ReleaseNoWindows|Any CPU.Build.0 = Release|Any CPU + {5CB51532-55F0-4255-B6E5-69ED5CCD14CD}.ReleaseNoWix|Any CPU.ActiveCfg = Release|Any CPU + {5CB51532-55F0-4255-B6E5-69ED5CCD14CD}.ReleaseNoWix|Any CPU.Build.0 = Release|Any CPU {5813CC33-B16C-485D-A74D-20204DDF6542}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {5813CC33-B16C-485D-A74D-20204DDF6542}.Debug|Any CPU.Build.0 = Debug|Any CPU - {5813CC33-B16C-485D-A74D-20204DDF6542}.DebugNoService|Any CPU.ActiveCfg = Debug|Any CPU - {5813CC33-B16C-485D-A74D-20204DDF6542}.DebugNoService|Any CPU.Build.0 = Debug|Any CPU + {5813CC33-B16C-485D-A74D-20204DDF6542}.DebugNoWindows|Any CPU.ActiveCfg = Debug|Any CPU + {5813CC33-B16C-485D-A74D-20204DDF6542}.DebugNoWindows|Any CPU.Build.0 = Debug|Any CPU + {5813CC33-B16C-485D-A74D-20204DDF6542}.DebugNoWix|Any CPU.ActiveCfg = Debug|Any CPU + {5813CC33-B16C-485D-A74D-20204DDF6542}.DebugNoWix|Any CPU.Build.0 = Debug|Any CPU {5813CC33-B16C-485D-A74D-20204DDF6542}.Release|Any CPU.ActiveCfg = Release|Any CPU {5813CC33-B16C-485D-A74D-20204DDF6542}.Release|Any CPU.Build.0 = Release|Any CPU - {5813CC33-B16C-485D-A74D-20204DDF6542}.ReleaseNoService|Any CPU.ActiveCfg = Release|Any CPU - {5813CC33-B16C-485D-A74D-20204DDF6542}.ReleaseNoService|Any CPU.Build.0 = Release|Any CPU + {5813CC33-B16C-485D-A74D-20204DDF6542}.ReleaseNoWindows|Any CPU.ActiveCfg = Release|Any CPU + {5813CC33-B16C-485D-A74D-20204DDF6542}.ReleaseNoWindows|Any CPU.Build.0 = Release|Any CPU + {5813CC33-B16C-485D-A74D-20204DDF6542}.ReleaseNoWix|Any CPU.ActiveCfg = Release|Any CPU + {5813CC33-B16C-485D-A74D-20204DDF6542}.ReleaseNoWix|Any CPU.Build.0 = Release|Any CPU {CE499888-B22B-457C-891E-0EA9DC317228}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {CE499888-B22B-457C-891E-0EA9DC317228}.Debug|Any CPU.Build.0 = Debug|Any CPU - {CE499888-B22B-457C-891E-0EA9DC317228}.DebugNoService|Any CPU.ActiveCfg = Debug|Any CPU - {CE499888-B22B-457C-891E-0EA9DC317228}.DebugNoService|Any CPU.Build.0 = Debug|Any CPU + {CE499888-B22B-457C-891E-0EA9DC317228}.DebugNoWindows|Any CPU.ActiveCfg = Debug|Any CPU + {CE499888-B22B-457C-891E-0EA9DC317228}.DebugNoWindows|Any CPU.Build.0 = Debug|Any CPU + {CE499888-B22B-457C-891E-0EA9DC317228}.DebugNoWix|Any CPU.ActiveCfg = Debug|Any CPU + {CE499888-B22B-457C-891E-0EA9DC317228}.DebugNoWix|Any CPU.Build.0 = Debug|Any CPU {CE499888-B22B-457C-891E-0EA9DC317228}.Release|Any CPU.ActiveCfg = Release|Any CPU {CE499888-B22B-457C-891E-0EA9DC317228}.Release|Any CPU.Build.0 = Release|Any CPU - {CE499888-B22B-457C-891E-0EA9DC317228}.ReleaseNoService|Any CPU.ActiveCfg = Release|Any CPU - {CE499888-B22B-457C-891E-0EA9DC317228}.ReleaseNoService|Any CPU.Build.0 = Release|Any CPU + {CE499888-B22B-457C-891E-0EA9DC317228}.ReleaseNoWindows|Any CPU.ActiveCfg = Release|Any CPU + {CE499888-B22B-457C-891E-0EA9DC317228}.ReleaseNoWindows|Any CPU.Build.0 = Release|Any CPU + {CE499888-B22B-457C-891E-0EA9DC317228}.ReleaseNoWix|Any CPU.ActiveCfg = Release|Any CPU + {CE499888-B22B-457C-891E-0EA9DC317228}.ReleaseNoWix|Any CPU.Build.0 = Release|Any CPU {07ED0FD5-E46B-4841-931D-BA2B673E16B2}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {07ED0FD5-E46B-4841-931D-BA2B673E16B2}.Debug|Any CPU.Build.0 = Debug|Any CPU - {07ED0FD5-E46B-4841-931D-BA2B673E16B2}.DebugNoService|Any CPU.ActiveCfg = Debug|Any CPU - {07ED0FD5-E46B-4841-931D-BA2B673E16B2}.DebugNoService|Any CPU.Build.0 = Debug|Any CPU + {07ED0FD5-E46B-4841-931D-BA2B673E16B2}.DebugNoWindows|Any CPU.ActiveCfg = Debug|Any CPU + {07ED0FD5-E46B-4841-931D-BA2B673E16B2}.DebugNoWindows|Any CPU.Build.0 = Debug|Any CPU + {07ED0FD5-E46B-4841-931D-BA2B673E16B2}.DebugNoWix|Any CPU.ActiveCfg = Debug|Any CPU + {07ED0FD5-E46B-4841-931D-BA2B673E16B2}.DebugNoWix|Any CPU.Build.0 = Debug|Any CPU {07ED0FD5-E46B-4841-931D-BA2B673E16B2}.Release|Any CPU.ActiveCfg = Release|Any CPU {07ED0FD5-E46B-4841-931D-BA2B673E16B2}.Release|Any CPU.Build.0 = Release|Any CPU - {07ED0FD5-E46B-4841-931D-BA2B673E16B2}.ReleaseNoService|Any CPU.ActiveCfg = Release|Any CPU - {07ED0FD5-E46B-4841-931D-BA2B673E16B2}.ReleaseNoService|Any CPU.Build.0 = Release|Any CPU + {07ED0FD5-E46B-4841-931D-BA2B673E16B2}.ReleaseNoWindows|Any CPU.ActiveCfg = Release|Any CPU + {07ED0FD5-E46B-4841-931D-BA2B673E16B2}.ReleaseNoWindows|Any CPU.Build.0 = Release|Any CPU + {07ED0FD5-E46B-4841-931D-BA2B673E16B2}.ReleaseNoWix|Any CPU.ActiveCfg = Release|Any CPU + {07ED0FD5-E46B-4841-931D-BA2B673E16B2}.ReleaseNoWix|Any CPU.Build.0 = Release|Any CPU {CF3968A0-EA81-4464-B2D4-C7D40F6B5BCB}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {CF3968A0-EA81-4464-B2D4-C7D40F6B5BCB}.Debug|Any CPU.Build.0 = Debug|Any CPU - {CF3968A0-EA81-4464-B2D4-C7D40F6B5BCB}.DebugNoService|Any CPU.ActiveCfg = Debug|Any CPU - {CF3968A0-EA81-4464-B2D4-C7D40F6B5BCB}.DebugNoService|Any CPU.Build.0 = Debug|Any CPU + {CF3968A0-EA81-4464-B2D4-C7D40F6B5BCB}.DebugNoWindows|Any CPU.ActiveCfg = Debug|Any CPU + {CF3968A0-EA81-4464-B2D4-C7D40F6B5BCB}.DebugNoWindows|Any CPU.Build.0 = Debug|Any CPU + {CF3968A0-EA81-4464-B2D4-C7D40F6B5BCB}.DebugNoWix|Any CPU.ActiveCfg = Debug|Any CPU + {CF3968A0-EA81-4464-B2D4-C7D40F6B5BCB}.DebugNoWix|Any CPU.Build.0 = Debug|Any CPU {CF3968A0-EA81-4464-B2D4-C7D40F6B5BCB}.Release|Any CPU.ActiveCfg = Release|Any CPU {CF3968A0-EA81-4464-B2D4-C7D40F6B5BCB}.Release|Any CPU.Build.0 = Release|Any CPU - {CF3968A0-EA81-4464-B2D4-C7D40F6B5BCB}.ReleaseNoService|Any CPU.ActiveCfg = Release|Any CPU - {CF3968A0-EA81-4464-B2D4-C7D40F6B5BCB}.ReleaseNoService|Any CPU.Build.0 = Release|Any CPU + {CF3968A0-EA81-4464-B2D4-C7D40F6B5BCB}.ReleaseNoWindows|Any CPU.ActiveCfg = Release|Any CPU + {CF3968A0-EA81-4464-B2D4-C7D40F6B5BCB}.ReleaseNoWindows|Any CPU.Build.0 = Release|Any CPU + {CF3968A0-EA81-4464-B2D4-C7D40F6B5BCB}.ReleaseNoWix|Any CPU.ActiveCfg = Release|Any CPU + {CF3968A0-EA81-4464-B2D4-C7D40F6B5BCB}.ReleaseNoWix|Any CPU.Build.0 = Release|Any CPU {70CD9A98-D31A-44A4-81D1-D02764CEEEFD}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {70CD9A98-D31A-44A4-81D1-D02764CEEEFD}.Debug|Any CPU.Build.0 = Debug|Any CPU - {70CD9A98-D31A-44A4-81D1-D02764CEEEFD}.DebugNoService|Any CPU.ActiveCfg = Debug|Any CPU - {70CD9A98-D31A-44A4-81D1-D02764CEEEFD}.DebugNoService|Any CPU.Build.0 = Debug|Any CPU + {70CD9A98-D31A-44A4-81D1-D02764CEEEFD}.DebugNoWindows|Any CPU.ActiveCfg = Debug|Any CPU + {70CD9A98-D31A-44A4-81D1-D02764CEEEFD}.DebugNoWindows|Any CPU.Build.0 = Debug|Any CPU + {70CD9A98-D31A-44A4-81D1-D02764CEEEFD}.DebugNoWix|Any CPU.ActiveCfg = Debug|Any CPU + {70CD9A98-D31A-44A4-81D1-D02764CEEEFD}.DebugNoWix|Any CPU.Build.0 = Debug|Any CPU {70CD9A98-D31A-44A4-81D1-D02764CEEEFD}.Release|Any CPU.ActiveCfg = Release|Any CPU {70CD9A98-D31A-44A4-81D1-D02764CEEEFD}.Release|Any CPU.Build.0 = Release|Any CPU - {70CD9A98-D31A-44A4-81D1-D02764CEEEFD}.ReleaseNoService|Any CPU.ActiveCfg = Release|Any CPU - {70CD9A98-D31A-44A4-81D1-D02764CEEEFD}.ReleaseNoService|Any CPU.Build.0 = Release|Any CPU + {70CD9A98-D31A-44A4-81D1-D02764CEEEFD}.ReleaseNoWindows|Any CPU.ActiveCfg = Release|Any CPU + {70CD9A98-D31A-44A4-81D1-D02764CEEEFD}.ReleaseNoWindows|Any CPU.Build.0 = Release|Any CPU + {70CD9A98-D31A-44A4-81D1-D02764CEEEFD}.ReleaseNoWix|Any CPU.ActiveCfg = Release|Any CPU + {70CD9A98-D31A-44A4-81D1-D02764CEEEFD}.ReleaseNoWix|Any CPU.Build.0 = Release|Any CPU + {28669D98-E15D-4A62-B1D1-7C11D4CB1DE0}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {28669D98-E15D-4A62-B1D1-7C11D4CB1DE0}.Debug|Any CPU.Build.0 = Debug|Any CPU + {28669D98-E15D-4A62-B1D1-7C11D4CB1DE0}.DebugNoWindows|Any CPU.ActiveCfg = Debug|Any CPU + {28669D98-E15D-4A62-B1D1-7C11D4CB1DE0}.DebugNoWix|Any CPU.ActiveCfg = Debug|Any CPU + {28669D98-E15D-4A62-B1D1-7C11D4CB1DE0}.Release|Any CPU.ActiveCfg = Release|Any CPU + {28669D98-E15D-4A62-B1D1-7C11D4CB1DE0}.Release|Any CPU.Build.0 = Release|Any CPU + {28669D98-E15D-4A62-B1D1-7C11D4CB1DE0}.ReleaseNoWindows|Any CPU.ActiveCfg = Release|Any CPU + {28669D98-E15D-4A62-B1D1-7C11D4CB1DE0}.ReleaseNoWix|Any CPU.ActiveCfg = Release|Any CPU + {82996E9D-5E63-4CC5-8179-612C71B4F56C}.Debug|Any CPU.ActiveCfg = Debug|x86 + {82996E9D-5E63-4CC5-8179-612C71B4F56C}.Debug|Any CPU.Build.0 = Debug|x86 + {82996E9D-5E63-4CC5-8179-612C71B4F56C}.DebugNoWindows|Any CPU.ActiveCfg = Debug|x86 + {82996E9D-5E63-4CC5-8179-612C71B4F56C}.DebugNoWix|Any CPU.ActiveCfg = Debug|x86 + {82996E9D-5E63-4CC5-8179-612C71B4F56C}.Release|Any CPU.ActiveCfg = Release|x86 + {82996E9D-5E63-4CC5-8179-612C71B4F56C}.Release|Any CPU.Build.0 = Release|x86 + {82996E9D-5E63-4CC5-8179-612C71B4F56C}.ReleaseNoWindows|Any CPU.ActiveCfg = Release|x86 + {82996E9D-5E63-4CC5-8179-612C71B4F56C}.ReleaseNoWix|Any CPU.ActiveCfg = Release|x86 + {9E66C6F1-E235-4979-83B5-C2B3FDAD6E01}.Debug|Any CPU.ActiveCfg = Debug|x86 + {9E66C6F1-E235-4979-83B5-C2B3FDAD6E01}.Debug|Any CPU.Build.0 = Debug|x86 + {9E66C6F1-E235-4979-83B5-C2B3FDAD6E01}.DebugNoWindows|Any CPU.ActiveCfg = Debug|x86 + {9E66C6F1-E235-4979-83B5-C2B3FDAD6E01}.DebugNoWix|Any CPU.ActiveCfg = Debug|x86 + {9E66C6F1-E235-4979-83B5-C2B3FDAD6E01}.Release|Any CPU.ActiveCfg = Release|x86 + {9E66C6F1-E235-4979-83B5-C2B3FDAD6E01}.Release|Any CPU.Build.0 = Release|x86 + {9E66C6F1-E235-4979-83B5-C2B3FDAD6E01}.ReleaseNoWindows|Any CPU.ActiveCfg = Release|x86 + {9E66C6F1-E235-4979-83B5-C2B3FDAD6E01}.ReleaseNoWix|Any CPU.ActiveCfg = Release|x86 EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE @@ -415,6 +542,11 @@ Global {457A1F89-6201-4430-BCC6-2F4438A54B9E} = {2648A85F-61AE-428E-95E1-66D06C7A3768} {08E7C650-A447-4DE2-974E-ED123B50F8D6} = {457A1F89-6201-4430-BCC6-2F4438A54B9E} {B4B5570C-8071-46DE-BB55-64C13480C606} = {08E7C650-A447-4DE2-974E-ED123B50F8D6} + {506B9092-AF88-4DA2-84FD-C11646B695B0} = {2648A85F-61AE-428E-95E1-66D06C7A3768} + {3F141E03-6ABB-46A8-AA24-1B4B6814E58F} = {506B9092-AF88-4DA2-84FD-C11646B695B0} + {28669D98-E15D-4A62-B1D1-7C11D4CB1DE0} = {506B9092-AF88-4DA2-84FD-C11646B695B0} + {82996E9D-5E63-4CC5-8179-612C71B4F56C} = {506B9092-AF88-4DA2-84FD-C11646B695B0} + {9E66C6F1-E235-4979-83B5-C2B3FDAD6E01} = {506B9092-AF88-4DA2-84FD-C11646B695B0} EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {DFD36C95-3E49-41C7-ACDB-86BAF5B18A79} diff --git a/tools/ReleaseNotes/Program.cs b/tools/ReleaseNotes/Program.cs index 7a6d8cc000..94a58bdb58 100644 --- a/tools/ReleaseNotes/Program.cs +++ b/tools/ReleaseNotes/Program.cs @@ -31,9 +31,11 @@ namespace ReleaseNotes } var versionString = args[0]; - var ensureRelease = versionString == "--ensure-release"; + var ensureRelease = versionString.Equals("--ensure-release", StringComparison.OrdinalIgnoreCase); + var linkWinget = versionString.Equals("--link-winget", StringComparison.OrdinalIgnoreCase); + var shaCheck = versionString.Equals("--winget-template-check", StringComparison.OrdinalIgnoreCase); - if ((!Version.TryParse(versionString, out var version) || version.Revision != -1) && !ensureRelease) + if ((!Version.TryParse(versionString, out var version) || version.Revision != -1) && !ensureRelease && !linkWinget && !shaCheck) { Console.WriteLine("Invalid version: " + versionString); return 2; @@ -60,6 +62,28 @@ namespace ReleaseNotes if (ensureRelease) return await EnsureRelease(client); + if (linkWinget) + { + if (args.Length < 2 || !Uri.TryCreate(args[1], new UriCreationOptions(), out var actionsUrl)) + { + Console.WriteLine("Missing/Invalid actions URL!"); + return 30; + } + + return await Winget(client, actionsUrl, null); + } + + if (shaCheck) + { + if(args.Length < 2) + { + Console.WriteLine("Missing SHA for PR template!"); + return 32; + } + + return await Winget(client, null, args[1]); + } + var releasesTask = client.Repository.Release.GetAll(RepoOwner, RepoName); Console.WriteLine("Getting merged pull requests in milestone " + versionString + "..."); @@ -479,5 +503,74 @@ namespace ReleaseNotes return 0; } + + static async Task Winget(IGitHubClient client, Uri actionUrl, string expectedTemplateSha) + { + const string PropsPath = "build/Version.props"; + + var doc = XDocument.Load(PropsPath); + var project = doc.Root; + var xmlNamespace = project.GetDefaultNamespace(); + var versionsPropertyGroup = project.Elements().First(x => x.Name == xmlNamespace + "PropertyGroup"); + var coreVersion = Version.Parse(versionsPropertyGroup.Element(xmlNamespace + "TgsCoreVersion").Value); + + const string BodyForPRSha = "596da68f8da0926ae17a1497328e368d7b83aac2"; + var prBody = $@"# Automated Pull Request + +This pull request was generated by our [deployment pipeline]({actionUrl}) as a result of the release of [tgstation-server-v{coreVersion}](https://github.com/tgstation/tgstation-server/releases/tag/tgstation-server-v{coreVersion}). Validation was performed as part of the process. + +The user account that created this pull request is available to correct any issues. + +**_We would like to be verified as the publisher of this software but we cannot find documentation on how to do so._** + +- [x] Have you signed the [Contributor License Agreement](https://cla.opensource.microsoft.com/microsoft/winget-pkgs)? +- [x] Have you checked that there aren't other open [pull requests](https://github.com/microsoft/winget-pkgs/pulls) for the same manifest update/change? + - This PR is generated as a direct result of a new release of `tgstation-server` this should be impossible +- [x] This PR only modifies one (1) manifest +- [x] Have you [validated](https://github.com/microsoft/winget-pkgs/blob/master/AUTHORING_MANIFESTS.md#validation) your manifest locally with `winget validate --manifest `? + - Validation is performed as a prerequisite to deployment. +- [x] Have you tested your manifest locally with `winget install --manifest `? + - Manifest installation and uninstallation is performed as a prerequisite to deployment. +- [x] Does your manifest conform to the [1.4 schema](https://github.com/microsoft/winget-pkgs/tree/master/doc/manifest/schema/1.4.0)?"; + + if (expectedTemplateSha != null) + { + if (expectedTemplateSha != BodyForPRSha) + { + Console.WriteLine("winget-pkgs pull request template has updated. This tool will need to be updated to match!"); + Console.WriteLine($"Expected {BodyForPRSha} found {expectedTemplateSha}"); + return 33; + } + + return 0; + } + + var clientUser = await client.User.Current(); + + var userPrsOnWingetRepo = await client.Search.SearchIssues(new SearchIssuesRequest + { + Author = clientUser.Login, + Is = new List { IssueIsQualifier.PullRequest }, + State = ItemState.Open, + Repos = new RepositoryCollection + { + { "microsoft", "winget-pkgs" }, + }, + }); + + var prToModify = userPrsOnWingetRepo.Items.OrderByDescending(pr => pr.Number).FirstOrDefault(); + if(prToModify == null) + { + Console.WriteLine("Could not find open winget-pkgs PR!"); + return 31; + } + + await client.Issue.Update("microsoft", "winget-pkgs", prToModify.Number, new IssueUpdate + { + Body = prBody, + }); + + return 0; + } } } diff --git a/tools/ReleaseNotes/README.md b/tools/ReleaseNotes/README.md index 63aa900ef3..2a71aad6e0 100644 --- a/tools/ReleaseNotes/README.md +++ b/tools/ReleaseNotes/README.md @@ -1,7 +1,21 @@ -This is a small tool to generate TGS release notes from PR descriptions +This is a small tool to automate generating TGS releases Requires environment variable `TGS_RELEASE_NOTES_TOKEN` Run it with `dotnet run [--no-close (optionally doesn't close the milestone, USE WHILE DEBUGGING)]` Will close the release milestone and output `release_notes.md` with the updated release notes + +Alternative modes + +`dotnet run --ensure-release` + +Ensures the latest GitHub release is a TGS release + +`dotnet run --link-winget ` + +Updates an existing https://github.com/microsoft/winget-pkgs manifest update pull request with the TGS template. The PR updated is the last one opened by the user + +`dotnet run --winget-template-check ` + +Validates the template we are PRing is up-to-date. diff --git a/tools/ReleaseNotes/ReleaseNotes.csproj b/tools/ReleaseNotes/ReleaseNotes.csproj index d2a5370e27..2f2e6ab506 100644 --- a/tools/ReleaseNotes/ReleaseNotes.csproj +++ b/tools/ReleaseNotes/ReleaseNotes.csproj @@ -3,7 +3,7 @@ Exe - $(TgsNetVersion) + $(TgsFrameworkVersion) diff --git a/tools/Tgstation.Server.Migrator.Comms/Program.cs b/tools/Tgstation.Server.Migrator.Comms/Program.cs index cb8a68cfe3..8cebd477c7 100644 --- a/tools/Tgstation.Server.Migrator.Comms/Program.cs +++ b/tools/Tgstation.Server.Migrator.Comms/Program.cs @@ -15,6 +15,7 @@ using Tgstation.Server.Api.Models; using Tgstation.Server.Api.Models.Internal; using Tgstation.Server.Api.Models.Request; using Tgstation.Server.Client; +using Tgstation.Server.Common.Extensions; static class Program { diff --git a/tools/Tgstation.Server.Migrator/Program.cs b/tools/Tgstation.Server.Migrator/Program.cs index 5f7fb006c1..3332b5b52e 100644 --- a/tools/Tgstation.Server.Migrator/Program.cs +++ b/tools/Tgstation.Server.Migrator/Program.cs @@ -21,7 +21,9 @@ using Octokit; using Tgstation.Server.Api; using Tgstation.Server.Client; +using Tgstation.Server.Common.Extensions; using Tgstation.Server.Common.Http; +using Tgstation.Server.Host.Common; using Tgstation.Server.Host.IO; using Tgstation.Server.Host.Setup; @@ -121,7 +123,7 @@ try Console.WriteLine("Checking for TGS3 service..."); const string OldServiceName = "TG Station Server"; - const string NewServiceName = "tgstation-server"; + const string NewServiceName = Constants.CanonicalPackageName; static ServiceController GetTgs3Service(bool checkNewOneIsntInstalled) { @@ -153,7 +155,7 @@ try return tgs3Service; } - var tgs3Service = GetTgs3Service(true); + using var tgs3Service = GetTgs3Service(true); if (tgs3Service.Status != ServiceControllerStatus.Running) { @@ -225,8 +227,8 @@ try Directory.CreateDirectory(tgsInstallPath); // ASP.NET 6.0 RUNTIME CHECK - Console.WriteLine("Next step, we need to ensure the .NET 4.7.2 and ASP.NET Core 6 runtimes are installed on your machine."); - Console.WriteLine("We are assuming you already have .NET 4.7.2 installed if you're running TGS3 and this program. So we're going to download .NET 6 for you."); + Console.WriteLine("Next step, we need to ensure the ASP.NET Core 6 runtime is installed on your machine."); + Console.WriteLine("We're going to download it for you."); Console.WriteLine("Yes, this program runs .NET 6, but it contains the entire runtime embedded into it. You will need a system-wide install for TGS."); var runtimeInstalled = true; // assume for now @@ -439,7 +441,7 @@ try installer.DisplayName = "/tg/station server"; installer.StartType = ServiceStartMode.Automatic; installer.ServicesDependedOn = new string[] { "Tcpip", "Dhcp", "Dnscache" }; - installer.ServiceName = "tgstation-server"; + installer.ServiceName = NewServiceName; installer.Parent = processInstaller; var state = new ListDictionary(); @@ -527,7 +529,6 @@ try managementObject.InvokeMethod("ChangeStartMode", new object[] { "Disabled" }); } - tgs3Service = GetTgs3Service(false); if(tgs3Service.StartType != ServiceStartMode.Disabled) Console.WriteLine("Failed to disable TGS3 service! This isn't critical, however."); diff --git a/tools/Tgstation.Server.Migrator/Tgstation.Server.Migrator.csproj b/tools/Tgstation.Server.Migrator/Tgstation.Server.Migrator.csproj index 550a5f0ac8..a74f718d7c 100644 --- a/tools/Tgstation.Server.Migrator/Tgstation.Server.Migrator.csproj +++ b/tools/Tgstation.Server.Migrator/Tgstation.Server.Migrator.csproj @@ -1,20 +1,20 @@  - + Exe - $(TgsNetVersion) + $(TgsFrameworkVersion) win-x86 $(TgsMigratorVersion) enable CA1416 false - + - +