Merge branch 'dev' into FixUpdates

This commit is contained in:
Jordan Dominion
2024-08-11 16:14:09 -04:00
committed by GitHub
48 changed files with 583 additions and 153 deletions
+1 -1
View File
@@ -8,7 +8,7 @@ on:
env:
TGS_DOTNET_VERSION: 8
OD_MIN_COMPAT_DOTNET_VERSION: 7
TGS_DOTNET_QUALITY: ga
jobs:
master-merge:
@@ -0,0 +1,76 @@
name: 'Sync spacestation13/tgstation-server'
on:
push:
branches:
- dev
tags:
- '*'
workflow_dispatch:
env:
TGS_DOTNET_VERSION: 8
TGS_DOTNET_QUALITY: ga
jobs:
fork-sync:
name: Fork Sync
runs-on: ubuntu-latest
steps:
- name: Setup dotnet
uses: actions/setup-dotnet@v4
with:
dotnet-version: '${{ env.TGS_DOTNET_VERSION }}.0.x'
dotnet-quality: ${{ env.TGS_DOTNET_QUALITY }}
- name: Build Checkout
uses: actions/checkout@v4
with:
path: temp_workspace
- name: Restore
run: |
cd temp_workspace
dotnet restore
- name: Build ReleaseNotes
run: |
cd temp_workspace
dotnet build -c Release -p:TGS_HOST_NO_WEBPANEL=true tools/Tgstation.Server.ReleaseNotes/Tgstation.Server.ReleaseNotes.csproj
- name: Generate App Token
run: |
cd temp_workspace
dotnet run -c Release --no-build --project tools/Tgstation.Server.ReleaseNotes --token-output-file ${{ runner.temp }}/installation_secret.txt ${{ secrets.TGS_CI_GITHUB_APP_TOKEN_BASE64 }} --spacestation13
echo "INSTALLATION_TOKEN=$(cat ${{ runner.temp }}/installation_secret.txt)" >> $GITHUB_ENV
rm ${{ runner.temp }}/installation_secret.txt
env:
TGS_RELEASE_NOTES_TOKEN: ${{ secrets.DEV_PUSH_TOKEN }}
- name: Main Checkout
uses: actions/checkout@v4
with:
fetch-depth: 0
fetch-tags: true
token: ${{ env.INSTALLATION_TOKEN }}
- name: Restore
run: dotnet restore
- name: Build ReleaseNotes
run: dotnet build -c Release -p:TGS_HOST_NO_WEBPANEL=true tools/Tgstation.Server.ReleaseNotes/Tgstation.Server.ReleaseNotes.csproj
- name: Generate App Token
run: |
dotnet run -c Release --no-build --project tools/Tgstation.Server.ReleaseNotes --token-output-file ${{ runner.temp }}/installation_secret.txt ${{ secrets.TGS_CI_GITHUB_APP_TOKEN_BASE64 }}
echo "INSTALLATION_TOKEN=$(cat ${{ runner.temp }}/installation_secret.txt)" >> $GITHUB_ENV
rm ${{ runner.temp }}/installation_secret.txt
env:
TGS_RELEASE_NOTES_TOKEN: ${{ secrets.DEV_PUSH_TOKEN }}
- name: Push to Spacestation13 Fork
run: |
git config user.name "tgstation-server-ci[bot]"
git config user.email "161980869+tgstation-server-ci[bot]@users.noreply.github.com"
git push "https://tgstation-server-ci:${{ env.INSTALLATION_TOKEN }}@github.com/spacestation13/tgstation-server"
git push --tags "https://tgstation-server-ci:${{ env.INSTALLATION_TOKEN }}@github.com/spacestation13/tgstation-server"
+2 -2
View File
@@ -18,9 +18,9 @@
<!-- Pinned: Be VERY careful about updating https://github.com/moq/moq/issues/1372 -->
<PackageReference Include="Moq" Version="4.20.70" />
<!-- Usage: MSTest execution -->
<PackageReference Include="MSTest.TestAdapter" Version="3.5.0" />
<PackageReference Include="MSTest.TestAdapter" Version="3.5.1" />
<!-- Usage: MSTest asserts etc... -->
<PackageReference Include="MSTest.TestFramework" Version="3.5.0" />
<PackageReference Include="MSTest.TestFramework" Version="3.5.1" />
</ItemGroup>
</Project>
+8 -8
View File
@@ -3,13 +3,13 @@
<!-- Integration tests will ensure they match across the board -->
<Import Project="WebpanelVersion.props" />
<PropertyGroup>
<TgsCoreVersion>6.8.0</TgsCoreVersion>
<TgsConfigVersion>5.1.0</TgsConfigVersion>
<TgsApiVersion>10.6.0</TgsApiVersion>
<TgsCoreVersion>6.9.0</TgsCoreVersion>
<TgsConfigVersion>5.2.0</TgsConfigVersion>
<TgsApiVersion>10.7.0</TgsApiVersion>
<TgsCommonLibraryVersion>7.0.0</TgsCommonLibraryVersion>
<TgsApiLibraryVersion>13.6.0</TgsApiLibraryVersion>
<TgsClientVersion>15.6.0</TgsClientVersion>
<TgsDmapiVersion>7.1.3</TgsDmapiVersion>
<TgsApiLibraryVersion>13.7.0</TgsApiLibraryVersion>
<TgsClientVersion>16.0.0</TgsClientVersion>
<TgsDmapiVersion>7.2.1</TgsDmapiVersion>
<TgsInteropVersion>5.9.0</TgsInteropVersion>
<TgsHostWatchdogVersion>1.4.1</TgsHostWatchdogVersion>
<TgsContainerScriptVersion>1.2.1</TgsContainerScriptVersion>
@@ -18,9 +18,9 @@
<TgsNetMajorVersion>8</TgsNetMajorVersion>
<!-- Update this frequently with dotnet runtime patches. MAJOR MUST MATCH ABOVE! -->
<TgsDotnetRedistUrl>https://download.visualstudio.microsoft.com/download/pr/751d3fcd-72db-4da2-b8d0-709c19442225/33cc492bde704bfd6d70a2b9109005a0/dotnet-hosting-8.0.6-win.exe</TgsDotnetRedistUrl>
<TgsMariaDBRedistVersion>10.11.8</TgsMariaDBRedistVersion>
<TgsMariaDBRedistVersion>11.4.2</TgsMariaDBRedistVersion>
<!-- Only have this uncommented if the mariadb servers are shitting the bed, update if the version updates -->
<TgsMariaDBFallbackRedist>https://mirror.its.dal.ca/mariadb//mariadb-10.11.8/winx64-packages/mariadb-10.11.8-winx64.msi</TgsMariaDBFallbackRedist>
<!--<TgsMariaDBFallbackRedist>https://mirror.its.dal.ca/mariadb//mariadb-10.11.8/winx64-packages/mariadb-10.11.8-winx64.msi</TgsMariaDBFallbackRedist>-->
<TgsYarnVersion>1.22.21</TgsYarnVersion>
</PropertyGroup>
</Project>
+73 -27
View File
@@ -1,18 +1,19 @@
// tgstation-server DMAPI
// The key words "MUST", "MUST NOT", "REQUIRED", "SHALL", "SHALL NOT", "SHOULD", "SHOULD NOT", "RECOMMENDED", "MAY", and "OPTIONAL" in this document are to be interpreted as described in IETF RFC 2119.
#define TGS_DMAPI_VERSION "7.1.3"
#define TGS_DMAPI_VERSION "7.2.1"
// All functions and datums outside this document are subject to change with any version and should not be relied on.
// CONFIGURATION
/// Create this define if you want to do TGS configuration outside of this file.
/// Consumers SHOULD create this define if you want to do TGS configuration outside of this file.
#ifndef TGS_EXTERNAL_CONFIGURATION
// Comment this out once you've filled in the below.
// Consumers MUST comment this out once you've filled in the below and are not using [TGS_EXTERNAL_CONFIGURATION].
#error TGS API unconfigured
// Uncomment this if you wish to allow the game to interact with TGS 3..
// Consumers MUST uncomment this if you wish to allow the game to interact with TGS version 3.
// This will raise the minimum required security level of your game to TGS_SECURITY_TRUSTED due to it utilizing call()().
//#define TGS_V3_API
@@ -52,7 +53,7 @@
#ifndef TGS_FILE2TEXT_NATIVE
#ifdef file2text
#error Your codebase is re-defining the BYOND proc file2text. The DMAPI requires the native version to read the result of world.Export(). You can fix this by adding "#define TGS_FILE2TEXT_NATIVE file2text" before your override of file2text to allow the DMAPI to use the native version. This will only be used for world.Export(), not regular file accesses
#error Your codebase is re-defining the BYOND proc file2text. The DMAPI requires the native version to read the result of world.Export(). You SHOULD fix this by adding "#define TGS_FILE2TEXT_NATIVE file2text" before your override of file2text to allow the DMAPI to use the native version. This will only be used for world.Export(), not regular file accesses
#endif
#define TGS_FILE2TEXT_NATIVE file2text
#endif
@@ -152,16 +153,17 @@
//REQUIRED HOOKS
/**
* Call this somewhere in [/world/proc/New] that is always run. This function may sleep!
* Consumers MUST call this somewhere in [/world/proc/New] that is always run. This function may sleep!
*
* * event_handler - Optional user defined [/datum/tgs_event_handler].
* * minimum_required_security_level: The minimum required security level to run the game in which the DMAPI is integrated. Can be one of [TGS_SECURITY_ULTRASAFE], [TGS_SECURITY_SAFE], or [TGS_SECURITY_TRUSTED].
* * http_handler - Optional user defined [/datum/tgs_http_handler].
*/
/world/proc/TgsNew(datum/tgs_event_handler/event_handler, minimum_required_security_level = TGS_SECURITY_ULTRASAFE)
/world/proc/TgsNew(datum/tgs_event_handler/event_handler, minimum_required_security_level = TGS_SECURITY_ULTRASAFE, datum/tgs_http_handler/http_handler)
return
/**
* Call this when your initializations are complete and your game is ready to play before any player interactions happen.
* Consumers MUST call this when world initializations are complete and the game is ready to play before any player interactions happen.
*
* This may use [/world/var/sleep_offline] to make this happen so ensure no changes are made to it while this call is running.
* Afterwards, consider explicitly setting it to what you want to avoid this BYOND bug: http://www.byond.com/forum/post/2575184
@@ -170,12 +172,10 @@
/world/proc/TgsInitializationComplete()
return
/// Put this at the start of [/world/proc/Topic].
/// Consumers MUST run this macro at the start of [/world/proc/Topic].
#define TGS_TOPIC var/tgs_topic_return = TgsTopic(args[1]); if(tgs_topic_return) return tgs_topic_return
/**
* Call this as late as possible in [world/proc/Reboot] (BEFORE ..()).
*/
/// Consumers MUST call this as late as possible in [world/proc/Reboot] (BEFORE ..()).
/world/proc/TgsReboot()
return
@@ -269,7 +269,7 @@
/// The [/datum/tgs_chat_channel] the user was from.
var/datum/tgs_chat_channel/channel
/// User definable handler for TGS events.
/// User definable handler for TGS events This abstract version SHOULD be overridden to be used.
/datum/tgs_event_handler
/// If the handler receieves [TGS_EVENT_HEALTH_CHECK] events.
var/receive_health_checks = FALSE
@@ -283,7 +283,41 @@
set waitfor = FALSE
return
/// User definable chat command.
/// User definable handler for HTTP calls. This abstract version MUST be overridden to be used.
/datum/tgs_http_handler
/**
* User definable callback for executing HTTP GET requests.
* MUST perform BYOND sleeps while the request is in flight.
* MUST return a [/datum/tgs_http_result].
* SHOULD log its own errors
*
* url - The full URL to execute the GET request for including query parameters.
*/
/datum/tgs_http_handler/proc/PerformGet(url)
CRASH("[type]/PerformGet not implemented!")
/// Result of a [/datum/tgs_http_handler] call. MUST NOT be overridden.
/datum/tgs_http_result
/// HTTP response as text
var/response_text
/// Boolean request success flag. Set for any 2XX response code.
var/success
/**
* Create a [/datum/tgs_http_result].
*
* * response_text - HTTP response as text. Must be provided in New().
* * success - Boolean request success flag. Set for any 2XX response code. Must be provided in New().
*/
/datum/tgs_http_result/New(response_text, success)
if(response_text && !istext(response_text))
CRASH("response_text was not text!")
src.response_text = response_text
src.success = success
/// User definable chat command. This abstract version MUST be overridden to be used.
/datum/tgs_chat_command
/// The string to trigger this command on a chat bot. e.g `@bot name ...` or `!tgs name ...`.
var/name = ""
@@ -296,21 +330,27 @@
/**
* Process command activation. Should return a [/datum/tgs_message_content] to respond to the issuer with.
* MUST be implemented
*
* sender - The [/datum/tgs_chat_user] who issued the command.
* params - The trimmed string following the command `/datum/tgs_chat_command/var/name].
* * sender - The [/datum/tgs_chat_user] who issued the command.
* * params - The trimmed string following the command `/datum/tgs_chat_command/var/name].
*/
/datum/tgs_chat_command/proc/Run(datum/tgs_chat_user/sender, params)
CRASH("[type] has no implementation for Run()")
/// User definable chat message.
/// User definable chat message. MUST NOT be overridden.
/datum/tgs_message_content
/// The tring content of the message. Must be provided in New().
/// The string content of the message. Must be provided in New().
var/text
/// The [/datum/tgs_chat_embed] to embed in the message. Not supported on all chat providers.
var/datum/tgs_chat_embed/structure/embed
/**
* Create a [/datum/tgs_message_content].
*
* * text - The string content of the message.
*/
/datum/tgs_message_content/New(text)
..()
if(!istext(text))
@@ -319,7 +359,7 @@
src.text = text
/// User definable chat embed. Currently mirrors Discord chat embeds. See https://discord.com/developers/docs/resources/channel#embed-object-embed-structure for details.
/// User definable chat embed. Currently mirrors Discord chat embeds. See https://discord.com/developers/docs/resources/message#embed-object for details.
/datum/tgs_chat_embed/structure
var/title
var/description
@@ -331,13 +371,13 @@
/// Colour must be #AARRGGBB or #RRGGBB hex string.
var/colour
/// See https://discord.com/developers/docs/resources/channel#embed-object-embed-image-structure for details.
/// See https://discord.com/developers/docs/resources/message#embed-object-embed-image-structure for details.
var/datum/tgs_chat_embed/media/image
/// See https://discord.com/developers/docs/resources/channel#embed-object-embed-thumbnail-structure for details.
/// See https://discord.com/developers/docs/resources/message#embed-object-embed-thumbnail-structure for details.
var/datum/tgs_chat_embed/media/thumbnail
/// See https://discord.com/developers/docs/resources/channel#embed-object-embed-image-structure for details.
/// See https://discord.com/developers/docs/resources/message#embed-object-embed-video-structure for details.
var/datum/tgs_chat_embed/media/video
var/datum/tgs_chat_embed/footer/footer
@@ -346,7 +386,7 @@
var/list/datum/tgs_chat_embed/field/fields
/// Common datum for similar discord embed medias.
/// Common datum for similar Discord embed medias.
/datum/tgs_chat_embed/media
/// Must be set in New().
var/url
@@ -354,6 +394,7 @@
var/height
var/proxy_url
/// Create a [/datum/tgs_chat_embed].
/datum/tgs_chat_embed/media/New(url)
..()
if(!istext(url))
@@ -361,13 +402,14 @@
src.url = url
/// See https://discord.com/developers/docs/resources/channel#embed-object-embed-footer-structure for details.
/// See https://discord.com/developers/docs/resources/message#embed-object-embed-footer-structure for details.
/datum/tgs_chat_embed/footer
/// Must be set in New().
var/text
var/icon_url
var/proxy_icon_url
/// Create a [/datum/tgs_chat_embed/footer].
/datum/tgs_chat_embed/footer/New(text)
..()
if(!istext(text))
@@ -375,16 +417,17 @@
src.text = text
/// See https://discord.com/developers/docs/resources/channel#embed-object-embed-provider-structure for details.
/// See https://discord.com/developers/docs/resources/message#embed-object-embed-provider-structure for details.
/datum/tgs_chat_embed/provider
var/name
var/url
/// See https://discord.com/developers/docs/resources/channel#embed-object-embed-author-structure for details. Must have name set in New().
/// See https://discord.com/developers/docs/resources/message#embed-object-embed-author-structure for details. Must have name set in New().
/datum/tgs_chat_embed/provider/author
var/icon_url
var/proxy_icon_url
/// Create a [/datum/tgs_chat_embed/footer].
/datum/tgs_chat_embed/provider/author/New(name)
..()
if(!istext(name))
@@ -392,12 +435,15 @@
src.name = name
/// See https://discord.com/developers/docs/resources/channel#embed-object-embed-field-structure for details. Must have name and value set in New().
/// See https://discord.com/developers/docs/resources/message#embed-object-embed-field-structure for details.
/datum/tgs_chat_embed/field
/// Must be set in New().
var/name
/// Must be set in New().
var/value
var/is_inline
/// Create a [/datum/tgs_chat_embed/field].
/datum/tgs_chat_embed/field/New(name, value)
..()
if(!istext(name))
+1 -1
View File
@@ -3,7 +3,7 @@
This folder contains all DMAPI code not directly involved in an API.
- [_definitions.dm](./definitions.dm) contains defines needed across DMAPI internals.
- [byond_world_export.dm](./byond_world_export.dm) contains the default `/datum/tgs_http_handler` implementation which uses `world.Export()`.
- [core.dm](./core.dm) contains the implementations of the `/world/proc/TgsXXX()` procs. Many map directly to the `/datum/tgs_api` functions. It also contains the /datum selection and setup code.
- [datum.dm](./datum.dm) contains the `/datum/tgs_api` declarations that all APIs must implement.
- [tgs_version.dm](./tgs_version.dm) contains the `/datum/tgs_version` definition
-
+22
View File
@@ -0,0 +1,22 @@
/datum/tgs_http_handler/byond_world_export
/datum/tgs_http_handler/byond_world_export/PerformGet(url)
// This is an infinite sleep until we get a response
var/export_response = world.Export(url)
TGS_DEBUG_LOG("byond_world_export: Export complete")
if(!export_response)
TGS_ERROR_LOG("byond_world_export: Failed request: [url]")
return new /datum/tgs_http_result(null, FALSE)
var/content = export_response["CONTENT"]
if(!content)
TGS_ERROR_LOG("byond_world_export: Failed request, missing content!")
return new /datum/tgs_http_result(null, FALSE)
var/response_json = TGS_FILE2TEXT_NATIVE(content)
if(!response_json)
TGS_ERROR_LOG("byond_world_export: Failed request, failed to load content!")
return new /datum/tgs_http_result(null, FALSE)
return new /datum/tgs_http_result(response_json, TRUE)
+5 -2
View File
@@ -1,4 +1,4 @@
/world/TgsNew(datum/tgs_event_handler/event_handler, minimum_required_security_level = TGS_SECURITY_ULTRASAFE)
/world/TgsNew(datum/tgs_event_handler/event_handler, minimum_required_security_level = TGS_SECURITY_ULTRASAFE, datum/tgs_http_handler/http_handler = null)
var/current_api = TGS_READ_GLOBAL(tgs)
if(current_api)
TGS_ERROR_LOG("API datum already set (\ref[current_api] ([current_api]))! Was TgsNew() called more than once?")
@@ -55,7 +55,10 @@
TGS_ERROR_LOG("Invalid parameter for event_handler: [event_handler]")
event_handler = null
var/datum/tgs_api/new_api = new api_datum(event_handler, version)
if(!http_handler)
http_handler = new /datum/tgs_http_handler/byond_world_export
var/datum/tgs_api/new_api = new api_datum(event_handler, version, http_handler)
TGS_WRITE_GLOBAL(tgs, new_api)
+1 -1
View File
@@ -6,7 +6,7 @@ TGS_DEFINE_AND_SET_GLOBAL(tgs, null)
var/list/warned_deprecated_command_runs
/datum/tgs_api/New(datum/tgs_event_handler/event_handler, datum/tgs_version/version)
/datum/tgs_api/New(datum/tgs_event_handler/event_handler, datum/tgs_version/version, datum/tgs_http_handler/http_handler)
..()
src.event_handler = event_handler
src.version = version
+1
View File
@@ -1,4 +1,5 @@
#include "core\_definitions.dm"
#include "core\byond_world_export.dm"
#include "core\core.dm"
#include "core\datum.dm"
#include "core\tgs_version.dm"
+4 -1
View File
@@ -31,9 +31,12 @@
var/detached = FALSE
/datum/tgs_api/v5/New()
var/datum/tgs_http_handler/http_handler
/datum/tgs_api/v5/New(datum/tgs_event_handler/event_handler, datum/tgs_version/version, datum/tgs_http_handler/http_handler)
. = ..()
interop_version = version
src.http_handler = http_handler
TGS_DEBUG_LOG("V5 API created: [json_encode(args)]")
/datum/tgs_api/v5/ApiVersion()
+9 -12
View File
@@ -78,27 +78,24 @@
WaitForReattach(FALSE)
TGS_DEBUG_LOG("Bridge request start")
// This is an infinite sleep until we get a response
var/export_response = world.Export(bridge_request)
var/datum/tgs_http_result/result = http_handler.PerformGet(bridge_request)
TGS_DEBUG_LOG("Bridge request complete")
if(!export_response)
TGS_ERROR_LOG("Failed bridge request: [bridge_request]")
if(isnull(result))
TGS_ERROR_LOG("Failed bridge request, handler returned null!")
return
var/content = export_response["CONTENT"]
if(!content)
TGS_ERROR_LOG("Failed bridge request, missing content!")
if(!istype(result) || result.type != /datum/tgs_http_result)
TGS_ERROR_LOG("Failed bridge request, handler returned non-[/datum/tgs_http_result]!")
return
var/response_json = TGS_FILE2TEXT_NATIVE(content)
if(!response_json)
TGS_ERROR_LOG("Failed bridge request, failed to load content!")
if(!result.success)
TGS_DEBUG_LOG("Failed bridge request, HTTP request failed!")
return
var/list/bridge_response = json_decode(response_json)
var/list/bridge_response = json_decode(result.response_text)
if(!bridge_response)
TGS_ERROR_LOG("Failed bridge request, bad json: [response_json]")
TGS_ERROR_LOG("Failed bridge request, bad json: [result.response_text]")
return
var/error = bridge_response[DMAPI5_RESPONSE_ERROR_MESSAGE]
@@ -108,5 +108,11 @@ namespace Tgstation.Server.Api.Models
/// </summary>
[Description("Reconnect chat bot")]
ReconnectChatBot,
/// <summary>
/// When a repository is recloned.
/// </summary>
[Description("Reclone repository")]
RepositoryReclone,
}
}
@@ -16,5 +16,10 @@ namespace Tgstation.Server.Api.Models.Response
/// The latest available version of the Tgstation.Server.Host assembly from the upstream repository. If <see cref="Version.Major"/> is not equal to 4 the update cannot be applied due to API changes.
/// </summary>
public Version? LatestVersion { get; set; }
/// <summary>
/// This response is cached. This field indicates the <see cref="DateTimeOffset"/> when it was generated.
/// </summary>
public DateTimeOffset? GeneratedAt { get; set; }
}
}
@@ -59,5 +59,11 @@ namespace Tgstation.Server.Api.Models.Response
/// </summary>
[ResponseOptions]
public bool? CurrentAllowWebclient { get; set; }
/// <summary>
/// The amount of RAM in use by the game server in bytes.
/// </summary>
[ResponseOptions]
public long? ImmediateMemoryUsage { get; set; }
}
}
@@ -82,5 +82,10 @@ namespace Tgstation.Server.Api.Rights
/// User may change submodule update settings.
/// </summary>
ChangeSubmoduleUpdate = 1 << 13,
/// <summary>
/// User may trigger repository recloning.
/// </summary>
Reclone = 1 << 14,
}
}
@@ -24,7 +24,7 @@ namespace Tgstation.Server.Client
}
/// <inheritdoc />
public ValueTask<AdministrationResponse> Read(CancellationToken cancellationToken) => ApiClient.Read<AdministrationResponse>(Routes.Administration, cancellationToken);
public ValueTask<AdministrationResponse> Read(bool forceFresh, CancellationToken cancellationToken) => ApiClient.Read<AdministrationResponse>($"{Routes.Administration}?fresh={forceFresh}", cancellationToken);
/// <inheritdoc />
public async ValueTask<ServerUpdateResponse> Update(
@@ -43,7 +43,10 @@ namespace Tgstation.Server.Client.Components
public ValueTask<JobResponse> Restart(CancellationToken cancellationToken) => apiClient.Patch<JobResponse>(Routes.DreamDaemon, instance.Id!.Value, cancellationToken);
/// <inheritdoc />
public ValueTask<DreamDaemonResponse> Read(CancellationToken cancellationToken) => apiClient.Read<DreamDaemonResponse>(Routes.DreamDaemon, instance.Id!.Value, cancellationToken);
public ValueTask<DreamDaemonResponse> Read(CancellationToken cancellationToken) => apiClient.Read<DreamDaemonResponse>(
Routes.DreamDaemon,
instance.Id!.Value,
cancellationToken);
/// <inheritdoc />
public ValueTask<DreamDaemonResponse> Update(DreamDaemonRequest dreamDaemon, CancellationToken cancellationToken) => apiClient.Update<DreamDaemonRequest, DreamDaemonResponse>(Routes.DreamDaemon, dreamDaemon ?? throw new ArgumentNullException(nameof(dreamDaemon)), instance.Id!.Value, cancellationToken);
@@ -16,7 +16,7 @@ namespace Tgstation.Server.Client.Components
/// </summary>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> for the operation.</param>
/// <returns>A <see cref="ValueTask{TResult}"/> resulting in the <see cref="DreamDaemonResponse"/> information.</returns>
ValueTask<DreamDaemonResponse> Read(CancellationToken cancellationToken);
ValueTask<DreamDaemonResponse> Read(CancellationToken cancellationToken = default);
/// <summary>
/// Start <see cref="DreamDaemonResponse"/>.
@@ -40,5 +40,12 @@ namespace Tgstation.Server.Client.Components
/// <param name="cancellationToken">The <see cref="CancellationToken"/> for the operation.</param>
/// <returns>A <see cref="ValueTask{TResult}"/> resulting in the <see cref="RepositoryResponse"/>.</returns>
ValueTask<RepositoryResponse> Delete(CancellationToken cancellationToken);
/// <summary>
/// Deletes and reclones the repository.
/// </summary>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> for the operation.</param>
/// <returns>A <see cref="ValueTask{TResult}"/> resulting in the <see cref="RepositoryResponse"/>.</returns>
ValueTask<RepositoryResponse> Reclone(CancellationToken cancellationToken);
}
}
@@ -44,5 +44,8 @@ namespace Tgstation.Server.Client.Components
/// <inheritdoc />
public ValueTask<RepositoryResponse> Update(RepositoryUpdateRequest repository, CancellationToken cancellationToken) => apiClient.Update<RepositoryUpdateRequest, RepositoryResponse>(Routes.Repository, repository ?? throw new ArgumentNullException(nameof(repository)), instance.Id!.Value, cancellationToken);
/// <inheritdoc />
public ValueTask<RepositoryResponse> Reclone(CancellationToken cancellationToken) => apiClient.Patch<RepositoryResponse>(Routes.Repository, instance.Id!.Value, cancellationToken);
}
}
@@ -17,9 +17,10 @@ namespace Tgstation.Server.Client
/// <summary>
/// Get the <see cref="AdministrationResponse"/> represented by the <see cref="IAdministrationClient"/>.
/// </summary>
/// <param name="forceFresh">If <see langword="true"/> the response will be forcefully regenerated.</param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> for the operation.</param>
/// <returns>A <see cref="ValueTask{TResult}"/> resulting in the <see cref="AdministrationResponse"/> represented by the <see cref="IAdministrationClient"/>.</returns>
ValueTask<AdministrationResponse> Read(CancellationToken cancellationToken);
ValueTask<AdministrationResponse> Read(bool forceFresh = false, CancellationToken cancellationToken = default);
/// <summary>
/// Updates the <see cref="AdministrationResponse"/> setttings.
@@ -50,7 +50,7 @@ namespace Tgstation.Server.Host.Components.Chat.Commands
public async ValueTask<MessageContent> Invoke(string arguments, ChatUser user, CancellationToken cancellationToken)
{
string result;
if (arguments.Split(' ').Any(x => x.ToUpperInvariant() == "--REPO"))
if (arguments.Split(' ').Any(x => x.Equals("--repo", StringComparison.OrdinalIgnoreCase)))
{
if (repositoryManager.CloneInProgress || repositoryManager.InUse)
return new MessageContent
@@ -58,15 +58,13 @@ namespace Tgstation.Server.Host.Components.Chat.Commands
Text = "Repository busy! Try again later",
};
using (var repo = await repositoryManager.LoadRepository(cancellationToken))
{
if (repo == null)
return new MessageContent
{
Text = "Repository unavailable!",
};
result = repo.Head;
}
using var repo = await repositoryManager.LoadRepository(cancellationToken);
if (repo == null)
return new MessageContent
{
Text = "Repository unavailable!",
};
result = repo.Head;
}
else
{
@@ -157,6 +157,8 @@ namespace Tgstation.Server.Host.Components.Engine
{
Logger.LogTrace("OD repo seems to already exist, attempting load and fetch...");
repo = await repositoryManager.LoadRepository(cancellationToken);
if (repo == null)
throw new JobException("Can't load OpenDream repository! Please delete cache from disk!");
await repo!.FetchOrigin(
progressSection1,
@@ -176,6 +178,7 @@ namespace Tgstation.Server.Host.Components.Engine
null,
null,
true,
false,
progressSection2,
cancellationToken);
@@ -224,7 +224,7 @@ namespace Tgstation.Server.Host.Components.Engine
/// <inheritdoc />
protected override string GetDreamDaemonName(Version byondVersion, out bool supportsCli)
{
supportsCli = byondVersion >= DDExeVersion;
supportsCli = byondVersion >= DDExeVersion && !sessionConfiguration.ForceUseDreamDaemonExe;
return supportsCli ? "dd.exe" : "dreamdaemon.exe";
}
@@ -12,7 +12,7 @@
RepoResetOrigin,
/// <summary>
/// Parameters: Checkout target.
/// Parameters: Checkout target, hard reset flag (If "True", this is actually a hard reset, not a checkout).
/// </summary>
[EventScript("RepoCheckout")]
RepoCheckout,
@@ -47,6 +47,7 @@ namespace Tgstation.Server.Host.Components.Repository
/// <param name="username">The optional username used for fetching from submodule repositories.</param>
/// <param name="password">The optional password used for fetching from submodule repositories.</param>
/// <param name="updateSubmodules">If a submodule update should be attempted after the merge.</param>
/// <param name="moveCurrentReference">If a hard reset to the target committish should be performed instead of a checkout.</param>
/// <param name="progressReporter">The optional <see cref="JobProgressReporter"/> to report progress of the operation.</param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> for the operation.</param>
/// <returns>A <see cref="ValueTask"/> representing the running operation.</returns>
@@ -55,6 +56,7 @@ namespace Tgstation.Server.Host.Components.Repository
string? username,
string? password,
bool updateSubmodules,
bool moveCurrentReference,
JobProgressReporter? progressReporter,
CancellationToken cancellationToken);
@@ -295,7 +295,7 @@ namespace Tgstation.Server.Host.Components.Repository
var revertTo = originalCommit.CanonicalName ?? originalCommit.Tip.Sha;
logger.LogDebug("Merge conflict, aborting and reverting to {revertTarget}", revertTo);
progressReporter.ReportProgress(0);
RawCheckout(revertTo, progressReporter.CreateSection("Hard Reset to {revertTo}", 1.0), cancellationToken);
RawCheckout(revertTo, false, progressReporter.CreateSection("Hard Reset to {revertTo}", 1.0), cancellationToken);
cancellationToken.ThrowIfCancellationRequested();
}
@@ -376,19 +376,21 @@ namespace Tgstation.Server.Host.Components.Repository
string? username,
string? password,
bool updateSubmodules,
bool moveCurrentReference,
JobProgressReporter? progressReporter,
CancellationToken cancellationToken)
{
ArgumentNullException.ThrowIfNull(committish);
logger.LogDebug("Checkout object: {committish}...", committish);
await eventConsumer.HandleEvent(EventType.RepoCheckout, new List<string> { committish }, false, cancellationToken);
await eventConsumer.HandleEvent(EventType.RepoCheckout, new List<string> { committish, moveCurrentReference.ToString() }, false, cancellationToken);
await Task.Factory.StartNew(
() =>
{
libGitRepo.RemoveUntrackedFiles();
RawCheckout(
committish,
moveCurrentReference,
progressReporter?.CreateSection(null, updateSubmodules ? 2.0 / 3 : 1.0),
cancellationToken);
},
@@ -879,9 +881,10 @@ namespace Tgstation.Server.Host.Components.Repository
/// Runs a blocking force checkout to <paramref name="committish"/>.
/// </summary>
/// <param name="committish">The committish to checkout.</param>
/// <param name="moveCurrentReference">If a hard reset should actually be performed.</param>
/// <param name="progressReporter">The optional <see cref="JobProgressReporter"/> for the operation.</param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> for the operation.</param>
void RawCheckout(string committish, JobProgressReporter? progressReporter, CancellationToken cancellationToken)
void RawCheckout(string committish, bool moveCurrentReference, JobProgressReporter? progressReporter, CancellationToken cancellationToken)
{
logger.LogTrace("Checkout: {committish}", committish);
@@ -900,34 +903,52 @@ namespace Tgstation.Server.Host.Components.Repository
cancellationToken.ThrowIfCancellationRequested();
void RunCheckout() => commands.Checkout(
libGitRepo,
checkoutOptions,
committish);
try
if (moveCurrentReference)
{
RunCheckout();
if (Reference == NoReference)
throw new InvalidOperationException("Cannot move current reference when not on reference!");
var gitObject = libGitRepo.Lookup(committish);
if (gitObject == null)
throw new JobException($"Could not find committish: {committish}");
var commit = gitObject.Peel<Commit>();
cancellationToken.ThrowIfCancellationRequested();
libGitRepo.Reset(ResetMode.Hard, commit, checkoutOptions);
}
catch (NotFoundException)
else
{
// Maybe (likely) a remote?
var remoteName = $"origin/{committish}";
var remoteBranch = libGitRepo.Branches.FirstOrDefault(
branch => branch.FriendlyName.Equals(remoteName, StringComparison.Ordinal));
cancellationToken.ThrowIfCancellationRequested();
void RunCheckout() => commands.Checkout(
libGitRepo,
checkoutOptions,
committish);
if (remoteBranch == default)
throw;
try
{
RunCheckout();
}
catch (NotFoundException)
{
// Maybe (likely) a remote?
var remoteName = $"origin/{committish}";
var remoteBranch = libGitRepo.Branches.FirstOrDefault(
branch => branch.FriendlyName.Equals(remoteName, StringComparison.Ordinal));
cancellationToken.ThrowIfCancellationRequested();
logger.LogDebug("Creating local branch for {remoteBranchFriendlyName}...", remoteBranch.FriendlyName);
var branch = libGitRepo.CreateBranch(committish, remoteBranch.Tip);
if (remoteBranch == default)
throw;
libGitRepo.Branches.Update(branch, branchUpdate => branchUpdate.TrackedBranch = remoteBranch.CanonicalName);
logger.LogDebug("Creating local branch for {remoteBranchFriendlyName}...", remoteBranch.FriendlyName);
var branch = libGitRepo.CreateBranch(committish, remoteBranch.Tip);
cancellationToken.ThrowIfCancellationRequested();
libGitRepo.Branches.Update(branch, branchUpdate => branchUpdate.TrackedBranch = remoteBranch.CanonicalName);
RunCheckout();
cancellationToken.ThrowIfCancellationRequested();
RunCheckout();
}
}
cancellationToken.ThrowIfCancellationRequested();
@@ -22,11 +22,6 @@ namespace Tgstation.Server.Host.Components.Repository
/// </summary>
sealed class RepositoryUpdateService
{
/// <summary>
/// The <see cref="RepositoryUpdateRequest"/> for the <see cref="RepositoryUpdateService"/>.
/// </summary>
readonly RepositoryUpdateRequest model;
/// <summary>
/// The current <see cref="RepositorySettings"/> for the <see cref="RepositoryUpdateService"/>.
/// </summary>
@@ -50,19 +45,16 @@ namespace Tgstation.Server.Host.Components.Repository
/// <summary>
/// Initializes a new instance of the <see cref="RepositoryUpdateService"/> class.
/// </summary>
/// <param name="model">The value of <see cref="model"/>.</param>
/// <param name="currentModel">The value of <see cref="currentModel"/>.</param>
/// <param name="initiatingUser">The value of <see cref="initiatingUser"/>.</param>
/// <param name="logger">The value of <see cref="logger"/>.</param>
/// <param name="instanceId">The value of <see cref="instanceId"/>.</param>
public RepositoryUpdateService(
RepositoryUpdateRequest model,
RepositorySettings currentModel,
User initiatingUser,
ILogger<RepositoryUpdateService> logger,
long instanceId)
{
this.model = model ?? throw new ArgumentNullException(nameof(model));
this.currentModel = currentModel ?? throw new ArgumentNullException(nameof(currentModel));
this.initiatingUser = initiatingUser ?? throw new ArgumentNullException(nameof(initiatingUser));
this.logger = logger ?? throw new ArgumentNullException(nameof(logger));
@@ -139,24 +131,25 @@ namespace Tgstation.Server.Host.Components.Repository
/// <summary>
/// The job entrypoint used by <see cref="Controllers.RepositoryController"/> to update the repository's current HEAD.
/// </summary>
/// <param name="instance">The <see cref="IInstanceCore"/> the job is running on. <see langword="null"/> only when performing an instance move operation.</param>
/// <param name="model">The <see cref="RepositoryUpdateRequest"/>.</param>
/// <param name="instance">The <see cref="IInstanceCore"/> the job is running on.</param>
/// <param name="databaseContextFactory">The <see cref="IDatabaseContextFactory"/> for the operation.</param>
/// <param name="job">The running <see cref="Job"/>, ignored.</param>
/// <param name="progressReporter">The <see cref="JobProgressReporter"/> for the job.</param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> for the operation.</param>
/// <returns>A <see cref="ValueTask"/> representing the running operation.</returns>
#pragma warning disable CA1502, CA1506 // TODO: Decomplexify
public async ValueTask RepositoryUpdateJob(
RepositoryUpdateRequest model,
IInstanceCore? instance,
IDatabaseContextFactory databaseContextFactory,
Job job,
JobProgressReporter progressReporter,
CancellationToken cancellationToken)
#pragma warning restore CA1502, CA1506
{
ArgumentNullException.ThrowIfNull(model);
ArgumentNullException.ThrowIfNull(instance);
_ = job; // shuts up an IDE warning
ArgumentNullException.ThrowIfNull(databaseContextFactory);
ArgumentNullException.ThrowIfNull(progressReporter);
var repoManager = instance.RepositoryManager;
using var repo = await repoManager.LoadRepository(cancellationToken) ?? throw new JobException(ErrorCode.RepoMissing);
@@ -315,6 +308,7 @@ namespace Tgstation.Server.Host.Components.Repository
currentModel.AccessUser,
currentModel.AccessToken,
updateSubmodules,
false,
NextProgressReporter("Checkout"),
cancellationToken);
await CallLoadRevInfo(); // we've either seen origin before or what we're checking out is on origin
@@ -579,6 +573,7 @@ namespace Tgstation.Server.Host.Components.Repository
currentModel.AccessUser,
currentModel.AccessToken,
true,
false,
progressReporter.CreateSection($"Checkout {startReference ?? startSha[..7]}", secondStep ? 0.5 : 1.0),
default);
@@ -588,5 +583,86 @@ namespace Tgstation.Server.Host.Components.Repository
throw;
}
}
/// <summary>
/// The job entrypoint used by <see cref="Controllers.RepositoryController"/> to reclone a repository.
/// </summary>
/// <param name="instance">The <see cref="IInstanceCore"/> the job is running on.</param>
/// <param name="databaseContextFactory">The <see cref="IDatabaseContextFactory"/> for the operation.</param>
/// <param name="progressReporter">The <see cref="JobProgressReporter"/> for the job.</param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> for the operation.</param>
/// <returns>A <see cref="ValueTask"/> representing the running operation.</returns>
public async ValueTask RepositoryRecloneJob(
IInstanceCore? instance,
IDatabaseContextFactory databaseContextFactory,
JobProgressReporter progressReporter,
CancellationToken cancellationToken)
{
ArgumentNullException.ThrowIfNull(instance);
ArgumentNullException.ThrowIfNull(databaseContextFactory);
ArgumentNullException.ThrowIfNull(progressReporter);
progressReporter.StageName = "Loading Old Repository";
Uri origin;
string? oldReference;
string oldSha;
ValueTask deleteTask;
using (var oldRepo = await instance.RepositoryManager.LoadRepository(cancellationToken))
{
if (oldRepo == null)
throw new JobException(ErrorCode.RepoMissing);
origin = oldRepo.Origin;
oldSha = oldRepo.Head;
oldReference = oldRepo.Reference;
if (oldReference == Repository.NoReference)
oldReference = null;
progressReporter.StageName = "Deleting Old Repository";
deleteTask = instance.RepositoryManager.DeleteRepository(cancellationToken);
}
await deleteTask;
progressReporter.ReportProgress(0.1);
IRepository newRepo;
try
{
newRepo = await instance.RepositoryManager.CloneRepository(
origin,
oldReference,
currentModel.AccessUser,
currentModel.AccessToken,
progressReporter.CreateSection("Cloning New Repository", 0.8),
true, // TODO: Make configurable maybe...
cancellationToken)
?? throw new JobException("A race condition occurred while recloning the repository. Somehow, it was fully cloned instantly after being deleted!"); // I'll take lines of code that should never be hit for $10k
}
catch (Exception ex) when (ex is not JobException)
{
logger.LogWarning("Reclone failed, clearing credentials!");
// need to clear credentials here
await databaseContextFactory.UseContextTaskReturn(context =>
{
context.RepositorySettings.Attach(currentModel);
currentModel.AccessUser = null;
currentModel.AccessToken = null;
return context.Save(CancellationToken.None); // DCT: Must always run
});
throw;
}
using (newRepo)
await newRepo.CheckoutObject(
oldSha,
currentModel.AccessUser,
currentModel.AccessToken,
false,
oldReference != null,
progressReporter.CreateSection("Checking out previous Detached Commit", 0.1),
cancellationToken);
}
}
}
@@ -121,6 +121,9 @@ namespace Tgstation.Server.Host.Components.Session
/// </summary>
public FifoSemaphore TopicSendSemaphore { get; }
/// <inheritdoc />
public long MemoryUsage => process.MemoryUsage;
/// <summary>
/// The <see cref="Byond.TopicSender.ITopicClient"/> for the <see cref="SessionController"/>.
/// </summary>
@@ -24,6 +24,11 @@ namespace Tgstation.Server.Host.Components.Watchdog
/// </summary>
WatchdogStatus Status { get; }
/// <summary>
/// Gets the memory usage of the game server in bytes.
/// </summary>
long? MemoryUsage { get; }
/// <summary>
/// If the alpha server is the active server.
/// </summary>
@@ -50,6 +50,9 @@ namespace Tgstation.Server.Host.Components.Watchdog
}
}
/// <inheritdoc />
public long? MemoryUsage => GetActiveController()?.MemoryUsage;
/// <inheritdoc />
public abstract bool AlphaIsActive { get; }
@@ -29,5 +29,10 @@
/// If <see langword="true"/>, deployments that fail will not be immediately cleaned up. They will be cleaned up the next time the instance is onlined.
/// </summary>
public bool DelayCleaningFailedDeployments { get; set; }
/// <summary>
/// If set dd.exe will not be used on Windows systems in versions where it is present. Instead dreamdaemon.exe will always be used.
/// </summary>
public bool ForceUseDreamDaemonExe { get; set; }
}
}
@@ -7,6 +7,7 @@ using System.Threading.Tasks;
using System.Web;
using Microsoft.AspNetCore.Mvc;
using Microsoft.Extensions.Caching.Memory;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options;
@@ -42,6 +43,11 @@ namespace Tgstation.Server.Host.Controllers
/// </summary>
const string OctokitException = "Bad GitHub API response, check configuration!";
/// <summary>
/// The <see cref="IMemoryCache"/> key for <see cref="Read(bool?, CancellationToken)"/>.
/// </summary>
static readonly object ReadCacheKey = new();
/// <summary>
/// The <see cref="IGitHubServiceFactory"/> for the <see cref="AdministrationController"/>.
/// </summary>
@@ -77,6 +83,11 @@ namespace Tgstation.Server.Host.Controllers
/// </summary>
readonly IFileTransferTicketProvider fileTransferService;
/// <summary>
/// The <see cref="IMemoryCache"/> for the <see cref="AdministrationController"/>.
/// </summary>
readonly IMemoryCache cacheService;
/// <summary>
/// The <see cref="FileLoggingConfiguration"/> for the <see cref="AdministrationController"/>.
/// </summary>
@@ -94,6 +105,7 @@ namespace Tgstation.Server.Host.Controllers
/// <param name="ioManager">The value of <see cref="ioManager"/>.</param>
/// <param name="platformIdentifier">The value of <see cref="platformIdentifier"/>.</param>
/// <param name="fileTransferService">The value of <see cref="fileTransferService"/>.</param>
/// <param name="cacheService">The value of <see cref="cacheService"/>.</param>
/// <param name="logger">The <see cref="ILogger"/> for the <see cref="ApiController"/>.</param>
/// <param name="fileLoggingConfigurationOptions">The <see cref="IOptions{TOptions}"/> containing value of <see cref="fileLoggingConfiguration"/>.</param>
/// <param name="apiHeadersProvider">The <see cref="IApiHeadersProvider"/> for the <see cref="ApiController"/>.</param>
@@ -107,6 +119,7 @@ namespace Tgstation.Server.Host.Controllers
IIOManager ioManager,
IPlatformIdentifier platformIdentifier,
IFileTransferTicketProvider fileTransferService,
IMemoryCache cacheService,
ILogger<AdministrationController> logger,
IOptions<FileLoggingConfiguration> fileLoggingConfigurationOptions,
IApiHeadersProvider apiHeadersProvider)
@@ -124,12 +137,14 @@ namespace Tgstation.Server.Host.Controllers
this.ioManager = ioManager ?? throw new ArgumentNullException(nameof(ioManager));
this.platformIdentifier = platformIdentifier ?? throw new ArgumentNullException(nameof(platformIdentifier));
this.fileTransferService = fileTransferService ?? throw new ArgumentNullException(nameof(fileTransferService));
this.cacheService = cacheService ?? throw new ArgumentNullException(nameof(cacheService));
fileLoggingConfiguration = fileLoggingConfigurationOptions?.Value ?? throw new ArgumentNullException(nameof(fileLoggingConfigurationOptions));
}
/// <summary>
/// Get <see cref="AdministrationResponse"/> server information.
/// </summary>
/// <param name="fresh">If <see langword="true"/>, the cache should be bypassed.</param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> for the operation.</param>
/// <returns>A <see cref="ValueTask{TResult}"/> resulting in the <see cref="IActionResult"/> for the operation.</returns>
/// <response code="200">Retrieved <see cref="AdministrationResponse"/> data successfully.</response>
@@ -140,39 +155,56 @@ namespace Tgstation.Server.Host.Controllers
[ProducesResponseType(typeof(AdministrationResponse), 200)]
[ProducesResponseType(typeof(ErrorMessageResponse), 424)]
[ProducesResponseType(typeof(ErrorMessageResponse), 429)]
public async ValueTask<IActionResult> Read(CancellationToken cancellationToken)
public async ValueTask<IActionResult> Read([FromQuery] bool? fresh, CancellationToken cancellationToken)
{
try
{
Version? greatestVersion = null;
Uri? repoUrl = null;
try
async Task<JsonResult> CacheFactory()
{
var gitHubService = gitHubServiceFactory.CreateService();
var repositoryUrlTask = gitHubService.GetUpdatesRepositoryUrl(cancellationToken);
var releases = await gitHubService.GetTgsReleases(cancellationToken);
foreach (var kvp in releases)
Version? greatestVersion = null;
Uri? repoUrl = null;
try
{
var version = kvp.Key;
var release = kvp.Value;
if (version.Major > 3 // Forward/backward compatible but not before TGS4
&& (greatestVersion == null || version > greatestVersion))
greatestVersion = version;
var gitHubService = gitHubServiceFactory.CreateService();
var repositoryUrlTask = gitHubService.GetUpdatesRepositoryUrl(cancellationToken);
var releases = await gitHubService.GetTgsReleases(cancellationToken);
foreach (var kvp in releases)
{
var version = kvp.Key;
var release = kvp.Value;
if (version.Major > 3 // Forward/backward compatible but not before TGS4
&& (greatestVersion == null || version > greatestVersion))
greatestVersion = version;
}
repoUrl = await repositoryUrlTask;
}
catch (NotFoundException e)
{
Logger.LogWarning(e, "Not found exception while retrieving upstream repository info!");
}
repoUrl = await repositoryUrlTask;
}
catch (NotFoundException e)
{
Logger.LogWarning(e, "Not found exception while retrieving upstream repository info!");
return Json(new AdministrationResponse
{
LatestVersion = greatestVersion,
TrackedRepositoryUrl = repoUrl,
GeneratedAt = DateTimeOffset.UtcNow,
});
}
return Json(new AdministrationResponse
var ttl = TimeSpan.FromMinutes(30);
Task<JsonResult> task;
if (fresh == true || !cacheService.TryGetValue(ReadCacheKey, out var rawCacheObject))
{
LatestVersion = greatestVersion,
TrackedRepositoryUrl = repoUrl,
});
using var entry = cacheService.CreateEntry(ReadCacheKey);
entry.AbsoluteExpirationRelativeToNow = ttl;
entry.Value = task = CacheFactory();
}
else
task = (Task<JsonResult>)rawCacheObject!;
return await task;
}
catch (RateLimitExceededException e)
{
@@ -317,11 +317,12 @@ namespace Tgstation.Server.Host.Controllers
/// <param name="knownForcedReboot">If there was a settings change made that forced a switch to <see cref="RebootState.Restart"/>.</param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> for the operation.</param>
/// <returns>A <see cref="ValueTask{TResult}"/> resulting in the <see cref="IActionResult"/> of the operation.</returns>
#pragma warning disable CA1502 // TODO: Decomplexify
ValueTask<IActionResult> ReadImpl(DreamDaemonSettings? settings, bool knownForcedReboot, CancellationToken cancellationToken)
#pragma warning restore CA1502
=> WithComponentInstance(async instance =>
{
var dd = instance.Watchdog;
var metadata = (AuthenticationContext.GetRight(RightsType.DreamDaemon) & (ulong)DreamDaemonRights.ReadMetadata) != 0;
var revision = (AuthenticationContext.GetRight(RightsType.DreamDaemon) & (ulong)DreamDaemonRights.ReadRevision) != 0;
@@ -372,6 +373,7 @@ namespace Tgstation.Server.Host.Controllers
result.Visibility = settings.Visibility!.Value;
result.SoftRestart = rstate == RebootState.Restart;
result.SoftShutdown = rstate == RebootState.Shutdown;
result.ImmediateMemoryUsage = dd.MemoryUsage;
if (rstate == RebootState.Normal && knownForcedReboot)
result.SoftRestart = true;
@@ -223,6 +223,42 @@ namespace Tgstation.Server.Host.Controllers
return Accepted(api);
}
/// <summary>
/// Delete the repository.
/// </summary>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> for the operation.</param>
/// <returns>A <see cref="ValueTask{TResult}"/> resulting in the <see cref="IActionResult"/> of the operation.</returns>
/// <response code="202">Job to delete the repository created successfully.</response>
/// <response code="410">The database entity for the requested instance could not be retrieved. The instance was likely detached.</response>
[HttpPatch]
[TgsAuthorize(RepositoryRights.Reclone)]
[ProducesResponseType(typeof(RepositoryResponse), 202)]
[ProducesResponseType(typeof(ErrorMessageResponse), 410)]
public async ValueTask<IActionResult> Reclone(CancellationToken cancellationToken)
{
var currentModel = await DatabaseContext
.RepositorySettings
.AsQueryable()
.Where(x => x.InstanceId == Instance.Id)
.FirstOrDefaultAsync(cancellationToken);
if (currentModel == default)
return this.Gone();
Logger.LogInformation("Instance {instanceId} repository reclone initiated by user {userId}", Instance.Id, AuthenticationContext.User.Require(x => x.Id));
var repositoryUpdater = CreateRepositoryUpdateService(currentModel);
var job = Job.Create(JobCode.RepositoryReclone, AuthenticationContext.User, Instance);
var api = currentModel.ToApi();
await jobManager.RegisterOperation(
job,
(core, databaseContextFactory, paramJob, progressReporter, ct) => repositoryUpdater.RepositoryRecloneJob(core, databaseContextFactory, progressReporter, ct),
cancellationToken);
api.ActiveJob = job.ToApi();
return Accepted(api);
}
/// <summary>
/// Get the repository's status.
/// </summary>
@@ -443,17 +479,12 @@ namespace Tgstation.Server.Host.Controllers
var job = Job.Create(JobCode.RepositoryUpdate, AuthenticationContext.User, Instance, RepositoryRights.CancelPendingChanges);
job.Description = description;
var repositoryUpdater = new RepositoryUpdateService(
model,
currentModel,
AuthenticationContext.User,
loggerFactory.CreateLogger<RepositoryUpdateService>(),
Instance.Require(x => x.Id));
var repositoryUpdater = CreateRepositoryUpdateService(currentModel);
// Time to access git, do it in a job
await jobManager.RegisterOperation(
job,
repositoryUpdater.RepositoryUpdateJob,
(instance, databaseContextFactory, _, progressReporter, jobToken) => repositoryUpdater.RepositoryUpdateJob(model, instance, databaseContextFactory, progressReporter, jobToken),
cancellationToken);
api.ActiveJob = job.ToApi();
@@ -494,5 +525,17 @@ namespace Tgstation.Server.Host.Controllers
cancellationToken);
return needsDbUpdate;
}
/// <summary>
/// Creates a <see cref="RepositoryUpdateService"/>.
/// </summary>
/// <param name="currentModel">The current <see cref="RepositorySettings"/>.</param>
/// <returns>A new <see cref="RepositoryUpdateService"/>.</returns>
RepositoryUpdateService CreateRepositoryUpdateService(RepositorySettings currentModel)
=> new(
currentModel,
AuthenticationContext.User,
loggerFactory.CreateLogger<RepositoryUpdateService>(),
Instance.Require(x => x.Id));
}
}
@@ -13,6 +13,11 @@ namespace Tgstation.Server.Host.System
/// </summary>
Task<int?> Lifetime { get; }
/// <summary>
/// Gets the process' memory usage in bytes.
/// </summary>
long MemoryUsage { get; }
/// <summary>
/// Set's the owned <see cref="global::System.Diagnostics.Process.PriorityClass"/> to a non-normal value.
/// </summary>
@@ -22,6 +22,9 @@ namespace Tgstation.Server.Host.System
/// <inheritdoc />
public Task<int?> Lifetime { get; }
/// <inheritdoc />
public long MemoryUsage => handle.VirtualMemorySize64;
/// <summary>
/// The <see cref="IProcessFeatures"/> for the <see cref="Process"/>.
/// </summary>
@@ -115,9 +115,9 @@
<!-- Usage: File logging plugin -->
<PackageReference Include="Serilog.Sinks.File" Version="6.0.0" />
<!-- Usage: OpenAPI spec generator -->
<PackageReference Include="Swashbuckle.AspNetCore" Version="6.6.2" />
<PackageReference Include="Swashbuckle.AspNetCore" Version="6.7.0" />
<!-- Usage: Newtonsoft.Json plugin for OpenAPI spec generator -->
<PackageReference Include="Swashbuckle.AspNetCore.Newtonsoft" Version="6.6.2" />
<PackageReference Include="Swashbuckle.AspNetCore.Newtonsoft" Version="6.7.0" />
<!-- Usage: Windows authentication plugin allowing searching for users by name -->
<PackageReference Include="System.DirectoryServices.AccountManagement" Version="8.0.0" />
<!-- Usage: Identifying owning user of Windows Process objects -->
@@ -23,6 +23,7 @@ Session:
HighPriorityLiveDreamDaemon: false # If DreamDaemon instances should run as higher priority processes
LowPriorityDeploymentProcesses: true # If TGS Deployments should run as lower priority processes
DelayCleaningFailedDeployments: false # If true, deployments that fail will not be immediately cleaned up. They will be cleaned up the next time the instance is onlined
ForceUseDreamDaemonExe: false # If true, dd.exe will not be used on Windows systems in versions where it is present. Instead dreamdaemon.exe will always be used.
FileLogging:
Directory: # Directory in which log files are stored. Windows default: %PROGRAMDATA%/tgstation-server. Linux default: /var/log/tgstation-server
Disable: true # Disable file logging entirely
@@ -1,10 +1,10 @@
using Microsoft.VisualStudio.TestTools.UnitTesting;
using System;
using System;
using System.IO;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.VisualStudio.TestTools.UnitTesting;
using Tgstation.Server.Api.Models;
using Tgstation.Server.Api.Models.Response;
using Tgstation.Server.Client;
@@ -54,10 +54,24 @@ namespace Tgstation.Server.Tests.Live
async Task TestRead(CancellationToken cancellationToken)
{
var model = await client.Read(cancellationToken);
var model = await client.Read(false, cancellationToken);
//we've released a few 5.x versions now, check the release checker is at least somewhat functional
Assert.IsTrue(4 < model.LatestVersion.Major);
Assert.IsNotNull(model.TrackedRepositoryUrl);
Assert.IsTrue(model.GeneratedAt.HasValue);
Assert.IsTrue(model.GeneratedAt.Value <= DateTimeOffset.UtcNow);
// test the cache
var newerModel = await client.Read(false, cancellationToken);
Assert.AreEqual(model.GeneratedAt, newerModel.GeneratedAt);
await Task.Delay(TimeSpan.FromSeconds(1), cancellationToken);
var newestModel = await client.Read(true, cancellationToken);
Assert.AreNotEqual(model.GeneratedAt, newestModel.GeneratedAt);
Assert.IsNotNull(newestModel.GeneratedAt);
Assert.IsTrue(model.GeneratedAt < newestModel.GeneratedAt);
}
}
}
@@ -262,7 +262,7 @@ namespace Tgstation.Server.Tests.Live.Instance
Assert.AreEqual(HubConnectionState.Disconnected, permlessConn.State);
// force token refreshs
await Task.WhenAll(permedUser.Administration.Read(cancellationToken).AsTask(), permlessUser.Instances.List(null, cancellationToken).AsTask());
await Task.WhenAll(permedUser.Administration.Read(false, cancellationToken).AsTask(), permlessUser.Instances.List(null, cancellationToken).AsTask());
if (!permlessPsId.HasValue)
{
@@ -116,6 +116,8 @@ namespace Tgstation.Server.Tests.Live.Instance
await ApiAssert.ThrowsException<ApiConflictException, RepositoryResponse>(() => Checkout(new RepositoryUpdateRequest { Reference = "master", CheckoutSha = "286bb75" }, false, false, cancellationToken), ErrorCode.RepoMismatchShaAndReference);
var updated = await Checkout(new RepositoryUpdateRequest { CheckoutSha = "286bb75" }, false, false, cancellationToken);
await RecloneTest(cancellationToken);
// Fake SHA
updated = await Checkout(new RepositoryUpdateRequest { CheckoutSha = "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" }, true, false, cancellationToken);
@@ -142,6 +144,23 @@ namespace Tgstation.Server.Tests.Live.Instance
await TestMergeTests(updated, prNumber, cancellationToken);
}
async ValueTask RecloneTest(CancellationToken cancellationToken)
{
var initialState = await repositoryClient.Read(cancellationToken);
Assert.IsNotNull(initialState.Reference);
Assert.IsNotNull(initialState.RevisionInformation);
Assert.IsNotNull(initialState.RevisionInformation.CommitSha);
Assert.IsNotNull(initialState.RevisionInformation.OriginCommitSha);
var reclone = await repositoryClient.Reclone(cancellationToken);
await WaitForJob(reclone.ActiveJob, 70, false, null, cancellationToken);
var newState = await repositoryClient.Read(cancellationToken);
Assert.AreEqual(initialState.Reference, newState.Reference);
Assert.AreEqual(initialState.RevisionInformation.CommitSha, newState.RevisionInformation.CommitSha);
Assert.AreEqual(initialState.RevisionInformation.OriginCommitSha, newState.RevisionInformation.OriginCommitSha);
}
async ValueTask<RepositoryResponse> Checkout(RepositoryUpdateRequest updated, bool expectFailure, bool isRef, CancellationToken cancellationToken)
{
var newRef = isRef ? updated.Reference : updated.CheckoutSha;
@@ -150,6 +150,7 @@ namespace Tgstation.Server.Tests.Live.Instance
}, cancellationToken);
Assert.AreEqual<ushort?>(47, updated.OpenDreamTopicPort);
Assert.IsFalse(updated.ImmediateMemoryUsage.HasValue);
}
catch (ConflictException ex) when (ex.ErrorCode == ErrorCode.PortNotAvailable)
{
@@ -721,6 +722,8 @@ namespace Tgstation.Server.Tests.Live.Instance
await CheckDDPriority();
Assert.AreEqual(false, daemonStatus.SoftRestart);
Assert.AreEqual(false, daemonStatus.SoftShutdown);
Assert.IsTrue(daemonStatus.ImmediateMemoryUsage.HasValue);
Assert.AreNotEqual(0, daemonStatus.ImmediateMemoryUsage.Value);
await GracefulWatchdogShutdown(cancellationToken);
@@ -1365,6 +1368,9 @@ namespace Tgstation.Server.Tests.Live.Instance
Assert.IsNotNull(daemonStatus.ActiveCompileJob);
ValidateSessionId(daemonStatus, true);
Assert.IsTrue(daemonStatus.ImmediateMemoryUsage.HasValue);
Assert.AreNotEqual(0, daemonStatus.ImmediateMemoryUsage.Value);
Assert.AreEqual(initialStatus.ActiveCompileJob.Id, daemonStatus.ActiveCompileJob.Id);
var newerCompileJob = daemonStatus.StagedCompileJob;
Assert.AreNotEqual(daemonStatus.ActiveCompileJob.EngineVersion, newerCompileJob.EngineVersion);
@@ -216,7 +216,7 @@ namespace Tgstation.Server.Tests.Live
};
var badClient = clientFactory.CreateFromToken(serverClient.Url, newToken);
await ApiAssert.ThrowsException<UnauthorizedException, AdministrationResponse>(() => badClient.Administration.Read(cancellationToken));
await ApiAssert.ThrowsException<UnauthorizedException, AdministrationResponse>(() => badClient.Administration.Read(false, cancellationToken));
await ApiAssert.ThrowsException<UnauthorizedException, ServerInformationResponse>(() => badClient.ServerInformation(cancellationToken));
}
@@ -670,7 +670,7 @@ namespace Tgstation.Server.Tests.Live
"asdfasdfasdfasdf");
await using var node1BadClient = clientFactory.CreateFromToken(node1.RootUrl, controllerUserClient.Token);
await ApiAssert.ThrowsException<UnauthorizedException, AdministrationResponse>(() => node1BadClient.Administration.Read(cancellationToken));
await ApiAssert.ThrowsException<UnauthorizedException, AdministrationResponse>(() => node1BadClient.Administration.Read(false, cancellationToken));
// check instance info is not shared
var controllerInstance = await controllerClient.Instances.CreateOrAttach(
@@ -45,7 +45,7 @@ namespace Tgstation.Server.Tests
() => { });
const string StartSha = "af4da8beb9f9b374b04a3cc4d65acca662e8cc1a";
await repo.CheckoutObject(StartSha, null, null, true, new JobProgressReporter(Mock.Of<ILogger<JobProgressReporter>>(), null, (stage, progress) => { }), CancellationToken.None);
await repo.CheckoutObject(StartSha, null, null, true, false, new JobProgressReporter(Mock.Of<ILogger<JobProgressReporter>>(), null, (stage, progress) => { }), CancellationToken.None);
Assert.AreEqual(Host.Components.Repository.Repository.NoReference, repo.Reference);
+10 -5
View File
@@ -135,7 +135,7 @@ namespace Tgstation.Server.ReleaseNotes
return 454233;
}
await GenerateAppCredentials(client, args[1]);
await GenerateAppCredentials(client, args[1], false);
return await EnsureRelease(client);
}
@@ -171,7 +171,8 @@ namespace Tgstation.Server.ReleaseNotes
return 33847;
}
await GenerateAppCredentials(client, args[2]);
bool toSS13 = args.Length > 3 && args[3].Equals("--spacestation13", StringComparison.OrdinalIgnoreCase);
await GenerateAppCredentials(client, args[2], toSS13);
var token = client.Credentials.GetToken();
var destPath = args[1];
@@ -1640,7 +1641,7 @@ package (version) distribution(s); urgency=urgency
return 0;
}
static async ValueTask GenerateAppCredentials(GitHubClient gitHubClient, string pemBase64)
static async ValueTask GenerateAppCredentials(GitHubClient gitHubClient, string pemBase64, bool toSS13)
{
var pemBytes = Convert.FromBase64String(pemBase64);
var pem = Encoding.UTF8.GetString(pemBytes);
@@ -1665,7 +1666,11 @@ package (version) distribution(s); urgency=urgency
gitHubClient.Credentials = new Credentials(jwtStr, AuthenticationType.Bearer);
var installation = await gitHubClient.GitHubApps.GetRepositoryInstallationForCurrent(RepoOwner, RepoName);
var installation = await gitHubClient.GitHubApps.GetRepositoryInstallationForCurrent(
toSS13
? "spacestation13"
: RepoOwner,
RepoName);
var installToken = await gitHubClient.GitHubApps.CreateInstallationToken(installation.Id);
gitHubClient.Credentials = new Credentials(installToken.Token);
@@ -1673,7 +1678,7 @@ package (version) distribution(s); urgency=urgency
static async ValueTask<int> CICompletionCheck(GitHubClient gitHubClient, string currentSha, string pemBase64)
{
await GenerateAppCredentials(gitHubClient, pemBase64);
await GenerateAppCredentials(gitHubClient, pemBase64, false);
await gitHubClient.Check.Run.Create(RepoOwner, RepoName, new NewCheckRun("CI Completion", currentSha)
{