Merge pull request #2310 from tgstation/Omega [TGSDeploy]

v6.18.0: Merging way too many things at once
This commit is contained in:
Jordan Dominion
2025-07-27 13:30:05 -04:00
committed by GitHub
76 changed files with 1019 additions and 462 deletions
+3
View File
@@ -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.
+135 -46
View File
@@ -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/<TgsCoreVersion>${CURRENT_TGS_VERSION}/<TgsCoreVersion>${{ 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:
+1 -1
View File
@@ -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
+3 -1
View File
@@ -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
+3 -3
View File
@@ -13,14 +13,14 @@
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
<!-- Usage: Hard to say what exactly this is for, but not including it removes the test icon and breaks vstest.console.exe for some reason -->
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.13.0" Condition="'$(TgsTestNoSdk)' != 'true'" />
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.14.1" Condition="'$(TgsTestNoSdk)' != 'true'" />
<!-- Usage: Dependency mocking for tests -->
<!-- Pinned: Be VERY careful about updating https://github.com/moq/moq/issues/1372 -->
<PackageReference Include="Moq" Version="4.20.72" />
<!-- Usage: MSTest execution -->
<PackageReference Include="MSTest.TestAdapter" Version="3.8.3" />
<PackageReference Include="MSTest.TestAdapter" Version="3.9.3" />
<!-- Usage: MSTest asserts etc... -->
<PackageReference Include="MSTest.TestFramework" Version="3.8.3" />
<PackageReference Include="MSTest.TestFramework" Version="3.9.3" />
</ItemGroup>
</Project>
+2 -2
View File
@@ -3,8 +3,8 @@
<!-- Integration tests will ensure they match across the board -->
<Import Project="WebpanelVersion.props" />
<PropertyGroup>
<TgsCoreVersion>6.17.0</TgsCoreVersion>
<TgsConfigVersion>5.7.0</TgsConfigVersion>
<TgsCoreVersion>6.18.0</TgsCoreVersion>
<TgsConfigVersion>5.8.0</TgsConfigVersion>
<TgsRestVersion>10.13.0</TgsRestVersion>
<TgsGraphQLVersion>0.6.0</TgsGraphQLVersion>
<TgsCommonLibraryVersion>7.0.0</TgsCommonLibraryVersion>
-5
View File
@@ -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
+1
View File
@@ -21,6 +21,7 @@ Depends:
libstdc++6:i386 [amd64],
libstdc++6 [i386],
gcc-multilib [amd64],
libcurl4 [i386],
Recommends:
libsystemd0,
gdb,
+2 -1
View File
@@ -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 { };
};
}
+1
View File
@@ -86,6 +86,7 @@ stdenv.mkDerivation {
gcc_multi
glibc
bash
curl
];
nativeBuildInputs = with pkgs; [
makeWrapper
@@ -28,7 +28,7 @@
<!-- Usage: HTTP constants reference -->
<PackageReference Include="Microsoft.AspNetCore.Http.Extensions" Version="2.3.0" />
<!-- Usage: Decoding the 'nbf' property of JWTs -->
<PackageReference Include="Microsoft.IdentityModel.JsonWebTokens" Version="8.8.0" />
<PackageReference Include="Microsoft.IdentityModel.JsonWebTokens" Version="8.13.0" />
<!-- Usage: Data model annotating -->
<PackageReference Include="System.ComponentModel.Annotations" Version="5.0.0" />
</ItemGroup>
@@ -9,8 +9,7 @@
<ItemGroup>
<!-- GraphQL connector and code generator -->
<PackageReference Include="Microsoft.Bcl.AsyncInterfaces" Version="9.0.4" />
<PackageReference Include="StrawberryShake.Server" Version="15.1.3" />
<PackageReference Include="StrawberryShake.Server" Version="15.1.8" />
</ItemGroup>
<ItemGroup>
@@ -11,9 +11,9 @@
<ItemGroup>
<!-- Usage: Connecting to SignalR hubs in API -->
<PackageReference Include="Microsoft.AspNetCore.SignalR.Client" Version="9.0.4" />
<PackageReference Include="Microsoft.AspNetCore.SignalR.Client" Version="9.0.7" />
<!-- Usage: Using target JSON serializer for API -->
<PackageReference Include="Microsoft.AspNetCore.SignalR.Protocols.NewtonsoftJson" Version="9.0.4" />
<PackageReference Include="Microsoft.AspNetCore.SignalR.Protocols.NewtonsoftJson" Version="9.0.7" />
</ItemGroup>
<ItemGroup>
@@ -13,9 +13,9 @@
<ItemGroup>
<!-- Usage: Identifying if we're running under SystemD -->
<PackageReference Include="Microsoft.Extensions.Hosting.Systemd" Version="9.0.4" />
<PackageReference Include="Microsoft.Extensions.Hosting.Systemd" Version="9.0.7" />
<!-- Usage: Console logging plugin -->
<PackageReference Include="Microsoft.Extensions.Logging.Console" Version="9.0.4" />
<PackageReference Include="Microsoft.Extensions.Logging.Console" Version="9.0.7" />
</ItemGroup>
<ItemGroup>
@@ -21,21 +21,21 @@
<!-- Usage: Command line argument support -->
<PackageReference Include="McMaster.Extensions.CommandLineUtils" Version="4.1.1" />
<!-- Usage: Identifies when we are running in the context of the Windows SCM -->
<PackageReference Include="Microsoft.Extensions.Hosting.WindowsServices" Version="9.0.4" />
<PackageReference Include="Microsoft.Extensions.Hosting.WindowsServices" Version="9.0.7" />
<!-- Usage: Windows event log logging plugin -->
<PackageReference Include="Microsoft.Extensions.Logging.EventLog" Version="9.0.4" />
<PackageReference Include="Microsoft.Extensions.Logging.EventLog" Version="9.0.7" />
<!-- Usage: Console logging plugin -->
<PackageReference Include="Microsoft.Extensions.Logging.Console" Version="9.0.4" />
<PackageReference Include="Microsoft.Extensions.Logging.Console" Version="9.0.7" />
<!-- Usage: Updated transitive dependency of Core.System.ServiceProcess -->
<PackageReference Include="Newtonsoft.Json" Version="13.0.3" />
<!-- Usage: Updated transitive dependency of Core.System.ServiceProcess -->
<PackageReference Include="System.Drawing.Common" Version="9.0.4" />
<PackageReference Include="System.Drawing.Common" Version="9.0.7" />
<!-- Usage: Updated transitive dependency, unable to tell what of -->
<PackageReference Include="System.Private.Uri" Version="4.3.2" />
<!-- Usage: OS identification -->
<PackageReference Include="System.Runtime.InteropServices" Version="4.3.0" />
<!-- Usage: Windows Service Manager intergration -->
<PackageReference Include="System.ServiceProcess.ServiceController" Version="9.0.4" />
<PackageReference Include="System.ServiceProcess.ServiceController" Version="9.0.7" />
</ItemGroup>
<ItemGroup>
@@ -38,7 +38,8 @@
</Target>
<ItemGroup>
<PackageReference Include="StrawberryShake.Server" Version="15.1.3" />
<!-- GraphQL code generator -->
<PackageReference Include="StrawberryShake.Server" Version="15.1.8" />
</ItemGroup>
<ItemGroup>
@@ -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
@@ -10,7 +10,7 @@
<ItemGroup>
<!-- Usage: Logging abstractions -->
<PackageReference Include="Microsoft.Extensions.Logging" Version="9.0.4" />
<PackageReference Include="Microsoft.Extensions.Logging" Version="9.0.7" />
<!-- Usage: POSIX support for signals -->
<PackageReference Include="Mono.Posix.NETStandard" Version="1.0.0" />
</ItemGroup>
@@ -3,7 +3,7 @@
"isRoot": true,
"tools": {
"dotnet-ef": {
"version": "9.0.4",
"version": "9.0.7",
"commands": [
"dotnet-ef"
]
@@ -351,28 +351,11 @@ namespace Tgstation.Server.Host.Components.Chat
/// <inheritdoc />
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);
}
/// <inheritdoc />
public void QueueRawDeploymentMessage(string message)
=> QueueMessageGeneric(mapping => mapping.IsUpdatesChannel, message, null);
/// <inheritdoc />
public Func<string?, string, Action<bool>> QueueDeploymentMessage(
@@ -1111,5 +1094,38 @@ namespace Tgstation.Server.Host.Components.Chat
AddMessageTask(SendMessageTask());
}
/// <summary>
/// Queues a message to a selected set of <see cref="ChannelMapping"/>s.
/// </summary>
/// <param name="channelSelector">A <see cref="Predicate{T}"/> for selecting the <see cref="ChannelMapping"/>s to send to.</param>
/// <param name="message">The message to send.</param>
/// <param name="prefix">The optional prefix to the message to be sent.</param>
void QueueMessageGeneric(Predicate<ChannelMapping> 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);
}
}
}
@@ -57,6 +57,12 @@ namespace Tgstation.Server.Host.Components.Chat
/// <param name="message">The message being sent.</param>
void QueueWatchdogMessage(string message);
/// <summary>
/// Queue a chat <paramref name="message"/> to configured deployment channels.
/// </summary>
/// <param name="message">The message being sent.</param>
void QueueRawDeploymentMessage(string message);
/// <summary>
/// Send the message for a deployment to configured deployment channels.
/// </summary>
@@ -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
}
@@ -12,7 +12,18 @@ namespace Tgstation.Server.Host.Components.Deployment
sealed class DmbProvider : DmbProviderBase, IDmbProvider
{
/// <inheritdoc />
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);
}
}
/// <inheritdoc />
public override Models.CompileJob CompileJob { get; }
@@ -28,7 +39,7 @@ namespace Tgstation.Server.Host.Components.Deployment
/// <summary>
/// Extra path to add to the end of <see cref="CompileJob.DirectoryName"/>.
/// </summary>
readonly string directoryAppend;
readonly string? directoryAppend;
/// <summary>
/// The <see cref="Action"/> to run when <see cref="DisposeAsync"/> 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;
}
/// <inheritdoc />
@@ -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
/// <summary>
/// Recursively create tasks to create a hard link directory mirror of <paramref name="src"/> to <paramref name="dest"/>.
/// </summary>
/// <param name="src">The source directory path.</param>
/// <param name="src">The source <see cref="IDirectoryInfo"/>.</param>
/// <param name="dest">The destination directory path.</param>
/// <param name="semaphore">Optional <see cref="SemaphoreSlim"/> used to limit degree of parallelism.</param>
/// <param name="securityLevel">The launch <see cref="DreamDaemonSecurity"/> level.</param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> for the operation.</param>
/// <returns>A <see cref="IEnumerable{T}"/> of <see cref="Task"/>s representing the running operations. The first <see cref="Task"/> returned is always the necessary call to <see cref="IIOManager.CreateDirectory(string, CancellationToken)"/>.</returns>
/// <returns>An <see cref="IEnumerable{T}"/> of <see cref="Task"/>s representing the running operations. The first <see cref="Task"/> returned is always the necessary call to <see cref="IIOManager.CreateDirectory(string, CancellationToken)"/>.</returns>
/// <remarks>I genuinely don't know how this will work with symlinked files. Waiting for the issue report I guess.</remarks>
IEnumerable<Task> MirrorDirectoryImpl(string src, string dest, SemaphoreSlim? semaphore, DreamDaemonSecurity securityLevel, CancellationToken cancellationToken)
IEnumerable<Task> 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)
{
@@ -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
/// <inheritdoc />
protected override EngineType TargetEngineType => EngineType.Byond;
/// <summary>
/// The <see cref="GeneralConfiguration"/> <see cref="IOptionsMonitor{TOptions}"/> for the <see cref="ByondInstallerBase"/>.
/// </summary>
protected IOptionsMonitor<GeneralConfiguration> GeneralConfigurationOptions { get; }
/// <summary>
/// Path to the system user's local BYOND folder.
/// </summary>
@@ -52,25 +59,85 @@ namespace Tgstation.Server.Host.Components.Engine
protected abstract string DreamMakerName { get; }
/// <summary>
/// Gets the URL formatter string for downloading a byond version of {0:Major} {1:Minor}.
/// Template to do ${Marker:xxx} replacements in <see cref="GeneralConfiguration.ByondZipDownloadTemplate"/>.
/// </summary>
protected abstract string ByondRevisionsUrlTemplate { get; }
protected abstract string OSMarkerTemplate { get; }
/// <summary>
/// The <see cref="IFileDownloader"/> for the <see cref="ByondInstallerBase"/>.
/// </summary>
readonly IFileDownloader fileDownloader;
/// <summary>
/// Format a given <paramref name="byondZipDownloadTemplate"/>.
/// </summary>
/// <param name="semver">The BYOND version to download.</param>
/// <param name="byondZipDownloadTemplate">The template.</param>
/// <param name="osMarkerTemplate">The <see cref="OSMarkerTemplate"/>.</param>
/// <returns>The formatted byond download <see cref="Uri"/>.</returns>
/// <remarks>Exposed only for testability.</remarks>
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);
}
/// <summary>
/// Initializes a new instance of the <see cref="ByondInstallerBase"/> class.
/// </summary>
/// <param name="ioManager">The <see cref="IIOManager"/> for the <see cref="EngineInstallerBase"/>.</param>
/// <param name="logger">The <see cref="ILogger"/> for the <see cref="EngineInstallerBase"/>.</param>
/// <param name="fileDownloader">The value of <see cref="fileDownloader"/>.</param>
protected ByondInstallerBase(IIOManager ioManager, ILogger<ByondInstallerBase> logger, IFileDownloader fileDownloader)
/// <param name="generalConfigurationOptions">The value of <see cref="GeneralConfigurationOptions"/>.</param>
protected ByondInstallerBase(
IIOManager ioManager,
ILogger<ByondInstallerBase> logger,
IFileDownloader fileDownloader,
IOptionsMonitor<GeneralConfiguration> generalConfigurationOptions)
: base(ioManager, logger)
{
this.fileDownloader = fileDownloader ?? throw new ArgumentNullException(nameof(fileDownloader));
GeneralConfigurationOptions = generalConfigurationOptions ?? throw new ArgumentNullException(nameof(generalConfigurationOptions));
}
/// <inheritdoc />
@@ -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<IEngineInstallation>(
@@ -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);
}
}
}
@@ -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,
@@ -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;
/// <inheritdoc />
protected override string ByondRevisionsUrlTemplate => "https://www.byond.com/download/build/{0}/{0}.{1}_byond_linux.zip";
protected override string OSMarkerTemplate => "Linux";
/// <summary>
/// The <see cref="IPostWriteHandler"/> for the <see cref="PosixByondInstaller"/>.
@@ -52,13 +54,15 @@ namespace Tgstation.Server.Host.Components.Engine
/// <param name="postWriteHandler">The value of <see cref="postWriteHandler"/>.</param>
/// <param name="ioManager">The <see cref="IIOManager"/> for the <see cref="ByondInstallerBase"/>.</param>
/// <param name="fileDownloader">The <see cref="IFileDownloader"/> for the <see cref="ByondInstallerBase"/>.</param>
/// <param name="generalConfigurationOptions">The <see cref="GeneralConfiguration"/> <see cref="IOptionsMonitor{TOptions}"/>.</param>
/// <param name="logger">The <see cref="ILogger"/> for the <see cref="ByondInstallerBase"/>.</param>
public PosixByondInstaller(
IPostWriteHandler postWriteHandler,
IIOManager ioManager,
IFileDownloader fileDownloader,
IOptionsMonitor<GeneralConfiguration> generalConfigurationOptions,
ILogger<PosixByondInstaller> logger)
: base(ioManager, logger, fileDownloader)
: base(ioManager, logger, fileDownloader, generalConfigurationOptions)
{
this.postWriteHandler = postWriteHandler ?? throw new ArgumentNullException(nameof(postWriteHandler));
@@ -69,18 +69,13 @@ namespace Tgstation.Server.Host.Components.Engine
protected override string PathToUserFolder { get; }
/// <inheritdoc />
protected override string ByondRevisionsUrlTemplate => "https://www.byond.com/download/build/{0}/{0}.{1}_byond.zip";
protected override string OSMarkerTemplate => "Windows";
/// <summary>
/// The <see cref="IProcessExecutor"/> for the <see cref="WindowsByondInstaller"/>.
/// </summary>
readonly IProcessExecutor processExecutor;
/// <summary>
/// The <see cref="GeneralConfiguration"/> for the <see cref="WindowsByondInstaller"/>.
/// </summary>
readonly GeneralConfiguration generalConfiguration;
/// <summary>
/// The <see cref="SessionConfiguration"/> for the <see cref="WindowsByondInstaller"/>.
/// </summary>
@@ -100,7 +95,7 @@ namespace Tgstation.Server.Host.Components.Engine
/// Initializes a new instance of the <see cref="WindowsByondInstaller"/> class.
/// </summary>
/// <param name="processExecutor">The value of <see cref="processExecutor"/>.</param>
/// <param name="generalConfigurationOptions">The <see cref="IOptions{TOptions}"/> containing the value of <see cref="generalConfiguration"/>.</param>
/// <param name="generalConfigurationOptions">The <see cref="IOptionsMonitor{TOptions}"/> containing the <see cref="GeneralConfiguration"/>.</param>
/// <param name="sessionConfigurationOptions">The <see cref="IOptions{TOptions}"/> containing the value of <see cref="sessionConfiguration"/>.</param>
/// <param name="ioManager">The <see cref="IIOManager"/> for the <see cref="ByondInstallerBase"/>.</param>
/// <param name="fileDownloader">The <see cref="IFileDownloader"/> for the <see cref="ByondInstallerBase"/>.</param>
@@ -109,13 +104,12 @@ namespace Tgstation.Server.Host.Components.Engine
IProcessExecutor processExecutor,
IIOManager ioManager,
IFileDownloader fileDownloader,
IOptions<GeneralConfiguration> generalConfigurationOptions,
IOptionsMonitor<GeneralConfiguration> generalConfigurationOptions,
IOptions<SessionConfiguration> sessionConfigurationOptions,
ILogger<WindowsByondInstaller> 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)
@@ -18,7 +18,7 @@ namespace Tgstation.Server.Host.Components.Engine
readonly IIOManager ioManager;
/// <summary>
/// The <see cref="MemoryStream"/> containing the zip data of the engine.
/// The <see cref="Stream"/> containing the zip data of the engine.
/// </summary>
readonly Stream zipStream;
@@ -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)
{
@@ -164,7 +164,7 @@ namespace Tgstation.Server.Host.Components
/// </summary>
/// <param name="instanceIOManager">The instance's <see cref="IIOManager"/>.</param>
/// <returns>The <see cref="IIOManager"/> for the instance's "Game" directory.</returns>
static ResolvingIOManager CreateGameIOManager(IIOManager instanceIOManager) => new(instanceIOManager, "Game");
static IIOManager CreateGameIOManager(IIOManager instanceIOManager) => instanceIOManager.CreateResolverForSubdirectory("Game");
#pragma warning disable CA1502 // TODO: Decomplexify
/// <summary>
@@ -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<string, string>
@@ -441,6 +441,6 @@ namespace Tgstation.Server.Host.Components
/// </summary>
/// <param name="metadata">The <see cref="Models.Instance"/>.</param>
/// <returns>The <see cref="IIOManager"/> for the <paramref name="metadata"/>.</returns>
ResolvingIOManager CreateInstanceIOManager(Models.Instance metadata) => new(ioManager, metadata.Path!);
IIOManager CreateInstanceIOManager(Models.Instance metadata) => ioManager.CreateResolverForSubdirectory(metadata.Path!);
}
}
@@ -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())
{
@@ -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));
/// <summary>
/// Recusively update all <see cref="Submodule"/>s in the <see cref="libGitRepo"/>.
@@ -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;
@@ -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
/// </summary>
public const ushort DefaultApiPort = 5000;
/// <summary>
/// Default vale for <see cref="ByondZipDownloadTemplate"/>.
/// </summary>
public const string DefaultByondZipDownloadTemplate = "https://www.byond.com/download/build/${Major}/${Major}.${Minor}_byond${Linux:_linux}.zip";
/// <summary>
/// The default value for <see cref="ServerInformationBase.MinimumPasswordLength"/>.
/// </summary>
@@ -148,6 +153,17 @@ namespace Tgstation.Server.Host.Configuration
[YamlMember(SerializeAs = typeof(string))]
public Uri OpenDreamGitUrl { get; set; } = new Uri(DefaultOpenDreamGitUrl);
/// <summary>
/// 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.
/// </summary>
public string ByondZipDownloadTemplate { get; set; } = DefaultByondZipDownloadTemplate;
/// <summary>
/// The prefix to the OpenDream semver as tags appear in the git repository.
/// </summary>
@@ -178,7 +194,8 @@ namespace Tgstation.Server.Host.Configuration
/// Validates the current <see cref="ConfigVersion"/>'s compatibility and provides migration instructions.
/// </summary>
/// <param name="logger">The <see cref="ILogger"/> to use.</param>
public void CheckCompatibility(ILogger logger)
/// <param name="ioManager">The <see cref="IIOManager"/> to use.</param>
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!");
}
}
+10 -8
View File
@@ -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;
@@ -105,10 +106,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);
}
/// <summary>
@@ -154,11 +157,13 @@ namespace Tgstation.Server.Host.Core
/// <param name="assemblyInformationProvider">The <see cref="IAssemblyInformationProvider"/> needed for configuration.</param>
/// <param name="ioManager">The <see cref="IIOManager"/> needed for configuration.</param>
/// <param name="postSetupServices">The <see cref="IPostSetupServices"/> needed for configuration.</param>
/// <param name="fileSystem">The <see cref="IFileSystem"/> needed for configuration.</param>
public void ConfigureServices(
IServiceCollection services,
IAssemblyInformationProvider assemblyInformationProvider,
IIOManager ioManager,
IPostSetupServices postSetupServices)
IPostSetupServices postSetupServices,
IFileSystem fileSystem)
{
ConfigureServices(services, assemblyInformationProvider, ioManager);
@@ -485,8 +490,7 @@ namespace Tgstation.Server.Host.Core
services => services
.GetRequiredService<IRepositoryManagerFactory>()
.CreateRepositoryManager(
new ResolvingIOManager(
services.GetRequiredService<IIOManager>(),
services.GetRequiredService<IIOManager>().CreateResolverForSubdirectory(
openDreamRepositoryDirectory),
new NoopEventConsumer()));
@@ -542,6 +546,7 @@ namespace Tgstation.Server.Host.Core
services.AddSingleton<ISynchronousIOManager, SynchronousIOManager>();
services.AddSingleton<IServerPortProvider, ServerPortProivder>();
services.AddSingleton<ITopicClientFactory, TopicClientFactory>();
services.AddSingleton(fileSystem);
services.AddHostedService<CommandPipeManager>();
services.AddHostedService<VersionReportingService>();
@@ -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;
@@ -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
/// <param name="builder">The <see cref="IWebHostBuilder"/> to configure.</param>
/// <param name="assemblyInformationProvider">The <see cref="IAssemblyInformationProvider"/> to use.</param>
/// <param name="ioManager">The <see cref="IIOManager"/> to use.</param>
/// <returns>The configured <paramref name="builder"/>.</returns>
/// <param name="postSetupServices">The <see cref="IPostSetupServices"/> to use.</param>
/// <param name="fileSystem">The <see cref="IFileSystem"/> to use.</param>
/// <returns>The configured <paramref name="builder"/>.</returns>
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);
@@ -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
/// </summary>
public const TaskCreationOptions BlockingTaskCreationOptions = TaskCreationOptions.None;
/// <inheritdoc />
public char DirectorySeparatorChar => fileSystem.Path.DirectorySeparatorChar;
/// <inheritdoc />
public char AltDirectorySeparatorChar => fileSystem.Path.AltDirectorySeparatorChar;
/// <summary>
/// The backing <see cref="IFileSystem"/>.
/// </summary>
readonly IFileSystem fileSystem;
/// <summary>
/// Initializes a new instance of the <see cref="DefaultIOManager"/> class.
/// </summary>
/// <param name="fileSystem">The value of <see cref="fileSystem"/>.</param>
public DefaultIOManager(IFileSystem fileSystem)
{
this.fileSystem = fileSystem ?? throw new ArgumentNullException(nameof(fileSystem));
}
/// <summary>
/// Recursively empty a directory.
/// </summary>
/// <param name="dir"><see cref="DirectoryInfo"/> of the directory to empty.</param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> for the operation.</param>
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
}
/// <inheritdoc />
public string ConcatPath(params string[] paths) => Path.Combine(paths);
public string ConcatPath(params string[] paths) => fileSystem.Path.Combine(paths);
/// <inheritdoc />
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
}
/// <inheritdoc />
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);
/// <inheritdoc />
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);
}
/// <inheritdoc />
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);
/// <inheritdoc />
public Task<bool> FileExists(string path, CancellationToken cancellationToken) => Task.Factory.StartNew(() => File.Exists(ResolvePath(path)), cancellationToken, BlockingTaskCreationOptions, TaskScheduler.Current);
public Task<bool> FileExists(string path, CancellationToken cancellationToken) => Task.Factory.StartNew(() => fileSystem.File.Exists(ResolvePath(path)), cancellationToken, BlockingTaskCreationOptions, TaskScheduler.Current);
/// <inheritdoc />
public Task<bool> DirectoryExists(string path, CancellationToken cancellationToken) => Task.Factory.StartNew(() => Directory.Exists(ResolvePath(path)), cancellationToken, BlockingTaskCreationOptions, TaskScheduler.Current);
public Task<bool> DirectoryExists(string path, CancellationToken cancellationToken) => Task.Factory.StartNew(() => fileSystem.Directory.Exists(ResolvePath(path)), cancellationToken, BlockingTaskCreationOptions, TaskScheduler.Current);
/// <inheritdoc />
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!");
/// <inheritdoc />
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)));
/// <inheritdoc />
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)));
/// <inheritdoc />
public Task<List<string>> 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<string>();
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
/// <inheritdoc />
public async ValueTask<byte[]> 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
}
/// <inheritdoc />
public string ResolvePath() => ResolvePath(CurrentDirectory);
public string ResolvePath()
=> ResolvePath(CurrentDirectory);
/// <inheritdoc />
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)));
/// <inheritdoc />
public async ValueTask WriteAllBytes(string path, byte[] contents, CancellationToken cancellationToken)
@@ -242,10 +264,10 @@ namespace Tgstation.Server.Host.IO
}
/// <inheritdoc />
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
}
/// <inheritdoc />
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);
}
/// <inheritdoc />
@@ -274,7 +298,7 @@ namespace Tgstation.Server.Host.IO
path = ResolvePath(path);
var results = new List<string>();
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<string>();
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);
/// <inheritdoc />
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);
/// <inheritdoc />
public Task<bool> 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);
/// <inheritdoc />
public Task<IDirectoryInfo> DirectoryInfo(string path, CancellationToken cancellationToken)
=> Task.Factory.StartNew(
() => fileSystem.DirectoryInfo.New(ResolvePath(path)),
cancellationToken,
BlockingTaskCreationOptions,
TaskScheduler.Current);
/// <inheritdoc />
public bool IsPathRooted(string path)
=> fileSystem.Path.IsPathRooted(path);
/// <inheritdoc />
public IIOManager CreateResolverForSubdirectory(string subdirectoryPath)
{
ArgumentNullException.ThrowIfNull(subdirectoryPath);
if (!Path.IsPathRooted(subdirectoryPath))
subdirectoryPath = ConcatPath(
ResolvePath(),
subdirectoryPath);
return new ResolvingIOManager(
fileSystem,
subdirectoryPath);
}
/// <summary>
/// Copies a directory from <paramref name="src"/> to <paramref name="dest"/>.
/// </summary>
@@ -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)
{
+36 -10
View File
@@ -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
/// </summary>
public interface IIOManager
{
/// <summary>
/// Gets the primary directory separator character.
/// </summary>
char DirectorySeparatorChar { get; }
/// <summary>
/// Gets the alternative directory separator character.
/// </summary>
char AltDirectorySeparatorChar { get; }
/// <summary>
/// Create a new <see cref="IIOManager"/> that resolves paths to the specified <paramref name="subdirectoryPath"/>.
/// </summary>
/// <param name="subdirectoryPath">A relative or absolute path that the new <see cref="IIOManager"/> will resolve as its current directory.</param>
/// <returns>A new <see cref="IIOManager"/>.</returns>
IIOManager CreateResolverForSubdirectory(string subdirectoryPath);
/// <summary>
/// Retrieve the full path of the current working directory.
/// </summary>
@@ -117,15 +135,17 @@ namespace Tgstation.Server.Host.IO
/// Creates an asynchronous <see cref="FileStream"/> for sequential writing.
/// </summary>
/// <param name="path">The path of the file to write, will be truncated.</param>
/// <returns>The open <see cref="FileStream"/>.</returns>
FileStream CreateAsyncSequentialWriteStream(string path);
/// <returns>The open <see cref="Stream"/>.</returns>
Stream CreateAsyncSequentialWriteStream(string path);
/// <summary>
/// Creates an asynchronous <see cref="FileStream"/> for sequential reading.
/// </summary>
/// <param name="path">The path of the file to write, will be truncated.</param>
/// <returns>The open <see cref="FileStream"/>.</returns>
FileStream CreateAsyncSequentialReadStream(string path);
/// <param name="sequential">If the sequential read flag should be added.</param>
/// <param name="shareWrite">If <see cref="FileShare.Write"/> should be used.</param>
/// <returns>The open <see cref="Stream"/>.</returns>
Stream CreateAsyncReadStream(string path, bool sequential, bool shareWrite);
/// <summary>
/// Writes some <paramref name="contents"/> to a file at <paramref name="path"/> overwriting previous content.
@@ -230,12 +250,18 @@ namespace Tgstation.Server.Host.IO
Task<DateTimeOffset> GetLastModified(string path, CancellationToken cancellationToken);
/// <summary>
/// Gets the <see cref="Stream"/> for a given file <paramref name="path"/>.
/// Gets a <see cref="IDirectoryInfo"/> for the given <paramref name="path"/>.
/// </summary>
/// <param name="path">The path of the file.</param>
/// <param name="shareWrite">If <see cref="FileShare.Write"/> should be used.</param>
/// <returns>The <see cref="FileStream"/> of the file.</returns>
/// <remarks>This function is sychronous.</remarks>
FileStream GetFileStream(string path, bool shareWrite);
/// <param name="path">The path to get <see cref="IDirectoryInfo"/> for.</param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> for the operation.</param>
/// <returns>A <see cref="Task{TResult}"/> resulting in the <see cref="IDirectoryInfo"/> of the <paramref name="path"/>.</returns>
Task<IDirectoryInfo> DirectoryInfo(string path, CancellationToken cancellationToken);
/// <summary>
/// Check if a given <paramref name="path"/> is at the root level of the filesystem.
/// </summary>
/// <param name="path">The path to check.</param>
/// <returns><see langword="true"/> if the path is rooted, <see langword="false"/> otherwise.</returns>
bool IsPathRooted(string path);
}
}
@@ -63,5 +63,12 @@ namespace Tgstation.Server.Host.IO
/// <param name="path">The path to check.</param>
/// <returns><see langword="true"/> if <paramref name="path"/> is a directory, <see langword="false"/> otherwise.</returns>
bool IsDirectory(string path);
/// <summary>
/// Gets the <see cref="Stream"/> for a given file <paramref name="path"/> without write share.
/// </summary>
/// <param name="path">The path of the file.</param>
/// <returns>The <see cref="Stream"/> of the file.</returns>
Stream GetFileStream(string path);
}
}
@@ -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
/// <inheritdoc />
public bool SymlinkedDirectoriesAreDeletedAsFiles => true;
/// <summary>
/// The <see cref="IFileSystem"/> to use.
/// </summary>
readonly IFileSystem fileSystem;
/// <summary>
/// Initializes a new instance of the <see cref="PosixFilesystemLinkFactory"/> class.
/// </summary>
/// <param name="fileSystem">The value of <see cref="fileSystem"/>.</param>
public PosixFilesystemLinkFactory(IFileSystem fileSystem)
{
this.fileSystem = fileSystem ?? throw new ArgumentNullException(nameof(fileSystem));
}
/// <inheritdoc />
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);
@@ -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
/// <summary>
/// Initializes a new instance of the <see cref="ResolvingIOManager"/> class.
/// </summary>
/// <param name="parent">The <see cref="IIOManager"/> that resolves to the directory to work out of.</param>
/// <param name="fileSystem">The <see cref="IFileSystem"/> for the <see cref="DefaultIOManager"/>.</param>
/// <param name="subdirectory">The value of <see cref="subdirectory"/>.</param>
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));
}
/// <inheritdoc />
public override string ResolvePath(string path)
{
if (!Path.IsPathRooted(path))
if (!IsPathRooted(path))
return base.ResolvePath(ConcatPath(subdirectory, path));
return path;
}
@@ -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
/// <inheritdoc />
sealed class SynchronousIOManager : ISynchronousIOManager
{
/// <summary>
/// The <see cref="IFileSystem"/> to use.
/// </summary>
readonly IFileSystem fileSystem;
/// <summary>
/// The <see cref="ILogger"/> for the <see cref="SynchronousIOManager"/>.
/// </summary>
@@ -21,9 +27,11 @@ namespace Tgstation.Server.Host.IO
/// <summary>
/// Initializes a new instance of the <see cref="SynchronousIOManager"/> class.
/// </summary>
/// <param name="fileSystem">The value of <see cref="fileSystem"/>.</param>
/// <param name="logger">The value of <see cref="logger"/>.</param>
public SynchronousIOManager(ILogger<SynchronousIOManager> logger)
public SynchronousIOManager(IFileSystem fileSystem, ILogger<SynchronousIOManager> 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;
}
/// <inheritdoc />
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;
}
/// <inheritdoc />
public IEnumerable<string> 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
/// <inheritdoc />
public IEnumerable<string> 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);
}
/// <inheritdoc />
public byte[] ReadFile(string path)
{
ArgumentNullException.ThrowIfNull(path);
return File.ReadAllBytes(path);
return fileSystem.File.ReadAllBytes(path);
}
/// <inheritdoc />
@@ -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;
}
/// <inheritdoc />
public Stream GetFileStream(string path)
=> fileSystem.FileStream.New(
path,
FileMode.Open,
FileAccess.Read,
FileShare.Read | FileShare.Delete,
DefaultIOManager.DefaultBufferSize,
true);
}
}
@@ -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
/// <inheritdoc />
public bool SymlinkedDirectoriesAreDeletedAsFiles => false;
/// <summary>
/// The <see cref="IFileSystem"/> to use.
/// </summary>
readonly IFileSystem fileSystem;
/// <summary>
/// Initializes a new instance of the <see cref="WindowsFilesystemLinkFactory"/> class.
/// </summary>
/// <param name="fileSystem">The value of <see cref="fileSystem"/>.</param>
public WindowsFilesystemLinkFactory(IFileSystem fileSystem)
{
this.fileSystem = fileSystem ?? throw new ArgumentNullException(nameof(fileSystem));
}
/// <inheritdoc />
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
+15 -4
View File
@@ -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
/// </summary>
public const string AppSettings = "appsettings";
/// <inheritdoc />
public IIOManager IOManager { get; }
/// <summary>
/// The <see cref="IAssemblyInformationProvider"/> for the <see cref="ServerFactory"/>.
/// </summary>
readonly IAssemblyInformationProvider assemblyInformationProvider;
/// <inheritdoc />
public IIOManager IOManager { get; }
/// <summary>
/// The <see cref="IFileSystem"/> for the <see cref="ServerFactory"/>.
/// </summary>
readonly IFileSystem fileSystem;
/// <summary>
/// Initializes a new instance of the <see cref="ServerFactory"/> class.
/// </summary>
/// <param name="assemblyInformationProvider">The value of <see cref="assemblyInformationProvider"/>.</param>
/// <param name="ioManager">The value of <see cref="IOManager"/>.</param>
internal ServerFactory(IAssemblyInformationProvider assemblyInformationProvider, IIOManager ioManager)
/// <param name="fileSystem">The value of <see cref="fileSystem"/>.</param>
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));
}
/// <inheritdoc />
@@ -160,7 +171,7 @@ namespace Tgstation.Server.Host
})
.UseIIS()
.UseIISIntegration()
.UseApplication(assemblyInformationProvider, IOManager, postSetupServices)
.UseApplication(assemblyInformationProvider, IOManager, postSetupServices, fileSystem)
.SuppressStatusMessages(true)
.UseShutdownTimeout(
TimeSpan.FromMinutes(
@@ -304,7 +304,7 @@ namespace Tgstation.Server.Host.Setup
/// <returns>A <see cref="ValueTask{TResult}"/> resulting in the SQLite database path to store in the configuration.</returns>
async ValueTask<string?> ValidateNonExistantSqliteDBName(string databaseName, CancellationToken cancellationToken)
{
var dbPathIsRooted = Path.IsPathRooted(databaseName);
var dbPathIsRooted = ioManager.IsPathRooted(databaseName);
var resolvedPath = ioManager.ResolvePath(
dbPathIsRooted
? databaseName
@@ -64,39 +64,6 @@ namespace Tgstation.Server.Host.System
this.logger = logger ?? throw new ArgumentNullException(nameof(logger));
}
/// <summary>
/// Gets potential paths to the gcore executable.
/// </summary>
/// <returns>The potential paths to the gcore executable.</returns>
static IEnumerable<string> GetPotentialGCorePaths()
{
var enviromentPath = Environment.GetEnvironmentVariable("PATH");
IEnumerable<string> enumerator;
if (enviromentPath == null)
enumerator = Enumerable.Empty<string>();
else
{
var paths = enviromentPath.Split(';');
enumerator = paths
.Select(x => x.Split(':'))
.SelectMany(x => x);
}
var exeName = "gcore";
enumerator = enumerator
.Concat(new List<string>(2)
{
"/usr/bin",
"/usr/share/bin",
"/bin",
});
enumerator = enumerator.Select(x => Path.Combine(x, exeName));
return enumerator;
}
/// <inheritdoc />
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);
}
/// <summary>
/// Gets potential paths to the gcore executable.
/// </summary>
/// <returns>The potential paths to the gcore executable.</returns>
IEnumerable<string> GetPotentialGCorePaths()
{
var enviromentPath = Environment.GetEnvironmentVariable("PATH");
IEnumerable<string> enumerator;
if (enviromentPath == null)
enumerator = Enumerable.Empty<string>();
else
{
var paths = enviromentPath.Split(';');
enumerator = paths
.Select(x => x.Split(':'))
.SelectMany(x => x);
}
var exeName = "gcore";
enumerator = enumerator
.Concat(new List<string>(2)
{
"/usr/bin",
"/usr/share/bin",
"/bin",
});
enumerator = enumerator.Select(x => ioManager.ConcatPath(x, exeName));
return enumerator;
}
}
}
@@ -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(
@@ -96,41 +96,41 @@
<!-- Usage: .env file parsing -->
<PackageReference Include="DotEnv.Core" Version="3.1.0" />
<!-- Usage: Text formatter for Elasticsearch logging plugin -->
<PackageReference Include="Elastic.CommonSchema.Serilog" Version="8.12.3" />
<PackageReference Include="Elastic.CommonSchema.Serilog" Version="8.18.2" />
<!-- Usage: GraphQL API Engine -->
<PackageReference Include="HotChocolate.AspNetCore" Version="15.1.3" />
<PackageReference Include="HotChocolate.AspNetCore" Version="15.1.8" />
<!-- Usage: GraphQL Authorization Plugin -->
<PackageReference Include="HotChocolate.AspNetCore.Authorization" Version="15.1.3" />
<PackageReference Include="HotChocolate.AspNetCore.Authorization" Version="15.1.8" />
<!-- Usage: GraphQL IDatabaseContext support -->
<PackageReference Include="HotChocolate.Data.EntityFramework" Version="15.1.3" />
<PackageReference Include="HotChocolate.Data.EntityFramework" Version="15.1.8" />
<!-- Usage: DataLoader source generation -->
<PackageReference Include="HotChocolate.Types.Analyzers" Version="15.1.3" />
<PackageReference Include="HotChocolate.Types.Analyzers" Version="15.1.8" />
<!-- Usage: GraphQL additional scalar type definitions -->
<PackageReference Include="HotChocolate.Types.Scalars" Version="15.1.3" />
<PackageReference Include="HotChocolate.Types.Scalars" Version="15.1.8" />
<!-- Usage: git interop -->
<PackageReference Include="LibGit2Sharp" Version="0.31.0" />
<!-- Usage: OpenID Connect support -->
<PackageReference Include="Microsoft.AspNetCore.Authentication.OpenIdConnect" Version="8.0.15" />
<PackageReference Include="Microsoft.AspNetCore.Authentication.OpenIdConnect" Version="8.0.18" />
<!-- Usage: Support ""legacy"" Newotonsoft.Json in HTTP pipeline. The rest of our codebase uses Newtonsoft. -->
<PackageReference Include="Microsoft.AspNetCore.Mvc.NewtonsoftJson" Version="8.0.15" />
<PackageReference Include="Microsoft.AspNetCore.Mvc.NewtonsoftJson" Version="8.0.18" />
<!-- Usage: Using target JSON serializer for API -->
<PackageReference Include="Microsoft.AspNetCore.SignalR.Protocols.NewtonsoftJson" Version="9.0.4" />
<PackageReference Include="Microsoft.AspNetCore.SignalR.Protocols.NewtonsoftJson" Version="9.0.7" />
<!-- Usage: Generating dumps of dotnet engine processes -->
<PackageReference Include="Microsoft.Diagnostics.NETCore.Client" Version="0.2.621003" />
<!-- Usage: Database ORM -->
<PackageReference Include="Microsoft.EntityFrameworkCore" Version="9.0.4" />
<PackageReference Include="Microsoft.EntityFrameworkCore" Version="9.0.7" />
<!-- Usage: Automatic migration generation using command line -->
<PackageReference Include="Microsoft.EntityFrameworkCore.Design" Version="9.0.4">
<PackageReference Include="Microsoft.EntityFrameworkCore.Design" Version="9.0.7">
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
<!-- Usage: Sqlite ORM plugin -->
<PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite" Version="9.0.4" />
<PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite" Version="9.0.7" />
<!-- Usage: MSSQL ORM plugin -->
<PackageReference Include="Microsoft.EntityFrameworkCore.SqlServer" Version="9.0.4" />
<PackageReference Include="Microsoft.EntityFrameworkCore.SqlServer" Version="9.0.7" />
<!-- Usage: Database connectivity health check -->
<PackageReference Include="Microsoft.Extensions.Diagnostics.HealthChecks.EntityFrameworkCore" Version="8.0.15" />
<PackageReference Include="Microsoft.Extensions.Diagnostics.HealthChecks.EntityFrameworkCore" Version="8.0.18" />
<!-- Usage: OIDC support prerequisite. See https://stackoverflow.com/a/78650835/3976486 -->
<PackageReference Include="Microsoft.IdentityModel.Protocols.OpenIdConnect" Version="8.8.0" />
<PackageReference Include="Microsoft.IdentityModel.Protocols.OpenIdConnect" Version="8.13.0" />
<!-- Usage: POSIX support for syscalls, signals, and symlinks -->
<PackageReference Include="Mono.Posix.NETStandard" Version="1.0.0" />
<!-- Usage: Cron string parsing -->
@@ -144,9 +144,9 @@
<!-- Usage: Publishing Prometheus metrics -->
<PackageReference Include="prometheus-net.AspNetCore.HealthChecks" Version="8.2.1" />
<!-- Usage: Discord interop -->
<PackageReference Include="Remora.Discord" Version="2025.1.0" />
<PackageReference Include="Remora.Discord" Version="2025.2.0" />
<!-- Usage: Rich logger builder -->
<PackageReference Include="Serilog.Extensions.Logging" Version="9.0.1" />
<PackageReference Include="Serilog.Extensions.Logging" Version="9.0.2" />
<!-- Usage: Async logging plugin -->
<PackageReference Include="Serilog.Sinks.Async" Version="2.1.0" />
<!-- Usage: Console logging plugin -->
@@ -154,15 +154,17 @@
<!-- Usage: Elasticsearch logging plugin -->
<PackageReference Include="Serilog.Sinks.Elasticsearch" Version="10.0.0" />
<!-- Usage: File logging plugin -->
<PackageReference Include="Serilog.Sinks.File" Version="6.0.0" />
<PackageReference Include="Serilog.Sinks.File" Version="7.0.0" />
<!-- Usage: OpenAPI spec generator -->
<PackageReference Include="Swashbuckle.AspNetCore" Version="8.1.1" />
<PackageReference Include="Swashbuckle.AspNetCore" Version="9.0.3" />
<!-- Usage: Newtonsoft.Json plugin for OpenAPI spec generator -->
<PackageReference Include="Swashbuckle.AspNetCore.Newtonsoft" Version="8.1.1" />
<PackageReference Include="Swashbuckle.AspNetCore.Newtonsoft" Version="9.0.3" />
<!-- Usage: Windows authentication plugin allowing searching for users by name -->
<PackageReference Include="System.DirectoryServices.AccountManagement" Version="9.0.4" />
<PackageReference Include="System.DirectoryServices.AccountManagement" Version="9.0.7" />
<!-- Usage: Filesystem abstraction -->
<PackageReference Include="System.IO.Abstractions" Version="22.0.15" />
<!-- Usage: Identifying owning user of Windows Process objects -->
<PackageReference Include="System.Management" Version="9.0.4" />
<PackageReference Include="System.Management" Version="9.0.7" />
</ItemGroup>
<ItemGroup>
@@ -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)
{
@@ -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.
@@ -10,7 +10,7 @@
<ItemGroup>
<!-- Usage: JWT injection into HTTP pipeline -->
<PackageReference Include="Microsoft.AspNetCore.Authentication.JwtBearer" Version="8.0.15" />
<PackageReference Include="Microsoft.AspNetCore.Authentication.JwtBearer" Version="8.0.18" />
<!-- Usage: GitHub.com interop -->
<PackageReference Include="Octokit" Version="14.0.0" />
<!-- Usage: YAML conversion of Version objects -->
@@ -6,7 +6,7 @@
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Microsoft.Bcl.AsyncInterfaces" Version="9.0.4" />
<PackageReference Include="Microsoft.Bcl.AsyncInterfaces" Version="9.0.7" />
</ItemGroup>
<ItemGroup>
@@ -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));
}
}
}
@@ -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<ArgumentNullException>(() => new PosixByondInstaller(null, null, null, null));
Assert.ThrowsException<ArgumentNullException>(() => new PosixByondInstaller(null, null, null, null, null));
var mockPostWriteHandler = new Mock<IPostWriteHandler>();
Assert.ThrowsException<ArgumentNullException>(() => new PosixByondInstaller(mockPostWriteHandler.Object, null, null, null));
Assert.ThrowsException<ArgumentNullException>(() => new PosixByondInstaller(mockPostWriteHandler.Object, null, null, null, null));
var mockIOManager = new Mock<IIOManager>();
Assert.ThrowsException<ArgumentNullException>(() => new PosixByondInstaller(mockPostWriteHandler.Object, mockIOManager.Object, null, null));
Assert.ThrowsException<ArgumentNullException>(() => new PosixByondInstaller(mockPostWriteHandler.Object, mockIOManager.Object, null, null, null));
var mockFileDownloader = Mock.Of<IFileDownloader>();
Assert.ThrowsException<ArgumentNullException>(() => new PosixByondInstaller(mockPostWriteHandler.Object, mockIOManager.Object, mockFileDownloader, null));
Assert.ThrowsException<ArgumentNullException>(() => new PosixByondInstaller(mockPostWriteHandler.Object, mockIOManager.Object, mockFileDownloader, null, null));
var mockOptions = Mock.Of<IOptionsMonitor<GeneralConfiguration>>();
Assert.ThrowsException<ArgumentNullException>(() => new PosixByondInstaller(mockPostWriteHandler.Object, mockIOManager.Object, mockFileDownloader, mockOptions, null));
var mockLogger = new Mock<ILogger<PosixByondInstaller>>();
_ = 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<IIOManager>();
var mockLogger = new Mock<ILogger<PosixByondInstaller>>();
var mockFileDownloader = Mock.Of<IFileDownloader>();
var installer = new PosixByondInstaller(mockPostWriteHandler.Object, mockIOManager.Object, mockFileDownloader, mockLogger.Object);
var mockOptions = Mock.Of<IOptionsMonitor<GeneralConfiguration>>();
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<IPostWriteHandler>();
var mockLogger = new Mock<ILogger<PosixByondInstaller>>();
var mockFileDownloader = new Mock<IFileDownloader>();
var installer = new PosixByondInstaller(mockPostWriteHandler.Object, mockIOManager.Object, mockFileDownloader.Object, mockLogger.Object);
var mockOptions = new Mock<IOptionsMonitor<GeneralConfiguration>>();
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<ArgumentNullException>(() => 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 => uri == new Uri("https://www.byond.com/download/build/511/511.1385_byond_linux.zip")),
It.Is<Uri>(uri => uri == new Uri(TestUrl)),
null))
.Returns(
new BufferedFileStreamProvider(
@@ -87,7 +99,8 @@ namespace Tgstation.Server.Host.Components.Engine.Tests
var mockPostWriteHandler = new Mock<IPostWriteHandler>();
var mockLogger = new Mock<ILogger<PosixByondInstaller>>();
var mockFileDownloader = Mock.Of<IFileDownloader>();
var installer = new PosixByondInstaller(mockPostWriteHandler.Object, mockIOManager.Object, mockFileDownloader, mockLogger.Object);
var mockOptions = Mock.Of<IOptionsMonitor<GeneralConfiguration>>();
var installer = new PosixByondInstaller(mockPostWriteHandler.Object, mockIOManager.Object, mockFileDownloader, mockOptions, mockLogger.Object);
const string FakePath = "fake";
await Assert.ThrowsExceptionAsync<ArgumentNullException>(() => installer.Install(null, null, false, default).AsTask());
@@ -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<SynchronousIOManager>()),
new SynchronousIOManager(
mockFs,
loggerFactory.CreateLogger<SynchronousIOManager>()),
Mock.Of<IFilesystemLinkFactory>(),
Mock.Of<IProcessExecutor>(),
Mock.Of<IPostWriteHandler>(),
@@ -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()
@@ -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<byte>());
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);
}
}
}
@@ -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<IProcessExecutor>(() => processExecutor),
new DefaultIOManager(),
new DefaultIOManager(
new FileSystem()),
loggerFactory.CreateLogger<PosixProcessFeatures>()),
Mock.Of<IIOManager>(),
loggerFactory.CreateLogger<ProcessExecutor>(),
@@ -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<ILogger<WindowsProcessFeatures>>())
: new PosixProcessFeatures(new Lazy<IProcessExecutor>(() => null), new DefaultIOManager(), Mock.Of<ILogger<PosixProcessFeatures>>());
: new PosixProcessFeatures(new Lazy<IProcessExecutor>(() => null), new DefaultIOManager(new MockFileSystem()), Mock.Of<ILogger<PosixProcessFeatures>>());
}
[TestMethod]
@@ -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()
@@ -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<IServer>();
var mockFs = new MockFileSystem();
var exception = new DivideByZeroException();
mockServer.Setup(x => x.Run(It.IsAny<CancellationToken>())).Throws(exception);
mockServer.SetupGet(x => x.RestartRequested).Returns(true);
var mockServerFactory = new Mock<IServerFactory>();
mockServerFactory.SetupGet(x => x.IOManager).Returns(new DefaultIOManager());
mockServerFactory.SetupGet(x => x.IOManager).Returns(new DefaultIOManager(mockFs));
mockServerFactory.Setup(x => x.CreateServer(It.IsNotNull<string[]>(), It.IsAny<string>(), It.IsAny<CancellationToken>())).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<string>(), 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);
}
}
}
@@ -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<ArgumentNullException>(() => new ServerFactory(null, null));
Assert.ThrowsException<ArgumentNullException>(() => new ServerFactory(null, null, null));
IAssemblyInformationProvider assemblyInformationProvider = Mock.Of<IAssemblyInformationProvider>();
Assert.ThrowsException<ArgumentNullException>(() => new ServerFactory(assemblyInformationProvider, null));
Assert.ThrowsException<ArgumentNullException>(() => new ServerFactory(assemblyInformationProvider, null, null));
IIOManager ioManager = Mock.Of<IIOManager>();
_ = new ServerFactory(assemblyInformationProvider, ioManager);
Assert.ThrowsException<ArgumentNullException>(() => new ServerFactory(assemblyInformationProvider, ioManager, null));
var mockFileSystem = new MockFileSystem();
_ = new ServerFactory(assemblyInformationProvider, ioManager, mockFileSystem);
}
[TestMethod]
@@ -7,7 +7,9 @@
<ItemGroup>
<!-- Usage: Creating mock database implementations -->
<PackageReference Include="Microsoft.EntityFrameworkCore.InMemory" Version="9.0.4" />
<PackageReference Include="Microsoft.EntityFrameworkCore.InMemory" Version="9.0.7" />
<!-- Usage: Creating mock filesystem implementations -->
<PackageReference Include="System.IO.Abstractions.TestingHelpers" Version="22.0.15" />
</ItemGroup>
<ItemGroup>
@@ -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<CachingFileDownloader>());
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<CachingFileDownloader>();
}
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));
@@ -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,
@@ -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<EngineVersion> GetEdgeVersion(EngineType engineType, IFileDownloader fileDownloader, CancellationToken cancellationToken)
public static async ValueTask<EngineVersion> 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<string, string>()
{
{ "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<IOptions<GeneralConfiguration>>();
generalConfigOptionsMock.SetupGet(x => x.Value).Returns(new GeneralConfiguration());
var generalConfigOptionsMock = new Mock<IOptionsMonitor<GeneralConfiguration>>();
generalConfigOptionsMock.SetupGet(x => x.CurrentValue).Returns(new GeneralConfiguration
{
ByondZipDownloadTemplate = TestingUtils.ByondZipDownloadTemplate,
});
var sessionConfigOptionsMock = new Mock<IOptions<SessionConfiguration>>();
sessionConfigOptionsMock.SetupGet(x => x.Value).Returns(new SessionConfiguration());
@@ -303,6 +287,7 @@ namespace Tgstation.Server.Tests.Live.Instance
Mock.Of<IPostWriteHandler>(),
Mock.Of<IIOManager>(),
fileDownloader,
generalConfigOptionsMock.Object,
Mock.Of<ILogger<PosixByondInstaller>>());
using var windowsByondInstaller = byondInstaller as WindowsByondInstaller;
@@ -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<IOptions<GeneralConfiguration>>();
var mockOptionsMonitor = new Mock<IOptionsMonitor<GeneralConfiguration>>();
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<ILogger<OpenDreamInstaller>>(),
new PlatformIdentifier(),
Mock.Of<IProcessExecutor>(),
@@ -124,20 +127,21 @@ namespace Tgstation.Server.Tests.Live.Instance
genConfig),
Mock.Of<IAsyncDelayer>(),
Mock.Of<IAbstractHttpClientFactory>(),
mockOptions.Object,
Options.Create(genConfig),
Options.Create(new SessionConfiguration()))
: new PlatformIdentifier().IsWindows
? new WindowsByondInstaller(
Mock.Of<IProcessExecutor>(),
Mock.Of<IIOManager>(),
fileDownloader,
Options.Create(genConfig),
mockOptionsMonitor.Object,
Options.Create(new SessionConfiguration()),
Mock.Of<ILogger<WindowsByondInstaller>>())
: new PosixByondInstaller(
Mock.Of<IPostWriteHandler>(),
Mock.Of<IIOManager>(),
fileDownloader,
mockOptionsMonitor.Object,
Mock.Of<ILogger<PosixByondInstaller>>());
using var windowsByondInstaller = byondInstaller as WindowsByondInstaller;
@@ -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;
@@ -809,7 +810,7 @@ namespace Tgstation.Server.Tests.Live.Instance
var features = new PosixProcessFeatures(
new Lazy<IProcessExecutor>(Mock.Of<IProcessExecutor>()),
new DefaultIOManager(),
new DefaultIOManager(new FileSystem()),
Mock.Of<ILogger<PosixProcessFeatures>>());
features.SuspendProcess(proc);
@@ -876,7 +877,7 @@ namespace Tgstation.Server.Tests.Live.Instance
executor = new ProcessExecutor(
RuntimeInformation.IsOSPlatform(OSPlatform.Windows)
? new WindowsProcessFeatures(Mock.Of<ILogger<WindowsProcessFeatures>>())
: new PosixProcessFeatures(new Lazy<IProcessExecutor>(() => executor), new DefaultIOManager(), Mock.Of<ILogger<PosixProcessFeatures>>()),
: new PosixProcessFeatures(new Lazy<IProcessExecutor>(() => executor), new DefaultIOManager(new FileSystem()), Mock.Of<ILogger<PosixProcessFeatures>>()),
Mock.Of<IIOManager>(),
Mock.Of<ILogger<ProcessExecutor>>(),
LoggerFactory.Create(x => { }));
@@ -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)
@@ -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))
@@ -1135,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
@@ -1421,6 +1422,7 @@ namespace Tgstation.Server.Tests.Live
await Task.Yield();
InstanceManager GetInstanceManager() => ((Host.Server)server.RealServer).Host.Services.GetRequiredService<InstanceManager>();
ILogger GetLogger() => ((Host.Server)server.RealServer).Host.Services.GetRequiredService<ILogger<TestLiveServer>>();
// main run
var serverTask = server.Run(cancellationToken).AsTask();
@@ -1590,7 +1592,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<JobException>(
() => InstanceTest.DownloadEngineVersion(
@@ -1625,6 +1627,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(
@@ -1632,8 +1644,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),
@@ -1649,6 +1661,7 @@ namespace Tgstation.Server.Tests.Live
await FailFast(
instanceTest
.RunTests(
GetLogger(),
instanceClient,
mainDMPort.Value,
mainDDPort.Value,
@@ -1881,7 +1894,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;
@@ -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<IEventConsumer>(),
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);
}
@@ -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<IProcessFeatures>(),
new DefaultIOManager(),
new DefaultIOManager(new FileSystem()),
Mock.Of<ILogger<ProcessExecutor>>(),
loggerFactory);
@@ -52,7 +53,7 @@ namespace Tgstation.Server.Tests
var platformIdentifier = new PlatformIdentifier();
var processExecutor = new ProcessExecutor(
Mock.Of<IProcessFeatures>(),
new DefaultIOManager(),
new DefaultIOManager(new FileSystem()),
loggerFactory.CreateLogger<ProcessExecutor>(),
loggerFactory);
+15 -7
View File
@@ -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<IOptions<GeneralConfiguration>>();
mockGeneralConfigurationOptions.SetupGet(x => x.Value).Returns(new GeneralConfiguration());
var mockGeneralConfigurationOptions = new Mock<IOptionsMonitor<GeneralConfiguration>>();
mockGeneralConfigurationOptions.SetupGet(x => x.CurrentValue).Returns(new GeneralConfiguration
{
ByondZipDownloadTemplate = TestingUtils.ByondZipDownloadTemplate,
});
var mockSessionConfigurationOptions = new Mock<IOptions<SessionConfiguration>>();
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<IOptions<GeneralConfiguration>>();
mockGeneralConfigurationOptions.SetupGet(x => x.Value).Returns(new GeneralConfiguration
var mockGeneralConfigurationOptions = new Mock<IOptionsMonitor<GeneralConfiguration>>();
mockGeneralConfigurationOptions.SetupGet(x => x.CurrentValue).Returns(new GeneralConfiguration
{
SkipAddingByondFirewallException = true,
ByondZipDownloadTemplate = TestingUtils.ByondZipDownloadTemplate,
});
var mockSessionConfigurationOptions = new Mock<IOptions<SessionConfiguration>>();
mockSessionConfigurationOptions.SetupGet(x => x.Value).Returns(new SessionConfiguration());
@@ -207,8 +214,9 @@ namespace Tgstation.Server.Tests
loggerFactory.CreateLogger<WindowsByondInstaller>())
: new PosixByondInstaller(
new PosixPostWriteHandler(loggerFactory.CreateLogger<PosixPostWriteHandler>()),
new DefaultIOManager(),
new DefaultIOManager(new FileSystem()),
fileDownloader,
mockGeneralConfigurationOptions.Object,
loggerFactory.CreateLogger<PosixByondInstaller>());
using var disposable = byondInstaller as IDisposable;
@@ -217,13 +225,13 @@ namespace Tgstation.Server.Tests
? new WindowsProcessFeatures(Mock.Of<ILogger<WindowsProcessFeatures>>())
: new PosixProcessFeatures(
new Lazy<IProcessExecutor>(() => null),
new DefaultIOManager(),
new DefaultIOManager(new FileSystem()),
loggerFactory.CreateLogger<PosixProcessFeatures>()),
Mock.Of<IIOManager>(),
loggerFactory.CreateLogger<ProcessExecutor>(),
loggerFactory);
var ioManager = new DefaultIOManager();
var ioManager = new DefaultIOManager(new FileSystem());
var tempPath = ioManager.ConcatPath(LiveTestingServer.BaseDirectory, "mapthreads");
await ioManager.CreateDirectory(tempPath, default);
try
+92 -1
View File
@@ -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<string> GetByondEdgeVersion(ILogger logger, IFileDownloader fileDownloader, CancellationToken cancellationToken)
{
if (edgeVersion != null)
return edgeVersion;
async ValueTask<string> 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<string, string>()
{
{ "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;
}
}
}
@@ -5,10 +5,6 @@
<TargetFramework>$(TgsFrameworkVersion)</TargetFramework>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Microsoft.Bcl.AsyncInterfaces" Version="9.0.4" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\..\src\Tgstation.Server.Client.GraphQL\Tgstation.Server.Client.GraphQL.csproj" />
<ProjectReference Include="..\..\src\Tgstation.Server.Host.Watchdog\Tgstation.Server.Host.Watchdog.csproj" />