diff --git a/.github/CONTRIBUTING.md b/.github/CONTRIBUTING.md index 0dee22f9b4..2935d961a7 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 8.0 SDK, node>=v20, and npm>=v5.7 (in your PATH) to compile the server. +You need the .NET 8.0 SDK, node>=v20, and npm>=v5.7 (in your PATH) to compile the server. On Linux, you also need the `libgdiplus` package installed to generate icons. The recommended IDE is Visual Studio 2022 or VSCode. @@ -42,7 +42,8 @@ In order to build the service version and/or the Windows installer you need a to In addition, the installer project uses the Wix v4 Toolset which will cause an error on loading the .sln in Visual Studio if the [HeatWave for VS2022 Extension](https://marketplace.visualstudio.com/items?itemName=FireGiant.FireGiantHeatWaveDev17) is not installed. -In order to run the integration tests you must have the following environment variables set. To run them more accurately, include the optional ones. +In order to run the integration tests you must have the dotnet 7.0 SDK installed to properly build the OpenDream minimum compatible version. +You must also have the following environment variables set. To run them more accurately, include the optional ones. - `TGS_TEST_DATABASE_TYPE`: `MySql`, `MariaDB`, `PostgresSql`, or `SqlServer`. - `TGS_TEST_CONNECTION_STRING`: To a valid database connection string. You can use the setup wizard to create one. - (Optional) `TGS_TEST_GITHUB_TOKEN`: A GitHub personal access token with no scopes used to bypass rate limits. @@ -59,10 +60,9 @@ In order to run the integration tests you must have the following environment va For the full CI gambit, the following repository configuration must be set: -- Setting `Workflow Permissions` to `Read and write permissions`: Enables CodeQL uploads and GitHub Actions comments. +- Setting `Workflow Permissions` to `Read and write permissions`: Enables GitHub Actions comments. ![image](https://github.com/tgstation/tgstation-server/assets/8171642/ab17fa74-364f-4e66-b7c4-b9bb24c6a599) - Label `CI Cleared`: To allow PRs from forks to run CI with secrets after approval. -- Variable `TGS_ENABLE_CODE_QL` to `true`: Enables CodeQL scanning in actions. - Integration [CodeCov](https://github.com/apps/codecov): Enables CodeCov status checks. - Secret `CODECOV_TOKEN`: A CodeCov repo token to work around https://github.com/codecov/codecov-action/issues/837. - Secret `LIVE_TESTS_TOKEN`: A GitHub token with read access to the repository and write access to https://github.com/Cyberboss/common_core (TODO: Make the target repository here configurable). Despite it's name, it may be used across the entire test suite. @@ -73,7 +73,7 @@ For the full CI gambit, the following repository configuration must be set: If you don't plan on deploying TGS, the following secrets can be omitted: -- Secret `DEV_PUSH_TOKEN`: A GitHub token with write access to the repository. Enables doxygen pushes to `gh-pages` branch, and releases creation. +- Secret `DEV_PUSH_TOKEN`: A GitHub token with read/write access to the repository. Enables doxygen pushes to `gh-pages` branch, and releases creation. - Secret `DOCKER_USERNAME`: Login username for Docker image push. - Secret `DOCKER_PASSWORD`: Login password for Docker image push. - Secret `NUGET_API_KEY`: Nuget.org API Key for client libraries push. diff --git a/.github/workflows/ci-pipeline.yml b/.github/workflows/ci-pipeline.yml index cc033d61a7..0a6e1929dc 100644 --- a/.github/workflows/ci-pipeline.yml +++ b/.github/workflows/ci-pipeline.yml @@ -37,7 +37,8 @@ on: env: TGS_DOTNET_VERSION: 8 - OD_DOTNET_VERSION: 7 + OD_MIN_COMPAT_DOTNET_VERSION: 7 + OD_DOTNET_VERSION: 8 TGS_DOTNET_QUALITY: ga TGS_TEST_GITHUB_TOKEN: ${{ secrets.LIVE_TESTS_TOKEN }} TGS_RELEASE_NOTES_TOKEN: ${{ secrets.DEV_PUSH_TOKEN }} @@ -53,20 +54,35 @@ jobs: security-checkpoint: name: Check CI Clearance runs-on: ubuntu-latest + permissions: + pull-requests: write if: github.event_name == 'pull_request_target' && github.event.pull_request.head.repo.id != github.event.pull_request.base.repo.id && github.event.pull_request.state == 'open' steps: - name: Comment on new Fork PR if: github.event.action == 'opened' && !contains(github.event.pull_request.labels.*.name, 'CI Cleared') - uses: thollander/actions-comment-pull-request@v2 + uses: thollander/actions-comment-pull-request@1d3973dc4b8e1399c0620d3f2b1aa5e795465308 with: - message: Thank you for contributing to tgstation-server! As this pull request is from a fork, we can't allow the CI actions which require repository secrets to run on it without approval. After a brief review to make sure you're not misusing those secrets, a maintainer will add the `CI Cleared` label to allow the CI suite to run. Maintainers, please note that any changes to workflow files will not be reflected in the CI run. + message: Thank you for contributing to ${{ github.event.pull_request.base.repo.name }}! The workflow '${{ github.workflow }}' requires repository secrets and will not run without approval. Maintainers can add the `CI Cleared` label to allow it to run. Please note that any changes to the workflow file will not be reflected in the run. - name: "Remove Stale 'CI Cleared' Label" if: github.event.action == 'synchronize' || github.event.action == 'reopened' - uses: actions-ecosystem/action-remove-labels@v1 + uses: actions-ecosystem/action-remove-labels@2ce5d41b4b6aa8503e285553f75ed56e0a40bae0 with: labels: CI Cleared + - name: "Add 'CI Approval Required' Label" + if: (github.event.action == 'synchronize' || github.event.action == 'reopened') || ((github.event.action == 'opened' || github.event.action == 'labeled') && !contains(github.event.pull_request.labels.*.name, 'CI Cleared')) + uses: actions-ecosystem/action-add-labels@bd52874380e3909a1ac983768df6976535ece7f8 + with: + labels: CI Approval Required + github_token: ${{ github.token }} + + - name: "Remove 'CI Approval Required' Label" + if: (github.event.action == 'synchronize' || github.event.action == 'reopened') || ((github.event.action == 'opened' || github.event.action == 'labeled') && !contains(github.event.pull_request.labels.*.name, 'CI Cleared')) + uses: actions-ecosystem/action-remove-labels@2ce5d41b4b6aa8503e285553f75ed56e0a40bae0 + with: + labels: CI Approval Required + - name: Fail Clearance Check if PR has Unlabeled new Commits from Fork if: (github.event.action == 'synchronize' || github.event.action == 'reopened') || ((github.event.action == 'opened' || github.event.action == 'labeled') && !contains(github.event.pull_request.labels.*.name, 'CI Cleared')) run: exit 1 @@ -80,6 +96,44 @@ jobs: - name: GitHub Requires at Least One Step for a Job run: exit 0 + code-scanning: + name: Code Scanning + needs: start-ci-run-gate + runs-on: ubuntu-latest + permissions: + security-events: write + actions: read + if: (!(cancelled() || failure()) && needs.start-ci-run-gate.result == 'success') + steps: + - name: Setup dotnet + uses: actions/setup-dotnet@v4 + with: + dotnet-version: '${{ env.TGS_DOTNET_VERSION }}.0.x' + dotnet-quality: ${{ env.TGS_DOTNET_QUALITY }} + + - name: Checkout (Branch) + uses: actions/checkout@v4 + if: github.event_name == 'push' || github.event_name == 'schedule' + + - name: Checkout (PR Merge) + uses: actions/checkout@v4 + if: github.event_name != 'push' && github.event_name != 'schedule' + with: + ref: "refs/pull/${{ github.event.number }}/merge" + + - name: Initialize CodeQL + uses: github/codeql-action/init@v3 + with: + languages: csharp + + - name: Build + run: dotnet build -c ReleaseNoWindows -p:TGS_HOST_NO_WEBPANEL=true + + - name: Perform CodeQL Analysis + uses: github/codeql-action/analyze@v3 + with: + category: "/language:csharp" + dmapi-build: name: Build DMAPI needs: start-ci-run-gate @@ -97,7 +151,7 @@ jobs: sudo apt-get install -y -o APT::Immediate-Configure=0 libc6-i386 libstdc++6:i386 libgcc-s1:i386 - name: Cache BYOND .zips - uses: actions/cache@v3 + uses: actions/cache@v4 id: cache-byond with: path: ~/byond-zips-cache @@ -174,11 +228,19 @@ jobs: runs-on: ubuntu-latest steps: - name: Setup dotnet - uses: actions/setup-dotnet@v3 + uses: actions/setup-dotnet@v4 + if: matrix.committish == 'master' with: dotnet-version: '${{ env.OD_DOTNET_VERSION }}.0.x' dotnet-quality: ${{ env.TGS_DOTNET_QUALITY }} + - name: Setup dotnet (min-compat) + uses: actions/setup-dotnet@v4 + if: matrix.committish == 'tgs-min-compat' + with: + dotnet-version: '${{ env.OD_MIN_COMPAT_DOTNET_VERSION }}.0.x' + dotnet-quality: ${{ env.TGS_DOTNET_QUALITY }} + - name: Checkout (Branch) uses: actions/checkout@v4 if: github.event_name == 'push' || github.event_name == 'schedule' @@ -197,10 +259,20 @@ jobs: git checkout ${{ matrix.committish }} git submodule update --init --recursive + - name: Restore OpenDream + run: | + cd $HOME/OpenDream + dotnet restore + + - name: Build OpenDream + run: | + cd $HOME/OpenDream/OpenDreamPackageTool + dotnet build -c Release --nologo -v q --property WarningLevel=0 /clp:ErrorsOnly + - name: Create TGS Deployment run: | cd $HOME/OpenDream - dotnet run -c Release --project OpenDreamPackageTool --property WarningLevel=0 -- --tgs -o tgs_deploy + dotnet run -c Release --project OpenDreamPackageTool --no-build -- --tgs -o tgs_deploy - name: Build DMAPI run: | @@ -214,7 +286,7 @@ jobs: if: (!(cancelled() || failure()) && needs.start-ci-run-gate.result == 'success') steps: - name: Setup dotnet - uses: actions/setup-dotnet@v3 + uses: actions/setup-dotnet@v4 with: dotnet-version: '${{ env.TGS_DOTNET_VERSION }}.0.x' dotnet-quality: ${{ env.TGS_DOTNET_QUALITY }} @@ -246,7 +318,7 @@ jobs: - name: Patch Doxyfile run: | VERSION=$(cat "build/Version.props" | grep -oPm1 "(?<=)[^<]+") - echo -e "\nPROJECT_NUMBER = $VERSION\nINPUT = .\nOUTPUT_DIRECTORY = ./doxout\nPROJECT_LOGO = ./build/tgs.ico\nHAVE_DOT=YES" >> "docs/Doxyfile" + echo -e "\nPROJECT_NUMBER = $VERSION\nINPUT = .\nOUTPUT_DIRECTORY = ./doxout\nPROJECT_LOGO = ./artifacts/tgs.ico\nHAVE_DOT=YES" >> "docs/Doxyfile" - name: Doxygen Build uses: mattnotmitt/doxygen-action@411df0c62acb5b96b8a93d93a7bf4b753c47ea05 # v1.9.5 @@ -313,10 +385,10 @@ jobs: run: | sudo dpkg --add-architecture i386 sudo apt-get update - sudo apt-get install -y -o APT::Immediate-Configure=0 libc6-i386 libstdc++6:i386 libgcc-s1:i386 + sudo apt-get install -y -o APT::Immediate-Configure=0 libc6-i386 libstdc++6:i386 libgcc-s1:i386 libgdiplus - name: Setup dotnet - uses: actions/setup-dotnet@v3 + uses: actions/setup-dotnet@v4 with: dotnet-version: '${{ env.TGS_DOTNET_VERSION }}.0.x' dotnet-quality: ${{ env.TGS_DOTNET_QUALITY }} @@ -338,7 +410,7 @@ jobs: run: dotnet build -c ${{ matrix.configuration }}NoWindows - name: Cache BYOND .zips - uses: actions/cache@v3 + uses: actions/cache@v4 id: cache-byond with: path: ~/byond-zips-cache @@ -348,7 +420,7 @@ jobs: run: sudo dotnet test --no-build --logger "GitHubActions;summary.includePassedTests=true;summary.includeSkippedTests=true" --filter TestCategory!=RequiresDatabase -c ${{ matrix.configuration }}NoWindows --collect:"XPlat Code Coverage" --settings build/ci.runsettings --results-directory ./TestResults tgstation-server.sln - name: Store Code Coverage - uses: actions/upload-artifact@v3 + uses: actions/upload-artifact@v4 with: name: linux-unit-test-coverage-${{ matrix.configuration }} path: ./TestResults/ @@ -367,7 +439,7 @@ jobs: runs-on: windows-latest steps: - name: Setup dotnet - uses: actions/setup-dotnet@v3 + uses: actions/setup-dotnet@v4 with: dotnet-version: '${{ env.TGS_DOTNET_VERSION }}.0.x' dotnet-quality: ${{ env.TGS_DOTNET_QUALITY }} @@ -389,7 +461,7 @@ jobs: run: dotnet build -c ${{ matrix.configuration }}NoWix - name: Cache BYOND .zips - uses: actions/cache@v3 + uses: actions/cache@v4 id: cache-byond with: path: ~/byond-zips-cache @@ -399,7 +471,7 @@ jobs: run: dotnet test --no-build --logger "GitHubActions;summary.includePassedTests=true;summary.includeSkippedTests=true" --filter TestCategory!=RequiresDatabase -c ${{ matrix.configuration }}NoWix --collect:"XPlat Code Coverage" --settings build/ci.runsettings --results-directory ./TestResults tgstation-server.sln - name: Store Code Coverage - uses: actions/upload-artifact@v3 + uses: actions/upload-artifact@v4 with: name: windows-unit-test-coverage-${{ matrix.configuration }} path: ./TestResults/ @@ -424,11 +496,12 @@ jobs: sqlcmd -l 600 -S "(localdb)\MSSQLLocalDB" -Q "SELECT @@VERSION;" - name: Setup dotnet - uses: actions/setup-dotnet@v3 + uses: actions/setup-dotnet@v4 with: dotnet-version: | ${{ env.TGS_DOTNET_VERSION }}.0.x ${{ env.OD_DOTNET_VERSION }}.0.x + ${{ env.OD_MIN_COMPAT_DOTNET_VERSION }}.0.x dotnet-quality: ${{ env.TGS_DOTNET_QUALITY }} - name: Set TGS_TEST_DUMP_API_SPEC @@ -504,7 +577,7 @@ jobs: run: dotnet build -c ${{ matrix.configuration }} tests/Tgstation.Server.Tests/Tgstation.Server.Tests.csproj - name: Cache BYOND .zips - uses: actions/cache@v3 + uses: actions/cache@v4 id: cache-byond with: path: ~/byond-zips-cache @@ -530,14 +603,14 @@ jobs: - name: Store Live Tests Output if: ${{ steps.live-tests.outputs.succeeded == 'YES' }} - uses: actions/upload-artifact@v3 + uses: actions/upload-artifact@v4 with: name: windows-integration-test-logs-${{ matrix.configuration }}-${{ matrix.watchdog-type }}-${{ matrix.database-type }} path: ./test_output.txt - name: Store Errored Live Tests Output if: ${{ steps.live-tests.outputs.succeeded != 'YES' }} - uses: actions/upload-artifact@v3 + uses: actions/upload-artifact@v4 with: name: errored-windows-test-logs-${{ matrix.configuration }}-${{ matrix.watchdog-type }}-${{ matrix.database-type }} path: ./test_output.txt @@ -547,20 +620,20 @@ jobs: run: exit 1 - name: Store Code Coverage - uses: actions/upload-artifact@v3 + uses: actions/upload-artifact@v4 with: name: windows-integration-test-coverage-${{ matrix.configuration }}-${{ matrix.watchdog-type }}-${{ matrix.database-type }} path: ./TestResults/ - name: Store OpenAPI Spec if: ${{ matrix.configuration == 'Release' && matrix.watchdog-type == 'Advanced' && matrix.database-type == 'SqlServer' }} - uses: actions/upload-artifact@v3 + uses: actions/upload-artifact@v4 with: name: openapi-spec path: C:/tgs_api.json - name: Package Server Service - if: ${{ matrix.configuration == 'Release' && matrix.watchdog-type == 'Basic' }} + if: ${{ matrix.configuration == 'Release' && matrix.watchdog-type == 'Basic' && matrix.database-type == 'PostgresSql' }} run: | cd src/Tgstation.Server.Host.Service dotnet publish -c ${{ matrix.configuration }} -o ../../artifacts/Service @@ -572,14 +645,14 @@ jobs: build/RemoveUnsupportedServiceRuntimes.ps1 artifacts/Service - name: Store Server Service - if: ${{ matrix.configuration == 'Release' && matrix.watchdog-type == 'Basic' }} - uses: actions/upload-artifact@v3 + if: ${{ matrix.configuration == 'Release' && matrix.watchdog-type == 'Basic' && matrix.database-type == 'PostgresSql' }} + uses: actions/upload-artifact@v4 with: name: ServerService path: artifacts/Service/ - name: Install Code Signing Certificate - if: ${{ matrix.configuration == 'Release' && matrix.watchdog-type == 'Basic' }} + if: ${{ matrix.configuration == 'Release' && matrix.watchdog-type == 'Basic' && matrix.database-type == 'PostgresSql' }} shell: powershell run: | $pfxBytes = [convert]::FromBase64String("${{ secrets.CODE_SIGNING_BASE64 }}") @@ -589,7 +662,7 @@ jobs: rm tg_codesigning.pfx - name: Test Sign Service .exe - if: ${{ matrix.configuration == 'Release' && matrix.watchdog-type == 'Basic' }} + if: ${{ matrix.configuration == 'Release' && matrix.watchdog-type == 'Basic' && matrix.database-type == 'PostgresSql' }} shell: powershell run: Set-AuthenticodeSignature artifacts/Service/Tgstation.Server.Host.Service.exe -Certificate (Get-ChildItem Cert:\CurrentUser\My | Where-Object { $_.Thumbprint -eq "${{ vars.CODE_SIGNING_THUMBPRINT }}" }) -TimestampServer "http://timestamp.digicert.com" @@ -653,14 +726,15 @@ jobs: run: | sudo dpkg --add-architecture i386 sudo apt-get update - sudo apt-get install -y -o APT::Immediate-Configure=0 libc6-i386 libstdc++6:i386 gdb libgcc-s1:i386 + sudo apt-get install -y -o APT::Immediate-Configure=0 libc6-i386 libstdc++6:i386 gdb libgcc-s1:i386 libgdiplus - name: Setup dotnet - uses: actions/setup-dotnet@v3 + uses: actions/setup-dotnet@v4 with: dotnet-version: | ${{ env.TGS_DOTNET_VERSION }}.0.x ${{ env.OD_DOTNET_VERSION }}.0.x + ${{ env.OD_MIN_COMPAT_DOTNET_VERSION }}.0.x dotnet-quality: ${{ env.TGS_DOTNET_QUALITY }} - name: Set Sqlite Connection Info @@ -709,7 +783,7 @@ jobs: run: dotnet build -c ${{ matrix.configuration }}NoWindows tests/Tgstation.Server.Tests/Tgstation.Server.Tests.csproj - name: Cache BYOND .zips - uses: actions/cache@v3 + uses: actions/cache@v4 id: cache-byond with: path: ~/byond-zips-cache @@ -722,7 +796,7 @@ jobs: dotnet test -c ${{ matrix.configuration }}NoWindows --filter TestCategory=RequiresDatabase --logger "GitHubActions;summary.includePassedTests=true;summary.includeSkippedTests=true" --no-build --collect:"XPlat Code Coverage" --settings ../../build/ci.runsettings --results-directory ../../TestResults - name: Store Code Coverage - uses: actions/upload-artifact@v3 + uses: actions/upload-artifact@v4 with: name: linux-integration-test-coverage-${{ matrix.configuration }}-${{ matrix.watchdog-type }}-${{ matrix.database-type }} path: ./TestResults/ @@ -750,14 +824,14 @@ jobs: - name: Store Server Console if: ${{ matrix.configuration == 'Release' && matrix.watchdog-type == 'Advanced' && matrix.database-type == 'MariaDB' }} - uses: actions/upload-artifact@v3 + uses: actions/upload-artifact@v4 with: name: ServerConsole path: artifacts/Console/ - name: Store Server Update Package if: ${{ matrix.configuration == 'Release' && matrix.watchdog-type == 'Advanced' && matrix.database-type == 'PostgresSql' }} - uses: actions/upload-artifact@v3 + uses: actions/upload-artifact@v4 with: name: ServerUpdatePackage path: artifacts/ServerUpdate/ @@ -782,7 +856,7 @@ jobs: ref: "refs/pull/${{ github.event.number }}/merge" - name: Retrieve OpenAPI Spec - uses: actions/download-artifact@v3 + uses: actions/download-artifact@v4 with: name: openapi-spec path: ./swagger @@ -807,235 +881,235 @@ jobs: ref: "refs/pull/${{ github.event.number }}/merge" - name: Retrieve Linux Unit Test Coverage (Debug) - uses: actions/download-artifact@v3 + uses: actions/download-artifact@v4 with: name: linux-unit-test-coverage-Debug path: ./code_coverage/unit_tests/linux_unit_tests_debug - name: Retrieve Linux Unit Test Coverage (Release) - uses: actions/download-artifact@v3 + uses: actions/download-artifact@v4 with: name: linux-unit-test-coverage-Release path: ./code_coverage/unit_tests/linux_unit_tests_release - name: Retrieve Linux Integration Test Coverage (Release, Advanced, Sqlite) - uses: actions/download-artifact@v3 + uses: actions/download-artifact@v4 with: name: linux-integration-test-coverage-Release-Advanced-Sqlite path: ./code_coverage/integration_tests/linux_integration_tests_release_system_sqlite - name: Retrieve Linux Integration Test Coverage (Release, Advanced, PostgresSql) - uses: actions/download-artifact@v3 + uses: actions/download-artifact@v4 with: name: linux-integration-test-coverage-Release-Advanced-PostgresSql path: ./code_coverage/integration_tests/linux_integration_tests_release_system_mariadb - name: Retrieve Linux Integration Test Coverage (Release, Advanced, MariaDB) - uses: actions/download-artifact@v3 + uses: actions/download-artifact@v4 with: name: linux-integration-test-coverage-Release-Advanced-MariaDB path: ./code_coverage/integration_tests/linux_integration_tests_release_system_mysql - name: Retrieve Linux Integration Test Coverage (Release, Advanced, MySql) - uses: actions/download-artifact@v3 + uses: actions/download-artifact@v4 with: name: linux-integration-test-coverage-Release-Advanced-MySql path: ./code_coverage/integration_tests/linux_integration_tests_release_system_mysql - name: Retrieve Linux Integration Test Coverage (Release, Basic, Sqlite) - uses: actions/download-artifact@v3 + uses: actions/download-artifact@v4 with: name: linux-integration-test-coverage-Release-Advanced-Sqlite path: ./code_coverage/integration_tests/linux_integration_tests_release_basic_sqlite - name: Retrieve Linux Integration Test Coverage (Release, Basic, PostgresSql) - uses: actions/download-artifact@v3 + uses: actions/download-artifact@v4 with: name: linux-integration-test-coverage-Release-Advanced-PostgresSql path: ./code_coverage/integration_tests/linux_integration_tests_release_basic_mariadb - name: Retrieve Linux Integration Test Coverage (Release, Basic, MariaDB) - uses: actions/download-artifact@v3 + uses: actions/download-artifact@v4 with: name: linux-integration-test-coverage-Release-Advanced-MariaDB path: ./code_coverage/integration_tests/linux_integration_tests_release_basic_mysql - name: Retrieve Linux Integration Test Coverage (Release, Basic, MySql) - uses: actions/download-artifact@v3 + uses: actions/download-artifact@v4 with: name: linux-integration-test-coverage-Release-Advanced-MySql path: ./code_coverage/integration_tests/linux_integration_tests_release_basic_mysql - name: Retrieve Linux Integration Test Coverage (Debug, Advanced, Sqlite) - uses: actions/download-artifact@v3 + uses: actions/download-artifact@v4 with: name: linux-integration-test-coverage-Debug-Advanced-Sqlite path: ./code_coverage/integration_tests/linux_integration_tests_debug_system_sqlite - name: Retrieve Linux Integration Test Coverage (Debug, Advanced, PostgresSql) - uses: actions/download-artifact@v3 + uses: actions/download-artifact@v4 with: name: linux-integration-test-coverage-Debug-Advanced-PostgresSql path: ./code_coverage/integration_tests/linux_integration_tests_debug_system_mariadb - name: Retrieve Linux Integration Test Coverage (Debug, Advanced, MariaDB) - uses: actions/download-artifact@v3 + uses: actions/download-artifact@v4 with: name: linux-integration-test-coverage-Debug-Advanced-MariaDB path: ./code_coverage/integration_tests/linux_integration_tests_debug_system_mysql - name: Retrieve Linux Integration Test Coverage (Debug, Advanced, MySql) - uses: actions/download-artifact@v3 + uses: actions/download-artifact@v4 with: name: linux-integration-test-coverage-Debug-Advanced-MySql path: ./code_coverage/integration_tests/linux_integration_tests_debug_system_mysql - name: Retrieve Linux Integration Test Coverage (Debug, Basic, Sqlite) - uses: actions/download-artifact@v3 + uses: actions/download-artifact@v4 with: name: linux-integration-test-coverage-Debug-Advanced-Sqlite path: ./code_coverage/integration_tests/linux_integration_tests_debug_basic_sqlite - name: Retrieve Linux Integration Test Coverage (Debug, Basic, PostgresSql) - uses: actions/download-artifact@v3 + uses: actions/download-artifact@v4 with: name: linux-integration-test-coverage-Debug-Advanced-PostgresSql path: ./code_coverage/integration_tests/linux_integration_tests_debug_basic_mariadb - name: Retrieve Linux Integration Test Coverage (Debug, Basic, MariaDB) - uses: actions/download-artifact@v3 + uses: actions/download-artifact@v4 with: name: linux-integration-test-coverage-Debug-Advanced-MariaDB path: ./code_coverage/integration_tests/linux_integration_tests_debug_basic_mysql - name: Retrieve Linux Integration Test Coverage (Debug, Basic, MySql) - uses: actions/download-artifact@v3 + uses: actions/download-artifact@v4 with: name: linux-integration-test-coverage-Debug-Advanced-MySql path: ./code_coverage/integration_tests/linux_integration_tests_debug_basic_mysql - name: Retrieve Windows Unit Test Coverage (Release) - uses: actions/download-artifact@v3 + uses: actions/download-artifact@v4 with: name: windows-unit-test-coverage-Release path: ./code_coverage/unit_tests/windows_unit_tests_release - name: Retrieve Windows Integration Test Coverage (Debug, Basic, SqlServer) - uses: actions/download-artifact@v3 + uses: actions/download-artifact@v4 with: name: windows-integration-test-coverage-Debug-Basic-SqlServer path: ./code_coverage/integration_tests/windows_integration_tests_debug_basic_sqlserver - name: Retrieve Windows Integration Test Coverage (Release, Basic, SqlServer) - uses: actions/download-artifact@v3 + uses: actions/download-artifact@v4 with: name: windows-integration-test-coverage-Release-Basic-SqlServer path: ./code_coverage/integration_tests/windows_integration_tests_release_basic_sqlserver - name: Retrieve Windows Integration Test Coverage (Debug, Advanced, SqlServer) - uses: actions/download-artifact@v3 + uses: actions/download-artifact@v4 with: name: windows-integration-test-coverage-Debug-Advanced-SqlServer path: ./code_coverage/integration_tests/windows_integration_tests_debug_system_sqlserver - name: Retrieve Windows Integration Test Coverage (Release, Advanced, SqlServer) - uses: actions/download-artifact@v3 + uses: actions/download-artifact@v4 with: name: windows-integration-test-coverage-Release-Advanced-SqlServer path: ./code_coverage/integration_tests/windows_integration_tests_release_system_sqlserver - name: Retrieve Windows Integration Test Coverage (Debug, Basic, MariaDB) - uses: actions/download-artifact@v3 + uses: actions/download-artifact@v4 with: name: windows-integration-test-coverage-Debug-Basic-MariaDB path: ./code_coverage/integration_tests/windows_integration_tests_debug_basic_mariadb - name: Retrieve Windows Integration Test Coverage (Release, Basic, MariaDB) - uses: actions/download-artifact@v3 + uses: actions/download-artifact@v4 with: name: windows-integration-test-coverage-Release-Basic-MariaDB path: ./code_coverage/integration_tests/windows_integration_tests_release_basic_mariadb - name: Retrieve Windows Integration Test Coverage (Debug, Advanced, MariaDB) - uses: actions/download-artifact@v3 + uses: actions/download-artifact@v4 with: name: windows-integration-test-coverage-Debug-Advanced-MariaDB path: ./code_coverage/integration_tests/windows_integration_tests_debug_system_mariadb - name: Retrieve Windows Integration Test Coverage (Release, Advanced, MariaDB) - uses: actions/download-artifact@v3 + uses: actions/download-artifact@v4 with: name: windows-integration-test-coverage-Release-Advanced-MariaDB path: ./code_coverage/integration_tests/windows_integration_tests_release_system_mariadb - name: Retrieve Windows Integration Test Coverage (Debug, Basic, MySql) - uses: actions/download-artifact@v3 + uses: actions/download-artifact@v4 with: name: windows-integration-test-coverage-Debug-Basic-MySql path: ./code_coverage/integration_tests/windows_integration_tests_debug_basic_mysql - name: Retrieve Windows Integration Test Coverage (Release, Basic, MySql) - uses: actions/download-artifact@v3 + uses: actions/download-artifact@v4 with: name: windows-integration-test-coverage-Release-Basic-MySql path: ./code_coverage/integration_tests/windows_integration_tests_release_basic_mysql - name: Retrieve Windows Integration Test Coverage (Debug, Advanced, MySql) - uses: actions/download-artifact@v3 + uses: actions/download-artifact@v4 with: name: windows-integration-test-coverage-Debug-Advanced-MySql path: ./code_coverage/integration_tests/windows_integration_tests_debug_system_mysql - name: Retrieve Windows Integration Test Coverage (Release, Advanced, MySql) - uses: actions/download-artifact@v3 + uses: actions/download-artifact@v4 with: name: windows-integration-test-coverage-Release-Advanced-MySql path: ./code_coverage/integration_tests/windows_integration_tests_release_system_mysql - name: Retrieve Windows Integration Test Coverage (Debug, Basic, PostgresSql) - uses: actions/download-artifact@v3 + uses: actions/download-artifact@v4 with: name: windows-integration-test-coverage-Debug-Basic-PostgresSql path: ./code_coverage/integration_tests/windows_integration_tests_debug_basic_postgressql - name: Retrieve Windows Integration Test Coverage (Release, Basic, PostgresSql) - uses: actions/download-artifact@v3 + uses: actions/download-artifact@v4 with: name: windows-integration-test-coverage-Release-Basic-PostgresSql path: ./code_coverage/integration_tests/windows_integration_tests_release_basic_postgressql - name: Retrieve Windows Integration Test Coverage (Debug, Advanced, PostgresSql) - uses: actions/download-artifact@v3 + uses: actions/download-artifact@v4 with: name: windows-integration-test-coverage-Debug-Advanced-PostgresSql path: ./code_coverage/integration_tests/windows_integration_tests_debug_system_postgressql - name: Retrieve Windows Integration Test Coverage (Release, Advanced, PostgresSql) - uses: actions/download-artifact@v3 + uses: actions/download-artifact@v4 with: name: windows-integration-test-coverage-Release-Advanced-PostgresSql path: ./code_coverage/integration_tests/windows_integration_tests_release_system_postgressql - name: Retrieve Windows Integration Test Coverage (Debug, Basic, Sqlite) - uses: actions/download-artifact@v3 + uses: actions/download-artifact@v4 with: name: windows-integration-test-coverage-Debug-Basic-Sqlite path: ./code_coverage/integration_tests/windows_integration_tests_debug_basic_sqlite - name: Retrieve Windows Integration Test Coverage (Release, Basic, Sqlite) - uses: actions/download-artifact@v3 + uses: actions/download-artifact@v4 with: name: windows-integration-test-coverage-Release-Basic-Sqlite path: ./code_coverage/integration_tests/windows_integration_tests_release_basic_sqlite - name: Retrieve Windows Integration Test Coverage (Debug, Advanced, Sqlite) - uses: actions/download-artifact@v3 + uses: actions/download-artifact@v4 with: name: windows-integration-test-coverage-Debug-Advanced-Sqlite path: ./code_coverage/integration_tests/windows_integration_tests_debug_system_sqlite - name: Retrieve Windows Integration Test Coverage (Release, Advanced, Sqlite) - uses: actions/download-artifact@v3 + uses: actions/download-artifact@v4 with: name: windows-integration-test-coverage-Release-Advanced-Sqlite path: ./code_coverage/integration_tests/windows_integration_tests_release_system_sqlite @@ -1056,7 +1130,7 @@ jobs: run: | sudo dpkg --add-architecture i386 sudo apt-get update - sudo apt-get install -y -o APT::Immediate-Configure=0 libstdc++6:i386 libgcc-s1:i386 gnupg2 xmlstarlet + sudo apt-get install -y -o APT::Immediate-Configure=0 libstdc++6:i386 libgcc-s1:i386 gnupg2 xmlstarlet libgdiplus - name: Import GPG Key if: (github.event_name == 'push' && contains(github.event.head_commit.message, '[TGSDeploy]') && (github.event.ref == 'refs/heads/master' || github.event.ref == 'refs/heads/dev')) @@ -1072,7 +1146,7 @@ jobs: sudo apt-get install -y dotnet-sdk-${{ env.TGS_DOTNET_VERSION }}.0 - name: Setup dotnet - uses: actions/setup-dotnet@v3 + uses: actions/setup-dotnet@v4 with: dotnet-version: '${{ env.TGS_DOTNET_VERSION }}.0.x' dotnet-quality: ${{ env.TGS_DOTNET_QUALITY }} @@ -1137,7 +1211,7 @@ jobs: sleep 10 sudo apt-get remove -y tgstation-server if [[ -d "/opt/tgstation-server" ]]; then - ls -al /opt/tgstation-server + find /opt/tgstation-server exit 2 fi @@ -1145,7 +1219,7 @@ jobs: run: tar cfJ tgstation-server-v${{ env.TGS_VERSION }}.debian.packaging.tar.xz tgstation-server_* - name: Upload Packaging Archive - uses: actions/upload-artifact@v3 + uses: actions/upload-artifact@v4 with: name: packaging-debian path: tgstation-server-v${{ env.TGS_VERSION }}.debian.packaging.tar.xz @@ -1162,7 +1236,7 @@ jobs: GITHUB_TOKEN: ${{ env.WINGET_PUSH_TOKEN }} - name: Setup dotnet - uses: actions/setup-dotnet@v3 + uses: actions/setup-dotnet@v4 with: dotnet-version: '${{ env.TGS_DOTNET_VERSION }}.0.x' dotnet-quality: ${{ env.TGS_DOTNET_QUALITY }} @@ -1276,7 +1350,7 @@ jobs: } - name: Upload Unsigned Installer .exe - uses: actions/upload-artifact@v3 + uses: actions/upload-artifact@v4 with: name: packaging-preview-windows path: build/package/winget/Tgstation.Server.Host.Service.Wix.Bundle/bin/Release/tgstation-server-installer.exe @@ -1288,7 +1362,7 @@ jobs: runs-on: ubuntu-latest steps: - name: Setup dotnet - uses: actions/setup-dotnet@v3 + uses: actions/setup-dotnet@v4 with: dotnet-version: '${{ env.TGS_DOTNET_VERSION }}.0.x' dotnet-quality: ${{ env.TGS_DOTNET_QUALITY }} @@ -1320,12 +1394,39 @@ jobs: ci-completion-gate: # This job exists so there isn't a moving target for branch protections name: CI Completion Gate - needs: [ pages-build, docker-build, build-deb, build-msi, validate-openapi-spec, upload-code-coverage, check-winget-pr-template ] + needs: [ pages-build, docker-build, build-deb, build-msi, validate-openapi-spec, upload-code-coverage, check-winget-pr-template, code-scanning ] runs-on: ubuntu-latest - if: (!(cancelled() || failure()) && needs.pages-build.result == 'success' && needs.docker-build.result == 'success' && needs.build-deb.result == 'success' && needs.build-msi.result == 'success' && needs.validate-openapi-spec.result == 'success' && needs.upload-code-coverage.result == 'success' && needs.check-winget-pr-template.result == 'success') + if: (!(cancelled() || failure()) && needs.pages-build.result == 'success' && needs.docker-build.result == 'success' && needs.build-deb.result == 'success' && needs.build-msi.result == 'success' && needs.validate-openapi-spec.result == 'success' && needs.upload-code-coverage.result == 'success' && needs.check-winget-pr-template.result == 'success' && needs.code-scanning.result == 'success') steps: - - name: GitHub Requires at Least One Step for a Job - run: exit 0 + - name: Setup dotnet + uses: actions/setup-dotnet@v4 + with: + dotnet-version: '${{ env.TGS_DOTNET_VERSION }}.0.x' + dotnet-quality: ${{ env.TGS_DOTNET_QUALITY }} + + - name: Checkout (Branch) + uses: actions/checkout@v4 + if: github.event_name == 'push' || github.event_name == 'schedule' + + - name: Checkout (PR Merge) + uses: actions/checkout@v4 + if: github.event_name != 'push' && github.event_name != 'schedule' + with: + ref: "refs/pull/${{ github.event.number }}/merge" + + - name: Restore + run: dotnet restore + + - name: Build ReleaseNotes + run: dotnet build -c Release -p:TGS_HOST_NO_WEBPANEL=true tools/Tgstation.Server.ReleaseNotes/Tgstation.Server.ReleaseNotes.csproj + + - name: Run ReleaseNotes Create CI Completion Check (PR HEAD) + if: github.event_name != 'push' && github.event_name != 'schedule' + run: dotnet run -c Release --no-build --project tools/Tgstation.Server.ReleaseNotes --ci-completion-check ${{ github.event.pull_request.head.sha }} ${{ secrets.TGS_CI_GITHUB_APP_TOKEN_BASE64 }} + + - name: Run ReleaseNotes Create CI Completion Check (Branch) + if: github.event_name == 'push' || github.event_name == 'schedule' + run: dotnet run -c Release --no-build --project tools/Tgstation.Server.ReleaseNotes --ci-completion-check ${{ github.sha }} ${{ secrets.TGS_CI_GITHUB_APP_TOKEN_BASE64 }} deployment-gate: name: Deployment Start Gate @@ -1343,7 +1444,7 @@ jobs: if: (!(cancelled() || failure()) && needs.deployment-gate.result == 'success' && contains(github.event.head_commit.message, '[APIDeploy]')) steps: - name: Setup dotnet - uses: actions/setup-dotnet@v3 + uses: actions/setup-dotnet@v4 with: dotnet-version: '${{ env.TGS_DOTNET_VERSION }}.0.x' dotnet-quality: ${{ env.TGS_DOTNET_QUALITY }} @@ -1365,7 +1466,7 @@ jobs: echo "TGS_API_VERSION=$apiVersion" | Out-File -FilePath $Env:GITHUB_ENV -Encoding utf8 -Append - name: Retrieve OpenAPI Spec - uses: actions/download-artifact@v3 + uses: actions/download-artifact@v4 with: name: openapi-spec path: swagger @@ -1407,7 +1508,7 @@ jobs: if: (!(cancelled() || failure()) && needs.deployment-gate.result == 'success' && contains(github.event.head_commit.message, '[DMDeploy]')) steps: - name: Setup dotnet - uses: actions/setup-dotnet@v3 + uses: actions/setup-dotnet@v4 with: dotnet-version: '${{ env.TGS_DOTNET_VERSION }}.0.x' dotnet-quality: ${{ env.TGS_DOTNET_QUALITY }} @@ -1470,7 +1571,7 @@ jobs: if: (!(cancelled() || failure()) && needs.deployment-gate.result == 'success' && contains(github.event.head_commit.message, '[NugetDeploy]')) steps: - name: Setup dotnet - uses: actions/setup-dotnet@v3 + uses: actions/setup-dotnet@v4 with: dotnet-version: '${{ env.TGS_DOTNET_VERSION }}.0.x' dotnet-quality: ${{ env.TGS_DOTNET_QUALITY }} @@ -1521,7 +1622,7 @@ jobs: if: (!(cancelled() || failure()) && (needs.deploy-dm.result == 'success' || needs.deploy-http.result == 'success') && !contains(github.event.head_commit.message, '[TGSDeploy]')) steps: - name: Setup dotnet - uses: actions/setup-dotnet@v3 + uses: actions/setup-dotnet@v4 with: dotnet-version: '${{ env.TGS_DOTNET_VERSION }}.0.x' dotnet-quality: ${{ env.TGS_DOTNET_QUALITY }} @@ -1545,7 +1646,7 @@ jobs: if: (!(cancelled() || failure()) && needs.deployment-gate.result == 'success' && github.event.ref == 'refs/heads/master' && contains(github.event.head_commit.message, '[TGSDeploy]')) steps: - name: Setup dotnet - uses: actions/setup-dotnet@v3 + uses: actions/setup-dotnet@v4 with: dotnet-version: '${{ env.TGS_DOTNET_VERSION }}.0.x' dotnet-quality: ${{ env.TGS_DOTNET_QUALITY }} @@ -1591,37 +1692,37 @@ jobs: echo "MARIADB_VERSION=$mariaDBVerison" | Out-File -FilePath $Env:GITHUB_ENV -Encoding utf8 -Append - name: Upload .msi - uses: actions/upload-artifact@v3 + uses: actions/upload-artifact@v4 with: name: packaging-windows-raw-msi path: build/package/winget/Tgstation.Server.Host.Service.Wix/bin/Release/en-US/tgstation-server.msi - name: Retrieve Server Service - uses: actions/download-artifact@v3 + uses: actions/download-artifact@v4 with: name: ServerService path: ServerService - name: Retrieve Server Console - uses: actions/download-artifact@v3 + uses: actions/download-artifact@v4 with: name: ServerConsole path: ServerConsole - name: Retrieve Server Update Package - uses: actions/download-artifact@v3 + uses: actions/download-artifact@v4 with: name: ServerUpdatePackage path: ServerUpdatePackage - name: Retrieve OpenAPI Spec - uses: actions/download-artifact@v3 + uses: actions/download-artifact@v4 with: name: openapi-spec path: swagger - name: Retrieve Debian Packaging Archive - uses: actions/download-artifact@v3 + uses: actions/download-artifact@v4 with: name: packaging-debian path: packaging-debian @@ -1757,7 +1858,7 @@ jobs: if: (!(cancelled() || failure()) && needs.deploy-tgs.result == 'success') steps: - name: Setup dotnet - uses: actions/setup-dotnet@v3 + uses: actions/setup-dotnet@v4 with: dotnet-version: '${{ env.TGS_DOTNET_VERSION }}.0.x' dotnet-quality: ${{ env.TGS_DOTNET_QUALITY }} @@ -1845,7 +1946,7 @@ jobs: runs-on: windows-latest steps: - name: Setup dotnet - uses: actions/setup-dotnet@v3 + uses: actions/setup-dotnet@v4 with: dotnet-version: '${{ env.TGS_DOTNET_VERSION }}.0.x' dotnet-quality: ${{ env.TGS_DOTNET_QUALITY }} @@ -1865,7 +1966,7 @@ jobs: run: dotnet build -c Release -p:TGS_HOST_NO_WEBPANEL=true tools/Tgstation.Server.ReleaseNotes - name: Retrieve Server Service - uses: actions/download-artifact@v3 + uses: actions/download-artifact@v4 with: name: packaging-windows-raw-msi path: artifacts @@ -1874,8 +1975,10 @@ jobs: shell: powershell run: build/package/winget/push_manifest.ps1 + - name: Delay 10m to allow MS bot to update PR + shell: powershell + run: Sleep 600 + - name: Run ReleaseNotes with --link-winget shell: powershell - run: | - Sleep 600 - dotnet run -c Release --no-build --project tools/Tgstation.Server.ReleaseNotes --link-winget ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }} + run: dotnet run -c Release --no-build --project tools/Tgstation.Server.ReleaseNotes --link-winget ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }} diff --git a/.github/workflows/code-scanning.yml b/.github/workflows/code-scanning.yml deleted file mode 100644 index b661059144..0000000000 --- a/.github/workflows/code-scanning.yml +++ /dev/null @@ -1,51 +0,0 @@ -name: 'Code Scanning' - -on: - push: - branches: - - dev - - master - pull_request: - branches: - - dev - - master - -env: - TGS_DOTNET_VERSION: 8 - TGS_DOTNET_QUALITY: ga - -concurrency: - group: "code-scanning-${{ github.head_ref || github.run_id }}-${{ github.event_name }}" - cancel-in-progress: true - -jobs: - analyze: - name: Code Scanning - runs-on: ubuntu-latest - permissions: - actions: read - contents: read - security-events: write - if: ${{ vars.TGS_ENABLE_CODE_QL }} == 'true' - steps: - - name: Setup dotnet - uses: actions/setup-dotnet@v3 - with: - dotnet-version: '${{ env.TGS_DOTNET_VERSION }}.0.x' - dotnet-quality: ${{ env.TGS_DOTNET_QUALITY }} - - - name: Checkout - uses: actions/checkout@v4 - - - name: Initialize CodeQL - uses: github/codeql-action/init@v2 - with: - languages: csharp - - - name: Build - run: dotnet build -c ReleaseNoWindows -p:TGS_HOST_NO_WEBPANEL=true - - - name: Perform CodeQL Analysis - uses: github/codeql-action/analyze@v2 - with: - category: "/language:csharp" diff --git a/.github/workflows/rerun-flaky-tests.yml b/.github/workflows/rerun-flaky-tests.yml new file mode 100644 index 0000000000..522e1120cc --- /dev/null +++ b/.github/workflows/rerun-flaky-tests.yml @@ -0,0 +1,21 @@ +name: Rerun Flaky Live Tests +on: + workflow_run: + workflows: [CI Pipeline] + types: + - completed +jobs: + rerun_flaky_tests: + name: Rerun Flaky Tests + runs-on: ubuntu-latest + if: ${{ github.event.workflow_run.conclusion == 'failure' && github.event.workflow_run.run_attempt == 1 }} + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Rerun flaky tests + uses: actions/github-script@v6 + with: + script: | + const { rerunFlakyTests } = await import('${{ github.workspace }}/.github/workflows/scripts/rerunFlakyTests.js') + await rerunFlakyTests({ github, context }) diff --git a/.github/workflows/scripts/package.json b/.github/workflows/scripts/package.json new file mode 100644 index 0000000000..bedb411a91 --- /dev/null +++ b/.github/workflows/scripts/package.json @@ -0,0 +1,3 @@ +{ + "type": "module" +} diff --git a/.github/workflows/scripts/rerunFlakyTests.js b/.github/workflows/scripts/rerunFlakyTests.js new file mode 100644 index 0000000000..5bacef1278 --- /dev/null +++ b/.github/workflows/scripts/rerunFlakyTests.js @@ -0,0 +1,55 @@ +// Only check jobs that start with these. +// Helps make sure we don't restart something like which is not known to be flaky. +const CONSIDERED_JOBS = [ + "Windows Live Tests", + "Linux Live Tests", + "Build .deb Package" +]; + +async function getFailedJobsForRun(github, context, workflowRunId, runAttempt) { + const jobs = await github.paginate( + github.rest.actions.listJobsForWorkflowRunAttempt, + { + owner: context.repo.owner, + repo: context.repo.repo, + run_id: workflowRunId, + attempt_number: runAttempt + }, + response => { + return response.data; + }); + + return jobs + .filter((job) => job.conclusion === "failure"); +} + +export async function rerunFlakyTests({ github, context }) { + const failingJobs = await getFailedJobsForRun( + github, + context, + context.payload.workflow_run.id, + context.payload.workflow_run.run_attempt + ); + + if (failingJobs.length > 1) { + console.log("Multiple jobs failing. PROBABLY not flaky, not rerunning."); + return; + } + + const filteredFailingJobs = failingJobs.filter((job) => { + console.log(`Failing job: ${job.name}`) + return CONSIDERED_JOBS.some((title) => job.name.startsWith(title)); + }); + if (filteredFailingJobs.length === 0) { + console.log("Failing jobs are NOT designated flaky. Not rerunning."); + return; + } + + console.log(`Rerunning job: ${filteredFailingJobs[0].name}`); + + github.rest.actions.reRunWorkflowFailedJobs({ + owner: context.repo.owner, + repo: context.repo.repo, + run_id: context.payload.workflow_run.id, + }); +} diff --git a/.github/workflows/stable-merge.yml b/.github/workflows/stable-merge.yml index 8b77f4d7d3..54ce84245b 100644 --- a/.github/workflows/stable-merge.yml +++ b/.github/workflows/stable-merge.yml @@ -1,21 +1,23 @@ name: 'Master Merge' on: - push: - branches: - - master + workflow_run: + workflows: [CI Pipeline] + types: + - completed jobs: master-merge: - runs-on: ubuntu-latest - + if: ${{ github.event.workflow_run.conclusion == 'success' }} steps: - name: Checkout - uses: actions/checkout@v1 + uses: actions/checkout@v4 + with: + fetch-depth: 0 - name: Merge master into dev - uses: robotology/gh-action-nightly-merge@22f5e45d028f22837d617fa07512925457eec184 #v1.3.3 + uses: robotology/gh-action-nightly-merge@14b4a4cf358f7479aa708bee05cf8a794d6a2516 #v1.5.0 with: stable_branch: 'master' development_branch: 'dev' diff --git a/README.md b/README.md index 333a62165f..5f9246c5ab 100644 --- a/README.md +++ b/README.md @@ -6,13 +6,17 @@ [![forthebadge](http://forthebadge.com/images/badges/made-with-c-sharp.svg)](http://forthebadge.com) [![forinfinityandbyond](https://user-images.githubusercontent.com/5211576/29499758-4efff304-85e6-11e7-8267-62919c3688a9.gif)](https://www.reddit.com/r/SS13/comments/5oplxp/what_is_the_main_problem_with_byond_as_an_engine/dclbu1a) [![forthebadge](http://forthebadge.com/images/badges/built-with-love.svg)](http://forthebadge.com) -This is a toolset to manage production BYOND servers. It includes the ability to update the server without having to stop or shutdown the server (the update will take effect on a "reboot" of the server), the ability to start the server and restart it if it crashes, as well as systems for managing code and game files, and locally merging GitHub Pull Requests for test deployments. +This is a toolset to manage production DreamMaker servers. It includes the ability to update the server without having to stop or shutdown the server (the update will take effect on a "reboot" of the server), the ability to start the server and restart it if it crashes, as well as systems for managing code and game files, and locally merging GitHub Pull Requests for test deployments. ## Setup ### Pre-Requisites -_Note: If you opt to use the Windows installer, all pre-requisites for running BYOND servers (including MariaDB) are provided out of the box. If you wish to use OpenDream you will need to install the required dotnet SDK manually._ +_Note: If you opt to use the Windows installer, most pre-requisites for running BYOND servers (including MariaDB) are provided out of the box._ + +_If you are running on a Windows Server OS. You **might** need to install the [x86 Visual C++ 2015 Runtime](https://aka.ms/vs/17/release/vc_redist.x86.exe) to run BYOND._ + +_If you wish to use OpenDream you will need to install the required dotnet SDK manually._ tgstation-server needs a relational database to store it's data. @@ -127,7 +131,7 @@ sudo dpkg --add-architecture i386 \ && sudo systemctl start tgstation-server ``` -The service will execute as the newly created user: `tgstation-server`. +The service will execute as the newly created user: `tgstation-server`. You should, ideally, store your instances somewhere under `/home/tgstation-server`. ##### Manual Setup @@ -157,6 +161,7 @@ docker run \ --network="host" \ # Not recommended, eases networking setup if your sql server is on the same machine --name="tgs" \ # Name for the container --cap-add=sys_nice \ # Recommended, allows TGS to lower the niceness of child processes if it sees fit + --cap-add=sys_resource \ # Recommended, allows TGS to not be killed by the OOM killer before its child processes --init \ #Highly recommended, reaps potential zombie processes -p 5000:5000 \ # Port bridge for accessing TGS, you can change this if you need -p 0.0.0.0:: \ # Port bridge for accessing DreamDaemon @@ -407,15 +412,15 @@ _NOTE: Your reverse proxy setup may interfere with SSE (Server-Sent Events) whic #### IIS (Reccommended for Windows) 1. Acquire an HTTPS certificate. The easiet free way for Windows is [win-acme](https://github.com/PKISharp/win-acme) (requires you to set up the website first) -2. Install the [Web Platform Installer](https://www.microsoft.com/web/downloads/platform.aspx) -3. Open the web platform installer in the IIS Manager and install the Application Request Routing 3.0 module -4. Create a new website, bind it to HTTPS only with your chosen certificate and exposed port. The physical path won't matter since it won't be used. Use `Require Server Name Indication` if you want to limit requests to a specific URL prefix. Do not use the same port as the one TGS is running on. -5. Close and reopen the IIS Manager -5. Open the site and navigate to the `URL Rewrite` module -6. In the `Actions` Pane on the right click `Add Rule(s)...` -7. For the rule template, select `Reverse Proxy` under `Inbound and Outbound Rules` and click `OK` -8. You may get a prompt about enabling proxy functionality. Click `OK` -9. In the window that appears set the `Inbound Rules` textbox to the URL of your tgstation-server i.e. `http://localhost:5000`. Ensure `Enable SSL Offloading` is checked, then click `OK` +1. Install the [URL Rewrite Module](https://www.iis.net/downloads/microsoft/url-rewrite) +1. Install the [Application Request Routing Module](https://www.iis.net/downloads/microsoft/application-request-routing) +1. Create a new website, bind it to HTTPS only with your chosen certificate and exposed port. The physical path won't matter since it won't be used. Use `Require Server Name Indication` if you want to limit requests to a specific URL prefix. Do not use the same port as the one TGS is running on. +1. Close and reopen the IIS Manager +1. Open the site and navigate to the `URL Rewrite` module +1. In the `Actions` Pane on the right click `Add Rule(s)...` +1. For the rule template, select `Reverse Proxy` under `Inbound and Outbound Rules` and click `OK` +1. You may get a prompt about enabling proxy functionality. Click `OK` +1. In the window that appears set the `Inbound Rules` textbox to the URL of your tgstation-server i.e. `localhost:5000`. Ensure `Enable SSL Offloading` is checked, then click `OK` #### Caddy (Reccommended for Linux, or those unfamilar with configuring NGINX or Apache) @@ -539,7 +544,11 @@ Manual operations on the repository while an instance is running may lead to git #### Byond -The `Byond` folder contains installations of [BYOND](https://www.byond.com/) versions. The version which is used by your game code can be changed on a whim (Note that only versions >= 511.1385 have been thouroughly tested. Lower versions should work but if one doesn't function, please open an issue report) and the server will take care of installing it. +The `Byond` folder contains installations of [BYOND](https://www.byond.com/) or [OpenDream](https://github.com/OpenDreamProject/OpenDream) versions. The version which is used by your game code can be changed on a whim (Note that only versions >= 511.1385 have been thouroughly tested. Lower versions should work but if one doesn't function, please open an issue report) and the server will take care of installing it. + +##### Environment Variables + +You can specify additional environment variables to launch your server/compiler with by adding `server.env`/`compiler.env` to your engine installation directory (i.e. `/Byond/515.1530/server.env`). These are [.env](https://hexdocs.pm/dotenvy/dotenv-file-format.html) files. #### Compiler diff --git a/build/Dockerfile b/build/Dockerfile index 383df77bfb..cba330156e 100644 --- a/build/Dockerfile +++ b/build/Dockerfile @@ -16,6 +16,7 @@ RUN . $NVM_DIR/nvm.sh \ && apt-get update \ && apt-get install -y \ dos2unix \ + libgdiplus \ && rm -rf /var/lib/apt/lists/* # Build web control panel diff --git a/build/SrcCommon.props b/build/SrcCommon.props index d6e835dad0..a98bf6bad8 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 c533dded96..c932f8919d 100644 --- a/build/TestCommon.props +++ b/build/TestCommon.props @@ -3,7 +3,7 @@ - + all runtime; build; native; contentfiles; analyzers; buildtransitive @@ -13,14 +13,14 @@ runtime; build; native; contentfiles; analyzers; buildtransitive - + - + - + diff --git a/build/Version.props b/build/Version.props index 4f6f0fe8ac..9d2eb2ce7c 100644 --- a/build/Version.props +++ b/build/Version.props @@ -3,21 +3,21 @@ - 6.1.2 + 6.5.0 5.1.0 - 10.0.0 + 10.3.0 7.0.0 - 13.0.1 - 15.0.1 - 7.0.3 - 5.8.0 + 13.3.0 + 15.3.0 + 7.1.3 + 5.9.0 1.4.1 1.2.1 2.0.0 netstandard2.0 8 - https://download.visualstudio.microsoft.com/download/pr/2a7ae819-fbc4-4611-a1ba-f3b072d4ea25/32f3b931550f7b315d9827d564202eeb/dotnet-hosting-8.0.0-win.exe + https://download.visualstudio.microsoft.com/download/pr/00397fee-1bd9-44ef-899b-4504b26e6e96/ab9c73409659f3238d33faee304a8b7c/dotnet-hosting-8.0.4-win.exe 10.11.6 1.22.21 diff --git a/build/WebpanelVersion.props b/build/WebpanelVersion.props index 5b674b1734..6ce3ba7841 100644 --- a/build/WebpanelVersion.props +++ b/build/WebpanelVersion.props @@ -1,6 +1,6 @@ - 5.4.2 + 5.7.1 diff --git a/build/analyzers.ruleset b/build/analyzers.ruleset index e61e800d00..72be43fd43 100644 --- a/build/analyzers.ruleset +++ b/build/analyzers.ruleset @@ -1,4 +1,4 @@ - + @@ -6,7 +6,7 @@ - + @@ -1048,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 0732f96486..1a25546da8 100755 --- a/build/package/deb/build_package.sh +++ b/build/package/deb/build_package.sh @@ -19,7 +19,8 @@ apt-get install -y \ ca-certificates \ curl \ gnupg \ - xmlstarlet + xmlstarlet \ + libgdiplus declare repo_version=$(if command -v lsb_release &> /dev/null; then lsb_release -r -s; else grep -oP '(?<=^VERSION_ID=).+' /etc/os-release | tr -d '"'; fi) curl -L https://packages.microsoft.com/config/ubuntu/$repo_version/packages-microsoft-prod.deb -o packages-microsoft-prod.deb diff --git a/build/package/deb/debian/man/tgs-configure.1 b/build/package/deb/debian/man/tgs-configure.1 new file mode 100644 index 0000000000..43c8564684 --- /dev/null +++ b/build/package/deb/debian/man/tgs-configure.1 @@ -0,0 +1,18 @@ +.TH TGS-CONFIGURE 1 +.SH NAME +tgs-configure \- tgstation-server interactive configuration file generator +.SH SYNOPSIS +.B tgs-configure +.SH DESCRIPTION +.B tgs-configure +.SH OPTIONS +The +.B tgs-configure + command does not take any options. +modifies the tgstation-server configuration file stored in /etc/tgstation-server/appsettings.Production.yml. +.SH SEE ALSO +tgstation-server(7) +.SH BUGS +See issue tracker at https://github.com/tgstation/tgstation-server/issues. +.SH AUTHOR +Jordan Dominion (Cyberboss@users.noreply.github.com) diff --git a/build/package/deb/debian/man/tgstation-server.7 b/build/package/deb/debian/man/tgstation-server.7 new file mode 100644 index 0000000000..8f3127ecc5 --- /dev/null +++ b/build/package/deb/debian/man/tgstation-server.7 @@ -0,0 +1,11 @@ +.TH TGSTATION-SERVER 7 +.SH NAME +tgstation-server \- A production scale tool for DreamMaker server management +.SH DESCRIPTION +This is a toolset to manage production DreamMaker servers. It includes the ability to update the server without having to stop or shutdown the server (the update will take effect on a "reboot" of the server), the ability to start the server and restart it if it crashes, as well as systems for managing code and game files, and locally merging GitHub Pull Requests for test deployments. +.SH SEE ALSO +tgs-configure(1) +.SH BUGS +See issue tracker at https://github.com/tgstation/tgstation-server/issues. +.SH AUTHOR +Jordan Dominion (Cyberboss@users.noreply.github.com) diff --git a/build/package/deb/debian/manpages b/build/package/deb/debian/manpages new file mode 100644 index 0000000000..b78c157959 --- /dev/null +++ b/build/package/deb/debian/manpages @@ -0,0 +1,2 @@ +debian/man/tgs-configure.1 +debian/man/tgstation-server.7 diff --git a/build/package/deb/debian/postinst b/build/package/deb/debian/postinst index 2b98bd204c..28f147a87e 100755 --- a/build/package/deb/debian/postinst +++ b/build/package/deb/debian/postinst @@ -1,7 +1,7 @@ #!/bin/sh -e if [ -z "$2" ]; then - adduser --system tgstation-server + adduser --system --home /home/tgstation-server tgstation-server mkdir -m 754 -p /var/log/tgstation-server chown -R tgstation-server /etc/tgstation-server chown -R tgstation-server /opt/tgstation-server/lib diff --git a/build/package/winget/.config/dotnet-tools.json b/build/package/winget/.config/dotnet-tools.json index 14178b0a98..b94eed2541 100644 --- a/build/package/winget/.config/dotnet-tools.json +++ b/build/package/winget/.config/dotnet-tools.json @@ -3,7 +3,7 @@ "isRoot": true, "tools": { "wix": { - "version": "4.0.2", + "version": "4.0.4", "commands": [ "wix" ] diff --git a/build/package/winget/Tgstation.Server.Host.Service.Wix.Bundle/Tgstation.Server.Host.Service.Wix.Bundle.wixproj b/build/package/winget/Tgstation.Server.Host.Service.Wix.Bundle/Tgstation.Server.Host.Service.Wix.Bundle.wixproj index 827b40f156..af8ce69036 100644 --- a/build/package/winget/Tgstation.Server.Host.Service.Wix.Bundle/Tgstation.Server.Host.Service.Wix.Bundle.wixproj +++ b/build/package/winget/Tgstation.Server.Host.Service.Wix.Bundle/Tgstation.Server.Host.Service.Wix.Bundle.wixproj @@ -1,4 +1,4 @@ - + ProductVersion=$(TgsCoreVersion);NetMajorVersion=$(TgsNetMajorVersion);DotnetRedistUrl=$(TgsDotnetRedistUrl);MariaDBRedistUrl=https://github.com/tgstation/tgstation-server/releases/download/tgstation-server-v$(TgsCoreVersion)/mariadb-$(TgsMariaDBRedistVersion)-winx64.msi @@ -24,8 +24,8 @@ - - + + diff --git a/build/package/winget/Tgstation.Server.Host.Service.Wix.Extensions/Tgstation.Server.Host.Service.Wix.Extensions.csproj b/build/package/winget/Tgstation.Server.Host.Service.Wix.Extensions/Tgstation.Server.Host.Service.Wix.Extensions.csproj index c7edd3c403..85cbe7e02f 100644 --- a/build/package/winget/Tgstation.Server.Host.Service.Wix.Extensions/Tgstation.Server.Host.Service.Wix.Extensions.csproj +++ b/build/package/winget/Tgstation.Server.Host.Service.Wix.Extensions/Tgstation.Server.Host.Service.Wix.Extensions.csproj @@ -7,7 +7,7 @@ - + diff --git a/build/package/winget/Tgstation.Server.Host.Service.Wix/Tgstation.Server.Host.Service.Wix.wixproj b/build/package/winget/Tgstation.Server.Host.Service.Wix/Tgstation.Server.Host.Service.Wix.wixproj index 14838b0531..d8ada09e94 100644 --- a/build/package/winget/Tgstation.Server.Host.Service.Wix/Tgstation.Server.Host.Service.Wix.wixproj +++ b/build/package/winget/Tgstation.Server.Host.Service.Wix/Tgstation.Server.Host.Service.Wix.wixproj @@ -1,4 +1,4 @@ - + ProductVersion=$(TgsCoreVersion) @@ -25,8 +25,8 @@ - - + + diff --git a/build/package/winget/manifest/Tgstation.Server.installer.yaml b/build/package/winget/manifest/Tgstation.Server.installer.yaml index 5678bf542a..e19c13c9fb 100644 --- a/build/package/winget/manifest/Tgstation.Server.installer.yaml +++ b/build/package/winget/manifest/Tgstation.Server.installer.yaml @@ -22,9 +22,6 @@ Installers: AppsAndFeaturesEntries: - DisplayName: tgstation-server Publisher: /tg/station 13 - Dependencies: - PackageDependencies: - - PackageIdentifier: Microsoft.DotNet.HostingBundle.8 ReleaseDate: 2023-06-24 # Do not change. Set before publish by push_manifest.ps1 ManifestType: installer -ManifestVersion: 1.5.0 +ManifestVersion: 1.6.0 diff --git a/build/package/winget/manifest/Tgstation.Server.locale.en-US.yaml b/build/package/winget/manifest/Tgstation.Server.locale.en-US.yaml index 54556c5d92..a08b33f691 100644 --- a/build/package/winget/manifest/Tgstation.Server.locale.en-US.yaml +++ b/build/package/winget/manifest/Tgstation.Server.locale.en-US.yaml @@ -18,4 +18,4 @@ Documentations: ReleaseNotesUrl: https://github.com/tgstation/tgstation-server/releases/tag/tgstation-server-v0.22.475 PurchaseUrl: https://github.com/sponsors/Cyberboss ManifestType: defaultLocale -ManifestVersion: 1.5.0 +ManifestVersion: 1.6.0 diff --git a/build/package/winget/manifest/Tgstation.Server.yaml b/build/package/winget/manifest/Tgstation.Server.yaml index 09972f148d..2c28c483b4 100644 --- a/build/package/winget/manifest/Tgstation.Server.yaml +++ b/build/package/winget/manifest/Tgstation.Server.yaml @@ -5,4 +5,4 @@ PackageIdentifier: Tgstation.Server PackageVersion: 0.22.475 # Do not change. Set before publish by push_manifest.ps1 DefaultLocale: en-US ManifestType: version -ManifestVersion: 1.5.0 +ManifestVersion: 1.6.0 diff --git a/src/DMAPI/tgs.dm b/src/DMAPI/tgs.dm index afc51435b9..17464b44da 100644 --- a/src/DMAPI/tgs.dm +++ b/src/DMAPI/tgs.dm @@ -1,6 +1,6 @@ // tgstation-server DMAPI -#define TGS_DMAPI_VERSION "7.0.3" +#define TGS_DMAPI_VERSION "7.1.3" // All functions and datums outside this document are subject to change with any version and should not be relied on. @@ -50,6 +50,13 @@ #endif +#ifndef TGS_FILE2TEXT_NATIVE +#ifdef file2text +#error Your codebase is re-defining the BYOND proc file2text. The DMAPI requires the native version to read the result of world.Export(). You can fix this by adding "#define TGS_FILE2TEXT_NATIVE file2text" before your override of file2text to allow the DMAPI to use the native version. This will only be used for world.Export(), not regular file accesses +#endif +#define TGS_FILE2TEXT_NATIVE file2text +#endif + // EVENT CODES /// Before a reboot mode change, extras parameters are the current and new reboot mode enums. @@ -305,6 +312,7 @@ var/datum/tgs_chat_embed/structure/embed /datum/tgs_message_content/New(text) + ..() if(!istext(text)) TGS_ERROR_LOG("[/datum/tgs_message_content] created with no text!") text = null @@ -347,6 +355,7 @@ var/proxy_url /datum/tgs_chat_embed/media/New(url) + ..() if(!istext(url)) CRASH("[/datum/tgs_chat_embed/media] created with no url!") @@ -360,6 +369,7 @@ var/proxy_icon_url /datum/tgs_chat_embed/footer/New(text) + ..() if(!istext(text)) CRASH("[/datum/tgs_chat_embed/footer] created with no text!") @@ -376,6 +386,7 @@ var/proxy_icon_url /datum/tgs_chat_embed/provider/author/New(name) + ..() if(!istext(name)) CRASH("[/datum/tgs_chat_embed/provider/author] created with no name!") @@ -388,6 +399,7 @@ var/is_inline /datum/tgs_chat_embed/field/New(name, value) + ..() if(!istext(name)) CRASH("[/datum/tgs_chat_embed/field] created with no name!") @@ -490,10 +502,20 @@ /world/proc/TgsChatChannelInfo() return +/** + * Trigger an event in TGS. Requires TGS version >= 6.3.0. Returns [TRUE] if the event was triggered successfully, [FALSE] otherwise. This function may sleep! + * + * event_name - The name of the event to trigger + * parameters - Optional list of string parameters to pass as arguments to the event script. The first parameter passed to a script will always be the running game's directory followed by these parameters. + * wait_for_completion - If set, this function will not return until the event has run to completion. + */ +/world/proc/TgsTriggerEvent(event_name, list/parameters, wait_for_completion = FALSE) + return + /* The MIT License -Copyright (c) 2017-2023 Jordan Brown +Copyright (c) 2017-2024 Jordan Brown Permission is hereby granted, free of charge, to any person obtaining a copy of this software and diff --git a/src/DMAPI/tgs/LICENSE b/src/DMAPI/tgs/LICENSE index 2bedf9a63a..324c48e993 100644 --- a/src/DMAPI/tgs/LICENSE +++ b/src/DMAPI/tgs/LICENSE @@ -1,6 +1,6 @@ The MIT License -Copyright (c) 2017-2023 Jordan Brown +Copyright (c) 2017-2024 Jordan Brown Permission is hereby granted, free of charge, to any person obtaining a copy of this software and diff --git a/src/DMAPI/tgs/core/core.dm b/src/DMAPI/tgs/core/core.dm index 8be96f2740..15622228e9 100644 --- a/src/DMAPI/tgs/core/core.dm +++ b/src/DMAPI/tgs/core/core.dm @@ -166,3 +166,11 @@ var/datum/tgs_api/api = TGS_READ_GLOBAL(tgs) if(api) return api.Visibility() + +/world/TgsTriggerEvent(event_name, list/parameters, wait_for_completion = FALSE) + var/datum/tgs_api/api = TGS_READ_GLOBAL(tgs) + if(api) + if(!istype(parameters, /list)) + parameters = list() + + return api.TriggerEvent(event_name, parameters, wait_for_completion) diff --git a/src/DMAPI/tgs/core/datum.dm b/src/DMAPI/tgs/core/datum.dm index 07ce3b6845..f734fd0527 100644 --- a/src/DMAPI/tgs/core/datum.dm +++ b/src/DMAPI/tgs/core/datum.dm @@ -7,7 +7,7 @@ TGS_DEFINE_AND_SET_GLOBAL(tgs, null) var/list/warned_deprecated_command_runs /datum/tgs_api/New(datum/tgs_event_handler/event_handler, datum/tgs_version/version) - . = ..() + ..() src.event_handler = event_handler src.version = version @@ -17,7 +17,7 @@ TGS_DEFINE_AND_SET_GLOBAL(tgs, null) world.sleep_offline = FALSE // https://www.byond.com/forum/post/2894866 del(world) world.sleep_offline = FALSE // just in case, this is BYOND after all... - sleep(1) + sleep(world.tick_lag) TGS_DEBUG_LOG("BYOND DIDN'T TERMINATE THE WORLD!!! TICK IS: [world.time], sleep_offline: [world.sleep_offline]") /datum/tgs_api/latest @@ -69,3 +69,6 @@ TGS_PROTECT_DATUM(/datum/tgs_api) /datum/tgs_api/proc/Visibility() return TGS_UNIMPLEMENTED + +/datum/tgs_api/proc/TriggerEvent(event_name, list/parameters, wait_for_completion) + return FALSE diff --git a/src/DMAPI/tgs/core/tgs_version.dm b/src/DMAPI/tgs/core/tgs_version.dm index a5dae1241a..bc561e6748 100644 --- a/src/DMAPI/tgs/core/tgs_version.dm +++ b/src/DMAPI/tgs/core/tgs_version.dm @@ -1,4 +1,5 @@ /datum/tgs_version/New(raw_parameter) + ..() src.raw_parameter = raw_parameter deprefixed_parameter = replacetext(raw_parameter, "/tg/station 13 Server v", "") var/list/version_bits = splittext(deprefixed_parameter, ".") diff --git a/src/DMAPI/tgs/v4/api.dm b/src/DMAPI/tgs/v4/api.dm index 945e2e4117..7c87922750 100644 --- a/src/DMAPI/tgs/v4/api.dm +++ b/src/DMAPI/tgs/v4/api.dm @@ -181,7 +181,7 @@ var/json = json_encode(data) while(requesting_new_port && !override_requesting_new_port) - sleep(1) + sleep(world.tick_lag) //we need some port open at this point to facilitate return communication if(!world.port) @@ -209,7 +209,7 @@ requesting_new_port = FALSE while(export_lock) - sleep(1) + sleep(world.tick_lag) export_lock = TRUE last_interop_response = null @@ -217,7 +217,7 @@ text2file(json, server_commands_json_path) for(var/I = 0; I < EXPORT_TIMEOUT_DS && !last_interop_response; ++I) - sleep(1) + sleep(world.tick_lag) if(!last_interop_response) TGS_ERROR_LOG("Failed to get export result for: [json]") diff --git a/src/DMAPI/tgs/v5/__interop_version.dm b/src/DMAPI/tgs/v5/__interop_version.dm index 616263098f..f4806f7adb 100644 --- a/src/DMAPI/tgs/v5/__interop_version.dm +++ b/src/DMAPI/tgs/v5/__interop_version.dm @@ -1 +1 @@ -"5.8.0" +"5.9.0" diff --git a/src/DMAPI/tgs/v5/_defines.dm b/src/DMAPI/tgs/v5/_defines.dm index 1c7d67d20c..92c7a8388a 100644 --- a/src/DMAPI/tgs/v5/_defines.dm +++ b/src/DMAPI/tgs/v5/_defines.dm @@ -14,6 +14,7 @@ #define DMAPI5_BRIDGE_COMMAND_KILL 4 #define DMAPI5_BRIDGE_COMMAND_CHAT_SEND 5 #define DMAPI5_BRIDGE_COMMAND_CHUNK 6 +#define DMAPI5_BRIDGE_COMMAND_EVENT 7 #define DMAPI5_PARAMETER_ACCESS_IDENTIFIER "accessIdentifier" #define DMAPI5_PARAMETER_CUSTOM_COMMANDS "customCommands" @@ -34,6 +35,7 @@ #define DMAPI5_BRIDGE_PARAMETER_VERSION "version" #define DMAPI5_BRIDGE_PARAMETER_CHAT_MESSAGE "chatMessage" #define DMAPI5_BRIDGE_PARAMETER_MINIMUM_SECURITY_LEVEL "minimumSecurityLevel" +#define DMAPI5_BRIDGE_PARAMETER_EVENT_INVOCATION "eventInvocation" #define DMAPI5_BRIDGE_RESPONSE_NEW_PORT "newPort" #define DMAPI5_BRIDGE_RESPONSE_RUNTIME_INFORMATION "runtimeInformation" @@ -81,6 +83,7 @@ #define DMAPI5_TOPIC_COMMAND_SEND_CHUNK 9 #define DMAPI5_TOPIC_COMMAND_RECEIVE_CHUNK 10 #define DMAPI5_TOPIC_COMMAND_RECEIVE_BROADCAST 11 +#define DMAPI5_TOPIC_COMMAND_COMPLETE_EVENT 12 #define DMAPI5_TOPIC_PARAMETER_COMMAND_TYPE "commandType" #define DMAPI5_TOPIC_PARAMETER_CHAT_COMMAND "chatCommand" @@ -116,3 +119,9 @@ #define DMAPI5_CUSTOM_CHAT_COMMAND_NAME "name" #define DMAPI5_CUSTOM_CHAT_COMMAND_HELP_TEXT "helpText" #define DMAPI5_CUSTOM_CHAT_COMMAND_ADMIN_ONLY "adminOnly" + +#define DMAPI5_EVENT_ID "eventId" + +#define DMAPI5_EVENT_INVOCATION_NAME "eventName" +#define DMAPI5_EVENT_INVOCATION_PARAMETERS "parameters" +#define DMAPI5_EVENT_INVOCATION_NOTIFY_COMPLETION "notifyCompletion" diff --git a/src/DMAPI/tgs/v5/api.dm b/src/DMAPI/tgs/v5/api.dm index a5c064a8ea..95b8edd3ee 100644 --- a/src/DMAPI/tgs/v5/api.dm +++ b/src/DMAPI/tgs/v5/api.dm @@ -27,6 +27,8 @@ var/chunked_requests = 0 var/list/chunked_topics = list() + var/list/pending_events = list() + var/detached = FALSE /datum/tgs_api/v5/New() @@ -46,6 +48,10 @@ var/datum/tgs_version/api_version = ApiVersion() version = null // we want this to be the TGS version, not the interop version + + // sleep once to prevent an issue where world.Export on the first tick can hang indefinitely + sleep(world.tick_lag) + var/list/bridge_response = Bridge(DMAPI5_BRIDGE_COMMAND_STARTUP, list(DMAPI5_BRIDGE_PARAMETER_MINIMUM_SECURITY_LEVEL = minimum_required_security_level, DMAPI5_BRIDGE_PARAMETER_VERSION = api_version.raw_parameter, DMAPI5_PARAMETER_CUSTOM_COMMANDS = ListCustomCommands(), DMAPI5_PARAMETER_TOPIC_PORT = GetTopicPort())) if(!istype(bridge_response)) TGS_ERROR_LOG("Failed initial bridge request!") @@ -125,7 +131,7 @@ TGS_DEBUG_LOG("RequireInitialBridgeResponse: Starting sleep") logged = TRUE - sleep(1) + sleep(world.tick_lag) TGS_DEBUG_LOG("RequireInitialBridgeResponse: Passed") @@ -249,6 +255,40 @@ WaitForReattach(TRUE) return chat_channels.Copy() +/datum/tgs_api/v5/TriggerEvent(event_name, list/parameters, wait_for_completion) + RequireInitialBridgeResponse() + WaitForReattach(TRUE) + + if(interop_version.minor < 9) + TGS_WARNING_LOG("Interop version too low for custom events!") + return FALSE + + var/str_parameters = list() + for(var/i in parameters) + str_parameters += "[i]" + + var/list/response = Bridge(DMAPI5_BRIDGE_COMMAND_EVENT, list(DMAPI5_BRIDGE_PARAMETER_EVENT_INVOCATION = list(DMAPI5_EVENT_INVOCATION_NAME = event_name, DMAPI5_EVENT_INVOCATION_PARAMETERS = str_parameters, DMAPI5_EVENT_INVOCATION_NOTIFY_COMPLETION = wait_for_completion))) + if(!response) + return FALSE + + var/event_id = response[DMAPI5_EVENT_ID] + if(!event_id) + return FALSE + + TGS_DEBUG_LOG("Created event ID: [event_id]") + if(!wait_for_completion) + return TRUE + + TGS_DEBUG_LOG("Waiting for completion of event ID: [event_id]") + + while(!pending_events[event_id]) + sleep(world.tick_lag) + + TGS_DEBUG_LOG("Completed wait on event ID: [event_id]") + pending_events -= event_id + + return TRUE + /datum/tgs_api/v5/proc/DecodeChannels(chat_update_json) TGS_DEBUG_LOG("DecodeChannels()") var/list/chat_channels_json = chat_update_json[DMAPI5_CHAT_UPDATE_CHANNELS] diff --git a/src/DMAPI/tgs/v5/bridge.dm b/src/DMAPI/tgs/v5/bridge.dm index a0ab359876..0c5e701a32 100644 --- a/src/DMAPI/tgs/v5/bridge.dm +++ b/src/DMAPI/tgs/v5/bridge.dm @@ -65,7 +65,7 @@ if(detached) // Wait up to one minute for(var/i in 1 to 600) - sleep(1) + sleep(world.tick_lag) if(!detached && (!require_channels || length(chat_channels))) break @@ -77,8 +77,11 @@ /datum/tgs_api/v5/proc/PerformBridgeRequest(bridge_request) WaitForReattach(FALSE) + TGS_DEBUG_LOG("Bridge request start") // This is an infinite sleep until we get a response var/export_response = world.Export(bridge_request) + TGS_DEBUG_LOG("Bridge request complete") + if(!export_response) TGS_ERROR_LOG("Failed bridge request: [bridge_request]") return @@ -88,7 +91,7 @@ TGS_ERROR_LOG("Failed bridge request, missing content!") return - var/response_json = file2text(content) + var/response_json = TGS_FILE2TEXT_NATIVE(content) if(!response_json) TGS_ERROR_LOG("Failed bridge request, failed to load content!") return diff --git a/src/DMAPI/tgs/v5/topic.dm b/src/DMAPI/tgs/v5/topic.dm index 05e6c4e1b2..e1f2cb6385 100644 --- a/src/DMAPI/tgs/v5/topic.dm +++ b/src/DMAPI/tgs/v5/topic.dm @@ -176,6 +176,10 @@ var/list/reattach_response = TopicResponse(error_message) reattach_response[DMAPI5_PARAMETER_CUSTOM_COMMANDS] = ListCustomCommands() reattach_response[DMAPI5_PARAMETER_TOPIC_PORT] = GetTopicPort() + + for(var/eventId in pending_events) + pending_events[eventId] = TRUE + return reattach_response if(DMAPI5_TOPIC_COMMAND_SEND_CHUNK) @@ -276,6 +280,15 @@ TGS_WORLD_ANNOUNCE(message) return TopicResponse() + if(DMAPI5_TOPIC_COMMAND_COMPLETE_EVENT) + var/event_id = topic_parameters[DMAPI5_EVENT_ID] + if (!istext(event_id)) + return TopicResponse("Invalid or missing [DMAPI5_EVENT_ID]") + + TGS_DEBUG_LOG("Completing event ID [event_id]...") + pending_events[event_id] = TRUE + return TopicResponse() + return TopicResponse("Unknown command: [command]") /datum/tgs_api/v5/proc/WorldBroadcast(message) diff --git a/src/DMAPI/tgs/v5/undefs.dm b/src/DMAPI/tgs/v5/undefs.dm index d531d4b7b9..237207fdfd 100644 --- a/src/DMAPI/tgs/v5/undefs.dm +++ b/src/DMAPI/tgs/v5/undefs.dm @@ -14,6 +14,7 @@ #undef DMAPI5_BRIDGE_COMMAND_KILL #undef DMAPI5_BRIDGE_COMMAND_CHAT_SEND #undef DMAPI5_BRIDGE_COMMAND_CHUNK +#undef DMAPI5_BRIDGE_COMMAND_EVENT #undef DMAPI5_PARAMETER_ACCESS_IDENTIFIER #undef DMAPI5_PARAMETER_CUSTOM_COMMANDS @@ -34,6 +35,7 @@ #undef DMAPI5_BRIDGE_PARAMETER_VERSION #undef DMAPI5_BRIDGE_PARAMETER_CHAT_MESSAGE #undef DMAPI5_BRIDGE_PARAMETER_MINIMUM_SECURITY_LEVEL +#undef DMAPI5_BRIDGE_PARAMETER_EVENT_INVOCATION #undef DMAPI5_BRIDGE_RESPONSE_NEW_PORT #undef DMAPI5_BRIDGE_RESPONSE_RUNTIME_INFORMATION @@ -81,6 +83,7 @@ #undef DMAPI5_TOPIC_COMMAND_SEND_CHUNK #undef DMAPI5_TOPIC_COMMAND_RECEIVE_CHUNK #undef DMAPI5_TOPIC_COMMAND_RECEIVE_BROADCAST +#undef DMAPI5_TOPIC_COMMAND_COMPLETE_EVENT #undef DMAPI5_TOPIC_PARAMETER_COMMAND_TYPE #undef DMAPI5_TOPIC_PARAMETER_CHAT_COMMAND @@ -116,3 +119,9 @@ #undef DMAPI5_CUSTOM_CHAT_COMMAND_NAME #undef DMAPI5_CUSTOM_CHAT_COMMAND_HELP_TEXT #undef DMAPI5_CUSTOM_CHAT_COMMAND_ADMIN_ONLY + +#undef DMAPI5_EVENT_ID + +#undef DMAPI5_EVENT_INVOCATION_NAME +#undef DMAPI5_EVENT_INVOCATION_PARAMETERS +#undef DMAPI5_EVENT_INVOCATION_NOTIFY_COMPLETION diff --git a/src/Tgstation.Server.Api/Models/ErrorCode.cs b/src/Tgstation.Server.Api/Models/ErrorCode.cs index 28f39a7638..b75c837585 100644 --- a/src/Tgstation.Server.Api/Models/ErrorCode.cs +++ b/src/Tgstation.Server.Api/Models/ErrorCode.cs @@ -646,5 +646,11 @@ namespace Tgstation.Server.Api.Models /// [Description("The specified OpenDream version is too old!")] OpenDreamTooOld, + + /// + /// Failed dotnet diagnostics dump. + /// + [Description("Could not create dump as dotnet diagnostics threw an exception!")] + DotnetDiagnosticsFailure, } } diff --git a/src/Tgstation.Server.Api/Models/Internal/DreamDaemonLaunchParameters.cs b/src/Tgstation.Server.Api/Models/Internal/DreamDaemonLaunchParameters.cs index b21ce258ff..72b9ef88d6 100644 --- a/src/Tgstation.Server.Api/Models/Internal/DreamDaemonLaunchParameters.cs +++ b/src/Tgstation.Server.Api/Models/Internal/DreamDaemonLaunchParameters.cs @@ -98,6 +98,13 @@ namespace Tgstation.Server.Api.Models.Internal [ResponseOptions] public uint? MapThreads { get; set; } + /// + /// If minidumps should be taken instead of full dumps. + /// + [Required] + [ResponseOptions] + public bool? Minidumps { get; set; } + /// /// Check if we match a given set of . is excluded. /// @@ -116,7 +123,7 @@ namespace Tgstation.Server.Api.Models.Internal && AdditionalParameters == otherParameters.AdditionalParameters && StartProfiler == otherParameters.StartProfiler && LogOutput == otherParameters.LogOutput - && MapThreads == otherParameters.MapThreads; // We intentionally don't check StartupTimeout, health check seconds, or health check dump as they don't matter in terms of the watchdog + && MapThreads == otherParameters.MapThreads; // We intentionally don't check StartupTimeout, Minidumps, health check seconds, or health check dump as they don't matter in terms of the watchdog } } } diff --git a/src/Tgstation.Server.Api/Models/Internal/DreamMakerSettings.cs b/src/Tgstation.Server.Api/Models/Internal/DreamMakerSettings.cs index d6c229e673..c7a11c1b3b 100644 --- a/src/Tgstation.Server.Api/Models/Internal/DreamMakerSettings.cs +++ b/src/Tgstation.Server.Api/Models/Internal/DreamMakerSettings.cs @@ -39,5 +39,12 @@ namespace Tgstation.Server.Api.Models.Internal /// [Required] public TimeSpan? Timeout { get; set; } + + /// + /// Additional arguments added to the compiler command line. + /// + [StringLength(Limits.MaximumStringLength)] + [ResponseOptions] + public string? CompilerAdditionalArguments { get; set; } } } diff --git a/src/Tgstation.Server.Api/Rights/DreamDaemonRights.cs b/src/Tgstation.Server.Api/Rights/DreamDaemonRights.cs index 9af0522856..279c49c81b 100644 --- a/src/Tgstation.Server.Api/Rights/DreamDaemonRights.cs +++ b/src/Tgstation.Server.Api/Rights/DreamDaemonRights.cs @@ -117,5 +117,10 @@ namespace Tgstation.Server.Api.Rights /// User can use . /// BroadcastMessage = 1 << 20, + + /// + /// User can use . + /// + SetMinidumps = 1 << 21, } } diff --git a/src/Tgstation.Server.Api/Rights/DreamMakerRights.cs b/src/Tgstation.Server.Api/Rights/DreamMakerRights.cs index c8852ce338..6e7e882dd4 100644 --- a/src/Tgstation.Server.Api/Rights/DreamMakerRights.cs +++ b/src/Tgstation.Server.Api/Rights/DreamMakerRights.cs @@ -57,5 +57,10 @@ namespace Tgstation.Server.Api.Rights /// User may modify . /// SetTimeout = 1 << 8, + + /// + /// User may modify . + /// + SetCompilerArguments = 1 << 9, } } diff --git a/src/Tgstation.Server.Api/Tgstation.Server.Api.csproj b/src/Tgstation.Server.Api/Tgstation.Server.Api.csproj index 1c5c6e8c65..502f74b255 100644 --- a/src/Tgstation.Server.Api/Tgstation.Server.Api.csproj +++ b/src/Tgstation.Server.Api/Tgstation.Server.Api.csproj @@ -27,7 +27,7 @@ - + diff --git a/src/Tgstation.Server.Client/Tgstation.Server.Client.csproj b/src/Tgstation.Server.Client/Tgstation.Server.Client.csproj index 62e15714d5..f085719f84 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/Tgstation.Server.Common.csproj b/src/Tgstation.Server.Common/Tgstation.Server.Common.csproj index b77bdc4dc1..b23195dfca 100644 --- a/src/Tgstation.Server.Common/Tgstation.Server.Common.csproj +++ b/src/Tgstation.Server.Common/Tgstation.Server.Common.csproj @@ -15,21 +15,13 @@ - - - - + - + - - - - - diff --git a/src/Tgstation.Server.Common/build_logo.js b/src/Tgstation.Server.Common/build_logo.js deleted file mode 100644 index c79a46903c..0000000000 --- a/src/Tgstation.Server.Common/build_logo.js +++ /dev/null @@ -1,48 +0,0 @@ -// Prereq packages: svg-to-ico@1.0.14 svg2img@1.0.0-beta.2 -// Usage: node ./build_logo.js -// Generates ../../artifacts/tgs.ico and ../../artifacts/tgs.ico - -const svg_to_img = require("svg-to-ico"); -const svg2img = require('svg2img'); -const fs = require('fs'); -const { exit } = require("process"); -if (!fs.existsSync("../../artifacts")) { - fs.mkdirSync("../../artifacts",'0777', true); -} - -const svg_bytes = fs.readFileSync("../../build/logo.svg"); -const svg = svg_bytes.toString(); -const white_bg_svg = svg - .replace(" - + diff --git a/src/Tgstation.Server.Host/.config/dotnet-tools.json b/src/Tgstation.Server.Host/.config/dotnet-tools.json index c03564f970..8e82e30018 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": "8.0.0", + "version": "8.0.4", "commands": [ "dotnet-ef" ] diff --git a/src/Tgstation.Server.Host/Components/Chat/ChatManager.cs b/src/Tgstation.Server.Host/Components/Chat/ChatManager.cs index d53e997b25..049c217efb 100644 --- a/src/Tgstation.Server.Host/Components/Chat/ChatManager.cs +++ b/src/Tgstation.Server.Host/Components/Chat/ChatManager.cs @@ -839,7 +839,7 @@ namespace Tgstation.Server.Host.Components.Chat splits.RemoveAt(0); var arguments = String.Join(" ", splits); - Tuple? GetCommand() + Tuple? GetCommand(string command) { if (!builtinCommands.TryGetValue(command, out var handler)) return trackingContexts @@ -867,7 +867,7 @@ namespace Tgstation.Server.Host.Components.Chat } else { - var helpTuple = GetCommand(); + var helpTuple = GetCommand(splits[0]); if (helpTuple != default) { var (helpHandler, _) = helpTuple; @@ -881,7 +881,7 @@ namespace Tgstation.Server.Host.Components.Chat return; } - var tuple = GetCommand(); + var tuple = GetCommand(command); if (tuple == default) { diff --git a/src/Tgstation.Server.Host/Components/Chat/Providers/DiscordProvider.cs b/src/Tgstation.Server.Host/Components/Chat/Providers/DiscordProvider.cs index da46f6e678..7012a94baf 100644 --- a/src/Tgstation.Server.Host/Components/Chat/Providers/DiscordProvider.cs +++ b/src/Tgstation.Server.Host/Components/Chat/Providers/DiscordProvider.cs @@ -320,7 +320,7 @@ namespace Tgstation.Server.Host.Components.Chat.Providers Optional author = new EmbedAuthor(assemblyInformationProvider.VersionPrefix) { Url = "https://github.com/tgstation/tgstation-server", - IconUrl = "https://cdn.discordapp.com/attachments/1114451486374637629/1151650846019432448/tgs.png", + IconUrl = "https://cdn.discordapp.com/attachments/1114451486374637629/1151650846019432448/tgs.png", // 404's in browsers but works in Discord }; var embed = new Embed { diff --git a/src/Tgstation.Server.Host/Components/Deployment/DmbFactory.cs b/src/Tgstation.Server.Host/Components/Deployment/DmbFactory.cs index 3b6695313b..fbeb7d3f92 100644 --- a/src/Tgstation.Server.Host/Components/Deployment/DmbFactory.cs +++ b/src/Tgstation.Server.Host/Components/Deployment/DmbFactory.cs @@ -3,6 +3,7 @@ using System.Collections.Generic; using System.Diagnostics.CodeAnalysis; using System.IO; using System.Linq; +using System.Text; using System.Threading; using System.Threading.Tasks; @@ -178,7 +179,8 @@ namespace Tgstation.Server.Host.Components.Deployment { var jobId = nextDmbProvider.CompileJob.Require(x => x.Id); var incremented = jobLockCounts[jobId] += lockCount; - logger.LogTrace("Compile job {jobId} lock count now: {lockCount}", jobId, incremented); + logger.LogTrace("Compile job {jobId} lock increased by: {increment}", jobId, lockCount); + LogLockCounts(); return nextDmbProvider; } } @@ -325,14 +327,18 @@ namespace Tgstation.Server.Host.Components.Deployment if (!jobLockCounts.TryGetValue(compileJobId, out int value)) { value = 1; + logger.LogTrace("Initializing lock count for compile job {id}", compileJobId); jobLockCounts.Add(compileJobId, 1); } else + { + logger.LogTrace("FromCompileJob already had a jobLockCounts entry for {id}. Incrementing lock count to {value}.", compileJobId, value); jobLockCounts[compileJobId] = ++value; + } providerSubmitted = true; - logger.LogTrace("Compile job {id} lock count now: {lockCount}", compileJobId, value); + LogLockCounts(); return newProvider; } } @@ -385,7 +391,7 @@ namespace Tgstation.Server.Host.Components.Deployment await ioManager.CreateDirectory(gameDirectory, cancellationToken); var directories = await ioManager.GetDirectories(gameDirectory, cancellationToken); int deleting = 0; - var tasks = directories.Select(async x => + var tasks = directories.Select(async x => { var nameOnly = ioManager.GetFileName(x); if (jobUidsToNotErase.Contains(nameOnly)) @@ -396,17 +402,13 @@ namespace Tgstation.Server.Host.Components.Deployment ++deleting; await DeleteCompileJobContent(x, cancellationToken); } - catch (OperationCanceledException) - { - throw; - } - catch (Exception e) + catch (Exception e) when (e is not OperationCanceledException) { logger.LogWarning(e, "Error deleting directory {dirName}!", x); } }).ToList(); if (deleting > 0) - await Task.WhenAll(tasks); + await ValueTaskExtensions.WhenAll(tasks); } #pragma warning restore CA1506 @@ -424,30 +426,31 @@ namespace Tgstation.Server.Host.Components.Deployment /// The to clean. void CleanRegisteredCompileJob(CompileJob job) { - async Task HandleCleanup() + Task HandleCleanup() { - // First kill the GitHub deployment - var remoteDeploymentManager = remoteDeploymentManagerFactory.CreateRemoteDeploymentManager(metadata, job); - - // DCT: None available - var deploymentJob = remoteDeploymentManager.MarkInactive(job, CancellationToken.None); - - var deleteTask = DeleteCompileJobContent(job.DirectoryName!.Value.ToString(), cleanupCts.Token); var otherTask = cleanupTask; async Task WrapThrowableTasks() { try { + // First kill the GitHub deployment + var remoteDeploymentManager = remoteDeploymentManagerFactory.CreateRemoteDeploymentManager(metadata, job); + + var cancellationToken = cleanupCts.Token; + var deploymentJob = remoteDeploymentManager.MarkInactive(job, cancellationToken); + + var deleteTask = DeleteCompileJobContent(job.DirectoryName!.Value.ToString(), cancellationToken); + await ValueTaskExtensions.WhenAll(deleteTask, deploymentJob); } - catch (Exception ex) + catch (Exception ex) when (ex is not OperationCanceledException) { logger.LogWarning(ex, "Error cleaning up compile job {jobGuid}!", job.DirectoryName); } } - await Task.WhenAll(otherTask, WrapThrowableTasks()); + return Task.WhenAll(otherTask, WrapThrowableTasks()); } lock (jobLockCounts) @@ -467,6 +470,8 @@ namespace Tgstation.Server.Host.Components.Deployment } else logger.LogError("Extra Dispose of DmbProvider for CompileJob {compileJobId}!", jobId); + + LogLockCounts(); } } @@ -482,5 +487,30 @@ namespace Tgstation.Server.Host.Components.Deployment await eventConsumer.HandleEvent(EventType.DeploymentCleanup, new List { ioManager.ResolvePath(directory) }, true, cancellationToken); await ioManager.DeleteDirectory(directory, cancellationToken); } + + /// + /// Log out the current lock counts to Trace. + /// + /// must be locked before calling this function. + void LogLockCounts() + { + if (jobLockCounts.Count == 0) + { + logger.LogWarning("No compile jobs registered!"); + return; + } + + var builder = new StringBuilder(); + foreach (var jobId in jobLockCounts.Keys) + { + builder.AppendLine(); + builder.Append("\t- "); + builder.Append(jobId); + builder.Append(": "); + builder.Append(jobLockCounts[jobId]); + } + + logger.LogTrace("Compile Job Lock Counts:{details}", builder.ToString()); + } } } diff --git a/src/Tgstation.Server.Host/Components/Deployment/DreamMaker.cs b/src/Tgstation.Server.Host/Components/Deployment/DreamMaker.cs index ac5c4576e3..76c415bac4 100644 --- a/src/Tgstation.Server.Host/Components/Deployment/DreamMaker.cs +++ b/src/Tgstation.Server.Host/Components/Deployment/DreamMaker.cs @@ -571,6 +571,7 @@ namespace Tgstation.Server.Host.Components.Deployment progressReporter.StageName = "Copying repository"; var resolvedOutputDirectory = ioManager.ResolvePath(outputDirectory); var repoOrigin = repository.Origin; + var repoReference = repository.Reference; using (repository) await repository.CopyTo(resolvedOutputDirectory, cancellationToken); @@ -585,6 +586,7 @@ namespace Tgstation.Server.Host.Components.Deployment resolvedOutputDirectory, repoOrigin.ToString(), engineLock.Version.ToString(), + repoReference, }, true, cancellationToken); @@ -630,7 +632,7 @@ namespace Tgstation.Server.Host.Components.Deployment // run compiler progressReporter.StageName = "Running Compiler"; - var compileSuceeded = await RunDreamMaker(engineLock, job, cancellationToken); + var compileSuceeded = await RunDreamMaker(engineLock, job, dreamMakerSettings.CompilerAdditionalArguments, cancellationToken); // Session takes ownership of the lock and Disposes it so save this for later var engineVersion = engineLock.Version; @@ -848,17 +850,25 @@ namespace Tgstation.Server.Host.Components.Deployment /// /// The to use. /// The for the operation. + /// Additional arguments to be added to the compiler. /// The for the operation. /// A resulting in if compilation succeeded, otherwise. - async ValueTask RunDreamMaker(IEngineExecutableLock engineLock, Models.CompileJob job, CancellationToken cancellationToken) + async ValueTask RunDreamMaker( + IEngineExecutableLock engineLock, + Models.CompileJob job, + string? additionalCompilerArguments, + CancellationToken cancellationToken) { - var arguments = engineLock.FormatCompilerArguments($"{job.DmeName}.{DmeExtension}"); + var environment = await engineLock.LoadEnv(logger, true, cancellationToken); + var arguments = engineLock.FormatCompilerArguments($"{job.DmeName}.{DmeExtension}", additionalCompilerArguments); - await using var dm = processExecutor.LaunchProcess( + await using var dm = await processExecutor.LaunchProcess( engineLock.CompilerExePath, ioManager.ResolvePath( job.DirectoryName!.Value.ToString()), arguments, + cancellationToken, + environment, readStandardHandles: true, noShellExecute: true); diff --git a/src/Tgstation.Server.Host/Components/Deployment/HardLinkDmbProvider.cs b/src/Tgstation.Server.Host/Components/Deployment/HardLinkDmbProvider.cs index 98a8361e34..09cdfb31d5 100644 --- a/src/Tgstation.Server.Host/Components/Deployment/HardLinkDmbProvider.cs +++ b/src/Tgstation.Server.Host/Components/Deployment/HardLinkDmbProvider.cs @@ -46,12 +46,14 @@ namespace Tgstation.Server.Host.Components.Deployment /// The for the . /// The value of . /// The for the . + /// The launch level. public HardLinkDmbProvider( IDmbProvider baseProvider, IIOManager ioManager, IFilesystemLinkFactory linkFactory, ILogger logger, - GeneralConfiguration generalConfiguration) + GeneralConfiguration generalConfiguration, + DreamDaemonSecurity securityLevel) : base( baseProvider, ioManager, @@ -61,7 +63,7 @@ namespace Tgstation.Server.Host.Components.Deployment cancellationTokenSource = new CancellationTokenSource(); try { - mirroringTask = MirrorSourceDirectory(generalConfiguration.GetCopyDirectoryTaskThrottle(), cancellationTokenSource.Token); + mirroringTask = MirrorSourceDirectory(generalConfiguration.GetCopyDirectoryTaskThrottle(), securityLevel, cancellationTokenSource.Token); } catch { @@ -143,9 +145,10 @@ namespace Tgstation.Server.Host.Components.Deployment /// Mirror the . /// /// The optional maximum number of simultaneous tasks allowed to execute. + /// The launch level. /// The for the operation. /// A resulting in the full path to the mirrored directory. - async Task MirrorSourceDirectory(int? taskThrottle, CancellationToken cancellationToken) + async Task MirrorSourceDirectory(int? taskThrottle, DreamDaemonSecurity securityLevel, CancellationToken cancellationToken) { var stopwatch = Stopwatch.StartNew(); var mirrorGuid = Guid.NewGuid(); @@ -157,7 +160,12 @@ namespace Tgstation.Server.Host.Components.Deployment var dest = IOManager.ResolvePath(mirrorGuid.ToString()); using var semaphore = taskThrottle.HasValue ? new SemaphoreSlim(taskThrottle.Value) : null; - await Task.WhenAll(MirrorDirectoryImpl(src, dest, semaphore, cancellationToken)); + await Task.WhenAll(MirrorDirectoryImpl( + src, + dest, + semaphore, + securityLevel, + cancellationToken)); stopwatch.Stop(); logger.LogDebug( @@ -175,14 +183,15 @@ namespace Tgstation.Server.Host.Components.Deployment /// The source directory path. /// The destination directory path. /// Optional used to limit degree of parallelism. + /// The launch level. /// 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, DreamDaemonSecurity securityLevel, CancellationToken cancellationToken) { var dir = new DirectoryInfo(src); Task? subdirCreationTask = null; - var dreamDaemonWillAcceptOutOfDirectorySymlinks = CompileJob.MinimumSecurityLevel == DreamDaemonSecurity.Trusted; + var dreamDaemonWillAcceptOutOfDirectorySymlinks = securityLevel == DreamDaemonSecurity.Trusted; foreach (var subDirectory in dir.EnumerateDirectories()) { var mirroredName = Path.Combine(dest, subDirectory.Name); @@ -216,7 +225,7 @@ namespace Tgstation.Server.Host.Components.Deployment logger.LogDebug("Recreating symlinked directory {name} as hard links...", subDirectory.Name); var checkingSubdirCreationTask = true; - foreach (var copyTask in MirrorDirectoryImpl(subDirectory.FullName, mirroredName, semaphore, cancellationToken)) + foreach (var copyTask in MirrorDirectoryImpl(subDirectory.FullName, mirroredName, semaphore, securityLevel, cancellationToken)) { if (subdirCreationTask == null) { diff --git a/src/Tgstation.Server.Host/Components/Deployment/IDmbProvider.cs b/src/Tgstation.Server.Host/Components/Deployment/IDmbProvider.cs index 820796c318..7d5f0fd6a9 100644 --- a/src/Tgstation.Server.Host/Components/Deployment/IDmbProvider.cs +++ b/src/Tgstation.Server.Host/Components/Deployment/IDmbProvider.cs @@ -15,7 +15,7 @@ namespace Tgstation.Server.Host.Components.Deployment string DmbName { get; } /// - /// The primary game directory with a trailing directory separator. + /// The primary game directory. /// string Directory { get; } diff --git a/src/Tgstation.Server.Host/Components/Deployment/SwappableDmbProvider.cs b/src/Tgstation.Server.Host/Components/Deployment/SwappableDmbProvider.cs index 75c5a1b6bb..7ad999856c 100644 --- a/src/Tgstation.Server.Host/Components/Deployment/SwappableDmbProvider.cs +++ b/src/Tgstation.Server.Host/Components/Deployment/SwappableDmbProvider.cs @@ -87,7 +87,7 @@ namespace Tgstation.Server.Host.Components.Deployment } /// - /// Should be . before calling to ensure the is ready to instantly swap. Can be called multiple times. + /// Should be ed. before calling to ensure the is ready to instantly swap. Can be called multiple times. /// /// The for the operation. /// A representing the preparation process. diff --git a/src/Tgstation.Server.Host/Components/Engine/ByondInstallation.cs b/src/Tgstation.Server.Host/Components/Engine/ByondInstallation.cs index ecfb965e58..8ee095babc 100644 --- a/src/Tgstation.Server.Host/Components/Engine/ByondInstallation.cs +++ b/src/Tgstation.Server.Host/Components/Engine/ByondInstallation.cs @@ -6,6 +6,7 @@ using System.Threading.Tasks; using Tgstation.Server.Api.Models; using Tgstation.Server.Api.Models.Internal; using Tgstation.Server.Host.Components.Deployment; +using Tgstation.Server.Host.IO; namespace Tgstation.Server.Host.Components.Engine { @@ -32,6 +33,9 @@ namespace Tgstation.Server.Host.Components.Engine /// public override bool PreferFileLogging => false; + /// + public override bool UseDotnetDump => false; + /// public override Task InstallationTask { get; } @@ -75,6 +79,7 @@ namespace Tgstation.Server.Host.Components.Engine /// /// Initializes a new instance of the class. /// + /// The for the . /// The value of . /// The value of . /// The value of . @@ -82,12 +87,14 @@ namespace Tgstation.Server.Host.Components.Engine /// If a CLI application is being used. /// The value of . public ByondInstallation( + IIOManager installationIOManager, Task installationTask, EngineVersion version, string dreamDaemonPath, string dreamMakerPath, bool supportsCli, bool supportsMapThreads) + : base(installationIOManager) { InstallationTask = installationTask ?? throw new ArgumentNullException(nameof(installationTask)); ArgumentNullException.ThrowIfNull(version); @@ -140,7 +147,14 @@ namespace Tgstation.Server.Host.Components.Engine } /// - public override string FormatCompilerArguments(string dmePath) - => $"-clean \"{dmePath ?? throw new ArgumentNullException(nameof(dmePath))}\""; + public override string FormatCompilerArguments(string dmePath, string? additionalArguments) + { + if (String.IsNullOrWhiteSpace(additionalArguments)) + additionalArguments = String.Empty; + else + additionalArguments = $"{additionalArguments.Trim()} "; + + return $"-clean {additionalArguments}\"{dmePath ?? throw new ArgumentNullException(nameof(dmePath))}\""; + } } } diff --git a/src/Tgstation.Server.Host/Components/Engine/ByondInstallerBase.cs b/src/Tgstation.Server.Host/Components/Engine/ByondInstallerBase.cs index a992073d10..c7c73e3a23 100644 --- a/src/Tgstation.Server.Host/Components/Engine/ByondInstallerBase.cs +++ b/src/Tgstation.Server.Host/Components/Engine/ByondInstallerBase.cs @@ -1,6 +1,7 @@ using System; +using System.Collections.Generic; using System.Globalization; -using System.Text; +using System.Linq; using System.Threading; using System.Threading.Tasks; @@ -9,7 +10,6 @@ using Microsoft.Extensions.Logging; using Tgstation.Server.Api.Models; using Tgstation.Server.Host.IO; using Tgstation.Server.Host.Jobs; -using Tgstation.Server.Host.Utils; namespace Tgstation.Server.Host.Components.Engine { @@ -23,31 +23,21 @@ namespace Tgstation.Server.Host.Components.Engine /// protected const string ByondBinPath = "byond/bin"; + /// + /// The path to the cfg directory. + /// + protected const string CfgDirectoryName = "cfg"; + /// /// 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; @@ -88,21 +78,22 @@ namespace Tgstation.Server.Host.Components.Engine { CheckVersionValidity(version); - var binPathForVersion = IOManager.ConcatPath(path, ByondBinPath); + var installationIOManager = new ResolvingIOManager(IOManager, path); var supportsMapThreads = version.Version >= MapThreadsVersion; return new ByondInstallation( + installationIOManager, installationTask, version, - IOManager.ResolvePath( - IOManager.ConcatPath( - binPathForVersion, + installationIOManager.ResolvePath( + installationIOManager.ConcatPath( + ByondBinPath, GetDreamDaemonName( version.Version!, out var supportsCli))), - IOManager.ResolvePath( - IOManager.ConcatPath( - binPathForVersion, + installationIOManager.ResolvePath( + installationIOManager.ConcatPath( + ByondBinPath, DreamMakerName)), supportsCli, supportsMapThreads); @@ -143,18 +134,10 @@ namespace Tgstation.Server.Host.Components.Engine localCfgDirectory, cancellationToken); - // Delete trusted.txt so it doesn't grow too large - var trustedFilePath = - IOManager.ConcatPath( - localCfgDirectory, - TrustedDmbFileName); + var additionalCleanTasks = AdditionalCacheCleanFilePaths(localCfgDirectory) + .Select(path => IOManager.DeleteFile(path, cancellationToken)); - Logger.LogTrace("Deleting trusted .dmbs file {trustedFilePath}", trustedFilePath); - var trustedDmbDeleteTask = IOManager.DeleteFile( - trustedFilePath, - cancellationToken); - - await Task.WhenAll(cacheCleanTask, cfgCreateTask, trustedDmbDeleteTask); + await Task.WhenAll(cacheCleanTask, cfgCreateTask, Task.WhenAll(additionalCleanTasks)); } catch (Exception ex) when (ex is not OperationCanceledException) { @@ -162,49 +145,6 @@ namespace Tgstation.Server.Host.Components.Engine } } - /// - 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) { @@ -239,6 +179,13 @@ namespace Tgstation.Server.Host.Components.Engine /// The file name of the DreamDaemon executable. protected abstract string GetDreamDaemonName(Version byondVersion, out bool supportsCli); + /// + /// List off additional file paths in the to delete. + /// + /// The full path to the relevant . + /// An of paths in to clean. + protected virtual IEnumerable AdditionalCacheCleanFilePaths(string configDirectory) => Enumerable.Empty(); + /// /// Create a pointing to the location of the download for a given . /// diff --git a/src/Tgstation.Server.Host/Components/Engine/DelegatingEngineInstaller.cs b/src/Tgstation.Server.Host/Components/Engine/DelegatingEngineInstaller.cs index 1157dff565..91887e7314 100644 --- a/src/Tgstation.Server.Host/Components/Engine/DelegatingEngineInstaller.cs +++ b/src/Tgstation.Server.Host/Components/Engine/DelegatingEngineInstaller.cs @@ -41,8 +41,8 @@ namespace Tgstation.Server.Host.Components.Engine => 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 Install(EngineVersion version, string path, bool deploymentPipelineProcesses, CancellationToken cancellationToken) + => DelegateCall(version, installer => installer.Install(version, path, deploymentPipelineProcesses, cancellationToken)); /// public ValueTask TrustDmbPath(EngineVersion version, string fullDmbPath, CancellationToken cancellationToken) diff --git a/src/Tgstation.Server.Host/Components/Engine/EngineExecutableLock.cs b/src/Tgstation.Server.Host/Components/Engine/EngineExecutableLock.cs index 5d7a1d7fef..3589d6c4af 100644 --- a/src/Tgstation.Server.Host/Components/Engine/EngineExecutableLock.cs +++ b/src/Tgstation.Server.Host/Components/Engine/EngineExecutableLock.cs @@ -36,6 +36,9 @@ namespace Tgstation.Server.Host.Components.Engine /// public Task InstallationTask => Instance.InstallationTask; + /// + public bool UseDotnetDump => Instance.UseDotnetDump; + /// public void DoNotDeleteThisSession() => DangerousDropReference(); @@ -52,7 +55,7 @@ namespace Tgstation.Server.Host.Components.Engine logFilePath); /// - public string FormatCompilerArguments(string dmePath) => Instance.FormatCompilerArguments(dmePath); + public string FormatCompilerArguments(string dmePath, string? additionalArguments) => Instance.FormatCompilerArguments(dmePath, additionalArguments); /// public ValueTask StopServerProcess(ILogger logger, IProcess process, string accessIdentifier, ushort port, CancellationToken cancellationToken) @@ -62,5 +65,9 @@ namespace Tgstation.Server.Host.Components.Engine accessIdentifier, port, cancellationToken); + + /// + public ValueTask?> LoadEnv(ILogger logger, bool forCompiler, CancellationToken cancellationToken) + => Instance.LoadEnv(logger, forCompiler, cancellationToken); } } diff --git a/src/Tgstation.Server.Host/Components/Engine/EngineInstallationBase.cs b/src/Tgstation.Server.Host/Components/Engine/EngineInstallationBase.cs index 5edf44609f..928f8e3006 100644 --- a/src/Tgstation.Server.Host/Components/Engine/EngineInstallationBase.cs +++ b/src/Tgstation.Server.Host/Components/Engine/EngineInstallationBase.cs @@ -1,15 +1,19 @@ using System; using System.Collections.Generic; using System.Linq; +using System.Text; using System.Threading; using System.Threading.Tasks; using System.Web; +using DotEnv.Core; + using Microsoft.Extensions.Logging; using Tgstation.Server.Api.Models; using Tgstation.Server.Api.Models.Internal; using Tgstation.Server.Host.Components.Deployment; +using Tgstation.Server.Host.IO; using Tgstation.Server.Host.System; namespace Tgstation.Server.Host.Components.Engine @@ -35,9 +39,17 @@ namespace Tgstation.Server.Host.Components.Engine /// public abstract bool PromptsForNetworkAccess { get; } + /// + public abstract bool UseDotnetDump { get; } + /// public abstract Task InstallationTask { get; } + /// + /// The pointing to the installation directory. + /// + protected IIOManager InstallationIOManager { get; } + /// /// Encode given parameters for passing as world.params on the command line. /// @@ -56,8 +68,17 @@ namespace Tgstation.Server.Host.Components.Engine return parametersString; } + /// + /// Initializes a new instance of the class. + /// + /// The value of . + public EngineInstallationBase(IIOManager installationIOManager) + { + InstallationIOManager = installationIOManager ?? throw new ArgumentNullException(nameof(installationIOManager)); + } + /// - public abstract string FormatCompilerArguments(string dmePath); + public abstract string FormatCompilerArguments(string dmePath, string? additionalArguments); /// public abstract string FormatServerArguments( @@ -69,10 +90,45 @@ namespace Tgstation.Server.Host.Components.Engine /// public virtual async ValueTask StopServerProcess(ILogger logger, IProcess process, string accessIdentifier, ushort port, CancellationToken cancellationToken) { + ArgumentNullException.ThrowIfNull(logger); cancellationToken.ThrowIfCancellationRequested(); logger.LogTrace("Terminating engine server process..."); process.Terminate(); await process.Lifetime; } + + /// + public async ValueTask?> LoadEnv(ILogger logger, bool forCompiler, CancellationToken cancellationToken) + { + ArgumentNullException.ThrowIfNull(logger); + + var envFile = forCompiler + ? "compiler.env" + : "server.env"; + + if (!await InstallationIOManager.FileExists(envFile, cancellationToken)) + { + logger.LogTrace("No {envFile} present in engine installation {version}", envFile, Version); + return null; + } + + logger.LogDebug("Loading {envFile} for engine installation {version}...", envFile, Version); + + var fileBytes = await InstallationIOManager.ReadAllBytes(envFile, cancellationToken); + var fileContents = Encoding.UTF8.GetString(fileBytes); + var parser = new EnvParser(); + + try + { + var variables = parser.Parse(fileContents); + + return variables.ToDictionary(); + } + catch (Exception ex) + { + logger.LogWarning(ex, "Unable to parse {envFile}!", envFile); + return null; + } + } } } diff --git a/src/Tgstation.Server.Host/Components/Engine/EngineInstallerBase.cs b/src/Tgstation.Server.Host/Components/Engine/EngineInstallerBase.cs index 7c25c0a13c..12cf8e657c 100644 --- a/src/Tgstation.Server.Host/Components/Engine/EngineInstallerBase.cs +++ b/src/Tgstation.Server.Host/Components/Engine/EngineInstallerBase.cs @@ -46,7 +46,7 @@ namespace Tgstation.Server.Host.Components.Engine public abstract Task CleanCache(CancellationToken cancellationToken); /// - public abstract ValueTask Install(EngineVersion version, string path, CancellationToken cancellationToken); + public abstract ValueTask Install(EngineVersion version, string path, bool deploymentPipelineProcesses, CancellationToken cancellationToken); /// public abstract ValueTask UpgradeInstallation(EngineVersion version, string path, CancellationToken cancellationToken); diff --git a/src/Tgstation.Server.Host/Components/Engine/EngineManager.cs b/src/Tgstation.Server.Host/Components/Engine/EngineManager.cs index 00290840f0..a7cbd10f4f 100644 --- a/src/Tgstation.Server.Host/Components/Engine/EngineManager.cs +++ b/src/Tgstation.Server.Host/Components/Engine/EngineManager.cs @@ -438,6 +438,7 @@ namespace Tgstation.Server.Host.Components.Engine installLock = installationContainer.AddReference(); } + var deploymentPipelineProcesses = !neededForLock; try { if (installedOrInstalling) @@ -471,18 +472,18 @@ namespace Tgstation.Server.Host.Components.Engine progressReporter.StageName = "Running event"; var versionString = version.ToString(); - await eventConsumer.HandleEvent(EventType.EngineInstallStart, new List { versionString }, false, cancellationToken); + await eventConsumer.HandleEvent(EventType.EngineInstallStart, new List { versionString }, deploymentPipelineProcesses, cancellationToken); - await InstallVersionFiles(progressReporter, version, customVersionStream, cancellationToken); + await InstallVersionFiles(progressReporter, version, customVersionStream, deploymentPipelineProcesses, cancellationToken); ourTcs.SetResult(); - await eventConsumer.HandleEvent(EventType.EngineInstallComplete, new List { versionString }, false, cancellationToken); + await eventConsumer.HandleEvent(EventType.EngineInstallComplete, new List { versionString }, deploymentPipelineProcesses, cancellationToken); } catch (Exception ex) { if (ex is not OperationCanceledException) - await eventConsumer.HandleEvent(EventType.EngineInstallFail, new List { ex.Message }, false, cancellationToken); + await eventConsumer.HandleEvent(EventType.EngineInstallFail, new List { ex.Message }, deploymentPipelineProcesses, cancellationToken); lock (installedVersions) installedVersions.Remove(version); @@ -506,9 +507,15 @@ namespace Tgstation.Server.Host.Components.Engine /// The optional for the operation. /// The being installed with the number set if appropriate. /// Custom zip file to use. Will cause a number to be added. + /// If processes should be launched as part of the deployment pipeline. /// The for the operation. /// A representing the running operation. - async ValueTask InstallVersionFiles(JobProgressReporter? progressReporter, EngineVersion version, Stream? customVersionStream, CancellationToken cancellationToken) + async ValueTask InstallVersionFiles( + JobProgressReporter? progressReporter, + EngineVersion version, + Stream? customVersionStream, + bool deploymentPipelineProcesses, + CancellationToken cancellationToken) { var installFullPath = ioManager.ResolvePath(version.ToString()); async ValueTask DirectoryCleanup() @@ -554,7 +561,7 @@ namespace Tgstation.Server.Host.Components.Engine if (progressReporter != null) progressReporter.StageName = "Running installation actions"; - await engineInstaller.Install(version, installFullPath, cancellationToken); + await engineInstaller.Install(version, installFullPath, deploymentPipelineProcesses, cancellationToken); if (progressReporter != null) progressReporter.StageName = "Writing version file"; diff --git a/src/Tgstation.Server.Host/Components/Engine/IEngineInstallation.cs b/src/Tgstation.Server.Host/Components/Engine/IEngineInstallation.cs index ff2e4155f3..1d467a2881 100644 --- a/src/Tgstation.Server.Host/Components/Engine/IEngineInstallation.cs +++ b/src/Tgstation.Server.Host/Components/Engine/IEngineInstallation.cs @@ -46,6 +46,11 @@ namespace Tgstation.Server.Host.Components.Engine /// bool PreferFileLogging { get; } + /// + /// If dotnet-dump should be used to create process dumps for this installation. + /// + bool UseDotnetDump { get; } + /// /// The that completes when the BYOND version finished installing. /// @@ -69,8 +74,9 @@ namespace Tgstation.Server.Host.Components.Engine /// Return the command line arguments for compiling a given if compilation is necessary. /// /// The full path to the .dme to compile. + /// Optional additional arguments provided to the compiler. /// The formatted arguments . - string FormatCompilerArguments(string dmePath); + string FormatCompilerArguments(string dmePath, string? additionalArguments); /// /// Kills a given engine server . @@ -82,5 +88,14 @@ namespace Tgstation.Server.Host.Components.Engine /// The for the operation. /// A representing the running operation. ValueTask StopServerProcess(ILogger logger, IProcess process, string accessIdentifier, ushort port, CancellationToken cancellationToken); + + /// + /// Loads the environment settings for either the server or compiler. + /// + /// The to write to. + /// If server.env will be loaded. If compiler.env will be loaded. + /// The for the operation. + /// A resulting in the environment or if the target environment file doesn't exist. + ValueTask?> LoadEnv(ILogger logger, bool forCompiler, CancellationToken cancellationToken); } } diff --git a/src/Tgstation.Server.Host/Components/Engine/IEngineInstaller.cs b/src/Tgstation.Server.Host/Components/Engine/IEngineInstaller.cs index 7169ffe4b5..a4ad57a4c0 100644 --- a/src/Tgstation.Server.Host/Components/Engine/IEngineInstaller.cs +++ b/src/Tgstation.Server.Host/Components/Engine/IEngineInstaller.cs @@ -34,9 +34,10 @@ namespace Tgstation.Server.Host.Components.Engine /// /// The being installed. /// The path to the installation. + /// If the operation should consider processes it launches to be part of the deployment pipeline. /// The for the operation. /// A representing the running operation. - ValueTask Install(EngineVersion version, string path, CancellationToken cancellationToken); + ValueTask Install(EngineVersion version, string path, bool deploymentPipelineProcesses, CancellationToken cancellationToken); /// /// Does actions necessary to get upgrade a version installed by a previous version of TGS. diff --git a/src/Tgstation.Server.Host/Components/Engine/OpenDreamInstallation.cs b/src/Tgstation.Server.Host/Components/Engine/OpenDreamInstallation.cs index ab9eb923d8..49c5ed1470 100644 --- a/src/Tgstation.Server.Host/Components/Engine/OpenDreamInstallation.cs +++ b/src/Tgstation.Server.Host/Components/Engine/OpenDreamInstallation.cs @@ -45,12 +45,10 @@ namespace Tgstation.Server.Host.Components.Engine public override bool PreferFileLogging => true; /// - public override Task InstallationTask { get; } + public override bool UseDotnetDump => true; - /// - /// The for the . - /// - readonly IIOManager ioManager; + /// + public override Task InstallationTask { get; } /// /// The for the . @@ -65,7 +63,7 @@ namespace Tgstation.Server.Host.Components.Engine /// /// Initializes a new instance of the class. /// - /// The value of . + /// The for the . /// The value of . /// The value of . /// The value of . @@ -73,15 +71,15 @@ namespace Tgstation.Server.Host.Components.Engine /// The value of . /// The value of . public OpenDreamInstallation( - IIOManager ioManager, + IIOManager installationIOManager, IAsyncDelayer asyncDelayer, IAbstractHttpClientFactory httpClientFactory, string serverExePath, string compilerExePath, Task installationTask, EngineVersion version) + : base(installationIOManager) { - this.ioManager = ioManager ?? throw new ArgumentNullException(nameof(ioManager)); this.asyncDelayer = asyncDelayer ?? throw new ArgumentNullException(nameof(asyncDelayer)); this.httpClientFactory = httpClientFactory ?? throw new ArgumentNullException(nameof(httpClientFactory)); ServerExePath = serverExePath ?? throw new ArgumentNullException(nameof(serverExePath)); @@ -109,13 +107,20 @@ namespace Tgstation.Server.Host.Components.Engine var parametersString = EncodeParameters(parameters, launchParameters); - var arguments = $"--cvar {(logFilePath != null ? $"log.path=\"{ioManager.GetDirectoryName(logFilePath)}\" --cvar log.format=\"{ioManager.GetFileName(logFilePath)}\"" : "log.enabled=false")} --cvar watchdog.token={accessIdentifier} --cvar log.runtimelog=false --cvar net.port={launchParameters.Port!.Value} --cvar opendream.topic_port=0 --cvar opendream.world_params=\"{parametersString}\" --cvar opendream.json_path=\"./{dmbProvider.DmbName}\""; + var arguments = $"--cvar {(logFilePath != null ? $"log.path=\"{InstallationIOManager.GetDirectoryName(logFilePath)}\" --cvar log.format=\"{InstallationIOManager.GetFileName(logFilePath)}\"" : "log.enabled=false")} --cvar watchdog.token={accessIdentifier} --cvar log.runtimelog=false --cvar net.port={launchParameters.Port!.Value} --cvar opendream.topic_port=0 --cvar opendream.world_params=\"{parametersString}\" --cvar opendream.json_path=\"./{dmbProvider.DmbName}\""; return arguments; } /// - public override string FormatCompilerArguments(string dmePath) - => $"--suppress-unimplemented --notices-enabled \"{dmePath ?? throw new ArgumentNullException(nameof(dmePath))}\""; + public override string FormatCompilerArguments(string dmePath, string? additionalArguments) + { + if (String.IsNullOrWhiteSpace(additionalArguments)) + additionalArguments = String.Empty; + else + additionalArguments = $"{additionalArguments.Trim()} "; + + return $"--suppress-unimplemented --notices-enabled {additionalArguments}\"{dmePath ?? throw new ArgumentNullException(nameof(dmePath))}\""; + } /// public override async ValueTask StopServerProcess( @@ -125,6 +130,8 @@ namespace Tgstation.Server.Host.Components.Engine ushort port, CancellationToken cancellationToken) { + ArgumentNullException.ThrowIfNull(logger); + const int MaximumTerminationSeconds = 5; logger.LogTrace("Attempting Robust.Server graceful exit (Timeout: {seconds}s)...", MaximumTerminationSeconds); diff --git a/src/Tgstation.Server.Host/Components/Engine/OpenDreamInstaller.cs b/src/Tgstation.Server.Host/Components/Engine/OpenDreamInstaller.cs index a2b74e1445..e552d16b40 100644 --- a/src/Tgstation.Server.Host/Components/Engine/OpenDreamInstaller.cs +++ b/src/Tgstation.Server.Host/Components/Engine/OpenDreamInstaller.cs @@ -9,7 +9,6 @@ using Microsoft.Extensions.Options; using Tgstation.Server.Api.Models; using Tgstation.Server.Common.Extensions; using Tgstation.Server.Common.Http; -using Tgstation.Server.Host.Common; using Tgstation.Server.Host.Components.Repository; using Tgstation.Server.Host.Configuration; using Tgstation.Server.Host.IO; @@ -124,7 +123,7 @@ namespace Tgstation.Server.Host.Components.Engine CheckVersionValidity(version); GetExecutablePaths(path, out var serverExePath, out var compilerExePath); return new OpenDreamInstallation( - IOManager, + new ResolvingIOManager(IOManager, path), asyncDelayer, httpClientFactory, serverExePath, @@ -193,7 +192,7 @@ namespace Tgstation.Server.Host.Components.Engine } /// - public override async ValueTask Install(EngineVersion version, string installPath, CancellationToken cancellationToken) + public override async ValueTask Install(EngineVersion version, string installPath, bool deploymentPipelineProcesses, CancellationToken cancellationToken) { CheckVersionValidity(version); ArgumentNullException.ThrowIfNull(installPath); @@ -232,36 +231,27 @@ namespace Tgstation.Server.Host.Components.Engine await Task.WhenAll(dirsMoveTasks.Concat(filesMoveTask)); } - var dotnetPaths = DotnetHelper.GetPotentialDotnetPaths(platformIdentifier.IsWindows) - .ToList(); - var tasks = dotnetPaths - .Select(path => IOManager.FileExists(path, cancellationToken)) - .ToList(); - - await Task.WhenAll(tasks); - - var selectedPathIndex = tasks.FindIndex(pathValidTask => pathValidTask.Result); - - if (selectedPathIndex == -1) + var dotnetPath = await DotnetHelper.GetDotnetPath(platformIdentifier, IOManager, cancellationToken); + if (dotnetPath == null) throw new JobException(ErrorCode.OpenDreamCantFindDotnet); - var dotnetPath = dotnetPaths[selectedPathIndex]; - const string DeployDir = "tgs_deploy"; int? buildExitCode = null; await HandleExtremelyLongPathOperation( async shortenedPath => { var shortenedDeployPath = IOManager.ConcatPath(shortenedPath, DeployDir); - await using var buildProcess = ProcessExecutor.LaunchProcess( + await using var buildProcess = await ProcessExecutor.LaunchProcess( dotnetPath, shortenedPath, $"run -c Release --project OpenDreamPackageTool -- --tgs -o {shortenedDeployPath}", + cancellationToken, + null, null, !GeneralConfiguration.OpenDreamSuppressInstallOutput, !GeneralConfiguration.OpenDreamSuppressInstallOutput); - if (SessionConfiguration.LowPriorityDeploymentProcesses) + if (deploymentPipelineProcesses && SessionConfiguration.LowPriorityDeploymentProcesses) buildProcess.AdjustPriority(false); using (cancellationToken.Register(() => buildProcess.Terminate())) diff --git a/src/Tgstation.Server.Host/Components/Engine/PosixByondInstaller.cs b/src/Tgstation.Server.Host/Components/Engine/PosixByondInstaller.cs index 037cacd437..fff62b3b3f 100644 --- a/src/Tgstation.Server.Host/Components/Engine/PosixByondInstaller.cs +++ b/src/Tgstation.Server.Host/Components/Engine/PosixByondInstaller.cs @@ -67,11 +67,11 @@ namespace Tgstation.Server.Host.Components.Engine Environment.GetFolderPath( Environment.SpecialFolder.UserProfile, Environment.SpecialFolderOption.DoNotVerify), - "./byond/cache")); + "./.byond/cache")); } /// - public override ValueTask Install(EngineVersion version, string path, CancellationToken cancellationToken) + public override ValueTask Install(EngineVersion version, string path, bool deploymentPipelineProcesses, CancellationToken cancellationToken) { CheckVersionValidity(version); ArgumentNullException.ThrowIfNull(path); @@ -119,6 +119,16 @@ namespace Tgstation.Server.Host.Components.Engine return ValueTask.CompletedTask; } + /// + public override ValueTask TrustDmbPath(EngineVersion version, string fullDmbPath, CancellationToken cancellationToken) + { + ArgumentNullException.ThrowIfNull(version); + ArgumentNullException.ThrowIfNull(fullDmbPath); + + Logger.LogTrace("No need to trust .dmb path \"{path}\" on POSIX", fullDmbPath); + return ValueTask.CompletedTask; + } + /// protected override string GetDreamDaemonName(Version byondVersion, out bool supportsCli) { diff --git a/src/Tgstation.Server.Host/Components/Engine/WindowsByondInstaller.cs b/src/Tgstation.Server.Host/Components/Engine/WindowsByondInstaller.cs index 4bafeb9e05..35bcbf63dc 100644 --- a/src/Tgstation.Server.Host/Components/Engine/WindowsByondInstaller.cs +++ b/src/Tgstation.Server.Host/Components/Engine/WindowsByondInstaller.cs @@ -47,6 +47,16 @@ namespace Tgstation.Server.Host.Components.Engine /// const string TgsFirewalledDDFile = "TGSFirewalledDD"; + /// + /// The name of the list of trusted .dmb files in the user's BYOND cfg directory. + /// + const string TrustedDmbFileName = "trusted.txt"; + + /// + /// for writing to files in the user's BYOND directory. + /// + static readonly SemaphoreSlim UserFilesSemaphore = new(1, 1); + /// /// The first version of BYOND to ship with dd.exe on the Windows build. /// @@ -127,7 +137,7 @@ namespace Tgstation.Server.Host.Components.Engine public void Dispose() => semaphore.Dispose(); /// - public override ValueTask Install(EngineVersion version, string path, CancellationToken cancellationToken) + public override ValueTask Install(EngineVersion version, string path, bool deploymentPipelineProcesses, CancellationToken cancellationToken) { CheckVersionValidity(version); ArgumentNullException.ThrowIfNull(path); @@ -142,7 +152,7 @@ namespace Tgstation.Server.Host.Components.Engine if (!generalConfiguration.SkipAddingByondFirewallException) { - var firewallTask = AddDreamDaemonToFirewall(version, path, cancellationToken); + var firewallTask = AddDreamDaemonToFirewall(version, path, deploymentPipelineProcesses, cancellationToken); tasks.Add(firewallTask); } @@ -165,7 +175,50 @@ namespace Tgstation.Server.Host.Components.Engine return; Logger.LogInformation("BYOND Version {version} needs dd.exe added to firewall", version); - await AddDreamDaemonToFirewall(version, path, cancellationToken); + await AddDreamDaemonToFirewall(version, path, true, cancellationToken); + } + + /// + 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); + } } /// @@ -175,6 +228,19 @@ namespace Tgstation.Server.Host.Components.Engine return supportsCli ? "dd.exe" : "dreamdaemon.exe"; } + /// + protected override IEnumerable AdditionalCacheCleanFilePaths(string configDirectory) + { + // Delete trusted.txt so it doesn't grow too large + var trustedFilePath = + IOManager.ConcatPath( + configDirectory, + TrustedDmbFileName); + + Logger.LogTrace("Deleting trusted .dmbs file {trustedFilePath}", trustedFilePath); + yield return trustedFilePath; + } + /// /// Creates the BYOND cfg file that prevents the trusted mode dialog from appearing when launching DreamDaemon. /// @@ -217,10 +283,11 @@ namespace Tgstation.Server.Host.Components.Engine try { // noShellExecute because we aren't doing runas shennanigans - await using var directXInstaller = processExecutor.LaunchProcess( + await using var directXInstaller = await processExecutor.LaunchProcess( IOManager.ConcatPath(rbdx, "DXSETUP.exe"), rbdx, "/silent", + cancellationToken, noShellExecute: true); int exitCode; @@ -243,9 +310,10 @@ namespace Tgstation.Server.Host.Components.Engine /// /// The BYOND . /// The path to the BYOND installation. + /// If the operation is part of the deployment pipeline. /// The for the operation. /// A representing the running operation. - async ValueTask AddDreamDaemonToFirewall(EngineVersion version, string path, CancellationToken cancellationToken) + async ValueTask AddDreamDaemonToFirewall(EngineVersion version, string path, bool deploymentPipelineProcesses, CancellationToken cancellationToken) { var dreamDaemonName = GetDreamDaemonName(version.Version!, out var usesDDExe); @@ -268,7 +336,7 @@ namespace Tgstation.Server.Host.Components.Engine Logger, ruleName, dreamDaemonPath, - sessionConfiguration.LowPriorityDeploymentProcesses, + deploymentPipelineProcesses && sessionConfiguration.LowPriorityDeploymentProcesses, cancellationToken); } catch (Exception ex) diff --git a/src/Tgstation.Server.Host/Components/Engine/WindowsOpenDreamInstaller.cs b/src/Tgstation.Server.Host/Components/Engine/WindowsOpenDreamInstaller.cs index 25968446fe..1cc8da52c5 100644 --- a/src/Tgstation.Server.Host/Components/Engine/WindowsOpenDreamInstaller.cs +++ b/src/Tgstation.Server.Host/Components/Engine/WindowsOpenDreamInstaller.cs @@ -66,15 +66,17 @@ namespace Tgstation.Server.Host.Components.Engine } /// - public override ValueTask Install(EngineVersion version, string installPath, CancellationToken cancellationToken) + public override ValueTask Install(EngineVersion version, string installPath, bool deploymentPipelineProcesses, CancellationToken cancellationToken) { var installTask = base.Install( version, installPath, + deploymentPipelineProcesses, cancellationToken); var firewallTask = AddServerFirewallException( version, installPath, + deploymentPipelineProcesses, cancellationToken); return ValueTaskExtensions.WhenAll(installTask, firewallTask); @@ -101,9 +103,10 @@ namespace Tgstation.Server.Host.Components.Engine /// /// The BYOND . /// The path to the BYOND installation. + /// If the operation is part of the deployment pipeline. /// The for the operation. /// A representing the running operation. - async ValueTask AddServerFirewallException(EngineVersion version, string path, CancellationToken cancellationToken) + async ValueTask AddServerFirewallException(EngineVersion version, string path, bool deploymentPipelineProcesses, CancellationToken cancellationToken) { if (GeneralConfiguration.SkipAddingByondFirewallException) return; @@ -123,7 +126,7 @@ namespace Tgstation.Server.Host.Components.Engine Logger, ruleName, serverExePath, - SessionConfiguration.LowPriorityDeploymentProcesses, + deploymentPipelineProcesses && SessionConfiguration.LowPriorityDeploymentProcesses, cancellationToken); } catch (Exception ex) diff --git a/src/Tgstation.Server.Host/Components/Events/EventConsumer.cs b/src/Tgstation.Server.Host/Components/Events/EventConsumer.cs index 559e584a27..fa98e56607 100644 --- a/src/Tgstation.Server.Host/Components/Events/EventConsumer.cs +++ b/src/Tgstation.Server.Host/Components/Events/EventConsumer.cs @@ -30,6 +30,10 @@ namespace Tgstation.Server.Host.Components.Events this.configuration = configuration ?? throw new ArgumentNullException(nameof(configuration)); } + /// + public ValueTask? HandleCustomEvent(string eventName, IEnumerable parameters, CancellationToken cancellationToken) + => configuration.HandleCustomEvent(eventName, parameters, cancellationToken); + /// public async ValueTask HandleEvent(EventType eventType, IEnumerable parameters, bool deploymentPipeline, CancellationToken cancellationToken) { diff --git a/src/Tgstation.Server.Host/Components/Events/EventScriptAttribute.cs b/src/Tgstation.Server.Host/Components/Events/EventScriptAttribute.cs index ba93d34cf9..7d772c2318 100644 --- a/src/Tgstation.Server.Host/Components/Events/EventScriptAttribute.cs +++ b/src/Tgstation.Server.Host/Components/Events/EventScriptAttribute.cs @@ -1,5 +1,4 @@ using System; -using System.Collections.Generic; namespace Tgstation.Server.Host.Components.Events { @@ -12,7 +11,7 @@ namespace Tgstation.Server.Host.Components.Events /// /// The name and order of the scripts the event script the runs. /// - public IReadOnlyList ScriptNames { get; } + public string[] ScriptNames { get; } /// /// Initializes a new instance of the class. diff --git a/src/Tgstation.Server.Host/Components/Events/EventType.cs b/src/Tgstation.Server.Host/Components/Events/EventType.cs index 4872998c61..5d8c0b9054 100644 --- a/src/Tgstation.Server.Host/Components/Events/EventType.cs +++ b/src/Tgstation.Server.Host/Components/Events/EventType.cs @@ -55,7 +55,7 @@ EngineActiveVersionChange, /// - /// After the repo is copied, before CodeModifications are applied. Parameters: Game directory path, origin commit sha, engine version string. + /// After the repo is copied, before CodeModifications are applied. Parameters: Game directory path, origin commit sha, engine version string, repository reference (or "(no branch)" if there is no reference). /// [EventScript("PreCompile")] CompileStart, diff --git a/src/Tgstation.Server.Host/Components/Events/IEventConsumer.cs b/src/Tgstation.Server.Host/Components/Events/IEventConsumer.cs index 4d268011f1..a12547e8c2 100644 --- a/src/Tgstation.Server.Host/Components/Events/IEventConsumer.cs +++ b/src/Tgstation.Server.Host/Components/Events/IEventConsumer.cs @@ -18,5 +18,14 @@ namespace Tgstation.Server.Host.Components.Events /// The for the operation. /// A representing the running operation. ValueTask HandleEvent(EventType eventType, IEnumerable parameters, bool deploymentPipeline, CancellationToken cancellationToken); + + /// + /// Handles a given custom event. + /// + /// The name of the event. + /// An of parameters for the event. + /// The for the operation. + /// A representing the running operation if the event was triggered successfully, if it matched a TGS event and wasn't executed. + ValueTask? HandleCustomEvent(string eventName, IEnumerable parameters, CancellationToken cancellationToken); } } diff --git a/src/Tgstation.Server.Host/Components/Events/NoopEventConsumer.cs b/src/Tgstation.Server.Host/Components/Events/NoopEventConsumer.cs index dde777572a..880a45c9ef 100644 --- a/src/Tgstation.Server.Host/Components/Events/NoopEventConsumer.cs +++ b/src/Tgstation.Server.Host/Components/Events/NoopEventConsumer.cs @@ -12,5 +12,9 @@ namespace Tgstation.Server.Host.Components.Events /// public ValueTask HandleEvent(EventType eventType, IEnumerable parameters, bool deploymentPipeline, CancellationToken cancellationToken) => ValueTask.CompletedTask; + + /// + public ValueTask? HandleCustomEvent(string eventName, IEnumerable parameters, CancellationToken cancellationToken) + => ValueTask.CompletedTask; } } diff --git a/src/Tgstation.Server.Host/Components/InstanceFactory.cs b/src/Tgstation.Server.Host/Components/InstanceFactory.cs index 2b24bcb6cb..f12eb28fa5 100644 --- a/src/Tgstation.Server.Host/Components/InstanceFactory.cs +++ b/src/Tgstation.Server.Host/Components/InstanceFactory.cs @@ -135,6 +135,11 @@ namespace Tgstation.Server.Host.Components /// readonly IAsyncDelayer asyncDelayer; + /// + /// The for the . + /// + readonly IDotnetDumpService dotnetDumpService; + /// /// The for the . /// @@ -177,6 +182,7 @@ namespace Tgstation.Server.Host.Components /// The value of . /// The value of . /// The value of . + /// The value of . /// The containing the value of . /// The containing the value of . public InstanceFactory( @@ -201,6 +207,7 @@ namespace Tgstation.Server.Host.Components IFileTransferTicketProvider fileTransferService, IRemoteDeploymentManagerFactory remoteDeploymentManagerFactory, IAsyncDelayer asyncDelayer, + IDotnetDumpService dotnetDumpService, IOptions generalConfigurationOptions, IOptions sessionConfigurationOptions) { @@ -225,6 +232,7 @@ namespace Tgstation.Server.Host.Components this.fileTransferService = fileTransferService ?? throw new ArgumentNullException(nameof(fileTransferService)); this.remoteDeploymentManagerFactory = remoteDeploymentManagerFactory ?? throw new ArgumentNullException(nameof(remoteDeploymentManagerFactory)); this.asyncDelayer = asyncDelayer ?? throw new ArgumentNullException(nameof(asyncDelayer)); + this.dotnetDumpService = dotnetDumpService ?? throw new ArgumentNullException(nameof(dotnetDumpService)); generalConfiguration = generalConfigurationOptions?.Value ?? throw new ArgumentNullException(nameof(generalConfigurationOptions)); sessionConfiguration = sessionConfigurationOptions?.Value ?? throw new ArgumentNullException(nameof(sessionConfigurationOptions)); } @@ -271,7 +279,11 @@ namespace Tgstation.Server.Host.Components var repoManager = repositoryManagerFactory.CreateRepositoryManager(repoIoManager, eventConsumer); try { - var engineManager = new EngineManager(byondIOManager, engineInstaller, eventConsumer, loggerFactory.CreateLogger()); + var engineManager = new EngineManager( + byondIOManager, + engineInstaller, + eventConsumer, + loggerFactory.CreateLogger()); var dmbFactory = new DmbFactory( databaseContextFactory, @@ -309,6 +321,7 @@ namespace Tgstation.Server.Host.Components serverPortProvider, eventConsumer, asyncDelayer, + dotnetDumpService, loggerFactory, loggerFactory.CreateLogger(), sessionConfiguration, diff --git a/src/Tgstation.Server.Host/Components/Interop/Bridge/BridgeCommandType.cs b/src/Tgstation.Server.Host/Components/Interop/Bridge/BridgeCommandType.cs index 8c18f74c39..287e9aceda 100644 --- a/src/Tgstation.Server.Host/Components/Interop/Bridge/BridgeCommandType.cs +++ b/src/Tgstation.Server.Host/Components/Interop/Bridge/BridgeCommandType.cs @@ -39,5 +39,10 @@ /// DreamDaemon attempting to send a longer bridge message. /// Chunk, + + /// + /// DreamDaemon requesting a custom event to be triggered. + /// + Event, } } diff --git a/src/Tgstation.Server.Host/Components/Interop/Bridge/BridgeParameters.cs b/src/Tgstation.Server.Host/Components/Interop/Bridge/BridgeParameters.cs index 1240bc81cb..228c467768 100644 --- a/src/Tgstation.Server.Host/Components/Interop/Bridge/BridgeParameters.cs +++ b/src/Tgstation.Server.Host/Components/Interop/Bridge/BridgeParameters.cs @@ -51,6 +51,11 @@ namespace Tgstation.Server.Host.Components.Interop.Bridge /// public ushort? TopicPort { get; set; } + /// + /// The being triggered. + /// + public CustomEventInvocation? EventInvocation { get; set; } + /// /// Initializes a new instance of the class. /// diff --git a/src/Tgstation.Server.Host/Components/Interop/Bridge/BridgeResponse.cs b/src/Tgstation.Server.Host/Components/Interop/Bridge/BridgeResponse.cs index 620241fafb..da3a3bb563 100644 --- a/src/Tgstation.Server.Host/Components/Interop/Bridge/BridgeResponse.cs +++ b/src/Tgstation.Server.Host/Components/Interop/Bridge/BridgeResponse.cs @@ -21,5 +21,10 @@ namespace Tgstation.Server.Host.Components.Interop.Bridge /// The s missing from a chunked request. /// public IReadOnlyCollection? MissingChunks { get; set; } + + /// + /// The triggered event ID for requests. + /// + public string? EventId { get; set; } } } diff --git a/src/Tgstation.Server.Host/Components/Interop/Bridge/CustomEventInvocation.cs b/src/Tgstation.Server.Host/Components/Interop/Bridge/CustomEventInvocation.cs new file mode 100644 index 0000000000..afb6ab66cf --- /dev/null +++ b/src/Tgstation.Server.Host/Components/Interop/Bridge/CustomEventInvocation.cs @@ -0,0 +1,25 @@ +using System.Collections.Generic; + +namespace Tgstation.Server.Host.Components.Interop.Bridge +{ + /// + /// Parameters for invoking a custom event. + /// + public sealed class CustomEventInvocation + { + /// + /// The name of the event being invoked. + /// + public string? EventName { get; set; } + + /// + /// The parameters for the invoked event. + /// + public ICollection? Parameters { get; set; } + + /// + /// If the DMAPI should be notified when the event compeletes. + /// + public bool? NotifyCompletion { 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 75eb3bc9f4..1284940b12 100644 --- a/src/Tgstation.Server.Host/Components/Interop/Topic/EventNotification.cs +++ b/src/Tgstation.Server.Host/Components/Interop/Topic/EventNotification.cs @@ -15,12 +15,12 @@ namespace Tgstation.Server.Host.Components.Interop.Topic /// The triggered. /// /// Nullable to prevent ignoring when serializing. - public EventType? Type { get; } + public EventType Type { get; } /// /// The set of parameters. /// - public IReadOnlyCollection Parameters { get; } + public IReadOnlyCollection? Parameters { get; } /// /// Initializes a new instance of the class. diff --git a/src/Tgstation.Server.Host/Components/Interop/Topic/TopicCommandType.cs b/src/Tgstation.Server.Host/Components/Interop/Topic/TopicCommandType.cs index 286c605d07..c7d21332c3 100644 --- a/src/Tgstation.Server.Host/Components/Interop/Topic/TopicCommandType.cs +++ b/src/Tgstation.Server.Host/Components/Interop/Topic/TopicCommandType.cs @@ -67,5 +67,10 @@ namespace Tgstation.Server.Host.Components.Interop.Topic /// Sending a broadcast message. /// Broadcast, + + /// + /// Notifying about the completion of a custom event. + /// + CompleteEvent, } } diff --git a/src/Tgstation.Server.Host/Components/Interop/Topic/TopicParameters.cs b/src/Tgstation.Server.Host/Components/Interop/Topic/TopicParameters.cs index bdc8405b02..1b2745ab6a 100644 --- a/src/Tgstation.Server.Host/Components/Interop/Topic/TopicParameters.cs +++ b/src/Tgstation.Server.Host/Components/Interop/Topic/TopicParameters.cs @@ -62,6 +62,11 @@ namespace Tgstation.Server.Host.Components.Interop.Topic /// public ChunkData? Chunk { get; } + /// + /// The completed custom event ID. + /// + public string? EventId { get; set; } + /// /// Whether or not the constitute a priority request. /// @@ -74,6 +79,7 @@ namespace Tgstation.Server.Host.Components.Interop.Topic or TopicCommandType.InstanceRenamed or TopicCommandType.ChatChannelsUpdate or TopicCommandType.Broadcast + or TopicCommandType.CompleteEvent or TopicCommandType.ServerRestarted => true, TopicCommandType.ChatCommand or TopicCommandType.HealthCheck @@ -174,6 +180,16 @@ namespace Tgstation.Server.Host.Components.Interop.Topic Chunk = chunk ?? throw new ArgumentNullException(nameof(chunk)); } + /// + /// Initializes a new instance of the class. + /// + /// The containig the value of . + public TopicParameters(Guid eventId) + : this(TopicCommandType.CompleteEvent) + { + EventId = eventId.ToString(); + } + /// /// Initializes a new instance of the class. /// diff --git a/src/Tgstation.Server.Host/Components/Repository/GitHubRemoteFeatures.cs b/src/Tgstation.Server.Host/Components/Repository/GitHubRemoteFeatures.cs index 7a21ebde2b..6109f490bb 100644 --- a/src/Tgstation.Server.Host/Components/Repository/GitHubRemoteFeatures.cs +++ b/src/Tgstation.Server.Host/Components/Repository/GitHubRemoteFeatures.cs @@ -93,7 +93,7 @@ namespace Tgstation.Server.Host.Components.Repository Comment = parameters.Comment, Number = parameters.Number, TargetCommitSha = revisionToUse, - Url = pr?.HtmlUrl ?? errorMessage, + Url = pr?.HtmlUrl ?? $"https://github.com/{RemoteRepositoryOwner}/{RemoteRepositoryName}/pull/{parameters.Number}", }; return testMerge; diff --git a/src/Tgstation.Server.Host/Components/Repository/GitLabRemoteFeatures.cs b/src/Tgstation.Server.Host/Components/Repository/GitLabRemoteFeatures.cs index 9e468b06e6..9cf32f3f7b 100644 --- a/src/Tgstation.Server.Host/Components/Repository/GitLabRemoteFeatures.cs +++ b/src/Tgstation.Server.Host/Components/Repository/GitLabRemoteFeatures.cs @@ -85,7 +85,7 @@ namespace Tgstation.Server.Host.Components.Repository Comment = parameters.Comment, Number = parameters.Number, TargetCommitSha = parameters.TargetCommitSha, - Url = ex.Message, + Url = $"https://gitlab.com/{RemoteRepositoryOwner}/{RemoteRepositoryName}/-/merge_requests/{parameters.Number}", }; } } diff --git a/src/Tgstation.Server.Host/Components/Repository/Repository.cs b/src/Tgstation.Server.Host/Components/Repository/Repository.cs index 2a362ff90c..972fae31cc 100644 --- a/src/Tgstation.Server.Host/Components/Repository/Repository.cs +++ b/src/Tgstation.Server.Host/Components/Repository/Repository.cs @@ -46,6 +46,11 @@ namespace Tgstation.Server.Host.Components.Repository /// public const string RemoteTemporaryBranchName = "___TGSTempBranch"; + /// + /// The value of when not on a reference. + /// + public const string NoReference = "(no branch)"; + /// /// Used when a reference cannot be determined. /// diff --git a/src/Tgstation.Server.Host/Components/Session/ISessionController.cs b/src/Tgstation.Server.Host/Components/Session/ISessionController.cs index ccdaa448c4..bba0dd4152 100644 --- a/src/Tgstation.Server.Host/Components/Session/ISessionController.cs +++ b/src/Tgstation.Server.Host/Components/Session/ISessionController.cs @@ -22,7 +22,7 @@ namespace Tgstation.Server.Host.Components.Session /// /// If the DreamDaemon instance sent a. /// - bool TerminationWasRequested { get; } + bool TerminationWasIntentional { get; } /// /// The DMAPI . @@ -84,6 +84,11 @@ namespace Tgstation.Server.Host.Components.Session /// bool DMApiAvailable { get; } + /// + /// The file extension to use for process dumps created from this session. + /// + string DumpFileExtension { get; } + /// /// Releases the without terminating it. Also calls . /// diff --git a/src/Tgstation.Server.Host/Components/Session/SessionController.cs b/src/Tgstation.Server.Host/Components/Session/SessionController.cs index ad9bdf1728..e32c251eb5 100644 --- a/src/Tgstation.Server.Host/Components/Session/SessionController.cs +++ b/src/Tgstation.Server.Host/Components/Session/SessionController.cs @@ -19,6 +19,7 @@ using Tgstation.Server.Host.Components.Chat; using Tgstation.Server.Host.Components.Chat.Commands; using Tgstation.Server.Host.Components.Deployment; using Tgstation.Server.Host.Components.Engine; +using Tgstation.Server.Host.Components.Events; using Tgstation.Server.Host.Components.Interop; using Tgstation.Server.Host.Components.Interop.Bridge; using Tgstation.Server.Host.Components.Interop.Topic; @@ -63,7 +64,7 @@ namespace Tgstation.Server.Host.Components.Session public Version? DMApiVersion { get; private set; } /// - public bool TerminationWasRequested { get; private set; } + public bool TerminationWasIntentional => terminationWasIntentional || (Lifetime.IsCompleted && Lifetime.Result == 0); /// public Task LaunchResult { get; } @@ -104,6 +105,11 @@ namespace Tgstation.Server.Host.Components.Session /// public bool ProcessingRebootBridgeRequest => rebootBridgeRequestsProcessing > 0; + /// + public string DumpFileExtension => engineLock.UseDotnetDump + ? ".net.dmp" + : ".dmp"; + /// /// The up to date . /// @@ -149,6 +155,16 @@ namespace Tgstation.Server.Host.Components.Session /// readonly IAsyncDelayer asyncDelayer; + /// + /// The for the . + /// + readonly IDotnetDumpService dotnetDumpService; + + /// + /// The for the . + /// + readonly IEventConsumer eventConsumer; + /// /// The that completes when DD makes it's first bridge request. /// @@ -160,9 +176,9 @@ namespace Tgstation.Server.Host.Components.Session readonly Api.Models.Instance metadata; /// - /// A used for the topic send operation made on reattaching. + /// A used for tasks that should not exceed the lifetime of the session. /// - readonly CancellationTokenSource reattachTopicCts; + readonly CancellationTokenSource sessionDurationCts; /// /// for port updates and . @@ -194,6 +210,11 @@ namespace Tgstation.Server.Host.Components.Session /// volatile Task rebootGate; + /// + /// The representing calls to . + /// + volatile Task customEventProcessingTask; + /// /// for shutting down the server if it is taking too long after validation. /// @@ -219,6 +240,11 @@ namespace Tgstation.Server.Host.Components.Session /// bool released; + /// + /// Backing field for overriding . + /// + bool terminationWasIntentional; + /// /// Initializes a new instance of the class. /// @@ -231,7 +257,9 @@ namespace Tgstation.Server.Host.Components.Session /// The value of . /// The value of . /// The for the . - /// The for the . + /// The value of . + /// The value of . + /// The value of . /// The value of . /// The returning a to be run after the ends. /// The optional time to wait before failing the . @@ -248,6 +276,8 @@ namespace Tgstation.Server.Host.Components.Session IChatManager chat, IAssemblyInformationProvider assemblyInformationProvider, IAsyncDelayer asyncDelayer, + IDotnetDumpService dotnetDumpService, + IEventConsumer eventConsumer, ILogger logger, Func postLifetimeCallback, uint? startupTimeout, @@ -267,6 +297,8 @@ namespace Tgstation.Server.Host.Components.Session ArgumentNullException.ThrowIfNull(assemblyInformationProvider); this.asyncDelayer = asyncDelayer ?? throw new ArgumentNullException(nameof(asyncDelayer)); + this.dotnetDumpService = dotnetDumpService ?? throw new ArgumentNullException(nameof(dotnetDumpService)); + this.eventConsumer = eventConsumer ?? throw new ArgumentNullException(nameof(eventConsumer)); apiValidationSession = apiValidate; @@ -279,12 +311,13 @@ namespace Tgstation.Server.Host.Components.Session primeTcs = new TaskCompletionSource(); rebootGate = Task.CompletedTask; + customEventProcessingTask = Task.CompletedTask; // Run this asynchronously because we want to try to avoid any effects sending topics to the server while the initial bridge request is processing // It MAY be the source of a DD crash. See this gist https://gist.github.com/Cyberboss/7776bbeff3a957d76affe0eae95c9f14 // Worth further investigation as to if that sequence of events is a reliable crash vector and opening a BYOND bug if it is initialBridgeRequestTcs = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously); - reattachTopicCts = new CancellationTokenSource(); + sessionDurationCts = new CancellationTokenSource(); TopicSendSemaphore = new FifoSemaphore(); synchronizationLock = new object(); @@ -338,7 +371,7 @@ namespace Tgstation.Server.Host.Components.Session Logger.LogTrace("Disposing..."); - reattachTopicCts.Cancel(); + sessionDurationCts.Cancel(); var cancellationToken = CancellationToken.None; // DCT: None available var semaphoreLockTask = TopicSendSemaphore.Lock(cancellationToken); @@ -363,13 +396,15 @@ namespace Tgstation.Server.Host.Components.Session await regularDmbDisposeTask; chatTrackingContext.Dispose(); - reattachTopicCts.Dispose(); + sessionDurationCts.Dispose(); if (!released) await Lifetime; // finish the async callback (await semaphoreLockTask).Dispose(); TopicSendSemaphore.Dispose(); + + await customEventProcessingTask; } /// @@ -469,7 +504,13 @@ namespace Tgstation.Server.Host.Components.Session cancellationToken); /// - public ValueTask CreateDump(string outputFile, CancellationToken cancellationToken) => process.CreateDump(outputFile, cancellationToken); + public ValueTask CreateDump(string outputFile, bool minidump, CancellationToken cancellationToken) + { + if (engineLock.UseDotnetDump) + return dotnetDumpService.Dump(process, outputFile, minidump, cancellationToken); + + return process.CreateDump(outputFile, minidump, cancellationToken); + } /// /// The for . @@ -523,7 +564,7 @@ namespace Tgstation.Server.Host.Components.Session assemblyInformationProvider.Version, ReattachInformation.RuntimeInformation!.ServerPort), true, - reattachTopicCts.Token); + sessionDurationCts.Token); if (reattachResponse != null) { @@ -622,7 +663,7 @@ namespace Tgstation.Server.Host.Components.Session case BridgeCommandType.Kill: Logger.LogInformation("Bridge requested process termination!"); chatTrackingContext.Active = false; - TerminationWasRequested = true; + terminationWasIntentional = true; process.Terminate(); break; case BridgeCommandType.DeprecatedPortUpdate: @@ -711,6 +752,8 @@ namespace Tgstation.Server.Host.Components.Session break; case BridgeCommandType.Chunk: return await ProcessChunk(ProcessBridgeCommand, BridgeError, parameters.Chunk, cancellationToken); + case BridgeCommandType.Event: + return TriggerCustomEvent(parameters.EventInvocation); case null: return BridgeError("Missing commandType!"); default: @@ -1078,5 +1121,81 @@ namespace Tgstation.Server.Host.Components.Session return fullResponse; } + + /// + /// Trigger a custom event from a given . + /// + /// The . + /// An appropriate . + BridgeResponse TriggerCustomEvent(CustomEventInvocation? invocation) + { + if (invocation == null) + return BridgeError("Missing eventInvocation!"); + + var eventName = invocation.EventName; + if (eventName == null) + return BridgeError("Missing eventName!"); + + var notifyCompletion = invocation.NotifyCompletion; + if (!notifyCompletion.HasValue) + return BridgeError("Missing notifyCompletion!"); + + var eventParams = new List + { + ReattachInformation.Dmb.Directory, + }; + + eventParams.AddRange(invocation + .Parameters? + .Where(param => param != null) + .Cast() + ?? Enumerable.Empty()); + + var eventId = Guid.NewGuid(); + Logger.LogInformation("Triggering custom event \"{eventName}\": {eventId}", eventName, eventId); + + var cancellationToken = sessionDurationCts.Token; + ValueTask? eventTask = eventConsumer.HandleCustomEvent(eventName, eventParams, cancellationToken); + + async Task ProcessEvent() + { + try + { + await eventTask.Value; + + if (notifyCompletion.Value) + await SendCommand( + new TopicParameters(eventId), + cancellationToken); + else + Logger.LogTrace("Finished custom event {eventId}, not sending notification.", eventId); + } + catch (OperationCanceledException ex) + { + Logger.LogDebug(ex, "Custom event invocation {eventId} aborted!", eventId); + } + catch (Exception ex) + { + Logger.LogWarning(ex, "Custom event invocation {eventId} errored!", eventId); + } + } + + if (!eventTask.HasValue) + return BridgeError("Event refused to execute due to matching a TGS event!"); + + lock (sessionDurationCts) + { + var previousEventProcessingTask = customEventProcessingTask; + var eventProcessingTask = ProcessEvent(); + customEventProcessingTask = Task.WhenAll(customEventProcessingTask, eventProcessingTask); + } + + return new BridgeResponse + { + EventId = notifyCompletion.Value + ? eventId.ToString() + : null, + }; + } } } diff --git a/src/Tgstation.Server.Host/Components/Session/SessionControllerFactory.cs b/src/Tgstation.Server.Host/Components/Session/SessionControllerFactory.cs index 70bed0f5ba..1573d4c48a 100644 --- a/src/Tgstation.Server.Host/Components/Session/SessionControllerFactory.cs +++ b/src/Tgstation.Server.Host/Components/Session/SessionControllerFactory.cs @@ -106,6 +106,11 @@ namespace Tgstation.Server.Host.Components.Session /// readonly IAsyncDelayer asyncDelayer; + /// + /// The for the . + /// + readonly IDotnetDumpService dotnetDumpService; + /// /// The for the . /// @@ -178,6 +183,7 @@ namespace Tgstation.Server.Host.Components.Session /// The value of . /// The value of . /// The value of . + /// The value of . /// The value of . /// The value of . /// The value of . @@ -196,6 +202,7 @@ namespace Tgstation.Server.Host.Components.Session IServerPortProvider serverPortProvider, IEventConsumer eventConsumer, IAsyncDelayer asyncDelayer, + IDotnetDumpService dotnetDumpService, ILoggerFactory loggerFactory, ILogger logger, SessionConfiguration sessionConfiguration, @@ -215,6 +222,7 @@ namespace Tgstation.Server.Host.Components.Session this.serverPortProvider = serverPortProvider ?? throw new ArgumentNullException(nameof(serverPortProvider)); this.eventConsumer = eventConsumer ?? throw new ArgumentNullException(nameof(eventConsumer)); this.asyncDelayer = asyncDelayer ?? throw new ArgumentNullException(nameof(asyncDelayer)); + this.dotnetDumpService = dotnetDumpService ?? throw new ArgumentNullException(nameof(dotnetDumpService)); this.loggerFactory = loggerFactory ?? throw new ArgumentNullException(nameof(loggerFactory)); this.logger = logger ?? throw new ArgumentNullException(nameof(logger)); this.sessionConfiguration = sessionConfiguration ?? throw new ArgumentNullException(nameof(sessionConfiguration)); @@ -346,6 +354,8 @@ namespace Tgstation.Server.Host.Components.Session chat, assemblyInformationProvider, asyncDelayer, + dotnetDumpService, + eventConsumer, loggerFactory.CreateLogger(), () => LogDDOutput( process, @@ -436,6 +446,8 @@ namespace Tgstation.Server.Host.Components.Session chat, assemblyInformationProvider, asyncDelayer, + dotnetDumpService, + eventConsumer, loggerFactory.CreateLogger(), () => ValueTask.CompletedTask, null, @@ -490,6 +502,7 @@ namespace Tgstation.Server.Host.Components.Session CancellationToken cancellationToken) { // important to run on all ports to allow port changing + var environment = await engineLock.LoadEnv(logger, false, cancellationToken); var arguments = engineLock.FormatServerArguments( dmbProvider, new Dictionary @@ -503,10 +516,12 @@ namespace Tgstation.Server.Host.Components.Session ? logFilePath : null); - var process = processExecutor.LaunchProcess( + var process = await processExecutor.LaunchProcess( engineLock.ServerExePath, dmbProvider.Directory, arguments, + cancellationToken, + environment, logFilePath, engineLock.HasStandardOutput, true); diff --git a/src/Tgstation.Server.Host/Components/StaticFiles/Configuration.cs b/src/Tgstation.Server.Host/Components/StaticFiles/Configuration.cs index 8261afe918..86ae2c28ca 100644 --- a/src/Tgstation.Server.Host/Components/StaticFiles/Configuration.cs +++ b/src/Tgstation.Server.Host/Components/StaticFiles/Configuration.cs @@ -71,11 +71,11 @@ namespace Tgstation.Server.Host.Components.StaticFiles /// /// Map of s to the filename of the event scripts they trigger. /// - public static IReadOnlyDictionary> EventTypeScriptFileNameMap { get; } = 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())! @@ -600,70 +600,39 @@ namespace Tgstation.Server.Host.Components.StaticFiles public Task StopAsync(CancellationToken cancellationToken) => EnsureDirectories(cancellationToken); /// - public async ValueTask HandleEvent(EventType eventType, IEnumerable parameters, bool deploymentPipeline, CancellationToken cancellationToken) + public ValueTask HandleEvent(EventType eventType, IEnumerable parameters, bool deploymentPipeline, CancellationToken cancellationToken) { ArgumentNullException.ThrowIfNull(parameters); - await EnsureDirectories(cancellationToken); - if (!EventTypeScriptFileNameMap.TryGetValue(eventType, out var scriptNames)) - return; - - // always execute in serial - using (await SemaphoreSlimContext.Lock(semaphore, cancellationToken)) { - var files = await ioManager.GetFilesWithExtension(EventScriptsSubdirectory, platformIdentifier.ScriptFileExtension, false, cancellationToken); - var resolvedScriptsDir = ioManager.ResolvePath(EventScriptsSubdirectory); - - var scriptFiles = files - .Select(x => ioManager.GetFileName(x)) - .Where(x => scriptNames.Any( - scriptName => x.StartsWith(scriptName, StringComparison.Ordinal))) - .ToList(); - - if (scriptFiles.Count == 0) - { - logger.LogTrace("No event scripts starting with \"{scriptName}\" detected", String.Join("\" or \"", scriptNames)); - return; - } - - foreach (var scriptFile in scriptFiles) - { - logger.LogTrace("Running event script {scriptFile}...", scriptFile); - await using (var script = processExecutor.LaunchProcess( - ioManager.ConcatPath(resolvedScriptsDir, scriptFile), - resolvedScriptsDir, - String.Join( - ' ', - parameters.Select(arg => - { - if (arg == null) - return "(NULL)"; - - if (!arg.Contains(' ', StringComparison.Ordinal)) - return arg; - - arg = arg.Replace("\"", "\\\"", StringComparison.Ordinal); - - return $"\"{arg}\""; - })), - readStandardHandles: true, - noShellExecute: true)) - using (cancellationToken.Register(() => script.Terminate())) - { - if (sessionConfiguration.LowPriorityDeploymentProcesses) - script.AdjustPriority(false); - - var exitCode = await script.Lifetime; - cancellationToken.ThrowIfCancellationRequested(); - var scriptOutput = await script.GetCombinedOutput(cancellationToken); - if (exitCode != 0) - throw new JobException($"Script {scriptFile} exited with code {exitCode}:{Environment.NewLine}{scriptOutput}"); - else - logger.LogDebug("Script output:{newLine}{scriptOutput}", Environment.NewLine, scriptOutput); - } - } + logger.LogTrace("No event script for event {event}!", eventType); + return ValueTask.CompletedTask; } + + return ExecuteEventScripts(parameters, deploymentPipeline, cancellationToken, scriptNames); + } + + /// + public ValueTask? HandleCustomEvent(string scriptName, IEnumerable parameters, CancellationToken cancellationToken) + { + var scriptNameIsTgsEventName = EventTypeScriptFileNameMap + .Values + .SelectMany(scriptNames => scriptNames) + .Any(tgsScriptName => tgsScriptName.Equals( + scriptName, + platformIdentifier.IsWindows + ? StringComparison.OrdinalIgnoreCase + : StringComparison.Ordinal)); + if (scriptNameIsTgsEventName) + { + logger.LogWarning("DMAPI attempted to execute TGS reserved event: {eventName}", scriptName); + return null; + } + +#pragma warning disable CA2012 // Use ValueTasks correctly + return ExecuteEventScripts(parameters, false, cancellationToken, scriptName); +#pragma warning restore CA2012 // Use ValueTasks correctly } /// @@ -758,5 +727,75 @@ namespace Tgstation.Server.Host.Components.StaticFiles throw new InvalidOperationException("Attempted to access file outside of configuration manager!"); return resolved; } + + /// + /// Execute a set of given . + /// + /// An of parameters for the . + /// If this event is part of the deployment pipeline. + /// The for the operation. + /// The names of the scripts to execute. + /// A representing the running operation. + async ValueTask ExecuteEventScripts(IEnumerable parameters, bool deploymentPipeline, CancellationToken cancellationToken, params string[] scriptNames) + { + await EnsureDirectories(cancellationToken); + + // always execute in serial + using (await SemaphoreSlimContext.Lock(semaphore, cancellationToken)) + { + var files = await ioManager.GetFilesWithExtension(EventScriptsSubdirectory, platformIdentifier.ScriptFileExtension, false, cancellationToken); + var resolvedScriptsDir = ioManager.ResolvePath(EventScriptsSubdirectory); + + var scriptFiles = files + .Select(x => ioManager.GetFileName(x)) + .Where(x => scriptNames.Any( + scriptName => x.StartsWith(scriptName, StringComparison.Ordinal))) + .ToList(); + + if (scriptFiles.Count == 0) + { + logger.LogTrace("No event scripts starting with \"{scriptName}\" detected", String.Join("\" or \"", scriptNames)); + return; + } + + foreach (var scriptFile in scriptFiles) + { + logger.LogTrace("Running event script {scriptFile}...", scriptFile); + await using (var script = await processExecutor.LaunchProcess( + ioManager.ConcatPath(resolvedScriptsDir, scriptFile), + resolvedScriptsDir, + String.Join( + ' ', + parameters.Select(arg => + { + if (arg == null) + return "(NULL)"; + + if (!arg.Contains(' ', StringComparison.Ordinal)) + return arg; + + arg = arg.Replace("\"", "\\\"", StringComparison.Ordinal); + + return $"\"{arg}\""; + })), + cancellationToken, + readStandardHandles: true, + noShellExecute: true)) + using (cancellationToken.Register(() => script.Terminate())) + { + if (sessionConfiguration.LowPriorityDeploymentProcesses && deploymentPipeline) + script.AdjustPriority(false); + + var exitCode = await script.Lifetime; + cancellationToken.ThrowIfCancellationRequested(); + var scriptOutput = await script.GetCombinedOutput(cancellationToken); + if (exitCode != 0) + throw new JobException($"Script {scriptFile} exited with code {exitCode}:{Environment.NewLine}{scriptOutput}"); + else + logger.LogDebug("Script output:{newLine}{scriptOutput}", Environment.NewLine, scriptOutput); + } + } + } + } } } diff --git a/src/Tgstation.Server.Host/Components/Watchdog/AdvancedWatchdog.cs b/src/Tgstation.Server.Host/Components/Watchdog/AdvancedWatchdog.cs index 884541e9fb..610fe25bba 100644 --- a/src/Tgstation.Server.Host/Components/Watchdog/AdvancedWatchdog.cs +++ b/src/Tgstation.Server.Host/Components/Watchdog/AdvancedWatchdog.cs @@ -127,8 +127,12 @@ namespace Tgstation.Server.Host.Components.Watchdog // If we reach this point, we can guarantee PrepServerForLaunch will be called before starting again. ActiveSwappable = null; - await (pendingSwappable?.DisposeAsync() ?? ValueTask.CompletedTask); - pendingSwappable = null; + + if (pendingSwappable != null) + { + await pendingSwappable.DisposeAsync(); + pendingSwappable = null; + } await DrainDeploymentCleanupTasks(true); } @@ -138,8 +142,6 @@ namespace Tgstation.Server.Host.Components.Watchdog { if (pendingSwappable != null) { - ValueTask RunPrequel() => BeforeApplyDmb(pendingSwappable.CompileJob, cancellationToken); - var needToSwap = !pendingSwappable.Swapped; var controller = Server!; if (needToSwap) @@ -151,7 +153,6 @@ namespace Tgstation.Server.Host.Components.Watchdog // integration test logging will catch this Logger.LogError( "The reboot bridge request completed before the watchdog could suspend the server! This can lead to buggy DreamDaemon behaviour and should be reported! To ensure stability, we will need to hard reboot the server"); - await RunPrequel(); return MonitorAction.Restart; } @@ -165,7 +166,7 @@ namespace Tgstation.Server.Host.Components.Watchdog } } - var updateTask = RunPrequel(); + var updateTask = BeforeApplyDmb(pendingSwappable.CompileJob, cancellationToken); if (needToSwap) await PerformDmbSwap(pendingSwappable, cancellationToken); @@ -183,7 +184,8 @@ namespace Tgstation.Server.Host.Components.Watchdog currentCompileJobId, lingeringDeploymentExpirySeconds); - var timeout = AsyncDelayer.Delay(TimeSpan.FromSeconds(lingeringDeploymentExpirySeconds), cancellationToken); + // DCT: A cancel firing here can result in us leaving a dmbprovider undisposed, localDeploymentCleanupGate will always fire in that case + var timeout = AsyncDelayer.Delay(TimeSpan.FromSeconds(lingeringDeploymentExpirySeconds), CancellationToken.None); var completedTask = await Task.WhenAny( localDeploymentCleanupGate.Task, diff --git a/src/Tgstation.Server.Host/Components/Watchdog/BasicWatchdog.cs b/src/Tgstation.Server.Host/Components/Watchdog/BasicWatchdog.cs index 8494cc9aeb..8dfb10e412 100644 --- a/src/Tgstation.Server.Host/Components/Watchdog/BasicWatchdog.cs +++ b/src/Tgstation.Server.Host/Components/Watchdog/BasicWatchdog.cs @@ -113,12 +113,12 @@ namespace Tgstation.Server.Host.Components.Watchdog switch (reason) { case MonitorActivationReason.ActiveServerCrashed: - var eventType = controller.TerminationWasRequested || (await controller.Lifetime) == 0 + var eventType = controller.TerminationWasIntentional ? EventType.WorldEndProcess : EventType.WatchdogCrash; await HandleEventImpl(eventType, Enumerable.Empty(), false, cancellationToken); - var exitWord = controller.TerminationWasRequested ? "exited" : "crashed"; + var exitWord = controller.TerminationWasIntentional ? "exited" : "crashed"; if (controller.RebootState == Session.RebootState.Shutdown) { // the time for graceful shutdown is now diff --git a/src/Tgstation.Server.Host/Components/Watchdog/PosixWatchdog.cs b/src/Tgstation.Server.Host/Components/Watchdog/PosixWatchdog.cs index 27874178af..b4c7a74229 100644 --- a/src/Tgstation.Server.Host/Components/Watchdog/PosixWatchdog.cs +++ b/src/Tgstation.Server.Host/Components/Watchdog/PosixWatchdog.cs @@ -95,6 +95,6 @@ namespace Tgstation.Server.Host.Components.Watchdog /// protected override SwappableDmbProvider CreateSwappableDmbProvider(IDmbProvider dmbProvider) - => new HardLinkDmbProvider(dmbProvider, GameIOManager, LinkFactory, Logger, generalConfiguration); + => new HardLinkDmbProvider(dmbProvider, GameIOManager, LinkFactory, Logger, generalConfiguration, ActiveLaunchParameters.SecurityLevel!.Value); } } diff --git a/src/Tgstation.Server.Host/Components/Watchdog/WatchdogBase.cs b/src/Tgstation.Server.Host/Components/Watchdog/WatchdogBase.cs index 13f62157f1..211249e0df 100644 --- a/src/Tgstation.Server.Host/Components/Watchdog/WatchdogBase.cs +++ b/src/Tgstation.Server.Host/Components/Watchdog/WatchdogBase.cs @@ -507,6 +507,10 @@ namespace Tgstation.Server.Host.Components.Watchdog HandleChatResponses(result); } + /// + ValueTask? IEventConsumer.HandleCustomEvent(string eventName, IEnumerable parameters, CancellationToken cancellationToken) + => throw new NotSupportedException("Watchdogs do not support custom events!"); + /// /// Starts all s. /// @@ -1225,25 +1229,31 @@ namespace Tgstation.Server.Host.Components.Watchdog async ValueTask CreateDumpNoLock(CancellationToken cancellationToken) { const string DumpDirectory = "ProcessDumps"; - var dumpFileNameTemplate = diagnosticsIOManager.ResolvePath( - diagnosticsIOManager.ConcatPath( - DumpDirectory, - $"DreamDaemon-{DateTimeOffset.UtcNow.ToFileStamp()}.dmp")); - - var dumpFileName = dumpFileNameTemplate; - var iteration = 0; - while (await diagnosticsIOManager.FileExists(dumpFileName, cancellationToken)) - dumpFileName = $"{dumpFileNameTemplate} ({++iteration})"; - - if (iteration == 0) - await diagnosticsIOManager.CreateDirectory(DumpDirectory, cancellationToken); var session = GetActiveController(); if (session?.Lifetime.IsCompleted != false) throw new JobException(ErrorCode.GameServerOffline); + var dumpFileExtension = session.DumpFileExtension; + + var dumpFileNameTemplate = diagnosticsIOManager.ResolvePath( + diagnosticsIOManager.ConcatPath( + DumpDirectory, + $"DreamDaemon-{DateTimeOffset.UtcNow.ToFileStamp()}")); + + var dumpFileName = $"{dumpFileNameTemplate}{dumpFileExtension}"; + var iteration = 0; + while (await diagnosticsIOManager.FileExists(dumpFileName, cancellationToken)) + dumpFileName = $"{dumpFileNameTemplate} ({++iteration}){dumpFileExtension}"; + + if (iteration == 0) + await diagnosticsIOManager.CreateDirectory(DumpDirectory, cancellationToken); + + if (session.Lifetime.IsCompleted) + throw new JobException(ErrorCode.GameServerOffline); + Logger.LogInformation("Dumping session to {dumpFileName}...", dumpFileName); - await session.CreateDump(dumpFileName, cancellationToken); + await session.CreateDump(dumpFileName, ActiveLaunchParameters.Minidumps!.Value, cancellationToken); } } } diff --git a/src/Tgstation.Server.Host/Configuration/GeneralConfiguration.cs b/src/Tgstation.Server.Host/Configuration/GeneralConfiguration.cs index ae084860df..bc9c11e2f3 100644 --- a/src/Tgstation.Server.Host/Configuration/GeneralConfiguration.cs +++ b/src/Tgstation.Server.Host/Configuration/GeneralConfiguration.cs @@ -187,6 +187,9 @@ namespace Tgstation.Server.Host.Configuration else if (this.GetCopyDirectoryTaskThrottle() < 1) throw new InvalidOperationException( $"{nameof(DeploymentDirectoryCopyTasksPerCore)} is too large for the CPU core count of {Environment.ProcessorCount} and overflows a 32-bit signed integer. Please lower the value!"); + + if (ByondTopicTimeout <= 1000) + logger.LogWarning("The timeout for sending BYOND topics is very low ({ms}ms). Topic calls may fail to complete at all!", ByondTopicTimeout); } } } diff --git a/src/Tgstation.Server.Host/Controllers/ApiRootController.cs b/src/Tgstation.Server.Host/Controllers/ApiRootController.cs index fa6b3e0c4b..9bf56a0e4d 100644 --- a/src/Tgstation.Server.Host/Controllers/ApiRootController.cs +++ b/src/Tgstation.Server.Host/Controllers/ApiRootController.cs @@ -357,7 +357,7 @@ namespace Tgstation.Server.Host.Controllers var identExpiry = token.ParseJwt().ValidTo; identExpiry += tokenFactory.ValidationParameters.ClockSkew; identExpiry += TimeSpan.FromSeconds(15); - identityCache.CacheSystemIdentity(user, systemIdentity!, identExpiry); + await identityCache.CacheSystemIdentity(user, systemIdentity!, identExpiry); } Logger.LogDebug("Successfully logged in user {userId}!", user.Id); diff --git a/src/Tgstation.Server.Host/Controllers/ControlPanelController.cs b/src/Tgstation.Server.Host/Controllers/ControlPanelController.cs index 7e450874f5..ec763dd955 100644 --- a/src/Tgstation.Server.Host/Controllers/ControlPanelController.cs +++ b/src/Tgstation.Server.Host/Controllers/ControlPanelController.cs @@ -30,6 +30,11 @@ namespace Tgstation.Server.Host.Controllers /// public const string ControlPanelRoute = "/app"; + /// + /// The route to the control panel channel .json. + /// + public const string ChannelJsonRoute = "channel.json"; + /// /// Header for forcing channel.json to be fetched. /// @@ -70,7 +75,7 @@ namespace Tgstation.Server.Host.Controllers /// Returns the . /// /// A with the . - [Route("channel.json")] + [Route(ChannelJsonRoute)] [HttpGet] public IActionResult GetChannelJson() { diff --git a/src/Tgstation.Server.Host/Controllers/DreamDaemonController.cs b/src/Tgstation.Server.Host/Controllers/DreamDaemonController.cs index cce77f0dc3..17006bfa2e 100644 --- a/src/Tgstation.Server.Host/Controllers/DreamDaemonController.cs +++ b/src/Tgstation.Server.Host/Controllers/DreamDaemonController.cs @@ -149,7 +149,8 @@ namespace Tgstation.Server.Host.Controllers | DreamDaemonRights.SetProfiler | DreamDaemonRights.SetLogOutput | DreamDaemonRights.SetMapThreads - | DreamDaemonRights.BroadcastMessage)] + | DreamDaemonRights.BroadcastMessage + | DreamDaemonRights.SetMinidumps)] [ProducesResponseType(typeof(DreamDaemonResponse), 200)] [ProducesResponseType(typeof(ErrorMessageResponse), 410)] #pragma warning disable CA1502 // TODO: Decomplexify @@ -222,7 +223,8 @@ namespace Tgstation.Server.Host.Controllers || CheckModified(x => x.AdditionalParameters, DreamDaemonRights.SetAdditionalParameters) || CheckModified(x => x.StartProfiler, DreamDaemonRights.SetProfiler) || CheckModified(x => x.LogOutput, DreamDaemonRights.SetLogOutput) - || CheckModified(x => x.MapThreads, DreamDaemonRights.SetMapThreads)) + || CheckModified(x => x.MapThreads, DreamDaemonRights.SetMapThreads) + || CheckModified(x => x.Minidumps, DreamDaemonRights.SetMinidumps)) return Forbid(); return await WithComponentInstance( @@ -379,6 +381,7 @@ namespace Tgstation.Server.Host.Controllers result.StartProfiler = settings.StartProfiler; result.LogOutput = settings.LogOutput; result.MapThreads = settings.MapThreads; + result.Minidumps = settings.Minidumps; } if (revision) diff --git a/src/Tgstation.Server.Host/Controllers/DreamMakerController.cs b/src/Tgstation.Server.Host/Controllers/DreamMakerController.cs index 8b4cfce563..22325779c7 100644 --- a/src/Tgstation.Server.Host/Controllers/DreamMakerController.cs +++ b/src/Tgstation.Server.Host/Controllers/DreamMakerController.cs @@ -171,7 +171,8 @@ namespace Tgstation.Server.Host.Controllers | DreamMakerRights.SetApiValidationPort | DreamMakerRights.SetSecurityLevel | DreamMakerRights.SetApiValidationRequirement - | DreamMakerRights.SetTimeout)] + | DreamMakerRights.SetTimeout + | DreamMakerRights.SetCompilerArguments)] [ProducesResponseType(typeof(DreamMakerResponse), 200)] [ProducesResponseType(204)] [ProducesResponseType(typeof(ErrorMessageResponse), 410)] @@ -196,7 +197,8 @@ namespace Tgstation.Server.Host.Controllers { if (!dreamMakerRights.HasFlag(DreamMakerRights.SetDme)) return Forbid(); - if (model.ProjectName.Length == 0) + + if (model.ProjectName.Length == 0) // can't use isnullorwhitespace because linux memes hostModel.ProjectName = null; else hostModel.ProjectName = model.ProjectName; @@ -230,6 +232,7 @@ namespace Tgstation.Server.Host.Controllers { if (!dreamMakerRights.HasFlag(DreamMakerRights.SetSecurityLevel)) return Forbid(); + hostModel.ApiValidationSecurityLevel = model.ApiValidationSecurityLevel; } @@ -237,6 +240,7 @@ namespace Tgstation.Server.Host.Controllers { if (!dreamMakerRights.HasFlag(DreamMakerRights.SetApiValidationRequirement)) return Forbid(); + hostModel.RequireDMApiValidation = model.RequireDMApiValidation; } @@ -244,9 +248,22 @@ namespace Tgstation.Server.Host.Controllers { if (!dreamMakerRights.HasFlag(DreamMakerRights.SetTimeout)) return Forbid(); + hostModel.Timeout = model.Timeout; } + if (model.CompilerAdditionalArguments != null) + { + if (!dreamMakerRights.HasFlag(DreamMakerRights.SetCompilerArguments)) + return Forbid(); + + var sanitizedArguments = model.CompilerAdditionalArguments.Trim(); + if (sanitizedArguments.Length == 0) + hostModel.CompilerAdditionalArguments = null; + else + hostModel.CompilerAdditionalArguments = sanitizedArguments; + } + await DatabaseContext.Save(cancellationToken); if (!dreamMakerRights.HasFlag(DreamMakerRights.Read)) diff --git a/src/Tgstation.Server.Host/Controllers/InstanceController.cs b/src/Tgstation.Server.Host/Controllers/InstanceController.cs index ded9313475..e74d4fdce1 100644 --- a/src/Tgstation.Server.Host/Controllers/InstanceController.cs +++ b/src/Tgstation.Server.Host/Controllers/InstanceController.cs @@ -732,6 +732,7 @@ namespace Tgstation.Server.Host.Controllers StartProfiler = false, LogOutput = false, MapThreads = 0, + Minidumps = true, }, DreamMakerSettings = new DreamMakerSettings { @@ -739,6 +740,7 @@ namespace Tgstation.Server.Host.Controllers ApiValidationSecurityLevel = DreamDaemonSecurity.Safe, RequireDMApiValidation = true, Timeout = TimeSpan.FromHours(1), + CompilerAdditionalArguments = null, }, Name = initialSettings.Name, Online = false, diff --git a/src/Tgstation.Server.Host/Controllers/RootController.cs b/src/Tgstation.Server.Host/Controllers/RootController.cs index 32a7987857..df363970bb 100644 --- a/src/Tgstation.Server.Host/Controllers/RootController.cs +++ b/src/Tgstation.Server.Host/Controllers/RootController.cs @@ -1,5 +1,6 @@ using System; using System.Collections.Generic; +using System.Linq.Expressions; using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Hosting; @@ -61,6 +62,27 @@ namespace Tgstation.Server.Host.Controllers /// readonly ControlPanelConfiguration controlPanelConfiguration; + /// + /// Gets a giving the and action names for a given . + /// + /// An expression invoking the action on . + /// A containing the controller and action names. + static Tuple GetControlPanelActionLink(Expression> actionExpression) + { + var memberSelectorExpression = (MethodCallExpression)actionExpression.Body; + var method = memberSelectorExpression.Method; + + var controllerName = typeof(ControlPanelController).Name; + + const string ControllerSuffix = nameof(Controller); + if (controllerName.EndsWith(ControllerSuffix, StringComparison.Ordinal)) + controllerName = controllerName.Substring(0, controllerName.Length - ControllerSuffix.Length); + + var actionName = method.Name; + + return Tuple.Create(controllerName, actionName); + } + /// /// Initializes a new instance of the class. /// @@ -136,5 +158,19 @@ namespace Tgstation.Server.Host.Controllers return (IActionResult?)this.TryServeFile(hostEnvironment, logger, $"{logoFileName}.svg") ?? NotFound(); } + + /// + /// Workaround for the webpanel always expecting a trailing slash. + /// + /// A to the . + [HttpGet(ControlPanelController.ChannelJsonRoute)] + public RedirectToActionResult WebpanelChannelRedirect() + { + var actionTuple = GetControlPanelActionLink(controller => controller.GetChannelJson()); + + return RedirectToActionPermanent( + actionTuple.Item2, + actionTuple.Item1); + } } } diff --git a/src/Tgstation.Server.Host/Core/Application.cs b/src/Tgstation.Server.Host/Core/Application.cs index 702ab054da..9569d2510b 100644 --- a/src/Tgstation.Server.Host/Core/Application.cs +++ b/src/Tgstation.Server.Host/Core/Application.cs @@ -356,6 +356,7 @@ namespace Tgstation.Server.Host.Core services.AddSingleton(); services.AddSingleton(); + services.AddHostedService(); // PosixProcessFeatures also needs a IProcessExecutor for gcore services.AddSingleton(x => new Lazy(() => x.GetRequiredService(), true)); @@ -365,11 +366,9 @@ namespace Tgstation.Server.Host.Core } // only global repo manager should be for the OD repo + // god help me if we need more var openDreamRepositoryDirectory = ioManager.ConcatPath( - Environment.GetFolderPath( - Environment.SpecialFolder.LocalApplicationData, - Environment.SpecialFolderOption.DoNotVerify), - assemblyInformationProvider.VersionPrefix, + ioManager.GetPathInLocalDirectory(assemblyInformationProvider), "OpenDreamRepository"); services.AddSingleton( services => services @@ -416,6 +415,7 @@ namespace Tgstation.Server.Host.Core services.AddSingleton(); services.AddSingleton(); services.AddSingleton(); + services.AddSingleton(); // configure misc services services.AddSingleton(); @@ -503,6 +503,10 @@ namespace Tgstation.Server.Host.Core if (generalConfiguration.HostApiDocumentation) { + var siteDocPath = Routes.ApiRoot + $"doc/{SwaggerConfiguration.DocumentName}.json"; + if (!String.IsNullOrWhiteSpace(controlPanelConfiguration.PublicPath)) + siteDocPath = controlPanelConfiguration.PublicPath.TrimEnd('/') + siteDocPath; + applicationBuilder.UseSwagger(options => { options.RouteTemplate = Routes.ApiRoot + "doc/{documentName}.{json|yaml}"; @@ -510,7 +514,7 @@ namespace Tgstation.Server.Host.Core applicationBuilder.UseSwaggerUI(options => { options.RoutePrefix = SwaggerConfiguration.DocumentationSiteRouteExtension; - options.SwaggerEndpoint(Routes.ApiRoot + $"doc/{SwaggerConfiguration.DocumentName}.json", "TGS API"); + options.SwaggerEndpoint(siteDocPath, "TGS API"); }); logger.LogTrace("Swagger API generation enabled"); } @@ -524,6 +528,7 @@ namespace Tgstation.Server.Host.Core RequestPath = ControlPanelController.ControlPanelRoute, EnableDefaultFiles = true, EnableDirectoryBrowsing = false, + RedirectToAppendTrailingSlash = false, }); } else diff --git a/src/Tgstation.Server.Host/Database/DatabaseContext.cs b/src/Tgstation.Server.Host/Database/DatabaseContext.cs index 978a73076b..d4fb8fe084 100644 --- a/src/Tgstation.Server.Host/Database/DatabaseContext.cs +++ b/src/Tgstation.Server.Host/Database/DatabaseContext.cs @@ -375,22 +375,22 @@ namespace Tgstation.Server.Host.Database /// /// Used by unit tests to remind us to setup the correct MSSQL migration downgrades. /// - internal static readonly Type MSLatestMigration = typeof(MSAddTopicPort); + internal static readonly Type MSLatestMigration = typeof(MSAddCompilerAdditionalArguments); /// /// Used by unit tests to remind us to setup the correct MYSQL migration downgrades. /// - internal static readonly Type MYLatestMigration = typeof(MYAddTopicPort); + internal static readonly Type MYLatestMigration = typeof(MYAddCompilerAdditionalArguments); /// /// Used by unit tests to remind us to setup the correct PostgresSQL migration downgrades. /// - internal static readonly Type PGLatestMigration = typeof(PGAddTopicPort); + internal static readonly Type PGLatestMigration = typeof(PGAddCompilerAdditionalArguments); /// /// Used by unit tests to remind us to setup the correct SQLite migration downgrades. /// - internal static readonly Type SLLatestMigration = typeof(SLAddTopicPort); + internal static readonly Type SLLatestMigration = typeof(SLAddCompilerAdditionalArguments); /// #pragma warning disable CA1502 // Cyclomatic complexity @@ -419,6 +419,26 @@ namespace Tgstation.Server.Host.Database string BadDatabaseType() => throw new ArgumentException($"Invalid DatabaseType: {currentDatabaseType}", nameof(currentDatabaseType)); + if (targetVersion < new Version(6, 5, 0)) + targetMigration = currentDatabaseType switch + { + DatabaseType.MySql => nameof(MYAddMinidumpsOption), + DatabaseType.PostgresSql => nameof(PGAddMinidumpsOption), + DatabaseType.SqlServer => nameof(MSAddMinidumpsOption), + DatabaseType.Sqlite => nameof(SLAddMinidumpsOption), + _ => BadDatabaseType(), + }; + + if (targetVersion < new Version(6, 2, 0)) + targetMigration = currentDatabaseType switch + { + DatabaseType.MySql => nameof(MYAddTopicPort), + DatabaseType.PostgresSql => nameof(PGAddTopicPort), + DatabaseType.SqlServer => nameof(MSAddTopicPort), + DatabaseType.Sqlite => nameof(SLAddTopicPort), + _ => BadDatabaseType(), + }; + if (targetVersion < new Version(6, 0, 0)) targetMigration = currentDatabaseType switch { diff --git a/src/Tgstation.Server.Host/Database/Migrations/20240202202038_MSAddMinidumpsOption.Designer.cs b/src/Tgstation.Server.Host/Database/Migrations/20240202202038_MSAddMinidumpsOption.Designer.cs new file mode 100644 index 0000000000..dec415ded3 --- /dev/null +++ b/src/Tgstation.Server.Host/Database/Migrations/20240202202038_MSAddMinidumpsOption.Designer.cs @@ -0,0 +1,1080 @@ +// +using System; + +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Migrations; + +namespace Tgstation.Server.Host.Database.Migrations +{ + [DbContext(typeof(SqlServerDatabaseContext))] + [Migration("20240202202038_MSAddMinidumpsOption")] + partial class MSAddMinidumpsOption + { + /// + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("ProductVersion", "8.0.1") + .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("Minidumps") + .IsRequired() + .HasColumnType("bit"); + + 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/20240202202038_MSAddMinidumpsOption.cs b/src/Tgstation.Server.Host/Database/Migrations/20240202202038_MSAddMinidumpsOption.cs new file mode 100644 index 0000000000..3439dbe5cb --- /dev/null +++ b/src/Tgstation.Server.Host/Database/Migrations/20240202202038_MSAddMinidumpsOption.cs @@ -0,0 +1,37 @@ +using System; + +using Microsoft.EntityFrameworkCore.Migrations; + +using Tgstation.Server.Host.System; + +namespace Tgstation.Server.Host.Database.Migrations +{ + /// + public partial class MSAddMinidumpsOption : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + ArgumentNullException.ThrowIfNull(migrationBuilder); + + // This was originally minidumps on Linux and full dumps on Windows + var defaultValue = !new PlatformIdentifier().IsWindows; + migrationBuilder.AddColumn( + name: "Minidumps", + table: "DreamDaemonSettings", + type: "bit", + nullable: false, + defaultValue: defaultValue); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + ArgumentNullException.ThrowIfNull(migrationBuilder); + + migrationBuilder.DropColumn( + name: "Minidumps", + table: "DreamDaemonSettings"); + } + } +} diff --git a/src/Tgstation.Server.Host/Database/Migrations/20240202202051_MYAddMinidumpsOption.Designer.cs b/src/Tgstation.Server.Host/Database/Migrations/20240202202051_MYAddMinidumpsOption.Designer.cs new file mode 100644 index 0000000000..b579255da3 --- /dev/null +++ b/src/Tgstation.Server.Host/Database/Migrations/20240202202051_MYAddMinidumpsOption.Designer.cs @@ -0,0 +1,1114 @@ +// +using System; + +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Migrations; + +namespace Tgstation.Server.Host.Database.Migrations +{ + [DbContext(typeof(MySqlDatabaseContext))] + [Migration("20240202202051_MYAddMinidumpsOption")] + partial class MYAddMinidumpsOption + { + /// + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("ProductVersion", "8.0.1") + .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("Minidumps") + .IsRequired() + .HasColumnType("tinyint(1)"); + + 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/20240202202051_MYAddMinidumpsOption.cs b/src/Tgstation.Server.Host/Database/Migrations/20240202202051_MYAddMinidumpsOption.cs new file mode 100644 index 0000000000..05693e58f2 --- /dev/null +++ b/src/Tgstation.Server.Host/Database/Migrations/20240202202051_MYAddMinidumpsOption.cs @@ -0,0 +1,37 @@ +using System; + +using Microsoft.EntityFrameworkCore.Migrations; + +using Tgstation.Server.Host.System; + +namespace Tgstation.Server.Host.Database.Migrations +{ + /// + public partial class MYAddMinidumpsOption : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + ArgumentNullException.ThrowIfNull(migrationBuilder); + + // This was originally minidumps on Linux and full dumps on Windows + var defaultValue = !new PlatformIdentifier().IsWindows; + migrationBuilder.AddColumn( + name: "Minidumps", + table: "DreamDaemonSettings", + type: "tinyint(1)", + nullable: false, + defaultValue: defaultValue); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + ArgumentNullException.ThrowIfNull(migrationBuilder); + + migrationBuilder.DropColumn( + name: "Minidumps", + table: "DreamDaemonSettings"); + } + } +} diff --git a/src/Tgstation.Server.Host/Database/Migrations/20240202202106_PGAddMinidumpsOption.Designer.cs b/src/Tgstation.Server.Host/Database/Migrations/20240202202106_PGAddMinidumpsOption.Designer.cs new file mode 100644 index 0000000000..5a19f5dedf --- /dev/null +++ b/src/Tgstation.Server.Host/Database/Migrations/20240202202106_PGAddMinidumpsOption.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(PostgresSqlDatabaseContext))] + [Migration("20240202202106_PGAddMinidumpsOption")] + partial class PGAddMinidumpsOption + { + /// + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("ProductVersion", "8.0.1") + .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("Minidumps") + .IsRequired() + .HasColumnType("boolean"); + + 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/20240202202106_PGAddMinidumpsOption.cs b/src/Tgstation.Server.Host/Database/Migrations/20240202202106_PGAddMinidumpsOption.cs new file mode 100644 index 0000000000..4f73ab4080 --- /dev/null +++ b/src/Tgstation.Server.Host/Database/Migrations/20240202202106_PGAddMinidumpsOption.cs @@ -0,0 +1,37 @@ +using System; + +using Microsoft.EntityFrameworkCore.Migrations; + +using Tgstation.Server.Host.System; + +namespace Tgstation.Server.Host.Database.Migrations +{ + /// + public partial class PGAddMinidumpsOption : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + ArgumentNullException.ThrowIfNull(migrationBuilder); + + // This was originally minidumps on Linux and full dumps on Windows + var defaultValue = !new PlatformIdentifier().IsWindows; + migrationBuilder.AddColumn( + name: "Minidumps", + table: "DreamDaemonSettings", + type: "boolean", + nullable: false, + defaultValue: defaultValue); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + ArgumentNullException.ThrowIfNull(migrationBuilder); + + migrationBuilder.DropColumn( + name: "Minidumps", + table: "DreamDaemonSettings"); + } + } +} diff --git a/src/Tgstation.Server.Host/Database/Migrations/20240202202121_SLAddMinidumpsOption.Designer.cs b/src/Tgstation.Server.Host/Database/Migrations/20240202202121_SLAddMinidumpsOption.Designer.cs new file mode 100644 index 0000000000..db4c7542e2 --- /dev/null +++ b/src/Tgstation.Server.Host/Database/Migrations/20240202202121_SLAddMinidumpsOption.Designer.cs @@ -0,0 +1,1046 @@ +// +using System; + +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Migrations; + +namespace Tgstation.Server.Host.Database.Migrations +{ + [DbContext(typeof(SqliteDatabaseContext))] + [Migration("20240202202121_SLAddMinidumpsOption")] + partial class SLAddMinidumpsOption + { + /// + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder.HasAnnotation("ProductVersion", "8.0.1"); + + 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("Minidumps") + .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/20240202202121_SLAddMinidumpsOption.cs b/src/Tgstation.Server.Host/Database/Migrations/20240202202121_SLAddMinidumpsOption.cs new file mode 100644 index 0000000000..7a4af46540 --- /dev/null +++ b/src/Tgstation.Server.Host/Database/Migrations/20240202202121_SLAddMinidumpsOption.cs @@ -0,0 +1,37 @@ +using System; + +using Microsoft.EntityFrameworkCore.Migrations; + +using Tgstation.Server.Host.System; + +namespace Tgstation.Server.Host.Database.Migrations +{ + /// + public partial class SLAddMinidumpsOption : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + ArgumentNullException.ThrowIfNull(migrationBuilder); + + // This was originally minidumps on Linux and full dumps on Windows + var defaultValue = !new PlatformIdentifier().IsWindows; + migrationBuilder.AddColumn( + name: "Minidumps", + table: "DreamDaemonSettings", + type: "INTEGER", + nullable: false, + defaultValue: defaultValue); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + ArgumentNullException.ThrowIfNull(migrationBuilder); + + migrationBuilder.DropColumn( + name: "Minidumps", + table: "DreamDaemonSettings"); + } + } +} diff --git a/src/Tgstation.Server.Host/Database/Migrations/20240420153929_MYNormalizeVersionUpdates.Designer.cs b/src/Tgstation.Server.Host/Database/Migrations/20240420153929_MYNormalizeVersionUpdates.Designer.cs new file mode 100644 index 0000000000..982497eff8 --- /dev/null +++ b/src/Tgstation.Server.Host/Database/Migrations/20240420153929_MYNormalizeVersionUpdates.Designer.cs @@ -0,0 +1,1150 @@ +// +using System; + +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Migrations; + +namespace Tgstation.Server.Host.Database.Migrations +{ + [DbContext(typeof(MySqlDatabaseContext))] + [Migration("20240420153929_MYNormalizeVersionUpdates")] + partial class MYNormalizeVersionUpdates + { + /// + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("ProductVersion", "8.0.4") + .HasAnnotation("Relational:MaxIdentifierLength", 64); + + MySqlModelBuilderExtensions.AutoIncrementColumns(modelBuilder); + + modelBuilder.Entity("Tgstation.Server.Host.Models.ChatBot", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("Id")); + + 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"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("Id")); + + 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"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("Id")); + + 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"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("Id")); + + 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("Minidumps") + .IsRequired() + .HasColumnType("tinyint(1)"); + + 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"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("Id")); + + 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"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("Id")); + + 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"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("Id")); + + 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"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("Id")); + + 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"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("Id")); + + 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"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("Id")); + + 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"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("Id")); + + 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"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("Id")); + + 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"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(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"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("Id")); + + 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"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("Id")); + + 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"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("Id")); + + 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"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("Id")); + + 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/20240420153929_MYNormalizeVersionUpdates.cs b/src/Tgstation.Server.Host/Database/Migrations/20240420153929_MYNormalizeVersionUpdates.cs new file mode 100644 index 0000000000..81cd88a047 --- /dev/null +++ b/src/Tgstation.Server.Host/Database/Migrations/20240420153929_MYNormalizeVersionUpdates.cs @@ -0,0 +1,175 @@ +using System; + +using Microsoft.EntityFrameworkCore.Metadata; +using Microsoft.EntityFrameworkCore.Migrations; + +namespace Tgstation.Server.Host.Database.Migrations +{ + /// + public partial class MYNormalizeVersionUpdates : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + ArgumentNullException.ThrowIfNull(migrationBuilder); + + migrationBuilder.AlterColumn( + name: "Id", + table: "Users", + type: "bigint", + nullable: false, + oldClrType: typeof(long), + oldType: "bigint") + .Annotation("MySql:ValueGenerationStrategy", MySqlValueGenerationStrategy.IdentityColumn); + + migrationBuilder.AlterColumn( + name: "Id", + table: "TestMerges", + type: "bigint", + nullable: false, + oldClrType: typeof(long), + oldType: "bigint") + .Annotation("MySql:ValueGenerationStrategy", MySqlValueGenerationStrategy.IdentityColumn); + + migrationBuilder.AlterColumn( + name: "Id", + table: "RevisionInformations", + type: "bigint", + nullable: false, + oldClrType: typeof(long), + oldType: "bigint") + .Annotation("MySql:ValueGenerationStrategy", MySqlValueGenerationStrategy.IdentityColumn); + + migrationBuilder.AlterColumn( + name: "Id", + table: "RevInfoTestMerges", + type: "bigint", + nullable: false, + oldClrType: typeof(long), + oldType: "bigint") + .Annotation("MySql:ValueGenerationStrategy", MySqlValueGenerationStrategy.IdentityColumn); + + migrationBuilder.AlterColumn( + name: "Id", + table: "RepositorySettings", + type: "bigint", + nullable: false, + oldClrType: typeof(long), + oldType: "bigint") + .Annotation("MySql:ValueGenerationStrategy", MySqlValueGenerationStrategy.IdentityColumn); + + migrationBuilder.AlterColumn( + name: "Id", + table: "ReattachInformations", + type: "bigint", + nullable: false, + oldClrType: typeof(long), + oldType: "bigint") + .Annotation("MySql:ValueGenerationStrategy", MySqlValueGenerationStrategy.IdentityColumn); + + migrationBuilder.AlterColumn( + name: "Id", + table: "PermissionSets", + type: "bigint", + nullable: false, + oldClrType: typeof(long), + oldType: "bigint") + .Annotation("MySql:ValueGenerationStrategy", MySqlValueGenerationStrategy.IdentityColumn); + + migrationBuilder.AlterColumn( + name: "Id", + table: "OAuthConnections", + type: "bigint", + nullable: false, + oldClrType: typeof(long), + oldType: "bigint") + .Annotation("MySql:ValueGenerationStrategy", MySqlValueGenerationStrategy.IdentityColumn); + + migrationBuilder.AlterColumn( + name: "Id", + table: "Jobs", + type: "bigint", + nullable: false, + oldClrType: typeof(long), + oldType: "bigint") + .Annotation("MySql:ValueGenerationStrategy", MySqlValueGenerationStrategy.IdentityColumn); + + migrationBuilder.AlterColumn( + name: "Id", + table: "Instances", + type: "bigint", + nullable: false, + oldClrType: typeof(long), + oldType: "bigint") + .Annotation("MySql:ValueGenerationStrategy", MySqlValueGenerationStrategy.IdentityColumn); + + migrationBuilder.AlterColumn( + name: "Id", + table: "InstancePermissionSets", + type: "bigint", + nullable: false, + oldClrType: typeof(long), + oldType: "bigint") + .Annotation("MySql:ValueGenerationStrategy", MySqlValueGenerationStrategy.IdentityColumn); + + migrationBuilder.AlterColumn( + name: "Id", + table: "Groups", + type: "bigint", + nullable: false, + oldClrType: typeof(long), + oldType: "bigint") + .Annotation("MySql:ValueGenerationStrategy", MySqlValueGenerationStrategy.IdentityColumn); + + migrationBuilder.AlterColumn( + name: "Id", + table: "DreamMakerSettings", + type: "bigint", + nullable: false, + oldClrType: typeof(long), + oldType: "bigint") + .Annotation("MySql:ValueGenerationStrategy", MySqlValueGenerationStrategy.IdentityColumn); + + migrationBuilder.AlterColumn( + name: "Id", + table: "DreamDaemonSettings", + type: "bigint", + nullable: false, + oldClrType: typeof(long), + oldType: "bigint") + .Annotation("MySql:ValueGenerationStrategy", MySqlValueGenerationStrategy.IdentityColumn); + + migrationBuilder.AlterColumn( + name: "Id", + table: "CompileJobs", + type: "bigint", + nullable: false, + oldClrType: typeof(long), + oldType: "bigint") + .Annotation("MySql:ValueGenerationStrategy", MySqlValueGenerationStrategy.IdentityColumn); + + migrationBuilder.AlterColumn( + name: "Id", + table: "ChatChannels", + type: "bigint", + nullable: false, + oldClrType: typeof(long), + oldType: "bigint") + .Annotation("MySql:ValueGenerationStrategy", MySqlValueGenerationStrategy.IdentityColumn); + + migrationBuilder.AlterColumn( + name: "Id", + table: "ChatBots", + type: "bigint", + nullable: false, + oldClrType: typeof(long), + oldType: "bigint") + .Annotation("MySql:ValueGenerationStrategy", MySqlValueGenerationStrategy.IdentityColumn); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + } + } +} diff --git a/src/Tgstation.Server.Host/Database/Migrations/20240420154501_MSAddCompilerAdditionalArguments.Designer.cs b/src/Tgstation.Server.Host/Database/Migrations/20240420154501_MSAddCompilerAdditionalArguments.Designer.cs new file mode 100644 index 0000000000..904833311b --- /dev/null +++ b/src/Tgstation.Server.Host/Database/Migrations/20240420154501_MSAddCompilerAdditionalArguments.Designer.cs @@ -0,0 +1,1084 @@ +// +using System; + +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Migrations; + +namespace Tgstation.Server.Host.Database.Migrations +{ + [DbContext(typeof(SqlServerDatabaseContext))] + [Migration("20240420154501_MSAddCompilerAdditionalArguments")] + partial class MSAddCompilerAdditionalArguments + { + /// + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("ProductVersion", "8.0.4") + .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("Minidumps") + .IsRequired() + .HasColumnType("bit"); + + 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("CompilerAdditionalArguments") + .HasMaxLength(10000) + .HasColumnType("nvarchar(max)"); + + 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/20240420154501_MSAddCompilerAdditionalArguments.cs b/src/Tgstation.Server.Host/Database/Migrations/20240420154501_MSAddCompilerAdditionalArguments.cs new file mode 100644 index 0000000000..9ac1be9d78 --- /dev/null +++ b/src/Tgstation.Server.Host/Database/Migrations/20240420154501_MSAddCompilerAdditionalArguments.cs @@ -0,0 +1,33 @@ +using System; + +using Microsoft.EntityFrameworkCore.Migrations; + +namespace Tgstation.Server.Host.Database.Migrations +{ + /// + public partial class MSAddCompilerAdditionalArguments : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + ArgumentNullException.ThrowIfNull(migrationBuilder); + + migrationBuilder.AddColumn( + name: "CompilerAdditionalArguments", + table: "DreamMakerSettings", + type: "nvarchar(max)", + maxLength: 10000, + nullable: true); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + ArgumentNullException.ThrowIfNull(migrationBuilder); + + migrationBuilder.DropColumn( + name: "CompilerAdditionalArguments", + table: "DreamMakerSettings"); + } + } +} diff --git a/src/Tgstation.Server.Host/Database/Migrations/20240420154509_MYAddCompilerAdditionalArguments.Designer.cs b/src/Tgstation.Server.Host/Database/Migrations/20240420154509_MYAddCompilerAdditionalArguments.Designer.cs new file mode 100644 index 0000000000..17a818166e --- /dev/null +++ b/src/Tgstation.Server.Host/Database/Migrations/20240420154509_MYAddCompilerAdditionalArguments.Designer.cs @@ -0,0 +1,1154 @@ +// +using System; + +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Migrations; + +namespace Tgstation.Server.Host.Database.Migrations +{ + [DbContext(typeof(MySqlDatabaseContext))] + [Migration("20240420154509_MYAddCompilerAdditionalArguments")] + partial class MYAddCompilerAdditionalArguments + { + /// + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("ProductVersion", "8.0.4") + .HasAnnotation("Relational:MaxIdentifierLength", 64); + + MySqlModelBuilderExtensions.AutoIncrementColumns(modelBuilder); + + modelBuilder.Entity("Tgstation.Server.Host.Models.ChatBot", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("Id")); + + 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"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("Id")); + + 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"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("Id")); + + 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"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("Id")); + + 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("Minidumps") + .IsRequired() + .HasColumnType("tinyint(1)"); + + 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"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("Id")); + + b.Property("ApiValidationPort") + .IsRequired() + .HasColumnType("smallint unsigned"); + + b.Property("ApiValidationSecurityLevel") + .HasColumnType("int"); + + b.Property("CompilerAdditionalArguments") + .HasMaxLength(10000) + .HasColumnType("varchar(10000)"); + + 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"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("Id")); + + 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"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("Id")); + + 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"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("Id")); + + 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"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("Id")); + + 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"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("Id")); + + 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"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("Id")); + + 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"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("Id")); + + 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"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(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"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("Id")); + + 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"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("Id")); + + 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"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("Id")); + + 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"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("Id")); + + 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/20240420154509_MYAddCompilerAdditionalArguments.cs b/src/Tgstation.Server.Host/Database/Migrations/20240420154509_MYAddCompilerAdditionalArguments.cs new file mode 100644 index 0000000000..b5065bfc99 --- /dev/null +++ b/src/Tgstation.Server.Host/Database/Migrations/20240420154509_MYAddCompilerAdditionalArguments.cs @@ -0,0 +1,34 @@ +using System; + +using Microsoft.EntityFrameworkCore.Migrations; + +namespace Tgstation.Server.Host.Database.Migrations +{ + /// + public partial class MYAddCompilerAdditionalArguments : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + ArgumentNullException.ThrowIfNull(migrationBuilder); + + migrationBuilder.AddColumn( + name: "CompilerAdditionalArguments", + table: "DreamMakerSettings", + type: "varchar(10000)", + maxLength: 10000, + nullable: true) + .Annotation("MySql:CharSet", "utf8mb4"); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + ArgumentNullException.ThrowIfNull(migrationBuilder); + + migrationBuilder.DropColumn( + name: "CompilerAdditionalArguments", + table: "DreamMakerSettings"); + } + } +} diff --git a/src/Tgstation.Server.Host/Database/Migrations/20240420154517_PGAddCompilerAdditionalArguments.Designer.cs b/src/Tgstation.Server.Host/Database/Migrations/20240420154517_PGAddCompilerAdditionalArguments.Designer.cs new file mode 100644 index 0000000000..f28e016375 --- /dev/null +++ b/src/Tgstation.Server.Host/Database/Migrations/20240420154517_PGAddCompilerAdditionalArguments.Designer.cs @@ -0,0 +1,1078 @@ +// +using System; + +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Migrations; + +namespace Tgstation.Server.Host.Database.Migrations +{ + [DbContext(typeof(PostgresSqlDatabaseContext))] + [Migration("20240420154517_PGAddCompilerAdditionalArguments")] + partial class PGAddCompilerAdditionalArguments + { + /// + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("ProductVersion", "8.0.4") + .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("Minidumps") + .IsRequired() + .HasColumnType("boolean"); + + 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("CompilerAdditionalArguments") + .HasMaxLength(10000) + .HasColumnType("character varying(10000)"); + + 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/20240420154517_PGAddCompilerAdditionalArguments.cs b/src/Tgstation.Server.Host/Database/Migrations/20240420154517_PGAddCompilerAdditionalArguments.cs new file mode 100644 index 0000000000..6fe29b918b --- /dev/null +++ b/src/Tgstation.Server.Host/Database/Migrations/20240420154517_PGAddCompilerAdditionalArguments.cs @@ -0,0 +1,33 @@ +using System; + +using Microsoft.EntityFrameworkCore.Migrations; + +namespace Tgstation.Server.Host.Database.Migrations +{ + /// + public partial class PGAddCompilerAdditionalArguments : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + ArgumentNullException.ThrowIfNull(migrationBuilder); + + migrationBuilder.AddColumn( + name: "CompilerAdditionalArguments", + table: "DreamMakerSettings", + type: "character varying(10000)", + maxLength: 10000, + nullable: true); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + ArgumentNullException.ThrowIfNull(migrationBuilder); + + migrationBuilder.DropColumn( + name: "CompilerAdditionalArguments", + table: "DreamMakerSettings"); + } + } +} diff --git a/src/Tgstation.Server.Host/Database/Migrations/20240420154525_SLAddCompilerAdditionalArguments.Designer.cs b/src/Tgstation.Server.Host/Database/Migrations/20240420154525_SLAddCompilerAdditionalArguments.Designer.cs new file mode 100644 index 0000000000..51b37d7870 --- /dev/null +++ b/src/Tgstation.Server.Host/Database/Migrations/20240420154525_SLAddCompilerAdditionalArguments.Designer.cs @@ -0,0 +1,1050 @@ +// +using System; + +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Migrations; + +namespace Tgstation.Server.Host.Database.Migrations +{ + [DbContext(typeof(SqliteDatabaseContext))] + [Migration("20240420154525_SLAddCompilerAdditionalArguments")] + partial class SLAddCompilerAdditionalArguments + { + /// + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder.HasAnnotation("ProductVersion", "8.0.4"); + + 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("Minidumps") + .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("CompilerAdditionalArguments") + .HasMaxLength(10000) + .HasColumnType("TEXT"); + + 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/20240420154525_SLAddCompilerAdditionalArguments.cs b/src/Tgstation.Server.Host/Database/Migrations/20240420154525_SLAddCompilerAdditionalArguments.cs new file mode 100644 index 0000000000..5115fed881 --- /dev/null +++ b/src/Tgstation.Server.Host/Database/Migrations/20240420154525_SLAddCompilerAdditionalArguments.cs @@ -0,0 +1,33 @@ +using System; + +using Microsoft.EntityFrameworkCore.Migrations; + +namespace Tgstation.Server.Host.Database.Migrations +{ + /// + public partial class SLAddCompilerAdditionalArguments : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + ArgumentNullException.ThrowIfNull(migrationBuilder); + + migrationBuilder.AddColumn( + name: "CompilerAdditionalArguments", + table: "DreamMakerSettings", + type: "TEXT", + maxLength: 10000, + nullable: true); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + ArgumentNullException.ThrowIfNull(migrationBuilder); + + migrationBuilder.DropColumn( + name: "CompilerAdditionalArguments", + table: "DreamMakerSettings"); + } + } +} diff --git a/src/Tgstation.Server.Host/Database/Migrations/MySqlDatabaseContextModelSnapshot.cs b/src/Tgstation.Server.Host/Database/Migrations/MySqlDatabaseContextModelSnapshot.cs index 1f42bd3856..7022822362 100644 --- a/src/Tgstation.Server.Host/Database/Migrations/MySqlDatabaseContextModelSnapshot.cs +++ b/src/Tgstation.Server.Host/Database/Migrations/MySqlDatabaseContextModelSnapshot.cs @@ -13,15 +13,19 @@ namespace Tgstation.Server.Host.Database.Migrations { #pragma warning disable 612, 618 modelBuilder - .HasAnnotation("ProductVersion", "8.0.0") + .HasAnnotation("ProductVersion", "8.0.4") .HasAnnotation("Relational:MaxIdentifierLength", 64); + MySqlModelBuilderExtensions.AutoIncrementColumns(modelBuilder); + modelBuilder.Entity("Tgstation.Server.Host.Models.ChatBot", b => { b.Property("Id") .ValueGeneratedOnAdd() .HasColumnType("bigint"); + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("Id")); + b.Property("ChannelLimit") .IsRequired() .HasColumnType("smallint unsigned"); @@ -65,6 +69,8 @@ namespace Tgstation.Server.Host.Database.Migrations .ValueGeneratedOnAdd() .HasColumnType("bigint"); + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("Id")); + b.Property("ChatSettingsId") .HasColumnType("bigint"); @@ -116,6 +122,8 @@ namespace Tgstation.Server.Host.Database.Migrations .ValueGeneratedOnAdd() .HasColumnType("bigint"); + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("Id")); + b.Property("DMApiMajorVersion") .HasColumnType("int"); @@ -185,6 +193,8 @@ namespace Tgstation.Server.Host.Database.Migrations .ValueGeneratedOnAdd() .HasColumnType("bigint"); + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("Id")); + b.Property("AdditionalParameters") .IsRequired() .HasMaxLength(10000) @@ -219,6 +229,10 @@ namespace Tgstation.Server.Host.Database.Migrations .IsRequired() .HasColumnType("int unsigned"); + b.Property("Minidumps") + .IsRequired() + .HasColumnType("tinyint(1)"); + b.Property("Port") .IsRequired() .HasColumnType("smallint unsigned"); @@ -255,6 +269,8 @@ namespace Tgstation.Server.Host.Database.Migrations .ValueGeneratedOnAdd() .HasColumnType("bigint"); + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("Id")); + b.Property("ApiValidationPort") .IsRequired() .HasColumnType("smallint unsigned"); @@ -262,6 +278,10 @@ namespace Tgstation.Server.Host.Database.Migrations b.Property("ApiValidationSecurityLevel") .HasColumnType("int"); + b.Property("CompilerAdditionalArguments") + .HasMaxLength(10000) + .HasColumnType("varchar(10000)"); + b.Property("InstanceId") .HasColumnType("bigint"); @@ -293,6 +313,8 @@ namespace Tgstation.Server.Host.Database.Migrations .ValueGeneratedOnAdd() .HasColumnType("bigint"); + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("Id")); + b.Property("AutoUpdateInterval") .IsRequired() .HasColumnType("int unsigned"); @@ -340,6 +362,8 @@ namespace Tgstation.Server.Host.Database.Migrations .ValueGeneratedOnAdd() .HasColumnType("bigint"); + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("Id")); + b.Property("ChatBotRights") .HasColumnType("bigint unsigned"); @@ -383,6 +407,8 @@ namespace Tgstation.Server.Host.Database.Migrations .ValueGeneratedOnAdd() .HasColumnType("bigint"); + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("Id")); + b.Property("CancelRight") .HasColumnType("bigint unsigned"); @@ -443,6 +469,8 @@ namespace Tgstation.Server.Host.Database.Migrations .ValueGeneratedOnAdd() .HasColumnType("bigint"); + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("Id")); + b.Property("ExternalUserId") .IsRequired() .HasMaxLength(100) @@ -472,6 +500,8 @@ namespace Tgstation.Server.Host.Database.Migrations .ValueGeneratedOnAdd() .HasColumnType("bigint"); + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("Id")); + b.Property("AdministrationRights") .HasColumnType("bigint unsigned"); @@ -501,6 +531,8 @@ namespace Tgstation.Server.Host.Database.Migrations .ValueGeneratedOnAdd() .HasColumnType("bigint"); + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("Id")); + b.Property("AccessIdentifier") .IsRequired() .HasColumnType("longtext"); @@ -546,6 +578,8 @@ namespace Tgstation.Server.Host.Database.Migrations .ValueGeneratedOnAdd() .HasColumnType("bigint"); + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("Id")); + b.Property("AccessToken") .HasMaxLength(10000) .HasColumnType("longtext"); @@ -617,6 +651,8 @@ namespace Tgstation.Server.Host.Database.Migrations .ValueGeneratedOnAdd() .HasColumnType("bigint"); + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("Id")); + b.Property("RevisionInformationId") .HasColumnType("bigint"); @@ -638,6 +674,8 @@ namespace Tgstation.Server.Host.Database.Migrations .ValueGeneratedOnAdd() .HasColumnType("bigint"); + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("Id")); + b.Property("CommitSha") .IsRequired() .HasMaxLength(40) @@ -672,6 +710,8 @@ namespace Tgstation.Server.Host.Database.Migrations .ValueGeneratedOnAdd() .HasColumnType("bigint"); + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("Id")); + b.Property("Author") .IsRequired() .HasColumnType("longtext"); @@ -738,6 +778,8 @@ namespace Tgstation.Server.Host.Database.Migrations .ValueGeneratedOnAdd() .HasColumnType("bigint"); + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("Id")); + b.Property("CanonicalName") .IsRequired() .HasMaxLength(100) @@ -801,6 +843,8 @@ namespace Tgstation.Server.Host.Database.Migrations .ValueGeneratedOnAdd() .HasColumnType("bigint"); + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("Id")); + b.Property("Name") .IsRequired() .HasMaxLength(100) diff --git a/src/Tgstation.Server.Host/Database/Migrations/PostgresSqlDatabaseContextModelSnapshot.cs b/src/Tgstation.Server.Host/Database/Migrations/PostgresSqlDatabaseContextModelSnapshot.cs index 631cf53bc9..2202d8c234 100644 --- a/src/Tgstation.Server.Host/Database/Migrations/PostgresSqlDatabaseContextModelSnapshot.cs +++ b/src/Tgstation.Server.Host/Database/Migrations/PostgresSqlDatabaseContextModelSnapshot.cs @@ -13,7 +13,7 @@ namespace Tgstation.Server.Host.Database.Migrations { #pragma warning disable 612, 618 modelBuilder - .HasAnnotation("ProductVersion", "8.0.0") + .HasAnnotation("ProductVersion", "8.0.4") .HasAnnotation("Relational:MaxIdentifierLength", 63); NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder); @@ -209,6 +209,10 @@ namespace Tgstation.Server.Host.Database.Migrations b.Property("MapThreads") .HasColumnType("bigint"); + b.Property("Minidumps") + .IsRequired() + .HasColumnType("boolean"); + b.Property("Port") .HasColumnType("integer"); @@ -250,6 +254,10 @@ namespace Tgstation.Server.Host.Database.Migrations b.Property("ApiValidationSecurityLevel") .HasColumnType("integer"); + b.Property("CompilerAdditionalArguments") + .HasMaxLength(10000) + .HasColumnType("character varying(10000)"); + b.Property("InstanceId") .HasColumnType("bigint"); diff --git a/src/Tgstation.Server.Host/Database/Migrations/SqlServerDatabaseContextModelSnapshot.cs b/src/Tgstation.Server.Host/Database/Migrations/SqlServerDatabaseContextModelSnapshot.cs index 1b1bb5276c..29cf386275 100644 --- a/src/Tgstation.Server.Host/Database/Migrations/SqlServerDatabaseContextModelSnapshot.cs +++ b/src/Tgstation.Server.Host/Database/Migrations/SqlServerDatabaseContextModelSnapshot.cs @@ -13,7 +13,7 @@ namespace Tgstation.Server.Host.Database.Migrations { #pragma warning disable 612, 618 modelBuilder - .HasAnnotation("ProductVersion", "8.0.0") + .HasAnnotation("ProductVersion", "8.0.4") .HasAnnotation("Relational:MaxIdentifierLength", 128); SqlServerModelBuilderExtensions.UseIdentityColumns(modelBuilder); @@ -211,6 +211,10 @@ namespace Tgstation.Server.Host.Database.Migrations b.Property("MapThreads") .HasColumnType("bigint"); + b.Property("Minidumps") + .IsRequired() + .HasColumnType("bit"); + b.Property("Port") .HasColumnType("int"); @@ -252,6 +256,10 @@ namespace Tgstation.Server.Host.Database.Migrations b.Property("ApiValidationSecurityLevel") .HasColumnType("int"); + b.Property("CompilerAdditionalArguments") + .HasMaxLength(10000) + .HasColumnType("nvarchar(max)"); + b.Property("InstanceId") .HasColumnType("bigint"); diff --git a/src/Tgstation.Server.Host/Database/Migrations/SqliteDatabaseContextModelSnapshot.cs b/src/Tgstation.Server.Host/Database/Migrations/SqliteDatabaseContextModelSnapshot.cs index 29d7469c65..3af9f50ca5 100644 --- a/src/Tgstation.Server.Host/Database/Migrations/SqliteDatabaseContextModelSnapshot.cs +++ b/src/Tgstation.Server.Host/Database/Migrations/SqliteDatabaseContextModelSnapshot.cs @@ -12,7 +12,7 @@ namespace Tgstation.Server.Host.Database.Migrations protected override void BuildModel(ModelBuilder modelBuilder) { #pragma warning disable 612, 618 - modelBuilder.HasAnnotation("ProductVersion", "8.0.0"); + modelBuilder.HasAnnotation("ProductVersion", "8.0.4"); modelBuilder.Entity("Tgstation.Server.Host.Models.ChatBot", b => { @@ -201,6 +201,10 @@ namespace Tgstation.Server.Host.Database.Migrations .IsRequired() .HasColumnType("INTEGER"); + b.Property("Minidumps") + .IsRequired() + .HasColumnType("INTEGER"); + b.Property("Port") .IsRequired() .HasColumnType("INTEGER"); @@ -244,6 +248,10 @@ namespace Tgstation.Server.Host.Database.Migrations b.Property("ApiValidationSecurityLevel") .HasColumnType("INTEGER"); + b.Property("CompilerAdditionalArguments") + .HasMaxLength(10000) + .HasColumnType("TEXT"); + b.Property("InstanceId") .HasColumnType("INTEGER"); diff --git a/src/Tgstation.Server.Host/Extensions/IOManagerExtensions.cs b/src/Tgstation.Server.Host/Extensions/IOManagerExtensions.cs new file mode 100644 index 0000000000..1992eeb435 --- /dev/null +++ b/src/Tgstation.Server.Host/Extensions/IOManagerExtensions.cs @@ -0,0 +1,31 @@ +using System; + +using Tgstation.Server.Host.IO; +using Tgstation.Server.Host.System; + +namespace Tgstation.Server.Host.Extensions +{ + /// + /// Extension methods for . + /// + static class IOManagerExtensions + { + /// + /// Gets the local application data folder used by TGS. + /// + /// The to use. + /// The to use. + /// The path to the local application data directory used by TGS. + public static string GetPathInLocalDirectory(this IIOManager ioManager, IAssemblyInformationProvider assemblyInformationProvider) + { + ArgumentNullException.ThrowIfNull(ioManager); + ArgumentNullException.ThrowIfNull(assemblyInformationProvider); + + return ioManager.ConcatPath( + Environment.GetFolderPath( + Environment.SpecialFolder.LocalApplicationData, // we use local application data here instead of comman application data because we store stuff here we don't want other users interfering with + Environment.SpecialFolderOption.DoNotVerify), + assemblyInformationProvider.VersionPrefix); + } + } +} diff --git a/src/Tgstation.Server.Host/Extensions/WebHostBuilderExtensions.cs b/src/Tgstation.Server.Host/Extensions/WebHostBuilderExtensions.cs index e6815cd7a0..9c210856a1 100644 --- a/src/Tgstation.Server.Host/Extensions/WebHostBuilderExtensions.cs +++ b/src/Tgstation.Server.Host/Extensions/WebHostBuilderExtensions.cs @@ -53,7 +53,7 @@ namespace Tgstation.Server.Host.Extensions /// Configures a given . /// /// The to configure. - private static void ConfigureApplication(IApplicationBuilder applicationBuilder) + static void ConfigureApplication(IApplicationBuilder applicationBuilder) => applicationBuilder .ApplicationServices .GetRequiredService() diff --git a/src/Tgstation.Server.Host/IO/DefaultIOManager.cs b/src/Tgstation.Server.Host/IO/DefaultIOManager.cs index cf7b616587..0ce1d50f45 100644 --- a/src/Tgstation.Server.Host/IO/DefaultIOManager.cs +++ b/src/Tgstation.Server.Host/IO/DefaultIOManager.cs @@ -221,14 +221,7 @@ namespace Tgstation.Server.Host.IO /// public async ValueTask ReadAllBytes(string path, CancellationToken cancellationToken) { - path = ResolvePath(path); - await using var file = new FileStream( - path, - FileMode.Open, - FileAccess.Read, - FileShare.ReadWrite | FileShare.Delete, - DefaultBufferSize, - FileOptions.Asynchronous | FileOptions.SequentialScan); + await using var file = CreateAsyncSequentialReadStream(path); byte[] buf; buf = new byte[file.Length]; await file.ReadAsync(buf, cancellationToken); @@ -261,6 +254,19 @@ namespace Tgstation.Server.Host.IO FileOptions.Asynchronous | FileOptions.SequentialScan); } + /// + public FileStream CreateAsyncSequentialReadStream(string path) + { + path = ResolvePath(path); + return new FileStream( + path, + FileMode.Open, + FileAccess.Read, + FileShare.ReadWrite | FileShare.Delete, + DefaultBufferSize, + FileOptions.Asynchronous | FileOptions.SequentialScan); + } + /// public Task> GetDirectories(string path, CancellationToken cancellationToken) => Task.Factory.StartNew( () => diff --git a/src/Tgstation.Server.Host/IO/IIOManager.cs b/src/Tgstation.Server.Host/IO/IIOManager.cs index d88ec3ec17..e14f15cf29 100644 --- a/src/Tgstation.Server.Host/IO/IIOManager.cs +++ b/src/Tgstation.Server.Host/IO/IIOManager.cs @@ -85,6 +85,7 @@ namespace Tgstation.Server.Host.IO /// The path of the file to read. /// A for the operation. /// A that results in the contents of a file at . + /// This function will fail to read files from the /proc filesystem on Linux. ValueTask ReadAllBytes(string path, CancellationToken cancellationToken); /// @@ -110,6 +111,13 @@ namespace Tgstation.Server.Host.IO /// The open . FileStream CreateAsyncSequentialWriteStream(string path); + /// + /// Creates an asynchronous for sequential reading. + /// + /// The path of the file to write, will be truncated. + /// The open . + FileStream CreateAsyncSequentialReadStream(string path); + /// /// Writes some to a file at overwriting previous content. /// diff --git a/src/Tgstation.Server.Host/Models/DreamMakerSettings.cs b/src/Tgstation.Server.Host/Models/DreamMakerSettings.cs index 72f5a99f29..33a93e49bd 100644 --- a/src/Tgstation.Server.Host/Models/DreamMakerSettings.cs +++ b/src/Tgstation.Server.Host/Models/DreamMakerSettings.cs @@ -31,6 +31,7 @@ namespace Tgstation.Server.Host.Models ApiValidationSecurityLevel = ApiValidationSecurityLevel, RequireDMApiValidation = RequireDMApiValidation, Timeout = Timeout, + CompilerAdditionalArguments = CompilerAdditionalArguments, }; } } diff --git a/src/Tgstation.Server.Host/Security/IIdentityCache.cs b/src/Tgstation.Server.Host/Security/IIdentityCache.cs index 75010f2c64..4a74ed15ac 100644 --- a/src/Tgstation.Server.Host/Security/IIdentityCache.cs +++ b/src/Tgstation.Server.Host/Security/IIdentityCache.cs @@ -1,4 +1,5 @@ using System; +using System.Threading.Tasks; using Tgstation.Server.Host.Models; @@ -15,7 +16,8 @@ namespace Tgstation.Server.Host.Security /// The the belongs to. /// The to cache. /// When the should expire. - void CacheSystemIdentity(User user, ISystemIdentity systemIdentity, DateTimeOffset expiry); + /// A representing the running operation. + ValueTask CacheSystemIdentity(User user, ISystemIdentity systemIdentity, DateTimeOffset expiry); /// /// Attempt to load a cached . diff --git a/src/Tgstation.Server.Host/Security/IdentityCache.cs b/src/Tgstation.Server.Host/Security/IdentityCache.cs index c42ffab3f6..1b40269101 100644 --- a/src/Tgstation.Server.Host/Security/IdentityCache.cs +++ b/src/Tgstation.Server.Host/Security/IdentityCache.cs @@ -1,16 +1,18 @@ using System; using System.Collections.Generic; using System.Linq; +using System.Threading.Tasks; using Microsoft.Extensions.Logging; +using Tgstation.Server.Common.Extensions; using Tgstation.Server.Host.Models; using Tgstation.Server.Host.Utils; namespace Tgstation.Server.Host.Security { /// - sealed class IdentityCache : IIdentityCache, IDisposable + sealed class IdentityCache : IIdentityCache, IAsyncDisposable { /// /// The for the . @@ -41,15 +43,14 @@ namespace Tgstation.Server.Host.Security } /// - public void Dispose() + public ValueTask DisposeAsync() { logger.LogTrace("Disposing..."); - foreach (var cachedIdentity in cachedIdentities.Select(x => x.Value).ToList()) - cachedIdentity.Dispose(); + return ValueTaskExtensions.WhenAll(cachedIdentities.Select(x => x.Value.DisposeAsync())); } /// - public void CacheSystemIdentity(User user, ISystemIdentity systemIdentity, DateTimeOffset expiry) + public async ValueTask CacheSystemIdentity(User user, ISystemIdentity systemIdentity, DateTimeOffset expiry) { ArgumentNullException.ThrowIfNull(user); ArgumentNullException.ThrowIfNull(systemIdentity); @@ -57,27 +58,35 @@ namespace Tgstation.Server.Host.Security var uid = user.Require(x => x.Id); var sysId = systemIdentity.Uid; - lock (cachedIdentities) + ValueTask oldIdentityDisposal = ValueTask.CompletedTask; + try { - logger.LogDebug("Caching system identity {sysId} of user {uid}", sysId, uid); - - if (cachedIdentities.TryGetValue(uid, out var identCache)) + lock (cachedIdentities) { - logger.LogTrace("Expiring previously cached identity..."); - identCache.Dispose(); // also clears it out - } + logger.LogDebug("Caching system identity {sysId} of user {uid}", sysId, uid); - identCache = new IdentityCacheObject( - systemIdentity.Clone(), - asyncDelayer, - () => + if (cachedIdentities.TryGetValue(uid, out var identCache)) { - logger.LogDebug("Expiring system identity cache for user {uid}", uid); - lock (cachedIdentities) - cachedIdentities.Remove(uid); - }, - expiry); - cachedIdentities.Add(uid, identCache); + logger.LogTrace("Expiring previously cached identity..."); + oldIdentityDisposal = identCache.DisposeAsync(); // also clears it out + } + + identCache = new IdentityCacheObject( + systemIdentity.Clone(), + asyncDelayer, + () => + { + logger.LogDebug("Expiring system identity cache for user {uid}", uid); + lock (cachedIdentities) + cachedIdentities.Remove(uid); + }, + expiry); + cachedIdentities.Add(uid, identCache); + } + } + finally + { + await oldIdentityDisposal; } } diff --git a/src/Tgstation.Server.Host/Security/IdentityCacheObject.cs b/src/Tgstation.Server.Host/Security/IdentityCacheObject.cs index 7cc10664e5..af0e57dbd1 100644 --- a/src/Tgstation.Server.Host/Security/IdentityCacheObject.cs +++ b/src/Tgstation.Server.Host/Security/IdentityCacheObject.cs @@ -9,7 +9,7 @@ namespace Tgstation.Server.Host.Security /// /// For keeping a specific alive for a period of time. /// - sealed class IdentityCacheObject : IDisposable + sealed class IdentityCacheObject : IAsyncDisposable { /// /// The the manages. @@ -53,6 +53,9 @@ namespace Tgstation.Server.Host.Security { await asyncDelayer.Delay(expiry - now, cancellationToken); } + catch (OperationCanceledException) + { + } finally { onExpiry(); @@ -63,18 +66,11 @@ namespace Tgstation.Server.Host.Security } /// - public void Dispose() + public async ValueTask DisposeAsync() { cancellationTokenSource.Cancel(); - try - { - task.GetAwaiter().GetResult(); - } - catch (OperationCanceledException) - { - } - cancellationTokenSource.Dispose(); + await task; } } } diff --git a/src/Tgstation.Server.Host/Security/WindowsSystemIdentity.cs b/src/Tgstation.Server.Host/Security/WindowsSystemIdentity.cs index e15b51557d..136c3c24e7 100644 --- a/src/Tgstation.Server.Host/Security/WindowsSystemIdentity.cs +++ b/src/Tgstation.Server.Host/Security/WindowsSystemIdentity.cs @@ -47,7 +47,7 @@ namespace Tgstation.Server.Host.Security { this.identity = identity ?? throw new ArgumentNullException(nameof(identity)); if (identity.IsAnonymous) - throw new InvalidOperationException($"Cannot use anonymous {nameof(WindowsIdentity)} as a {nameof(WindowsSystemIdentity)}!"); + throw new ArgumentException($"Cannot use anonymous {nameof(WindowsIdentity)} as a {nameof(WindowsSystemIdentity)}!", nameof(identity)); canCreateSymlinks = new WindowsPrincipal(identity).IsInRole(WindowsBuiltInRole.Administrator); } @@ -86,7 +86,7 @@ namespace Tgstation.Server.Host.Security } // can't clone a UP, shouldn't be trying to anyway, cloning is for impersonation - throw new InvalidOperationException("Cannot clone a UserPrincipal based WindowsSystemIdentity!"); + throw new NotSupportedException("Cannot clone a UserPrincipal based WindowsSystemIdentity!"); } /// @@ -95,7 +95,7 @@ namespace Tgstation.Server.Host.Security { ArgumentNullException.ThrowIfNull(action); if (identity == null) - throw new InvalidOperationException("Impersonate using a UserPrincipal based WindowsSystemIdentity!"); + throw new NotSupportedException("Impersonate using a UserPrincipal based WindowsSystemIdentity!"); WindowsIdentity.RunImpersonated(identity.AccessToken, action); }, cancellationToken, diff --git a/src/Tgstation.Server.Host/Swarm/ISwarmService.cs b/src/Tgstation.Server.Host/Swarm/ISwarmService.cs index 8e827eb131..a07a8478d4 100644 --- a/src/Tgstation.Server.Host/Swarm/ISwarmService.cs +++ b/src/Tgstation.Server.Host/Swarm/ISwarmService.cs @@ -38,6 +38,6 @@ namespace Tgstation.Server.Host.Swarm /// 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(); + List? GetSwarmServers(); } } diff --git a/src/Tgstation.Server.Host/Swarm/SwarmService.cs b/src/Tgstation.Server.Host/Swarm/SwarmService.cs index 0ff0131c72..3e9ac7d151 100644 --- a/src/Tgstation.Server.Host/Swarm/SwarmService.cs +++ b/src/Tgstation.Server.Host/Swarm/SwarmService.cs @@ -349,7 +349,7 @@ namespace Tgstation.Server.Host.Swarm } /// - public ICollection? GetSwarmServers() + public List? GetSwarmServers() { if (!SwarmMode) return null; diff --git a/src/Tgstation.Server.Host/System/DotnetDumpService.cs b/src/Tgstation.Server.Host/System/DotnetDumpService.cs new file mode 100644 index 0000000000..de24e5387a --- /dev/null +++ b/src/Tgstation.Server.Host/System/DotnetDumpService.cs @@ -0,0 +1,64 @@ +using System; +using System.Threading; +using System.Threading.Tasks; + +using Microsoft.Diagnostics.NETCore.Client; +using Microsoft.Extensions.Logging; + +using Tgstation.Server.Api.Models; +using Tgstation.Server.Host.Jobs; + +namespace Tgstation.Server.Host.System +{ + /// + sealed class DotnetDumpService : IDotnetDumpService + { + /// + /// The for the . + /// + readonly ILogger logger; + + /// + /// Initializes a new instance of the class. + /// + /// The value of . + public DotnetDumpService( + ILogger logger) + { + this.logger = logger ?? throw new ArgumentNullException(nameof(logger)); + } + + /// + public async ValueTask Dump(IProcess process, string outputFile, bool minidump, CancellationToken cancellationToken) + { + // need to use an extra timeout here because if the process is truly deadlocked. A cooperative dump will hang forever + using var cts = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken); + + const int TimeoutMinutes = 5; + cts.CancelAfter(TimeSpan.FromMinutes(TimeoutMinutes)); + cts.Token.Register(() => + { + if (!cancellationToken.IsCancellationRequested) + logger.LogError("dotnet-dump timed out after {minutes} minutes!", TimeoutMinutes); + }); + + var pid = process.Id; + logger.LogDebug("dotnet-dump requested for PID {pid}...", pid); + var client = new DiagnosticsClient(pid); + try + { + await client.WriteDumpAsync( + minidump + ? DumpType.Normal + : DumpType.Full, + outputFile, + false, + cts.Token); + } + catch (Exception ex) + { + throw new JobException(ErrorCode.GCoreFailure, ex); + } + } + } +} diff --git a/src/Tgstation.Server.Host/System/DotnetHelper.cs b/src/Tgstation.Server.Host/System/DotnetHelper.cs new file mode 100644 index 0000000000..33adbbeb7e --- /dev/null +++ b/src/Tgstation.Server.Host/System/DotnetHelper.cs @@ -0,0 +1,45 @@ +using System; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; + +using Tgstation.Server.Host.IO; + +namespace Tgstation.Server.Host.System +{ + /// + /// Helper methods for working with the dotnet executable. + /// + static class DotnetHelper + { + /// + /// Locate a dotnet executable to use. + /// + /// The to use. + /// The to use. + /// The for the operation. + /// A resulting in a dotnet executable path to use on success, otherwise. + public static async ValueTask GetDotnetPath(IPlatformIdentifier platformIdentifier, IIOManager ioManager, CancellationToken cancellationToken) + { + ArgumentNullException.ThrowIfNull(platformIdentifier); + ArgumentNullException.ThrowIfNull(ioManager); + + var dotnetPaths = Common.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) + return null; + + var dotnetPath = dotnetPaths[selectedPathIndex]; + + return dotnetPath; + } + } +} diff --git a/src/Tgstation.Server.Host/System/IDotnetDumpService.cs b/src/Tgstation.Server.Host/System/IDotnetDumpService.cs new file mode 100644 index 0000000000..43aea37806 --- /dev/null +++ b/src/Tgstation.Server.Host/System/IDotnetDumpService.cs @@ -0,0 +1,21 @@ +using System.Threading; +using System.Threading.Tasks; + +namespace Tgstation.Server.Host.System +{ + /// + /// Service for managing the dotnet-dump installation. + /// + public interface IDotnetDumpService + { + /// + /// Attempt to dump a given . + /// + /// The to dump. + /// The path to the output dump file. + /// If a minidump should be taken as opposed to a full dump. + /// The for the operation. + /// A representing the running operation. + ValueTask Dump(IProcess process, string outputFile, bool minidump, CancellationToken cancellationToken); + } +} diff --git a/src/Tgstation.Server.Host/System/IProcessBase.cs b/src/Tgstation.Server.Host/System/IProcessBase.cs index d7a20f43b7..92f6bdb159 100644 --- a/src/Tgstation.Server.Host/System/IProcessBase.cs +++ b/src/Tgstation.Server.Host/System/IProcessBase.cs @@ -33,8 +33,9 @@ namespace Tgstation.Server.Host.System /// Create a dump file of the process. /// /// The full path to the output file. + /// If a minidump should be taken as opposed to a full dump. /// The for the operation. /// A representing the running operation. - ValueTask CreateDump(string outputFile, CancellationToken cancellationToken); + ValueTask CreateDump(string outputFile, bool minidump, CancellationToken cancellationToken); } } diff --git a/src/Tgstation.Server.Host/System/IProcessExecutor.cs b/src/Tgstation.Server.Host/System/IProcessExecutor.cs index aae807eec2..0ce0c98a8c 100644 --- a/src/Tgstation.Server.Host/System/IProcessExecutor.cs +++ b/src/Tgstation.Server.Host/System/IProcessExecutor.cs @@ -1,4 +1,8 @@ -namespace Tgstation.Server.Host.System +using System.Collections.Generic; +using System.Threading; +using System.Threading.Tasks; + +namespace Tgstation.Server.Host.System { /// /// For launching '. @@ -11,14 +15,18 @@ /// The full path to the executable file. /// The working directory for the . /// The arguments for the . + /// The for the operation. + /// A of environment variables to set. /// File to write process output and error streams to. Requires to be . /// If the process output and error streams should be read. /// If shell execute should not be used. Must be set if is set. - /// The new . - IProcess LaunchProcess( + /// A resulting in the new . + ValueTask LaunchProcess( string fileName, string workingDirectory, string arguments, + CancellationToken cancellationToken, + IReadOnlyDictionary? environment = null, string? fileRedirect = null, bool readStandardHandles = false, bool noShellExecute = false); diff --git a/src/Tgstation.Server.Host/System/IProcessFeatures.cs b/src/Tgstation.Server.Host/System/IProcessFeatures.cs index abfaca6b7b..b00c5a4934 100644 --- a/src/Tgstation.Server.Host/System/IProcessFeatures.cs +++ b/src/Tgstation.Server.Host/System/IProcessFeatures.cs @@ -18,11 +18,11 @@ namespace Tgstation.Server.Host.System /// /// Suspend a given . /// - /// The to suspend. + /// The to suspend. void SuspendProcess(global::System.Diagnostics.Process process); /// - /// Resume a given suspended . + /// Resume a given suspended . /// /// The to suspended. void ResumeProcess(global::System.Diagnostics.Process process); @@ -30,10 +30,19 @@ namespace Tgstation.Server.Host.System /// /// Create a dump file for a given . /// - /// The to dump. + /// The to dump. /// The full path to the output file. + /// If a minidump should be taken as opposed to a full dump. /// The for the operation. /// A representing the running operation. - ValueTask CreateDump(global::System.Diagnostics.Process process, string outputFile, CancellationToken cancellationToken); + ValueTask CreateDump(global::System.Diagnostics.Process process, string outputFile, bool minidump, CancellationToken cancellationToken); + + /// + /// Run events on starting a process. + /// + /// The that was started. + /// The for the operation. + /// A resulting in the ID. + ValueTask HandleProcessStart(global::System.Diagnostics.Process process, CancellationToken cancellationToken); } } diff --git a/src/Tgstation.Server.Host/System/PosixProcessFeatures.cs b/src/Tgstation.Server.Host/System/PosixProcessFeatures.cs index 8077577aaf..f9894e9ac9 100644 --- a/src/Tgstation.Server.Host/System/PosixProcessFeatures.cs +++ b/src/Tgstation.Server.Host/System/PosixProcessFeatures.cs @@ -1,7 +1,11 @@ using System; +using System.Globalization; +using System.IO; +using System.Text; using System.Threading; using System.Threading.Tasks; +using Microsoft.Extensions.Hosting; using Microsoft.Extensions.Logging; using Mono.Unix; using Mono.Unix.Native; @@ -13,8 +17,18 @@ using Tgstation.Server.Host.Jobs; namespace Tgstation.Server.Host.System { /// - sealed class PosixProcessFeatures : IProcessFeatures + sealed class PosixProcessFeatures : IProcessFeatures, IHostedService { + /// + /// Difference from to set our own oom_score_adj to. 1 higher host watchdog. + /// + const short SelfOomAdjust = 1; + + /// + /// Difference from to set the oom_score_adj of child processes to. 1 higher than ourselves. + /// + const short ChildProcessOomAdjust = SelfOomAdjust + 1; + /// /// loaded . /// @@ -30,6 +44,11 @@ namespace Tgstation.Server.Host.System /// readonly ILogger logger; + /// + /// The original value of oom_score_adj as read from the /proc/ filesystem. Inherited from parent process. + /// + short baselineOomAdjust; + /// /// Initializes a new instance of the class. /// @@ -64,7 +83,7 @@ namespace Tgstation.Server.Host.System => throw new NotSupportedException(); /// - public async ValueTask CreateDump(global::System.Diagnostics.Process process, string outputFile, CancellationToken cancellationToken) + public async ValueTask CreateDump(global::System.Diagnostics.Process process, string outputFile, bool minidump, CancellationToken cancellationToken) { ArgumentNullException.ThrowIfNull(process); ArgumentNullException.ThrowIfNull(outputFile); @@ -88,10 +107,11 @@ namespace Tgstation.Server.Host.System string? output; int exitCode; - await using (var gcoreProc = lazyLoadedProcessExecutor.Value.LaunchProcess( + await using (var gcoreProc = await lazyLoadedProcessExecutor.Value.LaunchProcess( GCorePath, Environment.CurrentDirectory, - $"-o {outputFile} {process.Id}", + $"{(!minidump ? "-a " : String.Empty)}-o {outputFile} {process.Id}", + cancellationToken, readStandardHandles: true, noShellExecute: true)) { @@ -99,7 +119,7 @@ namespace Tgstation.Server.Host.System exitCode = (await gcoreProc.Lifetime).Value; output = await gcoreProc.GetCombinedOutput(cancellationToken); - logger.LogDebug("gcore output:{0}{1}", Environment.NewLine, output); + logger.LogDebug("gcore output:{newline}{output}", Environment.NewLine, output); } if (exitCode != 0) @@ -112,5 +132,86 @@ namespace Tgstation.Server.Host.System var generatedGCoreFile = $"{outputFile}.{pid}"; await ioManager.MoveFile(generatedGCoreFile, outputFile, cancellationToken); } + + /// + public async ValueTask HandleProcessStart(global::System.Diagnostics.Process process, CancellationToken cancellationToken) + { + ArgumentNullException.ThrowIfNull(process); + var pid = process.Id; + try + { + // make sure all processes we spawn are killed _before_ us + await AdjustOutOfMemoryScore(pid, ChildProcessOomAdjust, cancellationToken); + } + catch (Exception ex) when (ex is not OperationCanceledException) + { + logger.LogWarning(ex, "Failed to adjust OOM killer score for pid {pid}!", pid); + } + + return pid; + } + + /// + public async Task StartAsync(CancellationToken cancellationToken) + { + // let this all throw + string originalString; + { + // can't use ReadAllBytes here, /proc files have 0 length so the buffer is initialized to empty + // https://stackoverflow.com/questions/12237712/how-can-i-show-the-size-of-files-in-proc-it-should-not-be-size-zero + await using var fileStream = ioManager.CreateAsyncSequentialReadStream( + "/proc/self/oom_score_adj"); + using var reader = new StreamReader(fileStream, Encoding.UTF8, leaveOpen: true); + originalString = await reader.ReadToEndAsync(cancellationToken); + } + + var trimmedString = originalString.Trim(); + + logger.LogTrace("Original oom_score_adj is \"{original}\"", trimmedString); + + var originalOomAdjust = Int16.Parse(trimmedString, CultureInfo.InvariantCulture); + baselineOomAdjust = Math.Clamp(originalOomAdjust, (short)-1000, (short)1000); + + if (baselineOomAdjust == 1000) + if (originalOomAdjust != baselineOomAdjust) + logger.LogWarning("oom_score_adj is at it's limit of 1000 (Clamped from {original}). TGS cannot guarantee the kill order of its parent/child processes!", originalOomAdjust); + else + logger.LogWarning("oom_score_adj is at it's limit of 1000. TGS cannot guarantee the kill order of its parent/child processes!"); + + try + { + // we do not want to be killed before the host watchdog + await AdjustOutOfMemoryScore(null, SelfOomAdjust, cancellationToken); + } + catch (Exception ex) when (ex is not OperationCanceledException) + { + logger.LogWarning(ex, "Could not increase oom_score_adj!"); + } + } + + /// + public Task StopAsync(CancellationToken cancellationToken) => Task.CompletedTask; + + /// + /// Set oom_score_adj for a given . + /// + /// The or to self adjust. + /// The value being written to the adjustment file. + /// The for the operation. + /// A representing the running operation. + ValueTask AdjustOutOfMemoryScore(int? pid, short adjustment, CancellationToken cancellationToken) + { + var adjustedValue = Math.Clamp(baselineOomAdjust + adjustment, -1000, 1000); + + var pidStr = pid.HasValue + ? pid.Value.ToString(CultureInfo.InvariantCulture) + : "self"; + logger.LogTrace( + "Setting oom_score_adj of {pid} to {adjustment}...", pidStr, adjustedValue); + return ioManager.WriteAllBytes( + $"/proc/{pidStr}/oom_score_adj", + Encoding.UTF8.GetBytes(adjustedValue.ToString(CultureInfo.InvariantCulture)), + cancellationToken); + } } } diff --git a/src/Tgstation.Server.Host/System/Process.cs b/src/Tgstation.Server.Host/System/Process.cs index 4362083247..896195944c 100644 --- a/src/Tgstation.Server.Host/System/Process.cs +++ b/src/Tgstation.Server.Host/System/Process.cs @@ -224,13 +224,13 @@ namespace Tgstation.Server.Host.System } /// - public ValueTask CreateDump(string outputFile, CancellationToken cancellationToken) + public ValueTask CreateDump(string outputFile, bool minidump, CancellationToken cancellationToken) { ArgumentNullException.ThrowIfNull(outputFile); CheckDisposed(); logger.LogTrace("Dumping PID {pid} to {dumpFilePath}...", Id, outputFile); - return processFeatures.CreateDump(handle, outputFile, cancellationToken); + return processFeatures.CreateDump(handle, outputFile, minidump, cancellationToken); } /// diff --git a/src/Tgstation.Server.Host/System/ProcessExecutor.cs b/src/Tgstation.Server.Host/System/ProcessExecutor.cs index 7820ca11b3..01fe2c00a4 100644 --- a/src/Tgstation.Server.Host/System/ProcessExecutor.cs +++ b/src/Tgstation.Server.Host/System/ProcessExecutor.cs @@ -1,6 +1,8 @@ using System; +using System.Collections.Generic; using System.Diagnostics; using System.IO; +using System.Linq; using System.Text; using System.Threading; using System.Threading.Channels; @@ -103,10 +105,12 @@ namespace Tgstation.Server.Host.System } /// - public IProcess LaunchProcess( + public async ValueTask LaunchProcess( string fileName, string workingDirectory, string arguments, + CancellationToken cancellationToken, + IReadOnlyDictionary? environment, string? fileRedirect, bool readStandardHandles, bool noShellExecute) @@ -115,24 +119,33 @@ namespace Tgstation.Server.Host.System ArgumentNullException.ThrowIfNull(workingDirectory); ArgumentNullException.ThrowIfNull(arguments); + var enviromentLogLines = environment == null + ? String.Empty + : String.Concat(environment.Select(kvp => $"{Environment.NewLine}\t- {kvp.Key}={kvp.Value}")); if (noShellExecute) logger.LogDebug( - "Launching process in {workingDirectory}: {exe} {arguments}", + "Launching process in {workingDirectory}: {exe} {arguments}{environment}", workingDirectory, fileName, - arguments); + arguments, + enviromentLogLines); else logger.LogDebug( - "Shell launching process in {workingDirectory}: {exe} {arguments}", + "Shell launching process in {workingDirectory}: {exe} {arguments}{environment}", workingDirectory, fileName, - arguments); + arguments, + enviromentLogLines); var handle = new global::System.Diagnostics.Process(); try { handle.StartInfo.FileName = fileName; handle.StartInfo.Arguments = arguments; + if (environment != null) + foreach (var kvp in environment) + handle.StartInfo.Environment.Add(kvp!); + handle.StartInfo.WorkingDirectory = workingDirectory; handle.StartInfo.UseShellExecute = !noShellExecute; @@ -162,7 +175,16 @@ namespace Tgstation.Server.Host.System ExclusiveProcessLaunchLock.ExitReadLock(); } - pid = handle.Id; + try + { + pid = await processFeatures.HandleProcessStart(handle, cancellationToken); + } + catch + { + handle.Kill(); + throw; + } + processStartTcs?.SetResult(pid); } catch (Exception ex) diff --git a/src/Tgstation.Server.Host/System/WindowsFirewallHelper.cs b/src/Tgstation.Server.Host/System/WindowsFirewallHelper.cs index fff1521092..13e1171053 100644 --- a/src/Tgstation.Server.Host/System/WindowsFirewallHelper.cs +++ b/src/Tgstation.Server.Host/System/WindowsFirewallHelper.cs @@ -31,10 +31,11 @@ namespace Tgstation.Server.Host.System { 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( + await using var netshProcess = await processExecutor.LaunchProcess( "netsh.exe", Environment.CurrentDirectory, arguments, + cancellationToken, readStandardHandles: true, noShellExecute: true); diff --git a/src/Tgstation.Server.Host/System/WindowsProcessFeatures.cs b/src/Tgstation.Server.Host/System/WindowsProcessFeatures.cs index 41789aec8c..6b71b2b253 100644 --- a/src/Tgstation.Server.Host/System/WindowsProcessFeatures.cs +++ b/src/Tgstation.Server.Host/System/WindowsProcessFeatures.cs @@ -1,4 +1,5 @@ using System; +using System.Collections.Generic; using System.Diagnostics; using System.IO; using System.Linq; @@ -68,28 +69,40 @@ namespace Tgstation.Server.Host.System { ArgumentNullException.ThrowIfNull(process); - process.Refresh(); - foreach (ProcessThread thread in process.Threads) + var suspendedThreadIds = new HashSet(); + bool suspendedNewThreads; + do { - var threadId = (uint)thread.Id; - logger.LogTrace("Suspending thread {threadId}...", threadId); - var pOpenThread = NativeMethods.OpenThread(NativeMethods.ThreadAccess.SuspendResume, false, threadId); - if (pOpenThread == IntPtr.Zero) + suspendedNewThreads = false; + process.Refresh(); + foreach (ProcessThread thread in process.Threads) { - logger.LogDebug(new Win32Exception(), "Failed to open thread {threadId}!", threadId); - continue; - } + var threadId = (uint)thread.Id; - try - { - if (NativeMethods.SuspendThread(pOpenThread) == UInt32.MaxValue) - throw new Win32Exception(); - } - finally - { - NativeMethods.CloseHandle(pOpenThread); + if (!suspendedThreadIds.Add(threadId)) + continue; + + suspendedNewThreads = true; + logger.LogTrace("Suspending thread {threadId}...", threadId); + var pOpenThread = NativeMethods.OpenThread(NativeMethods.ThreadAccess.SuspendResume, false, threadId); + if (pOpenThread == IntPtr.Zero) + { + logger.LogDebug(new Win32Exception(), "Failed to open thread {threadId}!", threadId); + continue; + } + + try + { + if (NativeMethods.SuspendThread(pOpenThread) == UInt32.MaxValue) + throw new Win32Exception(); + } + finally + { + NativeMethods.CloseHandle(pOpenThread); + } } } + while (suspendedNewThreads); } /// @@ -120,7 +133,7 @@ namespace Tgstation.Server.Host.System } /// - public async ValueTask CreateDump(global::System.Diagnostics.Process process, string outputFile, CancellationToken cancellationToken) + public async ValueTask CreateDump(global::System.Diagnostics.Process process, string outputFile, bool minidump, CancellationToken cancellationToken) { try { @@ -137,15 +150,19 @@ namespace Tgstation.Server.Host.System await Task.Factory.StartNew( () => { + var flags = NativeMethods.MiniDumpType.WithHandleData + | NativeMethods.MiniDumpType.WithThreadInfo + | NativeMethods.MiniDumpType.WithUnloadedModules; + + if (!minidump) + flags |= NativeMethods.MiniDumpType.WithDataSegs + | NativeMethods.MiniDumpType.WithFullMemory; + if (!NativeMethods.MiniDumpWriteDump( process.Handle, (uint)process.Id, fileStream.SafeFileHandle, - NativeMethods.MiniDumpType.WithDataSegs - | NativeMethods.MiniDumpType.WithFullMemory - | NativeMethods.MiniDumpType.WithHandleData - | NativeMethods.MiniDumpType.WithThreadInfo - | NativeMethods.MiniDumpType.WithUnloadedModules, + flags, IntPtr.Zero, IntPtr.Zero, IntPtr.Zero)) @@ -155,5 +172,9 @@ namespace Tgstation.Server.Host.System DefaultIOManager.BlockingTaskCreationOptions, TaskScheduler.Current); } + + /// + public ValueTask HandleProcessStart(global::System.Diagnostics.Process process, CancellationToken cancellationToken) + => ValueTask.FromResult((process ?? throw new ArgumentNullException(nameof(process))).Id); } } diff --git a/src/Tgstation.Server.Host/Tgstation.Server.Host.csproj b/src/Tgstation.Server.Host/Tgstation.Server.Host.csproj index e6a0a19d97..a2afbf98de 100644 --- a/src/Tgstation.Server.Host/Tgstation.Server.Host.csproj +++ b/src/Tgstation.Server.Host/Tgstation.Server.Host.csproj @@ -1,4 +1,4 @@ - + @@ -69,40 +69,44 @@ + + - + - + - + - + - + + + - + - + runtime; build; native; contentfiles; analyzers; buildtransitive - + - + - + - + - + - + @@ -110,7 +114,7 @@ - + @@ -124,7 +128,7 @@ - + diff --git a/src/Tgstation.Server.Host/Utils/FifoSemaphore.cs b/src/Tgstation.Server.Host/Utils/FifoSemaphore.cs index 0bb696680a..a00d6dba5c 100644 --- a/src/Tgstation.Server.Host/Utils/FifoSemaphore.cs +++ b/src/Tgstation.Server.Host/Utils/FifoSemaphore.cs @@ -14,7 +14,7 @@ namespace Tgstation.Server.Host.Utils /// /// to represent a ticket in the and whether or not it is . /// - class FifoSemaphoreTicket + sealed class FifoSemaphoreTicket { /// /// Set if the wait operation on a was cancelled to avoid clogging the queue. diff --git a/src/Tgstation.Server.Host/Utils/SignalR/ComprehensiveHubContext.cs b/src/Tgstation.Server.Host/Utils/SignalR/ComprehensiveHubContext.cs index d9c32143ce..b47f17d57b 100644 --- a/src/Tgstation.Server.Host/Utils/SignalR/ComprehensiveHubContext.cs +++ b/src/Tgstation.Server.Host/Utils/SignalR/ComprehensiveHubContext.cs @@ -147,8 +147,9 @@ namespace Tgstation.Server.Host.Utils.SignalR return old; }); - foreach (var context in connections!) - context.Abort(); + if (connections != null) + foreach (var context in connections) + context.Abort(); } } } diff --git a/tests/DMAPI/BasicOperation/Test.dm b/tests/DMAPI/BasicOperation/Test.dm index 022088fc67..6eecddfaa7 100644 --- a/tests/DMAPI/BasicOperation/Test.dm +++ b/tests/DMAPI/BasicOperation/Test.dm @@ -15,10 +15,31 @@ sleep(50) world.TgsTargetedChatBroadcast("Sample admin-only message", TRUE) + world.log << "params check" var/list/world_params = world.params if(!("test" in world_params) || world_params["test"] != "bababooey") FailTest("Expected parameter test=bababooey but did not receive", "test_fail_reason.txt") + world.log << "file check 1" + fdel("test_event_output.txt") + var/test_data = "nwfiuurhfu" + world.TgsTriggerEvent("test_event", list(test_data), TRUE) + if(!fexists("test_event_output.txt")) + FailTest("Expected test_event_output.txt to exist here", "test_fail_reason.txt") + + var/test_contents = copytext(file2text("test_event_output.txt"), 1, length(test_data) + 1) + if(test_contents != test_data) + FailTest("Expected test_event_output.txt to contain [test_data] here. Got [test_contents]", "test_fail_reason.txt") + + world.log << "file check 1" + fdel("test_event_output.txt") + + var/start_time = world.timeofday + world.TgsTriggerEvent("test_event", list("asdf"), FALSE) + + if((world.timeofday - start_time) <= 50 && fexists("test_event_output.txt")) + FailTest("Expected test_event_output.txt to not exist here", "test_fail_reason.txt") + world.log << "sleep2" sleep(150) world.log << "Terminating..." diff --git a/tests/DMAPI/BasicOperation/test_event-qwer.bat b/tests/DMAPI/BasicOperation/test_event-qwer.bat new file mode 100644 index 0000000000..7eb6a4dff9 --- /dev/null +++ b/tests/DMAPI/BasicOperation/test_event-qwer.bat @@ -0,0 +1,5 @@ +echo "Running test_event script" +C:\Windows\System32\WindowsPowerShell\v1.0\powershell.exe -ExecutionPolicy Bypass -Command "Start-Sleep -Seconds 5" +cd %1 +cd tests\DMAPI\BasicOperation +echo %2 > test_event_output.txt diff --git a/tests/DMAPI/BasicOperation/test_event-qwer.sh b/tests/DMAPI/BasicOperation/test_event-qwer.sh new file mode 100755 index 0000000000..185bc88fed --- /dev/null +++ b/tests/DMAPI/BasicOperation/test_event-qwer.sh @@ -0,0 +1,13 @@ +#!/bin/bash + +set -e + +echo "Running test_event script - $1 - $2" + +sleep 5 + +cd $1 +cd tests/DMAPI/BasicOperation + +echo $2 > test_event_output.txt + diff --git a/tests/DMAPI/LongRunning/EngineActiveVersionChange-SetupEnv.bat b/tests/DMAPI/LongRunning/EngineActiveVersionChange-SetupEnv.bat new file mode 100644 index 0000000000..6f026682c0 --- /dev/null +++ b/tests/DMAPI/LongRunning/EngineActiveVersionChange-SetupEnv.bat @@ -0,0 +1,4 @@ +cd /D "%~dp0" +cd ../../Byond/%1 +echo # Comment > server.env +echo NOTA=Real Comment>> server.env diff --git a/tests/DMAPI/LongRunning/EngineActiveVersionChange-SetupEnv.sh b/tests/DMAPI/LongRunning/EngineActiveVersionChange-SetupEnv.sh new file mode 100644 index 0000000000..52b8090bd1 --- /dev/null +++ b/tests/DMAPI/LongRunning/EngineActiveVersionChange-SetupEnv.sh @@ -0,0 +1,7 @@ +#!/bin/bash + +set -e + +cd "../../Byond/$1" + +echo -e '# This is a comment\nNOTA=Real Comment\n\n\n' > server.env diff --git a/tests/DMAPI/LongRunning/Test.dm b/tests/DMAPI/LongRunning/Test.dm index 90164c7e80..2735c8e0ab 100644 --- a/tests/DMAPI/LongRunning/Test.dm +++ b/tests/DMAPI/LongRunning/Test.dm @@ -34,7 +34,7 @@ if(!res) FailTest("Failed to resource!") - var/res_contents = file2text(res) // we need a .rsc to be generated + var/res_contents = TGS_FILE2TEXT_NATIVE(res) // we need a .rsc to be generated if(!res_contents) FailTest("Failed to resource? No contents!") @@ -203,7 +203,7 @@ var/run_bridge_test DetachedChatMessageQueuingP2() /proc/DetachedChatMessageQueuingP2() - sleep(1) + sleep(world.tick_lag) DetachedChatMessageQueuingP3() /proc/DetachedChatMessageQueuingP3() @@ -240,7 +240,7 @@ var/received_health_check = FALSE DelayCheckDetach() /proc/DelayCheckDetach() - sleep(1) + sleep(world.tick_lag) // hack hack, calling world.TgsChatChannelInfo() will try to delay until the channels come back var/datum/tgs_api/v5/api = TGS_READ_GLOBAL(tgs) if(length(api.chat_channels)) diff --git a/tests/DMAPI/test_setup.dm b/tests/DMAPI/test_setup.dm index 56f4c03eb8..ac345230b7 100644 --- a/tests/DMAPI/test_setup.dm +++ b/tests/DMAPI/test_setup.dm @@ -32,4 +32,4 @@ text2file(reason, "test_fail_reason.txt") world.log << "Terminating..." del(world) - sleep(1) // https://www.byond.com/forum/post/2894866 + sleep(world.tick_lag) // https://www.byond.com/forum/post/2894866 diff --git a/tests/Tgstation.Server.Host.Tests/Components/Engine/TestPosixByondInstaller.cs b/tests/Tgstation.Server.Host.Tests/Components/Engine/TestPosixByondInstaller.cs index 22cc145683..60e86ad63e 100644 --- a/tests/Tgstation.Server.Host.Tests/Components/Engine/TestPosixByondInstaller.cs +++ b/tests/Tgstation.Server.Host.Tests/Components/Engine/TestPosixByondInstaller.cs @@ -90,7 +90,7 @@ namespace Tgstation.Server.Host.Components.Engine.Tests var installer = new PosixByondInstaller(mockPostWriteHandler.Object, mockIOManager.Object, mockFileDownloader, mockLogger.Object); const string FakePath = "fake"; - await Assert.ThrowsExceptionAsync(() => installer.Install(null, null, default).AsTask()); + await Assert.ThrowsExceptionAsync(() => installer.Install(null, null, false, default).AsTask()); var byondVersion = new EngineVersion { @@ -98,10 +98,10 @@ namespace Tgstation.Server.Host.Components.Engine.Tests Version = new Version(123, 252345), }; - await Assert.ThrowsExceptionAsync(() => installer.Install(byondVersion, null, default).AsTask()); + await Assert.ThrowsExceptionAsync(() => installer.Install(byondVersion, null, false, default).AsTask()); byondVersion.Version = new Version(511, 1385); - await installer.Install(byondVersion, FakePath, default); + await installer.Install(byondVersion, FakePath, false, default); mockPostWriteHandler.Verify(x => x.HandleWrite(It.IsAny()), Times.Exactly(4)); } diff --git a/tests/Tgstation.Server.Host.Tests/Components/Repository/TestRepositoryFactory.cs b/tests/Tgstation.Server.Host.Tests/Components/Repository/TestRepositoryFactory.cs index 04e7cdb66b..84aea24ef6 100644 --- a/tests/Tgstation.Server.Host.Tests/Components/Repository/TestRepositoryFactory.cs +++ b/tests/Tgstation.Server.Host.Tests/Components/Repository/TestRepositoryFactory.cs @@ -14,7 +14,7 @@ namespace Tgstation.Server.Host.Components.Repository.Tests [TestClass] public sealed class TestRepositoryFactory { - static ILibGit2RepositoryFactory CreateFactory() => new LibGit2RepositoryFactory(Mock.Of>()); + static LibGit2RepositoryFactory CreateFactory() => new (Mock.Of>()); static async Task TestRepoLoading( string path, diff --git a/tests/Tgstation.Server.Host.Tests/System/TestPosixSignalHandler.cs b/tests/Tgstation.Server.Host.Tests/System/TestPosixSignalHandler.cs index 083657a075..dd0f5fd5fb 100644 --- a/tests/Tgstation.Server.Host.Tests/System/TestPosixSignalHandler.cs +++ b/tests/Tgstation.Server.Host.Tests/System/TestPosixSignalHandler.cs @@ -63,11 +63,13 @@ namespace Tgstation.Server.Host.System.Tests Mock.Of(), loggerFactory.CreateLogger(), loggerFactory); - await using var subProc = processExecutor + await using var subProc = await processExecutor .LaunchProcess( "dotnet", pathToSignalTestApp, $"run -c {CurrentConfig} --no-build", + CancellationToken.None, + null, null, true, true); 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 d6f3aa8e39..a179871b32 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/TestReferenceCountingContainer.cs b/tests/Tgstation.Server.Host.Tests/Utils/TestReferenceCountingContainer.cs new file mode 100644 index 0000000000..f517d66de2 --- /dev/null +++ b/tests/Tgstation.Server.Host.Tests/Utils/TestReferenceCountingContainer.cs @@ -0,0 +1,56 @@ +using Microsoft.VisualStudio.TestTools.UnitTesting; + +namespace Tgstation.Server.Host.Utils.Tests +{ + [TestClass] + public sealed class TestReferenceCountingContainer + { + interface IInterface + { + } + + sealed class Implementation : IInterface + { + } + + sealed class Lock : ReferenceCounter + { + public IInterface PubInstance => Instance; + } + + + [TestMethod] + public void TestReferenceCounting() + { + var impl = new Implementation(); + var container = new ReferenceCountingContainer(impl); + + Assert.IsTrue(container.OnZeroReferences.IsCompleted); + + var ref1 = container.AddReference(); + + Assert.AreSame(ref1.PubInstance, impl); + + var task = container.OnZeroReferences; + + Assert.AreSame(task, container.OnZeroReferences); + Assert.IsFalse(task.IsCompleted); + + ref1.Dispose(); + + Assert.IsTrue(task.IsCompleted); + Assert.IsTrue(container.OnZeroReferences.IsCompleted); + + ref1.Dispose(); + Assert.IsTrue(container.OnZeroReferences.IsCompleted); + + var ref2 = container.AddReference(); + Assert.IsFalse((task = container.OnZeroReferences).IsCompleted); + + + ref2.Dispose(); + Assert.IsTrue(task.IsCompleted); + Assert.IsTrue(container.OnZeroReferences.IsCompleted); + } + } +} diff --git a/tests/Tgstation.Server.Tests/Live/Instance/ChatTest.cs b/tests/Tgstation.Server.Tests/Live/Instance/ChatTest.cs index abb699815e..1dcfd368da 100644 --- a/tests/Tgstation.Server.Tests/Live/Instance/ChatTest.cs +++ b/tests/Tgstation.Server.Tests/Live/Instance/ChatTest.cs @@ -6,6 +6,8 @@ using System.Threading.Tasks; using Microsoft.VisualStudio.TestTools.UnitTesting; +using Newtonsoft.Json; + using Tgstation.Server.Api.Models; using Tgstation.Server.Api.Models.Request; using Tgstation.Server.Api.Models.Response; @@ -196,7 +198,7 @@ namespace Tgstation.Server.Tests.Live.Instance .OrderBy(x => x.StartedAt) .FirstOrDefault(); - Assert.IsNotNull(reconnectJob); + Assert.IsNotNull(reconnectJob, $"Jobs: {JsonConvert.SerializeObject(jobs)}"); await WaitForJob(reconnectJob, 60, false, null, cancellationToken); var channelIdStr = Environment.GetEnvironmentVariable("TGS_TEST_DISCORD_CHANNEL"); diff --git a/tests/Tgstation.Server.Tests/Live/Instance/ConfigurationTest.cs b/tests/Tgstation.Server.Tests/Live/Instance/ConfigurationTest.cs index 73c658487b..eb39799414 100644 --- a/tests/Tgstation.Server.Tests/Live/Instance/ConfigurationTest.cs +++ b/tests/Tgstation.Server.Tests/Live/Instance/ConfigurationTest.cs @@ -118,18 +118,25 @@ namespace Tgstation.Server.Tests.Live.Instance await using var memoryStream2 = new MemoryStream(Encoding.UTF8.GetBytes("bbb")); await configurationClient.Write(staticFile2, memoryStream2, cancellationToken); - var shellScriptExtension = new PlatformIdentifier().IsWindows ? ".bat" : ".sh"; - var scriptName = $"PreCompile-GenerateRandomResource{shellScriptExtension}"; - var resourcingScript = new ConfigurationFileRequest + async ValueTask UploadScript(string scriptId, bool basic) { - Path = $"/EventScripts/{scriptName}" - }; + var shellScriptExtension = new PlatformIdentifier().IsWindows ? ".bat" : ".sh"; + var scriptName = $"{scriptId}{shellScriptExtension}"; + var resourcingScript = new ConfigurationFileRequest + { + Path = $"/EventScripts/{scriptName}" + }; - await using var readStream = ioManager.GetFileStream($"../../../../DMAPI/LongRunning/{scriptName}", false); - await configurationClient.Write( - resourcingScript, - readStream, - cancellationToken); + await using var readStream = ioManager.GetFileStream($"../../../../DMAPI/{(basic ? "BasicOperation" : "LongRunning")}/{scriptName}", false); + await configurationClient.Write( + resourcingScript, + readStream, + cancellationToken); + } + + await UploadScript("PreCompile-GenerateRandomResource", false); + await UploadScript("EngineActiveVersionChange-SetupEnv", false); + await UploadScript("test_event-qwer", true); } return ValueTaskExtensions.WhenAll( diff --git a/tests/Tgstation.Server.Tests/Live/Instance/DeploymentTest.cs b/tests/Tgstation.Server.Tests/Live/Instance/DeploymentTest.cs index 28485a7732..a158ed4edf 100644 --- a/tests/Tgstation.Server.Tests/Live/Instance/DeploymentTest.cs +++ b/tests/Tgstation.Server.Tests/Live/Instance/DeploymentTest.cs @@ -125,17 +125,24 @@ namespace Tgstation.Server.Tests.Live.Instance { ProjectName = "tests/DMAPI/ApiFree/api_free", ApiValidationPort = dmPort, + CompilerAdditionalArguments = " ", }, cancellationToken); Assert.AreEqual(dmPort, updatedDM.ApiValidationPort); Assert.AreEqual("tests/DMAPI/ApiFree/api_free", updatedDM.ProjectName); + Assert.IsNull(updatedDM.CompilerAdditionalArguments); } else { var updatedDM = await dreamMakerClient.Update(new DreamMakerRequest { - ApiValidationPort = dmPort + ApiValidationPort = dmPort, + CompilerAdditionalArguments = testEngine == EngineType.Byond ? " -DBABABOOEY" : " ", }, cancellationToken); Assert.AreEqual(dmPort, updatedDM.ApiValidationPort); + if (testEngine == EngineType.Byond) + Assert.AreEqual("-DBABABOOEY", updatedDM.CompilerAdditionalArguments); + else + Assert.IsNull(updatedDM.CompilerAdditionalArguments); } Console.WriteLine($"PORT REUSE BUG 1: Setting I-{instanceClient.Metadata.Id} DD to {ddPort}"); diff --git a/tests/Tgstation.Server.Tests/Live/Instance/WatchdogTest.cs b/tests/Tgstation.Server.Tests/Live/Instance/WatchdogTest.cs index 4d5e1685a5..48d8a5faee 100644 --- a/tests/Tgstation.Server.Tests/Live/Instance/WatchdogTest.cs +++ b/tests/Tgstation.Server.Tests/Live/Instance/WatchdogTest.cs @@ -351,13 +351,11 @@ namespace Tgstation.Server.Tests.Live.Instance var deleteJob = await deleteJobTask; - // And this freezes DD - await DumpTests(cancellationToken); + // And this freezes DD (also restarts it) + await DumpTests(false, cancellationToken); + await DumpTests(true, cancellationToken); - // Restart to unlock previous BYOND version - var restartJob = await instanceClient.DreamDaemon.Restart(cancellationToken); await WaitForJob(deleteJob, 15, false, null, cancellationToken); - await WaitForJob(restartJob, 15, false, null, cancellationToken); } async ValueTask RegressionTest1550(CancellationToken cancellationToken) @@ -519,14 +517,19 @@ namespace Tgstation.Server.Tests.Live.Instance Assert.AreEqual("sent", topicRequestResult.StringData); } - async Task DumpTests(CancellationToken cancellationToken) + async Task DumpTests(bool mini, CancellationToken cancellationToken) { System.Console.WriteLine("TEST: WATCHDOG DUMP TESTS"); + var updated = await instanceClient.DreamDaemon.Update(new DreamDaemonRequest + { + Minidumps = mini, + }, cancellationToken); + Assert.AreEqual(mini, updated.Minidumps); var dumpJob = await instanceClient.DreamDaemon.CreateDump(cancellationToken); - await WaitForJob(dumpJob, 30, false, null, cancellationToken); + await WaitForJob(dumpJob, 60, false, null, cancellationToken); var dumpFiles = Directory.GetFiles(Path.Combine( - instanceClient.Metadata.Path, "Diagnostics", "ProcessDumps"), "*.dmp"); + instanceClient.Metadata.Path, "Diagnostics", "ProcessDumps"), testVersion.Engine == EngineType.OpenDream ? "*.net.dmp" : "*.dmp"); Assert.AreEqual(1, dumpFiles.Length); File.Delete(dumpFiles.Single()); @@ -726,28 +729,41 @@ namespace Tgstation.Server.Tests.Live.Instance var foundLivePath = false; var allPaths = new List(); - Assert.IsFalse(proc.HasExited); - foreach (var fd in Directory.GetFiles($"/proc/{pid}/fd")) + var features = new PosixProcessFeatures( + new Lazy(Mock.Of()), + new DefaultIOManager(), + Mock.Of>()); + + features.SuspendProcess(proc); + try { - var sb = new StringBuilder(UInt16.MaxValue); - if (Syscall.readlink(fd, sb) == -1) - throw new UnixIOException(Stdlib.GetLastError()); + 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) + throw new UnixIOException(Stdlib.GetLastError()); - var path = sb.ToString(); + var path = sb.ToString(); - allPaths.Add($"Path: {path}"); - if (path.Contains($"Game/{previousStatus.DirectoryName}")) - failingLinks.Add($"Found fd {fd} resolving to previous absolute path game dir path: {path}"); + allPaths.Add($"Path: {path}"); + if (path.Contains($"Game/{previousStatus.DirectoryName}")) + failingLinks.Add($"Found fd {fd} resolving to previous absolute path game dir path: {path}"); - if (path.Contains($"Game/{currentStatus.ActiveCompileJob.DirectoryName}")) - failingLinks.Add($"Found fd {fd} resolving to current absolute path game dir path: {path}"); + if (path.Contains($"Game/{currentStatus.ActiveCompileJob.DirectoryName}")) + failingLinks.Add($"Found fd {fd} resolving to current absolute path game dir path: {path}"); - if (path.Contains($"Game/Live")) - foundLivePath = true; + if (path.Contains($"Game/Live")) + foundLivePath = true; + } + + if (!foundLivePath) + failingLinks.Add($"Failed to find a path containing the 'Live' directory!"); + } + finally + { + features.ResumeProcess(proc); } - - if (!foundLivePath) - failingLinks.Add($"Failed to find a path containing the 'Live' directory!"); Assert.IsTrue(failingLinks.Count == 0, String.Join(Environment.NewLine, failingLinks.Concat(allPaths))); } @@ -782,7 +798,7 @@ namespace Tgstation.Server.Tests.Live.Instance executor = new ProcessExecutor( RuntimeInformation.IsOSPlatform(OSPlatform.Windows) ? new WindowsProcessFeatures(Mock.Of>()) - : new PosixProcessFeatures(new Lazy(() => executor), Mock.Of(), Mock.Of>()), + : new PosixProcessFeatures(new Lazy(() => executor), new DefaultIOManager(), Mock.Of>()), Mock.Of(), Mock.Of>(), LoggerFactory.Create(x => { })); @@ -813,7 +829,21 @@ namespace Tgstation.Server.Tests.Live.Instance ourProcessHandler.SuspendProcess(); global::System.Console.WriteLine($"WATCHDOG TEST {instanceClient.Metadata.Id}: FINISH PROCESS SUSPEND FOR HEALTH CHECK DEATH. WAITING FOR LIFETIME {ourProcessHandler.Id}."); + if (testVersion.Engine == EngineType.OpenDream && checkDump) + { + // because dotnet diagnostics relies on the engine process to write its own dump, we actually have to unpause it after the watchdog has decided to kill it + // incredibly cursed, because we don't have the means to accurately tell when that will happen. ESP in CI + return; // CBA rn + /* + await Task.Delay(TimeSpan.FromSeconds(30), cancellationToken); + ourProcessHandler.ResumeProcess(); + global::System.Console.WriteLine($"WATCHDOG TEST {instanceClient.Metadata.Id}: PROCESS RESUMING FOR DOTNET DUMP. WAITING FOR LIFETIME {ourProcessHandler.Id}.");*/ + } + await Task.WhenAny(ourProcessHandler.Lifetime, Task.Delay(TimeSpan.FromMinutes(4), cancellationToken)); + if (testVersion.Engine == EngineType.OpenDream && checkDump && !ourProcessHandler.Lifetime.IsCompleted) + return; + Assert.IsTrue(ourProcessHandler.Lifetime.IsCompleted); var timeout = 20; @@ -1468,7 +1498,7 @@ namespace Tgstation.Server.Tests.Live.Instance var newStatus = await instanceClient.DreamDaemon.Read(cancellationToken); Assert.IsTrue(newStatus.SoftShutdown.Value || newStatus.Status.Value == WatchdogStatus.Offline); - var timeout = 20; + var timeout = 40; do { await Task.Delay(TimeSpan.FromSeconds(3), cancellationToken); diff --git a/tests/Tgstation.Server.Tests/Live/TestLiveServer.cs b/tests/Tgstation.Server.Tests/Live/TestLiveServer.cs index 91466db1d0..b218b14680 100644 --- a/tests/Tgstation.Server.Tests/Live/TestLiveServer.cs +++ b/tests/Tgstation.Server.Tests/Live/TestLiveServer.cs @@ -1100,10 +1100,12 @@ namespace Tgstation.Server.Tests.Live async ValueTask RunGitCommand(string args) { - await using var gitRemoteOriginFixProc = processExecutor.LaunchProcess( + await using var gitRemoteOriginFixProc = await processExecutor.LaunchProcess( "git", repoPath, args, + cancellationToken, + null, null, true, true); diff --git a/tests/Tgstation.Server.Tests/TestDatabase.cs b/tests/Tgstation.Server.Tests/TestDatabase.cs index 320c4e32b8..c5b4b13f50 100644 --- a/tests/Tgstation.Server.Tests/TestDatabase.cs +++ b/tests/Tgstation.Server.Tests/TestDatabase.cs @@ -122,6 +122,7 @@ namespace Tgstation.Server.Tests StartProfiler = false, LogOutput = true, MapThreads = 69, + Minidumps = true, }, DreamMakerSettings = new Host.Models.DreamMakerSettings { diff --git a/tests/Tgstation.Server.Tests/TestRepository.cs b/tests/Tgstation.Server.Tests/TestRepository.cs index a9b3ca4305..e73731de40 100644 --- a/tests/Tgstation.Server.Tests/TestRepository.cs +++ b/tests/Tgstation.Server.Tests/TestRepository.cs @@ -46,6 +46,9 @@ namespace Tgstation.Server.Tests const string StartSha = "af4da8beb9f9b374b04a3cc4d65acca662e8cc1a"; await repo.CheckoutObject(StartSha, null, null, true, new JobProgressReporter(Mock.Of>(), null, (stage, progress) => { }), CancellationToken.None); + + Assert.AreEqual(Host.Components.Repository.Repository.NoReference, repo.Reference); + var result = await repo.CommittishIsParent("2f8588a3ca0f6b027704a2a04381215619de3412", CancellationToken.None); Assert.IsTrue(result); Assert.AreEqual(StartSha, repo.Head); diff --git a/tests/Tgstation.Server.Tests/TestSystemInteraction.cs b/tests/Tgstation.Server.Tests/TestSystemInteraction.cs index 98b7c82f44..e7c8e28962 100644 --- a/tests/Tgstation.Server.Tests/TestSystemInteraction.cs +++ b/tests/Tgstation.Server.Tests/TestSystemInteraction.cs @@ -28,7 +28,7 @@ namespace Tgstation.Server.Tests Mock.Of>(), loggerFactory); - await using var process = processExecutor.LaunchProcess("test." + platformIdentifier.ScriptFileExtension, ".", string.Empty, null, true, true); + await using var process = await processExecutor.LaunchProcess("test." + platformIdentifier.ScriptFileExtension, ".", string.Empty, CancellationToken.None, null, null, true, true); using var cts = new CancellationTokenSource(); cts.CancelAfter(3000); var exitCode = await process.Lifetime.WaitAsync(cts.Token); @@ -63,7 +63,7 @@ namespace Tgstation.Server.Tests File.Delete(tempFile); try { - await using (var process = processExecutor.LaunchProcess("test." + platformIdentifier.ScriptFileExtension, ".", string.Empty, tempFile, true, true)) + await using (var process = await processExecutor.LaunchProcess("test." + platformIdentifier.ScriptFileExtension, ".", string.Empty, CancellationToken.None, null, tempFile, true, true)) { using var cts = new CancellationTokenSource(); cts.CancelAfter(3000); diff --git a/tests/Tgstation.Server.Tests/TestVersions.cs b/tests/Tgstation.Server.Tests/TestVersions.cs index 7b52b79773..adadd4bfcb 100644 --- a/tests/Tgstation.Server.Tests/TestVersions.cs +++ b/tests/Tgstation.Server.Tests/TestVersions.cs @@ -208,7 +208,7 @@ namespace Tgstation.Server.Tests ? new WindowsProcessFeatures(Mock.Of>()) : new PosixProcessFeatures( new Lazy(() => null), - Mock.Of(), + new DefaultIOManager(), loggerFactory.CreateLogger()), Mock.Of(), loggerFactory.CreateLogger(), @@ -477,7 +477,7 @@ namespace Tgstation.Server.Tests if (byondInstaller is WindowsByondInstaller) typeof(WindowsByondInstaller).GetField("installedDirectX", BindingFlags.Instance | BindingFlags.NonPublic).SetValue(byondInstaller, true); - await byondInstaller.Install(engineVersion, tempPath, default); + await byondInstaller.Install(engineVersion, tempPath, false, default); var binPath = (string)typeof(ByondInstallerBase).GetField("ByondBinPath", BindingFlags.Static | BindingFlags.NonPublic).GetValue(null); var ddNameFunc = installerType.GetMethod("GetDreamDaemonName", BindingFlags.Instance | BindingFlags.NonPublic); @@ -498,10 +498,12 @@ namespace Tgstation.Server.Tests try { - await using var process = processExecutor.LaunchProcess( + await using var process = await processExecutor.LaunchProcess( ddPath, Environment.CurrentDirectory, "fake.dmb -map-threads 3 -close", + CancellationToken.None, + null, null, true, true); diff --git a/tgstation-server.sln b/tgstation-server.sln index 581410a016..a8b1f999bf 100644 --- a/tgstation-server.sln +++ b/tgstation-server.sln @@ -155,6 +155,8 @@ EndProject Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "LongRunning", "LongRunning", "{EB1DDE8C-CA6F-4BE3-947B-597CA8EABEA5}" ProjectSection(SolutionItems) = preProject tests\DMAPI\LongRunning\Config.dm = tests\DMAPI\LongRunning\Config.dm + tests\DMAPI\LongRunning\EngineActiveVersionChange-SetupEnv.bat = tests\DMAPI\LongRunning\EngineActiveVersionChange-SetupEnv.bat + tests\DMAPI\LongRunning\EngineActiveVersionChange-SetupEnv.sh = tests\DMAPI\LongRunning\EngineActiveVersionChange-SetupEnv.sh tests\DMAPI\LongRunning\long_running_test.dme = tests\DMAPI\LongRunning\long_running_test.dme tests\DMAPI\LongRunning\long_running_test_copy.dme = tests\DMAPI\LongRunning\long_running_test_copy.dme tests\DMAPI\LongRunning\long_running_test_rooted.dme = tests\DMAPI\LongRunning\long_running_test_rooted.dme @@ -171,9 +173,11 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "ApiFree", "ApiFree", "{7B8F EndProject Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "BasicOperation", "BasicOperation", "{F32B9514-AAD9-429D-841A-ED810FC2598C}" ProjectSection(SolutionItems) = preProject - tests\DMAPI\BasicOperation\Config.dm = tests\DMAPI\BasicOperation\Config.dm tests\DMAPI\BasicOperation\basic operation_test.dme = tests\DMAPI\BasicOperation\basic operation_test.dme + tests\DMAPI\BasicOperation\Config.dm = tests\DMAPI\BasicOperation\Config.dm tests\DMAPI\BasicOperation\Test.dm = tests\DMAPI\BasicOperation\Test.dm + tests\DMAPI\BasicOperation\test_event-qwer.bat = tests\DMAPI\BasicOperation\test_event-qwer.bat + tests\DMAPI\BasicOperation\test_event-qwer.sh = tests\DMAPI\BasicOperation\test_event-qwer.sh EndProjectSection EndProject Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "BuildFail", "BuildFail", "{103C61AB-67D6-46FE-AA47-CC633B88EE0F}" @@ -200,6 +204,9 @@ EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Tgstation.Server.Host.Common", "src\Tgstation.Server.Host.Common\Tgstation.Server.Host.Common.csproj", "{CF3968A0-EA81-4464-B2D4-C7D40F6B5BCB}" EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Tgstation.Server.Common", "src\Tgstation.Server.Common\Tgstation.Server.Common.csproj", "{70CD9A98-D31A-44A4-81D1-D02764CEEEFD}" + ProjectSection(ProjectDependencies) = postProject + {7F7FCFDF-271D-45C2-830C-BCCB19C57077} = {7F7FCFDF-271D-45C2-830C-BCCB19C57077} + EndProjectSection EndProject Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "package", "package", "{2648A85F-61AE-428E-95E1-66D06C7A3768}" ProjectSection(SolutionItems) = preProject @@ -254,6 +261,8 @@ Project("{B7DD6F7E-DEF8-4E67-B5B7-07EF123DB6F0}") = "Tgstation.Server.Host.Servi EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Tgstation.Server.ReleaseNotes", "tools\Tgstation.Server.ReleaseNotes\Tgstation.Server.ReleaseNotes.csproj", "{69944039-65C2-40E1-9D86-0608FA0C2D70}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Tgstation.Server.LogoGenerator", "tools\Tgstation.Server.LogoGenerator\Tgstation.Server.LogoGenerator.csproj", "{7F7FCFDF-271D-45C2-830C-BCCB19C57077}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -512,6 +521,18 @@ Global {69944039-65C2-40E1-9D86-0608FA0C2D70}.ReleaseNoWindows|Any CPU.Build.0 = Release|Any CPU {69944039-65C2-40E1-9D86-0608FA0C2D70}.ReleaseNoWix|Any CPU.ActiveCfg = Release|Any CPU {69944039-65C2-40E1-9D86-0608FA0C2D70}.ReleaseNoWix|Any CPU.Build.0 = Release|Any CPU + {7F7FCFDF-271D-45C2-830C-BCCB19C57077}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {7F7FCFDF-271D-45C2-830C-BCCB19C57077}.Debug|Any CPU.Build.0 = Debug|Any CPU + {7F7FCFDF-271D-45C2-830C-BCCB19C57077}.DebugNoWindows|Any CPU.ActiveCfg = Debug|Any CPU + {7F7FCFDF-271D-45C2-830C-BCCB19C57077}.DebugNoWindows|Any CPU.Build.0 = Debug|Any CPU + {7F7FCFDF-271D-45C2-830C-BCCB19C57077}.DebugNoWix|Any CPU.ActiveCfg = Debug|Any CPU + {7F7FCFDF-271D-45C2-830C-BCCB19C57077}.DebugNoWix|Any CPU.Build.0 = Debug|Any CPU + {7F7FCFDF-271D-45C2-830C-BCCB19C57077}.Release|Any CPU.ActiveCfg = Release|Any CPU + {7F7FCFDF-271D-45C2-830C-BCCB19C57077}.Release|Any CPU.Build.0 = Release|Any CPU + {7F7FCFDF-271D-45C2-830C-BCCB19C57077}.ReleaseNoWindows|Any CPU.ActiveCfg = Release|Any CPU + {7F7FCFDF-271D-45C2-830C-BCCB19C57077}.ReleaseNoWindows|Any CPU.Build.0 = Release|Any CPU + {7F7FCFDF-271D-45C2-830C-BCCB19C57077}.ReleaseNoWix|Any CPU.ActiveCfg = Release|Any CPU + {7F7FCFDF-271D-45C2-830C-BCCB19C57077}.ReleaseNoWix|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE @@ -549,6 +570,7 @@ Global {82996E9D-5E63-4CC5-8179-612C71B4F56C} = {506B9092-AF88-4DA2-84FD-C11646B695B0} {9E66C6F1-E235-4979-83B5-C2B3FDAD6E01} = {506B9092-AF88-4DA2-84FD-C11646B695B0} {69944039-65C2-40E1-9D86-0608FA0C2D70} = {A55C1117-5808-4AB2-BEA6-4D4A3E66A2F2} + {7F7FCFDF-271D-45C2-830C-BCCB19C57077} = {A55C1117-5808-4AB2-BEA6-4D4A3E66A2F2} EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {DFD36C95-3E49-41C7-ACDB-86BAF5B18A79} diff --git a/tools/Tgstation.Server.LogoGenerator/Program.cs b/tools/Tgstation.Server.LogoGenerator/Program.cs new file mode 100644 index 0000000000..2ec845f18d --- /dev/null +++ b/tools/Tgstation.Server.LogoGenerator/Program.cs @@ -0,0 +1,34 @@ +// Simplest app 2024 +using System; +using System.Drawing; +using System.Drawing.Imaging; +using System.IO; +using System.Text; + +using Svg; + +var svgContents = await File.ReadAllTextAsync("build/logo.svg"); +var whiteBgSvgContents = svgContents + .Replace("