Merge branch 'dev' into TheGreatAuthenticationRework

This commit is contained in:
Jordan Dominion
2025-07-27 13:33:24 -04:00
77 changed files with 1026 additions and 463 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
+1 -1
View File
@@ -639,7 +639,7 @@ Bots have a set of built-in commands that can be triggered via `!tgs`, mentionin
All files in game code deployments are considered transient by default, meaning when new code is deployed, changes will be lost. Static files allow you to specify which files and folders stick around throughout all deployments.
The `StaticFiles` folder contains 3 root folders which cannot be deleted and operate under special rules
The `Configuration` folder contains 3 root folders which cannot be deleted and operate under special rules
- `CodeModifications`
- `EventScripts`
- `GameStaticFiles`
+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.9.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;
@@ -106,10 +107,12 @@ namespace Tgstation.Server.Host.Core
public static IServerFactory CreateDefaultServerFactory()
{
var assemblyInformationProvider = new AssemblyInformationProvider();
var ioManager = new DefaultIOManager();
var fileSystem = new FileSystem();
var ioManager = new DefaultIOManager(fileSystem);
return new ServerFactory(
assemblyInformationProvider,
ioManager);
ioManager,
fileSystem);
}
/// <summary>
@@ -155,11 +158,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);
@@ -470,8 +475,7 @@ namespace Tgstation.Server.Host.Core
services => services
.GetRequiredService<IRepositoryManagerFactory>()
.CreateRepositoryManager(
new ResolvingIOManager(
services.GetRequiredService<IIOManager>(),
services.GetRequiredService<IIOManager>().CreateResolverForSubdirectory(
openDreamRepositoryDirectory),
new NoopEventConsumer()));
@@ -527,6 +531,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.9.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;
@@ -815,7 +816,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);
@@ -882,7 +883,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))
@@ -922,6 +923,12 @@ namespace Tgstation.Server.Tests.Live
await using var node1Client = await CreateAdminClient(node1.ApiUrl, cancellationToken);
await using var node2Client = await CreateAdminClient(node2.ApiUrl, cancellationToken);
// test a token signed from any one node will work on another
var token = node2Client.RestClient.Token;
var testNode1Client = restClientFactory.CreateFromToken(node1.ApiUrl, token);
await testNode1Client.ServerInformation(cancellationToken);
var controllerInfo = await controllerClient.RestClient.ServerInformation(cancellationToken);
async Task WaitForSwarmServerUpdate(IRestServerClient client, int currentServerCount)
@@ -1129,7 +1136,7 @@ namespace Tgstation.Server.Tests.Live
ApiValidationSecurityLevel = DreamDaemonSecurity.Trusted,
}, cancellationToken);
var ioManager = new Host.IO.DefaultIOManager();
var ioManager = new Host.IO.DefaultIOManager(new FileSystem());
var repoPath = ioManager.ConcatPath(instance.Path, "Repository");
await using var jobsTest = new JobsRequiredTest(instanceClient.Jobs);
var postWriteHandler = (Host.IO.IPostWriteHandler)(new PlatformIdentifier().IsWindows
@@ -1415,6 +1422,7 @@ namespace Tgstation.Server.Tests.Live
await Task.Yield();
InstanceManager GetInstanceManager() => ((Host.Server)server.RealServer).Host.Services.GetRequiredService<InstanceManager>();
ILogger GetLogger() => ((Host.Server)server.RealServer).Host.Services.GetRequiredService<ILogger<TestLiveServer>>();
// main run
var serverTask = server.Run(cancellationToken).AsTask();
@@ -1587,7 +1595,7 @@ namespace Tgstation.Server.Tests.Live
var testSerialized = TestingUtils.RunningInGitHubActions; // they only have 2 cores, can't handle intense parallelization
async Task ODCompatTests()
{
var edgeODVersionTask = EngineTest.GetEdgeVersion(EngineType.OpenDream, fileDownloader, cancellationToken);
var edgeODVersionTask = EngineTest.GetEdgeVersion(EngineType.OpenDream, GetLogger(), fileDownloader, cancellationToken);
var ex = await Assert.ThrowsExceptionAsync<JobException>(
() => InstanceTest.DownloadEngineVersion(
@@ -1622,6 +1630,16 @@ namespace Tgstation.Server.Tests.Live
if (openDreamOnly)
return;
var windowsMinCompat = new Version(510, 1346);
var linuxMinCompat = new Version(512, 1451); // http://www.byond.com/forum/?forum=5&command=search&scope=local&text=resolved%3a512.1451
await CachingFileDownloader.InitializeByondVersion(
GetLogger(),
new PlatformIdentifier().IsWindows
? windowsMinCompat
: linuxMinCompat,
new PlatformIdentifier().IsWindows,
cancellationToken);
var compatTests = FailFast(
instanceTest
.RunCompatTests(
@@ -1629,8 +1647,8 @@ namespace Tgstation.Server.Tests.Live
{
Engine = EngineType.Byond,
Version = new PlatformIdentifier().IsWindows
? new Version(510, 1346)
: new Version(512, 1451) // http://www.byond.com/forum/?forum=5&command=search&scope=local&text=resolved%3a512.1451
? windowsMinCompat
: linuxMinCompat,
},
server.OpenDreamUrl,
firstAdminRestClient.Instances.CreateClient(compatInstance),
@@ -1646,6 +1664,7 @@ namespace Tgstation.Server.Tests.Live
await FailFast(
instanceTest
.RunTests(
GetLogger(),
instanceClient,
mainDMPort.Value,
mainDDPort.Value,
@@ -1878,7 +1897,7 @@ namespace Tgstation.Server.Tests.Live
preStartupTime = DateTimeOffset.UtcNow;
serverTask = server.Run(cancellationToken).AsTask();
long expectedCompileJobId, expectedStaged;
var edgeVersion = await EngineTest.GetEdgeVersion(EngineType.Byond, fileDownloader, cancellationToken);
var edgeVersion = await EngineTest.GetEdgeVersion(EngineType.Byond, GetLogger(), fileDownloader, cancellationToken);
await using (var adminClient = await CreateAdminClient(server.ApiUrl, cancellationToken))
{
var restAdminClient = adminClient.RestClient;
@@ -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" />