Merge branch 'dev' of https://github.com/tgstation/tgstation-server into EFCoreUpdate

This commit is contained in:
Jordan Dominion
2025-03-15 09:16:00 -04:00
119 changed files with 5008 additions and 304 deletions
+5 -5
View File
@@ -36,7 +36,7 @@ on:
env:
TGS_DOTNET_VERSION: 8
OD_MIN_COMPAT_DOTNET_VERSION: 7
OD_DOTNET_VERSION: 8
OD_DOTNET_VERSION: 9
TGS_DOTNET_QUALITY: ga
TGS_WEBPANEL_NODE_VERSION: 20.x
TGS_TEST_GITHUB_TOKEN: ${{ secrets.LIVE_TESTS_TOKEN }}
@@ -2322,9 +2322,9 @@ jobs:
update-nix:
name: Update Nix SHA
needs: deploy-tgs
needs: changelog-regen
runs-on: ubuntu-latest
if: (!(cancelled() || failure())) && needs.deploy-tgs.result == 'success'
if: (!(cancelled() || failure())) && needs.changelog-regen.result == 'success'
steps:
- name: Install Native Packages # Name checked in rerunFlakyTests.js
run: |
@@ -2345,7 +2345,7 @@ jobs:
private-key: ${{ secrets.APP_PRIVATE_KEY }}
- name: Clone
run: git clone -b master --single-branch "https://git@github.com/tgstation/tgstation-server" --depth 1 .
run: git clone -b master --single-branch "https://tgstation-server-ci:${{ steps.app-token-generation.outputs.token }}@github.com/tgstation/tgstation-server" --depth 1 .
- name: Parse TGS version
run: echo "TGS_VERSION=$(xmlstarlet sel -N X="http://schemas.microsoft.com/developer/msbuild/2003" --template --value-of /X:Project/X:PropertyGroup/X:TgsCoreVersion build/Version.props)" >> $GITHUB_ENV
@@ -2378,7 +2378,7 @@ jobs:
run: git push "https://tgstation-server-ci:${{ steps.app-token-generation.outputs.token }}@github.com/tgstation/tgstation-server"
- name: Force Push Tags
run: git push -f --tags "https://tgstation-server-ci:${{ steps.app-token-generation.outputs.token }}@github.com/tgstation/tgstation-server"
run: git push -f origin tag tgstation-server-v${{ env.TGS_VERSION }}
changelog-regen:
name: Regenerate Changelog
+1 -1
View File
@@ -25,7 +25,7 @@ jobs:
if: github.event.pull_request.user.id != 49699333
uses: thollander/actions-comment-pull-request@24bffb9b452ba05a4f3f77933840a6a841d1b32b
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 and ci-pipeline.yml will not be reflected.
message: Thank you for contributing to ${{ github.event.pull_request.base.repo.name }}! The CI Pipeline 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 and ci-pipeline.yml will not be reflected.
github-token: ${{ steps.app-token-generation.outputs.token }}
- name: Comment on dependabot PR
+3
View File
@@ -21,6 +21,8 @@ artifacts/
/src/Tgstation.Server.Host/wwwroot
/src/Tgstation.Server.Host/--applicationName
/src/Tgstation.Server.Host/ClientApp
/src/Tgstation.Server.Host.Utils.GitLab.GraphQL/node_modules
/src/Tgstation.Server.Host.Utils.GitLab.GraphQL/schema.graphql
/tools/Tgstation.Server.ReleaseNotes/release_notes.md
launchSettings.json
release_notes.md
@@ -29,3 +31,4 @@ changelog.yml
*.sqlite3
packaging/
yarn-error.log*
.graphqlrc.json
+3 -1
View File
@@ -256,7 +256,9 @@ Create an `appsettings.Production.yml` file next to `appsettings.yml`. This will
- `General:ConfigVersion`: Suppresses warnings about out of date config versions. You should change this after updating TGS to one with a new config version. The current version can be found on the releases page for your server version.
- `General:MinimumPasswordLength`: Minimum password length requirement for database users
- `General:MinimumPasswordLength`: Minimum password length requirement for database users.
- `General:PrometheusPort`: Port Prometheus metrics are published on under /metrics. This can be set to the same value as the `ApiPort`, just note that accessing it does not require authentication.
- `General:ValidInstancePaths`: Array meant to limit the directories in which instances may be created.
+4 -1
View File
@@ -58,11 +58,12 @@ RUN export TGS_TELEMETRY_KEY_FILE="../../${TGS_TELEMETRY_KEY_FILE}" \
FROM mcr.microsoft.com/dotnet/aspnet:8.0-bookworm-slim
#needed for byond
#needed for byond, curl for healthchecks
RUN apt-get update \
&& apt-get install -y \
gcc-multilib \
gdb \
curl \
&& rm -rf /var/lib/apt/lists/*
EXPOSE 5000
@@ -78,4 +79,6 @@ COPY --from=build /repo/build/tgs.docker.sh tgs.sh
VOLUME ["/config_data", "/tgs_logs", "/app/lib"]
HEALTHCHECK --start-interval=60s CMD curl --fail http://localhost:5000/health || exit
ENTRYPOINT ["./tgs.sh"]
+4 -4
View File
@@ -3,7 +3,7 @@
<ItemGroup>
<!-- Usage: Code coverage collection -->
<PackageReference Include="coverlet.collector" Version="6.0.3">
<PackageReference Include="coverlet.collector" Version="6.0.4">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
@@ -13,14 +13,14 @@
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
<!-- Usage: Hard to say what exactly this is for, but not including it removes the test icon and breaks vstest.console.exe for some reason -->
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.12.0" Condition="'$(TgsTestNoSdk)' != 'true'" />
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.13.0" Condition="'$(TgsTestNoSdk)' != 'true'" />
<!-- Usage: Dependency mocking for tests -->
<!-- Pinned: Be VERY careful about updating https://github.com/moq/moq/issues/1372 -->
<PackageReference Include="Moq" Version="4.20.72" />
<!-- Usage: MSTest execution -->
<PackageReference Include="MSTest.TestAdapter" Version="3.7.0" />
<PackageReference Include="MSTest.TestAdapter" Version="3.8.2" />
<!-- Usage: MSTest asserts etc... -->
<PackageReference Include="MSTest.TestFramework" Version="3.7.0" />
<PackageReference Include="MSTest.TestFramework" Version="3.8.2" />
</ItemGroup>
</Project>
+4 -4
View File
@@ -3,12 +3,12 @@
<!-- Integration tests will ensure they match across the board -->
<Import Project="WebpanelVersion.props" />
<PropertyGroup>
<TgsCoreVersion>6.12.2</TgsCoreVersion>
<TgsConfigVersion>5.4.0</TgsConfigVersion>
<TgsRestVersion>10.12.0</TgsRestVersion>
<TgsCoreVersion>6.14.1</TgsCoreVersion>
<TgsConfigVersion>5.5.0</TgsConfigVersion>
<TgsRestVersion>10.12.1</TgsRestVersion>
<TgsGraphQLVersion>0.5.0</TgsGraphQLVersion>
<TgsCommonLibraryVersion>7.0.0</TgsCommonLibraryVersion>
<TgsApiLibraryVersion>17.0.0</TgsApiLibraryVersion>
<TgsApiLibraryVersion>17.0.1</TgsApiLibraryVersion>
<TgsClientVersion>20.0.0</TgsClientVersion>
<TgsDmapiVersion>7.3.1</TgsDmapiVersion>
<TgsInteropVersion>5.10.0</TgsInteropVersion>
+1 -1
View File
@@ -1 +1 @@
sha256-l4HG2/IYJO7YE12MkP8TGZtZt4/tMOSCP2lVzbKpvH0=
sha256-6Wkdb1U2Xmw239zKJTbKcStdAV/G7aMa2oeCsGaSucA=
+36 -6
View File
@@ -1,6 +1,7 @@
inputs@{
config,
lib,
systemdUtils,
nixpkgs,
pkgs,
writeShellScriptBin,
@@ -53,7 +54,7 @@ in
};
username = lib.mkOption {
type = lib.types.str;
type = lib.types.nonEmptyStr;
default = "tgstation-server";
description = ''
The name of the user used to execute tgstation-server.
@@ -61,7 +62,7 @@ in
};
groupname = lib.mkOption {
type = lib.types.str;
type = lib.types.nonEmptyStr;
default = "tgstation-server";
description = ''
The name of group the user used to execute tgstation-server will belong to.
@@ -69,7 +70,7 @@ in
};
home-directory = lib.mkOption {
type = lib.types.str;
type = lib.types.nonEmptyStr;
default = "/home/tgstation-server";
description = ''
The home directory of TGS. Should be persistent.
@@ -77,10 +78,10 @@ in
};
production-appsettings = lib.mkOption {
type = lib.types.lines;
type = lib.types.path;
default = '''';
description = ''
The contents of appsettings.Production.yml in the /etc/tgstation-server.d directory.
A formatted appsettings.Production.yml file.
'';
};
@@ -91,6 +92,22 @@ in
Extra PATH entries to add to the TGS process
'';
};
environmentFile = lib.mkOption {
type = lib.types.nullOr lib.types.path;
default = null;
description = ''
Environment file as defined in {manpage}`systemd.exec(5)`
'';
};
wants = lib.mkOption {
type = lib.types.listOf systemdUtils.lib.unitNameType;
default = [];
description = ''
Start the specified units when this unit is started.
'';
};
};
};
@@ -111,7 +128,7 @@ in
mode = "0644";
};
"tgstation-server.d/appsettings.Production.yml" = {
text = cfg.production-appsettings;
source = cfg.production-appsettings;
group = cfg.groupname;
mode = "0640";
};
@@ -125,6 +142,7 @@ in
systemd.services.tgstation-server = {
description = "tgstation-server";
serviceConfig = {
EnvironmentFile = lib.mkIf (cfg.environmentFile != null) cfg.environmentFile;
User = cfg.username;
Type = "notify-reload";
NotifyAccess = "all";
@@ -138,7 +156,19 @@ in
WatchdogSignal = "SIGTERM";
LogsDirectory = "tgstation-server";
};
reloadTriggers = [
(lib.mkIf (cfg.environmentFile != null) [ cfg.environmentFile ])
"/etc/tgstation.server.d/appsettings.Production.yml"
];
restartIfChanged = false; # So that the TGS service doesn't just get restarted whenever it's updated, and boots players
wantedBy = [ "multi-user.target" ];
after = [
"network.target"
"mysql.service"
"mariadb.service"
"postgresql.service"
"mssql-server.service"
];
};
};
}
+2 -1
View File
@@ -5,7 +5,8 @@
"documentPrivateElements": true,
"documentPrivateFields": true,
"excludeFromPunctuationCheck": [
"remarks"
"remarks",
"example"
]
},
"indentation": {
+1 -1
View File
@@ -244,7 +244,7 @@ namespace Tgstation.Server.Api
else
{
var auth = authorization.First();
var splits = new List<string>(auth.Split(' '));
var splits = new List<string>(auth?.Split(' ') ?? Enumerable.Empty<string>());
var scheme = splits.First();
if (String.IsNullOrWhiteSpace(scheme))
AddError(HeaderErrorTypes.AuthorizationInvalid, "Missing authentication scheme!");
@@ -14,6 +14,7 @@ namespace Tgstation.Server.Api.Models
/// For <see cref="ChatProvider.Irc"/>, it's the IRC channel name and optional password colon separated.
/// For <see cref="ChatProvider.Discord"/>, it's the stringified Discord channel snowflake.
/// </summary>
/// <example>124823852418</example>
[Required]
[StringLength(Limits.MaximumIndexableStringLength, MinimumLength = 1)]
public string? ChannelData { get; set; }
@@ -26,12 +26,14 @@ namespace Tgstation.Server.Api.Models
/// <summary>
/// The <see cref="System.Version"/> of the engine. Currently only valid when <see cref="Engine"/> is <see cref="EngineType.Byond"/>.
/// </summary>
/// <example>516.1651.0</example>
[ResponseOptions]
public Version? Version { get; set; }
/// <summary>
/// The git commit SHA of the engine. Currently only valid when <see cref="Engine"/> is <see cref="EngineType.OpenDream"/>.
/// </summary>
/// <example>caa1e1f400c8b6a535e03cff28cf57f919e9378c</example>
[ResponseOptions]
[StringLength(Limits.MaximumCommitShaLength, MinimumLength = Limits.MaximumCommitShaLength)]
public string? SourceSHA { get; set; }
@@ -8,6 +8,7 @@
/// <summary>
/// The ID of the entity.
/// </summary>
/// <example>1</example>
[RequestOptions(FieldPresence.Required)]
[RequestOptions(FieldPresence.Ignored, PutOnly = true)]
public virtual long? Id { get; set; }
@@ -16,6 +16,7 @@ namespace Tgstation.Server.Api.Models.Internal
/// <summary>
/// The time interval in minutes the chat bot attempts to reconnect if <see cref="Enabled"/> and disconnected. Must not be zero.
/// </summary>
/// <example>60</example>
[Required]
[Range(1, UInt32.MaxValue)]
public uint? ReconnectionInterval { get; set; }
@@ -23,6 +24,7 @@ namespace Tgstation.Server.Api.Models.Internal
/// <summary>
/// The maximum number of <see cref="ChatChannel"/>s the <see cref="ChatBotSettings"/> may contain.
/// </summary>
/// <example>5</example>
[Required]
public ushort? ChannelLimit { get; set; }
@@ -10,18 +10,21 @@ namespace Tgstation.Server.Api.Models.Internal
/// <summary>
/// If the <see cref="ChatChannel"/> is an admin channel.
/// </summary>
/// <example>false</example>
[Required]
public bool? IsAdminChannel { get; set; }
/// <summary>
/// If the <see cref="ChatChannel"/> is a watchdog channel.
/// </summary>
/// <example>false</example>
[Required]
public bool? IsWatchdogChannel { get; set; }
/// <summary>
/// If the <see cref="ChatChannel"/> is an updates channel.
/// </summary>
/// <example>false</example>
[Required]
public bool? IsUpdatesChannel { get; set; }
@@ -34,6 +37,7 @@ namespace Tgstation.Server.Api.Models.Internal
/// <summary>
/// A custom tag users can define to group channels together.
/// </summary>
/// <example>system-channel-only</example>
[ResponseOptions]
[StringLength(Limits.MaximumStringLength)]
public string? Tag { get; set; }
@@ -12,6 +12,7 @@ namespace Tgstation.Server.Api.Models.Internal
/// <summary>
/// The .dme file used for compilation.
/// </summary>
/// <example>tgstation.dme</example>
[Required]
public string? DmeName { get; set; }
@@ -36,6 +37,7 @@ namespace Tgstation.Server.Api.Models.Internal
/// <summary>
/// The DMAPI <see cref="Version"/>.
/// </summary>
/// <example>7.3.0</example>
[NotMapped]
[ResponseOptions]
public virtual Version? DMApiVersion { get; set; }
@@ -10,6 +10,7 @@ namespace Tgstation.Server.Api.Models.Internal
/// <summary>
/// An incrementing ID for representing current server execution.
/// </summary>
/// <example>1</example>
[ResponseOptions]
public long? SessionId { get; set; }
@@ -22,12 +23,14 @@ namespace Tgstation.Server.Api.Models.Internal
/// <summary>
/// The last known count of connected players. Requires <see cref="DreamDaemonLaunchParameters.HealthCheckSeconds"/> to not be 0 and a game server interop version >= 5.10.0 to populate.
/// </summary>
/// <example>30</example>
[ResponseOptions]
public uint? ClientCount { get; set; }
/// <summary>
/// If the server is undergoing a soft reset. This may be automatically set by changes to other fields.
/// </summary>
/// <example>false</example>
[ResponseOptions]
public bool? SoftRestart { get; set; }
@@ -11,6 +11,7 @@ namespace Tgstation.Server.Api.Models.Internal
/// <summary>
/// If the BYOND web client can be used to connect to the game server. No-op for <see cref="EngineType.OpenDream"/>.
/// </summary>
/// <example>false</example>
[Required]
[ResponseOptions]
public bool? AllowWebClient { get; set; }
@@ -25,6 +26,7 @@ namespace Tgstation.Server.Api.Models.Internal
/// <summary>
/// The <see cref="DreamDaemonVisibility"/> level of DreamDaemon. No-op for <see cref="EngineType.OpenDream"/>.
/// </summary>
/// <example>2</example>
[Required]
[ResponseOptions]
[EnumDataType(typeof(DreamDaemonVisibility))]
@@ -33,6 +35,7 @@ namespace Tgstation.Server.Api.Models.Internal
/// <summary>
/// The <see cref="DreamDaemonSecurity"/> level of DreamDaemon. No-op for <see cref="EngineType.OpenDream"/>.
/// </summary>
/// <example>1</example>
[Required]
[ResponseOptions]
[EnumDataType(typeof(DreamDaemonSecurity))]
@@ -41,6 +44,7 @@ namespace Tgstation.Server.Api.Models.Internal
/// <summary>
/// The port DreamDaemon uses. This should be publically accessible.
/// </summary>
/// <example>1337</example>
[Required]
[ResponseOptions]
[Range(1, UInt16.MaxValue)]
@@ -49,6 +53,7 @@ namespace Tgstation.Server.Api.Models.Internal
/// <summary>
/// The port used by <see cref="EngineType.OpenDream"/> for its topic port.
/// </summary>
/// <example>2337</example>
[Required]
[ResponseOptions]
public ushort? OpenDreamTopicPort { get; set; }
@@ -56,6 +61,7 @@ namespace Tgstation.Server.Api.Models.Internal
/// <summary>
/// The DreamDaemon startup timeout in seconds.
/// </summary>
/// <example>5</example>
[Required]
[ResponseOptions]
[Range(1, UInt32.MaxValue)]
@@ -64,6 +70,7 @@ namespace Tgstation.Server.Api.Models.Internal
/// <summary>
/// The number of seconds between each watchdog health check. 0 disables.
/// </summary>
/// <example>5</example>
[Required]
[ResponseOptions]
public uint? HealthCheckSeconds { get; set; }
@@ -78,6 +85,7 @@ namespace Tgstation.Server.Api.Models.Internal
/// <summary>
/// The timeout for sending and receiving BYOND topics in milliseconds.
/// </summary>
/// <example>500</example>
[Required]
[ResponseOptions]
[Range(1, UInt32.MaxValue)]
@@ -20,6 +20,7 @@ namespace Tgstation.Server.Api.Models.Internal
/// English description of the <see cref="Job"/>.
/// </summary>
/// <remarks>May not match the <see cref="System.ComponentModel.DescriptionAttribute"/> listed on the <see cref="Models.JobCode"/>.</remarks>
/// <example>Installing and configuring important objects.</example>
[Required]
public string? Description { get; set; }
@@ -50,6 +51,7 @@ namespace Tgstation.Server.Api.Models.Internal
/// <summary>
/// If the <see cref="Job"/> was cancelled.
/// </summary>
/// <example>false</example>
[Required]
public bool? Cancelled { get; set; }
@@ -10,26 +10,31 @@ namespace Tgstation.Server.Api.Models.Internal
/// <summary>
/// Minimum length of database user passwords.
/// </summary>
/// <example>20</example>
public uint MinimumPasswordLength { get; set; }
/// <summary>
/// The maximum number of <see cref="Instance"/>s allowed.
/// </summary>
/// <example>100</example>
public uint InstanceLimit { get; set; }
/// <summary>
/// The maximum number of users allowed.
/// </summary>
/// <example>100</example>
public uint UserLimit { get; set; }
/// <summary>
/// The maximum number of user groups allowed.
/// </summary>
/// <example>50</example>
public uint UserGroupLimit { get; set; }
/// <summary>
/// Limits the locations instances may be created or attached from.
/// </summary>
/// <example>["/home/tgstation-server/my-server-1", "/home/tgstation-server/my-server-2"]</example>
[ResponseOptions]
public List<string>? ValidInstancePaths { get; set; }
}
@@ -22,6 +22,7 @@ namespace Tgstation.Server.Api.Models.Internal
/// <summary>
/// The server's identifier.
/// </summary>
/// <example>myserver-us-east</example>
[Required]
public string? Identifier { get; set; }
@@ -10,6 +10,7 @@ namespace Tgstation.Server.Api.Models.Internal
/// <summary>
/// If the <see cref="SwarmServerResponse"/> is the controller.
/// </summary>
/// <example>false</example>
public bool Controller { get; set; }
/// <summary>
@@ -11,6 +11,7 @@ namespace Tgstation.Server.Api.Models.Internal
/// <summary>
/// The ID of the <see cref="TestMergeApiBase"/>.
/// </summary>
/// <example>1</example>
public long Id { get; set; }
/// <summary>
@@ -11,18 +11,21 @@ namespace Tgstation.Server.Api.Models.Internal
/// <summary>
/// The title of the test merge source.
/// </summary>
/// <example>Fixes and Breaks everything</example>
[Required]
public string? TitleAtMerge { get; set; }
/// <summary>
/// The body of the test merge source.
/// </summary>
/// <example># GitHub markdown\n\rI assume?</example>
[Required]
public string? BodyAtMerge { get; set; }
/// <summary>
/// The URL of the test merge source.
/// </summary>
/// <example>https://github.com/tgstation/tgstation/pull/31026</example>
[Required]
#pragma warning disable CA1056 // Uri properties should not be strings
public string? Url { get; set; }
@@ -31,6 +34,7 @@ namespace Tgstation.Server.Api.Models.Internal
/// <summary>
/// The author of the test merge source.
/// </summary>
/// <example>MrStonedOne</example>
[Required]
public string? Author { get; set; }
@@ -10,6 +10,7 @@ namespace Tgstation.Server.Api.Models.Internal
/// <summary>
/// The latest available version of the Tgstation.Server.Host assembly from the upstream repository. If <see cref="Version.Major"/> is less than 4 the update cannot be applied due to API changes.
/// </summary>
/// <example>1.0.0</example>
public Version? LatestVersion { get; set; }
/// <summary>
@@ -10,6 +10,7 @@ namespace Tgstation.Server.Api.Models
/// <summary>
/// The name of the entity represented by the <see cref="NamedEntity"/>.
/// </summary>
/// <example>MyThingyName</example>
[Required]
[RequestOptions(FieldPresence.Required, PutOnly = true)]
[StringLength(Limits.MaximumIndexableStringLength, MinimumLength = 1)]
@@ -10,6 +10,7 @@ namespace Tgstation.Server.Api.Models.Request
/// <summary>
/// Changes the version of tgstation-server to the given version from the upstream repository.
/// </summary>
/// <example>6.12.3</example>
[RequestOptions(FieldPresence.Required)]
public Version? NewVersion { get; set; }
@@ -12,6 +12,7 @@ namespace Tgstation.Server.Api.Models.Response
/// <summary>
/// The GitHub repository the server is built to receive updates from.
/// </summary>
/// <example>https://github.com/tgstation/tgstation</example>
public Uri? TrackedRepositoryUrl { get; set; }
}
}
@@ -24,6 +24,7 @@ namespace Tgstation.Server.Api.Models.Response
/// <summary>
/// The current <see cref="WatchdogStatus"/>.
/// </summary>
/// <example>2</example>
[EnumDataType(typeof(WatchdogStatus))]
[ResponseOptions]
public WatchdogStatus? Status { get; set; }
@@ -31,6 +32,7 @@ namespace Tgstation.Server.Api.Models.Response
/// <summary>
/// The current <see cref="DreamDaemonSecurity"/>. May be upgraded. due to requirements of <see cref="ActiveCompileJob"/>.
/// </summary>
/// <example>1</example>
[EnumDataType(typeof(DreamDaemonSecurity))]
[ResponseOptions]
public DreamDaemonSecurity? CurrentSecurity { get; set; }
@@ -38,6 +40,7 @@ namespace Tgstation.Server.Api.Models.Response
/// <summary>
/// The current <see cref="DreamDaemonVisibility"/>.
/// </summary>
/// <example>2</example>
[EnumDataType(typeof(DreamDaemonVisibility))]
[ResponseOptions]
public DreamDaemonVisibility? CurrentVisibility { get; set; }
@@ -45,24 +48,28 @@ namespace Tgstation.Server.Api.Models.Response
/// <summary>
/// The port the running <see cref="DreamDaemonResponse"/> instance is set to.
/// </summary>
/// <example>1337</example>
[ResponseOptions]
public ushort? CurrentPort { get; set; }
/// <summary>
/// The <see cref="EngineType.OpenDream"/> topic port the running <see cref="DreamDaemonResponse"/> instance is set to.
/// </summary>
/// <example>3000</example>
[ResponseOptions]
public ushort? CurrentTopicPort { get; set; }
/// <summary>
/// The webclient status the running <see cref="DreamDaemonResponse"/> instance is set to.
/// </summary>
/// <example>false</example>
[ResponseOptions]
public bool? CurrentAllowWebclient { get; set; }
/// <summary>
/// The amount of RAM in use by the game server in bytes.
/// </summary>
/// <example>1073741824</example>
[ResponseOptions]
public long? ImmediateMemoryUsage { get; set; }
}
@@ -13,16 +13,19 @@ namespace Tgstation.Server.Api.Models.Response
/// <summary>
/// The version of the API the server is using.
/// </summary>
/// <example>9.3.0</example>
public Version? ServerApiVersion { get; set; }
/// <summary>
/// A human readable description of the error.
/// A human-readable description of the error.
/// </summary>
/// <example>Oopsie woopsie, we did a fucky wucky!</example>
public string? Message { get; set; }
/// <summary>
/// Additional data associated with the error message.
/// </summary>
/// <example>Error at thing.app.dependency.class.function in line 32</example>
[ResponseOptions]
public string? AdditionalData { get; set; }
@@ -8,6 +8,7 @@
/// <summary>
/// The <see cref="EntityId.Id"/> of the <see cref="Instance"/>.
/// </summary>
/// <example>1</example>
public long? InstanceId { get; set; }
/// <summary>
@@ -24,12 +25,14 @@
/// <summary>
/// Optional progress between 0 and 100 inclusive.
/// </summary>
/// <example>25</example>
[ResponseOptions]
public int? Progress { get; set; }
/// <summary>
/// Optional description of the job's current .
/// Optional description of the job's current progress.
/// </summary>
/// <example>Doing something important</example>
[ResponseOptions]
public string? Stage { get; set; }
}
@@ -10,6 +10,7 @@ namespace Tgstation.Server.Api.Models.Response
/// <summary>
/// The name of the log file.
/// </summary>
/// <example>tgs-20250118.log</example>
public string? Name { get; set; }
/// <summary>
@@ -18,16 +18,19 @@ namespace Tgstation.Server.Api.Models.Response
/// <summary>
/// The total number of pages in the query.
/// </summary>
/// <example>5</example>
public int TotalPages { get; set; }
/// <summary>
/// The current size of pages in the query.
/// </summary>
/// <example>20</example>
public int PageSize { get; set; }
/// <summary>
/// The total items across all pages.
/// </summary>
/// <example>100</example>
public int TotalItems { get; set; }
}
}
@@ -11,16 +11,19 @@ namespace Tgstation.Server.Api.Models.Response
/// <summary>
/// The version of the host.
/// </summary>
/// <example>6.12.3</example>
public Version? Version { get; set; }
/// <summary>
/// The <see cref="Api"/> version of the host.
/// </summary>
/// <example>10.12.0</example>
public Version? ApiVersion { get; set; }
/// <summary>
/// The DMAPI interop version the server uses.
/// </summary>
/// <example>7.3.0</example>
public Version? DMApiVersion { get; set; }
/// <summary>
@@ -36,6 +39,7 @@ namespace Tgstation.Server.Api.Models.Response
/// <summary>
/// If there is a server update in progress.
/// </summary>
/// <example>false</example>
public bool UpdateInProgress { get; set; }
/// <summary>
@@ -12,6 +12,7 @@ namespace Tgstation.Server.Api.Models.Response
/// <summary>
/// The version of tgstation-server pending update.
/// </summary>
/// <example>6.12.3</example>
public Version NewVersion { get; }
/// <summary>
@@ -10,6 +10,7 @@ namespace Tgstation.Server.Api.Models.Response
/// <summary>
/// The value of the JWT.
/// </summary>
/// <example>eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxIiwibmJmIjoxNzM3MDkzNDgwLCJleHAiOjE3MzcwOTQzODAsImlhdCI6MTczNzA5MzQ4MCwiaXNzIjoiVGdzdGF0aW9uLlNlcnZlci5Ib3N0IiwiYXVkIjoiVGdzdGF0aW9uLlNlcnZlci5BcGkifQ.v64KX34_YOpH-HCbwqlx1p8u-MNbb4L6a9qEyXhcNcU</example>
public string? Bearer { get; set; }
/// <summary>
@@ -10,18 +10,21 @@ namespace Tgstation.Server.Api.Models
/// <summary>
/// The number of the test merge source.
/// </summary>
/// <example>31026</example>
public int Number { get; set; }
/// <summary>
/// The sha of the test merge revision to merge. If not specified, the latest commit from the source will be used.
/// </summary>
/// <example>caa1e1f400c8b6a535e03cff28cf57f919e9378c</example>
[Required]
[StringLength(40)]
[StringLength(Limits.MaximumCommitShaLength, MinimumLength = Limits.MaximumCommitShaLength)]
public virtual string? TargetCommitSha { get; set; }
/// <summary>
/// Optional comment about the test.
/// </summary>
/// <example>this will fix everything -Admin</example>
[ResponseOptions]
[StringLength(Limits.MaximumStringLength)]
public string? Comment { get; set; }
@@ -6,6 +6,7 @@
public class UserName : NamedEntity
{
/// <inheritdoc />
/// <example>Admin</example>
[RequestOptions(FieldPresence.Optional)]
public override string? Name
{
@@ -26,12 +26,11 @@
<ItemGroup>
<!-- Usage: HTTP constants reference -->
<PackageReference Include="Microsoft.AspNetCore.Http.Extensions" Version="2.2.0" />
<PackageReference Include="Microsoft.AspNetCore.Http.Extensions" Version="2.3.0" />
<!-- Usage: Decoding the 'nbf' property of JWTs -->
<PackageReference Include="Microsoft.IdentityModel.JsonWebTokens" Version="8.3.0" />
<PackageReference Include="Microsoft.IdentityModel.JsonWebTokens" Version="8.6.1" />
<!-- Usage: Data model annotating -->
<PackageReference Include="System.ComponentModel.Annotations" Version="5.0.0" />
<PackageReference Include="System.Text.Encodings.Web" Version="9.0.0" />
</ItemGroup>
<ItemGroup>
@@ -9,8 +9,8 @@
<ItemGroup>
<!-- GraphQL connector and code generator -->
<PackageReference Include="Microsoft.Bcl.AsyncInterfaces" Version="9.0.0" />
<PackageReference Include="StrawberryShake.Server" Version="14.3.0" />
<PackageReference Include="Microsoft.Bcl.AsyncInterfaces" Version="9.0.2" />
<PackageReference Include="StrawberryShake.Server" Version="15.0.3" />
</ItemGroup>
<ItemGroup>
@@ -11,9 +11,9 @@
<ItemGroup>
<!-- Usage: Connecting to SignalR hubs in API -->
<PackageReference Include="Microsoft.AspNetCore.SignalR.Client" Version="9.0.0" />
<PackageReference Include="Microsoft.AspNetCore.SignalR.Client" Version="9.0.2" />
<!-- Usage: Using target JSON serializer for API -->
<PackageReference Include="Microsoft.AspNetCore.SignalR.Protocols.NewtonsoftJson" Version="9.0.0" />
<PackageReference Include="Microsoft.AspNetCore.SignalR.Protocols.NewtonsoftJson" Version="9.0.2" />
</ItemGroup>
<ItemGroup>
@@ -13,9 +13,9 @@
<ItemGroup>
<!-- Usage: Identifying if we're running under SystemD -->
<PackageReference Include="Microsoft.Extensions.Hosting.Systemd" Version="9.0.0" />
<PackageReference Include="Microsoft.Extensions.Hosting.Systemd" Version="9.0.2" />
<!-- Usage: Console logging plugin -->
<PackageReference Include="Microsoft.Extensions.Logging.Console" Version="9.0.0" />
<PackageReference Include="Microsoft.Extensions.Logging.Console" Version="9.0.2" />
</ItemGroup>
<ItemGroup>
@@ -21,21 +21,21 @@
<!-- Usage: Command line argument support -->
<PackageReference Include="McMaster.Extensions.CommandLineUtils" Version="4.1.1" />
<!-- Usage: Identifies when we are running in the context of the Windows SCM -->
<PackageReference Include="Microsoft.Extensions.Hosting.WindowsServices" Version="9.0.0" />
<PackageReference Include="Microsoft.Extensions.Hosting.WindowsServices" Version="9.0.2" />
<!-- Usage: Windows event log logging plugin -->
<PackageReference Include="Microsoft.Extensions.Logging.EventLog" Version="9.0.0" />
<PackageReference Include="Microsoft.Extensions.Logging.EventLog" Version="9.0.2" />
<!-- Usage: Console logging plugin -->
<PackageReference Include="Microsoft.Extensions.Logging.Console" Version="9.0.0" />
<PackageReference Include="Microsoft.Extensions.Logging.Console" Version="9.0.2" />
<!-- Usage: Updated transitive dependency of Core.System.ServiceProcess -->
<PackageReference Include="Newtonsoft.Json" Version="13.0.3" />
<!-- Usage: Updated transitive dependency of Core.System.ServiceProcess -->
<PackageReference Include="System.Drawing.Common" Version="9.0.0" />
<PackageReference Include="System.Drawing.Common" Version="9.0.2" />
<!-- Usage: Updated transitive dependency, unable to tell what of -->
<PackageReference Include="System.Private.Uri" Version="4.3.2" />
<!-- Usage: OS identification -->
<PackageReference Include="System.Runtime.InteropServices" Version="4.3.0" />
<!-- Usage: Windows Service Manager intergration -->
<PackageReference Include="System.ServiceProcess.ServiceController" Version="9.0.0" />
<PackageReference Include="System.ServiceProcess.ServiceController" Version="9.0.2" />
</ItemGroup>
<ItemGroup>
@@ -0,0 +1,21 @@
{
"schema": "schema.graphql",
"documents": "**/*.graphql",
"extensions": {
"strawberryShake": {
"name": "GraphQLClient",
"url": "../../artifacts/gitlab-api.graphql",
"namespace": "Tgstation.Server.Host.Utils.GitLab.GraphQL",
"records": {
"inputs": false,
"entities": false
},
"transportProfiles": [
{
"default": "Http",
"subscription": "Http"
}
]
}
}
}
@@ -0,0 +1,12 @@
mutation CreateNote($id: NoteableID!, $body: String!) {
createNote(input: { noteableId: $id, body: $body }) {
note {
id
body
discussion {
id
}
}
errors
}
}
@@ -0,0 +1,9 @@
mutation ModifyNote($id: NoteID!, $body: String!) {
updateNote(input: { id: $id, body: $body }) {
note {
id
body
}
errors
}
}
@@ -0,0 +1,6 @@
query GetCurrentUser {
currentUser
{
username
}
}
@@ -0,0 +1,14 @@
query GetMergeRequest($project: ID!, $number: String!) {
project(fullPath: $project) {
mergeRequest(iid: $number) {
author { username }
description
title
diffHeadSha
mergeCommitSha
webUrl
iid
id
}
}
}
@@ -0,0 +1,15 @@
query GetMergeRequestNotes($project: ID!, $number: String!) {
project(fullPath: $project) {
mergeRequest(iid: $number) {
iid
id
notes {
nodes {
author { username }
body
id
}
}
}
}
}
@@ -0,0 +1,14 @@
query GetMergeRequests($project: ID!, $numbers: [String!]!) {
project(fullPath: $project) {
mergeRequests(iids: $numbers) {
nodes {
state
diffHeadSha
mergeCommitSha
closedAt
iid
id
}
}
}
}
@@ -0,0 +1,33 @@
using System;
using System.Threading.Tasks;
using Microsoft.Extensions.DependencyInjection;
namespace Tgstation.Server.Host.Utils.GitLab.GraphQL
{
/// <inheritdoc />
sealed class GraphQLGitLabClient : IGraphQLGitLabClient
{
/// <inheritdoc />
public IGraphQLClient GraphQL { get; }
/// <summary>
/// The <see cref="ServiceProvider"/> containing the <see cref="GraphQL"/> client.
/// </summary>
readonly ServiceProvider serviceProvider;
/// <summary>
/// Initializes a new instance of the <see cref="GraphQLGitLabClient"/> class.
/// </summary>
/// <param name="serviceProvider">The value of <see cref="serviceProvider"/>.</param>
public GraphQLGitLabClient(ServiceProvider serviceProvider)
{
this.serviceProvider = serviceProvider ?? throw new ArgumentNullException(nameof(serviceProvider));
GraphQL = serviceProvider.GetService<IGraphQLClient>() ?? throw new ArgumentException($"Expected an {nameof(IGraphQLClient)} service in the provider!", nameof(serviceProvider));
}
/// <inheritdoc />
public ValueTask DisposeAsync()
=> serviceProvider.DisposeAsync();
}
}
@@ -0,0 +1,49 @@
using System;
using System.Net.Http.Headers;
using System.Threading.Tasks;
using Microsoft.Extensions.DependencyInjection;
namespace Tgstation.Server.Host.Utils.GitLab.GraphQL
{
/// <summary>
/// Factory for creating <see cref="IGraphQLGitLabClient"/>s.
/// </summary>
public sealed class GraphQLGitLabClientFactory
{
/// <summary>
/// Sets up a <see cref="IGraphQLGitLabClient"/>.
/// </summary>
/// <param name="bearerToken">The token to use for authentication, if any.</param>
/// <returns>A <see cref="ValueTask{TResult}"/> resulting in a new <see cref="IGraphQLGitLabClient"/>.</returns>
public static async ValueTask<IGraphQLGitLabClient> CreateClient(string? bearerToken = null)
{
var serviceCollection = new ServiceCollection();
var clientBuilder = serviceCollection
.AddGraphQLClient();
var graphQLEndpoint = new Uri("https://gitlab.com/api/graphql");
clientBuilder.ConfigureHttpClient(
client =>
{
client.BaseAddress = new Uri("https://gitlab.com/api/graphql");
if (bearerToken != null)
{
client.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", bearerToken);
}
});
var serviceProvider = serviceCollection.BuildServiceProvider();
try
{
return new GraphQLGitLabClient(serviceProvider);
}
catch
{
await serviceProvider.DisposeAsync();
throw;
}
}
}
}
@@ -0,0 +1,15 @@
using System;
namespace Tgstation.Server.Host.Utils.GitLab.GraphQL
{
/// <summary>
/// Wrapper for using a GitLab <see cref="IGraphQLClient"/>.
/// </summary>
public interface IGraphQLGitLabClient : IAsyncDisposable
{
/// <summary>
/// Gets the underlying <see cref="IGraphQLClient"/>.
/// </summary>
IGraphQLClient GraphQL { get; }
}
}
@@ -0,0 +1,44 @@
<Project Sdk="Microsoft.NET.Sdk">
<Import Project="../../build/SrcCommon.props" />
<PropertyGroup>
<TargetFrameworks>$(TgsFrameworkVersion)</TargetFrameworks>
<Version>$(TgsCoreVersion)</Version>
<Nullable>enable</Nullable>
</PropertyGroup>
<Target Name="InstallApollo" Inputs="package.json;yarn.lock" Outputs="node_modules/.bin/apollo">
<Message Text="Installing Apollo..." Importance="high" />
<Exec Command="yarn install --immutable --ignore-engines" />
</Target>
<Target Name="GetApi" DependsOnTargets="InstallApollo" Inputs="node_modules/.bin/apollo" Outputs="../../artifacts/gitlab-api.graphql">
<Message Text="Fetching GitLab GraphQL API schema..." Importance="high" />
<MakeDir Directories="../../artifacts" />
<Exec Command="node_modules/.bin/apollo client:download-schema --endpoint=https://gitlab.com/api/graphql ../../artifacts/gitlab-api.graphql" />
</Target>
<!-- https://github.com/ChilliCream/graphql-platform/blob/c0c8df525ca0f47bf3b3b409a8b22cbe37f7a9c0/src/StrawberryShake/MetaPackages/Common/MSBuild/StrawberryShake.targets#L20 -->
<Target Name="ImportGraphQLApiSchema" DependsOnTargets="GetApi" BeforeTargets="_GraphQLCodeGenerationRoot" Inputs="../../artifacts/gitlab-api.graphql" Outputs="schema.graphql">
<Copy SkipUnchangedFiles="true" SourceFiles="../../artifacts/gitlab-api.graphql" DestinationFiles="schema.graphql" />
<WriteLinesToFile File="schema.graphql" Lines="$([System.IO.File]::ReadAllText('schema.graphql').Replace('\', ''))" Overwrite="true" Encoding="UTF-8" />
</Target>
<Target Name="FixWarningsInGeneratedSchema" AfterTargets="GenerateGraphQLCode">
<PropertyGroup>
<InputFile>$(IntermediateOutputPath)berry/GraphQLClient.Client.cs</InputFile>
<OutputFile>$(IntermediateOutputPath)berry/GraphQLClient.Client.cs</OutputFile>
</PropertyGroup>
<WriteLinesToFile File="$(OutputFile)" Lines="$([System.IO.File]::ReadAllText($(InputFile)).Replace('/ &lt;auto-generated/&gt;','/ &lt;auto-generated /&gt;%0d%0a#pragma warning disable'))" Overwrite="true" Encoding="Unicode" />
</Target>
<ItemGroup>
<PackageReference Include="StrawberryShake.Server" Version="15.0.3" />
</ItemGroup>
<ItemGroup>
<Folder Include="GQL\Queries\" />
<Folder Include="GQL\Mutations\" />
</ItemGroup>
</Project>
@@ -0,0 +1,6 @@
{
"devDependencies": {
"apollo": "^2.34.0"
},
"packageManager": "yarn@1.22.22+sha1.ac34549e6aa8e7ead463a7407e1c7390f61a6610"
}
File diff suppressed because it is too large Load Diff
@@ -10,7 +10,7 @@
<ItemGroup>
<!-- Usage: Logging abstractions -->
<PackageReference Include="Microsoft.Extensions.Logging" Version="9.0.0" />
<PackageReference Include="Microsoft.Extensions.Logging" Version="9.0.2" />
<!-- Usage: POSIX support for signals -->
<PackageReference Include="Mono.Posix.NETStandard" Version="1.0.0" />
</ItemGroup>
+17 -13
View File
@@ -177,6 +177,10 @@ namespace Tgstation.Server.Host.Watchdog
var watchdogVersion = executingAssembly.GetName().Version?.Semver().ToString();
var serializerOptions = new JsonSerializerOptions
{
WriteIndented = true,
};
while (!cancellationToken.IsCancellationRequested)
{
if (!File.Exists(assemblyPath))
@@ -214,10 +218,7 @@ namespace Tgstation.Server.Host.Watchdog
bootstrapperSettingsFile,
JsonSerializer.Serialize(
bootstrapSettings,
new JsonSerializerOptions
{
WriteIndented = true,
}),
serializerOptions),
cancellationToken);
}
}
@@ -258,6 +259,7 @@ namespace Tgstation.Server.Host.Watchdog
process.StartInfo.UseShellExecute = false; // runs in the same console
var killedHostProcess = false;
var createdShutdownFile = false;
try
{
Task? processTask = null;
@@ -279,6 +281,7 @@ namespace Tgstation.Server.Host.Watchdog
{
logger.LogInformation("Cancellation requested! Writing shutdown lock file...");
File.WriteAllBytes(updateDirectory, Array.Empty<byte>());
createdShutdownFile = true;
}
else
logger.LogWarning("Cancellation requested while update directory exists!");
@@ -330,15 +333,16 @@ namespace Tgstation.Server.Host.Watchdog
logger.LogWarning(ex2, "Error killing host process!");
}
try
{
if (File.Exists(updateDirectory))
File.Delete(updateDirectory);
}
catch (Exception ex2)
{
logger.LogWarning(ex2, "Error deleting comms file!");
}
if (createdShutdownFile)
try
{
if (File.Exists(updateDirectory))
File.Delete(updateDirectory);
}
catch (Exception ex2)
{
logger.LogWarning(ex2, "Error deleting comms file!");
}
logger.LogInformation("Host exited!");
}
@@ -377,6 +377,7 @@ namespace Tgstation.Server.Host.Components.Chat
/// <inheritdoc />
public Func<string?, string, Action<bool>> QueueDeploymentMessage(
Models.RevisionInformation revisionInformation,
Models.RevisionInformation? previousRevisionInformation,
EngineVersion engineVersion,
DateTimeOffset? estimatedCompletionTime,
string? gitHubOwner,
@@ -407,6 +408,7 @@ namespace Tgstation.Server.Host.Components.Chat
{
var callback = await provider.SendUpdateMessage(
revisionInformation,
previousRevisionInformation,
engineVersion,
estimatedCompletionTime,
gitHubOwner,
@@ -61,6 +61,7 @@ namespace Tgstation.Server.Host.Components.Chat
/// Send the message for a deployment to configured deployment channels.
/// </summary>
/// <param name="revisionInformation">The <see cref="RevisionInformation"/> of the deployment.</param>
/// <param name="previousRevisionInformation">The optional <see cref="RevisionInformation"/> of the previous deployment.</param>
/// <param name="engineVersion">The <see cref="Api.Models.EngineVersion"/> of the deployment.</param>
/// <param name="estimatedCompletionTime">The optional <see cref="DateTimeOffset"/> the deployment is expected to be completed at.</param>
/// <param name="gitHubOwner">The repository GitHub owner, if any.</param>
@@ -69,6 +70,7 @@ namespace Tgstation.Server.Host.Components.Chat
/// <returns>A <see cref="Func{T1, T2, TResult}"/> to call to update the message at the deployment's conclusion. Parameters: Error message if any, DreamMaker output if any. Returns an <see cref="Action"/> to call to mark the deployment as active/inactive. Parameter: If the deployment is being activated or inactivated.</returns>
Func<string?, string, Action<bool>> QueueDeploymentMessage(
Models.RevisionInformation revisionInformation,
Models.RevisionInformation? previousRevisionInformation,
Api.Models.EngineVersion engineVersion,
DateTimeOffset? estimatedCompletionTime,
string? gitHubOwner,
@@ -303,6 +303,7 @@ namespace Tgstation.Server.Host.Components.Chat.Providers
/// <inheritdoc />
public override async ValueTask<Func<string?, string, ValueTask<Func<bool, ValueTask>>>> SendUpdateMessage(
Models.RevisionInformation revisionInformation,
Models.RevisionInformation? previousRevisionInformation,
EngineVersion engineVersion,
DateTimeOffset? estimatedCompletionTime,
string? gitHubOwner,
@@ -316,7 +317,7 @@ namespace Tgstation.Server.Host.Components.Chat.Providers
localCommitPushed |= revisionInformation.CommitSha == revisionInformation.OriginCommitSha;
var fields = BuildUpdateEmbedFields(revisionInformation, engineVersion, gitHubOwner, gitHubRepo, localCommitPushed);
var fields = BuildUpdateEmbedFields(revisionInformation, previousRevisionInformation, engineVersion, gitHubOwner, gitHubRepo, localCommitPushed);
Optional<IEmbedAuthor> author = new EmbedAuthor(assemblyInformationProvider.VersionPrefix)
{
Url = "https://github.com/tgstation/tgstation-server",
@@ -900,6 +901,7 @@ namespace Tgstation.Server.Host.Components.Chat.Providers
/// Create a <see cref="List{T}"/> of <see cref="IEmbedField"/>s for a discord update embed.
/// </summary>
/// <param name="revisionInformation">The <see cref="RevisionInformation"/> of the deployment.</param>
/// <param name="previousRevisionInformation">The optional <see cref="RevisionInformation"/> of the previous deployment.</param>
/// <param name="engineVersion">The <see cref="EngineVersion"/> of the deployment.</param>
/// <param name="gitHubOwner">The repository GitHub owner, if any.</param>
/// <param name="gitHubRepo">The repository GitHub name, if any.</param>
@@ -907,6 +909,7 @@ namespace Tgstation.Server.Host.Components.Chat.Providers
/// <returns>A new <see cref="List{T}"/> of <see cref="IEmbedField"/>s to use.</returns>
List<IEmbedField> BuildUpdateEmbedFields(
Models.RevisionInformation revisionInformation,
Models.RevisionInformation? previousRevisionInformation,
EngineVersion engineVersion,
string? gitHubOwner,
string? gitHubRepo,
@@ -936,6 +939,31 @@ namespace Tgstation.Server.Host.Components.Chat.Providers
if (gitHubOwner == null || gitHubRepo == null)
return fields;
var previousTestMerges = (IEnumerable<RevInfoTestMerge>?)previousRevisionInformation?.ActiveTestMerges ?? Enumerable.Empty<RevInfoTestMerge>();
var currentTestMerges = (IEnumerable<RevInfoTestMerge>?)revisionInformation.ActiveTestMerges ?? Enumerable.Empty<RevInfoTestMerge>();
// determine what TMs were changed and how
var addedTestMerges = currentTestMerges
.Select(x => x.TestMerge)
.Where(x => !previousTestMerges
.Any(y => y.TestMerge.Number == x.Number))
.ToList();
var removedTestMerges = previousTestMerges
.Select(x => x.TestMerge)
.Where(x => !currentTestMerges
.Any(y => y.TestMerge.Number == x.Number))
.ToList();
var updatedTestMerges = currentTestMerges
.Select(x => x.TestMerge)
.Where(x => previousTestMerges
.Any(y => y.TestMerge.Number == x.Number && y.TestMerge.TargetCommitSha != x.TargetCommitSha))
.ToList();
var unchangedTestMerges = currentTestMerges
.Select(x => x.TestMerge)
.Where(x => previousTestMerges
.Any(y => y.TestMerge.Number == x.Number && y.TestMerge.TargetCommitSha == x.TargetCommitSha))
.ToList();
fields.Add(
new EmbedField(
"Local Commit",
@@ -952,13 +980,33 @@ namespace Tgstation.Server.Host.Components.Chat.Providers
: revisionOriginSha[..7],
true));
fields.AddRange((revisionInformation.ActiveTestMerges ?? Enumerable.Empty<RevInfoTestMerge>())
.Select(x => x.TestMerge)
fields.AddRange(addedTestMerges
.Select(x => new EmbedField(
$"#{x.Number} (Added)",
$"[{x.TitleAtMerge}]({x.Url}) by _[@{x.Author}](https://github.com/{x.Author})_{Environment.NewLine}Commit: [{x.TargetCommitSha![..7]}](https://github.com/{gitHubOwner}/{gitHubRepo}/commit/{x.TargetCommitSha}){(String.IsNullOrWhiteSpace(x.Comment) ? String.Empty : $"{Environment.NewLine}_**{x.Comment}**_")}",
false)));
fields.AddRange(updatedTestMerges
.Select(x => new EmbedField(
$"#{x.Number} (Updated)",
$"[{x.TitleAtMerge}]({x.Url}) by _[@{x.Author}](https://github.com/{x.Author})_{Environment.NewLine}Commit: [{x.TargetCommitSha![..7]}](https://github.com/{gitHubOwner}/{gitHubRepo}/commit/{x.TargetCommitSha}){(String.IsNullOrWhiteSpace(x.Comment) ? String.Empty : $"{Environment.NewLine}_**{x.Comment}**_")}",
false)));
fields.AddRange(unchangedTestMerges
.Select(x => new EmbedField(
$"#{x.Number}",
$"[{x.TitleAtMerge}]({x.Url}) by _[@{x.Author}](https://github.com/{x.Author})_{Environment.NewLine}Commit: [{x.TargetCommitSha![..7]}](https://github.com/{gitHubOwner}/{gitHubRepo}/commit/{x.TargetCommitSha}){(String.IsNullOrWhiteSpace(x.Comment) ? String.Empty : $"{Environment.NewLine}_**{x.Comment}**_")}",
false)));
if (removedTestMerges.Count != 0)
fields.Add(
new EmbedField(
"Removed:",
String.Join(
Environment.NewLine,
removedTestMerges
.Select(x => $"- #{x.Number} [{x.TitleAtMerge}]({x.Url}) by _[@{x.Author}](https://github.com/{x.Author})_"))));
return fields;
}
@@ -84,6 +84,7 @@ namespace Tgstation.Server.Host.Components.Chat.Providers
/// Send the message for a deployment.
/// </summary>
/// <param name="revisionInformation">The <see cref="RevisionInformation"/> of the deployment.</param>
/// <param name="previousRevisionInformation">The <see cref="RevisionInformation"/> of the previous deployment if any.</param>
/// <param name="engineVersion">The <see cref="Api.Models.EngineVersion"/> of the deployment.</param>
/// <param name="estimatedCompletionTime">The optional <see cref="DateTimeOffset"/> the deployment is expected to be completed at.</param>
/// <param name="gitHubOwner">The repository GitHub owner, if any.</param>
@@ -94,6 +95,7 @@ namespace Tgstation.Server.Host.Components.Chat.Providers
/// <returns>A <see cref="ValueTask{TResult}"/> resulting in a <see cref="Func{T1, T2, TResult}"/> to call to update the message at the deployment's conclusion. Parameters: Error message if any, DreamMaker output if any. Returns another callback which should be called to mark the deployment as active.</returns>
ValueTask<Func<string?, string, ValueTask<Func<bool, ValueTask>>>> SendUpdateMessage(
Models.RevisionInformation revisionInformation,
Models.RevisionInformation? previousRevisionInformation,
Api.Models.EngineVersion engineVersion,
DateTimeOffset? estimatedCompletionTime,
string? gitHubOwner,
@@ -17,6 +17,7 @@ using Tgstation.Server.Host.Configuration;
using Tgstation.Server.Host.Extensions;
using Tgstation.Server.Host.IO;
using Tgstation.Server.Host.Jobs;
using Tgstation.Server.Host.Models;
using Tgstation.Server.Host.System;
using Tgstation.Server.Host.Utils;
@@ -217,6 +218,7 @@ namespace Tgstation.Server.Host.Components.Chat.Providers
/// <inheritdoc />
public override async ValueTask<Func<string?, string, ValueTask<Func<bool, ValueTask>>>> SendUpdateMessage(
Models.RevisionInformation revisionInformation,
Models.RevisionInformation? previousRevisionInformation,
EngineVersion engineVersion,
DateTimeOffset? estimatedCompletionTime,
string? gitHubOwner,
@@ -227,8 +229,9 @@ namespace Tgstation.Server.Host.Components.Chat.Providers
{
ArgumentNullException.ThrowIfNull(revisionInformation);
ArgumentNullException.ThrowIfNull(engineVersion);
ArgumentNullException.ThrowIfNull(gitHubOwner);
ArgumentNullException.ThrowIfNull(gitHubRepo);
var previousTestMerges = (IEnumerable<RevInfoTestMerge>?)previousRevisionInformation?.ActiveTestMerges ?? Enumerable.Empty<RevInfoTestMerge>();
var currentTestMerges = (IEnumerable<RevInfoTestMerge>?)revisionInformation.ActiveTestMerges ?? Enumerable.Empty<RevInfoTestMerge>();
var commitInsert = revisionInformation.CommitSha![..7];
string remoteCommitInsert;
@@ -240,21 +243,35 @@ namespace Tgstation.Server.Host.Components.Chat.Providers
else
remoteCommitInsert = String.Format(CultureInfo.InvariantCulture, ". Remote commit: ^{0}", revisionInformation.OriginCommitSha![..7]);
var testmergeInsert = (revisionInformation.ActiveTestMerges?.Count ?? 0) == 0
var testmergeInsert = !currentTestMerges.Any()
? String.Empty
: String.Format(
CultureInfo.InvariantCulture,
" (Test Merges: {0})",
String.Join(
", ",
revisionInformation
.ActiveTestMerges!
currentTestMerges
.Select(x => x.TestMerge)
.Select(x =>
{
var result = String.Format(CultureInfo.InvariantCulture, "#{0} at {1}", x.Number, x.TargetCommitSha![..7]);
if (x.Comment != null)
result += String.Format(CultureInfo.InvariantCulture, " ({0})", x.Comment);
var status = string.Empty;
if (!previousTestMerges.Any(y => y.TestMerge.Number == x.Number))
status = "Added";
else if (previousTestMerges.Any(y => y.TestMerge.Number == x.Number && y.TestMerge.TargetCommitSha != x.TargetCommitSha))
status = "Updated";
var result = $"#{x.Number} at {x.TargetCommitSha![..7]}";
if (!string.IsNullOrEmpty(x.Comment))
{
if (!string.IsNullOrEmpty(status))
result += $" ({status} - {x.Comment})";
else
result += $" ({x.Comment})";
}
else if (!string.IsNullOrEmpty(status))
result += $" ({status})";
return result;
})));
@@ -199,6 +199,7 @@ namespace Tgstation.Server.Host.Components.Chat.Providers
/// <inheritdoc />
public abstract ValueTask<Func<string?, string, ValueTask<Func<bool, ValueTask>>>> SendUpdateMessage(
RevisionInformation revisionInformation,
RevisionInformation? previousRevisionInformation,
Api.Models.EngineVersion engineVersion,
DateTimeOffset? estimatedCompletionTime,
string? gitHubOwner,
@@ -8,6 +8,8 @@ using System.Threading.Tasks;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Logging;
using Prometheus;
using Tgstation.Server.Api.Models;
using Tgstation.Server.Api.Models.Internal;
using Tgstation.Server.Common.Extensions;
@@ -105,6 +107,21 @@ namespace Tgstation.Server.Host.Components.Deployment
/// </summary>
readonly Api.Models.Instance metadata;
/// <summary>
/// The number of attempted deployments.
/// </summary>
readonly Counter attemptedDeployments;
/// <summary>
/// The number of successful deployments.
/// </summary>
readonly Counter successfulDeployments;
/// <summary>
/// The number of failed deployments.
/// </summary>
readonly Counter failedDeployments;
/// <summary>
/// <see langword="lock"/> <see cref="object"/> for <see cref="deploying"/>.
/// </summary>
@@ -149,6 +166,7 @@ namespace Tgstation.Server.Host.Components.Deployment
/// <param name="repositoryManager">The value of <see cref="repositoryManager"/>.</param>
/// <param name="remoteDeploymentManagerFactory">The value of <see cref="remoteDeploymentManagerFactory"/>.</param>
/// <param name="asyncDelayer">The value of <see cref="asyncDelayer"/>.</param>
/// <param name="metricFactory">The <see cref="IMetricFactory"/> to use.</param>
/// <param name="logger">The value of <see cref="logger"/>.</param>
/// <param name="sessionConfiguration">The value of <see cref="sessionConfiguration"/>.</param>
/// <param name="metadata">The value of <see cref="metadata"/>.</param>
@@ -164,6 +182,7 @@ namespace Tgstation.Server.Host.Components.Deployment
IRepositoryManager repositoryManager,
IRemoteDeploymentManagerFactory remoteDeploymentManagerFactory,
IAsyncDelayer asyncDelayer,
IMetricFactory metricFactory,
ILogger<DreamMaker> logger,
SessionConfiguration sessionConfiguration,
Api.Models.Instance metadata)
@@ -177,12 +196,17 @@ namespace Tgstation.Server.Host.Components.Deployment
this.processExecutor = processExecutor ?? throw new ArgumentNullException(nameof(processExecutor));
this.compileJobConsumer = compileJobConsumer ?? throw new ArgumentNullException(nameof(compileJobConsumer));
this.repositoryManager = repositoryManager ?? throw new ArgumentNullException(nameof(repositoryManager));
this.asyncDelayer = asyncDelayer ?? throw new ArgumentNullException(nameof(asyncDelayer));
this.remoteDeploymentManagerFactory = remoteDeploymentManagerFactory ?? throw new ArgumentNullException(nameof(remoteDeploymentManagerFactory));
this.asyncDelayer = asyncDelayer ?? throw new ArgumentNullException(nameof(asyncDelayer));
ArgumentNullException.ThrowIfNull(metricFactory);
this.logger = logger ?? throw new ArgumentNullException(nameof(logger));
this.sessionConfiguration = sessionConfiguration ?? throw new ArgumentNullException(nameof(sessionConfiguration));
this.metadata = metadata ?? throw new ArgumentNullException(nameof(metadata));
successfulDeployments = metricFactory.CreateCounter("tgs_successful_deployments", "The number of deployments that have completed successfully");
failedDeployments = metricFactory.CreateCounter("tgs_failed_deployments", "The number of deployments that have failed");
attemptedDeployments = metricFactory.CreateCounter("tgs_total_deployments", "The number of deployments that have been attempted");
deploymentLock = new object();
}
@@ -205,9 +229,12 @@ namespace Tgstation.Server.Host.Components.Deployment
deploying = true;
}
attemptedDeployments.Inc();
currentChatCallback = null;
currentDreamMakerOutput = null;
Models.CompileJob? compileJob = null;
bool success = false;
try
{
string? repoOwner = null;
@@ -309,13 +336,17 @@ namespace Tgstation.Server.Host.Components.Deployment
}
});
var likelyPushedTestMergeCommit =
repositorySettings!.PushTestMergeCommits!.Value
&& repositorySettings.AccessToken != null
&& repositorySettings.AccessUser != null;
Models.CompileJob? oldCompileJob;
using (repo)
{
var likelyPushedTestMergeCommit =
repositorySettings!.PushTestMergeCommits!.Value
&& repositorySettings.AccessToken != null
&& repositorySettings.AccessUser != null;
oldCompileJob = await compileJobConsumer.LatestCompileJob();
compileJob = await Compile(
job,
oldCompileJob,
revInfo!,
dreamMakerSettings!,
ddSettings!,
@@ -325,8 +356,8 @@ namespace Tgstation.Server.Host.Components.Deployment
averageSpan,
likelyPushedTestMergeCommit,
cancellationToken);
}
var activeCompileJob = await compileJobConsumer.LatestCompileJob();
try
{
await databaseContextFactory.UseContext(
@@ -351,6 +382,7 @@ namespace Tgstation.Server.Host.Components.Deployment
{
var chatNotificationAction = currentChatCallback!(null, compileJob.Output!);
await compileJobConsumer.LoadCompileJob(compileJob, chatNotificationAction, cancellationToken);
success = true;
}
catch
{
@@ -374,7 +406,7 @@ namespace Tgstation.Server.Host.Components.Deployment
var commentsTask = remoteDeploymentManager!.PostDeploymentComments(
compileJob,
activeCompileJob?.RevisionInformation,
oldCompileJob?.RevisionInformation,
repositorySettings,
repoOwner,
repoName,
@@ -406,6 +438,10 @@ namespace Tgstation.Server.Host.Components.Deployment
finally
{
deploying = false;
if (success)
successfulDeployments.Inc();
else
failedDeployments.Inc();
}
}
#pragma warning restore CA1506
@@ -447,6 +483,7 @@ namespace Tgstation.Server.Host.Components.Deployment
/// Run the compile implementation.
/// </summary>
/// <param name="job">The currently running <see cref="Job"/>.</param>
/// <param name="oldCompileJob">The optional <see cref="CompileJob"/> of the previous deployment.</param>
/// <param name="revisionInformation">The <see cref="RevisionInformation"/>.</param>
/// <param name="dreamMakerSettings">The <see cref="Api.Models.Internal.DreamMakerSettings"/>.</param>
/// <param name="launchParameters">The <see cref="DreamDaemonLaunchParameters"/>.</param>
@@ -459,6 +496,7 @@ namespace Tgstation.Server.Host.Components.Deployment
/// <returns>A <see cref="ValueTask{TResult}"/> resulting in the completed <see cref="CompileJob"/>.</returns>
async ValueTask<Models.CompileJob> Compile(
Models.Job job,
Models.CompileJob? oldCompileJob,
Models.RevisionInformation revisionInformation,
Api.Models.Internal.DreamMakerSettings dreamMakerSettings,
DreamDaemonLaunchParameters launchParameters,
@@ -480,6 +518,7 @@ namespace Tgstation.Server.Host.Components.Deployment
using var engineLock = await engineManager.UseExecutables(null, null, cancellationToken);
currentChatCallback = chatManager.QueueDeploymentMessage(
revisionInformation,
oldCompileJob?.RevisionInformation,
engineLock.Version,
DateTimeOffset.UtcNow + estimatedDuration,
repository.RemoteRepositoryOwner,
@@ -18,6 +18,11 @@ namespace Tgstation.Server.Host.Components.Deployment.Remote
/// </summary>
abstract class BaseRemoteDeploymentManager : IRemoteDeploymentManager
{
/// <summary>
/// The header comment that begins every deployment message comment/note.
/// </summary>
public const string DeploymentMsgHeaderStart = "<!-- tgs_test_merge_comment -->";
/// <summary>
/// The <see cref="Api.Models.Instance"/> for the <see cref="BaseRemoteDeploymentManager"/>.
/// </summary>
@@ -66,36 +71,28 @@ namespace Tgstation.Server.Host.Components.Deployment.Remote
if (repositorySettings.AccessToken == null)
return;
var deployedRevisionInformation = compileJob.RevisionInformation;
if ((previousRevisionInformation != null && previousRevisionInformation.CommitSha == deployedRevisionInformation.CommitSha)
var revisionInformation = compileJob.RevisionInformation;
if ((previousRevisionInformation != null && previousRevisionInformation.CommitSha == revisionInformation.CommitSha)
|| !repositorySettings.PostTestMergeComment!.Value)
return;
previousRevisionInformation ??= new RevisionInformation();
previousRevisionInformation.ActiveTestMerges ??= new List<RevInfoTestMerge>();
var previousTestMerges = (IEnumerable<RevInfoTestMerge>?)previousRevisionInformation?.ActiveTestMerges ?? Enumerable.Empty<RevInfoTestMerge>();
var currentTestMerges = (IEnumerable<RevInfoTestMerge>?)revisionInformation.ActiveTestMerges ?? Enumerable.Empty<RevInfoTestMerge>();
deployedRevisionInformation.ActiveTestMerges ??= new List<RevInfoTestMerge>();
// added prs
var addedTestMerges = deployedRevisionInformation
.ActiveTestMerges
// determine what TMs were changed and how
var addedTestMerges = currentTestMerges
.Select(x => x.TestMerge)
.Where(x => !previousRevisionInformation
.ActiveTestMerges
.Where(x => !previousTestMerges
.Any(y => y.TestMerge.Number == x.Number))
.ToList();
var removedTestMerges = previousRevisionInformation
.ActiveTestMerges
var removedTestMerges = previousTestMerges
.Select(x => x.TestMerge)
.Where(x => !deployedRevisionInformation
.ActiveTestMerges
.Where(x => !currentTestMerges
.Any(y => y.TestMerge.Number == x.Number))
.ToList();
var updatedTestMerges = deployedRevisionInformation
.ActiveTestMerges
var updatedTestMerges = currentTestMerges
.Select(x => x.TestMerge)
.Where(x => previousRevisionInformation
.ActiveTestMerges
.Where(x => previousTestMerges
.Any(y => y.TestMerge.Number == x.Number))
.ToList();
@@ -133,7 +130,12 @@ namespace Tgstation.Server.Host.Components.Deployment.Remote
repositorySettings,
repoOwner,
repoName,
"#### Test Merge Removed",
FormatTestMergeRemoval(
repositorySettings,
compileJob,
removedTestMerge,
repoOwner,
repoName),
removedTestMerge.Number,
cancellationToken);
tasks.Add(removeCommentTask);
@@ -253,6 +255,22 @@ namespace Tgstation.Server.Host.Components.Deployment.Remote
string remoteRepositoryName,
bool updated);
/// <summary>
/// Formats a comment for a given <paramref name="testMerge"/> removal.
/// </summary>
/// <param name="repositorySettings">The <see cref="RepositorySettings"/> to use.</param>
/// <param name="compileJob">The test merge's <see cref="CompileJob"/>.</param>
/// <param name="testMerge">The <see cref="TestMerge"/>.</param>
/// <param name="remoteRepositoryOwner">The <see cref="Api.Models.Internal.IGitRemoteInformation.RemoteRepositoryOwner"/>.</param>
/// <param name="remoteRepositoryName">The <see cref="Api.Models.Internal.IGitRemoteInformation.RemoteRepositoryName"/>.</param>
/// <returns>A formatted <see cref="string"/> for posting a informative comment about the <paramref name="testMerge"/> removal.</returns>
protected abstract string FormatTestMergeRemoval(
RepositorySettings repositorySettings,
CompileJob compileJob,
TestMerge testMerge,
string remoteRepositoryOwner,
string remoteRepositoryName);
/// <summary>
/// Create a comment of a given <paramref name="testMergeNumber"/>'s source.
/// </summary>
@@ -282,7 +282,17 @@ namespace Tgstation.Server.Host.Components.Deployment.Remote
try
{
await gitHubService.CommentOnIssue(remoteRepositoryOwner, remoteRepositoryName, comment, testMergeNumber, cancellationToken);
string header = String.Format(CultureInfo.InvariantCulture, "{1}{0}## Test merge deployment history:{0}{0}", Environment.NewLine, DeploymentMsgHeaderStart);
var existingComment = await gitHubService.GetExistingCommentOnIssue(remoteRepositoryOwner, remoteRepositoryName, DeploymentMsgHeaderStart, testMergeNumber, cancellationToken);
if (existingComment != null)
{
await gitHubService.AppendCommentOnIssue(remoteRepositoryOwner, remoteRepositoryName, comment, existingComment, cancellationToken);
}
else
{
await gitHubService.CommentOnIssue(remoteRepositoryOwner, remoteRepositoryName, header + comment, testMergeNumber, cancellationToken);
}
}
catch (Exception ex) when (ex is not OperationCanceledException)
{
@@ -299,30 +309,43 @@ namespace Tgstation.Server.Host.Components.Deployment.Remote
string remoteRepositoryName,
bool updated) => String.Format(
CultureInfo.InvariantCulture,
"#### Test Merge {4}{0}{0}<details><summary>Details</summary>{0}{0}##### Server Instance{0}{5}{1}{0}{0}##### Revision{0}Origin: {6}{0}Pull Request: {2}{0}Server: {7}{3}{8}{0}</details>",
Environment.NewLine,
"<details><summary>Test Merge {4} @ {8}:</summary>{0}{0}##### Server Instance{0}{5}{1}{0}{0}##### Revision{0}Origin: {6}{0}Pull Request: {2}{0}Server: {7}{3}{0}</details>{0}",
Environment.NewLine, // 0
repositorySettings.ShowTestMergeCommitters!.Value
? String.Format(
CultureInfo.InvariantCulture,
"{0}{0}##### Merged By{0}{1}",
Environment.NewLine,
testMerge.MergedBy!.Name)
: String.Empty,
testMerge.TargetCommitSha,
testMerge.Comment != null
? String.Format(
: String.Empty, // 1
testMerge.TargetCommitSha, // 2
String.IsNullOrEmpty(testMerge.Comment)
? String.Empty
: String.Format(
CultureInfo.InvariantCulture,
"{0}{0}##### Comment{0}{1}",
Environment.NewLine,
testMerge.Comment)
: String.Empty,
updated ? "Updated" : "Deployed",
Metadata.Name,
compileJob.RevisionInformation.OriginCommitSha,
compileJob.RevisionInformation.CommitSha,
testMerge.Comment), // 3
updated ? "Updated" : "Deployed", // 4
compileJob.GitHubDeploymentId.HasValue
? $"{Environment.NewLine}[GitHub Deployments](https://github.com/{remoteRepositoryOwner}/{remoteRepositoryName}/deployments/activity_log?environment=TGS%3A+{Metadata.Name!.Replace(" ", "+", StringComparison.Ordinal)})"
: String.Empty);
? $"{Environment.NewLine}[{Metadata.Name}](https://github.com/{remoteRepositoryOwner}/{remoteRepositoryName}/deployments/activity_log?environment=TGS%3A+{Metadata.Name!.Replace(" ", "+", StringComparison.Ordinal)})"
: Metadata.Name, // 5
compileJob.RevisionInformation.OriginCommitSha, // 6
compileJob.RevisionInformation.CommitSha, // 7
compileJob.Job.StartedAt); // 8
/// <inheritdoc />
protected override string FormatTestMergeRemoval(
RepositorySettings repositorySettings,
CompileJob compileJob,
TestMerge testMerge,
string remoteRepositoryOwner,
string remoteRepositoryName) => String.Format(
CultureInfo.InvariantCulture,
"<details><summary>Test Merge Removed @ {2}:</summary>{0}{0}##### Server Instance{0}{1}{0}</details>{0}",
Environment.NewLine, // 0
Metadata.Name, // 1
compileJob.Job.StartedAt); // 2
/// <summary>
/// Update the deployment for a given <paramref name="compileJob"/>.
@@ -6,13 +6,13 @@ using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using GitLabApiClient;
using GitLabApiClient.Models.MergeRequests.Responses;
using GitLabApiClient.Models.Notes.Requests;
using Microsoft.Extensions.Logging;
using StrawberryShake;
using Tgstation.Server.Host.Components.Repository;
using Tgstation.Server.Host.Models;
using Tgstation.Server.Host.Utils.GitLab.GraphQL;
namespace Tgstation.Server.Host.Components.Deployment.Remote
{
@@ -52,49 +52,78 @@ namespace Tgstation.Server.Host.Components.Deployment.Remote
return Array.Empty<TestMerge>();
}
var client = repositorySettings.AccessToken != null
? new GitLabClient(GitLabRemoteFeatures.GitLabUrl, repositorySettings.AccessToken)
: new GitLabClient(GitLabRemoteFeatures.GitLabUrl);
var newList = revisionInformation.ActiveTestMerges.Select(x => x.TestMerge).ToList();
var tasks = revisionInformation
.ActiveTestMerges
.Select(x => client
.MergeRequests
.GetAsync(
$"{repository.RemoteRepositoryOwner}/{repository.RemoteRepositoryName}",
x.TestMerge.Number)
.WaitAsync(cancellationToken));
await using var client = await GraphQLGitLabClientFactory.CreateClient(repositorySettings.AccessToken);
IOperationResult<IGetMergeRequestsResult> operationResult;
try
{
await Task.WhenAll(tasks);
operationResult = await client.GraphQL.GetMergeRequests.ExecuteAsync(
$"{repository.RemoteRepositoryOwner}/{repository.RemoteRepositoryName}",
revisionInformation.ActiveTestMerges.Select(revInfoTestMerge => revInfoTestMerge.TestMerge.Number.ToString(CultureInfo.InvariantCulture)).ToList(),
cancellationToken);
operationResult.EnsureNoErrors();
}
catch (Exception ex) when (ex is not OperationCanceledException)
{
Logger.LogWarning(ex, "Merge requests update check failed!");
return newList;
}
var newList = revisionInformation.ActiveTestMerges.Select(x => x.TestMerge).ToList();
MergeRequest? lastMerged = null;
async ValueTask CheckRemoveMR(Task<MergeRequest> task)
var data = operationResult.Data?.Project?.MergeRequests?.Nodes;
if (data == null)
{
var mergeRequest = await task;
Logger.LogWarning("GitLab MergeRequests check returned null!");
return newList;
}
async ValueTask CheckRemoveMR(IGetMergeRequests_Project_MergeRequests_Nodes? mergeRequest)
{
if (mergeRequest == null)
{
Logger.LogWarning("GitLab MergeRequest node was null!");
return;
}
if (mergeRequest.State != MergeRequestState.Merged)
return;
// We don't just assume, actually check the repo contains the merge commit.
if (await repository.CommittishIsParent(mergeRequest.MergeCommitSha, cancellationToken))
var mergeCommitSha = mergeRequest.MergeCommitSha;
if (mergeCommitSha == null)
{
if (lastMerged == null || lastMerged.ClosedAt < mergeRequest.ClosedAt)
lastMerged = mergeRequest;
Logger.LogWarning("MergeRequest #{id} had no MergeCommitSha!", mergeRequest.Iid);
return;
}
var closedAtStr = mergeRequest.ClosedAt;
if (closedAtStr == null)
{
Logger.LogWarning("MergeRequest #{id} had no ClosedAt!", mergeRequest.Iid);
return;
}
if (!DateTimeOffset.TryParseExact(closedAtStr, "O", CultureInfo.InvariantCulture, DateTimeStyles.RoundtripKind, out DateTimeOffset closedAt))
{
Logger.LogWarning("MergeRequest #{id} had invalid ClosedAt: {closedAt}", mergeRequest.Iid, closedAtStr);
return;
}
if (!Int64.TryParse(mergeRequest.Iid, out long number))
{
Logger.LogWarning("MergeRequest #{id} is non-numeric!", mergeRequest.Iid);
return;
}
// We don't just assume, actually check the repo contains the merge commit.
if (await repository.CommittishIsParent(mergeCommitSha, cancellationToken))
newList.Remove(
newList.First(
potential => potential.Number == mergeRequest.Id));
}
potential => potential.Number == number));
}
foreach (var prTask in tasks)
await CheckRemoveMR(prTask);
foreach (var mergeRequest in data)
await CheckRemoveMR(mergeRequest);
return newList;
}
@@ -131,23 +160,69 @@ namespace Tgstation.Server.Host.Components.Deployment.Remote
int testMergeNumber,
CancellationToken cancellationToken)
{
var client = repositorySettings.AccessToken != null
? new GitLabClient(GitLabRemoteFeatures.GitLabUrl, repositorySettings.AccessToken)
: new GitLabClient(GitLabRemoteFeatures.GitLabUrl);
await using var client = await GraphQLGitLabClientFactory.CreateClient(repositorySettings.AccessToken);
try
{
await client
.MergeRequests
.CreateNoteAsync(
$"{remoteRepositoryOwner}/{remoteRepositoryName}",
testMergeNumber,
new CreateMergeRequestNoteRequest(comment))
.WaitAsync(cancellationToken);
string header = String.Format(CultureInfo.InvariantCulture, "{1}{0}## Test merge deployment history:{0}{0}", Environment.NewLine, DeploymentMsgHeaderStart);
// Try to find an existing note
var notesQueryResult = await client.GraphQL.GetMergeRequestNotes.ExecuteAsync(
$"{remoteRepositoryOwner}/{remoteRepositoryName}",
testMergeNumber.ToString(CultureInfo.InvariantCulture),
cancellationToken);
notesQueryResult.EnsureNoErrors();
var mergeRequest = notesQueryResult.Data?.Project?.MergeRequest;
if (mergeRequest == null)
{
Logger.LogWarning("GitLab GetMergeRequestNotes mergeRequest returned null!");
return;
}
var comments = mergeRequest.Notes?.Nodes;
IGetMergeRequestNotes_Project_MergeRequest_Notes_Nodes? existingComment = null;
if (comments != null)
{
for (int i = comments.Count - 1; i > -1; i--)
{
var currentComment = comments[i];
if (currentComment?.Author?.Username == repositorySettings.AccessUser && (currentComment?.Body?.StartsWith(DeploymentMsgHeaderStart) ?? false))
{
if (currentComment.Body.Length > 987856)
{ // Limit should be 999,999 so we'll leave a 12,143 buffer
break;
}
existingComment = currentComment;
break;
}
}
}
// Either amend or create the note
if (existingComment != null)
{
var noteModificationResult = await client.GraphQL.ModifyNote.ExecuteAsync(
existingComment.Id,
existingComment.Body + comment,
cancellationToken);
notesQueryResult.EnsureNoErrors();
}
else
{
var noteCreationResult = await client.GraphQL.CreateNote.ExecuteAsync(
mergeRequest.Id,
header + comment,
cancellationToken);
noteCreationResult.EnsureNoErrors();
}
}
catch (Exception ex) when (ex is not OperationCanceledException)
{
Logger.LogWarning(ex, "Error posting GitHub comment!");
Logger.LogWarning(ex, "Error posting GitLab comment!");
}
}
@@ -160,26 +235,40 @@ namespace Tgstation.Server.Host.Components.Deployment.Remote
string remoteRepositoryName,
bool updated) => String.Format(
CultureInfo.InvariantCulture,
"#### Test Merge {4}{0}{0}##### Server Instance{0}{5}{1}{0}{0}##### Revision{0}Origin: {6}{0}Merge Request: {2}{0}Server: {7}{3}",
Environment.NewLine,
"<details><summary>Test Merge {4} @ {8}</summary>{0}{0}##### Server Instance{0}{5}{1}{0}{0}##### Revision{0}Origin: {6}{0}Merge Request: {2}{0}Server: {7}{3}</details>{0}",
Environment.NewLine, // 0
repositorySettings.ShowTestMergeCommitters!.Value
? String.Format(
CultureInfo.InvariantCulture,
"{0}{0}##### Merged By{0}{1}",
Environment.NewLine,
testMerge.MergedBy!.Name)
: String.Empty,
testMerge.TargetCommitSha,
testMerge.Comment != null
? String.Format(
: String.Empty, // 1
testMerge.TargetCommitSha, // 2
String.IsNullOrEmpty(testMerge.Comment)
? String.Empty
: String.Format(
CultureInfo.InvariantCulture,
"{0}{0}##### Comment{0}{1}",
Environment.NewLine,
testMerge.Comment)
: String.Empty,
updated ? "Updated" : "Deployed",
Metadata.Name,
compileJob.RevisionInformation.OriginCommitSha,
compileJob.RevisionInformation.CommitSha);
testMerge.Comment), // 3
updated ? "Updated" : "Deployed", // 4
Metadata.Name, // 5
compileJob.RevisionInformation.OriginCommitSha, // 6
compileJob.RevisionInformation.CommitSha, // 7
compileJob.Job.StartedAt); // 8
/// <inheritdoc />
protected override string FormatTestMergeRemoval(
RepositorySettings repositorySettings,
CompileJob compileJob,
TestMerge testMerge,
string remoteRepositoryOwner,
string remoteRepositoryName) => String.Format(
CultureInfo.InvariantCulture,
"<details><summary>Test Merge Removed @ {2}:</summary>{0}{0}##### Server Instance{0}{1}{0}</details>{0}",
Environment.NewLine, // 0
Metadata.Name, // 1
compileJob.Job.StartedAt); // 2
}
}
@@ -65,6 +65,15 @@ namespace Tgstation.Server.Host.Components.Deployment.Remote
bool updated)
=> String.Empty;
/// <inheritdoc />
protected override string FormatTestMergeRemoval(
Models.RepositorySettings repositorySettings,
Models.CompileJob compileJob,
TestMerge testMerge,
string remoteRepositoryOwner,
string remoteRepositoryName)
=> String.Empty;
/// <inheritdoc />
protected override ValueTask MarkInactiveImpl(Models.CompileJob compileJob, CancellationToken cancellationToken) => ValueTask.CompletedTask;
@@ -173,5 +173,11 @@
/// </summary>
[EventScript("EngineInstallComplete")]
EngineInstallComplete,
/// <summary>
/// Before game server process is created. No parameters.
/// </summary>
[EventScript("DreamDaemonPreLaunch")]
DreamDaemonPreLaunch,
}
}
@@ -1,10 +1,14 @@
using System;
using System.Collections.Generic;
using System.Globalization;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options;
using Prometheus;
using Tgstation.Server.Host.Components.Chat;
using Tgstation.Server.Host.Components.Chat.Commands;
using Tgstation.Server.Host.Components.Deployment;
@@ -140,6 +144,11 @@ namespace Tgstation.Server.Host.Components
/// </summary>
readonly IDotnetDumpService dotnetDumpService;
/// <summary>
/// The <see cref="IMetricFactory"/> for the <see cref="InstanceFactory"/>.
/// </summary>
readonly IMetricFactory metricFactory;
/// <summary>
/// The <see cref="GeneralConfiguration"/> for the <see cref="InstanceFactory"/>.
/// </summary>
@@ -183,6 +192,7 @@ namespace Tgstation.Server.Host.Components
/// <param name="remoteDeploymentManagerFactory">The value of <see cref="remoteDeploymentManagerFactory"/>.</param>
/// <param name="asyncDelayer">The value of <see cref="asyncDelayer"/>.</param>
/// <param name="dotnetDumpService">The value of <see cref="dotnetDumpService"/>.</param>
/// <param name="metricFactory">The value of <see cref="metricFactory"/>.</param>
/// <param name="generalConfigurationOptions">The <see cref="IOptions{TOptions}"/> containing the value of <see cref="generalConfiguration"/>.</param>
/// <param name="sessionConfigurationOptions">The <see cref="IOptions{TOptions}"/> containing the value of <see cref="sessionConfiguration"/>.</param>
public InstanceFactory(
@@ -208,6 +218,7 @@ namespace Tgstation.Server.Host.Components
IRemoteDeploymentManagerFactory remoteDeploymentManagerFactory,
IAsyncDelayer asyncDelayer,
IDotnetDumpService dotnetDumpService,
IMetricFactory metricFactory,
IOptions<GeneralConfiguration> generalConfigurationOptions,
IOptions<SessionConfiguration> sessionConfigurationOptions)
{
@@ -233,6 +244,7 @@ namespace Tgstation.Server.Host.Components
this.remoteDeploymentManagerFactory = remoteDeploymentManagerFactory ?? throw new ArgumentNullException(nameof(remoteDeploymentManagerFactory));
this.asyncDelayer = asyncDelayer ?? throw new ArgumentNullException(nameof(asyncDelayer));
this.dotnetDumpService = dotnetDumpService ?? throw new ArgumentNullException(nameof(dotnetDumpService));
this.metricFactory = metricFactory ?? throw new ArgumentNullException(nameof(metricFactory));
generalConfiguration = generalConfigurationOptions?.Value ?? throw new ArgumentNullException(nameof(generalConfigurationOptions));
sessionConfiguration = sessionConfigurationOptions?.Value ?? throw new ArgumentNullException(nameof(sessionConfigurationOptions));
}
@@ -264,6 +276,13 @@ namespace Tgstation.Server.Host.Components
var diagnosticsIOManager = new ResolvingIOManager(instanceIoManager, "Diagnostics");
var configurationIoManager = new ResolvingIOManager(instanceIoManager, "Configuration");
var metricFactory = this.metricFactory.WithLabels(
new Dictionary<string, string>
{
{ "instance_name", metadata.Name! },
{ "instance_id", metadata.Id!.Value.ToString(CultureInfo.InvariantCulture) },
});
var configuration = new StaticFiles.Configuration(
configurationIoManager,
synchronousIOManager,
@@ -323,6 +342,7 @@ namespace Tgstation.Server.Host.Components
eventConsumer,
asyncDelayer,
dotnetDumpService,
metricFactory,
loggerFactory,
loggerFactory.CreateLogger<SessionControllerFactory>(),
sessionConfiguration,
@@ -337,6 +357,7 @@ namespace Tgstation.Server.Host.Components
diagnosticsIOManager,
configuration, // watchdog doesn't need itself as an event consumer
remoteDeploymentManagerFactory,
metricFactory,
metadata,
metadata.DreamDaemonSettings!);
try
@@ -357,6 +378,7 @@ namespace Tgstation.Server.Host.Components
repoManager,
remoteDeploymentManagerFactory,
asyncDelayer,
metricFactory,
loggerFactory.CreateLogger<DreamMaker>(),
sessionConfiguration,
metadata);
@@ -9,6 +9,8 @@ using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options;
using Prometheus;
using Tgstation.Server.Api.Models;
using Tgstation.Server.Common;
using Tgstation.Server.Common.Extensions;
@@ -150,6 +152,11 @@ namespace Tgstation.Server.Host.Components
/// </summary>
readonly CancellationTokenSource shutdownCancellationTokenSource;
/// <summary>
/// The count of online instances.
/// </summary>
readonly Gauge onlineInstances;
/// <summary>
/// The original <see cref="IConsole.Title"/> of <see cref="console"/>.
/// </summary>
@@ -180,6 +187,8 @@ namespace Tgstation.Server.Host.Components
/// <param name="swarmServiceController">The value of <see cref="swarmServiceController"/>.</param>
/// <param name="console">The value of <see cref="console"/>.</param>
/// <param name="platformIdentifier">The value of <see cref="platformIdentifier"/>.</param>
/// <param name="metricFactory">The <see cref="IMetricFactory"/> used to create metrics.</param>
/// <param name="collectorRegistry">The <see cref="ICollectorRegistry"/> to use.</param>
/// <param name="generalConfigurationOptions">The <see cref="IOptions{TOptions}"/> containing the value of <see cref="generalConfiguration"/>.</param>
/// <param name="swarmConfigurationOptions">The <see cref="IOptions{TOptions}"/> containing the value of <see cref="swarmConfiguration"/>.</param>
/// <param name="internalConfigurationOptions">The <see cref="IOptions{TOptions}"/> containing the value of <see cref="internalConfiguration"/>.</param>
@@ -197,6 +206,8 @@ namespace Tgstation.Server.Host.Components
ISwarmServiceController swarmServiceController,
IConsole console,
IPlatformIdentifier platformIdentifier,
IMetricFactory metricFactory,
ICollectorRegistry collectorRegistry,
IOptions<GeneralConfiguration> generalConfigurationOptions,
IOptions<SwarmConfiguration> swarmConfigurationOptions,
IOptions<InternalConfiguration> internalConfigurationOptions,
@@ -214,6 +225,8 @@ namespace Tgstation.Server.Host.Components
this.swarmServiceController = swarmServiceController ?? throw new ArgumentNullException(nameof(swarmServiceController));
this.console = console ?? throw new ArgumentNullException(nameof(console));
this.platformIdentifier = platformIdentifier ?? throw new ArgumentNullException(nameof(platformIdentifier));
ArgumentNullException.ThrowIfNull(metricFactory);
ArgumentNullException.ThrowIfNull(collectorRegistry);
generalConfiguration = generalConfigurationOptions?.Value ?? throw new ArgumentNullException(nameof(generalConfigurationOptions));
swarmConfiguration = swarmConfigurationOptions?.Value ?? throw new ArgumentNullException(nameof(swarmConfigurationOptions));
internalConfiguration = internalConfigurationOptions?.Value ?? throw new ArgumentNullException(nameof(internalConfigurationOptions));
@@ -221,12 +234,21 @@ namespace Tgstation.Server.Host.Components
originalConsoleTitle = console.Title;
onlineInstances = metricFactory.CreateGauge("tgs_online_instances", "The total number of instances online");
instances = new Dictionary<long, ReferenceCountingContainer<IInstance, InstanceWrapper>>();
bridgeHandlers = new Dictionary<string, IBridgeHandler>();
readyTcs = new TaskCompletionSource();
instanceStateChangeSemaphore = new SemaphoreSlim(1);
startupCancellationTokenSource = new CancellationTokenSource();
shutdownCancellationTokenSource = new CancellationTokenSource();
collectorRegistry.AddBeforeCollectCallback(async cancellationToken =>
{
using (await SemaphoreSlimContext.Lock(instanceStateChangeSemaphore, cancellationToken))
foreach (var container in instances.Values)
container.Instance.Watchdog.RunMetricsScrape();
});
}
/// <inheritdoc />
@@ -396,6 +418,7 @@ namespace Tgstation.Server.Host.Components
finally
{
await container.Instance.DisposeAsync();
onlineInstances.Dec();
}
}
}
@@ -426,6 +449,8 @@ namespace Tgstation.Server.Host.Components
instances.Add(
instanceId,
new ReferenceCountingContainer<IInstance, InstanceWrapper>(instance));
onlineInstances.Inc();
}
catch (Exception ex)
{
@@ -52,7 +52,7 @@ namespace Tgstation.Server.Host.Components.Interop.Bridge
public ushort? TopicPort { get; set; }
/// <summary>
/// The <see cref="Bridge.CustomEventInvocation"/> being triggered.
/// The <see cref="CustomEventInvocation"/> being triggered.
/// </summary>
public CustomEventInvocation? EventInvocation { get; set; }
@@ -1,11 +1,14 @@
using System;
using System.Globalization;
using System.Threading;
using System.Threading.Tasks;
using GitLabApiClient;
using Microsoft.Extensions.Logging;
using StrawberryShake;
using Tgstation.Server.Api.Models;
using Tgstation.Server.Host.Utils.GitLab.GraphQL;
namespace Tgstation.Server.Host.Components.Repository
{
@@ -45,30 +48,25 @@ namespace Tgstation.Server.Host.Components.Repository
RepositorySettings repositorySettings,
CancellationToken cancellationToken)
{
var client = repositorySettings.AccessToken != null
? new GitLabClient(GitLabUrl, repositorySettings.AccessToken)
: new GitLabClient(GitLabUrl);
await using var client = await GraphQLGitLabClientFactory.CreateClient(repositorySettings.AccessToken);
try
{
var mr = await client
.MergeRequests
.GetAsync($"{RemoteRepositoryOwner}/{RemoteRepositoryName}", parameters.Number)
.WaitAsync(cancellationToken);
var operationResult = await client.GraphQL.GetMergeRequest.ExecuteAsync(
$"{RemoteRepositoryOwner}/{RemoteRepositoryName}",
parameters.Number.ToString(CultureInfo.InvariantCulture),
cancellationToken);
var revisionToUse = parameters.TargetCommitSha == null
|| mr.Sha.StartsWith(parameters.TargetCommitSha, StringComparison.OrdinalIgnoreCase)
? mr.Sha
: parameters.TargetCommitSha;
operationResult.EnsureNoErrors();
var mr = operationResult.Data?.Project?.MergeRequest ?? throw new InvalidOperationException("GitLab MergeRequest check returned null!");
return new Models.TestMerge
{
Author = mr.Author.Username,
Author = mr.Author?.Username,
BodyAtMerge = mr.Description,
TitleAtMerge = mr.Title,
Comment = parameters.Comment,
Number = parameters.Number,
TargetCommitSha = mr.Sha,
TargetCommitSha = mr.DiffHeadSha,
Url = mr.WebUrl,
};
}
@@ -199,7 +199,7 @@ namespace Tgstation.Server.Host.Components.Repository
var commitMessage = String.Format(
CultureInfo.InvariantCulture,
"TGS Test Merge (#{0}){1}{2}",
"TGS Test Merge (PR {0}){1}{2}",
testMergeParameters.Number,
testMergeParameters.Comment != null
? Environment.NewLine
@@ -519,6 +519,10 @@ namespace Tgstation.Server.Host.Components.Session
return process.CreateDump(outputFile, minidump, cancellationToken);
}
/// <inheritdoc />
public double MeasureProcessorTimeDelta()
=> process.MeasureProcessorTimeDelta();
/// <summary>
/// The <see cref="Task{TResult}"/> for <see cref="LaunchResult"/>.
/// </summary>
@@ -1171,14 +1175,26 @@ namespace Tgstation.Server.Host.Components.Session
{
try
{
await eventTask.Value;
Exception? exception;
try
{
await eventTask.Value;
exception = null;
}
catch (Exception ex)
{
exception = ex;
}
if (notifyCompletion.Value)
await SendCommand(
new TopicParameters(eventId),
cancellationToken);
else
else if (exception == null)
Logger.LogTrace("Finished custom event {eventId}, not sending notification.", eventId);
if (exception != null)
throw exception;
}
catch (OperationCanceledException ex)
{
@@ -1,6 +1,7 @@
using System;
using System.Collections.Generic;
using System.Globalization;
using System.Linq;
using System.Net.Sockets;
using System.Text;
using System.Threading;
@@ -8,6 +9,8 @@ using System.Threading.Tasks;
using Microsoft.Extensions.Logging;
using Prometheus;
using Tgstation.Server.Api.Models;
using Tgstation.Server.Api.Models.Internal;
using Tgstation.Server.Common.Extensions;
@@ -126,6 +129,16 @@ namespace Tgstation.Server.Host.Components.Session
/// </summary>
readonly SessionConfiguration sessionConfiguration;
/// <summary>
/// The number of sessions launched.
/// </summary>
readonly Counter sessionsLaunched;
/// <summary>
/// The time the current session was launched.
/// </summary>
readonly Gauge lastSessionLaunch;
/// <summary>
/// The <see cref="Api.Models.Instance"/> for the <see cref="SessionControllerFactory"/>.
/// </summary>
@@ -184,6 +197,7 @@ namespace Tgstation.Server.Host.Components.Session
/// <param name="eventConsumer">The value of <see cref="eventConsumer"/>.</param>
/// <param name="asyncDelayer">The value of <see cref="asyncDelayer"/>.</param>
/// <param name="dotnetDumpService">The value of <see cref="dotnetDumpService"/>.</param>
/// <param name="metricFactory">The <see cref="IMetricFactory"/> used to create metrics.</param>
/// <param name="loggerFactory">The value of <see cref="loggerFactory"/>.</param>
/// <param name="logger">The value of <see cref="logger"/>.</param>
/// <param name="sessionConfiguration">The value of <see cref="sessionConfiguration"/>.</param>
@@ -203,6 +217,7 @@ namespace Tgstation.Server.Host.Components.Session
IEventConsumer eventConsumer,
IAsyncDelayer asyncDelayer,
IDotnetDumpService dotnetDumpService,
IMetricFactory metricFactory,
ILoggerFactory loggerFactory,
ILogger<SessionControllerFactory> logger,
SessionConfiguration sessionConfiguration,
@@ -223,10 +238,14 @@ namespace Tgstation.Server.Host.Components.Session
this.eventConsumer = eventConsumer ?? throw new ArgumentNullException(nameof(eventConsumer));
this.asyncDelayer = asyncDelayer ?? throw new ArgumentNullException(nameof(asyncDelayer));
this.dotnetDumpService = dotnetDumpService ?? throw new ArgumentNullException(nameof(dotnetDumpService));
ArgumentNullException.ThrowIfNull(metricFactory);
this.loggerFactory = loggerFactory ?? throw new ArgumentNullException(nameof(loggerFactory));
this.logger = logger ?? throw new ArgumentNullException(nameof(logger));
this.sessionConfiguration = sessionConfiguration ?? throw new ArgumentNullException(nameof(sessionConfiguration));
this.instance = instance ?? throw new ArgumentNullException(nameof(instance));
sessionsLaunched = metricFactory.CreateCounter("tgs_sessions_launched", "The number of game server processes created");
lastSessionLaunch = metricFactory.CreateGauge("tgs_session_start_time", "The UTC unix timestamp the most recent session was started");
}
/// <inheritdoc />
@@ -367,6 +386,12 @@ namespace Tgstation.Server.Host.Components.Session
false,
apiValidate);
if (!apiValidate)
{
sessionsLaunched.Inc();
lastSessionLaunch.SetToCurrentTimeUtc();
}
return sessionController;
}
catch
@@ -516,6 +541,14 @@ namespace Tgstation.Server.Host.Components.Session
? logFilePath
: null);
// If this isnt a staging DD (From a Deployment), fire off events
if (!apiValidate)
await eventConsumer.HandleEvent(
EventType.DreamDaemonPreLaunch,
Enumerable.Empty<string?>(),
false,
cancellationToken);
var process = await processExecutor.LaunchProcess(
engineLock.ServerExePath,
dmbProvider.Directory,
@@ -539,7 +572,6 @@ namespace Tgstation.Server.Host.Components.Session
if (!engineLock.HasStandardOutput)
networkPromptReaper.RegisterProcess(process);
// If this isnt a staging DD (From a Deployment), fire off an event
if (!apiValidate)
await eventConsumer.HandleEvent(
EventType.DreamDaemonLaunch,
@@ -6,6 +6,8 @@ using System.Threading.Tasks;
using Microsoft.Extensions.Logging;
using Prometheus;
using Tgstation.Server.Api.Models;
using Tgstation.Server.Api.Models.Internal;
using Tgstation.Server.Host.Components.Chat;
@@ -63,6 +65,7 @@ namespace Tgstation.Server.Host.Components.Watchdog
/// <param name="diagnosticsIOManager">The 'Diagnostics' <see cref="IIOManager"/> for the <see cref="WatchdogBase"/>.</param>
/// <param name="eventConsumer">The <see cref="IEventConsumer"/> for the <see cref="WatchdogBase"/>.</param>
/// <param name="remoteDeploymentManagerFactory">The <see cref="IRemoteDeploymentManagerFactory"/> for the <see cref="WatchdogBase"/>.</param>
/// <param name="metricFactory">The <see cref="IMetricFactory"/> for the <see cref="WatchdogBase"/>.</param>
/// <param name="gameIOManager">The 'Game' <see cref="IIOManager"/> for the <see cref="WatchdogBase"/>.</param>
/// <param name="linkFactory">The value of <see cref="LinkFactory"/>.</param>
/// <param name="logger">The <see cref="ILogger"/> for the <see cref="WatchdogBase"/>.</param>
@@ -80,6 +83,7 @@ namespace Tgstation.Server.Host.Components.Watchdog
IIOManager diagnosticsIOManager,
IEventConsumer eventConsumer,
IRemoteDeploymentManagerFactory remoteDeploymentManagerFactory,
IMetricFactory metricFactory,
IIOManager gameIOManager,
IFilesystemLinkFactory linkFactory,
ILogger<AdvancedWatchdog> logger,
@@ -97,6 +101,7 @@ namespace Tgstation.Server.Host.Components.Watchdog
diagnosticsIOManager,
eventConsumer,
remoteDeploymentManagerFactory,
metricFactory,
gameIOManager,
logger,
initialLaunchParameters,
@@ -6,6 +6,8 @@ using System.Threading.Tasks;
using Microsoft.Extensions.Logging;
using Prometheus;
using Tgstation.Server.Api.Models.Internal;
using Tgstation.Server.Host.Components.Chat;
using Tgstation.Server.Host.Components.Deployment;
@@ -53,6 +55,7 @@ namespace Tgstation.Server.Host.Components.Watchdog
/// <param name="diagnosticsIOManager">The 'Diagnostics' <see cref="IIOManager"/> for the <see cref="WatchdogBase"/>.</param>
/// <param name="eventConsumer">The <see cref="IEventConsumer"/> for the <see cref="WatchdogBase"/>.</param>
/// <param name="remoteDeploymentManagerFactory">The <see cref="IRemoteDeploymentManagerFactory"/> for the <see cref="WatchdogBase"/>.</param>
/// <param name="metricFactory">The <see cref="IMetricFactory"/> for the <see cref="WatchdogBase"/>.</param>
/// <param name="gameIOManager">The 'Game' <see cref="IIOManager"/> for the <see cref="WatchdogBase"/>.</param>
/// <param name="logger">The <see cref="ILogger"/> for the <see cref="WatchdogBase"/>.</param>
/// <param name="initialLaunchParameters">The <see cref="DreamDaemonLaunchParameters"/> for the <see cref="WatchdogBase"/>.</param>
@@ -69,6 +72,7 @@ namespace Tgstation.Server.Host.Components.Watchdog
IIOManager diagnosticsIOManager,
IEventConsumer eventConsumer,
IRemoteDeploymentManagerFactory remoteDeploymentManagerFactory,
IMetricFactory metricFactory,
IIOManager gameIOManager,
ILogger<BasicWatchdog> logger,
DreamDaemonLaunchParameters initialLaunchParameters,
@@ -85,6 +89,7 @@ namespace Tgstation.Server.Host.Components.Watchdog
diagnosticsIOManager,
eventConsumer,
remoteDeploymentManagerFactory,
metricFactory,
gameIOManager,
logger,
initialLaunchParameters,
@@ -117,5 +117,10 @@ namespace Tgstation.Server.Host.Components.Watchdog
/// <param name="cancellationToken">The <see cref="CancellationToken"/> for the operation.</param>
/// <returns>A <see cref="ValueTask{TResult}"/> resulting in <see langword="true"/> if the broadcast succeeded., <see langword="false"/> otherwise.</returns>
ValueTask<bool> Broadcast(string message, CancellationToken cancellationToken);
/// <summary>
/// Callback to update transient metrics.
/// </summary>
void RunMetricsScrape();
}
}
@@ -1,4 +1,6 @@
using Tgstation.Server.Api.Models.Internal;
using Prometheus;
using Tgstation.Server.Api.Models.Internal;
using Tgstation.Server.Host.Components.Chat;
using Tgstation.Server.Host.Components.Deployment;
using Tgstation.Server.Host.Components.Deployment.Remote;
@@ -24,6 +26,7 @@ namespace Tgstation.Server.Host.Components.Watchdog
/// <param name="diagnosticsIOManager">The <see cref="IIOManager"/> pointing to the Diagnostics directory for the <see cref="IWatchdog"/>.</param>
/// <param name="eventConsumer">The <see cref="IEventConsumer"/> for the <see cref="IWatchdog"/>.</param>
/// <param name="remoteDeploymentManagerFactory">The <see cref="IRemoteDeploymentManagerFactory"/> for the <see cref="IWatchdog"/>.</param>
/// <param name="metricFactory">The <see cref="IMetricFactory"/> used to create metrics.</param>
/// <param name="instance">The <see cref="Instance"/> for the <see cref="IWatchdog"/>.</param>
/// <param name="settings">The initial <see cref="DreamDaemonSettings"/> for the <see cref="IWatchdog"/>.</param>
/// <returns>A new <see cref="IWatchdog"/>.</returns>
@@ -36,6 +39,7 @@ namespace Tgstation.Server.Host.Components.Watchdog
IIOManager diagnosticsIOManager,
IEventConsumer eventConsumer,
IRemoteDeploymentManagerFactory remoteDeploymentManagerFactory,
IMetricFactory metricFactory,
Api.Models.Instance instance,
DreamDaemonSettings settings);
}
@@ -5,6 +5,8 @@ using System.Threading.Tasks;
using Microsoft.Extensions.Logging;
using Prometheus;
using Tgstation.Server.Api.Models.Internal;
using Tgstation.Server.Host.Components.Chat;
using Tgstation.Server.Host.Components.Deployment;
@@ -43,6 +45,7 @@ namespace Tgstation.Server.Host.Components.Watchdog
/// <param name="diagnosticsIOManager">The <see cref="IIOManager"/> for the <see cref="WatchdogBase"/>.</param>
/// <param name="eventConsumer">The <see cref="IEventConsumer"/> for the <see cref="WatchdogBase"/>.</param>
/// <param name="remoteDeploymentManagerFactory">The <see cref="IRemoteDeploymentManagerFactory"/> for the <see cref="WatchdogBase"/>.</param>
/// <param name="metricFactory">The <see cref="IMetricFactory"/> for the <see cref="WatchdogBase"/>.</param>
/// <param name="gameIOManager">The <see cref="IIOManager"/> pointing to the game directory for the <see cref="AdvancedWatchdog"/>..</param>
/// <param name="linkFactory">The <see cref="IFilesystemLinkFactory"/> for the <see cref="AdvancedWatchdog"/>.</param>
/// <param name="logger">The <see cref="ILogger"/> for the <see cref="WatchdogBase"/>.</param>
@@ -61,6 +64,7 @@ namespace Tgstation.Server.Host.Components.Watchdog
IIOManager diagnosticsIOManager,
IEventConsumer eventConsumer,
IRemoteDeploymentManagerFactory remoteDeploymentManagerFactory,
IMetricFactory metricFactory,
IIOManager gameIOManager,
IFilesystemLinkFactory linkFactory,
ILogger<PosixWatchdog> logger,
@@ -79,6 +83,7 @@ namespace Tgstation.Server.Host.Components.Watchdog
diagnosticsIOManager,
eventConsumer,
remoteDeploymentManagerFactory,
metricFactory,
gameIOManager,
linkFactory,
logger,
@@ -4,6 +4,8 @@ using System.Runtime.Versioning;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options;
using Prometheus;
using Tgstation.Server.Api.Models.Internal;
using Tgstation.Server.Host.Components.Chat;
using Tgstation.Server.Host.Components.Deployment;
@@ -60,6 +62,7 @@ namespace Tgstation.Server.Host.Components.Watchdog
IIOManager diagnosticsIOManager,
IEventConsumer eventConsumer,
IRemoteDeploymentManagerFactory remoteDeploymentManagerFactory,
IMetricFactory metricFactory,
Api.Models.Instance instance,
DreamDaemonSettings settings)
=> new PosixWatchdog(
@@ -73,6 +76,7 @@ namespace Tgstation.Server.Host.Components.Watchdog
diagnosticsIOManager,
eventConsumer,
remoteDeploymentManagerFactory,
metricFactory,
gameIOManager,
LinkFactory,
LoggerFactory.CreateLogger<PosixWatchdog>(),
@@ -7,6 +7,8 @@ using System.Threading.Tasks;
using Microsoft.Extensions.Logging;
using Prometheus;
using Serilog.Context;
using Tgstation.Server.Api.Models;
@@ -123,6 +125,21 @@ namespace Tgstation.Server.Host.Components.Watchdog
/// </summary>
readonly SemaphoreSlim controllerDisposeSemaphore;
/// <summary>
/// The <see cref="Status"/> as a metric.
/// </summary>
readonly Gauge watchdogStatusMetric;
/// <summary>
/// Active session CPU usage as a metric.
/// </summary>
readonly Gauge cpuUsageMetric;
/// <summary>
/// <see cref="MemoryUsage"/> as a metric.
/// </summary>
readonly Gauge ramUsageMetric;
/// <summary>
/// The <see cref="IEventConsumer"/> that is not the <see cref="WatchdogBase"/>.
/// </summary>
@@ -201,6 +218,7 @@ namespace Tgstation.Server.Host.Components.Watchdog
/// <param name="diagnosticsIOManager">The value of <see cref="diagnosticsIOManager"/>.</param>
/// <param name="eventConsumer">The value of <see cref="EventConsumer"/>.</param>
/// <param name="remoteDeploymentManagerFactory">The value of <see cref="remoteDeploymentManagerFactory"/>.</param>
/// <param name="metricFactory">The <see cref="IMetricFactory"/> used to create metrics.</param>
/// <param name="gameIOManager">The value of <see cref="GameIOManager"/>.</param>
/// <param name="logger">The value of <see cref="Logger"/>.</param>
/// <param name="initialLaunchParameters">The initial value of <see cref="ActiveLaunchParameters"/>. May be modified.</param>
@@ -217,6 +235,7 @@ namespace Tgstation.Server.Host.Components.Watchdog
IIOManager diagnosticsIOManager,
IEventConsumer eventConsumer,
IRemoteDeploymentManagerFactory remoteDeploymentManagerFactory,
IMetricFactory metricFactory,
IIOManager gameIOManager,
ILogger<WatchdogBase> logger,
DreamDaemonLaunchParameters initialLaunchParameters,
@@ -232,6 +251,7 @@ namespace Tgstation.Server.Host.Components.Watchdog
this.diagnosticsIOManager = diagnosticsIOManager ?? throw new ArgumentNullException(nameof(diagnosticsIOManager));
this.eventConsumer = eventConsumer ?? throw new ArgumentNullException(nameof(eventConsumer));
this.remoteDeploymentManagerFactory = remoteDeploymentManagerFactory ?? throw new ArgumentNullException(nameof(remoteDeploymentManagerFactory));
ArgumentNullException.ThrowIfNull(metricFactory);
GameIOManager = gameIOManager ?? throw new ArgumentNullException(nameof(gameIOManager));
Logger = logger ?? throw new ArgumentNullException(nameof(logger));
ActiveLaunchParameters = initialLaunchParameters ?? throw new ArgumentNullException(nameof(initialLaunchParameters));
@@ -240,6 +260,12 @@ namespace Tgstation.Server.Host.Components.Watchdog
ArgumentNullException.ThrowIfNull(serverControl);
watchdogStatusMetric = metricFactory.CreateGauge(
"tgs_watchdog_status",
$"TGS Watchdog status: {(int)WatchdogStatus.Offline} = Offline, {(int)WatchdogStatus.Online} = Online, {(int)WatchdogStatus.Restoring} = Restoring, {(int)WatchdogStatus.DelayedRestart} = Delayed Restart");
cpuUsageMetric = metricFactory.CreateGauge("tgs_game_cpu_usage", "Estimated total CPU usage time for the game process from 0-1");
ramUsageMetric = metricFactory.CreateGauge("tgs_game_ram_usage", "Total used bytes of private memory for the game process");
chat.RegisterCommandHandler(this);
ActiveLaunchParameters = initialLaunchParameters;
@@ -272,6 +298,7 @@ namespace Tgstation.Server.Host.Components.Watchdog
await DisposeAndNullControllersImpl();
controllerDisposeSemaphore.Dispose();
monitorCts?.Dispose();
disposed = true;
}
@@ -501,6 +528,15 @@ namespace Tgstation.Server.Host.Components.Watchdog
return response != null && response.ErrorMessage == null;
}
/// <inheritdoc />
public void RunMetricsScrape()
{
watchdogStatusMetric.Set((int)Status);
var controller = GetActiveController();
ramUsageMetric.Set(controller?.MemoryUsage ?? 0);
cpuUsageMetric.Set(controller?.MeasureProcessorTimeDelta() ?? 0);
}
/// <inheritdoc />
async ValueTask IEventConsumer.HandleEvent(EventType eventType, IEnumerable<string?> parameters, bool deploymentPipeline, CancellationToken cancellationToken)
{
@@ -3,6 +3,8 @@
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options;
using Prometheus;
using Tgstation.Server.Api.Models.Internal;
using Tgstation.Server.Host.Components.Chat;
using Tgstation.Server.Host.Components.Deployment;
@@ -77,6 +79,7 @@ namespace Tgstation.Server.Host.Components.Watchdog
IIOManager diagnosticsIOManager,
IEventConsumer eventConsumer,
IRemoteDeploymentManagerFactory remoteDeploymentManagerfactory,
IMetricFactory metricFactory,
Api.Models.Instance instance,
DreamDaemonSettings settings)
=> new BasicWatchdog(
@@ -90,6 +93,7 @@ namespace Tgstation.Server.Host.Components.Watchdog
diagnosticsIOManager,
eventConsumer,
remoteDeploymentManagerfactory,
metricFactory,
gameIOManager,
LoggerFactory.CreateLogger<BasicWatchdog>(),
settings,
@@ -3,6 +3,8 @@ using System.Threading.Tasks;
using Microsoft.Extensions.Logging;
using Prometheus;
using Tgstation.Server.Api.Models;
using Tgstation.Server.Api.Models.Internal;
using Tgstation.Server.Host.Components.Chat;
@@ -35,6 +37,7 @@ namespace Tgstation.Server.Host.Components.Watchdog
/// <param name="diagnosticsIOManager">The <see cref="IIOManager"/> for the <see cref="WatchdogBase"/>.</param>
/// <param name="eventConsumer">The <see cref="IEventConsumer"/> for the <see cref="WatchdogBase"/>.</param>
/// <param name="remoteDeploymentManagerFactory">The <see cref="IRemoteDeploymentManagerFactory"/> for the <see cref="WatchdogBase"/>.</param>
/// <param name="metricFactory">The <see cref="IMetricFactory"/> for the <see cref="WatchdogBase"/>.</param>
/// <param name="gameIOManager">The <see cref="IIOManager"/> pointing to the game directory for the <see cref="AdvancedWatchdog"/>..</param>
/// <param name="linkFactory">The <see cref="IFilesystemLinkFactory"/> for the <see cref="AdvancedWatchdog"/>.</param>
/// <param name="logger">The <see cref="ILogger"/> for the <see cref="WatchdogBase"/>.</param>
@@ -52,6 +55,7 @@ namespace Tgstation.Server.Host.Components.Watchdog
IIOManager diagnosticsIOManager,
IEventConsumer eventConsumer,
IRemoteDeploymentManagerFactory remoteDeploymentManagerFactory,
IMetricFactory metricFactory,
IIOManager gameIOManager,
IFilesystemLinkFactory linkFactory,
ILogger<WindowsWatchdog> logger,
@@ -69,6 +73,7 @@ namespace Tgstation.Server.Host.Components.Watchdog
diagnosticsIOManager,
eventConsumer,
remoteDeploymentManagerFactory,
metricFactory,
gameIOManager,
linkFactory,
logger,
@@ -3,6 +3,8 @@
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options;
using Prometheus;
using Tgstation.Server.Api.Models.Internal;
using Tgstation.Server.Host.Components.Chat;
using Tgstation.Server.Host.Components.Deployment;
@@ -63,6 +65,7 @@ namespace Tgstation.Server.Host.Components.Watchdog
IIOManager diagnosticsIOManager,
IEventConsumer eventConsumer,
IRemoteDeploymentManagerFactory remoteDeploymentManagerFactory,
IMetricFactory metricFactory,
Api.Models.Instance instance,
DreamDaemonSettings settings)
=> new WindowsWatchdog(
@@ -76,6 +79,7 @@ namespace Tgstation.Server.Host.Components.Watchdog
diagnosticsIOManager,
eventConsumer,
remoteDeploymentManagerFactory,
metricFactory,
gameIOManager,
LinkFactory,
LoggerFactory.CreateLogger<WindowsWatchdog>(),
@@ -91,6 +91,11 @@ namespace Tgstation.Server.Host.Configuration
/// </summary>
public ushort ApiPort { get; set; }
/// <summary>
/// The port Prometheus metrics are published on, if any.
/// </summary>
public ushort? PrometheusPort { get; set; }
/// <summary>
/// A classic GitHub personal access token to use for bypassing rate limits on requests. Requires no scopes.
/// </summary>
@@ -7,12 +7,12 @@ using System.Reflection;
using System.Threading;
using System.Threading.Tasks;
using GitLabApiClient;
using Microsoft.AspNetCore.Mvc;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Logging;
using StrawberryShake;
using Tgstation.Server.Api;
using Tgstation.Server.Api.Models;
using Tgstation.Server.Api.Models.Request;
@@ -27,6 +27,7 @@ using Tgstation.Server.Host.Models;
using Tgstation.Server.Host.Security;
using Tgstation.Server.Host.Utils;
using Tgstation.Server.Host.Utils.GitHub;
using Tgstation.Server.Host.Utils.GitLab.GraphQL;
namespace Tgstation.Server.Host.Controllers
{
@@ -585,70 +586,84 @@ namespace Tgstation.Server.Host.Controllers
switch (remoteFeatures.RemoteGitProvider!.Value)
{
case RemoteGitProvider.GitHub:
var gitHubClient = await gitHubClientFactory.CreateClientForRepository(
{
var gitHubClient = await gitHubClientFactory.CreateClientForRepository(
model.AccessToken,
new RepositoryIdentifier(
remoteFeatures.RemoteRepositoryOwner!,
remoteFeatures.RemoteRepositoryName!),
cancellationToken);
if (gitHubClient == null)
{
return this.StatusCode(HttpStatusCode.FailedDependency, new ErrorMessageResponse(ErrorCode.RemoteApiError)
if (gitHubClient == null)
{
AdditionalData = "GitHub authentication failed!",
});
}
try
{
string username;
if (!model.AccessToken.StartsWith(Api.Models.RepositorySettings.TgsAppPrivateKeyPrefix))
{
var user = await gitHubClient.User.Current();
username = user.Login;
}
else
{
// we literally need to app auth again to get the damn bot username
var appClient = gitHubClientFactory.CreateAppClient(model.AccessToken)!;
var app = await appClient.GitHubApps.GetCurrent();
username = app.Name;
return this.StatusCode(HttpStatusCode.FailedDependency, new ErrorMessageResponse(ErrorCode.RemoteApiError)
{
AdditionalData = "GitHub authentication failed!",
});
}
if (username != model.AccessUser)
return Conflict(new ErrorMessageResponse(ErrorCode.RepoTokenUsernameMismatch));
}
catch (Exception ex)
{
return this.StatusCode(HttpStatusCode.FailedDependency, new ErrorMessageResponse(ErrorCode.RemoteApiError)
try
{
AdditionalData = $"GitHub Authentication Failure: {ex.Message}",
});
string username;
if (!model.AccessToken.StartsWith(Api.Models.RepositorySettings.TgsAppPrivateKeyPrefix))
{
var user = await gitHubClient.User.Current();
username = user.Login;
}
else
{
// we literally need to app auth again to get the damn bot username
var appClient = gitHubClientFactory.CreateAppClient(model.AccessToken)!;
var app = await appClient.GitHubApps.GetCurrent();
username = app.Name;
}
if (username != model.AccessUser)
return Conflict(new ErrorMessageResponse(ErrorCode.RepoTokenUsernameMismatch));
}
catch (Exception ex)
{
return this.StatusCode(HttpStatusCode.FailedDependency, new ErrorMessageResponse(ErrorCode.RemoteApiError)
{
AdditionalData = $"GitHub Authentication Failure: {ex.Message}",
});
}
break;
}
break;
case RemoteGitProvider.GitLab:
// need to abstract this eventually
var gitLabClient = new GitLabClient(GitLabRemoteFeatures.GitLabUrl, model.AccessToken);
try
{
var user = await gitLabClient.Users.GetCurrentSessionAsync();
if (user.Username != model.AccessUser)
return Conflict(new ErrorMessageResponse(ErrorCode.RepoTokenUsernameMismatch));
}
catch (Exception ex)
{
return this.StatusCode(HttpStatusCode.FailedDependency, new ErrorMessageResponse(ErrorCode.RemoteApiError)
// need to abstract this eventually
await using var gitLabClient = await GraphQLGitLabClientFactory.CreateClient(model.AccessToken);
try
{
AdditionalData = $"GitLab Authentication Failure: {ex.Message}",
});
var operationResult = await gitLabClient.GraphQL.GetCurrentUser.ExecuteAsync(cancellationToken);
operationResult.EnsureNoErrors();
var user = operationResult.Data?.CurrentUser;
if (user == null || user.Username != model.AccessUser)
{
return Conflict(new ErrorMessageResponse(ErrorCode.RepoTokenUsernameMismatch));
}
}
catch (Exception ex)
{
return this.StatusCode(HttpStatusCode.FailedDependency, new ErrorMessageResponse(ErrorCode.RemoteApiError)
{
AdditionalData = $"GitLab Authentication Failure: {ex.Message}",
});
}
break;
}
break;
case RemoteGitProvider.Unknown:
default:
Logger.LogWarning("RemoteGitProvider is {provider}, no auth check implemented!", remoteFeatures.RemoteGitProvider.Value);
break;
{
Logger.LogWarning("RemoteGitProvider is {provider}, no auth check implemented!", remoteFeatures.RemoteGitProvider.Value);
break;
}
}
return null;
+44 -1
View File
@@ -30,6 +30,8 @@ using Microsoft.Extensions.Options;
using Newtonsoft.Json;
using Prometheus;
using Serilog;
using Serilog.Events;
using Serilog.Formatting.Display;
@@ -294,11 +296,29 @@ namespace Tgstation.Server.Host.Core
services.AddHttpClient();
services.AddSingleton<IAbstractHttpClientFactory, AbstractHttpClientFactory>();
// configure metrics
var prometheusPort = postSetupServices.GeneralConfiguration.PrometheusPort;
services.AddSingleton<IMetricFactory>(_ => Metrics.DefaultFactory);
services.AddSingleton<ICollectorRegistry>(_ => Metrics.DefaultRegistry);
if (prometheusPort.HasValue && prometheusPort != postSetupServices.GeneralConfiguration.ApiPort)
services.AddMetricServer(options => options.Port = prometheusPort.Value);
services.UseHttpClientMetrics();
var healthChecksBuilder = services
.AddHealthChecks()
.ForwardToPrometheus();
// configure graphql
services
.AddScoped<GraphQL.Subscriptions.ITopicEventReceiver, ShutdownAwareTopicEventReceiver>()
.AddGraphQLServer()
.AddAuthorization()
.AddAuthorization(
options => options.AddPolicy(
TgsAuthorizeAttribute.PolicyName,
builder => builder.RequireRole(TgsAuthorizeAttribute.UserEnabledRole)))
.ModifyOptions(options =>
{
options.EnsureAllNodesCanBeResolved = true;
@@ -359,6 +379,9 @@ namespace Tgstation.Server.Host.Core
configureAction(builder, databaseConfig);
});
services.AddScoped<IDatabaseContext>(x => x.GetRequiredService<TContext>());
healthChecksBuilder
.AddDbContextCheck<TContext>();
}
// add the correct database context type
@@ -524,6 +547,7 @@ namespace Tgstation.Server.Host.Core
/// <param name="assemblyInformationProvider">The <see cref="IAssemblyInformationProvider"/>.</param>
/// <param name="controlPanelConfigurationOptions">The <see cref="IOptions{TOptions}"/> containing the <see cref="ControlPanelConfiguration"/> to use.</param>
/// <param name="generalConfigurationOptions">The <see cref="IOptions{TOptions}"/> containing the <see cref="GeneralConfiguration"/> to use.</param>
/// <param name="databaseConfigurationOptions">The <see cref="IOptions{TOptions}"/> containing the <see cref="DatabaseConfiguration"/> to use.</param>
/// <param name="swarmConfigurationOptions">The <see cref="IOptions{TOptions}"/> containing the <see cref="SwarmConfiguration"/> to use.</param>
/// <param name="internalConfigurationOptions">The <see cref="IOptions{TOptions}"/> containing the <see cref="InternalConfiguration"/> to use.</param>
/// <param name="logger">The <see cref="Microsoft.Extensions.Logging.ILogger"/> for the <see cref="Application"/>.</param>
@@ -535,6 +559,7 @@ namespace Tgstation.Server.Host.Core
IAssemblyInformationProvider assemblyInformationProvider,
IOptions<ControlPanelConfiguration> controlPanelConfigurationOptions,
IOptions<GeneralConfiguration> generalConfigurationOptions,
IOptions<DatabaseConfiguration> databaseConfigurationOptions,
IOptions<SwarmConfiguration> swarmConfigurationOptions,
IOptions<InternalConfiguration> internalConfigurationOptions,
ILogger<Application> logger)
@@ -549,11 +574,13 @@ namespace Tgstation.Server.Host.Core
var controlPanelConfiguration = controlPanelConfigurationOptions?.Value ?? throw new ArgumentNullException(nameof(controlPanelConfigurationOptions));
var generalConfiguration = generalConfigurationOptions?.Value ?? throw new ArgumentNullException(nameof(generalConfigurationOptions));
var databaseConfiguration = databaseConfigurationOptions?.Value ?? throw new ArgumentNullException(nameof(databaseConfigurationOptions));
var swarmConfiguration = swarmConfigurationOptions?.Value ?? throw new ArgumentNullException(nameof(swarmConfigurationOptions));
var internalConfiguration = internalConfigurationOptions?.Value ?? throw new ArgumentNullException(nameof(internalConfigurationOptions));
ArgumentNullException.ThrowIfNull(logger);
logger.LogDebug("Database provider: {provider}", databaseConfiguration.DatabaseType);
logger.LogDebug("Content Root: {contentRoot}", hostingEnvironment.ContentRootPath);
logger.LogTrace("Web Root: {webRoot}", hostingEnvironment.WebRootPath);
@@ -564,6 +591,9 @@ namespace Tgstation.Server.Host.Core
// Wrap exceptions in a 500 (ErrorMessage) response
applicationBuilder.UseServerErrorHandling();
// metrics capture
applicationBuilder.UseHttpMetrics();
// Add the X-Powered-By response header
applicationBuilder.UseServerBranding(assemblyInformationProvider);
@@ -685,6 +715,19 @@ namespace Tgstation.Server.Host.Core
.MapGraphQL(Routes.GraphQL)
.WithOptions(gqlOptions);
}
if (generalConfiguration.PrometheusPort.HasValue)
if (generalConfiguration.PrometheusPort == generalConfiguration.ApiPort)
{
endpoints.MapMetrics();
logger.LogDebug("Prometheus being hosted alongside server");
}
else
logger.LogDebug("Prometheus being hosted on port {prometheusPort}", generalConfiguration.PrometheusPort);
else
logger.LogTrace("Prometheus disabled");
endpoints.MapHealthChecks("/health");
});
// 404 anything that gets this far
@@ -65,6 +65,7 @@ namespace Tgstation.Server.Host.Extensions
applicationBuilder.ApplicationServices.GetRequiredService<IAssemblyInformationProvider>(),
applicationBuilder.ApplicationServices.GetRequiredService<IOptions<ControlPanelConfiguration>>(),
applicationBuilder.ApplicationServices.GetRequiredService<IOptions<GeneralConfiguration>>(),
applicationBuilder.ApplicationServices.GetRequiredService<IOptions<DatabaseConfiguration>>(),
applicationBuilder.ApplicationServices.GetRequiredService<IOptions<SwarmConfiguration>>(),
applicationBuilder.ApplicationServices.GetRequiredService<IOptions<InternalConfiguration>>(),
applicationBuilder.ApplicationServices.GetRequiredService<ILogger<Application>>());
@@ -38,7 +38,7 @@ namespace Tgstation.Server.Host.GraphQL
return error;
var errorBuilder = ErrorBuilder.FromError(error)
.RemoveException()
.SetException(null)
.ClearExtensions();
const string ErrorCodeFieldName = "errorCode";
@@ -9,6 +9,8 @@ using Microsoft.AspNetCore.SignalR;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Logging;
using Prometheus;
using Serilog.Context;
using Tgstation.Server.Api.Extensions;
@@ -67,6 +69,16 @@ namespace Tgstation.Server.Host.Jobs
/// </summary>
readonly TaskCompletionSource<IInstanceCoreProvider> activationTcs;
/// <summary>
/// Total number of jobs processed.
/// </summary>
readonly Counter processedJobs;
/// <summary>
/// Jobs currently running.
/// </summary>
readonly Gauge runningJobs;
/// <summary>
/// <see langword="lock"/> <see cref="object"/> for various operations.
/// </summary>
@@ -87,19 +99,25 @@ namespace Tgstation.Server.Host.Jobs
/// </summary>
/// <param name="hub">The value of <see cref="hub"/>.</param>
/// <param name="databaseContextFactory">The value of <see cref="databaseContextFactory"/>.</param>
/// <param name="metricFactory">The <see cref="IMetricFactory"/> to use.</param>
/// <param name="loggerFactory">The value of <see cref="loggerFactory"/>.</param>
/// <param name="logger">The value of <see cref="logger"/>.</param>
public JobService(
IConnectionMappedHubContext<JobsHub, IJobsHub> hub,
IDatabaseContextFactory databaseContextFactory,
IMetricFactory metricFactory,
ILoggerFactory loggerFactory,
ILogger<JobService> logger)
{
this.hub = hub ?? throw new ArgumentNullException(nameof(hub));
this.databaseContextFactory = databaseContextFactory ?? throw new ArgumentNullException(nameof(databaseContextFactory));
this.loggerFactory = loggerFactory ?? throw new ArgumentNullException(nameof(loggerFactory));
ArgumentNullException.ThrowIfNull(metricFactory);
this.logger = logger ?? throw new ArgumentNullException(nameof(logger));
runningJobs = metricFactory.CreateGauge("tgs_jobs_running", "The number of TGS jobs running across all instances");
processedJobs = metricFactory.CreateCounter("tgs_jobs_processed", "The number of TGS jobs that have run previously");
jobs = new Dictionary<long, JobHandler>();
hubUpdateActions = new Dictionary<long, Action>();
activationTcs = new TaskCompletionSource<IInstanceCoreProvider>();
@@ -363,6 +381,7 @@ namespace Tgstation.Server.Host.Jobs
{
var jid = job.Require(x => x.Id);
using (LogContext.PushProperty(SerilogContextHelper.JobIdContextProperty, jid))
using (runningJobs.TrackInProgress())
try
{
void LogException(Exception ex) => logger.LogDebug(ex, "Job {jobId} exited with error!", jid);
@@ -553,6 +572,8 @@ namespace Tgstation.Server.Host.Jobs
jobs.Remove(jid);
handler.Dispose();
}
processedJobs.Inc();
}
}
}
@@ -15,10 +15,15 @@ namespace Tgstation.Server.Host.Security
[AttributeUsage(AttributeTargets.Class | AttributeTargets.Method, AllowMultiple = true, Inherited = true)]
sealed class TgsAuthorizeAttribute : AuthorizeAttribute
{
/// <summary>
/// Policy used to apply global requirement of <see cref="UserEnabledRole"/>.
/// </summary>
public const string PolicyName = "Policy.UserEnabled";
/// <summary>
/// Role used to indicate access to the server is allowed.
/// </summary>
public const string UserEnabledRole = "Core.UserEnabled";
public const string UserEnabledRole = "Role.UserEnabled";
/// <summary>
/// Gets the <see cref="Api.Rights.RightsType"/> associated with the <see cref="TgsAuthorizeAttribute"/> if any.
@@ -130,8 +135,12 @@ namespace Tgstation.Server.Host.Security
private TgsAuthorizeAttribute(IEnumerable<string> roles)
{
var listRoles = roles.ToList();
listRoles.Add(UserEnabledRole);
Roles = String.Join(",", listRoles);
if (listRoles.Count != 0)
{
Roles = String.Join(",", listRoles);
}
Policy = PolicyName;
}
}
}
@@ -125,8 +125,12 @@ namespace Tgstation.Server.Host.Security
private TgsGraphQLAuthorizeAttribute(IEnumerable<string> roleNames)
{
var listRoles = roleNames.ToList();
listRoles.Add(TgsAuthorizeAttribute.UserEnabledRole);
Roles = [.. listRoles];
if (listRoles.Count != 0)
{
Roles = [.. listRoles];
}
Policy = TgsAuthorizeAttribute.PolicyName;
Apply = ApplyPolicy.Validation;
}
}
@@ -24,6 +24,12 @@ namespace Tgstation.Server.Host.System
/// </summary>
long? MemoryUsage { get; }
/// <summary>
/// Gets the estimated CPU usage fraction of the process based on the last time this was called.
/// </summary>
/// <returns>The CPU's estimated usage as a value between 0 and 1.</returns>
double MeasureProcessorTimeDelta();
/// <summary>
/// Set's the owned <see cref="global::System.Diagnostics.Process.PriorityClass"/> to a non-normal value.
/// </summary>
+53 -1
View File
@@ -87,10 +87,30 @@ namespace Tgstation.Server.Host.System
/// </summary>
readonly Task<string?>? readTask;
/// <summary>
/// <see langword="lock"/> object for measuring processor time usage.
/// </summary>
readonly object processTimeMeasureLock;
/// <summary>
/// The last time <see cref="MeasureProcessorTimeDelta"/> was called.
/// </summary>
DateTimeOffset lastProcessorMeasureTime;
/// <summary>
/// The last value of <see cref="global::System.Diagnostics.Process.TotalProcessorTime"/>.
/// </summary>
TimeSpan lastProcessorUsageTime;
/// <summary>
/// The last valid return value of <see cref="MeasureProcessorTimeDelta"/>.
/// </summary>
double lastProcessorUsageEstimation;
/// <summary>
/// If the <see cref="Process"/> was disposed.
/// </summary>
volatile int disposed;
int disposed;
/// <summary>
/// Initializes a new instance of the <see cref="Process"/> class.
@@ -123,6 +143,8 @@ namespace Tgstation.Server.Host.System
this.logger = logger ?? throw new ArgumentNullException(nameof(logger));
processTimeMeasureLock = new object();
Lifetime = WrapLifetimeTask();
if (preExisting)
@@ -147,6 +169,8 @@ namespace Tgstation.Server.Host.System
DefaultIOManager.BlockingTaskCreationOptions,
TaskScheduler.Current);
MeasureProcessorTimeDelta();
logger.LogTrace("Created process ID: {pid}", Id);
}
@@ -267,6 +291,34 @@ namespace Tgstation.Server.Host.System
return processFeatures.CreateDump(handle, outputFile, minidump, cancellationToken);
}
/// <inheritdoc />
public double MeasureProcessorTimeDelta()
{
lock (processTimeMeasureLock)
{
try
{
var now = DateTimeOffset.UtcNow;
var newTime = handle.TotalProcessorTime;
var timeDelta = now - lastProcessorMeasureTime;
var newUsage = newTime - lastProcessorUsageTime;
lastProcessorMeasureTime = now;
lastProcessorUsageTime = newTime;
if (timeDelta != TimeSpan.Zero)
lastProcessorUsageEstimation = newUsage / (Environment.ProcessorCount * timeDelta);
}
catch (Exception ex)
{
logger.LogWarning(ex, "Error measuring processor time delta!");
}
return lastProcessorUsageEstimation;
}
}
/// <summary>
/// Attaches a log message to the process' exit event.
/// </summary>
@@ -1,4 +1,4 @@
<Project Sdk="Microsoft.NET.Sdk.Web">
<Project Sdk="Microsoft.NET.Sdk.Web">
<Import Project="../../build/SrcCommon.props" />
<PropertyGroup>
@@ -97,26 +97,24 @@
<PackageReference Include="DotEnv.Core" Version="3.1.0" />
<!-- Usage: Text formatter for Elasticsearch logging plugin -->
<PackageReference Include="Elastic.CommonSchema.Serilog" Version="8.12.3" />
<!-- Usage: GitLab interop -->
<PackageReference Include="GitLabApiClient" Version="1.8.0" />
<!-- Usage: GraphQL API Engine -->
<PackageReference Include="HotChocolate.AspNetCore" Version="14.3.0" />
<PackageReference Include="HotChocolate.AspNetCore" Version="15.0.3" />
<!-- Usage: GraphQL Authorization Plugin -->
<PackageReference Include="HotChocolate.AspNetCore.Authorization" Version="14.3.0" />
<PackageReference Include="HotChocolate.AspNetCore.Authorization" Version="15.0.3" />
<!-- Usage: GraphQL IDatabaseContext support -->
<PackageReference Include="HotChocolate.Data.EntityFramework" Version="14.3.0" />
<PackageReference Include="HotChocolate.Data.EntityFramework" Version="15.0.3" />
<!-- Usage: DataLoader source generation -->
<PackageReference Include="HotChocolate.Types.Analyzers" Version="14.3.0" />
<PackageReference Include="HotChocolate.Types.Analyzers" Version="15.0.3" />
<!-- Usage: GraphQL additional scalar type definitions -->
<PackageReference Include="HotChocolate.Types.Scalars" Version="14.3.0" />
<PackageReference Include="HotChocolate.Types.Scalars" Version="15.0.3" />
<!-- Usage: git interop -->
<PackageReference Include="LibGit2Sharp" Version="0.31.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.11" />
<PackageReference Include="Microsoft.AspNetCore.Mvc.NewtonsoftJson" Version="8.0.13" />
<!-- Usage: Using target JSON serializer for API -->
<PackageReference Include="Microsoft.AspNetCore.SignalR.Protocols.NewtonsoftJson" Version="9.0.0" />
<PackageReference Include="Microsoft.AspNetCore.SignalR.Protocols.NewtonsoftJson" Version="9.0.2" />
<!-- Usage: Generating dumps of dotnet engine processes -->
<PackageReference Include="Microsoft.Diagnostics.NETCore.Client" Version="0.2.553101" />
<PackageReference Include="Microsoft.Diagnostics.NETCore.Client" Version="0.2.607501" />
<!-- Usage: Database ORM -->
<PackageReference Include="Microsoft.EntityFrameworkCore" Version="9.0.0" />
<!-- Usage: Automatic migration generation using command line -->
@@ -127,6 +125,8 @@
<PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite" Version="9.0.0" />
<!-- Usage: MSSQL ORM plugin -->
<PackageReference Include="Microsoft.EntityFrameworkCore.SqlServer" Version="9.0.0" />
<!-- Usage: Database connectivity health check -->
<PackageReference Include="Microsoft.Extensions.Diagnostics.HealthChecks.EntityFrameworkCore" Version="9.0.0" />
<!-- Usage: POSIX support for syscalls, signals, and symlinks -->
<PackageReference Include="Mono.Posix.NETStandard" Version="1.0.0" />
<!-- Usage: Cron string parsing -->
@@ -137,8 +137,10 @@
<PackageReference Include="Npgsql.EntityFrameworkCore.PostgreSQL" Version="9.0.2" />
<!-- Usage: MYSQL/MariaDB ORM plugin -->
<PackageReference Include="Pomelo.EntityFrameworkCore.MySql" Version="9.0.0-preview.2.efcore.9.0.0" />
<!-- Usage: Publishing Prometheus metrics -->
<PackageReference Include="prometheus-net.AspNetCore.HealthChecks" Version="8.2.1" />
<!-- Usage: Discord interop -->
<PackageReference Include="Remora.Discord" Version="2024.3.0" />
<PackageReference Include="Remora.Discord" Version="2025.1.0" />
<!-- Usage: Rich logger builder -->
<PackageReference Include="Serilog.Extensions.Logging" Version="9.0.0" />
<!-- Usage: Async logging plugin -->
@@ -150,20 +152,21 @@
<!-- Usage: File logging plugin -->
<PackageReference Include="Serilog.Sinks.File" Version="6.0.0" />
<!-- Usage: OpenAPI spec generator -->
<PackageReference Include="Swashbuckle.AspNetCore" Version="7.2.0" />
<PackageReference Include="Swashbuckle.AspNetCore" Version="7.3.1" />
<!-- Usage: Newtonsoft.Json plugin for OpenAPI spec generator -->
<PackageReference Include="Swashbuckle.AspNetCore.Newtonsoft" Version="7.2.0" />
<PackageReference Include="Swashbuckle.AspNetCore.Newtonsoft" Version="7.3.1" />
<!-- Usage: Windows authentication plugin allowing searching for users by name -->
<PackageReference Include="System.DirectoryServices.AccountManagement" Version="9.0.0" />
<PackageReference Include="System.DirectoryServices.AccountManagement" Version="9.0.2" />
<!-- Usage: Identifying owning user of Windows Process objects -->
<PackageReference Include="System.Management" Version="9.0.0" />
<PackageReference Include="System.Management" Version="9.0.2" />
<!-- Usage: Temporary resolution to compatibility issues with EFCore 7 and .NET 8 -->
<PackageReference Include="System.Security.Permissions" Version="9.0.0" />
<PackageReference Include="System.Security.Permissions" Version="9.0.2" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\Tgstation.Server.Api\Tgstation.Server.Api.csproj" />
<ProjectReference Include="..\Tgstation.Server.Host.Common\Tgstation.Server.Host.Common.csproj" />
<ProjectReference Include="..\Tgstation.Server.Host.Utils.GitLab.GraphQL\Tgstation.Server.Host.Utils.GitLab.GraphQL.csproj" />
<ProjectReference Include="..\Tgstation.Server.Shared\Tgstation.Server.Shared.csproj" />
</ItemGroup>
@@ -2,6 +2,7 @@
using System.Collections.Generic;
using System.IdentityModel.Tokens.Jwt;
using System.Linq;
using System.Net.Http;
using System.Security.Cryptography;
using System.Text;
using System.Threading;
@@ -12,6 +13,7 @@ using Microsoft.Extensions.Options;
using Microsoft.IdentityModel.Tokens;
using Octokit;
using Octokit.Internal;
using Tgstation.Server.Api.Models;
using Tgstation.Server.Host.Configuration;
@@ -28,6 +30,11 @@ namespace Tgstation.Server.Host.Utils.GitHub
/// <remarks>Set to app installation token lifetime, which is the lowest. See https://docs.github.com/en/apps/creating-github-apps/authenticating-with-a-github-app/generating-an-installation-access-token-for-a-github-app.</remarks>
const uint ClientCacheHours = 1;
/// <summary>
/// Minutes before tokens expire before not using them.
/// </summary>
const uint AppTokenExpiryGraceMinutes = 15;
/// <summary>
/// The <see cref="clientCache"/> <see cref="KeyValuePair{TKey, TValue}.Key"/> used in place of <see langword="null"/> when accessing a configuration-based client with no token set in <see cref="GeneralConfiguration.GitHubAccessToken"/>.
/// </summary>
@@ -38,6 +45,11 @@ namespace Tgstation.Server.Host.Utils.GitHub
/// </summary>
readonly IAssemblyInformationProvider assemblyInformationProvider;
/// <summary>
/// The <see cref="IHttpMessageHandlerFactory"/> for the <see cref="GitHubClientFactory"/>.
/// </summary>
readonly IHttpMessageHandlerFactory httpMessageHandlerFactory;
/// <summary>
/// The <see cref="ILogger"/> for the <see cref="GitHubClientFactory"/>.
/// </summary>
@@ -49,9 +61,9 @@ namespace Tgstation.Server.Host.Utils.GitHub
readonly GeneralConfiguration generalConfiguration;
/// <summary>
/// Cache of created <see cref="GitHubClient"/>s and last used times, keyed by access token.
/// Cache of created <see cref="GitHubClient"/>s and last used/expiry times, keyed by access token.
/// </summary>
readonly Dictionary<string, (GitHubClient Client, DateTimeOffset LastUsed)> clientCache;
readonly Dictionary<string, (GitHubClient Client, DateTimeOffset LastUsed, DateTimeOffset? Expiry)> clientCache;
/// <summary>
/// The <see cref="SemaphoreSlim"/> used to guard access to <see cref="clientCache"/>.
@@ -62,18 +74,21 @@ namespace Tgstation.Server.Host.Utils.GitHub
/// Initializes a new instance of the <see cref="GitHubClientFactory"/> class.
/// </summary>
/// <param name="assemblyInformationProvider">The value of <see cref="assemblyInformationProvider"/>.</param>
/// <param name="httpMessageHandlerFactory">The value of <see cref="httpMessageHandlerFactory"/>.</param>
/// <param name="logger">The value of <see cref="logger"/>.</param>
/// <param name="generalConfigurationOptions">The <see cref="IOptions{TOptions}"/> containing the value of <see cref="generalConfiguration"/>.</param>
public GitHubClientFactory(
IAssemblyInformationProvider assemblyInformationProvider,
IHttpMessageHandlerFactory httpMessageHandlerFactory,
ILogger<GitHubClientFactory> logger,
IOptions<GeneralConfiguration> generalConfigurationOptions)
{
this.assemblyInformationProvider = assemblyInformationProvider ?? throw new ArgumentNullException(nameof(assemblyInformationProvider));
this.httpMessageHandlerFactory = httpMessageHandlerFactory ?? throw new ArgumentNullException(nameof(httpMessageHandlerFactory));
this.logger = logger ?? throw new ArgumentNullException(nameof(logger));
generalConfiguration = generalConfigurationOptions?.Value ?? throw new ArgumentNullException(nameof(generalConfigurationOptions));
clientCache = new Dictionary<string, (GitHubClient, DateTimeOffset)>();
clientCache = new Dictionary<string, (GitHubClient, DateTimeOffset, DateTimeOffset?)>();
clientCacheSemaphore = new SemaphoreSlim(1, 1);
}
@@ -127,13 +142,20 @@ namespace Tgstation.Server.Host.Utils.GitHub
else
cacheKey = accessString;
cacheHit = clientCache.TryGetValue(cacheKey, out var tuple);
var now = DateTimeOffset.UtcNow;
if (!cacheHit)
cacheHit = clientCache.TryGetValue(cacheKey, out var tuple);
var tokenValid = cacheHit && (!tuple.Expiry.HasValue || tuple.Expiry.Value <= now);
if (!tokenValid)
{
if (cacheHit)
{
logger.LogDebug("Previously cached GitHub token has expired!");
clientCache.Remove(cacheKey);
}
logger.LogTrace("Creating new GitHubClient...");
DateTimeOffset? expiry = null;
if (accessString != null)
{
if (accessString.StartsWith(RepositorySettings.TgsAppPrivateKeyPrefix))
@@ -167,6 +189,7 @@ namespace Tgstation.Server.Host.Utils.GitHub
var installToken = await client.GitHubApps.CreateInstallationToken(installation.Id);
client.Credentials = new Credentials(installToken.Token);
expiry = installToken.ExpiresAt.AddMinutes(-AppTokenExpiryGraceMinutes);
}
catch (Exception ex)
{
@@ -183,7 +206,7 @@ namespace Tgstation.Server.Host.Utils.GitHub
else
client = CreateUnauthenticatedClient();
clientCache.Add(cacheKey, (Client: client, LastUsed: now));
clientCache.Add(cacheKey, (Client: client, LastUsed: now, Expiry: expiry));
lastUsed = null;
}
else
@@ -203,7 +226,7 @@ namespace Tgstation.Server.Host.Utils.GitHub
continue; // save the hash lookup
tuple = clientCache[key];
if (tuple.LastUsed <= purgeAfter)
if (tuple.LastUsed <= purgeAfter || (tuple.Expiry.HasValue && tuple.Expiry.Value <= now))
{
clientCache.Remove(key);
++purgeCount;
@@ -315,10 +338,33 @@ namespace Tgstation.Server.Host.Utils.GitHub
GitHubClient CreateUnauthenticatedClient()
{
var product = assemblyInformationProvider.ProductInfoHeaderValue.Product!;
return new GitHubClient(
new ProductHeaderValue(
product.Name,
product.Version));
#pragma warning disable CA2000 // Dispose objects before losing scope
var handler = httpMessageHandlerFactory.CreateHandler();
try
{
var clientAdapter = new HttpClientAdapter(() => handler);
#pragma warning restore CA2000 // Dispose objects before losing scope
handler = null;
try
{
return new GitHubClient(
new Connection(
new ProductHeaderValue(
product.Name,
product.Version),
clientAdapter));
}
catch
{
clientAdapter.Dispose();
throw;
}
}
catch
{
handler?.Dispose();
throw;
}
}
}
}

Some files were not shown because too many files have changed in this diff Show More