diff --git a/src/Tgstation.Server.Api/Models/ErrorCode.cs b/src/Tgstation.Server.Api/Models/ErrorCode.cs index 585bd0b137..a2e59a6856 100644 --- a/src/Tgstation.Server.Api/Models/ErrorCode.cs +++ b/src/Tgstation.Server.Api/Models/ErrorCode.cs @@ -6,7 +6,7 @@ namespace Tgstation.Server.Api.Models /// /// Types of s that the API may return. /// - /// Entries marked with the are no longer in use but kept for reference. + /// Entries marked with the are no longer in use but kept for placeholders until they can be recycled in the next major API version. public enum ErrorCode : uint { /// @@ -178,11 +178,10 @@ namespace Tgstation.Server.Api.Models ConfigurationDirectoryNotEmpty, /// - /// Currently unused. + /// The server swarm has less than the expected amount of nodes. /// - [Obsolete("Unused", true)] - [Description("Unknown error code.")] - UnusedErrorCode1, + [Description("The server swarm has less than the expected amount of nodes!")] + SwarmIntegrityCheckFailed, /// /// One of and is set while the other isn't. @@ -227,11 +226,10 @@ namespace Tgstation.Server.Api.Models RepoMismatchShaAndUpdate, /// - /// Currently unused. + /// Could not delete a BYOND version due to it being set as the active version for the instance. /// - [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, /// /// contained duplicate s. @@ -630,10 +628,6 @@ namespace Tgstation.Server.Api.Models [Description("The deployment took longer than the configured timeout!")] DeploymentTimeout, - /// - /// The server swarm has less than the expected amount of nodes. - /// - [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 } } diff --git a/src/Tgstation.Server.Api/Models/Request/ByondVersionDeleteRequest.cs b/src/Tgstation.Server.Api/Models/Request/ByondVersionDeleteRequest.cs new file mode 100644 index 0000000000..aaf8585c9b --- /dev/null +++ b/src/Tgstation.Server.Api/Models/Request/ByondVersionDeleteRequest.cs @@ -0,0 +1,16 @@ +using System; + +namespace Tgstation.Server.Api.Models.Request +{ + /// + /// A request to delete a specific . + /// + public class ByondVersionDeleteRequest + { + /// + /// The BYOND version to install. + /// + [RequestOptions(FieldPresence.Required)] + public Version? Version { get; set; } + } +} diff --git a/src/Tgstation.Server.Api/Models/Request/ByondVersionRequest.cs b/src/Tgstation.Server.Api/Models/Request/ByondVersionRequest.cs index 72b37a8378..b86eea074a 100644 --- a/src/Tgstation.Server.Api/Models/Request/ByondVersionRequest.cs +++ b/src/Tgstation.Server.Api/Models/Request/ByondVersionRequest.cs @@ -1,18 +1,10 @@ -using System; - -namespace Tgstation.Server.Api.Models.Request +namespace Tgstation.Server.Api.Models.Request { /// - /// A request to install a BYOND . + /// A request to install a BYOND . /// - public sealed class ByondVersionRequest + public sealed class ByondVersionRequest : ByondVersionDeleteRequest { - /// - /// The BYOND version to install. - /// - [RequestOptions(FieldPresence.Required)] - public Version? Version { get; set; } - /// /// If a custom BYOND version is to be uploaded. /// diff --git a/src/Tgstation.Server.Api/Rights/ByondRights.cs b/src/Tgstation.Server.Api/Rights/ByondRights.cs index 1d5e5d3848..288d8ca3b8 100644 --- a/src/Tgstation.Server.Api/Rights/ByondRights.cs +++ b/src/Tgstation.Server.Api/Rights/ByondRights.cs @@ -37,5 +37,10 @@ namespace Tgstation.Server.Api.Rights /// User may upload and activate custom BYOND builds. /// InstallCustomVersion = 1 << 4, + + /// + /// User may delete non-active BYOND builds. + /// + DeleteInstall = 1 << 5, } } diff --git a/src/Tgstation.Server.Api/Tgstation.Server.Api.csproj b/src/Tgstation.Server.Api/Tgstation.Server.Api.csproj index 77b0e05672..40ff67bf21 100644 --- a/src/Tgstation.Server.Api/Tgstation.Server.Api.csproj +++ b/src/Tgstation.Server.Api/Tgstation.Server.Api.csproj @@ -16,7 +16,7 @@ https://github.com/tgstation/tgstation-server 2018-2023 json web api tgstation-server tgstation ss13 byond - Added ErrorCode.SwarmIntegrityCheckFailed. + Added ByondRights.DeleteInstall, ErrorCode.SwarmIntegrityCheckFailed, ErrorCode.ByondCannotDeleteActiveVersion, and Models.Request.ByondVersionDeleteRequest. true snupkg ../../build/analyzers.ruleset diff --git a/src/Tgstation.Server.Client/ApiClient.cs b/src/Tgstation.Server.Client/ApiClient.cs index cfb7795d98..e48727de1d 100644 --- a/src/Tgstation.Server.Client/ApiClient.cs +++ b/src/Tgstation.Server.Client/ApiClient.cs @@ -209,6 +209,9 @@ namespace Tgstation.Server.Client /// public Task Delete(string route, long instanceId, CancellationToken cancellationToken) => RunRequest(route, null, HttpMethod.Delete, instanceId, false, cancellationToken); + /// + public Task Delete(string route, TBody body, long instanceId, CancellationToken cancellationToken) where TBody : class => RunRequest(route, body, HttpMethod.Delete, instanceId, false, cancellationToken); + /// public Task Create(string route, long instanceId, CancellationToken cancellationToken) => RunRequest(route, new object(), HttpMethod.Put, instanceId, false, cancellationToken); diff --git a/src/Tgstation.Server.Client/Components/ByondClient.cs b/src/Tgstation.Server.Client/Components/ByondClient.cs index 673794b38b..c167af0665 100644 --- a/src/Tgstation.Server.Client/Components/ByondClient.cs +++ b/src/Tgstation.Server.Client/Components/ByondClient.cs @@ -33,6 +33,10 @@ namespace Tgstation.Server.Client.Components /// public Task ActiveVersion(CancellationToken cancellationToken) => ApiClient.Read(Routes.Byond, instance.Id!.Value, cancellationToken); + /// + public Task DeleteVersion(ByondVersionDeleteRequest deleteRequest, CancellationToken cancellationToken) + => ApiClient.Delete(Routes.Byond, deleteRequest, instance.Id!.Value, cancellationToken); + /// public Task> InstalledVersions(PaginationSettings? paginationSettings, CancellationToken cancellationToken) => ReadPaged(paginationSettings, Routes.ListRoute(Routes.Byond), instance.Id, cancellationToken); diff --git a/src/Tgstation.Server.Client/Components/IByondClient.cs b/src/Tgstation.Server.Client/Components/IByondClient.cs index 554adceab2..b2fabe472a 100644 --- a/src/Tgstation.Server.Client/Components/IByondClient.cs +++ b/src/Tgstation.Server.Client/Components/IByondClient.cs @@ -29,12 +29,20 @@ namespace Tgstation.Server.Client.Components Task> InstalledVersions(PaginationSettings? paginationSettings, CancellationToken cancellationToken); /// - /// Updates the information. + /// Updates the active BYOND version. /// /// The . /// The for the .zip file if is . /// The for the operation. /// A resulting in the updated information. Task SetActiveVersion(ByondVersionRequest installRequest, Stream? zipFileStream, CancellationToken cancellationToken); + + /// + /// Starts a jobs to delete a specific BYOND version. + /// + /// The specifying the version to delete. + /// The for the operation. + /// A resulting in the for the delete job. + Task DeleteVersion(ByondVersionDeleteRequest deleteRequest, CancellationToken cancellationToken); } } diff --git a/src/Tgstation.Server.Client/IApiClient.cs b/src/Tgstation.Server.Client/IApiClient.cs index 665960f25a..7758e9aefe 100644 --- a/src/Tgstation.Server.Client/IApiClient.cs +++ b/src/Tgstation.Server.Client/IApiClient.cs @@ -204,6 +204,18 @@ namespace Tgstation.Server.Client /// A resulting in the response body as a . Task Delete(string route, long instanceId, CancellationToken cancellationToken); + /// + /// Run an HTTP DELETE request. + /// + /// The type to of the request body. + /// The type of the response body. + /// The server route to make the request to. + /// The request body. + /// The instance to make the request to. + /// The for the operation. + /// A representing the running operation. + Task Delete(string route, TBody body, long instanceId, CancellationToken cancellationToken) where TBody : class; + /// /// Downloads a file for a given . /// diff --git a/src/Tgstation.Server.Client/Tgstation.Server.Client.csproj b/src/Tgstation.Server.Client/Tgstation.Server.Client.csproj index c5af5ef053..264b8d9db3 100644 --- a/src/Tgstation.Server.Client/Tgstation.Server.Client.csproj +++ b/src/Tgstation.Server.Client/Tgstation.Server.Client.csproj @@ -16,7 +16,7 @@ https://github.com/tgstation/tgstation-server 2018-2023 json web api tgstation-server tgstation ss13 byond client - Updated definitions for API version 9.10.0. + Updated definitions for API version 9.10.0. Added support for deleting BYOND versions. true snupkg ../../build/analyzers.ruleset diff --git a/src/Tgstation.Server.Common/HttpClientFactory.cs b/src/Tgstation.Server.Common/HttpClientFactory.cs new file mode 100644 index 0000000000..d0ed887fcb --- /dev/null +++ b/src/Tgstation.Server.Common/HttpClientFactory.cs @@ -0,0 +1,41 @@ +using System; +using System.Net.Http.Headers; + +namespace Tgstation.Server.Common +{ + /// + /// that creates s. + /// + public sealed class HttpClientFactory : IAbstractHttpClientFactory + { + /// + public IHttpClient CreateClient() + { + var client = new HttpClient(); + try + { + client.DefaultRequestHeaders.UserAgent.Add(userAgent); + return client; + } + catch + { + client.Dispose(); + throw; + } + } + + /// + /// The used as created client's User-Agent header on request. + /// + readonly ProductInfoHeaderValue userAgent; + + /// + /// Initializes a new instance of the class. + /// + /// The value of . + public HttpClientFactory(ProductInfoHeaderValue userAgent) + { + this.userAgent = userAgent ?? throw new ArgumentNullException(nameof(userAgent)); + } + } +} diff --git a/src/Tgstation.Server.Host/Core/IAbstractHttpClientFactory.cs b/src/Tgstation.Server.Common/IAbstractHttpClientFactory.cs similarity index 80% rename from src/Tgstation.Server.Host/Core/IAbstractHttpClientFactory.cs rename to src/Tgstation.Server.Common/IAbstractHttpClientFactory.cs index d031fb350f..d43c24e987 100644 --- a/src/Tgstation.Server.Host/Core/IAbstractHttpClientFactory.cs +++ b/src/Tgstation.Server.Common/IAbstractHttpClientFactory.cs @@ -1,6 +1,4 @@ -using Tgstation.Server.Common; - -namespace Tgstation.Server.Host.Core +namespace Tgstation.Server.Common { /// /// Creates s. diff --git a/src/Tgstation.Server.Host/Components/Byond/ByondExecutableLock.cs b/src/Tgstation.Server.Host/Components/Byond/ByondExecutableLock.cs index e6f83363a9..fd1fd2a8db 100644 --- a/src/Tgstation.Server.Host/Components/Byond/ByondExecutableLock.cs +++ b/src/Tgstation.Server.Host/Components/Byond/ByondExecutableLock.cs @@ -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 { /// - sealed class ByondExecutableLock : IByondExecutableLock + sealed class ByondExecutableLock : ReferenceCounter, IByondExecutableLock { /// - public Version Version { get; } + public Version Version => Instance.Version; /// - public string DreamDaemonPath { get; } + public string DreamDaemonPath => Instance.DreamDaemonPath; /// - public string DreamMakerPath { get; } + public string DreamMakerPath => Instance.DreamMakerPath; /// - public bool SupportsCli { get; } - - /// - /// The for the . - /// - readonly IIOManager ioManager; - - /// - /// used to guard access to the . - /// - readonly SemaphoreSlim trustedFileSemaphore; - - /// - /// The path to the BYOND trusted .dmbs configuration file. - /// - readonly string trustedFilePath; - - /// - /// Initializes a new instance of the class. - /// - /// The value of . - /// The value of . - /// The value of . - /// The value of . - /// The value of . - /// The value of . - /// The value of . - 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; /// - public void Dispose() - { - } - - /// - public void DoNotDeleteThisSession() - { - } - - /// - 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(); } } diff --git a/src/Tgstation.Server.Host/Components/Byond/ByondInstallation.cs b/src/Tgstation.Server.Host/Components/Byond/ByondInstallation.cs new file mode 100644 index 0000000000..18671e28da --- /dev/null +++ b/src/Tgstation.Server.Host/Components/Byond/ByondInstallation.cs @@ -0,0 +1,48 @@ +using System; +using System.Threading.Tasks; + +namespace Tgstation.Server.Host.Components.Byond +{ + /// + sealed class ByondInstallation : IByondInstallation + { + /// + public Version Version { get; } + + /// + public string DreamDaemonPath { get; } + + /// + public string DreamMakerPath { get; } + + /// + public bool SupportsCli { get; } + + /// + /// The that completes when the BYOND version finished installing. + /// + public Task InstallationTask { get; } + + /// + /// Initializes a new instance of the class. + /// + /// The value of . + /// The value of . + /// The value of . + /// The value of . + /// The value of . + 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; + } + } +} diff --git a/src/Tgstation.Server.Host/Components/Byond/ByondManager.cs b/src/Tgstation.Server.Host/Components/Byond/ByondManager.cs index eab96c95d8..49456d01ae 100644 --- a/src/Tgstation.Server.Host/Components/Byond/ByondManager.cs +++ b/src/Tgstation.Server.Host/Components/Byond/ByondManager.cs @@ -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"; /// - /// The file in which we store the for installations. + /// The file in which we store the for installations. /// const string VersionFileName = "Version.txt"; /// - /// The file in which we store the for the active installation. + /// The file in which we store the . /// 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(); } } + /// + /// for writing to files in the user's BYOND directory. + /// + static readonly SemaphoreSlim UserFilesSemaphore = new (1); + /// /// The for the . /// @@ -82,22 +87,30 @@ namespace Tgstation.Server.Host.Components.Byond /// /// Map of byond s to s that complete when they are installed. /// - readonly Dictionary installedVersions; + readonly Dictionary> installedVersions; /// - /// The for the . + /// The for changing or deleting the active BYOND version. /// - readonly SemaphoreSlim semaphore; + readonly SemaphoreSlim changeDeleteSemaphore; /// - /// Converts a BYOND to a . + /// that notifes when the changes. /// - /// The to convert. - /// If the property of should be kept. - /// The representation of . - 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; + + /// + /// Validates a given parameter. + /// + /// The to validate. + 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)); + } /// /// Initializes a new instance of the 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(); - semaphore = new SemaphoreSlim(1); + installedVersions = new Dictionary>(); + changeDeleteSemaphore = new SemaphoreSlim(1); + activeVersionChanged = new TaskCompletionSource(); } /// - public void Dispose() => semaphore.Dispose(); + public void Dispose() => changeDeleteSemaphore.Dispose(); /// - 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 { - 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); + } + + /// + public async Task 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; } } /// - public async Task 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 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); + } + } } /// @@ -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(); @@ -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; /// - /// Installs a BYOND if it isn't already. + /// Ensures a BYOND is installed if it isn't already. /// + /// The optional for the operation. /// The BYOND to install. /// Custom zip file to use. Will cause a number to be added. + /// If this BYOND version is required as part of a locking operation. + /// If an installation should be performed if the is not installed. If and an installation is required an will be thrown. /// The for the operation. - /// A representing the running operation. - async Task InstallVersion(Version version, Stream customVersionStream, CancellationToken cancellationToken) + /// A resulting in the . + async Task 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 { 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 { 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 { 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 { e.Message }, cancellationToken); - lock (installedVersions) - installedVersions.Remove(versionKey); - ourTcs.SetException(e); + installLock.Dispose(); throw; } + } - return versionKey; + /// + /// Installs the files for a given BYOND . + /// + /// The optional for the operation. + /// The BYOND being installed with the number set if appropriate. + /// Custom zip file to use. Will cause a number to be added. + /// The for the operation. + /// A representing the running operation. + 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; + } + } + + /// + /// Create and add a new to . + /// + /// The being added. + /// The representing the installation process. + /// The new containing the new . + ReferenceCountingContainer 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(installation); + + lock (installedVersions) + installedVersions.Add(version, installationContainer); + + return installationContainer; + } + + /// + /// Add a given to the trusted DMBs list in BYOND's config. + /// + /// Full path to the .dmb that should be trusted. + /// The for the operation. + /// A representing the running operation. + 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); + } } } } diff --git a/src/Tgstation.Server.Host/Components/Byond/IByondExecutableLock.cs b/src/Tgstation.Server.Host/Components/Byond/IByondExecutableLock.cs index ef90080042..a320109dce 100644 --- a/src/Tgstation.Server.Host/Components/Byond/IByondExecutableLock.cs +++ b/src/Tgstation.Server.Host/Components/Byond/IByondExecutableLock.cs @@ -1,45 +1,15 @@ using System; -using System.Threading; -using System.Threading.Tasks; namespace Tgstation.Server.Host.Components.Byond { /// /// Represents usage of the two primary BYOND server executables. /// - public interface IByondExecutableLock : IDisposable + public interface IByondExecutableLock : IByondInstallation, IDisposable { - /// - /// The of the locked executables. - /// - Version Version { get; } - - /// - /// The path to the DreamDaemon executable. - /// - string DreamDaemonPath { get; } - - /// - /// The path to the dm/DreamMaker executable. - /// - string DreamMakerPath { get; } - - /// - /// If supports being run as a command-line application. - /// - bool SupportsCli { get; } - /// /// Call if, during a detach, this version should not be deleted. /// void DoNotDeleteThisSession(); - - /// - /// Add a given to the trusted DMBs list in BYOND's config. - /// - /// Full path to the .dmb that should be trusted. - /// The for the operation. - /// A representing the running operation. - Task TrustDmbPath(string fullDmbPath, CancellationToken cancellationToken); } } diff --git a/src/Tgstation.Server.Host/Components/Byond/IByondInstallation.cs b/src/Tgstation.Server.Host/Components/Byond/IByondInstallation.cs new file mode 100644 index 0000000000..ce95eeca5f --- /dev/null +++ b/src/Tgstation.Server.Host/Components/Byond/IByondInstallation.cs @@ -0,0 +1,30 @@ +using System; + +namespace Tgstation.Server.Host.Components.Byond +{ + /// + /// Represents a BYOND installation. + /// + public interface IByondInstallation + { + /// + /// The of the . + /// + Version Version { get; } + + /// + /// The full path to the DreamDaemon executable. + /// + string DreamDaemonPath { get; } + + /// + /// The full path to the dm/DreamMaker executable. + /// + string DreamMakerPath { get; } + + /// + /// If supports being run as a command-line application. + /// + bool SupportsCli { get; } + } +} diff --git a/src/Tgstation.Server.Host/Components/Byond/IByondManager.cs b/src/Tgstation.Server.Host/Components/Byond/IByondManager.cs index b38217c987..10222483ec 100644 --- a/src/Tgstation.Server.Host/Components/Byond/IByondManager.cs +++ b/src/Tgstation.Server.Host/Components/Byond/IByondManager.cs @@ -6,11 +6,14 @@ using System.Threading.Tasks; using Microsoft.Extensions.Hosting; +using Tgstation.Server.Host.Jobs; + namespace Tgstation.Server.Host.Components.Byond { /// /// For managing the BYOND installation. /// + /// When passing in s, ensure they are BYOND format versions unless referring to a custom version. This means should NEVER be 0. public interface IByondManager : IHostedService, IDisposable { /// @@ -26,18 +29,33 @@ namespace Tgstation.Server.Host.Components.Byond /// /// Change the active BYOND version. /// + /// The optional for the operation. /// The new . /// Optional of a custom BYOND version zip file. + /// If an installation should be performed if the is not installed. If and an installation is required an will be thrown. /// The for the operation. /// A representing the running operation. - Task ChangeVersion(Version version, Stream customVersionStream, CancellationToken cancellationToken); + Task ChangeVersion(JobProgressReporter progressReporter, Version version, Stream customVersionStream, bool allowInstallation, CancellationToken cancellationToken); + + /// + /// Deletes a given BYOND version from the disk. + /// + /// The for the operation. + /// The to delete. + /// The for the operation. + /// A representing the running operation. + Task DeleteVersion(JobProgressReporter progressReporter, Version version, CancellationToken cancellationToken); /// /// Lock the current installation's location and return a . /// /// The BYOND required. + /// The optional full path to .dmb to trust while using the executables. /// The for the operation. /// A resulting in the requested . - Task UseExecutables(Version requiredVersion, CancellationToken cancellationToken); + Task UseExecutables( + Version requiredVersion, + string trustDmbFullPath, + CancellationToken cancellationToken); } } diff --git a/src/Tgstation.Server.Host/Components/Byond/WindowsByondInstaller.cs b/src/Tgstation.Server.Host/Components/Byond/WindowsByondInstaller.cs index bb999f2e89..54110072ca 100644 --- a/src/Tgstation.Server.Host/Components/Byond/WindowsByondInstaller.cs +++ b/src/Tgstation.Server.Host/Components/Byond/WindowsByondInstaller.cs @@ -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); } /// diff --git a/src/Tgstation.Server.Host/Components/Chat/ChatManager.cs b/src/Tgstation.Server.Host/Components/Chat/ChatManager.cs index 2c5f6c924a..ddd0aff92b 100644 --- a/src/Tgstation.Server.Host/Components/Chat/ChatManager.cs +++ b/src/Tgstation.Server.Host/Components/Chat/ChatManager.cs @@ -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); diff --git a/src/Tgstation.Server.Host/Components/Chat/Providers/IrcProvider.cs b/src/Tgstation.Server.Host/Components/Chat/Providers/IrcProvider.cs index 509497232f..b0563406ea 100644 --- a/src/Tgstation.Server.Host/Components/Chat/Providers/IrcProvider.cs +++ b/src/Tgstation.Server.Host/Components/Chat/Providers/IrcProvider.cs @@ -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 { diff --git a/src/Tgstation.Server.Host/Components/Chat/Providers/ProviderFactory.cs b/src/Tgstation.Server.Host/Components/Chat/Providers/ProviderFactory.cs index 2d3893ef6d..6a7e0abd32 100644 --- a/src/Tgstation.Server.Host/Components/Chat/Providers/ProviderFactory.cs +++ b/src/Tgstation.Server.Host/Components/Chat/Providers/ProviderFactory.cs @@ -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 { diff --git a/src/Tgstation.Server.Host/Components/Deployment/DreamMaker.cs b/src/Tgstation.Server.Host/Components/Deployment/DreamMaker.cs index d409c71bd7..ab04afebb9 100644 --- a/src/Tgstation.Server.Host/Components/Deployment/DreamMaker.cs +++ b/src/Tgstation.Server.Host/Components/Deployment/DreamMaker.cs @@ -119,11 +119,6 @@ namespace Tgstation.Server.Host.Components.Deployment /// string currentDreamMakerOutput; - /// - /// Current stage to report on the job. - /// - string currentStage; - /// /// If a compile job is running. /// @@ -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(), default); throw; } @@ -558,6 +552,7 @@ namespace Tgstation.Server.Host.Components.Deployment /// /// Executes and populate a given . /// + /// The for the operation. /// The to run and populate. /// The to use. /// The to use. @@ -567,6 +562,7 @@ namespace Tgstation.Server.Host.Components.Deployment /// The for the operation. /// A representing the running operation. 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 @@ -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 @@ -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 { 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 { 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 /// A representing the running operation. 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); } diff --git a/src/Tgstation.Server.Host/Components/Deployment/Remote/GitHubRemoteDeploymentManager.cs b/src/Tgstation.Server.Host/Components/Deployment/Remote/GitHubRemoteDeploymentManager.cs index 5731c82b2a..8f9f24105d 100644 --- a/src/Tgstation.Server.Host/Components/Deployment/Remote/GitHubRemoteDeploymentManager.cs +++ b/src/Tgstation.Server.Host/Components/Deployment/Remote/GitHubRemoteDeploymentManager.cs @@ -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 { diff --git a/src/Tgstation.Server.Host/Components/Deployment/Remote/RemoteDeploymentManagerFactory.cs b/src/Tgstation.Server.Host/Components/Deployment/Remote/RemoteDeploymentManagerFactory.cs index 29d8d327a9..21b13b7a4c 100644 --- a/src/Tgstation.Server.Host/Components/Deployment/Remote/RemoteDeploymentManagerFactory.cs +++ b/src/Tgstation.Server.Host/Components/Deployment/Remote/RemoteDeploymentManagerFactory.cs @@ -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 { diff --git a/src/Tgstation.Server.Host/Components/IInstanceManager.cs b/src/Tgstation.Server.Host/Components/IInstanceManager.cs index fa573312e1..3174686efe 100644 --- a/src/Tgstation.Server.Host/Components/IInstanceManager.cs +++ b/src/Tgstation.Server.Host/Components/IInstanceManager.cs @@ -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 { /// /// For managing s. /// - public interface IInstanceManager : IBridgeDispatcher + public interface IInstanceManager : IInstanceOperations, IBridgeDispatcher { /// /// that completes when the finishes initializing. @@ -22,31 +20,5 @@ namespace Tgstation.Server.Host.Components /// The of the desired . /// The associated with the given if it is online, otherwise. IInstanceReference GetInstanceReference(Api.Models.Instance metadata); - - /// - /// Online an . - /// - /// The of the desired . - /// The for the operation. - /// A representing the running operation. - Task OnlineInstance(Models.Instance metadata, CancellationToken cancellationToken); - - /// - /// Offline an . - /// - /// The of the desired . - /// The performing the operation. - /// The for the operation. - /// A representing the running operation. - Task OfflineInstance(Models.Instance metadata, User user, CancellationToken cancellationToken); - - /// - /// Move an . - /// - /// The of the desired with the updated path. - /// The old path of the . will have this set on if the operation fails. - /// The for the operation. - /// A representing the running operation. - Task MoveInstance(Models.Instance metadata, string oldPath, CancellationToken cancellationToken); } } diff --git a/src/Tgstation.Server.Host/Components/IInstanceOperations.cs b/src/Tgstation.Server.Host/Components/IInstanceOperations.cs new file mode 100644 index 0000000000..701b937d52 --- /dev/null +++ b/src/Tgstation.Server.Host/Components/IInstanceOperations.cs @@ -0,0 +1,39 @@ +using System.Threading; +using System.Threading.Tasks; + +using Tgstation.Server.Host.Models; + +namespace Tgstation.Server.Host.Components +{ + /// + /// Operations that can be performed on a given . + /// + public interface IInstanceOperations + { + /// + /// Online an . + /// + /// The of the desired . + /// The for the operation. + /// A representing the running operation. + Task OnlineInstance(Models.Instance metadata, CancellationToken cancellationToken); + + /// + /// Offline an . + /// + /// The of the desired . + /// The performing the operation. + /// The for the operation. + /// A representing the running operation. + Task OfflineInstance(Models.Instance metadata, User user, CancellationToken cancellationToken); + + /// + /// Move an . + /// + /// The of the desired with the updated path. + /// The old path of the . will have this set on if the operation fails. + /// The for the operation. + /// A representing the running operation. + Task MoveInstance(Models.Instance metadata, string oldPath, CancellationToken cancellationToken); + } +} diff --git a/src/Tgstation.Server.Host/Components/Instance.cs b/src/Tgstation.Server.Host/Components/Instance.cs index aba8fec889..8d36d468b3 100644 --- a/src/Tgstation.Server.Host/Components/Instance.cs +++ b/src/Tgstation.Server.Host/Components/Instance.cs @@ -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 /// 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 /// 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 /// 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; diff --git a/src/Tgstation.Server.Host/Components/InstanceManager.cs b/src/Tgstation.Server.Host/Components/InstanceManager.cs index 699aef0c47..ad3ee3de51 100644 --- a/src/Tgstation.Server.Host/Components/InstanceManager.cs +++ b/src/Tgstation.Server.Host/Components/InstanceManager.cs @@ -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 logger; /// - /// Map of instance s to respective s. Also used as a . + /// Map of instance s to the respective for s. Also used as a . /// - readonly IDictionary instances; + readonly Dictionary> instances; /// /// Map of s to their respective s. /// - readonly IDictionary bridgeHandlers; + readonly Dictionary bridgeHandlers; /// /// used to guard calls to and . @@ -121,6 +122,21 @@ namespace Tgstation.Server.Host.Components /// readonly TaskCompletionSource readyTcs; + /// + /// The for . + /// + readonly CancellationTokenSource startupCancellationTokenSource; + + /// + /// The linked with the token given to . + /// + readonly CancellationTokenSource shutdownCancellationTokenSource; + + /// + /// The returned by . + /// + Task startupTask; + /// /// If the has been 'd. /// @@ -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(); + instances = new Dictionary>(); bridgeHandlers = new Dictionary(); readyTcs = new TaskCompletionSource(); instanceStateChangeSemaphore = new SemaphoreSlim(1); + startupCancellationTokenSource = new CancellationTokenSource(); + shutdownCancellationTokenSource = new CancellationTokenSource(); } /// @@ -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 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(); - 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(); + 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(instance)); } catch (Exception ex) { @@ -388,105 +421,52 @@ namespace Tgstation.Server.Host.Components } /// - 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 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; } /// 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!"); - } } /// @@ -553,10 +533,86 @@ namespace Tgstation.Server.Host.Components } /// - /// Check we have a valid system identity. + /// Initializes the . + /// + /// The for the operation. + /// A representing the running operation. + 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 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!"); + } + } + } + + /// + /// Check we have a valid system and configuration. /// void CheckSystemCompatibility() { + generalConfiguration.CheckCompatibility(logger); + using (var systemIdentity = systemIdentityFactory.GetCurrent()) { if (!systemIdentity.CanCreateSymlinks) diff --git a/src/Tgstation.Server.Host/Components/InstanceWrapper.cs b/src/Tgstation.Server.Host/Components/InstanceWrapper.cs index d2359c1b3b..3ea82d62d8 100644 --- a/src/Tgstation.Server.Host/Components/InstanceWrapper.cs +++ b/src/Tgstation.Server.Host/Components/InstanceWrapper.cs @@ -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 { /// - /// Warpper around a given with a . + /// for a given . /// - sealed class InstanceWrapper : IInstanceReference + sealed class InstanceWrapper : ReferenceCounter, IInstanceReference { /// public Guid Uid { get; } - /// - /// The object for . - /// - readonly object disposeLock; + /// + public IRepositoryManager RepositoryManager => Instance.RepositoryManager; - /// - /// The to take when is called. - /// - Action onDisposed; + /// + public IByondManager ByondManager => Instance.ByondManager; - /// - /// The calls are forwarded to. - /// - IInstanceCore actualInstance; + /// + public IDreamMaker DreamMaker => Instance.DreamMaker; + + /// + public IWatchdog Watchdog => Instance.Watchdog; + + /// + public IChatManager Chat => Instance.Chat; + + /// + public IConfiguration Configuration => Instance.Configuration; /// /// Initializes a new instance of the class. /// - /// The value of . - /// The value of . - 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(); } /// - public void Dispose() - { - lock (disposeLock) - { - onDisposed?.Invoke(); - onDisposed = null; - actualInstance = null; - } - } + public Task InstanceRenamed(string newInstanceName, CancellationToken cancellationToken) => Instance.InstanceRenamed(newInstanceName, cancellationToken); /// - public IRepositoryManager RepositoryManager => actualInstance?.RepositoryManager ?? throw new ObjectDisposedException(nameof(InstanceWrapper)); + public Task SetAutoUpdateInterval(uint newInterval) => Instance.SetAutoUpdateInterval(newInterval); /// - public IByondManager ByondManager => actualInstance?.ByondManager ?? throw new ObjectDisposedException(nameof(InstanceWrapper)); - - /// - public IDreamMaker DreamMaker => actualInstance?.DreamMaker ?? throw new ObjectDisposedException(nameof(InstanceWrapper)); - - /// - public IWatchdog Watchdog => actualInstance?.Watchdog ?? throw new ObjectDisposedException(nameof(InstanceWrapper)); - - /// - public IChatManager Chat => actualInstance?.Chat ?? throw new ObjectDisposedException(nameof(InstanceWrapper)); - - /// - public IConfiguration Configuration => actualInstance?.Configuration ?? throw new ObjectDisposedException(nameof(InstanceWrapper)); - - /// - public Task InstanceRenamed(string newInstanceName, CancellationToken cancellationToken) - => actualInstance?.InstanceRenamed(newInstanceName, cancellationToken) ?? throw new ObjectDisposedException(nameof(InstanceWrapper)); - - /// - public CompileJob LatestCompileJob() - { - if (actualInstance == null) - throw new ObjectDisposedException(nameof(InstanceWrapper)); - return actualInstance.LatestCompileJob(); - } - - /// - public Task SetAutoUpdateInterval(uint newInterval) - { - if (actualInstance == null) - throw new ObjectDisposedException(nameof(InstanceWrapper)); - return actualInstance.SetAutoUpdateInterval(newInterval); - } + public CompileJob LatestCompileJob() => Instance.LatestCompileJob(); } } diff --git a/src/Tgstation.Server.Host/Components/Repository/GitHubRemoteFeatures.cs b/src/Tgstation.Server.Host/Components/Repository/GitHubRemoteFeatures.cs index 0a5ca26fc3..52d6c6efe4 100644 --- a/src/Tgstation.Server.Host/Components/Repository/GitHubRemoteFeatures.cs +++ b/src/Tgstation.Server.Host/Components/Repository/GitHubRemoteFeatures.cs @@ -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 { diff --git a/src/Tgstation.Server.Host/Components/Repository/GitRemoteFeaturesFactory.cs b/src/Tgstation.Server.Host/Components/Repository/GitRemoteFeaturesFactory.cs index 9a9762b6a4..d43747fe68 100644 --- a/src/Tgstation.Server.Host/Components/Repository/GitRemoteFeaturesFactory.cs +++ b/src/Tgstation.Server.Host/Components/Repository/GitRemoteFeaturesFactory.cs @@ -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 { diff --git a/src/Tgstation.Server.Host/Components/Repository/RepositoryManager.cs b/src/Tgstation.Server.Host/Components/Repository/RepositoryManager.cs index ccf009173c..4ebfd23836 100644 --- a/src/Tgstation.Server.Host/Components/Repository/RepositoryManager.cs +++ b/src/Tgstation.Server.Host/Components/Repository/RepositoryManager.cs @@ -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 { diff --git a/src/Tgstation.Server.Host/Components/Session/SessionController.cs b/src/Tgstation.Server.Host/Components/Session/SessionController.cs index d6c9758d0e..82c0063193 100644 --- a/src/Tgstation.Server.Host/Components/Session/SessionController.cs +++ b/src/Tgstation.Server.Host/Components/Session/SessionController.cs @@ -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); diff --git a/src/Tgstation.Server.Host/Components/Session/SessionControllerFactory.cs b/src/Tgstation.Server.Host/Components/Session/SessionControllerFactory.cs index 849b1c30dc..f785629871 100644 --- a/src/Tgstation.Server.Host/Components/Session/SessionControllerFactory.cs +++ b/src/Tgstation.Server.Host/Components/Session/SessionControllerFactory.cs @@ -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 { diff --git a/src/Tgstation.Server.Host/Components/StaticFiles/Configuration.cs b/src/Tgstation.Server.Host/Components/StaticFiles/Configuration.cs index 74f771da31..3ba224a038 100644 --- a/src/Tgstation.Server.Host/Components/StaticFiles/Configuration.cs +++ b/src/Tgstation.Server.Host/Components/StaticFiles/Configuration.cs @@ -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 { diff --git a/src/Tgstation.Server.Host/Components/Watchdog/BasicWatchdog.cs b/src/Tgstation.Server.Host/Components/Watchdog/BasicWatchdog.cs index a44adb23da..5445ae311c 100644 --- a/src/Tgstation.Server.Host/Components/Watchdog/BasicWatchdog.cs +++ b/src/Tgstation.Server.Host/Components/Watchdog/BasicWatchdog.cs @@ -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; } diff --git a/src/Tgstation.Server.Host/Components/Watchdog/PosixWatchdog.cs b/src/Tgstation.Server.Host/Components/Watchdog/PosixWatchdog.cs index 8195e3c9e5..f9f3e6bbb4 100644 --- a/src/Tgstation.Server.Host/Components/Watchdog/PosixWatchdog.cs +++ b/src/Tgstation.Server.Host/Components/Watchdog/PosixWatchdog.cs @@ -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); } diff --git a/src/Tgstation.Server.Host/Components/Watchdog/PosixWatchdogFactory.cs b/src/Tgstation.Server.Host/Components/Watchdog/PosixWatchdogFactory.cs index 308f43ba1e..6774c73cb5 100644 --- a/src/Tgstation.Server.Host/Components/Watchdog/PosixWatchdogFactory.cs +++ b/src/Tgstation.Server.Host/Components/Watchdog/PosixWatchdogFactory.cs @@ -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 { diff --git a/src/Tgstation.Server.Host/Components/Watchdog/WatchdogBase.cs b/src/Tgstation.Server.Host/Components/Watchdog/WatchdogBase.cs index da26dfb886..e2b1a6b7dc 100644 --- a/src/Tgstation.Server.Host/Components/Watchdog/WatchdogBase.cs +++ b/src/Tgstation.Server.Host/Components/Watchdog/WatchdogBase.cs @@ -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( diff --git a/src/Tgstation.Server.Host/Components/Watchdog/WatchdogFactory.cs b/src/Tgstation.Server.Host/Components/Watchdog/WatchdogFactory.cs index d4a49d99a9..2fb7182755 100644 --- a/src/Tgstation.Server.Host/Components/Watchdog/WatchdogFactory.cs +++ b/src/Tgstation.Server.Host/Components/Watchdog/WatchdogFactory.cs @@ -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 { diff --git a/src/Tgstation.Server.Host/Components/Watchdog/WindowsWatchdog.cs b/src/Tgstation.Server.Host/Components/Watchdog/WindowsWatchdog.cs index 45c11df84d..e797127e3b 100644 --- a/src/Tgstation.Server.Host/Components/Watchdog/WindowsWatchdog.cs +++ b/src/Tgstation.Server.Host/Components/Watchdog/WindowsWatchdog.cs @@ -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 { diff --git a/src/Tgstation.Server.Host/Components/Watchdog/WindowsWatchdogFactory.cs b/src/Tgstation.Server.Host/Components/Watchdog/WindowsWatchdogFactory.cs index 8d8285e06a..7909be59af 100644 --- a/src/Tgstation.Server.Host/Components/Watchdog/WindowsWatchdogFactory.cs +++ b/src/Tgstation.Server.Host/Components/Watchdog/WindowsWatchdogFactory.cs @@ -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 { diff --git a/src/Tgstation.Server.Host/Controllers/AdministrationController.cs b/src/Tgstation.Server.Host/Controllers/AdministrationController.cs index df0bb5f2a7..b8e50b458c 100644 --- a/src/Tgstation.Server.Host/Controllers/AdministrationController.cs +++ b/src/Tgstation.Server.Host/Controllers/AdministrationController.cs @@ -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 { diff --git a/src/Tgstation.Server.Host/Controllers/ApiController.cs b/src/Tgstation.Server.Host/Controllers/ApiController.cs index fdea1e26f0..f983eaa2b9 100644 --- a/src/Tgstation.Server.Host/Controllers/ApiController.cs +++ b/src/Tgstation.Server.Host/Controllers/ApiController.cs @@ -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) { diff --git a/src/Tgstation.Server.Host/Controllers/BridgeController.cs b/src/Tgstation.Server.Host/Controllers/BridgeController.cs index 275cebe60f..32bbee7fee 100644 --- a/src/Tgstation.Server.Host/Controllers/BridgeController.cs +++ b/src/Tgstation.Server.Host/Controllers/BridgeController.cs @@ -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 diff --git a/src/Tgstation.Server.Host/Controllers/ByondController.cs b/src/Tgstation.Server.Host/Controllers/ByondController.cs index 4990d25c6d..1e561025a5 100644 --- a/src/Tgstation.Server.Host/Controllers/ByondController.cs +++ b/src/Tgstation.Server.Host/Controllers/ByondController.cs @@ -37,27 +37,34 @@ namespace Tgstation.Server.Host.Controllers /// readonly IFileTransferTicketProvider fileTransferService; + /// + /// Remove the from a given if present. + /// + /// The to normalize. + /// The normalized . May be a reference to . + static Version NormalizeVersion(Version version) => version.Build == 0 ? new Version(version.Major, version.Minor) : version; + /// /// Initializes a new instance of the class. /// - /// The for the . - /// The for the . + /// The for the . + /// The for the . + /// The for the . /// The for the . /// The value of . /// The value of . - /// The for the . public ByondController( IDatabaseContext databaseContext, IAuthenticationContextFactory authenticationContextFactory, + ILogger logger, IInstanceManager instanceManager, IJobManager jobManager, - IFileTransferTicketProvider fileTransferService, - ILogger 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 /// /// Changes the active BYOND version to the one specified in a given . /// - /// The to switch to. + /// The containing the to switch to. /// The for the operation. /// A resulting in the for the operation. /// Switched active version successfully. @@ -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); - }) - ; + }); + } + + /// + /// Changes the active BYOND version to the one specified in a given . + /// + /// The containing the to delete. + /// The for the operation. + /// A resulting in the for the operation. + /// Created to delete target version successfully. + /// Attempted to delete the active BYOND . + /// The specified was not installed. + [HttpDelete] + [TgsAuthorize(ByondRights.DeleteInstall)] + [ProducesResponseType(typeof(JobResponse), 202)] + [ProducesResponseType(typeof(ErrorMessageResponse), 409)] + [ProducesResponseType(typeof(ErrorMessageResponse), 410)] + public async Task 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( + Conflict(new ErrorMessageResponse(ErrorCode.ByondCannotDeleteActiveVersion))); + + var versionNotInstalled = !byondManager.InstalledVersions.Any(x => x == version); + + return Task.FromResult( + 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); } } } diff --git a/src/Tgstation.Server.Host/Controllers/ChatController.cs b/src/Tgstation.Server.Host/Controllers/ChatController.cs index 21c5c72627..b71f9817ae 100644 --- a/src/Tgstation.Server.Host/Controllers/ChatController.cs +++ b/src/Tgstation.Server.Host/Controllers/ChatController.cs @@ -37,20 +37,20 @@ namespace Tgstation.Server.Host.Controllers /// /// Initializes a new instance of the class. /// - /// The for the . - /// The for the . + /// The for the . + /// The for the . + /// The for the . /// The for the . - /// The for the . public ChatController( IDatabaseContext databaseContext, IAuthenticationContextFactory authenticationContextFactory, - IInstanceManager instanceManager, - ILogger logger) + ILogger logger, + IInstanceManager instanceManager) : base( - instanceManager, databaseContext, authenticationContextFactory, - logger) + logger, + instanceManager) { } diff --git a/src/Tgstation.Server.Host/Controllers/ComponentInterfacingController.cs b/src/Tgstation.Server.Host/Controllers/ComponentInterfacingController.cs new file mode 100644 index 0000000000..46874a4142 --- /dev/null +++ b/src/Tgstation.Server.Host/Controllers/ComponentInterfacingController.cs @@ -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 +{ + /// + /// for operations on s. + /// + public abstract class ComponentInterfacingController : ApiController + { + /// + /// Access the instance. + /// + public IInstanceOperations InstanceOperations => instanceManager; + + /// + /// The for the . + /// + readonly IInstanceManager instanceManager; + + /// + /// If the header should be checked and used to perform validation for every request. + /// + readonly bool useInstanceRequestHeader; + + /// + /// Initializes a new instance of the class. + /// + /// The value of . + /// The for the . + /// The for the . + /// The for the . + /// The value of . + protected ComponentInterfacingController( + IDatabaseContext databaseContext, + IAuthenticationContextFactory authenticationContextFactory, + ILogger logger, + IInstanceManager instanceManager, + bool useInstanceRequestHeader = false) + : base( + databaseContext, + authenticationContextFactory, + logger, + true) + { + this.instanceManager = instanceManager ?? throw new ArgumentNullException(nameof(instanceManager)); + this.useInstanceRequestHeader = useInstanceRequestHeader; + } + + /// + protected override async Task 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; + } + + /// + /// Corrects discrepencies between the status of s in the database vs the service. + /// + /// The to check. + /// if an unsaved DB update was made, otherwise. + 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; + } + + /// + /// Run a given with the relevant . + /// + /// A accepting the and returning a with the . + /// A resulting in the that should be returned. + /// The context of should be as small as possible so as to avoid race conditions. This function can return a if the requested instance was offline. + protected async Task WithComponentInstance(Func> 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); + } + } + } +} diff --git a/src/Tgstation.Server.Host/Controllers/ConfigurationController.cs b/src/Tgstation.Server.Host/Controllers/ConfigurationController.cs index 71b132b816..cc401330f4 100644 --- a/src/Tgstation.Server.Host/Controllers/ConfigurationController.cs +++ b/src/Tgstation.Server.Host/Controllers/ConfigurationController.cs @@ -34,22 +34,22 @@ namespace Tgstation.Server.Host.Controllers /// /// Initializes a new instance of the class. /// - /// The for the . - /// The for the . + /// The for the . + /// The for the . + /// The for the . /// The for the . /// The value of . - /// The for the . public ConfigurationController( IDatabaseContext databaseContext, IAuthenticationContextFactory authenticationContextFactory, + ILogger logger, IInstanceManager instanceManager, - IIOManager ioManager, - ILogger logger) + IIOManager ioManager) : base( - instanceManager, databaseContext, authenticationContextFactory, - logger) + logger, + instanceManager) { this.ioManager = ioManager ?? throw new ArgumentNullException(nameof(ioManager)); } diff --git a/src/Tgstation.Server.Host/Controllers/DreamDaemonController.cs b/src/Tgstation.Server.Host/Controllers/DreamDaemonController.cs index c4af8f06d1..068b65d825 100644 --- a/src/Tgstation.Server.Host/Controllers/DreamDaemonController.cs +++ b/src/Tgstation.Server.Host/Controllers/DreamDaemonController.cs @@ -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 /// /// Initializes a new instance of the class. /// - /// The for the . - /// The for the . - /// The value of . + /// The for the . + /// The for the . + /// The for the . /// The for the . + /// The value of . /// The value of . - /// The for the . public DreamDaemonController( IDatabaseContext databaseContext, IAuthenticationContextFactory authenticationContextFactory, - IJobManager jobManager, + ILogger logger, IInstanceManager instanceManager, - IPortAllocator portAllocator, - ILogger 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)); diff --git a/src/Tgstation.Server.Host/Controllers/DreamMakerController.cs b/src/Tgstation.Server.Host/Controllers/DreamMakerController.cs index 4fad8b743a..acd862ee72 100644 --- a/src/Tgstation.Server.Host/Controllers/DreamMakerController.cs +++ b/src/Tgstation.Server.Host/Controllers/DreamMakerController.cs @@ -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 /// /// Initializes a new instance of the class. /// - /// The for the . - /// The for the . - /// The value of . + /// The for the . + /// The for the . + /// The for the . /// The for the . + /// The value of . /// The value of . - /// The for the . public DreamMakerController( IDatabaseContext databaseContext, IAuthenticationContextFactory authenticationContextFactory, - IJobManager jobManager, + ILogger logger, IInstanceManager instanceManager, - IPortAllocator portAllocator, - ILogger 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)); diff --git a/src/Tgstation.Server.Host/Controllers/InstanceController.cs b/src/Tgstation.Server.Host/Controllers/InstanceController.cs index 55862af80f..60be70f7d2 100644 --- a/src/Tgstation.Server.Host/Controllers/InstanceController.cs +++ b/src/Tgstation.Server.Host/Controllers/InstanceController.cs @@ -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 /// [Route(Routes.InstanceManager)] #pragma warning disable CA1506 // TODO: Decomplexify - public sealed class InstanceController : ApiController + public sealed class InstanceController : ComponentInterfacingController { /// /// File name to allow attaching instances. @@ -51,11 +51,6 @@ namespace Tgstation.Server.Host.Controllers /// readonly IJobManager jobManager; - /// - /// The for the . - /// - readonly IInstanceManager instanceManager; - /// /// The for the . /// @@ -84,35 +79,34 @@ namespace Tgstation.Server.Host.Controllers /// /// Initializes a new instance of the class. /// - /// The for the . - /// The for the . + /// The for the . + /// The for the . + /// The for the . + /// The for the . /// The value of . - /// The value of . /// The value of . /// The value of . /// The value of . /// The containing the value of . /// The containing the value of . - /// The for the . public InstanceController( IDatabaseContext databaseContext, IAuthenticationContextFactory authenticationContextFactory, - IJobManager jobManager, + ILogger logger, IInstanceManager instanceManager, + IJobManager jobManager, IIOManager ioManager, IPortAllocator portAllocator, IPlatformIdentifier platformIdentifier, IOptions generalConfigurationOptions, - IOptions swarmConfigurationOptions, - ILogger logger) + IOptions 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 && diff --git a/src/Tgstation.Server.Host/Controllers/InstancePermissionSetController.cs b/src/Tgstation.Server.Host/Controllers/InstancePermissionSetController.cs index 37acb2e8d8..c04d169e3d 100644 --- a/src/Tgstation.Server.Host/Controllers/InstancePermissionSetController.cs +++ b/src/Tgstation.Server.Host/Controllers/InstancePermissionSetController.cs @@ -30,20 +30,20 @@ namespace Tgstation.Server.Host.Controllers /// /// Initializes a new instance of the class. /// + /// The for the . + /// The for the . + /// The for the . /// The for the . - /// The for the . - /// The for the . - /// The for the . public InstancePermissionSetController( - IInstanceManager instanceManager, IDatabaseContext databaseContext, IAuthenticationContextFactory authenticationContextFactory, - ILogger logger) + ILogger 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(); } } } diff --git a/src/Tgstation.Server.Host/Controllers/InstanceRequiredController.cs b/src/Tgstation.Server.Host/Controllers/InstanceRequiredController.cs index 0c10d676df..1f119500f5 100644 --- a/src/Tgstation.Server.Host/Controllers/InstanceRequiredController.cs +++ b/src/Tgstation.Server.Host/Controllers/InstanceRequiredController.cs @@ -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 { /// - /// for operations on an . + /// for operations that require an instance. /// - public abstract class InstanceRequiredController : ApiController + public abstract class InstanceRequiredController : ComponentInterfacingController { - /// - /// The for the . - /// - readonly IInstanceManager instanceManager; - - /// - /// Corrects discrepencies between the status of s in the database vs the service. - /// - /// The to use. - /// The to use. - /// The to check. - /// if an unsaved DB update was made, otherwise. - 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; - } - /// /// Initializes a new instance of the class. /// - /// The value of . - /// The for the . - /// The for the . - /// The for the . + /// The for the . + /// The for the . + /// The for the . + /// The for the . protected InstanceRequiredController( - IInstanceManager instanceManager, IDatabaseContext databaseContext, IAuthenticationContextFactory authenticationContextFactory, - ILogger logger) + ILogger logger, + IInstanceManager instanceManager) : base( databaseContext, authenticationContextFactory, logger, + instanceManager, true) { - this.instanceManager = instanceManager ?? throw new ArgumentNullException(nameof(instanceManager)); - } - - /// - protected override async Task 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; - } - - /// - /// Run a given with the relevant . - /// - /// A accepting the and returning a with the . - /// A resulting in the that should be returned. - /// The context of should be as small as possible so as to avoid race conditions. - protected async Task WithComponentInstance(Func> 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); - } } } } diff --git a/src/Tgstation.Server.Host/Controllers/JobController.cs b/src/Tgstation.Server.Host/Controllers/JobController.cs index d67c7204b7..577c3113bb 100644 --- a/src/Tgstation.Server.Host/Controllers/JobController.cs +++ b/src/Tgstation.Server.Host/Controllers/JobController.cs @@ -32,22 +32,22 @@ namespace Tgstation.Server.Host.Controllers /// /// Initializes a new instance of the class. /// + /// The for the . + /// The for the . + /// The for the . /// The for the . - /// The for the . - /// The for the . /// The value of . - /// The for the . public JobController( - IInstanceManager instanceManager, IDatabaseContext databaseContext, IAuthenticationContextFactory authenticationContextFactory, - IJobManager jobManager, - ILogger logger) + ILogger logger, + IInstanceManager instanceManager, + IJobManager jobManager) : base( - instanceManager, databaseContext, authenticationContextFactory, - logger) + logger, + instanceManager) { this.jobManager = jobManager ?? throw new ArgumentNullException(nameof(jobManager)); } diff --git a/src/Tgstation.Server.Host/Controllers/RepositoryController.cs b/src/Tgstation.Server.Host/Controllers/RepositoryController.cs index 27bacaab39..269165b4fd 100644 --- a/src/Tgstation.Server.Host/Controllers/RepositoryController.cs +++ b/src/Tgstation.Server.Host/Controllers/RepositoryController.cs @@ -44,24 +44,24 @@ namespace Tgstation.Server.Host.Controllers /// /// Initializes a new instance of the class. /// - /// The for the . - /// The for the . + /// The for the . + /// The for the . + /// The for the . /// The for the . /// The value of . /// The value of . - /// The for the . public RepositoryController( IDatabaseContext databaseContext, IAuthenticationContextFactory authenticationContextFactory, + ILogger logger, IInstanceManager instanceManager, ILoggerFactory loggerFactory, - IJobManager jobManager, - ILogger 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 diff --git a/src/Tgstation.Server.Host/Controllers/SwarmController.cs b/src/Tgstation.Server.Host/Controllers/SwarmController.cs index abd329de50..c90b381fa7 100644 --- a/src/Tgstation.Server.Host/Controllers/SwarmController.cs +++ b/src/Tgstation.Server.Host/Controllers/SwarmController.cs @@ -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 /// 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) diff --git a/src/Tgstation.Server.Host/Core/Application.cs b/src/Tgstation.Server.Host/Core/Application.cs index eb88be12f7..9ce061cd03 100644 --- a/src/Tgstation.Server.Host/Core/Application.cs +++ b/src/Tgstation.Server.Host/Core/Application.cs @@ -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 /// The to configure. /// The for the . /// The value of . - /// The . /// The . /// The . /// The containing the to use. /// The containing the to use. + /// The containing the to use. /// The for the . public void Configure( IApplicationBuilder applicationBuilder, IServerControl serverControl, ITokenFactory tokenFactory, - IInstanceManager instanceManager, IServerPortProvider serverPortProvider, IAssemblyInformationProvider assemblyInformationProvider, IOptions controlPanelConfigurationOptions, IOptions generalConfigurationOptions, + IOptions swarmConfigurationOptions, ILogger 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( + (instanceManager, cancellationToken) => instanceManager.Ready.WithToken(cancellationToken)); + if (generalConfiguration.HostApiDocumentation) { applicationBuilder.UseSwagger(); diff --git a/src/Tgstation.Server.Host/Core/ServerUpdater.cs b/src/Tgstation.Server.Host/Core/ServerUpdater.cs index d33ce063b0..ed9b5e1cbf 100644 --- a/src/Tgstation.Server.Host/Core/ServerUpdater.cs +++ b/src/Tgstation.Server.Host/Core/ServerUpdater.cs @@ -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 { diff --git a/src/Tgstation.Server.Host/Extensions/ApplicationBuilderExtensions.cs b/src/Tgstation.Server.Host/Extensions/ApplicationBuilderExtensions.cs index ca1369dc46..807cca74f3 100644 --- a/src/Tgstation.Server.Host/Extensions/ApplicationBuilderExtensions.cs +++ b/src/Tgstation.Server.Host/Extensions/ApplicationBuilderExtensions.cs @@ -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 /// static class ApplicationBuilderExtensions { + /// + /// If the server's swarm identifier should be pushed onto the log context for all requests. + /// + internal static bool LogSwarmIdentifier { get; set; } + /// /// Return a for s. /// @@ -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 }); } + /// + /// Adds additional global to the request pipeline. + /// + /// The to configure. + /// The . + 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(); + }); + } + /// /// Gets a from a given . /// diff --git a/src/Tgstation.Server.Host/Extensions/ServiceCollectionExtensions.cs b/src/Tgstation.Server.Host/Extensions/ServiceCollectionExtensions.cs index a5cd24ca6f..46bb712d39 100644 --- a/src/Tgstation.Server.Host/Extensions/ServiceCollectionExtensions.cs +++ b/src/Tgstation.Server.Host/Extensions/ServiceCollectionExtensions.cs @@ -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 /// static class ServiceCollectionExtensions { - /// - /// Common template used for adding our custom log context to serilog. - /// - /// Should not be changed. Only mutable for the sake of identifying swarm nodes under a single test environment - public static string SerilogContextTemplate { get; set; } - - /// - /// Initializes static members of the class. - /// - static ServiceCollectionExtensions() - { - SerilogContextTemplate = "(Instance:{Instance}|Job:{Job}|Request:{Request}|User:{User}|Monitor:{Monitor}|Bridge:{Bridge}|Chat:{ChatMessage}"; - } - /// /// Add a standard binding. /// @@ -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); }); diff --git a/src/Tgstation.Server.Host/Extensions/WebHostBuilderExtensions.cs b/src/Tgstation.Server.Host/Extensions/WebHostBuilderExtensions.cs index cf76ba515d..d93bbff7a7 100644 --- a/src/Tgstation.Server.Host/Extensions/WebHostBuilderExtensions.cs +++ b/src/Tgstation.Server.Host/Extensions/WebHostBuilderExtensions.cs @@ -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(), applicationBuilder.ApplicationServices.GetRequiredService(), - applicationBuilder.ApplicationServices.GetRequiredService(), applicationBuilder.ApplicationServices.GetRequiredService(), applicationBuilder.ApplicationServices.GetRequiredService(), applicationBuilder.ApplicationServices.GetRequiredService>(), applicationBuilder.ApplicationServices.GetRequiredService>(), + applicationBuilder.ApplicationServices.GetRequiredService>(), applicationBuilder.ApplicationServices.GetRequiredService>()); } } diff --git a/src/Tgstation.Server.Host/IO/DefaultIOManager.cs b/src/Tgstation.Server.Host/IO/DefaultIOManager.cs index 75f0c66f1d..d3d36804e3 100644 --- a/src/Tgstation.Server.Host/IO/DefaultIOManager.cs +++ b/src/Tgstation.Server.Host/IO/DefaultIOManager.cs @@ -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 { diff --git a/src/Tgstation.Server.Host/IO/FileDownloader.cs b/src/Tgstation.Server.Host/IO/FileDownloader.cs index 976f4d7ec7..5cd08b7635 100644 --- a/src/Tgstation.Server.Host/IO/FileDownloader.cs +++ b/src/Tgstation.Server.Host/IO/FileDownloader.cs @@ -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 { diff --git a/src/Tgstation.Server.Host/Jobs/JobManager.cs b/src/Tgstation.Server.Host/Jobs/JobManager.cs index 73b66dbcc4..dbf9352ea7 100644 --- a/src/Tgstation.Server.Host/Jobs/JobManager.cs +++ b/src/Tgstation.Server.Host/Jobs/JobManager.cs @@ -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 /// A representing the running operation. 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(), null, UpdateProgress), - cancellationToken) - ; + cancellationToken); logger.LogDebug("Job {jobId} completed!", job.Id); } diff --git a/src/Tgstation.Server.Host/Jobs/JobProgressReporter.cs b/src/Tgstation.Server.Host/Jobs/JobProgressReporter.cs index afa250e2aa..4d23385e63 100644 --- a/src/Tgstation.Server.Host/Jobs/JobProgressReporter.cs +++ b/src/Tgstation.Server.Host/Jobs/JobProgressReporter.cs @@ -14,7 +14,18 @@ namespace Tgstation.Server.Host.Jobs /// /// The name of the current stage. /// - public string StageName { get; set; } + public string StageName + { + get => stageName; + set + { + if (stageName == value) + return; + + stageName = value; + callback(stageName, lastProgress); + } + } /// /// The for the . @@ -26,6 +37,16 @@ namespace Tgstation.Server.Host.Jobs /// readonly Action callback; + /// + /// Backing field for . + /// + string stageName; + + /// + /// The last progress value pushed into the . + /// + double? lastProgress; + /// /// The total progress reported so far in this section. /// @@ -66,6 +87,7 @@ namespace Tgstation.Server.Host.Jobs sectionProgression = progress.Value; callback(StageName, clampedProgress); + lastProgress = clampedProgress; } /// diff --git a/src/Tgstation.Server.Host/Models/Job.cs b/src/Tgstation.Server.Host/Models/Job.cs index d956711281..efa3268250 100644 --- a/src/Tgstation.Server.Host/Models/Job.cs +++ b/src/Tgstation.Server.Host/Models/Job.cs @@ -27,7 +27,7 @@ namespace Tgstation.Server.Host.Models public Instance Instance { get; set; } /// - public JobResponse ToApi() => new JobResponse + public JobResponse ToApi() => new () { Id = Id, StartedAt = StartedAt, diff --git a/src/Tgstation.Server.Host/Security/IdentityCache.cs b/src/Tgstation.Server.Host/Security/IdentityCache.cs index 244c23a585..74eb439f56 100644 --- a/src/Tgstation.Server.Host/Security/IdentityCache.cs +++ b/src/Tgstation.Server.Host/Security/IdentityCache.cs @@ -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 { diff --git a/src/Tgstation.Server.Host/Security/IdentityCacheObject.cs b/src/Tgstation.Server.Host/Security/IdentityCacheObject.cs index 273fc173d3..35a576a18a 100644 --- a/src/Tgstation.Server.Host/Security/IdentityCacheObject.cs +++ b/src/Tgstation.Server.Host/Security/IdentityCacheObject.cs @@ -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 { diff --git a/src/Tgstation.Server.Host/Security/OAuth/DiscordOAuthValidator.cs b/src/Tgstation.Server.Host/Security/OAuth/DiscordOAuthValidator.cs index f147d20696..1f6d9c751e 100644 --- a/src/Tgstation.Server.Host/Security/OAuth/DiscordOAuthValidator.cs +++ b/src/Tgstation.Server.Host/Security/OAuth/DiscordOAuthValidator.cs @@ -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 { diff --git a/src/Tgstation.Server.Host/Security/OAuth/GenericOAuthValidator.cs b/src/Tgstation.Server.Host/Security/OAuth/GenericOAuthValidator.cs index a88003bb46..50709b5522 100644 --- a/src/Tgstation.Server.Host/Security/OAuth/GenericOAuthValidator.cs +++ b/src/Tgstation.Server.Host/Security/OAuth/GenericOAuthValidator.cs @@ -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 { diff --git a/src/Tgstation.Server.Host/Security/OAuth/GitHubOAuthValidator.cs b/src/Tgstation.Server.Host/Security/OAuth/GitHubOAuthValidator.cs index e35d4ab59c..3991fc33f9 100644 --- a/src/Tgstation.Server.Host/Security/OAuth/GitHubOAuthValidator.cs +++ b/src/Tgstation.Server.Host/Security/OAuth/GitHubOAuthValidator.cs @@ -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 { diff --git a/src/Tgstation.Server.Host/Security/OAuth/InvisionCommunityOAuthValidator.cs b/src/Tgstation.Server.Host/Security/OAuth/InvisionCommunityOAuthValidator.cs index d310e026f4..a7568e47a7 100644 --- a/src/Tgstation.Server.Host/Security/OAuth/InvisionCommunityOAuthValidator.cs +++ b/src/Tgstation.Server.Host/Security/OAuth/InvisionCommunityOAuthValidator.cs @@ -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 { diff --git a/src/Tgstation.Server.Host/Security/OAuth/KeycloakOAuthValidator.cs b/src/Tgstation.Server.Host/Security/OAuth/KeycloakOAuthValidator.cs index 6d168aa471..74e87cb819 100644 --- a/src/Tgstation.Server.Host/Security/OAuth/KeycloakOAuthValidator.cs +++ b/src/Tgstation.Server.Host/Security/OAuth/KeycloakOAuthValidator.cs @@ -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 { diff --git a/src/Tgstation.Server.Host/Security/OAuth/OAuthProviders.cs b/src/Tgstation.Server.Host/Security/OAuth/OAuthProviders.cs index 298d9b25e1..a217de3935 100644 --- a/src/Tgstation.Server.Host/Security/OAuth/OAuthProviders.cs +++ b/src/Tgstation.Server.Host/Security/OAuth/OAuthProviders.cs @@ -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 { diff --git a/src/Tgstation.Server.Host/Security/OAuth/TGForumsOAuthValidator.cs b/src/Tgstation.Server.Host/Security/OAuth/TGForumsOAuthValidator.cs index 91d21a5291..767e37f6e6 100644 --- a/src/Tgstation.Server.Host/Security/OAuth/TGForumsOAuthValidator.cs +++ b/src/Tgstation.Server.Host/Security/OAuth/TGForumsOAuthValidator.cs @@ -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 { diff --git a/src/Tgstation.Server.Host/Security/TokenFactory.cs b/src/Tgstation.Server.Host/Security/TokenFactory.cs index 737e7ba0e2..bcca46c607 100644 --- a/src/Tgstation.Server.Host/Security/TokenFactory.cs +++ b/src/Tgstation.Server.Host/Security/TokenFactory.cs @@ -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 { diff --git a/src/Tgstation.Server.Host/Setup/SetupApplication.cs b/src/Tgstation.Server.Host/Setup/SetupApplication.cs index ba63302e95..75c41b888d 100644 --- a/src/Tgstation.Server.Host/Setup/SetupApplication.cs +++ b/src/Tgstation.Server.Host/Setup/SetupApplication.cs @@ -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 { diff --git a/src/Tgstation.Server.Host/Setup/SetupWizard.cs b/src/Tgstation.Server.Host/Setup/SetupWizard.cs index b446cd81b2..629dbf0a41 100644 --- a/src/Tgstation.Server.Host/Setup/SetupWizard.cs +++ b/src/Tgstation.Server.Host/Setup/SetupWizard.cs @@ -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 diff --git a/src/Tgstation.Server.Host/Swarm/SwarmService.cs b/src/Tgstation.Server.Host/Swarm/SwarmService.cs index 023e0b9273..af907c1985 100644 --- a/src/Tgstation.Server.Host/Swarm/SwarmService.cs +++ b/src/Tgstation.Server.Host/Swarm/SwarmService.cs @@ -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 { diff --git a/src/Tgstation.Server.Host/System/PosixSignalHandler.cs b/src/Tgstation.Server.Host/System/PosixSignalHandler.cs index d095175c2f..93df0ae593 100644 --- a/src/Tgstation.Server.Host/System/PosixSignalHandler.cs +++ b/src/Tgstation.Server.Host/System/PosixSignalHandler.cs @@ -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 { diff --git a/src/Tgstation.Server.Host/System/WindowsNetworkPromptReaper.cs b/src/Tgstation.Server.Host/System/WindowsNetworkPromptReaper.cs index d1da25f7b2..dbf6db58b9 100644 --- a/src/Tgstation.Server.Host/System/WindowsNetworkPromptReaper.cs +++ b/src/Tgstation.Server.Host/System/WindowsNetworkPromptReaper.cs @@ -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 { diff --git a/src/Tgstation.Server.Host/Transfer/FileTransferService.cs b/src/Tgstation.Server.Host/Transfer/FileTransferService.cs index 31d799634a..4aba111804 100644 --- a/src/Tgstation.Server.Host/Transfer/FileTransferService.cs +++ b/src/Tgstation.Server.Host/Transfer/FileTransferService.cs @@ -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(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(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(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); } diff --git a/src/Tgstation.Server.Host/Core/AbstractHttpClientFactory.cs b/src/Tgstation.Server.Host/Utils/AbstractHttpClientFactory.cs similarity index 98% rename from src/Tgstation.Server.Host/Core/AbstractHttpClientFactory.cs rename to src/Tgstation.Server.Host/Utils/AbstractHttpClientFactory.cs index 607b0dc760..73d2218ed8 100644 --- a/src/Tgstation.Server.Host/Core/AbstractHttpClientFactory.cs +++ b/src/Tgstation.Server.Host/Utils/AbstractHttpClientFactory.cs @@ -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 { /// sealed class AbstractHttpClientFactory : IAbstractHttpClientFactory diff --git a/src/Tgstation.Server.Host/Core/AsyncDelayer.cs b/src/Tgstation.Server.Host/Utils/AsyncDelayer.cs similarity index 88% rename from src/Tgstation.Server.Host/Core/AsyncDelayer.cs rename to src/Tgstation.Server.Host/Utils/AsyncDelayer.cs index 1e874366d3..79ffce1838 100644 --- a/src/Tgstation.Server.Host/Core/AsyncDelayer.cs +++ b/src/Tgstation.Server.Host/Utils/AsyncDelayer.cs @@ -2,7 +2,7 @@ using System.Threading; using System.Threading.Tasks; -namespace Tgstation.Server.Host.Core +namespace Tgstation.Server.Host.Utils { /// sealed class AsyncDelayer : IAsyncDelayer diff --git a/src/Tgstation.Server.Host/Core/GitHubClientFactory.cs b/src/Tgstation.Server.Host/Utils/GitHubClientFactory.cs similarity index 96% rename from src/Tgstation.Server.Host/Core/GitHubClientFactory.cs rename to src/Tgstation.Server.Host/Utils/GitHubClientFactory.cs index 7d76cb6bff..9753f3ad7b 100644 --- a/src/Tgstation.Server.Host/Core/GitHubClientFactory.cs +++ b/src/Tgstation.Server.Host/Utils/GitHubClientFactory.cs @@ -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 { /// 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; diff --git a/src/Tgstation.Server.Host/Core/IAsyncDelayer.cs b/src/Tgstation.Server.Host/Utils/IAsyncDelayer.cs similarity index 94% rename from src/Tgstation.Server.Host/Core/IAsyncDelayer.cs rename to src/Tgstation.Server.Host/Utils/IAsyncDelayer.cs index 377af13a68..d47c5cbc62 100644 --- a/src/Tgstation.Server.Host/Core/IAsyncDelayer.cs +++ b/src/Tgstation.Server.Host/Utils/IAsyncDelayer.cs @@ -2,7 +2,7 @@ using System.Threading; using System.Threading.Tasks; -namespace Tgstation.Server.Host.Core +namespace Tgstation.Server.Host.Utils { /// /// For waiting asynchronously. diff --git a/src/Tgstation.Server.Host/Core/IGitHubClientFactory.cs b/src/Tgstation.Server.Host/Utils/IGitHubClientFactory.cs similarity index 94% rename from src/Tgstation.Server.Host/Core/IGitHubClientFactory.cs rename to src/Tgstation.Server.Host/Utils/IGitHubClientFactory.cs index 2f3010a413..636796a284 100644 --- a/src/Tgstation.Server.Host/Core/IGitHubClientFactory.cs +++ b/src/Tgstation.Server.Host/Utils/IGitHubClientFactory.cs @@ -1,6 +1,6 @@ using Octokit; -namespace Tgstation.Server.Host.Core +namespace Tgstation.Server.Host.Utils { /// /// For creating s. diff --git a/src/Tgstation.Server.Host/Core/IPortAllocator.cs b/src/Tgstation.Server.Host/Utils/IPortAllocator.cs similarity index 95% rename from src/Tgstation.Server.Host/Core/IPortAllocator.cs rename to src/Tgstation.Server.Host/Utils/IPortAllocator.cs index b2b2697aa4..865107bb1a 100644 --- a/src/Tgstation.Server.Host/Core/IPortAllocator.cs +++ b/src/Tgstation.Server.Host/Utils/IPortAllocator.cs @@ -1,7 +1,7 @@ using System.Threading; using System.Threading.Tasks; -namespace Tgstation.Server.Host.Core +namespace Tgstation.Server.Host.Utils { /// /// Gets unassigned ports for use by TGS. diff --git a/src/Tgstation.Server.Host/Core/OpenApiEnumVarNamesExtension.cs b/src/Tgstation.Server.Host/Utils/OpenApiEnumVarNamesExtension.cs similarity index 98% rename from src/Tgstation.Server.Host/Core/OpenApiEnumVarNamesExtension.cs rename to src/Tgstation.Server.Host/Utils/OpenApiEnumVarNamesExtension.cs index 287c963620..538f4ea84e 100644 --- a/src/Tgstation.Server.Host/Core/OpenApiEnumVarNamesExtension.cs +++ b/src/Tgstation.Server.Host/Utils/OpenApiEnumVarNamesExtension.cs @@ -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 { /// /// Implements the "x-enum-varnames" OpenAPI 3.0 extension. diff --git a/src/Tgstation.Server.Host/Core/PortAllocator.cs b/src/Tgstation.Server.Host/Utils/PortAllocator.cs similarity index 96% rename from src/Tgstation.Server.Host/Core/PortAllocator.cs rename to src/Tgstation.Server.Host/Utils/PortAllocator.cs index 31309deed8..87ac3ecd05 100644 --- a/src/Tgstation.Server.Host/Core/PortAllocator.cs +++ b/src/Tgstation.Server.Host/Utils/PortAllocator.cs @@ -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 { /// 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; diff --git a/src/Tgstation.Server.Host/Utils/ReferenceCounter.cs b/src/Tgstation.Server.Host/Utils/ReferenceCounter.cs new file mode 100644 index 0000000000..95e03ddca6 --- /dev/null +++ b/src/Tgstation.Server.Host/Utils/ReferenceCounter.cs @@ -0,0 +1,101 @@ +using System; + +namespace Tgstation.Server.Host.Utils +{ + /// + /// Class used for counting references with . + /// + /// The reference . + abstract class ReferenceCounter : IDisposable + where TInstance : class + { + /// + /// The referenced . + /// + protected TInstance Instance => actualInstance ?? throw UninitializedOrDisposedException(); + + /// + /// The object for and . + /// + readonly object initDisposeLock; + + /// + /// Backing field for . + /// + TInstance actualInstance; + + /// + /// The to take when is called. + /// + Action referenceCleanupAction; + + /// + /// If the was initialized. + /// + bool initialized; + + /// + /// Initializes a new instance of the class. + /// + protected ReferenceCounter() + { + initDisposeLock = new object(); + } + + /// + public void Dispose() + { + lock (initDisposeLock) + { + referenceCleanupAction?.Invoke(); + referenceCleanupAction = null; + actualInstance = null; + } + } + + /// + /// Initialize the . + /// + /// The reference counted . + /// The to take to clean up the reference. + 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)} already initialized!"); + + actualInstance = instance; + this.referenceCleanupAction = referenceCleanupAction; + initialized = true; + } + } + + /// + /// Prevents the aquired reference from being dropped when is called. + /// + /// This will prevent from ever completing. + protected void DangerousDropReference() + { + referenceCleanupAction = null; + } + + /// + /// Throw the appropriate when the is uninitialized or disposed. + /// + /// A new to throw. + InvalidOperationException UninitializedOrDisposedException() + { + if (initialized) + return new ObjectDisposedException(nameof(ReferenceCounter)); + + return new InvalidOperationException($"{nameof(ReferenceCounter)} not initialized!"); + } + } +} diff --git a/src/Tgstation.Server.Host/Components/InstanceContainer.cs b/src/Tgstation.Server.Host/Utils/ReferenceCountingContainer.cs similarity index 54% rename from src/Tgstation.Server.Host/Components/InstanceContainer.cs rename to src/Tgstation.Server.Host/Utils/ReferenceCountingContainer.cs index 6a8c49500e..121411bd1d 100644 --- a/src/Tgstation.Server.Host/Components/InstanceContainer.cs +++ b/src/Tgstation.Server.Host/Utils/ReferenceCountingContainer.cs @@ -1,20 +1,24 @@ using System; using System.Threading.Tasks; -namespace Tgstation.Server.Host.Components +namespace Tgstation.Server.Host.Utils { /// - /// Wrapper for managing s. + /// Wrapper for managing some . /// - sealed class InstanceContainer + /// The type being wrapped. + /// The disposable reference type returned. + sealed class ReferenceCountingContainer + where TWrapped : class + where TReference : ReferenceCounter, new() { /// - /// The . + /// The . /// - public IInstance Instance { get; } + public TWrapped Instance { get; } /// - /// A that completes when there are no s active for the . + /// A that completes when there are no s active for the . /// public Task OnZeroReferences { @@ -40,15 +44,15 @@ namespace Tgstation.Server.Host.Components TaskCompletionSource onZeroReferencesTcs; /// - /// Count of active s. + /// Count of active s. /// ulong referenceCount; /// - /// Initializes a new instance of the class. + /// Initializes a new instance of the class. /// /// The value of . - public InstanceContainer(IInstance instance) + public ReferenceCountingContainer(TWrapped instance) { Instance = instance ?? throw new ArgumentNullException(nameof(instance)); @@ -56,10 +60,10 @@ namespace Tgstation.Server.Host.Components } /// - /// Create a new . + /// Create a new to the . /// - /// A new . - public IInstanceReference AddReference() + /// A new . + 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 { diff --git a/src/Tgstation.Server.Host/Core/SemaphoreSlimContext.cs b/src/Tgstation.Server.Host/Utils/SemaphoreSlimContext.cs similarity index 97% rename from src/Tgstation.Server.Host/Core/SemaphoreSlimContext.cs rename to src/Tgstation.Server.Host/Utils/SemaphoreSlimContext.cs index d027e0bf81..06e4dbf9a9 100644 --- a/src/Tgstation.Server.Host/Core/SemaphoreSlimContext.cs +++ b/src/Tgstation.Server.Host/Utils/SemaphoreSlimContext.cs @@ -2,7 +2,7 @@ using System.Threading; using System.Threading.Tasks; -namespace Tgstation.Server.Host.Core +namespace Tgstation.Server.Host.Utils { /// /// Async lock context helper. diff --git a/src/Tgstation.Server.Host/Utils/SerilogContextHelper.cs b/src/Tgstation.Server.Host/Utils/SerilogContextHelper.cs new file mode 100644 index 0000000000..e4398a357b --- /dev/null +++ b/src/Tgstation.Server.Host/Utils/SerilogContextHelper.cs @@ -0,0 +1,80 @@ +namespace Tgstation.Server.Host.Utils +{ + /// + /// Helpers for manipulating the . + /// + public static class SerilogContextHelper + { + /// + /// The property name for s. + /// + public const string InstanceIdContextProperty = "Instance"; + + /// + /// The property name for s. + /// + public const string JobIdContextProperty = "Job"; + + /// + /// The property name for s. + /// + public const string RequestPathContextProperty = "Request"; + + /// + /// The property name for s. + /// + public const string UserIdContextProperty = "User"; + + /// + /// The property name for the ID of the watchdog monitor iteration currently being processed. + /// + public const string WatchdogMonitorIterationContextProperty = "Monitor"; + + /// + /// The property name for the ID of the bridge request currently being processed. + /// + public const string BridgeRequestIterationContextProperty = "Bridge"; + + /// + /// The property name for the ID of the chat message currently being processed. + /// + public const string ChatMessageIterationContextProperty = "ChatMessage"; + + /// + /// The property name for s. + /// + public const string InstanceReferenceContextProperty = "InstanceReference"; + + /// + /// The property name for s. + /// + public const string SwarmIdentifierContextProperty = "Node"; + + /// + /// The default value of . + /// + const string DefaultTemplate = $"Instance:{{{InstanceIdContextProperty}}}|Job:{{{JobIdContextProperty}}}|Request:{{{RequestPathContextProperty}}}|User:{{{UserIdContextProperty}}}|Monitor:{{{WatchdogMonitorIterationContextProperty}}}|Bridge:{{{BridgeRequestIterationContextProperty}}}|Chat:{{{ChatMessageIterationContextProperty}}}|IR:{{{InstanceReferenceContextProperty}}}"; + + /// + /// Common template used for adding our custom log context to serilog. + /// + /// Should not be changed. Only mutable for the sake of identifying swarm nodes under a single test environment + public static string Template { get; private set; } + + /// + /// Initializes static members of the class. + /// + static SerilogContextHelper() + { + Template = DefaultTemplate; + } + + /// + /// Adds the placeholder for the to the . + /// + public static void AddSwarmNodeIdentifierToTemplate() + { + Template = $"{DefaultTemplate}|Node:{SwarmIdentifierContextProperty}"; + } + } +} diff --git a/src/Tgstation.Server.Host/Core/SwaggerConfiguration.cs b/src/Tgstation.Server.Host/Utils/SwaggerConfiguration.cs similarity index 99% rename from src/Tgstation.Server.Host/Core/SwaggerConfiguration.cs rename to src/Tgstation.Server.Host/Utils/SwaggerConfiguration.cs index 62912708b0..cd997b3796 100644 --- a/src/Tgstation.Server.Host/Core/SwaggerConfiguration.cs +++ b/src/Tgstation.Server.Host/Utils/SwaggerConfiguration.cs @@ -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 { /// /// Implements various filters for . diff --git a/tests/DMAPI/LongRunning/Test.dm b/tests/DMAPI/LongRunning/Test.dm index 96f4152bd5..14b927c7e8 100644 --- a/tests/DMAPI/LongRunning/Test.dm +++ b/tests/DMAPI/LongRunning/Test.dm @@ -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() diff --git a/tests/Tgstation.Server.Api.Tests/Rights/TestRights.cs b/tests/Tgstation.Server.Api.Tests/Rights/TestRights.cs index 0f03ab7ccb..62e41b7d98 100644 --- a/tests/Tgstation.Server.Api.Tests/Rights/TestRights.cs +++ b/tests/Tgstation.Server.Api.Tests/Rights/TestRights.cs @@ -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(); Assert.AreEqual(allByondRights, automaticByondRights); diff --git a/tests/Tgstation.Server.Host.Tests.Signals/Program.cs b/tests/Tgstation.Server.Host.Tests.Signals/Program.cs index 16aa655fab..c60a40d3a9 100644 --- a/tests/Tgstation.Server.Host.Tests.Signals/Program.cs +++ b/tests/Tgstation.Server.Host.Tests.Signals/Program.cs @@ -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 { diff --git a/tests/Tgstation.Server.Host.Tests/Components/Chat/Providers/TestIrcProvider.cs b/tests/Tgstation.Server.Host.Tests/Components/Chat/Providers/TestIrcProvider.cs index 68413a0809..2efc860a6a 100644 --- a/tests/Tgstation.Server.Host.Tests/Components/Chat/Providers/TestIrcProvider.cs +++ b/tests/Tgstation.Server.Host.Tests/Components/Chat/Providers/TestIrcProvider.cs @@ -4,10 +4,10 @@ using Moq; using System; using System.Threading.Tasks; using Tgstation.Server.Api.Models; -using Tgstation.Server.Host.Core; using Tgstation.Server.Host.Jobs; using Tgstation.Server.Host.Models; using Tgstation.Server.Host.System; +using Tgstation.Server.Host.Utils; namespace Tgstation.Server.Host.Components.Chat.Providers.Tests { diff --git a/tests/Tgstation.Server.Host.Tests/Core/TestApplication.cs b/tests/Tgstation.Server.Host.Tests/Core/TestApplication.cs index d3101bb4e7..47947987e1 100644 --- a/tests/Tgstation.Server.Host.Tests/Core/TestApplication.cs +++ b/tests/Tgstation.Server.Host.Tests/Core/TestApplication.cs @@ -51,24 +51,25 @@ namespace Tgstation.Server.Host.Core.Tests var mockTokenFactory = new Mock(); Assert.ThrowsException(() => app.Configure(mockAppBuilder.Object, mockServerControl.Object, mockTokenFactory.Object, null, null, null, null, null, null)); - var mockInstanceManager = new Mock(); - mockInstanceManager.SetupGet(x => x.Ready).Returns(Extensions.TaskExtensions.InfiniteTask()); - Assert.ThrowsException(() => app.Configure(mockAppBuilder.Object, mockServerControl.Object, mockTokenFactory.Object, mockInstanceManager.Object, null, null, null, null, null)); - var mockServerPortProvider = new Mock(); mockServerPortProvider.SetupGet(x => x.HttpApiPort).Returns(5345); - Assert.ThrowsException(() => app.Configure(mockAppBuilder.Object, mockServerControl.Object, mockTokenFactory.Object, mockInstanceManager.Object, mockServerPortProvider.Object, null, null, null, null)); + Assert.ThrowsException(() => app.Configure(mockAppBuilder.Object, mockServerControl.Object, mockTokenFactory.Object, mockServerPortProvider.Object, null, null, null, null, null)); var mockAssemblyInformationProvider = Mock.Of(); - Assert.ThrowsException(() => app.Configure(mockAppBuilder.Object, mockServerControl.Object, mockTokenFactory.Object, mockInstanceManager.Object, mockServerPortProvider.Object, mockAssemblyInformationProvider, null, null, null)); + Assert.ThrowsException(() => app.Configure(mockAppBuilder.Object, mockServerControl.Object, mockTokenFactory.Object, mockServerPortProvider.Object, mockAssemblyInformationProvider, null, null, null, null)); var mockControlPanelOptions = new Mock>(); mockControlPanelOptions.SetupGet(x => x.Value).Returns(new ControlPanelConfiguration()).Verifiable(); - Assert.ThrowsException(() => app.Configure(mockAppBuilder.Object, mockServerControl.Object, mockTokenFactory.Object, mockInstanceManager.Object, mockServerPortProvider.Object, mockAssemblyInformationProvider, mockControlPanelOptions.Object, null, null)); + Assert.ThrowsException(() => app.Configure(mockAppBuilder.Object, mockServerControl.Object, mockTokenFactory.Object, mockServerPortProvider.Object, mockAssemblyInformationProvider, mockControlPanelOptions.Object, null, null, null)); var mockGeneralOptions = new Mock>(); mockGeneralOptions.SetupGet(x => x.Value).Returns(new GeneralConfiguration()).Verifiable(); - Assert.ThrowsException(() => app.Configure(mockAppBuilder.Object, mockServerControl.Object, mockTokenFactory.Object, mockInstanceManager.Object, mockServerPortProvider.Object, mockAssemblyInformationProvider, mockControlPanelOptions.Object, mockGeneralOptions.Object, null)); + Assert.ThrowsException(() => app.Configure(mockAppBuilder.Object, mockServerControl.Object, mockTokenFactory.Object, mockServerPortProvider.Object, mockAssemblyInformationProvider, mockControlPanelOptions.Object, mockGeneralOptions.Object, null, null)); + + var mockSwarmOptions = new Mock>(); + mockSwarmOptions.SetupGet(x => x.Value).Returns(new SwarmConfiguration()).Verifiable(); + Assert.ThrowsException(() => app.Configure(mockAppBuilder.Object, mockServerControl.Object, mockTokenFactory.Object, mockServerPortProvider.Object, mockAssemblyInformationProvider, mockControlPanelOptions.Object, mockGeneralOptions.Object, mockSwarmOptions.Object, null)); + mockControlPanelOptions.VerifyAll(); mockGeneralOptions.VerifyAll(); } diff --git a/tests/Tgstation.Server.Host.Tests/Setup/TestSetupWizard.cs b/tests/Tgstation.Server.Host.Tests/Setup/TestSetupWizard.cs index 67175e8fc9..841f97bfd1 100644 --- a/tests/Tgstation.Server.Host.Tests/Setup/TestSetupWizard.cs +++ b/tests/Tgstation.Server.Host.Tests/Setup/TestSetupWizard.cs @@ -14,10 +14,10 @@ using System.Threading; using System.Threading.Tasks; using Tgstation.Server.Host.Configuration; -using Tgstation.Server.Host.Core; using Tgstation.Server.Host.Database; using Tgstation.Server.Host.IO; using Tgstation.Server.Host.System; +using Tgstation.Server.Host.Utils; namespace Tgstation.Server.Host.Setup.Tests { diff --git a/tests/Tgstation.Server.Host.Tests/Swarm/TestableSwarmNode.cs b/tests/Tgstation.Server.Host.Tests/Swarm/TestableSwarmNode.cs index 541cd46879..248e09bb94 100644 --- a/tests/Tgstation.Server.Host.Tests/Swarm/TestableSwarmNode.cs +++ b/tests/Tgstation.Server.Host.Tests/Swarm/TestableSwarmNode.cs @@ -17,6 +17,7 @@ using Tgstation.Server.Host.Controllers; using Tgstation.Server.Host.Core; using Tgstation.Server.Host.Database; using Tgstation.Server.Host.System; +using Tgstation.Server.Host.Utils; namespace Tgstation.Server.Host.Swarm.Tests { diff --git a/tests/Tgstation.Server.Host.Tests/System/TestPosixSignalHandler.cs b/tests/Tgstation.Server.Host.Tests/System/TestPosixSignalHandler.cs index 52c6129ef9..1ea6c67d38 100644 --- a/tests/Tgstation.Server.Host.Tests/System/TestPosixSignalHandler.cs +++ b/tests/Tgstation.Server.Host.Tests/System/TestPosixSignalHandler.cs @@ -10,6 +10,7 @@ using Moq; using Tgstation.Server.Host.Core; using Tgstation.Server.Host.IO; +using Tgstation.Server.Host.Utils; namespace Tgstation.Server.Host.System.Tests { diff --git a/tests/Tgstation.Server.Host.Tests/Core/TestAsyncDelayer.cs b/tests/Tgstation.Server.Host.Tests/Utils/TestAsyncDelayer.cs similarity index 86% rename from tests/Tgstation.Server.Host.Tests/Core/TestAsyncDelayer.cs rename to tests/Tgstation.Server.Host.Tests/Utils/TestAsyncDelayer.cs index 48aa62790b..2760e9a4fe 100644 --- a/tests/Tgstation.Server.Host.Tests/Core/TestAsyncDelayer.cs +++ b/tests/Tgstation.Server.Host.Tests/Utils/TestAsyncDelayer.cs @@ -1,9 +1,10 @@ -using Microsoft.VisualStudio.TestTools.UnitTesting; -using System; +using System; using System.Threading; using System.Threading.Tasks; -namespace Tgstation.Server.Host.Core.Tests +using Microsoft.VisualStudio.TestTools.UnitTesting; + +namespace Tgstation.Server.Host.Utils.Tests { [TestClass] public sealed class TestAsyncDelayer diff --git a/tests/Tgstation.Server.Host.Tests/Core/TestGitHubClientFactory.cs b/tests/Tgstation.Server.Host.Tests/Utils/TestGitHubClientFactory.cs similarity index 97% rename from tests/Tgstation.Server.Host.Tests/Core/TestGitHubClientFactory.cs rename to tests/Tgstation.Server.Host.Tests/Utils/TestGitHubClientFactory.cs index 1901f46b03..605cdd0a49 100644 --- a/tests/Tgstation.Server.Host.Tests/Core/TestGitHubClientFactory.cs +++ b/tests/Tgstation.Server.Host.Tests/Utils/TestGitHubClientFactory.cs @@ -1,15 +1,18 @@ -using Microsoft.Extensions.Options; -using Microsoft.VisualStudio.TestTools.UnitTesting; -using Moq; -using Octokit; -using System; +using System; using System.Net.Http.Headers; using System.Threading.Tasks; +using Microsoft.Extensions.Options; +using Microsoft.VisualStudio.TestTools.UnitTesting; + +using Moq; + +using Octokit; + using Tgstation.Server.Host.Configuration; using Tgstation.Server.Host.System; -namespace Tgstation.Server.Host.Core.Tests +namespace Tgstation.Server.Host.Utils.Tests { [TestClass] public sealed class TestGitHubClientFactory diff --git a/tests/Tgstation.Server.Tests/Live/Instance/ByondTest.cs b/tests/Tgstation.Server.Tests/Live/Instance/ByondTest.cs index 6531ac585b..84faa305df 100644 --- a/tests/Tgstation.Server.Tests/Live/Instance/ByondTest.cs +++ b/tests/Tgstation.Server.Tests/Live/Instance/ByondTest.cs @@ -1,17 +1,20 @@ -using Microsoft.Extensions.Logging; -using Microsoft.Extensions.Options; -using Microsoft.VisualStudio.TestTools.UnitTesting; -using Moq; -using System; +using System; using System.IO; using System.Threading; using System.Threading.Tasks; +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Options; +using Microsoft.VisualStudio.TestTools.UnitTesting; + +using Moq; + using Tgstation.Server.Api; using Tgstation.Server.Api.Models; using Tgstation.Server.Api.Models.Request; using Tgstation.Server.Client; using Tgstation.Server.Client.Components; +using Tgstation.Server.Common; using Tgstation.Server.Host.Components.Byond; using Tgstation.Server.Host.Configuration; using Tgstation.Server.Host.IO; @@ -51,6 +54,50 @@ namespace Tgstation.Server.Tests.Live.Instance await firstInstall; await TestInstallFakeVersion(cancellationToken); await TestCustomInstalls(cancellationToken); + await TestDeletes(cancellationToken); + } + + async Task TestDeletes(CancellationToken cancellationToken) + { + var nonExistentUninstallResponseTask = Assert.ThrowsExceptionAsync(() => byondClient.DeleteVersion( + new ByondVersionDeleteRequest + { + Version = new(509, 1000) + }, + cancellationToken)); + + var uninstallResponseTask = byondClient.DeleteVersion( + new ByondVersionDeleteRequest + { + Version = TestVersion + }, + cancellationToken); + + var badBecauseActiveResponseTask = ApiAssert.ThrowsException(() => byondClient.DeleteVersion( + new ByondVersionDeleteRequest + { + Version = new(TestVersion.Major, TestVersion.Minor, 1) + }, + cancellationToken), ErrorCode.ByondCannotDeleteActiveVersion); + + await badBecauseActiveResponseTask; + + var uninstallJob = await uninstallResponseTask; + Assert.IsNotNull(uninstallJob); + + // Has to wait on deployment test possibly + var uninstallTask = WaitForJob(uninstallJob, 120, false, null, cancellationToken); + + await nonExistentUninstallResponseTask; + + await uninstallTask; + var byondDir = Path.Combine(metadata.Path, "Byond", TestVersion.ToString()); + Assert.IsFalse(Directory.Exists(byondDir)); + + var newVersions = await byondClient.InstalledVersions(null, cancellationToken); + Assert.IsNotNull(newVersions); + Assert.AreEqual(1, newVersions.Count); + Assert.AreEqual(new Version(TestVersion.Major, TestVersion.Minor, 1), newVersions[0].Version); } async Task TestInstallFakeVersion(CancellationToken cancellationToken) @@ -83,7 +130,10 @@ namespace Tgstation.Server.Tests.Live.Instance var dreamMakerDir = Path.Combine(metadata.Path, "Byond", newModel.Version.ToString(), "byond", "bin"); Assert.IsTrue(Directory.Exists(dreamMakerDir), $"Directory {dreamMakerDir} does not exist!"); - Assert.IsTrue(File.Exists(Path.Combine(dreamMakerDir, dreamMaker)), $"Missing DreamMaker executable! Dir contents: {string.Join(", ", Directory.GetFileSystemEntries(dreamMakerDir))}"); + Assert.IsTrue( + File.Exists( + Path.Combine(dreamMakerDir, dreamMaker)), + $"Missing DreamMaker executable! Dir contents: {string.Join(", ", Directory.GetFileSystemEntries(dreamMakerDir))}"); } async Task TestNoVersion(CancellationToken cancellationToken) @@ -102,21 +152,22 @@ namespace Tgstation.Server.Tests.Live.Instance var generalConfigOptionsMock = new Mock>(); generalConfigOptionsMock.SetupGet(x => x.Value).Returns(new GeneralConfiguration()); - var byondInstaller = new PlatformIdentifier().IsWindows - ? (IByondInstaller)new WindowsByondInstaller( + var assemblyInformationProvider = new AssemblyInformationProvider(); + var fileDownloader = new FileDownloader( + new HttpClientFactory(assemblyInformationProvider.ProductInfoHeaderValue), + Mock.Of>()); + + IByondInstaller byondInstaller = new PlatformIdentifier().IsWindows + ? new WindowsByondInstaller( Mock.Of(), Mock.Of(), - new FileDownloader( - new ConcreteHttpClientFactory(), - Mock.Of>()), + fileDownloader, generalConfigOptionsMock.Object, Mock.Of>()) : new PosixByondInstaller( Mock.Of(), Mock.Of(), - new FileDownloader( - new ConcreteHttpClientFactory(), - Mock.Of>()), + fileDownloader, Mock.Of>()); using var windowsByondInstaller = byondInstaller as WindowsByondInstaller; diff --git a/tests/Tgstation.Server.Tests/Live/Instance/ConcreteHttpClientFactory.cs b/tests/Tgstation.Server.Tests/Live/Instance/ConcreteHttpClientFactory.cs deleted file mode 100644 index 4050ea8125..0000000000 --- a/tests/Tgstation.Server.Tests/Live/Instance/ConcreteHttpClientFactory.cs +++ /dev/null @@ -1,10 +0,0 @@ -using Tgstation.Server.Common; -using Tgstation.Server.Host.Core; - -namespace Tgstation.Server.Tests.Live.Instance -{ - sealed class ConcreteHttpClientFactory : IAbstractHttpClientFactory - { - public IHttpClient CreateClient() => new HttpClient(); - } -} diff --git a/tests/Tgstation.Server.Tests/Live/Instance/JobsRequiredTest.cs b/tests/Tgstation.Server.Tests/Live/Instance/JobsRequiredTest.cs index be3003b1e3..c561d7abde 100644 --- a/tests/Tgstation.Server.Tests/Live/Instance/JobsRequiredTest.cs +++ b/tests/Tgstation.Server.Tests/Live/Instance/JobsRequiredTest.cs @@ -47,7 +47,7 @@ namespace Tgstation.Server.Tests.Live.Instance return job; } - protected async Task WaitForJobProgressThenCancel(JobResponse originalJob, int timeout, CancellationToken cancellationToken) + protected async Task WaitForJobProgress(JobResponse originalJob, int timeout, CancellationToken cancellationToken) { var job = originalJob; do @@ -56,18 +56,28 @@ namespace Tgstation.Server.Tests.Live.Instance job = await JobsClient.GetId(job, cancellationToken); --timeout; } - while (!job.Progress.HasValue && timeout > 0); + while (!job.Progress.HasValue && job.Stage == null && timeout > 0); + + if (job.ExceptionDetails != null) + Assert.Fail(job.ExceptionDetails); + + return job; + } + + protected async Task WaitForJobProgressThenCancel(JobResponse originalJob, int timeout, CancellationToken cancellationToken) + { + var start = DateTimeOffset.UtcNow; + var job = await WaitForJobProgress(originalJob, timeout, cancellationToken); if (job.StoppedAt.HasValue) - { - await JobsClient.Cancel(job, cancellationToken); Assert.Fail($"Job ID {job.Id} \"{job.Description}\" completed when we wanted it to just progress!"); - } if (job.ExceptionDetails != null) Assert.Fail(job.ExceptionDetails); await JobsClient.Cancel(job, cancellationToken); + + timeout -= (int)Math.Ceiling((DateTimeOffset.UtcNow - start).TotalSeconds); return await WaitForJob(job, timeout, false, null, cancellationToken); } } diff --git a/tests/Tgstation.Server.Tests/Live/Instance/RepositoryTest.cs b/tests/Tgstation.Server.Tests/Live/Instance/RepositoryTest.cs index b03b676693..d93c50e3d2 100644 --- a/tests/Tgstation.Server.Tests/Live/Instance/RepositoryTest.cs +++ b/tests/Tgstation.Server.Tests/Live/Instance/RepositoryTest.cs @@ -27,13 +27,6 @@ namespace Tgstation.Server.Tests.Live.Instance { var workingBranch = "master"; - var initalRepo = await repositoryClient.Read(cancellationToken); - Assert.IsNotNull(initalRepo); - Assert.IsNull(initalRepo.Origin); - Assert.IsNull(initalRepo.Reference); - Assert.IsNull(initalRepo.RevisionInformation); - Assert.IsNull(initalRepo.ActiveJob); - const string Origin = "https://github.com/tgstation/tgstation"; var cloneRequest = new RepositoryCreateRequest { @@ -66,6 +59,13 @@ namespace Tgstation.Server.Tests.Live.Instance { await WaitForJobProgressThenCancel(await longCloneJob, 40, cancellationToken); + var initalRepo = await repositoryClient.Read(cancellationToken); + Assert.IsNotNull(initalRepo); + Assert.IsNull(initalRepo.Origin); + Assert.IsNull(initalRepo.Reference); + Assert.IsNull(initalRepo.RevisionInformation); + Assert.IsNull(initalRepo.ActiveJob); + var secondRead = await repositoryClient.Read(cancellationToken); Assert.IsNotNull(secondRead); Assert.IsNull(secondRead.ActiveJob); diff --git a/tests/Tgstation.Server.Tests/Live/Instance/WatchdogTest.cs b/tests/Tgstation.Server.Tests/Live/Instance/WatchdogTest.cs index d2ff2aa408..484be88225 100644 --- a/tests/Tgstation.Server.Tests/Live/Instance/WatchdogTest.cs +++ b/tests/Tgstation.Server.Tests/Live/Instance/WatchdogTest.cs @@ -96,6 +96,8 @@ namespace Tgstation.Server.Tests.Live.Instance { await StartAndLeaveRunning(cancellationToken); + var deleteJobTask = TestDeleteByondInstallErrorCasesAndQueing(cancellationToken); + await WhiteBoxChatCommandTest(cancellationToken); await SendChatOverloadCommand(cancellationToken); await ValidateTopicLimits(cancellationToken); @@ -106,8 +108,88 @@ namespace Tgstation.Server.Tests.Live.Instance var ddInfo = await instanceClient.DreamDaemon.Read(cancellationToken); await CheckDMApiFail(ddInfo.ActiveCompileJob, cancellationToken); + var deleteJob = await deleteJobTask; + // And this freezes DD await DumpTests(cancellationToken); + + // Restart to unlock previous BYOND version + var restartJob = await instanceClient.DreamDaemon.Restart(cancellationToken); + await WaitForJob(deleteJob, 15, false, null, cancellationToken); + await WaitForJob(restartJob, 15, false, null, cancellationToken); + } + + async Task TestDeleteByondInstallErrorCasesAndQueing(CancellationToken cancellationToken) + { + var testCustomVersion = new Version(ByondTest.TestVersion.Major, ByondTest.TestVersion.Minor, 1); + var currentByond = await instanceClient.Byond.ActiveVersion(cancellationToken); + Assert.IsNotNull(currentByond); + Assert.AreEqual(ByondTest.TestVersion.Semver(), currentByond.Version); + + // Change the active version and check we get delayed while deleting the old one because the watchdog is using it + var setActiveResponse = await instanceClient.Byond.SetActiveVersion( + new ByondVersionRequest + { + Version = testCustomVersion, + }, + null, + cancellationToken); + + Assert.IsNotNull(setActiveResponse); + Assert.IsNull(setActiveResponse.InstallJob); + + var deleteJob = await instanceClient.Byond.DeleteVersion( + new ByondVersionDeleteRequest + { + Version = ByondTest.TestVersion, + }, + cancellationToken); + + Assert.IsNotNull(deleteJob); + + deleteJob = await WaitForJobProgress(deleteJob, 15, cancellationToken); + Assert.IsNotNull(deleteJob); + Assert.IsNotNull(deleteJob.Stage); + Assert.IsTrue(deleteJob.Stage.Contains("Waiting")); + + // then change it back and check it fails the job because it's active again + setActiveResponse = await instanceClient.Byond.SetActiveVersion( + new ByondVersionRequest + { + Version = ByondTest.TestVersion, + }, + null, + cancellationToken); + + Assert.IsNotNull(setActiveResponse); + Assert.IsNull(setActiveResponse.InstallJob); + + await WaitForJob(deleteJob, 5, true, ErrorCode.ByondCannotDeleteActiveVersion, cancellationToken); + + // finally, queue the last delete job which should complete when the watchdog restarts with a newly deployed .dmb + // queue the byond change followed by the deployment for that first + setActiveResponse = await instanceClient.Byond.SetActiveVersion( + new ByondVersionRequest + { + Version = testCustomVersion, + }, + null, + cancellationToken); + + Assert.IsNotNull(setActiveResponse); + Assert.IsNull(setActiveResponse.InstallJob); + + deleteJob = await instanceClient.Byond.DeleteVersion( + new ByondVersionDeleteRequest + { + Version = ByondTest.TestVersion, + }, + cancellationToken); + + Assert.IsNotNull(deleteJob); + + await DeployTestDme("LongRunning/long_running_test", DreamDaemonSecurity.Safe, true, cancellationToken); + return deleteJob; } static async Task SendChatOverloadCommand(CancellationToken cancellationToken) @@ -602,7 +684,7 @@ namespace Tgstation.Server.Tests.Live.Instance Assert.IsNotNull(daemonStatus.ActiveCompileJob); Assert.IsNull(daemonStatus.StagedCompileJob); Assert.AreEqual(DMApiConstants.InteropVersion, daemonStatus.ActiveCompileJob.DMApiVersion); - Assert.AreEqual(DreamDaemonSecurity.Ultrasafe, daemonStatus.ActiveCompileJob.MinimumSecurityLevel); + Assert.AreEqual(DreamDaemonSecurity.Safe, daemonStatus.ActiveCompileJob.MinimumSecurityLevel); var startJob = await StartDD(cancellationToken); @@ -617,7 +699,7 @@ namespace Tgstation.Server.Tests.Live.Instance Assert.IsNotNull(newerCompileJob); Assert.AreNotEqual(initialCompileJob.Id, newerCompileJob.Id); - Assert.AreEqual(DreamDaemonSecurity.Ultrasafe, newerCompileJob.MinimumSecurityLevel); + Assert.AreEqual(DreamDaemonSecurity.Safe, newerCompileJob.MinimumSecurityLevel); await CheckDMApiFail(daemonStatus.ActiveCompileJob, cancellationToken); daemonStatus = await TellWorldToReboot(cancellationToken); @@ -644,7 +726,7 @@ namespace Tgstation.Server.Tests.Live.Instance Assert.IsNotNull(daemonStatus.ActiveCompileJob); Assert.IsNull(daemonStatus.StagedCompileJob); Assert.AreEqual(DMApiConstants.InteropVersion, daemonStatus.ActiveCompileJob.DMApiVersion); - Assert.AreEqual(DreamDaemonSecurity.Ultrasafe, daemonStatus.ActiveCompileJob.MinimumSecurityLevel); + Assert.AreEqual(DreamDaemonSecurity.Safe, daemonStatus.ActiveCompileJob.MinimumSecurityLevel); var startJob = await StartDD(cancellationToken); @@ -659,7 +741,7 @@ namespace Tgstation.Server.Tests.Live.Instance Assert.IsNotNull(newerCompileJob); Assert.AreNotEqual(initialCompileJob.Id, newerCompileJob.Id); - Assert.AreEqual(DreamDaemonSecurity.Ultrasafe, newerCompileJob.MinimumSecurityLevel); + Assert.AreEqual(DreamDaemonSecurity.Safe, newerCompileJob.MinimumSecurityLevel); await CheckDMApiFail(daemonStatus.ActiveCompileJob, cancellationToken); daemonStatus = await TellWorldToReboot(cancellationToken); @@ -698,7 +780,9 @@ namespace Tgstation.Server.Tests.Live.Instance cancellationToken); var byondInstallJob = await byondInstallJobTask; - Assert.IsNull(byondInstallJob.InstallJob); + // This used to be the case but it gets deleted now that we have and test that + // Assert.IsNull(byondInstallJob.InstallJob); + await WaitForJob(byondInstallJob.InstallJob, 60, false, null, cancellationToken); const string DmeName = "LongRunning/long_running_test"; diff --git a/tests/Tgstation.Server.Tests/Live/LiveTestingServer.cs b/tests/Tgstation.Server.Tests/Live/LiveTestingServer.cs index 60f177837e..ccc564461e 100644 --- a/tests/Tgstation.Server.Tests/Live/LiveTestingServer.cs +++ b/tests/Tgstation.Server.Tests/Live/LiveTestingServer.cs @@ -14,8 +14,8 @@ using Tgstation.Server.Api.Models; using Tgstation.Server.Host; using Tgstation.Server.Host.Configuration; using Tgstation.Server.Host.Core; -using Tgstation.Server.Host.Extensions; using Tgstation.Server.Host.Setup; +using Tgstation.Server.Host.Utils; namespace Tgstation.Server.Tests.Live { @@ -42,7 +42,7 @@ namespace Tgstation.Server.Tests.Live static LiveTestingServer() { - ServiceCollectionExtensions.SerilogContextTemplate += "|Node:{node}"; + SerilogContextHelper.AddSwarmNodeIdentifierToTemplate(); } public LiveTestingServer(SwarmConfiguration swarmConfiguration, bool enableOAuth, ushort port = 5010) @@ -57,7 +57,6 @@ namespace Tgstation.Server.Tests.Live System.IO.Directory.Delete(Directory, true); } catch { } - } Directory = Path.Combine(Directory, Guid.NewGuid().ToString()); @@ -191,7 +190,7 @@ namespace Tgstation.Server.Tests.Live args[0] = string.Format(CultureInfo.InvariantCulture, "Database:DropDatabase={0}", false); using (swarmNodeId != null - ? LogContext.PushProperty("node", swarmNodeId) + ? LogContext.PushProperty(SerilogContextHelper.SwarmIdentifierContextProperty, swarmNodeId) : null) await RealServer.Run(cancellationToken); Console.WriteLine($"TEST SERVER END" + messageAddition); diff --git a/tools/Tgstation.Server.Migrator/ConcreteHttpClientFactory.cs b/tools/Tgstation.Server.Migrator/ConcreteHttpClientFactory.cs deleted file mode 100644 index 2a15963fed..0000000000 --- a/tools/Tgstation.Server.Migrator/ConcreteHttpClientFactory.cs +++ /dev/null @@ -1,10 +0,0 @@ -using Tgstation.Server.Common; -using Tgstation.Server.Host.Core; - -namespace Tgstation.Server.Migrator -{ - sealed class ConcreteHttpClientFactory : IAbstractHttpClientFactory - { - public IHttpClient CreateClient() => new HttpClient(); - } -} diff --git a/tools/Tgstation.Server.Migrator/Program.cs b/tools/Tgstation.Server.Migrator/Program.cs index 298877c75a..6d6406500c 100644 --- a/tools/Tgstation.Server.Migrator/Program.cs +++ b/tools/Tgstation.Server.Migrator/Program.cs @@ -21,9 +21,9 @@ using Octokit; using Tgstation.Server.Api; using Tgstation.Server.Client; +using Tgstation.Server.Common; using Tgstation.Server.Host.IO; using Tgstation.Server.Host.Setup; -using Tgstation.Server.Migrator; using FileMode = System.IO.FileMode; @@ -264,6 +264,7 @@ try new ProductInfoHeaderValue( assemblyName.Name!, assemblyName.Version!.Semver().ToString()); + var httpClientFactory = new HttpClientFactory(productInfoHeaderValue); if (!runtimeInstalled) { // RUNTIME DONWLOAD @@ -279,9 +280,9 @@ try Console.WriteLine($"Downloading {downloadUri} to {Path.GetFullPath(dotnetDownloadFilePath)}..."); - using var httpClient = new HttpClient(); - httpClient.DefaultRequestHeaders.UserAgent.Add(productInfoHeaderValue); - var webRequestTask = httpClient.GetAsync(downloadUri); + using var httpClient = httpClientFactory.CreateClient(); + using var request = new HttpRequestMessage(HttpMethod.Get, downloadUri); + var webRequestTask = httpClient.SendAsync(request, default); using var response = await webRequestTask; response.EnsureSuccessStatusCode(); using (var responseStream = await response.Content.ReadAsStreamAsync()) @@ -382,7 +383,6 @@ try // TGS5 DOWNLOAD AND UNZIP Console.WriteLine("Downloading TGS5..."); - var httpClientFactory = new ConcreteHttpClientFactory(); using (var loggerFactory = LoggerFactory.Create(builder => { })) { var fileDownloader = new FileDownloader(httpClientFactory, loggerFactory.CreateLogger());