mirror of
https://github.com/tgstation/tgstation-server.git
synced 2026-08-26 06:27:19 +01:00
+3
-3
@@ -5,10 +5,10 @@
|
||||
<PropertyGroup>
|
||||
<TgsCoreVersion>6.8.0</TgsCoreVersion>
|
||||
<TgsConfigVersion>5.1.0</TgsConfigVersion>
|
||||
<TgsApiVersion>10.6.0</TgsApiVersion>
|
||||
<TgsApiVersion>10.7.0</TgsApiVersion>
|
||||
<TgsCommonLibraryVersion>7.0.0</TgsCommonLibraryVersion>
|
||||
<TgsApiLibraryVersion>13.6.0</TgsApiLibraryVersion>
|
||||
<TgsClientVersion>15.6.0</TgsClientVersion>
|
||||
<TgsApiLibraryVersion>13.7.0</TgsApiLibraryVersion>
|
||||
<TgsClientVersion>16.0.0</TgsClientVersion>
|
||||
<TgsDmapiVersion>7.2.0</TgsDmapiVersion>
|
||||
<TgsInteropVersion>5.9.0</TgsInteropVersion>
|
||||
<TgsHostWatchdogVersion>1.4.1</TgsHostWatchdogVersion>
|
||||
|
||||
@@ -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; }
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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)
|
||||
{
|
||||
|
||||
@@ -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)
|
||||
{
|
||||
|
||||
@@ -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(
|
||||
|
||||
Reference in New Issue
Block a user