diff --git a/.dockerignore b/.dockerignore index 3d9e0e32dd5..4ce7bb37b3d 100644 --- a/.dockerignore +++ b/.dockerignore @@ -12,7 +12,7 @@ appveyor.yml omnisharp.json README.md -build/package/** +build/package/deb build/ci.runsettings build/Dockerfile build/GenerateMigrations.sh @@ -26,9 +26,6 @@ src/Tgstation.Server.Host/appsettings.Development.json src/Tgstation.Server.Host/appsettings.Development.yml src/Tgstation.Server.Host/tgs.bat src/Tgstation.Server.Host/tgs.sh -src/Tgstation.Server.Client -src/Tgstation.Server.Host.Service -tests -tools +tests/DMAPI artifacts packaging diff --git a/.github/CONTRIBUTING.md b/.github/CONTRIBUTING.md index 68ae14de2f3..0dee22f9b4b 100644 --- a/.github/CONTRIBUTING.md +++ b/.github/CONTRIBUTING.md @@ -34,7 +34,7 @@ You can of course, as always, ask for help at [#coderbus](irc://irc.rizon.net/co ### Development Environment -You need the .NET 6.0 SDK and npm>=v5.7 (in your PATH) to compile the server. +You need the .NET 8.0 SDK, node>=v20, and npm>=v5.7 (in your PATH) to compile the server. The recommended IDE is Visual Studio 2022 or VSCode. @@ -51,6 +51,9 @@ In order to run the integration tests you must have the following environment va - `TGS_TEST_DISCORD_CHANNEL`: To a valid discord channel ID that the above bot can access. - `TGS_TEST_IRC_CONNECTION_STRING`: To a valid IRC connection string. See the code for [IrcConnectionStringBuilder](../src/Tgstation.Server.Api/Models/IrcConnectionStringBuilder.cs) for details. - `TGS_TEST_IRC_CHANNEL`: To a valid IRC channel accessible with the above connection. +- (Optional) `TGS_TEST_OD_ENGINE_VERSION`: Specify the full git commit SHA of the [OpenDream](https://github.com/OpenDreamProject/OpenDream) version to use in the main integration test, the default is the current HEAD of the default branch. +- (Optional) `TGS_TEST_OD_GIT_DIRECTORY`: Path to a local [OpenDream](https://github.com/OpenDreamProject/OpenDream) git repository to use as an upstream for testing. +- (Optional) `TGS_TEST_OD_EXCLUSIVE`: Set to `true` to enable the quicker integration test that only runs [OpenDream](https://github.com/OpenDreamProject/OpenDream) functionality. This is tested by default in the main integration test. ### Notes About Forks diff --git a/.github/workflows/auto-approve-dominions-prs.yml b/.github/workflows/auto-approve-dominions-prs.yml index 53546b6f667..cd499fffde8 100644 --- a/.github/workflows/auto-approve-dominions-prs.yml +++ b/.github/workflows/auto-approve-dominions-prs.yml @@ -8,7 +8,6 @@ on: branches: - dev - master - - V6 concurrency: group: "approve-dominion-${{ github.head_ref || github.run_id }}-${{ github.event_name }}" diff --git a/.github/workflows/check-pr-has-milestone.yml b/.github/workflows/check-pr-has-milestone.yml index 7f6ed97d2d7..0ce8c605f99 100644 --- a/.github/workflows/check-pr-has-milestone.yml +++ b/.github/workflows/check-pr-has-milestone.yml @@ -6,7 +6,6 @@ on: branches: - dev - master - - V6 concurrency: group: "check-pr-milestone-${{ github.head_ref || github.run_id }}-${{ github.event_name }}" diff --git a/.github/workflows/ci-pipeline.yml b/.github/workflows/ci-pipeline.yml index 4439daea556..bad76f3ba3a 100644 --- a/.github/workflows/ci-pipeline.yml +++ b/.github/workflows/ci-pipeline.yml @@ -2,6 +2,7 @@ # Does CI on push/PR/cron. Deployments on push when triggered # - Validates Documentation # - Builds C# and DMAPI +# - Runs CodeQL Anaylsis # - Tests everything on massive matrix # - Packages # - Tests package installs/services/uninstalls @@ -19,26 +20,25 @@ name: 'CI Pipeline' on: schedule: - - cron: 0 23 * * * + - cron: 0 9 * * * push: branches: - dev - master - - V6 pull_request: branches: - dev - master - - V6 pull_request_target: types: [ opened, reopened, labeled, synchronize ] branches: - dev - master - - V6 env: - TGS_DOTNET_VERSION: 6.0.x + TGS_DOTNET_VERSION: 8 + OD_DOTNET_VERSION: 7 + TGS_DOTNET_QUALITY: ga TGS_TEST_GITHUB_TOKEN: ${{ secrets.LIVE_TESTS_TOKEN }} TGS_RELEASE_NOTES_TOKEN: ${{ secrets.DEV_PUSH_TOKEN }} WINGET_PUSH_TOKEN: ${{ secrets.DEV_PUSH_TOKEN }} @@ -134,11 +134,11 @@ jobs: exit 0 - name: Checkout (Branch) - uses: actions/checkout@v3 + uses: actions/checkout@v4 if: github.event_name == 'push' || github.event_name == 'schedule' - name: Checkout (PR Merge) - uses: actions/checkout@v3 + uses: actions/checkout@v4 if: github.event_name != 'push' && github.event_name != 'schedule' with: ref: "refs/pull/${{ github.event.number }}/merge" @@ -163,6 +163,50 @@ jobs: fi exit $retval + opendream-build: + name: Build DMAPI (OpenDream) + needs: start-ci-run-gate + if: (!(cancelled() || failure()) && needs.start-ci-run-gate.result == 'success') + strategy: + fail-fast: false + matrix: + committish: [ 'master', 'tgs-min-compat' ] + runs-on: ubuntu-latest + steps: + - name: Setup dotnet + uses: actions/setup-dotnet@v3 + with: + dotnet-version: '${{ env.OD_DOTNET_VERSION }}.0.x' + dotnet-quality: ${{ env.TGS_DOTNET_QUALITY }} + + - name: Checkout (Branch) + uses: actions/checkout@v4 + if: github.event_name == 'push' || github.event_name == 'schedule' + + - name: Checkout (PR Merge) + uses: actions/checkout@v4 + if: github.event_name != 'push' && github.event_name != 'schedule' + with: + ref: "refs/pull/${{ github.event.number }}/merge" + + - name: Checkout OpenDream + run: | + cd $HOME + git clone https://github.com/OpenDreamProject/OpenDream + cd OpenDream + git checkout ${{ matrix.committish }} + git submodule update --init --recursive + + - name: Create TGS Deployment + run: | + cd $HOME/OpenDream + dotnet run -c Release --project OpenDreamPackageTool --property WarningLevel=0 -- --tgs -o tgs_deploy + + - name: Build DMAPI + run: | + cd tests/DMAPI/BasicOperation + $HOME/OpenDream/tgs_deploy/bin/compiler/DMCompiler --verbose --notices-enabled basic_operation_test.dme + pages-build: name: Build gh-pages runs-on: ubuntu-latest @@ -170,16 +214,17 @@ jobs: if: (!(cancelled() || failure()) && needs.start-ci-run-gate.result == 'success') steps: - name: Setup dotnet - uses: actions/setup-dotnet@v2 + uses: actions/setup-dotnet@v3 with: - dotnet-version: ${{ env.TGS_DOTNET_VERSION }} + dotnet-version: '${{ env.TGS_DOTNET_VERSION }}.0.x' + dotnet-quality: ${{ env.TGS_DOTNET_QUALITY }} - name: Checkout (Branch) - uses: actions/checkout@v3 + uses: actions/checkout@v4 if: github.event_name == 'push' || github.event_name == 'schedule' - name: Checkout (PR Merge) - uses: actions/checkout@v3 + uses: actions/checkout@v4 if: github.event_name != 'push' && github.event_name != 'schedule' with: ref: "refs/pull/${{ github.event.number }}/merge" @@ -239,11 +284,11 @@ jobs: if: (!(cancelled() || failure()) && needs.start-ci-run-gate.result == 'success') steps: - name: Checkout (Branch) - uses: actions/checkout@v3 + uses: actions/checkout@v4 if: github.event_name == 'push' || github.event_name == 'schedule' - name: Checkout (PR Merge) - uses: actions/checkout@v3 + uses: actions/checkout@v4 if: github.event_name != 'push' && github.event_name != 'schedule' with: ref: "refs/pull/${{ github.event.number }}/merge" @@ -271,16 +316,17 @@ jobs: sudo apt-get install -y -o APT::Immediate-Configure=0 libc6-i386 libstdc++6:i386 libgcc-s1:i386 - name: Setup dotnet - uses: actions/setup-dotnet@v2 + uses: actions/setup-dotnet@v3 with: - dotnet-version: ${{ env.TGS_DOTNET_VERSION }} + dotnet-version: '${{ env.TGS_DOTNET_VERSION }}.0.x' + dotnet-quality: ${{ env.TGS_DOTNET_QUALITY }} - name: Checkout (Branch) - uses: actions/checkout@v3 + uses: actions/checkout@v4 if: github.event_name == 'push' || github.event_name == 'schedule' - name: Checkout (PR Merge) - uses: actions/checkout@v3 + uses: actions/checkout@v4 if: github.event_name != 'push' && github.event_name != 'schedule' with: ref: "refs/pull/${{ github.event.number }}/merge" @@ -321,16 +367,17 @@ jobs: runs-on: windows-latest steps: - name: Setup dotnet - uses: actions/setup-dotnet@v2 + uses: actions/setup-dotnet@v3 with: - dotnet-version: ${{ env.TGS_DOTNET_VERSION }} + dotnet-version: '${{ env.TGS_DOTNET_VERSION }}.0.x' + dotnet-quality: ${{ env.TGS_DOTNET_QUALITY }} - name: Checkout (Branch) - uses: actions/checkout@v3 + uses: actions/checkout@v4 if: github.event_name == 'push' || github.event_name == 'schedule' - name: Checkout (PR Merge) - uses: actions/checkout@v3 + uses: actions/checkout@v4 if: github.event_name != 'push' && github.event_name != 'schedule' with: ref: "refs/pull/${{ github.event.number }}/merge" @@ -359,8 +406,8 @@ jobs: windows-integration-test: name: Windows Live Tests - needs: dmapi-build - if: (!(cancelled() || failure()) && needs.dmapi-build.result == 'success') + needs: [dmapi-build, opendream-build] + if: (!(cancelled() || failure()) && needs.dmapi-build.result == 'success' && needs.opendream-build.result == 'success') strategy: fail-fast: false matrix: @@ -377,9 +424,12 @@ jobs: sqlcmd -l 600 -S "(localdb)\MSSQLLocalDB" -Q "SELECT @@VERSION;" - name: Setup dotnet - uses: actions/setup-dotnet@v2 + uses: actions/setup-dotnet@v3 with: - dotnet-version: ${{ env.TGS_DOTNET_VERSION }} + dotnet-version: | + ${{ env.TGS_DOTNET_VERSION }}.0.x + ${{ env.OD_DOTNET_VERSION }}.0.x + dotnet-quality: ${{ env.TGS_DOTNET_QUALITY }} - name: Set TGS_TEST_DUMP_API_SPEC if: ${{ matrix.configuration == 'Release' && matrix.watchdog-type == 'Advanced' && matrix.database-type == 'SqlServer' }} @@ -433,16 +483,16 @@ jobs: if: ${{ matrix.database-type == 'SqlServer' }} shell: bash run: | - TGS_CONNSTRING_VALUE="Server=(localdb)\MSSQLLocalDB;Integrated Security=true;Initial Catalog=TGS_${{ matrix.watchdog-type }}_${{ matrix.configuration }};Application Name=tgstation-server" + TGS_CONNSTRING_VALUE="Server=(localdb)\MSSQLLocalDB;Encrypt=false;Integrated Security=true;Initial Catalog=TGS_${{ matrix.watchdog-type }}_${{ matrix.configuration }};Application Name=tgstation-server" echo "TGS_TEST_CONNECTION_STRING=$(echo $TGS_CONNSTRING_VALUE)" >> $GITHUB_ENV echo "TGS_TEST_DATABASE_TYPE=SqlServer" >> $GITHUB_ENV - name: Checkout (Branch) - uses: actions/checkout@v3 + uses: actions/checkout@v4 if: github.event_name == 'push' || github.event_name == 'schedule' - name: Checkout (PR Merge) - uses: actions/checkout@v3 + uses: actions/checkout@v4 if: github.event_name != 'push' && github.event_name != 'schedule' with: ref: "refs/pull/${{ github.event.number }}/merge" @@ -479,11 +529,19 @@ jobs: fi - name: Store Live Tests Output + if: ${{ steps.live-tests.outputs.succeeded == 'YES' }} uses: actions/upload-artifact@v3 with: name: windows-integration-test-logs-${{ matrix.configuration }}-${{ matrix.watchdog-type }}-${{ matrix.database-type }} path: ./test_output.txt + - name: Store Errored Live Tests Output + if: ${{ steps.live-tests.outputs.succeeded != 'YES' }} + uses: actions/upload-artifact@v3 + with: + name: errored-windows-test-logs-${{ matrix.configuration }}-${{ matrix.watchdog-type }}-${{ matrix.database-type }} + path: ./test_output.txt + - name: Fail if Live Tests Failed if: ${{ steps.live-tests.outputs.succeeded != 'YES' }} run: exit 1 @@ -499,7 +557,7 @@ jobs: uses: actions/upload-artifact@v3 with: name: openapi-spec - path: C:/swagger.json + path: C:/tgs_api.json - name: Package Server Service if: ${{ matrix.configuration == 'Release' && matrix.watchdog-type == 'Basic' }} @@ -537,18 +595,18 @@ jobs: linux-integration-tests: name: Linux Live Tests - needs: dmapi-build - if: (!(cancelled() || failure()) && needs.dmapi-build.result == 'success') + needs: [dmapi-build, opendream-build] + if: (!(cancelled() || failure()) && needs.dmapi-build.result == 'success' && needs.opendream-build.result == 'success') services: # We start all dbs here so we can just code the stuff once mssql: - image: mcr.microsoft.com/mssql/server:2019-latest + image: ${{ (matrix.database-type == 'SqlServer') && 'mcr.microsoft.com/mssql/server:2019-latest' || '' }} env: SA_PASSWORD: myPassword ACCEPT_EULA: 'Y' ports: - 1433:1433 postgres: - image: cyberboss/postgres-max-connections # Fork of _/postgres:latest with max_connections=500 becuase GitHub actions service containers have no way to set command lines. Rebuilds with updates. + image: ${{ (matrix.database-type == 'PostgresSql') && 'cyberboss/postgres-max-connections' || '' }} # Fork of _/postgres:latest with max_connections=500 becuase GitHub actions service containers have no way to set command lines. Rebuilds with updates. ports: - 5432:5432 env: @@ -559,7 +617,7 @@ jobs: --health-timeout 5s --health-retries 5 mariadb: - image: mariadb + image: ${{ (matrix.database-type == 'MariaDB') && 'mariadb' || '' }} ports: - 3306:3306 env: @@ -570,7 +628,7 @@ jobs: --health-timeout=2s --health-retries=3 mysql: - image: mysql:5.7.31 + image: ${{ (matrix.database-type == 'MySql') && 'mysql:5.7.31' || '' }} ports: - 3307:3306 env: @@ -598,9 +656,12 @@ jobs: sudo apt-get install -y -o APT::Immediate-Configure=0 libc6-i386 libstdc++6:i386 gdb libgcc-s1:i386 - name: Setup dotnet - uses: actions/setup-dotnet@v2 + uses: actions/setup-dotnet@v3 with: - dotnet-version: ${{ env.TGS_DOTNET_VERSION }} + dotnet-version: | + ${{ env.TGS_DOTNET_VERSION }}.0.x + ${{ env.OD_DOTNET_VERSION }}.0.x + dotnet-quality: ${{ env.TGS_DOTNET_QUALITY }} - name: Set Sqlite Connection Info if: ${{ matrix.database-type == 'Sqlite' }} @@ -632,11 +693,11 @@ jobs: run: echo "General__UseBasicWatchdog=true" >> $GITHUB_ENV - name: Checkout (Branch) - uses: actions/checkout@v3 + uses: actions/checkout@v4 if: github.event_name == 'push' || github.event_name == 'schedule' - name: Checkout (PR Merge) - uses: actions/checkout@v3 + uses: actions/checkout@v4 if: github.event_name != 'push' && github.event_name != 'schedule' with: ref: "refs/pull/${{ github.event.number }}/merge" @@ -711,11 +772,11 @@ jobs: run: npm i -g ibm-openapi-validator@0.51.3 - name: Checkout (Branch) - uses: actions/checkout@v3 + uses: actions/checkout@v4 if: github.event_name == 'push' || github.event_name == 'schedule' - name: Checkout (PR Merge) - uses: actions/checkout@v3 + uses: actions/checkout@v4 if: github.event_name != 'push' && github.event_name != 'schedule' with: ref: "refs/pull/${{ github.event.number }}/merge" @@ -727,7 +788,7 @@ jobs: path: ./swagger - name: Lint OpenAPI Spec - run: npx lint-openapi -v -p -c build/OpenApiValidationSettings.json ./swagger/swagger.json + run: npx lint-openapi -v -p -c build/OpenApiValidationSettings.json ./swagger/tgs_api.json upload-code-coverage: name: Upload Code Coverage @@ -736,11 +797,11 @@ jobs: runs-on: ubuntu-latest steps: - name: Checkout (Branch) - uses: actions/checkout@v3 + uses: actions/checkout@v4 if: github.event_name == 'push' || github.event_name == 'schedule' - name: Checkout (PR Merge) - uses: actions/checkout@v3 + uses: actions/checkout@v4 if: github.event_name != 'push' && github.event_name != 'schedule' with: ref: "refs/pull/${{ github.event.number }}/merge" @@ -1004,15 +1065,17 @@ jobs: echo ${{ secrets.PACKAGING_PRIVATE_KEY_PASSPHRASE }} | gpg --batch --yes --passphrase-fd 0 --import private.pgp rm private.pgp - - name: Install dotnet-sdk-6.0 + - name: Install dotnet-sdk system package + if: (!contains(env.TGS_DOTNET_QUALITY, 'preview')) run: | sudo apt-get update - sudo apt-get install -y dotnet-sdk-6.0 + sudo apt-get install -y dotnet-sdk-${{ env.TGS_DOTNET_VERSION }}.0 - name: Setup dotnet - uses: actions/setup-dotnet@v2 + uses: actions/setup-dotnet@v3 with: - dotnet-version: ${{ env.TGS_DOTNET_VERSION }} + dotnet-version: '${{ env.TGS_DOTNET_VERSION }}.0.x' + dotnet-quality: ${{ env.TGS_DOTNET_QUALITY }} - name: Override /usr/bin/dotnet run: | @@ -1022,11 +1085,11 @@ jobs: echo "New dotnet path should be $DOTNET_PATH" - name: Checkout (Branch) - uses: actions/checkout@v3 + uses: actions/checkout@v4 if: github.event_name == 'push' || github.event_name == 'schedule' - name: Checkout (PR Merge) - uses: actions/checkout@v3 + uses: actions/checkout@v4 if: github.event_name != 'push' && github.event_name != 'schedule' with: ref: "refs/pull/${{ github.event.number }}/merge" @@ -1099,16 +1162,17 @@ jobs: GITHUB_TOKEN: ${{ env.WINGET_PUSH_TOKEN }} - name: Setup dotnet - uses: actions/setup-dotnet@v2 + uses: actions/setup-dotnet@v3 with: - dotnet-version: ${{ env.TGS_DOTNET_VERSION }} + dotnet-version: '${{ env.TGS_DOTNET_VERSION }}.0.x' + dotnet-quality: ${{ env.TGS_DOTNET_QUALITY }} - name: Checkout (Branch) - uses: actions/checkout@v3 + uses: actions/checkout@v4 if: github.event_name == 'push' || github.event_name == 'schedule' - name: Checkout (PR Merge) - uses: actions/checkout@v3 + uses: actions/checkout@v4 if: github.event_name != 'push' && github.event_name != 'schedule' with: ref: "refs/pull/${{ github.event.number }}/merge" @@ -1224,9 +1288,10 @@ jobs: runs-on: ubuntu-latest steps: - name: Setup dotnet - uses: actions/setup-dotnet@v2 + uses: actions/setup-dotnet@v3 with: - dotnet-version: ${{ env.TGS_DOTNET_VERSION }} + dotnet-version: '${{ env.TGS_DOTNET_VERSION }}.0.x' + dotnet-quality: ${{ env.TGS_DOTNET_QUALITY }} - name: Retrieve Latest winget-pkgs PULL_REQUEST_TEMPLATE commit SHA from GitHub API id: get-sha @@ -1235,11 +1300,11 @@ jobs: echo "pr_template_sha=$(cat commits.json | jq '.[0].sha')" >> $GITHUB_OUTPUT - name: Checkout (Branch) - uses: actions/checkout@v3 + uses: actions/checkout@v4 if: github.event_name == 'push' || github.event_name == 'schedule' - name: Checkout (PR Merge) - uses: actions/checkout@v3 + uses: actions/checkout@v4 if: github.event_name != 'push' && github.event_name != 'schedule' with: ref: "refs/pull/${{ github.event.number }}/merge" @@ -1278,12 +1343,13 @@ jobs: if: (!(cancelled() || failure()) && needs.deployment-gate.result == 'success' && contains(github.event.head_commit.message, '[APIDeploy]')) steps: - name: Setup dotnet - uses: actions/setup-dotnet@v2 + uses: actions/setup-dotnet@v3 with: - dotnet-version: ${{ env.TGS_DOTNET_VERSION }} + dotnet-version: '${{ env.TGS_DOTNET_VERSION }}.0.x' + dotnet-quality: ${{ env.TGS_DOTNET_QUALITY }} - name: Checkout - uses: actions/checkout@v3 + uses: actions/checkout@v4 - name: Restore run: dotnet restore @@ -1330,7 +1396,7 @@ jobs: GITHUB_TOKEN: ${{ secrets.DEV_PUSH_TOKEN }} with: upload_url: ${{ steps.create_release.outputs.upload_url }} - asset_path: ./swagger/swagger.json + asset_path: ./swagger/tgs_api.json asset_name: swagger.json asset_content_type: application/json @@ -1341,12 +1407,13 @@ jobs: if: (!(cancelled() || failure()) && needs.deployment-gate.result == 'success' && contains(github.event.head_commit.message, '[DMDeploy]')) steps: - name: Setup dotnet - uses: actions/setup-dotnet@v2 + uses: actions/setup-dotnet@v3 with: - dotnet-version: ${{ env.TGS_DOTNET_VERSION }} + dotnet-version: '${{ env.TGS_DOTNET_VERSION }}.0.x' + dotnet-quality: ${{ env.TGS_DOTNET_QUALITY }} - name: Checkout - uses: actions/checkout@v3 + uses: actions/checkout@v4 - name: Restore run: dotnet restore @@ -1403,12 +1470,13 @@ jobs: if: (!(cancelled() || failure()) && needs.deployment-gate.result == 'success' && contains(github.event.head_commit.message, '[NugetDeploy]')) steps: - name: Setup dotnet - uses: actions/setup-dotnet@v2 + uses: actions/setup-dotnet@v3 with: - dotnet-version: ${{ env.TGS_DOTNET_VERSION }} + dotnet-version: '${{ env.TGS_DOTNET_VERSION }}.0.x' + dotnet-quality: ${{ env.TGS_DOTNET_QUALITY }} - name: Checkout - uses: actions/checkout@v3 + uses: actions/checkout@v4 - name: Restore run: dotnet restore @@ -1453,12 +1521,13 @@ jobs: if: (!(cancelled() || failure()) && (needs.deploy-dm.result == 'success' || needs.deploy-http.result == 'success') && !contains(github.event.head_commit.message, '[TGSDeploy]')) steps: - name: Setup dotnet - uses: actions/setup-dotnet@v2 + uses: actions/setup-dotnet@v3 with: - dotnet-version: ${{ env.TGS_DOTNET_VERSION }} + dotnet-version: '${{ env.TGS_DOTNET_VERSION }}.0.x' + dotnet-quality: ${{ env.TGS_DOTNET_QUALITY }} - name: Checkout - uses: actions/checkout@v3 + uses: actions/checkout@v4 - name: Restore run: dotnet restore @@ -1476,12 +1545,13 @@ jobs: if: (!(cancelled() || failure()) && needs.deployment-gate.result == 'success' && github.event.ref == 'refs/heads/master' && contains(github.event.head_commit.message, '[TGSDeploy]')) steps: - name: Setup dotnet - uses: actions/setup-dotnet@v2 + uses: actions/setup-dotnet@v3 with: - dotnet-version: ${{ env.TGS_DOTNET_VERSION }} + dotnet-version: '${{ env.TGS_DOTNET_VERSION }}.0.x' + dotnet-quality: ${{ env.TGS_DOTNET_QUALITY }} - name: Checkout - uses: actions/checkout@v3 + uses: actions/checkout@v4 - name: Restore run: dotnet restore @@ -1636,7 +1706,7 @@ jobs: GITHUB_TOKEN: ${{ secrets.DEV_PUSH_TOKEN }} with: upload_url: ${{ steps.create_release.outputs.upload_url }} - asset_path: ./swagger/swagger.json + asset_path: ./swagger/tgs_api.json asset_name: swagger.json asset_content_type: application/json @@ -1687,12 +1757,13 @@ jobs: if: (!(cancelled() || failure()) && needs.deploy-tgs.result == 'success') steps: - name: Setup dotnet - uses: actions/setup-dotnet@v2 + uses: actions/setup-dotnet@v3 with: - dotnet-version: ${{ env.TGS_DOTNET_VERSION }} + dotnet-version: '${{ env.TGS_DOTNET_VERSION }}.0.x' + dotnet-quality: ${{ env.TGS_DOTNET_QUALITY }} - name: Checkout - uses: actions/checkout@v3 + uses: actions/checkout@v4 - name: Restore run: dotnet restore @@ -1731,7 +1802,7 @@ jobs: runs-on: ubuntu-latest steps: - name: Checkout - uses: actions/checkout@v3 + uses: actions/checkout@v4 - name: Parse TGS version run: | @@ -1755,7 +1826,7 @@ jobs: runs-on: ubuntu-latest steps: - name: Checkout - uses: actions/checkout@v3 + uses: actions/checkout@v4 - name: Parse TGS version run: | @@ -1774,9 +1845,10 @@ jobs: runs-on: windows-latest steps: - name: Setup dotnet - uses: actions/setup-dotnet@v2 + uses: actions/setup-dotnet@v3 with: - dotnet-version: ${{ env.TGS_DOTNET_VERSION }} + dotnet-version: '${{ env.TGS_DOTNET_VERSION }}.0.x' + dotnet-quality: ${{ env.TGS_DOTNET_QUALITY }} - name: Install winget uses: Cyberboss/install-winget@v1 @@ -1787,7 +1859,7 @@ jobs: run: winget install wingetcreate --version 1.2.8.0 --disable-interactivity --accept-source-agreements # Pinned due to breaking every other version - name: Checkout - uses: actions/checkout@v3 + uses: actions/checkout@v4 - name: Build ReleaseNotes run: dotnet build -c Release -p:TGS_HOST_NO_WEBPANEL=true tools/Tgstation.Server.ReleaseNotes diff --git a/.github/workflows/code-scanning.yml b/.github/workflows/code-scanning.yml index fdd0e0ecdce..b6610591447 100644 --- a/.github/workflows/code-scanning.yml +++ b/.github/workflows/code-scanning.yml @@ -1,26 +1,23 @@ name: 'Code Scanning' on: - schedule: - - cron: 0 23 * * 1 push: branches: - dev - master - - V6 pull_request: branches: - dev - master - - V6 + +env: + TGS_DOTNET_VERSION: 8 + TGS_DOTNET_QUALITY: ga concurrency: group: "code-scanning-${{ github.head_ref || github.run_id }}-${{ github.event_name }}" cancel-in-progress: true -env: - TGS_DOTNET_VERSION: 6.0.x - jobs: analyze: name: Code Scanning @@ -32,12 +29,13 @@ jobs: if: ${{ vars.TGS_ENABLE_CODE_QL }} == 'true' steps: - name: Setup dotnet - uses: actions/setup-dotnet@v2 + uses: actions/setup-dotnet@v3 with: - dotnet-version: ${{ env.TGS_DOTNET_VERSION }} + dotnet-version: '${{ env.TGS_DOTNET_VERSION }}.0.x' + dotnet-quality: ${{ env.TGS_DOTNET_QUALITY }} - name: Checkout - uses: actions/checkout@v3 + uses: actions/checkout@v4 - name: Initialize CodeQL uses: github/codeql-action/init@v2 diff --git a/.github/workflows/v6-integration.yml b/.github/workflows/v6-integration.yml deleted file mode 100644 index 83be1561eb4..00000000000 --- a/.github/workflows/v6-integration.yml +++ /dev/null @@ -1,29 +0,0 @@ -name: 'V6 Integration' - -on: - push: - branches: - - dev - -jobs: - v6-integration: - - runs-on: ubuntu-latest - - steps: - - name: Checkout - uses: actions/checkout@v1 - - - name: Merge dev into V6 - uses: robotology/gh-action-nightly-merge@22f5e45d028f22837d617fa07512925457eec184 #v1.3.3 - with: - stable_branch: 'dev' - development_branch: 'V6' - allow_ff: true - allow_forks: true - user_name: tgstation-server - user_email: tgstation-server@users.noreply.github.com - push_token: DEV_PUSH_TOKEN - env: - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - DEV_PUSH_TOKEN: ${{ secrets.DEV_PUSH_TOKEN }} diff --git a/README.md b/README.md index 6f76b38e347..d2830e93d9a 100644 --- a/README.md +++ b/README.md @@ -12,7 +12,7 @@ This is a toolset to manage production BYOND servers. It includes the ability to ### Pre-Requisites -_Note: If you opt to use the Windows installer, all pre-requisites (including MariaDB) are provided out of the box._ +_Note: If you opt to use the Windows installer, all pre-requisites for running BYOND servers (including MariaDB) are provided out of the box. If you wish to use OpenDream you will need to install the required dotnet SDK manually._ tgstation-server needs a relational database to store it's data. @@ -85,7 +85,7 @@ Note: The `winget` package is submitted to Microsoft for approval once TGS relea ##### 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`. +If you don't have it installed already, download and install the [ASP .NET Core Runtime Hosting Bundle (>= v8.0)](https://dotnet.microsoft.com/download/dotnet/8.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. @@ -101,9 +101,19 @@ If using the console version, run `./tgs.bat` in the root of the installation di Installing natively is the recommended way to run tgstation-server on Linux. -##### Ubuntu +##### Ubuntu/Debian Package -Install TGS and all it's dependencies via our apt repository, interactively configure it, and start the service with this one-liner: +You first need to add the appropriate Microsoft package repository for your distribution + +Refer to the Microsoft website for steps for + +- [Ubuntu](https://learn.microsoft.com/en-us/dotnet/core/install/linux-ubuntu#register-the-microsoft-package-repository) +- [Debian 12](https://learn.microsoft.com/en-us/dotnet/core/install/linux-debian#debian-12) +- [Debian 11](https://learn.microsoft.com/en-us/dotnet/core/install/linux-debian#debian-11) +- [Debian 10](https://learn.microsoft.com/en-us/dotnet/core/install/linux-debian#debian-10) +- [Other Distros](https://learn.microsoft.com/en-us/dotnet/core/install/linux-scripted-manual#manual-install) + +After that, install TGS and all it's dependencies via our apt repository, interactively configure it, and start the service with this one-liner: ```sh sudo dpkg --add-architecture i386 \ @@ -117,25 +127,13 @@ sudo dpkg --add-architecture i386 \ && sudo systemctl start tgstation-server ``` -##### Debian - -The `aspnetcore-runtime-6.0` package isn't yet available on mainline Debian and must be [installed from Microsoft](https://learn.microsoft.com/en-us/dotnet/core/install/linux-debian) first. Use the following one-liner to add their packages repository. - -```sh -curl -L https://packages.microsoft.com/config/debian/11/packages-microsoft-prod.deb -o packages-microsoft-prod.deb \ -&& sudo dpkg -i packages-microsoft-prod.deb \ -&& rm packages-microsoft-prod.deb -``` - -After that, run the same command as the Ubuntu installation. +The service will execute as the newly created user: `tgstation-server`. -_Support for more distros coming soon_ - -##### Manual +##### Manual Setup The following dependencies are required. -- aspnetcore-runtime-6.0 (Note, not all supported distros have this package, see the links above for official Microsoft installation instructions) +- aspnetcore-runtime-8.0 (See Prerequisites under the `Ubuntu/Debian Package` section) - libc6-i386 - libstdc++6:i386 - gcc-multilib (Only on 64-bit systems) @@ -143,7 +141,7 @@ The following dependencies are required. [Download the latest release .zip](https://github.com/tgstation/tgstation-server/releases/latest). Choose `ServerConsole`. -If you have SystemD installed, we recommend installing the service unit [here](./build/tgstation-server.service). It assumes TGS is installed into `/opt/tgstation-server` and you will be using the but feel free to adjust it to your needs. Note that the server will need to have it's configuration file setup before running with SystemD. +If you have SystemD installed, we recommend installing the service unit [here](./build/tgstation-server.service). It assumes TGS is installed into `/opt/tgstation-server`, it is executing as the user `tgstation-server`, and you will be using the console runner, but feel free to adjust it to your needs. Note that the server will need to have it's configuration file setup before running with SystemD. Alternatively, to launch the server in the current shell, run `./tgs.sh` in the root of the installation directory. The process will run in a blocking fashion. SIGQUIT will close the server, terminating all live game instances. @@ -183,6 +181,21 @@ Note that automatic configuration reloading is currently not supported in the co If using manual configuration, before starting your container make sure the aforementioned `appsettings.Production.yml` is setup properly. See below +#### OpenDream + +In order for TGS to use [OpenDream](https://github.com/OpenDreamProject/OpenDream), it requires the full .NET SDK to build whichever version your servers target. Whatever that is, it must be available using the `dotnet` command for whichever user runs TGS. + +OpenDream currently requires [.NET SDK 7.0](https://dotnet.microsoft.com/en-us/download/dotnet/7.0) at the time of this writing. You must install this manually. + +On Linux, as long as OpenDream and TGS do not use the same .NET major version, you cannot achieve this with the package manager as they will conflict. The 7.0 SDK can be added to an 8.0 runtime installation via the following steps. + +1. Install `tgstation-server` using any of the above methods. +1. [Download the Linux SDK binaries](https://dotnet.microsoft.com/en-us/download/dotnet/7.0) for your selected architecture. +1. Extract everything EXCEPT the `dotnet` executable, `LICENSE.txt``, and `ThirdPartyNotices.txt` in the `.tar.gz` on top of the existing installation directory `/usr/share/dotnet/` +1. Run `sudo chown -R root /usr/share/dotnet` + +You should now be able to run the `dotnet --list-sdks` command and see an entry for `7.0.XXX [/usr/share/dotnet/sdk]`. + ### Configuring The first time you run TGS you should be prompted with a configuration wizard which will guide you through setting up your `appsettings.Production.yml` @@ -197,7 +210,7 @@ There are 3 primary supported ways to configure TGS: - Set environment variables in the form `Section__Subsection=value` or `Section__ArraySubsection__0=value` for arrays. - Set command line arguments in the form `--Section:Subsection=value` or `--Section:ArraySubsection:0=value` for arrays. -The latter two are not recommended as they cannot be dynamically changed at runtime. See more on ASP.NET core configuration [here](https://docs.microsoft.com/en-us/aspnet/core/fundamentals/configuration/?view=aspnetcore-6.0). +The latter two are not recommended as they cannot be dynamically changed at runtime. See more on ASP.NET core configuration [here](https://docs.microsoft.com/en-us/aspnet/core/fundamentals/configuration/?view=aspnetcore-8.0). #### Manual Configuration @@ -383,7 +396,7 @@ Exposing the builtin Kestrel server to the internet directly over HTTP is highly System administrators will most likely have their own configuration plans, but here are some basic guides for beginners. -Once complete, test that your configuration worked by visiting your proxy site from a browser on a different computer. You should recieve a 401 Unauthorized response. +Once complete, test that your configuration worked by visiting your proxy site from a browser on a different computer. You should receive a 401 Unauthorized response. _NOTE: Your reverse proxy setup may interfere with SSE (Server-Sent Events) which is used for real-time job updates. If you find this to be the case, please open an issue describing what you did to fix it as there may be a way for us to bypass the need for a workaround from our end._ diff --git a/build/Dockerfile b/build/Dockerfile index a65df7defe6..383df77bfb0 100644 --- a/build/Dockerfile +++ b/build/Dockerfile @@ -1,4 +1,4 @@ -FROM mcr.microsoft.com/dotnet/sdk:6.0-bookworm-slim AS build +FROM mcr.microsoft.com/dotnet/sdk:8.0-bookworm-slim AS build # install node and npm # replace shell with bash so we can source files @@ -21,11 +21,7 @@ RUN . $NVM_DIR/nvm.sh \ # Build web control panel WORKDIR /repo/build -COPY build/Common.props Common.props -COPY build/NugetCommon.props NugetCommon.props -COPY build/Version.props Version.props -COPY build/WebpanelVersion.props WebpanelVersion.props -COPY build/SrcCommon.props SrcCommon.props +COPY build/*.props ./ WORKDIR /repo/src/Tgstation.Server.Host @@ -36,22 +32,11 @@ RUN dotnet msbuild -target:NpmBuild WORKDIR /repo -# Restore nuget packages -COPY tgstation-server.sln ./ - -COPY src/Tgstation.Server.Host.Console/Tgstation.Server.Host.Console.csproj src/Tgstation.Server.Host.Console/ -COPY src/Tgstation.Server.Host.Watchdog/Tgstation.Server.Host.Watchdog.csproj src/Tgstation.Server.Host.Watchdog/ -COPY src/Tgstation.Server.Api/Tgstation.Server.Api.csproj src/Tgstation.Server.Api/ -COPY src/Tgstation.Server.Common/Tgstation.Server.Common.csproj src/Tgstation.Server.Common/ -COPY src/Tgstation.Server.Host.Common/Tgstation.Server.Host.Common.csproj src/Tgstation.Server.Host.Common/ - -RUN dotnet restore -nowarn:MSB3202,nu1503 -p:RestoreUseSkipNonexistentTargets=false - # Final copy for building COPY . . #run dos2unix on tgs.docker.sh so we can build without issue on windows -RUN dos2unix build/tgs.docker.sh build/RemoveUnsupportedRuntimes.sh +RUN dos2unix build/tgs.docker.sh build/RemoveUnsupportedRuntimes.sh && dotnet restore WORKDIR /repo/src/Tgstation.Server.Host.Console RUN dotnet publish -c Release -o /app \ @@ -64,7 +49,7 @@ RUN dotnet publish -c Release -o /app/lib/Default \ && build/RemoveUnsupportedRuntimes.sh /app/lib/Default \ && mv /app/lib/Default/appsettings* /app -FROM mcr.microsoft.com/dotnet/aspnet:6.0-bookworm-slim +FROM mcr.microsoft.com/dotnet/aspnet:8.0-bookworm-slim #needed for byond RUN apt-get update \ diff --git a/build/NugetCommon.props b/build/NugetCommon.props index d1c341efd00..f07a00a656b 100644 --- a/build/NugetCommon.props +++ b/build/NugetCommon.props @@ -25,7 +25,7 @@ - + diff --git a/build/SrcCommon.props b/build/SrcCommon.props index fb1f8c29d5c..d6e835dad08 100644 --- a/build/SrcCommon.props +++ b/build/SrcCommon.props @@ -17,7 +17,7 @@ - + all runtime; build; native; contentfiles; analyzers; buildtransitive diff --git a/build/TestCommon.props b/build/TestCommon.props index 234e06735a4..b68ecb34159 100644 --- a/build/TestCommon.props +++ b/build/TestCommon.props @@ -13,7 +13,7 @@ runtime; build; native; contentfiles; analyzers; buildtransitive - + diff --git a/build/Version.props b/build/Version.props index 950f68e7a1c..c27dc59c942 100644 --- a/build/Version.props +++ b/build/Version.props @@ -3,21 +3,21 @@ - 5.18.2 - 4.7.1 - 9.14.0 + 6.0.0 + 5.0.0 + 10.0.0 7.0.0 - 12.2.0 - 14.2.0 - 6.7.0 + 13.0.0 + 15.0.0 + 7.0.0 5.7.0 - 1.4.0 + 1.4.1 1.2.1 - 1.0.2 + 2.0.0 netstandard2.0 - 6 + 8 - https://dotnetcli.azureedge.net/dotnet/aspnetcore/Runtime/6.0.24/dotnet-hosting-6.0.24-win.exe + https://download.visualstudio.microsoft.com/download/pr/2a7ae819-fbc4-4611-a1ba-f3b072d4ea25/32f3b931550f7b315d9827d564202eeb/dotnet-hosting-8.0.0-win.exe 10.11.6 1.22.21 diff --git a/build/WebpanelVersion.props b/build/WebpanelVersion.props index 3e3a8aa08cb..8b9090a8059 100644 --- a/build/WebpanelVersion.props +++ b/build/WebpanelVersion.props @@ -1,6 +1,6 @@ - 5.1.0 + 5.3.0 diff --git a/build/analyzers.ruleset b/build/analyzers.ruleset index 0f86cbd400e..e61e800d008 100644 --- a/build/analyzers.ruleset +++ b/build/analyzers.ruleset @@ -1,4 +1,4 @@ - + @@ -88,7 +88,6 @@ - @@ -666,6 +665,12 @@ + + + + + + @@ -715,8 +720,8 @@ - - + + @@ -751,6 +756,8 @@ + + @@ -952,8 +959,8 @@ - - + + @@ -1026,8 +1033,6 @@ - - @@ -1043,4 +1048,4 @@ - + \ No newline at end of file diff --git a/build/package/deb/build_package.sh b/build/package/deb/build_package.sh index 626b48e4bb5..0732f96486e 100755 --- a/build/package/deb/build_package.sh +++ b/build/package/deb/build_package.sh @@ -1,5 +1,5 @@ #!/bin/bash -# Run from git root +# Run from git root, certified for ubuntu only since that's what gha uses SCRIPT_DIR=$( cd -- "$( dirname -- "${BASH_SOURCE[0]}" )" &> /dev/null && pwd ) @@ -8,7 +8,31 @@ set -x dpkg --add-architecture i386 apt-get update -apt-get install -y npm dotnet-sdk-6.0 build-essential binutils lintian debhelper dh-make devscripts xmlstarlet # needs cleanup probably, SO copypasta +# This package set needs cleanup probably, StackOverflow copypasta +apt-get install -y \ + build-essential \ + binutils \ + lintian \ + debhelper \ + dh-make \ + devscripts \ + ca-certificates \ + curl \ + gnupg \ + xmlstarlet + +declare repo_version=$(if command -v lsb_release &> /dev/null; then lsb_release -r -s; else grep -oP '(?<=^VERSION_ID=).+' /etc/os-release | tr -d '"'; fi) +curl -L https://packages.microsoft.com/config/ubuntu/$repo_version/packages-microsoft-prod.deb -o packages-microsoft-prod.deb +dpkg -i ./packages-microsoft-prod.deb +rm packages-microsoft-prod.deb + +# https://github.com/nodesource/distributions +mkdir -p /etc/apt/keyrings +curl -fsSL https://deb.nodesource.com/gpgkey/nodesource-repo.gpg.key | gpg --dearmor -o /etc/apt/keyrings/nodesource.gpg +export NODE_MAJOR=20 +echo "deb [signed-by=/etc/apt/keyrings/nodesource.gpg] https://deb.nodesource.com/node_$NODE_MAJOR.x nodistro main" | tee /etc/apt/sources.list.d/nodesource.list +apt-get update +apt-get install nodejs dotnet-sdk-8.0 -y CURRENT_COMMIT=$(git rev-parse HEAD) @@ -47,14 +71,13 @@ set +e if [[ -z "$PACKAGING_KEYGRIP" ]]; then dpkg-buildpackage --no-sign + EXIT_CODE=$? else dpkg-buildpackage --sign-key=$PACKAGING_KEYGRIP --sign-command="$SIGN_COMMAND" + cat /tmp/tgs_wrap_gpg_output.log + EXIT_CODE=$? fi -EXIT_CODE=$? - -cat /tmp/tgs_wrap_gpg_output.log - cd .. exit $EXIT_CODE diff --git a/build/package/deb/debian/control b/build/package/deb/debian/control index a4e47829c70..f0a7ecbbe3f 100644 --- a/build/package/deb/debian/control +++ b/build/package/deb/debian/control @@ -5,8 +5,8 @@ Maintainer: Jordan Dominion Rules-Requires-Root: no Build-Depends: debhelper-compat (= 13), - dotnet-sdk-6.0, - npm, + nodejs, + dotnet-sdk-8.0, Standards-Version: 4.6.2 Homepage: https://tgstation.github.io/tgstation-server Vcs-Browser: https://github.com/tgstation/tgstation-server @@ -16,7 +16,7 @@ Package: tgstation-server Architecture: any Depends: ${misc:Depends}, - aspnetcore-runtime-6.0, + aspnetcore-runtime-8.0, libc6-i386, libstdc++6:i386 [amd64], libstdc++6 [i386], diff --git a/build/package/deb/debian/postinst b/build/package/deb/debian/postinst index a9ce7b96961..2b98bd204c9 100755 --- a/build/package/deb/debian/postinst +++ b/build/package/deb/debian/postinst @@ -1,9 +1,16 @@ #!/bin/sh -e +if [ -z "$2" ]; then + adduser --system tgstation-server + mkdir -m 754 -p /var/log/tgstation-server + chown -R tgstation-server /etc/tgstation-server + chown -R tgstation-server /opt/tgstation-server/lib + chown -R tgstation-server /var/log/tgstation-server +fi + #DEBHELPER# if [ -z "$2" ]; then - chmod 600 /etc/tgstation-server deb-systemd-helper stop 'tgstation-server.service' >/dev/null || true echo " _ _ _ _ " diff --git a/build/package/deb/debian/rules b/build/package/deb/debian/rules index a68a2cd7d78..7a13e96dd26 100755 --- a/build/package/deb/debian/rules +++ b/build/package/deb/debian/rules @@ -3,23 +3,23 @@ export DH_VERBOSE = 1 %: - dh $@ + dh $@ override_dh_auto_clean: - rm -rf artifacts - dotnet clean -c ReleaseNoWindows + rm -rf artifacts + 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 - rm artifacts/lib/Default/appsettings.yml - build/RemoveUnsupportedRuntimes.sh artifacts/lib/Default - build/RemoveUnsupportedRuntimes.sh artifacts + 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 + rm artifacts/lib/Default/appsettings.yml + build/RemoveUnsupportedRuntimes.sh artifacts/lib/Default + build/RemoveUnsupportedRuntimes.sh artifacts override_dh_auto_install: - cp build/package/deb/MakeInstall ./Makefile - dh_auto_install -- + cp build/package/deb/MakeInstall ./Makefile + dh_auto_install -- override_dh_strip: @@ -27,3 +27,13 @@ override_dh_shlibdeps: override_dh_installsystemd: dh_installsystemd -v --restart-after-upgrade + +override_dh_fixperms: + dh_fixperms + find debian/tgstation-server/opt/tgstation-server -exec chmod 544 {} + + find debian/tgstation-server/opt/tgstation-server -type d -exec chmod 555 {} + + find debian/tgstation-server/opt/tgstation-server/lib -exec chmod 744 {} + + find debian/tgstation-server/opt/tgstation-server/lib -type d -exec chmod 755 {} + + find debian/tgstation-server/etc/tgstation-server -exec chmod 644 {} + + find debian/tgstation-server/etc/tgstation-server -type d -exec chmod 755 {} + + chmod 640 debian/tgstation-server/etc/tgstation-server/appsettings.Production.yml diff --git a/build/package/deb/tgs-configure b/build/package/deb/tgs-configure index 96186f68b07..a4bfd3e5f4c 100755 --- a/build/package/deb/tgs-configure +++ b/build/package/deb/tgs-configure @@ -1,5 +1,3 @@ #!/bin/sh -cd /opt/tgstation-server -export General__SetupWizardMode=Only -exec /usr/bin/dotnet /opt/tgstation-server/lib/Default/Tgstation.Server.Host.dll /tmp/tgs_temp_should_not_be_used --appsettings-base-path=/etc/tgstation-server +exec su -s /bin/sh -c "cd /opt/tgstation-server && export General__SetupWizardMode=Only && exec /usr/bin/dotnet /opt/tgstation-server/lib/Default/Tgstation.Server.Host.dll /tmp/tgs_temp_should_not_be_used --appsettings-base-path=/etc/tgstation-server" tgstation-server diff --git a/build/package/winget/manifest/Tgstation.Server.installer.yaml b/build/package/winget/manifest/Tgstation.Server.installer.yaml index 687528b9273..5678bf542a3 100644 --- a/build/package/winget/manifest/Tgstation.Server.installer.yaml +++ b/build/package/winget/manifest/Tgstation.Server.installer.yaml @@ -24,7 +24,7 @@ Installers: Publisher: /tg/station 13 Dependencies: PackageDependencies: - - PackageIdentifier: Microsoft.DotNet.HostingBundle.6 + - PackageIdentifier: Microsoft.DotNet.HostingBundle.8 ReleaseDate: 2023-06-24 # Do not change. Set before publish by push_manifest.ps1 ManifestType: installer ManifestVersion: 1.5.0 diff --git a/build/tgstation-server.service b/build/tgstation-server.service index 83e8e47793d..6f3c087584b 100644 --- a/build/tgstation-server.service +++ b/build/tgstation-server.service @@ -7,6 +7,7 @@ After=postgresql.service After=mssql-server.service [Service] +User=tgstation-server Type=notify-reload NotifyAccess=all WorkingDirectory=/opt/tgstation-server diff --git a/docs/API.dox b/docs/API.dox index a859e36c47e..b4c6114cc6b 100644 --- a/docs/API.dox +++ b/docs/API.dox @@ -72,7 +72,6 @@ TGS will only every return the response codes listed here - 410: Gone. Attempted to access/modify a resource that ideally should have been ready, but isn't or no longer is - 422: Unprocessable Entity: Used specifically when an operation that requires a server restart is unable to be performed due to the @ref Tgstation.Server.Host.Watchdog not being present in the deployment. Should not happen with a proper server configuration. Response body contains an @ref Tgstation.Server.Api.Models.ErrorMessage - 424: Failed Dependency: When a request that depends on an external API fails for a reason other than rate limiting. The response body will contain an @ref Tgstation.Server.Api.Models.ErrorMessage model detailing the error. -- 426: Upgrade required: Used when the client's API version is not compatible with the server's. Response body contains an @ref Tgstation.Server.Api.Models.ErrorMessage - 429: Rate limited. Used with operations that rely on GitHub.com. If a rate limit is hit for an operation this will be returned. Response will contain a Retry-After header with the amount of seconds to wait. - 500: Server error. Please report the request and response body to the code repository - 501: Not implemented. Functionality not available in the current server version @@ -115,13 +114,13 @@ If the provided credentials are valid and your user account is enabled you will } @endcode -If your account is disabled, you will recieve a 403 response. +If your account is disabled, you will receive a 403 response. You may recognize the bearer value as a Json Web Token. This is a secure representation of your identity to the server. It expires after a set period of time or until your password changes. It must be present for requests made to all other APIs. To do so add the following header to your other requests - Authorization:Bearer `` -Continue to use this token until you begin to recieve 401 responses from the API. Then repeat the process to get a new one if your credentials are still valid +Continue to use this token until you begin to receive 401 responses from the API. Then repeat the process to get a new one if your credentials are still valid @subsection api_auth_o OAuth 2.0 @@ -468,7 +467,7 @@ I POST "/Config" @ref Tgstation.Server.Api.Models.ConfigurationFile => @ref Tgst When creating a file, only @ref Tgstation.Server.Api.Models.ConfigurationFile.Path and @ref Tgstation.Server.Api.Models.ConfigurationFile.Content should be specified -If the file already exists, the @ref Tgstation.Server.Api.Models.ConfigurationFile.LastReadHash field must also be present with the last version recieved from the server for that file. If this does not match at the time of the request, 409 will be returned, indicating the file has changed since it was last viewed by the client. +If the file already exists, the @ref Tgstation.Server.Api.Models.ConfigurationFile.LastReadHash field must also be present with the last version received from the server for that file. If this does not match at the time of the request, 409 will be returned, indicating the file has changed since it was last viewed by the client. To delete a file set @ref Tgstation.Server.Api.Models.ConfigurationFile.Content to null in the request. diff --git a/global.json b/global.json index e866df76d5a..9c97a328789 100644 --- a/global.json +++ b/global.json @@ -1,6 +1,6 @@ { "sdk": { - "version": "6.0.0", + "version": "8.0.0", "rollForward": "latestMajor", "allowPrerelease": false } diff --git a/src/DMAPI/tgs.dm b/src/DMAPI/tgs.dm index b0e97e05e9b..c6596ea46c1 100644 --- a/src/DMAPI/tgs.dm +++ b/src/DMAPI/tgs.dm @@ -1,6 +1,6 @@ // tgstation-server DMAPI -#define TGS_DMAPI_VERSION "6.7.0" +#define TGS_DMAPI_VERSION "7.0.0" // All functions and datums outside this document are subject to change with any version and should not be relied on. @@ -73,12 +73,12 @@ #define TGS_EVENT_REPO_MERGE_PULL_REQUEST 3 /// Before the repository makes a sychronize operation. Parameters: Absolute repostiory path. #define TGS_EVENT_REPO_PRE_SYNCHRONIZE 4 -/// Before a BYOND install operation begins. Parameters: [/datum/tgs_version] of the installing BYOND. -#define TGS_EVENT_BYOND_INSTALL_START 5 -/// When a BYOND install operation fails. Parameters: Error message -#define TGS_EVENT_BYOND_INSTALL_FAIL 6 -/// When the active BYOND version changes. Parameters: (Nullable) [/datum/tgs_version] of the current BYOND, [/datum/tgs_version] of the new BYOND. -#define TGS_EVENT_BYOND_ACTIVE_VERSION_CHANGE 7 +/// Before a engine install operation begins. Parameters: Version string of the installing engine. +#define TGS_EVENT_ENGINE_INSTALL_START 5 +/// When a engine install operation fails. Parameters: Error message +#define TGS_EVENT_ENGINE_INSTALL_FAIL 6 +/// When the active engine version changes. Parameters: (Nullable) Version string of the current engine, version string of the new engine. +#define TGS_EVENT_ENGINE_ACTIVE_VERSION_CHANGE 7 /// When the compiler starts running. Parameters: Game directory path, origin commit SHA. #define TGS_EVENT_COMPILE_START 8 /// When a compile is cancelled. No parameters. @@ -108,7 +108,7 @@ // #define TGS_EVENT_DREAM_DAEMON_LAUNCH 22 /// After a single submodule update is performed. Parameters: Updated submodule name. #define TGS_EVENT_REPO_SUBMODULE_UPDATE 23 -/// After CodeModifications are applied, before DreamMaker is run. Parameters: Game directory path, origin commit sha, byond version. +/// After CodeModifications are applied, before DreamMaker is run. Parameters: Game directory path, origin commit sha, version string of the used engine. #define TGS_EVENT_PRE_DREAM_MAKER 24 /// Whenever a deployment folder is deleted from disk. Parameters: Game directory path. #define TGS_EVENT_DEPLOYMENT_CLEANUP 25 @@ -122,6 +122,7 @@ /// The watchdog will restart on reboot. #define TGS_REBOOT_MODE_RESTART 2 +// Note that security levels are currently meaningless in OpenDream /// DreamDaemon Trusted security level. #define TGS_SECURITY_TRUSTED 0 /// DreamDaemon Safe security level. @@ -136,6 +137,11 @@ /// DreamDaemon invisible visibility level. #define TGS_VISIBILITY_INVISIBLE 2 +/// The Build Your Own Net Dream engine. +#define TGS_ENGINE_TYPE_BYOND 0 +/// The OpenDream engine. +#define TGS_ENGINE_TYPE_OPENDREAM 1 + //REQUIRED HOOKS /** @@ -449,6 +455,10 @@ /world/proc/TgsVersion() return +/// Returns the running engine type +/world/proc/TgsEngine() + return + /// Returns the current [/datum/tgs_version] of the DMAPI being used if it was activated, null otherwise. This function may sleep if the call to [/world/proc/TgsNew] is sleeping! /world/proc/TgsApiVersion() return diff --git a/src/DMAPI/tgs/core/core.dm b/src/DMAPI/tgs/core/core.dm index b9a9f27a28a..8be96f27404 100644 --- a/src/DMAPI/tgs/core/core.dm +++ b/src/DMAPI/tgs/core/core.dm @@ -42,11 +42,11 @@ var/datum/tgs_version/max_api_version = TgsMaximumApiVersion(); if(version.suite != null && version.minor != null && version.patch != null && version.deprecated_patch != null && version.deprefixed_parameter > max_api_version.deprefixed_parameter) - TGS_ERROR_LOG("Detected unknown API version! Defaulting to latest. Update the DMAPI to fix this problem.") + TGS_ERROR_LOG("Detected unknown Interop API version! Defaulting to latest. Update the DMAPI to fix this problem.") api_datum = /datum/tgs_api/latest if(!api_datum) - TGS_ERROR_LOG("Found unsupported API version: [raw_parameter]. If this is a valid version please report this, backporting is done on demand.") + TGS_ERROR_LOG("Found unsupported Interop API version: [raw_parameter]. If this is a valid version please report this, backporting is done on demand.") return TGS_INFO_LOG("Activating API for version [version.deprefixed_parameter]") @@ -107,6 +107,13 @@ if(api) return api.ApiVersion() +/world/TgsEngine() +#ifdef OPENDREAM + return TGS_ENGINE_TYPE_OPENDREAM +#else + return TGS_ENGINE_TYPE_BYOND +#endif + /world/TgsInstanceName() var/datum/tgs_api/api = TGS_READ_GLOBAL(tgs) if(api) diff --git a/src/DMAPI/tgs/v5/_defines.dm b/src/DMAPI/tgs/v5/_defines.dm index 48969c0c7d5..1c7d67d20cd 100644 --- a/src/DMAPI/tgs/v5/_defines.dm +++ b/src/DMAPI/tgs/v5/_defines.dm @@ -8,7 +8,6 @@ #define DMAPI5_TOPIC_REQUEST_LIMIT 65528 #define DMAPI5_TOPIC_RESPONSE_LIMIT 65529 -#define DMAPI5_BRIDGE_COMMAND_PORT_UPDATE 0 #define DMAPI5_BRIDGE_COMMAND_STARTUP 1 #define DMAPI5_BRIDGE_COMMAND_PRIME 2 #define DMAPI5_BRIDGE_COMMAND_REBOOT 3 @@ -18,6 +17,7 @@ #define DMAPI5_PARAMETER_ACCESS_IDENTIFIER "accessIdentifier" #define DMAPI5_PARAMETER_CUSTOM_COMMANDS "customCommands" +#define DMAPI5_PARAMETER_TOPIC_PORT "topicPort" #define DMAPI5_CHUNK "chunk" #define DMAPI5_CHUNK_PAYLOAD "payload" diff --git a/src/DMAPI/tgs/v5/api.dm b/src/DMAPI/tgs/v5/api.dm index 7226f29bba6..25d49b3e3bd 100644 --- a/src/DMAPI/tgs/v5/api.dm +++ b/src/DMAPI/tgs/v5/api.dm @@ -17,6 +17,8 @@ var/list/chat_channels var/initialized = FALSE + var/initial_bridge_request_received = FALSE + var/datum/tgs_version/interop_version var/chunked_requests = 0 var/list/chunked_topics = list() @@ -25,7 +27,8 @@ /datum/tgs_api/v5/New() . = ..() - TGS_DEBUG_LOG("V5 API created") + interop_version = version + TGS_DEBUG_LOG("V5 API created: [json_encode(args)]") /datum/tgs_api/v5/ApiVersion() return new /datum/tgs_version( @@ -38,8 +41,8 @@ access_identifier = world.params[DMAPI5_PARAM_ACCESS_IDENTIFIER] var/datum/tgs_version/api_version = ApiVersion() - version = null - var/list/bridge_response = Bridge(DMAPI5_BRIDGE_COMMAND_STARTUP, list(DMAPI5_BRIDGE_PARAMETER_MINIMUM_SECURITY_LEVEL = minimum_required_security_level, DMAPI5_BRIDGE_PARAMETER_VERSION = api_version.raw_parameter, DMAPI5_PARAMETER_CUSTOM_COMMANDS = ListCustomCommands())) + version = null // we want this to be the TGS version, not the interop version + var/list/bridge_response = Bridge(DMAPI5_BRIDGE_COMMAND_STARTUP, list(DMAPI5_BRIDGE_PARAMETER_MINIMUM_SECURITY_LEVEL = minimum_required_security_level, DMAPI5_BRIDGE_PARAMETER_VERSION = api_version.raw_parameter, DMAPI5_PARAMETER_CUSTOM_COMMANDS = ListCustomCommands(), DMAPI5_PARAMETER_TOPIC_PORT = GetTopicPort())) if(!istype(bridge_response)) TGS_ERROR_LOG("Failed initial bridge request!") return FALSE @@ -53,7 +56,8 @@ TGS_INFO_LOG("DMAPI validation, exiting...") TerminateWorld() - version = new /datum/tgs_version(runtime_information[DMAPI5_RUNTIME_INFORMATION_SERVER_VERSION]) + initial_bridge_request_received = TRUE + version = new /datum/tgs_version(runtime_information[DMAPI5_RUNTIME_INFORMATION_SERVER_VERSION]) // reassigning this because it can change if TGS updates security_level = runtime_information[DMAPI5_RUNTIME_INFORMATION_SECURITY_LEVEL] visibility = runtime_information[DMAPI5_RUNTIME_INFORMATION_VISIBILITY] instance_name = runtime_information[DMAPI5_RUNTIME_INFORMATION_INSTANCE_NAME] @@ -102,10 +106,17 @@ initialized = TRUE return TRUE +/datum/tgs_api/v5/proc/GetTopicPort() +#if defined(OPENDREAM) && defined(OPENDREAM_TOPIC_PORT_EXISTS) + return "[world.opendream_topic_port]" +#else + return null +#endif + /datum/tgs_api/v5/proc/RequireInitialBridgeResponse() TGS_DEBUG_LOG("RequireInitialBridgeResponse()") var/logged = FALSE - while(!version) + while(!initial_bridge_request_received) if(!logged) TGS_DEBUG_LOG("RequireInitialBridgeResponse: Starting sleep") logged = TRUE diff --git a/src/DMAPI/tgs/v5/bridge.dm b/src/DMAPI/tgs/v5/bridge.dm index 37f58bcdf63..60cbcbfb7df 100644 --- a/src/DMAPI/tgs/v5/bridge.dm +++ b/src/DMAPI/tgs/v5/bridge.dm @@ -48,7 +48,9 @@ var/json = CreateBridgeData(command, data, TRUE) var/encoded_json = url_encode(json) - var/url = "http://127.0.0.1:[server_port]/Bridge?[DMAPI5_BRIDGE_DATA]=[encoded_json]" + var/api_prefix = interop_version.minor >= 7 ? "api/" : "" + + var/url = "http://127.0.0.1:[server_port]/[api_prefix]Bridge?[DMAPI5_BRIDGE_DATA]=[encoded_json]" return url /datum/tgs_api/v5/proc/CreateBridgeData(command, list/data, needs_auth) @@ -81,11 +83,16 @@ TGS_ERROR_LOG("Failed bridge request: [bridge_request]") return - var/response_json = file2text(export_response["CONTENT"]) - if(!response_json) + var/content = export_response["CONTENT"] + if(!content) TGS_ERROR_LOG("Failed bridge request, missing content!") return + var/response_json = file2text(content) + if(!response_json) + TGS_ERROR_LOG("Failed bridge request, failed to load content!") + return + var/list/bridge_response = json_decode(response_json) if(!bridge_response) TGS_ERROR_LOG("Failed bridge request, bad json: [response_json]") diff --git a/src/DMAPI/tgs/v5/topic.dm b/src/DMAPI/tgs/v5/topic.dm index 2ef0c70a97f..05e6c4e1b21 100644 --- a/src/DMAPI/tgs/v5/topic.dm +++ b/src/DMAPI/tgs/v5/topic.dm @@ -175,6 +175,7 @@ var/list/reattach_response = TopicResponse(error_message) reattach_response[DMAPI5_PARAMETER_CUSTOM_COMMANDS] = ListCustomCommands() + reattach_response[DMAPI5_PARAMETER_TOPIC_PORT] = GetTopicPort() return reattach_response if(DMAPI5_TOPIC_COMMAND_SEND_CHUNK) diff --git a/src/DMAPI/tgs/v5/undefs.dm b/src/DMAPI/tgs/v5/undefs.dm index fd1ed7e4cf5..d531d4b7b9d 100644 --- a/src/DMAPI/tgs/v5/undefs.dm +++ b/src/DMAPI/tgs/v5/undefs.dm @@ -8,7 +8,6 @@ #undef DMAPI5_TOPIC_REQUEST_LIMIT #undef DMAPI5_TOPIC_RESPONSE_LIMIT -#undef DMAPI5_BRIDGE_COMMAND_PORT_UPDATE #undef DMAPI5_BRIDGE_COMMAND_STARTUP #undef DMAPI5_BRIDGE_COMMAND_PRIME #undef DMAPI5_BRIDGE_COMMAND_REBOOT @@ -18,6 +17,7 @@ #undef DMAPI5_PARAMETER_ACCESS_IDENTIFIER #undef DMAPI5_PARAMETER_CUSTOM_COMMANDS +#undef DMAPI5_PARAMETER_TOPIC_PORT #undef DMAPI5_CHUNK #undef DMAPI5_CHUNK_PAYLOAD diff --git a/src/Tgstation.Server.Api/ApiHeaders.cs b/src/Tgstation.Server.Api/ApiHeaders.cs index 01e61cb373b..fad13a03305 100644 --- a/src/Tgstation.Server.Api/ApiHeaders.cs +++ b/src/Tgstation.Server.Api/ApiHeaders.cs @@ -287,9 +287,7 @@ void AddError(HeaderErrorTypes headerType, string message) try { -#pragma warning disable CS0618 // Type or member is obsolete - Token.ExpiresAt = Token.ParseJwt().ValidTo; -#pragma warning restore CS0618 // Type or member is obsolete + Token.ParseJwt(); } catch (ArgumentException ex) when (ex is not ArgumentNullException) { diff --git a/src/Tgstation.Server.Api/Extensions/JobCodeExtensions.cs b/src/Tgstation.Server.Api/Extensions/JobCodeExtensions.cs new file mode 100644 index 00000000000..90c379f8245 --- /dev/null +++ b/src/Tgstation.Server.Api/Extensions/JobCodeExtensions.cs @@ -0,0 +1,25 @@ +using System; + +using Tgstation.Server.Api.Models; + +namespace Tgstation.Server.Api.Extensions +{ + /// + /// Extension methods for the . + /// + public static class JobCodeExtensions + { + /// + /// If a given can be triggered by TGS startup. + /// + /// The . + /// if the can trigger before startup, otherwise. + public static bool IsServerStartupJob(this JobCode jobCode) + => jobCode switch + { + JobCode.Unknown or JobCode.Move or JobCode.RepositoryClone or JobCode.RepositoryUpdate or JobCode.RepositoryAutoUpdate or JobCode.RepositoryDelete or JobCode.EngineOfficialInstall or JobCode.EngineCustomInstall or JobCode.EngineDelete or JobCode.Deployment or JobCode.AutomaticDeployment or JobCode.WatchdogLaunch or JobCode.WatchdogRestart or JobCode.WatchdogDump => false, + JobCode.StartupWatchdogLaunch or JobCode.StartupWatchdogReattach or JobCode.ReconnectChatBot => true, + _ => throw new InvalidOperationException($"Invalid JobCode: {jobCode}"), + }; + } +} diff --git a/src/Tgstation.Server.Api/Models/ChatChannel.cs b/src/Tgstation.Server.Api/Models/ChatChannel.cs index 07b643fcab5..3192603e2b0 100644 --- a/src/Tgstation.Server.Api/Models/ChatChannel.cs +++ b/src/Tgstation.Server.Api/Models/ChatChannel.cs @@ -1,5 +1,4 @@ -using System; -using System.ComponentModel.DataAnnotations; +using System.ComponentModel.DataAnnotations; using Tgstation.Server.Api.Models.Internal; @@ -11,28 +10,12 @@ namespace Tgstation.Server.Api.Models public class ChatChannel : ChatChannelBase { /// - /// The channel identifier. Supercedes and . + /// The channel identifier. /// For , it's the IRC channel name and optional password colon separated. /// For , it's the stringified Discord channel snowflake. /// [Required] [StringLength(Limits.MaximumIndexableStringLength, MinimumLength = 1)] public string? ChannelData { get; set; } - - /// - /// The IRC channel name. Also potentially contains the channel passsword (if separated by a colon). - /// If multiple copies of the same channel with different keys are added to the server, the one that will be used is undefined. - /// - [ResponseOptions] - [StringLength(Limits.MaximumIndexableStringLength, MinimumLength = 1)] - [Obsolete($"Use {nameof(ChannelData)}")] - public string? IrcChannel { get; set; } - - /// - /// The Discord channel ID. - /// - [Obsolete($"Use {nameof(ChannelData)}")] - [ResponseOptions] - public ulong? DiscordChannelId { get; set; } } } diff --git a/src/Tgstation.Server.Api/Models/DiscordConnectionStringBuilder.cs b/src/Tgstation.Server.Api/Models/DiscordConnectionStringBuilder.cs index 2f954d4e808..0da207aa97c 100644 --- a/src/Tgstation.Server.Api/Models/DiscordConnectionStringBuilder.cs +++ b/src/Tgstation.Server.Api/Models/DiscordConnectionStringBuilder.cs @@ -19,12 +19,6 @@ public sealed class DiscordConnectionStringBuilder : ChatConnectionStringBuilder /// See https://discordapp.com/developers/docs/topics/oauth2#bots public string? BotToken { get; set; } - /// - /// to enable based mode. Will auto reply with a youtube link to a video that says "based on the hardware that's installed in it" to anyone saying 'based on what?' case-insensitive. - /// - [Obsolete("Will be removed in next major TGS version")] - public bool BasedMeme { get; set; } - /// /// If the tgstation-server logo is shown in deployment embeds. /// @@ -35,6 +29,11 @@ public sealed class DiscordConnectionStringBuilder : ChatConnectionStringBuilder /// public DiscordDMOutputDisplayType DMOutputDisplay { get; set; } + /// + /// Currently unused. Note its origin in based meme before repurposing. + /// + readonly bool unusedFlag; + /// /// Initializes a new instance of the class. /// @@ -60,10 +59,7 @@ public DiscordConnectionStringBuilder(string connectionString) DMOutputDisplay = dMOutputDisplayType; if (splits.Length > 2 && Int32.TryParse(splits[2], out Int32 basedMeme)) -#pragma warning disable CS0618 // Type or member is obsolete - BasedMeme = Convert.ToBoolean(basedMeme); - else - BasedMeme = false; // oranges said this needs to be true by default :pensive: + unusedFlag = Convert.ToBoolean(basedMeme); if (splits.Length > 3 && Int32.TryParse(splits[3], out Int32 branding)) DeploymentBranding = Convert.ToBoolean(branding); @@ -72,7 +68,6 @@ public DiscordConnectionStringBuilder(string connectionString) } /// - public override string ToString() => $"{BotToken};{(int)DMOutputDisplay};{Convert.ToInt32(BasedMeme)};{Convert.ToInt32(DeploymentBranding)}"; -#pragma warning restore CS0618 // Type or member is obsolete + public override string ToString() => $"{BotToken};{(int)DMOutputDisplay};{Convert.ToInt32(unusedFlag)};{Convert.ToInt32(DeploymentBranding)}"; } } diff --git a/src/Tgstation.Server.Api/Models/EngineType.cs b/src/Tgstation.Server.Api/Models/EngineType.cs new file mode 100644 index 00000000000..df4b3ab3aa2 --- /dev/null +++ b/src/Tgstation.Server.Api/Models/EngineType.cs @@ -0,0 +1,18 @@ +namespace Tgstation.Server.Api.Models +{ + /// + /// The type of engine the codebase is using. + /// + public enum EngineType + { + /// + /// Build your own net dream. + /// + Byond, + + /// + /// The OpenDream BYOND reimplementation. + /// + OpenDream, + } +} diff --git a/src/Tgstation.Server.Api/Models/EngineVersion.cs b/src/Tgstation.Server.Api/Models/EngineVersion.cs new file mode 100644 index 00000000000..adaa90bcbf0 --- /dev/null +++ b/src/Tgstation.Server.Api/Models/EngineVersion.cs @@ -0,0 +1,194 @@ +using System; +using System.ComponentModel.DataAnnotations; +using System.Diagnostics; +using System.Linq; + +using Tgstation.Server.Common.Extensions; + +namespace Tgstation.Server.Api.Models +{ + /// + /// Information about an engine installation. + /// + public sealed class EngineVersion : IEquatable + { + /// + /// The . + /// + [RequestOptions(FieldPresence.Required)] + public EngineType? Engine { get; set; } + + /// + /// The of the engine. Currently only valid when is . + /// + [ResponseOptions] + public Version? Version { get; set; } + + /// + /// The git commit SHA of the engine. Currently only valid when is . + /// + [ResponseOptions] + [StringLength(Limits.MaximumCommitShaLength, MinimumLength = Limits.MaximumCommitShaLength)] + public string? SourceSHA { get; set; } + + /// + /// The revision of the custom build. + /// + [ResponseOptions] + public int? CustomIteration { get; set; } + + /// + /// Attempts to parse a stringified . + /// + /// The input . + /// The output . + /// if parsing was successful, otherwise. + public static bool TryParse(string input, out EngineVersion? engineVersion) + { + if (input == null) + throw new ArgumentNullException(nameof(input)); + + var splits = input.Split(new char[] { '-' }, StringSplitOptions.RemoveEmptyEntries); + engineVersion = null; + + if (splits.Length > 3) + return false; + + EngineType engine; + var hasPrefix = splits.Length > 1; + if (hasPrefix) + { + if (!Enum.TryParse(splits[0], out engine)) + return false; + } + else + engine = EngineType.Byond; + + Version? version; + string? sha; + int? customRev = null; + if (engine == EngineType.Byond) + { + if (!Version.TryParse(splits.Last(), out version)) + return false; + + if (version.Build > 0) + { + customRev = version.Build; + version = new Version(version.Major, version.Minor); + } + + sha = null; + } + else + { + Debug.Assert(engine == EngineType.OpenDream, "This does not support whatever ungodly new engine you've added"); + + var shaIndex = hasPrefix ? 1 : 0; + sha = splits[shaIndex]; + if (sha.Length != Limits.MaximumCommitShaLength) + return false; + + version = null; + + if (splits.Length - 1 > shaIndex) + { + if (!Int32.TryParse(splits.Last(), out var customRevResult)) + return false; + + customRev = customRevResult; + } + } + + engineVersion = new EngineVersion + { + Engine = engine, + Version = version, + SourceSHA = sha, + CustomIteration = customRev, + }; + return true; + } + + /// + /// Parses a stringified . + /// + /// The input . + /// The output . + /// If the is not a valid stringified . + public static EngineVersion Parse(string input) + { + if (input == null) + throw new ArgumentNullException(nameof(input)); + + if (TryParse(input, out var engineVersion)) + return engineVersion!; + + throw new InvalidOperationException($"Invalid engine version: {input}"); + } + + /// + /// Initializes a new instance of the class. + /// + public EngineVersion() + { + } + + /// + /// Initializes a new instance of the class. + /// + /// The to copy. + public EngineVersion(EngineVersion other) + { + if (other == null) + throw new ArgumentNullException(nameof(other)); + + Version = other.Version; + Engine = other.Engine; + SourceSHA = other.SourceSHA; + CustomIteration = other.CustomIteration; + } + + /// + public bool Equals(EngineVersion other) + { + // https://github.com/dotnet/roslyn-analyzers/issues/2875 +#pragma warning disable CA1062 // Validate arguments of public methods + return other!.Version?.Semver() == Version?.Semver() + && other.Engine == Engine + && (other.SourceSHA == SourceSHA + || (other.SourceSHA != null + && SourceSHA != null + && other.SourceSHA.Equals(SourceSHA, StringComparison.OrdinalIgnoreCase))) + && other.CustomIteration == CustomIteration; +#pragma warning restore CA1062 // Validate arguments of public methods + } + + /// + public override bool Equals(object obj) + => obj is EngineVersion other && Equals(other); + + /// + public override string ToString() + { + var isByond = Engine == EngineType.Byond; + + // BYOND encodes differently for backwards compatibility + var enginePrefix = !isByond + ? $"{Engine}-" + : String.Empty; + var displayedVersion = isByond + ? (CustomIteration.HasValue + ? new Version(Version!.Major, Version.Minor, CustomIteration.Value) + : Version!).ToString() + : SourceSHA; + var displayedCustomIteration = !isByond && CustomIteration.HasValue + ? $"-{CustomIteration}" + : String.Empty; + return $"{enginePrefix}{displayedVersion}{displayedCustomIteration}"; + } + + /// + public override int GetHashCode() => ToString().GetHashCode(); + } +} diff --git a/src/Tgstation.Server.Api/Models/ErrorCode.cs b/src/Tgstation.Server.Api/Models/ErrorCode.cs index 69f4d8dbedc..28f39a76386 100644 --- a/src/Tgstation.Server.Api/Models/ErrorCode.cs +++ b/src/Tgstation.Server.Api/Models/ErrorCode.cs @@ -36,7 +36,7 @@ public enum ErrorCode : uint IOError, /// - /// The failed to validate! + /// The failed to validate. /// [Description("A header validation error occurred!")] BadHeaders, @@ -150,10 +150,10 @@ public enum ErrorCode : uint InstanceLimitReached, /// - /// Attempted to create an with a whitespace . + /// Attempted to create an with a whitespace or . /// - [Description("Instance names cannot be whitespace!")] - InstanceWhitespaceName, + [Description("Instance names and paths cannot be whitespace!")] + InstanceWhitespaceNameOrPath, /// /// The header was required but not set. @@ -228,10 +228,10 @@ public enum ErrorCode : uint RepoMismatchShaAndUpdate, /// - /// Could not delete a BYOND version due to it being set as the active version for the instance. + /// Could not delete a engine version due to it being set as the active version for the instance. /// - [Description("Could not delete BYOND version due to it being selected as the instance's active version.")] - ByondCannotDeleteActiveVersion, + [Description("Could not delete engine version due to it being selected as the instance's active version.")] + EngineCannotDeleteActiveVersion, /// /// contained duplicate s. @@ -312,28 +312,28 @@ public enum ErrorCode : uint ByondDirectXInstallFail, /// - /// Failed to download a given BYOND version. + /// Failed to download a given engine version. /// - [Description("Error downloading specified BYOND version!")] - ByondDownloadFail, + [Description("Error downloading specified engine version!")] + EngineDownloadFail, /// - /// Failed to lock BYOND executables. + /// Failed to lock engine executables. /// - [Description("Could not acquire lock on BYOND installation as none exist!")] - ByondNoVersionsInstalled, + [Description("Could not acquire lock on engine installation as none exist!")] + EngineNoVersionsInstalled, /// - /// The DMAPI never validated itself + /// The DMAPI never validated itself. /// [Description("DMAPI validation failed! See FAQ at https://github.com/tgstation/tgstation-server/discussions/1695")] - DreamMakerNeverValidated, + DeploymentNeverValidated, /// /// The DMAPI sent an invalid validation request. /// [Description("The DMAPI sent an invalid validation request!")] - DreamMakerInvalidValidation, + DeploymentInvalidValidation, /// /// Tried to remove the last for a passwordless user. @@ -345,25 +345,25 @@ public enum ErrorCode : uint /// No .dme could be found for deployment. /// [Description("No .dme configured and could not automatically detect one!")] - DreamMakerNoDme, + DeploymentNoDme, /// /// The configured .dme could not be found. /// [Description("Could not load configured .dme!")] - DreamMakerMissingDme, + DeploymentMissingDme, /// - /// DreamMaker failed to compile. + /// Compiler failed to compile. /// - [Description("DreamMaker exited with a non-zero exit code!")] - DreamMakerExitCode, + [Description("Compiler exited with a non-zero exit code!")] + DeploymentExitCode, /// - /// Deployment already in progress + /// Deployment already in progress. /// [Description("There is already a deployment operation in progress!")] - DreamMakerCompileJobInProgress, + DeploymentInProgress, /// /// Missing in database. @@ -420,15 +420,15 @@ public enum ErrorCode : uint WatchdogCompileJobCorrupted, /// - /// DreamDaemon exited before it finished starting. + /// Game server exited before it finished starting. /// - [Description("DreamDaemon failed to start!")] + [Description("Game server failed to start!")] WatchdogStartupFailed, /// - /// DreamDaemon timed-out before it finished starting. + /// Game server timed-out before it finished starting. /// - [Description("DreamDaemon failed to start within the configured timeout!")] + [Description("Game server failed to start within the configured timeout!")] WatchdogStartupTimeout, /// @@ -468,22 +468,22 @@ public enum ErrorCode : uint InstanceNotAtWhitelistedPath, /// - /// Attempted to make a DreamDaemon update with both and set. + /// Attempted to make a game server update with both and set. /// [Description("Cannot set both softShutdown and softReboot at once!")] - DreamDaemonDoubleSoft, + GameServerDoubleSoft, /// /// Attempted to launch DreamDaemon on a user account that had the BYOND pager running. /// [Description("Cannot start DreamDaemon headless with the BYOND pager running!")] - DeploymentPagerRunning, + DreamDaemonPagerRunning, /// - /// Could not bind to port we wanted to launch DreamDaemon on. + /// Could not bind to port we wanted to launch the game server on. /// - [Description("Could not bind to requested DreamDaemon port! Is there another service running on that port?")] - DreamDaemonPortInUse, + [Description("Could not bind to requested game server port! Is there another service running on that port?")] + GameServerPortInUse, /// /// Failed to post GitHub comments, or send TGS event. @@ -540,16 +540,16 @@ public enum ErrorCode : uint RepoTestMergeInvalidRemote, /// - /// Attempted to switch to a custom BYOND version that does not exist. + /// Attempted to switch to a custom engine version that does not exist. /// - [Description("Cannot switch to requested custom BYOND version as it is not currently installed.")] - ByondNonExistentCustomVersion, + [Description("Cannot switch to requested custom engine version as it is not currently installed.")] + EngineNonExistentCustomVersion, /// - /// Attempted to perform an operation that requires DreamDaemon (not the watchdog) to be running but it wasn't. + /// Attempted to perform an operation that requires server (not the watchdog) to be running but it wasn't. /// - [Description("Cannot perform this operation as DreamDaemon is not currently running!")] - DreamDaemonOffline, + [Description("Cannot perform this operation as the game server is not currently running!")] + GameServerOffline, /// /// Attempted to perform an instance operation with an offline instance. @@ -566,8 +566,8 @@ public enum ErrorCode : uint /// /// Attempt to add DreamDaemon to the list of firewall exempt processes failed. /// - [Description("Failed to allow DreamDaemon through the Windows firewall!")] - ByondDreamDaemonFirewallFail, + [Description("Failed to allow game server through the Windows firewall!")] + EngineFirewallFail, /// /// Attempted to create an instance but no free ports could be found. @@ -600,7 +600,7 @@ public enum ErrorCode : uint FileUploadExpired, /// - /// Tried to update a user to have both a and + /// Tried to update a user to have both a and . /// [Description("A user may not have both a permissionSet and group!")] UserGroupAndPermissionSet, @@ -634,5 +634,17 @@ public enum ErrorCode : uint /// [Description("Could not send broadcast to the DMAPI. This can happen either due to there being an insufficient DMAPI version, a communication failure, or the server being offline.")] BroadcastFailure, + + /// + /// Could not compile OpenDream due to a missing dotnet executable. + /// + [Description("OpenDream could not be compiled due to being unable to locate the dotnet executable!")] + OpenDreamCantFindDotnet, + + /// + /// Could not install OpenDream due to it not meeting the minimum version requirements. + /// + [Description("The specified OpenDream version is too old!")] + OpenDreamTooOld, } } diff --git a/src/Tgstation.Server.Api/Models/FieldPresence.cs b/src/Tgstation.Server.Api/Models/FieldPresence.cs index 11db2cd2cb5..e09ac60975b 100644 --- a/src/Tgstation.Server.Api/Models/FieldPresence.cs +++ b/src/Tgstation.Server.Api/Models/FieldPresence.cs @@ -6,7 +6,7 @@ public enum FieldPresence { /// - /// The field is optional + /// The field is optional. /// Optional, diff --git a/src/Tgstation.Server.Api/Models/Internal/ChatBotApiBase.cs b/src/Tgstation.Server.Api/Models/Internal/ChatBotApiBase.cs index 231a94ffbe5..e490c8e5fb1 100644 --- a/src/Tgstation.Server.Api/Models/Internal/ChatBotApiBase.cs +++ b/src/Tgstation.Server.Api/Models/Internal/ChatBotApiBase.cs @@ -22,10 +22,8 @@ public bool ValidateProviderChannelTypes() return true; return Provider.Value switch { -#pragma warning disable CS0618 - ChatProvider.Discord => Channels?.Select(x => (x.DiscordChannelId.HasValue || ulong.TryParse(x.ChannelData, out _)) && x.IrcChannel == null).All(x => x) ?? true, - ChatProvider.Irc => Channels?.Select(x => !x.DiscordChannelId.HasValue && (x.IrcChannel != null || x.ChannelData != null)).All(x => x) ?? true, -#pragma warning restore CS0618 + ChatProvider.Discord => Channels?.All(x => UInt64.TryParse(x.ChannelData, out _)) ?? true, + ChatProvider.Irc => Channels?.All(x => x.ChannelData != null && x.ChannelData[0] == '#') ?? true, _ => throw new InvalidOperationException("Invalid provider type!"), }; } diff --git a/src/Tgstation.Server.Api/Models/Internal/DreamDaemonApiBase.cs b/src/Tgstation.Server.Api/Models/Internal/DreamDaemonApiBase.cs index 50de44e64b7..6bde7948d03 100644 --- a/src/Tgstation.Server.Api/Models/Internal/DreamDaemonApiBase.cs +++ b/src/Tgstation.Server.Api/Models/Internal/DreamDaemonApiBase.cs @@ -1,7 +1,4 @@ -using System; -using System.ComponentModel.DataAnnotations; - -namespace Tgstation.Server.Api.Models.Internal +namespace Tgstation.Server.Api.Models.Internal { /// /// Base class for DreamDaemon API models. @@ -9,31 +6,21 @@ namespace Tgstation.Server.Api.Models.Internal public abstract class DreamDaemonApiBase : DreamDaemonSettings { /// - /// If the server is undergoing a soft reset. This may be automatically set by changes to other fields. + /// An incrementing ID for representing current server execution. /// [ResponseOptions] - public bool? SoftRestart { get; set; } + public long? SessionId { get; set; } /// - /// If the server is undergoing a soft shutdown. - /// - [ResponseOptions] - public bool? SoftShutdown { get; set; } - - /// - /// Deprecated, use . + /// If the server is undergoing a soft reset. This may be automatically set by changes to other fields. /// - [Required] [ResponseOptions] - [Obsolete("Use HealthCheckSeconds")] - public uint? HeartbeatSeconds { get; set; } + public bool? SoftRestart { get; set; } /// - /// Deprecated, use . + /// If the server is undergoing a soft shutdown. /// - [Required] [ResponseOptions] - [Obsolete("Use DumpOnHealthCheckRestart")] - public bool? DumpOnHeartbeatRestart { get; set; } + public bool? SoftShutdown { get; set; } } } diff --git a/src/Tgstation.Server.Api/Models/Internal/DreamDaemonLaunchParameters.cs b/src/Tgstation.Server.Api/Models/Internal/DreamDaemonLaunchParameters.cs index b563d955855..b21ce258ff0 100644 --- a/src/Tgstation.Server.Api/Models/Internal/DreamDaemonLaunchParameters.cs +++ b/src/Tgstation.Server.Api/Models/Internal/DreamDaemonLaunchParameters.cs @@ -9,21 +9,21 @@ namespace Tgstation.Server.Api.Models.Internal public class DreamDaemonLaunchParameters { /// - /// If the BYOND web client can be used to connect to the game server. + /// If the BYOND web client can be used to connect to the game server. No-op for . /// [Required] [ResponseOptions] public bool? AllowWebClient { get; set; } /// - /// If -profile is passed in on the DreamDaemon command line. + /// If -profile is passed in on the DreamDaemon command line. No-op for . /// [Required] [ResponseOptions] public bool? StartProfiler { get; set; } /// - /// The level of DreamDaemon. + /// The level of DreamDaemon. No-op for . /// [Required] [ResponseOptions] @@ -31,7 +31,7 @@ public class DreamDaemonLaunchParameters public DreamDaemonVisibility? Visibility { get; set; } /// - /// The level of DreamDaemon. + /// The level of DreamDaemon. No-op for . /// [Required] [ResponseOptions] @@ -92,7 +92,7 @@ public class DreamDaemonLaunchParameters public bool? LogOutput { get; set; } /// - /// If DreamDaemon supports it, the value added as the -map-threads parameter. 0 uses the default BYOND value. + /// If DreamDaemon supports it, the value added as the -map-threads parameter. 0 uses the default BYOND value. No-op for . /// [Required] [ResponseOptions] diff --git a/src/Tgstation.Server.Api/Models/Internal/InstancePermissionSet.cs b/src/Tgstation.Server.Api/Models/Internal/InstancePermissionSet.cs index ad0e4d3f1d0..da6af696403 100644 --- a/src/Tgstation.Server.Api/Models/Internal/InstancePermissionSet.cs +++ b/src/Tgstation.Server.Api/Models/Internal/InstancePermissionSet.cs @@ -1,5 +1,7 @@ using System.ComponentModel.DataAnnotations; +using Newtonsoft.Json; + using Tgstation.Server.Api.Rights; namespace Tgstation.Server.Api.Models.Internal @@ -22,10 +24,10 @@ public abstract class InstancePermissionSet public InstancePermissionSetRights? InstancePermissionSetRights { get; set; } /// - /// The of the . + /// The of the . /// [Required] - public ByondRights? ByondRights { get; set; } + public EngineRights? EngineRights { get; set; } /// /// The of the . diff --git a/src/Tgstation.Server.Api/Models/IrcPasswordType.cs b/src/Tgstation.Server.Api/Models/IrcPasswordType.cs index ad6ac6d9c20..add54b1565f 100644 --- a/src/Tgstation.Server.Api/Models/IrcPasswordType.cs +++ b/src/Tgstation.Server.Api/Models/IrcPasswordType.cs @@ -6,17 +6,17 @@ public enum IrcPasswordType { /// - /// Use server authentication + /// Use server authentication. /// Server, /// - /// Use PLAIN sasl authentication + /// Use PLAIN sasl authentication. /// Sasl, /// - /// Use NickServ authentication + /// Use NickServ authentication. /// NickServ, } diff --git a/src/Tgstation.Server.Api/Models/JobCode.cs b/src/Tgstation.Server.Api/Models/JobCode.cs index 072c71eda85..be5db8d5a6b 100644 --- a/src/Tgstation.Server.Api/Models/JobCode.cs +++ b/src/Tgstation.Server.Api/Models/JobCode.cs @@ -16,7 +16,7 @@ public enum JobCode : byte /// /// When the instance is being moved. /// - [Description("Instance move")] + [Description("Move instance")] Move, /// @@ -44,22 +44,22 @@ public enum JobCode : byte RepositoryDelete, /// - /// When a new BYOND version is being installed. + /// When a new official engine version is being installed. /// - [Description("Install BYOND version")] - ByondOfficialInstall, + [Description("Install engine version")] + EngineOfficialInstall, /// - /// When a new BYOND version is being installed. + /// When a new custom engine version is being installed. /// - [Description("Install custom BYOND version")] - ByondCustomInstall, + [Description("Install custom engine version")] + EngineCustomInstall, /// - /// When an installed BYOND version is being deleted. + /// When an installed engine version is being deleted. /// - [Description("Delete installed BYOND version")] - ByondDelete, + [Description("Delete installed engine version")] + EngineDelete, /// /// When a deployment is manually triggered. @@ -76,7 +76,7 @@ public enum JobCode : byte /// /// When the watchdog is started manually. /// - [Description("Launch DreamDaemon")] + [Description("Launch Watchdog")] WatchdogLaunch, /// diff --git a/src/Tgstation.Server.Api/Models/OAuthProvider.cs b/src/Tgstation.Server.Api/Models/OAuthProvider.cs index a6dfbe07bb7..0581cf33698 100644 --- a/src/Tgstation.Server.Api/Models/OAuthProvider.cs +++ b/src/Tgstation.Server.Api/Models/OAuthProvider.cs @@ -10,27 +10,27 @@ namespace Tgstation.Server.Api.Models public enum OAuthProvider { /// - /// https://github.com + /// https://github.com. /// GitHub, /// - /// https://discord.com + /// https://discord.com. /// Discord, /// - /// https://tgstation13.org + /// https://tgstation13.org. /// TGForums, /// - /// https://www.keycloak.org + /// https://www.keycloak.org. /// Keycloak, /// - /// https://invisioncommunity.com/ + /// https://invisioncommunity.com. /// InvisionCommunity, } diff --git a/src/Tgstation.Server.Api/Models/RemoteGitProvider.cs b/src/Tgstation.Server.Api/Models/RemoteGitProvider.cs index c6df292adfb..b90016a4229 100644 --- a/src/Tgstation.Server.Api/Models/RemoteGitProvider.cs +++ b/src/Tgstation.Server.Api/Models/RemoteGitProvider.cs @@ -11,12 +11,12 @@ public enum RemoteGitProvider Unknown, /// - /// Remote provider is GitHub.com + /// Remote provider is GitHub.com. /// GitHub, /// - /// Remote provider is GitLab.com + /// Remote provider is GitLab.com. /// GitLab, } diff --git a/src/Tgstation.Server.Api/Models/Request/ByondVersionDeleteRequest.cs b/src/Tgstation.Server.Api/Models/Request/ByondVersionDeleteRequest.cs deleted file mode 100644 index aaf8585c9be..00000000000 --- a/src/Tgstation.Server.Api/Models/Request/ByondVersionDeleteRequest.cs +++ /dev/null @@ -1,16 +0,0 @@ -using System; - -namespace Tgstation.Server.Api.Models.Request -{ - /// - /// A request to delete a specific . - /// - public class ByondVersionDeleteRequest - { - /// - /// The BYOND version to install. - /// - [RequestOptions(FieldPresence.Required)] - public Version? Version { get; set; } - } -} diff --git a/src/Tgstation.Server.Api/Models/Request/ByondVersionRequest.cs b/src/Tgstation.Server.Api/Models/Request/ByondVersionRequest.cs deleted file mode 100644 index b86eea074a9..00000000000 --- a/src/Tgstation.Server.Api/Models/Request/ByondVersionRequest.cs +++ /dev/null @@ -1,13 +0,0 @@ -namespace Tgstation.Server.Api.Models.Request -{ - /// - /// A request to install a BYOND . - /// - public sealed class ByondVersionRequest : ByondVersionDeleteRequest - { - /// - /// If a custom BYOND version is to be uploaded. - /// - public bool? UploadCustomZip { get; set; } - } -} diff --git a/src/Tgstation.Server.Api/Models/Request/EngineVersionDeleteRequest.cs b/src/Tgstation.Server.Api/Models/Request/EngineVersionDeleteRequest.cs new file mode 100644 index 00000000000..6002483ca1e --- /dev/null +++ b/src/Tgstation.Server.Api/Models/Request/EngineVersionDeleteRequest.cs @@ -0,0 +1,13 @@ +namespace Tgstation.Server.Api.Models.Request +{ + /// + /// A request to delete a specific . + /// + public class EngineVersionDeleteRequest + { + /// + /// The to delete. + /// + public EngineVersion? EngineVersion { get; set; } + } +} diff --git a/src/Tgstation.Server.Api/Models/Request/EngineVersionRequest.cs b/src/Tgstation.Server.Api/Models/Request/EngineVersionRequest.cs new file mode 100644 index 00000000000..4a9a7dc7993 --- /dev/null +++ b/src/Tgstation.Server.Api/Models/Request/EngineVersionRequest.cs @@ -0,0 +1,18 @@ +namespace Tgstation.Server.Api.Models.Request +{ + /// + /// A request to switch to a given . + /// + public sealed class EngineVersionRequest + { + /// + /// The to switch to. + /// + public EngineVersion? EngineVersion { get; set; } + + /// + /// If a custom BYOND version is to be uploaded. + /// + public bool? UploadCustomZip { get; set; } + } +} diff --git a/src/Tgstation.Server.Api/Models/Request/RepositoryCreateRequest.cs b/src/Tgstation.Server.Api/Models/Request/RepositoryCreateRequest.cs index dded782d643..a29ba1b674c 100644 --- a/src/Tgstation.Server.Api/Models/Request/RepositoryCreateRequest.cs +++ b/src/Tgstation.Server.Api/Models/Request/RepositoryCreateRequest.cs @@ -14,11 +14,5 @@ public sealed class RepositoryCreateRequest : RepositoryApiBase /// [RequestOptions(FieldPresence.Required)] public Uri? Origin { get; set; } - - /// - /// If submodules should be recursively cloned. Note that further updates are not recursive. - /// - [Obsolete("Use updateSubmodules instead.")] - public bool? RecurseSubmodules { get; set; } } } diff --git a/src/Tgstation.Server.Api/Models/Response/AdministrationResponse.cs b/src/Tgstation.Server.Api/Models/Response/AdministrationResponse.cs index ab8ce0aba6b..55d7f646e5c 100644 --- a/src/Tgstation.Server.Api/Models/Response/AdministrationResponse.cs +++ b/src/Tgstation.Server.Api/Models/Response/AdministrationResponse.cs @@ -8,7 +8,7 @@ namespace Tgstation.Server.Api.Models.Response public sealed class AdministrationResponse { /// - /// The GitHub repository the server is built to recieve updates from. + /// The GitHub repository the server is built to receive updates from. /// public Uri? TrackedRepositoryUrl { get; set; } diff --git a/src/Tgstation.Server.Api/Models/Response/ByondResponse.cs b/src/Tgstation.Server.Api/Models/Response/ByondResponse.cs deleted file mode 100644 index 64b01090d53..00000000000 --- a/src/Tgstation.Server.Api/Models/Response/ByondResponse.cs +++ /dev/null @@ -1,16 +0,0 @@ -using System; - -namespace Tgstation.Server.Api.Models.Response -{ - /// - /// Represents an installed BYOND . - /// - public sealed class ByondResponse - { - /// - /// The installed BYOND . BYOND itself only considers the and numbers. TGS uses the number to represent installed custom versions. - /// - [ResponseOptions] - public Version? Version { get; set; } - } -} diff --git a/src/Tgstation.Server.Api/Models/Response/CompileJobResponse.cs b/src/Tgstation.Server.Api/Models/Response/CompileJobResponse.cs index 60444d9c3c9..7f3415bb5bc 100644 --- a/src/Tgstation.Server.Api/Models/Response/CompileJobResponse.cs +++ b/src/Tgstation.Server.Api/Models/Response/CompileJobResponse.cs @@ -1,9 +1,11 @@ using System; +using Tgstation.Server.Api.Models.Internal; + namespace Tgstation.Server.Api.Models.Response { /// - public sealed class CompileJobResponse : Internal.CompileJob + public sealed class CompileJobResponse : CompileJob { /// /// The relating to this job. @@ -16,9 +18,9 @@ public sealed class CompileJobResponse : Internal.CompileJob public RevisionInformation? RevisionInformation { get; set; } /// - /// The the was made with. + /// The the was made with. /// - public Version? ByondVersion { get; set; } + public EngineVersion? EngineVersion { get; set; } /// /// The origin of the repository the compile job was built from. diff --git a/src/Tgstation.Server.Api/Models/Response/ByondInstallResponse.cs b/src/Tgstation.Server.Api/Models/Response/EngineInstallResponse.cs similarity index 51% rename from src/Tgstation.Server.Api/Models/Response/ByondInstallResponse.cs rename to src/Tgstation.Server.Api/Models/Response/EngineInstallResponse.cs index 1b66a459b45..d4d953c8305 100644 --- a/src/Tgstation.Server.Api/Models/Response/ByondInstallResponse.cs +++ b/src/Tgstation.Server.Api/Models/Response/EngineInstallResponse.cs @@ -1,14 +1,12 @@ -using System; - -namespace Tgstation.Server.Api.Models.Response +namespace Tgstation.Server.Api.Models.Response { /// - /// Represents a BYOND installation. is used to upload custom BYOND version zip files, though must still be set. + /// Represents an engine installation job. is used to upload custom version zip files. /// - public sealed class ByondInstallResponse : FileTicketResponse + public sealed class EngineInstallResponse : FileTicketResponse { /// - /// The being used to install a new . + /// The being used to install a new . /// [ResponseOptions] public JobResponse? InstallJob { get; set; } diff --git a/src/Tgstation.Server.Api/Models/Response/EngineResponse.cs b/src/Tgstation.Server.Api/Models/Response/EngineResponse.cs new file mode 100644 index 00000000000..f9768f0a7e4 --- /dev/null +++ b/src/Tgstation.Server.Api/Models/Response/EngineResponse.cs @@ -0,0 +1,13 @@ +namespace Tgstation.Server.Api.Models.Response +{ + /// + /// Represents an installed . + /// + public sealed class EngineResponse + { + /// + /// The represented . If that indicates none were found. + /// + public EngineVersion? EngineVersion { get; set; } + } +} diff --git a/src/Tgstation.Server.Api/Models/Response/ServerUpdateResponse.cs b/src/Tgstation.Server.Api/Models/Response/ServerUpdateResponse.cs index 3578e6cd0ae..48c9b99e6a1 100644 --- a/src/Tgstation.Server.Api/Models/Response/ServerUpdateResponse.cs +++ b/src/Tgstation.Server.Api/Models/Response/ServerUpdateResponse.cs @@ -20,7 +20,7 @@ public sealed class ServerUpdateResponse : FileTicketResponse /// The value of . /// The optional value of . [JsonConstructor] - public ServerUpdateResponse(Version newVersion, string fileTicket) + public ServerUpdateResponse(Version newVersion, string? fileTicket) { NewVersion = newVersion ?? throw new ArgumentNullException(nameof(newVersion)); FileTicket = fileTicket; diff --git a/src/Tgstation.Server.Api/Models/Response/TokenResponse.cs b/src/Tgstation.Server.Api/Models/Response/TokenResponse.cs index 8f7b01762b8..f81066741a4 100644 --- a/src/Tgstation.Server.Api/Models/Response/TokenResponse.cs +++ b/src/Tgstation.Server.Api/Models/Response/TokenResponse.cs @@ -1,6 +1,4 @@ -using System; - -using Microsoft.IdentityModel.JsonWebTokens; +using Microsoft.IdentityModel.JsonWebTokens; namespace Tgstation.Server.Api.Models.Response { @@ -14,16 +12,10 @@ public sealed class TokenResponse /// public string? Bearer { get; set; } - /// - /// When the expires. - /// - [Obsolete("Will be removed in a future API version")] - public DateTimeOffset? ExpiresAt { get; set; } - /// /// Parses the as a . /// /// A new based on . - public JsonWebToken ParseJwt() => new (Bearer); + public JsonWebToken ParseJwt() => new(Bearer); } } diff --git a/src/Tgstation.Server.Api/Rights/AdministrationRights.cs b/src/Tgstation.Server.Api/Rights/AdministrationRights.cs index 018a239e8e2..ed530eee44a 100644 --- a/src/Tgstation.Server.Api/Rights/AdministrationRights.cs +++ b/src/Tgstation.Server.Api/Rights/AdministrationRights.cs @@ -9,7 +9,7 @@ namespace Tgstation.Server.Api.Rights public enum AdministrationRights : ulong { /// - /// User has no rights + /// User has no rights. /// None = 0, diff --git a/src/Tgstation.Server.Api/Rights/ByondRights.cs b/src/Tgstation.Server.Api/Rights/ByondRights.cs deleted file mode 100644 index 288d8ca3b8d..00000000000 --- a/src/Tgstation.Server.Api/Rights/ByondRights.cs +++ /dev/null @@ -1,46 +0,0 @@ -using System; - -namespace Tgstation.Server.Api.Rights -{ - /// - /// Rights for BYOND version management. - /// - [Flags] - public enum ByondRights : ulong - { - /// - /// User has no rights. - /// - None = 0, - - /// - /// User may view the active installed BYOND version. - /// - ReadActive = 1 << 0, - - /// - /// User may list all installed BYOND versions. - /// - ListInstalled = 1 << 1, - - /// - /// User may install official BYOND versions or change the active BYOND version. - /// - InstallOfficialOrChangeActiveVersion = 1 << 2, - - /// - /// User may cancel BYOND installation job. - /// - CancelInstall = 1 << 3, - - /// - /// User may upload and activate custom BYOND builds. - /// - InstallCustomVersion = 1 << 4, - - /// - /// User may delete non-active BYOND builds. - /// - DeleteInstall = 1 << 5, - } -} diff --git a/src/Tgstation.Server.Api/Rights/ChatBotRights.cs b/src/Tgstation.Server.Api/Rights/ChatBotRights.cs index d94106ead89..d00af08c86b 100644 --- a/src/Tgstation.Server.Api/Rights/ChatBotRights.cs +++ b/src/Tgstation.Server.Api/Rights/ChatBotRights.cs @@ -29,7 +29,7 @@ public enum ChatBotRights : ulong WriteChannels = 1 << 2, /// - /// User can change + /// User can change . /// WriteConnectionString = 1 << 3, @@ -39,7 +39,7 @@ public enum ChatBotRights : ulong ReadConnectionString = 1 << 4, /// - /// User can read all chat bot properties except + /// User can read all chat bot properties except . /// Read = 1 << 5, diff --git a/src/Tgstation.Server.Api/Rights/DreamDaemonRights.cs b/src/Tgstation.Server.Api/Rights/DreamDaemonRights.cs index 378783bf1e2..9af0522856f 100644 --- a/src/Tgstation.Server.Api/Rights/DreamDaemonRights.cs +++ b/src/Tgstation.Server.Api/Rights/DreamDaemonRights.cs @@ -74,7 +74,7 @@ public enum DreamDaemonRights : ulong SetStartupTimeout = 1 << 11, /// - /// User can change + /// User can change . /// SetHealthCheckInterval = 1 << 12, diff --git a/src/Tgstation.Server.Api/Rights/EngineRights.cs b/src/Tgstation.Server.Api/Rights/EngineRights.cs new file mode 100644 index 00000000000..93f8c39dd9e --- /dev/null +++ b/src/Tgstation.Server.Api/Rights/EngineRights.cs @@ -0,0 +1,56 @@ +using System; + +namespace Tgstation.Server.Api.Rights +{ + /// + /// Rights for engine version management. + /// + [Flags] + public enum EngineRights : ulong + { + /// + /// User has no rights. + /// + None = 0, + + /// + /// User may view the active installed engine versions. + /// + ReadActive = 1 << 0, + + /// + /// User may list all installed engine versions. + /// + ListInstalled = 1 << 1, + + /// + /// User may install official versions or change the active version. + /// + InstallOfficialOrChangeActiveByondVersion = 1 << 2, + + /// + /// User may cancel an engine installation job. + /// + CancelInstall = 1 << 3, + + /// + /// User may upload and activate custom builds. + /// + InstallCustomByondVersion = 1 << 4, + + /// + /// User may delete non-active engine builds. + /// + DeleteInstall = 1 << 5, + + /// + /// User may install official versions or change the active version. + /// + InstallOfficialOrChangeActiveOpenDreamVersion = 1 << 6, + + /// + /// User may activate custom builds via zip upload or custom git committish. + /// + InstallCustomOpenDreamVersion = 1 << 7, + } +} diff --git a/src/Tgstation.Server.Api/Rights/InstancePermissionSetRights.cs b/src/Tgstation.Server.Api/Rights/InstancePermissionSetRights.cs index 6562e383980..0b6e3b5f9a7 100644 --- a/src/Tgstation.Server.Api/Rights/InstancePermissionSetRights.cs +++ b/src/Tgstation.Server.Api/Rights/InstancePermissionSetRights.cs @@ -9,7 +9,7 @@ namespace Tgstation.Server.Api.Rights public enum InstancePermissionSetRights : ulong { /// - /// User has no rights/ + /// User has no rights. /// None = 0, diff --git a/src/Tgstation.Server.Api/Rights/RightsHelper.cs b/src/Tgstation.Server.Api/Rights/RightsHelper.cs index 73e948bdbb0..2f60e9a0a0f 100644 --- a/src/Tgstation.Server.Api/Rights/RightsHelper.cs +++ b/src/Tgstation.Server.Api/Rights/RightsHelper.cs @@ -18,7 +18,7 @@ public static class RightsHelper { RightsType.Administration, typeof(AdministrationRights) }, { RightsType.InstanceManager, typeof(InstanceManagerRights) }, { RightsType.Repository, typeof(RepositoryRights) }, - { RightsType.Byond, typeof(ByondRights) }, + { RightsType.Engine, typeof(EngineRights) }, { RightsType.DreamMaker, typeof(DreamMakerRights) }, { RightsType.DreamDaemon, typeof(DreamDaemonRights) }, { RightsType.ChatBots, typeof(ChatBotRights) }, diff --git a/src/Tgstation.Server.Api/Rights/RightsType.cs b/src/Tgstation.Server.Api/Rights/RightsType.cs index ae40fe46be9..7c60417def4 100644 --- a/src/Tgstation.Server.Api/Rights/RightsType.cs +++ b/src/Tgstation.Server.Api/Rights/RightsType.cs @@ -6,47 +6,47 @@ public enum RightsType : ulong { /// - /// + /// . /// Administration, /// - /// + /// . /// InstanceManager, /// - /// + /// . /// Repository, /// - /// + /// . /// - Byond, + Engine, /// - /// + /// . /// DreamMaker, /// - /// + /// . /// DreamDaemon, /// - /// + /// . /// ChatBots, /// - /// + /// . /// Configuration, /// - /// + /// . /// InstancePermissionSet, } diff --git a/src/Tgstation.Server.Api/Routes.cs b/src/Tgstation.Server.Api/Routes.cs index b62d7816043..4372929ed75 100644 --- a/src/Tgstation.Server.Api/Routes.cs +++ b/src/Tgstation.Server.Api/Routes.cs @@ -8,19 +8,19 @@ namespace Tgstation.Server.Api public static class Routes { /// - /// The root controller. + /// The root of API methods. /// - public const string Root = "/"; + public const string ApiRoot = "/api/"; /// /// The root route of all hubs. /// - public const string HubsRoot = Root + "hubs"; + public const string HubsRoot = ApiRoot + "hubs"; /// /// The server administration controller. /// - public const string Administration = Root + "Administration"; + public const string Administration = ApiRoot + "Administration"; /// /// The endpoint to download server logs. @@ -30,32 +30,32 @@ public static class Routes /// /// The user controller. /// - public const string User = Root + "User"; + public const string User = ApiRoot + "User"; /// /// The user group controller. /// - public const string UserGroup = Root + "UserGroup"; + public const string UserGroup = ApiRoot + "UserGroup"; /// /// The controller. /// - public const string InstanceManager = Root + "Instance"; + public const string InstanceManager = ApiRoot + "Instance"; /// - /// The BYOND controller. + /// The engine controller. /// - public const string Byond = Root + "Byond"; + public const string Engine = ApiRoot + "Engine"; /// /// The git repository controller. /// - public const string Repository = Root + "Repository"; + public const string Repository = ApiRoot + "Repository"; /// /// The DreamDaemon controller. /// - public const string DreamDaemon = Root + "DreamDaemon"; + public const string DreamDaemon = ApiRoot + "DreamDaemon"; /// /// For accessing DD diagnostics. @@ -65,7 +65,7 @@ public static class Routes /// /// The configuration controller. /// - public const string Configuration = Root + "Config"; + public const string Configuration = ApiRoot + "Config"; /// /// To be paired with for accessing s. @@ -80,27 +80,27 @@ public static class Routes /// /// The instance permission set controller. /// - public const string InstancePermissionSet = Root + "InstancePermissionSet"; + public const string InstancePermissionSet = ApiRoot + "InstancePermissionSet"; /// /// The chat bot controller. /// - public const string Chat = Root + "Chat"; + public const string Chat = ApiRoot + "Chat"; /// /// The deployment controller. /// - public const string DreamMaker = Root + "DreamMaker"; + public const string DreamMaker = ApiRoot + "DreamMaker"; /// /// The jobs controller. /// - public const string Jobs = Root + "Job"; + public const string Jobs = ApiRoot + "Job"; /// /// The transfer controller. /// - public const string Transfer = Root + "Transfer"; + public const string Transfer = ApiRoot + "Transfer"; /// /// The postfix for list operations. @@ -134,8 +134,7 @@ public static class Routes /// The sanitized path. public static string SanitizeGetPath(string path) { - if (path == null) - path = String.Empty; + path ??= String.Empty; if (path.Length == 0 || path[0] != '/') path = '/' + path; return path; diff --git a/src/Tgstation.Server.Client/ApiClient.cs b/src/Tgstation.Server.Client/ApiClient.cs index bf0934a48b9..e9824a433db 100644 --- a/src/Tgstation.Server.Client/ApiClient.cs +++ b/src/Tgstation.Server.Client/ApiClient.cs @@ -37,7 +37,7 @@ class ApiClient : IApiClient /// PATCH . /// /// HOW IS THIS NOT INCLUDED IN THE FRAMEWORK??!?!? - static readonly HttpMethod HttpPatch = new ("PATCH"); + static readonly HttpMethod HttpPatch = new("PATCH"); /// public Uri Url { get; } @@ -59,7 +59,7 @@ public TimeSpan Timeout /// /// The to use. /// - static readonly JsonSerializerSettings SerializerSettings = new () + static readonly JsonSerializerSettings SerializerSettings = new() { ContractResolver = new CamelCasePropertyNamesContractResolver(), Converters = new[] @@ -126,13 +126,9 @@ static void HandleBadResponse(HttpResponseMessage response, string json) } #pragma warning disable IDE0010 // Add missing cases -#pragma warning disable IDE0066 // Convert switch statement to expression switch (response.StatusCode) -#pragma warning restore IDE0066 // Convert switch statement to expression #pragma warning restore IDE0010 // Add missing cases { - case HttpStatusCode.UpgradeRequired: - throw new VersionMismatchException(errorMessage, response); case HttpStatusCode.Unauthorized: throw new UnauthorizedException(errorMessage, response); case HttpStatusCode.InternalServerError: @@ -154,6 +150,9 @@ static void HandleBadResponse(HttpResponseMessage response, string json) case (HttpStatusCode)429: throw new RateLimitException(errorMessage, response); default: + if (errorMessage?.ErrorCode == ErrorCode.ApiMismatch) + throw new VersionMismatchException(errorMessage, response); + throw new ApiConflictException(errorMessage, response); } } @@ -309,9 +308,7 @@ public async ValueTask Upload(FileTicketResponse ticket, Stream? uploadStream, C using (memoryStream) { -#pragma warning disable CA2000 // Dispose objects before losing scope var streamContent = new StreamContent(uploadStream ?? memoryStream); -#pragma warning restore CA2000 // Dispose objects before losing scope try { await RunRequest( @@ -348,7 +345,7 @@ public async ValueTask RefreshToken(CancellationToken cancellationToken) if (startingToken != headers.Token) return true; - var token = await RunRequest(Routes.Root, new object(), HttpMethod.Post, null, true, cancellationToken).ConfigureAwait(false); + var token = await RunRequest(Routes.ApiRoot, new object(), HttpMethod.Post, null, true, cancellationToken).ConfigureAwait(false); headers = new ApiHeaders(headers.UserAgent!, token); } finally @@ -506,25 +503,14 @@ protected virtual async ValueTask RunRequest( var bearer = headersToUse.Token?.Bearer; if (bearer != null) { - try - { - var parsed = headersToUse.Token!.ParseJwt(); - var nbf = parsed.ValidFrom; - var now = DateTime.UtcNow; - if (nbf >= now) - { - var delay = (nbf - now).Add(TimeSpan.FromMilliseconds(1)); - await Task.Delay(delay, cancellationToken); - } - } - catch (ArgumentException ex) when (ex is not ArgumentNullException) + var parsed = headersToUse.Token!.ParseJwt(); + var nbf = parsed.ValidFrom; + var now = DateTime.UtcNow; + if (nbf >= now) { - // backwards compat, API <=9 put out invalid JWTs, remove in API 10 + var delay = (nbf - now).Add(TimeSpan.FromMilliseconds(1)); + await Task.Delay(delay, cancellationToken); } -#if DEBUG - if (ApiHeaders.Version.Major > 9) - throw new NotImplementedException(); -#endif } } diff --git a/src/Tgstation.Server.Client/Components/ByondClient.cs b/src/Tgstation.Server.Client/Components/EngineClient.cs similarity index 50% rename from src/Tgstation.Server.Client/Components/ByondClient.cs rename to src/Tgstation.Server.Client/Components/EngineClient.cs index e171af7860e..9f062995bb2 100644 --- a/src/Tgstation.Server.Client/Components/ByondClient.cs +++ b/src/Tgstation.Server.Client/Components/EngineClient.cs @@ -11,46 +11,46 @@ namespace Tgstation.Server.Client.Components { - /// - sealed class ByondClient : PaginatedClient, IByondClient + /// + sealed class EngineClient : PaginatedClient, IEngineClient { /// - /// The for the . + /// The for the . /// readonly Instance instance; /// - /// Initializes a new instance of the class. + /// Initializes a new instance of the class. /// /// The for the . /// The value of . - public ByondClient(IApiClient apiClient, Instance instance) + public EngineClient(IApiClient apiClient, Instance instance) : base(apiClient) { this.instance = instance ?? throw new ArgumentNullException(nameof(instance)); } /// - public ValueTask ActiveVersion(CancellationToken cancellationToken) => ApiClient.Read(Routes.Byond, instance.Id!.Value, cancellationToken); + public ValueTask ActiveVersion(CancellationToken cancellationToken) => ApiClient.Read(Routes.Engine, instance.Id!.Value, cancellationToken); /// - public ValueTask DeleteVersion(ByondVersionDeleteRequest deleteRequest, CancellationToken cancellationToken) - => ApiClient.Delete(Routes.Byond, deleteRequest, instance.Id!.Value, cancellationToken); + public ValueTask DeleteVersion(EngineVersionDeleteRequest deleteRequest, CancellationToken cancellationToken) + => ApiClient.Delete(Routes.Engine, deleteRequest, instance.Id!.Value, cancellationToken); /// - public ValueTask> InstalledVersions(PaginationSettings? paginationSettings, CancellationToken cancellationToken) - => ReadPaged(paginationSettings, Routes.ListRoute(Routes.Byond), instance.Id, cancellationToken); + public ValueTask> InstalledVersions(PaginationSettings? paginationSettings, CancellationToken cancellationToken) + => ReadPaged(paginationSettings, Routes.ListRoute(Routes.Engine), instance.Id, cancellationToken); /// - public async ValueTask SetActiveVersion(ByondVersionRequest installRequest, Stream? zipFileStream, CancellationToken cancellationToken) + public async ValueTask SetActiveVersion(EngineVersionRequest installRequest, Stream? zipFileStream, CancellationToken cancellationToken) { if (installRequest == null) throw new ArgumentNullException(nameof(installRequest)); if (installRequest.UploadCustomZip == true && zipFileStream == null) throw new ArgumentNullException(nameof(zipFileStream)); - var result = await ApiClient.Update( - Routes.Byond, + var result = await ApiClient.Update( + Routes.Engine, installRequest, instance.Id!.Value, cancellationToken) diff --git a/src/Tgstation.Server.Client/Components/IByondClient.cs b/src/Tgstation.Server.Client/Components/IByondClient.cs deleted file mode 100644 index bde6e04535c..00000000000 --- a/src/Tgstation.Server.Client/Components/IByondClient.cs +++ /dev/null @@ -1,48 +0,0 @@ -using System.Collections.Generic; -using System.IO; -using System.Threading; -using System.Threading.Tasks; - -using Tgstation.Server.Api.Models.Request; -using Tgstation.Server.Api.Models.Response; - -namespace Tgstation.Server.Client.Components -{ - /// - /// For managing the installation. - /// - public interface IByondClient - { - /// - /// Get the active information. - /// - /// The for the operation. - /// A resulting in the active information. - ValueTask ActiveVersion(CancellationToken cancellationToken); - - /// - /// Get all installed s. - /// - /// The optional for the operation. - /// The for the operation. - /// A resulting in an of installed s. - ValueTask> InstalledVersions(PaginationSettings? paginationSettings, CancellationToken cancellationToken); - - /// - /// Updates the active BYOND version. - /// - /// The . - /// The for the .zip file if is . Will be ignored if it is . - /// The for the operation. - /// A resulting in the updated information. - ValueTask SetActiveVersion(ByondVersionRequest installRequest, Stream? zipFileStream, CancellationToken cancellationToken); - - /// - /// Starts a jobs to delete a specific BYOND version. - /// - /// The specifying the version to delete. - /// The for the operation. - /// A resulting in the for the delete job. - ValueTask DeleteVersion(ByondVersionDeleteRequest deleteRequest, CancellationToken cancellationToken); - } -} diff --git a/src/Tgstation.Server.Client/Components/IEngineClient.cs b/src/Tgstation.Server.Client/Components/IEngineClient.cs new file mode 100644 index 00000000000..046f81e0bce --- /dev/null +++ b/src/Tgstation.Server.Client/Components/IEngineClient.cs @@ -0,0 +1,48 @@ +using System.Collections.Generic; +using System.IO; +using System.Threading; +using System.Threading.Tasks; + +using Tgstation.Server.Api.Models.Request; +using Tgstation.Server.Api.Models.Response; + +namespace Tgstation.Server.Client.Components +{ + /// + /// For managing the engine installations. + /// + public interface IEngineClient + { + /// + /// Get the active . + /// + /// The for the operation. + /// A resulting in the . + ValueTask ActiveVersion(CancellationToken cancellationToken); + + /// + /// Get all installed s. + /// + /// The optional for the operation. + /// The for the operation. + /// A resulting in an of installed s. + ValueTask> InstalledVersions(PaginationSettings? paginationSettings, CancellationToken cancellationToken); + + /// + /// Updates the active engine version. + /// + /// The . + /// The for the .zip file if is . Will be ignored if it is . + /// The for the operation. + /// A resulting in the . + ValueTask SetActiveVersion(EngineVersionRequest installRequest, Stream? zipFileStream, CancellationToken cancellationToken); + + /// + /// Starts a job to delete a specific engine version. + /// + /// The specifying the to delete. + /// The for the operation. + /// A resulting in the for the delete job. + ValueTask DeleteVersion(EngineVersionDeleteRequest deleteRequest, CancellationToken cancellationToken); + } +} diff --git a/src/Tgstation.Server.Client/Components/IInstanceClient.cs b/src/Tgstation.Server.Client/Components/IInstanceClient.cs index 564e089e200..9799b5699b2 100644 --- a/src/Tgstation.Server.Client/Components/IInstanceClient.cs +++ b/src/Tgstation.Server.Client/Components/IInstanceClient.cs @@ -13,9 +13,9 @@ public interface IInstanceClient Instance Metadata { get; } /// - /// Access the . + /// Access the . /// - IByondClient Byond { get; } + IEngineClient Engine { get; } /// /// Access the . diff --git a/src/Tgstation.Server.Client/Components/InstanceClient.cs b/src/Tgstation.Server.Client/Components/InstanceClient.cs index 70024af14b2..51a61aec1ed 100644 --- a/src/Tgstation.Server.Client/Components/InstanceClient.cs +++ b/src/Tgstation.Server.Client/Components/InstanceClient.cs @@ -11,7 +11,7 @@ sealed class InstanceClient : IInstanceClient public Instance Metadata { get; } /// - public IByondClient Byond { get; } + public IEngineClient Engine { get; } /// public IRepositoryClient Repository { get; } @@ -48,7 +48,7 @@ public InstanceClient(IApiClient apiClient, Instance instance) Metadata = instance ?? throw new ArgumentNullException(nameof(instance)); - Byond = new ByondClient(apiClient, instance); + Engine = new EngineClient(apiClient, instance); Repository = new RepositoryClient(apiClient, instance); DreamDaemon = new DreamDaemonClient(apiClient, instance); Configuration = new ConfigurationClient(apiClient, instance); diff --git a/src/Tgstation.Server.Client/IServerClient.cs b/src/Tgstation.Server.Client/IServerClient.cs index 65014f66b1e..317e012ed08 100644 --- a/src/Tgstation.Server.Client/IServerClient.cs +++ b/src/Tgstation.Server.Client/IServerClient.cs @@ -16,7 +16,7 @@ namespace Tgstation.Server.Client public interface IServerClient : IAsyncDisposable { /// - /// The connected server . + /// The connected server's root . /// Uri Url { get; } diff --git a/src/Tgstation.Server.Client/ServerClient.cs b/src/Tgstation.Server.Client/ServerClient.cs index 186e32745b2..6fce35fcf9c 100644 --- a/src/Tgstation.Server.Client/ServerClient.cs +++ b/src/Tgstation.Server.Client/ServerClient.cs @@ -66,7 +66,7 @@ public ServerClient(IApiClient apiClient) public ValueTask DisposeAsync() => apiClient.DisposeAsync(); /// - public ValueTask ServerInformation(CancellationToken cancellationToken) => apiClient.Read(Routes.Root, cancellationToken); + public ValueTask ServerInformation(CancellationToken cancellationToken) => apiClient.Read(Routes.ApiRoot, cancellationToken); /// public void AddRequestLogger(IRequestLogger requestLogger) => apiClient.AddRequestLogger(requestLogger); diff --git a/src/Tgstation.Server.Client/ServerClientFactory.cs b/src/Tgstation.Server.Client/ServerClientFactory.cs index 155198996a2..98a51e8dc5d 100644 --- a/src/Tgstation.Server.Client/ServerClientFactory.cs +++ b/src/Tgstation.Server.Client/ServerClientFactory.cs @@ -138,7 +138,7 @@ public async ValueTask GetServerInformation( if (timeout.HasValue) api.Timeout = timeout.Value; - return await api.Read(Routes.Root, cancellationToken).ConfigureAwait(false); + return await api.Read(Routes.ApiRoot, cancellationToken).ConfigureAwait(false); } /// @@ -169,7 +169,7 @@ async ValueTask CreateWithNewToken( if (timeout.HasValue) api.Timeout = timeout.Value; - token = await api.Update(Routes.Root, cancellationToken).ConfigureAwait(false); + token = await api.Update(Routes.ApiRoot, cancellationToken).ConfigureAwait(false); } var apiHeaders = new ApiHeaders(productHeaderValue, token); diff --git a/src/Tgstation.Server.Client/Tgstation.Server.Client.csproj b/src/Tgstation.Server.Client/Tgstation.Server.Client.csproj index e6c2aaaea74..876f1097d6f 100644 --- a/src/Tgstation.Server.Client/Tgstation.Server.Client.csproj +++ b/src/Tgstation.Server.Client/Tgstation.Server.Client.csproj @@ -11,9 +11,9 @@ - + - + diff --git a/src/Tgstation.Server.Common/Extensions/ValueTaskExtensions.cs b/src/Tgstation.Server.Common/Extensions/ValueTaskExtensions.cs index 6cb8e6ad8ac..1dead09d935 100644 --- a/src/Tgstation.Server.Common/Extensions/ValueTaskExtensions.cs +++ b/src/Tgstation.Server.Common/Extensions/ValueTaskExtensions.cs @@ -138,7 +138,7 @@ public static async ValueTask WhenAll(IReadOnlyList tasks) } catch (Exception ex) { - exceptions ??= new (tasks.Count - i); + exceptions ??= new(tasks.Count - i); exceptions.Add(ex); } diff --git a/src/Tgstation.Server.Common/Tgstation.Server.Common.csproj b/src/Tgstation.Server.Common/Tgstation.Server.Common.csproj index 07b8b364a24..b77bdc4dc15 100644 --- a/src/Tgstation.Server.Common/Tgstation.Server.Common.csproj +++ b/src/Tgstation.Server.Common/Tgstation.Server.Common.csproj @@ -4,6 +4,7 @@ $(TgsNugetNetFramework) $(TgsCommonLibraryVersion) + enable Common functions for tgstation-server. web tgstation-server tgstation ss13 byond client http $(TGS_NUGET_RELEASE_NOTES_COMMON) diff --git a/src/Tgstation.Server.Host.Common/DotnetHelper.cs b/src/Tgstation.Server.Host.Common/DotnetHelper.cs new file mode 100644 index 00000000000..b5d80db7844 --- /dev/null +++ b/src/Tgstation.Server.Host.Common/DotnetHelper.cs @@ -0,0 +1,54 @@ +#if NET6_0_OR_GREATER +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +#endif + +namespace Tgstation.Server.Host.Common +{ +#if NET6_0_OR_GREATER + /// + /// Helper functions for working with dotnet.exe. + /// + public static class DotnetHelper + { + /// + /// Gets the path to the dotnet executable. + /// + /// If the current system is a Windows OS. + /// The path to the dotnet executable. + public static IEnumerable GetPotentialDotnetPaths(bool isWindows) + { + var enviromentPath = Environment.GetEnvironmentVariable("PATH"); + var paths = enviromentPath.Split(';'); + + var exeName = "dotnet"; + IEnumerable enumerator; + if (isWindows) + { + exeName += ".exe"; + enumerator = new List(paths) + { + "C:/Program Files/dotnet", + "C:/Program Files (x86)/dotnet", + }; + } + else + 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)); + + return enumerator; + } + } +#endif +} diff --git a/src/Tgstation.Server.Host.Console/PosixSignalChecker.cs b/src/Tgstation.Server.Host.Console/PosixSignalChecker.cs index e037b5936fb..b18ece3c03f 100644 --- a/src/Tgstation.Server.Host.Console/PosixSignalChecker.cs +++ b/src/Tgstation.Server.Host.Console/PosixSignalChecker.cs @@ -31,7 +31,7 @@ public PosixSignalChecker(ILogger logger) } /// - public async ValueTask CheckSignals(Func startChild, CancellationToken cancellationToken) + public async ValueTask CheckSignals(Func startChild, CancellationToken cancellationToken) { var (childPid, _) = startChild?.Invoke(null) ?? throw new ArgumentNullException(nameof(startChild)); var signalTcs = new TaskCompletionSource(); diff --git a/src/Tgstation.Server.Host.Console/Program.cs b/src/Tgstation.Server.Host.Console/Program.cs index e142de81be4..c8383865af4 100644 --- a/src/Tgstation.Server.Host.Console/Program.cs +++ b/src/Tgstation.Server.Host.Console/Program.cs @@ -38,7 +38,7 @@ static Program() /// A representing the running operation. internal static async Task Main(string[] args) { - System.Console.Title = $"{Constants.CanonicalPackageName} Host Watchdog v{Assembly.GetExecutingAssembly().GetName().Version.Semver()}"; + System.Console.Title = $"{Constants.CanonicalPackageName} Host Watchdog v{Assembly.GetExecutingAssembly().GetName().Version?.Semver()}"; var arguments = new List(args); var trace = arguments.Remove("--trace-host-watchdog"); @@ -61,7 +61,7 @@ internal static async Task Main(string[] args) } using var cts = new CancellationTokenSource(); - void AppDomainHandler(object a, EventArgs b) => cts.Cancel(); + void AppDomainHandler(object? a, EventArgs b) => cts.Cancel(); AppDomain.CurrentDomain.ProcessExit += AppDomainHandler; try { 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 7a85df7ea40..3cbf1c1a163 100644 --- a/src/Tgstation.Server.Host.Console/Tgstation.Server.Host.Console.csproj +++ b/src/Tgstation.Server.Host.Console/Tgstation.Server.Host.Console.csproj @@ -5,24 +5,14 @@ Exe $(TgsFrameworkVersion) $(TgsCoreVersion) + enable false ../../build/uac_elevation_manifest.xml - - true - - bin\Release\net6.0\Tgstation.Server.Host.Console.xml - - - - 1701;1702;SA1652 - bin\Debug\net6.0\Tgstation.Server.Host.Console.xml - - - + diff --git a/src/Tgstation.Server.Host.Service/Program.cs b/src/Tgstation.Server.Host.Service/Program.cs index 9e6deaeb633..4a9d39fcd21 100644 --- a/src/Tgstation.Server.Host.Service/Program.cs +++ b/src/Tgstation.Server.Host.Service/Program.cs @@ -77,7 +77,7 @@ sealed class Program /// The --passthroughargs or -p option. /// [Option(ShortName = "p", Description = "Arguments passed to main host process")] - public string PassthroughArgs { get; set; } + public string? PassthroughArgs { get; set; } /// /// Entrypoint for the application. @@ -157,7 +157,7 @@ public async Task OnExecuteAsync() /// Runs sc.exe to either uninstall a given or install the running . /// /// The name of a service to uninstall. - void InvokeSC(string serviceToUninstall) + void InvokeSC(string? serviceToUninstall) { using var installer = new ServiceInstaller(); if (serviceToUninstall != null) @@ -172,11 +172,16 @@ void InvokeSC(string serviceToUninstall) Assembly.GetExecutingAssembly().Location); var assemblyDirectory = Path.GetDirectoryName(fullPathToAssembly); + if (assemblyDirectory == null) + throw new InvalidOperationException($"Failed to resolve directory name of {assemblyDirectory}"); + var assemblyNameWithoutExtension = Path.GetFileNameWithoutExtension(fullPathToAssembly); var exePath = Path.Combine(assemblyDirectory, $"{assemblyNameWithoutExtension}.exe"); var programDataDirectory = Path.Combine( - Environment.GetFolderPath(Environment.SpecialFolder.CommonApplicationData), + Environment.GetFolderPath( + Environment.SpecialFolder.CommonApplicationData, + Environment.SpecialFolderOption.DoNotVerify), Server.Common.Constants.CanonicalPackageName); using var processInstaller = new ServiceProcessInstaller(); @@ -260,10 +265,8 @@ void RestartService(ServiceController serviceController) var stop = !Detach; if (!stop) { - serviceController.ExecuteCommand( - PipeCommands.GetServiceCommandId( - PipeCommands.CommandDetachingShutdown) - .Value); + var serviceControllerCommand = PipeCommands.GetServiceCommandId(PipeCommands.CommandDetachingShutdown); + serviceController.ExecuteCommand(serviceControllerCommand!.Value); serviceController.WaitForStatus(ServiceControllerStatus.Stopped, TimeSpan.FromSeconds(30)); if (serviceController.Status != ServiceControllerStatus.Stopped) stop = true; diff --git a/src/Tgstation.Server.Host.Service/ServerService.cs b/src/Tgstation.Server.Host.Service/ServerService.cs index 8f5554304fe..0ab3a73cb69 100644 --- a/src/Tgstation.Server.Host.Service/ServerService.cs +++ b/src/Tgstation.Server.Host.Service/ServerService.cs @@ -1,20 +1,14 @@ using System; using System.Collections.Generic; using System.Diagnostics; -using System.IO; -using System.IO.Pipes; -using System.Linq; using System.Runtime.Versioning; 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.Common; -using Tgstation.Server.Host.Common; using Tgstation.Server.Host.Watchdog; namespace Tgstation.Server.Host.Service @@ -23,7 +17,7 @@ namespace Tgstation.Server.Host.Service /// Represents a as a . /// [SupportedOSPlatform("windows")] - sealed class ServerService : ServiceBase, ISignalChecker + sealed class ServerService : ServiceBase { /// /// The canonical windows service name. @@ -36,44 +30,21 @@ sealed class ServerService : ServiceBase, ISignalChecker readonly IWatchdogFactory watchdogFactory; /// - /// The of command line arguments the service was invoked with. - /// - readonly string[] commandLineArguments; - - /// - /// The minimum for the . - /// - readonly LogLevel minimumLogLevel; - - /// - /// The used by the . - /// - ILoggerFactory loggerFactory; - - /// - /// The for the . - /// - ILogger logger; - - /// - /// The that represents the running . - /// - Task watchdogTask; - - /// - /// The for the . + /// The used by the . /// - CancellationTokenSource cancellationTokenSource; + readonly Lazy loggerFactory; /// - /// The for sending to the server process. + /// The of command line arguments the service was invoked with. /// - AnonymousPipeServerStream commandPipeServer; + readonly string[] commandLineArguments; /// - /// The for receiving the . + /// The active . /// - AnonymousPipeServerStream readyPipeServer; +#pragma warning disable CA2213 // Disposable fields should be disposed + volatile ServiceLifetime? serviceLifetime; +#pragma warning restore CA2213 // Disposable fields should be disposed /// /// Initializes a new instance of the class. @@ -85,21 +56,15 @@ public ServerService(IWatchdogFactory watchdogFactory, string[] commandLineArgum { this.watchdogFactory = watchdogFactory ?? throw new ArgumentNullException(nameof(watchdogFactory)); this.commandLineArguments = commandLineArguments ?? throw new ArgumentNullException(nameof(commandLineArguments)); - this.minimumLogLevel = minimumLogLevel; - ServiceName = Name; - } - /// - public async ValueTask CheckSignals(Func startChildAndGetPid, CancellationToken cancellationToken) - { - await using (commandPipeServer = new AnonymousPipeServerStream(PipeDirection.Out, HandleInheritability.Inheritable)) - await using (readyPipeServer = new AnonymousPipeServerStream(PipeDirection.In, HandleInheritability.Inheritable)) + ServiceName = Name; + loggerFactory = new Lazy(() => LoggerFactory.Create(builder => builder.AddEventLog(new EventLogSettings { - var (_, lifetimeTask) = startChildAndGetPid($"--Internal:CommandPipe={commandPipeServer.GetClientHandleAsString()} --Internal:ReadyPipe={readyPipeServer.GetClientHandleAsString()}"); - commandPipeServer.DisposeLocalCopyOfClientHandle(); - readyPipeServer.DisposeLocalCopyOfClientHandle(); - await lifetimeTask; - } + LogName = EventLog.Log, + MachineName = EventLog.MachineName, + SourceName = EventLog.Source, + Filter = (message, logLevel) => logLevel >= minimumLogLevel, + }))); } /// @@ -112,53 +77,21 @@ protected override void Dispose(bool disposing) { if (disposing) { - loggerFactory?.Dispose(); - cancellationTokenSource?.Dispose(); - commandPipeServer?.Dispose(); - readyPipeServer?.Dispose(); + OnStop(); + + if (loggerFactory.IsValueCreated) + loggerFactory.Value.Dispose(); } base.Dispose(disposing); } /// - protected override void OnCustomCommand(int command) - { - var commandsToCheck = PipeCommands.AllCommands; - foreach (var stringCommand in commandsToCheck) - { - var commandId = PipeCommands.GetServiceCommandId(stringCommand); - if (command == commandId) - { - SendCommandToHostThroughPipe(stringCommand); - return; - } - } - - logger.LogWarning("Received unknown service command: {command}", command); - } + protected override void OnCustomCommand(int command) => serviceLifetime!.HandleCustomCommand(command); /// protected override void OnStart(string[] args) { - if (loggerFactory == null) - { - loggerFactory = LoggerFactory.Create(builder => builder.AddEventLog(new EventLogSettings - { - LogName = EventLog.Log, - MachineName = EventLog.MachineName, - SourceName = EventLog.Source, - Filter = (message, logLevel) => logLevel >= minimumLogLevel, - })); - - logger = loggerFactory.CreateLogger(); - } - - var watchdog = watchdogFactory.CreateWatchdog(this, loggerFactory); - - cancellationTokenSource?.Dispose(); - cancellationTokenSource = new CancellationTokenSource(); - var newArgs = new List(commandLineArguments.Length + args.Length + 1) { "--General:SetupWizardMode=Never", @@ -167,94 +100,18 @@ protected override void OnStart(string[] args) newArgs.AddRange(commandLineArguments); newArgs.AddRange(args); - watchdogTask = RunWatchdog(watchdog, newArgs.ToArray(), cancellationTokenSource.Token); - - if (!watchdogTask.IsCompleted && watchdog.InitialHostVersion >= new Version(5, 14, 0)) - { - logger.LogInformation("Waiting for host to finish starting..."); - using var streamReader = new StreamReader( - readyPipeServer, - Encoding.UTF8, - leaveOpen: true); - - var line = streamReader.ReadLine(); // Intentionally blocking service startup - logger.LogDebug("Pipe read: {line}", line); - } - - // Maybe we'll use this pipe more in the future, but for now leaving it open is just a resource waste - readyPipeServer.Dispose(); + serviceLifetime = new ServiceLifetime( + Stop, + signalChecker => watchdogFactory.CreateWatchdog(signalChecker, loggerFactory.Value), + loggerFactory.Value.CreateLogger(), + newArgs.ToArray()); } /// protected override void OnStop() { - cancellationTokenSource.Cancel(); - watchdogTask.GetAwaiter().GetResult(); - } - - /// - /// Executes the , stopping the service if it exits. - /// - /// The to run. - /// The arguments for the . - /// The for the operation. - /// A representing the running operation. - async Task RunWatchdog(IWatchdog watchdog, string[] args, CancellationToken cancellationToken) - { - await watchdog.RunAsync(false, args, cancellationToken); - - async void StopServiceAsync() - { - try - { - await Task.Run(Stop, cancellationToken); // DCT intentional - } - catch (OperationCanceledException ex) - { - logger.LogTrace(ex, "Stopping service cancelled!"); - } - catch (Exception ex) - { - logger.LogError(ex, "Error stopping service!"); - } - } - - StopServiceAsync(); - } - - /// - /// Sends a command to the main server process. - /// - /// One of the . - void SendCommandToHostThroughPipe(string command) - { - var localPipeServer = commandPipeServer; - 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); - } + var oldLifetime = Interlocked.Exchange(ref serviceLifetime, null); + oldLifetime?.DisposeAsync().GetAwaiter().GetResult(); } } } diff --git a/src/Tgstation.Server.Host.Service/ServiceLifetime.cs b/src/Tgstation.Server.Host.Service/ServiceLifetime.cs new file mode 100644 index 00000000000..f29cd0e9192 --- /dev/null +++ b/src/Tgstation.Server.Host.Service/ServiceLifetime.cs @@ -0,0 +1,210 @@ +using System; +using System.IO; +using System.IO.Pipes; +using System.Linq; +using System.Text; +using System.Threading; +using System.Threading.Tasks; + +using Microsoft.Extensions.Logging; + +using Tgstation.Server.Host.Common; +using Tgstation.Server.Host.Watchdog; + +namespace Tgstation.Server.Host.Service +{ + /// + /// Represents the lifetime of the service. + /// + sealed class ServiceLifetime : ISignalChecker, IAsyncDisposable + { + /// + /// The for the . + /// + readonly ILogger logger; + + /// + /// The that represents the running . + /// + readonly Task watchdogTask; + + /// + /// The for the . + /// + readonly CancellationTokenSource cancellationTokenSource; + + /// + /// The for sending to the server process. + /// + AnonymousPipeServerStream? commandPipeServer; + + /// + /// The for receiving the . + /// + AnonymousPipeServerStream? readyPipeServer; + + /// + /// Initializes a new instance of the class. + /// + /// An to manually stop the service. + /// A taking a and returning the to run. + /// The value of . + /// The arguments for the . + public ServiceLifetime(Action stopService, Func watchdogFactory, ILogger logger, string[] args) + { + ArgumentNullException.ThrowIfNull(stopService); + ArgumentNullException.ThrowIfNull(watchdogFactory); + this.logger = logger ?? throw new ArgumentNullException(nameof(logger)); + ArgumentNullException.ThrowIfNull(args); + + cancellationTokenSource = new CancellationTokenSource(); + watchdogTask = RunWatchdog( + stopService, + watchdogFactory(this), + args, + cancellationTokenSource.Token); + } + + /// + public async ValueTask DisposeAsync() + { + cancellationTokenSource.Cancel(); + await watchdogTask; + cancellationTokenSource.Dispose(); + + if (commandPipeServer != null) + await commandPipeServer.DisposeAsync(); + + if (readyPipeServer != null) + await readyPipeServer.DisposeAsync(); + } + + /// + public async ValueTask CheckSignals(Func startChildAndGetPid, CancellationToken cancellationToken) + { + try + { + await using (commandPipeServer = new AnonymousPipeServerStream(PipeDirection.Out, HandleInheritability.Inheritable)) + await using (readyPipeServer = new AnonymousPipeServerStream(PipeDirection.In, HandleInheritability.Inheritable)) + { + var (_, lifetimeTask) = startChildAndGetPid($"--Internal:CommandPipe={commandPipeServer.GetClientHandleAsString()} --Internal:ReadyPipe={readyPipeServer.GetClientHandleAsString()}"); + commandPipeServer.DisposeLocalCopyOfClientHandle(); + readyPipeServer.DisposeLocalCopyOfClientHandle(); + await lifetimeTask; + } + } + finally + { + readyPipeServer = null; + commandPipeServer = null; + } + } + + /// + /// Handle a custom service . + /// + /// The command sent to the service. + public void HandleCustomCommand(int command) + { + var commandsToCheck = PipeCommands.AllCommands; + foreach (var stringCommand in commandsToCheck) + { + var commandId = PipeCommands.GetServiceCommandId(stringCommand); + if (command == commandId) + { + SendCommandToHostThroughPipe(stringCommand); + return; + } + } + + logger.LogWarning("Received unknown service command: {command}", command); + } + + /// + /// Executes the , stopping the service if it exits. + /// + /// An to manually stop the service. + /// The to run. + /// The arguments for the . + /// The for the operation. + /// A representing the running operation. + async Task RunWatchdog(Action stopService, IWatchdog watchdog, string[] args, CancellationToken cancellationToken) + { + var localWatchdogTask = watchdog.RunAsync(false, args, cancellationToken); + + if (!localWatchdogTask.IsCompleted && (await watchdog.InitialHostVersion) >= new Version(5, 14, 0)) + if (readyPipeServer != null) + { + logger.LogInformation("Waiting for host to finish starting..."); + using var streamReader = new StreamReader( + readyPipeServer, + Encoding.UTF8, + leaveOpen: true); + + var line = streamReader.ReadLine(); // Intentionally blocking service startup + logger.LogDebug("Pipe read: {line}", line); + + // Maybe we'll use this pipe more in the future, but for now leaving it open is just a resource waste + readyPipeServer.Dispose(); + } + else + logger.LogError("Watchdog started and ready pipe was not initialized!"); + + await localWatchdogTask; + + async void StopServiceAsync() + { + try + { + // This can call OnStop which waits on this task to complete, must be threaded off or it will deadlock + await Task.Run(stopService, cancellationToken); + } + catch (OperationCanceledException ex) + { + logger.LogDebug(ex, "Stopping service cancelled!"); + } + catch (Exception ex) + { + logger.LogError(ex, "Error stopping service!"); + } + } + + StopServiceAsync(); + } + + /// + /// Sends a command to the main server process. + /// + /// One of the . + void SendCommandToHostThroughPipe(string command) + { + var localPipeServer = commandPipeServer; + 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 40b77d40cab..54710f6b47c 100644 --- a/src/Tgstation.Server.Host.Service/Tgstation.Server.Host.Service.csproj +++ b/src/Tgstation.Server.Host.Service/Tgstation.Server.Host.Service.csproj @@ -4,6 +4,7 @@ WinExe win-x86;win-x64 + enable $(TgsFrameworkVersion) $(TgsCoreVersion) @@ -19,15 +20,15 @@ - + - + - + - + diff --git a/src/Tgstation.Server.Host.Watchdog/ISignalChecker.cs b/src/Tgstation.Server.Host.Watchdog/ISignalChecker.cs index b23295dd0fd..5ba7f8dda58 100644 --- a/src/Tgstation.Server.Host.Watchdog/ISignalChecker.cs +++ b/src/Tgstation.Server.Host.Watchdog/ISignalChecker.cs @@ -12,9 +12,9 @@ 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 . + /// An to start the main process. It accepts an optional additional command line argument as a paramter and returns it's and lifetime . Must be called. /// The for the operation. /// A representing the running operation. - ValueTask CheckSignals(Func startChild, CancellationToken cancellationToken); + ValueTask CheckSignals(Func startChildAndGetPid, CancellationToken cancellationToken); } } diff --git a/src/Tgstation.Server.Host.Watchdog/IWatchdog.cs b/src/Tgstation.Server.Host.Watchdog/IWatchdog.cs index dadff2c41b3..c9f9e89da90 100644 --- a/src/Tgstation.Server.Host.Watchdog/IWatchdog.cs +++ b/src/Tgstation.Server.Host.Watchdog/IWatchdog.cs @@ -10,9 +10,9 @@ namespace Tgstation.Server.Host.Watchdog public interface IWatchdog { /// - /// Gets the current version of the host process. Set once begins and doesn't immediately return . + /// Gets a resulting in the current version of the host process. Guaranteed to complete once begins and doesn't immediately return . /// - Version InitialHostVersion { get; } + Task InitialHostVersion { get; } /// /// Run the . diff --git a/src/Tgstation.Server.Host.Watchdog/NoopSignalChecker.cs b/src/Tgstation.Server.Host.Watchdog/NoopSignalChecker.cs index fa5dd56f946..270cf633807 100644 --- a/src/Tgstation.Server.Host.Watchdog/NoopSignalChecker.cs +++ b/src/Tgstation.Server.Host.Watchdog/NoopSignalChecker.cs @@ -10,10 +10,10 @@ namespace Tgstation.Server.Host.Watchdog public sealed class NoopSignalChecker : ISignalChecker { /// - public ValueTask CheckSignals(Func startChild, CancellationToken cancellationToken) + public ValueTask CheckSignals(Func startChildAndGetPid, CancellationToken cancellationToken) { - ArgumentNullException.ThrowIfNull(startChild); - startChild(null); + ArgumentNullException.ThrowIfNull(startChildAndGetPid); + startChildAndGetPid(null); return ValueTask.CompletedTask; } } 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 4cda1e51b14..04f187c08e1 100644 --- a/src/Tgstation.Server.Host.Watchdog/Tgstation.Server.Host.Watchdog.csproj +++ b/src/Tgstation.Server.Host.Watchdog/Tgstation.Server.Host.Watchdog.csproj @@ -3,13 +3,14 @@ $(TgsFrameworkVersion) + enable false $(TgsHostWatchdogVersion) - + diff --git a/src/Tgstation.Server.Host.Watchdog/Watchdog.cs b/src/Tgstation.Server.Host.Watchdog/Watchdog.cs index a13dc2f5f1b..4813383440d 100644 --- a/src/Tgstation.Server.Host.Watchdog/Watchdog.cs +++ b/src/Tgstation.Server.Host.Watchdog/Watchdog.cs @@ -20,7 +20,7 @@ namespace Tgstation.Server.Host.Watchdog sealed class Watchdog : IWatchdog { /// - public Version InitialHostVersion { get; private set; } + public Task InitialHostVersion => initialHostVersionTcs.Task; /// /// The for the . @@ -32,6 +32,11 @@ sealed class Watchdog : IWatchdog /// readonly ILogger logger; + /// + /// Backing for . + /// + readonly TaskCompletionSource initialHostVersionTcs; + /// /// Initializes a new instance of the class. /// @@ -41,6 +46,8 @@ public Watchdog(ISignalChecker signalChecker, ILogger logger) { this.signalChecker = signalChecker ?? throw new ArgumentNullException(nameof(signalChecker)); this.logger = logger ?? throw new ArgumentNullException(nameof(logger)); + + initialHostVersionTcs = new TaskCompletionSource(); } /// @@ -54,11 +61,18 @@ public async ValueTask RunAsync(bool runConfigure, string[] args, Cancella currentProcessId = currentProc.Id; logger.LogDebug("PID: {pid}", currentProcessId); - string updateDirectory = null; + string? updateDirectory = null; try { var isWindows = RuntimeInformation.IsOSPlatform(OSPlatform.Windows); - var dotnetPath = GetDotnetPath(isWindows); + var dotnetPath = DotnetHelper.GetPotentialDotnetPaths(isWindows) + .Where(potentialDotnetPath => + { + logger.LogTrace("Checking for dotnet at {potentialDotnetPath}", potentialDotnetPath); + return File.Exists(potentialDotnetPath); + }) + .FirstOrDefault(); + 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!"); @@ -69,6 +83,11 @@ public async ValueTask RunAsync(bool runConfigure, string[] args, Cancella var executingAssembly = Assembly.GetExecutingAssembly(); var rootLocation = Path.GetDirectoryName(executingAssembly.Location); + if (rootLocation == null) + { + logger.LogCritical("Failed to get the directory name of the executing assembly: {location}", executingAssembly.Location); + return false; + } var assemblyStoragePath = Path.Combine(rootLocation, "lib"); // always always next to watchdog @@ -82,7 +101,10 @@ public async ValueTask RunAsync(bool runConfigure, string[] args, Cancella Directory.Delete(assemblyStoragePath, true); Directory.CreateDirectory(defaultAssemblyPath); - var sourcePath = "../../../../Tgstation.Server.Host/bin/Debug/net6.0"; + var sourcePath = Path.GetFullPath( + Path.Combine( + rootLocation, + "../../../../Tgstation.Server.Host/bin/Debug/net8.0")); foreach (string dirPath in Directory.GetDirectories(sourcePath, "*", SearchOption.AllDirectories)) Directory.CreateDirectory(dirPath.Replace(sourcePath, defaultAssemblyPath, StringComparison.Ordinal)); @@ -112,9 +134,18 @@ public async ValueTask RunAsync(bool runConfigure, string[] args, Cancella return false; } - InitialHostVersion = Version.Parse(FileVersionInfo.GetVersionInfo(assemblyPath).FileVersion); + var fileVersion = FileVersionInfo.GetVersionInfo(assemblyPath).FileVersion; + if (fileVersion == null) + { + logger.LogCritical("Failed to parse version info from {assemblyPath}!", assemblyPath); + return false; + } + + initialHostVersionTcs.SetResult( + Version.Parse( + fileVersion)); - var watchdogVersion = executingAssembly.GetName().Version.Semver().ToString(); + var watchdogVersion = executingAssembly.GetName().Version?.Semver().ToString(); while (!cancellationToken.IsCancellationRequested) using (logger.BeginScope("Host invocation")) @@ -151,8 +182,8 @@ public async ValueTask RunAsync(bool runConfigure, string[] args, Cancella var killedHostProcess = false; try { - Task processTask = null; - (int, Task) StartProcess(string additionalArg) + Task? processTask = null; + (int, Task) StartProcess(string? additionalArg) { if (additionalArg != null) process.StartInfo.Arguments += $" {additionalArg}"; @@ -192,7 +223,7 @@ public async ValueTask RunAsync(bool runConfigure, string[] args, Cancella var checkerTask = signalChecker.CheckSignals(StartProcess, cts.Token); try { - await processTask; + await processTask!; } finally { @@ -331,10 +362,11 @@ public async ValueTask RunAsync(bool runConfigure, string[] args, Cancella catch (OperationCanceledException ex) { logger.LogDebug(ex, "Exiting due to cancellation..."); - if (!Directory.Exists(updateDirectory)) - File.Delete(updateDirectory); - else - Directory.Delete(updateDirectory, true); + if (updateDirectory != null) + if (!Directory.Exists(updateDirectory)) + File.Delete(updateDirectory); + else + Directory.Delete(updateDirectory, true); } catch (Exception ex) { @@ -350,48 +382,5 @@ public async ValueTask RunAsync(bool runConfigure, string[] args, Cancella } #pragma warning restore CA1502 #pragma warning restore CA1506 - - /// - /// Gets the path to the dotnet executable. - /// - /// If the current system is a Windows OS. - /// The path to the dotnet executable. - string GetDotnetPath(bool isWindows) - { - var enviromentPath = Environment.GetEnvironmentVariable("PATH"); - var paths = enviromentPath.Split(';'); - - var exeName = "dotnet"; - IEnumerable enumerator; - if (isWindows) - { - exeName += ".exe"; - enumerator = new List(paths) - { - "C:/Program Files/dotnet", - "C:/Program Files (x86)/dotnet", - }; - } - else - 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)); - - return enumerator - .Where(potentialDotnetPath => - { - logger.LogTrace("Checking for dotnet at {potentialDotnetPath}", potentialDotnetPath); - return File.Exists(potentialDotnetPath); - }) - .FirstOrDefault(); - } } } diff --git a/src/Tgstation.Server.Host/.config/dotnet-tools.json b/src/Tgstation.Server.Host/.config/dotnet-tools.json index 37aa7721e35..c03564f9704 100644 --- a/src/Tgstation.Server.Host/.config/dotnet-tools.json +++ b/src/Tgstation.Server.Host/.config/dotnet-tools.json @@ -3,7 +3,7 @@ "isRoot": true, "tools": { "dotnet-ef": { - "version": "7.0.13", + "version": "8.0.0", "commands": [ "dotnet-ef" ] diff --git a/src/Tgstation.Server.Host/Components/Byond/ByondExecutableLock.cs b/src/Tgstation.Server.Host/Components/Byond/ByondExecutableLock.cs deleted file mode 100644 index 334b3469ea1..00000000000 --- a/src/Tgstation.Server.Host/Components/Byond/ByondExecutableLock.cs +++ /dev/null @@ -1,28 +0,0 @@ -using System; - -using Tgstation.Server.Host.Utils; - -namespace Tgstation.Server.Host.Components.Byond -{ - /// - sealed class ByondExecutableLock : ReferenceCounter, IByondExecutableLock - { - /// - public Version Version => Instance.Version; - - /// - public string DreamDaemonPath => Instance.DreamDaemonPath; - - /// - public string DreamMakerPath => Instance.DreamMakerPath; - - /// - public bool SupportsCli => Instance.SupportsCli; - - /// - public bool SupportsMapThreads => Instance.SupportsMapThreads; - - /// - public void DoNotDeleteThisSession() => DangerousDropReference(); - } -} diff --git a/src/Tgstation.Server.Host/Components/Byond/ByondInstallation.cs b/src/Tgstation.Server.Host/Components/Byond/ByondInstallation.cs deleted file mode 100644 index 8554190d0f6..00000000000 --- a/src/Tgstation.Server.Host/Components/Byond/ByondInstallation.cs +++ /dev/null @@ -1,54 +0,0 @@ -using System; -using System.Threading.Tasks; - -namespace Tgstation.Server.Host.Components.Byond -{ - /// - sealed class ByondInstallation : IByondInstallation - { - /// - public Version Version { get; } - - /// - public string DreamDaemonPath { get; } - - /// - public string DreamMakerPath { get; } - - /// - public bool SupportsCli { get; } - - /// - public bool SupportsMapThreads { get; } - - /// - /// The that completes when the BYOND version finished installing. - /// - public Task InstallationTask { get; } - - /// - /// Initializes a new instance of the class. - /// - /// The value of . - /// The value of . - /// The value of . - /// The value of . - /// The value of . - /// The value of . - public ByondInstallation( - Task installationTask, - Version version, - string dreamDaemonPath, - string dreamMakerPath, - bool supportsCli, - bool supportsMapThreads) - { - InstallationTask = installationTask ?? throw new ArgumentNullException(nameof(installationTask)); - Version = version ?? throw new ArgumentNullException(nameof(version)); - DreamDaemonPath = dreamDaemonPath ?? throw new ArgumentNullException(nameof(dreamDaemonPath)); - DreamMakerPath = dreamMakerPath ?? throw new ArgumentNullException(nameof(dreamMakerPath)); - SupportsCli = supportsCli; - SupportsMapThreads = supportsMapThreads; - } - } -} diff --git a/src/Tgstation.Server.Host/Components/Byond/ByondInstallerBase.cs b/src/Tgstation.Server.Host/Components/Byond/ByondInstallerBase.cs deleted file mode 100644 index 2478acdc1f7..00000000000 --- a/src/Tgstation.Server.Host/Components/Byond/ByondInstallerBase.cs +++ /dev/null @@ -1,106 +0,0 @@ -using System; -using System.Globalization; -using System.IO; -using System.Threading; -using System.Threading.Tasks; - -using Microsoft.Extensions.Logging; - -using Tgstation.Server.Host.IO; - -namespace Tgstation.Server.Host.Components.Byond -{ - /// - abstract class ByondInstallerBase : IByondInstaller - { - /// - /// The name of BYOND's cache directory. - /// - const string CacheDirectoryName = "cache"; - - /// - /// The first of BYOND that supports the '-map-threads' parameter on DreamDaemon. - /// - public static Version MapThreadsVersion => new (515, 1609); - - /// - public abstract string DreamMakerName { get; } - - /// - public abstract string PathToUserByondFolder { get; } - - /// - /// Gets the URL formatter string for downloading a byond version of {0:Major} {1:Minor}. - /// - protected abstract string ByondRevisionsUrlTemplate { get; } - - /// - /// Gets the for the . - /// - protected IIOManager IOManager { get; } - - /// - /// Gets the for the . - /// - protected ILogger Logger { get; } - - /// - /// The for the . - /// - readonly IFileDownloader fileDownloader; - - /// - /// Initializes a new instance of the class. - /// - /// The value of . - /// The value of . - /// The value of . - protected ByondInstallerBase(IIOManager ioManager, IFileDownloader fileDownloader, ILogger logger) - { - IOManager = ioManager ?? throw new ArgumentNullException(nameof(ioManager)); - this.fileDownloader = fileDownloader ?? throw new ArgumentNullException(nameof(fileDownloader)); - Logger = logger ?? throw new ArgumentNullException(nameof(logger)); - } - - /// - public abstract string GetDreamDaemonName(Version version, out bool supportsCli, out bool supportsMapThreads); - - /// - public async Task CleanCache(CancellationToken cancellationToken) - { - try - { - Logger.LogDebug("Cleaning BYOND cache..."); - await IOManager.DeleteDirectory( - IOManager.ConcatPath( - PathToUserByondFolder, - CacheDirectoryName), - cancellationToken); - } - catch (Exception ex) when (ex is not OperationCanceledException) - { - Logger.LogWarning(ex, "Error deleting BYOND cache!"); - } - } - - /// - public abstract ValueTask InstallByond(Version version, string path, CancellationToken cancellationToken); - - /// - public abstract ValueTask UpgradeInstallation(Version version, string path, CancellationToken cancellationToken); - - /// - public async ValueTask DownloadVersion(Version version, CancellationToken cancellationToken) - { - ArgumentNullException.ThrowIfNull(version); - - Logger.LogTrace("Downloading BYOND version {major}.{minor}...", version.Major, version.Minor); - var url = String.Format(CultureInfo.InvariantCulture, ByondRevisionsUrlTemplate, version.Major, version.Minor); - - await using var download = fileDownloader.DownloadFile(new Uri(url), null); - await using var buffer = new BufferedFileStreamProvider( - await download.GetResult(cancellationToken)); - return await buffer.GetOwnedResult(cancellationToken); - } - } -} diff --git a/src/Tgstation.Server.Host/Components/Byond/IByondInstallation.cs b/src/Tgstation.Server.Host/Components/Byond/IByondInstallation.cs deleted file mode 100644 index 52a4e694b71..00000000000 --- a/src/Tgstation.Server.Host/Components/Byond/IByondInstallation.cs +++ /dev/null @@ -1,35 +0,0 @@ -using System; - -namespace Tgstation.Server.Host.Components.Byond -{ - /// - /// Represents a BYOND installation. - /// - public interface IByondInstallation - { - /// - /// The of the . - /// - Version Version { get; } - - /// - /// The full path to the DreamDaemon executable. - /// - string DreamDaemonPath { get; } - - /// - /// The full path to the dm/DreamMaker executable. - /// - string DreamMakerPath { get; } - - /// - /// If supports being run as a command-line application. - /// - bool SupportsCli { get; } - - /// - /// If supports the -map-threads parameter. - /// - bool SupportsMapThreads { get; } - } -} diff --git a/src/Tgstation.Server.Host/Components/Byond/IByondInstaller.cs b/src/Tgstation.Server.Host/Components/Byond/IByondInstaller.cs deleted file mode 100644 index 9c353378303..00000000000 --- a/src/Tgstation.Server.Host/Components/Byond/IByondInstaller.cs +++ /dev/null @@ -1,65 +0,0 @@ -using System; -using System.IO; -using System.Threading; -using System.Threading.Tasks; - -namespace Tgstation.Server.Host.Components.Byond -{ - /// - /// For downloading and installing BYOND extractions for a given system. - /// - interface IByondInstaller - { - /// - /// Get the file name of the DreamMaker executable. - /// - string DreamMakerName { get; } - - /// - /// The path to the BYOND folder for the user. - /// - string PathToUserByondFolder { get; } - - /// - /// Get the file name of the DreamDaemon executable. - /// - /// The of BYOND to select the executable name for. - /// Whether or not the returned path supports being run as a command-line application. - /// Whether or not the returned path supports the '-map-threads' parameter. - /// The file name of the DreamDaemon executable. - string GetDreamDaemonName(Version version, out bool supportsCli, out bool supportsMapThreads); - - /// - /// Download a given BYOND . - /// - /// The of BYOND to download. - /// The for the operation. - /// A resulting in a of the zipfile. - ValueTask DownloadVersion(Version version, CancellationToken cancellationToken); - - /// - /// Does actions necessary to get an extracted BYOND installation working. - /// - /// The of BYOND being installed. - /// The path to the BYOND installation. - /// The for the operation. - /// A representing the running operation. - ValueTask InstallByond(Version version, string path, CancellationToken cancellationToken); - - /// - /// Does actions necessary to get upgrade a BYOND version installed by a previous version of TGS. - /// - /// The of BYOND being installed. - /// The path to the BYOND installation. - /// The for the operation. - /// A representing the running operation. - ValueTask UpgradeInstallation(Version version, string path, CancellationToken cancellationToken); - - /// - /// Attempts to cleans the BYOND cache folder for the system. - /// - /// The for the operation. - /// A representing the running operation. - Task CleanCache(CancellationToken cancellationToken); - } -} diff --git a/src/Tgstation.Server.Host/Components/Chat/ChannelMapping.cs b/src/Tgstation.Server.Host/Components/Chat/ChannelMapping.cs index d7430d7c69e..19581b09bbb 100644 --- a/src/Tgstation.Server.Host/Components/Chat/ChannelMapping.cs +++ b/src/Tgstation.Server.Host/Components/Chat/ChannelMapping.cs @@ -1,4 +1,6 @@ -namespace Tgstation.Server.Host.Components.Chat +using System; + +namespace Tgstation.Server.Host.Components.Chat { /// /// Represents a mapping of a . @@ -38,6 +40,15 @@ sealed class ChannelMapping /// /// The with the mapped Id. /// - public ChannelRepresentation Channel { get; set; } + public ChannelRepresentation Channel { get; } + + /// + /// Initializes a new instance of the class. + /// + /// The value of . + public ChannelMapping(ChannelRepresentation channel) + { + Channel = channel ?? throw new ArgumentNullException(nameof(channel)); + } } } diff --git a/src/Tgstation.Server.Host/Components/Chat/ChannelRepresentation.cs b/src/Tgstation.Server.Host/Components/Chat/ChannelRepresentation.cs index a24f416ec11..b1d4aa3ad20 100644 --- a/src/Tgstation.Server.Host/Components/Chat/ChannelRepresentation.cs +++ b/src/Tgstation.Server.Host/Components/Chat/ChannelRepresentation.cs @@ -14,7 +14,7 @@ public sealed class ChannelRepresentation /// /// Backing field for . Represented as a to avoid BYOND percision loss. /// - public string Id { get; set; } + public string Id { get; private set; } /// /// The channel Id. @@ -30,12 +30,12 @@ public ulong RealId /// /// The user friendly name of the . /// - public string FriendlyName { get; set; } + public string FriendlyName { get; } /// /// The name of the connection the belongs to. /// - public string ConnectionName { get; set; } + public string ConnectionName { get; } /// /// If this is considered a channel for admin commands. @@ -45,16 +45,30 @@ public ulong RealId /// /// If this is a 1-to-1 chat channel. /// - public bool IsPrivateChannel { get; set; } + public bool IsPrivateChannel { get; init; } /// /// For user use. /// - public string Tag { get; set; } + public string? Tag { get; set; } /// /// If this channel supports embeds. /// - public bool EmbedsSupported { get; set; } + public bool EmbedsSupported { get; init; } + + /// + /// Initializes a new instance of the class. + /// + /// The value of . + /// The value of . + /// The value of /. + public ChannelRepresentation(string connectionName, string friendlyName, ulong id) + { + ConnectionName = connectionName ?? throw new ArgumentNullException(nameof(connectionName)); + FriendlyName = friendlyName ?? throw new ArgumentNullException(nameof(friendlyName)); + Id = null!; + RealId = id; + } } } diff --git a/src/Tgstation.Server.Host/Components/Chat/ChatManager.cs b/src/Tgstation.Server.Host/Components/Chat/ChatManager.cs index 680318d0877..d53e997b256 100644 --- a/src/Tgstation.Server.Host/Components/Chat/ChatManager.cs +++ b/src/Tgstation.Server.Host/Components/Chat/ChatManager.cs @@ -12,6 +12,7 @@ using Serilog.Context; +using Tgstation.Server.Api.Models; using Tgstation.Server.Api.Models.Internal; using Tgstation.Server.Common.Extensions; using Tgstation.Server.Host.Components.Chat.Commands; @@ -23,8 +24,7 @@ namespace Tgstation.Server.Host.Components.Chat { /// - // TODO: Decomplexify -#pragma warning disable CA1506 +#pragma warning disable CA1506 // TODO: Decomplexify sealed class ChatManager : IChatManager, IRestartHandler { /// @@ -63,12 +63,12 @@ sealed class ChatManager : IChatManager, IRestartHandler readonly Dictionary builtinCommands; /// - /// Map of s in use, keyed by . + /// Map of s in use, keyed by . /// readonly Dictionary providers; /// - /// Map of s used to guard concurrent access to , keyed by . + /// Map of s used to guard concurrent access to , keyed by . /// readonly ConcurrentDictionary changeChannelSemaphores; @@ -100,17 +100,17 @@ sealed class ChatManager : IChatManager, IRestartHandler /// /// The for the . /// - ICustomCommandHandler customCommandHandler; + ICustomCommandHandler? customCommandHandler; /// /// The that monitors incoming chat messages. /// - Task chatHandler; + Task? chatHandler; /// /// A that represents the s initial connection. /// - Task initialProviderConnectionsTask; + Task? initialProviderConnectionsTask; /// /// A that represents all sent messages. @@ -233,7 +233,7 @@ public async ValueTask ChangeChannels(long connectionId, IEnumerable kvp.Value.Select( - channelRepresentation => new ChannelMapping + channelRepresentation => new ChannelMapping(channelRepresentation) { IsWatchdogChannel = kvp.Key.IsWatchdogChannel == true, IsUpdatesChannel = kvp.Key.IsUpdatesChannel == true, @@ -241,7 +241,6 @@ public async ValueTask ChangeChannels(long connectionId, IEnumerable x.Id); + var newSettingsEnabled = Models.ModelExtensions.Require(newSettings, x => x.Enabled); lock (providers) { // raw settings changes forces a rebuild of the provider - if (providers.ContainsKey(newSettings.Id.Value)) - disconnectTask = DeleteConnection(newSettings.Id.Value, cancellationToken); + if (providers.ContainsKey(newSettingsId)) + disconnectTask = DeleteConnection(newSettingsId, cancellationToken); else disconnectTask = Task.CompletedTask; - if (newSettings.Enabled.Value) + if (newSettingsEnabled) { provider = providerFactory.CreateProvider(newSettings); - providers.Add(newSettings.Id.Value, provider); + providers.Add(newSettingsId, provider); } } lock (mappedChannels) - foreach (var oldMappedChannelId in mappedChannels.Where(x => x.Value.ProviderId == newSettings.Id).Select(x => x.Key).ToList()) + foreach (var oldMappedChannelId in mappedChannels.Where(x => x.Value.ProviderId == newSettingsId).Select(x => x.Key).ToList()) mappedChannels.Remove(oldMappedChannelId); await disconnectTask; @@ -316,8 +317,8 @@ public async ValueTask ChangeSettings(Models.ChatBot newSettings, CancellationTo } var reconnectionUpdateTask = provider?.SetReconnectInterval( - newSettings.ReconnectionInterval.Value, - newSettings.Enabled.Value) + Models.ModelExtensions.Require(newSettings, x => x.ReconnectionInterval), + newSettingsEnabled) ?? Task.CompletedTask; lock (activeChatBots) { @@ -325,7 +326,7 @@ public async ValueTask ChangeSettings(Models.ChatBot newSettings, CancellationTo if (originalChatBot != null) activeChatBots.Remove(originalChatBot); - activeChatBots.Add(new Models.ChatBot + activeChatBots.Add(new Models.ChatBot(newSettings.Channels) { Id = newSettings.Id, ConnectionString = newSettings.ConnectionString, @@ -333,7 +334,6 @@ public async ValueTask ChangeSettings(Models.ChatBot newSettings, CancellationTo Name = newSettings.Name, ReconnectionInterval = newSettings.ReconnectionInterval, Provider = newSettings.Provider, - Channels = newSettings.Channels, }); } @@ -356,7 +356,7 @@ public void QueueWatchdogMessage(string message) message = String.Format(CultureInfo.InvariantCulture, "WD: {0}", message); - if (!initialProviderConnectionsTask.IsCompleted) + if (!initialProviderConnectionsTask!.IsCompleted) logger.LogTrace("Waiting for initial provider connections before sending watchdog message..."); // Reimplementing QueueMessage @@ -375,12 +375,12 @@ public void QueueWatchdogMessage(string message) } /// - public Func> QueueDeploymentMessage( + public Func> QueueDeploymentMessage( Models.RevisionInformation revisionInformation, - Version byondVersion, + EngineVersion engineVersion, DateTimeOffset? estimatedCompletionTime, - string gitHubOwner, - string gitHubRepo, + string? gitHubOwner, + string? gitHubRepo, bool localCommitPushed) { List wdChannels; @@ -389,17 +389,17 @@ public Func> QueueDeploymentMessage( logger.LogTrace("Sending deployment message for RevisionInformation: {revisionInfoId}", revisionInformation.Id); - var callbacks = new List>>>(); + var callbacks = new List>>>(); var task = Task.WhenAll( wdChannels.Select( async x => { - ChannelMapping channelMapping; + ChannelMapping? channelMapping; lock (mappedChannels) if (!mappedChannels.TryGetValue(x, out channelMapping)) return; - IProvider provider; + IProvider? provider; lock (providers) if (!providers.TryGetValue(channelMapping.ProviderId, out provider)) return; @@ -407,7 +407,7 @@ public Func> QueueDeploymentMessage( { var callback = await provider.SendUpdateMessage( revisionInformation, - byondVersion, + engineVersion, estimatedCompletionTime, gitHubOwner, gitHubRepo, @@ -430,8 +430,8 @@ public Func> QueueDeploymentMessage( AddMessageTask(task); Task callbackTask; - Func finalUpdateAction = null; - async Task CallbackTask(string errorMessage, string dreamMakerOutput) + Func? finalUpdateAction = null; + async Task CallbackTask(string? errorMessage, string dreamMakerOutput) { await task; var callbackResults = await ValueTaskExtensions.WhenAll( @@ -456,7 +456,7 @@ async Task CompletionTask(bool active) return; } - AddMessageTask(finalUpdateAction(active)); + AddMessageTask(finalUpdateAction!(active)); } return (errorMessage, dreamMakerOutput) => @@ -494,7 +494,7 @@ public IChatTrackingContext CreateTrackingContext() if (customCommandHandler == null) throw new InvalidOperationException("RegisterCommandHandler() hasn't been called!"); - IChatTrackingContext context = null; + IChatTrackingContext context = null!; lock (mappedChannels) context = new ChatTrackingContext( customCommandHandler, @@ -523,7 +523,7 @@ async Task UpdateTrackingContext(IChatTrackingContext channelSink, IEnumerable - public ValueTask HandleRestart(Version updateVersion, bool handlerMayDelayShutdownWithExtremelyLongRunningTasks, CancellationToken cancellationToken) + public ValueTask HandleRestart(Version? updateVersion, bool handlerMayDelayShutdownWithExtremelyLongRunningTasks, CancellationToken cancellationToken) { var message = updateVersion == null ? $"TGS: {(handlerMayDelayShutdownWithExtremelyLongRunningTasks ? "Graceful shutdown" : "Going down")}..." @@ -613,14 +613,14 @@ public ValueTask HandleRestart(Version updateVersion, bool handlerMayDelayShutdo /// /// Remove a from optionally removing the provider itself from and updating the as well. /// - /// The of the to delete. + /// The of the to delete. /// If the provider should be removed from and should be update. /// The for the operation. /// A resulting in the being removed if it exists, otherwise. - async ValueTask RemoveProviderChannels(long connectionId, bool removeProvider, CancellationToken cancellationToken) + async ValueTask RemoveProviderChannels(long connectionId, bool removeProvider, CancellationToken cancellationToken) { logger.LogTrace("RemoveProviderChannels {connectionId}...", connectionId); - IProvider provider; + IProvider? provider; lock (providers) { if (!providers.TryGetValue(connectionId, out provider)) @@ -662,7 +662,7 @@ async ValueTask RemoveProviderChannels(long connectionId, bool remove async ValueTask RemapProvider(IProvider provider, CancellationToken cancellationToken) { logger.LogTrace("Remapping channels for provider reconnection..."); - IEnumerable channelsToMap; + IEnumerable? channelsToMap; long providerId; lock (providers) providerId = providers.Where(x => x.Value == provider).Select(x => x.Key).First(); @@ -683,7 +683,7 @@ async ValueTask RemapProvider(IProvider provider, CancellationToken cancellation /// The for the operation. /// A representing the running operation. #pragma warning disable CA1502 - async ValueTask ProcessMessage(IProvider provider, Message message, bool recursed, CancellationToken cancellationToken) + async ValueTask ProcessMessage(IProvider provider, Message? message, bool recursed, CancellationToken cancellationToken) #pragma warning restore CA1502 { if (!provider.Connected) @@ -764,11 +764,10 @@ ValueTask TextReply(string reply) => SendMessage( message.User.Channel.ConnectionName, message.User.FriendlyName, newId); - mappedChannels.Add(newId, new ChannelMapping + mappedChannels.Add(newId, new ChannelMapping(message.User.Channel) { ProviderChannelId = message.User.Channel.RealId, ProviderId = providerId, - Channel = message.User.Channel, }); logger.LogTrace( @@ -798,7 +797,7 @@ ValueTask TextReply(string reply) => SendMessage( var mappingChannelRepresentation = mappedChannel.Value.Value.Channel; - message.User.Channel.Id = mappingChannelRepresentation.Id; + message.User.Channel.RealId = mappingChannelRepresentation.RealId; message.User.Channel.Tag = mappingChannelRepresentation.Tag; message.User.Channel.IsAdminChannel = mappingChannelRepresentation.IsAdminChannel; } @@ -812,11 +811,9 @@ ValueTask TextReply(string reply) => SendMessage( if (address.Length > 1 && (address.Last() == ':' || address.Last() == ',')) address = address[0..^1]; - address = address.ToUpperInvariant(); - var addressed = - address == CommonMention.ToUpperInvariant() - || address == provider.BotMention.ToUpperInvariant(); + address.Equals(CommonMention, StringComparison.OrdinalIgnoreCase) + || address.Equals(provider.BotMention, StringComparison.OrdinalIgnoreCase); // no mention if (!addressed && !message.User.Channel.IsPrivateChannel) @@ -842,16 +839,16 @@ ValueTask TextReply(string reply) => SendMessage( splits.RemoveAt(0); var arguments = String.Join(" ", splits); - Tuple GetCommand() + Tuple? GetCommand() { if (!builtinCommands.TryGetValue(command, out var handler)) return trackingContexts .Where(trackingContext => trackingContext.Active) - .SelectMany(trackingContext => trackingContext.CustomCommands.Select(customCommand => Tuple.Create(customCommand, trackingContext))) + .SelectMany(trackingContext => trackingContext.CustomCommands.Select(customCommand => Tuple.Create(customCommand, trackingContext))) .Where(tuple => tuple.Item1.Name.Equals(command, StringComparison.OrdinalIgnoreCase)) .FirstOrDefault(); - return Tuple.Create(handler, null); + return Tuple.Create(handler, null); } const string UnknownCommandMessage = "TGS: Unknown command! Type '?' or 'help' for available commands."; @@ -934,11 +931,11 @@ Tuple GetCommand() async Task MonitorMessages(CancellationToken cancellationToken) { logger.LogTrace("Starting processing loop..."); - var messageTasks = new Dictionary>(); + var messageTasks = new Dictionary>(); ValueTask activeProcessingTask = ValueTask.CompletedTask; try { - Task updatedTask = null; + Task? updatedTask = null; while (!cancellationToken.IsCancellationRequested) { if (updatedTask?.IsCompleted != false) @@ -1024,7 +1021,7 @@ async ValueTask WrapProcessMessage() /// The to send. /// The for the operation. /// A representing the running operation. - ValueTask SendMessage(IEnumerable channelIds, Message replyTo, MessageContent message, CancellationToken cancellationToken) + ValueTask SendMessage(IEnumerable channelIds, Message? replyTo, MessageContent message, CancellationToken cancellationToken) { var channelIdsList = channelIds.ToList(); @@ -1034,17 +1031,17 @@ ValueTask SendMessage(IEnumerable channelIds, Message replyTo, MessageCon message.Embed != null ? " (with embed)" : String.Empty, String.Join(", ", channelIdsList)); - if (!channelIdsList.Any()) + if (channelIdsList.Count == 0) return ValueTask.CompletedTask; return ValueTaskExtensions.WhenAll( channelIdsList.Select(x => { - ChannelMapping channelMapping; + ChannelMapping? channelMapping; lock (mappedChannels) if (!mappedChannels.TryGetValue(x, out channelMapping)) return ValueTask.CompletedTask; - IProvider provider; + IProvider? provider; lock (providers) if (!providers.TryGetValue(channelMapping.ProviderId, out provider)) return ValueTask.CompletedTask; @@ -1101,7 +1098,7 @@ async Task SendMessageTask() { var cancellationToken = handlerCts.Token; if (waitForConnections) - await initialProviderConnectionsTask.WaitAsync(cancellationToken); + await initialProviderConnectionsTask!.WaitAsync(cancellationToken); await SendMessage( channelIdsFactory(), diff --git a/src/Tgstation.Server.Host/Components/Chat/ChatManagerFactory.cs b/src/Tgstation.Server.Host/Components/Chat/ChatManagerFactory.cs index 12427da6bef..a1697a97117 100644 --- a/src/Tgstation.Server.Host/Components/Chat/ChatManagerFactory.cs +++ b/src/Tgstation.Server.Host/Components/Chat/ChatManagerFactory.cs @@ -7,6 +7,7 @@ using Tgstation.Server.Host.Components.Chat.Commands; using Tgstation.Server.Host.Components.Chat.Providers; using Tgstation.Server.Host.Core; +using Tgstation.Server.Host.Models; namespace Tgstation.Server.Host.Components.Chat { @@ -54,6 +55,6 @@ public IChatManager CreateChatManager( serverControl, loggerFactory, loggerFactory.CreateLogger(), - initialChatBots.Where(x => x.Enabled.Value)); + initialChatBots.Where(x => x.Require(y => y.Enabled))); } } diff --git a/src/Tgstation.Server.Host/Components/Chat/ChatTrackingContext.cs b/src/Tgstation.Server.Host/Components/Chat/ChatTrackingContext.cs index 369ccdeeebe..02b6675323f 100644 --- a/src/Tgstation.Server.Host/Components/Chat/ChatTrackingContext.cs +++ b/src/Tgstation.Server.Host/Components/Chat/ChatTrackingContext.cs @@ -7,16 +7,17 @@ using Microsoft.Extensions.Logging; using Tgstation.Server.Host.Components.Chat.Commands; +using Tgstation.Server.Host.Utils; namespace Tgstation.Server.Host.Components.Chat { /// - sealed class ChatTrackingContext : IChatTrackingContext + sealed class ChatTrackingContext : DisposeInvoker, IChatTrackingContext { /// public bool Active { - get => active; + get => active && !IsDisposed; set { if (active == value) @@ -61,24 +62,19 @@ public IEnumerable CustomCommands readonly ILogger logger; /// - /// for modifying , , and . + /// for modifying and calling . /// readonly object synchronizationLock; - /// - /// Backing field for . - /// - IReadOnlyCollection customCommands; - /// /// The if any. /// - IChannelSink channelSink; + volatile IChannelSink? channelSink; /// - /// The to run when d. + /// Backing field for . /// - Action onDispose; + IReadOnlyCollection customCommands; /// /// Backing field for . @@ -91,45 +87,31 @@ public IEnumerable CustomCommands /// The value of . /// The initial value of . /// The value of . - /// The value of . + /// The action for the . public ChatTrackingContext( ICustomCommandHandler customCommandHandler, IEnumerable initialChannels, ILogger logger, - Action onDispose) + Action disposeAction) + : base(disposeAction) { this.customCommandHandler = customCommandHandler ?? throw new ArgumentNullException(nameof(customCommandHandler)); Channels = initialChannels?.ToList() ?? throw new ArgumentNullException(nameof(initialChannels)); this.logger = logger ?? throw new ArgumentNullException(nameof(logger)); - this.onDispose = onDispose ?? throw new ArgumentNullException(nameof(onDispose)); synchronizationLock = new object(); Active = true; customCommands = Array.Empty(); } - /// - public void Dispose() - { - lock (synchronizationLock) - { - onDispose?.Invoke(); - onDispose = null; - } - } - /// public void SetChannelSink(IChannelSink channelSink) { ArgumentNullException.ThrowIfNull(channelSink); - lock (synchronizationLock) - { - if (this.channelSink != null) - throw new InvalidOperationException("channelSink already set!"); - - this.channelSink = channelSink; - } + var originalValue = Interlocked.CompareExchange(ref this.channelSink, channelSink, null); + if (originalValue != null) + throw new InvalidOperationException("channelSink already set!"); } /// diff --git a/src/Tgstation.Server.Host/Components/Chat/ChatUser.cs b/src/Tgstation.Server.Host/Components/Chat/ChatUser.cs index 6de07109eac..69757279b07 100644 --- a/src/Tgstation.Server.Host/Components/Chat/ChatUser.cs +++ b/src/Tgstation.Server.Host/Components/Chat/ChatUser.cs @@ -13,7 +13,7 @@ public sealed class ChatUser /// /// Backing field for . Represented as a to avoid BYOND percision loss. /// - public string Id { get; set; } + public string Id { get; private set; } /// /// The internal user id. @@ -21,23 +21,40 @@ public sealed class ChatUser [JsonIgnore] public ulong RealId { - get => UInt64.Parse(Id, CultureInfo.InvariantCulture); - set => Id = value.ToString(CultureInfo.InvariantCulture); + get => UInt64.Parse(Id!, CultureInfo.InvariantCulture); + private set => Id = value.ToString(CultureInfo.InvariantCulture); } /// /// The friendly name of the user. /// - public string FriendlyName { get; set; } + public string FriendlyName { get; } /// /// The text to mention the user. /// - public string Mention { get; set; } + public string Mention { get; } /// /// The the user spoke from. /// - public ChannelRepresentation Channel { get; set; } + public ChannelRepresentation Channel { get; } + + /// + /// Initializes a new instance of the class. + /// + /// The value of . + /// The value of . + /// The value of . + /// The value of . + public ChatUser(ChannelRepresentation channel, string friendlyName, string mention, ulong realId) + { + Channel = channel ?? throw new ArgumentNullException(nameof(channel)); + FriendlyName = friendlyName ?? throw new ArgumentNullException(nameof(friendlyName)); + Mention = mention ?? throw new ArgumentNullException(nameof(mention)); + + Id = null!; + RealId = realId; + } } } diff --git a/src/Tgstation.Server.Host/Components/Chat/Commands/ByondCommand.cs b/src/Tgstation.Server.Host/Components/Chat/Commands/ByondCommand.cs deleted file mode 100644 index f0c87ef4df3..00000000000 --- a/src/Tgstation.Server.Host/Components/Chat/Commands/ByondCommand.cs +++ /dev/null @@ -1,68 +0,0 @@ -using System; -using System.Globalization; -using System.Linq; -using System.Threading; -using System.Threading.Tasks; - -using Tgstation.Server.Api.Models; -using Tgstation.Server.Host.Components.Byond; -using Tgstation.Server.Host.Components.Interop; -using Tgstation.Server.Host.Components.Watchdog; - -namespace Tgstation.Server.Host.Components.Chat.Commands -{ - /// - /// For displaying the installed Byond version. - /// - sealed class ByondCommand : ICommand - { - /// - public string Name => "byond"; - - /// - public string HelpText => "Displays the running Byond version. Use --active for the version used in future deployments"; - - /// - public bool AdminOnly => false; - - /// - /// The for the . - /// - readonly IByondManager byondManager; - - /// - /// The for the . - /// - readonly IWatchdog watchdog; - - /// - /// Initializes a new instance of the class. - /// - /// The value of . - /// The value of . - public ByondCommand(IByondManager byondManager, IWatchdog watchdog) - { - this.byondManager = byondManager ?? throw new ArgumentNullException(nameof(byondManager)); - this.watchdog = watchdog ?? throw new ArgumentNullException(nameof(watchdog)); - } - - /// - public ValueTask Invoke(string arguments, ChatUser user, CancellationToken cancellationToken) - { - if (arguments.Split(' ').Any(x => x.ToUpperInvariant() == "--ACTIVE")) - return ValueTask.FromResult(new MessageContent - { - Text = byondManager.ActiveVersion == null ? "None!" : String.Format(CultureInfo.InvariantCulture, "{0}.{1}", byondManager.ActiveVersion.Major, byondManager.ActiveVersion.Minor), - }); - if (watchdog.Status == WatchdogStatus.Offline) - return ValueTask.FromResult(new MessageContent - { - Text = "Server offline!", - }); - return ValueTask.FromResult(new MessageContent - { - Text = watchdog.ActiveCompileJob?.ByondVersion ?? "None!", - }); - } - } -} diff --git a/src/Tgstation.Server.Host/Components/Chat/Commands/CommandFactory.cs b/src/Tgstation.Server.Host/Components/Chat/Commands/CommandFactory.cs index 06eb90dd758..a575ec9c87d 100644 --- a/src/Tgstation.Server.Host/Components/Chat/Commands/CommandFactory.cs +++ b/src/Tgstation.Server.Host/Components/Chat/Commands/CommandFactory.cs @@ -1,8 +1,8 @@ using System; using System.Collections.Generic; -using Tgstation.Server.Host.Components.Byond; using Tgstation.Server.Host.Components.Deployment; +using Tgstation.Server.Host.Components.Engine; using Tgstation.Server.Host.Components.Repository; using Tgstation.Server.Host.Components.Watchdog; using Tgstation.Server.Host.Database; @@ -19,9 +19,9 @@ sealed class CommandFactory : ICommandFactory readonly IAssemblyInformationProvider assemblyInformationProvider; /// - /// The for the . + /// The for the . /// - readonly IByondManager byondManager; + readonly IEngineManager engineManager; /// /// The for the . @@ -46,27 +46,27 @@ sealed class CommandFactory : ICommandFactory /// /// The for the . /// - IWatchdog watchdog; + IWatchdog? watchdog; /// /// Initializes a new instance of the class. /// /// The value of . - /// The value of . + /// The value of . /// The value of . /// The value of . /// The value of . /// The value of . public CommandFactory( IAssemblyInformationProvider assemblyInformationProvider, - IByondManager byondManager, + IEngineManager engineManager, IRepositoryManager repositoryManager, IDatabaseContextFactory databaseContextFactory, ILatestCompileJobProvider compileJobProvider, Models.Instance instance) { this.assemblyInformationProvider = assemblyInformationProvider ?? throw new ArgumentNullException(nameof(assemblyInformationProvider)); - this.byondManager = byondManager ?? throw new ArgumentNullException(nameof(byondManager)); + this.engineManager = engineManager ?? throw new ArgumentNullException(nameof(engineManager)); this.repositoryManager = repositoryManager ?? throw new ArgumentNullException(nameof(repositoryManager)); this.databaseContextFactory = databaseContextFactory ?? throw new ArgumentNullException(nameof(databaseContextFactory)); this.compileJobProvider = compileJobProvider ?? throw new ArgumentNullException(nameof(compileJobProvider)); @@ -92,7 +92,7 @@ public IReadOnlyList GenerateCommands() return new List { new VersionCommand(assemblyInformationProvider), - new ByondCommand(byondManager, watchdog), + new EngineCommand(engineManager, watchdog), new RevisionCommand(watchdog, repositoryManager), new PullRequestsCommand(watchdog, repositoryManager, databaseContextFactory, compileJobProvider, instance), new KekCommand(), diff --git a/src/Tgstation.Server.Host/Components/Chat/Commands/CustomCommand.cs b/src/Tgstation.Server.Host/Components/Chat/Commands/CustomCommand.cs index 617b1dba48b..9492e925d72 100644 --- a/src/Tgstation.Server.Host/Components/Chat/Commands/CustomCommand.cs +++ b/src/Tgstation.Server.Host/Components/Chat/Commands/CustomCommand.cs @@ -12,18 +12,31 @@ namespace Tgstation.Server.Host.Components.Chat.Commands public sealed class CustomCommand : ICommand { /// - public string Name { get; set; } + public string Name { get; } /// - public string HelpText { get; set; } + public string HelpText { get; } /// - public bool AdminOnly { get; set; } + public bool AdminOnly { get; } /// /// The for the . /// - ICustomCommandHandler handler; + ICustomCommandHandler? handler; + + /// + /// Initializes a new instance of the class. + /// + /// The value of . + /// The value of . + /// The value of . + public CustomCommand(string name, string helpText, bool adminOnly) + { + Name = name ?? throw new ArgumentNullException(nameof(name)); + HelpText = helpText ?? throw new ArgumentNullException(nameof(helpText)); + AdminOnly = adminOnly; + } /// /// Set a new . diff --git a/src/Tgstation.Server.Host/Components/Chat/Commands/EngineCommand.cs b/src/Tgstation.Server.Host/Components/Chat/Commands/EngineCommand.cs new file mode 100644 index 00000000000..c0bf80ea594 --- /dev/null +++ b/src/Tgstation.Server.Host/Components/Chat/Commands/EngineCommand.cs @@ -0,0 +1,96 @@ +using System; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; + +using Tgstation.Server.Api.Models; +using Tgstation.Server.Host.Components.Engine; +using Tgstation.Server.Host.Components.Interop; +using Tgstation.Server.Host.Components.Watchdog; + +namespace Tgstation.Server.Host.Components.Chat.Commands +{ + /// + /// For displaying the installed Byond version. + /// + sealed class EngineCommand : ICommand + { + /// + public string Name => "engine"; + + /// + public string HelpText => "Displays the running engine version. Use --active for the version used in future deployments"; + + /// + public bool AdminOnly => false; + + /// + /// The for the . + /// + readonly IEngineManager engineManager; + + /// + /// The for the . + /// + readonly IWatchdog watchdog; + + /// + /// Initializes a new instance of the class. + /// + /// The value of . + /// The value of . + public EngineCommand(IEngineManager engineManager, IWatchdog watchdog) + { + this.engineManager = engineManager ?? throw new ArgumentNullException(nameof(engineManager)); + this.watchdog = watchdog ?? throw new ArgumentNullException(nameof(watchdog)); + } + + /// + public ValueTask Invoke(string arguments, ChatUser user, CancellationToken cancellationToken) + { + EngineVersion? engineVersion; + if (arguments.Split(' ').Any(x => x.Equals("--active", StringComparison.OrdinalIgnoreCase))) + engineVersion = engineManager.ActiveVersion; + else + { + if (watchdog.Status == WatchdogStatus.Offline) + return ValueTask.FromResult( + new MessageContent + { + Text = "Server offline!", + }); + + if (watchdog.ActiveCompileJob == null) + return ValueTask.FromResult( + new MessageContent + { + Text = "None!", + }); + + engineVersion = EngineVersion.Parse(watchdog.ActiveCompileJob.EngineVersion); + } + + string text; + if (engineVersion == null) + text = "None!"; + else + { + text = engineVersion.Engine!.Value switch + { + EngineType.OpenDream => $"OpenDream: {engineVersion.SourceSHA}", + EngineType.Byond => $"BYOND {engineVersion.Version!.Major}.{engineVersion.Version.Minor}", + _ => throw new InvalidOperationException($"Invalid EngineType: {engineVersion.Engine.Value}"), + }; + + if (engineVersion.CustomIteration.HasValue) + text += $" (Custom Upload #{engineVersion.CustomIteration.Value})"; + } + + return ValueTask.FromResult( + new MessageContent + { + Text = text, + }); + } + } +} diff --git a/src/Tgstation.Server.Host/Components/Chat/Commands/PullRequestsCommand.cs b/src/Tgstation.Server.Host/Components/Chat/Commands/PullRequestsCommand.cs index 39a99bcac4e..3fd8c9e64bf 100644 --- a/src/Tgstation.Server.Host/Components/Chat/Commands/PullRequestsCommand.cs +++ b/src/Tgstation.Server.Host/Components/Chat/Commands/PullRequestsCommand.cs @@ -81,7 +81,7 @@ public PullRequestsCommand( #pragma warning disable CA1506 public async ValueTask Invoke(string arguments, ChatUser user, CancellationToken cancellationToken) { - IEnumerable results = null; + IEnumerable results; var splits = arguments.Split(' '); var hasRepo = splits.Any(x => x.Equals("--repo", StringComparison.OrdinalIgnoreCase)); var hasStaged = splits.Any(x => x.Equals("--staged", StringComparison.OrdinalIgnoreCase)); @@ -112,12 +112,13 @@ public async ValueTask Invoke(string arguments, ChatUser user, C head = repo.Head; } + results = null!; await databaseContextFactory.UseContext( async db => results = await db .RevisionInformations .AsQueryable() - .Where(x => x.Instance.Id == instance.Id && x.CommitSha == head) - .SelectMany(x => x.ActiveTestMerges) + .Where(x => x.Instance!.Id == instance.Id && x.CommitSha == head) + .SelectMany(x => x.ActiveTestMerges!) .Select(x => x.TestMerge) .Select(x => new Models.TestMerge { @@ -143,7 +144,7 @@ await databaseContextFactory.UseContext( compileJobToUse = null; } - results = compileJobToUse?.RevisionInformation.ActiveTestMerges.Select(x => x.TestMerge).ToList() ?? Enumerable.Empty(); + results = compileJobToUse?.RevisionInformation.ActiveTestMerges?.Select(x => x.TestMerge).ToList() ?? Enumerable.Empty(); } return new MessageContent @@ -153,7 +154,7 @@ await databaseContextFactory.UseContext( : String.Join( ", ", results.Select( - x => $"#{x.Number} at {x.TargetCommitSha[..7]}")), + x => $"#{x.Number} at {x.TargetCommitSha![..7]}")), }; } #pragma warning restore CA1506 diff --git a/src/Tgstation.Server.Host/Components/Chat/Commands/RevisionCommand.cs b/src/Tgstation.Server.Host/Components/Chat/Commands/RevisionCommand.cs index aceff96435e..39ed128d591 100644 --- a/src/Tgstation.Server.Host/Components/Chat/Commands/RevisionCommand.cs +++ b/src/Tgstation.Server.Host/Components/Chat/Commands/RevisionCommand.cs @@ -75,7 +75,7 @@ public async ValueTask Invoke(string arguments, ChatUser user, C { Text = "Server offline!", }; - result = watchdog.ActiveCompileJob?.RevisionInformation.OriginCommitSha; + result = watchdog.ActiveCompileJob?.RevisionInformation.OriginCommitSha!; } return new MessageContent diff --git a/src/Tgstation.Server.Host/Components/Chat/IChatManager.cs b/src/Tgstation.Server.Host/Components/Chat/IChatManager.cs index 769ab7b2515..8a0aa33fe4b 100644 --- a/src/Tgstation.Server.Host/Components/Chat/IChatManager.cs +++ b/src/Tgstation.Server.Host/Components/Chat/IChatManager.cs @@ -61,18 +61,18 @@ public interface IChatManager : IComponentService, IAsyncDisposable /// Send the message for a deployment to configured deployment channels. /// /// The of the deployment. - /// The BYOND of the deployment. + /// The of the deployment. /// The optional the deployment is expected to be completed at. /// The repository GitHub owner, if any. /// The repository GitHub name, if any. /// if the local deployment commit was pushed to the remote repository. /// A to call to update the message at the deployment's conclusion. Parameters: Error message if any, DreamMaker output if any. Returns an to call to mark the deployment as active/inactive. Parameter: If the deployment is being activated or inactivated. - Func> QueueDeploymentMessage( + Func> QueueDeploymentMessage( Models.RevisionInformation revisionInformation, - Version byondVersion, + Api.Models.EngineVersion engineVersion, DateTimeOffset? estimatedCompletionTime, - string gitHubOwner, - string gitHubRepo, + string? gitHubOwner, + string? gitHubRepo, bool localCommitPushed); /// diff --git a/src/Tgstation.Server.Host/Components/Chat/Providers/DiscordMessage.cs b/src/Tgstation.Server.Host/Components/Chat/Providers/DiscordMessage.cs index b0b3fbb56eb..565d78d654d 100644 --- a/src/Tgstation.Server.Host/Components/Chat/Providers/DiscordMessage.cs +++ b/src/Tgstation.Server.Host/Components/Chat/Providers/DiscordMessage.cs @@ -12,5 +12,19 @@ sealed class DiscordMessage : Message /// The of the source . /// public Optional MessageReference { get; set; } + + /// + /// Initializes a new instance of the class. + /// + /// The value of . + /// The value of . + /// The value of . + public DiscordMessage(ChatUser user, string content, Optional messageReference) + : base( + user, + content) + { + MessageReference = messageReference; + } } } diff --git a/src/Tgstation.Server.Host/Components/Chat/Providers/DiscordProvider.cs b/src/Tgstation.Server.Host/Components/Chat/Providers/DiscordProvider.cs index 42c849b276b..6f7ad59c5a8 100644 --- a/src/Tgstation.Server.Host/Components/Chat/Providers/DiscordProvider.cs +++ b/src/Tgstation.Server.Host/Components/Chat/Providers/DiscordProvider.cs @@ -23,8 +23,10 @@ using Remora.Results; using Tgstation.Server.Api.Models; +using Tgstation.Server.Api.Models.Internal; using Tgstation.Server.Common.Extensions; using Tgstation.Server.Host.Components.Interop; +using Tgstation.Server.Host.Configuration; using Tgstation.Server.Host.Extensions; using Tgstation.Server.Host.Jobs; using Tgstation.Server.Host.Models; @@ -56,19 +58,24 @@ public override string BotMention /// /// The s supported by the for mapping. /// - static readonly ChannelType[] SupportedGuildChannelTypes = new[] - { + static readonly ChannelType[] SupportedGuildChannelTypes = + [ ChannelType.GuildText, ChannelType.GuildAnnouncement, ChannelType.PrivateThread, ChannelType.PublicThread, - }; + ]; /// /// The for the . /// readonly IAssemblyInformationProvider assemblyInformationProvider; + /// + /// The for the . + /// + readonly GeneralConfiguration generalConfiguration; + /// /// The containing Discord services. /// @@ -84,11 +91,6 @@ public override string BotMention /// readonly object connectDisconnectLock; - /// - /// to enable based mode. Will auto reply with a youtube link to a video that says "based on the hardware that's installed in it" to anyone saying 'based on what?' case-insensitive. - /// - readonly bool basedMeme; - /// /// If the tgstation-server logo is shown in deployment embeds. /// @@ -102,17 +104,17 @@ public override string BotMention /// /// The for the . /// - CancellationTokenSource gatewayCts; + CancellationTokenSource? gatewayCts; /// /// The for the initial gateway connection event. /// - TaskCompletionSource gatewayReadyTcs; + TaskCompletionSource? gatewayReadyTcs; /// /// The representing the lifetime of the client. /// - Task gatewayTask; + Task? gatewayTask; /// /// The bot's . @@ -131,53 +133,6 @@ public override string BotMention /// The normalized mention . static string NormalizeMentions(string fromDiscord) => fromDiscord.Replace("<@!", "<@", StringComparison.Ordinal); - /// - /// Create a of s for a discord update embed. - /// - /// The of the deployment. - /// The BYOND of the deployment. - /// The repository GitHub owner, if any. - /// The repository GitHub name, if any. - /// if the local deployment commit was pushed to the remote repository. - /// A new of s to use. - static List BuildUpdateEmbedFields( - Models.RevisionInformation revisionInformation, - Version byondVersion, - string gitHubOwner, - string gitHubRepo, - bool localCommitPushed) - { - bool gitHub = gitHubOwner != null && gitHubRepo != null; - var fields = new List - { - new EmbedField( - "BYOND Version", - $"{byondVersion.Major}.{byondVersion.Minor}{(byondVersion.Build > 0 ? $".{byondVersion.Build}" : String.Empty)}", - true), - new EmbedField( - "Local Commit", - localCommitPushed && gitHub - ? $"[{revisionInformation.CommitSha[..7]}](https://github.com/{gitHubOwner}/{gitHubRepo}/commit/{revisionInformation.CommitSha})" - : revisionInformation.CommitSha[..7], - true), - new EmbedField( - "Branch Commit", - gitHub - ? $"[{revisionInformation.OriginCommitSha[..7]}](https://github.com/{gitHubOwner}/{gitHubRepo}/commit/{revisionInformation.OriginCommitSha})" - : revisionInformation.OriginCommitSha[..7], - true), - }; - - fields.AddRange((revisionInformation.ActiveTestMerges ?? Enumerable.Empty()) - .Select(x => x.TestMerge) - .Select(x => new EmbedField( - $"#{x.Number}", - $"[{x.TitleAtMerge}]({x.Url}) by _[@{x.Author}](https://github.com/{x.Author})_{Environment.NewLine}Commit: [{x.TargetCommitSha[..7]}](https://github.com/{gitHubOwner}/{gitHubRepo}/commit/{x.TargetCommitSha}){(String.IsNullOrWhiteSpace(x.Comment) ? String.Empty : $"{Environment.NewLine}_**{x.Comment}**_")}", - false))); - - return fields; - } - /// /// Initializes a new instance of the class. /// @@ -186,24 +141,24 @@ static List BuildUpdateEmbedFields( /// The for the . /// The value of . /// The for the . + /// The value of . public DiscordProvider( IJobManager jobManager, IAsyncDelayer asyncDelayer, ILogger logger, IAssemblyInformationProvider assemblyInformationProvider, - ChatBot chatBot) + ChatBot chatBot, + GeneralConfiguration generalConfiguration) : base(jobManager, asyncDelayer, logger, chatBot) { this.assemblyInformationProvider = assemblyInformationProvider ?? throw new ArgumentNullException(nameof(assemblyInformationProvider)); + this.generalConfiguration = generalConfiguration ?? throw new ArgumentNullException(nameof(generalConfiguration)); mappedChannels = new List(); connectDisconnectLock = new object(); - var csb = new DiscordConnectionStringBuilder(chatBot.ConnectionString); - var botToken = csb.BotToken; -#pragma warning disable CS0618 // Type or member is obsolete - basedMeme = csb.BasedMeme; -#pragma warning restore CS0618 // Type or member is obsolete + var csb = new DiscordConnectionStringBuilder(chatBot.ConnectionString!); + var botToken = csb.BotToken!; outputDisplayType = csb.DMOutputDisplay; deploymentBranding = csb.DeploymentBranding; @@ -239,7 +194,7 @@ public override async ValueTask DisposeAsync() } /// - public override async ValueTask SendMessage(Message replyTo, MessageContent message, ulong channelId, CancellationToken cancellationToken) + public override async ValueTask SendMessage(Message? replyTo, MessageContent message, ulong channelId, CancellationToken cancellationToken) { ArgumentNullException.ThrowIfNull(message); @@ -263,6 +218,22 @@ public override async ValueTask SendMessage(Message replyTo, MessageContent mess var channelsClient = serviceProvider.GetRequiredService(); async ValueTask SendToChannel(Snowflake channelId) { + if (message.Text == null) + { + Logger.LogWarning( + "Failed to send to channel {channelId}: Message was null!", + channelId); + + await channelsClient.CreateMessageAsync( + channelId, + "TGS: Could not send message to Discord. Message was `null`!", + messageReference: replyToReference, + allowedMentions: allowedMentions, + ct: cancellationToken); + + return; + } + var result = await channelsClient.CreateMessageAsync( channelId, message.Text, @@ -330,25 +301,23 @@ await ValueTaskExtensions.WhenAll( } /// - public override async ValueTask>>> SendUpdateMessage( + public override async ValueTask>>> SendUpdateMessage( Models.RevisionInformation revisionInformation, - Version byondVersion, + EngineVersion engineVersion, DateTimeOffset? estimatedCompletionTime, - string gitHubOwner, - string gitHubRepo, + string? gitHubOwner, + string? gitHubRepo, ulong channelId, bool localCommitPushed, CancellationToken cancellationToken) { ArgumentNullException.ThrowIfNull(revisionInformation); - ArgumentNullException.ThrowIfNull(byondVersion); - ArgumentNullException.ThrowIfNull(gitHubOwner); - ArgumentNullException.ThrowIfNull(gitHubRepo); + ArgumentNullException.ThrowIfNull(engineVersion); localCommitPushed |= revisionInformation.CommitSha == revisionInformation.OriginCommitSha; - var fields = BuildUpdateEmbedFields(revisionInformation, byondVersion, gitHubOwner, gitHubRepo, localCommitPushed); - var author = new EmbedAuthor(assemblyInformationProvider.VersionPrefix) + var fields = BuildUpdateEmbedFields(revisionInformation, engineVersion, gitHubOwner, gitHubRepo, localCommitPushed); + Optional author = new EmbedAuthor(assemblyInformationProvider.VersionPrefix) { Url = "https://github.com/tgstation/tgstation-server", IconUrl = "https://cdn.discordapp.com/attachments/1114451486374637629/1151650846019432448/tgs.png", @@ -368,9 +337,10 @@ public override async ValueTask(); + var prefix = GetEngineCompilerPrefix(engineVersion.Engine!.Value); var messageResponse = await channelsClient.CreateMessageAsync( new Snowflake(channelId), - "DM: Deployment in progress...", + $"{prefix}: Deployment in progress...", embeds: new List { embed }, ct: cancellationToken); @@ -381,7 +351,7 @@ public override async ValueTask new () + Embed CreateUpdatedEmbed(string message, Color color) => new() { Author = embed.Author, Colour = color, @@ -417,7 +387,7 @@ public override async ValueTask RespondAsync(IMessageCreate messageCreateEvent, Cancel FailIfNotExists = false, }; - if (basedMeme && messageCreateEvent.Content.Equals("Based on what?", StringComparison.OrdinalIgnoreCase)) - { - await SendMessage( - new DiscordMessage - { - MessageReference = messageReference, - }, - new MessageContent - { - Text = "https://youtu.be/LrNu-SuFF_o", - }, - messageCreateEvent.ChannelID.Value, - cancellationToken); - return Result.FromSuccess(); - } - var channelsClient = serviceProvider.GetRequiredService(); var channelResponse = await channelsClient.GetChannelAsync(messageCreateEvent.ChannelID, cancellationToken); if (!channelResponse.IsSuccess) @@ -592,27 +546,23 @@ await SendMessage( messageGuildResponse.LogFormat()); } - var result = new DiscordMessage - { - MessageReference = messageReference, - Content = content, - User = new ChatUser - { - RealId = messageCreateEvent.Author.ID.Value, - Channel = new ChannelRepresentation + var result = new DiscordMessage( + new ChatUser( + new ChannelRepresentation( + pm ? messageCreateEvent.Author.Username : guildName, + channelResponse.Entity.Name.Value!, + messageCreateEvent.ChannelID.Value) { - RealId = messageCreateEvent.ChannelID.Value, IsPrivateChannel = pm, - ConnectionName = pm ? messageCreateEvent.Author.Username : guildName, - FriendlyName = channelResponse.Entity.Name.Value, EmbedsSupported = true, // isAdmin and Tag populated by manager }, - FriendlyName = messageCreateEvent.Author.Username, - Mention = NormalizeMentions($"<@{messageCreateEvent.Author.ID}>"), - }, - }; + messageCreateEvent.Author.Username, + NormalizeMentions($"<@{messageCreateEvent.Author.ID}>"), + messageCreateEvent.Author.ID.Value), + content, + messageReference); EnqueueMessage(result); return Result.FromSuccess(); @@ -695,8 +645,8 @@ protected override async ValueTask DisconnectImpl(CancellationToken cancellation CancellationTokenSource localGatewayCts; lock (connectDisconnectLock) { - localGatewayTask = gatewayTask; - localGatewayCts = gatewayCts; + localGatewayTask = gatewayTask!; + localGatewayCts = gatewayCts!; gatewayTask = null; gatewayCts = null; if (localGatewayTask == null) @@ -722,7 +672,7 @@ protected override async ValueTask DisconnectImpl(CancellationToken cancellation var guildsClient = serviceProvider.GetRequiredService(); var guildTasks = new ConcurrentDictionary>>(); - async ValueTask>> GetModelChannelFromDBChannel(Models.ChatChannel channelFromDB) + async ValueTask>?> GetModelChannelFromDBChannel(Models.ChatChannel channelFromDB) { if (!channelFromDB.DiscordChannelId.HasValue) throw new InvalidOperationException("ChatChannel missing DiscordChannelId!"); @@ -779,12 +729,12 @@ protected override async ValueTask DisconnectImpl(CancellationToken cancellation var connectionName = guildsResponse.Entity.Name; - var channelModel = new ChannelRepresentation + var channelModel = new ChannelRepresentation( + guildsResponse.Entity.Name, + discordChannelResponse.Entity.Name.Value!, + channelId) { - RealId = channelId, IsAdminChannel = channelFromDB.IsAdminChannel == true, - ConnectionName = guildsResponse.Entity.Name, - FriendlyName = discordChannelResponse.Entity.Name.Value, IsPrivateChannel = false, Tag = channelFromDB.Tag, EmbedsSupported = true, @@ -802,8 +752,9 @@ protected override async ValueTask DisconnectImpl(CancellationToken cancellation var channelTuples = await ValueTaskExtensions.WhenAll(tasks.ToList()); - var enumerator = channelTuples + var list = channelTuples .Where(x => x != null) + .Cast>>() // NRT my beloathed .ToList(); var channelIdZeroModel = channels.FirstOrDefault(x => x.DiscordChannelId == 0); @@ -812,7 +763,7 @@ protected override async ValueTask DisconnectImpl(CancellationToken cancellation Logger.LogInformation("Mapping ALL additional accessible text channels"); var allAccessibleChannels = await GetAllAccessibleTextChannels(cancellationToken); var unmappedTextChannels = allAccessibleChannels - .Where(x => !tasks.Any(task => task.Result != null && new Snowflake(task.Result.Item1.DiscordChannelId.Value) == x.ID)); + .Where(x => !tasks.Any(task => task.Result != null && new Snowflake(task.Result.Item1.DiscordChannelId!.Value) == x.ID)); async ValueTask>> CreateMappingsForUnmappedChannels() { @@ -833,32 +784,36 @@ protected override async ValueTask DisconnectImpl(CancellationToken cancellation .ToList(); // Add catch-all channel - unmappedTasks.Add(Task.FromResult( - new ChannelRepresentation + unmappedTasks.Add(Task.FromResult( + new ChannelRepresentation( + "(Unknown Discord Guilds)", + "(Unknown Discord Channels)", + 0) { - IsAdminChannel = channelIdZeroModel.IsAdminChannel.Value, - ConnectionName = "(Unknown Discord Guilds)", + IsAdminChannel = channelIdZeroModel.IsAdminChannel!.Value, EmbedsSupported = true, - FriendlyName = "(Unknown Discord Channels)", - RealId = 0, Tag = channelIdZeroModel.Tag, })); await Task.WhenAll(unmappedTasks); return Tuple.Create>( channelIdZeroModel, - unmappedTasks.Select(x => x.Result).Where(x => x != null).ToList()); + unmappedTasks + .Select(x => x.Result) + .Where(x => x != null) + .Cast() // NRT my beloathed + .ToList()); } var task = CreateMappingsForUnmappedChannels(); var tuple = await task; - enumerator.Add(tuple); + list.Add(tuple); } lock (mappedChannels) { mappedChannels.Clear(); - mappedChannels.AddRange(enumerator.SelectMany(x => x.Item2).Select(x => x.RealId)); + mappedChannels.AddRange(list.SelectMany(x => x.Item2).Select(x => x.RealId)); } if (remapRequired) @@ -867,7 +822,7 @@ protected override async ValueTask DisconnectImpl(CancellationToken cancellation EnqueueMessage(null); } - return new Dictionary>(enumerator.Select(x => new KeyValuePair>(x.Item1, x.Item2))); + return new Dictionary>(list.Select(x => new KeyValuePair>(x.Item1, x.Item2))); } /// @@ -929,13 +884,79 @@ async ValueTask> GetGuildChannels(IPartialGuild guild) return allAccessibleChannels; } + /// + /// Create a of s for a discord update embed. + /// + /// The of the deployment. + /// The of the deployment. + /// The repository GitHub owner, if any. + /// The repository GitHub name, if any. + /// if the local deployment commit was pushed to the remote repository. + /// A new of s to use. + List BuildUpdateEmbedFields( + Models.RevisionInformation revisionInformation, + EngineVersion engineVersion, + string? gitHubOwner, + string? gitHubRepo, + bool localCommitPushed) + { + bool gitHub = gitHubOwner != null && gitHubRepo != null; + var engineField = engineVersion.Engine!.Value switch + { + EngineType.Byond => new EmbedField( + "BYOND Version", + $"{engineVersion.Version!.Major}.{engineVersion.Version.Minor}{(engineVersion.CustomIteration.HasValue ? $".{engineVersion.CustomIteration.Value}" : String.Empty)}", + true), + EngineType.OpenDream => new EmbedField( + "OpenDream Version", + $"[{engineVersion.SourceSHA![..7]}]({generalConfiguration.OpenDreamGitUrl}/commit/{engineVersion.SourceSHA})", + true), + _ => throw new InvalidOperationException($"Invaild EngineType: {engineVersion.Engine.Value}"), + }; + + var revisionSha = revisionInformation.CommitSha!; + var revisionOriginSha = revisionInformation.OriginCommitSha!; + var fields = new List + { + engineField, + }; + + if (gitHubOwner == null || gitHubRepo == null) + return fields; + + fields.Add( + new EmbedField( + "Local Commit", + localCommitPushed && gitHub + ? $"[{revisionSha[..7]}](https://github.com/{gitHubOwner}/{gitHubRepo}/commit/{revisionSha})" + : revisionSha[..7], + true)); + + fields.Add( + new EmbedField( + "Branch Commit", + gitHub + ? $"[{revisionOriginSha[..7]}](https://github.com/{gitHubOwner}/{gitHubRepo}/commit/{revisionOriginSha})" + : revisionOriginSha[..7], + true)); + + fields.AddRange((revisionInformation.ActiveTestMerges ?? Enumerable.Empty()) + .Select(x => x.TestMerge) + .Select(x => new EmbedField( + $"#{x.Number}", + $"[{x.TitleAtMerge}]({x.Url}) by _[@{x.Author}](https://github.com/{x.Author})_{Environment.NewLine}Commit: [{x.TargetCommitSha![..7]}](https://github.com/{gitHubOwner}/{gitHubRepo}/commit/{x.TargetCommitSha}){(String.IsNullOrWhiteSpace(x.Comment) ? String.Empty : $"{Environment.NewLine}_**{x.Comment}**_")}", + false))); + + return fields; + } + /// /// Convert a to an parameters. /// /// The to convert. /// The parameter for sending a single . - #pragma warning disable CA1502 - Optional> ConvertEmbed(ChatEmbed embed) +#pragma warning disable CA1502 + Optional> ConvertEmbed(ChatEmbed? embed) { if (embed == null) return default; @@ -958,7 +979,7 @@ Optional> ConvertEmbed(ChatEmbed embed) embed.Author = null; } - List fields = null; + List? fields = null; if (embed.Fields != null) { fields = new List(); @@ -990,7 +1011,7 @@ Optional> ConvertEmbed(ChatEmbed embed) if (invalid) continue; - fields.Add(new EmbedField(field.Name, field.Value) + fields.Add(new EmbedField(field.Name!, field.Value!) { IsInline = field.IsInline ?? default(Optional), }); @@ -1029,7 +1050,7 @@ Optional> ConvertEmbed(ChatEmbed embed) var discordEmbed = new Embed { Author = embed.Author != null - ? new EmbedAuthor(embed.Author.Name) + ? new EmbedAuthor(embed.Author.Name!) { IconUrl = embed.Author.IconUrl ?? default(Optional), ProxyIconUrl = embed.Author.ProxyIconUrl ?? default(Optional), @@ -1040,14 +1061,14 @@ Optional> ConvertEmbed(ChatEmbed embed) Description = embed.Description ?? default(Optional), Fields = fields ?? default(Optional>), Footer = embed.Footer != null - ? new EmbedFooter(embed.Footer.Text) + ? (Optional)new EmbedFooter(embed.Footer.Text!) { IconUrl = embed.Footer.IconUrl ?? default(Optional), ProxyIconUrl = embed.Footer.ProxyIconUrl ?? default(Optional), } : default, Image = embed.Image != null - ? new EmbedImage(embed.Image.Url) + ? new EmbedImage(embed.Image.Url!) { Width = embed.Image.Width ?? default(Optional), Height = embed.Image.Height ?? default(Optional), @@ -1062,7 +1083,7 @@ Optional> ConvertEmbed(ChatEmbed embed) } : default(Optional), Thumbnail = embed.Thumbnail != null - ? new EmbedThumbnail(embed.Thumbnail.Url) + ? new EmbedThumbnail(embed.Thumbnail.Url!) { Width = embed.Thumbnail.Width ?? default(Optional), Height = embed.Thumbnail.Height ?? default(Optional), diff --git a/src/Tgstation.Server.Host/Components/Chat/Providers/IProvider.cs b/src/Tgstation.Server.Host/Components/Chat/Providers/IProvider.cs index 641372c3180..845892d9e09 100644 --- a/src/Tgstation.Server.Host/Components/Chat/Providers/IProvider.cs +++ b/src/Tgstation.Server.Host/Components/Chat/Providers/IProvider.cs @@ -3,6 +3,7 @@ using System.Threading; using System.Threading.Tasks; +using Tgstation.Server.Api.Models.Internal; using Tgstation.Server.Host.Components.Interop; using Tgstation.Server.Host.Models; @@ -39,12 +40,12 @@ interface IProvider : IAsyncDisposable void InitialMappingComplete(); /// - /// Get a resulting in the next the recieves or on a disconnect. + /// Get a resulting in the next the receives or on a disconnect. /// /// The for the operation. /// A resulting in the next available or if the needed to reconnect. /// Note that private messages will come in the form of s not returned in . - Task NextMessage(CancellationToken cancellationToken); + Task NextMessage(CancellationToken cancellationToken); /// /// Gracefully disconnects the provider. Permanently stops the reconnection timer. @@ -64,12 +65,12 @@ interface IProvider : IAsyncDisposable /// /// Send a message to the . /// - /// The to reply to. + /// The optional to reply to. /// The . /// The to send to. /// The for the operation. /// A representing the running operation. - ValueTask SendMessage(Message replyTo, MessageContent message, ulong channelId, CancellationToken cancellationToken); + ValueTask SendMessage(Message? replyTo, MessageContent message, ulong channelId, CancellationToken cancellationToken); /// /// Set the interval at which the provider starts jobs to try to reconnect. @@ -83,7 +84,7 @@ interface IProvider : IAsyncDisposable /// Send the message for a deployment. /// /// The of the deployment. - /// The BYOND of the deployment. + /// The of the deployment. /// The optional the deployment is expected to be completed at. /// The repository GitHub owner, if any. /// The repository GitHub name, if any. @@ -91,12 +92,12 @@ interface IProvider : IAsyncDisposable /// if the local deployment commit was pushed to the remote repository. /// The for the operation. /// A resulting in a to call to update the message at the deployment's conclusion. Parameters: Error message if any, DreamMaker output if any. Returns another callback which should be called to mark the deployment as active. - ValueTask>>> SendUpdateMessage( - RevisionInformation revisionInformation, - Version byondVersion, + ValueTask>>> SendUpdateMessage( + Models.RevisionInformation revisionInformation, + Api.Models.EngineVersion engineVersion, DateTimeOffset? estimatedCompletionTime, - string gitHubOwner, - string gitHubRepo, + string? gitHubOwner, + string? gitHubRepo, ulong channelId, bool localCommitPushed, CancellationToken cancellationToken); diff --git a/src/Tgstation.Server.Host/Components/Chat/Providers/IrcProvider.cs b/src/Tgstation.Server.Host/Components/Chat/Providers/IrcProvider.cs index 533f5010a0e..ad83a80b18b 100644 --- a/src/Tgstation.Server.Host/Components/Chat/Providers/IrcProvider.cs +++ b/src/Tgstation.Server.Host/Components/Chat/Providers/IrcProvider.cs @@ -75,7 +75,7 @@ sealed class IrcProvider : Provider /// /// Map of s to channel names. /// - readonly Dictionary channelIdMap; + readonly Dictionary channelIdMap; /// /// Map of s to query users. @@ -83,14 +83,14 @@ sealed class IrcProvider : Provider readonly Dictionary queryChannelIdMap; /// - /// Id counter for . + /// The used for . /// - ulong channelIdCounter; + Task? listenTask; /// - /// The used for . + /// Id counter for . /// - Task listenTask; + ulong channelIdCounter; /// /// If we are disconnecting. @@ -119,11 +119,11 @@ public IrcProvider( if (builder == null || !builder.Valid || builder is not IrcConnectionStringBuilder ircBuilder) throw new InvalidOperationException("Invalid ChatConnectionStringBuilder!"); - address = ircBuilder.Address; - port = ircBuilder.Port.Value; - nickname = ircBuilder.Nickname; + address = ircBuilder.Address!; + port = ircBuilder.Port!.Value; + nickname = ircBuilder.Nickname!; - password = ircBuilder.Password; + password = ircBuilder.Password!; passwordType = ircBuilder.PasswordType; client = new IrcFeatures @@ -138,7 +138,7 @@ public IrcProvider( ActiveChannelSyncing = true, AutoNickHandling = true, CtcpVersion = assemblyInformationProvider.VersionString, - UseSsl = ircBuilder.UseSsl.Value, + UseSsl = ircBuilder.UseSsl!.Value, }; if (ircBuilder.UseSsl.Value) client.ValidateServerCertificate = true; // dunno if it defaults to that or what @@ -149,7 +149,7 @@ public IrcProvider( /*client.OnReadLine += (sender, e) => Logger.LogTrace("READ: {line}", e.Line); client.OnWriteLine += (sender, e) => Logger.LogTrace("WRITE: {line}", e.Line);*/ - channelIdMap = new Dictionary(); + channelIdMap = new Dictionary(); queryChannelIdMap = new Dictionary(); channelIdCounter = 1; } @@ -164,7 +164,7 @@ public override async ValueTask DisposeAsync() } /// - public override async ValueTask SendMessage(Message replyTo, MessageContent message, ulong channelId, CancellationToken cancellationToken) + public override async ValueTask SendMessage(Message? replyTo, MessageContent message, ulong channelId, CancellationToken cancellationToken) { ArgumentNullException.ThrowIfNull(message); @@ -218,22 +218,22 @@ await Task.Factory.StartNew( } /// - public override async ValueTask>>> SendUpdateMessage( + public override async ValueTask>>> SendUpdateMessage( Models.RevisionInformation revisionInformation, - Version byondVersion, + EngineVersion engineVersion, DateTimeOffset? estimatedCompletionTime, - string gitHubOwner, - string gitHubRepo, + string? gitHubOwner, + string? gitHubRepo, ulong channelId, bool localCommitPushed, CancellationToken cancellationToken) { ArgumentNullException.ThrowIfNull(revisionInformation); - ArgumentNullException.ThrowIfNull(byondVersion); + ArgumentNullException.ThrowIfNull(engineVersion); ArgumentNullException.ThrowIfNull(gitHubOwner); ArgumentNullException.ThrowIfNull(gitHubRepo); - var commitInsert = revisionInformation.CommitSha[..7]; + var commitInsert = revisionInformation.CommitSha![..7]; string remoteCommitInsert; if (revisionInformation.CommitSha == revisionInformation.OriginCommitSha) { @@ -241,7 +241,7 @@ public override async ValueTask x.TestMerge) .Select(x => { - var result = String.Format(CultureInfo.InvariantCulture, "#{0} at {1}", x.Number, x.TargetCommitSha[..7]); + var result = String.Format(CultureInfo.InvariantCulture, "#{0} at {1}", x.Number, x.TargetCommitSha![..7]); if (x.Comment != null) result += String.Format(CultureInfo.InvariantCulture, " ({0})", x.Comment); return result; }))); + var prefix = GetEngineCompilerPrefix(engineVersion.Engine!.Value); await SendMessage( null, new MessageContent { Text = String.Format( CultureInfo.InvariantCulture, - "DM: Deploying revision: {0}{1}{2} BYOND Version: {3}{4}", + $"{prefix}: Deploying revision: {0}{1}{2} BYOND Version: {3}{4}", commitInsert, testmergeInsert, remoteCommitInsert, - byondVersion.Build > 0 - ? byondVersion.ToString() - : $"{byondVersion.Major}.{byondVersion.Minor}", + engineVersion.ToString(), estimatedCompletionTime.HasValue ? $" ETA: {estimatedCompletionTime - DateTimeOffset.UtcNow}" : String.Empty), @@ -287,7 +286,7 @@ await SendMessage( null, new MessageContent { - Text = $"DM: Deployment {(errorMessage == null ? "complete" : "failed")}!", + Text = $"{prefix}: Deployment {(errorMessage == null ? "complete" : "failed")}!", }, channelId, cancellationToken); @@ -352,14 +351,11 @@ await SendMessage( dbChannel, new List { - new () + new(address, channelName, id!.Value) { - RealId = id.Value, + Tag = dbChannel.Tag, IsAdminChannel = dbChannel.IsAdminChannel == true, - ConnectionName = address, - FriendlyName = channelIdMap[id.Value], IsPrivateChannel = false, - Tag = dbChannel.Tag, EmbedsSupported = false, }, }); @@ -498,13 +494,13 @@ await Task.Factory.StartNew( /// If this is a query message. void HandleMessage(IrcEventArgs e, bool isPrivate) { - if (e.Data.Nick.ToUpperInvariant() == client.Nickname.ToUpperInvariant()) + if (e.Data.Nick.Equals(client.Nickname, StringComparison.OrdinalIgnoreCase)) return; var username = e.Data.Nick; var channelName = isPrivate ? username : e.Data.Channel; - ulong MapAndGetChannelId(Dictionary dicToCheck) + ulong MapAndGetChannelId(Dictionary dicToCheck) { ulong? resultId = null; if (!dicToCheck.Any(x => @@ -521,36 +517,31 @@ ulong MapAndGetChannelId(Dictionary dicToCheck) channelIdMap.Add(resultId.Value, null); } - return resultId.Value; + return resultId!.Value; } ulong userId, channelId; lock (client) { - userId = MapAndGetChannelId(queryChannelIdMap); + userId = MapAndGetChannelId(new Dictionary(queryChannelIdMap + .Cast>())); // NRT my beloathed channelId = isPrivate ? userId : MapAndGetChannelId(channelIdMap); } - var message = new Message - { - Content = e.Data.Message, - User = new ChatUser - { - Channel = new ChannelRepresentation + var channelFriendlyName = isPrivate ? String.Format(CultureInfo.InvariantCulture, "PM: {0}", channelName) : channelName; + var message = new Message( + new ChatUser( + new ChannelRepresentation(address, channelFriendlyName, channelId) { - ConnectionName = address, - FriendlyName = isPrivate ? String.Format(CultureInfo.InvariantCulture, "PM: {0}", channelName) : channelName, - RealId = channelId, IsPrivateChannel = isPrivate, EmbedsSupported = false, // isAdmin and Tag populated by manager }, - FriendlyName = username, - RealId = userId, - Mention = username, - }, - }; + username, + username, + userId), + e.Data.Message); EnqueueMessage(message); } @@ -605,16 +596,16 @@ async ValueTask SaslAuthenticate(CancellationToken cancellationToken) client.Login(nickname, nickname, 0, nickname); cancellationToken.ThrowIfCancellationRequested(); - // wait for the sasl ack or timeout - var recievedAck = false; - var recievedPlus = false; + // wait for the SASL ack or timeout + var receivedAck = false; + var receivedPlus = false; void AuthenticationDelegate(object sender, ReadLineEventArgs e) { if (e.Line.Contains("ACK :sasl", StringComparison.Ordinal)) - recievedAck = true; + receivedAck = true; else if (e.Line.Contains("AUTHENTICATE +", StringComparison.Ordinal)) - recievedPlus = true; + receivedPlus = true; } Logger.LogTrace("Performing handshake..."); @@ -626,14 +617,14 @@ void AuthenticationDelegate(object sender, ReadLineEventArgs e) var timeoutToken = timeoutCts.Token; var listenTimeSpan = TimeSpan.FromMilliseconds(10); - for (; !recievedAck; + for (; !receivedAck; await AsyncDelayer.Delay(listenTimeSpan, timeoutToken)) await NonBlockingListen(cancellationToken); client.WriteLine("AUTHENTICATE PLAIN", Priority.Critical); timeoutToken.ThrowIfCancellationRequested(); - for (; !recievedPlus; + for (; !receivedPlus; await AsyncDelayer.Delay(listenTimeSpan, timeoutToken)) await NonBlockingListen(cancellationToken); } diff --git a/src/Tgstation.Server.Host/Components/Chat/Providers/Message.cs b/src/Tgstation.Server.Host/Components/Chat/Providers/Message.cs index 321907029bd..c9084657fcb 100644 --- a/src/Tgstation.Server.Host/Components/Chat/Providers/Message.cs +++ b/src/Tgstation.Server.Host/Components/Chat/Providers/Message.cs @@ -1,18 +1,31 @@ -namespace Tgstation.Server.Host.Components.Chat.Providers +using System; + +namespace Tgstation.Server.Host.Components.Chat.Providers { /// - /// Represents a message recieved by a . + /// Represents a message received by a . /// class Message { /// /// The text of the message. /// - public string Content { get; set; } + public string Content { get; } /// /// The who sent the . /// - public ChatUser User { get; set; } + public ChatUser User { get; } + + /// + /// Initializes a new instance of the class. + /// + /// The value of . + /// The value of . + public Message(ChatUser user, string content) + { + User = user ?? throw new ArgumentNullException(nameof(user)); + Content = content ?? throw new ArgumentNullException(nameof(content)); + } } } diff --git a/src/Tgstation.Server.Host/Components/Chat/Providers/Provider.cs b/src/Tgstation.Server.Host/Components/Chat/Providers/Provider.cs index a6496c1cdad..6be6b715210 100644 --- a/src/Tgstation.Server.Host/Components/Chat/Providers/Provider.cs +++ b/src/Tgstation.Server.Host/Components/Chat/Providers/Provider.cs @@ -42,7 +42,7 @@ abstract class Provider : IProvider /// /// of received s. /// - readonly Queue messageQueue; + readonly Queue messageQueue; /// /// The backing for . @@ -62,12 +62,25 @@ abstract class Provider : IProvider /// /// The auto reconnect . /// - Task reconnectTask; + Task? reconnectTask; /// /// for . /// - CancellationTokenSource reconnectCts; + CancellationTokenSource? reconnectCts; + + /// + /// Get the prefix for messages about deployments. + /// + /// The of the deployment. + /// The prefix. + protected static string GetEngineCompilerPrefix(Api.Models.EngineType engineType) + => engineType switch + { + Api.Models.EngineType.Byond => "DM", + Api.Models.EngineType.OpenDream => "OD", + _ => throw new InvalidOperationException($"Unsupported engine type: {engineType}"), + }; /// /// Initializes a new instance of the class. @@ -83,7 +96,10 @@ protected Provider(IJobManager jobManager, IAsyncDelayer asyncDelayer, ILogger

(); + if (chatBot.Instance == null) + throw new ArgumentException("chatBot must have Instance!", nameof(chatBot)); + + messageQueue = new Queue(); nextMessage = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously); initialConnectionTcs = new TaskCompletionSource(); reconnectTaskLock = new object(); @@ -144,7 +160,7 @@ public async ValueTask - public async Task NextMessage(CancellationToken cancellationToken) + public async Task NextMessage(CancellationToken cancellationToken) { while (true) { @@ -178,15 +194,15 @@ public Task SetReconnectInterval(uint reconnectInterval, bool connectNow) } /// - public abstract ValueTask SendMessage(Message replyTo, MessageContent message, ulong channelId, CancellationToken cancellationToken); + public abstract ValueTask SendMessage(Message? replyTo, MessageContent message, ulong channelId, CancellationToken cancellationToken); /// - public abstract ValueTask>>> SendUpdateMessage( + public abstract ValueTask>>> SendUpdateMessage( RevisionInformation revisionInformation, - Version byondVersion, + Api.Models.EngineVersion engineVersion, DateTimeOffset? estimatedCompletionTime, - string gitHubOwner, - string gitHubRepo, + string? gitHubOwner, + string? gitHubRepo, ulong channelId, bool localCommitPushed, CancellationToken cancellationToken); @@ -219,7 +235,7 @@ protected abstract ValueTask for . ///

/// The to queue. A value of indicates the channel mappings are out of date. - protected void EnqueueMessage(Message message) + protected void EnqueueMessage(Message? message) { if (message == null) Logger.LogTrace("Requesting channel remap..."); @@ -244,7 +260,7 @@ Task StopReconnectionTimer() reconnectCts.Cancel(); reconnectCts.Dispose(); reconnectCts = null; - var reconnectTask = this.reconnectTask; + var reconnectTask = this.reconnectTask!; this.reconnectTask = null; return reconnectTask; } @@ -273,7 +289,7 @@ async Task ReconnectionLoop(uint reconnectInterval, bool connectNow, Cancellatio connectNow = false; if (!Connected) { - var job = Job.Create(Api.Models.JobCode.ReconnectChatBot, null, ChatBot.Instance, ChatBotRights.WriteEnabled); + var job = Job.Create(Api.Models.JobCode.ReconnectChatBot, null, ChatBot.Instance!, ChatBotRights.WriteEnabled); job.Description += $": {ChatBot.Name}"; await jobManager.RegisterOperation( diff --git a/src/Tgstation.Server.Host/Components/Chat/Providers/ProviderFactory.cs b/src/Tgstation.Server.Host/Components/Chat/Providers/ProviderFactory.cs index eeed8c15838..ed3608b92da 100644 --- a/src/Tgstation.Server.Host/Components/Chat/Providers/ProviderFactory.cs +++ b/src/Tgstation.Server.Host/Components/Chat/Providers/ProviderFactory.cs @@ -2,8 +2,10 @@ using System.Globalization; using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Options; using Tgstation.Server.Api.Models; +using Tgstation.Server.Host.Configuration; using Tgstation.Server.Host.Jobs; using Tgstation.Server.Host.System; using Tgstation.Server.Host.Utils; @@ -33,6 +35,11 @@ sealed class ProviderFactory : IProviderFactory ///
readonly ILoggerFactory loggerFactory; + /// + /// The for the . + /// + readonly GeneralConfiguration generalConfiguration; + /// /// Initializes a new instance of the class. /// @@ -40,16 +47,19 @@ sealed class ProviderFactory : IProviderFactory /// The value of . /// The value of . /// The value of . + /// The containing the value of . public ProviderFactory( IJobManager jobManager, IAssemblyInformationProvider assemblyInformationProvider, IAsyncDelayer asyncDelayer, - ILoggerFactory loggerFactory) + ILoggerFactory loggerFactory, + IOptions generalConfigurationOptions) { this.jobManager = jobManager ?? throw new ArgumentNullException(nameof(jobManager)); this.loggerFactory = loggerFactory ?? throw new ArgumentNullException(nameof(loggerFactory)); this.asyncDelayer = asyncDelayer ?? throw new ArgumentNullException(nameof(asyncDelayer)); this.assemblyInformationProvider = assemblyInformationProvider ?? throw new ArgumentNullException(nameof(assemblyInformationProvider)); + generalConfiguration = generalConfigurationOptions?.Value ?? throw new ArgumentNullException(nameof(generalConfigurationOptions)); } /// @@ -69,7 +79,8 @@ public IProvider CreateProvider(Models.ChatBot settings) asyncDelayer, loggerFactory.CreateLogger(), assemblyInformationProvider, - settings), + settings, + generalConfiguration), _ => throw new InvalidOperationException(String.Format(CultureInfo.InvariantCulture, "Invalid ChatProvider: {0}", settings.Provider)), }; } diff --git a/src/Tgstation.Server.Host/Components/Deployment/DmbFactory.cs b/src/Tgstation.Server.Host/Components/Deployment/DmbFactory.cs index fdb223da823..3b6695313b5 100644 --- a/src/Tgstation.Server.Host/Components/Deployment/DmbFactory.cs +++ b/src/Tgstation.Server.Host/Components/Deployment/DmbFactory.cs @@ -1,5 +1,6 @@ using System; using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; using System.IO; using System.Linq; using System.Threading; @@ -8,12 +9,14 @@ using Microsoft.EntityFrameworkCore; using Microsoft.Extensions.Logging; +using Tgstation.Server.Api.Models; using Tgstation.Server.Common.Extensions; using Tgstation.Server.Host.Components.Deployment.Remote; using Tgstation.Server.Host.Components.Events; using Tgstation.Server.Host.Database; using Tgstation.Server.Host.IO; using Tgstation.Server.Host.Models; +using Tgstation.Server.Host.Utils; namespace Tgstation.Server.Host.Components.Deployment { @@ -33,6 +36,7 @@ public Task OnNewerDmb } /// + [MemberNotNullWhen(true, nameof(nextDmbProvider))] public bool DmbAvailable => nextDmbProvider != null; /// @@ -73,7 +77,7 @@ public Task OnNewerDmb /// /// Map of s to locks on them. /// - readonly IDictionary jobLockCounts; + readonly Dictionary jobLockCounts; /// /// resulting in the latest yet to exist. @@ -88,7 +92,7 @@ public Task OnNewerDmb /// /// The latest . /// - IDmbProvider nextDmbProvider; + IDmbProvider? nextDmbProvider; /// /// If the is "started" via . @@ -129,7 +133,7 @@ public DmbFactory( public void Dispose() => cleanupCts.Dispose(); // we don't dispose nextDmbProvider here, since it might be the only thing we have /// - public async ValueTask LoadCompileJob(CompileJob job, Action activationAction, CancellationToken cancellationToken) + public async ValueTask LoadCompileJob(CompileJob job, Action? activationAction, CancellationToken cancellationToken) { ArgumentNullException.ThrowIfNull(job); @@ -172,8 +176,8 @@ public IDmbProvider LockNextDmb(int lockCount) throw new ArgumentOutOfRangeException(nameof(lockCount), lockCount, "lockCount must be greater than or equal to 0!"); lock (jobLockCounts) { - var jobId = nextDmbProvider.CompileJob.Id; - var incremented = jobLockCounts[jobId.Value] += lockCount; + var jobId = nextDmbProvider.CompileJob.Require(x => x.Id); + var incremented = jobLockCounts[jobId] += lockCount; logger.LogTrace("Compile job {jobId} lock count now: {lockCount}", jobId, incremented); return nextDmbProvider; } @@ -182,16 +186,15 @@ public IDmbProvider LockNextDmb(int lockCount) /// public async Task StartAsync(CancellationToken cancellationToken) { - CompileJob cj = null; - await databaseContextFactory.UseContext(async (db) => - { - cj = await db - .CompileJobs - .AsQueryable() - .Where(x => x.Job.Instance.Id == metadata.Id) - .OrderByDescending(x => x.Job.StoppedAt) - .FirstOrDefaultAsync(cancellationToken); - }); + CompileJob? cj = null; + await databaseContextFactory.UseContext( + async (db) => + cj = await db + .CompileJobs + .AsQueryable() + .Where(x => x.Job.Instance!.Id == metadata.Id) + .OrderByDescending(x => x.Job.StoppedAt) + .FirstOrDefaultAsync(cancellationToken)); try { @@ -226,30 +229,40 @@ public async Task StopAsync(CancellationToken cancellationToken) /// #pragma warning disable CA1506 // TODO: Decomplexify - public async ValueTask FromCompileJob(CompileJob compileJob, CancellationToken cancellationToken) + public async ValueTask FromCompileJob(CompileJob compileJob, CancellationToken cancellationToken) { ArgumentNullException.ThrowIfNull(compileJob); // ensure we have the entire metadata tree - logger.LogTrace("Loading compile job {id}...", compileJob.Id); + var compileJobId = compileJob.Require(x => x.Id); + logger.LogTrace("Loading compile job {id}...", compileJobId); await databaseContextFactory.UseContext( async db => compileJob = await db .CompileJobs .AsQueryable() - .Where(x => x.Id == compileJob.Id) - .Include(x => x.Job) + .Where(x => x!.Id == compileJobId) + .Include(x => x.Job!) .ThenInclude(x => x.StartedBy) - .Include(x => x.Job) + .Include(x => x.Job!) .ThenInclude(x => x.Instance) - .Include(x => x.RevisionInformation) - .ThenInclude(x => x.PrimaryTestMerge) - .ThenInclude(x => x.MergedBy) - .Include(x => x.RevisionInformation) - .ThenInclude(x => x.ActiveTestMerges) - .ThenInclude(x => x.TestMerge) - .ThenInclude(x => x.MergedBy) + .Include(x => x.RevisionInformation!) + .ThenInclude(x => x.PrimaryTestMerge!) + .ThenInclude(x => x.MergedBy) + .Include(x => x.RevisionInformation!) + .ThenInclude(x => x.ActiveTestMerges!) + .ThenInclude(x => x.TestMerge!) + .ThenInclude(x => x.MergedBy) .FirstAsync(cancellationToken)); // can't wait to see that query + EngineVersion engineVersion; + if (!EngineVersion.TryParse(compileJob.EngineVersion, out var engineVersionNullable)) + { + logger.LogWarning("Error loading compile job, bad engine version: {engineVersion}", compileJob.EngineVersion); + return null; // omae wa mou shinderu + } + else + engineVersion = engineVersionNullable!; + if (!compileJob.Job.StoppedAt.HasValue) { // This happens when we're told to load the compile job that is currently finished up @@ -267,7 +280,7 @@ void CleanupAction() CleanRegisteredCompileJob(compileJob); } - var newProvider = new DmbProvider(compileJob, ioManager, CleanupAction); + var newProvider = new DmbProvider(compileJob, engineVersion, ioManager, new DisposeInvoker(CleanupAction)); try { const string LegacyADirectoryName = "A"; @@ -304,22 +317,22 @@ void CleanupAction() // rebuild the provider because it's using the legacy style directories // Don't dispose it logger.LogDebug("Creating legacy two folder .dmb provider targeting {aDirName} directory...", LegacyADirectoryName); - newProvider = new DmbProvider(compileJob, ioManager, CleanupAction, Path.DirectorySeparatorChar + LegacyADirectoryName); + newProvider = new DmbProvider(compileJob, engineVersion, ioManager, new DisposeInvoker(CleanupAction), Path.DirectorySeparatorChar + LegacyADirectoryName); } lock (jobLockCounts) { - if (!jobLockCounts.TryGetValue(compileJob.Id.Value, out int value)) + if (!jobLockCounts.TryGetValue(compileJobId, out int value)) { value = 1; - jobLockCounts.Add(compileJob.Id.Value, 1); + jobLockCounts.Add(compileJobId, 1); } else - jobLockCounts[compileJob.Id.Value] = ++value; + jobLockCounts[compileJobId] = ++value; providerSubmitted = true; - logger.LogTrace("Compile job {id} lock count now: {lockCount}", compileJob.Id, value); + logger.LogTrace("Compile job {id} lock count now: {lockCount}", compileJobId, value); return newProvider; } } @@ -341,10 +354,10 @@ public async ValueTask CleanUnusedCompileJobs(CancellationToken cancellationToke lock (jobLockCounts) jobIdsToSkip = jobLockCounts.Keys.ToList(); - List jobUidsToNotErase = null; + List? jobUidsToNotErase = null; // find the uids of locked directories - if (jobIdsToSkip.Any()) + if (jobIdsToSkip.Count > 0) { await databaseContextFactory.UseContext(async db => { @@ -352,9 +365,9 @@ await databaseContextFactory.UseContext(async db => .CompileJobs .AsQueryable() .Where( - x => x.Job.Instance.Id == metadata.Id - && jobIdsToSkip.Contains(x.Id.Value)) - .Select(x => x.DirectoryName.Value) + x => x.Job.Instance!.Id == metadata.Id + && jobIdsToSkip.Contains(x.Id!.Value)) + .Select(x => x.DirectoryName!.Value) .ToListAsync(cancellationToken)) .Select(x => x.ToString()) .ToList(); @@ -363,7 +376,7 @@ await databaseContextFactory.UseContext(async db => else jobUidsToNotErase = new List(); - jobUidsToNotErase.Add(SwappableDmbProvider.LiveGameDirectory); + jobUidsToNotErase!.Add(SwappableDmbProvider.LiveGameDirectory); logger.LogTrace("We will not clean the following directories: {directoriesToNotClean}", String.Join(", ", jobUidsToNotErase)); @@ -398,7 +411,7 @@ await databaseContextFactory.UseContext(async db => #pragma warning restore CA1506 /// - public CompileJob LatestCompileJob() + public CompileJob? LatestCompileJob() { if (!DmbAvailable) return null; @@ -419,7 +432,7 @@ async Task HandleCleanup() // DCT: None available var deploymentJob = remoteDeploymentManager.MarkInactive(job, CancellationToken.None); - var deleteTask = DeleteCompileJobContent(job.DirectoryName.ToString(), cleanupCts.Token); + var deleteTask = DeleteCompileJobContent(job.DirectoryName!.Value.ToString(), cleanupCts.Token); var otherTask = cleanupTask; async Task WrapThrowableTasks() @@ -438,20 +451,23 @@ async Task WrapThrowableTasks() } lock (jobLockCounts) - if (jobLockCounts.TryGetValue(job.Id.Value, out var currentVal)) + { + var jobId = job.Require(x => x.Id); + if (jobLockCounts.TryGetValue(jobId, out var currentVal)) if (currentVal == 1) { - jobLockCounts.Remove(job.Id.Value); - logger.LogDebug("Cleaning lock-free compile job {id} => {dirName}", job.Id, job.DirectoryName); + jobLockCounts.Remove(jobId); + logger.LogDebug("Cleaning lock-free compile job {id} => {dirName}", jobId, job.DirectoryName); cleanupTask = HandleCleanup(); } else { - var decremented = --jobLockCounts[job.Id.Value]; - logger.LogTrace("Compile job {id} lock count now: {lockCount}", job.Id, decremented); + var decremented = --jobLockCounts[jobId]; + logger.LogTrace("Compile job {id} lock count now: {lockCount}", jobId, decremented); } else - logger.LogError("Extra Dispose of DmbProvider for CompileJob {compileJobId}!", job.Id); + logger.LogError("Extra Dispose of DmbProvider for CompileJob {compileJobId}!", jobId); + } } /// diff --git a/src/Tgstation.Server.Host/Components/Deployment/DmbProvider.cs b/src/Tgstation.Server.Host/Components/Deployment/DmbProvider.cs index 770409f7902..be47aeb6b3c 100644 --- a/src/Tgstation.Server.Host/Components/Deployment/DmbProvider.cs +++ b/src/Tgstation.Server.Host/Components/Deployment/DmbProvider.cs @@ -1,24 +1,24 @@ using System; using System.Threading.Tasks; +using Tgstation.Server.Api.Models; +using Tgstation.Server.Api.Models.Internal; using Tgstation.Server.Host.IO; -using Tgstation.Server.Host.Models; +using Tgstation.Server.Host.Utils; namespace Tgstation.Server.Host.Components.Deployment { /// - sealed class DmbProvider : IDmbProvider + sealed class DmbProvider : DmbProviderBase, IDmbProvider { /// - public string DmbName => String.Concat(CompileJob.DmeName, DreamMaker.DmbExtension); + public override string Directory => ioManager.ResolvePath(CompileJob.DirectoryName!.Value.ToString() + directoryAppend); /// - public string Directory => ioManager.ResolvePath(CompileJob.DirectoryName.ToString() + directoryAppend); + public override Models.CompileJob CompileJob { get; } - /// - /// The for the . - /// - public CompileJob CompileJob { get; } + /// + public override EngineVersion EngineVersion { get; } /// /// The for the . @@ -26,38 +26,40 @@ sealed class DmbProvider : IDmbProvider readonly IIOManager ioManager; /// - /// Extra path to add to the end of . + /// Extra path to add to the end of . /// readonly string directoryAppend; /// /// The to run when is called. /// - Action onDispose; + DisposeInvoker? onDispose; /// /// Initializes a new instance of the class. /// /// The value of . + /// The value of . /// The value of . /// The value of . /// The optional value of . - public DmbProvider(CompileJob compileJob, IIOManager ioManager, Action onDispose, string directoryAppend = null) + public DmbProvider(Models.CompileJob compileJob, EngineVersion engineVersion, IIOManager ioManager, DisposeInvoker onDispose, string? directoryAppend = null) { CompileJob = compileJob ?? throw new ArgumentNullException(nameof(compileJob)); + EngineVersion = engineVersion ?? throw new ArgumentNullException(nameof(engineVersion)); this.ioManager = ioManager ?? throw new ArgumentNullException(nameof(ioManager)); this.onDispose = onDispose ?? throw new ArgumentNullException(nameof(onDispose)); this.directoryAppend = directoryAppend ?? String.Empty; } /// - public ValueTask DisposeAsync() + public override ValueTask DisposeAsync() { - onDispose?.Invoke(); + onDispose?.Dispose(); return ValueTask.CompletedTask; } /// - public void KeepAlive() => onDispose = null; + public override void KeepAlive() => onDispose = null; } } diff --git a/src/Tgstation.Server.Host/Components/Deployment/DmbProviderBase.cs b/src/Tgstation.Server.Host/Components/Deployment/DmbProviderBase.cs new file mode 100644 index 00000000000..5929b74167b --- /dev/null +++ b/src/Tgstation.Server.Host/Components/Deployment/DmbProviderBase.cs @@ -0,0 +1,38 @@ +using System; +using System.Threading.Tasks; + +using Tgstation.Server.Api.Models; +using Tgstation.Server.Api.Models.Internal; +using Tgstation.Server.Host.Models; + +namespace Tgstation.Server.Host.Components.Deployment +{ + /// + abstract class DmbProviderBase : IDmbProvider + { + /// + public string DmbName => String.Concat( + CompileJob.DmeName, + EngineVersion.Engine switch + { + EngineType.Byond => ".dmb", + EngineType.OpenDream => ".json", + _ => throw new InvalidOperationException($"Invalid EngineType: {EngineVersion.Engine}"), + }); + + /// + public abstract string Directory { get; } + + /// + public abstract Models.CompileJob CompileJob { get; } + + /// + public abstract EngineVersion EngineVersion { get; } + + /// + public abstract ValueTask DisposeAsync(); + + /// + public abstract void KeepAlive(); + } +} diff --git a/src/Tgstation.Server.Host/Components/Deployment/DreamMaker.cs b/src/Tgstation.Server.Host/Components/Deployment/DreamMaker.cs index 8792e2f304c..f72d848082f 100644 --- a/src/Tgstation.Server.Host/Components/Deployment/DreamMaker.cs +++ b/src/Tgstation.Server.Host/Components/Deployment/DreamMaker.cs @@ -11,9 +11,9 @@ 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.Remote; +using Tgstation.Server.Host.Components.Engine; using Tgstation.Server.Host.Components.Events; using Tgstation.Server.Host.Components.Repository; using Tgstation.Server.Host.Components.Session; @@ -30,20 +30,15 @@ namespace Tgstation.Server.Host.Components.Deployment /// sealed class DreamMaker : IDreamMaker { - /// - /// Extension for .dmbs. - /// - public const string DmbExtension = ".dmb"; - /// /// Extension for .dmes. /// const string DmeExtension = "dme"; /// - /// The for . + /// The for . /// - readonly IByondManager byond; + readonly IEngineManager engineManager; /// /// The for . @@ -118,12 +113,12 @@ sealed class DreamMaker : IDreamMaker /// /// The active callback from . /// - Func> currentChatCallback; + Func>? currentChatCallback; /// /// Cached for . /// - string currentDreamMakerOutput; + string? currentDreamMakerOutput; /// /// If a compile job is running. @@ -143,7 +138,7 @@ static string FormatExceptionForUsers(Exception exception) /// /// Initializes a new instance of the class. /// - /// The value of . + /// The value of . /// The value of . /// The value of . /// The value of . @@ -158,7 +153,7 @@ static string FormatExceptionForUsers(Exception exception) /// The value of . /// The value of . public DreamMaker( - IByondManager byond, + IEngineManager engineManager, IIOManager ioManager, StaticFiles.IConfiguration configuration, ISessionControllerFactory sessionControllerFactory, @@ -173,7 +168,7 @@ public DreamMaker( SessionConfiguration sessionConfiguration, Api.Models.Instance metadata) { - this.byond = byond ?? throw new ArgumentNullException(nameof(byond)); + this.engineManager = engineManager ?? throw new ArgumentNullException(nameof(engineManager)); this.ioManager = ioManager ?? throw new ArgumentNullException(nameof(ioManager)); this.configuration = configuration ?? throw new ArgumentNullException(nameof(configuration)); this.sessionControllerFactory = sessionControllerFactory ?? throw new ArgumentNullException(nameof(sessionControllerFactory)); @@ -206,24 +201,24 @@ public async ValueTask DeploymentProcess( lock (deploymentLock) { if (deploying) - throw new JobException(ErrorCode.DreamMakerCompileJobInProgress); + throw new JobException(ErrorCode.DeploymentInProgress); deploying = true; } currentChatCallback = null; currentDreamMakerOutput = null; - Models.CompileJob compileJob = null; + Models.CompileJob? compileJob = null; try { - string repoOwner = null; - string repoName = null; + string? repoOwner = null; + string? repoName = null; TimeSpan? averageSpan = null; - Models.RepositorySettings repositorySettings = null; - Models.DreamDaemonSettings ddSettings = null; - Models.DreamMakerSettings dreamMakerSettings = null; - IRepository repo = null; - IRemoteDeploymentManager remoteDeploymentManager = null; - Models.RevisionInformation revInfo = null; + Models.RepositorySettings? repositorySettings = null; + Models.DreamDaemonSettings? ddSettings = null; + Models.DreamMakerSettings? dreamMakerSettings = null; + IRepository? repo = null; + IRemoteDeploymentManager? remoteDeploymentManager = null; + Models.RevisionInformation? revInfo = null; await databaseContextFactory.UseContext( async databaseContext => { @@ -273,7 +268,7 @@ await databaseContextFactory.UseContext( throw new JobException(ErrorCode.RepoMissing); remoteDeploymentManager = remoteDeploymentManagerFactory - .CreateRemoteDeploymentManager(metadata, repo.RemoteGitProvider.Value); + .CreateRemoteDeploymentManager(metadata, repo.RemoteGitProvider!.Value); var repoSha = repo.Head; repoOwner = repo.RemoteRepositoryOwner; @@ -281,13 +276,13 @@ await databaseContextFactory.UseContext( revInfo = await databaseContext .RevisionInformations .AsQueryable() - .Where(x => x.CommitSha == repoSha && x.Instance.Id == metadata.Id) - .Include(x => x.ActiveTestMerges) - .ThenInclude(x => x.TestMerge) - .ThenInclude(x => x.MergedBy) + .Where(x => x.CommitSha == repoSha && x.InstanceId == metadata.Id) + .Include(x => x.ActiveTestMerges!) + .ThenInclude(x => x.TestMerge!) + .ThenInclude(x => x.MergedBy) .FirstOrDefaultAsync(cancellationToken); - if (revInfo == default) + if (revInfo == null) { revInfo = new Models.RevisionInformation { @@ -315,16 +310,17 @@ await databaseContextFactory.UseContext( }); var likelyPushedTestMergeCommit = - repositorySettings.PushTestMergeCommits.Value + repositorySettings!.PushTestMergeCommits!.Value && repositorySettings.AccessToken != null && repositorySettings.AccessUser != null; using (repo) compileJob = await Compile( - revInfo, - dreamMakerSettings, - ddSettings, - repo, - remoteDeploymentManager, + job, + revInfo!, + dreamMakerSettings!, + ddSettings!, + repo!, + remoteDeploymentManager!, progressReporter, averageSpan, likelyPushedTestMergeCommit, @@ -337,11 +333,11 @@ await databaseContextFactory.UseContext( async databaseContext => { var fullJob = compileJob.Job; - compileJob.Job = new Models.Job(job.Id.Value); + compileJob.Job = new Models.Job(job.Require(x => x.Id)); var fullRevInfo = compileJob.RevisionInformation; compileJob.RevisionInformation = new Models.RevisionInformation { - Id = revInfo.Id, + Id = revInfo!.Id, }; databaseContext.Jobs.Attach(compileJob.Job); @@ -353,7 +349,7 @@ await databaseContextFactory.UseContext( logger.LogTrace("Created CompileJob {compileJobId}", compileJob.Id); try { - var chatNotificationAction = currentChatCallback(null, compileJob.Output); + var chatNotificationAction = currentChatCallback!(null, compileJob.Output!); await compileJobConsumer.LoadCompileJob(compileJob, chatNotificationAction, cancellationToken); } catch @@ -372,11 +368,11 @@ await databaseContextFactory.UseContext( } catch (Exception ex) { - await CleanupFailedCompile(compileJob, remoteDeploymentManager, ex); + await CleanupFailedCompile(compileJob, remoteDeploymentManager!, ex); throw; } - var commentsTask = remoteDeploymentManager.PostDeploymentComments( + var commentsTask = remoteDeploymentManager!.PostDeploymentComments( compileJob, activeCompileJob?.RevisionInformation, repositorySettings, @@ -403,7 +399,7 @@ await databaseContextFactory.UseContext( { currentChatCallback?.Invoke( FormatExceptionForUsers(ex), - currentDreamMakerOutput); + currentDreamMakerOutput!); throw; } @@ -425,13 +421,13 @@ await databaseContextFactory.UseContext( var previousCompileJobs = await databaseContext .CompileJobs .AsQueryable() - .Where(x => x.Job.Instance.Id == metadata.Id) + .Where(x => x.Job.Instance!.Id == metadata.Id) .OrderByDescending(x => x.Job.StoppedAt) .Take(10) .Select(x => new { - x.Job.StoppedAt, - x.Job.StartedAt, + StoppedAt = x.Job.StoppedAt!.Value, + StartedAt = x.Job.StartedAt!.Value, }) .ToListAsync(cancellationToken); @@ -440,7 +436,7 @@ await databaseContextFactory.UseContext( { var totalSpan = TimeSpan.Zero; foreach (var previousCompileJob in previousCompileJobs) - totalSpan += previousCompileJob.StoppedAt.Value - previousCompileJob.StartedAt.Value; + totalSpan += previousCompileJob.StoppedAt - previousCompileJob.StartedAt; averageSpan = totalSpan / previousCompileJobs.Count; } @@ -450,6 +446,7 @@ await databaseContextFactory.UseContext( /// /// Run the compile implementation. /// + /// The currently running . /// The . /// The . /// The . @@ -461,6 +458,7 @@ await databaseContextFactory.UseContext( /// The for the operation. /// A resulting in the completed . async ValueTask Compile( + Models.Job job, Models.RevisionInformation revisionInformation, Api.Models.Internal.DreamMakerSettings dreamMakerSettings, DreamDaemonLaunchParameters launchParameters, @@ -479,31 +477,29 @@ await databaseContextFactory.UseContext( var progressTask = ProgressTask(progressReporter, estimatedDuration, progressCts.Token); try { - using var byondLock = await byond.UseExecutables(null, null, cancellationToken); + using var engineLock = await engineManager.UseExecutables(null, null, cancellationToken); currentChatCallback = chatManager.QueueDeploymentMessage( revisionInformation, - byondLock.Version, + engineLock.Version, DateTimeOffset.UtcNow + estimatedDuration, repository.RemoteRepositoryOwner, repository.RemoteRepositoryName, localCommitExistsOnRemote); - var job = new Models.CompileJob + var compileJob = new Models.CompileJob(job, revisionInformation, engineLock.Version.ToString()) { DirectoryName = Guid.NewGuid(), DmeName = dreamMakerSettings.ProjectName, - RevisionInformation = revisionInformation, - ByondVersion = byondLock.Version.ToString(), RepositoryOrigin = repository.Origin.ToString(), }; progressReporter.StageName = "Creating remote deployment notification"; await remoteDeploymentManager.StartDeployment( repository, - job, + compileJob, cancellationToken); - logger.LogTrace("Deployment will timeout at {timeoutTime}", DateTimeOffset.UtcNow + dreamMakerSettings.Timeout.Value); + logger.LogTrace("Deployment will timeout at {timeoutTime}", DateTimeOffset.UtcNow + dreamMakerSettings.Timeout!.Value); using var timeoutTokenSource = new CancellationTokenSource(dreamMakerSettings.Timeout.Value); var timeoutToken = timeoutTokenSource.Token; using (timeoutToken.Register(() => logger.LogWarning("Deployment timed out!"))) @@ -513,10 +509,10 @@ await remoteDeploymentManager.StartDeployment( { await RunCompileJob( progressReporter, - job, + compileJob, dreamMakerSettings, launchParameters, - byondLock, + engineLock, repository, remoteDeploymentManager, combinedTokenSource.Token); @@ -527,7 +523,7 @@ await RunCompileJob( } } - return job; + return compileJob; } catch (OperationCanceledException) { @@ -550,7 +546,7 @@ await RunCompileJob( /// The to run and populate. /// The to use. /// The to use. - /// The to use. + /// The to use. /// The to use. /// The to use. /// The for the operation. @@ -560,12 +556,12 @@ async ValueTask RunCompileJob( Models.CompileJob job, Api.Models.Internal.DreamMakerSettings dreamMakerSettings, DreamDaemonLaunchParameters launchParameters, - IByondExecutableLock byondLock, + IEngineExecutableLock engineLock, IRepository repository, IRemoteDeploymentManager remoteDeploymentManager, CancellationToken cancellationToken) { - var outputDirectory = job.DirectoryName.ToString(); + var outputDirectory = job.DirectoryName!.Value.ToString(); logger.LogTrace("Compile output GUID: {dirGuid}", outputDirectory); try @@ -588,7 +584,7 @@ await eventConsumer.HandleEvent( { resolvedOutputDirectory, repoOrigin.ToString(), - $"{byondLock.Version.Major}.{byondLock.Version.Minor}", + engineLock.Version.ToString(), }, true, cancellationToken); @@ -601,7 +597,7 @@ await eventConsumer.HandleEvent( var foundPaths = await ioManager.GetFilesWithExtension(resolvedOutputDirectory, DmeExtension, true, cancellationToken); var foundPath = foundPaths.FirstOrDefault(); if (foundPath == default) - throw new JobException(ErrorCode.DreamMakerNoDme); + throw new JobException(ErrorCode.DeploymentNoDme); job.DmeName = foundPath.Substring( resolvedOutputDirectory.Length + 1, foundPath.Length - resolvedOutputDirectory.Length - DmeExtension.Length - 2); // +1 for . in extension @@ -611,7 +607,7 @@ await eventConsumer.HandleEvent( var targetDme = ioManager.ConcatPath(outputDirectory, String.Join('.', job.DmeName, DmeExtension)); var targetDmeExists = await ioManager.FileExists(targetDme, cancellationToken); if (!targetDmeExists) - throw new JobException(ErrorCode.DreamMakerMissingDme); + throw new JobException(ErrorCode.DeploymentMissingDme); } logger.LogDebug("Selected {dmeName}.dme for compilation!", job.DmeName); @@ -627,35 +623,35 @@ await eventConsumer.HandleEvent( { resolvedOutputDirectory, repoOrigin.ToString(), - $"{byondLock.Version.Major}.{byondLock.Version.Minor}", + engineLock.Version.ToString(), }, true, cancellationToken); // run compiler - progressReporter.StageName = "Running DreamMaker"; - var exitCode = await RunDreamMaker(byondLock.DreamMakerPath, job, cancellationToken); + progressReporter.StageName = "Running Compiler"; + var compileSuceeded = await RunDreamMaker(engineLock, job, cancellationToken); // Session takes ownership of the lock and Disposes it so save this for later - var byondVersion = byondLock.Version; + var engineVersion = engineLock.Version; // verify api try { - if (exitCode != 0) + if (!compileSuceeded) throw new JobException( - ErrorCode.DreamMakerExitCode, - new JobException($"Exit code: {exitCode}{Environment.NewLine}{Environment.NewLine}{job.Output}")); + ErrorCode.DeploymentExitCode, + new JobException($"Compilation failed:{Environment.NewLine}{Environment.NewLine}{job.Output}")); progressReporter.StageName = "Validating DMAPI"; await VerifyApi( - launchParameters.StartupTimeout.Value, - dreamMakerSettings.ApiValidationSecurityLevel.Value, + launchParameters.StartupTimeout!.Value, + dreamMakerSettings.ApiValidationSecurityLevel!.Value, job, - byondLock, - dreamMakerSettings.ApiValidationPort.Value, - dreamMakerSettings.RequireDMApiValidation.Value, - launchParameters.LogOutput.Value, + engineLock, + dreamMakerSettings.ApiValidationPort!.Value, + dreamMakerSettings.RequireDMApiValidation!.Value, + launchParameters.LogOutput!.Value, cancellationToken); } catch (JobException) @@ -667,8 +663,8 @@ await eventConsumer.HandleEvent( new List { resolvedOutputDirectory, - exitCode == 0 ? "1" : "0", - byondVersion.ToString(), + compileSuceeded ? "1" : "0", + engineVersion.ToString(), }, true, cancellationToken); @@ -681,7 +677,7 @@ await eventConsumer.HandleEvent( new List { resolvedOutputDirectory, - byondVersion.ToString(), + engineVersion.ToString(), }, true, cancellationToken); @@ -766,7 +762,7 @@ async ValueTask ProgressTask(JobProgressReporter progressReporter, TimeSpan? est /// The timeout in seconds for validation. /// The level to use to validate the API. /// The for the operation. - /// The current . + /// The current . /// The port to use for API validation. /// If the API validation is required to complete the deployment. /// If output should be logged to the DreamDaemon Diagnostics folder. @@ -776,7 +772,7 @@ async ValueTask VerifyApi( uint timeout, DreamDaemonSecurity securityLevel, Models.CompileJob job, - IByondExecutableLock byondLock, + IEngineExecutableLock engineLock, ushort portToUse, bool requireValidate, bool logOutput, @@ -800,8 +796,11 @@ async ValueTask VerifyApi( job.MinimumSecurityLevel = securityLevel; // needed for the TempDmbProvider ApiValidationStatus validationStatus; - await using (var provider = new TemporaryDmbProvider(ioManager.ResolvePath(job.DirectoryName.ToString()), String.Concat(job.DmeName, DmbExtension), job)) - await using (var controller = await sessionControllerFactory.LaunchNew(provider, byondLock, launchParameters, true, cancellationToken)) + await using (var provider = new TemporaryDmbProvider( + ioManager.ResolvePath(job.DirectoryName!.Value.ToString()), + job, + engineLock.Version)) + await using (var controller = await sessionControllerFactory.LaunchNew(provider, engineLock, launchParameters, true, cancellationToken)) { var launchResult = await controller.LaunchResult.WaitAsync(cancellationToken); @@ -813,9 +812,6 @@ async ValueTask VerifyApi( validationStatus = controller.ApiValidationStatus; - if (requireValidate && validationStatus == ApiValidationStatus.NeverValidated) - throw new JobException(ErrorCode.DreamMakerNeverValidated); - logger.LogTrace("API validation status: {validationStatus}", validationStatus); job.DMApiVersion = controller.DMApiVersion; @@ -834,12 +830,12 @@ async ValueTask VerifyApi( return; case ApiValidationStatus.NeverValidated: if (requireValidate) - throw new JobException(ErrorCode.DreamMakerNeverValidated); + throw new JobException(ErrorCode.DeploymentNeverValidated); job.MinimumSecurityLevel = DreamDaemonSecurity.Ultrasafe; break; case ApiValidationStatus.BadValidationRequest: case ApiValidationStatus.Incompatible: - throw new JobException(ErrorCode.DreamMakerInvalidValidation); + throw new JobException(ErrorCode.DeploymentInvalidValidation); case ApiValidationStatus.UnaskedValidationRequest: default: throw new InvalidOperationException( @@ -850,17 +846,19 @@ async ValueTask VerifyApi( /// /// Compiles a .dme with DreamMaker. /// - /// The path to the DreamMaker executable. + /// The to use. /// The for the operation. /// The for the operation. - /// A representing the running operation. - async ValueTask RunDreamMaker(string dreamMakerPath, Models.CompileJob job, CancellationToken cancellationToken) + /// A resulting in if compilation succeeded, otherwise. + async ValueTask RunDreamMaker(IEngineExecutableLock engineLock, Models.CompileJob job, CancellationToken cancellationToken) { + var arguments = engineLock.FormatCompilerArguments($"{job.DmeName}.{DmeExtension}"); + await using var dm = processExecutor.LaunchProcess( - dreamMakerPath, + engineLock.CompilerExePath, ioManager.ResolvePath( - job.DirectoryName.ToString()), - $"-clean {job.DmeName}.{DmeExtension}", + job.DirectoryName!.Value.ToString()), + arguments, readStandardHandles: true, noShellExecute: true); @@ -873,10 +871,11 @@ async ValueTask RunDreamMaker(string dreamMakerPath, Models.CompileJob job, cancellationToken.ThrowIfCancellationRequested(); logger.LogDebug("DreamMaker exit code: {exitCode}", exitCode); - job.Output = await dm.GetCombinedOutput(cancellationToken); - currentDreamMakerOutput = job.Output; + job.Output = $"{await dm.GetCombinedOutput(cancellationToken)}{Environment.NewLine}{Environment.NewLine}Exit Code: {exitCode}"; logger.LogDebug("DreamMaker output: {newLine}{output}", Environment.NewLine, job.Output); - return exitCode; + + currentDreamMakerOutput = job.Output; + return exitCode == 0; } /// @@ -888,14 +887,15 @@ async ValueTask RunDreamMaker(string dreamMakerPath, Models.CompileJob job, async ValueTask ModifyDme(Models.CompileJob job, CancellationToken cancellationToken) { var dmeFileName = String.Join('.', job.DmeName, DmeExtension); - var dmePath = ioManager.ConcatPath(job.DirectoryName.ToString(), dmeFileName); + var stringDirectoryName = job.DirectoryName!.Value.ToString(); + var dmePath = ioManager.ConcatPath(stringDirectoryName, dmeFileName); var dmeReadTask = ioManager.ReadAllBytes(dmePath, cancellationToken); var dmeModificationsTask = configuration.CopyDMFilesTo( dmeFileName, ioManager.ResolvePath( ioManager.ConcatPath( - job.DirectoryName.ToString(), + stringDirectoryName, ioManager.GetDirectoryName(dmeFileName))), cancellationToken); @@ -938,7 +938,7 @@ async ValueTask ModifyDme(Models.CompileJob job, CancellationToken cancellationT } } - dmeBytes = Encoding.UTF8.GetBytes(String.Join(Environment.NewLine, dmeLines)); + dmeBytes = Encoding.UTF8.GetBytes(String.Join('\n', dmeLines)); await ioManager.WriteAllBytes(dmePath, dmeBytes, cancellationToken); } @@ -954,7 +954,7 @@ ValueTask CleanupFailedCompile(Models.CompileJob job, IRemoteDeploymentManager r async ValueTask CleanDir() { logger.LogTrace("Cleaning compile directory..."); - var jobPath = job.DirectoryName.ToString(); + var jobPath = job.DirectoryName!.Value.ToString(); try { // DCT: None available @@ -967,13 +967,16 @@ async ValueTask CleanDir() } } - // DCT: None available + var dirCleanTask = CleanDir(); + + var failRemoteDeployTask = remoteDeploymentManager.FailDeployment( + job, + FormatExceptionForUsers(exception), + CancellationToken.None); // DCT: None available + return ValueTaskExtensions.WhenAll( - CleanDir(), - remoteDeploymentManager.FailDeployment( - job, - FormatExceptionForUsers(exception), - CancellationToken.None)); + dirCleanTask, + failRemoteDeployTask); } } } diff --git a/src/Tgstation.Server.Host/Components/Deployment/HardLinkDmbProvider.cs b/src/Tgstation.Server.Host/Components/Deployment/HardLinkDmbProvider.cs index 568786e3408..98a8361e342 100644 --- a/src/Tgstation.Server.Host/Components/Deployment/HardLinkDmbProvider.cs +++ b/src/Tgstation.Server.Host/Components/Deployment/HardLinkDmbProvider.cs @@ -153,7 +153,7 @@ async Task MirrorSourceDirectory(int? taskThrottle, CancellationToken ca if (taskThrottle.HasValue && taskThrottle < 1) throw new ArgumentOutOfRangeException(nameof(taskThrottle), taskThrottle, "taskThrottle must be at least 1!"); - var src = IOManager.ResolvePath(CompileJob.DirectoryName.ToString()); + var src = IOManager.ResolvePath(CompileJob.DirectoryName!.Value.ToString()); var dest = IOManager.ResolvePath(mirrorGuid.ToString()); using var semaphore = taskThrottle.HasValue ? new SemaphoreSlim(taskThrottle.Value) : null; @@ -178,10 +178,10 @@ async Task MirrorSourceDirectory(int? taskThrottle, CancellationToken ca /// The for the operation. /// A of s representing the running operations. The first returned is always the necessary call to . /// I genuinely don't know how this will work with symlinked files. Waiting for the issue report I guess. - IEnumerable MirrorDirectoryImpl(string src, string dest, SemaphoreSlim semaphore, CancellationToken cancellationToken) + IEnumerable MirrorDirectoryImpl(string src, string dest, SemaphoreSlim? semaphore, CancellationToken cancellationToken) { var dir = new DirectoryInfo(src); - Task subdirCreationTask = null; + Task? subdirCreationTask = null; var dreamDaemonWillAcceptOutOfDirectorySymlinks = CompileJob.MinimumSecurityLevel == DreamDaemonSecurity.Trusted; foreach (var subDirectory in dir.EnumerateDirectories()) { @@ -191,7 +191,8 @@ IEnumerable MirrorDirectoryImpl(string src, string dest, SemaphoreSlim sem if (subDirectory.Attributes.HasFlag(FileAttributes.ReparsePoint)) if (dreamDaemonWillAcceptOutOfDirectorySymlinks) { - var target = subDirectory.ResolveLinkTarget(false); + var target = subDirectory.ResolveLinkTarget(false) + ?? throw new InvalidOperationException($"\"{subDirectory.FullName}\" was incorrectly identified as a symlinked directory!"); logger.LogDebug("Recreating directory {name} as symlink to {target}", subDirectory.Name, target); if (subdirCreationTask == null) { @@ -250,7 +251,9 @@ async Task LinkThisFile() if (fileInfo.Attributes.HasFlag(FileAttributes.ReparsePoint)) { // AHHHHHHHHHHHHH - var target = fileInfo.ResolveLinkTarget(!dreamDaemonWillAcceptOutOfDirectorySymlinks); + var target = fileInfo.ResolveLinkTarget(!dreamDaemonWillAcceptOutOfDirectorySymlinks) + ?? throw new InvalidOperationException($"\"{fileInfo.FullName}\" was incorrectly identified as a symlinked file!"); + if (dreamDaemonWillAcceptOutOfDirectorySymlinks) { logger.LogDebug("Recreating symlinked file {name} as symlink to {target}", fileInfo.Name, target.FullName); diff --git a/src/Tgstation.Server.Host/Components/Deployment/ICompileJobSink.cs b/src/Tgstation.Server.Host/Components/Deployment/ICompileJobSink.cs index 49d4eae165e..cf18fc04121 100644 --- a/src/Tgstation.Server.Host/Components/Deployment/ICompileJobSink.cs +++ b/src/Tgstation.Server.Host/Components/Deployment/ICompileJobSink.cs @@ -15,9 +15,9 @@ public interface ICompileJobSink : ILatestCompileJobProvider /// Load a new into the . /// /// The to load. - /// An to be called when the becomes active or is discarded with or respectively. + /// An optional to be called when the becomes active or is discarded with or respectively. /// The for the operation. /// A representing the running operation. - ValueTask LoadCompileJob(CompileJob job, Action activationAction, CancellationToken cancellationToken); + ValueTask LoadCompileJob(CompileJob job, Action? activationAction, CancellationToken cancellationToken); } } diff --git a/src/Tgstation.Server.Host/Components/Deployment/IDmbFactory.cs b/src/Tgstation.Server.Host/Components/Deployment/IDmbFactory.cs index 6ff04b0a00f..08d1a782b54 100644 --- a/src/Tgstation.Server.Host/Components/Deployment/IDmbFactory.cs +++ b/src/Tgstation.Server.Host/Components/Deployment/IDmbFactory.cs @@ -23,7 +23,7 @@ public interface IDmbFactory : ILatestCompileJobProvider, IComponentService, IDi bool DmbAvailable { get; } /// - /// Gets the next . + /// Gets the next . is a precondition. /// /// The amount of locks to give the resulting . It's must be called this many times to properly clean the job. /// A new . @@ -35,7 +35,7 @@ public interface IDmbFactory : ILatestCompileJobProvider, IComponentService, IDi /// The to make the for. /// The for the operation. /// A resulting in a new representing the on success, on failure. - ValueTask FromCompileJob(CompileJob compileJob, CancellationToken cancellationToken); + ValueTask FromCompileJob(CompileJob compileJob, CancellationToken cancellationToken); /// /// Deletes all compile jobs that are inactive in the Game folder. diff --git a/src/Tgstation.Server.Host/Components/Deployment/IDmbProvider.cs b/src/Tgstation.Server.Host/Components/Deployment/IDmbProvider.cs index c1c477ffff7..820796c3186 100644 --- a/src/Tgstation.Server.Host/Components/Deployment/IDmbProvider.cs +++ b/src/Tgstation.Server.Host/Components/Deployment/IDmbProvider.cs @@ -1,6 +1,6 @@ using System; -using Tgstation.Server.Host.Models; +using Tgstation.Server.Api.Models; namespace Tgstation.Server.Host.Components.Deployment { @@ -22,7 +22,12 @@ public interface IDmbProvider : IAsyncDisposable /// /// The of the .dmb. /// - CompileJob CompileJob { get; } + Models.CompileJob CompileJob { get; } + + /// + /// The used to build the .dmb. + /// + EngineVersion EngineVersion { get; } /// /// Disposing the won't cause a cleanup of the working directory. diff --git a/src/Tgstation.Server.Host/Components/Deployment/ILatestCompileJobProvider.cs b/src/Tgstation.Server.Host/Components/Deployment/ILatestCompileJobProvider.cs index 38a90c8dc60..021f62602d7 100644 --- a/src/Tgstation.Server.Host/Components/Deployment/ILatestCompileJobProvider.cs +++ b/src/Tgstation.Server.Host/Components/Deployment/ILatestCompileJobProvider.cs @@ -10,7 +10,7 @@ public interface ILatestCompileJobProvider /// /// Gets the latest . /// - /// The latest . - CompileJob LatestCompileJob(); + /// The latest or if none are available. + CompileJob? LatestCompileJob(); } } diff --git a/src/Tgstation.Server.Host/Components/Deployment/Remote/BaseRemoteDeploymentManager.cs b/src/Tgstation.Server.Host/Components/Deployment/Remote/BaseRemoteDeploymentManager.cs index db48c659ba5..0f8c86732f9 100644 --- a/src/Tgstation.Server.Host/Components/Deployment/Remote/BaseRemoteDeploymentManager.cs +++ b/src/Tgstation.Server.Host/Components/Deployment/Remote/BaseRemoteDeploymentManager.cs @@ -52,18 +52,23 @@ protected BaseRemoteDeploymentManager( /// public async ValueTask PostDeploymentComments( CompileJob compileJob, - RevisionInformation previousRevisionInformation, + RevisionInformation? previousRevisionInformation, RepositorySettings repositorySettings, - string repoOwner, - string repoName, + string? repoOwner, + string? repoName, CancellationToken cancellationToken) { - if (repositorySettings?.AccessToken == null) + ArgumentNullException.ThrowIfNull(compileJob); + ArgumentNullException.ThrowIfNull(repositorySettings); + ArgumentNullException.ThrowIfNull(repoOwner); + ArgumentNullException.ThrowIfNull(repoName); + + if (repositorySettings.AccessToken == null) return; var deployedRevisionInformation = compileJob.RevisionInformation; if ((previousRevisionInformation != null && previousRevisionInformation.CommitSha == deployedRevisionInformation.CommitSha) - || !repositorySettings.PostTestMergeComment.Value) + || !repositorySettings.PostTestMergeComment!.Value) return; previousRevisionInformation ??= new RevisionInformation(); @@ -94,7 +99,7 @@ public async ValueTask PostDeploymentComments( .Any(y => y.TestMerge.Number == x.Number)) .ToList(); - if (!addedTestMerges.Any() && !removedTestMerges.Any() && !updatedTestMerges.Any()) + if (addedTestMerges.Count == 0 && removedTestMerges.Count == 0 && updatedTestMerges.Count == 0) return; Logger.LogTrace( @@ -105,48 +110,54 @@ public async ValueTask PostDeploymentComments( var tasks = new List(addedTestMerges.Count + updatedTestMerges.Count + removedTestMerges.Count); foreach (var addedTestMerge in addedTestMerges) - tasks.Add( - CommentOnTestMergeSource( + { + var addCommentTask = CommentOnTestMergeSource( + repositorySettings, + repoOwner, + repoName, + FormatTestMerge( repositorySettings, + compileJob, + addedTestMerge, repoOwner, repoName, - FormatTestMerge( - repositorySettings, - compileJob, - addedTestMerge, - repoOwner, - repoName, - false), - addedTestMerge.Number, - cancellationToken)); + false), + addedTestMerge.Number, + cancellationToken); + tasks.Add(addCommentTask); + } foreach (var removedTestMerge in removedTestMerges) - tasks.Add( - CommentOnTestMergeSource( - repositorySettings, - repoOwner, - repoName, - "#### Test Merge Removed", - removedTestMerge.Number, - cancellationToken)); + { + var removeCommentTask = CommentOnTestMergeSource( + repositorySettings, + repoOwner, + repoName, + "#### Test Merge Removed", + removedTestMerge.Number, + cancellationToken); + tasks.Add(removeCommentTask); + } foreach (var updatedTestMerge in updatedTestMerges) - tasks.Add( - CommentOnTestMergeSource( + { + var updateCommentTask = CommentOnTestMergeSource( + repositorySettings, + repoOwner, + repoName, + FormatTestMerge( repositorySettings, + compileJob, + updatedTestMerge, repoOwner, repoName, - FormatTestMerge( - repositorySettings, - compileJob, - updatedTestMerge, - repoOwner, - repoName, - true), - updatedTestMerge.Number, - cancellationToken)); - - if (tasks.Any()) + true), + updatedTestMerge.Number, + cancellationToken); + tasks.Add(updateCommentTask); + } + + if (tasks.Count > 0) await ValueTaskExtensions.WhenAll(tasks); } @@ -155,7 +166,7 @@ public ValueTask ApplyDeployment(CompileJob compileJob, CancellationToken cancel { ArgumentNullException.ThrowIfNull(compileJob); - if (activationCallbacks.TryGetValue(compileJob.Id.Value, out var activationCallback)) + if (activationCallbacks.TryGetValue(compileJob.Require(x => x.Id), out var activationCallback)) activationCallback(true); return ApplyDeploymentImpl(compileJob, cancellationToken); @@ -169,7 +180,7 @@ public ValueTask MarkInactive(CompileJob compileJob, CancellationToken cancellat { ArgumentNullException.ThrowIfNull(compileJob); - if (activationCallbacks.TryRemove(compileJob.Id.Value, out var activationCallback)) + if (activationCallbacks.TryRemove(compileJob.Require(x => x.Id), out var activationCallback)) activationCallback(false); return MarkInactiveImpl(compileJob, cancellationToken); @@ -183,12 +194,13 @@ public abstract ValueTask> RemoveMergedTestMerges CancellationToken cancellationToken); /// - public ValueTask StageDeployment(CompileJob compileJob, Action activationCallback, CancellationToken cancellationToken) + public ValueTask StageDeployment(CompileJob compileJob, Action? activationCallback, CancellationToken cancellationToken) { ArgumentNullException.ThrowIfNull(compileJob); - if (activationCallback != null && !activationCallbacks.TryAdd(compileJob.Id.Value, activationCallback)) - Logger.LogError("activationCallbacks conflicted on CompileJob #{id}!", compileJob.Id.Value); + var compileJobId = compileJob.Require(x => x.Id); + if (activationCallback != null && !activationCallbacks.TryAdd(compileJobId, activationCallback)) + Logger.LogError("activationCallbacks conflicted on CompileJob #{id}!", compileJobId); return StageDeploymentImpl(compileJob, cancellationToken); } diff --git a/src/Tgstation.Server.Host/Components/Deployment/Remote/GitHubRemoteDeploymentManager.cs b/src/Tgstation.Server.Host/Components/Deployment/Remote/GitHubRemoteDeploymentManager.cs index a31780bf214..4e37e7d008b 100644 --- a/src/Tgstation.Server.Host/Components/Deployment/Remote/GitHubRemoteDeploymentManager.cs +++ b/src/Tgstation.Server.Host/Components/Deployment/Remote/GitHubRemoteDeploymentManager.cs @@ -65,7 +65,7 @@ public override async ValueTask StartDeployment( Logger.LogTrace("Starting deployment..."); - RepositorySettings repositorySettings = null; + RepositorySettings? repositorySettings = null; await databaseContextFactory.UseContext( async databaseContext => repositorySettings = await databaseContext @@ -74,12 +74,12 @@ await databaseContextFactory.UseContext( .Where(x => x.InstanceId == Metadata.Id) .FirstAsync(cancellationToken)); - var instanceAuthenticated = repositorySettings.AccessToken != null; - IAuthenticatedGitHubService authenticatedGitHubService; + var instanceAuthenticated = repositorySettings!.AccessToken != null; + IAuthenticatedGitHubService? authenticatedGitHubService; IGitHubService gitHubService; if (instanceAuthenticated) { - authenticatedGitHubService = gitHubServiceFactory.CreateService(repositorySettings.AccessToken); + authenticatedGitHubService = gitHubServiceFactory.CreateService(repositorySettings.AccessToken!); gitHubService = authenticatedGitHubService; } else @@ -88,12 +88,14 @@ await databaseContextFactory.UseContext( gitHubService = gitHubServiceFactory.CreateService(); } + var repoOwner = remoteInformation.RemoteRepositoryOwner!; + var repoName = remoteInformation.RemoteRepositoryName!; var repositoryIdTask = gitHubService.GetRepositoryId( - remoteInformation.RemoteRepositoryOwner, - remoteInformation.RemoteRepositoryName, + repoOwner, + repoName, cancellationToken); - if (!repositorySettings.CreateGitHubDeployments.Value) + if (!repositorySettings.CreateGitHubDeployments!.Value) Logger.LogTrace("Not creating deployment"); else if (!instanceAuthenticated) Logger.LogWarning("Can't create GitHub deployment as no access token is set for repository!"); @@ -102,7 +104,7 @@ await databaseContextFactory.UseContext( Logger.LogTrace("Creating deployment..."); try { - compileJob.GitHubDeploymentId = await authenticatedGitHubService.CreateDeployment( + compileJob.GitHubDeploymentId = await authenticatedGitHubService!.CreateDeployment( new NewDeployment(compileJob.RevisionInformation.CommitSha) { AutoMerge = false, @@ -111,8 +113,8 @@ await databaseContextFactory.UseContext( ProductionEnvironment = true, RequiredContexts = new Collection(), }, - remoteInformation.RemoteRepositoryOwner, - remoteInformation.RemoteRepositoryName, + repoOwner, + repoName, cancellationToken); Logger.LogDebug("Created deployment ID {deploymentId}", compileJob.GitHubDeploymentId); @@ -123,8 +125,8 @@ await authenticatedGitHubService.CreateDeploymentStatus( Description = "The project is being deployed", AutoInactive = false, }, - remoteInformation.RemoteRepositoryOwner, - remoteInformation.RemoteRepositoryName, + repoOwner, + repoName, compileJob.GitHubDeploymentId.Value, cancellationToken); @@ -166,7 +168,7 @@ public override async ValueTask> RemoveMergedTest ArgumentNullException.ThrowIfNull(repositorySettings); ArgumentNullException.ThrowIfNull(revisionInformation); - if (revisionInformation.ActiveTestMerges?.Any() != true) + if ((revisionInformation.ActiveTestMerges?.Count > 0) != true) { Logger.LogTrace("No test merges to remove."); return Array.Empty(); @@ -178,7 +180,11 @@ public override async ValueTask> RemoveMergedTest var tasks = revisionInformation .ActiveTestMerges - .Select(x => gitHubService.GetPullRequest(repository.RemoteRepositoryOwner, repository.RemoteRepositoryName, x.TestMerge.Number, cancellationToken)); + .Select(x => gitHubService.GetPullRequest( + repository.RemoteRepositoryOwner!, + repository.RemoteRepositoryName!, + x.TestMerge.Number, + cancellationToken)); try { await Task.WhenAll(tasks); @@ -190,7 +196,7 @@ public override async ValueTask> RemoveMergedTest var newList = revisionInformation.ActiveTestMerges.Select(x => x.TestMerge).ToList(); - PullRequest lastMerged = null; + PullRequest? lastMerged = null; async ValueTask CheckRemovePR(Task task) { var pr = await task; @@ -198,7 +204,7 @@ async ValueTask CheckRemovePR(Task task) return; // We don't just assume, actually check the repo contains the merge commit. - if (await repository.ShaIsParent(pr.MergeCommitSha, cancellationToken)) + if (await repository.CommittishIsParent(pr.MergeCommitSha, cancellationToken)) { if (lastMerged == null || lastMerged.MergedAt < pr.MergedAt) lastMerged = pr; @@ -249,7 +255,7 @@ protected override async ValueTask CommentOnTestMergeSource( int testMergeNumber, CancellationToken cancellationToken) { - var gitHubService = gitHubServiceFactory.CreateService(repositorySettings.AccessToken); + var gitHubService = gitHubServiceFactory.CreateService(repositorySettings.AccessToken!); try { @@ -272,12 +278,12 @@ protected override string FormatTestMerge( CultureInfo.InvariantCulture, "#### Test Merge {4}{0}{0}
Details{0}{0}##### Server Instance{0}{5}{1}{0}{0}##### Revision{0}Origin: {6}{0}Pull Request: {2}{0}Server: {7}{3}{8}{0}
", Environment.NewLine, - repositorySettings.ShowTestMergeCommitters.Value + repositorySettings.ShowTestMergeCommitters!.Value ? String.Format( CultureInfo.InvariantCulture, "{0}{0}##### Merged By{0}{1}", Environment.NewLine, - testMerge.MergedBy.Name) + testMerge.MergedBy!.Name) : String.Empty, testMerge.TargetCommitSha, testMerge.Comment != null @@ -292,7 +298,7 @@ protected override string FormatTestMerge( compileJob.RevisionInformation.OriginCommitSha, compileJob.RevisionInformation.CommitSha, compileJob.GitHubDeploymentId.HasValue - ? $"{Environment.NewLine}[GitHub Deployments](https://github.com/{remoteRepositoryOwner}/{remoteRepositoryName}/deployments/activity_log?environment=TGS%3A+{Metadata.Name.Replace(" ", "+", StringComparison.Ordinal)})" + ? $"{Environment.NewLine}[GitHub Deployments](https://github.com/{remoteRepositoryOwner}/{remoteRepositoryName}/deployments/activity_log?environment=TGS%3A+{Metadata.Name!.Replace(" ", "+", StringComparison.Ordinal)})" : String.Empty); /// @@ -319,7 +325,7 @@ async ValueTask UpdateDeployment( Logger.LogTrace("Updating deployment {gitHubDeploymentId} to {deploymentState}...", compileJob.GitHubDeploymentId.Value, deploymentState); - string gitHubAccessToken = null; + string? gitHubAccessToken = null; await databaseContextFactory.UseContext( async databaseContext => gitHubAccessToken = await databaseContext diff --git a/src/Tgstation.Server.Host/Components/Deployment/Remote/GitLabRemoteDeploymentManager.cs b/src/Tgstation.Server.Host/Components/Deployment/Remote/GitLabRemoteDeploymentManager.cs index 3154b2ee03c..9d0adeaf37f 100644 --- a/src/Tgstation.Server.Host/Components/Deployment/Remote/GitLabRemoteDeploymentManager.cs +++ b/src/Tgstation.Server.Host/Components/Deployment/Remote/GitLabRemoteDeploymentManager.cs @@ -46,7 +46,7 @@ public override async ValueTask> RemoveMergedTest ArgumentNullException.ThrowIfNull(repositorySettings); ArgumentNullException.ThrowIfNull(revisionInformation); - if (revisionInformation.ActiveTestMerges?.Any() != true) + if ((revisionInformation.ActiveTestMerges?.Count > 0) != true) { Logger.LogTrace("No test merges to remove."); return Array.Empty(); @@ -75,7 +75,7 @@ public override async ValueTask> RemoveMergedTest var newList = revisionInformation.ActiveTestMerges.Select(x => x.TestMerge).ToList(); - MergeRequest lastMerged = null; + MergeRequest? lastMerged = null; async ValueTask CheckRemoveMR(Task task) { var mergeRequest = await task; @@ -83,7 +83,7 @@ async ValueTask CheckRemoveMR(Task task) return; // We don't just assume, actually check the repo contains the merge commit. - if (await repository.ShaIsParent(mergeRequest.MergeCommitSha, cancellationToken)) + if (await repository.CommittishIsParent(mergeRequest.MergeCommitSha, cancellationToken)) { if (lastMerged == null || lastMerged.ClosedAt < mergeRequest.ClosedAt) lastMerged = mergeRequest; @@ -162,12 +162,12 @@ protected override string FormatTestMerge( CultureInfo.InvariantCulture, "#### Test Merge {4}{0}{0}##### Server Instance{0}{5}{1}{0}{0}##### Revision{0}Origin: {6}{0}Merge Request: {2}{0}Server: {7}{3}", Environment.NewLine, - repositorySettings.ShowTestMergeCommitters.Value + repositorySettings.ShowTestMergeCommitters!.Value ? String.Format( CultureInfo.InvariantCulture, "{0}{0}##### Merged By{0}{1}", Environment.NewLine, - testMerge.MergedBy.Name) + testMerge.MergedBy!.Name) : String.Empty, testMerge.TargetCommitSha, testMerge.Comment != null diff --git a/src/Tgstation.Server.Host/Components/Deployment/Remote/IRemoteDeploymentManager.cs b/src/Tgstation.Server.Host/Components/Deployment/Remote/IRemoteDeploymentManager.cs index 42b154ac7ef..d8017a6686c 100644 --- a/src/Tgstation.Server.Host/Components/Deployment/Remote/IRemoteDeploymentManager.cs +++ b/src/Tgstation.Server.Host/Components/Deployment/Remote/IRemoteDeploymentManager.cs @@ -34,7 +34,7 @@ ValueTask StartDeployment( /// A representing the running operation. ValueTask StageDeployment( CompileJob compileJob, - Action activationCallback, + Action? activationCallback, CancellationToken cancellationToken); /// @@ -66,18 +66,18 @@ ValueTask StageDeployment( /// Post deployment comments to the test merge ticket. /// /// The deployed . - /// The of the previous deployment. + /// The optional of the previous deployment. /// The . - /// The GitHub repostiory owner. - /// The GitHub repostiory name. + /// The remote repostiory owner. + /// The remote repostiory name. /// The for the operation. /// A representing the running operation. ValueTask PostDeploymentComments( CompileJob compileJob, - RevisionInformation previousRevisionInformation, + RevisionInformation? previousRevisionInformation, RepositorySettings repositorySettings, - string repoOwner, - string repoName, + string? repoOwner, + string? repoName, CancellationToken cancellationToken); /// diff --git a/src/Tgstation.Server.Host/Components/Deployment/SwappableDmbProvider.cs b/src/Tgstation.Server.Host/Components/Deployment/SwappableDmbProvider.cs index 7279297bf51..75c5a1b6bb1 100644 --- a/src/Tgstation.Server.Host/Components/Deployment/SwappableDmbProvider.cs +++ b/src/Tgstation.Server.Host/Components/Deployment/SwappableDmbProvider.cs @@ -2,8 +2,8 @@ using System.Threading; using System.Threading.Tasks; +using Tgstation.Server.Api.Models; using Tgstation.Server.Host.IO; -using Tgstation.Server.Host.Models; namespace Tgstation.Server.Host.Components.Deployment { @@ -24,7 +24,10 @@ abstract class SwappableDmbProvider : IDmbProvider public string Directory => IOManager.ResolvePath(LiveGameDirectory); /// - public CompileJob CompileJob => BaseProvider.CompileJob; + public Models.CompileJob CompileJob => BaseProvider.CompileJob; + + /// + public EngineVersion EngineVersion => BaseProvider.EngineVersion; /// /// If has been run. diff --git a/src/Tgstation.Server.Host/Components/Deployment/TemporaryDmbProvider.cs b/src/Tgstation.Server.Host/Components/Deployment/TemporaryDmbProvider.cs index 4476d9d2c3d..4d528d7ba1e 100644 --- a/src/Tgstation.Server.Host/Components/Deployment/TemporaryDmbProvider.cs +++ b/src/Tgstation.Server.Host/Components/Deployment/TemporaryDmbProvider.cs @@ -1,41 +1,41 @@ using System; using System.Threading.Tasks; -using Tgstation.Server.Host.Models; +using Tgstation.Server.Api.Models; namespace Tgstation.Server.Host.Components.Deployment { /// /// Temporary . /// - sealed class TemporaryDmbProvider : IDmbProvider + sealed class TemporaryDmbProvider : DmbProviderBase { /// - public string DmbName { get; } + public override string Directory { get; } /// - public string Directory { get; } + public override Models.CompileJob CompileJob { get; } /// - public CompileJob CompileJob { get; } + public override EngineVersion EngineVersion { get; } /// /// Initializes a new instance of the class. /// /// The value of . - /// The value of . /// The value of . - public TemporaryDmbProvider(string directory, string dmb, CompileJob compileJob) + /// The value of . + public TemporaryDmbProvider(string directory, Models.CompileJob compileJob, EngineVersion engineVersion) { - DmbName = dmb ?? throw new ArgumentNullException(nameof(dmb)); Directory = directory ?? throw new ArgumentNullException(nameof(directory)); CompileJob = compileJob ?? throw new ArgumentNullException(nameof(compileJob)); + EngineVersion = engineVersion ?? throw new ArgumentNullException(nameof(engineVersion)); } /// - public ValueTask DisposeAsync() => ValueTask.CompletedTask; + public override ValueTask DisposeAsync() => ValueTask.CompletedTask; /// - public void KeepAlive() => throw new NotSupportedException(); + public override void KeepAlive() => throw new NotSupportedException(); } } diff --git a/src/Tgstation.Server.Host/Components/Engine/ByondInstallation.cs b/src/Tgstation.Server.Host/Components/Engine/ByondInstallation.cs new file mode 100644 index 00000000000..fce9b2c5bc3 --- /dev/null +++ b/src/Tgstation.Server.Host/Components/Engine/ByondInstallation.cs @@ -0,0 +1,146 @@ +using System; +using System.Collections.Generic; +using System.Globalization; +using System.Threading.Tasks; + +using Tgstation.Server.Api.Models; +using Tgstation.Server.Api.Models.Internal; +using Tgstation.Server.Host.Components.Deployment; + +namespace Tgstation.Server.Host.Components.Engine +{ + /// + /// Implementation of for . + /// + sealed class ByondInstallation : EngineInstallationBase + { + /// + public override EngineVersion Version { get; } + + /// + public override string ServerExePath { get; } + + /// + public override string CompilerExePath { get; } + + /// + public override bool PromptsForNetworkAccess { get; } + + /// + public override bool HasStandardOutput { get; } + + /// + public override bool PreferFileLogging => false; + + /// + public override Task InstallationTask { get; } + + /// + /// If map threads are supported by the . + /// + readonly bool supportsMapThreads; + + /// + /// Change a given into the appropriate DreamDaemon command line word. + /// + /// The level to change. + /// A representation of the command line parameter. + static string SecurityWord(DreamDaemonSecurity securityLevel) + { + return securityLevel switch + { + DreamDaemonSecurity.Safe => "safe", + DreamDaemonSecurity.Trusted => "trusted", + DreamDaemonSecurity.Ultrasafe => "ultrasafe", + _ => throw new ArgumentOutOfRangeException(nameof(securityLevel), securityLevel, String.Format(CultureInfo.InvariantCulture, "Bad DreamDaemon security level: {0}", securityLevel)), + }; + } + + /// + /// Change a given into the appropriate DreamDaemon command line word. + /// + /// The level to change. + /// A representation of the command line parameter. + static string VisibilityWord(DreamDaemonVisibility visibility) + { + return visibility switch + { + DreamDaemonVisibility.Public => "public", + DreamDaemonVisibility.Private => "private", + DreamDaemonVisibility.Invisible => "invisible", + _ => throw new ArgumentOutOfRangeException(nameof(visibility), visibility, String.Format(CultureInfo.InvariantCulture, "Bad DreamDaemon visibility level: {0}", visibility)), + }; + } + + /// + /// Initializes a new instance of the class. + /// + /// The value of . + /// The value of . + /// The value of . + /// The value of . + /// If a CLI application is being used. + /// The value of . + public ByondInstallation( + Task installationTask, + EngineVersion version, + string dreamDaemonPath, + string dreamMakerPath, + bool supportsCli, + bool supportsMapThreads) + { + InstallationTask = installationTask ?? throw new ArgumentNullException(nameof(installationTask)); + ArgumentNullException.ThrowIfNull(version); + + if (version.Engine != EngineType.Byond) + throw new ArgumentException($"Invalid EngineType: {version.Engine}", nameof(version)); + + Version = version ?? throw new ArgumentNullException(nameof(version)); + ServerExePath = dreamDaemonPath ?? throw new ArgumentNullException(nameof(dreamDaemonPath)); + CompilerExePath = dreamMakerPath ?? throw new ArgumentNullException(nameof(dreamMakerPath)); + HasStandardOutput = supportsCli; + PromptsForNetworkAccess = !supportsCli; + this.supportsMapThreads = supportsMapThreads; + } + + /// + public override string FormatServerArguments( + IDmbProvider dmbProvider, + IReadOnlyDictionary parameters, + DreamDaemonLaunchParameters launchParameters, + string? logFilePath) + { + ArgumentNullException.ThrowIfNull(dmbProvider); + ArgumentNullException.ThrowIfNull(parameters); + ArgumentNullException.ThrowIfNull(launchParameters); + + var parametersString = EncodeParameters(parameters, launchParameters); + + var arguments = String.Format( + CultureInfo.InvariantCulture, + "{0} -port {1} -ports 1-65535 {2}-close -verbose -{3} -{4}{5}{6}{7} -params \"{8}\"", + dmbProvider.DmbName, + launchParameters.Port!.Value, + launchParameters.AllowWebClient!.Value + ? "-webclient " + : String.Empty, + SecurityWord(launchParameters.SecurityLevel!.Value), + VisibilityWord(launchParameters.Visibility!.Value), + logFilePath != null + ? $" -logself -log {logFilePath}" + : String.Empty, // DD doesn't output anything if -logself is set??? + launchParameters.StartProfiler!.Value + ? " -profile" + : String.Empty, + supportsMapThreads && launchParameters.MapThreads!.Value != 0 + ? $" -map-threads {launchParameters.MapThreads.Value}" + : String.Empty, + parametersString); + return arguments; + } + + /// + public override string FormatCompilerArguments(string dmePath) + => $"-clean \"{dmePath ?? throw new ArgumentNullException(nameof(dmePath))}\""; + } +} diff --git a/src/Tgstation.Server.Host/Components/Engine/ByondInstallerBase.cs b/src/Tgstation.Server.Host/Components/Engine/ByondInstallerBase.cs new file mode 100644 index 00000000000..02691acfc08 --- /dev/null +++ b/src/Tgstation.Server.Host/Components/Engine/ByondInstallerBase.cs @@ -0,0 +1,242 @@ +using System; +using System.Globalization; +using System.Text; +using System.Threading; +using System.Threading.Tasks; + +using Microsoft.Extensions.Logging; + +using Tgstation.Server.Api.Models; +using Tgstation.Server.Host.IO; +using Tgstation.Server.Host.Jobs; +using Tgstation.Server.Host.Utils; + +namespace Tgstation.Server.Host.Components.Engine +{ + /// + /// Base implementation of for . + /// + abstract class ByondInstallerBase : EngineInstallerBase + { + /// + /// The path to the BYOND bin folder. + /// + protected const string ByondBinPath = "byond/bin"; + + /// + /// The name of BYOND's cache directory. + /// + const string CacheDirectoryName = "cache"; + + /// + /// The path to the cfg directory. + /// + const string CfgDirectoryName = "cfg"; + + /// + /// The name of the list of trusted .dmb files in the user's BYOND cfg directory. + /// + const string TrustedDmbFileName = "trusted.txt"; + + /// + /// The first of BYOND that supports the '-map-threads' parameter on DreamDaemon. + /// + static readonly Version MapThreadsVersion = new(515, 1609); + + /// + /// for writing to files in the user's BYOND directory. + /// + static readonly SemaphoreSlim UserFilesSemaphore = new(1); + + /// + protected override EngineType TargetEngineType => EngineType.Byond; + + /// + /// Path to the system user's local BYOND folder. + /// + protected abstract string PathToUserFolder { get; } + + /// + /// Path to the DreamMaker executable. + /// + protected abstract string DreamMakerName { get; } + + /// + /// Gets the URL formatter string for downloading a byond version of {0:Major} {1:Minor}. + /// + protected abstract string ByondRevisionsUrlTemplate { get; } + + /// + /// The for the . + /// + readonly IFileDownloader fileDownloader; + + /// + /// Initializes a new instance of the class. + /// + /// The for the . + /// The for the . + /// The value of . + protected ByondInstallerBase(IIOManager ioManager, ILogger logger, IFileDownloader fileDownloader) + : base(ioManager, logger) + { + this.fileDownloader = fileDownloader ?? throw new ArgumentNullException(nameof(fileDownloader)); + } + + /// + public override IEngineInstallation CreateInstallation(EngineVersion version, string path, Task installationTask) + { + CheckVersionValidity(version); + + var binPathForVersion = IOManager.ConcatPath(path, ByondBinPath); + var supportsMapThreads = version.Version >= MapThreadsVersion; + + return new ByondInstallation( + installationTask, + version, + IOManager.ResolvePath( + IOManager.ConcatPath( + binPathForVersion, + GetDreamDaemonName( + version.Version!, + out var supportsCli))), + IOManager.ResolvePath( + IOManager.ConcatPath( + binPathForVersion, + DreamMakerName)), + supportsCli, + supportsMapThreads); + } + + /// + public override async Task CleanCache(CancellationToken cancellationToken) + { + try + { + var byondDir = PathToUserFolder; + + Logger.LogDebug("Cleaning BYOND cache..."); + var cacheCleanTask = IOManager.DeleteDirectory( + IOManager.ConcatPath( + byondDir, + CacheDirectoryName), + cancellationToken); + + // Create local cfg directory in case it doesn't exist + var localCfgDirectory = IOManager.ConcatPath( + byondDir, + CfgDirectoryName); + + var cfgCreateTask = IOManager.CreateDirectory( + localCfgDirectory, + 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); + var trustedDmbDeleteTask = IOManager.DeleteFile( + trustedFilePath, + cancellationToken); + + await Task.WhenAll(cacheCleanTask, cfgCreateTask, trustedDmbDeleteTask); + } + catch (Exception ex) when (ex is not OperationCanceledException) + { + Logger.LogWarning(ex, "Error cleaning BYOND cache!"); + } + } + + /// + public override async ValueTask TrustDmbPath(EngineVersion version, string fullDmbPath, CancellationToken cancellationToken) + { + ArgumentNullException.ThrowIfNull(version); + ArgumentNullException.ThrowIfNull(fullDmbPath); + + var byondDir = PathToUserFolder; + var cfgDir = IOManager.ConcatPath( + byondDir, + CfgDirectoryName); + var trustedFilePath = IOManager.ConcatPath( + cfgDir, + TrustedDmbFileName); + + Logger.LogDebug("Adding .dmb ({dmbPath}) to {trustedFilePath}", fullDmbPath, trustedFilePath); + + using (await SemaphoreSlimContext.Lock(UserFilesSemaphore, cancellationToken)) + { + string trustedFileText; + var filePreviouslyExisted = await IOManager.FileExists(trustedFilePath, cancellationToken); + if (filePreviouslyExisted) + { + var trustedFileBytes = await IOManager.ReadAllBytes(trustedFilePath, cancellationToken); + trustedFileText = Encoding.UTF8.GetString(trustedFileBytes); + trustedFileText = $"{trustedFileText.Trim()}{Environment.NewLine}"; + } + else + trustedFileText = String.Empty; + + if (trustedFileText.Contains(fullDmbPath, StringComparison.Ordinal)) + return; + + trustedFileText = $"{trustedFileText}{fullDmbPath}{Environment.NewLine}"; + + var newTrustedFileBytes = Encoding.UTF8.GetBytes(trustedFileText); + + if (!filePreviouslyExisted) + await IOManager.CreateDirectory(cfgDir, cancellationToken); + + await IOManager.WriteAllBytes(trustedFilePath, newTrustedFileBytes, cancellationToken); + } + } + + /// + public override async ValueTask DownloadVersion(EngineVersion version, JobProgressReporter? progressReporter, CancellationToken cancellationToken) + { + CheckVersionValidity(version); + + var url = GetDownloadZipUrl(version); + Logger.LogTrace("Downloading {engineType} version {version} from {url}...", TargetEngineType, version, url); + + await using var download = fileDownloader.DownloadFile(url, null); + await using var buffer = new BufferedFileStreamProvider( + await download.GetResult(cancellationToken)); + + var stream = await buffer.GetOwnedResult(cancellationToken); + try + { + return new ZipStreamEngineInstallationData( + IOManager, + stream); + } + catch + { + await stream.DisposeAsync(); + throw; + } + } + + /// + /// Get the file name of the DreamDaemon executable. + /// + /// The of BYOND to select the executable name for. + /// Whether or not the returned path supports being run as a command-line application. + /// The file name of the DreamDaemon executable. + protected abstract string GetDreamDaemonName(Version byondVersion, out bool supportsCli); + + /// + /// Create a pointing to the location of the download for a given . + /// + /// The to create a for. + /// A pointing to the version download location. + Uri GetDownloadZipUrl(EngineVersion version) + { + CheckVersionValidity(version); + var url = String.Format(CultureInfo.InvariantCulture, ByondRevisionsUrlTemplate, version.Version!.Major, version.Version.Minor); + return new Uri(url); + } + } +} diff --git a/src/Tgstation.Server.Host/Components/Engine/DelegatingEngineInstaller.cs b/src/Tgstation.Server.Host/Components/Engine/DelegatingEngineInstaller.cs new file mode 100644 index 00000000000..1157dff565a --- /dev/null +++ b/src/Tgstation.Server.Host/Components/Engine/DelegatingEngineInstaller.cs @@ -0,0 +1,68 @@ +using System; +using System.Collections.Frozen; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; + +using Tgstation.Server.Api.Models; +using Tgstation.Server.Host.Jobs; + +namespace Tgstation.Server.Host.Components.Engine +{ + /// + /// Implementation of that forwards calls to different based on their appropriate . + /// + sealed class DelegatingEngineInstaller : IEngineInstaller + { + /// + /// The mapping s to their appropriate . + /// + readonly FrozenDictionary delegatedInstallers; + + /// + /// Initializes a new instance of the class. + /// + /// The value of . + public DelegatingEngineInstaller(FrozenDictionary delegatedInstallers) + { + this.delegatedInstallers = delegatedInstallers ?? throw new ArgumentNullException(nameof(delegatedInstallers)); + } + + /// + public Task CleanCache(CancellationToken cancellationToken) + => Task.WhenAll(delegatedInstallers.Values.Select(installer => installer.CleanCache(cancellationToken))); + + /// + public IEngineInstallation CreateInstallation(EngineVersion version, string path, Task installationTask) + => DelegateCall(version, installer => installer.CreateInstallation(version, path, installationTask)); + + /// + public ValueTask DownloadVersion(EngineVersion version, JobProgressReporter? jobProgressReporter, CancellationToken cancellationToken) + => DelegateCall(version, installer => installer.DownloadVersion(version, jobProgressReporter, cancellationToken)); + + /// + public ValueTask Install(EngineVersion version, string path, CancellationToken cancellationToken) + => DelegateCall(version, installer => installer.Install(version, path, cancellationToken)); + + /// + public ValueTask TrustDmbPath(EngineVersion version, string fullDmbPath, CancellationToken cancellationToken) + => DelegateCall(version, installer => installer.TrustDmbPath(version, fullDmbPath, cancellationToken)); + + /// + public ValueTask UpgradeInstallation(EngineVersion version, string path, CancellationToken cancellationToken) + => DelegateCall(version, installer => installer.UpgradeInstallation(version, path, cancellationToken)); + + /// + /// Delegate a given to its appropriate . + /// + /// The return of the call. + /// The used to perform delegate selection. + /// The that will be called with the correct based on . + /// The value of the delegated call. + TReturn DelegateCall(EngineVersion version, Func call) + { + ArgumentNullException.ThrowIfNull(version); + return call(delegatedInstallers[version.Engine!.Value]); + } + } +} diff --git a/src/Tgstation.Server.Host/Components/Engine/EngineExecutableLock.cs b/src/Tgstation.Server.Host/Components/Engine/EngineExecutableLock.cs new file mode 100644 index 00000000000..5d7a1d7fef3 --- /dev/null +++ b/src/Tgstation.Server.Host/Components/Engine/EngineExecutableLock.cs @@ -0,0 +1,66 @@ +using System.Collections.Generic; +using System.Threading; +using System.Threading.Tasks; + +using Microsoft.Extensions.Logging; + +using Tgstation.Server.Api.Models; +using Tgstation.Server.Api.Models.Internal; +using Tgstation.Server.Host.Components.Deployment; +using Tgstation.Server.Host.System; +using Tgstation.Server.Host.Utils; + +namespace Tgstation.Server.Host.Components.Engine +{ + /// + class EngineExecutableLock : ReferenceCounter, IEngineExecutableLock + { + /// + public EngineVersion Version => Instance.Version; + + /// + public string ServerExePath => Instance.ServerExePath; + + /// + public string CompilerExePath => Instance.CompilerExePath; + + /// + public bool HasStandardOutput => Instance.HasStandardOutput; + + /// + public bool PreferFileLogging => Instance.PreferFileLogging; + + /// + public bool PromptsForNetworkAccess => Instance.PromptsForNetworkAccess; + + /// + public Task InstallationTask => Instance.InstallationTask; + + /// + public void DoNotDeleteThisSession() => DangerousDropReference(); + + /// + public string FormatServerArguments( + IDmbProvider dmbProvider, + IReadOnlyDictionary parameters, + DreamDaemonLaunchParameters launchParameters, + string? logFilePath) + => Instance.FormatServerArguments( + dmbProvider, + parameters, + launchParameters, + logFilePath); + + /// + public string FormatCompilerArguments(string dmePath) => Instance.FormatCompilerArguments(dmePath); + + /// + public ValueTask StopServerProcess(ILogger logger, IProcess process, string accessIdentifier, ushort port, CancellationToken cancellationToken) + => Instance.StopServerProcess( + logger, + process, + accessIdentifier, + port, + cancellationToken); + } +} diff --git a/src/Tgstation.Server.Host/Components/Engine/EngineInstallationBase.cs b/src/Tgstation.Server.Host/Components/Engine/EngineInstallationBase.cs new file mode 100644 index 00000000000..5edf44609f0 --- /dev/null +++ b/src/Tgstation.Server.Host/Components/Engine/EngineInstallationBase.cs @@ -0,0 +1,78 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; +using System.Web; + +using Microsoft.Extensions.Logging; + +using Tgstation.Server.Api.Models; +using Tgstation.Server.Api.Models.Internal; +using Tgstation.Server.Host.Components.Deployment; +using Tgstation.Server.Host.System; + +namespace Tgstation.Server.Host.Components.Engine +{ + /// + abstract class EngineInstallationBase : IEngineInstallation + { + /// + public abstract EngineVersion Version { get; } + + /// + public abstract string ServerExePath { get; } + + /// + public abstract string CompilerExePath { get; } + + /// + public abstract bool HasStandardOutput { get; } + + /// + public abstract bool PreferFileLogging { get; } + + /// + public abstract bool PromptsForNetworkAccess { get; } + + /// + public abstract Task InstallationTask { get; } + + /// + /// Encode given parameters for passing as world.params on the command line. + /// + /// of parameters to encode. + /// The active . + /// The formatted parameters . + protected static string EncodeParameters( + IReadOnlyDictionary parameters, + DreamDaemonLaunchParameters launchParameters) + { + var parametersString = String.Join('&', parameters.Select(kvp => $"{HttpUtility.UrlEncode(kvp.Key)}={HttpUtility.UrlEncode(kvp.Value)}")); + + if (!String.IsNullOrEmpty(launchParameters.AdditionalParameters)) + parametersString = $"{parametersString}&{launchParameters.AdditionalParameters}"; + + return parametersString; + } + + /// + public abstract string FormatCompilerArguments(string dmePath); + + /// + public abstract string FormatServerArguments( + IDmbProvider dmbProvider, + IReadOnlyDictionary parameters, + DreamDaemonLaunchParameters launchParameters, + string? logFilePath); + + /// + public virtual async ValueTask StopServerProcess(ILogger logger, IProcess process, string accessIdentifier, ushort port, CancellationToken cancellationToken) + { + cancellationToken.ThrowIfCancellationRequested(); + logger.LogTrace("Terminating engine server process..."); + process.Terminate(); + await process.Lifetime; + } + } +} diff --git a/src/Tgstation.Server.Host/Components/Engine/EngineInstallerBase.cs b/src/Tgstation.Server.Host/Components/Engine/EngineInstallerBase.cs new file mode 100644 index 00000000000..7c25c0a13c6 --- /dev/null +++ b/src/Tgstation.Server.Host/Components/Engine/EngineInstallerBase.cs @@ -0,0 +1,71 @@ +using System; +using System.Threading; +using System.Threading.Tasks; + +using Microsoft.Extensions.Logging; + +using Tgstation.Server.Api.Models; +using Tgstation.Server.Host.IO; +using Tgstation.Server.Host.Jobs; + +namespace Tgstation.Server.Host.Components.Engine +{ + /// + abstract class EngineInstallerBase : IEngineInstaller + { + /// + /// The the installer supports. + /// + protected abstract EngineType TargetEngineType { get; } + + /// + /// Gets the for the . + /// + protected IIOManager IOManager { get; } + + /// + /// Gets the for the . + /// + protected ILogger Logger { get; } + + /// + /// Initializes a new instance of the class. + /// + /// The value of . + /// The value of . + protected EngineInstallerBase(IIOManager ioManager, ILogger logger) + { + IOManager = ioManager ?? throw new ArgumentNullException(nameof(ioManager)); + Logger = logger ?? throw new ArgumentNullException(nameof(logger)); + } + + /// + public abstract IEngineInstallation CreateInstallation(EngineVersion version, string path, Task installationTask); + + /// + public abstract Task CleanCache(CancellationToken cancellationToken); + + /// + public abstract ValueTask Install(EngineVersion version, string path, CancellationToken cancellationToken); + + /// + public abstract ValueTask UpgradeInstallation(EngineVersion version, string path, CancellationToken cancellationToken); + + /// + public abstract ValueTask DownloadVersion(EngineVersion version, JobProgressReporter? jobProgressReporter, CancellationToken cancellationToken); + + /// + public abstract ValueTask TrustDmbPath(EngineVersion version, string fullDmbPath, CancellationToken cancellationToken); + + /// + /// Check that a given is of type . + /// + /// The to check. + protected void CheckVersionValidity(EngineVersion version) + { + ArgumentNullException.ThrowIfNull(version); + if (version.Engine!.Value != TargetEngineType) + throw new InvalidOperationException($"Non-{TargetEngineType} engine specified: {version.Engine.Value}"); + } + } +} diff --git a/src/Tgstation.Server.Host/Components/Byond/ByondManager.cs b/src/Tgstation.Server.Host/Components/Engine/EngineManager.cs similarity index 59% rename from src/Tgstation.Server.Host/Components/Byond/ByondManager.cs rename to src/Tgstation.Server.Host/Components/Engine/EngineManager.cs index 73fadec9874..529eec0fb8e 100644 --- a/src/Tgstation.Server.Host/Components/Byond/ByondManager.cs +++ b/src/Tgstation.Server.Host/Components/Engine/EngineManager.cs @@ -16,26 +16,11 @@ using Tgstation.Server.Host.Jobs; using Tgstation.Server.Host.Utils; -namespace Tgstation.Server.Host.Components.Byond +namespace Tgstation.Server.Host.Components.Engine { /// - sealed class ByondManager : IByondManager + sealed class EngineManager : IEngineManager { - /// - /// The path to the BYOND bin folder. - /// - public const string BinPath = "byond/bin"; - - /// - /// The path to the cfg directory. - /// - const string CfgDirectoryName = "cfg"; - - /// - /// The name of the list of trusted .dmb files in the user's BYOND cfg directory. - /// - const string TrustedDmbFileName = "trusted.txt"; - /// /// The file in which we store the for installations. /// @@ -47,10 +32,10 @@ sealed class ByondManager : IByondManager const string ActiveVersionFileName = "ActiveVersion.txt"; /// - public Version ActiveVersion { get; private set; } + public EngineVersion? ActiveVersion { get; private set; } /// - public IReadOnlyList InstalledVersions + public IReadOnlyList InstalledVersions { get { @@ -60,34 +45,29 @@ public IReadOnlyList InstalledVersions } /// - /// for writing to files in the user's BYOND directory. - /// - static readonly SemaphoreSlim UserFilesSemaphore = new (1); - - /// - /// The for the . + /// The for the . /// readonly IIOManager ioManager; /// - /// The for the . + /// The for the . /// - readonly IByondInstaller byondInstaller; + readonly IEngineInstaller engineInstaller; /// - /// The for the . + /// The for the . /// readonly IEventConsumer eventConsumer; /// - /// The for the . + /// The for the . /// - readonly ILogger logger; + readonly ILogger logger; /// - /// Map of byond s to s that complete when they are installed. + /// Map of byond s to s that complete when they are installed. /// - readonly Dictionary> installedVersions; + readonly Dictionary> installedVersions; /// /// The for changing or deleting the active BYOND version. @@ -97,35 +77,38 @@ public IReadOnlyList InstalledVersions /// /// that notifes when the changes. /// - TaskCompletionSource activeVersionChanged; + volatile TaskCompletionSource activeVersionChanged; /// /// Validates a given parameter. /// /// The to validate. - static void CheckVersionParameter(Version version) + static void CheckVersionParameter(EngineVersion version) { ArgumentNullException.ThrowIfNull(version); - if (version.Build == 0) - throw new InvalidOperationException("version.Build cannot be 0!"); + if (!version.Engine.HasValue) + throw new InvalidOperationException("version.Engine cannot be null!"); + + if (version.CustomIteration == 0) + throw new InvalidOperationException("version.CustomIteration cannot be 0!"); } /// - /// Initializes a new instance of the class. + /// Initializes a new instance of the class. /// /// The value of . - /// The value of . + /// The value of . /// The value of . /// The value of . - public ByondManager(IIOManager ioManager, IByondInstaller byondInstaller, IEventConsumer eventConsumer, ILogger logger) + public EngineManager(IIOManager ioManager, IEngineInstaller engineInstaller, IEventConsumer eventConsumer, ILogger logger) { this.ioManager = ioManager ?? throw new ArgumentNullException(nameof(ioManager)); - this.byondInstaller = byondInstaller ?? throw new ArgumentNullException(nameof(byondInstaller)); + this.engineInstaller = engineInstaller ?? throw new ArgumentNullException(nameof(engineInstaller)); this.eventConsumer = eventConsumer ?? throw new ArgumentNullException(nameof(eventConsumer)); this.logger = logger ?? throw new ArgumentNullException(nameof(logger)); - installedVersions = new Dictionary>(); + installedVersions = new Dictionary>(); changeDeleteSemaphore = new SemaphoreSlim(1); activeVersionChanged = new TaskCompletionSource(); } @@ -135,9 +118,9 @@ public ByondManager(IIOManager ioManager, IByondInstaller byondInstaller, IEvent /// public async ValueTask ChangeVersion( - JobProgressReporter progressReporter, - Version version, - Stream customVersionStream, + JobProgressReporter? progressReporter, + EngineVersion version, + Stream? customVersionStream, bool allowInstallation, CancellationToken cancellationToken) { @@ -154,13 +137,13 @@ public async ValueTask ChangeVersion( cancellationToken); // We reparse the version because it could be changed after a custom install. - version = installLock.Version; + version = new EngineVersion(installLock.Version); var stringVersion = version.ToString(); await ioManager.WriteAllBytes(ActiveVersionFileName, Encoding.UTF8.GetBytes(stringVersion), cancellationToken); await eventConsumer.HandleEvent( - EventType.ByondActiveVersionChange, - new List + EventType.EngineActiveVersionChange, + new List { ActiveVersion?.ToString(), stringVersion, @@ -169,20 +152,20 @@ await eventConsumer.HandleEvent( cancellationToken); ActiveVersion = version; - activeVersionChanged.SetResult(); - activeVersionChanged = new TaskCompletionSource(); - } - logger.LogInformation("Active version changed to {version}", version); + logger.LogInformation("Active version changed to {version}", version); + var oldTcs = Interlocked.Exchange(ref activeVersionChanged, new TaskCompletionSource()); + oldTcs.SetResult(); + } } /// - public async ValueTask UseExecutables(Version requiredVersion, string trustDmbFullPath, CancellationToken cancellationToken) + public async ValueTask UseExecutables(EngineVersion? requiredVersion, string? trustDmbFullPath, CancellationToken cancellationToken) { logger.LogTrace( "Acquiring lock on BYOND version {version}...", requiredVersion?.ToString() ?? $"{ActiveVersion} (active)"); - var versionToUse = requiredVersion ?? ActiveVersion ?? throw new JobException(ErrorCode.ByondNoVersionsInstalled); + var versionToUse = requiredVersion ?? ActiveVersion ?? throw new JobException(ErrorCode.EngineNoVersionsInstalled); var installLock = await AssertAndLockVersion( null, versionToUse, @@ -193,7 +176,7 @@ public async ValueTask UseExecutables(Version requiredVers try { if (trustDmbFullPath != null) - await TrustDmbPath(trustDmbFullPath, cancellationToken); + await engineInstaller.TrustDmbPath(installLock.Version, trustDmbFullPath, cancellationToken); return installLock; } @@ -205,7 +188,7 @@ public async ValueTask UseExecutables(Version requiredVers } /// - public async ValueTask DeleteVersion(JobProgressReporter progressReporter, Version version, CancellationToken cancellationToken) + public async ValueTask DeleteVersion(JobProgressReporter progressReporter, EngineVersion version, CancellationToken cancellationToken) { ArgumentNullException.ThrowIfNull(progressReporter); @@ -213,15 +196,24 @@ public async ValueTask DeleteVersion(JobProgressReporter progressReporter, Versi logger.LogTrace("DeleteVersion {version}", version); - if (version == ActiveVersion) - throw new JobException(ErrorCode.ByondCannotDeleteActiveVersion); + var activeVersion = ActiveVersion; + if (activeVersion != null && version.Equals(activeVersion)) + throw new JobException(ErrorCode.EngineCannotDeleteActiveVersion); - ReferenceCountingContainer container; + ReferenceCountingContainer container; + logger.LogTrace("Waiting to acquire installedVersions lock..."); lock (installedVersions) - if (!installedVersions.TryGetValue(version, out container)) - return; // already "deleted" + { + if (!installedVersions.TryGetValue(version, out var containerNullable)) + { + logger.LogTrace("Version {version} already deleted.", version); + return; + } + + container = containerNullable; + logger.LogTrace("Installation container acquired for deletion"); + } - logger.LogInformation("Deleting BYOND version {version}...", version); progressReporter.StageName = "Waiting for version to not be in use..."; while (true) { @@ -232,27 +224,32 @@ public async ValueTask DeleteVersion(JobProgressReporter progressReporter, Versi using (await SemaphoreSlimContext.Lock(changeDeleteSemaphore, cancellationToken)) activeVersionUpdate = activeVersionChanged.Task; + logger.LogTrace("Waiting for container.OnZeroReferences or switch of active version..."); await Task.WhenAny( containerTask, activeVersionUpdate) .WaitAsync(cancellationToken); if (containerTask.IsCompleted) - logger.LogTrace("All BYOND locks for {version} are gone", version); + logger.LogTrace("All locks for version {version} are gone", version); + else + logger.LogTrace("activeVersion changed, we may have to wait again. Acquiring semaphore..."); using (await SemaphoreSlimContext.Lock(changeDeleteSemaphore, cancellationToken)) { // check again because it could have become the active version. - if (version == ActiveVersion) - throw new JobException(ErrorCode.ByondCannotDeleteActiveVersion); + activeVersion = ActiveVersion; + if (activeVersion != null && version.Equals(activeVersion)) + throw new JobException(ErrorCode.EngineCannotDeleteActiveVersion); bool proceed; + logger.LogTrace("Locking installedVersions..."); lock (installedVersions) { proceed = container.OnZeroReferences.IsCompleted; if (proceed) if (!installedVersions.TryGetValue(version, out var newerContainer)) - logger.LogWarning("Unable to remove BYOND installation {version} from list! Is there a duplicate job running?", version); + logger.LogWarning("Unable to remove engine installation {version} from list! Is there a duplicate job running?", version); else { if (container != newerContainer) @@ -266,12 +263,16 @@ await Task.WhenAny( } if (proceed) + { + logger.LogTrace("Proceeding with installation deletion..."); installedVersions.Remove(version); + } } } if (proceed) { + logger.LogInformation("Deleting version {version}...", version); progressReporter.StageName = "Deleting installation..."; // delete the version file first, because we will know not to re-discover the installation if it's not present and it will get cleaned on reboot @@ -287,6 +288,8 @@ await ioManager.DeleteFile( logger.LogDebug( "Another lock was acquired before we could remove version {version} from the list. We will have to wait again.", version); + else + logger.LogTrace("Not proceeding for some reason or another"); } } } @@ -294,7 +297,7 @@ await ioManager.DeleteFile( /// public async Task StartAsync(CancellationToken cancellationToken) { - async ValueTask GetActiveVersion() + async ValueTask GetActiveVersion() { var activeVersionFileExists = await ioManager.FileExists(ActiveVersionFileName, cancellationToken); return !activeVersionFileExists ? null : await ioManager.ReadAllBytes(ActiveVersionFileName, cancellationToken); @@ -302,33 +305,10 @@ async ValueTask GetActiveVersion() var activeVersionBytesTask = GetActiveVersion(); - 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, - 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); - var installedVersionPaths = new Dictionary(); + var installedVersionPaths = new Dictionary(); async ValueTask ReadVersion(string path) { @@ -342,16 +322,19 @@ async ValueTask ReadVersion(string path) var bytes = await ioManager.ReadAllBytes(versionFile, cancellationToken); var text = Encoding.UTF8.GetString(bytes); - if (!Version.TryParse(text, out var version)) + EngineVersion version; + if (!EngineVersion.TryParse(text, out var versionNullable)) { logger.LogWarning("Cleaning path with unparsable version file: {versionPath}", ioManager.ResolvePath(path)); await ioManager.DeleteDirectory(path, cancellationToken); // cleanup return; } + else + version = versionNullable!; try { - AddInstallationContainer(version, Task.CompletedTask); + AddInstallationContainer(version, path, Task.CompletedTask); logger.LogDebug("Added detected BYOND version {versionKey}...", version); } catch (Exception ex) @@ -376,18 +359,18 @@ await ValueTaskExtensions.WhenAll( logger.LogTrace("Upgrading BYOND installations..."); await ValueTaskExtensions.WhenAll( installedVersionPaths - .Select(kvp => byondInstaller.UpgradeInstallation(kvp.Value, kvp.Key, cancellationToken))); + .Select(kvp => engineInstaller.UpgradeInstallation(kvp.Value, kvp.Key, cancellationToken))); var activeVersionBytes = await activeVersionBytesTask; if (activeVersionBytes != null) { var activeVersionString = Encoding.UTF8.GetString(activeVersionBytes); - Version activeVersion; + EngineVersion? activeVersion; bool hasRequestedActiveVersion; lock (installedVersions) - hasRequestedActiveVersion = Version.TryParse(activeVersionString, out activeVersion) - && installedVersions.ContainsKey(activeVersion); + hasRequestedActiveVersion = EngineVersion.TryParse(activeVersionString, out activeVersion) + && installedVersions.ContainsKey(activeVersion!); if (hasRequestedActiveVersion) ActiveVersion = activeVersion; // not setting TCS because there's no need during init @@ -406,23 +389,23 @@ await ValueTaskExtensions.WhenAll( /// Ensures a BYOND is installed if it isn't already. /// /// The optional for the operation. - /// The BYOND to install. - /// Custom zip file to use. Will cause a number to be added. + /// The to install. + /// Optional custom zip file to use. Will cause a number to be added. /// If this BYOND version is required as part of a locking operation. /// If an installation should be performed if the is not installed. If and an installation is required an will be thrown. /// The for the operation. - /// A resulting in the . - async ValueTask AssertAndLockVersion( - JobProgressReporter progressReporter, - Version version, - Stream customVersionStream, + /// A resulting in the . + async ValueTask AssertAndLockVersion( + JobProgressReporter? progressReporter, + EngineVersion version, + Stream? customVersionStream, bool neededForLock, bool allowInstallation, CancellationToken cancellationToken) { var ourTcs = new TaskCompletionSource(); - ByondInstallation installation; - ByondExecutableLock installLock; + IEngineInstallation installation; + EngineExecutableLock installLock; bool installedOrInstalling; lock (installedVersions) { @@ -431,19 +414,25 @@ async ValueTask AssertAndLockVersion( var customInstallationNumber = 1; do { - version = new Version(version.Major, version.Minor, customInstallationNumber++); + version.CustomIteration = customInstallationNumber++; } while (installedVersions.ContainsKey(version)); } - installedOrInstalling = installedVersions.TryGetValue(version, out var installationContainer); + installedOrInstalling = installedVersions.TryGetValue(version, out var installationContainerNullable); + ReferenceCountingContainer installationContainer; if (!installedOrInstalling) { if (!allowInstallation) - throw new InvalidOperationException($"BYOND version {version} not installed!"); + throw new InvalidOperationException($"Engine version {version} not installed!"); - installationContainer = AddInstallationContainer(version, ourTcs.Task); + installationContainer = AddInstallationContainer( + version, + ioManager.ResolvePath(version.ToString()), + ourTcs.Task); } + else + installationContainer = installationContainerNullable!; installation = installationContainer.Instance; installLock = installationContainer.AddReference(); @@ -457,7 +446,7 @@ async ValueTask AssertAndLockVersion( progressReporter.StageName = "Waiting for existing installation job..."; 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); + logger.LogWarning("The required engine version ({version}) is not readily available! We will have to wait for it to install.", version); await installation.InstallationTask.WaitAsync(cancellationToken); return installLock; @@ -467,22 +456,22 @@ async ValueTask AssertAndLockVersion( try { if (customVersionStream != null) - logger.LogInformation("Installing custom BYOND version as {version}...", version); + logger.LogInformation("Installing custom engine version as {version}...", version); else if (neededForLock) { - if (version.Build > 0) - throw new JobException(ErrorCode.ByondNonExistentCustomVersion); + if (version.CustomIteration.HasValue) + throw new JobException(ErrorCode.EngineNonExistentCustomVersion); - logger.LogWarning("The required BYOND version ({version}) is not readily available! We will have to install it.", version); + logger.LogWarning("The required engine version ({version}) is not readily available! We will have to install it.", version); } else - logger.LogDebug("Requested BYOND version {version} not currently installed. Doing so now...", version); + logger.LogDebug("Requested engine version {version} not currently installed. Doing so now...", version); if (progressReporter != null) progressReporter.StageName = "Running event"; var versionString = version.ToString(); - await eventConsumer.HandleEvent(EventType.ByondInstallStart, new List { versionString }, false, cancellationToken); + await eventConsumer.HandleEvent(EventType.EngineInstallStart, new List { versionString }, false, cancellationToken); await InstallVersionFiles(progressReporter, version, customVersionStream, cancellationToken); @@ -491,7 +480,7 @@ async ValueTask AssertAndLockVersion( catch (Exception ex) { if (ex is not OperationCanceledException) - await eventConsumer.HandleEvent(EventType.ByondInstallFail, new List { ex.Message }, false, cancellationToken); + await eventConsumer.HandleEvent(EventType.EngineInstallFail, new List { ex.Message }, false, cancellationToken); lock (installedVersions) installedVersions.Remove(version); @@ -513,11 +502,11 @@ async ValueTask AssertAndLockVersion( /// Installs the files for a given BYOND . /// /// The optional for the operation. - /// The BYOND being installed with the number set if appropriate. + /// The being installed with the number set if appropriate. /// Custom zip file to use. Will cause a number to be added. /// The for the operation. /// A representing the running operation. - async ValueTask InstallVersionFiles(JobProgressReporter progressReporter, Version version, Stream customVersionStream, CancellationToken cancellationToken) + async ValueTask InstallVersionFiles(JobProgressReporter? progressReporter, EngineVersion version, Stream? customVersionStream, CancellationToken cancellationToken) { var installFullPath = ioManager.ResolvePath(version.ToString()); async ValueTask DirectoryCleanup() @@ -529,18 +518,24 @@ async ValueTask DirectoryCleanup() var directoryCleanupTask = DirectoryCleanup(); try { - Stream versionZipStream; + IEngineInstallationData engineInstallationData; if (customVersionStream == null) { if (progressReporter != null) progressReporter.StageName = "Downloading version"; - versionZipStream = await byondInstaller.DownloadVersion(version, cancellationToken); + engineInstallationData = await engineInstaller.DownloadVersion(version, progressReporter, cancellationToken); + + progressReporter?.ReportProgress(null); } else - versionZipStream = customVersionStream; +#pragma warning disable CA2000 // Dispose objects before losing scope, false positive + engineInstallationData = new ZipStreamEngineInstallationData( + ioManager, + customVersionStream); +#pragma warning restore CA2000 // Dispose objects before losing scope - await using (versionZipStream) + await using (engineInstallationData) { if (progressReporter != null) progressReporter.StageName = "Cleaning target directory"; @@ -548,16 +543,16 @@ async ValueTask DirectoryCleanup() await directoryCleanupTask; if (progressReporter != null) - progressReporter.StageName = "Extracting zip"; + progressReporter.StageName = "Extracting data"; - logger.LogTrace("Extracting downloaded BYOND zip to {extractPath}...", installFullPath); - await ioManager.ZipToDirectory(installFullPath, versionZipStream, cancellationToken); + logger.LogTrace("Extracting engine to {extractPath}...", installFullPath); + await engineInstallationData.ExtractToPath(installFullPath, cancellationToken); } if (progressReporter != null) progressReporter.StageName = "Running installation actions"; - await byondInstaller.InstallByond(version, installFullPath, cancellationToken); + await engineInstaller.Install(version, installFullPath, cancellationToken); if (progressReporter != null) progressReporter.StageName = "Writing version file"; @@ -571,7 +566,7 @@ await ioManager.WriteAllBytes( catch (HttpRequestException ex) { // since the user can easily provide non-exitent version numbers, we'll turn this into a JobException - throw new JobException(ErrorCode.ByondDownloadFail, ex); + throw new JobException(ErrorCode.EngineDownloadFail, ex); } catch (OperationCanceledException) { @@ -585,81 +580,22 @@ await ioManager.WriteAllBytes( } /// - /// Create and add a new to . + /// Create and add a new to . /// /// The being added. + /// The path to the installation. /// The representing the installation process. - /// The new containing the new . - ReferenceCountingContainer AddInstallationContainer(Version version, Task installationTask) + /// The new . + ReferenceCountingContainer AddInstallationContainer(EngineVersion version, string installPath, Task installationTask) { - var binPathForVersion = ioManager.ConcatPath(version.ToString(), BinPath); - var installation = new ByondInstallation( - installationTask, - version, - ioManager.ResolvePath( - ioManager.ConcatPath( - binPathForVersion, - byondInstaller.GetDreamDaemonName( - version, - out var supportsCli, - out var supportsMapThreads))), - ioManager.ResolvePath( - ioManager.ConcatPath( - binPathForVersion, - byondInstaller.DreamMakerName)), - supportsCli, - supportsMapThreads); - - var installationContainer = new ReferenceCountingContainer(installation); + var installation = engineInstaller.CreateInstallation(version, installPath, installationTask); + + var installationContainer = new ReferenceCountingContainer(installation); lock (installedVersions) installedVersions.Add(version, installationContainer); return installationContainer; } - - /// - /// Add a given to the trusted DMBs list in BYOND's config. - /// - /// Full path to the .dmb that should be trusted. - /// The for the operation. - /// A representing the running operation. - async ValueTask 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( - byondDir, - CfgDirectoryName, - TrustedDmbFileName); - - logger.LogDebug("Adding .dmb ({dmbPath}) to {trustedFilePath}", fullDmbPath, trustedFilePath); - - using (await SemaphoreSlimContext.Lock(UserFilesSemaphore, cancellationToken)) - { - string trustedFileText; - if (await ioManager.FileExists(trustedFilePath, cancellationToken)) - { - var trustedFileBytes = await ioManager.ReadAllBytes(trustedFilePath, cancellationToken); - trustedFileText = Encoding.UTF8.GetString(trustedFileBytes); - trustedFileText = $"{trustedFileText.Trim()}{Environment.NewLine}"; - } - else - trustedFileText = String.Empty; - - if (trustedFileText.Contains(fullDmbPath, StringComparison.Ordinal)) - return; - - trustedFileText = $"{trustedFileText}{fullDmbPath}{Environment.NewLine}"; - - var newTrustedFileBytes = Encoding.UTF8.GetBytes(trustedFileText); - await ioManager.WriteAllBytes(trustedFilePath, newTrustedFileBytes, cancellationToken); - } - } } } diff --git a/src/Tgstation.Server.Host/Components/Byond/IByondExecutableLock.cs b/src/Tgstation.Server.Host/Components/Engine/IEngineExecutableLock.cs similarity index 67% rename from src/Tgstation.Server.Host/Components/Byond/IByondExecutableLock.cs rename to src/Tgstation.Server.Host/Components/Engine/IEngineExecutableLock.cs index a320109dce0..66031902fbb 100644 --- a/src/Tgstation.Server.Host/Components/Byond/IByondExecutableLock.cs +++ b/src/Tgstation.Server.Host/Components/Engine/IEngineExecutableLock.cs @@ -1,11 +1,11 @@ using System; -namespace Tgstation.Server.Host.Components.Byond +namespace Tgstation.Server.Host.Components.Engine { /// /// Represents usage of the two primary BYOND server executables. /// - public interface IByondExecutableLock : IByondInstallation, IDisposable + public interface IEngineExecutableLock : IEngineInstallation, IDisposable { /// /// Call if, during a detach, this version should not be deleted. diff --git a/src/Tgstation.Server.Host/Components/Engine/IEngineInstallation.cs b/src/Tgstation.Server.Host/Components/Engine/IEngineInstallation.cs new file mode 100644 index 00000000000..ff2e4155f34 --- /dev/null +++ b/src/Tgstation.Server.Host/Components/Engine/IEngineInstallation.cs @@ -0,0 +1,86 @@ +using System.Collections.Generic; +using System.Threading; +using System.Threading.Tasks; + +using Microsoft.Extensions.Logging; + +using Tgstation.Server.Api.Models; +using Tgstation.Server.Api.Models.Internal; +using Tgstation.Server.Host.Components.Deployment; +using Tgstation.Server.Host.System; + +namespace Tgstation.Server.Host.Components.Engine +{ + /// + /// Represents a BYOND installation. + /// + public interface IEngineInstallation + { + /// + /// The of the . + /// + EngineVersion Version { get; } + + /// + /// The full path to the game server executable. + /// + string ServerExePath { get; } + + /// + /// The full path to the dm/DreamMaker executable. + /// + string CompilerExePath { get; } + + /// + /// If supports being run as a command-line application and outputs log information to be captured. + /// + bool HasStandardOutput { get; } + + /// + /// If may create network prompts. + /// + bool PromptsForNetworkAccess { get; } + + /// + /// If is set, this indicates that the engine server has good file logging that should be preferred to ours. + /// + bool PreferFileLogging { get; } + + /// + /// The that completes when the BYOND version finished installing. + /// + Task InstallationTask { get; } + + /// + /// Return the command line arguments for launching with given . + /// + /// The . + /// The map of parameter s as a . MUST include . Should NOT include the of . + /// The . + /// The full path to the log file, if any. + /// The formatted arguments . + string FormatServerArguments( + IDmbProvider dmbProvider, + IReadOnlyDictionary parameters, + DreamDaemonLaunchParameters launchParameters, + string? logFilePath); + + /// + /// Return the command line arguments for compiling a given if compilation is necessary. + /// + /// The full path to the .dme to compile. + /// The formatted arguments . + string FormatCompilerArguments(string dmePath); + + /// + /// Kills a given engine server . + /// + /// The to write to. + /// The to be terminated. + /// The of the session. + /// The port the server is running on. + /// The for the operation. + /// A representing the running operation. + ValueTask StopServerProcess(ILogger logger, IProcess process, string accessIdentifier, ushort port, CancellationToken cancellationToken); + } +} diff --git a/src/Tgstation.Server.Host/Components/Engine/IEngineInstallationData.cs b/src/Tgstation.Server.Host/Components/Engine/IEngineInstallationData.cs new file mode 100644 index 00000000000..b3d8458973c --- /dev/null +++ b/src/Tgstation.Server.Host/Components/Engine/IEngineInstallationData.cs @@ -0,0 +1,20 @@ +using System; +using System.Threading; +using System.Threading.Tasks; + +namespace Tgstation.Server.Host.Components.Engine +{ + /// + /// Wraps data containing an engine installation. + /// + interface IEngineInstallationData : IAsyncDisposable + { + /// + /// Extracts the installation to a given path. + /// + /// The full path to extract to. + /// The for the operation. + /// A representing the running operation. + Task ExtractToPath(string path, CancellationToken cancellationToken); + } +} diff --git a/src/Tgstation.Server.Host/Components/Engine/IEngineInstaller.cs b/src/Tgstation.Server.Host/Components/Engine/IEngineInstaller.cs new file mode 100644 index 00000000000..7169ffe4b57 --- /dev/null +++ b/src/Tgstation.Server.Host/Components/Engine/IEngineInstaller.cs @@ -0,0 +1,66 @@ +using System.Threading; +using System.Threading.Tasks; + +using Tgstation.Server.Api.Models; +using Tgstation.Server.Host.Jobs; + +namespace Tgstation.Server.Host.Components.Engine +{ + /// + /// For downloading and installing game engines for a given system. + /// + interface IEngineInstaller + { + /// + /// Creates an for a given . + /// + /// The of the installation. + /// The path to the installation. + /// The representing the installation process for the installation. + /// The . + IEngineInstallation CreateInstallation(EngineVersion version, string path, Task installationTask); + + /// + /// Download a given engine . + /// + /// The of the engine to download. + /// The optional for the operation. + /// The for the operation. + /// A resulting in the for the download. + ValueTask DownloadVersion(EngineVersion version, JobProgressReporter? jobProgressReporter, CancellationToken cancellationToken); + + /// + /// Does actions necessary to get an extracted installation working. + /// + /// The being installed. + /// The path to the installation. + /// The for the operation. + /// A representing the running operation. + ValueTask Install(EngineVersion version, string path, CancellationToken cancellationToken); + + /// + /// Does actions necessary to get upgrade a version installed by a previous version of TGS. + /// + /// The being installed. + /// The path to the installation. + /// The for the operation. + /// A representing the running operation. + ValueTask UpgradeInstallation(EngineVersion version, string path, CancellationToken cancellationToken); + + /// + /// Add a given to the trusted DMBs list in BYOND's config. + /// + /// The being used. + /// Full path to the .dmb that should be trusted. + /// The for the operation. + /// A representing the running operation. + ValueTask TrustDmbPath(EngineVersion version, string fullDmbPath, CancellationToken cancellationToken); + + /// + /// Attempts to cleans the engine's cache folder for the system. + /// + /// The for the operation. + /// A representing the running operation. + Task CleanCache(CancellationToken cancellationToken); + } +} diff --git a/src/Tgstation.Server.Host/Components/Byond/IByondManager.cs b/src/Tgstation.Server.Host/Components/Engine/IEngineManager.cs similarity index 53% rename from src/Tgstation.Server.Host/Components/Byond/IByondManager.cs rename to src/Tgstation.Server.Host/Components/Engine/IEngineManager.cs index 092034b1c0b..18af1469e6d 100644 --- a/src/Tgstation.Server.Host/Components/Byond/IByondManager.cs +++ b/src/Tgstation.Server.Host/Components/Engine/IEngineManager.cs @@ -4,56 +4,62 @@ using System.Threading; using System.Threading.Tasks; +using Tgstation.Server.Api.Models; using Tgstation.Server.Host.Jobs; -namespace Tgstation.Server.Host.Components.Byond +namespace Tgstation.Server.Host.Components.Engine { /// - /// For managing the BYOND installation. + /// For managing the engine installations. /// - /// When passing in s, ensure they are BYOND format versions unless referring to a custom version. This means should NEVER be 0. - public interface IByondManager : IComponentService, IDisposable + /// When passing in s for , ensure they are BYOND format versions unless referring to a custom version. This means should NEVER be 0. + public interface IEngineManager : IComponentService, IDisposable { /// - /// The currently active BYOND version. + /// The currently active . /// - Version ActiveVersion { get; } + EngineVersion? ActiveVersion { get; } /// - /// The installed BYOND versions. + /// The installed s. /// - IReadOnlyList InstalledVersions { get; } + IReadOnlyList InstalledVersions { get; } /// - /// Change the active BYOND version. + /// Change the active . /// /// The optional for the operation. - /// The new . + /// The new . /// Optional of a custom BYOND version zip file. /// If an installation should be performed if the is not installed. If and an installation is required an will be thrown. /// The for the operation. /// A representing the running operation. - ValueTask ChangeVersion(JobProgressReporter progressReporter, Version version, Stream customVersionStream, bool allowInstallation, CancellationToken cancellationToken); + ValueTask ChangeVersion( + JobProgressReporter? progressReporter, + EngineVersion version, + Stream? customVersionStream, + bool allowInstallation, + CancellationToken cancellationToken); /// - /// Deletes a given BYOND version from the disk. + /// Deletes a given from the disk. /// /// The for the operation. - /// The to delete. + /// The to delete. /// The for the operation. - /// A representing the running operation. - ValueTask DeleteVersion(JobProgressReporter progressReporter, Version version, CancellationToken cancellationToken); + /// A representing the running operation. + ValueTask DeleteVersion(JobProgressReporter progressReporter, EngineVersion version, CancellationToken cancellationToken); /// - /// Lock the current installation's location and return a . + /// Lock the current installation's location and return a . /// - /// The BYOND required. + /// The required. /// The optional full path to .dmb to trust while using the executables. /// The for the operation. - /// A resulting in the requested . - ValueTask UseExecutables( - Version requiredVersion, - string trustDmbFullPath, + /// A resulting in the requested . + ValueTask UseExecutables( + EngineVersion? requiredVersion, + string? trustDmbFullPath, CancellationToken cancellationToken); } } diff --git a/src/Tgstation.Server.Host/Components/Engine/OpenDreamInstallation.cs b/src/Tgstation.Server.Host/Components/Engine/OpenDreamInstallation.cs new file mode 100644 index 00000000000..ab9eb923d84 --- /dev/null +++ b/src/Tgstation.Server.Host/Components/Engine/OpenDreamInstallation.cs @@ -0,0 +1,183 @@ +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.Net.Http; +using System.Net.Http.Headers; +using System.Net.Mime; +using System.Text; +using System.Threading; +using System.Threading.Tasks; + +using Microsoft.Extensions.Logging; + +using Tgstation.Server.Api.Models; +using Tgstation.Server.Api.Models.Internal; +using Tgstation.Server.Common.Http; +using Tgstation.Server.Host.Components.Deployment; +using Tgstation.Server.Host.Components.Interop; +using Tgstation.Server.Host.IO; +using Tgstation.Server.Host.System; +using Tgstation.Server.Host.Utils; + +namespace Tgstation.Server.Host.Components.Engine +{ + /// + /// Implementation of for . + /// + sealed class OpenDreamInstallation : EngineInstallationBase + { + /// + public override EngineVersion Version { get; } + + /// + public override string ServerExePath { get; } + + /// + public override string CompilerExePath { get; } + + /// + public override bool PromptsForNetworkAccess => false; + + /// + public override bool HasStandardOutput => true; + + /// + public override bool PreferFileLogging => true; + + /// + public override Task InstallationTask { get; } + + /// + /// The for the . + /// + readonly IIOManager ioManager; + + /// + /// The for the . + /// + readonly IAsyncDelayer asyncDelayer; + + /// + /// The for the . + /// + readonly IAbstractHttpClientFactory httpClientFactory; + + /// + /// Initializes a new instance of the class. + /// + /// The value of . + /// The value of . + /// The value of . + /// The value of . + /// The value of . + /// The value of . + /// The value of . + public OpenDreamInstallation( + IIOManager ioManager, + IAsyncDelayer asyncDelayer, + IAbstractHttpClientFactory httpClientFactory, + string serverExePath, + string compilerExePath, + Task installationTask, + EngineVersion version) + { + this.ioManager = ioManager ?? throw new ArgumentNullException(nameof(ioManager)); + this.asyncDelayer = asyncDelayer ?? throw new ArgumentNullException(nameof(asyncDelayer)); + this.httpClientFactory = httpClientFactory ?? throw new ArgumentNullException(nameof(httpClientFactory)); + ServerExePath = serverExePath ?? throw new ArgumentNullException(nameof(serverExePath)); + CompilerExePath = compilerExePath ?? throw new ArgumentNullException(nameof(compilerExePath)); + InstallationTask = installationTask ?? throw new ArgumentNullException(nameof(installationTask)); + Version = version ?? throw new ArgumentNullException(nameof(version)); + + if (version.Engine!.Value != EngineType.OpenDream) + throw new ArgumentException($"Invalid EngineType: {version.Engine.Value}", nameof(version)); + } + + /// + public override string FormatServerArguments( + IDmbProvider dmbProvider, + IReadOnlyDictionary parameters, + DreamDaemonLaunchParameters launchParameters, + string? logFilePath) + { + ArgumentNullException.ThrowIfNull(dmbProvider); + ArgumentNullException.ThrowIfNull(parameters); + ArgumentNullException.ThrowIfNull(launchParameters); + + if (!parameters.TryGetValue(DMApiConstants.ParamAccessIdentifier, out var accessIdentifier)) + throw new ArgumentException($"parameters must have \"{DMApiConstants.ParamAccessIdentifier}\" set!", nameof(parameters)); + + var parametersString = EncodeParameters(parameters, launchParameters); + + var arguments = $"--cvar {(logFilePath != null ? $"log.path=\"{ioManager.GetDirectoryName(logFilePath)}\" --cvar log.format=\"{ioManager.GetFileName(logFilePath)}\"" : "log.enabled=false")} --cvar watchdog.token={accessIdentifier} --cvar log.runtimelog=false --cvar net.port={launchParameters.Port!.Value} --cvar opendream.topic_port=0 --cvar opendream.world_params=\"{parametersString}\" --cvar opendream.json_path=\"./{dmbProvider.DmbName}\""; + return arguments; + } + + /// + public override string FormatCompilerArguments(string dmePath) + => $"--suppress-unimplemented --notices-enabled \"{dmePath ?? throw new ArgumentNullException(nameof(dmePath))}\""; + + /// + public override async ValueTask StopServerProcess( + ILogger logger, + IProcess process, + string accessIdentifier, + ushort port, + CancellationToken cancellationToken) + { + const int MaximumTerminationSeconds = 5; + + logger.LogTrace("Attempting Robust.Server graceful exit (Timeout: {seconds}s)...", MaximumTerminationSeconds); + var timeout = asyncDelayer.Delay(TimeSpan.FromSeconds(MaximumTerminationSeconds), cancellationToken); + var lifetime = process.Lifetime; + + var stopwatch = Stopwatch.StartNew(); + try + { + using var httpClient = httpClientFactory.CreateClient(); + using var request = new HttpRequestMessage(); + request.Headers.Add("WatchdogToken", accessIdentifier); + request.RequestUri = new Uri($"http://localhost:{port}/shutdown"); + request.Content = new StringContent( + "{\"Reason\":\"TGS session termination\"}", + Encoding.UTF8, + new MediaTypeHeaderValue(MediaTypeNames.Application.Json)); + request.Method = HttpMethod.Post; + + var responseTask = httpClient.SendAsync(request, HttpCompletionOption.ResponseHeadersRead, cancellationToken); + try + { + await Task.WhenAny(timeout, lifetime, responseTask); + if (responseTask.IsCompleted) + { + using var response = await responseTask; + if (response.IsSuccessStatusCode) + { + logger.LogDebug("Robust.Server responded to the shutdown command successfully ({requestMs}ms). Waiting for exit...", stopwatch.ElapsedMilliseconds); + await Task.WhenAny(timeout, lifetime); + } + } + + if (!lifetime.IsCompleted) + logger.LogWarning("Robust.Server graceful exit timed out!"); + } + catch (Exception ex) when (ex is not OperationCanceledException) + { + logger.LogDebug(ex, "Unable to send graceful exit request to Robust.Server watchdog API!"); + } + + if (lifetime.IsCompleted) + { + logger.LogTrace("Robust.Server exited without termination"); + return; + } + } + finally + { + logger.LogTrace("Robust.Server graceful shutdown attempt took {totalMs}ms", stopwatch.ElapsedMilliseconds); + } + + await base.StopServerProcess(logger, process, accessIdentifier, port, cancellationToken); + } + } +} diff --git a/src/Tgstation.Server.Host/Components/Engine/OpenDreamInstaller.cs b/src/Tgstation.Server.Host/Components/Engine/OpenDreamInstaller.cs new file mode 100644 index 00000000000..a2b74e14457 --- /dev/null +++ b/src/Tgstation.Server.Host/Components/Engine/OpenDreamInstaller.cs @@ -0,0 +1,382 @@ +using System; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; + +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Options; + +using Tgstation.Server.Api.Models; +using Tgstation.Server.Common.Extensions; +using Tgstation.Server.Common.Http; +using Tgstation.Server.Host.Common; +using Tgstation.Server.Host.Components.Repository; +using Tgstation.Server.Host.Configuration; +using Tgstation.Server.Host.IO; +using Tgstation.Server.Host.Jobs; +using Tgstation.Server.Host.System; +using Tgstation.Server.Host.Utils; + +namespace Tgstation.Server.Host.Components.Engine +{ + /// + /// Implementation of for . + /// + class OpenDreamInstaller : EngineInstallerBase + { + /// + /// The bin directory name. + /// + const string BinDir = "bin"; + + /// + /// The OD server directory name. + /// + const string ServerDir = "server"; + + /// + /// The name of the subdirectory in an installation's used to store the compiler binaries. + /// + const string InstallationCompilerDirectory = "compiler"; + + /// + /// The name of the subdirectory used for the 's copy. + /// + const string InstallationSourceSubDirectory = "TgsSourceSubdir"; + + /// + protected override EngineType TargetEngineType => EngineType.OpenDream; + + /// + /// The for the . + /// + protected IProcessExecutor ProcessExecutor { get; } + + /// + /// The for the . + /// + protected GeneralConfiguration GeneralConfiguration { get; } + + /// + /// The for the . + /// + protected SessionConfiguration SessionConfiguration { get; } + + /// + /// The for the . + /// + readonly IPlatformIdentifier platformIdentifier; + + /// + /// The for the OpenDream repository. + /// + readonly IRepositoryManager repositoryManager; + + /// + /// The for the . + /// + readonly IAsyncDelayer asyncDelayer; + + /// + /// The for the . + /// + readonly IAbstractHttpClientFactory httpClientFactory; + + /// + /// Initializes a new instance of the class. + /// + /// The for the . + /// The for the . + /// The value of . + /// The value of . + /// The value of . + /// The value of . + /// The value of . + /// The containing value of . + /// The containing value of . + public OpenDreamInstaller( + IIOManager ioManager, + ILogger logger, + IPlatformIdentifier platformIdentifier, + IProcessExecutor processExecutor, + IRepositoryManager repositoryManager, + IAsyncDelayer asyncDelayer, + IAbstractHttpClientFactory httpClientFactory, + IOptions generalConfigurationOptions, + IOptions sessionConfigurationOptions) + : base(ioManager, logger) + { + this.platformIdentifier = platformIdentifier ?? throw new ArgumentNullException(nameof(platformIdentifier)); + ProcessExecutor = processExecutor ?? throw new ArgumentNullException(nameof(processExecutor)); + this.repositoryManager = repositoryManager ?? throw new ArgumentNullException(nameof(repositoryManager)); + this.asyncDelayer = asyncDelayer ?? throw new ArgumentNullException(nameof(asyncDelayer)); + this.httpClientFactory = httpClientFactory ?? throw new ArgumentNullException(nameof(httpClientFactory)); + GeneralConfiguration = generalConfigurationOptions?.Value ?? throw new ArgumentNullException(nameof(generalConfigurationOptions)); + SessionConfiguration = sessionConfigurationOptions?.Value ?? throw new ArgumentNullException(nameof(sessionConfigurationOptions)); + } + + /// + public override Task CleanCache(CancellationToken cancellationToken) => Task.CompletedTask; + + /// + public override IEngineInstallation CreateInstallation(EngineVersion version, string path, Task installationTask) + { + CheckVersionValidity(version); + GetExecutablePaths(path, out var serverExePath, out var compilerExePath); + return new OpenDreamInstallation( + IOManager, + asyncDelayer, + httpClientFactory, + serverExePath, + compilerExePath, + installationTask, + version); + } + + /// + public override async ValueTask DownloadVersion(EngineVersion version, JobProgressReporter? jobProgressReporter, CancellationToken cancellationToken) + { + CheckVersionValidity(version); + + // get a lock on a system wide OD repo + Logger.LogTrace("Cloning OD repo..."); + + var progressSection1 = jobProgressReporter?.CreateSection("Updating OpenDream git repository", 0.5f); + + var repo = await repositoryManager.CloneRepository( + GeneralConfiguration.OpenDreamGitUrl, + null, + null, + null, + progressSection1, + true, + cancellationToken); + + try + { + if (repo == null) + { + Logger.LogTrace("OD repo seems to already exist, attempting load and fetch..."); + repo = await repositoryManager.LoadRepository(cancellationToken); + + await repo!.FetchOrigin( + progressSection1, + null, + null, + false, + cancellationToken); + } + + var progressSection2 = jobProgressReporter?.CreateSection("Checking out OpenDream version", 0.5f); + + var committish = version.SourceSHA + ?? $"{GeneralConfiguration.OpenDreamGitTagPrefix}{version.Version!.Semver()}"; + + await repo.CheckoutObject( + committish, + null, + null, + true, + progressSection2, + cancellationToken); + + if (!await repo.CommittishIsParent("tgs-min-compat", cancellationToken)) + throw new JobException(ErrorCode.OpenDreamTooOld); + + return new RepositoryEngineInstallationData(IOManager, repo, InstallationSourceSubDirectory); + } + catch + { + repo?.Dispose(); + throw; + } + } + + /// + public override async ValueTask Install(EngineVersion version, string installPath, CancellationToken cancellationToken) + { + CheckVersionValidity(version); + ArgumentNullException.ThrowIfNull(installPath); + var sourcePath = IOManager.ConcatPath(installPath, InstallationSourceSubDirectory); + + if (!await IOManager.DirectoryExists(sourcePath, cancellationToken)) + { + // a zip install that didn't come from us? + // we want to use the bin dir, so put everything where we expect + Logger.LogDebug("Correcting extraction location..."); + var dirsTask = IOManager.GetDirectories(installPath, cancellationToken); + var filesTask = IOManager.GetFiles(installPath, cancellationToken); + var dirCreateTask = IOManager.CreateDirectory(sourcePath, cancellationToken); + + await Task.WhenAll(dirsTask, filesTask, dirCreateTask); + + var dirsMoveTasks = dirsTask + .Result + .Select( + dirPath => IOManager.MoveDirectory( + dirPath, + IOManager.ConcatPath( + sourcePath, + IOManager.GetFileName(dirPath)), + cancellationToken)); + var filesMoveTask = filesTask + .Result + .Select( + filePath => IOManager.MoveFile( + filePath, + IOManager.ConcatPath( + sourcePath, + IOManager.GetFileName(filePath)), + cancellationToken)); + + await Task.WhenAll(dirsMoveTasks.Concat(filesMoveTask)); + } + + var dotnetPaths = DotnetHelper.GetPotentialDotnetPaths(platformIdentifier.IsWindows) + .ToList(); + var tasks = dotnetPaths + .Select(path => IOManager.FileExists(path, cancellationToken)) + .ToList(); + + await Task.WhenAll(tasks); + + var selectedPathIndex = tasks.FindIndex(pathValidTask => pathValidTask.Result); + + if (selectedPathIndex == -1) + throw new JobException(ErrorCode.OpenDreamCantFindDotnet); + + var dotnetPath = dotnetPaths[selectedPathIndex]; + + const string DeployDir = "tgs_deploy"; + int? buildExitCode = null; + await HandleExtremelyLongPathOperation( + async shortenedPath => + { + var shortenedDeployPath = IOManager.ConcatPath(shortenedPath, DeployDir); + await using var buildProcess = ProcessExecutor.LaunchProcess( + dotnetPath, + shortenedPath, + $"run -c Release --project OpenDreamPackageTool -- --tgs -o {shortenedDeployPath}", + null, + !GeneralConfiguration.OpenDreamSuppressInstallOutput, + !GeneralConfiguration.OpenDreamSuppressInstallOutput); + + if (SessionConfiguration.LowPriorityDeploymentProcesses) + buildProcess.AdjustPriority(false); + + using (cancellationToken.Register(() => buildProcess.Terminate())) + buildExitCode = await buildProcess.Lifetime; + + string? output; + if (!GeneralConfiguration.OpenDreamSuppressInstallOutput) + { + var buildOutputTask = buildProcess.GetCombinedOutput(cancellationToken); + if (!buildOutputTask.IsCompleted) + Logger.LogTrace("OD build complete, waiting for output..."); + output = await buildOutputTask; + } + else + output = ""; + + Logger.LogDebug( + "OpenDream build exited with code {exitCode}:{newLine}{output}", + buildExitCode, + Environment.NewLine, + output); + }, + sourcePath, + cancellationToken); + + if (buildExitCode != 0) + throw new JobException("OpenDream build failed!"); + + var deployPath = IOManager.ConcatPath(sourcePath, DeployDir); + async ValueTask MoveDirs() + { + var dirs = await IOManager.GetDirectories(deployPath, cancellationToken); + await Task.WhenAll( + dirs.Select( + dir => IOManager.MoveDirectory( + dir, + IOManager.ConcatPath( + installPath, + IOManager.GetFileName(dir)), + cancellationToken))); + } + + async ValueTask MoveFiles() + { + var files = await IOManager.GetFiles(deployPath, cancellationToken); + await Task.WhenAll( + files.Select( + file => IOManager.MoveFile( + file, + IOManager.ConcatPath( + installPath, + IOManager.GetFileName(file)), + cancellationToken))); + } + + var dirsMoveTask = MoveDirs(); + var outputFilesMoveTask = MoveFiles(); + await ValueTaskExtensions.WhenAll(dirsMoveTask, outputFilesMoveTask); + await IOManager.DeleteDirectory(sourcePath, cancellationToken); + } + + /// + public override ValueTask UpgradeInstallation(EngineVersion version, string path, CancellationToken cancellationToken) + { + CheckVersionValidity(version); + ArgumentNullException.ThrowIfNull(path); + return ValueTask.CompletedTask; + } + + /// + public override ValueTask TrustDmbPath(EngineVersion engineVersion, string fullDmbPath, CancellationToken cancellationToken) + { + ArgumentNullException.ThrowIfNull(engineVersion); + ArgumentNullException.ThrowIfNull(fullDmbPath); + + Logger.LogTrace("TrustDmbPath is a no-op: {path}", fullDmbPath); + return ValueTask.CompletedTask; + } + + /// + /// Perform an operation on a very long path. + /// + /// A taking a shortened path and resulting in a representing the running operation. + /// The original path to the directory. + /// The for the operation. + /// A representing the running operation. + protected virtual ValueTask HandleExtremelyLongPathOperation( + Func shortenedPathOperation, + string originalPath, + CancellationToken cancellationToken) + => shortenedPathOperation(originalPath); // based god linux has no such weakness + + /// + /// Gets the paths to the server and client executables. + /// + /// The path to the OpenDream installation. + /// The path to the OpenDreamServer executable. + /// The path to the DMCompiler executable. + protected void GetExecutablePaths(string installationPath, out string serverExePath, out string compilerExePath) + { + var exeExtension = platformIdentifier.IsWindows + ? ".exe" + : String.Empty; + + serverExePath = IOManager.ConcatPath( + installationPath, + BinDir, + ServerDir, + $"Robust.Server{exeExtension}"); + + compilerExePath = IOManager.ConcatPath( + installationPath, + BinDir, + InstallationCompilerDirectory, + $"DMCompiler{exeExtension}"); + } + } +} diff --git a/src/Tgstation.Server.Host/Components/Byond/PosixByondInstaller.cs b/src/Tgstation.Server.Host/Components/Engine/PosixByondInstaller.cs similarity index 77% rename from src/Tgstation.Server.Host/Components/Byond/PosixByondInstaller.cs rename to src/Tgstation.Server.Host/Components/Engine/PosixByondInstaller.cs index 3657e015d47..037cacd437c 100644 --- a/src/Tgstation.Server.Host/Components/Byond/PosixByondInstaller.cs +++ b/src/Tgstation.Server.Host/Components/Engine/PosixByondInstaller.cs @@ -6,13 +6,14 @@ using Microsoft.Extensions.Logging; +using Tgstation.Server.Api.Models; using Tgstation.Server.Common.Extensions; using Tgstation.Server.Host.IO; -namespace Tgstation.Server.Host.Components.Byond +namespace Tgstation.Server.Host.Components.Engine { /// - /// for Posix systems. + /// for Posix systems. /// sealed class PosixByondInstaller : ByondInstallerBase { @@ -32,10 +33,10 @@ sealed class PosixByondInstaller : ByondInstallerBase const string ShellScriptExtension = ".sh"; /// - public override string DreamMakerName => DreamMakerExecutableName + ShellScriptExtension; + protected override string PathToUserFolder { get; } /// - public override string PathToUserByondFolder { get; } + protected override string DreamMakerName => DreamMakerExecutableName + ShellScriptExtension; /// protected override string ByondRevisionsUrlTemplate => "https://www.byond.com/download/build/{0}/{0}.{1}_byond_linux.zip"; @@ -57,31 +58,22 @@ public PosixByondInstaller( IIOManager ioManager, IFileDownloader fileDownloader, ILogger logger) - : base(ioManager, fileDownloader, logger) + : base(ioManager, logger, fileDownloader) { this.postWriteHandler = postWriteHandler ?? throw new ArgumentNullException(nameof(postWriteHandler)); - PathToUserByondFolder = IOManager.ResolvePath( + PathToUserFolder = IOManager.ResolvePath( IOManager.ConcatPath( Environment.GetFolderPath( - Environment.SpecialFolder.UserProfile), + Environment.SpecialFolder.UserProfile, + Environment.SpecialFolderOption.DoNotVerify), "./byond/cache")); } /// - public override string GetDreamDaemonName(Version version, out bool supportsCli, out bool supportsMapThreads) + public override ValueTask Install(EngineVersion version, string path, CancellationToken cancellationToken) { - ArgumentNullException.ThrowIfNull(version); - - supportsCli = true; - supportsMapThreads = version >= MapThreadsVersion; - return DreamDaemonExecutableName + ShellScriptExtension; - } - - /// - public override ValueTask InstallByond(Version version, string path, CancellationToken cancellationToken) - { - ArgumentNullException.ThrowIfNull(version); + CheckVersionValidity(version); ArgumentNullException.ThrowIfNull(path); // write the scripts for running the ting @@ -98,10 +90,10 @@ async ValueTask WriteAndMakeExecutable(string pathToScript, string script) postWriteHandler.HandleWrite(IOManager.ResolvePath(pathToScript)); } - var basePath = IOManager.ConcatPath(path, ByondManager.BinPath); + var basePath = IOManager.ConcatPath(path, ByondBinPath); var ddTask = WriteAndMakeExecutable( - IOManager.ConcatPath(basePath, GetDreamDaemonName(version, out _, out _)), + IOManager.ConcatPath(basePath, GetDreamDaemonName(version.Version!, out _)), dreamDaemonScript); var dmTask = WriteAndMakeExecutable( @@ -119,12 +111,19 @@ async ValueTask WriteAndMakeExecutable(string pathToScript, string script) } /// - public override ValueTask UpgradeInstallation(Version version, string path, CancellationToken cancellationToken) + public override ValueTask UpgradeInstallation(EngineVersion version, string path, CancellationToken cancellationToken) { - ArgumentNullException.ThrowIfNull(version); + CheckVersionValidity(version); ArgumentNullException.ThrowIfNull(path); return ValueTask.CompletedTask; } + + /// + protected override string GetDreamDaemonName(Version byondVersion, out bool supportsCli) + { + supportsCli = true; + return DreamDaemonExecutableName + ShellScriptExtension; + } } } diff --git a/src/Tgstation.Server.Host/Components/Engine/RepositoryEngineInstallationData.cs b/src/Tgstation.Server.Host/Components/Engine/RepositoryEngineInstallationData.cs new file mode 100644 index 00000000000..88fbb779117 --- /dev/null +++ b/src/Tgstation.Server.Host/Components/Engine/RepositoryEngineInstallationData.cs @@ -0,0 +1,59 @@ +using System; +using System.Threading; +using System.Threading.Tasks; + +using Tgstation.Server.Host.Components.Repository; +using Tgstation.Server.Host.IO; + +namespace Tgstation.Server.Host.Components.Engine +{ + /// + /// Implementation of using a . + /// + sealed class RepositoryEngineInstallationData : IEngineInstallationData + { + /// + /// The for the . + /// + readonly IIOManager ioManager; + + /// + /// The backing . + /// + readonly IRepository repository; + + /// + /// The name of the subdirectory the is copied to. + /// + readonly string targetSubDirectory; + + /// + /// Initializes a new instance of the class. + /// + /// The value of . + /// The value of . + /// The value of . + public RepositoryEngineInstallationData(IIOManager ioManager, IRepository repository, string targetSubDirectory) + { + this.ioManager = ioManager ?? throw new ArgumentNullException(nameof(ioManager)); + this.repository = repository ?? throw new ArgumentNullException(nameof(repository)); + this.targetSubDirectory = targetSubDirectory ?? throw new ArgumentNullException(nameof(targetSubDirectory)); + } + + /// + public ValueTask DisposeAsync() + { + repository.Dispose(); + return ValueTask.CompletedTask; + } + + /// + public Task ExtractToPath(string path, CancellationToken cancellationToken) + => repository.CopyTo( + ioManager.ConcatPath( + path, + targetSubDirectory), + cancellationToken) + .AsTask(); + } +} diff --git a/src/Tgstation.Server.Host/Components/Byond/WindowsByondInstaller.cs b/src/Tgstation.Server.Host/Components/Engine/WindowsByondInstaller.cs similarity index 72% rename from src/Tgstation.Server.Host/Components/Byond/WindowsByondInstaller.cs rename to src/Tgstation.Server.Host/Components/Engine/WindowsByondInstaller.cs index 8080abc12ff..87424718d5d 100644 --- a/src/Tgstation.Server.Host/Components/Byond/WindowsByondInstaller.cs +++ b/src/Tgstation.Server.Host/Components/Engine/WindowsByondInstaller.cs @@ -15,10 +15,10 @@ using Tgstation.Server.Host.System; using Tgstation.Server.Host.Utils; -namespace Tgstation.Server.Host.Components.Byond +namespace Tgstation.Server.Host.Components.Engine { /// - /// for windows systems. + /// for windows systems. /// sealed class WindowsByondInstaller : ByondInstallerBase, IDisposable { @@ -50,13 +50,13 @@ sealed class WindowsByondInstaller : ByondInstallerBase, IDisposable /// /// The first version of BYOND to ship with dd.exe on the Windows build. /// - public static Version DDExeVersion => new (515, 1598); + public static Version DDExeVersion => new(515, 1598); /// - public override string DreamMakerName => "dm.exe"; + protected override string DreamMakerName => "dm.exe"; /// - public override string PathToUserByondFolder { get; } + protected override string PathToUserFolder { get; } /// protected override string ByondRevisionsUrlTemplate => "https://www.byond.com/download/build/{0}/{0}.{1}_byond.zip"; @@ -71,6 +71,11 @@ sealed class WindowsByondInstaller : ByondInstallerBase, IDisposable /// readonly GeneralConfiguration generalConfiguration; + /// + /// The for the . + /// + readonly SessionConfiguration sessionConfiguration; + /// /// The for the . /// @@ -86,6 +91,7 @@ sealed class WindowsByondInstaller : ByondInstallerBase, IDisposable /// /// The value of . /// The containing the value of . + /// The containing the value of . /// The for the . /// The for the . /// The for the . @@ -94,17 +100,20 @@ public WindowsByondInstaller( IIOManager ioManager, IFileDownloader fileDownloader, IOptions generalConfigurationOptions, + IOptions sessionConfigurationOptions, ILogger logger) - : base(ioManager, fileDownloader, logger) + : base(ioManager, logger, fileDownloader) { this.processExecutor = processExecutor ?? throw new ArgumentNullException(nameof(processExecutor)); generalConfiguration = generalConfigurationOptions?.Value ?? throw new ArgumentNullException(nameof(generalConfigurationOptions)); + sessionConfiguration = sessionConfigurationOptions?.Value ?? throw new ArgumentNullException(nameof(sessionConfigurationOptions)); + + var documentsDirectory = Environment.GetFolderPath( + Environment.SpecialFolder.MyDocuments, + Environment.SpecialFolderOption.DoNotVerify); - 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")); + PathToUserFolder = IOManager.ResolvePath( + IOManager.ConcatPath(documentsDirectory, "BYOND")); semaphore = new SemaphoreSlim(1); installedDirectX = false; @@ -114,40 +123,38 @@ public WindowsByondInstaller( public void Dispose() => semaphore.Dispose(); /// - public override string GetDreamDaemonName(Version version, out bool supportsCli, out bool supportsMapThreads) + public override ValueTask Install(EngineVersion version, string path, CancellationToken cancellationToken) { - ArgumentNullException.ThrowIfNull(version); - - supportsCli = version >= DDExeVersion; - supportsMapThreads = version >= MapThreadsVersion; - return supportsCli ? "dd.exe" : "dreamdaemon.exe"; - } + CheckVersionValidity(version); + ArgumentNullException.ThrowIfNull(path); - /// - public override ValueTask InstallByond(Version version, string path, CancellationToken cancellationToken) - { + var noPromptTrustedTask = SetNoPromptTrusted(path, cancellationToken); + var installDirectXTask = InstallDirectX(path, cancellationToken); var tasks = new List(3) { - SetNoPromptTrusted(path, cancellationToken), - InstallDirectX(path, cancellationToken), + noPromptTrustedTask, + installDirectXTask, }; if (!generalConfiguration.SkipAddingByondFirewallException) - tasks.Add(AddDreamDaemonToFirewall(version, path, cancellationToken)); + { + var firewallTask = AddDreamDaemonToFirewall(version, path, cancellationToken); + tasks.Add(firewallTask); + } return ValueTaskExtensions.WhenAll(tasks); } /// - public override async ValueTask UpgradeInstallation(Version version, string path, CancellationToken cancellationToken) + public override async ValueTask UpgradeInstallation(EngineVersion version, string path, CancellationToken cancellationToken) { - ArgumentNullException.ThrowIfNull(version); + CheckVersionValidity(version); ArgumentNullException.ThrowIfNull(path); if (generalConfiguration.SkipAddingByondFirewallException) return; - if (version < DDExeVersion) + if (version.Version < DDExeVersion) return; if (await IOManager.FileExists(IOManager.ConcatPath(path, TgsFirewalledDDFile), cancellationToken)) @@ -157,6 +164,13 @@ public override async ValueTask UpgradeInstallation(Version version, string path await AddDreamDaemonToFirewall(version, path, cancellationToken); } + /// + protected override string GetDreamDaemonName(Version byondVersion, out bool supportsCli) + { + supportsCli = byondVersion >= DDExeVersion; + return supportsCli ? "dd.exe" : "dreamdaemon.exe"; + } + /// /// Creates the BYOND cfg file that prevents the trusted mode dialog from appearing when launching DreamDaemon. /// @@ -223,57 +237,49 @@ async ValueTask InstallDirectX(string path, CancellationToken cancellationToken) /// /// Attempt to add the DreamDaemon executable as an exception to the Windows firewall. /// - /// The BYOND . + /// The BYOND . /// The path to the BYOND installation. /// The for the operation. /// A representing the running operation. - async ValueTask AddDreamDaemonToFirewall(Version version, string path, CancellationToken cancellationToken) + async ValueTask AddDreamDaemonToFirewall(EngineVersion version, string path, CancellationToken cancellationToken) { - var dreamDaemonName = GetDreamDaemonName(version, out var usesDDExe, out var _); + var dreamDaemonName = GetDreamDaemonName(version.Version!, out var usesDDExe); var dreamDaemonPath = IOManager.ResolvePath( IOManager.ConcatPath( path, - ByondManager.BinPath, + ByondBinPath, dreamDaemonName)); - Logger.LogInformation("Adding Windows Firewall exception for {path}...", dreamDaemonPath); + int exitCode; try { // I really wish we could add the instance name here but // 1. It'd make IByondInstaller need to be transient per-instance and WindowsByondInstaller relys on being a singleton for its DX installer call // 2. The instance could be renamed, so it'd have to be an unfriendly ID anyway. - var arguments = $"advfirewall firewall add rule name=\"TGS DreamDaemon {version}\" program=\"{dreamDaemonPath}\" protocol=tcp dir=in enable=yes action=allow"; - await using var netshProcess = processExecutor.LaunchProcess( - "netsh.exe", - IOManager.ResolvePath(), - arguments, - readStandardHandles: true, - noShellExecute: true); - - int exitCode; - using (cancellationToken.Register(() => netshProcess.Terminate())) - exitCode = (await netshProcess.Lifetime).Value; - cancellationToken.ThrowIfCancellationRequested(); - - Logger.LogDebug( - "netsh.exe output:{newLine}{output}", - Environment.NewLine, - await netshProcess.GetCombinedOutput(cancellationToken)); - - if (exitCode != 0) - throw new JobException(ErrorCode.ByondDreamDaemonFirewallFail, new JobException($"Invalid exit code: {exitCode}")); - - if (usesDDExe) - await IOManager.WriteAllBytes( - IOManager.ConcatPath(path, TgsFirewalledDDFile), - Array.Empty(), - cancellationToken); + var ruleName = $"TGS DreamDaemon {version}"; + + exitCode = await WindowsFirewallHelper.AddFirewallException( + processExecutor, + Logger, + ruleName, + dreamDaemonPath, + sessionConfiguration.LowPriorityDeploymentProcesses, + cancellationToken); } catch (Exception ex) { - throw new JobException(ErrorCode.ByondDreamDaemonFirewallFail, ex); + throw new JobException(ErrorCode.EngineFirewallFail, ex); } + + if (exitCode != 0) + throw new JobException(ErrorCode.EngineFirewallFail, new JobException($"Invalid exit code: {exitCode}")); + + if (usesDDExe) + await IOManager.WriteAllBytes( + IOManager.ConcatPath(path, TgsFirewalledDDFile), + Array.Empty(), + cancellationToken); } } } diff --git a/src/Tgstation.Server.Host/Components/Engine/WindowsOpenDreamInstaller.cs b/src/Tgstation.Server.Host/Components/Engine/WindowsOpenDreamInstaller.cs new file mode 100644 index 00000000000..25968446fe0 --- /dev/null +++ b/src/Tgstation.Server.Host/Components/Engine/WindowsOpenDreamInstaller.cs @@ -0,0 +1,138 @@ +using System; +using System.Threading; +using System.Threading.Tasks; + +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Options; + +using Tgstation.Server.Api.Models; +using Tgstation.Server.Common.Extensions; +using Tgstation.Server.Common.Http; +using Tgstation.Server.Host.Components.Repository; +using Tgstation.Server.Host.Configuration; +using Tgstation.Server.Host.IO; +using Tgstation.Server.Host.Jobs; +using Tgstation.Server.Host.System; +using Tgstation.Server.Host.Utils; + +namespace Tgstation.Server.Host.Components.Engine +{ + /// + /// Implementation of for Windows systems. + /// + sealed class WindowsOpenDreamInstaller : OpenDreamInstaller + { + /// + /// The for the . + /// + readonly IFilesystemLinkFactory linkFactory; + + /// + /// Initializes a new instance of the class. + /// + /// The for the . + /// The for the . + /// The for the . + /// The for the . + /// The for the . + /// The for the . + /// The for the . + /// The of for the . + /// The of for the . + /// The value of . + public WindowsOpenDreamInstaller( + IIOManager ioManager, + ILogger logger, + IPlatformIdentifier platformIdentifier, + IProcessExecutor processExecutor, + IRepositoryManager repositoryManager, + IAsyncDelayer asyncDelayer, + IAbstractHttpClientFactory httpClientFactory, + IOptions generalConfigurationOptions, + IOptions sessionConfigurationOptions, + IFilesystemLinkFactory linkFactory) + : base( + ioManager, + logger, + platformIdentifier, + processExecutor, + repositoryManager, + asyncDelayer, + httpClientFactory, + generalConfigurationOptions, + sessionConfigurationOptions) + { + this.linkFactory = linkFactory ?? throw new ArgumentNullException(nameof(linkFactory)); + } + + /// + public override ValueTask Install(EngineVersion version, string installPath, CancellationToken cancellationToken) + { + var installTask = base.Install( + version, + installPath, + cancellationToken); + var firewallTask = AddServerFirewallException( + version, + installPath, + cancellationToken); + + return ValueTaskExtensions.WhenAll(installTask, firewallTask); + } + + /// + protected override async ValueTask HandleExtremelyLongPathOperation(Func shortenedPathOperation, string originalPath, CancellationToken cancellationToken) + { + var shortPath = $"C:/{Guid.NewGuid()}"; + Logger.LogDebug("Shortening path for build from {long} to {short}...", originalPath, shortPath); + await linkFactory.CreateSymbolicLink(originalPath, shortPath, cancellationToken); + try + { + await shortenedPathOperation(shortPath); + } + finally + { + await IOManager.DeleteDirectory(shortPath, CancellationToken.None); // DCT: Should always run + } + } + + /// + /// Attempt to add the DreamDaemon executable as an exception to the Windows firewall. + /// + /// The BYOND . + /// The path to the BYOND installation. + /// The for the operation. + /// A representing the running operation. + async ValueTask AddServerFirewallException(EngineVersion version, string path, CancellationToken cancellationToken) + { + if (GeneralConfiguration.SkipAddingByondFirewallException) + return; + + GetExecutablePaths(path, out var serverExePath, out _); + + int exitCode; + try + { + // I really wish we could add the instance name here but + // 1. It'd make IByondInstaller need to be transient per-instance and WindowsByondInstaller relys on being a singleton for its DX installer call + // 2. The instance could be renamed, so it'd have to be an unfriendly ID anyway. + var ruleName = $"TGS OpenDream {version}"; + + exitCode = await WindowsFirewallHelper.AddFirewallException( + ProcessExecutor, + Logger, + ruleName, + serverExePath, + SessionConfiguration.LowPriorityDeploymentProcesses, + cancellationToken); + } + catch (Exception ex) + { + throw new JobException(ErrorCode.EngineFirewallFail, ex); + } + + if (exitCode != 0) + throw new JobException(ErrorCode.EngineFirewallFail, new JobException($"Invalid exit code: {exitCode}")); + } + } +} diff --git a/src/Tgstation.Server.Host/Components/Engine/ZipStreamEngineInstallationData.cs b/src/Tgstation.Server.Host/Components/Engine/ZipStreamEngineInstallationData.cs new file mode 100644 index 00000000000..292b7259817 --- /dev/null +++ b/src/Tgstation.Server.Host/Components/Engine/ZipStreamEngineInstallationData.cs @@ -0,0 +1,43 @@ +using System; +using System.IO; +using System.Threading; +using System.Threading.Tasks; + +using Tgstation.Server.Host.IO; + +namespace Tgstation.Server.Host.Components.Engine +{ + /// + /// Implementation of for a zip file in a . + /// + sealed class ZipStreamEngineInstallationData : IEngineInstallationData + { + /// + /// The for the . + /// + readonly IIOManager ioManager; + + /// + /// The containing the zip data of the engine. + /// + readonly Stream zipStream; + + /// + /// Initializes a new instance of the class. + /// + /// The value of . + /// The value of . + public ZipStreamEngineInstallationData(IIOManager ioManager, Stream zipStream) + { + this.ioManager = ioManager ?? throw new ArgumentNullException(nameof(ioManager)); + this.zipStream = zipStream ?? throw new ArgumentNullException(nameof(zipStream)); + } + + /// + public ValueTask DisposeAsync() => zipStream.DisposeAsync(); + + /// + public Task ExtractToPath(string path, CancellationToken cancellationToken) + => ioManager.ZipToDirectory(path, zipStream, cancellationToken); + } +} diff --git a/src/Tgstation.Server.Host/Components/Events/EventConsumer.cs b/src/Tgstation.Server.Host/Components/Events/EventConsumer.cs index 81a0d34585d..559e584a27f 100644 --- a/src/Tgstation.Server.Host/Components/Events/EventConsumer.cs +++ b/src/Tgstation.Server.Host/Components/Events/EventConsumer.cs @@ -19,7 +19,7 @@ sealed class EventConsumer : IEventConsumer /// /// The for the . /// - IWatchdog watchdog; + IWatchdog? watchdog; /// /// Initializes a new instance of the class. @@ -31,7 +31,7 @@ public EventConsumer(IConfiguration configuration) } /// - public async ValueTask HandleEvent(EventType eventType, IEnumerable parameters, bool deploymentPipeline, CancellationToken cancellationToken) + public async ValueTask HandleEvent(EventType eventType, IEnumerable parameters, bool deploymentPipeline, CancellationToken cancellationToken) { ArgumentNullException.ThrowIfNull(parameters); @@ -50,10 +50,9 @@ public async ValueTask HandleEvent(EventType eventType, IEnumerable para public void SetWatchdog(IWatchdog watchdog) { ArgumentNullException.ThrowIfNull(watchdog); - if (this.watchdog != null) + var oldWatchdog = Interlocked.CompareExchange(ref this.watchdog, watchdog, null); + if (oldWatchdog != null) throw new InvalidOperationException("watchdog already set!"); - - this.watchdog = watchdog; } } } diff --git a/src/Tgstation.Server.Host/Components/Events/EventScriptAttribute.cs b/src/Tgstation.Server.Host/Components/Events/EventScriptAttribute.cs index c1b14e7b678..ba93d34cf95 100644 --- a/src/Tgstation.Server.Host/Components/Events/EventScriptAttribute.cs +++ b/src/Tgstation.Server.Host/Components/Events/EventScriptAttribute.cs @@ -1,4 +1,5 @@ using System; +using System.Collections.Generic; namespace Tgstation.Server.Host.Components.Events { @@ -9,17 +10,17 @@ namespace Tgstation.Server.Host.Components.Events sealed class EventScriptAttribute : Attribute { /// - /// The name of the script the event script the runs. + /// The name and order of the scripts the event script the runs. /// - public string ScriptName { get; } + public IReadOnlyList ScriptNames { get; } /// /// Initializes a new instance of the class. /// - /// The value of . - public EventScriptAttribute(string scriptName) + /// The value of . + public EventScriptAttribute(params string[] scriptNames) { - ScriptName = scriptName ?? throw new ArgumentNullException(nameof(scriptName)); + ScriptNames = scriptNames ?? throw new ArgumentNullException(nameof(scriptNames)); } } } diff --git a/src/Tgstation.Server.Host/Components/Events/EventType.cs b/src/Tgstation.Server.Host/Components/Events/EventType.cs index ef7571ac833..fa8321c7e02 100644 --- a/src/Tgstation.Server.Host/Components/Events/EventType.cs +++ b/src/Tgstation.Server.Host/Components/Events/EventType.cs @@ -1,97 +1,97 @@ namespace Tgstation.Server.Host.Components.Events { /// - /// Types of events. Mirror in tgs.dm. + /// Types of events. Mirror in tgs.dm. Prefer last listed name for script. /// public enum EventType { /// - /// Parameters: Reference name, commit sha + /// Parameters: Reference name, commit sha. /// [EventScript("RepoResetOrigin")] RepoResetOrigin, /// - /// Parameters: Checkout target + /// Parameters: Checkout target. /// [EventScript("RepoCheckout")] RepoCheckout, /// - /// No parameters + /// No parameters. /// [EventScript("RepoFetch")] RepoFetch, /// - /// Parameters: Test merge number, test merge target sha, merger message + /// Parameters: Test merge number, test merge target sha, merger message. /// [EventScript("RepoMergePullRequest")] RepoAddTestMerge, /// - /// Parameters: Absolute path to repository root + /// Parameters: Absolute path to repository root. /// /// Changes made to the repository during this event will be pushed to the tracked branch if no test merges are present. [EventScript("PreSynchronize")] RepoPreSynchronize, /// - /// Parameters: Version being installed + /// Parameters: Version being installed. /// - [EventScript("ByondInstallStart")] - ByondInstallStart, + [EventScript("ByondInstallStart", "EngineInstallStart")] + EngineInstallStart, /// - /// Parameters: Error string + /// Parameters: Error string. /// - [EventScript("ByondInstallFail")] - ByondInstallFail, + [EventScript("ByondInstallFail", "EngineInstallFail")] + EngineInstallFail, /// - /// Parameters: Old active version, new active version + /// Parameters: Old active version, new active version. /// - [EventScript("ByondActiveVersionChange")] - ByondActiveVersionChange, + [EventScript("ByondActiveVersionChange", "EngineActiveVersionChange")] + EngineActiveVersionChange, /// - /// After the repo is copied, before CodeModifications are applied. Parameters: Game directory path, origin commit sha, byond version + /// After the repo is copied, before CodeModifications are applied. Parameters: Game directory path, origin commit sha, engine version string. /// [EventScript("PreCompile")] CompileStart, /// - /// No parameters + /// No parameters. /// [EventScript("CompileCancelled")] CompileCancelled, /// - /// Parameters: Game directory path, "1" if compile succeeded and api validation failed, "0" otherwise, BYOND version used + /// Parameters: Game directory path, "1" if compile succeeded and api validation failed, "0" otherwise, engine version string. /// [EventScript("CompileFailure")] CompileFailure, /// - /// Parameters: Game directory path, BYOND version used + /// Parameters: Game directory path, engine version string. /// [EventScript("PostCompile")] CompileComplete, /// - /// No parameters + /// No parameters. /// [EventScript("InstanceAutoUpdateStart")] InstanceAutoUpdateStart, /// - /// Parameters: Base sha, target sha, base reference, target reference, all conflicting files + /// Parameters: Base sha, target sha, base reference, target reference, all conflicting files. /// [EventScript("RepoMergeConflict")] RepoMergeConflict, /// - /// No parameters + /// No parameters. /// [EventScript("DeploymentComplete")] DeploymentComplete, @@ -139,31 +139,31 @@ public enum EventType WorldPrime, /// - /// After DD has launched. Not the same as WatchdogLaunch. Parameters: PID of DreamDaemon + /// After DD has launched. Not the same as WatchdogLaunch. Parameters: PID of DreamDaemon. /// [EventScript("DreamDaemonLaunch")] DreamDaemonLaunch, /// - /// After a single submodule update is performed. Parameters: Updated submodule name + /// After a single submodule update is performed. Parameters: Updated submodule name. /// [EventScript("RepoSubmoduleUpdate")] RepoSubmoduleUpdate, /// - /// After CodeModifications are applied, before DreamMaker is run. Parameters: Game directory path, origin commit sha, byond version + /// After CodeModifications are applied, before DreamMaker is run. Parameters: Game directory path, origin commit sha, engine version string. /// [EventScript("PreDreamMaker")] PreDreamMaker, /// - /// Whenever a deployment folder is deleted from disk. Parameters: Game directory path + /// Whenever a deployment folder is deleted from disk. Parameters: Game directory path. /// [EventScript("DeploymentCleanup")] DeploymentCleanup, /// - /// Whenever a deployment is about to be used by the game server. May fire multiple times per deployment. Parameters: Game directory path + /// Whenever a deployment is about to be used by the game server. May fire multiple times per deployment. Parameters: Game directory path. /// [EventScript("DeploymentActivation")] DeploymentActivation, diff --git a/src/Tgstation.Server.Host/Components/Events/IEventConsumer.cs b/src/Tgstation.Server.Host/Components/Events/IEventConsumer.cs index 7fddbb8950f..4d268011f12 100644 --- a/src/Tgstation.Server.Host/Components/Events/IEventConsumer.cs +++ b/src/Tgstation.Server.Host/Components/Events/IEventConsumer.cs @@ -17,6 +17,6 @@ public interface IEventConsumer /// If this event is part of the deployment pipeline. /// The for the operation. /// A representing the running operation. - ValueTask HandleEvent(EventType eventType, IEnumerable parameters, bool deploymentPipeline, CancellationToken cancellationToken); + ValueTask HandleEvent(EventType eventType, IEnumerable parameters, bool deploymentPipeline, CancellationToken cancellationToken); } } diff --git a/src/Tgstation.Server.Host/Components/Events/NoopEventConsumer.cs b/src/Tgstation.Server.Host/Components/Events/NoopEventConsumer.cs new file mode 100644 index 00000000000..dde777572ad --- /dev/null +++ b/src/Tgstation.Server.Host/Components/Events/NoopEventConsumer.cs @@ -0,0 +1,16 @@ +using System.Collections.Generic; +using System.Threading; +using System.Threading.Tasks; + +namespace Tgstation.Server.Host.Components.Events +{ + /// + /// No-op implementation of . + /// + sealed class NoopEventConsumer : IEventConsumer + { + /// + public ValueTask HandleEvent(EventType eventType, IEnumerable parameters, bool deploymentPipeline, CancellationToken cancellationToken) + => ValueTask.CompletedTask; + } +} diff --git a/src/Tgstation.Server.Host/Components/IInstanceCore.cs b/src/Tgstation.Server.Host/Components/IInstanceCore.cs index d42b4e69da0..78bce9e3f31 100644 --- a/src/Tgstation.Server.Host/Components/IInstanceCore.cs +++ b/src/Tgstation.Server.Host/Components/IInstanceCore.cs @@ -1,8 +1,8 @@ using System.Threading.Tasks; -using Tgstation.Server.Host.Components.Byond; using Tgstation.Server.Host.Components.Chat; using Tgstation.Server.Host.Components.Deployment; +using Tgstation.Server.Host.Components.Engine; using Tgstation.Server.Host.Components.Repository; using Tgstation.Server.Host.Components.StaticFiles; using Tgstation.Server.Host.Components.Watchdog; @@ -20,9 +20,9 @@ public interface IInstanceCore : ILatestCompileJobProvider, IRenameNotifyee IRepositoryManager RepositoryManager { get; } /// - /// The for the . + /// The for the . /// - IByondManager ByondManager { get; } + IEngineManager EngineManager { get; } /// /// The for the . diff --git a/src/Tgstation.Server.Host/Components/IInstanceCoreProvider.cs b/src/Tgstation.Server.Host/Components/IInstanceCoreProvider.cs index 7e6b0b8dc52..ab1db54e915 100644 --- a/src/Tgstation.Server.Host/Components/IInstanceCoreProvider.cs +++ b/src/Tgstation.Server.Host/Components/IInstanceCoreProvider.cs @@ -10,6 +10,6 @@ public interface IInstanceCoreProvider /// /// The to get the for. /// The if it is online, otherwise. - IInstanceCore GetInstance(Models.Instance instance); + IInstanceCore? GetInstance(Models.Instance instance); } } diff --git a/src/Tgstation.Server.Host/Components/IInstanceManager.cs b/src/Tgstation.Server.Host/Components/IInstanceManager.cs index 3174686efe8..516b2a8f302 100644 --- a/src/Tgstation.Server.Host/Components/IInstanceManager.cs +++ b/src/Tgstation.Server.Host/Components/IInstanceManager.cs @@ -19,6 +19,6 @@ public interface IInstanceManager : IInstanceOperations, IBridgeDispatcher /// /// The of the desired . /// The associated with the given if it is online, otherwise. - IInstanceReference GetInstanceReference(Api.Models.Instance metadata); + IInstanceReference? GetInstanceReference(Api.Models.Instance metadata); } } diff --git a/src/Tgstation.Server.Host/Components/Instance.cs b/src/Tgstation.Server.Host/Components/Instance.cs index de7e70d7966..a8b9a407212 100644 --- a/src/Tgstation.Server.Host/Components/Instance.cs +++ b/src/Tgstation.Server.Host/Components/Instance.cs @@ -9,10 +9,10 @@ using Serilog.Context; using Tgstation.Server.Api.Rights; -using Tgstation.Server.Host.Components.Byond; using Tgstation.Server.Host.Components.Chat; using Tgstation.Server.Host.Components.Deployment; using Tgstation.Server.Host.Components.Deployment.Remote; +using Tgstation.Server.Host.Components.Engine; using Tgstation.Server.Host.Components.Events; using Tgstation.Server.Host.Components.Repository; using Tgstation.Server.Host.Components.Watchdog; @@ -36,7 +36,7 @@ sealed class Instance : IInstance public IRepositoryManager RepositoryManager { get; } /// - public IByondManager ByondManager { get; } + public IEngineManager EngineManager { get; } /// public IWatchdog Watchdog { get; } @@ -93,19 +93,19 @@ sealed class Instance : IInstance /// /// The auto update . /// - Task timerTask; + Task? timerTask; /// /// for . /// - CancellationTokenSource timerCts; + CancellationTokenSource? timerCts; /// /// Initializes a new instance of the class. /// /// The value of . /// The value of . - /// The value of . + /// The value of . /// The value of . /// The value of . /// The value of . @@ -119,7 +119,7 @@ sealed class Instance : IInstance public Instance( Api.Models.Instance metadata, IRepositoryManager repositoryManager, - IByondManager byondManager, + IEngineManager engineManager, IDreamMaker dreamMaker, IWatchdog watchdog, IChatManager chat, @@ -134,7 +134,7 @@ public Instance( { this.metadata = metadata ?? throw new ArgumentNullException(nameof(metadata)); RepositoryManager = repositoryManager ?? throw new ArgumentNullException(nameof(repositoryManager)); - ByondManager = byondManager ?? throw new ArgumentNullException(nameof(byondManager)); + EngineManager = engineManager ?? throw new ArgumentNullException(nameof(engineManager)); DreamMaker = dreamMaker ?? throw new ArgumentNullException(nameof(dreamMaker)); Watchdog = watchdog ?? throw new ArgumentNullException(nameof(watchdog)); Chat = chat ?? throw new ArgumentNullException(nameof(chat)); @@ -160,7 +160,7 @@ public async ValueTask DisposeAsync() Configuration.Dispose(); dmbFactory.Dispose(); RepositoryManager.Dispose(); - ByondManager.Dispose(); + EngineManager.Dispose(); await chatDispose; await watchdogDispose; } @@ -183,9 +183,9 @@ public async Task StartAsync(CancellationToken cancellationToken) using (LogContext.PushProperty(SerilogContextHelper.InstanceIdContextProperty, metadata.Id)) { await Task.WhenAll( - SetAutoUpdateInterval(metadata.AutoUpdateInterval.Value).AsTask(), + SetAutoUpdateInterval(metadata.Require(x => x.AutoUpdateInterval)).AsTask(), Configuration.StartAsync(cancellationToken), - ByondManager.StartAsync(cancellationToken), + EngineManager.StartAsync(cancellationToken), Chat.StartAsync(cancellationToken), dmbFactory.StartAsync(cancellationToken)); @@ -206,7 +206,7 @@ public async Task StopAsync(CancellationToken cancellationToken) await Watchdog.StopAsync(cancellationToken); await Task.WhenAll( Configuration.StopAsync(cancellationToken), - ByondManager.StopAsync(cancellationToken), + EngineManager.StopAsync(cancellationToken), Chat.StopAsync(cancellationToken), dmbFactory.StopAsync(cancellationToken)); } @@ -221,7 +221,7 @@ public async ValueTask SetAutoUpdateInterval(uint newInterval) if (timerTask != null) { logger.LogTrace("Cancelling auto-update task"); - timerCts.Cancel(); + timerCts!.Cancel(); timerCts.Dispose(); toWait = timerTask; timerTask = null; @@ -253,7 +253,7 @@ public async ValueTask SetAutoUpdateInterval(uint newInterval) } /// - public CompileJob LatestCompileJob() => dmbFactory.LatestCompileJob(); + public CompileJob? LatestCompileJob() => dmbFactory.LatestCompileJob(); /// /// The for updating the repository. @@ -266,7 +266,7 @@ public async ValueTask SetAutoUpdateInterval(uint newInterval) /// A representing the running operation. #pragma warning disable CA1502 // Cyclomatic complexity ValueTask RepositoryAutoUpdateJob( - IInstanceCore core, + IInstanceCore? core, IDatabaseContextFactory databaseContextFactory, Job job, JobProgressReporter progressReporter, @@ -315,18 +315,18 @@ await repo.FetchOrigin( cancellationToken); var hasDbChanges = false; - RevisionInformation currentRevInfo = null; - Models.Instance attachedInstance = null; - async ValueTask UpdateRevInfo(string currentHead, bool onOrigin, IEnumerable updatedTestMerges) + RevisionInformation? currentRevInfo = null; + Models.Instance? attachedInstance = null; + async ValueTask UpdateRevInfo(string currentHead, bool onOrigin, IEnumerable? updatedTestMerges) { if (currentRevInfo == null) { logger.LogTrace("Loading revision info for commit {sha}...", startSha[..7]); currentRevInfo = await databaseContext - .RevisionInformations + .RevisionInformations .AsQueryable() - .Where(x => x.CommitSha == startSha && x.Instance.Id == metadata.Id) - .Include(x => x.ActiveTestMerges) + .Where(x => x.CommitSha == startSha && x.InstanceId == metadata.Id) + .Include(x => x.ActiveTestMerges!) .ThenInclude(x => x.TestMerge) .FirstOrDefaultAsync(cancellationToken); } @@ -364,12 +364,9 @@ async ValueTask UpdateRevInfo(string currentHead, bool onOrigin, IEnumerable x.TestMerge); + var testMerges = updatedTestMerges ?? oldRevInfo!.ActiveTestMerges!.Select(x => x.TestMerge); var revInfoTestMerges = testMerges.Select( - testMerge => new RevInfoTestMerge - { - TestMerge = testMerge, - }) + testMerge => new RevInfoTestMerge(testMerge, currentRevInfo)) .ToList(); currentRevInfo.ActiveTestMerges = revInfoTestMerges; @@ -382,21 +379,21 @@ async ValueTask UpdateRevInfo(string currentHead, bool onOrigin, IEnumerable x.CommitSha == currentHead && x.Instance.Id == metadata.Id) + .Where(x => x.CommitSha == currentHead && x.InstanceId == metadata.Id) .FirstOrDefaultAsync(cancellationToken); if (currentHead != startSha && currentRevInfo == default) @@ -453,19 +450,19 @@ await repo.ResetToOrigin( } // synch if necessary - if (repositorySettings.AutoUpdatesSynchronize.Value && startSha != repo.Head && (shouldSyncTracked || repositorySettings.PushTestMergeCommits.Value)) + if (repositorySettings.AutoUpdatesSynchronize!.Value && startSha != repo.Head && (shouldSyncTracked || repositorySettings.PushTestMergeCommits!.Value)) { - var pushedOrigin = await repo.Sychronize( + var pushedOrigin = await repo.Synchronize( NextProgressReporter("Synchronize"), repositorySettings.AccessUser, repositorySettings.AccessToken, - repositorySettings.CommitterName, - repositorySettings.CommitterEmail, + repositorySettings.CommitterName!, + repositorySettings.CommitterEmail!, shouldSyncTracked, true, cancellationToken); var currentHead = repo.Head; - if (currentHead != currentRevInfo.CommitSha) + if (currentHead != currentRevInfo!.CommitSha) await UpdateRevInfo(currentHead, pushedOrigin, null); } @@ -517,6 +514,9 @@ await jobManager.RegisterOperation( Job compileProcessJob; using (var repo = await RepositoryManager.LoadRepository(cancellationToken)) { + if (repo == null) + throw new JobException(Api.Models.ErrorCode.RepoMissing); + var deploySha = repo.Head; if (deploySha == null) { diff --git a/src/Tgstation.Server.Host/Components/InstanceFactory.cs b/src/Tgstation.Server.Host/Components/InstanceFactory.cs index b3817ab86c7..2b24bcb6cb4 100644 --- a/src/Tgstation.Server.Host/Components/InstanceFactory.cs +++ b/src/Tgstation.Server.Host/Components/InstanceFactory.cs @@ -5,11 +5,11 @@ using Microsoft.Extensions.Logging; using Microsoft.Extensions.Options; -using Tgstation.Server.Host.Components.Byond; using Tgstation.Server.Host.Components.Chat; using Tgstation.Server.Host.Components.Chat.Commands; using Tgstation.Server.Host.Components.Deployment; using Tgstation.Server.Host.Components.Deployment.Remote; +using Tgstation.Server.Host.Components.Engine; using Tgstation.Server.Host.Components.Events; using Tgstation.Server.Host.Components.Interop.Bridge; using Tgstation.Server.Host.Components.Repository; @@ -71,9 +71,9 @@ sealed class InstanceFactory : IInstanceFactory readonly IFilesystemLinkFactory linkFactory; /// - /// The for the . + /// The for the . /// - readonly IByondInstaller byondInstaller; + readonly IEngineInstaller engineInstaller; /// /// The for the . @@ -111,14 +111,9 @@ sealed class InstanceFactory : IInstanceFactory readonly IPlatformIdentifier platformIdentifier; /// - /// The for the . + /// The for the . /// - readonly ILibGit2RepositoryFactory repositoryFactory; - - /// - /// The for the . - /// - readonly ILibGit2Commands repositoryCommands; + readonly IRepositoryManagerFactory repositoryManagerFactory; /// /// The for the . @@ -130,11 +125,6 @@ sealed class InstanceFactory : IInstanceFactory /// readonly IFileTransferTicketProvider fileTransferService; - /// - /// The for the . - /// - readonly IGitRemoteFeaturesFactory gitRemoteFeaturesFactory; - /// /// The for the . /// @@ -160,7 +150,7 @@ sealed class InstanceFactory : IInstanceFactory /// /// The instance's . /// The for the instance's "Game" directory. - static IIOManager CreateGameIOManager(IIOManager instanceIOManager) => new ResolvingIOManager(instanceIOManager, "Game"); + static ResolvingIOManager CreateGameIOManager(IIOManager instanceIOManager) => new(instanceIOManager, "Game"); #pragma warning disable CA1502 // TODO: Decomplexify /// @@ -174,7 +164,7 @@ sealed class InstanceFactory : IInstanceFactory /// The value of . /// The value of . /// The value of . - /// The value of . + /// The value of . /// The value of . /// The value of . /// The value of . @@ -182,11 +172,9 @@ sealed class InstanceFactory : IInstanceFactory /// The value of . /// The value of . /// The value of . - /// The value of . - /// The value of . + /// The value of . /// The value of . /// The value of . - /// The value of . /// The value of . /// The value of . /// The containing the value of . @@ -200,7 +188,7 @@ public InstanceFactory( ICryptographySuite cryptographySuite, ISynchronousIOManager synchronousIOManager, IFilesystemLinkFactory linkFactory, - IByondInstaller byondInstaller, + IEngineInstaller engineInstaller, IChatManagerFactory chatFactory, IProcessExecutor processExecutor, IPostWriteHandler postWriteHandler, @@ -208,11 +196,9 @@ public InstanceFactory( IJobManager jobManager, INetworkPromptReaper networkPromptReaper, IPlatformIdentifier platformIdentifier, - ILibGit2RepositoryFactory repositoryFactory, - ILibGit2Commands repositoryCommands, + IRepositoryManagerFactory repositoryManagerFactory, IServerPortProvider serverPortProvider, IFileTransferTicketProvider fileTransferService, - IGitRemoteFeaturesFactory gitRemoteFeaturesFactory, IRemoteDeploymentManagerFactory remoteDeploymentManagerFactory, IAsyncDelayer asyncDelayer, IOptions generalConfigurationOptions, @@ -226,7 +212,7 @@ public InstanceFactory( this.cryptographySuite = cryptographySuite ?? throw new ArgumentNullException(nameof(cryptographySuite)); this.synchronousIOManager = synchronousIOManager ?? throw new ArgumentNullException(nameof(synchronousIOManager)); this.linkFactory = linkFactory ?? throw new ArgumentNullException(nameof(linkFactory)); - this.byondInstaller = byondInstaller ?? throw new ArgumentNullException(nameof(byondInstaller)); + this.engineInstaller = engineInstaller ?? throw new ArgumentNullException(nameof(engineInstaller)); this.chatFactory = chatFactory ?? throw new ArgumentNullException(nameof(chatFactory)); this.processExecutor = processExecutor ?? throw new ArgumentNullException(nameof(processExecutor)); this.postWriteHandler = postWriteHandler ?? throw new ArgumentNullException(nameof(postWriteHandler)); @@ -234,11 +220,9 @@ public InstanceFactory( this.jobManager = jobManager ?? throw new ArgumentNullException(nameof(jobManager)); this.networkPromptReaper = networkPromptReaper ?? throw new ArgumentNullException(nameof(networkPromptReaper)); this.platformIdentifier = platformIdentifier ?? throw new ArgumentNullException(nameof(platformIdentifier)); - this.repositoryFactory = repositoryFactory ?? throw new ArgumentNullException(nameof(repositoryFactory)); - this.repositoryCommands = repositoryCommands ?? throw new ArgumentNullException(nameof(repositoryCommands)); + this.repositoryManagerFactory = repositoryManagerFactory ?? throw new ArgumentNullException(nameof(repositoryManagerFactory)); this.serverPortProvider = serverPortProvider ?? throw new ArgumentNullException(nameof(serverPortProvider)); this.fileTransferService = fileTransferService ?? throw new ArgumentNullException(nameof(fileTransferService)); - this.gitRemoteFeaturesFactory = gitRemoteFeaturesFactory ?? throw new ArgumentNullException(nameof(gitRemoteFeaturesFactory)); this.remoteDeploymentManagerFactory = remoteDeploymentManagerFactory ?? throw new ArgumentNullException(nameof(remoteDeploymentManagerFactory)); this.asyncDelayer = asyncDelayer ?? throw new ArgumentNullException(nameof(asyncDelayer)); generalConfiguration = generalConfigurationOptions?.Value ?? throw new ArgumentNullException(nameof(generalConfigurationOptions)); @@ -284,19 +268,10 @@ public async ValueTask CreateInstance(IBridgeRegistrar bridgeRegistra generalConfiguration, sessionConfiguration); var eventConsumer = new EventConsumer(configuration); - var repoManager = new RepositoryManager( - repositoryFactory, - repositoryCommands, - repoIoManager, - eventConsumer, - postWriteHandler, - gitRemoteFeaturesFactory, - loggerFactory.CreateLogger(), - loggerFactory.CreateLogger(), - generalConfiguration); + var repoManager = repositoryManagerFactory.CreateRepositoryManager(repoIoManager, eventConsumer); try { - var byond = new ByondManager(byondIOManager, byondInstaller, eventConsumer, loggerFactory.CreateLogger()); + var engineManager = new EngineManager(byondIOManager, engineInstaller, eventConsumer, loggerFactory.CreateLogger()); var dmbFactory = new DmbFactory( databaseContextFactory, @@ -307,7 +282,7 @@ public async ValueTask CreateInstance(IBridgeRegistrar bridgeRegistra metadata); try { - var commandFactory = new CommandFactory(assemblyInformationProvider, byond, repoManager, databaseContextFactory, dmbFactory, metadata); + var commandFactory = new CommandFactory(assemblyInformationProvider, engineManager, repoManager, databaseContextFactory, dmbFactory, metadata); var chatManager = chatFactory.CreateChatManager(commandFactory, metadata.ChatSettings); try @@ -321,7 +296,7 @@ public async ValueTask CreateInstance(IBridgeRegistrar bridgeRegistra var sessionControllerFactory = new SessionControllerFactory( processExecutor, - byond, + engineManager, topicClientFactory, cryptographySuite, assemblyInformationProvider, @@ -349,15 +324,15 @@ public async ValueTask CreateInstance(IBridgeRegistrar bridgeRegistra configuration, // watchdog doesn't need itself as an event consumer remoteDeploymentManagerFactory, metadata, - metadata.DreamDaemonSettings); + metadata.DreamDaemonSettings!); try { eventConsumer.SetWatchdog(watchdog); commandFactory.SetWatchdog(watchdog); - Instance instance = null; + Instance? instance = null; var dreamMaker = new DreamMaker( - byond, + engineManager, gameIoManager, configuration, sessionControllerFactory, @@ -375,7 +350,7 @@ public async ValueTask CreateInstance(IBridgeRegistrar bridgeRegistra instance = new Instance( metadata, repoManager, - byond, + engineManager, dreamMaker, watchdog, chatManager, @@ -417,24 +392,18 @@ public async ValueTask CreateInstance(IBridgeRegistrar bridgeRegistra /// public Task StartAsync(CancellationToken cancellationToken) - { - CheckSystemCompatibility(); - return byondInstaller.CleanCache(cancellationToken); - } + => Task.WhenAll( + repositoryManagerFactory.StartAsync(cancellationToken), + engineInstaller.CleanCache(cancellationToken)); /// - public Task StopAsync(CancellationToken cancellationToken) => Task.CompletedTask; - - /// - /// Test that the is functional. - /// - void CheckSystemCompatibility() => repositoryFactory.CreateInMemory(); + public Task StopAsync(CancellationToken cancellationToken) => repositoryManagerFactory.StopAsync(cancellationToken); /// /// Create the for a given set of instance . /// /// The . /// The for the . - IIOManager CreateInstanceIOManager(Models.Instance metadata) => new ResolvingIOManager(ioManager, metadata.Path); + ResolvingIOManager CreateInstanceIOManager(Models.Instance metadata) => new(ioManager, metadata.Path!); } } diff --git a/src/Tgstation.Server.Host/Components/InstanceManager.cs b/src/Tgstation.Server.Host/Components/InstanceManager.cs index 86dbad53320..613b9d78004 100644 --- a/src/Tgstation.Server.Host/Components/InstanceManager.cs +++ b/src/Tgstation.Server.Host/Components/InstanceManager.cs @@ -21,6 +21,7 @@ using Tgstation.Server.Host.Extensions; using Tgstation.Server.Host.IO; using Tgstation.Server.Host.Jobs; +using Tgstation.Server.Host.Models; using Tgstation.Server.Host.Security; using Tgstation.Server.Host.Swarm; using Tgstation.Server.Host.System; @@ -94,6 +95,11 @@ sealed class InstanceManager : /// readonly IConsole console; + /// + /// The for the . + /// + readonly IPlatformIdentifier platformIdentifier; + /// /// The for the . /// @@ -110,7 +116,7 @@ sealed class InstanceManager : readonly Dictionary bridgeHandlers; /// - /// used to guard calls to and . + /// used to guard calls to and . /// readonly SemaphoreSlim instanceStateChangeSemaphore; @@ -142,12 +148,12 @@ sealed class InstanceManager : /// /// The original of . /// - readonly string originalConsoleTitle; + readonly string? originalConsoleTitle; /// /// The returned by . /// - Task startupTask; + Task? startupTask; /// /// If the has been 'd. @@ -168,6 +174,7 @@ sealed class InstanceManager : /// 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 . @@ -183,6 +190,7 @@ public InstanceManager( IServerPortProvider serverPortProvider, ISwarmServiceController swarmServiceController, IConsole console, + IPlatformIdentifier platformIdentifier, IOptions generalConfigurationOptions, IOptions swarmConfigurationOptions, ILogger logger) @@ -198,6 +206,7 @@ public InstanceManager( this.serverPortProvider = serverPortProvider ?? throw new ArgumentNullException(nameof(serverPortProvider)); this.swarmServiceController = swarmServiceController ?? throw new ArgumentNullException(nameof(swarmServiceController)); this.console = console ?? throw new ArgumentNullException(nameof(console)); + this.platformIdentifier = platformIdentifier ?? throw new ArgumentNullException(nameof(platformIdentifier)); generalConfiguration = generalConfigurationOptions?.Value ?? throw new ArgumentNullException(nameof(generalConfigurationOptions)); swarmConfiguration = swarmConfigurationOptions?.Value ?? throw new ArgumentNullException(nameof(swarmConfigurationOptions)); this.logger = logger ?? throw new ArgumentNullException(nameof(logger)); @@ -233,13 +242,13 @@ public async ValueTask DisposeAsync() } /// - public IInstanceReference GetInstanceReference(Api.Models.Instance metadata) + public IInstanceReference? GetInstanceReference(Api.Models.Instance metadata) { ArgumentNullException.ThrowIfNull(metadata); lock (instances) { - if (!instances.TryGetValue(metadata.Id.Value, out var instance)) + if (!instances.TryGetValue(metadata.Require(x => x.Id), out var instance)) return null; return instance.AddReference(); @@ -255,7 +264,7 @@ public async ValueTask MoveInstance(Models.Instance instance, string oldPath, Ca using var instanceReferenceCheck = GetInstanceReference(instance); if (instanceReferenceCheck != null) throw new InvalidOperationException("Cannot move an online instance!"); - var newPath = instance.Path; + var newPath = instance.Path!; try { await ioManager.MoveDirectory(oldPath, newPath, cancellationToken); @@ -318,22 +327,23 @@ await ioManager.WriteAllBytes( } /// - public async ValueTask OfflineInstance(Models.Instance metadata, Models.User user, CancellationToken cancellationToken) + public async ValueTask OfflineInstance(Models.Instance metadata, User user, CancellationToken cancellationToken) { ArgumentNullException.ThrowIfNull(metadata); using (await SemaphoreSlimContext.Lock(instanceStateChangeSemaphore, cancellationToken)) { - ReferenceCountingContainer container; + ReferenceCountingContainer? container; + var instanceId = metadata.Require(x => x.Id); lock (instances) { - if (!instances.TryGetValue(metadata.Id.Value, out container)) + if (!instances.TryGetValue(instanceId, out container)) { logger.LogDebug("Not offlining removed instance {instanceId}", metadata.Id); return; } - instances.Remove(metadata.Id.Value); + instances.Remove(instanceId); } logger.LogInformation("Offlining instance ID {instanceId}", metadata.Id); @@ -343,27 +353,29 @@ public async ValueTask OfflineInstance(Models.Instance metadata, Models.User use await container.OnZeroReferences.WaitAsync(cancellationToken); // we are the one responsible for cancelling his jobs - var tasks = new List>(); + ValueTask groupedTask = default; await databaseContextFactory.UseContext( async db => { var jobs = await db .Jobs .AsQueryable() - .Where(x => x.Instance.Id == metadata.Id && !x.StoppedAt.HasValue) - .Select(x => new Models.Job(x.Id.Value)) + .Where(x => x.Instance!.Id == metadata.Id && !x.StoppedAt.HasValue) + .Select(x => new Job(x.Id!.Value)) .ToListAsync(cancellationToken); - foreach (var job in jobs) - tasks.Add(jobService.CancelJob(job, user, true, cancellationToken)); + + groupedTask = ValueTaskExtensions.WhenAll( + jobs.Select(job => jobService.CancelJob(job, user, true, cancellationToken)), + jobs.Count); }); - await ValueTaskExtensions.WhenAll(tasks); + await groupedTask; } catch { // not too late to change your mind lock (instances) - instances.Add(metadata.Id.Value, container); + instances.Add(instanceId, container); throw; } @@ -385,9 +397,10 @@ public async ValueTask OnlineInstance(Models.Instance metadata, CancellationToke { ArgumentNullException.ThrowIfNull(metadata); + var instanceId = metadata.Require(x => x.Id); using var lockContext = await SemaphoreSlimContext.Lock(instanceStateChangeSemaphore, cancellationToken); lock (instances) - if (instances.ContainsKey(metadata.Id.Value)) + if (instances.ContainsKey(instanceId)) { logger.LogDebug("Aborting instance creation due to it seemingly already being online"); return; @@ -403,7 +416,7 @@ public async ValueTask OnlineInstance(Models.Instance metadata, CancellationToke { lock (instances) instances.Add( - metadata.Id.Value, + instanceId, new ReferenceCountingContainer(instance)); } catch (Exception ex) @@ -445,6 +458,12 @@ public async Task StopAsync(CancellationToken cancellationToken) using (cancellationToken.Register(shutdownCancellationTokenSource.Cancel)) try { + if (startupTask == null) + { + logger.LogWarning("InstanceManager was never started!"); + return; + } + logger.LogDebug("Stopping instance manager..."); if (!startupTask.IsCompleted) @@ -477,7 +496,7 @@ async ValueTask OfflineInstanceImmediate(IInstance instance, CancellationToken c finally { if (originalConsoleTitle != null) - console.Title = originalConsoleTitle; + console.SetTitle(originalConsoleTitle); } } catch (Exception ex) @@ -487,18 +506,25 @@ async ValueTask OfflineInstanceImmediate(IInstance instance, CancellationToken c } /// - public async ValueTask ProcessBridgeRequest(BridgeParameters parameters, CancellationToken cancellationToken) + public async ValueTask ProcessBridgeRequest(BridgeParameters parameters, CancellationToken cancellationToken) { ArgumentNullException.ThrowIfNull(parameters); - IBridgeHandler bridgeHandler = null; + var accessIdentifier = parameters.AccessIdentifier; + if (accessIdentifier == null) + { + logger.LogWarning("Received invalid bridge request with null access identifier!"); + return null; + } + + IBridgeHandler? bridgeHandler = null; for (var i = 0; bridgeHandler == null && i < 30; ++i) { // There's a miniscule time period where we could potentially receive a bridge request and not have the registration ready when we launch DD // This is a stopgap Task delayTask = Task.CompletedTask; lock (bridgeHandlers) - if (!bridgeHandlers.TryGetValue(parameters.AccessIdentifier, out bridgeHandler)) + if (!bridgeHandlers.TryGetValue(accessIdentifier, out bridgeHandler)) delayTask = asyncDelayer.Delay(TimeSpan.FromMilliseconds(100), cancellationToken); await delayTask; @@ -506,9 +532,9 @@ public async ValueTask ProcessBridgeRequest(BridgeParameters par if (bridgeHandler == null) lock (bridgeHandlers) - if (!bridgeHandlers.TryGetValue(parameters.AccessIdentifier, out bridgeHandler)) + if (!bridgeHandlers.TryGetValue(accessIdentifier, out bridgeHandler)) { - logger.LogWarning("Recieved invalid bridge request with access identifier: {accessIdentifier}", parameters.AccessIdentifier); + logger.LogWarning("Received invalid bridge request with access identifier: {accessIdentifier}", accessIdentifier); return null; } @@ -520,7 +546,8 @@ public IBridgeRegistration RegisterHandler(IBridgeHandler bridgeHandler) { ArgumentNullException.ThrowIfNull(bridgeHandler); - var accessIdentifier = bridgeHandler.DMApiParameters.AccessIdentifier; + var accessIdentifier = bridgeHandler.DMApiParameters.AccessIdentifier + ?? throw new InvalidOperationException("Attempted bridge registration with null AccessIdentifier!"); lock (bridgeHandlers) { bridgeHandlers.Add(accessIdentifier, bridgeHandler); @@ -538,11 +565,11 @@ public IBridgeRegistration RegisterHandler(IBridgeHandler bridgeHandler) } /// - public IInstanceCore GetInstance(Models.Instance metadata) + public IInstanceCore? GetInstance(Models.Instance metadata) { lock (instances) { - instances.TryGetValue(metadata.Id.Value, out var container); + instances.TryGetValue(metadata.Require(x => x.Id), out var container); return container?.Instance; } } @@ -557,7 +584,7 @@ async Task Initialize(CancellationToken cancellationToken) try { logger.LogInformation("{versionString}", assemblyInformationProvider.VersionString); - console.Title = assemblyInformationProvider.VersionString; + console.SetTitle(assemblyInformationProvider.VersionString); CheckSystemCompatibility(); @@ -566,13 +593,13 @@ async Task Initialize(CancellationToken cancellationToken) await InitializeSwarm(cancellationToken); - List dbInstances = null; + List? dbInstances = null; async ValueTask EnumerateInstances(IDatabaseContext databaseContext) => dbInstances = await databaseContext .Instances .AsQueryable() - .Where(x => x.Online.Value && x.SwarmIdentifer == swarmConfiguration.Identifier) + .Where(x => x.Online!.Value && x.SwarmIdentifer == swarmConfiguration.Identifier) .Include(x => x.RepositorySettings) .Include(x => x.ChatSettings) .ThenInclude(x => x.Channels) @@ -586,7 +613,7 @@ async ValueTask EnumerateInstances(IDatabaseContext databaseContext) await Task.WhenAll(instanceEnumeration.AsTask(), factoryStartup, jobManagerStartup); - var instanceOnliningTasks = dbInstances.Select( + var instanceOnliningTasks = dbInstances!.Select( async metadata => { try @@ -601,10 +628,11 @@ async ValueTask EnumerateInstances(IDatabaseContext databaseContext) await Task.WhenAll(instanceOnliningTasks); - jobService.Activate(this); - logger.LogInformation("Server ready!"); readyTcs.SetResult(); + + // this needs to happen after the HTTP API opens with readyTcs otherwise it can race and cause failed bridge requests with 503's + jobService.Activate(this); } catch (OperationCanceledException ex) { @@ -640,7 +668,7 @@ void CheckSystemCompatibility() // This runs before the real socket is opened, ensures we don't perform reattaches unless we're fairly certain the bind won't fail // If it does fail, DD will be killed. - SocketExtensions.BindTest(serverPortProvider.HttpApiPort, true); + SocketExtensions.BindTest(platformIdentifier, serverPortProvider.HttpApiPort, true, false); } /// diff --git a/src/Tgstation.Server.Host/Components/InstanceWrapper.cs b/src/Tgstation.Server.Host/Components/InstanceWrapper.cs index 1e1359c3e9b..ba2c84d6880 100644 --- a/src/Tgstation.Server.Host/Components/InstanceWrapper.cs +++ b/src/Tgstation.Server.Host/Components/InstanceWrapper.cs @@ -1,9 +1,9 @@ using System.Threading; using System.Threading.Tasks; -using Tgstation.Server.Host.Components.Byond; using Tgstation.Server.Host.Components.Chat; using Tgstation.Server.Host.Components.Deployment; +using Tgstation.Server.Host.Components.Engine; using Tgstation.Server.Host.Components.Repository; using Tgstation.Server.Host.Components.StaticFiles; using Tgstation.Server.Host.Components.Watchdog; @@ -29,7 +29,7 @@ sealed class InstanceWrapper : ReferenceCounter, IInstanceReference public IRepositoryManager RepositoryManager => Instance.RepositoryManager; /// - public IByondManager ByondManager => Instance.ByondManager; + public IEngineManager EngineManager => Instance.EngineManager; /// public IDreamMaker DreamMaker => Instance.DreamMaker; @@ -58,6 +58,6 @@ public InstanceWrapper() public ValueTask SetAutoUpdateInterval(uint newInterval) => Instance.SetAutoUpdateInterval(newInterval); /// - public CompileJob LatestCompileJob() => Instance.LatestCompileJob(); + public CompileJob? LatestCompileJob() => Instance.LatestCompileJob(); } } diff --git a/src/Tgstation.Server.Host/Components/Interop/Bridge/BridgeCommandType.cs b/src/Tgstation.Server.Host/Components/Interop/Bridge/BridgeCommandType.cs index ad3c2273569..8c18f74c397 100644 --- a/src/Tgstation.Server.Host/Components/Interop/Bridge/BridgeCommandType.cs +++ b/src/Tgstation.Server.Host/Components/Interop/Bridge/BridgeCommandType.cs @@ -8,7 +8,7 @@ public enum BridgeCommandType /// /// DreamDaemon notifying us of its current port and requesting a change if necessary. /// - PortUpdate, + DeprecatedPortUpdate, /// /// DreamDaemon notifying it is starting. @@ -16,7 +16,7 @@ public enum BridgeCommandType Startup, /// - /// DreamDaemon notifying the server is primed + /// DreamDaemon notifying the server is primed. /// Prime, diff --git a/src/Tgstation.Server.Host/Components/Interop/Bridge/BridgeParameters.cs b/src/Tgstation.Server.Host/Components/Interop/Bridge/BridgeParameters.cs index 6bd13cb72d8..1240bc81cb9 100644 --- a/src/Tgstation.Server.Host/Components/Interop/Bridge/BridgeParameters.cs +++ b/src/Tgstation.Server.Host/Components/Interop/Bridge/BridgeParameters.cs @@ -17,19 +17,19 @@ public sealed class BridgeParameters : DMApiParameters public BridgeCommandType? CommandType { get; set; } /// - /// The current port for requests. + /// The current port for requests. /// public ushort? CurrentPort { get; set; } /// /// The DMAPI for requests. /// - public Version Version { get; set; } + public Version? Version { get; set; } /// /// The DMAPI s for requests. /// - public ICollection CustomCommands { get; set; } + public ICollection? CustomCommands { get; set; } /// /// The minimum required level for requests. @@ -39,11 +39,25 @@ public sealed class BridgeParameters : DMApiParameters /// /// The for requests. /// - public ChatMessage ChatMessage { get; set; } + public ChatMessage? ChatMessage { get; set; } /// /// The for requests. /// - public ChunkData Chunk { get; set; } + public ChunkData? Chunk { get; set; } + + /// + /// The port that should be used to send world topics, if not the default. + /// + public ushort? TopicPort { get; set; } + + /// + /// Initializes a new instance of the class. + /// + /// The access identifier for the . + public BridgeParameters(string accessIdentifier) + : base(accessIdentifier) + { + } } } diff --git a/src/Tgstation.Server.Host/Components/Interop/Bridge/BridgeRegistration.cs b/src/Tgstation.Server.Host/Components/Interop/Bridge/BridgeRegistration.cs index ade7d5adfe8..1ba9673ee61 100644 --- a/src/Tgstation.Server.Host/Components/Interop/Bridge/BridgeRegistration.cs +++ b/src/Tgstation.Server.Host/Components/Interop/Bridge/BridgeRegistration.cs @@ -1,38 +1,19 @@ using System; +using Tgstation.Server.Host.Utils; + namespace Tgstation.Server.Host.Components.Interop.Bridge { /// - sealed class BridgeRegistration : IBridgeRegistration + sealed class BridgeRegistration : DisposeInvoker, IBridgeRegistration { - /// - /// for accessing . - /// - readonly object lockObject; - - /// - /// to run when d. - /// - Action onDispose; - /// /// Initializes a new instance of the class. /// - /// The value of . - public BridgeRegistration(Action onDispose) - { - this.onDispose = onDispose ?? throw new ArgumentNullException(nameof(onDispose)); - lockObject = new object(); - } - - /// - public void Dispose() + /// The action for the . + public BridgeRegistration(Action disposeAction) + : base(disposeAction) { - lock (lockObject) - { - onDispose?.Invoke(); - onDispose = null; - } } } } diff --git a/src/Tgstation.Server.Host/Components/Interop/Bridge/BridgeResponse.cs b/src/Tgstation.Server.Host/Components/Interop/Bridge/BridgeResponse.cs index f7f358d6121..620241fafb1 100644 --- a/src/Tgstation.Server.Host/Components/Interop/Bridge/BridgeResponse.cs +++ b/src/Tgstation.Server.Host/Components/Interop/Bridge/BridgeResponse.cs @@ -15,11 +15,11 @@ public class BridgeResponse : DMApiResponse, IMissingPayloadsCommunication /// /// The for requests. /// - public RuntimeInformation RuntimeInformation { get; set; } + public RuntimeInformation? RuntimeInformation { get; set; } /// /// The s missing from a chunked request. /// - public IReadOnlyCollection MissingChunks { get; set; } + public IReadOnlyCollection? MissingChunks { get; set; } } } diff --git a/src/Tgstation.Server.Host/Components/Interop/Bridge/IBridgeDispatcher.cs b/src/Tgstation.Server.Host/Components/Interop/Bridge/IBridgeDispatcher.cs index 82de2d66312..80355834425 100644 --- a/src/Tgstation.Server.Host/Components/Interop/Bridge/IBridgeDispatcher.cs +++ b/src/Tgstation.Server.Host/Components/Interop/Bridge/IBridgeDispatcher.cs @@ -14,6 +14,6 @@ public interface IBridgeDispatcher /// The to handle. /// The for the operation. /// A resulting in the for the request or if the request could not be dispatched. - ValueTask ProcessBridgeRequest(BridgeParameters parameters, CancellationToken cancellationToken); + ValueTask ProcessBridgeRequest(BridgeParameters parameters, CancellationToken cancellationToken); } } diff --git a/src/Tgstation.Server.Host/Components/Interop/Bridge/RuntimeInformation.cs b/src/Tgstation.Server.Host/Components/Interop/Bridge/RuntimeInformation.cs index b7d05b67d79..a011dad75a4 100644 --- a/src/Tgstation.Server.Host/Components/Interop/Bridge/RuntimeInformation.cs +++ b/src/Tgstation.Server.Host/Components/Interop/Bridge/RuntimeInformation.cs @@ -91,11 +91,11 @@ public RuntimeInformation( OriginCommitSha = dmbProvider.CompileJob.RevisionInformation.OriginCommitSha, }; - TestMerges = (IReadOnlyCollection)dmbProvider + TestMerges = (IReadOnlyCollection?)dmbProvider .CompileJob .RevisionInformation - .ActiveTestMerges? - .Select(x => x.TestMerge) + .ActiveTestMerges + ?.Select(x => x.TestMerge) .Select(x => new TestMergeInformation(x, Revision)) .ToList() ?? Array.Empty(); diff --git a/src/Tgstation.Server.Host/Components/Interop/Bridge/TestMergeInformation.cs b/src/Tgstation.Server.Host/Components/Interop/Bridge/TestMergeInformation.cs index 1270af68cf3..4537c56bd92 100644 --- a/src/Tgstation.Server.Host/Components/Interop/Bridge/TestMergeInformation.cs +++ b/src/Tgstation.Server.Host/Components/Interop/Bridge/TestMergeInformation.cs @@ -18,10 +18,10 @@ public sealed class TestMergeInformation : TestMergeModelBase /// /// Backing field for needed to continue to support DMAPI 5. /// - public string PullRequestRevision { get; set; } + public string? PullRequestRevision { get; set; } /// - public override string TargetCommitSha + public override string? TargetCommitSha { get => PullRequestRevision; set => PullRequestRevision = value; diff --git a/src/Tgstation.Server.Host/Components/Interop/ChatEmbed.cs b/src/Tgstation.Server.Host/Components/Interop/ChatEmbed.cs index da3518ec2a6..41563191f30 100644 --- a/src/Tgstation.Server.Host/Components/Interop/ChatEmbed.cs +++ b/src/Tgstation.Server.Host/Components/Interop/ChatEmbed.cs @@ -10,63 +10,63 @@ public sealed class ChatEmbed /// /// The title of the embed. /// - public string Title { get; set; } + public string? Title { get; set; } /// /// The description of the embed. /// - public string Description { get; set; } + public string? Description { get; set; } /// /// The URL of the embed. /// #pragma warning disable CA1056 // Uri properties should not be strings - public string Url { get; set; } + public string? Url { get; set; } #pragma warning restore CA1056 // Uri properties should not be strings /// /// The ISO 8601 timestamp of the embed. /// - public string Timestamp { get; set; } + public string? Timestamp { get; set; } /// /// The colour of the embed in the format hex "#AARRGGBB". /// - public string Colour { get; set; } + public string? Colour { get; set; } /// /// The . /// - public ChatEmbedFooter Footer { get; set; } + public ChatEmbedFooter? Footer { get; set; } /// /// The for an image. /// - public ChatEmbedMedia Image { get; set; } + public ChatEmbedMedia? Image { get; set; } /// /// The for a thumbnail. /// - public ChatEmbedMedia Thumbnail { get; set; } + public ChatEmbedMedia? Thumbnail { get; set; } /// /// The for a video. /// - public ChatEmbedMedia Video { get; set; } + public ChatEmbedMedia? Video { get; set; } /// /// The . /// - public ChatEmbedProvider Provider { get; set; } + public ChatEmbedProvider? Provider { get; set; } /// /// The . /// - public ChatEmbedAuthor Author { get; set; } + public ChatEmbedAuthor? Author { get; set; } /// /// The s. /// - public ICollection Fields { get; set; } + public ICollection? Fields { get; set; } } } diff --git a/src/Tgstation.Server.Host/Components/Interop/ChatEmbedAuthor.cs b/src/Tgstation.Server.Host/Components/Interop/ChatEmbedAuthor.cs index b79d38be5b5..d136b895b01 100644 --- a/src/Tgstation.Server.Host/Components/Interop/ChatEmbedAuthor.cs +++ b/src/Tgstation.Server.Host/Components/Interop/ChatEmbedAuthor.cs @@ -9,12 +9,12 @@ public sealed class ChatEmbedAuthor : ChatEmbedProvider /// Gets the icon URL of the author. /// #pragma warning disable CA1056 // Uri properties should not be strings - public string IconUrl { get; set; } + public string? IconUrl { get; set; } /// /// Gets the proxied icon URL of the thumbnail. /// - public string ProxyIconUrl { get; set; } + public string? ProxyIconUrl { get; set; } #pragma warning restore CA1056 // Uri properties should not be strings } } diff --git a/src/Tgstation.Server.Host/Components/Interop/ChatEmbedField.cs b/src/Tgstation.Server.Host/Components/Interop/ChatEmbedField.cs index 9d3ca0b864c..70b3c37c9fc 100644 --- a/src/Tgstation.Server.Host/Components/Interop/ChatEmbedField.cs +++ b/src/Tgstation.Server.Host/Components/Interop/ChatEmbedField.cs @@ -8,12 +8,12 @@ public sealed class ChatEmbedField /// /// Gets the name of the field. /// - public string Name { get; set; } + public string? Name { get; set; } /// /// Gets the value of the field. /// - public string Value { get; set; } + public string? Value { get; set; } /// /// Gets a value indicating whether the field should display inline. diff --git a/src/Tgstation.Server.Host/Components/Interop/ChatEmbedFooter.cs b/src/Tgstation.Server.Host/Components/Interop/ChatEmbedFooter.cs index 0df7a2c5d9c..57d45ec7eba 100644 --- a/src/Tgstation.Server.Host/Components/Interop/ChatEmbedFooter.cs +++ b/src/Tgstation.Server.Host/Components/Interop/ChatEmbedFooter.cs @@ -8,18 +8,18 @@ public sealed class ChatEmbedFooter /// /// Gets the text of the footer. /// - public string Text { get; set; } + public string? Text { get; set; } /// /// Gets the URL of the footer icon. Only supports http(s) and attachments. /// #pragma warning disable CA1056 // Uri properties should not be strings - public string IconUrl { get; set; } + public string? IconUrl { get; set; } /// /// Gets the proxied icon URL. /// - public string ProxyIconUrl { get; set; } + public string? ProxyIconUrl { get; set; } #pragma warning restore CA1056 // Uri properties should not be strings } } diff --git a/src/Tgstation.Server.Host/Components/Interop/ChatEmbedMedia.cs b/src/Tgstation.Server.Host/Components/Interop/ChatEmbedMedia.cs index 1cad4c4349b..c031a01e92b 100644 --- a/src/Tgstation.Server.Host/Components/Interop/ChatEmbedMedia.cs +++ b/src/Tgstation.Server.Host/Components/Interop/ChatEmbedMedia.cs @@ -9,12 +9,12 @@ public class ChatEmbedMedia /// Gets the source URL of the media. Only supports http(s) and attachments. /// #pragma warning disable CA1056 // Uri properties should not be strings - public string Url { get; set; } + public string? Url { get; set; } /// /// Gets the proxied URL of the media. /// - public string ProxyUrl { get; set; } + public string? ProxyUrl { get; set; } #pragma warning restore CA1056 // Uri properties should not be strings /// diff --git a/src/Tgstation.Server.Host/Components/Interop/ChatEmbedProvider.cs b/src/Tgstation.Server.Host/Components/Interop/ChatEmbedProvider.cs index 386a269a7d3..bc4dba30d76 100644 --- a/src/Tgstation.Server.Host/Components/Interop/ChatEmbedProvider.cs +++ b/src/Tgstation.Server.Host/Components/Interop/ChatEmbedProvider.cs @@ -8,13 +8,13 @@ public class ChatEmbedProvider /// /// Gets the name of the provider. /// - public string Name { get; set; } + public string? Name { get; set; } /// /// Gets the URL of the provider. /// #pragma warning disable CA1056 // Uri properties should not be strings - public string Url { get; set; } + public string? Url { get; set; } #pragma warning restore CA1056 // Uri properties should not be strings } } diff --git a/src/Tgstation.Server.Host/Components/Interop/ChatMessage.cs b/src/Tgstation.Server.Host/Components/Interop/ChatMessage.cs index 3d8900e4276..c84e44e6cc0 100644 --- a/src/Tgstation.Server.Host/Components/Interop/ChatMessage.cs +++ b/src/Tgstation.Server.Host/Components/Interop/ChatMessage.cs @@ -10,6 +10,6 @@ public sealed class ChatMessage : MessageContent /// /// The of s to sent the to. Must be safe to parse as s. /// - public ICollection ChannelIds { get; set; } + public ICollection? ChannelIds { get; set; } } } diff --git a/src/Tgstation.Server.Host/Components/Interop/ChunkData.cs b/src/Tgstation.Server.Host/Components/Interop/ChunkData.cs index ee6122d7649..409179b6606 100644 --- a/src/Tgstation.Server.Host/Components/Interop/ChunkData.cs +++ b/src/Tgstation.Server.Host/Components/Interop/ChunkData.cs @@ -14,6 +14,6 @@ public sealed class ChunkData : ChunkSetInfo /// /// The partial JSON payload of the chunk. /// - public string Payload { get; set; } + public string? Payload { get; set; } } } diff --git a/src/Tgstation.Server.Host/Components/Interop/Chunker.cs b/src/Tgstation.Server.Host/Components/Interop/Chunker.cs index d6a0c7c024e..e13cfd00bac 100644 --- a/src/Tgstation.Server.Host/Components/Interop/Chunker.cs +++ b/src/Tgstation.Server.Host/Components/Interop/Chunker.cs @@ -14,6 +14,11 @@ namespace Tgstation.Server.Host.Components.Interop /// abstract class Chunker { + /// + /// The for the . + /// + protected ILogger Logger { get; } + /// /// Gets a payload ID for use in a new . /// @@ -39,11 +44,6 @@ protected uint NextPayloadId /// uint highestSeenPayloadId; - /// - /// The for the . - /// - protected ILogger Logger { get; } - /// /// Initializes a new instance of the class. /// @@ -57,18 +57,19 @@ protected Chunker(ILogger logger) /// /// Process a given . /// - /// The of communication that was chunked. + /// The of communication that was chunked. /// The of expected. - /// The callback that receives the completed . + /// The callback that receives the completed . /// The callback that generates a for a given error. /// The . /// The for the operation. /// A resulting in the for the chunked request. - protected async ValueTask ProcessChunk( - Func> completionCallback, - Func chunkErrorCallback, - ChunkData chunk, + protected async ValueTask ProcessChunk( + Func> completionCallback, + Func chunkErrorCallback, + ChunkData? chunk, CancellationToken cancellationToken) + where TCommunication : class where TResponse : IMissingPayloadsCommunication, new() { if (chunk == null) @@ -80,6 +81,9 @@ protected async ValueTask ProcessChunk( if (!chunk.SequenceId.HasValue) return chunkErrorCallback("Missing chunk sequenceId!"); + if (chunk.Payload == null) + return chunkErrorCallback("Missing chunk payload!"); + ChunkSetInfo requestInfo; string[] payloads; lock (chunkSets) @@ -102,13 +106,13 @@ protected async ValueTask ProcessChunk( if (chunk.TotalChunks != requestInfo.TotalChunks) { - chunkSets.Remove(requestInfo.PayloadId.Value); + chunkSets.Remove(requestInfo.PayloadId!.Value); return chunkErrorCallback("Received differing total chunks for same payloadId! Invalidating payloadId!"); } if (payloads[chunk.SequenceId.Value] != null && payloads[chunk.SequenceId.Value] != chunk.Payload) { - chunkSets.Remove(requestInfo.PayloadId.Value); + chunkSets.Remove(requestInfo.PayloadId!.Value); return chunkErrorCallback("Received differing payload for same sequenceId! Invalidating payloadId!"); } @@ -125,21 +129,24 @@ protected async ValueTask ProcessChunk( }; Logger.LogTrace("Received all chunks for P{payloadId}, processing request...", requestInfo.PayloadId); - chunkSets.Remove(requestInfo.PayloadId.Value); + chunkSets.Remove(requestInfo.PayloadId!.Value); } - TCommnication completedCommunication; + TCommunication? completedCommunication; var fullCommunicationJson = String.Concat(payloads); try { - completedCommunication = JsonConvert.DeserializeObject(fullCommunicationJson, DMApiConstants.SerializerSettings); + completedCommunication = JsonConvert.DeserializeObject(fullCommunicationJson, DMApiConstants.SerializerSettings); } catch (Exception ex) { Logger.LogDebug(ex, "Bad chunked communication for payload {payloadId}!", requestInfo.PayloadId); - return chunkErrorCallback("Chunked request completed with bad JSON!"); + completedCommunication = null; } + if (completedCommunication == null) + return chunkErrorCallback("Chunked request completed with bad JSON!"); + return await completionCallback(completedCommunication, cancellationToken); } } diff --git a/src/Tgstation.Server.Host/Components/Interop/DMApiConstants.cs b/src/Tgstation.Server.Host/Components/Interop/DMApiConstants.cs index 9cca307361c..7386e856691 100644 --- a/src/Tgstation.Server.Host/Components/Interop/DMApiConstants.cs +++ b/src/Tgstation.Server.Host/Components/Interop/DMApiConstants.cs @@ -40,12 +40,12 @@ static class DMApiConstants public const uint MaximumBridgeRequestLength = 8198; /// - /// The maximum length in bytes of a payload. + /// The maximum length in bytes of a payload. /// public const uint MaximumTopicRequestLength = 65528; /// - /// The maximum length in bytes of a response. + /// The maximum length in bytes of a response. /// public const uint MaximumTopicResponseLength = 65529; @@ -57,7 +57,7 @@ static class DMApiConstants /// /// for use when communicating with the DMAPI. /// - public static readonly JsonSerializerSettings SerializerSettings = new () + public static readonly JsonSerializerSettings SerializerSettings = new() { ContractResolver = new DefaultContractResolver { diff --git a/src/Tgstation.Server.Host/Components/Interop/DMApiParameters.cs b/src/Tgstation.Server.Host/Components/Interop/DMApiParameters.cs index a6faad32928..64b774aeb51 100644 --- a/src/Tgstation.Server.Host/Components/Interop/DMApiParameters.cs +++ b/src/Tgstation.Server.Host/Components/Interop/DMApiParameters.cs @@ -1,4 +1,5 @@ -using System.ComponentModel.DataAnnotations; +using System; +using System.ComponentModel.DataAnnotations; namespace Tgstation.Server.Host.Components.Interop { @@ -12,5 +13,23 @@ public abstract class DMApiParameters /// [Required] public string AccessIdentifier { get; set; } + + /// + /// Initializes a new instance of the class. + /// + /// The value of . + public DMApiParameters(string accessIdentifier) + { + AccessIdentifier = accessIdentifier ?? throw new ArgumentNullException(nameof(accessIdentifier)); + } + + /// + /// Initializes a new instance of the class. + /// + /// For use by EFCore only. + protected DMApiParameters() + { + AccessIdentifier = null!; + } } } diff --git a/src/Tgstation.Server.Host/Components/Interop/DMApiResponse.cs b/src/Tgstation.Server.Host/Components/Interop/DMApiResponse.cs index bb1adf4925e..e6c75020639 100644 --- a/src/Tgstation.Server.Host/Components/Interop/DMApiResponse.cs +++ b/src/Tgstation.Server.Host/Components/Interop/DMApiResponse.cs @@ -8,6 +8,6 @@ public abstract class DMApiResponse /// /// Any errors in the client's parameters. /// - public string ErrorMessage { get; set; } + public string? ErrorMessage { get; set; } } } diff --git a/src/Tgstation.Server.Host/Components/Interop/IMissingPayloadsCommunication.cs b/src/Tgstation.Server.Host/Components/Interop/IMissingPayloadsCommunication.cs index 392cd7eecb9..7adf7d9cce0 100644 --- a/src/Tgstation.Server.Host/Components/Interop/IMissingPayloadsCommunication.cs +++ b/src/Tgstation.Server.Host/Components/Interop/IMissingPayloadsCommunication.cs @@ -10,6 +10,6 @@ interface IMissingPayloadsCommunication /// /// The s missing from a chunked request. /// - IReadOnlyCollection MissingChunks { get; set; } + IReadOnlyCollection? MissingChunks { get; set; } } } diff --git a/src/Tgstation.Server.Host/Components/Interop/MessageContent.cs b/src/Tgstation.Server.Host/Components/Interop/MessageContent.cs index 6d484a411b0..6b04fb10ae9 100644 --- a/src/Tgstation.Server.Host/Components/Interop/MessageContent.cs +++ b/src/Tgstation.Server.Host/Components/Interop/MessageContent.cs @@ -8,11 +8,11 @@ public class MessageContent /// /// The message . /// - public string Text { get; set; } + public string? Text { get; set; } /// /// The . /// - public ChatEmbed Embed { get; set; } + public ChatEmbed? Embed { get; set; } } } diff --git a/src/Tgstation.Server.Host/Components/Interop/Topic/ChunkedTopicParameters.cs b/src/Tgstation.Server.Host/Components/Interop/Topic/ChunkedTopicParameters.cs index 79353a66bb6..0940281ecea 100644 --- a/src/Tgstation.Server.Host/Components/Interop/Topic/ChunkedTopicParameters.cs +++ b/src/Tgstation.Server.Host/Components/Interop/Topic/ChunkedTopicParameters.cs @@ -8,7 +8,7 @@ namespace Tgstation.Server.Host.Components.Interop.Topic sealed class ChunkedTopicParameters : TopicParameters, IMissingPayloadsCommunication, IChunkPayloadId { /// - public IReadOnlyCollection MissingChunks { get; set; } + public IReadOnlyCollection? MissingChunks { get; set; } /// public uint? PayloadId { get; set; } diff --git a/src/Tgstation.Server.Host/Components/Interop/Topic/EventNotification.cs b/src/Tgstation.Server.Host/Components/Interop/Topic/EventNotification.cs index 54a8d496a5a..75eb3bc9f41 100644 --- a/src/Tgstation.Server.Host/Components/Interop/Topic/EventNotification.cs +++ b/src/Tgstation.Server.Host/Components/Interop/Topic/EventNotification.cs @@ -20,14 +20,14 @@ sealed class EventNotification /// /// The set of parameters. /// - public IReadOnlyCollection Parameters { get; } + public IReadOnlyCollection Parameters { get; } /// /// Initializes a new instance of the class. /// /// The value of . /// The that forms the value of . - public EventNotification(EventType eventType, IEnumerable parameters = null) + public EventNotification(EventType eventType, IEnumerable parameters) { Type = eventType; Parameters = parameters?.ToList() ?? throw new ArgumentNullException(nameof(parameters)); diff --git a/src/Tgstation.Server.Host/Components/Interop/Topic/TopicParameters.cs b/src/Tgstation.Server.Host/Components/Interop/Topic/TopicParameters.cs index fff5453b0de..bdc8405b022 100644 --- a/src/Tgstation.Server.Host/Components/Interop/Topic/TopicParameters.cs +++ b/src/Tgstation.Server.Host/Components/Interop/Topic/TopicParameters.cs @@ -20,12 +20,12 @@ class TopicParameters : DMApiParameters /// /// The for requests. /// - public ChatCommand ChatCommand { get; } + public ChatCommand? ChatCommand { get; } /// /// The for requests. /// - public EventNotification EventNotification { get; } + public EventNotification? EventNotification { get; } /// /// The new port for or requests. @@ -40,27 +40,27 @@ class TopicParameters : DMApiParameters /// /// The new for requests. /// - public string NewInstanceName { get; } + public string? NewInstanceName { get; } /// /// The message to broadcast for requests. /// - public string BroadcastMessage { get; } + public string? BroadcastMessage { get; } /// /// The for requests. /// - public ChatUpdate ChatUpdate { get; } + public ChatUpdate? ChatUpdate { get; } /// /// The new server after a reattach. /// - public Version NewServerVersion { get; } + public Version? NewServerVersion { get; } /// /// The for a partial request. /// - public ChunkData Chunk { get; } + public ChunkData? Chunk { get; } /// /// Whether or not the constitute a priority request. @@ -88,7 +88,7 @@ or TopicCommandType.HealthCheck /// The value of . /// The created . public static TopicParameters CreateInstanceRenamedTopicParameters(string newInstanceName) - => new ( + => new( newInstanceName ?? throw new ArgumentNullException(nameof(newInstanceName)), TopicCommandType.InstanceRenamed); @@ -98,7 +98,7 @@ public static TopicParameters CreateInstanceRenamedTopicParameters(string newIns /// The value of . /// The created . public static TopicParameters CreateBroadcastParameters(string broadcastMessage) - => new ( + => new( broadcastMessage ?? throw new ArgumentNullException(nameof(broadcastMessage)), TopicCommandType.Broadcast); @@ -188,6 +188,7 @@ public TopicParameters() /// /// The value of . protected TopicParameters(TopicCommandType commandType) + : base(String.Empty) // access identifier gets set before send { CommandType = commandType; } diff --git a/src/Tgstation.Server.Host/Components/Interop/Topic/TopicResponse.cs b/src/Tgstation.Server.Host/Components/Interop/Topic/TopicResponse.cs index f108aba5432..12db6f3ef17 100644 --- a/src/Tgstation.Server.Host/Components/Interop/Topic/TopicResponse.cs +++ b/src/Tgstation.Server.Host/Components/Interop/Topic/TopicResponse.cs @@ -12,29 +12,29 @@ sealed class TopicResponse : DMApiResponse, IMissingPayloadsCommunication /// /// The text to reply with as the result of a request, if any. Deprecated circa Interop 5.4.0. /// - public string CommandResponseMessage { get; set; } + public string? CommandResponseMessage { get; set; } /// /// The response from a . Added in Interop 5.4.0. /// - public ChatMessage CommandResponse { get; set; } + public ChatMessage? CommandResponse { get; set; } /// /// The s to send as the result of a request, if any. /// - public ICollection ChatResponses { get; set; } + public ICollection? ChatResponses { get; set; } /// /// The DMAPI s for requests. /// - public ICollection CustomCommands { get; set; } + public ICollection? CustomCommands { get; set; } /// /// The for a partial response. /// - public ChunkData Chunk { get; set; } + public ChunkData? Chunk { get; set; } /// - public IReadOnlyCollection MissingChunks { get; set; } + public IReadOnlyCollection? MissingChunks { get; set; } } } diff --git a/src/Tgstation.Server.Host/Components/README.md b/src/Tgstation.Server.Host/Components/README.md index e13f2a3dfbc..2106f47dd7a 100644 --- a/src/Tgstation.Server.Host/Components/README.md +++ b/src/Tgstation.Server.Host/Components/README.md @@ -20,4 +20,4 @@ While the database represents stored instance data, in component code an instanc `IInstance`s ([implementation](./Instance.cs)) are created via the [IInstanceFactory](./IInstanceFactory.cs) ([implementation](./InstanceFactory.cs)) and are generally controlled via the [IInstanceOperations](./IInstanceOperations.cs) interface (implemented in the `InstanceManager`). -Many classes in here implement [IHostedService](https://docs.microsoft.com/en-us/aspnet/core/fundamentals/host/hosted-services?view=aspnetcore-6.0&tabs=visual-studio), `InstanceManager` being the only one that is called by the ASP.NET runtime. In the case of instances `StartAsync()` is called when an `Instance` is being brought online (from server startup or user request). The `Instance` handles calling `StartAsync()` on its various subcomponents that need it. When an `Instance` is being brought offline (from server shutdown/restart/update or user request) the same pattern is followed calling `StopAsync()`. +Many classes in here implement [IHostedService](https://docs.microsoft.com/en-us/aspnet/core/fundamentals/host/hosted-services?view=aspnetcore-8.0&tabs=visual-studio), `InstanceManager` being the only one that is called by the ASP.NET runtime. In the case of instances `StartAsync()` is called when an `Instance` is being brought online (from server startup or user request). The `Instance` handles calling `StartAsync()` on its various subcomponents that need it. When an `Instance` is being brought offline (from server shutdown/restart/update or user request) the same pattern is followed calling `StopAsync()`. diff --git a/src/Tgstation.Server.Host/Components/Repository/DefaultGitRemoteFeatures.cs b/src/Tgstation.Server.Host/Components/Repository/DefaultGitRemoteFeatures.cs index ead4007720f..05d9408a677 100644 --- a/src/Tgstation.Server.Host/Components/Repository/DefaultGitRemoteFeatures.cs +++ b/src/Tgstation.Server.Host/Components/Repository/DefaultGitRemoteFeatures.cs @@ -21,10 +21,10 @@ sealed class DefaultGitRemoteFeatures : IGitRemoteFeatures public RemoteGitProvider? RemoteGitProvider => Api.Models.RemoteGitProvider.Unknown; /// - public string RemoteRepositoryOwner => null; + public string? RemoteRepositoryOwner => null; /// - public string RemoteRepositoryName => null; + public string? RemoteRepositoryName => null; /// public ValueTask GetTestMerge( diff --git a/src/Tgstation.Server.Host/Components/Repository/GitHubRemoteFeatures.cs b/src/Tgstation.Server.Host/Components/Repository/GitHubRemoteFeatures.cs index 6e5d5c4ec06..7a21ebde2b9 100644 --- a/src/Tgstation.Server.Host/Components/Repository/GitHubRemoteFeatures.cs +++ b/src/Tgstation.Server.Host/Components/Repository/GitHubRemoteFeatures.cs @@ -25,12 +25,6 @@ sealed class GitHubRemoteFeatures : GitRemoteFeaturesBase /// public override RemoteGitProvider? RemoteGitProvider => Api.Models.RemoteGitProvider.GitHub; - /// - public override string RemoteRepositoryOwner { get; } - - /// - public override string RemoteRepositoryName { get; } - /// /// The for the . /// @@ -46,13 +40,6 @@ public GitHubRemoteFeatures(IGitHubServiceFactory gitHubServiceFactory, ILogger< : base(logger, remoteUrl) { this.gitHubServiceFactory = gitHubServiceFactory ?? throw new ArgumentNullException(nameof(gitHubServiceFactory)); - - ArgumentNullException.ThrowIfNull(remoteUrl); - - RemoteRepositoryOwner = remoteUrl.Segments[1].TrimEnd('/'); - RemoteRepositoryName = remoteUrl.Segments[2].TrimEnd('/'); - if (RemoteRepositoryName.EndsWith(".git", StringComparison.OrdinalIgnoreCase)) - RemoteRepositoryName = RemoteRepositoryName[0..^4]; } /// @@ -65,9 +52,9 @@ public GitHubRemoteFeatures(IGitHubServiceFactory gitHubServiceFactory, ILogger< ? gitHubServiceFactory.CreateService(repositorySettings.AccessToken) : gitHubServiceFactory.CreateService(); - PullRequest pr = null; - ApiException exception = null; - string errorMessage = null; + PullRequest? pr = null; + ApiException? exception = null; + string? errorMessage = null; try { pr = await gitHubService.GetPullRequest(RemoteRepositoryOwner, RemoteRepositoryName, parameters.Number, cancellationToken); diff --git a/src/Tgstation.Server.Host/Components/Repository/GitLabRemoteFeatures.cs b/src/Tgstation.Server.Host/Components/Repository/GitLabRemoteFeatures.cs index def1adbaaf6..9e468b06e66 100644 --- a/src/Tgstation.Server.Host/Components/Repository/GitLabRemoteFeatures.cs +++ b/src/Tgstation.Server.Host/Components/Repository/GitLabRemoteFeatures.cs @@ -30,12 +30,6 @@ sealed class GitLabRemoteFeatures : GitRemoteFeaturesBase /// public override RemoteGitProvider? RemoteGitProvider => Api.Models.RemoteGitProvider.GitLab; - /// - public override string RemoteRepositoryOwner { get; } - - /// - public override string RemoteRepositoryName { get; } - /// /// Initializes a new instance of the class. /// @@ -44,10 +38,6 @@ sealed class GitLabRemoteFeatures : GitRemoteFeaturesBase public GitLabRemoteFeatures(ILogger logger, Uri remoteUrl) : base(logger, remoteUrl) { - RemoteRepositoryOwner = remoteUrl.Segments[1].TrimEnd('/'); - RemoteRepositoryName = remoteUrl.Segments[2].TrimEnd('/'); - if (RemoteRepositoryName.EndsWith(".git", StringComparison.OrdinalIgnoreCase)) - RemoteRepositoryName = RemoteRepositoryName[0..^4]; } /// diff --git a/src/Tgstation.Server.Host/Components/Repository/GitRemoteFeaturesBase.cs b/src/Tgstation.Server.Host/Components/Repository/GitRemoteFeaturesBase.cs index 1b1c5337afb..d42e9287929 100644 --- a/src/Tgstation.Server.Host/Components/Repository/GitRemoteFeaturesBase.cs +++ b/src/Tgstation.Server.Host/Components/Repository/GitRemoteFeaturesBase.cs @@ -25,10 +25,10 @@ abstract class GitRemoteFeaturesBase : IGitRemoteFeatures public abstract RemoteGitProvider? RemoteGitProvider { get; } /// - public abstract string RemoteRepositoryOwner { get; } + public string RemoteRepositoryOwner { get; } /// - public abstract string RemoteRepositoryName { get; } + public string RemoteRepositoryName { get; } /// /// The for the . @@ -50,6 +50,11 @@ public GitRemoteFeaturesBase(ILogger logger, Uri remoteUr Logger = logger ?? throw new ArgumentNullException(nameof(logger)); ArgumentNullException.ThrowIfNull(remoteUrl); + RemoteRepositoryOwner = remoteUrl.Segments[1].TrimEnd('/'); + RemoteRepositoryName = remoteUrl.Segments[2].TrimEnd('/'); + if (RemoteRepositoryName.EndsWith(".git", StringComparison.OrdinalIgnoreCase)) + RemoteRepositoryName = RemoteRepositoryName[0..^4]; + cachedLookups = new Dictionary(); } @@ -62,18 +67,18 @@ public GitRemoteFeaturesBase(ILogger logger, Uri remoteUr ArgumentNullException.ThrowIfNull(parameters); ArgumentNullException.ThrowIfNull(repositorySettings); - Models.TestMerge result; + Models.TestMerge? result; lock (cachedLookups) if (cachedLookups.TryGetValue(parameters, out result)) - Logger.LogTrace("Using cache for test merge #{0}", parameters.Number); + Logger.LogTrace("Using cache for test merge #{testMergeNumber}", parameters.Number); if (result == null) { - Logger.LogTrace("Retrieving metadata for test merge #{0}...", parameters.Number); + Logger.LogTrace("Retrieving metadata for test merge #{testMergeNumber}...", parameters.Number); result = await GetTestMergeImpl(parameters, repositorySettings, cancellationToken); lock (cachedLookups) if (!cachedLookups.TryAdd(parameters, result)) - Logger.LogError("Race condition on adding test merge #{0}!", parameters.Number); + Logger.LogError("Race condition on adding test merge #{testMergeNumber}!", parameters.Number); } return result; diff --git a/src/Tgstation.Server.Host/Components/Repository/GitRemoteFeaturesFactory.cs b/src/Tgstation.Server.Host/Components/Repository/GitRemoteFeaturesFactory.cs index ab5dcb45ec9..70694d0b940 100644 --- a/src/Tgstation.Server.Host/Components/Repository/GitRemoteFeaturesFactory.cs +++ b/src/Tgstation.Server.Host/Components/Repository/GitRemoteFeaturesFactory.cs @@ -78,7 +78,7 @@ public RemoteGitProvider ParseRemoteGitProviderFromOrigin(Uri origin) case "GIT.GITLAB.COM": return RemoteGitProvider.GitLab; default: - logger.LogTrace("Unknown git remote: {0}", origin); + logger.LogDebug("Unknown git remote: {origin}", origin); return RemoteGitProvider.Unknown; } } diff --git a/src/Tgstation.Server.Host/Components/Repository/ICredentialsProvider.cs b/src/Tgstation.Server.Host/Components/Repository/ICredentialsProvider.cs index cef08354c67..1324c35320b 100644 --- a/src/Tgstation.Server.Host/Components/Repository/ICredentialsProvider.cs +++ b/src/Tgstation.Server.Host/Components/Repository/ICredentialsProvider.cs @@ -16,7 +16,7 @@ interface ICredentialsProvider /// The optional username to use in the . /// The optional password to use in the . /// A new . - CredentialsHandler GenerateCredentialsHandler(string username, string password); + CredentialsHandler GenerateCredentialsHandler(string? username, string? password); /// /// Rethrow the authentication failure message as a if it is one. diff --git a/src/Tgstation.Server.Host/Components/Repository/IRepository.cs b/src/Tgstation.Server.Host/Components/Repository/IRepository.cs index 5cce4208ea2..65bcd1d61ed 100644 --- a/src/Tgstation.Server.Host/Components/Repository/IRepository.cs +++ b/src/Tgstation.Server.Host/Components/Repository/IRepository.cs @@ -44,18 +44,18 @@ public interface IRepository : IGitRemoteAdditionalInformation, IDisposable /// Checks out a given . /// /// The sha or reference to checkout. - /// The username used for fetching from submodule repositories. - /// The password used for fetching from submodule repositories. + /// The optional username used for fetching from submodule repositories. + /// The optional password used for fetching from submodule repositories. /// If a submodule update should be attempted after the merge. - /// The to report progress of the operation. + /// The optional to report progress of the operation. /// The for the operation. /// A representing the running operation. ValueTask CheckoutObject( string committish, - string username, - string password, + string? username, + string? password, bool updateSubmodules, - JobProgressReporter progressReporter, + JobProgressReporter? progressReporter, CancellationToken cancellationToken); /// @@ -64,8 +64,8 @@ ValueTask CheckoutObject( /// The of the pull request. /// The name of the merge committer. /// The e-mail of the merge committer. - /// The username used to fetch from the origin and submodule repositories. - /// The password used to fetch from the origin and submodule repositories. + /// The optional username used to fetch from the origin and submodule repositories. + /// The optional password used to fetch from the origin and submodule repositories. /// If a submodule update should be attempted after the merge. /// The to report progress of the operation. /// The for the operation. @@ -74,8 +74,8 @@ ValueTask AddTestMerge( TestMergeParameters testMergeParameters, string committerName, string committerEmail, - string username, - string password, + string? username, + string? password, bool updateSubmodules, JobProgressReporter progressReporter, CancellationToken cancellationToken); @@ -83,16 +83,16 @@ ValueTask AddTestMerge( /// /// Fetch commits from the origin repository. /// - /// The to report progress of the operation. - /// The username to fetch from the origin repository. - /// The password to fetch from the origin repository. + /// The optional to report progress of the operation. + /// The optional username to fetch from the origin repository. + /// The optional password to fetch from the origin repository. /// If any events created should be marked as part of the deployment pipeline. /// The for the operation. /// A representing the running operation. ValueTask FetchOrigin( - JobProgressReporter progressReporter, - string username, - string password, + JobProgressReporter? progressReporter, + string? username, + string? password, bool deploymentPipeline, CancellationToken cancellationToken); @@ -100,16 +100,16 @@ ValueTask FetchOrigin( /// Requires the current HEAD to be a tracked reference. Hard resets the reference to what it tracks on the origin repository. /// /// The to report progress of the operation. - /// The username used for fetching from submodule repositories. - /// The password used for fetching from submodule repositories. + /// The optional username used for fetching from submodule repositories. + /// The optional password used for fetching from submodule repositories. /// If a submodule update should be attempted after the merge. /// If any events created should be marked as part of the deployment pipeline. /// The for the operation. /// A resulting in the SHA of the new HEAD. ValueTask ResetToOrigin( JobProgressReporter progressReporter, - string username, - string password, + string? username, + string? password, bool updateSubmodules, bool deploymentPipeline, CancellationToken cancellationToken); @@ -151,10 +151,10 @@ ValueTask ResetToOrigin( /// If any events created should be marked as part of the deployment pipeline. /// The for the operation. /// A resulting in if commits were pushed to the tracked origin reference, otherwise. - ValueTask Sychronize( + ValueTask Synchronize( JobProgressReporter progressReporter, - string username, - string password, + string? username, + string? password, string committerName, string committerEmail, bool synchronizeTrackedBranch, @@ -170,13 +170,13 @@ ValueTask Sychronize( ValueTask CopyTo(string path, CancellationToken cancellationToken); /// - /// Check if a given is a parent of the current . + /// Check if a given is a parent of the current . /// - /// The SHA to check. + /// The committish of the SHA to check. /// The for the operation. - /// A resulting in if is a parent of , otherwise. + /// A resulting in if is a parent of , otherwise. /// This function is NOT reentrant. - Task ShaIsParent(string sha, CancellationToken cancellationToken); + Task CommittishIsParent(string committish, CancellationToken cancellationToken); /// /// Get the tracked reference's current SHA. diff --git a/src/Tgstation.Server.Host/Components/Repository/IRepositoryManager.cs b/src/Tgstation.Server.Host/Components/Repository/IRepositoryManager.cs index f7c4a546083..596301611ac 100644 --- a/src/Tgstation.Server.Host/Components/Repository/IRepositoryManager.cs +++ b/src/Tgstation.Server.Host/Components/Repository/IRepositoryManager.cs @@ -26,25 +26,25 @@ public interface IRepositoryManager : IDisposable /// /// The for the operation. /// A resulting in the loaded if it exists, otherwise. - ValueTask LoadRepository(CancellationToken cancellationToken); + ValueTask LoadRepository(CancellationToken cancellationToken); /// /// Clone the repository at . /// /// The of the remote repository to clone. - /// The branch to clone. - /// The username to clone from . - /// The password to clone from . - /// The for progress of the clone. + /// The optional branch to clone. + /// The optional username to clone from . + /// The optional password to clone from . + /// The optional for progress of the clone. /// If submodules should be recusively cloned and initialized. /// The for the operation. /// A resulting i the newly cloned , if one already exists. - ValueTask CloneRepository( + ValueTask CloneRepository( Uri url, - string initialBranch, - string username, - string password, - JobProgressReporter progressReporter, + string? initialBranch, + string? username, + string? password, + JobProgressReporter? progressReporter, bool recurseSubmodules, CancellationToken cancellationToken); diff --git a/src/Tgstation.Server.Host/Components/Repository/IRepositoryManagerFactory.cs b/src/Tgstation.Server.Host/Components/Repository/IRepositoryManagerFactory.cs new file mode 100644 index 00000000000..0e8c54c70e9 --- /dev/null +++ b/src/Tgstation.Server.Host/Components/Repository/IRepositoryManagerFactory.cs @@ -0,0 +1,19 @@ +using Tgstation.Server.Host.Components.Events; +using Tgstation.Server.Host.IO; + +namespace Tgstation.Server.Host.Components.Repository +{ + /// + /// Factory for creating s. + /// + interface IRepositoryManagerFactory : IComponentService + { + /// + /// Create a . + /// + /// The to use. + /// The to use. + /// A new . + IRepositoryManager CreateRepositoryManager(IIOManager ioManager, IEventConsumer eventConsumer); + } +} diff --git a/src/Tgstation.Server.Host/Components/Repository/LibGit2RepositoryFactory.cs b/src/Tgstation.Server.Host/Components/Repository/LibGit2RepositoryFactory.cs index 83011a61b1a..418758ebe73 100644 --- a/src/Tgstation.Server.Host/Components/Repository/LibGit2RepositoryFactory.cs +++ b/src/Tgstation.Server.Host/Components/Repository/LibGit2RepositoryFactory.cs @@ -80,7 +80,7 @@ public Task Clone(Uri url, CloneOptions cloneOptions, string path, CancellationT TaskScheduler.Current); /// - public CredentialsHandler GenerateCredentialsHandler(string username, string password) => (a, b, supportedCredentialTypes) => + public CredentialsHandler GenerateCredentialsHandler(string? username, string? password) => (a, b, supportedCredentialTypes) => { var hasCreds = username != null; var supportsUserPass = supportedCredentialTypes.HasFlag(SupportedCredentialTypes.UsernamePassword); diff --git a/src/Tgstation.Server.Host/Components/Repository/Repository.cs b/src/Tgstation.Server.Host/Components/Repository/Repository.cs index ea7844c2a80..5518c1a44d3 100644 --- a/src/Tgstation.Server.Host/Components/Repository/Repository.cs +++ b/src/Tgstation.Server.Host/Components/Repository/Repository.cs @@ -17,12 +17,13 @@ using Tgstation.Server.Host.Extensions; using Tgstation.Server.Host.IO; using Tgstation.Server.Host.Jobs; +using Tgstation.Server.Host.Utils; namespace Tgstation.Server.Host.Components.Repository { /// #pragma warning disable CA1506 // TODO: Decomplexify - sealed class Repository : IRepository + sealed class Repository : DisposeInvoker, IRepository { /// /// The default username for committers. @@ -53,10 +54,10 @@ sealed class Repository : IRepository public RemoteGitProvider? RemoteGitProvider => gitRemoteFeatures.RemoteGitProvider; /// - public string RemoteRepositoryOwner => gitRemoteFeatures.RemoteRepositoryOwner; + public string? RemoteRepositoryOwner => gitRemoteFeatures.RemoteRepositoryOwner; /// - public string RemoteRepositoryName => gitRemoteFeatures.RemoteRepositoryName; + public string? RemoteRepositoryName => gitRemoteFeatures.RemoteRepositoryName; /// public bool Tracking => Reference != null && libGitRepo.Head.IsTracking; @@ -68,7 +69,7 @@ sealed class Repository : IRepository public string Reference => libGitRepo.Head.FriendlyName; /// - public Uri Origin => new (libGitRepo.Network.Remotes.First().Url); + public Uri Origin => new(libGitRepo.Network.Remotes.First().Url); /// /// The for the . @@ -115,16 +116,6 @@ sealed class Repository : IRepository /// readonly GeneralConfiguration generalConfiguration; - /// - /// to be taken when is called. - /// - readonly Action onDispose; - - /// - /// If the was disposed. - /// - bool disposed; - /// /// Initializes a new instance of the class. /// @@ -137,7 +128,7 @@ sealed class Repository : IRepository /// The to provide the value of . /// The value of . /// The value of . - /// The value if . + /// The action for the . public Repository( LibGit2Sharp.IRepository libGitRepo, ILibGit2Commands commands, @@ -148,7 +139,8 @@ public Repository( IGitRemoteFeaturesFactory gitRemoteFeaturesFactory, ILogger logger, GeneralConfiguration generalConfiguration, - Action onDispose) + Action disposeAction) + : base(disposeAction) { this.libGitRepo = libGitRepo ?? throw new ArgumentNullException(nameof(libGitRepo)); this.commands = commands ?? throw new ArgumentNullException(nameof(commands)); @@ -160,35 +152,18 @@ public Repository( this.logger = logger ?? throw new ArgumentNullException(nameof(logger)); this.generalConfiguration = generalConfiguration ?? throw new ArgumentNullException(nameof(generalConfiguration)); - this.onDispose = onDispose ?? throw new ArgumentNullException(nameof(onDispose)); gitRemoteFeatures = gitRemoteFeaturesFactory.CreateGitRemoteFeatures(this); } - /// - public void Dispose() - { - lock (onDispose) - { - if (disposed) - return; - - disposed = true; - } - - logger.LogTrace("Disposing..."); - libGitRepo.Dispose(); - onDispose(); - } - /// #pragma warning disable CA1506 // TODO: Decomplexify public async ValueTask AddTestMerge( TestMergeParameters testMergeParameters, string committerName, string committerEmail, - string username, - string password, + string? username, + string? password, bool updateSubmodules, JobProgressReporter progressReporter, CancellationToken cancellationToken) @@ -227,12 +202,12 @@ public async ValueTask AddTestMerge( var originalCommit = libGitRepo.Head; - MergeResult result = null; + MergeResult? result = null; var progressFactor = 1.0 / (updateSubmodules ? 3 : 2); var sig = new Signature(new Identity(committerName, committerEmail), DateTimeOffset.UtcNow); - List conflictedPaths = null; + List? conflictedPaths = null; await Task.Factory.StartNew( () => { @@ -247,20 +222,16 @@ await Task.Factory.StartNew( libGitRepo, refSpecList, remote, - new FetchOptions - { - Prune = true, - OnProgress = (a) => !cancellationToken.IsCancellationRequested, - OnTransferProgress = TransferProgressHandler( - progressReporter.CreateSection($"Fetch {refSpec}", progressFactor), - cancellationToken), - OnUpdateTips = (a, b, c) => !cancellationToken.IsCancellationRequested, - CredentialsProvider = credentialsProvider.GenerateCredentialsHandler(username, password), - }, + new FetchOptions().Hydrate( + logger, + progressReporter.CreateSection($"Fetch {refSpec}", progressFactor), + credentialsProvider.GenerateCredentialsHandler(username, password), + cancellationToken), logMessage); } - catch (UserCancelledException) + catch (UserCancelledException ex) { + logger.LogTrace(ex, "Suppressing fetch cancel exception"); } catch (LibGit2SharpException ex) { @@ -320,17 +291,17 @@ await Task.Factory.StartNew( DefaultIOManager.BlockingTaskCreationOptions, TaskScheduler.Current); - if (result.Status == MergeStatus.Conflicts) + if (result!.Status == MergeStatus.Conflicts) { var arguments = new List { originalCommit.Tip.Sha, - testMergeParameters.TargetCommitSha, + testMergeParameters.TargetCommitSha!, originalCommit.FriendlyName ?? UnknownReference, testMergeBranchName, }; - arguments.AddRange(conflictedPaths); + arguments.AddRange(conflictedPaths!); await eventConsumer.HandleEvent( EventType.RepoMergeConflict, @@ -369,10 +340,10 @@ await UpdateSubmodules( await eventConsumer.HandleEvent( EventType.RepoAddTestMerge, - new List + new List { testMergeParameters.Number.ToString(CultureInfo.InvariantCulture), - testMergeParameters.TargetCommitSha, + testMergeParameters.TargetCommitSha!, testMergeParameters.Comment, }, false, @@ -388,14 +359,14 @@ await eventConsumer.HandleEvent( /// public async ValueTask CheckoutObject( string committish, - string username, - string password, + string? username, + string? password, bool updateSubmodules, - JobProgressReporter progressReporter, + JobProgressReporter? progressReporter, CancellationToken cancellationToken) { ArgumentNullException.ThrowIfNull(committish); - ArgumentNullException.ThrowIfNull(progressReporter); + logger.LogDebug("Checkout object: {committish}...", committish); await eventConsumer.HandleEvent(EventType.RepoCheckout, new List { committish }, false, cancellationToken); await Task.Factory.StartNew( @@ -404,7 +375,7 @@ await Task.Factory.StartNew( libGitRepo.RemoveUntrackedFiles(); RawCheckout( committish, - progressReporter.CreateSection(null, updateSubmodules ? 2.0 / 3 : 1.0), + progressReporter?.CreateSection(null, updateSubmodules ? 2.0 / 3 : 1.0), cancellationToken); }, cancellationToken, @@ -413,7 +384,7 @@ await Task.Factory.StartNew( if (updateSubmodules) await UpdateSubmodules( - progressReporter.CreateSection(null, 1.0 / 3), + progressReporter?.CreateSection(null, 1.0 / 3), username, password, false, @@ -422,13 +393,12 @@ await UpdateSubmodules( /// public async ValueTask FetchOrigin( - JobProgressReporter progressReporter, - string username, - string password, + JobProgressReporter? progressReporter, + string? username, + string? password, bool deploymentPipeline, CancellationToken cancellationToken) { - ArgumentNullException.ThrowIfNull(progressReporter); logger.LogDebug("Fetch origin..."); await eventConsumer.HandleEvent(EventType.RepoFetch, Enumerable.Empty(), deploymentPipeline, cancellationToken); await Task.Factory.StartNew( @@ -437,21 +407,23 @@ await Task.Factory.StartNew( var remote = libGitRepo.Network.Remotes.First(); try { + var fetchOptions = new FetchOptions + { + Prune = true, + TagFetchMode = TagFetchMode.All, + }.Hydrate( + logger, + progressReporter?.CreateSection("Fetch Origin", 1.0), + credentialsProvider.GenerateCredentialsHandler(username, password), + cancellationToken); + commands.Fetch( libGitRepo, remote .FetchRefSpecs .Select(x => x.Specification), remote, - new FetchOptions - { - Prune = true, - OnProgress = (a) => !cancellationToken.IsCancellationRequested, - OnTransferProgress = TransferProgressHandler(progressReporter.CreateSection("Fetch Origin", 1.0), cancellationToken), - OnUpdateTips = (a, b, c) => !cancellationToken.IsCancellationRequested, - CredentialsProvider = credentialsProvider.GenerateCredentialsHandler(username, password), - TagFetchMode = TagFetchMode.All, - }, + fetchOptions, "Fetch origin commits"); } catch (UserCancelledException) @@ -471,8 +443,8 @@ await Task.Factory.StartNew( /// public async ValueTask ResetToOrigin( JobProgressReporter progressReporter, - string username, - string password, + string? username, + string? password, bool updateSubmodules, bool deploymentPipeline, CancellationToken cancellationToken) @@ -569,8 +541,8 @@ public Task GetOriginSha(CancellationToken cancellationToken) => Task.Fa { ArgumentNullException.ThrowIfNull(progressReporter); - MergeResult result = null; - Branch trackedBranch = null; + MergeResult? result = null; + Branch? trackedBranch = null; var oldHead = libGitRepo.Head; var oldTip = oldHead.Tip; @@ -619,14 +591,14 @@ await Task.Factory.StartNew( DefaultIOManager.BlockingTaskCreationOptions, TaskScheduler.Current); - if (result.Status == MergeStatus.Conflicts) + if (result!.Status == MergeStatus.Conflicts) { await eventConsumer.HandleEvent( EventType.RepoMergeConflict, new List { oldTip.Sha, - trackedBranch.Tip.Sha, + trackedBranch!.Tip.Sha, oldHead.FriendlyName ?? UnknownReference, trackedBranch.FriendlyName, }, @@ -639,10 +611,10 @@ await eventConsumer.HandleEvent( } /// - public async ValueTask Sychronize( + public async ValueTask Synchronize( JobProgressReporter progressReporter, - string username, - string password, + string? username, + string? password, string committerName, string committerEmail, bool synchronizeTrackedBranch, @@ -789,16 +761,34 @@ public Task IsSha(string committish, CancellationToken cancellationToken) TaskScheduler.Current); /// - public Task ShaIsParent(string sha, CancellationToken cancellationToken) => Task.Factory.StartNew( + public Task CommittishIsParent(string committish, CancellationToken cancellationToken) => Task.Factory.StartNew( () => { - var targetCommit = libGitRepo.Lookup(sha); - if (targetCommit == null) + var targetObject = libGitRepo.Lookup(committish); + if (targetObject == null) { - logger.LogTrace("Commit {sha} not found in repository", sha); + logger.LogTrace("Committish {committish} not found in repository", committish); return false; } + if (targetObject is not Commit targetCommit) + { + if (targetObject is not TagAnnotation) + { + logger.LogTrace("Committish {committish} is a {type} and does not point to a commit!", committish, targetObject.GetType().Name); + return false; + } + + targetCommit = targetObject.Peel(); + if (targetCommit == null) + { + logger.LogError( + "TagAnnotation {committish} was found but the commit associated with it could not be found in repository!", + committish); + return false; + } + } + cancellationToken.ThrowIfCancellationRequested(); var startSha = Head; var mergeResult = libGitRepo.Merge( @@ -852,27 +842,39 @@ public Task TimestampCommit(string sha, CancellationToken cancel DefaultIOManager.BlockingTaskCreationOptions, TaskScheduler.Current); + /// + protected override void DisposeImpl() + { + logger.LogTrace("Disposing..."); + libGitRepo.Dispose(); + base.DisposeImpl(); + } + /// /// Runs a blocking force checkout to . /// /// The committish to checkout. - /// The for the operation. + /// The optional for the operation. /// The for the operation. - void RawCheckout(string committish, JobProgressReporter progressReporter, CancellationToken cancellationToken) + void RawCheckout(string committish, JobProgressReporter? progressReporter, CancellationToken cancellationToken) { logger.LogTrace("Checkout: {committish}", committish); - var stage = $"Checkout {committish}"; - progressReporter = progressReporter.CreateSection(stage, 1.0); - progressReporter.ReportProgress(0); - cancellationToken.ThrowIfCancellationRequested(); - var checkoutOptions = new CheckoutOptions { CheckoutModifiers = CheckoutModifiers.Force, - OnCheckoutProgress = CheckoutProgressHandler(progressReporter), }; + if (progressReporter != null) + { + var stage = $"Checkout {committish}"; + progressReporter = progressReporter.CreateSection(stage, 1.0); + progressReporter.ReportProgress(0); + checkoutOptions.OnCheckoutProgress = CheckoutProgressHandler(progressReporter); + } + + cancellationToken.ThrowIfCancellationRequested(); + void RunCheckout() => commands.Checkout( libGitRepo, checkoutOptions, @@ -988,16 +990,16 @@ PushOptions GeneratePushOptions(JobProgressReporter progressReporter, string use /// /// Recusively update all s in the . /// - /// of the operation. - /// The username for the . - /// The password for the . + /// Optional of the operation. + /// The optional username for the . + /// The optional password for the . /// If any events created should be marked as part of the deployment pipeline. /// The for the operation. /// A representing the running operation. async ValueTask UpdateSubmodules( - JobProgressReporter progressReporter, - string username, - string password, + JobProgressReporter? progressReporter, + string? username, + string? password, bool deploymentPipeline, CancellationToken cancellationToken) { @@ -1016,16 +1018,19 @@ async ValueTask UpdateSubmodules( var submoduleUpdateOptions = new SubmoduleUpdateOptions { Init = true, - OnTransferProgress = TransferProgressHandler( - progressReporter.CreateSection($"Fetch submodule {submodule.Name}", factor), - cancellationToken), - OnProgress = output => !cancellationToken.IsCancellationRequested, - OnUpdateTips = (a, b, c) => !cancellationToken.IsCancellationRequested, - CredentialsProvider = credentialsProvider.GenerateCredentialsHandler(username, password), - OnCheckoutProgress = CheckoutProgressHandler( - progressReporter.CreateSection($"Checkout submodule {submodule.Name}", factor)), + OnCheckoutNotify = (_, _) => !cancellationToken.IsCancellationRequested, }; + submoduleUpdateOptions.FetchOptions.Hydrate( + logger, + progressReporter?.CreateSection($"Fetch submodule {submodule.Name}", factor), + credentialsProvider.GenerateCredentialsHandler(username, password), + cancellationToken); + + if (progressReporter != null) + submoduleUpdateOptions.OnCheckoutProgress = CheckoutProgressHandler( + progressReporter.CreateSection($"Checkout submodule {submodule.Name}", factor)); + logger.LogDebug("Updating submodule {submoduleName}...", submodule.Name); Task RawSubModuleUpdate() => Task.Factory.StartNew( () => libGitRepo.Submodules.Update(submodule.Name, submoduleUpdateOptions), @@ -1040,7 +1045,7 @@ Task RawSubModuleUpdate() => Task.Factory.StartNew( { // workaround for https://github.com/libgit2/libgit2/issues/3820 // kill off the modules/ folder in .git and try again - progressReporter.ReportProgress(null); + progressReporter?.ReportProgress(null); credentialsProvider.CheckBadCredentialsException(ex); logger.LogWarning(ex, "Initial update of submodule {submoduleName} failed. Deleting submodule directories and re-attempting...", submodule.Name); @@ -1102,37 +1107,6 @@ CheckoutProgressHandler CheckoutProgressHandler(JobProgressReporter progressRepo progressReporter.ReportProgress(percentage); }; - - /// - /// Generate a from a given and . - /// - /// The of the operation. - /// The for the operation. - /// A new based on . - TransferProgressHandler TransferProgressHandler(JobProgressReporter progressReporter, CancellationToken cancellationToken) => (transferProgress) => - { - double? percentage; - var totalObjectsToProcess = transferProgress.TotalObjects * 2; - var processedObjects = transferProgress.IndexedObjects + transferProgress.ReceivedObjects; - if (totalObjectsToProcess < processedObjects || totalObjectsToProcess == 0) - percentage = null; - else - { - percentage = (double)processedObjects / totalObjectsToProcess; - if (percentage < 0) - percentage = null; - } - - if (percentage == null) - logger.LogDebug( - "Bad transfer progress values (Please tell Cyberboss)! Indexed: {indexed}, Received: {received}, Total: {total}", - transferProgress.IndexedObjects, - transferProgress.ReceivedObjects, - transferProgress.TotalObjects); - - progressReporter.ReportProgress(percentage); - return !cancellationToken.IsCancellationRequested; - }; } #pragma warning restore CA1506 } diff --git a/src/Tgstation.Server.Host/Components/Repository/RepositoryManager.cs b/src/Tgstation.Server.Host/Components/Repository/RepositoryManager.cs index 61b6e609b03..727d171e4e6 100644 --- a/src/Tgstation.Server.Host/Components/Repository/RepositoryManager.cs +++ b/src/Tgstation.Server.Host/Components/Repository/RepositoryManager.cs @@ -9,6 +9,7 @@ using Tgstation.Server.Api.Models; using Tgstation.Server.Host.Components.Events; using Tgstation.Server.Host.Configuration; +using Tgstation.Server.Host.Extensions; using Tgstation.Server.Host.IO; using Tgstation.Server.Host.Jobs; using Tgstation.Server.Host.Utils; @@ -117,19 +118,16 @@ public void Dispose() } /// - public async ValueTask CloneRepository( + public async ValueTask CloneRepository( Uri url, - string initialBranch, - string username, - string password, - JobProgressReporter progressReporter, + string? initialBranch, + string? username, + string? password, + JobProgressReporter? progressReporter, bool recurseSubmodules, CancellationToken cancellationToken) { ArgumentNullException.ThrowIfNull(url); - ArgumentNullException.ThrowIfNull(progressReporter); - - logger.LogInformation("Begin clone {url} (Branch: {initialBranch})", url, initialBranch); lock (semaphore) { if (CloneInProgress) @@ -137,31 +135,39 @@ public async ValueTask CloneRepository( CloneInProgress = true; } + var repositoryPath = ioManager.ResolvePath(); + logger.LogInformation("Begin clone {url} to {path} (Branch: {initialBranch})", url, repositoryPath, initialBranch); + try { using (await SemaphoreSlimContext.Lock(semaphore, cancellationToken)) { logger.LogTrace("Semaphore acquired for clone"); - var repositoryPath = ioManager.ResolvePath(); if (!await ioManager.DirectoryExists(repositoryPath, cancellationToken)) try { + var cloneProgressReporter = progressReporter?.CreateSection(null, 0.75f); + var checkoutProgressReporter = progressReporter?.CreateSection(null, 0.25f); var cloneOptions = new CloneOptions { - OnProgress = (a) => !cancellationToken.IsCancellationRequested, - OnTransferProgress = (a) => + RecurseSubmodules = recurseSubmodules, + OnCheckoutProgress = (path, completed, remaining) => { - var percentage = ((double)a.IndexedObjects + a.ReceivedObjects) / (a.TotalObjects * 2); - progressReporter.ReportProgress(percentage); - return !cancellationToken.IsCancellationRequested; + if (checkoutProgressReporter == null) + return; + + var percentage = (double)completed / remaining; + checkoutProgressReporter.ReportProgress(percentage); }, - RecurseSubmodules = recurseSubmodules, - OnUpdateTips = (a, b, c) => !cancellationToken.IsCancellationRequested, - RepositoryOperationStarting = (a) => !cancellationToken.IsCancellationRequested, BranchName = initialBranch, - CredentialsProvider = repositoryFactory.GenerateCredentialsHandler(username, password), }; + cloneOptions.FetchOptions.Hydrate( + logger, + cloneProgressReporter, + repositoryFactory.GenerateCredentialsHandler(username, password), + cancellationToken); + await repositoryFactory.Clone( url, cloneOptions, @@ -202,7 +208,7 @@ await repositoryFactory.Clone( } /// - public async ValueTask LoadRepository(CancellationToken cancellationToken) + public async ValueTask LoadRepository(CancellationToken cancellationToken) { logger.LogTrace("Begin LoadRepository..."); lock (semaphore) diff --git a/src/Tgstation.Server.Host/Components/Repository/RepositoryUpdateService.cs b/src/Tgstation.Server.Host/Components/Repository/RepositoryUpdateService.cs index 418dc9462d5..a7aca78c4b8 100644 --- a/src/Tgstation.Server.Host/Components/Repository/RepositoryUpdateService.cs +++ b/src/Tgstation.Server.Host/Components/Repository/RepositoryUpdateService.cs @@ -85,16 +85,18 @@ public static async ValueTask LoadRevisionInformation( IDatabaseContext databaseContext, ILogger logger, Models.Instance instance, - string lastOriginCommitSha, - Action revInfoSink, + string? lastOriginCommitSha, + Action? revInfoSink, CancellationToken cancellationToken) { var repoSha = repository.Head; IQueryable ApplyQuery(IQueryable query) => query - .Where(x => x.CommitSha == repoSha && x.Instance.Id == instance.Id) + .Where(x => x.CommitSha == repoSha && x.InstanceId == instance.Id) .Include(x => x.CompileJobs) - .Include(x => x.ActiveTestMerges).ThenInclude(x => x.TestMerge).ThenInclude(x => x.MergedBy); + .Include(x => x.ActiveTestMerges!) + .ThenInclude(x => x.TestMerge) + .ThenInclude(x => x.MergedBy); var revisionInfo = await ApplyQuery(databaseContext.RevisionInformations).FirstOrDefaultAsync(cancellationToken); @@ -103,7 +105,7 @@ public static async ValueTask LoadRevisionInformation( revisionInfo = databaseContext .RevisionInformations .Local - .Where(x => x.CommitSha == repoSha && x.Instance.Id == instance.Id) + .Where(x => x.CommitSha == repoSha && x.InstanceId == instance.Id) .FirstOrDefault(); var needsDbUpdate = revisionInfo == default; @@ -123,7 +125,7 @@ public static async ValueTask LoadRevisionInformation( databaseContext.RevisionInformations.Add(revisionInfo); } - revisionInfo.OriginCommitSha ??= lastOriginCommitSha; + revisionInfo!.OriginCommitSha ??= lastOriginCommitSha; if (revisionInfo.OriginCommitSha == null) { revisionInfo.OriginCommitSha = repoSha; @@ -145,13 +147,15 @@ public static async ValueTask LoadRevisionInformation( /// A representing the running operation. #pragma warning disable CA1502, CA1506 // TODO: Decomplexify public async ValueTask RepositoryUpdateJob( - IInstanceCore instance, + IInstanceCore? instance, IDatabaseContextFactory databaseContextFactory, Job job, JobProgressReporter progressReporter, CancellationToken cancellationToken) #pragma warning restore CA1502, CA1506 { + ArgumentNullException.ThrowIfNull(instance); + _ = job; // shuts up an IDE warning var repoManager = instance.RepositoryManager; @@ -160,23 +164,23 @@ public async ValueTask RepositoryUpdateJob( var startReference = repo.Reference; var startSha = repo.Head; - string postUpdateSha = null; + string? postUpdateSha = null; var newTestMerges = model.NewTestMerges != null && model.NewTestMerges.Count > 0; if (newTestMerges && repo.RemoteGitProvider == RemoteGitProvider.Unknown) throw new JobException(ErrorCode.RepoUnsupportedTestMergeRemote); - var committerName = currentModel.ShowTestMergeCommitters.Value + var committerName = (currentModel.ShowTestMergeCommitters!.Value ? initiatingUser.Name - : currentModel.CommitterName; + : currentModel.CommitterName)!; var hardResettingToOriginReference = model.UpdateFromOrigin == true && model.Reference != null; var numSteps = (model.NewTestMerges?.Count ?? 0) + (model.UpdateFromOrigin == true ? 1 : 0) + (!modelHasShaOrReference ? 2 : (hardResettingToOriginReference ? 3 : 1)); var progressFactor = 1.0 / numSteps; - JobProgressReporter NextProgressReporter(string stage) + JobProgressReporter NextProgressReporter(string? stage) { return progressReporter.CreateSection(stage, progressFactor); } @@ -184,19 +188,19 @@ JobProgressReporter NextProgressReporter(string stage) progressReporter.ReportProgress(0); // get a base line for where we are - Models.RevisionInformation lastRevisionInfo = null; + Models.RevisionInformation? lastRevisionInfo = null; var attachedInstance = new Models.Instance { Id = instanceId, }; - ValueTask CallLoadRevInfo(Models.TestMerge testMergeToAdd = null, string lastOriginCommitSha = null) => databaseContextFactory + ValueTask CallLoadRevInfo(Models.TestMerge? testMergeToAdd = null, string? lastOriginCommitSha = null) => databaseContextFactory .UseContext( async databaseContext => { databaseContext.Instances.Attach(attachedInstance); - var previousRevInfo = lastRevisionInfo; + var previousRevInfo = lastRevisionInfo!; var needsUpdate = await LoadRevisionInformation( repo, databaseContext, @@ -223,13 +227,11 @@ ValueTask CallLoadRevInfo(Models.TestMerge testMergeToAdd = null, string lastOri testMergeToAdd.MergedBy = mergedBy; testMergeToAdd.MergedAt = DateTimeOffset.UtcNow; - foreach (var activeTestMerge in previousRevInfo.ActiveTestMerges) - lastRevisionInfo.ActiveTestMerges.Add(activeTestMerge); + var activeTestMerges = lastRevisionInfo!.ActiveTestMerges!; + foreach (var activeTestMerge in previousRevInfo.ActiveTestMerges!) + activeTestMerges.Add(activeTestMerge); - lastRevisionInfo.ActiveTestMerges.Add(new RevInfoTestMerge - { - TestMerge = testMergeToAdd, - }); + activeTestMerges.Add(new RevInfoTestMerge(testMergeToAdd, lastRevisionInfo)); lastRevisionInfo.PrimaryTestMerge = testMergeToAdd; needsUpdate = true; @@ -242,7 +244,7 @@ ValueTask CallLoadRevInfo(Models.TestMerge testMergeToAdd = null, string lastOri await CallLoadRevInfo(); // apply new rev info, tracking applied test merges - ValueTask UpdateRevInfo(Models.TestMerge testMergeToAdd = null) => CallLoadRevInfo(testMergeToAdd, lastRevisionInfo.OriginCommitSha); + ValueTask UpdateRevInfo(Models.TestMerge? testMergeToAdd = null) => CallLoadRevInfo(testMergeToAdd, lastRevisionInfo!.OriginCommitSha); try { @@ -263,21 +265,21 @@ await repo.FetchOrigin( var fastForward = await repo.MergeOrigin( NextProgressReporter("Merge Origin"), committerName, - currentModel.CommitterEmail, + currentModel.CommitterEmail!, false, cancellationToken); if (!fastForward.HasValue) throw new JobException(ErrorCode.RepoMergeConflict); - lastRevisionInfo.OriginCommitSha = await repo.GetOriginSha(cancellationToken); + lastRevisionInfo!.OriginCommitSha = await repo.GetOriginSha(cancellationToken); await UpdateRevInfo(); if (fastForward.Value) { - await repo.Sychronize( + await repo.Synchronize( NextProgressReporter("Sychronize"), currentModel.AccessUser, currentModel.AccessToken, - currentModel.CommitterName, - currentModel.CommitterEmail, + currentModel.CommitterName!, + currentModel.CommitterEmail!, true, false, cancellationToken); @@ -288,7 +290,7 @@ await repo.Sychronize( } } - var updateSubmodules = currentModel.UpdateSubmodules.Value; + var updateSubmodules = currentModel.UpdateSubmodules!.Value; // checkout/hard reset if (modelHasShaOrReference) @@ -302,7 +304,7 @@ await repo.Sychronize( if (validCheckoutSha || validCheckoutReference) { - var committish = model.CheckoutSha ?? model.Reference; + var committish = model.CheckoutSha ?? model.Reference!; var isSha = await repo.IsSha(committish, cancellationToken); if ((isSha && model.Reference != null) || (!isSha && model.CheckoutSha != null)) @@ -331,12 +333,12 @@ await repo.ResetToOrigin( updateSubmodules, false, cancellationToken); - await repo.Sychronize( + await repo.Synchronize( NextProgressReporter("Synchronize"), currentModel.AccessUser, currentModel.AccessToken, - currentModel.CommitterName, - currentModel.CommitterEmail, + currentModel.CommitterName!, + currentModel.CommitterEmail!, true, false, cancellationToken); @@ -344,7 +346,7 @@ await repo.Sychronize( // repo head is on origin so force this // will update the db if necessary - lastRevisionInfo.OriginCommitSha = repo.Head; + lastRevisionInfo!.OriginCommitSha = repo.Head; } } @@ -355,19 +357,20 @@ await repo.Sychronize( throw new JobException(ErrorCode.RepoTestMergeInvalidRemote); // bit of sanitization - foreach (var newTestMergeWithoutTargetCommitSha in model.NewTestMerges.Where(x => String.IsNullOrWhiteSpace(x.TargetCommitSha))) + var newTestMergeModels = model.NewTestMerges!; + foreach (var newTestMergeWithoutTargetCommitSha in newTestMergeModels.Where(x => String.IsNullOrWhiteSpace(x.TargetCommitSha))) newTestMergeWithoutTargetCommitSha.TargetCommitSha = null; var repoOwner = repo.RemoteRepositoryOwner; var repoName = repo.RemoteRepositoryName; // optimization: if we've already merged these exact same commits in this fashion before, just find the rev info for it and check it out - Models.RevisionInformation revInfoWereLookingFor = null; + Models.RevisionInformation? revInfoWereLookingFor = null; bool needToApplyRemainingPrs = true; - if (lastRevisionInfo.OriginCommitSha == lastRevisionInfo.CommitSha) + if (lastRevisionInfo!.OriginCommitSha == lastRevisionInfo.CommitSha) { bool cantSearch = false; - foreach (var newTestMerge in model.NewTestMerges) + foreach (var newTestMerge in newTestMergeModels) { if (newTestMerge.TargetCommitSha != null) #pragma warning disable CA1308 // Normalize strings to uppercase @@ -391,56 +394,58 @@ await repo.Sychronize( if (!cantSearch) { - List dbPull = null; + List? dbPull = null; await databaseContextFactory.UseContext( async databaseContext => dbPull = await databaseContext.RevisionInformations .AsQueryable() - .Where(x => x.Instance.Id == instanceId - && x.OriginCommitSha == lastRevisionInfo.OriginCommitSha - && x.ActiveTestMerges.Count <= model.NewTestMerges.Count - && x.ActiveTestMerges.Count > 0) - .Include(x => x.ActiveTestMerges) - .ThenInclude(x => x.TestMerge) + .Where(x => x.InstanceId == instanceId + && x.OriginCommitSha == lastRevisionInfo.OriginCommitSha + && x.ActiveTestMerges!.Count <= newTestMergeModels.Count + && x.ActiveTestMerges!.Count > 0) + .Include(x => x.ActiveTestMerges!) + .ThenInclude(x => x.TestMerge) .ToListAsync(cancellationToken)); // split here cause this bit has to be done locally - revInfoWereLookingFor = dbPull - .Where(x => x.ActiveTestMerges.Count == model.NewTestMerges.Count - && x.ActiveTestMerges.Select(y => y.TestMerge) - .All(y => model.NewTestMerges.Any(z => - y.Number == z.Number - && y.TargetCommitSha.StartsWith(z.TargetCommitSha, StringComparison.Ordinal) - && (y.Comment?.Trim().ToUpperInvariant() == z.Comment?.Trim().ToUpperInvariant() || z.Comment == null)))) + revInfoWereLookingFor = dbPull! + .Where(x => x.ActiveTestMerges!.Count == newTestMergeModels.Count + && x.ActiveTestMerges + .Select(y => y.TestMerge) + .All(y => newTestMergeModels + .Any(z => + y.Number == z.Number + && y.TargetCommitSha!.StartsWith(z.TargetCommitSha!, StringComparison.Ordinal) + && (y.Comment?.Trim().ToUpperInvariant() == z.Comment?.Trim().ToUpperInvariant() || z.Comment == null)))) .FirstOrDefault(); - if (revInfoWereLookingFor == default && model.NewTestMerges.Count > 1) + if (revInfoWereLookingFor == default && newTestMergeModels.Count > 1) { // okay try to add at least SOME prs we've seen before - var listedNewTestMerges = model.NewTestMerges.ToList(); + var listedNewTestMerges = newTestMergeModels.ToList(); var appliedTestMergeIds = new List(); - Models.RevisionInformation lastGoodRevInfo = null; + Models.RevisionInformation? lastGoodRevInfo = null; do { foreach (var newTestMergeParameters in listedNewTestMerges) { - revInfoWereLookingFor = dbPull + revInfoWereLookingFor = dbPull! .Where(testRevInfo => { if (testRevInfo.PrimaryTestMerge == null) return false; - var testMergeMatch = model.NewTestMerges.Any(testTestMerge => + var testMergeMatch = newTestMergeModels.Any(testTestMerge => { var numberMatch = testRevInfo.PrimaryTestMerge.Number == testTestMerge.Number; if (!numberMatch) return false; - var shaMatch = testRevInfo.PrimaryTestMerge.TargetCommitSha.StartsWith( - testTestMerge.TargetCommitSha, + var shaMatch = testRevInfo.PrimaryTestMerge.TargetCommitSha!.StartsWith( + testTestMerge.TargetCommitSha!, StringComparison.Ordinal); if (!shaMatch) return false; @@ -453,7 +458,7 @@ await databaseContextFactory.UseContext( return false; var previousTestMergesMatch = testRevInfo - .ActiveTestMerges + .ActiveTestMerges! .Select(previousRevInfoTestMerge => previousRevInfoTestMerge.TestMerge) .All(previousTestMerge => appliedTestMergeIds.Contains(previousTestMerge.Id)); @@ -464,7 +469,7 @@ await databaseContextFactory.UseContext( if (revInfoWereLookingFor != null) { lastGoodRevInfo = revInfoWereLookingFor; - appliedTestMergeIds.Add(revInfoWereLookingFor.PrimaryTestMerge.Id); + appliedTestMergeIds.Add(revInfoWereLookingFor.PrimaryTestMerge!.Id); listedNewTestMerges.Remove(newTestMergeParameters); break; } @@ -485,16 +490,17 @@ await databaseContextFactory.UseContext( if (revInfoWereLookingFor != null) { // goteem - logger.LogDebug("Reusing existing SHA {sha}...", revInfoWereLookingFor.CommitSha); - await repo.ResetToSha(revInfoWereLookingFor.CommitSha, NextProgressReporter($"Reset to {revInfoWereLookingFor.CommitSha[..7]}"), cancellationToken); + var commitSha = revInfoWereLookingFor.CommitSha!; + logger.LogDebug("Reusing existing SHA {sha}...", commitSha); + await repo.ResetToSha(commitSha, NextProgressReporter($"Reset to {commitSha[..7]}"), cancellationToken); lastRevisionInfo = revInfoWereLookingFor; } if (needToApplyRemainingPrs) { - foreach (var newTestMerge in model.NewTestMerges) + foreach (var newTestMerge in newTestMergeModels) { - if (lastRevisionInfo.ActiveTestMerges.Any(x => x.TestMerge.Number == newTestMerge.Number)) + if (lastRevisionInfo.ActiveTestMerges!.Any(x => x.TestMerge.Number == newTestMerge.Number)) throw new JobException(ErrorCode.RepoDuplicateTestMerge); var fullTestMergeTask = repo.GetTestMerge(newTestMerge, currentModel, cancellationToken); @@ -502,7 +508,7 @@ await databaseContextFactory.UseContext( var mergeResult = await repo.AddTestMerge( newTestMerge, committerName, - currentModel.CommitterEmail, + currentModel.CommitterEmail!, currentModel.AccessUser, currentModel.AccessToken, updateSubmodules, @@ -513,7 +519,7 @@ await databaseContextFactory.UseContext( throw new JobException( ErrorCode.RepoTestMergeConflict, new JobException( - $"Test Merge #{newTestMerge.Number} at {newTestMerge.TargetCommitSha[..7]} conflicted! Conflicting files:{Environment.NewLine}{String.Join(Environment.NewLine, mergeResult.ConflictingFiles.Select(file => $"\t- /{file}"))}")); + $"Test Merge #{newTestMerge.Number} at {newTestMerge.TargetCommitSha![..7]} conflicted! Conflicting files:{Environment.NewLine}{String.Join(Environment.NewLine, mergeResult.ConflictingFiles!.Select(file => $"\t- /{file}"))}")); Models.TestMerge fullTestMerge; try @@ -544,14 +550,14 @@ await databaseContextFactory.UseContext( } var currentHead = repo.Head; - if (currentModel.PushTestMergeCommits.Value && (startSha != currentHead || (postUpdateSha != null && postUpdateSha != currentHead))) + if (currentModel.PushTestMergeCommits!.Value && (startSha != currentHead || (postUpdateSha != null && postUpdateSha != currentHead))) { - await repo.Sychronize( + await repo.Synchronize( NextProgressReporter("Synchronize"), currentModel.AccessUser, currentModel.AccessToken, - currentModel.CommitterName, - currentModel.CommitterEmail, + currentModel.CommitterName!, + currentModel.CommitterEmail!, false, false, cancellationToken); diff --git a/src/Tgstation.Server.Host/Components/Repository/RepostoryManagerFactory.cs b/src/Tgstation.Server.Host/Components/Repository/RepostoryManagerFactory.cs new file mode 100644 index 00000000000..cba66ff69a4 --- /dev/null +++ b/src/Tgstation.Server.Host/Components/Repository/RepostoryManagerFactory.cs @@ -0,0 +1,100 @@ +using System; +using System.Threading; +using System.Threading.Tasks; + +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Options; + +using Tgstation.Server.Host.Components.Events; +using Tgstation.Server.Host.Configuration; +using Tgstation.Server.Host.IO; + +namespace Tgstation.Server.Host.Components.Repository +{ + /// + sealed class RepostoryManagerFactory : IRepositoryManagerFactory + { + /// + /// The for the . + /// + readonly ILibGit2RepositoryFactory repositoryFactory; + + /// + /// The for the . + /// + readonly ILibGit2Commands repositoryCommands; + + /// + /// The for the . + /// + readonly IPostWriteHandler postWriteHandler; + + /// + /// The for the . + /// + readonly IGitRemoteFeaturesFactory gitRemoteFeaturesFactory; + + /// + /// The for the . + /// + readonly ILoggerFactory loggerFactory; + + /// + /// The for the . + /// + readonly GeneralConfiguration generalConfiguration; + + /// + /// Initializes a new instance of the class. + /// + /// The value of . + /// The value of . + /// The value of . + /// The value of . + /// The value of . + /// The containing the value of . + public RepostoryManagerFactory( + ILibGit2RepositoryFactory repositoryFactory, + ILibGit2Commands repositoryCommands, + IPostWriteHandler postWriteHandler, + IGitRemoteFeaturesFactory gitRemoteFeaturesFactory, + ILoggerFactory loggerFactory, + IOptions generalConfigurationOptions) + { + this.repositoryFactory = repositoryFactory ?? throw new ArgumentNullException(nameof(repositoryFactory)); + this.repositoryCommands = repositoryCommands ?? throw new ArgumentNullException(nameof(repositoryCommands)); + this.postWriteHandler = postWriteHandler ?? throw new ArgumentNullException(nameof(postWriteHandler)); + this.gitRemoteFeaturesFactory = gitRemoteFeaturesFactory ?? throw new ArgumentNullException(nameof(gitRemoteFeaturesFactory)); + this.loggerFactory = loggerFactory ?? throw new ArgumentNullException(nameof(loggerFactory)); + generalConfiguration = generalConfigurationOptions?.Value ?? throw new ArgumentNullException(nameof(generalConfigurationOptions)); + } + + /// + public IRepositoryManager CreateRepositoryManager(IIOManager ioManager, IEventConsumer eventConsumer) + => new RepositoryManager( + repositoryFactory, + repositoryCommands, + ioManager, + eventConsumer, + postWriteHandler, + gitRemoteFeaturesFactory, + loggerFactory.CreateLogger(), + loggerFactory.CreateLogger(), + generalConfiguration); + + /// + public Task StartAsync(CancellationToken cancellationToken) + { + CheckSystemCompatibility(); + return Task.CompletedTask; + } + + /// + public Task StopAsync(CancellationToken cancellationToken) => Task.CompletedTask; + + /// + /// Test that the is functional. + /// + void CheckSystemCompatibility() => repositoryFactory.CreateInMemory(); + } +} diff --git a/src/Tgstation.Server.Host/Components/Repository/TestMergeResult.cs b/src/Tgstation.Server.Host/Components/Repository/TestMergeResult.cs index 387a6baad1f..2505a491d85 100644 --- a/src/Tgstation.Server.Host/Components/Repository/TestMergeResult.cs +++ b/src/Tgstation.Server.Host/Components/Repository/TestMergeResult.cs @@ -17,6 +17,6 @@ public sealed class TestMergeResult /// /// List of conflicting file paths relative to the repository root. Only present if is . /// - public IReadOnlyList ConflictingFiles { get; init; } + public IReadOnlyList? ConflictingFiles { get; init; } } } diff --git a/src/Tgstation.Server.Host/Components/Session/ApiValidationStatus.cs b/src/Tgstation.Server.Host/Components/Session/ApiValidationStatus.cs index 74ea6b4c540..6b3c6db3573 100644 --- a/src/Tgstation.Server.Host/Components/Session/ApiValidationStatus.cs +++ b/src/Tgstation.Server.Host/Components/Session/ApiValidationStatus.cs @@ -6,32 +6,32 @@ enum ApiValidationStatus { /// - /// The DMAPI never contacted the server for validation + /// The DMAPI never contacted the server for validation. /// NeverValidated, /// - /// The server was contacted for validation but it was never requested + /// The server was contacted for validation but it was never requested. /// UnaskedValidationRequest, /// - /// The validation request was malformed + /// The validation request was malformed. /// BadValidationRequest, /// - /// Valid API. The game must be run with a minimum security level of + /// Valid API. The game must be run with a minimum security level of . /// RequiresSafe, /// - /// Valid API. The game must be run with a security level of + /// Valid API. The game must be run with a security level of . /// RequiresTrusted, /// - /// Valid API. The game must be run with a minimum security level of + /// Valid API. The game must be run with a minimum security level of . /// RequiresUltrasafe, diff --git a/src/Tgstation.Server.Host/Components/Session/CombinedTopicResponse.cs b/src/Tgstation.Server.Host/Components/Session/CombinedTopicResponse.cs index 417934f04a8..ad22b6204b7 100644 --- a/src/Tgstation.Server.Host/Components/Session/CombinedTopicResponse.cs +++ b/src/Tgstation.Server.Host/Components/Session/CombinedTopicResponse.cs @@ -5,26 +5,26 @@ namespace Tgstation.Server.Host.Components.Session { /// - /// Combines a with a . + /// Combines a with a . /// sealed class CombinedTopicResponse { /// - /// The raw . + /// The raw . /// - public global::Byond.TopicSender.TopicResponse ByondTopicResponse { get; } + public Byond.TopicSender.TopicResponse ByondTopicResponse { get; } /// /// The interop , if any. /// - public TopicResponse InteropResponse { get; } + public TopicResponse? InteropResponse { get; } /// /// Initializes a new instance of the class. /// /// The value of . /// The optional value of . - public CombinedTopicResponse(global::Byond.TopicSender.TopicResponse byondTopicResponse, TopicResponse interopResponse) + public CombinedTopicResponse(Byond.TopicSender.TopicResponse byondTopicResponse, TopicResponse? interopResponse) { ByondTopicResponse = byondTopicResponse ?? throw new ArgumentNullException(nameof(byondTopicResponse)); InteropResponse = interopResponse; diff --git a/src/Tgstation.Server.Host/Components/Session/ISessionController.cs b/src/Tgstation.Server.Host/Components/Session/ISessionController.cs index 0372a32ec0a..ccdaa448c4c 100644 --- a/src/Tgstation.Server.Host/Components/Session/ISessionController.cs +++ b/src/Tgstation.Server.Host/Components/Session/ISessionController.cs @@ -2,9 +2,9 @@ using System.Threading; using System.Threading.Tasks; +using Tgstation.Server.Api.Models; using Tgstation.Server.Host.Components.Deployment; using Tgstation.Server.Host.Components.Interop.Topic; -using Tgstation.Server.Host.Models; using Tgstation.Server.Host.System; namespace Tgstation.Server.Host.Components.Session @@ -32,22 +32,22 @@ interface ISessionController : IProcessBase, IRenameNotifyee, IAsyncDisposable /// /// The DMAPI . /// - Version DMApiVersion { get; } + Version? DMApiVersion { get; } /// /// Gets the associated with the . /// - CompileJob CompileJob { get; } + Models.CompileJob CompileJob { get; } /// - /// Gets the associated with the . + /// Gets the associated with the . /// - ReattachInformation ReattachInformation { get; } + EngineVersion EngineVersion { get; } /// - /// If the port should be rotated off when the world reboots. + /// Gets the associated with the . /// - bool ClosePortOnReboot { get; set; } + ReattachInformation ReattachInformation { get; } /// /// If the is currently processing a bridge request from TgsReboot(). @@ -96,15 +96,7 @@ interface ISessionController : IProcessBase, IRenameNotifyee, IAsyncDisposable /// The to send. /// The for the operation. /// A resulting in the of /world/Topic(). - ValueTask SendCommand(TopicParameters parameters, CancellationToken cancellationToken); - - /// - /// Causes the world to start listening on a . - /// - /// The port to change to. - /// The for the operation. - /// A resulting in if the operation succeeded, otherwise. - Task SetPort(ushort newPort, CancellationToken cancellatonToken); + ValueTask SendCommand(TopicParameters parameters, CancellationToken cancellationToken); /// /// Attempts to change the current to . diff --git a/src/Tgstation.Server.Host/Components/Session/ISessionControllerFactory.cs b/src/Tgstation.Server.Host/Components/Session/ISessionControllerFactory.cs index b5bbf07f902..7058d3f4e49 100644 --- a/src/Tgstation.Server.Host/Components/Session/ISessionControllerFactory.cs +++ b/src/Tgstation.Server.Host/Components/Session/ISessionControllerFactory.cs @@ -2,8 +2,8 @@ using System.Threading.Tasks; using Tgstation.Server.Api.Models.Internal; -using Tgstation.Server.Host.Components.Byond; using Tgstation.Server.Host.Components.Deployment; +using Tgstation.Server.Host.Components.Engine; namespace Tgstation.Server.Host.Components.Session { @@ -16,14 +16,14 @@ interface ISessionControllerFactory /// Create a from a freshly launch DreamDaemon instance. /// /// The to use. - /// The current if any. + /// The current . if any. /// The to use. will be updated with the minumum required security level for the launch. /// If the should only validate the DMAPI then exit. /// The for the operation. /// A resulting in a new . ValueTask LaunchNew( IDmbProvider dmbProvider, - IByondExecutableLock currentByondLock, + IEngineExecutableLock? currentByondLock, DreamDaemonLaunchParameters launchParameters, bool apiValidate, CancellationToken cancellationToken); @@ -34,7 +34,7 @@ ValueTask LaunchNew( /// The to use. /// The for the operation. /// A resulting in a new on success or on failure to reattach. - ValueTask Reattach( + ValueTask Reattach( ReattachInformation reattachInformation, CancellationToken cancellationToken); } diff --git a/src/Tgstation.Server.Host/Components/Session/ISessionPersistor.cs b/src/Tgstation.Server.Host/Components/Session/ISessionPersistor.cs index 9e088bd7b14..9d3806c3177 100644 --- a/src/Tgstation.Server.Host/Components/Session/ISessionPersistor.cs +++ b/src/Tgstation.Server.Host/Components/Session/ISessionPersistor.cs @@ -11,17 +11,25 @@ public interface ISessionPersistor /// /// Save some . /// - /// The to save. + /// The to save. will be written back into it. /// The for the operation. /// A representing the running operation. ValueTask Save(ReattachInformation reattachInformation, CancellationToken cancellationToken); + /// + /// Update some . + /// + /// The to update. Requires to have been called on it previously or it was retrieved from . + /// The for the operation. + /// A representing the running operation. + ValueTask Update(ReattachInformation reattachInformation, CancellationToken cancellationToken); + /// /// Load a saved . /// /// The for the operation. /// A resulting in the stored if any. - ValueTask Load(CancellationToken cancellationToken); + ValueTask Load(CancellationToken cancellationToken); /// /// Clear any stored . diff --git a/src/Tgstation.Server.Host/Components/Session/LaunchResult.cs b/src/Tgstation.Server.Host/Components/Session/LaunchResult.cs index fd5785fd416..eb44b6e55f7 100644 --- a/src/Tgstation.Server.Host/Components/Session/LaunchResult.cs +++ b/src/Tgstation.Server.Host/Components/Session/LaunchResult.cs @@ -11,12 +11,12 @@ public sealed class LaunchResult /// /// The time it took for to return or the initial bridge request to process. If the startup timed out. /// - public TimeSpan? StartupTime { get; set; } + public TimeSpan? StartupTime { get; init; } /// /// The if it exited. /// - public int? ExitCode { get; set; } + public int? ExitCode { get; init; } /// public override string ToString() => String.Format(CultureInfo.InvariantCulture, "Exit Code: {0}, Time {1}ms", ExitCode, StartupTime?.TotalMilliseconds); diff --git a/src/Tgstation.Server.Host/Components/Session/ReattachInformation.cs b/src/Tgstation.Server.Host/Components/Session/ReattachInformation.cs index 6c429d2d0a6..df2ce141d45 100644 --- a/src/Tgstation.Server.Host/Components/Session/ReattachInformation.cs +++ b/src/Tgstation.Server.Host/Components/Session/ReattachInformation.cs @@ -20,12 +20,12 @@ public sealed class ReattachInformation : ReattachInformationBase /// /// The initially used to launch DreamDaemon. Should be a different than . Should not be set if persisting the initial isn't necessary. /// - public IDmbProvider InitialDmb { get; set; } + public IDmbProvider? InitialDmb { get; set; } /// /// The for the DMAPI. /// - public RuntimeInformation RuntimeInformation { get; private set; } + public RuntimeInformation? RuntimeInformation { get; private set; } /// /// The which indicates when topic requests should timeout. @@ -47,7 +47,7 @@ public sealed class ReattachInformation : ReattachInformationBase public ReattachInformation( Models.ReattachInformation copy, IDmbProvider dmb, - IDmbProvider initialDmb, + IDmbProvider? initialDmb, TimeSpan topicRequestTimeout) : base(copy) { @@ -72,13 +72,12 @@ internal ReattachInformation( RuntimeInformation runtimeInformation, string accessIdentifier, ushort port) + : base(accessIdentifier) { Dmb = dmb ?? throw new ArgumentNullException(nameof(dmb)); ProcessId = process?.Id ?? throw new ArgumentNullException(nameof(process)); RuntimeInformation = runtimeInformation ?? throw new ArgumentNullException(nameof(runtimeInformation)); - AccessIdentifier = accessIdentifier ?? throw new ArgumentNullException(nameof(accessIdentifier)); - LaunchSecurityLevel = runtimeInformation.SecurityLevel; LaunchVisibility = runtimeInformation.Visibility; Port = port; diff --git a/src/Tgstation.Server.Host/Components/Session/RebootState.cs b/src/Tgstation.Server.Host/Components/Session/RebootState.cs index 0b58f492dda..030e8be98e8 100644 --- a/src/Tgstation.Server.Host/Components/Session/RebootState.cs +++ b/src/Tgstation.Server.Host/Components/Session/RebootState.cs @@ -6,17 +6,17 @@ public enum RebootState : int { /// - /// Run DreamDaemon's normal reboot process + /// Run DreamDaemon's normal reboot process. /// Normal = 0, /// - /// Shutdown DreamDaemon + /// Shutdown DreamDaemon. /// Shutdown = 1, /// - /// Restart the DreamDaemon process + /// Restart the DreamDaemon process. /// Restart = 2, } diff --git a/src/Tgstation.Server.Host/Components/Session/SessionController.cs b/src/Tgstation.Server.Host/Components/Session/SessionController.cs index d57db476104..ad9bdf17288 100644 --- a/src/Tgstation.Server.Host/Components/Session/SessionController.cs +++ b/src/Tgstation.Server.Host/Components/Session/SessionController.cs @@ -2,7 +2,6 @@ using System.Collections.Generic; using System.Globalization; using System.Linq; -using System.Net; using System.Text; using System.Threading; using System.Threading.Tasks; @@ -14,13 +13,16 @@ using Serilog.Context; 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.Chat.Commands; using Tgstation.Server.Host.Components.Deployment; +using Tgstation.Server.Host.Components.Engine; using Tgstation.Server.Host.Components.Interop; using Tgstation.Server.Host.Components.Interop.Bridge; using Tgstation.Server.Host.Components.Interop.Topic; +using Tgstation.Server.Host.Extensions; using Tgstation.Server.Host.System; using Tgstation.Server.Host.Utils; @@ -52,13 +54,13 @@ public ApiValidationStatus ApiValidationStatus public Models.CompileJob CompileJob => ReattachInformation.Dmb.CompileJob; /// - public RebootState RebootState => ReattachInformation.RebootState; + public EngineVersion EngineVersion => ReattachInformation.Dmb.EngineVersion; /// - public Version DMApiVersion { get; private set; } + public RebootState RebootState => ReattachInformation.RebootState; /// - public bool ClosePortOnReboot { get; set; } + public Version? DMApiVersion { get; private set; } /// public bool TerminationWasRequested { get; private set; } @@ -108,14 +110,19 @@ async Task Wrap() public ReattachInformation ReattachInformation { get; } /// - /// The for the . + /// The used to prevent concurrent calls into /world/Topic(). + /// + public FifoSemaphore TopicSendSemaphore { get; } + + /// + /// The for the . /// - readonly global::Byond.TopicSender.ITopicClient byondTopicSender; + readonly Byond.TopicSender.ITopicClient byondTopicSender; /// /// The for the . /// - readonly IBridgeRegistration bridgeRegistration; + readonly IBridgeRegistration? bridgeRegistration; /// /// The for the . @@ -123,9 +130,9 @@ async Task Wrap() readonly IProcess process; /// - /// The for the . + /// The for the . /// - readonly IByondExecutableLock byondLock; + readonly IEngineExecutableLock engineLock; /// /// The for the . @@ -147,11 +154,6 @@ async Task Wrap() /// readonly TaskCompletionSource initialBridgeRequestTcs; - /// - /// The used to prevent concurrent calls into /world/Topic(). - /// - readonly FifoSemaphore topicSendSemaphore; - /// /// The metadata. /// @@ -172,11 +174,6 @@ async Task Wrap() /// readonly bool apiValidationSession; - /// - /// The waits on when DreamDaemon currently has it's ports closed. - /// - TaskCompletionSource portAssignmentTcs; - /// /// The that completes when DD sends a valid startup bridge request. /// @@ -200,28 +197,18 @@ async Task Wrap() /// /// for shutting down the server if it is taking too long after validation. /// - volatile Task postValidationShutdownTask; + volatile Task? postValidationShutdownTask; /// /// The number of currently active calls to from TgsReboot(). /// volatile uint rebootBridgeRequestsProcessing; - /// - /// The port to assign DreamDaemon when it queries for it. - /// - ushort? nextPort; - /// /// The for the . /// ApiValidationStatus apiValidationStatus; - /// - /// If we know DreamDaemon currently has it's port closed. - /// - bool portClosedForReboot; - /// /// If the has been disposed. /// @@ -238,7 +225,7 @@ async Task Wrap() /// The value of . /// The owning . /// The value of . - /// The value of . + /// The value of . /// The value of . /// The used to populate . /// The value of . @@ -254,8 +241,8 @@ public SessionController( ReattachInformation reattachInformation, Api.Models.Instance metadata, IProcess process, - IByondExecutableLock byondLock, - global::Byond.TopicSender.ITopicClient byondTopicSender, + IEngineExecutableLock engineLock, + Byond.TopicSender.ITopicClient byondTopicSender, IChatTrackingContext chatTrackingContext, IBridgeRegistrar bridgeRegistrar, IChatManager chat, @@ -271,7 +258,7 @@ public SessionController( ReattachInformation = reattachInformation ?? throw new ArgumentNullException(nameof(reattachInformation)); this.metadata = metadata ?? throw new ArgumentNullException(nameof(metadata)); this.process = process ?? throw new ArgumentNullException(nameof(process)); - this.byondLock = byondLock ?? throw new ArgumentNullException(nameof(byondLock)); + this.engineLock = engineLock ?? throw new ArgumentNullException(nameof(engineLock)); this.byondTopicSender = byondTopicSender ?? throw new ArgumentNullException(nameof(byondTopicSender)); this.chatTrackingContext = chatTrackingContext ?? throw new ArgumentNullException(nameof(chatTrackingContext)); ArgumentNullException.ThrowIfNull(bridgeRegistrar); @@ -283,7 +270,6 @@ public SessionController( apiValidationSession = apiValidate; - portClosedForReboot = false; disposed = false; apiValidationStatus = ApiValidationStatus.NeverValidated; released = false; @@ -300,7 +286,7 @@ public SessionController( initialBridgeRequestTcs = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously); reattachTopicCts = new CancellationTokenSource(); - topicSendSemaphore = new FifoSemaphore(); + TopicSendSemaphore = new FifoSemaphore(); synchronizationLock = new object(); if (apiValidationSession || DMApiAvailable) @@ -352,18 +338,22 @@ public async ValueTask DisposeAsync() Logger.LogTrace("Disposing..."); - // yield then acquire the topic semaphore to prevent new calls from starting - await Task.Yield(); - (await topicSendSemaphore.Lock(CancellationToken.None)).Dispose(); // DCT: None available + reattachTopicCts.Cancel(); + var cancellationToken = CancellationToken.None; // DCT: None available + var semaphoreLockTask = TopicSendSemaphore.Lock(cancellationToken); if (!released) { - process.Terminate(); - await process.Lifetime; + await engineLock.StopServerProcess( + Logger, + process, + ReattachInformation.AccessIdentifier, + ReattachInformation.Port, + cancellationToken); } await process.DisposeAsync(); - byondLock.Dispose(); + engineLock.Dispose(); bridgeRegistration?.Dispose(); var regularDmbDisposeTask = ReattachInformation.Dmb.DisposeAsync(); var initialDmb = ReattachInformation.InitialDmb; @@ -378,11 +368,12 @@ public async ValueTask DisposeAsync() if (!released) await Lifetime; // finish the async callback - topicSendSemaphore.Dispose(); + (await semaphoreLockTask).Dispose(); + TopicSendSemaphore.Dispose(); } /// - public async ValueTask ProcessBridgeRequest(BridgeParameters parameters, CancellationToken cancellationToken) + public async ValueTask ProcessBridgeRequest(BridgeParameters parameters, CancellationToken cancellationToken) { ArgumentNullException.ThrowIfNull(parameters); @@ -408,49 +399,15 @@ public ValueTask Release() ReattachInformation.Dmb.KeepAlive(); ReattachInformation.InitialDmb?.KeepAlive(); - byondLock.DoNotDeleteThisSession(); + engineLock.DoNotDeleteThisSession(); released = true; return DisposeAsync(); } /// - public ValueTask SendCommand(TopicParameters parameters, CancellationToken cancellationToken) + public ValueTask SendCommand(TopicParameters parameters, CancellationToken cancellationToken) => SendCommand(parameters, false, cancellationToken); - /// - public Task SetPort(ushort port, CancellationToken cancellationToken) - { - CheckDisposed(); - - if (port == 0) - throw new ArgumentOutOfRangeException(nameof(port), port, "port must not be zero!"); - - async Task ImmediateTopicPortChange() - { - var commandResult = await SendCommand( - new TopicParameters(port), - cancellationToken); - - if (commandResult?.ErrorMessage != null) - return false; - - ReattachInformation.Port = port; - return true; - } - - lock (synchronizationLock) - if (portClosedForReboot) - { - if (portAssignmentTcs != null) - throw new InvalidOperationException("A port change operation is already in progress!"); - nextPort = port; - portAssignmentTcs = new TaskCompletionSource(); - return portAssignmentTcs.Task; - } - else - return ImmediateTopicPortChange(); - } - /// public async ValueTask SetRebootState(RebootState newRebootState, CancellationToken cancellationToken) { @@ -479,10 +436,10 @@ public void ResetRebootState() public void AdjustPriority(bool higher) => process.AdjustPriority(higher); /// - public void Suspend() => process.Suspend(); + public void SuspendProcess() => process.SuspendProcess(); /// - public void Resume() => process.Resume(); + public void ResumeProcess() => process.ResumeProcess(); /// public IAsyncDisposable ReplaceDmbProvider(IDmbProvider dmbProvider) @@ -495,7 +452,10 @@ public IAsyncDisposable ReplaceDmbProvider(IDmbProvider dmbProvider) /// public async ValueTask InstanceRenamed(string newInstanceName, CancellationToken cancellationToken) { - ReattachInformation.RuntimeInformation.InstanceName = newInstanceName; + var runtimeInformation = ReattachInformation.RuntimeInformation; + if (runtimeInformation != null) + runtimeInformation.InstanceName = newInstanceName; + await SendCommand( TopicParameters.CreateInstanceRenamedTopicParameters(newInstanceName), cancellationToken); @@ -561,7 +521,7 @@ async Task GetLaunchResult( var reattachResponse = await SendCommand( new TopicParameters( assemblyInformationProvider.Version, - ReattachInformation.RuntimeInformation.ServerPort), + ReattachInformation.RuntimeInformation!.ServerPort), true, reattachTopicCts.Token); @@ -575,7 +535,7 @@ async Task GetLaunchResult( ? LogLevel.Warning : LogLevel.Debug, "DMAPI Interop v{interopVersion} isn't returning the TGS custom commands list. Functionality added in v5.2.0.", - CompileJob.DMApiVersion.Semver()); + CompileJob.DMApiVersion!.Semver()); } } @@ -585,11 +545,7 @@ async Task GetLaunchResult( /// /// Throws an if has been called. /// - void CheckDisposed() - { - if (disposed) - throw new ObjectDisposedException(nameof(SessionController)); - } + void CheckDisposed() => ObjectDisposedException.ThrowIf(disposed, this); /// /// Terminates the server after ten seconds if it does not exit. @@ -627,7 +583,7 @@ async Task PostValidationShutdown(Task proceedTask) /// The for the operation. /// A resulting in the for the request or if the request could not be dispatched. #pragma warning disable CA1502 // TODO: Decomplexify - async ValueTask ProcessBridgeCommand(BridgeParameters parameters, CancellationToken cancellationToken) + async ValueTask ProcessBridgeCommand(BridgeParameters parameters, CancellationToken cancellationToken) { var response = new BridgeResponse(); switch (parameters.CommandType) @@ -669,37 +625,8 @@ async ValueTask ProcessBridgeCommand(BridgeParameters parameters TerminationWasRequested = true; process.Terminate(); break; - case BridgeCommandType.PortUpdate: - lock (synchronizationLock) - { - if (!parameters.CurrentPort.HasValue) - { - /////UHHHH - Logger.LogWarning("DreamDaemon sent new port command without providing it's own!"); - return BridgeError("Missing stringified port as data parameter!"); - } - - var currentPort = parameters.CurrentPort.Value; - if (!nextPort.HasValue) - ReattachInformation.Port = parameters.CurrentPort.Value; // not ready yet, so what we'll do is accept the random port DD opened on for now and change it later when we decide to - else - { - // nextPort is ready, tell DD to switch to that - // if it fails it'll kill itself - response.NewPort = nextPort.Value; - ReattachInformation.Port = nextPort.Value; - nextPort = null; - - // we'll also get here from SetPort so complete that task - var tmpTcs = portAssignmentTcs; - portAssignmentTcs = null; - tmpTcs.SetResult(true); - } - - portClosedForReboot = false; - } - - break; + case BridgeCommandType.DeprecatedPortUpdate: + return BridgeError("Port switching is no longer supported!"); case BridgeCommandType.Startup: apiValidationStatus = ApiValidationStatus.BadValidationRequest; @@ -717,7 +644,10 @@ async ValueTask ProcessBridgeCommand(BridgeParameters parameters return BridgeError("Missing dmApiVersion field!"); DMApiVersion = parameters.Version; - if (DMApiVersion.Major != DMApiConstants.InteropVersion.Major) + + // TODO: When OD figures out how to unite port and topic_port, set an upper version bound on OD for this check + if (DMApiVersion.Major != DMApiConstants.InteropVersion.Major + || (EngineVersion.Engine == EngineType.OpenDream && DMApiVersion < new Version(5, 7))) { apiValidationStatus = ApiValidationStatus.Incompatible; return BridgeError("Incompatible dmApiVersion!"); @@ -742,18 +672,26 @@ async ValueTask ProcessBridgeCommand(BridgeParameters parameters Logger.LogTrace("ApiValidationStatus set to {apiValidationStatus}", apiValidationStatus); + // we create new runtime info here because of potential .Dmb changes (i think. i forget...) response.RuntimeInformation = new RuntimeInformation( chatTrackingContext, ReattachInformation.Dmb, - ReattachInformation.RuntimeInformation.ServerVersion, + ReattachInformation.RuntimeInformation!.ServerVersion, ReattachInformation.RuntimeInformation.InstanceName, ReattachInformation.RuntimeInformation.SecurityLevel, ReattachInformation.RuntimeInformation.Visibility, ReattachInformation.RuntimeInformation.ServerPort, ReattachInformation.RuntimeInformation.ApiValidateOnly); + if (parameters.TopicPort.HasValue) + { + var newTopicPort = parameters.TopicPort.Value; + Logger.LogInformation("Server is requesting use of port {topicPort} for topic communications", newTopicPort); + ReattachInformation.TopicPort = newTopicPort; + } + // Load custom commands - chatTrackingContext.CustomCommands = parameters.CustomCommands; + chatTrackingContext.CustomCommands = parameters.CustomCommands ?? Array.Empty(); chatTrackingContext.Active = true; Interlocked.Exchange(ref startupTcs, new TaskCompletionSource()).SetResult(); break; @@ -762,13 +700,6 @@ async ValueTask ProcessBridgeCommand(BridgeParameters parameters try { chatTrackingContext.Active = false; - - if (ClosePortOnReboot) - { - response.NewPort = 0; - portClosedForReboot = true; - } - Interlocked.Exchange(ref rebootTcs, new TaskCompletionSource()).SetResult(); await RebootGate.WaitAsync(cancellationToken); } @@ -797,7 +728,7 @@ async ValueTask ProcessBridgeCommand(BridgeParameters parameters /// A new errored . BridgeResponse BridgeError(string message) { - Logger.LogWarning("Bridge request chunking error: {message}", message); + Logger.LogWarning("Bridge request error: {message}", message); return new BridgeResponse { ErrorMessage = message, @@ -810,7 +741,7 @@ BridgeResponse BridgeError(string message) /// The to send. /// The for the operation. /// A resulting in the of the topic request. - async ValueTask SendTopicRequest(TopicParameters parameters, CancellationToken cancellationToken) + async ValueTask SendTopicRequest(TopicParameters parameters, CancellationToken cancellationToken) { parameters.AccessIdentifier = ReattachInformation.AccessIdentifier; @@ -836,14 +767,14 @@ async ValueTask SendTopicRequest(TopicParameters paramete var payloadId = NextPayloadId; // AccessIdentifer is just noise in a chunked request - parameters.AccessIdentifier = null; + parameters.AccessIdentifier = null!; GenerateQueryString(parameters, out json); // yes, this straight up ignores unicode, precalculating it is useless when we don't // even know if the UTF8 bytes of the url encoded chunk will fit the window until we do said encoding var fullPayloadSize = (uint)json.Length; - List chunkQueryStrings = null; + List? chunkQueryStrings = null; for (var chunkCount = 2; chunkQueryStrings == null; ++chunkCount) { var standardChunkSize = fullPayloadSize / chunkCount; @@ -889,7 +820,7 @@ async ValueTask SendTopicRequest(TopicParameters paramete Logger.LogTrace("Chunking topic request ({totalChunks} total)...", chunkQueryStrings.Count); - CombinedTopicResponse combinedResponse = null; + CombinedTopicResponse? combinedResponse = null; bool LogRequestIssue(bool possiblyFromCompletedRequest) { if (combinedResponse?.InteropResponse == null || combinedResponse.InteropResponse.ErrorMessage != null) @@ -913,11 +844,12 @@ bool LogRequestIssue(bool possiblyFromCompletedRequest) return null; } - while ((combinedResponse.InteropResponse.MissingChunks?.Count ?? 0) > 0) + while ((combinedResponse?.InteropResponse?.MissingChunks?.Count ?? 0) > 0) { Logger.LogWarning("DD is still missing some chunks of topic request P{payloadId}! Sending missing chunks...", payloadId); - var lastIndex = combinedResponse.InteropResponse.MissingChunks.Last(); - foreach (var missingChunkIndex in combinedResponse.InteropResponse.MissingChunks) + var missingChunks = combinedResponse!.InteropResponse!.MissingChunks!; + var lastIndex = missingChunks.Last(); + foreach (var missingChunkIndex in missingChunks) { var chunkCommandString = chunkQueryStrings[(int)missingChunkIndex]; combinedResponse = await SendRawTopic(chunkCommandString, topicPriority, cancellationToken); @@ -930,11 +862,11 @@ bool LogRequestIssue(bool possiblyFromCompletedRequest) } /// - /// Generates a query string for a given set of . + /// Generates a query string for a given set of . /// /// The to serialize. /// The intermediate JSON prior to URL encoding. - /// The query string for the given . + /// The query string for the given . string GenerateQueryString(TopicParameters parameters, out string json) { json = JsonConvert.SerializeObject(parameters, DMApiConstants.SerializerSettings); @@ -953,7 +885,7 @@ string GenerateQueryString(TopicParameters parameters, out string json) /// If this is a priority message. If so, the topic will make 5 attempts to send unless BYOND reboots or exits. /// The for the operation. /// A resulting in the of the topic request. - async ValueTask SendRawTopic(string queryString, bool priority, CancellationToken cancellationToken) + async ValueTask SendRawTopic(string queryString, bool priority, CancellationToken cancellationToken) { if (disposed) { @@ -962,36 +894,16 @@ async ValueTask SendRawTopic(string queryString, bool pri return null; } - var targetPort = ReattachInformation.Port; - global::Byond.TopicSender.TopicResponse byondResponse = null; - var firstSend = true; - - using (await topicSendSemaphore.Lock(cancellationToken)) - { - const int PrioritySendAttempts = 5; - var endpoint = new IPEndPoint(IPAddress.Loopback, targetPort); - for (var i = PrioritySendAttempts - 1; i >= 0 && (priority || firstSend); --i) - try - { - firstSend = false; - - Logger.LogTrace("Begin topic request"); - byondResponse = await byondTopicSender.SendTopic( - endpoint, - queryString, - cancellationToken); - - Logger.LogTrace("End topic request"); - break; - } - catch (Exception ex) when (ex is not OperationCanceledException) - { - Logger.LogWarning(ex, "SendTopic exception!{retryDetails}", priority ? $" {i} attempts remaining." : String.Empty); - - if (priority && i > 0) - await asyncDelayer.Delay(TimeSpan.FromSeconds(2), cancellationToken); - } - } + var targetPort = ReattachInformation.TopicPort ?? ReattachInformation.Port; + Byond.TopicSender.TopicResponse? byondResponse; + using (await TopicSendSemaphore.Lock(cancellationToken)) + byondResponse = await byondTopicSender.SendWithOptionalPriority( + asyncDelayer, + Logger, + queryString, + targetPort, + priority, + cancellationToken); if (byondResponse == null) { @@ -1005,7 +917,7 @@ async ValueTask SendRawTopic(string queryString, bool pri var topicReturn = byondResponse.StringData; - TopicResponse interopResponse = null; + TopicResponse? interopResponse = null; if (topicReturn != null) try { @@ -1026,7 +938,7 @@ async ValueTask SendRawTopic(string queryString, bool pri /// If waiting for the should be bypassed. /// The for the operation. /// A resulting in the of /world/Topic(). - async ValueTask SendCommand(TopicParameters parameters, bool bypassLaunchResult, CancellationToken cancellationToken) + async ValueTask SendCommand(TopicParameters parameters, bool bypassLaunchResult, CancellationToken cancellationToken) { ArgumentNullException.ThrowIfNull(parameters); @@ -1088,7 +1000,7 @@ async ValueTask CancelIfLifetimeElapses() } } - TopicResponse fullResponse = null; + TopicResponse? fullResponse = null; var lifetimeWatchingTask = CancelIfLifetimeElapses(); try { @@ -1106,14 +1018,14 @@ void LogCombinedResponse() { Logger.LogTrace("Topic response is chunked..."); - ChunkData nextChunk = combinedResponse.InteropResponse.Chunk; + ChunkData? nextChunk = combinedResponse.InteropResponse.Chunk; do { var nextRequest = await ProcessChunk( (completedResponse, _) => { fullResponse = completedResponse; - return ValueTask.FromResult(null); + return ValueTask.FromResult(null); }, error => { diff --git a/src/Tgstation.Server.Host/Components/Session/SessionControllerFactory.cs b/src/Tgstation.Server.Host/Components/Session/SessionControllerFactory.cs index 6d48b990fe9..70bed0f5ba3 100644 --- a/src/Tgstation.Server.Host/Components/Session/SessionControllerFactory.cs +++ b/src/Tgstation.Server.Host/Components/Session/SessionControllerFactory.cs @@ -6,16 +6,14 @@ using System.Threading; using System.Threading.Tasks; -using Byond.TopicSender; - using Microsoft.Extensions.Logging; 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; +using Tgstation.Server.Host.Components.Engine; using Tgstation.Server.Host.Components.Events; using Tgstation.Server.Host.Components.Interop; using Tgstation.Server.Host.Components.Interop.Bridge; @@ -44,9 +42,9 @@ sealed class SessionControllerFactory : ISessionControllerFactory readonly IProcessExecutor processExecutor; /// - /// The for the . + /// The for the . /// - readonly IByondManager byond; + readonly IEngineManager engineManager; /// /// The for the . @@ -128,52 +126,37 @@ sealed class SessionControllerFactory : ISessionControllerFactory /// readonly Api.Models.Instance instance; - /// - /// Change a given into the appropriate DreamDaemon command line word. - /// - /// The level to change. - /// A representation of the command line parameter. - static string SecurityWord(DreamDaemonSecurity securityLevel) - { - return securityLevel switch - { - DreamDaemonSecurity.Safe => "safe", - DreamDaemonSecurity.Trusted => "trusted", - DreamDaemonSecurity.Ultrasafe => "ultrasafe", - _ => throw new ArgumentOutOfRangeException(nameof(securityLevel), securityLevel, String.Format(CultureInfo.InvariantCulture, "Bad DreamDaemon security level: {0}", securityLevel)), - }; - } - - /// - /// Change a given into the appropriate DreamDaemon command line word. - /// - /// The level to change. - /// A representation of the command line parameter. - static string VisibilityWord(DreamDaemonVisibility visibility) - { - return visibility switch - { - DreamDaemonVisibility.Public => "public", - DreamDaemonVisibility.Private => "private", - DreamDaemonVisibility.Invisible => "invisible", - _ => throw new ArgumentOutOfRangeException(nameof(visibility), visibility, String.Format(CultureInfo.InvariantCulture, "Bad DreamDaemon visibility level: {0}", visibility)), - }; - } - /// /// Check if a given can be bound to. /// /// The port number to test. - void PortBindTest(ushort port) + /// The we're bind testing for. + /// The for the operation. + /// A representing the running operation. + async ValueTask PortBindTest(ushort port, EngineType engineType, CancellationToken cancellationToken) { + logger.LogTrace("Bind test: {port}", port); try { - logger.LogTrace("Bind test: {port}", port); - SocketExtensions.BindTest(port, false); + // GIVE ME THE FUCKING PORT BACK WINDOWS!!!! + const int MaxAttempts = 5; + for (var i = 0; i < MaxAttempts; ++i) + try + { + SocketExtensions.BindTest(platformIdentifier, port, false, engineType == EngineType.OpenDream); + if (i > 0) + logger.LogDebug("Clearing the socket took {iterations} attempts :/", i + 1); + + break; + } + catch (SocketException ex) when (platformIdentifier.IsWindows && ex.SocketErrorCode == SocketError.AddressAlreadyInUse && i < (MaxAttempts - 1)) + { + await asyncDelayer.Delay(TimeSpan.FromSeconds(1), cancellationToken); + } } catch (SocketException ex) when (ex.SocketErrorCode == SocketError.AddressAlreadyInUse) { - throw new JobException(ErrorCode.DreamDaemonPortInUse, ex); + throw new JobException(ErrorCode.GameServerPortInUse, ex); } } @@ -181,7 +164,7 @@ void PortBindTest(ushort port) /// Initializes a new instance of the class. /// /// The value of . - /// The value of . + /// The value of . /// The value of . /// The value of . /// The value of . @@ -200,7 +183,7 @@ void PortBindTest(ushort port) /// The value of . public SessionControllerFactory( IProcessExecutor processExecutor, - IByondManager byond, + IEngineManager engineManager, ITopicClientFactory topicClientFactory, ICryptographySuite cryptographySuite, IAssemblyInformationProvider assemblyInformationProvider, @@ -219,7 +202,7 @@ public SessionControllerFactory( Api.Models.Instance instance) { this.processExecutor = processExecutor ?? throw new ArgumentNullException(nameof(processExecutor)); - this.byond = byond ?? throw new ArgumentNullException(nameof(byond)); + this.engineManager = engineManager ?? throw new ArgumentNullException(nameof(engineManager)); this.topicClientFactory = topicClientFactory ?? throw new ArgumentNullException(nameof(topicClientFactory)); this.cryptographySuite = cryptographySuite ?? throw new ArgumentNullException(nameof(cryptographySuite)); this.assemblyInformationProvider = assemblyInformationProvider ?? throw new ArgumentNullException(nameof(assemblyInformationProvider)); @@ -242,7 +225,7 @@ public SessionControllerFactory( #pragma warning disable CA1506 // TODO: Decomplexify public async ValueTask LaunchNew( IDmbProvider dmbProvider, - IByondExecutableLock currentByondLock, + IEngineExecutableLock? currentByondLock, DreamDaemonLaunchParameters launchParameters, bool apiValidate, CancellationToken cancellationToken) @@ -250,6 +233,7 @@ public async ValueTask LaunchNew( logger.LogTrace("Begin session launch..."); if (!launchParameters.Port.HasValue) throw new InvalidOperationException("Given port is null!"); + switch (dmbProvider.CompileJob.MinimumSecurityLevel) { case DreamDaemonSecurity.Ultrasafe: @@ -273,8 +257,8 @@ public async ValueTask LaunchNew( } // get the byond lock - var byondLock = currentByondLock ?? await byond.UseExecutables( - Version.Parse(dmbProvider.CompileJob.ByondVersion), + var engineLock = currentByondLock ?? await engineManager.UseExecutables( + dmbProvider.EngineVersion, gameIOManager.ConcatPath(dmbProvider.Directory, dmbProvider.DmbName), cancellationToken); try @@ -283,14 +267,18 @@ public async ValueTask LaunchNew( "Launching session with CompileJob {compileJobId}...", dmbProvider.CompileJob.Id); - PortBindTest(launchParameters.Port.Value); - await CheckPagerIsNotRunning(); + // mad this isn't abstracted but whatever + var engineType = dmbProvider.EngineVersion.Engine!.Value; + if (engineType == EngineType.Byond) + await CheckPagerIsNotRunning(); - string outputFilePath = null; + await PortBindTest(launchParameters.Port.Value, engineType, cancellationToken); + + string? outputFilePath = null; var preserveLogFile = true; - var cliSupported = byondLock.SupportsCli; - if (launchParameters.LogOutput.Value) + var hasStandardOutput = engineLock.HasStandardOutput; + if (launchParameters.LogOutput!.Value) { var now = DateTimeOffset.UtcNow; var dateDirectory = diagnosticsIOManager.ConcatPath(DreamDaemonLogsPath, now.ToString("yyyy-MM-dd", CultureInfo.InvariantCulture)); @@ -298,30 +286,25 @@ public async ValueTask LaunchNew( outputFilePath = diagnosticsIOManager.ResolvePath( diagnosticsIOManager.ConcatPath( dateDirectory, - $"dd-utc-{now.ToString("yyyy-MM-dd-HH-mm-ss", CultureInfo.InvariantCulture)}{(apiValidate ? "-dmapi" : String.Empty)}.log")); + $"server-utc-{now.ToString("yyyy-MM-dd-HH-mm-ss", CultureInfo.InvariantCulture)}{(apiValidate ? "-dmapi" : String.Empty)}.log")); - logger.LogInformation("Logging DreamDaemon output to {path}...", outputFilePath); + logger.LogInformation("Logging server output to {path}...", outputFilePath); } - else if (!cliSupported) + else if (!hasStandardOutput) { - outputFilePath = gameIOManager.ConcatPath(dmbProvider.Directory, $"{Guid.NewGuid()}.dd.log"); + outputFilePath = gameIOManager.ConcatPath(dmbProvider.Directory, $"{Guid.NewGuid()}.server.log"); preserveLogFile = false; } var accessIdentifier = cryptographySuite.GetSecureString(); - var byondTopicSender = topicClientFactory.CreateTopicClient( - TimeSpan.FromMilliseconds( - launchParameters.TopicRequestTimeout.Value)); - if (!apiValidate && dmbProvider.CompileJob.DMApiVersion == null) logger.LogDebug("Session will have no DMAPI support!"); // launch dd - var process = await CreateDreamDaemonProcess( + var process = await CreateGameServerProcess( dmbProvider, - byondTopicSender, - byondLock, + engineLock, launchParameters, accessIdentifier, outputFilePath, @@ -337,8 +320,8 @@ public async ValueTask LaunchNew( var runtimeInformation = CreateRuntimeInformation( dmbProvider, chatTrackingContext, - launchParameters.SecurityLevel.Value, - launchParameters.Visibility.Value, + launchParameters.SecurityLevel!.Value, + launchParameters.Visibility!.Value, apiValidate); var reattachInformation = new ReattachInformation( @@ -348,11 +331,15 @@ public async ValueTask LaunchNew( accessIdentifier, launchParameters.Port.Value); + var byondTopicSender = topicClientFactory.CreateTopicClient( + TimeSpan.FromMilliseconds( + launchParameters.TopicRequestTimeout!.Value)); + var sessionController = new SessionController( reattachInformation, instance, process, - byondLock, + engineLock, byondTopicSender, chatTrackingContext, bridgeRegistrar, @@ -363,7 +350,7 @@ public async ValueTask LaunchNew( () => LogDDOutput( process, outputFilePath, - cliSupported, + hasStandardOutput, preserveLogFile, CancellationToken.None), // DCT: None available launchParameters.StartupTimeout, @@ -391,14 +378,14 @@ public async ValueTask LaunchNew( catch { if (currentByondLock == null) - byondLock.Dispose(); + engineLock.Dispose(); throw; } } #pragma warning restore CA1506 /// - public async ValueTask Reattach( + public async ValueTask Reattach( ReattachInformation reattachInformation, CancellationToken cancellationToken) { @@ -406,8 +393,8 @@ public async ValueTask Reattach( logger.LogTrace("Begin session reattach..."); var byondTopicSender = topicClientFactory.CreateTopicClient(reattachInformation.TopicRequestTimeout); - var byondLock = await byond.UseExecutables( - Version.Parse(reattachInformation.Dmb.CompileJob.ByondVersion), + var engineLock = await engineManager.UseExecutables( + reattachInformation.Dmb.EngineVersion, null, // Doesn't matter if it's trusted or not on reattach cancellationToken); @@ -424,7 +411,7 @@ public async ValueTask Reattach( try { - if (!byondLock.SupportsCli) + if (engineLock.PromptsForNetworkAccess) networkPromptReaper.RegisterProcess(process); var chatTrackingContext = chat.CreateTrackingContext(); @@ -442,7 +429,7 @@ public async ValueTask Reattach( reattachInformation, instance, process, - byondLock, + engineLock, byondTopicSender, chatTrackingContext, bridgeRegistrar, @@ -456,85 +443,72 @@ public async ValueTask Reattach( false); process = null; - byondLock = null; + engineLock = null; chatTrackingContext = null; return controller; } catch { - chatTrackingContext.Dispose(); + chatTrackingContext?.Dispose(); throw; } } catch { - await process.DisposeAsync(); + if (process != null) + await process.DisposeAsync(); + throw; } } catch { - byondLock.Dispose(); + engineLock?.Dispose(); throw; } } /// - /// Creates the DreamDaemon . + /// Creates the game server . /// /// The . - /// The to use for sanitization. - /// The . + /// The . /// The . /// The secure string to use for the session. - /// The path to log DreamDaemon output to. + /// The optional full path to log DreamDaemon output to. /// If we are only validating the DMAPI then exiting. /// The for the operation. /// A resulting in the DreamDaemon . - async ValueTask CreateDreamDaemonProcess( + async ValueTask CreateGameServerProcess( IDmbProvider dmbProvider, - ITopicClient byondTopicSender, - IByondExecutableLock byondLock, + IEngineExecutableLock engineLock, DreamDaemonLaunchParameters launchParameters, string accessIdentifier, - string logFilePath, + string? logFilePath, bool apiValidate, CancellationToken cancellationToken) { - // set command line options - // more sanitization here cause it uses the same scheme - var parameters = $"{DMApiConstants.ParamApiVersion}={byondTopicSender.SanitizeString(DMApiConstants.InteropVersion.Semver().ToString())}&{byondTopicSender.SanitizeString(DMApiConstants.ParamServerPort)}={serverPortProvider.HttpApiPort}&{byondTopicSender.SanitizeString(DMApiConstants.ParamAccessIdentifier)}={byondTopicSender.SanitizeString(accessIdentifier)}"; - - if (!String.IsNullOrEmpty(launchParameters.AdditionalParameters)) - parameters = $"{parameters}&{launchParameters.AdditionalParameters}"; - // important to run on all ports to allow port changing - var arguments = String.Format( - CultureInfo.InvariantCulture, - "{0} -port {1} -ports 1-65535 {2}-close -verbose -{3} -{4}{5}{6}{7} -params \"{8}\"", - dmbProvider.DmbName, - launchParameters.Port.Value, - launchParameters.AllowWebClient.Value ? "-webclient " : String.Empty, - SecurityWord(launchParameters.SecurityLevel.Value), - VisibilityWord(launchParameters.Visibility.Value), - !byondLock.SupportsCli - ? $" -logself -log {logFilePath}" - : String.Empty, // DD doesn't output anything if -logself is set??? - launchParameters.StartProfiler.Value - ? " -profile" - : String.Empty, - byondLock.SupportsMapThreads && launchParameters.MapThreads.Value != 0 - ? $" -map-threads {launchParameters.MapThreads.Value}" - : String.Empty, - parameters); + var arguments = engineLock.FormatServerArguments( + dmbProvider, + new Dictionary + { + { DMApiConstants.ParamApiVersion, DMApiConstants.InteropVersion.Semver().ToString() }, + { DMApiConstants.ParamServerPort, serverPortProvider.HttpApiPort.ToString(CultureInfo.InvariantCulture) }, + { DMApiConstants.ParamAccessIdentifier, accessIdentifier }, + }, + launchParameters, + !engineLock.HasStandardOutput || engineLock.PreferFileLogging + ? logFilePath + : null); var process = processExecutor.LaunchProcess( - byondLock.DreamDaemonPath, + engineLock.ServerExePath, dmbProvider.Directory, arguments, logFilePath, - byondLock.SupportsCli, + engineLock.HasStandardOutput, true); try @@ -547,7 +521,7 @@ async ValueTask CreateDreamDaemonProcess( else if (sessionConfiguration.LowPriorityDeploymentProcesses) process.AdjustPriority(false); - if (!byondLock.SupportsCli) + if (!engineLock.HasStandardOutput) networkPromptReaper.RegisterProcess(process); // If this isnt a staging DD (From a Deployment), fire off an event @@ -583,19 +557,19 @@ await eventConsumer.HandleEvent( /// If , will be deleted. /// The for the operation. /// A representing the running operation. - async ValueTask LogDDOutput(IProcess process, string outputFilePath, bool cliSupported, bool preserveFile, CancellationToken cancellationToken) + async ValueTask LogDDOutput(IProcess process, string? outputFilePath, bool cliSupported, bool preserveFile, CancellationToken cancellationToken) { try { - string ddOutput = null; + string? ddOutput = null; if (cliSupported) - ddOutput = await process.GetCombinedOutput(cancellationToken); + ddOutput = (await process.GetCombinedOutput(cancellationToken))!; if (ddOutput == null) try { var dreamDaemonLogBytes = await gameIOManager.ReadAllBytes( - outputFilePath, + outputFilePath!, cancellationToken); ddOutput = Encoding.UTF8.GetString(dreamDaemonLogBytes); @@ -606,22 +580,24 @@ async ValueTask LogDDOutput(IProcess process, string outputFilePath, bool cliSup try { logger.LogTrace("Deleting temporary log file {path}...", outputFilePath); - await gameIOManager.DeleteFile(outputFilePath, cancellationToken); + await gameIOManager.DeleteFile(outputFilePath!, cancellationToken); } catch (Exception ex) { - logger.LogWarning(ex, "Failed to delete DreamDaemon log file {outputFilePath}!", outputFilePath); + // this is expected on OD at time of the support changes. + // I've open a change to fix it: https://github.com/space-wizards/RobustToolbox/pull/4501 + logger.LogWarning(ex, "Failed to delete server log file {outputFilePath}!", outputFilePath); } } logger.LogTrace( - "DreamDaemon Output:{newLine}{output}", + "Server Output:{newLine}{output}", Environment.NewLine, ddOutput); } catch (Exception ex) { - logger.LogWarning(ex, "Error reading DreamDaemon output!"); + logger.LogWarning(ex, "Error reading server output!"); } } @@ -640,11 +616,11 @@ RuntimeInformation CreateRuntimeInformation( DreamDaemonSecurity securityLevel, DreamDaemonVisibility visibility, bool apiValidateOnly) - => new ( + => new( chatTrackingContext, dmbProvider, assemblyInformationProvider.Version, - instance.Name, + instance.Name!, securityLevel, visibility, serverPortProvider.HttpApiPort, @@ -669,7 +645,7 @@ async ValueTask CheckPagerIsNotRunning() var ourUsername = ourProcess.GetExecutingUsername(); if (otherUsername.Equals(ourUsername, StringComparison.Ordinal)) - throw new JobException(ErrorCode.DeploymentPagerRunning); + throw new JobException(ErrorCode.DreamDaemonPagerRunning); } } } diff --git a/src/Tgstation.Server.Host/Components/Session/SessionPersistor.cs b/src/Tgstation.Server.Host/Components/Session/SessionPersistor.cs index 5c3819f8bea..a916ea792ba 100644 --- a/src/Tgstation.Server.Host/Components/Session/SessionPersistor.cs +++ b/src/Tgstation.Server.Host/Components/Session/SessionPersistor.cs @@ -8,6 +8,7 @@ using Tgstation.Server.Host.Components.Deployment; using Tgstation.Server.Host.Database; +using Tgstation.Server.Host.Models; using Tgstation.Server.Host.System; using Z.EntityFramework.Plus; @@ -69,15 +70,14 @@ public ValueTask Save(ReattachInformation reattachInformation, CancellationToken { ArgumentNullException.ThrowIfNull(reattachInformation); - logger.LogDebug("Saving reattach information: {info}...", reattachInformation); + logger.LogTrace("Saving reattach information: {info}...", reattachInformation); await ClearImpl(db, false, cancellationToken); - var dbReattachInfo = new Models.ReattachInformation + var dbReattachInfo = new Models.ReattachInformation(reattachInformation.AccessIdentifier) { - AccessIdentifier = reattachInformation.AccessIdentifier, - CompileJobId = reattachInformation.Dmb.CompileJob.Id.Value, - InitialCompileJobId = reattachInformation.InitialDmb?.CompileJob.Id.Value, + CompileJobId = reattachInformation.Dmb.CompileJob.Require(x => x.Id), + InitialCompileJobId = reattachInformation.InitialDmb?.CompileJob.Require(x => x.Id), Port = reattachInformation.Port, ProcessId = reattachInformation.ProcessId, RebootState = reattachInformation.RebootState, @@ -87,12 +87,45 @@ public ValueTask Save(ReattachInformation reattachInformation, CancellationToken db.ReattachInformations.Add(dbReattachInfo); await db.Save(cancellationToken); + + reattachInformation.Id = dbReattachInfo.Id!.Value; + logger.LogDebug("Saved reattach information: {info}", reattachInformation); + }); + + /// + public ValueTask Update(ReattachInformation reattachInformation, CancellationToken cancellationToken) => databaseContextFactory.UseContextTaskReturn(async db => + { + ArgumentNullException.ThrowIfNull(reattachInformation); + if (!reattachInformation.Id.HasValue) + throw new InvalidOperationException("Provided reattachInformation has no Id!"); + + logger.LogTrace("Updating reattach information: {info}...", reattachInformation); + + var dbReattachInfo = new Models.ReattachInformation(String.Empty) + { + Id = reattachInformation.Id.Value, + }; + + db.ReattachInformations.Attach(dbReattachInfo); + + dbReattachInfo.AccessIdentifier = reattachInformation.AccessIdentifier; + dbReattachInfo.CompileJobId = reattachInformation.Dmb.CompileJob.Require(x => x.Id); + dbReattachInfo.InitialCompileJobId = reattachInformation.InitialDmb?.CompileJob.Require(x => x.Id); + dbReattachInfo.Port = reattachInformation.Port; + dbReattachInfo.ProcessId = reattachInformation.ProcessId; + dbReattachInfo.RebootState = reattachInformation.RebootState; + dbReattachInfo.LaunchSecurityLevel = reattachInformation.LaunchSecurityLevel; + dbReattachInfo.LaunchVisibility = reattachInformation.LaunchVisibility; + + await db.Save(cancellationToken); + + logger.LogDebug("Updated reattach information: {info}", reattachInformation); }); /// - public async ValueTask Load(CancellationToken cancellationToken) + public async ValueTask Load(CancellationToken cancellationToken) { - Models.ReattachInformation result = null; + Models.ReattachInformation? result = null; TimeSpan? topicTimeout = null; async ValueTask KillProcess(Models.ReattachInformation reattachInfo) @@ -126,19 +159,19 @@ await databaseContextFactory.UseContext(async (db) => var dbReattachInfos = await db .ReattachInformations .AsQueryable() - .Where(x => x.CompileJob.Job.Instance.Id == metadata.Id) + .Where(x => x.CompileJob!.Job.Instance!.Id == metadata.Id) .Include(x => x.CompileJob) .Include(x => x.InitialCompileJob) .ToListAsync(cancellationToken); result = dbReattachInfos.FirstOrDefault(); - if (result == default) + if (result == null) return; var timeoutMilliseconds = await db .Instances .AsQueryable() .Where(x => x.Id == metadata.Id) - .Select(x => x.DreamDaemonSettings.TopicRequestTimeout) + .Select(x => x.DreamDaemonSettings!.TopicRequestTimeout) .FirstOrDefaultAsync(cancellationToken); if (timeoutMilliseconds == default) @@ -173,7 +206,7 @@ await databaseContextFactory.UseContext(async (db) => return null; } - var dmb = await dmbFactory.FromCompileJob(result.CompileJob, cancellationToken); + var dmb = await dmbFactory.FromCompileJob(result!.CompileJob!, cancellationToken); if (dmb == null) { logger.LogError("Unable to reattach! Could not load .dmb!"); @@ -191,7 +224,7 @@ await db return null; } - IDmbProvider initialDmb = null; + IDmbProvider? initialDmb = null; if (result.InitialCompileJob != null) { logger.LogTrace("Loading initial compile job..."); @@ -232,7 +265,7 @@ async ValueTask ClearImpl(IDatabaseContext databaseContext, bool instant, Cancel var baseQuery = databaseContext .ReattachInformations .AsQueryable() - .Where(x => x.CompileJob.Job.Instance.Id == metadata.Id); + .Where(x => x.CompileJob!.Job.Instance!.Id == metadata.Id); if (instant) await baseQuery diff --git a/src/Tgstation.Server.Host/Components/Session/TopicClientFactory.cs b/src/Tgstation.Server.Host/Components/Session/TopicClientFactory.cs index 40605e5de8e..3ed153a6412 100644 --- a/src/Tgstation.Server.Host/Components/Session/TopicClientFactory.cs +++ b/src/Tgstation.Server.Host/Components/Session/TopicClientFactory.cs @@ -1,6 +1,7 @@ using System; using Byond.TopicSender; + using Microsoft.Extensions.Logging; namespace Tgstation.Server.Host.Components.Session @@ -11,7 +12,7 @@ sealed class TopicClientFactory : ITopicClientFactory /// /// The for created s. /// - readonly ILogger logger; + readonly ILogger? logger; /// /// Initializes a new instance of the class. diff --git a/src/Tgstation.Server.Host/Components/StaticFiles/Configuration.cs b/src/Tgstation.Server.Host/Components/StaticFiles/Configuration.cs index a9406e0e8d8..8261afe9181 100644 --- a/src/Tgstation.Server.Host/Components/StaticFiles/Configuration.cs +++ b/src/Tgstation.Server.Host/Components/StaticFiles/Configuration.cs @@ -71,18 +71,18 @@ sealed class Configuration : IConfiguration /// /// Map of s to the filename of the event scripts they trigger. /// - static readonly IReadOnlyDictionary EventTypeScriptFileNameMap = new Dictionary( + public static IReadOnlyDictionary> EventTypeScriptFileNameMap { get; } = new Dictionary>( Enum.GetValues(typeof(EventType)) .Cast() .Select( - eventType => new KeyValuePair( + eventType => new KeyValuePair>( eventType, typeof(EventType) - .GetField(eventType.ToString()) + .GetField(eventType.ToString())! .GetCustomAttributes(false) .OfType() .First() - .ScriptName))); + .ScriptNames))); /// /// The for . @@ -199,7 +199,7 @@ public void Dispose() } /// - public async ValueTask CopyDMFilesTo(string dmeFile, string destination, CancellationToken cancellationToken) + public async ValueTask CopyDMFilesTo(string dmeFile, string destination, CancellationToken cancellationToken) { using (await SemaphoreSlimContext.Lock(semaphore, cancellationToken)) { @@ -232,12 +232,19 @@ public async ValueTask CopyDMFilesTo(string dmeFile, st static string IncludeLine(string filePath) => String.Format(CultureInfo.InvariantCulture, "#include \"{0}\"", filePath); - return new ServerSideModifications(headFileExistsTask.Result ? IncludeLine(CodeModificationsHeadFile) : null, tailFileExistsTask.Result ? IncludeLine(CodeModificationsTailFile) : null, false); + return new ServerSideModifications( + headFileExistsTask.Result + ? IncludeLine(CodeModificationsHeadFile) + : null, + tailFileExistsTask.Result + ? IncludeLine(CodeModificationsTailFile) + : null, + false); } } /// - public async ValueTask> ListDirectory(string configurationRelativePath, ISystemIdentity systemIdentity, CancellationToken cancellationToken) + public async ValueTask?> ListDirectory(string? configurationRelativePath, ISystemIdentity? systemIdentity, CancellationToken cancellationToken) { await EnsureDirectories(cancellationToken); var path = ValidateConfigRelativePath(configurationRelativePath); @@ -284,12 +291,12 @@ void ListImpl() } /// - public async ValueTask Read(string configurationRelativePath, ISystemIdentity systemIdentity, CancellationToken cancellationToken) + public async ValueTask Read(string configurationRelativePath, ISystemIdentity? systemIdentity, CancellationToken cancellationToken) { await EnsureDirectories(cancellationToken); var path = ValidateConfigRelativePath(configurationRelativePath); - ConfigurationFileResponse result = null; + ConfigurationFileResponse? result = null; void ReadImpl() { @@ -298,8 +305,7 @@ void ReadImpl() string GetFileSha() { var content = synchronousIOManager.ReadFile(path); - using var sha1 = SHA1.Create(); - return String.Join(String.Empty, sha1.ComputeHash(content).Select(b => b.ToString("x2", CultureInfo.InvariantCulture))); + return String.Join(String.Empty, SHA1.HashData(content).Select(b => b.ToString("x2", CultureInfo.InvariantCulture))); } var originalSha = GetFileSha(); @@ -320,7 +326,7 @@ string GetFileSha() }, async cancellationToken => { - FileStream result = null; + FileStream? result = null; void GetFileStream() { result = ioManager.GetFileStream(path, false); @@ -331,7 +337,7 @@ void GetFileStream() else await systemIdentity.RunImpersonated(GetFileStream, cancellationToken); - return result; + return result!; }, path, false)); @@ -406,12 +412,10 @@ await ValueTaskExtensions.WhenAll(entries.Select(async file = var fileName = ioManager.GetFileName(file); // need to normalize - bool ignored; - if (platformIdentifier.IsWindows) - ignored = ignoreFiles.Any(y => fileName.ToUpperInvariant() == y.ToUpperInvariant()); - else - ignored = ignoreFiles.Any(y => fileName == y); - + var fileComparison = platformIdentifier.IsWindows + ? StringComparison.OrdinalIgnoreCase + : StringComparison.Ordinal; + var ignored = ignoreFiles.Any(y => fileName.Equals(y, fileComparison)); if (ignored) { logger.LogTrace("Ignoring static file {fileName}...", fileName); @@ -442,22 +446,24 @@ await ValueTaskExtensions.WhenAll(entries.Select(async file = using (var reader = new StringReader(ignoreFileText)) { cancellationToken.ThrowIfCancellationRequested(); - var line = await reader.ReadLineAsync(); + var line = await reader.ReadLineAsync(cancellationToken); if (!String.IsNullOrEmpty(line)) ignoreFiles.Add(line); } - await ValueTaskExtensions.WhenAll(SymlinkBase(true), SymlinkBase(false)); + var filesSymlinkTask = SymlinkBase(true); + var dirsSymlinkTask = SymlinkBase(false); + await ValueTaskExtensions.WhenAll(filesSymlinkTask, dirsSymlinkTask); } } /// - public async ValueTask Write(string configurationRelativePath, ISystemIdentity systemIdentity, string previousHash, CancellationToken cancellationToken) + public async ValueTask Write(string configurationRelativePath, ISystemIdentity? systemIdentity, string? previousHash, CancellationToken cancellationToken) { await EnsureDirectories(cancellationToken); var path = ValidateConfigRelativePath(configurationRelativePath); - ConfigurationFileResponse result = null; + ConfigurationFileResponse? result = null; void WriteImpl() { @@ -562,7 +568,7 @@ void WriteCallback() } /// - public async ValueTask CreateDirectory(string configurationRelativePath, ISystemIdentity systemIdentity, CancellationToken cancellationToken) + public async ValueTask CreateDirectory(string configurationRelativePath, ISystemIdentity? systemIdentity, CancellationToken cancellationToken) { await EnsureDirectories(cancellationToken); var path = ValidateConfigRelativePath(configurationRelativePath); @@ -584,7 +590,7 @@ void WriteCallback() await systemIdentity.RunImpersonated(DoCreate, cancellationToken); } - return result.Value; + return result!.Value; } /// @@ -594,13 +600,13 @@ void WriteCallback() public Task StopAsync(CancellationToken cancellationToken) => EnsureDirectories(cancellationToken); /// - public async ValueTask HandleEvent(EventType eventType, IEnumerable parameters, bool deploymentPipeline, CancellationToken cancellationToken) + public async ValueTask HandleEvent(EventType eventType, IEnumerable parameters, bool deploymentPipeline, CancellationToken cancellationToken) { ArgumentNullException.ThrowIfNull(parameters); await EnsureDirectories(cancellationToken); - if (!EventTypeScriptFileNameMap.TryGetValue(eventType, out var scriptName)) + if (!EventTypeScriptFileNameMap.TryGetValue(eventType, out var scriptNames)) return; // always execute in serial @@ -611,12 +617,13 @@ public async ValueTask HandleEvent(EventType eventType, IEnumerable para var scriptFiles = files .Select(x => ioManager.GetFileName(x)) - .Where(x => x.StartsWith(scriptName, StringComparison.Ordinal)) + .Where(x => scriptNames.Any( + scriptName => x.StartsWith(scriptName, StringComparison.Ordinal))) .ToList(); - if (!scriptFiles.Any()) + if (scriptFiles.Count == 0) { - logger.LogTrace("No event scripts starting with \"{scriptName}\" detected", scriptName); + logger.LogTrace("No event scripts starting with \"{scriptName}\" detected", String.Join("\" or \"", scriptNames)); return; } @@ -630,6 +637,9 @@ public async ValueTask HandleEvent(EventType eventType, IEnumerable para ' ', parameters.Select(arg => { + if (arg == null) + return "(NULL)"; + if (!arg.Contains(' ', StringComparison.Ordinal)) return arg; @@ -657,7 +667,7 @@ public async ValueTask HandleEvent(EventType eventType, IEnumerable para } /// - public async ValueTask DeleteDirectory(string configurationRelativePath, ISystemIdentity systemIdentity, CancellationToken cancellationToken) + public async ValueTask DeleteDirectory(string configurationRelativePath, ISystemIdentity? systemIdentity, CancellationToken cancellationToken) { await EnsureDirectories(cancellationToken); var path = ValidateConfigRelativePath(configurationRelativePath); @@ -709,19 +719,19 @@ async Task ValidateCodeModsFolder() return; await ioManager.CreateDirectory(CodeModificationsSubdirectory, cancellationToken); - await ValueTaskExtensions.WhenAll( - ioManager.WriteAllBytes( - ioManager.ConcatPath( - CodeModificationsSubdirectory, - CodeModificationsHeadFile), - Encoding.UTF8.GetBytes(DefaultHeadInclude), - cancellationToken), - ioManager.WriteAllBytes( - ioManager.ConcatPath( - CodeModificationsSubdirectory, - CodeModificationsTailFile), - Encoding.UTF8.GetBytes(DefaultTailInclude), - cancellationToken)); + var headWriteTask = ioManager.WriteAllBytes( + ioManager.ConcatPath( + CodeModificationsSubdirectory, + CodeModificationsHeadFile), + Encoding.UTF8.GetBytes(DefaultHeadInclude), + cancellationToken); + var tailWriteTask = ioManager.WriteAllBytes( + ioManager.ConcatPath( + CodeModificationsSubdirectory, + CodeModificationsTailFile), + Encoding.UTF8.GetBytes(DefaultTailInclude), + cancellationToken); + await ValueTaskExtensions.WhenAll(headWriteTask, tailWriteTask); } return Task.WhenAll( @@ -735,16 +745,16 @@ await ValueTaskExtensions.WhenAll( /// /// A relative path in the instance's configuration directory. /// The full on-disk path of . - string ValidateConfigRelativePath(string configurationRelativePath) + string ValidateConfigRelativePath(string? configurationRelativePath) { var nullOrEmptyCheck = String.IsNullOrEmpty(configurationRelativePath); if (nullOrEmptyCheck) configurationRelativePath = DefaultIOManager.CurrentDirectory; - if (configurationRelativePath[0] == Path.DirectorySeparatorChar || configurationRelativePath[0] == Path.AltDirectorySeparatorChar) + if (configurationRelativePath![0] == Path.DirectorySeparatorChar || configurationRelativePath[0] == Path.AltDirectorySeparatorChar) configurationRelativePath = DefaultIOManager.CurrentDirectory + configurationRelativePath; var resolved = ioManager.ResolvePath(configurationRelativePath); var local = !nullOrEmptyCheck ? ioManager.ResolvePath() : null; - if (!nullOrEmptyCheck && resolved.Length < local.Length) // .. fuccbois + if (!nullOrEmptyCheck && resolved.Length < local!.Length) // .. fuccbois throw new InvalidOperationException("Attempted to access file outside of configuration manager!"); return resolved; } diff --git a/src/Tgstation.Server.Host/Components/StaticFiles/IConfiguration.cs b/src/Tgstation.Server.Host/Components/StaticFiles/IConfiguration.cs index 59546a044ef..d156df54cb2 100644 --- a/src/Tgstation.Server.Host/Components/StaticFiles/IConfiguration.cs +++ b/src/Tgstation.Server.Host/Components/StaticFiles/IConfiguration.cs @@ -22,7 +22,7 @@ public interface IConfiguration : IComponentService, IEventConsumer, IDisposable /// Path to the destination folder. /// The for the operation. /// A resulting in the if any. - ValueTask CopyDMFilesTo(string dmeFile, string destination, CancellationToken cancellationToken); + ValueTask CopyDMFilesTo(string dmeFile, string destination, CancellationToken cancellationToken); /// /// Symlinks all directories in the GameData directory to . @@ -39,7 +39,7 @@ public interface IConfiguration : IComponentService, IEventConsumer, IDisposable /// The for the operation. If , the operation will be performed as the user of the . /// The for the operation. /// A resulting in an of the s for the items in the directory. and will both be . will be returned if the operation failed due to access contention. - ValueTask> ListDirectory(string configurationRelativePath, ISystemIdentity systemIdentity, CancellationToken cancellationToken); + ValueTask?> ListDirectory(string? configurationRelativePath, ISystemIdentity? systemIdentity, CancellationToken cancellationToken); /// /// Reads a given . @@ -48,7 +48,7 @@ public interface IConfiguration : IComponentService, IEventConsumer, IDisposable /// The for the operation. If , the operation will be performed as the user of the . /// The for the operation. /// A resulting in the of the file. will be returned if the operation failed due to access contention. - ValueTask Read(string configurationRelativePath, ISystemIdentity systemIdentity, CancellationToken cancellationToken); + ValueTask Read(string configurationRelativePath, ISystemIdentity? systemIdentity, CancellationToken cancellationToken); /// /// Create an empty directory at . @@ -57,7 +57,7 @@ public interface IConfiguration : IComponentService, IEventConsumer, IDisposable /// The for the operation. If , the operation will be performed as the user of the . /// The for the operation. Usage may result in partial writes. /// A resulting in if the directory already existed, otherwise. will be returned if the operation failed due to access contention. - ValueTask CreateDirectory(string configurationRelativePath, ISystemIdentity systemIdentity, CancellationToken cancellationToken); + ValueTask CreateDirectory(string configurationRelativePath, ISystemIdentity? systemIdentity, CancellationToken cancellationToken); /// /// Attempt to delete an empty directory at . @@ -66,7 +66,7 @@ public interface IConfiguration : IComponentService, IEventConsumer, IDisposable /// The for the operation. If , the operation will be performed as the user of the . /// The for the operation. /// A resulting in if the directory was empty and deleted, otherwise. will be returned if the operation failed due to access contention. - ValueTask DeleteDirectory(string configurationRelativePath, ISystemIdentity systemIdentity, CancellationToken cancellationToken); + ValueTask DeleteDirectory(string configurationRelativePath, ISystemIdentity? systemIdentity, CancellationToken cancellationToken); /// /// Writes to a given . @@ -76,6 +76,6 @@ public interface IConfiguration : IComponentService, IEventConsumer, IDisposable /// The hash any existing file must match in order for the write to succeed. /// The for the operation. Usage may result in partial writes. /// A resulting in the updated and associated writing . will be returned if the operation failed due to access contention. - ValueTask Write(string configurationRelativePath, ISystemIdentity systemIdentity, string previousHash, CancellationToken cancellationToken); + ValueTask Write(string configurationRelativePath, ISystemIdentity? systemIdentity, string? previousHash, CancellationToken cancellationToken); } } diff --git a/src/Tgstation.Server.Host/Components/StaticFiles/ServerSideModifications.cs b/src/Tgstation.Server.Host/Components/StaticFiles/ServerSideModifications.cs index 6b7e358f0b5..e28920a06ed 100644 --- a/src/Tgstation.Server.Host/Components/StaticFiles/ServerSideModifications.cs +++ b/src/Tgstation.Server.Host/Components/StaticFiles/ServerSideModifications.cs @@ -13,12 +13,12 @@ public sealed class ServerSideModifications /// /// The #include line which should be added to the beginning of the .dme if any. /// - public string HeadIncludeLine { get; } + public string? HeadIncludeLine { get; } /// /// The #include line which should be added to the end of the .dme if any. /// - public string TailIncludeLine { get; } + public string? TailIncludeLine { get; } /// /// Initializes a new instance of the class. @@ -26,7 +26,7 @@ public sealed class ServerSideModifications /// The value of . /// The value of . /// The value of . - public ServerSideModifications(string headIncludeLine, string tailIncludeLine, bool totalDmeOverwrite) + public ServerSideModifications(string? headIncludeLine, string? tailIncludeLine, bool totalDmeOverwrite) { HeadIncludeLine = headIncludeLine; TailIncludeLine = tailIncludeLine; diff --git a/src/Tgstation.Server.Host/Components/Watchdog/AdvancedWatchdog.cs b/src/Tgstation.Server.Host/Components/Watchdog/AdvancedWatchdog.cs index 00e91224796..884541e9fb8 100644 --- a/src/Tgstation.Server.Host/Components/Watchdog/AdvancedWatchdog.cs +++ b/src/Tgstation.Server.Host/Components/Watchdog/AdvancedWatchdog.cs @@ -6,6 +6,7 @@ using Microsoft.Extensions.Logging; +using Tgstation.Server.Api.Models; using Tgstation.Server.Api.Models.Internal; using Tgstation.Server.Host.Components.Chat; using Tgstation.Server.Host.Components.Deployment; @@ -27,7 +28,7 @@ abstract class AdvancedWatchdog : BasicWatchdog /// /// The for . /// - protected SwappableDmbProvider ActiveSwappable { get; private set; } + protected SwappableDmbProvider? ActiveSwappable { get; private set; } /// /// The for the . @@ -42,12 +43,12 @@ abstract class AdvancedWatchdog : BasicWatchdog /// /// The active for . /// - SwappableDmbProvider pendingSwappable; + SwappableDmbProvider? pendingSwappable; /// /// The representing the cleanup of an unused . /// - volatile TaskCompletionSource deploymentCleanupGate; + volatile TaskCompletionSource? deploymentCleanupGate; /// /// Initializes a new instance of the class. @@ -140,11 +141,12 @@ protected sealed override async ValueTask HandleNormalReboot(Canc ValueTask RunPrequel() => BeforeApplyDmb(pendingSwappable.CompileJob, cancellationToken); var needToSwap = !pendingSwappable.Swapped; + var controller = Server!; if (needToSwap) { // IMPORTANT: THE SESSIONCONTROLLER SHOULD STILL BE PROCESSING THE BRIDGE REQUEST SO WE KNOW DD IS SLEEPING // OTHERWISE, IT COULD RETURN TO /world/Reboot() TOO EARLY AND LOAD THE WRONG .DMB - if (!Server.ProcessingRebootBridgeRequest) + if (!controller.ProcessingRebootBridgeRequest) { // integration test logging will catch this Logger.LogError( @@ -167,7 +169,7 @@ protected sealed override async ValueTask HandleNormalReboot(Canc if (needToSwap) await PerformDmbSwap(pendingSwappable, cancellationToken); - var currentCompileJobId = Server.ReattachInformation.Dmb.CompileJob.Id; + var currentCompileJobId = controller.ReattachInformation.Dmb.CompileJob.Id; await DrainDeploymentCleanupTasks(false); @@ -175,7 +177,7 @@ protected sealed override async ValueTask HandleNormalReboot(Canc var localDeploymentCleanupGate = new TaskCompletionSource(); async Task CleanupLingeringDeployment() { - var lingeringDeploymentExpirySeconds = ActiveLaunchParameters.StartupTimeout.Value; + var lingeringDeploymentExpirySeconds = ActiveLaunchParameters.StartupTimeout!.Value; Logger.LogDebug( "Holding old deployment {compileJobId} for up to {expiry} seconds...", currentCompileJobId, @@ -208,7 +210,7 @@ async Task CleanupLingeringDeployment() lock (deploymentCleanupTasks) { - lingeringDeployment = Server.ReplaceDmbProvider(pendingSwappable); + lingeringDeployment = controller.ReplaceDmbProvider(pendingSwappable); deploymentCleanupTasks.Add( CleanupLingeringDeployment()); } @@ -216,7 +218,7 @@ async Task CleanupLingeringDeployment() ActiveSwappable = pendingSwappable; pendingSwappable = null; - await SessionPersistor.Save(Server.ReattachInformation, cancellationToken); + await SessionPersistor.Update(controller.ReattachInformation, cancellationToken); await updateTask; } else @@ -229,41 +231,41 @@ async Task CleanupLingeringDeployment() protected sealed override async ValueTask HandleNewDmbAvailable(CancellationToken cancellationToken) { IDmbProvider compileJobProvider = DmbFactory.LockNextDmb(1); - bool canSeamlesslySwap = true; - - if (compileJobProvider.CompileJob.ByondVersion != ActiveCompileJob.ByondVersion) - { - // have to do a graceful restart - Logger.LogDebug( - "Not swapping to new compile job {0} as it uses a different BYOND version ({1}) than what is currently active {2}. Queueing graceful restart instead...", - compileJobProvider.CompileJob.Id, - compileJobProvider.CompileJob.ByondVersion, - ActiveCompileJob.ByondVersion); - canSeamlesslySwap = false; - } - - if (compileJobProvider.CompileJob.DmeName != ActiveCompileJob.DmeName) - { - Logger.LogDebug( - "Not swapping to new compile job {0} as it uses a different .dmb name ({1}) than what is currently active {2}. Queueing graceful restart instead...", - compileJobProvider.CompileJob.Id, - compileJobProvider.CompileJob.DmeName, - ActiveCompileJob.DmeName); - canSeamlesslySwap = false; - } + bool canSeamlesslySwap = CanUseSwappableDmbProvider(compileJobProvider); + if (canSeamlesslySwap) + if (compileJobProvider.CompileJob.EngineVersion != ActiveCompileJob!.EngineVersion) + { + // have to do a graceful restart + Logger.LogDebug( + "Not swapping to new compile job {compileJobId} as it uses a different engine version ({newEngineVersion}) than what is currently active {oldEngineVersion}.", + compileJobProvider.CompileJob.Id, + compileJobProvider.CompileJob.EngineVersion, + ActiveCompileJob.EngineVersion); + canSeamlesslySwap = false; + } + else if (compileJobProvider.CompileJob.DmeName != ActiveCompileJob.DmeName) + { + Logger.LogDebug( + "Not swapping to new compile job {compileJobId} as it uses a different .dmb name ({newDmbName}) than what is currently active {oldDmbName}.", + compileJobProvider.CompileJob.Id, + compileJobProvider.CompileJob.DmeName, + ActiveCompileJob.DmeName); + canSeamlesslySwap = false; + } if (!canSeamlesslySwap) { + Logger.LogDebug("Queueing graceful restart instead..."); await compileJobProvider.DisposeAsync(); await base.HandleNewDmbAvailable(cancellationToken); return; } - SwappableDmbProvider swappableProvider = null; + SwappableDmbProvider? swappableProvider = null; try { swappableProvider = CreateSwappableDmbProvider(compileJobProvider); - if (ActiveCompileJob.DMApiVersion == null) + if (ActiveCompileJob!.DMApiVersion == null) { Logger.LogWarning("Active compile job has no DMAPI! Commencing immediate .dmb swap. Note this behavior is known to be buggy in some DM code contexts. See https://github.com/tgstation/tgstation-server/issues/1550"); await PerformDmbSwap(swappableProvider, cancellationToken); @@ -290,6 +292,8 @@ protected sealed override async ValueTask PrepServerForLaunch(IDmb throw new InvalidOperationException("Expected pendingSwappable to be null!"); Logger.LogTrace("Prep for server launch"); + if (!CanUseSwappableDmbProvider(dmbToUse)) + return dmbToUse; ActiveSwappable = CreateSwappableDmbProvider(dmbToUse); try @@ -338,6 +342,22 @@ protected override async ValueTask HandleMonitorWakeup(MonitorAct return result; } + /// + /// If the feature of the can be used with a given . + /// + /// The that is to be activated. + /// if swapping is possible, otherwise. + bool CanUseSwappableDmbProvider(IDmbProvider dmbProvider) + { + if (dmbProvider.EngineVersion.Engine != EngineType.Byond) + { + Logger.LogDebug("Not using SwappableDmbProvider for engine type {engineType}", dmbProvider.EngineVersion.Engine); + return false; + } + + return true; + } + /// /// Create the initial link to the live game directory using . /// @@ -345,7 +365,7 @@ protected override async ValueTask HandleMonitorWakeup(MonitorAct /// A representing the running operation. async ValueTask InitialLink(CancellationToken cancellationToken) { - await ActiveSwappable.FinishActivationPreparation(cancellationToken); + await ActiveSwappable!.FinishActivationPreparation(cancellationToken); Logger.LogTrace("Linking compile job..."); await ActiveSwappable.MakeActive(cancellationToken); } @@ -363,10 +383,10 @@ async ValueTask PerformDmbSwap(SwappableDmbProvider newProvider, CancellationTok await newProvider.FinishActivationPreparation(cancellationToken); var suspended = false; - var server = Server; + var server = Server!; try { - server.Suspend(); + server.SuspendProcess(); suspended = true; } catch (Exception ex) @@ -376,13 +396,14 @@ async ValueTask PerformDmbSwap(SwappableDmbProvider newProvider, CancellationTok try { + Logger.LogTrace("Making new provider {id} active...", newProvider.CompileJob.Id); await newProvider.MakeActive(cancellationToken); } finally { // Let this throw hard if it fails if (suspended) - server.Resume(); + server.ResumeProcess(); } } diff --git a/src/Tgstation.Server.Host/Components/Watchdog/BasicWatchdog.cs b/src/Tgstation.Server.Host/Components/Watchdog/BasicWatchdog.cs index 7483760afb2..10b6a7b19ca 100644 --- a/src/Tgstation.Server.Host/Components/Watchdog/BasicWatchdog.cs +++ b/src/Tgstation.Server.Host/Components/Watchdog/BasicWatchdog.cs @@ -33,7 +33,7 @@ class BasicWatchdog : WatchdogBase /// /// The single . /// - protected ISessionController Server { get; private set; } + protected ISessionController? Server { get; private set; } /// /// If the server is set to gracefully reboot due to a pending dmb or settings change. @@ -109,16 +109,17 @@ public sealed override ValueTask InstanceRenamed(string newInstanceName, Cancell /// protected override async ValueTask HandleMonitorWakeup(MonitorActivationReason reason, CancellationToken cancellationToken) { + var controller = Server!; switch (reason) { case MonitorActivationReason.ActiveServerCrashed: - var eventType = Server.TerminationWasRequested + var eventType = controller.TerminationWasRequested ? EventType.WorldEndProcess : EventType.WatchdogCrash; await HandleEventImpl(eventType, Enumerable.Empty(), false, cancellationToken); - var exitWord = Server.TerminationWasRequested ? "exited" : "crashed"; - if (Server.RebootState == Session.RebootState.Shutdown) + var exitWord = controller.TerminationWasRequested ? "exited" : "crashed"; + if (controller.RebootState == Session.RebootState.Shutdown) { // the time for graceful shutdown is now Chat.QueueWatchdogMessage( @@ -136,7 +137,7 @@ protected override async ValueTask HandleMonitorWakeup(MonitorAct exitWord)); return MonitorAction.Restart; case MonitorActivationReason.ActiveServerRebooted: - var rebootState = Server.RebootState; + var rebootState = controller.RebootState; if (gracefulRebootRequired && rebootState == Session.RebootState.Normal) { Logger.LogError("Watchdog reached normal reboot state with gracefulRebootRequired set!"); @@ -144,7 +145,7 @@ protected override async ValueTask HandleMonitorWakeup(MonitorAct } gracefulRebootRequired = false; - Server.ResetRebootState(); + controller.ResetRebootState(); var eventTask = HandleEventImpl(EventType.WorldReboot, Enumerable.Empty(), false, cancellationToken); try @@ -170,7 +171,7 @@ protected override async ValueTask HandleMonitorWakeup(MonitorAct } case MonitorActivationReason.ActiveLaunchParametersUpdated: - await Server.SetRebootState(Session.RebootState.Restart, cancellationToken); + await controller.SetRebootState(Session.RebootState.Restart, cancellationToken); gracefulRebootRequired = true; break; case MonitorActivationReason.NewDmbAvailable: @@ -202,12 +203,12 @@ protected override async ValueTask DisposeAndNullControllersImpl() } /// - protected sealed override ISessionController GetActiveController() => Server; + protected sealed override ISessionController? GetActiveController() => Server; /// protected override async ValueTask InitController( ValueTask eventTask, - ReattachInformation reattachInfo, + ReattachInformation? reattachInfo, CancellationToken cancellationToken) { // don't need a new dmb if reattaching @@ -220,15 +221,14 @@ protected override async ValueTask InitController( // start the alpha server task, either by launch a new process or attaching to an existing one // The tasks returned are mainly for writing interop files to the directories among other things and should generally never fail // The tasks pertaining to server startup times are in the ISessionControllers - ValueTask serverLaunchTask; if (!reattachInProgress) { - Logger.LogTrace("Initializing controller with CompileJob {compileJobId}...", dmbToUse.CompileJob.Id); + Logger.LogTrace("Initializing controller with CompileJob {compileJobId}...", dmbToUse!.CompileJob.Id); await BeforeApplyDmb(dmbToUse.CompileJob, cancellationToken); dmbToUse = await PrepServerForLaunch(dmbToUse, cancellationToken); await eventTask; - serverLaunchTask = SessionControllerFactory.LaunchNew( + Server = await SessionControllerFactory.LaunchNew( dmbToUse, null, ActiveLaunchParameters, @@ -238,12 +238,9 @@ protected override async ValueTask InitController( else { await eventTask; - serverLaunchTask = SessionControllerFactory.Reattach(reattachInfo, cancellationToken); + Server = await SessionControllerFactory.Reattach(reattachInfo!, cancellationToken); } - // retrieve the session controller - Server = await serverLaunchTask; - // possiblity of null servers due to failed reattaches if (Server == null) { @@ -255,7 +252,17 @@ await ReattachFailure( if (!reattachInProgress) await SessionStartupPersist(cancellationToken); + if (!SessionId.HasValue) + Logger.LogError("Server should have a session ID allocated by now but it doesn't!"); + else + Logger.LogInformation("Watchdog starting session ID {id}", SessionId.Value); + await CheckLaunchResult(Server, "Server", cancellationToken); + + // persist again, because the DMAPI can say we need a different topic port (Original OD behavior) + // kinda hacky imo, but at least we can safely forget about this + if (!reattachInProgress) + await SessionPersistor.Update(Server.ReattachInformation, cancellationToken); } catch (Exception ex) { @@ -281,7 +288,7 @@ await ReattachFailure( /// The for the operation. /// A representing the running operation. protected virtual ValueTask SessionStartupPersist(CancellationToken cancellationToken) - => SessionPersistor.Save(Server.ReattachInformation, cancellationToken); + => SessionPersistor.Save(Server!.ReattachInformation, cancellationToken); /// /// Handler for when the is . @@ -299,7 +306,7 @@ protected virtual ValueTask HandleNormalReboot(CancellationToken protected virtual async ValueTask HandleNewDmbAvailable(CancellationToken cancellationToken) { gracefulRebootRequired = true; - if (Server.CompileJob.DMApiVersion == null) + if (Server!.CompileJob.DMApiVersion == null) { Chat.QueueWatchdogMessage( "A new deployment has been made but cannot be applied automatically as the currently running server has no DMAPI. Please manually reboot the server to apply the update."); diff --git a/src/Tgstation.Server.Host/Components/Watchdog/IWatchdog.cs b/src/Tgstation.Server.Host/Components/Watchdog/IWatchdog.cs index 39600383f3a..96c6c9ccf9a 100644 --- a/src/Tgstation.Server.Host/Components/Watchdog/IWatchdog.cs +++ b/src/Tgstation.Server.Host/Components/Watchdog/IWatchdog.cs @@ -14,6 +14,11 @@ namespace Tgstation.Server.Host.Components.Watchdog /// public interface IWatchdog : IComponentService, IAsyncDisposable, IEventConsumer, IRenameNotifyee { + /// + /// An incrementing ID for representing current server execution. + /// + long? SessionId { get; } + /// /// The current . /// @@ -27,7 +32,7 @@ public interface IWatchdog : IComponentService, IAsyncDisposable, IEventConsumer /// /// Retrieves the currently running on the server. /// - Models.CompileJob ActiveCompileJob { get; } + Models.CompileJob? ActiveCompileJob { get; } /// /// The to be applied. @@ -38,7 +43,7 @@ public interface IWatchdog : IComponentService, IAsyncDisposable, IEventConsumer /// The the active server is using. /// /// This may not be the exact same as but still be associated with the same session. - DreamDaemonLaunchParameters LastLaunchParameters { get; } + DreamDaemonLaunchParameters? LastLaunchParameters { get; } /// /// The of the active server. @@ -57,8 +62,8 @@ public interface IWatchdog : IComponentService, IAsyncDisposable, IEventConsumer /// /// The new . May be modified. /// The for the operation. - /// A representing the running operation. - ValueTask ChangeSettings(DreamDaemonLaunchParameters launchParameters, CancellationToken cancellationToken); + /// A resulting in if a reboot is required, otherwise. + ValueTask ChangeSettings(DreamDaemonLaunchParameters launchParameters, CancellationToken cancellationToken); /// /// Restarts the watchdog. diff --git a/src/Tgstation.Server.Host/Components/Watchdog/MonitorAction.cs b/src/Tgstation.Server.Host/Components/Watchdog/MonitorAction.cs index 50c33760bb1..999df90fd18 100644 --- a/src/Tgstation.Server.Host/Components/Watchdog/MonitorAction.cs +++ b/src/Tgstation.Server.Host/Components/Watchdog/MonitorAction.cs @@ -6,22 +6,22 @@ enum MonitorAction { /// - /// The monitor should continue as normal + /// The monitor should continue as normal. /// Continue, /// - /// Skips the next call to HandleMonitorWakeup action + /// Skips the next call to HandleMonitorWakeup action. /// Skip, /// - /// The monitor should kill and restart both servers + /// The monitor should kill and restart both servers. /// Restart, /// - /// The monitor should stop checking actions for this iteration and continue its loop + /// The monitor should stop checking actions for this iteration and continue its loop. /// Break, diff --git a/src/Tgstation.Server.Host/Components/Watchdog/MonitorActivationReason.cs b/src/Tgstation.Server.Host/Components/Watchdog/MonitorActivationReason.cs index 9cd09acd7ab..1d9c760d13d 100644 --- a/src/Tgstation.Server.Host/Components/Watchdog/MonitorActivationReason.cs +++ b/src/Tgstation.Server.Host/Components/Watchdog/MonitorActivationReason.cs @@ -6,22 +6,22 @@ enum MonitorActivationReason { /// - /// The active server crashed or exited + /// The active server crashed or exited. /// ActiveServerCrashed, /// - /// The active server called /world/Reboot() + /// The active server called /world/Reboot(). /// ActiveServerRebooted, /// - /// A new .dmb was deployed + /// A new .dmb was deployed. /// NewDmbAvailable, /// - /// Server launch parameters were changed + /// Server launch parameters were changed. /// ActiveLaunchParametersUpdated, diff --git a/src/Tgstation.Server.Host/Components/Watchdog/WatchdogBase.cs b/src/Tgstation.Server.Host/Components/Watchdog/WatchdogBase.cs index a77496bae26..e9223229956 100644 --- a/src/Tgstation.Server.Host/Components/Watchdog/WatchdogBase.cs +++ b/src/Tgstation.Server.Host/Components/Watchdog/WatchdogBase.cs @@ -35,6 +35,9 @@ namespace Tgstation.Server.Host.Components.Watchdog #pragma warning disable CA1506 // TODO: Decomplexify abstract class WatchdogBase : IWatchdog, ICustomCommandHandler, IRestartHandler { + /// + public long? SessionId => GetActiveController()?.ReattachInformation.Id; + /// public WatchdogStatus Status { @@ -53,19 +56,14 @@ public WatchdogStatus Status public DreamDaemonLaunchParameters ActiveLaunchParameters { get; protected set; } /// - public DreamDaemonLaunchParameters LastLaunchParameters { get; protected set; } + public DreamDaemonLaunchParameters? LastLaunchParameters { get; protected set; } /// - public Models.CompileJob ActiveCompileJob => GetActiveController()?.CompileJob; + public Models.CompileJob? ActiveCompileJob => GetActiveController()?.CompileJob; /// public abstract RebootState? RebootState { get; } - /// - /// that completes when are changed and we are running. - /// - protected TaskCompletionSource ActiveParametersUpdated { get; set; } - /// /// The for the . /// @@ -146,15 +144,20 @@ public WatchdogStatus Status /// readonly bool autoStart; + /// + /// that completes when are changed and we are running. + /// + volatile TaskCompletionSource activeParametersUpdated; + /// /// The for the monitor loop. /// - CancellationTokenSource monitorCts; + CancellationTokenSource? monitorCts; /// /// The running the monitor loop. /// - Task monitorTask; + Task? monitorTask; /// /// Backing field for . @@ -232,7 +235,7 @@ protected WatchdogBase( ActiveLaunchParameters = initialLaunchParameters; releaseServers = false; - ActiveParametersUpdated = new TaskCompletionSource(); + activeParametersUpdated = new TaskCompletionSource(); restartRegistration = serverControl.RegisterForRestart(this); try @@ -264,18 +267,20 @@ public async ValueTask DisposeAsync() } /// - public async ValueTask ChangeSettings(DreamDaemonLaunchParameters launchParameters, CancellationToken cancellationToken) + public async ValueTask ChangeSettings(DreamDaemonLaunchParameters launchParameters, CancellationToken cancellationToken) { using (await SemaphoreSlimContext.Lock(synchronizationSemaphore, cancellationToken)) { bool match = launchParameters.CanApplyWithoutReboot(ActiveLaunchParameters); ActiveLaunchParameters = launchParameters; - if (match || Status == WatchdogStatus.Offline) - return; + if (match || Status == WatchdogStatus.Offline || Status == WatchdogStatus.DelayedRestart) + return false; - ActiveParametersUpdated.TrySetResult(); // queue an update - ActiveParametersUpdated = new TaskCompletionSource(); + var oldTcs = Interlocked.Exchange(ref activeParametersUpdated, new TaskCompletionSource()); + oldTcs.SetResult(); } + + return true; } /// @@ -388,7 +393,7 @@ await jobManager.RegisterOperation( job, async (core, databaseContextFactory, paramJob, progressFunction, ct) => { - if (core.Watchdog != this) + if (core?.Watchdog != this) throw new InvalidOperationException(Instance.DifferentCoreExceptionMessage); using (await SemaphoreSlimContext.Lock(synchronizationSemaphore, ct)) @@ -411,7 +416,7 @@ public async ValueTask Terminate(bool graceful, CancellationToken cancellationTo } /// - public async ValueTask HandleRestart(Version updateVersion, bool handlerMayDelayShutdownWithExtremelyLongRunningTasks, CancellationToken cancellationToken) + public async ValueTask HandleRestart(Version? updateVersion, bool handlerMayDelayShutdownWithExtremelyLongRunningTasks, CancellationToken cancellationToken) { if (handlerMayDelayShutdownWithExtremelyLongRunningTasks) { @@ -420,7 +425,7 @@ public async ValueTask HandleRestart(Version updateVersion, bool handlerMayDelay if (Status != WatchdogStatus.Offline) { Logger.LogDebug("Waiting for server to gracefully shut down."); - await monitorTask.WaitAsync(cancellationToken); + await monitorTask!.WaitAsync(cancellationToken); } else Logger.LogTrace("Graceful shutdown requested but server is already offline."); @@ -441,20 +446,8 @@ public async ValueTask HandleRestart(Version updateVersion, bool handlerMayDelay /// public async ValueTask CreateDump(CancellationToken cancellationToken) { - const string DumpDirectory = "ProcessDumps"; - await diagnosticsIOManager.CreateDirectory(DumpDirectory, cancellationToken); - - var dumpFileName = diagnosticsIOManager.ResolvePath( - diagnosticsIOManager.ConcatPath( - DumpDirectory, - $"DreamDaemon-{DateTimeOffset.UtcNow.ToFileStamp()}.dmp")); - - var session = GetActiveController(); - if (session?.Lifetime.IsCompleted != false) - throw new JobException(ErrorCode.DreamDaemonOffline); - - Logger.LogInformation("Dumping session to {dumpFileName}...", dumpFileName); - await session.CreateDump(dumpFileName, cancellationToken); + using (await SemaphoreSlimContext.Lock(synchronizationSemaphore, cancellationToken)) + await CreateDumpNoLock(cancellationToken); } /// @@ -495,7 +488,7 @@ public async ValueTask Broadcast(string message, CancellationToken cancell } /// - async ValueTask IEventConsumer.HandleEvent(EventType eventType, IEnumerable parameters, bool deploymentPipeline, CancellationToken cancellationToken) + async ValueTask IEventConsumer.HandleEvent(EventType eventType, IEnumerable parameters, bool deploymentPipeline, CancellationToken cancellationToken) { ArgumentNullException.ThrowIfNull(parameters); @@ -521,7 +514,7 @@ async ValueTask IEventConsumer.HandleEvent(EventType eventType, IEnumerable to use, if any. /// The for the operation. /// A representing the running operation. - protected abstract ValueTask InitController(ValueTask eventTask, ReattachInformation reattachInfo, CancellationToken cancellationToken); + protected abstract ValueTask InitController(ValueTask eventTask, ReattachInformation? reattachInfo, CancellationToken cancellationToken); /// /// Launches the watchdog. @@ -536,7 +529,7 @@ protected async ValueTask LaunchNoLock( bool startMonitor, bool announce, bool announceFailure, - ReattachInformation reattachInfo, + ReattachInformation? reattachInfo, CancellationToken cancellationToken) { Logger.LogTrace("Begin LaunchImplNoLock"); @@ -617,7 +610,7 @@ protected async ValueTask StopMonitor() if (monitorTask == null) return false; var wasRunning = !monitorTask.IsCompleted; - monitorCts.Cancel(); + monitorCts!.Cancel(); await monitorTask; Logger.LogTrace("Stopped Monitor"); monitorCts.Dispose(); @@ -645,7 +638,7 @@ protected async ValueTask CheckLaunchResult(ISessionController controller, strin if (!launchResult.StartupTime.HasValue) throw new JobException( ErrorCode.WatchdogStartupTimeout, - new JobException($"{serverName} timed out on startup: {ActiveLaunchParameters.StartupTimeout.Value}s")); + new JobException($"{serverName} timed out on startup: {ActiveLaunchParameters.StartupTimeout!.Value}s")); } /// @@ -690,8 +683,8 @@ protected async ValueTask DisposeAndNullControllers(CancellationToken cancellati /// /// Get the active . /// - /// The active . - protected abstract ISessionController GetActiveController(); + /// The active , if any. + protected abstract ISessionController? GetActiveController(); /// /// Handles the actions to take when the monitor has to "wake up". @@ -723,9 +716,9 @@ protected async ValueTask BeforeApplyDmb(Models.CompileJob newCompileJob, Cancel var eventTask = eventConsumer.HandleEvent( EventType.DeploymentActivation, - new List + new List { - GameIOManager.ResolvePath(newCompileJob.DirectoryName.ToString()), + GameIOManager.ResolvePath(newCompileJob.DirectoryName!.Value.ToString()), }, false, cancellationToken); @@ -755,8 +748,9 @@ protected async ValueTask HandleEventImpl(EventType eventType, IEnumerable newTaskFactory) + var sameController = lastController == controller; + void TryUpdateTask(ref Task? oldTask, Func newTaskFactory) { - if (oldTask?.IsCompleted == true) + if (sameController && oldTask?.IsCompleted == true) return; oldTask = newTaskFactory(); } - controller.RebootGate = nextMonitorWakeupTcs.Task; - if (lastController == controller) - { - TryUpdateTask(ref activeServerLifetime, () => controller.Lifetime); - TryUpdateTask(ref activeServerReboot, () => controller.OnReboot); - TryUpdateTask(ref serverPrimed, () => controller.OnPrime); - TryUpdateTask(ref activeServerStartup, () => controller.OnStartup); - } - else - { - activeServerLifetime = controller.Lifetime; - activeServerReboot = controller.OnReboot; - serverPrimed = controller.OnPrime; - activeServerStartup = controller.OnStartup; + controller!.RebootGate = nextMonitorWakeupTcs.Task; + + TryUpdateTask(ref activeServerLifetime, () => controller.Lifetime); + TryUpdateTask(ref activeServerReboot, () => controller.OnReboot); + TryUpdateTask(ref serverPrimed, () => controller.OnPrime); + TryUpdateTask(ref activeServerStartup, () => controller.OnStartup); + + if (!sameController) lastController = controller; - } - TryUpdateTask(ref activeLaunchParametersChanged, () => ActiveParametersUpdated.Task); + TryUpdateTask(ref activeLaunchParametersChanged, () => activeParametersUpdated.Task); TryUpdateTask( ref newDmbAvailable, () => @@ -908,28 +897,36 @@ static void TryUpdateTask(ref Task oldTask, Func newTaskFactory) }); } - UpdateMonitoredTasks(); - - var healthCheckSeconds = ActiveLaunchParameters.HealthCheckSeconds.Value; - var healthCheck = healthCheckSeconds == 0 - || !controller.DMApiAvailable - ? Extensions.TaskExtensions.InfiniteTask - : Task.Delay( - TimeSpan.FromSeconds(healthCheckSeconds), - cancellationToken); - - // cancel waiting if requested - var toWaitOn = Task.WhenAny( - activeServerLifetime, - activeServerReboot, - activeServerStartup, - healthCheck, - newDmbAvailable, - activeLaunchParametersChanged, - serverPrimed); - - // wait for something to happen - await toWaitOn.WaitAsync(cancellationToken); + if (controller != null) + { + UpdateMonitoredTasks(); + + var healthCheckSeconds = ActiveLaunchParameters.HealthCheckSeconds!.Value; + healthCheck = healthCheckSeconds == 0 + || !controller.DMApiAvailable + ? Extensions.TaskExtensions.InfiniteTask + : Task.Delay( + TimeSpan.FromSeconds(healthCheckSeconds), + cancellationToken); + + // cancel waiting if requested + var toWaitOn = Task.WhenAny( + activeServerLifetime!, + activeServerReboot!, + activeServerStartup!, + healthCheck, + newDmbAvailable!, + activeLaunchParametersChanged!, + serverPrimed!); + + // wait for something to happen + await toWaitOn.WaitAsync(cancellationToken); + } + else + { + Logger.LogError("Controller was null on monitor wakeup! Attempting restart..."); + nextAction = MonitorAction.Restart; // excuse me wtf? + } cancellationToken.ThrowIfCancellationRequested(); Logger.LogTrace("Monitor activated"); @@ -938,7 +935,7 @@ static void TryUpdateTask(ref Task oldTask, Func newTaskFactory) using (await SemaphoreSlimContext.Lock(synchronizationSemaphore, cancellationToken)) { // Set this sooner so chat sends don't hold us up - if (activeServerLifetime.IsCompleted) + if (activeServerLifetime!.IsCompleted) Status = WatchdogStatus.Restoring; // multiple things may have happened, handle them one at a time @@ -946,7 +943,7 @@ static void TryUpdateTask(ref Task oldTask, Func newTaskFactory) { MonitorActivationReason activationReason = default; // this will always be assigned before being used - bool CheckActivationReason(ref Task task, MonitorActivationReason testActivationReason) + bool CheckActivationReason(ref Task? task, MonitorActivationReason testActivationReason) { var taskCompleted = task?.IsCompleted == true; task = null; @@ -1037,7 +1034,10 @@ bool CheckActivationReason(ref Task task, MonitorActivationReason testActivation { Logger.LogTrace("Detaching server..."); var controller = GetActiveController(); - await controller.Release(); + if (controller != null) + await controller.Release(); + else + Logger.LogError("Controller was null on monitor shutdown!"); } } @@ -1101,6 +1101,9 @@ async ValueTask HandleHealthCheck(CancellationToken cancellationT { Logger.LogTrace("Sending health check to active server..."); var activeServer = GetActiveController(); + if (activeServer == null) + return MonitorAction.Restart; // uhhhh??? + var response = await activeServer.SendCommand(new TopicParameters(), cancellationToken); var shouldShutdown = activeServer.RebootState == Session.RebootState.Shutdown; @@ -1140,12 +1143,12 @@ async ValueTask HandleHealthCheck(CancellationToken cancellationT actionTaken, StringComparison.Ordinal)); - if (ActiveLaunchParameters.DumpOnHealthCheckRestart.Value) + if (ActiveLaunchParameters.DumpOnHealthCheckRestart!.Value) { Logger.LogDebug("DumpOnHealthCheckRestart enabled."); try { - await CreateDump(cancellationToken); + await CreateDumpNoLock(cancellationToken); } catch (JobException ex) { @@ -1176,13 +1179,30 @@ async ValueTask HandleHealthCheck(CancellationToken cancellationT /// Handle any in a given topic . /// /// The . - void HandleChatResponses(TopicResponse result) + void HandleChatResponses(TopicResponse? result) { if (result?.ChatResponses != null) - foreach (var response in result.ChatResponses) + { + var warnedMissingChannelIds = false; + foreach (var response in result.ChatResponses + .Where(response => + { + if (response.ChannelIds == null) + { + if (!warnedMissingChannelIds) + { + Logger.LogWarning("DMAPI response contains null channelIds!"); + warnedMissingChannelIds = true; + } + + return false; + } + + return true; + })) Chat.QueueMessage( response, - response.ChannelIds + response.ChannelIds! .Select(channelIdString => { if (UInt64.TryParse(channelIdString, out var channelId)) @@ -1193,7 +1213,37 @@ void HandleChatResponses(TopicResponse result) return null; }) .Where(nullableChannelId => nullableChannelId.HasValue) - .Select(nullableChannelId => nullableChannelId.Value)); + .Select(nullableChannelId => nullableChannelId!.Value)); + } + } + + /// + /// Attempt to create a process dump for the game server. Requires a lock on . + /// + /// The for the operation. + /// A representing the running operation. + async ValueTask CreateDumpNoLock(CancellationToken cancellationToken) + { + const string DumpDirectory = "ProcessDumps"; + var dumpFileNameTemplate = diagnosticsIOManager.ResolvePath( + diagnosticsIOManager.ConcatPath( + DumpDirectory, + $"DreamDaemon-{DateTimeOffset.UtcNow.ToFileStamp()}.dmp")); + + var dumpFileName = dumpFileNameTemplate; + var iteration = 0; + while (await diagnosticsIOManager.FileExists(dumpFileName, cancellationToken)) + dumpFileName = $"{dumpFileNameTemplate} ({++iteration})"; + + if (iteration == 0) + await diagnosticsIOManager.CreateDirectory(DumpDirectory, cancellationToken); + + var session = GetActiveController(); + if (session?.Lifetime.IsCompleted != false) + throw new JobException(ErrorCode.GameServerOffline); + + Logger.LogInformation("Dumping session to {dumpFileName}...", dumpFileName); + await session.CreateDump(dumpFileName, cancellationToken); } } } diff --git a/src/Tgstation.Server.Host/Components/Watchdog/WindowsWatchdog.cs b/src/Tgstation.Server.Host/Components/Watchdog/WindowsWatchdog.cs index 8180630ea49..e75ba745119 100644 --- a/src/Tgstation.Server.Host/Components/Watchdog/WindowsWatchdog.cs +++ b/src/Tgstation.Server.Host/Components/Watchdog/WindowsWatchdog.cs @@ -3,6 +3,7 @@ using Microsoft.Extensions.Logging; +using Tgstation.Server.Api.Models; using Tgstation.Server.Api.Models.Internal; using Tgstation.Server.Host.Components.Chat; using Tgstation.Server.Host.Components.Deployment; @@ -80,6 +81,12 @@ public WindowsWatchdog( /// protected override async ValueTask ApplyInitialDmb(CancellationToken cancellationToken) { + if (Server!.EngineVersion.Engine != EngineType.Byond) + { + Logger.LogTrace("Not setting InitialDmb for engine type {engineType}", Server.EngineVersion.Engine); + return; + } + Server.ReattachInformation.InitialDmb = await DmbFactory.FromCompileJob(Server.CompileJob, cancellationToken); } diff --git a/src/Tgstation.Server.Host/Configuration/ControlPanelConfiguration.cs b/src/Tgstation.Server.Host/Configuration/ControlPanelConfiguration.cs index 19dfa9cace0..12da65e3122 100644 --- a/src/Tgstation.Server.Host/Configuration/ControlPanelConfiguration.cs +++ b/src/Tgstation.Server.Host/Configuration/ControlPanelConfiguration.cs @@ -36,16 +36,16 @@ public bool Enable /// /// The channel to retrieve the webpanel from. "local" uses the bundled version. /// - public string Channel { get; set; } + public string? Channel { get; set; } /// /// The public path to the TGS control panel from a wider network. /// - public string PublicPath { get; set; } + public string? PublicPath { get; set; } /// /// Origins allowed for CORS requests. /// - public ICollection AllowedOrigins { get; set; } + public ICollection? AllowedOrigins { get; set; } } } diff --git a/src/Tgstation.Server.Host/Configuration/DatabaseConfiguration.cs b/src/Tgstation.Server.Host/Configuration/DatabaseConfiguration.cs index d5adce7577f..ea699cd4ada 100644 --- a/src/Tgstation.Server.Host/Configuration/DatabaseConfiguration.cs +++ b/src/Tgstation.Server.Host/Configuration/DatabaseConfiguration.cs @@ -27,7 +27,7 @@ public sealed class DatabaseConfiguration /// /// The connection string for the database. /// - public string ConnectionString { get; set; } + public string? ConnectionString { get; set; } /// /// If the database should be deleted on application startup. Should not be used in production!. @@ -37,6 +37,6 @@ public sealed class DatabaseConfiguration /// /// The form of the of the target server. /// - public string ServerVersion { get; set; } + public string? ServerVersion { get; set; } } } diff --git a/src/Tgstation.Server.Host/Configuration/DatabaseType.cs b/src/Tgstation.Server.Host/Configuration/DatabaseType.cs index 9b3f1c89ca5..ab2e6fcae61 100644 --- a/src/Tgstation.Server.Host/Configuration/DatabaseType.cs +++ b/src/Tgstation.Server.Host/Configuration/DatabaseType.cs @@ -6,27 +6,27 @@ public enum DatabaseType { /// - /// Use Microsoft SQL Server + /// Use Microsoft SQL Server. /// SqlServer, /// - /// Use MySQL + /// Use MySQL. /// MySql, /// - /// Use MariaDB + /// Use MariaDB. /// MariaDB, /// - /// Use Sqlite + /// Use Sqlite. /// Sqlite, /// - /// Use PostgresSql + /// Use PostgresSql. /// PostgresSql, } diff --git a/src/Tgstation.Server.Host/Configuration/ElasticsearchConfiguration.cs b/src/Tgstation.Server.Host/Configuration/ElasticsearchConfiguration.cs index 8c45f2e9f5f..a99f3b03e8b 100644 --- a/src/Tgstation.Server.Host/Configuration/ElasticsearchConfiguration.cs +++ b/src/Tgstation.Server.Host/Configuration/ElasticsearchConfiguration.cs @@ -1,7 +1,7 @@ using System; namespace Tgstation.Server.Host.Configuration - { +{ /// /// Configuration options pertaining to elasticsearch log storage. /// @@ -20,16 +20,16 @@ public sealed class ElasticsearchConfiguration /// /// The host of the elasticsearch endpoint. /// - public Uri Host { get; set; } + public Uri? Host { get; set; } /// /// Username for elasticsearch. /// - public string Username { get; set; } + public string? Username { get; set; } /// /// Password for elasticsearch. /// - public string Password { get; set; } + public string? Password { get; set; } } } diff --git a/src/Tgstation.Server.Host/Configuration/FileLoggingConfiguration.cs b/src/Tgstation.Server.Host/Configuration/FileLoggingConfiguration.cs index ba1bb3c8752..534daf8ebb9 100644 --- a/src/Tgstation.Server.Host/Configuration/FileLoggingConfiguration.cs +++ b/src/Tgstation.Server.Host/Configuration/FileLoggingConfiguration.cs @@ -19,20 +19,10 @@ public sealed class FileLoggingConfiguration /// public const string Section = "FileLogging"; - /// - /// Default value for . - /// - const LogLevel DefaultLogLevel = LogLevel.Debug; - - /// - /// Default value for . - /// - const LogLevel DefaultMicrosoftLogLevel = LogLevel.Warning; - /// /// Where log files are stored. /// - public string Directory { get; set; } + public string? Directory { get; set; } /// /// If file logging is disabled. @@ -43,13 +33,13 @@ public sealed class FileLoggingConfiguration /// The minimum to display in logs. /// [JsonConverter(typeof(StringEnumConverter))] - public LogLevel LogLevel { get; set; } = DefaultLogLevel; + public LogLevel LogLevel { get; set; } = LogLevel.Debug; // Not a `const` b/c of https://github.com/coverlet-coverage/coverlet/issues/1507 /// /// The minimum to display in logs for Microsoft library sources. /// [JsonConverter(typeof(StringEnumConverter))] - public LogLevel MicrosoftLogLevel { get; set; } = DefaultMicrosoftLogLevel; + public LogLevel MicrosoftLogLevel { get; set; } = LogLevel.Warning; // Not a `const` b/c of https://github.com/coverlet-coverage/coverlet/issues/1507 /// /// Gets the evaluated log . @@ -72,7 +62,9 @@ public string GetFullLogDirectory( return platformIdentifier.IsWindows ? ioManager.ConcatPath( - Environment.GetFolderPath(Environment.SpecialFolder.CommonApplicationData), + Environment.GetFolderPath( + Environment.SpecialFolder.CommonApplicationData, + Environment.SpecialFolderOption.DoNotVerify), assemblyInformationProvider.VersionPrefix, "logs") : ioManager.ConcatPath( diff --git a/src/Tgstation.Server.Host/Configuration/GeneralConfiguration.cs b/src/Tgstation.Server.Host/Configuration/GeneralConfiguration.cs index 7b02f6d177f..ae084860df6 100644 --- a/src/Tgstation.Server.Host/Configuration/GeneralConfiguration.cs +++ b/src/Tgstation.Server.Host/Configuration/GeneralConfiguration.cs @@ -9,6 +9,8 @@ using Tgstation.Server.Host.Properties; using Tgstation.Server.Host.Setup; +using YamlDotNet.Serialization; + namespace Tgstation.Server.Host.Configuration { /// @@ -61,6 +63,16 @@ public sealed class GeneralConfiguration : ServerInformationBase /// const uint DefaultShutdownTimeoutMinutes = 300; + /// + /// The default value for . + /// + const string DefaultOpenDreamGitUrl = "https://github.com/OpenDreamProject/OpenDream"; + + /// + /// The default value for . + /// + const string DefaultOpenDreamGitTagPrefix = "v"; + /// /// The current . /// @@ -69,7 +81,7 @@ public sealed class GeneralConfiguration : ServerInformationBase /// /// The the file says it is. /// - public Version ConfigVersion { get; set; } + public Version? ConfigVersion { get; set; } /// /// The port the TGS API listens on. @@ -79,7 +91,7 @@ public sealed class GeneralConfiguration : ServerInformationBase /// /// A GitHub personal access token to use for bypassing rate limits on requests. Requires no scopes. /// - public string GitHubAccessToken { get; set; } + public string? GitHubAccessToken { get; set; } /// /// The . @@ -108,7 +120,7 @@ public sealed class GeneralConfiguration : ServerInformationBase public bool UseBasicWatchdog { get; set; } /// - /// If the swagger UI should be made avaiable. + /// If the swagger documentation and UI should be made avaiable. /// public bool HostApiDocumentation { get; set; } @@ -122,6 +134,22 @@ public sealed class GeneralConfiguration : ServerInformationBase /// public uint? DeploymentDirectoryCopyTasksPerCore { get; set; } + /// + /// Location of a publically accessible OpenDream repository. + /// + [YamlMember(SerializeAs = typeof(string))] + public Uri OpenDreamGitUrl { get; set; } = new Uri(DefaultOpenDreamGitUrl); + + /// + /// The prefix to the OpenDream semver as tags appear in the git repository. + /// + public string OpenDreamGitTagPrefix { get; set; } = DefaultOpenDreamGitTagPrefix; + + /// + /// If the dotnet output of creating an OpenDream installation should be suppressed. Known to cause issues in CI. + /// + public bool OpenDreamSuppressInstallOutput { get; set; } + /// /// Initializes a new instance of the class. /// diff --git a/src/Tgstation.Server.Host/Configuration/InternalConfiguration.cs b/src/Tgstation.Server.Host/Configuration/InternalConfiguration.cs index 6bf30bafdc7..58f2ad4720b 100644 --- a/src/Tgstation.Server.Host/Configuration/InternalConfiguration.cs +++ b/src/Tgstation.Server.Host/Configuration/InternalConfiguration.cs @@ -13,12 +13,12 @@ public sealed class InternalConfiguration /// /// The name of the pipe opened by the host watchdog for sending commands, if any. /// - public string CommandPipe { get; set; } + public string? CommandPipe { get; set; } /// /// The name of the pipe opened by the host watchdog for receiving commands, if any. /// - public string ReadyPipe { get; set; } + public string? ReadyPipe { get; set; } /// /// If the server is running under SystemD. @@ -28,7 +28,7 @@ public sealed class InternalConfiguration /// /// The base path for the app settings configuration files. /// - public string AppSettingsBasePath { get; set; } + public string AppSettingsBasePath { get; set; } = "UNINITIALIZED"; // this is set in a hacky way in ServerFactory /// /// Coerce the to select . @@ -38,6 +38,6 @@ public sealed class InternalConfiguration /// /// Generate default configuration using the given default password. /// - public string MariaDBDefaultRootPassword { get; set; } + public string? MariaDBDefaultRootPassword { get; set; } } } diff --git a/src/Tgstation.Server.Host/Configuration/OAuthConfiguration.cs b/src/Tgstation.Server.Host/Configuration/OAuthConfiguration.cs index 1818a15d8b2..2d4c7811028 100644 --- a/src/Tgstation.Server.Host/Configuration/OAuthConfiguration.cs +++ b/src/Tgstation.Server.Host/Configuration/OAuthConfiguration.cs @@ -10,16 +10,16 @@ public sealed class OAuthConfiguration : OAuthConfigurationBase /// /// The client redirect URL. Not used by all providers. /// - public Uri ServerUrl { get; set; } + public Uri? ServerUrl { get; set; } /// /// The authentication server URL. Not used by all providers. /// - public Uri RedirectUrl { get; set; } + public Uri? RedirectUrl { get; set; } /// /// User information URL override. Not supported by the provider. /// - public Uri UserInformationUrlOverride { get; set; } + public Uri? UserInformationUrlOverride { get; set; } } } diff --git a/src/Tgstation.Server.Host/Configuration/OAuthConfigurationBase.cs b/src/Tgstation.Server.Host/Configuration/OAuthConfigurationBase.cs index d2d76680728..a170cd235f3 100644 --- a/src/Tgstation.Server.Host/Configuration/OAuthConfigurationBase.cs +++ b/src/Tgstation.Server.Host/Configuration/OAuthConfigurationBase.cs @@ -10,12 +10,12 @@ public abstract class OAuthConfigurationBase /// /// The client ID. /// - public string ClientId { get; set; } + public string? ClientId { get; set; } /// /// The client secret. /// - public string ClientSecret { get; set; } + public string? ClientSecret { get; set; } /// /// Initializes a new instance of the class. diff --git a/src/Tgstation.Server.Host/Configuration/SecurityConfiguration.cs b/src/Tgstation.Server.Host/Configuration/SecurityConfiguration.cs index b78cf55b9ab..35585612c05 100644 --- a/src/Tgstation.Server.Host/Configuration/SecurityConfiguration.cs +++ b/src/Tgstation.Server.Host/Configuration/SecurityConfiguration.cs @@ -1,4 +1,7 @@ using System.Collections.Generic; +using System.Linq; + +using Swashbuckle.AspNetCore.SwaggerGen; using Tgstation.Server.Api.Models; @@ -57,11 +60,32 @@ public sealed class SecurityConfiguration /// /// A custom token signing key. Overrides . /// - public string CustomTokenSigningKeyBase64 { get; set; } + public string? CustomTokenSigningKeyBase64 { get; set; } /// /// OAuth provider settings. /// - public IDictionary OAuth { get; set; } + public IDictionary? OAuth + { + get => oAuth; + set + { + // Workaround for https://github.com/dotnet/runtime/issues/89547 + var publicProperties = typeof(OAuthConfiguration) + .GetProperties() + .Where(property => property.CanWrite && property.SetMethod!.IsPublic) + .ToList(); + oAuth = value + ?.Where( + kvp => !publicProperties.All( + prop => prop.GetValue(kvp.Value) == prop.PropertyType.GetDefaultValue())) + .ToDictionary(kvp => kvp.Key, kvp => kvp.Value); + } + } + + /// + /// Backing field for . + /// + IDictionary? oAuth; } } diff --git a/src/Tgstation.Server.Host/Configuration/SwarmConfiguration.cs b/src/Tgstation.Server.Host/Configuration/SwarmConfiguration.cs index 38c709ec399..12c9cf25a06 100644 --- a/src/Tgstation.Server.Host/Configuration/SwarmConfiguration.cs +++ b/src/Tgstation.Server.Host/Configuration/SwarmConfiguration.cs @@ -1,6 +1,7 @@ using System; using Tgstation.Server.Api.Models.Internal; + using YamlDotNet.Serialization; namespace Tgstation.Server.Host.Configuration @@ -17,7 +18,7 @@ public sealed class SwarmConfiguration : SwarmServer /// [YamlMember(SerializeAs = typeof(string))] - public override Uri Address + public override Uri? Address { get => base.Address; set => base.Address = value; @@ -25,7 +26,7 @@ public override Uri Address /// [YamlMember(SerializeAs = typeof(string))] - public override Uri PublicAddress + public override Uri? PublicAddress { get => base.PublicAddress; set => base.PublicAddress = value; @@ -35,12 +36,12 @@ public override Uri PublicAddress /// The of the swarm controller. If , the current server is considered the controller. /// [YamlMember(SerializeAs = typeof(string))] - public Uri ControllerAddress { get; set; } + public Uri? ControllerAddress { get; set; } /// /// The private key used for swarm communication. /// - public string PrivateKey { get; set; } + public string? PrivateKey { get; set; } /// /// The number of nodes in addition to the controller required to be connected a server swarm before performing an update. diff --git a/src/Tgstation.Server.Host/Configuration/UpdatesConfiguration.cs b/src/Tgstation.Server.Host/Configuration/UpdatesConfiguration.cs index 8f1a6d503e3..af475412eeb 100644 --- a/src/Tgstation.Server.Host/Configuration/UpdatesConfiguration.cs +++ b/src/Tgstation.Server.Host/Configuration/UpdatesConfiguration.cs @@ -28,18 +28,18 @@ public sealed class UpdatesConfiguration const string DefaultUpdatePackageAssetName = "ServerUpdatePackage.zip"; /// - /// The of the tgstation-server fork to recieve updates from. + /// The of the tgstation-server fork to receive updates from. /// public long GitHubRepositoryId { get; set; } = DefaultGitHubRepositoryId; /// /// Prefix before the of TGS published in git tags. /// - public string GitTagPrefix { get; set; } = DefaultGitTagPrefix; + public string? GitTagPrefix { get; set; } = DefaultGitTagPrefix; /// /// Asset package containing the new assembly in zip form. /// - public string UpdatePackageAssetName { get; set; } = DefaultUpdatePackageAssetName; + public string? UpdatePackageAssetName { get; set; } = DefaultUpdatePackageAssetName; } } diff --git a/src/Tgstation.Server.Host/Controllers/AdministrationController.cs b/src/Tgstation.Server.Host/Controllers/AdministrationController.cs index c1da69a69d6..2ccc4f4c8bc 100644 --- a/src/Tgstation.Server.Host/Controllers/AdministrationController.cs +++ b/src/Tgstation.Server.Host/Controllers/AdministrationController.cs @@ -144,8 +144,8 @@ public async ValueTask Read(CancellationToken cancellationToken) { try { - Version greatestVersion = null; - Uri repoUrl = null; + Version? greatestVersion = null; + Uri? repoUrl = null; try { var gitHubService = gitHubServiceFactory.CreateService(); @@ -213,10 +213,10 @@ public async ValueTask Update([FromBody] ServerUpdateRequest mode var attemptingUpload = model.UploadZip == true; if (attemptingUpload) { - if (!AuthenticationContext.PermissionSet.AdministrationRights.Value.HasFlag(AdministrationRights.UploadVersion)) + if (!AuthenticationContext.PermissionSet.AdministrationRights!.Value.HasFlag(AdministrationRights.UploadVersion)) return Forbid(); } - else if (!AuthenticationContext.PermissionSet.AdministrationRights.Value.HasFlag(AdministrationRights.ChangeVersion)) + else if (!AuthenticationContext.PermissionSet.AdministrationRights!.Value.HasFlag(AdministrationRights.ChangeVersion)) return Forbid(); if (model.NewVersion == null) @@ -377,7 +377,7 @@ public async ValueTask GetLog(string path, CancellationToken canc /// A resulting in the of the request. async ValueTask AttemptInitiateUpdate(Version newVersion, bool attemptingUpload, CancellationToken cancellationToken) { - IFileUploadTicket uploadTicket = attemptingUpload + IFileUploadTicket? uploadTicket = attemptingUpload ? fileTransferService.CreateUpload(FileUploadStreamKind.None) : null; @@ -391,7 +391,7 @@ async ValueTask AttemptInitiateUpdate(Version newVersion, bool at catch { if (attemptingUpload) - await uploadTicket.DisposeAsync(); + await uploadTicket!.DisposeAsync(); throw; } diff --git a/src/Tgstation.Server.Host/Controllers/ApiController.cs b/src/Tgstation.Server.Host/Controllers/ApiController.cs index ffa4e849600..bad2e6c10f1 100644 --- a/src/Tgstation.Server.Host/Controllers/ApiController.cs +++ b/src/Tgstation.Server.Host/Controllers/ApiController.cs @@ -48,7 +48,7 @@ public abstract class ApiController : ApiControllerBase /// /// The for the operation. /// - protected ApiHeaders ApiHeaders => ApiHeadersProvider.ApiHeaders; + protected ApiHeaders? ApiHeaders => ApiHeadersProvider.ApiHeaders; /// /// The containing value of . @@ -73,7 +73,7 @@ public abstract class ApiController : ApiControllerBase /// /// The for the operation. /// - protected Models.Instance Instance { get; } + protected Models.Instance? Instance { get; } /// /// If are required. @@ -100,13 +100,13 @@ protected ApiController( ApiHeadersProvider = apiHeadersProvider ?? throw new ArgumentNullException(nameof(apiHeadersProvider)); Logger = logger ?? throw new ArgumentNullException(nameof(logger)); - Instance = AuthenticationContext?.InstancePermissionSet?.Instance; + Instance = AuthenticationContext.InstancePermissionSet?.Instance; this.requireHeaders = requireHeaders; } /// #pragma warning disable CA1506 // TODO: Decomplexify - protected override async ValueTask HookExecuteAction(Func executeAction, CancellationToken cancellationToken) + protected override async ValueTask HookExecuteAction(Func executeAction, CancellationToken cancellationToken) { ArgumentNullException.ThrowIfNull(executeAction); @@ -114,7 +114,7 @@ protected override async ValueTask HookExecuteAction(Func e if (ApiHeaders == null) { if (requireHeaders) - return HeadersIssue(ApiHeadersProvider.HeadersException); + return HeadersIssue(ApiHeadersProvider.HeadersException!); } var errorCase = await ValidateRequest(cancellationToken); @@ -124,7 +124,7 @@ protected override async ValueTask HookExecuteAction(Func e if (ModelState?.IsValid == false) { var errorMessages = ModelState - .SelectMany(x => x.Value.Errors) + .SelectMany(x => x.Value!.Errors) .Select(x => x.ErrorMessage) // We use RequiredAttributes purely for preventing properties from becoming nullable in the databases @@ -238,8 +238,8 @@ protected ObjectResult RateLimit(RateLimitExceededException rateLimitException) /// /// The for the operation. /// A resulting in an appropriate on validation failure, otherwise. - protected virtual ValueTask ValidateRequest(CancellationToken cancellationToken) - => ValueTask.FromResult(null); + protected virtual ValueTask ValidateRequest(CancellationToken cancellationToken) + => ValueTask.FromResult(null); /// /// Response for missing/Invalid headers. @@ -267,14 +267,14 @@ protected IActionResult HeadersIssue(HeadersException headersException) /// /// The of model being generated and returned. /// A resulting in a resulting in the generated . - /// A to transform the s after being queried. + /// Optional to transform the s after being queried. /// The requested page from the query. /// The requested page size from the query. /// The for the operation. /// A resulting in the for the operation. protected ValueTask Paginated( Func>> queryGenerator, - Func resultTransformer, + Func? resultTransformer, int? pageQuery, int? pageSizeQuery, CancellationToken cancellationToken) => PaginatedImpl( @@ -297,7 +297,7 @@ protected ValueTask Paginated( /// A resulting in the for the operation. protected ValueTask Paginated( Func>> queryGenerator, - Func resultTransformer, + Func? resultTransformer, int? pageQuery, int? pageSizeQuery, CancellationToken cancellationToken) @@ -322,7 +322,7 @@ protected ValueTask Paginated( /// A resulting in the for the operation. async ValueTask PaginatedImpl( Func>> queryGenerator, - Func resultTransformer, + Func? resultTransformer, int? pageQuery, int? pageSizeQuery, CancellationToken cancellationToken) @@ -342,7 +342,7 @@ async ValueTask PaginatedImpl( var page = pageQuery ?? 1; var paginationResult = await queryGenerator(); - if (paginationResult.EarlyOut != null) + if (!paginationResult.Valid) return paginationResult.EarlyOut; var queriedResults = paginationResult diff --git a/src/Tgstation.Server.Host/Controllers/ApiControllerBase.cs b/src/Tgstation.Server.Host/Controllers/ApiControllerBase.cs index 9a8b1198082..748dd810966 100644 --- a/src/Tgstation.Server.Host/Controllers/ApiControllerBase.cs +++ b/src/Tgstation.Server.Host/Controllers/ApiControllerBase.cs @@ -37,7 +37,7 @@ public sealed override async Task OnActionExecutionAsync(ActionExecutingContext /// A that should be invoked and its response awaited to continue normal execution of the request. Should NOT be called if this method returns a non- value. /// The for the operation. /// A resulting in an that, if not , is executed. - protected virtual async ValueTask HookExecuteAction(Func executeAction, CancellationToken cancellationToken) + protected virtual async ValueTask HookExecuteAction(Func executeAction, CancellationToken cancellationToken) { ArgumentNullException.ThrowIfNull(executeAction); diff --git a/src/Tgstation.Server.Host/Controllers/HomeController.cs b/src/Tgstation.Server.Host/Controllers/ApiRootController.cs similarity index 83% rename from src/Tgstation.Server.Host/Controllers/HomeController.cs rename to src/Tgstation.Server.Host/Controllers/ApiRootController.cs index 659a379591d..fa6b3e0c4b6 100644 --- a/src/Tgstation.Server.Host/Controllers/HomeController.cs +++ b/src/Tgstation.Server.Host/Controllers/ApiRootController.cs @@ -32,66 +32,61 @@ namespace Tgstation.Server.Host.Controllers /// /// Root for the . /// - [Route(Routes.Root)] - public sealed class HomeController : ApiController + [Route(Routes.ApiRoot)] + public sealed class ApiRootController : ApiController { /// - /// The for the . + /// The for the . /// readonly ITokenFactory tokenFactory; /// - /// The for the . + /// The for the . /// readonly ISystemIdentityFactory systemIdentityFactory; /// - /// The for the . + /// The for the . /// readonly ICryptographySuite cryptographySuite; /// - /// The for the . + /// The for the . /// readonly IAssemblyInformationProvider assemblyInformationProvider; /// - /// The for the . + /// The for the . /// readonly IIdentityCache identityCache; /// - /// The for the . + /// The for the . /// readonly IOAuthProviders oAuthProviders; /// - /// The for the . + /// The for the . /// readonly IPlatformIdentifier platformIdentifier; /// - /// The for the . + /// The for the . /// readonly ISwarmService swarmService; /// - /// The for the . + /// The for the . /// readonly IServerControl serverControl; /// - /// The for the . + /// The for the . /// readonly GeneralConfiguration generalConfiguration; /// - /// The for the . - /// - readonly ControlPanelConfiguration controlPanelConfiguration; - - /// - /// Initializes a new instance of the class. + /// Initializes a new instance of the class. /// /// The for the . /// The for the . @@ -105,10 +100,9 @@ public sealed class HomeController : ApiController /// The value of . /// The value of . /// The containing the value of . - /// The containing the value of . /// The for the . /// The for the . - public HomeController( + public ApiRootController( IDatabaseContext databaseContext, IAuthenticationContext authenticationContext, ITokenFactory tokenFactory, @@ -121,8 +115,7 @@ public HomeController( ISwarmService swarmService, IServerControl serverControl, IOptions generalConfigurationOptions, - IOptions controlPanelConfigurationOptions, - ILogger logger, + ILogger logger, IApiHeadersProvider apiHeadersProvider) : base( databaseContext, @@ -141,7 +134,6 @@ public HomeController( this.swarmService = swarmService ?? throw new ArgumentNullException(nameof(swarmService)); this.serverControl = serverControl ?? throw new ArgumentNullException(nameof(serverControl)); generalConfiguration = generalConfigurationOptions?.Value ?? throw new ArgumentNullException(nameof(generalConfigurationOptions)); - controlPanelConfiguration = controlPanelConfigurationOptions?.Value ?? throw new ArgumentNullException(nameof(controlPanelConfigurationOptions)); } /// @@ -154,29 +146,12 @@ public HomeController( [HttpGet] [AllowAnonymous] [ProducesResponseType(typeof(ServerInformationResponse), 200)] -#pragma warning disable CA1506 - public IActionResult Home() + public IActionResult ServerInfo() { - if (controlPanelConfiguration.Enable) - Response.Headers.Add( - HeaderNames.Vary, - new StringValues(ApiHeaders.ApiVersionHeader)); - // if they tried to authenticate in any form and failed, let them know immediately bool failIfUnauthed; if (ApiHeaders == null) { - if (controlPanelConfiguration.Enable && !Request.Headers.TryGetValue(ApiHeaders.ApiVersionHeader, out _)) - { - Logger.LogDebug("No API headers on request, redirecting to control panel..."); - - var controlPanelRoute = controlPanelConfiguration.PublicPath; - if (String.IsNullOrWhiteSpace(controlPanelRoute)) - controlPanelRoute = ControlPanelController.ControlPanelRoute; - - return Redirect(controlPanelRoute); - } - try { // we only allow authorization header issues @@ -187,7 +162,7 @@ public IActionResult Home() return HeadersIssue(ex); } - failIfUnauthed = Request.Headers.Authorization.Any(); + failIfUnauthed = Request.Headers.Authorization.Count > 0; } else failIfUnauthed = ApiHeaders.Token != null; @@ -211,7 +186,6 @@ public IActionResult Home() UpdateInProgress = serverControl.UpdateInProgress, }); } -#pragma warning restore CA1506 /// /// Attempt to authenticate a using . @@ -231,7 +205,7 @@ public async ValueTask CreateToken(CancellationToken cancellation if (ApiHeaders == null) { Response.Headers.Add(HeaderNames.WWWAuthenticate, new StringValues($"basic realm=\"Create TGS {ApiHeaders.BearerAuthenticationScheme} token\"")); - return HeadersIssue(ApiHeadersProvider.HeadersException); + return HeadersIssue(ApiHeadersProvider.HeadersException!); } if (ApiHeaders.IsTokenAuthentication) @@ -239,12 +213,12 @@ public async ValueTask CreateToken(CancellationToken cancellation var oAuthLogin = ApiHeaders.OAuthProvider.HasValue; - ISystemIdentity systemIdentity = null; + ISystemIdentity? systemIdentity = null; if (!oAuthLogin) try { // trust the system over the database because a user's name can change while still having the same SID - systemIdentity = await systemIdentityFactory.CreateSystemIdentity(ApiHeaders.Username, ApiHeaders.Password, cancellationToken); + systemIdentity = await systemIdentityFactory.CreateSystemIdentity(ApiHeaders.Username!, ApiHeaders.Password!, cancellationToken); } catch (NotImplementedException) { @@ -257,8 +231,8 @@ public async ValueTask CreateToken(CancellationToken cancellation IQueryable query = DatabaseContext.Users.AsQueryable(); if (oAuthLogin) { - var oAuthProvider = ApiHeaders.OAuthProvider.Value; - string externalUserId; + var oAuthProvider = ApiHeaders.OAuthProvider!.Value; + string? externalUserId; try { var validator = oAuthProviders @@ -281,13 +255,13 @@ public async ValueTask CreateToken(CancellationToken cancellation return Unauthorized(); query = query.Where( - x => x.OAuthConnections.Any( + x => x.OAuthConnections!.Any( y => y.Provider == oAuthProvider && y.ExternalUserId == externalUserId)); } else { - var canonicalUserName = Models.User.CanonicalizeName(ApiHeaders.Username); + var canonicalUserName = Models.User.CanonicalizeName(ApiHeaders.Username!); if (canonicalUserName == Models.User.CanonicalizeName(Models.User.TgsSystemUserName)) return Unauthorized(); @@ -329,7 +303,7 @@ public async ValueTask CreateToken(CancellationToken cancellation if (!usingSystemIdentity) { // DB User password check and update - if (!isLikelyDbUser || !cryptographySuite.CheckUserPassword(user, ApiHeaders.Password)) + if (!isLikelyDbUser || !cryptographySuite.CheckUserPassword(user, ApiHeaders.Password!)) return Unauthorized(); if (user.PasswordHash != originalHash) { @@ -345,7 +319,7 @@ public async ValueTask CreateToken(CancellationToken cancellation } else { - var usernameMismatch = systemIdentity.Username != user.Name; + var usernameMismatch = systemIdentity!.Username != user.Name; if (isLikelyDbUser || usernameMismatch) { DatabaseContext.Users.Attach(user); @@ -370,7 +344,7 @@ public async ValueTask CreateToken(CancellationToken cancellation } // Now that the bookeeping is done, tell them to fuck off if necessary - if (!user.Enabled.Value) + if (!user.Enabled!.Value) { Logger.LogTrace("Not logging in disabled user {userId}.", user.Id); return Forbid(); @@ -383,7 +357,7 @@ public async ValueTask CreateToken(CancellationToken cancellation var identExpiry = token.ParseJwt().ValidTo; identExpiry += tokenFactory.ValidationParameters.ClockSkew; identExpiry += TimeSpan.FromSeconds(15); - identityCache.CacheSystemIdentity(user, systemIdentity, identExpiry); + identityCache.CacheSystemIdentity(user, systemIdentity!, identExpiry); } Logger.LogDebug("Successfully logged in user {userId}!", user.Id); diff --git a/src/Tgstation.Server.Host/Controllers/BridgeController.cs b/src/Tgstation.Server.Host/Controllers/BridgeController.cs index 787bb33f869..4b1a1fac3e6 100644 --- a/src/Tgstation.Server.Host/Controllers/BridgeController.cs +++ b/src/Tgstation.Server.Host/Controllers/BridgeController.cs @@ -8,9 +8,12 @@ using Microsoft.AspNetCore.Mvc; using Microsoft.Extensions.Hosting; using Microsoft.Extensions.Logging; + using Newtonsoft.Json; + using Serilog.Context; +using Tgstation.Server.Api; using Tgstation.Server.Host.Components.Interop; using Tgstation.Server.Host.Components.Interop.Bridge; using Tgstation.Server.Host.Utils; @@ -18,12 +21,18 @@ namespace Tgstation.Server.Host.Controllers { /// - /// for recieving DMAPI requests from DreamDaemon. + /// for receiving DMAPI requests from DreamDaemon. /// - [Route("/Bridge")] + [Route("/" + RouteExtension)] // obsolete route, but BYOND can't handle a simple fucking 301 + [Route(Routes.ApiRoot + RouteExtension)] [ApiExplorerSettings(IgnoreApi = true)] public sealed class BridgeController : ApiControllerBase { + /// + /// The route to the . + /// + const string RouteExtension = "Bridge"; + /// /// If the content of bridge requests and responses should be logged. /// @@ -80,7 +89,7 @@ public async ValueTask Process([FromQuery] string data, Cancellat { // Nothing to see here var remoteIP = Request.HttpContext.Connection.RemoteIpAddress; - if (!IPAddress.IsLoopback(remoteIP)) + if (remoteIP == null || !IPAddress.IsLoopback(remoteIP)) { logger.LogTrace("Rejecting remote bridge request from {remoteIP}", remoteIP); return Forbid(); @@ -88,10 +97,10 @@ public async ValueTask Process([FromQuery] string data, Cancellat using (LogContext.PushProperty(SerilogContextHelper.BridgeRequestIterationContextProperty, Interlocked.Increment(ref requestsProcessed))) { - var request = new BridgeParameters(); + BridgeParameters? request; try { - JsonConvert.PopulateObject(data, request, DMApiConstants.SerializerSettings); + request = JsonConvert.DeserializeObject(data, DMApiConstants.SerializerSettings); } catch (Exception ex) { @@ -100,6 +109,13 @@ public async ValueTask Process([FromQuery] string data, Cancellat return BadRequest(); } + if (request == null) + { + if (LogContent) + logger.LogWarning("Error deserializing bridge request: {badJson}", data); + return BadRequest(); + } + if (LogContent) logger.LogTrace("Bridge Request: {json}", data); diff --git a/src/Tgstation.Server.Host/Controllers/ChatController.cs b/src/Tgstation.Server.Host/Controllers/ChatController.cs index 7f9682c43aa..9ea7f4f5a70 100644 --- a/src/Tgstation.Server.Host/Controllers/ChatController.cs +++ b/src/Tgstation.Server.Host/Controllers/ChatController.cs @@ -25,6 +25,7 @@ using Tgstation.Server.Host.Models; using Tgstation.Server.Host.Security; using Tgstation.Server.Host.Utils; + using Z.EntityFramework.Plus; namespace Tgstation.Server.Host.Controllers @@ -69,10 +70,6 @@ static Models.ChatChannel ConvertApiChatChannel(Api.Models.ChatChannel api, Chat { var result = new Models.ChatChannel { -#pragma warning disable CS0618 - DiscordChannelId = api.DiscordChannelId, - IrcChannel = api.IrcChannel, -#pragma warning restore CS0618 IsAdminChannel = api.IsAdminChannel ?? false, IsWatchdogChannel = api.IsWatchdogChannel ?? false, IsUpdatesChannel = api.IsUpdatesChannel ?? false, @@ -122,20 +119,20 @@ public async ValueTask Create([FromBody] ChatBotCreateRequest mod .Where(x => x.InstanceId == Instance.Id) .CountAsync(cancellationToken); - if (countOfExistingBotsInInstance >= Instance.ChatBotLimit.Value) + if (countOfExistingBotsInInstance >= Instance.ChatBotLimit!.Value) return Conflict(new ErrorMessageResponse(ErrorCode.ChatBotMax)); model.Enabled ??= false; model.ReconnectionInterval ??= 1; // try to update das db first - var dbModel = new ChatBot + var newChannels = model.Channels?.Select(x => ConvertApiChatChannel(x, model.Provider!.Value)).ToList() ?? new List(); // important that this isn't null + var dbModel = new ChatBot(newChannels) { Name = model.Name, ConnectionString = model.ConnectionString, Enabled = model.Enabled, - Channels = model.Channels?.Select(x => ConvertApiChatChannel(x, model.Provider.Value)).ToList() ?? new List(), // important that this isn't null - InstanceId = Instance.Id.Value, + InstanceId = Instance.Id!.Value, Provider = model.Provider, ReconnectionInterval = model.ReconnectionInterval, ChannelLimit = model.ChannelLimit, @@ -144,7 +141,7 @@ public async ValueTask Create([FromBody] ChatBotCreateRequest mod DatabaseContext.ChatBots.Add(dbModel); await DatabaseContext.Save(cancellationToken); - return await WithComponentInstance( + return await WithComponentInstanceNullable( async instance => { try @@ -153,7 +150,7 @@ public async ValueTask Create([FromBody] ChatBotCreateRequest mod await instance.Chat.ChangeSettings(dbModel, cancellationToken); if (dbModel.Channels.Count > 0) - await instance.Chat.ChangeChannels(dbModel.Id.Value, dbModel.Channels, cancellationToken); + await instance.Chat.ChangeChannels(dbModel.Id!.Value, dbModel.Channels, cancellationToken); } catch { @@ -162,7 +159,7 @@ public async ValueTask Create([FromBody] ChatBotCreateRequest mod // DCTx2: Operations must always run await DatabaseContext.Save(default); - await instance.Chat.DeleteConnection(dbModel.Id.Value, default); + await instance.Chat.DeleteConnection(dbModel.Id!.Value, default); throw; } @@ -183,7 +180,7 @@ public async ValueTask Create([FromBody] ChatBotCreateRequest mod [TgsAuthorize(ChatBotRights.Delete)] [ProducesResponseType(204)] public async ValueTask Delete(long id, CancellationToken cancellationToken) - => await WithComponentInstance( + => await WithComponentInstanceNullable( async instance => { await Task.WhenAll( @@ -284,7 +281,7 @@ public async ValueTask Update([FromBody] ChatBotUpdateRequest mod { ArgumentNullException.ThrowIfNull(model); - var earlyOut = StandardModelChecks(model, false); + IActionResult? earlyOut = StandardModelChecks(model, false); if (earlyOut != null) return earlyOut; @@ -299,7 +296,7 @@ public async ValueTask Update([FromBody] ChatBotUpdateRequest mod if (current == default) return this.Gone(); - if ((model.Channels?.Count ?? current.Channels.Count) > (model.ChannelLimit ?? current.ChannelLimit.Value)) + if ((model.Channels?.Count ?? current.Channels!.Count) > (model.ChannelLimit ?? current.ChannelLimit!.Value)) { // 400 or 409 depends on if the client sent both var errorMessage = new ErrorMessageResponse(ErrorCode.ChatBotMaxChannels); @@ -342,28 +339,28 @@ bool CheckModified(Expression> expression, ChatBotRi var hasChannels = model.Channels != null; if (hasChannels || (model.Provider.HasValue && model.Provider != oldProvider)) { - DatabaseContext.ChatChannels.RemoveRange(current.Channels); + DatabaseContext.ChatChannels.RemoveRange(current.Channels!); if (hasChannels) { - var dbChannels = model.Channels.Select(x => ConvertApiChatChannel(x, model.Provider ?? current.Provider.Value)).ToList(); + var dbChannels = model.Channels!.Select(x => ConvertApiChatChannel(x, model.Provider ?? current.Provider!.Value)).ToList(); DatabaseContext.ChatChannels.AddRange(dbChannels); current.Channels = dbChannels; } else - current.Channels.Clear(); + current.Channels!.Clear(); } await DatabaseContext.Save(cancellationToken); - earlyOut = await WithComponentInstance( + earlyOut = await WithComponentInstanceNullable( async instance => { var chat = instance.Chat; if (anySettingsModified) await chat.ChangeSettings(current, cancellationToken); // have to rebuild the thing first - if ((model.Channels != null || anySettingsModified) && current.Enabled.Value) - await chat.ChangeChannels(current.Id.Value, current.Channels, cancellationToken); + if ((model.Channels != null || anySettingsModified) && current.Enabled!.Value) + await chat.ChangeChannels(current.Id!.Value, current.Channels, cancellationToken); return null; }); @@ -385,8 +382,8 @@ bool CheckModified(Expression> expression, ChatBotRi /// /// The to validate. /// If the is being created. - /// An to respond with or . - IActionResult StandardModelChecks(ChatBotApiBase model, bool forCreation) + /// An to respond with or . + BadRequestObjectResult? StandardModelChecks(ChatBotApiBase model, bool forCreation) { if (model.ReconnectionInterval == 0) throw new InvalidOperationException("RecconnectionInterval cannot be zero!"); diff --git a/src/Tgstation.Server.Host/Controllers/ComponentInterfacingController.cs b/src/Tgstation.Server.Host/Controllers/ComponentInterfacingController.cs index eef1af8b180..f04e620d06a 100644 --- a/src/Tgstation.Server.Host/Controllers/ComponentInterfacingController.cs +++ b/src/Tgstation.Server.Host/Controllers/ComponentInterfacingController.cs @@ -10,6 +10,7 @@ using Tgstation.Server.Api.Models.Response; using Tgstation.Server.Host.Components; using Tgstation.Server.Host.Database; +using Tgstation.Server.Host.Models; using Tgstation.Server.Host.Security; using Tgstation.Server.Host.Utils; @@ -63,21 +64,22 @@ protected ComponentInterfacingController( } /// - protected override async ValueTask ValidateRequest(CancellationToken cancellationToken) + protected override async ValueTask ValidateRequest(CancellationToken cancellationToken) { if (!useInstanceRequestHeader) return null; - if (!ApiHeaders.InstanceId.HasValue) + if (!ApiHeaders!.InstanceId.HasValue) return BadRequest(new ErrorMessageResponse(ErrorCode.InstanceHeaderRequired)); if (AuthenticationContext.InstancePermissionSet == null) return Forbid(); - if (ValidateInstanceOnlineStatus(Instance)) + var instance = Instance!; + if (ValidateInstanceOnlineStatus(instance)) await DatabaseContext.Save(cancellationToken); - using var instanceReferenceCheck = instanceManager.GetInstanceReference(Instance); + using var instanceReferenceCheck = instanceManager.GetInstanceReference(instance); if (instanceReferenceCheck == null) return Conflict(new ErrorMessageResponse(ErrorCode.InstanceOffline)); @@ -97,7 +99,7 @@ protected bool ValidateInstanceOnlineStatus(Api.Models.Instance metadata) using (var instanceReferenceCheck = instanceManager.GetInstanceReference(metadata)) online = instanceReferenceCheck != null; - if (metadata.Online.Value == online) + if (metadata.Require(x => x.Online) == online) return false; const string OfflineWord = "offline"; @@ -120,19 +122,29 @@ protected bool ValidateInstanceOnlineStatus(Api.Models.Instance metadata) /// The to grab. If , will be used. /// A resulting in the that should be returned. /// The context of should be as small as possible so as to avoid race conditions. This function can return a if the requested instance was offline. - protected async ValueTask WithComponentInstance(Func> action, Models.Instance instance = null) + protected async ValueTask WithComponentInstanceNullable(Func> action, Models.Instance? instance = null) { ArgumentNullException.ThrowIfNull(action); - instance ??= Instance; + instance ??= Instance ?? throw new InvalidOperationException("ComponentInterfacingController has no Instance!"); using var instanceReference = instanceManager.GetInstanceReference(instance); - using (LogContext.PushProperty(SerilogContextHelper.InstanceReferenceContextProperty, instanceReference.Uid)) + using (LogContext.PushProperty(SerilogContextHelper.InstanceReferenceContextProperty, instanceReference?.Uid)) { if (instanceReference == null) return Conflict(new ErrorMessageResponse(ErrorCode.InstanceOffline)); return await action(instanceReference); } } + + /// + /// Run a given with the relevant . + /// + /// A accepting the and returning a with the . + /// The to grab. If , will be used. + /// A resulting in the that should be returned. + /// The context of should be as small as possible so as to avoid race conditions. This function can return a if the requested instance was offline. + protected async ValueTask WithComponentInstance(Func> action, Models.Instance? instance = null) + => (await WithComponentInstanceNullable(async core => await action(core), instance))!; } } diff --git a/src/Tgstation.Server.Host/Controllers/ConfigurationController.cs b/src/Tgstation.Server.Host/Controllers/ConfigurationController.cs index 37b7e89ed1d..8232be095c0 100644 --- a/src/Tgstation.Server.Host/Controllers/ConfigurationController.cs +++ b/src/Tgstation.Server.Host/Controllers/ConfigurationController.cs @@ -73,7 +73,7 @@ public ConfigurationController( public async ValueTask Update([FromBody] ConfigurationFileRequest model, CancellationToken cancellationToken) { ArgumentNullException.ThrowIfNull(model); - if (ForbidDueToModeConflicts(model.Path, out var systemIdentity)) + if (ForbidDueToModeConflicts(model.Path!, out var systemIdentity)) return Forbid(); try @@ -84,7 +84,7 @@ public async ValueTask Update([FromBody] ConfigurationFileRequest var newFile = await instance .Configuration .Write( - model.Path, + model.Path!, systemIdentity, model.LastReadHash, cancellationToken); @@ -164,7 +164,7 @@ public async ValueTask File(string filePath, CancellationToken ca [ProducesResponseType(typeof(ErrorMessageResponse), 409)] [ProducesResponseType(typeof(ErrorMessageResponse), 410)] public ValueTask Directory( - string directoryPath, + string? directoryPath, [FromQuery] int? page, [FromQuery] int? pageSize, CancellationToken cancellationToken) @@ -240,6 +240,9 @@ public async ValueTask CreateDirectory([FromBody] ConfigurationFi { ArgumentNullException.ThrowIfNull(model); + if (model.Path == null) + return BadRequest(new ErrorMessageResponse(ErrorCode.ModelValidationFailure)); + if (ForbidDueToModeConflicts(model.Path, out var systemIdentity)) return Forbid(); @@ -338,7 +341,7 @@ public async ValueTask DeleteDirectory([FromBody] ConfigurationFi /// The path to validate if any. /// The to use when calling into . /// if a should be returned, otherwise. - bool ForbidDueToModeConflicts(string path, out ISystemIdentity systemIdentityToUse) + bool ForbidDueToModeConflicts(string? path, out ISystemIdentity? systemIdentityToUse) { if (Instance.ConfigurationType == ConfigurationType.Disallowed || (Instance.ConfigurationType == ConfigurationType.SystemIdentityWrite && AuthenticationContext.SystemIdentity == null) diff --git a/src/Tgstation.Server.Host/Controllers/ControlPanelController.cs b/src/Tgstation.Server.Host/Controllers/ControlPanelController.cs index 08d8f74dad1..d69947d4ba9 100644 --- a/src/Tgstation.Server.Host/Controllers/ControlPanelController.cs +++ b/src/Tgstation.Server.Host/Controllers/ControlPanelController.cs @@ -7,7 +7,6 @@ using Microsoft.AspNetCore.Hosting; using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Mvc.Filters; -using Microsoft.AspNetCore.StaticFiles; using Microsoft.Extensions.Logging; using Microsoft.Extensions.Options; using Microsoft.Extensions.Primitives; @@ -15,6 +14,7 @@ using Tgstation.Server.Api; using Tgstation.Server.Host.Configuration; +using Tgstation.Server.Host.Extensions; namespace Tgstation.Server.Host.Controllers { @@ -136,22 +136,11 @@ public IActionResult Get([FromRoute] string appRoute) if (Request.Headers.ContainsKey(FetchChannelVaryHeader)) return GetChannelJson(); - var fileInfo = hostEnvironment.WebRootFileProvider.GetFileInfo(appRoute); - if (fileInfo.Exists) - { - logger.LogTrace("Serving static file \"{filename}\"...", appRoute); - var contentTypeProvider = new FileExtensionContentTypeProvider(); - if (!contentTypeProvider.TryGetContentType(fileInfo.Name, out var contentType)) - contentType = MediaTypeNames.Application.Octet; - else if (contentType == MediaTypeNames.Application.Json) - Response.Headers.Add( - HeaderNames.CacheControl, - new StringValues(new[] { "public", "max-age=31536000", "immutable" })); - - return File(appRoute, contentType); - } - else - logger.LogTrace("Requested static file \"{filename}\" does not exist! Redirecting to index...", appRoute); + var foundFile = this.TryServeFile(hostEnvironment, logger, appRoute); + if (foundFile != null) + return foundFile; + + logger.LogTrace("Requested static file \"{filename}\" does not exist! Redirecting to index...", appRoute); return File("index.html", MediaTypeNames.Text.Html); } diff --git a/src/Tgstation.Server.Host/Controllers/DreamDaemonController.cs b/src/Tgstation.Server.Host/Controllers/DreamDaemonController.cs index 8e57d022c5d..069b272959b 100644 --- a/src/Tgstation.Server.Host/Controllers/DreamDaemonController.cs +++ b/src/Tgstation.Server.Host/Controllers/DreamDaemonController.cs @@ -90,7 +90,7 @@ public ValueTask Create(CancellationToken cancellationToken) var job = Job.Create(JobCode.WatchdogLaunch, AuthenticationContext.User, Instance, DreamDaemonRights.Shutdown); await jobManager.RegisterOperation( job, - (core, databaseContextFactory, paramJob, progressHandler, innerCt) => core.Watchdog.Launch(innerCt), + (core, databaseContextFactory, paramJob, progressHandler, innerCt) => core!.Watchdog.Launch(innerCt), cancellationToken); return Accepted(job.ToApi()); }); @@ -106,7 +106,7 @@ await jobManager.RegisterOperation( [TgsAuthorize(DreamDaemonRights.ReadMetadata | DreamDaemonRights.ReadRevision)] [ProducesResponseType(typeof(DreamDaemonResponse), 200)] [ProducesResponseType(typeof(ErrorMessageResponse), 410)] - public ValueTask Read(CancellationToken cancellationToken) => ReadImpl(null, cancellationToken); + public ValueTask Read(CancellationToken cancellationToken) => ReadImpl(null, false, cancellationToken); /// /// Stops the Watchdog if it's running. @@ -159,7 +159,7 @@ public async ValueTask Update([FromBody] DreamDaemonRequest model ArgumentNullException.ThrowIfNull(model); if (model.SoftShutdown == true && model.SoftRestart == true) - return BadRequest(new ErrorMessageResponse(ErrorCode.DreamDaemonDoubleSoft)); + return BadRequest(new ErrorMessageResponse(ErrorCode.GameServerDoubleSoft)); // alias for changing DD settings var current = await DatabaseContext @@ -172,7 +172,7 @@ public async ValueTask Update([FromBody] DreamDaemonRequest model if (current == default) return this.Gone(); - if (model.Port.HasValue && model.Port.Value != current.Port.Value) + if (model.Port.HasValue && model.Port.Value != current.Port!.Value) { var verifiedPort = await portAllocator .GetAvailablePort( @@ -184,14 +184,6 @@ public async ValueTask Update([FromBody] DreamDaemonRequest model return Conflict(new ErrorMessageResponse(ErrorCode.PortNotAvailable)); } -#pragma warning disable CS0618 // Type or member is obsolete - if (model.HeartbeatSeconds.HasValue && !model.HealthCheckSeconds.HasValue) - model.HealthCheckSeconds = model.HeartbeatSeconds; - - if (model.DumpOnHeartbeatRestart.HasValue && !model.DumpOnHealthCheckRestart.HasValue) - model.DumpOnHealthCheckRestart = model.DumpOnHeartbeatRestart; -#pragma warning restore CS0618 // Type or member is obsolete - var userRights = (DreamDaemonRights)AuthenticationContext.GetRight(RightsType.DreamDaemon); bool CheckModified(Expression> expression, DreamDaemonRights requiredRight) @@ -209,14 +201,15 @@ bool CheckModified(Expression x.AllowWebClient, DreamDaemonRights.SetWebClient) || CheckModified(x => x.AutoStart, DreamDaemonRights.SetAutoStart) || CheckModified(x => x.Port, DreamDaemonRights.SetPort) || CheckModified(x => x.SecurityLevel, DreamDaemonRights.SetSecurity) || CheckModified(x => x.Visibility, DreamDaemonRights.SetVisibility) - || (model.SoftRestart.HasValue && !AuthenticationContext.InstancePermissionSet.DreamDaemonRights.Value.HasFlag(DreamDaemonRights.SoftRestart)) - || (model.SoftShutdown.HasValue && !AuthenticationContext.InstancePermissionSet.DreamDaemonRights.Value.HasFlag(DreamDaemonRights.SoftShutdown)) - || (!String.IsNullOrWhiteSpace(model.BroadcastMessage) && !AuthenticationContext.InstancePermissionSet.DreamDaemonRights.Value.HasFlag(DreamDaemonRights.BroadcastMessage)) + || (model.SoftRestart.HasValue && !ddRights.HasFlag(DreamDaemonRights.SoftRestart)) + || (model.SoftShutdown.HasValue && !ddRights.HasFlag(DreamDaemonRights.SoftShutdown)) + || (!String.IsNullOrWhiteSpace(model.BroadcastMessage) && !ddRights.HasFlag(DreamDaemonRights.BroadcastMessage)) || CheckModified(x => x.StartupTimeout, DreamDaemonRights.SetStartupTimeout) || CheckModified(x => x.HealthCheckSeconds, DreamDaemonRights.SetHealthCheckInterval) || CheckModified(x => x.DumpOnHealthCheckRestart, DreamDaemonRights.CreateDump) @@ -239,7 +232,7 @@ bool CheckModified(Expression(Expression Restart(CancellationToken cancellationToken) await jobManager.RegisterOperation( job, - (core, paramJob, databaseContextFactory, progressReporter, ct) => core.Watchdog.Restart(false, ct), + (core, paramJob, databaseContextFactory, progressReporter, ct) => core!.Watchdog.Restart(false, ct), cancellationToken); return Accepted(job.ToApi()); }); @@ -304,7 +297,7 @@ public ValueTask CreateDump(CancellationToken cancellationToken) await jobManager.RegisterOperation( job, - (core, databaseContextFactory, paramJob, progressReporter, ct) => core.Watchdog.CreateDump(ct), + (core, databaseContextFactory, paramJob, progressReporter, ct) => core!.Watchdog.CreateDump(ct), cancellationToken); return Accepted(job.ToApi()); }); @@ -313,9 +306,10 @@ await jobManager.RegisterOperation( /// Implementation of . /// /// The to operate on if any. + /// If there was a settings change made that forced a switch to . /// The for the operation. /// A resulting in the of the operation. - ValueTask ReadImpl(DreamDaemonSettings settings, CancellationToken cancellationToken) + ValueTask ReadImpl(DreamDaemonSettings? settings, bool knownForcedReboot, CancellationToken cancellationToken) => WithComponentInstance(async instance => { var dd = instance.Watchdog; @@ -329,7 +323,7 @@ ValueTask ReadImpl(DreamDaemonSettings settings, CancellationToke .Instances .AsQueryable() .Where(x => x.Id == Instance.Id) - .Select(x => x.DreamDaemonSettings) + .Select(x => x.DreamDaemonSettings!) .FirstOrDefaultAsync(cancellationToken); if (settings == default) return this.Gone(); @@ -341,26 +335,41 @@ ValueTask ReadImpl(DreamDaemonSettings settings, CancellationToke var alphaActive = dd.AlphaIsActive; var llp = dd.LastLaunchParameters; var rstate = dd.RebootState; - result.AutoStart = settings.AutoStart.Value; - result.CurrentPort = llp?.Port.Value; - result.CurrentSecurity = llp?.SecurityLevel.Value; - result.CurrentVisibility = llp?.Visibility.Value; - result.CurrentAllowWebclient = llp?.AllowWebClient.Value; - result.Port = settings.Port.Value; - result.AllowWebClient = settings.AllowWebClient.Value; - result.Status = dd.Status; - result.SecurityLevel = settings.SecurityLevel.Value; - result.Visibility = settings.Visibility.Value; + result.AutoStart = settings.AutoStart!.Value; + result.CurrentPort = llp?.Port!.Value; + result.CurrentSecurity = llp?.SecurityLevel!.Value; + result.CurrentVisibility = llp?.Visibility!.Value; + result.CurrentAllowWebclient = llp?.AllowWebClient!.Value; + result.Port = settings.Port!.Value; + result.AllowWebClient = settings.AllowWebClient!.Value; + + var firstIteration = true; + do + { + if (!firstIteration) + { + cancellationToken.ThrowIfCancellationRequested(); + await Task.Yield(); + } + + firstIteration = false; + result.Status = dd.Status; + result.SessionId = dd.SessionId; + } + while (result.Status == WatchdogStatus.Online && !result.SessionId.HasValue); // this is the one invalid combo, it's not that racy + + result.SecurityLevel = settings.SecurityLevel!.Value; + result.Visibility = settings.Visibility!.Value; result.SoftRestart = rstate == RebootState.Restart; result.SoftShutdown = rstate == RebootState.Shutdown; - result.StartupTimeout = settings.StartupTimeout.Value; - result.HealthCheckSeconds = settings.HealthCheckSeconds.Value; - result.DumpOnHealthCheckRestart = settings.DumpOnHealthCheckRestart.Value; -#pragma warning disable CS0618 // Type or member is obsolete - result.HeartbeatSeconds = settings.HealthCheckSeconds.Value; - result.DumpOnHeartbeatRestart = settings.DumpOnHealthCheckRestart.Value; -#pragma warning restore CS0618 // Type or member is obsolete - result.TopicRequestTimeout = settings.TopicRequestTimeout.Value; + + if (rstate == RebootState.Normal && knownForcedReboot) + result.SoftRestart = true; + + result.StartupTimeout = settings.StartupTimeout!.Value; + result.HealthCheckSeconds = settings.HealthCheckSeconds!.Value; + result.DumpOnHealthCheckRestart = settings.DumpOnHealthCheckRestart!.Value; + result.TopicRequestTimeout = settings.TopicRequestTimeout!.Value; result.AdditionalParameters = settings.AdditionalParameters; result.StartProfiler = settings.StartProfiler; result.LogOutput = settings.LogOutput; diff --git a/src/Tgstation.Server.Host/Controllers/DreamMakerController.cs b/src/Tgstation.Server.Host/Controllers/DreamMakerController.cs index 714713763f9..419bc9c90d7 100644 --- a/src/Tgstation.Server.Host/Controllers/DreamMakerController.cs +++ b/src/Tgstation.Server.Host/Controllers/DreamMakerController.cs @@ -27,7 +27,6 @@ namespace Tgstation.Server.Host.Controllers /// for managing the deployment system. /// [Route(Routes.DreamMaker)] -#pragma warning disable CA1506 // TODO: Decomplexify public sealed class DreamMakerController : InstanceRequiredController { /// @@ -85,6 +84,10 @@ public async ValueTask Read(CancellationToken cancellationToken) .AsQueryable() .Where(x => x.InstanceId == Instance.Id) .FirstOrDefaultAsync(cancellationToken); + + if (dreamMakerSettings == null) + return this.Gone(); + return Json(dreamMakerSettings.ToApi()); } @@ -148,7 +151,7 @@ public async ValueTask Create(CancellationToken cancellationToken await jobManager.RegisterOperation( job, (core, databaseContextFactory, paramJob, progressReporter, jobCancellationToken) - => core.DreamMaker.DeploymentProcess(paramJob, databaseContextFactory, progressReporter, jobCancellationToken), + => core!.DreamMaker.DeploymentProcess(paramJob, databaseContextFactory, progressReporter, jobCancellationToken), cancellationToken); return Accepted(job.ToApi()); } @@ -187,9 +190,10 @@ public async ValueTask Update([FromBody] DreamMakerRequest model, if (hostModel == null) return this.Gone(); + var dreamMakerRights = InstancePermissionSet.DreamMakerRights!.Value; if (model.ProjectName != null) { - if (!AuthenticationContext.InstancePermissionSet.DreamMakerRights.Value.HasFlag(DreamMakerRights.SetDme)) + if (!dreamMakerRights.HasFlag(DreamMakerRights.SetDme)) return Forbid(); if (model.ProjectName.Length == 0) hostModel.ProjectName = null; @@ -199,10 +203,10 @@ public async ValueTask Update([FromBody] DreamMakerRequest model, if (model.ApiValidationPort.HasValue) { - if (!AuthenticationContext.InstancePermissionSet.DreamMakerRights.Value.HasFlag(DreamMakerRights.SetApiValidationPort)) + if (!dreamMakerRights.HasFlag(DreamMakerRights.SetApiValidationPort)) return Forbid(); - if (model.ApiValidationPort.Value != hostModel.ApiValidationPort.Value) + if (model.ApiValidationPort.Value != hostModel.ApiValidationPort!.Value) { var verifiedPort = await portAllocator .GetAvailablePort( @@ -218,28 +222,28 @@ public async ValueTask Update([FromBody] DreamMakerRequest model, if (model.ApiValidationSecurityLevel.HasValue) { - if (!AuthenticationContext.InstancePermissionSet.DreamMakerRights.Value.HasFlag(DreamMakerRights.SetSecurityLevel)) + if (!dreamMakerRights.HasFlag(DreamMakerRights.SetSecurityLevel)) return Forbid(); hostModel.ApiValidationSecurityLevel = model.ApiValidationSecurityLevel; } if (model.RequireDMApiValidation.HasValue) { - if (!AuthenticationContext.InstancePermissionSet.DreamMakerRights.Value.HasFlag(DreamMakerRights.SetApiValidationRequirement)) + if (!dreamMakerRights.HasFlag(DreamMakerRights.SetApiValidationRequirement)) return Forbid(); hostModel.RequireDMApiValidation = model.RequireDMApiValidation; } if (model.Timeout.HasValue) { - if (!AuthenticationContext.InstancePermissionSet.DreamMakerRights.Value.HasFlag(DreamMakerRights.SetTimeout)) + if (!dreamMakerRights.HasFlag(DreamMakerRights.SetTimeout)) return Forbid(); hostModel.Timeout = model.Timeout; } await DatabaseContext.Save(cancellationToken); - if ((AuthenticationContext.GetRight(RightsType.DreamMaker) & (ulong)DreamMakerRights.Read) == 0) + if (!dreamMakerRights.HasFlag(DreamMakerRights.Read)) return NoContent(); return await Read(cancellationToken); @@ -252,17 +256,17 @@ public async ValueTask Update([FromBody] DreamMakerRequest model, IQueryable BaseCompileJobsQuery() => DatabaseContext .CompileJobs .AsQueryable() - .Include(x => x.Job) + .Include(x => x.Job!) .ThenInclude(x => x.StartedBy) - .Include(x => x.Job) + .Include(x => x.Job!) .ThenInclude(x => x.Instance) - .Include(x => x.RevisionInformation) - .ThenInclude(x => x.PrimaryTestMerge) + .Include(x => x.RevisionInformation!) + .ThenInclude(x => x.PrimaryTestMerge!) .ThenInclude(x => x.MergedBy) .Include(x => x.RevisionInformation) - .ThenInclude(x => x.ActiveTestMerges) - .ThenInclude(x => x.TestMerge) - .ThenInclude(x => x.MergedBy) - .Where(x => x.Job.Instance.Id == Instance.Id); + .ThenInclude(x => x.ActiveTestMerges!) + .ThenInclude(x => x!.TestMerge) + .ThenInclude(x => x!.MergedBy) + .Where(x => x.Job.Instance!.Id == Instance.Id); } } diff --git a/src/Tgstation.Server.Host/Controllers/ByondController.cs b/src/Tgstation.Server.Host/Controllers/EngineController.cs similarity index 53% rename from src/Tgstation.Server.Host/Controllers/ByondController.cs rename to src/Tgstation.Server.Host/Controllers/EngineController.cs index 10a02eee868..c1a20fe99ca 100644 --- a/src/Tgstation.Server.Host/Controllers/ByondController.cs +++ b/src/Tgstation.Server.Host/Controllers/EngineController.cs @@ -9,6 +9,7 @@ using Tgstation.Server.Api; using Tgstation.Server.Api.Models; +using Tgstation.Server.Api.Models.Internal; using Tgstation.Server.Api.Models.Request; using Tgstation.Server.Api.Models.Response; using Tgstation.Server.Api.Rights; @@ -25,18 +26,18 @@ namespace Tgstation.Server.Host.Controllers { /// - /// Controller for managing BYOND installations. + /// Controller for managing engine installations. /// - [Route(Routes.Byond)] - public sealed class ByondController : InstanceRequiredController + [Route(Routes.Engine)] + public sealed class EngineController : InstanceRequiredController { /// - /// The for the . + /// The for the . /// readonly IJobManager jobManager; /// - /// The for the . + /// The for the . /// readonly IFileTransferTicketProvider fileTransferService; @@ -45,10 +46,10 @@ public sealed class ByondController : InstanceRequiredController /// /// The to normalize. /// The normalized . May be a reference to . - static Version NormalizeVersion(Version version) => version.Build == 0 ? new Version(version.Major, version.Minor) : version; + static Version NormalizeByondVersion(Version version) => version.Build == 0 ? new Version(version.Major, version.Minor) : version; /// - /// Initializes a new instance of the class. + /// Initializes a new instance of the class. /// /// The for the . /// The for the . @@ -57,10 +58,10 @@ public sealed class ByondController : InstanceRequiredController /// The value of . /// The value of . /// The for the . - public ByondController( + public EngineController( IDatabaseContext databaseContext, IAuthenticationContext authenticationContext, - ILogger logger, + ILogger logger, IInstanceManager instanceManager, IJobManager jobManager, IFileTransferTicketProvider fileTransferService, @@ -77,23 +78,26 @@ public ByondController( } /// - /// Gets the active . + /// Gets the active . /// /// A resulting in the for the operation. /// Retrieved version information successfully. + /// No engine versions installed. [HttpGet] - [TgsAuthorize(ByondRights.ReadActive)] - [ProducesResponseType(typeof(ByondResponse), 200)] + [TgsAuthorize(EngineRights.ReadActive)] + [ProducesResponseType(typeof(EngineResponse), 200)] + [ProducesResponseType(typeof(ErrorMessageResponse), 409)] public ValueTask Read() => WithComponentInstance(instance => ValueTask.FromResult( - Json(new ByondResponse - { - Version = instance.ByondManager.ActiveVersion, - }))); + Json( + new EngineResponse + { + EngineVersion = instance.EngineManager.ActiveVersion, + }))); /// - /// Lists installed s. + /// Lists installed s. /// /// The current page. /// The page size. @@ -101,110 +105,119 @@ public ValueTask Read() /// A resulting in the for the operation. /// Retrieved version information successfully. [HttpGet(Routes.List)] - [TgsAuthorize(ByondRights.ListInstalled)] - [ProducesResponseType(typeof(PaginatedResponse), 200)] + [TgsAuthorize(EngineRights.ListInstalled)] + [ProducesResponseType(typeof(PaginatedResponse), 200)] public ValueTask List([FromQuery] int? page, [FromQuery] int? pageSize, CancellationToken cancellationToken) => WithComponentInstance( instance => Paginated( () => ValueTask.FromResult( - new PaginatableResult( + new PaginatableResult( instance - .ByondManager + .EngineManager .InstalledVersions - .Select(x => new ByondResponse + .Select(x => new EngineResponse { - Version = x, + EngineVersion = x, }) .AsQueryable() - .OrderBy(x => x.Version))), + .OrderBy(x => x.EngineVersion!.ToString()))), null, page, pageSize, cancellationToken)); /// - /// Changes the active BYOND version to the one specified in a given . + /// Changes the active engine version to the one specified in a given . /// - /// The containing the to switch to. + /// The containing the to switch to. /// The for the operation. /// A resulting in the for the operation. - /// Switched active version successfully. - /// Created to install and switch active version successfully. + /// Switched active engine version successfully. + /// Created to install and switch active engine version successfully. [HttpPost] - [TgsAuthorize(ByondRights.InstallOfficialOrChangeActiveVersion | ByondRights.InstallCustomVersion)] - [ProducesResponseType(typeof(ByondInstallResponse), 200)] - [ProducesResponseType(typeof(ByondInstallResponse), 202)] -#pragma warning disable CA1506 // TODO: Decomplexify - public async ValueTask Update([FromBody] ByondVersionRequest model, CancellationToken cancellationToken) + [TgsAuthorize( + EngineRights.InstallOfficialOrChangeActiveByondVersion + | EngineRights.InstallCustomByondVersion + | EngineRights.InstallOfficialOrChangeActiveOpenDreamVersion + | EngineRights.InstallCustomOpenDreamVersion)] + [ProducesResponseType(typeof(EngineInstallResponse), 200)] + [ProducesResponseType(typeof(EngineInstallResponse), 202)] +#pragma warning disable CA1502 // TODO: Decomplexify +#pragma warning disable CA1506 + public async ValueTask Update([FromBody] EngineVersionRequest model, CancellationToken cancellationToken) #pragma warning restore CA1506 +#pragma warning restore CA1502 { ArgumentNullException.ThrowIfNull(model); + var earlyOut = ValidateEngineVersion(model.EngineVersion); + if (earlyOut != null) + return earlyOut; var uploadingZip = model.UploadCustomZip == true; - if (model.Version == null - || model.Version.Revision != -1 - || (uploadingZip && model.Version.Build > 0)) - return BadRequest(new ErrorMessageResponse(ErrorCode.ModelValidationFailure)); - - var version = NormalizeVersion(model.Version); - - var userByondRights = AuthenticationContext.InstancePermissionSet.ByondRights.Value; - if ((!userByondRights.HasFlag(ByondRights.InstallOfficialOrChangeActiveVersion) && !uploadingZip) - || (!userByondRights.HasFlag(ByondRights.InstallCustomVersion) && uploadingZip)) + var engineRights = InstancePermissionSet.EngineRights!.Value; + var isByondEngine = model.EngineVersion!.Engine!.Value == EngineType.Byond; + var officialPerm = isByondEngine + ? EngineRights.InstallOfficialOrChangeActiveByondVersion + : EngineRights.InstallOfficialOrChangeActiveOpenDreamVersion; + var customPerm = isByondEngine + ? EngineRights.InstallCustomByondVersion + : EngineRights.InstallCustomOpenDreamVersion; + if ((!engineRights.HasFlag(officialPerm) && !uploadingZip) + || (!engineRights.HasFlag(customPerm) && uploadingZip)) return Forbid(); // remove cruff fields - var result = new ByondInstallResponse(); + var result = new EngineInstallResponse(); return await WithComponentInstance( async instance => { - var byondManager = instance.ByondManager; - var versionAlreadyInstalled = !uploadingZip && byondManager.InstalledVersions.Any(x => x == version); + var byondManager = instance.EngineManager; + var versionAlreadyInstalled = !uploadingZip && byondManager.InstalledVersions.Any(x => x.Equals(model.EngineVersion)); if (versionAlreadyInstalled) { Logger.LogInformation( - "User ID {userId} changing instance ID {instanceId} BYOND version to {newByondVersion}", + "User ID {userId} changing instance ID {instanceId} engine to {newByondVersion}", AuthenticationContext.User.Id, Instance.Id, - version); + model); try { - await byondManager.ChangeVersion(null, version, null, false, cancellationToken); + await byondManager.ChangeVersion(null, model.EngineVersion, null, false, cancellationToken); } catch (InvalidOperationException ex) { Logger.LogDebug( ex, - "Race condition: BYOND version {version} uninstalled before we could switch to it. Creating install job instead...", - version); + "Race condition: Engine {version} uninstalled before we could switch to it. Creating install job instead...", + model); versionAlreadyInstalled = false; } } if (!versionAlreadyInstalled) { - if (version.Build > 0) - return BadRequest(new ErrorMessageResponse(ErrorCode.ByondNonExistentCustomVersion)); + if (model.EngineVersion.CustomIteration.HasValue) + return BadRequest(new ErrorMessageResponse(ErrorCode.EngineNonExistentCustomVersion)); Logger.LogInformation( - "User ID {userId} installing BYOND version to {newByondVersion} on instance ID {instanceId}", + "User ID {userId} installing engine version {newByondVersion} on instance ID {instanceId}", AuthenticationContext.User.Id, - version, + model, Instance.Id); // run the install through the job manager - var job = Job.Create( + var job = Models.Job.Create( uploadingZip - ? JobCode.ByondCustomInstall - : JobCode.ByondOfficialInstall, + ? JobCode.EngineCustomInstall + : JobCode.EngineOfficialInstall, AuthenticationContext.User, Instance, - ByondRights.CancelInstall); - job.Description += $" {version}"; + EngineRights.CancelInstall); + job.Description += $" {model.EngineVersion}"; - IFileUploadTicket fileUploadTicket = null; + IFileUploadTicket? fileUploadTicket = null; if (uploadingZip) fileUploadTicket = fileTransferService.CreateUpload(FileUploadStreamKind.None); @@ -214,7 +227,7 @@ await jobManager.RegisterOperation( job, async (core, databaseContextFactory, paramJob, progressHandler, jobCancellationToken) => { - Stream zipFileStream = null; + MemoryStream? zipFileStream = null; if (fileUploadTicket != null) await using (fileUploadTicket) { @@ -232,9 +245,9 @@ await jobManager.RegisterOperation( } await using (zipFileStream) - await core.ByondManager.ChangeVersion( + await core!.EngineManager.ChangeVersion( progressHandler, - version, + model.EngineVersion, zipFileStream, true, jobCancellationToken); @@ -260,39 +273,37 @@ await core.ByondManager.ChangeVersion( /// /// Attempts to delete the BYOND version specified in a given from the instance. /// - /// The containing the to delete. + /// The containing the to delete. /// The for the operation. /// A resulting in the for the operation. /// Created to delete target version successfully. /// Attempted to delete the active BYOND . - /// The specified was not installed. + /// The specified was not installed. [HttpDelete] - [TgsAuthorize(ByondRights.DeleteInstall)] + [TgsAuthorize(EngineRights.DeleteInstall)] [ProducesResponseType(typeof(JobResponse), 202)] [ProducesResponseType(typeof(ErrorMessageResponse), 409)] [ProducesResponseType(typeof(ErrorMessageResponse), 410)] - public async ValueTask Delete([FromBody] ByondVersionDeleteRequest model, CancellationToken cancellationToken) + public async ValueTask Delete([FromBody] EngineVersionDeleteRequest model, CancellationToken cancellationToken) { ArgumentNullException.ThrowIfNull(model); + var earlyOut = ValidateEngineVersion(model.EngineVersion); + if (earlyOut != null) + return earlyOut; - if (model.Version == null - || model.Version.Revision != -1) - return BadRequest(new ErrorMessageResponse(ErrorCode.ModelValidationFailure)); - - var version = NormalizeVersion(model.Version); - - var notInstalledResponse = await WithComponentInstance( + var engineVersion = model.EngineVersion!; + var notInstalledResponse = await WithComponentInstanceNullable( instance => { - var byondManager = instance.ByondManager; + var byondManager = instance.EngineManager; + var activeVersion = byondManager.ActiveVersion; + if (activeVersion != null && engineVersion.Equals(activeVersion)) + return ValueTask.FromResult( + Conflict(new ErrorMessageResponse(ErrorCode.EngineCannotDeleteActiveVersion))); - if (version == byondManager.ActiveVersion) - return ValueTask.FromResult( - Conflict(new ErrorMessageResponse(ErrorCode.ByondCannotDeleteActiveVersion))); + var versionNotInstalled = !byondManager.InstalledVersions.Any(x => x.Equals(engineVersion)); - var versionNotInstalled = !byondManager.InstalledVersions.Any(x => x == version); - - return ValueTask.FromResult( + return ValueTask.FromResult( versionNotInstalled ? this.Gone() : null); @@ -301,20 +312,58 @@ public async ValueTask Delete([FromBody] ByondVersionDeleteReques if (notInstalledResponse != null) return notInstalledResponse; - var isCustomVersion = version.Build != -1; + var isByondVersion = engineVersion.Engine!.Value == EngineType.Byond; // run the install through the job manager - var job = Job.Create(JobCode.ByondDelete, AuthenticationContext.User, Instance, isCustomVersion ? ByondRights.InstallOfficialOrChangeActiveVersion : ByondRights.InstallCustomVersion); - job.Description += $" {version}"; + var cancelRight = isByondVersion + ? engineVersion.CustomIteration.HasValue + ? EngineRights.InstallCustomByondVersion + : EngineRights.InstallOfficialOrChangeActiveByondVersion + : engineVersion.CustomIteration.HasValue + ? EngineRights.InstallOfficialOrChangeActiveOpenDreamVersion + : EngineRights.InstallCustomOpenDreamVersion; + + var job = Models.Job.Create(JobCode.EngineDelete, AuthenticationContext.User, Instance, cancelRight); + job.Description += $" {engineVersion}"; await jobManager.RegisterOperation( job, (instanceCore, databaseContextFactory, job, progressReporter, jobCancellationToken) - => instanceCore.ByondManager.DeleteVersion(progressReporter, version, jobCancellationToken), + => instanceCore!.EngineManager.DeleteVersion(progressReporter, engineVersion, jobCancellationToken), cancellationToken); var apiResponse = job.ToApi(); return Accepted(apiResponse); } + + /// + /// Validate and normalize a given . + /// + /// The to validate and normalize. + /// The to return, if any. otherwise. + BadRequestObjectResult? ValidateEngineVersion(EngineVersion? version) + { + if (version == null || !version.Engine.HasValue) + return BadRequest(new ErrorMessageResponse(ErrorCode.ModelValidationFailure)); + + var isByond = version.Engine.Value == EngineType.Byond; + var validSha = version.SourceSHA?.Length == Limits.MaximumCommitShaLength; + if ((isByond + && (version.Version == null + || validSha)) + || (version.Engine.Value == EngineType.OpenDream && + ((version.SourceSHA == null && version.Version == null) + || (version.Version != null && (version.Version.Revision != -1 || validSha))))) + return BadRequest(new ErrorMessageResponse(ErrorCode.ModelValidationFailure)); + + if (isByond) + { + version.Version = NormalizeByondVersion(version.Version!); + if (version.Version.Build != -1) + return BadRequest(new ErrorMessageResponse(ErrorCode.ModelValidationFailure)); + } + + return null; + } } } diff --git a/src/Tgstation.Server.Host/Controllers/InstanceController.cs b/src/Tgstation.Server.Host/Controllers/InstanceController.cs index 342d9193b6c..ded93134755 100644 --- a/src/Tgstation.Server.Host/Controllers/InstanceController.cs +++ b/src/Tgstation.Server.Host/Controllers/InstanceController.cs @@ -1,5 +1,6 @@ using System; using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; using System.IO; using System.Linq; using System.Linq.Expressions; @@ -43,11 +44,6 @@ public sealed class InstanceController : ComponentInterfacingController /// public const string InstanceAttachFileName = "TGS4_ALLOW_INSTANCE_ATTACH"; - /// - /// Prefix for move s. - /// - const string MoveInstanceJobPrefix = "Move instance ID "; - /// /// The for the . /// @@ -145,8 +141,8 @@ public async ValueTask Create([FromBody] InstanceCreateRequest mo { ArgumentNullException.ThrowIfNull(model); - if (String.IsNullOrWhiteSpace(model.Name)) - return BadRequest(new ErrorMessageResponse(ErrorCode.InstanceWhitespaceName)); + if (String.IsNullOrWhiteSpace(model.Name) || String.IsNullOrWhiteSpace(model.Path)) + return BadRequest(new ErrorMessageResponse(ErrorCode.InstanceWhitespaceNameOrPath)); var unNormalizedPath = model.Path; var targetInstancePath = NormalizePath(unNormalizedPath); @@ -170,7 +166,7 @@ bool InstanceIsChildOf(string otherPath) return Conflict(new ErrorMessageResponse(ErrorCode.InstanceAtConflictingPath)); // Validate it's not a child of any other instance - IActionResult earlyOut = null; + IActionResult? earlyOut = null; ulong countOfOtherInstances = 0; using (var cts = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken)) { @@ -190,7 +186,7 @@ await DatabaseContext { if (++countOfOtherInstances >= generalConfiguration.InstanceLimit) earlyOut ??= Conflict(new ErrorMessageResponse(ErrorCode.InstanceLimitReached)); - else if (InstanceIsChildOf(otherInstance.Path)) + else if (InstanceIsChildOf(otherInstance.Path!)) earlyOut ??= Conflict(new ErrorMessageResponse(ErrorCode.InstanceAtConflictingPath)); if (earlyOut != null && !newCancellationToken.IsCancellationRequested) @@ -306,15 +302,16 @@ public async ValueTask Delete(long id, CancellationToken cancella .FirstOrDefaultAsync(cancellationToken); if (originalModel == default) return this.Gone(); - if (originalModel.Online.Value) + if (originalModel.Online!.Value) return Conflict(new ErrorMessageResponse(ErrorCode.InstanceDetachOnline)); DatabaseContext.Instances.Remove(originalModel); - var attachFileName = ioManager.ConcatPath(originalModel.Path, InstanceAttachFileName); + var originalPath = originalModel.Path!; + var attachFileName = ioManager.ConcatPath(originalPath, InstanceAttachFileName); try { - if (await ioManager.DirectoryExists(originalModel.Path, cancellationToken)) + if (await ioManager.DirectoryExists(originalPath, cancellationToken)) await ioManager.WriteAllBytes(attachFileName, Array.Empty(), cancellationToken); } catch (OperationCanceledException) @@ -353,14 +350,15 @@ public async ValueTask Update([FromBody] InstanceUpdateRequest mo .Where(x => x.Id == model.Id && x.SwarmIdentifer == swarmConfiguration.Identifier); var moveJob = await InstanceQuery() - .SelectMany(x => x.Jobs). - Where(x => !x.StoppedAt.HasValue && x.Description.StartsWith(MoveInstanceJobPrefix)) - .Select(x => new Job(x.Id.Value)).FirstOrDefaultAsync(cancellationToken); + .SelectMany(x => x.Jobs) + .Where(x => !x.StoppedAt.HasValue && x.JobCode == JobCode.Move) + .Select(x => new Job(x.Id!.Value)) + .FirstOrDefaultAsync(cancellationToken); - if (moveJob != default) + if (moveJob != null) { // don't allow them to cancel it if they can't start it. - if (!AuthenticationContext.PermissionSet.InstanceManagerRights.Value.HasFlag(InstanceManagerRights.Relocate)) + if (!AuthenticationContext.PermissionSet.InstanceManagerRights!.Value.HasFlag(InstanceManagerRights.Relocate)) return Forbid(); await jobManager.CancelJob(moveJob, AuthenticationContext.User, true, cancellationToken); // cancel it now } @@ -393,8 +391,9 @@ bool CheckModified(Expression> expression, Insta return false; } - string originalModelPath = null; - string rawPath = null; + string? originalModelPath = null; + string? rawPath = null; + var originalOnline = originalModel.Online!.Value; if (model.Path != null) { rawPath = NormalizePath(model.Path); @@ -403,7 +402,7 @@ bool CheckModified(Expression> expression, Insta { if (!userRights.HasFlag(InstanceManagerRights.Relocate)) return Forbid(); - if (originalModel.Online.Value && model.Online != true) + if (originalOnline && model.Online != true) return Conflict(new ErrorMessageResponse(ErrorCode.InstanceRelocateOnline)); var dirExistsTask = ioManager.DirectoryExists(model.Path, cancellationToken); @@ -415,8 +414,7 @@ bool CheckModified(Expression> expression, Insta } } - var oldAutoUpdateInterval = originalModel.AutoUpdateInterval.Value; - var originalOnline = originalModel.Online.Value; + var oldAutoUpdateInterval = originalModel.AutoUpdateInterval!.Value; var renamed = model.Name != null && originalModel.Name != model.Name; if (CheckModified(x => x.AutoUpdateInterval, InstanceManagerRights.SetAutoUpdate) @@ -443,16 +441,16 @@ bool CheckModified(Expression> expression, Insta if (renamed) { // ignoring retval because we don't care if it's offline - await WithComponentInstance( + await WithComponentInstanceNullable( async componentInstance => { - await componentInstance.InstanceRenamed(originalModel.Name, cancellationToken); + await componentInstance.InstanceRenamed(originalModel.Name!, cancellationToken); return null; }, originalModel); } - var oldAutoStart = originalModel.DreamDaemonSettings.AutoStart; + var oldAutoStart = originalModel.DreamDaemonSettings!.AutoStart; try { if (originalOnline && model.Online == false) @@ -487,14 +485,14 @@ await WithComponentInstance( var moving = originalModelPath != null; if (moving) { - var description = $"{MoveInstanceJobPrefix}{originalModel.Id} from {originalModelPath} to {rawPath}"; + var description = $"Move instance ID {originalModel.Id} from {originalModelPath} to {rawPath}"; var job = Job.Create(JobCode.Move, AuthenticationContext.User, originalModel, InstanceManagerRights.Relocate); job.Description = description; await jobManager.RegisterOperation( job, (core, databaseContextFactory, paramJob, progressHandler, ct) // core will be null here since the instance is offline - => InstanceOperations.MoveInstance(originalModel, originalModelPath, ct), + => InstanceOperations.MoveInstance(originalModel, originalModelPath!, ct), cancellationToken); api.MoveJob = job.ToApi(); } @@ -502,7 +500,7 @@ await jobManager.RegisterOperation( if (model.AutoUpdateInterval.HasValue && oldAutoUpdateInterval != model.AutoUpdateInterval) { // ignoring retval because we don't care if it's offline - await WithComponentInstance( + await WithComponentInstanceNullable( async componentInstance => { await componentInstance.SetAutoUpdateInterval(model.AutoUpdateInterval.Value); @@ -538,11 +536,11 @@ public async ValueTask List( .Instances .AsQueryable() .Where(x => x.SwarmIdentifer == swarmConfiguration.Identifier); - if (!AuthenticationContext.PermissionSet.InstanceManagerRights.Value.HasFlag(InstanceManagerRights.List)) + if (!AuthenticationContext.PermissionSet.InstanceManagerRights!.Value.HasFlag(InstanceManagerRights.List)) query = query - .Where(x => x.InstancePermissionSets.Any(y => y.PermissionSetId == AuthenticationContext.PermissionSet.Id.Value)) + .Where(x => x.InstancePermissionSets.Any(y => y.PermissionSetId == AuthenticationContext.PermissionSet.Id)) .Where(x => x.InstancePermissionSets.Any(instanceUser => - instanceUser.ByondRights != ByondRights.None || + instanceUser.EngineRights != EngineRights.None || instanceUser.ChatBotRights != ChatBotRights.None || instanceUser.ConfigurationRights != ConfigurationRights.None || instanceUser.DreamDaemonRights != DreamDaemonRights.None || @@ -555,8 +553,9 @@ public async ValueTask List( var moveJobs = await GetBaseQuery() .SelectMany(x => x.Jobs) - .Where(x => !x.StoppedAt.HasValue && x.Description.StartsWith(MoveInstanceJobPrefix)) - .Include(x => x.StartedBy).ThenInclude(x => x.CreatedBy) + .Where(x => !x.StoppedAt.HasValue && x.JobCode == JobCode.Move) + .Include(x => x.StartedBy!) + .ThenInclude(x => x.CreatedBy) .Include(x => x.Instance) .ToListAsync(cancellationToken); @@ -569,7 +568,7 @@ public async ValueTask List( async instance => { needsUpdate |= ValidateInstanceOnlineStatus(instance); - instance.MoveJob = moveJobs.FirstOrDefault(x => x.Instance.Id == instance.Id)?.ToApi(); + instance.MoveJob = moveJobs.FirstOrDefault(x => x.Instance!.Id == instance.Id)?.ToApi(); await CheckAccessible(instance, cancellationToken); }, page, @@ -596,7 +595,7 @@ public async ValueTask List( [ProducesResponseType(typeof(ErrorMessageResponse), 410)] public async ValueTask GetId(long id, CancellationToken cancellationToken) { - var cantList = !AuthenticationContext.PermissionSet.InstanceManagerRights.Value.HasFlag(InstanceManagerRights.List); + var cantList = !AuthenticationContext.PermissionSet.InstanceManagerRights!.Value.HasFlag(InstanceManagerRights.List); IQueryable QueryForUser() { var query = DatabaseContext @@ -617,9 +616,9 @@ public async ValueTask GetId(long id, CancellationToken cancellat if (ValidateInstanceOnlineStatus(instance)) await DatabaseContext.Save(cancellationToken); - if (cantList && !instance.InstancePermissionSets.Any(instanceUser => instanceUser.PermissionSetId == AuthenticationContext.PermissionSet.Id.Value && - (instanceUser.RepositoryRights != RepositoryRights.None || - instanceUser.ByondRights != ByondRights.None || + if (cantList && !instance.InstancePermissionSets.Any(instanceUser => instanceUser.PermissionSetId == AuthenticationContext.PermissionSet.Require(x => x.Id) + && (instanceUser.RepositoryRights != RepositoryRights.None || + instanceUser.EngineRights != EngineRights.None || instanceUser.ChatBotRights != ChatBotRights.None || instanceUser.ConfigurationRights != ConfigurationRights.None || instanceUser.DreamDaemonRights != DreamDaemonRights.None || @@ -631,8 +630,8 @@ public async ValueTask GetId(long id, CancellationToken cancellat var moveJob = await QueryForUser() .SelectMany(x => x.Jobs) - .Where(x => !x.StoppedAt.HasValue && x.Description.StartsWith(MoveInstanceJobPrefix)) - .Include(x => x.StartedBy) + .Where(x => !x.StoppedAt.HasValue && x.JobCode == JobCode.Move) + .Include(x => x.StartedBy!) .ThenInclude(x => x.CreatedBy) .Include(x => x.Instance) .FirstOrDefaultAsync(cancellationToken); @@ -662,7 +661,7 @@ public async ValueTask GrantPermissions(long id, CancellationToke // ensure the current user has write privilege on the instance var usersInstancePermissionSet = await BaseQuery() .SelectMany(x => x.InstancePermissionSets) - .Where(x => x.PermissionSetId == AuthenticationContext.PermissionSet.Id.Value) + .Where(x => x.PermissionSetId == AuthenticationContext.PermissionSet.Id) .FirstOrDefaultAsync(cancellationToken); if (usersInstancePermissionSet == default) { @@ -691,7 +690,7 @@ public async ValueTask GrantPermissions(long id, CancellationToke /// The . /// The for the operation. /// A resulting in the new or if ports could not be allocated. - async ValueTask CreateDefaultInstance(InstanceCreateRequest initialSettings, CancellationToken cancellationToken) + async ValueTask CreateDefaultInstance(InstanceCreateRequest initialSettings, CancellationToken cancellationToken) { var ddPort = await portAllocator.GetAvailablePort(1024, false, cancellationToken); if (!ddPort.HasValue) @@ -771,14 +770,14 @@ public async ValueTask GrantPermissions(long id, CancellationToke /// /// An optional existing to update. /// or a new with full rights. - InstancePermissionSet InstanceAdminPermissionSet(InstancePermissionSet permissionSetToModify) + InstancePermissionSet InstanceAdminPermissionSet(InstancePermissionSet? permissionSetToModify) { permissionSetToModify ??= new InstancePermissionSet() { PermissionSet = AuthenticationContext.PermissionSet, - PermissionSetId = AuthenticationContext.PermissionSet.Id.Value, + PermissionSetId = AuthenticationContext.PermissionSet.Require(x => x.Id), }; - permissionSetToModify.ByondRights = RightsHelper.AllRights(); + permissionSetToModify.EngineRights = RightsHelper.AllRights(); permissionSetToModify.ChatBotRights = RightsHelper.AllRights(); permissionSetToModify.ConfigurationRights = RightsHelper.AllRights(); permissionSetToModify.DreamDaemonRights = RightsHelper.AllRights(); @@ -793,7 +792,8 @@ InstancePermissionSet InstanceAdminPermissionSet(InstancePermissionSet permissio /// /// The path to normalize. /// The normalized . - string NormalizePath(string path) + [return: NotNullIfNotNull(nameof(path))] + string? NormalizePath(string? path) { if (path == null) return null; diff --git a/src/Tgstation.Server.Host/Controllers/InstancePermissionSetController.cs b/src/Tgstation.Server.Host/Controllers/InstancePermissionSetController.cs index 30a1827de3d..d86f0b7877b 100644 --- a/src/Tgstation.Server.Host/Controllers/InstancePermissionSetController.cs +++ b/src/Tgstation.Server.Host/Controllers/InstancePermissionSetController.cs @@ -107,7 +107,7 @@ public async ValueTask Create([FromBody] InstancePermissionSetReq var dbUser = new InstancePermissionSet { - ByondRights = RightsHelper.Clamp(model.ByondRights ?? ByondRights.None), + EngineRights = RightsHelper.Clamp(model.EngineRights ?? EngineRights.None), ChatBotRights = RightsHelper.Clamp(model.ChatBotRights ?? ChatBotRights.None), ConfigurationRights = RightsHelper.Clamp(model.ConfigurationRights ?? ConfigurationRights.None), DreamDaemonRights = RightsHelper.Clamp(model.DreamDaemonRights ?? DreamDaemonRights.None), @@ -115,7 +115,7 @@ public async ValueTask Create([FromBody] InstancePermissionSetReq RepositoryRights = RightsHelper.Clamp(model.RepositoryRights ?? RepositoryRights.None), InstancePermissionSetRights = RightsHelper.Clamp(model.InstancePermissionSetRights ?? InstancePermissionSetRights.None), PermissionSetId = model.PermissionSetId, - InstanceId = Instance.Id.Value, + InstanceId = Instance.Require(x => x.Id), }; DatabaseContext.InstancePermissionSets.Add(dbUser); @@ -156,16 +156,16 @@ public async ValueTask Update([FromBody] InstancePermissionSetReq if (originalPermissionSet == null) return this.Gone(); - originalPermissionSet.ByondRights = RightsHelper.Clamp(model.ByondRights ?? originalPermissionSet.ByondRights.Value); - originalPermissionSet.RepositoryRights = RightsHelper.Clamp(model.RepositoryRights ?? originalPermissionSet.RepositoryRights.Value); - originalPermissionSet.InstancePermissionSetRights = RightsHelper.Clamp(model.InstancePermissionSetRights ?? originalPermissionSet.InstancePermissionSetRights.Value); - originalPermissionSet.ChatBotRights = RightsHelper.Clamp(model.ChatBotRights ?? originalPermissionSet.ChatBotRights.Value); - originalPermissionSet.ConfigurationRights = RightsHelper.Clamp(model.ConfigurationRights ?? originalPermissionSet.ConfigurationRights.Value); - originalPermissionSet.DreamDaemonRights = RightsHelper.Clamp(model.DreamDaemonRights ?? originalPermissionSet.DreamDaemonRights.Value); - originalPermissionSet.DreamMakerRights = RightsHelper.Clamp(model.DreamMakerRights ?? originalPermissionSet.DreamMakerRights.Value); + originalPermissionSet.EngineRights = RightsHelper.Clamp(model.EngineRights ?? originalPermissionSet.EngineRights!.Value); + originalPermissionSet.RepositoryRights = RightsHelper.Clamp(model.RepositoryRights ?? originalPermissionSet.RepositoryRights!.Value); + originalPermissionSet.InstancePermissionSetRights = RightsHelper.Clamp(model.InstancePermissionSetRights ?? originalPermissionSet.InstancePermissionSetRights!.Value); + originalPermissionSet.ChatBotRights = RightsHelper.Clamp(model.ChatBotRights ?? originalPermissionSet.ChatBotRights!.Value); + originalPermissionSet.ConfigurationRights = RightsHelper.Clamp(model.ConfigurationRights ?? originalPermissionSet.ConfigurationRights!.Value); + originalPermissionSet.DreamDaemonRights = RightsHelper.Clamp(model.DreamDaemonRights ?? originalPermissionSet.DreamDaemonRights!.Value); + originalPermissionSet.DreamMakerRights = RightsHelper.Clamp(model.DreamMakerRights ?? originalPermissionSet.DreamMakerRights!.Value); await DatabaseContext.Save(cancellationToken); - var showFullPermissionSet = originalPermissionSet.PermissionSetId == AuthenticationContext.PermissionSet.Id.Value + var showFullPermissionSet = originalPermissionSet.PermissionSetId == AuthenticationContext.PermissionSet.Require(x => x.Id) || (AuthenticationContext.GetRight(RightsType.InstancePermissionSet) & (ulong)InstancePermissionSetRights.Read) != 0; return Json( showFullPermissionSet @@ -184,7 +184,7 @@ public async ValueTask Update([FromBody] InstancePermissionSetReq [HttpGet] [TgsAuthorize] [ProducesResponseType(typeof(InstancePermissionSetResponse), 200)] - public IActionResult Read() => Json(AuthenticationContext.InstancePermissionSet.ToApi()); + public IActionResult Read() => Json(InstancePermissionSet.ToApi()); /// /// Lists s for the instance. diff --git a/src/Tgstation.Server.Host/Controllers/InstanceRequiredController.cs b/src/Tgstation.Server.Host/Controllers/InstanceRequiredController.cs index cdf3f0e139c..f3a6ff9bdcd 100644 --- a/src/Tgstation.Server.Host/Controllers/InstanceRequiredController.cs +++ b/src/Tgstation.Server.Host/Controllers/InstanceRequiredController.cs @@ -12,6 +12,16 @@ namespace Tgstation.Server.Host.Controllers /// public abstract class InstanceRequiredController : ComponentInterfacingController { + /// + /// The . + /// + protected new Models.Instance Instance => base.Instance!; + + /// + /// The for the request. + /// + protected Models.InstancePermissionSet InstancePermissionSet => AuthenticationContext.InstancePermissionSet!; + /// /// Initializes a new instance of the class. /// diff --git a/src/Tgstation.Server.Host/Controllers/JobController.cs b/src/Tgstation.Server.Host/Controllers/JobController.cs index 521a560a0b8..2b9f1b15f1a 100644 --- a/src/Tgstation.Server.Host/Controllers/JobController.cs +++ b/src/Tgstation.Server.Host/Controllers/JobController.cs @@ -79,7 +79,7 @@ public ValueTask Read([FromQuery] int? page, [FromQuery] int? pag .Include(x => x.StartedBy) .Include(x => x.CancelledBy) .Include(x => x.Instance) - .Where(x => x.Instance.Id == Instance.Id && !x.StoppedAt.HasValue) + .Where(x => x.Instance!.Id == Instance.Id && !x.StoppedAt.HasValue) .OrderByDescending(x => x.StartedAt))), AddJobProgressResponseTransformer, page, @@ -107,7 +107,7 @@ public ValueTask List([FromQuery] int? page, [FromQuery] int? pag .Include(x => x.StartedBy) .Include(x => x.CancelledBy) .Include(x => x.Instance) - .Where(x => x.Instance.Id == Instance.Id) + .Where(x => x.Instance!.Id == Instance.Id) .OrderByDescending(x => x.StartedAt))), AddJobProgressResponseTransformer, page, @@ -135,7 +135,7 @@ public async ValueTask Delete(long id, CancellationToken cancella .AsQueryable() .Include(x => x.StartedBy) .Include(x => x.Instance) - .Where(x => x.Id == id && x.Instance.Id == Instance.Id) + .Where(x => x.Id == id && x.Instance!.Id == Instance.Id) .FirstOrDefaultAsync(cancellationToken); if (job == default) return NotFound(); @@ -167,7 +167,7 @@ public async ValueTask GetId(long id, CancellationToken cancellat var job = await DatabaseContext .Jobs .AsQueryable() - .Where(x => x.Id == id && x.Instance.Id == Instance.Id) + .Where(x => x.Id == id && x.Instance!.Id == Instance.Id) .Include(x => x.StartedBy) .Include(x => x.CancelledBy) .Include(x => x.Instance) diff --git a/src/Tgstation.Server.Host/Controllers/README.md b/src/Tgstation.Server.Host/Controllers/README.md index e1214f91f27..66c5df1244d 100644 --- a/src/Tgstation.Server.Host/Controllers/README.md +++ b/src/Tgstation.Server.Host/Controllers/README.md @@ -7,7 +7,6 @@ Some notable exceptions: - [ApiController](./ApiController.cs) is the base class of nearly all API related controllers. It does the following: - Contains code to deny the request if the instance is not present when it should be. - Contains the `IDatabaseContext` and `ILogger` properties for child controllers. - - Returns 426 Upgrade Required if the API version in the headers are incompatible with the request. - Returns 400 Bad Request if the headers or the PUT/POST'd model is invalid. - Returns 401 If an `IAuthenticationContext` could not be created for a request. - [BridgeController](./BridgeController.cs) is a special controller accessible only from localhost and is used to receive bridge request from DreamDaemon diff --git a/src/Tgstation.Server.Host/Controllers/RepositoryController.cs b/src/Tgstation.Server.Host/Controllers/RepositoryController.cs index 37766ce0fd1..5d4d686aaf2 100644 --- a/src/Tgstation.Server.Host/Controllers/RepositoryController.cs +++ b/src/Tgstation.Server.Host/Controllers/RepositoryController.cs @@ -99,10 +99,6 @@ public async ValueTask Create([FromBody] RepositoryCreateRequest || ((model.CommitterEmail ?? model.CommitterName) != null && !userRights.HasFlag(RepositoryRights.ChangeCommitter))) return Forbid(); - #pragma warning disable CS0618 // Support for obsolete API field - model.UpdateSubmodules ??= model.RecurseSubmodules; - #pragma warning restore CS0618 - var currentModel = await DatabaseContext .RepositorySettings .AsQueryable() @@ -155,7 +151,7 @@ await jobManager.RegisterOperation( job, async (core, databaseContextFactory, paramJob, progressReporter, ct) => { - var repoManager = core.RepositoryManager; + var repoManager = core!.RepositoryManager; using var repos = await repoManager.CloneRepository( origin, cloneBranch, @@ -215,13 +211,13 @@ public async ValueTask Delete(CancellationToken cancellationToken await DatabaseContext.Save(cancellationToken); - Logger.LogInformation("Instance {instanceId} repository delete initiated by user {userId}", Instance.Id, AuthenticationContext.User.Id.Value); + Logger.LogInformation("Instance {instanceId} repository delete initiated by user {userId}", Instance.Id, AuthenticationContext.User.Require(x => x.Id)); var job = Job.Create(JobCode.RepositoryDelete, AuthenticationContext.User, Instance); var api = currentModel.ToApi(); await jobManager.RegisterOperation( job, - (core, databaseContextFactory, paramJob, progressReporter, ct) => core.RepositoryManager.DeleteRepository(ct), + (core, databaseContextFactory, paramJob, progressReporter, ct) => core!.RepositoryManager.DeleteRepository(ct), cancellationToken); api.ActiveJob = job.ToApi(); return Accepted(api); @@ -380,23 +376,23 @@ bool CheckModified(Expression var api = canRead ? currentModel.ToApi() : new RepositoryResponse(); if (canRead) { - var earlyOut = await WithComponentInstance( - async instance => - { - var repoManager = instance.RepositoryManager; - if (repoManager.CloneInProgress) - return Conflict(new ErrorMessageResponse(ErrorCode.RepoCloning)); + var earlyOut = await WithComponentInstanceNullable( + async instance => + { + var repoManager = instance.RepositoryManager; + if (repoManager.CloneInProgress) + return Conflict(new ErrorMessageResponse(ErrorCode.RepoCloning)); - if (repoManager.InUse) - return Conflict(new ErrorMessageResponse(ErrorCode.RepoBusy)); + if (repoManager.InUse) + return Conflict(new ErrorMessageResponse(ErrorCode.RepoBusy)); - using var repo = await repoManager.LoadRepository(cancellationToken); - if (repo == null) - return Conflict(new ErrorMessageResponse(ErrorCode.RepoMissing)); - await PopulateApi(api, repo, DatabaseContext, Instance, cancellationToken); + using var repo = await repoManager.LoadRepository(cancellationToken); + if (repo == null) + return Conflict(new ErrorMessageResponse(ErrorCode.RepoMissing)); + await PopulateApi(api, repo, DatabaseContext, Instance, cancellationToken); - return null; - }); + return null; + }); if (earlyOut != null) return earlyOut; @@ -406,7 +402,7 @@ bool CheckModified(Expression await DatabaseContext.Save(cancellationToken); // format the job description - string description = null; + string? description = null; if (model.UpdateFromOrigin == true) if (model.Reference != null) description = String.Format(CultureInfo.InvariantCulture, "Fetch and hard reset repository to origin/{0}", model.Reference); @@ -426,7 +422,7 @@ bool CheckModified(Expression : "T", String.Join( ", ", - model.NewTestMerges.Select( + model.NewTestMerges!.Select( x => String.Format( CultureInfo.InvariantCulture, "#{0}{1}", @@ -452,7 +448,7 @@ bool CheckModified(Expression currentModel, AuthenticationContext.User, loggerFactory.CreateLogger(), - Instance.Id.Value); + Instance.Require(x => x.Id)); // Time to access git, do it in a job await jobManager.RegisterOperation( @@ -488,16 +484,14 @@ async ValueTask PopulateApi( apiResponse.Reference = repository.Reference; // rev info stuff - Models.RevisionInformation revisionInfo = null; var needsDbUpdate = await RepositoryUpdateService.LoadRevisionInformation( repository, databaseContext, Logger, instance, null, - newRevInfo => revisionInfo = newRevInfo, + newRevInfo => apiResponse.RevisionInformation = newRevInfo.ToApi(), cancellationToken); - apiResponse.RevisionInformation = revisionInfo.ToApi(); return needsDbUpdate; } } diff --git a/src/Tgstation.Server.Host/Controllers/Results/LimitedStreamResultExecutor.cs b/src/Tgstation.Server.Host/Controllers/Results/LimitedStreamResultExecutor.cs index 3178db355b4..c3d3b7d3728 100644 --- a/src/Tgstation.Server.Host/Controllers/Results/LimitedStreamResultExecutor.cs +++ b/src/Tgstation.Server.Host/Controllers/Results/LimitedStreamResultExecutor.cs @@ -53,7 +53,7 @@ await StreamCopyOperation.CopyToAsync( } else { - stream.Seek(range.From.Value, SeekOrigin.Begin); + stream.Seek(range.From ?? 0, SeekOrigin.Begin); await StreamCopyOperation.CopyToAsync( stream, outputStream, diff --git a/src/Tgstation.Server.Host/Controllers/Results/PaginatableResult.cs b/src/Tgstation.Server.Host/Controllers/Results/PaginatableResult.cs index b936d0f3e6e..5d4f80d0237 100644 --- a/src/Tgstation.Server.Host/Controllers/Results/PaginatableResult.cs +++ b/src/Tgstation.Server.Host/Controllers/Results/PaginatableResult.cs @@ -1,4 +1,5 @@ using System; +using System.Diagnostics.CodeAnalysis; using System.Linq; using Microsoft.AspNetCore.Mvc; @@ -11,15 +12,22 @@ namespace Tgstation.Server.Host.Controllers.Results /// The of model intended to be returned. public sealed class PaginatableResult { + /// + /// Whether or not the is valid. + /// + [MemberNotNullWhen(true, nameof(Results))] + [MemberNotNullWhen(false, nameof(EarlyOut))] + public bool Valid => EarlyOut == null; + /// /// The results. /// - public IOrderedQueryable Results { get; } + public IOrderedQueryable? Results { get; } /// /// An to return immediately. /// - public IActionResult EarlyOut { get; } + public IActionResult? EarlyOut { get; } /// /// Initializes a new instance of the class. diff --git a/src/Tgstation.Server.Host/Controllers/RootController.cs b/src/Tgstation.Server.Host/Controllers/RootController.cs new file mode 100644 index 00000000000..9ae6e7e9425 --- /dev/null +++ b/src/Tgstation.Server.Host/Controllers/RootController.cs @@ -0,0 +1,140 @@ +using System; +using System.Collections.Generic; + +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Hosting; +using Microsoft.AspNetCore.Mvc; +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Options; + +using Tgstation.Server.Host.Configuration; +using Tgstation.Server.Host.Extensions; +using Tgstation.Server.Host.System; +using Tgstation.Server.Host.Utils; + +namespace Tgstation.Server.Host.Controllers +{ + /// + /// The root path . + /// + [Route("/")] + [ApiExplorerSettings(IgnoreApi = true)] + public sealed class RootController : Controller + { + /// + /// The name of the TGS logo .svg in the on Windows. + /// + const string LogoSvgWindowsName = "0176d5d8b7d307f158e0"; + + /// + /// The name of the TGS logo .svg in the on Linux. + /// + const string LogoSvgLinuxName = "b5616c99bf2052a6bbd7"; + + /// + /// The for the . + /// + readonly IAssemblyInformationProvider assemblyInformationProvider; + + /// + /// The for the . + /// + readonly IPlatformIdentifier platformIdentifier; + + /// + /// THe for the . + /// + readonly IWebHostEnvironment hostEnvironment; + + /// + /// The for the . + /// + readonly ILogger logger; + + /// + /// The for the . + /// + readonly GeneralConfiguration generalConfiguration; + + /// + /// The for the . + /// + readonly ControlPanelConfiguration controlPanelConfiguration; + + /// + /// Initializes a new instance of the class. + /// + /// The value of . + /// The value of . + /// The value of . + /// The value of . + /// The containing the value of . + /// The containing the value of . + public RootController( + IAssemblyInformationProvider assemblyInformationProvider, + IPlatformIdentifier platformIdentifier, + IWebHostEnvironment hostEnvironment, + ILogger logger, + IOptions generalConfigurationOptions, + IOptions controlPanelConfigurationOptions) + { + this.assemblyInformationProvider = assemblyInformationProvider ?? throw new ArgumentNullException(nameof(assemblyInformationProvider)); + this.platformIdentifier = platformIdentifier ?? throw new ArgumentNullException(nameof(platformIdentifier)); + this.hostEnvironment = hostEnvironment ?? throw new ArgumentNullException(nameof(hostEnvironment)); + this.logger = logger ?? throw new ArgumentNullException(nameof(logger)); + generalConfiguration = generalConfigurationOptions?.Value ?? throw new ArgumentNullException(nameof(generalConfigurationOptions)); + controlPanelConfiguration = controlPanelConfigurationOptions?.Value ?? throw new ArgumentNullException(nameof(controlPanelConfigurationOptions)); + } + + /// + /// Gets the server's homepage. + /// + /// The appropriate . + [HttpGet] + [AllowAnonymous] + public IActionResult Index() + { + const string ApiDocumentationRoute = "/" + SwaggerConfiguration.DocumentationSiteRouteExtension; + var panelEnabled = controlPanelConfiguration.Enable; + var apiDocsEnabled = generalConfiguration.HostApiDocumentation; + + if (panelEnabled ^ apiDocsEnabled) + if (panelEnabled) + return Redirect(ControlPanelController.ControlPanelRoute); + else + return Redirect(ApiDocumentationRoute); + + Dictionary? links; + if (panelEnabled) + links = new Dictionary() + { + { "Web Control Panel", ControlPanelController.ControlPanelRoute.TrimStart('/') }, + { "API Documentation", SwaggerConfiguration.DocumentationSiteRouteExtension }, + }; + else + links = null; + + var model = new + { + Links = links, + Title = assemblyInformationProvider.VersionString, + }; + + return View(model); + } + + /// + /// Retrieve the logo .svg for the webpanel. + /// + /// The appropriate . + [HttpGet("logo.svg")] + public IActionResult GetLogo() + { + var logoFileName = platformIdentifier.IsWindows // these are different because of motherfucking line endings -_- + ? LogoSvgWindowsName + : LogoSvgLinuxName; + + return (IActionResult?)this.TryServeFile(hostEnvironment, logger, $"{logoFileName}.svg") ?? NotFound(); + } + } +} diff --git a/src/Tgstation.Server.Host/Controllers/SwarmController.cs b/src/Tgstation.Server.Host/Controllers/SwarmController.cs index 15b3bae421a..be171b19d8b 100644 --- a/src/Tgstation.Server.Host/Controllers/SwarmController.cs +++ b/src/Tgstation.Server.Host/Controllers/SwarmController.cs @@ -32,7 +32,7 @@ public sealed class SwarmController : ApiControllerBase /// /// Get the current registration from the . /// - internal Guid RequestRegistrationId => Guid.Parse(Request.Headers[SwarmConstants.RegistrationIdHeader].First()); + internal Guid RequestRegistrationId => Guid.Parse(Request.Headers[SwarmConstants.RegistrationIdHeader].First()!); /// /// The for the . @@ -150,6 +150,9 @@ public IActionResult UpdateNodeList([FromBody] SwarmServersUpdateRequest servers { ArgumentNullException.ThrowIfNull(serversUpdateRequest); + if (serversUpdateRequest.SwarmServers == null) + return BadRequest(); + if (!ValidateRegistration()) return Forbid(); @@ -189,7 +192,7 @@ public async ValueTask CommitUpdate(CancellationToken cancellatio if (!ValidateRegistration()) return Forbid(); - var result = await swarmOperations.RemoteCommitRecieved(RequestRegistrationId, cancellationToken); + var result = await swarmOperations.RemoteCommitReceived(RequestRegistrationId, cancellationToken); if (!result) return Conflict(); return NoContent(); @@ -210,7 +213,7 @@ public async ValueTask AbortUpdate() } /// - protected override async ValueTask HookExecuteAction(Func executeAction, CancellationToken cancellationToken) + protected override async ValueTask HookExecuteAction(Func executeAction, CancellationToken cancellationToken) { using (LogContext.PushProperty(SerilogContextHelper.RequestPathContextProperty, $"{Request.Method} {Request.Path}")) { @@ -241,7 +244,7 @@ protected override async ValueTask HookExecuteAction(Func e if (ModelState?.IsValid == false) { var errorMessages = ModelState - .SelectMany(x => x.Value.Errors) + .SelectMany(x => x.Value!.Errors) .Select(x => x.ErrorMessage); logger.LogDebug( diff --git a/src/Tgstation.Server.Host/Controllers/UserController.cs b/src/Tgstation.Server.Host/Controllers/UserController.cs index 08c61e05fe0..68bccfa1692 100644 --- a/src/Tgstation.Server.Host/Controllers/UserController.cs +++ b/src/Tgstation.Server.Host/Controllers/UserController.cs @@ -103,7 +103,7 @@ public async ValueTask Create([FromBody] UserCreateRequest model, return BadRequest(new ErrorMessageResponse(ErrorCode.ModelValidationFailure)); if ((model.Password != null && model.SystemIdentifier != null) - || (model.Password == null && model.SystemIdentifier == null && model.OAuthConnections?.Any() != true)) + || (model.Password == null && model.SystemIdentifier == null && (model.OAuthConnections?.Count > 0) != true)) return BadRequest(new ErrorMessageResponse(ErrorCode.UserMismatchPasswordSid)); if (model.Group != null && model.PermissionSet != null) @@ -144,14 +144,14 @@ public async ValueTask Create([FromBody] UserCreateRequest model, { return RequiresPosixSystemIdentity(ex); } - else if (!(model.Password?.Length == 0 && model.OAuthConnections?.Any() == true)) + else if (!(model.Password?.Length == 0 && (model.OAuthConnections?.Count > 0) == true)) { - var result = TrySetPassword(dbUser, model.Password, true); + var result = TrySetPassword(dbUser, model.Password!, true); if (result != null) return result; } - dbUser.CanonicalName = Models.User.CanonicalizeName(dbUser.Name); + dbUser.CanonicalName = Models.User.CanonicalizeName(dbUser.Name!); DatabaseContext.Users.Add(dbUser); @@ -204,7 +204,7 @@ public async ValueTask Update([FromBody] UserUpdateRequest model, .Where(x => x.Id == model.Id) .Include(x => x.CreatedBy) .Include(x => x.OAuthConnections) - .Include(x => x.Group) + .Include(x => x.Group!) .ThenInclude(x => x.PermissionSet) .Include(x => x.PermissionSet) .FirstOrDefaultAsync(cancellationToken); @@ -254,7 +254,7 @@ public async ValueTask Update([FromBody] UserUpdateRequest model, bool userWasDisabled; if (model.Enabled.HasValue) { - userWasDisabled = originalUser.Enabled.Value && !model.Enabled.Value; + userWasDisabled = originalUser.Require(x => x.Enabled) && !model.Enabled.Value; if (userWasDisabled) originalUser.LastPasswordUpdate = DateTimeOffset.UtcNow; @@ -264,7 +264,7 @@ public async ValueTask Update([FromBody] UserUpdateRequest model, userWasDisabled = false; if (model.OAuthConnections != null - && (model.OAuthConnections.Count != originalUser.OAuthConnections.Count + && (model.OAuthConnections.Count != originalUser.OAuthConnections!.Count || !model.OAuthConnections.All(x => originalUser.OAuthConnections.Any(y => y.Provider == x.Provider && y.ExternalUserId == x.ExternalUserId)))) { if (originalUser.CanonicalName == Models.User.CanonicalizeName(DefaultCredentials.AdminUserName)) @@ -372,7 +372,7 @@ public ValueTask List([FromQuery] int? page, [FromQuery] int? pag .Include(x => x.CreatedBy) .Include(x => x.PermissionSet) .Include(x => x.OAuthConnections) - .Include(x => x.Group) + .Include(x => x.Group!) .ThenInclude(x => x.PermissionSet) .OrderBy(x => x.Id))), null, @@ -405,7 +405,7 @@ public async ValueTask GetId(long id, CancellationToken cancellat .Where(x => x.Id == id) .Include(x => x.CreatedBy) .Include(x => x.OAuthConnections) - .Include(x => x.Group) + .Include(x => x.Group!) .ThenInclude(x => x.PermissionSet) .Include(x => x.PermissionSet) .FirstOrDefaultAsync(cancellationToken); @@ -426,8 +426,8 @@ public async ValueTask GetId(long id, CancellationToken cancellat /// A resulting in a new on success, if the requested did not exist. async ValueTask CreateNewUserFromModel(Api.Models.Internal.UserApiBase model, CancellationToken cancellationToken) { - Models.PermissionSet permissionSet = null; - UserGroup group = null; + Models.PermissionSet? permissionSet = null; + UserGroup? group = null; if (model.Group != null) group = await DatabaseContext .Groups @@ -469,7 +469,7 @@ async ValueTask CreateNewUserFromModel(Api.Models.Internal.UserApiBase mod /// The to check. /// If this is a new . /// if is valid, a otherwise. - BadRequestObjectResult CheckValidName(UserUpdateRequest model, bool newUser) + BadRequestObjectResult? CheckValidName(UserUpdateRequest model, bool newUser) { var userInvalidWithNullName = newUser && model.Name == null && model.SystemIdentifier == null; if (userInvalidWithNullName || (model.Name != null && String.IsNullOrWhiteSpace(model.Name))) @@ -488,7 +488,7 @@ BadRequestObjectResult CheckValidName(UserUpdateRequest model, bool newUser) /// The new password. /// If this is for a new . /// on success, if is too short. - BadRequestObjectResult TrySetPassword(User dbUser, string newPassword, bool newUser) + BadRequestObjectResult? TrySetPassword(User dbUser, string newPassword, bool newUser) { newPassword ??= String.Empty; if (newPassword.Length < generalConfiguration.MinimumPasswordLength) diff --git a/src/Tgstation.Server.Host/Controllers/UserGroupController.cs b/src/Tgstation.Server.Host/Controllers/UserGroupController.cs index 3f2c5ea3a24..b6f48b81976 100644 --- a/src/Tgstation.Server.Host/Controllers/UserGroupController.cs +++ b/src/Tgstation.Server.Host/Controllers/UserGroupController.cs @@ -131,7 +131,7 @@ public async ValueTask Update([FromBody] UserGroupUpdateRequest m if (model.PermissionSet != null) { - currentGroup.PermissionSet.AdministrationRights = model.PermissionSet.AdministrationRights ?? currentGroup.PermissionSet.AdministrationRights; + currentGroup.PermissionSet!.AdministrationRights = model.PermissionSet.AdministrationRights ?? currentGroup.PermissionSet.AdministrationRights; currentGroup.PermissionSet.InstanceManagerRights = model.PermissionSet.InstanceManagerRights ?? currentGroup.PermissionSet.InstanceManagerRights; } @@ -139,7 +139,7 @@ public async ValueTask Update([FromBody] UserGroupUpdateRequest m await DatabaseContext.Save(cancellationToken); - if (!AuthenticationContext.PermissionSet.AdministrationRights.Value.HasFlag(AdministrationRights.ReadUsers)) + if (!AuthenticationContext.PermissionSet.AdministrationRights!.Value.HasFlag(AdministrationRights.ReadUsers)) return Json(new UserGroupResponse { Id = currentGroup.Id, @@ -220,7 +220,7 @@ public async ValueTask Delete(long id, CancellationToken cancella var numDeleted = await DatabaseContext .Groups .AsQueryable() - .Where(x => x.Id == id && x.Users.Count == 0) + .Where(x => x.Id == id && x.Users!.Count == 0) .DeleteAsync(cancellationToken); if (numDeleted > 0) diff --git a/src/Tgstation.Server.Host/Core/Application.cs b/src/Tgstation.Server.Host/Core/Application.cs index 3daa375299c..3b80d9da389 100644 --- a/src/Tgstation.Server.Host/Core/Application.cs +++ b/src/Tgstation.Server.Host/Core/Application.cs @@ -1,8 +1,7 @@ using System; +using System.Collections.Frozen; using System.Collections.Generic; using System.Globalization; -using System.IdentityModel.Tokens.Jwt; -using System.Linq; using System.Threading.Tasks; using Cyberboss.AspNetCore.AsyncInitializer; @@ -35,11 +34,13 @@ using Tgstation.Server.Api; using Tgstation.Server.Api.Hubs; +using Tgstation.Server.Api.Models; using Tgstation.Server.Common.Http; using Tgstation.Server.Host.Components; -using Tgstation.Server.Host.Components.Byond; using Tgstation.Server.Host.Components.Chat; using Tgstation.Server.Host.Components.Deployment.Remote; +using Tgstation.Server.Host.Components.Engine; +using Tgstation.Server.Host.Components.Events; using Tgstation.Server.Host.Components.Interop; using Tgstation.Server.Host.Components.Interop.Bridge; using Tgstation.Server.Host.Components.Repository; @@ -78,7 +79,7 @@ public sealed class Application : SetupApplication /// /// The for the . /// - ITokenFactory tokenFactory; + ITokenFactory? tokenFactory; /// /// Create the default . @@ -337,7 +338,8 @@ void AddTypedContext() AddWatchdog(services, postSetupServices); services.AddSingleton(); services.AddSingleton(); - services.AddSingleton(); + services.AddSingleton(); + services.AddSingleton(); services.AddSingleton(); services.AddSingleton(); @@ -350,7 +352,8 @@ void AddTypedContext() AddWatchdog(services, postSetupServices); services.AddSingleton(); services.AddSingleton(); - services.AddSingleton(); + services.AddSingleton(); + services.AddSingleton(); services.AddSingleton(); services.AddSingleton(); @@ -362,6 +365,31 @@ void AddTypedContext() services.AddHostedService(); } + // only global repo manager should be for the OD repo + var openDreamRepositoryDirectory = ioManager.ConcatPath( + Environment.GetFolderPath( + Environment.SpecialFolder.LocalApplicationData, + Environment.SpecialFolderOption.DoNotVerify), + assemblyInformationProvider.VersionPrefix, + "OpenDreamRepository"); + services.AddSingleton( + services => services + .GetRequiredService() + .CreateRepositoryManager( + new ResolvingIOManager( + services.GetRequiredService(), + openDreamRepositoryDirectory), + new NoopEventConsumer())); + + services.AddSingleton( + serviceProvider => new Dictionary + { + { EngineType.Byond, serviceProvider.GetRequiredService() }, + { EngineType.OpenDream, serviceProvider.GetRequiredService() }, + } + .ToFrozenDictionary()); + services.AddSingleton(); + if (postSetupServices.InternalConfiguration.UsingSystemD) services.AddHostedService(); @@ -383,6 +411,7 @@ void AddTypedContext() services.AddSingleton(); services.AddSingleton(); services.AddSingleton(); + services.AddSingleton(); services.AddSingleton(); services.AddChatProviderFactory(); services.AddSingleton(); @@ -475,8 +504,15 @@ public void Configure( if (generalConfiguration.HostApiDocumentation) { - applicationBuilder.UseSwagger(); - applicationBuilder.UseSwaggerUI(c => c.SwaggerEndpoint("/swagger/v1/swagger.json", "TGS API")); + applicationBuilder.UseSwagger(options => + { + options.RouteTemplate = Routes.ApiRoot + "doc/{documentName}.{json|yaml}"; + }); + applicationBuilder.UseSwaggerUI(options => + { + options.RoutePrefix = SwaggerConfiguration.DocumentationSiteRouteExtension; + options.SwaggerEndpoint(Routes.ApiRoot + $"doc/{SwaggerConfiguration.DocumentName}.json", "TGS API"); + }); logger.LogTrace("Swagger API generation enabled"); } @@ -502,7 +538,7 @@ public void Configure( applicationBuilder.UseRouting(); // Set up CORS based on configuration if necessary - Action corsBuilder = null; + Action? corsBuilder = null; if (controlPanelConfiguration.AllowAnyOrigin) { logger.LogTrace("Access-Control-Allow-Origin: *"); @@ -511,7 +547,7 @@ public void Configure( else if (controlPanelConfiguration.AllowedOrigins?.Count > 0) { logger.LogTrace("Access-Control-Allow-Origin: {allowedOrigins}", String.Join(',', controlPanelConfiguration.AllowedOrigins)); - corsBuilder = builder => builder.WithOrigins(controlPanelConfiguration.AllowedOrigins.ToArray()); + corsBuilder = builder => builder.WithOrigins([.. controlPanelConfiguration.AllowedOrigins]); } var originalBuilder = corsBuilder; @@ -586,9 +622,9 @@ void ConfigureAuthenticationPipeline(IServiceCollection services) // return provider.GetRequiredService().CurrentAuthenticationContext // But M$ said // https://stackoverflow.com/questions/56792917/scoped-services-in-asp-net-core-with-signalr-hubs - services.AddScoped(provider => provider + services.AddScoped(provider => (provider .GetRequiredService() - .HttpContext + .HttpContext ?? throw new InvalidOperationException($"Unable to resolve {nameof(IAuthenticationContext)} due to no HttpContext being available!")) .RequestServices .GetRequiredService() .CurrentAuthenticationContext); @@ -602,6 +638,7 @@ void ConfigureAuthenticationPipeline(IServiceCollection services) // this line isn't actually run until the first request is made // at that point tokenFactory will be populated jwtBearerOptions.TokenValidationParameters = tokenFactory?.ValidationParameters ?? throw new InvalidOperationException("tokenFactory not initialized!"); + jwtBearerOptions.MapInboundClaims = false; jwtBearerOptions.Events = new JwtBearerEvents { OnMessageReceived = context => @@ -622,11 +659,6 @@ void ConfigureAuthenticationPipeline(IServiceCollection services) }, }; }); - - // WARNING: STATIC CODE - // fucking prevents converting 'sub' to M$ bs - // can't be done in the above lambda, that's too late - JwtSecurityTokenHandler.DefaultInboundClaimTypeMap.Clear(); } } } diff --git a/src/Tgstation.Server.Host/Core/CommandPipeManager.cs b/src/Tgstation.Server.Host/Core/CommandPipeManager.cs index cd06a7a0738..6a5c6325c62 100644 --- a/src/Tgstation.Server.Host/Core/CommandPipeManager.cs +++ b/src/Tgstation.Server.Host/Core/CommandPipeManager.cs @@ -65,22 +65,24 @@ protected override async Task ExecuteAsync(CancellationToken cancellationToken) logger.LogTrace("Starting..."); // grab both pipes asap so we can close them on error - var supportsPipeCommands = !String.IsNullOrWhiteSpace(internalConfiguration.CommandPipe); + var commandPipe = internalConfiguration.CommandPipe; + var supportsPipeCommands = !String.IsNullOrWhiteSpace(commandPipe); await using var commandPipeClient = supportsPipeCommands ? new AnonymousPipeClientStream( PipeDirection.In, - internalConfiguration.CommandPipe) + commandPipe!) : null; if (!supportsPipeCommands) logger.LogDebug("No command pipe name specified in configuration"); - var supportsReadyNotification = !String.IsNullOrWhiteSpace(internalConfiguration.ReadyPipe); + var readyPipe = internalConfiguration.ReadyPipe; + var supportsReadyNotification = !String.IsNullOrWhiteSpace(readyPipe); if (supportsReadyNotification) { await using var readyPipeClient = new AnonymousPipeClientStream( PipeDirection.Out, - internalConfiguration.ReadyPipe); + readyPipe!); logger.LogTrace("Waiting to send ready notification..."); await instanceManager.Ready.WaitAsync(cancellationToken); @@ -96,13 +98,13 @@ protected override async Task ExecuteAsync(CancellationToken cancellationToken) try { - using var streamReader = new StreamReader(commandPipeClient, Encoding.UTF8, leaveOpen: true); + using var streamReader = new StreamReader(commandPipeClient!, Encoding.UTF8, leaveOpen: true); while (!cancellationToken.IsCancellationRequested) { logger.LogTrace("Waiting to read command line..."); - var line = await streamReader.ReadLineAsync().WaitAsync(cancellationToken); + var line = await streamReader.ReadLineAsync(cancellationToken); - logger?.LogInformation("Received pipe command: {command}", line); + logger.LogInformation("Received pipe command: {command}", line); switch (line) { case PipeCommands.CommandStop: @@ -118,22 +120,22 @@ protected override async Task ExecuteAsync(CancellationToken cancellationToken) logger.LogError("Read null from pipe!"); return; default: - logger?.LogWarning("Unrecognized pipe command: {command}", line); + logger.LogWarning("Unrecognized pipe command: {command}", line); break; } } } catch (OperationCanceledException ex) { - logger?.LogTrace(ex, "Command read task cancelled!"); + logger.LogTrace(ex, "Command read task cancelled!"); } catch (Exception ex) { - logger?.LogError(ex, "Command read task errored!"); + logger.LogError(ex, "Command read task errored!"); } finally { - logger?.LogTrace("Command read task exiting..."); + logger.LogTrace("Command read task exiting..."); } } } diff --git a/src/Tgstation.Server.Host/Core/IRestartHandler.cs b/src/Tgstation.Server.Host/Core/IRestartHandler.cs index e1c8a771ef5..b663ae5d22c 100644 --- a/src/Tgstation.Server.Host/Core/IRestartHandler.cs +++ b/src/Tgstation.Server.Host/Core/IRestartHandler.cs @@ -16,6 +16,6 @@ public interface IRestartHandler /// If the should aim to complete the returned from this function ASAP. /// The for the operation. /// A representing the running operation. - ValueTask HandleRestart(Version updateVersion, bool handlerMayDelayShutdownWithExtremelyLongRunningTasks, CancellationToken cancellationToken); + ValueTask HandleRestart(Version? updateVersion, bool handlerMayDelayShutdownWithExtremelyLongRunningTasks, CancellationToken cancellationToken); } } diff --git a/src/Tgstation.Server.Host/Core/IServerControl.cs b/src/Tgstation.Server.Host/Core/IServerControl.cs index d864080db69..92bbfe75f50 100644 --- a/src/Tgstation.Server.Host/Core/IServerControl.cs +++ b/src/Tgstation.Server.Host/Core/IServerControl.cs @@ -51,6 +51,6 @@ public interface IServerControl /// /// The to propagate to the watchdog if any. /// A representing the running operation. - ValueTask Die(Exception exception); + ValueTask Die(Exception? exception); } } diff --git a/src/Tgstation.Server.Host/Core/IServerUpdateInitiator.cs b/src/Tgstation.Server.Host/Core/IServerUpdateInitiator.cs index 8b290bdcd2c..35aaa4037b9 100644 --- a/src/Tgstation.Server.Host/Core/IServerUpdateInitiator.cs +++ b/src/Tgstation.Server.Host/Core/IServerUpdateInitiator.cs @@ -18,6 +18,6 @@ public interface IServerUpdateInitiator /// The TGS to update to. /// The for the operation. /// A resulting in the . - ValueTask InitiateUpdate(IFileStreamProvider fileStreamProvider, Version version, CancellationToken cancellationToken); + ValueTask InitiateUpdate(IFileStreamProvider? fileStreamProvider, Version version, CancellationToken cancellationToken); } } diff --git a/src/Tgstation.Server.Host/Core/IServerUpdater.cs b/src/Tgstation.Server.Host/Core/IServerUpdater.cs index 6485ed47134..4a7a7c0519a 100644 --- a/src/Tgstation.Server.Host/Core/IServerUpdater.cs +++ b/src/Tgstation.Server.Host/Core/IServerUpdater.cs @@ -20,6 +20,6 @@ interface IServerUpdater /// The TGS to update to. /// The for the operation. /// A resulting in the . - ValueTask BeginUpdate(ISwarmService swarmService, IFileStreamProvider fileStreamProvider, Version version, CancellationToken cancellationToken); + ValueTask BeginUpdate(ISwarmService swarmService, IFileStreamProvider? fileStreamProvider, Version version, CancellationToken cancellationToken); } } diff --git a/src/Tgstation.Server.Host/Core/RestartRegistration.cs b/src/Tgstation.Server.Host/Core/RestartRegistration.cs index 025c274e075..2cd24e307de 100644 --- a/src/Tgstation.Server.Host/Core/RestartRegistration.cs +++ b/src/Tgstation.Server.Host/Core/RestartRegistration.cs @@ -1,4 +1,4 @@ -using System; +using Tgstation.Server.Host.Utils; namespace Tgstation.Server.Host.Core { @@ -6,20 +6,20 @@ namespace Tgstation.Server.Host.Core sealed class RestartRegistration : IRestartRegistration { /// - /// The . + /// The . /// - readonly Action onDispose; + readonly DisposeInvoker? disposeInvoker; /// /// Initializes a new instance of the class. /// - /// The value of . - public RestartRegistration(Action onDispose) + /// The value of . + public RestartRegistration(DisposeInvoker? disposeInvoker) { - this.onDispose = onDispose; + this.disposeInvoker = disposeInvoker; } /// - public void Dispose() => onDispose?.Invoke(); + public void Dispose() => disposeInvoker?.Dispose(); } } diff --git a/src/Tgstation.Server.Host/Core/ServerPortProivder.cs b/src/Tgstation.Server.Host/Core/ServerPortProivder.cs index e4c4927c534..a3f39a4ab82 100644 --- a/src/Tgstation.Server.Host/Core/ServerPortProivder.cs +++ b/src/Tgstation.Server.Host/Core/ServerPortProivder.cs @@ -34,18 +34,17 @@ public ServerPortProivder( generalConfiguration = generalConfigurationOptions?.Value ?? throw new ArgumentNullException(nameof(generalConfigurationOptions)); ArgumentNullException.ThrowIfNull(configuration); + var usingDefaultPort = generalConfiguration.ApiPort == default; + if (!usingDefaultPort) + return; + var httpEndpoint = configuration .GetSection("Kestrel") .GetSection("EndPoints") .GetSection("Http") .GetSection("Url") - .Value; - - if (generalConfiguration.ApiPort == default && httpEndpoint == null) - throw new InvalidOperationException("Missing required configuration option General:ApiPort!"); - - if (generalConfiguration.ApiPort != default) - return; + .Value + ?? throw new InvalidOperationException("Missing required configuration option General:ApiPort!"); logger.LogWarning("The \"Kestrel\" configuration section is deprecated! Please set your API port using the \"General:ApiPort\" configuration option!"); diff --git a/src/Tgstation.Server.Host/Core/ServerUpdateInitiator.cs b/src/Tgstation.Server.Host/Core/ServerUpdateInitiator.cs index f7fad731229..fc1dc084cc7 100644 --- a/src/Tgstation.Server.Host/Core/ServerUpdateInitiator.cs +++ b/src/Tgstation.Server.Host/Core/ServerUpdateInitiator.cs @@ -32,7 +32,7 @@ public ServerUpdateInitiator(ISwarmService swarmService, IServerUpdater serverUp } /// - public ValueTask InitiateUpdate(IFileStreamProvider fileStreamProvider, Version version, CancellationToken cancellationToken) + public ValueTask InitiateUpdate(IFileStreamProvider? fileStreamProvider, Version version, CancellationToken cancellationToken) => serverUpdater.BeginUpdate(swarmService, fileStreamProvider, version, cancellationToken); } } diff --git a/src/Tgstation.Server.Host/Core/ServerUpdater.cs b/src/Tgstation.Server.Host/Core/ServerUpdater.cs index aacf44fd193..8c9d9681687 100644 --- a/src/Tgstation.Server.Host/Core/ServerUpdater.cs +++ b/src/Tgstation.Server.Host/Core/ServerUpdater.cs @@ -59,7 +59,7 @@ sealed class ServerUpdater : IServerUpdater, IServerUpdateExecutor /// /// for an in-progress update operation. /// - ServerUpdateOperation serverUpdateOperation; + ServerUpdateOperation? serverUpdateOperation; /// /// Initializes a new instance of the class. @@ -92,7 +92,7 @@ public ServerUpdater( } /// - public async ValueTask BeginUpdate(ISwarmService swarmService, IFileStreamProvider fileStreamProvider, Version version, CancellationToken cancellationToken) + public async ValueTask BeginUpdate(ISwarmService swarmService, IFileStreamProvider? fileStreamProvider, Version version, CancellationToken cancellationToken) { ArgumentNullException.ThrowIfNull(swarmService); @@ -201,7 +201,7 @@ async ValueTask TryAbort(Exception exception) { try { - await serverUpdateOperation.SwarmService.AbortUpdate(); + await serverUpdateOperation!.SwarmService.AbortUpdate(); } catch (Exception e2) { @@ -214,11 +214,11 @@ async ValueTask TryAbort(Exception exception) /// /// The directory the server update is initially extracted to. /// The for the operation. - /// A resulting in containing a new based on the of and if it needs to be kept active until the swarm commit. + /// A resulting in containing a new based on the of and if it needs to be kept active until the swarm commit. If , the update failed to prepare. /// Requires to be populated. - async ValueTask> PrepareUpdateClearStagingAndBufferStream(string stagingDirectory, CancellationToken cancellationToken) + async ValueTask?> PrepareUpdateClearStagingAndBufferStream(string stagingDirectory, CancellationToken cancellationToken) { - await using var fileStreamProvider = serverUpdateOperation.FileStreamProvider; + await using var fileStreamProvider = serverUpdateOperation!.FileStreamProvider; var bufferedStream = new BufferedFileStreamProvider( await fileStreamProvider.GetResult(cancellationToken)); @@ -276,12 +276,12 @@ async ValueTask> PrepareUpdateClearStagi /// A resulting in the . async ValueTask BeginUpdateImpl( ISwarmService swarmService, - IFileStreamProvider fileStreamProvider, + IFileStreamProvider? fileStreamProvider, Version newVersion, bool recursed, CancellationToken cancellationToken) { - ServerUpdateOperation ourUpdateOperation = null; + ServerUpdateOperation? ourUpdateOperation = null; try { if (fileStreamProvider == null) diff --git a/src/Tgstation.Server.Host/Database/DatabaseContext.cs b/src/Tgstation.Server.Host/Database/DatabaseContext.cs index 51e10a4e4b0..978a73076bc 100644 --- a/src/Tgstation.Server.Host/Database/DatabaseContext.cs +++ b/src/Tgstation.Server.Host/Database/DatabaseContext.cs @@ -245,11 +245,8 @@ public static Action GetConfigur const string ConfigureMethodName = nameof(SqlServerDatabaseContext.ConfigureWith); var configureFunction = typeof(TDatabaseContext).GetMethod( ConfigureMethodName, - BindingFlags.Public | BindingFlags.Static); - - if (configureFunction == null) - throw new InvalidOperationException($"Context type {typeof(TDatabaseContext).FullName} missing static {ConfigureMethodName} function!"); - + BindingFlags.Public | BindingFlags.Static) + ?? throw new InvalidOperationException($"Context type {typeof(TDatabaseContext).FullName} missing static {ConfigureMethodName} function!"); return (optionsBuilder, config) => configureFunction.Invoke(null, new object[] { optionsBuilder, config }); } @@ -260,21 +257,21 @@ public static Action GetConfigur protected DatabaseContext(DbContextOptions dbContextOptions) : base(dbContextOptions) { - usersCollection = new DatabaseCollection(Users); - instancesCollection = new DatabaseCollection(Instances); - instancePermissionSetsCollection = new DatabaseCollection(InstancePermissionSets); - compileJobsCollection = new DatabaseCollection(CompileJobs); - repositorySettingsCollection = new DatabaseCollection(RepositorySettings); - dreamMakerSettingsCollection = new DatabaseCollection(DreamMakerSettings); - dreamDaemonSettingsCollection = new DatabaseCollection(DreamDaemonSettings); - chatBotsCollection = new DatabaseCollection(ChatBots); - chatChannelsCollection = new DatabaseCollection(ChatChannels); - revisionInformationsCollection = new DatabaseCollection(RevisionInformations); - jobsCollection = new DatabaseCollection(Jobs); - reattachInformationsCollection = new DatabaseCollection(ReattachInformations); - oAuthConnections = new DatabaseCollection(OAuthConnections); - groups = new DatabaseCollection(Groups); - permissionSets = new DatabaseCollection(PermissionSets); + usersCollection = new DatabaseCollection(Users!); + instancesCollection = new DatabaseCollection(Instances!); + instancePermissionSetsCollection = new DatabaseCollection(InstancePermissionSets!); + compileJobsCollection = new DatabaseCollection(CompileJobs!); + repositorySettingsCollection = new DatabaseCollection(RepositorySettings!); + dreamMakerSettingsCollection = new DatabaseCollection(DreamMakerSettings!); + dreamDaemonSettingsCollection = new DatabaseCollection(DreamDaemonSettings!); + chatBotsCollection = new DatabaseCollection(ChatBots!); + chatChannelsCollection = new DatabaseCollection(ChatChannels!); + revisionInformationsCollection = new DatabaseCollection(RevisionInformations!); + jobsCollection = new DatabaseCollection(Jobs!); + reattachInformationsCollection = new DatabaseCollection(ReattachInformations!); + oAuthConnections = new DatabaseCollection(OAuthConnections!); + groups = new DatabaseCollection(Groups!); + permissionSets = new DatabaseCollection(PermissionSets!); } /// @@ -378,22 +375,22 @@ protected override void OnModelCreating(ModelBuilder modelBuilder) /// /// Used by unit tests to remind us to setup the correct MSSQL migration downgrades. /// - internal static readonly Type MSLatestMigration = typeof(MSAddJobCodes); + internal static readonly Type MSLatestMigration = typeof(MSAddTopicPort); /// /// Used by unit tests to remind us to setup the correct MYSQL migration downgrades. /// - internal static readonly Type MYLatestMigration = typeof(MYAddJobCodes); + internal static readonly Type MYLatestMigration = typeof(MYAddTopicPort); /// /// Used by unit tests to remind us to setup the correct PostgresSQL migration downgrades. /// - internal static readonly Type PGLatestMigration = typeof(PGAddJobCodes); + internal static readonly Type PGLatestMigration = typeof(PGAddTopicPort); /// /// Used by unit tests to remind us to setup the correct SQLite migration downgrades. /// - internal static readonly Type SLLatestMigration = typeof(SLAddJobCodes); + internal static readonly Type SLLatestMigration = typeof(SLAddTopicPort); /// #pragma warning disable CA1502 // Cyclomatic complexity @@ -418,10 +415,19 @@ public async ValueTask SchemaDowngradeForServerVersion( throw new NotSupportedException("Cannot migrate below version 4.1.0!"); // Update this with new migrations as they are made - string targetMigration = null; + string? targetMigration = null; string BadDatabaseType() => throw new ArgumentException($"Invalid DatabaseType: {currentDatabaseType}", nameof(currentDatabaseType)); + if (targetVersion < new Version(6, 0, 0)) + targetMigration = currentDatabaseType switch + { + DatabaseType.MySql => nameof(MYAddJobCodes), + DatabaseType.PostgresSql => nameof(PGAddJobCodes), + DatabaseType.SqlServer => nameof(MSAddJobCodes), + DatabaseType.Sqlite => nameof(SLAddJobCodes), + _ => BadDatabaseType(), + }; if (targetVersion < new Version(5, 17, 0)) targetMigration = currentDatabaseType switch { diff --git a/src/Tgstation.Server.Host/Database/DatabaseSeeder.cs b/src/Tgstation.Server.Host/Database/DatabaseSeeder.cs index 19efc59b471..c88c7a5e0d3 100644 --- a/src/Tgstation.Server.Host/Database/DatabaseSeeder.cs +++ b/src/Tgstation.Server.Host/Database/DatabaseSeeder.cs @@ -57,7 +57,7 @@ sealed class DatabaseSeeder : IDatabaseSeeder /// The to add a system to. /// An existing , if any. /// The created system . - static User SeedSystemUser(IDatabaseContext databaseContext, User tgsUser = null) + static User SeedSystemUser(IDatabaseContext databaseContext, User? tgsUser = null) { bool alreadyExists = tgsUser != null; tgsUser ??= new User() @@ -231,7 +231,7 @@ async ValueTask SanitizeDatabase(IDatabaseContext databaseContext, CancellationT .AsQueryable() .ToListAsync(cancellationToken); foreach (var instance in allInstances) - instance.Path = instance.Path.Replace('\\', '/'); + instance.Path = instance.Path!.Replace('\\', '/'); } if (generalConfiguration.ByondTopicTimeout != 0) @@ -303,7 +303,7 @@ async ValueTask ResetAdminPassword(IDatabaseContext databaseContext, Cancellatio /// The to use. /// The for the operation. /// A resulting in the admin or . If , must be called on . - async ValueTask GetAdminUser(IDatabaseContext databaseContext, CancellationToken cancellationToken) + async ValueTask GetAdminUser(IDatabaseContext databaseContext, CancellationToken cancellationToken) { var admin = await databaseContext .Users diff --git a/src/Tgstation.Server.Host/Database/Design/DesignTimeDbContextFactoryHelpers.cs b/src/Tgstation.Server.Host/Database/Design/DesignTimeDbContextFactoryHelpers.cs index 54d79bddc35..126dfbef4a5 100644 --- a/src/Tgstation.Server.Host/Database/Design/DesignTimeDbContextFactoryHelpers.cs +++ b/src/Tgstation.Server.Host/Database/Design/DesignTimeDbContextFactoryHelpers.cs @@ -21,7 +21,7 @@ static class DesignTimeDbContextFactoryHelpers public static DbContextOptions CreateDatabaseContextOptions( DatabaseType databaseType, string connectionString, - string serverVersion = null) + string? serverVersion = null) where TDatabaseContext : DatabaseContext { var dbConfig = new DatabaseConfiguration diff --git a/src/Tgstation.Server.Host/Database/Design/SqlServerDesignTimeDbContextFactory.cs b/src/Tgstation.Server.Host/Database/Design/SqlServerDesignTimeDbContextFactory.cs index bf137fcf4bc..9b5623725e1 100644 --- a/src/Tgstation.Server.Host/Database/Design/SqlServerDesignTimeDbContextFactory.cs +++ b/src/Tgstation.Server.Host/Database/Design/SqlServerDesignTimeDbContextFactory.cs @@ -14,6 +14,6 @@ public SqlServerDatabaseContext CreateDbContext(string[] args) => new SqlServerDatabaseContext( DesignTimeDbContextFactoryHelpers.CreateDatabaseContextOptions( DatabaseType.SqlServer, - "Data Source=fake;Initial Catalog=TGS_Design;Integrated Security=True;Application Name=tgstation-server")); + "Data Source=fake;Initial Catalog=TGS_Design;Integrated Security=True;Encrypt=false;Application Name=tgstation-server")); } } diff --git a/src/Tgstation.Server.Host/Database/Migrations/20230331220749_MSAddDreamDaemonLogOutput.Designer.cs b/src/Tgstation.Server.Host/Database/Migrations/20230331220749_MSAddDreamDaemonLogOutput.Designer.cs index 02be0acad16..8bc0ff72d47 100644 --- a/src/Tgstation.Server.Host/Database/Migrations/20230331220749_MSAddDreamDaemonLogOutput.Designer.cs +++ b/src/Tgstation.Server.Host/Database/Migrations/20230331220749_MSAddDreamDaemonLogOutput.Designer.cs @@ -5,8 +5,6 @@ using Microsoft.EntityFrameworkCore.Infrastructure; using Microsoft.EntityFrameworkCore.Migrations; -#nullable disable - namespace Tgstation.Server.Host.Database.Migrations { [DbContext(typeof(SqlServerDatabaseContext))] diff --git a/src/Tgstation.Server.Host/Database/Migrations/20230331220749_MSAddDreamDaemonLogOutput.cs b/src/Tgstation.Server.Host/Database/Migrations/20230331220749_MSAddDreamDaemonLogOutput.cs index bc8e3d1880f..b2d86c3ae76 100644 --- a/src/Tgstation.Server.Host/Database/Migrations/20230331220749_MSAddDreamDaemonLogOutput.cs +++ b/src/Tgstation.Server.Host/Database/Migrations/20230331220749_MSAddDreamDaemonLogOutput.cs @@ -2,8 +2,6 @@ using Microsoft.EntityFrameworkCore.Migrations; -#nullable disable - namespace Tgstation.Server.Host.Database.Migrations { /// diff --git a/src/Tgstation.Server.Host/Database/Migrations/20230331221032_PGAddDreamDaemonLogOutput.Designer.cs b/src/Tgstation.Server.Host/Database/Migrations/20230331221032_PGAddDreamDaemonLogOutput.Designer.cs index c1bb3e23c5f..adca6dbfa74 100644 --- a/src/Tgstation.Server.Host/Database/Migrations/20230331221032_PGAddDreamDaemonLogOutput.Designer.cs +++ b/src/Tgstation.Server.Host/Database/Migrations/20230331221032_PGAddDreamDaemonLogOutput.Designer.cs @@ -5,8 +5,6 @@ using Microsoft.EntityFrameworkCore.Infrastructure; using Microsoft.EntityFrameworkCore.Migrations; -#nullable disable - namespace Tgstation.Server.Host.Database.Migrations { [DbContext(typeof(PostgresSqlDatabaseContext))] diff --git a/src/Tgstation.Server.Host/Database/Migrations/20230331221032_PGAddDreamDaemonLogOutput.cs b/src/Tgstation.Server.Host/Database/Migrations/20230331221032_PGAddDreamDaemonLogOutput.cs index 1828f867bba..cded945a221 100644 --- a/src/Tgstation.Server.Host/Database/Migrations/20230331221032_PGAddDreamDaemonLogOutput.cs +++ b/src/Tgstation.Server.Host/Database/Migrations/20230331221032_PGAddDreamDaemonLogOutput.cs @@ -2,8 +2,6 @@ using Microsoft.EntityFrameworkCore.Migrations; -#nullable disable - namespace Tgstation.Server.Host.Database.Migrations { /// diff --git a/src/Tgstation.Server.Host/Database/Migrations/20230331221156_SLAddDreamDaemonLogOutput.Designer.cs b/src/Tgstation.Server.Host/Database/Migrations/20230331221156_SLAddDreamDaemonLogOutput.Designer.cs index 1e7c30621fb..2610389f7e0 100644 --- a/src/Tgstation.Server.Host/Database/Migrations/20230331221156_SLAddDreamDaemonLogOutput.Designer.cs +++ b/src/Tgstation.Server.Host/Database/Migrations/20230331221156_SLAddDreamDaemonLogOutput.Designer.cs @@ -5,8 +5,6 @@ using Microsoft.EntityFrameworkCore.Infrastructure; using Microsoft.EntityFrameworkCore.Migrations; -#nullable disable - namespace Tgstation.Server.Host.Database.Migrations { [DbContext(typeof(SqliteDatabaseContext))] diff --git a/src/Tgstation.Server.Host/Database/Migrations/20230331221156_SLAddDreamDaemonLogOutput.cs b/src/Tgstation.Server.Host/Database/Migrations/20230331221156_SLAddDreamDaemonLogOutput.cs index 587dbf9aa24..f574ac0195a 100644 --- a/src/Tgstation.Server.Host/Database/Migrations/20230331221156_SLAddDreamDaemonLogOutput.cs +++ b/src/Tgstation.Server.Host/Database/Migrations/20230331221156_SLAddDreamDaemonLogOutput.cs @@ -2,8 +2,6 @@ using Microsoft.EntityFrameworkCore.Migrations; -#nullable disable - namespace Tgstation.Server.Host.Database.Migrations { /// diff --git a/src/Tgstation.Server.Host/Database/Migrations/20230401210715_MYAddDreamDaemonLogOutput.Designer.cs b/src/Tgstation.Server.Host/Database/Migrations/20230401210715_MYAddDreamDaemonLogOutput.Designer.cs index 69acf576b12..aa0b1d70c11 100644 --- a/src/Tgstation.Server.Host/Database/Migrations/20230401210715_MYAddDreamDaemonLogOutput.Designer.cs +++ b/src/Tgstation.Server.Host/Database/Migrations/20230401210715_MYAddDreamDaemonLogOutput.Designer.cs @@ -5,8 +5,6 @@ using Microsoft.EntityFrameworkCore.Infrastructure; using Microsoft.EntityFrameworkCore.Migrations; -#nullable disable - namespace Tgstation.Server.Host.Database.Migrations { [DbContext(typeof(MySqlDatabaseContext))] diff --git a/src/Tgstation.Server.Host/Database/Migrations/20230401210715_MYAddDreamDaemonLogOutput.cs b/src/Tgstation.Server.Host/Database/Migrations/20230401210715_MYAddDreamDaemonLogOutput.cs index 2b023f5fa24..18fc203d23f 100644 --- a/src/Tgstation.Server.Host/Database/Migrations/20230401210715_MYAddDreamDaemonLogOutput.cs +++ b/src/Tgstation.Server.Host/Database/Migrations/20230401210715_MYAddDreamDaemonLogOutput.cs @@ -2,8 +2,6 @@ using Microsoft.EntityFrameworkCore.Migrations; -#nullable disable - namespace Tgstation.Server.Host.Database.Migrations { /// diff --git a/src/Tgstation.Server.Host/Database/Migrations/20230403050623_MSAddReattachInfoInitialCompileJob.Designer.cs b/src/Tgstation.Server.Host/Database/Migrations/20230403050623_MSAddReattachInfoInitialCompileJob.Designer.cs index f7499925800..d1c93465d63 100644 --- a/src/Tgstation.Server.Host/Database/Migrations/20230403050623_MSAddReattachInfoInitialCompileJob.Designer.cs +++ b/src/Tgstation.Server.Host/Database/Migrations/20230403050623_MSAddReattachInfoInitialCompileJob.Designer.cs @@ -5,8 +5,6 @@ using Microsoft.EntityFrameworkCore.Infrastructure; using Microsoft.EntityFrameworkCore.Migrations; -#nullable disable - namespace Tgstation.Server.Host.Database.Migrations { [DbContext(typeof(SqlServerDatabaseContext))] diff --git a/src/Tgstation.Server.Host/Database/Migrations/20230403050623_MSAddReattachInfoInitialCompileJob.cs b/src/Tgstation.Server.Host/Database/Migrations/20230403050623_MSAddReattachInfoInitialCompileJob.cs index 2c378335d75..2fa00301433 100644 --- a/src/Tgstation.Server.Host/Database/Migrations/20230403050623_MSAddReattachInfoInitialCompileJob.cs +++ b/src/Tgstation.Server.Host/Database/Migrations/20230403050623_MSAddReattachInfoInitialCompileJob.cs @@ -2,8 +2,6 @@ using Microsoft.EntityFrameworkCore.Migrations; -#nullable disable - namespace Tgstation.Server.Host.Database.Migrations { /// diff --git a/src/Tgstation.Server.Host/Database/Migrations/20230403050737_MYAddReattachInfoInitialCompileJob.Designer.cs b/src/Tgstation.Server.Host/Database/Migrations/20230403050737_MYAddReattachInfoInitialCompileJob.Designer.cs index a53708cfc02..e5a7f0400b8 100644 --- a/src/Tgstation.Server.Host/Database/Migrations/20230403050737_MYAddReattachInfoInitialCompileJob.Designer.cs +++ b/src/Tgstation.Server.Host/Database/Migrations/20230403050737_MYAddReattachInfoInitialCompileJob.Designer.cs @@ -5,8 +5,6 @@ using Microsoft.EntityFrameworkCore.Infrastructure; using Microsoft.EntityFrameworkCore.Migrations; -#nullable disable - namespace Tgstation.Server.Host.Database.Migrations { [DbContext(typeof(MySqlDatabaseContext))] diff --git a/src/Tgstation.Server.Host/Database/Migrations/20230403050737_MYAddReattachInfoInitialCompileJob.cs b/src/Tgstation.Server.Host/Database/Migrations/20230403050737_MYAddReattachInfoInitialCompileJob.cs index da962107e12..3dc01170b72 100644 --- a/src/Tgstation.Server.Host/Database/Migrations/20230403050737_MYAddReattachInfoInitialCompileJob.cs +++ b/src/Tgstation.Server.Host/Database/Migrations/20230403050737_MYAddReattachInfoInitialCompileJob.cs @@ -2,8 +2,6 @@ using Microsoft.EntityFrameworkCore.Migrations; -#nullable disable - namespace Tgstation.Server.Host.Database.Migrations { /// diff --git a/src/Tgstation.Server.Host/Database/Migrations/20230403050832_PGAddReattachInfoInitialCompileJob.Designer.cs b/src/Tgstation.Server.Host/Database/Migrations/20230403050832_PGAddReattachInfoInitialCompileJob.Designer.cs index 8e4962a1e08..3129b863f99 100644 --- a/src/Tgstation.Server.Host/Database/Migrations/20230403050832_PGAddReattachInfoInitialCompileJob.Designer.cs +++ b/src/Tgstation.Server.Host/Database/Migrations/20230403050832_PGAddReattachInfoInitialCompileJob.Designer.cs @@ -5,8 +5,6 @@ using Microsoft.EntityFrameworkCore.Infrastructure; using Microsoft.EntityFrameworkCore.Migrations; -#nullable disable - namespace Tgstation.Server.Host.Database.Migrations { [DbContext(typeof(PostgresSqlDatabaseContext))] diff --git a/src/Tgstation.Server.Host/Database/Migrations/20230403050832_PGAddReattachInfoInitialCompileJob.cs b/src/Tgstation.Server.Host/Database/Migrations/20230403050832_PGAddReattachInfoInitialCompileJob.cs index 14d08913ec0..bab6bbc9c1e 100644 --- a/src/Tgstation.Server.Host/Database/Migrations/20230403050832_PGAddReattachInfoInitialCompileJob.cs +++ b/src/Tgstation.Server.Host/Database/Migrations/20230403050832_PGAddReattachInfoInitialCompileJob.cs @@ -2,8 +2,6 @@ using Microsoft.EntityFrameworkCore.Migrations; -#nullable disable - namespace Tgstation.Server.Host.Database.Migrations { /// diff --git a/src/Tgstation.Server.Host/Database/Migrations/20230403050941_SLAddReattachInfoInitialCompileJob.Designer.cs b/src/Tgstation.Server.Host/Database/Migrations/20230403050941_SLAddReattachInfoInitialCompileJob.Designer.cs index 8330e52cb1c..3ac89843099 100644 --- a/src/Tgstation.Server.Host/Database/Migrations/20230403050941_SLAddReattachInfoInitialCompileJob.Designer.cs +++ b/src/Tgstation.Server.Host/Database/Migrations/20230403050941_SLAddReattachInfoInitialCompileJob.Designer.cs @@ -5,8 +5,6 @@ using Microsoft.EntityFrameworkCore.Infrastructure; using Microsoft.EntityFrameworkCore.Migrations; -#nullable disable - namespace Tgstation.Server.Host.Database.Migrations { [DbContext(typeof(SqliteDatabaseContext))] diff --git a/src/Tgstation.Server.Host/Database/Migrations/20230403050941_SLAddReattachInfoInitialCompileJob.cs b/src/Tgstation.Server.Host/Database/Migrations/20230403050941_SLAddReattachInfoInitialCompileJob.cs index 630411174d5..91f1ea6bb8e 100644 --- a/src/Tgstation.Server.Host/Database/Migrations/20230403050941_SLAddReattachInfoInitialCompileJob.cs +++ b/src/Tgstation.Server.Host/Database/Migrations/20230403050941_SLAddReattachInfoInitialCompileJob.cs @@ -2,8 +2,6 @@ using Microsoft.EntityFrameworkCore.Migrations; -#nullable disable - namespace Tgstation.Server.Host.Database.Migrations { /// diff --git a/src/Tgstation.Server.Host/Database/Migrations/20230520203236_MSAddSystemChannels.Designer.cs b/src/Tgstation.Server.Host/Database/Migrations/20230520203236_MSAddSystemChannels.Designer.cs index 4fa9364f35f..23b628d9c88 100644 --- a/src/Tgstation.Server.Host/Database/Migrations/20230520203236_MSAddSystemChannels.Designer.cs +++ b/src/Tgstation.Server.Host/Database/Migrations/20230520203236_MSAddSystemChannels.Designer.cs @@ -5,8 +5,6 @@ using Microsoft.EntityFrameworkCore.Infrastructure; using Microsoft.EntityFrameworkCore.Migrations; -#nullable disable - namespace Tgstation.Server.Host.Database.Migrations { [DbContext(typeof(SqlServerDatabaseContext))] diff --git a/src/Tgstation.Server.Host/Database/Migrations/20230520203236_MSAddSystemChannels.cs b/src/Tgstation.Server.Host/Database/Migrations/20230520203236_MSAddSystemChannels.cs index 410d150c1e1..3ab2299fe55 100644 --- a/src/Tgstation.Server.Host/Database/Migrations/20230520203236_MSAddSystemChannels.cs +++ b/src/Tgstation.Server.Host/Database/Migrations/20230520203236_MSAddSystemChannels.cs @@ -2,8 +2,6 @@ using Microsoft.EntityFrameworkCore.Migrations; -#nullable disable - namespace Tgstation.Server.Host.Database.Migrations { /// diff --git a/src/Tgstation.Server.Host/Database/Migrations/20230520203305_MYAddSystemChannels.Designer.cs b/src/Tgstation.Server.Host/Database/Migrations/20230520203305_MYAddSystemChannels.Designer.cs index 3f1e7b32494..2a7dbd32d8a 100644 --- a/src/Tgstation.Server.Host/Database/Migrations/20230520203305_MYAddSystemChannels.Designer.cs +++ b/src/Tgstation.Server.Host/Database/Migrations/20230520203305_MYAddSystemChannels.Designer.cs @@ -5,8 +5,6 @@ using Microsoft.EntityFrameworkCore.Infrastructure; using Microsoft.EntityFrameworkCore.Migrations; -#nullable disable - namespace Tgstation.Server.Host.Database.Migrations { [DbContext(typeof(MySqlDatabaseContext))] diff --git a/src/Tgstation.Server.Host/Database/Migrations/20230520203305_MYAddSystemChannels.cs b/src/Tgstation.Server.Host/Database/Migrations/20230520203305_MYAddSystemChannels.cs index ec249f3e446..691e439fdca 100644 --- a/src/Tgstation.Server.Host/Database/Migrations/20230520203305_MYAddSystemChannels.cs +++ b/src/Tgstation.Server.Host/Database/Migrations/20230520203305_MYAddSystemChannels.cs @@ -2,8 +2,6 @@ using Microsoft.EntityFrameworkCore.Migrations; -#nullable disable - namespace Tgstation.Server.Host.Database.Migrations { /// diff --git a/src/Tgstation.Server.Host/Database/Migrations/20230520203332_PGAddSystemChannels.Designer.cs b/src/Tgstation.Server.Host/Database/Migrations/20230520203332_PGAddSystemChannels.Designer.cs index 613cd8034ef..cecc00d99f6 100644 --- a/src/Tgstation.Server.Host/Database/Migrations/20230520203332_PGAddSystemChannels.Designer.cs +++ b/src/Tgstation.Server.Host/Database/Migrations/20230520203332_PGAddSystemChannels.Designer.cs @@ -5,8 +5,6 @@ using Microsoft.EntityFrameworkCore.Infrastructure; using Microsoft.EntityFrameworkCore.Migrations; -#nullable disable - namespace Tgstation.Server.Host.Database.Migrations { [DbContext(typeof(PostgresSqlDatabaseContext))] diff --git a/src/Tgstation.Server.Host/Database/Migrations/20230520203332_PGAddSystemChannels.cs b/src/Tgstation.Server.Host/Database/Migrations/20230520203332_PGAddSystemChannels.cs index 60bf1c52297..17df0a48ef5 100644 --- a/src/Tgstation.Server.Host/Database/Migrations/20230520203332_PGAddSystemChannels.cs +++ b/src/Tgstation.Server.Host/Database/Migrations/20230520203332_PGAddSystemChannels.cs @@ -2,8 +2,6 @@ using Microsoft.EntityFrameworkCore.Migrations; -#nullable disable - namespace Tgstation.Server.Host.Database.Migrations { /// diff --git a/src/Tgstation.Server.Host/Database/Migrations/20230520203402_SLAddSystemChannels.Designer.cs b/src/Tgstation.Server.Host/Database/Migrations/20230520203402_SLAddSystemChannels.Designer.cs index fbe9ae470e3..2fbf1587188 100644 --- a/src/Tgstation.Server.Host/Database/Migrations/20230520203402_SLAddSystemChannels.Designer.cs +++ b/src/Tgstation.Server.Host/Database/Migrations/20230520203402_SLAddSystemChannels.Designer.cs @@ -5,8 +5,6 @@ using Microsoft.EntityFrameworkCore.Infrastructure; using Microsoft.EntityFrameworkCore.Migrations; -#nullable disable - namespace Tgstation.Server.Host.Database.Migrations { [DbContext(typeof(SqliteDatabaseContext))] diff --git a/src/Tgstation.Server.Host/Database/Migrations/20230520203402_SLAddSystemChannels.cs b/src/Tgstation.Server.Host/Database/Migrations/20230520203402_SLAddSystemChannels.cs index 4dc47740e86..95dc26a1b80 100644 --- a/src/Tgstation.Server.Host/Database/Migrations/20230520203402_SLAddSystemChannels.cs +++ b/src/Tgstation.Server.Host/Database/Migrations/20230520203402_SLAddSystemChannels.cs @@ -2,8 +2,6 @@ using Microsoft.EntityFrameworkCore.Migrations; -#nullable disable - namespace Tgstation.Server.Host.Database.Migrations { /// diff --git a/src/Tgstation.Server.Host/Database/Migrations/20230614053739_MSRenameHeartbeatsToHealthChecks.Designer.cs b/src/Tgstation.Server.Host/Database/Migrations/20230614053739_MSRenameHeartbeatsToHealthChecks.Designer.cs index 0b7c9281fb4..af40476708d 100644 --- a/src/Tgstation.Server.Host/Database/Migrations/20230614053739_MSRenameHeartbeatsToHealthChecks.Designer.cs +++ b/src/Tgstation.Server.Host/Database/Migrations/20230614053739_MSRenameHeartbeatsToHealthChecks.Designer.cs @@ -5,8 +5,6 @@ using Microsoft.EntityFrameworkCore.Infrastructure; using Microsoft.EntityFrameworkCore.Migrations; -#nullable disable - namespace Tgstation.Server.Host.Database.Migrations { [DbContext(typeof(SqlServerDatabaseContext))] diff --git a/src/Tgstation.Server.Host/Database/Migrations/20230614053739_MSRenameHeartbeatsToHealthChecks.cs b/src/Tgstation.Server.Host/Database/Migrations/20230614053739_MSRenameHeartbeatsToHealthChecks.cs index 37dbc148166..20f53e2d0a4 100644 --- a/src/Tgstation.Server.Host/Database/Migrations/20230614053739_MSRenameHeartbeatsToHealthChecks.cs +++ b/src/Tgstation.Server.Host/Database/Migrations/20230614053739_MSRenameHeartbeatsToHealthChecks.cs @@ -2,8 +2,6 @@ using Microsoft.EntityFrameworkCore.Migrations; -#nullable disable - namespace Tgstation.Server.Host.Database.Migrations { /// diff --git a/src/Tgstation.Server.Host/Database/Migrations/20230614053927_MYRenameHeartbeatsToHealthChecks.Designer.cs b/src/Tgstation.Server.Host/Database/Migrations/20230614053927_MYRenameHeartbeatsToHealthChecks.Designer.cs index 78f17b9159e..ce9233fc4e5 100644 --- a/src/Tgstation.Server.Host/Database/Migrations/20230614053927_MYRenameHeartbeatsToHealthChecks.Designer.cs +++ b/src/Tgstation.Server.Host/Database/Migrations/20230614053927_MYRenameHeartbeatsToHealthChecks.Designer.cs @@ -5,8 +5,6 @@ using Microsoft.EntityFrameworkCore.Infrastructure; using Microsoft.EntityFrameworkCore.Migrations; -#nullable disable - namespace Tgstation.Server.Host.Database.Migrations { [DbContext(typeof(MySqlDatabaseContext))] diff --git a/src/Tgstation.Server.Host/Database/Migrations/20230614053927_MYRenameHeartbeatsToHealthChecks.cs b/src/Tgstation.Server.Host/Database/Migrations/20230614053927_MYRenameHeartbeatsToHealthChecks.cs index 37d18bf1fb0..25fb3f21c80 100644 --- a/src/Tgstation.Server.Host/Database/Migrations/20230614053927_MYRenameHeartbeatsToHealthChecks.cs +++ b/src/Tgstation.Server.Host/Database/Migrations/20230614053927_MYRenameHeartbeatsToHealthChecks.cs @@ -2,8 +2,6 @@ using Microsoft.EntityFrameworkCore.Migrations; -#nullable disable - namespace Tgstation.Server.Host.Database.Migrations { /// diff --git a/src/Tgstation.Server.Host/Database/Migrations/20230614054432_PGRenameHeartbeatsToHealthChecks.Designer.cs b/src/Tgstation.Server.Host/Database/Migrations/20230614054432_PGRenameHeartbeatsToHealthChecks.Designer.cs index 650e56fe66d..849210b5854 100644 --- a/src/Tgstation.Server.Host/Database/Migrations/20230614054432_PGRenameHeartbeatsToHealthChecks.Designer.cs +++ b/src/Tgstation.Server.Host/Database/Migrations/20230614054432_PGRenameHeartbeatsToHealthChecks.Designer.cs @@ -5,8 +5,6 @@ using Microsoft.EntityFrameworkCore.Infrastructure; using Microsoft.EntityFrameworkCore.Migrations; -#nullable disable - namespace Tgstation.Server.Host.Database.Migrations { [DbContext(typeof(PostgresSqlDatabaseContext))] diff --git a/src/Tgstation.Server.Host/Database/Migrations/20230614054432_PGRenameHeartbeatsToHealthChecks.cs b/src/Tgstation.Server.Host/Database/Migrations/20230614054432_PGRenameHeartbeatsToHealthChecks.cs index 5ccae3445bb..75e99990d91 100644 --- a/src/Tgstation.Server.Host/Database/Migrations/20230614054432_PGRenameHeartbeatsToHealthChecks.cs +++ b/src/Tgstation.Server.Host/Database/Migrations/20230614054432_PGRenameHeartbeatsToHealthChecks.cs @@ -2,8 +2,6 @@ using Microsoft.EntityFrameworkCore.Migrations; -#nullable disable - namespace Tgstation.Server.Host.Database.Migrations { /// diff --git a/src/Tgstation.Server.Host/Database/Migrations/20230614054537_SLRenameHeartbeatsToHealthChecks.Designer.cs b/src/Tgstation.Server.Host/Database/Migrations/20230614054537_SLRenameHeartbeatsToHealthChecks.Designer.cs index 9180386ff12..9ffc8607f92 100644 --- a/src/Tgstation.Server.Host/Database/Migrations/20230614054537_SLRenameHeartbeatsToHealthChecks.Designer.cs +++ b/src/Tgstation.Server.Host/Database/Migrations/20230614054537_SLRenameHeartbeatsToHealthChecks.Designer.cs @@ -5,8 +5,6 @@ using Microsoft.EntityFrameworkCore.Infrastructure; using Microsoft.EntityFrameworkCore.Migrations; -#nullable disable - namespace Tgstation.Server.Host.Database.Migrations { [DbContext(typeof(SqliteDatabaseContext))] diff --git a/src/Tgstation.Server.Host/Database/Migrations/20230614054537_SLRenameHeartbeatsToHealthChecks.cs b/src/Tgstation.Server.Host/Database/Migrations/20230614054537_SLRenameHeartbeatsToHealthChecks.cs index dc88c94012c..8326dca934b 100644 --- a/src/Tgstation.Server.Host/Database/Migrations/20230614054537_SLRenameHeartbeatsToHealthChecks.cs +++ b/src/Tgstation.Server.Host/Database/Migrations/20230614054537_SLRenameHeartbeatsToHealthChecks.cs @@ -2,8 +2,6 @@ using Microsoft.EntityFrameworkCore.Migrations; -#nullable disable - namespace Tgstation.Server.Host.Database.Migrations { /// diff --git a/src/Tgstation.Server.Host/Database/Migrations/20230622020600_MSAddMapThreads.Designer.cs b/src/Tgstation.Server.Host/Database/Migrations/20230622020600_MSAddMapThreads.Designer.cs index 09c34848021..8910b644ac2 100644 --- a/src/Tgstation.Server.Host/Database/Migrations/20230622020600_MSAddMapThreads.Designer.cs +++ b/src/Tgstation.Server.Host/Database/Migrations/20230622020600_MSAddMapThreads.Designer.cs @@ -5,8 +5,6 @@ using Microsoft.EntityFrameworkCore.Infrastructure; using Microsoft.EntityFrameworkCore.Migrations; -#nullable disable - namespace Tgstation.Server.Host.Database.Migrations { [DbContext(typeof(SqlServerDatabaseContext))] diff --git a/src/Tgstation.Server.Host/Database/Migrations/20230622020600_MSAddMapThreads.cs b/src/Tgstation.Server.Host/Database/Migrations/20230622020600_MSAddMapThreads.cs index dab10dc6af1..2bfc981d34a 100644 --- a/src/Tgstation.Server.Host/Database/Migrations/20230622020600_MSAddMapThreads.cs +++ b/src/Tgstation.Server.Host/Database/Migrations/20230622020600_MSAddMapThreads.cs @@ -2,8 +2,6 @@ using Microsoft.EntityFrameworkCore.Migrations; -#nullable disable - namespace Tgstation.Server.Host.Database.Migrations { /// diff --git a/src/Tgstation.Server.Host/Database/Migrations/20230622020623_MYAddMapThreads.Designer.cs b/src/Tgstation.Server.Host/Database/Migrations/20230622020623_MYAddMapThreads.Designer.cs index f7039334cf4..b9fe9c7cdd4 100644 --- a/src/Tgstation.Server.Host/Database/Migrations/20230622020623_MYAddMapThreads.Designer.cs +++ b/src/Tgstation.Server.Host/Database/Migrations/20230622020623_MYAddMapThreads.Designer.cs @@ -5,8 +5,6 @@ using Microsoft.EntityFrameworkCore.Infrastructure; using Microsoft.EntityFrameworkCore.Migrations; -#nullable disable - namespace Tgstation.Server.Host.Database.Migrations { [DbContext(typeof(MySqlDatabaseContext))] diff --git a/src/Tgstation.Server.Host/Database/Migrations/20230622020623_MYAddMapThreads.cs b/src/Tgstation.Server.Host/Database/Migrations/20230622020623_MYAddMapThreads.cs index 1f83abc44d4..aa52cc0d6ba 100644 --- a/src/Tgstation.Server.Host/Database/Migrations/20230622020623_MYAddMapThreads.cs +++ b/src/Tgstation.Server.Host/Database/Migrations/20230622020623_MYAddMapThreads.cs @@ -2,8 +2,6 @@ using Microsoft.EntityFrameworkCore.Migrations; -#nullable disable - namespace Tgstation.Server.Host.Database.Migrations { /// diff --git a/src/Tgstation.Server.Host/Database/Migrations/20230622020647_PGAddMapThreads.Designer.cs b/src/Tgstation.Server.Host/Database/Migrations/20230622020647_PGAddMapThreads.Designer.cs index 6ef71f98eea..37c731af707 100644 --- a/src/Tgstation.Server.Host/Database/Migrations/20230622020647_PGAddMapThreads.Designer.cs +++ b/src/Tgstation.Server.Host/Database/Migrations/20230622020647_PGAddMapThreads.Designer.cs @@ -5,8 +5,6 @@ using Microsoft.EntityFrameworkCore.Infrastructure; using Microsoft.EntityFrameworkCore.Migrations; -#nullable disable - namespace Tgstation.Server.Host.Database.Migrations { [DbContext(typeof(PostgresSqlDatabaseContext))] diff --git a/src/Tgstation.Server.Host/Database/Migrations/20230622020647_PGAddMapThreads.cs b/src/Tgstation.Server.Host/Database/Migrations/20230622020647_PGAddMapThreads.cs index 1ff154f4489..12314271580 100644 --- a/src/Tgstation.Server.Host/Database/Migrations/20230622020647_PGAddMapThreads.cs +++ b/src/Tgstation.Server.Host/Database/Migrations/20230622020647_PGAddMapThreads.cs @@ -2,8 +2,6 @@ using Microsoft.EntityFrameworkCore.Migrations; -#nullable disable - namespace Tgstation.Server.Host.Database.Migrations { /// diff --git a/src/Tgstation.Server.Host/Database/Migrations/20230622020712_SLAddMapThreads.Designer.cs b/src/Tgstation.Server.Host/Database/Migrations/20230622020712_SLAddMapThreads.Designer.cs index 0db22004a66..23859c77bed 100644 --- a/src/Tgstation.Server.Host/Database/Migrations/20230622020712_SLAddMapThreads.Designer.cs +++ b/src/Tgstation.Server.Host/Database/Migrations/20230622020712_SLAddMapThreads.Designer.cs @@ -5,8 +5,6 @@ using Microsoft.EntityFrameworkCore.Infrastructure; using Microsoft.EntityFrameworkCore.Migrations; -#nullable disable - namespace Tgstation.Server.Host.Database.Migrations { [DbContext(typeof(SqliteDatabaseContext))] diff --git a/src/Tgstation.Server.Host/Database/Migrations/20230622020712_SLAddMapThreads.cs b/src/Tgstation.Server.Host/Database/Migrations/20230622020712_SLAddMapThreads.cs index 218c9d7d4bc..8909a495d28 100644 --- a/src/Tgstation.Server.Host/Database/Migrations/20230622020712_SLAddMapThreads.cs +++ b/src/Tgstation.Server.Host/Database/Migrations/20230622020712_SLAddMapThreads.cs @@ -2,8 +2,6 @@ using Microsoft.EntityFrameworkCore.Migrations; -#nullable disable - namespace Tgstation.Server.Host.Database.Migrations { /// diff --git a/src/Tgstation.Server.Host/Database/Migrations/20231105004801_MSAddJobCodes.Designer.cs b/src/Tgstation.Server.Host/Database/Migrations/20231105004801_MSAddJobCodes.Designer.cs index 556f6d1009d..3d2f9959ab7 100644 --- a/src/Tgstation.Server.Host/Database/Migrations/20231105004801_MSAddJobCodes.Designer.cs +++ b/src/Tgstation.Server.Host/Database/Migrations/20231105004801_MSAddJobCodes.Designer.cs @@ -5,8 +5,6 @@ using Microsoft.EntityFrameworkCore.Infrastructure; using Microsoft.EntityFrameworkCore.Migrations; -#nullable disable - namespace Tgstation.Server.Host.Database.Migrations { [DbContext(typeof(SqlServerDatabaseContext))] diff --git a/src/Tgstation.Server.Host/Database/Migrations/20231105004801_MSAddJobCodes.cs b/src/Tgstation.Server.Host/Database/Migrations/20231105004801_MSAddJobCodes.cs index 87636a060f9..4a6606334ac 100644 --- a/src/Tgstation.Server.Host/Database/Migrations/20231105004801_MSAddJobCodes.cs +++ b/src/Tgstation.Server.Host/Database/Migrations/20231105004801_MSAddJobCodes.cs @@ -2,8 +2,6 @@ using Microsoft.EntityFrameworkCore.Migrations; -#nullable disable - namespace Tgstation.Server.Host.Database.Migrations { /// diff --git a/src/Tgstation.Server.Host/Database/Migrations/20231105004808_MYAddJobCodes.Designer.cs b/src/Tgstation.Server.Host/Database/Migrations/20231105004808_MYAddJobCodes.Designer.cs index c5f5fb94dcc..197b2441731 100644 --- a/src/Tgstation.Server.Host/Database/Migrations/20231105004808_MYAddJobCodes.Designer.cs +++ b/src/Tgstation.Server.Host/Database/Migrations/20231105004808_MYAddJobCodes.Designer.cs @@ -5,8 +5,6 @@ using Microsoft.EntityFrameworkCore.Infrastructure; using Microsoft.EntityFrameworkCore.Migrations; -#nullable disable - namespace Tgstation.Server.Host.Database.Migrations { [DbContext(typeof(MySqlDatabaseContext))] diff --git a/src/Tgstation.Server.Host/Database/Migrations/20231105004808_MYAddJobCodes.cs b/src/Tgstation.Server.Host/Database/Migrations/20231105004808_MYAddJobCodes.cs index 61eb53b6a5d..4afb9021831 100644 --- a/src/Tgstation.Server.Host/Database/Migrations/20231105004808_MYAddJobCodes.cs +++ b/src/Tgstation.Server.Host/Database/Migrations/20231105004808_MYAddJobCodes.cs @@ -2,8 +2,6 @@ using Microsoft.EntityFrameworkCore.Migrations; -#nullable disable - namespace Tgstation.Server.Host.Database.Migrations { /// diff --git a/src/Tgstation.Server.Host/Database/Migrations/20231105004814_PGAddJobCodes.Designer.cs b/src/Tgstation.Server.Host/Database/Migrations/20231105004814_PGAddJobCodes.Designer.cs index 6ad0149338d..501367d6fc3 100644 --- a/src/Tgstation.Server.Host/Database/Migrations/20231105004814_PGAddJobCodes.Designer.cs +++ b/src/Tgstation.Server.Host/Database/Migrations/20231105004814_PGAddJobCodes.Designer.cs @@ -5,8 +5,6 @@ using Microsoft.EntityFrameworkCore.Infrastructure; using Microsoft.EntityFrameworkCore.Migrations; -#nullable disable - namespace Tgstation.Server.Host.Database.Migrations { [DbContext(typeof(PostgresSqlDatabaseContext))] diff --git a/src/Tgstation.Server.Host/Database/Migrations/20231105004814_PGAddJobCodes.cs b/src/Tgstation.Server.Host/Database/Migrations/20231105004814_PGAddJobCodes.cs index 5bd1b863f68..b11ba1b584c 100644 --- a/src/Tgstation.Server.Host/Database/Migrations/20231105004814_PGAddJobCodes.cs +++ b/src/Tgstation.Server.Host/Database/Migrations/20231105004814_PGAddJobCodes.cs @@ -2,8 +2,6 @@ using Microsoft.EntityFrameworkCore.Migrations; -#nullable disable - namespace Tgstation.Server.Host.Database.Migrations { /// diff --git a/src/Tgstation.Server.Host/Database/Migrations/20231105004820_SLAddJobCodes.Designer.cs b/src/Tgstation.Server.Host/Database/Migrations/20231105004820_SLAddJobCodes.Designer.cs index 718743a339c..7fb555cc8b8 100644 --- a/src/Tgstation.Server.Host/Database/Migrations/20231105004820_SLAddJobCodes.Designer.cs +++ b/src/Tgstation.Server.Host/Database/Migrations/20231105004820_SLAddJobCodes.Designer.cs @@ -5,8 +5,6 @@ using Microsoft.EntityFrameworkCore.Infrastructure; using Microsoft.EntityFrameworkCore.Migrations; -#nullable disable - namespace Tgstation.Server.Host.Database.Migrations { [DbContext(typeof(SqliteDatabaseContext))] diff --git a/src/Tgstation.Server.Host/Database/Migrations/20231105004820_SLAddJobCodes.cs b/src/Tgstation.Server.Host/Database/Migrations/20231105004820_SLAddJobCodes.cs index 76a2be02ee4..77076a8361b 100644 --- a/src/Tgstation.Server.Host/Database/Migrations/20231105004820_SLAddJobCodes.cs +++ b/src/Tgstation.Server.Host/Database/Migrations/20231105004820_SLAddJobCodes.cs @@ -2,8 +2,6 @@ using Microsoft.EntityFrameworkCore.Migrations; -#nullable disable - namespace Tgstation.Server.Host.Database.Migrations { /// diff --git a/src/Tgstation.Server.Host/Database/Migrations/20231108004349_MSRenameByondColumnsToEngine.Designer.cs b/src/Tgstation.Server.Host/Database/Migrations/20231108004349_MSRenameByondColumnsToEngine.Designer.cs new file mode 100644 index 00000000000..99cdc64c2e5 --- /dev/null +++ b/src/Tgstation.Server.Host/Database/Migrations/20231108004349_MSRenameByondColumnsToEngine.Designer.cs @@ -0,0 +1,1074 @@ +// +using System; + +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Migrations; + +namespace Tgstation.Server.Host.Database.Migrations +{ + [DbContext(typeof(SqlServerDatabaseContext))] + [Migration("20231108004349_MSRenameByondColumnsToEngine")] + partial class MSRenameByondColumnsToEngine + { + /// + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("ProductVersion", "8.0.0-rc.1.23419.6") + .HasAnnotation("Relational:MaxIdentifierLength", 128); + + SqlServerModelBuilderExtensions.UseIdentityColumns(modelBuilder); + + modelBuilder.Entity("Tgstation.Server.Host.Models.ChatBot", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("ChannelLimit") + .HasColumnType("int"); + + b.Property("ConnectionString") + .IsRequired() + .HasMaxLength(10000) + .HasColumnType("nvarchar(max)"); + + b.Property("Enabled") + .HasColumnType("bit"); + + b.Property("InstanceId") + .HasColumnType("bigint"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("nvarchar(100)"); + + b.Property("Provider") + .HasColumnType("int"); + + b.Property("ReconnectionInterval") + .HasColumnType("bigint"); + + b.HasKey("Id"); + + b.HasIndex("InstanceId", "Name") + .IsUnique(); + + b.ToTable("ChatBots"); + }); + + modelBuilder.Entity("Tgstation.Server.Host.Models.ChatChannel", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("ChatSettingsId") + .HasColumnType("bigint"); + + b.Property("DiscordChannelId") + .HasColumnType("decimal(20,0)"); + + b.Property("IrcChannel") + .HasMaxLength(100) + .HasColumnType("nvarchar(100)"); + + b.Property("IsAdminChannel") + .IsRequired() + .HasColumnType("bit"); + + b.Property("IsSystemChannel") + .IsRequired() + .HasColumnType("bit"); + + b.Property("IsUpdatesChannel") + .IsRequired() + .HasColumnType("bit"); + + b.Property("IsWatchdogChannel") + .IsRequired() + .HasColumnType("bit"); + + b.Property("Tag") + .HasMaxLength(10000) + .HasColumnType("nvarchar(max)"); + + b.HasKey("Id"); + + b.HasIndex("ChatSettingsId", "DiscordChannelId") + .IsUnique() + .HasFilter("[DiscordChannelId] IS NOT NULL"); + + b.HasIndex("ChatSettingsId", "IrcChannel") + .IsUnique() + .HasFilter("[IrcChannel] IS NOT NULL"); + + b.ToTable("ChatChannels"); + }); + + modelBuilder.Entity("Tgstation.Server.Host.Models.CompileJob", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("ByondVersion") + .IsRequired() + .HasColumnType("nvarchar(max)") + .HasColumnName("EngineVersion"); + + b.Property("DMApiMajorVersion") + .HasColumnType("int"); + + b.Property("DMApiMinorVersion") + .HasColumnType("int"); + + b.Property("DMApiPatchVersion") + .HasColumnType("int"); + + b.Property("DirectoryName") + .IsRequired() + .HasColumnType("uniqueidentifier"); + + b.Property("DmeName") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("GitHubDeploymentId") + .HasColumnType("int"); + + b.Property("GitHubRepoId") + .HasColumnType("bigint"); + + b.Property("JobId") + .HasColumnType("bigint"); + + b.Property("MinimumSecurityLevel") + .HasColumnType("int"); + + b.Property("Output") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("RepositoryOrigin") + .HasColumnType("nvarchar(max)"); + + b.Property("RevisionInformationId") + .HasColumnType("bigint"); + + b.HasKey("Id"); + + b.HasIndex("DirectoryName"); + + b.HasIndex("JobId") + .IsUnique(); + + b.HasIndex("RevisionInformationId"); + + b.ToTable("CompileJobs"); + }); + + modelBuilder.Entity("Tgstation.Server.Host.Models.DreamDaemonSettings", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("AdditionalParameters") + .IsRequired() + .HasMaxLength(10000) + .HasColumnType("nvarchar(max)"); + + b.Property("AllowWebClient") + .IsRequired() + .HasColumnType("bit"); + + b.Property("AutoStart") + .IsRequired() + .HasColumnType("bit"); + + b.Property("DumpOnHealthCheckRestart") + .IsRequired() + .HasColumnType("bit"); + + b.Property("HealthCheckSeconds") + .HasColumnType("bigint"); + + b.Property("InstanceId") + .HasColumnType("bigint"); + + b.Property("LogOutput") + .IsRequired() + .HasColumnType("bit"); + + b.Property("MapThreads") + .HasColumnType("bigint"); + + b.Property("Port") + .HasColumnType("int"); + + b.Property("SecurityLevel") + .HasColumnType("int"); + + b.Property("StartProfiler") + .IsRequired() + .HasColumnType("bit"); + + b.Property("StartupTimeout") + .HasColumnType("bigint"); + + b.Property("TopicRequestTimeout") + .HasColumnType("bigint"); + + b.Property("Visibility") + .HasColumnType("int"); + + b.HasKey("Id"); + + b.HasIndex("InstanceId") + .IsUnique(); + + b.ToTable("DreamDaemonSettings"); + }); + + modelBuilder.Entity("Tgstation.Server.Host.Models.DreamMakerSettings", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("ApiValidationPort") + .HasColumnType("int"); + + b.Property("ApiValidationSecurityLevel") + .HasColumnType("int"); + + b.Property("InstanceId") + .HasColumnType("bigint"); + + b.Property("ProjectName") + .HasMaxLength(10000) + .HasColumnType("nvarchar(max)"); + + b.Property("RequireDMApiValidation") + .IsRequired() + .HasColumnType("bit"); + + b.Property("Timeout") + .IsRequired() + .HasColumnType("time"); + + b.HasKey("Id"); + + b.HasIndex("InstanceId") + .IsUnique(); + + b.ToTable("DreamMakerSettings"); + }); + + modelBuilder.Entity("Tgstation.Server.Host.Models.Instance", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("AutoUpdateInterval") + .HasColumnType("bigint"); + + b.Property("ChatBotLimit") + .HasColumnType("int"); + + b.Property("ConfigurationType") + .HasColumnType("int"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("nvarchar(100)"); + + b.Property("Online") + .IsRequired() + .HasColumnType("bit"); + + b.Property("Path") + .IsRequired() + .HasColumnType("nvarchar(450)"); + + b.Property("SwarmIdentifer") + .HasColumnType("nvarchar(450)"); + + b.HasKey("Id"); + + b.HasIndex("Path", "SwarmIdentifer") + .IsUnique() + .HasFilter("[SwarmIdentifer] IS NOT NULL"); + + b.ToTable("Instances"); + }); + + modelBuilder.Entity("Tgstation.Server.Host.Models.InstancePermissionSet", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("ChatBotRights") + .HasColumnType("decimal(20,0)"); + + b.Property("ConfigurationRights") + .HasColumnType("decimal(20,0)"); + + b.Property("DreamDaemonRights") + .HasColumnType("decimal(20,0)"); + + b.Property("DreamMakerRights") + .HasColumnType("decimal(20,0)"); + + b.Property("EngineRights") + .HasColumnType("decimal(20,0)"); + + b.Property("InstanceId") + .HasColumnType("bigint"); + + b.Property("InstancePermissionSetRights") + .HasColumnType("decimal(20,0)"); + + b.Property("PermissionSetId") + .HasColumnType("bigint"); + + b.Property("RepositoryRights") + .HasColumnType("decimal(20,0)"); + + b.HasKey("Id"); + + b.HasIndex("InstanceId"); + + b.HasIndex("PermissionSetId", "InstanceId") + .IsUnique(); + + b.ToTable("InstancePermissionSets"); + }); + + modelBuilder.Entity("Tgstation.Server.Host.Models.Job", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("CancelRight") + .HasColumnType("decimal(20,0)"); + + b.Property("CancelRightsType") + .HasColumnType("decimal(20,0)"); + + b.Property("Cancelled") + .IsRequired() + .HasColumnType("bit"); + + b.Property("CancelledById") + .HasColumnType("bigint"); + + b.Property("Description") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("ErrorCode") + .HasColumnType("bigint"); + + b.Property("ExceptionDetails") + .HasColumnType("nvarchar(max)"); + + b.Property("InstanceId") + .HasColumnType("bigint"); + + b.Property("JobCode") + .HasColumnType("tinyint"); + + b.Property("StartedAt") + .IsRequired() + .HasColumnType("datetimeoffset"); + + b.Property("StartedById") + .HasColumnType("bigint"); + + b.Property("StoppedAt") + .HasColumnType("datetimeoffset"); + + b.HasKey("Id"); + + b.HasIndex("CancelledById"); + + b.HasIndex("InstanceId"); + + b.HasIndex("StartedById"); + + b.ToTable("Jobs"); + }); + + modelBuilder.Entity("Tgstation.Server.Host.Models.OAuthConnection", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("ExternalUserId") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("nvarchar(100)"); + + b.Property("Provider") + .HasColumnType("int"); + + b.Property("UserId") + .HasColumnType("bigint"); + + b.HasKey("Id"); + + b.HasIndex("UserId"); + + b.HasIndex("Provider", "ExternalUserId") + .IsUnique(); + + b.ToTable("OAuthConnections"); + }); + + modelBuilder.Entity("Tgstation.Server.Host.Models.PermissionSet", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("AdministrationRights") + .HasColumnType("decimal(20,0)"); + + b.Property("GroupId") + .HasColumnType("bigint"); + + b.Property("InstanceManagerRights") + .HasColumnType("decimal(20,0)"); + + b.Property("UserId") + .HasColumnType("bigint"); + + b.HasKey("Id"); + + b.HasIndex("GroupId") + .IsUnique() + .HasFilter("[GroupId] IS NOT NULL"); + + b.HasIndex("UserId") + .IsUnique() + .HasFilter("[UserId] IS NOT NULL"); + + b.ToTable("PermissionSets"); + }); + + modelBuilder.Entity("Tgstation.Server.Host.Models.ReattachInformation", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("AccessIdentifier") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("CompileJobId") + .HasColumnType("bigint"); + + b.Property("InitialCompileJobId") + .HasColumnType("bigint"); + + b.Property("LaunchSecurityLevel") + .HasColumnType("int"); + + b.Property("LaunchVisibility") + .HasColumnType("int"); + + b.Property("Port") + .HasColumnType("int"); + + b.Property("ProcessId") + .HasColumnType("int"); + + b.Property("RebootState") + .HasColumnType("int"); + + b.HasKey("Id"); + + b.HasIndex("CompileJobId"); + + b.HasIndex("InitialCompileJobId"); + + b.ToTable("ReattachInformations"); + }); + + modelBuilder.Entity("Tgstation.Server.Host.Models.RepositorySettings", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("AccessToken") + .HasMaxLength(10000) + .HasColumnType("nvarchar(max)"); + + b.Property("AccessUser") + .HasMaxLength(10000) + .HasColumnType("nvarchar(max)"); + + b.Property("AutoUpdatesKeepTestMerges") + .IsRequired() + .HasColumnType("bit"); + + b.Property("AutoUpdatesSynchronize") + .IsRequired() + .HasColumnType("bit"); + + b.Property("CommitterEmail") + .IsRequired() + .HasMaxLength(10000) + .HasColumnType("nvarchar(max)"); + + b.Property("CommitterName") + .IsRequired() + .HasMaxLength(10000) + .HasColumnType("nvarchar(max)"); + + b.Property("CreateGitHubDeployments") + .IsRequired() + .HasColumnType("bit"); + + b.Property("InstanceId") + .HasColumnType("bigint"); + + b.Property("PostTestMergeComment") + .IsRequired() + .HasColumnType("bit"); + + b.Property("PushTestMergeCommits") + .IsRequired() + .HasColumnType("bit"); + + b.Property("ShowTestMergeCommitters") + .IsRequired() + .HasColumnType("bit"); + + b.Property("UpdateSubmodules") + .IsRequired() + .HasColumnType("bit"); + + b.HasKey("Id"); + + b.HasIndex("InstanceId") + .IsUnique(); + + b.ToTable("RepositorySettings"); + }); + + modelBuilder.Entity("Tgstation.Server.Host.Models.RevInfoTestMerge", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("RevisionInformationId") + .HasColumnType("bigint"); + + b.Property("TestMergeId") + .HasColumnType("bigint"); + + b.HasKey("Id"); + + b.HasIndex("RevisionInformationId"); + + b.HasIndex("TestMergeId"); + + b.ToTable("RevInfoTestMerges"); + }); + + modelBuilder.Entity("Tgstation.Server.Host.Models.RevisionInformation", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("CommitSha") + .IsRequired() + .HasMaxLength(40) + .HasColumnType("nvarchar(40)"); + + b.Property("InstanceId") + .HasColumnType("bigint"); + + b.Property("OriginCommitSha") + .IsRequired() + .HasMaxLength(40) + .HasColumnType("nvarchar(40)"); + + b.Property("Timestamp") + .HasColumnType("datetimeoffset"); + + b.HasKey("Id"); + + b.HasIndex("InstanceId", "CommitSha") + .IsUnique(); + + b.ToTable("RevisionInformations"); + }); + + modelBuilder.Entity("Tgstation.Server.Host.Models.TestMerge", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("Author") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("BodyAtMerge") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("Comment") + .HasMaxLength(10000) + .HasColumnType("nvarchar(max)"); + + b.Property("MergedAt") + .HasColumnType("datetimeoffset"); + + b.Property("MergedById") + .HasColumnType("bigint"); + + b.Property("Number") + .HasColumnType("int"); + + b.Property("PrimaryRevisionInformationId") + .IsRequired() + .HasColumnType("bigint"); + + b.Property("TargetCommitSha") + .IsRequired() + .HasMaxLength(40) + .HasColumnType("nvarchar(40)"); + + b.Property("TitleAtMerge") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("Url") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.HasKey("Id"); + + b.HasIndex("MergedById"); + + b.HasIndex("PrimaryRevisionInformationId") + .IsUnique(); + + b.ToTable("TestMerges"); + }); + + modelBuilder.Entity("Tgstation.Server.Host.Models.User", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("CanonicalName") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("nvarchar(100)"); + + b.Property("CreatedAt") + .IsRequired() + .HasColumnType("datetimeoffset"); + + b.Property("CreatedById") + .HasColumnType("bigint"); + + b.Property("Enabled") + .IsRequired() + .HasColumnType("bit"); + + b.Property("GroupId") + .HasColumnType("bigint"); + + b.Property("LastPasswordUpdate") + .HasColumnType("datetimeoffset"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("nvarchar(100)"); + + b.Property("PasswordHash") + .HasColumnType("nvarchar(max)"); + + b.Property("SystemIdentifier") + .HasMaxLength(100) + .HasColumnType("nvarchar(100)"); + + b.HasKey("Id"); + + b.HasIndex("CanonicalName") + .IsUnique(); + + b.HasIndex("CreatedById"); + + b.HasIndex("GroupId"); + + b.HasIndex("SystemIdentifier") + .IsUnique() + .HasFilter("[SystemIdentifier] IS NOT NULL"); + + b.ToTable("Users"); + }); + + modelBuilder.Entity("Tgstation.Server.Host.Models.UserGroup", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("Name") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("nvarchar(100)"); + + b.HasKey("Id"); + + b.HasIndex("Name") + .IsUnique(); + + b.ToTable("Groups"); + }); + + modelBuilder.Entity("Tgstation.Server.Host.Models.ChatBot", b => + { + b.HasOne("Tgstation.Server.Host.Models.Instance", "Instance") + .WithMany("ChatSettings") + .HasForeignKey("InstanceId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Instance"); + }); + + modelBuilder.Entity("Tgstation.Server.Host.Models.ChatChannel", b => + { + b.HasOne("Tgstation.Server.Host.Models.ChatBot", "ChatSettings") + .WithMany("Channels") + .HasForeignKey("ChatSettingsId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("ChatSettings"); + }); + + modelBuilder.Entity("Tgstation.Server.Host.Models.CompileJob", b => + { + b.HasOne("Tgstation.Server.Host.Models.Job", "Job") + .WithOne() + .HasForeignKey("Tgstation.Server.Host.Models.CompileJob", "JobId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Tgstation.Server.Host.Models.RevisionInformation", "RevisionInformation") + .WithMany("CompileJobs") + .HasForeignKey("RevisionInformationId") + .OnDelete(DeleteBehavior.ClientNoAction) + .IsRequired(); + + b.Navigation("Job"); + + b.Navigation("RevisionInformation"); + }); + + modelBuilder.Entity("Tgstation.Server.Host.Models.DreamDaemonSettings", b => + { + b.HasOne("Tgstation.Server.Host.Models.Instance", "Instance") + .WithOne("DreamDaemonSettings") + .HasForeignKey("Tgstation.Server.Host.Models.DreamDaemonSettings", "InstanceId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Instance"); + }); + + modelBuilder.Entity("Tgstation.Server.Host.Models.DreamMakerSettings", b => + { + b.HasOne("Tgstation.Server.Host.Models.Instance", "Instance") + .WithOne("DreamMakerSettings") + .HasForeignKey("Tgstation.Server.Host.Models.DreamMakerSettings", "InstanceId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Instance"); + }); + + modelBuilder.Entity("Tgstation.Server.Host.Models.InstancePermissionSet", b => + { + b.HasOne("Tgstation.Server.Host.Models.Instance", "Instance") + .WithMany("InstancePermissionSets") + .HasForeignKey("InstanceId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Tgstation.Server.Host.Models.PermissionSet", "PermissionSet") + .WithMany("InstancePermissionSets") + .HasForeignKey("PermissionSetId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Instance"); + + b.Navigation("PermissionSet"); + }); + + modelBuilder.Entity("Tgstation.Server.Host.Models.Job", b => + { + b.HasOne("Tgstation.Server.Host.Models.User", "CancelledBy") + .WithMany() + .HasForeignKey("CancelledById"); + + b.HasOne("Tgstation.Server.Host.Models.Instance", "Instance") + .WithMany("Jobs") + .HasForeignKey("InstanceId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Tgstation.Server.Host.Models.User", "StartedBy") + .WithMany() + .HasForeignKey("StartedById") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("CancelledBy"); + + b.Navigation("Instance"); + + b.Navigation("StartedBy"); + }); + + modelBuilder.Entity("Tgstation.Server.Host.Models.OAuthConnection", b => + { + b.HasOne("Tgstation.Server.Host.Models.User", "User") + .WithMany("OAuthConnections") + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Tgstation.Server.Host.Models.PermissionSet", b => + { + b.HasOne("Tgstation.Server.Host.Models.UserGroup", "Group") + .WithOne("PermissionSet") + .HasForeignKey("Tgstation.Server.Host.Models.PermissionSet", "GroupId") + .OnDelete(DeleteBehavior.Cascade); + + b.HasOne("Tgstation.Server.Host.Models.User", "User") + .WithOne("PermissionSet") + .HasForeignKey("Tgstation.Server.Host.Models.PermissionSet", "UserId") + .OnDelete(DeleteBehavior.Cascade); + + b.Navigation("Group"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Tgstation.Server.Host.Models.ReattachInformation", b => + { + b.HasOne("Tgstation.Server.Host.Models.CompileJob", "CompileJob") + .WithMany() + .HasForeignKey("CompileJobId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Tgstation.Server.Host.Models.CompileJob", "InitialCompileJob") + .WithMany() + .HasForeignKey("InitialCompileJobId"); + + b.Navigation("CompileJob"); + + b.Navigation("InitialCompileJob"); + }); + + modelBuilder.Entity("Tgstation.Server.Host.Models.RepositorySettings", b => + { + b.HasOne("Tgstation.Server.Host.Models.Instance", "Instance") + .WithOne("RepositorySettings") + .HasForeignKey("Tgstation.Server.Host.Models.RepositorySettings", "InstanceId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Instance"); + }); + + modelBuilder.Entity("Tgstation.Server.Host.Models.RevInfoTestMerge", b => + { + b.HasOne("Tgstation.Server.Host.Models.RevisionInformation", "RevisionInformation") + .WithMany("ActiveTestMerges") + .HasForeignKey("RevisionInformationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Tgstation.Server.Host.Models.TestMerge", "TestMerge") + .WithMany("RevisonInformations") + .HasForeignKey("TestMergeId") + .OnDelete(DeleteBehavior.ClientNoAction) + .IsRequired(); + + b.Navigation("RevisionInformation"); + + b.Navigation("TestMerge"); + }); + + modelBuilder.Entity("Tgstation.Server.Host.Models.RevisionInformation", b => + { + b.HasOne("Tgstation.Server.Host.Models.Instance", "Instance") + .WithMany("RevisionInformations") + .HasForeignKey("InstanceId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Instance"); + }); + + modelBuilder.Entity("Tgstation.Server.Host.Models.TestMerge", b => + { + b.HasOne("Tgstation.Server.Host.Models.User", "MergedBy") + .WithMany("TestMerges") + .HasForeignKey("MergedById") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("Tgstation.Server.Host.Models.RevisionInformation", "PrimaryRevisionInformation") + .WithOne("PrimaryTestMerge") + .HasForeignKey("Tgstation.Server.Host.Models.TestMerge", "PrimaryRevisionInformationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("MergedBy"); + + b.Navigation("PrimaryRevisionInformation"); + }); + + modelBuilder.Entity("Tgstation.Server.Host.Models.User", b => + { + b.HasOne("Tgstation.Server.Host.Models.User", "CreatedBy") + .WithMany("CreatedUsers") + .HasForeignKey("CreatedById"); + + b.HasOne("Tgstation.Server.Host.Models.UserGroup", "Group") + .WithMany("Users") + .HasForeignKey("GroupId"); + + b.Navigation("CreatedBy"); + + b.Navigation("Group"); + }); + + modelBuilder.Entity("Tgstation.Server.Host.Models.ChatBot", b => + { + b.Navigation("Channels"); + }); + + modelBuilder.Entity("Tgstation.Server.Host.Models.Instance", b => + { + b.Navigation("ChatSettings"); + + b.Navigation("DreamDaemonSettings"); + + b.Navigation("DreamMakerSettings"); + + b.Navigation("InstancePermissionSets"); + + b.Navigation("Jobs"); + + b.Navigation("RepositorySettings"); + + b.Navigation("RevisionInformations"); + }); + + modelBuilder.Entity("Tgstation.Server.Host.Models.PermissionSet", b => + { + b.Navigation("InstancePermissionSets"); + }); + + modelBuilder.Entity("Tgstation.Server.Host.Models.RevisionInformation", b => + { + b.Navigation("ActiveTestMerges"); + + b.Navigation("CompileJobs"); + + b.Navigation("PrimaryTestMerge"); + }); + + modelBuilder.Entity("Tgstation.Server.Host.Models.TestMerge", b => + { + b.Navigation("RevisonInformations"); + }); + + modelBuilder.Entity("Tgstation.Server.Host.Models.User", b => + { + b.Navigation("CreatedUsers"); + + b.Navigation("OAuthConnections"); + + b.Navigation("PermissionSet"); + + b.Navigation("TestMerges"); + }); + + modelBuilder.Entity("Tgstation.Server.Host.Models.UserGroup", b => + { + b.Navigation("PermissionSet") + .IsRequired(); + + b.Navigation("Users"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/src/Tgstation.Server.Host/Database/Migrations/20231108004349_MSRenameByondColumnsToEngine.cs b/src/Tgstation.Server.Host/Database/Migrations/20231108004349_MSRenameByondColumnsToEngine.cs new file mode 100644 index 00000000000..cdf156ec28c --- /dev/null +++ b/src/Tgstation.Server.Host/Database/Migrations/20231108004349_MSRenameByondColumnsToEngine.cs @@ -0,0 +1,42 @@ +using System; + +using Microsoft.EntityFrameworkCore.Migrations; + +namespace Tgstation.Server.Host.Database.Migrations +{ + /// + public partial class MSRenameByondColumnsToEngine : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + ArgumentNullException.ThrowIfNull(migrationBuilder); + + migrationBuilder.RenameColumn( + name: "ByondRights", + table: "InstancePermissionSets", + newName: "EngineRights"); + + migrationBuilder.RenameColumn( + name: "ByondVersion", + table: "CompileJobs", + newName: "EngineVersion"); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + ArgumentNullException.ThrowIfNull(migrationBuilder); + + migrationBuilder.RenameColumn( + name: "EngineRights", + table: "InstancePermissionSets", + newName: "ByondRights"); + + migrationBuilder.RenameColumn( + name: "EngineVersion", + table: "CompileJobs", + newName: "ByondVersion"); + } + } +} diff --git a/src/Tgstation.Server.Host/Database/Migrations/20231108004356_MYRenameByondColumnsToEngine.Designer.cs b/src/Tgstation.Server.Host/Database/Migrations/20231108004356_MYRenameByondColumnsToEngine.Designer.cs new file mode 100644 index 00000000000..841a10d43df --- /dev/null +++ b/src/Tgstation.Server.Host/Database/Migrations/20231108004356_MYRenameByondColumnsToEngine.Designer.cs @@ -0,0 +1,1108 @@ +// +using System; + +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Migrations; + +namespace Tgstation.Server.Host.Database.Migrations +{ + [DbContext(typeof(MySqlDatabaseContext))] + [Migration("20231108004356_MYRenameByondColumnsToEngine")] + partial class MYRenameByondColumnsToEngine + { + /// + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("ProductVersion", "8.0.0-rc.1.23419.6") + .HasAnnotation("Relational:MaxIdentifierLength", 64); + + modelBuilder.Entity("Tgstation.Server.Host.Models.ChatBot", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + b.Property("ChannelLimit") + .IsRequired() + .HasColumnType("smallint unsigned"); + + b.Property("ConnectionString") + .IsRequired() + .HasMaxLength(10000) + .HasColumnType("longtext"); + + MySqlPropertyBuilderExtensions.HasCharSet(b.Property("ConnectionString"), "utf8mb4"); + + b.Property("Enabled") + .HasColumnType("tinyint(1)"); + + b.Property("InstanceId") + .HasColumnType("bigint"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("varchar(100)"); + + b.Property("Provider") + .HasColumnType("int"); + + b.Property("ReconnectionInterval") + .IsRequired() + .HasColumnType("int unsigned"); + + b.HasKey("Id"); + + b.HasIndex("InstanceId", "Name") + .IsUnique(); + + b.ToTable("ChatBots"); + }); + + modelBuilder.Entity("Tgstation.Server.Host.Models.ChatChannel", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + b.Property("ChatSettingsId") + .HasColumnType("bigint"); + + b.Property("DiscordChannelId") + .HasColumnType("bigint unsigned"); + + b.Property("IrcChannel") + .HasMaxLength(100) + .HasColumnType("varchar(100)"); + + MySqlPropertyBuilderExtensions.HasCharSet(b.Property("IrcChannel"), "utf8mb4"); + + b.Property("IsAdminChannel") + .IsRequired() + .HasColumnType("tinyint(1)"); + + b.Property("IsSystemChannel") + .IsRequired() + .HasColumnType("tinyint(1)"); + + b.Property("IsUpdatesChannel") + .IsRequired() + .HasColumnType("tinyint(1)"); + + b.Property("IsWatchdogChannel") + .IsRequired() + .HasColumnType("tinyint(1)"); + + b.Property("Tag") + .HasMaxLength(10000) + .HasColumnType("longtext"); + + MySqlPropertyBuilderExtensions.HasCharSet(b.Property("Tag"), "utf8mb4"); + + b.HasKey("Id"); + + b.HasIndex("ChatSettingsId", "DiscordChannelId") + .IsUnique(); + + b.HasIndex("ChatSettingsId", "IrcChannel") + .IsUnique(); + + b.ToTable("ChatChannels"); + }); + + modelBuilder.Entity("Tgstation.Server.Host.Models.CompileJob", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + b.Property("ByondVersion") + .IsRequired() + .HasColumnType("longtext") + .HasColumnName("EngineVersion"); + + MySqlPropertyBuilderExtensions.HasCharSet(b.Property("ByondVersion"), "utf8mb4"); + + b.Property("DMApiMajorVersion") + .HasColumnType("int"); + + b.Property("DMApiMinorVersion") + .HasColumnType("int"); + + b.Property("DMApiPatchVersion") + .HasColumnType("int"); + + b.Property("DirectoryName") + .IsRequired() + .HasColumnType("char(36)"); + + b.Property("DmeName") + .IsRequired() + .HasColumnType("longtext"); + + MySqlPropertyBuilderExtensions.HasCharSet(b.Property("DmeName"), "utf8mb4"); + + b.Property("GitHubDeploymentId") + .HasColumnType("int"); + + b.Property("GitHubRepoId") + .HasColumnType("bigint"); + + b.Property("JobId") + .HasColumnType("bigint"); + + b.Property("MinimumSecurityLevel") + .HasColumnType("int"); + + b.Property("Output") + .IsRequired() + .HasColumnType("longtext"); + + MySqlPropertyBuilderExtensions.HasCharSet(b.Property("Output"), "utf8mb4"); + + b.Property("RepositoryOrigin") + .HasColumnType("longtext"); + + MySqlPropertyBuilderExtensions.HasCharSet(b.Property("RepositoryOrigin"), "utf8mb4"); + + b.Property("RevisionInformationId") + .HasColumnType("bigint"); + + b.HasKey("Id"); + + b.HasIndex("DirectoryName"); + + b.HasIndex("JobId") + .IsUnique(); + + b.HasIndex("RevisionInformationId"); + + b.ToTable("CompileJobs"); + }); + + modelBuilder.Entity("Tgstation.Server.Host.Models.DreamDaemonSettings", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + b.Property("AdditionalParameters") + .IsRequired() + .HasMaxLength(10000) + .HasColumnType("longtext"); + + MySqlPropertyBuilderExtensions.HasCharSet(b.Property("AdditionalParameters"), "utf8mb4"); + + b.Property("AllowWebClient") + .IsRequired() + .HasColumnType("tinyint(1)"); + + b.Property("AutoStart") + .IsRequired() + .HasColumnType("tinyint(1)"); + + b.Property("DumpOnHealthCheckRestart") + .IsRequired() + .HasColumnType("tinyint(1)"); + + b.Property("HealthCheckSeconds") + .IsRequired() + .HasColumnType("int unsigned"); + + b.Property("InstanceId") + .HasColumnType("bigint"); + + b.Property("LogOutput") + .IsRequired() + .HasColumnType("tinyint(1)"); + + b.Property("MapThreads") + .IsRequired() + .HasColumnType("int unsigned"); + + b.Property("Port") + .IsRequired() + .HasColumnType("smallint unsigned"); + + b.Property("SecurityLevel") + .HasColumnType("int"); + + b.Property("StartProfiler") + .IsRequired() + .HasColumnType("tinyint(1)"); + + b.Property("StartupTimeout") + .IsRequired() + .HasColumnType("int unsigned"); + + b.Property("TopicRequestTimeout") + .IsRequired() + .HasColumnType("int unsigned"); + + b.Property("Visibility") + .HasColumnType("int"); + + b.HasKey("Id"); + + b.HasIndex("InstanceId") + .IsUnique(); + + b.ToTable("DreamDaemonSettings"); + }); + + modelBuilder.Entity("Tgstation.Server.Host.Models.DreamMakerSettings", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + b.Property("ApiValidationPort") + .IsRequired() + .HasColumnType("smallint unsigned"); + + b.Property("ApiValidationSecurityLevel") + .HasColumnType("int"); + + b.Property("InstanceId") + .HasColumnType("bigint"); + + b.Property("ProjectName") + .HasMaxLength(10000) + .HasColumnType("longtext"); + + MySqlPropertyBuilderExtensions.HasCharSet(b.Property("ProjectName"), "utf8mb4"); + + b.Property("RequireDMApiValidation") + .IsRequired() + .HasColumnType("tinyint(1)"); + + b.Property("Timeout") + .IsRequired() + .HasColumnType("time(6)"); + + b.HasKey("Id"); + + b.HasIndex("InstanceId") + .IsUnique(); + + b.ToTable("DreamMakerSettings"); + }); + + modelBuilder.Entity("Tgstation.Server.Host.Models.Instance", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + b.Property("AutoUpdateInterval") + .IsRequired() + .HasColumnType("int unsigned"); + + b.Property("ChatBotLimit") + .IsRequired() + .HasColumnType("smallint unsigned"); + + b.Property("ConfigurationType") + .HasColumnType("int"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("varchar(100)"); + + MySqlPropertyBuilderExtensions.HasCharSet(b.Property("Name"), "utf8mb4"); + + b.Property("Online") + .IsRequired() + .HasColumnType("tinyint(1)"); + + b.Property("Path") + .IsRequired() + .HasColumnType("varchar(255)"); + + MySqlPropertyBuilderExtensions.HasCharSet(b.Property("Path"), "utf8mb4"); + + b.Property("SwarmIdentifer") + .HasColumnType("varchar(255)"); + + MySqlPropertyBuilderExtensions.HasCharSet(b.Property("SwarmIdentifer"), "utf8mb4"); + + b.HasKey("Id"); + + b.HasIndex("Path", "SwarmIdentifer") + .IsUnique(); + + b.ToTable("Instances"); + }); + + modelBuilder.Entity("Tgstation.Server.Host.Models.InstancePermissionSet", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + b.Property("ChatBotRights") + .HasColumnType("bigint unsigned"); + + b.Property("ConfigurationRights") + .HasColumnType("bigint unsigned"); + + b.Property("DreamDaemonRights") + .HasColumnType("bigint unsigned"); + + b.Property("DreamMakerRights") + .HasColumnType("bigint unsigned"); + + b.Property("EngineRights") + .HasColumnType("bigint unsigned"); + + b.Property("InstanceId") + .HasColumnType("bigint"); + + b.Property("InstancePermissionSetRights") + .HasColumnType("bigint unsigned"); + + b.Property("PermissionSetId") + .HasColumnType("bigint"); + + b.Property("RepositoryRights") + .HasColumnType("bigint unsigned"); + + b.HasKey("Id"); + + b.HasIndex("InstanceId"); + + b.HasIndex("PermissionSetId", "InstanceId") + .IsUnique(); + + b.ToTable("InstancePermissionSets"); + }); + + modelBuilder.Entity("Tgstation.Server.Host.Models.Job", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + b.Property("CancelRight") + .HasColumnType("bigint unsigned"); + + b.Property("CancelRightsType") + .HasColumnType("bigint unsigned"); + + b.Property("Cancelled") + .IsRequired() + .HasColumnType("tinyint(1)"); + + b.Property("CancelledById") + .HasColumnType("bigint"); + + b.Property("Description") + .IsRequired() + .HasColumnType("longtext"); + + MySqlPropertyBuilderExtensions.HasCharSet(b.Property("Description"), "utf8mb4"); + + b.Property("ErrorCode") + .HasColumnType("int unsigned"); + + b.Property("ExceptionDetails") + .HasColumnType("longtext"); + + MySqlPropertyBuilderExtensions.HasCharSet(b.Property("ExceptionDetails"), "utf8mb4"); + + b.Property("InstanceId") + .HasColumnType("bigint"); + + b.Property("JobCode") + .HasColumnType("tinyint unsigned"); + + b.Property("StartedAt") + .IsRequired() + .HasColumnType("datetime(6)"); + + b.Property("StartedById") + .HasColumnType("bigint"); + + b.Property("StoppedAt") + .HasColumnType("datetime(6)"); + + b.HasKey("Id"); + + b.HasIndex("CancelledById"); + + b.HasIndex("InstanceId"); + + b.HasIndex("StartedById"); + + b.ToTable("Jobs"); + }); + + modelBuilder.Entity("Tgstation.Server.Host.Models.OAuthConnection", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + b.Property("ExternalUserId") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("varchar(100)"); + + MySqlPropertyBuilderExtensions.HasCharSet(b.Property("ExternalUserId"), "utf8mb4"); + + b.Property("Provider") + .HasColumnType("int"); + + b.Property("UserId") + .HasColumnType("bigint"); + + b.HasKey("Id"); + + b.HasIndex("UserId"); + + b.HasIndex("Provider", "ExternalUserId") + .IsUnique(); + + b.ToTable("OAuthConnections"); + }); + + modelBuilder.Entity("Tgstation.Server.Host.Models.PermissionSet", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + b.Property("AdministrationRights") + .HasColumnType("bigint unsigned"); + + b.Property("GroupId") + .HasColumnType("bigint"); + + b.Property("InstanceManagerRights") + .HasColumnType("bigint unsigned"); + + b.Property("UserId") + .HasColumnType("bigint"); + + b.HasKey("Id"); + + b.HasIndex("GroupId") + .IsUnique(); + + b.HasIndex("UserId") + .IsUnique(); + + b.ToTable("PermissionSets"); + }); + + modelBuilder.Entity("Tgstation.Server.Host.Models.ReattachInformation", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + b.Property("AccessIdentifier") + .IsRequired() + .HasColumnType("longtext"); + + MySqlPropertyBuilderExtensions.HasCharSet(b.Property("AccessIdentifier"), "utf8mb4"); + + b.Property("CompileJobId") + .HasColumnType("bigint"); + + b.Property("InitialCompileJobId") + .HasColumnType("bigint"); + + b.Property("LaunchSecurityLevel") + .HasColumnType("int"); + + b.Property("LaunchVisibility") + .HasColumnType("int"); + + b.Property("Port") + .HasColumnType("smallint unsigned"); + + b.Property("ProcessId") + .HasColumnType("int"); + + b.Property("RebootState") + .HasColumnType("int"); + + b.HasKey("Id"); + + b.HasIndex("CompileJobId"); + + b.HasIndex("InitialCompileJobId"); + + b.ToTable("ReattachInformations"); + }); + + modelBuilder.Entity("Tgstation.Server.Host.Models.RepositorySettings", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + b.Property("AccessToken") + .HasMaxLength(10000) + .HasColumnType("longtext"); + + MySqlPropertyBuilderExtensions.HasCharSet(b.Property("AccessToken"), "utf8mb4"); + + b.Property("AccessUser") + .HasMaxLength(10000) + .HasColumnType("longtext"); + + MySqlPropertyBuilderExtensions.HasCharSet(b.Property("AccessUser"), "utf8mb4"); + + b.Property("AutoUpdatesKeepTestMerges") + .IsRequired() + .HasColumnType("tinyint(1)"); + + b.Property("AutoUpdatesSynchronize") + .IsRequired() + .HasColumnType("tinyint(1)"); + + b.Property("CommitterEmail") + .IsRequired() + .HasMaxLength(10000) + .HasColumnType("longtext"); + + MySqlPropertyBuilderExtensions.HasCharSet(b.Property("CommitterEmail"), "utf8mb4"); + + b.Property("CommitterName") + .IsRequired() + .HasMaxLength(10000) + .HasColumnType("longtext"); + + MySqlPropertyBuilderExtensions.HasCharSet(b.Property("CommitterName"), "utf8mb4"); + + b.Property("CreateGitHubDeployments") + .IsRequired() + .HasColumnType("tinyint(1)"); + + b.Property("InstanceId") + .HasColumnType("bigint"); + + b.Property("PostTestMergeComment") + .IsRequired() + .HasColumnType("tinyint(1)"); + + b.Property("PushTestMergeCommits") + .IsRequired() + .HasColumnType("tinyint(1)"); + + b.Property("ShowTestMergeCommitters") + .IsRequired() + .HasColumnType("tinyint(1)"); + + b.Property("UpdateSubmodules") + .IsRequired() + .HasColumnType("tinyint(1)"); + + b.HasKey("Id"); + + b.HasIndex("InstanceId") + .IsUnique(); + + b.ToTable("RepositorySettings"); + }); + + modelBuilder.Entity("Tgstation.Server.Host.Models.RevInfoTestMerge", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + b.Property("RevisionInformationId") + .HasColumnType("bigint"); + + b.Property("TestMergeId") + .HasColumnType("bigint"); + + b.HasKey("Id"); + + b.HasIndex("RevisionInformationId"); + + b.HasIndex("TestMergeId"); + + b.ToTable("RevInfoTestMerges"); + }); + + modelBuilder.Entity("Tgstation.Server.Host.Models.RevisionInformation", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + b.Property("CommitSha") + .IsRequired() + .HasMaxLength(40) + .HasColumnType("varchar(40)"); + + MySqlPropertyBuilderExtensions.HasCharSet(b.Property("CommitSha"), "utf8mb4"); + + b.Property("InstanceId") + .HasColumnType("bigint"); + + b.Property("OriginCommitSha") + .IsRequired() + .HasMaxLength(40) + .HasColumnType("varchar(40)"); + + MySqlPropertyBuilderExtensions.HasCharSet(b.Property("OriginCommitSha"), "utf8mb4"); + + b.Property("Timestamp") + .HasColumnType("datetime(6)"); + + b.HasKey("Id"); + + b.HasIndex("InstanceId", "CommitSha") + .IsUnique(); + + b.ToTable("RevisionInformations"); + }); + + modelBuilder.Entity("Tgstation.Server.Host.Models.TestMerge", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + b.Property("Author") + .IsRequired() + .HasColumnType("longtext"); + + MySqlPropertyBuilderExtensions.HasCharSet(b.Property("Author"), "utf8mb4"); + + b.Property("BodyAtMerge") + .IsRequired() + .HasColumnType("longtext"); + + MySqlPropertyBuilderExtensions.HasCharSet(b.Property("BodyAtMerge"), "utf8mb4"); + + b.Property("Comment") + .HasMaxLength(10000) + .HasColumnType("longtext"); + + MySqlPropertyBuilderExtensions.HasCharSet(b.Property("Comment"), "utf8mb4"); + + b.Property("MergedAt") + .HasColumnType("datetime(6)"); + + b.Property("MergedById") + .HasColumnType("bigint"); + + b.Property("Number") + .HasColumnType("int"); + + b.Property("PrimaryRevisionInformationId") + .IsRequired() + .HasColumnType("bigint"); + + b.Property("TargetCommitSha") + .IsRequired() + .HasMaxLength(40) + .HasColumnType("varchar(40)"); + + MySqlPropertyBuilderExtensions.HasCharSet(b.Property("TargetCommitSha"), "utf8mb4"); + + b.Property("TitleAtMerge") + .IsRequired() + .HasColumnType("longtext"); + + MySqlPropertyBuilderExtensions.HasCharSet(b.Property("TitleAtMerge"), "utf8mb4"); + + b.Property("Url") + .IsRequired() + .HasColumnType("longtext"); + + MySqlPropertyBuilderExtensions.HasCharSet(b.Property("Url"), "utf8mb4"); + + b.HasKey("Id"); + + b.HasIndex("MergedById"); + + b.HasIndex("PrimaryRevisionInformationId") + .IsUnique(); + + b.ToTable("TestMerges"); + }); + + modelBuilder.Entity("Tgstation.Server.Host.Models.User", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + b.Property("CanonicalName") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("varchar(100)"); + + MySqlPropertyBuilderExtensions.HasCharSet(b.Property("CanonicalName"), "utf8mb4"); + + b.Property("CreatedAt") + .IsRequired() + .HasColumnType("datetime(6)"); + + b.Property("CreatedById") + .HasColumnType("bigint"); + + b.Property("Enabled") + .IsRequired() + .HasColumnType("tinyint(1)"); + + b.Property("GroupId") + .HasColumnType("bigint"); + + b.Property("LastPasswordUpdate") + .HasColumnType("datetime(6)"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("varchar(100)"); + + MySqlPropertyBuilderExtensions.HasCharSet(b.Property("Name"), "utf8mb4"); + + b.Property("PasswordHash") + .HasColumnType("longtext"); + + MySqlPropertyBuilderExtensions.HasCharSet(b.Property("PasswordHash"), "utf8mb4"); + + b.Property("SystemIdentifier") + .HasMaxLength(100) + .HasColumnType("varchar(100)"); + + MySqlPropertyBuilderExtensions.HasCharSet(b.Property("SystemIdentifier"), "utf8mb4"); + + b.HasKey("Id"); + + b.HasIndex("CanonicalName") + .IsUnique(); + + b.HasIndex("CreatedById"); + + b.HasIndex("GroupId"); + + b.HasIndex("SystemIdentifier") + .IsUnique(); + + b.ToTable("Users"); + }); + + modelBuilder.Entity("Tgstation.Server.Host.Models.UserGroup", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("varchar(100)"); + + MySqlPropertyBuilderExtensions.HasCharSet(b.Property("Name"), "utf8mb4"); + + b.HasKey("Id"); + + b.HasIndex("Name") + .IsUnique(); + + b.ToTable("Groups"); + }); + + modelBuilder.Entity("Tgstation.Server.Host.Models.ChatBot", b => + { + b.HasOne("Tgstation.Server.Host.Models.Instance", "Instance") + .WithMany("ChatSettings") + .HasForeignKey("InstanceId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Instance"); + }); + + modelBuilder.Entity("Tgstation.Server.Host.Models.ChatChannel", b => + { + b.HasOne("Tgstation.Server.Host.Models.ChatBot", "ChatSettings") + .WithMany("Channels") + .HasForeignKey("ChatSettingsId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("ChatSettings"); + }); + + modelBuilder.Entity("Tgstation.Server.Host.Models.CompileJob", b => + { + b.HasOne("Tgstation.Server.Host.Models.Job", "Job") + .WithOne() + .HasForeignKey("Tgstation.Server.Host.Models.CompileJob", "JobId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Tgstation.Server.Host.Models.RevisionInformation", "RevisionInformation") + .WithMany("CompileJobs") + .HasForeignKey("RevisionInformationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Job"); + + b.Navigation("RevisionInformation"); + }); + + modelBuilder.Entity("Tgstation.Server.Host.Models.DreamDaemonSettings", b => + { + b.HasOne("Tgstation.Server.Host.Models.Instance", "Instance") + .WithOne("DreamDaemonSettings") + .HasForeignKey("Tgstation.Server.Host.Models.DreamDaemonSettings", "InstanceId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Instance"); + }); + + modelBuilder.Entity("Tgstation.Server.Host.Models.DreamMakerSettings", b => + { + b.HasOne("Tgstation.Server.Host.Models.Instance", "Instance") + .WithOne("DreamMakerSettings") + .HasForeignKey("Tgstation.Server.Host.Models.DreamMakerSettings", "InstanceId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Instance"); + }); + + modelBuilder.Entity("Tgstation.Server.Host.Models.InstancePermissionSet", b => + { + b.HasOne("Tgstation.Server.Host.Models.Instance", "Instance") + .WithMany("InstancePermissionSets") + .HasForeignKey("InstanceId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Tgstation.Server.Host.Models.PermissionSet", "PermissionSet") + .WithMany("InstancePermissionSets") + .HasForeignKey("PermissionSetId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Instance"); + + b.Navigation("PermissionSet"); + }); + + modelBuilder.Entity("Tgstation.Server.Host.Models.Job", b => + { + b.HasOne("Tgstation.Server.Host.Models.User", "CancelledBy") + .WithMany() + .HasForeignKey("CancelledById"); + + b.HasOne("Tgstation.Server.Host.Models.Instance", "Instance") + .WithMany("Jobs") + .HasForeignKey("InstanceId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Tgstation.Server.Host.Models.User", "StartedBy") + .WithMany() + .HasForeignKey("StartedById") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("CancelledBy"); + + b.Navigation("Instance"); + + b.Navigation("StartedBy"); + }); + + modelBuilder.Entity("Tgstation.Server.Host.Models.OAuthConnection", b => + { + b.HasOne("Tgstation.Server.Host.Models.User", "User") + .WithMany("OAuthConnections") + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Tgstation.Server.Host.Models.PermissionSet", b => + { + b.HasOne("Tgstation.Server.Host.Models.UserGroup", "Group") + .WithOne("PermissionSet") + .HasForeignKey("Tgstation.Server.Host.Models.PermissionSet", "GroupId") + .OnDelete(DeleteBehavior.Cascade); + + b.HasOne("Tgstation.Server.Host.Models.User", "User") + .WithOne("PermissionSet") + .HasForeignKey("Tgstation.Server.Host.Models.PermissionSet", "UserId") + .OnDelete(DeleteBehavior.Cascade); + + b.Navigation("Group"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Tgstation.Server.Host.Models.ReattachInformation", b => + { + b.HasOne("Tgstation.Server.Host.Models.CompileJob", "CompileJob") + .WithMany() + .HasForeignKey("CompileJobId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Tgstation.Server.Host.Models.CompileJob", "InitialCompileJob") + .WithMany() + .HasForeignKey("InitialCompileJobId"); + + b.Navigation("CompileJob"); + + b.Navigation("InitialCompileJob"); + }); + + modelBuilder.Entity("Tgstation.Server.Host.Models.RepositorySettings", b => + { + b.HasOne("Tgstation.Server.Host.Models.Instance", "Instance") + .WithOne("RepositorySettings") + .HasForeignKey("Tgstation.Server.Host.Models.RepositorySettings", "InstanceId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Instance"); + }); + + modelBuilder.Entity("Tgstation.Server.Host.Models.RevInfoTestMerge", b => + { + b.HasOne("Tgstation.Server.Host.Models.RevisionInformation", "RevisionInformation") + .WithMany("ActiveTestMerges") + .HasForeignKey("RevisionInformationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Tgstation.Server.Host.Models.TestMerge", "TestMerge") + .WithMany("RevisonInformations") + .HasForeignKey("TestMergeId") + .OnDelete(DeleteBehavior.ClientNoAction) + .IsRequired(); + + b.Navigation("RevisionInformation"); + + b.Navigation("TestMerge"); + }); + + modelBuilder.Entity("Tgstation.Server.Host.Models.RevisionInformation", b => + { + b.HasOne("Tgstation.Server.Host.Models.Instance", "Instance") + .WithMany("RevisionInformations") + .HasForeignKey("InstanceId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Instance"); + }); + + modelBuilder.Entity("Tgstation.Server.Host.Models.TestMerge", b => + { + b.HasOne("Tgstation.Server.Host.Models.User", "MergedBy") + .WithMany("TestMerges") + .HasForeignKey("MergedById") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("Tgstation.Server.Host.Models.RevisionInformation", "PrimaryRevisionInformation") + .WithOne("PrimaryTestMerge") + .HasForeignKey("Tgstation.Server.Host.Models.TestMerge", "PrimaryRevisionInformationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("MergedBy"); + + b.Navigation("PrimaryRevisionInformation"); + }); + + modelBuilder.Entity("Tgstation.Server.Host.Models.User", b => + { + b.HasOne("Tgstation.Server.Host.Models.User", "CreatedBy") + .WithMany("CreatedUsers") + .HasForeignKey("CreatedById"); + + b.HasOne("Tgstation.Server.Host.Models.UserGroup", "Group") + .WithMany("Users") + .HasForeignKey("GroupId"); + + b.Navigation("CreatedBy"); + + b.Navigation("Group"); + }); + + modelBuilder.Entity("Tgstation.Server.Host.Models.ChatBot", b => + { + b.Navigation("Channels"); + }); + + modelBuilder.Entity("Tgstation.Server.Host.Models.Instance", b => + { + b.Navigation("ChatSettings"); + + b.Navigation("DreamDaemonSettings"); + + b.Navigation("DreamMakerSettings"); + + b.Navigation("InstancePermissionSets"); + + b.Navigation("Jobs"); + + b.Navigation("RepositorySettings"); + + b.Navigation("RevisionInformations"); + }); + + modelBuilder.Entity("Tgstation.Server.Host.Models.PermissionSet", b => + { + b.Navigation("InstancePermissionSets"); + }); + + modelBuilder.Entity("Tgstation.Server.Host.Models.RevisionInformation", b => + { + b.Navigation("ActiveTestMerges"); + + b.Navigation("CompileJobs"); + + b.Navigation("PrimaryTestMerge"); + }); + + modelBuilder.Entity("Tgstation.Server.Host.Models.TestMerge", b => + { + b.Navigation("RevisonInformations"); + }); + + modelBuilder.Entity("Tgstation.Server.Host.Models.User", b => + { + b.Navigation("CreatedUsers"); + + b.Navigation("OAuthConnections"); + + b.Navigation("PermissionSet"); + + b.Navigation("TestMerges"); + }); + + modelBuilder.Entity("Tgstation.Server.Host.Models.UserGroup", b => + { + b.Navigation("PermissionSet") + .IsRequired(); + + b.Navigation("Users"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/src/Tgstation.Server.Host/Database/Migrations/20231108004356_MYRenameByondColumnsToEngine.cs b/src/Tgstation.Server.Host/Database/Migrations/20231108004356_MYRenameByondColumnsToEngine.cs new file mode 100644 index 00000000000..00a5d1fe1c2 --- /dev/null +++ b/src/Tgstation.Server.Host/Database/Migrations/20231108004356_MYRenameByondColumnsToEngine.cs @@ -0,0 +1,42 @@ +using System; + +using Microsoft.EntityFrameworkCore.Migrations; + +namespace Tgstation.Server.Host.Database.Migrations +{ + /// + public partial class MYRenameByondColumnsToEngine : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + ArgumentNullException.ThrowIfNull(migrationBuilder); + + migrationBuilder.RenameColumn( + name: "ByondRights", + table: "InstancePermissionSets", + newName: "EngineRights"); + + migrationBuilder.RenameColumn( + name: "ByondVersion", + table: "CompileJobs", + newName: "EngineVersion"); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + ArgumentNullException.ThrowIfNull(migrationBuilder); + + migrationBuilder.RenameColumn( + name: "EngineRights", + table: "InstancePermissionSets", + newName: "ByondRights"); + + migrationBuilder.RenameColumn( + name: "EngineVersion", + table: "CompileJobs", + newName: "ByondVersion"); + } + } +} diff --git a/src/Tgstation.Server.Host/Database/Migrations/20231108004402_PGRenameByondColumnsToEngine.Designer.cs b/src/Tgstation.Server.Host/Database/Migrations/20231108004402_PGRenameByondColumnsToEngine.Designer.cs new file mode 100644 index 00000000000..7f755e8876e --- /dev/null +++ b/src/Tgstation.Server.Host/Database/Migrations/20231108004402_PGRenameByondColumnsToEngine.Designer.cs @@ -0,0 +1,1068 @@ +// +using System; + +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Migrations; + +namespace Tgstation.Server.Host.Database.Migrations +{ + [DbContext(typeof(PostgresSqlDatabaseContext))] + [Migration("20231108004402_PGRenameByondColumnsToEngine")] + partial class PGRenameByondColumnsToEngine + { + /// + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("ProductVersion", "8.0.0-rc.1.23419.6") + .HasAnnotation("Relational:MaxIdentifierLength", 63); + + NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder); + + modelBuilder.Entity("Tgstation.Server.Host.Models.ChatBot", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("ChannelLimit") + .HasColumnType("integer"); + + b.Property("ConnectionString") + .IsRequired() + .HasMaxLength(10000) + .HasColumnType("character varying(10000)"); + + b.Property("Enabled") + .HasColumnType("boolean"); + + b.Property("InstanceId") + .HasColumnType("bigint"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("character varying(100)"); + + b.Property("Provider") + .HasColumnType("integer"); + + b.Property("ReconnectionInterval") + .HasColumnType("bigint"); + + b.HasKey("Id"); + + b.HasIndex("InstanceId", "Name") + .IsUnique(); + + b.ToTable("ChatBots"); + }); + + modelBuilder.Entity("Tgstation.Server.Host.Models.ChatChannel", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("ChatSettingsId") + .HasColumnType("bigint"); + + b.Property("DiscordChannelId") + .HasColumnType("numeric(20,0)"); + + b.Property("IrcChannel") + .HasMaxLength(100) + .HasColumnType("character varying(100)"); + + b.Property("IsAdminChannel") + .IsRequired() + .HasColumnType("boolean"); + + b.Property("IsSystemChannel") + .IsRequired() + .HasColumnType("boolean"); + + b.Property("IsUpdatesChannel") + .IsRequired() + .HasColumnType("boolean"); + + b.Property("IsWatchdogChannel") + .IsRequired() + .HasColumnType("boolean"); + + b.Property("Tag") + .HasMaxLength(10000) + .HasColumnType("character varying(10000)"); + + b.HasKey("Id"); + + b.HasIndex("ChatSettingsId", "DiscordChannelId") + .IsUnique(); + + b.HasIndex("ChatSettingsId", "IrcChannel") + .IsUnique(); + + b.ToTable("ChatChannels"); + }); + + modelBuilder.Entity("Tgstation.Server.Host.Models.CompileJob", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("ByondVersion") + .IsRequired() + .HasColumnType("text") + .HasColumnName("EngineVersion"); + + b.Property("DMApiMajorVersion") + .HasColumnType("integer"); + + b.Property("DMApiMinorVersion") + .HasColumnType("integer"); + + b.Property("DMApiPatchVersion") + .HasColumnType("integer"); + + b.Property("DirectoryName") + .IsRequired() + .HasColumnType("uuid"); + + b.Property("DmeName") + .IsRequired() + .HasColumnType("text"); + + b.Property("GitHubDeploymentId") + .HasColumnType("integer"); + + b.Property("GitHubRepoId") + .HasColumnType("bigint"); + + b.Property("JobId") + .HasColumnType("bigint"); + + b.Property("MinimumSecurityLevel") + .HasColumnType("integer"); + + b.Property("Output") + .IsRequired() + .HasColumnType("text"); + + b.Property("RepositoryOrigin") + .HasColumnType("text"); + + b.Property("RevisionInformationId") + .HasColumnType("bigint"); + + b.HasKey("Id"); + + b.HasIndex("DirectoryName"); + + b.HasIndex("JobId") + .IsUnique(); + + b.HasIndex("RevisionInformationId"); + + b.ToTable("CompileJobs"); + }); + + modelBuilder.Entity("Tgstation.Server.Host.Models.DreamDaemonSettings", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("AdditionalParameters") + .IsRequired() + .HasMaxLength(10000) + .HasColumnType("character varying(10000)"); + + b.Property("AllowWebClient") + .IsRequired() + .HasColumnType("boolean"); + + b.Property("AutoStart") + .IsRequired() + .HasColumnType("boolean"); + + b.Property("DumpOnHealthCheckRestart") + .IsRequired() + .HasColumnType("boolean"); + + b.Property("HealthCheckSeconds") + .HasColumnType("bigint"); + + b.Property("InstanceId") + .HasColumnType("bigint"); + + b.Property("LogOutput") + .IsRequired() + .HasColumnType("boolean"); + + b.Property("MapThreads") + .HasColumnType("bigint"); + + b.Property("Port") + .HasColumnType("integer"); + + b.Property("SecurityLevel") + .HasColumnType("integer"); + + b.Property("StartProfiler") + .IsRequired() + .HasColumnType("boolean"); + + b.Property("StartupTimeout") + .HasColumnType("bigint"); + + b.Property("TopicRequestTimeout") + .HasColumnType("bigint"); + + b.Property("Visibility") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.HasIndex("InstanceId") + .IsUnique(); + + b.ToTable("DreamDaemonSettings"); + }); + + modelBuilder.Entity("Tgstation.Server.Host.Models.DreamMakerSettings", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("ApiValidationPort") + .HasColumnType("integer"); + + b.Property("ApiValidationSecurityLevel") + .HasColumnType("integer"); + + b.Property("InstanceId") + .HasColumnType("bigint"); + + b.Property("ProjectName") + .HasMaxLength(10000) + .HasColumnType("character varying(10000)"); + + b.Property("RequireDMApiValidation") + .IsRequired() + .HasColumnType("boolean"); + + b.Property("Timeout") + .IsRequired() + .HasColumnType("interval"); + + b.HasKey("Id"); + + b.HasIndex("InstanceId") + .IsUnique(); + + b.ToTable("DreamMakerSettings"); + }); + + modelBuilder.Entity("Tgstation.Server.Host.Models.Instance", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("AutoUpdateInterval") + .HasColumnType("bigint"); + + b.Property("ChatBotLimit") + .HasColumnType("integer"); + + b.Property("ConfigurationType") + .HasColumnType("integer"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("character varying(100)"); + + b.Property("Online") + .IsRequired() + .HasColumnType("boolean"); + + b.Property("Path") + .IsRequired() + .HasColumnType("text"); + + b.Property("SwarmIdentifer") + .HasColumnType("text"); + + b.HasKey("Id"); + + b.HasIndex("Path", "SwarmIdentifer") + .IsUnique(); + + b.ToTable("Instances"); + }); + + modelBuilder.Entity("Tgstation.Server.Host.Models.InstancePermissionSet", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("ChatBotRights") + .HasColumnType("numeric(20,0)"); + + b.Property("ConfigurationRights") + .HasColumnType("numeric(20,0)"); + + b.Property("DreamDaemonRights") + .HasColumnType("numeric(20,0)"); + + b.Property("DreamMakerRights") + .HasColumnType("numeric(20,0)"); + + b.Property("EngineRights") + .HasColumnType("numeric(20,0)"); + + b.Property("InstanceId") + .HasColumnType("bigint"); + + b.Property("InstancePermissionSetRights") + .HasColumnType("numeric(20,0)"); + + b.Property("PermissionSetId") + .HasColumnType("bigint"); + + b.Property("RepositoryRights") + .HasColumnType("numeric(20,0)"); + + b.HasKey("Id"); + + b.HasIndex("InstanceId"); + + b.HasIndex("PermissionSetId", "InstanceId") + .IsUnique(); + + b.ToTable("InstancePermissionSets"); + }); + + modelBuilder.Entity("Tgstation.Server.Host.Models.Job", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("CancelRight") + .HasColumnType("numeric(20,0)"); + + b.Property("CancelRightsType") + .HasColumnType("numeric(20,0)"); + + b.Property("Cancelled") + .IsRequired() + .HasColumnType("boolean"); + + b.Property("CancelledById") + .HasColumnType("bigint"); + + b.Property("Description") + .IsRequired() + .HasColumnType("text"); + + b.Property("ErrorCode") + .HasColumnType("bigint"); + + b.Property("ExceptionDetails") + .HasColumnType("text"); + + b.Property("InstanceId") + .HasColumnType("bigint"); + + b.Property("JobCode") + .HasColumnType("smallint"); + + b.Property("StartedAt") + .IsRequired() + .HasColumnType("timestamp with time zone"); + + b.Property("StartedById") + .HasColumnType("bigint"); + + b.Property("StoppedAt") + .HasColumnType("timestamp with time zone"); + + b.HasKey("Id"); + + b.HasIndex("CancelledById"); + + b.HasIndex("InstanceId"); + + b.HasIndex("StartedById"); + + b.ToTable("Jobs"); + }); + + modelBuilder.Entity("Tgstation.Server.Host.Models.OAuthConnection", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("ExternalUserId") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("character varying(100)"); + + b.Property("Provider") + .HasColumnType("integer"); + + b.Property("UserId") + .HasColumnType("bigint"); + + b.HasKey("Id"); + + b.HasIndex("UserId"); + + b.HasIndex("Provider", "ExternalUserId") + .IsUnique(); + + b.ToTable("OAuthConnections"); + }); + + modelBuilder.Entity("Tgstation.Server.Host.Models.PermissionSet", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("AdministrationRights") + .HasColumnType("numeric(20,0)"); + + b.Property("GroupId") + .HasColumnType("bigint"); + + b.Property("InstanceManagerRights") + .HasColumnType("numeric(20,0)"); + + b.Property("UserId") + .HasColumnType("bigint"); + + b.HasKey("Id"); + + b.HasIndex("GroupId") + .IsUnique(); + + b.HasIndex("UserId") + .IsUnique(); + + b.ToTable("PermissionSets"); + }); + + modelBuilder.Entity("Tgstation.Server.Host.Models.ReattachInformation", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("AccessIdentifier") + .IsRequired() + .HasColumnType("text"); + + b.Property("CompileJobId") + .HasColumnType("bigint"); + + b.Property("InitialCompileJobId") + .HasColumnType("bigint"); + + b.Property("LaunchSecurityLevel") + .HasColumnType("integer"); + + b.Property("LaunchVisibility") + .HasColumnType("integer"); + + b.Property("Port") + .HasColumnType("integer"); + + b.Property("ProcessId") + .HasColumnType("integer"); + + b.Property("RebootState") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.HasIndex("CompileJobId"); + + b.HasIndex("InitialCompileJobId"); + + b.ToTable("ReattachInformations"); + }); + + modelBuilder.Entity("Tgstation.Server.Host.Models.RepositorySettings", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("AccessToken") + .HasMaxLength(10000) + .HasColumnType("character varying(10000)"); + + b.Property("AccessUser") + .HasMaxLength(10000) + .HasColumnType("character varying(10000)"); + + b.Property("AutoUpdatesKeepTestMerges") + .IsRequired() + .HasColumnType("boolean"); + + b.Property("AutoUpdatesSynchronize") + .IsRequired() + .HasColumnType("boolean"); + + b.Property("CommitterEmail") + .IsRequired() + .HasMaxLength(10000) + .HasColumnType("character varying(10000)"); + + b.Property("CommitterName") + .IsRequired() + .HasMaxLength(10000) + .HasColumnType("character varying(10000)"); + + b.Property("CreateGitHubDeployments") + .IsRequired() + .HasColumnType("boolean"); + + b.Property("InstanceId") + .HasColumnType("bigint"); + + b.Property("PostTestMergeComment") + .IsRequired() + .HasColumnType("boolean"); + + b.Property("PushTestMergeCommits") + .IsRequired() + .HasColumnType("boolean"); + + b.Property("ShowTestMergeCommitters") + .IsRequired() + .HasColumnType("boolean"); + + b.Property("UpdateSubmodules") + .IsRequired() + .HasColumnType("boolean"); + + b.HasKey("Id"); + + b.HasIndex("InstanceId") + .IsUnique(); + + b.ToTable("RepositorySettings"); + }); + + modelBuilder.Entity("Tgstation.Server.Host.Models.RevInfoTestMerge", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("RevisionInformationId") + .HasColumnType("bigint"); + + b.Property("TestMergeId") + .HasColumnType("bigint"); + + b.HasKey("Id"); + + b.HasIndex("RevisionInformationId"); + + b.HasIndex("TestMergeId"); + + b.ToTable("RevInfoTestMerges"); + }); + + modelBuilder.Entity("Tgstation.Server.Host.Models.RevisionInformation", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("CommitSha") + .IsRequired() + .HasMaxLength(40) + .HasColumnType("character varying(40)"); + + b.Property("InstanceId") + .HasColumnType("bigint"); + + b.Property("OriginCommitSha") + .IsRequired() + .HasMaxLength(40) + .HasColumnType("character varying(40)"); + + b.Property("Timestamp") + .HasColumnType("timestamp with time zone"); + + b.HasKey("Id"); + + b.HasIndex("InstanceId", "CommitSha") + .IsUnique(); + + b.ToTable("RevisionInformations"); + }); + + modelBuilder.Entity("Tgstation.Server.Host.Models.TestMerge", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("Author") + .IsRequired() + .HasColumnType("text"); + + b.Property("BodyAtMerge") + .IsRequired() + .HasColumnType("text"); + + b.Property("Comment") + .HasMaxLength(10000) + .HasColumnType("character varying(10000)"); + + b.Property("MergedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("MergedById") + .HasColumnType("bigint"); + + b.Property("Number") + .HasColumnType("integer"); + + b.Property("PrimaryRevisionInformationId") + .IsRequired() + .HasColumnType("bigint"); + + b.Property("TargetCommitSha") + .IsRequired() + .HasMaxLength(40) + .HasColumnType("character varying(40)"); + + b.Property("TitleAtMerge") + .IsRequired() + .HasColumnType("text"); + + b.Property("Url") + .IsRequired() + .HasColumnType("text"); + + b.HasKey("Id"); + + b.HasIndex("MergedById"); + + b.HasIndex("PrimaryRevisionInformationId") + .IsUnique(); + + b.ToTable("TestMerges"); + }); + + modelBuilder.Entity("Tgstation.Server.Host.Models.User", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("CanonicalName") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("character varying(100)"); + + b.Property("CreatedAt") + .IsRequired() + .HasColumnType("timestamp with time zone"); + + b.Property("CreatedById") + .HasColumnType("bigint"); + + b.Property("Enabled") + .IsRequired() + .HasColumnType("boolean"); + + b.Property("GroupId") + .HasColumnType("bigint"); + + b.Property("LastPasswordUpdate") + .HasColumnType("timestamp with time zone"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("character varying(100)"); + + b.Property("PasswordHash") + .HasColumnType("text"); + + b.Property("SystemIdentifier") + .HasMaxLength(100) + .HasColumnType("character varying(100)"); + + b.HasKey("Id"); + + b.HasIndex("CanonicalName") + .IsUnique(); + + b.HasIndex("CreatedById"); + + b.HasIndex("GroupId"); + + b.HasIndex("SystemIdentifier") + .IsUnique(); + + b.ToTable("Users"); + }); + + modelBuilder.Entity("Tgstation.Server.Host.Models.UserGroup", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("Name") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("character varying(100)"); + + b.HasKey("Id"); + + b.HasIndex("Name") + .IsUnique(); + + b.ToTable("Groups"); + }); + + modelBuilder.Entity("Tgstation.Server.Host.Models.ChatBot", b => + { + b.HasOne("Tgstation.Server.Host.Models.Instance", "Instance") + .WithMany("ChatSettings") + .HasForeignKey("InstanceId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Instance"); + }); + + modelBuilder.Entity("Tgstation.Server.Host.Models.ChatChannel", b => + { + b.HasOne("Tgstation.Server.Host.Models.ChatBot", "ChatSettings") + .WithMany("Channels") + .HasForeignKey("ChatSettingsId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("ChatSettings"); + }); + + modelBuilder.Entity("Tgstation.Server.Host.Models.CompileJob", b => + { + b.HasOne("Tgstation.Server.Host.Models.Job", "Job") + .WithOne() + .HasForeignKey("Tgstation.Server.Host.Models.CompileJob", "JobId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Tgstation.Server.Host.Models.RevisionInformation", "RevisionInformation") + .WithMany("CompileJobs") + .HasForeignKey("RevisionInformationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Job"); + + b.Navigation("RevisionInformation"); + }); + + modelBuilder.Entity("Tgstation.Server.Host.Models.DreamDaemonSettings", b => + { + b.HasOne("Tgstation.Server.Host.Models.Instance", "Instance") + .WithOne("DreamDaemonSettings") + .HasForeignKey("Tgstation.Server.Host.Models.DreamDaemonSettings", "InstanceId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Instance"); + }); + + modelBuilder.Entity("Tgstation.Server.Host.Models.DreamMakerSettings", b => + { + b.HasOne("Tgstation.Server.Host.Models.Instance", "Instance") + .WithOne("DreamMakerSettings") + .HasForeignKey("Tgstation.Server.Host.Models.DreamMakerSettings", "InstanceId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Instance"); + }); + + modelBuilder.Entity("Tgstation.Server.Host.Models.InstancePermissionSet", b => + { + b.HasOne("Tgstation.Server.Host.Models.Instance", "Instance") + .WithMany("InstancePermissionSets") + .HasForeignKey("InstanceId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Tgstation.Server.Host.Models.PermissionSet", "PermissionSet") + .WithMany("InstancePermissionSets") + .HasForeignKey("PermissionSetId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Instance"); + + b.Navigation("PermissionSet"); + }); + + modelBuilder.Entity("Tgstation.Server.Host.Models.Job", b => + { + b.HasOne("Tgstation.Server.Host.Models.User", "CancelledBy") + .WithMany() + .HasForeignKey("CancelledById"); + + b.HasOne("Tgstation.Server.Host.Models.Instance", "Instance") + .WithMany("Jobs") + .HasForeignKey("InstanceId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Tgstation.Server.Host.Models.User", "StartedBy") + .WithMany() + .HasForeignKey("StartedById") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("CancelledBy"); + + b.Navigation("Instance"); + + b.Navigation("StartedBy"); + }); + + modelBuilder.Entity("Tgstation.Server.Host.Models.OAuthConnection", b => + { + b.HasOne("Tgstation.Server.Host.Models.User", "User") + .WithMany("OAuthConnections") + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Tgstation.Server.Host.Models.PermissionSet", b => + { + b.HasOne("Tgstation.Server.Host.Models.UserGroup", "Group") + .WithOne("PermissionSet") + .HasForeignKey("Tgstation.Server.Host.Models.PermissionSet", "GroupId") + .OnDelete(DeleteBehavior.Cascade); + + b.HasOne("Tgstation.Server.Host.Models.User", "User") + .WithOne("PermissionSet") + .HasForeignKey("Tgstation.Server.Host.Models.PermissionSet", "UserId") + .OnDelete(DeleteBehavior.Cascade); + + b.Navigation("Group"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Tgstation.Server.Host.Models.ReattachInformation", b => + { + b.HasOne("Tgstation.Server.Host.Models.CompileJob", "CompileJob") + .WithMany() + .HasForeignKey("CompileJobId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Tgstation.Server.Host.Models.CompileJob", "InitialCompileJob") + .WithMany() + .HasForeignKey("InitialCompileJobId"); + + b.Navigation("CompileJob"); + + b.Navigation("InitialCompileJob"); + }); + + modelBuilder.Entity("Tgstation.Server.Host.Models.RepositorySettings", b => + { + b.HasOne("Tgstation.Server.Host.Models.Instance", "Instance") + .WithOne("RepositorySettings") + .HasForeignKey("Tgstation.Server.Host.Models.RepositorySettings", "InstanceId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Instance"); + }); + + modelBuilder.Entity("Tgstation.Server.Host.Models.RevInfoTestMerge", b => + { + b.HasOne("Tgstation.Server.Host.Models.RevisionInformation", "RevisionInformation") + .WithMany("ActiveTestMerges") + .HasForeignKey("RevisionInformationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Tgstation.Server.Host.Models.TestMerge", "TestMerge") + .WithMany("RevisonInformations") + .HasForeignKey("TestMergeId") + .OnDelete(DeleteBehavior.ClientNoAction) + .IsRequired(); + + b.Navigation("RevisionInformation"); + + b.Navigation("TestMerge"); + }); + + modelBuilder.Entity("Tgstation.Server.Host.Models.RevisionInformation", b => + { + b.HasOne("Tgstation.Server.Host.Models.Instance", "Instance") + .WithMany("RevisionInformations") + .HasForeignKey("InstanceId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Instance"); + }); + + modelBuilder.Entity("Tgstation.Server.Host.Models.TestMerge", b => + { + b.HasOne("Tgstation.Server.Host.Models.User", "MergedBy") + .WithMany("TestMerges") + .HasForeignKey("MergedById") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("Tgstation.Server.Host.Models.RevisionInformation", "PrimaryRevisionInformation") + .WithOne("PrimaryTestMerge") + .HasForeignKey("Tgstation.Server.Host.Models.TestMerge", "PrimaryRevisionInformationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("MergedBy"); + + b.Navigation("PrimaryRevisionInformation"); + }); + + modelBuilder.Entity("Tgstation.Server.Host.Models.User", b => + { + b.HasOne("Tgstation.Server.Host.Models.User", "CreatedBy") + .WithMany("CreatedUsers") + .HasForeignKey("CreatedById"); + + b.HasOne("Tgstation.Server.Host.Models.UserGroup", "Group") + .WithMany("Users") + .HasForeignKey("GroupId"); + + b.Navigation("CreatedBy"); + + b.Navigation("Group"); + }); + + modelBuilder.Entity("Tgstation.Server.Host.Models.ChatBot", b => + { + b.Navigation("Channels"); + }); + + modelBuilder.Entity("Tgstation.Server.Host.Models.Instance", b => + { + b.Navigation("ChatSettings"); + + b.Navigation("DreamDaemonSettings"); + + b.Navigation("DreamMakerSettings"); + + b.Navigation("InstancePermissionSets"); + + b.Navigation("Jobs"); + + b.Navigation("RepositorySettings"); + + b.Navigation("RevisionInformations"); + }); + + modelBuilder.Entity("Tgstation.Server.Host.Models.PermissionSet", b => + { + b.Navigation("InstancePermissionSets"); + }); + + modelBuilder.Entity("Tgstation.Server.Host.Models.RevisionInformation", b => + { + b.Navigation("ActiveTestMerges"); + + b.Navigation("CompileJobs"); + + b.Navigation("PrimaryTestMerge"); + }); + + modelBuilder.Entity("Tgstation.Server.Host.Models.TestMerge", b => + { + b.Navigation("RevisonInformations"); + }); + + modelBuilder.Entity("Tgstation.Server.Host.Models.User", b => + { + b.Navigation("CreatedUsers"); + + b.Navigation("OAuthConnections"); + + b.Navigation("PermissionSet"); + + b.Navigation("TestMerges"); + }); + + modelBuilder.Entity("Tgstation.Server.Host.Models.UserGroup", b => + { + b.Navigation("PermissionSet") + .IsRequired(); + + b.Navigation("Users"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/src/Tgstation.Server.Host/Database/Migrations/20231108004402_PGRenameByondColumnsToEngine.cs b/src/Tgstation.Server.Host/Database/Migrations/20231108004402_PGRenameByondColumnsToEngine.cs new file mode 100644 index 00000000000..4766b285e80 --- /dev/null +++ b/src/Tgstation.Server.Host/Database/Migrations/20231108004402_PGRenameByondColumnsToEngine.cs @@ -0,0 +1,240 @@ +using System; + +using Microsoft.EntityFrameworkCore.Migrations; + +namespace Tgstation.Server.Host.Database.Migrations +{ + /// + public partial class PGRenameByondColumnsToEngine : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + ArgumentNullException.ThrowIfNull(migrationBuilder); + + migrationBuilder.RenameColumn( + name: "ByondRights", + table: "InstancePermissionSets", + newName: "EngineRights"); + + migrationBuilder.RenameColumn( + name: "ByondVersion", + table: "CompileJobs", + newName: "EngineVersion"); + + migrationBuilder.AlterColumn( + name: "InstanceManagerRights", + table: "PermissionSets", + type: "numeric(20,0)", + nullable: false, + oldClrType: typeof(decimal), + oldType: "numeric(20)"); + + migrationBuilder.AlterColumn( + name: "AdministrationRights", + table: "PermissionSets", + type: "numeric(20,0)", + nullable: false, + oldClrType: typeof(decimal), + oldType: "numeric(20)"); + + migrationBuilder.AlterColumn( + name: "CancelRightsType", + table: "Jobs", + type: "numeric(20,0)", + nullable: true, + oldClrType: typeof(decimal), + oldType: "numeric(20)", + oldNullable: true); + + migrationBuilder.AlterColumn( + name: "CancelRight", + table: "Jobs", + type: "numeric(20,0)", + nullable: true, + oldClrType: typeof(decimal), + oldType: "numeric(20)", + oldNullable: true); + + migrationBuilder.AlterColumn( + name: "RepositoryRights", + table: "InstancePermissionSets", + type: "numeric(20,0)", + nullable: false, + oldClrType: typeof(decimal), + oldType: "numeric(20)"); + + migrationBuilder.AlterColumn( + name: "InstancePermissionSetRights", + table: "InstancePermissionSets", + type: "numeric(20,0)", + nullable: false, + oldClrType: typeof(decimal), + oldType: "numeric(20)"); + + migrationBuilder.AlterColumn( + name: "DreamMakerRights", + table: "InstancePermissionSets", + type: "numeric(20,0)", + nullable: false, + oldClrType: typeof(decimal), + oldType: "numeric(20)"); + + migrationBuilder.AlterColumn( + name: "DreamDaemonRights", + table: "InstancePermissionSets", + type: "numeric(20,0)", + nullable: false, + oldClrType: typeof(decimal), + oldType: "numeric(20)"); + + migrationBuilder.AlterColumn( + name: "ConfigurationRights", + table: "InstancePermissionSets", + type: "numeric(20,0)", + nullable: false, + oldClrType: typeof(decimal), + oldType: "numeric(20)"); + + migrationBuilder.AlterColumn( + name: "ChatBotRights", + table: "InstancePermissionSets", + type: "numeric(20,0)", + nullable: false, + oldClrType: typeof(decimal), + oldType: "numeric(20)"); + + migrationBuilder.AlterColumn( + name: "EngineRights", + table: "InstancePermissionSets", + type: "numeric(20,0)", + nullable: false, + oldClrType: typeof(decimal), + oldType: "numeric(20)"); + + migrationBuilder.AlterColumn( + name: "DiscordChannelId", + table: "ChatChannels", + type: "numeric(20,0)", + nullable: true, + oldClrType: typeof(decimal), + oldType: "numeric(20)", + oldNullable: true); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + ArgumentNullException.ThrowIfNull(migrationBuilder); + + migrationBuilder.RenameColumn( + name: "EngineRights", + table: "InstancePermissionSets", + newName: "ByondRights"); + + migrationBuilder.RenameColumn( + name: "EngineVersion", + table: "CompileJobs", + newName: "ByondVersion"); + + migrationBuilder.AlterColumn( + name: "InstanceManagerRights", + table: "PermissionSets", + type: "numeric(20)", + nullable: false, + oldClrType: typeof(decimal), + oldType: "numeric(20,0)"); + + migrationBuilder.AlterColumn( + name: "AdministrationRights", + table: "PermissionSets", + type: "numeric(20)", + nullable: false, + oldClrType: typeof(decimal), + oldType: "numeric(20,0)"); + + migrationBuilder.AlterColumn( + name: "CancelRightsType", + table: "Jobs", + type: "numeric(20)", + nullable: true, + oldClrType: typeof(decimal), + oldType: "numeric(20,0)", + oldNullable: true); + + migrationBuilder.AlterColumn( + name: "CancelRight", + table: "Jobs", + type: "numeric(20)", + nullable: true, + oldClrType: typeof(decimal), + oldType: "numeric(20,0)", + oldNullable: true); + + migrationBuilder.AlterColumn( + name: "RepositoryRights", + table: "InstancePermissionSets", + type: "numeric(20)", + nullable: false, + oldClrType: typeof(decimal), + oldType: "numeric(20,0)"); + + migrationBuilder.AlterColumn( + name: "InstancePermissionSetRights", + table: "InstancePermissionSets", + type: "numeric(20)", + nullable: false, + oldClrType: typeof(decimal), + oldType: "numeric(20,0)"); + + migrationBuilder.AlterColumn( + name: "DreamMakerRights", + table: "InstancePermissionSets", + type: "numeric(20)", + nullable: false, + oldClrType: typeof(decimal), + oldType: "numeric(20,0)"); + + migrationBuilder.AlterColumn( + name: "DreamDaemonRights", + table: "InstancePermissionSets", + type: "numeric(20)", + nullable: false, + oldClrType: typeof(decimal), + oldType: "numeric(20,0)"); + + migrationBuilder.AlterColumn( + name: "ConfigurationRights", + table: "InstancePermissionSets", + type: "numeric(20)", + nullable: false, + oldClrType: typeof(decimal), + oldType: "numeric(20,0)"); + + migrationBuilder.AlterColumn( + name: "ChatBotRights", + table: "InstancePermissionSets", + type: "numeric(20)", + nullable: false, + oldClrType: typeof(decimal), + oldType: "numeric(20,0)"); + + migrationBuilder.AlterColumn( + name: "ByondRights", + table: "InstancePermissionSets", + type: "numeric(20)", + nullable: false, + oldClrType: typeof(decimal), + oldType: "numeric(20,0)"); + + migrationBuilder.AlterColumn( + name: "DiscordChannelId", + table: "ChatChannels", + type: "numeric(20)", + nullable: true, + oldClrType: typeof(decimal), + oldType: "numeric(20,0)", + oldNullable: true); + } + } +} diff --git a/src/Tgstation.Server.Host/Database/Migrations/20231108004409_SLRenameByondColumnsToEngine.Designer.cs b/src/Tgstation.Server.Host/Database/Migrations/20231108004409_SLRenameByondColumnsToEngine.Designer.cs new file mode 100644 index 00000000000..dae9036d079 --- /dev/null +++ b/src/Tgstation.Server.Host/Database/Migrations/20231108004409_SLRenameByondColumnsToEngine.Designer.cs @@ -0,0 +1,1040 @@ +// +using System; + +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Migrations; + +namespace Tgstation.Server.Host.Database.Migrations +{ + [DbContext(typeof(SqliteDatabaseContext))] + [Migration("20231108004409_SLRenameByondColumnsToEngine")] + partial class SLRenameByondColumnsToEngine + { + /// + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder.HasAnnotation("ProductVersion", "8.0.0-rc.1.23419.6"); + + modelBuilder.Entity("Tgstation.Server.Host.Models.ChatBot", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("ChannelLimit") + .IsRequired() + .HasColumnType("INTEGER"); + + b.Property("ConnectionString") + .IsRequired() + .HasMaxLength(10000) + .HasColumnType("TEXT"); + + b.Property("Enabled") + .HasColumnType("INTEGER"); + + b.Property("InstanceId") + .HasColumnType("INTEGER"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("TEXT"); + + b.Property("Provider") + .HasColumnType("INTEGER"); + + b.Property("ReconnectionInterval") + .IsRequired() + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("InstanceId", "Name") + .IsUnique(); + + b.ToTable("ChatBots"); + }); + + modelBuilder.Entity("Tgstation.Server.Host.Models.ChatChannel", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("ChatSettingsId") + .HasColumnType("INTEGER"); + + b.Property("DiscordChannelId") + .HasColumnType("INTEGER"); + + b.Property("IrcChannel") + .HasMaxLength(100) + .HasColumnType("TEXT"); + + b.Property("IsAdminChannel") + .IsRequired() + .HasColumnType("INTEGER"); + + b.Property("IsSystemChannel") + .IsRequired() + .HasColumnType("INTEGER"); + + b.Property("IsUpdatesChannel") + .IsRequired() + .HasColumnType("INTEGER"); + + b.Property("IsWatchdogChannel") + .IsRequired() + .HasColumnType("INTEGER"); + + b.Property("Tag") + .HasMaxLength(10000) + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("ChatSettingsId", "DiscordChannelId") + .IsUnique(); + + b.HasIndex("ChatSettingsId", "IrcChannel") + .IsUnique(); + + b.ToTable("ChatChannels"); + }); + + modelBuilder.Entity("Tgstation.Server.Host.Models.CompileJob", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("ByondVersion") + .IsRequired() + .HasColumnType("TEXT") + .HasColumnName("EngineVersion"); + + b.Property("DMApiMajorVersion") + .HasColumnType("INTEGER"); + + b.Property("DMApiMinorVersion") + .HasColumnType("INTEGER"); + + b.Property("DMApiPatchVersion") + .HasColumnType("INTEGER"); + + b.Property("DirectoryName") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("DmeName") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("GitHubDeploymentId") + .HasColumnType("INTEGER"); + + b.Property("GitHubRepoId") + .HasColumnType("INTEGER"); + + b.Property("JobId") + .HasColumnType("INTEGER"); + + b.Property("MinimumSecurityLevel") + .HasColumnType("INTEGER"); + + b.Property("Output") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("RepositoryOrigin") + .HasColumnType("TEXT"); + + b.Property("RevisionInformationId") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("DirectoryName"); + + b.HasIndex("JobId") + .IsUnique(); + + b.HasIndex("RevisionInformationId"); + + b.ToTable("CompileJobs"); + }); + + modelBuilder.Entity("Tgstation.Server.Host.Models.DreamDaemonSettings", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("AdditionalParameters") + .IsRequired() + .HasMaxLength(10000) + .HasColumnType("TEXT"); + + b.Property("AllowWebClient") + .IsRequired() + .HasColumnType("INTEGER"); + + b.Property("AutoStart") + .IsRequired() + .HasColumnType("INTEGER"); + + b.Property("DumpOnHealthCheckRestart") + .IsRequired() + .HasColumnType("INTEGER"); + + b.Property("HealthCheckSeconds") + .IsRequired() + .HasColumnType("INTEGER"); + + b.Property("InstanceId") + .HasColumnType("INTEGER"); + + b.Property("LogOutput") + .IsRequired() + .HasColumnType("INTEGER"); + + b.Property("MapThreads") + .IsRequired() + .HasColumnType("INTEGER"); + + b.Property("Port") + .IsRequired() + .HasColumnType("INTEGER"); + + b.Property("SecurityLevel") + .HasColumnType("INTEGER"); + + b.Property("StartProfiler") + .IsRequired() + .HasColumnType("INTEGER"); + + b.Property("StartupTimeout") + .IsRequired() + .HasColumnType("INTEGER"); + + b.Property("TopicRequestTimeout") + .IsRequired() + .HasColumnType("INTEGER"); + + b.Property("Visibility") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("InstanceId") + .IsUnique(); + + b.ToTable("DreamDaemonSettings"); + }); + + modelBuilder.Entity("Tgstation.Server.Host.Models.DreamMakerSettings", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("ApiValidationPort") + .IsRequired() + .HasColumnType("INTEGER"); + + b.Property("ApiValidationSecurityLevel") + .HasColumnType("INTEGER"); + + b.Property("InstanceId") + .HasColumnType("INTEGER"); + + b.Property("ProjectName") + .HasMaxLength(10000) + .HasColumnType("TEXT"); + + b.Property("RequireDMApiValidation") + .IsRequired() + .HasColumnType("INTEGER"); + + b.Property("Timeout") + .IsRequired() + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("InstanceId") + .IsUnique(); + + b.ToTable("DreamMakerSettings"); + }); + + modelBuilder.Entity("Tgstation.Server.Host.Models.Instance", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("AutoUpdateInterval") + .IsRequired() + .HasColumnType("INTEGER"); + + b.Property("ChatBotLimit") + .IsRequired() + .HasColumnType("INTEGER"); + + b.Property("ConfigurationType") + .HasColumnType("INTEGER"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("TEXT"); + + b.Property("Online") + .IsRequired() + .HasColumnType("INTEGER"); + + b.Property("Path") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("SwarmIdentifer") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("Path", "SwarmIdentifer") + .IsUnique(); + + b.ToTable("Instances"); + }); + + modelBuilder.Entity("Tgstation.Server.Host.Models.InstancePermissionSet", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("ChatBotRights") + .HasColumnType("INTEGER"); + + b.Property("ConfigurationRights") + .HasColumnType("INTEGER"); + + b.Property("DreamDaemonRights") + .HasColumnType("INTEGER"); + + b.Property("DreamMakerRights") + .HasColumnType("INTEGER"); + + b.Property("EngineRights") + .HasColumnType("INTEGER"); + + b.Property("InstanceId") + .HasColumnType("INTEGER"); + + b.Property("InstancePermissionSetRights") + .HasColumnType("INTEGER"); + + b.Property("PermissionSetId") + .HasColumnType("INTEGER"); + + b.Property("RepositoryRights") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("InstanceId"); + + b.HasIndex("PermissionSetId", "InstanceId") + .IsUnique(); + + b.ToTable("InstancePermissionSets"); + }); + + modelBuilder.Entity("Tgstation.Server.Host.Models.Job", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("CancelRight") + .HasColumnType("INTEGER"); + + b.Property("CancelRightsType") + .HasColumnType("INTEGER"); + + b.Property("Cancelled") + .IsRequired() + .HasColumnType("INTEGER"); + + b.Property("CancelledById") + .HasColumnType("INTEGER"); + + b.Property("Description") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("ErrorCode") + .HasColumnType("INTEGER"); + + b.Property("ExceptionDetails") + .HasColumnType("TEXT"); + + b.Property("InstanceId") + .HasColumnType("INTEGER"); + + b.Property("JobCode") + .HasColumnType("INTEGER"); + + b.Property("StartedAt") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("StartedById") + .HasColumnType("INTEGER"); + + b.Property("StoppedAt") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("CancelledById"); + + b.HasIndex("InstanceId"); + + b.HasIndex("StartedById"); + + b.ToTable("Jobs"); + }); + + modelBuilder.Entity("Tgstation.Server.Host.Models.OAuthConnection", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("ExternalUserId") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("TEXT"); + + b.Property("Provider") + .HasColumnType("INTEGER"); + + b.Property("UserId") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("UserId"); + + b.HasIndex("Provider", "ExternalUserId") + .IsUnique(); + + b.ToTable("OAuthConnections"); + }); + + modelBuilder.Entity("Tgstation.Server.Host.Models.PermissionSet", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("AdministrationRights") + .HasColumnType("INTEGER"); + + b.Property("GroupId") + .HasColumnType("INTEGER"); + + b.Property("InstanceManagerRights") + .HasColumnType("INTEGER"); + + b.Property("UserId") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("GroupId") + .IsUnique(); + + b.HasIndex("UserId") + .IsUnique(); + + b.ToTable("PermissionSets"); + }); + + modelBuilder.Entity("Tgstation.Server.Host.Models.ReattachInformation", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("AccessIdentifier") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("CompileJobId") + .HasColumnType("INTEGER"); + + b.Property("InitialCompileJobId") + .HasColumnType("INTEGER"); + + b.Property("LaunchSecurityLevel") + .HasColumnType("INTEGER"); + + b.Property("LaunchVisibility") + .HasColumnType("INTEGER"); + + b.Property("Port") + .HasColumnType("INTEGER"); + + b.Property("ProcessId") + .HasColumnType("INTEGER"); + + b.Property("RebootState") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("CompileJobId"); + + b.HasIndex("InitialCompileJobId"); + + b.ToTable("ReattachInformations"); + }); + + modelBuilder.Entity("Tgstation.Server.Host.Models.RepositorySettings", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("AccessToken") + .HasMaxLength(10000) + .HasColumnType("TEXT"); + + b.Property("AccessUser") + .HasMaxLength(10000) + .HasColumnType("TEXT"); + + b.Property("AutoUpdatesKeepTestMerges") + .IsRequired() + .HasColumnType("INTEGER"); + + b.Property("AutoUpdatesSynchronize") + .IsRequired() + .HasColumnType("INTEGER"); + + b.Property("CommitterEmail") + .IsRequired() + .HasMaxLength(10000) + .HasColumnType("TEXT"); + + b.Property("CommitterName") + .IsRequired() + .HasMaxLength(10000) + .HasColumnType("TEXT"); + + b.Property("CreateGitHubDeployments") + .IsRequired() + .HasColumnType("INTEGER"); + + b.Property("InstanceId") + .HasColumnType("INTEGER"); + + b.Property("PostTestMergeComment") + .IsRequired() + .HasColumnType("INTEGER"); + + b.Property("PushTestMergeCommits") + .IsRequired() + .HasColumnType("INTEGER"); + + b.Property("ShowTestMergeCommitters") + .IsRequired() + .HasColumnType("INTEGER"); + + b.Property("UpdateSubmodules") + .IsRequired() + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("InstanceId") + .IsUnique(); + + b.ToTable("RepositorySettings"); + }); + + modelBuilder.Entity("Tgstation.Server.Host.Models.RevInfoTestMerge", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("RevisionInformationId") + .HasColumnType("INTEGER"); + + b.Property("TestMergeId") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("RevisionInformationId"); + + b.HasIndex("TestMergeId"); + + b.ToTable("RevInfoTestMerges"); + }); + + modelBuilder.Entity("Tgstation.Server.Host.Models.RevisionInformation", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("CommitSha") + .IsRequired() + .HasMaxLength(40) + .HasColumnType("TEXT"); + + b.Property("InstanceId") + .HasColumnType("INTEGER"); + + b.Property("OriginCommitSha") + .IsRequired() + .HasMaxLength(40) + .HasColumnType("TEXT"); + + b.Property("Timestamp") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("InstanceId", "CommitSha") + .IsUnique(); + + b.ToTable("RevisionInformations"); + }); + + modelBuilder.Entity("Tgstation.Server.Host.Models.TestMerge", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("Author") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("BodyAtMerge") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("Comment") + .HasMaxLength(10000) + .HasColumnType("TEXT"); + + b.Property("MergedAt") + .HasColumnType("TEXT"); + + b.Property("MergedById") + .HasColumnType("INTEGER"); + + b.Property("Number") + .HasColumnType("INTEGER"); + + b.Property("PrimaryRevisionInformationId") + .IsRequired() + .HasColumnType("INTEGER"); + + b.Property("TargetCommitSha") + .IsRequired() + .HasMaxLength(40) + .HasColumnType("TEXT"); + + b.Property("TitleAtMerge") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("Url") + .IsRequired() + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("MergedById"); + + b.HasIndex("PrimaryRevisionInformationId") + .IsUnique(); + + b.ToTable("TestMerges"); + }); + + modelBuilder.Entity("Tgstation.Server.Host.Models.User", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("CanonicalName") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("TEXT"); + + b.Property("CreatedAt") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("CreatedById") + .HasColumnType("INTEGER"); + + b.Property("Enabled") + .IsRequired() + .HasColumnType("INTEGER"); + + b.Property("GroupId") + .HasColumnType("INTEGER"); + + b.Property("LastPasswordUpdate") + .HasColumnType("TEXT"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("TEXT"); + + b.Property("PasswordHash") + .HasColumnType("TEXT"); + + b.Property("SystemIdentifier") + .HasMaxLength(100) + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("CanonicalName") + .IsUnique(); + + b.HasIndex("CreatedById"); + + b.HasIndex("GroupId"); + + b.HasIndex("SystemIdentifier") + .IsUnique(); + + b.ToTable("Users"); + }); + + modelBuilder.Entity("Tgstation.Server.Host.Models.UserGroup", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("Name") + .IsUnique(); + + b.ToTable("Groups"); + }); + + modelBuilder.Entity("Tgstation.Server.Host.Models.ChatBot", b => + { + b.HasOne("Tgstation.Server.Host.Models.Instance", "Instance") + .WithMany("ChatSettings") + .HasForeignKey("InstanceId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Instance"); + }); + + modelBuilder.Entity("Tgstation.Server.Host.Models.ChatChannel", b => + { + b.HasOne("Tgstation.Server.Host.Models.ChatBot", "ChatSettings") + .WithMany("Channels") + .HasForeignKey("ChatSettingsId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("ChatSettings"); + }); + + modelBuilder.Entity("Tgstation.Server.Host.Models.CompileJob", b => + { + b.HasOne("Tgstation.Server.Host.Models.Job", "Job") + .WithOne() + .HasForeignKey("Tgstation.Server.Host.Models.CompileJob", "JobId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Tgstation.Server.Host.Models.RevisionInformation", "RevisionInformation") + .WithMany("CompileJobs") + .HasForeignKey("RevisionInformationId") + .OnDelete(DeleteBehavior.ClientNoAction) + .IsRequired(); + + b.Navigation("Job"); + + b.Navigation("RevisionInformation"); + }); + + modelBuilder.Entity("Tgstation.Server.Host.Models.DreamDaemonSettings", b => + { + b.HasOne("Tgstation.Server.Host.Models.Instance", "Instance") + .WithOne("DreamDaemonSettings") + .HasForeignKey("Tgstation.Server.Host.Models.DreamDaemonSettings", "InstanceId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Instance"); + }); + + modelBuilder.Entity("Tgstation.Server.Host.Models.DreamMakerSettings", b => + { + b.HasOne("Tgstation.Server.Host.Models.Instance", "Instance") + .WithOne("DreamMakerSettings") + .HasForeignKey("Tgstation.Server.Host.Models.DreamMakerSettings", "InstanceId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Instance"); + }); + + modelBuilder.Entity("Tgstation.Server.Host.Models.InstancePermissionSet", b => + { + b.HasOne("Tgstation.Server.Host.Models.Instance", "Instance") + .WithMany("InstancePermissionSets") + .HasForeignKey("InstanceId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Tgstation.Server.Host.Models.PermissionSet", "PermissionSet") + .WithMany("InstancePermissionSets") + .HasForeignKey("PermissionSetId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Instance"); + + b.Navigation("PermissionSet"); + }); + + modelBuilder.Entity("Tgstation.Server.Host.Models.Job", b => + { + b.HasOne("Tgstation.Server.Host.Models.User", "CancelledBy") + .WithMany() + .HasForeignKey("CancelledById"); + + b.HasOne("Tgstation.Server.Host.Models.Instance", "Instance") + .WithMany("Jobs") + .HasForeignKey("InstanceId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Tgstation.Server.Host.Models.User", "StartedBy") + .WithMany() + .HasForeignKey("StartedById") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("CancelledBy"); + + b.Navigation("Instance"); + + b.Navigation("StartedBy"); + }); + + modelBuilder.Entity("Tgstation.Server.Host.Models.OAuthConnection", b => + { + b.HasOne("Tgstation.Server.Host.Models.User", "User") + .WithMany("OAuthConnections") + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Tgstation.Server.Host.Models.PermissionSet", b => + { + b.HasOne("Tgstation.Server.Host.Models.UserGroup", "Group") + .WithOne("PermissionSet") + .HasForeignKey("Tgstation.Server.Host.Models.PermissionSet", "GroupId") + .OnDelete(DeleteBehavior.Cascade); + + b.HasOne("Tgstation.Server.Host.Models.User", "User") + .WithOne("PermissionSet") + .HasForeignKey("Tgstation.Server.Host.Models.PermissionSet", "UserId") + .OnDelete(DeleteBehavior.Cascade); + + b.Navigation("Group"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Tgstation.Server.Host.Models.ReattachInformation", b => + { + b.HasOne("Tgstation.Server.Host.Models.CompileJob", "CompileJob") + .WithMany() + .HasForeignKey("CompileJobId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Tgstation.Server.Host.Models.CompileJob", "InitialCompileJob") + .WithMany() + .HasForeignKey("InitialCompileJobId"); + + b.Navigation("CompileJob"); + + b.Navigation("InitialCompileJob"); + }); + + modelBuilder.Entity("Tgstation.Server.Host.Models.RepositorySettings", b => + { + b.HasOne("Tgstation.Server.Host.Models.Instance", "Instance") + .WithOne("RepositorySettings") + .HasForeignKey("Tgstation.Server.Host.Models.RepositorySettings", "InstanceId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Instance"); + }); + + modelBuilder.Entity("Tgstation.Server.Host.Models.RevInfoTestMerge", b => + { + b.HasOne("Tgstation.Server.Host.Models.RevisionInformation", "RevisionInformation") + .WithMany("ActiveTestMerges") + .HasForeignKey("RevisionInformationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Tgstation.Server.Host.Models.TestMerge", "TestMerge") + .WithMany("RevisonInformations") + .HasForeignKey("TestMergeId") + .OnDelete(DeleteBehavior.ClientNoAction) + .IsRequired(); + + b.Navigation("RevisionInformation"); + + b.Navigation("TestMerge"); + }); + + modelBuilder.Entity("Tgstation.Server.Host.Models.RevisionInformation", b => + { + b.HasOne("Tgstation.Server.Host.Models.Instance", "Instance") + .WithMany("RevisionInformations") + .HasForeignKey("InstanceId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Instance"); + }); + + modelBuilder.Entity("Tgstation.Server.Host.Models.TestMerge", b => + { + b.HasOne("Tgstation.Server.Host.Models.User", "MergedBy") + .WithMany("TestMerges") + .HasForeignKey("MergedById") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("Tgstation.Server.Host.Models.RevisionInformation", "PrimaryRevisionInformation") + .WithOne("PrimaryTestMerge") + .HasForeignKey("Tgstation.Server.Host.Models.TestMerge", "PrimaryRevisionInformationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("MergedBy"); + + b.Navigation("PrimaryRevisionInformation"); + }); + + modelBuilder.Entity("Tgstation.Server.Host.Models.User", b => + { + b.HasOne("Tgstation.Server.Host.Models.User", "CreatedBy") + .WithMany("CreatedUsers") + .HasForeignKey("CreatedById"); + + b.HasOne("Tgstation.Server.Host.Models.UserGroup", "Group") + .WithMany("Users") + .HasForeignKey("GroupId"); + + b.Navigation("CreatedBy"); + + b.Navigation("Group"); + }); + + modelBuilder.Entity("Tgstation.Server.Host.Models.ChatBot", b => + { + b.Navigation("Channels"); + }); + + modelBuilder.Entity("Tgstation.Server.Host.Models.Instance", b => + { + b.Navigation("ChatSettings"); + + b.Navigation("DreamDaemonSettings"); + + b.Navigation("DreamMakerSettings"); + + b.Navigation("InstancePermissionSets"); + + b.Navigation("Jobs"); + + b.Navigation("RepositorySettings"); + + b.Navigation("RevisionInformations"); + }); + + modelBuilder.Entity("Tgstation.Server.Host.Models.PermissionSet", b => + { + b.Navigation("InstancePermissionSets"); + }); + + modelBuilder.Entity("Tgstation.Server.Host.Models.RevisionInformation", b => + { + b.Navigation("ActiveTestMerges"); + + b.Navigation("CompileJobs"); + + b.Navigation("PrimaryTestMerge"); + }); + + modelBuilder.Entity("Tgstation.Server.Host.Models.TestMerge", b => + { + b.Navigation("RevisonInformations"); + }); + + modelBuilder.Entity("Tgstation.Server.Host.Models.User", b => + { + b.Navigation("CreatedUsers"); + + b.Navigation("OAuthConnections"); + + b.Navigation("PermissionSet"); + + b.Navigation("TestMerges"); + }); + + modelBuilder.Entity("Tgstation.Server.Host.Models.UserGroup", b => + { + b.Navigation("PermissionSet") + .IsRequired(); + + b.Navigation("Users"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/src/Tgstation.Server.Host/Database/Migrations/20231108004409_SLRenameByondColumnsToEngine.cs b/src/Tgstation.Server.Host/Database/Migrations/20231108004409_SLRenameByondColumnsToEngine.cs new file mode 100644 index 00000000000..3b95c6433c1 --- /dev/null +++ b/src/Tgstation.Server.Host/Database/Migrations/20231108004409_SLRenameByondColumnsToEngine.cs @@ -0,0 +1,42 @@ +using System; + +using Microsoft.EntityFrameworkCore.Migrations; + +namespace Tgstation.Server.Host.Database.Migrations +{ + /// + public partial class SLRenameByondColumnsToEngine : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + ArgumentNullException.ThrowIfNull(migrationBuilder); + + migrationBuilder.RenameColumn( + name: "ByondRights", + table: "InstancePermissionSets", + newName: "EngineRights"); + + migrationBuilder.RenameColumn( + name: "ByondVersion", + table: "CompileJobs", + newName: "EngineVersion"); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + ArgumentNullException.ThrowIfNull(migrationBuilder); + + migrationBuilder.RenameColumn( + name: "EngineRights", + table: "InstancePermissionSets", + newName: "ByondRights"); + + migrationBuilder.RenameColumn( + name: "EngineVersion", + table: "CompileJobs", + newName: "ByondVersion"); + } + } +} diff --git a/src/Tgstation.Server.Host/Database/Migrations/20231220032508_MSAddTopicPort.Designer.cs b/src/Tgstation.Server.Host/Database/Migrations/20231220032508_MSAddTopicPort.Designer.cs new file mode 100644 index 00000000000..39d9629c7c8 --- /dev/null +++ b/src/Tgstation.Server.Host/Database/Migrations/20231220032508_MSAddTopicPort.Designer.cs @@ -0,0 +1,1076 @@ +// +using System; + +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Migrations; + +namespace Tgstation.Server.Host.Database.Migrations +{ + [DbContext(typeof(SqlServerDatabaseContext))] + [Migration("20231220032508_MSAddTopicPort")] + partial class MSAddTopicPort + { + /// + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("ProductVersion", "8.0.0") + .HasAnnotation("Relational:MaxIdentifierLength", 128); + + SqlServerModelBuilderExtensions.UseIdentityColumns(modelBuilder); + + modelBuilder.Entity("Tgstation.Server.Host.Models.ChatBot", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("ChannelLimit") + .HasColumnType("int"); + + b.Property("ConnectionString") + .IsRequired() + .HasMaxLength(10000) + .HasColumnType("nvarchar(max)"); + + b.Property("Enabled") + .HasColumnType("bit"); + + b.Property("InstanceId") + .HasColumnType("bigint"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("nvarchar(100)"); + + b.Property("Provider") + .HasColumnType("int"); + + b.Property("ReconnectionInterval") + .HasColumnType("bigint"); + + b.HasKey("Id"); + + b.HasIndex("InstanceId", "Name") + .IsUnique(); + + b.ToTable("ChatBots"); + }); + + modelBuilder.Entity("Tgstation.Server.Host.Models.ChatChannel", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("ChatSettingsId") + .HasColumnType("bigint"); + + b.Property("DiscordChannelId") + .HasColumnType("decimal(20,0)"); + + b.Property("IrcChannel") + .HasMaxLength(100) + .HasColumnType("nvarchar(100)"); + + b.Property("IsAdminChannel") + .IsRequired() + .HasColumnType("bit"); + + b.Property("IsSystemChannel") + .IsRequired() + .HasColumnType("bit"); + + b.Property("IsUpdatesChannel") + .IsRequired() + .HasColumnType("bit"); + + b.Property("IsWatchdogChannel") + .IsRequired() + .HasColumnType("bit"); + + b.Property("Tag") + .HasMaxLength(10000) + .HasColumnType("nvarchar(max)"); + + b.HasKey("Id"); + + b.HasIndex("ChatSettingsId", "DiscordChannelId") + .IsUnique() + .HasFilter("[DiscordChannelId] IS NOT NULL"); + + b.HasIndex("ChatSettingsId", "IrcChannel") + .IsUnique() + .HasFilter("[IrcChannel] IS NOT NULL"); + + b.ToTable("ChatChannels"); + }); + + modelBuilder.Entity("Tgstation.Server.Host.Models.CompileJob", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("DMApiMajorVersion") + .HasColumnType("int"); + + b.Property("DMApiMinorVersion") + .HasColumnType("int"); + + b.Property("DMApiPatchVersion") + .HasColumnType("int"); + + b.Property("DirectoryName") + .IsRequired() + .HasColumnType("uniqueidentifier"); + + b.Property("DmeName") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("EngineVersion") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("GitHubDeploymentId") + .HasColumnType("int"); + + b.Property("GitHubRepoId") + .HasColumnType("bigint"); + + b.Property("JobId") + .HasColumnType("bigint"); + + b.Property("MinimumSecurityLevel") + .HasColumnType("int"); + + b.Property("Output") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("RepositoryOrigin") + .HasColumnType("nvarchar(max)"); + + b.Property("RevisionInformationId") + .HasColumnType("bigint"); + + b.HasKey("Id"); + + b.HasIndex("DirectoryName"); + + b.HasIndex("JobId") + .IsUnique(); + + b.HasIndex("RevisionInformationId"); + + b.ToTable("CompileJobs"); + }); + + modelBuilder.Entity("Tgstation.Server.Host.Models.DreamDaemonSettings", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("AdditionalParameters") + .IsRequired() + .HasMaxLength(10000) + .HasColumnType("nvarchar(max)"); + + b.Property("AllowWebClient") + .IsRequired() + .HasColumnType("bit"); + + b.Property("AutoStart") + .IsRequired() + .HasColumnType("bit"); + + b.Property("DumpOnHealthCheckRestart") + .IsRequired() + .HasColumnType("bit"); + + b.Property("HealthCheckSeconds") + .HasColumnType("bigint"); + + b.Property("InstanceId") + .HasColumnType("bigint"); + + b.Property("LogOutput") + .IsRequired() + .HasColumnType("bit"); + + b.Property("MapThreads") + .HasColumnType("bigint"); + + b.Property("Port") + .HasColumnType("int"); + + b.Property("SecurityLevel") + .HasColumnType("int"); + + b.Property("StartProfiler") + .IsRequired() + .HasColumnType("bit"); + + b.Property("StartupTimeout") + .HasColumnType("bigint"); + + b.Property("TopicRequestTimeout") + .HasColumnType("bigint"); + + b.Property("Visibility") + .HasColumnType("int"); + + b.HasKey("Id"); + + b.HasIndex("InstanceId") + .IsUnique(); + + b.ToTable("DreamDaemonSettings"); + }); + + modelBuilder.Entity("Tgstation.Server.Host.Models.DreamMakerSettings", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("ApiValidationPort") + .HasColumnType("int"); + + b.Property("ApiValidationSecurityLevel") + .HasColumnType("int"); + + b.Property("InstanceId") + .HasColumnType("bigint"); + + b.Property("ProjectName") + .HasMaxLength(10000) + .HasColumnType("nvarchar(max)"); + + b.Property("RequireDMApiValidation") + .IsRequired() + .HasColumnType("bit"); + + b.Property("Timeout") + .IsRequired() + .HasColumnType("time"); + + b.HasKey("Id"); + + b.HasIndex("InstanceId") + .IsUnique(); + + b.ToTable("DreamMakerSettings"); + }); + + modelBuilder.Entity("Tgstation.Server.Host.Models.Instance", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("AutoUpdateInterval") + .HasColumnType("bigint"); + + b.Property("ChatBotLimit") + .HasColumnType("int"); + + b.Property("ConfigurationType") + .HasColumnType("int"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("nvarchar(100)"); + + b.Property("Online") + .IsRequired() + .HasColumnType("bit"); + + b.Property("Path") + .IsRequired() + .HasColumnType("nvarchar(450)"); + + b.Property("SwarmIdentifer") + .HasColumnType("nvarchar(450)"); + + b.HasKey("Id"); + + b.HasIndex("Path", "SwarmIdentifer") + .IsUnique() + .HasFilter("[SwarmIdentifer] IS NOT NULL"); + + b.ToTable("Instances"); + }); + + modelBuilder.Entity("Tgstation.Server.Host.Models.InstancePermissionSet", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("ChatBotRights") + .HasColumnType("decimal(20,0)"); + + b.Property("ConfigurationRights") + .HasColumnType("decimal(20,0)"); + + b.Property("DreamDaemonRights") + .HasColumnType("decimal(20,0)"); + + b.Property("DreamMakerRights") + .HasColumnType("decimal(20,0)"); + + b.Property("EngineRights") + .HasColumnType("decimal(20,0)"); + + b.Property("InstanceId") + .HasColumnType("bigint"); + + b.Property("InstancePermissionSetRights") + .HasColumnType("decimal(20,0)"); + + b.Property("PermissionSetId") + .HasColumnType("bigint"); + + b.Property("RepositoryRights") + .HasColumnType("decimal(20,0)"); + + b.HasKey("Id"); + + b.HasIndex("InstanceId"); + + b.HasIndex("PermissionSetId", "InstanceId") + .IsUnique(); + + b.ToTable("InstancePermissionSets"); + }); + + modelBuilder.Entity("Tgstation.Server.Host.Models.Job", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("CancelRight") + .HasColumnType("decimal(20,0)"); + + b.Property("CancelRightsType") + .HasColumnType("decimal(20,0)"); + + b.Property("Cancelled") + .IsRequired() + .HasColumnType("bit"); + + b.Property("CancelledById") + .HasColumnType("bigint"); + + b.Property("Description") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("ErrorCode") + .HasColumnType("bigint"); + + b.Property("ExceptionDetails") + .HasColumnType("nvarchar(max)"); + + b.Property("InstanceId") + .HasColumnType("bigint"); + + b.Property("JobCode") + .HasColumnType("tinyint"); + + b.Property("StartedAt") + .IsRequired() + .HasColumnType("datetimeoffset"); + + b.Property("StartedById") + .HasColumnType("bigint"); + + b.Property("StoppedAt") + .HasColumnType("datetimeoffset"); + + b.HasKey("Id"); + + b.HasIndex("CancelledById"); + + b.HasIndex("InstanceId"); + + b.HasIndex("StartedById"); + + b.ToTable("Jobs"); + }); + + modelBuilder.Entity("Tgstation.Server.Host.Models.OAuthConnection", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("ExternalUserId") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("nvarchar(100)"); + + b.Property("Provider") + .HasColumnType("int"); + + b.Property("UserId") + .HasColumnType("bigint"); + + b.HasKey("Id"); + + b.HasIndex("UserId"); + + b.HasIndex("Provider", "ExternalUserId") + .IsUnique(); + + b.ToTable("OAuthConnections"); + }); + + modelBuilder.Entity("Tgstation.Server.Host.Models.PermissionSet", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("AdministrationRights") + .HasColumnType("decimal(20,0)"); + + b.Property("GroupId") + .HasColumnType("bigint"); + + b.Property("InstanceManagerRights") + .HasColumnType("decimal(20,0)"); + + b.Property("UserId") + .HasColumnType("bigint"); + + b.HasKey("Id"); + + b.HasIndex("GroupId") + .IsUnique() + .HasFilter("[GroupId] IS NOT NULL"); + + b.HasIndex("UserId") + .IsUnique() + .HasFilter("[UserId] IS NOT NULL"); + + b.ToTable("PermissionSets"); + }); + + modelBuilder.Entity("Tgstation.Server.Host.Models.ReattachInformation", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("AccessIdentifier") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("CompileJobId") + .HasColumnType("bigint"); + + b.Property("InitialCompileJobId") + .HasColumnType("bigint"); + + b.Property("LaunchSecurityLevel") + .HasColumnType("int"); + + b.Property("LaunchVisibility") + .HasColumnType("int"); + + b.Property("Port") + .HasColumnType("int"); + + b.Property("ProcessId") + .HasColumnType("int"); + + b.Property("RebootState") + .HasColumnType("int"); + + b.Property("TopicPort") + .HasColumnType("int"); + + b.HasKey("Id"); + + b.HasIndex("CompileJobId"); + + b.HasIndex("InitialCompileJobId"); + + b.ToTable("ReattachInformations"); + }); + + modelBuilder.Entity("Tgstation.Server.Host.Models.RepositorySettings", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("AccessToken") + .HasMaxLength(10000) + .HasColumnType("nvarchar(max)"); + + b.Property("AccessUser") + .HasMaxLength(10000) + .HasColumnType("nvarchar(max)"); + + b.Property("AutoUpdatesKeepTestMerges") + .IsRequired() + .HasColumnType("bit"); + + b.Property("AutoUpdatesSynchronize") + .IsRequired() + .HasColumnType("bit"); + + b.Property("CommitterEmail") + .IsRequired() + .HasMaxLength(10000) + .HasColumnType("nvarchar(max)"); + + b.Property("CommitterName") + .IsRequired() + .HasMaxLength(10000) + .HasColumnType("nvarchar(max)"); + + b.Property("CreateGitHubDeployments") + .IsRequired() + .HasColumnType("bit"); + + b.Property("InstanceId") + .HasColumnType("bigint"); + + b.Property("PostTestMergeComment") + .IsRequired() + .HasColumnType("bit"); + + b.Property("PushTestMergeCommits") + .IsRequired() + .HasColumnType("bit"); + + b.Property("ShowTestMergeCommitters") + .IsRequired() + .HasColumnType("bit"); + + b.Property("UpdateSubmodules") + .IsRequired() + .HasColumnType("bit"); + + b.HasKey("Id"); + + b.HasIndex("InstanceId") + .IsUnique(); + + b.ToTable("RepositorySettings"); + }); + + modelBuilder.Entity("Tgstation.Server.Host.Models.RevInfoTestMerge", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("RevisionInformationId") + .HasColumnType("bigint"); + + b.Property("TestMergeId") + .HasColumnType("bigint"); + + b.HasKey("Id"); + + b.HasIndex("RevisionInformationId"); + + b.HasIndex("TestMergeId"); + + b.ToTable("RevInfoTestMerges"); + }); + + modelBuilder.Entity("Tgstation.Server.Host.Models.RevisionInformation", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("CommitSha") + .IsRequired() + .HasMaxLength(40) + .HasColumnType("nvarchar(40)"); + + b.Property("InstanceId") + .HasColumnType("bigint"); + + b.Property("OriginCommitSha") + .IsRequired() + .HasMaxLength(40) + .HasColumnType("nvarchar(40)"); + + b.Property("Timestamp") + .HasColumnType("datetimeoffset"); + + b.HasKey("Id"); + + b.HasIndex("InstanceId", "CommitSha") + .IsUnique(); + + b.ToTable("RevisionInformations"); + }); + + modelBuilder.Entity("Tgstation.Server.Host.Models.TestMerge", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("Author") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("BodyAtMerge") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("Comment") + .HasMaxLength(10000) + .HasColumnType("nvarchar(max)"); + + b.Property("MergedAt") + .HasColumnType("datetimeoffset"); + + b.Property("MergedById") + .HasColumnType("bigint"); + + b.Property("Number") + .HasColumnType("int"); + + b.Property("PrimaryRevisionInformationId") + .IsRequired() + .HasColumnType("bigint"); + + b.Property("TargetCommitSha") + .IsRequired() + .HasMaxLength(40) + .HasColumnType("nvarchar(40)"); + + b.Property("TitleAtMerge") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("Url") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.HasKey("Id"); + + b.HasIndex("MergedById"); + + b.HasIndex("PrimaryRevisionInformationId") + .IsUnique(); + + b.ToTable("TestMerges"); + }); + + modelBuilder.Entity("Tgstation.Server.Host.Models.User", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("CanonicalName") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("nvarchar(100)"); + + b.Property("CreatedAt") + .IsRequired() + .HasColumnType("datetimeoffset"); + + b.Property("CreatedById") + .HasColumnType("bigint"); + + b.Property("Enabled") + .IsRequired() + .HasColumnType("bit"); + + b.Property("GroupId") + .HasColumnType("bigint"); + + b.Property("LastPasswordUpdate") + .HasColumnType("datetimeoffset"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("nvarchar(100)"); + + b.Property("PasswordHash") + .HasColumnType("nvarchar(max)"); + + b.Property("SystemIdentifier") + .HasMaxLength(100) + .HasColumnType("nvarchar(100)"); + + b.HasKey("Id"); + + b.HasIndex("CanonicalName") + .IsUnique(); + + b.HasIndex("CreatedById"); + + b.HasIndex("GroupId"); + + b.HasIndex("SystemIdentifier") + .IsUnique() + .HasFilter("[SystemIdentifier] IS NOT NULL"); + + b.ToTable("Users"); + }); + + modelBuilder.Entity("Tgstation.Server.Host.Models.UserGroup", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("Name") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("nvarchar(100)"); + + b.HasKey("Id"); + + b.HasIndex("Name") + .IsUnique(); + + b.ToTable("Groups"); + }); + + modelBuilder.Entity("Tgstation.Server.Host.Models.ChatBot", b => + { + b.HasOne("Tgstation.Server.Host.Models.Instance", "Instance") + .WithMany("ChatSettings") + .HasForeignKey("InstanceId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Instance"); + }); + + modelBuilder.Entity("Tgstation.Server.Host.Models.ChatChannel", b => + { + b.HasOne("Tgstation.Server.Host.Models.ChatBot", "ChatSettings") + .WithMany("Channels") + .HasForeignKey("ChatSettingsId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("ChatSettings"); + }); + + modelBuilder.Entity("Tgstation.Server.Host.Models.CompileJob", b => + { + b.HasOne("Tgstation.Server.Host.Models.Job", "Job") + .WithOne() + .HasForeignKey("Tgstation.Server.Host.Models.CompileJob", "JobId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Tgstation.Server.Host.Models.RevisionInformation", "RevisionInformation") + .WithMany("CompileJobs") + .HasForeignKey("RevisionInformationId") + .OnDelete(DeleteBehavior.ClientNoAction) + .IsRequired(); + + b.Navigation("Job"); + + b.Navigation("RevisionInformation"); + }); + + modelBuilder.Entity("Tgstation.Server.Host.Models.DreamDaemonSettings", b => + { + b.HasOne("Tgstation.Server.Host.Models.Instance", "Instance") + .WithOne("DreamDaemonSettings") + .HasForeignKey("Tgstation.Server.Host.Models.DreamDaemonSettings", "InstanceId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Instance"); + }); + + modelBuilder.Entity("Tgstation.Server.Host.Models.DreamMakerSettings", b => + { + b.HasOne("Tgstation.Server.Host.Models.Instance", "Instance") + .WithOne("DreamMakerSettings") + .HasForeignKey("Tgstation.Server.Host.Models.DreamMakerSettings", "InstanceId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Instance"); + }); + + modelBuilder.Entity("Tgstation.Server.Host.Models.InstancePermissionSet", b => + { + b.HasOne("Tgstation.Server.Host.Models.Instance", "Instance") + .WithMany("InstancePermissionSets") + .HasForeignKey("InstanceId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Tgstation.Server.Host.Models.PermissionSet", "PermissionSet") + .WithMany("InstancePermissionSets") + .HasForeignKey("PermissionSetId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Instance"); + + b.Navigation("PermissionSet"); + }); + + modelBuilder.Entity("Tgstation.Server.Host.Models.Job", b => + { + b.HasOne("Tgstation.Server.Host.Models.User", "CancelledBy") + .WithMany() + .HasForeignKey("CancelledById"); + + b.HasOne("Tgstation.Server.Host.Models.Instance", "Instance") + .WithMany("Jobs") + .HasForeignKey("InstanceId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Tgstation.Server.Host.Models.User", "StartedBy") + .WithMany() + .HasForeignKey("StartedById") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("CancelledBy"); + + b.Navigation("Instance"); + + b.Navigation("StartedBy"); + }); + + modelBuilder.Entity("Tgstation.Server.Host.Models.OAuthConnection", b => + { + b.HasOne("Tgstation.Server.Host.Models.User", "User") + .WithMany("OAuthConnections") + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Tgstation.Server.Host.Models.PermissionSet", b => + { + b.HasOne("Tgstation.Server.Host.Models.UserGroup", "Group") + .WithOne("PermissionSet") + .HasForeignKey("Tgstation.Server.Host.Models.PermissionSet", "GroupId") + .OnDelete(DeleteBehavior.Cascade); + + b.HasOne("Tgstation.Server.Host.Models.User", "User") + .WithOne("PermissionSet") + .HasForeignKey("Tgstation.Server.Host.Models.PermissionSet", "UserId") + .OnDelete(DeleteBehavior.Cascade); + + b.Navigation("Group"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Tgstation.Server.Host.Models.ReattachInformation", b => + { + b.HasOne("Tgstation.Server.Host.Models.CompileJob", "CompileJob") + .WithMany() + .HasForeignKey("CompileJobId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Tgstation.Server.Host.Models.CompileJob", "InitialCompileJob") + .WithMany() + .HasForeignKey("InitialCompileJobId"); + + b.Navigation("CompileJob"); + + b.Navigation("InitialCompileJob"); + }); + + modelBuilder.Entity("Tgstation.Server.Host.Models.RepositorySettings", b => + { + b.HasOne("Tgstation.Server.Host.Models.Instance", "Instance") + .WithOne("RepositorySettings") + .HasForeignKey("Tgstation.Server.Host.Models.RepositorySettings", "InstanceId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Instance"); + }); + + modelBuilder.Entity("Tgstation.Server.Host.Models.RevInfoTestMerge", b => + { + b.HasOne("Tgstation.Server.Host.Models.RevisionInformation", "RevisionInformation") + .WithMany("ActiveTestMerges") + .HasForeignKey("RevisionInformationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Tgstation.Server.Host.Models.TestMerge", "TestMerge") + .WithMany("RevisonInformations") + .HasForeignKey("TestMergeId") + .OnDelete(DeleteBehavior.ClientNoAction) + .IsRequired(); + + b.Navigation("RevisionInformation"); + + b.Navigation("TestMerge"); + }); + + modelBuilder.Entity("Tgstation.Server.Host.Models.RevisionInformation", b => + { + b.HasOne("Tgstation.Server.Host.Models.Instance", "Instance") + .WithMany("RevisionInformations") + .HasForeignKey("InstanceId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Instance"); + }); + + modelBuilder.Entity("Tgstation.Server.Host.Models.TestMerge", b => + { + b.HasOne("Tgstation.Server.Host.Models.User", "MergedBy") + .WithMany("TestMerges") + .HasForeignKey("MergedById") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("Tgstation.Server.Host.Models.RevisionInformation", "PrimaryRevisionInformation") + .WithOne("PrimaryTestMerge") + .HasForeignKey("Tgstation.Server.Host.Models.TestMerge", "PrimaryRevisionInformationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("MergedBy"); + + b.Navigation("PrimaryRevisionInformation"); + }); + + modelBuilder.Entity("Tgstation.Server.Host.Models.User", b => + { + b.HasOne("Tgstation.Server.Host.Models.User", "CreatedBy") + .WithMany("CreatedUsers") + .HasForeignKey("CreatedById"); + + b.HasOne("Tgstation.Server.Host.Models.UserGroup", "Group") + .WithMany("Users") + .HasForeignKey("GroupId"); + + b.Navigation("CreatedBy"); + + b.Navigation("Group"); + }); + + modelBuilder.Entity("Tgstation.Server.Host.Models.ChatBot", b => + { + b.Navigation("Channels"); + }); + + modelBuilder.Entity("Tgstation.Server.Host.Models.Instance", b => + { + b.Navigation("ChatSettings"); + + b.Navigation("DreamDaemonSettings"); + + b.Navigation("DreamMakerSettings"); + + b.Navigation("InstancePermissionSets"); + + b.Navigation("Jobs"); + + b.Navigation("RepositorySettings"); + + b.Navigation("RevisionInformations"); + }); + + modelBuilder.Entity("Tgstation.Server.Host.Models.PermissionSet", b => + { + b.Navigation("InstancePermissionSets"); + }); + + modelBuilder.Entity("Tgstation.Server.Host.Models.RevisionInformation", b => + { + b.Navigation("ActiveTestMerges"); + + b.Navigation("CompileJobs"); + + b.Navigation("PrimaryTestMerge"); + }); + + modelBuilder.Entity("Tgstation.Server.Host.Models.TestMerge", b => + { + b.Navigation("RevisonInformations"); + }); + + modelBuilder.Entity("Tgstation.Server.Host.Models.User", b => + { + b.Navigation("CreatedUsers"); + + b.Navigation("OAuthConnections"); + + b.Navigation("PermissionSet"); + + b.Navigation("TestMerges"); + }); + + modelBuilder.Entity("Tgstation.Server.Host.Models.UserGroup", b => + { + b.Navigation("PermissionSet") + .IsRequired(); + + b.Navigation("Users"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/src/Tgstation.Server.Host/Database/Migrations/20231220032508_MSAddTopicPort.cs b/src/Tgstation.Server.Host/Database/Migrations/20231220032508_MSAddTopicPort.cs new file mode 100644 index 00000000000..2c1754e5f1a --- /dev/null +++ b/src/Tgstation.Server.Host/Database/Migrations/20231220032508_MSAddTopicPort.cs @@ -0,0 +1,32 @@ +using System; + +using Microsoft.EntityFrameworkCore.Migrations; + +namespace Tgstation.Server.Host.Database.Migrations +{ + /// + public partial class MSAddTopicPort : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + ArgumentNullException.ThrowIfNull(migrationBuilder); + + migrationBuilder.AddColumn( + name: "TopicPort", + table: "ReattachInformations", + type: "int", + nullable: true); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + ArgumentNullException.ThrowIfNull(migrationBuilder); + + migrationBuilder.DropColumn( + name: "TopicPort", + table: "ReattachInformations"); + } + } +} diff --git a/src/Tgstation.Server.Host/Database/Migrations/20231220032515_MYAddTopicPort.Designer.cs b/src/Tgstation.Server.Host/Database/Migrations/20231220032515_MYAddTopicPort.Designer.cs new file mode 100644 index 00000000000..593d5f4db2b --- /dev/null +++ b/src/Tgstation.Server.Host/Database/Migrations/20231220032515_MYAddTopicPort.Designer.cs @@ -0,0 +1,1110 @@ +// +using System; + +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Migrations; + +namespace Tgstation.Server.Host.Database.Migrations +{ + [DbContext(typeof(MySqlDatabaseContext))] + [Migration("20231220032515_MYAddTopicPort")] + partial class MYAddTopicPort + { + /// + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("ProductVersion", "8.0.0") + .HasAnnotation("Relational:MaxIdentifierLength", 64); + + modelBuilder.Entity("Tgstation.Server.Host.Models.ChatBot", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + b.Property("ChannelLimit") + .IsRequired() + .HasColumnType("smallint unsigned"); + + b.Property("ConnectionString") + .IsRequired() + .HasMaxLength(10000) + .HasColumnType("longtext"); + + MySqlPropertyBuilderExtensions.HasCharSet(b.Property("ConnectionString"), "utf8mb4"); + + b.Property("Enabled") + .HasColumnType("tinyint(1)"); + + b.Property("InstanceId") + .HasColumnType("bigint"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("varchar(100)"); + + b.Property("Provider") + .HasColumnType("int"); + + b.Property("ReconnectionInterval") + .IsRequired() + .HasColumnType("int unsigned"); + + b.HasKey("Id"); + + b.HasIndex("InstanceId", "Name") + .IsUnique(); + + b.ToTable("ChatBots"); + }); + + modelBuilder.Entity("Tgstation.Server.Host.Models.ChatChannel", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + b.Property("ChatSettingsId") + .HasColumnType("bigint"); + + b.Property("DiscordChannelId") + .HasColumnType("bigint unsigned"); + + b.Property("IrcChannel") + .HasMaxLength(100) + .HasColumnType("varchar(100)"); + + MySqlPropertyBuilderExtensions.HasCharSet(b.Property("IrcChannel"), "utf8mb4"); + + b.Property("IsAdminChannel") + .IsRequired() + .HasColumnType("tinyint(1)"); + + b.Property("IsSystemChannel") + .IsRequired() + .HasColumnType("tinyint(1)"); + + b.Property("IsUpdatesChannel") + .IsRequired() + .HasColumnType("tinyint(1)"); + + b.Property("IsWatchdogChannel") + .IsRequired() + .HasColumnType("tinyint(1)"); + + b.Property("Tag") + .HasMaxLength(10000) + .HasColumnType("longtext"); + + MySqlPropertyBuilderExtensions.HasCharSet(b.Property("Tag"), "utf8mb4"); + + b.HasKey("Id"); + + b.HasIndex("ChatSettingsId", "DiscordChannelId") + .IsUnique(); + + b.HasIndex("ChatSettingsId", "IrcChannel") + .IsUnique(); + + b.ToTable("ChatChannels"); + }); + + modelBuilder.Entity("Tgstation.Server.Host.Models.CompileJob", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + b.Property("DMApiMajorVersion") + .HasColumnType("int"); + + b.Property("DMApiMinorVersion") + .HasColumnType("int"); + + b.Property("DMApiPatchVersion") + .HasColumnType("int"); + + b.Property("DirectoryName") + .IsRequired() + .HasColumnType("char(36)"); + + b.Property("DmeName") + .IsRequired() + .HasColumnType("longtext"); + + MySqlPropertyBuilderExtensions.HasCharSet(b.Property("DmeName"), "utf8mb4"); + + b.Property("EngineVersion") + .IsRequired() + .HasColumnType("longtext"); + + MySqlPropertyBuilderExtensions.HasCharSet(b.Property("EngineVersion"), "utf8mb4"); + + b.Property("GitHubDeploymentId") + .HasColumnType("int"); + + b.Property("GitHubRepoId") + .HasColumnType("bigint"); + + b.Property("JobId") + .HasColumnType("bigint"); + + b.Property("MinimumSecurityLevel") + .HasColumnType("int"); + + b.Property("Output") + .IsRequired() + .HasColumnType("longtext"); + + MySqlPropertyBuilderExtensions.HasCharSet(b.Property("Output"), "utf8mb4"); + + b.Property("RepositoryOrigin") + .HasColumnType("longtext"); + + MySqlPropertyBuilderExtensions.HasCharSet(b.Property("RepositoryOrigin"), "utf8mb4"); + + b.Property("RevisionInformationId") + .HasColumnType("bigint"); + + b.HasKey("Id"); + + b.HasIndex("DirectoryName"); + + b.HasIndex("JobId") + .IsUnique(); + + b.HasIndex("RevisionInformationId"); + + b.ToTable("CompileJobs"); + }); + + modelBuilder.Entity("Tgstation.Server.Host.Models.DreamDaemonSettings", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + b.Property("AdditionalParameters") + .IsRequired() + .HasMaxLength(10000) + .HasColumnType("longtext"); + + MySqlPropertyBuilderExtensions.HasCharSet(b.Property("AdditionalParameters"), "utf8mb4"); + + b.Property("AllowWebClient") + .IsRequired() + .HasColumnType("tinyint(1)"); + + b.Property("AutoStart") + .IsRequired() + .HasColumnType("tinyint(1)"); + + b.Property("DumpOnHealthCheckRestart") + .IsRequired() + .HasColumnType("tinyint(1)"); + + b.Property("HealthCheckSeconds") + .IsRequired() + .HasColumnType("int unsigned"); + + b.Property("InstanceId") + .HasColumnType("bigint"); + + b.Property("LogOutput") + .IsRequired() + .HasColumnType("tinyint(1)"); + + b.Property("MapThreads") + .IsRequired() + .HasColumnType("int unsigned"); + + b.Property("Port") + .IsRequired() + .HasColumnType("smallint unsigned"); + + b.Property("SecurityLevel") + .HasColumnType("int"); + + b.Property("StartProfiler") + .IsRequired() + .HasColumnType("tinyint(1)"); + + b.Property("StartupTimeout") + .IsRequired() + .HasColumnType("int unsigned"); + + b.Property("TopicRequestTimeout") + .IsRequired() + .HasColumnType("int unsigned"); + + b.Property("Visibility") + .HasColumnType("int"); + + b.HasKey("Id"); + + b.HasIndex("InstanceId") + .IsUnique(); + + b.ToTable("DreamDaemonSettings"); + }); + + modelBuilder.Entity("Tgstation.Server.Host.Models.DreamMakerSettings", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + b.Property("ApiValidationPort") + .IsRequired() + .HasColumnType("smallint unsigned"); + + b.Property("ApiValidationSecurityLevel") + .HasColumnType("int"); + + b.Property("InstanceId") + .HasColumnType("bigint"); + + b.Property("ProjectName") + .HasMaxLength(10000) + .HasColumnType("longtext"); + + MySqlPropertyBuilderExtensions.HasCharSet(b.Property("ProjectName"), "utf8mb4"); + + b.Property("RequireDMApiValidation") + .IsRequired() + .HasColumnType("tinyint(1)"); + + b.Property("Timeout") + .IsRequired() + .HasColumnType("time(6)"); + + b.HasKey("Id"); + + b.HasIndex("InstanceId") + .IsUnique(); + + b.ToTable("DreamMakerSettings"); + }); + + modelBuilder.Entity("Tgstation.Server.Host.Models.Instance", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + b.Property("AutoUpdateInterval") + .IsRequired() + .HasColumnType("int unsigned"); + + b.Property("ChatBotLimit") + .IsRequired() + .HasColumnType("smallint unsigned"); + + b.Property("ConfigurationType") + .HasColumnType("int"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("varchar(100)"); + + MySqlPropertyBuilderExtensions.HasCharSet(b.Property("Name"), "utf8mb4"); + + b.Property("Online") + .IsRequired() + .HasColumnType("tinyint(1)"); + + b.Property("Path") + .IsRequired() + .HasColumnType("varchar(255)"); + + MySqlPropertyBuilderExtensions.HasCharSet(b.Property("Path"), "utf8mb4"); + + b.Property("SwarmIdentifer") + .HasColumnType("varchar(255)"); + + MySqlPropertyBuilderExtensions.HasCharSet(b.Property("SwarmIdentifer"), "utf8mb4"); + + b.HasKey("Id"); + + b.HasIndex("Path", "SwarmIdentifer") + .IsUnique(); + + b.ToTable("Instances"); + }); + + modelBuilder.Entity("Tgstation.Server.Host.Models.InstancePermissionSet", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + b.Property("ChatBotRights") + .HasColumnType("bigint unsigned"); + + b.Property("ConfigurationRights") + .HasColumnType("bigint unsigned"); + + b.Property("DreamDaemonRights") + .HasColumnType("bigint unsigned"); + + b.Property("DreamMakerRights") + .HasColumnType("bigint unsigned"); + + b.Property("EngineRights") + .HasColumnType("bigint unsigned"); + + b.Property("InstanceId") + .HasColumnType("bigint"); + + b.Property("InstancePermissionSetRights") + .HasColumnType("bigint unsigned"); + + b.Property("PermissionSetId") + .HasColumnType("bigint"); + + b.Property("RepositoryRights") + .HasColumnType("bigint unsigned"); + + b.HasKey("Id"); + + b.HasIndex("InstanceId"); + + b.HasIndex("PermissionSetId", "InstanceId") + .IsUnique(); + + b.ToTable("InstancePermissionSets"); + }); + + modelBuilder.Entity("Tgstation.Server.Host.Models.Job", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + b.Property("CancelRight") + .HasColumnType("bigint unsigned"); + + b.Property("CancelRightsType") + .HasColumnType("bigint unsigned"); + + b.Property("Cancelled") + .IsRequired() + .HasColumnType("tinyint(1)"); + + b.Property("CancelledById") + .HasColumnType("bigint"); + + b.Property("Description") + .IsRequired() + .HasColumnType("longtext"); + + MySqlPropertyBuilderExtensions.HasCharSet(b.Property("Description"), "utf8mb4"); + + b.Property("ErrorCode") + .HasColumnType("int unsigned"); + + b.Property("ExceptionDetails") + .HasColumnType("longtext"); + + MySqlPropertyBuilderExtensions.HasCharSet(b.Property("ExceptionDetails"), "utf8mb4"); + + b.Property("InstanceId") + .HasColumnType("bigint"); + + b.Property("JobCode") + .HasColumnType("tinyint unsigned"); + + b.Property("StartedAt") + .IsRequired() + .HasColumnType("datetime(6)"); + + b.Property("StartedById") + .HasColumnType("bigint"); + + b.Property("StoppedAt") + .HasColumnType("datetime(6)"); + + b.HasKey("Id"); + + b.HasIndex("CancelledById"); + + b.HasIndex("InstanceId"); + + b.HasIndex("StartedById"); + + b.ToTable("Jobs"); + }); + + modelBuilder.Entity("Tgstation.Server.Host.Models.OAuthConnection", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + b.Property("ExternalUserId") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("varchar(100)"); + + MySqlPropertyBuilderExtensions.HasCharSet(b.Property("ExternalUserId"), "utf8mb4"); + + b.Property("Provider") + .HasColumnType("int"); + + b.Property("UserId") + .HasColumnType("bigint"); + + b.HasKey("Id"); + + b.HasIndex("UserId"); + + b.HasIndex("Provider", "ExternalUserId") + .IsUnique(); + + b.ToTable("OAuthConnections"); + }); + + modelBuilder.Entity("Tgstation.Server.Host.Models.PermissionSet", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + b.Property("AdministrationRights") + .HasColumnType("bigint unsigned"); + + b.Property("GroupId") + .HasColumnType("bigint"); + + b.Property("InstanceManagerRights") + .HasColumnType("bigint unsigned"); + + b.Property("UserId") + .HasColumnType("bigint"); + + b.HasKey("Id"); + + b.HasIndex("GroupId") + .IsUnique(); + + b.HasIndex("UserId") + .IsUnique(); + + b.ToTable("PermissionSets"); + }); + + modelBuilder.Entity("Tgstation.Server.Host.Models.ReattachInformation", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + b.Property("AccessIdentifier") + .IsRequired() + .HasColumnType("longtext"); + + MySqlPropertyBuilderExtensions.HasCharSet(b.Property("AccessIdentifier"), "utf8mb4"); + + b.Property("CompileJobId") + .HasColumnType("bigint"); + + b.Property("InitialCompileJobId") + .HasColumnType("bigint"); + + b.Property("LaunchSecurityLevel") + .HasColumnType("int"); + + b.Property("LaunchVisibility") + .HasColumnType("int"); + + b.Property("Port") + .HasColumnType("smallint unsigned"); + + b.Property("ProcessId") + .HasColumnType("int"); + + b.Property("RebootState") + .HasColumnType("int"); + + b.Property("TopicPort") + .HasColumnType("smallint unsigned"); + + b.HasKey("Id"); + + b.HasIndex("CompileJobId"); + + b.HasIndex("InitialCompileJobId"); + + b.ToTable("ReattachInformations"); + }); + + modelBuilder.Entity("Tgstation.Server.Host.Models.RepositorySettings", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + b.Property("AccessToken") + .HasMaxLength(10000) + .HasColumnType("longtext"); + + MySqlPropertyBuilderExtensions.HasCharSet(b.Property("AccessToken"), "utf8mb4"); + + b.Property("AccessUser") + .HasMaxLength(10000) + .HasColumnType("longtext"); + + MySqlPropertyBuilderExtensions.HasCharSet(b.Property("AccessUser"), "utf8mb4"); + + b.Property("AutoUpdatesKeepTestMerges") + .IsRequired() + .HasColumnType("tinyint(1)"); + + b.Property("AutoUpdatesSynchronize") + .IsRequired() + .HasColumnType("tinyint(1)"); + + b.Property("CommitterEmail") + .IsRequired() + .HasMaxLength(10000) + .HasColumnType("longtext"); + + MySqlPropertyBuilderExtensions.HasCharSet(b.Property("CommitterEmail"), "utf8mb4"); + + b.Property("CommitterName") + .IsRequired() + .HasMaxLength(10000) + .HasColumnType("longtext"); + + MySqlPropertyBuilderExtensions.HasCharSet(b.Property("CommitterName"), "utf8mb4"); + + b.Property("CreateGitHubDeployments") + .IsRequired() + .HasColumnType("tinyint(1)"); + + b.Property("InstanceId") + .HasColumnType("bigint"); + + b.Property("PostTestMergeComment") + .IsRequired() + .HasColumnType("tinyint(1)"); + + b.Property("PushTestMergeCommits") + .IsRequired() + .HasColumnType("tinyint(1)"); + + b.Property("ShowTestMergeCommitters") + .IsRequired() + .HasColumnType("tinyint(1)"); + + b.Property("UpdateSubmodules") + .IsRequired() + .HasColumnType("tinyint(1)"); + + b.HasKey("Id"); + + b.HasIndex("InstanceId") + .IsUnique(); + + b.ToTable("RepositorySettings"); + }); + + modelBuilder.Entity("Tgstation.Server.Host.Models.RevInfoTestMerge", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + b.Property("RevisionInformationId") + .HasColumnType("bigint"); + + b.Property("TestMergeId") + .HasColumnType("bigint"); + + b.HasKey("Id"); + + b.HasIndex("RevisionInformationId"); + + b.HasIndex("TestMergeId"); + + b.ToTable("RevInfoTestMerges"); + }); + + modelBuilder.Entity("Tgstation.Server.Host.Models.RevisionInformation", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + b.Property("CommitSha") + .IsRequired() + .HasMaxLength(40) + .HasColumnType("varchar(40)"); + + MySqlPropertyBuilderExtensions.HasCharSet(b.Property("CommitSha"), "utf8mb4"); + + b.Property("InstanceId") + .HasColumnType("bigint"); + + b.Property("OriginCommitSha") + .IsRequired() + .HasMaxLength(40) + .HasColumnType("varchar(40)"); + + MySqlPropertyBuilderExtensions.HasCharSet(b.Property("OriginCommitSha"), "utf8mb4"); + + b.Property("Timestamp") + .HasColumnType("datetime(6)"); + + b.HasKey("Id"); + + b.HasIndex("InstanceId", "CommitSha") + .IsUnique(); + + b.ToTable("RevisionInformations"); + }); + + modelBuilder.Entity("Tgstation.Server.Host.Models.TestMerge", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + b.Property("Author") + .IsRequired() + .HasColumnType("longtext"); + + MySqlPropertyBuilderExtensions.HasCharSet(b.Property("Author"), "utf8mb4"); + + b.Property("BodyAtMerge") + .IsRequired() + .HasColumnType("longtext"); + + MySqlPropertyBuilderExtensions.HasCharSet(b.Property("BodyAtMerge"), "utf8mb4"); + + b.Property("Comment") + .HasMaxLength(10000) + .HasColumnType("longtext"); + + MySqlPropertyBuilderExtensions.HasCharSet(b.Property("Comment"), "utf8mb4"); + + b.Property("MergedAt") + .HasColumnType("datetime(6)"); + + b.Property("MergedById") + .HasColumnType("bigint"); + + b.Property("Number") + .HasColumnType("int"); + + b.Property("PrimaryRevisionInformationId") + .IsRequired() + .HasColumnType("bigint"); + + b.Property("TargetCommitSha") + .IsRequired() + .HasMaxLength(40) + .HasColumnType("varchar(40)"); + + MySqlPropertyBuilderExtensions.HasCharSet(b.Property("TargetCommitSha"), "utf8mb4"); + + b.Property("TitleAtMerge") + .IsRequired() + .HasColumnType("longtext"); + + MySqlPropertyBuilderExtensions.HasCharSet(b.Property("TitleAtMerge"), "utf8mb4"); + + b.Property("Url") + .IsRequired() + .HasColumnType("longtext"); + + MySqlPropertyBuilderExtensions.HasCharSet(b.Property("Url"), "utf8mb4"); + + b.HasKey("Id"); + + b.HasIndex("MergedById"); + + b.HasIndex("PrimaryRevisionInformationId") + .IsUnique(); + + b.ToTable("TestMerges"); + }); + + modelBuilder.Entity("Tgstation.Server.Host.Models.User", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + b.Property("CanonicalName") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("varchar(100)"); + + MySqlPropertyBuilderExtensions.HasCharSet(b.Property("CanonicalName"), "utf8mb4"); + + b.Property("CreatedAt") + .IsRequired() + .HasColumnType("datetime(6)"); + + b.Property("CreatedById") + .HasColumnType("bigint"); + + b.Property("Enabled") + .IsRequired() + .HasColumnType("tinyint(1)"); + + b.Property("GroupId") + .HasColumnType("bigint"); + + b.Property("LastPasswordUpdate") + .HasColumnType("datetime(6)"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("varchar(100)"); + + MySqlPropertyBuilderExtensions.HasCharSet(b.Property("Name"), "utf8mb4"); + + b.Property("PasswordHash") + .HasColumnType("longtext"); + + MySqlPropertyBuilderExtensions.HasCharSet(b.Property("PasswordHash"), "utf8mb4"); + + b.Property("SystemIdentifier") + .HasMaxLength(100) + .HasColumnType("varchar(100)"); + + MySqlPropertyBuilderExtensions.HasCharSet(b.Property("SystemIdentifier"), "utf8mb4"); + + b.HasKey("Id"); + + b.HasIndex("CanonicalName") + .IsUnique(); + + b.HasIndex("CreatedById"); + + b.HasIndex("GroupId"); + + b.HasIndex("SystemIdentifier") + .IsUnique(); + + b.ToTable("Users"); + }); + + modelBuilder.Entity("Tgstation.Server.Host.Models.UserGroup", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("varchar(100)"); + + MySqlPropertyBuilderExtensions.HasCharSet(b.Property("Name"), "utf8mb4"); + + b.HasKey("Id"); + + b.HasIndex("Name") + .IsUnique(); + + b.ToTable("Groups"); + }); + + modelBuilder.Entity("Tgstation.Server.Host.Models.ChatBot", b => + { + b.HasOne("Tgstation.Server.Host.Models.Instance", "Instance") + .WithMany("ChatSettings") + .HasForeignKey("InstanceId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Instance"); + }); + + modelBuilder.Entity("Tgstation.Server.Host.Models.ChatChannel", b => + { + b.HasOne("Tgstation.Server.Host.Models.ChatBot", "ChatSettings") + .WithMany("Channels") + .HasForeignKey("ChatSettingsId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("ChatSettings"); + }); + + modelBuilder.Entity("Tgstation.Server.Host.Models.CompileJob", b => + { + b.HasOne("Tgstation.Server.Host.Models.Job", "Job") + .WithOne() + .HasForeignKey("Tgstation.Server.Host.Models.CompileJob", "JobId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Tgstation.Server.Host.Models.RevisionInformation", "RevisionInformation") + .WithMany("CompileJobs") + .HasForeignKey("RevisionInformationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Job"); + + b.Navigation("RevisionInformation"); + }); + + modelBuilder.Entity("Tgstation.Server.Host.Models.DreamDaemonSettings", b => + { + b.HasOne("Tgstation.Server.Host.Models.Instance", "Instance") + .WithOne("DreamDaemonSettings") + .HasForeignKey("Tgstation.Server.Host.Models.DreamDaemonSettings", "InstanceId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Instance"); + }); + + modelBuilder.Entity("Tgstation.Server.Host.Models.DreamMakerSettings", b => + { + b.HasOne("Tgstation.Server.Host.Models.Instance", "Instance") + .WithOne("DreamMakerSettings") + .HasForeignKey("Tgstation.Server.Host.Models.DreamMakerSettings", "InstanceId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Instance"); + }); + + modelBuilder.Entity("Tgstation.Server.Host.Models.InstancePermissionSet", b => + { + b.HasOne("Tgstation.Server.Host.Models.Instance", "Instance") + .WithMany("InstancePermissionSets") + .HasForeignKey("InstanceId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Tgstation.Server.Host.Models.PermissionSet", "PermissionSet") + .WithMany("InstancePermissionSets") + .HasForeignKey("PermissionSetId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Instance"); + + b.Navigation("PermissionSet"); + }); + + modelBuilder.Entity("Tgstation.Server.Host.Models.Job", b => + { + b.HasOne("Tgstation.Server.Host.Models.User", "CancelledBy") + .WithMany() + .HasForeignKey("CancelledById"); + + b.HasOne("Tgstation.Server.Host.Models.Instance", "Instance") + .WithMany("Jobs") + .HasForeignKey("InstanceId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Tgstation.Server.Host.Models.User", "StartedBy") + .WithMany() + .HasForeignKey("StartedById") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("CancelledBy"); + + b.Navigation("Instance"); + + b.Navigation("StartedBy"); + }); + + modelBuilder.Entity("Tgstation.Server.Host.Models.OAuthConnection", b => + { + b.HasOne("Tgstation.Server.Host.Models.User", "User") + .WithMany("OAuthConnections") + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Tgstation.Server.Host.Models.PermissionSet", b => + { + b.HasOne("Tgstation.Server.Host.Models.UserGroup", "Group") + .WithOne("PermissionSet") + .HasForeignKey("Tgstation.Server.Host.Models.PermissionSet", "GroupId") + .OnDelete(DeleteBehavior.Cascade); + + b.HasOne("Tgstation.Server.Host.Models.User", "User") + .WithOne("PermissionSet") + .HasForeignKey("Tgstation.Server.Host.Models.PermissionSet", "UserId") + .OnDelete(DeleteBehavior.Cascade); + + b.Navigation("Group"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Tgstation.Server.Host.Models.ReattachInformation", b => + { + b.HasOne("Tgstation.Server.Host.Models.CompileJob", "CompileJob") + .WithMany() + .HasForeignKey("CompileJobId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Tgstation.Server.Host.Models.CompileJob", "InitialCompileJob") + .WithMany() + .HasForeignKey("InitialCompileJobId"); + + b.Navigation("CompileJob"); + + b.Navigation("InitialCompileJob"); + }); + + modelBuilder.Entity("Tgstation.Server.Host.Models.RepositorySettings", b => + { + b.HasOne("Tgstation.Server.Host.Models.Instance", "Instance") + .WithOne("RepositorySettings") + .HasForeignKey("Tgstation.Server.Host.Models.RepositorySettings", "InstanceId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Instance"); + }); + + modelBuilder.Entity("Tgstation.Server.Host.Models.RevInfoTestMerge", b => + { + b.HasOne("Tgstation.Server.Host.Models.RevisionInformation", "RevisionInformation") + .WithMany("ActiveTestMerges") + .HasForeignKey("RevisionInformationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Tgstation.Server.Host.Models.TestMerge", "TestMerge") + .WithMany("RevisonInformations") + .HasForeignKey("TestMergeId") + .OnDelete(DeleteBehavior.ClientNoAction) + .IsRequired(); + + b.Navigation("RevisionInformation"); + + b.Navigation("TestMerge"); + }); + + modelBuilder.Entity("Tgstation.Server.Host.Models.RevisionInformation", b => + { + b.HasOne("Tgstation.Server.Host.Models.Instance", "Instance") + .WithMany("RevisionInformations") + .HasForeignKey("InstanceId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Instance"); + }); + + modelBuilder.Entity("Tgstation.Server.Host.Models.TestMerge", b => + { + b.HasOne("Tgstation.Server.Host.Models.User", "MergedBy") + .WithMany("TestMerges") + .HasForeignKey("MergedById") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("Tgstation.Server.Host.Models.RevisionInformation", "PrimaryRevisionInformation") + .WithOne("PrimaryTestMerge") + .HasForeignKey("Tgstation.Server.Host.Models.TestMerge", "PrimaryRevisionInformationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("MergedBy"); + + b.Navigation("PrimaryRevisionInformation"); + }); + + modelBuilder.Entity("Tgstation.Server.Host.Models.User", b => + { + b.HasOne("Tgstation.Server.Host.Models.User", "CreatedBy") + .WithMany("CreatedUsers") + .HasForeignKey("CreatedById"); + + b.HasOne("Tgstation.Server.Host.Models.UserGroup", "Group") + .WithMany("Users") + .HasForeignKey("GroupId"); + + b.Navigation("CreatedBy"); + + b.Navigation("Group"); + }); + + modelBuilder.Entity("Tgstation.Server.Host.Models.ChatBot", b => + { + b.Navigation("Channels"); + }); + + modelBuilder.Entity("Tgstation.Server.Host.Models.Instance", b => + { + b.Navigation("ChatSettings"); + + b.Navigation("DreamDaemonSettings"); + + b.Navigation("DreamMakerSettings"); + + b.Navigation("InstancePermissionSets"); + + b.Navigation("Jobs"); + + b.Navigation("RepositorySettings"); + + b.Navigation("RevisionInformations"); + }); + + modelBuilder.Entity("Tgstation.Server.Host.Models.PermissionSet", b => + { + b.Navigation("InstancePermissionSets"); + }); + + modelBuilder.Entity("Tgstation.Server.Host.Models.RevisionInformation", b => + { + b.Navigation("ActiveTestMerges"); + + b.Navigation("CompileJobs"); + + b.Navigation("PrimaryTestMerge"); + }); + + modelBuilder.Entity("Tgstation.Server.Host.Models.TestMerge", b => + { + b.Navigation("RevisonInformations"); + }); + + modelBuilder.Entity("Tgstation.Server.Host.Models.User", b => + { + b.Navigation("CreatedUsers"); + + b.Navigation("OAuthConnections"); + + b.Navigation("PermissionSet"); + + b.Navigation("TestMerges"); + }); + + modelBuilder.Entity("Tgstation.Server.Host.Models.UserGroup", b => + { + b.Navigation("PermissionSet") + .IsRequired(); + + b.Navigation("Users"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/src/Tgstation.Server.Host/Database/Migrations/20231220032515_MYAddTopicPort.cs b/src/Tgstation.Server.Host/Database/Migrations/20231220032515_MYAddTopicPort.cs new file mode 100644 index 00000000000..1630786ccf5 --- /dev/null +++ b/src/Tgstation.Server.Host/Database/Migrations/20231220032515_MYAddTopicPort.cs @@ -0,0 +1,32 @@ +using System; + +using Microsoft.EntityFrameworkCore.Migrations; + +namespace Tgstation.Server.Host.Database.Migrations +{ + /// + public partial class MYAddTopicPort : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + ArgumentNullException.ThrowIfNull(migrationBuilder); + + migrationBuilder.AddColumn( + name: "TopicPort", + table: "ReattachInformations", + type: "smallint unsigned", + nullable: true); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + ArgumentNullException.ThrowIfNull(migrationBuilder); + + migrationBuilder.DropColumn( + name: "TopicPort", + table: "ReattachInformations"); + } + } +} diff --git a/src/Tgstation.Server.Host/Database/Migrations/20231220032521_PGAddTopicPort.Designer.cs b/src/Tgstation.Server.Host/Database/Migrations/20231220032521_PGAddTopicPort.Designer.cs new file mode 100644 index 00000000000..6f31c5e109d --- /dev/null +++ b/src/Tgstation.Server.Host/Database/Migrations/20231220032521_PGAddTopicPort.Designer.cs @@ -0,0 +1,1070 @@ +// +using System; + +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Migrations; + +namespace Tgstation.Server.Host.Database.Migrations +{ + [DbContext(typeof(PostgresSqlDatabaseContext))] + [Migration("20231220032521_PGAddTopicPort")] + partial class PGAddTopicPort + { + /// + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("ProductVersion", "8.0.0") + .HasAnnotation("Relational:MaxIdentifierLength", 63); + + NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder); + + modelBuilder.Entity("Tgstation.Server.Host.Models.ChatBot", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("ChannelLimit") + .HasColumnType("integer"); + + b.Property("ConnectionString") + .IsRequired() + .HasMaxLength(10000) + .HasColumnType("character varying(10000)"); + + b.Property("Enabled") + .HasColumnType("boolean"); + + b.Property("InstanceId") + .HasColumnType("bigint"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("character varying(100)"); + + b.Property("Provider") + .HasColumnType("integer"); + + b.Property("ReconnectionInterval") + .HasColumnType("bigint"); + + b.HasKey("Id"); + + b.HasIndex("InstanceId", "Name") + .IsUnique(); + + b.ToTable("ChatBots"); + }); + + modelBuilder.Entity("Tgstation.Server.Host.Models.ChatChannel", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("ChatSettingsId") + .HasColumnType("bigint"); + + b.Property("DiscordChannelId") + .HasColumnType("numeric(20,0)"); + + b.Property("IrcChannel") + .HasMaxLength(100) + .HasColumnType("character varying(100)"); + + b.Property("IsAdminChannel") + .IsRequired() + .HasColumnType("boolean"); + + b.Property("IsSystemChannel") + .IsRequired() + .HasColumnType("boolean"); + + b.Property("IsUpdatesChannel") + .IsRequired() + .HasColumnType("boolean"); + + b.Property("IsWatchdogChannel") + .IsRequired() + .HasColumnType("boolean"); + + b.Property("Tag") + .HasMaxLength(10000) + .HasColumnType("character varying(10000)"); + + b.HasKey("Id"); + + b.HasIndex("ChatSettingsId", "DiscordChannelId") + .IsUnique(); + + b.HasIndex("ChatSettingsId", "IrcChannel") + .IsUnique(); + + b.ToTable("ChatChannels"); + }); + + modelBuilder.Entity("Tgstation.Server.Host.Models.CompileJob", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("DMApiMajorVersion") + .HasColumnType("integer"); + + b.Property("DMApiMinorVersion") + .HasColumnType("integer"); + + b.Property("DMApiPatchVersion") + .HasColumnType("integer"); + + b.Property("DirectoryName") + .IsRequired() + .HasColumnType("uuid"); + + b.Property("DmeName") + .IsRequired() + .HasColumnType("text"); + + b.Property("EngineVersion") + .IsRequired() + .HasColumnType("text"); + + b.Property("GitHubDeploymentId") + .HasColumnType("integer"); + + b.Property("GitHubRepoId") + .HasColumnType("bigint"); + + b.Property("JobId") + .HasColumnType("bigint"); + + b.Property("MinimumSecurityLevel") + .HasColumnType("integer"); + + b.Property("Output") + .IsRequired() + .HasColumnType("text"); + + b.Property("RepositoryOrigin") + .HasColumnType("text"); + + b.Property("RevisionInformationId") + .HasColumnType("bigint"); + + b.HasKey("Id"); + + b.HasIndex("DirectoryName"); + + b.HasIndex("JobId") + .IsUnique(); + + b.HasIndex("RevisionInformationId"); + + b.ToTable("CompileJobs"); + }); + + modelBuilder.Entity("Tgstation.Server.Host.Models.DreamDaemonSettings", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("AdditionalParameters") + .IsRequired() + .HasMaxLength(10000) + .HasColumnType("character varying(10000)"); + + b.Property("AllowWebClient") + .IsRequired() + .HasColumnType("boolean"); + + b.Property("AutoStart") + .IsRequired() + .HasColumnType("boolean"); + + b.Property("DumpOnHealthCheckRestart") + .IsRequired() + .HasColumnType("boolean"); + + b.Property("HealthCheckSeconds") + .HasColumnType("bigint"); + + b.Property("InstanceId") + .HasColumnType("bigint"); + + b.Property("LogOutput") + .IsRequired() + .HasColumnType("boolean"); + + b.Property("MapThreads") + .HasColumnType("bigint"); + + b.Property("Port") + .HasColumnType("integer"); + + b.Property("SecurityLevel") + .HasColumnType("integer"); + + b.Property("StartProfiler") + .IsRequired() + .HasColumnType("boolean"); + + b.Property("StartupTimeout") + .HasColumnType("bigint"); + + b.Property("TopicRequestTimeout") + .HasColumnType("bigint"); + + b.Property("Visibility") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.HasIndex("InstanceId") + .IsUnique(); + + b.ToTable("DreamDaemonSettings"); + }); + + modelBuilder.Entity("Tgstation.Server.Host.Models.DreamMakerSettings", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("ApiValidationPort") + .HasColumnType("integer"); + + b.Property("ApiValidationSecurityLevel") + .HasColumnType("integer"); + + b.Property("InstanceId") + .HasColumnType("bigint"); + + b.Property("ProjectName") + .HasMaxLength(10000) + .HasColumnType("character varying(10000)"); + + b.Property("RequireDMApiValidation") + .IsRequired() + .HasColumnType("boolean"); + + b.Property("Timeout") + .IsRequired() + .HasColumnType("interval"); + + b.HasKey("Id"); + + b.HasIndex("InstanceId") + .IsUnique(); + + b.ToTable("DreamMakerSettings"); + }); + + modelBuilder.Entity("Tgstation.Server.Host.Models.Instance", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("AutoUpdateInterval") + .HasColumnType("bigint"); + + b.Property("ChatBotLimit") + .HasColumnType("integer"); + + b.Property("ConfigurationType") + .HasColumnType("integer"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("character varying(100)"); + + b.Property("Online") + .IsRequired() + .HasColumnType("boolean"); + + b.Property("Path") + .IsRequired() + .HasColumnType("text"); + + b.Property("SwarmIdentifer") + .HasColumnType("text"); + + b.HasKey("Id"); + + b.HasIndex("Path", "SwarmIdentifer") + .IsUnique(); + + b.ToTable("Instances"); + }); + + modelBuilder.Entity("Tgstation.Server.Host.Models.InstancePermissionSet", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("ChatBotRights") + .HasColumnType("numeric(20,0)"); + + b.Property("ConfigurationRights") + .HasColumnType("numeric(20,0)"); + + b.Property("DreamDaemonRights") + .HasColumnType("numeric(20,0)"); + + b.Property("DreamMakerRights") + .HasColumnType("numeric(20,0)"); + + b.Property("EngineRights") + .HasColumnType("numeric(20,0)"); + + b.Property("InstanceId") + .HasColumnType("bigint"); + + b.Property("InstancePermissionSetRights") + .HasColumnType("numeric(20,0)"); + + b.Property("PermissionSetId") + .HasColumnType("bigint"); + + b.Property("RepositoryRights") + .HasColumnType("numeric(20,0)"); + + b.HasKey("Id"); + + b.HasIndex("InstanceId"); + + b.HasIndex("PermissionSetId", "InstanceId") + .IsUnique(); + + b.ToTable("InstancePermissionSets"); + }); + + modelBuilder.Entity("Tgstation.Server.Host.Models.Job", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("CancelRight") + .HasColumnType("numeric(20,0)"); + + b.Property("CancelRightsType") + .HasColumnType("numeric(20,0)"); + + b.Property("Cancelled") + .IsRequired() + .HasColumnType("boolean"); + + b.Property("CancelledById") + .HasColumnType("bigint"); + + b.Property("Description") + .IsRequired() + .HasColumnType("text"); + + b.Property("ErrorCode") + .HasColumnType("bigint"); + + b.Property("ExceptionDetails") + .HasColumnType("text"); + + b.Property("InstanceId") + .HasColumnType("bigint"); + + b.Property("JobCode") + .HasColumnType("smallint"); + + b.Property("StartedAt") + .IsRequired() + .HasColumnType("timestamp with time zone"); + + b.Property("StartedById") + .HasColumnType("bigint"); + + b.Property("StoppedAt") + .HasColumnType("timestamp with time zone"); + + b.HasKey("Id"); + + b.HasIndex("CancelledById"); + + b.HasIndex("InstanceId"); + + b.HasIndex("StartedById"); + + b.ToTable("Jobs"); + }); + + modelBuilder.Entity("Tgstation.Server.Host.Models.OAuthConnection", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("ExternalUserId") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("character varying(100)"); + + b.Property("Provider") + .HasColumnType("integer"); + + b.Property("UserId") + .HasColumnType("bigint"); + + b.HasKey("Id"); + + b.HasIndex("UserId"); + + b.HasIndex("Provider", "ExternalUserId") + .IsUnique(); + + b.ToTable("OAuthConnections"); + }); + + modelBuilder.Entity("Tgstation.Server.Host.Models.PermissionSet", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("AdministrationRights") + .HasColumnType("numeric(20,0)"); + + b.Property("GroupId") + .HasColumnType("bigint"); + + b.Property("InstanceManagerRights") + .HasColumnType("numeric(20,0)"); + + b.Property("UserId") + .HasColumnType("bigint"); + + b.HasKey("Id"); + + b.HasIndex("GroupId") + .IsUnique(); + + b.HasIndex("UserId") + .IsUnique(); + + b.ToTable("PermissionSets"); + }); + + modelBuilder.Entity("Tgstation.Server.Host.Models.ReattachInformation", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("AccessIdentifier") + .IsRequired() + .HasColumnType("text"); + + b.Property("CompileJobId") + .HasColumnType("bigint"); + + b.Property("InitialCompileJobId") + .HasColumnType("bigint"); + + b.Property("LaunchSecurityLevel") + .HasColumnType("integer"); + + b.Property("LaunchVisibility") + .HasColumnType("integer"); + + b.Property("Port") + .HasColumnType("integer"); + + b.Property("ProcessId") + .HasColumnType("integer"); + + b.Property("RebootState") + .HasColumnType("integer"); + + b.Property("TopicPort") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.HasIndex("CompileJobId"); + + b.HasIndex("InitialCompileJobId"); + + b.ToTable("ReattachInformations"); + }); + + modelBuilder.Entity("Tgstation.Server.Host.Models.RepositorySettings", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("AccessToken") + .HasMaxLength(10000) + .HasColumnType("character varying(10000)"); + + b.Property("AccessUser") + .HasMaxLength(10000) + .HasColumnType("character varying(10000)"); + + b.Property("AutoUpdatesKeepTestMerges") + .IsRequired() + .HasColumnType("boolean"); + + b.Property("AutoUpdatesSynchronize") + .IsRequired() + .HasColumnType("boolean"); + + b.Property("CommitterEmail") + .IsRequired() + .HasMaxLength(10000) + .HasColumnType("character varying(10000)"); + + b.Property("CommitterName") + .IsRequired() + .HasMaxLength(10000) + .HasColumnType("character varying(10000)"); + + b.Property("CreateGitHubDeployments") + .IsRequired() + .HasColumnType("boolean"); + + b.Property("InstanceId") + .HasColumnType("bigint"); + + b.Property("PostTestMergeComment") + .IsRequired() + .HasColumnType("boolean"); + + b.Property("PushTestMergeCommits") + .IsRequired() + .HasColumnType("boolean"); + + b.Property("ShowTestMergeCommitters") + .IsRequired() + .HasColumnType("boolean"); + + b.Property("UpdateSubmodules") + .IsRequired() + .HasColumnType("boolean"); + + b.HasKey("Id"); + + b.HasIndex("InstanceId") + .IsUnique(); + + b.ToTable("RepositorySettings"); + }); + + modelBuilder.Entity("Tgstation.Server.Host.Models.RevInfoTestMerge", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("RevisionInformationId") + .HasColumnType("bigint"); + + b.Property("TestMergeId") + .HasColumnType("bigint"); + + b.HasKey("Id"); + + b.HasIndex("RevisionInformationId"); + + b.HasIndex("TestMergeId"); + + b.ToTable("RevInfoTestMerges"); + }); + + modelBuilder.Entity("Tgstation.Server.Host.Models.RevisionInformation", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("CommitSha") + .IsRequired() + .HasMaxLength(40) + .HasColumnType("character varying(40)"); + + b.Property("InstanceId") + .HasColumnType("bigint"); + + b.Property("OriginCommitSha") + .IsRequired() + .HasMaxLength(40) + .HasColumnType("character varying(40)"); + + b.Property("Timestamp") + .HasColumnType("timestamp with time zone"); + + b.HasKey("Id"); + + b.HasIndex("InstanceId", "CommitSha") + .IsUnique(); + + b.ToTable("RevisionInformations"); + }); + + modelBuilder.Entity("Tgstation.Server.Host.Models.TestMerge", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("Author") + .IsRequired() + .HasColumnType("text"); + + b.Property("BodyAtMerge") + .IsRequired() + .HasColumnType("text"); + + b.Property("Comment") + .HasMaxLength(10000) + .HasColumnType("character varying(10000)"); + + b.Property("MergedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("MergedById") + .HasColumnType("bigint"); + + b.Property("Number") + .HasColumnType("integer"); + + b.Property("PrimaryRevisionInformationId") + .IsRequired() + .HasColumnType("bigint"); + + b.Property("TargetCommitSha") + .IsRequired() + .HasMaxLength(40) + .HasColumnType("character varying(40)"); + + b.Property("TitleAtMerge") + .IsRequired() + .HasColumnType("text"); + + b.Property("Url") + .IsRequired() + .HasColumnType("text"); + + b.HasKey("Id"); + + b.HasIndex("MergedById"); + + b.HasIndex("PrimaryRevisionInformationId") + .IsUnique(); + + b.ToTable("TestMerges"); + }); + + modelBuilder.Entity("Tgstation.Server.Host.Models.User", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("CanonicalName") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("character varying(100)"); + + b.Property("CreatedAt") + .IsRequired() + .HasColumnType("timestamp with time zone"); + + b.Property("CreatedById") + .HasColumnType("bigint"); + + b.Property("Enabled") + .IsRequired() + .HasColumnType("boolean"); + + b.Property("GroupId") + .HasColumnType("bigint"); + + b.Property("LastPasswordUpdate") + .HasColumnType("timestamp with time zone"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("character varying(100)"); + + b.Property("PasswordHash") + .HasColumnType("text"); + + b.Property("SystemIdentifier") + .HasMaxLength(100) + .HasColumnType("character varying(100)"); + + b.HasKey("Id"); + + b.HasIndex("CanonicalName") + .IsUnique(); + + b.HasIndex("CreatedById"); + + b.HasIndex("GroupId"); + + b.HasIndex("SystemIdentifier") + .IsUnique(); + + b.ToTable("Users"); + }); + + modelBuilder.Entity("Tgstation.Server.Host.Models.UserGroup", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("Name") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("character varying(100)"); + + b.HasKey("Id"); + + b.HasIndex("Name") + .IsUnique(); + + b.ToTable("Groups"); + }); + + modelBuilder.Entity("Tgstation.Server.Host.Models.ChatBot", b => + { + b.HasOne("Tgstation.Server.Host.Models.Instance", "Instance") + .WithMany("ChatSettings") + .HasForeignKey("InstanceId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Instance"); + }); + + modelBuilder.Entity("Tgstation.Server.Host.Models.ChatChannel", b => + { + b.HasOne("Tgstation.Server.Host.Models.ChatBot", "ChatSettings") + .WithMany("Channels") + .HasForeignKey("ChatSettingsId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("ChatSettings"); + }); + + modelBuilder.Entity("Tgstation.Server.Host.Models.CompileJob", b => + { + b.HasOne("Tgstation.Server.Host.Models.Job", "Job") + .WithOne() + .HasForeignKey("Tgstation.Server.Host.Models.CompileJob", "JobId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Tgstation.Server.Host.Models.RevisionInformation", "RevisionInformation") + .WithMany("CompileJobs") + .HasForeignKey("RevisionInformationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Job"); + + b.Navigation("RevisionInformation"); + }); + + modelBuilder.Entity("Tgstation.Server.Host.Models.DreamDaemonSettings", b => + { + b.HasOne("Tgstation.Server.Host.Models.Instance", "Instance") + .WithOne("DreamDaemonSettings") + .HasForeignKey("Tgstation.Server.Host.Models.DreamDaemonSettings", "InstanceId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Instance"); + }); + + modelBuilder.Entity("Tgstation.Server.Host.Models.DreamMakerSettings", b => + { + b.HasOne("Tgstation.Server.Host.Models.Instance", "Instance") + .WithOne("DreamMakerSettings") + .HasForeignKey("Tgstation.Server.Host.Models.DreamMakerSettings", "InstanceId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Instance"); + }); + + modelBuilder.Entity("Tgstation.Server.Host.Models.InstancePermissionSet", b => + { + b.HasOne("Tgstation.Server.Host.Models.Instance", "Instance") + .WithMany("InstancePermissionSets") + .HasForeignKey("InstanceId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Tgstation.Server.Host.Models.PermissionSet", "PermissionSet") + .WithMany("InstancePermissionSets") + .HasForeignKey("PermissionSetId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Instance"); + + b.Navigation("PermissionSet"); + }); + + modelBuilder.Entity("Tgstation.Server.Host.Models.Job", b => + { + b.HasOne("Tgstation.Server.Host.Models.User", "CancelledBy") + .WithMany() + .HasForeignKey("CancelledById"); + + b.HasOne("Tgstation.Server.Host.Models.Instance", "Instance") + .WithMany("Jobs") + .HasForeignKey("InstanceId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Tgstation.Server.Host.Models.User", "StartedBy") + .WithMany() + .HasForeignKey("StartedById") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("CancelledBy"); + + b.Navigation("Instance"); + + b.Navigation("StartedBy"); + }); + + modelBuilder.Entity("Tgstation.Server.Host.Models.OAuthConnection", b => + { + b.HasOne("Tgstation.Server.Host.Models.User", "User") + .WithMany("OAuthConnections") + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Tgstation.Server.Host.Models.PermissionSet", b => + { + b.HasOne("Tgstation.Server.Host.Models.UserGroup", "Group") + .WithOne("PermissionSet") + .HasForeignKey("Tgstation.Server.Host.Models.PermissionSet", "GroupId") + .OnDelete(DeleteBehavior.Cascade); + + b.HasOne("Tgstation.Server.Host.Models.User", "User") + .WithOne("PermissionSet") + .HasForeignKey("Tgstation.Server.Host.Models.PermissionSet", "UserId") + .OnDelete(DeleteBehavior.Cascade); + + b.Navigation("Group"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Tgstation.Server.Host.Models.ReattachInformation", b => + { + b.HasOne("Tgstation.Server.Host.Models.CompileJob", "CompileJob") + .WithMany() + .HasForeignKey("CompileJobId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Tgstation.Server.Host.Models.CompileJob", "InitialCompileJob") + .WithMany() + .HasForeignKey("InitialCompileJobId"); + + b.Navigation("CompileJob"); + + b.Navigation("InitialCompileJob"); + }); + + modelBuilder.Entity("Tgstation.Server.Host.Models.RepositorySettings", b => + { + b.HasOne("Tgstation.Server.Host.Models.Instance", "Instance") + .WithOne("RepositorySettings") + .HasForeignKey("Tgstation.Server.Host.Models.RepositorySettings", "InstanceId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Instance"); + }); + + modelBuilder.Entity("Tgstation.Server.Host.Models.RevInfoTestMerge", b => + { + b.HasOne("Tgstation.Server.Host.Models.RevisionInformation", "RevisionInformation") + .WithMany("ActiveTestMerges") + .HasForeignKey("RevisionInformationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Tgstation.Server.Host.Models.TestMerge", "TestMerge") + .WithMany("RevisonInformations") + .HasForeignKey("TestMergeId") + .OnDelete(DeleteBehavior.ClientNoAction) + .IsRequired(); + + b.Navigation("RevisionInformation"); + + b.Navigation("TestMerge"); + }); + + modelBuilder.Entity("Tgstation.Server.Host.Models.RevisionInformation", b => + { + b.HasOne("Tgstation.Server.Host.Models.Instance", "Instance") + .WithMany("RevisionInformations") + .HasForeignKey("InstanceId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Instance"); + }); + + modelBuilder.Entity("Tgstation.Server.Host.Models.TestMerge", b => + { + b.HasOne("Tgstation.Server.Host.Models.User", "MergedBy") + .WithMany("TestMerges") + .HasForeignKey("MergedById") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("Tgstation.Server.Host.Models.RevisionInformation", "PrimaryRevisionInformation") + .WithOne("PrimaryTestMerge") + .HasForeignKey("Tgstation.Server.Host.Models.TestMerge", "PrimaryRevisionInformationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("MergedBy"); + + b.Navigation("PrimaryRevisionInformation"); + }); + + modelBuilder.Entity("Tgstation.Server.Host.Models.User", b => + { + b.HasOne("Tgstation.Server.Host.Models.User", "CreatedBy") + .WithMany("CreatedUsers") + .HasForeignKey("CreatedById"); + + b.HasOne("Tgstation.Server.Host.Models.UserGroup", "Group") + .WithMany("Users") + .HasForeignKey("GroupId"); + + b.Navigation("CreatedBy"); + + b.Navigation("Group"); + }); + + modelBuilder.Entity("Tgstation.Server.Host.Models.ChatBot", b => + { + b.Navigation("Channels"); + }); + + modelBuilder.Entity("Tgstation.Server.Host.Models.Instance", b => + { + b.Navigation("ChatSettings"); + + b.Navigation("DreamDaemonSettings"); + + b.Navigation("DreamMakerSettings"); + + b.Navigation("InstancePermissionSets"); + + b.Navigation("Jobs"); + + b.Navigation("RepositorySettings"); + + b.Navigation("RevisionInformations"); + }); + + modelBuilder.Entity("Tgstation.Server.Host.Models.PermissionSet", b => + { + b.Navigation("InstancePermissionSets"); + }); + + modelBuilder.Entity("Tgstation.Server.Host.Models.RevisionInformation", b => + { + b.Navigation("ActiveTestMerges"); + + b.Navigation("CompileJobs"); + + b.Navigation("PrimaryTestMerge"); + }); + + modelBuilder.Entity("Tgstation.Server.Host.Models.TestMerge", b => + { + b.Navigation("RevisonInformations"); + }); + + modelBuilder.Entity("Tgstation.Server.Host.Models.User", b => + { + b.Navigation("CreatedUsers"); + + b.Navigation("OAuthConnections"); + + b.Navigation("PermissionSet"); + + b.Navigation("TestMerges"); + }); + + modelBuilder.Entity("Tgstation.Server.Host.Models.UserGroup", b => + { + b.Navigation("PermissionSet") + .IsRequired(); + + b.Navigation("Users"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/src/Tgstation.Server.Host/Database/Migrations/20231220032521_PGAddTopicPort.cs b/src/Tgstation.Server.Host/Database/Migrations/20231220032521_PGAddTopicPort.cs new file mode 100644 index 00000000000..1994a0b3f90 --- /dev/null +++ b/src/Tgstation.Server.Host/Database/Migrations/20231220032521_PGAddTopicPort.cs @@ -0,0 +1,32 @@ +using System; + +using Microsoft.EntityFrameworkCore.Migrations; + +namespace Tgstation.Server.Host.Database.Migrations +{ + /// + public partial class PGAddTopicPort : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + ArgumentNullException.ThrowIfNull(migrationBuilder); + + migrationBuilder.AddColumn( + name: "TopicPort", + table: "ReattachInformations", + type: "integer", + nullable: true); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + ArgumentNullException.ThrowIfNull(migrationBuilder); + + migrationBuilder.DropColumn( + name: "TopicPort", + table: "ReattachInformations"); + } + } +} diff --git a/src/Tgstation.Server.Host/Database/Migrations/20231220032528_SLAddTopicPort.Designer.cs b/src/Tgstation.Server.Host/Database/Migrations/20231220032528_SLAddTopicPort.Designer.cs new file mode 100644 index 00000000000..1352af4855c --- /dev/null +++ b/src/Tgstation.Server.Host/Database/Migrations/20231220032528_SLAddTopicPort.Designer.cs @@ -0,0 +1,1042 @@ +// +using System; + +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Migrations; + +namespace Tgstation.Server.Host.Database.Migrations +{ + [DbContext(typeof(SqliteDatabaseContext))] + [Migration("20231220032528_SLAddTopicPort")] + partial class SLAddTopicPort + { + /// + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder.HasAnnotation("ProductVersion", "8.0.0"); + + modelBuilder.Entity("Tgstation.Server.Host.Models.ChatBot", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("ChannelLimit") + .IsRequired() + .HasColumnType("INTEGER"); + + b.Property("ConnectionString") + .IsRequired() + .HasMaxLength(10000) + .HasColumnType("TEXT"); + + b.Property("Enabled") + .HasColumnType("INTEGER"); + + b.Property("InstanceId") + .HasColumnType("INTEGER"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("TEXT"); + + b.Property("Provider") + .HasColumnType("INTEGER"); + + b.Property("ReconnectionInterval") + .IsRequired() + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("InstanceId", "Name") + .IsUnique(); + + b.ToTable("ChatBots"); + }); + + modelBuilder.Entity("Tgstation.Server.Host.Models.ChatChannel", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("ChatSettingsId") + .HasColumnType("INTEGER"); + + b.Property("DiscordChannelId") + .HasColumnType("INTEGER"); + + b.Property("IrcChannel") + .HasMaxLength(100) + .HasColumnType("TEXT"); + + b.Property("IsAdminChannel") + .IsRequired() + .HasColumnType("INTEGER"); + + b.Property("IsSystemChannel") + .IsRequired() + .HasColumnType("INTEGER"); + + b.Property("IsUpdatesChannel") + .IsRequired() + .HasColumnType("INTEGER"); + + b.Property("IsWatchdogChannel") + .IsRequired() + .HasColumnType("INTEGER"); + + b.Property("Tag") + .HasMaxLength(10000) + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("ChatSettingsId", "DiscordChannelId") + .IsUnique(); + + b.HasIndex("ChatSettingsId", "IrcChannel") + .IsUnique(); + + b.ToTable("ChatChannels"); + }); + + modelBuilder.Entity("Tgstation.Server.Host.Models.CompileJob", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("DMApiMajorVersion") + .HasColumnType("INTEGER"); + + b.Property("DMApiMinorVersion") + .HasColumnType("INTEGER"); + + b.Property("DMApiPatchVersion") + .HasColumnType("INTEGER"); + + b.Property("DirectoryName") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("DmeName") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("EngineVersion") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("GitHubDeploymentId") + .HasColumnType("INTEGER"); + + b.Property("GitHubRepoId") + .HasColumnType("INTEGER"); + + b.Property("JobId") + .HasColumnType("INTEGER"); + + b.Property("MinimumSecurityLevel") + .HasColumnType("INTEGER"); + + b.Property("Output") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("RepositoryOrigin") + .HasColumnType("TEXT"); + + b.Property("RevisionInformationId") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("DirectoryName"); + + b.HasIndex("JobId") + .IsUnique(); + + b.HasIndex("RevisionInformationId"); + + b.ToTable("CompileJobs"); + }); + + modelBuilder.Entity("Tgstation.Server.Host.Models.DreamDaemonSettings", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("AdditionalParameters") + .IsRequired() + .HasMaxLength(10000) + .HasColumnType("TEXT"); + + b.Property("AllowWebClient") + .IsRequired() + .HasColumnType("INTEGER"); + + b.Property("AutoStart") + .IsRequired() + .HasColumnType("INTEGER"); + + b.Property("DumpOnHealthCheckRestart") + .IsRequired() + .HasColumnType("INTEGER"); + + b.Property("HealthCheckSeconds") + .IsRequired() + .HasColumnType("INTEGER"); + + b.Property("InstanceId") + .HasColumnType("INTEGER"); + + b.Property("LogOutput") + .IsRequired() + .HasColumnType("INTEGER"); + + b.Property("MapThreads") + .IsRequired() + .HasColumnType("INTEGER"); + + b.Property("Port") + .IsRequired() + .HasColumnType("INTEGER"); + + b.Property("SecurityLevel") + .HasColumnType("INTEGER"); + + b.Property("StartProfiler") + .IsRequired() + .HasColumnType("INTEGER"); + + b.Property("StartupTimeout") + .IsRequired() + .HasColumnType("INTEGER"); + + b.Property("TopicRequestTimeout") + .IsRequired() + .HasColumnType("INTEGER"); + + b.Property("Visibility") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("InstanceId") + .IsUnique(); + + b.ToTable("DreamDaemonSettings"); + }); + + modelBuilder.Entity("Tgstation.Server.Host.Models.DreamMakerSettings", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("ApiValidationPort") + .IsRequired() + .HasColumnType("INTEGER"); + + b.Property("ApiValidationSecurityLevel") + .HasColumnType("INTEGER"); + + b.Property("InstanceId") + .HasColumnType("INTEGER"); + + b.Property("ProjectName") + .HasMaxLength(10000) + .HasColumnType("TEXT"); + + b.Property("RequireDMApiValidation") + .IsRequired() + .HasColumnType("INTEGER"); + + b.Property("Timeout") + .IsRequired() + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("InstanceId") + .IsUnique(); + + b.ToTable("DreamMakerSettings"); + }); + + modelBuilder.Entity("Tgstation.Server.Host.Models.Instance", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("AutoUpdateInterval") + .IsRequired() + .HasColumnType("INTEGER"); + + b.Property("ChatBotLimit") + .IsRequired() + .HasColumnType("INTEGER"); + + b.Property("ConfigurationType") + .HasColumnType("INTEGER"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("TEXT"); + + b.Property("Online") + .IsRequired() + .HasColumnType("INTEGER"); + + b.Property("Path") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("SwarmIdentifer") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("Path", "SwarmIdentifer") + .IsUnique(); + + b.ToTable("Instances"); + }); + + modelBuilder.Entity("Tgstation.Server.Host.Models.InstancePermissionSet", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("ChatBotRights") + .HasColumnType("INTEGER"); + + b.Property("ConfigurationRights") + .HasColumnType("INTEGER"); + + b.Property("DreamDaemonRights") + .HasColumnType("INTEGER"); + + b.Property("DreamMakerRights") + .HasColumnType("INTEGER"); + + b.Property("EngineRights") + .HasColumnType("INTEGER"); + + b.Property("InstanceId") + .HasColumnType("INTEGER"); + + b.Property("InstancePermissionSetRights") + .HasColumnType("INTEGER"); + + b.Property("PermissionSetId") + .HasColumnType("INTEGER"); + + b.Property("RepositoryRights") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("InstanceId"); + + b.HasIndex("PermissionSetId", "InstanceId") + .IsUnique(); + + b.ToTable("InstancePermissionSets"); + }); + + modelBuilder.Entity("Tgstation.Server.Host.Models.Job", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("CancelRight") + .HasColumnType("INTEGER"); + + b.Property("CancelRightsType") + .HasColumnType("INTEGER"); + + b.Property("Cancelled") + .IsRequired() + .HasColumnType("INTEGER"); + + b.Property("CancelledById") + .HasColumnType("INTEGER"); + + b.Property("Description") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("ErrorCode") + .HasColumnType("INTEGER"); + + b.Property("ExceptionDetails") + .HasColumnType("TEXT"); + + b.Property("InstanceId") + .HasColumnType("INTEGER"); + + b.Property("JobCode") + .HasColumnType("INTEGER"); + + b.Property("StartedAt") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("StartedById") + .HasColumnType("INTEGER"); + + b.Property("StoppedAt") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("CancelledById"); + + b.HasIndex("InstanceId"); + + b.HasIndex("StartedById"); + + b.ToTable("Jobs"); + }); + + modelBuilder.Entity("Tgstation.Server.Host.Models.OAuthConnection", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("ExternalUserId") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("TEXT"); + + b.Property("Provider") + .HasColumnType("INTEGER"); + + b.Property("UserId") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("UserId"); + + b.HasIndex("Provider", "ExternalUserId") + .IsUnique(); + + b.ToTable("OAuthConnections"); + }); + + modelBuilder.Entity("Tgstation.Server.Host.Models.PermissionSet", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("AdministrationRights") + .HasColumnType("INTEGER"); + + b.Property("GroupId") + .HasColumnType("INTEGER"); + + b.Property("InstanceManagerRights") + .HasColumnType("INTEGER"); + + b.Property("UserId") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("GroupId") + .IsUnique(); + + b.HasIndex("UserId") + .IsUnique(); + + b.ToTable("PermissionSets"); + }); + + modelBuilder.Entity("Tgstation.Server.Host.Models.ReattachInformation", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("AccessIdentifier") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("CompileJobId") + .HasColumnType("INTEGER"); + + b.Property("InitialCompileJobId") + .HasColumnType("INTEGER"); + + b.Property("LaunchSecurityLevel") + .HasColumnType("INTEGER"); + + b.Property("LaunchVisibility") + .HasColumnType("INTEGER"); + + b.Property("Port") + .HasColumnType("INTEGER"); + + b.Property("ProcessId") + .HasColumnType("INTEGER"); + + b.Property("RebootState") + .HasColumnType("INTEGER"); + + b.Property("TopicPort") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("CompileJobId"); + + b.HasIndex("InitialCompileJobId"); + + b.ToTable("ReattachInformations"); + }); + + modelBuilder.Entity("Tgstation.Server.Host.Models.RepositorySettings", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("AccessToken") + .HasMaxLength(10000) + .HasColumnType("TEXT"); + + b.Property("AccessUser") + .HasMaxLength(10000) + .HasColumnType("TEXT"); + + b.Property("AutoUpdatesKeepTestMerges") + .IsRequired() + .HasColumnType("INTEGER"); + + b.Property("AutoUpdatesSynchronize") + .IsRequired() + .HasColumnType("INTEGER"); + + b.Property("CommitterEmail") + .IsRequired() + .HasMaxLength(10000) + .HasColumnType("TEXT"); + + b.Property("CommitterName") + .IsRequired() + .HasMaxLength(10000) + .HasColumnType("TEXT"); + + b.Property("CreateGitHubDeployments") + .IsRequired() + .HasColumnType("INTEGER"); + + b.Property("InstanceId") + .HasColumnType("INTEGER"); + + b.Property("PostTestMergeComment") + .IsRequired() + .HasColumnType("INTEGER"); + + b.Property("PushTestMergeCommits") + .IsRequired() + .HasColumnType("INTEGER"); + + b.Property("ShowTestMergeCommitters") + .IsRequired() + .HasColumnType("INTEGER"); + + b.Property("UpdateSubmodules") + .IsRequired() + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("InstanceId") + .IsUnique(); + + b.ToTable("RepositorySettings"); + }); + + modelBuilder.Entity("Tgstation.Server.Host.Models.RevInfoTestMerge", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("RevisionInformationId") + .HasColumnType("INTEGER"); + + b.Property("TestMergeId") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("RevisionInformationId"); + + b.HasIndex("TestMergeId"); + + b.ToTable("RevInfoTestMerges"); + }); + + modelBuilder.Entity("Tgstation.Server.Host.Models.RevisionInformation", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("CommitSha") + .IsRequired() + .HasMaxLength(40) + .HasColumnType("TEXT"); + + b.Property("InstanceId") + .HasColumnType("INTEGER"); + + b.Property("OriginCommitSha") + .IsRequired() + .HasMaxLength(40) + .HasColumnType("TEXT"); + + b.Property("Timestamp") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("InstanceId", "CommitSha") + .IsUnique(); + + b.ToTable("RevisionInformations"); + }); + + modelBuilder.Entity("Tgstation.Server.Host.Models.TestMerge", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("Author") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("BodyAtMerge") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("Comment") + .HasMaxLength(10000) + .HasColumnType("TEXT"); + + b.Property("MergedAt") + .HasColumnType("TEXT"); + + b.Property("MergedById") + .HasColumnType("INTEGER"); + + b.Property("Number") + .HasColumnType("INTEGER"); + + b.Property("PrimaryRevisionInformationId") + .IsRequired() + .HasColumnType("INTEGER"); + + b.Property("TargetCommitSha") + .IsRequired() + .HasMaxLength(40) + .HasColumnType("TEXT"); + + b.Property("TitleAtMerge") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("Url") + .IsRequired() + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("MergedById"); + + b.HasIndex("PrimaryRevisionInformationId") + .IsUnique(); + + b.ToTable("TestMerges"); + }); + + modelBuilder.Entity("Tgstation.Server.Host.Models.User", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("CanonicalName") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("TEXT"); + + b.Property("CreatedAt") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("CreatedById") + .HasColumnType("INTEGER"); + + b.Property("Enabled") + .IsRequired() + .HasColumnType("INTEGER"); + + b.Property("GroupId") + .HasColumnType("INTEGER"); + + b.Property("LastPasswordUpdate") + .HasColumnType("TEXT"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("TEXT"); + + b.Property("PasswordHash") + .HasColumnType("TEXT"); + + b.Property("SystemIdentifier") + .HasMaxLength(100) + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("CanonicalName") + .IsUnique(); + + b.HasIndex("CreatedById"); + + b.HasIndex("GroupId"); + + b.HasIndex("SystemIdentifier") + .IsUnique(); + + b.ToTable("Users"); + }); + + modelBuilder.Entity("Tgstation.Server.Host.Models.UserGroup", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("Name") + .IsUnique(); + + b.ToTable("Groups"); + }); + + modelBuilder.Entity("Tgstation.Server.Host.Models.ChatBot", b => + { + b.HasOne("Tgstation.Server.Host.Models.Instance", "Instance") + .WithMany("ChatSettings") + .HasForeignKey("InstanceId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Instance"); + }); + + modelBuilder.Entity("Tgstation.Server.Host.Models.ChatChannel", b => + { + b.HasOne("Tgstation.Server.Host.Models.ChatBot", "ChatSettings") + .WithMany("Channels") + .HasForeignKey("ChatSettingsId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("ChatSettings"); + }); + + modelBuilder.Entity("Tgstation.Server.Host.Models.CompileJob", b => + { + b.HasOne("Tgstation.Server.Host.Models.Job", "Job") + .WithOne() + .HasForeignKey("Tgstation.Server.Host.Models.CompileJob", "JobId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Tgstation.Server.Host.Models.RevisionInformation", "RevisionInformation") + .WithMany("CompileJobs") + .HasForeignKey("RevisionInformationId") + .OnDelete(DeleteBehavior.ClientNoAction) + .IsRequired(); + + b.Navigation("Job"); + + b.Navigation("RevisionInformation"); + }); + + modelBuilder.Entity("Tgstation.Server.Host.Models.DreamDaemonSettings", b => + { + b.HasOne("Tgstation.Server.Host.Models.Instance", "Instance") + .WithOne("DreamDaemonSettings") + .HasForeignKey("Tgstation.Server.Host.Models.DreamDaemonSettings", "InstanceId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Instance"); + }); + + modelBuilder.Entity("Tgstation.Server.Host.Models.DreamMakerSettings", b => + { + b.HasOne("Tgstation.Server.Host.Models.Instance", "Instance") + .WithOne("DreamMakerSettings") + .HasForeignKey("Tgstation.Server.Host.Models.DreamMakerSettings", "InstanceId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Instance"); + }); + + modelBuilder.Entity("Tgstation.Server.Host.Models.InstancePermissionSet", b => + { + b.HasOne("Tgstation.Server.Host.Models.Instance", "Instance") + .WithMany("InstancePermissionSets") + .HasForeignKey("InstanceId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Tgstation.Server.Host.Models.PermissionSet", "PermissionSet") + .WithMany("InstancePermissionSets") + .HasForeignKey("PermissionSetId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Instance"); + + b.Navigation("PermissionSet"); + }); + + modelBuilder.Entity("Tgstation.Server.Host.Models.Job", b => + { + b.HasOne("Tgstation.Server.Host.Models.User", "CancelledBy") + .WithMany() + .HasForeignKey("CancelledById"); + + b.HasOne("Tgstation.Server.Host.Models.Instance", "Instance") + .WithMany("Jobs") + .HasForeignKey("InstanceId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Tgstation.Server.Host.Models.User", "StartedBy") + .WithMany() + .HasForeignKey("StartedById") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("CancelledBy"); + + b.Navigation("Instance"); + + b.Navigation("StartedBy"); + }); + + modelBuilder.Entity("Tgstation.Server.Host.Models.OAuthConnection", b => + { + b.HasOne("Tgstation.Server.Host.Models.User", "User") + .WithMany("OAuthConnections") + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Tgstation.Server.Host.Models.PermissionSet", b => + { + b.HasOne("Tgstation.Server.Host.Models.UserGroup", "Group") + .WithOne("PermissionSet") + .HasForeignKey("Tgstation.Server.Host.Models.PermissionSet", "GroupId") + .OnDelete(DeleteBehavior.Cascade); + + b.HasOne("Tgstation.Server.Host.Models.User", "User") + .WithOne("PermissionSet") + .HasForeignKey("Tgstation.Server.Host.Models.PermissionSet", "UserId") + .OnDelete(DeleteBehavior.Cascade); + + b.Navigation("Group"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Tgstation.Server.Host.Models.ReattachInformation", b => + { + b.HasOne("Tgstation.Server.Host.Models.CompileJob", "CompileJob") + .WithMany() + .HasForeignKey("CompileJobId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Tgstation.Server.Host.Models.CompileJob", "InitialCompileJob") + .WithMany() + .HasForeignKey("InitialCompileJobId"); + + b.Navigation("CompileJob"); + + b.Navigation("InitialCompileJob"); + }); + + modelBuilder.Entity("Tgstation.Server.Host.Models.RepositorySettings", b => + { + b.HasOne("Tgstation.Server.Host.Models.Instance", "Instance") + .WithOne("RepositorySettings") + .HasForeignKey("Tgstation.Server.Host.Models.RepositorySettings", "InstanceId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Instance"); + }); + + modelBuilder.Entity("Tgstation.Server.Host.Models.RevInfoTestMerge", b => + { + b.HasOne("Tgstation.Server.Host.Models.RevisionInformation", "RevisionInformation") + .WithMany("ActiveTestMerges") + .HasForeignKey("RevisionInformationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Tgstation.Server.Host.Models.TestMerge", "TestMerge") + .WithMany("RevisonInformations") + .HasForeignKey("TestMergeId") + .OnDelete(DeleteBehavior.ClientNoAction) + .IsRequired(); + + b.Navigation("RevisionInformation"); + + b.Navigation("TestMerge"); + }); + + modelBuilder.Entity("Tgstation.Server.Host.Models.RevisionInformation", b => + { + b.HasOne("Tgstation.Server.Host.Models.Instance", "Instance") + .WithMany("RevisionInformations") + .HasForeignKey("InstanceId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Instance"); + }); + + modelBuilder.Entity("Tgstation.Server.Host.Models.TestMerge", b => + { + b.HasOne("Tgstation.Server.Host.Models.User", "MergedBy") + .WithMany("TestMerges") + .HasForeignKey("MergedById") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("Tgstation.Server.Host.Models.RevisionInformation", "PrimaryRevisionInformation") + .WithOne("PrimaryTestMerge") + .HasForeignKey("Tgstation.Server.Host.Models.TestMerge", "PrimaryRevisionInformationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("MergedBy"); + + b.Navigation("PrimaryRevisionInformation"); + }); + + modelBuilder.Entity("Tgstation.Server.Host.Models.User", b => + { + b.HasOne("Tgstation.Server.Host.Models.User", "CreatedBy") + .WithMany("CreatedUsers") + .HasForeignKey("CreatedById"); + + b.HasOne("Tgstation.Server.Host.Models.UserGroup", "Group") + .WithMany("Users") + .HasForeignKey("GroupId"); + + b.Navigation("CreatedBy"); + + b.Navigation("Group"); + }); + + modelBuilder.Entity("Tgstation.Server.Host.Models.ChatBot", b => + { + b.Navigation("Channels"); + }); + + modelBuilder.Entity("Tgstation.Server.Host.Models.Instance", b => + { + b.Navigation("ChatSettings"); + + b.Navigation("DreamDaemonSettings"); + + b.Navigation("DreamMakerSettings"); + + b.Navigation("InstancePermissionSets"); + + b.Navigation("Jobs"); + + b.Navigation("RepositorySettings"); + + b.Navigation("RevisionInformations"); + }); + + modelBuilder.Entity("Tgstation.Server.Host.Models.PermissionSet", b => + { + b.Navigation("InstancePermissionSets"); + }); + + modelBuilder.Entity("Tgstation.Server.Host.Models.RevisionInformation", b => + { + b.Navigation("ActiveTestMerges"); + + b.Navigation("CompileJobs"); + + b.Navigation("PrimaryTestMerge"); + }); + + modelBuilder.Entity("Tgstation.Server.Host.Models.TestMerge", b => + { + b.Navigation("RevisonInformations"); + }); + + modelBuilder.Entity("Tgstation.Server.Host.Models.User", b => + { + b.Navigation("CreatedUsers"); + + b.Navigation("OAuthConnections"); + + b.Navigation("PermissionSet"); + + b.Navigation("TestMerges"); + }); + + modelBuilder.Entity("Tgstation.Server.Host.Models.UserGroup", b => + { + b.Navigation("PermissionSet") + .IsRequired(); + + b.Navigation("Users"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/src/Tgstation.Server.Host/Database/Migrations/20231220032528_SLAddTopicPort.cs b/src/Tgstation.Server.Host/Database/Migrations/20231220032528_SLAddTopicPort.cs new file mode 100644 index 00000000000..7ea822bd2ac --- /dev/null +++ b/src/Tgstation.Server.Host/Database/Migrations/20231220032528_SLAddTopicPort.cs @@ -0,0 +1,32 @@ +using System; + +using Microsoft.EntityFrameworkCore.Migrations; + +namespace Tgstation.Server.Host.Database.Migrations +{ + /// + public partial class SLAddTopicPort : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + ArgumentNullException.ThrowIfNull(migrationBuilder); + + migrationBuilder.AddColumn( + name: "TopicPort", + table: "ReattachInformations", + type: "INTEGER", + nullable: true); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + ArgumentNullException.ThrowIfNull(migrationBuilder); + + migrationBuilder.DropColumn( + name: "TopicPort", + table: "ReattachInformations"); + } + } +} diff --git a/src/Tgstation.Server.Host/Database/Migrations/MySqlDatabaseContextModelSnapshot.cs b/src/Tgstation.Server.Host/Database/Migrations/MySqlDatabaseContextModelSnapshot.cs index c5e6c701d7c..1f42bd3856b 100644 --- a/src/Tgstation.Server.Host/Database/Migrations/MySqlDatabaseContextModelSnapshot.cs +++ b/src/Tgstation.Server.Host/Database/Migrations/MySqlDatabaseContextModelSnapshot.cs @@ -4,8 +4,6 @@ using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore.Infrastructure; -#nullable disable - namespace Tgstation.Server.Host.Database.Migrations { [DbContext(typeof(MySqlDatabaseContext))] @@ -15,7 +13,7 @@ protected override void BuildModel(ModelBuilder modelBuilder) { #pragma warning disable 612, 618 modelBuilder - .HasAnnotation("ProductVersion", "7.0.13") + .HasAnnotation("ProductVersion", "8.0.0") .HasAnnotation("Relational:MaxIdentifierLength", 64); modelBuilder.Entity("Tgstation.Server.Host.Models.ChatBot", b => @@ -118,12 +116,6 @@ protected override void BuildModel(ModelBuilder modelBuilder) .ValueGeneratedOnAdd() .HasColumnType("bigint"); - b.Property("ByondVersion") - .IsRequired() - .HasColumnType("longtext"); - - MySqlPropertyBuilderExtensions.HasCharSet(b.Property("ByondVersion"), "utf8mb4"); - b.Property("DMApiMajorVersion") .HasColumnType("int"); @@ -143,6 +135,12 @@ protected override void BuildModel(ModelBuilder modelBuilder) MySqlPropertyBuilderExtensions.HasCharSet(b.Property("DmeName"), "utf8mb4"); + b.Property("EngineVersion") + .IsRequired() + .HasColumnType("longtext"); + + MySqlPropertyBuilderExtensions.HasCharSet(b.Property("EngineVersion"), "utf8mb4"); + b.Property("GitHubDeploymentId") .HasColumnType("int"); @@ -342,9 +340,6 @@ protected override void BuildModel(ModelBuilder modelBuilder) .ValueGeneratedOnAdd() .HasColumnType("bigint"); - b.Property("ByondRights") - .HasColumnType("bigint unsigned"); - b.Property("ChatBotRights") .HasColumnType("bigint unsigned"); @@ -357,6 +352,9 @@ protected override void BuildModel(ModelBuilder modelBuilder) b.Property("DreamMakerRights") .HasColumnType("bigint unsigned"); + b.Property("EngineRights") + .HasColumnType("bigint unsigned"); + b.Property("InstanceId") .HasColumnType("bigint"); @@ -499,7 +497,7 @@ protected override void BuildModel(ModelBuilder modelBuilder) modelBuilder.Entity("Tgstation.Server.Host.Models.ReattachInformation", b => { - b.Property("Id") + b.Property("Id") .ValueGeneratedOnAdd() .HasColumnType("bigint"); @@ -530,6 +528,9 @@ protected override void BuildModel(ModelBuilder modelBuilder) b.Property("RebootState") .HasColumnType("int"); + b.Property("TopicPort") + .HasColumnType("smallint unsigned"); + b.HasKey("Id"); b.HasIndex("CompileJobId"); diff --git a/src/Tgstation.Server.Host/Database/Migrations/PostgresSqlDatabaseContextModelSnapshot.cs b/src/Tgstation.Server.Host/Database/Migrations/PostgresSqlDatabaseContextModelSnapshot.cs index f550058f18e..631cf53bc9d 100644 --- a/src/Tgstation.Server.Host/Database/Migrations/PostgresSqlDatabaseContextModelSnapshot.cs +++ b/src/Tgstation.Server.Host/Database/Migrations/PostgresSqlDatabaseContextModelSnapshot.cs @@ -4,8 +4,6 @@ using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore.Infrastructure; -#nullable disable - namespace Tgstation.Server.Host.Database.Migrations { [DbContext(typeof(PostgresSqlDatabaseContext))] @@ -15,7 +13,7 @@ protected override void BuildModel(ModelBuilder modelBuilder) { #pragma warning disable 612, 618 modelBuilder - .HasAnnotation("ProductVersion", "7.0.13") + .HasAnnotation("ProductVersion", "8.0.0") .HasAnnotation("Relational:MaxIdentifierLength", 63); NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder); @@ -118,10 +116,6 @@ protected override void BuildModel(ModelBuilder modelBuilder) NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); - b.Property("ByondVersion") - .IsRequired() - .HasColumnType("text"); - b.Property("DMApiMajorVersion") .HasColumnType("integer"); @@ -139,6 +133,10 @@ protected override void BuildModel(ModelBuilder modelBuilder) .IsRequired() .HasColumnType("text"); + b.Property("EngineVersion") + .IsRequired() + .HasColumnType("text"); + b.Property("GitHubDeploymentId") .HasColumnType("integer"); @@ -324,9 +322,6 @@ protected override void BuildModel(ModelBuilder modelBuilder) NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); - b.Property("ByondRights") - .HasColumnType("numeric(20,0)"); - b.Property("ChatBotRights") .HasColumnType("numeric(20,0)"); @@ -339,6 +334,9 @@ protected override void BuildModel(ModelBuilder modelBuilder) b.Property("DreamMakerRights") .HasColumnType("numeric(20,0)"); + b.Property("EngineRights") + .HasColumnType("numeric(20,0)"); + b.Property("InstanceId") .HasColumnType("bigint"); @@ -481,11 +479,11 @@ protected override void BuildModel(ModelBuilder modelBuilder) modelBuilder.Entity("Tgstation.Server.Host.Models.ReattachInformation", b => { - b.Property("Id") + b.Property("Id") .ValueGeneratedOnAdd() .HasColumnType("bigint"); - NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); b.Property("AccessIdentifier") .IsRequired() @@ -512,6 +510,9 @@ protected override void BuildModel(ModelBuilder modelBuilder) b.Property("RebootState") .HasColumnType("integer"); + b.Property("TopicPort") + .HasColumnType("integer"); + b.HasKey("Id"); b.HasIndex("CompileJobId"); diff --git a/src/Tgstation.Server.Host/Database/Migrations/SqlServerDatabaseContextModelSnapshot.cs b/src/Tgstation.Server.Host/Database/Migrations/SqlServerDatabaseContextModelSnapshot.cs index f8a83396a92..1b1bb5276c0 100644 --- a/src/Tgstation.Server.Host/Database/Migrations/SqlServerDatabaseContextModelSnapshot.cs +++ b/src/Tgstation.Server.Host/Database/Migrations/SqlServerDatabaseContextModelSnapshot.cs @@ -4,8 +4,6 @@ using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore.Infrastructure; -#nullable disable - namespace Tgstation.Server.Host.Database.Migrations { [DbContext(typeof(SqlServerDatabaseContext))] @@ -15,7 +13,7 @@ protected override void BuildModel(ModelBuilder modelBuilder) { #pragma warning disable 612, 618 modelBuilder - .HasAnnotation("ProductVersion", "7.0.13") + .HasAnnotation("ProductVersion", "8.0.0") .HasAnnotation("Relational:MaxIdentifierLength", 128); SqlServerModelBuilderExtensions.UseIdentityColumns(modelBuilder); @@ -120,10 +118,6 @@ protected override void BuildModel(ModelBuilder modelBuilder) SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); - b.Property("ByondVersion") - .IsRequired() - .HasColumnType("nvarchar(max)"); - b.Property("DMApiMajorVersion") .HasColumnType("int"); @@ -141,6 +135,10 @@ protected override void BuildModel(ModelBuilder modelBuilder) .IsRequired() .HasColumnType("nvarchar(max)"); + b.Property("EngineVersion") + .IsRequired() + .HasColumnType("nvarchar(max)"); + b.Property("GitHubDeploymentId") .HasColumnType("int"); @@ -327,9 +325,6 @@ protected override void BuildModel(ModelBuilder modelBuilder) SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); - b.Property("ByondRights") - .HasColumnType("decimal(20,0)"); - b.Property("ChatBotRights") .HasColumnType("decimal(20,0)"); @@ -342,6 +337,9 @@ protected override void BuildModel(ModelBuilder modelBuilder) b.Property("DreamMakerRights") .HasColumnType("decimal(20,0)"); + b.Property("EngineRights") + .HasColumnType("decimal(20,0)"); + b.Property("InstanceId") .HasColumnType("bigint"); @@ -486,11 +484,11 @@ protected override void BuildModel(ModelBuilder modelBuilder) modelBuilder.Entity("Tgstation.Server.Host.Models.ReattachInformation", b => { - b.Property("Id") + b.Property("Id") .ValueGeneratedOnAdd() .HasColumnType("bigint"); - SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); b.Property("AccessIdentifier") .IsRequired() @@ -517,6 +515,9 @@ protected override void BuildModel(ModelBuilder modelBuilder) b.Property("RebootState") .HasColumnType("int"); + b.Property("TopicPort") + .HasColumnType("int"); + b.HasKey("Id"); b.HasIndex("CompileJobId"); diff --git a/src/Tgstation.Server.Host/Database/Migrations/SqliteDatabaseContextModelSnapshot.cs b/src/Tgstation.Server.Host/Database/Migrations/SqliteDatabaseContextModelSnapshot.cs index c5f426c4701..29d7469c653 100644 --- a/src/Tgstation.Server.Host/Database/Migrations/SqliteDatabaseContextModelSnapshot.cs +++ b/src/Tgstation.Server.Host/Database/Migrations/SqliteDatabaseContextModelSnapshot.cs @@ -4,8 +4,6 @@ using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore.Infrastructure; -#nullable disable - namespace Tgstation.Server.Host.Database.Migrations { [DbContext(typeof(SqliteDatabaseContext))] @@ -14,7 +12,7 @@ partial class SqliteDatabaseContextModelSnapshot : ModelSnapshot protected override void BuildModel(ModelBuilder modelBuilder) { #pragma warning disable 612, 618 - modelBuilder.HasAnnotation("ProductVersion", "7.0.13"); + modelBuilder.HasAnnotation("ProductVersion", "8.0.0"); modelBuilder.Entity("Tgstation.Server.Host.Models.ChatBot", b => { @@ -110,10 +108,6 @@ protected override void BuildModel(ModelBuilder modelBuilder) .ValueGeneratedOnAdd() .HasColumnType("INTEGER"); - b.Property("ByondVersion") - .IsRequired() - .HasColumnType("TEXT"); - b.Property("DMApiMajorVersion") .HasColumnType("INTEGER"); @@ -131,6 +125,10 @@ protected override void BuildModel(ModelBuilder modelBuilder) .IsRequired() .HasColumnType("TEXT"); + b.Property("EngineVersion") + .IsRequired() + .HasColumnType("TEXT"); + b.Property("GitHubDeploymentId") .HasColumnType("INTEGER"); @@ -316,9 +314,6 @@ protected override void BuildModel(ModelBuilder modelBuilder) .ValueGeneratedOnAdd() .HasColumnType("INTEGER"); - b.Property("ByondRights") - .HasColumnType("INTEGER"); - b.Property("ChatBotRights") .HasColumnType("INTEGER"); @@ -331,6 +326,9 @@ protected override void BuildModel(ModelBuilder modelBuilder) b.Property("DreamMakerRights") .HasColumnType("INTEGER"); + b.Property("EngineRights") + .HasColumnType("INTEGER"); + b.Property("InstanceId") .HasColumnType("INTEGER"); @@ -467,7 +465,7 @@ protected override void BuildModel(ModelBuilder modelBuilder) modelBuilder.Entity("Tgstation.Server.Host.Models.ReattachInformation", b => { - b.Property("Id") + b.Property("Id") .ValueGeneratedOnAdd() .HasColumnType("INTEGER"); @@ -496,6 +494,9 @@ protected override void BuildModel(ModelBuilder modelBuilder) b.Property("RebootState") .HasColumnType("INTEGER"); + b.Property("TopicPort") + .HasColumnType("INTEGER"); + b.HasKey("Id"); b.HasIndex("CompileJobId"); diff --git a/src/Tgstation.Server.Host/Database/MySqlDatabaseContext.cs b/src/Tgstation.Server.Host/Database/MySqlDatabaseContext.cs index 6d819dfb337..4bdcf9a066d 100644 --- a/src/Tgstation.Server.Host/Database/MySqlDatabaseContext.cs +++ b/src/Tgstation.Server.Host/Database/MySqlDatabaseContext.cs @@ -73,7 +73,7 @@ protected override void OnModelCreating(ModelBuilder modelBuilder) .MapMySqlTextField(x => x.ConnectionString) .MapMySqlTextField(x => x.Tag) .MapMySqlTextField(x => x.IrcChannel) - .MapMySqlTextField(x => x.ByondVersion) + .MapMySqlTextField(x => x.EngineVersion) .MapMySqlTextField(x => x.DmeName) .MapMySqlTextField(x => x.Output) .MapMySqlTextField(x => x.RepositoryOrigin) diff --git a/src/Tgstation.Server.Host/Database/SqlServerDatabaseContext.cs b/src/Tgstation.Server.Host/Database/SqlServerDatabaseContext.cs index a2b760b426b..3997e0a46b3 100644 --- a/src/Tgstation.Server.Host/Database/SqlServerDatabaseContext.cs +++ b/src/Tgstation.Server.Host/Database/SqlServerDatabaseContext.cs @@ -1,6 +1,5 @@ using System; -using Microsoft.Data.SqlClient; using Microsoft.EntityFrameworkCore; using Tgstation.Server.Host.Configuration; @@ -34,24 +33,8 @@ public static void ConfigureWith(DbContextOptionsBuilder options, DatabaseConfig if (databaseConfiguration.DatabaseType != DatabaseType.SqlServer) throw new InvalidOperationException($"Invalid DatabaseType for {nameof(SqlServerDatabaseContext)}!"); -#if NET7_0_OR_GREATER -#error Perform this breaking config change -#endif - - // Workaround for breaking change https://learn.microsoft.com/en-us/ef/core/what-is-new/ef-core-7.0/breaking-changes#encrypt-true - var connectionString = databaseConfiguration.ConnectionString; - if (!connectionString.Contains("Encrypt=", StringComparison.OrdinalIgnoreCase)) - { - var connectionStringBuilder = new SqlConnectionStringBuilder(databaseConfiguration.ConnectionString) - { - Encrypt = false, - }; - - connectionString = connectionStringBuilder.ToString(); - } - options.UseSqlServer( - connectionString, + databaseConfiguration.ConnectionString, sqlServerOptions => { sqlServerOptions.EnableRetryOnFailure(); diff --git a/src/Tgstation.Server.Host/Extensions/ApplicationBuilderExtensions.cs b/src/Tgstation.Server.Host/Extensions/ApplicationBuilderExtensions.cs index bc2a6dd3d72..637484f5bd1 100644 --- a/src/Tgstation.Server.Host/Extensions/ApplicationBuilderExtensions.cs +++ b/src/Tgstation.Server.Host/Extensions/ApplicationBuilderExtensions.cs @@ -132,11 +132,8 @@ public static void UseApiCompatibility(this IApplicationBuilder applicationBuild var apiHeadersProvider = context.RequestServices.GetRequiredService(); if (apiHeadersProvider.ApiHeaders?.Compatible() == false) { - await new JsonResult( + await new BadRequestObjectResult( new ErrorMessageResponse(ErrorCode.ApiMismatch)) - { - StatusCode = (int)HttpStatusCode.UpgradeRequired, - } .ExecuteResultAsync(new ActionContext { HttpContext = context, diff --git a/src/Tgstation.Server.Host/Extensions/ChatChannelExtensions.cs b/src/Tgstation.Server.Host/Extensions/ChatChannelExtensions.cs index edfb7bb4264..3b77bd0b1f3 100644 --- a/src/Tgstation.Server.Host/Extensions/ChatChannelExtensions.cs +++ b/src/Tgstation.Server.Host/Extensions/ChatChannelExtensions.cs @@ -23,7 +23,7 @@ static class ChatChannelExtensions /// /// The to retrieve information from. /// The IRC channel key stored in the if it exists, otherwise. - public static string GetIrcChannelKey(this ChatChannel chatChannel) + public static string? GetIrcChannelKey(this ChatChannel chatChannel) { var splits = GetIrcChannelSplits(chatChannel); if (splits.Count < 2) diff --git a/src/Tgstation.Server.Host/Extensions/ControllerBaseExtensions.cs b/src/Tgstation.Server.Host/Extensions/ControllerBaseExtensions.cs index 9ab3b1cc87e..818b0c44b59 100644 --- a/src/Tgstation.Server.Host/Extensions/ControllerBaseExtensions.cs +++ b/src/Tgstation.Server.Host/Extensions/ControllerBaseExtensions.cs @@ -1,7 +1,13 @@ using System; using System.Net; +using System.Net.Mime; +using Microsoft.AspNetCore.Hosting; using Microsoft.AspNetCore.Mvc; +using Microsoft.AspNetCore.StaticFiles; +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Primitives; +using Microsoft.Net.Http.Headers; using Tgstation.Server.Api.Models; using Tgstation.Server.Api.Models.Response; @@ -28,7 +34,40 @@ public static ObjectResult Gone(this ControllerBase controller) /// The . /// The accompanying payload. /// A with the given . - public static ObjectResult StatusCode(this ControllerBase controller, HttpStatusCode statusCode, object errorMessage) + public static ObjectResult StatusCode(this ControllerBase controller, HttpStatusCode statusCode, object? errorMessage) => controller?.StatusCode((int)statusCode, errorMessage) ?? throw new ArgumentNullException(nameof(controller)); + + /// + /// Try to serve a given file . + /// + /// The . + /// The . + /// The . + /// The path to the file in the 'wwwroot'. + /// A if the file was found. otherwise. + public static VirtualFileResult? TryServeFile(this ControllerBase controller, IWebHostEnvironment hostEnvironment, ILogger logger, string path) + { + ArgumentNullException.ThrowIfNull(controller); + ArgumentNullException.ThrowIfNull(hostEnvironment); + ArgumentNullException.ThrowIfNull(logger); + ArgumentNullException.ThrowIfNull(path); + + var fileInfo = hostEnvironment.WebRootFileProvider.GetFileInfo(path); + if (fileInfo.Exists) + { + logger.LogTrace("Serving static file \"{filename}\"...", path); + var contentTypeProvider = new FileExtensionContentTypeProvider(); + if (!contentTypeProvider.TryGetContentType(fileInfo.Name, out var contentType)) + contentType = MediaTypeNames.Application.Octet; + else if (contentType == MediaTypeNames.Application.Json) + controller.Response.Headers.Add( + HeaderNames.CacheControl, + new StringValues(new[] { "public", "max-age=31536000", "immutable" })); + + return controller.File(path, contentType); + } + + return null; + } } } diff --git a/src/Tgstation.Server.Host/Extensions/Converters/BoolConverter.cs b/src/Tgstation.Server.Host/Extensions/Converters/BoolConverter.cs index ece30522829..7812072f4a9 100644 --- a/src/Tgstation.Server.Host/Extensions/Converters/BoolConverter.cs +++ b/src/Tgstation.Server.Host/Extensions/Converters/BoolConverter.cs @@ -10,10 +10,18 @@ namespace Tgstation.Server.Host.Extensions.Converters sealed class BoolConverter : JsonConverter { /// - public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer) => writer.WriteValue(((bool)value) ? 1 : 0); + public override void WriteJson(JsonWriter? writer, object? value, JsonSerializer serializer) + { + ArgumentNullException.ThrowIfNull(writer); + writer.WriteValue(((bool)value!) ? 1 : 0); + } /// - public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer) => reader.Value.ToString() == "1"; + public override object? ReadJson(JsonReader? reader, Type? objectType, object? existingValue, JsonSerializer serializer) + { + ArgumentNullException.ThrowIfNull(reader); + return reader.Value!.ToString() == "1"; + } /// public override bool CanConvert(Type objectType) => objectType == typeof(bool); diff --git a/src/Tgstation.Server.Host/Extensions/Converters/VersionConverter.cs b/src/Tgstation.Server.Host/Extensions/Converters/VersionConverter.cs index 0373dc5e20e..48c593d72fb 100644 --- a/src/Tgstation.Server.Host/Extensions/Converters/VersionConverter.cs +++ b/src/Tgstation.Server.Host/Extensions/Converters/VersionConverter.cs @@ -21,7 +21,7 @@ public sealed class VersionConverter : JsonConverter, IYamlTypeConverter /// The to check. /// If the method should if validation fails. /// if is a , otherwise. - static bool CheckSupportsType(Type type, bool validate) + static bool CheckSupportsType(Type? type, bool validate) { ArgumentNullException.ThrowIfNull(type); @@ -33,7 +33,7 @@ static bool CheckSupportsType(Type type, bool validate) } /// - public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer) + public override void WriteJson(JsonWriter? writer, object? value, JsonSerializer serializer) { ArgumentNullException.ThrowIfNull(writer); @@ -52,7 +52,7 @@ public override void WriteJson(JsonWriter writer, object value, JsonSerializer s } /// - public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer) + public override object? ReadJson(JsonReader? reader, Type? objectType, object? existingValue, JsonSerializer serializer) { ArgumentNullException.ThrowIfNull(reader); @@ -65,7 +65,7 @@ public override object ReadJson(JsonReader reader, Type objectType, object exist { try { - var v = global::System.Version.Parse((string)reader.Value); + var v = global::System.Version.Parse((string)reader.Value!); return v.Semver(); } catch (Exception ex) @@ -88,7 +88,7 @@ public override object ReadJson(JsonReader reader, Type objectType, object exist public object ReadYaml(IParser parser, Type type) => throw new NotSupportedException("Deserialization not supported!"); // The default implementation is fine at handling this /// - public void WriteYaml(IEmitter emitter, object value, Type type) + public void WriteYaml(IEmitter? emitter, object? value, Type type) { ArgumentNullException.ThrowIfNull(emitter); diff --git a/src/Tgstation.Server.Host/Extensions/FetchOptionsExtensions.cs b/src/Tgstation.Server.Host/Extensions/FetchOptionsExtensions.cs new file mode 100644 index 00000000000..6d71a68edf8 --- /dev/null +++ b/src/Tgstation.Server.Host/Extensions/FetchOptionsExtensions.cs @@ -0,0 +1,91 @@ +using System; +using System.Threading; + +using LibGit2Sharp; +using LibGit2Sharp.Handlers; + +using Microsoft.Extensions.Logging; + +using Tgstation.Server.Host.Jobs; + +namespace Tgstation.Server.Host.Extensions +{ + /// + /// Extension methods for . + /// + static class FetchOptionsExtensions + { + /// + /// Hydrate a given set of . + /// + /// The to hydrate. + /// The for the operation. + /// The optional . + /// The optional . + /// The for the operation. + /// The hydrated . + public static FetchOptions Hydrate( + this FetchOptions fetchOptions, + ILogger logger, + JobProgressReporter? progressReporter, + CredentialsHandler credentialsHandler, + CancellationToken cancellationToken) + { + ArgumentNullException.ThrowIfNull(fetchOptions); + ArgumentNullException.ThrowIfNull(logger); + + fetchOptions.OnProgress = _ => !cancellationToken.IsCancellationRequested; + fetchOptions.OnTransferProgress = transferProgress => + { + if (progressReporter != null) + { + var percentage = ((double)transferProgress.IndexedObjects + transferProgress.ReceivedObjects) / (transferProgress.TotalObjects * 2); + progressReporter.ReportProgress(percentage); + } + + return !cancellationToken.IsCancellationRequested; + }; + fetchOptions.OnUpdateTips = (_, _, _) => !cancellationToken.IsCancellationRequested; + fetchOptions.CredentialsProvider = credentialsHandler; + fetchOptions.RepositoryOperationStarting = _ => !cancellationToken.IsCancellationRequested; + fetchOptions.OnTransferProgress = TransferProgressHandler( + logger, + progressReporter, + cancellationToken); + + return fetchOptions; + } + + /// + /// Generate a from a given and . + /// + /// The for the operation. + /// The optional of the operation. + /// The for the operation. + /// A new based on . + static TransferProgressHandler TransferProgressHandler(ILogger logger, JobProgressReporter? progressReporter, CancellationToken cancellationToken) => transferProgress => + { + double? percentage; + var totalObjectsToProcess = transferProgress.TotalObjects * 2; + var processedObjects = transferProgress.IndexedObjects + transferProgress.ReceivedObjects; + if (totalObjectsToProcess < processedObjects || totalObjectsToProcess == 0) + percentage = null; + else + { + percentage = (double)processedObjects / totalObjectsToProcess; + if (percentage < 0) + percentage = null; + } + + if (percentage == null) + logger.LogDebug( + "Bad transfer progress values (Please tell Cyberboss)! Indexed: {indexed}, Received: {received}, Total: {total}", + transferProgress.IndexedObjects, + transferProgress.ReceivedObjects, + transferProgress.TotalObjects); + + progressReporter?.ReportProgress(percentage); + return !cancellationToken.IsCancellationRequested; + }; + } +} diff --git a/src/Tgstation.Server.Host/Extensions/FileTransferStreamHandlerExtensions.cs b/src/Tgstation.Server.Host/Extensions/FileTransferStreamHandlerExtensions.cs index a7e84abac19..5bae10e79bd 100644 --- a/src/Tgstation.Server.Host/Extensions/FileTransferStreamHandlerExtensions.cs +++ b/src/Tgstation.Server.Host/Extensions/FileTransferStreamHandlerExtensions.cs @@ -67,7 +67,9 @@ public static async ValueTask GenerateDownloadResponse( } catch { - await stream.DisposeAsync(); + if (stream != null) + await stream.DisposeAsync(); + throw; } } diff --git a/src/Tgstation.Server.Host/Extensions/ModelBuilderExtensions.cs b/src/Tgstation.Server.Host/Extensions/ModelBuilderExtensions.cs index f503f2c7179..0db51009526 100644 --- a/src/Tgstation.Server.Host/Extensions/ModelBuilderExtensions.cs +++ b/src/Tgstation.Server.Host/Extensions/ModelBuilderExtensions.cs @@ -23,7 +23,7 @@ static class ModelBuilderExtensions /// . public static ModelBuilder MapMySqlTextField( this ModelBuilder modelBuilder, - Expression> expression) + Expression> expression) where TEntity : class { var property = modelBuilder @@ -47,7 +47,7 @@ public static ModelBuilder MapMySqlTextField( /// The entity. /// The accessing the relevant property. /// The pointed to by . - static PropertyInfo GetPropertyFromExpression(Expression> expression) + static PropertyInfo GetPropertyFromExpression(Expression> expression) { MemberExpression memberExpression; diff --git a/src/Tgstation.Server.Host/Extensions/ResultExtensions.cs b/src/Tgstation.Server.Host/Extensions/ResultExtensions.cs index 5d4370734f6..f1157f22866 100644 --- a/src/Tgstation.Server.Host/Extensions/ResultExtensions.cs +++ b/src/Tgstation.Server.Host/Extensions/ResultExtensions.cs @@ -116,7 +116,7 @@ static void FormatErrorDetails(IPropertyErrorDetails propertyErrorDetails, Strin /// /// The of . /// The to mutate. - static void FormatErrorDetails(IEnumerable errorDetails, StringBuilder stringBuilder) + static void FormatErrorDetails(IEnumerable? errorDetails, StringBuilder stringBuilder) { if (errorDetails == null) return; diff --git a/src/Tgstation.Server.Host/Extensions/ServiceCollectionExtensions.cs b/src/Tgstation.Server.Host/Extensions/ServiceCollectionExtensions.cs index c90433f1ff0..cc7eb227a93 100644 --- a/src/Tgstation.Server.Host/Extensions/ServiceCollectionExtensions.cs +++ b/src/Tgstation.Server.Host/Extensions/ServiceCollectionExtensions.cs @@ -28,22 +28,22 @@ static class ServiceCollectionExtensions /// /// The implementation used in calls to . /// - static Type chatProviderFactoryType; + static Type? chatProviderFactoryType; /// /// The implementation used in calls to . /// - static Type gitHubServiceFactoryType; + static Type? gitHubServiceFactoryType; /// /// The implementation used in calls to . /// - static Type fileDownloaderType; + static Type? fileDownloaderType; /// /// A for an additional to use. /// - static ServiceDescriptor additionalLoggerProvider; + static ServiceDescriptor? additionalLoggerProvider; /// /// Initializes static members of the class. @@ -92,7 +92,7 @@ public static IServiceCollection AddFileDownloader(this IServiceCollection servi { ArgumentNullException.ThrowIfNull(serviceCollection); - serviceCollection.AddSingleton(typeof(IFileDownloader), fileDownloaderType); + serviceCollection.AddSingleton(typeof(IFileDownloader), fileDownloaderType ?? throw new InvalidOperationException("fileDownloaderType not set!")); return serviceCollection; } @@ -107,7 +107,7 @@ public static IServiceCollection AddGitHub(this IServiceCollection serviceCollec ArgumentNullException.ThrowIfNull(serviceCollection); serviceCollection.AddSingleton(); - serviceCollection.AddSingleton(typeof(IGitHubServiceFactory), gitHubServiceFactoryType); + serviceCollection.AddSingleton(typeof(IGitHubServiceFactory), gitHubServiceFactoryType ?? throw new InvalidOperationException("gitHubServiceFactoryType not set!")); return serviceCollection; } @@ -133,7 +133,7 @@ public static IServiceCollection AddChatProviderFactory(this IServiceCollection { ArgumentNullException.ThrowIfNull(serviceCollection); - return serviceCollection.AddSingleton(typeof(IProviderFactory), chatProviderFactoryType); + return serviceCollection.AddSingleton(typeof(IProviderFactory), chatProviderFactoryType ?? throw new InvalidOperationException("chatProviderFactoryType not set!")); } /// @@ -158,7 +158,7 @@ public static IServiceCollection UseStandardConfig(this IServiceCollect if (sectionField.FieldType != stringType) throw new InvalidOperationException(String.Format(CultureInfo.InvariantCulture, "{0} has invalid {1} field type, must be {2}!", configType, SectionFieldName, stringType)); - var sectionName = (string)sectionField.GetValue(null); + var sectionName = (string)sectionField.GetValue(null)!; return serviceCollection.Configure(configuration.GetSection(sectionName)); } @@ -176,10 +176,10 @@ public static IServiceCollection UseStandardConfig(this IServiceCollect public static IServiceCollection SetupLogging( this IServiceCollection serviceCollection, Action configurationAction, - Action sinkConfigurationAction = null, - ElasticsearchSinkOptions elasticsearchSinkOptions = null, - InternalConfiguration internalConfiguration = null, - FileLoggingConfiguration fileLoggingConfiguration = null) + Action? sinkConfigurationAction = null, + ElasticsearchSinkOptions? elasticsearchSinkOptions = null, + InternalConfiguration? internalConfiguration = null, + FileLoggingConfiguration? fileLoggingConfiguration = null) { if (internalConfiguration != null) ArgumentNullException.ThrowIfNull(fileLoggingConfiguration); @@ -203,7 +203,7 @@ public static IServiceCollection SetupLogging( + SerilogContextHelper.Template + "){NewLine} {Message:lj}{NewLine}{Exception}"; - if (!((internalConfiguration?.UsingSystemD ?? false) && !fileLoggingConfiguration.Disable)) + if (!((internalConfiguration?.UsingSystemD ?? false) && !(fileLoggingConfiguration?.Disable ?? false))) sinkConfiguration.Console(outputTemplate: template, formatProvider: CultureInfo.InvariantCulture); sinkConfigurationAction?.Invoke(sinkConfiguration); }); diff --git a/src/Tgstation.Server.Host/Extensions/SocketExtensions.cs b/src/Tgstation.Server.Host/Extensions/SocketExtensions.cs index a9e6a975327..6c1a695025e 100644 --- a/src/Tgstation.Server.Host/Extensions/SocketExtensions.cs +++ b/src/Tgstation.Server.Host/Extensions/SocketExtensions.cs @@ -1,6 +1,9 @@ -using System.Net; +using System; +using System.Net; using System.Net.Sockets; +using Tgstation.Server.Host.System; + namespace Tgstation.Server.Host.Extensions { /// @@ -11,27 +14,41 @@ static class SocketExtensions /// /// Attempt to exclusively bind to a given . /// + /// The to use. /// The port number to bind to. /// If IPV6 should be tested as well. - public static void BindTest(ushort port, bool includeIPv6) + /// If we're bind testing for UDP. If TCP will be checked. + public static void BindTest(IPlatformIdentifier platformIdentifier, ushort port, bool includeIPv6, bool udp) { - using var socket = new Socket( - includeIPv6 - ? AddressFamily.InterNetworkV6 - : AddressFamily.InterNetwork, - SocketType.Stream, - ProtocolType.Tcp); - socket.SetSocketOption(SocketOptionLevel.Socket, SocketOptionName.ExclusiveAddressUse, true); - socket.SetSocketOption(SocketOptionLevel.Socket, SocketOptionName.ReuseAddress, false); - if (includeIPv6) - socket.DualMode = true; - - socket.Bind( - new IPEndPoint( + ArgumentNullException.ThrowIfNull(platformIdentifier); + ProcessExecutor.WithProcessLaunchExclusivity(() => + { + using var socket = new Socket( includeIPv6 - ? IPAddress.IPv6Any - : IPAddress.Any, - port)); + ? AddressFamily.InterNetworkV6 + : AddressFamily.InterNetwork, + udp + ? SocketType.Dgram + : SocketType.Stream, + udp + ? ProtocolType.Udp + : ProtocolType.Tcp); + + socket.SetSocketOption(SocketOptionLevel.Socket, SocketOptionName.ExclusiveAddressUse, true); + socket.SetSocketOption(SocketOptionLevel.Socket, SocketOptionName.ReuseAddress, false); + if (!udp && platformIdentifier.IsWindows) + socket.SetSocketOption(SocketOptionLevel.Socket, SocketOptionName.DontLinger, true); + + if (includeIPv6) + socket.DualMode = true; + + socket.Bind( + new IPEndPoint( + includeIPv6 + ? IPAddress.IPv6Any + : IPAddress.Any, + port)); + }); } } } diff --git a/src/Tgstation.Server.Host/Extensions/TaskExtensions.cs b/src/Tgstation.Server.Host/Extensions/TaskExtensions.cs index 68f8f698744..55b6701dbbf 100644 --- a/src/Tgstation.Server.Host/Extensions/TaskExtensions.cs +++ b/src/Tgstation.Server.Host/Extensions/TaskExtensions.cs @@ -10,7 +10,7 @@ static class TaskExtensions /// /// A that never completes. /// - static readonly TaskCompletionSource InfiniteTaskCompletionSource = new (); + static readonly TaskCompletionSource InfiniteTaskCompletionSource = new(); /// /// Gets a that never completes. diff --git a/src/Tgstation.Server.Host/Extensions/TopicClientExtensions.cs b/src/Tgstation.Server.Host/Extensions/TopicClientExtensions.cs new file mode 100644 index 00000000000..3a7fb564036 --- /dev/null +++ b/src/Tgstation.Server.Host/Extensions/TopicClientExtensions.cs @@ -0,0 +1,68 @@ +using System; +using System.Net; +using System.Threading; +using System.Threading.Tasks; + +using Byond.TopicSender; + +using Microsoft.Extensions.Logging; + +using Tgstation.Server.Host.Utils; + +namespace Tgstation.Server.Host.Extensions +{ + /// + /// Extension methods for . + /// + static class TopicClientExtensions + { + /// + /// Send a with optional repeated priority. + /// + /// The to send with. + /// The to use for delayed retries if an error occurs. + /// The to write to. + /// The to send. + /// The local port to send the topic to. + /// If priority retries should be used. + /// The for the operation. + /// A resulting in the on success, on failure. + public static async ValueTask SendWithOptionalPriority( + this ITopicClient topicClient, + IAsyncDelayer delayer, + ILogger logger, + string queryString, + ushort port, + bool priority, + CancellationToken cancellationToken) + { + const int PrioritySendAttempts = 5; + var endpoint = new IPEndPoint(IPAddress.Loopback, port); + var firstSend = true; + + for (var i = PrioritySendAttempts - 1; i >= 0 && (priority || firstSend); --i) + try + { + firstSend = false; + + logger.LogTrace("Begin topic request"); + var byondResponse = await topicClient.SendTopic( + endpoint, + queryString, + cancellationToken); + + logger.LogTrace("End topic request"); + return byondResponse; + } + catch (Exception ex) when (ex is not OperationCanceledException) + { + logger.LogWarning(ex, "SendTopic exception!{retryDetails}", priority ? $" {i} attempts remaining." : String.Empty); + + if (priority && i > 0) + await delayer.Delay(TimeSpan.FromSeconds(2), cancellationToken); + } + + return null; + } + } +} diff --git a/src/Tgstation.Server.Host/IO/BufferedFileStreamProvider.cs b/src/Tgstation.Server.Host/IO/BufferedFileStreamProvider.cs index a369f410478..d1310029df3 100644 --- a/src/Tgstation.Server.Host/IO/BufferedFileStreamProvider.cs +++ b/src/Tgstation.Server.Host/IO/BufferedFileStreamProvider.cs @@ -28,7 +28,7 @@ public sealed class BufferedFileStreamProvider : ISeekableFileStreamProvider /// /// The backing . /// - volatile MemoryStream buffer; + volatile MemoryStream? buffer; /// /// If has been populated. @@ -58,7 +58,7 @@ public BufferedFileStreamProvider(Stream input) /// public async ValueTask DisposeAsync() { - MemoryStream localBuffer; + MemoryStream? localBuffer; lock (semaphore) { localBuffer = buffer; @@ -102,7 +102,7 @@ public async ValueTask GetOwnedResult(CancellationToken cancellati /// /// The for the operation. /// A resulting in and its . - async ValueTask<(MemoryStream, long)> GetResultInternal(CancellationToken cancellationToken) + async ValueTask<(MemoryStream Stream, long StreamLength)> GetResultInternal(CancellationToken cancellationToken) { if (!buffered) using (await SemaphoreSlimContext.Lock(semaphore, cancellationToken)) @@ -115,15 +115,15 @@ public async ValueTask GetOwnedResult(CancellationToken cancellati await input.CopyToAsync(localBuffer, cancellationToken); localBuffer.Seek(0, SeekOrigin.Begin); buffered = true; - return (localBuffer, localBuffer.Length); + return (Stream: localBuffer, StreamLength: localBuffer.Length); } lock (semaphore) { var localBuffer = buffer ?? throw new ObjectDisposedException(nameof(BufferedFileStreamProvider)); return ( - localBuffer, - localBuffer.Length); + Stream: localBuffer, + StreamLength: localBuffer.Length); } } } diff --git a/src/Tgstation.Server.Host/IO/Console.cs b/src/Tgstation.Server.Host/IO/Console.cs index fdf06eea52c..6a94540bb0b 100644 --- a/src/Tgstation.Server.Host/IO/Console.cs +++ b/src/Tgstation.Server.Host/IO/Console.cs @@ -13,13 +13,9 @@ 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 string? Title => platformIdentifier.IsWindows + ? global::System.Console.Title + : null; /// public bool Available => Environment.UserInteractive; @@ -89,7 +85,8 @@ public Task ReadLineAsync(bool usePasswordChar, CancellationToken cancel // TODO: Make this better: https://stackoverflow.com/questions/9479573/how-to-interrupt-console-readline CheckAvailable(); if (!usePasswordChar) - return global::System.Console.ReadLine(); + return global::System.Console.ReadLine() + ?? throw new InvalidOperationException("Console input has been closed!"); var passwordBuilder = new StringBuilder(); do @@ -124,7 +121,7 @@ public Task ReadLineAsync(bool usePasswordChar, CancellationToken cancel .WaitAsync(cancellationToken); /// - public Task WriteAsync(string text, bool newLine, CancellationToken cancellationToken) => Task.Factory.StartNew( + public Task WriteAsync(string? text, bool newLine, CancellationToken cancellationToken) => Task.Factory.StartNew( () => { CheckAvailable(); @@ -143,6 +140,13 @@ public Task WriteAsync(string text, bool newLine, CancellationToken cancellation DefaultIOManager.BlockingTaskCreationOptions, TaskScheduler.Current); + /// + public void SetTitle(string newTitle) + { + ArgumentNullException.ThrowIfNull(newTitle); + global::System.Console.Title = newTitle; + } + /// /// Assert that the is available, throwing an otherwise. /// diff --git a/src/Tgstation.Server.Host/IO/DefaultIOManager.cs b/src/Tgstation.Server.Host/IO/DefaultIOManager.cs index 8416ca9bf37..cdae64ce445 100644 --- a/src/Tgstation.Server.Host/IO/DefaultIOManager.cs +++ b/src/Tgstation.Server.Host/IO/DefaultIOManager.cs @@ -69,8 +69,8 @@ static void NormalizeAndDelete(DirectoryInfo dir, CancellationToken cancellation /// public async ValueTask CopyDirectory( - IEnumerable ignore, - Func postCopyCallback, + IEnumerable? ignore, + Func? postCopyCallback, string src, string dest, int? taskThrottle, @@ -140,7 +140,8 @@ public Task DeleteDirectory(string path, CancellationToken cancellationToken) public Task DirectoryExists(string path, CancellationToken cancellationToken) => Task.Factory.StartNew(() => Directory.Exists(ResolvePath(path)), cancellationToken, BlockingTaskCreationOptions, TaskScheduler.Current); /// - public string GetDirectoryName(string path) => Path.GetDirectoryName(path ?? throw new ArgumentNullException(nameof(path))); + public string GetDirectoryName(string path) => Path.GetDirectoryName(path ?? throw new ArgumentNullException(nameof(path))) + ?? throw new InvalidOperationException($"Null was returned. Path ({path}) must be rooted. This is not supported!"); /// public string GetFileName(string path) => Path.GetFileName(path ?? throw new ArgumentNullException(nameof(path))); @@ -284,8 +285,8 @@ public Task ZipToDirectory(string path, Stream zipFile, CancellationToken cancel path = ResolvePath(path); ArgumentNullException.ThrowIfNull(zipFile); -#if NET7_0_OR_GREATER -#warning Check if zip file seeking has been addressesed. See https://github.com/tgstation/tgstation-server/issues/1531 +#if NET9_0_OR_GREATER +#error Check if zip file seeking has been addressesed. See https://github.com/tgstation/tgstation-server/issues/1531 #endif // ZipArchive does a synchronous copy on unseekable streams we want to avoid @@ -302,11 +303,10 @@ public Task ZipToDirectory(string path, Stream zipFile, CancellationToken cancel /// public bool PathContainsParentAccess(string path) => path ?.Split( - new[] - { + [ Path.DirectorySeparatorChar, Path.AltDirectorySeparatorChar, - }) + ]) .Any(x => x == "..") ?? throw new ArgumentNullException(nameof(path)); @@ -323,7 +323,7 @@ public Task GetLastModified(string path, CancellationToken cance TaskScheduler.Current); /// - public FileStream GetFileStream(string path, bool shareWrite) => new ( + public FileStream GetFileStream(string path, bool shareWrite) => new( ResolvePath(path), FileMode.Open, FileAccess.Read, @@ -336,7 +336,7 @@ public Task GetLastModified(string path, CancellationToken cance /// /// The source directory path. /// The destination directory path. - /// Files and folders to ignore at the root level. + /// Optional files and folders to ignore at the root level. /// The optional callback called for each source/dest file pair post copy. /// Optional used to limit degree of parallelism. /// The for the operation. @@ -344,13 +344,13 @@ public Task GetLastModified(string path, CancellationToken cance IEnumerable CopyDirectoryImpl( string src, string dest, - IEnumerable ignore, - Func postCopyCallback, - SemaphoreSlim semaphore, + IEnumerable? ignore, + Func? postCopyCallback, + SemaphoreSlim? semaphore, CancellationToken cancellationToken) { var dir = new DirectoryInfo(src); - Task subdirCreationTask = null; + Task? subdirCreationTask = null; foreach (var subDirectory in dir.EnumerateDirectories()) { if (ignore != null && ignore.Contains(subDirectory.Name)) diff --git a/src/Tgstation.Server.Host/IO/FileDownloader.cs b/src/Tgstation.Server.Host/IO/FileDownloader.cs index 1aa554e647c..30efdb6718d 100644 --- a/src/Tgstation.Server.Host/IO/FileDownloader.cs +++ b/src/Tgstation.Server.Host/IO/FileDownloader.cs @@ -34,7 +34,7 @@ public FileDownloader(IAbstractHttpClientFactory httpClientFactory, ILogger - public IFileStreamProvider DownloadFile(Uri url, string bearerToken) + public IFileStreamProvider DownloadFile(Uri url, string? bearerToken) { ArgumentNullException.ThrowIfNull(url); diff --git a/src/Tgstation.Server.Host/IO/IConsole.cs b/src/Tgstation.Server.Host/IO/IConsole.cs index 605f3e421d9..ee1e9488917 100644 --- a/src/Tgstation.Server.Host/IO/IConsole.cs +++ b/src/Tgstation.Server.Host/IO/IConsole.cs @@ -11,7 +11,7 @@ interface IConsole /// /// Gets or sets the window's title. Can return if getting the console title is not supported. /// - string Title { get; set; } + string? Title { get; } /// /// If the is visible to the user. @@ -30,7 +30,7 @@ interface IConsole /// If there should be a new line after the . /// The for the operation. /// A representing the running operation. - Task WriteAsync(string text, bool newLine, CancellationToken cancellationToken); + Task WriteAsync(string? text, bool newLine, CancellationToken cancellationToken); /// /// Wait for a key press on the . @@ -46,5 +46,11 @@ interface IConsole /// The for the operation. /// A resulting in the read by the . Task ReadLineAsync(bool usePasswordChar, CancellationToken cancellationToken); + + /// + /// Sets a console window. + /// + /// The new . + void SetTitle(string newTitle); } } diff --git a/src/Tgstation.Server.Host/IO/IFileDownloader.cs b/src/Tgstation.Server.Host/IO/IFileDownloader.cs index 5a2c9891709..446700e55f2 100644 --- a/src/Tgstation.Server.Host/IO/IFileDownloader.cs +++ b/src/Tgstation.Server.Host/IO/IFileDownloader.cs @@ -13,6 +13,6 @@ interface IFileDownloader /// The URL to download. /// Optional to use as the "Bearer" value in the optional "Authorization" header for the request. /// A new for the downloaded file. - IFileStreamProvider DownloadFile(Uri url, string bearerToken); + IFileStreamProvider DownloadFile(Uri url, string? bearerToken); } } diff --git a/src/Tgstation.Server.Host/IO/IFileStreamProvider.cs b/src/Tgstation.Server.Host/IO/IFileStreamProvider.cs index e27f779314f..6b1670194d0 100644 --- a/src/Tgstation.Server.Host/IO/IFileStreamProvider.cs +++ b/src/Tgstation.Server.Host/IO/IFileStreamProvider.cs @@ -14,7 +14,7 @@ public interface IFileStreamProvider : IAsyncDisposable /// Gets the provided . May be called multiple times, though cancelling any may cause all calls to be cancelled. All calls yield the same reference. /// /// The for the operation. - /// A resulting in the provided on success, if it could not be provided. + /// A resulting in the provided . /// The resulting is owned by the and is short lived unless otherwise specified. It should be buffered if it needs use outside the lifetime of the . ValueTask GetResult(CancellationToken cancellationToken); } diff --git a/src/Tgstation.Server.Host/IO/IIOManager.cs b/src/Tgstation.Server.Host/IO/IIOManager.cs index 045036e98cc..d88ec3ec176 100644 --- a/src/Tgstation.Server.Host/IO/IIOManager.cs +++ b/src/Tgstation.Server.Host/IO/IIOManager.cs @@ -56,8 +56,8 @@ public interface IIOManager /// The for the operation. /// A representing the running operation. ValueTask CopyDirectory( - IEnumerable ignore, - Func postCopyCallback, + IEnumerable? ignore, + Func? postCopyCallback, string src, string dest, int? taskThrottle, @@ -88,7 +88,7 @@ ValueTask CopyDirectory( ValueTask ReadAllBytes(string path, CancellationToken cancellationToken); /// - /// Returns directory names in a given . + /// Returns full directory names in a given . /// /// The path to search for directories. /// The for the operation. @@ -96,7 +96,7 @@ ValueTask CopyDirectory( Task> GetDirectories(string path, CancellationToken cancellationToken); /// - /// Returns file names in a given . + /// Returns full file names in a given . /// /// The path to search for files. /// The for the operation. @@ -133,6 +133,7 @@ ValueTask CopyDirectory( /// /// A path to check. /// The directory portion of the given . + /// If is rooted. string GetDirectoryName(string path); /// diff --git a/src/Tgstation.Server.Host/IO/ISeekableFileStreamProvider.cs b/src/Tgstation.Server.Host/IO/ISeekableFileStreamProvider.cs index 7a6b76eb0d4..42cfc869534 100644 --- a/src/Tgstation.Server.Host/IO/ISeekableFileStreamProvider.cs +++ b/src/Tgstation.Server.Host/IO/ISeekableFileStreamProvider.cs @@ -18,7 +18,7 @@ public interface ISeekableFileStreamProvider : IFileStreamProvider /// Gets the provided . May be called multiple times, though cancelling any may cause all calls to be cancelled. /// /// The for the operation. - /// A resulting in the provided on success, if it could not be provided. + /// A resulting in the provided on success. ValueTask GetOwnedResult(CancellationToken cancellationToken); } } diff --git a/src/Tgstation.Server.Host/IO/ISynchronousIOManager.cs b/src/Tgstation.Server.Host/IO/ISynchronousIOManager.cs index 36f90140aa1..16e0ea370e8 100644 --- a/src/Tgstation.Server.Host/IO/ISynchronousIOManager.cs +++ b/src/Tgstation.Server.Host/IO/ISynchronousIOManager.cs @@ -55,7 +55,7 @@ interface ISynchronousIOManager /// The function only succeeds if this parameter matches the SHA-1 hash of the contents of the current file. Contains the SHA1 of the file on disk once the function returns. /// The for the operation. /// on success, if the operation failed due to not matching the file's contents. - bool WriteFileChecked(string path, Stream data, ref string sha1InOut, CancellationToken cancellationToken); + bool WriteFileChecked(string path, Stream data, ref string? sha1InOut, CancellationToken cancellationToken); /// /// Checks if a given is a directory. diff --git a/src/Tgstation.Server.Host/IO/RequestFileStreamProvider.cs b/src/Tgstation.Server.Host/IO/RequestFileStreamProvider.cs index c0c100cfcf7..12461e4e7f3 100644 --- a/src/Tgstation.Server.Host/IO/RequestFileStreamProvider.cs +++ b/src/Tgstation.Server.Host/IO/RequestFileStreamProvider.cs @@ -31,7 +31,7 @@ sealed class RequestFileStreamProvider : IFileStreamProvider /// /// The resulting in the downloaded . /// - Task downloadTask; + Task? downloadTask; /// /// If has been called. @@ -54,7 +54,7 @@ public RequestFileStreamProvider(IHttpClient httpClient, HttpRequestMessage requ /// public async ValueTask DisposeAsync() { - Task localDownloadTask; + Task? localDownloadTask; lock (downloadCts) { if (disposed) diff --git a/src/Tgstation.Server.Host/IO/SynchronousIOManager.cs b/src/Tgstation.Server.Host/IO/SynchronousIOManager.cs index a629ddcff44..4dbf3d833bf 100644 --- a/src/Tgstation.Server.Host/IO/SynchronousIOManager.cs +++ b/src/Tgstation.Server.Host/IO/SynchronousIOManager.cs @@ -72,14 +72,13 @@ public byte[] ReadFile(string path) } /// - public bool WriteFileChecked(string path, Stream data, ref string sha1InOut, CancellationToken cancellationToken) + public bool WriteFileChecked(string path, Stream data, ref string? sha1InOut, CancellationToken cancellationToken) { ArgumentNullException.ThrowIfNull(path); ArgumentNullException.ThrowIfNull(data); cancellationToken.ThrowIfCancellationRequested(); - var directory = Path.GetDirectoryName(path); - + var directory = Path.GetDirectoryName(path) ?? throw new ArgumentException("path cannot be rooted!", nameof(path)); Directory.CreateDirectory(directory); var newFile = !File.Exists(path); @@ -97,7 +96,7 @@ public bool WriteFileChecked(string path, Stream data, ref string sha1InOut, Can // suppressed due to only using for consistency checks using (var sha1 = SHA1.Create()) { - string GetSha1(Stream dataToHash) + string? GetSha1(Stream dataToHash) { if (dataToHash == null) return null; diff --git a/src/Tgstation.Server.Host/IO/WindowsFilesystemLinkFactory.cs b/src/Tgstation.Server.Host/IO/WindowsFilesystemLinkFactory.cs index 1513f4e1666..6636838ab42 100644 --- a/src/Tgstation.Server.Host/IO/WindowsFilesystemLinkFactory.cs +++ b/src/Tgstation.Server.Host/IO/WindowsFilesystemLinkFactory.cs @@ -4,6 +4,7 @@ using System.Threading.Tasks; using BetterWin32Errors; + using Tgstation.Server.Host.System; namespace Tgstation.Server.Host.IO diff --git a/src/Tgstation.Server.Host/IServerFactory.cs b/src/Tgstation.Server.Host/IServerFactory.cs index 46f95105231..b205d6b62e2 100644 --- a/src/Tgstation.Server.Host/IServerFactory.cs +++ b/src/Tgstation.Server.Host/IServerFactory.cs @@ -22,6 +22,6 @@ public interface IServerFactory /// The directory in which to install server updates. /// The for the operation. /// A resulting in a new if it should be run, otherwise. - ValueTask CreateServer(string[] args, string updatePath, CancellationToken cancellationToken); + ValueTask CreateServer(string[] args, string? updatePath, CancellationToken cancellationToken); } } diff --git a/src/Tgstation.Server.Host/Jobs/IJobManager.cs b/src/Tgstation.Server.Host/Jobs/IJobManager.cs index 80ef2008592..9f582b34ab8 100644 --- a/src/Tgstation.Server.Host/Jobs/IJobManager.cs +++ b/src/Tgstation.Server.Host/Jobs/IJobManager.cs @@ -34,7 +34,7 @@ public interface IJobManager /// A that will cancel the . /// The for the operation. /// A representing the . Results in if the completed without errors, if errors occurred, or if the job isn't registered. - ValueTask WaitForJobCompletion(Job job, User canceller, CancellationToken jobCancellationToken, CancellationToken cancellationToken); + ValueTask WaitForJobCompletion(Job job, User? canceller, CancellationToken jobCancellationToken, CancellationToken cancellationToken); /// /// Cancels a give . @@ -44,6 +44,6 @@ public interface IJobManager /// If the operation should wait until the job exits before completing. /// The for the operation. /// A resulting in the updated if it was cancelled, if it couldn't be found. - ValueTask CancelJob(Job job, User user, bool blocking, CancellationToken cancellationToken); + ValueTask CancelJob(Job job, User? user, bool blocking, CancellationToken cancellationToken); } } diff --git a/src/Tgstation.Server.Host/Jobs/JobEntrypoint.cs b/src/Tgstation.Server.Host/Jobs/JobEntrypoint.cs index 79e71b5048c..82e81a647ce 100644 --- a/src/Tgstation.Server.Host/Jobs/JobEntrypoint.cs +++ b/src/Tgstation.Server.Host/Jobs/JobEntrypoint.cs @@ -17,7 +17,7 @@ namespace Tgstation.Server.Host.Jobs /// The for the operation. /// A representing the running operation. public delegate ValueTask JobEntrypoint( - IInstanceCore instance, + IInstanceCore? instance, IDatabaseContextFactory databaseContextFactory, Job job, JobProgressReporter progressReporter, diff --git a/src/Tgstation.Server.Host/Jobs/JobHandler.cs b/src/Tgstation.Server.Host/Jobs/JobHandler.cs index 9d9c692c070..04c3b845101 100644 --- a/src/Tgstation.Server.Host/Jobs/JobHandler.cs +++ b/src/Tgstation.Server.Host/Jobs/JobHandler.cs @@ -22,7 +22,7 @@ sealed class JobHandler : IDisposable /// /// The stage of the job. /// - public string Stage { get; set; } + public string? Stage { get; set; } /// /// The for . @@ -37,7 +37,7 @@ sealed class JobHandler : IDisposable /// /// The being run. /// - Task task; + Task? task; /// /// Initializes a new instance of the class. diff --git a/src/Tgstation.Server.Host/Jobs/JobProgressReporter.cs b/src/Tgstation.Server.Host/Jobs/JobProgressReporter.cs index 4d23385e632..753f8cf519e 100644 --- a/src/Tgstation.Server.Host/Jobs/JobProgressReporter.cs +++ b/src/Tgstation.Server.Host/Jobs/JobProgressReporter.cs @@ -14,7 +14,7 @@ public sealed class JobProgressReporter /// /// The name of the current stage. /// - public string StageName + public string? StageName { get => stageName; set @@ -35,12 +35,12 @@ public string StageName /// /// Progress reporter callback taking a description of what the job is currently doing and the (optional) progress of the job on a scale from 0.0-1.0. /// - readonly Action callback; + readonly Action callback; /// /// Backing field for . /// - string stageName; + string? stageName; /// /// The last progress value pushed into the . @@ -58,7 +58,7 @@ public string StageName /// The value of . /// The value of . /// The value of . - public JobProgressReporter(ILogger logger, string stageName, Action callback) + public JobProgressReporter(ILogger logger, string? stageName, Action callback) { this.logger = logger ?? throw new ArgumentNullException(nameof(logger)); this.callback = callback ?? throw new ArgumentNullException(nameof(callback)); @@ -97,7 +97,7 @@ public void ReportProgress(double? progress) /// The 0.0f-1.0f percentage of the current 's percentage should be given to the section. /// A new that is a subsection of this one. /// A should only have one active child at a time. - public JobProgressReporter CreateSection(string newStageName, double percentage) + public JobProgressReporter CreateSection(string? newStageName, double percentage) { if (percentage > 1 || percentage < 0) { diff --git a/src/Tgstation.Server.Host/Jobs/JobService.cs b/src/Tgstation.Server.Host/Jobs/JobService.cs index 87387c60a93..ea2782a8218 100644 --- a/src/Tgstation.Server.Host/Jobs/JobService.cs +++ b/src/Tgstation.Server.Host/Jobs/JobService.cs @@ -11,6 +11,7 @@ using Serilog.Context; +using Tgstation.Server.Api.Extensions; using Tgstation.Server.Api.Hubs; using Tgstation.Server.Api.Models.Response; using Tgstation.Server.Common.Extensions; @@ -119,17 +120,14 @@ public async ValueTask RegisterOperation(Job job, JobEntrypoint operation, Cance ArgumentNullException.ThrowIfNull(job); ArgumentNullException.ThrowIfNull(operation); - job.StartedAt = DateTimeOffset.UtcNow; - job.Cancelled = false; + if (job.StartedBy != null && job.StartedBy.Name == null) + throw new InvalidOperationException("StartedBy User associated with job does not have a Name!"); - if (job.StartedBy != null) - { - if (!job.StartedBy.Id.HasValue) - throw new InvalidOperationException("StartedBy User associated with job does not have an Id!"); + if (job.Instance == null) + throw new InvalidOperationException("No Instance associated with job!"); - if (job.StartedBy.Name == null) - throw new InvalidOperationException("StartedBy User associated with job does not have a Name!"); - } + job.StartedAt = DateTimeOffset.UtcNow; + job.Cancelled = false; var originalStartedBy = job.StartedBy; await databaseContextFactory.UseContext( @@ -137,7 +135,7 @@ await databaseContextFactory.UseContext( { job.Instance = new Models.Instance { - Id = job.Instance.Id.Value, + Id = job.Instance.Require(x => x.Id), }; databaseContext.Instances.Attach(job.Instance); @@ -147,14 +145,14 @@ await databaseContextFactory.UseContext( .GetTgsUser( dbUser => new User { - Id = dbUser.Id.Value, + Id = dbUser.Id!.Value, Name = dbUser.Name, }, cancellationToken); job.StartedBy = new User { - Id = originalStartedBy.Id.Value, + Id = originalStartedBy.Require(x => x.Id), }; databaseContext.Users.Attach(job.StartedBy); @@ -175,7 +173,7 @@ await databaseContextFactory.UseContext( bool jobShouldStart; lock (synchronizationLock) { - jobs.Add(job.Id.Value, jobHandler); + jobs.Add(job.Require(x => x.Id), jobHandler); jobShouldStart = !noMoreJobsShouldStart; } @@ -199,7 +197,7 @@ public Task StartAsync(CancellationToken cancellationToken) .Jobs .AsQueryable() .Where(y => !y.StoppedAt.HasValue) - .Select(y => y.Id.Value) + .Select(y => y.Id!.Value) .ToListAsync(cancellationToken); if (badJobIds.Count > 0) { @@ -222,7 +220,7 @@ public Task StartAsync(CancellationToken cancellationToken) /// public Task StopAsync(CancellationToken cancellationToken) { - List> joinTasks; + List> joinTasks; lock (addCancelLock) lock (synchronizationLock) { @@ -239,24 +237,25 @@ public Task StopAsync(CancellationToken cancellationToken) } /// - public async ValueTask CancelJob(Job job, User user, bool blocking, CancellationToken cancellationToken) + public async ValueTask CancelJob(Job job, User? user, bool blocking, CancellationToken cancellationToken) { ArgumentNullException.ThrowIfNull(job); - JobHandler handler; + var jid = job.Require(x => x.Id); + JobHandler? handler; lock (addCancelLock) { lock (synchronizationLock) - if (!jobs.TryGetValue(job.Id.Value, out handler)) + if (!jobs.TryGetValue(jid, out handler)) return null; - logger.LogDebug("Cancelling job ID {jobId}...", job.Id.Value); + logger.LogDebug("Cancelling job ID {jobId}...", jid); handler.Cancel(); // this will ensure the db update is only done once } await databaseContextFactory.UseContext(async databaseContext => { - var updatedJob = new Job(job.Id.Value); + var updatedJob = new Job(jid); databaseContext.Jobs.Attach(updatedJob); var attachedUser = user == null ? await databaseContext @@ -264,12 +263,12 @@ await databaseContextFactory.UseContext(async databaseContext => .GetTgsUser( dbUser => new User { - Id = dbUser.Id.Value, + Id = dbUser.Id!.Value, }, cancellationToken) : new User { - Id = user.Id.Value, + Id = user.Require(x => x.Id), }; databaseContext.Users.Attach(attachedUser); @@ -296,7 +295,7 @@ public void SetJobProgress(JobResponse apiResponse) ArgumentNullException.ThrowIfNull(apiResponse); lock (synchronizationLock) { - if (!jobs.TryGetValue(apiResponse.Id.Value, out var handler)) + if (!jobs.TryGetValue(apiResponse.Require(x => x.Id), out var handler)) return; apiResponse.Progress = handler.Progress; apiResponse.Stage = handler.Stage; @@ -304,18 +303,18 @@ public void SetJobProgress(JobResponse apiResponse) } /// - public async ValueTask WaitForJobCompletion(Job job, User canceller, CancellationToken jobCancellationToken, CancellationToken cancellationToken) + public async ValueTask WaitForJobCompletion(Job job, User? canceller, CancellationToken jobCancellationToken, CancellationToken cancellationToken) { ArgumentNullException.ThrowIfNull(job); if (!cancellationToken.CanBeCanceled) throw new ArgumentException("A cancellable CancellationToken should be provided!", nameof(cancellationToken)); - JobHandler handler; + JobHandler? handler; bool noMoreJobsShouldStart; lock (synchronizationLock) { - if (!jobs.TryGetValue(job.Id.Value, out handler)) + if (!jobs.TryGetValue(job.Require(x => x.Id), out handler)) return null; noMoreJobsShouldStart = this.noMoreJobsShouldStart; @@ -324,7 +323,7 @@ public void SetJobProgress(JobResponse apiResponse) if (noMoreJobsShouldStart && !handler.Started) await Extensions.TaskExtensions.InfiniteTask.WaitAsync(cancellationToken); - var cancelTask = ValueTask.FromResult(null); + var cancelTask = ValueTask.FromResult(null); bool result; using (jobCancellationToken.Register(() => cancelTask = CancelJob(job, canceller, true, cancellationToken))) result = await handler.Wait(cancellationToken); @@ -362,17 +361,18 @@ public void QueueActiveJobUpdates() async Task RunJob(Job job, JobEntrypoint operation, CancellationToken cancellationToken) #pragma warning restore CA1506 { - using (LogContext.PushProperty(SerilogContextHelper.JobIdContextProperty, job.Id)) + var jid = job.Require(x => x.Id); + using (LogContext.PushProperty(SerilogContextHelper.JobIdContextProperty, jid)) try { - void LogException(Exception ex) => logger.LogDebug(ex, "Job {jobId} exited with error!", job.Id); + void LogException(Exception ex) => logger.LogDebug(ex, "Job {jobId} exited with error!", jid); var hubUpdatesTask = Task.CompletedTask; var result = false; var firstLogHappened = false; var hubGroupName = JobsHub.HubGroupName(job); - Stopwatch stopwatch = null; + Stopwatch? stopwatch = null; void QueueHubUpdate(JobResponse update, bool final) { void NextUpdate(bool bypassRate) @@ -384,7 +384,7 @@ async Task ChainHubUpdate() if (!firstLogHappened) { - logger.LogTrace("Sending updates for job {id} to hub group {group}", update.Id.Value, hubGroupName); + logger.LogTrace("Sending updates for job {id} to hub group {group}", jid, hubGroupName); firstLogHappened = true; } @@ -395,7 +395,7 @@ await hub .ReceiveJobUpdate(update, CancellationToken.None); } - Stopwatch enteredLock = null; + Stopwatch? enteredLock = null; try { if (!bypassRate && stopwatch != null) @@ -416,19 +416,18 @@ await hub } } - var jobId = update.Id.Value; lock (hubUpdateActions) if (final) - hubUpdateActions.Remove(jobId); + hubUpdateActions.Remove(jid); else - hubUpdateActions[jobId] = () => NextUpdate(true); + hubUpdateActions[jid] = () => NextUpdate(true); NextUpdate(false); } try { - void UpdateProgress(string stage, double? progress) + void UpdateProgress(string? stage, double? progress) { if (progress.HasValue && (progress.Value < 0 || progress.Value > 1)) @@ -440,7 +439,7 @@ void UpdateProgress(string stage, double? progress) int? newProgress = progress.HasValue ? (int)Math.Floor(progress.Value * 100) : null; lock (synchronizationLock) - if (jobs.TryGetValue(job.Id.Value, out var handler)) + if (jobs.TryGetValue(jid, out var handler)) { handler.Stage = stage; handler.Progress = newProgress; @@ -452,12 +451,17 @@ void UpdateProgress(string stage, double? progress) } } - var instanceCoreProvider = await activationTcs.Task.WaitAsync(cancellationToken); + var activationTask = activationTcs.Task; + + Debug.Assert(activationTask.IsCompleted || job.Require(x => x.JobCode).IsServerStartupJob(), "Non-server startup job registered before activation!"); + + var instanceCoreProvider = await activationTask.WaitAsync(cancellationToken); + QueueHubUpdate(job.ToApi(), false); logger.LogTrace("Starting job..."); await operation( - instanceCoreProvider.GetInstance(job.Instance), + instanceCoreProvider.GetInstance(job.Instance!), databaseContextFactory, job, new JobProgressReporter( @@ -490,7 +494,7 @@ await operation( { await databaseContextFactory.UseContext(async databaseContext => { - var attachedJob = new Job(job.Id.Value); + var attachedJob = new Job(jid); databaseContext.Jobs.Attach(attachedJob); attachedJob.StoppedAt = DateTimeOffset.UtcNow; @@ -515,7 +519,7 @@ await databaseContextFactory.UseContext(async databaseContext => .Include(x => x.Instance) .Include(x => x.StartedBy) .Include(x => x.CancelledBy) - .Where(dbJob => dbJob.Id == job.Id.Value) + .Where(dbJob => dbJob.Id == jid) .FirstAsync(CancellationToken.None); QueueHubUpdate(finalJob.ToApi(), true); }); @@ -523,7 +527,7 @@ await databaseContextFactory.UseContext(async databaseContext => catch { lock (hubUpdateActions) - hubUpdateActions.Remove(job.Id.Value); + hubUpdateActions.Remove(jid); throw; } @@ -543,8 +547,8 @@ await databaseContextFactory.UseContext(async databaseContext => { lock (synchronizationLock) { - var handler = jobs[job.Id.Value]; - jobs.Remove(job.Id.Value); + var handler = jobs[jid]; + jobs.Remove(jid); handler.Dispose(); } } diff --git a/src/Tgstation.Server.Host/Jobs/JobsHub.cs b/src/Tgstation.Server.Host/Jobs/JobsHub.cs index b74e19d22b0..d11fdf79138 100644 --- a/src/Tgstation.Server.Host/Jobs/JobsHub.cs +++ b/src/Tgstation.Server.Host/Jobs/JobsHub.cs @@ -34,7 +34,7 @@ public static string HubGroupName(Job job) if (job.Instance == null) throw new InvalidOperationException("job.Instance was null!"); - return HubGroupName(job.Instance.Id.Value); + return HubGroupName(job.Instance.Require(x => x.Id)); } /// diff --git a/src/Tgstation.Server.Host/Jobs/JobsHubGroupMapper.cs b/src/Tgstation.Server.Host/Jobs/JobsHubGroupMapper.cs index 2e2a5da93d9..a961fbaef99 100644 --- a/src/Tgstation.Server.Host/Jobs/JobsHubGroupMapper.cs +++ b/src/Tgstation.Server.Host/Jobs/JobsHubGroupMapper.cs @@ -67,7 +67,7 @@ public JobsHubGroupMapper( public ValueTask InstancePermissionSetCreated(InstancePermissionSet instancePermissionSet, CancellationToken cancellationToken) { ArgumentNullException.ThrowIfNull(instancePermissionSet); - var permissionSetId = instancePermissionSet.PermissionSet.Id ?? instancePermissionSet.PermissionSetId; + var permissionSetId = instancePermissionSet.PermissionSetId; logger.LogTrace("InstancePermissionSetCreated"); return RefreshHubGroups( @@ -117,20 +117,24 @@ async ValueTask MapConnectionGroups( { ArgumentNullException.ThrowIfNull(authenticationContext); - logger.LogTrace("MapConnectionGroups UID: {uid}", authenticationContext.User.Id.Value); + var pid = authenticationContext.PermissionSet.Require(x => x.Id); + logger.LogTrace( + "MapConnectionGroups UID: {uid} PID: {pid}", + authenticationContext.User.Require(x => x.Id), + pid); - List permedInstanceIds = null; + List? permedInstanceIds = null; await databaseContextFactory.UseContext( async databaseContext => permedInstanceIds = await databaseContext .InstancePermissionSets .AsQueryable() - .Where(ips => ips.PermissionSetId == authenticationContext.PermissionSet.Id.Value) + .Where(ips => ips.PermissionSetId == pid) .Select(ips => ips.InstanceId) .ToListAsync(cancellationToken)); await mappingFunc( - permedInstanceIds.Select( + permedInstanceIds!.Select( JobsHub.HubGroupName)); jobsHubUpdater.QueueActiveJobUpdates(); @@ -149,12 +153,12 @@ ValueTask RefreshHubGroups(long permissionSetId, CancellationToken cancellationT logger.LogTrace("RefreshHubGroups"); var permissionSetUsers = await databaseContext .Users - .Where(x => x.PermissionSet.Id == permissionSetId) + .Where(x => x.PermissionSet!.Id == permissionSetId) .ToListAsync(cancellationToken); var allInstanceIds = await databaseContext .Instances .Select( - instance => instance.Id.Value) + instance => instance.Id!.Value) .ToListAsync(cancellationToken); var permissionSetAccessibleInstanceIds = await databaseContext .InstancePermissionSets diff --git a/src/Tgstation.Server.Host/Models/ChatBot.cs b/src/Tgstation.Server.Host/Models/ChatBot.cs index f98f4a43c3a..caf1f0c645f 100644 --- a/src/Tgstation.Server.Host/Models/ChatBot.cs +++ b/src/Tgstation.Server.Host/Models/ChatBot.cs @@ -1,4 +1,5 @@ -using System.Collections.Generic; +using System; +using System.Collections.Generic; using System.ComponentModel.DataAnnotations; using System.Linq; @@ -23,17 +24,34 @@ public sealed class ChatBot : Api.Models.Internal.ChatBotSettings, IApiTransform /// The parent . /// [Required] - public Instance Instance { get; set; } + public Instance? Instance { get; set; } /// /// See . /// public ICollection Channels { get; set; } + /// + /// Initializes a new instance of the class. + /// + public ChatBot() + : this(new List()) + { + } + + /// + /// Initializes a new instance of the class. + /// + /// The value of . + public ChatBot(ICollection channels) + { + Channels = channels ?? throw new ArgumentNullException(nameof(channels)); + } + /// - public ChatBotResponse ToApi() => new ChatBotResponse + public ChatBotResponse ToApi() => new() { - Channels = Channels.Select(x => x.ToApi(Provider.Value)).ToList(), + Channels = Channels.Select(x => x.ToApi(this.Require(x => x.Provider))).ToList(), ConnectionString = ConnectionString, Enabled = Enabled, Provider = Provider, diff --git a/src/Tgstation.Server.Host/Models/ChatChannel.cs b/src/Tgstation.Server.Host/Models/ChatChannel.cs index 8519ffffdbb..9263d2f9a60 100644 --- a/src/Tgstation.Server.Host/Models/ChatChannel.cs +++ b/src/Tgstation.Server.Host/Models/ChatChannel.cs @@ -20,33 +20,29 @@ public sealed class ChatChannel : ChatChannelBase public long ChatSettingsId { get; set; } /// - /// See . + /// The IRC channel name. /// [StringLength(Limits.MaximumIndexableStringLength, MinimumLength = 1)] - public string IrcChannel { get; set; } + public string? IrcChannel { get; set; } /// - /// See . + /// The Discord channel snowflake. /// public ulong? DiscordChannelId { get; set; } /// /// The . /// - public ChatBot ChatSettings { get; set; } + public ChatBot? ChatSettings { get; set; } /// /// Convert to a . /// /// The channel's . /// The converted . - public Api.Models.ChatChannel ToApi(ChatProvider chatProvider) => new Api.Models.ChatChannel + public Api.Models.ChatChannel ToApi(ChatProvider chatProvider) => new() { - ChannelData = chatProvider == ChatProvider.Discord ? DiscordChannelId.Value.ToString(CultureInfo.InvariantCulture) : IrcChannel, -#pragma warning disable CS0618 - IrcChannel = IrcChannel, - DiscordChannelId = DiscordChannelId, -#pragma warning restore CS0618 + ChannelData = chatProvider == ChatProvider.Discord ? DiscordChannelId!.Value.ToString(CultureInfo.InvariantCulture) : IrcChannel, IsAdminChannel = IsAdminChannel, IsWatchdogChannel = IsWatchdogChannel, IsUpdatesChannel = IsUpdatesChannel, diff --git a/src/Tgstation.Server.Host/Models/CompileJob.cs b/src/Tgstation.Server.Host/Models/CompileJob.cs index 5ee95f96ef5..0f75b830e70 100644 --- a/src/Tgstation.Server.Host/Models/CompileJob.cs +++ b/src/Tgstation.Server.Host/Models/CompileJob.cs @@ -30,7 +30,7 @@ public sealed class CompileJob : Api.Models.Internal.CompileJob, IApiTransformab /// The the was made with in string form. /// [Required] - public string ByondVersion { get; set; } + public string EngineVersion { get; set; } /// /// Backing field for of . @@ -50,7 +50,7 @@ public sealed class CompileJob : Api.Models.Internal.CompileJob, IApiTransformab /// /// The origin of the repository the compile job was built from. /// - public string RepositoryOrigin { get; set; } + public string? RepositoryOrigin { get; set; } /// /// The source GitHub repository the deployment came from if any. @@ -63,14 +63,14 @@ public sealed class CompileJob : Api.Models.Internal.CompileJob, IApiTransformab public int? GitHubDeploymentId { get; set; } /// - public override Version DMApiVersion + public override Version? DMApiVersion { get { if (!DMApiMajorVersion.HasValue) return null; - return new Version(DMApiMajorVersion.Value, DMApiMinorVersion.Value, DMApiPatchVersion.Value); + return new Version(DMApiMajorVersion.Value, DMApiMinorVersion!.Value, DMApiPatchVersion!.Value); } set @@ -81,8 +81,49 @@ public override Version DMApiVersion } } + /// + /// Initializes a new instance of the class. + /// + [Obsolete("For use by EFCore only", true)] + public CompileJob() + : this(null!, null!, null!, false) + { + } + + /// + /// Initializes a new instance of the class. + /// + /// The value of . + /// The value of . + /// The value of . + public CompileJob(Job job, RevisionInformation revisionInformation, string engineVersion) + : this(job, revisionInformation, engineVersion, true) + { + } + + /// + /// Initializes a new instance of the class. + /// + /// The value of . + /// The value of . + /// The value of . + /// If , , and should be checked for nulls. + CompileJob(Job job, RevisionInformation revisionInformation, string engineVersion, bool nullChecks) + { + if (nullChecks) + { + ArgumentNullException.ThrowIfNull(job); + ArgumentNullException.ThrowIfNull(revisionInformation); + ArgumentNullException.ThrowIfNull(engineVersion); + } + + Job = job; + RevisionInformation = revisionInformation; + EngineVersion = engineVersion; + } + /// - public CompileJobResponse ToApi() => new () + public CompileJobResponse ToApi() => new() { DirectoryName = DirectoryName, DmeName = DmeName, @@ -90,7 +131,9 @@ public override Version DMApiVersion Job = Job.ToApi(), Output = Output, RevisionInformation = RevisionInformation.ToApi(), - ByondVersion = Version.Parse(ByondVersion), + EngineVersion = Api.Models.EngineVersion.TryParse(EngineVersion, out var version) + ? version + : throw new InvalidOperationException($"Failed to parse engine version: {EngineVersion}"), MinimumSecurityLevel = MinimumSecurityLevel, DMApiVersion = DMApiVersion, RepositoryOrigin = RepositoryOrigin != null ? new Uri(RepositoryOrigin) : null, diff --git a/src/Tgstation.Server.Host/Models/DreamDaemonSettings.cs b/src/Tgstation.Server.Host/Models/DreamDaemonSettings.cs index 5a263910154..0da7f220796 100644 --- a/src/Tgstation.Server.Host/Models/DreamDaemonSettings.cs +++ b/src/Tgstation.Server.Host/Models/DreamDaemonSettings.cs @@ -19,6 +19,6 @@ public sealed class DreamDaemonSettings : Api.Models.Internal.DreamDaemonSetting /// The parent . /// [Required] - public Instance Instance { get; set; } + public Instance? Instance { get; set; } } } diff --git a/src/Tgstation.Server.Host/Models/DreamMakerSettings.cs b/src/Tgstation.Server.Host/Models/DreamMakerSettings.cs index 3a55c433d0a..72f5a99f29c 100644 --- a/src/Tgstation.Server.Host/Models/DreamMakerSettings.cs +++ b/src/Tgstation.Server.Host/Models/DreamMakerSettings.cs @@ -21,10 +21,10 @@ public sealed class DreamMakerSettings : Api.Models.Internal.DreamMakerSettings, /// The parent . /// [Required] - public Instance Instance { get; set; } + public Instance? Instance { get; set; } /// - public DreamMakerResponse ToApi() => new DreamMakerResponse + public DreamMakerResponse ToApi() => new() { ProjectName = ProjectName, ApiValidationPort = ApiValidationPort, diff --git a/src/Tgstation.Server.Host/Models/Instance.cs b/src/Tgstation.Server.Host/Models/Instance.cs index 8acf4f15703..a96470e9fa8 100644 --- a/src/Tgstation.Server.Host/Models/Instance.cs +++ b/src/Tgstation.Server.Host/Models/Instance.cs @@ -17,22 +17,22 @@ public sealed class Instance : Api.Models.Instance, IApiTransformable /// The for the . /// - public DreamMakerSettings DreamMakerSettings { get; set; } + public DreamMakerSettings? DreamMakerSettings { get; set; } /// /// The for the . /// - public DreamDaemonSettings DreamDaemonSettings { get; set; } + public DreamDaemonSettings? DreamDaemonSettings { get; set; } /// /// The for the . /// - public RepositorySettings RepositorySettings { get; set; } + public RepositorySettings? RepositorySettings { get; set; } /// /// The of the the server in the swarm this instance belongs to. /// - public string SwarmIdentifer { get; set; } + public string? SwarmIdentifer { get; set; } /// /// The s in the . @@ -54,8 +54,19 @@ public sealed class Instance : Api.Models.Instance, IApiTransformable public ICollection Jobs { get; set; } + /// + /// Initializes a new instance of the class. + /// + public Instance() + { + InstancePermissionSets = new List(); + ChatSettings = new List(); + RevisionInformations = new List(); + Jobs = new List(); + } + /// - public InstanceResponse ToApi() => new () + public InstanceResponse ToApi() => new() { AutoUpdateInterval = AutoUpdateInterval, ConfigurationType = ConfigurationType, diff --git a/src/Tgstation.Server.Host/Models/InstancePermissionSet.cs b/src/Tgstation.Server.Host/Models/InstancePermissionSet.cs index 00509126f0d..8db1f17fba4 100644 --- a/src/Tgstation.Server.Host/Models/InstancePermissionSet.cs +++ b/src/Tgstation.Server.Host/Models/InstancePermissionSet.cs @@ -21,18 +21,18 @@ public sealed class InstancePermissionSet : Api.Models.Internal.InstancePermissi /// The the belongs to. /// [Required] - public Instance Instance { get; set; } + public Instance? Instance { get; set; } /// /// The the belongs to. /// [Required] - public PermissionSet PermissionSet { get; set; } + public PermissionSet? PermissionSet { get; set; } /// - public InstancePermissionSetResponse ToApi() => new InstancePermissionSetResponse + public InstancePermissionSetResponse ToApi() => new() { - ByondRights = ByondRights, + EngineRights = EngineRights, ChatBotRights = ChatBotRights, ConfigurationRights = ConfigurationRights, DreamDaemonRights = DreamDaemonRights, diff --git a/src/Tgstation.Server.Host/Models/Job.cs b/src/Tgstation.Server.Host/Models/Job.cs index 8193402a6a5..85327de2e27 100644 --- a/src/Tgstation.Server.Host/Models/Job.cs +++ b/src/Tgstation.Server.Host/Models/Job.cs @@ -18,18 +18,18 @@ public sealed class Job : Api.Models.Internal.Job, IApiTransformable. /// [Required] - public User StartedBy { get; set; } + public User? StartedBy { get; set; } /// /// See . /// - public User CancelledBy { get; set; } + public User? CancelledBy { get; set; } /// /// The the job belongs to if any. /// [Required] - public Instance Instance { get; set; } + public Instance? Instance { get; set; } /// /// Creates a new job for registering in the . @@ -40,9 +40,9 @@ public sealed class Job : Api.Models.Internal.Job, IApiTransformableThe used to generate the value of . /// The value of . will be derived from this. /// A new ready to be registered with the . - public static Job Create(JobCode code, User startedBy, Api.Models.Instance instance, TRight cancelRight) + public static Job Create(JobCode code, User? startedBy, Api.Models.Instance instance, TRight cancelRight) where TRight : Enum - => new ( + => new( code, startedBy, instance, @@ -56,8 +56,8 @@ public static Job Create(JobCode code, User startedBy, Api.Models.Instan /// The value of . If , the user will be used. /// The used to generate the value of . /// A new ready to be registered with the . - public static Job Create(JobCode code, User startedBy, Api.Models.Instance instance) - => new ( + public static Job Create(JobCode code, User? startedBy, Api.Models.Instance instance) + => new( code, startedBy, instance, @@ -89,7 +89,7 @@ public Job(long id) /// The value of . /// The value of . /// The value of . - Job(JobCode code, User startedBy, Api.Models.Instance instance, RightsType? cancelRightsType, ulong? cancelRight) + Job(JobCode code, User? startedBy, Api.Models.Instance instance, RightsType? cancelRightsType, ulong? cancelRight) { StartedBy = startedBy; ArgumentNullException.ThrowIfNull(instance); @@ -98,7 +98,7 @@ public Job(long id) Id = instance.Id ?? throw new InvalidOperationException("Instance associated with job does not have an Id!"), }; Description = typeof(JobCode) - .GetField(code.ToString()) + .GetField(code.ToString())! .GetCustomAttributes(false) .OfType() .First() @@ -109,11 +109,11 @@ public Job(long id) } /// - public JobResponse ToApi() => new () + public JobResponse ToApi() => new() { Id = Id, - JobCode = JobCode.Value, - InstanceId = Instance.Id.Value, + JobCode = this.Require(x => x.JobCode), + InstanceId = (Instance ?? throw new InvalidOperationException("Instance needs to be set!")).Require(x => x.Id), StartedAt = StartedAt, StoppedAt = StoppedAt, Cancelled = Cancelled, @@ -123,7 +123,7 @@ public Job(long id) Description = Description, ExceptionDetails = ExceptionDetails, ErrorCode = ErrorCode, - StartedBy = StartedBy.CreateUserName(), + StartedBy = (StartedBy ?? throw new InvalidOperationException("StartedBy needs to be set!")).CreateUserName(), }; } } diff --git a/src/Tgstation.Server.Host/Models/ModelExtensions.cs b/src/Tgstation.Server.Host/Models/ModelExtensions.cs new file mode 100644 index 00000000000..0c88a1ab07f --- /dev/null +++ b/src/Tgstation.Server.Host/Models/ModelExtensions.cs @@ -0,0 +1,40 @@ +using System; +using System.Linq.Expressions; +using System.Reflection; + +using Tgstation.Server.Api.Models; + +namespace Tgstation.Server.Host.Models +{ + /// + /// Extensions for . + /// + static class ModelExtensions + { + /// + /// Require a given property of a given be non-. + /// + /// The of the being accessed. + /// The of the property being accessed. + /// The . + /// The access . + /// The value of in . + /// When in is . + public static TProperty Require(this TModel model, Expression> accessor) + where TModel : EntityId + where TProperty : struct + { + ArgumentNullException.ThrowIfNull(model); + ArgumentNullException.ThrowIfNull(accessor); + + var memberSelectorExpression = (MemberExpression)accessor.Body; + var property = (PropertyInfo)memberSelectorExpression.Member; + + var nullableValue = (TProperty?)property.GetValue(model); + if (!nullableValue.HasValue) + throw new InvalidOperationException($"Expected {model.GetType().Name}.{property.Name} to be set here!"); + + return nullableValue.Value; + } + } +} diff --git a/src/Tgstation.Server.Host/Models/OAuthConnection.cs b/src/Tgstation.Server.Host/Models/OAuthConnection.cs index e72476c1349..d622b358c85 100644 --- a/src/Tgstation.Server.Host/Models/OAuthConnection.cs +++ b/src/Tgstation.Server.Host/Models/OAuthConnection.cs @@ -11,10 +11,10 @@ public sealed class OAuthConnection : Api.Models.OAuthConnection, IApiTransforma /// /// The owning . /// - public User User { get; set; } + public User? User { get; set; } /// - public Api.Models.OAuthConnection ToApi() => new Api.Models.OAuthConnection + public Api.Models.OAuthConnection ToApi() => new() { Provider = Provider, ExternalUserId = ExternalUserId, diff --git a/src/Tgstation.Server.Host/Models/PermissionSet.cs b/src/Tgstation.Server.Host/Models/PermissionSet.cs index 021a8239f27..2c18a2ae1f1 100644 --- a/src/Tgstation.Server.Host/Models/PermissionSet.cs +++ b/src/Tgstation.Server.Host/Models/PermissionSet.cs @@ -18,23 +18,23 @@ public sealed class PermissionSet : Api.Models.PermissionSet /// /// The the belongs to, if it is for a . /// - public User User { get; set; } + public User? User { get; set; } /// /// The the belongs to, if it is for a . /// - public UserGroup Group { get; set; } + public UserGroup? Group { get; set; } /// /// The s associated with the . /// - public ICollection InstancePermissionSets { get; set; } + public ICollection? InstancePermissionSets { get; set; } /// /// Convert the to it's API form. /// /// A new . - public Api.Models.PermissionSet ToApi() => new Api.Models.PermissionSet + public Api.Models.PermissionSet ToApi() => new() { Id = Id, AdministrationRights = AdministrationRights, diff --git a/src/Tgstation.Server.Host/Models/ReattachInformation.cs b/src/Tgstation.Server.Host/Models/ReattachInformation.cs index 00409df99a9..49360cf3e50 100644 --- a/src/Tgstation.Server.Host/Models/ReattachInformation.cs +++ b/src/Tgstation.Server.Host/Models/ReattachInformation.cs @@ -1,4 +1,5 @@ -using System.ComponentModel.DataAnnotations; +using System; +using System.ComponentModel.DataAnnotations; namespace Tgstation.Server.Host.Models { @@ -7,16 +8,11 @@ namespace Tgstation.Server.Host.Models /// public sealed class ReattachInformation : ReattachInformationBase { - /// - /// The row Id. - /// - public long Id { get; set; } - /// /// The for the . /// [Required] - public CompileJob CompileJob { get; set; } + public CompileJob? CompileJob { get; set; } /// /// The of . @@ -26,11 +22,28 @@ public sealed class ReattachInformation : ReattachInformationBase /// /// The the server was initially launched with in the case of Windows. /// - public CompileJob InitialCompileJob { get; set; } + public CompileJob? InitialCompileJob { get; set; } /// /// The of . /// public long? InitialCompileJobId { get; set; } + + /// + /// Initializes a new instance of the class. + /// + [Obsolete("For use by EFCore only", true)] + public ReattachInformation() + { + } + + /// + /// Initializes a new instance of the class. + /// + /// The access identifier for the . + public ReattachInformation(string accessIdentifier) + : base(accessIdentifier) + { + } } } diff --git a/src/Tgstation.Server.Host/Models/ReattachInformationBase.cs b/src/Tgstation.Server.Host/Models/ReattachInformationBase.cs index 241fae5141a..fc6d07a8f30 100644 --- a/src/Tgstation.Server.Host/Models/ReattachInformationBase.cs +++ b/src/Tgstation.Server.Host/Models/ReattachInformationBase.cs @@ -1,5 +1,4 @@ using System; -using System.Globalization; using Tgstation.Server.Api.Models; using Tgstation.Server.Host.Components.Interop; @@ -12,16 +11,26 @@ namespace Tgstation.Server.Host.Models /// public abstract class ReattachInformationBase : DMApiParameters { + /// + /// The database row Id. + /// + public long? Id { get; set; } + /// /// The system process ID. /// public int ProcessId { get; set; } /// - /// The port DreamDaemon was last listening on. + /// The port the game server was last listening on. /// public ushort Port { get; set; } + /// + /// The port the game server was last listening on for topics. + /// + public ushort? TopicPort { get; set; } + /// /// The current DreamDaemon reboot state. /// @@ -40,19 +49,32 @@ public abstract class ReattachInformationBase : DMApiParameters /// /// Initializes a new instance of the class. /// + /// For use by EFCore only. protected ReattachInformationBase() { } + /// + /// Initializes a new instance of the class. + /// + /// The access identifier for the . + protected ReattachInformationBase(string accessIdentifier) + : base(accessIdentifier) + { + } + /// /// Initializes a new instance of the class. /// /// The to copy values from. protected ReattachInformationBase(ReattachInformationBase copy) + : base(copy == null + ? throw new ArgumentNullException(nameof(copy)) + : copy.AccessIdentifier) { - ArgumentNullException.ThrowIfNull(copy); - AccessIdentifier = copy.AccessIdentifier; + Id = copy.Id; Port = copy.Port; + TopicPort = copy.TopicPort; ProcessId = copy.ProcessId; RebootState = copy.RebootState; LaunchSecurityLevel = copy.LaunchSecurityLevel; @@ -60,6 +82,6 @@ protected ReattachInformationBase(ReattachInformationBase copy) } /// - public override string ToString() => String.Format(CultureInfo.InvariantCulture, "Process ID: {0}, Access Identifier {1}, RebootState: {2}, Port: {3}", ProcessId, AccessIdentifier, RebootState, Port); + public override string ToString() => $"Session: {Id}, PID: {ProcessId}, Access Identifier {AccessIdentifier}, RebootState: {RebootState}, Port: {Port}"; } } diff --git a/src/Tgstation.Server.Host/Models/RepositorySettings.cs b/src/Tgstation.Server.Host/Models/RepositorySettings.cs index b62f235d5eb..e8fb7f60197 100644 --- a/src/Tgstation.Server.Host/Models/RepositorySettings.cs +++ b/src/Tgstation.Server.Host/Models/RepositorySettings.cs @@ -21,10 +21,10 @@ public sealed class RepositorySettings : Api.Models.Internal.RepositorySettings, /// The parent . /// [Required] - public Instance Instance { get; set; } + public Instance? Instance { get; set; } /// - public RepositoryResponse ToApi() => new RepositoryResponse + public RepositoryResponse ToApi() => new() { // AccessToken = AccessToken, // never show this AccessUser = AccessUser, diff --git a/src/Tgstation.Server.Host/Models/RevInfoTestMerge.cs b/src/Tgstation.Server.Host/Models/RevInfoTestMerge.cs index 4328910d0c5..bed334b0bc8 100644 --- a/src/Tgstation.Server.Host/Models/RevInfoTestMerge.cs +++ b/src/Tgstation.Server.Host/Models/RevInfoTestMerge.cs @@ -1,4 +1,5 @@ -using System.ComponentModel.DataAnnotations; +using System; +using System.ComponentModel.DataAnnotations; namespace Tgstation.Server.Host.Models { @@ -23,5 +24,26 @@ public sealed class RevInfoTestMerge /// [Required] public RevisionInformation RevisionInformation { get; set; } + + /// + /// Initializes a new instance of the class. + /// + [Obsolete("For use by EFCore only", true)] + public RevInfoTestMerge() + { + TestMerge = null!; + RevisionInformation = null!; + } + + /// + /// Initializes a new instance of the class. + /// + /// The value of . + /// The value of . + public RevInfoTestMerge(TestMerge testMerge, RevisionInformation revisionInformation) + { + TestMerge = testMerge ?? throw new ArgumentNullException(nameof(testMerge)); + RevisionInformation = revisionInformation ?? throw new ArgumentNullException(nameof(revisionInformation)); + } } } diff --git a/src/Tgstation.Server.Host/Models/RevisionInformation.cs b/src/Tgstation.Server.Host/Models/RevisionInformation.cs index 290e118ae98..eb99816ab68 100644 --- a/src/Tgstation.Server.Host/Models/RevisionInformation.cs +++ b/src/Tgstation.Server.Host/Models/RevisionInformation.cs @@ -1,4 +1,5 @@ -using System.Collections.Generic; +using System; +using System.Collections.Generic; using System.ComponentModel.DataAnnotations; using System.Linq; @@ -21,32 +22,32 @@ public sealed class RevisionInformation : Api.Models.Internal.RevisionInformatio /// The the belongs to. /// [Required] - public Instance Instance { get; set; } + public Instance? Instance { get; set; } /// /// See . /// - public TestMerge PrimaryTestMerge { get; set; } + public TestMerge? PrimaryTestMerge { get; set; } /// /// See . /// - public ICollection ActiveTestMerges { get; set; } + public ICollection? ActiveTestMerges { get; set; } /// /// See s made from this . /// - public ICollection CompileJobs { get; set; } + public ICollection? CompileJobs { get; set; } /// - public Api.Models.RevisionInformation ToApi() => new Api.Models.RevisionInformation + public Api.Models.RevisionInformation ToApi() => new() { CommitSha = CommitSha, Timestamp = Timestamp, OriginCommitSha = OriginCommitSha, PrimaryTestMerge = PrimaryTestMerge?.ToApi(), - ActiveTestMerges = ActiveTestMerges.Select(x => x.TestMerge.ToApi()).ToList(), - CompileJobs = CompileJobs.Select(x => new Api.Models.EntityId + ActiveTestMerges = (ActiveTestMerges ?? throw new InvalidOperationException("ActiveTestMerges must be set!")).Select(x => x.TestMerge.ToApi()).ToList(), + CompileJobs = (CompileJobs ?? throw new InvalidOperationException("CompileJobs must be set!")).Select(x => new Api.Models.EntityId { Id = x.Id, }).ToList(), diff --git a/src/Tgstation.Server.Host/Models/TestMerge.cs b/src/Tgstation.Server.Host/Models/TestMerge.cs index 43d7f35b281..36416403789 100644 --- a/src/Tgstation.Server.Host/Models/TestMerge.cs +++ b/src/Tgstation.Server.Host/Models/TestMerge.cs @@ -1,4 +1,5 @@ -using System.Collections.Generic; +using System; +using System.Collections.Generic; using System.ComponentModel.DataAnnotations; namespace Tgstation.Server.Host.Models @@ -10,13 +11,13 @@ public sealed class TestMerge : Api.Models.Internal.TestMergeApiBase, IApiTransf /// See . /// [Required] - public User MergedBy { get; set; } + public User? MergedBy { get; set; } /// /// The initial the was merged with. /// [Required] - public RevisionInformation PrimaryRevisionInformation { get; set; } + public RevisionInformation? PrimaryRevisionInformation { get; set; } /// /// Foreign key for . @@ -26,10 +27,10 @@ public sealed class TestMerge : Api.Models.Internal.TestMergeApiBase, IApiTransf /// /// All the for the . /// - public ICollection RevisonInformations { get; set; } + public ICollection? RevisonInformations { get; set; } /// - public Api.Models.TestMerge ToApi() => new Api.Models.TestMerge + public Api.Models.TestMerge ToApi() => new() { Author = Author, BodyAtMerge = BodyAtMerge, @@ -37,7 +38,7 @@ public sealed class TestMerge : Api.Models.Internal.TestMergeApiBase, IApiTransf TitleAtMerge = TitleAtMerge, Comment = Comment, Id = Id, - MergedBy = MergedBy.CreateUserName(), + MergedBy = (MergedBy ?? throw new InvalidOperationException("MergedBy must be set!")).CreateUserName(), Number = Number, TargetCommitSha = TargetCommitSha, Url = Url, diff --git a/src/Tgstation.Server.Host/Models/User.cs b/src/Tgstation.Server.Host/Models/User.cs index 0d275c7d18c..0939dd8042c 100644 --- a/src/Tgstation.Server.Host/Models/User.cs +++ b/src/Tgstation.Server.Host/Models/User.cs @@ -19,17 +19,17 @@ public sealed class User : Api.Models.Internal.UserModelBase, IApiTransformable< /// /// The hash of the user's password. /// - public string PasswordHash { get; set; } + public string? PasswordHash { get; set; } /// /// See . /// - public User CreatedBy { get; set; } + public User? CreatedBy { get; set; } /// /// The the belongs to, if any. /// - public UserGroup Group { get; set; } + public UserGroup? Group { get; set; } /// /// The ID of the 's . @@ -39,14 +39,14 @@ public sealed class User : Api.Models.Internal.UserModelBase, IApiTransformable< /// /// The the has, if any. /// - public PermissionSet PermissionSet { get; set; } + public PermissionSet? PermissionSet { get; set; } /// /// The uppercase invariant of . /// [Required] [StringLength(Limits.MaximumIndexableStringLength, MinimumLength = 1)] - public string CanonicalName { get; set; } + public string? CanonicalName { get; set; } /// /// When was last changed. @@ -56,17 +56,17 @@ public sealed class User : Api.Models.Internal.UserModelBase, IApiTransformable< /// /// s created by this . /// - public ICollection CreatedUsers { get; set; } + public ICollection? CreatedUsers { get; set; } /// /// The s made by the . /// - public ICollection TestMerges { get; set; } + public ICollection? TestMerges { get; set; } /// /// The s made by the . /// - public ICollection OAuthConnections { get; set; } + public ICollection? OAuthConnections { get; set; } /// /// Change a into a . diff --git a/src/Tgstation.Server.Host/Models/UserGroup.cs b/src/Tgstation.Server.Host/Models/UserGroup.cs index 11329e9397d..4451c1a0732 100644 --- a/src/Tgstation.Server.Host/Models/UserGroup.cs +++ b/src/Tgstation.Server.Host/Models/UserGroup.cs @@ -1,4 +1,5 @@ -using System.Collections.Generic; +using System; +using System.Collections.Generic; using System.ComponentModel.DataAnnotations; using System.Linq; @@ -16,23 +17,23 @@ public sealed class UserGroup : NamedEntity, IApiTransformable the has. /// [Required] - public PermissionSet PermissionSet { get; set; } + public PermissionSet? PermissionSet { get; set; } /// /// The s the has. /// - public ICollection Users { get; set; } + public ICollection? Users { get; set; } /// /// Convert the to it's API form. /// /// If should be populated. /// A new . - public UserGroupResponse ToApi(bool showUsers) => new () + public UserGroupResponse ToApi(bool showUsers) => new() { Id = Id, Name = Name, - PermissionSet = PermissionSet.ToApi(), + PermissionSet = (PermissionSet ?? throw new InvalidOperationException("PermissionSet must be set!")).ToApi(), Users = showUsers ? Users ?.Select(x => x.CreateUserName()) diff --git a/src/Tgstation.Server.Host/Program.cs b/src/Tgstation.Server.Host/Program.cs index e372c0563d6..b23633b7dff 100644 --- a/src/Tgstation.Server.Host/Program.cs +++ b/src/Tgstation.Server.Host/Program.cs @@ -44,7 +44,7 @@ public Program() public static async Task Main(string[] args) { // first arg is 100% always the update path, starting it otherwise is solely for debugging purposes - string updatePath = null; + string? updatePath = null; if (args.Length > 0) { var listArgs = new List(args); @@ -77,13 +77,13 @@ public static async Task Main(string[] args) /// The command line arguments, minus the . /// The path to extract server updates to be applied to. /// A resulting in the . - internal async ValueTask Main(string[] args, string updatePath) + internal async ValueTask Main(string[] args, string? updatePath) { try { using var shutdownNotifier = new ProgramShutdownTokenSource(); var cancellationToken = shutdownNotifier.Token; - IServer server; + IServer? server; try { server = await ServerFactory.CreateServer( diff --git a/src/Tgstation.Server.Host/Properties/MasterVersionsAttribute.cs b/src/Tgstation.Server.Host/Properties/MasterVersionsAttribute.cs index d6461713bb8..d77bcd349bc 100644 --- a/src/Tgstation.Server.Host/Properties/MasterVersionsAttribute.cs +++ b/src/Tgstation.Server.Host/Properties/MasterVersionsAttribute.cs @@ -14,7 +14,7 @@ sealed class MasterVersionsAttribute : Attribute /// public static MasterVersionsAttribute Instance => Assembly .GetExecutingAssembly() - .GetCustomAttribute(); + .GetCustomAttribute()!; /// /// The of the version built. diff --git a/src/Tgstation.Server.Host/Properties/launchSettings.json b/src/Tgstation.Server.Host/Properties/launchSettings.json index 52ba379ea36..b93f29050d8 100644 --- a/src/Tgstation.Server.Host/Properties/launchSettings.json +++ b/src/Tgstation.Server.Host/Properties/launchSettings.json @@ -7,9 +7,15 @@ "ASPNETCORE_ENVIRONMENT": "Development" } }, - "Docker": { - "commandName": "Docker", - "publishAllPorts": true + "WSL": { + "commandName": "WSL2", + "distributionName": "Ubuntu", + "environmentVariables": { + "DOTNET_ENVIRONMENT": "Development", + "ASPNETCORE_ENVIRONMENT": "Development", + "Database__ConnectionString": "Data Source=192.168.2.16,1433;Initial Catalog=TGS_Linux;User Id=tgs_debug;Password=asdf;Encrypt=False;Application Name=tgstation-server", + "General__ValidInstancePaths__0": "/home/dominion/tgs_debug_pen" + } } } -} \ No newline at end of file +} diff --git a/src/Tgstation.Server.Host/README.md b/src/Tgstation.Server.Host/README.md index ccb01971a64..1f8df0feee3 100644 --- a/src/Tgstation.Server.Host/README.md +++ b/src/Tgstation.Server.Host/README.md @@ -12,8 +12,8 @@ Server startup can be a bit complicated so here's a walkthrough 1. `CreateServer()` is called on the `IServerFactory` to get the `IServer` instance. - The factory pattern is used throughout TGS to construct implementations where the composition root is not sufficient. `ServerFactory` is somewhat of an exception to this because it exists outside of the dependency injection umbrella. 1. Inside `CreateServer()` we run the [setup code](./Setup) if need be. - - This is implemented as a separate [dotnet host](https://docs.microsoft.com/en-us/aspnet/core/fundamentals/host/generic-host?view=aspnetcore-6.0) to the main server. -1. Still inside `CreateServer()` we configure the main [dotnet host (IHostBuilder)](https://docs.microsoft.com/en-us/aspnet/core/fundamentals/host/generic-host?view=aspnetcore-6.0) using the application [Application](./Core/Application.cs) class as the [Startup class](https://docs.microsoft.com/en-us/aspnet/core/fundamentals/startup?view=aspnetcore-6.0#the-startup-class). + - This is implemented as a separate [dotnet host](https://docs.microsoft.com/en-us/aspnet/core/fundamentals/host/generic-host?view=aspnetcore-8.0) to the main server. +1. Still inside `CreateServer()` we configure the main [dotnet host (IHostBuilder)](https://docs.microsoft.com/en-us/aspnet/core/fundamentals/host/generic-host?view=aspnetcore-8.0) using the application [Application](./Core/Application.cs) class as the [Startup class](https://docs.microsoft.com/en-us/aspnet/core/fundamentals/startup?view=aspnetcore-8.0#the-startup-class). 1. The `IHostBuilder` is used to construct the return `Server` implementation. 1. `Run()` is called on the `IServer` instance. 1. The DI container is built using the [Application](./Core/Application.cs) class. diff --git a/src/Tgstation.Server.Host/Security/AuthenticationContext.cs b/src/Tgstation.Server.Host/Security/AuthenticationContext.cs index 5ed4dafe3f1..53bf23c2a21 100644 --- a/src/Tgstation.Server.Host/Security/AuthenticationContext.cs +++ b/src/Tgstation.Server.Host/Security/AuthenticationContext.cs @@ -13,16 +13,26 @@ sealed class AuthenticationContext : IAuthenticationContext, IDisposable public bool Valid { get; private set; } /// - public User User { get; private set; } + public User User => user ?? throw new InvalidOperationException("AuthenticationContext is invalid!"); /// - public PermissionSet PermissionSet { get; private set; } + public PermissionSet PermissionSet => permissionSet ?? throw new InvalidOperationException("AuthenticationContext is invalid!"); /// - public InstancePermissionSet InstancePermissionSet { get; private set; } + public InstancePermissionSet? InstancePermissionSet { get; private set; } /// - public ISystemIdentity SystemIdentity { get; private set; } + public ISystemIdentity? SystemIdentity { get; private set; } + + /// + /// Backing field for . + /// + User? user; + + /// + /// Backing field for . + /// + PermissionSet? permissionSet; /// /// Initializes a new instance of the class. @@ -40,13 +50,13 @@ public AuthenticationContext() /// The value of . /// The value of . /// The value of . - public void Initialize(ISystemIdentity systemIdentity, User user, InstancePermissionSet instanceUser) + public void Initialize(ISystemIdentity? systemIdentity, User user, InstancePermissionSet? instanceUser) { - User = user ?? throw new ArgumentNullException(nameof(user)); + this.user = user ?? throw new ArgumentNullException(nameof(user)); if (systemIdentity == null && User.SystemIdentifier != null) throw new ArgumentNullException(nameof(systemIdentity)); - PermissionSet = user.PermissionSet - ?? user.Group.PermissionSet + permissionSet = user.PermissionSet + ?? user.Group!.PermissionSet ?? throw new ArgumentException("No PermissionSet provider", nameof(user)); InstancePermissionSet = instanceUser; SystemIdentity = systemIdentity; @@ -72,16 +82,14 @@ public ulong GetRight(RightsType rightsType) var nullableType = typeof(Nullable<>); var nullableRightsType = nullableType.MakeGenericType(rightsEnum); - var prop = typeToCheck.GetProperties().Where(x => x.PropertyType == nullableRightsType).First(); + var prop = typeToCheck.GetProperties().Where(x => x.PropertyType == nullableRightsType && x.CanRead).First(); - var right = prop.GetMethod.Invoke( + var right = prop.GetMethod!.Invoke( isInstance ? InstancePermissionSet : PermissionSet, - Array.Empty()); - - if (right == null) - throw new InvalidOperationException("A user right was null!"); + Array.Empty()) + ?? throw new InvalidOperationException("A user right was null!"); return (ulong)right; } diff --git a/src/Tgstation.Server.Host/Security/AuthenticationContextAuthorizationFilter.cs b/src/Tgstation.Server.Host/Security/AuthenticationContextAuthorizationFilter.cs index 562f49f443c..b009f599ada 100644 --- a/src/Tgstation.Server.Host/Security/AuthenticationContextAuthorizationFilter.cs +++ b/src/Tgstation.Server.Host/Security/AuthenticationContextAuthorizationFilter.cs @@ -5,6 +5,8 @@ using Microsoft.AspNetCore.Mvc.Filters; using Microsoft.Extensions.Logging; +using Tgstation.Server.Host.Models; + namespace Tgstation.Server.Host.Security { /// @@ -43,7 +45,7 @@ public void OnAuthorization(AuthorizationFilterContext context) return; } - if (authenticationContext.User.Enabled.Value) + if (authenticationContext.User.Require(x => x.Enabled)) return; logger.LogTrace("authenticationContext is for a disabled user!"); diff --git a/src/Tgstation.Server.Host/Security/AuthenticationContextClaimsTransformation.cs b/src/Tgstation.Server.Host/Security/AuthenticationContextClaimsTransformation.cs index 4f02bb0b0d7..c5b1fe0ce22 100644 --- a/src/Tgstation.Server.Host/Security/AuthenticationContextClaimsTransformation.cs +++ b/src/Tgstation.Server.Host/Security/AuthenticationContextClaimsTransformation.cs @@ -28,7 +28,7 @@ sealed class AuthenticationContextClaimsTransformation : IClaimsTransformation /// /// The for the . /// - readonly ApiHeaders apiHeaders; + readonly ApiHeaders? apiHeaders; /// /// Initializes a new instance of the class. diff --git a/src/Tgstation.Server.Host/Security/AuthenticationContextFactory.cs b/src/Tgstation.Server.Host/Security/AuthenticationContextFactory.cs index f39daaa9897..80a9f1df920 100644 --- a/src/Tgstation.Server.Host/Security/AuthenticationContextFactory.cs +++ b/src/Tgstation.Server.Host/Security/AuthenticationContextFactory.cs @@ -88,7 +88,7 @@ public async ValueTask CreateAuthenticationContext(long .Include(x => x.CreatedBy) .Include(x => x.PermissionSet) .Include(x => x.Group) - .ThenInclude(x => x.PermissionSet) + .ThenInclude(x => x!.PermissionSet) .Include(x => x.OAuthConnections) .FirstOrDefaultAsync(cancellationToken); if (user == default) @@ -97,7 +97,7 @@ public async ValueTask CreateAuthenticationContext(long return currentAuthenticationContext; } - ISystemIdentity systemIdentity; + ISystemIdentity? systemIdentity; if (user.SystemIdentifier != null) systemIdentity = identityCache.LoadCachedIdentity(user); else @@ -111,15 +111,15 @@ public async ValueTask CreateAuthenticationContext(long systemIdentity = null; } - var userPermissionSet = user.PermissionSet ?? user.Group.PermissionSet; + var userPermissionSet = user.PermissionSet ?? user.Group!.PermissionSet; try { - InstancePermissionSet instancePermissionSet = null; + InstancePermissionSet? instancePermissionSet = null; if (instanceId.HasValue) { instancePermissionSet = await databaseContext.InstancePermissionSets .AsQueryable() - .Where(x => x.PermissionSetId == userPermissionSet.Id && x.InstanceId == instanceId && x.Instance.SwarmIdentifer == swarmConfiguration.Identifier) + .Where(x => x.PermissionSetId == userPermissionSet!.Id && x.InstanceId == instanceId && x.Instance!.SwarmIdentifer == swarmConfiguration.Identifier) .Include(x => x.Instance) .FirstOrDefaultAsync(cancellationToken); diff --git a/src/Tgstation.Server.Host/Security/AuthorizationContextHubFilter.cs b/src/Tgstation.Server.Host/Security/AuthorizationContextHubFilter.cs index 38360cab9ba..3eefac4f2ea 100644 --- a/src/Tgstation.Server.Host/Security/AuthorizationContextHubFilter.cs +++ b/src/Tgstation.Server.Host/Security/AuthorizationContextHubFilter.cs @@ -5,6 +5,8 @@ using Microsoft.AspNetCore.SignalR; using Microsoft.Extensions.Logging; +using Tgstation.Server.Host.Models; + namespace Tgstation.Server.Host.Security { /// @@ -44,7 +46,7 @@ public async Task OnConnectedAsync(HubLifetimeContext context, Func - public async ValueTask InvokeMethodAsync(HubInvocationContext invocationContext, Func> next) + public async ValueTask InvokeMethodAsync(HubInvocationContext invocationContext, Func> next) { ArgumentNullException.ThrowIfNull(invocationContext); if (ValidateAuthenticationContext(invocationContext.Hub)) @@ -62,7 +64,7 @@ bool ValidateAuthenticationContext(Hub hub) { if (!authenticationContext.Valid) logger.LogTrace("The token for connection {connectionId} is no longer authenticated! Aborting...", hub.Context.ConnectionId); - else if (!authenticationContext.User.Enabled.Value) + else if (!authenticationContext.User.Require(x => x.Enabled)) logger.LogTrace("The token for connection {connectionId} is no longer authorized! Aborting...", hub.Context.ConnectionId); else return true; @@ -73,8 +75,8 @@ bool ValidateAuthenticationContext(Hub hub) prop => prop.PropertyType.IsConstructedGenericType && prop.Name == nameof(hub.Clients)); var clients = typedClientsProperty.GetValue(hub); - var callerProperty = clients.GetType().GetProperty(nameof(hub.Clients.Caller)); - var caller = callerProperty.GetValue(clients); + var callerProperty = clients!.GetType().GetProperty(nameof(hub.Clients.Caller)); + var caller = callerProperty!.GetValue(clients); hub.Context.Abort(); return false; diff --git a/src/Tgstation.Server.Host/Security/CryptographySuite.cs b/src/Tgstation.Server.Host/Security/CryptographySuite.cs index d98e61c303f..6a401e75751 100644 --- a/src/Tgstation.Server.Host/Security/CryptographySuite.cs +++ b/src/Tgstation.Server.Host/Security/CryptographySuite.cs @@ -55,6 +55,9 @@ public bool CheckUserPassword(User user, string password) ArgumentNullException.ThrowIfNull(user); ArgumentNullException.ThrowIfNull(password); + if (user.PasswordHash == null) + throw new ArgumentException("user must have PasswordHash!", nameof(user)); + var result = passwordHasher.VerifyHashedPassword(user, user.PasswordHash, password); switch (result) { diff --git a/src/Tgstation.Server.Host/Security/IAuthenticationContext.cs b/src/Tgstation.Server.Host/Security/IAuthenticationContext.cs index 36abee2b2c5..8277a6601d2 100644 --- a/src/Tgstation.Server.Host/Security/IAuthenticationContext.cs +++ b/src/Tgstation.Server.Host/Security/IAuthenticationContext.cs @@ -26,7 +26,7 @@ public interface IAuthenticationContext /// /// The 's effective if applicable. /// - InstancePermissionSet InstancePermissionSet { get; } + InstancePermissionSet? InstancePermissionSet { get; } /// /// Get the value of a given . @@ -38,6 +38,6 @@ public interface IAuthenticationContext /// /// The of if applicable. /// - ISystemIdentity SystemIdentity { get; } + ISystemIdentity? SystemIdentity { get; } } } diff --git a/src/Tgstation.Server.Host/Security/IIdentityCache.cs b/src/Tgstation.Server.Host/Security/IIdentityCache.cs index 0832189f83f..75010f2c644 100644 --- a/src/Tgstation.Server.Host/Security/IIdentityCache.cs +++ b/src/Tgstation.Server.Host/Security/IIdentityCache.cs @@ -21,7 +21,7 @@ public interface IIdentityCache /// Attempt to load a cached . /// /// The the belongs to. - /// The cached or if it doesn't exist or expired. + /// The cached . ISystemIdentity LoadCachedIdentity(User user); } } diff --git a/src/Tgstation.Server.Host/Security/ISystemIdentityFactory.cs b/src/Tgstation.Server.Host/Security/ISystemIdentityFactory.cs index 2568861e53a..e2e245e38e2 100644 --- a/src/Tgstation.Server.Host/Security/ISystemIdentityFactory.cs +++ b/src/Tgstation.Server.Host/Security/ISystemIdentityFactory.cs @@ -22,7 +22,7 @@ public interface ISystemIdentityFactory /// The user to create a for. /// The for the operation. /// A resulting in a new based on the given or if the has no . - Task CreateSystemIdentity(User user, CancellationToken cancellationToken); + Task CreateSystemIdentity(User user, CancellationToken cancellationToken); /// /// Create a for a given username and password. @@ -30,7 +30,7 @@ public interface ISystemIdentityFactory /// The username of the user. /// The password of the user. /// The for the operation. - /// A resulting in a new based on the given credentials. - Task CreateSystemIdentity(string username, string password, CancellationToken cancellationToken); + /// A resulting in a new based on the given credentials on success, on failure. + Task CreateSystemIdentity(string username, string password, CancellationToken cancellationToken); } } diff --git a/src/Tgstation.Server.Host/Security/IdentityCache.cs b/src/Tgstation.Server.Host/Security/IdentityCache.cs index 0db031aba21..c42ffab3f67 100644 --- a/src/Tgstation.Server.Host/Security/IdentityCache.cs +++ b/src/Tgstation.Server.Host/Security/IdentityCache.cs @@ -54,12 +54,14 @@ public void CacheSystemIdentity(User user, ISystemIdentity systemIdentity, DateT ArgumentNullException.ThrowIfNull(user); ArgumentNullException.ThrowIfNull(systemIdentity); + var uid = user.Require(x => x.Id); + var sysId = systemIdentity.Uid; + lock (cachedIdentities) { - var uid = systemIdentity.Uid; - logger.LogDebug("Caching system identity {0} of user {1}", uid, user.Id); + logger.LogDebug("Caching system identity {sysId} of user {uid}", sysId, uid); - if (cachedIdentities.TryGetValue(user.Id.Value, out var identCache)) + if (cachedIdentities.TryGetValue(uid, out var identCache)) { logger.LogTrace("Expiring previously cached identity..."); identCache.Dispose(); // also clears it out @@ -70,12 +72,12 @@ public void CacheSystemIdentity(User user, ISystemIdentity systemIdentity, DateT asyncDelayer, () => { - logger.LogDebug("Expiring system identity cache for user {0}", user.Id); + logger.LogDebug("Expiring system identity cache for user {uid}", uid); lock (cachedIdentities) - cachedIdentities.Remove(user.Id.Value); + cachedIdentities.Remove(uid); }, expiry); - cachedIdentities.Add(user.Id.Value, identCache); + cachedIdentities.Add(uid, identCache); } } @@ -83,9 +85,11 @@ public void CacheSystemIdentity(User user, ISystemIdentity systemIdentity, DateT public ISystemIdentity LoadCachedIdentity(User user) { ArgumentNullException.ThrowIfNull(user); + var uid = user.Require(x => x.Id); lock (cachedIdentities) - if (cachedIdentities.TryGetValue(user.Id.Value, out var identity)) + if (cachedIdentities.TryGetValue(uid, out var identity)) return identity.SystemIdentity.Clone(); + throw new InvalidOperationException("Cached system identity has expired!"); } } diff --git a/src/Tgstation.Server.Host/Security/OAuth/DiscordOAuthValidator.cs b/src/Tgstation.Server.Host/Security/OAuth/DiscordOAuthValidator.cs index ccb4d7972a5..3195ec9dcf3 100644 --- a/src/Tgstation.Server.Host/Security/OAuth/DiscordOAuthValidator.cs +++ b/src/Tgstation.Server.Host/Security/OAuth/DiscordOAuthValidator.cs @@ -17,10 +17,10 @@ sealed class DiscordOAuthValidator : GenericOAuthValidator public override OAuthProvider Provider => OAuthProvider.Discord; /// - protected override Uri TokenUrl => new ("https://discord.com/api/oauth2/token"); + protected override Uri TokenUrl => new("https://discord.com/api/oauth2/token"); /// - protected override Uri UserInformationUrl => new ("https://discord.com/api/users/@me"); + protected override Uri UserInformationUrl => new("https://discord.com/api/users/@me"); /// /// Initializes a new instance of the class. @@ -37,7 +37,7 @@ public DiscordOAuthValidator( } /// - protected override OAuthTokenRequest CreateTokenRequest(string code) => new (OAuthConfiguration, code, "identify"); + protected override OAuthTokenRequest CreateTokenRequest(string code) => new(OAuthConfiguration, code, "identify"); /// protected override string DecodeTokenPayload(dynamic responseJson) => responseJson.access_token; diff --git a/src/Tgstation.Server.Host/Security/OAuth/GenericOAuthValidator.cs b/src/Tgstation.Server.Host/Security/OAuth/GenericOAuthValidator.cs index 8606d0921d5..5963023b0fc 100644 --- a/src/Tgstation.Server.Host/Security/OAuth/GenericOAuthValidator.cs +++ b/src/Tgstation.Server.Host/Security/OAuth/GenericOAuthValidator.cs @@ -55,7 +55,7 @@ abstract class GenericOAuthValidator : IOAuthValidator /// Gets that should be used. /// /// A new . - protected static JsonSerializerSettings SerializerSettings() => new () + protected static JsonSerializerSettings SerializerSettings() => new() { ContractResolver = new DefaultContractResolver { @@ -80,11 +80,11 @@ public GenericOAuthValidator( } /// - public async ValueTask ValidateResponseCode(string code, CancellationToken cancellationToken) + public async ValueTask ValidateResponseCode(string code, CancellationToken cancellationToken) { using var httpClient = CreateHttpClient(); - string tokenResponsePayload = null; - string userInformationPayload = null; + string? tokenResponsePayload = null; + string? userInformationPayload = null; try { Logger.LogTrace("Validating response code..."); @@ -97,7 +97,7 @@ public async ValueTask ValidateResponseCode(string code, CancellationTok tokenRequestPayload, SerializerSettings()); - var tokenRequestDictionary = JsonConvert.DeserializeObject>(tokenRequestJson); + var tokenRequestDictionary = JsonConvert.DeserializeObject>(tokenRequestJson)!; tokenRequest.Content = new FormUrlEncodedContent(tokenRequestDictionary); using var tokenResponse = await httpClient.SendAsync(tokenRequest, HttpCompletionOption.ResponseHeadersRead, cancellationToken); @@ -141,7 +141,7 @@ public async ValueTask ValidateResponseCode(string code, CancellationTok /// public OAuthProviderInfo GetProviderInfo() - => new () + => new() { ClientId = OAuthConfiguration.ClientId, RedirectUri = OAuthConfiguration.RedirectUrl, diff --git a/src/Tgstation.Server.Host/Security/OAuth/GitHubOAuthValidator.cs b/src/Tgstation.Server.Host/Security/OAuth/GitHubOAuthValidator.cs index f53fe8137c1..3f3180f9da2 100644 --- a/src/Tgstation.Server.Host/Security/OAuth/GitHubOAuthValidator.cs +++ b/src/Tgstation.Server.Host/Security/OAuth/GitHubOAuthValidator.cs @@ -52,7 +52,7 @@ public GitHubOAuthValidator( } /// - public async ValueTask ValidateResponseCode(string code, CancellationToken cancellationToken) + public async ValueTask ValidateResponseCode(string code, CancellationToken cancellationToken) { ArgumentNullException.ThrowIfNull(code); @@ -85,7 +85,7 @@ public async ValueTask ValidateResponseCode(string code, CancellationTok /// public OAuthProviderInfo GetProviderInfo() - => new () + => new() { ClientId = oAuthConfiguration.ClientId, RedirectUri = oAuthConfiguration.RedirectUrl, diff --git a/src/Tgstation.Server.Host/Security/OAuth/IOAuthProviders.cs b/src/Tgstation.Server.Host/Security/OAuth/IOAuthProviders.cs index c8b8a7b1615..2da6651c041 100644 --- a/src/Tgstation.Server.Host/Security/OAuth/IOAuthProviders.cs +++ b/src/Tgstation.Server.Host/Security/OAuth/IOAuthProviders.cs @@ -14,7 +14,7 @@ public interface IOAuthProviders /// /// The to get the validator for. /// The for . - IOAuthValidator GetValidator(OAuthProvider oAuthProvider); + IOAuthValidator? GetValidator(OAuthProvider oAuthProvider); /// /// Gets a of the provider client IDs. diff --git a/src/Tgstation.Server.Host/Security/OAuth/IOAuthValidator.cs b/src/Tgstation.Server.Host/Security/OAuth/IOAuthValidator.cs index 7f3ca57287f..472a7ff091d 100644 --- a/src/Tgstation.Server.Host/Security/OAuth/IOAuthValidator.cs +++ b/src/Tgstation.Server.Host/Security/OAuth/IOAuthValidator.cs @@ -27,6 +27,6 @@ public interface IOAuthValidator /// The OAuth response string from web application. /// The for the operation. /// A resulting in if authentication failed, if a rate limit occurred, and the validated otherwise. - ValueTask ValidateResponseCode(string code, CancellationToken cancellationToken); + ValueTask ValidateResponseCode(string code, CancellationToken cancellationToken); } } diff --git a/src/Tgstation.Server.Host/Security/OAuth/InvisionCommunityOAuthValidator.cs b/src/Tgstation.Server.Host/Security/OAuth/InvisionCommunityOAuthValidator.cs index 54d6883e3de..0e912bd3688 100644 --- a/src/Tgstation.Server.Host/Security/OAuth/InvisionCommunityOAuthValidator.cs +++ b/src/Tgstation.Server.Host/Security/OAuth/InvisionCommunityOAuthValidator.cs @@ -12,15 +12,15 @@ namespace Tgstation.Server.Host.Security.OAuth /// OAuth validator for Invision Community (selfhosted). /// sealed class InvisionCommunityOAuthValidator : GenericOAuthValidator - { + { /// public override OAuthProvider Provider => OAuthProvider.InvisionCommunity; /// - protected override Uri TokenUrl => new ($"{OAuthConfiguration.ServerUrl}/oauth/token/"); // This needs the trailing slash or it doesnt get the token. Do not remove. + protected override Uri TokenUrl => new($"{OAuthConfiguration.ServerUrl}/oauth/token/"); // This needs the trailing slash or it doesnt get the token. Do not remove. /// - protected override Uri UserInformationUrl => new ($"{OAuthConfiguration.ServerUrl}/api/core/me"); + protected override Uri UserInformationUrl => new($"{OAuthConfiguration.ServerUrl}/api/core/me"); /// /// Initializes a new instance of the class. @@ -37,7 +37,7 @@ public InvisionCommunityOAuthValidator( } /// - protected override OAuthTokenRequest CreateTokenRequest(string code) => new (OAuthConfiguration, code, "profile"); + protected override OAuthTokenRequest CreateTokenRequest(string code) => new(OAuthConfiguration, code, "profile"); /// protected override string DecodeTokenPayload(dynamic responseJson) => responseJson.access_token; diff --git a/src/Tgstation.Server.Host/Security/OAuth/KeycloakOAuthValidator.cs b/src/Tgstation.Server.Host/Security/OAuth/KeycloakOAuthValidator.cs index 03b3ea87325..f4e61b4b5ce 100644 --- a/src/Tgstation.Server.Host/Security/OAuth/KeycloakOAuthValidator.cs +++ b/src/Tgstation.Server.Host/Security/OAuth/KeycloakOAuthValidator.cs @@ -17,10 +17,10 @@ sealed class KeycloakOAuthValidator : GenericOAuthValidator public override OAuthProvider Provider => OAuthProvider.Keycloak; /// - protected override Uri TokenUrl => new ($"{BaseProtocolPath}/token"); + protected override Uri TokenUrl => new($"{BaseProtocolPath}/token"); /// - protected override Uri UserInformationUrl => new ($"{BaseProtocolPath}/userinfo"); + protected override Uri UserInformationUrl => new($"{BaseProtocolPath}/userinfo"); /// /// Base path to the server's OAuth endpoint. @@ -42,7 +42,7 @@ public KeycloakOAuthValidator( } /// - protected override OAuthTokenRequest CreateTokenRequest(string code) => new (OAuthConfiguration, code, "openid"); + protected override OAuthTokenRequest CreateTokenRequest(string code) => new(OAuthConfiguration, code, "openid"); /// protected override string DecodeTokenPayload(dynamic responseJson) => responseJson.access_token; diff --git a/src/Tgstation.Server.Host/Security/OAuth/OAuthProviders.cs b/src/Tgstation.Server.Host/Security/OAuth/OAuthProviders.cs index 7a562668ee8..9d28ab1c824 100644 --- a/src/Tgstation.Server.Host/Security/OAuth/OAuthProviders.cs +++ b/src/Tgstation.Server.Host/Security/OAuth/OAuthProviders.cs @@ -80,7 +80,7 @@ public OAuthProviders( } /// - public IOAuthValidator GetValidator(OAuthProvider oAuthProvider) => validators.FirstOrDefault(x => x.Provider == oAuthProvider); + public IOAuthValidator? GetValidator(OAuthProvider oAuthProvider) => validators.FirstOrDefault(x => x.Provider == oAuthProvider); /// public Dictionary ProviderInfos() diff --git a/src/Tgstation.Server.Host/Security/OAuth/OAuthTokenRequest.cs b/src/Tgstation.Server.Host/Security/OAuth/OAuthTokenRequest.cs index 9256092f101..a3955ceaff6 100644 --- a/src/Tgstation.Server.Host/Security/OAuth/OAuthTokenRequest.cs +++ b/src/Tgstation.Server.Host/Security/OAuth/OAuthTokenRequest.cs @@ -22,7 +22,7 @@ sealed class OAuthTokenRequest : OAuthConfigurationBase /// /// The OAuth redirect URI. /// - public Uri RedirectUri { get; } + public Uri? RedirectUri { get; } /// /// The OAuth grant type. diff --git a/src/Tgstation.Server.Host/Security/OAuth/TGForumsOAuthValidator.cs b/src/Tgstation.Server.Host/Security/OAuth/TGForumsOAuthValidator.cs index a65b8346a4f..c81a54ec738 100644 --- a/src/Tgstation.Server.Host/Security/OAuth/TGForumsOAuthValidator.cs +++ b/src/Tgstation.Server.Host/Security/OAuth/TGForumsOAuthValidator.cs @@ -17,10 +17,10 @@ sealed class TGForumsOAuthValidator : GenericOAuthValidator public override OAuthProvider Provider => OAuthProvider.TGForums; /// - protected override Uri TokenUrl => new ("https://tgstation13.org/phpBB/app.php/tgapi/oauth/token"); + protected override Uri TokenUrl => new("https://tgstation13.org/phpBB/app.php/tgapi/oauth/token"); /// - protected override Uri UserInformationUrl => new ("https://tgstation13.org/phpBB/app.php/tgapi/user/me"); + protected override Uri UserInformationUrl => new("https://tgstation13.org/phpBB/app.php/tgapi/user/me"); /// /// Initializes a new instance of the class. @@ -46,6 +46,6 @@ public TGForumsOAuthValidator( protected override string DecodeUserInformationPayload(dynamic responseJson) => responseJson.phpbb_username; /// - protected override OAuthTokenRequest CreateTokenRequest(string code) => new (OAuthConfiguration, code, "user"); + protected override OAuthTokenRequest CreateTokenRequest(string code) => new(OAuthConfiguration, code, "user"); } } diff --git a/src/Tgstation.Server.Host/Security/PosixSystemIdentityFactory.cs b/src/Tgstation.Server.Host/Security/PosixSystemIdentityFactory.cs index e85882665bf..062a42747f3 100644 --- a/src/Tgstation.Server.Host/Security/PosixSystemIdentityFactory.cs +++ b/src/Tgstation.Server.Host/Security/PosixSystemIdentityFactory.cs @@ -16,9 +16,9 @@ sealed class PosixSystemIdentityFactory : ISystemIdentityFactory public ISystemIdentity GetCurrent() => new PosixSystemIdentity(); /// - public Task CreateSystemIdentity(User user, CancellationToken cancellationToken) => throw new NotImplementedException(); + public Task CreateSystemIdentity(User user, CancellationToken cancellationToken) => throw new NotImplementedException(); /// - public Task CreateSystemIdentity(string username, string password, CancellationToken cancellationToken) => throw new NotImplementedException(); + public Task CreateSystemIdentity(string username, string password, CancellationToken cancellationToken) => throw new NotImplementedException(); } } diff --git a/src/Tgstation.Server.Host/Security/README.md b/src/Tgstation.Server.Host/Security/README.md index 404b27e1627..ac6cbbc08ff 100644 --- a/src/Tgstation.Server.Host/Security/README.md +++ b/src/Tgstation.Server.Host/Security/README.md @@ -16,7 +16,7 @@ ## For the login request (`POST /`) -1. An attempt to parse the `ApiHeaders` is made. If they were valid, the API version check is performed. If it fails, HTTP 426 with an `ErrorMessageResponse` will be returned. +1. An attempt to parse the `ApiHeaders` is made. If they were valid, the API version check is performed. If it fails, HTTP 400 with an `ErrorMessageResponse` will be returned. 1. If, for some reason, the user attempts to use a JWT to authenticate this request, steps 2-4 of the non-login pipeline list below are performed. 1. The `ApiController` base class inspects the request. - At this point, if the `ApiHeaders` (MINUS the `Authorization` header) cannot be properly parsed, HTTP 400 with an `ErrorMessageResponse` is returned. @@ -51,7 +51,7 @@ ## For all other authenticated requests -1. An attempt to parse the `ApiHeaders` is made. If they were valid. The API version check is performed. If it fails, HTTP 426 with an `ErrorMessageResponse` will be returned. +1. An attempt to parse the `ApiHeaders` is made. If they were valid. The API version check is performed. If it fails, HTTP 400 with an `ErrorMessageResponse` will be returned. 1. The JWT, if present, is validated. If it is, the scope's [AuthenticationContextFactory](./AuthenticationContextFactory.cs) has `SetTokenNbf` called. If not, HTTP 401 will be returned. - Inside ASP.NET Core, this initializes the calling user's identity principal and sets the "sub" claim to the TGS user ID parsed out of the JWT. - We know it's the user ID because we set it up like that in the [TokenFactory](./TokenFactory.cs) diff --git a/src/Tgstation.Server.Host/Security/TgsAuthorizeAttribute.cs b/src/Tgstation.Server.Host/Security/TgsAuthorizeAttribute.cs index 2f6b62a848b..0fc4c11c1a0 100644 --- a/src/Tgstation.Server.Host/Security/TgsAuthorizeAttribute.cs +++ b/src/Tgstation.Server.Host/Security/TgsAuthorizeAttribute.cs @@ -58,11 +58,11 @@ public TgsAuthorizeAttribute(RepositoryRights requiredRights) /// /// Initializes a new instance of the class. /// - /// The required. - public TgsAuthorizeAttribute(ByondRights requiredRights) + /// The required. + public TgsAuthorizeAttribute(EngineRights requiredRights) { Roles = RightsHelper.RoleNames(requiredRights); - RightsType = Api.Rights.RightsType.Byond; + RightsType = Api.Rights.RightsType.Engine; } /// diff --git a/src/Tgstation.Server.Host/Security/TokenFactory.cs b/src/Tgstation.Server.Host/Security/TokenFactory.cs index c45a282e2ae..fcc5990c2f1 100644 --- a/src/Tgstation.Server.Host/Security/TokenFactory.cs +++ b/src/Tgstation.Server.Host/Security/TokenFactory.cs @@ -10,6 +10,7 @@ using Tgstation.Server.Api.Models.Response; using Tgstation.Server.Host.Configuration; +using Tgstation.Server.Host.Models; using Tgstation.Server.Host.System; namespace Tgstation.Server.Host.Security @@ -82,10 +83,11 @@ public TokenFactory( } /// - public TokenResponse CreateToken(Models.User user, bool oAuth) + public TokenResponse CreateToken(User user, bool oAuth) { ArgumentNullException.ThrowIfNull(user); + var uid = user.Require(x => x.Id); var now = DateTimeOffset.UtcNow; var nowUnix = now.ToUnixTimeSeconds(); @@ -113,19 +115,16 @@ public TokenResponse CreateToken(Models.User user, bool oAuth) Enumerable.Empty(), new Dictionary { - { JwtRegisteredClaimNames.Sub, user.Id.Value.ToString(CultureInfo.InvariantCulture) }, + { JwtRegisteredClaimNames.Sub, uid.ToString(CultureInfo.InvariantCulture) }, }, notBefore.UtcDateTime, expiry.UtcDateTime, now.UtcDateTime)); -#pragma warning disable CS0618 // Type or member is obsolete var tokenResponse = new TokenResponse { Bearer = tokenHandler.WriteToken(securityToken), - ExpiresAt = expiry, }; -#pragma warning restore CS0618 // Type or member is obsolete return tokenResponse; } diff --git a/src/Tgstation.Server.Host/Security/WindowsSystemIdentity.cs b/src/Tgstation.Server.Host/Security/WindowsSystemIdentity.cs index 28b6bbcec7a..e15b51557d5 100644 --- a/src/Tgstation.Server.Host/Security/WindowsSystemIdentity.cs +++ b/src/Tgstation.Server.Host/Security/WindowsSystemIdentity.cs @@ -16,10 +16,10 @@ namespace Tgstation.Server.Host.Security sealed class WindowsSystemIdentity : ISystemIdentity { /// - public string Uid => (userPrincipal?.Sid ?? identity.User).ToString(); + public string Uid => (userPrincipal?.Sid ?? identity!.User!).ToString(); // we kno user isn't null because it can only be the case when anonymous (checked in this constructor) /// - public string Username => userPrincipal?.Name ?? identity.Name; + public string Username => userPrincipal?.Name ?? identity!.Name; /// public bool CanCreateSymlinks => canCreateSymlinks ?? throw new NotSupportedException(); @@ -27,12 +27,12 @@ sealed class WindowsSystemIdentity : ISystemIdentity /// /// The for the . /// - readonly WindowsIdentity identity; + readonly WindowsIdentity? identity; /// /// The for the . /// - readonly UserPrincipal userPrincipal; + readonly UserPrincipal? userPrincipal; /// /// Backing field for . @@ -46,6 +46,9 @@ sealed class WindowsSystemIdentity : ISystemIdentity public WindowsSystemIdentity(WindowsIdentity identity) { this.identity = identity ?? throw new ArgumentNullException(nameof(identity)); + if (identity.IsAnonymous) + throw new InvalidOperationException($"Cannot use anonymous {nameof(WindowsIdentity)} as a {nameof(WindowsSystemIdentity)}!"); + canCreateSymlinks = new WindowsPrincipal(identity).IsInRole(WindowsBuiltInRole.Administrator); } @@ -65,7 +68,7 @@ public void Dispose() identity.Dispose(); else { - var context = userPrincipal.Context; + var context = userPrincipal!.Context; userPrincipal.Dispose(); context.Dispose(); } diff --git a/src/Tgstation.Server.Host/Security/WindowsSystemIdentityFactory.cs b/src/Tgstation.Server.Host/Security/WindowsSystemIdentityFactory.cs index b677a56413d..ca47a871929 100644 --- a/src/Tgstation.Server.Host/Security/WindowsSystemIdentityFactory.cs +++ b/src/Tgstation.Server.Host/Security/WindowsSystemIdentityFactory.cs @@ -1,4 +1,5 @@ using System; +using System.Diagnostics.CodeAnalysis; using System.DirectoryServices.AccountManagement; using System.Runtime.Versioning; using System.Security.Principal; @@ -31,7 +32,7 @@ sealed class WindowsSystemIdentityFactory : ISystemIdentityFactory /// The input . /// The output username. /// The output domain name. May be . - static void GetUserAndDomainName(string input, out string username, out string domainName) + static void GetUserAndDomainName(string input, out string username, out string? domainName) { var splits = input.Split('\\'); username = splits.Length > 1 ? splits[1] : splits[0]; @@ -51,7 +52,7 @@ public WindowsSystemIdentityFactory(ILogger logger public ISystemIdentity GetCurrent() => new WindowsSystemIdentity(WindowsIdentity.GetCurrent()); /// - public Task CreateSystemIdentity(User user, CancellationToken cancellationToken) => Task.Factory.StartNew( + public Task CreateSystemIdentity(User user, CancellationToken cancellationToken) => Task.Factory.StartNew( () => { ArgumentNullException.ThrowIfNull(user); @@ -59,13 +60,12 @@ public Task CreateSystemIdentity(User user, CancellationToken c if (user.SystemIdentifier == null) throw new InvalidOperationException("User's SystemIdentifier must not be null!"); - PrincipalContext pc = null; - UserPrincipal principal = null; - + PrincipalContext? pc = null; GetUserAndDomainName(user.SystemIdentifier, out _, out var domainName); - bool TryGetPrincipalFromContextType(ContextType contextType) + bool TryGetPrincipalFromContextType(ContextType contextType, [NotNullWhen(true)] out UserPrincipal? principal) { + principal = null; try { pc = domainName != null @@ -98,7 +98,7 @@ bool TryGetPrincipalFromContextType(ContextType contextType) return principal != null; } - if (!TryGetPrincipalFromContextType(ContextType.Machine) && !TryGetPrincipalFromContextType(ContextType.Domain)) + if (!TryGetPrincipalFromContextType(ContextType.Machine, out var principal) && !TryGetPrincipalFromContextType(ContextType.Domain, out principal)) return null; return (ISystemIdentity)new WindowsSystemIdentity(principal); }, @@ -107,7 +107,7 @@ bool TryGetPrincipalFromContextType(ContextType contextType) TaskScheduler.Current); /// - public Task CreateSystemIdentity(string username, string password, CancellationToken cancellationToken) => Task.Factory.StartNew( + public Task CreateSystemIdentity(string username, string password, CancellationToken cancellationToken) => Task.Factory.StartNew( () => { ArgumentNullException.ThrowIfNull(username); diff --git a/src/Tgstation.Server.Host/Server.cs b/src/Tgstation.Server.Host/Server.cs index e6ea8c04b1e..751cd2d453d 100644 --- a/src/Tgstation.Server.Host/Server.cs +++ b/src/Tgstation.Server.Host/Server.cs @@ -13,6 +13,7 @@ using Tgstation.Server.Common.Extensions; using Tgstation.Server.Host.Configuration; using Tgstation.Server.Host.Core; +using Tgstation.Server.Host.Utils; namespace Tgstation.Server.Host { @@ -36,7 +37,7 @@ sealed class Server : IServer, IServerControl /// /// The of the running server. /// - internal IHost Host { get; private set; } + internal IHost? Host { get; private set; } /// /// The for the . @@ -51,7 +52,7 @@ sealed class Server : IServer, IServerControl /// /// The absolute path to install updates to. /// - readonly string updatePath; + readonly string? updatePath; /// /// for certain restart related operations. @@ -61,27 +62,27 @@ sealed class Server : IServer, IServerControl /// /// The for the . /// - ILogger logger; + ILogger? logger; /// /// The for the . /// - GeneralConfiguration generalConfiguration; + GeneralConfiguration? generalConfiguration; /// /// The for the . /// - CancellationTokenSource cancellationTokenSource; + CancellationTokenSource? cancellationTokenSource; /// /// The to propagate when the server terminates. /// - Exception propagatedException; + Exception? propagatedException; /// /// The that is used for asynchronously updating the server. /// - Task updateTask; + Task? updateTask; /// /// If the server is being shut down or restarted. @@ -98,7 +99,7 @@ sealed class Server : IServer, IServerControl /// /// The value of . /// The value of . - public Server(IHostBuilder hostBuilder, string updatePath) + public Server(IHostBuilder hostBuilder, string? updatePath) { this.hostBuilder = hostBuilder ?? throw new ArgumentNullException(nameof(hostBuilder)); this.updatePath = updatePath; @@ -107,15 +108,17 @@ public Server(IHostBuilder hostBuilder, string updatePath) restartHandlers = new List(); restartLock = new object(); + logger = null; } /// public async ValueTask Run(CancellationToken cancellationToken) { + var updateDirectory = updatePath != null ? Path.GetDirectoryName(updatePath) : null; using (cancellationTokenSource = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken)) - using (var fsWatcher = updatePath != null ? new FileSystemWatcher(Path.GetDirectoryName(updatePath)) : null) + using (var fsWatcher = updateDirectory != null ? new FileSystemWatcher(updateDirectory) : null) { - if (updatePath != null) + if (fsWatcher != 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 @@ -128,9 +131,10 @@ public async ValueTask Run(CancellationToken cancellationToken) try { using (Host = hostBuilder.Build()) + { + logger = Host.Services.GetRequiredService>(); try { - logger = Host.Services.GetRequiredService>(); using (cancellationToken.Register(() => logger.LogInformation("Server termination requested!"))) { var generalConfigurationOptions = Host.Services.GetRequiredService>(); @@ -154,6 +158,7 @@ public async ValueTask Run(CancellationToken cancellationToken) { logger = null; } + } } finally { @@ -172,6 +177,10 @@ public bool TryStartUpdate(IServerUpdateExecutor updateExecutor, Version newVers CheckSanity(true); + if (updatePath == null) + throw new InvalidOperationException("Tried to start update when server was initialized without an updatePath set!"); + + var logger = this.logger!; logger.LogTrace("Begin ApplyUpdate..."); CancellationToken criticalCancellationToken; @@ -220,17 +229,19 @@ public IRestartRegistration RegisterForRestart(IRestartHandler handler) CheckSanity(false); + var logger = this.logger!; lock (restartLock) if (!shutdownInProgress) { logger.LogTrace("Registering restart handler {handlerImplementationName}...", handler); restartHandlers.Add(handler); - return new RestartRegistration(() => - { - lock (restartLock) - if (!shutdownInProgress) - restartHandlers.Remove(handler); - }); + return new RestartRegistration( + new DisposeInvoker(() => + { + lock (restartLock) + if (!shutdownInProgress) + restartHandlers.Remove(handler); + })); } logger.LogWarning("Restart handler {handlerImplementationName} register after a shutdown had begun!", handler); @@ -244,7 +255,7 @@ public IRestartRegistration RegisterForRestart(IRestartHandler handler) public ValueTask GracefulShutdown(bool detach) => RestartImpl(null, null, false, detach); /// - public ValueTask Die(Exception exception) + public ValueTask Die(Exception? exception) { if (exception != null) return RestartImpl(null, exception, false, true); @@ -270,7 +281,7 @@ void CheckSanity(bool checkWatchdog) /// Re-throw if it exists. /// /// An existing that should be thrown as well, but not by itself. - void CheckExceptionPropagation(Exception otherException) + void CheckExceptionPropagation(Exception? otherException) { if (propagatedException == null) return; @@ -289,12 +300,13 @@ void CheckExceptionPropagation(Exception otherException) /// If the host watchdog is required for this "restart". /// If the restart should wait for extremely long running tasks to complete (Like the current DreamDaemon world). /// A representing the running operation. - async ValueTask RestartImpl(Version newVersion, Exception exception, bool requireWatchdog, bool completeAsap) + async ValueTask RestartImpl(Version? newVersion, Exception? exception, bool requireWatchdog, bool completeAsap) { CheckSanity(requireWatchdog); // if the watchdog isn't required and there's no issue, this is just a graceful shutdown bool isGracefulShutdown = !requireWatchdog && exception == null; + var logger = this.logger!; logger.LogTrace( "Begin {restartType}...", isGracefulShutdown @@ -322,8 +334,8 @@ async ValueTask RestartImpl(Version newVersion, Exception exception, bool requir using var cts = new CancellationTokenSource( TimeSpan.FromMinutes( giveHandlersTimeToWaitAround - ? generalConfiguration.ShutdownTimeoutMinutes - : generalConfiguration.RestartTimeoutMinutes)); + ? generalConfiguration!.ShutdownTimeoutMinutes + : generalConfiguration!.RestartTimeoutMinutes)); var cancellationToken = cts.Token; try { @@ -366,7 +378,7 @@ void WatchForShutdownFileCreation(object sender, FileSystemEventArgs eventArgs) logger?.LogTrace("FileSystemWatcher triggered."); // TODO: Refactor this to not use System.IO function here. - if (eventArgs.FullPath == Path.GetFullPath(updatePath) && File.Exists(eventArgs.FullPath)) + if (eventArgs.FullPath == Path.GetFullPath(updatePath!) && File.Exists(eventArgs.FullPath)) { logger?.LogInformation("Host watchdog appears to be requesting server termination!"); lock (restartLock) @@ -390,8 +402,8 @@ void WatchForShutdownFileCreation(object sender, FileSystemEventArgs eventArgs) void StopServerImmediate() { shutdownInProgress = true; - logger.LogDebug("Stopping host..."); - cancellationTokenSource.Cancel(); + logger!.LogDebug("Stopping host..."); + cancellationTokenSource!.Cancel(); } } } diff --git a/src/Tgstation.Server.Host/ServerFactory.cs b/src/Tgstation.Server.Host/ServerFactory.cs index 9007f9bc58b..f92298da10d 100644 --- a/src/Tgstation.Server.Host/ServerFactory.cs +++ b/src/Tgstation.Server.Host/ServerFactory.cs @@ -51,7 +51,7 @@ internal ServerFactory(IAssemblyInformationProvider assemblyInformationProvider, /// // TODO: Decomplexify #pragma warning disable CA1506 - public async ValueTask CreateServer(string[] args, string updatePath, CancellationToken cancellationToken) + public async ValueTask CreateServer(string[] args, string? updatePath, CancellationToken cancellationToken) { ArgumentNullException.ThrowIfNull(args); @@ -93,10 +93,11 @@ IHostBuilder CreateDefaultBuilder() => Microsoft.Extensions.Hosting.Host.CreateD // CURSED // https://github.com/dotnet/runtime/blob/30dc7e7aedb7aab085c7d9702afeae5bc5a43133/src/libraries/Microsoft.Extensions.Hosting/src/HostingHostBuilderExtensions.cs#L246-L249 -#if !NET6_0 +#if !NET8_0 #error Validate this monstrosity works on current .NET #endif - IConfigurationSource cmdLineConfig, baseYmlConfig, environmentYmlConfig; + IConfigurationSource? cmdLineConfig; + IConfigurationSource baseYmlConfig, environmentYmlConfig; if (args.Length == 0) { cmdLineConfig = null; @@ -145,7 +146,7 @@ IHostBuilder CreateDefaultBuilder() => Microsoft.Extensions.Hosting.Host.CreateD var serverPortProvider = kestrelOptions.ApplicationServices.GetRequiredService(); kestrelOptions.ListenAnyIP( serverPortProvider.HttpApiPort, - listenOptions => listenOptions.Protocols = HttpProtocols.Http1AndHttp2); + listenOptions => listenOptions.Protocols = HttpProtocols.Http1); // Can't use Http1And2 without TLS. Let the reverse proxy handle it // with 515 we lost the ability to test this effectively. Just bump it slightly above the default and let the existing limit hold us back kestrelOptions.Limits.MaxRequestLineSize = 8400; diff --git a/src/Tgstation.Server.Host/Setup/SetupWizard.cs b/src/Tgstation.Server.Host/Setup/SetupWizard.cs index a1a5717e8ca..c8007f16b50 100644 --- a/src/Tgstation.Server.Host/Setup/SetupWizard.cs +++ b/src/Tgstation.Server.Host/Setup/SetupWizard.cs @@ -220,9 +220,12 @@ async ValueTask TestDatabaseConnection( await console.WriteAsync($"Checking {databaseConfiguration.DatabaseType} version...", true, cancellationToken); using var command = testConnection.CreateCommand(); command.CommandText = "SELECT VERSION()"; - var fullVersion = (string)await command.ExecuteScalarAsync(cancellationToken); + var fullVersion = (string?)await command.ExecuteScalarAsync(cancellationToken); await console.WriteAsync(String.Format(CultureInfo.InvariantCulture, "Found {0}", fullVersion), true, cancellationToken); + if (fullVersion == null) + throw new InvalidOperationException($"\"{command.CommandText}\" returned null!"); + if (databaseConfiguration.DatabaseType == DatabaseType.PostgresSql) { var splits = fullVersion.Split(' '); @@ -291,7 +294,7 @@ async ValueTask TestDatabaseConnection( /// The path to the potential SQLite database file. /// The for the operation. /// A resulting in the SQLite database path to store in the configuration. - async ValueTask ValidateNonExistantSqliteDBName(string databaseName, CancellationToken cancellationToken) + async ValueTask ValidateNonExistantSqliteDBName(string databaseName, CancellationToken cancellationToken) { var dbPathIsRooted = Path.IsPathRooted(databaseName); var resolvedPath = ioManager.ResolvePath( @@ -406,12 +409,12 @@ async ValueTask ConfigureDatabase(CancellationToken cance DatabaseType = await PromptDatabaseType(firstTime, cancellationToken), }; - string serverAddress = null; + string? serverAddress = null; ushort? serverPort = null; var definitelyLocalMariaDB = firstTime && internalConfiguration.MariaDBSetup; var isSqliteDB = databaseConfiguration.DatabaseType == DatabaseType.Sqlite; - IPHostEntry serverAddressEntry = null; + IPHostEntry? serverAddressEntry = null; if (!isSqliteDB) do { @@ -468,7 +471,7 @@ async ValueTask ConfigureDatabase(CancellationToken cance await console.WriteAsync(null, true, cancellationToken); await console.WriteAsync($"Enter the database {(isSqliteDB ? "file path" : "name")} ({(definitelyLocalMariaDB ? "leave blank for \"tgs\")" : "Can be from previous installation. Otherwise, should not exist")}): ", false, cancellationToken); - string databaseName; + string? databaseName; bool dbExists = false; do { @@ -510,8 +513,8 @@ async ValueTask ConfigureDatabase(CancellationToken cance await console.WriteAsync(null, true, cancellationToken); - string username = null; - string password = null; + string? username = null; + string? password = null; if (!isSqliteDB) if (!useWinAuth) { @@ -552,6 +555,7 @@ void CreateTestConnection(string connectionString) => { ApplicationName = assemblyInformationProvider.VersionPrefix, DataSource = serverAddress ?? "(local)", + Encrypt = encrypt, }; if (useWinAuth) @@ -599,6 +603,8 @@ void CreateTestConnection(string connectionString) => }; CreateTestConnection(csb.ConnectionString); + + csb.Mode = SqliteOpenMode.ReadWriteCreate; databaseConfiguration.ConnectionString = csb.ConnectionString; } @@ -882,7 +888,7 @@ async ValueTask ConfigureControlPanel(CancellationTok /// /// The for the operation. /// A resulting in the new . - async ValueTask ConfigureSwarm(CancellationToken cancellationToken) + async ValueTask ConfigureSwarm(CancellationToken cancellationToken) { var enable = await PromptYesNo("Enable swarm mode?", false, cancellationToken); if (!enable) @@ -899,7 +905,7 @@ async ValueTask ConfigureSwarm(CancellationToken cancellatio async ValueTask ParseAddress(string question) { var first = true; - Uri address; + Uri? address; do { if (first) @@ -930,7 +936,7 @@ async ValueTask ParseAddress(string question) while (String.IsNullOrWhiteSpace(privateKey)); var controller = await PromptYesNo("Is this server the swarm's controller? (y/n): ", null, cancellationToken); - Uri controllerAddress = null; + Uri? controllerAddress = null; if (!controller) controllerAddress = await ParseAddress("Enter the swarm controller's HTTP(S) address: "); @@ -962,15 +968,15 @@ async ValueTask SaveConfiguration( ushort? hostingPort, DatabaseConfiguration databaseConfiguration, GeneralConfiguration newGeneralConfiguration, - FileLoggingConfiguration fileLoggingConfiguration, - ElasticsearchConfiguration elasticsearchConfiguration, + FileLoggingConfiguration? fileLoggingConfiguration, + ElasticsearchConfiguration? elasticsearchConfiguration, ControlPanelConfiguration controlPanelConfiguration, - SwarmConfiguration swarmConfiguration, + SwarmConfiguration? swarmConfiguration, CancellationToken cancellationToken) { newGeneralConfiguration.ApiPort = hostingPort ?? GeneralConfiguration.DefaultApiPort; newGeneralConfiguration.ConfigVersion = GeneralConfiguration.CurrentConfigVersion; - var map = new Dictionary() + var map = new Dictionary() { { DatabaseConfiguration.Section, databaseConfiguration }, { GeneralConfiguration.Section, newGeneralConfiguration }, @@ -1094,14 +1100,14 @@ async Task HandleSetupCancel() } Task finalTask = Task.CompletedTask; - string originalConsoleTitle = null; + string? originalConsoleTitle = null; void SetConsoleTitle() { if (originalConsoleTitle != null) return; originalConsoleTitle = console.Title; - console.Title = $"{assemblyInformationProvider.VersionString} Setup Wizard"; + console.SetTitle($"{assemblyInformationProvider.VersionString} Setup Wizard"); } // Link passed cancellationToken with cancel key press @@ -1191,7 +1197,7 @@ await SaveConfiguration( { await finalTask; if (originalConsoleTitle != null) - console.Title = originalConsoleTitle; + console.SetTitle(originalConsoleTitle); } } } diff --git a/src/Tgstation.Server.Host/Swarm/ISwarmOperations.cs b/src/Tgstation.Server.Host/Swarm/ISwarmOperations.cs index a1c0a276cda..dd9c01b7830 100644 --- a/src/Tgstation.Server.Host/Swarm/ISwarmOperations.cs +++ b/src/Tgstation.Server.Host/Swarm/ISwarmOperations.cs @@ -56,6 +56,6 @@ public interface ISwarmOperations : ISwarmUpdateAborter /// The registration . /// The for the operation. /// A representing the running operation. - ValueTask RemoteCommitRecieved(Guid registrationId, CancellationToken cancellationToken); + ValueTask RemoteCommitReceived(Guid registrationId, CancellationToken cancellationToken); } } diff --git a/src/Tgstation.Server.Host/Swarm/ISwarmService.cs b/src/Tgstation.Server.Host/Swarm/ISwarmService.cs index 9b863895402..8e827eb1319 100644 --- a/src/Tgstation.Server.Host/Swarm/ISwarmService.cs +++ b/src/Tgstation.Server.Host/Swarm/ISwarmService.cs @@ -38,6 +38,6 @@ public interface ISwarmService : ISwarmUpdateAborter /// Gets the list of s in the swarm, including the current one. /// /// A of s in the swarm. If the server is not part of a swarm, will be returned. - ICollection GetSwarmServers(); + ICollection? GetSwarmServers(); } } diff --git a/src/Tgstation.Server.Host/Swarm/SwarmConstants.cs b/src/Tgstation.Server.Host/Swarm/SwarmConstants.cs index fe2b892c327..988c60243ef 100644 --- a/src/Tgstation.Server.Host/Swarm/SwarmConstants.cs +++ b/src/Tgstation.Server.Host/Swarm/SwarmConstants.cs @@ -14,7 +14,7 @@ static class SwarmConstants /// /// The base route for . /// - public const string ControllerRoute = Routes.Root + "Swarm"; + public const string ControllerRoute = Routes.ApiRoot + "Swarm"; /// /// The header used to pass in the . @@ -66,7 +66,7 @@ static class SwarmConstants /// static SwarmConstants() { - SerializerSettings = new () + SerializerSettings = new() { ContractResolver = new DefaultContractResolver { diff --git a/src/Tgstation.Server.Host/Swarm/SwarmRegistrationRequest.cs b/src/Tgstation.Server.Host/Swarm/SwarmRegistrationRequest.cs index cad1cebe279..229d4d28d20 100644 --- a/src/Tgstation.Server.Host/Swarm/SwarmRegistrationRequest.cs +++ b/src/Tgstation.Server.Host/Swarm/SwarmRegistrationRequest.cs @@ -14,6 +14,15 @@ public sealed class SwarmRegistrationRequest : SwarmServer /// The TGS of the sending server. /// [Required] - public Version ServerVersion { get; set; } + public Version ServerVersion { get; } + + /// + /// Initializes a new instance of the class. + /// + /// The value of . + public SwarmRegistrationRequest(Version serverVersion) + { + ServerVersion = serverVersion ?? throw new ArgumentNullException(nameof(serverVersion)); + } } } diff --git a/src/Tgstation.Server.Host/Swarm/SwarmServersUpdateRequest.cs b/src/Tgstation.Server.Host/Swarm/SwarmServersUpdateRequest.cs index 61b60cc0242..06fb6260bba 100644 --- a/src/Tgstation.Server.Host/Swarm/SwarmServersUpdateRequest.cs +++ b/src/Tgstation.Server.Host/Swarm/SwarmServersUpdateRequest.cs @@ -14,6 +14,6 @@ public sealed class SwarmServersUpdateRequest /// The of updated s. /// [Required] - public ICollection SwarmServers { get; set; } + public ICollection? SwarmServers { get; set; } } } diff --git a/src/Tgstation.Server.Host/Swarm/SwarmService.cs b/src/Tgstation.Server.Host/Swarm/SwarmService.cs index 7340d4b25df..0ff0131c723 100644 --- a/src/Tgstation.Server.Host/Swarm/SwarmService.cs +++ b/src/Tgstation.Server.Host/Swarm/SwarmService.cs @@ -1,5 +1,6 @@ using System; using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; using System.Linq; using System.Net; using System.Net.Http; @@ -49,6 +50,7 @@ public bool ExpectedNumberOfNodesConnected /// /// If the swarm system is enabled. /// + [MemberNotNullWhen(true, nameof(serverHealthCheckTask), nameof(forceHealthCheckTcs), nameof(serverHealthCheckCancellationTokenSource), nameof(swarmServers))] bool SwarmMode => swarmConfiguration.PrivateKey != null; /// @@ -99,17 +101,17 @@ public bool ExpectedNumberOfNodesConnected /// /// The for . /// - readonly CancellationTokenSource serverHealthCheckCancellationTokenSource; + readonly CancellationTokenSource? serverHealthCheckCancellationTokenSource; /// /// of connected s. /// - readonly List swarmServers; + readonly List? swarmServers; /// /// of s to registration s and when they were created. /// - readonly Dictionary registrationIdsAndTimes; + readonly Dictionary? registrationIdsAndTimes; /// /// If the current server is the swarm controller. @@ -119,17 +121,17 @@ public bool ExpectedNumberOfNodesConnected /// /// A that is currently in progress. /// - volatile SwarmUpdateOperation updateOperation; + volatile SwarmUpdateOperation? updateOperation; /// /// A that is used to force a health check. /// - volatile TaskCompletionSource forceHealthCheckTcs; + volatile TaskCompletionSource? forceHealthCheckTcs; /// /// The for the . /// - Task serverHealthCheckTask; + Task? serverHealthCheckTask; /// /// The registration provided by the swarm controller. @@ -183,17 +185,16 @@ public SwarmService( { if (swarmConfiguration.Address == null) throw new InvalidOperationException("Swarm configuration missing Address!"); + if (String.IsNullOrWhiteSpace(swarmConfiguration.Identifier)) throw new InvalidOperationException("Swarm configuration missing Identifier!"); - } - swarmController = !SwarmMode || swarmConfiguration.ControllerAddress == null; - if (SwarmMode) - { + swarmController = swarmConfiguration.ControllerAddress == null; + if (swarmController) + registrationIdsAndTimes = new(); + serverHealthCheckCancellationTokenSource = new CancellationTokenSource(); forceHealthCheckTcs = new TaskCompletionSource(); - if (swarmController) - registrationIdsAndTimes = new (); swarmServers = new List { @@ -206,6 +207,8 @@ public SwarmService( }, }; } + else + swarmController = true; } /// @@ -346,7 +349,7 @@ async ValueTask SendRemoteCommitUpdate(SwarmServerResponse swarmServer) } /// - public ICollection GetSwarmServers() + public ICollection? GetSwarmServers() { if (!SwarmMode) return null; @@ -424,7 +427,7 @@ public async ValueTask Shutdown(CancellationToken cancellationToken) { logger.LogTrace("Begin Shutdown"); - async ValueTask SendUnregistrationRequest(SwarmServerResponse swarmServer) + async ValueTask SendUnregistrationRequest(SwarmServerResponse? swarmServer) { using var httpClient = httpClientFactory.CreateClient(); using var request = PrepareSwarmRequest( @@ -443,13 +446,13 @@ async ValueTask SendUnregistrationRequest(SwarmServerResponse swarmServer) logger.LogWarning( ex, "Error unregistering {nodeType}!", - swarmController + swarmServer != null ? $"node {swarmServer.Identifier}" : "from controller"); } } - if (serverHealthCheckTask != null) + if (SwarmMode && serverHealthCheckTask != null) { serverHealthCheckCancellationTokenSource.Cancel(); await serverHealthCheckTask; @@ -489,7 +492,7 @@ await databaseContextFactory.UseContext( .Select(SendUnregistrationRequest) .ToList()); swarmServers.RemoveRange(1, swarmServers.Count - 1); - registrationIdsAndTimes.Clear(); + registrationIdsAndTimes!.Clear(); } await task; @@ -504,6 +507,9 @@ public void UpdateSwarmServersList(IEnumerable swarmServers { ArgumentNullException.ThrowIfNull(swarmServers); + if (!SwarmMode) + throw new InvalidOperationException("Swarm mode not enabled!"); + if (swarmController) throw new InvalidOperationException("Cannot UpdateSwarmServersList on swarm controller!"); @@ -518,9 +524,12 @@ public void UpdateSwarmServersList(IEnumerable swarmServers /// public bool ValidateRegistration(Guid registrationId) { + if (!SwarmMode) + throw new InvalidOperationException("Swarm mode not enabled!"); + if (swarmController) lock (swarmServers) - return registrationIdsAndTimes.Values.Any(x => x.Item1 == registrationId); + return registrationIdsAndTimes!.Values.Any(x => x.RegistrationId == registrationId); if (registrationId != controllerRegistration) return false; @@ -540,6 +549,9 @@ public async ValueTask RegisterNode(Api.Models.Internal.SwarmServer node, if (node.Address == null) throw new ArgumentException("Node missing Address!", nameof(node)); + if (!SwarmMode) + throw new InvalidOperationException("Swarm mode not enabled!"); + if (!swarmController) throw new InvalidOperationException("Cannot RegisterNode on swarm node!"); @@ -547,11 +559,12 @@ public async ValueTask RegisterNode(Api.Models.Internal.SwarmServer node, await AbortUpdate(); + var registrationIdsAndTimes = this.registrationIdsAndTimes!; lock (swarmServers) { - if (registrationIdsAndTimes.Any(x => x.Value.Item1 == registrationId)) + if (registrationIdsAndTimes.Any(x => x.Value.RegistrationId == registrationId)) { - var preExistingRegistrationKvp = registrationIdsAndTimes.FirstOrDefault(x => x.Value.Item1 == registrationId); + var preExistingRegistrationKvp = registrationIdsAndTimes.FirstOrDefault(x => x.Value.RegistrationId == registrationId); if (preExistingRegistrationKvp.Key == node.Identifier) { logger.LogWarning("Node {nodeId} has already registered!", node.Identifier); @@ -580,7 +593,7 @@ public async ValueTask RegisterNode(Api.Models.Internal.SwarmServer node, Identifier = node.Identifier, Controller = false, }); - registrationIdsAndTimes.Add(node.Identifier, (registrationId, DateTimeOffset.UtcNow)); + registrationIdsAndTimes.Add(node.Identifier, (RegistrationId: registrationId, DateTimeOffset.UtcNow)); } logger.LogInformation("Registered node {nodeId} ({nodeIP}) with ID {registrationId}", node.Identifier, node.Address, registrationId); @@ -589,7 +602,7 @@ public async ValueTask RegisterNode(Api.Models.Internal.SwarmServer node, } /// - public async ValueTask RemoteCommitRecieved(Guid registrationId, CancellationToken cancellationToken) + public async ValueTask RemoteCommitReceived(Guid registrationId, CancellationToken cancellationToken) { var localUpdateOperation = updateOperation; if (!swarmController) @@ -640,6 +653,9 @@ public async ValueTask RemoteCommitRecieved(Guid registrationId, Cancellat /// public async ValueTask UnregisterNode(Guid registrationId, CancellationToken cancellationToken) { + if (!SwarmMode) + throw new InvalidOperationException("Swarm mode not enabled!"); + logger.LogTrace("UnregisterNode {registrationId}", registrationId); await AbortUpdate(); @@ -661,7 +677,7 @@ public async ValueTask UnregisterNode(Guid registrationId, CancellationToken can lock (swarmServers) { swarmServers.RemoveAll(x => x.Identifier == nodeIdentifier); - registrationIdsAndTimes.Remove(nodeIdentifier); + registrationIdsAndTimes!.Remove(nodeIdentifier); } MarkServersDirty(); @@ -708,7 +724,7 @@ async ValueTask SendRemoteAbort(SwarmServerResponse swarmServer) Address = swarmConfiguration.ControllerAddress, }); - lock (swarmServers) + lock (swarmServers!) return ValueTaskExtensions.WhenAll( swarmServers .Where(x => !x.Controller) @@ -758,16 +774,16 @@ RequestFileStreamProvider CreateUpdateStreamProvider(SwarmServerResponse sourceN /// The . Must always have populated. If is , it must be fully populated. /// The for the operation. /// A resulting in the . - async ValueTask PrepareUpdateImpl(ISeekableFileStreamProvider initiatorProvider, SwarmUpdateRequest updateRequest, CancellationToken cancellationToken) + async ValueTask PrepareUpdateImpl(ISeekableFileStreamProvider? initiatorProvider, SwarmUpdateRequest updateRequest, CancellationToken cancellationToken) { + var version = updateRequest.UpdateVersion!; if (!SwarmMode) { // we still need an active update operation for the TargetVersion - updateOperation = new SwarmUpdateOperation(updateRequest.UpdateVersion); + updateOperation = new SwarmUpdateOperation(version); return SwarmPrepareResult.SuccessProviderNotRequired; } - var version = updateRequest.UpdateVersion; var initiator = initiatorProvider != null; logger.LogTrace("PrepareUpdateImpl {version}...", version); @@ -775,7 +791,7 @@ async ValueTask PrepareUpdateImpl(ISeekableFileStreamProvide SwarmUpdateOperation localUpdateOperation; try { - SwarmServerResponse sourceNode = null; + SwarmServerResponse? sourceNode = null; List currentNodes; lock (swarmServers) { @@ -812,7 +828,7 @@ async ValueTask PrepareUpdateImpl(ISeekableFileStreamProvide if (!swarmController && initiator) { - var downloadTickets = await CreateDownloadTickets(initiatorProvider, currentNodes, cancellationToken); + var downloadTickets = await CreateDownloadTickets(initiatorProvider!, currentNodes, cancellationToken); // condition of initiator logger.LogInformation("Forwarding update request to swarm controller..."); using var httpClient = httpClientFactory.CreateClient(); @@ -852,7 +868,13 @@ async ValueTask PrepareUpdateImpl(ISeekableFileStreamProvide return SwarmPrepareResult.Failure; } - if (!updateRequest.DownloadTickets.TryGetValue(swarmConfiguration.Identifier, out var ticket)) + if (updateRequest.DownloadTickets == null) + { + logger.LogError("Missing download tickets in update request!"); + return SwarmPrepareResult.Failure; + } + + if (!updateRequest.DownloadTickets.TryGetValue(swarmConfiguration.Identifier!, out var ticket)) { logger.Log( swarmController @@ -922,7 +944,7 @@ async ValueTask PrepareUpdateImpl(ISeekableFileStreamProvide /// The for the operation. /// A resulting in the . async ValueTask ControllerDistributedPrepareUpdate( - ISeekableFileStreamProvider initiatorProvider, + ISeekableFileStreamProvider? initiatorProvider, SwarmUpdateRequest updateRequest, SwarmUpdateOperation currentUpdateOperation, CancellationToken cancellationToken) @@ -954,7 +976,7 @@ async ValueTask ControllerDistributedPrepareUpdate( } // The initiator node obviously doesn't create a ticket for itself - else if (!weAreInitiator && updateRequest.DownloadTickets.Count != currentUpdateOperation.InvolvedServers.Count - 1) + else if (!weAreInitiator && updateRequest.DownloadTickets!.Count != currentUpdateOperation.InvolvedServers.Count - 1) { logger.LogWarning( "Aborting update, {receivedTickets} download tickets were provided but there are {nodesToUpdate} nodes in the swarm that require the package!", @@ -965,8 +987,8 @@ async ValueTask ControllerDistributedPrepareUpdate( } var downloadTicketDictionary = weAreInitiator - ? await CreateDownloadTickets(initiatorProvider, currentUpdateOperation.InvolvedServers, cancellationToken) - : updateRequest.DownloadTickets; + ? await CreateDownloadTickets(initiatorProvider!, currentUpdateOperation.InvolvedServers, cancellationToken) + : updateRequest.DownloadTickets!; var sourceNode = weAreInitiator ? swarmConfiguration.Identifier @@ -982,18 +1004,20 @@ async ValueTask ControllerDistributedPrepareUpdate( .Select(node => { // only send the necessary ticket to each node from the controller - Dictionary localTicketDictionary; - if (!downloadTicketDictionary.TryGetValue(node.Identifier, out var ticket) - && node.Identifier != sourceNode) + Dictionary? localTicketDictionary; + var nodeId = node.Identifier!; + if (nodeId == sourceNode) + localTicketDictionary = null; + else if (!downloadTicketDictionary.TryGetValue(nodeId, out var ticket)) { - logger.LogError("Missing download ticket for node {missingNodeId}!", node.Identifier); + logger.LogError("Missing download ticket for node {missingNodeId}!", nodeId); anyFailed = true; return null; } else localTicketDictionary = new Dictionary { - { node.Identifier, ticket }, + { nodeId, ticket }, }; var request = new SwarmUpdateRequest @@ -1013,7 +1037,7 @@ async ValueTask ControllerDistributedPrepareUpdate( var tasks = updateRequests .Select(async tuple => { - var node = tuple.Item1; + var node = tuple!.Item1; var body = tuple.Item2; using var request = PrepareSwarmRequest( @@ -1086,7 +1110,7 @@ async ValueTask> CreateDownloadTickets( var downloadTickets = new Dictionary(serversRequiringTickets.Count); foreach (var node in serversRequiringTickets) downloadTickets.Add( - node.Identifier, + node.Identifier!, transferService.CreateDownload(downloadProvider)); await streamRetrievalTask; @@ -1103,9 +1127,10 @@ async ValueTask HealthCheckNodes(CancellationToken cancellationToken) using var httpClient = httpClientFactory.CreateClient(); List currentSwarmServers; - lock (swarmServers) + lock (swarmServers!) currentSwarmServers = swarmServers.ToList(); + var registrationIdsAndTimes = this.registrationIdsAndTimes!; async ValueTask HealthRequestForServer(SwarmServerResponse swarmServer) { using var request = PrepareSwarmRequest( @@ -1131,15 +1156,15 @@ async ValueTask HealthRequestForServer(SwarmServerResponse swarmServer) lock (swarmServers) { swarmServers.Remove(swarmServer); - registrationIdsAndTimes.Remove(swarmServer.Identifier); + registrationIdsAndTimes.Remove(swarmServer.Identifier!); } } await ValueTaskExtensions.WhenAll( currentSwarmServers .Where(node => !node.Controller - && registrationIdsAndTimes.TryGetValue(node.Identifier, out var registrationAndTime) - && registrationAndTime.Item2.AddMinutes(SwarmConstants.ControllerHealthCheckIntervalMinutes) < DateTimeOffset.UtcNow) + && registrationIdsAndTimes.TryGetValue(node.Identifier!, out var registrationAndTime) + && registrationAndTime.RegisteredAt.AddMinutes(SwarmConstants.ControllerHealthCheckIntervalMinutes) < DateTimeOffset.UtcNow) .Select(HealthRequestForServer)); lock (swarmServers) @@ -1167,7 +1192,7 @@ void MarkServersDirty() bool TriggerHealthCheck() { var currentTcs = Interlocked.Exchange(ref forceHealthCheckTcs, new TaskCompletionSource()); - return currentTcs.TrySetResult(); + return currentTcs!.TrySetResult(); } /// @@ -1242,9 +1267,8 @@ async ValueTask RegisterWithController(CancellationToke null, HttpMethod.Post, SwarmConstants.RegisterRoute, - new SwarmRegistrationRequest + new SwarmRegistrationRequest(assemblyInformationProvider.Version) { - ServerVersion = assemblyInformationProvider.Version, Identifier = swarmConfiguration.Identifier, Address = swarmConfiguration.Address, PublicAddress = swarmConfiguration.PublicAddress, @@ -1297,7 +1321,7 @@ async ValueTask RegisterWithController(CancellationToke async ValueTask SendUpdatedServerListToNodes(CancellationToken cancellationToken) { List currentSwarmServers; - lock (swarmServers) + lock (swarmServers!) { serversDirty = false; currentSwarmServers = swarmServers.ToList(); @@ -1335,7 +1359,7 @@ async ValueTask UpdateRequestForServer(SwarmServerResponse swarmServer) lock (swarmServers) { swarmServers.Remove(swarmServer); - registrationIdsAndTimes.Remove(swarmServer.Identifier); + registrationIdsAndTimes!.Remove(swarmServer.Identifier!); } } } @@ -1350,17 +1374,17 @@ await ValueTaskExtensions.WhenAll( /// /// Prepares a for swarm communication. /// - /// The the message is for, if null will be sent to swarm controller. + /// The the message is for. Must have and set. If , will be sent to swarm controller. /// The . /// The route on to use. /// The body if any. /// An optional override to the . /// A new . HttpRequestMessage PrepareSwarmRequest( - SwarmServerResponse swarmServer, + SwarmServerResponse? swarmServer, HttpMethod httpMethod, string route, - object body, + object? body, Guid? registrationIdOverride = null) { swarmServer ??= new SwarmServerResponse @@ -1373,7 +1397,7 @@ HttpRequestMessage PrepareSwarmRequest( "{method} {route} to swarm server {nodeIdOrAddress}", httpMethod, fullRoute, - swarmServer.Identifier ?? swarmServer.Address.ToString()); + swarmServer.Identifier ?? swarmServer.Address!.ToString()); var request = new HttpRequestMessage( httpMethod, @@ -1388,9 +1412,9 @@ HttpRequestMessage PrepareSwarmRequest( request.Headers.Add(SwarmConstants.RegistrationIdHeader, registrationIdOverride.Value.ToString()); else if (swarmController) { - lock (swarmServers) - if (registrationIdsAndTimes.TryGetValue(swarmServer.Identifier, out var registrationIdAndTime)) - request.Headers.Add(SwarmConstants.RegistrationIdHeader, registrationIdAndTime.Item1.ToString()); + lock (swarmServers!) + if (registrationIdsAndTimes!.TryGetValue(swarmServer.Identifier!, out var registrationIdAndTime)) + request.Headers.Add(SwarmConstants.RegistrationIdHeader, registrationIdAndTime.RegistrationId.ToString()); } else if (controllerRegistration.HasValue) request.Headers.Add(SwarmConstants.RegistrationIdHeader, controllerRegistration.Value.ToString()); @@ -1420,7 +1444,7 @@ async Task HealthCheckLoop(CancellationToken cancellationToken) logger.LogTrace("Starting HealthCheckLoop..."); try { - var nextForceHealthCheckTask = forceHealthCheckTcs.Task; + var nextForceHealthCheckTask = forceHealthCheckTcs!.Task; while (!cancellationToken.IsCancellationRequested) { TimeSpan delay; @@ -1498,21 +1522,22 @@ async Task HealthCheckLoop(CancellationToken cancellationToken) /// /// The registration . /// The registered or if it does not exist. - string NodeIdentifierFromRegistration(Guid registrationId) + string? NodeIdentifierFromRegistration(Guid registrationId) { if (!swarmController) throw new InvalidOperationException("NodeIdentifierFromRegistration on node!"); - lock (swarmServers) + lock (swarmServers!) { - var exists = registrationIdsAndTimes.Any(x => x.Value.Item1 == registrationId); + var registrationIdsAndTimes = this.registrationIdsAndTimes!; + var exists = registrationIdsAndTimes.Any(x => x.Value.RegistrationId == registrationId); if (!exists) { logger.LogWarning("A node that was to be looked up ({registrationId}) disappeared from our records!", registrationId); return null; } - return registrationIdsAndTimes.First(x => x.Value.Item1 == registrationId).Key; + return registrationIdsAndTimes.First(x => x.Value.RegistrationId == registrationId).Key; } } } diff --git a/src/Tgstation.Server.Host/Swarm/SwarmUpdateOperation.cs b/src/Tgstation.Server.Host/Swarm/SwarmUpdateOperation.cs index 01b9f28d18f..67d65686ca8 100644 --- a/src/Tgstation.Server.Host/Swarm/SwarmUpdateOperation.cs +++ b/src/Tgstation.Server.Host/Swarm/SwarmUpdateOperation.cs @@ -31,7 +31,7 @@ public class SwarmUpdateOperation /// /// Backing field for . /// - readonly IReadOnlyList initialInvolvedServers; + readonly IReadOnlyList? initialInvolvedServers; /// /// The backing for . @@ -41,7 +41,7 @@ public class SwarmUpdateOperation /// /// of that need to send a ready-commit to the controller before the commit can happen. /// - readonly HashSet nodesThatNeedToBeReadyToCommit; + readonly HashSet? nodesThatNeedToBeReadyToCommit; /// /// Initializes a new instance of the class. @@ -58,7 +58,7 @@ public SwarmUpdateOperation(Version targetVersion) /// Initializes a new instance of the class. /// /// The value of . - /// An of the controller's current nodes as s. + /// An of the controller's current nodes as s. Must have and set. /// This is the variant for use by the controller. public SwarmUpdateOperation(Version targetVersion, IEnumerable currentNodes) : this(targetVersion) @@ -66,7 +66,7 @@ public SwarmUpdateOperation(Version targetVersion, IEnumerable !node.Controller) - .Select(node => node.Identifier) + .Select(node => node.Identifier!) .ToHashSet(); } diff --git a/src/Tgstation.Server.Host/Swarm/SwarmUpdateRequest.cs b/src/Tgstation.Server.Host/Swarm/SwarmUpdateRequest.cs index 1da49cea3c3..958a8f5afb8 100644 --- a/src/Tgstation.Server.Host/Swarm/SwarmUpdateRequest.cs +++ b/src/Tgstation.Server.Host/Swarm/SwarmUpdateRequest.cs @@ -15,17 +15,17 @@ public sealed class SwarmUpdateRequest /// The TGS to update to. /// [Required] - public Version UpdateVersion { get; init; } + public Version? UpdateVersion { get; init; } /// /// The of the node to download the update package from. /// [Required] - public string SourceNode { get; init; } + public string? SourceNode { get; init; } /// /// The map of s to s for retrieving the update package from the initiating server. /// - public Dictionary DownloadTickets { get; init; } + public Dictionary? DownloadTickets { get; init; } } } diff --git a/src/Tgstation.Server.Host/System/AssemblyInformationProvider.cs b/src/Tgstation.Server.Host/System/AssemblyInformationProvider.cs index 34d2a9a9402..475eb498626 100644 --- a/src/Tgstation.Server.Host/System/AssemblyInformationProvider.cs +++ b/src/Tgstation.Server.Host/System/AssemblyInformationProvider.cs @@ -26,7 +26,7 @@ sealed class AssemblyInformationProvider : IAssemblyInformationProvider public string VersionString { get; } /// - public ProductInfoHeaderValue ProductInfoHeaderValue => new ( + public ProductInfoHeaderValue ProductInfoHeaderValue => new( VersionPrefix, Version.ToString()); @@ -38,7 +38,7 @@ public AssemblyInformationProvider() Assembly assembly = Assembly.GetExecutingAssembly(); Path = assembly.Location; AssemblyName = assembly.GetName(); - Version = AssemblyName.Version.Semver(); + Version = AssemblyName.Version!.Semver(); VersionString = String.Concat(VersionPrefix, "-v", Version); } } diff --git a/src/Tgstation.Server.Host/System/IProcess.cs b/src/Tgstation.Server.Host/System/IProcess.cs index dc9a1d5eac5..b0783cbd7e0 100644 --- a/src/Tgstation.Server.Host/System/IProcess.cs +++ b/src/Tgstation.Server.Host/System/IProcess.cs @@ -7,7 +7,7 @@ namespace Tgstation.Server.Host.System /// /// Abstraction over a . /// - interface IProcess : IProcessBase, IAsyncDisposable + public interface IProcess : IProcessBase, IAsyncDisposable { /// /// The ' ID. @@ -28,7 +28,7 @@ interface IProcess : IProcessBase, IAsyncDisposable /// To guarantee that all data is received from the when redirecting streams to a file /// the result of this function must be ed before is called. /// - Task GetCombinedOutput(CancellationToken cancellationToken); + Task GetCombinedOutput(CancellationToken cancellationToken); /// /// Asycnhronously terminates the process. diff --git a/src/Tgstation.Server.Host/System/IProcessBase.cs b/src/Tgstation.Server.Host/System/IProcessBase.cs index 8fc0bd4484e..d7a20f43b7e 100644 --- a/src/Tgstation.Server.Host/System/IProcessBase.cs +++ b/src/Tgstation.Server.Host/System/IProcessBase.cs @@ -6,7 +6,7 @@ namespace Tgstation.Server.Host.System /// /// Represents process lifetime. /// - interface IProcessBase + public interface IProcessBase { /// /// The resulting in the exit code of the process or if the process was detached. @@ -22,12 +22,12 @@ interface IProcessBase /// /// Suspends the process. /// - void Suspend(); + void SuspendProcess(); /// /// Resumes the process. /// - void Resume(); + void ResumeProcess(); /// /// Create a dump file of the process. diff --git a/src/Tgstation.Server.Host/System/IProcessExecutor.cs b/src/Tgstation.Server.Host/System/IProcessExecutor.cs index 81593dbe37d..aae807eec25 100644 --- a/src/Tgstation.Server.Host/System/IProcessExecutor.cs +++ b/src/Tgstation.Server.Host/System/IProcessExecutor.cs @@ -18,8 +18,8 @@ interface IProcessExecutor IProcess LaunchProcess( string fileName, string workingDirectory, - string arguments = null, - string fileRedirect = null, + string arguments, + string? fileRedirect = null, bool readStandardHandles = false, bool noShellExecute = false); @@ -34,13 +34,13 @@ IProcess LaunchProcess( /// /// The . /// The represented by on success, on failure. - IProcess GetProcess(int id); + IProcess? GetProcess(int id); /// /// Get a with a given . /// /// The name of the process executable without the extension. /// The represented by on success, on failure. - IProcess GetProcessByName(string name); + IProcess? GetProcessByName(string name); } } diff --git a/src/Tgstation.Server.Host/System/NativeMethods.cs b/src/Tgstation.Server.Host/System/NativeMethods.cs index dc5046d1335..ffba29cc9c2 100644 --- a/src/Tgstation.Server.Host/System/NativeMethods.cs +++ b/src/Tgstation.Server.Host/System/NativeMethods.cs @@ -54,7 +54,7 @@ public enum MiniDumpType : uint /// See https://docs.microsoft.com/en-us/windows/desktop/api/winuser/nf-winuser-findwindoww. /// [DllImport("user32.dll", CharSet = CharSet.Unicode)] - public static extern IntPtr FindWindow(string lpClassName, string lpWindowName); + public static extern IntPtr FindWindow(string? lpClassName, string lpWindowName); /// /// See https://docs.microsoft.com/en-us/windows/desktop/api/winuser/nf-winuser-sendmessage. @@ -83,7 +83,7 @@ public enum MiniDumpType : uint /// See https://msdn.microsoft.com/en-us/library/windows/desktop/aa378184(v=vs.85).aspx. /// [DllImport("advapi32.dll", SetLastError = true, CharSet = CharSet.Unicode)] - public static extern bool LogonUser(string lpszUsername, string lpszDomain, string lpszPassword, int dwLogonType, int dwLogonProvider, out IntPtr phToken); + public static extern bool LogonUser(string lpszUsername, string? lpszDomain, string lpszPassword, int dwLogonType, int dwLogonProvider, out IntPtr phToken); /// /// See https://docs.microsoft.com/en-us/windows/desktop/api/winbase/nf-winbase-createsymboliclinkw. diff --git a/src/Tgstation.Server.Host/System/PosixProcessFeatures.cs b/src/Tgstation.Server.Host/System/PosixProcessFeatures.cs index 0e1dab1d3cf..8077577aafa 100644 --- a/src/Tgstation.Server.Host/System/PosixProcessFeatures.cs +++ b/src/Tgstation.Server.Host/System/PosixProcessFeatures.cs @@ -77,16 +77,16 @@ public async ValueTask CreateDump(global::System.Diagnostics.Process process, st try { if (process.HasExited) - throw new JobException(ErrorCode.DreamDaemonOffline); + throw new JobException(ErrorCode.GameServerOffline); pid = process.Id; } catch (InvalidOperationException ex) { - throw new JobException(ErrorCode.DreamDaemonOffline, ex); + throw new JobException(ErrorCode.GameServerOffline, ex); } - string output; + string? output; int exitCode; await using (var gcoreProc = lazyLoadedProcessExecutor.Value.LaunchProcess( GCorePath, diff --git a/src/Tgstation.Server.Host/System/PosixSignalHandler.cs b/src/Tgstation.Server.Host/System/PosixSignalHandler.cs index 693324d679b..eff4f6c7c20 100644 --- a/src/Tgstation.Server.Host/System/PosixSignalHandler.cs +++ b/src/Tgstation.Server.Host/System/PosixSignalHandler.cs @@ -45,7 +45,7 @@ sealed class PosixSignalHandler : IHostedService, IDisposable /// /// The thread used to check the signal. See http://docs.go-mono.com/?link=T%3aMono.Unix.UnixSignal. /// - Task signalCheckerTask; + Task? signalCheckerTask; /// /// Initializes a new instance of the class. diff --git a/src/Tgstation.Server.Host/System/Process.cs b/src/Tgstation.Server.Host/System/Process.cs index 0bc76ce03f8..43620832474 100644 --- a/src/Tgstation.Server.Host/System/Process.cs +++ b/src/Tgstation.Server.Host/System/Process.cs @@ -51,7 +51,12 @@ sealed class Process : IProcess /// /// The resulting in the process' standard output/error text. /// - readonly Task readTask; + readonly Task? readTask; + + /// + /// If the was disposed. + /// + volatile int disposed; /// /// Initializes a new instance of the class. @@ -65,8 +70,8 @@ sealed class Process : IProcess public Process( IProcessFeatures processFeatures, global::System.Diagnostics.Process handle, - CancellationTokenSource readerCts, - Task readTask, + CancellationTokenSource? readerCts, + Task? readTask, ILogger logger, bool preExisting) { @@ -114,6 +119,9 @@ public Process( /// public async ValueTask DisposeAsync() { + if (Interlocked.Exchange(ref disposed, 1) != 0) + return; + logger.LogTrace("Disposing PID {pid}...", Id); cancellationTokenSource.Cancel(); cancellationTokenSource.Dispose(); @@ -127,7 +135,7 @@ public async ValueTask DisposeAsync() } /// - public Task GetCombinedOutput(CancellationToken cancellationToken) + public Task GetCombinedOutput(CancellationToken cancellationToken) { if (readTask == null) throw new InvalidOperationException("Output/Error stream reading was not enabled!"); @@ -138,6 +146,7 @@ public Task GetCombinedOutput(CancellationToken cancellationToken) /// public void Terminate() { + CheckDisposed(); if (handle.HasExited) { logger.LogTrace("PID {pid} already exited", Id); @@ -160,6 +169,7 @@ public void Terminate() /// public void AdjustPriority(bool higher) { + CheckDisposed(); var targetPriority = higher ? ProcessPriorityClass.AboveNormal : ProcessPriorityClass.BelowNormal; try { @@ -173,8 +183,9 @@ public void AdjustPriority(bool higher) } /// - public void Suspend() + public void SuspendProcess() { + CheckDisposed(); try { processFeatures.SuspendProcess(handle); @@ -188,8 +199,9 @@ public void Suspend() } /// - public void Resume() + public void ResumeProcess() { + CheckDisposed(); try { processFeatures.ResumeProcess(handle); @@ -205,6 +217,7 @@ public void Resume() /// public string GetExecutingUsername() { + CheckDisposed(); var result = processFeatures.GetExecutingUsername(handle); logger.LogTrace("PID {pid} Username: {username}", Id, result); return result; @@ -214,6 +227,7 @@ public string GetExecutingUsername() public ValueTask CreateDump(string outputFile, CancellationToken cancellationToken) { ArgumentNullException.ThrowIfNull(outputFile); + CheckDisposed(); logger.LogTrace("Dumping PID {pid} to {dumpFilePath}...", Id, outputFile); return processFeatures.CreateDump(handle, outputFile, cancellationToken); @@ -225,18 +239,29 @@ public ValueTask CreateDump(string outputFile, CancellationToken cancellationTok /// A resulting in the or if the process was detached. async Task WrapLifetimeTask() { + bool hasExited; try { await handle.WaitForExitAsync(cancellationTokenSource.Token); - var exitCode = handle.ExitCode; - logger.LogTrace("PID {pid} exited with code {exitCode}", Id, exitCode); - return exitCode; + hasExited = true; } catch (OperationCanceledException ex) { logger.LogTrace(ex, "Process lifetime task cancelled!"); - return null; + hasExited = handle.HasExited; } + + if (!hasExited) + return null; + + var exitCode = handle.ExitCode; + logger.LogTrace("PID {pid} exited with code {exitCode}", Id, exitCode); + return exitCode; } + + /// + /// Throws an if a method of the was called after . + /// + void CheckDisposed() => ObjectDisposedException.ThrowIf(disposed != 0, this); } } diff --git a/src/Tgstation.Server.Host/System/ProcessExecutor.cs b/src/Tgstation.Server.Host/System/ProcessExecutor.cs index 3eb83ba522a..7820ca11b39 100644 --- a/src/Tgstation.Server.Host/System/ProcessExecutor.cs +++ b/src/Tgstation.Server.Host/System/ProcessExecutor.cs @@ -1,7 +1,9 @@ using System; +using System.Diagnostics; using System.IO; using System.Text; using System.Threading; +using System.Threading.Channels; using System.Threading.Tasks; using Microsoft.Extensions.Logging; @@ -13,6 +15,11 @@ namespace Tgstation.Server.Host.System /// sealed class ProcessExecutor : IProcessExecutor { + /// + /// for . + /// + static readonly ReaderWriterLockSlim ExclusiveProcessLaunchLock = new(); + /// /// The for the . /// @@ -33,6 +40,23 @@ sealed class ProcessExecutor : IProcessExecutor /// readonly ILoggerFactory loggerFactory; + /// + /// Runs a given making sure to not launch any processes while its running. + /// + /// The to execute. + public static void WithProcessLaunchExclusivity(Action action) + { + ExclusiveProcessLaunchLock.EnterWriteLock(); + try + { + action(); + } + finally + { + ExclusiveProcessLaunchLock.ExitWriteLock(); + } + } + /// /// Initializes a new instance of the class. /// @@ -53,7 +77,7 @@ public ProcessExecutor( } /// - public IProcess GetProcess(int id) + public IProcess? GetProcess(int id) { logger.LogDebug("Attaching to process {pid}...", id); global::System.Diagnostics.Process handle; @@ -83,7 +107,7 @@ public IProcess LaunchProcess( string fileName, string workingDirectory, string arguments, - string fileRedirect, + string? fileRedirect, bool readStandardHandles, bool noShellExecute) { @@ -91,16 +115,19 @@ public IProcess LaunchProcess( ArgumentNullException.ThrowIfNull(workingDirectory); ArgumentNullException.ThrowIfNull(arguments); - if (!noShellExecute && readStandardHandles) - throw new InvalidOperationException("Requesting output/error reading requires noShellExecute to be true!"); - - logger.LogDebug( - noShellExecute - ? "Launching process in {workingDirectory}: {exe} {arguments}" - : "Shell launching process in {workingDirectory}: {exe} {arguments}", + if (noShellExecute) + logger.LogDebug( + "Launching process in {workingDirectory}: {exe} {arguments}", + workingDirectory, + fileName, + arguments); + else + logger.LogDebug( + "Shell launching process in {workingDirectory}: {exe} {arguments}", workingDirectory, fileName, arguments); + var handle = new global::System.Diagnostics.Process(); try { @@ -110,26 +137,33 @@ public IProcess LaunchProcess( handle.StartInfo.UseShellExecute = !noShellExecute; - Task readTask = null; - CancellationTokenSource disposeCts = null; + Task? readTask = null; + CancellationTokenSource? disposeCts = null; try { - TaskCompletionSource processStartTcs = null; + TaskCompletionSource? processStartTcs = null; if (readStandardHandles) { - processStartTcs = new TaskCompletionSource(); - handle.StartInfo.RedirectStandardOutput = true; - handle.StartInfo.RedirectStandardError = true; - + processStartTcs = new TaskCompletionSource(); disposeCts = new CancellationTokenSource(); readTask = ConsumeReaders(handle, processStartTcs.Task, fileRedirect, disposeCts.Token); } + int pid; try { - handle.Start(); - - processStartTcs?.SetResult(); + ExclusiveProcessLaunchLock.EnterReadLock(); + try + { + handle.Start(); + } + finally + { + ExclusiveProcessLaunchLock.ExitReadLock(); + } + + pid = handle.Id; + processStartTcs?.SetResult(pid); } catch (Exception ex) { @@ -161,11 +195,11 @@ public IProcess LaunchProcess( } /// - public IProcess GetProcessByName(string name) + public IProcess? GetProcessByName(string name) { logger.LogTrace("GetProcessByName: {processName}...", name ?? throw new ArgumentNullException(nameof(name))); var procs = global::System.Diagnostics.Process.GetProcessesByName(name); - global::System.Diagnostics.Process handle = null; + global::System.Diagnostics.Process? handle = null; foreach (var proc in procs) if (handle == null) handle = proc; @@ -185,79 +219,102 @@ public IProcess GetProcessByName(string name) /// Consume the stdout/stderr streams into a . /// /// The . - /// The that completes when starts. + /// The resulting in the of the started process. /// The optional path to redirect the streams to. - /// The that triggers when the is disposed. + /// The for the operation. /// A resulting in the program's output/error text if is , otherwise. - async Task ConsumeReaders(global::System.Diagnostics.Process handle, Task startTask, string fileRedirect, CancellationToken disposeToken) + async Task ConsumeReaders(global::System.Diagnostics.Process handle, Task startupAndPid, string? fileRedirect, CancellationToken cancellationToken) { - await startTask; + handle.StartInfo.RedirectStandardOutput = true; + handle.StartInfo.RedirectStandardError = true; - var pid = handle.Id; - logger.LogTrace("Starting read for PID {pid}...", pid); + bool writingToFile; + await using var fileStream = (writingToFile = fileRedirect != null) ? ioManager.CreateAsyncSequentialWriteStream(fileRedirect!) : null; + await using var fileWriter = fileStream != null ? new StreamWriter(fileStream) : null; + + var stringBuilder = fileStream == null ? new StringBuilder() : null; - // once we obtain these handles we're responsible for them - using var stdOutHandle = handle.StandardOutput; - using var stdErrHandle = handle.StandardError; - Task outputReadTask = null, errorReadTask = null; - bool outputOpen = true, errorOpen = true; - async Task GetNextLine() + var dataChannel = Channel.CreateUnbounded( + new UnboundedChannelOptions + { + AllowSynchronousContinuations = !writingToFile, + SingleReader = true, + SingleWriter = false, + }); + + var handlesOpen = 2; + async void DataReceivedHandler(object sender, DataReceivedEventArgs eventArgs) { -#if NET7_0_OR_GREATER -#error ReadLineAsync supports cancellation now -#endif - if (outputOpen && outputReadTask == null) - outputReadTask = stdOutHandle.ReadLineAsync(); - - if (errorOpen && errorReadTask == null) - errorReadTask = stdErrHandle.ReadLineAsync(); - - var completedTask = await Task.WhenAny(outputReadTask ?? errorReadTask, errorReadTask ?? outputReadTask).WaitAsync(disposeToken); - var line = await completedTask; - if (completedTask == outputReadTask) + var line = eventArgs.Data; + if (line == null) { - outputReadTask = null; - if (line == null) - outputOpen = false; + var handlesRemaining = Interlocked.Decrement(ref handlesOpen); + if (handlesRemaining == 0) + dataChannel.Writer.Complete(); + + return; } - else + + try { - errorReadTask = null; - if (line == null) - errorOpen = false; + await dataChannel.Writer.WriteAsync(line, cancellationToken); + } + catch (OperationCanceledException ex) + { + logger.LogWarning(ex, "Handle channel write interrupted!"); } - - if (line == null && (errorOpen || outputOpen)) - return await GetNextLine(); - - return line; } - await using var fileStream = fileRedirect != null ? ioManager.CreateAsyncSequentialWriteStream(fileRedirect) : null; - await using var writer = fileStream != null ? new StreamWriter(fileStream) : null; + handle.OutputDataReceived += DataReceivedHandler; + handle.ErrorDataReceived += DataReceivedHandler; - string text; - var stringBuilder = fileStream == null ? new StringBuilder() : null; - try + async ValueTask OutputWriter() { - while ((text = await GetNextLine()) != null) + var enumerable = dataChannel.Reader.ReadAllAsync(cancellationToken); + if (writingToFile) { - if (fileStream != null) + var enumerator = enumerable.GetAsyncEnumerator(cancellationToken); + var nextEnumeration = enumerator.MoveNextAsync(); + while (await nextEnumeration) { - await writer.WriteLineAsync(text); - await writer.FlushAsync(); + var text = enumerator.Current; + nextEnumeration = enumerator.MoveNextAsync(); + await fileWriter!.WriteLineAsync(text.AsMemory(), cancellationToken); + + if (!nextEnumeration.IsCompleted) + await fileWriter.FlushAsync(cancellationToken); } - else - stringBuilder.AppendLine(text); } - - logger.LogTrace("Finished read for PID {pid}", pid); + else + await foreach (var text in enumerable) + stringBuilder!.AppendLine(text); } - catch (OperationCanceledException ex) + + var pid = await startupAndPid; + logger.LogTrace("Starting read for PID {pid}...", pid); + + using (cancellationToken.Register(() => dataChannel.Writer.TryComplete())) { - logger.LogWarning(ex, "PID {pid} stream reading interrupted!", pid); - if (fileStream != null) - await writer.WriteLineAsync("-- Process detached, log truncated. This is likely due a to TGS restart --"); + handle.BeginOutputReadLine(); + using (cancellationToken.Register(handle.CancelOutputRead)) + { + handle.BeginErrorReadLine(); + using (cancellationToken.Register(handle.CancelErrorRead)) + { + try + { + await OutputWriter(); + + logger.LogTrace("Finished read for PID {pid}", pid); + } + catch (OperationCanceledException ex) + { + logger.LogWarning(ex, "PID {pid} stream reading interrupted!", pid); + if (writingToFile) + await fileWriter!.WriteLineAsync("-- Process detached, log truncated. This is likely due a to TGS restart --"); + } + } + } } return stringBuilder?.ToString(); @@ -268,7 +325,7 @@ async Task GetNextLine() /// /// The to create a from. /// The based on . - IProcess CreateFromExistingHandle(global::System.Diagnostics.Process handle) + Process CreateFromExistingHandle(global::System.Diagnostics.Process handle) { try { diff --git a/src/Tgstation.Server.Host/System/ProgramShutdownTokenSource.cs b/src/Tgstation.Server.Host/System/ProgramShutdownTokenSource.cs index fb68be4ac95..0b9a77f7a6c 100644 --- a/src/Tgstation.Server.Host/System/ProgramShutdownTokenSource.cs +++ b/src/Tgstation.Server.Host/System/ProgramShutdownTokenSource.cs @@ -16,7 +16,7 @@ sealed class ProgramShutdownTokenSource : IDisposable /// /// The for the . /// - CancellationTokenSource cancellationTokenSource; + CancellationTokenSource? cancellationTokenSource; /// /// Gets the . diff --git a/src/Tgstation.Server.Host/System/SystemDManager.cs b/src/Tgstation.Server.Host/System/SystemDManager.cs index 30fc34416b4..7b0542fcad1 100644 --- a/src/Tgstation.Server.Host/System/SystemDManager.cs +++ b/src/Tgstation.Server.Host/System/SystemDManager.cs @@ -53,7 +53,7 @@ sealed class SystemDManager : BackgroundService, IRestartHandler, IDisposable /// /// A representing the clock time in nanoseconds. /// See https://linux.die.net/man/3/clock_gettime. - static long GetMonotonicUsec() => global::System.Diagnostics.Stopwatch.GetTimestamp(); // HACK: https://github.com/dotnet/runtime/blob/v6.0.19/src/libraries/Native/Unix/System.Native/pal_time.c#L51 clock_gettime_nsec_np is an OSX only thing apparently... + static long GetMonotonicUsec() => global::System.Diagnostics.Stopwatch.GetTimestamp(); // HACK: https://github.com/dotnet/runtime/blob/v8.0.0-preview.6.23329.7/src/native/libs/System.Native/pal_time.c#L84 clock_gettime_nsec_np is an OSX only thing apparently... /// /// Initializes a new instance of the class. @@ -86,7 +86,7 @@ public override void Dispose() } /// - public ValueTask HandleRestart(Version updateVersion, bool handlerMayDelayShutdownWithExtremelyLongRunningTasks, CancellationToken cancellationToken) + public ValueTask HandleRestart(Version? updateVersion, bool handlerMayDelayShutdownWithExtremelyLongRunningTasks, CancellationToken cancellationToken) { // If this is set, we know a gracefule SHUTDOWN was requested restartInProgress = !handlerMayDelayShutdownWithExtremelyLongRunningTasks; diff --git a/src/Tgstation.Server.Host/System/WindowsFirewallHelper.cs b/src/Tgstation.Server.Host/System/WindowsFirewallHelper.cs new file mode 100644 index 00000000000..fff15210927 --- /dev/null +++ b/src/Tgstation.Server.Host/System/WindowsFirewallHelper.cs @@ -0,0 +1,57 @@ +using System; +using System.Threading; +using System.Threading.Tasks; + +using Microsoft.Extensions.Logging; + +namespace Tgstation.Server.Host.System +{ + /// + /// Helper for interacting with the Windows Firewall. + /// + static class WindowsFirewallHelper + { + /// + /// Add an executable exception to the Windows firewall. + /// + /// The to use. + /// The to write to. + /// The name of the rule in Windows Firewall. + /// The path to the .exe to add a firewall exception for. + /// If the "netsh.exe" process should be run with lower process priority. + /// The for the operation. + /// A resulting in the exit code of the call to netsh.exe. + public static async ValueTask AddFirewallException( + IProcessExecutor processExecutor, + ILogger logger, + string exceptionName, + string exePath, + bool lowPriority, + CancellationToken cancellationToken) + { + logger.LogInformation("Adding Windows Firewall exception for {path}...", exePath); + var arguments = $"advfirewall firewall add rule name=\"{exceptionName}\" program=\"{exePath}\" protocol=tcp dir=in enable=yes action=allow"; + await using var netshProcess = processExecutor.LaunchProcess( + "netsh.exe", + Environment.CurrentDirectory, + arguments, + readStandardHandles: true, + noShellExecute: true); + + if (lowPriority) + netshProcess.AdjustPriority(false); + + int exitCode; + using (cancellationToken.Register(() => netshProcess.Terminate())) + exitCode = (await netshProcess.Lifetime).Value; + cancellationToken.ThrowIfCancellationRequested(); + + logger.LogDebug( + "netsh.exe output:{newLine}{output}", + Environment.NewLine, + await netshProcess.GetCombinedOutput(cancellationToken)); + + return exitCode; + } + } +} diff --git a/src/Tgstation.Server.Host/System/WindowsNetworkPromptReaper.cs b/src/Tgstation.Server.Host/System/WindowsNetworkPromptReaper.cs index c30f107ac36..48924155719 100644 --- a/src/Tgstation.Server.Host/System/WindowsNetworkPromptReaper.cs +++ b/src/Tgstation.Server.Host/System/WindowsNetworkPromptReaper.cs @@ -74,7 +74,7 @@ static List GetAllChildHandles(IntPtr mainWindow) try { var pointerChildHandlesList = GCHandle.ToIntPtr(gcChildhandlesList); - NativeMethods.EnumWindowProc childProc = new (EnumWindow); + NativeMethods.EnumWindowProc childProc = new(EnumWindow); NativeMethods.EnumChildWindows(mainWindow, childProc, pointerChildHandlesList); } finally @@ -113,11 +113,12 @@ public void RegisterProcess(IProcess process) process.Lifetime.ContinueWith( x => - { - logger.LogTrace("Unregistering process {0}...", process.Id); - lock (registeredProcesses) - registeredProcesses.Remove(process); - }, TaskScheduler.Current); + { + logger.LogTrace("Unregistering process {pid}...", process.Id); + lock (registeredProcesses) + registeredProcesses.Remove(process); + }, + TaskScheduler.Current); } /// diff --git a/src/Tgstation.Server.Host/System/WindowsProcessFeatures.cs b/src/Tgstation.Server.Host/System/WindowsProcessFeatures.cs index efbb029e0af..41789aec8cc 100644 --- a/src/Tgstation.Server.Host/System/WindowsProcessFeatures.cs +++ b/src/Tgstation.Server.Host/System/WindowsProcessFeatures.cs @@ -125,11 +125,11 @@ public async ValueTask CreateDump(global::System.Diagnostics.Process process, st try { if (process.HasExited) - throw new JobException(ErrorCode.DreamDaemonOffline); + throw new JobException(ErrorCode.GameServerOffline); } catch (InvalidOperationException ex) { - throw new JobException(ErrorCode.DreamDaemonOffline, ex); + throw new JobException(ErrorCode.GameServerOffline, ex); } await using var fileStream = new FileStream(outputFile, FileMode.CreateNew); diff --git a/src/Tgstation.Server.Host/Tgstation.Server.Host.csproj b/src/Tgstation.Server.Host/Tgstation.Server.Host.csproj index bcca6637c33..733400da3de 100644 --- a/src/Tgstation.Server.Host/Tgstation.Server.Host.csproj +++ b/src/Tgstation.Server.Host/Tgstation.Server.Host.csproj @@ -6,9 +6,10 @@ $(TgsCoreVersion) true false - API1000 + API1000;ASP0019 ClientApp/node_modules ClientApp/node_modules/.install-stamp + enable Linux ..\.. ../../build/uac_elevation_manifest.xml @@ -63,7 +64,7 @@ - + @@ -73,41 +74,41 @@ - + - + - + - + - + - + runtime; build; native; contentfiles; analyzers; buildtransitive - + - + - + - + - + - + - + @@ -117,13 +118,15 @@ - + - + + + - + diff --git a/src/Tgstation.Server.Host/Transfer/FileDownloadProvider.cs b/src/Tgstation.Server.Host/Transfer/FileDownloadProvider.cs index 603e099c076..1b0428665a5 100644 --- a/src/Tgstation.Server.Host/Transfer/FileDownloadProvider.cs +++ b/src/Tgstation.Server.Host/Transfer/FileDownloadProvider.cs @@ -20,7 +20,7 @@ public sealed class FileDownloadProvider /// /// A to specially provide a returning the of the file download. The caller will own the resulting . /// - public Func> StreamProvider { get; } + public Func>? StreamProvider { get; } /// /// The full path to the file on disk to download. @@ -41,7 +41,7 @@ public sealed class FileDownloadProvider /// The value of . public FileDownloadProvider( Func activationCallback, - Func> streamProvider, + Func>? streamProvider, string filePath, bool shareWrite) { diff --git a/src/Tgstation.Server.Host/Transfer/FileTransferService.cs b/src/Tgstation.Server.Host/Transfer/FileTransferService.cs index 263bd2ba9c3..bb1cafa171a 100644 --- a/src/Tgstation.Server.Host/Transfer/FileTransferService.cs +++ b/src/Tgstation.Server.Host/Transfer/FileTransferService.cs @@ -69,6 +69,11 @@ sealed class FileTransferService : IFileTransferTicketProvider, IFileTransferStr /// Task expireTask; + /// + /// If the is disposed. + /// + bool disposed; + /// /// Initializes a new instance of the class. /// @@ -101,12 +106,13 @@ public async ValueTask DisposeAsync() { Task toAwait; lock (synchronizationLock) - if (expireTask != null) + if (!disposed) { disposeCts.Cancel(); disposeCts.Dispose(); + disposed = true; toAwait = expireTask; - expireTask = null; + expireTask = Task.CompletedTask; } else toAwait = Task.CompletedTask; @@ -118,72 +124,86 @@ public async ValueTask DisposeAsync() public FileTicketResponse CreateDownload(FileDownloadProvider downloadProvider) { ArgumentNullException.ThrowIfNull(downloadProvider); + ObjectDisposedException.ThrowIf(disposed, this); logger.LogDebug("Creating download ticket for path {filePath}", downloadProvider.FilePath); - var ticketResult = CreateTicket(); + var ticket = cryptographySuite.GetSecureString(); lock (downloadTickets) - downloadTickets.Add(ticketResult.FileTicket, downloadProvider); + downloadTickets.Add(ticket, downloadProvider); QueueExpiry(() => { lock (downloadTickets) - if (downloadTickets.Remove(ticketResult.FileTicket)) - logger.LogTrace("Expired download ticket {ticket}...", ticketResult.FileTicket); + if (downloadTickets.Remove(ticket)) + logger.LogTrace("Expired download ticket {ticket}...", ticket); }); - logger.LogTrace("Created download ticket {ticket}", ticketResult.FileTicket); + logger.LogTrace("Created download ticket {ticket}", ticket); - return ticketResult; + return new FileTicketResponse + { + FileTicket = ticket, + }; } /// public IFileUploadTicket CreateUpload(FileUploadStreamKind streamKind) { + ObjectDisposedException.ThrowIf(disposed, this); + logger.LogDebug("Creating upload ticket..."); - var uploadTicket = new FileUploadProvider(CreateTicket(), streamKind); + var ticket = cryptographySuite.GetSecureString(); + var uploadTicket = new FileUploadProvider( + new FileTicketResponse + { + FileTicket = ticket, + }, + streamKind); lock (uploadTickets) - uploadTickets.Add(uploadTicket.Ticket.FileTicket, uploadTicket); + uploadTickets.Add(ticket, uploadTicket); QueueExpiry(() => { lock (uploadTickets) - if (uploadTickets.Remove(uploadTicket.Ticket.FileTicket)) - logger.LogTrace("Expired upload ticket {ticket}...", uploadTicket.Ticket.FileTicket); + if (uploadTickets.Remove(ticket)) + logger.LogTrace("Expired upload ticket {ticket}...", ticket); else return; uploadTicket.Expire(); }); - logger.LogTrace("Created upload ticket {ticket}", uploadTicket.Ticket.FileTicket); + logger.LogTrace("Created upload ticket {ticket}", ticket); return uploadTicket; } /// - public async ValueTask> RetrieveDownloadStream(FileTicketResponse ticket, CancellationToken cancellationToken) + public async ValueTask> RetrieveDownloadStream(FileTicketResponse ticketResponse, CancellationToken cancellationToken) { - ArgumentNullException.ThrowIfNull(ticket); + ArgumentNullException.ThrowIfNull(ticketResponse); + ObjectDisposedException.ThrowIf(disposed, this); - FileDownloadProvider downloadProvider; + var ticket = ticketResponse.FileTicket ?? throw new InvalidOperationException("ticketResponse must have FileTicket!"); + FileDownloadProvider? downloadProvider; lock (downloadTickets) { - if (!downloadTickets.TryGetValue(ticket.FileTicket, out downloadProvider)) + if (!downloadTickets.TryGetValue(ticket, out downloadProvider)) { - logger.LogTrace("Download ticket {ticket} not found!", ticket.FileTicket); - return Tuple.Create(null, null); + logger.LogTrace("Download ticket {ticket} not found!", ticket); + return Tuple.Create(null, null); } - downloadTickets.Remove(ticket.FileTicket); + downloadTickets.Remove(ticket); } var errorCode = downloadProvider.ActivationCallback(); if (errorCode.HasValue) { - logger.LogDebug("Download ticket {ticket} failed activation!", ticket.FileTicket); - return Tuple.Create(null, new ErrorMessageResponse(errorCode.Value)); + logger.LogDebug("Download ticket {ticket} failed activation!", ticket); + return Tuple.Create(null, new ErrorMessageResponse(errorCode.Value)); } Stream stream; @@ -196,7 +216,7 @@ public async ValueTask> RetrieveDownloadStre } catch (IOException ex) { - return Tuple.Create( + return Tuple.Create( null, new ErrorMessageResponse(ErrorCode.IOError) { @@ -206,8 +226,8 @@ public async ValueTask> RetrieveDownloadStre try { - logger.LogTrace("Ticket {ticket} downloading...", ticket.FileTicket); - return Tuple.Create(stream, null); + logger.LogTrace("Ticket {ticket} downloading...", ticket); + return Tuple.Create(stream, null); } catch { @@ -217,34 +237,27 @@ public async ValueTask> RetrieveDownloadStre } /// - public async ValueTask SetUploadStream(FileTicketResponse ticket, Stream stream, CancellationToken cancellationToken) + public async ValueTask SetUploadStream(FileTicketResponse ticketResponse, Stream stream, CancellationToken cancellationToken) { - ArgumentNullException.ThrowIfNull(ticket); + ArgumentNullException.ThrowIfNull(ticketResponse); + ObjectDisposedException.ThrowIf(disposed, this); - FileUploadProvider uploadProvider; + var ticket = ticketResponse.FileTicket ?? throw new InvalidOperationException("ticketResponse must have FileTicket!"); + FileUploadProvider? uploadProvider; lock (uploadTickets) { - if (!uploadTickets.TryGetValue(ticket.FileTicket, out uploadProvider)) + if (!uploadTickets.TryGetValue(ticket, out uploadProvider)) { - logger.LogTrace("Upload ticket {ticket} not found!", ticket.FileTicket); + logger.LogTrace("Upload ticket {ticket} not found!", ticket); return new ErrorMessageResponse(ErrorCode.ResourceNotPresent); } - uploadTickets.Remove(ticket.FileTicket); + uploadTickets.Remove(ticket); } return await uploadProvider.Completion(stream, cancellationToken); } - /// - /// Creates a new . - /// - /// A new . - FileTicketResponse CreateTicket() => new () - { - FileTicket = cryptographySuite.GetSecureString(), - }; - /// /// Queue an to run after . /// diff --git a/src/Tgstation.Server.Host/Transfer/FileUploadProvider.cs b/src/Tgstation.Server.Host/Transfer/FileUploadProvider.cs index 9fe8250da5e..0dd709dcc85 100644 --- a/src/Tgstation.Server.Host/Transfer/FileUploadProvider.cs +++ b/src/Tgstation.Server.Host/Transfer/FileUploadProvider.cs @@ -25,7 +25,7 @@ sealed class FileUploadProvider : IFileUploadTicket /// /// The for the . /// - readonly TaskCompletionSource streamTcs; + readonly TaskCompletionSource streamTcs; /// /// The that completes in or when is called. @@ -40,7 +40,7 @@ sealed class FileUploadProvider : IFileUploadTicket /// /// The that occurred while processing the upload if any. /// - ErrorMessageResponse errorMessage; + ErrorMessageResponse? errorMessage; /// /// Initializes a new instance of the class. @@ -52,7 +52,7 @@ public FileUploadProvider(FileTicketResponse ticket, FileUploadStreamKind stream Ticket = ticket ?? throw new ArgumentNullException(nameof(ticket)); ticketExpiryCts = new CancellationTokenSource(); - streamTcs = new TaskCompletionSource(); + streamTcs = new TaskCompletionSource(); completionTcs = new TaskCompletionSource(); this.streamKind = streamKind; } @@ -65,14 +65,6 @@ public ValueTask DisposeAsync() return ValueTask.CompletedTask; } - /// - public async ValueTask GetResult(CancellationToken cancellationToken) - { - using (cancellationToken.Register(() => streamTcs.TrySetCanceled(cancellationToken))) - using (ticketExpiryCts.Token.Register(() => streamTcs.TrySetResult(null))) - return await streamTcs.Task; - } - /// /// Expire the . /// @@ -88,14 +80,14 @@ public void Expire() /// The containing uploaded data. /// The for the operation. /// A resulting in , otherwise. - public async ValueTask Completion(Stream stream, CancellationToken cancellationToken) + public async ValueTask Completion(Stream stream, CancellationToken cancellationToken) { ArgumentNullException.ThrowIfNull(stream); if (ticketExpiryCts.IsCancellationRequested) return new ErrorMessageResponse(ErrorCode.ResourceNotPresent); - Stream bufferedStream = null; + Stream? bufferedStream = null; try { switch (streamKind) @@ -131,7 +123,7 @@ public async ValueTask Completion(Stream stream, Cancellat } /// - public void SetError(ErrorCode errorCode, string additionalData) + public void SetError(ErrorCode errorCode, string? additionalData) { if (errorMessage != null) throw new InvalidOperationException("Error already set!"); @@ -142,5 +134,18 @@ public void SetError(ErrorCode errorCode, string additionalData) }; completionTcs.TrySetResult(); } + + /// + public async ValueTask GetResult(CancellationToken cancellationToken) + => await ((IFileUploadTicket)this).GetResult(cancellationToken) + ?? throw new InvalidOperationException("Upload ticket expired!"); + + /// + async ValueTask IFileUploadTicket.GetResult(CancellationToken cancellationToken) + { + using (cancellationToken.Register(() => streamTcs.TrySetCanceled(cancellationToken))) + using (ticketExpiryCts.Token.Register(() => streamTcs.TrySetResult(null))) + return await streamTcs.Task; + } } } diff --git a/src/Tgstation.Server.Host/Transfer/IFileTransferStreamHandler.cs b/src/Tgstation.Server.Host/Transfer/IFileTransferStreamHandler.cs index 75603c86841..45aafc7cd7c 100644 --- a/src/Tgstation.Server.Host/Transfer/IFileTransferStreamHandler.cs +++ b/src/Tgstation.Server.Host/Transfer/IFileTransferStreamHandler.cs @@ -13,20 +13,20 @@ namespace Tgstation.Server.Host.Transfer public interface IFileTransferStreamHandler { /// - /// Sets the for a given associated with a pending upload. + /// Sets the for a given associated with a pending upload. /// - /// The . + /// The . /// The with uploaded data. /// The for the operation. /// A resulting in if the upload completed successfully, otherwise. - ValueTask SetUploadStream(FileTicketResponse ticket, Stream stream, CancellationToken cancellationToken); + ValueTask SetUploadStream(FileTicketResponse ticketResponse, Stream stream, CancellationToken cancellationToken); /// - /// Gets the the for a given associated with a pending download. + /// Gets the the for a given associated with a pending download. /// - /// The . + /// The . /// The for the operation. /// A resulting in a containing either a containing the data to download or an to return. - ValueTask> RetrieveDownloadStream(FileTicketResponse ticket, CancellationToken cancellationToken); + ValueTask> RetrieveDownloadStream(FileTicketResponse ticketResponse, CancellationToken cancellationToken); } } diff --git a/src/Tgstation.Server.Host/Transfer/IFileUploadTicket.cs b/src/Tgstation.Server.Host/Transfer/IFileUploadTicket.cs index c1bd6e5725b..7e1eabf813a 100644 --- a/src/Tgstation.Server.Host/Transfer/IFileUploadTicket.cs +++ b/src/Tgstation.Server.Host/Transfer/IFileUploadTicket.cs @@ -1,4 +1,8 @@ -using Tgstation.Server.Api.Models; +using System.IO; +using System.Threading; +using System.Threading.Tasks; + +using Tgstation.Server.Api.Models; using Tgstation.Server.Api.Models.Response; using Tgstation.Server.Host.IO; @@ -19,6 +23,14 @@ public interface IFileUploadTicket : IFileStreamProvider /// /// The to set. /// Any additional information that can be provided about the error. - void SetError(ErrorCode errorCode, string additionalData); + void SetError(ErrorCode errorCode, string? additionalData); + + /// + /// Gets the provided . May be called multiple times, though cancelling any may cause all calls to be cancelled. All calls yield the same reference. + /// + /// The for the operation. + /// A resulting in the provided on success, if the upload expired. + /// The resulting is owned by the and is short lived unless otherwise specified. It should be buffered if it needs use outside the lifetime of the . + new ValueTask GetResult(CancellationToken cancellationToken); } } diff --git a/src/Tgstation.Server.Host/Utils/ApiHeadersProvider.cs b/src/Tgstation.Server.Host/Utils/ApiHeadersProvider.cs index 56aa2886195..1805d337c1c 100644 --- a/src/Tgstation.Server.Host/Utils/ApiHeadersProvider.cs +++ b/src/Tgstation.Server.Host/Utils/ApiHeadersProvider.cs @@ -10,12 +10,12 @@ namespace Tgstation.Server.Host.Utils sealed class ApiHeadersProvider : IApiHeadersProvider { /// - public ApiHeaders ApiHeaders => attemptedApiHeadersCreation + public ApiHeaders? ApiHeaders => attemptedApiHeadersCreation ? apiHeaders : CreateApiHeaders(false); /// - public HeadersException HeadersException { get; private set; } + public HeadersException? HeadersException { get; private set; } /// /// The for the . @@ -25,7 +25,7 @@ sealed class ApiHeadersProvider : IApiHeadersProvider /// /// Backing field for . /// - ApiHeaders apiHeaders; + ApiHeaders? apiHeaders; /// /// If populating was previously attempted. @@ -42,14 +42,14 @@ public ApiHeadersProvider(IHttpContextAccessor httpContextAccessor) } /// - public ApiHeaders CreateAuthlessHeaders() => CreateApiHeaders(true); + public ApiHeaders CreateAuthlessHeaders() => CreateApiHeaders(true)!; /// /// Attempt to parse from the , optionally populating the properties. /// /// If the error should be ignored and / should not be populated. /// A newly parsed or if was set and the parse failed. - ApiHeaders CreateApiHeaders(bool authless) + ApiHeaders? CreateApiHeaders(bool authless) { if (httpContextAccessor.HttpContext == null) throw new InvalidOperationException("httpContextAccessor has no HttpContext!"); diff --git a/src/Tgstation.Server.Host/Utils/DisposeInvoker.cs b/src/Tgstation.Server.Host/Utils/DisposeInvoker.cs new file mode 100644 index 00000000000..b90d01318b7 --- /dev/null +++ b/src/Tgstation.Server.Host/Utils/DisposeInvoker.cs @@ -0,0 +1,49 @@ +using System; +using System.Threading; + +namespace Tgstation.Server.Host.Utils +{ + /// + /// Runs a given on . + /// + class DisposeInvoker : IDisposable + { + /// + /// If was called. + /// + public bool IsDisposed => disposeRan != 0; + + /// + /// The to run on . + /// + readonly Action disposeAction; + + /// + /// An representation of a indicating if has ran. + /// + volatile int disposeRan; + + /// + /// Initializes a new instance of the class. + /// + /// The value of . + public DisposeInvoker(Action disposeAction) + { + this.disposeAction = disposeAction ?? throw new ArgumentNullException(nameof(disposeAction)); + } + + /// + public void Dispose() + { + if (Interlocked.Exchange(ref disposeRan, 1) != 0) + return; + + DisposeImpl(); + } + + /// + /// Implementation of run after reentrancy check. + /// + protected virtual void DisposeImpl() => disposeAction(); + } +} diff --git a/src/Tgstation.Server.Host/Utils/FifoSemaphore.cs b/src/Tgstation.Server.Host/Utils/FifoSemaphore.cs index 7a4e200c9cb..0bb696680a2 100644 --- a/src/Tgstation.Server.Host/Utils/FifoSemaphore.cs +++ b/src/Tgstation.Server.Host/Utils/FifoSemaphore.cs @@ -51,7 +51,7 @@ public FifoSemaphore() /// A resulting in the locked . public async ValueTask Lock(CancellationToken cancellationToken) { - FifoSemaphoreTicket ticket = null; + FifoSemaphoreTicket? ticket = null; using (cancellationToken.Register( () => { @@ -63,7 +63,7 @@ public async ValueTask Lock(CancellationToken cancellation var context = await SemaphoreSlimContext.Lock(semaphore, cancellationToken); try { - FifoSemaphoreTicket peekedTicket = null; + FifoSemaphoreTicket? peekedTicket = null; while (ticketQueue.Count > 0) { peekedTicket = ticketQueue.Peek(); diff --git a/src/Tgstation.Server.Host/Utils/GitHub/GitHubClientFactory.cs b/src/Tgstation.Server.Host/Utils/GitHub/GitHubClientFactory.cs index 7595a70a431..e479388b79b 100644 --- a/src/Tgstation.Server.Host/Utils/GitHub/GitHubClientFactory.cs +++ b/src/Tgstation.Server.Host/Utils/GitHub/GitHubClientFactory.cs @@ -43,7 +43,7 @@ sealed class GitHubClientFactory : IGitHubClientFactory /// /// Cache of created s and last used times, keyed by access token. /// - readonly Dictionary clientCache; + readonly Dictionary clientCache; /// /// Initializes a new instance of the class. @@ -76,7 +76,7 @@ public IGitHubClient CreateClient(string accessToken) /// /// Optional access token to use as credentials. /// The for the given . - GitHubClient GetOrCreateClient(string accessToken) + GitHubClient GetOrCreateClient(string? accessToken) { GitHubClient client; bool cacheHit; @@ -97,23 +97,24 @@ GitHubClient GetOrCreateClient(string accessToken) var now = DateTimeOffset.UtcNow; if (!cacheHit) { + var product = assemblyInformationProvider.ProductInfoHeaderValue.Product!; client = new GitHubClient( new ProductHeaderValue( - assemblyInformationProvider.ProductInfoHeaderValue.Product.Name, - assemblyInformationProvider.ProductInfoHeaderValue.Product.Version)); + product.Name, + product.Version)); if (accessToken != null) client.Credentials = new Credentials(accessToken); - clientCache.Add(cacheKey, (client, now)); + clientCache.Add(cacheKey, (Client: client, LastUsed: now)); lastUsed = null; } else { logger.LogTrace("Cache hit for GitHubClient"); - client = tuple.Item1; - lastUsed = tuple.Item2; - tuple.Item2 = now; + client = tuple.Client; + lastUsed = tuple.LastUsed; + tuple.LastUsed = now; } // Prune the cache @@ -125,7 +126,7 @@ GitHubClient GetOrCreateClient(string accessToken) continue; // save the hash lookup tuple = clientCache[key]; - if (tuple.Item2 <= purgeAfter) + if (tuple.LastUsed <= purgeAfter) { clientCache.Remove(key); ++purgeCount; diff --git a/src/Tgstation.Server.Host/Utils/GitHub/GitHubService.cs b/src/Tgstation.Server.Host/Utils/GitHub/GitHubService.cs index c210cb1b547..a56c956ebf2 100644 --- a/src/Tgstation.Server.Host/Utils/GitHub/GitHubService.cs +++ b/src/Tgstation.Server.Host/Utils/GitHub/GitHubService.cs @@ -80,36 +80,38 @@ public async ValueTask> GetTgsReleases(Cancellation .GetAll(updatesConfiguration.GitHubRepositoryId) .WaitAsync(cancellationToken); + var gitPrefix = updatesConfiguration.GitTagPrefix ?? String.Empty; + logger.LogTrace("{totalReleases} total releases", allReleases.Count); - var releases = allReleases - .Where(release => + var releases = allReleases! + .Where(release => + { + if (!release.PublishedAt.HasValue) { - if (!release.PublishedAt.HasValue) - { - logger.LogDebug("Release tag without PublishedAt: {releaseTag}", release.TagName); - return false; - } - - if (!release.TagName.StartsWith(updatesConfiguration.GitTagPrefix, StringComparison.InvariantCulture)) - return false; - - return true; - }) - .GroupBy(release => + logger.LogDebug("Release tag without PublishedAt: {releaseTag}", release.TagName); + return false; + } + + if (!release.TagName.StartsWith(gitPrefix, StringComparison.InvariantCulture)) + return false; + + return true; + }) + .GroupBy(release => + { + if (!Version.TryParse(release.TagName.Replace(gitPrefix, String.Empty, StringComparison.Ordinal), out var version)) { - if (!Version.TryParse(release.TagName.Replace(updatesConfiguration.GitTagPrefix, String.Empty, StringComparison.Ordinal), out var version)) - { - logger.LogDebug("Unparsable release tag: {releaseTag}", release.TagName); - return null; - } + logger.LogDebug("Unparsable release tag: {releaseTag}", release.TagName); + return null; + } - return version; - }) - .Where(grouping => grouping.Key != null) + return version; + }) + .Where(grouping => grouping.Key != null) - // GitHub can return the same result twice or some other nonsense - .Select(grouping => Tuple.Create(grouping.Key, grouping.OrderBy(x => x.PublishedAt.Value).First())) - .ToDictionary(tuple => tuple.Item1, tuple => tuple.Item2); + // GitHub can return the same result twice or some other nonsense + .Select(grouping => Tuple.Create(grouping.Key!, grouping.OrderBy(x => x.PublishedAt ?? DateTimeOffset.MinValue).First())) + .ToDictionary(tuple => tuple.Item1, tuple => tuple.Item2); logger.LogTrace("{parsedReleases} parsed releases", releases.Count); return releases; @@ -259,5 +261,23 @@ public Task GetPullRequest(string repoOwner, string repoName, int p pullRequestNumber) .WaitAsync(cancellationToken); } + + /// + public Task GetCommit(string repoOwner, string repoName, string committish, CancellationToken cancellationToken) + { + ArgumentNullException.ThrowIfNull(repoOwner); + + ArgumentNullException.ThrowIfNull(repoName); + + logger.LogTrace("GetPulGetCommitlRequest"); + return gitHubClient + .Repository + .Commit + .Get( + repoOwner, + repoName, + committish) + .WaitAsync(cancellationToken); + } } } diff --git a/src/Tgstation.Server.Host/Utils/GitHub/GitHubServiceFactory.cs b/src/Tgstation.Server.Host/Utils/GitHub/GitHubServiceFactory.cs index 8ff53440a68..efa08d13bba 100644 --- a/src/Tgstation.Server.Host/Utils/GitHub/GitHubServiceFactory.cs +++ b/src/Tgstation.Server.Host/Utils/GitHub/GitHubServiceFactory.cs @@ -58,7 +58,7 @@ public IAuthenticatedGitHubService CreateService(string accessToken) /// The for the . /// A new . GitHubService CreateServiceImpl(IGitHubClient gitHubClient) - => new ( + => new( gitHubClient, loggerFactory.CreateLogger(), updatesConfiguration); diff --git a/src/Tgstation.Server.Host/Utils/GitHub/IGitHubService.cs b/src/Tgstation.Server.Host/Utils/GitHub/IGitHubService.cs index 2bc7e9c2d31..e2f338d77aa 100644 --- a/src/Tgstation.Server.Host/Utils/GitHub/IGitHubService.cs +++ b/src/Tgstation.Server.Host/Utils/GitHub/IGitHubService.cs @@ -63,5 +63,15 @@ public interface IGitHubService /// The for the operation. /// A resulting in the target . Task GetPullRequest(string repoOwner, string repoName, int pullRequestNumber, CancellationToken cancellationToken); + + /// + /// Get a given . + /// + /// The owner of the target repository. + /// The name of the target repository. + /// The target SHA or ref to get the commit for. + /// The for the operation. + /// A resulting in the target . + Task GetCommit(string repoOwner, string repoName, string committish, CancellationToken cancellationToken); } } diff --git a/src/Tgstation.Server.Host/Utils/IApiHeadersProvider.cs b/src/Tgstation.Server.Host/Utils/IApiHeadersProvider.cs index 7de65834d3d..bf3aa714bfc 100644 --- a/src/Tgstation.Server.Host/Utils/IApiHeadersProvider.cs +++ b/src/Tgstation.Server.Host/Utils/IApiHeadersProvider.cs @@ -10,12 +10,12 @@ public interface IApiHeadersProvider /// /// The created , if any. /// - ApiHeaders ApiHeaders { get; } + ApiHeaders? ApiHeaders { get; } /// /// The thrown when attempting to parse the if any. /// - HeadersException HeadersException { get; } + HeadersException? HeadersException { get; } /// /// Attempt to create without checking for the presence of an header. diff --git a/src/Tgstation.Server.Host/Utils/OpenApiEnumVarNamesExtension.cs b/src/Tgstation.Server.Host/Utils/OpenApiEnumVarNamesExtension.cs index a81beb83393..6953cc6480b 100644 --- a/src/Tgstation.Server.Host/Utils/OpenApiEnumVarNamesExtension.cs +++ b/src/Tgstation.Server.Host/Utils/OpenApiEnumVarNamesExtension.cs @@ -49,8 +49,8 @@ public void Write(IOpenApiWriter writer, OpenApiSpecVersion specVersion) writer.WriteStartArray(); foreach (var enumValue in Enum.GetValues(enumType)) { - var enumName = enumValue.ToString(); - var field = enumType.GetField(enumName); + var enumName = enumValue.ToString()!; + var field = enumType.GetField(enumName)!; if (field.IsDefined(typeof(ObsoleteAttribute), false)) enumName = $"DEPRECATED_{enumName}"; diff --git a/src/Tgstation.Server.Host/Utils/PortAllocator.cs b/src/Tgstation.Server.Host/Utils/PortAllocator.cs index c2337135bc9..1aa096ab0b1 100644 --- a/src/Tgstation.Server.Host/Utils/PortAllocator.cs +++ b/src/Tgstation.Server.Host/Utils/PortAllocator.cs @@ -12,6 +12,7 @@ using Tgstation.Server.Host.Core; using Tgstation.Server.Host.Database; using Tgstation.Server.Host.Extensions; +using Tgstation.Server.Host.System; namespace Tgstation.Server.Host.Utils { @@ -28,6 +29,11 @@ sealed class PortAllocator : IPortAllocator /// readonly IDatabaseContext databaseContext; + /// + /// The for the . + /// + readonly IPlatformIdentifier platformIdentifier; + /// /// The for the . /// @@ -43,16 +49,19 @@ sealed class PortAllocator : IPortAllocator /// /// The value of . /// The value of . + /// The value of . /// The containing the value of . /// The value of . public PortAllocator( IServerPortProvider serverPortProvider, IDatabaseContext databaseContext, + IPlatformIdentifier platformIdentifier, IOptions swarmConfigurationOptions, ILogger logger) { this.serverPortProvider = serverPortProvider ?? throw new ArgumentNullException(nameof(serverPortProvider)); this.databaseContext = databaseContext ?? throw new ArgumentNullException(nameof(databaseContext)); + this.platformIdentifier = platformIdentifier ?? throw new ArgumentNullException(nameof(platformIdentifier)); swarmConfiguration = swarmConfigurationOptions?.Value ?? throw new ArgumentNullException(nameof(swarmConfigurationOptions)); this.logger = logger ?? throw new ArgumentNullException(nameof(logger)); } @@ -65,14 +74,14 @@ public PortAllocator( var ddPorts = await databaseContext .DreamDaemonSettings .AsQueryable() - .Where(x => x.Instance.SwarmIdentifer == swarmConfiguration.Identifier) + .Where(x => x.Instance!.SwarmIdentifer == swarmConfiguration.Identifier) .Select(x => x.Port) .ToListAsync(cancellationToken); var dmPorts = await databaseContext .DreamMakerSettings .AsQueryable() - .Where(x => x.Instance.SwarmIdentifer == swarmConfiguration.Identifier) + .Where(x => x.Instance!.SwarmIdentifer == swarmConfiguration.Identifier) .Select(x => x.ApiValidationPort) .ToListAsync(cancellationToken); @@ -92,7 +101,8 @@ public PortAllocator( try { - SocketExtensions.BindTest(port, false); + SocketExtensions.BindTest(platformIdentifier, port, false, true); + SocketExtensions.BindTest(platformIdentifier, port, false, false); } catch (Exception ex) { diff --git a/src/Tgstation.Server.Host/Utils/README.md b/src/Tgstation.Server.Host/Utils/README.md index bad6a54616e..0eb93a26189 100644 --- a/src/Tgstation.Server.Host/Utils/README.md +++ b/src/Tgstation.Server.Host/Utils/README.md @@ -5,5 +5,5 @@ This is a bag of classes used throughout TGS that don't quite belong anywhere el - [IAsyncDelayer](./IAsyncDelayer.cs) and [implementation](./AsyncDelayer.cs) is a class used to sleep code. It's generally a no-op in test scenarios. - [IGitHubClientFactory](./IGitHubClientFactory.cs) and [implementation](./GitHubClientFactory.cs) is a class used to create GitHub API clients using [ocktokit.net](https://github.com/octokit/octokit.net). - [OpenApiEnumVarNamesExtension](./OpenApiEnumVarNamesExtension) implements the [x-var-names OpenAPI 3.0 extension](https://github.com/OpenAPITools/openapi-generator/blob/master/docs/templating.md#enum) in our generated API json. -- [SemaphoreSlimContext](./SemaphoreSlimContext.cs) is a helper class for working with [.NET asynchronous sempahores](https://docs.microsoft.com/en-us/dotnet/api/system.threading.semaphoreslim?view=netcore-6.0). +- [SemaphoreSlimContext](./SemaphoreSlimContext.cs) is a helper class for working with [.NET asynchronous sempahores](https://docs.microsoft.com/en-us/dotnet/api/system.threading.semaphoreslim?view=netcore-8.0). - [SwaggerConfiguration](./SwaggerConfiguration.cs) configures [Swashbuckle](https://github.com/domaindrivendev/Swashbuckle.AspNetCore) to generate our OpenAPI specification. diff --git a/src/Tgstation.Server.Host/Utils/ReferenceCounter.cs b/src/Tgstation.Server.Host/Utils/ReferenceCounter.cs index 4351c65db66..6377c0295f0 100644 --- a/src/Tgstation.Server.Host/Utils/ReferenceCounter.cs +++ b/src/Tgstation.Server.Host/Utils/ReferenceCounter.cs @@ -22,12 +22,12 @@ abstract class ReferenceCounter : IDisposable /// /// Backing field for . /// - TInstance actualInstance; + TInstance? actualInstance; /// /// The to take when is called. /// - Action referenceCleanupAction; + Action? referenceCleanupAction; /// /// If the was initialized. diff --git a/src/Tgstation.Server.Host/Utils/ReferenceCountingContainer.cs b/src/Tgstation.Server.Host/Utils/ReferenceCountingContainer.cs index 121411bd1da..798fc2d16b7 100644 --- a/src/Tgstation.Server.Host/Utils/ReferenceCountingContainer.cs +++ b/src/Tgstation.Server.Host/Utils/ReferenceCountingContainer.cs @@ -28,7 +28,7 @@ public Task OnZeroReferences { if (referenceCount == 0) return Task.CompletedTask; - return onZeroReferencesTcs.Task; + return onZeroReferencesTcs!.Task; } } } @@ -41,7 +41,7 @@ public Task OnZeroReferences /// /// Backing for . /// - TaskCompletionSource onZeroReferencesTcs; + TaskCompletionSource? onZeroReferencesTcs; /// /// Count of active s. @@ -77,7 +77,7 @@ public TReference AddReference() { lock (referenceCountLock) if (--referenceCount == 0) - onZeroReferencesTcs.SetResult(); + onZeroReferencesTcs!.SetResult(); }); return reference; } diff --git a/src/Tgstation.Server.Host/Utils/SemaphoreSlimContext.cs b/src/Tgstation.Server.Host/Utils/SemaphoreSlimContext.cs index 25f070cc4be..e9a40d7c68d 100644 --- a/src/Tgstation.Server.Host/Utils/SemaphoreSlimContext.cs +++ b/src/Tgstation.Server.Host/Utils/SemaphoreSlimContext.cs @@ -28,7 +28,7 @@ public static async ValueTask Lock(SemaphoreSlim semaphore /// The to lock. /// The result of the lock attempt. /// A for the lock on success, or if it was not acquired. - public static SemaphoreSlimContext TryLock(SemaphoreSlim semaphore, out bool locked) + public static SemaphoreSlimContext? TryLock(SemaphoreSlim semaphore, out bool locked) { ArgumentNullException.ThrowIfNull(semaphore); locked = semaphore.Wait(TimeSpan.Zero); diff --git a/src/Tgstation.Server.Host/Utils/SignalR/ComprehensiveHubContext.cs b/src/Tgstation.Server.Host/Utils/SignalR/ComprehensiveHubContext.cs index 1f09be35719..d9c32143cef 100644 --- a/src/Tgstation.Server.Host/Utils/SignalR/ComprehensiveHubContext.cs +++ b/src/Tgstation.Server.Host/Utils/SignalR/ComprehensiveHubContext.cs @@ -44,7 +44,7 @@ sealed class ComprehensiveHubContext : IConnectionMappedHubCo readonly ConcurrentDictionary> userConnections; /// - public event Func, Task>, CancellationToken, ValueTask> OnConnectionMapGroups; + public event Func, Task>, CancellationToken, ValueTask>? OnConnectionMapGroups; /// /// Initializes a new instance of the class. @@ -65,7 +65,7 @@ public ComprehensiveHubContext( public List UserConnectionIds(User user) { ArgumentNullException.ThrowIfNull(user); - var connectionIds = userConnections.GetOrAdd(user.Id.Value, _ => new Dictionary()); + var connectionIds = userConnections.GetOrAdd(user.Require(x => x.Id), _ => new Dictionary()); lock (connectionIds) return connectionIds.Keys.ToList(); } @@ -76,7 +76,7 @@ public ValueTask UserConnected(IAuthenticationContext authenticationContext, THu ArgumentNullException.ThrowIfNull(authenticationContext); ArgumentNullException.ThrowIfNull(hub); - var userId = authenticationContext.User.Id.Value; + var userId = authenticationContext.User.Require(x => x.Id); var context = hub.Context; logger.LogTrace( "Mapping user {userId} to hub connection ID: {connectionId}", @@ -129,11 +129,12 @@ public void UserDisconnected(string connectionId) public void AbortUnauthedConnections(User user) { ArgumentNullException.ThrowIfNull(user); - logger.LogTrace("NotifyAndAbortUnauthedConnections. UID {userId}", user.Id.Value); + var uid = user.Require(x => x.Id); + logger.LogTrace("NotifyAndAbortUnauthedConnections. UID {userId}", uid); - List connections = null; + List? connections = null; userConnections.AddOrUpdate( - user.Id.Value, + uid, _ => new Dictionary(), (_, old) => { @@ -146,7 +147,7 @@ public void AbortUnauthedConnections(User user) return old; }); - foreach (var context in connections) + foreach (var context in connections!) context.Abort(); } } diff --git a/src/Tgstation.Server.Host/Utils/SignalR/ConnectionMappingHub.cs b/src/Tgstation.Server.Host/Utils/SignalR/ConnectionMappingHub.cs index 05e2eb8742e..80e89089fa0 100644 --- a/src/Tgstation.Server.Host/Utils/SignalR/ConnectionMappingHub.cs +++ b/src/Tgstation.Server.Host/Utils/SignalR/ConnectionMappingHub.cs @@ -50,7 +50,7 @@ public override async Task OnConnectedAsync() /// [AllowAnonymous] - public override Task OnDisconnectedAsync(Exception exception) + public override Task OnDisconnectedAsync(Exception? exception) { connectionMapper.UserDisconnected(Context.ConnectionId); return base.OnDisconnectedAsync(exception); diff --git a/src/Tgstation.Server.Host/Utils/SwaggerConfiguration.cs b/src/Tgstation.Server.Host/Utils/SwaggerConfiguration.cs index 89f77167329..a700ccc9cba 100644 --- a/src/Tgstation.Server.Host/Utils/SwaggerConfiguration.cs +++ b/src/Tgstation.Server.Host/Utils/SwaggerConfiguration.cs @@ -27,6 +27,16 @@ namespace Tgstation.Server.Host.Utils /// sealed class SwaggerConfiguration : IOperationFilter, IDocumentFilter, ISchemaFilter, IRequestBodyFilter { + /// + /// The name of the swagger document. + /// + public const string DocumentName = "tgs_api"; + + /// + /// The path to the hosted documentation site. + /// + public const string DocumentationSiteRouteExtension = "documentation"; + /// /// The name for password authentication. /// @@ -51,7 +61,7 @@ sealed class SwaggerConfiguration : IOperationFilter, IDocumentFilter, ISchemaFi public static void Configure(SwaggerGenOptions swaggerGenOptions, string assemblyDocumentationPath, string apiDocumentationPath) { swaggerGenOptions.SwaggerDoc( - "v1", + DocumentName, new OpenApiInfo { Title = "TGS API", @@ -335,7 +345,7 @@ public void Apply(OpenApiOperation operation, OperationFilterContext context) ArgumentNullException.ThrowIfNull(operation); ArgumentNullException.ThrowIfNull(context); - operation.OperationId = $"{context.MethodInfo.DeclaringType.Name}.{context.MethodInfo.Name}"; + operation.OperationId = $"{context.MethodInfo.DeclaringType!.Name}.{context.MethodInfo.Name}"; var authAttributes = context .MethodInfo @@ -405,7 +415,7 @@ public void Apply(OpenApiOperation operation, OperationFilterContext context) twoHundredResponseContents.Add(MediaTypeNames.Application.Octet, fileContent); } } - else if (context.MethodInfo.Name == nameof(HomeController.CreateToken)) + else if (context.MethodInfo.Name == nameof(ApiRootController.CreateToken)) { var passwordScheme = new OpenApiSecurityScheme { diff --git a/src/Tgstation.Server.Host/Views/Root/Index.cshtml b/src/Tgstation.Server.Host/Views/Root/Index.cshtml new file mode 100644 index 00000000000..bc23093e87c --- /dev/null +++ b/src/Tgstation.Server.Host/Views/Root/Index.cshtml @@ -0,0 +1,27 @@ +@{ + var title = Model.Title; + + + + @title + + + + + @{ + if (Model.Links != null) + foreach (KeyValuePair kvp in Model.Links) + { +

+ @kvp.Key +

+ } + } + + +} diff --git a/src/Tgstation.Server.Host/appsettings.yml b/src/Tgstation.Server.Host/appsettings.yml index 7b0f60e8a3a..56f87f2169e 100644 --- a/src/Tgstation.Server.Host/appsettings.yml +++ b/src/Tgstation.Server.Host/appsettings.yml @@ -13,9 +13,12 @@ General: UserGroupLimit: 25 # Maximum number of allowed groups InstanceLimit: 10 # Maximum number of allowed instances ValidInstancePaths: # An array of directories instances may be created in (either directly or as a subdirectory). null removes the restriction - HostApiDocumentation: false # Make HTTP API documentation available at /swagger/v1/swagger.json - SkipAddingByondFirewallException: false # Windows Only: Prevent running netsh.exe to add a firewall exception for installed DreamDaemon binaries + HostApiDocumentation: false # Make HTTP API documentation available at /api/doc/tgs_api.json + SkipAddingByondFirewallException: false # Windows Only: Prevent running netsh.exe to add a firewall exception for installed engine binaries DeploymentDirectoryCopyTasksPerCore: 100 # Maximum number of concurrent file copy operations PER available CPU core + OpenDreamGitUrl: https://github.com/OpenDreamProject/OpenDream # The repository to retrieve OpenDream from + OpenDreamGitTagPrefix: v # The prefix to the OpenDream semver as tags appear in the git repository + OpenDreamSuppressInstallOutput: false # Suppress the dotnet output of creating an OpenDream installation. Known to cause hangs in CI. Session: HighPriorityLiveDreamDaemon: false # If DreamDaemon instances should run as higher priority processes LowPriorityDeploymentProcesses: true # If TGS Deployments should run as lower priority processes @@ -46,7 +49,7 @@ Updates: Database: DatabaseType: SqlServer # The database type TGS connects to ServerVersion: # The version of the database being connected to, generally not required to be specified - ConnectionString: Data Source=(local);Initial Catalog=TGS;Integrated Security=True # The connection string used to establish the database connection. Format varies for each DatabaseType + ConnectionString: Data Source=(local);Initial Catalog=TGS;Integrated Security=True;Encrypt=false # The connection string used to establish the database connection. Format varies for each DatabaseType DropDatabase: false # DANGEROUS! Causes TGS to recreate its database on startup. Must be unset manually ResetAdminPassword: false # DANGEROUS! Causes TGS to reset the `Admin` user password back to its default value on startup. Must be unset manually. Security: diff --git a/tests/DMAPI/LongRunning/Test.dm b/tests/DMAPI/LongRunning/Test.dm index 52e8f73c8cf..90164c7e800 100644 --- a/tests/DMAPI/LongRunning/Test.dm +++ b/tests/DMAPI/LongRunning/Test.dm @@ -6,9 +6,11 @@ log << "Initial value of sleep_offline: [sleep_offline]" sleep_offline = FALSE - // Intentionally slow down startup for testing purposes - for(var/i in 1 to 10000000) - dab() + if(params["slow_start"]) + // Intentionally slow down startup for health check testing purposes + for(var/i in 1 to 10000000) + dab() + TgsNew(new /datum/tgs_event_handler/impl, TGS_SECURITY_SAFE) var/sec = TgsSecurityLevel() @@ -36,8 +38,10 @@ if(!res_contents) FailTest("Failed to resource? No contents!") +#ifndef OPENDREAM if(!fexists("[DME_NAME].rsc")) FailTest("Failed to create .rsc!") +#endif #ifdef RUN_STATIC_FILE_TESTS if(params["expect_static_files"]) @@ -96,11 +100,11 @@ /world/Topic(T, Addr, Master, Keys) if(findtext(T, "tgs_integration_test_tactics3") == 0) - log << "Topic: [T]" + log << "Topic (sleep_offline: [sleep_offline]): [T]" else log << "tgs_integration_test_tactics3 " . = HandleTopic(T) - log << "Response: [.]" + log << "Response (sleep_offline: [sleep_offline]): [.]" var/startup_complete var/run_bridge_test @@ -184,7 +188,12 @@ var/run_bridge_test kajigger_test = TRUE return "we love casting spells" - TgsChatBroadcast(new /datum/tgs_message_content("Recieved non-tgs topic: `[T]`")) + var/its_sad = data["im_out_of_memes"] + if(its_sad) + TestLegacyBridge() + return "all gucci" + + TgsChatBroadcast(new /datum/tgs_message_content("Received non-tgs topic: `[T]`")) return "feck" @@ -223,7 +232,7 @@ var/received_health_check = FALSE /datum/tgs_event_handler/impl/HandleEvent(event_code, ...) set waitfor = FALSE - world.TgsChatBroadcast(new /datum/tgs_message_content("Recieved event: `[json_encode(args)]`")) + world.TgsChatBroadcast(new /datum/tgs_message_content("Received event: `[json_encode(args)]`")) if(event_code == TGS_EVENT_HEALTH_CHECK) received_health_check = TRUE @@ -239,15 +248,15 @@ var/received_health_check = FALSE /world/Export(url) var/redact = length(url) > 1000 - log << "Export: [redact ? "" : url]" + log << "Export (sleep_offline: [sleep_offline]): [redact ? "" : url]" . = ..() - log << "Export completed: [redact ? "" : json_encode(.)]" + log << "Export completed (sleep_offline: [sleep_offline]): [redact ? "" : json_encode(.)]" /proc/RebootAsync() set waitfor = FALSE - world.TgsChatBroadcast(new /datum/tgs_message_content("Rebooting after 3 seconds")); + world.TgsChatBroadcast(new /datum/tgs_message_content("Rebooting after 1 seconds")); world.log << "About to sleep. sleep_offline: [world.sleep_offline]" - sleep(30) + sleep(10) world.log << "Done sleep, calling Reboot" world.Reboot() @@ -353,3 +362,24 @@ var/suppress_bridge_spam = FALSE FailTest("Failed to end bridge limit test! [(istype(final_result) ? json_encode(final_result): (final_result || "null"))]") api.access_identifier = old_ai + +/proc/TestLegacyBridge() + var/datum/tgs_api/v5/api = TGS_READ_GLOBAL(tgs) + if(api.interop_version.suite != 5) + FailTest("Legacy bridge test not required anymore?") + + var/old_minor_version = api.interop_version.minor + api.interop_version.minor = 6 // before api repath + + var/result + var/bridge_request = api.CreateBridgeRequest(5, list("chatMessage" = list("text" = "legacy bridge test", "channelIds" = list()))) + try + result = api.PerformBridgeRequest(bridge_request) + catch(var/exception/e2) + world.log << "Caught exception: [e2]" + result = null + + if(!result || lastTgsError) + FailTest("Failed bridge request redirect test!") + + api.interop_version.minor = old_minor_version diff --git a/tests/Tgstation.Server.Api.Tests/Models/Internal/TestEngineVersion.cs b/tests/Tgstation.Server.Api.Tests/Models/Internal/TestEngineVersion.cs new file mode 100644 index 00000000000..f13f09f3ff7 --- /dev/null +++ b/tests/Tgstation.Server.Api.Tests/Models/Internal/TestEngineVersion.cs @@ -0,0 +1,46 @@ +using System; + +using Microsoft.VisualStudio.TestTools.UnitTesting; + +namespace Tgstation.Server.Api.Models.Internal.Tests +{ + [TestClass] + public sealed class TestEngineVersion + { + [TestMethod] + public void TestParsing() + { + Assert.IsTrue(EngineVersion.TryParse("OpenDream-6894ba0702c1764d333eb52aa0cc211d62e2cb1c-1", out var version)); + Assert.IsNotNull(version); + Assert.AreEqual(EngineType.OpenDream, version.Engine); + Assert.AreEqual("6894ba0702c1764d333eb52aa0cc211d62e2cb1c", version.SourceSHA); + Assert.IsNull(version.Version); + Assert.AreEqual(1, version.CustomIteration); + + Assert.IsTrue(EngineVersion.TryParse("OpenDream-6894ba0702c1764d333eb52aa0cc211d62e2cb1c", out version)); + Assert.IsNotNull(version); + Assert.AreEqual(EngineType.OpenDream, version.Engine); + Assert.AreEqual("6894ba0702c1764d333eb52aa0cc211d62e2cb1c", version.SourceSHA); + Assert.IsNull(version.Version); + Assert.IsFalse(version.CustomIteration.HasValue); + + Assert.IsTrue(EngineVersion.TryParse("515.1616", out version)); + Assert.IsNotNull(version); + Assert.AreEqual(EngineType.Byond, version.Engine); + Assert.AreEqual(new Version(515, 1616), version.Version); + Assert.IsNull(version.SourceSHA); + Assert.IsFalse(version.CustomIteration.HasValue); + + Assert.IsTrue(EngineVersion.TryParse("515.1616.12", out version)); + Assert.IsNotNull(version); + Assert.AreEqual(EngineType.Byond, version.Engine); + Assert.AreEqual(new Version(515, 1616), version.Version); + Assert.IsNull(version.SourceSHA); + Assert.AreEqual(12, version.CustomIteration); + + Assert.IsFalse(EngineVersion.TryParse("x", out version)); + Assert.IsNull(version); + Assert.ThrowsException(() => EngineVersion.Parse("x")); + } + } +} diff --git a/tests/Tgstation.Server.Api.Tests/Rights/TestRights.cs b/tests/Tgstation.Server.Api.Tests/Rights/TestRights.cs index 62e41b7d982..d96216968b7 100644 --- a/tests/Tgstation.Server.Api.Tests/Rights/TestRights.cs +++ b/tests/Tgstation.Server.Api.Tests/Rights/TestRights.cs @@ -40,10 +40,10 @@ public void TestAllPowerOfTwo() [TestMethod] public void TestAllRightsWorks() { - var allByondRights = ByondRights.CancelInstall | ByondRights.InstallOfficialOrChangeActiveVersion | ByondRights.ListInstalled | ByondRights.ReadActive | ByondRights.InstallCustomVersion | ByondRights.DeleteInstall; - var automaticByondRights = RightsHelper.AllRights(); + var allEngineRights = EngineRights.CancelInstall | EngineRights.InstallOfficialOrChangeActiveByondVersion | EngineRights.ListInstalled | EngineRights.ReadActive | EngineRights.InstallCustomByondVersion | EngineRights.DeleteInstall | EngineRights.InstallCustomOpenDreamVersion | EngineRights.InstallOfficialOrChangeActiveOpenDreamVersion; + var automaticByondRights = RightsHelper.AllRights(); - Assert.AreEqual(allByondRights, automaticByondRights); + Assert.AreEqual(allEngineRights, automaticByondRights); } } } diff --git a/tests/Tgstation.Server.Client.Tests/TestApiClient.cs b/tests/Tgstation.Server.Client.Tests/TestApiClient.cs index 47c5a130583..900d64a56f4 100644 --- a/tests/Tgstation.Server.Client.Tests/TestApiClient.cs +++ b/tests/Tgstation.Server.Client.Tests/TestApiClient.cs @@ -11,6 +11,8 @@ using System.Threading.Tasks; using Tgstation.Server.Api; +using Tgstation.Server.Api.Models; +using Tgstation.Server.Api.Models.Internal; using Tgstation.Server.Api.Models.Response; using Tgstation.Server.Common.Http; @@ -22,9 +24,13 @@ public sealed class TestApiClient [TestMethod] public async Task TestDeserializingByondModelsWork() { - var sample = new ByondResponse + var sample = new EngineResponse { - Version = new Version(511, 1385, 0) + EngineVersion = new EngineVersion + { + Engine = EngineType.Byond, + Version = new Version(511, 1385) + } }; var sampleJson = JsonConvert.SerializeObject(sample, new JsonSerializerSettings @@ -48,22 +54,27 @@ public async Task TestDeserializingByondModelsWork() new ProductHeaderValue("fake"), new TokenResponse { - Bearer = "fake", + Bearer = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMyIsImV4cCI6IjE2OTkzOTUwNTIiLCJuYmYiOiIxNjk5MzA4NjUyIiwiaXNzIjoiVGdzdGF0aW9uLlNlcnZlci5Ib3N0IiwiYXVkIjoiVGdzdGF0aW9uLlNlcnZlci5BcGkifQ.GRqEd3LRYLkbzk7NHTqcBPX-Xc1vmE_zmbJEDowAXV4", }), null, false); - var result = await client.Read(Routes.Byond, default); - Assert.AreEqual(sample.Version, result.Version); - Assert.AreEqual(0, result.Version.Build); + var result = await client.Read(Routes.Engine, default); + Assert.AreEqual(sample.EngineVersion, result.EngineVersion); + Assert.AreEqual(-1, result.EngineVersion.Version.Build); + Assert.IsFalse(result.EngineVersion.CustomIteration.HasValue); } [TestMethod] public async Task TestUnrecognizedResponse() { - var sample = new ByondResponse + var sample = new EngineResponse { - Version = new Version(511, 1385) + EngineVersion = new EngineVersion + { + Engine = EngineType.Byond, + Version = new Version(511, 1385) + } }; var fakeJson = "asdfasd <>F#(*)U*#JLI"; @@ -83,12 +94,12 @@ public async Task TestUnrecognizedResponse() new ProductHeaderValue("fake"), new TokenResponse { - Bearer = "fake" + Bearer = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMyIsImV4cCI6IjE2OTkzOTUwNTIiLCJuYmYiOiIxNjk5MzA4NjUyIiwiaXNzIjoiVGdzdGF0aW9uLlNlcnZlci5Ib3N0IiwiYXVkIjoiVGdzdGF0aW9uLlNlcnZlci5BcGkifQ.GRqEd3LRYLkbzk7NHTqcBPX-Xc1vmE_zmbJEDowAXV4" }), null, false); - await Assert.ThrowsExceptionAsync(() => client.Read(Routes.Byond, default).AsTask()); + await Assert.ThrowsExceptionAsync(() => client.Read(Routes.Engine, default).AsTask()); } } } 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 98434096f70..c2c09fe7b87 100644 --- a/tests/Tgstation.Server.Client.Tests/Tgstation.Server.Client.Tests.csproj +++ b/tests/Tgstation.Server.Client.Tests/Tgstation.Server.Client.Tests.csproj @@ -6,7 +6,7 @@ - + diff --git a/tests/Tgstation.Server.Host.Service.Tests/TestServerService.cs b/tests/Tgstation.Server.Host.Service.Tests/TestServerService.cs index 855870d4c82..6faf32f95eb 100644 --- a/tests/Tgstation.Server.Host.Service.Tests/TestServerService.cs +++ b/tests/Tgstation.Server.Host.Service.Tests/TestServerService.cs @@ -39,15 +39,25 @@ public void TestRun() var childStarted = false; ISignalChecker signalChecker = null; - mockWatchdog.Setup(x => x.RunAsync(false, It.IsNotNull(), It.IsAny())).Callback((bool x, string[] _, CancellationToken token) => + var hostVersionTcs = new TaskCompletionSource(); + var hostLifetimeTcs = new TaskCompletionSource(); + + mockWatchdog.Setup(x => x.RunAsync(false, It.IsNotNull(), It.IsAny())).Returns(async (bool x, string[] _, CancellationToken token) => { + hostVersionTcs.SetResult(typeof(ServerService).Assembly.GetName().Version); + cancellationToken = token; + cancellationToken.Register(() => hostLifetimeTcs.SetResult(true)); signalCheckerTask = signalChecker.CheckSignals(additionalArgs => { childStarted = true; - return (123, Task.CompletedTask); + return (123, hostLifetimeTcs.Task); }, cancellationToken).AsTask(); - }).ReturnsAsync(true).Verifiable(); + + await signalCheckerTask; + return true; + }).Verifiable(); + mockWatchdog.SetupGet(x => x.InitialHostVersion).Returns(hostVersionTcs.Task); var mockWatchdogFactory = new Mock(); mockWatchdogFactory.Setup(x => x.CreateWatchdog(It.IsNotNull(), It.IsNotNull())) @@ -68,6 +78,7 @@ public void TestRun() mockWatchdogFactory.VerifyAll(); Assert.IsTrue(signalCheckerTask.IsCompleted); + Assert.IsTrue(cancellationToken.IsCancellationRequested); } } } diff --git a/tests/Tgstation.Server.Host.Tests.Signals/Program.cs b/tests/Tgstation.Server.Host.Tests.Signals/Program.cs index 807fea235a1..92a733e59d2 100644 --- a/tests/Tgstation.Server.Host.Tests.Signals/Program.cs +++ b/tests/Tgstation.Server.Host.Tests.Signals/Program.cs @@ -21,7 +21,7 @@ static async Task Main() var tcs = new TaskCompletionSource(); mockServerControl .Setup(x => x.GracefulShutdown(It.IsAny())) - .Callback(() => tcs.SetResult()) + .Callback(tcs.SetResult) .Returns(ValueTask.CompletedTask); var mockAsyncDelayer = new Mock(); 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 7755d969458..c098cb04d35 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 @@ -1,4 +1,8 @@ + + true + + diff --git a/tests/Tgstation.Server.Host.Tests/Components/Chat/Providers/TestDiscordProvider.cs b/tests/Tgstation.Server.Host.Tests/Components/Chat/Providers/TestDiscordProvider.cs index 210b31447ed..58c63c6a998 100644 --- a/tests/Tgstation.Server.Host.Tests/Components/Chat/Providers/TestDiscordProvider.cs +++ b/tests/Tgstation.Server.Host.Tests/Components/Chat/Providers/TestDiscordProvider.cs @@ -5,9 +5,11 @@ using Microsoft.Extensions.Logging; using Microsoft.VisualStudio.TestTools.UnitTesting; + using Moq; using Tgstation.Server.Api.Models; +using Tgstation.Server.Host.Configuration; using Tgstation.Server.Host.Jobs; using Tgstation.Server.Host.Models; using Tgstation.Server.Host.System; @@ -29,7 +31,8 @@ public static void Initialize(TestContext _) testToken1 = new ChatBot { ConnectionString = actualToken, - ReconnectionInterval = 1 + ReconnectionInterval = 1, + Instance = new Models.Instance() }; var mockSetup = new Mock(); @@ -50,17 +53,20 @@ public async Task TestConstructionAndDisposal() { ConnectionString = "fake_token", ReconnectionInterval = 1, + Instance = new Models.Instance(), }; - Assert.ThrowsException(() => new DiscordProvider(null, null, null, null, null)); - Assert.ThrowsException(() => new DiscordProvider(mockJobManager, null, null, null, null)); + Assert.ThrowsException(() => new DiscordProvider(null, null, null, null, null, null)); + Assert.ThrowsException(() => new DiscordProvider(mockJobManager, null, null, null, null, null)); var mockDel = Mock.Of(); - Assert.ThrowsException(() => new DiscordProvider(mockJobManager, mockDel, null, null, null)); + Assert.ThrowsException(() => new DiscordProvider(mockJobManager, mockDel, null, null, null, null)); var mockLogger = Mock.Of>(); - Assert.ThrowsException(() => new DiscordProvider(mockJobManager, mockDel, mockLogger, null, null)); + Assert.ThrowsException(() => new DiscordProvider(mockJobManager, mockDel, mockLogger, null, null, null)); var mockAss = Mock.Of(); - Assert.ThrowsException(() => new DiscordProvider(mockJobManager, mockDel, mockLogger, mockAss, null)); - await new DiscordProvider(mockJobManager, mockDel, mockLogger, mockAss, bot).DisposeAsync(); + Assert.ThrowsException(() => new DiscordProvider(mockJobManager, mockDel, mockLogger, mockAss, null, null)); + Assert.ThrowsException(() => new DiscordProvider(mockJobManager, mockDel, mockLogger, mockAss, bot, null)); + var mockGen = new GeneralConfiguration(); + await new DiscordProvider(mockJobManager, mockDel, mockLogger, mockAss, bot, mockGen).DisposeAsync(); } static ValueTask InvokeConnect(IProvider provider, CancellationToken cancellationToken = default) => (ValueTask)provider.GetType().GetMethod("Connect", BindingFlags.Instance | BindingFlags.NonPublic).Invoke(provider, new object[] { cancellationToken }); @@ -72,8 +78,9 @@ public async Task TestConnectWithFakeTokenFails() await using var provider = new DiscordProvider(mockJobManager, Mock.Of(), mockLogger.Object, Mock.Of(), new ChatBot { ReconnectionInterval = 1, - ConnectionString = "asdf" - }); + ConnectionString = "asdf", + Instance = new Models.Instance(), + }, new GeneralConfiguration()); await Assert.ThrowsExceptionAsync(async () => await InvokeConnect(provider)); Assert.IsFalse(provider.Connected); } @@ -88,7 +95,7 @@ public async Task TestConnectAndDisconnect() Assert.Fail("TGS_TEST_DISCORD_TOKEN is not a valid Discord connection string!"); var mockLogger = new Mock>(); - await using var provider = new DiscordProvider(mockJobManager, Mock.Of(), mockLogger.Object, Mock.Of(), testToken1); + await using var provider = new DiscordProvider(mockJobManager, Mock.Of(), mockLogger.Object, Mock.Of(), testToken1, new GeneralConfiguration()); Assert.IsFalse(provider.Connected); await InvokeConnect(provider); Assert.IsTrue(provider.Connected); diff --git a/tests/Tgstation.Server.Host.Tests/Components/Chat/Providers/TestIrcProvider.cs b/tests/Tgstation.Server.Host.Tests/Components/Chat/Providers/TestIrcProvider.cs index 9e8565955e8..3f1e0502191 100644 --- a/tests/Tgstation.Server.Host.Tests/Components/Chat/Providers/TestIrcProvider.cs +++ b/tests/Tgstation.Server.Host.Tests/Components/Chat/Providers/TestIrcProvider.cs @@ -2,12 +2,11 @@ using System.Reflection; using System.Threading; using System.Threading.Tasks; -using System.Xml.Linq; using Microsoft.Extensions.Logging; using Microsoft.VisualStudio.TestTools.UnitTesting; + using Moq; -using Serilog.Parsing; using Tgstation.Server.Api.Models; using Tgstation.Server.Host.Jobs; @@ -36,6 +35,7 @@ public async Task TestConstructionAndDisposal() var mockBot = new ChatBot { Name = "test", + Instance = new Models.Instance(), Provider = ChatProvider.Irc }; @@ -82,6 +82,7 @@ public async Task TestConnectAndDisconnect() { ConnectionString = actualToken, Provider = ChatProvider.Irc, + Instance = new Models.Instance(), }); Assert.IsFalse(provider.Connected); await InvokeConnect(provider); diff --git a/tests/Tgstation.Server.Host.Tests/Components/Engine/TestOpenDreamInstaller.cs b/tests/Tgstation.Server.Host.Tests/Components/Engine/TestOpenDreamInstaller.cs new file mode 100644 index 00000000000..2a62fea29ad --- /dev/null +++ b/tests/Tgstation.Server.Host.Tests/Components/Engine/TestOpenDreamInstaller.cs @@ -0,0 +1,97 @@ +using System.Threading; +using System.Threading.Tasks; + +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Options; +using Microsoft.VisualStudio.TestTools.UnitTesting; + +using Moq; + +using Tgstation.Server.Api.Models; +using Tgstation.Server.Api.Models.Internal; +using Tgstation.Server.Common.Http; +using Tgstation.Server.Host.Components.Repository; +using Tgstation.Server.Host.Configuration; +using Tgstation.Server.Host.IO; +using Tgstation.Server.Host.System; +using Tgstation.Server.Host.Utils; + +namespace Tgstation.Server.Host.Components.Engine.Tests +{ + [TestClass] + public sealed class TestOpenDreamInstaller + { + [TestMethod] + public async Task TestDownloadsUseSameRepositoryIfItExists() + { + await RepoDownloadTest(false); + } + + [TestMethod] + public async Task TestDownloadsCloneRepositoryIfItDoesntExists() + { + await RepoDownloadTest(true); + } + + static async Task RepoDownloadTest(bool needsClone) + { + var mockGeneralConfigOptions = new Mock>(); + var generalConfig = new GeneralConfiguration(); + var mockSessionConfigOptions = new Mock>(); + var sessionConfig = new SessionConfiguration(); + Assert.IsNotNull(generalConfig.OpenDreamGitUrl); + mockGeneralConfigOptions.SetupGet(x => x.Value).Returns(generalConfig); + mockSessionConfigOptions.SetupGet(x => x.Value).Returns(sessionConfig); + + var cloneAttempts = 0; + var mockRepository = new Mock(); + mockRepository.Setup(x => x.CommittishIsParent(It.IsNotNull(), It.IsAny())).ReturnsAsync(true); + var mockRepositoryManager = new Mock(); + mockRepositoryManager.Setup(x => x.CloneRepository( + generalConfig.OpenDreamGitUrl, + null, + null, + null, + null, + true, + It.IsAny())) + .Callback(() => ++cloneAttempts) + .ReturnsAsync(needsClone ? mockRepository.Object : null) + .Verifiable(Times.Exactly(1)); + + mockRepositoryManager.Setup(x => x.LoadRepository( + It.IsAny())) + .Returns(() => + { + Assert.AreEqual(1, cloneAttempts); + return ValueTask.FromResult(mockRepository.Object); + }) + .Verifiable(Times.Exactly(needsClone ? 0 : 1)); + + var installer = new OpenDreamInstaller( + Mock.Of(), + Mock.Of>(), + Mock.Of(), + Mock.Of(), + mockRepositoryManager.Object, + Mock.Of(), + Mock.Of(), + mockGeneralConfigOptions.Object, + mockSessionConfigOptions.Object); + + var data = await installer.DownloadVersion( + new EngineVersion + { + Engine = EngineType.OpenDream, + SourceSHA = new string('a', Limits.MaximumCommitShaLength), + }, + null, + CancellationToken.None); + + + Assert.IsNotNull(data); + + mockRepositoryManager.VerifyAll(); + } + } +} diff --git a/tests/Tgstation.Server.Host.Tests/Components/Byond/TestPosixByondInstaller.cs b/tests/Tgstation.Server.Host.Tests/Components/Engine/TestPosixByondInstaller.cs similarity index 74% rename from tests/Tgstation.Server.Host.Tests/Components/Byond/TestPosixByondInstaller.cs rename to tests/Tgstation.Server.Host.Tests/Components/Engine/TestPosixByondInstaller.cs index dccf90d3e07..22cc145683a 100644 --- a/tests/Tgstation.Server.Host.Tests/Components/Byond/TestPosixByondInstaller.cs +++ b/tests/Tgstation.Server.Host.Tests/Components/Engine/TestPosixByondInstaller.cs @@ -4,11 +4,14 @@ using System; using System.IO; using System.Linq; +using System.Reflection; using System.Threading.Tasks; +using Tgstation.Server.Api.Models; +using Tgstation.Server.Api.Models.Internal; using Tgstation.Server.Host.IO; -namespace Tgstation.Server.Host.Components.Byond.Tests +namespace Tgstation.Server.Host.Components.Engine.Tests { [TestClass] public sealed class TestPosixByondInstaller @@ -49,7 +52,7 @@ public async Task TestDownload() var mockFileDownloader = new Mock(); var installer = new PosixByondInstaller(mockPostWriteHandler.Object, mockIOManager.Object, mockFileDownloader.Object, mockLogger.Object); - await Assert.ThrowsExceptionAsync(() => installer.DownloadVersion(null, default).AsTask()); + await Assert.ThrowsExceptionAsync(() => installer.DownloadVersion(null, null, default).AsTask()); var ourArray = Array.Empty(); mockFileDownloader @@ -62,11 +65,20 @@ public async Task TestDownload() new MemoryStream(ourArray))) .Verifiable(); - var result = await installer.DownloadVersion(new Version(511, 1385), default); + var result = ExtractMemoryStreamFromInstallationData(await installer.DownloadVersion(new EngineVersion + { + Engine = EngineType.Byond, + Version = new Version(511, 1385), + }, null, default)); Assert.IsTrue(ourArray.SequenceEqual(result.ToArray())); mockIOManager.Verify(); } + static MemoryStream ExtractMemoryStreamFromInstallationData(IEngineInstallationData engineInstallationData) + { + var zipStreamData = (ZipStreamEngineInstallationData)engineInstallationData; + return (MemoryStream)zipStreamData.GetType().GetField("zipStream", BindingFlags.NonPublic | BindingFlags.Instance).GetValue(zipStreamData); + } [TestMethod] public async Task TestInstallByond() @@ -78,10 +90,18 @@ public async Task TestInstallByond() var installer = new PosixByondInstaller(mockPostWriteHandler.Object, mockIOManager.Object, mockFileDownloader, mockLogger.Object); const string FakePath = "fake"; - await Assert.ThrowsExceptionAsync(() => installer.InstallByond(null, null, default).AsTask()); - await Assert.ThrowsExceptionAsync(() => installer.InstallByond(new Version(123,252345), null, default).AsTask()); + await Assert.ThrowsExceptionAsync(() => installer.Install(null, null, default).AsTask()); + + var byondVersion = new EngineVersion + { + Engine = EngineType.Byond, + Version = new Version(123, 252345), + }; + + await Assert.ThrowsExceptionAsync(() => installer.Install(byondVersion, null, default).AsTask()); - await installer.InstallByond(new Version(511, 1385), FakePath, default); + byondVersion.Version = new Version(511, 1385); + await installer.Install(byondVersion, FakePath, default); mockPostWriteHandler.Verify(x => x.HandleWrite(It.IsAny()), Times.Exactly(4)); } diff --git a/tests/Tgstation.Server.Host.Tests/Components/Events/TestEventScriptAttribute.cs b/tests/Tgstation.Server.Host.Tests/Components/Events/TestEventScriptAttribute.cs index 4f9e4501503..232f8767d65 100644 --- a/tests/Tgstation.Server.Host.Tests/Components/Events/TestEventScriptAttribute.cs +++ b/tests/Tgstation.Server.Host.Tests/Components/Events/TestEventScriptAttribute.cs @@ -1,5 +1,6 @@ using Microsoft.VisualStudio.TestTools.UnitTesting; using System; +using System.Linq; namespace Tgstation.Server.Host.Components.Events.Tests @@ -14,8 +15,8 @@ public sealed class TestEventScriptAttribute public void TestConstruction() { Assert.ThrowsException(() => new EventScriptAttribute(null)); - var test = new EventScriptAttribute("test"); - Assert.AreEqual("test", test.ScriptName); + var test = new EventScriptAttribute("test1", "test2"); + Assert.IsTrue(test.ScriptNames.SequenceEqual(["test1", "test2"])); } } } diff --git a/tests/Tgstation.Server.Host.Tests/Components/Events/TestEventType.cs b/tests/Tgstation.Server.Host.Tests/Components/Events/TestEventType.cs index 9c1a467160e..fcd032f60da 100644 --- a/tests/Tgstation.Server.Host.Tests/Components/Events/TestEventType.cs +++ b/tests/Tgstation.Server.Host.Tests/Components/Events/TestEventType.cs @@ -22,7 +22,8 @@ public void TestAllEventTypesHaveUniqueEventScriptAttributes() Assert.AreEqual(1, list.Count, $"EventType: {eventType}"); var attribute = list.First(); - Assert.IsTrue(allScripts.Add(attribute.ScriptName), $"Non-unique script Name: {attribute.ScriptName}"); + foreach (var scriptName in attribute.ScriptNames) + Assert.IsTrue(allScripts.Add(scriptName), $"Non-unique script Names: {scriptName}"); } } } diff --git a/tests/Tgstation.Server.Host.Tests/Components/Repository/TestRepositoryFactory.cs b/tests/Tgstation.Server.Host.Tests/Components/Repository/TestRepositoryFactory.cs index bbf369efcea..04e7cdb66bd 100644 --- a/tests/Tgstation.Server.Host.Tests/Components/Repository/TestRepositoryFactory.cs +++ b/tests/Tgstation.Server.Host.Tests/Components/Repository/TestRepositoryFactory.cs @@ -1,4 +1,4 @@ -using LibGit2Sharp; +using LibGit2Sharp; using Microsoft.Extensions.Logging; using Microsoft.VisualStudio.TestTools.UnitTesting; using Moq; @@ -39,12 +39,11 @@ public async Task TestCloning() try { var factory = CreateFactory(); + var cloneOpts = new CloneOptions(); + cloneOpts.FetchOptions.CredentialsProvider = factory.GenerateCredentialsHandler(null, null); await factory.Clone( new Uri("https://github.com/Cyberboss/Test"), - new CloneOptions - { - CredentialsProvider = factory.GenerateCredentialsHandler(null, null) - }, + cloneOpts, tempDir, default); diff --git a/tests/Tgstation.Server.Host.Tests/Components/Repository/TestRepositoryManager.cs b/tests/Tgstation.Server.Host.Tests/Components/Repository/TestRepositoryManager.cs new file mode 100644 index 00000000000..c4a17f51401 --- /dev/null +++ b/tests/Tgstation.Server.Host.Tests/Components/Repository/TestRepositoryManager.cs @@ -0,0 +1,133 @@ +using System; +using System.IO.Pipelines; +using System.Threading; +using System.Threading.Tasks; + +using LibGit2Sharp; + +using Microsoft.Extensions.Logging; +using Microsoft.VisualStudio.TestTools.UnitTesting; + +using Moq; + +using Remora.Rest.Core; + +using Tgstation.Server.Host.Components.Events; +using Tgstation.Server.Host.Configuration; +using Tgstation.Server.Host.IO; + +namespace Tgstation.Server.Host.Components.Repository.Tests +{ + [TestClass] + public sealed class TestRepositoryManager + { + const string TestRepoPath = "adfiwurjwhouerfiunfjdfn"; + + RepositoryManager repositoryManager; + Mock mockRepoFactory; + Mock mockCommands; + Mock mockIOManager; + Mock mockGitRemoteFeaturesFactory; + + [TestInitialize] + public void Initialize() + { + mockIOManager = new Mock(); + mockRepoFactory = new Mock(); + mockCommands = new Mock(); + mockGitRemoteFeaturesFactory = new Mock(); + + mockIOManager.Setup(x => x.ResolvePath()).Returns(TestRepoPath); + + repositoryManager = new RepositoryManager( + mockRepoFactory.Object, + mockCommands.Object, + mockIOManager.Object, + Mock.Of(), + Mock.Of(), + mockGitRemoteFeaturesFactory.Object, + Mock.Of>(), + Mock.Of>(), + new GeneralConfiguration()); + } + + [TestCleanup] + public void TestCleanup() => repositoryManager.Dispose(); + + [TestMethod] + public async Task TestCloneAbortsIfRepoExists() + { + mockIOManager.Setup(x => x.DirectoryExists(TestRepoPath, It.IsAny())).ReturnsAsync(true).Verifiable(); + + using var cloneResult = await repositoryManager.CloneRepository( + new Uri("https://github.com/Cyberboss/common_core"), + null, + null, + null, + null, + false, + CancellationToken.None); + + Assert.IsNull(cloneResult); + + mockIOManager.VerifyAll(); + } + + [TestMethod] + public async Task TestBasicClone() + { + mockIOManager.Setup(x => x.DirectoryExists(TestRepoPath, It.IsAny())).ReturnsAsync(false).Verifiable(); + var mockRepo = new Mock(); + + mockRepoFactory.Setup(x => x.CreateFromPath(TestRepoPath, It.IsAny())).ReturnsAsync(mockRepo.Object).Verifiable(); + + using var cloneResult = await repositoryManager.CloneRepository( + new Uri("https://github.com/Cyberboss/common_core"), + null, + null, + null, + null, + false, + CancellationToken.None); + + Assert.IsNotNull(cloneResult); + + mockRepoFactory.VerifyAll(); + mockIOManager.VerifyAll(); + } + + [TestMethod] + public async Task TestBasicLoad() + { + mockIOManager.Setup(x => x.DirectoryExists(TestRepoPath, It.IsAny())).ReturnsAsync(false).Verifiable(Times.Never); + var mockRepo = new Mock(); + + mockRepoFactory.Setup(x => x.CreateFromPath(TestRepoPath, It.IsAny())).ReturnsAsync(mockRepo.Object).Verifiable(); + + using var loadResult = await repositoryManager.LoadRepository( + CancellationToken.None); + + Assert.IsNotNull(loadResult); + + mockRepoFactory.VerifyAll(); + mockIOManager.VerifyAll(); + } + + [TestMethod] + public async Task TestLoadFailsIfRepoDoesntExist() + { + mockIOManager.Setup(x => x.DirectoryExists(TestRepoPath, It.IsAny())).ReturnsAsync(false).Verifiable(Times.Never); + var mockRepo = new Mock(); + + mockRepoFactory.Setup(x => x.CreateFromPath(TestRepoPath, It.IsAny())).ThrowsAsync(new RepositoryNotFoundException()).Verifiable(); + + using var loadResult = await repositoryManager.LoadRepository( + CancellationToken.None); + + Assert.IsNull(loadResult); + + mockRepoFactory.VerifyAll(); + mockIOManager.VerifyAll(); + } + } +} diff --git a/tests/Tgstation.Server.Host.Tests/Jobs/TestJobsHubGroupMapper.cs b/tests/Tgstation.Server.Host.Tests/Jobs/TestJobsHubGroupMapper.cs index b00d2af4e83..92be81d6588 100644 --- a/tests/Tgstation.Server.Host.Tests/Jobs/TestJobsHubGroupMapper.cs +++ b/tests/Tgstation.Server.Host.Tests/Jobs/TestJobsHubGroupMapper.cs @@ -47,7 +47,7 @@ public async Task TestGroupMapping() }; var testIps1 = new InstancePermissionSet { - ByondRights = RightsHelper.AllRights(), + EngineRights = RightsHelper.AllRights(), ChatBotRights = RightsHelper.AllRights(), ConfigurationRights = RightsHelper.AllRights(), DreamDaemonRights = RightsHelper.AllRights(), @@ -62,7 +62,7 @@ public async Task TestGroupMapping() var testIps2 = new InstancePermissionSet { - ByondRights = RightsHelper.AllRights(), + EngineRights = RightsHelper.AllRights(), ChatBotRights = RightsHelper.AllRights(), ConfigurationRights = RightsHelper.AllRights(), DreamDaemonRights = RightsHelper.AllRights(), diff --git a/tests/Tgstation.Server.Host.Tests/Security/TestAuthenticationContext.cs b/tests/Tgstation.Server.Host.Tests/Security/TestAuthenticationContext.cs index 9145401b347..1e0d7b7fa8f 100644 --- a/tests/Tgstation.Server.Host.Tests/Security/TestAuthenticationContext.cs +++ b/tests/Tgstation.Server.Host.Tests/Security/TestAuthenticationContext.cs @@ -51,9 +51,9 @@ public void TestGetRightsGeneric() authContext.Initialize(null, user, instanceUser); user.PermissionSet.AdministrationRights = AdministrationRights.WriteUsers; - instanceUser.ByondRights = ByondRights.InstallOfficialOrChangeActiveVersion | ByondRights.ReadActive; + instanceUser.EngineRights = EngineRights.InstallOfficialOrChangeActiveByondVersion | EngineRights.ReadActive; Assert.AreEqual((ulong)user.PermissionSet.AdministrationRights, authContext.GetRight(RightsType.Administration)); - Assert.AreEqual((ulong)instanceUser.ByondRights, authContext.GetRight(RightsType.Byond)); + Assert.AreEqual((ulong)instanceUser.EngineRights, authContext.GetRight(RightsType.Engine)); } } } diff --git a/tests/Tgstation.Server.Host.Tests/System/TestPosixSignalHandler.cs b/tests/Tgstation.Server.Host.Tests/System/TestPosixSignalHandler.cs index d70881cdabe..083657a0753 100644 --- a/tests/Tgstation.Server.Host.Tests/System/TestPosixSignalHandler.cs +++ b/tests/Tgstation.Server.Host.Tests/System/TestPosixSignalHandler.cs @@ -54,7 +54,7 @@ public async Task TestSignalListening() builder.SetMinimumLevel(LogLevel.Trace); }); - IProcessExecutor processExecutor = null; + ProcessExecutor processExecutor = null; processExecutor = new ProcessExecutor( new PosixProcessFeatures( new Lazy(() => processExecutor), 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 f0f88a74b57..331d95e564d 100644 --- a/tests/Tgstation.Server.Host.Tests/Tgstation.Server.Host.Tests.csproj +++ b/tests/Tgstation.Server.Host.Tests/Tgstation.Server.Host.Tests.csproj @@ -6,7 +6,7 @@ - + diff --git a/tests/Tgstation.Server.Host.Tests/Utils/TestAsyncDelayer.cs b/tests/Tgstation.Server.Host.Tests/Utils/TestAsyncDelayer.cs index 2760e9a4fe6..fcad8785c5c 100644 --- a/tests/Tgstation.Server.Host.Tests/Utils/TestAsyncDelayer.cs +++ b/tests/Tgstation.Server.Host.Tests/Utils/TestAsyncDelayer.cs @@ -13,8 +13,8 @@ public sealed class TestAsyncDelayer public async Task TestDelay() { var delayer = new AsyncDelayer(); - var startDelay = delayer.Delay(TimeSpan.FromSeconds(1), default); - var checkDelay = Task.Delay(TimeSpan.FromSeconds(1) - TimeSpan.FromMilliseconds(100), default); + var startDelay = delayer.Delay(TimeSpan.FromSeconds(1), CancellationToken.None); + var checkDelay = Task.Delay(TimeSpan.FromSeconds(1) - TimeSpan.FromMilliseconds(100), CancellationToken.None); await startDelay; Assert.IsTrue(checkDelay.IsCompleted); } diff --git a/tests/Tgstation.Server.Tests/CachingFileDownloader.cs b/tests/Tgstation.Server.Tests/CachingFileDownloader.cs index 17dcbbfaf59..de540b606d6 100644 --- a/tests/Tgstation.Server.Tests/CachingFileDownloader.cs +++ b/tests/Tgstation.Server.Tests/CachingFileDownloader.cs @@ -9,6 +9,8 @@ using Moq; +using Tgstation.Server.Api.Models; +using Tgstation.Server.Api.Models.Internal; using Tgstation.Server.Common.Http; using Tgstation.Server.Host.Extensions; using Tgstation.Server.Host.IO; @@ -42,9 +44,9 @@ public static async Task InitializeAndInjectForLiveTests(CancellationToken cance var logger = loggerFactory.CreateLogger("CachingFileDownloader"); var cfd = new CachingFileDownloader(loggerFactory.CreateLogger()); - var edgeVersion = await ByondTest.GetEdgeVersion(cfd, cancellationToken); + var edgeVersion = await EngineTest.GetEdgeVersion(Api.Models.EngineType.Byond, cfd, cancellationToken); - await InitializeByondVersion(logger, edgeVersion, new PlatformIdentifier().IsWindows, cancellationToken); + await InitializeByondVersion(logger, edgeVersion.Version, new PlatformIdentifier().IsWindows, cancellationToken); // predownload the target github release update asset var gitHubToken = Environment.GetEnvironmentVariable("TGS_TEST_GITHUB_TOKEN"); @@ -76,22 +78,30 @@ public static async Task InitializeAndInjectForLiveTests(CancellationToken cance ServiceCollectionExtensions.UseFileDownloader(); } - public static async ValueTask InitializeByondVersion(ILogger logger, Version version, bool windows, CancellationToken cancellationToken) + public static async ValueTask InitializeByondVersion(ILogger logger, Version byondVersion, bool windows, CancellationToken cancellationToken) { + var version = new EngineVersion + { + Engine = Api.Models.EngineType.Byond, + Version = byondVersion, + }; + var url = new Uri( - $"https://www.byond.com/download/build/{version.Major}/{version.Major}.{version.Minor}_byond{(!windows ? "_linux" : string.Empty)}.zip"); + $"https://www.byond.com/download/build/{version.Version.Major}/{version.Version.Major}.{version.Version.Minor}_byond{(!windows ? "_linux" : string.Empty)}.zip"); string path = null; if (TestingUtils.RunningInGitHubActions) { // actions is supposed to cache BYOND for us var dir = Path.Combine( - Environment.GetFolderPath(Environment.SpecialFolder.UserProfile), + Environment.GetFolderPath( + Environment.SpecialFolder.UserProfile, + Environment.SpecialFolderOption.DoNotVerify), "byond-zips-cache", windows ? "windows" : "linux"); path = Path.Combine( dir, - $"{version.Major}.{version.Minor}.zip"); + $"{version.Version.Major}.{version.Version.Minor}.zip"); } await (await CacheFile(logger, url, null, path, cancellationToken)).DisposeAsync(); diff --git a/tests/Tgstation.Server.Tests/Live/DummyChatProvider.cs b/tests/Tgstation.Server.Tests/Live/DummyChatProvider.cs index 4f1ddf73873..872e59e6e65 100644 --- a/tests/Tgstation.Server.Tests/Live/DummyChatProvider.cs +++ b/tests/Tgstation.Server.Tests/Live/DummyChatProvider.cs @@ -44,6 +44,8 @@ sealed class DummyChatProvider : Provider ulong channelIdAllocator; + public static Task MessageGuard = Task.CompletedTask; + static IAsyncDelayer CreateMockDelayer() { // at time of writing, this is used exclusively for the reconnection interval which works in minutes @@ -52,10 +54,11 @@ static IAsyncDelayer CreateMockDelayer() mock.Setup(x => x.Delay(It.IsAny(), It.IsAny())).Returns((delay, cancellationToken) => Task.Delay(TimeSpan.FromSeconds(3), cancellationToken)); return mock.Object; } - public static async Task RandomDisconnections(bool enabled, CancellationToken cancellationToken) + + public static void RandomDisconnections(bool enabled) { - if (Interlocked.Exchange(ref enableRandomDisconnections, enabled ? 1 : 0) != 0 && !enabled) - await Task.Delay(TimeSpan.FromSeconds(5), cancellationToken); + // we just don't do random disconnections when live testing these days, too many potential issue vectors like thread exhaustion on actions runners + enableRandomDisconnections = enabled ? 1 : 0; } public DummyChatProvider( @@ -100,10 +103,18 @@ public override ValueTask SendMessage(Message replyTo, MessageContent message, u return ValueTask.CompletedTask; } - public override ValueTask>>> SendUpdateMessage(RevisionInformation revisionInformation, Version byondVersion, DateTimeOffset? estimatedCompletionTime, string gitHubOwner, string gitHubRepo, ulong channelId, bool localCommitPushed, CancellationToken cancellationToken) + public override ValueTask>>> SendUpdateMessage( + RevisionInformation revisionInformation, + Api.Models.EngineVersion engineVersion, + DateTimeOffset? estimatedCompletionTime, + string gitHubOwner, + string gitHubRepo, + ulong channelId, + bool localCommitPushed, + CancellationToken cancellationToken) { ArgumentNullException.ThrowIfNull(revisionInformation); - ArgumentNullException.ThrowIfNull(byondVersion); + ArgumentNullException.ThrowIfNull(engineVersion); ArgumentNullException.ThrowIfNull(gitHubOwner); ArgumentNullException.ThrowIfNull(gitHubRepo); @@ -179,14 +190,14 @@ private ChannelRepresentation CreateChannel(ChatChannel channel) else channelId = (ulong)channel.IrcChannel.GetHashCode(); - var entry = new ChannelRepresentation + var entry = new ChannelRepresentation( + $"Connection_{channelId}", + $"(Friendly) Channel_ID_{channelId}", + channelId) { IsAdminChannel = channel.IsAdminChannel.Value, - ConnectionName = $"Connection_{channelId}", EmbedsSupported = ChatBot.Provider.Value != Api.Models.ChatProvider.Irc, - FriendlyName = $"(Friendly) Channel_ID_{channelId}", IsPrivateChannel = false, - RealId = channelId, Tag = channel.Tag, }; @@ -210,6 +221,8 @@ async Task RandomMessageLoop(CancellationToken cancellationToken) var delay = random.Next(0, 10000); await Task.Delay(delay, cancellationToken); + await MessageGuard; + // %5 chance to disconnect randomly if (enableRandomDisconnections != 0 && random.Next(0, 100) > 95) connected = false; @@ -244,12 +257,9 @@ async Task RandomMessageLoop(CancellationToken cancellationToken) } while (knownChannels.ContainsKey(channelId)); - channel = new ChannelRepresentation + channel = new ChannelRepresentation($"{username}_Connection", $"{username}_Channel", channelId) { - RealId = channelId, IsPrivateChannel = true, - ConnectionName = $"{username}_Connection", - FriendlyName = $"{username}_Channel", EmbedsSupported = ChatBot.Provider.Value != Api.Models.ChatProvider.Irc, // isAdmin and Tag populated by manager @@ -269,13 +279,11 @@ async Task RandomMessageLoop(CancellationToken cancellationToken) channel = enumerator[index].Value; } - var sender = new ChatUser - { - Channel = CloneChannel(channel), - FriendlyName = username, - RealId = i + 50000, - Mention = $"@{username}", - }; + var sender = new ChatUser( + CloneChannel(channel), + username, + $"@{username}", + i + 50000); var dice = random.Next(0, 100); string content; @@ -305,13 +313,8 @@ async Task RandomMessageLoop(CancellationToken cancellationToken) else content = $"{content} embeds_test"; // NEVER send the response_overload_test, it causes so much havoc in CI and we test it manually - EnqueueMessage(new Message - { - Content = content, - User = sender, - }); + EnqueueMessage(new Message(sender, content)); } - } catch (OperationCanceledException) { diff --git a/tests/Tgstation.Server.Tests/Live/DummyChatProviderFactory.cs b/tests/Tgstation.Server.Tests/Live/DummyChatProviderFactory.cs index d4d7c98b09b..fe6746cb73f 100644 --- a/tests/Tgstation.Server.Tests/Live/DummyChatProviderFactory.cs +++ b/tests/Tgstation.Server.Tests/Live/DummyChatProviderFactory.cs @@ -6,7 +6,7 @@ using Moq; using Tgstation.Server.Api.Models; -using Tgstation.Server.Host.Components.Byond; +using Tgstation.Server.Host.Components.Engine; using Tgstation.Server.Host.Components.Chat.Commands; using Tgstation.Server.Host.Components.Chat.Providers; using Tgstation.Server.Host.Components.Deployment; @@ -40,7 +40,7 @@ public DummyChatProviderFactory(IJobManager jobManager, ICryptographySuite crypt var commandFactory = new CommandFactory( Mock.Of(), - Mock.Of(), + Mock.Of(), Mock.Of(), Mock.Of(), Mock.Of(), @@ -55,7 +55,7 @@ public DummyChatProviderFactory(IJobManager jobManager, ICryptographySuite crypt ? new Random().Next() : 22475; - logger.LogInformation("Random seed: {0}", randomSeed); + logger.LogInformation("Random seed: {randomSeed}", randomSeed); var baseRng = new Random(randomSeed); seededRng = new Dictionary{ diff --git a/tests/Tgstation.Server.Tests/Live/DummyGitHubServiceFactory.cs b/tests/Tgstation.Server.Tests/Live/DummyGitHubServiceFactory.cs index 7df388085ea..2f657577820 100644 --- a/tests/Tgstation.Server.Tests/Live/DummyGitHubServiceFactory.cs +++ b/tests/Tgstation.Server.Tests/Live/DummyGitHubServiceFactory.cs @@ -10,9 +10,9 @@ namespace Tgstation.Server.Tests.Live sealed class DummyGitHubServiceFactory : IGitHubServiceFactory { readonly ICryptographySuite cryptographySuite; - readonly ILogger logger; + readonly ILogger logger; - public DummyGitHubServiceFactory(ICryptographySuite cryptographySuite, ILogger logger) + public DummyGitHubServiceFactory(ICryptographySuite cryptographySuite, ILogger logger) { this.cryptographySuite = cryptographySuite ?? throw new ArgumentNullException(nameof(cryptographySuite)); this.logger = logger ?? throw new ArgumentNullException(nameof(logger)); @@ -27,6 +27,6 @@ public IAuthenticatedGitHubService CreateService(string accessToken) return CreateDummyService(); } - DummyGitHubService CreateDummyService() => new DummyGitHubService(cryptographySuite, logger); + TestingGitHubService CreateDummyService() => new TestingGitHubService(cryptographySuite, logger); } } diff --git a/tests/Tgstation.Server.Tests/Live/Instance/ByondTest.cs b/tests/Tgstation.Server.Tests/Live/Instance/ByondTest.cs deleted file mode 100644 index ff86ed6db2b..00000000000 --- a/tests/Tgstation.Server.Tests/Live/Instance/ByondTest.cs +++ /dev/null @@ -1,269 +0,0 @@ -using System; -using System.Collections.Generic; -using System.IO; -using System.Linq; -using System.Text; -using System.Threading; -using System.Threading.Tasks; - -using Microsoft.Extensions.Logging; -using Microsoft.Extensions.Options; -using Microsoft.VisualStudio.TestTools.UnitTesting; - -using Moq; - -using Tgstation.Server.Api.Models; -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.Byond; -using Tgstation.Server.Host.Configuration; -using Tgstation.Server.Host.IO; -using Tgstation.Server.Host.System; - -namespace Tgstation.Server.Tests.Live.Instance -{ - sealed class ByondTest : JobsRequiredTest - { - readonly IByondClient byondClient; - readonly IFileDownloader fileDownloader; - - readonly Api.Models.Instance metadata; - - static Version edgeVersion; - - Version testVersion; - - public ByondTest(IByondClient byondClient, IJobsClient jobsClient, IFileDownloader fileDownloader, Api.Models.Instance metadata) - : base(jobsClient) - { - this.byondClient = byondClient ?? throw new ArgumentNullException(nameof(byondClient)); - this.fileDownloader = fileDownloader ?? throw new ArgumentNullException(nameof(fileDownloader)); - this.metadata = metadata ?? throw new ArgumentNullException(nameof(metadata)); - } - - public Task Run(CancellationToken cancellationToken, out Task firstInstall) - { - firstInstall = RunPartOne(cancellationToken); - return RunContinued(firstInstall, cancellationToken); - } - - public static async Task GetEdgeVersion(IFileDownloader fileDownloader, CancellationToken cancellationToken) - { - if (edgeVersion != null) - return edgeVersion; - - await using var provider = fileDownloader.DownloadFile(new Uri("https://www.byond.com/download/version.txt"), null); - var stream = await provider.GetResult(cancellationToken); - using var reader = new StreamReader(stream, Encoding.UTF8, false, -1, true); - var text = await reader.ReadToEndAsync(); - var splits = text.Split('\n', StringSplitOptions.TrimEntries | StringSplitOptions.RemoveEmptyEntries); - - var targetVersion = splits.Last(); - - var badVersionMap = new PlatformIdentifier().IsWindows - ? new Dictionary() - { - } - // linux map also needs updating in CI - : new Dictionary() - { - { "515.1612", "515.1611" } - }; - - badVersionMap.Add("515.1617", "515.1616"); - - if (badVersionMap.TryGetValue(targetVersion, out var remappedVersion)) - targetVersion = remappedVersion; - - return edgeVersion = Version.Parse(targetVersion); - } - - async Task RunPartOne(CancellationToken cancellationToken) - { - testVersion = await GetEdgeVersion(fileDownloader, cancellationToken); - await TestNoVersion(cancellationToken); - await TestInstallStable(cancellationToken); - } - - async Task RunContinued(Task firstInstall, CancellationToken cancellationToken) - { - await firstInstall; - await TestInstallFakeVersion(cancellationToken); - await TestCustomInstalls(cancellationToken); - await TestDeletes(cancellationToken); - } - - async Task TestDeletes(CancellationToken cancellationToken) - { - var deleteThisOneBecauseItWasntPartOfTheOriginalTest = await byondClient.DeleteVersion(new ByondVersionDeleteRequest - { - Version = new(testVersion.Major, testVersion.Minor, 2) - }, cancellationToken); - await WaitForJob(deleteThisOneBecauseItWasntPartOfTheOriginalTest, 30, false, null, cancellationToken); - - var nonExistentUninstallResponseTask = ApiAssert.ThrowsException(() => byondClient.DeleteVersion( - new ByondVersionDeleteRequest - { - Version = new(509, 1000) - }, - cancellationToken), ErrorCode.ResourceNotPresent); - - var uninstallResponseTask = byondClient.DeleteVersion( - new ByondVersionDeleteRequest - { - Version = testVersion - }, - cancellationToken); - - var badBecauseActiveResponseTask = ApiAssert.ThrowsException(() => byondClient.DeleteVersion( - new ByondVersionDeleteRequest - { - Version = new(testVersion.Major, testVersion.Minor, 1) - }, - cancellationToken), ErrorCode.ByondCannotDeleteActiveVersion); - - await badBecauseActiveResponseTask; - - var uninstallJob = await uninstallResponseTask; - Assert.IsNotNull(uninstallJob); - - // Has to wait on deployment test possibly - var uninstallTask = WaitForJob(uninstallJob, 120, false, null, cancellationToken); - - await nonExistentUninstallResponseTask; - - await uninstallTask; - var byondDir = Path.Combine(metadata.Path, "Byond", testVersion.ToString()); - Assert.IsFalse(Directory.Exists(byondDir)); - - var newVersions = await byondClient.InstalledVersions(null, cancellationToken); - Assert.IsNotNull(newVersions); - Assert.AreEqual(1, newVersions.Count); - Assert.AreEqual(new Version(testVersion.Major, testVersion.Minor, 1), newVersions[0].Version); - } - - async Task TestInstallFakeVersion(CancellationToken cancellationToken) - { - var newModel = new ByondVersionRequest - { - Version = new Version(5011, 1385) - }; - var test = await byondClient.SetActiveVersion(newModel, null, cancellationToken); - Assert.IsNotNull(test.InstallJob); - await WaitForJob(test.InstallJob, 60, true, ErrorCode.ByondDownloadFail, cancellationToken); - } - - async Task TestInstallStable(CancellationToken cancellationToken) - { - var newModel = new ByondVersionRequest - { - Version = testVersion - }; - var test = await byondClient.SetActiveVersion(newModel, null, cancellationToken); - Assert.IsNotNull(test.InstallJob); - await WaitForJob(test.InstallJob, 180, false, null, cancellationToken); - var currentShit = await byondClient.ActiveVersion(cancellationToken); - Assert.AreEqual(newModel.Version.Semver(), currentShit.Version); - - var dreamMaker = "DreamMaker"; - if (new PlatformIdentifier().IsWindows) - dreamMaker += ".exe"; - - var dreamMakerDir = Path.Combine(metadata.Path, "Byond", newModel.Version.ToString(), "byond", "bin"); - - Assert.IsTrue(Directory.Exists(dreamMakerDir), $"Directory {dreamMakerDir} does not exist!"); - Assert.IsTrue( - File.Exists( - Path.Combine(dreamMakerDir, dreamMaker)), - $"Missing DreamMaker executable! Dir contents: {string.Join(", ", Directory.GetFileSystemEntries(dreamMakerDir))}"); - } - - async Task TestNoVersion(CancellationToken cancellationToken) - { - var allVersionsTask = byondClient.InstalledVersions(null, cancellationToken); - var currentShit = await byondClient.ActiveVersion(cancellationToken); - Assert.IsNotNull(currentShit); - Assert.IsNull(currentShit.Version); - var otherShit = await allVersionsTask; - Assert.IsNotNull(otherShit); - Assert.AreEqual(0, otherShit.Count); - } - - async Task TestCustomInstalls(CancellationToken cancellationToken) - { - var generalConfigOptionsMock = new Mock>(); - generalConfigOptionsMock.SetupGet(x => x.Value).Returns(new GeneralConfiguration()); - var sessionConfigOptionsMock = new Mock>(); - sessionConfigOptionsMock.SetupGet(x => x.Value).Returns(new SessionConfiguration()); - - var assemblyInformationProvider = new AssemblyInformationProvider(); - - IByondInstaller byondInstaller = new PlatformIdentifier().IsWindows - ? new WindowsByondInstaller( - Mock.Of(), - Mock.Of(), - fileDownloader, - generalConfigOptionsMock.Object, - Mock.Of>()) - : new PosixByondInstaller( - Mock.Of(), - Mock.Of(), - fileDownloader, - Mock.Of>()); - - using var windowsByondInstaller = byondInstaller as WindowsByondInstaller; - - // get the bytes for stable - using var stableBytesMs = await byondInstaller.DownloadVersion(testVersion, cancellationToken); - - var test = await byondClient.SetActiveVersion( - new ByondVersionRequest - { - Version = testVersion, - UploadCustomZip = true - }, - stableBytesMs, - cancellationToken); - - Assert.IsNotNull(test.InstallJob); - await WaitForJob(test.InstallJob, 30, false, null, cancellationToken); - - // do it again. #1501 - stableBytesMs.Seek(0, SeekOrigin.Begin); - var test2 = await byondClient.SetActiveVersion( - new ByondVersionRequest - { - Version = testVersion, - UploadCustomZip = true - }, - stableBytesMs, - cancellationToken); - - Assert.IsNotNull(test2.InstallJob); - await WaitForJob(test2.InstallJob, 30, false, null, cancellationToken); - - var newSettings = await byondClient.ActiveVersion(cancellationToken); - Assert.AreEqual(new Version(testVersion.Major, testVersion.Minor, 2), newSettings.Version); - - // test a few switches - var installResponse = await byondClient.SetActiveVersion(new ByondVersionRequest - { - Version = testVersion - }, null, cancellationToken); - Assert.IsNull(installResponse.InstallJob); - await ApiAssert.ThrowsException(() => byondClient.SetActiveVersion(new ByondVersionRequest - { - Version = new Version(testVersion.Major, testVersion.Minor, 3) - }, null, cancellationToken), ErrorCode.ByondNonExistentCustomVersion); - - installResponse = await byondClient.SetActiveVersion(new ByondVersionRequest - { - Version = new Version(testVersion.Major, testVersion.Minor, 1) - }, null, cancellationToken); - Assert.IsNull(installResponse.InstallJob); - } - } -} diff --git a/tests/Tgstation.Server.Tests/Live/Instance/ChatTest.cs b/tests/Tgstation.Server.Tests/Live/Instance/ChatTest.cs index 3162ceb5889..abb699815e4 100644 --- a/tests/Tgstation.Server.Tests/Live/Instance/ChatTest.cs +++ b/tests/Tgstation.Server.Tests/Live/Instance/ChatTest.cs @@ -3,6 +3,7 @@ using System.Linq; using System.Threading; using System.Threading.Tasks; + using Microsoft.VisualStudio.TestTools.UnitTesting; using Tgstation.Server.Api.Models; @@ -122,9 +123,6 @@ async Task RunIrc(CancellationToken cancellationToken) IsSystemChannel = true, Tag = "butt2", ChannelData = channelId, -#pragma warning disable CS0618 - IrcChannel = "should_not_be_this!!!JHF*WW(#*(*$&(#*@))(" -#pragma warning restore CS0618 } } }, cancellationToken); @@ -137,10 +135,6 @@ async Task RunIrc(CancellationToken cancellationToken) Assert.AreEqual(true, updatedBot.Channels.First().IsUpdatesChannel); Assert.AreEqual(true, updatedBot.Channels.First().IsWatchdogChannel); Assert.AreEqual("butt2", updatedBot.Channels.First().Tag); -#pragma warning disable CS0618 - Assert.AreEqual(channelId, updatedBot.Channels.First().IrcChannel); - Assert.IsNull(updatedBot.Channels.First().DiscordChannelId); -#pragma warning restore CS0618 Assert.AreEqual(channelId, updatedBot.Channels.First().ChannelData); } @@ -224,9 +218,6 @@ async Task RunDiscord(CancellationToken cancellationToken) IsSystemChannel = true, Tag = "butt", ChannelData = channelId.ToString(), -#pragma warning disable CS0618 - DiscordChannelId = 1234, -#pragma warning restore CS0618 } } }, cancellationToken); @@ -239,10 +230,6 @@ async Task RunDiscord(CancellationToken cancellationToken) Assert.AreEqual(true, updatedBot.Channels.First().IsUpdatesChannel); Assert.AreEqual(true, updatedBot.Channels.First().IsWatchdogChannel); Assert.AreEqual("butt", updatedBot.Channels.First().Tag); -#pragma warning disable CS0618 - Assert.AreEqual(channelId, updatedBot.Channels.First().DiscordChannelId); - Assert.IsNull(updatedBot.Channels.First().IrcChannel); -#pragma warning restore CS0618 Assert.AreEqual(channelId.ToString(), updatedBot.Channels.First().ChannelData); } diff --git a/tests/Tgstation.Server.Tests/Live/Instance/DeploymentTest.cs b/tests/Tgstation.Server.Tests/Live/Instance/DeploymentTest.cs index 1ecf82aa9de..8cc7d0b6fb7 100644 --- a/tests/Tgstation.Server.Tests/Live/Instance/DeploymentTest.cs +++ b/tests/Tgstation.Server.Tests/Live/Instance/DeploymentTest.cs @@ -1,6 +1,7 @@ using System; using System.Threading; using System.Threading.Tasks; + using Microsoft.VisualStudio.TestTools.UnitTesting; using Tgstation.Server.Api.Models; @@ -23,6 +24,7 @@ sealed class DeploymentTest : JobsRequiredTest readonly ushort dmPort; readonly ushort ddPort; readonly bool lowPriorityDeployments; + readonly EngineType testEngine; Task vpTest; @@ -31,7 +33,8 @@ public DeploymentTest( IJobsClient jobsClient, ushort dmPort, ushort ddPort, - bool lowPriorityDeployments) : base(jobsClient) + bool lowPriorityDeployments, + EngineType testEngine) : base(jobsClient) { this.instanceClient = instanceClient ?? throw new ArgumentNullException(nameof(instanceClient)); dreamMakerClient = instanceClient.DreamMaker; @@ -39,6 +42,7 @@ public DeploymentTest( this.dmPort = dmPort; this.ddPort = ddPort; this.lowPriorityDeployments = lowPriorityDeployments; + this.testEngine = testEngine; } public async ValueTask RunPreRepoClone(CancellationToken cancellationToken) @@ -61,12 +65,12 @@ async ValueTask CheckDreamDaemonPriority(Task deploymentJobWaitTask, Cancellatio // this doesn't check dm's priority, but it really should while (!deploymentJobWaitTask.IsCompleted) { - var allProcesses = TestLiveServer.GetDDProcessesOnPort(dmPort); + var allProcesses = TestLiveServer.GetEngineServerProcessesOnPort(testEngine, dmPort); if (allProcesses.Count == 0) continue; if (allProcesses.Count > 1) - Assert.Fail("Multiple DreamDaemon-like processes running!"); + Assert.Fail("Multiple engine-like processes running!"); using var process = allProcesses[0]; @@ -135,10 +139,10 @@ public async Task RunPostRepoClone(Task byondTask, CancellationToken cancellatio var updatedDD = await dreamDaemonClient.Update(new DreamDaemonRequest { - StartupTimeout = 15, + StartupTimeout = 30, Port = ddPort }, cancellationToken); - Assert.AreEqual(15U, updatedDD.StartupTimeout); + Assert.AreEqual(30U, updatedDD.StartupTimeout); Assert.AreEqual(ddPort, updatedDD.Port); async Task CompileAfterByondInstall() @@ -149,7 +153,7 @@ async Task CompileAfterByondInstall() var deployJobTask = CompileAfterByondInstall(); var deployJob = await deployJobTask; - var deploymentJobWaitTask = WaitForJob(deployJob, 40, true, ErrorCode.DreamMakerNeverValidated, cancellationToken); + var deploymentJobWaitTask = WaitForJob(deployJob, 40, true, ErrorCode.DeploymentNeverValidated, cancellationToken); await CheckDreamDaemonPriority(deploymentJobWaitTask, cancellationToken); @@ -174,7 +178,7 @@ async Task CompileAfterByondInstall() Assert.AreEqual(FailProject, updated.ProjectName); deployJob = await dreamMakerClient.Compile(cancellationToken); - await WaitForJob(deployJob, 40, true, ErrorCode.DreamMakerExitCode, cancellationToken); + await WaitForJob(deployJob, 40, true, ErrorCode.DeploymentExitCode, cancellationToken); await dreamMakerClient.Update(new DreamMakerRequest { @@ -182,7 +186,7 @@ await dreamMakerClient.Update(new DreamMakerRequest }, cancellationToken); deployJob = await dreamMakerClient.Compile(cancellationToken); - await WaitForJob(deployJob, 40, true, ErrorCode.DreamMakerMissingDme, cancellationToken); + await WaitForJob(deployJob, 40, true, ErrorCode.DeploymentMissingDme, cancellationToken); // check that we can change the visibility diff --git a/tests/Tgstation.Server.Tests/Live/Instance/EngineTest.cs b/tests/Tgstation.Server.Tests/Live/Instance/EngineTest.cs new file mode 100644 index 00000000000..a2f82495448 --- /dev/null +++ b/tests/Tgstation.Server.Tests/Live/Instance/EngineTest.cs @@ -0,0 +1,387 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Text; +using System.Threading; +using System.Threading.Tasks; + +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Options; +using Microsoft.VisualStudio.TestTools.UnitTesting; + +using Moq; + +using Tgstation.Server.Api.Models; +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.Engine; +using Tgstation.Server.Host.Configuration; +using Tgstation.Server.Host.IO; +using Tgstation.Server.Host.System; + +namespace Tgstation.Server.Tests.Live.Instance +{ + sealed class EngineTest(IEngineClient engineClient, IJobsClient jobsClient, IFileDownloader fileDownloader, Api.Models.Instance metadata, EngineType engineType) : JobsRequiredTest(jobsClient) + { + readonly IEngineClient engineClient = engineClient ?? throw new ArgumentNullException(nameof(engineClient)); + readonly IFileDownloader fileDownloader = fileDownloader ?? throw new ArgumentNullException(nameof(fileDownloader)); + + readonly Api.Models.Instance metadata = metadata ?? throw new ArgumentNullException(nameof(metadata)); + + static readonly Dictionary edgeVersions = new () + { + { EngineType.Byond, null }, + { EngineType.OpenDream, null } + }; + + EngineVersion testVersion; + readonly EngineType testEngine = engineType; + + public Task Run(CancellationToken cancellationToken, out Task firstInstall) + { + firstInstall = RunPartOne(cancellationToken); + return RunContinued(firstInstall, cancellationToken); + } + + public static async ValueTask GetEdgeVersion(EngineType engineType, IFileDownloader fileDownloader, CancellationToken cancellationToken) + { + var edgeVersion = edgeVersions[engineType]; + + if (edgeVersion != null) + return edgeVersion; + + EngineVersion engineVersion; + if (engineType == EngineType.Byond) + { + await using var provider = fileDownloader.DownloadFile(new Uri("https://www.byond.com/download/version.txt"), null); + var stream = await provider.GetResult(cancellationToken); + using var reader = new StreamReader(stream, Encoding.UTF8, false, -1, true); + var text = await reader.ReadToEndAsync(cancellationToken); + var splits = text.Split('\n', StringSplitOptions.TrimEntries | StringSplitOptions.RemoveEmptyEntries); + + var targetVersion = splits.Last(); + + var badVersionMap = new PlatformIdentifier().IsWindows + ? [] + // linux map also needs updating in CI + : new Dictionary() + { + { "515.1612", "515.1611" } + }; + + badVersionMap.Add("515.1617", "515.1616"); + + if (badVersionMap.TryGetValue(targetVersion, out var remappedVersion)) + targetVersion = remappedVersion; + + Assert.IsTrue(EngineVersion.TryParse(targetVersion, out engineVersion), $"Bad version: {targetVersion}"); + } + else if (engineType == EngineType.OpenDream) + { + var forcedVersion = Environment.GetEnvironmentVariable("TGS_TEST_OD_ENGINE_VERSION"); + if (!String.IsNullOrWhiteSpace(forcedVersion)) + { + engineVersion = new EngineVersion + { + Engine = EngineType.OpenDream, + SourceSHA = forcedVersion, + }; + } + else + { + var masterBranch = await TestingGitHubService.RealTestClient.Repository.Branch.Get("OpenDreamProject", "OpenDream", "master"); + + engineVersion = new EngineVersion + { + Engine = EngineType.OpenDream, + SourceSHA = masterBranch.Commit.Sha, + }; + } + } + else + { + Assert.Fail($"Unimplemented edge retrieval for engine type: {engineType}"); + return null; + } + + global::System.Console.WriteLine($"Edge {engineType} version evalutated to {engineVersion}"); + return edgeVersions[engineType] = engineVersion; + } + + async Task RunPartOne(CancellationToken cancellationToken) + { + testVersion = await GetEdgeVersion(testEngine, fileDownloader, cancellationToken); + await TestNoVersion(cancellationToken); + await TestInstallNullVersion(cancellationToken); + await TestInstallStable(cancellationToken); + } + + ValueTask TestInstallNullVersion(CancellationToken cancellationToken) + => ApiAssert.ThrowsException( + () => engineClient.SetActiveVersion( + new EngineVersionRequest + { + EngineVersion = new EngineVersion + { + Engine = testEngine, + } + }, + null, + cancellationToken), + ErrorCode.ModelValidationFailure); + public static int EngineInstallationTimeout(EngineVersion testVersion) + => testVersion.Engine.Value switch + { + EngineType.Byond => 30, + EngineType.OpenDream => 500, + _ => throw new InvalidOperationException($"Unknown engine type: {testVersion.Engine.Value}"), + }; + + int EngineInstallationTimeout() => EngineInstallationTimeout(testVersion); + + async Task RunContinued(Task firstInstall, CancellationToken cancellationToken) + { + await firstInstall; + await TestInstallFakeVersion(cancellationToken); + await TestCustomInstalls(cancellationToken); + await TestDeletes(cancellationToken); + } + + async Task TestDeletes(CancellationToken cancellationToken) + { + var deleteThisOneBecauseItWasntPartOfTheOriginalTest = await engineClient.DeleteVersion(new EngineVersionDeleteRequest + { + EngineVersion = new EngineVersion + { + Engine = testEngine, + Version = testVersion.Version, + CustomIteration = 2, + } + }, cancellationToken); + await WaitForJob(deleteThisOneBecauseItWasntPartOfTheOriginalTest, EngineInstallationTimeout(), false, null, cancellationToken); + + var nonExistentUninstallResponseTask = ApiAssert.ThrowsException(() => engineClient.DeleteVersion( + new EngineVersionDeleteRequest + { + EngineVersion = new EngineVersion + { + Version = new(509, 1000), + Engine = testEngine, + } + }, + cancellationToken), ErrorCode.ResourceNotPresent); + + var uninstallResponseTask = engineClient.DeleteVersion( + new EngineVersionDeleteRequest + { + EngineVersion = new EngineVersion + { + Version = testVersion.Version, + Engine = testVersion.Engine, + SourceSHA = testVersion.SourceSHA, + } + }, + cancellationToken); + + var badBecauseActiveResponseTask = ApiAssert.ThrowsException(() => engineClient.DeleteVersion( + new EngineVersionDeleteRequest + { + EngineVersion = new EngineVersion + { + Version = testVersion.Version, + Engine = testVersion.Engine, + SourceSHA = testVersion.SourceSHA, + CustomIteration = 1, + } + }, + cancellationToken), ErrorCode.EngineCannotDeleteActiveVersion); + + await badBecauseActiveResponseTask; + + var uninstallJob = await uninstallResponseTask; + Assert.IsNotNull(uninstallJob); + + // Has to wait on deployment test possibly + var uninstallTask = WaitForJob(uninstallJob, EngineInstallationTimeout() + 90, false, null, cancellationToken); + + await nonExistentUninstallResponseTask; + + await uninstallTask; + var byondDir = Path.Combine(metadata.Path, "Byond", testVersion.ToString()); + Assert.IsFalse(Directory.Exists(byondDir)); + + var newVersions = await engineClient.InstalledVersions(null, cancellationToken); + Assert.IsNotNull(newVersions); + Assert.AreEqual(1, newVersions.Count); + Assert.AreEqual(testVersion.Version.Semver(), newVersions[0].EngineVersion.Version.Semver()); + Assert.AreEqual(1, newVersions[0].EngineVersion.CustomIteration); + } + + async Task TestInstallFakeVersion(CancellationToken cancellationToken) + { + var newModel = new EngineVersionRequest + { + EngineVersion = new EngineVersion + { + Version = new Version(5011, 1385), + } + }; + + await ApiAssert.ThrowsException(() => engineClient.SetActiveVersion(newModel, null, cancellationToken), ErrorCode.ModelValidationFailure); + + newModel.EngineVersion.Engine = testEngine; + + var test = await engineClient.SetActiveVersion(newModel, null, cancellationToken); + Assert.IsNotNull(test.InstallJob); + await WaitForJob(test.InstallJob, EngineInstallationTimeout() + 30, true, ErrorCode.EngineDownloadFail, cancellationToken); + } + + async Task TestInstallStable(CancellationToken cancellationToken) + { + var newModel = new EngineVersionRequest + { + EngineVersion = new EngineVersion + { + Version = testVersion.Version, + Engine = testVersion.Engine, + SourceSHA = testVersion.SourceSHA, + } + }; + var test = await engineClient.SetActiveVersion(newModel, null, cancellationToken); + Assert.IsNotNull(test.InstallJob); + await WaitForJob(test.InstallJob, EngineInstallationTimeout() + 150, false, null, cancellationToken); + var currentShit = await engineClient.ActiveVersion(cancellationToken); + Assert.AreEqual(newModel.EngineVersion, currentShit.EngineVersion); + Assert.IsFalse(currentShit.EngineVersion.CustomIteration.HasValue); + + var dreamMaker = "DreamMaker"; + if (new PlatformIdentifier().IsWindows) + dreamMaker += ".exe"; + + var dreamMakerDir = Path.Combine(metadata.Path, "Byond", newModel.EngineVersion.Version.ToString(), "byond", "bin"); + + Assert.IsTrue(Directory.Exists(dreamMakerDir), $"Directory {dreamMakerDir} does not exist!"); + Assert.IsTrue( + File.Exists( + Path.Combine(dreamMakerDir, dreamMaker)), + $"Missing DreamMaker executable! Dir contents: {string.Join(", ", Directory.GetFileSystemEntries(dreamMakerDir))}"); + } + + async Task TestNoVersion(CancellationToken cancellationToken) + { + var allVersionsTask = engineClient.InstalledVersions(null, cancellationToken); + var currentShit = await engineClient.ActiveVersion(cancellationToken); + Assert.IsNotNull(currentShit); + Assert.IsNull(currentShit.EngineVersion); + var otherShit = await allVersionsTask; + Assert.IsNotNull(otherShit); + Assert.AreEqual(0, otherShit.Count); + } + + async Task TestCustomInstalls(CancellationToken cancellationToken) + { + var generalConfigOptionsMock = new Mock>(); + generalConfigOptionsMock.SetupGet(x => x.Value).Returns(new GeneralConfiguration()); + var sessionConfigOptionsMock = new Mock>(); + sessionConfigOptionsMock.SetupGet(x => x.Value).Returns(new SessionConfiguration()); + + var assemblyInformationProvider = new AssemblyInformationProvider(); + + IEngineInstaller byondInstaller = new PlatformIdentifier().IsWindows + ? new WindowsByondInstaller( + Mock.Of(), + Mock.Of(), + fileDownloader, + generalConfigOptionsMock.Object, + sessionConfigOptionsMock.Object, + Mock.Of>()) + : new PosixByondInstaller( + Mock.Of(), + Mock.Of(), + fileDownloader, + Mock.Of>()); + + using var windowsByondInstaller = byondInstaller as WindowsByondInstaller; + + // get the bytes for stable + await using var stableBytesMs = await TestingUtils.ExtractMemoryStreamFromInstallationData(await byondInstaller.DownloadVersion(testVersion, null, cancellationToken), cancellationToken); + + var test = await engineClient.SetActiveVersion( + new EngineVersionRequest + { + EngineVersion = new EngineVersion + { + Engine = testVersion.Engine, + Version = testVersion.Version, + SourceSHA = testVersion.SourceSHA, + }, + UploadCustomZip = true, + }, + stableBytesMs, + cancellationToken); + + Assert.IsNotNull(test.InstallJob); + await WaitForJob(test.InstallJob, EngineInstallationTimeout(), false, null, cancellationToken); + + // do it again. #1501 + stableBytesMs.Seek(0, SeekOrigin.Begin); + var test2 = await engineClient.SetActiveVersion( + new EngineVersionRequest + { + EngineVersion = new EngineVersion + { + Version = testVersion.Version, + SourceSHA = testVersion.SourceSHA, + Engine = testVersion.Engine, + }, + UploadCustomZip = true, + + }, + stableBytesMs, + cancellationToken); + + Assert.IsNotNull(test2.InstallJob); + await WaitForJob(test2.InstallJob, EngineInstallationTimeout(), false, null, cancellationToken); + + var newSettings = await engineClient.ActiveVersion(cancellationToken); + Assert.AreEqual(new Version(testVersion.Version.Major, testVersion.Version.Minor, 0), newSettings.EngineVersion.Version); + Assert.AreEqual(2, newSettings.EngineVersion.CustomIteration); + + // test a few switches + var installResponse = await engineClient.SetActiveVersion(new EngineVersionRequest + { + EngineVersion = new EngineVersion + { + Version = testVersion.Version, + SourceSHA = testVersion.SourceSHA, + Engine = testVersion.Engine, + } + }, null, cancellationToken); + Assert.IsNull(installResponse.InstallJob); + await ApiAssert.ThrowsException(() => engineClient.SetActiveVersion(new EngineVersionRequest + { + EngineVersion = new EngineVersion + { + Version = testVersion.Version, + Engine = testEngine, + CustomIteration = 3, + } + }, null, cancellationToken), ErrorCode.EngineNonExistentCustomVersion); + + installResponse = await engineClient.SetActiveVersion(new EngineVersionRequest + { + EngineVersion = new EngineVersion + { + Version = new Version(testVersion.Version.Major, testVersion.Version.Minor), + Engine = testEngine, + CustomIteration = 1, + } + }, null, cancellationToken); + Assert.IsNull(installResponse.InstallJob); + } + } +} diff --git a/tests/Tgstation.Server.Tests/Live/Instance/InstanceTest.cs b/tests/Tgstation.Server.Tests/Live/Instance/InstanceTest.cs index 69e6b00802c..bbe349ca5c8 100644 --- a/tests/Tgstation.Server.Tests/Live/Instance/InstanceTest.cs +++ b/tests/Tgstation.Server.Tests/Live/Instance/InstanceTest.cs @@ -1,11 +1,14 @@ using System; using System.Collections.Generic; +using System.IO; using System.Linq; +using System.Reflection; using System.Threading; using System.Threading.Tasks; using Microsoft.Extensions.Logging; using Microsoft.Extensions.Options; +using Microsoft.VisualStudio.TestTools.UnitTesting; using Moq; @@ -14,28 +17,25 @@ using Tgstation.Server.Api.Models.Response; using Tgstation.Server.Client; using Tgstation.Server.Client.Components; +using Tgstation.Server.Common.Http; using Tgstation.Server.Host.Components; -using Tgstation.Server.Host.Components.Byond; +using Tgstation.Server.Host.Components.Engine; +using Tgstation.Server.Host.Components.Events; +using Tgstation.Server.Host.Components.Repository; using Tgstation.Server.Host.Configuration; using Tgstation.Server.Host.IO; +using Tgstation.Server.Host.Jobs; using Tgstation.Server.Host.System; +using Tgstation.Server.Host.Utils; namespace Tgstation.Server.Tests.Live.Instance { - sealed class InstanceTest + sealed class InstanceTest(IInstanceManagerClient instanceManagerClient, IFileDownloader fileDownloader, InstanceManager instanceManager, ushort serverPort) { - readonly IInstanceManagerClient instanceManagerClient; - readonly IFileDownloader fileDownloader; - readonly InstanceManager instanceManager; - readonly ushort serverPort; - - public InstanceTest(IInstanceManagerClient instanceManagerClient, IFileDownloader fileDownloader, InstanceManager instanceManager, ushort serverPort) - { - this.instanceManagerClient = instanceManagerClient ?? throw new ArgumentNullException(nameof(instanceManagerClient)); - this.fileDownloader = fileDownloader ?? throw new ArgumentNullException(nameof(fileDownloader)); - this.instanceManager = instanceManager ?? throw new ArgumentNullException(nameof(instanceManager)); - this.serverPort = serverPort; - } + readonly IInstanceManagerClient instanceManagerClient = instanceManagerClient ?? throw new ArgumentNullException(nameof(instanceManagerClient)); + readonly IFileDownloader fileDownloader = fileDownloader ?? throw new ArgumentNullException(nameof(fileDownloader)); + readonly InstanceManager instanceManager = instanceManager ?? throw new ArgumentNullException(nameof(instanceManager)); + readonly ushort serverPort = serverPort; public async Task RunTests( IInstanceClient instanceClient, @@ -46,13 +46,14 @@ public async Task RunTests( bool usingBasicWatchdog, CancellationToken cancellationToken) { - var byondTest = new ByondTest(instanceClient.Byond, instanceClient.Jobs, fileDownloader, instanceClient.Metadata); - var chatTest = new ChatTest(instanceClient.ChatBots, instanceManagerClient, instanceClient.Jobs, instanceClient.Metadata); + var testVersion = await EngineTest.GetEdgeVersion(EngineType.Byond, fileDownloader, cancellationToken); + await using var engineTest = new EngineTest(instanceClient.Engine, instanceClient.Jobs, fileDownloader, instanceClient.Metadata, testVersion.Engine.Value); + await using var chatTest = new ChatTest(instanceClient.ChatBots, instanceManagerClient, instanceClient.Jobs, instanceClient.Metadata); var configTest = new ConfigurationTest(instanceClient.Configuration, instanceClient.Metadata); - var repoTest = new RepositoryTest(instanceClient.Repository, instanceClient.Jobs); - var dmTest = new DeploymentTest(instanceClient, instanceClient.Jobs, dmPort, ddPort, lowPrioDeployment); + await using var repoTest = new RepositoryTest(instanceClient.Repository, instanceClient.Jobs); + await using var dmTest = new DeploymentTest(instanceClient, instanceClient.Jobs, dmPort, ddPort, lowPrioDeployment, testVersion.Engine.Value); - var byondTask = byondTest.Run(cancellationToken, out var firstInstall); + var byondTask = engineTest.Run(cancellationToken, out var firstInstall); var chatTask = chatTest.RunPreWatchdog(cancellationToken); var repoLongJob = await repoTest.RunLongClone(cancellationToken); @@ -67,12 +68,83 @@ public async Task RunTests( await configTest.SetupDMApiTests(true, cancellationToken); await byondTask; - await new WatchdogTest( - await ByondTest.GetEdgeVersion(fileDownloader, cancellationToken), instanceClient, instanceManager, serverPort, highPrioDD, ddPort, usingBasicWatchdog).Run(cancellationToken); + await using var wdt = new WatchdogTest( + testVersion, + instanceClient, + instanceManager, + serverPort, + highPrioDD, + ddPort, + usingBasicWatchdog); + await wdt.Run(cancellationToken); + } + + public static async ValueTask DownloadEngineVersion( + EngineVersion compatVersion, + IFileDownloader fileDownloader, + Uri openDreamUrl, + CancellationToken cancellationToken) + { + var ioManager = new DefaultIOManager(); + var odRepoDir = ioManager.ConcatPath( + Environment.GetFolderPath( + Environment.SpecialFolder.LocalApplicationData, + Environment.SpecialFolderOption.DoNotVerify), + new AssemblyInformationProvider().VersionPrefix, + "OpenDreamRepository"); + var odRepoIoManager = new ResolvingIOManager(ioManager, odRepoDir); + + var mockOptions = new Mock>(); + var genConfig = new GeneralConfiguration + { + OpenDreamGitUrl = openDreamUrl, + }; + mockOptions.SetupGet(x => x.Value).Returns(genConfig); + IEngineInstaller byondInstaller = + compatVersion.Engine == EngineType.OpenDream + ? new OpenDreamInstaller( + new DefaultIOManager(), + Mock.Of>(), + new PlatformIdentifier(), + Mock.Of(), + new RepositoryManager( + new LibGit2RepositoryFactory( + Mock.Of>()), + new LibGit2Commands(), + odRepoIoManager, + new NoopEventConsumer(), + Mock.Of(), + Mock.Of(), + Mock.Of>(), + Mock.Of>(), + genConfig), + Mock.Of(), + Mock.Of(), + mockOptions.Object, + Options.Create(new SessionConfiguration())) + : new PlatformIdentifier().IsWindows + ? new WindowsByondInstaller( + Mock.Of(), + Mock.Of(), + fileDownloader, + Options.Create(genConfig), + Options.Create(new SessionConfiguration()), + Mock.Of>()) + : new PosixByondInstaller( + Mock.Of(), + Mock.Of(), + fileDownloader, + Mock.Of>()); + + using var windowsByondInstaller = byondInstaller as WindowsByondInstaller; + + // get the bytes for stable + return await byondInstaller.DownloadVersion(compatVersion, null, cancellationToken); } public async Task RunCompatTests( - Version compatVersion, + EngineVersion compatVersion, + Uri openDreamUrl, IInstanceClient instanceClient, ushort dmPort, ushort ddPort, @@ -85,8 +157,7 @@ public async Task RunCompatTests( var cloneRequest = instanceClient.Repository.Clone(new RepositoryCreateRequest { Origin = new Uri(Origin), - }, cancellationToken); - + }, cancellationToken).AsTask(); var dmUpdateRequest = instanceClient.DreamMaker.Update(new DreamMakerRequest { @@ -117,7 +188,7 @@ public async Task RunCompatTests( ChannelLimit = 10, Channels = new List { - new ChatChannel + new () { ChannelData = channelIdStr, Tag = "some_tag", @@ -134,43 +205,44 @@ public async Task RunCompatTests( ReconnectionInterval = 1, }, cancellationToken); - var jrt = new JobsRequiredTest(instanceClient.Jobs); - - IByondInstaller byondInstaller = new PlatformIdentifier().IsWindows - ? new WindowsByondInstaller( - Mock.Of(), - Mock.Of(), - fileDownloader, - Options.Create(new GeneralConfiguration()), - Mock.Of>()) - : new PosixByondInstaller( - Mock.Of(), - Mock.Of(), - fileDownloader, - Mock.Of>()); + await using var jrt = new JobsRequiredTest(instanceClient.Jobs); - using var windowsByondInstaller = byondInstaller as WindowsByondInstaller; - - // get the bytes for stable - ByondInstallResponse installJob2; - using (var stableBytesMs = await byondInstaller.DownloadVersion(compatVersion, cancellationToken)) + EngineInstallResponse installJob2; + await using (var stableBytesMs = await TestingUtils.ExtractMemoryStreamFromInstallationData( + await DownloadEngineVersion(compatVersion, fileDownloader, openDreamUrl, cancellationToken), + cancellationToken)) { - installJob2 = await instanceClient.Byond.SetActiveVersion(new ByondVersionRequest + installJob2 = await instanceClient.Engine.SetActiveVersion(new EngineVersionRequest { UploadCustomZip = true, - Version = compatVersion, + EngineVersion = new EngineVersion + { + Version = compatVersion.Version, + Engine = compatVersion.Engine, + SourceSHA = compatVersion.SourceSHA, + }, }, stableBytesMs, cancellationToken); } await chatRequest; await Task.Yield(); - await Task.WhenAll( - jrt.WaitForJob(installJob2.InstallJob, 60, false, null, cancellationToken), + jrt.WaitForJob(installJob2.InstallJob, EngineTest.EngineInstallationTimeout(compatVersion) + 30, false, null, cancellationToken), jrt.WaitForJob(cloneRequest.Result.ActiveJob, 60, false, null, cancellationToken), dmUpdateRequest.AsTask(), - cloneRequest.AsTask()); + cloneRequest); + + if (compatVersion.Engine.Value == EngineType.OpenDream) + { + Assert.IsNotNull(compatVersion.SourceSHA); + var activeVersion = await instanceClient.Engine.ActiveVersion(cancellationToken); + Assert.AreEqual(Limits.MaximumCommitShaLength, activeVersion.EngineVersion.SourceSHA.Length); + Assert.AreEqual(compatVersion.SourceSHA, activeVersion.EngineVersion.SourceSHA); + Assert.AreEqual(compatVersion.Version, activeVersion.EngineVersion.Version); + Assert.AreEqual(compatVersion.Engine, activeVersion.EngineVersion.Engine); + Assert.AreEqual(1, activeVersion.EngineVersion.CustomIteration); + } var jobs = await instanceClient.Jobs.List(null, cancellationToken); var theJobWeWant = jobs @@ -180,9 +252,7 @@ await Task.WhenAll( var configSetupTask = new ConfigurationTest(instanceClient.Configuration, instanceClient.Metadata).SetupDMApiTests(true, cancellationToken); - if (TestingUtils.RunningInGitHubActions - || String.IsNullOrWhiteSpace(Environment.GetEnvironmentVariable("TGS_TEST_GITHUB_TOKEN")) - || Environment.MachineName.Equals("CYBERSTATIONXVI", StringComparison.OrdinalIgnoreCase)) + if (!String.IsNullOrWhiteSpace(Environment.GetEnvironmentVariable("TGS_TEST_GITHUB_TOKEN"))) await instanceClient.Repository.Update(new RepositoryUpdateRequest { CreateGitHubDeployments = true, @@ -194,7 +264,8 @@ await instanceClient.Repository.Update(new RepositoryUpdateRequest await configSetupTask; - await new WatchdogTest(compatVersion, instanceClient, instanceManager, serverPort, highPrioDD, ddPort, usingBasicWatchdog).Run(cancellationToken); + await using var wdt = new WatchdogTest(compatVersion, instanceClient, instanceManager, serverPort, highPrioDD, ddPort, usingBasicWatchdog); + await wdt.Run(cancellationToken); await instanceManagerClient.Update(new InstanceUpdateRequest { diff --git a/tests/Tgstation.Server.Tests/Live/Instance/JobsHubTests.cs b/tests/Tgstation.Server.Tests/Live/Instance/JobsHubTests.cs index 493e2d1d4f1..b3149556e56 100644 --- a/tests/Tgstation.Server.Tests/Live/Instance/JobsHubTests.cs +++ b/tests/Tgstation.Server.Tests/Live/Instance/JobsHubTests.cs @@ -8,6 +8,7 @@ using Microsoft.AspNetCore.SignalR.Client; using Microsoft.VisualStudio.TestTools.UnitTesting; +using Tgstation.Server.Api.Extensions; using Tgstation.Server.Api.Hubs; using Tgstation.Server.Api.Models; using Tgstation.Server.Api.Models.Request; @@ -43,7 +44,7 @@ public JobsHubTests(IServerClient permedUser, IServerClient permlessUser) finishTcs = new TaskCompletionSource(); seenJobs = new ConcurrentDictionary(); - permlessSeenJobs = new HashSet(); + permlessSeenJobs = []; } public Task ReceiveJobUpdate(JobResponse job, CancellationToken cancellationToken) @@ -66,7 +67,7 @@ public Task ReceiveJobUpdate(JobResponse job, CancellationToken cancellationToke } catch(Exception ex) { - finishTcs.SetException(ex); + finishTcs.TrySetException(ex); } return Task.CompletedTask; @@ -84,7 +85,7 @@ public Task ReceiveJobUpdate(JobResponse job, CancellationToken cancellationToke } } - public async Task Run(CancellationToken cancellationToken) + public async Task Run(CancellationToken cancellationToken) { var neverReceiver = new ShouldNeverReceiveUpdates() { @@ -98,16 +99,33 @@ public async Task Run(CancellationToken cancellationToken) }, }; - await using (permedConn = (HubConnection)await permedUser.SubscribeToJobUpdates( + permedConn = (HubConnection)await permedUser.SubscribeToJobUpdates( this, null, null, - cancellationToken)) - await using (permlessConn = (HubConnection)await permlessUser.SubscribeToJobUpdates( - neverReceiver, - null, - null, - cancellationToken)) + cancellationToken); + + try + { + permlessConn = (HubConnection)await permlessUser.SubscribeToJobUpdates( + neverReceiver, + null, + null, + cancellationToken); + } + catch + { + await permedConn.DisposeAsync(); + throw; + } + + return FinishAsync(cancellationToken); + } + + async Task FinishAsync(CancellationToken cancellationToken) + { + await using (permedConn) + await using (permlessConn) { Console.WriteLine($"Initial conn1: {permedConn.ConnectionId}"); Console.WriteLine($"Initial conn2: {permlessConn.ConnectionId}"); @@ -138,7 +156,10 @@ await permedUser.Instances.Update(new InstanceUpdateRequest Online = true, }, cancellationToken); - var jobs = await permedUser.Instances.CreateClient(instance).Jobs.List(null, cancellationToken); + var jobs = await permedUser.Instances.CreateClient(instance).Jobs.List(new PaginationSettings + { + PageSize = 100 + }, cancellationToken); if (wasOffline) await permedUser.Instances.Update(new InstanceUpdateRequest { @@ -153,6 +174,13 @@ await permedUser.Instances.Update(new InstanceUpdateRequest .Select(CheckInstance); var allJobs = (await ValueTaskExtensions.WhenAll(allJobsTask, allInstances.Count)).SelectMany(x => x).ToList(); + + var groups = allJobs.GroupBy(x => x.Id.Value).ToList(); + var uniqueAllJobs = groups.Select(x => x.First()).ToList(); + + static string JobListFormatter(IEnumerable jobs) => String.Join(Environment.NewLine, jobs.Select(x => $"- I:{x.InstanceId}|JID:{x.Id}|JC:{x.JobCode}|Desc:{x.Description}")); + Assert.AreEqual(allJobs.Count, uniqueAllJobs.Count, $"Duplicated Jobs:{Environment.NewLine}{JobListFormatter(groups.Where(x => x.Count() > 1).SelectMany(x => x))}"); + var missableMissedJobs = 0; foreach (var job in allJobs) { @@ -171,7 +199,7 @@ await permedUser.Instances.Update(new InstanceUpdateRequest } static DateTimeOffset PerformDBTruncation(DateTimeOffset original) - => new DateTimeOffset( + => new( original.Ticks - (original.Ticks % TimeSpan.TicksPerSecond), original.Offset); @@ -185,22 +213,28 @@ static DateTimeOffset PerformDBTruncation(DateTimeOffset original) } else { - var wasMissableJob = job.JobCode == JobCode.ReconnectChatBot - || job.JobCode == JobCode.StartupWatchdogLaunch - || job.JobCode == JobCode.StartupWatchdogReattach; - Assert.IsTrue(wasMissableJob); + var wasMissableJob = job.JobCode.Value.IsServerStartupJob(); + Assert.IsTrue(wasMissableJob, $"Found unexpected missed job: #{job.Id.Value} - {job.JobCode} - {job.Description}"); ++missableMissedJobs; } } + var jobsSeenByHubButNotInAllJobs = seenJobs.Values.Where(x => !allJobs.Any(y => y.Id.Value == x.Id.Value)).ToList(); + // some instances may be detached, but our cache remains var accountedJobs = allJobs.Count - missableMissedJobs; - var accountedSeenJobs = seenJobs.Where(x => allInstances.Any(i => i.Id.Value == x.Value.InstanceId)).Count(); - Assert.AreEqual(accountedJobs, accountedSeenJobs); + var errorMessage = $"Mismatch in seen jobs:{Environment.NewLine}Not seen in seen:{Environment.NewLine}{JobListFormatter(allJobs.Where(x => !seenJobs.Any(y => y.Key == x.Id.Value)))}{Environment.NewLine}Seen not in all:{Environment.NewLine}{JobListFormatter(jobsSeenByHubButNotInAllJobs)}{Environment.NewLine}Current Instances: {String.Join(", ", allInstances.Select(i => i.Id.Value))}"; + Assert.AreEqual( + accountedJobs, + seenJobs.Count - jobsSeenByHubButNotInAllJobs.Count, + errorMessage); + Assert.IsTrue( + jobsSeenByHubButNotInAllJobs.All(job => job.JobCode.Value == JobCode.Move), + errorMessage); Assert.IsTrue(accountedJobs <= seenJobs.Count); Assert.AreNotEqual(0, permlessSeenJobs.Count); Assert.IsTrue(permlessSeenJobs.Count < seenJobs.Count); - Assert.IsTrue(permlessSeenJobs.All(id => seenJobs.ContainsKey(id))); + Assert.IsTrue(permlessSeenJobs.All(id => seenJobs.ContainsKey(id)), $"Saw permless job(s) that wasn't seen:{Environment.NewLine}{JobListFormatter(permlessSeenJobs.Where(id => !seenJobs.ContainsKey(id)).Select(id => allJobs.First(x => x.Id == id)))}"); await using var conn3 = (HubConnection)await permedUser.SubscribeToJobUpdates( this, diff --git a/tests/Tgstation.Server.Tests/Live/Instance/JobsRequiredTest.cs b/tests/Tgstation.Server.Tests/Live/Instance/JobsRequiredTest.cs index 3dd3392723b..c6501e6766a 100644 --- a/tests/Tgstation.Server.Tests/Live/Instance/JobsRequiredTest.cs +++ b/tests/Tgstation.Server.Tests/Live/Instance/JobsRequiredTest.cs @@ -1,11 +1,11 @@ -using Microsoft.VisualStudio.TestTools.UnitTesting; -using System; -using System.Collections.Generic; +using System; +using System.Collections.Concurrent; using System.Reflection; -using System.Text; using System.Threading; using System.Threading.Tasks; +using Microsoft.VisualStudio.TestTools.UnitTesting; + using Tgstation.Server.Api.Hubs; using Tgstation.Server.Api.Models; using Tgstation.Server.Api.Models.Response; @@ -14,16 +14,72 @@ namespace Tgstation.Server.Tests.Live.Instance { - class JobsRequiredTest + class JobsRequiredTest : IAsyncDisposable { protected IJobsClient JobsClient { get; } readonly IApiClient apiClient; + IAsyncDisposable hubConnection; + readonly Task hubConnectionTask; + readonly CancellationTokenSource cancellationTokenSource; + + readonly ConcurrentDictionary> registry; + public JobsRequiredTest(IJobsClient jobsClient) { JobsClient = jobsClient ?? throw new ArgumentNullException(nameof(jobsClient)); apiClient = (IApiClient)jobsClient.GetType().GetProperty("ApiClient", BindingFlags.NonPublic | BindingFlags.Instance).GetValue(jobsClient); + registry = new ConcurrentDictionary>(); + + cancellationTokenSource = new CancellationTokenSource(); + hubConnectionTask = CreateHubConnection(); + } + + async Task CreateHubConnection() + { + var receiver = new JobReceiver + { + Callback = job => Register(job), + }; + + hubConnection = await apiClient.CreateHubConnection(receiver, null, null, cancellationTokenSource.Token); + } + + public async ValueTask DisposeAsync() + { + cancellationTokenSource.Cancel(); + cancellationTokenSource.Dispose(); + try + { + await hubConnectionTask; + } + catch (OperationCanceledException) + { + } + + if (hubConnection != null) + await hubConnection.DisposeAsync(); + } + + Task Register(JobResponse updatedJob) + { + var tcs = registry.AddOrUpdate(updatedJob.Id.Value, + _ => + { + var tcs = new TaskCompletionSource(); + if (updatedJob.StoppedAt.HasValue) + tcs.SetResult(updatedJob); + return tcs; + }, + (_, oldTcs) => + { + if (updatedJob.StoppedAt.HasValue) + oldTcs.TrySetResult(updatedJob); + return oldTcs; + }); + + return tcs.Task; } class JobReceiver : IJobsHub @@ -43,47 +99,21 @@ public async Task WaitForJob(JobResponse originalJob, int timeout, Assert.IsNotNull(originalJob.JobCode); var job = originalJob; + var registryTask = Register(job); + await Task.WhenAny( + registryTask, + Task.Delay(TimeSpan.FromSeconds(timeout), cancellationToken)); + + if (!registryTask.IsCompleted) + // one last get in case SignalR dropped the ball + job = await JobsClient.GetId(job, cancellationToken); + else + job = await registryTask; + if (!job.StoppedAt.HasValue) { - var tcs = new TaskCompletionSource(); - var receiver = new JobReceiver - { - Callback = updatedJob => - { - if (updatedJob.Id != job.Id) - return; - - job = updatedJob; - if (updatedJob.StoppedAt.HasValue) - tcs.TrySetResult(); - }, - }; - - JobResponse firstCheck; - await using (var hubConnection = await apiClient.CreateHubConnection(receiver, null, null, cancellationToken)) - { - // initial GET after connecting - firstCheck = await JobsClient.GetId(job, cancellationToken); - if (!firstCheck.StoppedAt.HasValue) - { - firstCheck = null; - await Task.WhenAny( - tcs.Task, - Task.Delay(TimeSpan.FromSeconds(timeout), cancellationToken)); - } - } - - if (firstCheck != null) - job = firstCheck; - else if (!job.StoppedAt.HasValue) - // one last get in case SignalR dropped the ball - job = await JobsClient.GetId(job, cancellationToken); - - if (!job.StoppedAt.HasValue) - { - await JobsClient.Cancel(job, cancellationToken); - Assert.Fail($"Job ID {job.Id} \"{job.Description}\" timed out!"); - } + await JobsClient.Cancel(job, cancellationToken); + Assert.Fail($"Job ID {job.Id} \"{job.Description}\" timed out!"); } if (expectFailure.HasValue && expectFailure.Value ^ job.ExceptionDetails != null) diff --git a/tests/Tgstation.Server.Tests/Live/Instance/TestBridgeHandler.cs b/tests/Tgstation.Server.Tests/Live/Instance/TestBridgeHandler.cs index 53e0c43a517..e00421e295f 100644 --- a/tests/Tgstation.Server.Tests/Live/Instance/TestBridgeHandler.cs +++ b/tests/Tgstation.Server.Tests/Live/Instance/TestBridgeHandler.cs @@ -15,17 +15,20 @@ namespace Tgstation.Server.Tests.Live.Instance { sealed class TestBridgeHandler : Chunker, IBridgeHandler { - class DMApiParametersImpl : DMApiParameters { } + class DMApiParametersImpl : DMApiParameters + { + public DMApiParametersImpl(string accessIdentifier) + : base(accessIdentifier) + { + } + } class BridgeResponseHack : BridgeResponse { public string IntegrationHack { get; set; } } - public DMApiParameters DMApiParameters => new DMApiParametersImpl - { - AccessIdentifier = accessIdentifier - }; + public DMApiParameters DMApiParameters => new DMApiParametersImpl(accessIdentifier); long lastBridgeRequestSize = 0; @@ -88,7 +91,7 @@ public async ValueTask ProcessBridgeRequest(BridgeParameters par Assert.AreEqual("payload", coreMessage); var serializedRequest = JsonConvert.SerializeObject(parameters, DMApiConstants.SerializerSettings); - var actualLastRequest = $"http://127.0.0.1:{serverPort}/Bridge?data=" + HttpUtility.UrlEncode(serializedRequest); + var actualLastRequest = $"http://127.0.0.1:{serverPort}/api/Bridge?data=" + HttpUtility.UrlEncode(serializedRequest); lastBridgeRequestSize = actualLastRequest.Length; return new BridgeResponseHack { diff --git a/tests/Tgstation.Server.Tests/Live/Instance/WatchdogTest.cs b/tests/Tgstation.Server.Tests/Live/Instance/WatchdogTest.cs index 9104a8201cd..678ef10fc77 100644 --- a/tests/Tgstation.Server.Tests/Live/Instance/WatchdogTest.cs +++ b/tests/Tgstation.Server.Tests/Live/Instance/WatchdogTest.cs @@ -17,6 +17,8 @@ using System.Linq; using System.Net; using System.Net.Sockets; +using System.Reflection; +using System.Runtime.CompilerServices; using System.Runtime.InteropServices; using System.Text; using System.Threading; @@ -27,7 +29,6 @@ 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; @@ -37,6 +38,7 @@ using Tgstation.Server.Host.Extensions; using Tgstation.Server.Host.IO; using Tgstation.Server.Host.System; +using Tgstation.Server.Host.Utils; namespace Tgstation.Server.Tests.Live.Instance { @@ -62,12 +64,13 @@ sealed class WatchdogTest : JobsRequiredTest readonly ushort ddPort; readonly bool highPrioDD; readonly TopicClient topicClient; - readonly Version testVersion; - readonly bool usingBasicWatchdog; + readonly EngineVersion testVersion; + readonly bool watchdogRestartsProcess; bool ranTimeoutTest = false; + const string BaseAdditionalParameters = "expect_chat_channels=1&expect_static_files=1"; - public WatchdogTest(Version testVersion, IInstanceClient instanceClient, InstanceManager instanceManager, ushort serverPort, bool highPrioDD, ushort ddPort, bool usingBasicWatchdog) + public WatchdogTest(EngineVersion testVersion, IInstanceClient instanceClient, InstanceManager instanceManager, ushort serverPort, bool highPrioDD, ushort ddPort, bool watchdogRestartsProcess) : base(instanceClient.Jobs) { this.instanceClient = instanceClient ?? throw new ArgumentNullException(nameof(instanceClient)); @@ -76,7 +79,7 @@ public WatchdogTest(Version testVersion, IInstanceClient instanceClient, Instanc this.highPrioDD = highPrioDD; this.ddPort = ddPort; this.testVersion = testVersion ?? throw new ArgumentNullException(nameof(testVersion)); - this.usingBasicWatchdog = usingBasicWatchdog; + this.watchdogRestartsProcess = watchdogRestartsProcess || testVersion.Engine.Value == EngineType.OpenDream; topicClient = new(new SocketParameters { @@ -86,42 +89,63 @@ public WatchdogTest(Version testVersion, IInstanceClient instanceClient, Instanc DisconnectTimeout = TimeSpan.FromSeconds(30) }, loggerFactory.CreateLogger($"WatchdogTest.TopicClient.{instanceClient.Metadata.Name}")); } - public async Task Run(CancellationToken cancellationToken) + { + try + { + await RunInt(cancellationToken); + } + catch + { + System.Console.WriteLine($"WATCHDOG TEST FAILING INSTANCE ID {instanceClient.Metadata.Id.Value}"); + throw; + } + } + + async Task RunInt(CancellationToken cancellationToken) { System.Console.WriteLine($"TEST: START WATCHDOG TESTS {instanceClient.Metadata.Name}"); async Task CheckByondVersions() { - var listTask = instanceClient.Byond.InstalledVersions(null, cancellationToken); + var listTask = instanceClient.Engine.InstalledVersions(null, cancellationToken); var list = await listTask; Assert.AreEqual(1, list.Count); var byondVersion = list[0]; - Assert.AreEqual(1, byondVersion.Version.Build); - Assert.AreEqual(testVersion.Major, byondVersion.Version.Major); - Assert.AreEqual(testVersion.Minor, byondVersion.Version.Minor); + Assert.AreEqual(1, byondVersion.EngineVersion.CustomIteration); + Assert.AreEqual(testVersion.Engine, byondVersion.EngineVersion.Engine); + if (testVersion.Version != null) + { + Assert.AreEqual(testVersion.Version.Major, byondVersion.EngineVersion.Version.Major); + Assert.AreEqual(testVersion.Version.Minor, byondVersion.EngineVersion.Version.Minor); + } + else + { + Assert.IsNull(byondVersion.EngineVersion.Version); + Assert.AreEqual(testVersion.SourceSHA, byondVersion.EngineVersion.SourceSHA); + } } await Task.WhenAll( // Increase startup timeout, disable heartbeats, enable map threads because we've tested without for years instanceClient.DreamDaemon.Update(new DreamDaemonRequest { - StartupTimeout = 15, + StartupTimeout = 30, HealthCheckSeconds = 0, Port = ddPort, MapThreads = 2, LogOutput = false, - AdditionalParameters = "expect_chat_channels=1&expect_static_files=1" + AdditionalParameters = BaseAdditionalParameters }, cancellationToken).AsTask(), CheckByondVersions(), ApiAssert.ThrowsException(() => instanceClient.DreamDaemon.Update(new DreamDaemonRequest { SoftShutdown = true, SoftRestart = true - }, cancellationToken), ErrorCode.DreamDaemonDoubleSoft).AsTask(), + }, cancellationToken), ErrorCode.GameServerDoubleSoft).AsTask(), ApiAssert.ThrowsException(() => instanceClient.DreamDaemon.Update(new DreamDaemonRequest { Port = 0 @@ -134,16 +158,28 @@ await Task.WhenAll( await TestDMApiFreeDeploy(cancellationToken); // long running test likes consistency with the channels - await DummyChatProvider.RandomDisconnections(false, cancellationToken); + DummyChatProvider.RandomDisconnections(false); await RunLongRunningTestThenUpdate(cancellationToken); await RunLongRunningTestThenUpdateWithNewDme(cancellationToken); await RunLongRunningTestThenUpdateWithByondVersionSwitch(cancellationToken); + // no chatty bullshit while we test health checks + var tcs = new TaskCompletionSource(); + var oldTask = Interlocked.Exchange(ref DummyChatProvider.MessageGuard, tcs.Task); + await RunHealthCheckTest(true, cancellationToken); await RunHealthCheckTest(false, cancellationToken); + async void Cleanup() + { + await oldTask; + tcs.SetResult(); + } + + Cleanup(); + await InteropTestsForLongRunningDme(cancellationToken); await instanceClient.DreamDaemon.Update(new DreamDaemonRequest @@ -165,7 +201,6 @@ async ValueTask RunTest(bool useTrusted) var ddUpdateTask = instanceClient.DreamDaemon.Update(new DreamDaemonRequest { SecurityLevel = useTrusted ? DreamDaemonSecurity.Trusted : DreamDaemonSecurity.Safe, - AdditionalParameters = "expect_chat_channels=1&expect_static_files=1", }, cancellationToken); var currentStatus = await DeployTestDme("long_running_test_rooted", DreamDaemonSecurity.Trusted, true, cancellationToken); await ddUpdateTask; @@ -180,12 +215,15 @@ async ValueTask RunTest(bool useTrusted) { SoftShutdown = true, }, cancellationToken); + ValidateSessionId(currentStatus, true); Assert.AreEqual(WatchdogStatus.Online, currentStatus.Status); // reimplement TellWorldToReboot because it expects a new deployment and we don't care System.Console.WriteLine("TEST: Hack world reboot topic..."); - var result = await topicClient.SendTopic(IPAddress.Loopback, "tgs_integration_test_special_tactics=1", ddPort, cancellationToken); + var result = await SendTestTopic( + "tgs_integration_test_special_tactics=1", + cancellationToken); Assert.AreEqual("ack", result.StringData); using var tempCts = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken); @@ -207,17 +245,42 @@ async ValueTask RunTest(bool useTrusted) await RunTest(true); - if (new PlatformIdentifier().IsWindows || !usingBasicWatchdog) + if (new PlatformIdentifier().IsWindows || !watchdogRestartsProcess) await RunTest(false); } + ValueTask SendTestTopic(string queryString, CancellationToken cancellationToken) + => SendTestTopic(queryString, topicClient, instanceManager.GetInstanceReference(instanceClient.Metadata), FindTopicPort(), cancellationToken); + + public static async ValueTask SendTestTopic(string queryString, ITopicClient topicClient, IInstanceReference instanceReference, ushort topicPort, CancellationToken cancellationToken) + { + using (instanceReference) + { + using var loggerFactory = LoggerFactory.Create(builder => + { + builder.AddConsole(); + builder.SetMinimumLevel(LogLevel.Trace); + }); + + var watchdog = instanceReference?.Watchdog; + var session = (SessionController)watchdog?.GetType().GetMethod("GetActiveController", BindingFlags.Instance | BindingFlags.NonPublic)?.Invoke(watchdog, null); + + using (session != null + ? await session.TopicSendSemaphore.Lock(cancellationToken) + : null) + return await topicClient.SendWithOptionalPriority( + new AsyncDelayer(), + loggerFactory.CreateLogger(), + queryString, + topicPort, + true, + cancellationToken); + } + } + async ValueTask BroadcastTest(CancellationToken cancellationToken) { - var topicRequestResult = await topicClient.SendTopic( - IPAddress.Loopback, - $"tgs_integration_test_tactics_broadcast=1", - ddPort, - cancellationToken); + var topicRequestResult = await SendTestTopic("tgs_integration_test_tactics_broadcast=1", cancellationToken); Assert.IsNotNull(topicRequestResult); Assert.AreEqual("!!NULL!!", topicRequestResult.StringData); @@ -228,10 +291,8 @@ await instanceClient.DreamDaemon.Update(new DreamDaemonRequest BroadcastMessage = TestBroadcastMessage, }, cancellationToken); - topicRequestResult = await topicClient.SendTopic( - IPAddress.Loopback, - $"tgs_integration_test_tactics_broadcast=1", - ddPort, + topicRequestResult = await SendTestTopic( + "tgs_integration_test_tactics_broadcast=1", cancellationToken); Assert.IsNotNull(topicRequestResult); @@ -253,6 +314,8 @@ await ApiAssert.ThrowsException(() => in await RegressionTest1550(cancellationToken); + await TestLegacyBridgeEndpoint(cancellationToken); + var deleteJobTask = TestDeleteByondInstallErrorCasesAndQueing(cancellationToken); SessionController.LogTopicRequests = false; @@ -280,33 +343,35 @@ await ApiAssert.ThrowsException(() => in async ValueTask RegressionTest1550(CancellationToken cancellationToken) { + // Previous test, StartAndLeaveRunning, has SoftRestart set. We don't want that. + var restartJob = await instanceClient.DreamDaemon.Restart(cancellationToken); + await WaitForJob(restartJob, 10, false, null, cancellationToken); + // we need to cycle deployments twice because TGS holds the initial deployment var currentStatus = await DeployTestDme("LongRunning/long_running_test", DreamDaemonSecurity.Trusted, true, cancellationToken); Assert.AreEqual(WatchdogStatus.Online, currentStatus.Status); Assert.IsNotNull(currentStatus.StagedCompileJob); + ValidateSessionId(currentStatus, true); var expectedStaged = currentStatus.StagedCompileJob; Assert.AreNotEqual(expectedStaged.Id, currentStatus.ActiveCompileJob.Id); - await TellWorldToReboot(cancellationToken); + Assert.IsFalse(currentStatus.SoftShutdown); - currentStatus = await instanceClient.DreamDaemon.Read(cancellationToken); + currentStatus = await TellWorldToReboot(true, cancellationToken); + ValidateSessionId(currentStatus, true); Assert.AreEqual(expectedStaged.Id, currentStatus.ActiveCompileJob.Id); - await Task.Delay(TimeSpan.FromSeconds(3), cancellationToken); - - var topicRequestResult = await topicClient.SendTopic( - IPAddress.Loopback, - $"shadow_wizard_money_gang=1", - ddPort, + var topicRequestResult = await SendTestTopic( + "shadow_wizard_money_gang=1", cancellationToken); Assert.IsNotNull(topicRequestResult); Assert.AreEqual("we love casting spells", topicRequestResult.StringData); - await DeployTestDme("LongRunning/long_running_test", DreamDaemonSecurity.Trusted, true, cancellationToken); - - currentStatus = await instanceClient.DreamDaemon.Read(cancellationToken); + currentStatus = await DeployTestDme("LongRunning/long_running_test", DreamDaemonSecurity.Trusted, true, cancellationToken); + Assert.AreEqual(watchdogRestartsProcess, currentStatus.SoftRestart); + ValidateSessionId(currentStatus, false); Assert.AreEqual(WatchdogStatus.Online, currentStatus.Status); Assert.IsNotNull(currentStatus.StagedCompileJob); @@ -314,9 +379,9 @@ async ValueTask RegressionTest1550(CancellationToken cancellationToken) expectedStaged = currentStatus.StagedCompileJob; Assert.AreNotEqual(expectedStaged.Id, currentStatus.ActiveCompileJob.Id); - await TellWorldToReboot(cancellationToken); + currentStatus = await TellWorldToReboot(true, cancellationToken); - currentStatus = await instanceClient.DreamDaemon.Read(cancellationToken); + ValidateSessionId(currentStatus, watchdogRestartsProcess); Assert.AreEqual(WatchdogStatus.Online, currentStatus.Status); Assert.IsNull(currentStatus.StagedCompileJob); Assert.AreEqual(expectedStaged.Id, currentStatus.ActiveCompileJob.Id); @@ -327,16 +392,21 @@ async ValueTask RegressionTest1550(CancellationToken cancellationToken) async Task TestDeleteByondInstallErrorCasesAndQueing(CancellationToken cancellationToken) { - var testCustomVersion = new Version(testVersion.Major, testVersion.Minor, 1); - var currentByond = await instanceClient.Byond.ActiveVersion(cancellationToken); + var currentByond = await instanceClient.Engine.ActiveVersion(cancellationToken); Assert.IsNotNull(currentByond); - Assert.AreEqual(testVersion.Semver(), currentByond.Version); + Assert.AreEqual(testVersion, currentByond.EngineVersion); // Change the active version and check we get delayed while deleting the old one because the watchdog is using it - var setActiveResponse = await instanceClient.Byond.SetActiveVersion( - new ByondVersionRequest + var setActiveResponse = await instanceClient.Engine.SetActiveVersion( + new EngineVersionRequest { - Version = testCustomVersion, + EngineVersion = new EngineVersion + { + Version = testVersion.Version, + SourceSHA = testVersion.SourceSHA, + Engine = testVersion.Engine, + CustomIteration = 1, + } }, null, cancellationToken); @@ -344,10 +414,15 @@ async Task TestDeleteByondInstallErrorCasesAndQueing(CancellationTo Assert.IsNotNull(setActiveResponse); Assert.IsNull(setActiveResponse.InstallJob); - var deleteJob = await instanceClient.Byond.DeleteVersion( - new ByondVersionDeleteRequest + var deleteJob = await instanceClient.Engine.DeleteVersion( + new EngineVersionDeleteRequest { - Version = testVersion, + EngineVersion = new EngineVersion + { + Version = testVersion.Version, + SourceSHA = testVersion.SourceSHA, + Engine = testVersion.Engine, + } }, cancellationToken); @@ -359,10 +434,15 @@ async Task TestDeleteByondInstallErrorCasesAndQueing(CancellationTo Assert.IsTrue(deleteJob.Stage.Contains("Waiting")); // then change it back and check it fails the job because it's active again - setActiveResponse = await instanceClient.Byond.SetActiveVersion( - new ByondVersionRequest + setActiveResponse = await instanceClient.Engine.SetActiveVersion( + new EngineVersionRequest { - Version = testVersion, + EngineVersion = new EngineVersion + { + Version = testVersion.Version, + Engine = testVersion.Engine, + SourceSHA = testVersion.SourceSHA + } }, null, cancellationToken); @@ -370,14 +450,20 @@ async Task TestDeleteByondInstallErrorCasesAndQueing(CancellationTo Assert.IsNotNull(setActiveResponse); Assert.IsNull(setActiveResponse.InstallJob); - await WaitForJob(deleteJob, 5, true, ErrorCode.ByondCannotDeleteActiveVersion, cancellationToken); + await WaitForJob(deleteJob, 5, true, ErrorCode.EngineCannotDeleteActiveVersion, cancellationToken); // finally, queue the last delete job which should complete when the watchdog restarts with a newly deployed .dmb // queue the byond change followed by the deployment for that first - setActiveResponse = await instanceClient.Byond.SetActiveVersion( - new ByondVersionRequest + setActiveResponse = await instanceClient.Engine.SetActiveVersion( + new EngineVersionRequest { - Version = testCustomVersion, + EngineVersion = new EngineVersion + { + Version = testVersion.Version, + Engine = testVersion.Engine, + SourceSHA = testVersion.SourceSHA, + CustomIteration = 1, + } }, null, cancellationToken); @@ -385,10 +471,15 @@ async Task TestDeleteByondInstallErrorCasesAndQueing(CancellationTo Assert.IsNotNull(setActiveResponse); Assert.IsNull(setActiveResponse.InstallJob); - deleteJob = await instanceClient.Byond.DeleteVersion( - new ByondVersionDeleteRequest + deleteJob = await instanceClient.Engine.DeleteVersion( + new EngineVersionDeleteRequest { - Version = testVersion, + EngineVersion = new EngineVersion + { + Version = testVersion.Version, + Engine = testVersion.Engine, + SourceSHA = testVersion.SourceSHA, + } }, cancellationToken); @@ -401,10 +492,8 @@ async Task TestDeleteByondInstallErrorCasesAndQueing(CancellationTo async Task SendChatOverloadCommand(CancellationToken cancellationToken) { // for the code coverage really... - var topicRequestResult = await topicClient.SendTopic( - IPAddress.Loopback, - $"tgs_integration_test_tactics5=1", - ddPort, + var topicRequestResult = await SendTestTopic( + "tgs_integration_test_tactics5=1", cancellationToken); Assert.IsNotNull(topicRequestResult); @@ -467,12 +556,10 @@ async Task DumpTests(CancellationToken cancellationToken) await WaitForJob(restartJob, 20, false, null, cancellationToken); } - Assert.IsTrue(job.ErrorCode == ErrorCode.DreamDaemonOffline || job.ErrorCode == ErrorCode.GCoreFailure, $"{job.ErrorCode}: {job.ExceptionDetails}"); - - await Task.Delay(TimeSpan.FromSeconds(20), cancellationToken); + Assert.IsTrue(job.ErrorCode == ErrorCode.GameServerOffline || job.ErrorCode == ErrorCode.GCoreFailure, $"{job.ErrorCode}: {job.ExceptionDetails}"); - var ddStatus = await instanceClient.DreamDaemon.Read(cancellationToken); - Assert.AreEqual(WatchdogStatus.Online, ddStatus.Status.Value); + var restartJob2 = await instanceClient.DreamDaemon.Restart(cancellationToken); + await WaitForJob(restartJob2, 20, false, null, cancellationToken); } async Task TestDMApiFreeDeploy(CancellationToken cancellationToken) @@ -493,7 +580,8 @@ async Task TestDMApiFreeDeploy(CancellationToken cancellationToken) daemonStatus = await instanceClient.DreamDaemon.Read(cancellationToken); Assert.AreEqual(WatchdogStatus.Online, daemonStatus.Status.Value); - CheckDDPriority(); + ValidateSessionId(daemonStatus, true); + await CheckDDPriority(); Assert.AreEqual(false, daemonStatus.SoftRestart); Assert.AreEqual(false, daemonStatus.SoftShutdown); Assert.AreEqual(string.Empty, daemonStatus.AdditionalParameters); @@ -528,22 +616,32 @@ async Task RunBasicTest(CancellationToken cancellationToken) Assert.AreEqual(WatchdogStatus.Offline, daemonStatus.Status.Value); Assert.IsNotNull(daemonStatus.ActiveCompileJob); + Assert.IsFalse(daemonStatus.SessionId.HasValue); Assert.IsNull(daemonStatus.StagedCompileJob); Assert.AreEqual(DMApiConstants.InteropVersion, daemonStatus.ActiveCompileJob.DMApiVersion); Assert.AreEqual(DreamDaemonSecurity.Trusted, daemonStatus.ActiveCompileJob.MinimumSecurityLevel); JobResponse startJob; if (new PlatformIdentifier().IsWindows) // Can't get address reuse to trigger on linux for some reason - using (var blockSocket = new Socket(SocketType.Stream, ProtocolType.Tcp)) + using (var blockSocket = new Socket( + testVersion.Engine.Value == EngineType.OpenDream + ? SocketType.Dgram + : SocketType.Stream, + testVersion.Engine.Value == EngineType.OpenDream + ? ProtocolType.Udp + : ProtocolType.Tcp)) { blockSocket.SetSocketOption(SocketOptionLevel.Socket, SocketOptionName.ExclusiveAddressUse, true); blockSocket.SetSocketOption(SocketOptionLevel.Socket, SocketOptionName.ReuseAddress, false); + if (testVersion.Engine.Value != EngineType.OpenDream) + blockSocket.SetSocketOption(SocketOptionLevel.Socket, SocketOptionName.DontLinger, true); + blockSocket.Bind(new IPEndPoint(IPAddress.Any, ddPort)); // Don't use StartDD here startJob = await instanceClient.DreamDaemon.Start(cancellationToken); - await WaitForJob(startJob, 40, true, ErrorCode.DreamDaemonPortInUse, cancellationToken); + await WaitForJob(startJob, 40, true, ErrorCode.GameServerPortInUse, cancellationToken); } startJob = await StartDD(cancellationToken); @@ -553,14 +651,16 @@ async Task RunBasicTest(CancellationToken cancellationToken) daemonStatus = await instanceClient.DreamDaemon.Read(cancellationToken); Assert.AreEqual(WatchdogStatus.Online, daemonStatus.Status.Value); - CheckDDPriority(); + ValidateSessionId(daemonStatus, true); + await CheckDDPriority(); Assert.AreEqual(false, daemonStatus.SoftRestart); Assert.AreEqual(false, daemonStatus.SoftShutdown); - await GracefulWatchdogShutdown(60, cancellationToken); + await GracefulWatchdogShutdown(cancellationToken); daemonStatus = await instanceClient.DreamDaemon.Read(cancellationToken); Assert.AreEqual(WatchdogStatus.Offline, daemonStatus.Status.Value); + Assert.IsFalse(daemonStatus.SessionId.HasValue); await CheckDMApiFail(daemonStatus.ActiveCompileJob, cancellationToken, false); @@ -570,25 +670,45 @@ async Task RunBasicTest(CancellationToken cancellationToken) LogOutput = true, }, cancellationToken); Assert.AreEqual(string.Empty, daemonStatus.AdditionalParameters); + Assert.IsFalse(daemonStatus.SessionId.HasValue); + } + + long? sessionIdTracker; + void ValidateSessionId(DreamDaemonResponse daemonStatus, bool? knownIncrease) + { + Assert.IsTrue(daemonStatus.SessionId.HasValue, $"Expected a session ID in the DreamDaemonResponse"); + + if (sessionIdTracker.HasValue) + if (knownIncrease.HasValue) + if (knownIncrease.Value) + Assert.IsTrue(daemonStatus.SessionId.Value > sessionIdTracker.Value, $"Expected a session ID > {sessionIdTracker.Value}, got {daemonStatus.SessionId.Value} instead"); + else + Assert.AreEqual(sessionIdTracker.Value, daemonStatus.SessionId.Value); + else + Assert.IsTrue(daemonStatus.SessionId.Value >= sessionIdTracker.Value, $"Expected a session ID >= {sessionIdTracker.Value}, got {daemonStatus.SessionId.Value} instead"); + + sessionIdTracker = daemonStatus.SessionId.Value; } void TestLinuxIsntBeingFuckingCheekyAboutFilePaths(DreamDaemonResponse currentStatus, CompileJobResponse previousStatus) { - if (new PlatformIdentifier().IsWindows || usingBasicWatchdog) + if (new PlatformIdentifier().IsWindows || watchdogRestartsProcess) return; Assert.IsNotNull(currentStatus.ActiveCompileJob); Assert.IsTrue(currentStatus.ActiveCompileJob.DmeName.Contains("long_running_test")); Assert.AreEqual(WatchdogStatus.Online, currentStatus.Status); - var procs = TestLiveServer.GetDDProcessesOnPort(currentStatus.Port.Value); + var procs = TestLiveServer.GetEngineServerProcessesOnPort(testVersion.Engine.Value, currentStatus.Port.Value); Assert.AreEqual(1, procs.Count); var failingLinks = new List(); using var proc = procs[0]; var pid = proc.Id; var foundLivePath = false; var allPaths = new List(); - foreach (var fd in Directory.EnumerateFiles($"/proc/{pid}/fd")) + + Assert.IsFalse(proc.HasExited); + foreach (var fd in Directory.GetFiles($"/proc/{pid}/fd")) { var sb = new StringBuilder(UInt16.MaxValue); if (Syscall.readlink(fd, sb) == -1) @@ -616,39 +736,30 @@ void TestLinuxIsntBeingFuckingCheekyAboutFilePaths(DreamDaemonResponse currentSt async Task RunHealthCheckTest(bool checkDump, CancellationToken cancellationToken) { System.Console.WriteLine("TEST: WATCHDOG HEALTH CHECK TEST"); -#pragma warning disable CS0618 // Type or member is obsolete - // Check reverse mapping - var status = await instanceClient.DreamDaemon.Update(new DreamDaemonRequest - { - DumpOnHealthCheckRestart = !checkDump, - }, cancellationToken); - - Assert.AreEqual(!checkDump, status.DumpOnHeartbeatRestart); // enable health checks - status = await instanceClient.DreamDaemon.Update(new DreamDaemonRequest + var status = await instanceClient.DreamDaemon.Update(new DreamDaemonRequest { HealthCheckSeconds = 1, - DumpOnHeartbeatRestart = checkDump, + DumpOnHealthCheckRestart = checkDump, }, cancellationToken); - Assert.AreEqual(checkDump, status.DumpOnHeartbeatRestart); -#pragma warning restore CS0618 // Type or member is obsolete Assert.AreEqual(checkDump, status.DumpOnHealthCheckRestart); + Assert.AreEqual(1U, status.HealthCheckSeconds.Value); var startJob = await StartDD(cancellationToken); await WaitForJob(startJob, 40, false, null, cancellationToken); - CheckDDPriority(); + await CheckDDPriority(); // lock on to DD and pause it so it can't health check - var ddProcs = TestLiveServer.GetDDProcessesOnPort(ddPort).Where(x => !x.HasExited).ToList(); + var ddProcs = TestLiveServer.GetEngineServerProcessesOnPort(testVersion.Engine.Value, ddPort).Where(x => !x.HasExited).ToList(); if (ddProcs.Count != 1) Assert.Fail($"Incorrect number of DD processes: {ddProcs.Count}"); using var ddProc = ddProcs.Single(); - IProcessExecutor executor = null; + ProcessExecutor executor = null; executor = new ProcessExecutor( RuntimeInformation.IsOSPlatform(OSPlatform.Windows) ? new WindowsProcessFeatures(Mock.Of>()) @@ -660,39 +771,37 @@ async Task RunHealthCheckTest(bool checkDump, CancellationToken cancellationToke .GetProcess(ddProc.Id); // Ensure it's responding to health checks - await Task.WhenAny(Task.Delay(20000, cancellationToken), ourProcessHandler.Lifetime); + await Task.WhenAny(Task.Delay(7000, cancellationToken), ourProcessHandler.Lifetime); Assert.IsFalse(ddProc.HasExited); // check DD agrees - var topicRequestResult = await topicClient.SendTopic( - IPAddress.Loopback, - $"tgs_integration_test_tactics8=1", - ddPort, + var topicRequestResult = await SendTestTopic( + "tgs_integration_test_tactics8=1", cancellationToken); Assert.IsNotNull(topicRequestResult); Assert.AreEqual(TopicResponseType.StringResponse, topicRequestResult.ResponseType); Assert.IsNotNull(topicRequestResult.StringData); - Assert.AreEqual(topicRequestResult.StringData, "received health check"); + Assert.AreEqual("received health check", topicRequestResult.StringData); - await instanceClient.DreamDaemon.Update(new DreamDaemonRequest + var ddStatus = await instanceClient.DreamDaemon.Update(new DreamDaemonRequest { SoftShutdown = true }, cancellationToken); + ValidateSessionId(ddStatus, true); - ourProcessHandler.Suspend(); + global::System.Console.WriteLine($"WATCHDOG TEST {instanceClient.Metadata.Id}: COMMENCE PROCESS SUSPEND FOR HEALTH CHECK DEATH PID {ourProcessHandler.Id}."); + ourProcessHandler.SuspendProcess(); + global::System.Console.WriteLine($"WATCHDOG TEST {instanceClient.Metadata.Id}: FINISH PROCESS SUSPEND FOR HEALTH CHECK DEATH. WAITING FOR LIFETIME {ourProcessHandler.Id}."); - await Task.WhenAny(ourProcessHandler.Lifetime, Task.Delay(TimeSpan.FromMinutes(1), cancellationToken)); + await Task.WhenAny(ourProcessHandler.Lifetime, Task.Delay(TimeSpan.FromMinutes(4), cancellationToken)); + Assert.IsTrue(ourProcessHandler.Lifetime.IsCompleted); - var timeout = 60; - DreamDaemonResponse ddStatus; + var timeout = 20; do { - await Task.Delay(TimeSpan.FromSeconds(1), cancellationToken); ddStatus = await instanceClient.DreamDaemon.Read(cancellationToken); Assert.AreEqual(1U, ddStatus.HealthCheckSeconds.Value); -#pragma warning disable CS0618 // Type or member is obsolete - Assert.AreEqual(1U, ddStatus.HeartbeatSeconds.Value); if (ddStatus.Status.Value == WatchdogStatus.Offline) { await CheckDMApiFail(ddStatus.ActiveCompileJob, cancellationToken); @@ -701,17 +810,17 @@ await instanceClient.DreamDaemon.Update(new DreamDaemonRequest if (--timeout == 0) Assert.Fail("DreamDaemon didn't shutdown within the timeout!"); + + await Task.Delay(TimeSpan.FromSeconds(3), cancellationToken); } while (timeout > 0); // disable health checks ddStatus = await instanceClient.DreamDaemon.Update(new DreamDaemonRequest { - HeartbeatSeconds = 0, + HealthCheckSeconds = 0, }, cancellationToken); Assert.AreEqual(0U, ddStatus.HealthCheckSeconds.Value); - Assert.AreEqual(0U, ddStatus.HeartbeatSeconds.Value); -#pragma warning restore CS0618 // Type or member is obsolete if (checkDump) { @@ -732,7 +841,7 @@ async Task StartDD(CancellationToken cancellationToken) { try { - SocketExtensions.BindTest(ddPort, false); + SocketExtensions.BindTest(new PlatformIdentifier(), ddPort, false, testVersion.Engine == EngineType.OpenDream); break; } catch @@ -749,7 +858,6 @@ async Task StartDD(CancellationToken cancellationToken) throw; } } - await Task.Delay(TimeSpan.FromSeconds(3), cts.Token); return await instanceClient.DreamDaemon.Start(cancellationToken); } @@ -779,7 +887,9 @@ async Task WhiteBoxValidateBridgeRequestLimitAndTestChunking(CancellationToken c System.Console.WriteLine("TEST: Sending Bridge tests topic..."); - var bridgeTestTopicResult = await topicClient.SendTopic(IPAddress.Loopback, $"tgs_integration_test_tactics2={accessIdentifier}", ddPort, cancellationToken); + var bridgeTestTopicResult = await SendTestTopic( + $"tgs_integration_test_tactics2={accessIdentifier}", + cancellationToken); Assert.AreEqual("ack2", bridgeTestTopicResult.StringData); await bridgeTestsTcs.Task.WaitAsync(cancellationToken); @@ -788,7 +898,7 @@ async Task WhiteBoxValidateBridgeRequestLimitAndTestChunking(CancellationToken c BridgeController.LogContent = true; // Time for DD to revert the bridge access identifier change - await Task.Delay(TimeSpan.FromSeconds(3), cancellationToken); + await Task.Delay(TimeSpan.FromSeconds(1), cancellationToken); } async Task ValidateTopicLimits(CancellationToken cancellationToken) @@ -829,10 +939,8 @@ async Task ValidateTopicLimits(CancellationToken cancellationToken) try { System.Console.WriteLine($"Topic send limit test S:{currentSize}..."); - topicRequestResult = await topicClient.SendTopic( - IPAddress.Loopback, + topicRequestResult = await SendTestTopic( $"tgs_integration_test_tactics3={topicClient.SanitizeString(JsonConvert.SerializeObject(topic, DMApiConstants.SerializerSettings))}", - ddPort, cancellationToken); } catch (ArgumentOutOfRangeException) @@ -844,7 +952,11 @@ async Task ValidateTopicLimits(CancellationToken cancellationToken) || topicRequestResult.StringData != "pass") { if (topicRequestResult != null) + { + Assert.AreEqual(TopicResponseType.StringResponse, topicRequestResult.ResponseType, $"String data is: {topicRequestResult.StringData ?? "<>"}"); Assert.AreEqual("fail", topicRequestResult.StringData); + } + if (currentSize == lastSize + 1) break; baseSize = lastSize; @@ -870,10 +982,8 @@ async Task ValidateTopicLimits(CancellationToken cancellationToken) { var currentSize = baseSize + (int)Math.Pow(2, nextPow); System.Console.WriteLine($"Topic recieve limit test S:{currentSize}..."); - var topicRequestResult = await topicClient.SendTopic( - IPAddress.Loopback, + var topicRequestResult = await SendTestTopic( $"tgs_integration_test_tactics4={topicClient.SanitizeString(currentSize.ToString())}", - ddPort, cancellationToken); if (topicRequestResult.ResponseType != TopicResponseType.StringResponse @@ -894,6 +1004,18 @@ async Task ValidateTopicLimits(CancellationToken cancellationToken) Assert.AreEqual(DMApiConstants.MaximumTopicResponseLength, (uint)lastSize); } + ushort FindTopicPort() + { + using var instanceReference = instanceManager.GetInstanceReference(instanceClient.Metadata); + var watchdog = instanceReference.Watchdog; + + var sessionObj = watchdog.GetType().GetProperty("Server", BindingFlags.Instance | BindingFlags.NonPublic).GetValue(watchdog); + Assert.IsNotNull(sessionObj); + + var session = (ISessionController)sessionObj; + return session.ReattachInformation.TopicPort ?? session.ReattachInformation.Port; + } + // - Uses instance manager concrete // - Injects a custom bridge handler into the bridge registrar and makes the test hack into the DMAPI and change its access_identifier async Task WhiteBoxChatCommandTest(CancellationToken cancellationToken) @@ -902,22 +1024,16 @@ async Task WhiteBoxChatCommandTest(CancellationToken cancellationToken) var startTime = DateTimeOffset.UtcNow - TimeSpan.FromSeconds(5); using (var instanceReference = instanceManager.GetInstanceReference(instanceClient.Metadata)) { - var mockChatUser = new ChatUser - { - Channel = new ChannelRepresentation + var mockChatUser = new ChatUser( + new ChannelRepresentation("test_connection", "Test Connection", 42) { IsAdminChannel = true, - ConnectionName = "test_connection", EmbedsSupported = true, - FriendlyName = "Test Connection", - Id = "test_channel_id", IsPrivateChannel = false, }, - FriendlyName = "Test Sender", - Id = "test_user_id", - Mention = "test_user_mention", - RealId = 1234, - }; + "Test Sender", + "test_user_mention", + 1234); var embedsResponseTask = ((WatchdogBase)instanceReference.Watchdog).HandleChatCommand( "embeds_test", @@ -973,7 +1089,8 @@ static void CheckEmbedsTest(MessageContent embedsResponse, DateTimeOffset startT Assert.AreEqual("Dominion", embedsResponse.Embed.Author?.Name); Assert.AreEqual("https://github.com/Cyberboss", embedsResponse.Embed.Author.Url); Assert.IsTrue(DateTimeOffset.TryParse(embedsResponse.Embed.Timestamp, CultureInfo.InvariantCulture, DateTimeStyles.AssumeLocal, out var timestamp)); - Assert.IsTrue(startTime < timestamp && endTime > timestamp); + var timestampCheck = startTime < timestamp && endTime > timestamp; + Assert.IsTrue(timestampCheck); Assert.AreEqual("https://github.com/tgstation/tgstation-server", embedsResponse.Embed.Url); Assert.AreEqual(3, embedsResponse.Embed.Fields?.Count); Assert.AreEqual("field1", embedsResponse.Embed.Fields.ElementAt(0).Name); @@ -988,14 +1105,15 @@ static void CheckEmbedsTest(MessageContent embedsResponse, DateTimeOffset startT Assert.AreEqual("Footer text", embedsResponse.Embed.Footer?.Text); } - void CheckDDPriority() + async ValueTask CheckDDPriority() { - var allProcesses = TestLiveServer.GetDDProcessesOnPort(ddPort).Where(x => !x.HasExited).ToList(); + await Task.Yield(); + var allProcesses = TestLiveServer.GetEngineServerProcessesOnPort(testVersion.Engine.Value, ddPort).Where(x => !x.HasExited).ToList(); if (allProcesses.Count == 0) - Assert.Fail("Expected DreamDaemon to be running here"); + Assert.Fail("Expected engine server to be running here"); if (allProcesses.Count > 1) - Assert.Fail("Multiple DreamDaemon-like processes running!"); + Assert.Fail("Multiple engine server-like processes running!"); using var process = allProcesses[0]; @@ -1028,7 +1146,8 @@ async Task RunLongRunningTestThenUpdate(CancellationToken cancellationToken) daemonStatus = await DeployTestDme(DmeName, DreamDaemonSecurity.Safe, true, cancellationToken); Assert.AreEqual(WatchdogStatus.Online, daemonStatus.Status.Value); - CheckDDPriority(); + ValidateSessionId(daemonStatus, true); + await CheckDDPriority(); Assert.AreEqual(initialCompileJob.Id, daemonStatus.ActiveCompileJob.Id); var newerCompileJob = daemonStatus.StagedCompileJob; @@ -1038,8 +1157,9 @@ async Task RunLongRunningTestThenUpdate(CancellationToken cancellationToken) Assert.AreEqual(DreamDaemonSecurity.Safe, newerCompileJob.MinimumSecurityLevel); await CheckDMApiFail(daemonStatus.ActiveCompileJob, cancellationToken); - daemonStatus = await TellWorldToReboot(cancellationToken); + daemonStatus = await TellWorldToReboot(true, cancellationToken); + ValidateSessionId(daemonStatus, watchdogRestartsProcess); Assert.AreNotEqual(initialCompileJob.Id, daemonStatus.ActiveCompileJob.Id); Assert.IsNull(daemonStatus.StagedCompileJob); @@ -1072,10 +1192,10 @@ async Task RunLongRunningTestThenUpdateWithNewDme(CancellationToken cancellation daemonStatus = await DeployTestDme("LongRunning/long_running_test_copy", DreamDaemonSecurity.Safe, true, cancellationToken); - + ValidateSessionId(daemonStatus, true); Assert.AreEqual(WatchdogStatus.Online, daemonStatus.Status.Value); Assert.AreEqual(true, daemonStatus.SoftRestart); - CheckDDPriority(); + await CheckDDPriority(); Assert.AreEqual(initialCompileJob.Id, daemonStatus.ActiveCompileJob.Id); var newerCompileJob = daemonStatus.StagedCompileJob; @@ -1085,8 +1205,9 @@ async Task RunLongRunningTestThenUpdateWithNewDme(CancellationToken cancellation Assert.AreEqual(DreamDaemonSecurity.Safe, newerCompileJob.MinimumSecurityLevel); await CheckDMApiFail(daemonStatus.ActiveCompileJob, cancellationToken); - daemonStatus = await TellWorldToReboot(cancellationToken); + daemonStatus = await TellWorldToReboot(true, cancellationToken); + ValidateSessionId(daemonStatus, true); // remember, dme name change triggers reboot Assert.AreNotEqual(initialCompileJob.Id, daemonStatus.ActiveCompileJob.Id); Assert.AreEqual(false, daemonStatus.SoftRestart); Assert.IsNull(daemonStatus.StagedCompileJob); @@ -1103,9 +1224,8 @@ async Task RunLongRunningTestThenUpdateWithByondVersionSwitch(CancellationToken System.Console.WriteLine("TEST: WATCHDOG BYOND VERSION UPDATE TEST"); var versionToInstall = testVersion; - versionToInstall = versionToInstall.Semver(); - var currentByondVersion = await instanceClient.Byond.ActiveVersion(cancellationToken); - Assert.AreNotEqual(versionToInstall, currentByondVersion.Version); + var currentByondVersion = await instanceClient.Engine.ActiveVersion(cancellationToken); + Assert.AreNotEqual(versionToInstall, currentByondVersion.EngineVersion); var initialStatus = await instanceClient.DreamDaemon.Read(cancellationToken); @@ -1113,12 +1233,17 @@ async Task RunLongRunningTestThenUpdateWithByondVersionSwitch(CancellationToken await WaitForJob(startJob, 70, false, null, cancellationToken); - CheckDDPriority(); + await CheckDDPriority(); - var byondInstallJobTask = instanceClient.Byond.SetActiveVersion( - new ByondVersionRequest + var byondInstallJobTask = instanceClient.Engine.SetActiveVersion( + new EngineVersionRequest { - Version = versionToInstall + EngineVersion = new EngineVersion + { + Version = versionToInstall.Version, + Engine = versionToInstall.Engine, + SourceSHA = versionToInstall.SourceSHA, + } }, null, cancellationToken); @@ -1126,7 +1251,7 @@ async Task RunLongRunningTestThenUpdateWithByondVersionSwitch(CancellationToken // This used to be the case but it gets deleted now that we have and test that // Assert.IsNull(byondInstallJob.InstallJob); - await WaitForJob(byondInstallJob.InstallJob, 60, false, null, cancellationToken); + await WaitForJob(byondInstallJob.InstallJob, EngineTest.EngineInstallationTimeout(versionToInstall) + 30, false, null, cancellationToken); const string DmeName = "LongRunning/long_running_test"; @@ -1135,20 +1260,22 @@ async Task RunLongRunningTestThenUpdateWithByondVersionSwitch(CancellationToken var daemonStatus = await instanceClient.DreamDaemon.Read(cancellationToken); Assert.AreEqual(WatchdogStatus.Online, daemonStatus.Status.Value); Assert.IsNotNull(daemonStatus.ActiveCompileJob); - + ValidateSessionId(daemonStatus, true); Assert.AreEqual(initialStatus.ActiveCompileJob.Id, daemonStatus.ActiveCompileJob.Id); var newerCompileJob = daemonStatus.StagedCompileJob; - Assert.AreNotEqual(daemonStatus.ActiveCompileJob.ByondVersion, newerCompileJob.ByondVersion); - Assert.AreEqual(versionToInstall, newerCompileJob.ByondVersion); + Assert.AreNotEqual(daemonStatus.ActiveCompileJob.EngineVersion, newerCompileJob.EngineVersion); + + Assert.AreEqual(versionToInstall, newerCompileJob.EngineVersion); Assert.AreEqual(true, daemonStatus.SoftRestart); await CheckDMApiFail(daemonStatus.ActiveCompileJob, cancellationToken); - daemonStatus = await TellWorldToReboot(cancellationToken); + daemonStatus = await TellWorldToReboot(true, cancellationToken); - Assert.AreEqual(versionToInstall, daemonStatus.ActiveCompileJob.ByondVersion); + Assert.AreEqual(versionToInstall, daemonStatus.ActiveCompileJob.EngineVersion); Assert.IsNull(daemonStatus.StagedCompileJob); + ValidateSessionId(daemonStatus, true); await instanceClient.DreamDaemon.Shutdown(cancellationToken); await CheckDMApiFail(daemonStatus.ActiveCompileJob, cancellationToken); @@ -1160,31 +1287,36 @@ async Task RunLongRunningTestThenUpdateWithByondVersionSwitch(CancellationToken public async Task StartAndLeaveRunning(CancellationToken cancellationToken) { System.Console.WriteLine("TEST: WATCHDOG STARTING ENDLESS"); - var dd = await instanceClient.DreamDaemon.Read(cancellationToken); - if (dd.ActiveCompileJob == null) - await DeployTestDme("LongRunning/long_running_test", DreamDaemonSecurity.Trusted, true, cancellationToken); - var startJob = await StartDD(cancellationToken); await WaitForJob(startJob, 40, false, null, cancellationToken); - var daemonStatus = await instanceClient.DreamDaemon.Read(cancellationToken); + var daemonStatus = await instanceClient.DreamDaemon.Update(new DreamDaemonRequest + { + AdditionalParameters = "slow_start=1", + }, + cancellationToken); - Assert.AreEqual(WatchdogStatus.Online, daemonStatus.Status.Value); - CheckDDPriority(); + Assert.AreEqual(WatchdogStatus.Online, daemonStatus.Status); + Assert.IsTrue(daemonStatus.SoftRestart); + await CheckDDPriority(); Assert.AreEqual(ddPort, daemonStatus.CurrentPort); // Try killing the DD process to ensure it gets set to the restoring state bool firstTime = true; do { + if(!firstTime) + Assert.IsFalse(daemonStatus.SoftRestart); + + ValidateSessionId(daemonStatus, true); KillDD(firstTime); firstTime = false; await Task.Delay(TimeSpan.FromSeconds(1), cancellationToken); daemonStatus = await instanceClient.DreamDaemon.Read(cancellationToken); } while (daemonStatus.Status == WatchdogStatus.Online); - Assert.AreEqual(WatchdogStatus.Restoring, daemonStatus.Status.Value); + Assert.AreEqual(WatchdogStatus.Restoring, daemonStatus.Status); // Kill it again do @@ -1193,18 +1325,24 @@ public async Task StartAndLeaveRunning(CancellationToken cancellationToken) daemonStatus = await instanceClient.DreamDaemon.Read(cancellationToken); } while (daemonStatus.Status == WatchdogStatus.Online || daemonStatus.Status == WatchdogStatus.Restoring); - Assert.AreEqual(WatchdogStatus.DelayedRestart, daemonStatus.Status.Value); + Assert.AreEqual(WatchdogStatus.DelayedRestart, daemonStatus.Status); await Task.Delay(TimeSpan.FromSeconds(15), cancellationToken); - daemonStatus = await instanceClient.DreamDaemon.Read(cancellationToken); - Assert.AreEqual(WatchdogStatus.Online, daemonStatus.Status.Value); + daemonStatus = await instanceClient.DreamDaemon.Update(new DreamDaemonRequest + { + AdditionalParameters = String.Empty, + }, + cancellationToken); + ValidateSessionId(daemonStatus, true); + Assert.AreEqual(WatchdogStatus.Online, daemonStatus.Status); + Assert.IsTrue(daemonStatus.SoftRestart); await CheckDMApiFail(daemonStatus.ActiveCompileJob, cancellationToken); } bool KillDD(bool require) { - var ddProcs = TestLiveServer.GetDDProcessesOnPort(ddPort).Where(x => !x.HasExited).ToList(); + var ddProcs = TestLiveServer.GetEngineServerProcessesOnPort(testVersion.Engine.Value, ddPort).Where(x => !x.HasExited).ToList(); if (require && ddProcs.Count == 0 || ddProcs.Count > 1) Assert.Fail($"Incorrect number of DD processes: {ddProcs.Count}"); @@ -1215,15 +1353,17 @@ bool KillDD(bool require) return ddProc != null; } - public Task TellWorldToReboot(CancellationToken cancellationToken) => TellWorldToReboot2(instanceClient, topicClient, ddPort, cancellationToken); - public static async Task TellWorldToReboot2(IInstanceClient instanceClient, ITopicClient topicClient, ushort ddPort, CancellationToken cancellationToken) + public Task TellWorldToReboot(bool waitForOnlineIfRestoring, CancellationToken cancellationToken, [CallerLineNumber]int source = 0) + => TellWorldToReboot2(instanceClient, instanceManager, topicClient, FindTopicPort(), waitForOnlineIfRestoring || testVersion.Engine.Value == EngineType.OpenDream, cancellationToken, source); + public static async Task TellWorldToReboot2(IInstanceClient instanceClient, IInstanceManager instanceManager, ITopicClient topicClient, ushort topicPort, bool waitForOnlineIfRestoring, CancellationToken cancellationToken, [CallerLineNumber]int source = 0, [CallerFilePath]string path = null) { var daemonStatus = await instanceClient.DreamDaemon.Read(cancellationToken); Assert.IsNotNull(daemonStatus.StagedCompileJob); - var initialCompileJob = daemonStatus.ActiveCompileJob; + var initialSession = daemonStatus.ActiveCompileJob; - System.Console.WriteLine("TEST: Sending world reboot topic..."); - var result = await topicClient.SendTopic(IPAddress.Loopback, "tgs_integration_test_special_tactics=1", ddPort, cancellationToken); + System.Console.WriteLine($"TEST: Sending world reboot topic @ {path}#L{source}"); + + var result = await SendTestTopic("tgs_integration_test_special_tactics=1", topicClient, instanceManager.GetInstanceReference(instanceClient.Metadata), topicPort, cancellationToken); Assert.AreEqual("ack", result.StringData); using var tempCts = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken); @@ -1237,10 +1377,18 @@ public static async Task TellWorldToReboot2(IInstanceClient await Task.Delay(TimeSpan.FromSeconds(1), tempToken); daemonStatus = await instanceClient.DreamDaemon.Read(tempToken); } - while (initialCompileJob.Id == daemonStatus.ActiveCompileJob.Id); + while (initialSession.Id == daemonStatus.ActiveCompileJob.Id); } - await Task.Delay(TimeSpan.FromSeconds(3), cancellationToken); + if (waitForOnlineIfRestoring && daemonStatus.Status == WatchdogStatus.Restoring) + { + do + { + await Task.Delay(TimeSpan.FromSeconds(1), tempToken); + daemonStatus = await instanceClient.DreamDaemon.Read(tempToken); + } + while (daemonStatus.Status == WatchdogStatus.Restoring); + } return daemonStatus; } @@ -1252,7 +1400,7 @@ async Task DeployTestDme(string dmeName, DreamDaemonSecurit ApiValidationSecurityLevel = deploymentSecurity, ProjectName = dmeName.Contains("rooted") ? dmeName : $"tests/DMAPI/{dmeName}", RequireDMApiValidation = requireApi, - Timeout = TimeSpan.FromMilliseconds(1), + Timeout = !ranTimeoutTest ? TimeSpan.FromMilliseconds(1) : TimeSpan.FromMinutes(5), }, cancellationToken); JobResponse compileJobJob; @@ -1265,24 +1413,33 @@ async Task DeployTestDme(string dmeName, DreamDaemonSecurit compileJobJob = await instanceClient.DreamMaker.Compile(cancellationToken); await WaitForJob(compileJobJob, 90, true, ErrorCode.DeploymentTimeout, cancellationToken); + + await instanceClient.DreamMaker.Update(new DreamMakerRequest + { + Timeout = TimeSpan.FromMinutes(5), + }, cancellationToken); ranTimeoutTest = true; } - await instanceClient.DreamMaker.Update(new DreamMakerRequest - { - Timeout = TimeSpan.FromMinutes(5), - }, cancellationToken); - compileJobJob = await instanceClient.DreamMaker.Compile(cancellationToken); await WaitForJob(compileJobJob, 90, false, null, cancellationToken); + // annoying but, with signalR instant job updates, this running task can get queued before the task that processes the watchdog's monitor activation + for (var i = 0; i < 10; ++i) + await Task.Yield(); + var ddInfo = await instanceClient.DreamDaemon.Read(cancellationToken); + var targetJob = ddInfo.StagedCompileJob ?? ddInfo.ActiveCompileJob; + Assert.IsNotNull(targetJob); if (requireApi) - Assert.IsNotNull((ddInfo.StagedCompileJob ?? ddInfo.ActiveCompileJob).DMApiVersion); + Assert.IsNotNull(targetJob.DMApiVersion); + else + Assert.IsNull(targetJob.DMApiVersion); + return ddInfo; } - async Task GracefulWatchdogShutdown(uint timeout, CancellationToken cancellationToken) + async Task GracefulWatchdogShutdown(CancellationToken cancellationToken) { await instanceClient.DreamDaemon.Update(new DreamDaemonRequest { @@ -1292,9 +1449,10 @@ await instanceClient.DreamDaemon.Update(new DreamDaemonRequest var newStatus = await instanceClient.DreamDaemon.Read(cancellationToken); Assert.IsTrue(newStatus.SoftShutdown.Value || newStatus.Status.Value == WatchdogStatus.Offline); + var timeout = 20; do { - await Task.Delay(TimeSpan.FromSeconds(1), cancellationToken); + await Task.Delay(TimeSpan.FromSeconds(3), cancellationToken); var ddStatus = await instanceClient.DreamDaemon.Read(cancellationToken); if (ddStatus.Status.Value == WatchdogStatus.Offline) break; @@ -1337,5 +1495,16 @@ async Task CheckDMApiFail(CompileJobResponse compileJob, CancellationToken cance var logtext = await File.ReadAllTextAsync(logfile.FullName, cancellationToken); Assert.IsFalse(String.IsNullOrWhiteSpace(logtext)); } + + async ValueTask TestLegacyBridgeEndpoint(CancellationToken cancellationToken) + { + System.Console.WriteLine("TEST: TestLegacyBridgeEndpoint"); + var result = await SendTestTopic( + "im_out_of_memes=1", + cancellationToken); + Assert.IsNotNull(result); + Assert.AreEqual("all gucci", result.StringData); + await CheckDMApiFail((await instanceClient.DreamDaemon.Read(cancellationToken)).ActiveCompileJob, cancellationToken); + } } } diff --git a/tests/Tgstation.Server.Tests/Live/LiveTestingServer.cs b/tests/Tgstation.Server.Tests/Live/LiveTestingServer.cs index 5f78b526b16..645edac147d 100644 --- a/tests/Tgstation.Server.Tests/Live/LiveTestingServer.cs +++ b/tests/Tgstation.Server.Tests/Live/LiveTestingServer.cs @@ -10,6 +10,7 @@ using System.Threading; using System.Threading.Tasks; +using Tgstation.Server.Api; using Tgstation.Server.Api.Models; using Tgstation.Server.Host; using Tgstation.Server.Host.Configuration; @@ -23,6 +24,7 @@ namespace Tgstation.Server.Tests.Live { sealed class LiveTestingServer : IServer, IDisposable { + static bool needCleanup = false; public static string BaseDirectory { get; } static LiveTestingServer() @@ -32,7 +34,7 @@ static LiveTestingServer() if (string.IsNullOrWhiteSpace(BaseDirectory)) { BaseDirectory = Path.Combine(Path.GetTempPath(), "TGS_INTEGRATION_TEST"); - Cleanup(BaseDirectory).GetAwaiter().GetResult(); + needCleanup = true; } } @@ -50,7 +52,11 @@ static async Task Cleanup(string directory) } } - public Uri Url { get; } + public Uri ApiUrl { get; } + + public Uri RootUrl { get; } + + public Uri OpenDreamUrl { get; } public string Directory { get; } @@ -76,13 +82,20 @@ static async Task Cleanup(string directory) public LiveTestingServer(SwarmConfiguration swarmConfiguration, bool enableOAuth, ushort port = 15010) { + if (needCleanup) + { + needCleanup = false; + Cleanup(BaseDirectory).GetAwaiter().GetResult(); + } + Assert.IsTrue(port >= 10000); // for testing bridge request limit Directory = BaseDirectory; - Directory = Path.Combine(Directory, Guid.NewGuid().ToString()); + Directory = Path.Combine(Directory, swarmConfiguration?.Identifier ?? "default"); System.IO.Directory.CreateDirectory(Directory); string urlString = $"http://localhost:{port}"; - Url = new Uri(urlString); + RootUrl = new Uri(urlString); + ApiUrl = new Uri(urlString + Routes.ApiRoot); //so we need a db //we have to rely on env vars @@ -112,6 +125,12 @@ public LiveTestingServer(SwarmConfiguration swarmConfiguration, bool enableOAuth HighPriorityDreamDaemon = nicingAllowed; LowPriorityDeployments = nicingAllowed; + var odGitDir = Environment.GetEnvironmentVariable("TGS_TEST_OD_GIT_DIRECTORY"); + if (!String.IsNullOrWhiteSpace(odGitDir)) + OpenDreamUrl = new Uri($"file://{Path.GetFullPath(odGitDir).Replace('\\', '/')}"); + else + OpenDreamUrl = new GeneralConfiguration().OpenDreamGitUrl; + args = new List() { String.Format(CultureInfo.InvariantCulture, "Database:DropDatabase={0}", true), // Replaced after first Run @@ -132,6 +151,10 @@ public LiveTestingServer(SwarmConfiguration swarmConfiguration, bool enableOAuth "General:ByondTopicTimeout=10000", $"Session:HighPriorityLiveDreamDaemon={HighPriorityDreamDaemon}", $"Session:LowPriorityDeploymentProcesses={LowPriorityDeployments}", + $"General:SkipAddingByondFirewallException={!TestingUtils.RunningInGitHubActions}", + $"General:OpenDreamGitUrl={OpenDreamUrl}", + $"Security:TokenExpiryMinutes=120", // timeouts are useless for us + $"General:OpenDreamSuppressInstallOutput={TestingUtils.RunningInGitHubActions}", }; swarmArgs = new List(); diff --git a/tests/Tgstation.Server.Tests/Live/RawRequestTests.cs b/tests/Tgstation.Server.Tests/Live/RawRequestTests.cs index 7e4a1010015..3e20609e600 100644 --- a/tests/Tgstation.Server.Tests/Live/RawRequestTests.cs +++ b/tests/Tgstation.Server.Tests/Live/RawRequestTests.cs @@ -37,7 +37,7 @@ static async Task TestRequestValidation(IServerClient serverClient, Cancellation var token = serverClient.Token.Bearer; // check that 400s are returned appropriately using var httpClient = new HttpClient(); - using (var request = new HttpRequestMessage(HttpMethod.Get, url.ToString())) + using (var request = new HttpRequestMessage(HttpMethod.Get, url.ToString() + Routes.ApiRoot.TrimStart('/'))) { request.Headers.Accept.Clear(); request.Headers.UserAgent.Add(new ProductInfoHeaderValue("RootTest", "1.0.0")); @@ -45,7 +45,7 @@ static async Task TestRequestValidation(IServerClient serverClient, Cancellation Assert.AreEqual(HttpStatusCode.NotAcceptable, response.StatusCode); } - using (var request = new HttpRequestMessage(HttpMethod.Get, url.ToString())) + using (var request = new HttpRequestMessage(HttpMethod.Get, url.ToString() + Routes.ApiRoot.TrimStart('/'))) { request.Headers.Accept.Clear(); request.Headers.UserAgent.Add(new ProductInfoHeaderValue("RootTest", "1.0.0")); @@ -54,7 +54,7 @@ static async Task TestRequestValidation(IServerClient serverClient, Cancellation Assert.AreEqual(HttpStatusCode.NotAcceptable, response.StatusCode); } - using (var request = new HttpRequestMessage(HttpMethod.Get, url.ToString())) + using (var request = new HttpRequestMessage(HttpMethod.Get, url.ToString() + Routes.ApiRoot.TrimStart('/'))) { request.Headers.Accept.Clear(); request.Headers.UserAgent.Add(new ProductInfoHeaderValue("RootTest", "1.0.0")); @@ -66,7 +66,7 @@ static async Task TestRequestValidation(IServerClient serverClient, Cancellation Assert.AreEqual(ErrorCode.BadHeaders, message.ErrorCode); } - using (var request = new HttpRequestMessage(HttpMethod.Get, url.ToString())) + using (var request = new HttpRequestMessage(HttpMethod.Get, url.ToString() + Routes.ApiRoot.TrimStart('/'))) { request.Headers.Accept.Clear(); request.Headers.UserAgent.Add(new ProductInfoHeaderValue("RootTest", "1.0.0")); @@ -79,7 +79,7 @@ static async Task TestRequestValidation(IServerClient serverClient, Cancellation Assert.AreEqual(ApiHeaders.Version, message.ApiVersion); } - using (var request = new HttpRequestMessage(HttpMethod.Get, url.ToString())) + using (var request = new HttpRequestMessage(HttpMethod.Get, url.ToString() + Routes.ApiRoot.TrimStart('/'))) { request.Headers.Accept.Clear(); request.Headers.UserAgent.Add(new ProductInfoHeaderValue("RootTest", "1.0.0")); @@ -87,7 +87,7 @@ static async Task TestRequestValidation(IServerClient serverClient, Cancellation request.Headers.Add(ApiHeaders.ApiVersionHeader, "Tgstation.Server.Api/6.0.0"); request.Headers.Authorization = new AuthenticationHeaderValue(ApiHeaders.BearerAuthenticationScheme, token); using var response = await httpClient.SendAsync(request, cancellationToken); - Assert.AreEqual(HttpStatusCode.UpgradeRequired, response.StatusCode); + Assert.AreEqual(HttpStatusCode.BadRequest, response.StatusCode); var content = await response.Content.ReadAsStringAsync(cancellationToken); var message = JsonConvert.DeserializeObject(content); Assert.AreEqual(ErrorCode.ApiMismatch, message.ErrorCode); @@ -101,7 +101,7 @@ static async Task TestRequestValidation(IServerClient serverClient, Cancellation request.Headers.Add(ApiHeaders.ApiVersionHeader, "Tgstation.Server.Api/6.0.0"); request.Headers.Authorization = new AuthenticationHeaderValue(ApiHeaders.BearerAuthenticationScheme, token); using var response = await httpClient.SendAsync(request, cancellationToken); - Assert.AreEqual(HttpStatusCode.UpgradeRequired, response.StatusCode); + Assert.AreEqual(HttpStatusCode.BadRequest, response.StatusCode); var content = await response.Content.ReadAsStringAsync(cancellationToken); var message = JsonConvert.DeserializeObject(content); Assert.AreEqual(ErrorCode.ApiMismatch, message.ErrorCode); @@ -149,7 +149,7 @@ static async Task TestRequestValidation(IServerClient serverClient, Cancellation Assert.AreEqual(ErrorCode.InstanceHeaderRequired, message.ErrorCode); } - using (var request = new HttpRequestMessage(HttpMethod.Get, url.ToString())) + using (var request = new HttpRequestMessage(HttpMethod.Get, url.ToString() + Routes.ApiRoot.TrimStart('/'))) { request.Headers.Accept.Clear(); request.Headers.UserAgent.Add(new ProductInfoHeaderValue("RootTest", "1.0.0")); @@ -163,7 +163,7 @@ static async Task TestRequestValidation(IServerClient serverClient, Cancellation Assert.AreEqual(ErrorCode.BadHeaders, message.ErrorCode); } - using (var request = new HttpRequestMessage(HttpMethod.Post, url.ToString())) + using (var request = new HttpRequestMessage(HttpMethod.Post, url.ToString() + Routes.ApiRoot.TrimStart('/'))) { request.Headers.Accept.Clear(); request.Headers.UserAgent.Add(new ProductInfoHeaderValue("RootTest", "1.0.0")); @@ -178,7 +178,7 @@ static async Task TestRequestValidation(IServerClient serverClient, Cancellation Assert.AreEqual(ErrorCode.BadHeaders, message.ErrorCode); } - using (var request = new HttpRequestMessage(HttpMethod.Get, url.ToString())) + using (var request = new HttpRequestMessage(HttpMethod.Get, url.ToString() + Routes.ApiRoot.TrimStart('/'))) { request.Headers.Accept.Clear(); request.Headers.UserAgent.Add(new ProductInfoHeaderValue("RootTest", "1.0.0")); @@ -210,13 +210,10 @@ static async Task TestServerInformation(IServerClientFactory clientFactory, ISer Assert.AreEqual(RuntimeInformation.IsOSPlatform(OSPlatform.Windows), serverInfo.WindowsHost); //check that modifying the token even slightly fucks up the auth -#pragma warning disable CS0618 // Type or member is obsolete var newToken = new TokenResponse { - ExpiresAt = serverClient.Token.ExpiresAt, Bearer = serverClient.Token.Bearer + '0' }; -#pragma warning restore CS0618 // Type or member is obsolete var badClient = clientFactory.CreateFromToken(serverClient.Url, newToken); await ApiAssert.ThrowsException(() => badClient.Administration.Read(cancellationToken)); @@ -232,7 +229,7 @@ static async Task TestOAuthFails(IServerClient serverClient, CancellationToken c // just hitting each type of oauth provider for coverage foreach (var I in Enum.GetValues(typeof(OAuthProvider))) - using (var request = new HttpRequestMessage(HttpMethod.Post, url.ToString())) + using (var request = new HttpRequestMessage(HttpMethod.Post, url.ToString() + Routes.ApiRoot.TrimStart('/'))) { request.Headers.Accept.Clear(); request.Headers.UserAgent.Add(new ProductInfoHeaderValue("RootTest", "1.0.0")); @@ -384,6 +381,10 @@ static async Task TestSignalRUsage(IServerClientFactory serverClientFactory, ISe .GetValue(serverClient)) .Headers .SetHubConnectionHeaders(options.Headers); + }) + .AddNewtonsoftJsonProtocol(options => + { + // we can get away without setting the serializer settings here }); hubConnectionBuilder.ConfigureLogging( diff --git a/tests/Tgstation.Server.Tests/Live/TestLiveServer.cs b/tests/Tgstation.Server.Tests/Live/TestLiveServer.cs index 696512cb2ae..9ac95ce865e 100644 --- a/tests/Tgstation.Server.Tests/Live/TestLiveServer.cs +++ b/tests/Tgstation.Server.Tests/Live/TestLiveServer.cs @@ -28,6 +28,7 @@ using Npgsql; using Tgstation.Server.Api; +using Tgstation.Server.Api.Extensions; using Tgstation.Server.Api.Models; using Tgstation.Server.Api.Models.Request; using Tgstation.Server.Api.Models.Response; @@ -39,7 +40,9 @@ using Tgstation.Server.Host.Configuration; using Tgstation.Server.Host.Database; using Tgstation.Server.Host.Extensions; +using Tgstation.Server.Host.Jobs; using Tgstation.Server.Host.System; +using Tgstation.Server.Host.Utils; using Tgstation.Server.Tests.Live.Instance; namespace Tgstation.Server.Tests.Live @@ -51,24 +54,51 @@ public sealed class TestLiveServer { public static readonly Version TestUpdateVersion = new(5, 11, 0); - static readonly ushort mainDDPort = FreeTcpPort(); - static readonly ushort mainDMPort = FreeTcpPort(mainDDPort); - static readonly ushort compatDMPort = FreeTcpPort(mainDDPort, mainDMPort); - static readonly ushort compatDDPort = FreeTcpPort(mainDDPort, mainDMPort, compatDMPort); + static readonly Lazy odDMPort = new(() => FreeTcpPort()); + static readonly Lazy odDDPort = new(() => FreeTcpPort(odDMPort.Value)); + static readonly Lazy compatDMPort = new(() => FreeTcpPort(odDDPort.Value, odDMPort.Value)); + static readonly Lazy compatDDPort = new(() => FreeTcpPort(odDDPort.Value, odDMPort.Value, compatDMPort.Value)); + static readonly Lazy mainDDPort = new(() => FreeTcpPort(odDDPort.Value, odDMPort.Value, compatDMPort.Value, compatDDPort.Value)); + static readonly Lazy mainDMPort = new(() => FreeTcpPort(odDDPort.Value, odDMPort.Value, compatDMPort.Value, compatDDPort.Value, mainDDPort.Value)); - readonly IServerClientFactory clientFactory = new ServerClientFactory(new ProductHeaderValue(Assembly.GetExecutingAssembly().GetName().Name, Assembly.GetExecutingAssembly().GetName().Version.ToString())); + readonly ServerClientFactory clientFactory = new (new ProductHeaderValue(Assembly.GetExecutingAssembly().GetName().Name, Assembly.GetExecutingAssembly().GetName().Version.ToString())); - public static List GetDDProcessesOnPort(ushort? port) + public static List GetEngineServerProcessesOnPort(EngineType engineType, ushort? port) { var result = new List(); - result.AddRange(System.Diagnostics.Process.GetProcessesByName("DreamDaemon")); - if (new PlatformIdentifier().IsWindows) - result.AddRange(System.Diagnostics.Process.GetProcessesByName("dd")); + + switch (engineType) { + case EngineType.Byond: + result.AddRange(System.Diagnostics.Process.GetProcessesByName("DreamDaemon")); + if (new PlatformIdentifier().IsWindows) + result.AddRange(System.Diagnostics.Process.GetProcessesByName("dd")); + break; + case EngineType.OpenDream: + result.AddRange(System.Diagnostics.Process.GetProcessesByName("Robust.Server")); + break; + default: + Assert.Fail($"Unknown engine type: {engineType}"); + return null; + } if (port.HasValue) result = result.Where(x => { - if (GetCommandLine(x)?.Contains($"-port {port.Value}") ?? false) + string portString = null; + switch (engineType) + { + case EngineType.OpenDream: + portString = $"--cvar net.port={port.Value}"; + break; + case EngineType.Byond: + portString = $"-port {port.Value}"; + break; + default: + Assert.Fail($"Unknown engine type: {engineType}"); + break; + } + + if (GetCommandLine(x)?.Contains(portString) ?? false) return true; x.Dispose(); @@ -100,20 +130,26 @@ private static string GetCommandLine(System.Diagnostics.Process process) } } - static void TerminateAllDDs() + static bool TerminateAllEngineServers() { - foreach (var proc in GetDDProcessesOnPort(null)) - using (proc) - { - proc.Kill(); - proc.WaitForExit(); - } + var result = false; + foreach (var enumValue in Enum.GetValues()) + foreach (var proc in GetEngineServerProcessesOnPort(enumValue, null)) + using (proc) + { + proc.Kill(); + proc.WaitForExit(); + result = true; + } + + return result; } static ushort FreeTcpPort(params ushort[] usedPorts) { ushort result; var listeners = new List(); + try { do @@ -132,7 +168,7 @@ static ushort FreeTcpPort(params ushort[] usedPorts) result = (ushort)((IPEndPoint)l.LocalEndpoint).Port; } - while (usedPorts.Contains(result)); + while (usedPorts.Contains(result) || result < 20000); } finally { @@ -141,18 +177,24 @@ static ushort FreeTcpPort(params ushort[] usedPorts) l.Stop(); } } + + Console.WriteLine($"Allocated port: {result}"); return result; } [ClassInitialize] public static async Task Initialize(TestContext _) { + // Clear problematic environment variables + Environment.SetEnvironmentVariable("MSBuildExtensionsPath", null); + Environment.SetEnvironmentVariable("MSBuildSDKsPath", null); + if (TestingUtils.RunningInGitHubActions || String.IsNullOrWhiteSpace(Environment.GetEnvironmentVariable("TGS_TEST_GITHUB_TOKEN"))) - await DummyGitHubService.InitializeAndInject(default); + await TestingGitHubService.InitializeAndInject(default); await CachingFileDownloader.InitializeAndInjectForLiveTests(default); - await DummyChatProvider.RandomDisconnections(true, default); + DummyChatProvider.RandomDisconnections(true); ServerClientFactory.ApiClientFactory = new RateLimitRetryingApiClientFactory(); var connectionString = Environment.GetEnvironmentVariable("TGS_TEST_CONNECTION_STRING"); @@ -258,11 +300,11 @@ async ValueTask TestWithoutAndWithPermission(Func( () => adminClient.Administration.Update( new ServerUpdateRequest @@ -445,7 +487,7 @@ public async Task TestOneServerSwarmUpdate() try { - await using var controllerClient = await CreateAdminClient(controller.Url, cancellationToken); + await using var controllerClient = await CreateAdminClient(controller.ApiUrl, cancellationToken); var controllerInfo = await controllerClient.ServerInformation(cancellationToken); @@ -513,7 +555,6 @@ public async Task TestSwarmSynchronizationAndUpdates() { // cleanup existing directories new LiveTestingServer(null, false).Dispose(); - const string PrivateKey = "adlfj73ywifhks7iwrgfegjs"; var controllerAddress = new Uri("http://localhost:15011"); @@ -547,9 +588,9 @@ public async Task TestSwarmSynchronizationAndUpdates() try { - await using var controllerClient = await CreateAdminClient(controller.Url, cancellationToken); - await using var node1Client = await CreateAdminClient(node1.Url, cancellationToken); - await using var node2Client = await CreateAdminClient(node2.Url, cancellationToken); + await using var controllerClient = await CreateAdminClient(controller.ApiUrl, cancellationToken); + await using var node1Client = await CreateAdminClient(node1.ApiUrl, cancellationToken); + await using var node2Client = await CreateAdminClient(node2.ApiUrl, cancellationToken); var controllerInfo = await controllerClient.ServerInformation(cancellationToken); @@ -618,7 +659,7 @@ await Task.WhenAny( newUser.Name, "asdfasdfasdfasdf"); - await using var node1BadClient = clientFactory.CreateFromToken(node1.Url, controllerUserClient.Token); + await using var node1BadClient = clientFactory.CreateFromToken(node1.RootUrl, controllerUserClient.Token); await ApiAssert.ThrowsException(() => node1BadClient.Administration.Read(cancellationToken)); // check instance info is not shared @@ -688,8 +729,8 @@ void CheckServerUpdated(LiveTestingServer server) controller.Run(cancellationToken).AsTask(), node1.Run(cancellationToken).AsTask()); - await using var controllerClient2 = await CreateAdminClient(controller.Url, cancellationToken); - await using var node1Client2 = await CreateAdminClient(node1.Url, cancellationToken); + await using var controllerClient2 = await CreateAdminClient(controller.ApiUrl, cancellationToken); + await using var node1Client2 = await CreateAdminClient(node1.ApiUrl, cancellationToken); await ApiAssert.ThrowsException(() => controllerClient2.Administration.Update( new ServerUpdateRequest @@ -704,7 +745,7 @@ await ApiAssert.ThrowsException(() = serverTask, node2.Run(cancellationToken).AsTask()); - await using var node2Client2 = await CreateAdminClient(node2.Url, cancellationToken); + await using var node2Client2 = await CreateAdminClient(node2.ApiUrl, cancellationToken); async Task WaitForSwarmServerUpdate2() { @@ -818,9 +859,9 @@ public async Task TestSwarmReconnection() try { - await using var controllerClient = await CreateAdminClient(controller.Url, cancellationToken); - await using var node1Client = await CreateAdminClient(node1.Url, cancellationToken); - await using var node2Client = await CreateAdminClient(node2.Url, cancellationToken); + await using var controllerClient = await CreateAdminClient(controller.ApiUrl, cancellationToken); + await using var node1Client = await CreateAdminClient(node1.ApiUrl, cancellationToken); + await using var node2Client = await CreateAdminClient(node2.ApiUrl, cancellationToken); var controllerInfo = await controllerClient.ServerInformation(cancellationToken); @@ -900,7 +941,7 @@ await Task.WhenAny( Assert.IsTrue(controllerTask.IsCompleted); controllerTask = controller.Run(cancellationToken).AsTask(); - await using var controllerClient2 = await CreateAdminClient(controller.Url, cancellationToken); + await using var controllerClient2 = await CreateAdminClient(controller.ApiUrl, cancellationToken); // node 2 should reconnect once it's health check triggers await Task.WhenAny( @@ -937,7 +978,7 @@ await ApiAssert.ThrowsException( ErrorCode.SwarmIntegrityCheckFailed); node2Task = node2.Run(cancellationToken).AsTask(); - await using var node2Client2 = await CreateAdminClient(node2.Url, cancellationToken); + await using var node2Client2 = await CreateAdminClient(node2.ApiUrl, cancellationToken); // should re-register await Task.WhenAny( @@ -970,7 +1011,7 @@ async ValueTask TestTgstation(bool interactive) var discordConnectionString = Environment.GetEnvironmentVariable("TGS_TEST_DISCORD_TOKEN"); var procs = System.Diagnostics.Process.GetProcessesByName("byond"); - if (procs.Any()) + if (procs.Length != 0) { foreach (var proc in procs) proc.Dispose(); @@ -987,14 +1028,14 @@ async ValueTask TestTgstation(bool interactive) }); using var server = new LiveTestingServer(null, true); - TerminateAllDDs(); + TerminateAllEngineServers(); using var serverCts = new CancellationTokenSource(); var cancellationToken = serverCts.Token; var serverTask = server.Run(cancellationToken); try { - await using var adminClient = await CreateAdminClient(server.Url, cancellationToken); + await using var adminClient = await CreateAdminClient(server.ApiUrl, cancellationToken); var instanceManagerTest = new InstanceManagerTest(adminClient, server.Directory); var instance = await instanceManagerTest.CreateTestInstance("TgTestInstance", cancellationToken); @@ -1012,7 +1053,7 @@ async ValueTask TestTgstation(bool interactive) var ioManager = new Host.IO.DefaultIOManager(); var repoPath = ioManager.ConcatPath(instance.Path, "Repository"); - var jobsTest = new JobsRequiredTest(instanceClient.Jobs); + await using var jobsTest = new JobsRequiredTest(instanceClient.Jobs); var postWriteHandler = (Host.IO.IPostWriteHandler)(new PlatformIdentifier().IsWindows ? new Host.IO.WindowsPostWriteHandler() : new Host.IO.PosixPostWriteHandler(loggerFactory.CreateLogger())); @@ -1038,7 +1079,7 @@ await ioManager.CopyDirectory( null, cancellationToken); - IProcessExecutor processExecutor = null; + ProcessExecutor processExecutor = null; processExecutor = new ProcessExecutor( new PlatformIdentifier().IsWindows ? new WindowsProcessFeatures(loggerFactory.CreateLogger()) @@ -1113,9 +1154,13 @@ async ValueTask RunGitCommand(string args) const string MinorPrefix = "export BYOND_MINOR="; var minor = Int32.Parse(lines.First(x => x.StartsWith(MinorPrefix))[MinorPrefix.Length..]); - var byondJob = await instanceClient.Byond.SetActiveVersion(new ByondVersionRequest + var byondJob = await instanceClient.Engine.SetActiveVersion(new EngineVersionRequest { - Version = new Version(major, minor) + EngineVersion = new EngineVersion + { + Version = new Version(major, minor), + Engine = EngineType.Byond, + }, }, null, cancellationToken); var byondJobTask = jobsTest.WaitForJob(byondJob.InstallJob, 60, false, null, cancellationToken); @@ -1166,7 +1211,18 @@ await instanceClient.DreamDaemon.Update(new DreamDaemonRequest } [TestMethod] - public async Task TestStandardTgsOperation() + public Task TestStandardTgsOperation() => TestStandardTgsOperation(false); + + [TestMethod] + public Task TestOpenDreamExclusiveTgsOperation() + { + if (Environment.GetEnvironmentVariable("TGS_TEST_OD_EXCLUSIVE") != "true") + Assert.Inconclusive("This test is covered by TestStandardTgsOperation"); + + return TestStandardTgsOperation(true); + } + + async Task TestStandardTgsOperation(bool openDreamOnly) { using(var currentProcess = System.Diagnostics.Process.GetCurrentProcess()) { @@ -1186,16 +1242,19 @@ public async Task TestStandardTgsOperation() using var hardCancellationTokenSource = new CancellationTokenSource(TimeSpan.FromMinutes(maximumTestMinutes)); var hardCancellationToken = hardCancellationTokenSource.Token; + hardCancellationToken.Register(() => Console.WriteLine("TGS TEST CANCELLED TOKEN DUE TO TIMEOUT")); + ServiceCollectionExtensions.UseAdditionalLoggerProvider(); var failureTask = HardFailLoggerProvider.FailureSource; - var internalTask = TestTgsInternal(hardCancellationToken); + var internalTask = TestTgsInternal(openDreamOnly, hardCancellationToken); await Task.WhenAny( internalTask, failureTask); if (!internalTask.IsCompleted) { + Console.WriteLine("TGS TEST CANCELLING TOKEN DUE TO ERROR"); hardCancellationTokenSource.Cancel(); try { @@ -1221,7 +1280,7 @@ await Task.WhenAny( await internalTask; } - async Task TestTgsInternal(CancellationToken hardCancellationToken) + async Task TestTgsInternal(bool openDreamOnly, CancellationToken hardCancellationToken) { var discordConnectionString = Environment.GetEnvironmentVariable("TGS_TEST_DISCORD_TOKEN"); var ircConnectionString = Environment.GetEnvironmentVariable("TGS_TEST_IRC_CONNECTION_STRING"); @@ -1235,7 +1294,7 @@ async Task TestTgsInternal(CancellationToken hardCancellationToken) // uncomment to force this test to run with DummyChatProviders // missingChatVarsCount = TotalChatVars; - // uncomment to force this test to run with pasic watchdog + // uncomment to force this test to run with basic watchdog // Environment.SetEnvironmentVariable("General__UseBasicWatchdog", "true"); if (missingChatVarsCount != 0) @@ -1253,7 +1312,7 @@ async Task TestTgsInternal(CancellationToken hardCancellationToken) } var procs = System.Diagnostics.Process.GetProcessesByName("byond"); - if (procs.Any()) + if (procs.Length != 0) { foreach (var proc in procs) proc.Dispose(); @@ -1262,12 +1321,16 @@ async Task TestTgsInternal(CancellationToken hardCancellationToken) Assert.Inconclusive("Cannot run server test because DreamDaemon will not start headless while the BYOND pager is running!"); } + if (TerminateAllEngineServers()) + await Task.Delay(TimeSpan.FromSeconds(5), hardCancellationToken); + using var server = new LiveTestingServer(null, true); using var serverCts = CancellationTokenSource.CreateLinkedTokenSource(hardCancellationToken); var cancellationToken = serverCts.Token; - TerminateAllDDs(); + for (var i = 0; i < 50; ++i) + await Task.Yield(); InstanceManager GetInstanceManager() => ((Host.Server)server.RealServer).Host.Services.GetRequiredService(); @@ -1278,8 +1341,8 @@ async Task TestTgsInternal(CancellationToken hardCancellationToken) try { Api.Models.Instance instance; - long initialStaged, initialActive; - await using var firstAdminClient = await CreateAdminClient(server.Url, cancellationToken); + long initialStaged, initialActive, initialSessionId; + await using var firstAdminClient = await CreateAdminClient(server.ApiUrl, cancellationToken); async ValueTask CreateUserWithNoInstancePerms() { @@ -1297,7 +1360,7 @@ async ValueTask CreateUserWithNoInstancePerms() var user = await firstAdminClient.Users.Create(createRequest, cancellationToken); Assert.IsTrue(user.Enabled); - return await clientFactory.CreateFromLogin(server.Url, createRequest.Name, createRequest.Password, cancellationToken: cancellationToken); + return await clientFactory.CreateFromLogin(server.RootUrl, createRequest.Name, createRequest.Password, cancellationToken: cancellationToken); } var jobsHubTest = new JobsHubTests(firstAdminClient, await CreateUserWithNoInstancePerms()); @@ -1308,11 +1371,11 @@ async ValueTask CreateUserWithNoInstancePerms() // Dump swagger to disk // This is purely for CI using var httpClient = new HttpClient(); - var webRequestTask = httpClient.GetAsync(server.Url.ToString() + "swagger/v1/swagger.json", cancellationToken); + var webRequestTask = httpClient.GetAsync(server.ApiUrl.ToString() + "doc/tgs_api.json", cancellationToken); using var response = await webRequestTask; response.EnsureSuccessStatusCode(); await using var content = await response.Content.ReadAsStreamAsync(cancellationToken); - await using var output = new FileStream(@"C:\swagger.json", FileMode.Create); + await using var output = new FileStream(@"C:\tgs_api.json", FileMode.Create); await content.CopyToAsync(output, cancellationToken); } @@ -1330,62 +1393,130 @@ async Task FailFast(Task task) } } - var rootTest = FailFast(RawRequestTests.Run(clientFactory, firstAdminClient, cancellationToken)); - var adminTest = FailFast(new AdministrationTest(firstAdminClient.Administration).Run(cancellationToken)); - var usersTest = FailFast(new UsersTest(firstAdminClient).Run(cancellationToken)); - - jobsHubTestTask = FailFast(jobsHubTest.Run(cancellationToken)); - var instanceManagerTest = new InstanceManagerTest(firstAdminClient, server.Directory); - var compatInstanceTask = instanceManagerTest.CreateTestInstance("CompatTestsInstance", cancellationToken); - instance = await instanceManagerTest.CreateTestInstance("LiveTestsInstance", cancellationToken); - var compatInstance = await compatInstanceTask; - var instancesTest = FailFast(instanceManagerTest.RunPreTest(cancellationToken)); - Assert.IsTrue(Directory.Exists(instance.Path)); - var instanceClient = firstAdminClient.Instances.CreateClient(instance); - - Assert.IsTrue(Directory.Exists(instanceClient.Metadata.Path)); + Task nonInstanceTests; + IInstanceClient instanceClient = null; + InstanceResponse odInstance, compatInstance; + if (!openDreamOnly) + { + jobsHubTestTask = FailFast(await jobsHubTest.Run(cancellationToken)); // returns Task + var rootTest = FailFast(RawRequestTests.Run(clientFactory, firstAdminClient, cancellationToken)); + var adminTest = FailFast(new AdministrationTest(firstAdminClient.Administration).Run(cancellationToken)); + var usersTest = FailFast(new UsersTest(firstAdminClient).Run(cancellationToken)); + + var instanceManagerTest = new InstanceManagerTest(firstAdminClient, server.Directory); + var compatInstanceTask = instanceManagerTest.CreateTestInstance("CompatTestsInstance", cancellationToken); + var odInstanceTask = instanceManagerTest.CreateTestInstance("OdTestsInstance", cancellationToken); + var byondApiCompatInstanceTask = instanceManagerTest.CreateTestInstance("BCAPITestsInstance", cancellationToken); + instance = await instanceManagerTest.CreateTestInstance("LiveTestsInstance", cancellationToken); + compatInstance = await compatInstanceTask; + odInstance = await odInstanceTask; + var byondApiCompatInstance = await byondApiCompatInstanceTask; + var instancesTest = FailFast(instanceManagerTest.RunPreTest(cancellationToken)); + Assert.IsTrue(Directory.Exists(instance.Path)); + instanceClient = firstAdminClient.Instances.CreateClient(instance); + + Assert.IsTrue(Directory.Exists(instanceClient.Metadata.Path)); + nonInstanceTests = Task.WhenAll(instancesTest, adminTest, rootTest, usersTest); + } + else + { + compatInstance = null; + nonInstanceTests = Task.CompletedTask; + jobsHubTestTask = null; + instance = null; + var instanceManagerTest = new InstanceManagerTest(firstAdminClient, server.Directory); + var odInstanceTask = instanceManagerTest.CreateTestInstance("OdTestsInstance", cancellationToken); + odInstance = await odInstanceTask; + } var instanceTest = new InstanceTest( firstAdminClient.Instances, fileDownloader, GetInstanceManager(), - (ushort)server.Url.Port); + (ushort)server.ApiUrl.Port); async Task RunInstanceTests() { + var testSerialized = TestingUtils.RunningInGitHubActions; // they only have 2 cores, can't handle intense parallelization + async Task ODCompatTests() + { + var edgeODVersionTask = EngineTest.GetEdgeVersion(EngineType.OpenDream, fileDownloader, cancellationToken); + + var ex = await Assert.ThrowsExceptionAsync( + () => InstanceTest.DownloadEngineVersion( + new EngineVersion + { + Engine = EngineType.OpenDream, + SourceSHA = "f1dc153caf9d84cd1d0056e52286cc0163e3f4d3", // 1b4 verified version + }, + fileDownloader, + server.OpenDreamUrl, + cancellationToken).AsTask()); + + Assert.AreEqual(ErrorCode.OpenDreamTooOld, ex.ErrorCode); + + await instanceTest + .RunCompatTests( + await edgeODVersionTask, + server.OpenDreamUrl, + firstAdminClient.Instances.CreateClient(odInstance), + odDMPort.Value, + odDDPort.Value, + server.HighPriorityDreamDaemon, + server.UsingBasicWatchdog, + cancellationToken); + } + + var odCompatTests = FailFast(ODCompatTests()); + + if (openDreamOnly || testSerialized) + await odCompatTests; + + if (openDreamOnly) + return; + var compatTests = FailFast( instanceTest .RunCompatTests( - new PlatformIdentifier().IsWindows - ? new Version(510, 1346) - : new Version(512, 1451), // http://www.byond.com/forum/?forum=5&command=search&scope=local&text=resolved%3a512.1451 + new EngineVersion + { + Engine = EngineType.Byond, + Version = new PlatformIdentifier().IsWindows + ? new Version(510, 1346) + : new Version(512, 1451) // http://www.byond.com/forum/?forum=5&command=search&scope=local&text=resolved%3a512.1451 + }, + server.OpenDreamUrl, firstAdminClient.Instances.CreateClient(compatInstance), - compatDMPort, - compatDDPort, + compatDMPort.Value, + compatDDPort.Value, server.HighPriorityDreamDaemon, server.UsingBasicWatchdog, cancellationToken)); - if (TestingUtils.RunningInGitHubActions) // they only have 2 cores, can't handle intense parallelization + if (testSerialized) await compatTests; await FailFast( instanceTest .RunTests( instanceClient, - mainDMPort, - mainDDPort, + mainDMPort.Value, + mainDDPort.Value, server.HighPriorityDreamDaemon, server.LowPriorityDeployments, server.UsingBasicWatchdog, cancellationToken)); await compatTests; + await odCompatTests; } var instanceTests = RunInstanceTests(); - await Task.WhenAll(rootTest, adminTest, instancesTest, instanceTests, usersTest); + await Task.WhenAll(nonInstanceTests, instanceTests); + + if (openDreamOnly) + return; var dd = await instanceClient.DreamDaemon.Read(cancellationToken); Assert.AreEqual(WatchdogStatus.Online, dd.Status.Value); @@ -1393,6 +1524,7 @@ await FailFast( Assert.AreNotEqual(dd.StagedCompileJob.Id, dd.ActiveCompileJob.Id); initialActive = dd.ActiveCompileJob.Id.Value; initialStaged = dd.StagedCompileJob.Id.Value; + initialSessionId = dd.SessionId.Value; jobsHubTest.ExpectShutdown(); await firstAdminClient.Administration.Restart(cancellationToken); @@ -1403,10 +1535,11 @@ await FailFast( // test the reattach message queueing // for the code coverage really... - var topicRequestResult = await WatchdogTest.StaticTopicClient.SendTopic( - IPAddress.Loopback, - $"tgs_integration_test_tactics6=1", - mainDDPort, + var topicRequestResult = await WatchdogTest.SendTestTopic( + "tgs_integration_test_tactics6=1", + WatchdogTest.StaticTopicClient, + null, + mainDDPort.Value, cancellationToken); Assert.IsNotNull(topicRequestResult); @@ -1421,7 +1554,7 @@ await FailFast( using var blockingSocket = new Socket(SocketType.Stream, ProtocolType.Tcp); blockingSocket.SetSocketOption(SocketOptionLevel.Socket, SocketOptionName.ExclusiveAddressUse, true); blockingSocket.SetSocketOption(SocketOptionLevel.Socket, SocketOptionName.ReuseAddress, false); - blockingSocket.Bind(new IPEndPoint(IPAddress.Any, server.Url.Port)); + blockingSocket.Bind(new IPEndPoint(IPAddress.Any, server.ApiUrl.Port)); // bind test run await server.Run(cancellationToken); Assert.Fail("Expected server task to end with a SocketException"); @@ -1443,13 +1576,13 @@ await FailFast( // chat bot start and DD reattach test serverTask = server.Run(cancellationToken).AsTask(); - await using (var adminClient = await CreateAdminClient(server.Url, cancellationToken)) + await using (var adminClient = await CreateAdminClient(server.ApiUrl, cancellationToken)) { await jobsHubTest.WaitForReconnect(cancellationToken); var instanceClient = adminClient.Instances.CreateClient(instance); var jobs = await instanceClient.Jobs.ListActive(null, cancellationToken); - if (!jobs.Any()) + if (jobs.Count == 0) { var entities = await instanceClient.Jobs.List(null, cancellationToken); var getTasks = entities @@ -1461,7 +1594,7 @@ await FailFast( .ToList(); } - var jrt = new JobsRequiredTest(instanceClient.Jobs); + await using var jrt = new JobsRequiredTest(instanceClient.Jobs); foreach (var job in jobs) { Assert.IsTrue(job.StartedAt.Value >= preStartupTime); @@ -1474,14 +1607,16 @@ await FailFast( Assert.AreNotEqual(dd.StagedCompileJob.Id, dd.ActiveCompileJob.Id); Assert.AreEqual(initialStaged, dd.StagedCompileJob.Id); Assert.AreEqual(initialActive, dd.ActiveCompileJob.Id); + Assert.AreEqual(initialSessionId, dd.SessionId); var chatReadTask = instanceClient.ChatBots.List(null, cancellationToken); // Check the DMAPI got the channels again https://github.com/tgstation/tgstation-server/issues/1490 - topicRequestResult = await WatchdogTest.StaticTopicClient.SendTopic( - IPAddress.Loopback, - $"tgs_integration_test_tactics7=1", - mainDDPort, + topicRequestResult = await WatchdogTest.SendTestTopic( + "tgs_integration_test_tactics7=1", + WatchdogTest.StaticTopicClient, + GetInstanceManager().GetInstanceReference(instanceClient.Metadata), + mainDDPort.Value, cancellationToken); Assert.IsNotNull(topicRequestResult); @@ -1492,9 +1627,14 @@ await FailFast( Assert.AreEqual(connectedChannelCount, topicRequestResult.FloatData.Value); - await WatchdogTest.TellWorldToReboot2(instanceClient, WatchdogTest.StaticTopicClient, mainDDPort, cancellationToken); + dd = await WatchdogTest.TellWorldToReboot2( + instanceClient, + GetInstanceManager(), + WatchdogTest.StaticTopicClient, + mainDDPort.Value, + true, + cancellationToken); - dd = await instanceClient.DreamDaemon.Read(cancellationToken); Assert.AreEqual(WatchdogStatus.Online, dd.Status.Value); // if this assert fails, you likely have to crack open the debugger and read test_fail_reason.txt manually Assert.IsNull(dd.StagedCompileJob); Assert.AreEqual(initialStaged, dd.ActiveCompileJob.Id); @@ -1519,23 +1659,18 @@ await FailFast( async Task WaitForInitialJobs(IInstanceClient instanceClient) { var jobs = await instanceClient.Jobs.ListActive(null, cancellationToken); - if (!jobs.Any()) - { - var entities = await instanceClient.Jobs.List(null, cancellationToken); - var getTasks = entities - .Select(e => instanceClient.Jobs.GetId(e, cancellationToken)) - .ToList(); - - jobs = (await ValueTaskExtensions.WhenAll(getTasks)) + if (jobs.Count == 0) + jobs = (await instanceClient.Jobs.List(null, cancellationToken)) .Where(x => x.StartedAt.Value > preStartupTime) - .ToList(); - } + .ToList(); + else + jobs = jobs.Where(x => x.JobCode.Value.IsServerStartupJob()).ToList(); - var jrt = new JobsRequiredTest(instanceClient.Jobs); + await using var jrt = new JobsRequiredTest(instanceClient.Jobs); foreach (var job in jobs) { Assert.IsTrue(job.StartedAt.Value >= preStartupTime); - await jrt.WaitForJob(job, 140, job.Description.Contains("Reconnect chat bot") ? null : false, null, cancellationToken); + await jrt.WaitForJob(job, 140, job.JobCode == JobCode.ReconnectChatBot ? null : false, null, cancellationToken); } } @@ -1543,32 +1678,26 @@ async Task WaitForInitialJobs(IInstanceClient instanceClient) preStartupTime = DateTimeOffset.UtcNow; serverTask = server.Run(cancellationToken).AsTask(); long expectedCompileJobId, expectedStaged; - var edgeByond = await ByondTest.GetEdgeVersion(fileDownloader, cancellationToken); - await using (var adminClient = await CreateAdminClient(server.Url, cancellationToken)) + var edgeVersion = await EngineTest.GetEdgeVersion(EngineType.Byond, fileDownloader, cancellationToken); + await using (var adminClient = await CreateAdminClient(server.ApiUrl, cancellationToken)) { + await jobsHubTest.WaitForReconnect(cancellationToken); var instanceClient = adminClient.Instances.CreateClient(instance); await WaitForInitialJobs(instanceClient); - await jobsHubTest.WaitForReconnect(cancellationToken); var dd = await instanceClient.DreamDaemon.Read(cancellationToken); Assert.AreEqual(WatchdogStatus.Online, dd.Status.Value); var compileJob = await instanceClient.DreamMaker.Compile(cancellationToken); - var wdt = new WatchdogTest(edgeByond, instanceClient, GetInstanceManager(), (ushort)server.Url.Port, server.HighPriorityDreamDaemon, mainDDPort, server.UsingBasicWatchdog); + await using var wdt = new WatchdogTest(edgeVersion, instanceClient, GetInstanceManager(), (ushort)server.ApiUrl.Port, server.HighPriorityDreamDaemon, mainDDPort.Value, server.UsingBasicWatchdog); await wdt.WaitForJob(compileJob, 30, false, null, cancellationToken); dd = await instanceClient.DreamDaemon.Read(cancellationToken); Assert.AreEqual(dd.StagedCompileJob.Job.Id, compileJob.Id); expectedCompileJobId = compileJob.Id.Value; - dd = await wdt.TellWorldToReboot(cancellationToken); - - while (dd.Status.Value == WatchdogStatus.Restoring) - { - await Task.Delay(TimeSpan.FromSeconds(1), cancellationToken); - dd = await instanceClient.DreamDaemon.Read(cancellationToken); - } + dd = await wdt.TellWorldToReboot(server.UsingBasicWatchdog, cancellationToken); Assert.AreEqual(dd.ActiveCompileJob.Job.Id, expectedCompileJobId); Assert.AreEqual(WatchdogStatus.Online, dd.Status.Value); @@ -1593,27 +1722,29 @@ await instanceClient.DreamDaemon.Update(new DreamDaemonRequest // post/entity deletion tests serverTask = server.Run(cancellationToken).AsTask(); - await using (var adminClient = await CreateAdminClient(server.Url, cancellationToken)) + await using (var adminClient = await CreateAdminClient(server.ApiUrl, cancellationToken)) { + await jobsHubTest.WaitForReconnect(cancellationToken); var instanceClient = adminClient.Instances.CreateClient(instance); await WaitForInitialJobs(instanceClient); - await jobsHubTest.WaitForReconnect(cancellationToken); var currentDD = await instanceClient.DreamDaemon.Read(cancellationToken); Assert.AreEqual(expectedCompileJobId, currentDD.ActiveCompileJob.Id.Value); Assert.AreEqual(WatchdogStatus.Online, currentDD.Status); Assert.AreEqual(expectedStaged, currentDD.StagedCompileJob.Job.Id.Value); - var wdt = new WatchdogTest(edgeByond, instanceClient, GetInstanceManager(), (ushort)server.Url.Port, server.HighPriorityDreamDaemon, mainDDPort, server.UsingBasicWatchdog); - currentDD = await wdt.TellWorldToReboot(cancellationToken); + await using var wdt = new WatchdogTest(edgeVersion, instanceClient, GetInstanceManager(), (ushort)server.ApiUrl.Port, server.HighPriorityDreamDaemon, mainDDPort.Value, server.UsingBasicWatchdog); + currentDD = await wdt.TellWorldToReboot(false, cancellationToken); Assert.AreEqual(expectedStaged, currentDD.ActiveCompileJob.Job.Id.Value); Assert.IsNull(currentDD.StagedCompileJob); - var repoTest = new RepositoryTest(instanceClient.Repository, instanceClient.Jobs).RunPostTest(cancellationToken); - await new ChatTest(instanceClient.ChatBots, adminClient.Instances, instanceClient.Jobs, instance).RunPostTest(cancellationToken); + await using var repoTestObj = new RepositoryTest(instanceClient.Repository, instanceClient.Jobs); + var repoTest = repoTestObj.RunPostTest(cancellationToken); + await using var chatTestObj = new ChatTest(instanceClient.ChatBots, adminClient.Instances, instanceClient.Jobs, instance); + await chatTestObj.RunPostTest(cancellationToken); await repoTest; - await DummyChatProvider.RandomDisconnections(false, cancellationToken); + DummyChatProvider.RandomDisconnections(false); jobsHubTest.CompleteNow(); await jobsHubTestTask; @@ -1626,13 +1757,19 @@ await instanceClient.DreamDaemon.Update(new DreamDaemonRequest Console.WriteLine($"[{DateTimeOffset.UtcNow}] TEST ERROR: {ex.ErrorCode}: {ex.Message}\n{ex.AdditionalServerData}"); throw; } - catch (Exception ex) when (ex is not OperationCanceledException) + catch(OperationCanceledException ex) + { + Console.WriteLine($"[{DateTimeOffset.UtcNow}] TEST ABORTED: {ex}"); + throw; + } + catch (Exception ex) { Console.WriteLine($"[{DateTimeOffset.UtcNow}] TEST ERROR: {ex}"); throw; } finally { + Console.WriteLine("TGS TEST CANCELLING TOKEN AS FINAL STEP"); serverCts.Cancel(); try { @@ -1640,7 +1777,7 @@ await instanceClient.DreamDaemon.Update(new DreamDaemonRequest } catch (OperationCanceledException) { } - TerminateAllDDs(); + TerminateAllEngineServers(); } Assert.IsTrue(serverTask.IsCompleted); @@ -1649,6 +1786,7 @@ await instanceClient.DreamDaemon.Update(new DreamDaemonRequest async Task CreateAdminClient(Uri url, CancellationToken cancellationToken) { + url = new Uri(url.ToString().Replace(Routes.ApiRoot, String.Empty)); var giveUpAt = DateTimeOffset.UtcNow.AddMinutes(2); for (var I = 1; ; ++I) { diff --git a/tests/Tgstation.Server.Tests/Live/DummyGitHubService.cs b/tests/Tgstation.Server.Tests/Live/TestingGitHubService.cs similarity index 82% rename from tests/Tgstation.Server.Tests/Live/DummyGitHubService.cs rename to tests/Tgstation.Server.Tests/Live/TestingGitHubService.cs index 709b312bbfc..99ddbda1ae5 100644 --- a/tests/Tgstation.Server.Tests/Live/DummyGitHubService.cs +++ b/tests/Tgstation.Server.Tests/Live/TestingGitHubService.cs @@ -19,15 +19,18 @@ namespace Tgstation.Server.Tests.Live { - sealed class DummyGitHubService : IAuthenticatedGitHubService + sealed class TestingGitHubService : IAuthenticatedGitHubService { static Dictionary releasesDictionary; static PullRequest testPr; + static GitHubCommit testCommit; readonly ICryptographySuite cryptographySuite; - readonly ILogger logger; + readonly ILogger logger; - public static async Task InitializeAndInject(CancellationToken cancellationToken) + public static readonly IGitHubClient RealTestClient; + + static TestingGitHubService() { var mockOptions = new Mock>(); mockOptions.SetupGet(x => x.Value).Returns(new GeneralConfiguration @@ -36,12 +39,15 @@ public static async Task InitializeAndInject(CancellationToken cancellationToken }); var gitHubClientFactory = new GitHubClientFactory(new AssemblyInformationProvider(), Mock.Of>(), mockOptions.Object); - var gitHubClient = gitHubClientFactory.CreateClient(); + RealTestClient = gitHubClientFactory.CreateClient(); + } + public static async Task InitializeAndInject(CancellationToken cancellationToken) + { Release targetRelease; do { - var releases = await gitHubClient + var releases = await RealTestClient .Repository .Release .GetAll("tgstation", "tgstation-server") @@ -56,15 +62,21 @@ public static async Task InitializeAndInject(CancellationToken cancellationToken { TestLiveServer.TestUpdateVersion, targetRelease } }; - testPr = await gitHubClient + var testCommitTask = RealTestClient + .Repository + .Commit + .Get("Cyberboss", "common_core", "4b4926dfaf6295f19f8ae7abf03cb357dbb05b29") + .WaitAsync(cancellationToken); + testPr = await RealTestClient .PullRequest .Get("Cyberboss", "common_core", 2) .WaitAsync(cancellationToken); + testCommit = await testCommitTask; ServiceCollectionExtensions.UseGitHubServiceFactory(); } - public DummyGitHubService(ICryptographySuite cryptographySuite, ILogger logger) + public TestingGitHubService(ICryptographySuite cryptographySuite, ILogger logger) { this.cryptographySuite = cryptographySuite ?? throw new ArgumentNullException(nameof(cryptographySuite)); this.logger = logger ?? throw new ArgumentNullException(nameof(logger)); @@ -125,6 +137,12 @@ public Task GetPullRequest(string repoOwner, string repoName, int p return Task.FromResult(testPr); } + public Task GetCommit(string repoOwner, string repoName, string committish, CancellationToken cancellationToken) + { + logger.LogTrace("GetPullRequest"); + return Task.FromResult(testCommit); + } + public ValueTask> GetTgsReleases(CancellationToken cancellationToken) { logger.LogTrace("GetTgsReleases"); diff --git a/tests/Tgstation.Server.Tests/TestDatabase.cs b/tests/Tgstation.Server.Tests/TestDatabase.cs index df8ffd5e4cf..320c4e32b80 100644 --- a/tests/Tgstation.Server.Tests/TestDatabase.cs +++ b/tests/Tgstation.Server.Tests/TestDatabase.cs @@ -134,7 +134,7 @@ DatabaseContext CreateContext() { new Host.Models.InstancePermissionSet { - ByondRights = ByondRights.InstallCustomVersion, + EngineRights = EngineRights.InstallCustomByondVersion, ChatBotRights = ChatBotRights.None, ConfigurationRights = ConfigurationRights.Read, DreamDaemonRights = DreamDaemonRights.ReadRevision, diff --git a/tests/Tgstation.Server.Tests/TestRepository.cs b/tests/Tgstation.Server.Tests/TestRepository.cs index d0549409f9c..ad99eb26032 100644 --- a/tests/Tgstation.Server.Tests/TestRepository.cs +++ b/tests/Tgstation.Server.Tests/TestRepository.cs @@ -45,10 +45,10 @@ public async Task TestRepoParentLookup() const string StartSha = "af4da8beb9f9b374b04a3cc4d65acca662e8cc1a"; await repo.CheckoutObject(StartSha, null, null, true, new JobProgressReporter(Mock.Of>(), null, (stage, progress) => { }), CancellationToken.None); - var result = await repo.ShaIsParent("2f8588a3ca0f6b027704a2a04381215619de3412", CancellationToken.None); + var result = await repo.CommittishIsParent("2f8588a3ca0f6b027704a2a04381215619de3412", CancellationToken.None); Assert.IsTrue(result); Assert.AreEqual(StartSha, repo.Head); - result = await repo.ShaIsParent("f636418bf47d238d33b0e4a34f0072b23a8aad0e", CancellationToken.None); + result = await repo.CommittishIsParent("f636418bf47d238d33b0e4a34f0072b23a8aad0e", CancellationToken.None); Assert.IsFalse(result); Assert.AreEqual(StartSha, repo.Head); } diff --git a/tests/Tgstation.Server.Tests/TestSystemInteraction.cs b/tests/Tgstation.Server.Tests/TestSystemInteraction.cs index 92e7aaee008..98b7c82f441 100644 --- a/tests/Tgstation.Server.Tests/TestSystemInteraction.cs +++ b/tests/Tgstation.Server.Tests/TestSystemInteraction.cs @@ -10,6 +10,7 @@ using Tgstation.Server.Host.Extensions; using Tgstation.Server.Host.IO; using Tgstation.Server.Host.System; +using Tgstation.Server.Host.Utils; namespace Tgstation.Server.Tests { @@ -43,39 +44,47 @@ public async Task TestScriptExecutionWithStdRead() [TestMethod] public async Task TestScriptExecutionWithFileOutput() { - using var loggerFactory = LoggerFactory.Create(x => { }); + using var loggerFactory = LoggerFactory.Create(builder => + { + builder.AddConsole(); + builder.SetMinimumLevel(LogLevel.Trace); + }); var platformIdentifier = new PlatformIdentifier(); var processExecutor = new ProcessExecutor( Mock.Of(), new DefaultIOManager(), - Mock.Of>(), + loggerFactory.CreateLogger(), loggerFactory); - var tempFile = Path.GetTempFileName(); - File.Delete(tempFile); - try + // run on a loop to spot the hang + for (var i = 0; i < 1000; ++i) { - await using (var process = processExecutor.LaunchProcess("test." + platformIdentifier.ScriptFileExtension, ".", string.Empty, tempFile, true, true)) + var tempFile = Path.GetTempFileName(); + File.Delete(tempFile); + try { - using var cts = new CancellationTokenSource(); - cts.CancelAfter(3000); - var exitCode = await process.Lifetime.WaitAsync(cts.Token); + await using (var process = processExecutor.LaunchProcess("test." + platformIdentifier.ScriptFileExtension, ".", string.Empty, tempFile, true, true)) + { + using var cts = new CancellationTokenSource(); + cts.CancelAfter(3000); + var exitCode = await process.Lifetime.WaitAsync(cts.Token); - await process.GetCombinedOutput(cts.Token); + await process.GetCombinedOutput(cts.Token); - Assert.AreEqual(0, exitCode); - } + Assert.AreEqual(0, exitCode); + } - Assert.IsTrue(File.Exists(tempFile)); - var result = File.ReadAllText(tempFile).Trim(); + Assert.IsTrue(File.Exists(tempFile), $"Could not find temp file: {tempFile}"); + var result = File.ReadAllText(tempFile).Trim(); - // no guarantees about order - Assert.IsTrue(result.Contains("Hello World!")); - Assert.IsTrue(result.Contains("Hello Error!")); - } - finally - { - File.Delete(tempFile); + // no guarantees about order + Assert.IsTrue(result.Contains("Hello World!"), $"Result: {result}"); + Assert.IsTrue(result.Contains("Hello Error!"), $"Result: {result}"); + } + finally + { + File.Delete(tempFile); + } } } } diff --git a/tests/Tgstation.Server.Tests/TestVersions.cs b/tests/Tgstation.Server.Tests/TestVersions.cs index a87b47b2b1e..7b52b79773a 100644 --- a/tests/Tgstation.Server.Tests/TestVersions.cs +++ b/tests/Tgstation.Server.Tests/TestVersions.cs @@ -1,9 +1,11 @@ using System; using System.IO; using System.IO.Compression; +using System.Globalization; using System.Linq; using System.Net.Http; using System.Reflection; +using System.Security.Cryptography; using System.Threading; using System.Threading.Tasks; using System.Xml.Linq; @@ -21,13 +23,16 @@ using Tgstation.Server.Client; using Tgstation.Server.Common.Extensions; using Tgstation.Server.Host; -using Tgstation.Server.Host.Components.Byond; +using Tgstation.Server.Host.Components.Engine; using Tgstation.Server.Host.Components.Interop; using Tgstation.Server.Host.Configuration; +using Tgstation.Server.Host.Controllers; using Tgstation.Server.Host.Database; using Tgstation.Server.Host.IO; using Tgstation.Server.Host.System; -using System.Net; +using Tgstation.Server.Api.Models; +using Tgstation.Server.Tests.Live; +using Tgstation.Server.Host.Utils; namespace Tgstation.Server.Tests { @@ -121,16 +126,26 @@ await CachingFileDownloader.InitializeByondVersion( Mock.Of(), new CachingFileDownloader(Mock.Of>()), mockGeneralConfigurationOptions.Object, + mockSessionConfigurationOptions.Object, Mock.Of>()); const string ArchiveEntryPath = "byond/bin/dd.exe"; var hasEntry = ArchiveHasFileEntry( - await byondInstaller.DownloadVersion(WindowsByondInstaller.DDExeVersion, default), + await TestingUtils.ExtractMemoryStreamFromInstallationData( + await byondInstaller.DownloadVersion( + new EngineVersion + { + Engine = EngineType.Byond, + Version = WindowsByondInstaller.DDExeVersion + }, + null, + default), + CancellationToken.None), ArchiveEntryPath); Assert.IsTrue(hasEntry); - var (byondBytes, version) = await GetByondVersionPriorTo(byondInstaller, WindowsByondInstaller.DDExeVersion); + var (byondBytes, _) = await GetByondVersionPriorTo(byondInstaller, WindowsByondInstaller.DDExeVersion); hasEntry = ArchiveHasFileEntry( byondBytes, ArchiveEntryPath); @@ -138,6 +153,8 @@ await byondInstaller.DownloadVersion(WindowsByondInstaller.DDExeVersion, default Assert.IsFalse(hasEntry); } + static Version MapThreadsVersion() => (Version)typeof(ByondInstallerBase).GetField("MapThreadsVersion", BindingFlags.Static | BindingFlags.NonPublic).GetValue(null) ?? throw new InvalidOperationException("Couldn't find MapThreadsVersion"); + [TestMethod] public async Task TestMapThreadsByondVersion() { @@ -159,24 +176,25 @@ public async Task TestMapThreadsByondVersion() var logger = loggerFactory.CreateLogger(); var init1 = CachingFileDownloader.InitializeByondVersion( logger, - ByondInstallerBase.MapThreadsVersion, + MapThreadsVersion(), platformIdentifier.IsWindows, CancellationToken.None); await CachingFileDownloader.InitializeByondVersion( logger, - new Version(ByondInstallerBase.MapThreadsVersion.Major, ByondInstallerBase.MapThreadsVersion.Minor - 1), + new Version(MapThreadsVersion().Major, MapThreadsVersion().Minor - 1), platformIdentifier.IsWindows, CancellationToken.None); await init1; var fileDownloader = new CachingFileDownloader(Mock.Of>()); - IByondInstaller byondInstaller = platformIdentifier.IsWindows + ByondInstallerBase byondInstaller = platformIdentifier.IsWindows ? new WindowsByondInstaller( Mock.Of(), Mock.Of(), fileDownloader, mockGeneralConfigurationOptions.Object, + mockSessionConfigurationOptions.Object, loggerFactory.CreateLogger()) : new PosixByondInstaller( new PosixPostWriteHandler(loggerFactory.CreateLogger()), @@ -197,14 +215,26 @@ await CachingFileDownloader.InitializeByondVersion( loggerFactory); var ioManager = new DefaultIOManager(); - var tempPath = Path.GetTempFileName(); - await ioManager.DeleteFile(tempPath, default); + var tempPath = ioManager.ConcatPath(LiveTestingServer.BaseDirectory, "mapthreads"); await ioManager.CreateDirectory(tempPath, default); try { await TestMapThreadsVersion( - ByondInstallerBase.MapThreadsVersion, - await byondInstaller.DownloadVersion(ByondInstallerBase.MapThreadsVersion, default), + new EngineVersion + { + Engine = EngineType.Byond, + Version = MapThreadsVersion(), + }, + await TestingUtils.ExtractMemoryStreamFromInstallationData( + await byondInstaller.DownloadVersion( + new EngineVersion + { + Engine = EngineType.Byond, + Version = MapThreadsVersion() + }, + null, + default), + CancellationToken.None), byondInstaller, ioManager, processExecutor, @@ -212,7 +242,7 @@ await byondInstaller.DownloadVersion(ByondInstallerBase.MapThreadsVersion, defau await ioManager.DeleteDirectory(tempPath, default); - var (byondBytes, version) = await GetByondVersionPriorTo(byondInstaller, ByondInstallerBase.MapThreadsVersion); + var (byondBytes, version) = await GetByondVersionPriorTo(byondInstaller, MapThreadsVersion()); await TestMapThreadsVersion( version, @@ -380,45 +410,91 @@ static string GetMigrationTimestampString(Type type) => type Assert.AreEqual(latestMigrationSL, DatabaseContext.SLLatestMigration); } - static async Task> GetByondVersionPriorTo(IByondInstaller byondInstaller, Version version) + [TestMethod] + public async Task CheckWebRootPathForTgsLogo() + { + var directory = Path.GetFullPath("../../../../../src/Tgstation.Server.Host/wwwroot"); + if (!Directory.Exists(directory)) + Assert.Inconclusive("Webpanel not built?"); + + static string GetConstField(string name) => (string)typeof(RootController).GetField(name, BindingFlags.NonPublic | BindingFlags.Static).GetValue(null); + + var logo = new PlatformIdentifier().IsWindows + ? GetConstField("LogoSvgWindowsName") + : GetConstField("LogoSvgLinuxName"); + + var path = $"../../../../../src/Tgstation.Server.Host/wwwroot/{logo}.svg"; + Assert.IsTrue(File.Exists(path)); + + var content = await File.ReadAllBytesAsync(path); + var hash = String.Join(String.Empty, SHA1.HashData(content).Select(b => b.ToString("x2", CultureInfo.InvariantCulture))); + Assert.AreEqual( + new PlatformIdentifier().IsWindows + ? "c5e4709774c14a6f376dbb5100bd80a0114a2287" + : "9eba2fac24c5c7e0008721690d07c3df575a00d6", + hash); + } + + static async Task> GetByondVersionPriorTo(ByondInstallerBase byondInstaller, Version version) { var minusOneMinor = new Version(version.Major, version.Minor - 1); + var byondVersion = new EngineVersion + { + Engine = EngineType.Byond, + Version = minusOneMinor + }; try { - return Tuple.Create(await byondInstaller.DownloadVersion(minusOneMinor, default), minusOneMinor); + return Tuple.Create(await TestingUtils.ExtractMemoryStreamFromInstallationData(await byondInstaller.DownloadVersion( + byondVersion, + null, + CancellationToken.None), CancellationToken.None), byondVersion); } catch (HttpRequestException) { var minusOneMajor = new Version(minusOneMinor.Major - 1, minusOneMinor.Minor); - return Tuple.Create(await byondInstaller.DownloadVersion(minusOneMajor, default), minusOneMajor); + byondVersion.Version = minusOneMajor; + return Tuple.Create(await TestingUtils.ExtractMemoryStreamFromInstallationData(await byondInstaller.DownloadVersion( + byondVersion, + null, + CancellationToken.None), CancellationToken.None), byondVersion); } } static async Task TestMapThreadsVersion( - Version byondVersion, + EngineVersion engineVersion, Stream byondBytes, - IByondInstaller byondInstaller, - IIOManager ioManager, - IProcessExecutor processExecutor, + ByondInstallerBase byondInstaller, + DefaultIOManager ioManager, + ProcessExecutor processExecutor, string tempPath) { using (byondBytes) await ioManager.ZipToDirectory(tempPath, byondBytes, default); // HAAAAAAAX - if (byondInstaller.GetType() == typeof(WindowsByondInstaller)) + var installerType = byondInstaller.GetType(); + if (byondInstaller is WindowsByondInstaller) typeof(WindowsByondInstaller).GetField("installedDirectX", BindingFlags.Instance | BindingFlags.NonPublic).SetValue(byondInstaller, true); - await byondInstaller.InstallByond(byondVersion, tempPath, default); + await byondInstaller.Install(engineVersion, tempPath, default); + var binPath = (string)typeof(ByondInstallerBase).GetField("ByondBinPath", BindingFlags.Static | BindingFlags.NonPublic).GetValue(null); + var ddNameFunc = installerType.GetMethod("GetDreamDaemonName", BindingFlags.Instance | BindingFlags.NonPublic); + var supportsCli = false; + var argArray = new object[] { engineVersion.Version, supportsCli }; + + // https://stackoverflow.com/questions/2438065/how-can-i-invoke-a-method-with-an-out-parameter var ddPath = ioManager.ConcatPath( tempPath, - ByondManager.BinPath, - byondInstaller.GetDreamDaemonName(byondVersion, out var supportsCli, out var shouldSupportMapThreads)); + binPath, + (string)ddNameFunc.Invoke(byondInstaller, argArray)); + + Assert.IsTrue((bool)argArray[1]); - Assert.IsTrue(supportsCli); + var shouldSupportMapThreads = engineVersion.Version >= MapThreadsVersion(); - await File.WriteAllBytesAsync("fake.dmb", Array.Empty(), CancellationToken.None); + await File.WriteAllBytesAsync("fake.dmb", [], CancellationToken.None); try { diff --git a/tests/Tgstation.Server.Tests/TestingUtils.cs b/tests/Tgstation.Server.Tests/TestingUtils.cs index d4f8d8caa32..3167d2c3d3f 100644 --- a/tests/Tgstation.Server.Tests/TestingUtils.cs +++ b/tests/Tgstation.Server.Tests/TestingUtils.cs @@ -1,10 +1,17 @@ using System; +using System.IO; +using System.IO.Compression; +using System.Reflection; +using System.Threading; +using System.Threading.Tasks; using Microsoft.Extensions.Logging; using Microsoft.VisualStudio.TestTools.UnitTesting; - using Moq; +using Tgstation.Server.Host.Components.Engine; +using Tgstation.Server.Host.IO; + namespace Tgstation.Server.Tests { static class TestingUtils @@ -25,5 +32,43 @@ public static ILoggerFactory CreateLoggerFactoryForLogger(ILogger logger, out Mo .Verifiable(); return mockLoggerFactory.Object; } + + public static async ValueTask ExtractMemoryStreamFromInstallationData(IEngineInstallationData engineInstallationData, CancellationToken cancellationToken) + { + if (engineInstallationData is ZipStreamEngineInstallationData zipStreamData) + return (MemoryStream)zipStreamData.GetType().GetField("zipStream", BindingFlags.NonPublic | BindingFlags.Instance).GetValue(zipStreamData); + + await using var grabby = engineInstallationData; + var tempFolder = Path.GetTempFileName(); + File.Delete(tempFolder); + try + { + await engineInstallationData.ExtractToPath(tempFolder, cancellationToken); + var resultStream = new FileStream( + $"{tempFolder}.zip", + FileMode.Create, + FileAccess.ReadWrite, + FileShare.Read | FileShare.Delete, + 4096, + FileOptions.Asynchronous); + + File.Delete(resultStream.Name); // now we have a ghost file that will delete when the stream closes + try + { + ZipFile.CreateFromDirectory(tempFolder, resultStream, CompressionLevel.NoCompression, false); + resultStream.Seek(0, SeekOrigin.Begin); + return resultStream; + } + catch + { + await resultStream.DisposeAsync(); + throw; + } + } + finally + { + await new DefaultIOManager().DeleteDirectory(tempFolder, cancellationToken); + } + } } } diff --git a/tests/Tgstation.Server.Tests/Tgstation.Server.Tests.csproj b/tests/Tgstation.Server.Tests/Tgstation.Server.Tests.csproj index 2ed1cd139b1..bbcbbc86edd 100644 --- a/tests/Tgstation.Server.Tests/Tgstation.Server.Tests.csproj +++ b/tests/Tgstation.Server.Tests/Tgstation.Server.Tests.csproj @@ -6,7 +6,7 @@ - + diff --git a/tools/Tgstation.Server.Migrator.Comms/Program.cs b/tools/Tgstation.Server.Migrator.Comms/Program.cs index 2277b9d76fb..326de7cf934 100644 --- a/tools/Tgstation.Server.Migrator.Comms/Program.cs +++ b/tools/Tgstation.Server.Migrator.Comms/Program.cs @@ -64,7 +64,7 @@ static async Task Migrate(IClient tgs3Client, ushort apiPort) Console.WriteLine("Connected!"); - Console.WriteLine("Connecting to TGS5..."); + Console.WriteLine("Connecting to TGS6..."); var assemblyName = Assembly.GetExecutingAssembly().GetName(); var productInfoHeaderValue = new ProductInfoHeaderValue( @@ -73,7 +73,7 @@ static async Task Migrate(IClient tgs3Client, ushort apiPort) var serverUrl = new Uri($"http://localhost:{apiPort}"); var clientFactory = new ServerClientFactory(productInfoHeaderValue.Product); - var tgs5Client = await clientFactory.CreateFromLogin( + var TGS6Client = await clientFactory.CreateFromLogin( serverUrl, DefaultCredentials.AdminUserName, DefaultCredentials.DefaultAdminUserPassword); @@ -216,21 +216,24 @@ static async Task Migrate(IClient tgs3Client, ushort apiPort) Console.WriteLine("Detaching TGS3 instance..."); tgs3Client.Server.InstanceManager.DetachInstance(instanceName); - Console.WriteLine("Creating TGS5 attach file..."); + Console.WriteLine("Creating TGS6 attach file..."); File.WriteAllText(Path.Combine(instancePath, "TGS4_ALLOW_INSTANCE_ATTACH"), String.Empty); Console.WriteLine("Checking BYOND install..."); var byondDirectory = Path.Combine(instancePath, "BYOND"); var byondVersionFile = Path.Combine(byondDirectory, "byond_version.dat"); - ByondVersionRequest? byondVersionRequest = null; + EngineVersionRequest? byondVersionRequest = null; if (Directory.Exists(byondDirectory) && File.Exists(byondVersionFile)) { var byondVersion = Version.Parse(File.ReadAllText(byondVersionFile).Trim()); Console.WriteLine($"Found installed BYOND version: {byondVersion.Major}.{byondVersion.Minor}"); - byondVersionRequest = new ByondVersionRequest + byondVersionRequest = new EngineVersionRequest { - Version = byondVersion + EngineVersion = new EngineVersion + { + Version = byondVersion + } }; } @@ -308,27 +311,27 @@ static async Task Migrate(IClient tgs3Client, ushort apiPort) Console.WriteLine("Deleting TGDreamDaemonBridge.dll..."); File.Delete(Path.Combine(instancePath, "TGDreamDaemonBridge.dll")); - Console.WriteLine("Attaching TGS5 instance..."); - var tgs5Instance = await tgs5Client.Instances.CreateOrAttach(new InstanceCreateRequest + Console.WriteLine("Attaching TGS6 instance..."); + var TGS6Instance = await TGS6Client.Instances.CreateOrAttach(new InstanceCreateRequest { ConfigurationType = ConfigurationType.Disallowed, Name = instanceName, Path = instancePath, }, default); - Console.WriteLine($"Onlining TGS5 instance ID {tgs5Instance.Id}..."); - tgs5Instance = await tgs5Client.Instances.Update(new InstanceUpdateRequest + Console.WriteLine($"Onlining TGS6 instance ID {TGS6Instance.Id}..."); + TGS6Instance = await TGS6Client.Instances.Update(new InstanceUpdateRequest { Online = true, - Id = tgs5Instance.Id + Id = TGS6Instance.Id }, default); - var v5InstanceClient = tgs5Client.Instances.CreateClient(tgs5Instance); + var v5InstanceClient = TGS6Client.Instances.CreateClient(TGS6Instance); if (byondVersionRequest != null) { Console.WriteLine("Triggering BYOND install job..."); - await v5InstanceClient.Byond.SetActiveVersion(byondVersionRequest, null, default); + await v5InstanceClient.Engine.SetActiveVersion(byondVersionRequest, null, default); } if (repositoryUpdateRequest != null) @@ -349,7 +352,7 @@ static async Task Migrate(IClient tgs3Client, ushort apiPort) await v5InstanceClient.ChatBots.Create(chatBotCreateRequest, default); } - Console.WriteLine($"Instance {instanceName} (TGS5 ID: {tgs5Instance.Id}) successfully migrated!"); + Console.WriteLine($"Instance {instanceName} (TGS6 ID: {TGS6Instance.Id}) successfully migrated!"); } Console.WriteLine("All enabled V3 instances migrated into V5 and detached from V3!"); diff --git a/tools/Tgstation.Server.Migrator/Program.cs b/tools/Tgstation.Server.Migrator/Program.cs index 83afbe25008..651c5e18522 100644 --- a/tools/Tgstation.Server.Migrator/Program.cs +++ b/tools/Tgstation.Server.Migrator/Program.cs @@ -24,9 +24,9 @@ using Tgstation.Server.Common; 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; +using Tgstation.Server.Migrator.Properties; using FileMode = System.IO.FileMode; @@ -44,7 +44,7 @@ static void ExitPause(int exitCode) var commandLineArguments = commandLine.Skip(1); var skipPreamble = commandLineArguments.Any(x => x.Equals("--skip-preamble", StringComparison.OrdinalIgnoreCase)); - Console.WriteLine("This is a very straightfoward script to migrate the instances of a TGS3 install into a new TGS5 install"); + Console.WriteLine("This is a very straightfoward script to migrate the instances of a TGS3 install into a new TGS6 install"); static bool PromptYesOrNo(string question) { @@ -194,7 +194,7 @@ static ServiceController GetTgs3Service(bool checkNewOneIsntInstalled) Console.WriteLine("- DISABLED INSTANCES WILL NOT BE MIGRATED! PLEASE ENABLE ALL INSTANCES YOU WISH TO MIGRATE BEFORE CONTINUING!"); Console.WriteLine("- INSTANCE AUTO UPDATE CAN INTERFERE WITH THE MIGRATION! PLEASE DISABLE IT ON ALL INSTANCES BEING MIGRATED BEFORE CONTINUING!"); Console.WriteLine("- DO NOT ATTEMPT TO USE TGS3 VIA NORMAL METHODS WHILE THIS MIGRATION IS TAKING PLACE OR YOU COULD CORRUPT YOUR DATA!"); - Console.WriteLine("Side note: You can skip the TGS5 setup wizard step by copying your premade appsettings.Production.yml file next to this .exe NOW."); + Console.WriteLine("Side note: You can skip the TGS6 setup wizard step by copying your premade appsettings.Production.yml file next to this .exe NOW."); if (!PromptYesOrNo("Proceed with upgrade?")) { Console.WriteLine("Prerequisite not met."); @@ -226,7 +226,7 @@ static ServiceController GetTgs3Service(bool checkNewOneIsntInstalled) Console.WriteLine("Attempting to create TGS install directory..."); Directory.CreateDirectory(tgsInstallPath); - // ASP.NET 6.0 RUNTIME CHECK + // ASP.NET 8.0 RUNTIME CHECK 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."); @@ -253,14 +253,14 @@ static ServiceController GetTgs3Service(bool checkNewOneIsntInstalled) if (runtimeInstalled) { var versions = await dotnetRuntimeCheck.StandardOutput.ReadToEndAsync(); - var regex = new Regex("Microsoft\\.AspNetCore\\.App 6\\.0\\.[0-9]+"); + var regex = new Regex("Microsoft\\.AspNetCore\\.App 8\\.0\\.[0-9]+"); if (!regex.IsMatch(versions)) runtimeInstalled = false; } } - // ASP.NET 6.0 RUNTIME SETUP + // ASP.NET 8.0 RUNTIME SETUP var assemblyName = currentAssembly.GetName(); var productInfoHeaderValue = new ProductInfoHeaderValue( @@ -276,9 +276,9 @@ static ServiceController GetTgs3Service(bool checkNewOneIsntInstalled) var xSubstitution = x64 ? "64" : "86"; Console.WriteLine($"Running on an x{xSubstitution} system."); - var downloadUri = new Uri("https://download.visualstudio.microsoft.com/download/pr/eaa3eab9-cc21-44b5-a4e4-af31ee73b9fa/d8ad75d525dec0a30b52adc990796b11/dotnet-hosting-6.0.9-win.exe"); + var downloadUri = RuntimeDistributableAttribute.Instance.RuntimeDistributableUrl; - var dotnetDownloadFilePath = $"dotnet-hosting-6.0.9-win.exe"; + var dotnetDownloadFilePath = $"dotnet-hosting-bundle-installer.exe"; Console.WriteLine($"Downloading {downloadUri} to {Path.GetFullPath(dotnetDownloadFilePath)}..."); @@ -327,7 +327,7 @@ static ServiceController GetTgs3Service(bool checkNewOneIsntInstalled) if (!PromptYesOrNo("Was the installation successful?")) { - Console.WriteLine("Cannot continue without ASP.NET 6.0 runtime installed."); + Console.WriteLine("Cannot continue without ASP.NET 8.0 runtime installed."); ExitPause(2); } } @@ -338,7 +338,7 @@ static ServiceController GetTgs3Service(bool checkNewOneIsntInstalled) } - // TGS5 ONLINE LOCATING + // TGS6 ONLINE LOCATING Console.WriteLine("Now we're going to locate the latest version of the TGS service."); Console.WriteLine("(This migrator does not support the console runner, but you may switch the installation to it after completion)"); @@ -376,14 +376,14 @@ static ServiceController GetTgs3Service(bool checkNewOneIsntInstalled) ExitPause(4); } - // TGS5 SETUP WIZARD + // TGS6 SETUP WIZARD Console.WriteLine("We are now going to run the TGS setup wizard to generate your new server configuration file."); var serverFactory = Tgstation.Server.Host.Core.Application.CreateDefaultServerFactory(); _ = await serverFactory.CreateServer(new[] { $"General:SetupWizardMode={SetupWizardMode.Only}" }, null, default); // This is where the wizard actually runs - // TGS5 DOWNLOAD AND UNZIP - Console.WriteLine("Downloading TGS5..."); + // TGS6 DOWNLOAD AND UNZIP + Console.WriteLine("Downloading TGS6..."); using (var loggerFactory = LoggerFactory.Create(builder => { })) { @@ -397,7 +397,7 @@ static ServiceController GetTgs3Service(bool checkNewOneIsntInstalled) await using (tgsFiveZipBuffer) { - Console.WriteLine("Unzipping TGS5..."); + Console.WriteLine("Unzipping TGS6..."); await serverFactory.IOManager.ZipToDirectory( tgsInstallPath, await tgsFiveZipBuffer.GetResult(default), @@ -405,7 +405,7 @@ await tgsFiveZipBuffer.GetResult(default), } } - // TGS5 CONFIG SETUP + // TGS6 CONFIG SETUP const string ConfigurationFileName = "appsettings.Production.yml"; Console.WriteLine("Extracting API port from configuration..."); ushort configuredApiPort; @@ -424,8 +424,8 @@ await tgsFiveZipBuffer.GetResult(default), Console.WriteLine("Moving configuration file from setup wizard to installation folder..."); File.Copy(ConfigurationFileName, Path.Combine(tgsInstallPath, ConfigurationFileName)); - // TGS5 SERVICE SETUP - Console.WriteLine("Installing TGS5 service..."); + // TGS6 SERVICE SETUP + Console.WriteLine("Installing TGS6 service..."); using (var processInstaller = new ServiceProcessInstaller()) using (var installer = new ServiceInstaller()) { @@ -448,42 +448,42 @@ await tgsFiveZipBuffer.GetResult(default), installer.Install(state); } - Console.WriteLine("Starting TGS5 service..."); + Console.WriteLine("Starting TGS6 service..."); var allServices = ServiceController.GetServices(); - using (var tgs5Service = allServices.FirstOrDefault(service => service.ServiceName == NewServiceName)) + using (var TGS6Service = allServices.FirstOrDefault(service => service.ServiceName == NewServiceName)) { - if (tgs5Service == null) + if (TGS6Service == null) { - Console.WriteLine("Unable to locate newly installed TGS5 service!"); + Console.WriteLine("Unable to locate newly installed TGS6 service!"); ExitPause(11); } foreach (var service in allServices) { - if (service == tgs5Service) + if (service == TGS6Service) continue; service.Dispose(); } - tgs5Service.Start(); - tgs5Service.WaitForStatus(ServiceControllerStatus.Running, TimeSpan.FromMinutes(2)); + TGS6Service.Start(); + TGS6Service.WaitForStatus(ServiceControllerStatus.Running, TimeSpan.FromMinutes(2)); } - // TGS5 CLIENT CONNECTION + // TGS6 CLIENT CONNECTION const int MaxWaitMinutes = 5; - Console.WriteLine($"Connecting to TGS5 (Max {MaxWaitMinutes} minute wait)..."); + Console.WriteLine($"Connecting to TGS6 (Max {MaxWaitMinutes} minute wait)..."); var giveUpAt = DateTimeOffset.UtcNow.AddMinutes(MaxWaitMinutes); var serverUrl = new Uri($"http://localhost:{configuredApiPort}"); var clientFactory = new ServerClientFactory(productInfoHeaderValue.Product); - IServerClient tgs5Client; + IServerClient TGS6Client; for (var I = 1; ; ++I) { try { Console.WriteLine($"Attempt {I}..."); - tgs5Client = await clientFactory.CreateFromLogin( + TGS6Client = await clientFactory.CreateFromLogin( serverUrl, DefaultCredentials.AdminUserName, DefaultCredentials.DefaultAdminUserPassword); @@ -505,7 +505,7 @@ await tgsFiveZipBuffer.GetResult(default), } } - Console.WriteLine("Successfully connected to TGS5!"); + Console.WriteLine("Successfully connected to TGS6!"); // COMMS MIGRATION Console.WriteLine("Deferring to Comms binary to migrate instances..."); @@ -533,7 +533,7 @@ await tgsFiveZipBuffer.GetResult(default), Console.WriteLine("Failed to disable TGS3 service! This isn't critical, however."); Console.WriteLine("Migration complete! Please continue uninstall TGS3 using Add/Remove Programs."); - Console.WriteLine("Then configure TGS5 using an interactive client to build and start your server."); + Console.WriteLine("Then configure TGS6 using an interactive client to build and start your server."); ExitPause(0); } catch (Exception ex) diff --git a/tools/Tgstation.Server.Migrator/Properties/RuntimeDistributableAttribute.cs b/tools/Tgstation.Server.Migrator/Properties/RuntimeDistributableAttribute.cs new file mode 100644 index 00000000000..d4434c61bb6 --- /dev/null +++ b/tools/Tgstation.Server.Migrator/Properties/RuntimeDistributableAttribute.cs @@ -0,0 +1,34 @@ +using System; +using System.Reflection; + +namespace Tgstation.Server.Migrator.Properties +{ + /// + /// Attribute for bringing in the runtime redistributable download link + /// + [AttributeUsage(AttributeTargets.Assembly)] + sealed class RuntimeDistributableAttribute : Attribute + { + /// + /// Return the 's instance of the . + /// + public static RuntimeDistributableAttribute Instance => Assembly + .GetExecutingAssembly() + .GetCustomAttribute()!; + + /// + /// The of the current runtime distributable. + /// + public Uri RuntimeDistributableUrl { get; } + + /// + /// Initializes a new instance of the class. + /// + /// The value of . + public RuntimeDistributableAttribute( + string runtimeDistributableUrl) + { + RuntimeDistributableUrl = new Uri(runtimeDistributableUrl ?? throw new ArgumentNullException(nameof(runtimeDistributableUrl))); + } + } +} diff --git a/tools/Tgstation.Server.Migrator/Tgstation.Server.Migrator.csproj b/tools/Tgstation.Server.Migrator/Tgstation.Server.Migrator.csproj index 4d4dc7f61dc..2511e50c18c 100644 --- a/tools/Tgstation.Server.Migrator/Tgstation.Server.Migrator.csproj +++ b/tools/Tgstation.Server.Migrator/Tgstation.Server.Migrator.csproj @@ -3,8 +3,9 @@ Exe - $(TgsFrameworkVersion) + $(TgsFrameworkVersion) win-x86 + true $(TgsMigratorVersion) enable CA1416 @@ -13,9 +14,22 @@ - + + + + + <_Parameter1>$(TgsDotnetRedistUrl) + + + + + + + + +