mirror of
https://github.com/tgstation/tgstation-server.git
synced 2026-08-28 07:27:18 +01:00
@@ -36,6 +36,8 @@ You can of course, as always, ask for help at [#coderbus](irc://irc.rizon.net/co
|
||||
|
||||
You need the .NET 8.0 SDK, node>=v20, and npm>=v5.7 (in your PATH) to compile the server. On Linux, you also need the `libgdiplus` package installed to generate icons.
|
||||
|
||||
You need to run `corepack enable` to configure node to correctly build the webpanel.
|
||||
|
||||
The recommended IDE is Visual Studio 2022 or VSCode.
|
||||
|
||||
In order to build the service version and/or the Windows installer you need a to run on Windows.
|
||||
|
||||
+331
-278
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,68 @@
|
||||
name: 'CI Security'
|
||||
|
||||
on:
|
||||
pull_request:
|
||||
branches:
|
||||
- dev
|
||||
- master
|
||||
pull_request_target:
|
||||
types: [ opened, reopened, labeled, synchronize ]
|
||||
branches:
|
||||
- dev
|
||||
- master
|
||||
|
||||
concurrency:
|
||||
group: "ci-security-${{ github.head_ref || github.run_id }}-${{ github.event_name }}"
|
||||
cancel-in-progress: true
|
||||
|
||||
jobs:
|
||||
security-checkpoint:
|
||||
name: Check CI Clearance
|
||||
if: github.event_name == 'pull_request_target' && (github.event.pull_request.head.repo.id != github.event.pull_request.base.repo.id || github.event.pull_request.user.id == 49699333) && github.event.pull_request.state == 'open'
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
pull-requests: write
|
||||
steps:
|
||||
- name: Comment on new Fork PR
|
||||
if: github.event.action == 'opened' && !contains(github.event.pull_request.labels.*.name, 'CI Cleared') && github.event.pull_request.user.id != 49699333
|
||||
uses: thollander/actions-comment-pull-request@1d3973dc4b8e1399c0620d3f2b1aa5e795465308
|
||||
with:
|
||||
message: Thank you for contributing to ${{ github.event.pull_request.base.repo.name }}! The workflow '${{ github.workflow }}' requires repository secrets and will not run without approval. Maintainers can add the `CI Cleared` label to allow it to run. Note that any changes to ci-security.yml will not be reflected in the run and the ci-pipeline.yml at the HEAD of the pull request will be used.
|
||||
|
||||
- name: Comment on dependabot PR
|
||||
if: github.event.action == 'opened' && !contains(github.event.pull_request.labels.*.name, 'CI Cleared') && github.event.pull_request.user.id == 49699333
|
||||
uses: thollander/actions-comment-pull-request@1d3973dc4b8e1399c0620d3f2b1aa5e795465308
|
||||
with:
|
||||
message: Set the milestone to the next minor version, check for supply chain attacks, and then add the `CI Cleared` label to allow CI to run.
|
||||
|
||||
- name: "Remove Stale 'CI Cleared' Label"
|
||||
if: github.event.action == 'synchronize' || github.event.action == 'reopened'
|
||||
uses: actions-ecosystem/action-remove-labels@2ce5d41b4b6aa8503e285553f75ed56e0a40bae0
|
||||
with:
|
||||
labels: CI Cleared
|
||||
|
||||
- name: "Remove 'CI Approval Required' Label"
|
||||
if: (github.event.action == 'synchronize' || github.event.action == 'reopened') || ((github.event.action == 'opened' || github.event.action == 'labeled') && contains(github.event.pull_request.labels.*.name, 'CI Cleared'))
|
||||
uses: actions-ecosystem/action-remove-labels@2ce5d41b4b6aa8503e285553f75ed56e0a40bae0
|
||||
with:
|
||||
labels: CI Approval Required
|
||||
|
||||
- name: "Add 'CI Approval Required' Label"
|
||||
if: (github.event.action == 'synchronize' || github.event.action == 'reopened') || ((github.event.action == 'opened' || github.event.action == 'labeled') && !contains(github.event.pull_request.labels.*.name, 'CI Cleared'))
|
||||
uses: actions-ecosystem/action-add-labels@bd52874380e3909a1ac983768df6976535ece7f8
|
||||
with:
|
||||
labels: CI Approval Required
|
||||
github_token: ${{ github.token }}
|
||||
|
||||
- name: Fail if PR has Unlabeled new Commits from User
|
||||
if: (github.event.action == 'synchronize' || github.event.action == 'reopened') || ((github.event.action == 'opened' || github.event.action == 'labeled') && !contains(github.event.pull_request.labels.*.name, 'CI Cleared'))
|
||||
run: exit 1
|
||||
|
||||
ci-pipline-workflow-call:
|
||||
name: CI Pipeline
|
||||
needs: security-checkpoint
|
||||
if: (!(cancelled() || failure()) && (needs.security-checkpoint.result == 'success' || (github.event_name != 'pull_request_target' && github.event.pull_request.head.repo.id == github.event.pull_request.base.repo.id && github.event.pull_request.user.id != 49699333)))
|
||||
uses: ./.github/workflows/ci-pipeline.yml
|
||||
secrets: inherit
|
||||
with:
|
||||
pull_request_number: ${{ github.event.pull_request.number }}
|
||||
@@ -1,7 +1,7 @@
|
||||
name: Rerun Flaky Live Tests
|
||||
on:
|
||||
workflow_run:
|
||||
workflows: [CI Pipeline]
|
||||
workflows: ['CI Pipeline', 'CI Security']
|
||||
types:
|
||||
- completed
|
||||
jobs:
|
||||
|
||||
@@ -3,7 +3,8 @@
|
||||
const CONSIDERED_JOBS = [
|
||||
"Windows Live Tests",
|
||||
"Linux Live Tests",
|
||||
"Build .deb Package"
|
||||
"Build .deb Package",
|
||||
"Upload Code Coverage"
|
||||
];
|
||||
|
||||
async function getFailedJobsForRun(github, context, workflowRunId, runAttempt) {
|
||||
@@ -31,17 +32,19 @@ export async function rerunFlakyTests({ github, context }) {
|
||||
context.payload.workflow_run.run_attempt
|
||||
);
|
||||
|
||||
if (failingJobs.length > 1) {
|
||||
console.log("Multiple jobs failing. PROBABLY not flaky, not rerunning.");
|
||||
if (failingJobs.length > 3) {
|
||||
console.log("Many jobs failing. PROBABLY not flaky, not rerunning.");
|
||||
return;
|
||||
}
|
||||
|
||||
const filteredFailingJobs = failingJobs.filter((job) => {
|
||||
console.log(`Failing job: ${job.name}`)
|
||||
return CONSIDERED_JOBS.some((title) => job.name.startsWith(title));
|
||||
return CONSIDERED_JOBS
|
||||
.flatMap(jobName => [jobName, 'CI Pipeline / ' + jobName])
|
||||
.some((title) => job.name.startsWith(title));
|
||||
});
|
||||
if (filteredFailingJobs.length === 0) {
|
||||
console.log("Failing jobs are NOT designated flaky. Not rerunning.");
|
||||
if (filteredFailingJobs.length !== failingJobs.length) {
|
||||
console.log("One or more failing jobs are NOT designated flaky. Not rerunning.");
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,16 @@
|
||||
name: Size Labelling
|
||||
on:
|
||||
pull_request_target:
|
||||
|
||||
jobs:
|
||||
size-label:
|
||||
name: Add Size Label
|
||||
permissions:
|
||||
contents: read
|
||||
pull-requests: write
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: size-label
|
||||
uses: "pascalgn/size-label-action@bbbaa0d5ccce8e2e76254560df5c64b82dac2e12" # v0.5.2, consider upgrading after https://github.com/pascalgn/size-label-action/pull/54 is merged
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
@@ -8,7 +8,7 @@ on:
|
||||
|
||||
env:
|
||||
TGS_DOTNET_VERSION: 8
|
||||
OD_MIN_COMPAT_DOTNET_VERSION: 7
|
||||
TGS_DOTNET_QUALITY: ga
|
||||
|
||||
jobs:
|
||||
master-merge:
|
||||
@@ -26,11 +26,6 @@ jobs:
|
||||
with:
|
||||
path: temp_workspace
|
||||
|
||||
- name: Restore
|
||||
run: |
|
||||
cd temp_workspace
|
||||
dotnet restore
|
||||
|
||||
- name: Build ReleaseNotes
|
||||
run: |
|
||||
cd temp_workspace
|
||||
|
||||
@@ -0,0 +1,72 @@
|
||||
name: 'Sync spacestation13/tgstation-server'
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- dev
|
||||
tags:
|
||||
- '*'
|
||||
workflow_dispatch:
|
||||
|
||||
env:
|
||||
TGS_DOTNET_VERSION: 8
|
||||
TGS_DOTNET_QUALITY: ga
|
||||
|
||||
concurrency:
|
||||
group: "ss13-mirror-sync"
|
||||
cancel-in-progress: true
|
||||
|
||||
jobs:
|
||||
fork-sync:
|
||||
name: Fork Sync
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Setup dotnet
|
||||
uses: actions/setup-dotnet@v4
|
||||
with:
|
||||
dotnet-version: '${{ env.TGS_DOTNET_VERSION }}.0.x'
|
||||
dotnet-quality: ${{ env.TGS_DOTNET_QUALITY }}
|
||||
|
||||
- name: Build Checkout
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
path: temp_workspace
|
||||
|
||||
- name: Build ReleaseNotes
|
||||
run: |
|
||||
cd temp_workspace
|
||||
dotnet build -c Release -p:TGS_HOST_NO_WEBPANEL=true tools/Tgstation.Server.ReleaseNotes/Tgstation.Server.ReleaseNotes.csproj
|
||||
|
||||
- name: Generate App Token
|
||||
run: |
|
||||
cd temp_workspace
|
||||
dotnet run -c Release --no-build --project tools/Tgstation.Server.ReleaseNotes --token-output-file ${{ runner.temp }}/installation_secret.txt ${{ secrets.TGS_CI_GITHUB_APP_TOKEN_BASE64 }} --spacestation13
|
||||
echo "INSTALLATION_TOKEN=$(cat ${{ runner.temp }}/installation_secret.txt)" >> $GITHUB_ENV
|
||||
rm ${{ runner.temp }}/installation_secret.txt
|
||||
env:
|
||||
TGS_RELEASE_NOTES_TOKEN: ${{ secrets.DEV_PUSH_TOKEN }}
|
||||
|
||||
- name: Main Checkout
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 0
|
||||
fetch-tags: true
|
||||
token: ${{ env.INSTALLATION_TOKEN }}
|
||||
|
||||
- name: Build ReleaseNotes
|
||||
run: dotnet build -c Release -p:TGS_HOST_NO_WEBPANEL=true tools/Tgstation.Server.ReleaseNotes/Tgstation.Server.ReleaseNotes.csproj
|
||||
|
||||
- name: Generate App Token
|
||||
run: |
|
||||
dotnet run -c Release --no-build --project tools/Tgstation.Server.ReleaseNotes --token-output-file ${{ runner.temp }}/installation_secret.txt ${{ secrets.TGS_CI_GITHUB_APP_TOKEN_BASE64 }}
|
||||
echo "INSTALLATION_TOKEN=$(cat ${{ runner.temp }}/installation_secret.txt)" >> $GITHUB_ENV
|
||||
rm ${{ runner.temp }}/installation_secret.txt
|
||||
env:
|
||||
TGS_RELEASE_NOTES_TOKEN: ${{ secrets.DEV_PUSH_TOKEN }}
|
||||
|
||||
- name: Push to Spacestation13 Fork
|
||||
run: |
|
||||
git config user.name "tgstation-server-ci[bot]"
|
||||
git config user.email "161980869+tgstation-server-ci[bot]@users.noreply.github.com"
|
||||
git push "https://tgstation-server-ci:${{ env.INSTALLATION_TOKEN }}@github.com/spacestation13/tgstation-server"
|
||||
git push --tags "https://tgstation-server-ci:${{ env.INSTALLATION_TOKEN }}@github.com/spacestation13/tgstation-server"
|
||||
@@ -4,7 +4,7 @@
|
||||
|
||||
# tgstation-server
|
||||
|
||||
 [](https://codecov.io/gh/tgstation/tgstation-server)
|
||||
[](https://github.com/tgstation/tgstation-server/actions/workflows/ci-pipeline.yml) [](https://codecov.io/gh/tgstation/tgstation-server)
|
||||
|
||||
[](LICENSE) [](http://isitmaintained.com/project/tgstation/tgstation-server "Average time to resolve an issue") [](https://www.nuget.org/packages/Tgstation.Server.Api) [](https://www.nuget.org/packages/Tgstation.Server.Client)
|
||||
|
||||
@@ -239,7 +239,7 @@ Create an `appsettings.Production.yml` file next to `appsettings.yml`. This will
|
||||
|
||||
- `General:InstanceLimit`: Maximum number of instances that may be created
|
||||
|
||||
- `General:GitHubAccessToken`: Specify a GitHub personal access token with no scopes here to highly mitigate the possiblity of 429 response codes from GitHub requests
|
||||
- `General:GitHubAccessToken`: Specify a classic GitHub personal access token with no scopes here to highly mitigate the possiblity of 429 response codes from GitHub requests
|
||||
|
||||
- `General:SkipAddingByondFirewallException`: Set to `true` if you have Windows firewall disabled
|
||||
|
||||
@@ -313,6 +313,12 @@ The following providers use the `ServerUrl` setting:
|
||||
- Keycloak
|
||||
- InvisionCommunity
|
||||
|
||||
- `Telemetry:DisableVersionReporting`: Prevents you installation and the version you're using from being reported on the source repository's deployments list
|
||||
|
||||
- `Telemetry:ServerFriendlyName`: Prevents anonymous TGS version usage statistics from being sent to be displayed on the repository.
|
||||
|
||||
- `Telemetry:VersionReportingRepositoryId`: The repository telemetry is sent to. For security reasons, this is not the main TGS repo. See the [tgstation-server-deployments](https://github.com/tgstation/tgstation-server-deployments) repository for more information.
|
||||
|
||||
### Database Configuration
|
||||
|
||||
If using a MariaDB/MySQL server, our client library [recommends you set 'utf8mb4' as your default charset](https://github.com/PomeloFoundation/Pomelo.EntityFrameworkCore.MySql#1-recommended-server-charset) disregard at your own risk.
|
||||
|
||||
+8
-2
@@ -1,5 +1,8 @@
|
||||
FROM mcr.microsoft.com/dotnet/sdk:8.0-bookworm-slim AS build
|
||||
|
||||
# Set in CI
|
||||
ARG TGS_TELEMETRY_KEY_FILE=
|
||||
|
||||
# install node and npm
|
||||
# replace shell with bash so we can source files
|
||||
RUN curl --silent -o- https://raw.githubusercontent.com/creationix/nvm/v0.39.1/install.sh | sh
|
||||
@@ -17,7 +20,8 @@ RUN . $NVM_DIR/nvm.sh \
|
||||
&& apt-get install -y \
|
||||
dos2unix \
|
||||
libgdiplus \
|
||||
&& rm -rf /var/lib/apt/lists/*
|
||||
&& rm -rf /var/lib/apt/lists/* \
|
||||
&& corepack enable
|
||||
|
||||
# Build web control panel
|
||||
WORKDIR /repo/build
|
||||
@@ -45,7 +49,9 @@ RUN dotnet publish -c Release -o /app \
|
||||
&& build/RemoveUnsupportedRuntimes.sh /app
|
||||
|
||||
WORKDIR /repo/src/Tgstation.Server.Host
|
||||
RUN dotnet publish -c Release -o /app/lib/Default \
|
||||
|
||||
RUN export TGS_TELEMETRY_KEY_FILE="../../${TGS_TELEMETRY_KEY_FILE}" \
|
||||
&& dotnet publish -c Release -o /app/lib/Default \
|
||||
&& cd ../.. \
|
||||
&& build/RemoveUnsupportedRuntimes.sh /app/lib/Default \
|
||||
&& mv /app/lib/Default/appsettings* /app
|
||||
|
||||
@@ -18,9 +18,9 @@
|
||||
<!-- Pinned: Be VERY careful about updating https://github.com/moq/moq/issues/1372 -->
|
||||
<PackageReference Include="Moq" Version="4.20.70" />
|
||||
<!-- Usage: MSTest execution -->
|
||||
<PackageReference Include="MSTest.TestAdapter" Version="3.5.0" />
|
||||
<PackageReference Include="MSTest.TestAdapter" Version="3.5.2" />
|
||||
<!-- Usage: MSTest asserts etc... -->
|
||||
<PackageReference Include="MSTest.TestFramework" Version="3.5.0" />
|
||||
<PackageReference Include="MSTest.TestFramework" Version="3.5.2" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
|
||||
+8
-9
@@ -3,13 +3,13 @@
|
||||
<!-- Integration tests will ensure they match across the board -->
|
||||
<Import Project="WebpanelVersion.props" />
|
||||
<PropertyGroup>
|
||||
<TgsCoreVersion>6.8.0</TgsCoreVersion>
|
||||
<TgsConfigVersion>5.1.0</TgsConfigVersion>
|
||||
<TgsApiVersion>10.6.0</TgsApiVersion>
|
||||
<TgsCoreVersion>6.9.0</TgsCoreVersion>
|
||||
<TgsConfigVersion>5.2.0</TgsConfigVersion>
|
||||
<TgsApiVersion>10.7.0</TgsApiVersion>
|
||||
<TgsCommonLibraryVersion>7.0.0</TgsCommonLibraryVersion>
|
||||
<TgsApiLibraryVersion>13.6.0</TgsApiLibraryVersion>
|
||||
<TgsClientVersion>15.6.0</TgsClientVersion>
|
||||
<TgsDmapiVersion>7.1.3</TgsDmapiVersion>
|
||||
<TgsApiLibraryVersion>13.7.0</TgsApiLibraryVersion>
|
||||
<TgsClientVersion>16.0.0</TgsClientVersion>
|
||||
<TgsDmapiVersion>7.2.1</TgsDmapiVersion>
|
||||
<TgsInteropVersion>5.9.0</TgsInteropVersion>
|
||||
<TgsHostWatchdogVersion>1.4.1</TgsHostWatchdogVersion>
|
||||
<TgsContainerScriptVersion>1.2.1</TgsContainerScriptVersion>
|
||||
@@ -18,9 +18,8 @@
|
||||
<TgsNetMajorVersion>8</TgsNetMajorVersion>
|
||||
<!-- Update this frequently with dotnet runtime patches. MAJOR MUST MATCH ABOVE! -->
|
||||
<TgsDotnetRedistUrl>https://download.visualstudio.microsoft.com/download/pr/751d3fcd-72db-4da2-b8d0-709c19442225/33cc492bde704bfd6d70a2b9109005a0/dotnet-hosting-8.0.6-win.exe</TgsDotnetRedistUrl>
|
||||
<TgsMariaDBRedistVersion>10.11.8</TgsMariaDBRedistVersion>
|
||||
<TgsMariaDBRedistVersion>11.4.2</TgsMariaDBRedistVersion>
|
||||
<!-- Only have this uncommented if the mariadb servers are shitting the bed, update if the version updates -->
|
||||
<TgsMariaDBFallbackRedist>https://mirror.its.dal.ca/mariadb//mariadb-10.11.8/winx64-packages/mariadb-10.11.8-winx64.msi</TgsMariaDBFallbackRedist>
|
||||
<TgsYarnVersion>1.22.21</TgsYarnVersion>
|
||||
<!--<TgsMariaDBFallbackRedist>https://mirror.its.dal.ca/mariadb//mariadb-10.11.8/winx64-packages/mariadb-10.11.8-winx64.msi</TgsMariaDBFallbackRedist>-->
|
||||
</PropertyGroup>
|
||||
</Project>
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
<Project xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
|
||||
<PropertyGroup>
|
||||
<!-- This is in it's own file to help incremental building, changing it causes a complete rebuild of the web panel -->
|
||||
<TgsWebpanelVersion>5.9.0</TgsWebpanelVersion>
|
||||
<TgsWebpanelVersion>6.1.0</TgsWebpanelVersion>
|
||||
</PropertyGroup>
|
||||
</Project>
|
||||
|
||||
@@ -18,7 +18,7 @@ apt-get install -y \
|
||||
devscripts \
|
||||
ca-certificates \
|
||||
curl \
|
||||
gnupg \
|
||||
gnupg2 \
|
||||
xmlstarlet \
|
||||
libgdiplus
|
||||
|
||||
@@ -35,6 +35,8 @@ echo "deb [signed-by=/etc/apt/keyrings/nodesource.gpg] https://deb.nodesource.co
|
||||
apt-get update
|
||||
apt-get install nodejs dotnet-sdk-8.0 -y
|
||||
|
||||
corepack enable
|
||||
|
||||
CURRENT_COMMIT=$(git rev-parse HEAD)
|
||||
|
||||
rm -rf packaging
|
||||
@@ -67,7 +69,6 @@ cp build/tgstation-server.service debian/
|
||||
SIGN_COMMAND="$SCRIPT_DIR/wrap_gpg.sh"
|
||||
|
||||
rm -f /tmp/tgs_wrap_gpg_output.log
|
||||
|
||||
set +e
|
||||
|
||||
if [[ -z "$PACKAGING_KEYGRIP" ]]; then
|
||||
|
||||
@@ -24,5 +24,5 @@ Depends:
|
||||
Recommends:
|
||||
libsystemd0,
|
||||
gdb,
|
||||
Description: A production scale tool for BYOND server management
|
||||
This is a toolset to manage production BYOND servers. It includes the ability to update the server without having to stop or shutdown the server (the update will take effect on a "reboot" of the server), the ability to start the server and restart it if it crashes, as well as systems for managing code and game files, and locally merging GitHub Pull Requests for test deployments.
|
||||
Description: A production scale tool for DreamMaker server management
|
||||
This is a toolset to manage production DreamMaker servers. It includes the ability to update the server without having to stop or shutdown the server (the update will take effect on a "reboot" of the server), the ability to start the server and restart it if it crashes, as well as systems for managing code and game files, and locally merging GitHub Pull Requests for test deployments.
|
||||
|
||||
@@ -10,7 +10,6 @@ override_dh_auto_clean:
|
||||
dotnet clean -c ReleaseNoWindows
|
||||
|
||||
override_dh_auto_build:
|
||||
dotnet restore
|
||||
cd src/Tgstation.Server.Host.Console && dotnet publish -c Release -o ../../artifacts
|
||||
cd src/Tgstation.Server.Host && dotnet publish -c Release -o ../../artifacts/lib/Default
|
||||
rm artifacts/lib/Default/appsettings.yml
|
||||
|
||||
@@ -17,7 +17,7 @@ Restart=always
|
||||
KillMode=process
|
||||
ReloadSignal=SIGUSR2
|
||||
RestartKillSignal=SIGUSR2
|
||||
AmbientCapabilities=CAP_SYS_NICE
|
||||
AmbientCapabilities=CAP_SYS_NICE CAP_SYS_PTRACE
|
||||
WatchdogSec=60
|
||||
WatchdogSignal=SIGTERM
|
||||
|
||||
|
||||
+1
-1
@@ -5,7 +5,7 @@
|
||||
|
||||
@section features_list Comprehensive Feature List
|
||||
|
||||
tgstation-server is a BYOND server managment suite. It includes all the following features
|
||||
tgstation-server is a DreamMaker server managment suite. It includes all the following features
|
||||
|
||||
- Standalone server with OpenAPI 3.0 defined HTTP REST API
|
||||
- Web based client included
|
||||
|
||||
+73
-27
@@ -1,18 +1,19 @@
|
||||
// tgstation-server DMAPI
|
||||
// The key words "MUST", "MUST NOT", "REQUIRED", "SHALL", "SHALL NOT", "SHOULD", "SHOULD NOT", "RECOMMENDED", "MAY", and "OPTIONAL" in this document are to be interpreted as described in IETF RFC 2119.
|
||||
|
||||
#define TGS_DMAPI_VERSION "7.1.3"
|
||||
#define TGS_DMAPI_VERSION "7.2.1"
|
||||
|
||||
// All functions and datums outside this document are subject to change with any version and should not be relied on.
|
||||
|
||||
// CONFIGURATION
|
||||
|
||||
/// Create this define if you want to do TGS configuration outside of this file.
|
||||
/// Consumers SHOULD create this define if you want to do TGS configuration outside of this file.
|
||||
#ifndef TGS_EXTERNAL_CONFIGURATION
|
||||
|
||||
// Comment this out once you've filled in the below.
|
||||
// Consumers MUST comment this out once you've filled in the below and are not using [TGS_EXTERNAL_CONFIGURATION].
|
||||
#error TGS API unconfigured
|
||||
|
||||
// Uncomment this if you wish to allow the game to interact with TGS 3..
|
||||
// Consumers MUST uncomment this if you wish to allow the game to interact with TGS version 3.
|
||||
// This will raise the minimum required security level of your game to TGS_SECURITY_TRUSTED due to it utilizing call()().
|
||||
//#define TGS_V3_API
|
||||
|
||||
@@ -52,7 +53,7 @@
|
||||
|
||||
#ifndef TGS_FILE2TEXT_NATIVE
|
||||
#ifdef file2text
|
||||
#error Your codebase is re-defining the BYOND proc file2text. The DMAPI requires the native version to read the result of world.Export(). You can fix this by adding "#define TGS_FILE2TEXT_NATIVE file2text" before your override of file2text to allow the DMAPI to use the native version. This will only be used for world.Export(), not regular file accesses
|
||||
#error Your codebase is re-defining the BYOND proc file2text. The DMAPI requires the native version to read the result of world.Export(). You SHOULD fix this by adding "#define TGS_FILE2TEXT_NATIVE file2text" before your override of file2text to allow the DMAPI to use the native version. This will only be used for world.Export(), not regular file accesses
|
||||
#endif
|
||||
#define TGS_FILE2TEXT_NATIVE file2text
|
||||
#endif
|
||||
@@ -152,16 +153,17 @@
|
||||
//REQUIRED HOOKS
|
||||
|
||||
/**
|
||||
* Call this somewhere in [/world/proc/New] that is always run. This function may sleep!
|
||||
* Consumers MUST call this somewhere in [/world/proc/New] that is always run. This function may sleep!
|
||||
*
|
||||
* * event_handler - Optional user defined [/datum/tgs_event_handler].
|
||||
* * minimum_required_security_level: The minimum required security level to run the game in which the DMAPI is integrated. Can be one of [TGS_SECURITY_ULTRASAFE], [TGS_SECURITY_SAFE], or [TGS_SECURITY_TRUSTED].
|
||||
* * http_handler - Optional user defined [/datum/tgs_http_handler].
|
||||
*/
|
||||
/world/proc/TgsNew(datum/tgs_event_handler/event_handler, minimum_required_security_level = TGS_SECURITY_ULTRASAFE)
|
||||
/world/proc/TgsNew(datum/tgs_event_handler/event_handler, minimum_required_security_level = TGS_SECURITY_ULTRASAFE, datum/tgs_http_handler/http_handler)
|
||||
return
|
||||
|
||||
/**
|
||||
* Call this when your initializations are complete and your game is ready to play before any player interactions happen.
|
||||
* Consumers MUST call this when world initializations are complete and the game is ready to play before any player interactions happen.
|
||||
*
|
||||
* This may use [/world/var/sleep_offline] to make this happen so ensure no changes are made to it while this call is running.
|
||||
* Afterwards, consider explicitly setting it to what you want to avoid this BYOND bug: http://www.byond.com/forum/post/2575184
|
||||
@@ -170,12 +172,10 @@
|
||||
/world/proc/TgsInitializationComplete()
|
||||
return
|
||||
|
||||
/// Put this at the start of [/world/proc/Topic].
|
||||
/// Consumers MUST run this macro at the start of [/world/proc/Topic].
|
||||
#define TGS_TOPIC var/tgs_topic_return = TgsTopic(args[1]); if(tgs_topic_return) return tgs_topic_return
|
||||
|
||||
/**
|
||||
* Call this as late as possible in [world/proc/Reboot] (BEFORE ..()).
|
||||
*/
|
||||
/// Consumers MUST call this as late as possible in [world/proc/Reboot] (BEFORE ..()).
|
||||
/world/proc/TgsReboot()
|
||||
return
|
||||
|
||||
@@ -269,7 +269,7 @@
|
||||
/// The [/datum/tgs_chat_channel] the user was from.
|
||||
var/datum/tgs_chat_channel/channel
|
||||
|
||||
/// User definable handler for TGS events.
|
||||
/// User definable handler for TGS events This abstract version SHOULD be overridden to be used.
|
||||
/datum/tgs_event_handler
|
||||
/// If the handler receieves [TGS_EVENT_HEALTH_CHECK] events.
|
||||
var/receive_health_checks = FALSE
|
||||
@@ -283,7 +283,41 @@
|
||||
set waitfor = FALSE
|
||||
return
|
||||
|
||||
/// User definable chat command.
|
||||
/// User definable handler for HTTP calls. This abstract version MUST be overridden to be used.
|
||||
/datum/tgs_http_handler
|
||||
|
||||
/**
|
||||
* User definable callback for executing HTTP GET requests.
|
||||
* MUST perform BYOND sleeps while the request is in flight.
|
||||
* MUST return a [/datum/tgs_http_result].
|
||||
* SHOULD log its own errors
|
||||
*
|
||||
* url - The full URL to execute the GET request for including query parameters.
|
||||
*/
|
||||
/datum/tgs_http_handler/proc/PerformGet(url)
|
||||
CRASH("[type]/PerformGet not implemented!")
|
||||
|
||||
/// Result of a [/datum/tgs_http_handler] call. MUST NOT be overridden.
|
||||
/datum/tgs_http_result
|
||||
/// HTTP response as text
|
||||
var/response_text
|
||||
/// Boolean request success flag. Set for any 2XX response code.
|
||||
var/success
|
||||
|
||||
/**
|
||||
* Create a [/datum/tgs_http_result].
|
||||
*
|
||||
* * response_text - HTTP response as text. Must be provided in New().
|
||||
* * success - Boolean request success flag. Set for any 2XX response code. Must be provided in New().
|
||||
*/
|
||||
/datum/tgs_http_result/New(response_text, success)
|
||||
if(response_text && !istext(response_text))
|
||||
CRASH("response_text was not text!")
|
||||
|
||||
src.response_text = response_text
|
||||
src.success = success
|
||||
|
||||
/// User definable chat command. This abstract version MUST be overridden to be used.
|
||||
/datum/tgs_chat_command
|
||||
/// The string to trigger this command on a chat bot. e.g `@bot name ...` or `!tgs name ...`.
|
||||
var/name = ""
|
||||
@@ -296,21 +330,27 @@
|
||||
|
||||
/**
|
||||
* Process command activation. Should return a [/datum/tgs_message_content] to respond to the issuer with.
|
||||
* MUST be implemented
|
||||
*
|
||||
* sender - The [/datum/tgs_chat_user] who issued the command.
|
||||
* params - The trimmed string following the command `/datum/tgs_chat_command/var/name].
|
||||
* * sender - The [/datum/tgs_chat_user] who issued the command.
|
||||
* * params - The trimmed string following the command `/datum/tgs_chat_command/var/name].
|
||||
*/
|
||||
/datum/tgs_chat_command/proc/Run(datum/tgs_chat_user/sender, params)
|
||||
CRASH("[type] has no implementation for Run()")
|
||||
|
||||
/// User definable chat message.
|
||||
/// User definable chat message. MUST NOT be overridden.
|
||||
/datum/tgs_message_content
|
||||
/// The tring content of the message. Must be provided in New().
|
||||
/// The string content of the message. Must be provided in New().
|
||||
var/text
|
||||
|
||||
/// The [/datum/tgs_chat_embed] to embed in the message. Not supported on all chat providers.
|
||||
var/datum/tgs_chat_embed/structure/embed
|
||||
|
||||
/**
|
||||
* Create a [/datum/tgs_message_content].
|
||||
*
|
||||
* * text - The string content of the message.
|
||||
*/
|
||||
/datum/tgs_message_content/New(text)
|
||||
..()
|
||||
if(!istext(text))
|
||||
@@ -319,7 +359,7 @@
|
||||
|
||||
src.text = text
|
||||
|
||||
/// User definable chat embed. Currently mirrors Discord chat embeds. See https://discord.com/developers/docs/resources/channel#embed-object-embed-structure for details.
|
||||
/// User definable chat embed. Currently mirrors Discord chat embeds. See https://discord.com/developers/docs/resources/message#embed-object for details.
|
||||
/datum/tgs_chat_embed/structure
|
||||
var/title
|
||||
var/description
|
||||
@@ -331,13 +371,13 @@
|
||||
/// Colour must be #AARRGGBB or #RRGGBB hex string.
|
||||
var/colour
|
||||
|
||||
/// See https://discord.com/developers/docs/resources/channel#embed-object-embed-image-structure for details.
|
||||
/// See https://discord.com/developers/docs/resources/message#embed-object-embed-image-structure for details.
|
||||
var/datum/tgs_chat_embed/media/image
|
||||
|
||||
/// See https://discord.com/developers/docs/resources/channel#embed-object-embed-thumbnail-structure for details.
|
||||
/// See https://discord.com/developers/docs/resources/message#embed-object-embed-thumbnail-structure for details.
|
||||
var/datum/tgs_chat_embed/media/thumbnail
|
||||
|
||||
/// See https://discord.com/developers/docs/resources/channel#embed-object-embed-image-structure for details.
|
||||
/// See https://discord.com/developers/docs/resources/message#embed-object-embed-video-structure for details.
|
||||
var/datum/tgs_chat_embed/media/video
|
||||
|
||||
var/datum/tgs_chat_embed/footer/footer
|
||||
@@ -346,7 +386,7 @@
|
||||
|
||||
var/list/datum/tgs_chat_embed/field/fields
|
||||
|
||||
/// Common datum for similar discord embed medias.
|
||||
/// Common datum for similar Discord embed medias.
|
||||
/datum/tgs_chat_embed/media
|
||||
/// Must be set in New().
|
||||
var/url
|
||||
@@ -354,6 +394,7 @@
|
||||
var/height
|
||||
var/proxy_url
|
||||
|
||||
/// Create a [/datum/tgs_chat_embed].
|
||||
/datum/tgs_chat_embed/media/New(url)
|
||||
..()
|
||||
if(!istext(url))
|
||||
@@ -361,13 +402,14 @@
|
||||
|
||||
src.url = url
|
||||
|
||||
/// See https://discord.com/developers/docs/resources/channel#embed-object-embed-footer-structure for details.
|
||||
/// See https://discord.com/developers/docs/resources/message#embed-object-embed-footer-structure for details.
|
||||
/datum/tgs_chat_embed/footer
|
||||
/// Must be set in New().
|
||||
var/text
|
||||
var/icon_url
|
||||
var/proxy_icon_url
|
||||
|
||||
/// Create a [/datum/tgs_chat_embed/footer].
|
||||
/datum/tgs_chat_embed/footer/New(text)
|
||||
..()
|
||||
if(!istext(text))
|
||||
@@ -375,16 +417,17 @@
|
||||
|
||||
src.text = text
|
||||
|
||||
/// See https://discord.com/developers/docs/resources/channel#embed-object-embed-provider-structure for details.
|
||||
/// See https://discord.com/developers/docs/resources/message#embed-object-embed-provider-structure for details.
|
||||
/datum/tgs_chat_embed/provider
|
||||
var/name
|
||||
var/url
|
||||
|
||||
/// See https://discord.com/developers/docs/resources/channel#embed-object-embed-author-structure for details. Must have name set in New().
|
||||
/// See https://discord.com/developers/docs/resources/message#embed-object-embed-author-structure for details. Must have name set in New().
|
||||
/datum/tgs_chat_embed/provider/author
|
||||
var/icon_url
|
||||
var/proxy_icon_url
|
||||
|
||||
/// Create a [/datum/tgs_chat_embed/footer].
|
||||
/datum/tgs_chat_embed/provider/author/New(name)
|
||||
..()
|
||||
if(!istext(name))
|
||||
@@ -392,12 +435,15 @@
|
||||
|
||||
src.name = name
|
||||
|
||||
/// See https://discord.com/developers/docs/resources/channel#embed-object-embed-field-structure for details. Must have name and value set in New().
|
||||
/// See https://discord.com/developers/docs/resources/message#embed-object-embed-field-structure for details.
|
||||
/datum/tgs_chat_embed/field
|
||||
/// Must be set in New().
|
||||
var/name
|
||||
/// Must be set in New().
|
||||
var/value
|
||||
var/is_inline
|
||||
|
||||
/// Create a [/datum/tgs_chat_embed/field].
|
||||
/datum/tgs_chat_embed/field/New(name, value)
|
||||
..()
|
||||
if(!istext(name))
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
This folder contains all DMAPI code not directly involved in an API.
|
||||
|
||||
- [_definitions.dm](./definitions.dm) contains defines needed across DMAPI internals.
|
||||
- [byond_world_export.dm](./byond_world_export.dm) contains the default `/datum/tgs_http_handler` implementation which uses `world.Export()`.
|
||||
- [core.dm](./core.dm) contains the implementations of the `/world/proc/TgsXXX()` procs. Many map directly to the `/datum/tgs_api` functions. It also contains the /datum selection and setup code.
|
||||
- [datum.dm](./datum.dm) contains the `/datum/tgs_api` declarations that all APIs must implement.
|
||||
- [tgs_version.dm](./tgs_version.dm) contains the `/datum/tgs_version` definition
|
||||
-
|
||||
|
||||
@@ -0,0 +1,22 @@
|
||||
/datum/tgs_http_handler/byond_world_export
|
||||
|
||||
/datum/tgs_http_handler/byond_world_export/PerformGet(url)
|
||||
// This is an infinite sleep until we get a response
|
||||
var/export_response = world.Export(url)
|
||||
TGS_DEBUG_LOG("byond_world_export: Export complete")
|
||||
|
||||
if(!export_response)
|
||||
TGS_ERROR_LOG("byond_world_export: Failed request: [url]")
|
||||
return new /datum/tgs_http_result(null, FALSE)
|
||||
|
||||
var/content = export_response["CONTENT"]
|
||||
if(!content)
|
||||
TGS_ERROR_LOG("byond_world_export: Failed request, missing content!")
|
||||
return new /datum/tgs_http_result(null, FALSE)
|
||||
|
||||
var/response_json = TGS_FILE2TEXT_NATIVE(content)
|
||||
if(!response_json)
|
||||
TGS_ERROR_LOG("byond_world_export: Failed request, failed to load content!")
|
||||
return new /datum/tgs_http_result(null, FALSE)
|
||||
|
||||
return new /datum/tgs_http_result(response_json, TRUE)
|
||||
@@ -1,4 +1,4 @@
|
||||
/world/TgsNew(datum/tgs_event_handler/event_handler, minimum_required_security_level = TGS_SECURITY_ULTRASAFE)
|
||||
/world/TgsNew(datum/tgs_event_handler/event_handler, minimum_required_security_level = TGS_SECURITY_ULTRASAFE, datum/tgs_http_handler/http_handler = null)
|
||||
var/current_api = TGS_READ_GLOBAL(tgs)
|
||||
if(current_api)
|
||||
TGS_ERROR_LOG("API datum already set (\ref[current_api] ([current_api]))! Was TgsNew() called more than once?")
|
||||
@@ -55,7 +55,10 @@
|
||||
TGS_ERROR_LOG("Invalid parameter for event_handler: [event_handler]")
|
||||
event_handler = null
|
||||
|
||||
var/datum/tgs_api/new_api = new api_datum(event_handler, version)
|
||||
if(!http_handler)
|
||||
http_handler = new /datum/tgs_http_handler/byond_world_export
|
||||
|
||||
var/datum/tgs_api/new_api = new api_datum(event_handler, version, http_handler)
|
||||
|
||||
TGS_WRITE_GLOBAL(tgs, new_api)
|
||||
|
||||
|
||||
@@ -6,7 +6,7 @@ TGS_DEFINE_AND_SET_GLOBAL(tgs, null)
|
||||
|
||||
var/list/warned_deprecated_command_runs
|
||||
|
||||
/datum/tgs_api/New(datum/tgs_event_handler/event_handler, datum/tgs_version/version)
|
||||
/datum/tgs_api/New(datum/tgs_event_handler/event_handler, datum/tgs_version/version, datum/tgs_http_handler/http_handler)
|
||||
..()
|
||||
src.event_handler = event_handler
|
||||
src.version = version
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
#include "core\_definitions.dm"
|
||||
#include "core\byond_world_export.dm"
|
||||
#include "core\core.dm"
|
||||
#include "core\datum.dm"
|
||||
#include "core\tgs_version.dm"
|
||||
|
||||
@@ -31,9 +31,12 @@
|
||||
|
||||
var/detached = FALSE
|
||||
|
||||
/datum/tgs_api/v5/New()
|
||||
var/datum/tgs_http_handler/http_handler
|
||||
|
||||
/datum/tgs_api/v5/New(datum/tgs_event_handler/event_handler, datum/tgs_version/version, datum/tgs_http_handler/http_handler)
|
||||
. = ..()
|
||||
interop_version = version
|
||||
src.http_handler = http_handler
|
||||
TGS_DEBUG_LOG("V5 API created: [json_encode(args)]")
|
||||
|
||||
/datum/tgs_api/v5/ApiVersion()
|
||||
|
||||
@@ -78,27 +78,24 @@
|
||||
WaitForReattach(FALSE)
|
||||
|
||||
TGS_DEBUG_LOG("Bridge request start")
|
||||
// This is an infinite sleep until we get a response
|
||||
var/export_response = world.Export(bridge_request)
|
||||
var/datum/tgs_http_result/result = http_handler.PerformGet(bridge_request)
|
||||
TGS_DEBUG_LOG("Bridge request complete")
|
||||
|
||||
if(!export_response)
|
||||
TGS_ERROR_LOG("Failed bridge request: [bridge_request]")
|
||||
if(isnull(result))
|
||||
TGS_ERROR_LOG("Failed bridge request, handler returned null!")
|
||||
return
|
||||
|
||||
var/content = export_response["CONTENT"]
|
||||
if(!content)
|
||||
TGS_ERROR_LOG("Failed bridge request, missing content!")
|
||||
if(!istype(result) || result.type != /datum/tgs_http_result)
|
||||
TGS_ERROR_LOG("Failed bridge request, handler returned non-[/datum/tgs_http_result]!")
|
||||
return
|
||||
|
||||
var/response_json = TGS_FILE2TEXT_NATIVE(content)
|
||||
if(!response_json)
|
||||
TGS_ERROR_LOG("Failed bridge request, failed to load content!")
|
||||
if(!result.success)
|
||||
TGS_DEBUG_LOG("Failed bridge request, HTTP request failed!")
|
||||
return
|
||||
|
||||
var/list/bridge_response = json_decode(response_json)
|
||||
var/list/bridge_response = json_decode(result.response_text)
|
||||
if(!bridge_response)
|
||||
TGS_ERROR_LOG("Failed bridge request, bad json: [response_json]")
|
||||
TGS_ERROR_LOG("Failed bridge request, bad json: [result.response_text]")
|
||||
return
|
||||
|
||||
var/error = bridge_response[DMAPI5_RESPONSE_ERROR_MESSAGE]
|
||||
|
||||
@@ -108,5 +108,11 @@ namespace Tgstation.Server.Api.Models
|
||||
/// </summary>
|
||||
[Description("Reconnect chat bot")]
|
||||
ReconnectChatBot,
|
||||
|
||||
/// <summary>
|
||||
/// When a repository is recloned.
|
||||
/// </summary>
|
||||
[Description("Reclone repository")]
|
||||
RepositoryReclone,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -16,5 +16,10 @@ namespace Tgstation.Server.Api.Models.Response
|
||||
/// The latest available version of the Tgstation.Server.Host assembly from the upstream repository. If <see cref="Version.Major"/> is not equal to 4 the update cannot be applied due to API changes.
|
||||
/// </summary>
|
||||
public Version? LatestVersion { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// This response is cached. This field indicates the <see cref="DateTimeOffset"/> when it was generated.
|
||||
/// </summary>
|
||||
public DateTimeOffset? GeneratedAt { get; set; }
|
||||
}
|
||||
}
|
||||
|
||||
@@ -59,5 +59,11 @@ namespace Tgstation.Server.Api.Models.Response
|
||||
/// </summary>
|
||||
[ResponseOptions]
|
||||
public bool? CurrentAllowWebclient { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The amount of RAM in use by the game server in bytes.
|
||||
/// </summary>
|
||||
[ResponseOptions]
|
||||
public long? ImmediateMemoryUsage { get; set; }
|
||||
}
|
||||
}
|
||||
|
||||
@@ -82,5 +82,10 @@ namespace Tgstation.Server.Api.Rights
|
||||
/// User may change submodule update settings.
|
||||
/// </summary>
|
||||
ChangeSubmoduleUpdate = 1 << 13,
|
||||
|
||||
/// <summary>
|
||||
/// User may trigger repository recloning.
|
||||
/// </summary>
|
||||
Reclone = 1 << 14,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -24,7 +24,7 @@ namespace Tgstation.Server.Client
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public ValueTask<AdministrationResponse> Read(CancellationToken cancellationToken) => ApiClient.Read<AdministrationResponse>(Routes.Administration, cancellationToken);
|
||||
public ValueTask<AdministrationResponse> Read(bool forceFresh, CancellationToken cancellationToken) => ApiClient.Read<AdministrationResponse>($"{Routes.Administration}?fresh={forceFresh}", cancellationToken);
|
||||
|
||||
/// <inheritdoc />
|
||||
public async ValueTask<ServerUpdateResponse> Update(
|
||||
|
||||
@@ -43,7 +43,10 @@ namespace Tgstation.Server.Client.Components
|
||||
public ValueTask<JobResponse> Restart(CancellationToken cancellationToken) => apiClient.Patch<JobResponse>(Routes.DreamDaemon, instance.Id!.Value, cancellationToken);
|
||||
|
||||
/// <inheritdoc />
|
||||
public ValueTask<DreamDaemonResponse> Read(CancellationToken cancellationToken) => apiClient.Read<DreamDaemonResponse>(Routes.DreamDaemon, instance.Id!.Value, cancellationToken);
|
||||
public ValueTask<DreamDaemonResponse> Read(CancellationToken cancellationToken) => apiClient.Read<DreamDaemonResponse>(
|
||||
Routes.DreamDaemon,
|
||||
instance.Id!.Value,
|
||||
cancellationToken);
|
||||
|
||||
/// <inheritdoc />
|
||||
public ValueTask<DreamDaemonResponse> Update(DreamDaemonRequest dreamDaemon, CancellationToken cancellationToken) => apiClient.Update<DreamDaemonRequest, DreamDaemonResponse>(Routes.DreamDaemon, dreamDaemon ?? throw new ArgumentNullException(nameof(dreamDaemon)), instance.Id!.Value, cancellationToken);
|
||||
|
||||
@@ -16,7 +16,7 @@ namespace Tgstation.Server.Client.Components
|
||||
/// </summary>
|
||||
/// <param name="cancellationToken">The <see cref="CancellationToken"/> for the operation.</param>
|
||||
/// <returns>A <see cref="ValueTask{TResult}"/> resulting in the <see cref="DreamDaemonResponse"/> information.</returns>
|
||||
ValueTask<DreamDaemonResponse> Read(CancellationToken cancellationToken);
|
||||
ValueTask<DreamDaemonResponse> Read(CancellationToken cancellationToken = default);
|
||||
|
||||
/// <summary>
|
||||
/// Start <see cref="DreamDaemonResponse"/>.
|
||||
|
||||
@@ -40,5 +40,12 @@ namespace Tgstation.Server.Client.Components
|
||||
/// <param name="cancellationToken">The <see cref="CancellationToken"/> for the operation.</param>
|
||||
/// <returns>A <see cref="ValueTask{TResult}"/> resulting in the <see cref="RepositoryResponse"/>.</returns>
|
||||
ValueTask<RepositoryResponse> Delete(CancellationToken cancellationToken);
|
||||
|
||||
/// <summary>
|
||||
/// Deletes and reclones the repository.
|
||||
/// </summary>
|
||||
/// <param name="cancellationToken">The <see cref="CancellationToken"/> for the operation.</param>
|
||||
/// <returns>A <see cref="ValueTask{TResult}"/> resulting in the <see cref="RepositoryResponse"/>.</returns>
|
||||
ValueTask<RepositoryResponse> Reclone(CancellationToken cancellationToken);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -44,5 +44,8 @@ namespace Tgstation.Server.Client.Components
|
||||
|
||||
/// <inheritdoc />
|
||||
public ValueTask<RepositoryResponse> Update(RepositoryUpdateRequest repository, CancellationToken cancellationToken) => apiClient.Update<RepositoryUpdateRequest, RepositoryResponse>(Routes.Repository, repository ?? throw new ArgumentNullException(nameof(repository)), instance.Id!.Value, cancellationToken);
|
||||
|
||||
/// <inheritdoc />
|
||||
public ValueTask<RepositoryResponse> Reclone(CancellationToken cancellationToken) => apiClient.Patch<RepositoryResponse>(Routes.Repository, instance.Id!.Value, cancellationToken);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -17,9 +17,10 @@ namespace Tgstation.Server.Client
|
||||
/// <summary>
|
||||
/// Get the <see cref="AdministrationResponse"/> represented by the <see cref="IAdministrationClient"/>.
|
||||
/// </summary>
|
||||
/// <param name="forceFresh">If <see langword="true"/> the response will be forcefully regenerated.</param>
|
||||
/// <param name="cancellationToken">The <see cref="CancellationToken"/> for the operation.</param>
|
||||
/// <returns>A <see cref="ValueTask{TResult}"/> resulting in the <see cref="AdministrationResponse"/> represented by the <see cref="IAdministrationClient"/>.</returns>
|
||||
ValueTask<AdministrationResponse> Read(CancellationToken cancellationToken);
|
||||
ValueTask<AdministrationResponse> Read(bool forceFresh = false, CancellationToken cancellationToken = default);
|
||||
|
||||
/// <summary>
|
||||
/// Updates the <see cref="AdministrationResponse"/> setttings.
|
||||
|
||||
@@ -11,9 +11,9 @@
|
||||
|
||||
<ItemGroup>
|
||||
<!-- Usage: Connecting to SignalR hubs in API -->
|
||||
<PackageReference Include="Microsoft.AspNetCore.SignalR.Client" Version="8.0.7" />
|
||||
<PackageReference Include="Microsoft.AspNetCore.SignalR.Client" Version="8.0.8" />
|
||||
<!-- Usage: Using target JSON serializer for API -->
|
||||
<PackageReference Include="Microsoft.AspNetCore.SignalR.Protocols.NewtonsoftJson" Version="8.0.7" />
|
||||
<PackageReference Include="Microsoft.AspNetCore.SignalR.Protocols.NewtonsoftJson" Version="8.0.8" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
"isRoot": true,
|
||||
"tools": {
|
||||
"dotnet-ef": {
|
||||
"version": "8.0.7",
|
||||
"version": "8.0.8",
|
||||
"commands": [
|
||||
"dotnet-ef"
|
||||
]
|
||||
|
||||
@@ -50,7 +50,7 @@ namespace Tgstation.Server.Host.Components.Chat.Commands
|
||||
public async ValueTask<MessageContent> Invoke(string arguments, ChatUser user, CancellationToken cancellationToken)
|
||||
{
|
||||
string result;
|
||||
if (arguments.Split(' ').Any(x => x.ToUpperInvariant() == "--REPO"))
|
||||
if (arguments.Split(' ').Any(x => x.Equals("--repo", StringComparison.OrdinalIgnoreCase)))
|
||||
{
|
||||
if (repositoryManager.CloneInProgress || repositoryManager.InUse)
|
||||
return new MessageContent
|
||||
@@ -58,15 +58,13 @@ namespace Tgstation.Server.Host.Components.Chat.Commands
|
||||
Text = "Repository busy! Try again later",
|
||||
};
|
||||
|
||||
using (var repo = await repositoryManager.LoadRepository(cancellationToken))
|
||||
{
|
||||
if (repo == null)
|
||||
return new MessageContent
|
||||
{
|
||||
Text = "Repository unavailable!",
|
||||
};
|
||||
result = repo.Head;
|
||||
}
|
||||
using var repo = await repositoryManager.LoadRepository(cancellationToken);
|
||||
if (repo == null)
|
||||
return new MessageContent
|
||||
{
|
||||
Text = "Repository unavailable!",
|
||||
};
|
||||
result = repo.Head;
|
||||
}
|
||||
else
|
||||
{
|
||||
|
||||
+6
-6
@@ -79,13 +79,13 @@ namespace Tgstation.Server.Host.Components.Deployment.Remote
|
||||
IGitHubService gitHubService;
|
||||
if (instanceAuthenticated)
|
||||
{
|
||||
authenticatedGitHubService = gitHubServiceFactory.CreateService(repositorySettings.AccessToken!);
|
||||
authenticatedGitHubService = await gitHubServiceFactory.CreateService(repositorySettings.AccessToken!, cancellationToken);
|
||||
gitHubService = authenticatedGitHubService;
|
||||
}
|
||||
else
|
||||
{
|
||||
authenticatedGitHubService = null;
|
||||
gitHubService = gitHubServiceFactory.CreateService();
|
||||
gitHubService = await gitHubServiceFactory.CreateService(cancellationToken);
|
||||
}
|
||||
|
||||
var repoOwner = remoteInformation.RemoteRepositoryOwner!;
|
||||
@@ -175,8 +175,8 @@ namespace Tgstation.Server.Host.Components.Deployment.Remote
|
||||
}
|
||||
|
||||
var gitHubService = repositorySettings.AccessToken != null
|
||||
? gitHubServiceFactory.CreateService(repositorySettings.AccessToken)
|
||||
: gitHubServiceFactory.CreateService();
|
||||
? await gitHubServiceFactory.CreateService(repositorySettings.AccessToken, cancellationToken)
|
||||
: await gitHubServiceFactory.CreateService(cancellationToken);
|
||||
|
||||
var tasks = revisionInformation
|
||||
.ActiveTestMerges
|
||||
@@ -255,7 +255,7 @@ namespace Tgstation.Server.Host.Components.Deployment.Remote
|
||||
int testMergeNumber,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
var gitHubService = gitHubServiceFactory.CreateService(repositorySettings.AccessToken!);
|
||||
var gitHubService = await gitHubServiceFactory.CreateService(repositorySettings.AccessToken!, cancellationToken);
|
||||
|
||||
try
|
||||
{
|
||||
@@ -343,7 +343,7 @@ namespace Tgstation.Server.Host.Components.Deployment.Remote
|
||||
return;
|
||||
}
|
||||
|
||||
var gitHubService = gitHubServiceFactory.CreateService(gitHubAccessToken);
|
||||
var gitHubService = await gitHubServiceFactory.CreateService(gitHubAccessToken, cancellationToken);
|
||||
|
||||
try
|
||||
{
|
||||
|
||||
@@ -146,7 +146,7 @@ namespace Tgstation.Server.Host.Components.Engine
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public override async ValueTask<IEngineInstallationData> DownloadVersion(EngineVersion version, JobProgressReporter? progressReporter, CancellationToken cancellationToken)
|
||||
public override async ValueTask<IEngineInstallationData> DownloadVersion(EngineVersion version, JobProgressReporter progressReporter, CancellationToken cancellationToken)
|
||||
{
|
||||
CheckVersionValidity(version);
|
||||
|
||||
|
||||
@@ -37,7 +37,7 @@ namespace Tgstation.Server.Host.Components.Engine
|
||||
=> DelegateCall(version, installer => installer.CreateInstallation(version, path, installationTask));
|
||||
|
||||
/// <inheritdoc />
|
||||
public ValueTask<IEngineInstallationData> DownloadVersion(EngineVersion version, JobProgressReporter? jobProgressReporter, CancellationToken cancellationToken)
|
||||
public ValueTask<IEngineInstallationData> DownloadVersion(EngineVersion version, JobProgressReporter jobProgressReporter, CancellationToken cancellationToken)
|
||||
=> DelegateCall(version, installer => installer.DownloadVersion(version, jobProgressReporter, cancellationToken));
|
||||
|
||||
/// <inheritdoc />
|
||||
|
||||
@@ -52,7 +52,7 @@ namespace Tgstation.Server.Host.Components.Engine
|
||||
public abstract ValueTask UpgradeInstallation(EngineVersion version, string path, CancellationToken cancellationToken);
|
||||
|
||||
/// <inheritdoc />
|
||||
public abstract ValueTask<IEngineInstallationData> DownloadVersion(EngineVersion version, JobProgressReporter? jobProgressReporter, CancellationToken cancellationToken);
|
||||
public abstract ValueTask<IEngineInstallationData> DownloadVersion(EngineVersion version, JobProgressReporter jobProgressReporter, CancellationToken cancellationToken);
|
||||
|
||||
/// <inheritdoc />
|
||||
public abstract ValueTask TrustDmbPath(EngineVersion version, string fullDmbPath, CancellationToken cancellationToken);
|
||||
|
||||
@@ -118,7 +118,7 @@ namespace Tgstation.Server.Host.Components.Engine
|
||||
|
||||
/// <inheritdoc />
|
||||
public async ValueTask ChangeVersion(
|
||||
JobProgressReporter? progressReporter,
|
||||
JobProgressReporter progressReporter,
|
||||
EngineVersion version,
|
||||
Stream? customVersionStream,
|
||||
bool allowInstallation,
|
||||
@@ -166,8 +166,11 @@ namespace Tgstation.Server.Host.Components.Engine
|
||||
"Acquiring lock on BYOND version {version}...",
|
||||
requiredVersion?.ToString() ?? $"{ActiveVersion} (active)");
|
||||
var versionToUse = requiredVersion ?? ActiveVersion ?? throw new JobException(ErrorCode.EngineNoVersionsInstalled);
|
||||
|
||||
using var progressReporter = new JobProgressReporter();
|
||||
|
||||
var installLock = await AssertAndLockVersion(
|
||||
null,
|
||||
progressReporter,
|
||||
versionToUse,
|
||||
null,
|
||||
requiredVersion != null,
|
||||
@@ -388,7 +391,7 @@ namespace Tgstation.Server.Host.Components.Engine
|
||||
/// <summary>
|
||||
/// Ensures a BYOND <paramref name="version"/> is installed if it isn't already.
|
||||
/// </summary>
|
||||
/// <param name="progressReporter">The optional <see cref="JobProgressReporter"/> for the operation.</param>
|
||||
/// <param name="progressReporter">The <see cref="JobProgressReporter"/> for the operation.</param>
|
||||
/// <param name="version">The <see cref="EngineVersion"/> to install.</param>
|
||||
/// <param name="customVersionStream">Optional custom zip file <see cref="Stream"/> to use. Will cause a <see cref="Version.Build"/> number to be added.</param>
|
||||
/// <param name="neededForLock">If this BYOND version is required as part of a locking operation.</param>
|
||||
@@ -396,7 +399,7 @@ namespace Tgstation.Server.Host.Components.Engine
|
||||
/// <param name="cancellationToken">The <see cref="CancellationToken"/> for the operation.</param>
|
||||
/// <returns>A <see cref="ValueTask{TResult}"/> resulting in the <see cref="EngineExecutableLock"/>.</returns>
|
||||
async ValueTask<EngineExecutableLock> AssertAndLockVersion(
|
||||
JobProgressReporter? progressReporter,
|
||||
JobProgressReporter progressReporter,
|
||||
EngineVersion version,
|
||||
Stream? customVersionStream,
|
||||
bool neededForLock,
|
||||
@@ -443,8 +446,7 @@ namespace Tgstation.Server.Host.Components.Engine
|
||||
{
|
||||
if (installedOrInstalling)
|
||||
{
|
||||
if (progressReporter != null)
|
||||
progressReporter.StageName = "Waiting for existing installation job...";
|
||||
progressReporter.StageName = "Waiting for existing installation job...";
|
||||
|
||||
if (neededForLock && !installation.InstallationTask.IsCompleted)
|
||||
logger.LogWarning("The required engine version ({version}) is not readily available! We will have to wait for it to install.", version);
|
||||
@@ -468,8 +470,7 @@ namespace Tgstation.Server.Host.Components.Engine
|
||||
else
|
||||
logger.LogInformation("Requested engine version {version} not currently installed. Doing so now...", version);
|
||||
|
||||
if (progressReporter != null)
|
||||
progressReporter.StageName = "Running event";
|
||||
progressReporter.StageName = "Running event";
|
||||
|
||||
var versionString = version.ToString();
|
||||
await eventConsumer.HandleEvent(EventType.EngineInstallStart, new List<string> { versionString }, deploymentPipelineProcesses, cancellationToken);
|
||||
@@ -504,14 +505,14 @@ namespace Tgstation.Server.Host.Components.Engine
|
||||
/// <summary>
|
||||
/// Installs the files for a given BYOND <paramref name="version"/>.
|
||||
/// </summary>
|
||||
/// <param name="progressReporter">The optional <see cref="JobProgressReporter"/> for the operation.</param>
|
||||
/// <param name="progressReporter">The <see cref="JobProgressReporter"/> for the operation.</param>
|
||||
/// <param name="version">The <see cref="EngineVersion"/> being installed with the <see cref="Version.Build"/> number set if appropriate.</param>
|
||||
/// <param name="customVersionStream">Custom zip file <see cref="Stream"/> to use. Will cause a <see cref="Version.Build"/> number to be added.</param>
|
||||
/// <param name="deploymentPipelineProcesses">If processes should be launched as part of the deployment pipeline.</param>
|
||||
/// <param name="cancellationToken">The <see cref="CancellationToken"/> for the operation.</param>
|
||||
/// <returns>A <see cref="ValueTask"/> representing the running operation.</returns>
|
||||
async ValueTask InstallVersionFiles(
|
||||
JobProgressReporter? progressReporter,
|
||||
JobProgressReporter progressReporter,
|
||||
EngineVersion version,
|
||||
Stream? customVersionStream,
|
||||
bool deploymentPipelineProcesses,
|
||||
@@ -528,14 +529,12 @@ namespace Tgstation.Server.Host.Components.Engine
|
||||
try
|
||||
{
|
||||
IEngineInstallationData engineInstallationData;
|
||||
var remainingProgress = 1.0;
|
||||
if (customVersionStream == null)
|
||||
{
|
||||
if (progressReporter != null)
|
||||
progressReporter.StageName = "Downloading version";
|
||||
|
||||
engineInstallationData = await engineInstaller.DownloadVersion(version, progressReporter, cancellationToken);
|
||||
|
||||
progressReporter?.ReportProgress(null);
|
||||
using var subReporter = progressReporter.CreateSection("Downloading Version", 0.5);
|
||||
remainingProgress -= 0.5;
|
||||
engineInstallationData = await engineInstaller.DownloadVersion(version, subReporter, cancellationToken);
|
||||
}
|
||||
else
|
||||
#pragma warning disable CA2000 // Dispose objects before losing scope, false positive
|
||||
@@ -544,33 +543,45 @@ namespace Tgstation.Server.Host.Components.Engine
|
||||
customVersionStream);
|
||||
#pragma warning restore CA2000 // Dispose objects before losing scope
|
||||
|
||||
await using (engineInstallationData)
|
||||
JobProgressReporter remainingReporter;
|
||||
try
|
||||
{
|
||||
if (progressReporter != null)
|
||||
progressReporter.StageName = "Cleaning target directory";
|
||||
|
||||
await directoryCleanupTask;
|
||||
|
||||
if (progressReporter != null)
|
||||
progressReporter.StageName = "Extracting data";
|
||||
|
||||
logger.LogTrace("Extracting engine to {extractPath}...", installFullPath);
|
||||
await engineInstallationData.ExtractToPath(installFullPath, cancellationToken);
|
||||
remainingReporter = progressReporter.CreateSection(null, remainingProgress);
|
||||
}
|
||||
catch
|
||||
{
|
||||
await engineInstallationData.DisposeAsync();
|
||||
throw;
|
||||
}
|
||||
|
||||
if (progressReporter != null)
|
||||
progressReporter.StageName = "Running installation actions";
|
||||
using (remainingReporter)
|
||||
{
|
||||
await using (engineInstallationData)
|
||||
{
|
||||
remainingReporter.StageName = "Cleaning target directory";
|
||||
|
||||
await engineInstaller.Install(version, installFullPath, deploymentPipelineProcesses, cancellationToken);
|
||||
await directoryCleanupTask;
|
||||
remainingReporter.ReportProgress(0.1);
|
||||
remainingReporter.StageName = "Extracting data";
|
||||
|
||||
if (progressReporter != null)
|
||||
progressReporter.StageName = "Writing version file";
|
||||
logger.LogTrace("Extracting engine to {extractPath}...", installFullPath);
|
||||
await engineInstallationData.ExtractToPath(installFullPath, cancellationToken);
|
||||
remainingReporter.ReportProgress(0.3);
|
||||
}
|
||||
|
||||
// make sure to do this last because this is what tells us we have a valid version in the future
|
||||
await ioManager.WriteAllBytes(
|
||||
ioManager.ConcatPath(installFullPath, VersionFileName),
|
||||
Encoding.UTF8.GetBytes(version.ToString()),
|
||||
cancellationToken);
|
||||
remainingReporter.StageName = "Running installation actions";
|
||||
|
||||
await engineInstaller.Install(version, installFullPath, deploymentPipelineProcesses, cancellationToken);
|
||||
|
||||
remainingReporter.ReportProgress(0.9);
|
||||
remainingReporter.StageName = "Writing version file";
|
||||
|
||||
// make sure to do this last because this is what tells us we have a valid version in the future
|
||||
await ioManager.WriteAllBytes(
|
||||
ioManager.ConcatPath(installFullPath, VersionFileName),
|
||||
Encoding.UTF8.GetBytes(version.ToString()),
|
||||
cancellationToken);
|
||||
}
|
||||
}
|
||||
catch (HttpRequestException ex)
|
||||
{
|
||||
|
||||
@@ -24,10 +24,10 @@ namespace Tgstation.Server.Host.Components.Engine
|
||||
/// Download a given engine <paramref name="version"/>.
|
||||
/// </summary>
|
||||
/// <param name="version">The <see cref="EngineVersion"/> of the engine to download.</param>
|
||||
/// <param name="jobProgressReporter">The optional <see cref="JobProgressReporter"/> for the operation.</param>
|
||||
/// <param name="jobProgressReporter">The <see cref="JobProgressReporter"/> for the operation.</param>
|
||||
/// <param name="cancellationToken">The <see cref="CancellationToken"/> for the operation.</param>
|
||||
/// <returns>A <see cref="ValueTask{TResult}"/> resulting in the <see cref="IEngineInstallationData"/> for the download.</returns>
|
||||
ValueTask<IEngineInstallationData> DownloadVersion(EngineVersion version, JobProgressReporter? jobProgressReporter, CancellationToken cancellationToken);
|
||||
ValueTask<IEngineInstallationData> DownloadVersion(EngineVersion version, JobProgressReporter jobProgressReporter, CancellationToken cancellationToken);
|
||||
|
||||
/// <summary>
|
||||
/// Does actions necessary to get an extracted installation working.
|
||||
|
||||
@@ -28,14 +28,14 @@ namespace Tgstation.Server.Host.Components.Engine
|
||||
/// <summary>
|
||||
/// Change the active <see cref="EngineVersion"/>.
|
||||
/// </summary>
|
||||
/// <param name="progressReporter">The optional <see cref="JobProgressReporter"/> for the operation.</param>
|
||||
/// <param name="progressReporter">The <see cref="JobProgressReporter"/> for the operation.</param>
|
||||
/// <param name="version">The new <see cref="EngineVersion"/>.</param>
|
||||
/// <param name="customVersionStream">Optional <see cref="Stream"/> of a custom BYOND version zip file.</param>
|
||||
/// <param name="allowInstallation">If an installation should be performed if the <paramref name="version"/> is not installed. If <see langword="false"/> and an installation is required an <see cref="InvalidOperationException"/> will be thrown.</param>
|
||||
/// <param name="cancellationToken">The <see cref="CancellationToken"/> for the operation.</param>
|
||||
/// <returns>A <see cref="ValueTask"/> representing the running operation.</returns>
|
||||
ValueTask ChangeVersion(
|
||||
JobProgressReporter? progressReporter,
|
||||
JobProgressReporter progressReporter,
|
||||
EngineVersion version,
|
||||
Stream? customVersionStream,
|
||||
bool allowInstallation,
|
||||
|
||||
@@ -133,23 +133,32 @@ namespace Tgstation.Server.Host.Components.Engine
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public override async ValueTask<IEngineInstallationData> DownloadVersion(EngineVersion version, JobProgressReporter? jobProgressReporter, CancellationToken cancellationToken)
|
||||
public override async ValueTask<IEngineInstallationData> DownloadVersion(EngineVersion version, JobProgressReporter jobProgressReporter, CancellationToken cancellationToken)
|
||||
{
|
||||
CheckVersionValidity(version);
|
||||
ArgumentNullException.ThrowIfNull(jobProgressReporter);
|
||||
|
||||
// get a lock on a system wide OD repo
|
||||
Logger.LogTrace("Cloning OD repo...");
|
||||
|
||||
var progressSection1 = jobProgressReporter?.CreateSection("Updating OpenDream git repository", 0.5f);
|
||||
|
||||
var repo = await repositoryManager.CloneRepository(
|
||||
GeneralConfiguration.OpenDreamGitUrl,
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
progressSection1,
|
||||
true,
|
||||
cancellationToken);
|
||||
var progressSection1 = jobProgressReporter.CreateSection("Updating OpenDream git repository", 0.5f);
|
||||
IRepository? repo;
|
||||
try
|
||||
{
|
||||
repo = await repositoryManager.CloneRepository(
|
||||
GeneralConfiguration.OpenDreamGitUrl,
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
progressSection1,
|
||||
true,
|
||||
cancellationToken);
|
||||
}
|
||||
catch
|
||||
{
|
||||
progressSection1.Dispose();
|
||||
throw;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
@@ -157,6 +166,8 @@ namespace Tgstation.Server.Host.Components.Engine
|
||||
{
|
||||
Logger.LogTrace("OD repo seems to already exist, attempting load and fetch...");
|
||||
repo = await repositoryManager.LoadRepository(cancellationToken);
|
||||
if (repo == null)
|
||||
throw new JobException("Can't load OpenDream repository! Please delete cache from disk!");
|
||||
|
||||
await repo!.FetchOrigin(
|
||||
progressSection1,
|
||||
@@ -166,18 +177,23 @@ namespace Tgstation.Server.Host.Components.Engine
|
||||
cancellationToken);
|
||||
}
|
||||
|
||||
var progressSection2 = jobProgressReporter?.CreateSection("Checking out OpenDream version", 0.5f);
|
||||
progressSection1.Dispose();
|
||||
progressSection1 = null;
|
||||
|
||||
var committish = version.SourceSHA
|
||||
?? $"{GeneralConfiguration.OpenDreamGitTagPrefix}{version.Version!.Semver()}";
|
||||
using (var progressSection2 = jobProgressReporter.CreateSection("Checking out OpenDream version", 0.5f))
|
||||
{
|
||||
var committish = version.SourceSHA
|
||||
?? $"{GeneralConfiguration.OpenDreamGitTagPrefix}{version.Version!.Semver()}";
|
||||
|
||||
await repo.CheckoutObject(
|
||||
committish,
|
||||
null,
|
||||
null,
|
||||
true,
|
||||
progressSection2,
|
||||
cancellationToken);
|
||||
await repo.CheckoutObject(
|
||||
committish,
|
||||
null,
|
||||
null,
|
||||
true,
|
||||
false,
|
||||
progressSection2,
|
||||
cancellationToken);
|
||||
}
|
||||
|
||||
if (!await repo.CommittishIsParent("tgs-min-compat", cancellationToken))
|
||||
throw new JobException(ErrorCode.OpenDreamTooOld);
|
||||
@@ -189,6 +205,10 @@ namespace Tgstation.Server.Host.Components.Engine
|
||||
repo?.Dispose();
|
||||
throw;
|
||||
}
|
||||
finally
|
||||
{
|
||||
progressSection1?.Dispose();
|
||||
}
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
|
||||
@@ -224,7 +224,7 @@ namespace Tgstation.Server.Host.Components.Engine
|
||||
/// <inheritdoc />
|
||||
protected override string GetDreamDaemonName(Version byondVersion, out bool supportsCli)
|
||||
{
|
||||
supportsCli = byondVersion >= DDExeVersion;
|
||||
supportsCli = byondVersion >= DDExeVersion && !sessionConfiguration.ForceUseDreamDaemonExe;
|
||||
return supportsCli ? "dd.exe" : "dreamdaemon.exe";
|
||||
}
|
||||
|
||||
|
||||
@@ -12,7 +12,7 @@
|
||||
RepoResetOrigin,
|
||||
|
||||
/// <summary>
|
||||
/// Parameters: Checkout target.
|
||||
/// Parameters: Checkout target, hard reset flag (If "True", this is actually a hard reset, not a checkout).
|
||||
/// </summary>
|
||||
[EventScript("RepoCheckout")]
|
||||
RepoCheckout,
|
||||
|
||||
@@ -49,8 +49,8 @@ namespace Tgstation.Server.Host.Components.Repository
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
var gitHubService = repositorySettings.AccessToken != null
|
||||
? gitHubServiceFactory.CreateService(repositorySettings.AccessToken)
|
||||
: gitHubServiceFactory.CreateService();
|
||||
? await gitHubServiceFactory.CreateService(repositorySettings.AccessToken, cancellationToken)
|
||||
: await gitHubServiceFactory.CreateService(cancellationToken);
|
||||
|
||||
PullRequest? pr = null;
|
||||
ApiException? exception = null;
|
||||
|
||||
@@ -47,7 +47,8 @@ namespace Tgstation.Server.Host.Components.Repository
|
||||
/// <param name="username">The optional username used for fetching from submodule repositories.</param>
|
||||
/// <param name="password">The optional password used for fetching from submodule repositories.</param>
|
||||
/// <param name="updateSubmodules">If a submodule update should be attempted after the merge.</param>
|
||||
/// <param name="progressReporter">The optional <see cref="JobProgressReporter"/> to report progress of the operation.</param>
|
||||
/// <param name="moveCurrentReference">If a hard reset to the target committish should be performed instead of a checkout.</param>
|
||||
/// <param name="progressReporter">The <see cref="JobProgressReporter"/> to report progress of the operation.</param>
|
||||
/// <param name="cancellationToken">The <see cref="CancellationToken"/> for the operation.</param>
|
||||
/// <returns>A <see cref="ValueTask"/> representing the running operation.</returns>
|
||||
ValueTask CheckoutObject(
|
||||
@@ -55,7 +56,8 @@ namespace Tgstation.Server.Host.Components.Repository
|
||||
string? username,
|
||||
string? password,
|
||||
bool updateSubmodules,
|
||||
JobProgressReporter? progressReporter,
|
||||
bool moveCurrentReference,
|
||||
JobProgressReporter progressReporter,
|
||||
CancellationToken cancellationToken);
|
||||
|
||||
/// <summary>
|
||||
@@ -83,14 +85,14 @@ namespace Tgstation.Server.Host.Components.Repository
|
||||
/// <summary>
|
||||
/// Fetch commits from the origin repository.
|
||||
/// </summary>
|
||||
/// <param name="progressReporter">The optional <see cref="JobProgressReporter"/> to report progress of the operation.</param>
|
||||
/// <param name="progressReporter">The <see cref="JobProgressReporter"/> to report progress of the operation.</param>
|
||||
/// <param name="username">The optional username to fetch from the origin repository.</param>
|
||||
/// <param name="password">The optional password to fetch from the origin repository.</param>
|
||||
/// <param name="deploymentPipeline">If any events created should be marked as part of the deployment pipeline.</param>
|
||||
/// <param name="cancellationToken">The <see cref="CancellationToken"/> for the operation.</param>
|
||||
/// <returns>A <see cref="ValueTask"/> representing the running operation.</returns>
|
||||
ValueTask FetchOrigin(
|
||||
JobProgressReporter? progressReporter,
|
||||
JobProgressReporter progressReporter,
|
||||
string? username,
|
||||
string? password,
|
||||
bool deploymentPipeline,
|
||||
|
||||
@@ -35,7 +35,7 @@ namespace Tgstation.Server.Host.Components.Repository
|
||||
/// <param name="initialBranch">The optional branch to clone.</param>
|
||||
/// <param name="username">The optional username to clone from <paramref name="url"/>.</param>
|
||||
/// <param name="password">The optional password to clone from <paramref name="url"/>.</param>
|
||||
/// <param name="progressReporter">The optional <see cref="JobProgressReporter"/> for progress of the clone.</param>
|
||||
/// <param name="progressReporter">The <see cref="JobProgressReporter"/> for progress of the clone.</param>
|
||||
/// <param name="recurseSubmodules">If submodules should be recusively cloned and initialized.</param>
|
||||
/// <param name="cancellationToken">The <see cref="CancellationToken"/> for the operation.</param>
|
||||
/// <returns>A <see cref="ValueTask{TResult}"/> resulting i the newly cloned <see cref="IRepository"/>, <see langword="null"/> if one already exists.</returns>
|
||||
@@ -44,7 +44,7 @@ namespace Tgstation.Server.Host.Components.Repository
|
||||
string? initialBranch,
|
||||
string? username,
|
||||
string? password,
|
||||
JobProgressReporter? progressReporter,
|
||||
JobProgressReporter progressReporter,
|
||||
bool recurseSubmodules,
|
||||
CancellationToken cancellationToken);
|
||||
|
||||
|
||||
@@ -232,13 +232,14 @@ namespace Tgstation.Server.Host.Components.Repository
|
||||
logger.LogTrace("Fetching refspec {refSpec}...", refSpec);
|
||||
|
||||
var remote = libGitRepo.Network.Remotes.First();
|
||||
using var fetchReporter = progressReporter.CreateSection($"Fetch {refSpec}", progressFactor);
|
||||
commands.Fetch(
|
||||
libGitRepo,
|
||||
refSpecList,
|
||||
remote,
|
||||
new FetchOptions().Hydrate(
|
||||
logger,
|
||||
progressReporter.CreateSection($"Fetch {refSpec}", progressFactor),
|
||||
fetchReporter,
|
||||
credentialsProvider.GenerateCredentialsHandler(username, password),
|
||||
cancellationToken),
|
||||
logMessage);
|
||||
@@ -267,14 +268,14 @@ namespace Tgstation.Server.Host.Components.Repository
|
||||
|
||||
logger.LogTrace("Merging {targetCommitSha} into {currentReference}...", testMergeParameters.TargetCommitSha[..7], Reference);
|
||||
|
||||
using var mergeReporter = progressReporter.CreateSection($"Merge {testMergeParameters.TargetCommitSha[..7]}", progressFactor);
|
||||
result = libGitRepo.Merge(testMergeParameters.TargetCommitSha, sig, new MergeOptions
|
||||
{
|
||||
CommitOnSuccess = commitMessage == null,
|
||||
FailOnConflict = false, // Needed to get conflicting files
|
||||
FastForwardStrategy = FastForwardStrategy.NoFastForward,
|
||||
SkipReuc = true,
|
||||
OnCheckoutProgress = CheckoutProgressHandler(
|
||||
progressReporter.CreateSection($"Merge {testMergeParameters.TargetCommitSha[..7]}", progressFactor)),
|
||||
OnCheckoutProgress = CheckoutProgressHandler(mergeReporter),
|
||||
});
|
||||
}
|
||||
finally
|
||||
@@ -295,7 +296,8 @@ namespace Tgstation.Server.Host.Components.Repository
|
||||
var revertTo = originalCommit.CanonicalName ?? originalCommit.Tip.Sha;
|
||||
logger.LogDebug("Merge conflict, aborting and reverting to {revertTarget}", revertTo);
|
||||
progressReporter.ReportProgress(0);
|
||||
RawCheckout(revertTo, progressReporter.CreateSection("Hard Reset to {revertTo}", 1.0), cancellationToken);
|
||||
using var revertReporter = progressReporter.CreateSection("Hard Reset to {revertTo}", 1.0);
|
||||
RawCheckout(revertTo, false, revertReporter, cancellationToken);
|
||||
cancellationToken.ThrowIfCancellationRequested();
|
||||
}
|
||||
|
||||
@@ -343,8 +345,9 @@ namespace Tgstation.Server.Host.Components.Repository
|
||||
|
||||
if (updateSubmodules)
|
||||
{
|
||||
using var progressReporter2 = progressReporter.CreateSection("Update Submodules", progressFactor);
|
||||
await UpdateSubmodules(
|
||||
progressReporter.CreateSection("Update Submodules", progressFactor),
|
||||
progressReporter2,
|
||||
username,
|
||||
password,
|
||||
false,
|
||||
@@ -376,20 +379,23 @@ namespace Tgstation.Server.Host.Components.Repository
|
||||
string? username,
|
||||
string? password,
|
||||
bool updateSubmodules,
|
||||
JobProgressReporter? progressReporter,
|
||||
bool moveCurrentReference,
|
||||
JobProgressReporter progressReporter,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(committish);
|
||||
|
||||
logger.LogDebug("Checkout object: {committish}...", committish);
|
||||
await eventConsumer.HandleEvent(EventType.RepoCheckout, new List<string> { committish }, false, cancellationToken);
|
||||
await eventConsumer.HandleEvent(EventType.RepoCheckout, new List<string> { committish, moveCurrentReference.ToString() }, false, cancellationToken);
|
||||
await Task.Factory.StartNew(
|
||||
() =>
|
||||
{
|
||||
libGitRepo.RemoveUntrackedFiles();
|
||||
using var progressReporter3 = progressReporter.CreateSection(null, updateSubmodules ? 2.0 / 3 : 1.0);
|
||||
RawCheckout(
|
||||
committish,
|
||||
progressReporter?.CreateSection(null, updateSubmodules ? 2.0 / 3 : 1.0),
|
||||
moveCurrentReference,
|
||||
progressReporter3,
|
||||
cancellationToken);
|
||||
},
|
||||
cancellationToken,
|
||||
@@ -397,17 +403,20 @@ namespace Tgstation.Server.Host.Components.Repository
|
||||
TaskScheduler.Current);
|
||||
|
||||
if (updateSubmodules)
|
||||
{
|
||||
using var progressReporter2 = progressReporter.CreateSection(null, 1.0 / 3);
|
||||
await UpdateSubmodules(
|
||||
progressReporter?.CreateSection(null, 1.0 / 3),
|
||||
progressReporter2,
|
||||
username,
|
||||
password,
|
||||
false,
|
||||
cancellationToken);
|
||||
}
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public async ValueTask FetchOrigin(
|
||||
JobProgressReporter? progressReporter,
|
||||
JobProgressReporter progressReporter,
|
||||
string? username,
|
||||
string? password,
|
||||
bool deploymentPipeline,
|
||||
@@ -421,13 +430,14 @@ namespace Tgstation.Server.Host.Components.Repository
|
||||
var remote = libGitRepo.Network.Remotes.First();
|
||||
try
|
||||
{
|
||||
using var subReporter = progressReporter.CreateSection("Fetch Origin", 1.0);
|
||||
var fetchOptions = new FetchOptions
|
||||
{
|
||||
Prune = true,
|
||||
TagFetchMode = TagFetchMode.All,
|
||||
}.Hydrate(
|
||||
logger,
|
||||
progressReporter?.CreateSection("Fetch Origin", 1.0),
|
||||
subReporter,
|
||||
credentialsProvider.GenerateCredentialsHandler(username, password),
|
||||
cancellationToken);
|
||||
|
||||
@@ -469,18 +479,23 @@ namespace Tgstation.Server.Host.Components.Repository
|
||||
logger.LogTrace("Reset to origin...");
|
||||
var trackedBranch = libGitRepo.Head.TrackedBranch;
|
||||
await eventConsumer.HandleEvent(EventType.RepoResetOrigin, new List<string> { trackedBranch.FriendlyName, trackedBranch.Tip.Sha }, deploymentPipeline, cancellationToken);
|
||||
await ResetToSha(
|
||||
trackedBranch.Tip.Sha,
|
||||
progressReporter.CreateSection(null, updateSubmodules ? 2.0 / 3 : 1.0),
|
||||
cancellationToken);
|
||||
|
||||
using (var progressReporter2 = progressReporter.CreateSection(null, updateSubmodules ? 2.0 / 3 : 1.0))
|
||||
await ResetToSha(
|
||||
trackedBranch.Tip.Sha,
|
||||
progressReporter2,
|
||||
cancellationToken);
|
||||
|
||||
if (updateSubmodules)
|
||||
{
|
||||
using var progressReporter3 = progressReporter.CreateSection(null, 1.0 / 3);
|
||||
await UpdateSubmodules(
|
||||
progressReporter.CreateSection(null, 1.0 / 3),
|
||||
progressReporter3,
|
||||
username,
|
||||
password,
|
||||
deploymentPipeline,
|
||||
cancellationToken);
|
||||
}
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
@@ -682,9 +697,10 @@ namespace Tgstation.Server.Host.Components.Repository
|
||||
await Task.Factory.StartNew(
|
||||
() =>
|
||||
{
|
||||
using var resetProgress = progressReporter.CreateSection("Hard reset and remove untracked files", 0.1);
|
||||
libGitRepo.Reset(ResetMode.Hard, libGitRepo.Head.Tip, new CheckoutOptions
|
||||
{
|
||||
OnCheckoutProgress = CheckoutProgressHandler(progressReporter.CreateSection("Hard reset and remove untracked files", 0.1)),
|
||||
OnCheckoutProgress = CheckoutProgressHandler(resetProgress),
|
||||
});
|
||||
cancellationToken.ThrowIfCancellationRequested();
|
||||
libGitRepo.RemoveUntrackedFiles();
|
||||
@@ -697,10 +713,11 @@ namespace Tgstation.Server.Host.Components.Repository
|
||||
var remainingProgressFactor = 0.9;
|
||||
if (!synchronizeTrackedBranch)
|
||||
{
|
||||
using var progressReporter2 = progressReporter.CreateSection("Push to temporary branch", remainingProgressFactor);
|
||||
await PushHeadToTemporaryBranch(
|
||||
username,
|
||||
password,
|
||||
progressReporter.CreateSection("Push to temporary branch", remainingProgressFactor),
|
||||
progressReporter2,
|
||||
cancellationToken);
|
||||
return false;
|
||||
}
|
||||
@@ -720,13 +737,24 @@ namespace Tgstation.Server.Host.Components.Repository
|
||||
var remote = libGitRepo.Network.Remotes.First();
|
||||
try
|
||||
{
|
||||
libGitRepo.Network.Push(
|
||||
libGitRepo.Head,
|
||||
GeneratePushOptions(
|
||||
progressReporter.CreateSection("Push to origin", remainingProgressFactor),
|
||||
username,
|
||||
password,
|
||||
cancellationToken));
|
||||
using var pushReporter = progressReporter.CreateSection("Push to origin", remainingProgressFactor);
|
||||
var (pushOptions, progressReporters) = GeneratePushOptions(
|
||||
pushReporter,
|
||||
username,
|
||||
password,
|
||||
cancellationToken);
|
||||
try
|
||||
{
|
||||
libGitRepo.Network.Push(
|
||||
libGitRepo.Head,
|
||||
pushOptions);
|
||||
}
|
||||
finally
|
||||
{
|
||||
foreach (var progressReporter in progressReporters)
|
||||
progressReporter.Dispose();
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
catch (NonFastForwardException)
|
||||
@@ -879,9 +907,10 @@ namespace Tgstation.Server.Host.Components.Repository
|
||||
/// Runs a blocking force checkout to <paramref name="committish"/>.
|
||||
/// </summary>
|
||||
/// <param name="committish">The committish to checkout.</param>
|
||||
/// <param name="progressReporter">The optional <see cref="JobProgressReporter"/> for the operation.</param>
|
||||
/// <param name="moveCurrentReference">If a hard reset should actually be performed.</param>
|
||||
/// <param name="progressReporter">The <see cref="JobProgressReporter"/> for the operation.</param>
|
||||
/// <param name="cancellationToken">The <see cref="CancellationToken"/> for the operation.</param>
|
||||
void RawCheckout(string committish, JobProgressReporter? progressReporter, CancellationToken cancellationToken)
|
||||
void RawCheckout(string committish, bool moveCurrentReference, JobProgressReporter progressReporter, CancellationToken cancellationToken)
|
||||
{
|
||||
logger.LogTrace("Checkout: {committish}", committish);
|
||||
|
||||
@@ -890,44 +919,59 @@ namespace Tgstation.Server.Host.Components.Repository
|
||||
CheckoutModifiers = CheckoutModifiers.Force,
|
||||
};
|
||||
|
||||
if (progressReporter != null)
|
||||
{
|
||||
var stage = $"Checkout {committish}";
|
||||
progressReporter = progressReporter.CreateSection(stage, 1.0);
|
||||
progressReporter.ReportProgress(0);
|
||||
checkoutOptions.OnCheckoutProgress = CheckoutProgressHandler(progressReporter);
|
||||
}
|
||||
var stage = $"Checkout {committish}";
|
||||
using var newProgressReporter = progressReporter.CreateSection(stage, 1.0);
|
||||
newProgressReporter.ReportProgress(0);
|
||||
checkoutOptions.OnCheckoutProgress = CheckoutProgressHandler(newProgressReporter);
|
||||
|
||||
cancellationToken.ThrowIfCancellationRequested();
|
||||
|
||||
void RunCheckout() => commands.Checkout(
|
||||
libGitRepo,
|
||||
checkoutOptions,
|
||||
committish);
|
||||
|
||||
try
|
||||
if (moveCurrentReference)
|
||||
{
|
||||
RunCheckout();
|
||||
if (Reference == NoReference)
|
||||
throw new InvalidOperationException("Cannot move current reference when not on reference!");
|
||||
|
||||
var gitObject = libGitRepo.Lookup(committish);
|
||||
if (gitObject == null)
|
||||
throw new JobException($"Could not find committish: {committish}");
|
||||
|
||||
var commit = gitObject.Peel<Commit>();
|
||||
|
||||
cancellationToken.ThrowIfCancellationRequested();
|
||||
|
||||
libGitRepo.Reset(ResetMode.Hard, commit, checkoutOptions);
|
||||
}
|
||||
catch (NotFoundException)
|
||||
else
|
||||
{
|
||||
// Maybe (likely) a remote?
|
||||
var remoteName = $"origin/{committish}";
|
||||
var remoteBranch = libGitRepo.Branches.FirstOrDefault(
|
||||
branch => branch.FriendlyName.Equals(remoteName, StringComparison.Ordinal));
|
||||
cancellationToken.ThrowIfCancellationRequested();
|
||||
void RunCheckout() => commands.Checkout(
|
||||
libGitRepo,
|
||||
checkoutOptions,
|
||||
committish);
|
||||
|
||||
if (remoteBranch == default)
|
||||
throw;
|
||||
try
|
||||
{
|
||||
RunCheckout();
|
||||
}
|
||||
catch (NotFoundException)
|
||||
{
|
||||
// Maybe (likely) a remote?
|
||||
var remoteName = $"origin/{committish}";
|
||||
var remoteBranch = libGitRepo.Branches.FirstOrDefault(
|
||||
branch => branch.FriendlyName.Equals(remoteName, StringComparison.Ordinal));
|
||||
cancellationToken.ThrowIfCancellationRequested();
|
||||
|
||||
logger.LogDebug("Creating local branch for {remoteBranchFriendlyName}...", remoteBranch.FriendlyName);
|
||||
var branch = libGitRepo.CreateBranch(committish, remoteBranch.Tip);
|
||||
if (remoteBranch == default)
|
||||
throw;
|
||||
|
||||
libGitRepo.Branches.Update(branch, branchUpdate => branchUpdate.TrackedBranch = remoteBranch.CanonicalName);
|
||||
logger.LogDebug("Creating local branch for {remoteBranchFriendlyName}...", remoteBranch.FriendlyName);
|
||||
var branch = libGitRepo.CreateBranch(committish, remoteBranch.Tip);
|
||||
|
||||
cancellationToken.ThrowIfCancellationRequested();
|
||||
libGitRepo.Branches.Update(branch, branchUpdate => branchUpdate.TrackedBranch = remoteBranch.CanonicalName);
|
||||
|
||||
RunCheckout();
|
||||
cancellationToken.ThrowIfCancellationRequested();
|
||||
|
||||
RunCheckout();
|
||||
}
|
||||
}
|
||||
|
||||
cancellationToken.ThrowIfCancellationRequested();
|
||||
@@ -955,9 +999,38 @@ namespace Tgstation.Server.Host.Components.Repository
|
||||
try
|
||||
{
|
||||
var forcePushString = String.Format(CultureInfo.InvariantCulture, "+{0}:{0}", branch.CanonicalName);
|
||||
libGitRepo.Network.Push(remote, forcePushString, GeneratePushOptions(progressReporter.CreateSection(null, 0.9), username, password, cancellationToken));
|
||||
|
||||
using (var mainPushReporter = progressReporter.CreateSection(null, 0.9))
|
||||
{
|
||||
var (pushOptions, progressReporters) = GeneratePushOptions(
|
||||
mainPushReporter,
|
||||
username,
|
||||
password,
|
||||
cancellationToken);
|
||||
|
||||
try
|
||||
{
|
||||
libGitRepo.Network.Push(remote, forcePushString, pushOptions);
|
||||
}
|
||||
finally
|
||||
{
|
||||
foreach (var progressReporter in progressReporters)
|
||||
progressReporter.Dispose();
|
||||
}
|
||||
}
|
||||
|
||||
var removalString = String.Format(CultureInfo.InvariantCulture, ":{0}", branch.CanonicalName);
|
||||
libGitRepo.Network.Push(remote, removalString, GeneratePushOptions(progressReporter.CreateSection(null, 0.1), username, password, cancellationToken));
|
||||
using var forcePushReporter = progressReporter.CreateSection(null, 0.1);
|
||||
var (forcePushOptions, forcePushReporters) = GeneratePushOptions(forcePushReporter, username, password, cancellationToken);
|
||||
try
|
||||
{
|
||||
libGitRepo.Network.Push(remote, removalString, forcePushOptions);
|
||||
}
|
||||
finally
|
||||
{
|
||||
foreach (var subForcePushReporter in forcePushReporters)
|
||||
forcePushReporter.Dispose();
|
||||
}
|
||||
}
|
||||
catch (UserCancelledException)
|
||||
{
|
||||
@@ -984,32 +1057,39 @@ namespace Tgstation.Server.Host.Components.Repository
|
||||
/// <param name="username">The username for the <see cref="credentialsProvider"/>.</param>
|
||||
/// <param name="password">The password for the <see cref="credentialsProvider"/>.</param>
|
||||
/// <param name="cancellationToken">The <see cref="CancellationToken"/> for the operation.</param>
|
||||
/// <returns>A new set of <see cref="PushOptions"/>.</returns>
|
||||
PushOptions GeneratePushOptions(JobProgressReporter progressReporter, string username, string password, CancellationToken cancellationToken)
|
||||
/// <returns>A new set of <see cref="PushOptions"/> and the associated <see cref="JobProgressReporter"/>s based off <paramref name="progressReporter"/>.</returns>
|
||||
(PushOptions PushOptions, IEnumerable<JobProgressReporter> SubProgressReporters) GeneratePushOptions(JobProgressReporter progressReporter, string username, string password, CancellationToken cancellationToken)
|
||||
{
|
||||
var subProgressReporter = progressReporter.CreateSection(null, 0.5);
|
||||
var packFileCountingReporter = progressReporter.CreateSection(null, 0.25);
|
||||
var packFileDeltafyingReporter = progressReporter.CreateSection(null, 0.25);
|
||||
var transferProgressReporter = progressReporter.CreateSection(null, 0.5);
|
||||
|
||||
return new PushOptions
|
||||
{
|
||||
OnPackBuilderProgress = (stage, current, total) =>
|
||||
return (
|
||||
PushOptions: new PushOptions
|
||||
{
|
||||
var baseProgress = stage == PackBuilderStage.Counting ? 0 : 0.5;
|
||||
var addon = total > 0 && current <= total ? (0.5 * ((double)current / total)) : 0;
|
||||
progressReporter.ReportProgress(baseProgress + addon);
|
||||
return !cancellationToken.IsCancellationRequested;
|
||||
OnPackBuilderProgress = (stage, current, total) =>
|
||||
{
|
||||
if (total < current)
|
||||
total = current;
|
||||
|
||||
var percentage = ((double)current) / total;
|
||||
(stage == PackBuilderStage.Counting ? packFileCountingReporter : packFileDeltafyingReporter).ReportProgress(percentage);
|
||||
return !cancellationToken.IsCancellationRequested;
|
||||
},
|
||||
OnNegotiationCompletedBeforePush = (a) => !cancellationToken.IsCancellationRequested,
|
||||
OnPushTransferProgress = (a, sentBytes, totalBytes) =>
|
||||
{
|
||||
packFileCountingReporter.ReportProgress((double)sentBytes / totalBytes);
|
||||
return !cancellationToken.IsCancellationRequested;
|
||||
},
|
||||
CredentialsProvider = credentialsProvider.GenerateCredentialsHandler(username, password),
|
||||
},
|
||||
OnNegotiationCompletedBeforePush = (a) =>
|
||||
SubProgressReporters: new List<JobProgressReporter>
|
||||
{
|
||||
subProgressReporter = progressReporter.CreateSection(null, 0.5);
|
||||
return !cancellationToken.IsCancellationRequested;
|
||||
},
|
||||
OnPushTransferProgress = (a, sentBytes, totalBytes) =>
|
||||
{
|
||||
progressReporter.ReportProgress((double)sentBytes / totalBytes);
|
||||
return !cancellationToken.IsCancellationRequested;
|
||||
},
|
||||
CredentialsProvider = credentialsProvider.GenerateCredentialsHandler(username, password),
|
||||
};
|
||||
packFileCountingReporter,
|
||||
packFileDeltafyingReporter,
|
||||
transferProgressReporter,
|
||||
});
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -1033,7 +1113,7 @@ namespace Tgstation.Server.Host.Components.Repository
|
||||
/// <param name="cancellationToken">The <see cref="CancellationToken"/> for the operation.</param>
|
||||
/// <returns>A <see cref="ValueTask"/> representing the running operation.</returns>
|
||||
ValueTask UpdateSubmodules(
|
||||
JobProgressReporter? progressReporter,
|
||||
JobProgressReporter progressReporter,
|
||||
string? username,
|
||||
string? password,
|
||||
bool deploymentPipeline,
|
||||
@@ -1041,7 +1121,7 @@ namespace Tgstation.Server.Host.Components.Repository
|
||||
{
|
||||
logger.LogTrace("Updating submodules {withOrWithout} credentials...", username == null ? "without" : "with");
|
||||
|
||||
async ValueTask RecursiveUpdateSubmodules(LibGit2Sharp.IRepository parentRepository, JobProgressReporter? currentProgressReporter, string parentGitDirectory)
|
||||
async ValueTask RecursiveUpdateSubmodules(LibGit2Sharp.IRepository parentRepository, JobProgressReporter currentProgressReporter, string parentGitDirectory)
|
||||
{
|
||||
var submoduleCount = libGitRepo.Submodules.Count();
|
||||
if (submoduleCount == 0)
|
||||
@@ -1060,15 +1140,16 @@ namespace Tgstation.Server.Host.Components.Repository
|
||||
OnCheckoutNotify = (_, _) => !cancellationToken.IsCancellationRequested,
|
||||
};
|
||||
|
||||
using var fetchReporter = currentProgressReporter.CreateSection($"Fetch submodule {submodule.Name}", factor);
|
||||
|
||||
submoduleUpdateOptions.FetchOptions.Hydrate(
|
||||
logger,
|
||||
currentProgressReporter?.CreateSection($"Fetch submodule {submodule.Name}", factor),
|
||||
fetchReporter,
|
||||
credentialsProvider.GenerateCredentialsHandler(username, password),
|
||||
cancellationToken);
|
||||
|
||||
if (currentProgressReporter != null)
|
||||
submoduleUpdateOptions.OnCheckoutProgress = CheckoutProgressHandler(
|
||||
currentProgressReporter.CreateSection($"Checkout submodule {submodule.Name}", factor));
|
||||
using var checkoutReporter = currentProgressReporter.CreateSection($"Checkout submodule {submodule.Name}", factor);
|
||||
submoduleUpdateOptions.OnCheckoutProgress = CheckoutProgressHandler(checkoutReporter);
|
||||
|
||||
logger.LogDebug("Updating submodule {submoduleName}...", submodule.Name);
|
||||
Task RawSubModuleUpdate() => Task.Factory.StartNew(
|
||||
@@ -1085,7 +1166,7 @@ namespace Tgstation.Server.Host.Components.Repository
|
||||
{
|
||||
// workaround for https://github.com/libgit2/libgit2/issues/3820
|
||||
// kill off the modules/ folder in .git and try again
|
||||
currentProgressReporter?.ReportProgress(null);
|
||||
currentProgressReporter.ReportProgress(0);
|
||||
credentialsProvider.CheckBadCredentialsException(ex);
|
||||
logger.LogWarning(ex, "Initial update of submodule {submoduleName} failed. Deleting submodule directories and re-attempting...", submodule.Name);
|
||||
|
||||
@@ -1124,9 +1205,11 @@ namespace Tgstation.Server.Host.Components.Repository
|
||||
using var submoduleRepo = await submoduleFactory.CreateFromPath(
|
||||
submodulePath,
|
||||
cancellationToken);
|
||||
|
||||
using var submoduleReporter = currentProgressReporter.CreateSection($"Entering submodule \"{submodule.Name}\"...", factor);
|
||||
await RecursiveUpdateSubmodules(
|
||||
submoduleRepo,
|
||||
currentProgressReporter?.CreateSection($"Entering submodule \"{submodule.Name}\"...", factor),
|
||||
submoduleReporter,
|
||||
submodulePath);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -123,7 +123,7 @@ namespace Tgstation.Server.Host.Components.Repository
|
||||
string? initialBranch,
|
||||
string? username,
|
||||
string? password,
|
||||
JobProgressReporter? progressReporter,
|
||||
JobProgressReporter progressReporter,
|
||||
bool recurseSubmodules,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
@@ -146,8 +146,8 @@ namespace Tgstation.Server.Host.Components.Repository
|
||||
if (!await ioManager.DirectoryExists(repositoryPath, cancellationToken))
|
||||
try
|
||||
{
|
||||
var cloneProgressReporter = progressReporter?.CreateSection(null, 0.75f);
|
||||
var checkoutProgressReporter = progressReporter?.CreateSection(null, 0.25f);
|
||||
using var cloneProgressReporter = progressReporter.CreateSection(null, 0.75f);
|
||||
using var checkoutProgressReporter = progressReporter.CreateSection(null, 0.25f);
|
||||
var cloneOptions = new CloneOptions
|
||||
{
|
||||
RecurseSubmodules = recurseSubmodules,
|
||||
|
||||
@@ -22,11 +22,6 @@ namespace Tgstation.Server.Host.Components.Repository
|
||||
/// </summary>
|
||||
sealed class RepositoryUpdateService
|
||||
{
|
||||
/// <summary>
|
||||
/// The <see cref="RepositoryUpdateRequest"/> for the <see cref="RepositoryUpdateService"/>.
|
||||
/// </summary>
|
||||
readonly RepositoryUpdateRequest model;
|
||||
|
||||
/// <summary>
|
||||
/// The current <see cref="RepositorySettings"/> for the <see cref="RepositoryUpdateService"/>.
|
||||
/// </summary>
|
||||
@@ -50,19 +45,16 @@ namespace Tgstation.Server.Host.Components.Repository
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="RepositoryUpdateService"/> class.
|
||||
/// </summary>
|
||||
/// <param name="model">The value of <see cref="model"/>.</param>
|
||||
/// <param name="currentModel">The value of <see cref="currentModel"/>.</param>
|
||||
/// <param name="initiatingUser">The value of <see cref="initiatingUser"/>.</param>
|
||||
/// <param name="logger">The value of <see cref="logger"/>.</param>
|
||||
/// <param name="instanceId">The value of <see cref="instanceId"/>.</param>
|
||||
public RepositoryUpdateService(
|
||||
RepositoryUpdateRequest model,
|
||||
RepositorySettings currentModel,
|
||||
User initiatingUser,
|
||||
ILogger<RepositoryUpdateService> logger,
|
||||
long instanceId)
|
||||
{
|
||||
this.model = model ?? throw new ArgumentNullException(nameof(model));
|
||||
this.currentModel = currentModel ?? throw new ArgumentNullException(nameof(currentModel));
|
||||
this.initiatingUser = initiatingUser ?? throw new ArgumentNullException(nameof(initiatingUser));
|
||||
this.logger = logger ?? throw new ArgumentNullException(nameof(logger));
|
||||
@@ -139,24 +131,25 @@ namespace Tgstation.Server.Host.Components.Repository
|
||||
/// <summary>
|
||||
/// The job entrypoint used by <see cref="Controllers.RepositoryController"/> to update the repository's current HEAD.
|
||||
/// </summary>
|
||||
/// <param name="instance">The <see cref="IInstanceCore"/> the job is running on. <see langword="null"/> only when performing an instance move operation.</param>
|
||||
/// <param name="model">The <see cref="RepositoryUpdateRequest"/>.</param>
|
||||
/// <param name="instance">The <see cref="IInstanceCore"/> the job is running on.</param>
|
||||
/// <param name="databaseContextFactory">The <see cref="IDatabaseContextFactory"/> for the operation.</param>
|
||||
/// <param name="job">The running <see cref="Job"/>, ignored.</param>
|
||||
/// <param name="progressReporter">The <see cref="JobProgressReporter"/> for the job.</param>
|
||||
/// <param name="cancellationToken">The <see cref="CancellationToken"/> for the operation.</param>
|
||||
/// <returns>A <see cref="ValueTask"/> representing the running operation.</returns>
|
||||
#pragma warning disable CA1502, CA1506 // TODO: Decomplexify
|
||||
public async ValueTask RepositoryUpdateJob(
|
||||
RepositoryUpdateRequest model,
|
||||
IInstanceCore? instance,
|
||||
IDatabaseContextFactory databaseContextFactory,
|
||||
Job job,
|
||||
JobProgressReporter progressReporter,
|
||||
CancellationToken cancellationToken)
|
||||
#pragma warning restore CA1502, CA1506
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(model);
|
||||
ArgumentNullException.ThrowIfNull(instance);
|
||||
|
||||
_ = job; // shuts up an IDE warning
|
||||
ArgumentNullException.ThrowIfNull(databaseContextFactory);
|
||||
ArgumentNullException.ThrowIfNull(progressReporter);
|
||||
|
||||
var repoManager = instance.RepositoryManager;
|
||||
using var repo = await repoManager.LoadRepository(cancellationToken) ?? throw new JobException(ErrorCode.RepoMissing);
|
||||
@@ -180,10 +173,7 @@ namespace Tgstation.Server.Host.Components.Repository
|
||||
var numSteps = (model.NewTestMerges?.Count ?? 0) + (model.UpdateFromOrigin == true ? 1 : 0) + (!modelHasShaOrReference ? 2 : (hardResettingToOriginReference ? 3 : 1));
|
||||
var progressFactor = 1.0 / numSteps;
|
||||
|
||||
JobProgressReporter NextProgressReporter(string? stage)
|
||||
{
|
||||
return progressReporter.CreateSection(stage, progressFactor);
|
||||
}
|
||||
JobProgressReporter NextProgressReporter(string? stage) => progressReporter.CreateSection(stage, progressFactor);
|
||||
|
||||
progressReporter.ReportProgress(0);
|
||||
|
||||
@@ -253,29 +243,35 @@ namespace Tgstation.Server.Host.Components.Repository
|
||||
{
|
||||
if (!repo.Tracking)
|
||||
throw new JobException(ErrorCode.RepoReferenceRequired);
|
||||
await repo.FetchOrigin(
|
||||
NextProgressReporter("Fetch Origin"),
|
||||
currentModel.AccessUser,
|
||||
currentModel.AccessToken,
|
||||
false,
|
||||
cancellationToken);
|
||||
using (var fetchReporter = NextProgressReporter("Fetch Origin"))
|
||||
await repo.FetchOrigin(
|
||||
fetchReporter,
|
||||
currentModel.AccessUser,
|
||||
currentModel.AccessToken,
|
||||
false,
|
||||
cancellationToken);
|
||||
|
||||
if (!modelHasShaOrReference)
|
||||
{
|
||||
var fastForward = await repo.MergeOrigin(
|
||||
NextProgressReporter("Merge Origin"),
|
||||
committerName,
|
||||
currentModel.CommitterEmail!,
|
||||
false,
|
||||
cancellationToken);
|
||||
bool? fastForward;
|
||||
using (var mergeReporter = NextProgressReporter("Merge Origin"))
|
||||
fastForward = await repo.MergeOrigin(
|
||||
mergeReporter,
|
||||
committerName,
|
||||
currentModel.CommitterEmail!,
|
||||
false,
|
||||
cancellationToken);
|
||||
|
||||
if (!fastForward.HasValue)
|
||||
throw new JobException(ErrorCode.RepoMergeConflict);
|
||||
|
||||
lastRevisionInfo!.OriginCommitSha = await repo.GetOriginSha(cancellationToken);
|
||||
await UpdateRevInfo();
|
||||
if (fastForward.Value)
|
||||
{
|
||||
using var syncReporter = NextProgressReporter("Sychronize");
|
||||
await repo.Synchronize(
|
||||
NextProgressReporter("Sychronize"),
|
||||
syncReporter,
|
||||
currentModel.AccessUser,
|
||||
currentModel.AccessToken,
|
||||
currentModel.CommitterName!,
|
||||
@@ -286,7 +282,7 @@ namespace Tgstation.Server.Host.Components.Repository
|
||||
postUpdateSha = repo.Head;
|
||||
}
|
||||
else
|
||||
NextProgressReporter(null).ReportProgress(1.0);
|
||||
NextProgressReporter(null).Dispose();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -310,38 +306,44 @@ namespace Tgstation.Server.Host.Components.Repository
|
||||
if ((isSha && model.Reference != null) || (!isSha && model.CheckoutSha != null))
|
||||
throw new JobException(ErrorCode.RepoSwappedShaOrReference);
|
||||
|
||||
await repo.CheckoutObject(
|
||||
committish,
|
||||
currentModel.AccessUser,
|
||||
currentModel.AccessToken,
|
||||
updateSubmodules,
|
||||
NextProgressReporter("Checkout"),
|
||||
cancellationToken);
|
||||
using (var checkoutReporter = NextProgressReporter("Checkout"))
|
||||
await repo.CheckoutObject(
|
||||
committish,
|
||||
currentModel.AccessUser,
|
||||
currentModel.AccessToken,
|
||||
updateSubmodules,
|
||||
false,
|
||||
checkoutReporter,
|
||||
cancellationToken);
|
||||
await CallLoadRevInfo(); // we've either seen origin before or what we're checking out is on origin
|
||||
}
|
||||
else
|
||||
NextProgressReporter(null).ReportProgress(1.0);
|
||||
NextProgressReporter(null).Dispose();
|
||||
|
||||
if (hardResettingToOriginReference)
|
||||
{
|
||||
if (!repo.Tracking)
|
||||
throw new JobException(ErrorCode.RepoReferenceNotTracking);
|
||||
await repo.ResetToOrigin(
|
||||
NextProgressReporter("Reset to Origin"),
|
||||
currentModel.AccessUser,
|
||||
currentModel.AccessToken,
|
||||
updateSubmodules,
|
||||
false,
|
||||
cancellationToken);
|
||||
await repo.Synchronize(
|
||||
NextProgressReporter("Synchronize"),
|
||||
currentModel.AccessUser,
|
||||
currentModel.AccessToken,
|
||||
currentModel.CommitterName!,
|
||||
currentModel.CommitterEmail!,
|
||||
true,
|
||||
false,
|
||||
cancellationToken);
|
||||
using (var resetReporter = NextProgressReporter("Reset to Origin"))
|
||||
await repo.ResetToOrigin(
|
||||
resetReporter,
|
||||
currentModel.AccessUser,
|
||||
currentModel.AccessToken,
|
||||
updateSubmodules,
|
||||
false,
|
||||
cancellationToken);
|
||||
|
||||
using (var syncReporter = NextProgressReporter("Synchronize"))
|
||||
await repo.Synchronize(
|
||||
syncReporter,
|
||||
currentModel.AccessUser,
|
||||
currentModel.AccessToken,
|
||||
currentModel.CommitterName!,
|
||||
currentModel.CommitterEmail!,
|
||||
true,
|
||||
false,
|
||||
cancellationToken);
|
||||
|
||||
await CallLoadRevInfo();
|
||||
|
||||
// repo head is on origin so force this
|
||||
@@ -492,7 +494,8 @@ namespace Tgstation.Server.Host.Components.Repository
|
||||
// goteem
|
||||
var commitSha = revInfoWereLookingFor.CommitSha!;
|
||||
logger.LogDebug("Reusing existing SHA {sha}...", commitSha);
|
||||
await repo.ResetToSha(commitSha, NextProgressReporter($"Reset to {commitSha[..7]}"), cancellationToken);
|
||||
using var resetReporter = NextProgressReporter($"Reset to {commitSha[..7]}");
|
||||
await repo.ResetToSha(commitSha, resetReporter, cancellationToken);
|
||||
lastRevisionInfo = revInfoWereLookingFor;
|
||||
}
|
||||
|
||||
@@ -505,15 +508,17 @@ namespace Tgstation.Server.Host.Components.Repository
|
||||
|
||||
var fullTestMergeTask = repo.GetTestMerge(newTestMerge, currentModel, cancellationToken);
|
||||
|
||||
var mergeResult = await repo.AddTestMerge(
|
||||
newTestMerge,
|
||||
committerName,
|
||||
currentModel.CommitterEmail!,
|
||||
currentModel.AccessUser,
|
||||
currentModel.AccessToken,
|
||||
updateSubmodules,
|
||||
NextProgressReporter($"Test merge #{newTestMerge.Number}"),
|
||||
cancellationToken);
|
||||
TestMergeResult mergeResult;
|
||||
using (var testMergeReporter = NextProgressReporter($"Test merge #{newTestMerge.Number}"))
|
||||
mergeResult = await repo.AddTestMerge(
|
||||
newTestMerge,
|
||||
committerName,
|
||||
currentModel.CommitterEmail!,
|
||||
currentModel.AccessUser,
|
||||
currentModel.AccessToken,
|
||||
updateSubmodules,
|
||||
testMergeReporter,
|
||||
cancellationToken);
|
||||
|
||||
if (mergeResult.Status == MergeStatus.Conflicts)
|
||||
throw new JobException(
|
||||
@@ -552,15 +557,17 @@ namespace Tgstation.Server.Host.Components.Repository
|
||||
var currentHead = repo.Head;
|
||||
if (currentModel.PushTestMergeCommits!.Value && (startSha != currentHead || (postUpdateSha != null && postUpdateSha != currentHead)))
|
||||
{
|
||||
await repo.Synchronize(
|
||||
NextProgressReporter("Synchronize"),
|
||||
currentModel.AccessUser,
|
||||
currentModel.AccessToken,
|
||||
currentModel.CommitterName!,
|
||||
currentModel.CommitterEmail!,
|
||||
false,
|
||||
false,
|
||||
cancellationToken);
|
||||
using (var syncReporter = NextProgressReporter("Synchronize"))
|
||||
await repo.Synchronize(
|
||||
syncReporter,
|
||||
currentModel.AccessUser,
|
||||
currentModel.AccessToken,
|
||||
currentModel.CommitterName!,
|
||||
currentModel.CommitterEmail!,
|
||||
false,
|
||||
false,
|
||||
cancellationToken);
|
||||
|
||||
await UpdateRevInfo();
|
||||
}
|
||||
}
|
||||
@@ -574,19 +581,109 @@ namespace Tgstation.Server.Host.Components.Repository
|
||||
var secondStep = startReference != null && repo.Head != startSha;
|
||||
|
||||
// DCTx2: Cancellation token is for job, operations should always run
|
||||
await repo.CheckoutObject(
|
||||
startReference ?? startSha,
|
||||
currentModel.AccessUser,
|
||||
currentModel.AccessToken,
|
||||
true,
|
||||
progressReporter.CreateSection($"Checkout {startReference ?? startSha[..7]}", secondStep ? 0.5 : 1.0),
|
||||
default);
|
||||
using (var checkoutReporter = progressReporter.CreateSection($"Checkout {startReference ?? startSha[..7]}", secondStep ? 0.5 : 1.0))
|
||||
await repo.CheckoutObject(
|
||||
startReference ?? startSha,
|
||||
currentModel.AccessUser,
|
||||
currentModel.AccessToken,
|
||||
true,
|
||||
false,
|
||||
checkoutReporter,
|
||||
default);
|
||||
|
||||
if (secondStep)
|
||||
await repo.ResetToSha(startSha, progressReporter.CreateSection($"Hard reset to SHA {startSha[..7]}", 0.5), default);
|
||||
using (var resetReporter = progressReporter.CreateSection($"Hard reset to SHA {startSha[..7]}", 0.5))
|
||||
await repo.ResetToSha(startSha, resetReporter, default);
|
||||
|
||||
throw;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// The job entrypoint used by <see cref="Controllers.RepositoryController"/> to reclone a repository.
|
||||
/// </summary>
|
||||
/// <param name="instance">The <see cref="IInstanceCore"/> the job is running on.</param>
|
||||
/// <param name="databaseContextFactory">The <see cref="IDatabaseContextFactory"/> for the operation.</param>
|
||||
/// <param name="progressReporter">The <see cref="JobProgressReporter"/> for the job.</param>
|
||||
/// <param name="cancellationToken">The <see cref="CancellationToken"/> for the operation.</param>
|
||||
/// <returns>A <see cref="ValueTask"/> representing the running operation.</returns>
|
||||
public async ValueTask RepositoryRecloneJob(
|
||||
IInstanceCore? instance,
|
||||
IDatabaseContextFactory databaseContextFactory,
|
||||
JobProgressReporter progressReporter,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(instance);
|
||||
ArgumentNullException.ThrowIfNull(databaseContextFactory);
|
||||
ArgumentNullException.ThrowIfNull(progressReporter);
|
||||
|
||||
progressReporter.StageName = "Loading Old Repository";
|
||||
|
||||
Uri origin;
|
||||
string? oldReference;
|
||||
string oldSha;
|
||||
ValueTask deleteTask;
|
||||
using (var deleteReporter = progressReporter.CreateSection("Deleting Old Repository", 0.1))
|
||||
{
|
||||
using (var oldRepo = await instance.RepositoryManager.LoadRepository(cancellationToken))
|
||||
{
|
||||
if (oldRepo == null)
|
||||
throw new JobException(ErrorCode.RepoMissing);
|
||||
|
||||
origin = oldRepo.Origin;
|
||||
oldSha = oldRepo.Head;
|
||||
oldReference = oldRepo.Reference;
|
||||
if (oldReference == Repository.NoReference)
|
||||
oldReference = null;
|
||||
|
||||
deleteTask = instance.RepositoryManager.DeleteRepository(cancellationToken);
|
||||
}
|
||||
|
||||
await deleteTask;
|
||||
}
|
||||
|
||||
IRepository newRepo;
|
||||
try
|
||||
{
|
||||
using var cloneReporter = progressReporter.CreateSection("Cloning New Repository", 0.8);
|
||||
newRepo = await instance.RepositoryManager.CloneRepository(
|
||||
origin,
|
||||
oldReference,
|
||||
currentModel.AccessUser,
|
||||
currentModel.AccessToken,
|
||||
cloneReporter,
|
||||
true, // TODO: Make configurable maybe...
|
||||
cancellationToken)
|
||||
?? throw new JobException("A race condition occurred while recloning the repository. Somehow, it was fully cloned instantly after being deleted!"); // I'll take lines of code that should never be hit for $10k
|
||||
}
|
||||
catch (Exception ex) when (ex is not JobException)
|
||||
{
|
||||
logger.LogWarning("Reclone failed, clearing credentials!");
|
||||
|
||||
// need to clear credentials here
|
||||
await databaseContextFactory.UseContextTaskReturn(context =>
|
||||
{
|
||||
context.RepositorySettings.Attach(currentModel);
|
||||
currentModel.AccessUser = null;
|
||||
currentModel.AccessToken = null;
|
||||
return context.Save(CancellationToken.None); // DCT: Must always run
|
||||
});
|
||||
|
||||
throw;
|
||||
}
|
||||
|
||||
using (newRepo)
|
||||
using (var checkoutReporter = progressReporter.CreateSection("Checking out previous Detached Commit", 0.1))
|
||||
{
|
||||
await newRepo.CheckoutObject(
|
||||
oldSha,
|
||||
currentModel.AccessUser,
|
||||
currentModel.AccessToken,
|
||||
false,
|
||||
oldReference != null,
|
||||
checkoutReporter,
|
||||
cancellationToken);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -121,6 +121,9 @@ namespace Tgstation.Server.Host.Components.Session
|
||||
/// </summary>
|
||||
public FifoSemaphore TopicSendSemaphore { get; }
|
||||
|
||||
/// <inheritdoc />
|
||||
public long? MemoryUsage => process.MemoryUsage;
|
||||
|
||||
/// <summary>
|
||||
/// The <see cref="Byond.TopicSender.ITopicClient"/> for the <see cref="SessionController"/>.
|
||||
/// </summary>
|
||||
|
||||
@@ -24,6 +24,11 @@ namespace Tgstation.Server.Host.Components.Watchdog
|
||||
/// </summary>
|
||||
WatchdogStatus Status { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the memory usage of the game server in bytes.
|
||||
/// </summary>
|
||||
long? MemoryUsage { get; }
|
||||
|
||||
/// <summary>
|
||||
/// If the alpha server is the active server.
|
||||
/// </summary>
|
||||
|
||||
@@ -50,6 +50,9 @@ namespace Tgstation.Server.Host.Components.Watchdog
|
||||
}
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public long? MemoryUsage => GetActiveController()?.MemoryUsage;
|
||||
|
||||
/// <inheritdoc />
|
||||
public abstract bool AlphaIsActive { get; }
|
||||
|
||||
|
||||
@@ -89,7 +89,7 @@ namespace Tgstation.Server.Host.Configuration
|
||||
public ushort ApiPort { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// A GitHub personal access token to use for bypassing rate limits on requests. Requires no scopes.
|
||||
/// A classic GitHub personal access token to use for bypassing rate limits on requests. Requires no scopes.
|
||||
/// </summary>
|
||||
public string? GitHubAccessToken { get; set; }
|
||||
|
||||
|
||||
@@ -29,5 +29,10 @@
|
||||
/// If <see langword="true"/>, deployments that fail will not be immediately cleaned up. They will be cleaned up the next time the instance is onlined.
|
||||
/// </summary>
|
||||
public bool DelayCleaningFailedDeployments { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// If set dd.exe will not be used on Windows systems in versions where it is present. Instead dreamdaemon.exe will always be used.
|
||||
/// </summary>
|
||||
public bool ForceUseDreamDaemonExe { get; set; }
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,33 @@
|
||||
namespace Tgstation.Server.Host.Configuration
|
||||
{
|
||||
/// <summary>
|
||||
/// Configuration options for telemetry.
|
||||
/// </summary>
|
||||
public sealed class TelemetryConfiguration
|
||||
{
|
||||
/// <summary>
|
||||
/// The key for the <see cref="Microsoft.Extensions.Configuration.IConfigurationSection"/> the <see cref="TelemetryConfiguration"/> resides in.
|
||||
/// </summary>
|
||||
public const string Section = "Telemetry";
|
||||
|
||||
/// <summary>
|
||||
/// The default value of <see cref="VersionReportingRepositoryId"/>.
|
||||
/// </summary>
|
||||
private const long DefaultVersionReportingRepositoryId = 841149827; // https://github.com/tgstation/tgstation-server-deployments
|
||||
|
||||
/// <summary>
|
||||
/// If version reporting telemetry is disabled.
|
||||
/// </summary>
|
||||
public bool DisableVersionReporting { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The friendly name used on GitHub deployments for version reporting. If <see langword="null"/> only the server <see cref="global::System.Guid"/> will be shown.
|
||||
/// </summary>
|
||||
public string? ServerFriendlyName { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The GitHub repository ID used for version reporting.
|
||||
/// </summary>
|
||||
public long? VersionReportingRepositoryId { get; set; } = DefaultVersionReportingRepositoryId;
|
||||
}
|
||||
}
|
||||
@@ -7,6 +7,7 @@ using System.Threading.Tasks;
|
||||
using System.Web;
|
||||
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.Extensions.Caching.Memory;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Microsoft.Extensions.Options;
|
||||
|
||||
@@ -42,6 +43,11 @@ namespace Tgstation.Server.Host.Controllers
|
||||
/// </summary>
|
||||
const string OctokitException = "Bad GitHub API response, check configuration!";
|
||||
|
||||
/// <summary>
|
||||
/// The <see cref="IMemoryCache"/> key for <see cref="Read(bool?, CancellationToken)"/>.
|
||||
/// </summary>
|
||||
static readonly object ReadCacheKey = new();
|
||||
|
||||
/// <summary>
|
||||
/// The <see cref="IGitHubServiceFactory"/> for the <see cref="AdministrationController"/>.
|
||||
/// </summary>
|
||||
@@ -77,6 +83,11 @@ namespace Tgstation.Server.Host.Controllers
|
||||
/// </summary>
|
||||
readonly IFileTransferTicketProvider fileTransferService;
|
||||
|
||||
/// <summary>
|
||||
/// The <see cref="IMemoryCache"/> for the <see cref="AdministrationController"/>.
|
||||
/// </summary>
|
||||
readonly IMemoryCache cacheService;
|
||||
|
||||
/// <summary>
|
||||
/// The <see cref="FileLoggingConfiguration"/> for the <see cref="AdministrationController"/>.
|
||||
/// </summary>
|
||||
@@ -94,6 +105,7 @@ namespace Tgstation.Server.Host.Controllers
|
||||
/// <param name="ioManager">The value of <see cref="ioManager"/>.</param>
|
||||
/// <param name="platformIdentifier">The value of <see cref="platformIdentifier"/>.</param>
|
||||
/// <param name="fileTransferService">The value of <see cref="fileTransferService"/>.</param>
|
||||
/// <param name="cacheService">The value of <see cref="cacheService"/>.</param>
|
||||
/// <param name="logger">The <see cref="ILogger"/> for the <see cref="ApiController"/>.</param>
|
||||
/// <param name="fileLoggingConfigurationOptions">The <see cref="IOptions{TOptions}"/> containing value of <see cref="fileLoggingConfiguration"/>.</param>
|
||||
/// <param name="apiHeadersProvider">The <see cref="IApiHeadersProvider"/> for the <see cref="ApiController"/>.</param>
|
||||
@@ -107,6 +119,7 @@ namespace Tgstation.Server.Host.Controllers
|
||||
IIOManager ioManager,
|
||||
IPlatformIdentifier platformIdentifier,
|
||||
IFileTransferTicketProvider fileTransferService,
|
||||
IMemoryCache cacheService,
|
||||
ILogger<AdministrationController> logger,
|
||||
IOptions<FileLoggingConfiguration> fileLoggingConfigurationOptions,
|
||||
IApiHeadersProvider apiHeadersProvider)
|
||||
@@ -124,12 +137,14 @@ namespace Tgstation.Server.Host.Controllers
|
||||
this.ioManager = ioManager ?? throw new ArgumentNullException(nameof(ioManager));
|
||||
this.platformIdentifier = platformIdentifier ?? throw new ArgumentNullException(nameof(platformIdentifier));
|
||||
this.fileTransferService = fileTransferService ?? throw new ArgumentNullException(nameof(fileTransferService));
|
||||
this.cacheService = cacheService ?? throw new ArgumentNullException(nameof(cacheService));
|
||||
fileLoggingConfiguration = fileLoggingConfigurationOptions?.Value ?? throw new ArgumentNullException(nameof(fileLoggingConfigurationOptions));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Get <see cref="AdministrationResponse"/> server information.
|
||||
/// </summary>
|
||||
/// <param name="fresh">If <see langword="true"/>, the cache should be bypassed.</param>
|
||||
/// <param name="cancellationToken">The <see cref="CancellationToken"/> for the operation.</param>
|
||||
/// <returns>A <see cref="ValueTask{TResult}"/> resulting in the <see cref="IActionResult"/> for the operation.</returns>
|
||||
/// <response code="200">Retrieved <see cref="AdministrationResponse"/> data successfully.</response>
|
||||
@@ -140,39 +155,56 @@ namespace Tgstation.Server.Host.Controllers
|
||||
[ProducesResponseType(typeof(AdministrationResponse), 200)]
|
||||
[ProducesResponseType(typeof(ErrorMessageResponse), 424)]
|
||||
[ProducesResponseType(typeof(ErrorMessageResponse), 429)]
|
||||
public async ValueTask<IActionResult> Read(CancellationToken cancellationToken)
|
||||
public async ValueTask<IActionResult> Read([FromQuery] bool? fresh, CancellationToken cancellationToken)
|
||||
{
|
||||
try
|
||||
{
|
||||
Version? greatestVersion = null;
|
||||
Uri? repoUrl = null;
|
||||
try
|
||||
async Task<JsonResult> CacheFactory()
|
||||
{
|
||||
var gitHubService = gitHubServiceFactory.CreateService();
|
||||
var repositoryUrlTask = gitHubService.GetUpdatesRepositoryUrl(cancellationToken);
|
||||
var releases = await gitHubService.GetTgsReleases(cancellationToken);
|
||||
|
||||
foreach (var kvp in releases)
|
||||
Version? greatestVersion = null;
|
||||
Uri? repoUrl = null;
|
||||
try
|
||||
{
|
||||
var version = kvp.Key;
|
||||
var release = kvp.Value;
|
||||
if (version.Major > 3 // Forward/backward compatible but not before TGS4
|
||||
&& (greatestVersion == null || version > greatestVersion))
|
||||
greatestVersion = version;
|
||||
var gitHubService = await gitHubServiceFactory.CreateService(cancellationToken);
|
||||
var repositoryUrlTask = gitHubService.GetUpdatesRepositoryUrl(cancellationToken);
|
||||
var releases = await gitHubService.GetTgsReleases(cancellationToken);
|
||||
|
||||
foreach (var kvp in releases)
|
||||
{
|
||||
var version = kvp.Key;
|
||||
var release = kvp.Value;
|
||||
if (version.Major > 3 // Forward/backward compatible but not before TGS4
|
||||
&& (greatestVersion == null || version > greatestVersion))
|
||||
greatestVersion = version;
|
||||
}
|
||||
|
||||
repoUrl = await repositoryUrlTask;
|
||||
}
|
||||
catch (NotFoundException e)
|
||||
{
|
||||
Logger.LogWarning(e, "Not found exception while retrieving upstream repository info!");
|
||||
}
|
||||
|
||||
repoUrl = await repositoryUrlTask;
|
||||
}
|
||||
catch (NotFoundException e)
|
||||
{
|
||||
Logger.LogWarning(e, "Not found exception while retrieving upstream repository info!");
|
||||
return Json(new AdministrationResponse
|
||||
{
|
||||
LatestVersion = greatestVersion,
|
||||
TrackedRepositoryUrl = repoUrl,
|
||||
GeneratedAt = DateTimeOffset.UtcNow,
|
||||
});
|
||||
}
|
||||
|
||||
return Json(new AdministrationResponse
|
||||
var ttl = TimeSpan.FromMinutes(30);
|
||||
Task<JsonResult> task;
|
||||
if (fresh == true || !cacheService.TryGetValue(ReadCacheKey, out var rawCacheObject))
|
||||
{
|
||||
LatestVersion = greatestVersion,
|
||||
TrackedRepositoryUrl = repoUrl,
|
||||
});
|
||||
using var entry = cacheService.CreateEntry(ReadCacheKey);
|
||||
entry.AbsoluteExpirationRelativeToNow = ttl;
|
||||
entry.Value = task = CacheFactory();
|
||||
}
|
||||
else
|
||||
task = (Task<JsonResult>)rawCacheObject!;
|
||||
|
||||
return await task;
|
||||
}
|
||||
catch (RateLimitExceededException e)
|
||||
{
|
||||
|
||||
@@ -317,11 +317,12 @@ namespace Tgstation.Server.Host.Controllers
|
||||
/// <param name="knownForcedReboot">If there was a settings change made that forced a switch to <see cref="RebootState.Restart"/>.</param>
|
||||
/// <param name="cancellationToken">The <see cref="CancellationToken"/> for the operation.</param>
|
||||
/// <returns>A <see cref="ValueTask{TResult}"/> resulting in the <see cref="IActionResult"/> of the operation.</returns>
|
||||
#pragma warning disable CA1502 // TODO: Decomplexify
|
||||
ValueTask<IActionResult> ReadImpl(DreamDaemonSettings? settings, bool knownForcedReboot, CancellationToken cancellationToken)
|
||||
#pragma warning restore CA1502
|
||||
=> WithComponentInstance(async instance =>
|
||||
{
|
||||
var dd = instance.Watchdog;
|
||||
|
||||
var metadata = (AuthenticationContext.GetRight(RightsType.DreamDaemon) & (ulong)DreamDaemonRights.ReadMetadata) != 0;
|
||||
var revision = (AuthenticationContext.GetRight(RightsType.DreamDaemon) & (ulong)DreamDaemonRights.ReadRevision) != 0;
|
||||
|
||||
@@ -372,6 +373,7 @@ namespace Tgstation.Server.Host.Controllers
|
||||
result.Visibility = settings.Visibility!.Value;
|
||||
result.SoftRestart = rstate == RebootState.Restart;
|
||||
result.SoftShutdown = rstate == RebootState.Shutdown;
|
||||
result.ImmediateMemoryUsage = dd.MemoryUsage;
|
||||
|
||||
if (rstate == RebootState.Normal && knownForcedReboot)
|
||||
result.SoftRestart = true;
|
||||
|
||||
@@ -184,7 +184,8 @@ namespace Tgstation.Server.Host.Controllers
|
||||
|
||||
try
|
||||
{
|
||||
await byondManager.ChangeVersion(null, model.EngineVersion, null, false, cancellationToken);
|
||||
using var progressReporter = new JobProgressReporter();
|
||||
await byondManager.ChangeVersion(progressReporter, model.EngineVersion, null, false, cancellationToken);
|
||||
}
|
||||
catch (InvalidOperationException ex)
|
||||
{
|
||||
|
||||
@@ -223,6 +223,42 @@ namespace Tgstation.Server.Host.Controllers
|
||||
return Accepted(api);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Delete the repository.
|
||||
/// </summary>
|
||||
/// <param name="cancellationToken">The <see cref="CancellationToken"/> for the operation.</param>
|
||||
/// <returns>A <see cref="ValueTask{TResult}"/> resulting in the <see cref="IActionResult"/> of the operation.</returns>
|
||||
/// <response code="202">Job to delete the repository created successfully.</response>
|
||||
/// <response code="410">The database entity for the requested instance could not be retrieved. The instance was likely detached.</response>
|
||||
[HttpPatch]
|
||||
[TgsAuthorize(RepositoryRights.Reclone)]
|
||||
[ProducesResponseType(typeof(RepositoryResponse), 202)]
|
||||
[ProducesResponseType(typeof(ErrorMessageResponse), 410)]
|
||||
public async ValueTask<IActionResult> Reclone(CancellationToken cancellationToken)
|
||||
{
|
||||
var currentModel = await DatabaseContext
|
||||
.RepositorySettings
|
||||
.AsQueryable()
|
||||
.Where(x => x.InstanceId == Instance.Id)
|
||||
.FirstOrDefaultAsync(cancellationToken);
|
||||
|
||||
if (currentModel == default)
|
||||
return this.Gone();
|
||||
|
||||
Logger.LogInformation("Instance {instanceId} repository reclone initiated by user {userId}", Instance.Id, AuthenticationContext.User.Require(x => x.Id));
|
||||
|
||||
var repositoryUpdater = CreateRepositoryUpdateService(currentModel);
|
||||
|
||||
var job = Job.Create(JobCode.RepositoryReclone, AuthenticationContext.User, Instance);
|
||||
var api = currentModel.ToApi();
|
||||
await jobManager.RegisterOperation(
|
||||
job,
|
||||
(core, databaseContextFactory, paramJob, progressReporter, ct) => repositoryUpdater.RepositoryRecloneJob(core, databaseContextFactory, progressReporter, ct),
|
||||
cancellationToken);
|
||||
api.ActiveJob = job.ToApi();
|
||||
return Accepted(api);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Get the repository's status.
|
||||
/// </summary>
|
||||
@@ -443,17 +479,12 @@ namespace Tgstation.Server.Host.Controllers
|
||||
var job = Job.Create(JobCode.RepositoryUpdate, AuthenticationContext.User, Instance, RepositoryRights.CancelPendingChanges);
|
||||
job.Description = description;
|
||||
|
||||
var repositoryUpdater = new RepositoryUpdateService(
|
||||
model,
|
||||
currentModel,
|
||||
AuthenticationContext.User,
|
||||
loggerFactory.CreateLogger<RepositoryUpdateService>(),
|
||||
Instance.Require(x => x.Id));
|
||||
var repositoryUpdater = CreateRepositoryUpdateService(currentModel);
|
||||
|
||||
// Time to access git, do it in a job
|
||||
await jobManager.RegisterOperation(
|
||||
job,
|
||||
repositoryUpdater.RepositoryUpdateJob,
|
||||
(instance, databaseContextFactory, _, progressReporter, jobToken) => repositoryUpdater.RepositoryUpdateJob(model, instance, databaseContextFactory, progressReporter, jobToken),
|
||||
cancellationToken);
|
||||
|
||||
api.ActiveJob = job.ToApi();
|
||||
@@ -494,5 +525,17 @@ namespace Tgstation.Server.Host.Controllers
|
||||
cancellationToken);
|
||||
return needsDbUpdate;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Creates a <see cref="RepositoryUpdateService"/>.
|
||||
/// </summary>
|
||||
/// <param name="currentModel">The current <see cref="RepositorySettings"/>.</param>
|
||||
/// <returns>A new <see cref="RepositoryUpdateService"/>.</returns>
|
||||
RepositoryUpdateService CreateRepositoryUpdateService(RepositorySettings currentModel)
|
||||
=> new(
|
||||
currentModel,
|
||||
AuthenticationContext.User,
|
||||
loggerFactory.CreateLogger<RepositoryUpdateService>(),
|
||||
Instance.Require(x => x.Id));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -152,11 +152,17 @@ namespace Tgstation.Server.Host.Controllers
|
||||
[HttpGet("logo.svg")]
|
||||
public IActionResult GetLogo()
|
||||
{
|
||||
var logoFileName = platformIdentifier.IsWindows // these are different because of motherfucking line endings -_-
|
||||
? LogoSvgWindowsName
|
||||
: LogoSvgLinuxName;
|
||||
// these are different because of motherfucking line endings -_-
|
||||
if (platformIdentifier.IsWindows)
|
||||
{
|
||||
VirtualFileResult? result = this.TryServeFile(hostEnvironment, logger, $"{LogoSvgWindowsName}.svg");
|
||||
if (result != null)
|
||||
return result;
|
||||
|
||||
return (IActionResult?)this.TryServeFile(hostEnvironment, logger, $"{logoFileName}.svg") ?? NotFound();
|
||||
// BUT THE UPDATE PACKAGES ARE BUILT ON LINUX RAAAAAGH
|
||||
}
|
||||
|
||||
return (IActionResult?)this.TryServeFile(hostEnvironment, logger, $"{LogoSvgLinuxName}.svg") ?? NotFound();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
||||
@@ -143,6 +143,7 @@ namespace Tgstation.Server.Host.Core
|
||||
services.UseStandardConfig<ControlPanelConfiguration>(Configuration);
|
||||
services.UseStandardConfig<SwarmConfiguration>(Configuration);
|
||||
services.UseStandardConfig<SessionConfiguration>(Configuration);
|
||||
services.UseStandardConfig<TelemetryConfiguration>(Configuration);
|
||||
|
||||
// enable options which give us config reloading
|
||||
services.AddOptions();
|
||||
@@ -423,6 +424,7 @@ namespace Tgstation.Server.Host.Core
|
||||
services.AddSingleton<IServerPortProvider, ServerPortProivder>();
|
||||
services.AddSingleton<ITopicClientFactory, TopicClientFactory>();
|
||||
services.AddHostedService<CommandPipeManager>();
|
||||
services.AddHostedService<VersionReportingService>();
|
||||
|
||||
services.AddFileDownloader();
|
||||
services.AddGitHub();
|
||||
|
||||
@@ -288,7 +288,7 @@ namespace Tgstation.Server.Host.Core
|
||||
{
|
||||
logger.LogDebug("Looking for GitHub releases version {version}...", newVersion);
|
||||
|
||||
var gitHubService = gitHubServiceFactory.CreateService();
|
||||
var gitHubService = await gitHubServiceFactory.CreateService(cancellationToken);
|
||||
var releases = await gitHubService.GetTgsReleases(cancellationToken);
|
||||
foreach (var kvp in releases)
|
||||
{
|
||||
|
||||
@@ -0,0 +1,269 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Text;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
using Microsoft.Extensions.Hosting;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Microsoft.Extensions.Options;
|
||||
|
||||
using Octokit;
|
||||
|
||||
using Tgstation.Server.Common.Extensions;
|
||||
using Tgstation.Server.Host.Configuration;
|
||||
using Tgstation.Server.Host.Extensions;
|
||||
using Tgstation.Server.Host.IO;
|
||||
using Tgstation.Server.Host.Properties;
|
||||
using Tgstation.Server.Host.System;
|
||||
using Tgstation.Server.Host.Utils;
|
||||
using Tgstation.Server.Host.Utils.GitHub;
|
||||
|
||||
namespace Tgstation.Server.Host.Core
|
||||
{
|
||||
/// <summary>
|
||||
/// Handles TGS version reporting, if enabled.
|
||||
/// </summary>
|
||||
sealed class VersionReportingService : BackgroundService
|
||||
{
|
||||
/// <summary>
|
||||
/// The <see cref="IGitHubClientFactory"/> for the <see cref="VersionReportingService"/>.
|
||||
/// </summary>
|
||||
readonly IGitHubClientFactory gitHubClientFactory;
|
||||
|
||||
/// <summary>
|
||||
/// The <see cref="IIOManager"/> for the <see cref="VersionReportingService"/>.
|
||||
/// </summary>
|
||||
readonly IIOManager ioManager;
|
||||
|
||||
/// <summary>
|
||||
/// The <see cref="IAsyncDelayer"/> for the <see cref="VersionReportingService"/>.
|
||||
/// </summary>
|
||||
readonly IAsyncDelayer asyncDelayer;
|
||||
|
||||
/// <summary>
|
||||
/// The <see cref="IAssemblyInformationProvider"/> for the <see cref="VersionReportingService"/>.
|
||||
/// </summary>
|
||||
readonly IAssemblyInformationProvider assemblyInformationProvider;
|
||||
|
||||
/// <summary>
|
||||
/// The <see cref="ILogger"/> for the <see cref="VersionReportingService"/>.
|
||||
/// </summary>
|
||||
readonly ILogger<VersionReportingService> logger;
|
||||
|
||||
/// <summary>
|
||||
/// The <see cref="TelemetryConfiguration"/> for the <see cref="VersionReportingService"/>.
|
||||
/// </summary>
|
||||
readonly TelemetryConfiguration telemetryConfiguration;
|
||||
|
||||
/// <summary>
|
||||
/// The <see cref="CancellationToken"/> passed to <see cref="StopAsync(CancellationToken)"/>.
|
||||
/// </summary>
|
||||
CancellationToken shutdownCancellationToken;
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="VersionReportingService"/> class.
|
||||
/// </summary>
|
||||
/// <param name="gitHubClientFactory">The value of <see cref="gitHubClientFactory"/>.</param>
|
||||
/// <param name="ioManager">The value of <see cref="ioManager"/>.</param>
|
||||
/// <param name="asyncDelayer">The value of <see cref="asyncDelayer"/>.</param>
|
||||
/// <param name="assemblyInformationProvider">The value of <see cref="assemblyInformationProvider"/>.</param>
|
||||
/// <param name="telemetryConfigurationOptions">The <see cref="IOptions{TOptions}"/> containing the value of <see cref="telemetryConfiguration"/>.</param>
|
||||
/// <param name="logger">The value of <see cref="logger"/>.</param>
|
||||
public VersionReportingService(
|
||||
IGitHubClientFactory gitHubClientFactory,
|
||||
IIOManager ioManager,
|
||||
IAsyncDelayer asyncDelayer,
|
||||
IAssemblyInformationProvider assemblyInformationProvider,
|
||||
IOptions<TelemetryConfiguration> telemetryConfigurationOptions,
|
||||
ILogger<VersionReportingService> logger)
|
||||
{
|
||||
this.gitHubClientFactory = gitHubClientFactory ?? throw new ArgumentNullException(nameof(gitHubClientFactory));
|
||||
this.ioManager = ioManager ?? throw new ArgumentNullException(nameof(ioManager));
|
||||
this.asyncDelayer = asyncDelayer ?? throw new ArgumentNullException(nameof(asyncDelayer));
|
||||
this.assemblyInformationProvider = assemblyInformationProvider ?? throw new ArgumentNullException(nameof(assemblyInformationProvider));
|
||||
telemetryConfiguration = telemetryConfigurationOptions?.Value ?? throw new ArgumentNullException(nameof(telemetryConfigurationOptions));
|
||||
this.logger = logger ?? throw new ArgumentNullException(nameof(logger));
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public override Task StopAsync(CancellationToken cancellationToken)
|
||||
{
|
||||
shutdownCancellationToken = cancellationToken;
|
||||
return base.StopAsync(cancellationToken);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override async Task ExecuteAsync(CancellationToken stoppingToken)
|
||||
{
|
||||
if (telemetryConfiguration.DisableVersionReporting)
|
||||
{
|
||||
logger.LogDebug("Version telemetry disabled");
|
||||
return;
|
||||
}
|
||||
|
||||
if (!telemetryConfiguration.VersionReportingRepositoryId.HasValue)
|
||||
{
|
||||
logger.LogError("Version reporting repository is misconfigured. Telemetry cannot be sent!");
|
||||
return;
|
||||
}
|
||||
|
||||
var attribute = TelemetryAppSerializedKeyAttribute.Instance;
|
||||
if (attribute == null)
|
||||
{
|
||||
logger.LogDebug("TGS build configuration does not allow for version telemetry");
|
||||
return;
|
||||
}
|
||||
|
||||
logger.LogDebug("Starting...");
|
||||
|
||||
try
|
||||
{
|
||||
var telemetryIdFile = ioManager.ResolvePath(
|
||||
ioManager.ConcatPath(
|
||||
ioManager.GetPathInLocalDirectory(assemblyInformationProvider),
|
||||
"telemetry.id"));
|
||||
|
||||
Guid telemetryId;
|
||||
if (!await ioManager.FileExists(telemetryIdFile, stoppingToken))
|
||||
{
|
||||
telemetryId = Guid.NewGuid();
|
||||
await ioManager.WriteAllBytes(telemetryIdFile, Encoding.UTF8.GetBytes(telemetryId.ToString()), stoppingToken);
|
||||
logger.LogInformation("Generated telemetry ID {telemetryId} and wrote to {file}", telemetryId, telemetryIdFile);
|
||||
}
|
||||
else
|
||||
{
|
||||
var contents = await ioManager.ReadAllBytes(telemetryIdFile, stoppingToken);
|
||||
|
||||
string guidStr;
|
||||
try
|
||||
{
|
||||
guidStr = Encoding.UTF8.GetString(contents);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
logger.LogError(ex, "Cannot decode telemetry ID from installation file ({path}). Telemetry will not be sent!", telemetryIdFile);
|
||||
return;
|
||||
}
|
||||
|
||||
if (!Guid.TryParse(guidStr, out telemetryId))
|
||||
{
|
||||
logger.LogError("Cannot parse telemetry ID from installation file ({path}). Telemetry will not be sent!", telemetryIdFile);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
while (!stoppingToken.IsCancellationRequested)
|
||||
{
|
||||
var nextDelayHours = await TryReportVersion(
|
||||
telemetryId,
|
||||
attribute.SerializedKey,
|
||||
telemetryConfiguration.VersionReportingRepositoryId.Value,
|
||||
false,
|
||||
stoppingToken)
|
||||
? 24
|
||||
: 1;
|
||||
|
||||
logger.LogDebug("Next version report in {hours} hours", nextDelayHours);
|
||||
await asyncDelayer.Delay(TimeSpan.FromHours(nextDelayHours), stoppingToken);
|
||||
}
|
||||
}
|
||||
catch (OperationCanceledException ex)
|
||||
{
|
||||
logger.LogTrace(ex, "Inner cancellation");
|
||||
}
|
||||
|
||||
shutdownCancellationToken.ThrowIfCancellationRequested();
|
||||
|
||||
logger.LogDebug("Sending shutdown telemetry");
|
||||
await TryReportVersion(
|
||||
telemetryId,
|
||||
attribute.SerializedKey,
|
||||
telemetryConfiguration.VersionReportingRepositoryId.Value,
|
||||
true,
|
||||
shutdownCancellationToken);
|
||||
}
|
||||
catch (OperationCanceledException ex)
|
||||
{
|
||||
logger.LogTrace(ex, "Exiting due to outer cancellation...");
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
logger.LogError(ex, "Crashed!");
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Make an attempt to report the current <see cref="IAssemblyInformationProvider.Version"/> to the configured GitHub repository.
|
||||
/// </summary>
|
||||
/// <param name="telemetryId">The telemetry <see cref="Guid"/> for the installation.</param>
|
||||
/// <param name="serializedPem">The serialized authentication <see cref="string"/> for the <see cref="gitHubClientFactory"/>.</param>
|
||||
/// <param name="repositoryId">The ID of the repository to send telemetry to.</param>
|
||||
/// <param name="shutdown">If this is shutdown telemetry.</param>
|
||||
/// <param name="cancellationToken">The <see cref="CancellationToken"/> for the operation.</param>
|
||||
/// <returns>A <see cref="ValueTask{TResult}"/> resulting in <see langword="true"/> if telemetry was reported successfully, <see langword="false"/> otherwise.</returns>
|
||||
async ValueTask<bool> TryReportVersion(Guid telemetryId, string serializedPem, long repositoryId, bool shutdown, CancellationToken cancellationToken)
|
||||
{
|
||||
logger.LogDebug("Sending version telemetry...");
|
||||
|
||||
var serverFriendlyName = telemetryConfiguration.ServerFriendlyName;
|
||||
if (String.IsNullOrWhiteSpace(serverFriendlyName))
|
||||
serverFriendlyName = null;
|
||||
|
||||
logger.LogTrace(
|
||||
"Repository ID: {repoId}, Server friendly name: {friendlyName}",
|
||||
repositoryId,
|
||||
serverFriendlyName == null
|
||||
? "(null)"
|
||||
: $"\"{serverFriendlyName}\"");
|
||||
try
|
||||
{
|
||||
var gitHubClient = await gitHubClientFactory.CreateInstallationClient(
|
||||
serializedPem,
|
||||
repositoryId,
|
||||
cancellationToken);
|
||||
|
||||
if (gitHubClient == null)
|
||||
{
|
||||
logger.LogWarning("Could not create GitHub client to connect to repository ID {repoId}!", repositoryId);
|
||||
return false;
|
||||
}
|
||||
|
||||
// remove this lookup once https://github.com/octokit/octokit.net/pull/2960 is merged and released
|
||||
var repository = await gitHubClient.Repository.Get(repositoryId);
|
||||
|
||||
logger.LogTrace("Repository ID {id} resolved to {owner}/{name}", repositoryId, repository.Owner.Name, repository.Name);
|
||||
|
||||
var inputs = new Dictionary<string, object>
|
||||
{
|
||||
{ "telemetry_id", telemetryId.ToString() },
|
||||
{ "tgs_semver", assemblyInformationProvider.Version.Semver().ToString() },
|
||||
{ "shutdown", shutdown ? "true" : "false" },
|
||||
};
|
||||
|
||||
if (serverFriendlyName != null)
|
||||
inputs.Add("server_friendly_name", serverFriendlyName);
|
||||
|
||||
await gitHubClient.Actions.Workflows.CreateDispatch(
|
||||
repository.Owner.Login,
|
||||
repository.Name,
|
||||
".github/workflows/tgs_deployments_telemetry.yml",
|
||||
new CreateWorkflowDispatch("main")
|
||||
{
|
||||
Inputs = inputs,
|
||||
});
|
||||
|
||||
logger.LogTrace("Telemetry sent successfully");
|
||||
|
||||
return true;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
logger.LogWarning(ex, "Failed to report version!");
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -20,14 +20,14 @@ namespace Tgstation.Server.Host.Extensions
|
||||
/// </summary>
|
||||
/// <param name="fetchOptions">The <see cref="FetchOptions"/> to hydrate.</param>
|
||||
/// <param name="logger">The <see cref="ILogger"/> for the operation.</param>
|
||||
/// <param name="progressReporter">The optional <see cref="JobProgressReporter"/>.</param>
|
||||
/// <param name="progressReporter">The <see cref="JobProgressReporter"/>.</param>
|
||||
/// <param name="credentialsHandler">The optional <see cref="CredentialsHandler"/>.</param>
|
||||
/// <param name="cancellationToken">The <see cref="CancellationToken"/> for the operation.</param>
|
||||
/// <returns>The hydrated <paramref name="fetchOptions"/>.</returns>
|
||||
public static FetchOptions Hydrate(
|
||||
this FetchOptions fetchOptions,
|
||||
ILogger logger,
|
||||
JobProgressReporter? progressReporter,
|
||||
JobProgressReporter progressReporter,
|
||||
CredentialsHandler credentialsHandler,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
@@ -60,10 +60,10 @@ namespace Tgstation.Server.Host.Extensions
|
||||
/// Generate a <see cref="LibGit2Sharp.Handlers.TransferProgressHandler"/> from a given <paramref name="progressReporter"/> and <paramref name="cancellationToken"/>.
|
||||
/// </summary>
|
||||
/// <param name="logger">The <see cref="ILogger"/> for the operation.</param>
|
||||
/// <param name="progressReporter">The optional <see cref="JobProgressReporter"/> of the operation.</param>
|
||||
/// <param name="progressReporter">The <see cref="JobProgressReporter"/> of the operation.</param>
|
||||
/// <param name="cancellationToken">The <see cref="CancellationToken"/> for the operation.</param>
|
||||
/// <returns>A new <see cref="LibGit2Sharp.Handlers.TransferProgressHandler"/> based on <paramref name="progressReporter"/>.</returns>
|
||||
static TransferProgressHandler TransferProgressHandler(ILogger logger, JobProgressReporter? progressReporter, CancellationToken cancellationToken) => transferProgress =>
|
||||
static TransferProgressHandler TransferProgressHandler(ILogger logger, JobProgressReporter progressReporter, CancellationToken cancellationToken) => transferProgress =>
|
||||
{
|
||||
double? percentage;
|
||||
var totalObjectsToProcess = transferProgress.TotalObjects * 2;
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
using System;
|
||||
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Microsoft.Extensions.Logging.Abstractions;
|
||||
|
||||
using Tgstation.Server.Host.Models;
|
||||
|
||||
@@ -9,7 +10,7 @@ namespace Tgstation.Server.Host.Jobs
|
||||
/// <summary>
|
||||
/// Progress reporter for a <see cref="Job"/>.
|
||||
/// </summary>
|
||||
public sealed class JobProgressReporter
|
||||
public sealed class JobProgressReporter : IDisposable
|
||||
{
|
||||
/// <summary>
|
||||
/// The name of the current stage.
|
||||
@@ -52,6 +53,24 @@ namespace Tgstation.Server.Host.Jobs
|
||||
/// </summary>
|
||||
double sectionProgression;
|
||||
|
||||
/// <summary>
|
||||
/// The total progress reserved for use in this section.
|
||||
/// </summary>
|
||||
double? sectionReservations;
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="JobProgressReporter"/> class.
|
||||
/// </summary>
|
||||
/// <remarks>This variant has no function.</remarks>
|
||||
public JobProgressReporter()
|
||||
: this(
|
||||
NullLogger<JobProgressReporter>.Instance,
|
||||
null,
|
||||
(_, _) => { },
|
||||
false)
|
||||
{
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="JobProgressReporter"/> class.
|
||||
/// </summary>
|
||||
@@ -59,27 +78,84 @@ namespace Tgstation.Server.Host.Jobs
|
||||
/// <param name="stageName">The value of <see cref="StageName"/>.</param>
|
||||
/// <param name="callback">The value of <see cref="callback"/>.</param>
|
||||
public JobProgressReporter(ILogger<JobProgressReporter> logger, string? stageName, Action<string?, double?> callback)
|
||||
: this(
|
||||
logger,
|
||||
stageName,
|
||||
callback,
|
||||
true)
|
||||
{
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="JobProgressReporter"/> class.
|
||||
/// </summary>
|
||||
/// <param name="logger">The value of <see cref="logger"/>.</param>
|
||||
/// <param name="stageName">The value of <see cref="StageName"/>.</param>
|
||||
/// <param name="callback">The value of <see cref="callback"/>.</param>
|
||||
/// <param name="setStageName">If <see langword="true"/> an initial call to <paramref name="callback"/> will be made with only the <paramref name="stageName"/>.</param>
|
||||
private JobProgressReporter(ILogger<JobProgressReporter> logger, string? stageName, Action<string?, double?> callback, bool setStageName)
|
||||
{
|
||||
this.logger = logger ?? throw new ArgumentNullException(nameof(logger));
|
||||
this.callback = callback ?? throw new ArgumentNullException(nameof(callback));
|
||||
StageName = stageName;
|
||||
if (setStageName)
|
||||
{
|
||||
StageName = stageName;
|
||||
}
|
||||
else
|
||||
{
|
||||
this.stageName = stageName;
|
||||
}
|
||||
|
||||
logger.LogDebug("Job progress reporter created. Stage: {stageName}", stageName ?? "(null)");
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public void Dispose()
|
||||
{
|
||||
if (sectionReservations.HasValue)
|
||||
if (sectionReservations.Value != 1.0)
|
||||
{
|
||||
// not an error, processes can throw
|
||||
sectionReservations = null;
|
||||
}
|
||||
else if (sectionProgression < 1.0)
|
||||
{
|
||||
logger.LogError(
|
||||
new InvalidOperationException($"Parent progress reporter has child sections that didn't complete! Current: {sectionProgression}"),
|
||||
"TGS BUG: Progress reporter children didn't complete!");
|
||||
sectionReservations = null;
|
||||
}
|
||||
|
||||
if (!sectionReservations.HasValue)
|
||||
ReportProgress(1);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Report progress.
|
||||
/// </summary>
|
||||
/// <param name="progress">A percentage value from 0.0f-1.0f.</param>
|
||||
public void ReportProgress(double? progress)
|
||||
{
|
||||
if (sectionReservations.HasValue)
|
||||
if (progress == 0)
|
||||
{
|
||||
// might be a stage reset
|
||||
sectionReservations = null;
|
||||
}
|
||||
else
|
||||
{
|
||||
logger.LogError(
|
||||
new InvalidOperationException("Progress reporter is reporting progress with existing nested sections!"),
|
||||
"TGS BUG: A progress reporter is using mixed local and nested progress, this is not supported");
|
||||
}
|
||||
|
||||
var clampedProgress = progress;
|
||||
if (progress.HasValue)
|
||||
if (progress > 1 || progress < 0)
|
||||
{
|
||||
logger.LogError(
|
||||
new ArgumentOutOfRangeException(nameof(progress), progress, "Progress must be a value from 0-1!"),
|
||||
"Invalid progress value for stage {stageName}",
|
||||
"TGS BUG: Invalid progress value for stage {stageName}",
|
||||
StageName ?? "(null)");
|
||||
clampedProgress = null;
|
||||
}
|
||||
@@ -103,16 +179,28 @@ namespace Tgstation.Server.Host.Jobs
|
||||
{
|
||||
logger.LogError(
|
||||
new ArgumentOutOfRangeException(nameof(percentage), percentage, "Percentage must be a value from 0-1!"),
|
||||
"Invalid percentage value for stage {newStageName}! Clamping...",
|
||||
"TGS BUG: Invalid percentage value for stage {newStageName}! Clamping...",
|
||||
newStageName ?? "(null)");
|
||||
|
||||
percentage = Math.Min(Math.Max(percentage, 0.0), 1.0);
|
||||
}
|
||||
|
||||
var childBaseProgress = sectionProgression;
|
||||
if (percentage + childBaseProgress > 1.0)
|
||||
if (!sectionReservations.HasValue)
|
||||
{
|
||||
var remainingPercentage = 1.0 - childBaseProgress;
|
||||
if (sectionProgression != 0)
|
||||
{
|
||||
logger.LogError(
|
||||
new InvalidOperationException("Progress reporter is creating a section with local progress!"),
|
||||
"TGS BUG: A progress reporter is using mixed local and nested progress, this is not supported");
|
||||
}
|
||||
|
||||
sectionReservations = 0;
|
||||
}
|
||||
|
||||
// floating point >.<
|
||||
if (percentage + sectionReservations.Value > 1.0001)
|
||||
{
|
||||
var remainingPercentage = 1.0 - sectionReservations.Value;
|
||||
logger.LogError(
|
||||
"Stage {newStageName} is overbudgeted ({budget}/{remainingPercentage})! Clamping...",
|
||||
newStageName,
|
||||
@@ -121,6 +209,9 @@ namespace Tgstation.Server.Host.Jobs
|
||||
percentage = remainingPercentage;
|
||||
}
|
||||
|
||||
Math.Min(sectionReservations.Value + percentage, 1);
|
||||
|
||||
var childLocalProgress = 0.0;
|
||||
var newReporter = new JobProgressReporter(
|
||||
logger,
|
||||
newStageName,
|
||||
@@ -133,11 +224,17 @@ namespace Tgstation.Server.Host.Jobs
|
||||
return;
|
||||
}
|
||||
|
||||
var childLocalProgress = progress.Value * percentage;
|
||||
var progressWithoutChild = sectionProgression - childLocalProgress;
|
||||
childLocalProgress = progress.Value * percentage;
|
||||
|
||||
// floating point >.<
|
||||
sectionProgression = Math.Min(progressWithoutChild + childLocalProgress, 1);
|
||||
if (sectionProgression > 9.9999)
|
||||
sectionProgression = 1;
|
||||
|
||||
sectionProgression = childLocalProgress + childBaseProgress;
|
||||
callback(currentStage, sectionProgression);
|
||||
});
|
||||
},
|
||||
false);
|
||||
|
||||
newReporter.ReportProgress(0);
|
||||
return newReporter;
|
||||
|
||||
@@ -460,14 +460,16 @@ namespace Tgstation.Server.Host.Jobs
|
||||
QueueHubUpdate(job.ToApi(), false);
|
||||
|
||||
logger.LogTrace("Starting job...");
|
||||
using var progressReporter = new JobProgressReporter(
|
||||
loggerFactory.CreateLogger<JobProgressReporter>(),
|
||||
null,
|
||||
UpdateProgress);
|
||||
using var innerReporter = progressReporter.CreateSection(null, 1.0);
|
||||
await operation(
|
||||
instanceCoreProvider.GetInstance(job.Instance!),
|
||||
databaseContextFactory,
|
||||
job,
|
||||
new JobProgressReporter(
|
||||
loggerFactory.CreateLogger<JobProgressReporter>(),
|
||||
null,
|
||||
UpdateProgress),
|
||||
innerReporter,
|
||||
cancellationToken);
|
||||
|
||||
logger.LogDebug("Job {jobId} completed!", job.Id);
|
||||
|
||||
@@ -0,0 +1,33 @@
|
||||
using System;
|
||||
using System.Reflection;
|
||||
|
||||
namespace Tgstation.Server.Host.Properties
|
||||
{
|
||||
/// <summary>
|
||||
/// Attribute for bundling the GitHub App serialized private key used for version telemetry.
|
||||
/// </summary>
|
||||
[AttributeUsage(AttributeTargets.Assembly)]
|
||||
sealed class TelemetryAppSerializedKeyAttribute : Attribute
|
||||
{
|
||||
/// <summary>
|
||||
/// Return the <see cref="Assembly"/>'s instance of the <see cref="TelemetryAppSerializedKeyAttribute"/>.
|
||||
/// </summary>
|
||||
public static TelemetryAppSerializedKeyAttribute? Instance => Assembly
|
||||
.GetExecutingAssembly()
|
||||
.GetCustomAttribute<TelemetryAppSerializedKeyAttribute>();
|
||||
|
||||
/// <summary>
|
||||
/// The serialized GitHub App Client ID and private key.
|
||||
/// </summary>
|
||||
public string SerializedKey { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="TelemetryAppSerializedKeyAttribute"/> class.
|
||||
/// </summary>
|
||||
/// <param name="serializedKey">The value of <see cref="SerializedKey"/>.</param>
|
||||
public TelemetryAppSerializedKeyAttribute(string serializedKey)
|
||||
{
|
||||
SerializedKey = serializedKey ?? throw new ArgumentNullException(nameof(serializedKey));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -60,12 +60,12 @@ namespace Tgstation.Server.Host.Security.OAuth
|
||||
{
|
||||
logger.LogTrace("Validating response code...");
|
||||
|
||||
var gitHubService = gitHubServiceFactory.CreateService();
|
||||
var gitHubService = await gitHubServiceFactory.CreateService(cancellationToken);
|
||||
var token = await gitHubService.CreateOAuthAccessToken(oAuthConfiguration, code, cancellationToken);
|
||||
if (token == null)
|
||||
return null;
|
||||
|
||||
var authenticatedClient = gitHubServiceFactory.CreateService(token);
|
||||
var authenticatedClient = await gitHubServiceFactory.CreateService(token, cancellationToken);
|
||||
|
||||
logger.LogTrace("Getting user details...");
|
||||
var userId = await authenticatedClient.GetCurrentUserId(cancellationToken);
|
||||
|
||||
@@ -713,7 +713,7 @@ namespace Tgstation.Server.Host.Setup
|
||||
while (true);
|
||||
|
||||
await console.WriteAsync(null, true, cancellationToken);
|
||||
await console.WriteAsync("Enter a GitHub personal access token to bypass some rate limits (this is optional and does not require any scopes)", true, cancellationToken);
|
||||
await console.WriteAsync("Enter a classic GitHub personal access token to bypass some rate limits (this is optional and does not require any scopes)", true, cancellationToken);
|
||||
await console.WriteAsync("GitHub personal access token: ", false, cancellationToken);
|
||||
newGeneralConfiguration.GitHubAccessToken = await console.ReadLineAsync(true, cancellationToken);
|
||||
if (String.IsNullOrWhiteSpace(newGeneralConfiguration.GitHubAccessToken))
|
||||
@@ -958,6 +958,31 @@ namespace Tgstation.Server.Host.Setup
|
||||
};
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Prompts the user to create a <see cref="TelemetryConfiguration"/>.
|
||||
/// </summary>
|
||||
/// <param name="cancellationToken">The <see cref="CancellationToken"/> for the operation.</param>
|
||||
/// <returns>A <see cref="ValueTask{TResult}"/> resulting in the new <see cref="TelemetryConfiguration"/>.</returns>
|
||||
async ValueTask<TelemetryConfiguration> ConfigureTelemetry(CancellationToken cancellationToken)
|
||||
{
|
||||
bool enableReporting = await PromptYesNo("Enable version telemetry? This anonymously reports the TGS version in use.", true, cancellationToken);
|
||||
|
||||
string? serverFriendlyName = null;
|
||||
if (enableReporting)
|
||||
{
|
||||
await console.WriteAsync("(Optional) Publically associate your reported version with a friendly name:", false, cancellationToken);
|
||||
serverFriendlyName = await console.ReadLineAsync(false, cancellationToken);
|
||||
if (String.IsNullOrWhiteSpace(serverFriendlyName))
|
||||
serverFriendlyName = null;
|
||||
}
|
||||
|
||||
return new TelemetryConfiguration
|
||||
{
|
||||
DisableVersionReporting = !enableReporting,
|
||||
ServerFriendlyName = serverFriendlyName,
|
||||
};
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Saves a given <see cref="Configuration"/> set to <paramref name="userConfigFileName"/>.
|
||||
/// </summary>
|
||||
@@ -969,6 +994,7 @@ namespace Tgstation.Server.Host.Setup
|
||||
/// <param name="elasticsearchConfiguration">The <see cref="ElasticsearchConfiguration"/> to save.</param>
|
||||
/// <param name="controlPanelConfiguration">The <see cref="ControlPanelConfiguration"/> to save.</param>
|
||||
/// <param name="swarmConfiguration">The <see cref="SwarmConfiguration"/> to save.</param>
|
||||
/// <param name="telemetryConfiguration">The <see cref="TelemetryConfiguration"/> to save.</param>
|
||||
/// <param name="cancellationToken">The <see cref="CancellationToken"/> for the operation.</param>
|
||||
/// <returns>A <see cref="ValueTask"/> representing the running operation.</returns>
|
||||
async ValueTask SaveConfiguration(
|
||||
@@ -980,6 +1006,7 @@ namespace Tgstation.Server.Host.Setup
|
||||
ElasticsearchConfiguration? elasticsearchConfiguration,
|
||||
ControlPanelConfiguration controlPanelConfiguration,
|
||||
SwarmConfiguration? swarmConfiguration,
|
||||
TelemetryConfiguration? telemetryConfiguration,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
newGeneralConfiguration.ApiPort = hostingPort ?? GeneralConfiguration.DefaultApiPort;
|
||||
@@ -992,6 +1019,7 @@ namespace Tgstation.Server.Host.Setup
|
||||
{ ElasticsearchConfiguration.Section, elasticsearchConfiguration },
|
||||
{ ControlPanelConfiguration.Section, controlPanelConfiguration },
|
||||
{ SwarmConfiguration.Section, swarmConfiguration },
|
||||
{ TelemetryConfiguration.Section, telemetryConfiguration },
|
||||
};
|
||||
|
||||
var versionConverter = new VersionConverter();
|
||||
@@ -1065,6 +1093,8 @@ namespace Tgstation.Server.Host.Setup
|
||||
|
||||
var swarmConfiguration = await ConfigureSwarm(cancellationToken);
|
||||
|
||||
var telemetryConfiguration = await ConfigureTelemetry(cancellationToken);
|
||||
|
||||
await console.WriteAsync(null, true, cancellationToken);
|
||||
await console.WriteAsync(String.Format(CultureInfo.InvariantCulture, "Configuration complete! Saving to {0}", userConfigFileName), true, cancellationToken);
|
||||
|
||||
@@ -1077,6 +1107,7 @@ namespace Tgstation.Server.Host.Setup
|
||||
elasticSearchConfiguration,
|
||||
controlPanelConfiguration,
|
||||
swarmConfiguration,
|
||||
telemetryConfiguration,
|
||||
cancellationToken);
|
||||
}
|
||||
|
||||
@@ -1194,6 +1225,7 @@ namespace Tgstation.Server.Host.Setup
|
||||
AllowAnyOrigin = true,
|
||||
},
|
||||
null,
|
||||
null,
|
||||
cancellationToken);
|
||||
}
|
||||
else
|
||||
|
||||
@@ -13,6 +13,11 @@ namespace Tgstation.Server.Host.System
|
||||
/// </summary>
|
||||
Task<int?> Lifetime { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the process' memory usage in bytes.
|
||||
/// </summary>
|
||||
long? MemoryUsage { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Set's the owned <see cref="global::System.Diagnostics.Process.PriorityClass"/> to a non-normal value.
|
||||
/// </summary>
|
||||
|
||||
@@ -22,6 +22,23 @@ namespace Tgstation.Server.Host.System
|
||||
/// <inheritdoc />
|
||||
public Task<int?> Lifetime { get; }
|
||||
|
||||
/// <inheritdoc />
|
||||
public long? MemoryUsage
|
||||
{
|
||||
get
|
||||
{
|
||||
try
|
||||
{
|
||||
return handle.VirtualMemorySize64;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
logger.LogWarning(ex, "Failed to get PID {pid}'s memory usage!", Id);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// The <see cref="IProcessFeatures"/> for the <see cref="Process"/>.
|
||||
/// </summary>
|
||||
|
||||
@@ -29,13 +29,13 @@
|
||||
<Exec Command="git clone https://github.com/tgstation/tgstation-server-webpanel --branch v$(TgsWebpanelVersion) --depth 1 ClientApp" />
|
||||
<RemoveDir Directories="ClientApp/.git" /> <!-- Allows git clean to clean it out -->
|
||||
<Message Text="Restoring yarn packages..." Importance="high" />
|
||||
<Exec WorkingDirectory="ClientApp" Command="npx --yes yarn@$(TgsYarnVersion) install --immutable" />
|
||||
<Exec WorkingDirectory="ClientApp" Command="yarn install --immutable" />
|
||||
<Touch Files="$(NpmInstallStampFile)" AlwaysCreate="true" />
|
||||
</Target>
|
||||
|
||||
<Target Condition="'$(TGS_HOST_NO_WEBPANEL)' != 'true'" Name="NpmBuild" BeforeTargets="BeforeBuild" DependsOnTargets="ClientInstall" Inputs="../../build/WebpanelVersion.props" Outputs="wwwroot\index.html">
|
||||
<Message Text="Building web control panel..." Importance="high" />
|
||||
<Exec WorkingDirectory="ClientApp" Command="npx --yes yarn@$(TgsYarnVersion) run msbuild" />
|
||||
<Exec WorkingDirectory="ClientApp" Command="yarn run msbuild" />
|
||||
</Target>
|
||||
|
||||
<Target Name="NpmClean" AfterTargets="Clean">
|
||||
@@ -45,21 +45,43 @@
|
||||
|
||||
<Target Name="ApplyMasterVersionsAttribute" BeforeTargets="CoreCompile">
|
||||
<ItemGroup>
|
||||
<AssemblyAttributes Include="Tgstation.Server.Host.Properties.MasterVersionsAttribute">
|
||||
<MasterVersionAssemblyAttributes Include="Tgstation.Server.Host.Properties.MasterVersionsAttribute">
|
||||
<_Parameter1>$(TgsConfigVersion)</_Parameter1>
|
||||
<_Parameter2>$(TgsInteropVersion)</_Parameter2>
|
||||
<_Parameter3>$(TgsWebpanelVersion)</_Parameter3>
|
||||
<_Parameter4>$(TgsHostWatchdogVersion)</_Parameter4>
|
||||
<_Parameter5>$(TgsMariaDBRedistVersion)</_Parameter5>
|
||||
</AssemblyAttributes>
|
||||
</MasterVersionAssemblyAttributes>
|
||||
</ItemGroup>
|
||||
|
||||
<WriteCodeFragment AssemblyAttributes="@(AssemblyAttributes)" Language="C#" OutputDirectory="$(IntermediateOutputPath)" OutputFile="MasterVersionsAssemblyInfo.cs">
|
||||
<WriteCodeFragment AssemblyAttributes="@(MasterVersionAssemblyAttributes)" Language="C#" OutputDirectory="$(IntermediateOutputPath)" OutputFile="MasterVersionsAssemblyInfo.cs">
|
||||
<Output TaskParameter="OutputFile" ItemName="Compile" />
|
||||
<Output TaskParameter="OutputFile" ItemName="FileWrites" />
|
||||
</WriteCodeFragment>
|
||||
</Target>
|
||||
|
||||
<Target Condition="'$(TGS_TELEMETRY_KEY_FILE)' != ''" Name="ApplyTelemetryAppSerializedKey" BeforeTargets="CoreCompile">
|
||||
<Error Condition="!Exists('$(TGS_TELEMETRY_KEY_FILE)')" Text="TGS_TELEMETRY_KEY_FILE set but does not exist!" />
|
||||
<ReadLinesFromFile
|
||||
File="$(TGS_TELEMETRY_KEY_FILE)" >
|
||||
<Output
|
||||
TaskParameter="Lines"
|
||||
ItemName="SerializedTelemetryKey"/>
|
||||
</ReadLinesFromFile>
|
||||
<ItemGroup>
|
||||
<TelemetryAppSerializedKeyAssemblyAttributes Include="Tgstation.Server.Host.Properties.TelemetryAppSerializedKeyAttribute">
|
||||
<_Parameter1>@(SerializedTelemetryKey)</_Parameter1>
|
||||
</TelemetryAppSerializedKeyAssemblyAttributes>
|
||||
</ItemGroup>
|
||||
<WriteCodeFragment AssemblyAttributes="@(TelemetryAppSerializedKeyAssemblyAttributes)" Language="C#" OutputDirectory="$(IntermediateOutputPath)" OutputFile="TelemetryAppSerializedKeyAssemblyInfo.cs">
|
||||
<Output TaskParameter="OutputFile" ItemName="Compile" />
|
||||
<Output TaskParameter="OutputFile" ItemName="FileWrites" />
|
||||
</WriteCodeFragment>
|
||||
</Target>
|
||||
|
||||
<Target Condition="'$(TGS_TELEMETRY_KEY_FILE)' == '' And '$(CI)' != ''" Name="FailBuildInCIWithoutTelemetryKey" BeforeTargets="CoreCompile">
|
||||
<Error Text="The TGS_TELEMETRY_KEY_FILE environment variable should be set in CI!"/>
|
||||
</Target>
|
||||
|
||||
<ItemGroup>
|
||||
<!-- Usage: Concise throw statements for native Win32 errors -->
|
||||
<PackageReference Include="BetterWin32Errors" Version="0.2.0" />
|
||||
@@ -78,21 +100,21 @@
|
||||
<!-- Usage: git interop -->
|
||||
<PackageReference Include="LibGit2Sharp" Version="0.30.0" />
|
||||
<!-- Usage: Support ""legacy"" Newotonsoft.Json in HTTP pipeline. The rest of our codebase uses Newtonsoft. -->
|
||||
<PackageReference Include="Microsoft.AspNetCore.Mvc.NewtonsoftJson" Version="8.0.7" />
|
||||
<PackageReference Include="Microsoft.AspNetCore.Mvc.NewtonsoftJson" Version="8.0.8" />
|
||||
<!-- Usage: Using target JSON serializer for API -->
|
||||
<PackageReference Include="Microsoft.AspNetCore.SignalR.Protocols.NewtonsoftJson" Version="8.0.7" />
|
||||
<PackageReference Include="Microsoft.AspNetCore.SignalR.Protocols.NewtonsoftJson" Version="8.0.8" />
|
||||
<!-- Usage: Generating dumps of dotnet engine processes -->
|
||||
<PackageReference Include="Microsoft.Diagnostics.NETCore.Client" Version="0.2.532401" />
|
||||
<!-- Usage: Database ORM -->
|
||||
<PackageReference Include="Microsoft.EntityFrameworkCore" Version="8.0.7" />
|
||||
<PackageReference Include="Microsoft.EntityFrameworkCore" Version="8.0.8" />
|
||||
<!-- Usage: Automatic migration generation using command line -->
|
||||
<PackageReference Include="Microsoft.EntityFrameworkCore.Design" Version="8.0.7">
|
||||
<PackageReference Include="Microsoft.EntityFrameworkCore.Design" Version="8.0.8">
|
||||
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
||||
</PackageReference>
|
||||
<!-- Usage: Sqlite ORM plugin -->
|
||||
<PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite" Version="8.0.7" />
|
||||
<PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite" Version="8.0.8" />
|
||||
<!-- Usage: MSSQL ORM plugin -->
|
||||
<PackageReference Include="Microsoft.EntityFrameworkCore.SqlServer" Version="8.0.7" />
|
||||
<PackageReference Include="Microsoft.EntityFrameworkCore.SqlServer" Version="8.0.8" />
|
||||
<!-- Usage: POSIX support for syscalls, signals, and symlinks -->
|
||||
<PackageReference Include="Mono.Posix.NETStandard" Version="1.0.0" />
|
||||
<PackageReference Include="NCrontab.Signed" Version="3.3.3" />
|
||||
@@ -115,9 +137,9 @@
|
||||
<!-- Usage: File logging plugin -->
|
||||
<PackageReference Include="Serilog.Sinks.File" Version="6.0.0" />
|
||||
<!-- Usage: OpenAPI spec generator -->
|
||||
<PackageReference Include="Swashbuckle.AspNetCore" Version="6.6.2" />
|
||||
<PackageReference Include="Swashbuckle.AspNetCore" Version="6.7.0" />
|
||||
<!-- Usage: Newtonsoft.Json plugin for OpenAPI spec generator -->
|
||||
<PackageReference Include="Swashbuckle.AspNetCore.Newtonsoft" Version="6.6.2" />
|
||||
<PackageReference Include="Swashbuckle.AspNetCore.Newtonsoft" Version="6.7.0" />
|
||||
<!-- Usage: Windows authentication plugin allowing searching for users by name -->
|
||||
<PackageReference Include="System.DirectoryServices.AccountManagement" Version="8.0.0" />
|
||||
<!-- Usage: Identifying owning user of Windows Process objects -->
|
||||
|
||||
@@ -1,9 +1,16 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IdentityModel.Tokens.Jwt;
|
||||
using System.Linq;
|
||||
using System.Security.Cryptography;
|
||||
using System.Text;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Microsoft.Extensions.Options;
|
||||
using Microsoft.IdentityModel.Tokens;
|
||||
|
||||
using Octokit;
|
||||
|
||||
using Tgstation.Server.Host.Configuration;
|
||||
@@ -12,7 +19,7 @@ using Tgstation.Server.Host.System;
|
||||
namespace Tgstation.Server.Host.Utils.GitHub
|
||||
{
|
||||
/// <inheritdoc />
|
||||
sealed class GitHubClientFactory : IGitHubClientFactory
|
||||
sealed class GitHubClientFactory : IGitHubClientFactory, IDisposable
|
||||
{
|
||||
/// <summary>
|
||||
/// Limit to the amount of days a <see cref="GitHubClient"/> can live in the <see cref="clientCache"/>.
|
||||
@@ -45,6 +52,11 @@ namespace Tgstation.Server.Host.Utils.GitHub
|
||||
/// </summary>
|
||||
readonly Dictionary<string, (GitHubClient Client, DateTimeOffset LastUsed)> clientCache;
|
||||
|
||||
/// <summary>
|
||||
/// The <see cref="SemaphoreSlim"/> used to guard access to <see cref="clientCache"/>.
|
||||
/// </summary>
|
||||
readonly SemaphoreSlim clientCacheSemaphore;
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="GitHubClientFactory"/> class.
|
||||
/// </summary>
|
||||
@@ -61,50 +73,139 @@ namespace Tgstation.Server.Host.Utils.GitHub
|
||||
generalConfiguration = generalConfigurationOptions?.Value ?? throw new ArgumentNullException(nameof(generalConfigurationOptions));
|
||||
|
||||
clientCache = new Dictionary<string, (GitHubClient, DateTimeOffset)>();
|
||||
clientCacheSemaphore = new SemaphoreSlim(1, 1);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public IGitHubClient CreateClient() => GetOrCreateClient(generalConfiguration.GitHubAccessToken);
|
||||
public void Dispose() => clientCacheSemaphore.Dispose();
|
||||
|
||||
/// <inheritdoc />
|
||||
public IGitHubClient CreateClient(string accessToken)
|
||||
=> GetOrCreateClient(
|
||||
accessToken ?? throw new ArgumentNullException(nameof(accessToken)));
|
||||
public async ValueTask<IGitHubClient> CreateClient(CancellationToken cancellationToken)
|
||||
=> (await GetOrCreateClient(
|
||||
generalConfiguration.GitHubAccessToken,
|
||||
null,
|
||||
cancellationToken))!;
|
||||
|
||||
/// <inheritdoc />
|
||||
public async ValueTask<IGitHubClient> CreateClient(string accessToken, CancellationToken cancellationToken)
|
||||
=> (await GetOrCreateClient(
|
||||
accessToken ?? throw new ArgumentNullException(nameof(accessToken)),
|
||||
null,
|
||||
cancellationToken))!;
|
||||
|
||||
/// <inheritdoc />
|
||||
public ValueTask<IGitHubClient?> CreateInstallationClient(string serializedPem, long repositoryId, CancellationToken cancellationToken)
|
||||
=> GetOrCreateClient(serializedPem, repositoryId, cancellationToken);
|
||||
|
||||
/// <summary>
|
||||
/// Retrieve a <see cref="GitHubClient"/> from the <see cref="clientCache"/> or add a new one based on a given <paramref name="accessToken"/>.
|
||||
/// Retrieve a <see cref="GitHubClient"/> from the <see cref="clientCache"/> or add a new one based on a given <paramref name="accessTokenOrSerializedPem"/>.
|
||||
/// </summary>
|
||||
/// <param name="accessToken">Optional access token to use as credentials.</param>
|
||||
/// <returns>The <see cref="GitHubClient"/> for the given <paramref name="accessToken"/>.</returns>
|
||||
GitHubClient GetOrCreateClient(string? accessToken)
|
||||
/// <param name="accessTokenOrSerializedPem">Optional access token to use as credentials or GitHub App private key. If using a private key, <paramref name="installationRepositoryId"/> must be set.</param>
|
||||
/// <param name="installationRepositoryId">Setting this specifies <paramref name="accessTokenOrSerializedPem"/> is a private key and a GitHub App installation authenticated client will be returned.</param>
|
||||
/// <param name="cancellationToken">The <see cref="CancellationToken"/> for the operation.</param>
|
||||
/// <returns>A <see cref="ValueTask{TResult}"/> resulting in the <see cref="GitHubClient"/> for the given <paramref name="accessTokenOrSerializedPem"/> or <see langword="null"/> if authentication failed.</returns>
|
||||
#pragma warning disable CA1506 // TODO: Decomplexify
|
||||
async ValueTask<IGitHubClient?> GetOrCreateClient(string? accessTokenOrSerializedPem, long? installationRepositoryId, CancellationToken cancellationToken)
|
||||
#pragma warning restore CA1506
|
||||
{
|
||||
GitHubClient client;
|
||||
bool cacheHit;
|
||||
DateTimeOffset? lastUsed;
|
||||
lock (clientCache)
|
||||
using (await SemaphoreSlimContext.Lock(clientCacheSemaphore, cancellationToken))
|
||||
{
|
||||
string cacheKey;
|
||||
if (String.IsNullOrWhiteSpace(accessToken))
|
||||
if (String.IsNullOrWhiteSpace(accessTokenOrSerializedPem))
|
||||
{
|
||||
accessToken = null;
|
||||
accessTokenOrSerializedPem = null;
|
||||
cacheKey = DefaultCacheKey;
|
||||
}
|
||||
else
|
||||
cacheKey = accessToken;
|
||||
cacheKey = accessTokenOrSerializedPem;
|
||||
|
||||
cacheHit = clientCache.TryGetValue(cacheKey, out var tuple);
|
||||
|
||||
var now = DateTimeOffset.UtcNow;
|
||||
if (!cacheHit)
|
||||
{
|
||||
logger.LogTrace("Creating new GitHubClient...");
|
||||
var product = assemblyInformationProvider.ProductInfoHeaderValue.Product!;
|
||||
client = new GitHubClient(
|
||||
new ProductHeaderValue(
|
||||
product.Name,
|
||||
product.Version));
|
||||
|
||||
if (accessToken != null)
|
||||
client.Credentials = new Credentials(accessToken);
|
||||
if (accessTokenOrSerializedPem != null)
|
||||
{
|
||||
if (installationRepositoryId.HasValue)
|
||||
{
|
||||
logger.LogTrace("Performing GitHub App authentication for installation on repository {installationRepositoryId}", installationRepositoryId.Value);
|
||||
var splits = accessTokenOrSerializedPem.Split(':');
|
||||
if (splits.Length != 2)
|
||||
{
|
||||
logger.LogError("Failed to parse serialized Client ID & PEM! Expected 2 chunks, got {chunkCount}", splits.Length);
|
||||
return null;
|
||||
}
|
||||
|
||||
byte[] pemBytes;
|
||||
try
|
||||
{
|
||||
pemBytes = Convert.FromBase64String(splits[1]);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
logger.LogError(ex, "Failed to parse supposed base64 PEM!");
|
||||
return null;
|
||||
}
|
||||
|
||||
var pem = Encoding.UTF8.GetString(pemBytes);
|
||||
|
||||
using var rsa = RSA.Create();
|
||||
rsa.ImportFromPem(pem);
|
||||
|
||||
var signingCredentials = new SigningCredentials(new RsaSecurityKey(rsa), SecurityAlgorithms.RsaSha256);
|
||||
var jwtSecurityTokenHandler = new JwtSecurityTokenHandler { SetDefaultTimesOnTokenCreation = false };
|
||||
|
||||
var nowDateTime = DateTime.UtcNow;
|
||||
|
||||
var jwt = jwtSecurityTokenHandler.CreateToken(new SecurityTokenDescriptor
|
||||
{
|
||||
Issuer = splits[0],
|
||||
Expires = nowDateTime.AddMinutes(10),
|
||||
IssuedAt = nowDateTime,
|
||||
SigningCredentials = signingCredentials,
|
||||
});
|
||||
|
||||
var jwtStr = jwtSecurityTokenHandler.WriteToken(jwt);
|
||||
|
||||
client.Credentials = new Credentials(jwtStr, AuthenticationType.Bearer);
|
||||
|
||||
Installation installation;
|
||||
try
|
||||
{
|
||||
installation = await client.GitHubApps.GetRepositoryInstallationForCurrent(installationRepositoryId.Value);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
logger.LogError(ex, "Failed to perform app authentication!");
|
||||
return null;
|
||||
}
|
||||
|
||||
cancellationToken.ThrowIfCancellationRequested();
|
||||
try
|
||||
{
|
||||
var installToken = await client.GitHubApps.CreateInstallationToken(installation.Id);
|
||||
|
||||
client.Credentials = new Credentials(installToken.Token);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
logger.LogError(ex, "Failed to perform installation authentication!");
|
||||
return null;
|
||||
}
|
||||
}
|
||||
else
|
||||
client.Credentials = new Credentials(accessTokenOrSerializedPem);
|
||||
}
|
||||
|
||||
clientCache.Add(cacheKey, (Client: client, LastUsed: now));
|
||||
lastUsed = null;
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
using System;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Microsoft.Extensions.Options;
|
||||
@@ -44,13 +46,16 @@ namespace Tgstation.Server.Host.Utils.GitHub
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public IGitHubService CreateService() => CreateServiceImpl(gitHubClientFactory.CreateClient());
|
||||
public async ValueTask<IGitHubService> CreateService(CancellationToken cancellationToken)
|
||||
=> CreateServiceImpl(
|
||||
await gitHubClientFactory.CreateClient(cancellationToken));
|
||||
|
||||
/// <inheritdoc />
|
||||
public IAuthenticatedGitHubService CreateService(string accessToken)
|
||||
public async ValueTask<IAuthenticatedGitHubService> CreateService(string accessToken, CancellationToken cancellationToken)
|
||||
=> CreateServiceImpl(
|
||||
gitHubClientFactory.CreateClient(
|
||||
accessToken ?? throw new ArgumentNullException(nameof(accessToken))));
|
||||
await gitHubClientFactory.CreateClient(
|
||||
accessToken ?? throw new ArgumentNullException(nameof(accessToken)),
|
||||
cancellationToken));
|
||||
|
||||
/// <summary>
|
||||
/// Create a <see cref="GitHubService"/>.
|
||||
|
||||
@@ -1,4 +1,7 @@
|
||||
using Octokit;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
using Octokit;
|
||||
|
||||
namespace Tgstation.Server.Host.Utils.GitHub
|
||||
{
|
||||
@@ -10,14 +13,25 @@ namespace Tgstation.Server.Host.Utils.GitHub
|
||||
/// <summary>
|
||||
/// Create a <see cref="IGitHubClient"/> client. Low rate limit unless the server's GitHubAccessToken is set to bypass it.
|
||||
/// </summary>
|
||||
/// <param name="cancellationToken">The <see cref="CancellationToken"/> for the operation.</param>
|
||||
/// <returns>A new <see cref="IGitHubClient"/>.</returns>
|
||||
IGitHubClient CreateClient();
|
||||
ValueTask<IGitHubClient> CreateClient(CancellationToken cancellationToken);
|
||||
|
||||
/// <summary>
|
||||
/// Create a client with authentication using a personal access token.
|
||||
/// </summary>
|
||||
/// <param name="accessToken">The GitHub personal access token.</param>
|
||||
/// <param name="cancellationToken">The <see cref="CancellationToken"/> for the operation.</param>
|
||||
/// <returns>A new <see cref="IGitHubClient"/>.</returns>
|
||||
IGitHubClient CreateClient(string accessToken);
|
||||
ValueTask<IGitHubClient> CreateClient(string accessToken, CancellationToken cancellationToken);
|
||||
|
||||
/// <summary>
|
||||
/// Creates a GitHub App client for an installation.
|
||||
/// </summary>
|
||||
/// <param name="pem">The private key <see cref="string"/>.</param>
|
||||
/// <param name="repositoryId">The GitHub repository ID.</param>
|
||||
/// <param name="cancellationToken">The <see cref="CancellationToken"/> for the operation.</param>
|
||||
/// <returns>A <see cref="ValueTask{TResult}"/> resulting in a new <see cref="IGitHubClient"/> for the given <paramref name="repositoryId"/> or <see langword="null"/> if authentication failed.</returns>
|
||||
ValueTask<IGitHubClient?> CreateInstallationClient(string pem, long repositoryId, CancellationToken cancellationToken);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,4 +1,7 @@
|
||||
namespace Tgstation.Server.Host.Utils.GitHub
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace Tgstation.Server.Host.Utils.GitHub
|
||||
{
|
||||
/// <summary>
|
||||
/// Factory for <see cref="IGitHubService"/>s.
|
||||
@@ -8,14 +11,16 @@
|
||||
/// <summary>
|
||||
/// Create a <see cref="IGitHubService"/>.
|
||||
/// </summary>
|
||||
/// <returns>A new <see cref="IGitHubService"/>.</returns>
|
||||
public IGitHubService CreateService();
|
||||
/// <param name="cancellationToken">The <see cref="CancellationToken"/> for the operation.</param>
|
||||
/// <returns>A <see cref="ValueTask{TResult}"/> resulting in a new <see cref="IGitHubService"/>.</returns>
|
||||
public ValueTask<IGitHubService> CreateService(CancellationToken cancellationToken);
|
||||
|
||||
/// <summary>
|
||||
/// Create an <see cref="IAuthenticatedGitHubService"/>.
|
||||
/// </summary>
|
||||
/// <param name="accessToken">The access token to use for communication with GitHub.</param>
|
||||
/// <returns>A new <see cref="IAuthenticatedGitHubService"/>.</returns>
|
||||
public IAuthenticatedGitHubService CreateService(string accessToken);
|
||||
/// <param name="cancellationToken">The <see cref="CancellationToken"/> for the operation.</param>
|
||||
/// <returns>A <see cref="ValueTask{TResult}"/> resulting in a new <see cref="IAuthenticatedGitHubService"/>.</returns>
|
||||
public ValueTask<IAuthenticatedGitHubService> CreateService(string accessToken, CancellationToken cancellationToken);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -76,7 +76,7 @@ namespace Tgstation.Server.Host.Utils
|
||||
Name = "/tg/station 13",
|
||||
Url = new Uri("https://github.com/tgstation"),
|
||||
},
|
||||
Description = "A production scale tool for BYOND server management",
|
||||
Description = "A production scale tool for DreamMaker server management",
|
||||
});
|
||||
|
||||
// Important to do this before applying our own filters
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
General:
|
||||
# ConfigVersion: # Basic semver. Differs from TGS version to version. See changelog for current version
|
||||
MinimumPasswordLength: 15 # Minimum TGS user password length
|
||||
GitHubAccessToken: # GitHub personal access token with no scopes used to bypass rate-limits
|
||||
GitHubAccessToken: # A classic GitHub personal access token with no scopes used to bypass rate-limits
|
||||
SetupWizardMode: AutoDetect # If the interactive TGS setup wizard should run
|
||||
ByondTopicTimeout: 5000 # Timeout for BYOND /world/Topic() calls in milliseconds
|
||||
RestartTimeoutMinutes: 1 # Timeout for server restarts after requested by SIGTERM or the HTTP API
|
||||
@@ -23,6 +23,7 @@ Session:
|
||||
HighPriorityLiveDreamDaemon: false # If DreamDaemon instances should run as higher priority processes
|
||||
LowPriorityDeploymentProcesses: true # If TGS Deployments should run as lower priority processes
|
||||
DelayCleaningFailedDeployments: false # If true, deployments that fail will not be immediately cleaned up. They will be cleaned up the next time the instance is onlined
|
||||
ForceUseDreamDaemonExe: false # If true, dd.exe will not be used on Windows systems in versions where it is present. Instead dreamdaemon.exe will always be used.
|
||||
FileLogging:
|
||||
Directory: # Directory in which log files are stored. Windows default: %PROGRAMDATA%/tgstation-server. Linux default: /var/log/tgstation-server
|
||||
Disable: true # Disable file logging entirely
|
||||
@@ -77,3 +78,7 @@ Swarm: # Should be left empty if using swarm mode is not desired
|
||||
# PublicAddress: # The public address of the swarm node
|
||||
# ControllerAddress: # Required on non-controller nodes. The internal address of the swarm controller's API'. Should be left empty on the controller itself
|
||||
# UpdateRequiredNodeCount: # The number of nodes expected to be in the swarm before initiating an update. This should count every server irrespective of whether or not they are the controller MINUS 1
|
||||
Telemetry:
|
||||
DisableVersionReporting: false # Prevents you installation and the version you're using from being reported on the source repository's deployments list
|
||||
ServerFriendlyName: null # Sets a friendly name for your server in reported telemetry. Must be unique. First come first serve
|
||||
VersionReportingRepositoryId: 841149827 # GitHub repostiory ID where the tgs_version_telemetry workflow can be found
|
||||
|
||||
@@ -10,7 +10,7 @@
|
||||
|
||||
<ItemGroup>
|
||||
<!-- Usage: JWT injection into HTTP pipeline -->
|
||||
<PackageReference Include="Microsoft.AspNetCore.Authentication.JwtBearer" Version="8.0.7" />
|
||||
<PackageReference Include="Microsoft.AspNetCore.Authentication.JwtBearer" Version="8.0.8" />
|
||||
<!-- Usage: GitHub.com interop -->
|
||||
<PackageReference Include="Octokit" Version="13.0.1" />
|
||||
<!-- Usage: YAML conversion of Version objects -->
|
||||
|
||||
@@ -13,6 +13,7 @@ using Tgstation.Server.Common.Http;
|
||||
using Tgstation.Server.Host.Components.Repository;
|
||||
using Tgstation.Server.Host.Configuration;
|
||||
using Tgstation.Server.Host.IO;
|
||||
using Tgstation.Server.Host.Jobs;
|
||||
using Tgstation.Server.Host.System;
|
||||
using Tgstation.Server.Host.Utils;
|
||||
|
||||
@@ -52,7 +53,7 @@ namespace Tgstation.Server.Host.Components.Engine.Tests
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
It.IsNotNull<JobProgressReporter>(),
|
||||
true,
|
||||
It.IsAny<CancellationToken>()))
|
||||
.Callback(() => ++cloneAttempts)
|
||||
@@ -85,7 +86,7 @@ namespace Tgstation.Server.Host.Components.Engine.Tests
|
||||
Engine = EngineType.OpenDream,
|
||||
SourceSHA = new string('a', Limits.MaximumCommitShaLength),
|
||||
},
|
||||
null,
|
||||
new JobProgressReporter(),
|
||||
CancellationToken.None);
|
||||
|
||||
|
||||
|
||||
@@ -15,6 +15,7 @@ using Remora.Rest.Core;
|
||||
using Tgstation.Server.Host.Components.Events;
|
||||
using Tgstation.Server.Host.Configuration;
|
||||
using Tgstation.Server.Host.IO;
|
||||
using Tgstation.Server.Host.Jobs;
|
||||
|
||||
namespace Tgstation.Server.Host.Components.Repository.Tests
|
||||
{
|
||||
@@ -86,7 +87,7 @@ namespace Tgstation.Server.Host.Components.Repository.Tests
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
new JobProgressReporter(),
|
||||
false,
|
||||
CancellationToken.None);
|
||||
|
||||
|
||||
@@ -0,0 +1,66 @@
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Microsoft.VisualStudio.TestTools.UnitTesting;
|
||||
|
||||
using Moq;
|
||||
|
||||
namespace Tgstation.Server.Host.Jobs.Tests
|
||||
{
|
||||
[TestClass]
|
||||
public sealed class TestJobProgressReporter
|
||||
{
|
||||
string expectedStageName = null;
|
||||
double? expectedProgress = null;
|
||||
void Validate(string stageName, double? progress)
|
||||
{
|
||||
Assert.AreEqual(expectedStageName, stageName);
|
||||
Assert.AreEqual(expectedProgress, progress);
|
||||
}
|
||||
|
||||
JobProgressReporter Setup()
|
||||
{
|
||||
expectedStageName = null;
|
||||
expectedProgress = 0;
|
||||
return new JobProgressReporter(
|
||||
Mock.Of<ILogger<JobProgressReporter>>(),
|
||||
null,
|
||||
Validate);
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void TestBasicUsage()
|
||||
{
|
||||
var progressReporter = Setup();
|
||||
|
||||
expectedProgress = 0.4;
|
||||
progressReporter.ReportProgress(0.4);
|
||||
expectedProgress = 1.0;
|
||||
progressReporter.ReportProgress(1.0);
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void TestNestedUsage()
|
||||
{
|
||||
var progressReporter = Setup();
|
||||
|
||||
expectedStageName = "Test1";
|
||||
var subReporter1 = progressReporter.CreateSection("Test1", 0.5);
|
||||
expectedProgress = 0.1;
|
||||
subReporter1.ReportProgress(0.2);
|
||||
expectedProgress = 0.4;
|
||||
subReporter1.ReportProgress(0.8);
|
||||
|
||||
expectedStageName = "Test2";
|
||||
var subReporter2 = progressReporter.CreateSection("Test2", 0.5);
|
||||
|
||||
expectedStageName = "Test1";
|
||||
expectedProgress = 0.5;
|
||||
subReporter1.ReportProgress(1);
|
||||
|
||||
expectedStageName = "Test2";
|
||||
expectedProgress = 0.6;
|
||||
subReporter2.ReportProgress(0.2);
|
||||
expectedProgress = 1.0;
|
||||
subReporter2.ReportProgress(1);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -195,6 +195,8 @@ namespace Tgstation.Server.Host.Setup.Tests
|
||||
"y",
|
||||
// swarm config
|
||||
"n",
|
||||
// telemetry config
|
||||
"n",
|
||||
//saved, now for second run
|
||||
//this time use defaults amap
|
||||
String.Empty,
|
||||
@@ -234,6 +236,9 @@ namespace Tgstation.Server.Host.Setup.Tests
|
||||
"privatekey",
|
||||
"n",
|
||||
"http://controller.com",
|
||||
// telemetry config
|
||||
"y",
|
||||
"telemetry name",
|
||||
//third run, we already hit all the code coverage so just get through it
|
||||
String.Empty,
|
||||
nameof(DatabaseType.MariaDB),
|
||||
@@ -271,7 +276,9 @@ namespace Tgstation.Server.Host.Setup.Tests
|
||||
"https://controllerinternal.com",
|
||||
"https://controllerpublic.com",
|
||||
"privatekey",
|
||||
"y"
|
||||
"y",
|
||||
// telemetry config
|
||||
"n",
|
||||
};
|
||||
|
||||
var inputPos = 0;
|
||||
|
||||
@@ -7,7 +7,7 @@
|
||||
|
||||
<ItemGroup>
|
||||
<!-- Usage: Creating mock database implementations -->
|
||||
<PackageReference Include="Microsoft.EntityFrameworkCore.InMemory" Version="8.0.7" />
|
||||
<PackageReference Include="Microsoft.EntityFrameworkCore.InMemory" Version="8.0.8" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
using System;
|
||||
using System.Net.Http.Headers;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
using Microsoft.Extensions.Logging;
|
||||
@@ -57,14 +58,14 @@ namespace Tgstation.Server.Host.Utils.GitHub.Tests
|
||||
mockOptions.SetupGet(x => x.Value).Returns(gc);
|
||||
var factory = new GitHubClientFactory(mockApp.Object, loggerFactory.CreateLogger<GitHubClientFactory>(), mockOptions.Object);
|
||||
|
||||
var client = factory.CreateClient();
|
||||
var client = await factory.CreateClient(CancellationToken.None);
|
||||
Assert.IsNotNull(client);
|
||||
var credentials = await client.Connection.CredentialStore.GetCredentials();
|
||||
|
||||
Assert.AreEqual(AuthenticationType.Anonymous, credentials.AuthenticationType);
|
||||
|
||||
gc.GitHubAccessToken = "asdfasdfasdfasdfasdfasdf";
|
||||
client = factory.CreateClient();
|
||||
client = await factory.CreateClient(CancellationToken.None);
|
||||
Assert.IsNotNull(client);
|
||||
credentials = await client.Connection.CredentialStore.GetCredentials();
|
||||
|
||||
@@ -83,9 +84,9 @@ namespace Tgstation.Server.Host.Utils.GitHub.Tests
|
||||
mockOptions.SetupGet(x => x.Value).Returns(new GeneralConfiguration());
|
||||
var factory = new GitHubClientFactory(mockApp.Object, loggerFactory.CreateLogger<GitHubClientFactory>(), mockOptions.Object);
|
||||
|
||||
Assert.ThrowsException<ArgumentNullException>(() => factory.CreateClient(null));
|
||||
await Assert.ThrowsExceptionAsync<ArgumentNullException>(() => factory.CreateClient(null, CancellationToken.None).AsTask());
|
||||
|
||||
var client = factory.CreateClient("asdf");
|
||||
var client = await factory.CreateClient("asdf", CancellationToken.None);
|
||||
Assert.IsNotNull(client);
|
||||
|
||||
var credentials = await client.Connection.CredentialStore.GetCredentials();
|
||||
@@ -96,7 +97,7 @@ namespace Tgstation.Server.Host.Utils.GitHub.Tests
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void TestClientCaching()
|
||||
public async Task TestClientCaching()
|
||||
{
|
||||
var mockApp = new Mock<IAssemblyInformationProvider>();
|
||||
mockApp.SetupGet(x => x.ProductInfoHeaderValue).Returns(new ProductInfoHeaderValue("TGSTests", "1.2.3")).Verifiable();
|
||||
@@ -105,10 +106,10 @@ namespace Tgstation.Server.Host.Utils.GitHub.Tests
|
||||
mockOptions.SetupGet(x => x.Value).Returns(new GeneralConfiguration());
|
||||
var factory = new GitHubClientFactory(mockApp.Object, loggerFactory.CreateLogger<GitHubClientFactory>(), mockOptions.Object);
|
||||
|
||||
var client1 = factory.CreateClient();
|
||||
var client2 = factory.CreateClient("asdf");
|
||||
var client3 = factory.CreateClient();
|
||||
var client4 = factory.CreateClient("asdf");
|
||||
var client1 = await factory.CreateClient(CancellationToken.None);
|
||||
var client2 = await factory.CreateClient("asdf", CancellationToken.None);
|
||||
var client3 = await factory.CreateClient(CancellationToken.None);
|
||||
var client4 = await factory.CreateClient("asdf", CancellationToken.None);
|
||||
Assert.ReferenceEquals(client1, client3);
|
||||
Assert.ReferenceEquals(client2, client4);
|
||||
}
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
using System;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Microsoft.Extensions.Options;
|
||||
@@ -28,27 +30,29 @@ namespace Tgstation.Server.Host.Utils.GitHub.Tests
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void TestCreateService()
|
||||
public async Task TestCreateService()
|
||||
{
|
||||
var mockFactory = new Mock<IGitHubClientFactory>();
|
||||
|
||||
mockFactory.Setup(x => x.CreateClient()).Returns(Mock.Of<IGitHubClient>()).Verifiable();
|
||||
#pragma warning disable CA2012 // Use ValueTasks correctly
|
||||
mockFactory.Setup(x => x.CreateClient(It.IsAny<CancellationToken>())).Returns(ValueTask.FromResult(Mock.Of<IGitHubClient>())).Verifiable();
|
||||
|
||||
var mockToken = "asdf";
|
||||
mockFactory.Setup(x => x.CreateClient(mockToken)).Returns(Mock.Of<IGitHubClient>()).Verifiable();
|
||||
mockFactory.Setup(x => x.CreateClient(mockToken, It.IsAny<CancellationToken>())).Returns(ValueTask.FromResult(Mock.Of<IGitHubClient>())).Verifiable();
|
||||
#pragma warning restore CA2012 // Use ValueTasks correctly
|
||||
|
||||
var mockOptions = new Mock<IOptions<UpdatesConfiguration>>();
|
||||
mockOptions.SetupGet(x => x.Value).Returns(new UpdatesConfiguration());
|
||||
|
||||
var factory = new GitHubServiceFactory(mockFactory.Object, Mock.Of<ILoggerFactory>(), mockOptions.Object);
|
||||
|
||||
Assert.ThrowsException<ArgumentNullException>(() => factory.CreateService(null));
|
||||
await Assert.ThrowsExceptionAsync<ArgumentNullException>(() => factory.CreateService(null, CancellationToken.None).AsTask());
|
||||
Assert.AreEqual(0, mockFactory.Invocations.Count);
|
||||
|
||||
var result1 = factory.CreateService();
|
||||
var result1 = await factory.CreateService(CancellationToken.None);
|
||||
Assert.IsNotNull(result1);
|
||||
|
||||
var result2 = factory.CreateService(mockToken);
|
||||
var result2 = factory.CreateService(mockToken, CancellationToken.None);
|
||||
Assert.IsNotNull(result2);
|
||||
|
||||
mockFactory.VerifyAll();
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
using Microsoft.VisualStudio.TestTools.UnitTesting;
|
||||
using System;
|
||||
using System;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
using Microsoft.VisualStudio.TestTools.UnitTesting;
|
||||
|
||||
using Tgstation.Server.Api.Models;
|
||||
using Tgstation.Server.Api.Models.Response;
|
||||
using Tgstation.Server.Client;
|
||||
@@ -54,10 +54,24 @@ namespace Tgstation.Server.Tests.Live
|
||||
|
||||
async Task TestRead(CancellationToken cancellationToken)
|
||||
{
|
||||
var model = await client.Read(cancellationToken);
|
||||
var model = await client.Read(false, cancellationToken);
|
||||
|
||||
//we've released a few 5.x versions now, check the release checker is at least somewhat functional
|
||||
Assert.IsTrue(4 < model.LatestVersion.Major);
|
||||
Assert.IsNotNull(model.TrackedRepositoryUrl);
|
||||
Assert.IsTrue(model.GeneratedAt.HasValue);
|
||||
Assert.IsTrue(model.GeneratedAt.Value <= DateTimeOffset.UtcNow);
|
||||
|
||||
// test the cache
|
||||
var newerModel = await client.Read(false, cancellationToken);
|
||||
Assert.AreEqual(model.GeneratedAt, newerModel.GeneratedAt);
|
||||
|
||||
await Task.Delay(TimeSpan.FromSeconds(1), cancellationToken);
|
||||
|
||||
var newestModel = await client.Read(true, cancellationToken);
|
||||
Assert.AreNotEqual(model.GeneratedAt, newestModel.GeneratedAt);
|
||||
Assert.IsNotNull(newestModel.GeneratedAt);
|
||||
Assert.IsTrue(model.GeneratedAt < newestModel.GeneratedAt);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
using System;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
using Microsoft.Extensions.Logging;
|
||||
|
||||
@@ -18,13 +20,13 @@ namespace Tgstation.Server.Tests.Live
|
||||
this.logger = logger ?? throw new ArgumentNullException(nameof(logger));
|
||||
}
|
||||
|
||||
public IGitHubService CreateService() => CreateDummyService();
|
||||
public ValueTask<IGitHubService> CreateService(CancellationToken cancellationToken) => ValueTask.FromResult<IGitHubService>(CreateDummyService());
|
||||
|
||||
public IAuthenticatedGitHubService CreateService(string accessToken)
|
||||
public ValueTask<IAuthenticatedGitHubService> CreateService(string accessToken, CancellationToken cancellationToken)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(accessToken);
|
||||
|
||||
return CreateDummyService();
|
||||
return ValueTask.FromResult<IAuthenticatedGitHubService>(CreateDummyService());
|
||||
}
|
||||
|
||||
TestingGitHubService CreateDummyService() => new TestingGitHubService(cryptographySuite, logger);
|
||||
|
||||
@@ -22,6 +22,7 @@ using Tgstation.Server.Host.Components.Events;
|
||||
using Tgstation.Server.Host.Components.Repository;
|
||||
using Tgstation.Server.Host.Configuration;
|
||||
using Tgstation.Server.Host.IO;
|
||||
using Tgstation.Server.Host.Jobs;
|
||||
using Tgstation.Server.Host.System;
|
||||
using Tgstation.Server.Host.Utils;
|
||||
|
||||
@@ -142,7 +143,7 @@ namespace Tgstation.Server.Tests.Live.Instance
|
||||
using var windowsByondInstaller = byondInstaller as WindowsByondInstaller;
|
||||
|
||||
// get the bytes for stable
|
||||
return await byondInstaller.DownloadVersion(compatVersion, null, cancellationToken);
|
||||
return await byondInstaller.DownloadVersion(compatVersion, new JobProgressReporter(), cancellationToken);
|
||||
}
|
||||
|
||||
public async Task RunCompatTests(
|
||||
|
||||
@@ -262,7 +262,7 @@ namespace Tgstation.Server.Tests.Live.Instance
|
||||
Assert.AreEqual(HubConnectionState.Disconnected, permlessConn.State);
|
||||
|
||||
// force token refreshs
|
||||
await Task.WhenAll(permedUser.Administration.Read(cancellationToken).AsTask(), permlessUser.Instances.List(null, cancellationToken).AsTask());
|
||||
await Task.WhenAll(permedUser.Administration.Read(false, cancellationToken).AsTask(), permlessUser.Instances.List(null, cancellationToken).AsTask());
|
||||
|
||||
if (!permlessPsId.HasValue)
|
||||
{
|
||||
|
||||
@@ -116,6 +116,8 @@ namespace Tgstation.Server.Tests.Live.Instance
|
||||
await ApiAssert.ThrowsException<ApiConflictException, RepositoryResponse>(() => Checkout(new RepositoryUpdateRequest { Reference = "master", CheckoutSha = "286bb75" }, false, false, cancellationToken), ErrorCode.RepoMismatchShaAndReference);
|
||||
var updated = await Checkout(new RepositoryUpdateRequest { CheckoutSha = "286bb75" }, false, false, cancellationToken);
|
||||
|
||||
await RecloneTest(cancellationToken);
|
||||
|
||||
// Fake SHA
|
||||
updated = await Checkout(new RepositoryUpdateRequest { CheckoutSha = "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" }, true, false, cancellationToken);
|
||||
|
||||
@@ -142,6 +144,23 @@ namespace Tgstation.Server.Tests.Live.Instance
|
||||
await TestMergeTests(updated, prNumber, cancellationToken);
|
||||
}
|
||||
|
||||
async ValueTask RecloneTest(CancellationToken cancellationToken)
|
||||
{
|
||||
var initialState = await repositoryClient.Read(cancellationToken);
|
||||
Assert.IsNotNull(initialState.Reference);
|
||||
Assert.IsNotNull(initialState.RevisionInformation);
|
||||
Assert.IsNotNull(initialState.RevisionInformation.CommitSha);
|
||||
Assert.IsNotNull(initialState.RevisionInformation.OriginCommitSha);
|
||||
|
||||
var reclone = await repositoryClient.Reclone(cancellationToken);
|
||||
await WaitForJob(reclone.ActiveJob, 70, false, null, cancellationToken);
|
||||
|
||||
var newState = await repositoryClient.Read(cancellationToken);
|
||||
Assert.AreEqual(initialState.Reference, newState.Reference);
|
||||
Assert.AreEqual(initialState.RevisionInformation.CommitSha, newState.RevisionInformation.CommitSha);
|
||||
Assert.AreEqual(initialState.RevisionInformation.OriginCommitSha, newState.RevisionInformation.OriginCommitSha);
|
||||
}
|
||||
|
||||
async ValueTask<RepositoryResponse> Checkout(RepositoryUpdateRequest updated, bool expectFailure, bool isRef, CancellationToken cancellationToken)
|
||||
{
|
||||
var newRef = isRef ? updated.Reference : updated.CheckoutSha;
|
||||
|
||||
@@ -150,6 +150,7 @@ namespace Tgstation.Server.Tests.Live.Instance
|
||||
}, cancellationToken);
|
||||
|
||||
Assert.AreEqual<ushort?>(47, updated.OpenDreamTopicPort);
|
||||
Assert.IsFalse(updated.ImmediateMemoryUsage.HasValue);
|
||||
}
|
||||
catch (ConflictException ex) when (ex.ErrorCode == ErrorCode.PortNotAvailable)
|
||||
{
|
||||
@@ -721,6 +722,8 @@ namespace Tgstation.Server.Tests.Live.Instance
|
||||
await CheckDDPriority();
|
||||
Assert.AreEqual(false, daemonStatus.SoftRestart);
|
||||
Assert.AreEqual(false, daemonStatus.SoftShutdown);
|
||||
Assert.IsTrue(daemonStatus.ImmediateMemoryUsage.HasValue);
|
||||
Assert.AreNotEqual(0, daemonStatus.ImmediateMemoryUsage.Value);
|
||||
|
||||
await GracefulWatchdogShutdown(cancellationToken);
|
||||
|
||||
@@ -1365,6 +1368,9 @@ namespace Tgstation.Server.Tests.Live.Instance
|
||||
Assert.IsNotNull(daemonStatus.ActiveCompileJob);
|
||||
ValidateSessionId(daemonStatus, true);
|
||||
|
||||
Assert.IsTrue(daemonStatus.ImmediateMemoryUsage.HasValue);
|
||||
Assert.AreNotEqual(0, daemonStatus.ImmediateMemoryUsage.Value);
|
||||
|
||||
Assert.AreEqual(initialStatus.ActiveCompileJob.Id, daemonStatus.ActiveCompileJob.Id);
|
||||
var newerCompileJob = daemonStatus.StagedCompileJob;
|
||||
Assert.AreNotEqual(daemonStatus.ActiveCompileJob.EngineVersion, newerCompileJob.EngineVersion);
|
||||
|
||||
@@ -155,6 +155,7 @@ namespace Tgstation.Server.Tests.Live
|
||||
$"General:OpenDreamGitUrl={OpenDreamUrl}",
|
||||
$"Security:TokenExpiryMinutes=120", // timeouts are useless for us
|
||||
$"General:OpenDreamSuppressInstallOutput={TestingUtils.RunningInGitHubActions}",
|
||||
"Telemetry:DisableVersionReporting=true",
|
||||
};
|
||||
|
||||
swarmArgs = new List<string>();
|
||||
|
||||
@@ -216,7 +216,7 @@ namespace Tgstation.Server.Tests.Live
|
||||
};
|
||||
|
||||
var badClient = clientFactory.CreateFromToken(serverClient.Url, newToken);
|
||||
await ApiAssert.ThrowsException<UnauthorizedException, AdministrationResponse>(() => badClient.Administration.Read(cancellationToken));
|
||||
await ApiAssert.ThrowsException<UnauthorizedException, AdministrationResponse>(() => badClient.Administration.Read(false, cancellationToken));
|
||||
await ApiAssert.ThrowsException<UnauthorizedException, ServerInformationResponse>(() => badClient.ServerInformation(cancellationToken));
|
||||
}
|
||||
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user