Merge pull request #1463 from tgstation/1084-DeleteByond [APIDeploy][NugetDeploy][DMDeploy]

DELETE /Byond
This commit is contained in:
Jordan Dominion
2023-04-25 17:33:17 -04:00
committed by GitHub
115 changed files with 1870 additions and 1032 deletions
+8 -14
View File
@@ -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>
+3
View File
@@ -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);
}
}
+12
View File
@@ -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,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);
}
@@ -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
{
@@ -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)
+13 -11
View File
@@ -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
{
+3 -3
View File
@@ -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>
+1 -1
View File
@@ -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);
}
@@ -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
@@ -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
@@ -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;
@@ -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,6 +1,6 @@
using Octokit;
namespace Tgstation.Server.Host.Core
namespace Tgstation.Server.Host.Utils
{
/// <summary>
/// For creating <see cref="IGitHubClient"/>s.
@@ -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.
@@ -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.
@@ -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!");
}
}
}
@@ -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
{
@@ -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}";
}
}
}
@@ -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"/>.
+1 -1
View File
@@ -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