diff --git a/.github/CONTRIBUTING.md b/.github/CONTRIBUTING.md index f116b2259d..54178ff953 100644 --- a/.github/CONTRIBUTING.md +++ b/.github/CONTRIBUTING.md @@ -50,6 +50,9 @@ You must also have the following environment variables set. To run them more acc - `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. +- (Optional) `TGS_TEST_BYOND_ZIP_DOWNLOAD_TEMPLATE`: Template URL for downloading BYOND zip files from a non-official mirror. +- (Optional) `TGS_TEST_BYOND_MIRROR_VERSION_TXT`: version.txt for a BYOND zip mirror. Requires `TGS_TEST_BYOND_ZIP_DOWNLOAD_TEMPLATE`. +- (Optional) `TGS_TEST_BYOND_ZIPS_BASE_PATH`: Directory on disk to cache BYOND zip files. - (Optional) The following variables are all interdependent, so if one is set they all must be. - `TGS_TEST_DISCORD_TOKEN`: To a valid discord bot token. - `TGS_TEST_DISCORD_CHANNEL`: To a valid discord channel ID that the above bot can access. diff --git a/.github/workflows/ci-pipeline.yml b/.github/workflows/ci-pipeline.yml index fe0b463dce..4651095d0a 100644 --- a/.github/workflows/ci-pipeline.yml +++ b/.github/workflows/ci-pipeline.yml @@ -42,6 +42,9 @@ env: TGS_NODE_VERSION: 20.x TGS_TEST_GITHUB_TOKEN: ${{ secrets.LIVE_TESTS_TOKEN }} PACKAGING_PRIVATE_KEY_PASSPHRASE: ${{ secrets.PACKAGING_PRIVATE_KEY_PASSPHRASE }} + TGS_TEST_BYOND_ZIP_DOWNLOAD_TEMPLATE: https://mocha.affectedarc07.co.uk/tgs_byond_mirrors/${Major}.${Minor}_byond${Linux:_linux}.zip + TGS_TEST_BYOND_MIRROR_VERSION_TXT: https://mocha.affectedarc07.co.uk/tgs_byond_mirrors/version.txt + DMAPI_BYOND_BUILD_MIRROR_DOWNLOAD_TEMPLATE: https://mocha.affectedarc07.co.uk/tgs_byond_mirrors/${FULL_VERSION}_byond_linux.zip # Available vars are $BYOND_MAJOR (i.e. 516) and $FULL_VERSION (i.e. 516.1666) concurrency: group: "ci-${{ (github.event_name != 'push' && github.event_name != 'schedule' && github.event.inputs.pull_request_number) || github.run_id }}-${{ github.event_name }}" @@ -90,6 +93,56 @@ jobs: name: release_notes_bins path: ./release_notes_bins/ + validate-nix-flake: + name: Validate Nix Flake + needs: start-gate + runs-on: ubuntu-latest + env: + TEST_TGS_VERSION: "6.17.0" # Version we use here doesn't matter as it won't be executed. Just used to download a zip and calc hash + steps: + - name: Install Native Packages # Name checked in rerunFlakyTests.js + run: | + sudo apt-get update + sudo apt-get install -y xmlstarlet + + - name: Setup Nix + uses: cachix/install-nix-action@v31 + with: + nix_path: nixpkgs=channel:nixos-unstable + + - 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/${{ inputs.pull_request_number }}/merge" + + - name: Replace current TGS version with test version + run: | + CURRENT_TGS_VERSION="$(xmlstarlet sel -N X="http://schemas.microsoft.com/developer/msbuild/2003" --template --value-of /X:Project/X:PropertyGroup/X:TgsCoreVersion build/Version.props)" + sed -i -e "s/${CURRENT_TGS_VERSION}/${{ env.TEST_TGS_VERSION }}/g" build/Version.props + + - name: Retrieve ServerConsole.zip Artifact + run: | + mkdir release + curl -L https://github.com/tgstation/tgstation-server/releases/download/tgstation-server-v${{ env.TEST_TGS_VERSION }}/ServerConsole.zip -f -o release/ServerConsole.zip + + - name: Regenerate Nix Hash + run: | + nix hash path ./release > build/package/nix/ServerConsole.sha256 + cat build/package/nix/ServerConsole.sha256 + + - name: Cleanup Download + run: rm -rf ./release + + - name: Check Flake + run: | + cd build/package/nix + nix flake check + code-scanning: name: Run CodeQL needs: start-gate @@ -164,21 +217,14 @@ 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 libcurl4:i386 - - name: Cache BYOND .zips - uses: actions/cache@v4 - id: cache-byond - with: - path: ~/byond-zips-cache - key: byond-zips - - - name: Setup BYOND Cache if Necessary and Install + - name: Evaluate BYOND version + id: version_evaluation run: | - echo "Setting up BYOND." FULL_VERSION=${{ matrix.byond }} if [[ "$FULL_VERSION" = "EDGE" ]] ; then - VERSIONS=$(curl https://www.byond.com/download/version.txt) + VERSIONS=$(curl https://spacestation13.github.io/byond-builds/version.txt) FULL_VERSION=$(echo "$VERSIONS" | tail -n1) echo "EDGE version evaluated to $FULL_VERSION" @@ -190,14 +236,31 @@ jobs: FULL_VERSION=${bad_linux_releases[$FULL_VERSION]} fi fi - if [[ ! -f $HOME/byond-zips-cache/linux/$FULL_VERSION.zip ]] ; then + echo "EVALUATED_VERSION=$FULL_VERSION" >> $GITHUB_OUTPUT + + - name: Cache BYOND .zips + uses: actions/cache@v4 + id: cache-byond + with: + path: ~/byond-zips-cache/linux/${{ steps.version_evaluation.outputs.EVALUATED_VERSION }} + key: byond-zips-linux-${{ steps.version_evaluation.outputs.EVALUATED_VERSION }} + + - name: Setup BYOND Cache if Necessary and Install + run: | + echo "Setting up BYOND." + FULL_VERSION=${{ steps.version_evaluation.outputs.EVALUATED_VERSION }} + if [[ ! -f $HOME/byond-zips-cache/linux/$FULL_VERSION/$FULL_VERSION.zip ]] ; then BYOND_MAJOR=${FULL_VERSION%.*} - mkdir -p $HOME/byond-zips-cache/linux - curl "https://www.byond.com/download/build/$BYOND_MAJOR/${FULL_VERSION}_byond_linux.zip" -o $HOME/byond-zips-cache/linux/$FULL_VERSION.zip + mkdir -p $HOME/byond-zips-cache/linux/$FULL_VERSION + DOWNLOAD_URL="${{ env.DMAPI_BYOND_BUILD_MIRROR_DOWNLOAD_TEMPLATE }}" + if [[ "${{ matrix.byond }}" = "EDGE" ]] ; then + DOWNLOAD_URL="https://spacestation13.github.io/byond-builds/$BYOND_MAJOR/${FULL_VERSION}_byond_linux.zip" + fi + curl "$DOWNLOAD_URL" -o $HOME/byond-zips-cache/linux/$FULL_VERSION/$FULL_VERSION.zip fi mkdir -p "$HOME/BYOND" cd "$HOME/BYOND" - cp $HOME/byond-zips-cache/linux/$FULL_VERSION.zip byond.zip + cp $HOME/byond-zips-cache/linux/$FULL_VERSION/$FULL_VERSION.zip byond.zip unzip byond.zip cd byond make here @@ -509,13 +572,6 @@ jobs: if: always() run: rm -f ${{ env.TGS_TELEMETRY_KEY_FILE }} - - name: Cache BYOND .zips - uses: actions/cache@v4 - id: cache-byond - with: - path: ~/byond-zips-cache - key: byond-zips - - name: Run Unit Tests 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 env: @@ -578,13 +634,6 @@ jobs: if: always() run: rm -f ${{ env.TGS_TELEMETRY_KEY_FILE }} - - name: Cache BYOND .zips - uses: actions/cache@v4 - id: cache-byond - with: - path: ~/byond-zips-cache - key: byond-zips - - name: Run Unit Tests 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 env: @@ -598,9 +647,50 @@ jobs: name: windows-unit-test-coverage-${{ matrix.configuration }} path: ./TestResults/ + prep-edge-versions: + name: Prepare Live Tests Cache of EDGE Versions + needs: start-gate + runs-on: ubuntu-latest + steps: + - name: Cache BYOND .zips (Linux) + uses: actions/cache@v4 + with: + path: ~/byond-zips-cache/live/linux + key: byond-zips-linux-live + + - name: Cache BYOND .zips (Windows) + uses: actions/cache@v4 + with: + path: ~/byond-zips-cache/live/windows + key: byond-zips-windows-live + + - name: Evaluate BYOND version + id: version_evaluation + run: | + VERSIONS=$(curl https://spacestation13.github.io/byond-builds/version.txt) + FULL_VERSION=$(echo "$VERSIONS" | tail -n1) + echo "EDGE version evaluated to $FULL_VERSION" + echo "EVALUATED_VERSION=$FULL_VERSION" >> $GITHUB_OUTPUT + + - name: Setup BYOND Cache if Necessary and Install + run: | + echo "Downloading BYOND." + FULL_VERSION=${{ steps.version_evaluation.outputs.EVALUATED_VERSION }} + BYOND_MAJOR=${FULL_VERSION%.*} + if [[ ! -f $HOME/byond-zips-cache/live/linux/$FULL_VERSION/$FULL_VERSION.zip ]] ; then + mkdir -p $HOME/byond-zips-cache/live/linux/$FULL_VERSION + DOWNLOAD_URL="https://spacestation13.github.io/byond-builds/$BYOND_MAJOR/${FULL_VERSION}_byond_linux.zip" + curl "$DOWNLOAD_URL" -o $HOME/byond-zips-cache/live/linux/$FULL_VERSION/$FULL_VERSION.zip + fi + if [[ ! -f $HOME/byond-zips-cache/live/windows/$FULL_VERSION/$FULL_VERSION.zip ]] ; then + mkdir -p $HOME/byond-zips-cache/live/windows/$FULL_VERSION + DOWNLOAD_URL="https://spacestation13.github.io/byond-builds/$BYOND_MAJOR/${FULL_VERSION}_byond.zip" + curl "$DOWNLOAD_URL" -o $HOME/byond-zips-cache/live/windows/$FULL_VERSION/$FULL_VERSION.zip + fi + windows-integration-tests: name: Windows Live Tests - needs: [dmapi-build, opendream-build] + needs: [dmapi-build, opendream-build, prep-edge-versions] strategy: fail-fast: false matrix: @@ -633,6 +723,12 @@ jobs: with: node-version: ${{ env.TGS_NODE_VERSION }} + - name: Cache BYOND .zips + uses: actions/cache@v4 + with: + path: ~/byond-zips-cache/live/windows + key: byond-zips-windows-live + - name: Set TGS_TEST_DUMP_API_SPEC if: ${{ matrix.configuration == 'Release' && matrix.watchdog-type == 'Advanced' && matrix.database-type == 'SqlServer' }} run: echo "TGS_TEST_DUMP_API_SPEC=yes" >> $Env:GITHUB_ENV @@ -717,13 +813,6 @@ jobs: if: always() run: rm -f ${{ env.TGS_TELEMETRY_KEY_FILE }} - - name: Cache BYOND .zips - uses: actions/cache@v4 - id: cache-byond - with: - path: ~/byond-zips-cache - key: byond-zips - - name: Run Live Tests # Logging here is weird because printing massive amounts of text on Windows runners is SLOW AS SHIT!!! id: live-tests shell: bash @@ -816,7 +905,7 @@ jobs: linux-integration-tests: name: Linux Live Tests - needs: [dmapi-build, opendream-build] + needs: [dmapi-build, opendream-build, prep-edge-versions] services: # We start all dbs here so we can just code the stuff once mssql: image: ${{ (matrix.database-type == 'SqlServer') && 'mcr.microsoft.com/mssql/server:2019-latest' || '' }} @@ -884,13 +973,19 @@ 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 libgdiplus + sudo apt-get install -y -o APT::Immediate-Configure=0 libc6-i386 libstdc++6:i386 gdb libgcc-s1:i386 libgdiplus libcurl4:i386 - name: Setup Node.JS uses: actions/setup-node@v4 with: node-version: ${{ env.TGS_NODE_VERSION }} + - name: Cache BYOND .zips + uses: actions/cache@v4 + with: + path: ~/byond-zips-cache/live/linux + key: byond-zips-linux-live + - name: Set Sqlite Connection Info if: ${{ matrix.database-type == 'Sqlite' }} run: | @@ -945,13 +1040,6 @@ jobs: if: always() run: rm -f ${{ env.TGS_TELEMETRY_KEY_FILE }} - - name: Cache BYOND .zips - uses: actions/cache@v4 - id: cache-byond - with: - path: ~/byond-zips-cache - key: byond-zips - - name: Run Live Tests run: | cd tests/Tgstation.Server.Tests @@ -1657,6 +1745,7 @@ jobs: check-winget-pr-template, efcore-version-match, code-scanning, + validate-nix-flake, ] runs-on: ubuntu-latest steps: diff --git a/.github/workflows/nix-deployment.yml b/.github/workflows/nix-deployment.yml index 16c62fe453..470913346e 100644 --- a/.github/workflows/nix-deployment.yml +++ b/.github/workflows/nix-deployment.yml @@ -15,7 +15,7 @@ jobs: sudo apt-get install -y xmlstarlet - name: Setup Nix - uses: cachix/install-nix-action@v30 + uses: cachix/install-nix-action@v31 with: nix_path: nixpkgs=channel:nixos-unstable diff --git a/README.md b/README.md index 8d0ac955b6..9b3eb0ad9b 100644 --- a/README.md +++ b/README.md @@ -639,7 +639,7 @@ Bots have a set of built-in commands that can be triggered via `!tgs`, mentionin All files in game code deployments are considered transient by default, meaning when new code is deployed, changes will be lost. Static files allow you to specify which files and folders stick around throughout all deployments. -The `StaticFiles` folder contains 3 root folders which cannot be deleted and operate under special rules +The `Configuration` folder contains 3 root folders which cannot be deleted and operate under special rules - `CodeModifications` - `EventScripts` - `GameStaticFiles` diff --git a/build/Dockerfile b/build/Dockerfile index e7cb447c2b..8a3a1d24f7 100644 --- a/build/Dockerfile +++ b/build/Dockerfile @@ -59,11 +59,13 @@ RUN export TGS_TELEMETRY_KEY_FILE="../../${TGS_TELEMETRY_KEY_FILE}" \ FROM mcr.microsoft.com/dotnet/aspnet:8.0-bookworm-slim #needed for byond, curl for healthchecks -RUN apt-get update \ +RUN dpkg --add-architecture i386 \ + && apt-get update \ && apt-get install -y \ gcc-multilib \ gdb \ curl \ + libcurl4:i386 \ && rm -rf /var/lib/apt/lists/* EXPOSE 5000 diff --git a/build/TestCommon.props b/build/TestCommon.props index 52ba145ada..8a6c5d675f 100644 --- a/build/TestCommon.props +++ b/build/TestCommon.props @@ -13,14 +13,14 @@ runtime; build; native; contentfiles; analyzers; buildtransitive - + - + - + diff --git a/build/Version.props b/build/Version.props index bebeff1790..747d368b68 100644 --- a/build/Version.props +++ b/build/Version.props @@ -3,8 +3,8 @@ - 6.17.0 - 5.7.0 + 6.18.0 + 5.8.0 10.13.0 0.6.0 7.0.0 diff --git a/build/package/deb/build_package.sh b/build/package/deb/build_package.sh index 29d828f1bb..58ee50cdfe 100755 --- a/build/package/deb/build_package.sh +++ b/build/package/deb/build_package.sh @@ -22,11 +22,6 @@ apt-get install -y \ 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 -dpkg -i ./packages-microsoft-prod.deb -rm packages-microsoft-prod.deb - # https://github.com/nodesource/distributions mkdir -p /etc/apt/keyrings curl -fsSL https://deb.nodesource.com/gpgkey/nodesource-repo.gpg.key | gpg --dearmor -o /etc/apt/keyrings/nodesource.gpg diff --git a/build/package/deb/debian/control b/build/package/deb/debian/control index 67356359b6..7ae12387a4 100644 --- a/build/package/deb/debian/control +++ b/build/package/deb/debian/control @@ -21,6 +21,7 @@ Depends: libstdc++6:i386 [amd64], libstdc++6 [i386], gcc-multilib [amd64], + libcurl4 [i386], Recommends: libsystemd0, gdb, diff --git a/build/package/nix/flake.nix b/build/package/nix/flake.nix index f2bf8c14a9..52ef59cd67 100644 --- a/build/package/nix/flake.nix +++ b/build/package/nix/flake.nix @@ -3,11 +3,12 @@ inputs = {}; - outputs = { ... }: { + outputs = { nixpkgs, ... }: { nixosModules = { default = { ... }: { imports = [ ./tgstation-server.nix ]; }; }; + checks.x86_64-linux.package-build = nixpkgs.legacyPackages.x86_64-linux.callPackage ./package.nix { }; }; } diff --git a/build/package/nix/package.nix b/build/package/nix/package.nix index 848ebe91f8..9deeba9fb8 100644 --- a/build/package/nix/package.nix +++ b/build/package/nix/package.nix @@ -86,6 +86,7 @@ stdenv.mkDerivation { gcc_multi glibc bash + curl ]; nativeBuildInputs = with pkgs; [ makeWrapper diff --git a/src/Tgstation.Server.Api/Tgstation.Server.Api.csproj b/src/Tgstation.Server.Api/Tgstation.Server.Api.csproj index 0e83f44424..6f32743090 100644 --- a/src/Tgstation.Server.Api/Tgstation.Server.Api.csproj +++ b/src/Tgstation.Server.Api/Tgstation.Server.Api.csproj @@ -28,7 +28,7 @@ - + diff --git a/src/Tgstation.Server.Client.GraphQL/Tgstation.Server.Client.GraphQL.csproj b/src/Tgstation.Server.Client.GraphQL/Tgstation.Server.Client.GraphQL.csproj index d5b8544c62..67e9d74754 100644 --- a/src/Tgstation.Server.Client.GraphQL/Tgstation.Server.Client.GraphQL.csproj +++ b/src/Tgstation.Server.Client.GraphQL/Tgstation.Server.Client.GraphQL.csproj @@ -9,8 +9,7 @@ - - + diff --git a/src/Tgstation.Server.Client/Tgstation.Server.Client.csproj b/src/Tgstation.Server.Client/Tgstation.Server.Client.csproj index 83d41dfff1..f681ecff8f 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.Host.Console/Tgstation.Server.Host.Console.csproj b/src/Tgstation.Server.Host.Console/Tgstation.Server.Host.Console.csproj index 4942cfd2ff..75f657b562 100644 --- a/src/Tgstation.Server.Host.Console/Tgstation.Server.Host.Console.csproj +++ b/src/Tgstation.Server.Host.Console/Tgstation.Server.Host.Console.csproj @@ -13,9 +13,9 @@ - + - + diff --git a/src/Tgstation.Server.Host.Service/Tgstation.Server.Host.Service.csproj b/src/Tgstation.Server.Host.Service/Tgstation.Server.Host.Service.csproj index 6a31f318db..59ab045d5a 100644 --- a/src/Tgstation.Server.Host.Service/Tgstation.Server.Host.Service.csproj +++ b/src/Tgstation.Server.Host.Service/Tgstation.Server.Host.Service.csproj @@ -21,21 +21,21 @@ - + - + - + - + - + diff --git a/src/Tgstation.Server.Host.Utils.GitLab.GraphQL/Tgstation.Server.Host.Utils.GitLab.GraphQL.csproj b/src/Tgstation.Server.Host.Utils.GitLab.GraphQL/Tgstation.Server.Host.Utils.GitLab.GraphQL.csproj index 41df3014e0..ab2f3f9dbc 100644 --- a/src/Tgstation.Server.Host.Utils.GitLab.GraphQL/Tgstation.Server.Host.Utils.GitLab.GraphQL.csproj +++ b/src/Tgstation.Server.Host.Utils.GitLab.GraphQL/Tgstation.Server.Host.Utils.GitLab.GraphQL.csproj @@ -38,7 +38,8 @@ - + + diff --git a/src/Tgstation.Server.Host.Utils.GitLab.GraphQL/yarn.lock b/src/Tgstation.Server.Host.Utils.GitLab.GraphQL/yarn.lock index d1d93cd55d..5e4dca122d 100644 --- a/src/Tgstation.Server.Host.Utils.GitLab.GraphQL/yarn.lock +++ b/src/Tgstation.Server.Host.Utils.GitLab.GraphQL/yarn.lock @@ -1855,14 +1855,15 @@ __metadata: linkType: hard "form-data@npm:^4.0.0": - version: 4.0.2 - resolution: "form-data@npm:4.0.2" + version: 4.0.4 + resolution: "form-data@npm:4.0.4" dependencies: asynckit: "npm:^0.4.0" combined-stream: "npm:^1.0.8" es-set-tostringtag: "npm:^2.1.0" + hasown: "npm:^2.0.2" mime-types: "npm:^2.1.12" - checksum: 10/82c65b426af4a40090e517a1bc9057f76970b4c6043e37aa49859c447d88553e77d4cc5626395079a53d2b0889ba5f2a49f3900db3ad3f3f1bf76613532572fb + checksum: 10/a4b62e21932f48702bc468cc26fb276d186e6b07b557e3dd7cc455872bdbb82db7db066844a64ad3cf40eaf3a753c830538183570462d3649fdfd705601cbcfb languageName: node linkType: hard diff --git a/src/Tgstation.Server.Host.Watchdog/Tgstation.Server.Host.Watchdog.csproj b/src/Tgstation.Server.Host.Watchdog/Tgstation.Server.Host.Watchdog.csproj index af47f499f3..5f31ab855f 100644 --- a/src/Tgstation.Server.Host.Watchdog/Tgstation.Server.Host.Watchdog.csproj +++ b/src/Tgstation.Server.Host.Watchdog/Tgstation.Server.Host.Watchdog.csproj @@ -10,7 +10,7 @@ - + diff --git a/src/Tgstation.Server.Host/.config/dotnet-tools.json b/src/Tgstation.Server.Host/.config/dotnet-tools.json index 76e6bce7b3..7cf60e92f7 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": "9.0.4", + "version": "9.0.7", "commands": [ "dotnet-ef" ] diff --git a/src/Tgstation.Server.Host/Components/Chat/ChatManager.cs b/src/Tgstation.Server.Host/Components/Chat/ChatManager.cs index 5faa30dbd2..2aeca4f8f3 100644 --- a/src/Tgstation.Server.Host/Components/Chat/ChatManager.cs +++ b/src/Tgstation.Server.Host/Components/Chat/ChatManager.cs @@ -351,28 +351,11 @@ namespace Tgstation.Server.Host.Components.Chat /// public void QueueWatchdogMessage(string message) - { - ArgumentNullException.ThrowIfNull(message); + => QueueMessageGeneric(mapping => mapping.IsWatchdogChannel, message, "WD"); - message = String.Format(CultureInfo.InvariantCulture, "WD: {0}", message); - - if (!initialProviderConnectionsTask!.IsCompleted) - logger.LogTrace("Waiting for initial provider connections before sending watchdog message..."); - - // Reimplementing QueueMessage - QueueMessageInternal( - new MessageContent - { - Text = message, - }, - () => - { - // so it doesn't change while we're using it - lock (mappedChannels) - return mappedChannels.Where(x => x.Value.IsWatchdogChannel).Select(x => x.Key).ToList(); - }, - true); - } + /// + public void QueueRawDeploymentMessage(string message) + => QueueMessageGeneric(mapping => mapping.IsUpdatesChannel, message, null); /// public Func> QueueDeploymentMessage( @@ -1111,5 +1094,38 @@ namespace Tgstation.Server.Host.Components.Chat AddMessageTask(SendMessageTask()); } + + /// + /// Queues a message to a selected set of s. + /// + /// A for selecting the s to send to. + /// The message to send. + /// The optional prefix to the message to be sent. + void QueueMessageGeneric(Predicate channelSelector, string message, string? prefix) + { + ArgumentNullException.ThrowIfNull(message); + + if (prefix != null) + { + message = $"{prefix}: {message}"; + } + + if (!initialProviderConnectionsTask!.IsCompleted) + logger.LogTrace("Waiting for initial provider connections before sending chat message..."); + + // Reimplementing QueueMessage + QueueMessageInternal( + new MessageContent + { + Text = message, + }, + () => + { + // so it doesn't change while we're using it + lock (mappedChannels) + return mappedChannels.Where(x => channelSelector(x.Value)).Select(x => x.Key).ToList(); + }, + true); + } } } diff --git a/src/Tgstation.Server.Host/Components/Chat/IChatManager.cs b/src/Tgstation.Server.Host/Components/Chat/IChatManager.cs index 669f21b305..4f112afb69 100644 --- a/src/Tgstation.Server.Host/Components/Chat/IChatManager.cs +++ b/src/Tgstation.Server.Host/Components/Chat/IChatManager.cs @@ -57,6 +57,12 @@ namespace Tgstation.Server.Host.Components.Chat /// The message being sent. void QueueWatchdogMessage(string message); + /// + /// Queue a chat to configured deployment channels. + /// + /// The message being sent. + void QueueRawDeploymentMessage(string message); + /// /// Send the message for a deployment to configured deployment channels. /// diff --git a/src/Tgstation.Server.Host/Components/Deployment/DmbFactory.cs b/src/Tgstation.Server.Host/Components/Deployment/DmbFactory.cs index 8c523d97d5..a9cd217f18 100644 --- a/src/Tgstation.Server.Host/Components/Deployment/DmbFactory.cs +++ b/src/Tgstation.Server.Host/Components/Deployment/DmbFactory.cs @@ -1,7 +1,6 @@ using System; using System.Collections.Generic; using System.Diagnostics.CodeAnalysis; -using System.IO; using System.Linq; using System.Runtime.CompilerServices; using System.Text; @@ -431,7 +430,7 @@ namespace Tgstation.Server.Host.Components.Deployment // Don't dispose it logger.LogDebug("Creating legacy two folder .dmb provider targeting {aDirName} directory...", LegacyADirectoryName); #pragma warning disable CA2000 // Dispose objects before losing scope (false positive) - newProvider = new DmbProvider(compileJob, engineVersion, ioManager, new DisposeInvoker(CleanupAction), Path.DirectorySeparatorChar + LegacyADirectoryName); + newProvider = new DmbProvider(compileJob, engineVersion, ioManager, new DisposeInvoker(CleanupAction), LegacyADirectoryName); #pragma warning restore CA2000 // Dispose objects before losing scope } diff --git a/src/Tgstation.Server.Host/Components/Deployment/DmbProvider.cs b/src/Tgstation.Server.Host/Components/Deployment/DmbProvider.cs index be47aeb6b3..4da03c795e 100644 --- a/src/Tgstation.Server.Host/Components/Deployment/DmbProvider.cs +++ b/src/Tgstation.Server.Host/Components/Deployment/DmbProvider.cs @@ -12,7 +12,18 @@ namespace Tgstation.Server.Host.Components.Deployment sealed class DmbProvider : DmbProviderBase, IDmbProvider { /// - public override string Directory => ioManager.ResolvePath(CompileJob.DirectoryName!.Value.ToString() + directoryAppend); + public override string Directory + { + get + { + var stringifiedCompileJobDirectory = CompileJob.DirectoryName!.Value.ToString(); + + if (directoryAppend != null) + stringifiedCompileJobDirectory = ioManager.ConcatPath(stringifiedCompileJobDirectory, directoryAppend); + + return ioManager.ResolvePath(stringifiedCompileJobDirectory); + } + } /// public override Models.CompileJob CompileJob { get; } @@ -28,7 +39,7 @@ namespace Tgstation.Server.Host.Components.Deployment /// /// Extra path to add to the end of . /// - readonly string directoryAppend; + readonly string? directoryAppend; /// /// The to run when is called. @@ -49,7 +60,7 @@ namespace Tgstation.Server.Host.Components.Deployment EngineVersion = engineVersion ?? throw new ArgumentNullException(nameof(engineVersion)); this.ioManager = ioManager ?? throw new ArgumentNullException(nameof(ioManager)); this.onDispose = onDispose ?? throw new ArgumentNullException(nameof(onDispose)); - this.directoryAppend = directoryAppend ?? String.Empty; + this.directoryAppend = directoryAppend; } /// diff --git a/src/Tgstation.Server.Host/Components/Deployment/HardLinkDmbProvider.cs b/src/Tgstation.Server.Host/Components/Deployment/HardLinkDmbProvider.cs index ed6e72b4b6..df3c9cf2a1 100644 --- a/src/Tgstation.Server.Host/Components/Deployment/HardLinkDmbProvider.cs +++ b/src/Tgstation.Server.Host/Components/Deployment/HardLinkDmbProvider.cs @@ -3,6 +3,7 @@ using System.Collections.Generic; using System.Diagnostics; using System.Globalization; using System.IO; +using System.IO.Abstractions; using System.Runtime.Versioning; using System.Threading; using System.Threading.Tasks; @@ -187,8 +188,10 @@ namespace Tgstation.Server.Host.Components.Deployment dest = IOManager.ResolvePath(mirrorGuid.ToString()); using var semaphore = taskThrottle.HasValue ? new SemaphoreSlim(taskThrottle.Value) : null; + + var dir = await IOManager.DirectoryInfo(src, cancellationToken); await Task.WhenAll(MirrorDirectoryImpl( - src, + dir, dest, semaphore, securityLevel, @@ -230,21 +233,20 @@ namespace Tgstation.Server.Host.Components.Deployment /// /// Recursively create tasks to create a hard link directory mirror of to . /// - /// The source directory path. + /// The source . /// 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 . + /// An 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, DreamDaemonSecurity securityLevel, CancellationToken cancellationToken) + IEnumerable MirrorDirectoryImpl(IDirectoryInfo src, string dest, SemaphoreSlim? semaphore, DreamDaemonSecurity securityLevel, CancellationToken cancellationToken) { - var dir = new DirectoryInfo(src); Task? subdirCreationTask = null; var dreamDaemonWillAcceptOutOfDirectorySymlinks = securityLevel == DreamDaemonSecurity.Trusted; - foreach (var subDirectory in dir.EnumerateDirectories()) + foreach (var subDirectory in src.EnumerateDirectories()) { - var mirroredName = Path.Combine(dest, subDirectory.Name); + var mirroredName = IOManager.ConcatPath(dest, subDirectory.Name); // check if we are a symbolic link if (subDirectory.Attributes.HasFlag(FileAttributes.ReparsePoint)) @@ -275,7 +277,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, securityLevel, cancellationToken)) + foreach (var copyTask in MirrorDirectoryImpl(subDirectory, mirroredName, semaphore, securityLevel, cancellationToken)) { if (subdirCreationTask == null) { @@ -289,7 +291,7 @@ namespace Tgstation.Server.Host.Components.Deployment } } - foreach (var fileInfo in dir.EnumerateFiles()) + foreach (var fileInfo in src.EnumerateFiles()) { if (subdirCreationTask == null) { diff --git a/src/Tgstation.Server.Host/Components/Engine/ByondInstallerBase.cs b/src/Tgstation.Server.Host/Components/Engine/ByondInstallerBase.cs index e02c109e6b..41f657f10f 100644 --- a/src/Tgstation.Server.Host/Components/Engine/ByondInstallerBase.cs +++ b/src/Tgstation.Server.Host/Components/Engine/ByondInstallerBase.cs @@ -6,8 +6,10 @@ using System.Threading; using System.Threading.Tasks; using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Options; using Tgstation.Server.Api.Models; +using Tgstation.Server.Host.Configuration; using Tgstation.Server.Host.IO; using Tgstation.Server.Host.Jobs; @@ -41,6 +43,11 @@ namespace Tgstation.Server.Host.Components.Engine /// protected override EngineType TargetEngineType => EngineType.Byond; + /// + /// The for the . + /// + protected IOptionsMonitor GeneralConfigurationOptions { get; } + /// /// Path to the system user's local BYOND folder. /// @@ -52,25 +59,85 @@ namespace Tgstation.Server.Host.Components.Engine protected abstract string DreamMakerName { get; } /// - /// Gets the URL formatter string for downloading a byond version of {0:Major} {1:Minor}. + /// Template to do ${Marker:xxx} replacements in . /// - protected abstract string ByondRevisionsUrlTemplate { get; } + protected abstract string OSMarkerTemplate { get; } /// /// The for the . /// readonly IFileDownloader fileDownloader; + /// + /// Format a given . + /// + /// The BYOND version to download. + /// The template. + /// The . + /// The formatted byond download . + /// Exposed only for testability. + internal static Uri GetDownloadZipUrl(Version semver, string byondZipDownloadTemplate, string osMarkerTemplate) + { + // god forbid + var guardGuid = Guid.NewGuid(); + + var url = byondZipDownloadTemplate + .Replace("$$", guardGuid.ToString(), StringComparison.Ordinal) + .Replace("${Major}", semver.Major.ToString(CultureInfo.InvariantCulture), StringComparison.Ordinal) + .Replace("${Minor}", semver.Minor.ToString(CultureInfo.InvariantCulture), StringComparison.Ordinal); + + var osMarkerPrefix = $"${{{osMarkerTemplate}:"; + var osMarkerIndex = url.IndexOf(osMarkerPrefix); + while (osMarkerIndex != -1) + { + var start = osMarkerIndex + osMarkerPrefix.Length; + var end = url.IndexOf('}', start); + if (end == -1) + break; + + var substitution = url.Substring(start, end - start); + url = url.Replace($"{osMarkerPrefix}{substitution}}}", substitution, StringComparison.Ordinal); + + osMarkerIndex = url.IndexOf(osMarkerPrefix); + } + + // at this point, any other substitution attempts should be removed + var otherMarkerPrefix = "${"; + var otherMarkerIndex = url.IndexOf(otherMarkerPrefix); + while (otherMarkerIndex != -1) + { + var start = otherMarkerIndex + otherMarkerPrefix.Length; + var end = url.IndexOf('}', start); + if (end == -1) + break; + + var substitution = url.Substring(start, end - start); + url = url.Replace($"{otherMarkerPrefix}{substitution}}}", String.Empty, StringComparison.Ordinal); + + otherMarkerIndex = url.IndexOf(otherMarkerPrefix); + } + + url = url.Replace(guardGuid.ToString(), "$", StringComparison.Ordinal); + + return new Uri(url); + } + /// /// Initializes a new instance of the class. /// /// The for the . /// The for the . /// The value of . - protected ByondInstallerBase(IIOManager ioManager, ILogger logger, IFileDownloader fileDownloader) + /// The value of . + protected ByondInstallerBase( + IIOManager ioManager, + ILogger logger, + IFileDownloader fileDownloader, + IOptionsMonitor generalConfigurationOptions) : base(ioManager, logger) { this.fileDownloader = fileDownloader ?? throw new ArgumentNullException(nameof(fileDownloader)); + GeneralConfigurationOptions = generalConfigurationOptions ?? throw new ArgumentNullException(nameof(generalConfigurationOptions)); } /// @@ -78,7 +145,7 @@ namespace Tgstation.Server.Host.Components.Engine { CheckVersionValidity(version); - var installationIOManager = new ResolvingIOManager(IOManager, path); + var installationIOManager = IOManager.CreateResolverForSubdirectory(path); var supportsMapThreads = version.Version >= MapThreadsVersion; return ValueTask.FromResult( @@ -195,8 +262,12 @@ namespace Tgstation.Server.Host.Components.Engine Uri GetDownloadZipUrl(EngineVersion version) { CheckVersionValidity(version); - var url = String.Format(CultureInfo.InvariantCulture, ByondRevisionsUrlTemplate, version.Version!.Major, version.Version.Minor); - return new Uri(url); + + var guardGuid = Guid.NewGuid(); + + var semver = version.Version!; + var template = GeneralConfigurationOptions.CurrentValue.ByondZipDownloadTemplate; + return GetDownloadZipUrl(semver, template, OSMarkerTemplate); } } } diff --git a/src/Tgstation.Server.Host/Components/Engine/OpenDreamInstaller.cs b/src/Tgstation.Server.Host/Components/Engine/OpenDreamInstaller.cs index ad2fb20e95..87ad2cd721 100644 --- a/src/Tgstation.Server.Host/Components/Engine/OpenDreamInstaller.cs +++ b/src/Tgstation.Server.Host/Components/Engine/OpenDreamInstaller.cs @@ -126,7 +126,7 @@ namespace Tgstation.Server.Host.Components.Engine var dotnetPath = (await DotnetHelper.GetDotnetPath(platformIdentifier, IOManager, cancellationToken)) ?? throw new JobException("Failed to find dotnet path!"); return new OpenDreamInstallation( - new ResolvingIOManager(IOManager, path), + IOManager.CreateResolverForSubdirectory(path), asyncDelayer, httpClientFactory, dotnetPath, diff --git a/src/Tgstation.Server.Host/Components/Engine/PosixByondInstaller.cs b/src/Tgstation.Server.Host/Components/Engine/PosixByondInstaller.cs index fff62b3b3f..7e21f6a378 100644 --- a/src/Tgstation.Server.Host/Components/Engine/PosixByondInstaller.cs +++ b/src/Tgstation.Server.Host/Components/Engine/PosixByondInstaller.cs @@ -5,9 +5,11 @@ using System.Threading; using System.Threading.Tasks; using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Options; using Tgstation.Server.Api.Models; using Tgstation.Server.Common.Extensions; +using Tgstation.Server.Host.Configuration; using Tgstation.Server.Host.IO; namespace Tgstation.Server.Host.Components.Engine @@ -39,7 +41,7 @@ namespace Tgstation.Server.Host.Components.Engine protected override string DreamMakerName => DreamMakerExecutableName + ShellScriptExtension; /// - protected override string ByondRevisionsUrlTemplate => "https://www.byond.com/download/build/{0}/{0}.{1}_byond_linux.zip"; + protected override string OSMarkerTemplate => "Linux"; /// /// The for the . @@ -52,13 +54,15 @@ namespace Tgstation.Server.Host.Components.Engine /// The value of . /// The for the . /// The for the . + /// The . /// The for the . public PosixByondInstaller( IPostWriteHandler postWriteHandler, IIOManager ioManager, IFileDownloader fileDownloader, + IOptionsMonitor generalConfigurationOptions, ILogger logger) - : base(ioManager, logger, fileDownloader) + : base(ioManager, logger, fileDownloader, generalConfigurationOptions) { this.postWriteHandler = postWriteHandler ?? throw new ArgumentNullException(nameof(postWriteHandler)); diff --git a/src/Tgstation.Server.Host/Components/Engine/WindowsByondInstaller.cs b/src/Tgstation.Server.Host/Components/Engine/WindowsByondInstaller.cs index ce2c7704e2..786d8df147 100644 --- a/src/Tgstation.Server.Host/Components/Engine/WindowsByondInstaller.cs +++ b/src/Tgstation.Server.Host/Components/Engine/WindowsByondInstaller.cs @@ -69,18 +69,13 @@ namespace Tgstation.Server.Host.Components.Engine protected override string PathToUserFolder { get; } /// - protected override string ByondRevisionsUrlTemplate => "https://www.byond.com/download/build/{0}/{0}.{1}_byond.zip"; + protected override string OSMarkerTemplate => "Windows"; /// /// The for the . /// readonly IProcessExecutor processExecutor; - /// - /// The for the . - /// - readonly GeneralConfiguration generalConfiguration; - /// /// The for the . /// @@ -100,7 +95,7 @@ namespace Tgstation.Server.Host.Components.Engine /// Initializes a new instance of the class. /// /// The value of . - /// The containing the value of . + /// The containing the . /// The containing the value of . /// The for the . /// The for the . @@ -109,13 +104,12 @@ namespace Tgstation.Server.Host.Components.Engine IProcessExecutor processExecutor, IIOManager ioManager, IFileDownloader fileDownloader, - IOptions generalConfigurationOptions, + IOptionsMonitor generalConfigurationOptions, IOptions sessionConfigurationOptions, ILogger logger) - : base(ioManager, logger, fileDownloader) + : base(ioManager, logger, fileDownloader, generalConfigurationOptions) { this.processExecutor = processExecutor ?? throw new ArgumentNullException(nameof(processExecutor)); - generalConfiguration = generalConfigurationOptions?.Value ?? throw new ArgumentNullException(nameof(generalConfigurationOptions)); sessionConfiguration = sessionConfigurationOptions?.Value ?? throw new ArgumentNullException(nameof(sessionConfigurationOptions)); var useServiceSpecialTactics = Environment.Is64BitProcess && Environment.UserName == $"{Environment.MachineName}$"; @@ -150,7 +144,7 @@ namespace Tgstation.Server.Host.Components.Engine installDirectXTask, }; - if (!generalConfiguration.SkipAddingByondFirewallException) + if (!GeneralConfigurationOptions.CurrentValue.SkipAddingByondFirewallException) { var firewallTask = AddDreamDaemonToFirewall(version, path, deploymentPipelineProcesses, cancellationToken); tasks.Add(firewallTask); @@ -165,7 +159,7 @@ namespace Tgstation.Server.Host.Components.Engine CheckVersionValidity(version); ArgumentNullException.ThrowIfNull(path); - if (generalConfiguration.SkipAddingByondFirewallException) + if (GeneralConfigurationOptions.CurrentValue.SkipAddingByondFirewallException) return; if (version.Version < DDExeVersion) diff --git a/src/Tgstation.Server.Host/Components/Engine/ZipStreamEngineInstallationData.cs b/src/Tgstation.Server.Host/Components/Engine/ZipStreamEngineInstallationData.cs index 292b725981..116689f8de 100644 --- a/src/Tgstation.Server.Host/Components/Engine/ZipStreamEngineInstallationData.cs +++ b/src/Tgstation.Server.Host/Components/Engine/ZipStreamEngineInstallationData.cs @@ -18,7 +18,7 @@ namespace Tgstation.Server.Host.Components.Engine readonly IIOManager ioManager; /// - /// The containing the zip data of the engine. + /// The containing the zip data of the engine. /// readonly Stream zipStream; diff --git a/src/Tgstation.Server.Host/Components/Instance.cs b/src/Tgstation.Server.Host/Components/Instance.cs index f0fd27f661..37454b7dbc 100644 --- a/src/Tgstation.Server.Host/Components/Instance.cs +++ b/src/Tgstation.Server.Host/Components/Instance.cs @@ -531,7 +531,10 @@ namespace Tgstation.Server.Host.Components } } else if (preserveTestMerges) + { + Chat.QueueRawDeploymentMessage("Automatic update has failed due to a conflicting testmerge!"); throw new JobException(Api.Models.ErrorCode.InstanceUpdateTestMergeConflict); + } if (!preserveTestMerges) { diff --git a/src/Tgstation.Server.Host/Components/InstanceFactory.cs b/src/Tgstation.Server.Host/Components/InstanceFactory.cs index c7d08ad4b1..04994acec5 100644 --- a/src/Tgstation.Server.Host/Components/InstanceFactory.cs +++ b/src/Tgstation.Server.Host/Components/InstanceFactory.cs @@ -164,7 +164,7 @@ namespace Tgstation.Server.Host.Components /// /// The instance's . /// The for the instance's "Game" directory. - static ResolvingIOManager CreateGameIOManager(IIOManager instanceIOManager) => new(instanceIOManager, "Game"); + static IIOManager CreateGameIOManager(IIOManager instanceIOManager) => instanceIOManager.CreateResolverForSubdirectory("Game"); #pragma warning disable CA1502 // TODO: Decomplexify /// @@ -270,11 +270,11 @@ namespace Tgstation.Server.Host.Components var instanceIoManager = CreateInstanceIOManager(metadata); // various other ioManagers - var repoIoManager = new ResolvingIOManager(instanceIoManager, "Repository"); - var byondIOManager = new ResolvingIOManager(instanceIoManager, "Byond"); + var repoIoManager = instanceIoManager.CreateResolverForSubdirectory("Repository"); + var byondIOManager = instanceIoManager.CreateResolverForSubdirectory("Byond"); var gameIoManager = CreateGameIOManager(instanceIoManager); - var diagnosticsIOManager = new ResolvingIOManager(instanceIoManager, "Diagnostics"); - var configurationIoManager = new ResolvingIOManager(instanceIoManager, "Configuration"); + var diagnosticsIOManager = instanceIoManager.CreateResolverForSubdirectory("Diagnostics"); + var configurationIoManager = instanceIoManager.CreateResolverForSubdirectory("Configuration"); var metricFactory = this.metricFactory.WithLabels( new Dictionary @@ -441,6 +441,6 @@ namespace Tgstation.Server.Host.Components /// /// The . /// The for the . - ResolvingIOManager CreateInstanceIOManager(Models.Instance metadata) => new(ioManager, metadata.Path!); + IIOManager CreateInstanceIOManager(Models.Instance metadata) => ioManager.CreateResolverForSubdirectory(metadata.Path!); } } diff --git a/src/Tgstation.Server.Host/Components/InstanceManager.cs b/src/Tgstation.Server.Host/Components/InstanceManager.cs index 1ded8b3dad..e1fa9b8edd 100644 --- a/src/Tgstation.Server.Host/Components/InstanceManager.cs +++ b/src/Tgstation.Server.Host/Components/InstanceManager.cs @@ -702,7 +702,7 @@ namespace Tgstation.Server.Host.Components { logger.LogDebug("Running as user: {username}", Environment.UserName); - generalConfiguration.CheckCompatibility(logger); + generalConfiguration.CheckCompatibility(logger, ioManager); using (var systemIdentity = systemIdentityFactory.GetCurrent()) { diff --git a/src/Tgstation.Server.Host/Components/Repository/Repository.cs b/src/Tgstation.Server.Host/Components/Repository/Repository.cs index b62cfbaa50..636321b5c7 100644 --- a/src/Tgstation.Server.Host/Components/Repository/Repository.cs +++ b/src/Tgstation.Server.Host/Components/Repository/Repository.cs @@ -1,7 +1,6 @@ using System; using System.Collections.Generic; using System.Globalization; -using System.IO; using System.Linq; using System.Threading; using System.Threading.Tasks; @@ -1099,8 +1098,8 @@ namespace Tgstation.Server.Host.Components.Repository => ioManager.GetDirectoryName(libGitRepo .Info .Path - .TrimEnd(Path.DirectorySeparatorChar) - .TrimEnd(Path.AltDirectorySeparatorChar)); + .TrimEnd(ioManager.DirectorySeparatorChar) + .TrimEnd(ioManager.AltDirectorySeparatorChar)); /// /// Recusively update all s in the . diff --git a/src/Tgstation.Server.Host/Components/StaticFiles/Configuration.cs b/src/Tgstation.Server.Host/Components/StaticFiles/Configuration.cs index f5d320ec2d..6dbfc3cc9c 100644 --- a/src/Tgstation.Server.Host/Components/StaticFiles/Configuration.cs +++ b/src/Tgstation.Server.Host/Components/StaticFiles/Configuration.cs @@ -333,10 +333,10 @@ namespace Tgstation.Server.Host.Components.StaticFiles }, async cancellationToken => { - FileStream? result = null; + Stream? result = null; void GetFileStream() { - result = ioManager.GetFileStream(path, false); + result = synchronousIOManager.GetFileStream(path); } if (systemIdentity == null) @@ -771,7 +771,7 @@ namespace Tgstation.Server.Host.Components.StaticFiles var nullOrEmptyCheck = String.IsNullOrEmpty(configurationRelativePath); if (nullOrEmptyCheck) configurationRelativePath = DefaultIOManager.CurrentDirectory; - if (configurationRelativePath![0] == Path.DirectorySeparatorChar || configurationRelativePath[0] == Path.AltDirectorySeparatorChar) + if (configurationRelativePath![0] == ioManager.DirectorySeparatorChar || configurationRelativePath[0] == ioManager.AltDirectorySeparatorChar) configurationRelativePath = DefaultIOManager.CurrentDirectory + configurationRelativePath; var resolved = ioManager.ResolvePath(configurationRelativePath); var local = !nullOrEmptyCheck ? ioManager.ResolvePath() : null; diff --git a/src/Tgstation.Server.Host/Configuration/GeneralConfiguration.cs b/src/Tgstation.Server.Host/Configuration/GeneralConfiguration.cs index 3ba8b3e878..2bd07a8d2a 100644 --- a/src/Tgstation.Server.Host/Configuration/GeneralConfiguration.cs +++ b/src/Tgstation.Server.Host/Configuration/GeneralConfiguration.cs @@ -1,6 +1,5 @@ using System; using System.Collections.Generic; -using System.IO; using System.Linq; using Microsoft.Extensions.Logging; @@ -9,6 +8,7 @@ using Newtonsoft.Json.Converters; using Tgstation.Server.Api.Models.Internal; using Tgstation.Server.Host.Extensions; +using Tgstation.Server.Host.IO; using Tgstation.Server.Host.Properties; using Tgstation.Server.Host.Setup; @@ -31,6 +31,11 @@ namespace Tgstation.Server.Host.Configuration /// public const ushort DefaultApiPort = 5000; + /// + /// Default vale for . + /// + public const string DefaultByondZipDownloadTemplate = "https://www.byond.com/download/build/${Major}/${Major}.${Minor}_byond${Linux:_linux}.zip"; + /// /// The default value for . /// @@ -148,6 +153,17 @@ namespace Tgstation.Server.Host.Configuration [YamlMember(SerializeAs = typeof(string))] public Uri OpenDreamGitUrl { get; set; } = new Uri(DefaultOpenDreamGitUrl); + /// + /// The formatter used to download official byond zip files for a given version + /// - ${Major} is substituted with the major version number + /// - ${Minor} is substituted with the minor version number + /// - ${Linux:xxx}, where xxx is any string, will be substituted with xxx if running under Linux. + /// - ${Windows:xxx}, where xxx is any string, will be substituted with xxx if running under Windows. + /// - $$ will evaluate to a literal $ and not be used for substitutions. + /// - Any inapplicable ${xxx} string will be removed. + /// + public string ByondZipDownloadTemplate { get; set; } = DefaultByondZipDownloadTemplate; + /// /// The prefix to the OpenDream semver as tags appear in the git repository. /// @@ -178,7 +194,8 @@ namespace Tgstation.Server.Host.Configuration /// Validates the current 's compatibility and provides migration instructions. /// /// The to use. - public void CheckCompatibility(ILogger logger) + /// The to use. + public void CheckCompatibility(ILogger logger, IIOManager ioManager) { ArgumentNullException.ThrowIfNull(logger); @@ -204,7 +221,7 @@ namespace Tgstation.Server.Host.Configuration 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); - if (AdditionalEventScriptsDirectories?.Any(path => !Path.IsPathRooted(path)) == true) + if (AdditionalEventScriptsDirectories?.Any(path => !ioManager.IsPathRooted(path)) == true) logger.LogWarning($"Config option \"{nameof(AdditionalEventScriptsDirectories)}\" contains non-rooted paths. These will be evaluated relative to each instances \"Configuration\" directory!"); } } diff --git a/src/Tgstation.Server.Host/Core/Application.cs b/src/Tgstation.Server.Host/Core/Application.cs index 7ef7656819..e712308b7b 100644 --- a/src/Tgstation.Server.Host/Core/Application.cs +++ b/src/Tgstation.Server.Host/Core/Application.cs @@ -2,6 +2,7 @@ using System.Collections.Frozen; using System.Collections.Generic; using System.Globalization; +using System.IO.Abstractions; using System.Threading.Tasks; using System.Web; @@ -106,10 +107,12 @@ namespace Tgstation.Server.Host.Core public static IServerFactory CreateDefaultServerFactory() { var assemblyInformationProvider = new AssemblyInformationProvider(); - var ioManager = new DefaultIOManager(); + var fileSystem = new FileSystem(); + var ioManager = new DefaultIOManager(fileSystem); return new ServerFactory( assemblyInformationProvider, - ioManager); + ioManager, + fileSystem); } /// @@ -155,11 +158,13 @@ namespace Tgstation.Server.Host.Core /// The needed for configuration. /// The needed for configuration. /// The needed for configuration. + /// The needed for configuration. public void ConfigureServices( IServiceCollection services, IAssemblyInformationProvider assemblyInformationProvider, IIOManager ioManager, - IPostSetupServices postSetupServices) + IPostSetupServices postSetupServices, + IFileSystem fileSystem) { ConfigureServices(services, assemblyInformationProvider, ioManager); @@ -470,8 +475,7 @@ namespace Tgstation.Server.Host.Core services => services .GetRequiredService() .CreateRepositoryManager( - new ResolvingIOManager( - services.GetRequiredService(), + services.GetRequiredService().CreateResolverForSubdirectory( openDreamRepositoryDirectory), new NoopEventConsumer())); @@ -527,6 +531,7 @@ namespace Tgstation.Server.Host.Core services.AddSingleton(); services.AddSingleton(); services.AddSingleton(); + services.AddSingleton(fileSystem); services.AddHostedService(); services.AddHostedService(); @@ -940,10 +945,7 @@ namespace Tgstation.Server.Host.Core options.Scope.Add(OpenIdConnectScope.OpenId); options.Scope.Add(OpenIdConnectScope.OfflineAccess); -#if DEBUG options.RequireHttpsMetadata = false; -#endif - options.SaveTokens = true; options.ResponseType = OpenIdConnectResponseType.Code; options.MapInboundClaims = false; diff --git a/src/Tgstation.Server.Host/Extensions/WebHostBuilderExtensions.cs b/src/Tgstation.Server.Host/Extensions/WebHostBuilderExtensions.cs index 3179c684f2..ba0316ad89 100644 --- a/src/Tgstation.Server.Host/Extensions/WebHostBuilderExtensions.cs +++ b/src/Tgstation.Server.Host/Extensions/WebHostBuilderExtensions.cs @@ -1,4 +1,5 @@ using System; +using System.IO.Abstractions; using Microsoft.AspNetCore.Builder; using Microsoft.AspNetCore.Hosting; @@ -26,24 +27,27 @@ namespace Tgstation.Server.Host.Extensions /// The to configure. /// The to use. /// The to use. - /// The configured . /// The to use. + /// The to use. + /// The configured . public static IWebHostBuilder UseApplication( this IWebHostBuilder builder, IAssemblyInformationProvider assemblyInformationProvider, IIOManager ioManager, - IPostSetupServices postSetupServices) + IPostSetupServices postSetupServices, + IFileSystem fileSystem) { ArgumentNullException.ThrowIfNull(builder); ArgumentNullException.ThrowIfNull(assemblyInformationProvider); ArgumentNullException.ThrowIfNull(ioManager); ArgumentNullException.ThrowIfNull(postSetupServices); + ArgumentNullException.ThrowIfNull(fileSystem); return builder.ConfigureServices( (context, services) => { var application = new Application(context.Configuration, context.HostingEnvironment); - application.ConfigureServices(services, assemblyInformationProvider, ioManager, postSetupServices); + application.ConfigureServices(services, assemblyInformationProvider, ioManager, postSetupServices, fileSystem); services.AddSingleton(application); }) .Configure(ConfigureApplication); diff --git a/src/Tgstation.Server.Host/IO/DefaultIOManager.cs b/src/Tgstation.Server.Host/IO/DefaultIOManager.cs index b392bb7654..eaf2b27736 100644 --- a/src/Tgstation.Server.Host/IO/DefaultIOManager.cs +++ b/src/Tgstation.Server.Host/IO/DefaultIOManager.cs @@ -1,6 +1,7 @@ using System; using System.Collections.Generic; using System.IO; +using System.IO.Abstractions; using System.IO.Compression; using System.Linq; using System.Threading; @@ -30,12 +31,32 @@ namespace Tgstation.Server.Host.IO /// public const TaskCreationOptions BlockingTaskCreationOptions = TaskCreationOptions.None; + /// + public char DirectorySeparatorChar => fileSystem.Path.DirectorySeparatorChar; + + /// + public char AltDirectorySeparatorChar => fileSystem.Path.AltDirectorySeparatorChar; + + /// + /// The backing . + /// + readonly IFileSystem fileSystem; + + /// + /// Initializes a new instance of the class. + /// + /// The value of . + public DefaultIOManager(IFileSystem fileSystem) + { + this.fileSystem = fileSystem ?? throw new ArgumentNullException(nameof(fileSystem)); + } + /// /// Recursively empty a directory. /// /// of the directory to empty. /// The for the operation. - static void NormalizeAndDelete(DirectoryInfo dir, CancellationToken cancellationToken) + static void NormalizeAndDelete(IDirectoryInfo dir, CancellationToken cancellationToken) { cancellationToken.ThrowIfCancellationRequested(); @@ -111,7 +132,7 @@ namespace Tgstation.Server.Host.IO } /// - public string ConcatPath(params string[] paths) => Path.Combine(paths); + public string ConcatPath(params string[] paths) => fileSystem.Path.Combine(paths); /// public async ValueTask CopyFile(string src, string dest, CancellationToken cancellationToken) @@ -120,7 +141,7 @@ namespace Tgstation.Server.Host.IO ArgumentNullException.ThrowIfNull(dest); // tested to hell and back, these are the optimal buffer sizes - await using var srcStream = new FileStream( + await using var srcStream = fileSystem.FileStream.New( ResolvePath(src), FileMode.Open, FileAccess.Read, @@ -134,41 +155,40 @@ namespace Tgstation.Server.Host.IO } /// - public Task CreateDirectory(string path, CancellationToken cancellationToken) => Task.Factory.StartNew(() => Directory.CreateDirectory(ResolvePath(path)), cancellationToken, BlockingTaskCreationOptions, TaskScheduler.Current); + public Task CreateDirectory(string path, CancellationToken cancellationToken) => Task.Factory.StartNew(() => fileSystem.Directory.CreateDirectory(ResolvePath(path)), cancellationToken, BlockingTaskCreationOptions, TaskScheduler.Current); /// public Task DeleteDirectory(string path, CancellationToken cancellationToken) - { - path = ResolvePath(path); - var di = new DirectoryInfo(path); - if (!di.Exists) - return Task.CompletedTask; - - return Task.Factory.StartNew( - () => NormalizeAndDelete(di, cancellationToken), + => Task.Factory.StartNew( + () => + { + var di = fileSystem.DirectoryInfo.New( + ResolvePath(path)); + if (di.Exists) + NormalizeAndDelete(di, cancellationToken); + }, cancellationToken, BlockingTaskCreationOptions, TaskScheduler.Current); - } /// - public Task DeleteFile(string path, CancellationToken cancellationToken) => Task.Factory.StartNew(() => File.Delete(ResolvePath(path)), cancellationToken, BlockingTaskCreationOptions, TaskScheduler.Current); + public Task DeleteFile(string path, CancellationToken cancellationToken) => Task.Factory.StartNew(() => fileSystem.File.Delete(ResolvePath(path)), cancellationToken, BlockingTaskCreationOptions, TaskScheduler.Current); /// - public Task FileExists(string path, CancellationToken cancellationToken) => Task.Factory.StartNew(() => File.Exists(ResolvePath(path)), cancellationToken, BlockingTaskCreationOptions, TaskScheduler.Current); + public Task FileExists(string path, CancellationToken cancellationToken) => Task.Factory.StartNew(() => fileSystem.File.Exists(ResolvePath(path)), cancellationToken, BlockingTaskCreationOptions, TaskScheduler.Current); /// - public Task DirectoryExists(string path, CancellationToken cancellationToken) => Task.Factory.StartNew(() => Directory.Exists(ResolvePath(path)), cancellationToken, BlockingTaskCreationOptions, TaskScheduler.Current); + public Task DirectoryExists(string path, CancellationToken cancellationToken) => Task.Factory.StartNew(() => fileSystem.Directory.Exists(ResolvePath(path)), cancellationToken, BlockingTaskCreationOptions, TaskScheduler.Current); /// - public string GetDirectoryName(string path) => Path.GetDirectoryName(path ?? throw new ArgumentNullException(nameof(path))) + public string GetDirectoryName(string path) => fileSystem.Path.GetDirectoryName(path ?? throw new ArgumentNullException(nameof(path))) ?? throw new InvalidOperationException($"Null was returned. Path ({path}) must be rooted. This is not supported!"); /// - public string GetFileName(string path) => Path.GetFileName(path ?? throw new ArgumentNullException(nameof(path))); + public string GetFileName(string path) => fileSystem.Path.GetFileName(path ?? throw new ArgumentNullException(nameof(path))); /// - public string GetFileNameWithoutExtension(string path) => Path.GetFileNameWithoutExtension(path ?? throw new ArgumentNullException(nameof(path))); + public string GetFileNameWithoutExtension(string path) => fileSystem.Path.GetFileNameWithoutExtension(path ?? throw new ArgumentNullException(nameof(path))); /// public Task> GetFilesWithExtension(string path, string extension, bool recursive, CancellationToken cancellationToken) => Task.Factory.StartNew( @@ -177,7 +197,7 @@ namespace Tgstation.Server.Host.IO path = ResolvePath(path); ArgumentNullException.ThrowIfNull(extension); var results = new List(); - foreach (var fileName in Directory.EnumerateFiles( + foreach (var fileName in fileSystem.Directory.EnumerateFiles( path, $"*.{extension}", recursive ? SearchOption.AllDirectories : SearchOption.TopDirectoryOnly)) @@ -199,7 +219,7 @@ namespace Tgstation.Server.Host.IO ArgumentNullException.ThrowIfNull(destination); source = ResolvePath(source ?? throw new ArgumentNullException(nameof(source))); destination = ResolvePath(destination); - File.Move(source, destination); + fileSystem.File.Move(source, destination); }, cancellationToken, BlockingTaskCreationOptions, @@ -212,7 +232,7 @@ namespace Tgstation.Server.Host.IO ArgumentNullException.ThrowIfNull(destination); source = ResolvePath(source ?? throw new ArgumentNullException(nameof(source))); destination = ResolvePath(destination); - Directory.Move(source, destination); + fileSystem.Directory.Move(source, destination); }, cancellationToken, BlockingTaskCreationOptions, @@ -221,7 +241,7 @@ namespace Tgstation.Server.Host.IO /// public async ValueTask ReadAllBytes(string path, CancellationToken cancellationToken) { - await using var file = CreateAsyncSequentialReadStream(path); + await using var file = CreateAsyncReadStream(path, true, true); byte[] buf; buf = new byte[file.Length]; await file.ReadAsync(buf, cancellationToken); @@ -229,10 +249,12 @@ namespace Tgstation.Server.Host.IO } /// - public string ResolvePath() => ResolvePath(CurrentDirectory); + public string ResolvePath() + => ResolvePath(CurrentDirectory); /// - public virtual string ResolvePath(string path) => Path.GetFullPath(path ?? throw new ArgumentNullException(nameof(path))); + public virtual string ResolvePath(string path) + => fileSystem.Path.GetFullPath(path ?? throw new ArgumentNullException(nameof(path))); /// public async ValueTask WriteAllBytes(string path, byte[] contents, CancellationToken cancellationToken) @@ -242,10 +264,10 @@ namespace Tgstation.Server.Host.IO } /// - public FileStream CreateAsyncSequentialWriteStream(string path) + public Stream CreateAsyncSequentialWriteStream(string path) { path = ResolvePath(path); - return new FileStream( + return fileSystem.FileStream.New( path, FileMode.Create, FileAccess.Write, @@ -255,16 +277,18 @@ namespace Tgstation.Server.Host.IO } /// - public FileStream CreateAsyncSequentialReadStream(string path) + public Stream CreateAsyncReadStream(string path, bool sequental, bool shareWrite) { path = ResolvePath(path); - return new FileStream( + return fileSystem.FileStream.New( path, FileMode.Open, FileAccess.Read, - FileShare.ReadWrite | FileShare.Delete, + FileShare.ReadWrite | FileShare.Delete | (shareWrite ? FileShare.Write : FileShare.None), DefaultBufferSize, - FileOptions.Asynchronous | FileOptions.SequentialScan); + sequental + ? FileOptions.Asynchronous | FileOptions.SequentialScan + : FileOptions.Asynchronous); } /// @@ -274,7 +298,7 @@ namespace Tgstation.Server.Host.IO path = ResolvePath(path); var results = new List(); cancellationToken.ThrowIfCancellationRequested(); - foreach (var directoryName in Directory.EnumerateDirectories(path)) + foreach (var directoryName in fileSystem.Directory.EnumerateDirectories(path)) { results.Add(directoryName); cancellationToken.ThrowIfCancellationRequested(); @@ -293,7 +317,7 @@ namespace Tgstation.Server.Host.IO path = ResolvePath(path); var results = new List(); cancellationToken.ThrowIfCancellationRequested(); - foreach (var fileName in Directory.EnumerateFiles(path)) + foreach (var fileName in fileSystem.Directory.EnumerateFiles(path)) { results.Add(fileName); cancellationToken.ThrowIfCancellationRequested(); @@ -331,8 +355,8 @@ namespace Tgstation.Server.Host.IO public bool PathContainsParentAccess(string path) => path ?.Split( [ - Path.DirectorySeparatorChar, - Path.AltDirectorySeparatorChar, + fileSystem.Path.DirectorySeparatorChar, + fileSystem.Path.AltDirectorySeparatorChar, ]) .Any(x => x == "..") ?? throw new ArgumentNullException(nameof(path)); @@ -342,22 +366,13 @@ namespace Tgstation.Server.Host.IO () => { path = ResolvePath(path ?? throw new ArgumentNullException(nameof(path))); - var fileInfo = new FileInfo(path); + var fileInfo = fileSystem.FileInfo.New(path); return new DateTimeOffset(fileInfo.LastWriteTimeUtc); }, cancellationToken, BlockingTaskCreationOptions, TaskScheduler.Current); - /// - public FileStream GetFileStream(string path, bool shareWrite) => new( - ResolvePath(path), - FileMode.Open, - FileAccess.Read, - FileShare.Read | FileShare.Delete | (shareWrite ? FileShare.Write : FileShare.None), - DefaultBufferSize, - true); - /// public Task PathIsChildOf(string parentPath, string childPath, CancellationToken cancellationToken) => Task.Factory.StartNew( () => @@ -369,8 +384,8 @@ namespace Tgstation.Server.Host.IO return true; // https://stackoverflow.com/questions/5617320/given-full-path-check-if-path-is-subdirectory-of-some-other-path-or-otherwise?lq=1 - var di1 = new DirectoryInfo(parentPath); - var di2 = new DirectoryInfo(childPath); + var di1 = fileSystem.DirectoryInfo.New(parentPath); + var di2 = fileSystem.DirectoryInfo.New(childPath); while (di2.Parent != null) { if (di2.Parent.FullName == di1.FullName) @@ -385,6 +400,33 @@ namespace Tgstation.Server.Host.IO BlockingTaskCreationOptions, TaskScheduler.Current); + /// + public Task DirectoryInfo(string path, CancellationToken cancellationToken) + => Task.Factory.StartNew( + () => fileSystem.DirectoryInfo.New(ResolvePath(path)), + cancellationToken, + BlockingTaskCreationOptions, + TaskScheduler.Current); + + /// + public bool IsPathRooted(string path) + => fileSystem.Path.IsPathRooted(path); + + /// + public IIOManager CreateResolverForSubdirectory(string subdirectoryPath) + { + ArgumentNullException.ThrowIfNull(subdirectoryPath); + + if (!Path.IsPathRooted(subdirectoryPath)) + subdirectoryPath = ConcatPath( + ResolvePath(), + subdirectoryPath); + + return new ResolvingIOManager( + fileSystem, + subdirectoryPath); + } + /// /// Copies a directory from to . /// @@ -403,7 +445,7 @@ namespace Tgstation.Server.Host.IO SemaphoreSlim? semaphore, CancellationToken cancellationToken) { - var dir = new DirectoryInfo(src); + var dir = fileSystem.DirectoryInfo.New(src); Task? subdirCreationTask = null; foreach (var subDirectory in dir.EnumerateDirectories()) { @@ -411,7 +453,7 @@ namespace Tgstation.Server.Host.IO continue; var checkingSubdirCreationTask = true; - foreach (var copyTask in CopyDirectoryImpl(subDirectory.FullName, Path.Combine(dest, subDirectory.Name), null, postCopyCallback, semaphore, cancellationToken)) + foreach (var copyTask in CopyDirectoryImpl(subDirectory.FullName, fileSystem.Path.Combine(dest, subDirectory.Name), null, postCopyCallback, semaphore, cancellationToken)) { if (subdirCreationTask == null) { diff --git a/src/Tgstation.Server.Host/IO/IIOManager.cs b/src/Tgstation.Server.Host/IO/IIOManager.cs index 13a29310e1..3b81869fb8 100644 --- a/src/Tgstation.Server.Host/IO/IIOManager.cs +++ b/src/Tgstation.Server.Host/IO/IIOManager.cs @@ -1,6 +1,7 @@ using System; using System.Collections.Generic; using System.IO; +using System.IO.Abstractions; using System.Threading; using System.Threading.Tasks; @@ -11,6 +12,23 @@ namespace Tgstation.Server.Host.IO /// public interface IIOManager { + /// + /// Gets the primary directory separator character. + /// + char DirectorySeparatorChar { get; } + + /// + /// Gets the alternative directory separator character. + /// + char AltDirectorySeparatorChar { get; } + + /// + /// Create a new that resolves paths to the specified . + /// + /// A relative or absolute path that the new will resolve as its current directory. + /// A new . + IIOManager CreateResolverForSubdirectory(string subdirectoryPath); + /// /// Retrieve the full path of the current working directory. /// @@ -117,15 +135,17 @@ namespace Tgstation.Server.Host.IO /// Creates an asynchronous for sequential writing. /// /// The path of the file to write, will be truncated. - /// The open . - FileStream CreateAsyncSequentialWriteStream(string path); + /// The open . + Stream 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); + /// If the sequential read flag should be added. + /// If should be used. + /// The open . + Stream CreateAsyncReadStream(string path, bool sequential, bool shareWrite); /// /// Writes some to a file at overwriting previous content. @@ -230,12 +250,18 @@ namespace Tgstation.Server.Host.IO Task GetLastModified(string path, CancellationToken cancellationToken); /// - /// Gets the for a given file . + /// Gets a for the given . /// - /// The path of the file. - /// If should be used. - /// The of the file. - /// This function is sychronous. - FileStream GetFileStream(string path, bool shareWrite); + /// The path to get for. + /// The for the operation. + /// A resulting in the of the . + Task DirectoryInfo(string path, CancellationToken cancellationToken); + + /// + /// Check if a given is at the root level of the filesystem. + /// + /// The path to check. + /// if the path is rooted, otherwise. + bool IsPathRooted(string path); } } diff --git a/src/Tgstation.Server.Host/IO/ISynchronousIOManager.cs b/src/Tgstation.Server.Host/IO/ISynchronousIOManager.cs index 16e0ea370e..850f6941f2 100644 --- a/src/Tgstation.Server.Host/IO/ISynchronousIOManager.cs +++ b/src/Tgstation.Server.Host/IO/ISynchronousIOManager.cs @@ -63,5 +63,12 @@ namespace Tgstation.Server.Host.IO /// The path to check. /// if is a directory, otherwise. bool IsDirectory(string path); + + /// + /// Gets the for a given file without write share. + /// + /// The path of the file. + /// The of the file. + Stream GetFileStream(string path); } } diff --git a/src/Tgstation.Server.Host/IO/PosixFilesystemLinkFactory.cs b/src/Tgstation.Server.Host/IO/PosixFilesystemLinkFactory.cs index b80af9e122..c0fa195906 100644 --- a/src/Tgstation.Server.Host/IO/PosixFilesystemLinkFactory.cs +++ b/src/Tgstation.Server.Host/IO/PosixFilesystemLinkFactory.cs @@ -1,5 +1,5 @@ using System; -using System.IO; +using System.IO.Abstractions; using System.Threading; using System.Threading.Tasks; @@ -15,6 +15,20 @@ namespace Tgstation.Server.Host.IO /// public bool SymlinkedDirectoriesAreDeletedAsFiles => true; + /// + /// The to use. + /// + readonly IFileSystem fileSystem; + + /// + /// Initializes a new instance of the class. + /// + /// The value of . + public PosixFilesystemLinkFactory(IFileSystem fileSystem) + { + this.fileSystem = fileSystem ?? throw new ArgumentNullException(nameof(fileSystem)); + } + /// public Task CreateHardLink(string targetPath, string linkPath, CancellationToken cancellationToken) => Task.Factory.StartNew( () => @@ -39,7 +53,7 @@ namespace Tgstation.Server.Host.IO ArgumentNullException.ThrowIfNull(linkPath); UnixFileSystemInfo fsInfo; - var isFile = File.Exists(targetPath); + var isFile = fileSystem.File.Exists(targetPath); cancellationToken.ThrowIfCancellationRequested(); if (isFile) fsInfo = new UnixFileInfo(targetPath); diff --git a/src/Tgstation.Server.Host/IO/ResolvingIOManager.cs b/src/Tgstation.Server.Host/IO/ResolvingIOManager.cs index 3b86b92c36..625d7da86e 100644 --- a/src/Tgstation.Server.Host/IO/ResolvingIOManager.cs +++ b/src/Tgstation.Server.Host/IO/ResolvingIOManager.cs @@ -1,5 +1,5 @@ using System; -using System.IO; +using System.IO.Abstractions; namespace Tgstation.Server.Host.IO { @@ -16,20 +16,20 @@ namespace Tgstation.Server.Host.IO /// /// Initializes a new instance of the class. /// - /// The that resolves to the directory to work out of. + /// The for the . /// The value of . - public ResolvingIOManager(IIOManager parent, string subdirectory) + public ResolvingIOManager( + IFileSystem fileSystem, + string subdirectory) + : base(fileSystem) { - ArgumentNullException.ThrowIfNull(parent); - ArgumentNullException.ThrowIfNull(subdirectory); - - this.subdirectory = ConcatPath(parent.ResolvePath(), subdirectory); + this.subdirectory = subdirectory ?? throw new ArgumentNullException(nameof(subdirectory)); } /// public override string ResolvePath(string path) { - if (!Path.IsPathRooted(path)) + if (!IsPathRooted(path)) return base.ResolvePath(ConcatPath(subdirectory, path)); return path; } diff --git a/src/Tgstation.Server.Host/IO/SynchronousIOManager.cs b/src/Tgstation.Server.Host/IO/SynchronousIOManager.cs index b0388b51af..892b3edaec 100644 --- a/src/Tgstation.Server.Host/IO/SynchronousIOManager.cs +++ b/src/Tgstation.Server.Host/IO/SynchronousIOManager.cs @@ -2,6 +2,7 @@ using System.Collections.Generic; using System.Globalization; using System.IO; +using System.IO.Abstractions; using System.Linq; using System.Security.Cryptography; using System.Threading; @@ -13,6 +14,11 @@ namespace Tgstation.Server.Host.IO /// sealed class SynchronousIOManager : ISynchronousIOManager { + /// + /// The to use. + /// + readonly IFileSystem fileSystem; + /// /// The for the . /// @@ -21,9 +27,11 @@ namespace Tgstation.Server.Host.IO /// /// Initializes a new instance of the class. /// + /// The value of . /// The value of . - public SynchronousIOManager(ILogger logger) + public SynchronousIOManager(IFileSystem fileSystem, ILogger logger) { + this.fileSystem = fileSystem ?? throw new ArgumentNullException(nameof(fileSystem)); this.logger = logger ?? throw new ArgumentNullException(nameof(logger)); } @@ -33,32 +41,32 @@ namespace Tgstation.Server.Host.IO if (IsDirectory(path)) return true; cancellationToken.ThrowIfCancellationRequested(); - Directory.CreateDirectory(path); + fileSystem.Directory.CreateDirectory(path); return false; } /// public bool DeleteDirectory(string path) { - if (File.Exists(path)) + if (fileSystem.File.Exists(path)) return false; - if (!Directory.Exists(path)) + if (!fileSystem.Directory.Exists(path)) return true; - if (Directory.EnumerateFileSystemEntries(path).Any()) + if (fileSystem.Directory.EnumerateFileSystemEntries(path).Any()) return false; - Directory.Delete(path); + fileSystem.Directory.Delete(path); return true; } /// public IEnumerable GetDirectories(string path, CancellationToken cancellationToken) { - foreach (var directoryName in Directory.EnumerateDirectories(path)) + foreach (var directoryName in fileSystem.Directory.EnumerateDirectories(path)) { - yield return Path.GetFileName(directoryName); + yield return fileSystem.Path.GetFileName(directoryName); cancellationToken.ThrowIfCancellationRequested(); } } @@ -66,9 +74,9 @@ namespace Tgstation.Server.Host.IO /// public IEnumerable GetFiles(string path, CancellationToken cancellationToken) { - foreach (var fileName in Directory.EnumerateFiles(path)) + foreach (var fileName in fileSystem.Directory.EnumerateFiles(path)) { - yield return Path.GetFileName(fileName); + yield return fileSystem.Path.GetFileName(fileName); cancellationToken.ThrowIfCancellationRequested(); } } @@ -77,14 +85,14 @@ namespace Tgstation.Server.Host.IO public bool IsDirectory(string path) { ArgumentNullException.ThrowIfNull(path); - return Directory.Exists(path); + return fileSystem.Directory.Exists(path); } /// public byte[] ReadFile(string path) { ArgumentNullException.ThrowIfNull(path); - return File.ReadAllBytes(path); + return fileSystem.File.ReadAllBytes(path); } /// @@ -94,16 +102,16 @@ namespace Tgstation.Server.Host.IO ArgumentNullException.ThrowIfNull(data); cancellationToken.ThrowIfCancellationRequested(); - var directory = Path.GetDirectoryName(path) ?? throw new ArgumentException("path cannot be rooted!", nameof(path)); - Directory.CreateDirectory(directory); + var directory = fileSystem.Path.GetDirectoryName(path) ?? throw new ArgumentException("path cannot be rooted!", nameof(path)); + fileSystem.Directory.CreateDirectory(directory); - var newFile = !File.Exists(path); + var newFile = !fileSystem.File.Exists(path); cancellationToken.ThrowIfCancellationRequested(); logger.LogTrace("Starting checked write to {path} ({fileType} file)", path, newFile ? "New" : "Pre-existing"); - using (var file = File.Open(path, FileMode.OpenOrCreate, FileAccess.ReadWrite, FileShare.None)) + using (var file = fileSystem.File.Open(path, FileMode.OpenOrCreate, FileAccess.ReadWrite, FileShare.None)) { cancellationToken.ThrowIfCancellationRequested(); @@ -160,10 +168,20 @@ namespace Tgstation.Server.Host.IO if (data.Length == 0) { logger.LogDebug("Stream is empty, deleting file"); - File.Delete(path); + fileSystem.File.Delete(path); } return true; } + + /// + public Stream GetFileStream(string path) + => fileSystem.FileStream.New( + path, + FileMode.Open, + FileAccess.Read, + FileShare.Read | FileShare.Delete, + DefaultIOManager.DefaultBufferSize, + true); } } diff --git a/src/Tgstation.Server.Host/IO/WindowsFilesystemLinkFactory.cs b/src/Tgstation.Server.Host/IO/WindowsFilesystemLinkFactory.cs index 54ff36b320..01cd4934d0 100644 --- a/src/Tgstation.Server.Host/IO/WindowsFilesystemLinkFactory.cs +++ b/src/Tgstation.Server.Host/IO/WindowsFilesystemLinkFactory.cs @@ -1,6 +1,6 @@ using System; using System.ComponentModel; -using System.IO; +using System.IO.Abstractions; using System.Threading; using System.Threading.Tasks; @@ -16,6 +16,20 @@ namespace Tgstation.Server.Host.IO /// public bool SymlinkedDirectoriesAreDeletedAsFiles => false; + /// + /// The to use. + /// + readonly IFileSystem fileSystem; + + /// + /// Initializes a new instance of the class. + /// + /// The value of . + public WindowsFilesystemLinkFactory(IFileSystem fileSystem) + { + this.fileSystem = fileSystem ?? throw new ArgumentNullException(nameof(fileSystem)); + } + /// public Task CreateHardLink(string targetPath, string linkPath, CancellationToken cancellationToken) => throw new NotSupportedException(); @@ -28,7 +42,7 @@ namespace Tgstation.Server.Host.IO ArgumentNullException.ThrowIfNull(linkPath); // check if its not a file - var flags = File.Exists(targetPath) ? NativeMethods.CreateSymbolicLinkFlags.None : NativeMethods.CreateSymbolicLinkFlags.Directory; + var flags = fileSystem.File.Exists(targetPath) ? NativeMethods.CreateSymbolicLinkFlags.None : NativeMethods.CreateSymbolicLinkFlags.Directory; /* * no don't fucking use this diff --git a/src/Tgstation.Server.Host/ServerFactory.cs b/src/Tgstation.Server.Host/ServerFactory.cs index 5306221a60..5f3230ec0a 100644 --- a/src/Tgstation.Server.Host/ServerFactory.cs +++ b/src/Tgstation.Server.Host/ServerFactory.cs @@ -1,4 +1,5 @@ using System; +using System.IO.Abstractions; using System.Linq; using System.Threading; using System.Threading.Tasks; @@ -29,23 +30,33 @@ namespace Tgstation.Server.Host /// public const string AppSettings = "appsettings"; + /// + public IIOManager IOManager { get; } + /// /// The for the . /// readonly IAssemblyInformationProvider assemblyInformationProvider; - /// - public IIOManager IOManager { get; } + /// + /// The for the . + /// + readonly IFileSystem fileSystem; /// /// Initializes a new instance of the class. /// /// The value of . /// The value of . - internal ServerFactory(IAssemblyInformationProvider assemblyInformationProvider, IIOManager ioManager) + /// The value of . + internal ServerFactory( + IAssemblyInformationProvider assemblyInformationProvider, + IIOManager ioManager, + IFileSystem fileSystem) { this.assemblyInformationProvider = assemblyInformationProvider ?? throw new ArgumentNullException(nameof(assemblyInformationProvider)); IOManager = ioManager ?? throw new ArgumentNullException(nameof(ioManager)); + this.fileSystem = fileSystem ?? throw new ArgumentNullException(nameof(fileSystem)); } /// @@ -160,7 +171,7 @@ namespace Tgstation.Server.Host }) .UseIIS() .UseIISIntegration() - .UseApplication(assemblyInformationProvider, IOManager, postSetupServices) + .UseApplication(assemblyInformationProvider, IOManager, postSetupServices, fileSystem) .SuppressStatusMessages(true) .UseShutdownTimeout( TimeSpan.FromMinutes( diff --git a/src/Tgstation.Server.Host/Setup/SetupWizard.cs b/src/Tgstation.Server.Host/Setup/SetupWizard.cs index 32e82877e5..ea1ba73d85 100644 --- a/src/Tgstation.Server.Host/Setup/SetupWizard.cs +++ b/src/Tgstation.Server.Host/Setup/SetupWizard.cs @@ -304,7 +304,7 @@ namespace Tgstation.Server.Host.Setup /// A resulting in the SQLite database path to store in the configuration. async ValueTask ValidateNonExistantSqliteDBName(string databaseName, CancellationToken cancellationToken) { - var dbPathIsRooted = Path.IsPathRooted(databaseName); + var dbPathIsRooted = ioManager.IsPathRooted(databaseName); var resolvedPath = ioManager.ResolvePath( dbPathIsRooted ? databaseName diff --git a/src/Tgstation.Server.Host/System/PosixProcessFeatures.cs b/src/Tgstation.Server.Host/System/PosixProcessFeatures.cs index 12a0164a1f..ee6abe8a62 100644 --- a/src/Tgstation.Server.Host/System/PosixProcessFeatures.cs +++ b/src/Tgstation.Server.Host/System/PosixProcessFeatures.cs @@ -64,39 +64,6 @@ namespace Tgstation.Server.Host.System this.logger = logger ?? throw new ArgumentNullException(nameof(logger)); } - /// - /// Gets potential paths to the gcore executable. - /// - /// The potential paths to the gcore executable. - static IEnumerable GetPotentialGCorePaths() - { - var enviromentPath = Environment.GetEnvironmentVariable("PATH"); - IEnumerable enumerator; - if (enviromentPath == null) - enumerator = Enumerable.Empty(); - else - { - var paths = enviromentPath.Split(';'); - enumerator = paths - .Select(x => x.Split(':')) - .SelectMany(x => x); - } - - var exeName = "gcore"; - - enumerator = enumerator - .Concat(new List(2) - { - "/usr/bin", - "/usr/share/bin", - "/bin", - }); - - enumerator = enumerator.Select(x => Path.Combine(x, exeName)); - - return enumerator; - } - /// public void ResumeProcess(global::System.Diagnostics.Process process) { @@ -201,8 +168,8 @@ namespace Tgstation.Server.Host.System { // 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"); + await using var fileStream = ioManager.CreateAsyncReadStream( + "/proc/self/oom_score_adj", true, true); using var reader = new StreamReader(fileStream, Encoding.UTF8, leaveOpen: true); originalString = await reader.ReadToEndAsync(cancellationToken); } @@ -255,5 +222,38 @@ namespace Tgstation.Server.Host.System Encoding.UTF8.GetBytes(adjustedValue.ToString(CultureInfo.InvariantCulture)), cancellationToken); } + + /// + /// Gets potential paths to the gcore executable. + /// + /// The potential paths to the gcore executable. + IEnumerable GetPotentialGCorePaths() + { + var enviromentPath = Environment.GetEnvironmentVariable("PATH"); + IEnumerable enumerator; + if (enviromentPath == null) + enumerator = Enumerable.Empty(); + else + { + var paths = enviromentPath.Split(';'); + enumerator = paths + .Select(x => x.Split(':')) + .SelectMany(x => x); + } + + var exeName = "gcore"; + + enumerator = enumerator + .Concat(new List(2) + { + "/usr/bin", + "/usr/share/bin", + "/bin", + }); + + enumerator = enumerator.Select(x => ioManager.ConcatPath(x, exeName)); + + return enumerator; + } } } diff --git a/src/Tgstation.Server.Host/System/WindowsProcessFeatures.cs b/src/Tgstation.Server.Host/System/WindowsProcessFeatures.cs index 21855c0062..a048736a22 100644 --- a/src/Tgstation.Server.Host/System/WindowsProcessFeatures.cs +++ b/src/Tgstation.Server.Host/System/WindowsProcessFeatures.cs @@ -145,6 +145,7 @@ namespace Tgstation.Server.Host.System throw new JobException(ErrorCode.GameServerOffline, ex); } + // Windows API so has to be a real FS await using var fileStream = new FileStream(outputFile, FileMode.CreateNew); await Task.Factory.StartNew( diff --git a/src/Tgstation.Server.Host/Tgstation.Server.Host.csproj b/src/Tgstation.Server.Host/Tgstation.Server.Host.csproj index 9e4a3056c4..91dcae4c65 100644 --- a/src/Tgstation.Server.Host/Tgstation.Server.Host.csproj +++ b/src/Tgstation.Server.Host/Tgstation.Server.Host.csproj @@ -96,41 +96,41 @@ - + - + - + - + - + - + - + - + - + - + - + runtime; build; native; contentfiles; analyzers; buildtransitive - + - + - + - + @@ -144,9 +144,9 @@ - + - + @@ -154,15 +154,17 @@ - + - + - + - + + + - + diff --git a/src/Tgstation.Server.Host/Transfer/FileTransferService.cs b/src/Tgstation.Server.Host/Transfer/FileTransferService.cs index bb1cafa171..ac8ce12a1b 100644 --- a/src/Tgstation.Server.Host/Transfer/FileTransferService.cs +++ b/src/Tgstation.Server.Host/Transfer/FileTransferService.cs @@ -212,7 +212,7 @@ namespace Tgstation.Server.Host.Transfer if (downloadProvider.StreamProvider != null) stream = await downloadProvider.StreamProvider(cancellationToken); else - stream = ioManager.GetFileStream(downloadProvider.FilePath, downloadProvider.ShareWrite); + stream = ioManager.CreateAsyncReadStream(downloadProvider.FilePath, false, downloadProvider.ShareWrite); } catch (IOException ex) { diff --git a/src/Tgstation.Server.Host/appsettings.yml b/src/Tgstation.Server.Host/appsettings.yml index 7d952c73d1..ff244ea7e4 100644 --- a/src/Tgstation.Server.Host/appsettings.yml +++ b/src/Tgstation.Server.Host/appsettings.yml @@ -17,6 +17,7 @@ General: HostApiDocumentation: false # Make HTTP API documentation available at /api/doc/tgs_api.json and /api/graphql SkipAddingByondFirewallException: false # Windows Only: Prevent running netsh.exe to add a firewall exception for installed engine binaries DeploymentDirectoryCopyTasksPerCore: 100 # Maximum number of concurrent file copy operations PER available CPU core + ByondZipDownloadTemplate: https://www.byond.com/download/build/${Major}/${Major}.${Minor}_byond${Linux:_linux}.zip # Template for downloading official byond versions OpenDreamGitUrl: https://github.com/OpenDreamProject/OpenDream # The repository to retrieve OpenDream from OpenDreamGitTagPrefix: v # The prefix to the OpenDream semver as tags appear in the git repository OpenDreamSuppressInstallOutput: false # Suppress the dotnet output of creating an OpenDream installation. Known to cause hangs in CI. diff --git a/src/Tgstation.Server.Shared/Tgstation.Server.Shared.csproj b/src/Tgstation.Server.Shared/Tgstation.Server.Shared.csproj index 861618a8a9..e636c80a73 100644 --- a/src/Tgstation.Server.Shared/Tgstation.Server.Shared.csproj +++ b/src/Tgstation.Server.Shared/Tgstation.Server.Shared.csproj @@ -10,7 +10,7 @@ - + diff --git a/tests/Tgstation.Server.Client.Tests/Tgstation.Server.Client.Tests.csproj b/tests/Tgstation.Server.Client.Tests/Tgstation.Server.Client.Tests.csproj index 725b7c8bcd..66888f4430 100644 --- a/tests/Tgstation.Server.Client.Tests/Tgstation.Server.Client.Tests.csproj +++ b/tests/Tgstation.Server.Client.Tests/Tgstation.Server.Client.Tests.csproj @@ -6,7 +6,7 @@ - + diff --git a/tests/Tgstation.Server.Host.Tests/Components/Engine/TestByondInstallerBase.cs b/tests/Tgstation.Server.Host.Tests/Components/Engine/TestByondInstallerBase.cs new file mode 100644 index 0000000000..a3a4605877 --- /dev/null +++ b/tests/Tgstation.Server.Host.Tests/Components/Engine/TestByondInstallerBase.cs @@ -0,0 +1,23 @@ +using System; + +using Microsoft.VisualStudio.TestTools.UnitTesting; + +namespace Tgstation.Server.Host.Components.Engine.Tests +{ + [TestClass] + public sealed class TestByondInstallerBase + { + [TestMethod] + public void TestUrlTemplateFormatting() + { + const string OSMarker = "TempleOS"; + + Assert.AreEqual( + new Uri("https://example.com/$515.1111_Hello Worl$d.zip"), + ByondInstallerBase.GetDownloadZipUrl( + new Version(515, 1111), + "https://example.com/$$${Major}.${Minor}_${TempleOS:Hello Worl$$d}.zip${Linux:Not this}${Or This}", + OSMarker)); + } + } +} diff --git a/tests/Tgstation.Server.Host.Tests/Components/Engine/TestPosixByondInstaller.cs b/tests/Tgstation.Server.Host.Tests/Components/Engine/TestPosixByondInstaller.cs index 60e86ad63e..6dbfb155b4 100644 --- a/tests/Tgstation.Server.Host.Tests/Components/Engine/TestPosixByondInstaller.cs +++ b/tests/Tgstation.Server.Host.Tests/Components/Engine/TestPosixByondInstaller.cs @@ -1,4 +1,5 @@ using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Options; using Microsoft.VisualStudio.TestTools.UnitTesting; using Moq; using System; @@ -9,6 +10,7 @@ using System.Threading.Tasks; using Tgstation.Server.Api.Models; using Tgstation.Server.Api.Models.Internal; +using Tgstation.Server.Host.Configuration; using Tgstation.Server.Host.IO; namespace Tgstation.Server.Host.Components.Engine.Tests @@ -19,16 +21,18 @@ namespace Tgstation.Server.Host.Components.Engine.Tests [TestMethod] public void TestConstruction() { - Assert.ThrowsException(() => new PosixByondInstaller(null, null, null, null)); + Assert.ThrowsException(() => new PosixByondInstaller(null, null, null, null, null)); var mockPostWriteHandler = new Mock(); - Assert.ThrowsException(() => new PosixByondInstaller(mockPostWriteHandler.Object, null, null, null)); + Assert.ThrowsException(() => new PosixByondInstaller(mockPostWriteHandler.Object, null, null, null, null)); var mockIOManager = new Mock(); - Assert.ThrowsException(() => new PosixByondInstaller(mockPostWriteHandler.Object, mockIOManager.Object, null, null)); + Assert.ThrowsException(() => new PosixByondInstaller(mockPostWriteHandler.Object, mockIOManager.Object, null, null, null)); var mockFileDownloader = Mock.Of(); - Assert.ThrowsException(() => new PosixByondInstaller(mockPostWriteHandler.Object, mockIOManager.Object, mockFileDownloader, null)); + Assert.ThrowsException(() => new PosixByondInstaller(mockPostWriteHandler.Object, mockIOManager.Object, mockFileDownloader, null, null)); + var mockOptions = Mock.Of>(); + Assert.ThrowsException(() => new PosixByondInstaller(mockPostWriteHandler.Object, mockIOManager.Object, mockFileDownloader, mockOptions, null)); var mockLogger = new Mock>(); - _ = new PosixByondInstaller(mockPostWriteHandler.Object, mockIOManager.Object, mockFileDownloader, mockLogger.Object); + _ = new PosixByondInstaller(mockPostWriteHandler.Object, mockIOManager.Object, mockFileDownloader, mockOptions, mockLogger.Object); } [TestMethod] @@ -38,7 +42,8 @@ namespace Tgstation.Server.Host.Components.Engine.Tests var mockIOManager = new Mock(); var mockLogger = new Mock>(); var mockFileDownloader = Mock.Of(); - var installer = new PosixByondInstaller(mockPostWriteHandler.Object, mockIOManager.Object, mockFileDownloader, mockLogger.Object); + var mockOptions = Mock.Of>(); + var installer = new PosixByondInstaller(mockPostWriteHandler.Object, mockIOManager.Object, mockFileDownloader, mockOptions, mockLogger.Object); await installer.CleanCache(default); } @@ -50,7 +55,14 @@ namespace Tgstation.Server.Host.Components.Engine.Tests var mockPostWriteHandler = new Mock(); var mockLogger = new Mock>(); var mockFileDownloader = new Mock(); - var installer = new PosixByondInstaller(mockPostWriteHandler.Object, mockIOManager.Object, mockFileDownloader.Object, mockLogger.Object); + var mockOptions = new Mock>(); + const string TestUrl = "https://chumb.is"; + mockOptions.SetupGet(x => x.CurrentValue).Returns(new GeneralConfiguration + { + ByondZipDownloadTemplate = TestUrl, + }); + + var installer = new PosixByondInstaller(mockPostWriteHandler.Object, mockIOManager.Object, mockFileDownloader.Object, mockOptions.Object, mockLogger.Object); await Assert.ThrowsExceptionAsync(() => installer.DownloadVersion(null, null, default).AsTask()); @@ -58,7 +70,7 @@ namespace Tgstation.Server.Host.Components.Engine.Tests mockFileDownloader .Setup( x => x.DownloadFile( - It.Is(uri => uri == new Uri("https://www.byond.com/download/build/511/511.1385_byond_linux.zip")), + It.Is(uri => uri == new Uri(TestUrl)), null)) .Returns( new BufferedFileStreamProvider( @@ -87,7 +99,8 @@ namespace Tgstation.Server.Host.Components.Engine.Tests var mockPostWriteHandler = new Mock(); var mockLogger = new Mock>(); var mockFileDownloader = Mock.Of(); - var installer = new PosixByondInstaller(mockPostWriteHandler.Object, mockIOManager.Object, mockFileDownloader, mockLogger.Object); + var mockOptions = Mock.Of>(); + var installer = new PosixByondInstaller(mockPostWriteHandler.Object, mockIOManager.Object, mockFileDownloader, mockOptions, mockLogger.Object); const string FakePath = "fake"; await Assert.ThrowsExceptionAsync(() => installer.Install(null, null, false, default).AsTask()); diff --git a/tests/Tgstation.Server.Host.Tests/Components/StaticFiles/TestConfiguration.cs b/tests/Tgstation.Server.Host.Tests/Components/StaticFiles/TestConfiguration.cs index f5e6b9bf5f..7314b0783a 100644 --- a/tests/Tgstation.Server.Host.Tests/Components/StaticFiles/TestConfiguration.cs +++ b/tests/Tgstation.Server.Host.Tests/Components/StaticFiles/TestConfiguration.cs @@ -1,5 +1,6 @@ using System; using System.IO; +using System.IO.Abstractions.TestingHelpers; using System.Linq; using System.Threading; using System.Threading.Tasks; @@ -31,9 +32,10 @@ namespace Tgstation.Server.Host.Components.StaticFiles.Tests builder.SetMinimumLevel(LogLevel.Trace); }); + var mockFs = new MockFileSystem(); var tempPath = Path.GetTempFileName(); File.Delete(tempPath); - var ioManager = new ResolvingIOManager(new DefaultIOManager(), tempPath); + var ioManager = new DefaultIOManager(mockFs).CreateResolverForSubdirectory(tempPath); await ioManager.CreateDirectory(".", CancellationToken.None); try { @@ -46,7 +48,9 @@ namespace Tgstation.Server.Host.Components.StaticFiles.Tests var configuration = new Configuration( ioManager, - new SynchronousIOManager(loggerFactory.CreateLogger()), + new SynchronousIOManager( + mockFs, + loggerFactory.CreateLogger()), Mock.Of(), Mock.Of(), Mock.Of(), diff --git a/tests/Tgstation.Server.Host.Tests/IO/TestFilesystemLinkFactory.cs b/tests/Tgstation.Server.Host.Tests/IO/TestFilesystemLinkFactory.cs index dbf2aaf2a8..16ab28a73c 100644 --- a/tests/Tgstation.Server.Host.Tests/IO/TestFilesystemLinkFactory.cs +++ b/tests/Tgstation.Server.Host.Tests/IO/TestFilesystemLinkFactory.cs @@ -1,5 +1,6 @@ using System; using System.IO; +using System.IO.Abstractions; using System.Runtime.InteropServices; using System.Security.Principal; using System.Threading; @@ -17,10 +18,11 @@ namespace Tgstation.Server.Host.IO.Tests [ClassInitialize] public static void SelectFactory(TestContext _) { + var fileSystem = new FileSystem(); if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) - linkFactory = new WindowsFilesystemLinkFactory(); + linkFactory = new WindowsFilesystemLinkFactory(fileSystem); else - linkFactory = new PosixFilesystemLinkFactory(); + linkFactory = new PosixFilesystemLinkFactory(fileSystem); } public static bool HasPermissionToMakeSymlinks() diff --git a/tests/Tgstation.Server.Host.Tests/IO/TestIOManager.cs b/tests/Tgstation.Server.Host.Tests/IO/TestIOManager.cs index 8cd5424a4f..60fa057449 100644 --- a/tests/Tgstation.Server.Host.Tests/IO/TestIOManager.cs +++ b/tests/Tgstation.Server.Host.Tests/IO/TestIOManager.cs @@ -1,5 +1,7 @@ using System; using System.IO; +using System.IO.Abstractions; +using System.IO.Abstractions.TestingHelpers; using System.Linq; using System.Text; using System.Threading; @@ -14,27 +16,34 @@ namespace Tgstation.Server.Host.IO.Tests [TestClass] public sealed class TestIOManager { - readonly IIOManager ioManager = new DefaultIOManager(); + readonly IFileSystem fileSystem; + readonly IIOManager ioManager; + + public TestIOManager() + { + fileSystem = new MockFileSystem(); + ioManager = new DefaultIOManager(fileSystem); + } [TestMethod] public async Task TestDeleteDirectory() { - var tempPath = Path.GetTempFileName(); - File.Delete(tempPath); - Directory.CreateDirectory(tempPath); + var tempPath = fileSystem.Path.Combine(fileSystem.Path.GetTempPath(), fileSystem.Path.GetRandomFileName()); + fileSystem.File.Delete(tempPath); + fileSystem.Directory.CreateDirectory(tempPath); try { - await File.WriteAllTextAsync(Path.Combine(tempPath, "file.txt"), "asdf"); - var subDir = Path.Combine(tempPath, "subdir"); - Directory.CreateDirectory(subDir); - await File.WriteAllTextAsync(Path.Combine(subDir, "file2.txt"), "fdsa"); + await fileSystem.File.WriteAllTextAsync(Path.Combine(tempPath, "file.txt"), "asdf"); + var subDir = fileSystem.Path.Combine(tempPath, "subdir"); + fileSystem.Directory.CreateDirectory(subDir); + await fileSystem.File.WriteAllTextAsync(Path.Combine(subDir, "file2.txt"), "fdsa"); await ioManager.DeleteDirectory(tempPath, default); - Assert.IsFalse(Directory.Exists(tempPath)); + Assert.IsFalse(fileSystem.Directory.Exists(tempPath)); } catch { - Directory.Delete(tempPath, true); + fileSystem.Directory.Delete(tempPath, true); throw; } } @@ -42,13 +51,17 @@ namespace Tgstation.Server.Host.IO.Tests [TestMethod] public async Task TestDeleteDirectoryWithSymlinkInsideDoesntRecurse() { - var linkFactory = (IFilesystemLinkFactory)(new PlatformIdentifier().IsWindows - ? new WindowsFilesystemLinkFactory() - : new PosixFilesystemLinkFactory()); + // need a real FS here + var fileSystem = new FileSystem(); + var ioManager = new DefaultIOManager(fileSystem); - var tempPath = Path.GetTempFileName(); - File.Delete(tempPath); - Directory.CreateDirectory(tempPath); + var linkFactory = (IFilesystemLinkFactory)(new PlatformIdentifier().IsWindows + ? new WindowsFilesystemLinkFactory(fileSystem) + : new PosixFilesystemLinkFactory(fileSystem)); + + var tempPath = fileSystem.Path.Combine(fileSystem.Path.GetTempPath(), fileSystem.Path.GetRandomFileName()); + fileSystem.File.Delete(tempPath); + fileSystem.Directory.CreateDirectory(tempPath); try { var targetDir = ioManager.ConcatPath(tempPath, "targetdir"); @@ -81,7 +94,7 @@ namespace Tgstation.Server.Host.IO.Tests } catch { - Directory.Delete(tempPath, true); + fileSystem.Directory.Delete(tempPath, true); throw; } } @@ -89,14 +102,15 @@ namespace Tgstation.Server.Host.IO.Tests [TestMethod] public async Task TestFileExists() { - var tempPath = Path.GetTempFileName(); + var tempPath = fileSystem.Path.Combine(fileSystem.Path.GetTempPath(), fileSystem.Path.GetRandomFileName()); + await fileSystem.File.WriteAllBytesAsync(tempPath, Array.Empty()); try { Assert.IsTrue(await ioManager.FileExists(tempPath, default)); } finally { - File.Delete(tempPath); + fileSystem.File.Delete(tempPath); } Assert.IsFalse(await ioManager.FileExists(tempPath, default)); @@ -105,12 +119,12 @@ namespace Tgstation.Server.Host.IO.Tests [TestMethod] public async Task TestDirectoryExists() { - var tempPath = Path.GetTempFileName(); - File.Delete(tempPath); + var tempPath = fileSystem.Path.Combine(fileSystem.Path.GetTempPath(), fileSystem.Path.GetRandomFileName()); + fileSystem.File.Delete(tempPath); Assert.IsFalse(await ioManager.DirectoryExists(tempPath, default)); - Directory.CreateDirectory(tempPath); + fileSystem.Directory.CreateDirectory(tempPath); try { @@ -118,7 +132,7 @@ namespace Tgstation.Server.Host.IO.Tests } catch { - Directory.Delete(tempPath); + fileSystem.Directory.Delete(tempPath); throw; } } @@ -180,18 +194,18 @@ namespace Tgstation.Server.Host.IO.Tests async Task TestCopyDirectory(int? throttle) { - var tempPath = Path.GetTempFileName(); - File.Delete(tempPath); - Directory.CreateDirectory(tempPath); + var tempPath = fileSystem.Path.Combine(fileSystem.Path.GetTempPath(), fileSystem.Path.GetRandomFileName()); + fileSystem.File.Delete(tempPath); + fileSystem.Directory.CreateDirectory(tempPath); try { - var tempPath2 = Path.GetTempFileName(); - File.Delete(tempPath2); + var tempPath2 = fileSystem.Path.Combine(fileSystem.Path.GetTempPath(), fileSystem.Path.GetRandomFileName()); + fileSystem.File.Delete(tempPath2); - await File.WriteAllTextAsync(Path.Combine(tempPath, "file.txt"), "asdf"); - var subDir = Path.Combine(tempPath, "subdir"); - Directory.CreateDirectory(subDir); - await File.WriteAllTextAsync(Path.Combine(subDir, "file2.txt"), "fdsa"); + await fileSystem.File.WriteAllTextAsync(fileSystem.Path.Combine(tempPath, "file.txt"), "asdf"); + var subDir = fileSystem.Path.Combine(tempPath, "subdir"); + fileSystem.Directory.CreateDirectory(subDir); + await fileSystem.File.WriteAllTextAsync(fileSystem.Path.Combine(subDir, "file2.txt"), "fdsa"); try { @@ -203,26 +217,26 @@ namespace Tgstation.Server.Host.IO.Tests throttle, default); - Assert.IsTrue(Directory.Exists(tempPath2)); - var newFilePath = Path.Combine(tempPath2, "file.txt"); - Assert.IsTrue(File.Exists(newFilePath)); - var newFileText = await File.ReadAllTextAsync(newFilePath); + Assert.IsTrue(fileSystem.Directory.Exists(tempPath2)); + var newFilePath = fileSystem.Path.Combine(tempPath2, "file.txt"); + Assert.IsTrue(fileSystem.File.Exists(newFilePath)); + var newFileText = await fileSystem.File.ReadAllTextAsync(newFilePath); Assert.AreEqual("asdf", newFileText); - var newDirPath = Path.Combine(tempPath2, "subdir"); - Assert.IsTrue(Directory.Exists(newDirPath)); - var newFile2Path = Path.Combine(newDirPath, "file2.txt"); - Assert.IsTrue(File.Exists(newFile2Path)); - var newFile2Text = await File.ReadAllTextAsync(newFile2Path); + var newDirPath = fileSystem.Path.Combine(tempPath2, "subdir"); + Assert.IsTrue(fileSystem.Directory.Exists(newDirPath)); + var newFile2Path = fileSystem.Path.Combine(newDirPath, "file2.txt"); + Assert.IsTrue(fileSystem.File.Exists(newFile2Path)); + var newFile2Text = await fileSystem.File.ReadAllTextAsync(newFile2Path); Assert.AreEqual("fdsa", newFile2Text); } finally { - Directory.Delete(tempPath2, true); + fileSystem.Directory.Delete(tempPath2, true); } } finally { - Directory.Delete(tempPath, true); + fileSystem.Directory.Delete(tempPath, true); } } } diff --git a/tests/Tgstation.Server.Host.Tests/System/TestPosixSignalHandler.cs b/tests/Tgstation.Server.Host.Tests/System/TestPosixSignalHandler.cs index dd0f5fd5fb..dd4f2debf3 100644 --- a/tests/Tgstation.Server.Host.Tests/System/TestPosixSignalHandler.cs +++ b/tests/Tgstation.Server.Host.Tests/System/TestPosixSignalHandler.cs @@ -1,5 +1,6 @@ using System; using System.IO; +using System.IO.Abstractions; using System.Reflection; using System.Threading; using System.Threading.Tasks; @@ -58,7 +59,8 @@ namespace Tgstation.Server.Host.System.Tests processExecutor = new ProcessExecutor( new PosixProcessFeatures( new Lazy(() => processExecutor), - new DefaultIOManager(), + new DefaultIOManager( + new FileSystem()), loggerFactory.CreateLogger()), Mock.Of(), loggerFactory.CreateLogger(), diff --git a/tests/Tgstation.Server.Host.Tests/System/TestProcessFeatures.cs b/tests/Tgstation.Server.Host.Tests/System/TestProcessFeatures.cs index a32b9e389c..34d902e8e8 100644 --- a/tests/Tgstation.Server.Host.Tests/System/TestProcessFeatures.cs +++ b/tests/Tgstation.Server.Host.Tests/System/TestProcessFeatures.cs @@ -2,6 +2,7 @@ using Microsoft.VisualStudio.TestTools.UnitTesting; using Moq; using System; +using System.IO.Abstractions.TestingHelpers; using System.Runtime.InteropServices; using Tgstation.Server.Host.IO; @@ -21,7 +22,7 @@ namespace Tgstation.Server.Host.System.Tests { features = RuntimeInformation.IsOSPlatform(OSPlatform.Windows) ? (IProcessFeatures)new WindowsProcessFeatures(Mock.Of>()) - : new PosixProcessFeatures(new Lazy(() => null), new DefaultIOManager(), Mock.Of>()); + : new PosixProcessFeatures(new Lazy(() => null), new DefaultIOManager(new MockFileSystem()), Mock.Of>()); } [TestMethod] diff --git a/tests/Tgstation.Server.Host.Tests/System/TestSymlinkFactory.cs b/tests/Tgstation.Server.Host.Tests/System/TestSymlinkFactory.cs index 4d3c5be749..222e870cd0 100644 --- a/tests/Tgstation.Server.Host.Tests/System/TestSymlinkFactory.cs +++ b/tests/Tgstation.Server.Host.Tests/System/TestSymlinkFactory.cs @@ -1,5 +1,6 @@ using System; using System.IO; +using System.IO.Abstractions; using System.Threading; using System.Threading.Tasks; @@ -13,8 +14,10 @@ namespace Tgstation.Server.Host.System.Tests public sealed class TestSymlinkFactory { readonly IFilesystemLinkFactory factory = new PlatformIdentifier().IsWindows - ? new WindowsFilesystemLinkFactory() - : new PosixFilesystemLinkFactory(); + ? new WindowsFilesystemLinkFactory( + new FileSystem()) + : new PosixFilesystemLinkFactory( + new FileSystem()); [TestMethod] public async Task TestSymlinks() diff --git a/tests/Tgstation.Server.Host.Tests/TestProgram.cs b/tests/Tgstation.Server.Host.Tests/TestProgram.cs index f04531154f..a4f9318e13 100644 --- a/tests/Tgstation.Server.Host.Tests/TestProgram.cs +++ b/tests/Tgstation.Server.Host.Tests/TestProgram.cs @@ -2,6 +2,7 @@ using Moq; using System; using System.IO; +using System.IO.Abstractions.TestingHelpers; using System.Threading; using System.Threading.Tasks; @@ -80,28 +81,29 @@ namespace Tgstation.Server.Host.Tests public async Task TestStandardRunWithExceptionAndWatchdog() { var mockServer = new Mock(); + var mockFs = new MockFileSystem(); var exception = new DivideByZeroException(); mockServer.Setup(x => x.Run(It.IsAny())).Throws(exception); mockServer.SetupGet(x => x.RestartRequested).Returns(true); var mockServerFactory = new Mock(); - mockServerFactory.SetupGet(x => x.IOManager).Returns(new DefaultIOManager()); + mockServerFactory.SetupGet(x => x.IOManager).Returns(new DefaultIOManager(mockFs)); mockServerFactory.Setup(x => x.CreateServer(It.IsNotNull(), It.IsAny(), It.IsAny())).ReturnsAsync(mockServer.Object); var program = new Program { ServerFactory = mockServerFactory.Object }; - var tempFileName = Path.GetTempFileName(); - File.Delete(tempFileName); + var tempFileName = mockFs.Path.Combine(mockFs.Path.GetTempPath(), mockFs.Path.GetRandomFileName()); + mockFs.File.Delete(tempFileName); try { var result = await program.Main(Array.Empty(), tempFileName); Assert.AreEqual(HostExitCode.Error, result); - Assert.AreEqual(exception.ToString(), File.ReadAllText(tempFileName)); + Assert.AreEqual(exception.ToString(), mockFs.File.ReadAllText(tempFileName)); } finally { - File.Delete(tempFileName); + mockFs.File.Delete(tempFileName); } } } diff --git a/tests/Tgstation.Server.Host.Tests/TestServerFactory.cs b/tests/Tgstation.Server.Host.Tests/TestServerFactory.cs index 9a96a8332f..a520286782 100644 --- a/tests/Tgstation.Server.Host.Tests/TestServerFactory.cs +++ b/tests/Tgstation.Server.Host.Tests/TestServerFactory.cs @@ -1,6 +1,7 @@ using Microsoft.VisualStudio.TestTools.UnitTesting; using Moq; using System; +using System.IO.Abstractions.TestingHelpers; using System.Threading.Tasks; using Tgstation.Server.Host.Core; @@ -20,11 +21,13 @@ namespace Tgstation.Server.Host.Tests [TestMethod] public void TestConstructor() { - Assert.ThrowsException(() => new ServerFactory(null, null)); + Assert.ThrowsException(() => new ServerFactory(null, null, null)); IAssemblyInformationProvider assemblyInformationProvider = Mock.Of(); - Assert.ThrowsException(() => new ServerFactory(assemblyInformationProvider, null)); + Assert.ThrowsException(() => new ServerFactory(assemblyInformationProvider, null, null)); IIOManager ioManager = Mock.Of(); - _ = new ServerFactory(assemblyInformationProvider, ioManager); + Assert.ThrowsException(() => new ServerFactory(assemblyInformationProvider, ioManager, null)); + var mockFileSystem = new MockFileSystem(); + _ = new ServerFactory(assemblyInformationProvider, ioManager, mockFileSystem); } [TestMethod] 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 b6cb2167ad..2b96d6fd6a 100644 --- a/tests/Tgstation.Server.Host.Tests/Tgstation.Server.Host.Tests.csproj +++ b/tests/Tgstation.Server.Host.Tests/Tgstation.Server.Host.Tests.csproj @@ -7,7 +7,9 @@ - + + + diff --git a/tests/Tgstation.Server.Tests/CachingFileDownloader.cs b/tests/Tgstation.Server.Tests/CachingFileDownloader.cs index de540b606d..16dce2af75 100644 --- a/tests/Tgstation.Server.Tests/CachingFileDownloader.cs +++ b/tests/Tgstation.Server.Tests/CachingFileDownloader.cs @@ -1,6 +1,7 @@ using System; using System.Collections.Generic; using System.IO; +using System.IO.Abstractions; using System.Threading; using System.Threading.Tasks; @@ -12,6 +13,8 @@ using Moq; using Tgstation.Server.Api.Models; using Tgstation.Server.Api.Models.Internal; using Tgstation.Server.Common.Http; +using Tgstation.Server.Host.Components.Engine; +using Tgstation.Server.Host.Configuration; using Tgstation.Server.Host.Extensions; using Tgstation.Server.Host.IO; using Tgstation.Server.Host.System; @@ -44,9 +47,9 @@ namespace Tgstation.Server.Tests var logger = loggerFactory.CreateLogger("CachingFileDownloader"); var cfd = new CachingFileDownloader(loggerFactory.CreateLogger()); - var edgeVersion = await EngineTest.GetEdgeVersion(Api.Models.EngineType.Byond, cfd, cancellationToken); - await InitializeByondVersion(logger, edgeVersion.Version, new PlatformIdentifier().IsWindows, cancellationToken); + // this also will inject the edge version + var edgeVersion = await EngineTest.GetEdgeVersion(Api.Models.EngineType.Byond, logger, cfd, cancellationToken); // predownload the target github release update asset var gitHubToken = Environment.GetEnvironmentVariable("TGS_TEST_GITHUB_TOKEN"); @@ -78,7 +81,7 @@ namespace Tgstation.Server.Tests ServiceCollectionExtensions.UseFileDownloader(); } - public static async ValueTask InitializeByondVersion(ILogger logger, Version byondVersion, bool windows, CancellationToken cancellationToken) + public static async ValueTask InitializeByondVersion(ILogger logger, Version byondVersion, bool windows, CancellationToken cancellationToken, string urlCacheOverrideTemplate = null) { var version = new EngineVersion { @@ -86,25 +89,50 @@ namespace Tgstation.Server.Tests Version = byondVersion, }; - var url = new Uri( - $"https://www.byond.com/download/build/{version.Version.Major}/{version.Version.Major}.{version.Version.Minor}_byond{(!windows ? "_linux" : string.Empty)}.zip"); - string path = null; - if (TestingUtils.RunningInGitHubActions) - { - // actions is supposed to cache BYOND for us + var urlTemplate = TestingUtils.ByondZipDownloadTemplate; - var dir = Path.Combine( + var url = ByondInstallerBase.GetDownloadZipUrl(byondVersion, urlTemplate, new PlatformIdentifier().IsWindows ? "Windows" : "Linux"); + string path = null; + string basePath = Environment.GetEnvironmentVariable("TGS_TEST_BYOND_ZIPS_BASE_PATH"); + if (basePath == null && TestingUtils.RunningInGitHubActions) + { + // actions is supposed to cache BYOND for us here + basePath = Path.Combine( Environment.GetFolderPath( Environment.SpecialFolder.UserProfile, Environment.SpecialFolderOption.DoNotVerify), - "byond-zips-cache", + "byond-zips-cache"); + } + + if (basePath != null) + { + var dir = Path.Combine( + basePath, + "live", windows ? "windows" : "linux"); path = Path.Combine( dir, + $"{version.Version.Major}.{version.Version.Minor}", $"{version.Version.Major}.{version.Version.Minor}.zip"); } - await (await CacheFile(logger, url, null, path, cancellationToken)).DisposeAsync(); + Uri overrideUrl = null; + if (urlCacheOverrideTemplate != null) + { + overrideUrl = url; + url = ByondInstallerBase.GetDownloadZipUrl(byondVersion, urlCacheOverrideTemplate, new PlatformIdentifier().IsWindows ? "Windows" : "Linux"); + } + + await (await CacheFile( + logger, + url, + null, + path, + cancellationToken)) + .DisposeAsync(); + + if (overrideUrl != null) + cachedPaths[overrideUrl.ToString()] = cachedPaths[url.ToString()]; } public static void Cleanup() @@ -195,7 +223,7 @@ namespace Tgstation.Server.Tests try { Directory.CreateDirectory(Path.GetDirectoryName(path)); - await using var fs = new DefaultIOManager().CreateAsyncSequentialWriteStream(path); + await using var fs = new DefaultIOManager(new FileSystem()).CreateAsyncSequentialWriteStream(path); await ms.CopyToAsync(fs, cancellationToken); cachedPaths.Add(url.ToString(), Tuple.Create(path, temporal)); diff --git a/tests/Tgstation.Server.Tests/Live/Instance/ConfigurationTest.cs b/tests/Tgstation.Server.Tests/Live/Instance/ConfigurationTest.cs index eb39799414..52217388c1 100644 --- a/tests/Tgstation.Server.Tests/Live/Instance/ConfigurationTest.cs +++ b/tests/Tgstation.Server.Tests/Live/Instance/ConfigurationTest.cs @@ -1,5 +1,6 @@ using System; using System.IO; +using System.IO.Abstractions; using System.Linq; using System.Text; using System.Threading; @@ -90,7 +91,8 @@ namespace Tgstation.Server.Tests.Live.Instance public ValueTask SetupDMApiTests(bool includingRoot, CancellationToken cancellationToken) { // just use an I/O manager here - var ioManager = new DefaultIOManager(); + var ioManager = new DefaultIOManager( + new FileSystem()); async ValueTask TestStaticFileAndDir() { @@ -127,7 +129,7 @@ namespace Tgstation.Server.Tests.Live.Instance Path = $"/EventScripts/{scriptName}" }; - await using var readStream = ioManager.GetFileStream($"../../../../DMAPI/{(basic ? "BasicOperation" : "LongRunning")}/{scriptName}", false); + await using var readStream = ioManager.CreateAsyncReadStream($"../../../../DMAPI/{(basic ? "BasicOperation" : "LongRunning")}/{scriptName}", true, false); await configurationClient.Write( resourcingScript, readStream, diff --git a/tests/Tgstation.Server.Tests/Live/Instance/EngineTest.cs b/tests/Tgstation.Server.Tests/Live/Instance/EngineTest.cs index ffdaeb63e4..1497054e7d 100644 --- a/tests/Tgstation.Server.Tests/Live/Instance/EngineTest.cs +++ b/tests/Tgstation.Server.Tests/Live/Instance/EngineTest.cs @@ -41,13 +41,13 @@ namespace Tgstation.Server.Tests.Live.Instance EngineVersion testVersion; readonly EngineType testEngine = engineType; - public Task Run(CancellationToken cancellationToken, out Task firstInstall) + public Task Run(ILogger logger, CancellationToken cancellationToken, out Task firstInstall) { - firstInstall = RunPartOne(cancellationToken); + firstInstall = RunPartOne(logger, cancellationToken); return RunContinued(firstInstall, cancellationToken); } - public static async ValueTask GetEdgeVersion(EngineType engineType, IFileDownloader fileDownloader, CancellationToken cancellationToken) + public static async ValueTask GetEdgeVersion(EngineType engineType, ILogger logger, IFileDownloader fileDownloader, CancellationToken cancellationToken) { var edgeVersion = edgeVersions[engineType]; @@ -57,26 +57,7 @@ namespace Tgstation.Server.Tests.Live.Instance EngineVersion engineVersion; if (engineType == EngineType.Byond) { - await using var provider = fileDownloader.DownloadFile(new Uri("https://www.byond.com/download/version.txt"), null); - var stream = await provider.GetResult(cancellationToken); - using var reader = new StreamReader(stream, Encoding.UTF8, false, -1, true); - var text = await reader.ReadToEndAsync(cancellationToken); - var splits = text.Split('\n', StringSplitOptions.TrimEntries | StringSplitOptions.RemoveEmptyEntries); - - var targetVersion = splits.Last(); - - var badVersionMap = new PlatformIdentifier().IsWindows - ? [] - // linux map also needs updating in CI - : new Dictionary() - { - { "515.1612", "515.1611" } - }; - - badVersionMap.Add("515.1617", "515.1616"); - - if (badVersionMap.TryGetValue(targetVersion, out var remappedVersion)) - targetVersion = remappedVersion; + var targetVersion = await TestingUtils.GetByondEdgeVersion(logger, fileDownloader, cancellationToken); Assert.IsTrue(EngineVersion.TryParse(targetVersion, out engineVersion), $"Bad version: {targetVersion}"); } @@ -112,9 +93,9 @@ namespace Tgstation.Server.Tests.Live.Instance return edgeVersions[engineType] = engineVersion; } - async Task RunPartOne(CancellationToken cancellationToken) + async Task RunPartOne(ILogger logger, CancellationToken cancellationToken) { - testVersion = await GetEdgeVersion(testEngine, fileDownloader, cancellationToken); + testVersion = await GetEdgeVersion(testEngine, logger, fileDownloader, cancellationToken); await TestNoVersion(cancellationToken); await TestInstallNullVersion(cancellationToken); await TestInstallStable(cancellationToken); @@ -284,8 +265,11 @@ namespace Tgstation.Server.Tests.Live.Instance async Task TestCustomInstalls(CancellationToken cancellationToken) { - var generalConfigOptionsMock = new Mock>(); - generalConfigOptionsMock.SetupGet(x => x.Value).Returns(new GeneralConfiguration()); + var generalConfigOptionsMock = new Mock>(); + generalConfigOptionsMock.SetupGet(x => x.CurrentValue).Returns(new GeneralConfiguration + { + ByondZipDownloadTemplate = TestingUtils.ByondZipDownloadTemplate, + }); var sessionConfigOptionsMock = new Mock>(); sessionConfigOptionsMock.SetupGet(x => x.Value).Returns(new SessionConfiguration()); @@ -303,6 +287,7 @@ namespace Tgstation.Server.Tests.Live.Instance Mock.Of(), Mock.Of(), fileDownloader, + generalConfigOptionsMock.Object, Mock.Of>()); using var windowsByondInstaller = byondInstaller as WindowsByondInstaller; diff --git a/tests/Tgstation.Server.Tests/Live/Instance/InstanceTest.cs b/tests/Tgstation.Server.Tests/Live/Instance/InstanceTest.cs index f9188313d0..5fd65574eb 100644 --- a/tests/Tgstation.Server.Tests/Live/Instance/InstanceTest.cs +++ b/tests/Tgstation.Server.Tests/Live/Instance/InstanceTest.cs @@ -1,5 +1,6 @@ using System; using System.Collections.Generic; +using System.IO.Abstractions; using System.Linq; using System.Threading; using System.Threading.Tasks; @@ -36,6 +37,7 @@ namespace Tgstation.Server.Tests.Live.Instance readonly ushort serverPort = serverPort; public async Task RunTests( + ILogger logger, IInstanceClient instanceClient, ushort dmPort, ushort ddPort, @@ -44,14 +46,14 @@ namespace Tgstation.Server.Tests.Live.Instance bool usingBasicWatchdog, CancellationToken cancellationToken) { - var testVersion = await EngineTest.GetEdgeVersion(EngineType.Byond, fileDownloader, cancellationToken); + var testVersion = await EngineTest.GetEdgeVersion(EngineType.Byond, logger, fileDownloader, cancellationToken); await using var engineTest = new EngineTest(instanceClient.Engine, instanceClient.Jobs, fileDownloader, instanceClient.Metadata, testVersion.Engine.Value); await using var chatTest = new ChatTest(instanceClient.ChatBots, instanceManagerClient, instanceClient.Jobs, instanceClient.Metadata); var configTest = new ConfigurationTest(instanceClient.Configuration, instanceClient.Metadata); await using var repoTest = new RepositoryTest(instanceClient, instanceClient.Repository, instanceClient.Jobs); await using var dmTest = new DeploymentTest(instanceClient, instanceClient.Jobs, dmPort, ddPort, lowPrioDeployment, testVersion); - var byondTask = engineTest.Run(cancellationToken, out var firstInstall); + var byondTask = engineTest.Run(logger, cancellationToken, out var firstInstall); var chatTask = chatTest.RunPreWatchdog(cancellationToken); var repoLongJob = await repoTest.RunLongClone(cancellationToken); @@ -89,25 +91,26 @@ namespace Tgstation.Server.Tests.Live.Instance Uri openDreamUrl, CancellationToken cancellationToken) { - var ioManager = new DefaultIOManager(); + var ioManager = new DefaultIOManager(new FileSystem()); var odRepoDir = ioManager.ConcatPath( Environment.GetFolderPath( Environment.SpecialFolder.LocalApplicationData, Environment.SpecialFolderOption.DoNotVerify), new AssemblyInformationProvider().VersionPrefix, "OpenDreamRepository"); - var odRepoIoManager = new ResolvingIOManager(ioManager, odRepoDir); + var odRepoIoManager = ioManager.CreateResolverForSubdirectory(odRepoDir); - var mockOptions = new Mock>(); + var mockOptionsMonitor = new Mock>(); var genConfig = new GeneralConfiguration { OpenDreamGitUrl = openDreamUrl, + ByondZipDownloadTemplate = TestingUtils.ByondZipDownloadTemplate, }; - mockOptions.SetupGet(x => x.Value).Returns(genConfig); + mockOptionsMonitor.SetupGet(x => x.CurrentValue).Returns(genConfig); IEngineInstaller byondInstaller = compatVersion.Engine == EngineType.OpenDream ? new OpenDreamInstaller( - new DefaultIOManager(), + ioManager, Mock.Of>(), new PlatformIdentifier(), Mock.Of(), @@ -124,20 +127,21 @@ namespace Tgstation.Server.Tests.Live.Instance genConfig), Mock.Of(), Mock.Of(), - mockOptions.Object, + Options.Create(genConfig), Options.Create(new SessionConfiguration())) : new PlatformIdentifier().IsWindows ? new WindowsByondInstaller( Mock.Of(), Mock.Of(), fileDownloader, - Options.Create(genConfig), + mockOptionsMonitor.Object, Options.Create(new SessionConfiguration()), Mock.Of>()) : new PosixByondInstaller( Mock.Of(), Mock.Of(), fileDownloader, + mockOptionsMonitor.Object, Mock.Of>()); using var windowsByondInstaller = byondInstaller as WindowsByondInstaller; diff --git a/tests/Tgstation.Server.Tests/Live/Instance/WatchdogTest.cs b/tests/Tgstation.Server.Tests/Live/Instance/WatchdogTest.cs index adeddfb9d6..02d086e055 100644 --- a/tests/Tgstation.Server.Tests/Live/Instance/WatchdogTest.cs +++ b/tests/Tgstation.Server.Tests/Live/Instance/WatchdogTest.cs @@ -14,6 +14,7 @@ using System; using System.Collections.Generic; using System.Globalization; using System.IO; +using System.IO.Abstractions; using System.Linq; using System.Net; using System.Net.Sockets; @@ -815,7 +816,7 @@ namespace Tgstation.Server.Tests.Live.Instance var features = new PosixProcessFeatures( new Lazy(Mock.Of()), - new DefaultIOManager(), + new DefaultIOManager(new FileSystem()), Mock.Of>()); features.SuspendProcess(proc); @@ -882,7 +883,7 @@ namespace Tgstation.Server.Tests.Live.Instance executor = new ProcessExecutor( RuntimeInformation.IsOSPlatform(OSPlatform.Windows) ? new WindowsProcessFeatures(Mock.Of>()) - : new PosixProcessFeatures(new Lazy(() => executor), new DefaultIOManager(), Mock.Of>()), + : new PosixProcessFeatures(new Lazy(() => executor), new DefaultIOManager(new FileSystem()), Mock.Of>()), Mock.Of(), Mock.Of>(), LoggerFactory.Create(x => { })); diff --git a/tests/Tgstation.Server.Tests/Live/LiveTestingServer.cs b/tests/Tgstation.Server.Tests/Live/LiveTestingServer.cs index ac4f519fa6..d465adc1d6 100644 --- a/tests/Tgstation.Server.Tests/Live/LiveTestingServer.cs +++ b/tests/Tgstation.Server.Tests/Live/LiveTestingServer.cs @@ -6,6 +6,7 @@ using System; using System.Collections.Generic; using System.Globalization; using System.IO; +using System.IO.Abstractions; using System.Linq; using System.Threading; using System.Threading.Tasks; @@ -43,7 +44,7 @@ namespace Tgstation.Server.Tests.Live for (int i = 0; i < 5; ++i) try { - new DefaultIOManager().DeleteDirectory(directory, default).GetAwaiter().GetResult(); + new DefaultIOManager(new FileSystem()).DeleteDirectory(directory, default).GetAwaiter().GetResult(); } catch { @@ -156,7 +157,8 @@ namespace Tgstation.Server.Tests.Live $"Security:TokenExpiryMinutes=120", // timeouts are useless for us $"General:OpenDreamSuppressInstallOutput={TestingUtils.RunningInGitHubActions}", "Telemetry:DisableVersionReporting=true", - $"General:PrometheusPort={port}" + $"General:PrometheusPort={port}", + $"General:ByondZipDownloadTemplate={TestingUtils.ByondZipDownloadTemplate}" }; if (MultiServerClient.UseGraphQL) diff --git a/tests/Tgstation.Server.Tests/Live/TestLiveServer.cs b/tests/Tgstation.Server.Tests/Live/TestLiveServer.cs index a4b63857f1..027a6aa0ab 100644 --- a/tests/Tgstation.Server.Tests/Live/TestLiveServer.cs +++ b/tests/Tgstation.Server.Tests/Live/TestLiveServer.cs @@ -2,6 +2,7 @@ using System.Collections.Generic; using System.Diagnostics; using System.IO; +using System.IO.Abstractions; using System.Linq; using System.Management; using System.Net; @@ -415,7 +416,7 @@ namespace Tgstation.Server.Tests.Live var gitHubToken = Environment.GetEnvironmentVariable("TGS_TEST_GITHUB_TOKEN"); if (String.IsNullOrWhiteSpace(gitHubToken)) gitHubToken = null; - await new Host.IO.DefaultIOManager().DeleteDirectory(server.UpdatePath, cancellationToken); + await new Host.IO.DefaultIOManager(new FileSystem()).DeleteDirectory(server.UpdatePath, cancellationToken); serverTask = server.Run(cancellationToken).AsTask(); await using (var adminClient = await CreateAdminClient(server.ApiUrl, cancellationToken)) @@ -922,6 +923,12 @@ namespace Tgstation.Server.Tests.Live await using var node1Client = await CreateAdminClient(node1.ApiUrl, cancellationToken); await using var node2Client = await CreateAdminClient(node2.ApiUrl, cancellationToken); + // test a token signed from any one node will work on another + var token = node2Client.RestClient.Token; + var testNode1Client = restClientFactory.CreateFromToken(node1.ApiUrl, token); + + await testNode1Client.ServerInformation(cancellationToken); + var controllerInfo = await controllerClient.RestClient.ServerInformation(cancellationToken); async Task WaitForSwarmServerUpdate(IRestServerClient client, int currentServerCount) @@ -1129,7 +1136,7 @@ namespace Tgstation.Server.Tests.Live ApiValidationSecurityLevel = DreamDaemonSecurity.Trusted, }, cancellationToken); - var ioManager = new Host.IO.DefaultIOManager(); + var ioManager = new Host.IO.DefaultIOManager(new FileSystem()); var repoPath = ioManager.ConcatPath(instance.Path, "Repository"); await using var jobsTest = new JobsRequiredTest(instanceClient.Jobs); var postWriteHandler = (Host.IO.IPostWriteHandler)(new PlatformIdentifier().IsWindows @@ -1415,6 +1422,7 @@ namespace Tgstation.Server.Tests.Live await Task.Yield(); InstanceManager GetInstanceManager() => ((Host.Server)server.RealServer).Host.Services.GetRequiredService(); + ILogger GetLogger() => ((Host.Server)server.RealServer).Host.Services.GetRequiredService>(); // main run var serverTask = server.Run(cancellationToken).AsTask(); @@ -1587,7 +1595,7 @@ namespace Tgstation.Server.Tests.Live var testSerialized = TestingUtils.RunningInGitHubActions; // they only have 2 cores, can't handle intense parallelization async Task ODCompatTests() { - var edgeODVersionTask = EngineTest.GetEdgeVersion(EngineType.OpenDream, fileDownloader, cancellationToken); + var edgeODVersionTask = EngineTest.GetEdgeVersion(EngineType.OpenDream, GetLogger(), fileDownloader, cancellationToken); var ex = await Assert.ThrowsExceptionAsync( () => InstanceTest.DownloadEngineVersion( @@ -1622,6 +1630,16 @@ namespace Tgstation.Server.Tests.Live if (openDreamOnly) return; + var windowsMinCompat = new Version(510, 1346); + var linuxMinCompat = new Version(512, 1451); // http://www.byond.com/forum/?forum=5&command=search&scope=local&text=resolved%3a512.1451 + await CachingFileDownloader.InitializeByondVersion( + GetLogger(), + new PlatformIdentifier().IsWindows + ? windowsMinCompat + : linuxMinCompat, + new PlatformIdentifier().IsWindows, + cancellationToken); + var compatTests = FailFast( instanceTest .RunCompatTests( @@ -1629,8 +1647,8 @@ namespace Tgstation.Server.Tests.Live { Engine = EngineType.Byond, Version = new PlatformIdentifier().IsWindows - ? new Version(510, 1346) - : new Version(512, 1451) // http://www.byond.com/forum/?forum=5&command=search&scope=local&text=resolved%3a512.1451 + ? windowsMinCompat + : linuxMinCompat, }, server.OpenDreamUrl, firstAdminRestClient.Instances.CreateClient(compatInstance), @@ -1646,6 +1664,7 @@ namespace Tgstation.Server.Tests.Live await FailFast( instanceTest .RunTests( + GetLogger(), instanceClient, mainDMPort.Value, mainDDPort.Value, @@ -1878,7 +1897,7 @@ namespace Tgstation.Server.Tests.Live preStartupTime = DateTimeOffset.UtcNow; serverTask = server.Run(cancellationToken).AsTask(); long expectedCompileJobId, expectedStaged; - var edgeVersion = await EngineTest.GetEdgeVersion(EngineType.Byond, fileDownloader, cancellationToken); + var edgeVersion = await EngineTest.GetEdgeVersion(EngineType.Byond, GetLogger(), fileDownloader, cancellationToken); await using (var adminClient = await CreateAdminClient(server.ApiUrl, cancellationToken)) { var restAdminClient = adminClient.RestClient; diff --git a/tests/Tgstation.Server.Tests/TestRepository.cs b/tests/Tgstation.Server.Tests/TestRepository.cs index 59b4783994..96a2349bea 100644 --- a/tests/Tgstation.Server.Tests/TestRepository.cs +++ b/tests/Tgstation.Server.Tests/TestRepository.cs @@ -1,5 +1,6 @@ using System; using System.IO; +using System.IO.Abstractions; using System.Linq; using System.Threading; using System.Threading.Tasks; @@ -58,7 +59,7 @@ namespace Tgstation.Server.Tests } finally { - await new DefaultIOManager().DeleteDirectory( + await new DefaultIOManager(new FileSystem()).DeleteDirectory( Path.GetDirectoryName(tempPath), CancellationToken.None); } @@ -75,8 +76,7 @@ namespace Tgstation.Server.Tests using var manager = new RepositoryManager( repoFac, commands, - new ResolvingIOManager( - new DefaultIOManager(), + new DefaultIOManager(new FileSystem()).CreateResolverForSubdirectory( tempPath), Mock.Of(), new WindowsPostWriteHandler(), @@ -131,7 +131,7 @@ namespace Tgstation.Server.Tests } finally { - await new DefaultIOManager().DeleteDirectory( + await new DefaultIOManager(new FileSystem()).DeleteDirectory( Path.GetDirectoryName(tempPath), CancellationToken.None); } diff --git a/tests/Tgstation.Server.Tests/TestSystemInteraction.cs b/tests/Tgstation.Server.Tests/TestSystemInteraction.cs index e7c8e28962..f2601acdee 100644 --- a/tests/Tgstation.Server.Tests/TestSystemInteraction.cs +++ b/tests/Tgstation.Server.Tests/TestSystemInteraction.cs @@ -1,5 +1,6 @@ using System; using System.IO; +using System.IO.Abstractions; using System.Threading; using System.Threading.Tasks; @@ -24,7 +25,7 @@ namespace Tgstation.Server.Tests var platformIdentifier = new PlatformIdentifier(); var processExecutor = new ProcessExecutor( Mock.Of(), - new DefaultIOManager(), + new DefaultIOManager(new FileSystem()), Mock.Of>(), loggerFactory); @@ -52,7 +53,7 @@ namespace Tgstation.Server.Tests var platformIdentifier = new PlatformIdentifier(); var processExecutor = new ProcessExecutor( Mock.Of(), - new DefaultIOManager(), + new DefaultIOManager(new FileSystem()), loggerFactory.CreateLogger(), loggerFactory); diff --git a/tests/Tgstation.Server.Tests/TestVersions.cs b/tests/Tgstation.Server.Tests/TestVersions.cs index a0809b78f6..d44fded9e2 100644 --- a/tests/Tgstation.Server.Tests/TestVersions.cs +++ b/tests/Tgstation.Server.Tests/TestVersions.cs @@ -33,6 +33,7 @@ using Tgstation.Server.Host.System; using Tgstation.Server.Api.Models; using Tgstation.Server.Tests.Live; using Tgstation.Server.Host.Properties; +using System.IO.Abstractions; namespace Tgstation.Server.Tests { @@ -103,10 +104,14 @@ namespace Tgstation.Server.Tests } [TestMethod] + [TestCategory("RequiresDatabase")] public async Task TestDDExeByondVersion() { - var mockGeneralConfigurationOptions = new Mock>(); - mockGeneralConfigurationOptions.SetupGet(x => x.Value).Returns(new GeneralConfiguration()); + var mockGeneralConfigurationOptions = new Mock>(); + mockGeneralConfigurationOptions.SetupGet(x => x.CurrentValue).Returns(new GeneralConfiguration + { + ByondZipDownloadTemplate = TestingUtils.ByondZipDownloadTemplate, + }); var mockSessionConfigurationOptions = new Mock>(); mockSessionConfigurationOptions.SetupGet(x => x.Value).Returns(new SessionConfiguration()); @@ -165,12 +170,14 @@ namespace Tgstation.Server.Tests static Version MapThreadsVersion() => (Version)typeof(ByondInstallerBase).GetField("MapThreadsVersion", BindingFlags.Static | BindingFlags.NonPublic).GetValue(null) ?? throw new InvalidOperationException("Couldn't find MapThreadsVersion"); [TestMethod] + [TestCategory("RequiresDatabase")] public async Task TestMapThreadsByondVersion() { - var mockGeneralConfigurationOptions = new Mock>(); - mockGeneralConfigurationOptions.SetupGet(x => x.Value).Returns(new GeneralConfiguration + var mockGeneralConfigurationOptions = new Mock>(); + mockGeneralConfigurationOptions.SetupGet(x => x.CurrentValue).Returns(new GeneralConfiguration { SkipAddingByondFirewallException = true, + ByondZipDownloadTemplate = TestingUtils.ByondZipDownloadTemplate, }); var mockSessionConfigurationOptions = new Mock>(); mockSessionConfigurationOptions.SetupGet(x => x.Value).Returns(new SessionConfiguration()); @@ -207,8 +214,9 @@ namespace Tgstation.Server.Tests loggerFactory.CreateLogger()) : new PosixByondInstaller( new PosixPostWriteHandler(loggerFactory.CreateLogger()), - new DefaultIOManager(), + new DefaultIOManager(new FileSystem()), fileDownloader, + mockGeneralConfigurationOptions.Object, loggerFactory.CreateLogger()); using var disposable = byondInstaller as IDisposable; @@ -217,13 +225,13 @@ namespace Tgstation.Server.Tests ? new WindowsProcessFeatures(Mock.Of>()) : new PosixProcessFeatures( new Lazy(() => null), - new DefaultIOManager(), + new DefaultIOManager(new FileSystem()), loggerFactory.CreateLogger()), Mock.Of(), loggerFactory.CreateLogger(), loggerFactory); - var ioManager = new DefaultIOManager(); + var ioManager = new DefaultIOManager(new FileSystem()); var tempPath = ioManager.ConcatPath(LiveTestingServer.BaseDirectory, "mapthreads"); await ioManager.CreateDirectory(tempPath, default); try diff --git a/tests/Tgstation.Server.Tests/TestingUtils.cs b/tests/Tgstation.Server.Tests/TestingUtils.cs index 3167d2c3d3..a6a6683f36 100644 --- a/tests/Tgstation.Server.Tests/TestingUtils.cs +++ b/tests/Tgstation.Server.Tests/TestingUtils.cs @@ -1,16 +1,24 @@ using System; +using System.Collections.Generic; using System.IO; +using System.IO.Abstractions; using System.IO.Compression; +using System.Linq; +using System.Net.Http; using System.Reflection; +using System.Text; using System.Threading; using System.Threading.Tasks; using Microsoft.Extensions.Logging; using Microsoft.VisualStudio.TestTools.UnitTesting; + using Moq; using Tgstation.Server.Host.Components.Engine; +using Tgstation.Server.Host.Configuration; using Tgstation.Server.Host.IO; +using Tgstation.Server.Host.System; namespace Tgstation.Server.Tests { @@ -67,8 +75,91 @@ namespace Tgstation.Server.Tests } finally { - await new DefaultIOManager().DeleteDirectory(tempFolder, cancellationToken); + await new DefaultIOManager(new FileSystem()).DeleteDirectory(tempFolder, cancellationToken); } } + + static string byondZipDownloadTemplate; + + public static string ByondZipDownloadTemplate + { + get + { + if (byondZipDownloadTemplate == null) + { + var envvar = Environment.GetEnvironmentVariable("TGS_TEST_BYOND_ZIP_DOWNLOAD_TEMPLATE"); + if (envvar != null) + byondZipDownloadTemplate = envvar; + else + byondZipDownloadTemplate = GeneralConfiguration.DefaultByondZipDownloadTemplate; + } + + return byondZipDownloadTemplate; + } + } + + static string edgeVersion = null; + public static async ValueTask GetByondEdgeVersion(ILogger logger, IFileDownloader fileDownloader, CancellationToken cancellationToken) + { + if (edgeVersion != null) + return edgeVersion; + + async ValueTask GetVersionFromResponse(string versionTxt) + { + await using var provider = fileDownloader.DownloadFile(new Uri(versionTxt), null); + var stream = await provider.GetResult(cancellationToken); + using var reader = new StreamReader(stream, Encoding.UTF8, false, -1, true); + var text = await reader.ReadToEndAsync(cancellationToken); + var splits = text.Split('\n', StringSplitOptions.TrimEntries | StringSplitOptions.RemoveEmptyEntries); + + var targetVersion = splits.Last(); + + var badVersionMap = new PlatformIdentifier().IsWindows + ? [] + // linux map also needs updating in CI + : new Dictionary() + { + { "515.1612", "515.1611" } + }; + + badVersionMap.Add("515.1617", "515.1616"); + + if (badVersionMap.TryGetValue(targetVersion, out var remappedVersion)) + targetVersion = remappedVersion; + + return targetVersion; + } + + var mirroredVersionTxt = Environment.GetEnvironmentVariable("TGS_TEST_BYOND_MIRROR_VERSION_TXT"); + try + { + // always check byond.com first for latest up-to-date, mirror should ALWAYS have stable versions + // except byond hates all CI runners now + const string DefaultMirror = "https://spacestation13.github.io/byond-builds/version.txt"; + edgeVersion = await GetVersionFromResponse(DefaultMirror); + + logger.LogInformation("Downloading edge version from SS13 mirror {edge}", edgeVersion); + + // if we got the result from byond.com, make sure the cache grabs the zip from there as well + await CachingFileDownloader.InitializeByondVersion( + logger, + Version.Parse(edgeVersion), + new PlatformIdentifier().IsWindows, + cancellationToken, + "https://spacestation13.github.io/byond-builds/${Major}/${Major}.${Minor}_byond${Linux:_linux}.zip"); + } + catch (Exception ex) + { + logger.LogWarning(ex, "Cannot download zip from byond.com!"); + if (ByondZipDownloadTemplate == GeneralConfiguration.DefaultByondZipDownloadTemplate || mirroredVersionTxt == null) + throw; + + // fall back to the mirrored version.txt + await using var provider = fileDownloader.DownloadFile(new Uri(mirroredVersionTxt), null); + edgeVersion = await GetVersionFromResponse(mirroredVersionTxt); + } + + return edgeVersion; + } } } diff --git a/tests/Tgstation.Server.Tests/Tgstation.Server.Tests.csproj b/tests/Tgstation.Server.Tests/Tgstation.Server.Tests.csproj index 8b87acdc61..b2e176f1e3 100644 --- a/tests/Tgstation.Server.Tests/Tgstation.Server.Tests.csproj +++ b/tests/Tgstation.Server.Tests/Tgstation.Server.Tests.csproj @@ -5,10 +5,6 @@ $(TgsFrameworkVersion) - - - -