mirror of
https://github.com/tgstation/tgstation-server.git
synced 2026-08-25 22:17:51 +01:00
Merge pull request #1463 from tgstation/1084-DeleteByond [APIDeploy][NugetDeploy][DMDeploy]
DELETE /Byond
This commit is contained in:
@@ -6,7 +6,7 @@ namespace Tgstation.Server.Api.Models
|
||||
/// <summary>
|
||||
/// Types of <see cref="Response.ErrorMessageResponse"/>s that the API may return.
|
||||
/// </summary>
|
||||
/// <remarks>Entries marked with the <see cref="ObsoleteAttribute"/> are no longer in use but kept for reference.</remarks>
|
||||
/// <remarks>Entries marked with the <see cref="ObsoleteAttribute"/> are no longer in use but kept for placeholders until they can be recycled in the next major API version.</remarks>
|
||||
public enum ErrorCode : uint
|
||||
{
|
||||
/// <summary>
|
||||
@@ -178,11 +178,10 @@ namespace Tgstation.Server.Api.Models
|
||||
ConfigurationDirectoryNotEmpty,
|
||||
|
||||
/// <summary>
|
||||
/// Currently unused.
|
||||
/// The server swarm has less than the expected amount of nodes.
|
||||
/// </summary>
|
||||
[Obsolete("Unused", true)]
|
||||
[Description("Unknown error code.")]
|
||||
UnusedErrorCode1,
|
||||
[Description("The server swarm has less than the expected amount of nodes!")]
|
||||
SwarmIntegrityCheckFailed,
|
||||
|
||||
/// <summary>
|
||||
/// One of <see cref="Internal.RepositorySettings.AccessUser"/> and <see cref="Internal.RepositorySettings.AccessToken"/> is set while the other isn't.
|
||||
@@ -227,11 +226,10 @@ namespace Tgstation.Server.Api.Models
|
||||
RepoMismatchShaAndUpdate,
|
||||
|
||||
/// <summary>
|
||||
/// Currently unused.
|
||||
/// Could not delete a BYOND version due to it being set as the active version for the instance.
|
||||
/// </summary>
|
||||
[Obsolete("Unused", true)]
|
||||
[Description("Unknown error code.")]
|
||||
UnusedErrorCode2,
|
||||
[Description("Could not delete BYOND version due to it being selected as the instance's active version.")]
|
||||
ByondCannotDeleteActiveVersion,
|
||||
|
||||
/// <summary>
|
||||
/// <see cref="Request.RepositoryUpdateRequest.NewTestMerges"/> contained duplicate <see cref="TestMergeParameters.Number"/>s.
|
||||
@@ -630,10 +628,6 @@ namespace Tgstation.Server.Api.Models
|
||||
[Description("The deployment took longer than the configured timeout!")]
|
||||
DeploymentTimeout,
|
||||
|
||||
/// <summary>
|
||||
/// The server swarm has less than the expected amount of nodes.
|
||||
/// </summary>
|
||||
[Description("The server swarm has less than the expected amount of nodes!")]
|
||||
SwarmIntegrityCheckFailed,
|
||||
// This comment is here to remind you that there is one more unused error code above and you should use it first
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,16 @@
|
||||
using System;
|
||||
|
||||
namespace Tgstation.Server.Api.Models.Request
|
||||
{
|
||||
/// <summary>
|
||||
/// A request to delete a specific <see cref="Version"/>.
|
||||
/// </summary>
|
||||
public class ByondVersionDeleteRequest
|
||||
{
|
||||
/// <summary>
|
||||
/// The BYOND version to install.
|
||||
/// </summary>
|
||||
[RequestOptions(FieldPresence.Required)]
|
||||
public Version? Version { get; set; }
|
||||
}
|
||||
}
|
||||
@@ -1,18 +1,10 @@
|
||||
using System;
|
||||
|
||||
namespace Tgstation.Server.Api.Models.Request
|
||||
namespace Tgstation.Server.Api.Models.Request
|
||||
{
|
||||
/// <summary>
|
||||
/// A request to install a BYOND <see cref="Version"/>.
|
||||
/// A request to install a BYOND <see cref="ByondVersionDeleteRequest.Version"/>.
|
||||
/// </summary>
|
||||
public sealed class ByondVersionRequest
|
||||
public sealed class ByondVersionRequest : ByondVersionDeleteRequest
|
||||
{
|
||||
/// <summary>
|
||||
/// The BYOND version to install.
|
||||
/// </summary>
|
||||
[RequestOptions(FieldPresence.Required)]
|
||||
public Version? Version { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// If a custom BYOND version is to be uploaded.
|
||||
/// </summary>
|
||||
|
||||
@@ -37,5 +37,10 @@ namespace Tgstation.Server.Api.Rights
|
||||
/// User may upload and activate custom BYOND builds.
|
||||
/// </summary>
|
||||
InstallCustomVersion = 1 << 4,
|
||||
|
||||
/// <summary>
|
||||
/// User may delete non-active BYOND builds.
|
||||
/// </summary>
|
||||
DeleteInstall = 1 << 5,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -16,7 +16,7 @@
|
||||
<RepositoryUrl>https://github.com/tgstation/tgstation-server</RepositoryUrl>
|
||||
<Copyright>2018-2023</Copyright>
|
||||
<PackageTags>json web api tgstation-server tgstation ss13 byond</PackageTags>
|
||||
<PackageReleaseNotes>Added ErrorCode.SwarmIntegrityCheckFailed.</PackageReleaseNotes>
|
||||
<PackageReleaseNotes>Added ByondRights.DeleteInstall, ErrorCode.SwarmIntegrityCheckFailed, ErrorCode.ByondCannotDeleteActiveVersion, and Models.Request.ByondVersionDeleteRequest.</PackageReleaseNotes>
|
||||
<IncludeSymbols>true</IncludeSymbols>
|
||||
<SymbolPackageFormat>snupkg</SymbolPackageFormat>
|
||||
<CodeAnalysisRuleSet>../../build/analyzers.ruleset</CodeAnalysisRuleSet>
|
||||
|
||||
@@ -209,6 +209,9 @@ namespace Tgstation.Server.Client
|
||||
/// <inheritdoc />
|
||||
public Task<TResult> Delete<TResult>(string route, long instanceId, CancellationToken cancellationToken) => RunRequest<TResult>(route, null, HttpMethod.Delete, instanceId, false, cancellationToken);
|
||||
|
||||
/// <inheritdoc />
|
||||
public Task<TResult> Delete<TBody, TResult>(string route, TBody body, long instanceId, CancellationToken cancellationToken) where TBody : class => RunRequest<TBody, TResult>(route, body, HttpMethod.Delete, instanceId, false, cancellationToken);
|
||||
|
||||
/// <inheritdoc />
|
||||
public Task<TResult> Create<TResult>(string route, long instanceId, CancellationToken cancellationToken) => RunRequest<object, TResult>(route, new object(), HttpMethod.Put, instanceId, false, cancellationToken);
|
||||
|
||||
|
||||
@@ -33,6 +33,10 @@ namespace Tgstation.Server.Client.Components
|
||||
/// <inheritdoc />
|
||||
public Task<ByondResponse> ActiveVersion(CancellationToken cancellationToken) => ApiClient.Read<ByondResponse>(Routes.Byond, instance.Id!.Value, cancellationToken);
|
||||
|
||||
/// <inheritdoc />
|
||||
public Task<JobResponse> DeleteVersion(ByondVersionDeleteRequest deleteRequest, CancellationToken cancellationToken)
|
||||
=> ApiClient.Delete<ByondVersionDeleteRequest, JobResponse>(Routes.Byond, deleteRequest, instance.Id!.Value, cancellationToken);
|
||||
|
||||
/// <inheritdoc />
|
||||
public Task<IReadOnlyList<ByondResponse>> InstalledVersions(PaginationSettings? paginationSettings, CancellationToken cancellationToken)
|
||||
=> ReadPaged<ByondResponse>(paginationSettings, Routes.ListRoute(Routes.Byond), instance.Id, cancellationToken);
|
||||
|
||||
@@ -29,12 +29,20 @@ namespace Tgstation.Server.Client.Components
|
||||
Task<IReadOnlyList<ByondResponse>> InstalledVersions(PaginationSettings? paginationSettings, CancellationToken cancellationToken);
|
||||
|
||||
/// <summary>
|
||||
/// Updates the <see cref="ByondInstallResponse"/> information.
|
||||
/// Updates the active BYOND version.
|
||||
/// </summary>
|
||||
/// <param name="installRequest">The <see cref="ByondVersionRequest"/>.</param>
|
||||
/// <param name="zipFileStream">The <see cref="Stream"/> for the .zip file if <see cref="ByondVersionRequest.UploadCustomZip"/> is <see langword="true"/>.</param>
|
||||
/// <param name="cancellationToken">The <see cref="CancellationToken"/> for the operation.</param>
|
||||
/// <returns>A <see cref="Task{TResult}"/> resulting in the updated <see cref="ByondInstallResponse"/> information.</returns>
|
||||
Task<ByondInstallResponse> SetActiveVersion(ByondVersionRequest installRequest, Stream? zipFileStream, CancellationToken cancellationToken);
|
||||
|
||||
/// <summary>
|
||||
/// Starts a jobs to delete a specific BYOND version.
|
||||
/// </summary>
|
||||
/// <param name="deleteRequest">The <see cref="ByondVersionDeleteRequest"/> specifying the version to delete.</param>
|
||||
/// <param name="cancellationToken">The <see cref="CancellationToken"/> for the operation.</param>
|
||||
/// <returns>A <see cref="Task{TResult}"/> resulting in the <see cref="JobResponse"/> for the delete job.</returns>
|
||||
Task<JobResponse> DeleteVersion(ByondVersionDeleteRequest deleteRequest, CancellationToken cancellationToken);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -204,6 +204,18 @@ namespace Tgstation.Server.Client
|
||||
/// <returns>A <see cref="Task{TResult}"/> resulting in the response body as a <typeparamref name="TResult"/>.</returns>
|
||||
Task<TResult> Delete<TResult>(string route, long instanceId, CancellationToken cancellationToken);
|
||||
|
||||
/// <summary>
|
||||
/// Run an HTTP DELETE request.
|
||||
/// </summary>
|
||||
/// <typeparam name="TBody">The type to of the request body.</typeparam>
|
||||
/// <typeparam name="TResult">The type of the response body.</typeparam>
|
||||
/// <param name="route">The server route to make the request to.</param>
|
||||
/// <param name="body">The request body.</param>
|
||||
/// <param name="instanceId">The instance <see cref="EntityId.Id"/> to make the request to.</param>
|
||||
/// <param name="cancellationToken">The <see cref="CancellationToken"/> for the operation.</param>
|
||||
/// <returns>A <see cref="Task"/> representing the running operation.</returns>
|
||||
Task<TResult> Delete<TBody, TResult>(string route, TBody body, long instanceId, CancellationToken cancellationToken) where TBody : class;
|
||||
|
||||
/// <summary>
|
||||
/// Downloads a file <see cref="Stream"/> for a given <paramref name="ticket"/>.
|
||||
/// </summary>
|
||||
|
||||
@@ -16,7 +16,7 @@
|
||||
<RepositoryUrl>https://github.com/tgstation/tgstation-server</RepositoryUrl>
|
||||
<Copyright>2018-2023</Copyright>
|
||||
<PackageTags>json web api tgstation-server tgstation ss13 byond client</PackageTags>
|
||||
<PackageReleaseNotes>Updated definitions for API version 9.10.0.</PackageReleaseNotes>
|
||||
<PackageReleaseNotes>Updated definitions for API version 9.10.0. Added support for deleting BYOND versions.</PackageReleaseNotes>
|
||||
<IncludeSymbols>true</IncludeSymbols>
|
||||
<SymbolPackageFormat>snupkg</SymbolPackageFormat>
|
||||
<CodeAnalysisRuleSet>../../build/analyzers.ruleset</CodeAnalysisRuleSet>
|
||||
|
||||
@@ -0,0 +1,41 @@
|
||||
using System;
|
||||
using System.Net.Http.Headers;
|
||||
|
||||
namespace Tgstation.Server.Common
|
||||
{
|
||||
/// <summary>
|
||||
/// <see cref="IAbstractHttpClientFactory"/> that creates <see cref="HttpClient"/>s.
|
||||
/// </summary>
|
||||
public sealed class HttpClientFactory : IAbstractHttpClientFactory
|
||||
{
|
||||
/// <inheritdoc />
|
||||
public IHttpClient CreateClient()
|
||||
{
|
||||
var client = new HttpClient();
|
||||
try
|
||||
{
|
||||
client.DefaultRequestHeaders.UserAgent.Add(userAgent);
|
||||
return client;
|
||||
}
|
||||
catch
|
||||
{
|
||||
client.Dispose();
|
||||
throw;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// The <see cref="ProductInfoHeaderValue"/> used as created client's User-Agent header on request.
|
||||
/// </summary>
|
||||
readonly ProductInfoHeaderValue userAgent;
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="HttpClientFactory"/> class.
|
||||
/// </summary>
|
||||
/// <param name="userAgent">The value of <see cref="userAgent"/>.</param>
|
||||
public HttpClientFactory(ProductInfoHeaderValue userAgent)
|
||||
{
|
||||
this.userAgent = userAgent ?? throw new ArgumentNullException(nameof(userAgent));
|
||||
}
|
||||
}
|
||||
}
|
||||
+1
-3
@@ -1,6 +1,4 @@
|
||||
using Tgstation.Server.Common;
|
||||
|
||||
namespace Tgstation.Server.Host.Core
|
||||
namespace Tgstation.Server.Common
|
||||
{
|
||||
/// <summary>
|
||||
/// Creates <see cref="IHttpClient"/>s.
|
||||
@@ -1,114 +1,25 @@
|
||||
using System;
|
||||
using System.Text;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
using Tgstation.Server.Host.Core;
|
||||
using Tgstation.Server.Host.IO;
|
||||
using Tgstation.Server.Host.Utils;
|
||||
|
||||
namespace Tgstation.Server.Host.Components.Byond
|
||||
{
|
||||
/// <inheritdoc />
|
||||
sealed class ByondExecutableLock : IByondExecutableLock
|
||||
sealed class ByondExecutableLock : ReferenceCounter<ByondInstallation>, IByondExecutableLock
|
||||
{
|
||||
/// <inheritdoc />
|
||||
public Version Version { get; }
|
||||
public Version Version => Instance.Version;
|
||||
|
||||
/// <inheritdoc />
|
||||
public string DreamDaemonPath { get; }
|
||||
public string DreamDaemonPath => Instance.DreamDaemonPath;
|
||||
|
||||
/// <inheritdoc />
|
||||
public string DreamMakerPath { get; }
|
||||
public string DreamMakerPath => Instance.DreamMakerPath;
|
||||
|
||||
/// <inheritdoc />
|
||||
public bool SupportsCli { get; }
|
||||
|
||||
/// <summary>
|
||||
/// The <see cref="IIOManager"/> for the <see cref="ByondExecutableLock"/>.
|
||||
/// </summary>
|
||||
readonly IIOManager ioManager;
|
||||
|
||||
/// <summary>
|
||||
/// <see cref="SemaphoreSlim"/> used to guard access to the <see cref="trustedFilePath"/>.
|
||||
/// </summary>
|
||||
readonly SemaphoreSlim trustedFileSemaphore;
|
||||
|
||||
/// <summary>
|
||||
/// The path to the BYOND trusted .dmbs configuration file.
|
||||
/// </summary>
|
||||
readonly string trustedFilePath;
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="ByondExecutableLock"/> class.
|
||||
/// </summary>
|
||||
/// <param name="ioManager">The value of <see cref="ioManager"/>.</param>
|
||||
/// <param name="trustedFileSemaphore">The value of <see cref="trustedFileSemaphore"/>.</param>
|
||||
/// <param name="version">The value of <see cref="Version"/>.</param>
|
||||
/// <param name="dreamDaemonPath">The value of <see cref="DreamDaemonPath"/>.</param>
|
||||
/// <param name="dreamMakerPath">The value of <see cref="DreamMakerPath"/>.</param>
|
||||
/// <param name="trustedFilePath">The value of <see cref="trustedFilePath"/>.</param>
|
||||
/// <param name="supportsCli">The value of <see cref="SupportsCli"/>.</param>
|
||||
public ByondExecutableLock(
|
||||
IIOManager ioManager,
|
||||
SemaphoreSlim trustedFileSemaphore,
|
||||
Version version,
|
||||
string dreamDaemonPath,
|
||||
string dreamMakerPath,
|
||||
string trustedFilePath,
|
||||
bool supportsCli)
|
||||
{
|
||||
this.ioManager = ioManager ?? throw new ArgumentNullException(nameof(ioManager));
|
||||
this.trustedFileSemaphore = trustedFileSemaphore ?? throw new ArgumentNullException(nameof(trustedFileSemaphore));
|
||||
Version = version ?? throw new ArgumentNullException(nameof(version));
|
||||
DreamDaemonPath = dreamDaemonPath ?? throw new ArgumentNullException(nameof(dreamDaemonPath));
|
||||
DreamMakerPath = dreamMakerPath ?? throw new ArgumentNullException(nameof(dreamMakerPath));
|
||||
this.trustedFilePath = trustedFilePath ?? throw new ArgumentNullException(nameof(trustedFilePath));
|
||||
|
||||
SupportsCli = supportsCli;
|
||||
}
|
||||
|
||||
// at one point in design, byond versions were to delete themselves if they weren't the active version
|
||||
// That changed at some point so these functions are intentioanlly left blank
|
||||
public bool SupportsCli => Instance.SupportsCli;
|
||||
|
||||
/// <inheritdoc />
|
||||
public void Dispose()
|
||||
{
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public void DoNotDeleteThisSession()
|
||||
{
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public async Task TrustDmbPath(string fullDmbPath, CancellationToken cancellationToken)
|
||||
{
|
||||
if (fullDmbPath == null)
|
||||
throw new ArgumentNullException(nameof(fullDmbPath));
|
||||
|
||||
using (await SemaphoreSlimContext.Lock(trustedFileSemaphore, cancellationToken))
|
||||
{
|
||||
string trustedFileText;
|
||||
|
||||
if (await ioManager.FileExists(trustedFilePath, cancellationToken))
|
||||
{
|
||||
var trustedFileBytes = await ioManager.ReadAllBytes(trustedFilePath, cancellationToken);
|
||||
trustedFileText = Encoding.UTF8.GetString(trustedFileBytes);
|
||||
trustedFileText = $"{trustedFileText.Trim()}{Environment.NewLine}";
|
||||
}
|
||||
else
|
||||
{
|
||||
trustedFileText = String.Empty;
|
||||
}
|
||||
|
||||
if (trustedFileText.Contains(fullDmbPath, StringComparison.Ordinal))
|
||||
return;
|
||||
|
||||
trustedFileText = $"{trustedFileText}{fullDmbPath}{Environment.NewLine}";
|
||||
|
||||
var newTrustedFileBytes = Encoding.UTF8.GetBytes(trustedFileText);
|
||||
await ioManager.WriteAllBytes(trustedFilePath, newTrustedFileBytes, cancellationToken);
|
||||
}
|
||||
}
|
||||
public void DoNotDeleteThisSession() => DangerousDropReference();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,48 @@
|
||||
using System;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace Tgstation.Server.Host.Components.Byond
|
||||
{
|
||||
/// <inheritdoc />
|
||||
sealed class ByondInstallation : IByondInstallation
|
||||
{
|
||||
/// <inheritdoc />
|
||||
public Version Version { get; }
|
||||
|
||||
/// <inheritdoc />
|
||||
public string DreamDaemonPath { get; }
|
||||
|
||||
/// <inheritdoc />
|
||||
public string DreamMakerPath { get; }
|
||||
|
||||
/// <inheritdoc />
|
||||
public bool SupportsCli { get; }
|
||||
|
||||
/// <summary>
|
||||
/// The <see cref="Task"/> that completes when the BYOND version finished installing.
|
||||
/// </summary>
|
||||
public Task InstallationTask { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="ByondInstallation"/> class.
|
||||
/// </summary>
|
||||
/// <param name="installationTask">The value of <see cref="InstallationTask"/>.</param>
|
||||
/// <param name="version">The value of <see cref="Version"/>.</param>
|
||||
/// <param name="dreamDaemonPath">The value of <see cref="DreamDaemonPath"/>.</param>
|
||||
/// <param name="dreamMakerPath">The value of <see cref="DreamMakerPath"/>.</param>
|
||||
/// <param name="supportsCli">The value of <see cref="SupportsCli"/>.</param>
|
||||
public ByondInstallation(
|
||||
Task installationTask,
|
||||
Version version,
|
||||
string dreamDaemonPath,
|
||||
string dreamMakerPath,
|
||||
bool supportsCli)
|
||||
{
|
||||
InstallationTask = installationTask ?? throw new ArgumentNullException(nameof(installationTask));
|
||||
Version = version ?? throw new ArgumentNullException(nameof(version));
|
||||
DreamDaemonPath = dreamDaemonPath ?? throw new ArgumentNullException(nameof(dreamDaemonPath));
|
||||
DreamMakerPath = dreamMakerPath ?? throw new ArgumentNullException(nameof(dreamMakerPath));
|
||||
SupportsCli = supportsCli;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -9,12 +9,12 @@ using System.Threading.Tasks;
|
||||
|
||||
using Microsoft.Extensions.Logging;
|
||||
|
||||
using Tgstation.Server.Api;
|
||||
using Tgstation.Server.Api.Models;
|
||||
using Tgstation.Server.Host.Components.Events;
|
||||
using Tgstation.Server.Host.Core;
|
||||
using Tgstation.Server.Host.Extensions;
|
||||
using Tgstation.Server.Host.IO;
|
||||
using Tgstation.Server.Host.Jobs;
|
||||
using Tgstation.Server.Host.Utils;
|
||||
|
||||
namespace Tgstation.Server.Host.Components.Byond
|
||||
{
|
||||
@@ -37,12 +37,12 @@ namespace Tgstation.Server.Host.Components.Byond
|
||||
const string TrustedDmbFileName = "trusted.txt";
|
||||
|
||||
/// <summary>
|
||||
/// The file in which we store the <see cref="VersionKey(Version, bool)"/> for installations.
|
||||
/// The file in which we store the <see cref="Version"/> for installations.
|
||||
/// </summary>
|
||||
const string VersionFileName = "Version.txt";
|
||||
|
||||
/// <summary>
|
||||
/// The file in which we store the <see cref="VersionKey(Version, bool)"/> for the active installation.
|
||||
/// The file in which we store the <see cref="ActiveVersion"/>.
|
||||
/// </summary>
|
||||
const string ActiveVersionFileName = "ActiveVersion.txt";
|
||||
|
||||
@@ -55,10 +55,15 @@ namespace Tgstation.Server.Host.Components.Byond
|
||||
get
|
||||
{
|
||||
lock (installedVersions)
|
||||
return installedVersions.Select(x => Version.Parse(x.Key).Semver()).ToList();
|
||||
return installedVersions.Keys.ToList();
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// <see cref="SemaphoreSlim"/> for writing to files in the user's BYOND directory.
|
||||
/// </summary>
|
||||
static readonly SemaphoreSlim UserFilesSemaphore = new (1);
|
||||
|
||||
/// <summary>
|
||||
/// The <see cref="IIOManager"/> for the <see cref="ByondManager"/>.
|
||||
/// </summary>
|
||||
@@ -82,22 +87,30 @@ namespace Tgstation.Server.Host.Components.Byond
|
||||
/// <summary>
|
||||
/// Map of byond <see cref="Version"/>s to <see cref="Task"/>s that complete when they are installed.
|
||||
/// </summary>
|
||||
readonly Dictionary<string, Task> installedVersions;
|
||||
readonly Dictionary<Version, ReferenceCountingContainer<ByondInstallation, ByondExecutableLock>> installedVersions;
|
||||
|
||||
/// <summary>
|
||||
/// The <see cref="SemaphoreSlim"/> for the <see cref="ByondManager"/>.
|
||||
/// The <see cref="SemaphoreSlim"/> for changing or deleting the active BYOND version.
|
||||
/// </summary>
|
||||
readonly SemaphoreSlim semaphore;
|
||||
readonly SemaphoreSlim changeDeleteSemaphore;
|
||||
|
||||
/// <summary>
|
||||
/// Converts a BYOND <paramref name="version"/> to a <see cref="string"/>.
|
||||
/// <see cref="TaskCompletionSource"/> that notifes when the <see cref="ActiveVersion"/> changes.
|
||||
/// </summary>
|
||||
/// <param name="version">The <see cref="Version"/> to convert.</param>
|
||||
/// <param name="allowPatch">If the <see cref="Version.Build"/> property of <paramref name="version"/> should be kept.</param>
|
||||
/// <returns>The <see cref="string"/> representation of <paramref name="version"/>.</returns>
|
||||
static string VersionKey(Version version, bool allowPatch) => (allowPatch && version.Build > 0
|
||||
? new Version(version.Major, version.Minor, version.Build)
|
||||
: new Version(version.Major, version.Minor)).ToString();
|
||||
TaskCompletionSource activeVersionChanged;
|
||||
|
||||
/// <summary>
|
||||
/// Validates a given <paramref name="version"/> parameter.
|
||||
/// </summary>
|
||||
/// <param name="version">The <see cref="Version"/> to validate.</param>
|
||||
static void CheckVersionParameter(Version version)
|
||||
{
|
||||
if (version == null)
|
||||
throw new ArgumentNullException(nameof(version));
|
||||
|
||||
if (version.Build == 0)
|
||||
throw new ArgumentException("version.Build cannot be 0!", nameof(version));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="ByondManager"/> class.
|
||||
@@ -113,68 +126,170 @@ namespace Tgstation.Server.Host.Components.Byond
|
||||
this.eventConsumer = eventConsumer ?? throw new ArgumentNullException(nameof(eventConsumer));
|
||||
this.logger = logger ?? throw new ArgumentNullException(nameof(logger));
|
||||
|
||||
installedVersions = new Dictionary<string, Task>();
|
||||
semaphore = new SemaphoreSlim(1);
|
||||
installedVersions = new Dictionary<Version, ReferenceCountingContainer<ByondInstallation, ByondExecutableLock>>();
|
||||
changeDeleteSemaphore = new SemaphoreSlim(1);
|
||||
activeVersionChanged = new TaskCompletionSource();
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public void Dispose() => semaphore.Dispose();
|
||||
public void Dispose() => changeDeleteSemaphore.Dispose();
|
||||
|
||||
/// <inheritdoc />
|
||||
public async Task ChangeVersion(Version version, Stream customVersionStream, CancellationToken cancellationToken)
|
||||
public async Task ChangeVersion(
|
||||
JobProgressReporter progressReporter,
|
||||
Version version,
|
||||
Stream customVersionStream,
|
||||
bool allowInstallation,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
if (version == null)
|
||||
throw new ArgumentNullException(nameof(version));
|
||||
CheckVersionParameter(version);
|
||||
|
||||
var versionKey = await InstallVersion(version, customVersionStream, cancellationToken);
|
||||
using (await SemaphoreSlimContext.Lock(semaphore, cancellationToken))
|
||||
using (await SemaphoreSlimContext.Lock(changeDeleteSemaphore, cancellationToken))
|
||||
{
|
||||
await ioManager.WriteAllBytes(ActiveVersionFileName, Encoding.UTF8.GetBytes(versionKey), cancellationToken);
|
||||
using var installLock = await AssertAndLockVersion(
|
||||
progressReporter,
|
||||
version,
|
||||
customVersionStream,
|
||||
false,
|
||||
allowInstallation,
|
||||
cancellationToken);
|
||||
|
||||
// We reparse the version because it could be changed after a custom install.
|
||||
version = installLock.Version;
|
||||
|
||||
var stringVersion = version.ToString();
|
||||
await ioManager.WriteAllBytes(ActiveVersionFileName, Encoding.UTF8.GetBytes(stringVersion), cancellationToken);
|
||||
await eventConsumer.HandleEvent(
|
||||
EventType.ByondActiveVersionChange,
|
||||
new List<string>
|
||||
{
|
||||
ActiveVersion != null
|
||||
? VersionKey(ActiveVersion, true)
|
||||
: null,
|
||||
versionKey,
|
||||
ActiveVersion?.ToString(),
|
||||
stringVersion,
|
||||
},
|
||||
cancellationToken)
|
||||
;
|
||||
cancellationToken);
|
||||
|
||||
// We reparse the version key because it could be changed after a custom install.
|
||||
ActiveVersion = Version.Parse(versionKey);
|
||||
ActiveVersion = version;
|
||||
activeVersionChanged.SetResult();
|
||||
activeVersionChanged = new TaskCompletionSource();
|
||||
}
|
||||
|
||||
logger.LogInformation("Active version changed to {version}", version);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public async Task<IByondExecutableLock> UseExecutables(Version requiredVersion, string trustDmbFullPath, CancellationToken cancellationToken)
|
||||
{
|
||||
logger.LogTrace(
|
||||
"Acquiring lock on BYOND version {version}...",
|
||||
requiredVersion?.ToString() ?? $"{ActiveVersion} (active)");
|
||||
var versionToUse = requiredVersion ?? ActiveVersion ?? throw new JobException(ErrorCode.ByondNoVersionsInstalled);
|
||||
var installLock = await AssertAndLockVersion(
|
||||
null,
|
||||
versionToUse,
|
||||
null,
|
||||
requiredVersion != null,
|
||||
true,
|
||||
cancellationToken);
|
||||
try
|
||||
{
|
||||
if (trustDmbFullPath != null)
|
||||
await TrustDmbPath(trustDmbFullPath, cancellationToken);
|
||||
|
||||
return installLock;
|
||||
}
|
||||
catch
|
||||
{
|
||||
installLock.Dispose();
|
||||
throw;
|
||||
}
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public async Task<IByondExecutableLock> UseExecutables(Version requiredVersion, CancellationToken cancellationToken)
|
||||
public async Task DeleteVersion(JobProgressReporter progressReporter, Version version, CancellationToken cancellationToken)
|
||||
{
|
||||
var versionToUse = requiredVersion ?? ActiveVersion ?? throw new JobException(ErrorCode.ByondNoVersionsInstalled);
|
||||
await InstallVersion(versionToUse, null, cancellationToken);
|
||||
if (progressReporter == null)
|
||||
throw new ArgumentNullException(nameof(progressReporter));
|
||||
|
||||
var versionKey = VersionKey(versionToUse, true);
|
||||
var binPathForVersion = ioManager.ConcatPath(versionKey, BinPath);
|
||||
CheckVersionParameter(version);
|
||||
|
||||
logger.LogTrace("Creating ByondExecutableLock lock for version {versionToUse}", versionToUse);
|
||||
return new ByondExecutableLock(
|
||||
ioManager,
|
||||
semaphore,
|
||||
versionToUse,
|
||||
ioManager.ResolvePath(
|
||||
ioManager.ConcatPath(
|
||||
binPathForVersion,
|
||||
byondInstaller.GetDreamDaemonName(versionToUse, out var supportsCli))),
|
||||
ioManager.ResolvePath(
|
||||
ioManager.ConcatPath(
|
||||
binPathForVersion,
|
||||
byondInstaller.DreamMakerName)),
|
||||
ioManager.ResolvePath(
|
||||
ioManager.ConcatPath(
|
||||
byondInstaller.PathToUserByondFolder,
|
||||
CfgDirectoryName,
|
||||
TrustedDmbFileName)),
|
||||
supportsCli);
|
||||
logger.LogTrace("DeleteVersion {version}", version);
|
||||
|
||||
if (version == ActiveVersion)
|
||||
throw new JobException(ErrorCode.ByondCannotDeleteActiveVersion);
|
||||
|
||||
ReferenceCountingContainer<ByondInstallation, ByondExecutableLock> container;
|
||||
lock (installedVersions)
|
||||
if (!installedVersions.TryGetValue(version, out container))
|
||||
return; // already "deleted"
|
||||
|
||||
logger.LogInformation("Deleting BYOND version {version}...", version);
|
||||
progressReporter.StageName = "Waiting for version to not be in use...";
|
||||
while (true)
|
||||
{
|
||||
var containerTask = container.OnZeroReferences;
|
||||
|
||||
// We also want to check when the active version changes in case we need to fail the job because of that.
|
||||
Task activeVersionUpdate;
|
||||
using (await SemaphoreSlimContext.Lock(changeDeleteSemaphore, cancellationToken))
|
||||
activeVersionUpdate = activeVersionChanged.Task;
|
||||
|
||||
await Task.WhenAny(
|
||||
containerTask,
|
||||
activeVersionUpdate)
|
||||
.WithToken(cancellationToken);
|
||||
|
||||
if (containerTask.IsCompleted)
|
||||
logger.LogTrace("All BYOND locks for {version} are gone", version);
|
||||
|
||||
using (await SemaphoreSlimContext.Lock(changeDeleteSemaphore, cancellationToken))
|
||||
{
|
||||
// check again because it could have become the active version.
|
||||
if (version == ActiveVersion)
|
||||
throw new JobException(ErrorCode.ByondCannotDeleteActiveVersion);
|
||||
|
||||
bool proceed;
|
||||
lock (installedVersions)
|
||||
{
|
||||
proceed = container.OnZeroReferences.IsCompleted;
|
||||
if (proceed)
|
||||
if (!installedVersions.TryGetValue(version, out var newerContainer))
|
||||
logger.LogWarning("Unable to remove BYOND installation {version} from list! Is there a duplicate job running?", version);
|
||||
else
|
||||
{
|
||||
if (container != newerContainer)
|
||||
{
|
||||
// Okay let me get this straight, there was a duplicate delete job, it ran before us after we grabbed the container, AND another installation of the same version completed?
|
||||
// I know realistically this is practically impossible, but god damn that small possiblility
|
||||
// best thing to do is check we exclusively own the newer container
|
||||
logger.LogDebug("Extreme race condition encountered, applying concentrated copium...");
|
||||
container = newerContainer;
|
||||
proceed = container.OnZeroReferences.IsCompleted;
|
||||
}
|
||||
|
||||
if (proceed)
|
||||
installedVersions.Remove(version);
|
||||
}
|
||||
}
|
||||
|
||||
if (proceed)
|
||||
{
|
||||
progressReporter.StageName = "Deleting installation...";
|
||||
|
||||
// delete the version file first, because we will know not to re-discover the installation if it's not present and it will get cleaned on reboot
|
||||
var installPath = version.ToString();
|
||||
await ioManager.DeleteFile(
|
||||
ioManager.ConcatPath(installPath, VersionFileName),
|
||||
cancellationToken);
|
||||
await ioManager.DeleteDirectory(installPath, cancellationToken);
|
||||
return;
|
||||
}
|
||||
|
||||
if (containerTask.IsCompleted)
|
||||
logger.LogDebug(
|
||||
"Another lock was acquired before we could remove version {version} from the list. We will have to wait again.",
|
||||
version);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
@@ -188,27 +303,29 @@ namespace Tgstation.Server.Host.Components.Byond
|
||||
|
||||
var activeVersionBytesTask = GetActiveVersion();
|
||||
|
||||
// Create local cfg directory in case it doesn't exist
|
||||
var localCfgDirectory = ioManager.ConcatPath(
|
||||
byondInstaller.PathToUserByondFolder,
|
||||
CfgDirectoryName);
|
||||
await ioManager.CreateDirectory(
|
||||
localCfgDirectory,
|
||||
cancellationToken);
|
||||
|
||||
// Delete trusted.txt so it doesn't grow too large
|
||||
var trustedFilePath =
|
||||
ioManager.ConcatPath(
|
||||
using (await SemaphoreSlimContext.Lock(UserFilesSemaphore, cancellationToken))
|
||||
{
|
||||
// Create local cfg directory in case it doesn't exist
|
||||
var localCfgDirectory = ioManager.ConcatPath(
|
||||
byondInstaller.PathToUserByondFolder,
|
||||
CfgDirectoryName);
|
||||
await ioManager.CreateDirectory(
|
||||
localCfgDirectory,
|
||||
TrustedDmbFileName);
|
||||
logger.LogTrace("Deleting trusted .dmbs file {trustedFilePath}", trustedFilePath);
|
||||
await ioManager.DeleteFile(
|
||||
trustedFilePath,
|
||||
cancellationToken);
|
||||
cancellationToken);
|
||||
|
||||
var byondDirectory = ioManager.ResolvePath();
|
||||
await ioManager.CreateDirectory(byondDirectory, cancellationToken);
|
||||
var directories = await ioManager.GetDirectories(byondDirectory, cancellationToken);
|
||||
// Delete trusted.txt so it doesn't grow too large
|
||||
var trustedFilePath =
|
||||
ioManager.ConcatPath(
|
||||
localCfgDirectory,
|
||||
TrustedDmbFileName);
|
||||
logger.LogTrace("Deleting trusted .dmbs file {trustedFilePath}", trustedFilePath);
|
||||
await ioManager.DeleteFile(
|
||||
trustedFilePath,
|
||||
cancellationToken);
|
||||
}
|
||||
|
||||
await ioManager.CreateDirectory(DefaultIOManager.CurrentDirectory, cancellationToken);
|
||||
var directories = await ioManager.GetDirectories(DefaultIOManager.CurrentDirectory, cancellationToken);
|
||||
|
||||
var installedVersionPaths = new Dictionary<string, Version>();
|
||||
|
||||
@@ -217,27 +334,38 @@ namespace Tgstation.Server.Host.Components.Byond
|
||||
var versionFile = ioManager.ConcatPath(path, VersionFileName);
|
||||
if (!await ioManager.FileExists(versionFile, cancellationToken))
|
||||
{
|
||||
logger.LogInformation("Cleaning unparsable version path: {versionPath}", ioManager.ResolvePath(path));
|
||||
logger.LogWarning("Cleaning path with no version file: {versionPath}", ioManager.ResolvePath(path));
|
||||
await ioManager.DeleteDirectory(path, cancellationToken); // cleanup
|
||||
return;
|
||||
}
|
||||
|
||||
var bytes = await ioManager.ReadAllBytes(versionFile, cancellationToken);
|
||||
var text = Encoding.UTF8.GetString(bytes);
|
||||
if (Version.TryParse(text, out var version))
|
||||
if (!Version.TryParse(text, out var version))
|
||||
{
|
||||
var key = VersionKey(version, true);
|
||||
lock (installedVersions)
|
||||
if (!installedVersions.ContainsKey(key))
|
||||
{
|
||||
logger.LogDebug("Adding detected BYOND version {versionKey}...", key);
|
||||
installedVersions.Add(key, Task.CompletedTask);
|
||||
installedVersionPaths.Add(ioManager.ResolvePath(key), version);
|
||||
return;
|
||||
}
|
||||
logger.LogWarning("Cleaning path with unparsable version file: {versionPath}", ioManager.ResolvePath(path));
|
||||
await ioManager.DeleteDirectory(path, cancellationToken); // cleanup
|
||||
return;
|
||||
}
|
||||
|
||||
await ioManager.DeleteDirectory(path, cancellationToken);
|
||||
try
|
||||
{
|
||||
AddInstallationContainer(version, Task.CompletedTask);
|
||||
logger.LogDebug("Added detected BYOND version {versionKey}...", version);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
logger.LogWarning(
|
||||
ex,
|
||||
"It seems that there are multiple directories that say they contain BYOND version {version}. We're ignoring and cleaning the duplicate: {duplicatePath}",
|
||||
version,
|
||||
ioManager.ResolvePath(path));
|
||||
await ioManager.DeleteDirectory(path, cancellationToken);
|
||||
return;
|
||||
}
|
||||
|
||||
lock (installedVersionPaths)
|
||||
installedVersionPaths.Add(ioManager.ResolvePath(version.ToString()), version);
|
||||
}
|
||||
|
||||
await Task.WhenAll(directories.Select(ReadVersion));
|
||||
@@ -249,14 +377,18 @@ namespace Tgstation.Server.Host.Components.Byond
|
||||
if (activeVersionBytes != null)
|
||||
{
|
||||
var activeVersionString = Encoding.UTF8.GetString(activeVersionBytes);
|
||||
|
||||
Version activeVersion;
|
||||
bool hasRequestedActiveVersion;
|
||||
lock (installedVersions)
|
||||
hasRequestedActiveVersion = installedVersions.ContainsKey(activeVersionString);
|
||||
if (hasRequestedActiveVersion && Version.TryParse(activeVersionString, out var activeVersion))
|
||||
ActiveVersion = activeVersion;
|
||||
hasRequestedActiveVersion = Version.TryParse(activeVersionString, out activeVersion)
|
||||
&& installedVersions.ContainsKey(activeVersion);
|
||||
|
||||
if (hasRequestedActiveVersion)
|
||||
ActiveVersion = activeVersion; // not setting TCS because there's no need during init
|
||||
else
|
||||
{
|
||||
logger.LogWarning("Failed to load saved active version {0}!", activeVersionString);
|
||||
logger.LogWarning("Failed to load saved active version {activeVersion}!", activeVersionString);
|
||||
await ioManager.DeleteFile(ActiveVersionFileName, cancellationToken);
|
||||
}
|
||||
}
|
||||
@@ -266,18 +398,27 @@ namespace Tgstation.Server.Host.Components.Byond
|
||||
public Task StopAsync(CancellationToken cancellationToken) => Task.CompletedTask;
|
||||
|
||||
/// <summary>
|
||||
/// Installs a BYOND <paramref name="version"/> if it isn't already.
|
||||
/// Ensures a BYOND <paramref name="version"/> is installed if it isn't already.
|
||||
/// </summary>
|
||||
/// <param name="progressReporter">The optional <see cref="JobProgressReporter"/> for the operation.</param>
|
||||
/// <param name="version">The BYOND <see cref="Version"/> to install.</param>
|
||||
/// <param name="customVersionStream">Custom zip file <see cref="Stream"/> to use. Will cause a <see cref="Version.Build"/> number to be added.</param>
|
||||
/// <param name="neededForLock">If this BYOND version is required as part of a locking operation.</param>
|
||||
/// <param name="allowInstallation">If an installation should be performed if the <paramref name="version"/> is not installed. If <see langword="false"/> and an installation is required an <see cref="InvalidOperationException"/> will be thrown.</param>
|
||||
/// <param name="cancellationToken">The <see cref="CancellationToken"/> for the operation.</param>
|
||||
/// <returns>A <see cref="Task"/> representing the running operation.</returns>
|
||||
async Task<string> InstallVersion(Version version, Stream customVersionStream, CancellationToken cancellationToken)
|
||||
/// <returns>A <see cref="Task{TResult}"/> resulting in the <see cref="ByondExecutableLock"/>.</returns>
|
||||
async Task<ByondExecutableLock> AssertAndLockVersion(
|
||||
JobProgressReporter progressReporter,
|
||||
Version version,
|
||||
Stream customVersionStream,
|
||||
bool neededForLock,
|
||||
bool allowInstallation,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
var ourTcs = new TaskCompletionSource();
|
||||
Task inProgressTask;
|
||||
string versionKey;
|
||||
bool installed;
|
||||
ByondInstallation installation;
|
||||
ByondExecutableLock installLock;
|
||||
bool installedOrInstalling;
|
||||
lock (installedVersions)
|
||||
{
|
||||
if (customVersionStream != null)
|
||||
@@ -285,102 +426,226 @@ namespace Tgstation.Server.Host.Components.Byond
|
||||
int customInstallationNumber = 1;
|
||||
do
|
||||
{
|
||||
versionKey = $"{VersionKey(version, false)}.{customInstallationNumber++}";
|
||||
version = new Version(version.Major, version.Minor, customInstallationNumber);
|
||||
}
|
||||
while (installedVersions.ContainsKey(versionKey));
|
||||
while (installedVersions.ContainsKey(version));
|
||||
}
|
||||
else
|
||||
versionKey = VersionKey(version, true);
|
||||
|
||||
installed = installedVersions.TryGetValue(versionKey, out inProgressTask);
|
||||
if (!installed)
|
||||
installedVersions.Add(versionKey, ourTcs.Task);
|
||||
installedOrInstalling = installedVersions.TryGetValue(version, out var installationContainer);
|
||||
if (!installedOrInstalling)
|
||||
{
|
||||
if (!allowInstallation)
|
||||
throw new InvalidOperationException($"BYOND version {version} not installed!");
|
||||
|
||||
installationContainer = AddInstallationContainer(version, ourTcs.Task);
|
||||
}
|
||||
|
||||
installation = installationContainer.Instance;
|
||||
installLock = installationContainer.AddReference();
|
||||
}
|
||||
|
||||
if (installed)
|
||||
using (cancellationToken.Register(() => ourTcs.SetCanceled()))
|
||||
{
|
||||
await Task.WhenAny(ourTcs.Task, inProgressTask);
|
||||
cancellationToken.ThrowIfCancellationRequested();
|
||||
return versionKey;
|
||||
}
|
||||
|
||||
if (customVersionStream != null)
|
||||
logger.LogInformation("Installing custom BYOND version as {versionKey}...", versionKey);
|
||||
else if (version.Build > 0)
|
||||
throw new JobException(ErrorCode.ByondNonExistentCustomVersion);
|
||||
else
|
||||
logger.LogDebug("Requested BYOND version {versionKey} not currently installed. Doing so now...", versionKey);
|
||||
|
||||
// okay up to us to install it then
|
||||
try
|
||||
{
|
||||
await eventConsumer.HandleEvent(EventType.ByondInstallStart, new List<string> { versionKey }, cancellationToken);
|
||||
|
||||
var extractPath = ioManager.ResolvePath(versionKey);
|
||||
async Task DirectoryCleanup()
|
||||
if (installedOrInstalling)
|
||||
{
|
||||
await ioManager.DeleteDirectory(extractPath, cancellationToken);
|
||||
await ioManager.CreateDirectory(extractPath, cancellationToken);
|
||||
if (progressReporter != null)
|
||||
progressReporter.StageName = "Waiting for existing installation job...";
|
||||
|
||||
if (neededForLock && !installation.InstallationTask.IsCompleted)
|
||||
logger.LogWarning("The required BYOND version ({version}) is not readily available! We will have to wait for it to install.", version);
|
||||
|
||||
await installation.InstallationTask.WithToken(cancellationToken);
|
||||
return installLock;
|
||||
}
|
||||
|
||||
var directoryCleanupTask = DirectoryCleanup();
|
||||
// okay up to us to install it then
|
||||
try
|
||||
{
|
||||
Stream versionZipStream;
|
||||
Stream downloadedStream = null;
|
||||
if (customVersionStream == null)
|
||||
if (customVersionStream != null)
|
||||
logger.LogInformation("Installing custom BYOND version as {version}...", version);
|
||||
else if (neededForLock)
|
||||
{
|
||||
downloadedStream = await byondInstaller.DownloadVersion(version, cancellationToken);
|
||||
versionZipStream = downloadedStream;
|
||||
if (version.Build > 0)
|
||||
throw new JobException(ErrorCode.ByondNonExistentCustomVersion);
|
||||
|
||||
logger.LogWarning("The required BYOND version ({version}) is not readily available! We will have to install it.", version);
|
||||
}
|
||||
else
|
||||
versionZipStream = customVersionStream;
|
||||
logger.LogDebug("Requested BYOND version {version} not currently installed. Doing so now...", version);
|
||||
|
||||
using (downloadedStream)
|
||||
{
|
||||
await directoryCleanupTask;
|
||||
logger.LogTrace("Extracting downloaded BYOND zip to {extractPath}...", extractPath);
|
||||
await ioManager.ZipToDirectory(extractPath, versionZipStream, cancellationToken);
|
||||
}
|
||||
if (progressReporter != null)
|
||||
progressReporter.StageName = "Running event";
|
||||
|
||||
await byondInstaller.InstallByond(version, extractPath, cancellationToken);
|
||||
var versionString = version.ToString();
|
||||
await eventConsumer.HandleEvent(EventType.ByondInstallStart, new List<string> { versionString }, cancellationToken);
|
||||
|
||||
// make sure to do this last because this is what tells us we have a valid version in the future
|
||||
await ioManager.WriteAllBytes(
|
||||
ioManager.ConcatPath(versionKey, VersionFileName),
|
||||
Encoding.UTF8.GetBytes(versionKey),
|
||||
cancellationToken)
|
||||
;
|
||||
await InstallVersionFiles(progressReporter, version, customVersionStream, cancellationToken);
|
||||
|
||||
ourTcs.SetResult();
|
||||
}
|
||||
catch (HttpRequestException e)
|
||||
catch (Exception ex)
|
||||
{
|
||||
// since the user can easily provide non-exitent version numbers, we'll turn this into a JobException
|
||||
throw new JobException(ErrorCode.ByondDownloadFail, e);
|
||||
}
|
||||
catch (OperationCanceledException)
|
||||
{
|
||||
throw;
|
||||
}
|
||||
catch
|
||||
{
|
||||
await ioManager.DeleteDirectory(versionKey, cancellationToken);
|
||||
if (ex is not OperationCanceledException)
|
||||
await eventConsumer.HandleEvent(EventType.ByondInstallFail, new List<string> { ex.Message }, cancellationToken);
|
||||
|
||||
lock (installedVersions)
|
||||
installedVersions.Remove(version);
|
||||
|
||||
ourTcs.SetException(ex);
|
||||
throw;
|
||||
}
|
||||
|
||||
ourTcs.SetResult();
|
||||
return installLock;
|
||||
}
|
||||
catch (Exception e)
|
||||
catch
|
||||
{
|
||||
if (e is not OperationCanceledException)
|
||||
await eventConsumer.HandleEvent(EventType.ByondInstallFail, new List<string> { e.Message }, cancellationToken);
|
||||
lock (installedVersions)
|
||||
installedVersions.Remove(versionKey);
|
||||
ourTcs.SetException(e);
|
||||
installLock.Dispose();
|
||||
throw;
|
||||
}
|
||||
}
|
||||
|
||||
return versionKey;
|
||||
/// <summary>
|
||||
/// Installs the files for a given BYOND <paramref name="version"/>.
|
||||
/// </summary>
|
||||
/// <param name="progressReporter">The optional <see cref="JobProgressReporter"/> for the operation.</param>
|
||||
/// <param name="version">The BYOND <see cref="Version"/> being installed with the <see cref="Version.Build"/> number set if appropriate.</param>
|
||||
/// <param name="customVersionStream">Custom zip file <see cref="Stream"/> to use. Will cause a <see cref="Version.Build"/> number to be added.</param>
|
||||
/// <param name="cancellationToken">The <see cref="CancellationToken"/> for the operation.</param>
|
||||
/// <returns>A <see cref="Task"/> representing the running operation.</returns>
|
||||
async Task InstallVersionFiles(JobProgressReporter progressReporter, Version version, Stream customVersionStream, CancellationToken cancellationToken)
|
||||
{
|
||||
var installFullPath = ioManager.ResolvePath(version.ToString());
|
||||
async Task DirectoryCleanup()
|
||||
{
|
||||
await ioManager.DeleteDirectory(installFullPath, cancellationToken);
|
||||
await ioManager.CreateDirectory(installFullPath, cancellationToken);
|
||||
}
|
||||
|
||||
var directoryCleanupTask = DirectoryCleanup();
|
||||
try
|
||||
{
|
||||
Stream versionZipStream;
|
||||
if (customVersionStream == null)
|
||||
{
|
||||
if (progressReporter != null)
|
||||
progressReporter.StageName = "Downloading version";
|
||||
|
||||
versionZipStream = await byondInstaller.DownloadVersion(version, cancellationToken);
|
||||
}
|
||||
else
|
||||
versionZipStream = customVersionStream;
|
||||
|
||||
using (versionZipStream)
|
||||
{
|
||||
if (progressReporter != null)
|
||||
progressReporter.StageName = "Cleaning target directory";
|
||||
|
||||
await directoryCleanupTask;
|
||||
|
||||
if (progressReporter != null)
|
||||
progressReporter.StageName = "Extracting zip";
|
||||
|
||||
logger.LogTrace("Extracting downloaded BYOND zip to {extractPath}...", installFullPath);
|
||||
await ioManager.ZipToDirectory(installFullPath, versionZipStream, cancellationToken);
|
||||
}
|
||||
|
||||
if (progressReporter != null)
|
||||
progressReporter.StageName = "Running installation actions";
|
||||
|
||||
await byondInstaller.InstallByond(version, installFullPath, cancellationToken);
|
||||
|
||||
if (progressReporter != null)
|
||||
progressReporter.StageName = "Writing version file";
|
||||
|
||||
// make sure to do this last because this is what tells us we have a valid version in the future
|
||||
await ioManager.WriteAllBytes(
|
||||
ioManager.ConcatPath(installFullPath, VersionFileName),
|
||||
Encoding.UTF8.GetBytes(version.ToString()),
|
||||
cancellationToken);
|
||||
}
|
||||
catch (HttpRequestException e)
|
||||
{
|
||||
// since the user can easily provide non-exitent version numbers, we'll turn this into a JobException
|
||||
throw new JobException(ErrorCode.ByondDownloadFail, e);
|
||||
}
|
||||
catch (OperationCanceledException)
|
||||
{
|
||||
throw;
|
||||
}
|
||||
catch
|
||||
{
|
||||
await ioManager.DeleteDirectory(installFullPath, cancellationToken);
|
||||
throw;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Create and add a new <see cref="ByondInstallation"/> to <see cref="installedVersions"/>.
|
||||
/// </summary>
|
||||
/// <param name="version">The <see cref="Version"/> being added.</param>
|
||||
/// <param name="installationTask">The <see cref="Task"/> representing the installation process.</param>
|
||||
/// <returns>The new <see cref="ReferenceCountingContainer{TWrapped, TReference}"/> containing the new <see cref="ByondInstallation"/>.</returns>
|
||||
ReferenceCountingContainer<ByondInstallation, ByondExecutableLock> AddInstallationContainer(Version version, Task installationTask)
|
||||
{
|
||||
var binPathForVersion = ioManager.ConcatPath(version.ToString(), BinPath);
|
||||
var installation = new ByondInstallation(
|
||||
installationTask,
|
||||
version,
|
||||
ioManager.ResolvePath(
|
||||
ioManager.ConcatPath(
|
||||
binPathForVersion,
|
||||
byondInstaller.GetDreamDaemonName(version, out var supportsCli))),
|
||||
ioManager.ResolvePath(
|
||||
ioManager.ConcatPath(
|
||||
binPathForVersion,
|
||||
byondInstaller.DreamMakerName)),
|
||||
supportsCli);
|
||||
|
||||
var installationContainer = new ReferenceCountingContainer<ByondInstallation, ByondExecutableLock>(installation);
|
||||
|
||||
lock (installedVersions)
|
||||
installedVersions.Add(version, installationContainer);
|
||||
|
||||
return installationContainer;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Add a given <paramref name="fullDmbPath"/> to the trusted DMBs list in BYOND's config.
|
||||
/// </summary>
|
||||
/// <param name="fullDmbPath">Full path to the .dmb that should be trusted.</param>
|
||||
/// <param name="cancellationToken">The <see cref="CancellationToken"/> for the operation.</param>
|
||||
/// <returns>A <see cref="Task"/> representing the running operation.</returns>
|
||||
async Task TrustDmbPath(string fullDmbPath, CancellationToken cancellationToken)
|
||||
{
|
||||
var trustedFilePath = ioManager.ConcatPath(
|
||||
byondInstaller.PathToUserByondFolder,
|
||||
CfgDirectoryName,
|
||||
TrustedDmbFileName);
|
||||
|
||||
logger.LogDebug("Adding .dmb ({dmbPath}) to {trustedFilePath}", fullDmbPath, trustedFilePath);
|
||||
|
||||
using (await SemaphoreSlimContext.Lock(UserFilesSemaphore, cancellationToken))
|
||||
{
|
||||
string trustedFileText;
|
||||
if (await ioManager.FileExists(trustedFilePath, cancellationToken))
|
||||
{
|
||||
var trustedFileBytes = await ioManager.ReadAllBytes(trustedFilePath, cancellationToken);
|
||||
trustedFileText = Encoding.UTF8.GetString(trustedFileBytes);
|
||||
trustedFileText = $"{trustedFileText.Trim()}{Environment.NewLine}";
|
||||
}
|
||||
else
|
||||
{
|
||||
trustedFileText = String.Empty;
|
||||
}
|
||||
|
||||
if (trustedFileText.Contains(fullDmbPath, StringComparison.Ordinal))
|
||||
return;
|
||||
|
||||
trustedFileText = $"{trustedFileText}{fullDmbPath}{Environment.NewLine}";
|
||||
|
||||
var newTrustedFileBytes = Encoding.UTF8.GetBytes(trustedFileText);
|
||||
await ioManager.WriteAllBytes(trustedFilePath, newTrustedFileBytes, cancellationToken);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,45 +1,15 @@
|
||||
using System;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace Tgstation.Server.Host.Components.Byond
|
||||
{
|
||||
/// <summary>
|
||||
/// Represents usage of the two primary BYOND server executables.
|
||||
/// </summary>
|
||||
public interface IByondExecutableLock : IDisposable
|
||||
public interface IByondExecutableLock : IByondInstallation, IDisposable
|
||||
{
|
||||
/// <summary>
|
||||
/// The <see cref="global::System.Version"/> of the locked executables.
|
||||
/// </summary>
|
||||
Version Version { get; }
|
||||
|
||||
/// <summary>
|
||||
/// The path to the DreamDaemon executable.
|
||||
/// </summary>
|
||||
string DreamDaemonPath { get; }
|
||||
|
||||
/// <summary>
|
||||
/// The path to the dm/DreamMaker executable.
|
||||
/// </summary>
|
||||
string DreamMakerPath { get; }
|
||||
|
||||
/// <summary>
|
||||
/// If <see cref="DreamDaemonPath"/> supports being run as a command-line application.
|
||||
/// </summary>
|
||||
bool SupportsCli { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Call if, during a detach, this version should not be deleted.
|
||||
/// </summary>
|
||||
void DoNotDeleteThisSession();
|
||||
|
||||
/// <summary>
|
||||
/// Add a given <paramref name="fullDmbPath"/> to the trusted DMBs list in BYOND's config.
|
||||
/// </summary>
|
||||
/// <param name="fullDmbPath">Full path to the .dmb that should be trusted.</param>
|
||||
/// <param name="cancellationToken">The <see cref="CancellationToken"/> for the operation.</param>
|
||||
/// <returns>A <see cref="Task"/> representing the running operation.</returns>
|
||||
Task TrustDmbPath(string fullDmbPath, CancellationToken cancellationToken);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,30 @@
|
||||
using System;
|
||||
|
||||
namespace Tgstation.Server.Host.Components.Byond
|
||||
{
|
||||
/// <summary>
|
||||
/// Represents a BYOND installation.
|
||||
/// </summary>
|
||||
public interface IByondInstallation
|
||||
{
|
||||
/// <summary>
|
||||
/// The <see cref="global::System.Version"/> of the <see cref="IByondInstallation"/>.
|
||||
/// </summary>
|
||||
Version Version { get; }
|
||||
|
||||
/// <summary>
|
||||
/// The full path to the DreamDaemon executable.
|
||||
/// </summary>
|
||||
string DreamDaemonPath { get; }
|
||||
|
||||
/// <summary>
|
||||
/// The full path to the dm/DreamMaker executable.
|
||||
/// </summary>
|
||||
string DreamMakerPath { get; }
|
||||
|
||||
/// <summary>
|
||||
/// If <see cref="DreamDaemonPath"/> supports being run as a command-line application.
|
||||
/// </summary>
|
||||
bool SupportsCli { get; }
|
||||
}
|
||||
}
|
||||
@@ -6,11 +6,14 @@ using System.Threading.Tasks;
|
||||
|
||||
using Microsoft.Extensions.Hosting;
|
||||
|
||||
using Tgstation.Server.Host.Jobs;
|
||||
|
||||
namespace Tgstation.Server.Host.Components.Byond
|
||||
{
|
||||
/// <summary>
|
||||
/// For managing the BYOND installation.
|
||||
/// </summary>
|
||||
/// <remarks>When passing in <see cref="Version"/>s, ensure they are BYOND format versions unless referring to a custom version. This means <see cref="Version.Build"/> should NEVER be 0.</remarks>
|
||||
public interface IByondManager : IHostedService, IDisposable
|
||||
{
|
||||
/// <summary>
|
||||
@@ -26,18 +29,33 @@ namespace Tgstation.Server.Host.Components.Byond
|
||||
/// <summary>
|
||||
/// Change the active BYOND version.
|
||||
/// </summary>
|
||||
/// <param name="progressReporter">The optional <see cref="JobProgressReporter"/> for the operation.</param>
|
||||
/// <param name="version">The new <see cref="Version"/>.</param>
|
||||
/// <param name="customVersionStream">Optional <see cref="Stream"/> of a custom BYOND version zip file.</param>
|
||||
/// <param name="allowInstallation">If an installation should be performed if the <paramref name="version"/> is not installed. If <see langword="false"/> and an installation is required an <see cref="InvalidOperationException"/> will be thrown.</param>
|
||||
/// <param name="cancellationToken">The <see cref="CancellationToken"/> for the operation.</param>
|
||||
/// <returns>A <see cref="Task"/> representing the running operation.</returns>
|
||||
Task ChangeVersion(Version version, Stream customVersionStream, CancellationToken cancellationToken);
|
||||
Task ChangeVersion(JobProgressReporter progressReporter, Version version, Stream customVersionStream, bool allowInstallation, CancellationToken cancellationToken);
|
||||
|
||||
/// <summary>
|
||||
/// Deletes a given BYOND version from the disk.
|
||||
/// </summary>
|
||||
/// <param name="progressReporter">The <see cref="JobProgressReporter"/> for the operation.</param>
|
||||
/// <param name="version">The <see cref="Version"/> to delete.</param>
|
||||
/// <param name="cancellationToken">The <see cref="CancellationToken"/> for the operation.</param>
|
||||
/// <returns>A <see cref="Task"/> representing the running operation.</returns>
|
||||
Task DeleteVersion(JobProgressReporter progressReporter, Version version, CancellationToken cancellationToken);
|
||||
|
||||
/// <summary>
|
||||
/// Lock the current installation's location and return a <see cref="IByondExecutableLock"/>.
|
||||
/// </summary>
|
||||
/// <param name="requiredVersion">The BYOND <see cref="Version"/> required.</param>
|
||||
/// <param name="trustDmbFullPath">The optional full path to .dmb to trust while using the executables.</param>
|
||||
/// <param name="cancellationToken">The <see cref="CancellationToken"/> for the operation.</param>
|
||||
/// <returns>A <see cref="Task{TResult}"/> resulting in the requested <see cref="IByondExecutableLock"/>.</returns>
|
||||
Task<IByondExecutableLock> UseExecutables(Version requiredVersion, CancellationToken cancellationToken);
|
||||
Task<IByondExecutableLock> UseExecutables(
|
||||
Version requiredVersion,
|
||||
string trustDmbFullPath,
|
||||
CancellationToken cancellationToken);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -9,10 +9,10 @@ using Microsoft.Extensions.Options;
|
||||
|
||||
using Tgstation.Server.Api.Models;
|
||||
using Tgstation.Server.Host.Configuration;
|
||||
using Tgstation.Server.Host.Core;
|
||||
using Tgstation.Server.Host.IO;
|
||||
using Tgstation.Server.Host.Jobs;
|
||||
using Tgstation.Server.Host.System;
|
||||
using Tgstation.Server.Host.Utils;
|
||||
|
||||
namespace Tgstation.Server.Host.Components.Byond
|
||||
{
|
||||
@@ -166,8 +166,7 @@ namespace Tgstation.Server.Host.Components.Byond
|
||||
await IOManager.WriteAllBytes(
|
||||
configFilePath,
|
||||
Encoding.UTF8.GetBytes(ByondNoPromptTrustedMode),
|
||||
cancellationToken)
|
||||
;
|
||||
cancellationToken);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
||||
@@ -15,6 +15,7 @@ using Tgstation.Server.Host.Components.Chat.Providers;
|
||||
using Tgstation.Server.Host.Components.Interop;
|
||||
using Tgstation.Server.Host.Core;
|
||||
using Tgstation.Server.Host.Extensions;
|
||||
using Tgstation.Server.Host.Utils;
|
||||
|
||||
namespace Tgstation.Server.Host.Components.Chat
|
||||
{
|
||||
@@ -911,7 +912,7 @@ namespace Tgstation.Server.Host.Components.Chat
|
||||
async Task WrapProcessMessage()
|
||||
{
|
||||
var localActiveProcessingTask = activeProcessingTask;
|
||||
using (LogContext.PushProperty("ChatMessage", messageNumber))
|
||||
using (LogContext.PushProperty(SerilogContextHelper.ChatMessageIterationContextProperty, messageNumber))
|
||||
try
|
||||
{
|
||||
await ProcessMessage(completedMessageTaskKvp.Key, message, false, cancellationToken);
|
||||
|
||||
@@ -13,11 +13,11 @@ using Newtonsoft.Json;
|
||||
|
||||
using Tgstation.Server.Api.Models;
|
||||
using Tgstation.Server.Host.Components.Interop;
|
||||
using Tgstation.Server.Host.Core;
|
||||
using Tgstation.Server.Host.Extensions;
|
||||
using Tgstation.Server.Host.IO;
|
||||
using Tgstation.Server.Host.Jobs;
|
||||
using Tgstation.Server.Host.System;
|
||||
using Tgstation.Server.Host.Utils;
|
||||
|
||||
namespace Tgstation.Server.Host.Components.Chat.Providers
|
||||
{
|
||||
|
||||
@@ -4,9 +4,9 @@ using System.Globalization;
|
||||
using Microsoft.Extensions.Logging;
|
||||
|
||||
using Tgstation.Server.Api.Models;
|
||||
using Tgstation.Server.Host.Core;
|
||||
using Tgstation.Server.Host.Jobs;
|
||||
using Tgstation.Server.Host.System;
|
||||
using Tgstation.Server.Host.Utils;
|
||||
|
||||
namespace Tgstation.Server.Host.Components.Chat.Providers
|
||||
{
|
||||
|
||||
@@ -119,11 +119,6 @@ namespace Tgstation.Server.Host.Components.Deployment
|
||||
/// </summary>
|
||||
string currentDreamMakerOutput;
|
||||
|
||||
/// <summary>
|
||||
/// Current stage to report on the job.
|
||||
/// </summary>
|
||||
string currentStage;
|
||||
|
||||
/// <summary>
|
||||
/// If a compile job is running.
|
||||
/// </summary>
|
||||
@@ -486,11 +481,11 @@ namespace Tgstation.Server.Host.Components.Deployment
|
||||
|
||||
using var progressCts = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken);
|
||||
|
||||
currentStage = "Reserving BYOND version";
|
||||
progressReporter.StageName = "Reserving BYOND version";
|
||||
var progressTask = ProgressTask(progressReporter, estimatedDuration, progressCts.Token);
|
||||
try
|
||||
{
|
||||
using var byondLock = await byond.UseExecutables(null, cancellationToken);
|
||||
using var byondLock = await byond.UseExecutables(null, null, cancellationToken);
|
||||
currentChatCallback = chatManager.QueueDeploymentMessage(
|
||||
revisionInformation,
|
||||
byondLock.Version,
|
||||
@@ -508,12 +503,11 @@ namespace Tgstation.Server.Host.Components.Deployment
|
||||
RepositoryOrigin = repository.Origin.ToString(),
|
||||
};
|
||||
|
||||
currentStage = "Creating remote deployment notification";
|
||||
progressReporter.StageName = "Creating remote deployment notification";
|
||||
await remoteDeploymentManager.StartDeployment(
|
||||
repository,
|
||||
job,
|
||||
cancellationToken)
|
||||
;
|
||||
cancellationToken);
|
||||
|
||||
logger.LogTrace("Deployment will timeout at {timeoutTime}", DateTimeOffset.UtcNow + dreamMakerSettings.Timeout.Value);
|
||||
using var timeoutTokenSource = new CancellationTokenSource(dreamMakerSettings.Timeout.Value);
|
||||
@@ -524,14 +518,14 @@ namespace Tgstation.Server.Host.Components.Deployment
|
||||
try
|
||||
{
|
||||
await RunCompileJob(
|
||||
progressReporter,
|
||||
job,
|
||||
dreamMakerSettings,
|
||||
launchParameters,
|
||||
byondLock,
|
||||
repository,
|
||||
remoteDeploymentManager,
|
||||
combinedTokenSource.Token)
|
||||
;
|
||||
combinedTokenSource.Token);
|
||||
}
|
||||
catch (OperationCanceledException) when (timeoutToken.IsCancellationRequested)
|
||||
{
|
||||
@@ -544,7 +538,7 @@ namespace Tgstation.Server.Host.Components.Deployment
|
||||
catch (OperationCanceledException)
|
||||
{
|
||||
// DCT: Cancellation token is for job, delaying here is fine
|
||||
currentStage = "Running CompileCancelled event";
|
||||
progressReporter.StageName = "Running CompileCancelled event";
|
||||
await eventConsumer.HandleEvent(EventType.CompileCancelled, Enumerable.Empty<string>(), default);
|
||||
throw;
|
||||
}
|
||||
@@ -558,6 +552,7 @@ namespace Tgstation.Server.Host.Components.Deployment
|
||||
/// <summary>
|
||||
/// Executes and populate a given <paramref name="job"/>.
|
||||
/// </summary>
|
||||
/// <param name="progressReporter">The <see cref="JobProgressReporter"/> for the operation.</param>
|
||||
/// <param name="job">The <see cref="CompileJob"/> to run and populate.</param>
|
||||
/// <param name="dreamMakerSettings">The <see cref="Api.Models.Internal.DreamMakerSettings"/> to use.</param>
|
||||
/// <param name="launchParameters">The <see cref="DreamDaemonLaunchParameters"/> to use.</param>
|
||||
@@ -567,6 +562,7 @@ namespace Tgstation.Server.Host.Components.Deployment
|
||||
/// <param name="cancellationToken">The <see cref="CancellationToken"/> for the operation.</param>
|
||||
/// <returns>A <see cref="Task"/> representing the running operation.</returns>
|
||||
async Task RunCompileJob(
|
||||
JobProgressReporter progressReporter,
|
||||
Models.CompileJob job,
|
||||
Api.Models.Internal.DreamMakerSettings dreamMakerSettings,
|
||||
DreamDaemonLaunchParameters launchParameters,
|
||||
@@ -582,7 +578,7 @@ namespace Tgstation.Server.Host.Components.Deployment
|
||||
{
|
||||
// copy the repository
|
||||
logger.LogTrace("Copying repository to game directory");
|
||||
currentStage = "Copying repository";
|
||||
progressReporter.StageName = "Copying repository";
|
||||
var resolvedOutputDirectory = ioManager.ResolvePath(outputDirectory);
|
||||
var repoOrigin = repository.Origin;
|
||||
using (repository)
|
||||
@@ -591,7 +587,7 @@ namespace Tgstation.Server.Host.Components.Deployment
|
||||
// repository closed now
|
||||
|
||||
// run precompile scripts
|
||||
currentStage = "Running PreCompile event";
|
||||
progressReporter.StageName = "Running PreCompile event";
|
||||
await eventConsumer.HandleEvent(
|
||||
EventType.CompileStart,
|
||||
new List<string>
|
||||
@@ -604,7 +600,7 @@ namespace Tgstation.Server.Host.Components.Deployment
|
||||
;
|
||||
|
||||
// determine the dme
|
||||
currentStage = "Determining .dme";
|
||||
progressReporter.StageName = "Determining .dme";
|
||||
if (job.DmeName == null)
|
||||
{
|
||||
logger.LogTrace("Searching for available .dmes");
|
||||
@@ -626,11 +622,11 @@ namespace Tgstation.Server.Host.Components.Deployment
|
||||
|
||||
logger.LogDebug("Selected {dmeName}.dme for compilation!", job.DmeName);
|
||||
|
||||
currentStage = "Modifying .dme";
|
||||
progressReporter.StageName = "Modifying .dme";
|
||||
await ModifyDme(job, cancellationToken);
|
||||
|
||||
// run precompile scripts
|
||||
currentStage = "Running PreDreamMaker event";
|
||||
progressReporter.StageName = "Running PreDreamMaker event";
|
||||
await eventConsumer.HandleEvent(
|
||||
EventType.PreDreamMaker,
|
||||
new List<string>
|
||||
@@ -639,13 +635,15 @@ namespace Tgstation.Server.Host.Components.Deployment
|
||||
repoOrigin.ToString(),
|
||||
$"{byondLock.Version.Major}.{byondLock.Version.Minor}",
|
||||
},
|
||||
cancellationToken)
|
||||
;
|
||||
cancellationToken);
|
||||
|
||||
// run compiler
|
||||
currentStage = "Running DreamMaker";
|
||||
progressReporter.StageName = "Running DreamMaker";
|
||||
var exitCode = await RunDreamMaker(byondLock.DreamMakerPath, job, cancellationToken);
|
||||
|
||||
// Session takes ownership of the lock and Disposes it so save this for later
|
||||
var byondVersion = byondLock.Version;
|
||||
|
||||
// verify api
|
||||
try
|
||||
{
|
||||
@@ -654,7 +652,7 @@ namespace Tgstation.Server.Host.Components.Deployment
|
||||
ErrorCode.DreamMakerExitCode,
|
||||
new JobException($"Exit code: {exitCode}{Environment.NewLine}{Environment.NewLine}{job.Output}"));
|
||||
|
||||
currentStage = "Validating DMAPI";
|
||||
progressReporter.StageName = "Validating DMAPI";
|
||||
await VerifyApi(
|
||||
launchParameters.StartupTimeout.Value,
|
||||
dreamMakerSettings.ApiValidationSecurityLevel.Value,
|
||||
@@ -668,33 +666,31 @@ namespace Tgstation.Server.Host.Components.Deployment
|
||||
catch (JobException)
|
||||
{
|
||||
// DD never validated or compile failed
|
||||
currentStage = "Running CompileFailure event";
|
||||
progressReporter.StageName = "Running CompileFailure event";
|
||||
await eventConsumer.HandleEvent(
|
||||
EventType.CompileFailure,
|
||||
new List<string>
|
||||
{
|
||||
resolvedOutputDirectory,
|
||||
exitCode == 0 ? "1" : "0",
|
||||
$"{byondLock.Version.Major}.{byondLock.Version.Minor}",
|
||||
byondVersion.ToString(),
|
||||
},
|
||||
cancellationToken)
|
||||
;
|
||||
cancellationToken);
|
||||
throw;
|
||||
}
|
||||
|
||||
currentStage = "Running CompileComplete event";
|
||||
progressReporter.StageName = "Running CompileComplete event";
|
||||
await eventConsumer.HandleEvent(
|
||||
EventType.CompileComplete,
|
||||
new List<string>
|
||||
{
|
||||
resolvedOutputDirectory,
|
||||
$"{byondLock.Version.Major}.{byondLock.Version.Minor}",
|
||||
byondVersion.ToString(),
|
||||
},
|
||||
cancellationToken)
|
||||
;
|
||||
cancellationToken);
|
||||
|
||||
logger.LogTrace("Applying static game file symlinks...");
|
||||
currentStage = "Symlinking GameStaticFiles";
|
||||
progressReporter.StageName = "Symlinking GameStaticFiles";
|
||||
|
||||
// symlink in the static data
|
||||
await configuration.SymlinkStaticFilesTo(resolvedOutputDirectory, cancellationToken);
|
||||
@@ -703,7 +699,7 @@ namespace Tgstation.Server.Host.Components.Deployment
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
currentStage = "Cleaning output directory";
|
||||
progressReporter.StageName = "Cleaning output directory";
|
||||
await CleanupFailedCompile(job, remoteDeploymentManager, ex);
|
||||
throw;
|
||||
}
|
||||
@@ -718,7 +714,6 @@ namespace Tgstation.Server.Host.Components.Deployment
|
||||
/// <returns>A <see cref="Task"/> representing the running operation.</returns>
|
||||
async Task ProgressTask(JobProgressReporter progressReporter, TimeSpan? estimatedDuration, CancellationToken cancellationToken)
|
||||
{
|
||||
progressReporter.StageName = currentStage;
|
||||
double? lastReport = estimatedDuration.HasValue ? 0 : null;
|
||||
progressReporter.ReportProgress(lastReport);
|
||||
|
||||
@@ -747,7 +742,6 @@ namespace Tgstation.Server.Host.Components.Deployment
|
||||
var nextSleepSpan = remainingSleepThisInterval < minimumSleepInterval ? minimumSleepInterval : remainingSleepThisInterval;
|
||||
|
||||
await Task.Delay(nextSleepSpan, cancellationToken);
|
||||
progressReporter.StageName = currentStage;
|
||||
progressReporter.ReportProgress(lastReport);
|
||||
}
|
||||
while (DateTimeOffset.UtcNow < nextInterval);
|
||||
@@ -755,7 +749,6 @@ namespace Tgstation.Server.Host.Components.Deployment
|
||||
else
|
||||
await Task.Delay(minimumSleepInterval, cancellationToken);
|
||||
|
||||
progressReporter.StageName = currentStage;
|
||||
lastReport = estimatedDuration.HasValue ? sleepInterval * (iteration + 1) / estimatedDuration.Value : null;
|
||||
progressReporter.ReportProgress(lastReport);
|
||||
}
|
||||
|
||||
+1
-1
@@ -11,10 +11,10 @@ using Microsoft.Extensions.Logging;
|
||||
using Octokit;
|
||||
|
||||
using Tgstation.Server.Host.Components.Repository;
|
||||
using Tgstation.Server.Host.Core;
|
||||
using Tgstation.Server.Host.Database;
|
||||
using Tgstation.Server.Host.Extensions;
|
||||
using Tgstation.Server.Host.Models;
|
||||
using Tgstation.Server.Host.Utils;
|
||||
|
||||
namespace Tgstation.Server.Host.Components.Deployment.Remote
|
||||
{
|
||||
|
||||
+1
-1
@@ -4,8 +4,8 @@ using Microsoft.Extensions.Logging;
|
||||
|
||||
using Tgstation.Server.Api.Models;
|
||||
using Tgstation.Server.Host.Components.Repository;
|
||||
using Tgstation.Server.Host.Core;
|
||||
using Tgstation.Server.Host.Database;
|
||||
using Tgstation.Server.Host.Utils;
|
||||
|
||||
namespace Tgstation.Server.Host.Components.Deployment.Remote
|
||||
{
|
||||
|
||||
@@ -1,15 +1,13 @@
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
using Tgstation.Server.Host.Components.Interop.Bridge;
|
||||
using Tgstation.Server.Host.Models;
|
||||
|
||||
namespace Tgstation.Server.Host.Components
|
||||
{
|
||||
/// <summary>
|
||||
/// For managing <see cref="IInstance"/>s.
|
||||
/// </summary>
|
||||
public interface IInstanceManager : IBridgeDispatcher
|
||||
public interface IInstanceManager : IInstanceOperations, IBridgeDispatcher
|
||||
{
|
||||
/// <summary>
|
||||
/// <see cref="Task"/> that completes when the <see cref="IInstanceManager"/> finishes initializing.
|
||||
@@ -22,31 +20,5 @@ namespace Tgstation.Server.Host.Components
|
||||
/// <param name="metadata">The <see cref="Models.Instance"/> of the desired <see cref="IInstance"/>.</param>
|
||||
/// <returns>The <see cref="IInstance"/> associated with the given <paramref name="metadata"/> if it is online, <see langword="null"/> otherwise.</returns>
|
||||
IInstanceReference GetInstanceReference(Api.Models.Instance metadata);
|
||||
|
||||
/// <summary>
|
||||
/// Online an <see cref="IInstance"/>.
|
||||
/// </summary>
|
||||
/// <param name="metadata">The <see cref="Models.Instance"/> of the desired <see cref="IInstance"/>.</param>
|
||||
/// <param name="cancellationToken">The <see cref="CancellationToken"/> for the operation.</param>
|
||||
/// <returns>A <see cref="Task"/> representing the running operation.</returns>
|
||||
Task OnlineInstance(Models.Instance metadata, CancellationToken cancellationToken);
|
||||
|
||||
/// <summary>
|
||||
/// Offline an <see cref="IInstance"/>.
|
||||
/// </summary>
|
||||
/// <param name="metadata">The <see cref="Models.Instance"/> of the desired <see cref="IInstance"/>.</param>
|
||||
/// <param name="user">The <see cref="User"/> performing the operation.</param>
|
||||
/// <param name="cancellationToken">The <see cref="CancellationToken"/> for the operation.</param>
|
||||
/// <returns>A <see cref="Task"/> representing the running operation.</returns>
|
||||
Task OfflineInstance(Models.Instance metadata, User user, CancellationToken cancellationToken);
|
||||
|
||||
/// <summary>
|
||||
/// Move an <see cref="IInstance"/>.
|
||||
/// </summary>
|
||||
/// <param name="metadata">The <see cref="Models.Instance"/> of the desired <see cref="IInstance"/> with the updated path.</param>
|
||||
/// <param name="oldPath">The old path of the <see cref="IInstance"/>. <paramref name="metadata"/> will have this set on <see cref="Api.Models.Instance.Path"/> if the operation fails.</param>
|
||||
/// <param name="cancellationToken">The <see cref="CancellationToken"/> for the operation.</param>
|
||||
/// <returns>A <see cref="Task"/> representing the running operation.</returns>
|
||||
Task MoveInstance(Models.Instance metadata, string oldPath, CancellationToken cancellationToken);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,39 @@
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
using Tgstation.Server.Host.Models;
|
||||
|
||||
namespace Tgstation.Server.Host.Components
|
||||
{
|
||||
/// <summary>
|
||||
/// Operations that can be performed on a given <see cref="Models.Instance"/>.
|
||||
/// </summary>
|
||||
public interface IInstanceOperations
|
||||
{
|
||||
/// <summary>
|
||||
/// Online an <see cref="IInstance"/>.
|
||||
/// </summary>
|
||||
/// <param name="metadata">The <see cref="Models.Instance"/> of the desired <see cref="IInstance"/>.</param>
|
||||
/// <param name="cancellationToken">The <see cref="CancellationToken"/> for the operation.</param>
|
||||
/// <returns>A <see cref="Task"/> representing the running operation.</returns>
|
||||
Task OnlineInstance(Models.Instance metadata, CancellationToken cancellationToken);
|
||||
|
||||
/// <summary>
|
||||
/// Offline an <see cref="IInstance"/>.
|
||||
/// </summary>
|
||||
/// <param name="metadata">The <see cref="Models.Instance"/> of the desired <see cref="IInstance"/>.</param>
|
||||
/// <param name="user">The <see cref="User"/> performing the operation.</param>
|
||||
/// <param name="cancellationToken">The <see cref="CancellationToken"/> for the operation.</param>
|
||||
/// <returns>A <see cref="Task"/> representing the running operation.</returns>
|
||||
Task OfflineInstance(Models.Instance metadata, User user, CancellationToken cancellationToken);
|
||||
|
||||
/// <summary>
|
||||
/// Move an <see cref="IInstance"/>.
|
||||
/// </summary>
|
||||
/// <param name="metadata">The <see cref="Models.Instance"/> of the desired <see cref="IInstance"/> with the updated path.</param>
|
||||
/// <param name="oldPath">The old path of the <see cref="IInstance"/>. <paramref name="metadata"/> will have this set on <see cref="Api.Models.Instance.Path"/> if the operation fails.</param>
|
||||
/// <param name="cancellationToken">The <see cref="CancellationToken"/> for the operation.</param>
|
||||
/// <returns>A <see cref="Task"/> representing the running operation.</returns>
|
||||
Task MoveInstance(Models.Instance metadata, string oldPath, CancellationToken cancellationToken);
|
||||
}
|
||||
}
|
||||
@@ -19,6 +19,7 @@ using Tgstation.Server.Host.Components.Watchdog;
|
||||
using Tgstation.Server.Host.Database;
|
||||
using Tgstation.Server.Host.Jobs;
|
||||
using Tgstation.Server.Host.Models;
|
||||
using Tgstation.Server.Host.Utils;
|
||||
|
||||
namespace Tgstation.Server.Host.Components
|
||||
{
|
||||
@@ -143,14 +144,17 @@ namespace Tgstation.Server.Host.Components
|
||||
/// <inheritdoc />
|
||||
public async ValueTask DisposeAsync()
|
||||
{
|
||||
using (LogContext.PushProperty("Instance", metadata.Id))
|
||||
using (LogContext.PushProperty(SerilogContextHelper.InstanceIdContextProperty, metadata.Id))
|
||||
{
|
||||
var chatDispose = Chat.DisposeAsync();
|
||||
var watchdogDispose = Watchdog.DisposeAsync();
|
||||
timerCts?.Dispose();
|
||||
Configuration.Dispose();
|
||||
await Chat.DisposeAsync();
|
||||
await Watchdog.DisposeAsync();
|
||||
dmbFactory.Dispose();
|
||||
RepositoryManager.Dispose();
|
||||
ByondManager.Dispose();
|
||||
await chatDispose;
|
||||
await watchdogDispose;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -166,7 +170,7 @@ namespace Tgstation.Server.Host.Components
|
||||
/// <inheritdoc />
|
||||
public async Task StartAsync(CancellationToken cancellationToken)
|
||||
{
|
||||
using (LogContext.PushProperty("Instance", metadata.Id))
|
||||
using (LogContext.PushProperty(SerilogContextHelper.InstanceIdContextProperty, metadata.Id))
|
||||
{
|
||||
await Task.WhenAll(
|
||||
SetAutoUpdateInterval(metadata.AutoUpdateInterval.Value),
|
||||
@@ -186,7 +190,7 @@ namespace Tgstation.Server.Host.Components
|
||||
/// <inheritdoc />
|
||||
public async Task StopAsync(CancellationToken cancellationToken)
|
||||
{
|
||||
using (LogContext.PushProperty("Instance", metadata.Id))
|
||||
using (LogContext.PushProperty(SerilogContextHelper.InstanceIdContextProperty, metadata.Id))
|
||||
{
|
||||
logger.LogDebug("Stopping instance...");
|
||||
await SetAutoUpdateInterval(0);
|
||||
@@ -309,7 +313,7 @@ namespace Tgstation.Server.Host.Components
|
||||
{
|
||||
if (currentRevInfo == null)
|
||||
{
|
||||
logger.LogTrace("Loading revision info for commit {0}...", startSha.Substring(0, 7));
|
||||
logger.LogTrace("Loading revision info for commit {sha}...", startSha[..7]);
|
||||
currentRevInfo = await databaseContext
|
||||
.RevisionInformations
|
||||
.AsQueryable()
|
||||
@@ -552,7 +556,7 @@ namespace Tgstation.Server.Host.Components
|
||||
|
||||
await jobManager.WaitForJobCompletion(compileProcessJob, null, default, cancellationToken);
|
||||
}
|
||||
catch (Exception e) when (!(e is OperationCanceledException))
|
||||
catch (Exception e) when (e is not OperationCanceledException)
|
||||
{
|
||||
logger.LogWarning(e, "Error in auto update loop!");
|
||||
continue;
|
||||
|
||||
@@ -22,6 +22,7 @@ using Tgstation.Server.Host.Jobs;
|
||||
using Tgstation.Server.Host.Security;
|
||||
using Tgstation.Server.Host.Swarm;
|
||||
using Tgstation.Server.Host.System;
|
||||
using Tgstation.Server.Host.Utils;
|
||||
|
||||
namespace Tgstation.Server.Host.Components
|
||||
{
|
||||
@@ -92,14 +93,14 @@ namespace Tgstation.Server.Host.Components
|
||||
readonly ILogger<InstanceManager> logger;
|
||||
|
||||
/// <summary>
|
||||
/// Map of instance <see cref="EntityId.Id"/>s to respective <see cref="InstanceContainer"/>s. Also used as a <see langword="lock"/> <see cref="object"/>.
|
||||
/// Map of instance <see cref="EntityId.Id"/>s to the respective <see cref="ReferenceCountingContainer{TWrapped, TReference}"/> for <see cref="IInstance"/>s. Also used as a <see langword="lock"/> <see cref="object"/>.
|
||||
/// </summary>
|
||||
readonly IDictionary<long, InstanceContainer> instances;
|
||||
readonly Dictionary<long, ReferenceCountingContainer<IInstance, InstanceWrapper>> instances;
|
||||
|
||||
/// <summary>
|
||||
/// Map of <see cref="DMApiParameters.AccessIdentifier"/>s to their respective <see cref="IBridgeHandler"/>s.
|
||||
/// </summary>
|
||||
readonly IDictionary<string, IBridgeHandler> bridgeHandlers;
|
||||
readonly Dictionary<string, IBridgeHandler> bridgeHandlers;
|
||||
|
||||
/// <summary>
|
||||
/// <see cref="SemaphoreSlim"/> used to guard calls to <see cref="OnlineInstance(Models.Instance, CancellationToken)"/> and <see cref="OfflineInstance(Models.Instance, Models.User, CancellationToken)"/>.
|
||||
@@ -121,6 +122,21 @@ namespace Tgstation.Server.Host.Components
|
||||
/// </summary>
|
||||
readonly TaskCompletionSource readyTcs;
|
||||
|
||||
/// <summary>
|
||||
/// The <see cref="CancellationTokenSource"/> for <see cref="Initialize(CancellationToken)"/>.
|
||||
/// </summary>
|
||||
readonly CancellationTokenSource startupCancellationTokenSource;
|
||||
|
||||
/// <summary>
|
||||
/// The <see cref="CancellationTokenSource"/> linked with the token given to <see cref="StopAsync(CancellationToken)"/>.
|
||||
/// </summary>
|
||||
readonly CancellationTokenSource shutdownCancellationTokenSource;
|
||||
|
||||
/// <summary>
|
||||
/// The <see cref="Task"/> returned by <see cref="Initialize(CancellationToken)"/>.
|
||||
/// </summary>
|
||||
Task startupTask;
|
||||
|
||||
/// <summary>
|
||||
/// If the <see cref="InstanceManager"/> has been <see cref="DisposeAsync"/>'d.
|
||||
/// </summary>
|
||||
@@ -171,10 +187,12 @@ namespace Tgstation.Server.Host.Components
|
||||
swarmConfiguration = swarmConfigurationOptions?.Value ?? throw new ArgumentNullException(nameof(swarmConfigurationOptions));
|
||||
this.logger = logger ?? throw new ArgumentNullException(nameof(logger));
|
||||
|
||||
instances = new Dictionary<long, InstanceContainer>();
|
||||
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();
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
@@ -191,6 +209,8 @@ namespace Tgstation.Server.Host.Components
|
||||
await instanceKvp.Value.Instance.DisposeAsync();
|
||||
|
||||
instanceStateChangeSemaphore.Dispose();
|
||||
startupCancellationTokenSource.Dispose();
|
||||
shutdownCancellationTokenSource.Dispose();
|
||||
|
||||
logger.LogInformation("Server shutdown");
|
||||
}
|
||||
@@ -204,10 +224,7 @@ namespace Tgstation.Server.Host.Components
|
||||
lock (instances)
|
||||
{
|
||||
if (!instances.TryGetValue(metadata.Id.Value, out var instance))
|
||||
{
|
||||
logger.LogTrace("Cannot reference instance {instanceId} as it is not online or on this node!", metadata.Id);
|
||||
return null;
|
||||
}
|
||||
|
||||
return instance.AddReference();
|
||||
}
|
||||
@@ -230,7 +247,7 @@ namespace Tgstation.Server.Host.Components
|
||||
|
||||
// Delete the Game directory to clear out broken symlinks
|
||||
var instanceGameIOManager = instanceFactory.CreateGameIOManager(instance);
|
||||
await instanceGameIOManager.DeleteDirectory(".", cancellationToken);
|
||||
await instanceGameIOManager.DeleteDirectory(DefaultIOManager.CurrentDirectory, cancellationToken);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
@@ -292,50 +309,64 @@ namespace Tgstation.Server.Host.Components
|
||||
if (metadata == null)
|
||||
throw new ArgumentNullException(nameof(metadata));
|
||||
|
||||
using var lockContext = await SemaphoreSlimContext.Lock(instanceStateChangeSemaphore, cancellationToken);
|
||||
|
||||
logger.LogInformation("Offlining instance ID {instanceId}", metadata.Id);
|
||||
InstanceContainer container;
|
||||
lock (instances)
|
||||
using (await SemaphoreSlimContext.Lock(instanceStateChangeSemaphore, cancellationToken))
|
||||
{
|
||||
if (!instances.TryGetValue(metadata.Id.Value, out container))
|
||||
ReferenceCountingContainer<IInstance, InstanceWrapper> container;
|
||||
lock (instances)
|
||||
{
|
||||
logger.LogDebug("Not offlining removed instance {instanceId}", metadata.Id);
|
||||
return;
|
||||
if (!instances.TryGetValue(metadata.Id.Value, out container))
|
||||
{
|
||||
logger.LogDebug("Not offlining removed instance {instanceId}", metadata.Id);
|
||||
return;
|
||||
}
|
||||
|
||||
instances.Remove(metadata.Id.Value);
|
||||
}
|
||||
|
||||
instances.Remove(metadata.Id.Value);
|
||||
}
|
||||
logger.LogInformation("Offlining instance ID {instanceId}", metadata.Id);
|
||||
|
||||
try
|
||||
{
|
||||
await container.OnZeroReferences;
|
||||
try
|
||||
{
|
||||
await container.OnZeroReferences.WithToken(cancellationToken);
|
||||
|
||||
// we are the one responsible for cancelling his jobs
|
||||
var tasks = new List<Task>();
|
||||
await databaseContextFactory.UseContext(
|
||||
async db =>
|
||||
{
|
||||
var jobs = await db
|
||||
.Jobs
|
||||
.AsQueryable()
|
||||
.Where(x => x.Instance.Id == metadata.Id && !x.StoppedAt.HasValue)
|
||||
.Select(x => new Models.Job
|
||||
{
|
||||
Id = x.Id,
|
||||
})
|
||||
.ToListAsync(cancellationToken);
|
||||
foreach (var job in jobs)
|
||||
tasks.Add(jobManager.CancelJob(job, user, true, cancellationToken));
|
||||
});
|
||||
// we are the one responsible for cancelling his jobs
|
||||
var tasks = new List<Task>();
|
||||
await databaseContextFactory.UseContext(
|
||||
async db =>
|
||||
{
|
||||
var jobs = await db
|
||||
.Jobs
|
||||
.AsQueryable()
|
||||
.Where(x => x.Instance.Id == metadata.Id && !x.StoppedAt.HasValue)
|
||||
.Select(x => new Models.Job
|
||||
{
|
||||
Id = x.Id,
|
||||
})
|
||||
.ToListAsync(cancellationToken);
|
||||
foreach (var job in jobs)
|
||||
tasks.Add(jobManager.CancelJob(job, user, true, cancellationToken));
|
||||
});
|
||||
|
||||
await Task.WhenAll(tasks);
|
||||
await Task.WhenAll(tasks);
|
||||
}
|
||||
catch
|
||||
{
|
||||
// not too late to change your mind
|
||||
lock (instances)
|
||||
instances.Add(metadata.Id.Value, container);
|
||||
|
||||
await container.Instance.StopAsync(cancellationToken);
|
||||
}
|
||||
finally
|
||||
{
|
||||
await container.Instance.DisposeAsync();
|
||||
throw;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
// at this point we can't really stop offlining the instance just because the request was cancelled
|
||||
await container.Instance.StopAsync(shutdownCancellationTokenSource.Token);
|
||||
}
|
||||
finally
|
||||
{
|
||||
await container.Instance.DisposeAsync();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -362,7 +393,9 @@ namespace Tgstation.Server.Host.Components
|
||||
try
|
||||
{
|
||||
lock (instances)
|
||||
instances.Add(metadata.Id.Value, new InstanceContainer(instance));
|
||||
instances.Add(
|
||||
metadata.Id.Value,
|
||||
new ReferenceCountingContainer<IInstance, InstanceWrapper>(instance));
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
@@ -388,105 +421,52 @@ namespace Tgstation.Server.Host.Components
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public async Task StartAsync(CancellationToken cancellationToken)
|
||||
public Task StartAsync(CancellationToken cancellationToken)
|
||||
{
|
||||
try
|
||||
{
|
||||
logger.LogInformation("{versionString}", assemblyInformationProvider.VersionString);
|
||||
generalConfiguration.CheckCompatibility(logger);
|
||||
|
||||
CheckSystemCompatibility();
|
||||
|
||||
await InitializeSwarm(cancellationToken);
|
||||
|
||||
List<Models.Instance> dbInstances = null;
|
||||
var instanceEnumeration = databaseContextFactory.UseContext(
|
||||
async databaseContext => dbInstances = await databaseContext
|
||||
.Instances
|
||||
.AsQueryable()
|
||||
.Where(x => x.Online.Value && x.SwarmIdentifer == swarmConfiguration.Identifier)
|
||||
.Include(x => x.RepositorySettings)
|
||||
.Include(x => x.ChatSettings)
|
||||
.ThenInclude(x => x.Channels)
|
||||
.Include(x => x.DreamDaemonSettings)
|
||||
.ToListAsync(cancellationToken));
|
||||
|
||||
var factoryStartup = instanceFactory.StartAsync(cancellationToken);
|
||||
var jobManagerStartup = jobManager.StartAsync(cancellationToken);
|
||||
|
||||
await Task.WhenAll(instanceEnumeration, factoryStartup, jobManagerStartup);
|
||||
|
||||
var instanceOnliningTasks = dbInstances.Select(
|
||||
async metadata =>
|
||||
{
|
||||
try
|
||||
{
|
||||
await OnlineInstance(metadata, cancellationToken);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
logger.LogError(ex, "Failed to online instance {instanceId}!", metadata.Id);
|
||||
}
|
||||
});
|
||||
|
||||
await Task.WhenAll(instanceOnliningTasks);
|
||||
|
||||
jobManager.Activate(this);
|
||||
|
||||
logger.LogInformation("Server ready!");
|
||||
readyTcs.SetResult();
|
||||
}
|
||||
catch (OperationCanceledException ex)
|
||||
{
|
||||
logger.LogInformation(ex, "Cancelled instance manager initialization!");
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
logger.LogCritical(e, "Instance manager startup error!");
|
||||
try
|
||||
{
|
||||
await serverControl.Die(e);
|
||||
return;
|
||||
}
|
||||
catch (Exception e2)
|
||||
{
|
||||
logger.LogCritical(e2, "Failed to kill server!");
|
||||
}
|
||||
|
||||
throw;
|
||||
}
|
||||
cancellationToken.Register(startupCancellationTokenSource.Cancel);
|
||||
startupTask = Initialize(startupCancellationTokenSource.Token);
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public async Task StopAsync(CancellationToken cancellationToken)
|
||||
{
|
||||
try
|
||||
{
|
||||
logger.LogDebug("Stopping instance manager...");
|
||||
var instanceFactoryStopTask = instanceFactory.StopAsync(cancellationToken);
|
||||
await jobManager.StopAsync(cancellationToken);
|
||||
|
||||
async Task OfflineInstanceImmediate(IInstance instance, CancellationToken cancellationToken)
|
||||
using (cancellationToken.Register(shutdownCancellationTokenSource.Cancel))
|
||||
try
|
||||
{
|
||||
try
|
||||
logger.LogDebug("Stopping instance manager...");
|
||||
|
||||
if (!startupTask.IsCompleted)
|
||||
{
|
||||
await instance.StopAsync(cancellationToken);
|
||||
logger.LogTrace("Interrupting startup task...");
|
||||
startupCancellationTokenSource.Cancel();
|
||||
await startupTask;
|
||||
}
|
||||
catch (Exception ex)
|
||||
|
||||
var instanceFactoryStopTask = instanceFactory.StopAsync(cancellationToken);
|
||||
await jobManager.StopAsync(cancellationToken);
|
||||
|
||||
async Task OfflineInstanceImmediate(IInstance instance, CancellationToken cancellationToken)
|
||||
{
|
||||
logger.LogError(ex, "Instance shutdown exception!");
|
||||
try
|
||||
{
|
||||
await instance.StopAsync(cancellationToken);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
logger.LogError(ex, "Instance shutdown exception!");
|
||||
}
|
||||
}
|
||||
|
||||
await Task.WhenAll(instances.Select(x => OfflineInstanceImmediate(x.Value.Instance, cancellationToken)));
|
||||
await instanceFactoryStopTask;
|
||||
|
||||
await swarmServiceController.Shutdown(cancellationToken);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
logger.LogCritical(ex, "Instance manager stop exception!");
|
||||
}
|
||||
|
||||
await Task.WhenAll(instances.Select(x => OfflineInstanceImmediate(x.Value.Instance, cancellationToken)));
|
||||
await instanceFactoryStopTask;
|
||||
|
||||
await swarmServiceController.Shutdown(cancellationToken);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
logger.LogCritical(ex, "Instance manager stop exception!");
|
||||
}
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
@@ -553,10 +533,86 @@ namespace Tgstation.Server.Host.Components
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Check we have a valid system identity.
|
||||
/// Initializes the <see cref="InstanceManager"/>.
|
||||
/// </summary>
|
||||
/// <param name="cancellationToken">The <see cref="CancellationToken"/> for the operation.</param>
|
||||
/// <returns>A <see cref="Task"/> representing the running operation.</returns>
|
||||
async Task Initialize(CancellationToken cancellationToken)
|
||||
{
|
||||
try
|
||||
{
|
||||
logger.LogInformation("{versionString}", assemblyInformationProvider.VersionString);
|
||||
|
||||
CheckSystemCompatibility();
|
||||
|
||||
// To let the web server startup immediately before we do any intense work
|
||||
await Task.Yield();
|
||||
|
||||
await InitializeSwarm(cancellationToken);
|
||||
|
||||
List<Models.Instance> dbInstances = null;
|
||||
var instanceEnumeration = databaseContextFactory.UseContext(
|
||||
async databaseContext => dbInstances = await databaseContext
|
||||
.Instances
|
||||
.AsQueryable()
|
||||
.Where(x => x.Online.Value && x.SwarmIdentifer == swarmConfiguration.Identifier)
|
||||
.Include(x => x.RepositorySettings)
|
||||
.Include(x => x.ChatSettings)
|
||||
.ThenInclude(x => x.Channels)
|
||||
.Include(x => x.DreamDaemonSettings)
|
||||
.ToListAsync(cancellationToken));
|
||||
|
||||
var factoryStartup = instanceFactory.StartAsync(cancellationToken);
|
||||
var jobManagerStartup = jobManager.StartAsync(cancellationToken);
|
||||
|
||||
await Task.WhenAll(instanceEnumeration, factoryStartup, jobManagerStartup);
|
||||
|
||||
var instanceOnliningTasks = dbInstances.Select(
|
||||
async metadata =>
|
||||
{
|
||||
try
|
||||
{
|
||||
await OnlineInstance(metadata, cancellationToken);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
logger.LogError(ex, "Failed to online instance {instanceId}!", metadata.Id);
|
||||
}
|
||||
});
|
||||
|
||||
await Task.WhenAll(instanceOnliningTasks);
|
||||
|
||||
jobManager.Activate(this);
|
||||
|
||||
logger.LogInformation("Server ready!");
|
||||
readyTcs.SetResult();
|
||||
}
|
||||
catch (OperationCanceledException ex)
|
||||
{
|
||||
logger.LogInformation(ex, "Cancelled instance manager initialization!");
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
logger.LogCritical(e, "Instance manager startup error!");
|
||||
try
|
||||
{
|
||||
await serverControl.Die(e);
|
||||
return;
|
||||
}
|
||||
catch (Exception e2)
|
||||
{
|
||||
logger.LogCritical(e2, "Failed to kill server!");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Check we have a valid system and configuration.
|
||||
/// </summary>
|
||||
void CheckSystemCompatibility()
|
||||
{
|
||||
generalConfiguration.CheckCompatibility(logger);
|
||||
|
||||
using (var systemIdentity = systemIdentityFactory.GetCurrent())
|
||||
{
|
||||
if (!systemIdentity.CanCreateSymlinks)
|
||||
|
||||
@@ -9,92 +9,51 @@ using Tgstation.Server.Host.Components.Repository;
|
||||
using Tgstation.Server.Host.Components.StaticFiles;
|
||||
using Tgstation.Server.Host.Components.Watchdog;
|
||||
using Tgstation.Server.Host.Models;
|
||||
using Tgstation.Server.Host.Utils;
|
||||
|
||||
namespace Tgstation.Server.Host.Components
|
||||
{
|
||||
/// <summary>
|
||||
/// Warpper around a given <see cref="IInstance"/> with a <see cref="Dispose"/> <see cref="Action"/>.
|
||||
/// <see cref="ReferenceCounter{TInstance}"/> for a given <see cref="IInstance"/>.
|
||||
/// </summary>
|
||||
sealed class InstanceWrapper : IInstanceReference
|
||||
sealed class InstanceWrapper : ReferenceCounter<IInstance>, IInstanceReference
|
||||
{
|
||||
/// <inheritdoc />
|
||||
public Guid Uid { get; }
|
||||
|
||||
/// <summary>
|
||||
/// The <see langword="lock"/> object for <see cref="Dispose"/>.
|
||||
/// </summary>
|
||||
readonly object disposeLock;
|
||||
/// <inheritdoc />
|
||||
public IRepositoryManager RepositoryManager => Instance.RepositoryManager;
|
||||
|
||||
/// <summary>
|
||||
/// The <see cref="Action"/> to take when <see cref="Dispose"/> is called.
|
||||
/// </summary>
|
||||
Action onDisposed;
|
||||
/// <inheritdoc />
|
||||
public IByondManager ByondManager => Instance.ByondManager;
|
||||
|
||||
/// <summary>
|
||||
/// The <see cref="IInstance"/> calls are forwarded to.
|
||||
/// </summary>
|
||||
IInstanceCore actualInstance;
|
||||
/// <inheritdoc />
|
||||
public IDreamMaker DreamMaker => Instance.DreamMaker;
|
||||
|
||||
/// <inheritdoc />
|
||||
public IWatchdog Watchdog => Instance.Watchdog;
|
||||
|
||||
/// <inheritdoc />
|
||||
public IChatManager Chat => Instance.Chat;
|
||||
|
||||
/// <inheritdoc />
|
||||
public IConfiguration Configuration => Instance.Configuration;
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="InstanceWrapper"/> class.
|
||||
/// </summary>
|
||||
/// <param name="actualInstance">The value of <see cref="actualInstance"/>.</param>
|
||||
/// <param name="onDisposed">The value of <see cref="onDisposed"/>.</param>
|
||||
public InstanceWrapper(IInstanceCore actualInstance, Action onDisposed)
|
||||
public InstanceWrapper()
|
||||
{
|
||||
this.actualInstance = actualInstance ?? throw new ArgumentNullException(nameof(actualInstance));
|
||||
this.onDisposed = onDisposed ?? throw new ArgumentNullException(nameof(onDisposed));
|
||||
Uid = Guid.NewGuid();
|
||||
disposeLock = new object();
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public void Dispose()
|
||||
{
|
||||
lock (disposeLock)
|
||||
{
|
||||
onDisposed?.Invoke();
|
||||
onDisposed = null;
|
||||
actualInstance = null;
|
||||
}
|
||||
}
|
||||
public Task InstanceRenamed(string newInstanceName, CancellationToken cancellationToken) => Instance.InstanceRenamed(newInstanceName, cancellationToken);
|
||||
|
||||
/// <inheritdoc />
|
||||
public IRepositoryManager RepositoryManager => actualInstance?.RepositoryManager ?? throw new ObjectDisposedException(nameof(InstanceWrapper));
|
||||
public Task SetAutoUpdateInterval(uint newInterval) => Instance.SetAutoUpdateInterval(newInterval);
|
||||
|
||||
/// <inheritdoc />
|
||||
public IByondManager ByondManager => actualInstance?.ByondManager ?? throw new ObjectDisposedException(nameof(InstanceWrapper));
|
||||
|
||||
/// <inheritdoc />
|
||||
public IDreamMaker DreamMaker => actualInstance?.DreamMaker ?? throw new ObjectDisposedException(nameof(InstanceWrapper));
|
||||
|
||||
/// <inheritdoc />
|
||||
public IWatchdog Watchdog => actualInstance?.Watchdog ?? throw new ObjectDisposedException(nameof(InstanceWrapper));
|
||||
|
||||
/// <inheritdoc />
|
||||
public IChatManager Chat => actualInstance?.Chat ?? throw new ObjectDisposedException(nameof(InstanceWrapper));
|
||||
|
||||
/// <inheritdoc />
|
||||
public IConfiguration Configuration => actualInstance?.Configuration ?? throw new ObjectDisposedException(nameof(InstanceWrapper));
|
||||
|
||||
/// <inheritdoc />
|
||||
public Task InstanceRenamed(string newInstanceName, CancellationToken cancellationToken)
|
||||
=> actualInstance?.InstanceRenamed(newInstanceName, cancellationToken) ?? throw new ObjectDisposedException(nameof(InstanceWrapper));
|
||||
|
||||
/// <inheritdoc />
|
||||
public CompileJob LatestCompileJob()
|
||||
{
|
||||
if (actualInstance == null)
|
||||
throw new ObjectDisposedException(nameof(InstanceWrapper));
|
||||
return actualInstance.LatestCompileJob();
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public Task SetAutoUpdateInterval(uint newInterval)
|
||||
{
|
||||
if (actualInstance == null)
|
||||
throw new ObjectDisposedException(nameof(InstanceWrapper));
|
||||
return actualInstance.SetAutoUpdateInterval(newInterval);
|
||||
}
|
||||
public CompileJob LatestCompileJob() => Instance.LatestCompileJob();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -7,8 +7,8 @@ using Octokit;
|
||||
|
||||
using Tgstation.Server.Api.Models;
|
||||
using Tgstation.Server.Api.Models.Internal;
|
||||
using Tgstation.Server.Host.Core;
|
||||
using Tgstation.Server.Host.Extensions;
|
||||
using Tgstation.Server.Host.Utils;
|
||||
|
||||
namespace Tgstation.Server.Host.Components.Repository
|
||||
{
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
using Microsoft.Extensions.Logging;
|
||||
|
||||
using Tgstation.Server.Api.Models;
|
||||
using Tgstation.Server.Host.Core;
|
||||
using Tgstation.Server.Host.Utils;
|
||||
|
||||
namespace Tgstation.Server.Host.Components.Repository
|
||||
{
|
||||
|
||||
@@ -8,9 +8,9 @@ using Microsoft.Extensions.Logging;
|
||||
using Tgstation.Server.Api.Models;
|
||||
using Tgstation.Server.Host.Components.Events;
|
||||
using Tgstation.Server.Host.Configuration;
|
||||
using Tgstation.Server.Host.Core;
|
||||
using Tgstation.Server.Host.IO;
|
||||
using Tgstation.Server.Host.Jobs;
|
||||
using Tgstation.Server.Host.Utils;
|
||||
|
||||
namespace Tgstation.Server.Host.Components.Repository
|
||||
{
|
||||
|
||||
@@ -22,6 +22,7 @@ using Tgstation.Server.Host.Components.Interop;
|
||||
using Tgstation.Server.Host.Components.Interop.Bridge;
|
||||
using Tgstation.Server.Host.Components.Interop.Topic;
|
||||
using Tgstation.Server.Host.System;
|
||||
using Tgstation.Server.Host.Utils;
|
||||
|
||||
namespace Tgstation.Server.Host.Components.Session
|
||||
{
|
||||
@@ -229,7 +230,7 @@ namespace Tgstation.Server.Host.Components.Session
|
||||
}
|
||||
else
|
||||
logger.LogTrace(
|
||||
"Not registering session with {0} DMAPI version for interop!",
|
||||
"Not registering session with {reasonWhyDmApiIsBad} DMAPI version for interop!",
|
||||
reattachInformation.Dmb.CompileJob.DMApiVersion == null
|
||||
? "no"
|
||||
: $"incompatible ({reattachInformation.Dmb.CompileJob.DMApiVersion})");
|
||||
@@ -250,7 +251,7 @@ namespace Tgstation.Server.Host.Components.Session
|
||||
apiValidate);
|
||||
|
||||
logger.LogDebug(
|
||||
"Created session controller. CommsKey: {0}, Port: {1}",
|
||||
"Created session controller. CommsKey: {accessIdentifier}, Port: {port}",
|
||||
reattachInformation.AccessIdentifier,
|
||||
reattachInformation.Port);
|
||||
}
|
||||
@@ -290,7 +291,7 @@ namespace Tgstation.Server.Host.Components.Session
|
||||
if (parameters == null)
|
||||
throw new ArgumentNullException(nameof(parameters));
|
||||
|
||||
using (LogContext.PushProperty("Instance", metadata.Id))
|
||||
using (LogContext.PushProperty(SerilogContextHelper.InstanceIdContextProperty, metadata.Id))
|
||||
{
|
||||
Logger.LogTrace("Handling bridge request...");
|
||||
|
||||
@@ -329,14 +330,14 @@ namespace Tgstation.Server.Host.Components.Session
|
||||
if (Lifetime.IsCompleted)
|
||||
{
|
||||
Logger.LogWarning(
|
||||
"Attempted to send a command to an inactive SessionController: {0}",
|
||||
"Attempted to send a command to an inactive SessionController: {commandType}",
|
||||
parameters.CommandType);
|
||||
return null;
|
||||
}
|
||||
|
||||
if (!DMApiAvailable)
|
||||
{
|
||||
Logger.LogTrace("Not sending topic request {0} to server without/with incompatible DMAPI!", parameters.CommandType);
|
||||
Logger.LogTrace("Not sending topic request {commandType} to server without/with incompatible DMAPI!", parameters.CommandType);
|
||||
return null;
|
||||
}
|
||||
|
||||
@@ -450,7 +451,7 @@ namespace Tgstation.Server.Host.Components.Session
|
||||
if (RebootState == newRebootState)
|
||||
return true;
|
||||
|
||||
Logger.LogTrace("Changing reboot state to {0}", newRebootState);
|
||||
Logger.LogTrace("Changing reboot state to {newRebootState}", newRebootState);
|
||||
|
||||
ReattachInformation.RebootState = newRebootState;
|
||||
var result = await SendCommand(
|
||||
@@ -528,7 +529,7 @@ namespace Tgstation.Server.Host.Components.Session
|
||||
toAwait = Task.WhenAny(toAwait, Task.Delay(TimeSpan.FromSeconds(startupTimeout.Value)));
|
||||
|
||||
Logger.LogTrace(
|
||||
"Waiting for LaunchResult based on {0}{1}...",
|
||||
"Waiting for LaunchResult based on {launchResultCompletionCause}{possibleTimeout}...",
|
||||
useBridgeRequestForLaunchResult ? "initial bridge request" : "process startup",
|
||||
startupTimeout.HasValue ? $" with a timeout of {startupTimeout.Value}s" : String.Empty);
|
||||
|
||||
@@ -540,7 +541,7 @@ namespace Tgstation.Server.Host.Components.Session
|
||||
StartupTime = startupTask.IsCompleted ? (DateTimeOffset.UtcNow - startTime) : null,
|
||||
};
|
||||
|
||||
Logger.LogTrace("Launch result: {0}", result);
|
||||
Logger.LogTrace("Launch result: {launchResult}", result);
|
||||
|
||||
if (!result.ExitCode.HasValue && reattached && !disposed)
|
||||
{
|
||||
@@ -548,16 +549,18 @@ namespace Tgstation.Server.Host.Components.Session
|
||||
new TopicParameters(
|
||||
assemblyInformationProvider.Version,
|
||||
ReattachInformation.RuntimeInformation.ServerPort),
|
||||
reattachTopicCts.Token)
|
||||
;
|
||||
reattachTopicCts.Token);
|
||||
|
||||
if (reattachResponse != null)
|
||||
{
|
||||
if (reattachResponse?.CustomCommands != null)
|
||||
chatTrackingContext.CustomCommands = reattachResponse.CustomCommands;
|
||||
else if (reattachResponse != null)
|
||||
Logger.LogWarning(
|
||||
"DMAPI Interop v{0} isn't returning the TGS custom commands list. Functionality added in v5.2.0.",
|
||||
Logger.Log(
|
||||
CompileJob.DMApiVersion >= new Version(5, 2, 0)
|
||||
? LogLevel.Warning
|
||||
: LogLevel.Debug,
|
||||
"DMAPI Interop v{interopVersion} isn't returning the TGS custom commands list. Functionality added in v5.2.0.",
|
||||
CompileJob.DMApiVersion.Semver());
|
||||
}
|
||||
}
|
||||
@@ -683,7 +686,7 @@ namespace Tgstation.Server.Host.Components.Session
|
||||
return BridgeError("Invalid minimumSecurityLevel!");
|
||||
}
|
||||
|
||||
Logger.LogTrace("ApiValidationStatus set to {0}", apiValidationStatus);
|
||||
Logger.LogTrace("ApiValidationStatus set to {apiValidationStatus}", apiValidationStatus);
|
||||
|
||||
response.RuntimeInformation = new RuntimeInformation(
|
||||
chatTrackingContext,
|
||||
@@ -746,7 +749,7 @@ namespace Tgstation.Server.Host.Components.Session
|
||||
parameters.AccessIdentifier = ReattachInformation.AccessIdentifier;
|
||||
|
||||
var fullCommandString = GenerateQueryString(parameters, out var json);
|
||||
Logger.LogTrace("Topic request: {0}", json);
|
||||
Logger.LogTrace("Topic request: {json}", json);
|
||||
var fullCommandByteCount = Encoding.UTF8.GetByteCount(fullCommandString);
|
||||
if (fullCommandByteCount <= DMApiConstants.MaximumTopicRequestLength)
|
||||
return await SendRawTopic(fullCommandString, cancellationToken);
|
||||
|
||||
@@ -263,16 +263,16 @@ namespace Tgstation.Server.Host.Components.Session
|
||||
}
|
||||
|
||||
// get the byond lock
|
||||
var byondLock = currentByondLock ?? await byond.UseExecutables(Version.Parse(dmbProvider.CompileJob.ByondVersion), cancellationToken);
|
||||
var byondLock = currentByondLock ?? await byond.UseExecutables(
|
||||
Version.Parse(dmbProvider.CompileJob.ByondVersion),
|
||||
gameIOManager.ConcatPath(dmbProvider.Directory, dmbProvider.DmbName),
|
||||
cancellationToken);
|
||||
try
|
||||
{
|
||||
logger.LogDebug(
|
||||
"Launching session with CompileJob {compileJobId}...",
|
||||
dmbProvider.CompileJob.Id);
|
||||
|
||||
if (launchParameters.SecurityLevel == DreamDaemonSecurity.Trusted)
|
||||
await byondLock.TrustDmbPath(gameIOManager.ConcatPath(dmbProvider.Directory, dmbProvider.DmbName), cancellationToken);
|
||||
|
||||
PortBindTest(launchParameters.Port.Value);
|
||||
await CheckPagerIsNotRunning(cancellationToken);
|
||||
|
||||
@@ -385,7 +385,10 @@ namespace Tgstation.Server.Host.Components.Session
|
||||
|
||||
logger.LogTrace("Begin session reattach...");
|
||||
var byondTopicSender = topicClientFactory.CreateTopicClient(reattachInformation.TopicRequestTimeout);
|
||||
var byondLock = await byond.UseExecutables(Version.Parse(reattachInformation.Dmb.CompileJob.ByondVersion), cancellationToken);
|
||||
var byondLock = await byond.UseExecutables(
|
||||
Version.Parse(reattachInformation.Dmb.CompileJob.ByondVersion),
|
||||
null, // Doesn't matter if it's trusted or not on reattach
|
||||
cancellationToken);
|
||||
|
||||
try
|
||||
{
|
||||
|
||||
@@ -14,13 +14,13 @@ using Tgstation.Server.Api.Models;
|
||||
using Tgstation.Server.Api.Models.Response;
|
||||
using Tgstation.Server.Host.Components.Events;
|
||||
using Tgstation.Server.Host.Configuration;
|
||||
using Tgstation.Server.Host.Core;
|
||||
using Tgstation.Server.Host.Extensions;
|
||||
using Tgstation.Server.Host.IO;
|
||||
using Tgstation.Server.Host.Jobs;
|
||||
using Tgstation.Server.Host.Security;
|
||||
using Tgstation.Server.Host.System;
|
||||
using Tgstation.Server.Host.Transfer;
|
||||
using Tgstation.Server.Host.Utils;
|
||||
|
||||
namespace Tgstation.Server.Host.Components.StaticFiles
|
||||
{
|
||||
|
||||
@@ -15,6 +15,7 @@ using Tgstation.Server.Host.Components.Session;
|
||||
using Tgstation.Server.Host.Core;
|
||||
using Tgstation.Server.Host.IO;
|
||||
using Tgstation.Server.Host.Jobs;
|
||||
using Tgstation.Server.Host.Utils;
|
||||
|
||||
namespace Tgstation.Server.Host.Components.Watchdog
|
||||
{
|
||||
@@ -132,8 +133,7 @@ namespace Tgstation.Server.Host.Components.Watchdog
|
||||
CultureInfo.InvariantCulture,
|
||||
"Server {0}! Rebooting...",
|
||||
exitWord),
|
||||
cancellationToken)
|
||||
;
|
||||
cancellationToken);
|
||||
return MonitorAction.Restart;
|
||||
case MonitorActivationReason.ActiveServerRebooted:
|
||||
var rebootState = Server.RebootState;
|
||||
@@ -242,8 +242,7 @@ namespace Tgstation.Server.Host.Components.Watchdog
|
||||
if (Server == null)
|
||||
{
|
||||
await ReattachFailure(
|
||||
cancellationToken)
|
||||
;
|
||||
cancellationToken);
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
@@ -13,6 +13,7 @@ using Tgstation.Server.Host.Components.Session;
|
||||
using Tgstation.Server.Host.Core;
|
||||
using Tgstation.Server.Host.IO;
|
||||
using Tgstation.Server.Host.Jobs;
|
||||
using Tgstation.Server.Host.Utils;
|
||||
|
||||
namespace Tgstation.Server.Host.Components.Watchdog
|
||||
{
|
||||
@@ -137,7 +138,7 @@ namespace Tgstation.Server.Host.Components.Watchdog
|
||||
{
|
||||
Logger.LogError(
|
||||
ex,
|
||||
"Failed to un-hard link compile job #{0} ({1})",
|
||||
"Failed to un-hard link compile job #{compileJobId} ({compileJobDirectory})",
|
||||
hardLinkedDmb.CompileJob.Id,
|
||||
hardLinkedDmb.CompileJob.DirectoryName);
|
||||
}
|
||||
|
||||
@@ -13,6 +13,7 @@ using Tgstation.Server.Host.Configuration;
|
||||
using Tgstation.Server.Host.Core;
|
||||
using Tgstation.Server.Host.IO;
|
||||
using Tgstation.Server.Host.Jobs;
|
||||
using Tgstation.Server.Host.Utils;
|
||||
|
||||
namespace Tgstation.Server.Host.Components.Watchdog
|
||||
{
|
||||
|
||||
@@ -21,6 +21,7 @@ using Tgstation.Server.Host.Core;
|
||||
using Tgstation.Server.Host.Extensions;
|
||||
using Tgstation.Server.Host.IO;
|
||||
using Tgstation.Server.Host.Jobs;
|
||||
using Tgstation.Server.Host.Utils;
|
||||
|
||||
namespace Tgstation.Server.Host.Components.Watchdog
|
||||
{
|
||||
@@ -740,11 +741,7 @@ namespace Tgstation.Server.Host.Components.Watchdog
|
||||
Logger.LogDebug("Relaunch successful, resuming monitor...");
|
||||
return;
|
||||
}
|
||||
catch (OperationCanceledException)
|
||||
{
|
||||
throw;
|
||||
}
|
||||
catch (Exception e)
|
||||
catch (Exception e) when (e is not OperationCanceledException)
|
||||
{
|
||||
launchException = e;
|
||||
}
|
||||
@@ -820,7 +817,7 @@ namespace Tgstation.Server.Host.Components.Watchdog
|
||||
ISessionController lastController = null;
|
||||
var ranInitialDmbCheck = false;
|
||||
for (ulong iteration = 1; nextAction != MonitorAction.Exit; ++iteration)
|
||||
using (LogContext.PushProperty("Monitor", iteration))
|
||||
using (LogContext.PushProperty(SerilogContextHelper.WatchdogMonitorIterationContextProperty, iteration))
|
||||
try
|
||||
{
|
||||
Logger.LogTrace("Iteration {iteration} of monitor loop", iteration);
|
||||
@@ -937,13 +934,11 @@ namespace Tgstation.Server.Host.Components.Watchdog
|
||||
Logger.LogTrace("Reason: {activationReason}", activationReason);
|
||||
if (activationReason == MonitorActivationReason.Heartbeat)
|
||||
nextAction = await HandleHeartbeat(
|
||||
cancellationToken)
|
||||
;
|
||||
cancellationToken);
|
||||
else
|
||||
nextAction = await HandleMonitorWakeup(
|
||||
activationReason,
|
||||
cancellationToken)
|
||||
;
|
||||
cancellationToken);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -957,12 +952,7 @@ namespace Tgstation.Server.Host.Components.Watchdog
|
||||
nextAction = MonitorAction.Continue;
|
||||
}
|
||||
}
|
||||
catch (OperationCanceledException)
|
||||
{
|
||||
// let this bubble, other exceptions caught below
|
||||
throw;
|
||||
}
|
||||
catch (Exception e)
|
||||
catch (Exception e) when (e is not OperationCanceledException)
|
||||
{
|
||||
// really, this should NEVER happen
|
||||
Logger.LogError(
|
||||
|
||||
@@ -13,6 +13,7 @@ using Tgstation.Server.Host.Configuration;
|
||||
using Tgstation.Server.Host.Core;
|
||||
using Tgstation.Server.Host.IO;
|
||||
using Tgstation.Server.Host.Jobs;
|
||||
using Tgstation.Server.Host.Utils;
|
||||
|
||||
namespace Tgstation.Server.Host.Components.Watchdog
|
||||
{
|
||||
|
||||
@@ -13,6 +13,7 @@ using Tgstation.Server.Host.Components.Session;
|
||||
using Tgstation.Server.Host.Core;
|
||||
using Tgstation.Server.Host.IO;
|
||||
using Tgstation.Server.Host.Jobs;
|
||||
using Tgstation.Server.Host.Utils;
|
||||
|
||||
namespace Tgstation.Server.Host.Components.Watchdog
|
||||
{
|
||||
|
||||
@@ -13,6 +13,7 @@ using Tgstation.Server.Host.Configuration;
|
||||
using Tgstation.Server.Host.Core;
|
||||
using Tgstation.Server.Host.IO;
|
||||
using Tgstation.Server.Host.Jobs;
|
||||
using Tgstation.Server.Host.Utils;
|
||||
|
||||
namespace Tgstation.Server.Host.Components.Watchdog
|
||||
{
|
||||
|
||||
@@ -24,6 +24,7 @@ using Tgstation.Server.Host.IO;
|
||||
using Tgstation.Server.Host.Security;
|
||||
using Tgstation.Server.Host.System;
|
||||
using Tgstation.Server.Host.Transfer;
|
||||
using Tgstation.Server.Host.Utils;
|
||||
|
||||
namespace Tgstation.Server.Host.Controllers
|
||||
{
|
||||
|
||||
@@ -23,6 +23,7 @@ using Tgstation.Server.Api.Models.Response;
|
||||
using Tgstation.Server.Host.Database;
|
||||
using Tgstation.Server.Host.Models;
|
||||
using Tgstation.Server.Host.Security;
|
||||
using Tgstation.Server.Host.Utils;
|
||||
|
||||
namespace Tgstation.Server.Host.Controllers
|
||||
{
|
||||
@@ -171,12 +172,12 @@ namespace Tgstation.Server.Host.Controllers
|
||||
}
|
||||
|
||||
using (ApiHeaders?.InstanceId != null
|
||||
? LogContext.PushProperty("Instance", ApiHeaders.InstanceId)
|
||||
? LogContext.PushProperty(SerilogContextHelper.InstanceIdContextProperty, ApiHeaders.InstanceId)
|
||||
: null)
|
||||
using (AuthenticationContext != null
|
||||
? LogContext.PushProperty("User", AuthenticationContext.User.Id)
|
||||
? LogContext.PushProperty(SerilogContextHelper.UserIdContextProperty, AuthenticationContext.User.Id)
|
||||
: null)
|
||||
using (LogContext.PushProperty("Request", $"{Request.Method} {Request.Path}"))
|
||||
using (LogContext.PushProperty(SerilogContextHelper.RequestPathContextProperty, $"{Request.Method} {Request.Path}"))
|
||||
{
|
||||
if (ApiHeaders != null)
|
||||
{
|
||||
|
||||
@@ -12,6 +12,7 @@ using Serilog.Context;
|
||||
|
||||
using Tgstation.Server.Host.Components.Interop;
|
||||
using Tgstation.Server.Host.Components.Interop.Bridge;
|
||||
using Tgstation.Server.Host.Utils;
|
||||
|
||||
namespace Tgstation.Server.Host.Controllers
|
||||
{
|
||||
@@ -86,7 +87,7 @@ namespace Tgstation.Server.Host.Controllers
|
||||
return Forbid();
|
||||
}
|
||||
|
||||
using (LogContext.PushProperty("Bridge", Interlocked.Increment(ref requestsProcessed)))
|
||||
using (LogContext.PushProperty(SerilogContextHelper.BridgeRequestIterationContextProperty, Interlocked.Increment(ref requestsProcessed)))
|
||||
{
|
||||
BridgeParameters request;
|
||||
try
|
||||
|
||||
@@ -37,27 +37,34 @@ namespace Tgstation.Server.Host.Controllers
|
||||
/// </summary>
|
||||
readonly IFileTransferTicketProvider fileTransferService;
|
||||
|
||||
/// <summary>
|
||||
/// Remove the <see cref="Version.Build"/> from a given <paramref name="version"/> if present.
|
||||
/// </summary>
|
||||
/// <param name="version">The <see cref="Version"/> to normalize.</param>
|
||||
/// <returns>The normalized <see cref="Version"/>. May be a reference to <paramref name="version"/>.</returns>
|
||||
static Version NormalizeVersion(Version version) => version.Build == 0 ? new Version(version.Major, version.Minor) : version;
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="ByondController"/> class.
|
||||
/// </summary>
|
||||
/// <param name="databaseContext">The <see cref="IDatabaseContext"/> for the <see cref="ApiController"/>.</param>
|
||||
/// <param name="authenticationContextFactory">The <see cref="IAuthenticationContextFactory"/> for the <see cref="ApiController"/>.</param>
|
||||
/// <param name="databaseContext">The <see cref="IDatabaseContext"/> for the <see cref="InstanceRequiredController"/>.</param>
|
||||
/// <param name="authenticationContextFactory">The <see cref="IAuthenticationContextFactory"/> for the <see cref="InstanceRequiredController"/>.</param>
|
||||
/// <param name="logger">The <see cref="ILogger"/> for the <see cref="InstanceRequiredController"/>.</param>
|
||||
/// <param name="instanceManager">The <see cref="IInstanceManager"/> for the <see cref="InstanceRequiredController"/>.</param>
|
||||
/// <param name="jobManager">The value of <see cref="jobManager"/>.</param>
|
||||
/// <param name="fileTransferService">The value of <see cref="fileTransferService"/>.</param>
|
||||
/// <param name="logger">The <see cref="ILogger"/> for the <see cref="ApiController"/>.</param>
|
||||
public ByondController(
|
||||
IDatabaseContext databaseContext,
|
||||
IAuthenticationContextFactory authenticationContextFactory,
|
||||
ILogger<ByondController> logger,
|
||||
IInstanceManager instanceManager,
|
||||
IJobManager jobManager,
|
||||
IFileTransferTicketProvider fileTransferService,
|
||||
ILogger<ByondController> logger)
|
||||
IFileTransferTicketProvider fileTransferService)
|
||||
: base(
|
||||
instanceManager,
|
||||
databaseContext,
|
||||
authenticationContextFactory,
|
||||
logger)
|
||||
logger,
|
||||
instanceManager)
|
||||
{
|
||||
this.jobManager = jobManager ?? throw new ArgumentNullException(nameof(jobManager));
|
||||
this.fileTransferService = fileTransferService ?? throw new ArgumentNullException(nameof(fileTransferService));
|
||||
@@ -112,7 +119,7 @@ namespace Tgstation.Server.Host.Controllers
|
||||
/// <summary>
|
||||
/// Changes the active BYOND version to the one specified in a given <paramref name="model"/>.
|
||||
/// </summary>
|
||||
/// <param name="model">The <see cref="ByondVersionRequest.Version"/> to switch to.</param>
|
||||
/// <param name="model">The <see cref="ByondVersionRequest"/> containing the <see cref="Version"/> to switch to.</param>
|
||||
/// <param name="cancellationToken">The <see cref="CancellationToken"/> for the operation.</param>
|
||||
/// <returns>A <see cref="Task{TResult}"/> resulting in the <see cref="IActionResult"/> for the operation.</returns>
|
||||
/// <response code="200">Switched active version successfully.</response>
|
||||
@@ -135,6 +142,8 @@ namespace Tgstation.Server.Host.Controllers
|
||||
|| (uploadingZip && model.Version.Build > 0))
|
||||
return BadRequest(new ErrorMessageResponse(ErrorCode.ModelValidationFailure));
|
||||
|
||||
var version = NormalizeVersion(model.Version);
|
||||
|
||||
var userByondRights = AuthenticationContext.InstancePermissionSet.ByondRights.Value;
|
||||
if ((!userByondRights.HasFlag(ByondRights.InstallOfficialOrChangeActiveVersion) && !uploadingZip)
|
||||
|| (!userByondRights.HasFlag(ByondRights.InstallCustomVersion) && uploadingZip))
|
||||
@@ -146,33 +155,44 @@ namespace Tgstation.Server.Host.Controllers
|
||||
async instance =>
|
||||
{
|
||||
var byondManager = instance.ByondManager;
|
||||
if (!uploadingZip && byondManager.InstalledVersions.Any(x => x == model.Version))
|
||||
var versionAlreadyInstalled = !uploadingZip && byondManager.InstalledVersions.Any(x => x == version);
|
||||
if (versionAlreadyInstalled)
|
||||
{
|
||||
Logger.LogInformation(
|
||||
"User ID {userId} changing instance ID {instanceId} BYOND version to {newByondVersion}",
|
||||
AuthenticationContext.User.Id,
|
||||
Instance.Id,
|
||||
model.Version);
|
||||
await byondManager.ChangeVersion(model.Version, null, cancellationToken);
|
||||
version);
|
||||
|
||||
try
|
||||
{
|
||||
await byondManager.ChangeVersion(null, version, null, false, cancellationToken);
|
||||
}
|
||||
catch (InvalidOperationException ex)
|
||||
{
|
||||
Logger.LogDebug(
|
||||
ex,
|
||||
"Race condition: BYOND version {version} uninstalled before we could switch to it. Creating install job instead...",
|
||||
version);
|
||||
versionAlreadyInstalled = false;
|
||||
}
|
||||
}
|
||||
else if (model.Version.Build > 0)
|
||||
return BadRequest(new ErrorMessageResponse(ErrorCode.ByondNonExistentCustomVersion));
|
||||
else
|
||||
|
||||
if (!versionAlreadyInstalled)
|
||||
{
|
||||
var installingVersion = model.Version.Build <= 0
|
||||
? new Version(model.Version.Major, model.Version.Minor)
|
||||
: model.Version;
|
||||
if (version.Build > 0)
|
||||
return BadRequest(new ErrorMessageResponse(ErrorCode.ByondNonExistentCustomVersion));
|
||||
|
||||
Logger.LogInformation(
|
||||
"User ID {userId} installing BYOND version to {newByondVersion} on instance ID {instanceId}",
|
||||
AuthenticationContext.User.Id,
|
||||
installingVersion,
|
||||
version,
|
||||
Instance.Id);
|
||||
|
||||
// run the install through the job manager
|
||||
var job = new Job
|
||||
{
|
||||
Description = $"Install {(!uploadingZip ? String.Empty : "custom ")}BYOND version {model.Version.Major}.{model.Version.Minor}",
|
||||
Description = $"Install {(!uploadingZip ? String.Empty : "custom ")}BYOND version {version}",
|
||||
StartedBy = AuthenticationContext.User,
|
||||
CancelRightsType = RightsType.Byond,
|
||||
CancelRight = (ulong)ByondRights.CancelInstall,
|
||||
@@ -193,10 +213,7 @@ namespace Tgstation.Server.Host.Controllers
|
||||
if (fileUploadTicket != null)
|
||||
using (fileUploadTicket)
|
||||
{
|
||||
var uploadStream = await fileUploadTicket.GetResult(jobCancellationToken);
|
||||
if (uploadStream == null)
|
||||
throw new JobException(ErrorCode.FileUploadExpired);
|
||||
|
||||
var uploadStream = await fileUploadTicket.GetResult(jobCancellationToken) ?? throw new JobException(ErrorCode.FileUploadExpired);
|
||||
zipFileStream = new MemoryStream();
|
||||
try
|
||||
{
|
||||
@@ -211,13 +228,13 @@ namespace Tgstation.Server.Host.Controllers
|
||||
|
||||
using (zipFileStream)
|
||||
await core.ByondManager.ChangeVersion(
|
||||
model.Version,
|
||||
progressHandler,
|
||||
version,
|
||||
zipFileStream,
|
||||
jobCancellationToken)
|
||||
;
|
||||
true,
|
||||
jobCancellationToken);
|
||||
},
|
||||
cancellationToken)
|
||||
;
|
||||
cancellationToken);
|
||||
|
||||
result.InstallJob = job.ToApi();
|
||||
result.FileTicket = fileUploadTicket?.Ticket.FileTicket;
|
||||
@@ -230,8 +247,74 @@ namespace Tgstation.Server.Host.Controllers
|
||||
}
|
||||
|
||||
return result.InstallJob != null ? Accepted(result) : Json(result);
|
||||
})
|
||||
;
|
||||
});
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Changes the active BYOND version to the one specified in a given <paramref name="model"/>.
|
||||
/// </summary>
|
||||
/// <param name="model">The <see cref="ByondVersionDeleteRequest"/> containing the <see cref="Version"/> to delete.</param>
|
||||
/// <param name="cancellationToken">The <see cref="CancellationToken"/> for the operation.</param>
|
||||
/// <returns>A <see cref="Task{TResult}"/> resulting in the <see cref="IActionResult"/> for the operation.</returns>
|
||||
/// <response code="202">Created <see cref="Job"/> to delete target version successfully.</response>
|
||||
/// <response code="409">Attempted to delete the active BYOND <see cref="Version"/>.</response>
|
||||
/// <response code="410">The <see cref="ByondVersionDeleteRequest.Version"/> specified was not installed.</response>
|
||||
[HttpDelete]
|
||||
[TgsAuthorize(ByondRights.DeleteInstall)]
|
||||
[ProducesResponseType(typeof(JobResponse), 202)]
|
||||
[ProducesResponseType(typeof(ErrorMessageResponse), 409)]
|
||||
[ProducesResponseType(typeof(ErrorMessageResponse), 410)]
|
||||
public async Task<IActionResult> Delete([FromBody] ByondVersionDeleteRequest model, CancellationToken cancellationToken)
|
||||
{
|
||||
if (model == null)
|
||||
throw new ArgumentNullException(nameof(model));
|
||||
|
||||
if (model.Version == null
|
||||
|| model.Version.Revision != -1)
|
||||
return BadRequest(new ErrorMessageResponse(ErrorCode.ModelValidationFailure));
|
||||
|
||||
var version = NormalizeVersion(model.Version);
|
||||
|
||||
var notInstalledResponse = await WithComponentInstance(
|
||||
instance =>
|
||||
{
|
||||
var byondManager = instance.ByondManager;
|
||||
|
||||
if (version == byondManager.ActiveVersion)
|
||||
return Task.FromResult<IActionResult>(
|
||||
Conflict(new ErrorMessageResponse(ErrorCode.ByondCannotDeleteActiveVersion)));
|
||||
|
||||
var versionNotInstalled = !byondManager.InstalledVersions.Any(x => x == version);
|
||||
|
||||
return Task.FromResult<IActionResult>(
|
||||
versionNotInstalled
|
||||
? Gone()
|
||||
: null);
|
||||
});
|
||||
|
||||
if (notInstalledResponse != null)
|
||||
return notInstalledResponse;
|
||||
|
||||
var isCustomVersion = version.Build != -1;
|
||||
|
||||
// run the install through the job manager
|
||||
var job = new Job
|
||||
{
|
||||
Description = $"Delete installed BYOND version {version}",
|
||||
StartedBy = AuthenticationContext.User,
|
||||
CancelRightsType = RightsType.Byond,
|
||||
CancelRight = (ulong)(isCustomVersion ? ByondRights.InstallOfficialOrChangeActiveVersion : ByondRights.InstallCustomVersion),
|
||||
Instance = Instance,
|
||||
};
|
||||
|
||||
await jobManager.RegisterOperation(
|
||||
job,
|
||||
(instanceCore, databaseContextFactory, job, progressReporter, jobCancellationToken)
|
||||
=> instanceCore.ByondManager.DeleteVersion(progressReporter, version, jobCancellationToken),
|
||||
cancellationToken);
|
||||
|
||||
var apiResponse = job.ToApi();
|
||||
return Accepted(apiResponse);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -37,20 +37,20 @@ namespace Tgstation.Server.Host.Controllers
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="ChatController"/> class.
|
||||
/// </summary>
|
||||
/// <param name="databaseContext">The <see cref="IDatabaseContext"/> for the <see cref="ApiController"/>.</param>
|
||||
/// <param name="authenticationContextFactory">The <see cref="IAuthenticationContextFactory"/> for the <see cref="ApiController"/>.</param>
|
||||
/// <param name="databaseContext">The <see cref="IDatabaseContext"/> for the <see cref="InstanceRequiredController"/>.</param>
|
||||
/// <param name="authenticationContextFactory">The <see cref="IAuthenticationContextFactory"/> for the <see cref="InstanceRequiredController"/>.</param>
|
||||
/// <param name="logger">The <see cref="ILogger"/> for the <see cref="InstanceRequiredController"/>.</param>
|
||||
/// <param name="instanceManager">The <see cref="IInstanceManager"/> for the <see cref="InstanceRequiredController"/>.</param>
|
||||
/// <param name="logger">The <see cref="ILogger"/> for the <see cref="ApiController"/>.</param>
|
||||
public ChatController(
|
||||
IDatabaseContext databaseContext,
|
||||
IAuthenticationContextFactory authenticationContextFactory,
|
||||
IInstanceManager instanceManager,
|
||||
ILogger<ChatController> logger)
|
||||
ILogger<ChatController> logger,
|
||||
IInstanceManager instanceManager)
|
||||
: base(
|
||||
instanceManager,
|
||||
databaseContext,
|
||||
authenticationContextFactory,
|
||||
logger)
|
||||
logger,
|
||||
instanceManager)
|
||||
{
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,134 @@
|
||||
using System;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Serilog.Context;
|
||||
|
||||
using Tgstation.Server.Api.Models;
|
||||
using Tgstation.Server.Api.Models.Response;
|
||||
using Tgstation.Server.Host.Components;
|
||||
using Tgstation.Server.Host.Database;
|
||||
using Tgstation.Server.Host.Security;
|
||||
using Tgstation.Server.Host.Utils;
|
||||
|
||||
namespace Tgstation.Server.Host.Controllers
|
||||
{
|
||||
/// <summary>
|
||||
/// <see cref="ApiController"/> for operations on <see cref="IInstanceCore"/>s.
|
||||
/// </summary>
|
||||
public abstract class ComponentInterfacingController : ApiController
|
||||
{
|
||||
/// <summary>
|
||||
/// Access the <see cref="IInstanceOperations"/> instance.
|
||||
/// </summary>
|
||||
public IInstanceOperations InstanceOperations => instanceManager;
|
||||
|
||||
/// <summary>
|
||||
/// The <see cref="IInstanceManager"/> for the <see cref="ComponentInterfacingController"/>.
|
||||
/// </summary>
|
||||
readonly IInstanceManager instanceManager;
|
||||
|
||||
/// <summary>
|
||||
/// If the <see cref="Api.ApiHeaders.InstanceId"/> header should be checked and used to perform validation for every request.
|
||||
/// </summary>
|
||||
readonly bool useInstanceRequestHeader;
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="ComponentInterfacingController"/> class.
|
||||
/// </summary>
|
||||
/// <param name="instanceManager">The value of <see cref="instanceManager"/>.</param>
|
||||
/// <param name="databaseContext">The <see cref="IDatabaseContext"/> for the <see cref="ApiController"/>.</param>
|
||||
/// <param name="authenticationContextFactory">The <see cref="IAuthenticationContextFactory"/> for the <see cref="ApiController"/>.</param>
|
||||
/// <param name="logger">The <see cref="ILogger"/> for the <see cref="ApiController"/>.</param>
|
||||
/// <param name="useInstanceRequestHeader">The value of <see cref="useInstanceRequestHeader"/>.</param>
|
||||
protected ComponentInterfacingController(
|
||||
IDatabaseContext databaseContext,
|
||||
IAuthenticationContextFactory authenticationContextFactory,
|
||||
ILogger<ComponentInterfacingController> logger,
|
||||
IInstanceManager instanceManager,
|
||||
bool useInstanceRequestHeader = false)
|
||||
: base(
|
||||
databaseContext,
|
||||
authenticationContextFactory,
|
||||
logger,
|
||||
true)
|
||||
{
|
||||
this.instanceManager = instanceManager ?? throw new ArgumentNullException(nameof(instanceManager));
|
||||
this.useInstanceRequestHeader = useInstanceRequestHeader;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override async Task<IActionResult> ValidateRequest(CancellationToken cancellationToken)
|
||||
{
|
||||
if (!useInstanceRequestHeader)
|
||||
return null;
|
||||
|
||||
if (!ApiHeaders.InstanceId.HasValue)
|
||||
return BadRequest(new ErrorMessageResponse(ErrorCode.InstanceHeaderRequired));
|
||||
|
||||
if (AuthenticationContext.InstancePermissionSet == null)
|
||||
return Forbid();
|
||||
|
||||
if (ValidateInstanceOnlineStatus(Instance))
|
||||
await DatabaseContext.Save(cancellationToken);
|
||||
|
||||
using var instanceReferenceCheck = instanceManager.GetInstanceReference(Instance);
|
||||
if (instanceReferenceCheck == null)
|
||||
return Conflict(new ErrorMessageResponse(ErrorCode.InstanceOffline));
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Corrects discrepencies between the <see cref="Api.Models.Instance.Online"/> status of <see cref="IInstance"/>s in the database vs the service.
|
||||
/// </summary>
|
||||
/// <param name="metadata">The <see cref="Api.Models.Instance"/> to check.</param>
|
||||
/// <returns><see langword="true"/> if an unsaved DB update was made, <see langword="false"/> otherwise.</returns>
|
||||
protected bool ValidateInstanceOnlineStatus(Api.Models.Instance metadata)
|
||||
{
|
||||
if (metadata == null)
|
||||
throw new ArgumentNullException(nameof(metadata));
|
||||
|
||||
bool online;
|
||||
using (var instanceReferenceCheck = instanceManager.GetInstanceReference(metadata))
|
||||
online = instanceReferenceCheck != null;
|
||||
|
||||
if (metadata.Online.Value == online)
|
||||
return false;
|
||||
|
||||
const string OfflineWord = "offline";
|
||||
const string OnlineWord = "online";
|
||||
|
||||
Logger.LogWarning(
|
||||
"Instance {instanceId} is says it's {databaseState} in the database, but it is actually {serviceState} in the service. Updating the database to reflect this...",
|
||||
metadata.Id,
|
||||
online ? OfflineWord : OnlineWord,
|
||||
online ? OnlineWord : OfflineWord);
|
||||
|
||||
metadata.Online = online;
|
||||
return true;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Run a given <paramref name="action"/> with the relevant <see cref="IInstance"/>.
|
||||
/// </summary>
|
||||
/// <param name="action">A <see cref="Func{T, TResult}"/> accepting the <see cref="IInstance"/> and returning a <see cref="Task{TResult}"/> with the <see cref="IActionResult"/>.</param>
|
||||
/// <returns>A <see cref="Task{TResult}"/> resulting in the <see cref="IActionResult"/> that should be returned.</returns>
|
||||
/// <remarks>The context of <paramref name="action"/> should be as small as possible so as to avoid race conditions. This function can return a <see cref="ConflictResult"/> if the requested instance was offline.</remarks>
|
||||
protected async Task<IActionResult> WithComponentInstance(Func<IInstanceCore, Task<IActionResult>> action)
|
||||
{
|
||||
if (action == null)
|
||||
throw new ArgumentNullException(nameof(action));
|
||||
|
||||
using var instanceReference = instanceManager.GetInstanceReference(Instance);
|
||||
using (LogContext.PushProperty(SerilogContextHelper.InstanceReferenceContextProperty, instanceReference.Uid))
|
||||
{
|
||||
if (instanceReference == null)
|
||||
return Conflict(new ErrorMessageResponse(ErrorCode.InstanceOffline));
|
||||
return await action(instanceReference);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -34,22 +34,22 @@ namespace Tgstation.Server.Host.Controllers
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="ConfigurationController"/> class.
|
||||
/// </summary>
|
||||
/// <param name="databaseContext">The <see cref="IDatabaseContext"/> for the <see cref="ApiController"/>.</param>
|
||||
/// <param name="authenticationContextFactory">The <see cref="IAuthenticationContextFactory"/> for the <see cref="ApiController"/>.</param>
|
||||
/// <param name="databaseContext">The <see cref="IDatabaseContext"/> for the <see cref="InstanceRequiredController"/>.</param>
|
||||
/// <param name="authenticationContextFactory">The <see cref="IAuthenticationContextFactory"/> for the <see cref="InstanceRequiredController"/>.</param>
|
||||
/// <param name="logger">The <see cref="ILogger"/> for the <see cref="InstanceRequiredController"/>.</param>
|
||||
/// <param name="instanceManager">The <see cref="IInstanceManager"/> for the <see cref="InstanceRequiredController"/>.</param>
|
||||
/// <param name="ioManager">The value of <see cref="ioManager"/>.</param>
|
||||
/// <param name="logger">The <see cref="ILogger"/> for the <see cref="ApiController"/>.</param>
|
||||
public ConfigurationController(
|
||||
IDatabaseContext databaseContext,
|
||||
IAuthenticationContextFactory authenticationContextFactory,
|
||||
ILogger<ConfigurationController> logger,
|
||||
IInstanceManager instanceManager,
|
||||
IIOManager ioManager,
|
||||
ILogger<ConfigurationController> logger)
|
||||
IIOManager ioManager)
|
||||
: base(
|
||||
instanceManager,
|
||||
databaseContext,
|
||||
authenticationContextFactory,
|
||||
logger)
|
||||
logger,
|
||||
instanceManager)
|
||||
{
|
||||
this.ioManager = ioManager ?? throw new ArgumentNullException(nameof(ioManager));
|
||||
}
|
||||
|
||||
@@ -16,11 +16,11 @@ using Tgstation.Server.Api.Models.Response;
|
||||
using Tgstation.Server.Api.Rights;
|
||||
using Tgstation.Server.Host.Components;
|
||||
using Tgstation.Server.Host.Components.Session;
|
||||
using Tgstation.Server.Host.Core;
|
||||
using Tgstation.Server.Host.Database;
|
||||
using Tgstation.Server.Host.Jobs;
|
||||
using Tgstation.Server.Host.Models;
|
||||
using Tgstation.Server.Host.Security;
|
||||
using Tgstation.Server.Host.Utils;
|
||||
|
||||
#pragma warning disable API1001 // Action method returns a success result without a corresponding ProducesResponseType. Somehow this happens ONLY IN THIS CONTROLLER???
|
||||
|
||||
@@ -45,24 +45,24 @@ namespace Tgstation.Server.Host.Controllers
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="DreamDaemonController"/> class.
|
||||
/// </summary>
|
||||
/// <param name="databaseContext">The <see cref="IDatabaseContext"/> for the <see cref="ApiController"/>.</param>
|
||||
/// <param name="authenticationContextFactory">The <see cref="IAuthenticationContextFactory"/> for the <see cref="ApiController"/>.</param>
|
||||
/// <param name="jobManager">The value of <see cref="jobManager"/>.</param>
|
||||
/// <param name="databaseContext">The <see cref="IDatabaseContext"/> for the <see cref="InstanceRequiredController"/>.</param>
|
||||
/// <param name="authenticationContextFactory">The <see cref="IAuthenticationContextFactory"/> for the <see cref="InstanceRequiredController"/>.</param>
|
||||
/// <param name="logger">The <see cref="ILogger"/> for the <see cref="InstanceRequiredController"/>.</param>
|
||||
/// <param name="instanceManager">The <see cref="IInstanceManager"/> for the <see cref="InstanceRequiredController"/>.</param>
|
||||
/// <param name="jobManager">The value of <see cref="jobManager"/>.</param>
|
||||
/// <param name="portAllocator">The value of <see cref="IPortAllocator"/>.</param>
|
||||
/// <param name="logger">The <see cref="ILogger"/> for the <see cref="ApiController"/>.</param>
|
||||
public DreamDaemonController(
|
||||
IDatabaseContext databaseContext,
|
||||
IAuthenticationContextFactory authenticationContextFactory,
|
||||
IJobManager jobManager,
|
||||
ILogger<DreamDaemonController> logger,
|
||||
IInstanceManager instanceManager,
|
||||
IPortAllocator portAllocator,
|
||||
ILogger<DreamDaemonController> logger)
|
||||
IJobManager jobManager,
|
||||
IPortAllocator portAllocator)
|
||||
: base(
|
||||
instanceManager,
|
||||
databaseContext,
|
||||
authenticationContextFactory,
|
||||
logger)
|
||||
logger,
|
||||
instanceManager)
|
||||
{
|
||||
this.jobManager = jobManager ?? throw new ArgumentNullException(nameof(jobManager));
|
||||
this.portAllocator = portAllocator ?? throw new ArgumentNullException(nameof(portAllocator));
|
||||
|
||||
@@ -13,11 +13,11 @@ using Tgstation.Server.Api.Models.Request;
|
||||
using Tgstation.Server.Api.Models.Response;
|
||||
using Tgstation.Server.Api.Rights;
|
||||
using Tgstation.Server.Host.Components;
|
||||
using Tgstation.Server.Host.Core;
|
||||
using Tgstation.Server.Host.Database;
|
||||
using Tgstation.Server.Host.Jobs;
|
||||
using Tgstation.Server.Host.Models;
|
||||
using Tgstation.Server.Host.Security;
|
||||
using Tgstation.Server.Host.Utils;
|
||||
|
||||
namespace Tgstation.Server.Host.Controllers
|
||||
{
|
||||
@@ -41,24 +41,24 @@ namespace Tgstation.Server.Host.Controllers
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="DreamMakerController"/> class.
|
||||
/// </summary>
|
||||
/// <param name="databaseContext">The <see cref="IDatabaseContext"/> for the <see cref="ApiController"/>.</param>
|
||||
/// <param name="authenticationContextFactory">The <see cref="IAuthenticationContextFactory"/> for the <see cref="ApiController"/>.</param>
|
||||
/// <param name="jobManager">The value of <see cref="jobManager"/>.</param>
|
||||
/// <param name="databaseContext">The <see cref="IDatabaseContext"/> for the <see cref="InstanceRequiredController"/>.</param>
|
||||
/// <param name="authenticationContextFactory">The <see cref="IAuthenticationContextFactory"/> for the <see cref="InstanceRequiredController"/>.</param>
|
||||
/// <param name="logger">The <see cref="ILogger"/> for the <see cref="InstanceRequiredController"/>.</param>
|
||||
/// <param name="instanceManager">The <see cref="IInstanceManager"/> for the <see cref="InstanceRequiredController"/>.</param>
|
||||
/// <param name="jobManager">The value of <see cref="jobManager"/>.</param>
|
||||
/// <param name="portAllocator">The value of <see cref="IPortAllocator"/>.</param>
|
||||
/// <param name="logger">The <see cref="ILogger"/> for the <see cref="ApiController"/>.</param>
|
||||
public DreamMakerController(
|
||||
IDatabaseContext databaseContext,
|
||||
IAuthenticationContextFactory authenticationContextFactory,
|
||||
IJobManager jobManager,
|
||||
ILogger<DreamMakerController> logger,
|
||||
IInstanceManager instanceManager,
|
||||
IPortAllocator portAllocator,
|
||||
ILogger<DreamMakerController> logger)
|
||||
IJobManager jobManager,
|
||||
IPortAllocator portAllocator)
|
||||
: base(
|
||||
instanceManager,
|
||||
databaseContext,
|
||||
authenticationContextFactory,
|
||||
logger)
|
||||
logger,
|
||||
instanceManager)
|
||||
{
|
||||
this.jobManager = jobManager ?? throw new ArgumentNullException(nameof(jobManager));
|
||||
this.portAllocator = portAllocator ?? throw new ArgumentNullException(nameof(portAllocator));
|
||||
|
||||
@@ -19,13 +19,13 @@ using Tgstation.Server.Api.Models.Response;
|
||||
using Tgstation.Server.Api.Rights;
|
||||
using Tgstation.Server.Host.Components;
|
||||
using Tgstation.Server.Host.Configuration;
|
||||
using Tgstation.Server.Host.Core;
|
||||
using Tgstation.Server.Host.Database;
|
||||
using Tgstation.Server.Host.IO;
|
||||
using Tgstation.Server.Host.Jobs;
|
||||
using Tgstation.Server.Host.Models;
|
||||
using Tgstation.Server.Host.Security;
|
||||
using Tgstation.Server.Host.System;
|
||||
using Tgstation.Server.Host.Utils;
|
||||
|
||||
namespace Tgstation.Server.Host.Controllers
|
||||
{
|
||||
@@ -34,7 +34,7 @@ namespace Tgstation.Server.Host.Controllers
|
||||
/// </summary>
|
||||
[Route(Routes.InstanceManager)]
|
||||
#pragma warning disable CA1506 // TODO: Decomplexify
|
||||
public sealed class InstanceController : ApiController
|
||||
public sealed class InstanceController : ComponentInterfacingController
|
||||
{
|
||||
/// <summary>
|
||||
/// File name to allow attaching instances.
|
||||
@@ -51,11 +51,6 @@ namespace Tgstation.Server.Host.Controllers
|
||||
/// </summary>
|
||||
readonly IJobManager jobManager;
|
||||
|
||||
/// <summary>
|
||||
/// The <see cref="IInstanceManager"/> for the <see cref="InstanceController"/>.
|
||||
/// </summary>
|
||||
readonly IInstanceManager instanceManager;
|
||||
|
||||
/// <summary>
|
||||
/// The <see cref="IIOManager"/> for the <see cref="InstanceController"/>.
|
||||
/// </summary>
|
||||
@@ -84,35 +79,34 @@ namespace Tgstation.Server.Host.Controllers
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="InstanceController"/> class.
|
||||
/// </summary>
|
||||
/// <param name="databaseContext">The <see cref="IDatabaseContext"/> for the <see cref="ApiController"/>.</param>
|
||||
/// <param name="authenticationContextFactory">The <see cref="IAuthenticationContextFactory"/> for the <see cref="ApiController"/>.</param>
|
||||
/// <param name="databaseContext">The <see cref="IDatabaseContext"/> for the <see cref="ComponentInterfacingController"/>.</param>
|
||||
/// <param name="authenticationContextFactory">The <see cref="IAuthenticationContextFactory"/> for the <see cref="ComponentInterfacingController"/>.</param>
|
||||
/// <param name="logger">The <see cref="ILogger"/> for the <see cref="ComponentInterfacingController"/>.</param>
|
||||
/// <param name="instanceManager">The <see cref="IInstanceManager"/> for the <see cref="ComponentInterfacingController"/>.</param>
|
||||
/// <param name="jobManager">The value of <see cref="jobManager"/>.</param>
|
||||
/// <param name="instanceManager">The value of <see cref="instanceManager"/>.</param>
|
||||
/// <param name="ioManager">The value of <see cref="ioManager"/>.</param>
|
||||
/// <param name="platformIdentifier">The value of <see cref="platformIdentifier"/>.</param>
|
||||
/// <param name="portAllocator">The value of <see cref="IPortAllocator"/>.</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="logger">The <see cref="ILogger"/> for the <see cref="ApiController"/>.</param>
|
||||
public InstanceController(
|
||||
IDatabaseContext databaseContext,
|
||||
IAuthenticationContextFactory authenticationContextFactory,
|
||||
IJobManager jobManager,
|
||||
ILogger<InstanceController> logger,
|
||||
IInstanceManager instanceManager,
|
||||
IJobManager jobManager,
|
||||
IIOManager ioManager,
|
||||
IPortAllocator portAllocator,
|
||||
IPlatformIdentifier platformIdentifier,
|
||||
IOptions<GeneralConfiguration> generalConfigurationOptions,
|
||||
IOptions<SwarmConfiguration> swarmConfigurationOptions,
|
||||
ILogger<InstanceController> logger)
|
||||
IOptions<SwarmConfiguration> swarmConfigurationOptions)
|
||||
: base(
|
||||
databaseContext,
|
||||
authenticationContextFactory,
|
||||
logger,
|
||||
true)
|
||||
instanceManager)
|
||||
{
|
||||
this.jobManager = jobManager ?? throw new ArgumentNullException(nameof(jobManager));
|
||||
this.instanceManager = instanceManager ?? throw new ArgumentNullException(nameof(instanceManager));
|
||||
this.ioManager = ioManager ?? throw new ArgumentNullException(nameof(ioManager));
|
||||
this.platformIdentifier = platformIdentifier ?? throw new ArgumentNullException(nameof(platformIdentifier));
|
||||
this.portAllocator = portAllocator ?? throw new ArgumentNullException(nameof(portAllocator));
|
||||
@@ -367,7 +361,7 @@ namespace Tgstation.Server.Host.Controllers
|
||||
if (originalModel == default(Models.Instance))
|
||||
return Gone();
|
||||
|
||||
if (InstanceRequiredController.ValidateInstanceOnlineStatus(instanceManager, Logger, originalModel))
|
||||
if (ValidateInstanceOnlineStatus(originalModel))
|
||||
await DatabaseContext.Save(cancellationToken);
|
||||
|
||||
var userRights = (InstanceManagerRights)AuthenticationContext.GetRight(RightsType.InstanceManager);
|
||||
@@ -436,22 +430,25 @@ namespace Tgstation.Server.Host.Controllers
|
||||
|
||||
if (renamed)
|
||||
{
|
||||
using var componentInstance = instanceManager.GetInstanceReference(originalModel);
|
||||
if (componentInstance != null)
|
||||
// ignoring retval because we don't care if it's offline
|
||||
await WithComponentInstance(async componentInstance =>
|
||||
{
|
||||
await componentInstance.InstanceRenamed(originalModel.Name, cancellationToken);
|
||||
return null;
|
||||
});
|
||||
}
|
||||
|
||||
var oldAutoStart = originalModel.DreamDaemonSettings.AutoStart;
|
||||
try
|
||||
{
|
||||
if (originalOnline && model.Online == false)
|
||||
await instanceManager.OfflineInstance(originalModel, AuthenticationContext.User, cancellationToken);
|
||||
await InstanceOperations.OfflineInstance(originalModel, AuthenticationContext.User, cancellationToken);
|
||||
else if (!originalOnline && model.Online == true)
|
||||
{
|
||||
// force autostart false here because we don't want any long running jobs right now
|
||||
// remember to document this
|
||||
originalModel.DreamDaemonSettings.AutoStart = false;
|
||||
await instanceManager.OnlineInstance(originalModel, cancellationToken);
|
||||
await InstanceOperations.OnlineInstance(originalModel, cancellationToken);
|
||||
}
|
||||
}
|
||||
catch (Exception e)
|
||||
@@ -488,7 +485,7 @@ namespace Tgstation.Server.Host.Controllers
|
||||
await jobManager.RegisterOperation(
|
||||
job,
|
||||
(core, databaseContextFactory, paramJob, progressHandler, ct) // core will be null here since the instance is offline
|
||||
=> instanceManager.MoveInstance(originalModel, originalModelPath, ct),
|
||||
=> InstanceOperations.MoveInstance(originalModel, originalModelPath, ct),
|
||||
cancellationToken)
|
||||
;
|
||||
api.MoveJob = job.ToApi();
|
||||
@@ -496,9 +493,12 @@ namespace Tgstation.Server.Host.Controllers
|
||||
|
||||
if (model.AutoUpdateInterval.HasValue && oldAutoUpdateInterval != model.AutoUpdateInterval)
|
||||
{
|
||||
using var componentInstance = instanceManager.GetInstanceReference(originalModel);
|
||||
if (componentInstance != null)
|
||||
// ignoring retval because we don't care if it's offline
|
||||
await WithComponentInstance(async componentInstance =>
|
||||
{
|
||||
await componentInstance.SetAutoUpdateInterval(model.AutoUpdateInterval.Value);
|
||||
return null;
|
||||
});
|
||||
}
|
||||
|
||||
await CheckAccessible(api, cancellationToken);
|
||||
@@ -559,7 +559,7 @@ namespace Tgstation.Server.Host.Controllers
|
||||
.OrderBy(x => x.Id))),
|
||||
async instance =>
|
||||
{
|
||||
needsUpdate |= InstanceRequiredController.ValidateInstanceOnlineStatus(instanceManager, Logger, instance);
|
||||
needsUpdate |= ValidateInstanceOnlineStatus(instance);
|
||||
instance.MoveJob = moveJobs.FirstOrDefault(x => x.Instance.Id == instance.Id)?.ToApi();
|
||||
await CheckAccessible(instance, cancellationToken);
|
||||
},
|
||||
@@ -606,7 +606,7 @@ namespace Tgstation.Server.Host.Controllers
|
||||
if (instance == null)
|
||||
return Gone();
|
||||
|
||||
if (InstanceRequiredController.ValidateInstanceOnlineStatus(instanceManager, Logger, instance))
|
||||
if (ValidateInstanceOnlineStatus(instance))
|
||||
await DatabaseContext.Save(cancellationToken);
|
||||
|
||||
if (cantList && !instance.InstancePermissionSets.Any(instanceUser => instanceUser.PermissionSetId == AuthenticationContext.PermissionSet.Id.Value &&
|
||||
|
||||
@@ -30,20 +30,20 @@ namespace Tgstation.Server.Host.Controllers
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="InstancePermissionSetController"/> class.
|
||||
/// </summary>
|
||||
/// <param name="databaseContext">The <see cref="IDatabaseContext"/> for the <see cref="InstanceRequiredController"/>.</param>
|
||||
/// <param name="authenticationContextFactory">The <see cref="IAuthenticationContextFactory"/> for the <see cref="InstanceRequiredController"/>.</param>
|
||||
/// <param name="logger">The <see cref="ILogger"/> for the <see cref="InstanceRequiredController"/>.</param>
|
||||
/// <param name="instanceManager">The <see cref="IInstanceManager"/> for the <see cref="InstanceRequiredController"/>.</param>
|
||||
/// <param name="databaseContext">The <see cref="IDatabaseContext"/> for the <see cref="ApiController"/>.</param>
|
||||
/// <param name="authenticationContextFactory">The <see cref="IAuthenticationContextFactory"/> for the <see cref="ApiController"/>.</param>
|
||||
/// <param name="logger">The <see cref="ILogger"/> for the <see cref="ApiController"/>.</param>
|
||||
public InstancePermissionSetController(
|
||||
IInstanceManager instanceManager,
|
||||
IDatabaseContext databaseContext,
|
||||
IAuthenticationContextFactory authenticationContextFactory,
|
||||
ILogger<InstancePermissionSetController> logger)
|
||||
ILogger<InstancePermissionSetController> logger,
|
||||
IInstanceManager instanceManager)
|
||||
: base(
|
||||
instanceManager,
|
||||
databaseContext,
|
||||
authenticationContextFactory,
|
||||
logger)
|
||||
logger,
|
||||
instanceManager)
|
||||
{
|
||||
}
|
||||
|
||||
@@ -248,7 +248,7 @@ namespace Tgstation.Server.Host.Controllers
|
||||
.Where(x => x.PermissionSetId == id)
|
||||
.DeleteAsync(cancellationToken)
|
||||
;
|
||||
return numDeleted > 0 ? (IActionResult)NoContent() : Gone();
|
||||
return numDeleted > 0 ? NoContent() : Gone();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,13 +1,5 @@
|
||||
using System;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.Extensions.Logging;
|
||||
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Serilog.Context;
|
||||
|
||||
using Tgstation.Server.Api.Models;
|
||||
using Tgstation.Server.Api.Models.Response;
|
||||
using Tgstation.Server.Host.Components;
|
||||
using Tgstation.Server.Host.Database;
|
||||
using Tgstation.Server.Host.Security;
|
||||
@@ -15,105 +7,29 @@ using Tgstation.Server.Host.Security;
|
||||
namespace Tgstation.Server.Host.Controllers
|
||||
{
|
||||
/// <summary>
|
||||
/// <see langword="abstract"/> <see cref="ApiController"/> for operations on an <see cref="IInstance"/>.
|
||||
/// <see cref="ComponentInterfacingController"/> for operations that require an instance.
|
||||
/// </summary>
|
||||
public abstract class InstanceRequiredController : ApiController
|
||||
public abstract class InstanceRequiredController : ComponentInterfacingController
|
||||
{
|
||||
/// <summary>
|
||||
/// The <see cref="IInstanceManager"/> for the <see cref="InstanceRequiredController"/>.
|
||||
/// </summary>
|
||||
readonly IInstanceManager instanceManager;
|
||||
|
||||
/// <summary>
|
||||
/// Corrects discrepencies between the <see cref="Api.Models.Instance.Online"/> status of <see cref="IInstance"/>s in the database vs the service.
|
||||
/// </summary>
|
||||
/// <param name="instanceManager">The <see cref="IInstanceManager"/> to use.</param>
|
||||
/// <param name="logger">The <see cref="ILogger"/> to use.</param>
|
||||
/// <param name="metadata">The <see cref="Api.Models.Instance"/> to check.</param>
|
||||
/// <returns><see langword="true"/> if an unsaved DB update was made, <see langword="false"/> otherwise.</returns>
|
||||
public static bool ValidateInstanceOnlineStatus(IInstanceManager instanceManager, ILogger logger, Api.Models.Instance metadata)
|
||||
{
|
||||
if (instanceManager == null)
|
||||
throw new ArgumentNullException(nameof(instanceManager));
|
||||
if (metadata == null)
|
||||
throw new ArgumentNullException(nameof(metadata));
|
||||
|
||||
bool online;
|
||||
using (var instanceReferenceCheck = instanceManager.GetInstanceReference(metadata))
|
||||
online = instanceReferenceCheck != null;
|
||||
|
||||
if (metadata.Online.Value == online)
|
||||
return false;
|
||||
|
||||
const string OfflineWord = "offline";
|
||||
const string OnlineWord = "online";
|
||||
|
||||
logger.LogWarning(
|
||||
"Instance {0} is says it's {1} in the database, but it is actually {2} in the service. Updating the database to reflect this...",
|
||||
metadata.Id,
|
||||
online ? OfflineWord : OnlineWord,
|
||||
online ? OnlineWord : OfflineWord);
|
||||
|
||||
metadata.Online = online;
|
||||
return true;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="InstanceRequiredController"/> class.
|
||||
/// </summary>
|
||||
/// <param name="instanceManager">The value of <see cref="instanceManager"/>.</param>
|
||||
/// <param name="databaseContext">The <see cref="IDatabaseContext"/> for the <see cref="ApiController"/>.</param>
|
||||
/// <param name="authenticationContextFactory">The <see cref="IAuthenticationContextFactory"/> for the <see cref="ApiController"/>.</param>
|
||||
/// <param name="logger">The <see cref="ILogger"/> for the <see cref="ApiController"/>.</param>
|
||||
/// <param name="databaseContext">The <see cref="IDatabaseContext"/> for the <see cref="ComponentInterfacingController"/>.</param>
|
||||
/// <param name="authenticationContextFactory">The <see cref="IAuthenticationContextFactory"/> for the <see cref="ComponentInterfacingController"/>.</param>
|
||||
/// <param name="logger">The <see cref="ILogger"/> for the <see cref="ComponentInterfacingController"/>.</param>
|
||||
/// <param name="instanceManager">The <see cref="IInstanceManager"/> for the <see cref="ComponentInterfacingController"/>.</param>
|
||||
protected InstanceRequiredController(
|
||||
IInstanceManager instanceManager,
|
||||
IDatabaseContext databaseContext,
|
||||
IAuthenticationContextFactory authenticationContextFactory,
|
||||
ILogger<InstanceRequiredController> logger)
|
||||
ILogger<InstanceRequiredController> logger,
|
||||
IInstanceManager instanceManager)
|
||||
: base(
|
||||
databaseContext,
|
||||
authenticationContextFactory,
|
||||
logger,
|
||||
instanceManager,
|
||||
true)
|
||||
{
|
||||
this.instanceManager = instanceManager ?? throw new ArgumentNullException(nameof(instanceManager));
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override async Task<IActionResult> ValidateRequest(CancellationToken cancellationToken)
|
||||
{
|
||||
if (!ApiHeaders.InstanceId.HasValue)
|
||||
return BadRequest(new ErrorMessageResponse(ErrorCode.InstanceHeaderRequired));
|
||||
if (AuthenticationContext.InstancePermissionSet == null)
|
||||
return Forbid();
|
||||
|
||||
if (ValidateInstanceOnlineStatus(instanceManager, Logger, Instance))
|
||||
await DatabaseContext.Save(cancellationToken);
|
||||
|
||||
using var instanceReferenceCheck = instanceManager.GetInstanceReference(Instance);
|
||||
if (instanceReferenceCheck == null)
|
||||
return Conflict(new ErrorMessageResponse(ErrorCode.InstanceOffline));
|
||||
return null;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Run a given <paramref name="action"/> with the relevant <see cref="IInstance"/>.
|
||||
/// </summary>
|
||||
/// <param name="action">A <see cref="Func{T, TResult}"/> accepting the <see cref="IInstance"/> and returning a <see cref="Task{TResult}"/> with the <see cref="IActionResult"/>.</param>
|
||||
/// <returns>A <see cref="Task{TResult}"/> resulting in the <see cref="IActionResult"/> that should be returned.</returns>
|
||||
/// <remarks>The context of <paramref name="action"/> should be as small as possible so as to avoid race conditions.</remarks>
|
||||
protected async Task<IActionResult> WithComponentInstance(Func<IInstanceCore, Task<IActionResult>> action)
|
||||
{
|
||||
if (action == null)
|
||||
throw new ArgumentNullException(nameof(action));
|
||||
|
||||
using var instanceReference = instanceManager.GetInstanceReference(Instance);
|
||||
using (LogContext.PushProperty("InstanceReference", instanceReference.Uid))
|
||||
{
|
||||
if (instanceReference == null)
|
||||
return Conflict(new ErrorMessageResponse(ErrorCode.InstanceOffline));
|
||||
return await action(instanceReference);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -32,22 +32,22 @@ namespace Tgstation.Server.Host.Controllers
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="JobController"/> class.
|
||||
/// </summary>
|
||||
/// <param name="databaseContext">The <see cref="IDatabaseContext"/> for the <see cref="InstanceRequiredController"/>.</param>
|
||||
/// <param name="authenticationContextFactory">The <see cref="IAuthenticationContextFactory"/> for the <see cref="InstanceRequiredController"/>.</param>
|
||||
/// <param name="logger">The <see cref="ILogger"/> for the <see cref="InstanceRequiredController"/>.</param>
|
||||
/// <param name="instanceManager">The <see cref="IInstanceManager"/> for the <see cref="InstanceRequiredController"/>.</param>
|
||||
/// <param name="databaseContext">The <see cref="IDatabaseContext"/> for the <see cref="ApiController"/>.</param>
|
||||
/// <param name="authenticationContextFactory">The <see cref="IAuthenticationContextFactory"/> for the <see cref="ApiController"/>.</param>
|
||||
/// <param name="jobManager">The value of <see cref="jobManager"/>.</param>
|
||||
/// <param name="logger">The <see cref="ILogger"/> for the <see cref="ApiController"/>.</param>
|
||||
public JobController(
|
||||
IInstanceManager instanceManager,
|
||||
IDatabaseContext databaseContext,
|
||||
IAuthenticationContextFactory authenticationContextFactory,
|
||||
IJobManager jobManager,
|
||||
ILogger<JobController> logger)
|
||||
ILogger<JobController> logger,
|
||||
IInstanceManager instanceManager,
|
||||
IJobManager jobManager)
|
||||
: base(
|
||||
instanceManager,
|
||||
databaseContext,
|
||||
authenticationContextFactory,
|
||||
logger)
|
||||
logger,
|
||||
instanceManager)
|
||||
{
|
||||
this.jobManager = jobManager ?? throw new ArgumentNullException(nameof(jobManager));
|
||||
}
|
||||
|
||||
@@ -44,24 +44,24 @@ namespace Tgstation.Server.Host.Controllers
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="RepositoryController"/> class.
|
||||
/// </summary>
|
||||
/// <param name="databaseContext">The <see cref="IDatabaseContext"/> for the <see cref="ApiController"/>.</param>
|
||||
/// <param name="authenticationContextFactory">The <see cref="IAuthenticationContextFactory"/> for the <see cref="ApiController"/>.</param>
|
||||
/// <param name="databaseContext">The <see cref="IDatabaseContext"/> for the <see cref="InstanceRequiredController"/>.</param>
|
||||
/// <param name="authenticationContextFactory">The <see cref="IAuthenticationContextFactory"/> for the <see cref="InstanceRequiredController"/>.</param>
|
||||
/// <param name="logger">The <see cref="ILogger"/> for the <see cref="InstanceRequiredController"/>.</param>
|
||||
/// <param name="instanceManager">The <see cref="IInstanceManager"/> for the <see cref="InstanceRequiredController"/>.</param>
|
||||
/// <param name="loggerFactory">The value of <see cref="loggerFactory"/>.</param>
|
||||
/// <param name="jobManager">The value of <see cref="jobManager"/>.</param>
|
||||
/// <param name="logger">The <see cref="ILogger"/> for the <see cref="ApiController"/>.</param>
|
||||
public RepositoryController(
|
||||
IDatabaseContext databaseContext,
|
||||
IAuthenticationContextFactory authenticationContextFactory,
|
||||
ILogger<RepositoryController> logger,
|
||||
IInstanceManager instanceManager,
|
||||
ILoggerFactory loggerFactory,
|
||||
IJobManager jobManager,
|
||||
ILogger<RepositoryController> logger)
|
||||
IJobManager jobManager)
|
||||
: base(
|
||||
instanceManager,
|
||||
databaseContext,
|
||||
authenticationContextFactory,
|
||||
logger)
|
||||
logger,
|
||||
instanceManager)
|
||||
{
|
||||
this.loggerFactory = loggerFactory ?? throw new ArgumentNullException(nameof(loggerFactory));
|
||||
this.jobManager = jobManager ?? throw new ArgumentNullException(nameof(jobManager));
|
||||
@@ -166,9 +166,8 @@ namespace Tgstation.Server.Host.Controllers
|
||||
progressReporter,
|
||||
currentModel.UpdateSubmodules.Value,
|
||||
ct)
|
||||
;
|
||||
if (repos == null)
|
||||
throw new JobException(ErrorCode.RepoExists);
|
||||
?? throw new JobException(ErrorCode.RepoExists);
|
||||
|
||||
var instance = new Models.Instance
|
||||
{
|
||||
Id = Instance.Id,
|
||||
@@ -222,7 +221,7 @@ namespace Tgstation.Server.Host.Controllers
|
||||
|
||||
await DatabaseContext.Save(cancellationToken);
|
||||
|
||||
Logger.LogInformation("Instance {0} repository delete initiated by user {1}", Instance.Id, AuthenticationContext.User.Id.Value);
|
||||
Logger.LogInformation("Instance {instanceId} repository delete initiated by user {userId}", Instance.Id, AuthenticationContext.User.Id.Value);
|
||||
|
||||
var job = new Job
|
||||
{
|
||||
@@ -453,7 +452,7 @@ namespace Tgstation.Server.Host.Controllers
|
||||
? String.Format(
|
||||
CultureInfo.InvariantCulture,
|
||||
" at {0}",
|
||||
x.TargetCommitSha.Substring(0, 7))
|
||||
x.TargetCommitSha[..7])
|
||||
: String.Empty))),
|
||||
description != null
|
||||
? String.Empty
|
||||
|
||||
@@ -15,6 +15,7 @@ using Serilog.Context;
|
||||
using Tgstation.Server.Host.Configuration;
|
||||
using Tgstation.Server.Host.Swarm;
|
||||
using Tgstation.Server.Host.System;
|
||||
using Tgstation.Server.Host.Utils;
|
||||
|
||||
namespace Tgstation.Server.Host.Controllers
|
||||
{
|
||||
@@ -194,7 +195,7 @@ namespace Tgstation.Server.Host.Controllers
|
||||
/// <inheritdoc />
|
||||
public override async Task OnActionExecutionAsync(ActionExecutingContext context, ActionExecutionDelegate next)
|
||||
{
|
||||
using (LogContext.PushProperty("Request", $"{Request.Method} {Request.Path}"))
|
||||
using (LogContext.PushProperty(SerilogContextHelper.RequestPathContextProperty, $"{Request.Method} {Request.Path}"))
|
||||
{
|
||||
logger.LogTrace("Swarm request from {remoteIP}...", Request.HttpContext.Connection.RemoteIpAddress);
|
||||
if (swarmConfiguration.PrivateKey == null)
|
||||
|
||||
@@ -5,6 +5,7 @@ using System.IdentityModel.Tokens.Jwt;
|
||||
using System.Linq;
|
||||
|
||||
using Cyberboss.AspNetCore.AsyncInitializer;
|
||||
|
||||
using Microsoft.AspNetCore.Authentication.JwtBearer;
|
||||
using Microsoft.AspNetCore.Builder;
|
||||
using Microsoft.AspNetCore.Cors.Infrastructure;
|
||||
@@ -17,12 +18,15 @@ using Microsoft.Extensions.Hosting;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Microsoft.Extensions.Options;
|
||||
using Microsoft.Extensions.Primitives;
|
||||
|
||||
using Newtonsoft.Json;
|
||||
|
||||
using Serilog;
|
||||
using Serilog.Events;
|
||||
using Serilog.Formatting.Display;
|
||||
|
||||
using Tgstation.Server.Api;
|
||||
using Tgstation.Server.Common;
|
||||
using Tgstation.Server.Host.Components;
|
||||
using Tgstation.Server.Host.Components.Byond;
|
||||
using Tgstation.Server.Host.Components.Chat;
|
||||
@@ -47,6 +51,7 @@ using Tgstation.Server.Host.Setup;
|
||||
using Tgstation.Server.Host.Swarm;
|
||||
using Tgstation.Server.Host.System;
|
||||
using Tgstation.Server.Host.Transfer;
|
||||
using Tgstation.Server.Host.Utils;
|
||||
|
||||
namespace Tgstation.Server.Host.Core
|
||||
{
|
||||
@@ -175,7 +180,7 @@ namespace Tgstation.Server.Host.Core
|
||||
|
||||
var formatter = new MessageTemplateTextFormatter(
|
||||
"{Timestamp:o} "
|
||||
+ ServiceCollectionExtensions.SerilogContextTemplate
|
||||
+ SerilogContextHelper.Template
|
||||
+ "): [{Level:u3}] {SourceContext:l}: {Message} ({EventId:x8}){NewLine}{Exception}",
|
||||
null);
|
||||
|
||||
@@ -382,21 +387,21 @@ namespace Tgstation.Server.Host.Core
|
||||
/// <param name="applicationBuilder">The <see cref="IApplicationBuilder"/> to configure.</param>
|
||||
/// <param name="serverControl">The <see cref="IServerControl"/> for the <see cref="Application"/>.</param>
|
||||
/// <param name="tokenFactory">The value of <see cref="tokenFactory"/>.</param>
|
||||
/// <param name="instanceManager">The <see cref="IInstanceManager"/>.</param>
|
||||
/// <param name="serverPortProvider">The <see cref="IServerPortProvider"/>.</param>
|
||||
/// <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="swarmConfigurationOptions">The <see cref="IOptions{TOptions}"/> containing the <see cref="SwarmConfiguration"/> to use.</param>
|
||||
/// <param name="logger">The <see cref="Microsoft.Extensions.Logging.ILogger"/> for the <see cref="Application"/>.</param>
|
||||
public void Configure(
|
||||
IApplicationBuilder applicationBuilder,
|
||||
IServerControl serverControl,
|
||||
ITokenFactory tokenFactory,
|
||||
IInstanceManager instanceManager,
|
||||
IServerPortProvider serverPortProvider,
|
||||
IAssemblyInformationProvider assemblyInformationProvider,
|
||||
IOptions<ControlPanelConfiguration> controlPanelConfigurationOptions,
|
||||
IOptions<GeneralConfiguration> generalConfigurationOptions,
|
||||
IOptions<SwarmConfiguration> swarmConfigurationOptions,
|
||||
ILogger<Application> logger)
|
||||
{
|
||||
if (applicationBuilder == null)
|
||||
@@ -406,8 +411,6 @@ namespace Tgstation.Server.Host.Core
|
||||
|
||||
this.tokenFactory = tokenFactory ?? throw new ArgumentNullException(nameof(tokenFactory));
|
||||
|
||||
if (instanceManager == null)
|
||||
throw new ArgumentNullException(nameof(instanceManager));
|
||||
if (serverPortProvider == null)
|
||||
throw new ArgumentNullException(nameof(serverPortProvider));
|
||||
if (assemblyInformationProvider == null)
|
||||
@@ -415,6 +418,7 @@ namespace Tgstation.Server.Host.Core
|
||||
|
||||
var controlPanelConfiguration = controlPanelConfigurationOptions?.Value ?? throw new ArgumentNullException(nameof(controlPanelConfigurationOptions));
|
||||
var generalConfiguration = generalConfigurationOptions?.Value ?? throw new ArgumentNullException(nameof(generalConfigurationOptions));
|
||||
var swarmConfiguration = swarmConfigurationOptions?.Value ?? throw new ArgumentNullException(nameof(swarmConfigurationOptions));
|
||||
|
||||
if (logger == null)
|
||||
throw new ArgumentNullException(nameof(logger));
|
||||
@@ -437,15 +441,13 @@ namespace Tgstation.Server.Host.Core
|
||||
// Add the X-Powered-By response header
|
||||
applicationBuilder.UseServerBranding(assemblyInformationProvider);
|
||||
|
||||
// 503 requests made while the application is starting
|
||||
applicationBuilder.UseAsyncInitialization(async (cancellationToken) =>
|
||||
{
|
||||
await instanceManager.Ready.WithToken(cancellationToken);
|
||||
});
|
||||
|
||||
// suppress OperationCancelledExceptions, they are just aborted HTTP requests
|
||||
applicationBuilder.UseCancelledRequestSuppression();
|
||||
|
||||
// 503 requests made while the application is starting
|
||||
applicationBuilder.UseAsyncInitialization<IInstanceManager>(
|
||||
(instanceManager, cancellationToken) => instanceManager.Ready.WithToken(cancellationToken));
|
||||
|
||||
if (generalConfiguration.HostApiDocumentation)
|
||||
{
|
||||
applicationBuilder.UseSwagger();
|
||||
|
||||
@@ -14,6 +14,7 @@ using Tgstation.Server.Host.Configuration;
|
||||
using Tgstation.Server.Host.Extensions;
|
||||
using Tgstation.Server.Host.IO;
|
||||
using Tgstation.Server.Host.Swarm;
|
||||
using Tgstation.Server.Host.Utils;
|
||||
|
||||
namespace Tgstation.Server.Host.Core
|
||||
{
|
||||
|
||||
@@ -11,9 +11,13 @@ using Microsoft.Extensions.Logging;
|
||||
using Microsoft.Extensions.Primitives;
|
||||
using Microsoft.Net.Http.Headers;
|
||||
|
||||
using Serilog.Context;
|
||||
|
||||
using Tgstation.Server.Api.Models;
|
||||
using Tgstation.Server.Api.Models.Response;
|
||||
using Tgstation.Server.Host.Configuration;
|
||||
using Tgstation.Server.Host.System;
|
||||
using Tgstation.Server.Host.Utils;
|
||||
|
||||
namespace Tgstation.Server.Host.Extensions
|
||||
{
|
||||
@@ -22,6 +26,11 @@ namespace Tgstation.Server.Host.Extensions
|
||||
/// </summary>
|
||||
static class ApplicationBuilderExtensions
|
||||
{
|
||||
/// <summary>
|
||||
/// If the server's swarm identifier should be pushed onto the log context for all requests.
|
||||
/// </summary>
|
||||
internal static bool LogSwarmIdentifier { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Return a <see cref="ConflictObjectResult"/> for <see cref="DbUpdateException"/>s.
|
||||
/// </summary>
|
||||
@@ -103,6 +112,7 @@ namespace Tgstation.Server.Host.Extensions
|
||||
{
|
||||
if (applicationBuilder == null)
|
||||
throw new ArgumentNullException(nameof(applicationBuilder));
|
||||
|
||||
applicationBuilder.Use(async (context, next) =>
|
||||
{
|
||||
var logger = GetLogger(context);
|
||||
@@ -124,8 +134,7 @@ namespace Tgstation.Server.Host.Extensions
|
||||
.ExecuteResultAsync(new ActionContext
|
||||
{
|
||||
HttpContext = context,
|
||||
})
|
||||
;
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
@@ -149,6 +158,26 @@ namespace Tgstation.Server.Host.Extensions
|
||||
});
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Adds additional global <see cref="LogContext"/> to the request pipeline.
|
||||
/// </summary>
|
||||
/// <param name="applicationBuilder">The <see cref="IApplicationBuilder"/> to configure.</param>
|
||||
/// <param name="swarmConfiguration">The <see cref="SwarmConfiguration"/>.</param>
|
||||
public static void UseAdditionalRequestLoggingContext(this IApplicationBuilder applicationBuilder, SwarmConfiguration swarmConfiguration)
|
||||
{
|
||||
if (applicationBuilder == null)
|
||||
throw new ArgumentNullException(nameof(applicationBuilder));
|
||||
if (swarmConfiguration == null)
|
||||
throw new ArgumentNullException(nameof(swarmConfiguration));
|
||||
|
||||
if (LogSwarmIdentifier && swarmConfiguration.Identifier != null)
|
||||
applicationBuilder.Use(async (context, next) =>
|
||||
{
|
||||
using (LogContext.PushProperty(SerilogContextHelper.SwarmIdentifierContextProperty, swarmConfiguration.Identifier))
|
||||
await next();
|
||||
});
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets a <see cref="ILogger"/> from a given <paramref name="httpContext"/>.
|
||||
/// </summary>
|
||||
|
||||
@@ -12,6 +12,7 @@ using Serilog.Configuration;
|
||||
using Serilog.Sinks.Elasticsearch;
|
||||
|
||||
using Tgstation.Server.Host.Configuration;
|
||||
using Tgstation.Server.Host.Utils;
|
||||
|
||||
namespace Tgstation.Server.Host.Extensions
|
||||
{
|
||||
@@ -20,20 +21,6 @@ namespace Tgstation.Server.Host.Extensions
|
||||
/// </summary>
|
||||
static class ServiceCollectionExtensions
|
||||
{
|
||||
/// <summary>
|
||||
/// Common template used for adding our custom log context to serilog.
|
||||
/// </summary>
|
||||
/// <remarks>Should not be changed. Only mutable for the sake of identifying swarm nodes under a single test environment</remarks>
|
||||
public static string SerilogContextTemplate { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Initializes static members of the <see cref="ServiceCollectionExtensions"/> class.
|
||||
/// </summary>
|
||||
static ServiceCollectionExtensions()
|
||||
{
|
||||
SerilogContextTemplate = "(Instance:{Instance}|Job:{Job}|Request:{Request}|User:{User}|Monitor:{Monitor}|Bridge:{Bridge}|Chat:{ChatMessage}";
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Add a standard <typeparamref name="TConfig"/> binding.
|
||||
/// </summary>
|
||||
@@ -90,9 +77,9 @@ namespace Tgstation.Server.Host.Extensions
|
||||
.WriteTo
|
||||
.Async(sinkConfiguration =>
|
||||
{
|
||||
var template = "[{Timestamp:HH:mm:ss}] {Level:w3}: {SourceContext:l} "
|
||||
+ SerilogContextTemplate
|
||||
+ "|IR:{InstanceReference}){NewLine} {Message:lj}{NewLine}{Exception}";
|
||||
var template = "[{Timestamp:HH:mm:ss}] {Level:w3}: {SourceContext:l} ("
|
||||
+ SerilogContextHelper.Template
|
||||
+ "){NewLine} {Message:lj}{NewLine}{Exception}";
|
||||
sinkConfiguration.Console(outputTemplate: template, formatProvider: CultureInfo.InvariantCulture);
|
||||
sinkConfigurationAction?.Invoke(sinkConfiguration);
|
||||
});
|
||||
|
||||
@@ -6,7 +6,6 @@ using Microsoft.Extensions.DependencyInjection;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Microsoft.Extensions.Options;
|
||||
|
||||
using Tgstation.Server.Host.Components;
|
||||
using Tgstation.Server.Host.Configuration;
|
||||
using Tgstation.Server.Host.Core;
|
||||
using Tgstation.Server.Host.IO;
|
||||
@@ -66,11 +65,11 @@ namespace Tgstation.Server.Host.Extensions
|
||||
applicationBuilder,
|
||||
applicationBuilder.ApplicationServices.GetRequiredService<IServerControl>(),
|
||||
applicationBuilder.ApplicationServices.GetRequiredService<ITokenFactory>(),
|
||||
applicationBuilder.ApplicationServices.GetRequiredService<IInstanceManager>(),
|
||||
applicationBuilder.ApplicationServices.GetRequiredService<IServerPortProvider>(),
|
||||
applicationBuilder.ApplicationServices.GetRequiredService<IAssemblyInformationProvider>(),
|
||||
applicationBuilder.ApplicationServices.GetRequiredService<IOptions<ControlPanelConfiguration>>(),
|
||||
applicationBuilder.ApplicationServices.GetRequiredService<IOptions<GeneralConfiguration>>(),
|
||||
applicationBuilder.ApplicationServices.GetRequiredService<IOptions<SwarmConfiguration>>(),
|
||||
applicationBuilder.ApplicationServices.GetRequiredService<ILogger<Application>>());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,7 +6,7 @@ using System.Linq;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
using Tgstation.Server.Host.Core;
|
||||
using Tgstation.Server.Host.Utils;
|
||||
|
||||
namespace Tgstation.Server.Host.IO
|
||||
{
|
||||
|
||||
@@ -6,7 +6,7 @@ using System.Threading.Tasks;
|
||||
|
||||
using Microsoft.Extensions.Logging;
|
||||
|
||||
using Tgstation.Server.Host.Core;
|
||||
using Tgstation.Server.Common;
|
||||
|
||||
namespace Tgstation.Server.Host.IO
|
||||
{
|
||||
|
||||
@@ -12,6 +12,7 @@ using Tgstation.Server.Host.Components;
|
||||
using Tgstation.Server.Host.Database;
|
||||
using Tgstation.Server.Host.Extensions;
|
||||
using Tgstation.Server.Host.Models;
|
||||
using Tgstation.Server.Host.Utils;
|
||||
|
||||
namespace Tgstation.Server.Host.Jobs
|
||||
{
|
||||
@@ -290,7 +291,7 @@ namespace Tgstation.Server.Host.Jobs
|
||||
/// <returns>A <see cref="Task"/> representing the running operation.</returns>
|
||||
async Task RunJob(Job job, JobEntrypoint operation, CancellationToken cancellationToken)
|
||||
{
|
||||
using (LogContext.PushProperty("Job", job.Id))
|
||||
using (LogContext.PushProperty(SerilogContextHelper.JobIdContextProperty, job.Id))
|
||||
try
|
||||
{
|
||||
void LogException(Exception ex) => logger.LogDebug(ex, "Job {jobId} exited with error!", job.Id);
|
||||
@@ -328,8 +329,7 @@ namespace Tgstation.Server.Host.Jobs
|
||||
loggerFactory.CreateLogger<JobProgressReporter>(),
|
||||
null,
|
||||
UpdateProgress),
|
||||
cancellationToken)
|
||||
;
|
||||
cancellationToken);
|
||||
|
||||
logger.LogDebug("Job {jobId} completed!", job.Id);
|
||||
}
|
||||
|
||||
@@ -14,7 +14,18 @@ namespace Tgstation.Server.Host.Jobs
|
||||
/// <summary>
|
||||
/// The name of the current stage.
|
||||
/// </summary>
|
||||
public string StageName { get; set; }
|
||||
public string StageName
|
||||
{
|
||||
get => stageName;
|
||||
set
|
||||
{
|
||||
if (stageName == value)
|
||||
return;
|
||||
|
||||
stageName = value;
|
||||
callback(stageName, lastProgress);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// The <see cref="ILogger{TCategoryName}"/> for the <see cref="JobProgressReporter"/>.
|
||||
@@ -26,6 +37,16 @@ namespace Tgstation.Server.Host.Jobs
|
||||
/// </summary>
|
||||
readonly Action<string, double?> callback;
|
||||
|
||||
/// <summary>
|
||||
/// Backing field for <see cref="StageName"/>.
|
||||
/// </summary>
|
||||
string stageName;
|
||||
|
||||
/// <summary>
|
||||
/// The last progress value pushed into the <see cref="callback"/>.
|
||||
/// </summary>
|
||||
double? lastProgress;
|
||||
|
||||
/// <summary>
|
||||
/// The total progress reported so far in this section.
|
||||
/// </summary>
|
||||
@@ -66,6 +87,7 @@ namespace Tgstation.Server.Host.Jobs
|
||||
sectionProgression = progress.Value;
|
||||
|
||||
callback(StageName, clampedProgress);
|
||||
lastProgress = clampedProgress;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
||||
@@ -27,7 +27,7 @@ namespace Tgstation.Server.Host.Models
|
||||
public Instance Instance { get; set; }
|
||||
|
||||
/// <inheritdoc />
|
||||
public JobResponse ToApi() => new JobResponse
|
||||
public JobResponse ToApi() => new ()
|
||||
{
|
||||
Id = Id,
|
||||
StartedAt = StartedAt,
|
||||
|
||||
@@ -4,8 +4,8 @@ using System.Linq;
|
||||
|
||||
using Microsoft.Extensions.Logging;
|
||||
|
||||
using Tgstation.Server.Host.Core;
|
||||
using Tgstation.Server.Host.Models;
|
||||
using Tgstation.Server.Host.Utils;
|
||||
|
||||
namespace Tgstation.Server.Host.Security
|
||||
{
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
using Tgstation.Server.Host.Core;
|
||||
using Tgstation.Server.Host.Utils;
|
||||
|
||||
namespace Tgstation.Server.Host.Security
|
||||
{
|
||||
|
||||
@@ -3,8 +3,8 @@
|
||||
using Microsoft.Extensions.Logging;
|
||||
|
||||
using Tgstation.Server.Api.Models;
|
||||
using Tgstation.Server.Common;
|
||||
using Tgstation.Server.Host.Configuration;
|
||||
using Tgstation.Server.Host.Core;
|
||||
|
||||
namespace Tgstation.Server.Host.Security.OAuth
|
||||
{
|
||||
|
||||
@@ -15,7 +15,6 @@ using Tgstation.Server.Api;
|
||||
using Tgstation.Server.Api.Models;
|
||||
using Tgstation.Server.Common;
|
||||
using Tgstation.Server.Host.Configuration;
|
||||
using Tgstation.Server.Host.Core;
|
||||
|
||||
namespace Tgstation.Server.Host.Security.OAuth
|
||||
{
|
||||
|
||||
@@ -8,7 +8,7 @@ using Octokit;
|
||||
|
||||
using Tgstation.Server.Api.Models;
|
||||
using Tgstation.Server.Host.Configuration;
|
||||
using Tgstation.Server.Host.Core;
|
||||
using Tgstation.Server.Host.Utils;
|
||||
|
||||
namespace Tgstation.Server.Host.Security.OAuth
|
||||
{
|
||||
|
||||
@@ -3,8 +3,8 @@
|
||||
using Microsoft.Extensions.Logging;
|
||||
|
||||
using Tgstation.Server.Api.Models;
|
||||
using Tgstation.Server.Common;
|
||||
using Tgstation.Server.Host.Configuration;
|
||||
using Tgstation.Server.Host.Core;
|
||||
|
||||
namespace Tgstation.Server.Host.Security.OAuth
|
||||
{
|
||||
|
||||
@@ -3,8 +3,8 @@
|
||||
using Microsoft.Extensions.Logging;
|
||||
|
||||
using Tgstation.Server.Api.Models;
|
||||
using Tgstation.Server.Common;
|
||||
using Tgstation.Server.Host.Configuration;
|
||||
using Tgstation.Server.Host.Core;
|
||||
|
||||
namespace Tgstation.Server.Host.Security.OAuth
|
||||
{
|
||||
|
||||
@@ -8,8 +8,9 @@ using Microsoft.Extensions.Logging;
|
||||
using Microsoft.Extensions.Options;
|
||||
|
||||
using Tgstation.Server.Api.Models;
|
||||
using Tgstation.Server.Common;
|
||||
using Tgstation.Server.Host.Configuration;
|
||||
using Tgstation.Server.Host.Core;
|
||||
using Tgstation.Server.Host.Utils;
|
||||
|
||||
namespace Tgstation.Server.Host.Security.OAuth
|
||||
{
|
||||
|
||||
@@ -3,8 +3,8 @@
|
||||
using Microsoft.Extensions.Logging;
|
||||
|
||||
using Tgstation.Server.Api.Models;
|
||||
using Tgstation.Server.Common;
|
||||
using Tgstation.Server.Host.Configuration;
|
||||
using Tgstation.Server.Host.Core;
|
||||
|
||||
namespace Tgstation.Server.Host.Security.OAuth
|
||||
{
|
||||
|
||||
@@ -10,8 +10,8 @@ using Microsoft.IdentityModel.Tokens;
|
||||
|
||||
using Tgstation.Server.Api.Models.Response;
|
||||
using Tgstation.Server.Host.Configuration;
|
||||
using Tgstation.Server.Host.Core;
|
||||
using Tgstation.Server.Host.System;
|
||||
using Tgstation.Server.Host.Utils;
|
||||
|
||||
namespace Tgstation.Server.Host.Security
|
||||
{
|
||||
|
||||
@@ -6,11 +6,11 @@ using Microsoft.Extensions.Hosting;
|
||||
using Serilog.Events;
|
||||
|
||||
using Tgstation.Server.Host.Configuration;
|
||||
using Tgstation.Server.Host.Core;
|
||||
using Tgstation.Server.Host.Database;
|
||||
using Tgstation.Server.Host.Extensions;
|
||||
using Tgstation.Server.Host.IO;
|
||||
using Tgstation.Server.Host.System;
|
||||
using Tgstation.Server.Host.Utils;
|
||||
|
||||
namespace Tgstation.Server.Host.Setup
|
||||
{
|
||||
|
||||
@@ -21,12 +21,11 @@ using MySqlConnector;
|
||||
using Npgsql;
|
||||
|
||||
using Tgstation.Server.Host.Configuration;
|
||||
using Tgstation.Server.Host.Core;
|
||||
using Tgstation.Server.Host.Database;
|
||||
using Tgstation.Server.Host.Extensions.Converters;
|
||||
using Tgstation.Server.Host.IO;
|
||||
using Tgstation.Server.Host.System;
|
||||
|
||||
using Tgstation.Server.Host.Utils;
|
||||
using YamlDotNet.Serialization;
|
||||
|
||||
namespace Tgstation.Server.Host.Setup
|
||||
|
||||
@@ -15,12 +15,14 @@ using Newtonsoft.Json;
|
||||
using Newtonsoft.Json.Serialization;
|
||||
|
||||
using Tgstation.Server.Api.Models.Response;
|
||||
using Tgstation.Server.Common;
|
||||
using Tgstation.Server.Host.Configuration;
|
||||
using Tgstation.Server.Host.Core;
|
||||
using Tgstation.Server.Host.Database;
|
||||
using Tgstation.Server.Host.Extensions;
|
||||
using Tgstation.Server.Host.Extensions.Converters;
|
||||
using Tgstation.Server.Host.System;
|
||||
using Tgstation.Server.Host.Utils;
|
||||
|
||||
namespace Tgstation.Server.Host.Swarm
|
||||
{
|
||||
|
||||
@@ -8,6 +8,7 @@ using Mono.Unix;
|
||||
using Mono.Unix.Native;
|
||||
|
||||
using Tgstation.Server.Host.Core;
|
||||
using Tgstation.Server.Host.Utils;
|
||||
|
||||
namespace Tgstation.Server.Host.System
|
||||
{
|
||||
|
||||
@@ -7,10 +7,11 @@ using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
using BetterWin32Errors;
|
||||
|
||||
using Microsoft.Extensions.Hosting;
|
||||
using Microsoft.Extensions.Logging;
|
||||
|
||||
using Tgstation.Server.Host.Core;
|
||||
using Tgstation.Server.Host.Utils;
|
||||
|
||||
namespace Tgstation.Server.Host.System
|
||||
{
|
||||
|
||||
@@ -8,10 +8,10 @@ using Microsoft.Extensions.Logging;
|
||||
|
||||
using Tgstation.Server.Api.Models;
|
||||
using Tgstation.Server.Api.Models.Response;
|
||||
using Tgstation.Server.Host.Core;
|
||||
using Tgstation.Server.Host.Extensions;
|
||||
using Tgstation.Server.Host.IO;
|
||||
using Tgstation.Server.Host.Security;
|
||||
using Tgstation.Server.Host.Utils;
|
||||
|
||||
namespace Tgstation.Server.Host.Transfer
|
||||
{
|
||||
@@ -121,7 +121,7 @@ namespace Tgstation.Server.Host.Transfer
|
||||
if (downloadProvider == null)
|
||||
throw new ArgumentNullException(nameof(downloadProvider));
|
||||
|
||||
logger.LogDebug("Creating download ticket for path {0}", downloadProvider.FilePath);
|
||||
logger.LogDebug("Creating download ticket for path {filePath}", downloadProvider.FilePath);
|
||||
var ticketResult = CreateTicket();
|
||||
|
||||
lock (downloadTickets)
|
||||
@@ -131,10 +131,10 @@ namespace Tgstation.Server.Host.Transfer
|
||||
{
|
||||
lock (downloadTickets)
|
||||
if (downloadTickets.Remove(ticketResult.FileTicket))
|
||||
logger.LogTrace("Expired download ticket {0}...", ticketResult.FileTicket);
|
||||
logger.LogTrace("Expired download ticket {ticket}...", ticketResult.FileTicket);
|
||||
});
|
||||
|
||||
logger.LogTrace("Created download ticket {0}", ticketResult.FileTicket);
|
||||
logger.LogTrace("Created download ticket {ticket}", ticketResult.FileTicket);
|
||||
|
||||
return ticketResult;
|
||||
}
|
||||
@@ -152,14 +152,14 @@ namespace Tgstation.Server.Host.Transfer
|
||||
{
|
||||
lock (uploadTickets)
|
||||
if (uploadTickets.Remove(uploadTicket.Ticket.FileTicket))
|
||||
logger.LogTrace("Expired upload ticket {0}...", uploadTicket.Ticket.FileTicket);
|
||||
logger.LogTrace("Expired upload ticket {ticket}...", uploadTicket.Ticket.FileTicket);
|
||||
else
|
||||
return;
|
||||
|
||||
uploadTicket.Expire();
|
||||
});
|
||||
|
||||
logger.LogTrace("Created upload ticket {0}", uploadTicket.Ticket.FileTicket);
|
||||
logger.LogTrace("Created upload ticket {ticket}", uploadTicket.Ticket.FileTicket);
|
||||
|
||||
return uploadTicket;
|
||||
}
|
||||
@@ -175,7 +175,7 @@ namespace Tgstation.Server.Host.Transfer
|
||||
{
|
||||
if (!downloadTickets.TryGetValue(ticket.FileTicket, out downloadProvider))
|
||||
{
|
||||
logger.LogTrace("Download ticket {0} not found!", ticket.FileTicket);
|
||||
logger.LogTrace("Download ticket {ticket} not found!", ticket.FileTicket);
|
||||
return Tuple.Create<FileStream, ErrorMessageResponse>(null, null);
|
||||
}
|
||||
|
||||
@@ -185,7 +185,7 @@ namespace Tgstation.Server.Host.Transfer
|
||||
var errorCode = downloadProvider.ActivationCallback();
|
||||
if (errorCode.HasValue)
|
||||
{
|
||||
logger.LogDebug("Download ticket {0} failed activation!", ticket.FileTicket);
|
||||
logger.LogDebug("Download ticket {ticket} failed activation!", ticket.FileTicket);
|
||||
return Tuple.Create<FileStream, ErrorMessageResponse>(null, new ErrorMessageResponse(errorCode.Value));
|
||||
}
|
||||
|
||||
@@ -209,7 +209,7 @@ namespace Tgstation.Server.Host.Transfer
|
||||
|
||||
try
|
||||
{
|
||||
logger.LogTrace("Ticket {0} downloading...", ticket.FileTicket);
|
||||
logger.LogTrace("Ticket {ticket} downloading...", ticket.FileTicket);
|
||||
return Tuple.Create<FileStream, ErrorMessageResponse>(stream, null);
|
||||
}
|
||||
catch
|
||||
@@ -230,7 +230,7 @@ namespace Tgstation.Server.Host.Transfer
|
||||
{
|
||||
if (!uploadTickets.TryGetValue(ticket.FileTicket, out uploadProvider))
|
||||
{
|
||||
logger.LogTrace("Upload ticket {0} not found!", ticket.FileTicket);
|
||||
logger.LogTrace("Upload ticket {ticket} not found!", ticket.FileTicket);
|
||||
return new ErrorMessageResponse(ErrorCode.ResourceNotPresent);
|
||||
}
|
||||
|
||||
|
||||
+1
-1
@@ -6,7 +6,7 @@ using Microsoft.Extensions.Logging;
|
||||
using Tgstation.Server.Common;
|
||||
using Tgstation.Server.Host.System;
|
||||
|
||||
namespace Tgstation.Server.Host.Core
|
||||
namespace Tgstation.Server.Host.Utils
|
||||
{
|
||||
/// <inheritdoc />
|
||||
sealed class AbstractHttpClientFactory : IAbstractHttpClientFactory
|
||||
+1
-1
@@ -2,7 +2,7 @@
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace Tgstation.Server.Host.Core
|
||||
namespace Tgstation.Server.Host.Utils
|
||||
{
|
||||
/// <inheritdoc />
|
||||
sealed class AsyncDelayer : IAsyncDelayer
|
||||
+2
-2
@@ -6,7 +6,7 @@ using Octokit;
|
||||
using Tgstation.Server.Host.Configuration;
|
||||
using Tgstation.Server.Host.System;
|
||||
|
||||
namespace Tgstation.Server.Host.Core
|
||||
namespace Tgstation.Server.Host.Utils
|
||||
{
|
||||
/// <inheritdoc />
|
||||
sealed class GitHubClientFactory : IGitHubClientFactory
|
||||
@@ -49,7 +49,7 @@ namespace Tgstation.Server.Host.Core
|
||||
new ProductHeaderValue(
|
||||
assemblyInformationProvider.ProductInfoHeaderValue.Product.Name,
|
||||
assemblyInformationProvider.ProductInfoHeaderValue.Product.Version));
|
||||
if (!String.IsNullOrWhiteSpace(accessToken))
|
||||
if (!string.IsNullOrWhiteSpace(accessToken))
|
||||
client.Credentials = new Credentials(accessToken);
|
||||
|
||||
return client;
|
||||
+1
-1
@@ -2,7 +2,7 @@
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace Tgstation.Server.Host.Core
|
||||
namespace Tgstation.Server.Host.Utils
|
||||
{
|
||||
/// <summary>
|
||||
/// For waiting asynchronously.
|
||||
+1
-1
@@ -1,6 +1,6 @@
|
||||
using Octokit;
|
||||
|
||||
namespace Tgstation.Server.Host.Core
|
||||
namespace Tgstation.Server.Host.Utils
|
||||
{
|
||||
/// <summary>
|
||||
/// For creating <see cref="IGitHubClient"/>s.
|
||||
+1
-1
@@ -1,7 +1,7 @@
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace Tgstation.Server.Host.Core
|
||||
namespace Tgstation.Server.Host.Utils
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets unassigned ports for use by TGS.
|
||||
+1
-1
@@ -5,7 +5,7 @@ using Microsoft.OpenApi.Interfaces;
|
||||
using Microsoft.OpenApi.Models;
|
||||
using Microsoft.OpenApi.Writers;
|
||||
|
||||
namespace Tgstation.Server.Host.Core
|
||||
namespace Tgstation.Server.Host.Utils
|
||||
{
|
||||
/// <summary>
|
||||
/// Implements the "x-enum-varnames" OpenAPI 3.0 extension.
|
||||
+3
-2
@@ -9,10 +9,11 @@ using Microsoft.Extensions.Logging;
|
||||
using Microsoft.Extensions.Options;
|
||||
|
||||
using Tgstation.Server.Host.Configuration;
|
||||
using Tgstation.Server.Host.Core;
|
||||
using Tgstation.Server.Host.Database;
|
||||
using Tgstation.Server.Host.Extensions;
|
||||
|
||||
namespace Tgstation.Server.Host.Core
|
||||
namespace Tgstation.Server.Host.Utils
|
||||
{
|
||||
/// <inheritdoc />
|
||||
sealed class PortAllocator : IPortAllocator
|
||||
@@ -81,7 +82,7 @@ namespace Tgstation.Server.Host.Core
|
||||
ushort port = 0;
|
||||
try
|
||||
{
|
||||
for (port = basePort; port < UInt16.MaxValue; ++port)
|
||||
for (port = basePort; port < ushort.MaxValue; ++port)
|
||||
{
|
||||
if (checkOne && port != basePort)
|
||||
break;
|
||||
@@ -0,0 +1,101 @@
|
||||
using System;
|
||||
|
||||
namespace Tgstation.Server.Host.Utils
|
||||
{
|
||||
/// <summary>
|
||||
/// Class used for counting references with <see cref="ReferenceCountingContainer{TWrapped, TReference}"/>.
|
||||
/// </summary>
|
||||
/// <typeparam name="TInstance">The reference <see langword="class"/>.</typeparam>
|
||||
abstract class ReferenceCounter<TInstance> : IDisposable
|
||||
where TInstance : class
|
||||
{
|
||||
/// <summary>
|
||||
/// The referenced <typeparamref name="TInstance"/>.
|
||||
/// </summary>
|
||||
protected TInstance Instance => actualInstance ?? throw UninitializedOrDisposedException();
|
||||
|
||||
/// <summary>
|
||||
/// The <see langword="lock"/> object for <see cref="Initialize(TInstance, Action)"/> and <see cref="Dispose"/>.
|
||||
/// </summary>
|
||||
readonly object initDisposeLock;
|
||||
|
||||
/// <summary>
|
||||
/// Backing field for <see cref="Instance"/>.
|
||||
/// </summary>
|
||||
TInstance actualInstance;
|
||||
|
||||
/// <summary>
|
||||
/// The <see cref="Action"/> to take when <see cref="Dispose"/> is called.
|
||||
/// </summary>
|
||||
Action referenceCleanupAction;
|
||||
|
||||
/// <summary>
|
||||
/// If the <see cref="ReferenceCounter{TInstance}"/> was initialized.
|
||||
/// </summary>
|
||||
bool initialized;
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="ReferenceCounter{TInstance}"/> class.
|
||||
/// </summary>
|
||||
protected ReferenceCounter()
|
||||
{
|
||||
initDisposeLock = new object();
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public void Dispose()
|
||||
{
|
||||
lock (initDisposeLock)
|
||||
{
|
||||
referenceCleanupAction?.Invoke();
|
||||
referenceCleanupAction = null;
|
||||
actualInstance = null;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Initialize the <see cref="ReferenceCounter{TInstance}"/>.
|
||||
/// </summary>
|
||||
/// <param name="instance">The reference counted <typeparamref name="TInstance"/>.</param>
|
||||
/// <param name="referenceCleanupAction">The <see cref="Action"/> to take to clean up the reference.</param>
|
||||
public void Initialize(TInstance instance, Action referenceCleanupAction)
|
||||
{
|
||||
if (instance == null)
|
||||
throw new ArgumentNullException(nameof(instance));
|
||||
|
||||
if (referenceCleanupAction == null)
|
||||
throw new ArgumentNullException(nameof(referenceCleanupAction));
|
||||
|
||||
lock (initDisposeLock)
|
||||
{
|
||||
if (initialized)
|
||||
throw new InvalidOperationException($"{nameof(ReferenceCounter<TInstance>)} already initialized!");
|
||||
|
||||
actualInstance = instance;
|
||||
this.referenceCleanupAction = referenceCleanupAction;
|
||||
initialized = true;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Prevents the aquired reference from being dropped when <see cref="Dispose"/> is called.
|
||||
/// </summary>
|
||||
/// <remarks>This will prevent <see cref="ReferenceCountingContainer{TWrapped, TReference}.OnZeroReferences"/> from ever completing.</remarks>
|
||||
protected void DangerousDropReference()
|
||||
{
|
||||
referenceCleanupAction = null;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Throw the appropriate <see cref="InvalidOperationException"/> when the <see cref="ReferenceCounter{TInstance}"/> is uninitialized or disposed.
|
||||
/// </summary>
|
||||
/// <returns>A new <see cref="InvalidOperationException"/> to throw.</returns>
|
||||
InvalidOperationException UninitializedOrDisposedException()
|
||||
{
|
||||
if (initialized)
|
||||
return new ObjectDisposedException(nameof(ReferenceCounter<TInstance>));
|
||||
|
||||
return new InvalidOperationException($"{nameof(ReferenceCounter<TInstance>)} not initialized!");
|
||||
}
|
||||
}
|
||||
}
|
||||
+19
-13
@@ -1,20 +1,24 @@
|
||||
using System;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace Tgstation.Server.Host.Components
|
||||
namespace Tgstation.Server.Host.Utils
|
||||
{
|
||||
/// <summary>
|
||||
/// Wrapper for managing <see cref="IInstance"/>s.
|
||||
/// Wrapper for managing some <typeparamref name="TWrapped"/>.
|
||||
/// </summary>
|
||||
sealed class InstanceContainer
|
||||
/// <typeparam name="TWrapped">The type being wrapped.</typeparam>
|
||||
/// <typeparam name="TReference">The disposable reference type returned.</typeparam>
|
||||
sealed class ReferenceCountingContainer<TWrapped, TReference>
|
||||
where TWrapped : class
|
||||
where TReference : ReferenceCounter<TWrapped>, new()
|
||||
{
|
||||
/// <summary>
|
||||
/// The <see cref="IInstance"/>.
|
||||
/// The <typeparamref name="TWrapped"/>.
|
||||
/// </summary>
|
||||
public IInstance Instance { get; }
|
||||
public TWrapped Instance { get; }
|
||||
|
||||
/// <summary>
|
||||
/// A <see cref="Task"/> that completes when there are no <see cref="IInstanceReference"/>s active for the <see cref="Instance"/>.
|
||||
/// A <see cref="Task"/> that completes when there are no <typeparamref name="TReference"/>s active for the <see cref="Instance"/>.
|
||||
/// </summary>
|
||||
public Task OnZeroReferences
|
||||
{
|
||||
@@ -40,15 +44,15 @@ namespace Tgstation.Server.Host.Components
|
||||
TaskCompletionSource onZeroReferencesTcs;
|
||||
|
||||
/// <summary>
|
||||
/// Count of active <see cref="IInstanceReference"/>s.
|
||||
/// Count of active <see cref="Instance"/>s.
|
||||
/// </summary>
|
||||
ulong referenceCount;
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="InstanceContainer"/> class.
|
||||
/// Initializes a new instance of the <see cref="ReferenceCountingContainer{TWrapped, TReference}"/> class.
|
||||
/// </summary>
|
||||
/// <param name="instance">The value of <see cref="Instance"/>.</param>
|
||||
public InstanceContainer(IInstance instance)
|
||||
public ReferenceCountingContainer(TWrapped instance)
|
||||
{
|
||||
Instance = instance ?? throw new ArgumentNullException(nameof(instance));
|
||||
|
||||
@@ -56,10 +60,10 @@ namespace Tgstation.Server.Host.Components
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Create a new <see cref="IInstanceReference"/>.
|
||||
/// Create a new <typeparamref name="TReference"/> to the <see cref="Instance"/>.
|
||||
/// </summary>
|
||||
/// <returns>A new <see cref="IInstanceReference"/>.</returns>
|
||||
public IInstanceReference AddReference()
|
||||
/// <returns>A new <typeparamref name="TReference"/>.</returns>
|
||||
public TReference AddReference()
|
||||
{
|
||||
lock (referenceCountLock)
|
||||
{
|
||||
@@ -68,12 +72,14 @@ namespace Tgstation.Server.Host.Components
|
||||
|
||||
try
|
||||
{
|
||||
return new InstanceWrapper(Instance, () =>
|
||||
var reference = new TReference();
|
||||
reference.Initialize(Instance, () =>
|
||||
{
|
||||
lock (referenceCountLock)
|
||||
if (--referenceCount == 0)
|
||||
onZeroReferencesTcs.SetResult();
|
||||
});
|
||||
return reference;
|
||||
}
|
||||
catch
|
||||
{
|
||||
+1
-1
@@ -2,7 +2,7 @@
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace Tgstation.Server.Host.Core
|
||||
namespace Tgstation.Server.Host.Utils
|
||||
{
|
||||
/// <summary>
|
||||
/// Async lock context helper.
|
||||
@@ -0,0 +1,80 @@
|
||||
namespace Tgstation.Server.Host.Utils
|
||||
{
|
||||
/// <summary>
|
||||
/// Helpers for manipulating the <see cref="Serilog.Context.LogContext"/>.
|
||||
/// </summary>
|
||||
public static class SerilogContextHelper
|
||||
{
|
||||
/// <summary>
|
||||
/// The <see cref="Serilog.Context.LogContext"/> property name for <see cref="Models.Instance"/> <see cref="Api.Models.EntityId.Id"/>s.
|
||||
/// </summary>
|
||||
public const string InstanceIdContextProperty = "Instance";
|
||||
|
||||
/// <summary>
|
||||
/// The <see cref="Serilog.Context.LogContext"/> property name for <see cref="Models.Job"/> <see cref="Api.Models.EntityId.Id"/>s.
|
||||
/// </summary>
|
||||
public const string JobIdContextProperty = "Job";
|
||||
|
||||
/// <summary>
|
||||
/// The <see cref="Serilog.Context.LogContext"/> property name for <see cref="Models.User"/> <see cref="Api.Models.EntityId.Id"/>s.
|
||||
/// </summary>
|
||||
public const string RequestPathContextProperty = "Request";
|
||||
|
||||
/// <summary>
|
||||
/// The <see cref="Serilog.Context.LogContext"/> property name for <see cref="Models.Instance"/> <see cref="Api.Models.EntityId.Id"/>s.
|
||||
/// </summary>
|
||||
public const string UserIdContextProperty = "User";
|
||||
|
||||
/// <summary>
|
||||
/// The <see cref="Serilog.Context.LogContext"/> property name for the ID of the watchdog monitor iteration currently being processed.
|
||||
/// </summary>
|
||||
public const string WatchdogMonitorIterationContextProperty = "Monitor";
|
||||
|
||||
/// <summary>
|
||||
/// The <see cref="Serilog.Context.LogContext"/> property name for the ID of the bridge request currently being processed.
|
||||
/// </summary>
|
||||
public const string BridgeRequestIterationContextProperty = "Bridge";
|
||||
|
||||
/// <summary>
|
||||
/// The <see cref="Serilog.Context.LogContext"/> property name for the ID of the chat message currently being processed.
|
||||
/// </summary>
|
||||
public const string ChatMessageIterationContextProperty = "ChatMessage";
|
||||
|
||||
/// <summary>
|
||||
/// The <see cref="Serilog.Context.LogContext"/> property name for <see cref="Components.IInstanceReference.Uid"/>s.
|
||||
/// </summary>
|
||||
public const string InstanceReferenceContextProperty = "InstanceReference";
|
||||
|
||||
/// <summary>
|
||||
/// The <see cref="Serilog.Context.LogContext"/> property name for <see cref="Api.Models.Internal.SwarmServer.Identifier"/>s.
|
||||
/// </summary>
|
||||
public const string SwarmIdentifierContextProperty = "Node";
|
||||
|
||||
/// <summary>
|
||||
/// The default value of <see cref="Template"/>.
|
||||
/// </summary>
|
||||
const string DefaultTemplate = $"Instance:{{{InstanceIdContextProperty}}}|Job:{{{JobIdContextProperty}}}|Request:{{{RequestPathContextProperty}}}|User:{{{UserIdContextProperty}}}|Monitor:{{{WatchdogMonitorIterationContextProperty}}}|Bridge:{{{BridgeRequestIterationContextProperty}}}|Chat:{{{ChatMessageIterationContextProperty}}}|IR:{{{InstanceReferenceContextProperty}}}";
|
||||
|
||||
/// <summary>
|
||||
/// Common template used for adding our custom log context to serilog.
|
||||
/// </summary>
|
||||
/// <remarks>Should not be changed. Only mutable for the sake of identifying swarm nodes under a single test environment</remarks>
|
||||
public static string Template { get; private set; }
|
||||
|
||||
/// <summary>
|
||||
/// Initializes static members of the <see cref="SerilogContextHelper"/> class.
|
||||
/// </summary>
|
||||
static SerilogContextHelper()
|
||||
{
|
||||
Template = DefaultTemplate;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Adds the placeholder for the <see cref="SwarmIdentifierContextProperty"/> to the <see cref="Template"/>.
|
||||
/// </summary>
|
||||
public static void AddSwarmNodeIdentifierToTemplate()
|
||||
{
|
||||
Template = $"{DefaultTemplate}|Node:{SwarmIdentifierContextProperty}";
|
||||
}
|
||||
}
|
||||
}
|
||||
+2
-1
@@ -10,6 +10,7 @@ using Microsoft.Extensions.DependencyInjection;
|
||||
using Microsoft.Net.Http.Headers;
|
||||
using Microsoft.OpenApi.Any;
|
||||
using Microsoft.OpenApi.Models;
|
||||
|
||||
using Swashbuckle.AspNetCore.SwaggerGen;
|
||||
|
||||
using Tgstation.Server.Api;
|
||||
@@ -17,7 +18,7 @@ using Tgstation.Server.Api.Models;
|
||||
using Tgstation.Server.Api.Models.Response;
|
||||
using Tgstation.Server.Host.Controllers;
|
||||
|
||||
namespace Tgstation.Server.Host.Core
|
||||
namespace Tgstation.Server.Host.Utils
|
||||
{
|
||||
/// <summary>
|
||||
/// Implements various filters for <see cref="Swashbuckle"/>.
|
||||
@@ -34,7 +34,7 @@
|
||||
// Intentionally slow down startup for testing purposes
|
||||
for(var/i in 1 to 10000000)
|
||||
dab()
|
||||
TgsNew(new /datum/tgs_event_handler/impl, TGS_SECURITY_ULTRASAFE)
|
||||
TgsNew(new /datum/tgs_event_handler/impl, TGS_SECURITY_SAFE)
|
||||
StartAsync()
|
||||
|
||||
/proc/dab()
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
using Microsoft.VisualStudio.TestTools.UnitTesting;
|
||||
using Microsoft.VisualStudio.TestTools.UnitTesting;
|
||||
using System;
|
||||
using System.Linq;
|
||||
|
||||
@@ -40,7 +40,7 @@ namespace Tgstation.Server.Api.Rights.Tests
|
||||
[TestMethod]
|
||||
public void TestAllRightsWorks()
|
||||
{
|
||||
var allByondRights = ByondRights.CancelInstall | ByondRights.InstallOfficialOrChangeActiveVersion | ByondRights.ListInstalled | ByondRights.ReadActive | ByondRights.InstallCustomVersion;
|
||||
var allByondRights = ByondRights.CancelInstall | ByondRights.InstallOfficialOrChangeActiveVersion | ByondRights.ListInstalled | ByondRights.ReadActive | ByondRights.InstallCustomVersion | ByondRights.DeleteInstall;
|
||||
var automaticByondRights = RightsHelper.AllRights<ByondRights>();
|
||||
|
||||
Assert.AreEqual(allByondRights, automaticByondRights);
|
||||
|
||||
@@ -9,6 +9,7 @@ using Moq;
|
||||
using Tgstation.Server.Host.Core;
|
||||
using Tgstation.Server.Host.Extensions;
|
||||
using Tgstation.Server.Host.System;
|
||||
using Tgstation.Server.Host.Utils;
|
||||
|
||||
namespace Tgstation.Server.Host.Tests.Signals
|
||||
{
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user