From d900eaf71a688bb7198f14b45f131e99c0328b0f Mon Sep 17 00:00:00 2001 From: Jordan Brown Date: Sat, 11 Jul 2020 09:48:03 -0400 Subject: [PATCH 01/68] Only throw error code 78 on correct socket error --- .../Components/Session/SessionControllerFactory.cs | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/Tgstation.Server.Host/Components/Session/SessionControllerFactory.cs b/src/Tgstation.Server.Host/Components/Session/SessionControllerFactory.cs index 3b7a617e41..e1e77a2098 100644 --- a/src/Tgstation.Server.Host/Components/Session/SessionControllerFactory.cs +++ b/src/Tgstation.Server.Host/Components/Session/SessionControllerFactory.cs @@ -124,8 +124,10 @@ namespace Tgstation.Server.Host.Components.Session { socket.Bind(new IPEndPoint(IPAddress.Any, port)); } - catch (Exception ex) + catch (SocketException ex) { + if(ex.SocketErrorCode != SocketError.AddressAlreadyInUse) + throw; throw new JobException(ErrorCode.DreamDaemonPortInUse, ex); } } From 6b4bbcb6605224c2e98111265c5e54665315bf70 Mon Sep 17 00:00:00 2001 From: Jordan Brown Date: Sat, 11 Jul 2020 09:52:17 -0400 Subject: [PATCH 02/68] Test for ErrorCode 78 --- .../Tgstation.Server.Tests/Instance/WatchdogTest.cs | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/tests/Tgstation.Server.Tests/Instance/WatchdogTest.cs b/tests/Tgstation.Server.Tests/Instance/WatchdogTest.cs index 822a8f6a3d..d8e666560f 100644 --- a/tests/Tgstation.Server.Tests/Instance/WatchdogTest.cs +++ b/tests/Tgstation.Server.Tests/Instance/WatchdogTest.cs @@ -6,6 +6,7 @@ using System; using System.IO; using System.Linq; using System.Net; +using System.Net.Sockets; using System.Threading; using System.Threading.Tasks; using Tgstation.Server.Api; @@ -137,7 +138,16 @@ namespace Tgstation.Server.Tests.Instance Assert.AreEqual(DMApiConstants.Version, daemonStatus.ActiveCompileJob.DMApiVersion); Assert.AreEqual(DreamDaemonSecurity.Safe, daemonStatus.ActiveCompileJob.MinimumSecurityLevel); - var startJob = await instanceClient.DreamDaemon.Start(cancellationToken).ConfigureAwait(false); + Job startJob; + using (var blockSocket = new Socket(SocketType.Stream, ProtocolType.Tcp)) + { + blockSocket.Bind(new IPEndPoint(IPAddress.Any, 1337)); + startJob = await instanceClient.DreamDaemon.Start(cancellationToken).ConfigureAwait(false); + + await WaitForJob(startJob, 10, true, ErrorCode.DreamDaemonPortInUse, cancellationToken); + } + + startJob = await instanceClient.DreamDaemon.Start(cancellationToken).ConfigureAwait(false); await WaitForJob(startJob, 10, false, null, cancellationToken); From 12b90f82bc903305fcf94f391dad5bb36920b9e0 Mon Sep 17 00:00:00 2001 From: Jordan Brown Date: Sat, 11 Jul 2020 09:53:04 -0400 Subject: [PATCH 03/68] Try to ensure the Kestrel port will be available when hosting starts - Fixes #1065 --- .../Components/InstanceManager.cs | 23 ++++++++++++++++--- 1 file changed, 20 insertions(+), 3 deletions(-) diff --git a/src/Tgstation.Server.Host/Components/InstanceManager.cs b/src/Tgstation.Server.Host/Components/InstanceManager.cs index 46f3ca2bd6..636d29235e 100644 --- a/src/Tgstation.Server.Host/Components/InstanceManager.cs +++ b/src/Tgstation.Server.Host/Components/InstanceManager.cs @@ -5,6 +5,8 @@ using Microsoft.Extensions.Options; using System; using System.Collections.Generic; using System.Linq; +using System.Net; +using System.Net.Sockets; using System.Threading; using System.Threading.Tasks; using Tgstation.Server.Api.Models; @@ -71,6 +73,11 @@ namespace Tgstation.Server.Host.Components /// readonly IDatabaseSeeder databaseSeeder; + /// + /// The for the + /// + readonly IServerPortProvider serverPortProvider; + /// /// The for the /// @@ -118,6 +125,7 @@ namespace Tgstation.Server.Host.Components /// The value of . /// The value of . /// The value of . + /// The value of . /// The containing the value of . /// The value of public InstanceManager( @@ -130,6 +138,7 @@ namespace Tgstation.Server.Host.Components ISystemIdentityFactory systemIdentityFactory, IAsyncDelayer asyncDelayer, IDatabaseSeeder databaseSeeder, + IServerPortProvider serverPortProvider, IOptions generalConfigurationOptions, ILogger logger) { @@ -142,6 +151,7 @@ namespace Tgstation.Server.Host.Components this.systemIdentityFactory = systemIdentityFactory ?? throw new ArgumentNullException(nameof(systemIdentityFactory)); this.asyncDelayer = asyncDelayer ?? throw new ArgumentNullException(nameof(asyncDelayer)); this.databaseSeeder = databaseSeeder ?? throw new ArgumentNullException(nameof(databaseSeeder)); + this.serverPortProvider = serverPortProvider ?? throw new ArgumentNullException(nameof(serverPortProvider)); generalConfiguration = generalConfigurationOptions?.Value ?? throw new ArgumentNullException(nameof(generalConfigurationOptions)); this.logger = logger ?? throw new ArgumentNullException(nameof(logger)); @@ -359,9 +369,16 @@ namespace Tgstation.Server.Host.Components /// private void CheckSystemCompatibility() { - using var systemIdentity = systemIdentityFactory.GetCurrent(); - if (!systemIdentity.CanCreateSymlinks) - throw new InvalidOperationException("The user running tgstation-server cannot create symlinks! Please try running as an administrative user!"); + using (var systemIdentity = systemIdentityFactory.GetCurrent()) + { + if (!systemIdentity.CanCreateSymlinks) + throw new InvalidOperationException("The user running tgstation-server cannot create symlinks! Please try running as an administrative user!"); + } + + // This runs before the real socket is opened, ensures we don't perform reattaches unless we're fairly certain the bind won't fail + // If it does fail, DD will be killed. + using var hostingSocket = new Socket(SocketType.Stream, ProtocolType.Tcp); + hostingSocket.Bind(new IPEndPoint(IPAddress.Any, serverPortProvider.HttpApiPort)); } /// From ec48e20bed1841ea5e21f1eeb669a94a4c01eb73 Mon Sep 17 00:00:00 2001 From: Jordan Brown Date: Sat, 11 Jul 2020 09:53:32 -0400 Subject: [PATCH 04/68] Regression test for #1065 --- .../Tgstation.Server.Tests/IntegrationTest.cs | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) diff --git a/tests/Tgstation.Server.Tests/IntegrationTest.cs b/tests/Tgstation.Server.Tests/IntegrationTest.cs index 2a2d5ab07b..8de906e8fe 100644 --- a/tests/Tgstation.Server.Tests/IntegrationTest.cs +++ b/tests/Tgstation.Server.Tests/IntegrationTest.cs @@ -8,6 +8,7 @@ using System.Linq; using System.Net; using System.Net.Http; using System.Net.Http.Headers; +using System.Net.Sockets; using System.Reflection; using System.Threading; using System.Threading.Tasks; @@ -191,6 +192,24 @@ namespace Tgstation.Server.Tests await Task.WhenAny(serverTask, Task.Delay(30000, cancellationToken)); Assert.IsTrue(serverTask.IsCompleted); + // http bind test https://github.com/tgstation/tgstation-server/issues/1065 + using (var blockingSocket = new Socket(SocketType.Stream, ProtocolType.Tcp)) + { + blockingSocket.Bind(new IPEndPoint(IPAddress.Any, server.Url.Port)); + try + { + await server.Run(cancellationToken); + Assert.Fail("Expected server task to end with a SocketException"); + } + catch (SocketException ex) + { + Assert.AreEqual(ex.SocketErrorCode, SocketError.AddressAlreadyInUse); + } + } + + await Task.WhenAny(serverTask, Task.Delay(30000, cancellationToken)); + Assert.IsTrue(serverTask.IsCompleted); + var preStartupTime = DateTimeOffset.Now; serverTask = server.Run(cancellationToken); From 308e71d8c9436c50286cab4f79b267de3ba68eb3 Mon Sep 17 00:00:00 2001 From: Jordan Brown Date: Sat, 11 Jul 2020 10:03:30 -0400 Subject: [PATCH 05/68] Make the first log message the server version --- .../Components/InstanceManager.cs | 15 ++++++++++++++- 1 file changed, 14 insertions(+), 1 deletion(-) diff --git a/src/Tgstation.Server.Host/Components/InstanceManager.cs b/src/Tgstation.Server.Host/Components/InstanceManager.cs index 636d29235e..5fe7cb053a 100644 --- a/src/Tgstation.Server.Host/Components/InstanceManager.cs +++ b/src/Tgstation.Server.Host/Components/InstanceManager.cs @@ -28,6 +28,11 @@ namespace Tgstation.Server.Host.Components /// public Task Ready => readyTcs.Task; + /// + /// The for the ; + /// + readonly Lazy lazyRestartRegistration; + /// /// The for the /// @@ -155,7 +160,7 @@ namespace Tgstation.Server.Host.Components generalConfiguration = generalConfigurationOptions?.Value ?? throw new ArgumentNullException(nameof(generalConfigurationOptions)); this.logger = logger ?? throw new ArgumentNullException(nameof(logger)); - serverControl.RegisterForRestart(this); + lazyRestartRegistration = new Lazy(() => serverControl.RegisterForRestart(this)); instances = new Dictionary(); bridgeHandlers = new Dictionary(); @@ -174,6 +179,10 @@ namespace Tgstation.Server.Host.Components foreach (var I in instances) I.Value.Dispose(); + + lazyRestartRegistration.Value.Dispose(); + + logger.LogInformation("Server shutdown"); } /// @@ -287,6 +296,10 @@ namespace Tgstation.Server.Host.Components { logger.LogInformation(assemblyInformationProvider.VersionString); + // we do this here because making the restart registration triggers a trace log message + // The above log message should be the first one one startup + var _ = lazyRestartRegistration.Value; + try { generalConfiguration.CheckCompatibility(logger); From ae252591ed50f6dbe22364949b0add03db8f9b2b Mon Sep 17 00:00:00 2001 From: Jordan Brown Date: Sat, 11 Jul 2020 10:41:13 -0400 Subject: [PATCH 06/68] An overly complicated change to make sure a task is awaited. - IDisposable moved from IProcessBase to IProcess - IInstanceManager, IInstance, IWatchdog, and ISessionController switched from IDisposable to IAsyncDisposable - Process lifetime continuations in SessionController now handled correctly --- .../Components/Deployment/DreamMaker.cs | 44 +++++----- .../Components/IInstance.cs | 2 +- .../Components/IInstanceFactory.cs | 5 +- .../Components/Instance.cs | 4 +- .../Components/InstanceFactory.cs | 4 +- .../Components/InstanceManager.cs | 14 ++-- .../Components/Session/ISessionController.cs | 6 +- .../Components/Session/SessionController.cs | 82 +++++++------------ .../Session/SessionControllerFactory.cs | 32 ++++---- .../Components/Watchdog/BasicWatchdog.cs | 10 ++- .../Components/Watchdog/IWatchdog.cs | 2 +- .../Components/Watchdog/WatchdogBase.cs | 77 ++++++++--------- .../Components/Watchdog/WindowsWatchdog.cs | 6 +- src/Tgstation.Server.Host/System/IProcess.cs | 5 +- .../System/IProcessBase.cs | 5 +- 15 files changed, 142 insertions(+), 156 deletions(-) diff --git a/src/Tgstation.Server.Host/Components/Deployment/DreamMaker.cs b/src/Tgstation.Server.Host/Components/Deployment/DreamMaker.cs index 02842f16cf..4c4508a45a 100644 --- a/src/Tgstation.Server.Host/Components/Deployment/DreamMaker.cs +++ b/src/Tgstation.Server.Host/Components/Deployment/DreamMaker.cs @@ -215,30 +215,34 @@ namespace Tgstation.Server.Host.Components.Deployment job.MinimumSecurityLevel = securityLevel; // needed for the TempDmbProvider var timeoutAt = DateTimeOffset.Now.AddSeconds(timeout); - using var provider = new TemporaryDmbProvider(ioManager.ResolvePath(job.DirectoryName.ToString()), String.Concat(job.DmeName, DmbExtension), job); - using var controller = await sessionControllerFactory.LaunchNew(provider, byondLock, launchParameters, true, cancellationToken).ConfigureAwait(false); - var launchResult = await controller.LaunchResult.ConfigureAwait(false); - - var now = DateTimeOffset.Now; - if (now < timeoutAt && launchResult.StartupTime.HasValue) + ApiValidationStatus validationStatus; + using (var provider = new TemporaryDmbProvider(ioManager.ResolvePath(job.DirectoryName.ToString()), String.Concat(job.DmeName, DmbExtension), job)) + await using (var controller = await sessionControllerFactory.LaunchNew(provider, byondLock, launchParameters, true, cancellationToken).ConfigureAwait(false)) { - var timeoutTask = Task.Delay(timeoutAt - now, cancellationToken); + var launchResult = await controller.LaunchResult.ConfigureAwait(false); - await Task.WhenAny(controller.Lifetime, timeoutTask).ConfigureAwait(false); - cancellationToken.ThrowIfCancellationRequested(); + var now = DateTimeOffset.Now; + if (now < timeoutAt && launchResult.StartupTime.HasValue) + { + var timeoutTask = Task.Delay(timeoutAt - now, cancellationToken); + + await Task.WhenAny(controller.Lifetime, timeoutTask).ConfigureAwait(false); + cancellationToken.ThrowIfCancellationRequested(); + } + + if (!controller.Lifetime.IsCompleted) + { + if (requireValidate) + throw new JobException(ErrorCode.DreamMakerNeverValidated); + await controller.DisposeAsync().ConfigureAwait(false); + } + + validationStatus = controller.ApiValidationStatus; + logger.LogTrace("API validation status: {0}", validationStatus); + + job.DMApiVersion = controller.DMApiVersion; } - if (!controller.Lifetime.IsCompleted) - { - if (requireValidate) - throw new JobException(ErrorCode.DreamMakerNeverValidated); - controller.Dispose(); - } - - var validationStatus = controller.ApiValidationStatus; - logger.LogTrace("API validation status: {0}", validationStatus); - - job.DMApiVersion = controller.DMApiVersion; switch (validationStatus) { case ApiValidationStatus.RequiresUltrasafe: diff --git a/src/Tgstation.Server.Host/Components/IInstance.cs b/src/Tgstation.Server.Host/Components/IInstance.cs index 475c625482..05a0c2fa91 100644 --- a/src/Tgstation.Server.Host/Components/IInstance.cs +++ b/src/Tgstation.Server.Host/Components/IInstance.cs @@ -13,7 +13,7 @@ namespace Tgstation.Server.Host.Components /// /// For interacting with the instance services /// - public interface IInstance : ILatestCompileJobProvider, IHostedService, IRenameNotifyee, IDisposable + public interface IInstance : ILatestCompileJobProvider, IHostedService, IRenameNotifyee, IAsyncDisposable { /// /// The for the diff --git a/src/Tgstation.Server.Host/Components/IInstanceFactory.cs b/src/Tgstation.Server.Host/Components/IInstanceFactory.cs index ce13cdf11e..473837ccf2 100644 --- a/src/Tgstation.Server.Host/Components/IInstanceFactory.cs +++ b/src/Tgstation.Server.Host/Components/IInstanceFactory.cs @@ -1,4 +1,5 @@ using Microsoft.Extensions.Hosting; +using System.Threading.Tasks; using Tgstation.Server.Host.Components.Interop.Bridge; namespace Tgstation.Server.Host.Components @@ -13,7 +14,7 @@ namespace Tgstation.Server.Host.Components /// /// The to use. /// The - /// A new - IInstance CreateInstance(IBridgeRegistrar bridgeRegistrar, Models.Instance metadata); + /// A resulting in a new . + Task CreateInstance(IBridgeRegistrar bridgeRegistrar, Models.Instance metadata); } } \ No newline at end of file diff --git a/src/Tgstation.Server.Host/Components/Instance.cs b/src/Tgstation.Server.Host/Components/Instance.cs index 078e3814e4..33058744c2 100644 --- a/src/Tgstation.Server.Host/Components/Instance.cs +++ b/src/Tgstation.Server.Host/Components/Instance.cs @@ -133,14 +133,14 @@ namespace Tgstation.Server.Host.Components } /// - public void Dispose() + public async ValueTask DisposeAsync() { using (LogContext.PushProperty("Instance", metadata.Id)) { timerCts?.Dispose(); Configuration.Dispose(); Chat.Dispose(); - Watchdog.Dispose(); + await Watchdog.DisposeAsync().ConfigureAwait(false); dmbFactory.Dispose(); RepositoryManager.Dispose(); } diff --git a/src/Tgstation.Server.Host/Components/InstanceFactory.cs b/src/Tgstation.Server.Host/Components/InstanceFactory.cs index a1a34295aa..4866ab4987 100644 --- a/src/Tgstation.Server.Host/Components/InstanceFactory.cs +++ b/src/Tgstation.Server.Host/Components/InstanceFactory.cs @@ -192,7 +192,7 @@ namespace Tgstation.Server.Host.Components /// #pragma warning disable CA1506 // TODO: Decomplexify - public IInstance CreateInstance(IBridgeRegistrar bridgeRegistrar, Models.Instance metadata) + public async Task CreateInstance(IBridgeRegistrar bridgeRegistrar, Models.Instance metadata) { // Create the ioManager for the instance var instanceIoManager = new ResolvingIOManager(ioManager, metadata.Path); @@ -294,7 +294,7 @@ namespace Tgstation.Server.Host.Components } catch { - watchdog.Dispose(); + await watchdog.DisposeAsync().ConfigureAwait(false); throw; } } diff --git a/src/Tgstation.Server.Host/Components/InstanceManager.cs b/src/Tgstation.Server.Host/Components/InstanceManager.cs index 5fe7cb053a..5c7606b403 100644 --- a/src/Tgstation.Server.Host/Components/InstanceManager.cs +++ b/src/Tgstation.Server.Host/Components/InstanceManager.cs @@ -23,7 +23,7 @@ using Tgstation.Server.Host.System; namespace Tgstation.Server.Host.Components { /// - sealed class InstanceManager : IInstanceManager, IRestartHandler, IHostedService, IBridgeRegistrar, IDisposable + sealed class InstanceManager : IInstanceManager, IRestartHandler, IHostedService, IBridgeRegistrar, IAsyncDisposable { /// public Task Ready => readyTcs.Task; @@ -114,7 +114,7 @@ namespace Tgstation.Server.Host.Components Version downgradeVersion; /// - /// If the has been d + /// If the has been 'd /// bool disposed; @@ -168,7 +168,7 @@ namespace Tgstation.Server.Host.Components } /// - public void Dispose() + public async ValueTask DisposeAsync() { lock (instances) { @@ -178,7 +178,7 @@ namespace Tgstation.Server.Host.Components } foreach (var I in instances) - I.Value.Dispose(); + await I.Value.DisposeAsync().ConfigureAwait(false); lazyRestartRegistration.Value.Dispose(); @@ -261,7 +261,7 @@ namespace Tgstation.Server.Host.Components } finally { - instance.Dispose(); + await instance.DisposeAsync().ConfigureAwait(false); } } @@ -271,7 +271,7 @@ namespace Tgstation.Server.Host.Components if (metadata == null) throw new ArgumentNullException(nameof(metadata)); logger.LogInformation("Onlining instance ID {0} ({1}) at {2}", metadata.Id, metadata.Name, metadata.Path); - var instance = instanceFactory.CreateInstance(this, metadata); + var instance = await instanceFactory.CreateInstance(this, metadata).ConfigureAwait(false); try { lock (instances) @@ -283,7 +283,7 @@ namespace Tgstation.Server.Host.Components } catch { - instance.Dispose(); + await instance.DisposeAsync().ConfigureAwait(false); throw; } diff --git a/src/Tgstation.Server.Host/Components/Session/ISessionController.cs b/src/Tgstation.Server.Host/Components/Session/ISessionController.cs index 21439f6cc2..3a42879935 100644 --- a/src/Tgstation.Server.Host/Components/Session/ISessionController.cs +++ b/src/Tgstation.Server.Host/Components/Session/ISessionController.cs @@ -10,7 +10,7 @@ namespace Tgstation.Server.Host.Components.Session /// /// Handles communication with a DreamDaemon /// - interface ISessionController : IRenameNotifyee, IProcessBase + interface ISessionController : IProcessBase, IRenameNotifyee, IAsyncDisposable { /// /// A that completes when DreamDaemon starts pumping the windows message queue after loading a .dmb or when it crashes @@ -70,8 +70,8 @@ namespace Tgstation.Server.Host.Components.Session /// /// Releases the without terminating it. Also calls /// - /// which can be used to create a new similar to this one - ReattachInformation Release(); + /// A resulting in which can be used to create a new . + Task Release(); /// /// Sends a command to DreamDaemon through /world/Topic() diff --git a/src/Tgstation.Server.Host/Components/Session/SessionController.cs b/src/Tgstation.Server.Host/Components/Session/SessionController.cs index bdda5618d3..3c867094f5 100644 --- a/src/Tgstation.Server.Host/Components/Session/SessionController.cs +++ b/src/Tgstation.Server.Host/Components/Session/SessionController.cs @@ -83,7 +83,7 @@ namespace Tgstation.Server.Host.Components.Session public Task LaunchResult { get; } /// - public Task Lifetime => process.Lifetime; + public Task Lifetime { get; } /// public Task OnReboot => rebootTcs.Task; @@ -202,6 +202,7 @@ namespace Tgstation.Server.Host.Components.Session /// The value of /// The for the . /// The value of + /// The returning a to be run after the ends. /// The optional time to wait before failing the /// If this is a reattached session. /// If this is a DMAPI validation session. @@ -216,6 +217,7 @@ namespace Tgstation.Server.Host.Components.Session IChatManager chat, IAssemblyInformationProvider assemblyInformationProvider, ILogger logger, + Func postLifetimeCallback, uint? startupTimeout, bool reattached, bool apiValidate) @@ -253,15 +255,14 @@ namespace Tgstation.Server.Host.Components.Session reattachTopicCts = new CancellationTokenSource(); synchronizationLock = new object(); - _ = process.Lifetime.ContinueWith( - x => - { - lock (synchronizationLock) - if (!disposed) - reattachTopicCts.Cancel(); - chatTrackingContext.Active = false; - }, - TaskScheduler.Current); + async Task WrapLifetime() + { + var exitCode = await process.Lifetime.ConfigureAwait(false); + await postLifetimeCallback().ConfigureAwait(false); + return exitCode; + } + + Lifetime = WrapLifetime(); LaunchResult = GetLaunchResult( assemblyInformationProvider, @@ -271,58 +272,31 @@ namespace Tgstation.Server.Host.Components.Session logger.LogDebug("Created session controller. CommsKey: {0}, Port: {1}", reattachInformation.AccessIdentifier, Port); } - /// - /// Finalizes an instance of the class. - /// - /// The finalizer dispose pattern is necessary so we don't accidentally leak the executable -#pragma warning disable CA1821 // Remove empty Finalizers TODO: remove this when https://github.com/dotnet/roslyn-analyzers/issues/1241 is fixed - ~SessionController() => Dispose(false); -#pragma warning restore CA1821 // Remove empty Finalizers - /// - public void Dispose() - { - Dispose(true); - GC.SuppressFinalize(this); - } - - /// - /// Implements the pattern - /// - /// If this function was NOT called by the finalizer - void Dispose(bool disposing) + public async ValueTask DisposeAsync() { lock (synchronizationLock) { if (disposed) return; disposed = true; - logger.LogTrace("Disposing..."); - if (disposing) - { - if (!released) - { - process.Terminate(); - byondLock.Dispose(); - } - process.Dispose(); - bridgeRegistration?.Dispose(); - reattachInformation.Dmb?.Dispose(); // will be null when released - chatTrackingContext.Dispose(); - reattachTopicCts.Dispose(); - } - else + logger.LogTrace("Disposing..."); + if (!released) { - if (logger != null) - logger.LogError("Being disposed via finalizer!"); - if (!released) - if (process != null) - process.Terminate(); - else if (logger != null) - logger.LogCritical("Unable to terminate active DreamDaemon session due to finalizer ordering!"); + process.Terminate(); + byondLock.Dispose(); } + + process.Dispose(); + bridgeRegistration?.Dispose(); + reattachInformation.Dmb?.Dispose(); // will be null when released + chatTrackingContext.Dispose(); + reattachTopicCts.Dispose(); } + + // finish the async callback + await Lifetime.ConfigureAwait(false); } /// @@ -530,7 +504,7 @@ namespace Tgstation.Server.Host.Components.Session } /// - /// Throws an if has been called + /// Throws an if has been called /// void CheckDisposed() { @@ -542,7 +516,7 @@ namespace Tgstation.Server.Host.Components.Session public void EnableCustomChatCommands() => chatTrackingContext.Active = DMApiAvailable; /// - public ReattachInformation Release() + public async Task Release() { CheckDisposed(); @@ -550,7 +524,7 @@ namespace Tgstation.Server.Host.Components.Session var tmpProvider = reattachInformation.Dmb; reattachInformation.Dmb = null; released = true; - Dispose(); + await DisposeAsync().ConfigureAwait(false); byondLock.DoNotDeleteThisSession(); tmpProvider.KeepAlive(); reattachInformation.Dmb = tmpProvider; diff --git a/src/Tgstation.Server.Host/Components/Session/SessionControllerFactory.cs b/src/Tgstation.Server.Host/Components/Session/SessionControllerFactory.cs index e1e77a2098..be29252054 100644 --- a/src/Tgstation.Server.Host/Components/Session/SessionControllerFactory.cs +++ b/src/Tgstation.Server.Host/Components/Session/SessionControllerFactory.cs @@ -297,22 +297,20 @@ namespace Tgstation.Server.Host.Components.Session } // Log DD output - _ = process.Lifetime.ContinueWith( - async x => - { - try - { - var ddOutput = await GetDDOutput().ConfigureAwait(false); - logger.LogTrace( - "DreamDaemon Output:{0}{1}", - Environment.NewLine, ddOutput); - } - catch (Exception ex) - { - logger.LogWarning("Error reading DreamDaemon output: {0}", ex); - } - }, - TaskScheduler.Current); + async Task PostLifetime() + { + try + { + var ddOutput = await GetDDOutput().ConfigureAwait(false); + logger.LogTrace( + "DreamDaemon Output:{0}{1}", + Environment.NewLine, ddOutput); + } + catch (Exception ex) + { + logger.LogWarning("Error reading DreamDaemon output: {0}", ex); + } + } try { @@ -342,6 +340,7 @@ namespace Tgstation.Server.Host.Components.Session chat, assemblyInformationProvider, loggerFactory.CreateLogger(), + PostLifetime, launchParameters.StartupTimeout, false, apiValidate); @@ -417,6 +416,7 @@ namespace Tgstation.Server.Host.Components.Session chat, assemblyInformationProvider, loggerFactory.CreateLogger(), + () => Task.CompletedTask, null, true, false); diff --git a/src/Tgstation.Server.Host/Components/Watchdog/BasicWatchdog.cs b/src/Tgstation.Server.Host/Components/Watchdog/BasicWatchdog.cs index 90791ab63e..31ad687b96 100644 --- a/src/Tgstation.Server.Host/Components/Watchdog/BasicWatchdog.cs +++ b/src/Tgstation.Server.Host/Components/Watchdog/BasicWatchdog.cs @@ -163,9 +163,13 @@ namespace Tgstation.Server.Host.Components.Watchdog } /// - protected override void DisposeAndNullControllersImpl() + protected override async Task DisposeAndNullControllersImpl() { - Server?.Dispose(); + var disposeTask = Server?.DisposeAsync(); + if (!disposeTask.HasValue) + return; + + await disposeTask.Value.ConfigureAwait(false); Server = null; gracefulRebootRequired = false; } @@ -227,7 +231,7 @@ namespace Tgstation.Server.Host.Components.Watchdog { // kill the controllers bool serverWasActive = Server != null; - DisposeAndNullControllers(); + await DisposeAndNullControllers(default).ConfigureAwait(false); // server didn't get control of this dmb if (dmbToUse != null && !serverWasActive) diff --git a/src/Tgstation.Server.Host/Components/Watchdog/IWatchdog.cs b/src/Tgstation.Server.Host/Components/Watchdog/IWatchdog.cs index b593667a5b..1b822a7e65 100644 --- a/src/Tgstation.Server.Host/Components/Watchdog/IWatchdog.cs +++ b/src/Tgstation.Server.Host/Components/Watchdog/IWatchdog.cs @@ -12,7 +12,7 @@ namespace Tgstation.Server.Host.Components.Watchdog /// /// Runs and monitors the twin server controllers /// - public interface IWatchdog : IHostedService, IDisposable, IEventConsumer, IRenameNotifyee + public interface IWatchdog : IHostedService, IAsyncDisposable, IEventConsumer, IRenameNotifyee { /// /// The current . diff --git a/src/Tgstation.Server.Host/Components/Watchdog/WatchdogBase.cs b/src/Tgstation.Server.Host/Components/Watchdog/WatchdogBase.cs index 0699f96772..20b5b6c628 100644 --- a/src/Tgstation.Server.Host/Components/Watchdog/WatchdogBase.cs +++ b/src/Tgstation.Server.Host/Components/Watchdog/WatchdogBase.cs @@ -59,11 +59,6 @@ namespace Tgstation.Server.Host.Components.Watchdog /// protected TaskCompletionSource ActiveParametersUpdated { get; set; } - /// - /// The for the . - /// - protected SemaphoreSlim Semaphore { get; } - /// /// The for the . /// @@ -94,6 +89,16 @@ namespace Tgstation.Server.Host.Components.Watchdog /// readonly Api.Models.Instance instance; + /// + /// The for the . + /// + readonly SemaphoreSlim synchronizationSemaphore; + + /// + /// used for . + /// + readonly SemaphoreSlim controllerDisposeSemaphore; + /// /// The for the /// @@ -124,11 +129,6 @@ namespace Tgstation.Server.Host.Components.Watchdog /// readonly IEventConsumer eventConsumer; - /// - /// used for . - /// - readonly object controllerDisposeLock; - /// /// If the should in /// @@ -165,7 +165,7 @@ namespace Tgstation.Server.Host.Components.Watchdog bool releaseServers; /// - /// If the has been d. + /// If the has been 'd. /// bool disposed; @@ -224,16 +224,17 @@ namespace Tgstation.Server.Host.Components.Watchdog ActiveLaunchParameters = initialLaunchParameters; releaseServers = false; ActiveParametersUpdated = new TaskCompletionSource(); - controllerDisposeLock = new object(); restartRegistration = serverControl.RegisterForRestart(this); try { - Semaphore = new SemaphoreSlim(1); + synchronizationSemaphore = new SemaphoreSlim(1); + controllerDisposeSemaphore = new SemaphoreSlim(1); } catch { restartRegistration.Dispose(); + synchronizationSemaphore?.Dispose(); throw; } @@ -241,18 +242,19 @@ namespace Tgstation.Server.Host.Components.Watchdog } /// - public void Dispose() + public async ValueTask DisposeAsync() { Logger.LogTrace("Disposing..."); - Semaphore.Dispose(); + synchronizationSemaphore.Dispose(); restartRegistration.Dispose(); - DisposeAndNullControllers(); + await DisposeAndNullControllers(default).ConfigureAwait(false); + controllerDisposeSemaphore.Dispose(); monitorCts?.Dispose(); disposed = true; } /// - /// Implementation of . Does not lock + /// Implementation of . Does not lock /// /// If the termination will be delayed until a reboot is detected in the active server's DMAPI and this function will return immediately /// If the termination will be announced using @@ -272,8 +274,6 @@ namespace Tgstation.Server.Host.Components.Watchdog await StopMonitor().ConfigureAwait(false); - DisposeAndNullControllers(); - LastLaunchParameters = null; await chatTask.ConfigureAwait(false); @@ -330,7 +330,7 @@ namespace Tgstation.Server.Host.Components.Watchdog var message4 = $"DEFCON 1: Four heartbeats have been missed! {actionTaken}..."; Logger.LogWarning(message4); await Chat.SendWatchdogMessage(message4, false, cancellationToken).ConfigureAwait(false); - DisposeAndNullControllers(); + await DisposeAndNullControllers(cancellationToken).ConfigureAwait(false); return shouldShutdown ? MonitorAction.Exit : MonitorAction.Restart; default: Logger.LogError("Invalid heartbeats missed count: {0}", heartbeatsMissed); @@ -477,7 +477,7 @@ namespace Tgstation.Server.Host.Components.Watchdog protected async Task ReattachFailure(Task chatTask, CancellationToken cancellationToken) { // we lost the server, just restart entirely - DisposeAndNullControllers(); + await DisposeAndNullControllers(default).ConfigureAwait(false); const string FailReattachMessage = "Unable to properly reattach to server! Restarting watchdog..."; Logger.LogWarning(FailReattachMessage); @@ -493,16 +493,19 @@ namespace Tgstation.Server.Host.Components.Watchdog /// /// Call and null the fields for all s. /// - protected abstract void DisposeAndNullControllersImpl(); + /// A representing the running operation. + protected abstract Task DisposeAndNullControllersImpl(); /// /// Wrapper for under a locked context. /// - protected void DisposeAndNullControllers() + /// The for the operation. + /// A representing the running operation. + protected async Task DisposeAndNullControllers(CancellationToken cancellationToken) { Logger.LogTrace("DisposeAndNullControllers"); - lock (controllerDisposeLock) - DisposeAndNullControllersImpl(); + using (await SemaphoreSlimContext.Lock(controllerDisposeSemaphore, cancellationToken).ConfigureAwait(false)) + await DisposeAndNullControllersImpl().ConfigureAwait(false); } /// @@ -529,14 +532,14 @@ namespace Tgstation.Server.Host.Components.Watchdog private async Task MonitorRestart(CancellationToken cancellationToken) { Logger.LogTrace("Monitor restart!"); - DisposeAndNullControllers(); + await DisposeAndNullControllers(cancellationToken).ConfigureAwait(false); var chatTask = Task.CompletedTask; for (var retryAttempts = 1; ; ++retryAttempts) { Status = WatchdogStatus.Restoring; Exception launchException; - using (await SemaphoreSlimContext.Lock(Semaphore, cancellationToken).ConfigureAwait(false)) + using (await SemaphoreSlimContext.Lock(synchronizationSemaphore, cancellationToken).ConfigureAwait(false)) try { // use LaunchImplNoLock without announcements or restarting the monitor @@ -633,7 +636,7 @@ namespace Tgstation.Server.Host.Components.Watchdog Logger.LogTrace("Monitor activated"); // always run HandleMonitorWakeup from the context of the semaphore lock - using (await SemaphoreSlimContext.Lock(Semaphore, cancellationToken).ConfigureAwait(false)) + using (await SemaphoreSlimContext.Lock(synchronizationSemaphore, cancellationToken).ConfigureAwait(false)) { // Set this sooner so chat sends don't hold us up if (activeServerLifetime.IsCompleted) @@ -736,11 +739,11 @@ namespace Tgstation.Server.Host.Components.Watchdog if (releaseServers) { Logger.LogTrace("Detaching servers..."); - releasedReattachInformation = GetActiveController().Release(); + releasedReattachInformation = await GetActiveController().Release().ConfigureAwait(false); } } - DisposeAndNullControllers(); + await DisposeAndNullControllers(default).ConfigureAwait(false); Status = WatchdogStatus.Offline; Logger.LogTrace("Monitor exiting..."); @@ -758,7 +761,7 @@ namespace Tgstation.Server.Host.Components.Watchdog /// public async Task ChangeSettings(DreamDaemonLaunchParameters launchParameters, CancellationToken cancellationToken) { - using (await SemaphoreSlimContext.Lock(Semaphore, cancellationToken).ConfigureAwait(false)) + using (await SemaphoreSlimContext.Lock(synchronizationSemaphore, cancellationToken).ConfigureAwait(false)) { bool match = launchParameters.CanApplyWithoutReboot(ActiveLaunchParameters); ActiveLaunchParameters = launchParameters; @@ -808,7 +811,7 @@ namespace Tgstation.Server.Host.Components.Watchdog /// public async Task HandleChatCommand(string commandName, string arguments, ChatUser sender, CancellationToken cancellationToken) { - using (await SemaphoreSlimContext.Lock(Semaphore, cancellationToken).ConfigureAwait(false)) + using (await SemaphoreSlimContext.Lock(synchronizationSemaphore, cancellationToken).ConfigureAwait(false)) { if (Status == WatchdogStatus.Offline) return "TGS: Server offline!"; @@ -836,14 +839,14 @@ namespace Tgstation.Server.Host.Components.Watchdog { if (Status != WatchdogStatus.Offline) throw new JobException(ErrorCode.WatchdogRunning); - using (await SemaphoreSlimContext.Lock(Semaphore, cancellationToken).ConfigureAwait(false)) + using (await SemaphoreSlimContext.Lock(synchronizationSemaphore, cancellationToken).ConfigureAwait(false)) await LaunchNoLock(true, true, true, null, cancellationToken).ConfigureAwait(false); } /// public virtual async Task ResetRebootState(CancellationToken cancellationToken) { - using (await SemaphoreSlimContext.Lock(Semaphore, cancellationToken).ConfigureAwait(false)) + using (await SemaphoreSlimContext.Lock(synchronizationSemaphore, cancellationToken).ConfigureAwait(false)) { if (Status == WatchdogStatus.Offline) return; @@ -860,7 +863,7 @@ namespace Tgstation.Server.Host.Components.Watchdog throw new JobException(ErrorCode.WatchdogNotRunning); Logger.LogTrace("Begin Restart. Graceful: {0}", graceful); - using (await SemaphoreSlimContext.Lock(Semaphore, cancellationToken).ConfigureAwait(false)) + using (await SemaphoreSlimContext.Lock(synchronizationSemaphore, cancellationToken).ConfigureAwait(false)) { if (!graceful) { @@ -912,7 +915,7 @@ namespace Tgstation.Server.Host.Components.Watchdog }; await jobManager.RegisterOperation(job, async (j, databaseContextFactory, progressFunction, ct) => { - using (await SemaphoreSlimContext.Lock(Semaphore, ct).ConfigureAwait(false)) + using (await SemaphoreSlimContext.Lock(synchronizationSemaphore, ct).ConfigureAwait(false)) await LaunchNoLock(true, true, true, reattachInfo, ct).ConfigureAwait(false); }, cancellationToken).ConfigureAwait(false); } @@ -942,7 +945,7 @@ namespace Tgstation.Server.Host.Components.Watchdog /// public async Task Terminate(bool graceful, CancellationToken cancellationToken) { - using (await SemaphoreSlimContext.Lock(Semaphore, cancellationToken).ConfigureAwait(false)) + using (await SemaphoreSlimContext.Lock(synchronizationSemaphore, cancellationToken).ConfigureAwait(false)) await TerminateNoLock(graceful, !releaseServers, cancellationToken).ConfigureAwait(false); } diff --git a/src/Tgstation.Server.Host/Components/Watchdog/WindowsWatchdog.cs b/src/Tgstation.Server.Host/Components/Watchdog/WindowsWatchdog.cs index e01a610f19..7fa442369d 100644 --- a/src/Tgstation.Server.Host/Components/Watchdog/WindowsWatchdog.cs +++ b/src/Tgstation.Server.Host/Components/Watchdog/WindowsWatchdog.cs @@ -102,15 +102,15 @@ namespace Tgstation.Server.Host.Components.Watchdog } catch { - Dispose(); + var _ = DisposeAsync(); throw; } } /// - protected override void DisposeAndNullControllersImpl() + protected override async Task DisposeAndNullControllersImpl() { - base.DisposeAndNullControllersImpl(); + await base.DisposeAndNullControllersImpl().ConfigureAwait(false); // If we reach this point, we can guarantee PrepServerForLaunch will be called before starting again. ActiveSwappable = null; diff --git a/src/Tgstation.Server.Host/System/IProcess.cs b/src/Tgstation.Server.Host/System/IProcess.cs index b813d95bbc..d961b65694 100644 --- a/src/Tgstation.Server.Host/System/IProcess.cs +++ b/src/Tgstation.Server.Host/System/IProcess.cs @@ -1,4 +1,5 @@ -using System.Threading; +using System; +using System.Threading; using System.Threading.Tasks; namespace Tgstation.Server.Host.System @@ -6,7 +7,7 @@ namespace Tgstation.Server.Host.System /// /// Abstraction over a /// - interface IProcess : IProcessBase + interface IProcess : IProcessBase, IDisposable { /// /// The ' ID diff --git a/src/Tgstation.Server.Host/System/IProcessBase.cs b/src/Tgstation.Server.Host/System/IProcessBase.cs index c861f8ae5f..3530b619b8 100644 --- a/src/Tgstation.Server.Host/System/IProcessBase.cs +++ b/src/Tgstation.Server.Host/System/IProcessBase.cs @@ -1,5 +1,4 @@ -using System; -using System.Threading; +using System.Threading; using System.Threading.Tasks; namespace Tgstation.Server.Host.System @@ -7,7 +6,7 @@ namespace Tgstation.Server.Host.System /// /// Represents process lifetime /// - interface IProcessBase : IDisposable + interface IProcessBase { /// /// The resulting in the exit code of the process From 14557617246631147e6335b647a47b431ec62b64 Mon Sep 17 00:00:00 2001 From: Jordan Brown Date: Sat, 11 Jul 2020 10:57:41 -0400 Subject: [PATCH 07/68] Document all usages of default cancellation tokens --- src/Tgstation.Server.Host.Service/Program.cs | 3 +++ .../Components/Chat/Providers/DiscordProvider.cs | 2 ++ .../Components/Deployment/DreamMaker.cs | 3 +++ src/Tgstation.Server.Host/Components/Instance.cs | 2 ++ .../Components/Repository/RepositoryManager.cs | 2 ++ .../Components/Session/SessionControllerFactory.cs | 1 + .../Components/Watchdog/BasicWatchdog.cs | 2 ++ .../Components/Watchdog/WatchdogBase.cs | 4 ++++ src/Tgstation.Server.Host/Controllers/ChatController.cs | 4 +++- src/Tgstation.Server.Host/Controllers/InstanceController.cs | 5 ++++- .../Controllers/RepositoryController.cs | 1 + src/Tgstation.Server.Host/Jobs/JobManager.cs | 1 + src/Tgstation.Server.Host/Program.cs | 1 + src/Tgstation.Server.Host/Setup/SetupWizard.cs | 1 + 14 files changed, 30 insertions(+), 2 deletions(-) diff --git a/src/Tgstation.Server.Host.Service/Program.cs b/src/Tgstation.Server.Host.Service/Program.cs index 3c20b6c98e..ac37e65e5a 100644 --- a/src/Tgstation.Server.Host.Service/Program.cs +++ b/src/Tgstation.Server.Host.Service/Program.cs @@ -150,7 +150,10 @@ namespace Tgstation.Server.Host.Service ServiceBase.Run(service); if (Configure) + { + // DCT: None available await WatchdogFactory.CreateWatchdog(loggerFactory).RunAsync(true, Array.Empty(), default).ConfigureAwait(false); + } } } } diff --git a/src/Tgstation.Server.Host/Components/Chat/Providers/DiscordProvider.cs b/src/Tgstation.Server.Host/Components/Chat/Providers/DiscordProvider.cs index 246e2ec74e..5d98784dbb 100644 --- a/src/Tgstation.Server.Host/Components/Chat/Providers/DiscordProvider.cs +++ b/src/Tgstation.Server.Host/Components/Chat/Providers/DiscordProvider.cs @@ -104,6 +104,8 @@ namespace Tgstation.Server.Host.Components.Chat.Providers if (mentionedUs) { Logger.LogTrace("Ignoring mention from {0} ({1}) by {2} ({3}). Channel not mapped!", e.Channel.Id, e.Channel.Name, e.Author.Id, e.Author.Username); + + // DCT: None available await SendMessage(e.Channel.Id, "I do not respond to this channel!", default).ConfigureAwait(false); } diff --git a/src/Tgstation.Server.Host/Components/Deployment/DreamMaker.cs b/src/Tgstation.Server.Host/Components/Deployment/DreamMaker.cs index 4c4508a45a..89667e14ec 100644 --- a/src/Tgstation.Server.Host/Components/Deployment/DreamMaker.cs +++ b/src/Tgstation.Server.Host/Components/Deployment/DreamMaker.cs @@ -641,6 +641,8 @@ namespace Tgstation.Server.Host.Components.Deployment { // So we need to un-commit the compile job if the above throws databaseContext.CompileJobs.Remove(compileJob); + + // DCT: Cancellation token is for job, operation must run regardless await databaseContext.Save(default).ConfigureAwait(false); throw; } @@ -769,6 +771,7 @@ namespace Tgstation.Server.Host.Components.Deployment } catch (OperationCanceledException) { + // DCT: Cancellation token is for job, delaying here is fine await eventConsumer.HandleEvent(EventType.CompileCancelled, null, default).ConfigureAwait(false); throw; } diff --git a/src/Tgstation.Server.Host/Components/Instance.cs b/src/Tgstation.Server.Host/Components/Instance.cs index 33058744c2..612ba18c53 100644 --- a/src/Tgstation.Server.Host/Components/Instance.cs +++ b/src/Tgstation.Server.Host/Components/Instance.cs @@ -335,6 +335,7 @@ namespace Tgstation.Server.Host.Components } catch { + // DCT: Cancellation token is for job, operation must run regardless await repo.ResetToSha(startSha, progressReporter, default).ConfigureAwait(false); throw; } @@ -345,6 +346,7 @@ namespace Tgstation.Server.Host.Components deploySha = repoHead; }, cancellationToken).ConfigureAwait(false); + // DCT: First token will cancel the job, second is for cancelling the cancellation, unwanted await jobManager.WaitForJobCompletion(repositoryUpdateJob, user, cancellationToken, default).ConfigureAwait(false); if (deploySha == null) diff --git a/src/Tgstation.Server.Host/Components/Repository/RepositoryManager.cs b/src/Tgstation.Server.Host/Components/Repository/RepositoryManager.cs index 55c19fdc82..f75bae0fc0 100644 --- a/src/Tgstation.Server.Host/Components/Repository/RepositoryManager.cs +++ b/src/Tgstation.Server.Host/Components/Repository/RepositoryManager.cs @@ -148,6 +148,8 @@ namespace Tgstation.Server.Host.Components.Repository try { logger.LogTrace("Deleting partially cloned repository..."); + + // DCT: Cancellation token is for job, operation must run regardless await ioManager.DeleteDirectory(repositoryPath, default).ConfigureAwait(false); } catch (Exception e) diff --git a/src/Tgstation.Server.Host/Components/Session/SessionControllerFactory.cs b/src/Tgstation.Server.Host/Components/Session/SessionControllerFactory.cs index be29252054..3b859791ab 100644 --- a/src/Tgstation.Server.Host/Components/Session/SessionControllerFactory.cs +++ b/src/Tgstation.Server.Host/Components/Session/SessionControllerFactory.cs @@ -287,6 +287,7 @@ namespace Tgstation.Server.Host.Components.Session { try { + // DCT: No token available await ioManager.DeleteFile(logFilePath, default).ConfigureAwait(false); } catch (Exception ex) diff --git a/src/Tgstation.Server.Host/Components/Watchdog/BasicWatchdog.cs b/src/Tgstation.Server.Host/Components/Watchdog/BasicWatchdog.cs index 31ad687b96..528fbb706f 100644 --- a/src/Tgstation.Server.Host/Components/Watchdog/BasicWatchdog.cs +++ b/src/Tgstation.Server.Host/Components/Watchdog/BasicWatchdog.cs @@ -231,6 +231,8 @@ namespace Tgstation.Server.Host.Components.Watchdog { // kill the controllers bool serverWasActive = Server != null; + + // DCT: Operation must always run await DisposeAndNullControllers(default).ConfigureAwait(false); // server didn't get control of this dmb diff --git a/src/Tgstation.Server.Host/Components/Watchdog/WatchdogBase.cs b/src/Tgstation.Server.Host/Components/Watchdog/WatchdogBase.cs index 20b5b6c628..c1b3a7c85a 100644 --- a/src/Tgstation.Server.Host/Components/Watchdog/WatchdogBase.cs +++ b/src/Tgstation.Server.Host/Components/Watchdog/WatchdogBase.cs @@ -247,6 +247,8 @@ namespace Tgstation.Server.Host.Components.Watchdog Logger.LogTrace("Disposing..."); synchronizationSemaphore.Dispose(); restartRegistration.Dispose(); + + // DCT: None available, Operation must always run await DisposeAndNullControllers(default).ConfigureAwait(false); controllerDisposeSemaphore.Dispose(); monitorCts?.Dispose(); @@ -477,6 +479,7 @@ namespace Tgstation.Server.Host.Components.Watchdog protected async Task ReattachFailure(Task chatTask, CancellationToken cancellationToken) { // we lost the server, just restart entirely + // DCT: Operation must always run await DisposeAndNullControllers(default).ConfigureAwait(false); const string FailReattachMessage = "Unable to properly reattach to server! Restarting watchdog..."; Logger.LogWarning(FailReattachMessage); @@ -743,6 +746,7 @@ namespace Tgstation.Server.Host.Components.Watchdog } } + // DCT: Operation must always run await DisposeAndNullControllers(default).ConfigureAwait(false); Status = WatchdogStatus.Offline; diff --git a/src/Tgstation.Server.Host/Controllers/ChatController.cs b/src/Tgstation.Server.Host/Controllers/ChatController.cs index f642156e0f..1c1e8d9e4b 100644 --- a/src/Tgstation.Server.Host/Controllers/ChatController.cs +++ b/src/Tgstation.Server.Host/Controllers/ChatController.cs @@ -129,8 +129,10 @@ namespace Tgstation.Server.Host.Controllers { // undo the add DatabaseContext.ChatBots.Remove(dbModel); + + // DCTx2: Operations must always run await DatabaseContext.Save(default).ConfigureAwait(false); - await instance.Chat.DeleteConnection(dbModel.Id, cancellationToken).ConfigureAwait(false); + await instance.Chat.DeleteConnection(dbModel.Id, default).ConfigureAwait(false); throw; } diff --git a/src/Tgstation.Server.Host/Controllers/InstanceController.cs b/src/Tgstation.Server.Host/Controllers/InstanceController.cs index 21275a96f3..ece13f2aad 100644 --- a/src/Tgstation.Server.Host/Controllers/InstanceController.cs +++ b/src/Tgstation.Server.Host/Controllers/InstanceController.cs @@ -296,6 +296,7 @@ namespace Tgstation.Server.Host.Controllers // oh shit delete the model DatabaseContext.Instances.Remove(newInstance); + // DCT: Operation must always run await DatabaseContext.Save(default).ConfigureAwait(false); throw; } @@ -341,7 +342,7 @@ namespace Tgstation.Server.Host.Controllers DatabaseContext.Instances.Remove(originalModel); var attachFileName = ioManager.ConcatPath(originalModel.Path, InstanceAttachFileName); - await ioManager.WriteAllBytes(attachFileName, Array.Empty(), default).ConfigureAwait(false); + await ioManager.WriteAllBytes(attachFileName, Array.Empty(), cancellationToken).ConfigureAwait(false); await DatabaseContext.Save(cancellationToken).ConfigureAwait(false); // cascades everything return NoContent(); } @@ -484,6 +485,8 @@ namespace Tgstation.Server.Host.Controllers originalModel.DreamDaemonSettings.AutoStart = oldAutoStart; if (originalModelPath != null) originalModel.Path = originalModelPath; + + // DCT: Operation must always run await DatabaseContext.Save(default).ConfigureAwait(false); throw; } diff --git a/src/Tgstation.Server.Host/Controllers/RepositoryController.cs b/src/Tgstation.Server.Host/Controllers/RepositoryController.cs index 9b14db234b..aa6c2fd8c1 100644 --- a/src/Tgstation.Server.Host/Controllers/RepositoryController.cs +++ b/src/Tgstation.Server.Host/Controllers/RepositoryController.cs @@ -856,6 +856,7 @@ namespace Tgstation.Server.Host.Controllers numSteps = 2; // Forget what we've done and abort + // DCTx2: Cancellation token is for job, operations should always run await repo.CheckoutObject(startReference ?? startSha, NextProgressReporter(), default).ConfigureAwait(false); if (startReference != null && repo.Head != startSha) await repo.ResetToSha(startSha, NextProgressReporter(), default).ConfigureAwait(false); diff --git a/src/Tgstation.Server.Host/Jobs/JobManager.cs b/src/Tgstation.Server.Host/Jobs/JobManager.cs index 5cfa3d94f9..113ec63cf0 100644 --- a/src/Tgstation.Server.Host/Jobs/JobManager.cs +++ b/src/Tgstation.Server.Host/Jobs/JobManager.cs @@ -128,6 +128,7 @@ namespace Tgstation.Server.Host.Jobs attachedJob.ErrorCode = job.ErrorCode; attachedJob.Cancelled = job.Cancelled; + // DCT: Cancellation token is for job, operation should always run await databaseContext.Save(default).ConfigureAwait(false); }).ConfigureAwait(false); } diff --git a/src/Tgstation.Server.Host/Program.cs b/src/Tgstation.Server.Host/Program.cs index 77d2dc2d09..ab0b58943d 100644 --- a/src/Tgstation.Server.Host/Program.cs +++ b/src/Tgstation.Server.Host/Program.cs @@ -82,6 +82,7 @@ namespace Tgstation.Server.Host { if (updatePath != null) { + // DCT: None available, operation should always run await ServerFactory.IOManager.WriteAllBytes(updatePath, Encoding.UTF8.GetBytes(e.ToString()), default).ConfigureAwait(false); return 2; } diff --git a/src/Tgstation.Server.Host/Setup/SetupWizard.cs b/src/Tgstation.Server.Host/Setup/SetupWizard.cs index 4d4942eef2..fe7fdf4e00 100644 --- a/src/Tgstation.Server.Host/Setup/SetupWizard.cs +++ b/src/Tgstation.Server.Host/Setup/SetupWizard.cs @@ -828,6 +828,7 @@ namespace Tgstation.Server.Host.Setup async Task HandleSetupCancel() { + // DCTx2: Operation should always run await console.WriteAsync(String.Empty, true, default).ConfigureAwait(false); await console.WriteAsync("Aborting setup!", true, default).ConfigureAwait(false); } From 24f0034e3a0f8e0b5bc77ab86de53434eb7c42d0 Mon Sep 17 00:00:00 2001 From: Jordan Brown Date: Sat, 11 Jul 2020 10:58:10 -0400 Subject: [PATCH 08/68] Update a comment about what the database seeder is for --- src/Tgstation.Server.Host/Database/DatabaseSeeder.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Tgstation.Server.Host/Database/DatabaseSeeder.cs b/src/Tgstation.Server.Host/Database/DatabaseSeeder.cs index 8982ce2205..c8fcde69cd 100644 --- a/src/Tgstation.Server.Host/Database/DatabaseSeeder.cs +++ b/src/Tgstation.Server.Host/Database/DatabaseSeeder.cs @@ -104,7 +104,7 @@ namespace Tgstation.Server.Host.Database } /// - /// Correct invalid database data caused by previous versions. + /// Correct invalid database data caused by previous versions (NOT user fuckery). /// /// The to sanitize. /// The for the operation. From ba0f63ef9ce8648098770e6da6768b86a65f0fc4 Mon Sep 17 00:00:00 2001 From: Jordan Brown Date: Sat, 11 Jul 2020 10:58:23 -0400 Subject: [PATCH 09/68] Minor code formatting change --- tests/Tgstation.Server.Tests/IntegrationTest.cs | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/tests/Tgstation.Server.Tests/IntegrationTest.cs b/tests/Tgstation.Server.Tests/IntegrationTest.cs index 8de906e8fe..44de2583a4 100644 --- a/tests/Tgstation.Server.Tests/IntegrationTest.cs +++ b/tests/Tgstation.Server.Tests/IntegrationTest.cs @@ -123,7 +123,10 @@ namespace Tgstation.Server.Tests using var hardTimeoutCts = new CancellationTokenSource(); hardTimeoutCts.CancelAfter(new TimeSpan(0, 9, 45)); var hardTimeoutCancellationToken = hardTimeoutCts.Token; - hardTimeoutCancellationToken.Register(() => Console.WriteLine($"[{DateTimeOffset.Now}] TEST TIMEOUT HARD!")); + hardTimeoutCancellationToken.Register(() => + { + Console.WriteLine($"[{DateTimeOffset.Now}] TEST TIMEOUT HARD!"); + }); using var softTimeoutCts = CancellationTokenSource.CreateLinkedTokenSource(hardTimeoutCancellationToken); softTimeoutCts.CancelAfter(new TimeSpan(0, 9, 15)); From 6139b0236185d7192b02f86c44aa18c39ba9ae8e Mon Sep 17 00:00:00 2001 From: Jordan Brown Date: Sat, 11 Jul 2020 11:03:39 -0400 Subject: [PATCH 10/68] Fix a hang in detaching --- .../Components/Session/SessionController.cs | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/src/Tgstation.Server.Host/Components/Session/SessionController.cs b/src/Tgstation.Server.Host/Components/Session/SessionController.cs index 3c867094f5..7ee1624b2a 100644 --- a/src/Tgstation.Server.Host/Components/Session/SessionController.cs +++ b/src/Tgstation.Server.Host/Components/Session/SessionController.cs @@ -295,8 +295,11 @@ namespace Tgstation.Server.Host.Components.Session reattachTopicCts.Dispose(); } - // finish the async callback - await Lifetime.ConfigureAwait(false); + if (!released) + { + // finish the async callback + await Lifetime.ConfigureAwait(false); + } } /// From e4734463156926c405b09ec9221a630f3f626bdb Mon Sep 17 00:00:00 2001 From: Jordan Brown Date: Sat, 11 Jul 2020 12:13:56 -0400 Subject: [PATCH 11/68] Create a system user for jobs created by TGS - API Version bump to 7.0.2 - Deprecate error code 46 - Instance user IDs can never be null - Make a IUsersClient.GetId more covariant (Right word?) --- build/Version.props | 2 +- src/Tgstation.Server.Api/Models/ErrorCode.cs | 1 + .../Models/InstanceUser.cs | 3 +- .../Components/InstanceUserClient.cs | 2 +- src/Tgstation.Server.Client/IUsersClient.cs | 4 +- src/Tgstation.Server.Client/UsersClient.cs | 2 +- .../Components/Instance.cs | 14 ++--- .../Components/Watchdog/WatchdogBase.cs | 13 ++--- .../Controllers/InstanceController.cs | 2 +- .../Controllers/InstanceUserController.cs | 39 ++++++------- .../Controllers/UserController.cs | 14 ++++- .../Database/DatabaseSeeder.cs | 58 ++++++++++++++++++- src/Tgstation.Server.Host/Models/User.cs | 5 ++ tests/Tgstation.Server.Tests/UsersTest.cs | 24 ++++++-- 14 files changed, 130 insertions(+), 53 deletions(-) diff --git a/build/Version.props b/build/Version.props index 07bb599fbd..3450bd90c6 100644 --- a/build/Version.props +++ b/build/Version.props @@ -4,7 +4,7 @@ 4.4.1 2.0.0 - 7.0.1 + 7.0.2 8.0.0 5.2.2 0.4.0 diff --git a/src/Tgstation.Server.Api/Models/ErrorCode.cs b/src/Tgstation.Server.Api/Models/ErrorCode.cs index a3f8e0fdc6..b54912874a 100644 --- a/src/Tgstation.Server.Api/Models/ErrorCode.cs +++ b/src/Tgstation.Server.Api/Models/ErrorCode.cs @@ -291,6 +291,7 @@ namespace Tgstation.Server.Api.Models /// Attempted to update a or without its ID. /// [Description("Missing user ID!")] + [Obsolete("Deprecated in favor of code 2", true)] UserMissingId, /// diff --git a/src/Tgstation.Server.Api/Models/InstanceUser.cs b/src/Tgstation.Server.Api/Models/InstanceUser.cs index a77d6640b8..569d6516e1 100644 --- a/src/Tgstation.Server.Api/Models/InstanceUser.cs +++ b/src/Tgstation.Server.Api/Models/InstanceUser.cs @@ -11,8 +11,7 @@ namespace Tgstation.Server.Api.Models /// /// The of the the belongs to /// - [Required] - public long? UserId { get; set; } + public long UserId { get; set; } /// /// The of the diff --git a/src/Tgstation.Server.Client/Components/InstanceUserClient.cs b/src/Tgstation.Server.Client/Components/InstanceUserClient.cs index b71b45359f..1dd1659ed7 100644 --- a/src/Tgstation.Server.Client/Components/InstanceUserClient.cs +++ b/src/Tgstation.Server.Client/Components/InstanceUserClient.cs @@ -38,7 +38,7 @@ namespace Tgstation.Server.Client.Components public Task Delete(InstanceUser instanceUser, CancellationToken cancellationToken) => apiClient.Delete( Routes.SetID( Routes.InstanceUser, - instanceUser.UserId ?? throw new ArgumentException("Missing instanceUser.UserId!", nameof(instanceUser))), + instanceUser.UserId), instance.Id, cancellationToken); diff --git a/src/Tgstation.Server.Client/IUsersClient.cs b/src/Tgstation.Server.Client/IUsersClient.cs index b0260c3174..8cf253f1bd 100644 --- a/src/Tgstation.Server.Client/IUsersClient.cs +++ b/src/Tgstation.Server.Client/IUsersClient.cs @@ -20,10 +20,10 @@ namespace Tgstation.Server.Client /// /// Get a specific /// - /// The to get + /// The to get. /// The for the operation /// A resulting in the requested - Task GetId(User user, CancellationToken cancellationToken); + Task GetId(Api.Models.Internal.User user, CancellationToken cancellationToken); /// /// List all s diff --git a/src/Tgstation.Server.Client/UsersClient.cs b/src/Tgstation.Server.Client/UsersClient.cs index 9d4aa53de6..25ff956856 100644 --- a/src/Tgstation.Server.Client/UsersClient.cs +++ b/src/Tgstation.Server.Client/UsersClient.cs @@ -28,7 +28,7 @@ namespace Tgstation.Server.Client public Task Create(UserUpdate user, CancellationToken cancellationToken) => apiClient.Create(Routes.User, user ?? throw new ArgumentNullException(nameof(user)), cancellationToken); /// - public Task GetId(User user, CancellationToken cancellationToken) => apiClient.Read(Routes.SetID(Routes.User, user?.Id ?? throw new ArgumentNullException(nameof(user))), cancellationToken); + public Task GetId(Api.Models.Internal.User user, CancellationToken cancellationToken) => apiClient.Read(Routes.SetID(Routes.User, user?.Id ?? throw new ArgumentNullException(nameof(user))), cancellationToken); /// public Task> List(CancellationToken cancellationToken) => apiClient.Read>(Routes.ListRoute(Routes.User), cancellationToken); diff --git a/src/Tgstation.Server.Host/Components/Instance.cs b/src/Tgstation.Server.Host/Components/Instance.cs index 612ba18c53..1617658bf5 100644 --- a/src/Tgstation.Server.Host/Components/Instance.cs +++ b/src/Tgstation.Server.Host/Components/Instance.cs @@ -164,12 +164,12 @@ namespace Tgstation.Server.Host.Components await eventConsumer.HandleEvent(EventType.InstanceAutoUpdateStart, new List(), cancellationToken).ConfigureAwait(false); try { - User user = null; + User systemUser = null; await databaseContextFactory.UseContext( - async (db) => user = await db + async (db) => systemUser = await db .Users .AsQueryable() - .Where(x => x.CanonicalName == User.CanonicalizeName(Api.Models.User.AdminName)) + .Where(x => x.CanonicalName == User.CanonicalizeName(User.TgsSystemUserName)) .FirstAsync(cancellationToken) .ConfigureAwait(false)) .ConfigureAwait(false); @@ -182,7 +182,7 @@ namespace Tgstation.Server.Host.Components Description = "Scheduled repository update", CancelRightsType = RightsType.Repository, CancelRight = (ulong)RepositoryRights.CancelPendingChanges, - StartedBy = user + StartedBy = systemUser }; string deploySha = null; @@ -347,7 +347,7 @@ namespace Tgstation.Server.Host.Components }, cancellationToken).ConfigureAwait(false); // DCT: First token will cancel the job, second is for cancelling the cancellation, unwanted - await jobManager.WaitForJobCompletion(repositoryUpdateJob, user, cancellationToken, default).ConfigureAwait(false); + await jobManager.WaitForJobCompletion(repositoryUpdateJob, systemUser, cancellationToken, default).ConfigureAwait(false); if (deploySha == null) { @@ -364,7 +364,7 @@ namespace Tgstation.Server.Host.Components // finally set up the job var compileProcessJob = new Job { - StartedBy = user, + StartedBy = systemUser, Instance = repositoryUpdateJob.Instance, Description = "Scheduled code deployment", CancelRightsType = RightsType.DreamMaker, @@ -376,7 +376,7 @@ namespace Tgstation.Server.Host.Components DreamMaker.DeploymentProcess, cancellationToken).ConfigureAwait(false); - await jobManager.WaitForJobCompletion(compileProcessJob, user, default, cancellationToken).ConfigureAwait(false); + await jobManager.WaitForJobCompletion(compileProcessJob, systemUser, default, cancellationToken).ConfigureAwait(false); } catch (OperationCanceledException) { diff --git a/src/Tgstation.Server.Host/Components/Watchdog/WatchdogBase.cs b/src/Tgstation.Server.Host/Components/Watchdog/WatchdogBase.cs index c1b3a7c85a..e2b4bbb176 100644 --- a/src/Tgstation.Server.Host/Components/Watchdog/WatchdogBase.cs +++ b/src/Tgstation.Server.Host/Components/Watchdog/WatchdogBase.cs @@ -892,23 +892,18 @@ namespace Tgstation.Server.Host.Components.Watchdog if (!autoStart && reattachInfo == null) return; - long? adminUserId = null; - + Models.User systemUser = null; await databaseContextFactory.UseContext( - async db => adminUserId = await db + async db => systemUser = await db .Users .AsQueryable() - .Where(x => x.CanonicalName == Models.User.CanonicalizeName(Api.Models.User.AdminName)) - .Select(x => x.Id) + .Where(x => x.CanonicalName == Models.User.CanonicalizeName(Models.User.TgsSystemUserName)) .FirstAsync(cancellationToken) .ConfigureAwait(false)) .ConfigureAwait(false); var job = new Models.Job { - StartedBy = new Models.User - { - Id = adminUserId.Value - }, + StartedBy = systemUser, Instance = new Models.Instance { Id = instance.Id diff --git a/src/Tgstation.Server.Host/Controllers/InstanceController.cs b/src/Tgstation.Server.Host/Controllers/InstanceController.cs index ece13f2aad..6efd51f9eb 100644 --- a/src/Tgstation.Server.Host/Controllers/InstanceController.cs +++ b/src/Tgstation.Server.Host/Controllers/InstanceController.cs @@ -122,7 +122,7 @@ namespace Tgstation.Server.Host.Controllers if (userToModify == null) userToModify = new Models.InstanceUser() { - UserId = AuthenticationContext.User.Id + UserId = AuthenticationContext.User.Id.Value }; userToModify.ByondRights = RightsHelper.AllRights(); userToModify.ChatBotRights = RightsHelper.AllRights(); diff --git a/src/Tgstation.Server.Host/Controllers/InstanceUserController.cs b/src/Tgstation.Server.Host/Controllers/InstanceUserController.cs index dbb3ff8dae..480698e9c7 100644 --- a/src/Tgstation.Server.Host/Controllers/InstanceUserController.cs +++ b/src/Tgstation.Server.Host/Controllers/InstanceUserController.cs @@ -39,22 +39,6 @@ namespace Tgstation.Server.Host.Controllers true) { } - /// - /// Checks a for errors. - /// - /// The to check - /// The to take if this is not a new - IActionResult StandardModelChecks(Api.Models.InstanceUser model) - { - if (model == null) - throw new ArgumentNullException(nameof(model)); - - if (!model.UserId.HasValue) - return BadRequest(new ErrorMessage(ErrorCode.UserMissingId)); - - return null; - } - /// /// Create an . /// @@ -67,8 +51,22 @@ namespace Tgstation.Server.Host.Controllers [ProducesResponseType(typeof(Api.Models.InstanceUser), 201)] public async Task Create([FromBody] Api.Models.InstanceUser model, CancellationToken cancellationToken) { - // Don't check the result as how can a new user have an ID - StandardModelChecks(model); + if (model == null) + throw new ArgumentNullException(nameof(model)); + + var userCanonicalName = await DatabaseContext + .Users + .AsQueryable() + .Where(x => x.Id == model.UserId) + .Select(x => x.CanonicalName) + .FirstOrDefaultAsync(cancellationToken) + .ConfigureAwait(false); + + if (userCanonicalName == default) + return BadRequest(new ErrorMessage(ErrorCode.ModelValidationFailure)); + + if (userCanonicalName == Models.User.CanonicalizeName(Models.User.TgsSystemUserName)) + return Forbid(); var dbUser = new Models.InstanceUser { @@ -104,9 +102,8 @@ namespace Tgstation.Server.Host.Controllers #pragma warning disable CA1506 // TODO: Decomplexify public async Task Update([FromBody] Api.Models.InstanceUser model, CancellationToken cancellationToken) { - var earlyOut = StandardModelChecks(model); - if (earlyOut != null) - return earlyOut; + if (model == null) + throw new ArgumentNullException(nameof(model)); var originalUser = await DatabaseContext .Instances diff --git a/src/Tgstation.Server.Host/Controllers/UserController.cs b/src/Tgstation.Server.Host/Controllers/UserController.cs index dc7a1a2b71..5144039371 100644 --- a/src/Tgstation.Server.Host/Controllers/UserController.cs +++ b/src/Tgstation.Server.Host/Controllers/UserController.cs @@ -193,7 +193,7 @@ namespace Tgstation.Server.Host.Controllers throw new ArgumentNullException(nameof(model)); if (!model.Id.HasValue) - return BadRequest(new ErrorMessage(ErrorCode.UserMissingId)); + return BadRequest(new ErrorMessage(ErrorCode.ModelValidationFailure)); var callerAdministrationRights = (AdministrationRights)AuthenticationContext.GetRight(RightsType.Administration); var passwordEditOnly = !callerAdministrationRights.HasFlag(AdministrationRights.WriteUsers); @@ -211,6 +211,9 @@ namespace Tgstation.Server.Host.Controllers if (originalUser == default) return NotFound(); + if (originalUser.CanonicalName == Models.User.CanonicalizeName(Models.User.TgsSystemUserName)) + return Forbid(); + // Ensure they are only trying to edit password (system identity change will trigger a bad request) if (passwordEditOnly && (model.Id != originalUser.Id @@ -285,7 +288,10 @@ namespace Tgstation.Server.Host.Controllers [ProducesResponseType(typeof(IEnumerable), 200)] public async Task List(CancellationToken cancellationToken) { - var users = await DatabaseContext.Users + var users = await DatabaseContext + .Users + .AsQueryable() + .Where(x => x.CanonicalName != Models.User.CanonicalizeName(Models.User.TgsSystemUserName)) .Include(x => x.CreatedBy) .ToListAsync(cancellationToken).ConfigureAwait(false); return Json(users.Select(x => x.ToApi(true))); @@ -318,6 +324,10 @@ namespace Tgstation.Server.Host.Controllers .FirstOrDefaultAsync(cancellationToken).ConfigureAwait(false); if (user == default) return NotFound(); + + if (user.CanonicalName == Models.User.CanonicalizeName(Models.User.TgsSystemUserName)) + return Forbid(); + return Json(user.ToApi(true)); } } diff --git a/src/Tgstation.Server.Host/Database/DatabaseSeeder.cs b/src/Tgstation.Server.Host/Database/DatabaseSeeder.cs index c8fcde69cd..020121cee3 100644 --- a/src/Tgstation.Server.Host/Database/DatabaseSeeder.cs +++ b/src/Tgstation.Server.Host/Database/DatabaseSeeder.cs @@ -72,11 +72,38 @@ namespace Tgstation.Server.Host.Database this.logger = logger ?? throw new ArgumentNullException(nameof(logger)); } + /// + /// Add a default system to a given + /// + /// The to add a system to + /// An existing , if any. + /// The created system . + static User SeedSystemUser(IDatabaseContext databaseContext, User tgsUser = null) + { + bool alreadyExists = tgsUser != null; + tgsUser ??= new User() + { + CreatedAt = DateTimeOffset.Now, + CanonicalName = User.CanonicalizeName(User.TgsSystemUserName), + }; + + tgsUser.Name = User.TgsSystemUserName; + tgsUser.PasswordHash = "_"; // This can't be hashed + tgsUser.Enabled = false; + tgsUser.InstanceManagerRights = InstanceManagerRights.None; + tgsUser.AdministrationRights = AdministrationRights.None; + + if (!alreadyExists) + databaseContext.Users.Add(tgsUser); + return tgsUser; + } + /// /// Add a default admin to a given /// /// The to add an admin to - void SeedAdminUser(IDatabaseContext databaseContext) + /// The created admin . + User SeedAdminUser(IDatabaseContext databaseContext) { var admin = new User { @@ -89,6 +116,7 @@ namespace Tgstation.Server.Host.Database }; cryptographySuite.SetUserPassword(admin, Api.Models.User.DefaultAdminPassword, true); databaseContext.Users.Add(admin); + return admin; } /// @@ -99,7 +127,14 @@ namespace Tgstation.Server.Host.Database /// A representing the running operation async Task SeedDatabase(IDatabaseContext databaseContext, CancellationToken cancellationToken) { - SeedAdminUser(databaseContext); + var adminUser = SeedAdminUser(databaseContext); + + // Save here because we want admin to have the first DB Id + // The system user isn't shown in the API except by references in the admin user and jobs + await databaseContext.Save(cancellationToken).ConfigureAwait(false); + var tgsUser = SeedSystemUser(databaseContext); + adminUser.CreatedBy = tgsUser; + await databaseContext.Save(cancellationToken).ConfigureAwait(false); } @@ -119,6 +154,25 @@ namespace Tgstation.Server.Host.Database // https://github.com/JamesNK/Newtonsoft.Json/issues/2301 admin.AdministrationRights &= RightsHelper.AllRights(); admin.InstanceManagerRights &= RightsHelper.AllRights(); + + if (admin.CreatedBy == null) + { + var tgsUser = await databaseContext + .Users + .AsQueryable() + .Where(x => x.CanonicalName == User.CanonicalizeName(User.TgsSystemUserName)) + .FirstOrDefaultAsync(cancellationToken) + .ConfigureAwait(false); + + if (tgsUser != null) + { + logger.LogError( + "A user named TGS (Canonically) exists but isn't marked as the admin's creator. This may be because it was created manually. This user is going to be adapted to use as the starter of system jobs."); + } + + tgsUser = SeedSystemUser(databaseContext, tgsUser); + admin.CreatedBy = tgsUser; + } } if (platformIdentifier.IsWindows) diff --git a/src/Tgstation.Server.Host/Models/User.cs b/src/Tgstation.Server.Host/Models/User.cs index bf443501ed..57912ce2ee 100644 --- a/src/Tgstation.Server.Host/Models/User.cs +++ b/src/Tgstation.Server.Host/Models/User.cs @@ -7,6 +7,11 @@ namespace Tgstation.Server.Host.Models /// public sealed class User : Api.Models.Internal.User { + /// + /// Username used when creating jobs automatically. + /// + public const string TgsSystemUserName = "TGS"; + /// /// The hash of the user's password /// diff --git a/tests/Tgstation.Server.Tests/UsersTest.cs b/tests/Tgstation.Server.Tests/UsersTest.cs index a0e49879d9..7241361cf9 100644 --- a/tests/Tgstation.Server.Tests/UsersTest.cs +++ b/tests/Tgstation.Server.Tests/UsersTest.cs @@ -21,18 +21,34 @@ namespace Tgstation.Server.Tests public async Task Run(CancellationToken cancellationToken) { - await TestRetrieveCurrentUser(cancellationToken).ConfigureAwait(false); - await TestCreateSysUser(cancellationToken); - await TestSpamCreation(cancellationToken).ConfigureAwait(false); + await Task.WhenAll( + BasicTests(cancellationToken), + TestCreateSysUser(cancellationToken), + TestSpamCreation(cancellationToken)).ConfigureAwait(false); } - async Task TestRetrieveCurrentUser(CancellationToken cancellationToken) + async Task BasicTests(CancellationToken cancellationToken) { var user = await this.client.Read(cancellationToken).ConfigureAwait(false); Assert.IsNotNull(user); Assert.AreEqual("Admin", user.Name); Assert.IsNull(user.SystemIdentifier); Assert.AreEqual(true, user.Enabled); + + var systemUser = user.CreatedBy; + Assert.IsNotNull(systemUser); + Assert.AreEqual("TGS", systemUser.Name); + Assert.AreEqual(false, systemUser.Enabled); + + var users = await client.List(cancellationToken); + Assert.IsTrue(users.Count > 0); + Assert.IsFalse(users.Any(x => x.Id == systemUser.Id)); + + await ApiAssert.ThrowsException(() => client.GetId(systemUser, cancellationToken), null); + await ApiAssert.ThrowsException(() => client.Update(new UserUpdate + { + Id = systemUser.Id + }, cancellationToken), null); } async Task TestCreateSysUser(CancellationToken cancellationToken) From 13441348b7cbe3e7248db334f51f69bc4fcbdf27 Mon Sep 17 00:00:00 2001 From: Jordan Brown Date: Sat, 11 Jul 2020 12:14:17 -0400 Subject: [PATCH 12/68] Fix a potential NullReferenceException in GetLaunchResult --- .../Components/Session/SessionController.cs | 15 +++++++++------ 1 file changed, 9 insertions(+), 6 deletions(-) diff --git a/src/Tgstation.Server.Host/Components/Session/SessionController.cs b/src/Tgstation.Server.Host/Components/Session/SessionController.cs index 7ee1624b2a..a5c1560ff9 100644 --- a/src/Tgstation.Server.Host/Components/Session/SessionController.cs +++ b/src/Tgstation.Server.Host/Components/Session/SessionController.cs @@ -339,12 +339,15 @@ namespace Tgstation.Server.Host.Components.Session reattachTopicCts.Token) .ConfigureAwait(false); - if (reattachResponse.InteropResponse?.CustomCommands != null) - chatTrackingContext.CustomCommands = reattachResponse.InteropResponse.CustomCommands; - else if (reattachResponse.InteropResponse != null) - logger.LogWarning( - "DMAPI v{0} isn't returning the TGS custom commands list. Functionality added in v5.2.0.", - Dmb.CompileJob.DMApiVersion.Semver()); + if (reattachResponse != null) + { + if (reattachResponse.InteropResponse?.CustomCommands != null) + chatTrackingContext.CustomCommands = reattachResponse.InteropResponse.CustomCommands; + else if (reattachResponse.InteropResponse != null) + logger.LogWarning( + "DMAPI v{0} isn't returning the TGS custom commands list. Functionality added in v5.2.0.", + Dmb.CompileJob.DMApiVersion.Semver()); + } } return result; From 844244aa72098d524569ffebe0e87c8ea3738fba Mon Sep 17 00:00:00 2001 From: Jordan Brown Date: Sun, 12 Jul 2020 11:58:21 -0400 Subject: [PATCH 13/68] Version bump to 4.4.2 --- build/Version.props | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/build/Version.props b/build/Version.props index 3450bd90c6..6b177cd997 100644 --- a/build/Version.props +++ b/build/Version.props @@ -2,7 +2,7 @@ - 4.4.1 + 4.4.2 2.0.0 7.0.2 8.0.0 From 8ac67141553cf0d252cdaad945b6518bb67cb834 Mon Sep 17 00:00:00 2001 From: Jordan Brown Date: Sun, 12 Jul 2020 13:37:13 -0400 Subject: [PATCH 14/68] Remove instance client caching This was so broken, wtf --- build/Version.props | 2 +- .../InstanceManagerClient.cs | 18 +----------------- 2 files changed, 2 insertions(+), 18 deletions(-) diff --git a/build/Version.props b/build/Version.props index 6b177cd997..d3117eac62 100644 --- a/build/Version.props +++ b/build/Version.props @@ -5,7 +5,7 @@ 4.4.2 2.0.0 7.0.2 - 8.0.0 + 8.0.1 5.2.2 0.4.0 1.1.0 diff --git a/src/Tgstation.Server.Client/InstanceManagerClient.cs b/src/Tgstation.Server.Client/InstanceManagerClient.cs index 6a00038d42..9bd9b4846f 100644 --- a/src/Tgstation.Server.Client/InstanceManagerClient.cs +++ b/src/Tgstation.Server.Client/InstanceManagerClient.cs @@ -16,11 +16,6 @@ namespace Tgstation.Server.Client /// readonly IApiClient apiClient; - /// - /// Map of already created s - /// - readonly Dictionary cachedClients; - /// /// Construct an /// @@ -28,8 +23,6 @@ namespace Tgstation.Server.Client public InstanceManagerClient(IApiClient apiClient) { this.apiClient = apiClient ?? throw new ArgumentNullException(nameof(apiClient)); - - cachedClients = new Dictionary(); } /// @@ -51,15 +44,6 @@ namespace Tgstation.Server.Client public Task GrantPermissions(Instance instance, CancellationToken cancellationToken) => apiClient.Patch(Routes.SetID(Routes.InstanceManager, instance?.Id ?? throw new ArgumentNullException(nameof(instance))), cancellationToken); /// - public IInstanceClient CreateClient(Instance instance) - { - if (!cachedClients.TryGetValue(instance?.Id ?? throw new ArgumentNullException(nameof(instance)), out var client)) - { - client = new InstanceClient(apiClient, instance); - cachedClients.Add(instance.Id, client); - } - - return client; - } + public IInstanceClient CreateClient(Instance instance) => new InstanceClient(apiClient, instance); } } \ No newline at end of file From f923d0c87d5002642f5962a47117329fee6da62a Mon Sep 17 00:00:00 2001 From: Jordan Brown Date: Sun, 12 Jul 2020 13:39:18 -0400 Subject: [PATCH 15/68] Component instance usage cleanup - GetInstance now returns null instead of throwing when the instance is offline. To be honest, we should wrap this in a using. - Add safe accessor class for controllers that also performs request validation. - API bump to 7.1.0 - Added ErrorCode 90 for offline instances, replacing the 403 response - Move jobs now move the directory and are more fail safe - Creating instances no longer uppercases the path on disk on Windows --- build/Version.props | 4 +- src/Tgstation.Server.Api/Models/ErrorCode.cs | 6 + .../Components/IInstanceManager.cs | 8 +- .../Components/InstanceManager.cs | 79 +++-- .../Controllers/ApiController.cs | 30 +- .../Controllers/ByondController.cs | 143 +++++---- .../Controllers/ChatController.cs | 101 +++--- .../Controllers/ConfigurationController.cs | 84 +++-- .../Controllers/DreamDaemonController.cs | 298 +++++++++--------- .../Controllers/DreamMakerController.cs | 52 ++- .../Controllers/InstanceController.cs | 96 +++--- .../Controllers/InstanceRequiredController.cs | 104 ++++++ .../Controllers/InstanceUserController.cs | 9 +- .../Controllers/JobController.cs | 9 +- .../Controllers/RepositoryController.cs | 245 ++++++++------ .../Security/AuthenticationContextFactory.cs | 2 +- .../InstanceManagerTest.cs | 12 +- .../Tgstation.Server.Tests/IntegrationTest.cs | 2 + 18 files changed, 765 insertions(+), 519 deletions(-) create mode 100644 src/Tgstation.Server.Host/Controllers/InstanceRequiredController.cs diff --git a/build/Version.props b/build/Version.props index d3117eac62..01d822a7b2 100644 --- a/build/Version.props +++ b/build/Version.props @@ -4,8 +4,8 @@ 4.4.2 2.0.0 - 7.0.2 - 8.0.1 + 7.1.0 + 8.1.0 5.2.2 0.4.0 1.1.0 diff --git a/src/Tgstation.Server.Api/Models/ErrorCode.cs b/src/Tgstation.Server.Api/Models/ErrorCode.cs index b54912874a..9a350f2377 100644 --- a/src/Tgstation.Server.Api/Models/ErrorCode.cs +++ b/src/Tgstation.Server.Api/Models/ErrorCode.cs @@ -552,5 +552,11 @@ namespace Tgstation.Server.Api.Models /// [Description("Cannot perform this operation as DreamDaemon is not currently running!")] DreamDaemonOffline, + + /// + /// Attempted to perform an instance operation with an offline instance. + /// + [Description("The instance associated with the operation is currently offline!")] + InstanceOffline } } \ No newline at end of file diff --git a/src/Tgstation.Server.Host/Components/IInstanceManager.cs b/src/Tgstation.Server.Host/Components/IInstanceManager.cs index 8e2f9b8f2b..a4ff54c906 100644 --- a/src/Tgstation.Server.Host/Components/IInstanceManager.cs +++ b/src/Tgstation.Server.Host/Components/IInstanceManager.cs @@ -19,7 +19,7 @@ namespace Tgstation.Server.Host.Components /// Get the associated with given /// /// The of the desired - /// The associated with the given + /// The associated with the given if it is online, otherwise. IInstance GetInstance(Models.Instance metadata); /// @@ -42,10 +42,10 @@ namespace Tgstation.Server.Host.Components /// /// Move an /// - /// The of the desired - /// The new path of the . will have this set on if the operation completes successfully + /// 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 newPath, CancellationToken cancellationToken); + Task MoveInstance(Models.Instance metadata, string oldPath, CancellationToken cancellationToken); } } diff --git a/src/Tgstation.Server.Host/Components/InstanceManager.cs b/src/Tgstation.Server.Host/Components/InstanceManager.cs index 5c7606b403..81bd54d97c 100644 --- a/src/Tgstation.Server.Host/Components/InstanceManager.cs +++ b/src/Tgstation.Server.Host/Components/InstanceManager.cs @@ -13,6 +13,7 @@ using Tgstation.Server.Api.Models; using Tgstation.Server.Host.Components.Interop; using Tgstation.Server.Host.Components.Interop.Bridge; using Tgstation.Server.Host.Configuration; +using Tgstation.Server.Host.Controllers; using Tgstation.Server.Host.Core; using Tgstation.Server.Host.Database; using Tgstation.Server.Host.IO; @@ -192,32 +193,74 @@ namespace Tgstation.Server.Host.Components throw new ArgumentNullException(nameof(metadata)); lock (instances) { - if (!instances.TryGetValue(metadata.Id, out IInstance instance)) - throw new InvalidOperationException("Instance not online!"); - return instance; + instances.TryGetValue(metadata.Id, out IInstance instance); + return instance; // null if above is false } } /// - public async Task MoveInstance(Models.Instance instance, string newPath, CancellationToken cancellationToken) + public async Task MoveInstance(Models.Instance instance, string oldPath, CancellationToken cancellationToken) { - if (newPath == null) - throw new ArgumentNullException(nameof(newPath)); - if (instance.Online.Value) + if (oldPath == null) + throw new ArgumentNullException(nameof(oldPath)); + if (GetInstance(instance) != null) throw new InvalidOperationException("Cannot move an online instance!"); - var oldPath = instance.Path; - await ioManager.CopyDirectory(oldPath, newPath, null, cancellationToken).ConfigureAwait(false); - await databaseContextFactory.UseContext(db => + var newPath = instance.Path; + try { - var targetInstance = new Models.Instance + await ioManager.MoveDirectory(oldPath, newPath, cancellationToken).ConfigureAwait(false); + } + catch (Exception ex) + { + logger.LogError( + "Error moving instance {0}! Exception: {2}", + instance.Id, + ex); + try { - Id = instance.Id - }; - db.Instances.Attach(targetInstance); - targetInstance.Path = newPath; - return db.Save(cancellationToken); - }).ConfigureAwait(false); - await ioManager.DeleteDirectory(oldPath, cancellationToken).ConfigureAwait(false); + logger.LogDebug("Reverting instance {0}'s path to {1} in the DB...", instance.Id, oldPath); + + // DCT: Operation must always run + await databaseContextFactory.UseContext(db => + { + var targetInstance = new Models.Instance + { + Id = instance.Id + }; + db.Instances.Attach(targetInstance); + targetInstance.Path = oldPath; + return db.Save(default); + }).ConfigureAwait(false); + } + catch (Exception innerEx) + { + logger.LogCritical( + "Error reverting database after failing to move instance {0}! Attempting to detach. Exception: {1}", + ex); + + try + { + // DCT: Operation must always run + await ioManager.WriteAllBytes( + ioManager.ConcatPath(oldPath, InstanceController.InstanceAttachFileName), + Array.Empty(), + default) + .ConfigureAwait(false); + } + catch (Exception tripleEx) + { + logger.LogCritical( + "Okay, what gamma radiation are you under? Failed to write instance attach file! Exception: {0}", + tripleEx); + + throw new AggregateException(tripleEx, innerEx, ex); + } + + throw new AggregateException(ex, innerEx); + } + + throw; + } } /// diff --git a/src/Tgstation.Server.Host/Controllers/ApiController.cs b/src/Tgstation.Server.Host/Controllers/ApiController.cs index 88309c993a..6c66ec832e 100644 --- a/src/Tgstation.Server.Host/Controllers/ApiController.cs +++ b/src/Tgstation.Server.Host/Controllers/ApiController.cs @@ -7,6 +7,7 @@ using System; using System.Linq; using System.Net; using System.Net.Mime; +using System.Threading; using System.Threading.Tasks; using Tgstation.Server.Api; using Tgstation.Server.Api.Models; @@ -40,7 +41,7 @@ namespace Tgstation.Server.Host.Controllers /// /// The for the /// - protected ILogger Logger { get; } + protected ILogger Logger { get; } /// /// The for the operation @@ -68,7 +69,7 @@ namespace Tgstation.Server.Host.Controllers public ApiController( IDatabaseContext databaseContext, IAuthenticationContextFactory authenticationContextFactory, - ILogger logger, + ILogger logger, bool requireInstance, bool requireHeaders = true) { @@ -122,6 +123,14 @@ namespace Tgstation.Server.Host.Controllers /// A with the given . protected ObjectResult Created(object payload) => StatusCode((int)HttpStatusCode.Created, payload); + /// + /// Performs validation steps for an instance request. + /// + /// The for the operation. + /// A resulting in an appropriate on validation failure, otherwise. + protected virtual Task ValidateInstanceRequest(CancellationToken cancellationToken) + => Task.FromResult(null); + /// /// Response for missing/Invalid headers. /// @@ -157,6 +166,9 @@ namespace Tgstation.Server.Host.Controllers #pragma warning disable CA1506 // TODO: Decomplexify public override async Task OnActionExecutionAsync(ActionExecutingContext context, ActionExecutionDelegate next) { + if (context == null) + throw new ArgumentNullException(nameof(context)); + // ALL valid token and login requests that match a route go through this function // 404 is returned before if (AuthenticationContext != null && AuthenticationContext.User == null) @@ -183,16 +195,16 @@ namespace Tgstation.Server.Host.Controllers if (requireInstance) { + IActionResult errorCase = null; if (!ApiHeaders.InstanceId.HasValue) - { - await BadRequest(new ErrorMessage(ErrorCode.InstanceHeaderRequired)).ExecuteResultAsync(context).ConfigureAwait(false); - return; - } + errorCase = BadRequest(new ErrorMessage(ErrorCode.InstanceHeaderRequired)); + else if (AuthenticationContext.InstanceUser == null) + errorCase = Forbid(); - if (AuthenticationContext.InstanceUser == null) + errorCase ??= await ValidateInstanceRequest(context.HttpContext.RequestAborted).ConfigureAwait(false); + if (errorCase != null) { - // accessing an instance they don't have access to or one that's disabled - await Forbid().ExecuteResultAsync(context).ConfigureAwait(false); + await errorCase.ExecuteResultAsync(context).ConfigureAwait(false); return; } } diff --git a/src/Tgstation.Server.Host/Controllers/ByondController.cs b/src/Tgstation.Server.Host/Controllers/ByondController.cs index 490c6308db..0db373c6e0 100644 --- a/src/Tgstation.Server.Host/Controllers/ByondController.cs +++ b/src/Tgstation.Server.Host/Controllers/ByondController.cs @@ -20,13 +20,8 @@ namespace Tgstation.Server.Host.Controllers /// Controller for managing s /// [Route(Routes.Byond)] - public sealed class ByondController : ApiController + public sealed class ByondController : InstanceRequiredController { - /// - /// The for the - /// - readonly IInstanceManager instanceManager; - /// /// The for the /// @@ -37,7 +32,7 @@ namespace Tgstation.Server.Host.Controllers /// /// The for the /// The for the - /// The value of + /// The for the . /// The value of /// The for the public ByondController( @@ -47,12 +42,11 @@ namespace Tgstation.Server.Host.Controllers IJobManager jobManager, ILogger logger) : base( + instanceManager, databaseContext, authenticationContextFactory, - logger, - true) + logger) { - this.instanceManager = instanceManager ?? throw new ArgumentNullException(nameof(instanceManager)); this.jobManager = jobManager ?? throw new ArgumentNullException(nameof(jobManager)); } @@ -64,11 +58,13 @@ namespace Tgstation.Server.Host.Controllers [HttpGet] [TgsAuthorize(ByondRights.ReadActive)] [ProducesResponseType(typeof(Api.Models.Byond), 200)] - public Task Read() => Task.FromResult( - Json(new Api.Models.Byond - { - Version = instanceManager.GetInstance(Instance).ByondManager.ActiveVersion - })); + public Task Read() + => WithComponentInstance(instance => + Task.FromResult( + Json(new Api.Models.Byond + { + Version = instance.ByondManager.ActiveVersion + }))); /// /// Lists installed versions. @@ -78,16 +74,16 @@ namespace Tgstation.Server.Host.Controllers [HttpGet(Routes.List)] [TgsAuthorize(ByondRights.ListInstalled)] [ProducesResponseType(typeof(IEnumerable), 200)] - public IActionResult List() - => Json( - instanceManager - .GetInstance(Instance) - .ByondManager - .InstalledVersions - .Select(x => new Api.Models.Byond - { - Version = x - })); + public Task List() + => WithComponentInstance(instance => + Task.FromResult( + Json(instance + .ByondManager + .InstalledVersions + .Select(x => new Api.Models.Byond + { + Version = x + })))); /// /// Changes the active BYOND version to the one specified in a given . @@ -118,57 +114,60 @@ namespace Tgstation.Server.Host.Controllers || (!userByondRights.HasFlag(ByondRights.InstallCustomVersion) && model.Content != null)) return Forbid(); - var byondManager = instanceManager.GetInstance(Instance).ByondManager; - // remove cruff fields var result = new Api.Models.Byond(); - - if (model.Content == null && byondManager.InstalledVersions.Any(x => x == model.Version)) - { - Logger.LogInformation( - "User ID {0} changing instance ID {1} BYOND version to {2}", - AuthenticationContext.User.Id, - Instance.Id, - model.Version); - await byondManager.ChangeVersion(model.Version, null, cancellationToken).ConfigureAwait(false); - } - else if (model.Version.Build > 0) - return BadRequest(new ErrorMessage(ErrorCode.ByondNonExistentCustomVersion)); - else - { - var installingVersion = model.Version.Build <= 0 - ? new Version(model.Version.Major, model.Version.Minor) - : model.Version; - - Logger.LogInformation( - "User ID {0} installing BYOND version to {1} on instance ID {2}", - AuthenticationContext.User.Id, - installingVersion, - Instance.Id); - - // run the install through the job manager - var job = new Models.Job + return await WithComponentInstance( + async instance => { - Description = $"Install {(model.Content == null ? String.Empty : "custom ")}BYOND version {model.Version.Major}.{model.Version.Minor}", - StartedBy = AuthenticationContext.User, - CancelRightsType = RightsType.Byond, - CancelRight = (ulong)ByondRights.CancelInstall, - Instance = Instance - }; - await jobManager.RegisterOperation( - job, - (paramJob, databaseContextFactory, progressHandler, jobCancellationToken) => byondManager.ChangeVersion( - model.Version, - model.Content, - jobCancellationToken), - cancellationToken) - .ConfigureAwait(false); - result.InstallJob = job.ToApi(); - } + var byondManager = instance.ByondManager; + if (model.Content == null && byondManager.InstalledVersions.Any(x => x == model.Version)) + { + Logger.LogInformation( + "User ID {0} changing instance ID {1} BYOND version to {2}", + AuthenticationContext.User.Id, + Instance.Id, + model.Version); + await byondManager.ChangeVersion(model.Version, null, cancellationToken).ConfigureAwait(false); + } + else if (model.Version.Build > 0) + return BadRequest(new ErrorMessage(ErrorCode.ByondNonExistentCustomVersion)); + else + { + var installingVersion = model.Version.Build <= 0 + ? new Version(model.Version.Major, model.Version.Minor) + : model.Version; - if ((AuthenticationContext.GetRight(RightsType.Byond) & (ulong)ByondRights.ReadActive) != 0) - result.Version = byondManager.ActiveVersion; - return result.InstallJob != null ? (IActionResult)Accepted(result) : Json(result); + Logger.LogInformation( + "User ID {0} installing BYOND version to {1} on instance ID {2}", + AuthenticationContext.User.Id, + installingVersion, + Instance.Id); + + // run the install through the job manager + var job = new Models.Job + { + Description = $"Install {(model.Content == null ? String.Empty : "custom ")}BYOND version {model.Version.Major}.{model.Version.Minor}", + StartedBy = AuthenticationContext.User, + CancelRightsType = RightsType.Byond, + CancelRight = (ulong)ByondRights.CancelInstall, + Instance = Instance + }; + await jobManager.RegisterOperation( + job, + (paramJob, databaseContextFactory, progressHandler, jobCancellationToken) => byondManager.ChangeVersion( + model.Version, + model.Content, + jobCancellationToken), + cancellationToken) + .ConfigureAwait(false); + result.InstallJob = job.ToApi(); + } + + if ((AuthenticationContext.GetRight(RightsType.Byond) & (ulong)ByondRights.ReadActive) != 0) + result.Version = byondManager.ActiveVersion; + return result.InstallJob != null ? (IActionResult)Accepted(result) : Json(result); + }) + .ConfigureAwait(false); } } } diff --git a/src/Tgstation.Server.Host/Controllers/ChatController.cs b/src/Tgstation.Server.Host/Controllers/ChatController.cs index 1c1e8d9e4b..45c2db5d74 100644 --- a/src/Tgstation.Server.Host/Controllers/ChatController.cs +++ b/src/Tgstation.Server.Host/Controllers/ChatController.cs @@ -25,19 +25,14 @@ namespace Tgstation.Server.Host.Controllers /// [Route(Routes.Chat)] #pragma warning disable CA1506 // TODO: Decomplexify - public sealed class ChatController : ApiController + public sealed class ChatController : InstanceRequiredController { - /// - /// The for the - /// - readonly IInstanceManager instanceManager; - /// /// Construct a /// /// The for the /// The for the - /// The value of + /// The for the . /// The for the public ChatController( IDatabaseContext databaseContext, @@ -45,12 +40,11 @@ namespace Tgstation.Server.Host.Controllers IInstanceManager instanceManager, ILogger logger) : base( + instanceManager, databaseContext, authenticationContextFactory, - logger, - true) + logger) { - this.instanceManager = instanceManager ?? throw new ArgumentNullException(nameof(instanceManager)); } /// @@ -116,27 +110,32 @@ namespace Tgstation.Server.Host.Controllers DatabaseContext.ChatBots.Add(dbModel); await DatabaseContext.Save(cancellationToken).ConfigureAwait(false); - var instance = instanceManager.GetInstance(Instance); - try - { - // try to create it - await instance.Chat.ChangeSettings(dbModel, cancellationToken).ConfigureAwait(false); + return await WithComponentInstance( + async instance => + { + try + { + // try to create it + await instance.Chat.ChangeSettings(dbModel, cancellationToken).ConfigureAwait(false); - if (dbModel.Channels.Count > 0) - await instance.Chat.ChangeChannels(dbModel.Id, dbModel.Channels, cancellationToken).ConfigureAwait(false); - } - catch - { - // undo the add - DatabaseContext.ChatBots.Remove(dbModel); + if (dbModel.Channels.Count > 0) + await instance.Chat.ChangeChannels(dbModel.Id, dbModel.Channels, cancellationToken).ConfigureAwait(false); + } + catch + { + // undo the add + DatabaseContext.ChatBots.Remove(dbModel); - // DCTx2: Operations must always run - await DatabaseContext.Save(default).ConfigureAwait(false); - await instance.Chat.DeleteConnection(dbModel.Id, default).ConfigureAwait(false); - throw; - } + // DCTx2: Operations must always run + await DatabaseContext.Save(default).ConfigureAwait(false); + await instance.Chat.DeleteConnection(dbModel.Id, default).ConfigureAwait(false); + throw; + } - return StatusCode(HttpStatusCode.Created, dbModel.ToApi()); + return null; + }) + .ConfigureAwait(false) + ?? StatusCode(HttpStatusCode.Created, dbModel.ToApi()); } /// @@ -150,19 +149,21 @@ namespace Tgstation.Server.Host.Controllers [TgsAuthorize(ChatBotRights.Delete)] [ProducesResponseType(204)] public async Task Delete(long id, CancellationToken cancellationToken) - { - var instance = instanceManager.GetInstance(Instance); - await Task.WhenAll( - instance.Chat.DeleteConnection(id, cancellationToken), - DatabaseContext - .ChatBots - .AsQueryable() - .Where(x => x.Id == id) - .DeleteAsync(cancellationToken)) - .ConfigureAwait(false); - - return NoContent(); - } + => await WithComponentInstance( + async instance => + { + await Task.WhenAll( + instance.Chat.DeleteConnection(id, cancellationToken), + DatabaseContext + .ChatBots + .AsQueryable() + .Where(x => x.Id == id) + .DeleteAsync(cancellationToken)) + .ConfigureAwait(false); + return null; + }) + .ConfigureAwait(false) + ?? NoContent(); /// /// List s. @@ -315,13 +316,21 @@ namespace Tgstation.Server.Host.Controllers await DatabaseContext.Save(cancellationToken).ConfigureAwait(false); - var chat = instanceManager.GetInstance(Instance).Chat; + earlyOut = await WithComponentInstance( + async instance => + { + var chat = instance.Chat; + if (anySettingsModified) + await chat.ChangeSettings(current, cancellationToken).ConfigureAwait(false); // have to rebuild the thing first - if (anySettingsModified) - await chat.ChangeSettings(current, cancellationToken).ConfigureAwait(false); // have to rebuild the thing first + if (model.Channels != null || anySettingsModified) + await chat.ChangeChannels(current.Id, current.Channels, cancellationToken).ConfigureAwait(false); - if (model.Channels != null || anySettingsModified) - await chat.ChangeChannels(current.Id, current.Channels, cancellationToken).ConfigureAwait(false); + return null; + }) + .ConfigureAwait(false); + if (earlyOut != null) + return earlyOut; if (userRights.HasFlag(ChatBotRights.Read)) { diff --git a/src/Tgstation.Server.Host/Controllers/ConfigurationController.cs b/src/Tgstation.Server.Host/Controllers/ConfigurationController.cs index a697f36a12..cecd5f2c93 100644 --- a/src/Tgstation.Server.Host/Controllers/ConfigurationController.cs +++ b/src/Tgstation.Server.Host/Controllers/ConfigurationController.cs @@ -20,13 +20,8 @@ namespace Tgstation.Server.Host.Controllers /// The for s /// [Route(Routes.Configuration)] - public sealed class ConfigurationController : ApiController + public sealed class ConfigurationController : InstanceRequiredController { - /// - /// The for the - /// - readonly IInstanceManager instanceManager; - /// /// The for the /// @@ -37,7 +32,7 @@ namespace Tgstation.Server.Host.Controllers /// /// The for the /// The for the - /// The value of + /// The for the . /// The value of /// The for the public ConfigurationController( @@ -47,12 +42,11 @@ namespace Tgstation.Server.Host.Controllers IIOManager ioManager, ILogger logger) : base( + instanceManager, databaseContext, authenticationContextFactory, - logger, - true) + logger) { - this.instanceManager = instanceManager ?? throw new ArgumentNullException(nameof(instanceManager)); this.ioManager = ioManager ?? throw new ArgumentNullException(nameof(ioManager)); } @@ -95,16 +89,28 @@ namespace Tgstation.Server.Host.Controllers if (ForbidDueToModeConflicts(model.Path, out var systemIdentity)) return Forbid(); - var config = instanceManager.GetInstance(Instance).Configuration; try { - var newFile = await config.Write(model.Path, systemIdentity, model.Content, model.LastReadHash, cancellationToken).ConfigureAwait(false); - if (newFile == null) - return Conflict(new ErrorMessage(ErrorCode.ConfigurationFileUpdated)); + return await WithComponentInstance( + async instance => + { + var newFile = await instance + .Configuration + .Write( + model.Path, + systemIdentity, + model.Content, + model.LastReadHash, + cancellationToken) + .ConfigureAwait(false); + if (newFile == null) + return Conflict(new ErrorMessage(ErrorCode.ConfigurationFileUpdated)); - newFile.Content = null; + newFile.Content = null; - return model.LastReadHash == null ? (IActionResult)Created(newFile) : Json(newFile); + return model.LastReadHash == null ? (IActionResult)Created(newFile) : Json(newFile); + }) + .ConfigureAwait(false); } catch(IOException e) { @@ -139,11 +145,19 @@ namespace Tgstation.Server.Host.Controllers try { - var result = await instanceManager.GetInstance(Instance).Configuration.Read(filePath, systemIdentity, cancellationToken).ConfigureAwait(false); - if (result == null) - return Gone(); + return await WithComponentInstance( + async instance => + { + var result = await instance + .Configuration + .Read(filePath, systemIdentity, cancellationToken) + .ConfigureAwait(false); + if (result == null) + return Gone(); - return Json(result); + return Json(result); + }) + .ConfigureAwait(false); } catch (IOException e) { @@ -178,11 +192,19 @@ namespace Tgstation.Server.Host.Controllers try { - var result = await instanceManager.GetInstance(Instance).Configuration.ListDirectory(directoryPath, systemIdentity, cancellationToken).ConfigureAwait(false); - if (result == null) - return Gone(); + return await WithComponentInstance( + async instance => + { + var result = await instance + .Configuration + .ListDirectory(directoryPath, systemIdentity, cancellationToken) + .ConfigureAwait(false); + if (result == null) + return Gone(); - return Json(result); + return Json(result); + }) + .ConfigureAwait(false); } catch (NotImplementedException) { @@ -227,13 +249,14 @@ namespace Tgstation.Server.Host.Controllers try { model.IsDirectory = true; - return await instanceManager - .GetInstance(Instance) + return await WithComponentInstance( + async instance => await instance .Configuration .CreateDirectory(model.Path, systemIdentity, cancellationToken) .ConfigureAwait(false) ? (IActionResult)Json(model) - : Created(model); + : Created(model)) + .ConfigureAwait(false); } catch (IOException e) { @@ -273,13 +296,14 @@ namespace Tgstation.Server.Host.Controllers try { - return await instanceManager - .GetInstance(Instance) + return await WithComponentInstance( + async instance => await instance .Configuration .DeleteDirectory(directory.Path, systemIdentity, cancellationToken) .ConfigureAwait(false) ? (IActionResult)NoContent() - : Conflict(new ErrorMessage(ErrorCode.ConfigurationDirectoryNotEmpty)); + : Conflict(new ErrorMessage(ErrorCode.ConfigurationDirectoryNotEmpty))) + .ConfigureAwait(false); } catch (NotImplementedException) { diff --git a/src/Tgstation.Server.Host/Controllers/DreamDaemonController.cs b/src/Tgstation.Server.Host/Controllers/DreamDaemonController.cs index 9ae5df0a0e..1cd85be3c2 100644 --- a/src/Tgstation.Server.Host/Controllers/DreamDaemonController.cs +++ b/src/Tgstation.Server.Host/Controllers/DreamDaemonController.cs @@ -23,25 +23,20 @@ namespace Tgstation.Server.Host.Controllers /// for managing the /// [Route(Routes.DreamDaemon)] - public sealed class DreamDaemonController : ApiController + public sealed class DreamDaemonController : InstanceRequiredController { /// /// The for the /// readonly IJobManager jobManager; - /// - /// The for the - /// - readonly IInstanceManager instanceManager; - /// /// Construct a /// /// The for the /// The for the /// The value of - /// The value of + /// The for the . /// The for the public DreamDaemonController( IDatabaseContext databaseContext, @@ -50,13 +45,12 @@ namespace Tgstation.Server.Host.Controllers IInstanceManager instanceManager, ILogger logger) : base( + instanceManager, databaseContext, authenticationContextFactory, - logger, - true) + logger) { this.jobManager = jobManager ?? throw new ArgumentNullException(nameof(jobManager)); - this.instanceManager = instanceManager ?? throw new ArgumentNullException(nameof(instanceManager)); } /// @@ -68,25 +62,27 @@ namespace Tgstation.Server.Host.Controllers [HttpPut] [TgsAuthorize(DreamDaemonRights.Start)] [ProducesResponseType(typeof(Api.Models.Job), 202)] - public async Task Create(CancellationToken cancellationToken) - { - // alias for launching DD - var instance = instanceManager.GetInstance(Instance); - - if (instance.Watchdog.Status != WatchdogStatus.Offline) - return Conflict(new ErrorMessage(ErrorCode.WatchdogRunning)); - - var job = new Models.Job + public Task Create(CancellationToken cancellationToken) + => WithComponentInstance(async instance => { - Description = "Launch DreamDaemon", - CancelRight = (ulong)DreamDaemonRights.Shutdown, - CancelRightsType = RightsType.DreamDaemon, - Instance = Instance, - StartedBy = AuthenticationContext.User - }; - await jobManager.RegisterOperation(job, (paramJob, databaseContextFactory, progressHandler, innerCt) => instance.Watchdog.Launch(innerCt), cancellationToken).ConfigureAwait(false); - return Accepted(job.ToApi()); - } + if (instance.Watchdog.Status != WatchdogStatus.Offline) + return Conflict(new ErrorMessage(ErrorCode.WatchdogRunning)); + + var job = new Models.Job + { + Description = "Launch DreamDaemon", + CancelRight = (ulong)DreamDaemonRights.Shutdown, + CancelRightsType = RightsType.DreamDaemon, + Instance = Instance, + StartedBy = AuthenticationContext.User + }; + await jobManager.RegisterOperation( + job, + (paramJob, databaseContextFactory, progressHandler, innerCt) => instance.Watchdog.Launch(innerCt), + cancellationToken) + .ConfigureAwait(false); + return Accepted(job.ToApi()); + }); /// /// Get the watchdog status. @@ -107,61 +103,61 @@ namespace Tgstation.Server.Host.Controllers /// The to operate on if any /// The for the operation /// A resulting in the of the operation - async Task ReadImpl(DreamDaemonSettings settings, CancellationToken cancellationToken) - { - var instance = instanceManager.GetInstance(Instance); - var dd = instance.Watchdog; - - var metadata = (AuthenticationContext.GetRight(RightsType.DreamDaemon) & (ulong)DreamDaemonRights.ReadMetadata) != 0; - var revision = (AuthenticationContext.GetRight(RightsType.DreamDaemon) & (ulong)DreamDaemonRights.ReadRevision) != 0; - - if (settings == null) + Task ReadImpl(DreamDaemonSettings settings, CancellationToken cancellationToken) + => WithComponentInstance(async instance => { - settings = await DatabaseContext - .Instances - .AsQueryable() - .Where(x => x.Id == Instance.Id) - .Select(x => x.DreamDaemonSettings) - .FirstOrDefaultAsync(cancellationToken) - .ConfigureAwait(false); - if (settings == default) - return Gone(); - } + var dd = instance.Watchdog; - var result = new DreamDaemon(); - if (metadata) - { - var alphaActive = dd.AlphaIsActive; - var llp = dd.LastLaunchParameters; - var rstate = dd.RebootState; - result.AutoStart = settings.AutoStart.Value; - result.CurrentPort = llp?.Port.Value; - result.CurrentSecurity = llp?.SecurityLevel.Value; - result.CurrentAllowWebclient = llp?.AllowWebClient.Value; - result.Port = settings.Port.Value; - result.AllowWebClient = settings.AllowWebClient.Value; - result.Status = dd.Status; - result.SecurityLevel = settings.SecurityLevel.Value; - result.SoftRestart = rstate == RebootState.Restart; - result.SoftShutdown = rstate == RebootState.Shutdown; - result.StartupTimeout = settings.StartupTimeout.Value; - result.HeartbeatSeconds = settings.HeartbeatSeconds.Value; - result.TopicRequestTimeout = settings.TopicRequestTimeout.Value; - } + var metadata = (AuthenticationContext.GetRight(RightsType.DreamDaemon) & (ulong)DreamDaemonRights.ReadMetadata) != 0; + var revision = (AuthenticationContext.GetRight(RightsType.DreamDaemon) & (ulong)DreamDaemonRights.ReadRevision) != 0; - if (revision) - { - var latestCompileJob = instance.LatestCompileJob(); - result.ActiveCompileJob = ((instance.Watchdog.Status != WatchdogStatus.Offline - ? dd.ActiveCompileJob - : latestCompileJob) ?? latestCompileJob) - ?.ToApi(); - if (latestCompileJob?.Id != result.ActiveCompileJob?.Id) - result.StagedCompileJob = latestCompileJob?.ToApi(); - } + if (settings == null) + { + settings = await DatabaseContext + .Instances + .AsQueryable() + .Where(x => x.Id == Instance.Id) + .Select(x => x.DreamDaemonSettings) + .FirstOrDefaultAsync(cancellationToken) + .ConfigureAwait(false); + if (settings == default) + return Gone(); + } - return Json(result); - } + var result = new DreamDaemon(); + if (metadata) + { + var alphaActive = dd.AlphaIsActive; + var llp = dd.LastLaunchParameters; + var rstate = dd.RebootState; + result.AutoStart = settings.AutoStart.Value; + result.CurrentPort = llp?.Port.Value; + result.CurrentSecurity = llp?.SecurityLevel.Value; + result.CurrentAllowWebclient = llp?.AllowWebClient.Value; + result.Port = settings.Port.Value; + result.AllowWebClient = settings.AllowWebClient.Value; + result.Status = dd.Status; + result.SecurityLevel = settings.SecurityLevel.Value; + result.SoftRestart = rstate == RebootState.Restart; + result.SoftShutdown = rstate == RebootState.Shutdown; + result.StartupTimeout = settings.StartupTimeout.Value; + result.HeartbeatSeconds = settings.HeartbeatSeconds.Value; + result.TopicRequestTimeout = settings.TopicRequestTimeout.Value; + } + + if (revision) + { + var latestCompileJob = instance.LatestCompileJob(); + result.ActiveCompileJob = ((instance.Watchdog.Status != WatchdogStatus.Offline + ? dd.ActiveCompileJob + : latestCompileJob) ?? latestCompileJob) + ?.ToApi(); + if (latestCompileJob?.Id != result.ActiveCompileJob?.Id) + result.StagedCompileJob = latestCompileJob?.ToApi(); + } + + return Json(result); + }); /// /// Stops the Watchdog if it's running. @@ -172,12 +168,12 @@ namespace Tgstation.Server.Host.Controllers [HttpDelete] [TgsAuthorize(DreamDaemonRights.Shutdown)] [ProducesResponseType(204)] - public async Task Delete(CancellationToken cancellationToken) - { - var instance = instanceManager.GetInstance(Instance); - await instance.Watchdog.Terminate(false, cancellationToken).ConfigureAwait(false); - return NoContent(); - } + public Task Delete(CancellationToken cancellationToken) + => WithComponentInstance(async instance => + { + await instance.Watchdog.Terminate(false, cancellationToken).ConfigureAwait(false); + return NoContent(); + }); /// /// Update watchdog settings to be applied at next server reboot. @@ -240,41 +236,43 @@ namespace Tgstation.Server.Host.Controllers return false; } - var instance = instanceManager.GetInstance(Instance); - var dd = instance.Watchdog; - var rebootState = dd.RebootState; - var oldSoftRestart = rebootState == RebootState.Restart; - var oldSoftShutdown = rebootState == RebootState.Shutdown; + return await WithComponentInstance( + async instance => + { + var watchdog = instance.Watchdog; + var rebootState = watchdog.RebootState; + var oldSoftRestart = rebootState == RebootState.Restart; + var oldSoftShutdown = rebootState == RebootState.Shutdown; - if (CheckModified(x => x.AllowWebClient, DreamDaemonRights.SetWebClient) - || CheckModified(x => x.AutoStart, DreamDaemonRights.SetAutoStart) - || CheckModified(x => x.Port, DreamDaemonRights.SetPort) - || CheckModified(x => x.SecurityLevel, DreamDaemonRights.SetSecurity) - || (model.SoftRestart.HasValue && !AuthenticationContext.InstanceUser.DreamDaemonRights.Value.HasFlag(DreamDaemonRights.SoftRestart)) - || (model.SoftShutdown.HasValue && !AuthenticationContext.InstanceUser.DreamDaemonRights.Value.HasFlag(DreamDaemonRights.SoftShutdown)) - || CheckModified(x => x.StartupTimeout, DreamDaemonRights.SetStartupTimeout) - || CheckModified(x => x.HeartbeatSeconds, DreamDaemonRights.SetHeartbeatInterval) - || CheckModified(x => x.TopicRequestTimeout, DreamDaemonRights.SetTopicTimeout)) - return Forbid(); + if (CheckModified(x => x.AllowWebClient, DreamDaemonRights.SetWebClient) + || CheckModified(x => x.AutoStart, DreamDaemonRights.SetAutoStart) + || CheckModified(x => x.Port, DreamDaemonRights.SetPort) + || CheckModified(x => x.SecurityLevel, DreamDaemonRights.SetSecurity) + || (model.SoftRestart.HasValue && !AuthenticationContext.InstanceUser.DreamDaemonRights.Value.HasFlag(DreamDaemonRights.SoftRestart)) + || (model.SoftShutdown.HasValue && !AuthenticationContext.InstanceUser.DreamDaemonRights.Value.HasFlag(DreamDaemonRights.SoftShutdown)) + || CheckModified(x => x.StartupTimeout, DreamDaemonRights.SetStartupTimeout) + || CheckModified(x => x.HeartbeatSeconds, DreamDaemonRights.SetHeartbeatInterval) + || CheckModified(x => x.TopicRequestTimeout, DreamDaemonRights.SetTopicTimeout)) + return Forbid(); - var wd = instanceManager.GetInstance(Instance).Watchdog; + await DatabaseContext.Save(cancellationToken).ConfigureAwait(false); - await DatabaseContext.Save(cancellationToken).ConfigureAwait(false); + // run this second because current may be modified by it + await watchdog.ChangeSettings(current, cancellationToken).ConfigureAwait(false); - // run this second because current may be modified by it - await wd.ChangeSettings(current, cancellationToken).ConfigureAwait(false); + if (!oldSoftRestart && model.SoftRestart == true) + await watchdog.Restart(true, cancellationToken).ConfigureAwait(false); + else if (!oldSoftShutdown && model.SoftShutdown == true) + await watchdog.Terminate(true, cancellationToken).ConfigureAwait(false); + else if ((oldSoftRestart && model.SoftRestart == false) || (oldSoftShutdown && model.SoftShutdown == false)) + await watchdog.ResetRebootState(cancellationToken).ConfigureAwait(false); - if (!oldSoftRestart && model.SoftRestart == true) - await wd.Restart(true, cancellationToken).ConfigureAwait(false); - else if (!oldSoftShutdown && model.SoftShutdown == true) - await wd.Terminate(true, cancellationToken).ConfigureAwait(false); - else if ((oldSoftRestart && model.SoftRestart == false) || (oldSoftShutdown && model.SoftShutdown == false)) - await wd.ResetRebootState(cancellationToken).ConfigureAwait(false); - - return await ReadImpl(current, cancellationToken).ConfigureAwait(false); + return await ReadImpl(current, cancellationToken).ConfigureAwait(false); + }) + .ConfigureAwait(false); } - #pragma warning restore CA1506 - #pragma warning restore CA1502 +#pragma warning restore CA1506 +#pragma warning restore CA1502 /// /// Creates a to restart the Watchdog. It will start if it wasn't already running. @@ -285,25 +283,30 @@ namespace Tgstation.Server.Host.Controllers [HttpPatch] [TgsAuthorize(DreamDaemonRights.Restart)] [ProducesResponseType(typeof(Api.Models.Job), 202)] - public async Task Restart(CancellationToken cancellationToken) - { - var job = new Models.Job + public Task Restart(CancellationToken cancellationToken) + => WithComponentInstance(async instance => { - Instance = Instance, - CancelRightsType = RightsType.DreamDaemon, - CancelRight = (ulong)DreamDaemonRights.Shutdown, - StartedBy = AuthenticationContext.User, - Description = "Restart Watchdog" - }; + var job = new Models.Job + { + Instance = Instance, + CancelRightsType = RightsType.DreamDaemon, + CancelRight = (ulong)DreamDaemonRights.Shutdown, + StartedBy = AuthenticationContext.User, + Description = "Restart Watchdog" + }; - var watchdog = instanceManager.GetInstance(Instance).Watchdog; + var watchdog = instance.Watchdog; - if (watchdog.Status == WatchdogStatus.Offline) - return Conflict(new ErrorMessage(ErrorCode.WatchdogNotRunning)); + if (watchdog.Status == WatchdogStatus.Offline) + return Conflict(new ErrorMessage(ErrorCode.WatchdogNotRunning)); - await jobManager.RegisterOperation(job, (paramJob, databaseContextFactory, progressReporter, ct) => watchdog.Restart(false, ct), cancellationToken).ConfigureAwait(false); - return Accepted(job.ToApi()); - } + await jobManager.RegisterOperation( + job, + (paramJob, databaseContextFactory, progressReporter, ct) => watchdog.Restart(false, ct), + cancellationToken) + .ConfigureAwait(false); + return Accepted(job.ToApi()); + }); /// /// Creates a to generate a DreamDaemon process dump. @@ -314,27 +317,28 @@ namespace Tgstation.Server.Host.Controllers [HttpPatch(Routes.Diagnostics)] [TgsAuthorize(DreamDaemonRights.CreateDump)] [ProducesResponseType(typeof(Api.Models.Job), 202)] - public async Task CreateDump(CancellationToken cancellationToken) - { - var job = new Models.Job + public Task CreateDump(CancellationToken cancellationToken) + => WithComponentInstance(async instance => { - Instance = Instance, - CancelRightsType = RightsType.DreamDaemon, - CancelRight = (ulong)DreamDaemonRights.CreateDump, - StartedBy = AuthenticationContext.User, - Description = "Create DreamDaemon Process Dump" - }; + var job = new Models.Job + { + Instance = Instance, + CancelRightsType = RightsType.DreamDaemon, + CancelRight = (ulong)DreamDaemonRights.CreateDump, + StartedBy = AuthenticationContext.User, + Description = "Create DreamDaemon Process Dump" + }; - var watchdog = instanceManager.GetInstance(Instance).Watchdog; + var watchdog = instance.Watchdog; - if (watchdog.Status == WatchdogStatus.Offline) - return Conflict(new ErrorMessage(ErrorCode.WatchdogNotRunning)); + if (watchdog.Status == WatchdogStatus.Offline) + return Conflict(new ErrorMessage(ErrorCode.WatchdogNotRunning)); - await jobManager.RegisterOperation( - job, - (paramJob, databaseContextFactory, progressReporter, ct) => watchdog.CreateDump(ct), cancellationToken) - .ConfigureAwait(false); - return Accepted(job.ToApi()); - } + await jobManager.RegisterOperation( + job, + (paramJob, databaseContextFactory, progressReporter, ct) => watchdog.CreateDump(ct), cancellationToken) + .ConfigureAwait(false); + return Accepted(job.ToApi()); + }); } } diff --git a/src/Tgstation.Server.Host/Controllers/DreamMakerController.cs b/src/Tgstation.Server.Host/Controllers/DreamMakerController.cs index 7c5fd60e24..6a27f95aba 100644 --- a/src/Tgstation.Server.Host/Controllers/DreamMakerController.cs +++ b/src/Tgstation.Server.Host/Controllers/DreamMakerController.cs @@ -22,25 +22,20 @@ namespace Tgstation.Server.Host.Controllers /// [Route(Routes.DreamMaker)] #pragma warning disable CA1506 // TODO: Decomplexify - public sealed class DreamMakerController : ApiController + public sealed class DreamMakerController : InstanceRequiredController { /// /// The for the /// readonly IJobManager jobManager; - /// - /// The for the - /// - readonly IInstanceManager instanceManager; - /// /// Construct a /// /// The for the /// The for the /// The value of - /// The value of + /// The for the . /// The for the public DreamMakerController( IDatabaseContext databaseContext, @@ -49,13 +44,12 @@ namespace Tgstation.Server.Host.Controllers IInstanceManager instanceManager, ILogger logger) : base( + instanceManager, databaseContext, authenticationContextFactory, - logger, - true) + logger) { this.jobManager = jobManager ?? throw new ArgumentNullException(nameof(jobManager)); - this.instanceManager = instanceManager ?? throw new ArgumentNullException(nameof(instanceManager)); } /// @@ -69,7 +63,6 @@ namespace Tgstation.Server.Host.Controllers [ProducesResponseType(typeof(DreamMaker), 200)] public async Task Read(CancellationToken cancellationToken) { - var instance = instanceManager.GetInstance(Instance); var dreamMakerSettings = await DatabaseContext .DreamMakerSettings .AsQueryable() @@ -140,25 +133,26 @@ namespace Tgstation.Server.Host.Controllers [HttpPut] [TgsAuthorize(DreamMakerRights.Compile)] [ProducesResponseType(typeof(Api.Models.Job), 202)] - public async Task Create(CancellationToken cancellationToken) - { - var job = new Models.Job - { - Description = "Compile active repository code", - StartedBy = AuthenticationContext.User, - CancelRightsType = RightsType.DreamMaker, - CancelRight = (ulong)DreamMakerRights.CancelCompile, - Instance = Instance - }; + public Task Create(CancellationToken cancellationToken) + => WithComponentInstance( + async instance => + { + var job = new Models.Job + { + Description = "Compile active repository code", + StartedBy = AuthenticationContext.User, + CancelRightsType = RightsType.DreamMaker, + CancelRight = (ulong)DreamMakerRights.CancelCompile, + Instance = Instance + }; - IInstance instance = instanceManager.GetInstance(Instance); - await jobManager.RegisterOperation( - job, - instance.DreamMaker.DeploymentProcess, - cancellationToken) - .ConfigureAwait(false); - return Accepted(job.ToApi()); - } + await jobManager.RegisterOperation( + job, + instance.DreamMaker.DeploymentProcess, + cancellationToken) + .ConfigureAwait(false); + return Accepted(job.ToApi()); + }); /// /// Update deployment settings. diff --git a/src/Tgstation.Server.Host/Controllers/InstanceController.cs b/src/Tgstation.Server.Host/Controllers/InstanceController.cs index 6efd51f9eb..afed0c82a1 100644 --- a/src/Tgstation.Server.Host/Controllers/InstanceController.cs +++ b/src/Tgstation.Server.Host/Controllers/InstanceController.cs @@ -4,7 +4,6 @@ using Microsoft.Extensions.Logging; using Microsoft.Extensions.Options; using System; using System.Collections.Generic; -using System.Globalization; using System.IO; using System.Linq; using System.Linq.Expressions; @@ -33,10 +32,13 @@ namespace Tgstation.Server.Host.Controllers public sealed class InstanceController : ApiController { /// - /// File name to allow attaching instances + /// File name to allow attaching instances. /// - const string InstanceAttachFileName = "TGS4_ALLOW_INSTANCE_ATTACH"; + public const string InstanceAttachFileName = "TGS4_ALLOW_INSTANCE_ATTACH"; + /// + /// Prefix for move s. + /// const string MoveInstanceJobPrefix = "Move instance ID "; /// @@ -154,7 +156,8 @@ namespace Tgstation.Server.Host.Controllers if (String.IsNullOrWhiteSpace(model.Name)) return BadRequest(new ErrorMessage(ErrorCode.InstanceWhitespaceName)); - var targetInstancePath = NormalizePath(model.Path); + var unNormalizedPath = model.Path; + var targetInstancePath = NormalizePath(unNormalizedPath); model.Path = targetInstancePath; var installationDirectoryPath = NormalizePath(DefaultIOManager.CurrentDirectory); @@ -288,7 +291,7 @@ namespace Tgstation.Server.Host.Controllers try { // actually reserve it now - await ioManager.CreateDirectory(targetInstancePath, cancellationToken).ConfigureAwait(false); + await ioManager.CreateDirectory(unNormalizedPath, cancellationToken).ConfigureAwait(false); await ioManager.DeleteFile(ioManager.ConcatPath(targetInstancePath, InstanceAttachFileName), cancellationToken).ConfigureAwait(false); } catch @@ -342,7 +345,17 @@ namespace Tgstation.Server.Host.Controllers DatabaseContext.Instances.Remove(originalModel); var attachFileName = ioManager.ConcatPath(originalModel.Path, InstanceAttachFileName); - await ioManager.WriteAllBytes(attachFileName, Array.Empty(), cancellationToken).ConfigureAwait(false); + try + { + await ioManager.WriteAllBytes(attachFileName, Array.Empty(), cancellationToken).ConfigureAwait(false); + } + catch (OperationCanceledException) + { + // DCT: Operation must always run + await ioManager.DeleteFile(attachFileName, default).ConfigureAwait(false); + throw; + } + await DatabaseContext.Save(cancellationToken).ConfigureAwait(false); // cascades everything return NoContent(); } @@ -383,7 +396,12 @@ namespace Tgstation.Server.Host.Controllers }).FirstOrDefaultAsync(cancellationToken).ConfigureAwait(false); if (moveJob != default) + { + // don't allow them to cancel it if they can't start it. + if (!AuthenticationContext.User.InstanceManagerRights.Value.HasFlag(InstanceManagerRights.Relocate)) + return Forbid(); await jobManager.CancelJob(moveJob, AuthenticationContext.User, true, cancellationToken).ConfigureAwait(false); // cancel it now + } var originalModel = await InstanceQuery() .Include(x => x.RepositorySettings) @@ -394,7 +412,7 @@ namespace Tgstation.Server.Host.Controllers if (originalModel == default(Models.Instance)) return Gone(); - if (ValidateInstanceOnlineStatus(originalModel)) + if (InstanceRequiredController.ValidateInstanceOnlineStatus(instanceManager, Logger, originalModel)) await DatabaseContext.Save(cancellationToken).ConfigureAwait(false); var userRights = (InstanceManagerRights)AuthenticationContext.GetRight(RightsType.InstanceManager); @@ -419,7 +437,7 @@ namespace Tgstation.Server.Host.Controllers { rawPath = NormalizePath(model.Path); - if (model.Path != originalModel.Path) + if (rawPath != originalModel.Path) { if (!userRights.HasFlag(InstanceManagerRights.Relocate)) return Forbid(); @@ -431,7 +449,7 @@ namespace Tgstation.Server.Host.Controllers return Conflict(new ErrorMessage(ErrorCode.InstanceAtExistingPath)); originalModelPath = originalModel.Path; - originalModel.Path = model.Path; + originalModel.Path = rawPath; } } @@ -462,7 +480,11 @@ namespace Tgstation.Server.Host.Controllers await DatabaseContext.Save(cancellationToken).ConfigureAwait(false); if (renamed) - await instanceManager.GetInstance(originalModel).InstanceRenamed(originalModel.Name, cancellationToken).ConfigureAwait(false); + { + var componentInstance = instanceManager.GetInstance(originalModel); + if (componentInstance != null) + await componentInstance.InstanceRenamed(originalModel.Name, cancellationToken).ConfigureAwait(false); + } var oldAutoStart = originalModel.DreamDaemonSettings.AutoStart; try @@ -501,19 +523,27 @@ namespace Tgstation.Server.Host.Controllers { var job = new Models.Job { - Description = String.Format(CultureInfo.InvariantCulture, MoveInstanceJobPrefix + "{0} from {1} to {2}", originalModel.Id, originalModel.Path, rawPath), + Description = $"{MoveInstanceJobPrefix}{originalModel.Id} from {originalModelPath} to {rawPath}", Instance = originalModel, CancelRightsType = RightsType.InstanceManager, CancelRight = (ulong)InstanceManagerRights.Relocate, StartedBy = AuthenticationContext.User }; - await jobManager.RegisterOperation(job, (paramJob, databaseContextFactory, progressHandler, ct) => instanceManager.MoveInstance(originalModel, rawPath, ct), cancellationToken).ConfigureAwait(false); + await jobManager.RegisterOperation( + job, + (paramJob, databaseContextFactory, progressHandler, ct) => instanceManager.MoveInstance(originalModel, originalModelPath, ct), + cancellationToken) + .ConfigureAwait(false); api.MoveJob = job.ToApi(); } - if (originalModel.Online.Value && model.AutoUpdateInterval.HasValue && oldAutoUpdateInterval != model.AutoUpdateInterval) - await instanceManager.GetInstance(originalModel).SetAutoUpdateInterval(model.AutoUpdateInterval.Value).ConfigureAwait(false); + if (model.AutoUpdateInterval.HasValue && oldAutoUpdateInterval != model.AutoUpdateInterval) + { + var componentInstance = instanceManager.GetInstance(originalModel); + if (componentInstance != null) + await componentInstance.SetAutoUpdateInterval(model.AutoUpdateInterval.Value).ConfigureAwait(false); + } return moving ? (IActionResult)Accepted(api) : Json(api); } @@ -564,7 +594,7 @@ namespace Tgstation.Server.Host.Controllers var needsUpdate = false; foreach (var instance in instances) - needsUpdate |= ValidateInstanceOnlineStatus(instance); + needsUpdate |= InstanceRequiredController.ValidateInstanceOnlineStatus(instanceManager, Logger, instance); if (needsUpdate) await DatabaseContext.Save(cancellationToken).ConfigureAwait(false); @@ -607,7 +637,7 @@ namespace Tgstation.Server.Host.Controllers if (instance == null) return Gone(); - if (ValidateInstanceOnlineStatus(instance)) + if (InstanceRequiredController.ValidateInstanceOnlineStatus(instanceManager, Logger, instance)) await DatabaseContext.Save(cancellationToken).ConfigureAwait(false); if (cantList && !instance.InstanceUsers.Any(instanceUser => instanceUser.UserId == AuthenticationContext.User.Id && @@ -667,39 +697,5 @@ namespace Tgstation.Server.Host.Controllers return NoContent(); } - - /// - /// Corrects discrepencies between the status of s in the database vs the service. - /// - /// The to check. - /// if an unsaved DB update was made, otherwise. - bool ValidateInstanceOnlineStatus(Models.Instance metadata) - { - bool online; - try - { - instanceManager.GetInstance(metadata); - online = true; - } - catch (InvalidOperationException ex) - { - Logger.LogDebug("Expected instance offline exception: {0}", ex); - online = false; - } - - 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...", - online ? OfflineWord : OnlineWord, - online ? OnlineWord : OfflineWord); - - metadata.Online = online; - return true; - } } } diff --git a/src/Tgstation.Server.Host/Controllers/InstanceRequiredController.cs b/src/Tgstation.Server.Host/Controllers/InstanceRequiredController.cs new file mode 100644 index 0000000000..d247daf857 --- /dev/null +++ b/src/Tgstation.Server.Host/Controllers/InstanceRequiredController.cs @@ -0,0 +1,104 @@ +using Microsoft.AspNetCore.Mvc; +using Microsoft.Extensions.Logging; +using System; +using System.Threading; +using System.Threading.Tasks; +using Tgstation.Server.Api.Models; +using Tgstation.Server.Host.Components; +using Tgstation.Server.Host.Database; +using Tgstation.Server.Host.Security; + +namespace Tgstation.Server.Host.Controllers +{ + /// + /// for operations on an . + /// + public abstract class InstanceRequiredController : ApiController + { + /// + /// The for the . + /// + readonly IInstanceManager instanceManager; + + /// + /// Initializes a new instance of the . + /// + /// The value of . + /// The for the . + /// The for the . + /// The for the + protected InstanceRequiredController( + IInstanceManager instanceManager, + IDatabaseContext databaseContext, + IAuthenticationContextFactory authenticationContextFactory, + ILogger logger) + : base( + databaseContext, + authenticationContextFactory, + logger, + true, + true) + { + this.instanceManager = instanceManager ?? throw new ArgumentNullException(nameof(instanceManager)); + } + + /// + protected override async Task ValidateInstanceRequest(CancellationToken cancellationToken) + { + if (ValidateInstanceOnlineStatus(instanceManager, Logger, Instance)) + await DatabaseContext.Save(cancellationToken).ConfigureAwait(false); + + if (instanceManager.GetInstance(Instance) == null) + return Conflict(new ErrorMessage(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 possinle so as to avoid race conditions. + protected async Task WithComponentInstance(Func> action) + { + if (action == null) + throw new ArgumentNullException(nameof(action)); + + var componentInstance = instanceManager.GetInstance(Instance); + if (componentInstance == null) + return Conflict(new ErrorMessage(ErrorCode.InstanceOffline)); + return await action(componentInstance).ConfigureAwait(false); + } + + /// + /// 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, Models.Instance metadata) + { + if (instanceManager == null) + throw new ArgumentNullException(nameof(instanceManager)); + if (metadata == null) + throw new ArgumentNullException(nameof(metadata)); + + var online = instanceManager.GetInstance(metadata) != 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...", + online ? OfflineWord : OnlineWord, + online ? OnlineWord : OfflineWord); + + metadata.Online = online; + return true; + } + } +} diff --git a/src/Tgstation.Server.Host/Controllers/InstanceUserController.cs b/src/Tgstation.Server.Host/Controllers/InstanceUserController.cs index 480698e9c7..34586ea5cf 100644 --- a/src/Tgstation.Server.Host/Controllers/InstanceUserController.cs +++ b/src/Tgstation.Server.Host/Controllers/InstanceUserController.cs @@ -9,6 +9,7 @@ using System.Threading.Tasks; using Tgstation.Server.Api; using Tgstation.Server.Api.Models; using Tgstation.Server.Api.Rights; +using Tgstation.Server.Host.Components; using Tgstation.Server.Host.Database; using Tgstation.Server.Host.Models; using Tgstation.Server.Host.Security; @@ -20,23 +21,25 @@ namespace Tgstation.Server.Host.Controllers /// for managing s. /// [Route(Routes.InstanceUser)] - public sealed class InstanceUserController : ApiController + public sealed class InstanceUserController : InstanceRequiredController { /// /// Construct a /// + /// The for the . /// The for the /// The for the /// The for the public InstanceUserController( + IInstanceManager instanceManager, IDatabaseContext databaseContext, IAuthenticationContextFactory authenticationContextFactory, ILogger logger) : base( + instanceManager, databaseContext, authenticationContextFactory, - logger, - true) + logger) { } /// diff --git a/src/Tgstation.Server.Host/Controllers/JobController.cs b/src/Tgstation.Server.Host/Controllers/JobController.cs index e6bca24a46..6347617699 100644 --- a/src/Tgstation.Server.Host/Controllers/JobController.cs +++ b/src/Tgstation.Server.Host/Controllers/JobController.cs @@ -8,6 +8,7 @@ using System.Threading; using System.Threading.Tasks; using Tgstation.Server.Api; using Tgstation.Server.Api.Models; +using Tgstation.Server.Host.Components; using Tgstation.Server.Host.Database; using Tgstation.Server.Host.Jobs; using Tgstation.Server.Host.Models; @@ -19,7 +20,7 @@ namespace Tgstation.Server.Host.Controllers /// for s /// [Route(Routes.Jobs)] - public sealed class JobController : ApiController + public sealed class JobController : InstanceRequiredController { /// /// The for the @@ -29,20 +30,22 @@ namespace Tgstation.Server.Host.Controllers /// /// Construct a /// + /// 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) : base( + instanceManager, databaseContext, authenticationContextFactory, - logger, - true) + logger) { 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 aa6c2fd8c1..10aea1aac6 100644 --- a/src/Tgstation.Server.Host/Controllers/RepositoryController.cs +++ b/src/Tgstation.Server.Host/Controllers/RepositoryController.cs @@ -29,13 +29,8 @@ namespace Tgstation.Server.Host.Controllers /// [Route(Routes.Repository)] #pragma warning disable CA1506 // TODO: Decomplexify - public sealed class RepositoryController : ApiController + public sealed class RepositoryController : InstanceRequiredController { - /// - /// The for the - /// - readonly IInstanceManager instanceManager; - /// /// The for the /// @@ -56,7 +51,7 @@ namespace Tgstation.Server.Host.Controllers /// /// The for the /// The for the - /// The value of + /// The for the . /// The value of /// The value of /// The for the @@ -70,13 +65,11 @@ namespace Tgstation.Server.Host.Controllers ILogger logger, IOptions generalConfigurationOptions) : base( + instanceManager, databaseContext, authenticationContextFactory, - logger, - true, - true) + logger) { - this.instanceManager = instanceManager ?? throw new ArgumentNullException(nameof(instanceManager)); this.gitHubClientFactory = gitHubClientFactory ?? throw new ArgumentNullException(nameof(gitHubClientFactory)); this.jobManager = jobManager ?? throw new ArgumentNullException(nameof(jobManager)); generalConfiguration = generalConfigurationOptions?.Value ?? throw new ArgumentNullException(nameof(generalConfigurationOptions)); @@ -192,61 +185,66 @@ namespace Tgstation.Server.Host.Controllers var cloneBranch = model.Reference; var origin = model.Origin; - var repoManager = instanceManager.GetInstance(Instance).RepositoryManager; - - if (repoManager.CloneInProgress) - return Conflict(new ErrorMessage(ErrorCode.RepoCloning)); - - if (repoManager.InUse) - return Conflict(new ErrorMessage(ErrorCode.RepoBusy)); - - using var repo = await repoManager.LoadRepository(cancellationToken).ConfigureAwait(false); - - // clone conflict - if (repo != null) - return Conflict(new ErrorMessage(ErrorCode.RepoExists)); - - var job = new Models.Job - { - Description = String.Format(CultureInfo.InvariantCulture, "Clone branch {1} of repository {0}", origin, cloneBranch ?? "master"), - StartedBy = AuthenticationContext.User, - CancelRightsType = RightsType.Repository, - CancelRight = (ulong)RepositoryRights.CancelClone, - Instance = Instance - }; - var api = currentModel.ToApi(); - await jobManager.RegisterOperation(job, async (paramJob, databaseContextFactory, progressReporter, ct) => - { - using var repos = await repoManager.CloneRepository( - new Uri(origin), - cloneBranch, - currentModel.AccessUser, - currentModel.AccessToken, - progressReporter, - model.RecurseSubmodules ?? true, - ct) - .ConfigureAwait(false); - if (repos == null) - throw new JobException(ErrorCode.RepoExists); - var instance = new Models.Instance + return await WithComponentInstance( + async instance => { - Id = Instance.Id - }; - await databaseContextFactory.UseContext( - async databaseContext => + var repoManager = instance.RepositoryManager; + + if (repoManager.CloneInProgress) + return Conflict(new ErrorMessage(ErrorCode.RepoCloning)); + + if (repoManager.InUse) + return Conflict(new ErrorMessage(ErrorCode.RepoBusy)); + + using var repo = await repoManager.LoadRepository(cancellationToken).ConfigureAwait(false); + + // clone conflict + if (repo != null) + return Conflict(new ErrorMessage(ErrorCode.RepoExists)); + + var job = new Models.Job { - databaseContext.Instances.Attach(instance); - if (await PopulateApi(api, repos, databaseContext, instance, ct).ConfigureAwait(false)) - await databaseContext.Save(ct).ConfigureAwait(false); - }) - .ConfigureAwait(false); - }, cancellationToken).ConfigureAwait(false); + Description = String.Format(CultureInfo.InvariantCulture, "Clone branch {1} of repository {0}", origin, cloneBranch ?? "master"), + StartedBy = AuthenticationContext.User, + CancelRightsType = RightsType.Repository, + CancelRight = (ulong)RepositoryRights.CancelClone, + Instance = Instance + }; + var api = currentModel.ToApi(); + await jobManager.RegisterOperation(job, async (paramJob, databaseContextFactory, progressReporter, ct) => + { + using var repos = await repoManager.CloneRepository( + new Uri(origin), + cloneBranch, + currentModel.AccessUser, + currentModel.AccessToken, + progressReporter, + model.RecurseSubmodules ?? true, + ct) + .ConfigureAwait(false); + if (repos == null) + throw new JobException(ErrorCode.RepoExists); + var instance = new Models.Instance + { + Id = Instance.Id + }; + await databaseContextFactory.UseContext( + async databaseContext => + { + databaseContext.Instances.Attach(instance); + if (await PopulateApi(api, repos, databaseContext, instance, ct).ConfigureAwait(false)) + await databaseContext.Save(ct).ConfigureAwait(false); + }) + .ConfigureAwait(false); + }, cancellationToken).ConfigureAwait(false); - api.Origin = model.Origin; - api.Reference = model.Reference; - api.ActiveJob = job.ToApi(); + api.Origin = model.Origin; + api.Reference = model.Reference; + api.ActiveJob = job.ToApi(); - return Created(api); + return Created(api); + }) + .ConfigureAwait(false); } /// @@ -286,9 +284,18 @@ namespace Tgstation.Server.Host.Controllers Instance = Instance }; var api = currentModel.ToApi(); - await jobManager.RegisterOperation(job, (paramJob, databaseContextFactory, progressReporter, ct) => instanceManager.GetInstance(Instance).RepositoryManager.DeleteRepository(cancellationToken), cancellationToken).ConfigureAwait(false); - api.ActiveJob = job.ToApi(); - return Accepted(api); + return await WithComponentInstance( + async instance => + { + await jobManager.RegisterOperation( + job, + (paramJob, databaseContextFactory, progressReporter, ct) => instance.RepositoryManager.DeleteRepository(ct), + cancellationToken) + .ConfigureAwait(false); + api.ActiveJob = job.ToApi(); + return Accepted(api); + }) + .ConfigureAwait(false); } /// @@ -317,23 +324,29 @@ namespace Tgstation.Server.Host.Controllers return Gone(); var api = currentModel.ToApi(); - var repoManager = instanceManager.GetInstance(Instance).RepositoryManager; - if (repoManager.CloneInProgress) - return Conflict(new ErrorMessage(ErrorCode.RepoCloning)); + return await WithComponentInstance( + async instance => + { + var repoManager = instance.RepositoryManager; - if (repoManager.InUse) - return Conflict(new ErrorMessage(ErrorCode.RepoBusy)); + if (repoManager.CloneInProgress) + return Conflict(new ErrorMessage(ErrorCode.RepoCloning)); - using var repo = await repoManager.LoadRepository(cancellationToken).ConfigureAwait(false); - if (repo != null && await PopulateApi(api, repo, DatabaseContext, Instance, cancellationToken).ConfigureAwait(false)) - { - // user may have fucked with the repo manually, do what we can - await DatabaseContext.Save(cancellationToken).ConfigureAwait(false); - return Created(api); - } + if (repoManager.InUse) + return Conflict(new ErrorMessage(ErrorCode.RepoBusy)); - return Json(api); + using var repo = await repoManager.LoadRepository(cancellationToken).ConfigureAwait(false); + if (repo != null && await PopulateApi(api, repo, DatabaseContext, Instance, cancellationToken).ConfigureAwait(false)) + { + // user may have fucked with the repo manually, do what we can + await DatabaseContext.Save(cancellationToken).ConfigureAwait(false); + return Created(api); + } + + return Json(api); + }) + .ConfigureAwait(false); } /// @@ -426,23 +439,32 @@ namespace Tgstation.Server.Host.Controllers var canRead = userRights.HasFlag(RepositoryRights.Read); var api = canRead ? currentModel.ToApi() : new Repository(); - var repoManager = instanceManager.GetInstance(Instance).RepositoryManager; - if (canRead) { - if (repoManager.CloneInProgress) - return Conflict(new ErrorMessage(ErrorCode.RepoCloning)); + var earlyOut = await WithComponentInstance( + async instance => + { + var repoManager = instance.RepositoryManager; + if (repoManager.CloneInProgress) + return Conflict(new ErrorMessage(ErrorCode.RepoCloning)); - if (repoManager.InUse) - return Conflict(new ErrorMessage(ErrorCode.RepoBusy)); + if (repoManager.InUse) + return Conflict(new ErrorMessage(ErrorCode.RepoBusy)); - using var repo = await repoManager.LoadRepository(cancellationToken).ConfigureAwait(false); - if (repo == null) - return Conflict(new ErrorMessage(ErrorCode.RepoMissing)); - await PopulateApi(api, repo, DatabaseContext, Instance, cancellationToken).ConfigureAwait(false); + using var repo = await repoManager.LoadRepository(cancellationToken).ConfigureAwait(false); + if (repo == null) + return Conflict(new ErrorMessage(ErrorCode.RepoMissing)); + await PopulateApi(api, repo, DatabaseContext, Instance, cancellationToken).ConfigureAwait(false); - if (model.Origin != null && model.Origin != repo.Origin) - return BadRequest(new ErrorMessage(ErrorCode.RepoCantChangeOrigin)); + if (model.Origin != null && model.Origin != repo.Origin) + return BadRequest(new ErrorMessage(ErrorCode.RepoCantChangeOrigin)); + + return null; + }) + .ConfigureAwait(false); + + if (earlyOut != null) + return earlyOut; } // this is just db stuf so stow it away @@ -471,18 +493,13 @@ namespace Tgstation.Server.Host.Controllers if (description == null) return Json(api); // no git changes - var job = new Models.Job - { - Description = description, - StartedBy = AuthenticationContext.User, - Instance = Instance, - CancelRightsType = RightsType.Repository, - CancelRight = (ulong)RepositoryRights.CancelPendingChanges, - }; - - // Time to access git, do it in a job - await jobManager.RegisterOperation(job, async (paramJob, databaseContextFactory, progressReporter, ct) => + async Task UpdateCallbackThatDesperatelyNeedsRefactoring( + IInstance instance, + IDatabaseContextFactory databaseContextFactory, + Action progressReporter, + CancellationToken ct) { + var repoManager = instance.RepositoryManager; using var repo = await repoManager.LoadRepository(ct).ConfigureAwait(false); if (repo == null) throw new JobException(ErrorCode.RepoMissing); @@ -849,6 +866,8 @@ namespace Tgstation.Server.Host.Controllers await repo.Sychronize(currentModel.AccessUser, currentModel.AccessToken, currentModel.CommitterName, currentModel.CommitterEmail, NextProgressReporter(), false, ct).ConfigureAwait(false); await UpdateRevInfo().ConfigureAwait(false); } + + return null; } catch { @@ -864,7 +883,29 @@ namespace Tgstation.Server.Host.Controllers progressReporter(100); throw; } - }, cancellationToken).ConfigureAwait(false); + } + + var job = new Models.Job + { + Description = description, + StartedBy = AuthenticationContext.User, + Instance = Instance, + CancelRightsType = RightsType.Repository, + CancelRight = (ulong)RepositoryRights.CancelPendingChanges, + }; + + // Time to access git, do it in a job + await jobManager.RegisterOperation( + job, + (paramJob, databaseContextFactory, progressReporter, ct) + => WithComponentInstance( // Will never fail in the context of a job + instance => UpdateCallbackThatDesperatelyNeedsRefactoring( + instance, + databaseContextFactory, + progressReporter, + ct)), + cancellationToken) + .ConfigureAwait(false); api.ActiveJob = job.ToApi(); return Accepted(api); diff --git a/src/Tgstation.Server.Host/Security/AuthenticationContextFactory.cs b/src/Tgstation.Server.Host/Security/AuthenticationContextFactory.cs index 45935ac098..d74ef23d4a 100644 --- a/src/Tgstation.Server.Host/Security/AuthenticationContextFactory.cs +++ b/src/Tgstation.Server.Host/Security/AuthenticationContextFactory.cs @@ -91,7 +91,7 @@ namespace Tgstation.Server.Host.Security { instanceUser = await databaseContext.InstanceUsers .AsQueryable() - .Where(x => x.UserId == userId && x.InstanceId == instanceId && x.Instance.Online.Value) + .Where(x => x.UserId == userId && x.InstanceId == instanceId) .Include(x => x.Instance) .FirstOrDefaultAsync(cancellationToken) .ConfigureAwait(false); diff --git a/tests/Tgstation.Server.Tests/InstanceManagerTest.cs b/tests/Tgstation.Server.Tests/InstanceManagerTest.cs index 208ed2bdb5..8333226b8e 100644 --- a/tests/Tgstation.Server.Tests/InstanceManagerTest.cs +++ b/tests/Tgstation.Server.Tests/InstanceManagerTest.cs @@ -45,6 +45,9 @@ namespace Tgstation.Server.Tests //check it exists Assert.IsTrue(Directory.Exists(firstTest.Path)); + var firstClient = instanceManagerClient.CreateClient(firstTest); + await ApiAssert.ThrowsException(() => firstClient.DreamDaemon.Start(cancellationToken), ErrorCode.InstanceOffline); + //cant create instances in existent directories var testNonEmpty = Path.Combine(testRootPath, Guid.NewGuid().ToString()); Directory.CreateDirectory(testNonEmpty); @@ -65,6 +68,7 @@ namespace Tgstation.Server.Tests }, cancellationToken).ConfigureAwait(false); await Assert.ThrowsExceptionAsync(() => instanceManagerClient.CreateOrAttach(firstTest, cancellationToken)).ConfigureAwait(false); + Assert.IsTrue(Directory.Exists(firstTest.Path)); //can't create instances in installation directory await ApiAssert.ThrowsException(() => instanceManagerClient.CreateOrAttach(new Api.Models.Instance @@ -79,6 +83,7 @@ namespace Tgstation.Server.Tests Path = Path.Combine(firstTest.Path, "subdir"), Name = "NoOtherInstanceDirTest" }, cancellationToken), ErrorCode.InstanceAtConflictingPath).ConfigureAwait(false); + Assert.IsTrue(Directory.Exists(firstTest.Path)); //can't move to existent directories await ApiAssert.ThrowsException(() => instanceManagerClient.Update(new Api.Models.Instance @@ -112,7 +117,7 @@ namespace Tgstation.Server.Tests firstTest = await instanceManagerClient.GetId(firstTest, cancellationToken).ConfigureAwait(false); await Task.Delay(TimeSpan.FromSeconds(1), cancellationToken).ConfigureAwait(false); } while (firstTest.MoveJob != null); - + Assert.IsTrue(Directory.Exists(firstTest.Path)); //online it for real for component tests firstTest.Online = true; @@ -120,6 +125,7 @@ namespace Tgstation.Server.Tests firstTest = await instanceManagerClient.Update(firstTest, cancellationToken).ConfigureAwait(false); Assert.AreEqual(true, firstTest.Online); Assert.AreEqual(ConfigurationType.HostWrite, firstTest.ConfigurationType); + Assert.IsTrue(Directory.Exists(firstTest.Path)); //can't move online instance await ApiAssert.ThrowsException(() => instanceManagerClient.Update(new Api.Models.Instance @@ -127,6 +133,7 @@ namespace Tgstation.Server.Tests Id = firstTest.Id, Path = initialPath }, cancellationToken), ErrorCode.InstanceRelocateOnline).ConfigureAwait(false); + Assert.IsTrue(Directory.Exists(firstTest.Path)); return firstTest; } @@ -159,8 +166,7 @@ namespace Tgstation.Server.Tests await instanceManagerClient.Detach(firstTest, cancellationToken).ConfigureAwait(false); - var instanceAttachFileName = (string)typeof(InstanceController).GetField("InstanceAttachFileName", BindingFlags.NonPublic | BindingFlags.Static).GetValue(null); - var attachPath = Path.Combine(firstTest.Path, instanceAttachFileName); + var attachPath = Path.Combine(firstTest.Path, InstanceController.InstanceAttachFileName); Assert.IsTrue(File.Exists(attachPath)); //can recreate detached instance diff --git a/tests/Tgstation.Server.Tests/IntegrationTest.cs b/tests/Tgstation.Server.Tests/IntegrationTest.cs index 44de2583a4..b842dd6345 100644 --- a/tests/Tgstation.Server.Tests/IntegrationTest.cs +++ b/tests/Tgstation.Server.Tests/IntegrationTest.cs @@ -183,7 +183,9 @@ namespace Tgstation.Server.Tests var usersTest = FailFast(new UsersTest(adminClient.Users).Run(cancellationToken)); instance = await new InstanceManagerTest(adminClient.Instances, adminClient.Users, server.Directory).RunPreInstanceTest(cancellationToken); + Assert.IsTrue(Directory.Exists(instance.Path)); var instanceClient = adminClient.Instances.CreateClient(instance); + Assert.IsTrue(Directory.Exists(instanceClient.Metadata.Path)); var instanceTests = FailFast(new InstanceTest(instanceClient, adminClient.Instances).RunTests(cancellationToken)); From 0ea8a11320731ca3d7564b1a44e0eb3789aaca28 Mon Sep 17 00:00:00 2001 From: Jordan Brown Date: Mon, 13 Jul 2020 13:05:39 -0400 Subject: [PATCH 16/68] DMAPI 5.2.3 - Fixed issue with API consumers being able to modify immutable lists --- build/Version.props | 2 +- src/DMAPI/tgs.dm | 2 +- src/DMAPI/tgs/v3210/api.dm | 2 +- src/DMAPI/tgs/v4/api.dm | 2 +- src/DMAPI/tgs/v5/api.dm | 6 +++--- .../Components/Interop/DMApiConstants.cs | 2 +- 6 files changed, 8 insertions(+), 8 deletions(-) diff --git a/build/Version.props b/build/Version.props index 01d822a7b2..1fcd4953c8 100644 --- a/build/Version.props +++ b/build/Version.props @@ -6,7 +6,7 @@ 2.0.0 7.1.0 8.1.0 - 5.2.2 + 5.2.3 0.4.0 1.1.0 diff --git a/src/DMAPI/tgs.dm b/src/DMAPI/tgs.dm index d0cf1618a6..35d9dbefbd 100644 --- a/src/DMAPI/tgs.dm +++ b/src/DMAPI/tgs.dm @@ -1,6 +1,6 @@ // tgstation-server DMAPI -#define TGS_DMAPI_VERSION "5.2.2" +#define TGS_DMAPI_VERSION "5.2.3" // All functions and datums outside this document are subject to change with any version and should not be relied on. diff --git a/src/DMAPI/tgs/v3210/api.dm b/src/DMAPI/tgs/v3210/api.dm index e0d00ad15d..96499fb0a6 100644 --- a/src/DMAPI/tgs/v3210/api.dm +++ b/src/DMAPI/tgs/v3210/api.dm @@ -180,7 +180,7 @@ ExportService(SERVICE_REQUEST_KILL_PROCESS) /datum/tgs_api/v3210/ChatChannelInfo() - return list() + return list() // :omegalul: /datum/tgs_api/v3210/ChatBroadcast(message, list/channels) if(channels) diff --git a/src/DMAPI/tgs/v4/api.dm b/src/DMAPI/tgs/v4/api.dm index 5e7f3c60be..d58cec0936 100644 --- a/src/DMAPI/tgs/v4/api.dm +++ b/src/DMAPI/tgs/v4/api.dm @@ -257,7 +257,7 @@ return instance_name /datum/tgs_api/v4/TestMerges() - return cached_test_merges + return cached_test_merges.Copy() /datum/tgs_api/v4/EndProcess() Export(TGS4_COMM_END_PROCESS) diff --git a/src/DMAPI/tgs/v5/api.dm b/src/DMAPI/tgs/v5/api.dm index 3791cbde84..6a7f2414e1 100644 --- a/src/DMAPI/tgs/v5/api.dm +++ b/src/DMAPI/tgs/v5/api.dm @@ -16,7 +16,7 @@ var/list/chat_channels /datum/tgs_api/v5/ApiVersion() - return new /datum/tgs_version("5.2.2") + return new /datum/tgs_version("5.2.3") /datum/tgs_api/v5/OnWorldNew(minimum_required_security_level) server_port = world.params[DMAPI5_PARAM_SERVER_PORT] @@ -284,7 +284,7 @@ /datum/tgs_api/v5/TestMerges() RequireInitialBridgeResponse() - return test_merges + return test_merges.Copy() /datum/tgs_api/v5/EndProcess() Bridge(DMAPI5_BRIDGE_COMMAND_KILL) @@ -329,7 +329,7 @@ /datum/tgs_api/v5/ChatChannelInfo() RequireInitialBridgeResponse() - return chat_channels + return chat_channels.Copy() /datum/tgs_api/v5/proc/DecodeChannels(chat_update_json) var/list/chat_channels_json = chat_update_json[DMAPI5_CHAT_UPDATE_CHANNELS] diff --git a/src/Tgstation.Server.Host/Components/Interop/DMApiConstants.cs b/src/Tgstation.Server.Host/Components/Interop/DMApiConstants.cs index d91d5bdbf7..1dc6e31381 100644 --- a/src/Tgstation.Server.Host/Components/Interop/DMApiConstants.cs +++ b/src/Tgstation.Server.Host/Components/Interop/DMApiConstants.cs @@ -33,7 +33,7 @@ namespace Tgstation.Server.Host.Components.Interop /// /// The DMAPI being used. /// - public static readonly Version Version = new Version(5, 2, 2); + public static readonly Version Version = new Version(5, 2, 3); /// /// for use when communicating with the DMAPI. From 466df416deed08d5759794695d32b08ffa3d9843 Mon Sep 17 00:00:00 2001 From: Jordan Brown Date: Mon, 13 Jul 2020 13:17:18 -0400 Subject: [PATCH 17/68] Even safe instance accessing - Break up IInstance for better separation of church and state - Jobs now provide the IInstanceCore to the operation callback - Added instance references to prevent race conditions with controllers and onlining/offlining --- .../Components/IInstance.cs | 49 +-------- .../Components/IInstanceCore.cs | 53 ++++++++++ .../Components/IInstanceCoreProvider.cs | 15 +++ .../Components/IInstanceManager.cs | 4 +- .../Components/IInstanceReference.cs | 15 +++ .../Components/Instance.cs | 21 +++- .../Components/InstanceContainer.cs | 86 +++++++++++++++ .../Components/InstanceManager.cs | 100 ++++++++++++++---- .../Components/InstanceWrapper.cs | 99 +++++++++++++++++ .../Components/Watchdog/WatchdogBase.cs | 15 ++- .../Controllers/ByondController.cs | 2 +- .../Controllers/DreamDaemonController.cs | 6 +- .../Controllers/DreamMakerController.cs | 37 ++++--- .../Controllers/InstanceController.cs | 7 +- .../Controllers/InstanceRequiredController.cs | 18 ++-- .../Controllers/RepositoryController.cs | 41 ++++--- src/Tgstation.Server.Host/Core/Application.cs | 3 +- .../Extensions/ServiceCollectionExtensions.cs | 4 +- src/Tgstation.Server.Host/Jobs/IJobManager.cs | 6 +- .../Jobs/JobEntrypoint.cs | 25 +++++ src/Tgstation.Server.Host/Jobs/JobHandler.cs | 31 ++++-- src/Tgstation.Server.Host/Jobs/JobManager.cs | 100 +++++++++++------- 22 files changed, 556 insertions(+), 181 deletions(-) create mode 100644 src/Tgstation.Server.Host/Components/IInstanceCore.cs create mode 100644 src/Tgstation.Server.Host/Components/IInstanceCoreProvider.cs create mode 100644 src/Tgstation.Server.Host/Components/IInstanceReference.cs create mode 100644 src/Tgstation.Server.Host/Components/InstanceContainer.cs create mode 100644 src/Tgstation.Server.Host/Components/InstanceWrapper.cs create mode 100644 src/Tgstation.Server.Host/Jobs/JobEntrypoint.cs diff --git a/src/Tgstation.Server.Host/Components/IInstance.cs b/src/Tgstation.Server.Host/Components/IInstance.cs index 05a0c2fa91..b7e64db9b2 100644 --- a/src/Tgstation.Server.Host/Components/IInstance.cs +++ b/src/Tgstation.Server.Host/Components/IInstance.cs @@ -1,55 +1,12 @@ using Microsoft.Extensions.Hosting; using System; -using System.Threading.Tasks; -using Tgstation.Server.Host.Components.Byond; -using Tgstation.Server.Host.Components.Chat; -using Tgstation.Server.Host.Components.Deployment; -using Tgstation.Server.Host.Components.Repository; -using Tgstation.Server.Host.Components.StaticFiles; -using Tgstation.Server.Host.Components.Watchdog; namespace Tgstation.Server.Host.Components { /// - /// For interacting with the instance services + /// Component version of . /// - public interface IInstance : ILatestCompileJobProvider, IHostedService, IRenameNotifyee, IAsyncDisposable + interface IInstance : IInstanceCore, IHostedService, IAsyncDisposable { - /// - /// The for the - /// - IRepositoryManager RepositoryManager { get; } - - /// - /// The for the - /// - IByondManager ByondManager { get; } - - /// - /// The for the . - /// - IDreamMaker DreamMaker { get; } - - /// - /// The for the - /// - IWatchdog Watchdog { get; } - - /// - /// The for the - /// - IChatManager Chat { get; } - - /// - /// The for the - /// - IConfiguration Configuration { get; } - - /// - /// Change the for the - /// - /// The new auto update inteval - /// A representing the running operation - Task SetAutoUpdateInterval(uint newInterval); } -} \ No newline at end of file +} diff --git a/src/Tgstation.Server.Host/Components/IInstanceCore.cs b/src/Tgstation.Server.Host/Components/IInstanceCore.cs new file mode 100644 index 0000000000..27552e5bc6 --- /dev/null +++ b/src/Tgstation.Server.Host/Components/IInstanceCore.cs @@ -0,0 +1,53 @@ +using System.Threading.Tasks; +using Tgstation.Server.Host.Components.Byond; +using Tgstation.Server.Host.Components.Chat; +using Tgstation.Server.Host.Components.Deployment; +using Tgstation.Server.Host.Components.Repository; +using Tgstation.Server.Host.Components.StaticFiles; +using Tgstation.Server.Host.Components.Watchdog; + +namespace Tgstation.Server.Host.Components +{ + /// + /// For interacting with the instance services + /// + public interface IInstanceCore : ILatestCompileJobProvider, IRenameNotifyee + { + /// + /// The for the + /// + IRepositoryManager RepositoryManager { get; } + + /// + /// The for the + /// + IByondManager ByondManager { get; } + + /// + /// The for the . + /// + IDreamMaker DreamMaker { get; } + + /// + /// The for the + /// + IWatchdog Watchdog { get; } + + /// + /// The for the + /// + IChatManager Chat { get; } + + /// + /// The for the + /// + IConfiguration Configuration { get; } + + /// + /// Change the for the + /// + /// The new auto update inteval + /// A representing the running operation + Task SetAutoUpdateInterval(uint newInterval); + } +} \ No newline at end of file diff --git a/src/Tgstation.Server.Host/Components/IInstanceCoreProvider.cs b/src/Tgstation.Server.Host/Components/IInstanceCoreProvider.cs new file mode 100644 index 0000000000..494aaf130f --- /dev/null +++ b/src/Tgstation.Server.Host/Components/IInstanceCoreProvider.cs @@ -0,0 +1,15 @@ +namespace Tgstation.Server.Host.Components +{ + /// + /// Provider for s + /// + interface IInstanceCoreProvider + { + /// + /// Get the for a given if it's online. + /// + /// The to get the for. + /// The if it is online, otherwise. + IInstanceCore GetInstance(Models.Instance instance); + } +} diff --git a/src/Tgstation.Server.Host/Components/IInstanceManager.cs b/src/Tgstation.Server.Host/Components/IInstanceManager.cs index a4ff54c906..1731be40cf 100644 --- a/src/Tgstation.Server.Host/Components/IInstanceManager.cs +++ b/src/Tgstation.Server.Host/Components/IInstanceManager.cs @@ -16,11 +16,11 @@ namespace Tgstation.Server.Host.Components Task Ready { get; } /// - /// Get the associated with given + /// Get the associated with given /// /// The of the desired /// The associated with the given if it is online, otherwise. - IInstance GetInstance(Models.Instance metadata); + IInstanceReference GetInstanceReference(Models.Instance metadata); /// /// Online an diff --git a/src/Tgstation.Server.Host/Components/IInstanceReference.cs b/src/Tgstation.Server.Host/Components/IInstanceReference.cs new file mode 100644 index 0000000000..a6453747f8 --- /dev/null +++ b/src/Tgstation.Server.Host/Components/IInstanceReference.cs @@ -0,0 +1,15 @@ +using System; + +namespace Tgstation.Server.Host.Components +{ + /// + /// Controller version of . + /// + public interface IInstanceReference : IInstanceCore, IDisposable + { + /// + /// A unique ID for the . + /// + public Guid Uid { get; } + } +} diff --git a/src/Tgstation.Server.Host/Components/Instance.cs b/src/Tgstation.Server.Host/Components/Instance.cs index 1617658bf5..8eba501262 100644 --- a/src/Tgstation.Server.Host/Components/Instance.cs +++ b/src/Tgstation.Server.Host/Components/Instance.cs @@ -23,6 +23,11 @@ namespace Tgstation.Server.Host.Components #pragma warning disable CA1506 // TODO: Decomplexify sealed class Instance : IInstance { + /// + /// Message for the if ever a job starts on a different than the one that queued it. + /// + public const string DifferentCoreExceptionMessage = "Job started on different instance core!"; + /// public IRepositoryManager RepositoryManager { get; } @@ -186,8 +191,11 @@ namespace Tgstation.Server.Host.Components }; string deploySha = null; - await jobManager.RegisterOperation(repositoryUpdateJob, async (paramJob, databaseContextFactory, progressReporter, jobCancellationToken) => + await jobManager.RegisterOperation(repositoryUpdateJob, async (core, databaseContextFactory, paramJob, progressReporter, jobCancellationToken) => { + if (core != this) + throw new InvalidOperationException(DifferentCoreExceptionMessage); + // assume 5 steps with synchronize const int ProgressSections = 7; const int ProgressStep = 100 / ProgressSections; @@ -373,7 +381,16 @@ namespace Tgstation.Server.Host.Components await jobManager.RegisterOperation( compileProcessJob, - DreamMaker.DeploymentProcess, + (core, databaseContextFactory, job, progressReporter, jobCancellationToken) => + { + if (core != this) + throw new InvalidOperationException(DifferentCoreExceptionMessage); + return DreamMaker.DeploymentProcess( + job, + databaseContextFactory, + progressReporter, + jobCancellationToken); + }, cancellationToken).ConfigureAwait(false); await jobManager.WaitForJobCompletion(compileProcessJob, systemUser, default, cancellationToken).ConfigureAwait(false); diff --git a/src/Tgstation.Server.Host/Components/InstanceContainer.cs b/src/Tgstation.Server.Host/Components/InstanceContainer.cs new file mode 100644 index 0000000000..3a1e64f92a --- /dev/null +++ b/src/Tgstation.Server.Host/Components/InstanceContainer.cs @@ -0,0 +1,86 @@ +using System; +using System.Threading.Tasks; + +namespace Tgstation.Server.Host.Components +{ + /// + /// Wrapper for managing s + /// + sealed class InstanceContainer + { + /// + /// The . + /// + public IInstance Instance { get; } + + /// + /// A that completes when there are no s active for the . + /// + public Task OnZeroReferences + { + get + { + lock (referenceCountLock) + { + if (referenceCount == 0) + return Task.CompletedTask; + return onZeroReferencesTcs.Task; + } + } + } + + /// + /// for . + /// + readonly object referenceCountLock; + + /// + /// Backing for . + /// + TaskCompletionSource onZeroReferencesTcs; + + /// + /// Count of active s. + /// + ulong referenceCount; + + /// + /// Initializes a new instance of the . + /// + /// The value of . + public InstanceContainer(IInstance instance) + { + Instance = instance ?? throw new ArgumentNullException(nameof(instance)); + + referenceCountLock = new object(); + } + + /// + /// Create a new . + /// + /// A new . + public IInstanceReference AddReference() + { + lock (referenceCountLock) + { + if (referenceCount++ == 0) + onZeroReferencesTcs = new TaskCompletionSource(); + + try + { + return new InstanceWrapper(Instance, () => + { + lock (referenceCountLock) + if (--referenceCount == 0) + onZeroReferencesTcs.SetResult(null); + }); + } + catch + { + --referenceCount; + throw; + } + } + } + } +} diff --git a/src/Tgstation.Server.Host/Components/InstanceManager.cs b/src/Tgstation.Server.Host/Components/InstanceManager.cs index 81bd54d97c..ce3d3f775f 100644 --- a/src/Tgstation.Server.Host/Components/InstanceManager.cs +++ b/src/Tgstation.Server.Host/Components/InstanceManager.cs @@ -24,7 +24,13 @@ using Tgstation.Server.Host.System; namespace Tgstation.Server.Host.Components { /// - sealed class InstanceManager : IInstanceManager, IRestartHandler, IHostedService, IBridgeRegistrar, IAsyncDisposable + sealed class InstanceManager : + IInstanceManager, + IInstanceCoreProvider, + IRestartHandler, + IHostedService, + IBridgeRegistrar, + IAsyncDisposable { /// public Task Ready => readyTcs.Task; @@ -90,15 +96,20 @@ namespace Tgstation.Server.Host.Components readonly ILogger logger; /// - /// Map of instance s to respective s. Also used as a . + /// Map of instance s to respective s. Also used as a . /// - readonly IDictionary instances; + readonly IDictionary instances; /// /// Map of s to their respective s. /// readonly IDictionary bridgeHandlers; + /// + /// used to guard calls to and . + /// + readonly SemaphoreSlim instanceStateChangeSemaphore; + /// /// The for the . /// @@ -163,9 +174,10 @@ namespace Tgstation.Server.Host.Components lazyRestartRegistration = new Lazy(() => serverControl.RegisterForRestart(this)); - instances = new Dictionary(); + instances = new Dictionary(); bridgeHandlers = new Dictionary(); readyTcs = new TaskCompletionSource(); + instanceStateChangeSemaphore = new SemaphoreSlim(1); } /// @@ -179,22 +191,29 @@ namespace Tgstation.Server.Host.Components } foreach (var I in instances) - await I.Value.DisposeAsync().ConfigureAwait(false); + await I.Value.Instance.DisposeAsync().ConfigureAwait(false); lazyRestartRegistration.Value.Dispose(); + instanceStateChangeSemaphore.Dispose(); logger.LogInformation("Server shutdown"); } /// - public IInstance GetInstance(Models.Instance metadata) + public IInstanceReference GetInstanceReference(Models.Instance metadata) { if (metadata == null) throw new ArgumentNullException(nameof(metadata)); + lock (instances) { - instances.TryGetValue(metadata.Id, out IInstance instance); - return instance; // null if above is false + if (!instances.TryGetValue(metadata.Id, out var instance)) + { + logger.LogTrace("Cannot reference instance {0} as it is not online!", metadata.Id); + return null; + } + + return instance.AddReference(); } } @@ -203,7 +222,7 @@ namespace Tgstation.Server.Host.Components { if (oldPath == null) throw new ArgumentNullException(nameof(oldPath)); - if (GetInstance(instance) != null) + if (GetInstanceReference(instance) != null) throw new InvalidOperationException("Cannot move an online instance!"); var newPath = instance.Path; try @@ -268,17 +287,26 @@ namespace Tgstation.Server.Host.Components { if (metadata == null) throw new ArgumentNullException(nameof(metadata)); + + using var _ = await SemaphoreSlimContext.Lock(instanceStateChangeSemaphore, cancellationToken).ConfigureAwait(false); + logger.LogInformation("Offlining instance ID {0}", metadata.Id); - IInstance instance; + InstanceContainer container; lock (instances) { - if (!instances.TryGetValue(metadata.Id, out instance)) - throw new InvalidOperationException("Instance not online!"); + if (!instances.TryGetValue(metadata.Id, out container)) + { + logger.LogDebug("Not offlining removed instance {0}", metadata.Id); + return; + } + instances.Remove(metadata.Id); } try { + await container.OnZeroReferences.ConfigureAwait(false); + // we are the one responsible for cancelling his jobs var tasks = new List(); await databaseContextFactory.UseContext(async db => @@ -300,11 +328,11 @@ namespace Tgstation.Server.Host.Components await Task.WhenAll(tasks).ConfigureAwait(false); - await instance.StopAsync(cancellationToken).ConfigureAwait(false); + await container.Instance.StopAsync(cancellationToken).ConfigureAwait(false); } finally { - await instance.DisposeAsync().ConfigureAwait(false); + await container.Instance.DisposeAsync().ConfigureAwait(false); } } @@ -313,15 +341,37 @@ namespace Tgstation.Server.Host.Components { if (metadata == null) throw new ArgumentNullException(nameof(metadata)); + + using var _ = await SemaphoreSlimContext.Lock(instanceStateChangeSemaphore, cancellationToken).ConfigureAwait(false); + lock (instances) + if (instances.ContainsKey(metadata.Id)) + { + logger.LogDebug("Aborting instance creation due to it seemingly already being online"); + return; + } + logger.LogInformation("Onlining instance ID {0} ({1}) at {2}", metadata.Id, metadata.Name, metadata.Path); var instance = await instanceFactory.CreateInstance(this, metadata).ConfigureAwait(false); try { - lock (instances) + await instance.StartAsync(cancellationToken).ConfigureAwait(false); + + try { - if (instances.ContainsKey(metadata.Id)) - throw new InvalidOperationException("Instance already online!"); - instances.Add(metadata.Id, instance); + lock (instances) + instances.Add(metadata.Id, new InstanceContainer(instance)); + } + catch (Exception ex) + { + logger.LogError("Unable to commit onlined instance {0} into service, offlining!", metadata.Id); + try + { + await instance.StopAsync(default).ConfigureAwait(false); + } + catch (Exception innerEx) + { + throw new AggregateException(innerEx, ex); + } } } catch @@ -329,8 +379,6 @@ namespace Tgstation.Server.Host.Components await instance.DisposeAsync().ConfigureAwait(false); throw; } - - await instance.StartAsync(cancellationToken).ConfigureAwait(false); } /// @@ -405,7 +453,7 @@ namespace Tgstation.Server.Host.Components public async Task StopAsync(CancellationToken cancellationToken) { await jobManager.StopAsync(cancellationToken).ConfigureAwait(false); - await Task.WhenAll(instances.Select(x => x.Value.StopAsync(cancellationToken))).ConfigureAwait(false); + await Task.WhenAll(instances.Select(x => x.Value.Instance.StopAsync(cancellationToken))).ConfigureAwait(false); await instanceFactory.StopAsync(cancellationToken).ConfigureAwait(false); // downgrade the db if necessary @@ -489,5 +537,15 @@ namespace Tgstation.Server.Host.Components } }); } + + /// + public IInstanceCore GetInstance(Models.Instance metadata) + { + lock (instances) + { + instances.TryGetValue(metadata.Id, out var container); + return container?.Instance; + } + } } } diff --git a/src/Tgstation.Server.Host/Components/InstanceWrapper.cs b/src/Tgstation.Server.Host/Components/InstanceWrapper.cs new file mode 100644 index 0000000000..0830098235 --- /dev/null +++ b/src/Tgstation.Server.Host/Components/InstanceWrapper.cs @@ -0,0 +1,99 @@ +using System; +using System.Threading; +using System.Threading.Tasks; +using Tgstation.Server.Host.Components.Byond; +using Tgstation.Server.Host.Components.Chat; +using Tgstation.Server.Host.Components.Deployment; +using Tgstation.Server.Host.Components.Repository; +using Tgstation.Server.Host.Components.StaticFiles; +using Tgstation.Server.Host.Components.Watchdog; +using Tgstation.Server.Host.Models; + +namespace Tgstation.Server.Host.Components +{ + /// + /// Warpper around a given with a . + /// + sealed class InstanceWrapper : IInstanceReference + { + /// + public Guid Uid { get; } + + /// + /// The object for . + /// + readonly object disposeLock; + + /// + /// The to take when is called. + /// + Action onDisposed; + + /// + /// The calls are forwarded to. + /// + IInstanceCore actualInstance; + + /// + /// Initializes a new instance of the . + /// + /// The value of . + /// The value of . + public InstanceWrapper(IInstanceCore actualInstance, Action onDisposed) + { + 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 IRepositoryManager RepositoryManager => actualInstance?.RepositoryManager ?? throw new ObjectDisposedException(nameof(InstanceWrapper)); + + /// + 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); + } + } +} diff --git a/src/Tgstation.Server.Host/Components/Watchdog/WatchdogBase.cs b/src/Tgstation.Server.Host/Components/Watchdog/WatchdogBase.cs index e2b4bbb176..5b061c5108 100644 --- a/src/Tgstation.Server.Host/Components/Watchdog/WatchdogBase.cs +++ b/src/Tgstation.Server.Host/Components/Watchdog/WatchdogBase.cs @@ -912,11 +912,16 @@ namespace Tgstation.Server.Host.Components.Watchdog CancelRight = (ulong)DreamDaemonRights.Shutdown, CancelRightsType = RightsType.DreamDaemon }; - await jobManager.RegisterOperation(job, async (j, databaseContextFactory, progressFunction, ct) => - { - using (await SemaphoreSlimContext.Lock(synchronizationSemaphore, ct).ConfigureAwait(false)) - await LaunchNoLock(true, true, true, reattachInfo, ct).ConfigureAwait(false); - }, cancellationToken).ConfigureAwait(false); + await jobManager.RegisterOperation( + job, + async (core, databaseContextFactory, paramJob, progressFunction, ct) => + { + // core will certainly be null here since jobs started before the instance is onlined can't provide one + using (await SemaphoreSlimContext.Lock(synchronizationSemaphore, ct).ConfigureAwait(false)) + await LaunchNoLock(true, true, true, reattachInfo, ct).ConfigureAwait(false); + }, + cancellationToken) + .ConfigureAwait(false); } /// diff --git a/src/Tgstation.Server.Host/Controllers/ByondController.cs b/src/Tgstation.Server.Host/Controllers/ByondController.cs index 0db373c6e0..cab2dd1fb2 100644 --- a/src/Tgstation.Server.Host/Controllers/ByondController.cs +++ b/src/Tgstation.Server.Host/Controllers/ByondController.cs @@ -154,7 +154,7 @@ namespace Tgstation.Server.Host.Controllers }; await jobManager.RegisterOperation( job, - (paramJob, databaseContextFactory, progressHandler, jobCancellationToken) => byondManager.ChangeVersion( + (core, databaseContextFactory, paramJob, progressHandler, jobCancellationToken) => core.ByondManager.ChangeVersion( model.Version, model.Content, jobCancellationToken), diff --git a/src/Tgstation.Server.Host/Controllers/DreamDaemonController.cs b/src/Tgstation.Server.Host/Controllers/DreamDaemonController.cs index 1cd85be3c2..82e6866312 100644 --- a/src/Tgstation.Server.Host/Controllers/DreamDaemonController.cs +++ b/src/Tgstation.Server.Host/Controllers/DreamDaemonController.cs @@ -78,7 +78,7 @@ namespace Tgstation.Server.Host.Controllers }; await jobManager.RegisterOperation( job, - (paramJob, databaseContextFactory, progressHandler, innerCt) => instance.Watchdog.Launch(innerCt), + (core, databaseContextFactory, paramJob, progressHandler, innerCt) => core.Watchdog.Launch(innerCt), cancellationToken) .ConfigureAwait(false); return Accepted(job.ToApi()); @@ -302,7 +302,7 @@ namespace Tgstation.Server.Host.Controllers await jobManager.RegisterOperation( job, - (paramJob, databaseContextFactory, progressReporter, ct) => watchdog.Restart(false, ct), + (core, paramJob, databaseContextFactory, progressReporter, ct) => core.Watchdog.Restart(false, ct), cancellationToken) .ConfigureAwait(false); return Accepted(job.ToApi()); @@ -336,7 +336,7 @@ namespace Tgstation.Server.Host.Controllers await jobManager.RegisterOperation( job, - (paramJob, databaseContextFactory, progressReporter, ct) => watchdog.CreateDump(ct), cancellationToken) + (core, databaseContextFactory, paramJob, progressReporter, ct) => core.Watchdog.CreateDump(ct), cancellationToken) .ConfigureAwait(false); return Accepted(job.ToApi()); }); diff --git a/src/Tgstation.Server.Host/Controllers/DreamMakerController.cs b/src/Tgstation.Server.Host/Controllers/DreamMakerController.cs index 6a27f95aba..7a4ec3bd0c 100644 --- a/src/Tgstation.Server.Host/Controllers/DreamMakerController.cs +++ b/src/Tgstation.Server.Host/Controllers/DreamMakerController.cs @@ -133,26 +133,25 @@ namespace Tgstation.Server.Host.Controllers [HttpPut] [TgsAuthorize(DreamMakerRights.Compile)] [ProducesResponseType(typeof(Api.Models.Job), 202)] - public Task Create(CancellationToken cancellationToken) - => WithComponentInstance( - async instance => - { - var job = new Models.Job - { - Description = "Compile active repository code", - StartedBy = AuthenticationContext.User, - CancelRightsType = RightsType.DreamMaker, - CancelRight = (ulong)DreamMakerRights.CancelCompile, - Instance = Instance - }; + public async Task Create(CancellationToken cancellationToken) + { + var job = new Models.Job + { + Description = "Compile active repository code", + StartedBy = AuthenticationContext.User, + CancelRightsType = RightsType.DreamMaker, + CancelRight = (ulong)DreamMakerRights.CancelCompile, + Instance = Instance + }; - await jobManager.RegisterOperation( - job, - instance.DreamMaker.DeploymentProcess, - cancellationToken) - .ConfigureAwait(false); - return Accepted(job.ToApi()); - }); + await jobManager.RegisterOperation( + job, + (core, databaseContextFactory, paramJob, progressReporter, jobCancellationToken) + => core.DreamMaker.DeploymentProcess(paramJob, databaseContextFactory, progressReporter, jobCancellationToken), + cancellationToken) + .ConfigureAwait(false); + return Accepted(job.ToApi()); + } /// /// Update deployment settings. diff --git a/src/Tgstation.Server.Host/Controllers/InstanceController.cs b/src/Tgstation.Server.Host/Controllers/InstanceController.cs index afed0c82a1..19c98d6f2c 100644 --- a/src/Tgstation.Server.Host/Controllers/InstanceController.cs +++ b/src/Tgstation.Server.Host/Controllers/InstanceController.cs @@ -481,7 +481,7 @@ namespace Tgstation.Server.Host.Controllers if (renamed) { - var componentInstance = instanceManager.GetInstance(originalModel); + var componentInstance = instanceManager.GetInstanceReference(originalModel); if (componentInstance != null) await componentInstance.InstanceRenamed(originalModel.Name, cancellationToken).ConfigureAwait(false); } @@ -532,7 +532,8 @@ namespace Tgstation.Server.Host.Controllers await jobManager.RegisterOperation( job, - (paramJob, databaseContextFactory, progressHandler, ct) => instanceManager.MoveInstance(originalModel, originalModelPath, ct), + (core, databaseContextFactory, paramJob, progressHandler, ct) // core will be null here since the instance is offline + => instanceManager.MoveInstance(originalModel, originalModelPath, ct), cancellationToken) .ConfigureAwait(false); api.MoveJob = job.ToApi(); @@ -540,7 +541,7 @@ namespace Tgstation.Server.Host.Controllers if (model.AutoUpdateInterval.HasValue && oldAutoUpdateInterval != model.AutoUpdateInterval) { - var componentInstance = instanceManager.GetInstance(originalModel); + var componentInstance = instanceManager.GetInstanceReference(originalModel); if (componentInstance != null) await componentInstance.SetAutoUpdateInterval(model.AutoUpdateInterval.Value).ConfigureAwait(false); } diff --git a/src/Tgstation.Server.Host/Controllers/InstanceRequiredController.cs b/src/Tgstation.Server.Host/Controllers/InstanceRequiredController.cs index d247daf857..c3d0ab996d 100644 --- a/src/Tgstation.Server.Host/Controllers/InstanceRequiredController.cs +++ b/src/Tgstation.Server.Host/Controllers/InstanceRequiredController.cs @@ -1,5 +1,6 @@ using Microsoft.AspNetCore.Mvc; using Microsoft.Extensions.Logging; +using Serilog.Context; using System; using System.Threading; using System.Threading.Tasks; @@ -48,7 +49,7 @@ namespace Tgstation.Server.Host.Controllers if (ValidateInstanceOnlineStatus(instanceManager, Logger, Instance)) await DatabaseContext.Save(cancellationToken).ConfigureAwait(false); - if (instanceManager.GetInstance(Instance) == null) + if (instanceManager.GetInstanceReference(Instance) == null) return Conflict(new ErrorMessage(ErrorCode.InstanceOffline)); return null; } @@ -59,15 +60,18 @@ namespace Tgstation.Server.Host.Controllers /// A accepting the and returning a with the . /// A resulting in the that should be returned. /// The context of should be as small as possinle so as to avoid race conditions. - protected async Task WithComponentInstance(Func> action) + protected async Task WithComponentInstance(Func> action) { if (action == null) throw new ArgumentNullException(nameof(action)); - var componentInstance = instanceManager.GetInstance(Instance); - if (componentInstance == null) - return Conflict(new ErrorMessage(ErrorCode.InstanceOffline)); - return await action(componentInstance).ConfigureAwait(false); + using var instanceReference = instanceManager.GetInstanceReference(Instance); + using (LogContext.PushProperty("InstanceReference", instanceReference.Uid)) + { + if (instanceReference == null) + return Conflict(new ErrorMessage(ErrorCode.InstanceOffline)); + return await action(instanceReference).ConfigureAwait(false); + } } /// @@ -84,7 +88,7 @@ namespace Tgstation.Server.Host.Controllers if (metadata == null) throw new ArgumentNullException(nameof(metadata)); - var online = instanceManager.GetInstance(metadata) != null; + var online = instanceManager.GetInstanceReference(metadata) != null; if (metadata.Online.Value == online) return false; diff --git a/src/Tgstation.Server.Host/Controllers/RepositoryController.cs b/src/Tgstation.Server.Host/Controllers/RepositoryController.cs index 10aea1aac6..423818dc6e 100644 --- a/src/Tgstation.Server.Host/Controllers/RepositoryController.cs +++ b/src/Tgstation.Server.Host/Controllers/RepositoryController.cs @@ -211,8 +211,9 @@ namespace Tgstation.Server.Host.Controllers Instance = Instance }; var api = currentModel.ToApi(); - await jobManager.RegisterOperation(job, async (paramJob, databaseContextFactory, progressReporter, ct) => + await jobManager.RegisterOperation(job, async (core, databaseContextFactory, paramJob, progressReporter, ct) => { + var repoManager = core.RepositoryManager; using var repos = await repoManager.CloneRepository( new Uri(origin), cloneBranch, @@ -284,18 +285,13 @@ namespace Tgstation.Server.Host.Controllers Instance = Instance }; var api = currentModel.ToApi(); - return await WithComponentInstance( - async instance => - { - await jobManager.RegisterOperation( - job, - (paramJob, databaseContextFactory, progressReporter, ct) => instance.RepositoryManager.DeleteRepository(ct), - cancellationToken) - .ConfigureAwait(false); - api.ActiveJob = job.ToApi(); - return Accepted(api); - }) - .ConfigureAwait(false); + await jobManager.RegisterOperation( + job, + (core, databaseContextFactory, paramJob, progressReporter, ct) => core.RepositoryManager.DeleteRepository(ct), + cancellationToken) + .ConfigureAwait(false); + api.ActiveJob = job.ToApi(); + return Accepted(api); } /// @@ -363,8 +359,8 @@ namespace Tgstation.Server.Host.Controllers [ProducesResponseType(typeof(Repository), 200)] [ProducesResponseType(typeof(Repository), 202)] [ProducesResponseType(typeof(ErrorMessage), 410)] - #pragma warning disable CA1502, CA1505 // TODO: Decomplexify - public async Task Update([FromBody]Repository model, CancellationToken cancellationToken) +#pragma warning disable CA1502, CA1505 // TODO: Decomplexify + public async Task Update([FromBody] Repository model, CancellationToken cancellationToken) { if (model == null) throw new ArgumentNullException(nameof(model)); @@ -494,7 +490,7 @@ namespace Tgstation.Server.Host.Controllers return Json(api); // no git changes async Task UpdateCallbackThatDesperatelyNeedsRefactoring( - IInstance instance, + IInstanceCore instance, IDatabaseContextFactory databaseContextFactory, Action progressReporter, CancellationToken ct) @@ -897,13 +893,12 @@ namespace Tgstation.Server.Host.Controllers // Time to access git, do it in a job await jobManager.RegisterOperation( job, - (paramJob, databaseContextFactory, progressReporter, ct) - => WithComponentInstance( // Will never fail in the context of a job - instance => UpdateCallbackThatDesperatelyNeedsRefactoring( - instance, - databaseContextFactory, - progressReporter, - ct)), + (core, databaseContextFactory, paramJob, progressReporter, ct) => + UpdateCallbackThatDesperatelyNeedsRefactoring( + core, + databaseContextFactory, + progressReporter, + ct), cancellationToken) .ConfigureAwait(false); diff --git a/src/Tgstation.Server.Host/Core/Application.cs b/src/Tgstation.Server.Host/Core/Application.cs index cad2b6970f..fa29699de4 100644 --- a/src/Tgstation.Server.Host/Core/Application.cs +++ b/src/Tgstation.Server.Host/Core/Application.cs @@ -132,7 +132,7 @@ namespace Tgstation.Server.Host.Core var formatter = new MessageTemplateTextFormatter( "{Timestamp:o} " + ServiceCollectionExtensions.SerilogContextTemplate - + ": [{Level:u3}] {SourceContext:l}: {Message} ({EventId:x8}){NewLine}{Exception}", + + "|IR:{InstanceReference}){): [{Level:u3}] {SourceContext:l}: {Message} ({EventId:x8}){NewLine}{Exception}", null); logPath = IOManager.ConcatPath(logPath, "tgs-.log"); @@ -301,6 +301,7 @@ namespace Tgstation.Server.Host.Core services.AddSingleton(); services.AddSingleton(x => x.GetRequiredService()); + services.AddSingleton(x => new Lazy(() => x.GetRequiredService())); services.AddSingleton(x => x.GetRequiredService()); } diff --git a/src/Tgstation.Server.Host/Extensions/ServiceCollectionExtensions.cs b/src/Tgstation.Server.Host/Extensions/ServiceCollectionExtensions.cs index 8ff326c398..b6c6e9e5d1 100644 --- a/src/Tgstation.Server.Host/Extensions/ServiceCollectionExtensions.cs +++ b/src/Tgstation.Server.Host/Extensions/ServiceCollectionExtensions.cs @@ -18,7 +18,7 @@ namespace Tgstation.Server.Host.Extensions /// /// Common template used for adding our custom log context to serilog. /// - public const string SerilogContextTemplate = "(Instance:{Instance}|Job:{Job}|Request:{Request}|User:{User}|Monitor:{Monitor}|Bridge:{Bridge}|Chat:{ChatMessage})"; + public const string SerilogContextTemplate = "(Instance:{Instance}|Job:{Job}|Request:{Request}|User:{User}|Monitor:{Monitor}|Bridge:{Bridge}|Chat:{ChatMessage}"; /// /// Add a standard binding @@ -79,7 +79,7 @@ namespace Tgstation.Server.Host.Extensions sinkConfiguration.Console( outputTemplate: "[{Timestamp:HH:mm:ss}] {Level:w3}: {SourceContext:l} " + SerilogContextTemplate - + "{NewLine} {Message:lj}{NewLine}{Exception}"); + + "|IR:{InstanceReference}){NewLine} {Message:lj}{NewLine}{Exception}"); sinkConfigurationAction?.Invoke(sinkConfiguration); }); diff --git a/src/Tgstation.Server.Host/Jobs/IJobManager.cs b/src/Tgstation.Server.Host/Jobs/IJobManager.cs index a6d3610a9d..526ca4a437 100644 --- a/src/Tgstation.Server.Host/Jobs/IJobManager.cs +++ b/src/Tgstation.Server.Host/Jobs/IJobManager.cs @@ -1,8 +1,6 @@ using Microsoft.Extensions.Hosting; -using System; using System.Threading; using System.Threading.Tasks; -using Tgstation.Server.Host.Database; using Tgstation.Server.Host.Models; namespace Tgstation.Server.Host.Jobs @@ -23,10 +21,10 @@ namespace Tgstation.Server.Host.Jobs /// Registers a given and begins running it /// /// The - /// The operation to run taking the started , a , progress reporter and a + /// The for the . /// The for the operation /// A representing a running operation - Task RegisterOperation(Job job, Func, CancellationToken, Task> operation, CancellationToken cancellationToken); + Task RegisterOperation(Job job, JobEntrypoint operation, CancellationToken cancellationToken); /// /// Wait for a given to complete diff --git a/src/Tgstation.Server.Host/Jobs/JobEntrypoint.cs b/src/Tgstation.Server.Host/Jobs/JobEntrypoint.cs new file mode 100644 index 0000000000..0c1f9fc16f --- /dev/null +++ b/src/Tgstation.Server.Host/Jobs/JobEntrypoint.cs @@ -0,0 +1,25 @@ +using System; +using System.Threading; +using System.Threading.Tasks; +using Tgstation.Server.Host.Components; +using Tgstation.Server.Host.Database; +using Tgstation.Server.Host.Models; + +namespace Tgstation.Server.Host.Jobs +{ + /// + /// Entrypoint for running a given job. + /// + /// The the job is running on. only when performing an instance move operation. + /// The for the operation. + /// The running . + /// A that will update the progress of the job. + /// The for the operation. + /// A representing the running operation. + public delegate Task JobEntrypoint( + IInstanceCore instance, + IDatabaseContextFactory databaseContextFactory, + Job job, + Action progressReporter, + CancellationToken cancellationToken); +} diff --git a/src/Tgstation.Server.Host/Jobs/JobHandler.cs b/src/Tgstation.Server.Host/Jobs/JobHandler.cs index 072b075fe2..86437139d5 100644 --- a/src/Tgstation.Server.Host/Jobs/JobHandler.cs +++ b/src/Tgstation.Server.Host/Jobs/JobHandler.cs @@ -14,21 +14,24 @@ namespace Tgstation.Server.Host.Jobs /// readonly CancellationTokenSource cancellationTokenSource; + /// + /// A taking a and returning a that the will wrap + /// + readonly Func jobActivator; + /// /// The being run /// - readonly Task task; + Task task; /// /// Construct a /// - /// A taking a and returning a that the will wrap - public JobHandler(Func job) + /// The value of . + public JobHandler(Func jobActivator) { - if (job == null) - throw new ArgumentNullException(nameof(job)); + this.jobActivator = jobActivator ?? throw new ArgumentNullException(nameof(jobActivator)); cancellationTokenSource = new CancellationTokenSource(); - task = job(cancellationTokenSource.Token); } /// @@ -46,6 +49,9 @@ namespace Tgstation.Server.Host.Jobs /// A representing the running operation public async Task Wait(CancellationToken cancellationToken) { + if (task == null) + throw new InvalidOperationException("Job not started!"); + TaskCompletionSource tcs = new TaskCompletionSource(); using (cancellationToken.Register(() => tcs.SetCanceled())) await Task.WhenAny(tcs.Task, task).ConfigureAwait(false); @@ -56,5 +62,18 @@ namespace Tgstation.Server.Host.Jobs /// Cancels /// public void Cancel() => cancellationTokenSource.Cancel(); + + /// + /// Starts the job. + /// + public void Start() + { + lock (cancellationTokenSource) + { + if (task != null) + throw new InvalidOperationException("Job already started"); + task = jobActivator(cancellationTokenSource.Token); + } + } } } \ No newline at end of file diff --git a/src/Tgstation.Server.Host/Jobs/JobManager.cs b/src/Tgstation.Server.Host/Jobs/JobManager.cs index 113ec63cf0..6ec0fdc7c3 100644 --- a/src/Tgstation.Server.Host/Jobs/JobManager.cs +++ b/src/Tgstation.Server.Host/Jobs/JobManager.cs @@ -6,6 +6,7 @@ using System.Collections.Generic; using System.Linq; using System.Threading; using System.Threading.Tasks; +using Tgstation.Server.Host.Components; using Tgstation.Server.Host.Database; using Tgstation.Server.Host.Models; @@ -24,6 +25,11 @@ namespace Tgstation.Server.Host.Jobs /// readonly ILogger logger; + /// + /// The for the . + /// + readonly Lazy instanceCoreProvider; + /// /// of s to running s /// @@ -38,10 +44,12 @@ namespace Tgstation.Server.Host.Jobs /// Construct a /// /// The value of + /// The value of . /// The value of - public JobManager(IDatabaseContextFactory databaseContextFactory, ILogger logger) + public JobManager(IDatabaseContextFactory databaseContextFactory, Lazy instanceCoreProvider, ILogger logger) { this.databaseContextFactory = databaseContextFactory ?? throw new ArgumentNullException(nameof(databaseContextFactory)); + this.instanceCoreProvider = instanceCoreProvider ?? throw new ArgumentNullException(nameof(instanceCoreProvider)); this.logger = logger ?? throw new ArgumentNullException(nameof(logger)); jobs = new Dictionary(); synchronizationLock = new object(); @@ -73,10 +81,10 @@ namespace Tgstation.Server.Host.Jobs /// Runner for s /// /// The being run - /// The operation for the + /// The for the /// The for the operation /// A representing the running operation - async Task RunJob(Job job, Func operation, CancellationToken cancellationToken) + async Task RunJob(Job job, JobEntrypoint operation, CancellationToken cancellationToken) { using (LogContext.PushProperty("Job", job.Id)) try @@ -87,7 +95,20 @@ namespace Tgstation.Server.Host.Jobs var oldJob = job; job = new Job { Id = oldJob.Id }; - await operation(job, databaseContextFactory, cancellationToken).ConfigureAwait(false); + void UpdateProgress(int progress) + { + lock (synchronizationLock) + if (jobs.TryGetValue(oldJob.Id, out var handler)) + handler.Progress = progress; + } + + await operation( + instanceCoreProvider.Value.GetInstance(oldJob.Instance), + databaseContextFactory, + job, + UpdateProgress, + cancellationToken) + .ConfigureAwait(false); logger.LogDebug("Job {0} completed!", job.Id); } @@ -144,43 +165,50 @@ namespace Tgstation.Server.Host.Jobs } /// - public Task RegisterOperation(Job job, Func, CancellationToken, Task> operation, CancellationToken cancellationToken) => databaseContextFactory.UseContext(async databaseContext => - { - if (job == null) - throw new ArgumentNullException(nameof(job)); - if (operation == null) - throw new ArgumentNullException(nameof(operation)); + public Task RegisterOperation(Job job, JobEntrypoint operation, CancellationToken cancellationToken) + => databaseContextFactory.UseContext( + async databaseContext => + { + if (job == null) + throw new ArgumentNullException(nameof(job)); + if (operation == null) + throw new ArgumentNullException(nameof(operation)); - job.StartedAt = DateTimeOffset.Now; - job.Cancelled = false; + job.StartedAt = DateTimeOffset.Now; + job.Cancelled = false; - job.Instance = new Instance - { - Id = job.Instance.Id - }; - databaseContext.Instances.Attach(job.Instance); + job.Instance = new Models.Instance + { + Id = job.Instance.Id + }; + databaseContext.Instances.Attach(job.Instance); - job.StartedBy = new User - { - Id = job.StartedBy.Id - }; - databaseContext.Users.Attach(job.StartedBy); + job.StartedBy = new User + { + Id = job.StartedBy.Id + }; + databaseContext.Users.Attach(job.StartedBy); - databaseContext.Jobs.Add(job); + databaseContext.Jobs.Add(job); - await databaseContext.Save(cancellationToken).ConfigureAwait(false); - logger.LogDebug("Starting job {0}: {1}...", job.Id, job.Description); - var jobHandler = new JobHandler(x => RunJob(job, (jobParam, serviceProvider, ct) => - operation(jobParam, serviceProvider, y => - { - lock (synchronizationLock) - if (jobs.TryGetValue(job.Id, out var handler)) - handler.Progress = y; - }, ct), - x)); - lock (synchronizationLock) - jobs.Add(job.Id, jobHandler); - }); + await databaseContext.Save(cancellationToken).ConfigureAwait(false); + + logger.LogDebug("Starting job {0}: {1}...", job.Id, job.Description); + var jobHandler = new JobHandler(jobCancellationToken => RunJob(job, operation, jobCancellationToken)); + try + { + lock (synchronizationLock) + { + jobs.Add(job.Id, jobHandler); + jobHandler.Start(); + } + } + catch + { + jobHandler.Dispose(); + throw; + } + }); /// public async Task StartAsync(CancellationToken cancellationToken) From 2938c128ea721805c1335808763d1eb2e1ce37cd Mon Sep 17 00:00:00 2001 From: Jordan Brown Date: Mon, 13 Jul 2020 13:27:04 -0400 Subject: [PATCH 18/68] Fix false positive failure when creating TGS user --- src/Tgstation.Server.Host/Database/DatabaseSeeder.cs | 1 + 1 file changed, 1 insertion(+) diff --git a/src/Tgstation.Server.Host/Database/DatabaseSeeder.cs b/src/Tgstation.Server.Host/Database/DatabaseSeeder.cs index 020121cee3..a9857e673e 100644 --- a/src/Tgstation.Server.Host/Database/DatabaseSeeder.cs +++ b/src/Tgstation.Server.Host/Database/DatabaseSeeder.cs @@ -249,6 +249,7 @@ namespace Tgstation.Server.Host.Database .Users .AsQueryable() .Where(x => x.CanonicalName == User.CanonicalizeName(Api.Models.User.AdminName)) + .Include(x => x.CreatedBy) .FirstOrDefaultAsync(cancellationToken) .ConfigureAwait(false); if (admin == default) From 11c80d5a495c37cd59e9ae81893afea99985744a Mon Sep 17 00:00:00 2001 From: Jordan Brown Date: Mon, 13 Jul 2020 14:21:47 -0400 Subject: [PATCH 19/68] Nuget update --- src/Tgstation.Server.Host/Tgstation.Server.Host.csproj | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Tgstation.Server.Host/Tgstation.Server.Host.csproj b/src/Tgstation.Server.Host/Tgstation.Server.Host.csproj index 4ab66ef068..aeb3e1bebd 100644 --- a/src/Tgstation.Server.Host/Tgstation.Server.Host.csproj +++ b/src/Tgstation.Server.Host/Tgstation.Server.Host.csproj @@ -86,7 +86,7 @@ - + From d47498370bf54b434aeff317bc34fe7f944f01e8 Mon Sep 17 00:00:00 2001 From: Jordan Brown Date: Mon, 13 Jul 2020 14:23:36 -0400 Subject: [PATCH 20/68] Use the beta version of FxCop analyzers Fix for https://github.com/dotnet/roslyn-analyzers/issues/3850 --- src/Tgstation.Server.Host/Tgstation.Server.Host.csproj | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Tgstation.Server.Host/Tgstation.Server.Host.csproj b/src/Tgstation.Server.Host/Tgstation.Server.Host.csproj index aeb3e1bebd..59715fd0a2 100644 --- a/src/Tgstation.Server.Host/Tgstation.Server.Host.csproj +++ b/src/Tgstation.Server.Host/Tgstation.Server.Host.csproj @@ -50,7 +50,7 @@ - + all runtime; build; native; contentfiles; analyzers From 80cf6c415b4029c9070c5cd8000ab3b390d00590 Mon Sep 17 00:00:00 2001 From: Jordan Brown Date: Mon, 13 Jul 2020 14:37:15 -0400 Subject: [PATCH 21/68] Fix new FxCop issues --- .../Components/Watchdog/WatchdogBase.cs | 2 +- .../Components/Watchdog/WindowsWatchdog.cs | 3 ++- .../ControlPanelConfiguration.cs | 2 +- .../Controllers/RepositoryController.cs | 4 +++- src/Tgstation.Server.Host/Core/Application.cs | 1 + .../Database/DatabaseCollection.cs | 2 +- .../IO/DefaultIOManager.cs | 21 ++++++++++++------- src/Tgstation.Server.Host/Models/ChatBot.cs | 2 +- src/Tgstation.Server.Host/Models/Instance.cs | 8 +++---- .../Models/RevisionInformation.cs | 4 ++-- src/Tgstation.Server.Host/Models/TestMerge.cs | 2 +- src/Tgstation.Server.Host/Models/User.cs | 6 +++--- 12 files changed, 33 insertions(+), 24 deletions(-) diff --git a/src/Tgstation.Server.Host/Components/Watchdog/WatchdogBase.cs b/src/Tgstation.Server.Host/Components/Watchdog/WatchdogBase.cs index 5b061c5108..2f14e4c5ce 100644 --- a/src/Tgstation.Server.Host/Components/Watchdog/WatchdogBase.cs +++ b/src/Tgstation.Server.Host/Components/Watchdog/WatchdogBase.cs @@ -619,7 +619,7 @@ namespace Tgstation.Server.Host.Components.Watchdog var heartbeat = heartbeatSeconds == 0 || !controller.DMApiAvailable ? Extensions.TaskExtensions.InfiniteTask() - : Task.Delay(TimeSpan.FromSeconds(heartbeatSeconds)); + : Task.Delay(TimeSpan.FromSeconds(heartbeatSeconds), cancellationToken); // cancel waiting if requested var cancelTcs = new TaskCompletionSource(); diff --git a/src/Tgstation.Server.Host/Components/Watchdog/WindowsWatchdog.cs b/src/Tgstation.Server.Host/Components/Watchdog/WindowsWatchdog.cs index 7fa442369d..b719086eaa 100644 --- a/src/Tgstation.Server.Host/Components/Watchdog/WindowsWatchdog.cs +++ b/src/Tgstation.Server.Host/Components/Watchdog/WindowsWatchdog.cs @@ -219,7 +219,8 @@ namespace Tgstation.Server.Host.Components.Watchdog // Add another lock to the startup DMB because it'll be used throughout the lifetime of the watchdog startupDmbProvider = await DmbFactory.FromCompileJob(dmbToUse.CompileJob, cancellationToken).ConfigureAwait(false); - ActiveSwappable = pendingSwappable ?? new SwappableDmbProvider(dmbToUse, GameIOManager, symlinkFactory); + pendingSwappable ??= new SwappableDmbProvider(dmbToUse, GameIOManager, symlinkFactory); + ActiveSwappable = pendingSwappable; pendingSwappable = null; try diff --git a/src/Tgstation.Server.Host/Configuration/ControlPanelConfiguration.cs b/src/Tgstation.Server.Host/Configuration/ControlPanelConfiguration.cs index 719ccef9e0..13320674dc 100644 --- a/src/Tgstation.Server.Host/Configuration/ControlPanelConfiguration.cs +++ b/src/Tgstation.Server.Host/Configuration/ControlPanelConfiguration.cs @@ -25,6 +25,6 @@ namespace Tgstation.Server.Host.Configuration /// /// Origins allowed for CORS requests /// - public List AllowedOrigins { get; set; } + public ICollection AllowedOrigins { get; set; } } } diff --git a/src/Tgstation.Server.Host/Controllers/RepositoryController.cs b/src/Tgstation.Server.Host/Controllers/RepositoryController.cs index 423818dc6e..4f6d855128 100644 --- a/src/Tgstation.Server.Host/Controllers/RepositoryController.cs +++ b/src/Tgstation.Server.Host/Controllers/RepositoryController.cs @@ -566,7 +566,9 @@ namespace Tgstation.Server.Host.Controllers testMergeToAdd.MergedBy = mergedBy; - lastRevisionInfo.ActiveTestMerges.AddRange(previousRevInfo.ActiveTestMerges); + foreach (var activeTestMerge in previousRevInfo.ActiveTestMerges) + lastRevisionInfo.ActiveTestMerges.Add(activeTestMerge); + lastRevisionInfo.ActiveTestMerges.Add(new RevInfoTestMerge { TestMerge = testMergeToAdd diff --git a/src/Tgstation.Server.Host/Core/Application.cs b/src/Tgstation.Server.Host/Core/Application.cs index fa29699de4..5093e1083c 100644 --- a/src/Tgstation.Server.Host/Core/Application.cs +++ b/src/Tgstation.Server.Host/Core/Application.cs @@ -17,6 +17,7 @@ using Serilog.Formatting.Display; using System; using System.Globalization; using System.IdentityModel.Tokens.Jwt; +using System.Linq; using System.Threading.Tasks; using Tgstation.Server.Api; using Tgstation.Server.Api.Models; diff --git a/src/Tgstation.Server.Host/Database/DatabaseCollection.cs b/src/Tgstation.Server.Host/Database/DatabaseCollection.cs index e2ebf973e8..5821cd0ef2 100644 --- a/src/Tgstation.Server.Host/Database/DatabaseCollection.cs +++ b/src/Tgstation.Server.Host/Database/DatabaseCollection.cs @@ -47,7 +47,7 @@ namespace Tgstation.Server.Host.Database public void Attach(TModel model) => dbSet.Attach(model); /// - public IAsyncEnumerator GetAsyncEnumerator(CancellationToken cancellationToken = default) => dbSet.AsAsyncEnumerable().GetAsyncEnumerator(); + public IAsyncEnumerator GetAsyncEnumerator(CancellationToken cancellationToken = default) => dbSet.AsAsyncEnumerable().GetAsyncEnumerator(cancellationToken); /// public IEnumerator GetEnumerator() => dbSet.AsQueryable().GetEnumerator(); diff --git a/src/Tgstation.Server.Host/IO/DefaultIOManager.cs b/src/Tgstation.Server.Host/IO/DefaultIOManager.cs index ad7beb4c35..92421074ba 100644 --- a/src/Tgstation.Server.Host/IO/DefaultIOManager.cs +++ b/src/Tgstation.Server.Host/IO/DefaultIOManager.cs @@ -96,12 +96,17 @@ namespace Tgstation.Server.Host.IO var tasks = new List(); - await dir.EnumerateFiles().ToAsyncEnumerable().ForEachAsync(fileInfo => - { - if (ignore != null && ignore.Contains(fileInfo.Name)) - return; - tasks.Add(CopyFile(fileInfo.FullName, Path.Combine(dest, fileInfo.Name), cancellationToken)); - }).ConfigureAwait(false); + await dir.EnumerateFiles() + .ToAsyncEnumerable() + .ForEachAsync( + fileInfo => + { + if (ignore != null && ignore.Contains(fileInfo.Name)) + return; + tasks.Add(CopyFile(fileInfo.FullName, Path.Combine(dest, fileInfo.Name), cancellationToken)); + }, + cancellationToken) + .ConfigureAwait(false); await Task.WhenAll(tasks).ConfigureAwait(false); } @@ -225,7 +230,7 @@ namespace Tgstation.Server.Host.IO using var file = new FileStream(path, FileMode.Open, FileAccess.Read, FileShare.ReadWrite | FileShare.Delete, DefaultBufferSize, true); byte[] buf; buf = new byte[file.Length]; - await file.ReadAsync(buf, 0, (int)file.Length, cancellationToken).ConfigureAwait(false); + await file.ReadAsync(buf, cancellationToken).ConfigureAwait(false); return buf; } @@ -240,7 +245,7 @@ namespace Tgstation.Server.Host.IO { path = ResolvePath(path); using var file = OpenWriteStream(path); - await file.WriteAsync(contents, 0, contents.Length, cancellationToken).ConfigureAwait(false); + await file.WriteAsync(contents, cancellationToken).ConfigureAwait(false); } /// diff --git a/src/Tgstation.Server.Host/Models/ChatBot.cs b/src/Tgstation.Server.Host/Models/ChatBot.cs index b34876bb40..e3565be4e8 100644 --- a/src/Tgstation.Server.Host/Models/ChatBot.cs +++ b/src/Tgstation.Server.Host/Models/ChatBot.cs @@ -26,7 +26,7 @@ namespace Tgstation.Server.Host.Models /// /// See /// - public List Channels { get; set; } + public ICollection Channels { get; set; } /// /// Convert the to it's API form diff --git a/src/Tgstation.Server.Host/Models/Instance.cs b/src/Tgstation.Server.Host/Models/Instance.cs index 6a87d51bca..1827a9511d 100644 --- a/src/Tgstation.Server.Host/Models/Instance.cs +++ b/src/Tgstation.Server.Host/Models/Instance.cs @@ -30,22 +30,22 @@ namespace Tgstation.Server.Host.Models /// /// The s in the /// - public List InstanceUsers { get; set; } + public ICollection InstanceUsers { get; set; } /// /// The s for the /// - public List ChatSettings { get; set; } + public ICollection ChatSettings { get; set; } /// /// The s in the /// - public List RevisionInformations { get; set; } + public ICollection RevisionInformations { get; set; } /// /// The s in the /// - public List Jobs { get; set; } + public ICollection Jobs { get; set; } /// /// Convert the to it's API form diff --git a/src/Tgstation.Server.Host/Models/RevisionInformation.cs b/src/Tgstation.Server.Host/Models/RevisionInformation.cs index f1d3a2c970..6c28d3864a 100644 --- a/src/Tgstation.Server.Host/Models/RevisionInformation.cs +++ b/src/Tgstation.Server.Host/Models/RevisionInformation.cs @@ -31,12 +31,12 @@ namespace Tgstation.Server.Host.Models /// /// See /// - public List ActiveTestMerges { get; set; } + public ICollection ActiveTestMerges { get; set; } /// /// See s made from this /// - public List CompileJobs { get; set; } + public ICollection CompileJobs { get; set; } /// /// Convert the to it's API form diff --git a/src/Tgstation.Server.Host/Models/TestMerge.cs b/src/Tgstation.Server.Host/Models/TestMerge.cs index 754951c355..e44b557cb4 100644 --- a/src/Tgstation.Server.Host/Models/TestMerge.cs +++ b/src/Tgstation.Server.Host/Models/TestMerge.cs @@ -26,7 +26,7 @@ namespace Tgstation.Server.Host.Models /// /// All the for the /// - public List RevisonInformations { get; set; } + public ICollection RevisonInformations { get; set; } /// /// Convert the to it's API form diff --git a/src/Tgstation.Server.Host/Models/User.cs b/src/Tgstation.Server.Host/Models/User.cs index 57912ce2ee..ed1ec273aa 100644 --- a/src/Tgstation.Server.Host/Models/User.cs +++ b/src/Tgstation.Server.Host/Models/User.cs @@ -36,17 +36,17 @@ namespace Tgstation.Server.Host.Models /// /// s created by this /// - public List CreatedUsers { get; set; } + public ICollection CreatedUsers { get; set; } /// /// The s for the /// - public List InstanceUsers { get; set; } + public ICollection InstanceUsers { get; set; } /// /// The s made by the /// - public List TestMerges { get; set; } + public ICollection TestMerges { get; set; } /// /// Change a into a . From bf2989fc1e2a784286a495ee512b33cdb903bec7 Mon Sep 17 00:00:00 2001 From: Jordan Brown Date: Mon, 13 Jul 2020 15:04:33 -0400 Subject: [PATCH 22/68] Fix JobHandler tests --- .../Jobs/TestJobHandler.cs | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/tests/Tgstation.Server.Host.Tests/Jobs/TestJobHandler.cs b/tests/Tgstation.Server.Host.Tests/Jobs/TestJobHandler.cs index 3326de98fd..a4143ca8cc 100644 --- a/tests/Tgstation.Server.Host.Tests/Jobs/TestJobHandler.cs +++ b/tests/Tgstation.Server.Host.Tests/Jobs/TestJobHandler.cs @@ -38,12 +38,12 @@ namespace Tgstation.Server.Host.Jobs.Tests var tcs = new TaskCompletionSource(); currentWaitTask = tcs.Task; cts.Cancel(); - using(var handler = new JobHandler(TestJob)) - { - await Assert.ThrowsExceptionAsync(() => handler.Wait(cts.Token)).ConfigureAwait(false); - tcs.SetResult(null); - await handler.Wait(default).ConfigureAwait(false); - } + using var handler = new JobHandler(TestJob); + await Assert.ThrowsExceptionAsync(() => handler.Wait(cts.Token)).ConfigureAwait(false); + handler.Start(); + await Assert.ThrowsExceptionAsync(() => handler.Wait(cts.Token)).ConfigureAwait(false); + tcs.SetResult(null); + await handler.Wait(default).ConfigureAwait(false); } Assert.IsFalse(cancelled); } @@ -70,6 +70,7 @@ namespace Tgstation.Server.Host.Jobs.Tests cancelled = false; using(var handler = new JobHandler(TestJob)) { + handler.Start(); handler.Cancel(); tcs.SetResult(null); await handler.Wait(default).ConfigureAwait(false); From d961186d1bab5bf3815d569d1813213bbb776a15 Mon Sep 17 00:00:00 2001 From: Jordan Brown Date: Mon, 13 Jul 2020 19:33:52 -0400 Subject: [PATCH 23/68] Discards aren't kept around for using statements Workaround for https://github.com/dotnet/csharplang/issues/2235 --- src/Tgstation.Server.Host/Components/InstanceManager.cs | 4 ++-- src/Tgstation.Server.Host/Components/Watchdog/WatchdogBase.cs | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/Tgstation.Server.Host/Components/InstanceManager.cs b/src/Tgstation.Server.Host/Components/InstanceManager.cs index ce3d3f775f..e70317ea68 100644 --- a/src/Tgstation.Server.Host/Components/InstanceManager.cs +++ b/src/Tgstation.Server.Host/Components/InstanceManager.cs @@ -288,7 +288,7 @@ namespace Tgstation.Server.Host.Components if (metadata == null) throw new ArgumentNullException(nameof(metadata)); - using var _ = await SemaphoreSlimContext.Lock(instanceStateChangeSemaphore, cancellationToken).ConfigureAwait(false); + using var lockContext = await SemaphoreSlimContext.Lock(instanceStateChangeSemaphore, cancellationToken).ConfigureAwait(false); logger.LogInformation("Offlining instance ID {0}", metadata.Id); InstanceContainer container; @@ -342,7 +342,7 @@ namespace Tgstation.Server.Host.Components if (metadata == null) throw new ArgumentNullException(nameof(metadata)); - using var _ = await SemaphoreSlimContext.Lock(instanceStateChangeSemaphore, cancellationToken).ConfigureAwait(false); + using var lockContext = await SemaphoreSlimContext.Lock(instanceStateChangeSemaphore, cancellationToken).ConfigureAwait(false); lock (instances) if (instances.ContainsKey(metadata.Id)) { diff --git a/src/Tgstation.Server.Host/Components/Watchdog/WatchdogBase.cs b/src/Tgstation.Server.Host/Components/Watchdog/WatchdogBase.cs index 2f14e4c5ce..2fca625767 100644 --- a/src/Tgstation.Server.Host/Components/Watchdog/WatchdogBase.cs +++ b/src/Tgstation.Server.Host/Components/Watchdog/WatchdogBase.cs @@ -595,7 +595,7 @@ namespace Tgstation.Server.Host.Components.Watchdog { Logger.LogTrace("Entered MonitorLifetimes"); Status = WatchdogStatus.Online; - using var _ = cancellationToken.Register(() => Logger.LogTrace("Monitor cancellationToken triggered")); + using var cancellationTokenLoggingRegistration = cancellationToken.Register(() => Logger.LogTrace("Monitor cancellationToken triggered")); // this function is responsible for calling HandlerMonitorWakeup when necessary and manitaining the MonitorState try From 46aee332618998df1ca11e7b80e5313af4e4a5a3 Mon Sep 17 00:00:00 2001 From: Jordan Brown Date: Mon, 13 Jul 2020 21:34:48 -0400 Subject: [PATCH 24/68] Fix IInstanceReferences not being disposed Also remove unused usings --- src/Tgstation.Server.Host/Components/InstanceManager.cs | 3 ++- .../Controllers/InstanceController.cs | 4 ++-- .../Controllers/InstanceRequiredController.cs | 7 +++++-- tests/Tgstation.Server.Tests/InstanceManagerTest.cs | 3 --- 4 files changed, 9 insertions(+), 8 deletions(-) diff --git a/src/Tgstation.Server.Host/Components/InstanceManager.cs b/src/Tgstation.Server.Host/Components/InstanceManager.cs index e70317ea68..9a6ecd8b62 100644 --- a/src/Tgstation.Server.Host/Components/InstanceManager.cs +++ b/src/Tgstation.Server.Host/Components/InstanceManager.cs @@ -222,7 +222,8 @@ namespace Tgstation.Server.Host.Components { if (oldPath == null) throw new ArgumentNullException(nameof(oldPath)); - if (GetInstanceReference(instance) != null) + using var instanceReferenceCheck = GetInstanceReference(instance); + if (instanceReferenceCheck != null) throw new InvalidOperationException("Cannot move an online instance!"); var newPath = instance.Path; try diff --git a/src/Tgstation.Server.Host/Controllers/InstanceController.cs b/src/Tgstation.Server.Host/Controllers/InstanceController.cs index 19c98d6f2c..13a29097d5 100644 --- a/src/Tgstation.Server.Host/Controllers/InstanceController.cs +++ b/src/Tgstation.Server.Host/Controllers/InstanceController.cs @@ -481,7 +481,7 @@ namespace Tgstation.Server.Host.Controllers if (renamed) { - var componentInstance = instanceManager.GetInstanceReference(originalModel); + using var componentInstance = instanceManager.GetInstanceReference(originalModel); if (componentInstance != null) await componentInstance.InstanceRenamed(originalModel.Name, cancellationToken).ConfigureAwait(false); } @@ -541,7 +541,7 @@ namespace Tgstation.Server.Host.Controllers if (model.AutoUpdateInterval.HasValue && oldAutoUpdateInterval != model.AutoUpdateInterval) { - var componentInstance = instanceManager.GetInstanceReference(originalModel); + using var componentInstance = instanceManager.GetInstanceReference(originalModel); if (componentInstance != null) await componentInstance.SetAutoUpdateInterval(model.AutoUpdateInterval.Value).ConfigureAwait(false); } diff --git a/src/Tgstation.Server.Host/Controllers/InstanceRequiredController.cs b/src/Tgstation.Server.Host/Controllers/InstanceRequiredController.cs index c3d0ab996d..faba875b7d 100644 --- a/src/Tgstation.Server.Host/Controllers/InstanceRequiredController.cs +++ b/src/Tgstation.Server.Host/Controllers/InstanceRequiredController.cs @@ -49,7 +49,8 @@ namespace Tgstation.Server.Host.Controllers if (ValidateInstanceOnlineStatus(instanceManager, Logger, Instance)) await DatabaseContext.Save(cancellationToken).ConfigureAwait(false); - if (instanceManager.GetInstanceReference(Instance) == null) + using var instanceReferenceCheck = instanceManager.GetInstanceReference(Instance); + if (instanceReferenceCheck == null) return Conflict(new ErrorMessage(ErrorCode.InstanceOffline)); return null; } @@ -88,7 +89,9 @@ namespace Tgstation.Server.Host.Controllers if (metadata == null) throw new ArgumentNullException(nameof(metadata)); - var online = instanceManager.GetInstanceReference(metadata) != null; + bool online; + using (var instanceReferenceCheck = instanceManager.GetInstanceReference(metadata)) + online = instanceReferenceCheck != null; if (metadata.Online.Value == online) return false; diff --git a/tests/Tgstation.Server.Tests/InstanceManagerTest.cs b/tests/Tgstation.Server.Tests/InstanceManagerTest.cs index 8333226b8e..fbab7dec20 100644 --- a/tests/Tgstation.Server.Tests/InstanceManagerTest.cs +++ b/tests/Tgstation.Server.Tests/InstanceManagerTest.cs @@ -2,15 +2,12 @@ using System; using System.IO; using System.Linq; -using System.Reflection; using System.Threading; using System.Threading.Tasks; using Tgstation.Server.Api.Models; using Tgstation.Server.Api.Rights; using Tgstation.Server.Client; -using Tgstation.Server.Client.Components; using Tgstation.Server.Host.Controllers; -using Tgstation.Server.Tests.Instance; namespace Tgstation.Server.Tests { From fece7567fb09fa4c4db50b5d3b044f54f883675a Mon Sep 17 00:00:00 2001 From: Jordan Brown Date: Tue, 14 Jul 2020 09:50:46 -0400 Subject: [PATCH 25/68] Disallow address reuse for port bind tests --- .../Components/InstanceManager.cs | 6 ++--- .../Session/SessionControllerFactory.cs | 6 ++--- .../Extensions/SocketExtensions.cs | 23 +++++++++++++++++++ .../Instance/JobsRequiredTest.cs | 3 ++- .../Instance/WatchdogTest.cs | 2 ++ .../Tgstation.Server.Tests/IntegrationTest.cs | 2 ++ 6 files changed, 33 insertions(+), 9 deletions(-) create mode 100644 src/Tgstation.Server.Host/Extensions/SocketExtensions.cs diff --git a/src/Tgstation.Server.Host/Components/InstanceManager.cs b/src/Tgstation.Server.Host/Components/InstanceManager.cs index 9a6ecd8b62..085dc79c32 100644 --- a/src/Tgstation.Server.Host/Components/InstanceManager.cs +++ b/src/Tgstation.Server.Host/Components/InstanceManager.cs @@ -5,8 +5,6 @@ using Microsoft.Extensions.Options; using System; using System.Collections.Generic; using System.Linq; -using System.Net; -using System.Net.Sockets; using System.Threading; using System.Threading.Tasks; using Tgstation.Server.Api.Models; @@ -16,6 +14,7 @@ using Tgstation.Server.Host.Configuration; using Tgstation.Server.Host.Controllers; using Tgstation.Server.Host.Core; using Tgstation.Server.Host.Database; +using Tgstation.Server.Host.Extensions; using Tgstation.Server.Host.IO; using Tgstation.Server.Host.Jobs; using Tgstation.Server.Host.Security; @@ -482,8 +481,7 @@ namespace Tgstation.Server.Host.Components // This runs before the real socket is opened, ensures we don't perform reattaches unless we're fairly certain the bind won't fail // If it does fail, DD will be killed. - using var hostingSocket = new Socket(SocketType.Stream, ProtocolType.Tcp); - hostingSocket.Bind(new IPEndPoint(IPAddress.Any, serverPortProvider.HttpApiPort)); + SocketExtensions.BindTest(serverPortProvider.HttpApiPort); } /// diff --git a/src/Tgstation.Server.Host/Components/Session/SessionControllerFactory.cs b/src/Tgstation.Server.Host/Components/Session/SessionControllerFactory.cs index 3b859791ab..84c2ca9667 100644 --- a/src/Tgstation.Server.Host/Components/Session/SessionControllerFactory.cs +++ b/src/Tgstation.Server.Host/Components/Session/SessionControllerFactory.cs @@ -2,7 +2,6 @@ using System; using System.Globalization; using System.Linq; -using System.Net; using System.Net.Sockets; using System.Text; using System.Threading; @@ -16,6 +15,7 @@ using Tgstation.Server.Host.Components.Deployment; using Tgstation.Server.Host.Components.Interop; using Tgstation.Server.Host.Components.Interop.Bridge; 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; @@ -118,11 +118,9 @@ namespace Tgstation.Server.Host.Components.Session /// The port number to test. static void PortBindTest(ushort port) { - using var socket = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp); - try { - socket.Bind(new IPEndPoint(IPAddress.Any, port)); + SocketExtensions.BindTest(port); } catch (SocketException ex) { diff --git a/src/Tgstation.Server.Host/Extensions/SocketExtensions.cs b/src/Tgstation.Server.Host/Extensions/SocketExtensions.cs new file mode 100644 index 0000000000..6d4ae76323 --- /dev/null +++ b/src/Tgstation.Server.Host/Extensions/SocketExtensions.cs @@ -0,0 +1,23 @@ +using System.Net; +using System.Net.Sockets; + +namespace Tgstation.Server.Host.Extensions +{ + /// + /// Extension methods for the . + /// + static class SocketExtensions + { + /// + /// Attempt to exclusively bind to a given . + /// + /// The port number to bind to. + public static void BindTest(ushort port) + { + using var socket = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp); + socket.SetSocketOption(SocketOptionLevel.Socket, SocketOptionName.ExclusiveAddressUse, true); + socket.SetSocketOption(SocketOptionLevel.Socket, SocketOptionName.ReuseAddress, false); + socket.Bind(new IPEndPoint(IPAddress.Any, port)); + } + } +} diff --git a/tests/Tgstation.Server.Tests/Instance/JobsRequiredTest.cs b/tests/Tgstation.Server.Tests/Instance/JobsRequiredTest.cs index 258e69de57..6594f345e2 100644 --- a/tests/Tgstation.Server.Tests/Instance/JobsRequiredTest.cs +++ b/tests/Tgstation.Server.Tests/Instance/JobsRequiredTest.cs @@ -36,7 +36,8 @@ namespace Tgstation.Server.Tests.Instance } if (expectFailure ^ job.ExceptionDetails != null) - Assert.Fail(job.ExceptionDetails ?? $"Expected job \"{job.Id}\" \"{job.Description}\" to fail but it didn't"); + Assert.Fail(job.ExceptionDetails + ?? $"Expected job \"{job.Id}\" \"{job.Description}\" to fail {(expectedCode.HasValue ? $"with ErrorCode \"{expectedCode.Value}\" " : String.Empty)}but it didn't"); if (expectedCode.HasValue) Assert.AreEqual(expectedCode.Value, job.ErrorCode, job.ExceptionDetails); diff --git a/tests/Tgstation.Server.Tests/Instance/WatchdogTest.cs b/tests/Tgstation.Server.Tests/Instance/WatchdogTest.cs index d8e666560f..532c18d704 100644 --- a/tests/Tgstation.Server.Tests/Instance/WatchdogTest.cs +++ b/tests/Tgstation.Server.Tests/Instance/WatchdogTest.cs @@ -141,6 +141,8 @@ namespace Tgstation.Server.Tests.Instance Job startJob; using (var blockSocket = new Socket(SocketType.Stream, ProtocolType.Tcp)) { + blockSocket.SetSocketOption(SocketOptionLevel.Socket, SocketOptionName.ExclusiveAddressUse, true); + blockSocket.SetSocketOption(SocketOptionLevel.Socket, SocketOptionName.ReuseAddress, false); blockSocket.Bind(new IPEndPoint(IPAddress.Any, 1337)); startJob = await instanceClient.DreamDaemon.Start(cancellationToken).ConfigureAwait(false); diff --git a/tests/Tgstation.Server.Tests/IntegrationTest.cs b/tests/Tgstation.Server.Tests/IntegrationTest.cs index b842dd6345..3f02965eca 100644 --- a/tests/Tgstation.Server.Tests/IntegrationTest.cs +++ b/tests/Tgstation.Server.Tests/IntegrationTest.cs @@ -200,6 +200,8 @@ namespace Tgstation.Server.Tests // http bind test https://github.com/tgstation/tgstation-server/issues/1065 using (var blockingSocket = new Socket(SocketType.Stream, ProtocolType.Tcp)) { + blockingSocket.SetSocketOption(SocketOptionLevel.Socket, SocketOptionName.ExclusiveAddressUse, true); + blockingSocket.SetSocketOption(SocketOptionLevel.Socket, SocketOptionName.ReuseAddress, false); blockingSocket.Bind(new IPEndPoint(IPAddress.Any, server.Url.Port)); try { From e01aedee03e289c8ff9e67dce6f14215d494fdb6 Mon Sep 17 00:00:00 2001 From: Jordan Brown Date: Tue, 14 Jul 2020 10:49:03 -0400 Subject: [PATCH 26/68] Minor code improvement --- .../Components/Session/SessionControllerFactory.cs | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/src/Tgstation.Server.Host/Components/Session/SessionControllerFactory.cs b/src/Tgstation.Server.Host/Components/Session/SessionControllerFactory.cs index 84c2ca9667..04e00f5536 100644 --- a/src/Tgstation.Server.Host/Components/Session/SessionControllerFactory.cs +++ b/src/Tgstation.Server.Host/Components/Session/SessionControllerFactory.cs @@ -122,10 +122,8 @@ namespace Tgstation.Server.Host.Components.Session { SocketExtensions.BindTest(port); } - catch (SocketException ex) + catch (SocketException ex) when (ex.SocketErrorCode == SocketError.AddressAlreadyInUse) { - if(ex.SocketErrorCode != SocketError.AddressAlreadyInUse) - throw; throw new JobException(ErrorCode.DreamDaemonPortInUse, ex); } } From 415c15dfd322b4cf7529bf796f8b8f47f6a5ec82 Mon Sep 17 00:00:00 2001 From: Jordan Brown Date: Tue, 14 Jul 2020 10:54:30 -0400 Subject: [PATCH 27/68] Make sure to test IPv6 --- .../Components/InstanceManager.cs | 2 +- .../Components/Session/SessionControllerFactory.cs | 2 +- .../Extensions/SocketExtensions.cs | 13 +++++++++++-- 3 files changed, 13 insertions(+), 4 deletions(-) diff --git a/src/Tgstation.Server.Host/Components/InstanceManager.cs b/src/Tgstation.Server.Host/Components/InstanceManager.cs index 085dc79c32..0d003e0a91 100644 --- a/src/Tgstation.Server.Host/Components/InstanceManager.cs +++ b/src/Tgstation.Server.Host/Components/InstanceManager.cs @@ -481,7 +481,7 @@ namespace Tgstation.Server.Host.Components // This runs before the real socket is opened, ensures we don't perform reattaches unless we're fairly certain the bind won't fail // If it does fail, DD will be killed. - SocketExtensions.BindTest(serverPortProvider.HttpApiPort); + SocketExtensions.BindTest(serverPortProvider.HttpApiPort, true); } /// diff --git a/src/Tgstation.Server.Host/Components/Session/SessionControllerFactory.cs b/src/Tgstation.Server.Host/Components/Session/SessionControllerFactory.cs index 04e00f5536..e211b982d2 100644 --- a/src/Tgstation.Server.Host/Components/Session/SessionControllerFactory.cs +++ b/src/Tgstation.Server.Host/Components/Session/SessionControllerFactory.cs @@ -120,7 +120,7 @@ namespace Tgstation.Server.Host.Components.Session { try { - SocketExtensions.BindTest(port); + SocketExtensions.BindTest(port, false); } catch (SocketException ex) when (ex.SocketErrorCode == SocketError.AddressAlreadyInUse) { diff --git a/src/Tgstation.Server.Host/Extensions/SocketExtensions.cs b/src/Tgstation.Server.Host/Extensions/SocketExtensions.cs index 6d4ae76323..b5f28c6a7f 100644 --- a/src/Tgstation.Server.Host/Extensions/SocketExtensions.cs +++ b/src/Tgstation.Server.Host/Extensions/SocketExtensions.cs @@ -12,12 +12,21 @@ namespace Tgstation.Server.Host.Extensions /// Attempt to exclusively bind to a given . /// /// The port number to bind to. - public static void BindTest(ushort port) + /// If IPV6 should be tested as well. + public static void BindTest(ushort port, bool includeIPv6) { using var socket = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp); socket.SetSocketOption(SocketOptionLevel.Socket, SocketOptionName.ExclusiveAddressUse, true); socket.SetSocketOption(SocketOptionLevel.Socket, SocketOptionName.ReuseAddress, false); - socket.Bind(new IPEndPoint(IPAddress.Any, port)); + if (includeIPv6) + socket.DualMode = true; + + socket.Bind( + new IPEndPoint( + includeIPv6 + ? IPAddress.IPv6Any + : IPAddress.Any, + port)); } } } From 4c33f212e5a3efa1d27c2fb87221bac5529b9b1e Mon Sep 17 00:00:00 2001 From: Jordan Brown Date: Tue, 14 Jul 2020 10:54:41 -0400 Subject: [PATCH 28/68] Give up on the test for linux --- .../Instance/WatchdogTest.cs | 17 +++++++++-------- 1 file changed, 9 insertions(+), 8 deletions(-) diff --git a/tests/Tgstation.Server.Tests/Instance/WatchdogTest.cs b/tests/Tgstation.Server.Tests/Instance/WatchdogTest.cs index 532c18d704..f7fce8c6e3 100644 --- a/tests/Tgstation.Server.Tests/Instance/WatchdogTest.cs +++ b/tests/Tgstation.Server.Tests/Instance/WatchdogTest.cs @@ -139,15 +139,16 @@ namespace Tgstation.Server.Tests.Instance Assert.AreEqual(DreamDaemonSecurity.Safe, daemonStatus.ActiveCompileJob.MinimumSecurityLevel); Job startJob; - using (var blockSocket = new Socket(SocketType.Stream, ProtocolType.Tcp)) - { - blockSocket.SetSocketOption(SocketOptionLevel.Socket, SocketOptionName.ExclusiveAddressUse, true); - blockSocket.SetSocketOption(SocketOptionLevel.Socket, SocketOptionName.ReuseAddress, false); - blockSocket.Bind(new IPEndPoint(IPAddress.Any, 1337)); - startJob = await instanceClient.DreamDaemon.Start(cancellationToken).ConfigureAwait(false); + if (new PlatformIdentifier().IsWindows) // Can't get address reuse to trigger on linux for some reason + using (var blockSocket = new Socket(SocketType.Stream, ProtocolType.Tcp)) + { + blockSocket.SetSocketOption(SocketOptionLevel.Socket, SocketOptionName.ExclusiveAddressUse, true); + blockSocket.SetSocketOption(SocketOptionLevel.Socket, SocketOptionName.ReuseAddress, false); + blockSocket.Bind(new IPEndPoint(IPAddress.Any, 1337)); + startJob = await instanceClient.DreamDaemon.Start(cancellationToken).ConfigureAwait(false); - await WaitForJob(startJob, 10, true, ErrorCode.DreamDaemonPortInUse, cancellationToken); - } + await WaitForJob(startJob, 10, true, ErrorCode.DreamDaemonPortInUse, cancellationToken); + } startJob = await instanceClient.DreamDaemon.Start(cancellationToken).ConfigureAwait(false); From edbbe16f110f3146b5f7a6db19dbdb5f23b53011 Mon Sep 17 00:00:00 2001 From: Jordan Brown Date: Tue, 14 Jul 2020 10:59:07 -0400 Subject: [PATCH 29/68] Run Release unit tests first as they take longer --- .travis.yml | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/.travis.yml b/.travis.yml index 0c495acc39..69fa734962 100644 --- a/.travis.yml +++ b/.travis.yml @@ -82,8 +82,8 @@ jobs: - DoxGeneration=false - DockerBuild=false - DMAPI=false - - CONFIG=Debug - name: "Debug Unit Tests" + - CONFIG=Release + name: "Release Unit Tests" language: csharp mono: none dotnet: 3.1 @@ -94,8 +94,8 @@ jobs: - DoxGeneration=false - DockerBuild=false - DMAPI=false - - CONFIG=Release - name: "Release Unit Tests" + - CONFIG=Debug + name: "Debug Unit Tests" language: csharp mono: none dotnet: 3.1 From 4f2b32b7577f9baaf770500d1c50c7b320e4c11c Mon Sep 17 00:00:00 2001 From: Jordan Brown Date: Tue, 14 Jul 2020 11:43:38 -0400 Subject: [PATCH 30/68] Fix IPV6 issue with BindTest --- src/Tgstation.Server.Host/Extensions/SocketExtensions.cs | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/src/Tgstation.Server.Host/Extensions/SocketExtensions.cs b/src/Tgstation.Server.Host/Extensions/SocketExtensions.cs index b5f28c6a7f..83eec75d21 100644 --- a/src/Tgstation.Server.Host/Extensions/SocketExtensions.cs +++ b/src/Tgstation.Server.Host/Extensions/SocketExtensions.cs @@ -15,7 +15,12 @@ namespace Tgstation.Server.Host.Extensions /// If IPV6 should be tested as well. public static void BindTest(ushort port, bool includeIPv6) { - using var socket = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp); + using var socket = new Socket( + includeIPv6 + ? AddressFamily.InterNetworkV6 + : AddressFamily.InterNetwork, + SocketType.Stream, + ProtocolType.Tcp); socket.SetSocketOption(SocketOptionLevel.Socket, SocketOptionName.ExclusiveAddressUse, true); socket.SetSocketOption(SocketOptionLevel.Socket, SocketOptionName.ReuseAddress, false); if (includeIPv6) From 561f9216bad6e561c846f2f791d9ab973a2090bb Mon Sep 17 00:00:00 2001 From: Jordan Brown Date: Tue, 14 Jul 2020 12:50:13 -0400 Subject: [PATCH 31/68] Add down migration tests --- .../20200511012117_SLAddHeartbeat.cs | 43 +++++++++++++-- .../20200616180742_SLTopicTimeout.cs | 39 ++++++++++++- .../20200705163700_SLAllowNullDMApi.cs | 34 +++++++++++- .../Tgstation.Server.Tests/IntegrationTest.cs | 55 +++++++++++++++++++ 4 files changed, 161 insertions(+), 10 deletions(-) diff --git a/src/Tgstation.Server.Host/Database/Migrations/20200511012117_SLAddHeartbeat.cs b/src/Tgstation.Server.Host/Database/Migrations/20200511012117_SLAddHeartbeat.cs index 6e4a97ffef..77fc1f8ad4 100644 --- a/src/Tgstation.Server.Host/Database/Migrations/20200511012117_SLAddHeartbeat.cs +++ b/src/Tgstation.Server.Host/Database/Migrations/20200511012117_SLAddHeartbeat.cs @@ -18,7 +18,7 @@ namespace Tgstation.Server.Host.Database.Migrations name: "HeartbeatSeconds", table: "DreamDaemonSettings", nullable: false, - defaultValue: 0u); + defaultValue: 0U); } /// @@ -27,9 +27,44 @@ namespace Tgstation.Server.Host.Database.Migrations if (migrationBuilder == null) throw new ArgumentNullException(nameof(migrationBuilder)); - migrationBuilder.DropColumn( - name: "HeartbeatSeconds", - table: "DreamDaemonSettings"); + migrationBuilder.RenameTable( + name: "DreamDaemonSettings", + newName: "DreamDaemonSettings_down"); + + migrationBuilder.CreateTable( + name: "DreamDaemonSettings", + columns: table => new + { + Id = table.Column(nullable: false) + .Annotation("Sqlite:Autoincrement", true), + AllowWebClient = table.Column(nullable: false), + SecurityLevel = table.Column(nullable: false), + PrimaryPort = table.Column(nullable: false), + SecondaryPort = table.Column(nullable: false), + StartupTimeout = table.Column(nullable: false), + AutoStart = table.Column(nullable: false), + SoftRestart = table.Column(nullable: false), + SoftShutdown = table.Column(nullable: false), + ProcessId = table.Column(nullable: true), + AccessToken = table.Column(nullable: true), + InstanceId = table.Column(nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_DreamDaemonSettings", x => x.Id); + table.ForeignKey( + name: "FK_DreamDaemonSettings_Instances_InstanceId", + column: x => x.InstanceId, + principalTable: "Instances", + principalColumn: "Id", + onDelete: ReferentialAction.Cascade); + }); + + migrationBuilder.Sql( + $"INSERT INTO DreamDaemonSettings SELECT Id,AllowWebClient,SecurityLevel,PrimaryPort,SecondaryPort,StartupTimeout,AutoStart,SoftRestart,SoftShutdown,ProcessId,AccessToken,InstanceId FROM DreamDaemonSettings_down"); + + migrationBuilder.DropTable( + name: "DreamDaemonSettings_down"); } } } diff --git a/src/Tgstation.Server.Host/Database/Migrations/20200616180742_SLTopicTimeout.cs b/src/Tgstation.Server.Host/Database/Migrations/20200616180742_SLTopicTimeout.cs index bca299417f..fcdcdf9500 100644 --- a/src/Tgstation.Server.Host/Database/Migrations/20200616180742_SLTopicTimeout.cs +++ b/src/Tgstation.Server.Host/Database/Migrations/20200616180742_SLTopicTimeout.cs @@ -25,9 +25,42 @@ namespace Tgstation.Server.Host.Database.Migrations { if (migrationBuilder == null) throw new ArgumentNullException(nameof(migrationBuilder)); - migrationBuilder.DropColumn( - name: "TopicRequestTimeout", - table: "DreamDaemonSettings"); + + migrationBuilder.RenameTable( + name: "DreamDaemonSettings", + newName: "DreamDaemonSettings_down"); + + migrationBuilder.CreateTable( + name: "DreamDaemonSettings", + columns: table => new + { + Id = table.Column(nullable: false) + .Annotation("Sqlite:Autoincrement", true), + AllowWebClient = table.Column(nullable: false), + SecurityLevel = table.Column(nullable: false), + PrimaryPort = table.Column(nullable: false), + SecondaryPort = table.Column(nullable: false), + StartupTimeout = table.Column(nullable: false), + AutoStart = table.Column(nullable: false), + InstanceId = table.Column(nullable: false), + HeartbeatSeconds = table.Column(nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_DreamDaemonSettings", x => x.Id); + table.ForeignKey( + name: "FK_DreamDaemonSettings_Instances_InstanceId", + column: x => x.InstanceId, + principalTable: "Instances", + principalColumn: "Id", + onDelete: ReferentialAction.Cascade); + }); + + migrationBuilder.Sql( + $"INSERT INTO DreamDaemonSettings SELECT Id,AllowWebClient,SecurityLevel,PrimaryPort,SecondaryPort,StartupTimeout,AutoStart,InstanceId,HeartbeatSeconds FROM DreamDaemonSettings_down"); + + migrationBuilder.DropTable( + name: "DreamDaemonSettings_down"); } } } diff --git a/src/Tgstation.Server.Host/Database/Migrations/20200705163700_SLAllowNullDMApi.cs b/src/Tgstation.Server.Host/Database/Migrations/20200705163700_SLAllowNullDMApi.cs index 422cdcc5c5..e001cc523f 100644 --- a/src/Tgstation.Server.Host/Database/Migrations/20200705163700_SLAllowNullDMApi.cs +++ b/src/Tgstation.Server.Host/Database/Migrations/20200705163700_SLAllowNullDMApi.cs @@ -131,9 +131,37 @@ namespace Tgstation.Server.Host.Database.Migrations name: "CompileJobs_down", newName: "CompileJobs"); - migrationBuilder.DropColumn( - name: "RequireDMApiValidation", - table: "DreamMakerSettings"); + migrationBuilder.RenameTable( + name: "DreamMakerSettings", + newName: "DreamMakerSettings_down"); + + migrationBuilder.CreateTable( + name: "DreamMakerSettings", + columns: table => new + { + Id = table.Column(nullable: false) + .Annotation("Sqlite:Autoincrement", true), + ProjectName = table.Column(maxLength: 10000, nullable: true), + ApiValidationPort = table.Column(nullable: false), + ApiValidationSecurityLevel = table.Column(nullable: false), + InstanceId = table.Column(nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_DreamMakerSettings", x => x.Id); + table.ForeignKey( + name: "FK_DreamMakerSettings_Instances_InstanceId", + column: x => x.InstanceId, + principalTable: "Instances", + principalColumn: "Id", + onDelete: ReferentialAction.Cascade); + }); + + migrationBuilder.Sql( + $"INSERT INTO DreamMakerSettings SELECT Id,ProjectName,ApiValidationPort,ApiValidationSecurityLevel,InstanceId FROM DreamMakerSettings_down"); + + migrationBuilder.DropTable( + name: "DreamMakerSettings_down"); } } } diff --git a/tests/Tgstation.Server.Tests/IntegrationTest.cs b/tests/Tgstation.Server.Tests/IntegrationTest.cs index 3f02965eca..f2669bcfc9 100644 --- a/tests/Tgstation.Server.Tests/IntegrationTest.cs +++ b/tests/Tgstation.Server.Tests/IntegrationTest.cs @@ -1,3 +1,7 @@ +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Logging; using Microsoft.VisualStudio.TestTools.UnitTesting; using Moq; @@ -15,6 +19,9 @@ using System.Threading.Tasks; using Tgstation.Server.Api; using Tgstation.Server.Api.Models; using Tgstation.Server.Client; +using Tgstation.Server.Host.Configuration; +using Tgstation.Server.Host.Database; +using Tgstation.Server.Host.Database.Migrations; using Tgstation.Server.Host.Extensions; using Tgstation.Server.Host.System; using Tgstation.Server.Tests.Instance; @@ -107,6 +114,54 @@ namespace Tgstation.Server.Tests } while (true); } +#if DEBUG + [TestMethod] + public async Task TestDownMigrations() + { + var connectionString = Environment.GetEnvironmentVariable("TGS4_TEST_CONNECTION_STRING"); + + if (String.IsNullOrEmpty(connectionString)) + Assert.Inconclusive("No connection string configured in env var TGS4_TEST_CONNECTION_STRING!"); + + var databaseTypeString = Environment.GetEnvironmentVariable("TGS4_TEST_DATABASE_TYPE"); + if (!Enum.TryParse(databaseTypeString, out var databaseType)) + Assert.Inconclusive("No/invalid database type configured in env var TGS4_TEST_DATABASE_TYPE!"); + + string migrationName = null; + DbContext CreateContext() + { + switch (databaseType) + { + case DatabaseType.MySql: + case DatabaseType.MariaDB: + migrationName = nameof(MYInitialCreate); + return new MySqlDatabaseContext(Host.Database.Design.DesignTimeDbContextFactoryHelpers.CreateDatabaseContextOptions(databaseType, connectionString)); + case DatabaseType.PostgresSql: + migrationName = nameof(PGCreate); + return new PostgresSqlDatabaseContext(Host.Database.Design.DesignTimeDbContextFactoryHelpers.CreateDatabaseContextOptions(databaseType, connectionString)); + case DatabaseType.SqlServer: + migrationName = nameof(MSInitialCreate); + return new SqlServerDatabaseContext(Host.Database.Design.DesignTimeDbContextFactoryHelpers.CreateDatabaseContextOptions(databaseType, connectionString)); + case DatabaseType.Sqlite: + migrationName = nameof(SLRebuild); + return new SqliteDatabaseContext(Host.Database.Design.DesignTimeDbContextFactoryHelpers.CreateDatabaseContextOptions(databaseType, connectionString)); + } + + return null; + } + + Task Delete(DbContext context) => databaseType == DatabaseType.Sqlite ? Task.CompletedTask : context.Database.EnsureCreatedAsync(); + + using var context = CreateContext(); + await Delete(context); + await context.Database.MigrateAsync(default); + var dbServiceProvider = ((IInfrastructure)context.Database).Instance; + var migrator = dbServiceProvider.GetRequiredService(); + await migrator.MigrateAsync(migrationName, default); + await Delete(context); + } +#endif + [TestMethod] public async Task TestServer() { From 916bf86fd2cf2d5d993256084f8958ae550267af Mon Sep 17 00:00:00 2001 From: Jordan Brown Date: Tue, 14 Jul 2020 16:20:29 -0400 Subject: [PATCH 32/68] Try using coverlet.msbuild to track down #1063 --- build/integration_test.sh | 5 +---- tests/Tgstation.Server.Tests/Tgstation.Server.Tests.csproj | 4 ++++ 2 files changed, 5 insertions(+), 4 deletions(-) diff --git a/build/integration_test.sh b/build/integration_test.sh index a534057c20..a81cbd8976 100755 --- a/build/integration_test.sh +++ b/build/integration_test.sh @@ -12,9 +12,6 @@ export TGS4_TEST_TEMP_DIRECTORY=~/tgs4_test cd tests/Tgstation.Server.Tests dotnet build -c $CONFIG - -sudo $HOME/.dotnet/tools/coverlet bin/$CONFIG/netcoreapp3.1/Tgstation.Server.Tests.dll --target "dotnet" --targetargs "test -c $CONFIG --no-build --logger:\"console;noprogress=true\"" --format opencover --output "../../TestResults/integration_test.xml" --include "[Tgstation.Server*]*" --exclude "[Tgstation.Server.Tests*]*" --exclude "[Tgstation.Server.Host]Tgstation.Server.Host.Database.Migrations.*" - -cd ../../TestResults +sudo dotnet test Tgstation.Server.Tests.csproj -l "console;verbosity=detailed" --no-build -c $CONFIG /p:CollectCoverage=true /p:CoverletOutputFormat=opencover /p:CoverletOutput='./integration_test.xml' bash <(curl -s https://codecov.io/bash) -f integration_test.xml -F integration diff --git a/tests/Tgstation.Server.Tests/Tgstation.Server.Tests.csproj b/tests/Tgstation.Server.Tests/Tgstation.Server.Tests.csproj index 71cd8cadc2..6f8f253315 100644 --- a/tests/Tgstation.Server.Tests/Tgstation.Server.Tests.csproj +++ b/tests/Tgstation.Server.Tests/Tgstation.Server.Tests.csproj @@ -8,6 +8,10 @@ + + runtime; build; native; contentfiles; analyzers; buildtransitive + all + From 0c9b24bb7d5ad6dc6e066b191e0aa74bf0092a77 Mon Sep 17 00:00:00 2001 From: Jordan Brown Date: Tue, 14 Jul 2020 17:57:38 -0400 Subject: [PATCH 33/68] Minor logging --- tests/Tgstation.Server.Tests/TestingServer.cs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/tests/Tgstation.Server.Tests/TestingServer.cs b/tests/Tgstation.Server.Tests/TestingServer.cs index aad3a9e24f..1fa45873a3 100644 --- a/tests/Tgstation.Server.Tests/TestingServer.cs +++ b/tests/Tgstation.Server.Tests/TestingServer.cs @@ -109,6 +109,7 @@ namespace Tgstation.Server.Tests public async Task Run(CancellationToken cancellationToken) { + Console.WriteLine("TEST SERVER START"); var firstRun = realServer == null; realServer = await Application .CreateDefaultServerFactory() @@ -125,6 +126,7 @@ namespace Tgstation.Server.Tests } await realServer.Run(cancellationToken); + Console.WriteLine("TEST SERVER END"); } } } From 83e08ae0946e8502387be70c0b41f69c977f5a97 Mon Sep 17 00:00:00 2001 From: Jordan Brown Date: Tue, 14 Jul 2020 19:56:17 -0400 Subject: [PATCH 34/68] Workaround for https://github.com/microsoft/vstest/issues/2080 --- build/integration_test.sh | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/build/integration_test.sh b/build/integration_test.sh index a81cbd8976..a12bca348d 100755 --- a/build/integration_test.sh +++ b/build/integration_test.sh @@ -11,7 +11,8 @@ export TGS4_TEST_TEMP_DIRECTORY=~/tgs4_test cd tests/Tgstation.Server.Tests +#npm will bitch if sudo'd dotnet build -c $CONFIG -sudo dotnet test Tgstation.Server.Tests.csproj -l "console;verbosity=detailed" --no-build -c $CONFIG /p:CollectCoverage=true /p:CoverletOutputFormat=opencover /p:CoverletOutput='./integration_test.xml' +sudo dotnet test Tgstation.Server.Tests.csproj -l "console;verbosity=detailed;noprogress=true" --no-build -c $CONFIG /p:CollectCoverage=true /p:CoverletOutputFormat=opencover /p:CoverletOutput='./integration_test.xml' bash <(curl -s https://codecov.io/bash) -f integration_test.xml -F integration From e4255b0a7ec97b386e64bbdddce3eff234b73383 Mon Sep 17 00:00:00 2001 From: Jordan Brown Date: Wed, 15 Jul 2020 13:30:12 -0400 Subject: [PATCH 35/68] Fix test suite --- .github/workflows/artifact-cleanup.yml | 15 + .github/workflows/suite.yml | 557 +++++++++++++----- appveyor.yml | 29 - build/UploadCoverage.ps1 | 7 - build/coverlet.runsettings | 13 + .../Components/Byond/ByondManager.cs | 4 +- .../System/PosixProcessFeatures.cs | 30 +- tests/DMAPI/BasicOperation/Test.dm | 4 +- .../Tgstation.Server.Api.Tests.csproj | 4 +- .../Tgstation.Server.Client.Tests.csproj | 4 +- ...Tgstation.Server.Host.Console.Tests.csproj | 4 +- ...Tgstation.Server.Host.Service.Tests.csproj | 4 +- .../System/TestProcessFeatures.cs | 4 +- .../Tgstation.Server.Host.Tests.csproj | 4 +- ...gstation.Server.Host.Watchdog.Tests.csproj | 4 +- .../Instance/DeploymentTest.cs | 7 + .../Instance/WatchdogTest.cs | 4 +- .../Tgstation.Server.Tests/IntegrationTest.cs | 7 +- .../Tgstation.Server.Tests.csproj | 4 +- tgstation-server.sln | 1 - 20 files changed, 478 insertions(+), 232 deletions(-) create mode 100644 .github/workflows/artifact-cleanup.yml delete mode 100644 build/UploadCoverage.ps1 create mode 100644 build/coverlet.runsettings diff --git a/.github/workflows/artifact-cleanup.yml b/.github/workflows/artifact-cleanup.yml new file mode 100644 index 0000000000..bee44d2fa6 --- /dev/null +++ b/.github/workflows/artifact-cleanup.yml @@ -0,0 +1,15 @@ +name: 'Delete Old Artifacts' +on: + schedule: + - cron: '0 * * * *' # every hour + +jobs: + delete-artifacts: + name: Delete Artifacts + runs-on: ubuntu-latest + steps: + - name: Delete Artifacts + uses: kolpav/purge-artifacts-action@v1 + with: + token: ${{ secrets.GITHUB_TOKEN }} + expire-in: 30days # Setting this to 0 will delete all artifacts diff --git a/.github/workflows/suite.yml b/.github/workflows/suite.yml index 3a0b81c4e6..2afa6cba58 100644 --- a/.github/workflows/suite.yml +++ b/.github/workflows/suite.yml @@ -10,64 +10,134 @@ on: - dev - master +env: + TGS4_TEST_DISCORD_CHANNEL: ${{ secrets.DISCORD_CHANNEL_ID }} + TGS4_TEST_DISCORD_TOKEN: ${{ secrets.DISCORD_TOKEN }} + TGS4_TEST_IRC_CHANNEL: ${{ secrets.IRC_CHANNEL }} + TGS4_TEST_IRC_CONNECTION_STRING: ${{ secrets.IRC_CONNECTION_STRING }} + TGS4_TEST_GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + jobs: - server-unit-tests: - name: Build Server and Run Unit Tests - strategy: - matrix: - dotnet: [ '3.1.x' ] - node: [ '12.x' ] - configuration: [ 'Debug', 'Release' ] + dmapi-build: + name: Build DMAPI env: - TGS4_TEST_DISCORD_CHANNEL: ${{ secrets.DISCORD_CHANNEL_ID }} - TGS4_TEST_DISCORD_TOKEN: ${{ secrets.DISCORD_TOKEN }} - TGS4_TEST_IRC_CHANNEL: ${{ secrets.IRC_CHANNEL }} - TGS4_TEST_IRC_CONNECTION_STRING: ${{ secrets.IRC_CONNECTION_STRING }} - TGS4_TEST_GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + BYOND_MAJOR: 513 + BYOND_MINOR: 1527 runs-on: ubuntu-latest steps: - - uses: actions/setup-node@v1 - with: - node-version: ${{ matrix.node }} + - name: Install x86 libc Dependencies + run: | + sudo dpkg --add-architecture i386 + sudo apt-get update + sudo apt-get install -y libc6-i386 libstdc++6:i386 - - name: Setup dotnet + - name: Install BYOND + if: steps.cache-byond.outputs.cache-hit != 'true' + run: | + echo "Setting up BYOND." + mkdir -p "$HOME/BYOND-${{ env.BYOND_MAJOR }}.${{ env.BYOND_MINOR }}" + cd "$HOME/BYOND-${{ env.BYOND_MAJOR }}.${{ env.BYOND_MINOR }}" + curl "http://www.byond.com/download/build/${{ env.BYOND_MAJOR }}/${{ env.BYOND_MAJOR }}.${{ env.BYOND_MINOR }}_byond_linux.zip" -o byond.zip + unzip byond.zip + cd byond + make here + exit 0 + + - name: Checkout + uses: actions/checkout@v1 + + - name: Build DMAPI Test Project + run: | + set -e + retval=1 + source $HOME/BYOND-${{ env.BYOND_MAJOR }}.${{ env.BYOND_MINOR }}/byond/bin/byondsetup + + if hash DreamMaker 2>/dev/null + then + DreamMaker tests/DMAPI/BasicOperation/basic_operation_test.dme 2>&1 | tee result.log + retval=$? + if ! grep '\- 0 errors, 0 warnings' result.log + then + retval=1 + fi + else + echo "Couldn't find the DreamMaker executable, aborting." + retval=2 + fi + exit $retval + + docker-build: + name: Build Docker Image + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v1 + + - name: Build Docker Image + run: docker build . -f build/Dockerfile + + linux-unit-tests: + name: Linux Unit Tests + strategy: + matrix: + configuration: [ 'Debug', 'Release' ] + runs-on: ubuntu-latest + steps: + - name: Install Node 12.X + uses: actions/setup-node@v1 + with: + node-version: 12.x + + - name: Setup dotnet 3.1.X uses: actions/setup-dotnet@v1 with: - dotnet-version: ${{ matrix.dotnet }} + dotnet-version: 3.1.x - name: Checkout uses: actions/checkout@v1 + - name: Build + run: dotnet build -c ${{ matrix.configuration }}NoService + + - name: Run Unit Tests + run: sudo dotnet test tgstation-server.sln --no-build --filter FullyQualifiedName!~IntegrationTest -l "console;verbosity=detailed;noprogress=true" -c ${{ matrix.configuration }}NoService --collect:"XPlat Code Coverage" --settings build/coverlet.runsettings -r ./TestResults + + - name: Store Code Coverage + uses: actions/upload-artifact@v2 + with: + name: linux-unit-test-coverage-${{ matrix.configuration }} + path: ./TestResults/ + + windows-unit-tests: + name: Windows Unit Tests + strategy: + matrix: + configuration: [ 'Debug', 'Release' ] + runs-on: windows-latest + steps: + - name: Checkout + uses: actions/checkout@v1 + - name: Build run: dotnet build -c ${{ matrix.configuration }} - name: Run Unit Tests - run: sudo dotnet test tgstation-server.sln --no-build --filter FullyQualifiedName!~IntegrationTests -l "console;verbosity=detailed;noprogress=true" -c ${{ matrix.configuration }} /p:CollectCoverage=true /p:CoverletOutputFormat=opencover /p:CoverletOutput="./unit_tests.xml" + run: dotnet test tgstation-server.sln --no-build --filter FullyQualifiedName!~IntegrationTest -l "console;verbosity=detailed;noprogress=true" -c ${{ matrix.configuration }} --collect:"XPlat Code Coverage" --settings build/coverlet.runsettings -r ./TestResults - - name: Publish Integration Test Artifacts - run: | - cd tests/Tgstation.Server.tests - dotnet publish -c ${{ matrix.configuration }} -o Artifacts --no-build - - - name: Store Integration Test Artifacts + - name: Store Code Coverage uses: actions/upload-artifact@v2 with: - name: integration-test-bins - path: tests/Tgstation.Server.Tests/Artifacts/ + name: windows-unit-test-coverage-${{ matrix.configuration }} + path: ./TestResults/ - - name: Upload Code Coverage - uses: codecov/codecov-action@v1 - with: - file: ./unit_tests.xml - flags: unittests - fail_ci_if_error: true - - integration-tests: - name: Integration Tests - needs: [server-unit-tests, dmapi-build] + linux-integration-tests: + name: Linux Integration Tests + needs: dmapi-build services: # We start all dbs here so we can just code the stuff once postgres: image: postgres + ports: + - 5432:5432 env: POSTGRES_PASSWORD: postgres # Set health checks to wait until postgres has started @@ -78,6 +148,8 @@ jobs: --health-retries 5 mariadb: image: mariadb + ports: + - 3306:3306 env: MYSQL_ROOT_PASSWORD: mariadb options: >- @@ -87,6 +159,8 @@ jobs: --health-retries=3 mysql: image: mysql:5.7.31 + ports: + - 3307:3306 env: MYSQL_ROOT_PASSWORD: mysql options: >- @@ -97,136 +171,329 @@ jobs: strategy: matrix: database-type: [ 'Sqlite', 'PostgresSql', 'MariaDB', 'MySql' ] - basic-watchdog: [ 'yes', 'no' ] + watchdog-type: [ 'Basic', 'System' ] + configuration: [ 'Debug', 'Release' ] runs-on: ubuntu-latest steps: - - name: Install Native Dependencies - run: | - sudo dpkg add-architecture i386 - sudo apt-get update - sudo apt-get install -y libc6-i386 libstdc++6:i386 gdb + - name: Disable ptrace_scope + run: echo 0 | sudo tee /proc/sys/kernel/yama/ptrace_scope - - name: Disable ptrace_scope - run: echo 0 | sudo tee /proc/sys/kernel/yama/ptrace_scope + - name: Install Native Dependencies + run: | + sudo dpkg --add-architecture i386 + sudo apt-get update + sudo apt-get install -y libc6-i386 libstdc++6:i386 gdb - - name: Setup dotnet - uses: actions/setup-dotnet@v1 - with: - dotnet-version: ${{ matrix.dotnet }} + - name: Install Node 12.X + uses: actions/setup-node@v1 + with: + node-version: 12.x - - name: Set Sqlite Connection Info - if: ${{ matrix.database-type == 'Sqlite' }} - run: | - echo "::set-env name=TGS4_TEST_DATABASE_TYPE::Sqlite" - echo "::set-env name=TGS4_TEST_CONNECTION_STRING::Data Source=TravisTestDB.sqlite3;Mode=ReadWriteCreate" + - name: Setup dotnet + uses: actions/setup-dotnet@v1 + with: + dotnet-version: ${{ matrix.dotnet }} - - name: Set PostgresSql Connection Info - if: ${{ matrix.database-type == 'PostgresSql' }} - run: | - echo "::set-env name=TGS4_TEST_DATABASE_TYPE::PostgresSql" - echo "::set-env name=TGS4_TEST_CONNECTION_STRING::Application Name=tgstation-server;Port=5432;Host=postgres;Username=postgres;Password=postgres;Database=TGS_Test" + - name: Set Sqlite Connection Info + if: ${{ matrix.database-type == 'Sqlite' }} + run: | + echo "::set-env name=TGS4_TEST_DATABASE_TYPE::Sqlite" + echo "::set-env name=TGS4_TEST_CONNECTION_STRING::Data Source=TravisTestDB.sqlite3;Mode=ReadWriteCreate" - - name: Set MariaDB Connection Info - if: ${{ matrix.database-type == 'MariaDB' }} - run: | - echo "::set-env name=TGS4_TEST_DATABASE_TYPE::MariaDB" - echo "::set-env name=TGS4_TEST_CONNECTION_STRING::server=mariadb;uid=root;pwd=mariadb;database=tgs_test" + - name: Set PostgresSql Connection Info + if: ${{ matrix.database-type == 'PostgresSql' }} + run: | + echo "::set-env name=TGS4_TEST_DATABASE_TYPE::PostgresSql" + echo "::set-env name=TGS4_TEST_CONNECTION_STRING::Application Name=tgstation-server;Host=127.0.0.1;Username=postgres;Password=postgres;Database=TGS_Test" - - name: Set MySQL Connection Info - if: ${{ matrix.database-type == 'MySql' }} - run: | - echo "::set-env name=TGS4_TEST_DATABASE_TYPE::MySql" - echo "::set-env name=TGS4_TEST_CONNECTION_STRING::server=mysql;uid=root;pwd=mysql;database=tgs_test" - echo "::set-env name=Database__ServerVersion::5.7.31" + - name: Set MariaDB Connection Info + if: ${{ matrix.database-type == 'MariaDB' }} + run: | + echo "::set-env name=TGS4_TEST_DATABASE_TYPE::MariaDB" + echo "::set-env name=TGS4_TEST_CONNECTION_STRING::Server=127.0.0.1;uid=root;pwd=mariadb;database=tgs_test" - - name: Set General__UseBasicWatchdog - if: ${{ matrix.basic-watchdog == 'yes' }} - run: echo "::set-env name=General__UseBasicWatchdog::true" + - name: Set MySQL Connection Info + if: ${{ matrix.database-type == 'MySql' }} + run: | + echo "::set-env name=TGS4_TEST_DATABASE_TYPE::MySql" + echo "::set-env name=TGS4_TEST_CONNECTION_STRING::Server=127.0.0.1;Port=3307;uid=root;pwd=mysql;database=tgs_test" + echo "::set-env name=Database__ServerVersion::5.7.31" - - name: Checkout - uses: actions/checkout@v1 + - name: Set General__UseBasicWatchdog + if: ${{ matrix.watchdog-type == 'Basic' }} + run: echo "::set-env name=General__UseBasicWatchdog::true" - - name: Set TGS4_TEST_PULL_REQUEST_NUMBER - if: ${{ github.event_name == 'pull_request' }} - run: echo "::set-env name=TGS4_TEST_PULL_REQUEST_NUMBER::${{ github.event.issue.number }}" + - name: Checkout + uses: actions/checkout@v1 - - name: Retrieve Integration Test Artifacts - uses: actions/download-artifact@v2 - with: - name: integration-test-bins - path: tests/Tgstation.Server.Tests/Artifacts + - name: Set TGS4_TEST_PULL_REQUEST_NUMBER + if: ${{ github.event_name == 'pull_request' }} + run: echo "::set-env name=TGS4_TEST_PULL_REQUEST_NUMBER::${{ github.event.number }}" - - name: Run Integration Test - run: | - cd tests/Tgstation.Server.Tests - dotnet test Artifacts/Tgstation.Server.Tests.dll -l "console;verbosity=detailed;noprogress=true" -c ${{ matrix.configuration }} /p:CollectCoverage=true /p:CoverletOutputFormat=opencover /p:CoverletOutput="./integration_tests.xml" + - name: Run Integration Test + run: | + cd tests/Tgstation.Server.Tests + sleep 10 + dotnet test -c ${{ matrix.configuration }} -l "console;verbosity=detailed;noprogress=true" --collect:"XPlat Code Coverage" --settings ../../build/coverlet.runsettings -r ./TestResults - - name: Upload Code Coverage - uses: codecov/codecov-action@v1 - with: - file: ./integration_tests.xml - flags: integration - fail_ci_if_error: true + - name: Store Code Coverage + uses: actions/upload-artifact@v2 + with: + name: linux-integration-test-coverage-${{ matrix.configuration }}-${{ matrix.watchdog-type }}-${{ matrix.database-type }} + path: tests/Tgstation.Server.Tests/TestResults/ - docker-build: - name: Build Docker Image - runs-on: ubuntu-latest - steps: - - name: Checkout - uses: actions/checkout@v1 + - name: Package Server Console + if: ${{ matrix.configuration == 'Release' && matrix.watchdog-type == 'System' && matrix.database-type == 'MariaDB' }} + run: | + cd src/Tgstation.Server.Host.Console + dotnet publish -c ${{ matrix.configuration }} -o ../../Artifacts/Console + cd ../Tgstation.Server.Host + dotnet publish -c ${{ matrix.configuration }} --no-build -o ../../Artifacts/Console/lib/Default - - name: Build Docker Image - run: docker build . -f build/Dockerfile + - name: Store Server Console + if: ${{ matrix.configuration == 'Release' && matrix.watchdog-type == 'System' && matrix.database-type == 'MariaDB' }} + uses: actions/upload-artifact@v2 + with: + name: ServerConsole + path: Artifacts/Console/ - dmapi-build: - name: Build DMAPI + windows-integration-test: + name: Windows Integration Test + needs: dmapi-build env: - BYOND_MAJOR: 513 - BYOND_MINOR: 1527 + TGS4_TEST_CONNECTION_STRING: Server=(localdb)\MSSQLLocalDB;Integrated Security=true;Initial Catalog=TGS_Test;Application Name=tgstation-server + TGS4_TEST_DATABASE_TYPE: SqlServer + TGS4_TEST_DUMP_API_SPEC: yes + strategy: + matrix: + watchdog-type: [ 'Basic', 'System' ] + configuration: [ 'Debug', 'Release' ] + runs-on: windows-latest + steps: + - name: Set General__UseBasicWatchdog + if: ${{ matrix.watchdog-type == 'Basic' }} + run: echo "::set-env name=General__UseBasicWatchdog::true" + + - name: Checkout + uses: actions/checkout@v1 + + - name: Set TGS4_TEST_PULL_REQUEST_NUMBER + if: ${{ github.event_name == 'pull_request' }} + run: echo "::set-env name=TGS4_TEST_PULL_REQUEST_NUMBER::${{ github.event.number }}" + + - name: Run Integration Test + run: | + cd tests/Tgstation.Server.Tests + Start-Sleep -Seconds 10 + dotnet test -c ${{ matrix.configuration }} -l "console;verbosity=detailed;noprogress=true" --collect:"XPlat Code Coverage" --settings ../../build/coverlet.runsettings -r ./TestResults + + - name: Store Code Coverage + uses: actions/upload-artifact@v2 + with: + name: windows-integration-test-coverage-${{ matrix.configuration }}-${{ matrix.watchdog-type }} + path: tests/Tgstation.Server.Tests/TestResults/ + + - name: Store OpenAPI Spec + if: ${{ matrix.configuration == 'Release' && matrix.watchdog-type == 'System' }} + uses: actions/upload-artifact@v2 + with: + name: openapi-spec + path: C:/swagger.json + + - name: Package Server Console + if: ${{ matrix.configuration == 'Release' && matrix.watchdog-type == 'Basic' }} + run: | + cd src/Tgstation.Server.Host.Service + dotnet publish -c ${{ matrix.configuration }} -o ../../Artifacts/Service + cd ../Tgstation.Server.Host + dotnet publish -c ${{ matrix.configuration }} --no-build -o ../../Artifacts/Service/lib/Default + + - name: Store Server Service + if: ${{ matrix.configuration == 'Release' && matrix.watchdog-type == 'Basic' }} + uses: actions/upload-artifact@v2 + with: + name: ServerService + path: Artifacts/Service/ + + validate-openapi-spec: + name: OpenAPI Spec Validation + needs: windows-integration-test + runs-on: windows-latest + steps: + - name: Install IBM OpenAPI Validator + run: npm i -g ibm-openapi-validator + + - name: Checkout + uses: actions/checkout@v1 + + - name: Retrieve OpenAPI Spec + uses: actions/download-artifact@v2 + with: + name: openapi-spec + path: ./swagger.json + + - name: Lint OpenAPI Spec + run: npx lint-openapi -p -c build/OpenApiValidationSettings.json ./swagger.json + + upload-code-coverage: + name: Upload Code Coverage + needs: [linux-unit-tests, linux-integration-tests, windows-unit-tests, windows-integration-test] runs-on: ubuntu-latest steps: - - name: Install x86 libc Dependencies - run: | - sudo dpkg add-architecture i386 - sudo apt-get update - sudo apt-get install -y libc6-i386 libstdc++6:i386 + - name: Retrieve Linux Unit Test Coverage (Debug) + uses: actions/download-artifact@v2 + with: + name: linux-unit-test-coverage-Debug + path: ./code_coverage/unit_tests/linux_unit_tests_debug - - name: Cache BYOND - id: cache-byond - uses: actions/cache@v2 - with: - path: byond - key: ${{ env.BYOND_MAJOR }}.${{ env.BYOND_MINOR }} + - name: Retrieve Linux Unit Test Coverage (Release) + uses: actions/download-artifact@v2 + with: + name: linux-unit-test-coverage-Release + path: ./code_coverage/unit_tests/linux_unit_tests_release - - name: Install BYOND - if: steps.cache-byond.outputs.cache-hit != 'true' - run: | - echo "Setting up BYOND." - mkdir -p "$HOME/BYOND-${{ env.BYOND_MAJOR }}.${{ env.BYOND_MINOR }}" - cd "$HOME/BYOND-${BYOND_MAJOR}.${BYOND_MINOR}" - curl "http://www.byond.com/download/build/${{ env.BYOND_MAJOR }}/${{ env.BYOND_MAJOR }}.${{ env.BYOND_MINOR }}_byond_linux.zip" -o byond.zip - unzip byond.zip - cd byond - make here - cd ~/ - exit 0 + - name: Retrieve Linux Integration Test Coverage (Debug, Basic, Sqlite) + uses: actions/download-artifact@v2 + with: + name: linux-integration-test-coverage-Debug-Basic-Sqlite + path: ./code_coverage/integration_tests/linux_integration_tests_debug_basic_sqlite - - name: Build DMAPI Test Project - run: | - set -e - retval=1 - source $HOME/BYOND-${{ env.BYOND_MAJOR }}.${{ env.BYOND_MINOR }}/byond/bin/byondsetup + - name: Retrieve Linux Integration Test Coverage (Release, Basic, Sqlite) + uses: actions/download-artifact@v2 + with: + name: linux-integration-test-coverage-Release-Basic-Sqlite + path: ./code_coverage/integration_tests/linux_integration_tests_release_basic_sqlite - if hash DreamMaker 2>/dev/null - then - DreamMaker $DMEName 2>&1 | tee result.log - retval=$? - if ! grep '\- 0 errors, 0 warnings' result.log - then - retval=1 - fi - else - echo "Couldn't find the DreamMaker executable, aborting." - retval=2 - fi - exit $retval + - name: Retrieve Linux Integration Test Coverage (Debug, System, Sqlite) + uses: actions/download-artifact@v2 + with: + name: linux-integration-test-coverage-Debug-System-Sqlite + path: ./code_coverage/integration_tests/linux_integration_tests_debug_system_sqlite + + - name: Retrieve Linux Integration Test Coverage (Release, System, Sqlite) + uses: actions/download-artifact@v2 + with: + name: linux-integration-test-coverage-Release-System-Sqlite + path: ./code_coverage/integration_tests/linux_integration_tests_release_system_sqlite + + - name: Retrieve Linux Integration Test Coverage (Debug, Basic, PostgresSql) + uses: actions/download-artifact@v2 + with: + name: linux-integration-test-coverage-Debug-Basic-PostgresSql + path: ./code_coverage/integration_tests/linux_integration_tests_debug_basic_postgressql + + - name: Retrieve Linux Integration Test Coverage (Release, Basic, PostgresSql) + uses: actions/download-artifact@v2 + with: + name: linux-integration-test-coverage-Release-Basic-PostgresSql + path: ./code_coverage/integration_tests/linux_integration_tests_release_basic_postgressql + + - name: Retrieve Linux Integration Test Coverage (Debug, System, PostgresSql) + uses: actions/download-artifact@v2 + with: + name: linux-integration-test-coverage-Debug-System-PostgresSql + path: ./code_coverage/integration_tests/linux_integration_tests_debug_system_postgressql + + - name: Retrieve Linux Integration Test Coverage (Release, System, PostgresSql) + uses: actions/download-artifact@v2 + with: + name: linux-integration-test-coverage-Release-System-PostgresSql + path: ./code_coverage/integration_tests/linux_integration_tests_release_system_mariadb + + - name: Retrieve Linux Integration Test Coverage (Debug, Basic, MariaDB) + uses: actions/download-artifact@v2 + with: + name: linux-integration-test-coverage-Debug-Basic-MariaDB + path: ./code_coverage/integration_tests/linux_integration_tests_debug_basic_mariadb + + - name: Retrieve Linux Integration Test Coverage (Release, Basic, MariaDB) + uses: actions/download-artifact@v2 + with: + name: linux-integration-test-coverage-Release-Basic-MariaDB + path: ./code_coverage/integration_tests/linux_integration_tests_release_basic_mariadb + + - name: Retrieve Linux Integration Test Coverage (Debug, System, MariaDB) + uses: actions/download-artifact@v2 + with: + name: linux-integration-test-coverage-Debug-System-MariaDB + path: ./code_coverage/integration_tests/linux_integration_tests_debug_system_mariadb + + - name: Retrieve Linux Integration Test Coverage (Release, System, MariaDB) + uses: actions/download-artifact@v2 + with: + name: linux-integration-test-coverage-Release-System-MariaDB + path: ./code_coverage/integration_tests/linux_integration_tests_release_system_mysql + + - name: Retrieve Linux Integration Test Coverage (Debug, Basic, MySql) + uses: actions/download-artifact@v2 + with: + name: linux-integration-test-coverage-Debug-Basic-MySql + path: ./code_coverage/integration_tests/linux_integration_tests_debug_basic_mysql + + - name: Retrieve Linux Integration Test Coverage (Release, Basic, MySql) + uses: actions/download-artifact@v2 + with: + name: linux-integration-test-coverage-Release-Basic-MySql + path: ./code_coverage/integration_tests/linux_integration_tests_release_basic_mysql + + - name: Retrieve Linux Integration Test Coverage (Debug, System, MySql) + uses: actions/download-artifact@v2 + with: + name: linux-integration-test-coverage-Debug-System-MySql + path: ./code_coverage/integration_tests/linux_integration_tests_debug_system_mysql + + - name: Retrieve Linux Integration Test Coverage (Release, System, MySql) + uses: actions/download-artifact@v2 + with: + name: linux-integration-test-coverage-Release-System-MySql + path: ./code_coverage/integration_tests/linux_integration_tests_release_system_mysql + + - name: Retrieve Windows Unit Test Coverage (Debug) + uses: actions/download-artifact@v2 + with: + name: windows-unit-test-coverage-Debug + path: ./code_coverage/unit_tests/windows_unit_tests_debug + + - name: Retrieve Windows Unit Test Coverage (Release) + uses: actions/download-artifact@v2 + with: + name: windows-unit-test-coverage-Release + path: ./code_coverage/unit_tests/windows_unit_tests_release + + - name: Retrieve Windows Integration Test Coverage (Debug, Basic) + uses: actions/download-artifact@v2 + with: + name: windows-integration-test-coverage-Debug-Basic + path: ./code_coverage/integration_tests/windows_integration_tests_debug_basic + + - name: Retrieve Windows Integration Test Coverage (Release, Basic) + uses: actions/download-artifact@v2 + with: + name: windows-integration-test-coverage-Release-Basic + path: ./code_coverage/integration_tests/windows_integration_tests_release_basic + + - name: Retrieve Windows Integration Test Coverage (Debug, System) + uses: actions/download-artifact@v2 + with: + name: windows-integration-test-coverage-Debug-System + path: ./code_coverage/integration_tests/windows_integration_tests_debug_system + + - name: Retrieve Windows Integration Test Coverage (Release, System) + uses: actions/download-artifact@v2 + with: + name: windows-integration-test-coverage-Release-System + path: ./code_coverage/integration_tests/windows_integration_tests_release_system + + - name: Upload Unit Test Coverage to CodeCov + uses: codecov/codecov-action@v1 + with: + directory: ./code_coverage/unit_tests + flags: unittests + fail_ci_if_error: true + + - name: Upload Integration Test Coverage to CodeCov + uses: codecov/codecov-action@v1 + with: + directory: ./code_coverage/integration_tests + flags: integration + fail_ci_if_error: true diff --git a/appveyor.yml b/appveyor.yml index 13d2da0ad0..9303b2b6c8 100644 --- a/appveyor.yml +++ b/appveyor.yml @@ -2,8 +2,6 @@ version: '{build}' pull_requests: do_not_increment_build_number: true environment: - TGS4_TEST_TEMP_DIRECTORY: C:/tgs4_test - TGS4_TEST_DATABASE_TYPE: SqlServer TGS4_TEST_DUMP_API_SPEC: yes TGS4_TEST_CONNECTION_STRING: Server=(local)\SQL2017;Initial Catalog=TGS_Test;User ID=sa;Password=Password12! TGS4_TEST_GITHUB_TOKEN: @@ -16,10 +14,8 @@ environment: TGS4_TEST_IRC_CHANNEL: \#botbus TGS4_RELEASE_NOTES_TOKEN: secure: lJNGAXwiB5HlWdthz3K4PetqpTG5IEAyRgKaiKxFMQ8HW8CcOjRtB97B05op7BsK - branches: only: - - dev - master skip_tags: true image: Visual Studio 2019 @@ -53,33 +49,8 @@ build: publish_nuget: true publish_nuget_symbols: true use_snupkg_format: true -test_script: - - OpenCover.Console.exe -returntargetcode -register:user -target:"C:/Program Files/dotnet/dotnet.exe" -targetargs:"test -c %CONFIGURATION% --logger:trx;LogFileName=results.trx /p:DebugType=full tests/Tgstation.Server.Api.Tests/Tgstation.Server.Api.Tests.csproj" -filter:"+[Tgstation.Server*]* -[Tgstation.Server.Api.Tests*]*" -output:".\api_coverage.xml" -oldstyle - - ps: $wc = New-Object 'System.Net.WebClient' - - ps: $wc.UploadFile("https://ci.appveyor.com/api/testresults/mstest/$($env:APPVEYOR_JOB_ID)", (Resolve-Path .\tests\Tgstation.Server.Api.Tests\TestResults\results.trx)) - - OpenCover.Console.exe -returntargetcode -register:user -target:"C:/Program Files/dotnet/dotnet.exe" -targetargs:"test -c %CONFIGURATION% --logger:trx;LogFileName=results.trx /p:DebugType=full tests/Tgstation.Server.Client.Tests/Tgstation.Server.Client.Tests.csproj" -filter:"+[Tgstation.Server*]* -[Tgstation.Server.Client.Tests*]*" -output:".\client_coverage.xml" -oldstyle - - ps: $wc = New-Object 'System.Net.WebClient' - - ps: $wc.UploadFile("https://ci.appveyor.com/api/testresults/mstest/$($env:APPVEYOR_JOB_ID)", (Resolve-Path .\tests\Tgstation.Server.Client.Tests\TestResults\results.trx)) - - OpenCover.Console.exe -returntargetcode -register:user -target:"C:/Program Files/dotnet/dotnet.exe" -targetargs:"test -c %CONFIGURATION% --logger:trx;LogFileName=results.trx /p:DebugType=full tests/Tgstation.Server.Host.Tests/Tgstation.Server.Host.Tests.csproj" -filter:"+[Tgstation.Server*]* -[Tgstation.Server.Host.Tests*]* -[Tgstation.Server.Host]Tgstation.Server.Host.Database.Migrations*" -output:".\host_coverage.xml" -oldstyle - - ps: $wc = New-Object 'System.Net.WebClient' - - ps: $wc.UploadFile("https://ci.appveyor.com/api/testresults/mstest/$($env:APPVEYOR_JOB_ID)", (Resolve-Path .\tests\Tgstation.Server.Host.Tests\TestResults\results.trx)) - - OpenCover.Console.exe -returntargetcode -register:user -target:"C:/Program Files/dotnet/dotnet.exe" -targetargs:"test -c %CONFIGURATION% --logger:trx;LogFileName=results.trx /p:DebugType=full tests/Tgstation.Server.Host.Console.Tests/Tgstation.Server.Host.Console.Tests.csproj" -filter:"+[Tgstation.Server*]* -[Tgstation.Server.Host.Console.Tests*]*" -output:".\console_coverage.xml" -oldstyle - - ps: $wc = New-Object 'System.Net.WebClient' - - ps: $wc.UploadFile("https://ci.appveyor.com/api/testresults/mstest/$($env:APPVEYOR_JOB_ID)", (Resolve-Path .\tests\Tgstation.Server.Host.Console.Tests\TestResults\results.trx)) - - set path=%ProgramFiles(x86)%\Microsoft Visual Studio\2019\TestAgent\Common7\IDE\CommonExtensions\Microsoft\TestWindow;%path% - - vstest.console /logger:trx;LogFileName=results.trx "tests\Tgstation.Server.Host.Service.Tests\bin\%CONFIGURATION%\net472\Tgstation.Server.Host.Service.Tests.dll" /inIsolation /Platform:x64 - - ps: $wc = New-Object 'System.Net.WebClient' - - ps: $wc.UploadFile("https://ci.appveyor.com/api/testresults/mstest/$($env:APPVEYOR_JOB_ID)", (Resolve-Path .\TestResults\results.trx)) - - OpenCover.Console.exe -returntargetcode -register:user -target:"C:/Program Files/dotnet/dotnet.exe" -targetargs:"test -c %CONFIGURATION% --logger:trx;LogFileName=results.trx /p:DebugType=full tests/Tgstation.Server.Host.Watchdog.Tests/Tgstation.Server.Host.Watchdog.Tests.csproj" -filter:"+[Tgstation.Server*]* -[Tgstation.Server.Host.Watchdog.Tests*]*" -output:".\watchdog_coverage.xml" -oldstyle - - ps: $wc = New-Object 'System.Net.WebClient' - - ps: $wc.UploadFile("https://ci.appveyor.com/api/testresults/mstest/$($env:APPVEYOR_JOB_ID)", (Resolve-Path .\tests\Tgstation.Server.Host.Watchdog.Tests\TestResults\results.trx)) - - OpenCover.Console.exe -returntargetcode -register:user -target:"C:/Program Files/dotnet/dotnet.exe" -targetargs:"test -c %CONFIGURATION% --logger:trx;LogFileName=results.trx --logger:console;noprogress=true /p:DebugType=full tests/Tgstation.Server.Tests/Tgstation.Server.Tests.csproj" -filter:"+[Tgstation.Server*]* -[Tgstation.Server.Tests*]* -[Tgstation.Server.Host]Tgstation.Server.Host.Database.Migrations..*" -output:".\server_coverage.xml" -oldstyle - - ps: $wc = New-Object 'System.Net.WebClient' - - ps: $wc.UploadFile("https://ci.appveyor.com/api/testresults/mstest/$($env:APPVEYOR_JOB_ID)", (Resolve-Path .\tests\Tgstation.Server.Tests\TestResults\results.trx)) - - lint-openapi -p -c build/OpenApiValidationSettings.json C:/swagger.json after_test: - ps: Move-Item -path C:/swagger.json swagger.json - - ps: build/UploadCoverage.ps1 #host updater - dotnet publish src/Tgstation.Server.Host/Tgstation.Server.Host.csproj -o artifacts/ServerHost -c %CONFIGURATION% #console diff --git a/build/UploadCoverage.ps1 b/build/UploadCoverage.ps1 deleted file mode 100644 index a74244feee..0000000000 --- a/build/UploadCoverage.ps1 +++ /dev/null @@ -1,7 +0,0 @@ -codecov -f api_coverage.xml --flag unittests -codecov -f client_coverage.xml --flag unittests -codecov -f host_coverage.xml --flag unittests -codecov -f console_coverage.xml --flag unittests -codecov -f watchdog_coverage.xml --flag unittests -#codecov -f service.coveragexml --flag unittests -codecov -f server_coverage.xml --flag integration diff --git a/build/coverlet.runsettings b/build/coverlet.runsettings new file mode 100644 index 0000000000..783e8323cc --- /dev/null +++ b/build/coverlet.runsettings @@ -0,0 +1,13 @@ + + + + + + + opencover + false + + + + + diff --git a/src/Tgstation.Server.Host/Components/Byond/ByondManager.cs b/src/Tgstation.Server.Host/Components/Byond/ByondManager.cs index 5e1e0312f5..72ea830c82 100644 --- a/src/Tgstation.Server.Host/Components/Byond/ByondManager.cs +++ b/src/Tgstation.Server.Host/Components/Byond/ByondManager.cs @@ -1,4 +1,4 @@ -using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Logging; using System; using System.Collections.Generic; using System.Linq; @@ -257,7 +257,7 @@ namespace Tgstation.Server.Host.Components.Byond var versionKey = VersionKey(versionToUse, true); var binPathForVersion = ioManager.ConcatPath(versionKey, BinPath); - logger.LogTrace("Creating ByondExecutableLock lock for version {0}", requiredVersion); + logger.LogTrace("Creating ByondExecutableLock lock for version {0}", versionToUse); return new ByondExecutableLock( ioManager, semaphore, diff --git a/src/Tgstation.Server.Host/System/PosixProcessFeatures.cs b/src/Tgstation.Server.Host/System/PosixProcessFeatures.cs index f808e588de..293fc880fe 100644 --- a/src/Tgstation.Server.Host/System/PosixProcessFeatures.cs +++ b/src/Tgstation.Server.Host/System/PosixProcessFeatures.cs @@ -1,10 +1,7 @@ -using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Logging; using Mono.Unix; using Mono.Unix.Native; using System; -using System.Globalization; -using System.Linq; -using System.Text; using System.Threading; using System.Threading.Tasks; using Tgstation.Server.Api.Models; @@ -79,29 +76,8 @@ namespace Tgstation.Server.Host.System } /// - public async Task GetExecutingUsername(global::System.Diagnostics.Process process, CancellationToken cancellationToken) - { - if (process == null) - throw new ArgumentNullException(nameof(process)); - - // Need to read /proc/[pid]/status - // http://man7.org/linux/man-pages/man5/proc.5.html - // https://unix.stackexchange.com/questions/102676/why-is-uid-information-not-in-proc-x-stat - var pid = process.Id; - var statusFile = ioManager.ConcatPath("/proc", pid.ToString(CultureInfo.InvariantCulture), "status"); - var statusBytes = await ioManager.ReadAllBytes(statusFile, cancellationToken).ConfigureAwait(false); - var statusText = Encoding.UTF8.GetString(statusBytes); - var splits = statusText.Split('\n', StringSplitOptions.RemoveEmptyEntries); - var entry = splits.FirstOrDefault(x => x.Trim().StartsWith("Uid:", StringComparison.Ordinal)); - if (entry == default) - return "UNKNOWN"; - - return entry - .Substring(4) - .Split(' ', StringSplitOptions.RemoveEmptyEntries) - .FirstOrDefault(x => !String.IsNullOrWhiteSpace(x)) - ?? "UNPARSABLE"; - } + public Task GetExecutingUsername(global::System.Diagnostics.Process process, CancellationToken cancellationToken) + => throw new NotSupportedException(); /// public async Task CreateDump(global::System.Diagnostics.Process process, string outputFile, CancellationToken cancellationToken) diff --git a/tests/DMAPI/BasicOperation/Test.dm b/tests/DMAPI/BasicOperation/Test.dm index a603a099be..066c9eceac 100644 --- a/tests/DMAPI/BasicOperation/Test.dm +++ b/tests/DMAPI/BasicOperation/Test.dm @@ -19,7 +19,9 @@ var/datum/tgs_version/dmapi_version = new /datum/tgs_version(TGS_DMAPI_VERSION) if(!active_version.Equals(dmapi_version)) text2file("DMAPI version [TGS_DMAPI_VERSION] does not match active API version [active_version.raw_parameter]", "test_fail_reason.txt") - + + world.log << "sleep2" + sleep(50) world.log << "Terminating..." world.TgsEndProcess() diff --git a/tests/Tgstation.Server.Api.Tests/Tgstation.Server.Api.Tests.csproj b/tests/Tgstation.Server.Api.Tests/Tgstation.Server.Api.Tests.csproj index 81b42afb96..4ac3e5c0c5 100644 --- a/tests/Tgstation.Server.Api.Tests/Tgstation.Server.Api.Tests.csproj +++ b/tests/Tgstation.Server.Api.Tests/Tgstation.Server.Api.Tests.csproj @@ -8,9 +8,9 @@ - - runtime; build; native; contentfiles; analyzers; buildtransitive + all + runtime; build; native; contentfiles; analyzers; buildtransitive diff --git a/tests/Tgstation.Server.Client.Tests/Tgstation.Server.Client.Tests.csproj b/tests/Tgstation.Server.Client.Tests/Tgstation.Server.Client.Tests.csproj index 0585c62165..1ce9b9982d 100644 --- a/tests/Tgstation.Server.Client.Tests/Tgstation.Server.Client.Tests.csproj +++ b/tests/Tgstation.Server.Client.Tests/Tgstation.Server.Client.Tests.csproj @@ -8,9 +8,9 @@ - - runtime; build; native; contentfiles; analyzers; buildtransitive + all + runtime; build; native; contentfiles; analyzers; buildtransitive diff --git a/tests/Tgstation.Server.Host.Console.Tests/Tgstation.Server.Host.Console.Tests.csproj b/tests/Tgstation.Server.Host.Console.Tests/Tgstation.Server.Host.Console.Tests.csproj index a53fb435d8..c37b05c82f 100644 --- a/tests/Tgstation.Server.Host.Console.Tests/Tgstation.Server.Host.Console.Tests.csproj +++ b/tests/Tgstation.Server.Host.Console.Tests/Tgstation.Server.Host.Console.Tests.csproj @@ -8,9 +8,9 @@ - - runtime; build; native; contentfiles; analyzers; buildtransitive + all + runtime; build; native; contentfiles; analyzers; buildtransitive diff --git a/tests/Tgstation.Server.Host.Service.Tests/Tgstation.Server.Host.Service.Tests.csproj b/tests/Tgstation.Server.Host.Service.Tests/Tgstation.Server.Host.Service.Tests.csproj index 68cbe52649..c47523526f 100644 --- a/tests/Tgstation.Server.Host.Service.Tests/Tgstation.Server.Host.Service.Tests.csproj +++ b/tests/Tgstation.Server.Host.Service.Tests/Tgstation.Server.Host.Service.Tests.csproj @@ -14,9 +14,9 @@ - - runtime; build; native; contentfiles; analyzers; buildtransitive + all + runtime; build; native; contentfiles; analyzers; buildtransitive diff --git a/tests/Tgstation.Server.Host.Tests/System/TestProcessFeatures.cs b/tests/Tgstation.Server.Host.Tests/System/TestProcessFeatures.cs index cfa6173cce..aca9cae1f1 100644 --- a/tests/Tgstation.Server.Host.Tests/System/TestProcessFeatures.cs +++ b/tests/Tgstation.Server.Host.Tests/System/TestProcessFeatures.cs @@ -27,8 +27,8 @@ namespace Tgstation.Server.Host.System.Tests [TestMethod] public async Task TestGetUsername() { - if (!String.IsNullOrWhiteSpace(Environment.GetEnvironmentVariable("TRAVIS"))) - Assert.Inconclusive("This test doesn't work on TRAVIS CI!"); + if (!new PlatformIdentifier().IsWindows) + Assert.Inconclusive("This test is buggy on linux and not required"); var username = await features.GetExecutingUsername(global::System.Diagnostics.Process.GetCurrentProcess(), default); Assert.IsTrue(username.Contains(Environment.UserName), $"Exepcted a string containing \"{Environment.UserName}\", got \"{username}\""); diff --git a/tests/Tgstation.Server.Host.Tests/Tgstation.Server.Host.Tests.csproj b/tests/Tgstation.Server.Host.Tests/Tgstation.Server.Host.Tests.csproj index 243e758b8c..60aba1cdb0 100644 --- a/tests/Tgstation.Server.Host.Tests/Tgstation.Server.Host.Tests.csproj +++ b/tests/Tgstation.Server.Host.Tests/Tgstation.Server.Host.Tests.csproj @@ -8,9 +8,9 @@ - - runtime; build; native; contentfiles; analyzers; buildtransitive + all + runtime; build; native; contentfiles; analyzers; buildtransitive diff --git a/tests/Tgstation.Server.Host.Watchdog.Tests/Tgstation.Server.Host.Watchdog.Tests.csproj b/tests/Tgstation.Server.Host.Watchdog.Tests/Tgstation.Server.Host.Watchdog.Tests.csproj index 0c9e91bb71..c4cfd002c5 100644 --- a/tests/Tgstation.Server.Host.Watchdog.Tests/Tgstation.Server.Host.Watchdog.Tests.csproj +++ b/tests/Tgstation.Server.Host.Watchdog.Tests/Tgstation.Server.Host.Watchdog.Tests.csproj @@ -14,9 +14,9 @@ - - runtime; build; native; contentfiles; analyzers; buildtransitive + all + runtime; build; native; contentfiles; analyzers; buildtransitive diff --git a/tests/Tgstation.Server.Tests/Instance/DeploymentTest.cs b/tests/Tgstation.Server.Tests/Instance/DeploymentTest.cs index 6e63bf7b55..7772e32343 100644 --- a/tests/Tgstation.Server.Tests/Instance/DeploymentTest.cs +++ b/tests/Tgstation.Server.Tests/Instance/DeploymentTest.cs @@ -4,6 +4,7 @@ using System.Threading.Tasks; using Microsoft.VisualStudio.TestTools.UnitTesting; using Tgstation.Server.Api.Models; using Tgstation.Server.Client.Components; +using Tgstation.Server.Host.System; namespace Tgstation.Server.Tests.Instance { @@ -32,6 +33,12 @@ namespace Tgstation.Server.Tests.Instance await repositoryTask; // by alphabetization rules, it should discover api_free here + if (!new PlatformIdentifier().IsWindows) + await dreamMakerClient.Update(new DreamMaker + { + ProjectName = "tests/DMAPI/ApiFree/api_free" + }, cancellationToken); + var updatedDD = await dreamDaemonClient.Update(new DreamDaemon { StartupTimeout = 5 diff --git a/tests/Tgstation.Server.Tests/Instance/WatchdogTest.cs b/tests/Tgstation.Server.Tests/Instance/WatchdogTest.cs index f7fce8c6e3..8af1be7c16 100644 --- a/tests/Tgstation.Server.Tests/Instance/WatchdogTest.cs +++ b/tests/Tgstation.Server.Tests/Instance/WatchdogTest.cs @@ -1,4 +1,4 @@ -using Byond.TopicSender; +using Byond.TopicSender; using Microsoft.Extensions.Logging; using Microsoft.VisualStudio.TestTools.UnitTesting; using Moq; @@ -437,7 +437,7 @@ namespace Tgstation.Server.Tests.Instance var result = await bts.SendTopic(IPAddress.Loopback, "tgs_integration_test_special_tactics=1", 1337, cancellationToken); Assert.AreEqual("ack", result.StringData); - await Task.Delay(10000, cancellationToken); + await Task.Delay(20000, cancellationToken); } catch (OperationCanceledException) { diff --git a/tests/Tgstation.Server.Tests/IntegrationTest.cs b/tests/Tgstation.Server.Tests/IntegrationTest.cs index f2669bcfc9..7efaace32c 100644 --- a/tests/Tgstation.Server.Tests/IntegrationTest.cs +++ b/tests/Tgstation.Server.Tests/IntegrationTest.cs @@ -176,7 +176,10 @@ namespace Tgstation.Server.Tests using var server = new TestingServer(); using var hardTimeoutCts = new CancellationTokenSource(); - hardTimeoutCts.CancelAfter(new TimeSpan(0, 9, 45)); + + var maximumTestDuration = new TimeSpan(0, 12, 0); + + hardTimeoutCts.CancelAfter(maximumTestDuration - new TimeSpan(0, 0, 15)); var hardTimeoutCancellationToken = hardTimeoutCts.Token; hardTimeoutCancellationToken.Register(() => { @@ -184,7 +187,7 @@ namespace Tgstation.Server.Tests }); using var softTimeoutCts = CancellationTokenSource.CreateLinkedTokenSource(hardTimeoutCancellationToken); - softTimeoutCts.CancelAfter(new TimeSpan(0, 9, 15)); + softTimeoutCts.CancelAfter(maximumTestDuration - new TimeSpan(0, 0, 45)); var softTimeoutCancellationToken = softTimeoutCts.Token; bool tooLateForSoftTimeout = false; softTimeoutCancellationToken.Register(() => diff --git a/tests/Tgstation.Server.Tests/Tgstation.Server.Tests.csproj b/tests/Tgstation.Server.Tests/Tgstation.Server.Tests.csproj index 6f8f253315..ebea335bde 100644 --- a/tests/Tgstation.Server.Tests/Tgstation.Server.Tests.csproj +++ b/tests/Tgstation.Server.Tests/Tgstation.Server.Tests.csproj @@ -8,9 +8,9 @@ - - runtime; build; native; contentfiles; analyzers; buildtransitive + all + runtime; build; native; contentfiles; analyzers; buildtransitive diff --git a/tgstation-server.sln b/tgstation-server.sln index 39cb8ff54e..aff47c7971 100644 --- a/tgstation-server.sln +++ b/tgstation-server.sln @@ -156,7 +156,6 @@ EndProject Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "BasicOperation", "BasicOperation", "{F32B9514-AAD9-429D-841A-ED810FC2598C}" ProjectSection(SolutionItems) = preProject tests\DMAPI\BasicOperation\basic_operation_test.dme = tests\DMAPI\BasicOperation\basic_operation_test.dme - tests\DMAPI\BasicOperation\build_byond.sh = tests\DMAPI\BasicOperation\build_byond.sh tests\DMAPI\BasicOperation\Config.dm = tests\DMAPI\BasicOperation\Config.dm tests\DMAPI\BasicOperation\Test.dm = tests\DMAPI\BasicOperation\Test.dm EndProjectSection From 3341ac8bfcbd99fd43770e5b72bcaa5e80e51fb4 Mon Sep 17 00:00:00 2001 From: Jordan Brown Date: Sat, 18 Jul 2020 12:18:09 -0400 Subject: [PATCH 36/68] Small README update --- README.md | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/README.md b/README.md index e69402d972..a1f9ff54ec 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ # tgstation-server v4: -[![Build status](https://ci.appveyor.com/api/projects/status/7t1h7bvuha0p9j5f/branch/master?svg=true)](https://ci.appveyor.com/project/Cyberboss/tgstation-server-tools/branch/master) [![Build Status](https://travis-ci.org/tgstation/tgstation-server.svg?branch=master)](https://travis-ci.org/tgstation/tgstation-server) [![codecov](https://codecov.io/gh/tgstation/tgstation-server/branch/master/graph/badge.svg)](https://codecov.io/gh/tgstation/tgstation-server) +![Test Suite](https://github.com/tgstation/tgstation-server/workflows/Test%20Suite/badge.svg) [![Build status](https://ci.appveyor.com/api/projects/status/7t1h7bvuha0p9j5f/branch/master?svg=true)](https://ci.appveyor.com/project/Cyberboss/tgstation-server-tools/branch/master) [![Build Status](https://travis-ci.org/tgstation/tgstation-server.svg?branch=master)](https://travis-ci.org/tgstation/tgstation-server) [![codecov](https://codecov.io/gh/tgstation/tgstation-server/branch/master/graph/badge.svg)](https://codecov.io/gh/tgstation/tgstation-server) [![GitHub license](https://img.shields.io/github/license/tgstation/tgstation-server.svg)](LICENSE) [![Average time to resolve an issue](http://isitmaintained.com/badge/resolution/tgstation/tgstation-server.svg)](http://isitmaintained.com/project/tgstation/tgstation-server "Average time to resolve an issue") [![NuGet version](https://img.shields.io/nuget/v/Tgstation.Server.Api.svg)](https://www.nuget.org/packages/Tgstation.Server.Api) [![NuGet version](https://img.shields.io/nuget/v/Tgstation.Server.Client.svg)](https://www.nuget.org/packages/Tgstation.Server.Client) @@ -8,7 +8,7 @@ [![forthebadge](http://forthebadge.com/images/badges/built-with-love.svg)](http://forthebadge.com) [![forthebadge](http://forthebadge.com/images/badges/60-percent-of-the-time-works-every-time.svg)](http://forthebadge.com) -This is a toolset to manage production BYOND servers. It includes the ability to update the server without having to stop or shutdown the server (the update will take effect on a "reboot" of the server) the ability start the server and restart it if it crashes, as well as systems for managing code and game files, and merging GitHub Pull Requests for test deployments. +This is a toolset to manage production BYOND servers. It includes the ability to update the server without having to stop or shutdown the server (the update will take effect on a "reboot" of the server) the ability start the server and restart it if it crashes, as well as systems for managing code and game files, and merging GitHub Pull Requests for test deployments. ### Legacy Servers @@ -78,13 +78,13 @@ If using manual configuration, before starting your container make sure the afor ### Configuring -The first time you run TGS4 you should be prompted with a configuration wizard which will guide you through setting up your appsettings.Production.json +The first time you run TGS4 you should be prompted with a configuration wizard which will guide you through setting up your appsettings.Production.json This wizard will, generally, run whenever the server is launched without detecting the config json. Follow the instructions below to perform this process manually. #### Manual Configuration -Create an `appsettings.Production.json` file next to `appsettings.json`. This will override the default settings in appsettings.json with your production settings. There are a few keys meant to be changed by hosts. Modifying any config files while the server is running will trigger a safe restart (Keeps DreamDaemon's running). Note these are all case-sensitive: +Create an `appsettings.Production.json` file next to `appsettings.json`. This will override the default settings in appsettings.json with your production settings. There are a few keys meant to be changed by hosts. Modifying any config files while the server is running will trigger a safe restart (Keeps DreamDaemon's running). Note these are all case-sensitive: - `General:ConfigVersion`: Suppresses warnings about out of date config versions. You should change this after updating TGS to one with a new config version. The current version can be found on the releases page for your server version (This field did not exist before v4.4.0). @@ -250,7 +250,7 @@ See https://docs.nginx.com/nginx/admin-guide/web-server/reverse-proxy/ ### Apache -1. Ensure the `mod_proxy` extension is installed. +1. Ensure the `mod_proxy` extension is installed. 2. Setup a basic website configuration. Instructions on how to do so are out of scope. 3. Acquire an HTTPS certificate, likely via Let's Encrypt, and configure Apache to use it. 4. Under a VirtualHost entry, setup the following (replace 8080 with the port TGS is hosted on): @@ -283,7 +283,7 @@ tgstation-server v4 is controlled via a RESTful HTTP json API. Documentation on ### Users -All actions apart from logging in must be taken by a user. TGS installs with one default user whose credentials can be found [here](src/Tgstation.Server.Api/Models/User.cs). It is recommended to disable this user ASAP as it is used to create Jobs that are started by the server itself. If access to all users is lost, the default user can be reset using the `Database:ResetAdminPassword` configuration setting. +All actions apart from logging in must be taken by a user. TGS installs with one default user whose credentials can be found [here](src/Tgstation.Server.Api/Models/User.cs). It is recommended to disable this user ASAP as it is used to create Jobs that are started by the server itself. If access to all users is lost, the default user can be reset using the `Database:ResetAdminPassword` configuration setting. Users can be enabled/disabled and have a very granular set of rights associated to them that determine the actions they are allowed to take (i.e. Modify the user list or create instances). Users can be _database based_ or _system based_. Database users are your standard web users with a username and password. System users, on the otherhand, are authenticated with the host OS. These users cannot have their password or names changed by TGS as they are managed by the system (and in reverse, login tokens don't expire when their password changes). The benefit to having these users is it allows the use of system ACLs for static file control. More on that later. @@ -295,7 +295,7 @@ An instance is stored in a single folder anywhere on a system and is made up of ##### Instance Users -All users with access to an instance have an InstanceUser object associated with the two that defines more rights specific to that instance (i.e. Deploy code, modify bots, edit other InstanceUsers). +All users with access to an instance have an InstanceUser object associated with the two that defines more rights specific to that instance (i.e. Deploy code, modify bots, edit other InstanceUsers). #### Repository From af9df2130ee476da899beb4dd3d51f29a2c1def3 Mon Sep 17 00:00:00 2001 From: Jordan Brown Date: Sun, 19 Jul 2020 17:17:29 -0400 Subject: [PATCH 37/68] General package update --- .../Tgstation.Server.Host.csproj | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/src/Tgstation.Server.Host/Tgstation.Server.Host.csproj b/src/Tgstation.Server.Host/Tgstation.Server.Host.csproj index 59715fd0a2..0222a5eb92 100644 --- a/src/Tgstation.Server.Host/Tgstation.Server.Host.csproj +++ b/src/Tgstation.Server.Host/Tgstation.Server.Host.csproj @@ -48,20 +48,20 @@ - - + + all runtime; build; native; contentfiles; analyzers - - + + all runtime; build; native; contentfiles; analyzers; buildtransitive - - - + + + all runtime; build; native; contentfiles; analyzers; buildtransitive @@ -86,7 +86,7 @@ - + From 7c0030180cb0b8e2e522abbf5927019fb591e151 Mon Sep 17 00:00:00 2001 From: Jordan Brown Date: Sun, 19 Jul 2020 17:27:47 -0400 Subject: [PATCH 38/68] Chungus refactors - Added a new ErrorCode for when chat bots fail to connect - Made IProvider IAsyncDispsable - Removed IDatabaseContextFactory dependency from watchdogs - Jobs will no longer start until instance manager startup is complete - Chat provider connection is now done as a job, there is no way to retrieve directly this job through the API currently - Fixed disabled chat bots connecting when their instance onlined - Fixed issue with chat provider reconnection channel remapping - IJobManager can now accept a null user in most cases which it will substitute for the TGS user - Fixed some watchdog issues relating to accessing disposed session controllers --- src/Tgstation.Server.Api/Models/ErrorCode.cs | 12 ++- .../Components/Chat/ChatManager.cs | 95 +++++++++---------- .../Components/Chat/ChatManagerFactory.cs | 23 ++++- .../Components/Chat/IChatManager.cs | 10 +- .../Chat/Providers/DiscordProvider.cs | 51 ++++------ .../Components/Chat/Providers/IProvider.cs | 23 +++-- .../Chat/Providers/IProviderFactory.cs | 2 +- .../Components/Chat/Providers/IrcProvider.cs | 79 +++++++-------- .../Components/Chat/Providers/Provider.cs | 80 ++++++++++++---- .../Chat/Providers/ProviderFactory.cs | 44 +++++---- .../Components/Instance.cs | 30 +----- .../Components/InstanceFactory.cs | 5 +- .../Components/InstanceManager.cs | 5 +- .../Components/Session/ISessionController.cs | 9 +- .../Components/Session/SessionController.cs | 13 +-- .../Components/Watchdog/BasicWatchdog.cs | 11 +-- .../Components/Watchdog/IWatchdog.cs | 4 +- .../Components/Watchdog/PosixWatchdog.cs | 6 +- .../Watchdog/PosixWatchdogFactory.cs | 7 +- .../Components/Watchdog/WatchdogBase.cs | 26 +---- .../Components/Watchdog/WatchdogFactory.cs | 12 +-- .../Components/Watchdog/WindowsWatchdog.cs | 6 +- .../Watchdog/WindowsWatchdogFactory.cs | 7 +- .../DatabaseCollectionExtensions.cs | 31 ++++++ src/Tgstation.Server.Host/Jobs/IJobManager.cs | 13 ++- src/Tgstation.Server.Host/Jobs/JobManager.cs | 68 ++++++++----- .../Chat/Providers/TestDiscordProvider.cs | 66 +++++++++---- .../Tgstation.Server.Tests/IntegrationTest.cs | 8 +- 28 files changed, 401 insertions(+), 345 deletions(-) create mode 100644 src/Tgstation.Server.Host/Extensions/DatabaseCollectionExtensions.cs diff --git a/src/Tgstation.Server.Api/Models/ErrorCode.cs b/src/Tgstation.Server.Api/Models/ErrorCode.cs index 9a350f2377..174de92a6f 100644 --- a/src/Tgstation.Server.Api/Models/ErrorCode.cs +++ b/src/Tgstation.Server.Api/Models/ErrorCode.cs @@ -1,4 +1,4 @@ -using System; +using System; using System.ComponentModel; namespace Tgstation.Server.Api.Models @@ -557,6 +557,12 @@ namespace Tgstation.Server.Api.Models /// Attempted to perform an instance operation with an offline instance. /// [Description("The instance associated with the operation is currently offline!")] - InstanceOffline + InstanceOffline, + + /// + /// An attempt to connect a chat bot failed. + /// + [Description("Failed to connect chat bot!")] + ChatCannotConnectProvider } -} \ No newline at end of file +} diff --git a/src/Tgstation.Server.Host/Components/Chat/ChatManager.cs b/src/Tgstation.Server.Host/Components/Chat/ChatManager.cs index dafe18f360..05f2c19ee4 100644 --- a/src/Tgstation.Server.Host/Components/Chat/ChatManager.cs +++ b/src/Tgstation.Server.Host/Components/Chat/ChatManager.cs @@ -1,4 +1,4 @@ -using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Logging; using Newtonsoft.Json; using Serilog.Context; using System; @@ -117,11 +117,6 @@ namespace Tgstation.Server.Host.Components.Chat /// long messagesProcessed; - /// - /// If has been called - /// - bool started; - /// /// Construct a /// @@ -159,13 +154,13 @@ namespace Tgstation.Server.Host.Components.Chat } /// - public void Dispose() + public async ValueTask DisposeAsync() { logger.LogTrace("Disposing..."); restartRegistration.Dispose(); handlerCts.Dispose(); foreach (var I in providers) - I.Value.Dispose(); + await I.Value.DisposeAsync().ConfigureAwait(false); } /// @@ -206,6 +201,21 @@ namespace Tgstation.Server.Host.Components.Chat return provider; } + async Task RemapProvider(IProvider provider, CancellationToken cancellationToken) + { + logger.LogTrace("Remapping channels for provider reconnection..."); + IEnumerable channelsToMap; + long providerId; + lock (providers) + providerId = providers.Where(x => x.Value == provider).Select(x => x.Key).First(); + + lock (activeChatBots) + channelsToMap = activeChatBots.FirstOrDefault(x => x.Id == providerId)?.Channels; + + if (channelsToMap?.Any() ?? false) + await ChangeChannels(providerId, channelsToMap, cancellationToken).ConfigureAwait(false); + } + /// /// Processes a /// @@ -213,26 +223,20 @@ namespace Tgstation.Server.Host.Components.Chat /// The to process. If , this indicates the provider reconnected. /// The for the operation /// A representing the running operation - #pragma warning disable CA1502 +#pragma warning disable CA1502 async Task ProcessMessage(IProvider provider, Message message, CancellationToken cancellationToken) - #pragma warning restore CA1502 +#pragma warning restore CA1502 { + if (!provider.Connected) + { + logger.LogTrace("Abort message processing because provider is disconnected!"); + return; + } + // provider reconnected, remap channels. if (message == null) { - logger.LogTrace("Remapping channels for provider reconnection..."); - IEnumerable channelsToMap; - lock (activeChatBots) - channelsToMap = activeChatBots.FirstOrDefault()?.Channels; - - if (channelsToMap?.Any() ?? false) - { - long providerId; - lock (providers) - providerId = providers.Where(x => x.Value == provider).Select(x => x.Key).First(); - await ChangeChannels(providerId, channelsToMap, cancellationToken).ConfigureAwait(false); - } - + await RemapProvider(provider, cancellationToken).ConfigureAwait(false); return; } @@ -243,9 +247,6 @@ namespace Tgstation.Server.Host.Components.Chat var enumerable = mappedChannels.Where(x => x.Value.ProviderId == providerId && x.Value.ProviderChannelId == message.User.Channel.RealId); if (message.User.Channel.IsPrivateChannel) lock (mappedChannels) - { - if (!provider.Connected) - return; if (!enumerable.Any()) { ulong newId; @@ -267,7 +268,6 @@ namespace Tgstation.Server.Host.Components.Chat } else message.User.Channel.RealId = enumerable.First().Key; - } else { // need to add tag and isAdminChannel @@ -285,7 +285,9 @@ namespace Tgstation.Server.Host.Components.Chat address = address.ToUpperInvariant(); - var addressed = address == CommonMention.ToUpperInvariant() || address == provider.BotMention.ToUpperInvariant(); + var addressed = + address == CommonMention.ToUpperInvariant() + || address == provider.BotMention.ToUpperInvariant(); // no mention if (!addressed && !message.User.Channel.IsPrivateChannel) @@ -406,7 +408,7 @@ namespace Tgstation.Server.Host.Components.Chat while (!cancellationToken.IsCancellationRequested) { // prune disconnected providers - foreach (var I in messageTasks.Where(x => !x.Key.Connected).ToList()) + foreach (var I in messageTasks.Where(x => !x.Key.Disposed).ToList()) messageTasks.Remove(I.Key); // add new ones @@ -415,7 +417,7 @@ namespace Tgstation.Server.Host.Components.Chat updatedTask = connectionsUpdated.Task; lock (providers) foreach (var I in providers) - if (I.Value.Connected && !messageTasks.ContainsKey(I.Value)) + if (!messageTasks.ContainsKey(I.Value)) messageTasks.Add(I.Value, I.Value.NextMessage(cancellationToken)); if (messageTasks.Count == 0) @@ -521,7 +523,7 @@ namespace Tgstation.Server.Host.Components.Chat } /// - public async Task ChangeSettings(ChatBot newSettings, CancellationToken cancellationToken) + public async Task ChangeSettings(Models.ChatBot newSettings, CancellationToken cancellationToken) { if (newSettings == null) throw new ArgumentNullException(nameof(newSettings)); @@ -537,7 +539,7 @@ namespace Tgstation.Server.Host.Components.Chat } finally { - p.Dispose(); + await p.DisposeAsync().ConfigureAwait(false); } } @@ -565,30 +567,23 @@ namespace Tgstation.Server.Host.Components.Chat await disconnectTask.ConfigureAwait(false); - if (started) + lock (synchronizationLock) { - if (newSettings.Enabled.Value) - await provider.Connect(cancellationToken).ConfigureAwait(false); - lock (synchronizationLock) - { - // same thread shennanigans - var oldOne = connectionsUpdated; - connectionsUpdated = new TaskCompletionSource(); - oldOne.SetResult(null); - } + // same thread shennanigans + var oldOne = connectionsUpdated; + connectionsUpdated = new TaskCompletionSource(); + oldOne.SetResult(null); } - Task reconnectionUpdateTask = Task.CompletedTask; + var reconnectionUpdateTask = provider?.SetReconnectInterval( + newSettings.ReconnectionInterval.Value, + newSettings.Enabled.Value) + ?? Task.CompletedTask; lock (activeChatBots) { var originalChatBot = activeChatBots.FirstOrDefault(bot => bot.Id == newSettings.Id); if (originalChatBot != null) - { - if (originalChatBot.ReconnectionInterval != newSettings.ReconnectionInterval) - reconnectionUpdateTask = provider.SetReconnectInterval(newSettings.ReconnectionInterval.Value); - activeChatBots.Remove(originalChatBot); - } activeChatBots.Add(new Models.ChatBot { @@ -716,10 +711,8 @@ namespace Tgstation.Server.Host.Components.Chat builtinCommands.Add(I.Name.ToUpperInvariant(), I); var initialChatBots = activeChatBots.ToList(); await Task.WhenAll(initialChatBots.Select(x => ChangeSettings(x, cancellationToken))).ConfigureAwait(false); - await Task.WhenAll(providers.Select(x => x.Value).Select(x => x.Connect(cancellationToken))).ConfigureAwait(false); await Task.WhenAll(initialChatBots.Select(x => ChangeChannels(x.Id, x.Channels, cancellationToken))).ConfigureAwait(false); chatHandler = MonitorMessages(handlerCts.Token); - started = true; } /// @@ -774,7 +767,7 @@ namespace Tgstation.Server.Host.Components.Chat } finally { - provider.Dispose(); + await provider.DisposeAsync().ConfigureAwait(false); } } diff --git a/src/Tgstation.Server.Host/Components/Chat/ChatManagerFactory.cs b/src/Tgstation.Server.Host/Components/Chat/ChatManagerFactory.cs index 0dc785c687..0ca59ef63c 100644 --- a/src/Tgstation.Server.Host/Components/Chat/ChatManagerFactory.cs +++ b/src/Tgstation.Server.Host/Components/Chat/ChatManagerFactory.cs @@ -1,6 +1,7 @@ -using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Logging; using System; using System.Collections.Generic; +using System.Linq; using Tgstation.Server.Host.Components.Chat.Commands; using Tgstation.Server.Host.Components.Chat.Providers; using Tgstation.Server.Host.Core; @@ -38,7 +39,11 @@ namespace Tgstation.Server.Host.Components.Chat /// The value of /// The value of /// The value of - public ChatManagerFactory(IProviderFactory providerFactory, IServerControl serverControl, IAsyncDelayer asyncDelayer, ILoggerFactory loggerFactory) + public ChatManagerFactory( + IProviderFactory providerFactory, + IServerControl serverControl, + IAsyncDelayer asyncDelayer, + ILoggerFactory loggerFactory) { this.providerFactory = providerFactory ?? throw new ArgumentNullException(nameof(providerFactory)); this.serverControl = serverControl ?? throw new ArgumentNullException(nameof(serverControl)); @@ -47,6 +52,18 @@ namespace Tgstation.Server.Host.Components.Chat } /// - public IChatManager CreateChatManager(IIOManager ioManager, ICommandFactory commandFactory, IEnumerable initialChatBots) => new ChatManager(providerFactory, ioManager, commandFactory, serverControl, asyncDelayer, loggerFactory, loggerFactory.CreateLogger(), initialChatBots); + public IChatManager CreateChatManager( + IIOManager ioManager, + ICommandFactory commandFactory, + IEnumerable initialChatBots) + => new ChatManager( + providerFactory, + ioManager, + commandFactory, + serverControl, + asyncDelayer, + loggerFactory, + loggerFactory.CreateLogger(), + initialChatBots.Where(x => x.Enabled.Value)); } } diff --git a/src/Tgstation.Server.Host/Components/Chat/IChatManager.cs b/src/Tgstation.Server.Host/Components/Chat/IChatManager.cs index 062e8fe1bd..d9a37cbe18 100644 --- a/src/Tgstation.Server.Host/Components/Chat/IChatManager.cs +++ b/src/Tgstation.Server.Host/Components/Chat/IChatManager.cs @@ -1,4 +1,4 @@ -using Microsoft.Extensions.Hosting; +using Microsoft.Extensions.Hosting; using System; using System.Collections.Generic; using System.Threading; @@ -10,7 +10,7 @@ namespace Tgstation.Server.Host.Components.Chat /// /// For managing connected chat services /// - public interface IChatManager : IHostedService, IDisposable + public interface IChatManager : IHostedService, IAsyncDisposable { /// /// Registers a to use @@ -21,10 +21,10 @@ namespace Tgstation.Server.Host.Components.Chat /// /// Change chat settings. If the is not currently in use, a new connection will be made instead /// - /// The new + /// The new /// The for the operation /// A representing the running operation. Will complete immediately if the property of is - Task ChangeSettings(ChatBot newSettings, CancellationToken cancellationToken); + Task ChangeSettings(Models.ChatBot newSettings, CancellationToken cancellationToken); /// /// Disconnects and deletes a given connection @@ -87,4 +87,4 @@ namespace Tgstation.Server.Host.Components.Chat /// A new . IChatTrackingContext CreateTrackingContext(); } -} \ No newline at end of file +} diff --git a/src/Tgstation.Server.Host/Components/Chat/Providers/DiscordProvider.cs b/src/Tgstation.Server.Host/Components/Chat/Providers/DiscordProvider.cs index 5d98784dbb..e41de6012a 100644 --- a/src/Tgstation.Server.Host/Components/Chat/Providers/DiscordProvider.cs +++ b/src/Tgstation.Server.Host/Components/Chat/Providers/DiscordProvider.cs @@ -1,4 +1,4 @@ -using Discord; +using Discord; using Discord.WebSocket; using Microsoft.Extensions.Logging; using System; @@ -6,6 +6,8 @@ using System.Collections.Generic; using System.Linq; using System.Threading; using System.Threading.Tasks; +using Tgstation.Server.Api.Models; +using Tgstation.Server.Host.Jobs; using Tgstation.Server.Host.Models; using Tgstation.Server.Host.System; @@ -30,6 +32,11 @@ namespace Tgstation.Server.Host.Components.Chat.Providers } } + /// + /// Gets the Discord bot token. + /// + string BotToken => ChatBot.ConnectionString; + /// /// The for the . /// @@ -40,11 +47,6 @@ namespace Tgstation.Server.Host.Components.Chat.Providers /// readonly DiscordSocketClient client; - /// - /// The token used for connecting to discord - /// - readonly string botToken; - /// /// of mapped s /// @@ -60,30 +62,28 @@ namespace Tgstation.Server.Host.Components.Chat.Providers /// /// Construct a /// + /// The for the . /// The value of . - /// The value of - /// The value of - /// The initial reconnect interval in minutes. + /// The for the . + /// The for the . public DiscordProvider( + IJobManager jobManager, IAssemblyInformationProvider assemblyInformationProvider, ILogger logger, - string botToken, - uint reconnectInterval) - : base(logger, reconnectInterval) + Models.ChatBot chatBot) + : base(jobManager, logger, chatBot) { this.assemblyInformationProvider = assemblyInformationProvider ?? throw new ArgumentNullException(nameof(assemblyInformationProvider)); - this.botToken = botToken ?? throw new ArgumentNullException(nameof(botToken)); client = new DiscordSocketClient(); client.MessageReceived += Client_MessageReceived; mappedChannels = new List(); } /// - public override void Dispose() + public override async ValueTask DisposeAsync() { + await base.DisposeAsync().ConfigureAwait(false); client.Dispose(); - - base.Dispose(); } /// @@ -135,18 +135,12 @@ namespace Tgstation.Server.Host.Components.Chat.Providers } /// - public override async Task Connect(CancellationToken cancellationToken) + protected override async Task Connect(CancellationToken cancellationToken) { Logger.LogTrace("Connecting..."); - if (Connected) - { - Logger.LogTrace("Already connected not doing connection attempt!"); - return true; - } - try { - await client.LoginAsync(TokenType.Bot, botToken, true).ConfigureAwait(false); + await client.LoginAsync(TokenType.Bot, BotToken, true).ConfigureAwait(false); Logger.LogTrace("Logged in."); cancellationToken.ThrowIfCancellationRequested(); @@ -171,11 +165,8 @@ namespace Tgstation.Server.Host.Components.Chat.Providers } catch (Exception e) { - Logger.LogWarning("Error connecting to Discord: {0}", e); - return false; + throw new JobException(ErrorCode.ChatCannotConnectProvider, e); } - - return true; } /// @@ -190,9 +181,9 @@ namespace Tgstation.Server.Host.Components.Chat.Providers try { + cancellationToken.ThrowIfCancellationRequested(); await client.StopAsync().ConfigureAwait(false); Logger.LogTrace("Stopped."); - cancellationToken.ThrowIfCancellationRequested(); await client.LogoutAsync().ConfigureAwait(false); Logger.LogDebug("Disconnected!"); } @@ -278,7 +269,7 @@ namespace Tgstation.Server.Host.Components.Chat.Providers /// public override async Task> SendUpdateMessage( - RevisionInformation revisionInformation, + Models.RevisionInformation revisionInformation, Version byondVersion, DateTimeOffset? estimatedCompletionTime, string gitHubOwner, diff --git a/src/Tgstation.Server.Host/Components/Chat/Providers/IProvider.cs b/src/Tgstation.Server.Host/Components/Chat/Providers/IProvider.cs index cca006f663..95264c0f34 100644 --- a/src/Tgstation.Server.Host/Components/Chat/Providers/IProvider.cs +++ b/src/Tgstation.Server.Host/Components/Chat/Providers/IProvider.cs @@ -1,4 +1,4 @@ -using System; +using System; using System.Collections.Generic; using System.Threading; using System.Threading.Tasks; @@ -9,13 +9,18 @@ namespace Tgstation.Server.Host.Components.Chat.Providers /// /// For interacting with a chat service /// - interface IProvider : IDisposable + interface IProvider : IAsyncDisposable { /// - /// If the + /// If the is currently connected. /// bool Connected { get; } + /// + /// If the was disposed. + /// + bool Disposed { get; } + /// /// The that indicates the was mentioned /// @@ -29,13 +34,6 @@ namespace Tgstation.Server.Host.Components.Chat.Providers /// Note that private messages will come in the form of s not returned in . Do not the on continuations run from the returned . Task NextMessage(CancellationToken cancellationToken); - /// - /// Attempt to connect the - /// - /// The for the operation - /// A resulting in on success, otherwise - Task Connect(CancellationToken cancellationToken); - /// /// Gracefully disconnects the provider. Permanently stops the reconnection timer. /// @@ -61,11 +59,12 @@ namespace Tgstation.Server.Host.Components.Chat.Providers Task SendMessage(ulong channelId, string message, CancellationToken cancellationToken); /// - /// Set the interval at which the provider tries to reconnect. + /// Set the interval at which the provider starts jobs to try to reconnect. /// /// The reconnection interval in minutes. + /// If a connection attempt should be made now. /// A representing the running operation. - Task SetReconnectInterval(uint reconnectInterval); + Task SetReconnectInterval(uint reconnectInterval, bool connectNow); /// /// Send the message for a deployment. diff --git a/src/Tgstation.Server.Host/Components/Chat/Providers/IProviderFactory.cs b/src/Tgstation.Server.Host/Components/Chat/Providers/IProviderFactory.cs index 91a83eeb96..9350efd7c6 100644 --- a/src/Tgstation.Server.Host/Components/Chat/Providers/IProviderFactory.cs +++ b/src/Tgstation.Server.Host/Components/Chat/Providers/IProviderFactory.cs @@ -1,4 +1,4 @@ -using Tgstation.Server.Api.Models.Internal; +using Tgstation.Server.Host.Models; namespace Tgstation.Server.Host.Components.Chat.Providers { diff --git a/src/Tgstation.Server.Host/Components/Chat/Providers/IrcProvider.cs b/src/Tgstation.Server.Host/Components/Chat/Providers/IrcProvider.cs index b29eaf15af..5a5d98cde4 100644 --- a/src/Tgstation.Server.Host/Components/Chat/Providers/IrcProvider.cs +++ b/src/Tgstation.Server.Host/Components/Chat/Providers/IrcProvider.cs @@ -1,4 +1,4 @@ -using Meebey.SmartIrc4net; +using Meebey.SmartIrc4net; using Microsoft.Extensions.Logging; using System; using System.Collections.Generic; @@ -10,6 +10,7 @@ using System.Threading.Tasks; using Tgstation.Server.Api.Models; using Tgstation.Server.Host.Core; using Tgstation.Server.Host.Extensions; +using Tgstation.Server.Host.Jobs; using Tgstation.Server.Host.System; namespace Tgstation.Server.Host.Components.Chat.Providers @@ -90,45 +91,33 @@ namespace Tgstation.Server.Host.Components.Chat.Providers /// /// Construct an /// + /// The for the provider. /// The to get the from /// The value of - /// The value of logger - /// The value of - /// The value of - /// The value of - /// The value of - /// The value of - /// The initial reconnect interval in minutes. - /// If should be used + /// The for the . + /// The for the . public IrcProvider( + IJobManager jobManager, IAssemblyInformationProvider assemblyInformationProvider, IAsyncDelayer asyncDelayer, ILogger logger, - string address, - ushort port, - string nickname, - string password, - IrcPasswordType? passwordType, - uint reconnectInterval, - bool useSsl) - : base(logger, reconnectInterval) + Models.ChatBot chatBot) + : base(jobManager, logger, chatBot) { if (assemblyInformationProvider == null) throw new ArgumentNullException(nameof(assemblyInformationProvider)); this.asyncDelayer = asyncDelayer ?? throw new ArgumentNullException(nameof(asyncDelayer)); - this.address = address ?? throw new ArgumentNullException(nameof(address)); - this.port = port; - this.nickname = nickname ?? throw new ArgumentNullException(nameof(nickname)); + var builder = chatBot.CreateConnectionStringBuilder(); + if (builder == null || !builder.Valid || !(builder is IrcConnectionStringBuilder ircBuilder)) + throw new InvalidOperationException("Invalid ChatConnectionStringBuilder!"); - if (passwordType.HasValue && password == null) - throw new ArgumentNullException(nameof(password)); + address = ircBuilder.Address; + port = ircBuilder.Port.Value; + nickname = ircBuilder.Nickname; - if (password != null && !passwordType.HasValue) - throw new ArgumentNullException(nameof(passwordType)); - - this.password = password; - this.passwordType = passwordType; + password = ircBuilder.Password; + passwordType = ircBuilder.PasswordType.Value; client = new IrcFeatures { @@ -143,9 +132,9 @@ namespace Tgstation.Server.Host.Components.Chat.Providers ActiveChannelSyncing = true, AutoNickHandling = true, CtcpVersion = assemblyInformationProvider.VersionString, - UseSsl = useSsl + UseSsl = ircBuilder.UseSsl.Value }; - if (useSsl) + if (ircBuilder.UseSsl.Value) client.ValidateServerCertificate = true; // dunno if it defaults to that or what client.OnChannelMessage += Client_OnChannelMessage; @@ -154,19 +143,13 @@ namespace Tgstation.Server.Host.Components.Chat.Providers channelIdMap = new Dictionary(); queryChannelIdMap = new Dictionary(); channelIdCounter = 1; - disconnecting = false; } /// - public override void Dispose() + public override async ValueTask DisposeAsync() { - if (Connected) - { - disconnecting = true; - client.Disconnect(); // just closes the socket - } - - base.Dispose(); + await base.DisposeAsync().ConfigureAwait(false); + await HardDisconnect().ConfigureAwait(false); } /// @@ -247,7 +230,7 @@ namespace Tgstation.Server.Host.Components.Chat.Providers void Client_OnChannelMessage(object sender, IrcEventArgs e) => HandleMessage(e, false); /// - public override Task Connect(CancellationToken cancellationToken) => Task.Factory.StartNew(() => + protected override Task Connect(CancellationToken cancellationToken) => Task.Factory.StartNew(() => { disconnecting = false; lock (client) @@ -338,11 +321,8 @@ namespace Tgstation.Server.Host.Components.Chat.Providers } catch (Exception e) { - Logger.LogWarning("Unable to connect to IRC: {0}", e); - return false; + throw new JobException(ErrorCode.ChatCannotConnectProvider, e); } - - return true; }, cancellationToken, TaskCreationOptions.LongRunning, TaskScheduler.Current); /// @@ -363,8 +343,7 @@ namespace Tgstation.Server.Host.Components.Chat.Providers Logger.LogWarning("Error quitting IRC: {0}", e); } }, cancellationToken, TaskCreationOptions.LongRunning, TaskScheduler.Current).ConfigureAwait(false); - Dispose(); - await listenTask.ConfigureAwait(false); + await HardDisconnect().ConfigureAwait(false); } catch (OperationCanceledException) { @@ -376,6 +355,16 @@ namespace Tgstation.Server.Host.Components.Chat.Providers } } + async Task HardDisconnect() + { + if (!Connected) + return; + + disconnecting = true; + client.Disconnect(); + await listenTask.ConfigureAwait(false); + } + /// public override Task> MapChannels( IEnumerable channels, diff --git a/src/Tgstation.Server.Host/Components/Chat/Providers/Provider.cs b/src/Tgstation.Server.Host/Components/Chat/Providers/Provider.cs index 070db5a8ef..ea03fea802 100644 --- a/src/Tgstation.Server.Host/Components/Chat/Providers/Provider.cs +++ b/src/Tgstation.Server.Host/Components/Chat/Providers/Provider.cs @@ -1,8 +1,10 @@ -using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Logging; using System; using System.Collections.Generic; using System.Threading; using System.Threading.Tasks; +using Tgstation.Server.Api.Rights; +using Tgstation.Server.Host.Jobs; using Tgstation.Server.Host.Models; namespace Tgstation.Server.Host.Components.Chat.Providers @@ -10,10 +12,20 @@ namespace Tgstation.Server.Host.Components.Chat.Providers /// abstract class Provider : IProvider { + /// + /// The the is for. + /// + protected ChatBot ChatBot { get; } + /// /// The for the . /// - protected ILogger Logger { get; } + protected ILogger Logger { get; } + + /// + /// The for the . + /// + readonly IJobManager jobManager; /// /// of received s @@ -43,18 +55,20 @@ namespace Tgstation.Server.Host.Components.Chat.Providers /// /// Construct a /// + /// The value of . /// The value of . - /// The initial reconnection interval. - protected Provider(ILogger logger, uint reconnectInterval) + /// The value of . + protected Provider(IJobManager jobManager, ILogger logger, ChatBot chatBot) { + this.jobManager = jobManager ?? throw new ArgumentNullException(nameof(jobManager)); Logger = logger ?? throw new ArgumentNullException(nameof(logger)); + ChatBot = chatBot ?? throw new ArgumentNullException(nameof(chatBot)); messageQueue = new Queue(); nextMessage = new TaskCompletionSource(); reconnectTaskLock = new object(); - SetReconnectInterval(reconnectInterval).GetAwaiter().GetResult(); logger.LogTrace("Created."); } @@ -64,6 +78,9 @@ namespace Tgstation.Server.Host.Components.Chat.Providers /// public abstract string BotMention { get; } + /// + public bool Disposed { get; private set; } + /// /// Queues a for /// @@ -78,14 +95,19 @@ namespace Tgstation.Server.Host.Components.Chat.Providers } /// - public virtual void Dispose() + public virtual async ValueTask DisposeAsync() { - StopReconnectionTimer().GetAwaiter().GetResult(); + Disposed = true; + await StopReconnectionTimer().ConfigureAwait(false); Logger.LogTrace("Disposed"); } - /// - public abstract Task Connect(CancellationToken cancellationToken); + /// + /// Attempt to connect the . + /// + /// The for the operation. + /// A representing the running operation. + protected abstract Task Connect(CancellationToken cancellationToken); /// /// Gracefully disconnects the provider. @@ -97,8 +119,9 @@ namespace Tgstation.Server.Host.Components.Chat.Providers /// public async Task Disconnect(CancellationToken cancellationToken) { + if(Connected) + await DisconnectImpl(cancellationToken).ConfigureAwait(false); await StopReconnectionTimer().ConfigureAwait(false); - await DisconnectImpl(cancellationToken).ConfigureAwait(false); } /// @@ -141,7 +164,7 @@ namespace Tgstation.Server.Host.Components.Chat.Providers } /// - public Task SetReconnectInterval(uint reconnectInterval) + public Task SetReconnectInterval(uint reconnectInterval, bool connectNow) { if (reconnectInterval == 0) throw new ArgumentOutOfRangeException(nameof(reconnectInterval), reconnectInterval, "Reconnect interval cannot be zero!"); @@ -151,7 +174,7 @@ namespace Tgstation.Server.Host.Components.Chat.Providers { stopOldTimerTask = StopReconnectionTimer(); reconnectCts = new CancellationTokenSource(); - reconnectTask = ReconnectionLoop(reconnectInterval, reconnectCts.Token); + reconnectTask = ReconnectionLoop(reconnectInterval, connectNow, reconnectCts.Token); } return stopOldTimerTask; @@ -161,21 +184,42 @@ namespace Tgstation.Server.Host.Components.Chat.Providers /// Creates a that will attempt to reconnect the every minutes. /// /// The amount of minutes to wait between reconnection attempts. + /// If a connection attempt should be immediately made. /// The for the operation. /// A representing the running operation. - async Task ReconnectionLoop(uint reconnectInterval, CancellationToken cancellationToken) + async Task ReconnectionLoop(uint reconnectInterval, bool connectNow, CancellationToken cancellationToken) { do { try { - await Task.Delay(TimeSpan.FromMinutes(reconnectInterval), cancellationToken).ConfigureAwait(false); + if (!connectNow) + await Task.Delay(TimeSpan.FromMinutes(reconnectInterval), cancellationToken).ConfigureAwait(false); + else + connectNow = false; if (!Connected) { - Logger.LogInformation("Attempting to reconnect provider..."); - await Disconnect(cancellationToken).ConfigureAwait(false); - if (await Connect(cancellationToken).ConfigureAwait(false)) - EnqueueMessage(null); + var job = new Job + { + Description = $"Reconnect chat bot: {ChatBot.Name}", + CancelRight = (ulong)ChatBotRights.WriteEnabled, + CancelRightsType = RightsType.ChatBots, + Instance = ChatBot.Instance + }; + + await jobManager.RegisterOperation( + job, + async (core, databaseContextFactory, paramJob, progressReporter, jobCancellationToken) => + { + await DisconnectImpl(jobCancellationToken).ConfigureAwait(false); + await Connect(jobCancellationToken).ConfigureAwait(false); + EnqueueMessage(null); + }, + cancellationToken) + .ConfigureAwait(false); + + // DCT: Always wait for the job to complete here + await jobManager.WaitForJobCompletion(job, null, cancellationToken, default).ConfigureAwait(false); } } catch (OperationCanceledException) diff --git a/src/Tgstation.Server.Host/Components/Chat/Providers/ProviderFactory.cs b/src/Tgstation.Server.Host/Components/Chat/Providers/ProviderFactory.cs index 3a23549cba..bdf1bbc0d0 100644 --- a/src/Tgstation.Server.Host/Components/Chat/Providers/ProviderFactory.cs +++ b/src/Tgstation.Server.Host/Components/Chat/Providers/ProviderFactory.cs @@ -1,8 +1,9 @@ -using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Logging; using System; using System.Globalization; using Tgstation.Server.Api.Models; using Tgstation.Server.Host.Core; +using Tgstation.Server.Host.Jobs; using Tgstation.Server.Host.System; namespace Tgstation.Server.Host.Components.Chat.Providers @@ -20,6 +21,11 @@ namespace Tgstation.Server.Host.Components.Chat.Providers /// readonly IAsyncDelayer asyncDelayer; + /// + /// The for the . + /// + readonly IJobManager jobManager; + /// /// The for the /// @@ -28,42 +34,42 @@ namespace Tgstation.Server.Host.Components.Chat.Providers /// /// Construct a /// + /// The value of . /// The value of /// The value of /// The value of public ProviderFactory( + IJobManager jobManager, IAssemblyInformationProvider assemblyInformationProvider, IAsyncDelayer asyncDelayer, ILoggerFactory loggerFactory) { + this.jobManager = jobManager ?? throw new ArgumentNullException(nameof(jobManager)); this.loggerFactory = loggerFactory ?? throw new ArgumentNullException(nameof(loggerFactory)); this.asyncDelayer = asyncDelayer ?? throw new ArgumentNullException(nameof(asyncDelayer)); this.assemblyInformationProvider = assemblyInformationProvider ?? throw new ArgumentNullException(nameof(assemblyInformationProvider)); } /// - public IProvider CreateProvider(Api.Models.Internal.ChatBot settings) + public IProvider CreateProvider(Models.ChatBot settings) { if (settings == null) throw new ArgumentNullException(nameof(settings)); - var builder = settings.CreateConnectionStringBuilder(); - if (builder == null || !builder.Valid) - throw new InvalidOperationException("Invalid ChatConnectionStringBuilder!"); - switch (settings.Provider) + return settings.Provider switch { - case ChatProvider.Irc: - var ircBuilder = (IrcConnectionStringBuilder)builder; - return new IrcProvider(assemblyInformationProvider, asyncDelayer, loggerFactory.CreateLogger(), ircBuilder.Address, ircBuilder.Port.Value, ircBuilder.Nickname, ircBuilder.Password, ircBuilder.PasswordType, settings.ReconnectionInterval.Value, ircBuilder.UseSsl.Value); - case ChatProvider.Discord: - var discordBuilder = (DiscordConnectionStringBuilder)builder; - return new DiscordProvider( - assemblyInformationProvider, - loggerFactory.CreateLogger(), - discordBuilder.BotToken, - settings.ReconnectionInterval.Value); - default: - throw new InvalidOperationException(String.Format(CultureInfo.InvariantCulture, "Invalid ChatProvider: {0}", settings.Provider)); - } + ChatProvider.Irc => new IrcProvider( + jobManager, + assemblyInformationProvider, + asyncDelayer, + loggerFactory.CreateLogger(), + settings), + ChatProvider.Discord => new DiscordProvider( + jobManager, + assemblyInformationProvider, + loggerFactory.CreateLogger(), + settings), + _ => throw new InvalidOperationException(String.Format(CultureInfo.InvariantCulture, "Invalid ChatProvider: {0}", settings.Provider)), + }; } } } diff --git a/src/Tgstation.Server.Host/Components/Instance.cs b/src/Tgstation.Server.Host/Components/Instance.cs index 8eba501262..8826d73929 100644 --- a/src/Tgstation.Server.Host/Components/Instance.cs +++ b/src/Tgstation.Server.Host/Components/Instance.cs @@ -1,4 +1,4 @@ -using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore; using Microsoft.Extensions.Logging; using Serilog.Context; using System; @@ -13,7 +13,6 @@ using Tgstation.Server.Host.Components.Deployment; using Tgstation.Server.Host.Components.Events; using Tgstation.Server.Host.Components.Repository; using Tgstation.Server.Host.Components.Watchdog; -using Tgstation.Server.Host.Database; using Tgstation.Server.Host.Jobs; using Tgstation.Server.Host.Models; @@ -46,11 +45,6 @@ namespace Tgstation.Server.Host.Components /// public IDreamMaker DreamMaker { get; } - /// - /// The for the - /// - readonly IDatabaseContextFactory databaseContextFactory; - /// /// The for the /// @@ -101,7 +95,6 @@ namespace Tgstation.Server.Host.Components /// The value of /// The value of /// The value of - /// The value of /// The value of /// The value of /// The value of @@ -115,7 +108,6 @@ namespace Tgstation.Server.Host.Components IChatManager chat, StaticFiles.IConfiguration configuration, - IDatabaseContextFactory databaseContextFactory, IDmbFactory dmbFactory, IJobManager jobManager, IEventConsumer eventConsumer, @@ -128,7 +120,6 @@ namespace Tgstation.Server.Host.Components Watchdog = watchdog ?? throw new ArgumentNullException(nameof(watchdog)); Chat = chat ?? throw new ArgumentNullException(nameof(chat)); Configuration = configuration ?? throw new ArgumentNullException(nameof(configuration)); - this.databaseContextFactory = databaseContextFactory ?? throw new ArgumentNullException(nameof(databaseContextFactory)); this.dmbFactory = dmbFactory ?? throw new ArgumentNullException(nameof(dmbFactory)); this.jobManager = jobManager ?? throw new ArgumentNullException(nameof(jobManager)); this.eventConsumer = eventConsumer ?? throw new ArgumentNullException(nameof(eventConsumer)); @@ -144,7 +135,7 @@ namespace Tgstation.Server.Host.Components { timerCts?.Dispose(); Configuration.Dispose(); - Chat.Dispose(); + await Chat.DisposeAsync().ConfigureAwait(false); await Watchdog.DisposeAsync().ConfigureAwait(false); dmbFactory.Dispose(); RepositoryManager.Dispose(); @@ -169,15 +160,6 @@ namespace Tgstation.Server.Host.Components await eventConsumer.HandleEvent(EventType.InstanceAutoUpdateStart, new List(), cancellationToken).ConfigureAwait(false); try { - User systemUser = null; - await databaseContextFactory.UseContext( - async (db) => systemUser = await db - .Users - .AsQueryable() - .Where(x => x.CanonicalName == User.CanonicalizeName(User.TgsSystemUserName)) - .FirstAsync(cancellationToken) - .ConfigureAwait(false)) - .ConfigureAwait(false); var repositoryUpdateJob = new Job { Instance = new Models.Instance @@ -186,8 +168,7 @@ namespace Tgstation.Server.Host.Components }, Description = "Scheduled repository update", CancelRightsType = RightsType.Repository, - CancelRight = (ulong)RepositoryRights.CancelPendingChanges, - StartedBy = systemUser + CancelRight = (ulong)RepositoryRights.CancelPendingChanges }; string deploySha = null; @@ -355,7 +336,7 @@ namespace Tgstation.Server.Host.Components }, cancellationToken).ConfigureAwait(false); // DCT: First token will cancel the job, second is for cancelling the cancellation, unwanted - await jobManager.WaitForJobCompletion(repositoryUpdateJob, systemUser, cancellationToken, default).ConfigureAwait(false); + await jobManager.WaitForJobCompletion(repositoryUpdateJob, null, cancellationToken, default).ConfigureAwait(false); if (deploySha == null) { @@ -372,7 +353,6 @@ namespace Tgstation.Server.Host.Components // finally set up the job var compileProcessJob = new Job { - StartedBy = systemUser, Instance = repositoryUpdateJob.Instance, Description = "Scheduled code deployment", CancelRightsType = RightsType.DreamMaker, @@ -393,7 +373,7 @@ namespace Tgstation.Server.Host.Components }, cancellationToken).ConfigureAwait(false); - await jobManager.WaitForJobCompletion(compileProcessJob, systemUser, default, cancellationToken).ConfigureAwait(false); + await jobManager.WaitForJobCompletion(compileProcessJob, null, default, cancellationToken).ConfigureAwait(false); } catch (OperationCanceledException) { diff --git a/src/Tgstation.Server.Host/Components/InstanceFactory.cs b/src/Tgstation.Server.Host/Components/InstanceFactory.cs index 4866ab4987..916b6950f4 100644 --- a/src/Tgstation.Server.Host/Components/InstanceFactory.cs +++ b/src/Tgstation.Server.Host/Components/InstanceFactory.cs @@ -1,4 +1,4 @@ -using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Logging; using System; using System.Threading; using System.Threading.Tasks; @@ -284,7 +284,6 @@ namespace Tgstation.Server.Host.Components watchdog, chatManager, configuration, - databaseContextFactory, dmbFactory, jobManager, eventConsumer, @@ -306,7 +305,7 @@ namespace Tgstation.Server.Host.Components } catch { - chatManager.Dispose(); + await chatManager.DisposeAsync().ConfigureAwait(false); throw; } } diff --git a/src/Tgstation.Server.Host/Components/InstanceManager.cs b/src/Tgstation.Server.Host/Components/InstanceManager.cs index 0d003e0a91..044169edc7 100644 --- a/src/Tgstation.Server.Host/Components/InstanceManager.cs +++ b/src/Tgstation.Server.Host/Components/InstanceManager.cs @@ -1,4 +1,4 @@ -using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore; using Microsoft.Extensions.Hosting; using Microsoft.Extensions.Logging; using Microsoft.Extensions.Options; @@ -424,6 +424,9 @@ namespace Tgstation.Server.Host.Components }) .ToList(); await Task.WhenAll(tasks).ConfigureAwait(false); + + jobManager.Activate(); + logger.LogInformation("Server ready!"); readyTcs.SetResult(null); } diff --git a/src/Tgstation.Server.Host/Components/Session/ISessionController.cs b/src/Tgstation.Server.Host/Components/Session/ISessionController.cs index 3a42879935..c85171fb56 100644 --- a/src/Tgstation.Server.Host/Components/Session/ISessionController.cs +++ b/src/Tgstation.Server.Host/Components/Session/ISessionController.cs @@ -1,8 +1,9 @@ -using System; +using System; using System.Threading; using System.Threading.Tasks; using Tgstation.Server.Host.Components.Deployment; using Tgstation.Server.Host.Components.Interop.Topic; +using Tgstation.Server.Host.Models; using Tgstation.Server.Host.System; namespace Tgstation.Server.Host.Components.Session @@ -33,9 +34,9 @@ namespace Tgstation.Server.Host.Components.Session Version DMApiVersion { get; } /// - /// The being used + /// Gets the associated with the . /// - IDmbProvider Dmb { get; } + CompileJob CompileJob { get; } /// /// The current port DreamDaemon is listening on @@ -108,7 +109,7 @@ namespace Tgstation.Server.Host.Components.Session void EnableCustomChatCommands(); /// - /// Replace with a given , disposing the old one. + /// Replace the in use with a given , disposing the old one. /// /// The new . void ReplaceDmbProvider(IDmbProvider newProvider); diff --git a/src/Tgstation.Server.Host/Components/Session/SessionController.cs b/src/Tgstation.Server.Host/Components/Session/SessionController.cs index a5c1560ff9..e790feb310 100644 --- a/src/Tgstation.Server.Host/Components/Session/SessionController.cs +++ b/src/Tgstation.Server.Host/Components/Session/SessionController.cs @@ -1,4 +1,4 @@ -using Byond.TopicSender; +using Byond.TopicSender; using Microsoft.Extensions.Logging; using Newtonsoft.Json; using Serilog.Context; @@ -39,14 +39,7 @@ namespace Tgstation.Server.Host.Components.Session } /// - public IDmbProvider Dmb - { - get - { - CheckDisposed(); - return reattachInformation.Dmb; - } - } + public Models.CompileJob CompileJob => reattachInformation.Dmb.CompileJob; /// public ushort? Port @@ -346,7 +339,7 @@ namespace Tgstation.Server.Host.Components.Session else if (reattachResponse.InteropResponse != null) logger.LogWarning( "DMAPI v{0} isn't returning the TGS custom commands list. Functionality added in v5.2.0.", - Dmb.CompileJob.DMApiVersion.Semver()); + CompileJob.DMApiVersion.Semver()); } } diff --git a/src/Tgstation.Server.Host/Components/Watchdog/BasicWatchdog.cs b/src/Tgstation.Server.Host/Components/Watchdog/BasicWatchdog.cs index 528fbb706f..9ccd92a28c 100644 --- a/src/Tgstation.Server.Host/Components/Watchdog/BasicWatchdog.cs +++ b/src/Tgstation.Server.Host/Components/Watchdog/BasicWatchdog.cs @@ -1,4 +1,4 @@ -using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Logging; using System; using System.Globalization; using System.Threading; @@ -9,7 +9,6 @@ using Tgstation.Server.Host.Components.Deployment; using Tgstation.Server.Host.Components.Events; using Tgstation.Server.Host.Components.Session; using Tgstation.Server.Host.Core; -using Tgstation.Server.Host.Database; using Tgstation.Server.Host.IO; using Tgstation.Server.Host.Jobs; @@ -23,9 +22,6 @@ namespace Tgstation.Server.Host.Components.Watchdog /// public sealed override bool AlphaIsActive => true; - /// - public sealed override Models.CompileJob ActiveCompileJob => Server?.Dmb.CompileJob; - /// public sealed override RebootState? RebootState => Server?.RebootState; @@ -46,7 +42,6 @@ namespace Tgstation.Server.Host.Components.Watchdog /// The for the . /// The for the . /// The for the . - /// The for the . /// The for the . /// The for the . /// The for the . @@ -61,7 +56,6 @@ namespace Tgstation.Server.Host.Components.Watchdog ISessionControllerFactory sessionControllerFactory, IDmbFactory dmbFactory, ISessionPersistor sessionPersistor, - IDatabaseContextFactory databaseContextFactory, IJobManager jobManager, IServerControl serverControl, IAsyncDelayer asyncDelayer, @@ -76,7 +70,6 @@ namespace Tgstation.Server.Host.Components.Watchdog sessionControllerFactory, dmbFactory, sessionPersistor, - databaseContextFactory, jobManager, serverControl, asyncDelayer, @@ -261,7 +254,7 @@ namespace Tgstation.Server.Host.Components.Watchdog protected virtual Task HandleNewDmbAvailable(CancellationToken cancellationToken) { gracefulRebootRequired = true; - if (Server.Dmb.CompileJob.DMApiVersion == null) + if (Server.CompileJob.DMApiVersion == null) return Chat.SendWatchdogMessage( "A new deployment has been made but cannot be applied automatically as the currently running server has no DMAPI. Please manually reboot the server to apply the update.", true, diff --git a/src/Tgstation.Server.Host/Components/Watchdog/IWatchdog.cs b/src/Tgstation.Server.Host/Components/Watchdog/IWatchdog.cs index 1b822a7e65..919f3e0cf9 100644 --- a/src/Tgstation.Server.Host/Components/Watchdog/IWatchdog.cs +++ b/src/Tgstation.Server.Host/Components/Watchdog/IWatchdog.cs @@ -1,4 +1,4 @@ -using Microsoft.Extensions.Hosting; +using Microsoft.Extensions.Hosting; using System; using System.Threading; using System.Threading.Tasks; @@ -25,7 +25,7 @@ namespace Tgstation.Server.Host.Components.Watchdog bool AlphaIsActive { get; } /// - /// The currently running on the server + /// Retrieves the currently running on the server /// Models.CompileJob ActiveCompileJob { get; } diff --git a/src/Tgstation.Server.Host/Components/Watchdog/PosixWatchdog.cs b/src/Tgstation.Server.Host/Components/Watchdog/PosixWatchdog.cs index 51b6c7a7fe..cc03802ded 100644 --- a/src/Tgstation.Server.Host/Components/Watchdog/PosixWatchdog.cs +++ b/src/Tgstation.Server.Host/Components/Watchdog/PosixWatchdog.cs @@ -1,4 +1,4 @@ -using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Logging; using System.Threading; using System.Threading.Tasks; using Tgstation.Server.Api.Models.Internal; @@ -7,7 +7,6 @@ using Tgstation.Server.Host.Components.Deployment; using Tgstation.Server.Host.Components.Events; using Tgstation.Server.Host.Components.Session; using Tgstation.Server.Host.Core; -using Tgstation.Server.Host.Database; using Tgstation.Server.Host.IO; using Tgstation.Server.Host.Jobs; @@ -30,7 +29,6 @@ namespace Tgstation.Server.Host.Components.Watchdog /// The for the . /// The for the . /// The for the . - /// The for the . /// The for the . /// The for the . /// The for the . @@ -47,7 +45,6 @@ namespace Tgstation.Server.Host.Components.Watchdog ISessionControllerFactory sessionControllerFactory, IDmbFactory dmbFactory, ISessionPersistor sessionPersistor, - IDatabaseContextFactory databaseContextFactory, IJobManager jobManager, IServerControl serverControl, IAsyncDelayer asyncDelayer, @@ -64,7 +61,6 @@ namespace Tgstation.Server.Host.Components.Watchdog sessionControllerFactory, dmbFactory, sessionPersistor, - databaseContextFactory, jobManager, serverControl, asyncDelayer, diff --git a/src/Tgstation.Server.Host/Components/Watchdog/PosixWatchdogFactory.cs b/src/Tgstation.Server.Host/Components/Watchdog/PosixWatchdogFactory.cs index 30a8169e97..279fa3b1e9 100644 --- a/src/Tgstation.Server.Host/Components/Watchdog/PosixWatchdogFactory.cs +++ b/src/Tgstation.Server.Host/Components/Watchdog/PosixWatchdogFactory.cs @@ -1,4 +1,4 @@ -using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Logging; using Microsoft.Extensions.Options; using System; using Tgstation.Server.Api.Models.Internal; @@ -8,7 +8,6 @@ using Tgstation.Server.Host.Components.Events; using Tgstation.Server.Host.Components.Session; 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; @@ -24,7 +23,6 @@ namespace Tgstation.Server.Host.Components.Watchdog /// /// The for the . /// The for the . - /// The for the . /// The for the . /// The for the . /// The for the . @@ -32,7 +30,6 @@ namespace Tgstation.Server.Host.Components.Watchdog public PosixWatchdogFactory( IServerControl serverControl, ILoggerFactory loggerFactory, - IDatabaseContextFactory databaseContextFactory, IJobManager jobManager, IAsyncDelayer asyncDelayer, ISymlinkFactory symlinkFactory, @@ -40,7 +37,6 @@ namespace Tgstation.Server.Host.Components.Watchdog : base( serverControl, loggerFactory, - databaseContextFactory, jobManager, asyncDelayer, symlinkFactory, @@ -63,7 +59,6 @@ namespace Tgstation.Server.Host.Components.Watchdog sessionControllerFactory, dmbFactory, sessionPersistor, - DatabaseContextFactory, JobManager, ServerControl, AsyncDelayer, diff --git a/src/Tgstation.Server.Host/Components/Watchdog/WatchdogBase.cs b/src/Tgstation.Server.Host/Components/Watchdog/WatchdogBase.cs index 2fca625767..bc6eeda484 100644 --- a/src/Tgstation.Server.Host/Components/Watchdog/WatchdogBase.cs +++ b/src/Tgstation.Server.Host/Components/Watchdog/WatchdogBase.cs @@ -1,4 +1,3 @@ -using Microsoft.EntityFrameworkCore; using Microsoft.Extensions.Logging; using Serilog.Context; using System; @@ -15,7 +14,6 @@ using Tgstation.Server.Host.Components.Events; using Tgstation.Server.Host.Components.Interop.Topic; using Tgstation.Server.Host.Components.Session; using Tgstation.Server.Host.Core; -using Tgstation.Server.Host.Database; using Tgstation.Server.Host.Extensions; using Tgstation.Server.Host.IO; using Tgstation.Server.Host.Jobs; @@ -42,15 +40,15 @@ namespace Tgstation.Server.Host.Components.Watchdog /// public abstract bool AlphaIsActive { get; } - /// - public abstract Models.CompileJob ActiveCompileJob { get; } - /// public DreamDaemonLaunchParameters ActiveLaunchParameters { get; protected set; } /// public DreamDaemonLaunchParameters LastLaunchParameters { get; protected set; } + /// + public Models.CompileJob ActiveCompileJob => GetActiveController()?.CompileJob; + /// public abstract RebootState? RebootState { get; } @@ -104,11 +102,6 @@ namespace Tgstation.Server.Host.Components.Watchdog /// readonly ISessionPersistor sessionPersistor; - /// - /// The for the - /// - readonly IDatabaseContextFactory databaseContextFactory; - /// /// The for the . /// @@ -176,7 +169,6 @@ namespace Tgstation.Server.Host.Components.Watchdog /// The value of /// The value of /// The value of - /// The value of /// The value of /// The to populate with /// The value of . @@ -191,7 +183,6 @@ namespace Tgstation.Server.Host.Components.Watchdog ISessionControllerFactory sessionControllerFactory, IDmbFactory dmbFactory, ISessionPersistor sessionPersistor, - IDatabaseContextFactory databaseContextFactory, IJobManager jobManager, IServerControl serverControl, IAsyncDelayer asyncDelayer, @@ -206,7 +197,6 @@ namespace Tgstation.Server.Host.Components.Watchdog SessionControllerFactory = sessionControllerFactory ?? throw new ArgumentNullException(nameof(sessionControllerFactory)); DmbFactory = dmbFactory ?? throw new ArgumentNullException(nameof(dmbFactory)); this.sessionPersistor = sessionPersistor ?? throw new ArgumentNullException(nameof(sessionPersistor)); - this.databaseContextFactory = databaseContextFactory ?? throw new ArgumentNullException(nameof(databaseContextFactory)); this.jobManager = jobManager ?? throw new ArgumentNullException(nameof(jobManager)); AsyncDelayer = asyncDelayer ?? throw new ArgumentNullException(nameof(asyncDelayer)); this.diagnosticsIOManager = diagnosticsIOManager ?? throw new ArgumentNullException(nameof(diagnosticsIOManager)); @@ -892,18 +882,8 @@ namespace Tgstation.Server.Host.Components.Watchdog if (!autoStart && reattachInfo == null) return; - Models.User systemUser = null; - await databaseContextFactory.UseContext( - async db => systemUser = await db - .Users - .AsQueryable() - .Where(x => x.CanonicalName == Models.User.CanonicalizeName(Models.User.TgsSystemUserName)) - .FirstAsync(cancellationToken) - .ConfigureAwait(false)) - .ConfigureAwait(false); var job = new Models.Job { - StartedBy = systemUser, Instance = new Models.Instance { Id = instance.Id diff --git a/src/Tgstation.Server.Host/Components/Watchdog/WatchdogFactory.cs b/src/Tgstation.Server.Host/Components/Watchdog/WatchdogFactory.cs index dbf3fe82e9..423892a249 100644 --- a/src/Tgstation.Server.Host/Components/Watchdog/WatchdogFactory.cs +++ b/src/Tgstation.Server.Host/Components/Watchdog/WatchdogFactory.cs @@ -1,4 +1,4 @@ -using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Logging; using Microsoft.Extensions.Options; using System; using Tgstation.Server.Api.Models.Internal; @@ -8,7 +8,6 @@ using Tgstation.Server.Host.Components.Events; using Tgstation.Server.Host.Components.Session; 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; @@ -27,11 +26,6 @@ namespace Tgstation.Server.Host.Components.Watchdog /// protected ILoggerFactory LoggerFactory { get; } - /// - /// The for the - /// - protected IDatabaseContextFactory DatabaseContextFactory { get; } - /// /// The for the /// @@ -52,21 +46,18 @@ namespace Tgstation.Server.Host.Components.Watchdog /// /// The value of /// The value of - /// The value of /// The value of /// The value of /// The containing the value of public WatchdogFactory( IServerControl serverControl, ILoggerFactory loggerFactory, - IDatabaseContextFactory databaseContextFactory, IJobManager jobManager, IAsyncDelayer asyncDelayer, IOptions generalConfigurationOptions) { ServerControl = serverControl ?? throw new ArgumentNullException(nameof(serverControl)); LoggerFactory = loggerFactory ?? throw new ArgumentNullException(nameof(loggerFactory)); - DatabaseContextFactory = databaseContextFactory ?? throw new ArgumentNullException(nameof(databaseContextFactory)); JobManager = jobManager ?? throw new ArgumentNullException(nameof(jobManager)); AsyncDelayer = asyncDelayer ?? throw new ArgumentNullException(nameof(asyncDelayer)); GeneralConfiguration = generalConfigurationOptions?.Value ?? throw new ArgumentNullException(nameof(generalConfigurationOptions)); @@ -88,7 +79,6 @@ namespace Tgstation.Server.Host.Components.Watchdog sessionControllerFactory, dmbFactory, sessionPersistor, - DatabaseContextFactory, JobManager, ServerControl, AsyncDelayer, diff --git a/src/Tgstation.Server.Host/Components/Watchdog/WindowsWatchdog.cs b/src/Tgstation.Server.Host/Components/Watchdog/WindowsWatchdog.cs index b719086eaa..2a084b150f 100644 --- a/src/Tgstation.Server.Host/Components/Watchdog/WindowsWatchdog.cs +++ b/src/Tgstation.Server.Host/Components/Watchdog/WindowsWatchdog.cs @@ -1,4 +1,4 @@ -using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Logging; using System; using System.Threading; using System.Threading.Tasks; @@ -8,7 +8,6 @@ using Tgstation.Server.Host.Components.Deployment; using Tgstation.Server.Host.Components.Events; using Tgstation.Server.Host.Components.Session; using Tgstation.Server.Host.Core; -using Tgstation.Server.Host.Database; using Tgstation.Server.Host.IO; using Tgstation.Server.Host.Jobs; @@ -51,7 +50,6 @@ namespace Tgstation.Server.Host.Components.Watchdog /// The for the . /// The for the . /// The for the . - /// The for the . /// The for the . /// The for the . /// The for the . @@ -68,7 +66,6 @@ namespace Tgstation.Server.Host.Components.Watchdog ISessionControllerFactory sessionControllerFactory, IDmbFactory dmbFactory, ISessionPersistor sessionPersistor, - IDatabaseContextFactory databaseContextFactory, IJobManager jobManager, IServerControl serverControl, IAsyncDelayer asyncDelayer, @@ -84,7 +81,6 @@ namespace Tgstation.Server.Host.Components.Watchdog sessionControllerFactory, dmbFactory, sessionPersistor, - databaseContextFactory, jobManager, serverControl, asyncDelayer, diff --git a/src/Tgstation.Server.Host/Components/Watchdog/WindowsWatchdogFactory.cs b/src/Tgstation.Server.Host/Components/Watchdog/WindowsWatchdogFactory.cs index 1e52e7ecad..a597a80997 100644 --- a/src/Tgstation.Server.Host/Components/Watchdog/WindowsWatchdogFactory.cs +++ b/src/Tgstation.Server.Host/Components/Watchdog/WindowsWatchdogFactory.cs @@ -1,4 +1,4 @@ -using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Logging; using Microsoft.Extensions.Options; using System; using Tgstation.Server.Api.Models.Internal; @@ -8,7 +8,6 @@ using Tgstation.Server.Host.Components.Events; using Tgstation.Server.Host.Components.Session; 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; @@ -29,7 +28,6 @@ namespace Tgstation.Server.Host.Components.Watchdog /// /// The for the . /// The for the . - /// The for the . /// The for the . /// The for the . /// The value of . @@ -37,7 +35,6 @@ namespace Tgstation.Server.Host.Components.Watchdog public WindowsWatchdogFactory( IServerControl serverControl, ILoggerFactory loggerFactory, - IDatabaseContextFactory databaseContextFactory, IJobManager jobManager, IAsyncDelayer asyncDelayer, ISymlinkFactory symlinkFactory, @@ -45,7 +42,6 @@ namespace Tgstation.Server.Host.Components.Watchdog : base( serverControl, loggerFactory, - databaseContextFactory, jobManager, asyncDelayer, generalConfigurationOptions) @@ -69,7 +65,6 @@ namespace Tgstation.Server.Host.Components.Watchdog sessionControllerFactory, dmbFactory, sessionPersistor, - DatabaseContextFactory, JobManager, ServerControl, AsyncDelayer, diff --git a/src/Tgstation.Server.Host/Extensions/DatabaseCollectionExtensions.cs b/src/Tgstation.Server.Host/Extensions/DatabaseCollectionExtensions.cs new file mode 100644 index 0000000000..6599830847 --- /dev/null +++ b/src/Tgstation.Server.Host/Extensions/DatabaseCollectionExtensions.cs @@ -0,0 +1,31 @@ +using Microsoft.EntityFrameworkCore; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; +using Tgstation.Server.Host.Database; +using Tgstation.Server.Host.Models; + +namespace Tgstation.Server.Host.Extensions +{ + /// + /// Extension methods for the . + /// + static class DatabaseCollectionExtensions + { + /// + /// Gets the unattached, unpopulated with the name . + /// + /// The of s to operate on. + /// The for the operation. + /// A resulting in the unattached TGS on success, on failure. + public static Task GetTgsUser(this IDatabaseCollection databaseCollection, CancellationToken cancellationToken) + => databaseCollection + .AsQueryable() + .Where(x => x.CanonicalName == User.CanonicalizeName(User.TgsSystemUserName)) + .Select(x => new User + { + Id = x.Id + }) + .FirstAsync(cancellationToken); + } +} diff --git a/src/Tgstation.Server.Host/Jobs/IJobManager.cs b/src/Tgstation.Server.Host/Jobs/IJobManager.cs index 526ca4a437..567d622d40 100644 --- a/src/Tgstation.Server.Host/Jobs/IJobManager.cs +++ b/src/Tgstation.Server.Host/Jobs/IJobManager.cs @@ -1,4 +1,4 @@ -using Microsoft.Extensions.Hosting; +using Microsoft.Extensions.Hosting; using System.Threading; using System.Threading.Tasks; using Tgstation.Server.Host.Models; @@ -20,7 +20,7 @@ namespace Tgstation.Server.Host.Jobs /// /// Registers a given and begins running it /// - /// The + /// The . Should at least have and . If is , the TGS user will be used. /// The for the . /// The for the operation /// A representing a running operation @@ -30,7 +30,7 @@ namespace Tgstation.Server.Host.Jobs /// Wait for a given to complete /// /// The to wait for - /// The to cancel the + /// The to cancel the . If the TGS user will be used. /// A that will cancel the /// The for the operation /// A representing the @@ -42,10 +42,15 @@ namespace Tgstation.Server.Host.Jobs /// Cancels a give /// /// The to cancel - /// The who cancelled the + /// The who cancelled the . If the TGS user will be used. /// If the operation should wait until the job exits before completing /// The for the operation /// A resulting in the updated if it was cancelled, if it couldn't be found. Task CancelJob(Job job, User user, bool blocking, CancellationToken cancellationToken); + + /// + /// Activate the . + /// + void Activate(); } } diff --git a/src/Tgstation.Server.Host/Jobs/JobManager.cs b/src/Tgstation.Server.Host/Jobs/JobManager.cs index 6ec0fdc7c3..ced1eb724b 100644 --- a/src/Tgstation.Server.Host/Jobs/JobManager.cs +++ b/src/Tgstation.Server.Host/Jobs/JobManager.cs @@ -1,4 +1,4 @@ -using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore; using Microsoft.Extensions.Logging; using Serilog.Context; using System; @@ -8,6 +8,7 @@ using System.Threading; using System.Threading.Tasks; using Tgstation.Server.Host.Components; using Tgstation.Server.Host.Database; +using Tgstation.Server.Host.Extensions; using Tgstation.Server.Host.Models; namespace Tgstation.Server.Host.Jobs @@ -35,6 +36,11 @@ namespace Tgstation.Server.Host.Jobs /// readonly Dictionary jobs; + /// + /// to delay starting jobs until the server is ready. + /// + readonly TaskCompletionSource activationTcs; + /// /// for various operations. /// @@ -52,6 +58,7 @@ namespace Tgstation.Server.Host.Jobs this.instanceCoreProvider = instanceCoreProvider ?? throw new ArgumentNullException(nameof(instanceCoreProvider)); this.logger = logger ?? throw new ArgumentNullException(nameof(logger)); jobs = new Dictionary(); + activationTcs = new TaskCompletionSource(); synchronizationLock = new object(); } @@ -89,7 +96,7 @@ namespace Tgstation.Server.Host.Jobs using (LogContext.PushProperty("Job", job.Id)) try { - void LogRegularException() => logger.LogDebug("Job {0} exited with error! Exception: {1}", job.Id, job.ExceptionDetails); + void LogException() => logger.LogDebug("Job {0} exited with error! Exception: {1}", job.Id, job.ExceptionDetails); try { var oldJob = job; @@ -102,6 +109,8 @@ namespace Tgstation.Server.Host.Jobs handler.Progress = progress; } + await activationTcs.Task.WithToken(cancellationToken).ConfigureAwait(false); + await operation( instanceCoreProvider.Value.GetInstance(oldJob.Instance), databaseContextFactory, @@ -120,20 +129,13 @@ namespace Tgstation.Server.Host.Jobs catch (JobException e) { job.ErrorCode = e.ErrorCode; - job.ExceptionDetails = e.Message; - LogRegularException(); - if (e.InnerException != null) - logger.LogDebug( - "Inner exception for job {0}: {1}", - job.Id, - e.InnerException is JobException - ? e.InnerException.Message - : e.InnerException.ToString()); + job.ExceptionDetails = String.IsNullOrWhiteSpace(e.Message) ? e.InnerException?.Message : e.Message; + LogException(); } catch (Exception e) { job.ExceptionDetails = e.ToString(); - LogRegularException(); + LogException(); } await databaseContextFactory.UseContext(async databaseContext => @@ -183,10 +185,16 @@ namespace Tgstation.Server.Host.Jobs }; databaseContext.Instances.Attach(job.Instance); - job.StartedBy = new User - { - Id = job.StartedBy.Id - }; + if (job.StartedBy == null) + job.StartedBy = await databaseContext + .Users + .GetTgsUser(cancellationToken) + .ConfigureAwait(false); + else + job.StartedBy = new User + { + Id = job.StartedBy.Id + }; databaseContext.Users.Attach(job.StartedBy); databaseContext.Jobs.Add(job); @@ -244,11 +252,14 @@ namespace Tgstation.Server.Host.Jobs /// public async Task StopAsync(CancellationToken cancellationToken) { - var joinTasks = jobs.Select(x => - { - x.Value.Cancel(); - return x.Value.Wait(cancellationToken); - }); + var joinTasks = jobs.Select(x => CancelJob( + new Job + { + Id = x.Key, + }, + null, + true, + cancellationToken)); await Task.WhenAll(joinTasks).ConfigureAwait(false); } @@ -257,8 +268,6 @@ namespace Tgstation.Server.Host.Jobs { if (job == null) throw new ArgumentNullException(nameof(job)); - if (user == null) - throw new ArgumentNullException(nameof(user)); JobHandler handler; try { @@ -273,6 +282,12 @@ namespace Tgstation.Server.Host.Jobs handler.Cancel(); // this will ensure the db update is only done once await databaseContextFactory.UseContext(async databaseContext => { + if (user == null) + { + user = await databaseContext.Users.GetTgsUser(cancellationToken).ConfigureAwait(false); + databaseContext.Users.Attach(user); + } + var updatedJob = new Job { Id = job.Id }; databaseContext.Jobs.Attach(job); var attachedUser = new User { Id = user.Id }; @@ -322,5 +337,12 @@ namespace Tgstation.Server.Host.Jobs if (cancelTask != null) await cancelTask.ConfigureAwait(false); } + + /// + public void Activate() + { + logger.LogTrace("Activating job manager..."); + activationTcs.SetResult(null); + } } } diff --git a/tests/Tgstation.Server.Host.Tests/Components/Chat/Providers/TestDiscordProvider.cs b/tests/Tgstation.Server.Host.Tests/Components/Chat/Providers/TestDiscordProvider.cs index 8cf32df3e6..a035eee34c 100644 --- a/tests/Tgstation.Server.Host.Tests/Components/Chat/Providers/TestDiscordProvider.cs +++ b/tests/Tgstation.Server.Host.Tests/Components/Chat/Providers/TestDiscordProvider.cs @@ -1,9 +1,12 @@ -using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Logging; using Microsoft.VisualStudio.TestTools.UnitTesting; using Moq; using System; +using System.Reflection; using System.Threading; using System.Threading.Tasks; +using Tgstation.Server.Host.Jobs; +using Tgstation.Server.Host.Models; using Tgstation.Server.Host.System; namespace Tgstation.Server.Host.Components.Chat.Providers.Tests @@ -11,52 +14,75 @@ namespace Tgstation.Server.Host.Components.Chat.Providers.Tests [TestClass] public sealed class TestDiscordProvider { - string testToken1; + ChatBot testToken1; + IJobManager mockJobManager; [TestInitialize] public void Initialize() { - testToken1 = Environment.GetEnvironmentVariable("TGS4_TEST_DISCORD_TOKEN_1"); + var actualToken = Environment.GetEnvironmentVariable("TGS4_TEST_DISCORD_TOKEN"); + if(!String.IsNullOrWhiteSpace(actualToken)) + testToken1 = new ChatBot + { + ConnectionString = actualToken, + ReconnectionInterval = 1 + }; + + var mockSetup = new Mock(); + mockSetup + .Setup(x => x.RegisterOperation(It.IsNotNull(), It.IsNotNull(), It.IsAny())) + .Callback((job, entrypoint, cancellationToken) => job.StartedBy ??= new User { }) + .Returns(Task.CompletedTask); + mockSetup + .Setup(x => x.WaitForJobCompletion(It.IsNotNull(), It.IsAny(), It.IsAny(), It.IsAny())) + .Returns(Task.CompletedTask); + mockJobManager = mockSetup.Object; } [TestMethod] - public void TestConstructionAndDisposal() + public async Task TestConstructionAndDisposal() { - Assert.ThrowsException(() => new DiscordProvider(null, null, null, 1)); + if (testToken1 == null) + Assert.Inconclusive("Required environment variable TGS4_TEST_DISCORD_TOKEN isn't set!"); + + Assert.ThrowsException(() => new DiscordProvider(null, null, null, null)); + Assert.ThrowsException(() => new DiscordProvider(mockJobManager, null, null, null)); var mockAss = new Mock(); - Assert.ThrowsException(() => new DiscordProvider(mockAss.Object, null, null, 1)); + Assert.ThrowsException(() => new DiscordProvider(mockJobManager, mockAss.Object, null, null)); var mockLogger = new Mock>(); - Assert.ThrowsException(() => new DiscordProvider(mockAss.Object, mockLogger.Object, null, 1)); - var mockToken = "asdf"; - Assert.ThrowsException(() => new DiscordProvider(mockAss.Object, mockLogger.Object, mockToken, 0)); - new DiscordProvider(mockAss.Object, mockLogger.Object, mockToken, 1).Dispose(); + Assert.ThrowsException(() => new DiscordProvider(mockJobManager, null, mockLogger.Object, null)); + await new DiscordProvider(mockJobManager, mockAss.Object, mockLogger.Object, testToken1).DisposeAsync(); } + static Task InvokeConnect(IProvider provider, CancellationToken cancellationToken = default) => (Task)provider.GetType().GetMethod("Connect", BindingFlags.Instance | BindingFlags.NonPublic).Invoke(provider, new object[] { cancellationToken }); + [TestMethod] public async Task TestConnectWithFakeTokenFails() { var mockLogger = new Mock>(); - using var provider = new DiscordProvider(Mock.Of(), mockLogger.Object, "asdf", 1); - Assert.IsFalse(await provider.Connect(default).ConfigureAwait(false)); + await using var provider = new DiscordProvider(mockJobManager, Mock.Of(), mockLogger.Object, new ChatBot + { + ReconnectionInterval = 1, + ConnectionString = "asdf" + }); + await Assert.ThrowsExceptionAsync(() => InvokeConnect(provider)); Assert.IsFalse(provider.Connected); } - [Ignore("Broken due to dependency issues after first call to .Connect()")] [TestMethod] public async Task TestConnectAndDisconnect() { if (testToken1 == null) - Assert.Inconclusive("Required environment variable TGS4_TEST_DISCORD_TOKEN_1 isn't set!"); - + Assert.Inconclusive("Required environment variable TGS4_TEST_DISCORD_TOKEN isn't set!"); var mockLogger = new Mock>(); - using var provider = new DiscordProvider(Mock.Of(), mockLogger.Object, testToken1, 1); + await using var provider = new DiscordProvider(mockJobManager, Mock.Of(), mockLogger.Object, testToken1); Assert.IsFalse(provider.Connected); await provider.Disconnect(default).ConfigureAwait(false); Assert.IsFalse(provider.Connected); - Assert.IsTrue(await provider.Connect(default).ConfigureAwait(false)); + await InvokeConnect(provider).ConfigureAwait(false); Assert.IsTrue(provider.Connected); - Assert.IsTrue(await provider.Connect(default).ConfigureAwait(false)); + await InvokeConnect(provider).ConfigureAwait(false); Assert.IsTrue(provider.Connected); await provider.Disconnect(default).ConfigureAwait(false); @@ -68,9 +94,9 @@ namespace Tgstation.Server.Host.Components.Chat.Providers.Tests using var cts = new CancellationTokenSource(); cts.Cancel(); var cancellationToken = cts.Token; - await Assert.ThrowsExceptionAsync(() => provider.Connect(cancellationToken)).ConfigureAwait(false); + await Assert.ThrowsExceptionAsync(() => InvokeConnect(provider, cancellationToken)).ConfigureAwait(false); Assert.IsFalse(provider.Connected); - Assert.IsTrue(await provider.Connect(default).ConfigureAwait(false)); + await InvokeConnect(provider).ConfigureAwait(false); Assert.IsTrue(provider.Connected); await Assert.ThrowsExceptionAsync(() => provider.Disconnect(cancellationToken)).ConfigureAwait(false); Assert.IsTrue(provider.Connected); diff --git a/tests/Tgstation.Server.Tests/IntegrationTest.cs b/tests/Tgstation.Server.Tests/IntegrationTest.cs index 7efaace32c..a82f8783e9 100644 --- a/tests/Tgstation.Server.Tests/IntegrationTest.cs +++ b/tests/Tgstation.Server.Tests/IntegrationTest.cs @@ -95,7 +95,13 @@ namespace Tgstation.Server.Tests { try { - return await clientFactory.CreateFromLogin(url, User.AdminName, User.DefaultAdminPassword, attemptLoginRefresh: false).ConfigureAwait(false); + return await clientFactory.CreateFromLogin( + url, + User.AdminName, + User.DefaultAdminPassword, + attemptLoginRefresh: false, + cancellationToken: cancellationToken) + .ConfigureAwait(false); } catch (HttpRequestException) { From 513177a4ebac1c16860201b781ce154409eb6754 Mon Sep 17 00:00:00 2001 From: Jordan Brown Date: Sun, 19 Jul 2020 21:26:32 -0400 Subject: [PATCH 39/68] IRC Refactor --- .../Components/Chat/Providers/IrcProvider.cs | 191 ++++++++++-------- 1 file changed, 112 insertions(+), 79 deletions(-) diff --git a/src/Tgstation.Server.Host/Components/Chat/Providers/IrcProvider.cs b/src/Tgstation.Server.Host/Components/Chat/Providers/IrcProvider.cs index 5a5d98cde4..dc4c2f1f50 100644 --- a/src/Tgstation.Server.Host/Components/Chat/Providers/IrcProvider.cs +++ b/src/Tgstation.Server.Host/Components/Chat/Providers/IrcProvider.cs @@ -230,100 +230,131 @@ namespace Tgstation.Server.Host.Components.Chat.Providers void Client_OnChannelMessage(object sender, IrcEventArgs e) => HandleMessage(e, false); /// - protected override Task Connect(CancellationToken cancellationToken) => Task.Factory.StartNew(() => + protected override async Task Connect(CancellationToken cancellationToken) { disconnecting = false; - lock (client) - try + try + { + client.Connect(address, port); + + cancellationToken.ThrowIfCancellationRequested(); + + switch (passwordType) { - client.Connect(address, port); - - cancellationToken.ThrowIfCancellationRequested(); - - if (passwordType == IrcPasswordType.Server) + case IrcPasswordType.Server: client.Login(nickname, nickname, 0, nickname, password); - else - { - if (passwordType == IrcPasswordType.Sasl) - { - client.WriteLine("CAP REQ :sasl", Priority.Critical); // needs to be put in the buffer before anything else - cancellationToken.ThrowIfCancellationRequested(); - } - + break; + case IrcPasswordType.NickServ: client.Login(nickname, nickname, 0, nickname); - } - - if (passwordType == IrcPasswordType.NickServ) - { cancellationToken.ThrowIfCancellationRequested(); client.SendMessage(SendType.Message, "NickServ", String.Format(CultureInfo.InvariantCulture, "IDENTIFY {0}", password)); - } - else if (passwordType == IrcPasswordType.Sasl) + break; + case IrcPasswordType.Sasl: + await SaslAuthenticate(cancellationToken).ConfigureAwait(false); + break; + case null: + break; + default: + throw new InvalidOperationException($"Invalid IrcPasswordType: {passwordType.Value}"); + } + + cancellationToken.ThrowIfCancellationRequested(); + client.Listen(false); + + listenTask = Task.Factory.StartNew(() => + { + while (!disconnecting && client.IsConnected && client.Nickname != nickname) { - // wait for the sasl ack or timeout - var recievedAck = false; - var recievedPlus = false; - client.OnReadLine += (sender, e) => - { - if (e.Line.Contains("ACK :sasl", StringComparison.Ordinal)) - recievedAck = true; - else if (e.Line.Contains("AUTHENTICATE +", StringComparison.Ordinal)) - recievedPlus = true; - }; + client.ListenOnce(true); + if (disconnecting || !client.IsConnected) + break; + client.Listen(false); - var startTime = DateTimeOffset.Now; - var endTime = DateTimeOffset.Now.AddSeconds(TimeoutSeconds); - cancellationToken.ThrowIfCancellationRequested(); - - var listenTimeSpan = TimeSpan.FromMilliseconds(10); - for (; !recievedAck && DateTimeOffset.Now <= endTime; asyncDelayer.Delay(listenTimeSpan, cancellationToken).GetAwaiter().GetResult()) - client.Listen(false); - - client.WriteLine("AUTHENTICATE PLAIN", Priority.Critical); - cancellationToken.ThrowIfCancellationRequested(); - - for (; !recievedPlus && DateTimeOffset.Now <= endTime; asyncDelayer.Delay(listenTimeSpan, cancellationToken).GetAwaiter().GetResult()) - client.Listen(false); - - // Stolen! https://github.com/znc/znc/blob/1e697580155d5a38f8b5a377f3b1d94aaa979539/modules/sasl.cpp#L196 - var authString = String.Format(CultureInfo.InvariantCulture, "{0}{1}{0}{1}{2}", nickname, '\0', password); - var b64 = Convert.ToBase64String(Encoding.UTF8.GetBytes(authString)); - var authLine = String.Format(CultureInfo.InvariantCulture, "AUTHENTICATE {0}", b64); - var chars = authLine.ToCharArray(); - client.WriteLine(authLine, Priority.Critical); - - cancellationToken.ThrowIfCancellationRequested(); - client.WriteLine("CAP END", Priority.Critical); + // ensure we have the correct nick + if (client.GetIrcUser(nickname) == null) + client.RfcNick(nickname); } - client.Listen(false); + client.Listen(); + }, cancellationToken, TaskCreationOptions.LongRunning, TaskScheduler.Current); + } + catch (OperationCanceledException) + { + throw; + } + catch (Exception e) + { + throw new JobException(ErrorCode.ChatCannotConnectProvider, e); + } + } - listenTask = Task.Factory.StartNew(() => - { - while (!disconnecting && client.IsConnected && client.Nickname != nickname) - { - client.ListenOnce(true); - if (disconnecting || !client.IsConnected) - break; - client.Listen(false); + /// + /// Run SASL authentication on . + /// + /// The for the operation. + /// A representing the running operation. + async Task SaslAuthenticate(CancellationToken cancellationToken) + { + client.WriteLine("CAP REQ :sasl", Priority.Critical); // needs to be put in the buffer before anything else + cancellationToken.ThrowIfCancellationRequested(); + client.Login(nickname, nickname, 0, nickname); + cancellationToken.ThrowIfCancellationRequested(); - // ensure we have the correct nick - if (client.GetIrcUser(nickname) == null) - client.RfcNick(nickname); - } + // wait for the sasl ack or timeout + var recievedAck = false; + var recievedPlus = false; - client.Listen(); - }, cancellationToken, TaskCreationOptions.LongRunning, TaskScheduler.Current); - } - catch (OperationCanceledException) + void AuthenticationDelegate(object sender, ReadLineEventArgs e) + { + if (e.Line.Contains("ACK :sasl", StringComparison.Ordinal)) + recievedAck = true; + else if (e.Line.Contains("AUTHENTICATE +", StringComparison.Ordinal)) + recievedPlus = true; + } + + client.OnReadLine += AuthenticationDelegate; + + try + { + using (var timeoutCts = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken)) { - throw; + timeoutCts.CancelAfter(TimeSpan.FromSeconds(TimeoutSeconds)); + var timeoutToken = timeoutCts.Token; + + var listenTimeSpan = TimeSpan.FromMilliseconds(10); + for (; !recievedAck; + await asyncDelayer.Delay(listenTimeSpan, timeoutToken).ConfigureAwait(false)) + client.Listen(false); + + client.WriteLine("AUTHENTICATE PLAIN", Priority.Critical); + timeoutToken.ThrowIfCancellationRequested(); + + for (; !recievedPlus; + await asyncDelayer.Delay(listenTimeSpan, timeoutToken).ConfigureAwait(false)) + client.Listen(false); } - catch (Exception e) - { - throw new JobException(ErrorCode.ChatCannotConnectProvider, e); - } - }, cancellationToken, TaskCreationOptions.LongRunning, TaskScheduler.Current); + + cancellationToken.ThrowIfCancellationRequested(); + + // Stolen! https://github.com/znc/znc/blob/1e697580155d5a38f8b5a377f3b1d94aaa979539/modules/sasl.cpp#L196 + var authString = String.Format( + CultureInfo.InvariantCulture, + "{0}{1}{0}{1}{2}", + nickname, + '\0', + password); + var b64 = Convert.ToBase64String(Encoding.UTF8.GetBytes(authString)); + var authLine = $"AUTHENTICATE {b64}"; + client.WriteLine(authLine, Priority.Critical); + + cancellationToken.ThrowIfCancellationRequested(); + client.WriteLine("CAP END", Priority.Critical); + } + finally + { + client.OnReadLine -= AuthenticationDelegate; + } + } /// protected override async Task DisconnectImpl(CancellationToken cancellationToken) @@ -362,7 +393,9 @@ namespace Tgstation.Server.Host.Components.Chat.Providers disconnecting = true; client.Disconnect(); - await listenTask.ConfigureAwait(false); + + if(listenTask != null) + await listenTask.ConfigureAwait(false); } /// From abd5e016ec128c9e5cb65b2966de49fba643cd72 Mon Sep 17 00:00:00 2001 From: Jordan Brown Date: Mon, 20 Jul 2020 20:08:47 -0400 Subject: [PATCH 40/68] More test fixes --- .../Components/InstanceManager.cs | 19 +++-- .../Components/Session/ISessionController.cs | 5 -- .../Components/Session/SessionController.cs | 26 ++---- .../Components/StaticFiles/Configuration.cs | 18 ++++- .../DesignTimeDbContextFactoryHelpers.cs | 9 ++- ...200423052709_MYKillJsonsAddDMApiVersion.cs | 9 +-- src/Tgstation.Server.Host/Jobs/JobHandler.cs | 12 ++- src/Tgstation.Server.Host/Jobs/JobManager.cs | 8 +- .../Instance/WatchdogTest.cs | 25 +++--- .../Tgstation.Server.Tests/IntegrationTest.cs | 80 +++++++++---------- 10 files changed, 108 insertions(+), 103 deletions(-) diff --git a/src/Tgstation.Server.Host/Components/InstanceManager.cs b/src/Tgstation.Server.Host/Components/InstanceManager.cs index 044169edc7..094b3d4db1 100644 --- a/src/Tgstation.Server.Host/Components/InstanceManager.cs +++ b/src/Tgstation.Server.Host/Components/InstanceManager.cs @@ -455,13 +455,20 @@ namespace Tgstation.Server.Host.Components /// public async Task StopAsync(CancellationToken cancellationToken) { - await jobManager.StopAsync(cancellationToken).ConfigureAwait(false); - await Task.WhenAll(instances.Select(x => x.Value.Instance.StopAsync(cancellationToken))).ConfigureAwait(false); - await instanceFactory.StopAsync(cancellationToken).ConfigureAwait(false); + try + { + await jobManager.StopAsync(cancellationToken).ConfigureAwait(false); + await Task.WhenAll(instances.Select(x => x.Value.Instance.StopAsync(cancellationToken))).ConfigureAwait(false); + await instanceFactory.StopAsync(cancellationToken).ConfigureAwait(false); - // downgrade the db if necessary - if (downgradeVersion != null) - await databaseContextFactory.UseContext(db => databaseSeeder.Downgrade(db, downgradeVersion, cancellationToken)).ConfigureAwait(false); + // downgrade the db if necessary + if (downgradeVersion != null) + await databaseContextFactory.UseContext(db => databaseSeeder.Downgrade(db, downgradeVersion, cancellationToken)).ConfigureAwait(false); + } + catch (Exception ex) + { + logger.LogError("Instance manager stop exception: {0}", ex); + } } /// diff --git a/src/Tgstation.Server.Host/Components/Session/ISessionController.cs b/src/Tgstation.Server.Host/Components/Session/ISessionController.cs index c85171fb56..778c39ba38 100644 --- a/src/Tgstation.Server.Host/Components/Session/ISessionController.cs +++ b/src/Tgstation.Server.Host/Components/Session/ISessionController.cs @@ -38,11 +38,6 @@ namespace Tgstation.Server.Host.Components.Session /// CompileJob CompileJob { get; } - /// - /// The current port DreamDaemon is listening on - /// - ushort? Port { get; } - /// /// If the port should be rotated off when the world reboots /// diff --git a/src/Tgstation.Server.Host/Components/Session/SessionController.cs b/src/Tgstation.Server.Host/Components/Session/SessionController.cs index e790feb310..e06caba888 100644 --- a/src/Tgstation.Server.Host/Components/Session/SessionController.cs +++ b/src/Tgstation.Server.Host/Components/Session/SessionController.cs @@ -42,26 +42,7 @@ namespace Tgstation.Server.Host.Components.Session public Models.CompileJob CompileJob => reattachInformation.Dmb.CompileJob; /// - public ushort? Port - { - get - { - CheckDisposed(); - if (portClosedForReboot) - return null; - return reattachInformation.Port; - } - } - - /// - public RebootState RebootState - { - get - { - CheckDisposed(); - return reattachInformation.RebootState; - } - } + public RebootState RebootState => reattachInformation.RebootState; /// public Version DMApiVersion { get; private set; } @@ -262,7 +243,10 @@ namespace Tgstation.Server.Host.Components.Session startupTimeout, reattached); - logger.LogDebug("Created session controller. CommsKey: {0}, Port: {1}", reattachInformation.AccessIdentifier, Port); + logger.LogDebug( + "Created session controller. CommsKey: {0}, Port: {1}", + reattachInformation.AccessIdentifier, + reattachInformation.Port); } /// diff --git a/src/Tgstation.Server.Host/Components/StaticFiles/Configuration.cs b/src/Tgstation.Server.Host/Components/StaticFiles/Configuration.cs index a61ba8cea7..12eb031994 100644 --- a/src/Tgstation.Server.Host/Components/StaticFiles/Configuration.cs +++ b/src/Tgstation.Server.Host/Components/StaticFiles/Configuration.cs @@ -1,4 +1,4 @@ -using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Logging; using System; using System.Collections.Generic; using System.Globalization; @@ -463,7 +463,20 @@ namespace Tgstation.Server.Host.Components.StaticFiles var files = await ioManager.GetFilesWithExtension(EventScriptsSubdirectory, platformIdentifier.ScriptFileExtension, false, cancellationToken).ConfigureAwait(false); var resolvedScriptsDir = ioManager.ResolvePath(EventScriptsSubdirectory); - foreach (var I in files.Select(x => ioManager.GetFileName(x)).Where(x => x.StartsWith(scriptName, StringComparison.Ordinal))) + var scriptFiles = files + .Select(x => ioManager.GetFileName(x)) + .Where(x => x.StartsWith(scriptName, StringComparison.Ordinal)) + .ToList(); + + if (!scriptFiles.Any()) + { + logger.LogTrace("No event scripts starting with \"{0}\" detected", scriptName); + return; + } + + foreach (var I in scriptFiles) + { + logger.LogTrace("Running event script {0}...", I); using (var script = processExecutor.LaunchProcess( ioManager.ConcatPath(resolvedScriptsDir, I), resolvedScriptsDir, @@ -481,6 +494,7 @@ namespace Tgstation.Server.Host.Components.StaticFiles else logger.LogDebug("Script output:{0}{1}", Environment.NewLine, scriptOutput); } + } } } diff --git a/src/Tgstation.Server.Host/Database/Design/DesignTimeDbContextFactoryHelpers.cs b/src/Tgstation.Server.Host/Database/Design/DesignTimeDbContextFactoryHelpers.cs index 4d4f4411dd..36da823aa4 100644 --- a/src/Tgstation.Server.Host/Database/Design/DesignTimeDbContextFactoryHelpers.cs +++ b/src/Tgstation.Server.Host/Database/Design/DesignTimeDbContextFactoryHelpers.cs @@ -1,4 +1,4 @@ -using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore; using Microsoft.Extensions.Options; using Tgstation.Server.Host.Configuration; @@ -15,17 +15,20 @@ namespace Tgstation.Server.Host.Database.Design /// The to create for. /// The . /// The . + /// The . /// The for the public static DbContextOptions CreateDatabaseContextOptions( DatabaseType databaseType, - string connectionString) + string connectionString, + string serverVersion = null) where TDatabaseContext : DatabaseContext { var dbConfig = new DatabaseConfiguration { DesignTime = true, DatabaseType = databaseType, - ConnectionString = connectionString + ConnectionString = connectionString, + ServerVersion = serverVersion }; var optionsFac = new DbContextOptionsBuilder(); diff --git a/src/Tgstation.Server.Host/Database/Migrations/20200423052709_MYKillJsonsAddDMApiVersion.cs b/src/Tgstation.Server.Host/Database/Migrations/20200423052709_MYKillJsonsAddDMApiVersion.cs index 9c6ed01066..32e03425c8 100644 --- a/src/Tgstation.Server.Host/Database/Migrations/20200423052709_MYKillJsonsAddDMApiVersion.cs +++ b/src/Tgstation.Server.Host/Database/Migrations/20200423052709_MYKillJsonsAddDMApiVersion.cs @@ -1,4 +1,4 @@ -using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Migrations; using System; namespace Tgstation.Server.Host.Database.Migrations @@ -39,19 +39,18 @@ namespace Tgstation.Server.Host.Database.Migrations name: "LaunchSecurityLevel", table: "ReattachInformations"); + // No default values b/c lol mysql migrationBuilder.AddColumn( name: "ChatChannelsJson", table: "ReattachInformations", type: "longtext CHARACTER SET utf8mb4", - nullable: false, - defaultValue: "chat_channels.tgs.json"); + nullable: false); migrationBuilder.AddColumn( name: "ChatCommandsJson", table: "ReattachInformations", type: "longtext CHARACTER SET utf8mb4", - nullable: false, - defaultValue: "chat_commands.tgs.json"); + nullable: false); } } } diff --git a/src/Tgstation.Server.Host/Jobs/JobHandler.cs b/src/Tgstation.Server.Host/Jobs/JobHandler.cs index 86437139d5..c6b923033c 100644 --- a/src/Tgstation.Server.Host/Jobs/JobHandler.cs +++ b/src/Tgstation.Server.Host/Jobs/JobHandler.cs @@ -1,6 +1,7 @@ -using System; +using System; using System.Threading; using System.Threading.Tasks; +using Tgstation.Server.Host.Extensions; namespace Tgstation.Server.Host.Jobs { @@ -47,15 +48,12 @@ namespace Tgstation.Server.Host.Jobs /// /// The for the operation /// A representing the running operation - public async Task Wait(CancellationToken cancellationToken) + public Task Wait(CancellationToken cancellationToken) { if (task == null) throw new InvalidOperationException("Job not started!"); - TaskCompletionSource tcs = new TaskCompletionSource(); - using (cancellationToken.Register(() => tcs.SetCanceled())) - await Task.WhenAny(tcs.Task, task).ConfigureAwait(false); - cancellationToken.ThrowIfCancellationRequested(); + return task.WithToken(cancellationToken); } /// @@ -76,4 +74,4 @@ namespace Tgstation.Server.Host.Jobs } } } -} \ No newline at end of file +} diff --git a/src/Tgstation.Server.Host/Jobs/JobManager.cs b/src/Tgstation.Server.Host/Jobs/JobManager.cs index ced1eb724b..d59736c8c9 100644 --- a/src/Tgstation.Server.Host/Jobs/JobManager.cs +++ b/src/Tgstation.Server.Host/Jobs/JobManager.cs @@ -298,8 +298,14 @@ namespace Tgstation.Server.Host.Jobs await databaseContext.Save(cancellationToken).ConfigureAwait(false); job.CancelledBy = user; }).ConfigureAwait(false); + if (blocking) + { + logger.LogTrace("Waiting on cancelled job #{0}...", job.Id); await handler.Wait(cancellationToken).ConfigureAwait(false); + logger.LogTrace("Done waiting on job #{0}...", job.Id); + } + return job; } @@ -321,8 +327,6 @@ namespace Tgstation.Server.Host.Jobs { if (job == null) throw new ArgumentNullException(nameof(job)); - if (canceller == null) - throw new ArgumentNullException(nameof(canceller)); JobHandler handler; lock (synchronizationLock) { diff --git a/tests/Tgstation.Server.Tests/Instance/WatchdogTest.cs b/tests/Tgstation.Server.Tests/Instance/WatchdogTest.cs index 8af1be7c16..9ae2620165 100644 --- a/tests/Tgstation.Server.Tests/Instance/WatchdogTest.cs +++ b/tests/Tgstation.Server.Tests/Instance/WatchdogTest.cs @@ -85,8 +85,9 @@ namespace Tgstation.Server.Tests.Instance var dumpTask = instanceClient.DreamDaemon.CreateDump(cancellationToken); while (!dumpTask.IsCompleted) KillDD(false); - await WaitForJob(await dumpTask, 5, true, ErrorCode.DreamDaemonOffline, cancellationToken); - await Task.Delay(TimeSpan.FromSeconds(5), cancellationToken); + var job = await WaitForJob(await dumpTask, 10, true, null, cancellationToken); + Assert.IsTrue(job.ErrorCode == ErrorCode.DreamDaemonOffline || job.ErrorCode == ErrorCode.GCoreFailure); + await Task.Delay(TimeSpan.FromSeconds(10), cancellationToken); var ddStatus = await instanceClient.DreamDaemon.Read(cancellationToken); Assert.AreEqual(WatchdogStatus.Online, ddStatus.Status.Value); @@ -105,7 +106,7 @@ namespace Tgstation.Server.Tests.Instance var startJob = await instanceClient.DreamDaemon.Start(cancellationToken).ConfigureAwait(false); - await WaitForJob(startJob, 10, false, null, cancellationToken); + await WaitForJob(startJob, 20, false, null, cancellationToken); daemonStatus = await instanceClient.DreamDaemon.Read(cancellationToken); Assert.AreEqual(WatchdogStatus.Online, daemonStatus.Status.Value); @@ -147,12 +148,12 @@ namespace Tgstation.Server.Tests.Instance blockSocket.Bind(new IPEndPoint(IPAddress.Any, 1337)); startJob = await instanceClient.DreamDaemon.Start(cancellationToken).ConfigureAwait(false); - await WaitForJob(startJob, 10, true, ErrorCode.DreamDaemonPortInUse, cancellationToken); + await WaitForJob(startJob, 20, true, ErrorCode.DreamDaemonPortInUse, cancellationToken); } startJob = await instanceClient.DreamDaemon.Start(cancellationToken).ConfigureAwait(false); - await WaitForJob(startJob, 10, false, null, cancellationToken); + await WaitForJob(startJob, 20, false, null, cancellationToken); daemonStatus = await instanceClient.DreamDaemon.Read(cancellationToken); Assert.AreEqual(WatchdogStatus.Online, daemonStatus.Status.Value); @@ -178,7 +179,7 @@ namespace Tgstation.Server.Tests.Instance var startJob = await instanceClient.DreamDaemon.Start(cancellationToken).ConfigureAwait(false); - await WaitForJob(startJob, 10, false, null, cancellationToken); + await WaitForJob(startJob, 20, false, null, cancellationToken); // lock on to DD and pause it so it can't heartbeat var ddProcs = System.Diagnostics.Process.GetProcessesByName("DreamDaemon").ToList(); @@ -246,7 +247,7 @@ namespace Tgstation.Server.Tests.Instance var startJob = await instanceClient.DreamDaemon.Start(cancellationToken).ConfigureAwait(false); - await WaitForJob(startJob, 10, false, null, cancellationToken); + await WaitForJob(startJob, 20, false, null, cancellationToken); daemonStatus = await DeployTestDme(DmeName, DreamDaemonSecurity.Safe, true, cancellationToken); @@ -287,7 +288,7 @@ namespace Tgstation.Server.Tests.Instance var startJob = await instanceClient.DreamDaemon.Start(cancellationToken).ConfigureAwait(false); - await WaitForJob(startJob, 10, false, null, cancellationToken); + await WaitForJob(startJob, 20, false, null, cancellationToken); daemonStatus = await DeployTestDme(DmeName + "_copy", DreamDaemonSecurity.Safe, true, cancellationToken); @@ -425,10 +426,10 @@ namespace Tgstation.Server.Tests.Instance { var bts = new TopicClient(new SocketParameters { - SendTimeout = TimeSpan.FromSeconds(7), - ReceiveTimeout = TimeSpan.FromSeconds(7), - ConnectTimeout = TimeSpan.FromSeconds(7), - DisconnectTimeout = TimeSpan.FromSeconds(7) + SendTimeout = TimeSpan.FromSeconds(15), + ReceiveTimeout = TimeSpan.FromSeconds(15), + ConnectTimeout = TimeSpan.FromSeconds(15), + DisconnectTimeout = TimeSpan.FromSeconds(15) }); try diff --git a/tests/Tgstation.Server.Tests/IntegrationTest.cs b/tests/Tgstation.Server.Tests/IntegrationTest.cs index a82f8783e9..77fc00eae6 100644 --- a/tests/Tgstation.Server.Tests/IntegrationTest.cs +++ b/tests/Tgstation.Server.Tests/IntegrationTest.cs @@ -136,21 +136,40 @@ namespace Tgstation.Server.Tests string migrationName = null; DbContext CreateContext() { + string serverVersion = Environment.GetEnvironmentVariable($"{DatabaseConfiguration.Section}__{nameof(DatabaseConfiguration.ServerVersion)}"); + if (String.IsNullOrWhiteSpace(serverVersion)) + serverVersion = null; switch (databaseType) { case DatabaseType.MySql: case DatabaseType.MariaDB: migrationName = nameof(MYInitialCreate); - return new MySqlDatabaseContext(Host.Database.Design.DesignTimeDbContextFactoryHelpers.CreateDatabaseContextOptions(databaseType, connectionString)); + return new MySqlDatabaseContext( + Host.Database.Design.DesignTimeDbContextFactoryHelpers.CreateDatabaseContextOptions( + databaseType, + connectionString, + serverVersion)); case DatabaseType.PostgresSql: migrationName = nameof(PGCreate); - return new PostgresSqlDatabaseContext(Host.Database.Design.DesignTimeDbContextFactoryHelpers.CreateDatabaseContextOptions(databaseType, connectionString)); + return new PostgresSqlDatabaseContext( + Host.Database.Design.DesignTimeDbContextFactoryHelpers.CreateDatabaseContextOptions( + databaseType, + connectionString, + serverVersion)); case DatabaseType.SqlServer: migrationName = nameof(MSInitialCreate); - return new SqlServerDatabaseContext(Host.Database.Design.DesignTimeDbContextFactoryHelpers.CreateDatabaseContextOptions(databaseType, connectionString)); + return new SqlServerDatabaseContext( + Host.Database.Design.DesignTimeDbContextFactoryHelpers.CreateDatabaseContextOptions( + databaseType, + connectionString, + serverVersion)); case DatabaseType.Sqlite: migrationName = nameof(SLRebuild); - return new SqliteDatabaseContext(Host.Database.Design.DesignTimeDbContextFactoryHelpers.CreateDatabaseContextOptions(databaseType, connectionString)); + return new SqliteDatabaseContext( + Host.Database.Design.DesignTimeDbContextFactoryHelpers.CreateDatabaseContextOptions( + databaseType, + connectionString, + serverVersion)); } return null; @@ -180,29 +199,7 @@ namespace Tgstation.Server.Tests } using var server = new TestingServer(); - - using var hardTimeoutCts = new CancellationTokenSource(); - - var maximumTestDuration = new TimeSpan(0, 12, 0); - - hardTimeoutCts.CancelAfter(maximumTestDuration - new TimeSpan(0, 0, 15)); - var hardTimeoutCancellationToken = hardTimeoutCts.Token; - hardTimeoutCancellationToken.Register(() => - { - Console.WriteLine($"[{DateTimeOffset.Now}] TEST TIMEOUT HARD!"); - }); - - using var softTimeoutCts = CancellationTokenSource.CreateLinkedTokenSource(hardTimeoutCancellationToken); - softTimeoutCts.CancelAfter(maximumTestDuration - new TimeSpan(0, 0, 45)); - var softTimeoutCancellationToken = softTimeoutCts.Token; - bool tooLateForSoftTimeout = false; - softTimeoutCancellationToken.Register(() => - { - if (!tooLateForSoftTimeout) - Console.WriteLine($"[{DateTimeOffset.Now}] TEST TIMEOUT SOFT!"); - }); - - using var serverCts = CancellationTokenSource.CreateLinkedTokenSource(softTimeoutCancellationToken); + using var serverCts = new CancellationTokenSource(); var cancellationToken = serverCts.Token; TerminateAllDDs(); @@ -258,7 +255,7 @@ namespace Tgstation.Server.Tests await adminClient.Administration.Restart(cancellationToken); } - await Task.WhenAny(serverTask, Task.Delay(30000, cancellationToken)); + await Task.WhenAny(serverTask, Task.Delay(TimeSpan.FromMinutes(1), cancellationToken)); Assert.IsTrue(serverTask.IsCompleted); // http bind test https://github.com/tgstation/tgstation-server/issues/1065 @@ -278,7 +275,7 @@ namespace Tgstation.Server.Tests } } - await Task.WhenAny(serverTask, Task.Delay(30000, cancellationToken)); + await Task.WhenAny(serverTask, Task.Delay(TimeSpan.FromMinutes(1), cancellationToken)); Assert.IsTrue(serverTask.IsCompleted); var preStartupTime = DateTimeOffset.Now; @@ -303,12 +300,12 @@ namespace Tgstation.Server.Tests .ToList(); } - Assert.AreEqual(1, jobs.Count, $"Why are there multiple active jobs? \"{String.Join("\", \"", jobs.Select(x => x.Description))}\""); - - var reattachJob = jobs.Single(); - Assert.IsTrue(reattachJob.StartedAt.Value >= preStartupTime); - - await new JobsRequiredTest(instanceClient.Jobs).WaitForJob(reattachJob, 40, false, null, cancellationToken); + var jrt = new JobsRequiredTest(instanceClient.Jobs); + foreach (var job in jobs) + { + Assert.IsTrue(job.StartedAt.Value >= preStartupTime); + await jrt.WaitForJob(job, 40, false, null, cancellationToken); + } var dd = await instanceClient.DreamDaemon.Read(cancellationToken); Assert.AreEqual(WatchdogStatus.Online, dd.Status.Value); @@ -322,7 +319,7 @@ namespace Tgstation.Server.Tests await adminClient.Administration.Restart(cancellationToken); } - await Task.WhenAny(serverTask, Task.Delay(30000, cancellationToken)); + await Task.WhenAny(serverTask, Task.Delay(TimeSpan.FromMinutes(1), cancellationToken)); Assert.IsTrue(serverTask.IsCompleted); preStartupTime = DateTimeOffset.Now; @@ -376,18 +373,21 @@ namespace Tgstation.Server.Tests } finally { - tooLateForSoftTimeout = true; serverCts.Cancel(); + + // Give the test 1 minute to cleanup + using var hardTimeoutCancellationTokenSource = new CancellationTokenSource(TimeSpan.FromMinutes(1)); try { - await serverTask.WithToken(hardTimeoutCancellationToken).ConfigureAwait(false); + await serverTask.WithToken(hardTimeoutCancellationTokenSource.Token).ConfigureAwait(false); } catch (OperationCanceledException) { } TerminateAllDDs(); - - hardTimeoutCancellationToken.ThrowIfCancellationRequested(); } + + Assert.IsTrue(serverTask.IsCompleted); + await serverTask; } [TestMethod] From 5ac7ad21318e84b0389cbb2c650361314d9774f2 Mon Sep 17 00:00:00 2001 From: Jordan Brown Date: Tue, 21 Jul 2020 19:55:07 -0400 Subject: [PATCH 41/68] Goodbye Appveyor --- .github/workflows/{suite.yml => ci-suite.yml} | 240 +++++++++++++++++- .gitignore | 2 +- README.md | 2 +- appveyor.yml | 103 -------- build/prep_deployment.ps1 | 44 ---- build/test_core.sh | 46 ---- 6 files changed, 241 insertions(+), 196 deletions(-) rename .github/workflows/{suite.yml => ci-suite.yml} (67%) delete mode 100644 appveyor.yml delete mode 100644 build/prep_deployment.ps1 delete mode 100755 build/test_core.sh diff --git a/.github/workflows/suite.yml b/.github/workflows/ci-suite.yml similarity index 67% rename from .github/workflows/suite.yml rename to .github/workflows/ci-suite.yml index 2afa6cba58..55a3e49cd8 100644 --- a/.github/workflows/suite.yml +++ b/.github/workflows/ci-suite.yml @@ -1,4 +1,4 @@ -name: 'Test Suite' +name: 'CI' on: push: @@ -250,6 +250,12 @@ jobs: cd ../Tgstation.Server.Host dotnet publish -c ${{ matrix.configuration }} --no-build -o ../../Artifacts/Console/lib/Default + - name: Package Server Update Package + if: ${{ matrix.configuration == 'Release' && matrix.watchdog-type == 'System' && matrix.database-type == 'PostgresSql' }} + run: | + cd ../Tgstation.Server.Host + dotnet publish -c ${{ matrix.configuration }} --no-build -o ../../Artifacts/ServerUpdate + - name: Store Server Console if: ${{ matrix.configuration == 'Release' && matrix.watchdog-type == 'System' && matrix.database-type == 'MariaDB' }} uses: actions/upload-artifact@v2 @@ -257,6 +263,13 @@ jobs: name: ServerConsole path: Artifacts/Console/ + - name: Store Server Update Package + if: ${{ matrix.configuration == 'Release' && matrix.watchdog-type == 'System' && matrix.database-type == 'PostgresSql' }} + uses: actions/upload-artifact@v2 + with: + name: ServerUpdatePackage + path: Artifacts/ServerUpdate/ + windows-integration-test: name: Windows Integration Test needs: dmapi-build @@ -497,3 +510,228 @@ jobs: directory: ./code_coverage/integration_tests flags: integration fail_ci_if_error: true + + deploy-http: + name: Deploy HTTP API + needs: [linux-unit-tests, linux-integration-tests, windows-unit-tests, windows-integration-test] + runs-on: windows-latest + if: github.event_name == 'push' && github.ref == 'master' && contains(github.event.head_commit.message, '[APIDeploy]') + steps: + - name: Checkout + uses: actions/checkout@v1 + + - name: Parse API version + shell: powershell + run: | + [XML]$versionXML = Get-Content build/Version.props + $apiVersion = $versionXML.Project.PropertyGroup.TgsApiVersion + Write-Host "::set-env name=TGS_API_VERSION::$apiVersion" + + - name: Retrieve OpenAPI Spec + uses: actions/download-artifact@v2 + with: + name: openapi-spec + path: swagger.json + + - name: Create GitHub Release + uses: juitnow/github-action-create-release@v1 + id: create_release + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + with: + tag_name: api-v${{ env.TGS_API_VERSION }} + release_name: tgstation-server 4 API v${{ env.TGS_API_VERSION }} + body: The TGS HTTP API + committish: ${{ github.event.after }} + + - name: Upload OpenApi Spec + uses: actions/upload-release-asset@v1 + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + with: + upload_url: ${{ steps.create_release.outputs.upload_url }} + asset_path: ./swagger.json + asset_name: swagger.json + asset_content_type: application/json + + deploy-dm: + name: Deploy DreamMaker API + needs: [linux-unit-tests, linux-integration-tests, windows-unit-tests, windows-integration-test] + runs-on: windows-latest + if: github.event_name == 'push' && github.ref == 'master' && contains(github.event.head_commit.message, '[DMDeploy]') + steps: + - name: Checkout + uses: actions/checkout@v1 + + - name: Parse DMAPI version + shell: powershell + run: | + [XML]$versionXML = Get-Content build/Version.props + $dmVersion = $versionXML.Project.PropertyGroup.TgsDmapiVersion + Write-Host "::set-env name=TGS_DM_VERSION::$dmVersion" + + - name: Zip DMAPI + shell: powershell + run: Compress-Archive -Path src/DMAPI -DestinationPath DMAPI.zip + + - name: Create GitHub Release + uses: juitnow/github-action-create-release@v1 + id: create_release + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + with: + tag_name: dmapi-v${{ env.TGS_DM_VERSION }} + release_name: tgstation-server 4 DMAPI v${{ env.TGS_DM_VERSION }} + body: The TGS DMAPI + committish: ${{ github.event.after }} + + - name: Upload DMAPI Artifact + uses: actions/upload-release-asset@v1 + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + with: + upload_url: ${{ steps.create_release.outputs.upload_url }} + asset_path: ./DMAPI.zip + asset_name: my-artifact.zip + asset_content_type: application/zip + + deploy-client: + name: Deploy Nuget Packages + needs: [linux-unit-tests, linux-integration-tests, windows-unit-tests, windows-integration-test] + runs-on: ubuntu-latest + if: github.event_name == 'push' && github.ref == 'master' && contains(github.event.head_commit.message, '[NugetDeploy]') + steps: + - name: Checkout + uses: actions/checkout@v1 + + - name: Publish API to NuGet + uses: rohith/publish-nuget@v2 + with: + PROJECT_FILE_PATH: src/Tgstation.Server.Api/Tgstation.Server.Api.csproj + TAG_COMMIT: false + INCLUDE_SYMBOLS: true + NUGET_KEY: ${{ secrets.NUGET_API_KEY }} + + - name: Publish Client to NuGet + uses: rohith/publish-nuget@v2 + with: + PROJECT_FILE_PATH: src/Tgstation.Server.Client/Tgstation.Server.Client.csproj + TAG_COMMIT: false + INCLUDE_SYMBOLS: true + NUGET_KEY: ${{ secrets.NUGET_API_KEY }} + + deploy-tgs: + name: Deploy tgstation-server + needs: [deploy-dm, deploy-http, deploy-client] + runs-on: windows-latest + if: github.event_name == 'push' && github.ref == 'master' && contains(github.event.head_commit.message, '[TGSDeploy]') + steps: + - name: Checkout + uses: actions/checkout@v1 + + - name: Parse TGS version + shell: powershell + run: | + [XML]$versionXML = Get-Content build/Version.props + $tgsVersion = $versionXML.Project.PropertyGroup.TgsCoreVersion + Write-Host "::set-env name=TGS_VERSION::$tgsVersion" + + - name: Retrieve Server Service + uses: actions/download-artifact@v2 + with: + name: ServerService + path: ServerService + + - name: Retrieve Server Console + uses: actions/download-artifact@v2 + with: + name: ServerConsole + path: ServerConsole + + - name: Retrieve Server Update Package + uses: actions/download-artifact@v2 + with: + name: ServerUpdatePackage + path: ServerUpdatePackage + + - name: Retrieve OpenAPI Spec + uses: actions/download-artifact@v2 + with: + name: openapi-spec + path: swagger.json + + - name: Zip Artifacts + shell: powershell + run: | + Compress-Archive -Path src/DMAPI -DestinationPath DMAPI.zip + Compress-Archive -Path ServerService -DestinationPath ServerService.zip + Compress-Archive -Path ServerConsole -DestinationPath ServerConsole.zip + Compress-Archive -Path ServerUpdatePackage -DestinationPath ServerUpdatePackage.zip + + - name: Generate Release Notes + shell: powershell + run: | + dotnet run -c Release -p tools/ReleaseNotes ${{ env.TGS_VERSION }} build/Version.props + $releaseNotes = [IO.File]::ReadAllText("release_notes.md") + Write-Host "::set-env name=TGS_RELEASE_NOTES::$releaseNotes" + + - name: Create GitHub Release + uses: juitnow/github-action-create-release@v1 + id: create_release + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + with: + tag_name: tgstation-server-v${{ env.TGS_VERSION }} + release_name: tgstation-server 4 DMAPI v${{ env.TGS_VERSION }} + body: ${{ env.TGS_RELEASE_NOTES }} + committish: ${{ github.event.after }} + + - name: Upload Server Console Artifact + uses: actions/upload-release-asset@v1 + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + with: + upload_url: ${{ steps.create_release.outputs.upload_url }} + asset_path: ./ServerConsole.zip + asset_name: ServerConsole.zip + asset_content_type: application/zip + + - name: Upload Server Service Artifact + uses: actions/upload-release-asset@v1 + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + with: + upload_url: ${{ steps.create_release.outputs.upload_url }} + asset_path: ./ServerService.zip + asset_name: ServerService.zip + asset_content_type: application/zip + + - name: Upload DMAPI Artifact + uses: actions/upload-release-asset@v1 + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + with: + upload_url: ${{ steps.create_release.outputs.upload_url }} + asset_path: ./DMAPI.zip + asset_name: DMAPI.zip + asset_content_type: application/zip + + - name: Upload OpenApi Spec Artifact + uses: actions/upload-release-asset@v1 + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + with: + upload_url: ${{ steps.create_release.outputs.upload_url }} + asset_path: ./swagger.json + asset_name: swagger.json + asset_content_type: application/json + + - name: Upload Server Update Package Artifact + uses: actions/upload-release-asset@v1 + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + with: + upload_url: ${{ steps.create_release.outputs.upload_url }} + asset_path: ./ServerUpdatePackage.zip + asset_name: ServerUpdatePackage.zip + asset_content_type: application/zip diff --git a/.gitignore b/.gitignore index 5885b2ed23..9b47efb6f7 100644 --- a/.gitignore +++ b/.gitignore @@ -21,5 +21,5 @@ artifacts/ /tools/ReleaseNotes/release_notes.md /tools/ReleaseNotes/Properties/launchSettings.json release_notes.md - +*nupkg *.sqlite3 diff --git a/README.md b/README.md index a1f9ff54ec..b5aa0d90df 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ # tgstation-server v4: -![Test Suite](https://github.com/tgstation/tgstation-server/workflows/Test%20Suite/badge.svg) [![Build status](https://ci.appveyor.com/api/projects/status/7t1h7bvuha0p9j5f/branch/master?svg=true)](https://ci.appveyor.com/project/Cyberboss/tgstation-server-tools/branch/master) [![Build Status](https://travis-ci.org/tgstation/tgstation-server.svg?branch=master)](https://travis-ci.org/tgstation/tgstation-server) [![codecov](https://codecov.io/gh/tgstation/tgstation-server/branch/master/graph/badge.svg)](https://codecov.io/gh/tgstation/tgstation-server) +![Test Suite](https://github.com/tgstation/tgstation-server/workflows/Test%20Suite/badge.svg) [![Build Status](https://travis-ci.org/tgstation/tgstation-server.svg?branch=master)](https://travis-ci.org/tgstation/tgstation-server) [![codecov](https://codecov.io/gh/tgstation/tgstation-server/branch/master/graph/badge.svg)](https://codecov.io/gh/tgstation/tgstation-server) [![GitHub license](https://img.shields.io/github/license/tgstation/tgstation-server.svg)](LICENSE) [![Average time to resolve an issue](http://isitmaintained.com/badge/resolution/tgstation/tgstation-server.svg)](http://isitmaintained.com/project/tgstation/tgstation-server "Average time to resolve an issue") [![NuGet version](https://img.shields.io/nuget/v/Tgstation.Server.Api.svg)](https://www.nuget.org/packages/Tgstation.Server.Api) [![NuGet version](https://img.shields.io/nuget/v/Tgstation.Server.Client.svg)](https://www.nuget.org/packages/Tgstation.Server.Client) diff --git a/appveyor.yml b/appveyor.yml deleted file mode 100644 index 9303b2b6c8..0000000000 --- a/appveyor.yml +++ /dev/null @@ -1,103 +0,0 @@ -version: '{build}' -pull_requests: - do_not_increment_build_number: true -environment: - TGS4_TEST_DUMP_API_SPEC: yes - TGS4_TEST_CONNECTION_STRING: Server=(local)\SQL2017;Initial Catalog=TGS_Test;User ID=sa;Password=Password12! - TGS4_TEST_GITHUB_TOKEN: - secure: lJNGAXwiB5HlWdthz3K4PetqpTG5IEAyRgKaiKxFMQ8HW8CcOjRtB97B05op7BsK - TGS4_TEST_DISCORD_TOKEN: - secure: 5ENtMfhHDI6SOgZ9nP7oHZVoIRFPfTjHQpfbCTB+UC8ePc4og5ew3DNzVCXNYxXvwqGyjz7gz3R302Iy3cfsDw== - TGS4_TEST_DISCORD_CHANNEL: 493119635319947269 - TGS4_TEST_IRC_CONNECTION_STRING: - secure: ralERi6QrigvQYZWkLruZGP37RwuadDIkocs+gvT5OcOMUlBODExyI9FPss3HtGx - TGS4_TEST_IRC_CHANNEL: \#botbus - TGS4_RELEASE_NOTES_TOKEN: - secure: lJNGAXwiB5HlWdthz3K4PetqpTG5IEAyRgKaiKxFMQ8HW8CcOjRtB97B05op7BsK -branches: - only: - - master -skip_tags: true -image: Visual Studio 2019 -configuration: - - Release -shallow_clone: true -artifacts: - - path: artifacts/ServerConsole - name: ServerConsole - - path: artifacts/ServerService - name: ServerService - - path: artifacts/ServerHost - name: ServerUpdatePackage - - path: src/DMAPI - name: DMAPI - - path: swagger.json - name: SwaggerSpec -cache: - - ~\.nuget\packages -> **\*.csproj -services: - - mssql2017 -install: - - choco install codecov opencover.portable - - npm i -g ibm-openapi-validator - - nuget restore tgstation-server.sln - - ps: Install-Product node 10 -build: - project: tgstation-server.sln - parallel: false - verbosity: minimal - publish_nuget: true - publish_nuget_symbols: true - use_snupkg_format: true -after_test: - - ps: Move-Item -path C:/swagger.json swagger.json - #host updater - - dotnet publish src/Tgstation.Server.Host/Tgstation.Server.Host.csproj -o artifacts/ServerHost -c %CONFIGURATION% - #console - - dotnet publish src/Tgstation.Server.Host.Console/Tgstation.Server.Host.Console.csproj -o artifacts/ServerConsole -c %CONFIGURATION% - - ps: Copy-Item -path "artifacts/ServerHost" -destination artifacts/ServerConsole/lib/Default -recurse - - ps: Move-Item -path artifacts/ServerConsole/lib/Default/appsettings.json -destination artifacts/ServerConsole/ - #service - - ps: Copy-Item -path "src/Tgstation.Server.Host.Service/bin/$env:CONFIGURATION/net472/win" -destination artifacts/ServerService -recurse - - ps: Copy-Item -path "artifacts/ServerHost" -destination artifacts/ServerService/lib/Default -recurse - - ps: Move-Item -path artifacts/ServerService/lib/Default/appsettings.json -destination artifacts/ServerService/ - - ps: Remove-Item artifacts/ServerHost/appsettings.json - #deploy stuff - - ps: build/prep_deployment.ps1 -deploy: - - provider: GitHub - release: "tgstation-server-v$(TGSVersion)" - description: "$(TGSReleaseNotes)" - auth_token: - secure: lJNGAXwiB5HlWdthz3K4PetqpTG5IEAyRgKaiKxFMQ8HW8CcOjRtB97B05op7BsK - artifact: ServerConsole,ServerService,ServerUpdatePackage,DMAPI,SwaggerSpec - draft: $(TGSDraftNotes) - prerelease: false - on: - TGSDeploy: "Do it." - - provider: GitHub - release: "api-v$(APIVersion)" - description: "$(APIReleaseNotes)" - auth_token: - secure: lJNGAXwiB5HlWdthz3K4PetqpTG5IEAyRgKaiKxFMQ8HW8CcOjRtB97B05op7BsK - artifact: SwaggerSpec - draft: false - prerelease: false - on: - APIDeploy: "Do it." - - provider: GitHub - release: "dmapi-v$(DMVersion)" - description: "$(DMReleaseNotes)" - auth_token: - secure: lJNGAXwiB5HlWdthz3K4PetqpTG5IEAyRgKaiKxFMQ8HW8CcOjRtB97B05op7BsK - artifact: DMAPI - draft: false - prerelease: false - on: - DMDeploy: "Do it." - - provider: NuGet - api_key: - secure: DzllxD27WDdbXf/U9myiLcu/DBlJBk1rlrPv629JHluIsffaJUbY1L+TRYo5gwjf - artifact: /.*\.nupkg/ - on: - NugetDeploy: "Do it." diff --git a/build/prep_deployment.ps1 b/build/prep_deployment.ps1 deleted file mode 100644 index 358b72d8c6..0000000000 --- a/build/prep_deployment.ps1 +++ /dev/null @@ -1,44 +0,0 @@ -$bf = $env:APPVEYOR_BUILD_FOLDER -$propsPath = "$bf/build/Version.props" - -[XML]$versionXML = Get-Content $propsPath -$env:TGSVersion = $versionXML.Project.PropertyGroup.TgsCoreVersion -$env:APIVersion = $versionXML.Project.PropertyGroup.TgsApiVersion -$env:DMVersion = $versionXML.Project.PropertyGroup.TgsDmapiVersion - -Write-Host "TGS Version: $env:TGSVersion" - -if (($env:CONFIGURATION -match "Release") -And ($env:APPVEYOR_REPO_BRANCH -match "master") -And ($env:APPVEYOR_REPO_COMMIT_MESSAGE -match "\[TGSDeploy\]")) { - Write-Host "Deploying TGS..." - $env:TGSDeploy = "Do it." - - Write-Host "Generating release notes..." - dotnet run -p "$bf/tools/ReleaseNotes" $env:TGSVersion $propsPath - $env:TGSDraftNotes = !($?) - $releaseNotesPath = "$bf/release_notes.md" - Write-Host "Reading release notes from $releaseNotesPath..." - if (Test-Path $releaseNotesPath -PathType Leaf) { - $env:TGSReleaseNotes = [IO.File]::ReadAllText($releaseNotesPath) - } - else { - Write-Host "Release note generation failed, release will be created as a draft!" - $env:TGSReleaseNotes = "Automatic generation failed, please fill manually!" - } -} - -if (($env:CONFIGURATION -match "Release") -And ($env:APPVEYOR_REPO_COMMIT_MESSAGE -match "\[APIDeploy\]")) { - Write-Host "Deploying API..." - $env:APIDeploy = "Do it." - $env:APIReleaseNotes = "# tgstation-server 4 API v$env:APIVersion" -} - -if (($env:CONFIGURATION -match "Release") -And ($env:APPVEYOR_REPO_COMMIT_MESSAGE -match "\[DMDeploy\]")) { - Write-Host "Deploying DMAPI..." - $env:DMDeploy = "Do it." - $env:DMReleaseNotes = "# tgstation-server 4 DMAPI v$env:DMReleaseNotes" -} - -if (($env:CONFIGURATION -match "Release") -And ($env:APPVEYOR_REPO_COMMIT_MESSAGE -match "\[NugetDeploy\]")) { - $env:NugetDeploy = "Do it." - Write-Host "Nuget deployment enabled" -} \ No newline at end of file diff --git a/build/test_core.sh b/build/test_core.sh deleted file mode 100755 index 0775ff7246..0000000000 --- a/build/test_core.sh +++ /dev/null @@ -1,46 +0,0 @@ -#!/bin/bash -set -e - -dotnet tool install --global coverlet.console - -mkdir TestResults - -source ~/.nvm/nvm.sh && nvm install 10 - -if [[ ! -z "${TGS4_TEST_CONNECTION_STRING}" ]]; then - build/integration_test.sh - exit -fi - -cd tests/Tgstation.Server.Api.Tests - -dotnet build -c $CONFIG /p:CopyLocalLockFileAssemblies=true -$HOME/.dotnet/tools/coverlet bin/$CONFIG/netcoreapp3.1/Tgstation.Server.Api.Tests.dll --target "dotnet" --targetargs "test -c $CONFIG --no-build" --format opencover --output "../../TestResults/api.xml" --include "[Tgstation.Server*]*" --exclude "[Tgstation.Server.Api.Tests*]*" - -cd ../Tgstation.Server.Client.Tests - -dotnet build -c $CONFIG /p:CopyLocalLockFileAssemblies=true -$HOME/.dotnet/tools/coverlet bin/$CONFIG/netcoreapp3.1/Tgstation.Server.Client.Tests.dll --target "dotnet" --targetargs "test -c $CONFIG --no-build" --format opencover --output "../../TestResults/client.xml" --include "[Tgstation.Server*]*" --exclude "[Tgstation.Server.Client.Tests*]*" - -cd ../Tgstation.Server.Host.Tests - -dotnet build -c $CONFIG /p:CopyLocalLockFileAssemblies=true -$HOME/.dotnet/tools/coverlet bin/$CONFIG/netcoreapp3.1/Tgstation.Server.Host.Tests.dll --target "dotnet" --targetargs "test -c $CONFIG --no-build" --format opencover --output "../../TestResults/host.xml" --include "[Tgstation.Server*]*" --exclude "[Tgstation.Server.Host.Tests*]*" --exclude "[Tgstation.Server.Host]Tgstation.Server.Host.Database.Migrations.*" - -cd ../Tgstation.Server.Host.Watchdog.Tests - -dotnet build -c $CONFIG /p:CopyLocalLockFileAssemblies=true -$HOME/.dotnet/tools/coverlet bin/$CONFIG/netcoreapp3.1/Tgstation.Server.Host.Watchdog.Tests.dll --target "dotnet" --targetargs "test -c $CONFIG --no-build" --format opencover --output "../../TestResults/watchdog.xml" --include "[Tgstation.Server*]*" --exclude "[Tgstation.Server.Host.Watchdog.Tests*]*" - -cd ../Tgstation.Server.Host.Console.Tests - -dotnet build -c $CONFIG /p:CopyLocalLockFileAssemblies=true -$HOME/.dotnet/tools/coverlet bin/$CONFIG/netcoreapp3.1/Tgstation.Server.Host.Console.Tests.dll --target "dotnet" --targetargs "test -c $CONFIG --no-build" --format opencover --output "../../TestResults/console.xml" --include "[Tgstation.Server*]*" --exclude "[Tgstation.Server.Host.Console.Tests*]*" - -cd ../../TestResults - -bash <(curl -s https://codecov.io/bash) -f api.xml -F unittests -bash <(curl -s https://codecov.io/bash) -f client.xml -F unittests -bash <(curl -s https://codecov.io/bash) -f host.xml -F unittests -bash <(curl -s https://codecov.io/bash) -f watchdog.xml -F unittests -bash <(curl -s https://codecov.io/bash) -f console.xml -F unittests From b158bfbd1eefe9223011a71f045f545ba49bbd28 Mon Sep 17 00:00:00 2001 From: Jordan Brown Date: Tue, 21 Jul 2020 20:10:55 -0400 Subject: [PATCH 42/68] More CI cleanups --- .github/workflows/ci-suite.yml | 6 +- README.md | 2 +- .../Chat/Providers/DiscordProvider.cs | 30 +-- .../Components/Chat/Providers/IrcProvider.cs | 209 ++++++++++++------ .../Components/Chat/Providers/Provider.cs | 37 +++- .../Components/Deployment/DmbFactory.cs | 28 ++- .../Components/Deployment/DreamMaker.cs | 3 +- .../Components/Instance.cs | 2 +- .../Session/SessionControllerFactory.cs | 5 +- .../Components/Watchdog/BasicWatchdog.cs | 2 +- .../Controllers/InstanceController.cs | 91 ++++---- src/Tgstation.Server.Host/Core/Application.cs | 8 +- ...00422010115_MYAddCompileJobDMApiVersion.cs | 5 +- .../Extensions/ServiceCollectionExtensions.cs | 2 +- src/Tgstation.Server.Host/Jobs/JobManager.cs | 10 +- src/Tgstation.Server.Host/Server.cs | 44 ++-- src/Tgstation.Server.Host/System/Process.cs | 32 ++- .../System/ProcessExecutor.cs | 7 +- .../Tgstation.Server.Host.csproj | 4 +- tests/DMAPI/BasicOperation/Test.dm | 2 +- .../Instance/DeploymentTest.cs | 5 +- .../Instance/WatchdogTest.cs | 56 +++-- .../Tgstation.Server.Tests/IntegrationTest.cs | 53 ++++- 23 files changed, 409 insertions(+), 234 deletions(-) diff --git a/.github/workflows/ci-suite.yml b/.github/workflows/ci-suite.yml index 55a3e49cd8..e2e0fa100b 100644 --- a/.github/workflows/ci-suite.yml +++ b/.github/workflows/ci-suite.yml @@ -513,7 +513,7 @@ jobs: deploy-http: name: Deploy HTTP API - needs: [linux-unit-tests, linux-integration-tests, windows-unit-tests, windows-integration-test] + needs: [upload-code-coverage, validate-openapi-spec] runs-on: windows-latest if: github.event_name == 'push' && github.ref == 'master' && contains(github.event.head_commit.message, '[APIDeploy]') steps: @@ -556,7 +556,7 @@ jobs: deploy-dm: name: Deploy DreamMaker API - needs: [linux-unit-tests, linux-integration-tests, windows-unit-tests, windows-integration-test] + needs: [upload-code-coverage, validate-openapi-spec] runs-on: windows-latest if: github.event_name == 'push' && github.ref == 'master' && contains(github.event.head_commit.message, '[DMDeploy]') steps: @@ -597,7 +597,7 @@ jobs: deploy-client: name: Deploy Nuget Packages - needs: [linux-unit-tests, linux-integration-tests, windows-unit-tests, windows-integration-test] + needs: [upload-code-coverage, validate-openapi-spec] runs-on: ubuntu-latest if: github.event_name == 'push' && github.ref == 'master' && contains(github.event.head_commit.message, '[NugetDeploy]') steps: diff --git a/README.md b/README.md index b5aa0d90df..09032179ea 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ # tgstation-server v4: -![Test Suite](https://github.com/tgstation/tgstation-server/workflows/Test%20Suite/badge.svg) [![Build Status](https://travis-ci.org/tgstation/tgstation-server.svg?branch=master)](https://travis-ci.org/tgstation/tgstation-server) [![codecov](https://codecov.io/gh/tgstation/tgstation-server/branch/master/graph/badge.svg)](https://codecov.io/gh/tgstation/tgstation-server) +![CI](https://github.com/tgstation/tgstation-server/workflows/CI/badge.svg) [![Build Status](https://travis-ci.org/tgstation/tgstation-server.svg?branch=master)](https://travis-ci.org/tgstation/tgstation-server) [![codecov](https://codecov.io/gh/tgstation/tgstation-server/branch/master/graph/badge.svg)](https://codecov.io/gh/tgstation/tgstation-server) [![GitHub license](https://img.shields.io/github/license/tgstation/tgstation-server.svg)](LICENSE) [![Average time to resolve an issue](http://isitmaintained.com/badge/resolution/tgstation/tgstation-server.svg)](http://isitmaintained.com/project/tgstation/tgstation-server "Average time to resolve an issue") [![NuGet version](https://img.shields.io/nuget/v/Tgstation.Server.Api.svg)](https://www.nuget.org/packages/Tgstation.Server.Api) [![NuGet version](https://img.shields.io/nuget/v/Tgstation.Server.Client.svg)](https://www.nuget.org/packages/Tgstation.Server.Client) diff --git a/src/Tgstation.Server.Host/Components/Chat/Providers/DiscordProvider.cs b/src/Tgstation.Server.Host/Components/Chat/Providers/DiscordProvider.cs index e41de6012a..6ba2897ba5 100644 --- a/src/Tgstation.Server.Host/Components/Chat/Providers/DiscordProvider.cs +++ b/src/Tgstation.Server.Host/Components/Chat/Providers/DiscordProvider.cs @@ -131,13 +131,13 @@ namespace Tgstation.Server.Host.Components.Chat.Providers Mention = NormalizeMentions(e.Author.Mention) } }; + EnqueueMessage(result); } /// protected override async Task Connect(CancellationToken cancellationToken) { - Logger.LogTrace("Connecting..."); try { await client.LoginAsync(TokenType.Bot, BotToken, true).ConfigureAwait(false); @@ -150,14 +150,22 @@ namespace Tgstation.Server.Host.Components.Chat.Providers Logger.LogTrace("Started."); var channelsAvailable = new TaskCompletionSource(); - client.Ready += () => + Task ReadyCallback() { channelsAvailable.TrySetResult(null); return Task.CompletedTask; - }; - using (cancellationToken.Register(() => channelsAvailable.SetCanceled())) - await channelsAvailable.Task.ConfigureAwait(false); - Logger.LogDebug("Connection established!"); + } + + client.Ready += ReadyCallback; + try + { + using (cancellationToken.Register(() => channelsAvailable.SetCanceled())) + await channelsAvailable.Task.ConfigureAwait(false); + } + finally + { + client.Ready -= ReadyCallback; + } } catch (OperationCanceledException) { @@ -172,13 +180,6 @@ namespace Tgstation.Server.Host.Components.Chat.Providers /// protected override async Task DisconnectImpl(CancellationToken cancellationToken) { - Logger.LogTrace("Disconnecting..."); - if (!Connected) - { - Logger.LogTrace("Already disconnected not doing disconnection attempt!"); - return; - } - try { cancellationToken.ThrowIfCancellationRequested(); @@ -254,7 +255,8 @@ namespace Tgstation.Server.Host.Components.Chat.Providers var channel = client.GetChannel(channelId) as IMessageChannel; await (channel?.SendMessageAsync(message, false, null, new RequestOptions { - CancelToken = cancellationToken + CancelToken = cancellationToken, + Timeout = 10000 // prevent stupid long hold ups from this }) ?? Task.CompletedTask).ConfigureAwait(false); } catch (OperationCanceledException) diff --git a/src/Tgstation.Server.Host/Components/Chat/Providers/IrcProvider.cs b/src/Tgstation.Server.Host/Components/Chat/Providers/IrcProvider.cs index dc4c2f1f50..7db05d816d 100644 --- a/src/Tgstation.Server.Host/Components/Chat/Providers/IrcProvider.cs +++ b/src/Tgstation.Server.Host/Components/Chat/Providers/IrcProvider.cs @@ -20,6 +20,9 @@ namespace Tgstation.Server.Host.Components.Chat.Providers /// sealed class IrcProvider : Provider { + /// + /// Number of seconds used for several IRC related timeouts. + /// const int TimeoutSeconds = 5; /// @@ -149,7 +152,9 @@ namespace Tgstation.Server.Host.Components.Chat.Providers public override async ValueTask DisposeAsync() { await base.DisposeAsync().ConfigureAwait(false); - await HardDisconnect().ConfigureAwait(false); + + // DCT: None available + await HardDisconnect(default).ConfigureAwait(false); } /// @@ -239,6 +244,7 @@ namespace Tgstation.Server.Host.Components.Chat.Providers cancellationToken.ThrowIfCancellationRequested(); + Logger.LogTrace("Authenticating ({0})...", passwordType); switch (passwordType) { case IrcPasswordType.Server: @@ -259,24 +265,50 @@ namespace Tgstation.Server.Host.Components.Chat.Providers } cancellationToken.ThrowIfCancellationRequested(); - client.Listen(false); + Logger.LogTrace("Processing initial messages..."); + await NonBlockingListen(cancellationToken).ConfigureAwait(false); - listenTask = Task.Factory.StartNew(() => + var nickCheckCompleteTcs = new TaskCompletionSource(); + using (cancellationToken.Register(() => nickCheckCompleteTcs.TrySetCanceled())) { - while (!disconnecting && client.IsConnected && client.Nickname != nickname) + listenTask = Task.Factory.StartNew( + async () => { - client.ListenOnce(true); - if (disconnecting || !client.IsConnected) - break; - client.Listen(false); + Logger.LogTrace("Entering nick check loop"); + while (!disconnecting && client.IsConnected && client.Nickname != nickname) + { + client.ListenOnce(true); + if (disconnecting || !client.IsConnected) + break; + await NonBlockingListen(cancellationToken).ConfigureAwait(false); - // ensure we have the correct nick - if (client.GetIrcUser(nickname) == null) - client.RfcNick(nickname); - } + // ensure we have the correct nick + if (client.GetIrcUser(nickname) == null) + client.RfcNick(nickname); + } - client.Listen(); - }, cancellationToken, TaskCreationOptions.LongRunning, TaskScheduler.Current); + nickCheckCompleteTcs.TrySetResult(null); + + Logger.LogTrace("Starting blocking listen..."); + try + { + client.Listen(); + } + catch (Exception ex) + { + Logger.LogWarning("IRC Listen Error: {0}", ex); + } + + Logger.LogTrace("Exiting listening task..."); + }, + cancellationToken, + TaskCreationOptions.LongRunning, + TaskScheduler.Current); + + await nickCheckCompleteTcs.Task.ConfigureAwait(false); + } + + Logger.LogTrace("Connection established!"); } catch (OperationCanceledException) { @@ -288,6 +320,28 @@ namespace Tgstation.Server.Host.Components.Chat.Providers } } + /// + /// Perform a non-blocking . + /// + /// The for the operation. + /// A representing the running operation. + Task NonBlockingListen(CancellationToken cancellationToken) => Task.Factory.StartNew( + () => + { + try + { + client.Listen(false); + } + catch (Exception ex) + { + Logger.LogWarning("IRC Listen Error: {0}", ex); + } + }, + cancellationToken, + TaskCreationOptions.None, + TaskScheduler.Current) + .WithToken(cancellationToken); + /// /// Run SASL authentication on . /// @@ -297,6 +351,8 @@ namespace Tgstation.Server.Host.Components.Chat.Providers { client.WriteLine("CAP REQ :sasl", Priority.Critical); // needs to be put in the buffer before anything else cancellationToken.ThrowIfCancellationRequested(); + + Logger.LogTrace("Logging in..."); client.Login(nickname, nickname, 0, nickname); cancellationToken.ThrowIfCancellationRequested(); @@ -312,69 +368,72 @@ namespace Tgstation.Server.Host.Components.Chat.Providers recievedPlus = true; } + Logger.LogTrace("Performing handshake..."); client.OnReadLine += AuthenticationDelegate; - try { - using (var timeoutCts = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken)) - { - timeoutCts.CancelAfter(TimeSpan.FromSeconds(TimeoutSeconds)); - var timeoutToken = timeoutCts.Token; + using var timeoutCts = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken); + timeoutCts.CancelAfter(TimeSpan.FromSeconds(TimeoutSeconds)); + var timeoutToken = timeoutCts.Token; - var listenTimeSpan = TimeSpan.FromMilliseconds(10); - for (; !recievedAck; - await asyncDelayer.Delay(listenTimeSpan, timeoutToken).ConfigureAwait(false)) - client.Listen(false); + var listenTimeSpan = TimeSpan.FromMilliseconds(10); + for (; !recievedAck; + await asyncDelayer.Delay(listenTimeSpan, timeoutToken).ConfigureAwait(false)) + await NonBlockingListen(cancellationToken).ConfigureAwait(false); - client.WriteLine("AUTHENTICATE PLAIN", Priority.Critical); - timeoutToken.ThrowIfCancellationRequested(); + client.WriteLine("AUTHENTICATE PLAIN", Priority.Critical); + timeoutToken.ThrowIfCancellationRequested(); - for (; !recievedPlus; - await asyncDelayer.Delay(listenTimeSpan, timeoutToken).ConfigureAwait(false)) - client.Listen(false); - } - - cancellationToken.ThrowIfCancellationRequested(); - - // Stolen! https://github.com/znc/znc/blob/1e697580155d5a38f8b5a377f3b1d94aaa979539/modules/sasl.cpp#L196 - var authString = String.Format( - CultureInfo.InvariantCulture, - "{0}{1}{0}{1}{2}", - nickname, - '\0', - password); - var b64 = Convert.ToBase64String(Encoding.UTF8.GetBytes(authString)); - var authLine = $"AUTHENTICATE {b64}"; - client.WriteLine(authLine, Priority.Critical); - - cancellationToken.ThrowIfCancellationRequested(); - client.WriteLine("CAP END", Priority.Critical); + for (; !recievedPlus; + await asyncDelayer.Delay(listenTimeSpan, timeoutToken).ConfigureAwait(false)) + await NonBlockingListen(cancellationToken).ConfigureAwait(false); } finally { client.OnReadLine -= AuthenticationDelegate; } + + cancellationToken.ThrowIfCancellationRequested(); + + // Stolen! https://github.com/znc/znc/blob/1e697580155d5a38f8b5a377f3b1d94aaa979539/modules/sasl.cpp#L196 + Logger.LogTrace("Sending credentials..."); + var authString = String.Format( + CultureInfo.InvariantCulture, + "{0}{1}{0}{1}{2}", + nickname, + '\0', + password); + var b64 = Convert.ToBase64String(Encoding.UTF8.GetBytes(authString)); + var authLine = $"AUTHENTICATE {b64}"; + client.WriteLine(authLine, Priority.Critical); + cancellationToken.ThrowIfCancellationRequested(); + + Logger.LogTrace("Finishing authentication..."); + client.WriteLine("CAP END", Priority.Critical); } /// protected override async Task DisconnectImpl(CancellationToken cancellationToken) { - if (!Connected) - return; try { - await Task.Factory.StartNew(() => - { - try + await Task.Factory.StartNew( + () => { - client.RfcQuit("Mr. Stark, I don't feel so good...", Priority.Critical); // priocritical otherwise it wont go through - } - catch (Exception e) - { - Logger.LogWarning("Error quitting IRC: {0}", e); - } - }, cancellationToken, TaskCreationOptions.LongRunning, TaskScheduler.Current).ConfigureAwait(false); - await HardDisconnect().ConfigureAwait(false); + try + { + client.RfcQuit("Mr. Stark, I don't feel so good...", Priority.Critical); // priocritical otherwise it wont go through + } + catch (Exception e) + { + Logger.LogWarning("Error quitting IRC: {0}", e); + } + }, + cancellationToken, + TaskCreationOptions.LongRunning, + TaskScheduler.Current) + .ConfigureAwait(false); + await HardDisconnect(cancellationToken).ConfigureAwait(false); } catch (OperationCanceledException) { @@ -386,16 +445,42 @@ namespace Tgstation.Server.Host.Components.Chat.Providers } } - async Task HardDisconnect() + async Task HardDisconnect(CancellationToken cancellationToken) { if (!Connected) + { + Logger.LogTrace("Not hard disconnecting, already offline"); return; + } + + Logger.LogTrace("Hard disconnect"); disconnecting = true; - client.Disconnect(); - if(listenTask != null) - await listenTask.ConfigureAwait(false); + // This call blocks permanently randomly sometimes + // Frankly I don't give a shit + var disconnectTask = Task.Factory.StartNew( + () => + { + try + { + client.Disconnect(); + } + catch (Exception e) + { + Logger.LogWarning("Error disconnecting IRC: {0}", e); + } + }, + cancellationToken, + TaskCreationOptions.None, + TaskScheduler.Current); + + await Task.WhenAny( + Task.WhenAll( + disconnectTask, + listenTask ?? Task.CompletedTask), + asyncDelayer.Delay(TimeSpan.FromSeconds(TimeoutSeconds), cancellationToken)) + .ConfigureAwait(false); } /// diff --git a/src/Tgstation.Server.Host/Components/Chat/Providers/Provider.cs b/src/Tgstation.Server.Host/Components/Chat/Providers/Provider.cs index ea03fea802..744e68867d 100644 --- a/src/Tgstation.Server.Host/Components/Chat/Providers/Provider.cs +++ b/src/Tgstation.Server.Host/Components/Chat/Providers/Provider.cs @@ -4,6 +4,7 @@ using System.Collections.Generic; using System.Threading; using System.Threading.Tasks; using Tgstation.Server.Api.Rights; +using Tgstation.Server.Host.Extensions; using Tgstation.Server.Host.Jobs; using Tgstation.Server.Host.Models; @@ -119,8 +120,12 @@ namespace Tgstation.Server.Host.Components.Chat.Providers /// public async Task Disconnect(CancellationToken cancellationToken) { - if(Connected) + if (Connected) + { await DisconnectImpl(cancellationToken).ConfigureAwait(false); + Logger.LogTrace("Disconnected"); + } + await StopReconnectionTimer().ConfigureAwait(false); } @@ -130,16 +135,17 @@ namespace Tgstation.Server.Host.Components.Chat.Providers /// public async Task NextMessage(CancellationToken cancellationToken) { - var cancelTcs = new TaskCompletionSource(); - using (cancellationToken.Register(() => cancelTcs.SetCanceled())) - await Task.WhenAny(nextMessage.Task, cancelTcs.Task).ConfigureAwait(false); - cancellationToken.ThrowIfCancellationRequested(); - lock (messageQueue) + while (true) { - var result = messageQueue.Dequeue(); - if (messageQueue.Count == 0) - nextMessage = new TaskCompletionSource(); - return result; + await nextMessage.Task.WithToken(cancellationToken).ConfigureAwait(false); + lock (messageQueue) + if (messageQueue.Count > 0) + { + var result = messageQueue.Dequeue(); + if (messageQueue.Count == 0) + nextMessage = new TaskCompletionSource(); + return result; + } } } @@ -211,8 +217,17 @@ namespace Tgstation.Server.Host.Components.Chat.Providers job, async (core, databaseContextFactory, paramJob, progressReporter, jobCancellationToken) => { - await DisconnectImpl(jobCancellationToken).ConfigureAwait(false); + if (Connected) + { + Logger.LogTrace("Disconnecting..."); + await DisconnectImpl(jobCancellationToken).ConfigureAwait(false); + } + else + Logger.LogTrace("Already disconnected not doing disconnection attempt!"); + + Logger.LogTrace("Connecting..."); await Connect(jobCancellationToken).ConfigureAwait(false); + Logger.LogTrace("Connected successfully"); EnqueueMessage(null); }, cancellationToken) diff --git a/src/Tgstation.Server.Host/Components/Deployment/DmbFactory.cs b/src/Tgstation.Server.Host/Components/Deployment/DmbFactory.cs index 1c63870f1b..1d5c26b26d 100644 --- a/src/Tgstation.Server.Host/Components/Deployment/DmbFactory.cs +++ b/src/Tgstation.Server.Host/Components/Deployment/DmbFactory.cs @@ -1,4 +1,4 @@ -using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore; using Microsoft.Extensions.Logging; using System; using System.Collections.Generic; @@ -202,26 +202,32 @@ namespace Tgstation.Server.Host.Components.Deployment if (compileJob == null) throw new ArgumentNullException(nameof(compileJob)); - // ensure we have the entire compile job tree + // ensure we have the entire metadata tree logger.LogTrace("Loading compile job {0}...", compileJob.Id); await databaseContextFactory.UseContext( async db => compileJob = await db .CompileJobs .AsQueryable() .Where(x => x.Id == compileJob.Id) - .Include(x => x.Job).ThenInclude(x => x.StartedBy) - .Include(x => x.RevisionInformation).ThenInclude(x => x.PrimaryTestMerge).ThenInclude(x => x.MergedBy) - .Include(x => x.RevisionInformation).ThenInclude(x => x.ActiveTestMerges).ThenInclude(x => x.TestMerge).ThenInclude(x => x.MergedBy) + .Include(x => x.Job) + .ThenInclude(x => x.StartedBy) + .Include(x => x.RevisionInformation) + .ThenInclude(x => x.PrimaryTestMerge) + .ThenInclude(x => x.MergedBy) + .Include(x => x.RevisionInformation) + .ThenInclude(x => x.ActiveTestMerges) + .ThenInclude(x => x.TestMerge) + .ThenInclude(x => x.MergedBy) .FirstAsync(cancellationToken) .ConfigureAwait(false)) .ConfigureAwait(false); // can't wait to see that query if (!compileJob.Job.StoppedAt.HasValue) { - // This happens if we're told to load the compile job that is currently finished up - // It can constitute an API violation if it's returned by the DreamDaemonController so just set it here - // Bit of a hack, but it should work out to be the same value - logger.LogTrace("Setting missing StoppedAt for CompileJob job..."); + // This happens when we're told to load the compile job that is currently finished up + // It constitutes an API violation if it's returned by the DreamDaemonController so just set it here + // Bit of a hack, but it works out to be nearly if not the same value that's put in the DB + logger.LogTrace("Setting missing StoppedAt for CompileJob.Job #{0}...", compileJob.Job.Id); compileJob.Job.StoppedAt = DateTimeOffset.Now; } @@ -284,9 +290,9 @@ namespace Tgstation.Server.Host.Components.Deployment else jobLockCounts[compileJob.Id] = ++value; - logger.LogTrace("Compile job {0} lock count now: {1}", compileJob.Id, value); - providerSubmitted = true; + + logger.LogTrace("Compile job {0} lock count now: {1}", compileJob.Id, value); return newProvider; } } diff --git a/src/Tgstation.Server.Host/Components/Deployment/DreamMaker.cs b/src/Tgstation.Server.Host/Components/Deployment/DreamMaker.cs index 89667e14ec..434cb9a781 100644 --- a/src/Tgstation.Server.Host/Components/Deployment/DreamMaker.cs +++ b/src/Tgstation.Server.Host/Components/Deployment/DreamMaker.cs @@ -1,4 +1,4 @@ -using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore; using Microsoft.Extensions.Logging; using Octokit; using System; @@ -633,6 +633,7 @@ namespace Tgstation.Server.Host.Components.Deployment // The difficulty with compile jobs is they have a two part commit await databaseContext.Save(cancellationToken).ConfigureAwait(false); + logger.LogTrace("Created CompileJob {0}", compileJob.Id); try { await compileJobConsumer.LoadCompileJob(compileJob, cancellationToken).ConfigureAwait(false); diff --git a/src/Tgstation.Server.Host/Components/Instance.cs b/src/Tgstation.Server.Host/Components/Instance.cs index 8826d73929..1553263b9f 100644 --- a/src/Tgstation.Server.Host/Components/Instance.cs +++ b/src/Tgstation.Server.Host/Components/Instance.cs @@ -471,7 +471,7 @@ namespace Tgstation.Server.Host.Components // race condition, just quit if (timerTask != null) { - logger.LogDebug("Aborting auto update interval change due to race condition!"); + logger.LogWarning("Aborting auto update interval change due to race condition!"); return; } diff --git a/src/Tgstation.Server.Host/Components/Session/SessionControllerFactory.cs b/src/Tgstation.Server.Host/Components/Session/SessionControllerFactory.cs index e211b982d2..b682e3682f 100644 --- a/src/Tgstation.Server.Host/Components/Session/SessionControllerFactory.cs +++ b/src/Tgstation.Server.Host/Components/Session/SessionControllerFactory.cs @@ -1,4 +1,4 @@ -using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Logging; using System; using System.Globalization; using System.Linq; @@ -116,10 +116,11 @@ namespace Tgstation.Server.Host.Components.Session /// Check if a given can be bound to. /// /// The port number to test. - static void PortBindTest(ushort port) + void PortBindTest(ushort port) { try { + logger.LogTrace("Bind test: {0}", port); SocketExtensions.BindTest(port, false); } catch (SocketException ex) when (ex.SocketErrorCode == SocketError.AddressAlreadyInUse) diff --git a/src/Tgstation.Server.Host/Components/Watchdog/BasicWatchdog.cs b/src/Tgstation.Server.Host/Components/Watchdog/BasicWatchdog.cs index 9ccd92a28c..810a3dc740 100644 --- a/src/Tgstation.Server.Host/Components/Watchdog/BasicWatchdog.cs +++ b/src/Tgstation.Server.Host/Components/Watchdog/BasicWatchdog.cs @@ -159,12 +159,12 @@ namespace Tgstation.Server.Host.Components.Watchdog protected override async Task DisposeAndNullControllersImpl() { var disposeTask = Server?.DisposeAsync(); + gracefulRebootRequired = false; if (!disposeTask.HasValue) return; await disposeTask.Value.ConfigureAwait(false); Server = null; - gracefulRebootRequired = false; } /// diff --git a/src/Tgstation.Server.Host/Controllers/InstanceController.cs b/src/Tgstation.Server.Host/Controllers/InstanceController.cs index 13a29097d5..87229c6c41 100644 --- a/src/Tgstation.Server.Host/Controllers/InstanceController.cs +++ b/src/Tgstation.Server.Host/Controllers/InstanceController.cs @@ -1,4 +1,4 @@ -using Microsoft.AspNetCore.Mvc; +using Microsoft.AspNetCore.Mvc; using Microsoft.EntityFrameworkCore; using Microsoft.Extensions.Logging; using Microsoft.Extensions.Options; @@ -56,11 +56,6 @@ namespace Tgstation.Server.Host.Controllers /// readonly IIOManager ioManager; - /// - /// The for the - /// - readonly IAssemblyInformationProvider assemblyInformationProvider; - /// /// The for the /// @@ -79,7 +74,6 @@ namespace Tgstation.Server.Host.Controllers /// The value of /// The value of /// The value of - /// The value of /// The value of /// The containing the value of . /// The for the @@ -89,7 +83,6 @@ namespace Tgstation.Server.Host.Controllers IJobManager jobManager, IInstanceManager instanceManager, IIOManager ioManager, - IAssemblyInformationProvider assemblyInformationProvider, IPlatformIdentifier platformIdentifier, IOptions generalConfigurationOptions, ILogger logger) @@ -102,11 +95,51 @@ namespace Tgstation.Server.Host.Controllers 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.assemblyInformationProvider = assemblyInformationProvider ?? throw new ArgumentNullException(nameof(assemblyInformationProvider)); this.platformIdentifier = platformIdentifier ?? throw new ArgumentNullException(nameof(platformIdentifier)); generalConfiguration = generalConfigurationOptions?.Value ?? throw new ArgumentNullException(nameof(generalConfigurationOptions)); } + Models.Instance CreateDefaultInstance(Api.Models.Instance initialSettings) + => new Models.Instance + { + ConfigurationType = initialSettings.ConfigurationType ?? ConfigurationType.Disallowed, + DreamDaemonSettings = new DreamDaemonSettings + { + AllowWebClient = false, + AutoStart = false, + Port = 1337, + SecurityLevel = DreamDaemonSecurity.Safe, + StartupTimeout = 60, + HeartbeatSeconds = 60, + TopicRequestTimeout = generalConfiguration.ByondTopicTimeout + }, + DreamMakerSettings = new DreamMakerSettings + { + ApiValidationPort = 1339, + ApiValidationSecurityLevel = DreamDaemonSecurity.Safe, + RequireDMApiValidation = true + }, + Name = initialSettings.Name, + Online = false, + Path = initialSettings.Path, + AutoUpdateInterval = initialSettings.AutoUpdateInterval ?? 0, + ChatBotLimit = initialSettings.ChatBotLimit ?? Models.Instance.DefaultChatBotLimit, + RepositorySettings = new RepositorySettings + { + CommitterEmail = "tgstation-server@users.noreply.github.com", + CommitterName = "tgstation-server", + PushTestMergeCommits = false, + ShowTestMergeCommitters = false, + AutoUpdatesKeepTestMerges = false, + AutoUpdatesSynchronize = false, + PostTestMergeComment = false + }, + InstanceUsers = new List // give this user full privileges on the instance + { + InstanceAdminUser(null) + } + }; + string NormalizePath(string path) { if (path == null) @@ -243,45 +276,7 @@ namespace Tgstation.Server.Host.Controllers else attached = true; - var newInstance = new Models.Instance - { - ConfigurationType = model.ConfigurationType ?? ConfigurationType.Disallowed, - DreamDaemonSettings = new DreamDaemonSettings - { - AllowWebClient = false, - AutoStart = false, - Port = 1337, - SecurityLevel = DreamDaemonSecurity.Safe, - StartupTimeout = 60, - HeartbeatSeconds = 60, - TopicRequestTimeout = generalConfiguration.ByondTopicTimeout - }, - DreamMakerSettings = new DreamMakerSettings - { - ApiValidationPort = 1339, - ApiValidationSecurityLevel = DreamDaemonSecurity.Safe, - RequireDMApiValidation = true - }, - Name = model.Name, - Online = false, - Path = model.Path, - AutoUpdateInterval = model.AutoUpdateInterval ?? 0, - ChatBotLimit = model.ChatBotLimit ?? Models.Instance.DefaultChatBotLimit, - RepositorySettings = new RepositorySettings - { - CommitterEmail = "tgstation-server@users.noreply.github.com", - CommitterName = assemblyInformationProvider.VersionPrefix, - PushTestMergeCommits = false, - ShowTestMergeCommitters = false, - AutoUpdatesKeepTestMerges = false, - AutoUpdatesSynchronize = false, - PostTestMergeComment = false - }, - InstanceUsers = new List // give this user full privileges on the instance - { - InstanceAdminUser(null) - } - }; + var newInstance = CreateDefaultInstance(model); DatabaseContext.Instances.Add(newInstance); try diff --git a/src/Tgstation.Server.Host/Core/Application.cs b/src/Tgstation.Server.Host/Core/Application.cs index 5093e1083c..c1f5852419 100644 --- a/src/Tgstation.Server.Host/Core/Application.cs +++ b/src/Tgstation.Server.Host/Core/Application.cs @@ -1,4 +1,4 @@ -using Cyberboss.AspNetCore.AsyncInitializer; +using Cyberboss.AspNetCore.AsyncInitializer; using Microsoft.AspNetCore.Authentication.JwtBearer; using Microsoft.AspNetCore.Builder; using Microsoft.AspNetCore.Cors.Infrastructure; @@ -98,6 +98,10 @@ namespace Tgstation.Server.Host.Core // enable options which give us config reloading services.AddOptions(); + // Set the timeout for IHostedService.StopAsync + services.Configure( + opts => opts.ShutdownTimeout = TimeSpan.FromMilliseconds(postSetupServices.GeneralConfiguration.RestartTimeout)); + static LogEventLevel? ConvertSeriLogLevel(LogLevel logLevel) => logLevel switch { @@ -441,4 +445,4 @@ namespace Tgstation.Server.Host.Core // 404 anything that gets this far } } -} \ No newline at end of file +} diff --git a/src/Tgstation.Server.Host/Database/Migrations/20200422010115_MYAddCompileJobDMApiVersion.cs b/src/Tgstation.Server.Host/Database/Migrations/20200422010115_MYAddCompileJobDMApiVersion.cs index 16972b87d9..cb14d3256e 100644 --- a/src/Tgstation.Server.Host/Database/Migrations/20200422010115_MYAddCompileJobDMApiVersion.cs +++ b/src/Tgstation.Server.Host/Database/Migrations/20200422010115_MYAddCompileJobDMApiVersion.cs @@ -1,4 +1,4 @@ -using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Migrations; using System; namespace Tgstation.Server.Host.Database.Migrations @@ -55,8 +55,7 @@ namespace Tgstation.Server.Host.Database.Migrations migrationBuilder.AddColumn( name: "ServerCommandsJson", table: "ReattachInformations", - nullable: false, - defaultValue: "server_commands.tgs.json"); + nullable: false); } } } diff --git a/src/Tgstation.Server.Host/Extensions/ServiceCollectionExtensions.cs b/src/Tgstation.Server.Host/Extensions/ServiceCollectionExtensions.cs index b6c6e9e5d1..e98ff88694 100644 --- a/src/Tgstation.Server.Host/Extensions/ServiceCollectionExtensions.cs +++ b/src/Tgstation.Server.Host/Extensions/ServiceCollectionExtensions.cs @@ -1,4 +1,4 @@ -using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.Configuration; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Logging; using Serilog; diff --git a/src/Tgstation.Server.Host/Jobs/JobManager.cs b/src/Tgstation.Server.Host/Jobs/JobManager.cs index d59736c8c9..2442c0d87e 100644 --- a/src/Tgstation.Server.Host/Jobs/JobManager.cs +++ b/src/Tgstation.Server.Host/Jobs/JobManager.cs @@ -111,6 +111,7 @@ namespace Tgstation.Server.Host.Jobs await activationTcs.Task.WithToken(cancellationToken).ConfigureAwait(false); + logger.LogTrace("Starting job..."); await operation( instanceCoreProvider.Value.GetInstance(oldJob.Instance), databaseContextFactory, @@ -201,7 +202,7 @@ namespace Tgstation.Server.Host.Jobs await databaseContext.Save(cancellationToken).ConfigureAwait(false); - logger.LogDebug("Starting job {0}: {1}...", job.Id, job.Description); + logger.LogDebug("Registering job {0}: {1}...", job.Id, job.Description); var jobHandler = new JobHandler(jobCancellationToken => RunJob(job, operation, jobCancellationToken)); try { @@ -283,15 +284,12 @@ namespace Tgstation.Server.Host.Jobs await databaseContextFactory.UseContext(async databaseContext => { if (user == null) - { user = await databaseContext.Users.GetTgsUser(cancellationToken).ConfigureAwait(false); - databaseContext.Users.Attach(user); - } var updatedJob = new Job { Id = job.Id }; - databaseContext.Jobs.Attach(job); + databaseContext.Jobs.Attach(updatedJob); var attachedUser = new User { Id = user.Id }; - databaseContext.Users.Attach(user); + databaseContext.Users.Attach(attachedUser); updatedJob.CancelledBy = attachedUser; // let either startup or cancellation set job.cancelled diff --git a/src/Tgstation.Server.Host/Server.cs b/src/Tgstation.Server.Host/Server.cs index 2d5dc1bd80..d62ecccc87 100644 --- a/src/Tgstation.Server.Host/Server.cs +++ b/src/Tgstation.Server.Host/Server.cs @@ -1,4 +1,4 @@ -using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Hosting; using Microsoft.Extensions.Logging; using Microsoft.Extensions.Options; @@ -290,29 +290,29 @@ namespace Tgstation.Server.Host } if (exception == null) - using (var cts = new CancellationTokenSource()) - { - logger.LogInformation("Restarting server..."); - var cancellationToken = cts.Token; - var eventsTask = Task.WhenAll(restartHandlers.Select(x => x.HandleRestart(newVersion, cancellationToken)).ToList()); + { + logger.LogInformation("Restarting server..."); + using var cts = new CancellationTokenSource(TimeSpan.FromMilliseconds(generalConfiguration.RestartTimeout)); + var cancellationToken = cts.Token; + var eventsTask = Task.WhenAll( + restartHandlers.Select( + x => x.HandleRestart(newVersion, cancellationToken)) + .ToList()); - var expiryTask = Task.Delay(TimeSpan.FromMilliseconds(generalConfiguration.RestartTimeout)); - await Task.WhenAny(eventsTask, expiryTask).ConfigureAwait(false); - logger.LogTrace("Joining restart handlers..."); - cts.Cancel(); - try - { - await eventsTask.ConfigureAwait(false); - } - catch (OperationCanceledException) - { - logger.LogError("Restart timeout hit! Existing DreamDaemon processes will be lost and must be killed manually before being restarted with TGS!"); - } - catch (Exception e) - { - logger.LogError("Restart handlers error! Exception: {0}", e); - } + logger.LogTrace("Joining restart handlers..."); + try + { + await eventsTask.ConfigureAwait(false); } + catch (OperationCanceledException) + { + logger.LogError("Restart timeout hit! Existing DreamDaemon processes will be lost and must be killed manually before being restarted with TGS!"); + } + catch (Exception e) + { + logger.LogError("Restart handlers error! Exception: {0}", e); + } + } logger.LogTrace("Stopping host..."); cancellationTokenSource.Cancel(); diff --git a/src/Tgstation.Server.Host/System/Process.cs b/src/Tgstation.Server.Host/System/Process.cs index 5f2c3cf5a6..1800b77563 100644 --- a/src/Tgstation.Server.Host/System/Process.cs +++ b/src/Tgstation.Server.Host/System/Process.cs @@ -1,4 +1,4 @@ -using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Logging; using System; using System.Diagnostics; using System.Text; @@ -10,6 +10,11 @@ namespace Tgstation.Server.Host.System /// sealed class Process : IProcess { + /// + /// Maximum time to wait in a call to . + /// + const int MaximumWaitMilliseconds = 30000; + /// public int Id { get; } @@ -31,6 +36,11 @@ namespace Tgstation.Server.Host.System readonly global::System.Diagnostics.Process handle; + /// + /// A so that we can complete if the becomes unresponsive. + /// + readonly TaskCompletionSource emergencyLifetimeTcs; + readonly StringBuilder outputStringBuilder; readonly StringBuilder errorStringBuilder; readonly StringBuilder combinedStringBuilder; @@ -65,6 +75,7 @@ namespace Tgstation.Server.Host.System this.logger = logger ?? throw new ArgumentNullException(nameof(logger)); + emergencyLifetimeTcs = new TaskCompletionSource(); Lifetime = WrapLifetimeTask(lifetime ?? throw new ArgumentNullException(nameof(lifetime))); Id = handle.Id; @@ -92,9 +103,12 @@ namespace Tgstation.Server.Host.System async Task WrapLifetimeTask(Task lifetimeTask) { - var result = await lifetimeTask.ConfigureAwait(false); - logger.LogTrace("PID {0} ended with code {1}", Id, result); - return result; + await Task.WhenAny(lifetimeTask, emergencyLifetimeTcs.Task).ConfigureAwait(false); + if (lifetimeTask.IsCompleted) + return await lifetimeTask.ConfigureAwait(false); + + logger.LogTrace("Using exit code -1 for hung PID {0}.", Id); + return -1; } /// @@ -130,7 +144,15 @@ namespace Tgstation.Server.Host.System { logger.LogTrace("Terminating PID {0}...", Id); handle.Kill(); - handle.WaitForExit(); + if (!handle.WaitForExit(MaximumWaitMilliseconds)) + { + logger.LogError( + "PID {0} hasn't exited in {1} seconds! This may cause issues with port reuse.", + Id, + TimeSpan.FromMilliseconds(MaximumWaitMilliseconds).TotalSeconds); + + emergencyLifetimeTcs.TrySetResult(null); + } } catch (Exception e) { diff --git a/src/Tgstation.Server.Host/System/ProcessExecutor.cs b/src/Tgstation.Server.Host/System/ProcessExecutor.cs index 9284a0703e..8abefc2cc7 100644 --- a/src/Tgstation.Server.Host/System/ProcessExecutor.cs +++ b/src/Tgstation.Server.Host/System/ProcessExecutor.cs @@ -1,4 +1,4 @@ -using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Logging; using System; using System.Text; using System.Threading.Tasks; @@ -28,7 +28,7 @@ namespace Tgstation.Server.Host.System /// /// The to attach the for /// A new resulting in the exit code of - static Task AttachExitHandler(global::System.Diagnostics.Process handle) + Task AttachExitHandler(global::System.Diagnostics.Process handle) { handle.EnableRaisingEvents = true; var tcs = new TaskCompletionSource(); @@ -45,7 +45,8 @@ namespace Tgstation.Server.Host.System } // Try because this can be invoked twice for weird reasons - tcs.TrySetResult(exitCode); + if (tcs.TrySetResult(exitCode)) + logger.LogTrace("Process exit event completed"); }; return tcs.Task; diff --git a/src/Tgstation.Server.Host/Tgstation.Server.Host.csproj b/src/Tgstation.Server.Host/Tgstation.Server.Host.csproj index 0222a5eb92..ba065d5aa6 100644 --- a/src/Tgstation.Server.Host/Tgstation.Server.Host.csproj +++ b/src/Tgstation.Server.Host/Tgstation.Server.Host.csproj @@ -1,4 +1,4 @@ - + @@ -69,7 +69,7 @@ - + diff --git a/tests/DMAPI/BasicOperation/Test.dm b/tests/DMAPI/BasicOperation/Test.dm index 066c9eceac..0fc07d72f9 100644 --- a/tests/DMAPI/BasicOperation/Test.dm +++ b/tests/DMAPI/BasicOperation/Test.dm @@ -21,7 +21,7 @@ text2file("DMAPI version [TGS_DMAPI_VERSION] does not match active API version [active_version.raw_parameter]", "test_fail_reason.txt") world.log << "sleep2" - sleep(50) + sleep(150) world.log << "Terminating..." world.TgsEndProcess() diff --git a/tests/Tgstation.Server.Tests/Instance/DeploymentTest.cs b/tests/Tgstation.Server.Tests/Instance/DeploymentTest.cs index 7772e32343..578dffa9a5 100644 --- a/tests/Tgstation.Server.Tests/Instance/DeploymentTest.cs +++ b/tests/Tgstation.Server.Tests/Instance/DeploymentTest.cs @@ -1,4 +1,4 @@ -using System; +using System; using System.Threading; using System.Threading.Tasks; using Microsoft.VisualStudio.TestTools.UnitTesting; @@ -36,7 +36,8 @@ namespace Tgstation.Server.Tests.Instance if (!new PlatformIdentifier().IsWindows) await dreamMakerClient.Update(new DreamMaker { - ProjectName = "tests/DMAPI/ApiFree/api_free" + ProjectName = "tests/DMAPI/ApiFree/api_free", + ApiValidationPort = IntegrationTest.DMPort }, cancellationToken); var updatedDD = await dreamDaemonClient.Update(new DreamDaemon diff --git a/tests/Tgstation.Server.Tests/Instance/WatchdogTest.cs b/tests/Tgstation.Server.Tests/Instance/WatchdogTest.cs index 9ae2620165..4b2e5e817d 100644 --- a/tests/Tgstation.Server.Tests/Instance/WatchdogTest.cs +++ b/tests/Tgstation.Server.Tests/Instance/WatchdogTest.cs @@ -36,7 +36,8 @@ namespace Tgstation.Server.Tests.Instance var initialSettings = await instanceClient.DreamDaemon.Update(new DreamDaemon { StartupTimeout = 30, - HeartbeatSeconds = 0 + HeartbeatSeconds = 0, + Port = IntegrationTest.DDPort }, cancellationToken); await ApiAssert.ThrowsException(() => instanceClient.DreamDaemon.Update(new DreamDaemon @@ -87,7 +88,7 @@ namespace Tgstation.Server.Tests.Instance KillDD(false); var job = await WaitForJob(await dumpTask, 10, true, null, cancellationToken); Assert.IsTrue(job.ErrorCode == ErrorCode.DreamDaemonOffline || job.ErrorCode == ErrorCode.GCoreFailure); - await Task.Delay(TimeSpan.FromSeconds(10), cancellationToken); + await Task.Delay(TimeSpan.FromSeconds(20), cancellationToken); var ddStatus = await instanceClient.DreamDaemon.Read(cancellationToken); Assert.AreEqual(WatchdogStatus.Online, ddStatus.Status.Value); @@ -145,7 +146,7 @@ namespace Tgstation.Server.Tests.Instance { blockSocket.SetSocketOption(SocketOptionLevel.Socket, SocketOptionName.ExclusiveAddressUse, true); blockSocket.SetSocketOption(SocketOptionLevel.Socket, SocketOptionName.ReuseAddress, false); - blockSocket.Bind(new IPEndPoint(IPAddress.Any, 1337)); + blockSocket.Bind(new IPEndPoint(IPAddress.Any, IntegrationTest.DDPort)); startJob = await instanceClient.DreamDaemon.Start(cancellationToken).ConfigureAwait(false); await WaitForJob(startJob, 20, true, ErrorCode.DreamDaemonPortInUse, cancellationToken); @@ -160,7 +161,7 @@ namespace Tgstation.Server.Tests.Instance Assert.AreEqual(false, daemonStatus.SoftRestart); Assert.AreEqual(false, daemonStatus.SoftShutdown); - await GracefulWatchdogShutdown(30, cancellationToken); + await GracefulWatchdogShutdown(60, cancellationToken); daemonStatus = await instanceClient.DreamDaemon.Read(cancellationToken); Assert.AreEqual(WatchdogStatus.Offline, daemonStatus.Status.Value); @@ -170,7 +171,7 @@ namespace Tgstation.Server.Tests.Instance async Task RunHeartbeatTest(CancellationToken cancellationToken) { - global::System.Console.WriteLine("TEST: WATCHDOG HEARTBEAT TEST"); + System.Console.WriteLine("TEST: WATCHDOG HEARTBEAT TEST"); // enable heartbeats await instanceClient.DreamDaemon.Update(new DreamDaemon { @@ -210,7 +211,7 @@ namespace Tgstation.Server.Tests.Instance await Task.WhenAny(ourProcessHandler.Lifetime, Task.Delay(TimeSpan.FromSeconds(20))); - var timeout = 10; + var timeout = 20; do { await Task.Delay(TimeSpan.FromSeconds(1), cancellationToken).ConfigureAwait(false); @@ -260,9 +261,8 @@ namespace Tgstation.Server.Tests.Instance Assert.AreNotEqual(initialCompileJob.Id, newerCompileJob.Id); Assert.AreEqual(DreamDaemonSecurity.Ultrasafe, newerCompileJob.MinimumSecurityLevel); - await TellWorldToReboot(cancellationToken); + daemonStatus = await TellWorldToReboot(cancellationToken); - daemonStatus = await instanceClient.DreamDaemon.Read(cancellationToken); Assert.AreNotEqual(initialCompileJob.Id, daemonStatus.ActiveCompileJob.Id); Assert.IsNull(daemonStatus.StagedCompileJob); @@ -301,9 +301,8 @@ namespace Tgstation.Server.Tests.Instance Assert.AreNotEqual(initialCompileJob.Id, newerCompileJob.Id); Assert.AreEqual(DreamDaemonSecurity.Ultrasafe, newerCompileJob.MinimumSecurityLevel); - await TellWorldToReboot(cancellationToken); + daemonStatus = await TellWorldToReboot(cancellationToken); - daemonStatus = await instanceClient.DreamDaemon.Read(cancellationToken); Assert.AreNotEqual(initialCompileJob.Id, daemonStatus.ActiveCompileJob.Id); Assert.IsNull(daemonStatus.StagedCompileJob); @@ -354,9 +353,8 @@ namespace Tgstation.Server.Tests.Instance Assert.AreEqual(true, daemonStatus.SoftRestart); - await TellWorldToReboot(cancellationToken); + daemonStatus = await TellWorldToReboot(cancellationToken); - daemonStatus = await instanceClient.DreamDaemon.Read(cancellationToken); Assert.AreEqual(versionToInstall, daemonStatus.ActiveCompileJob.ByondVersion); Assert.IsNull(daemonStatus.StagedCompileJob); @@ -379,6 +377,7 @@ namespace Tgstation.Server.Tests.Instance var daemonStatus = await instanceClient.DreamDaemon.Read(cancellationToken); Assert.AreEqual(WatchdogStatus.Online, daemonStatus.Status.Value); + Assert.AreEqual(IntegrationTest.DDPort, daemonStatus.CurrentPort); // The measure we use to test dream daemon startup doesn't work on linux currently if (new PlatformIdentifier().IsWindows) @@ -422,28 +421,45 @@ namespace Tgstation.Server.Tests.Instance return ddProc != null; } - async Task TellWorldToReboot(CancellationToken cancellationToken) + async Task TellWorldToReboot(CancellationToken cancellationToken) { + var daemonStatus = await instanceClient.DreamDaemon.Read(cancellationToken); + var initialCompileJob = daemonStatus.ActiveCompileJob; + var bts = new TopicClient(new SocketParameters { - SendTimeout = TimeSpan.FromSeconds(15), - ReceiveTimeout = TimeSpan.FromSeconds(15), - ConnectTimeout = TimeSpan.FromSeconds(15), - DisconnectTimeout = TimeSpan.FromSeconds(15) + SendTimeout = TimeSpan.FromSeconds(30), + ReceiveTimeout = TimeSpan.FromSeconds(30), + ConnectTimeout = TimeSpan.FromSeconds(30), + DisconnectTimeout = TimeSpan.FromSeconds(30) }); try { - global::System.Console.WriteLine("TEST: Sending world reboot topic..."); - var result = await bts.SendTopic(IPAddress.Loopback, "tgs_integration_test_special_tactics=1", 1337, cancellationToken); + System.Console.WriteLine("TEST: Sending world reboot topic..."); + var result = await bts.SendTopic(IPAddress.Loopback, "tgs_integration_test_special_tactics=1", IntegrationTest.DDPort, cancellationToken); Assert.AreEqual("ack", result.StringData); - await Task.Delay(20000, cancellationToken); + using (var tempCts = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken)) + using (tempCts.Token.Register(() => System.Console.WriteLine("TEST ERROR: Timeout in TellWorldToReboot!"))) + { + tempCts.CancelAfter(TimeSpan.FromMinutes(1)); + var tempToken = tempCts.Token; + + do + { + await Task.Delay(TimeSpan.FromSeconds(1), cancellationToken).ConfigureAwait(false); + daemonStatus = await instanceClient.DreamDaemon.Read(cancellationToken); + } + while (initialCompileJob.Id == daemonStatus.ActiveCompileJob.Id && !tempToken.IsCancellationRequested); + } } catch (OperationCanceledException) { throw; } + + return daemonStatus; } async Task DeployTestDme(string dmeName, DreamDaemonSecurity deploymentSecurity, bool requireApi, CancellationToken cancellationToken) diff --git a/tests/Tgstation.Server.Tests/IntegrationTest.cs b/tests/Tgstation.Server.Tests/IntegrationTest.cs index 77fc00eae6..496fa12d7a 100644 --- a/tests/Tgstation.Server.Tests/IntegrationTest.cs +++ b/tests/Tgstation.Server.Tests/IntegrationTest.cs @@ -199,7 +199,11 @@ namespace Tgstation.Server.Tests } using var server = new TestingServer(); - using var serverCts = new CancellationTokenSource(); + + const int MaximumTestMinutes = 20; + using var hardTimeoutCancellationTokenSource = new CancellationTokenSource(TimeSpan.FromMinutes(MaximumTestMinutes)); + var hardCancellationToken = hardTimeoutCancellationTokenSource.Token; + using var serverCts = CancellationTokenSource.CreateLinkedTokenSource(hardCancellationToken); var cancellationToken = serverCts.Token; TerminateAllDDs(); @@ -259,8 +263,9 @@ namespace Tgstation.Server.Tests Assert.IsTrue(serverTask.IsCompleted); // http bind test https://github.com/tgstation/tgstation-server/issues/1065 - using (var blockingSocket = new Socket(SocketType.Stream, ProtocolType.Tcp)) + if (new PlatformIdentifier().IsWindows) { + using var blockingSocket = new Socket(SocketType.Stream, ProtocolType.Tcp); blockingSocket.SetSocketOption(SocketOptionLevel.Socket, SocketOptionName.ExclusiveAddressUse, true); blockingSocket.SetSocketOption(SocketOptionLevel.Socket, SocketOptionName.ReuseAddress, false); blockingSocket.Bind(new IPEndPoint(IPAddress.Any, server.Url.Port)); @@ -343,12 +348,12 @@ namespace Tgstation.Server.Tests .ToList(); } - Assert.AreEqual(1, jobs.Count); - - var launchJob = jobs.Single(); - Assert.IsTrue(launchJob.StartedAt.Value >= preStartupTime); - - await new JobsRequiredTest(instanceClient.Jobs).WaitForJob(launchJob, 40, false, null, cancellationToken); + var jrt = new JobsRequiredTest(instanceClient.Jobs); + foreach (var job in jobs) + { + Assert.IsTrue(job.StartedAt.Value >= preStartupTime); + await jrt.WaitForJob(job, 40, false, null, cancellationToken); + } var dd = await instanceClient.DreamDaemon.Read(cancellationToken); @@ -374,12 +379,9 @@ namespace Tgstation.Server.Tests finally { serverCts.Cancel(); - - // Give the test 1 minute to cleanup - using var hardTimeoutCancellationTokenSource = new CancellationTokenSource(TimeSpan.FromMinutes(1)); try { - await serverTask.WithToken(hardTimeoutCancellationTokenSource.Token).ConfigureAwait(false); + await serverTask.WithToken(hardCancellationToken).ConfigureAwait(false); } catch (OperationCanceledException) { } @@ -390,6 +392,33 @@ namespace Tgstation.Server.Tests await serverTask; } + public static ushort DDPort = FreeTcpPort(); + public static ushort DMPort = GetDMPort(); + + static ushort GetDMPort() + { + ushort result; + do + { + result = FreeTcpPort(); + } while (result == DDPort); + return result; + } + + static ushort FreeTcpPort() + { + var l = new TcpListener(IPAddress.Loopback, 0); + l.Start(); + try + { + return (ushort)((IPEndPoint)l.LocalEndpoint).Port; + } + finally + { + l.Stop(); + } + } + [TestMethod] public async Task TestScriptExecution() { From d460330cabb0ee88285ea8479d9b8d43d9b337f8 Mon Sep 17 00:00:00 2001 From: Jordan Brown Date: Sat, 25 Jul 2020 15:53:50 -0400 Subject: [PATCH 43/68] Log message cleanup --- .../Watchdog.cs | 18 +++--- .../Components/Byond/ByondInstallerBase.cs | 4 +- .../Components/Chat/ChatManager.cs | 18 +++--- .../Chat/Providers/DiscordProvider.cs | 8 +-- .../Components/Chat/Providers/IrcProvider.cs | 12 ++-- .../Components/Deployment/DmbFactory.cs | 2 +- .../Components/Deployment/DreamMaker.cs | 4 +- .../Components/Instance.cs | 2 +- .../Components/InstanceManager.cs | 25 ++++---- .../Repository/LibGit2RepositoryFactory.cs | 16 +---- .../Components/Repository/Repository.cs | 6 +- .../Repository/RepositoryManager.cs | 7 +-- .../Components/Session/SessionController.cs | 9 +-- .../Session/SessionControllerFactory.cs | 4 +- .../Components/Session/SessionPersistor.cs | 4 +- .../Components/StaticFiles/Configuration.cs | 2 +- .../Components/Watchdog/WatchdogBase.cs | 19 +++--- .../Components/Watchdog/WindowsWatchdog.cs | 4 +- .../Controllers/AdministrationController.cs | 12 ++-- .../Controllers/BridgeController.cs | 12 ++-- .../Controllers/InstanceController.cs | 2 +- .../Controllers/RepositoryController.cs | 4 +- .../Database/DatabaseContext.cs | 4 +- .../Database/DatabaseSeeder.cs | 4 +- .../ApplicationBuilderExtensions.cs | 12 ++-- src/Tgstation.Server.Host/Jobs/JobManager.cs | 20 +++--- .../Security/WindowsSystemIdentityFactory.cs | 10 +-- src/Tgstation.Server.Host/Server.cs | 10 +-- .../System/PosixProcessFeatures.cs | 30 ++------- src/Tgstation.Server.Host/System/Process.cs | 40 ++++++++++-- .../System/ProcessExecutor.cs | 6 +- .../System/WindowsNetworkPromptReaper.cs | 8 +-- .../System/WindowsProcessFeatures.cs | 62 ++++++------------- .../System/TestProcessFeatures.cs | 4 +- .../Instance/WatchdogTest.cs | 2 +- 35 files changed, 192 insertions(+), 214 deletions(-) diff --git a/src/Tgstation.Server.Host.Watchdog/Watchdog.cs b/src/Tgstation.Server.Host.Watchdog/Watchdog.cs index caec57251d..4f2e4ae68d 100644 --- a/src/Tgstation.Server.Host.Watchdog/Watchdog.cs +++ b/src/Tgstation.Server.Host.Watchdog/Watchdog.cs @@ -1,4 +1,4 @@ -using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Logging; using System; using System.Collections.Generic; using System.Diagnostics; @@ -231,7 +231,7 @@ namespace Tgstation.Server.Host.Watchdog } catch (Exception e) { - logger.LogWarning("Unable to delete exception dump file at {0}! Exception: {1}", updateDirectory, e); + logger.LogWarning(e, "Unable to delete exception dump file at {0}!", updateDirectory); } #pragma warning disable CA2201 // Do not raise reserved exception types @@ -277,12 +277,12 @@ namespace Tgstation.Server.Host.Watchdog } catch (Exception e) { - logger.LogWarning("Error deleting old server at {0}! Exception: {1}", tempPath, e); + logger.LogWarning(e, "Error deleting old server at {0}!", tempPath); } } catch (Exception e) { - logger.LogError("Error moving updated server directory, attempting revert! Exception: {0}", e); + logger.LogError(e, "Error moving updated server directory, attempting revert!"); Directory.Delete(defaultAssemblyPath, true); Directory.Move(tempPath, defaultAssemblyPath); logger.LogInformation("Revert successful!"); @@ -290,22 +290,22 @@ namespace Tgstation.Server.Host.Watchdog } catch (Exception e) { - logger.LogWarning("Failed to move out active host assembly! Exception: {0}", e); + logger.LogWarning(e, "Failed to move out active host assembly!"); } } } } - catch (OperationCanceledException) + catch (OperationCanceledException ex) { - logger.LogDebug("Exiting due to cancellation..."); + logger.LogDebug(ex, "Exiting due to cancellation..."); if (!Directory.Exists(updateDirectory)) File.Delete(updateDirectory); else Directory.Delete(updateDirectory, true); } - catch (Exception e) + catch (Exception ex) { - logger.LogCritical("Watchdog error! Exception: {0}", e); + logger.LogCritical(ex, "Host watchdog error!"); } finally { diff --git a/src/Tgstation.Server.Host/Components/Byond/ByondInstallerBase.cs b/src/Tgstation.Server.Host/Components/Byond/ByondInstallerBase.cs index fe9bd1c716..2993e1df45 100644 --- a/src/Tgstation.Server.Host/Components/Byond/ByondInstallerBase.cs +++ b/src/Tgstation.Server.Host/Components/Byond/ByondInstallerBase.cs @@ -1,4 +1,4 @@ -using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Logging; using System; using System.Globalization; using System.Threading; @@ -69,7 +69,7 @@ namespace Tgstation.Server.Host.Components.Byond } catch (Exception e) { - Logger.LogWarning("Error deleting BYOND cache! Exception: {0}", e); + Logger.LogWarning(e, "Error deleting BYOND cache!"); } } diff --git a/src/Tgstation.Server.Host/Components/Chat/ChatManager.cs b/src/Tgstation.Server.Host/Components/Chat/ChatManager.cs index 05f2c19ee4..cec60f89b9 100644 --- a/src/Tgstation.Server.Host/Components/Chat/ChatManager.cs +++ b/src/Tgstation.Server.Host/Components/Chat/ChatManager.cs @@ -373,15 +373,15 @@ namespace Tgstation.Server.Host.Components.Chat if (result != null) await SendMessage(result, new List { message.User.Channel.RealId }, cancellationToken).ConfigureAwait(false); } - catch (OperationCanceledException) + catch (OperationCanceledException ex) { - logger.LogTrace("Command processing canceled!"); + logger.LogTrace(ex, "Command processing canceled!"); throw; } catch (Exception e) { // error bc custom commands should reply about why it failed - logger.LogError("Error processing chat command: {0}", e); + logger.LogError(e, "Error processing chat command"); await SendMessage( "TGS: Internal error processing command! Check server logs!", new List { message.User.Channel.RealId }, @@ -440,13 +440,13 @@ namespace Tgstation.Server.Host.Components.Chat } } } - catch (OperationCanceledException) + catch (OperationCanceledException ex) { - logger.LogTrace("Message processing loop cancelled!"); + logger.LogTrace(ex, "Message processing loop cancelled!"); } catch (Exception e) { - logger.LogError("Message loop crashed! Exception: {0}", e); + logger.LogError(e, "Message loop crashed!"); } logger.LogTrace("Leaving message processing loop"); @@ -694,9 +694,9 @@ namespace Tgstation.Server.Host.Components.Chat catch (Exception ex) { logger.LogWarning( - "Error sending deploy message to provider {0}! Exception: {1}", - channelMapping.ProviderId, - ex); + ex, + "Error sending deploy message to provider {0}!", + channelMapping.ProviderId); } })) .ConfigureAwait(false); diff --git a/src/Tgstation.Server.Host/Components/Chat/Providers/DiscordProvider.cs b/src/Tgstation.Server.Host/Components/Chat/Providers/DiscordProvider.cs index 6ba2897ba5..493a087f7b 100644 --- a/src/Tgstation.Server.Host/Components/Chat/Providers/DiscordProvider.cs +++ b/src/Tgstation.Server.Host/Components/Chat/Providers/DiscordProvider.cs @@ -194,7 +194,7 @@ namespace Tgstation.Server.Host.Components.Chat.Providers } catch (Exception e) { - Logger.LogWarning("Error disconnecting from discord: {0}", e); + Logger.LogWarning(e, "Error disconnecting from discord!"); } } @@ -265,7 +265,7 @@ namespace Tgstation.Server.Host.Components.Chat.Providers } catch (Exception e) { - Logger.LogWarning("Error sending discord message: {0}", e); + Logger.LogWarning(e, "Error sending discord message!"); } } @@ -391,7 +391,7 @@ namespace Tgstation.Server.Host.Components.Chat.Providers } catch (Exception ex) { - Logger.LogWarning("Updating deploy embed {0} failed, attempting new post! Exception: {1}", message.Id, ex); + Logger.LogWarning(ex, "Updating deploy embed {0} failed, attempting new post!", message.Id); try { await channel.SendMessageAsync( @@ -402,7 +402,7 @@ namespace Tgstation.Server.Host.Components.Chat.Providers } catch (Exception ex2) { - Logger.LogWarning("Posting completion deploy embed failed! Exception: {0}", ex2); + Logger.LogWarning(ex2, "Posting completion deploy embed failed!"); } } }; diff --git a/src/Tgstation.Server.Host/Components/Chat/Providers/IrcProvider.cs b/src/Tgstation.Server.Host/Components/Chat/Providers/IrcProvider.cs index 7db05d816d..aa63d7b6fa 100644 --- a/src/Tgstation.Server.Host/Components/Chat/Providers/IrcProvider.cs +++ b/src/Tgstation.Server.Host/Components/Chat/Providers/IrcProvider.cs @@ -296,7 +296,7 @@ namespace Tgstation.Server.Host.Components.Chat.Providers } catch (Exception ex) { - Logger.LogWarning("IRC Listen Error: {0}", ex); + Logger.LogWarning(ex, "IRC Main Listen Exception!"); } Logger.LogTrace("Exiting listening task..."); @@ -334,7 +334,7 @@ namespace Tgstation.Server.Host.Components.Chat.Providers } catch (Exception ex) { - Logger.LogWarning("IRC Listen Error: {0}", ex); + Logger.LogWarning(ex, "IRC Non-Blocking Listen Exception!"); } }, cancellationToken, @@ -426,7 +426,7 @@ namespace Tgstation.Server.Host.Components.Chat.Providers } catch (Exception e) { - Logger.LogWarning("Error quitting IRC: {0}", e); + Logger.LogWarning(e, "Error quitting IRC!"); } }, cancellationToken, @@ -441,7 +441,7 @@ namespace Tgstation.Server.Host.Components.Chat.Providers } catch (Exception e) { - Logger.LogWarning("Error disconnecting from IRC! Exception: {0}", e); + Logger.LogWarning(e, "Error disconnecting from IRC!"); } } @@ -468,7 +468,7 @@ namespace Tgstation.Server.Host.Components.Chat.Providers } catch (Exception e) { - Logger.LogWarning("Error disconnecting IRC: {0}", e); + Logger.LogWarning(e, "Error disconnecting IRC!"); } }, cancellationToken, @@ -571,7 +571,7 @@ namespace Tgstation.Server.Host.Components.Chat.Providers } catch (Exception e) { - Logger.LogWarning("Unable to send to channel: {0}", e); + Logger.LogWarning(e, "Unable to send to channel {0}!", channelName); } }, cancellationToken, TaskCreationOptions.LongRunning, TaskScheduler.Current); diff --git a/src/Tgstation.Server.Host/Components/Deployment/DmbFactory.cs b/src/Tgstation.Server.Host/Components/Deployment/DmbFactory.cs index 1d5c26b26d..7e9b0b7767 100644 --- a/src/Tgstation.Server.Host/Components/Deployment/DmbFactory.cs +++ b/src/Tgstation.Server.Host/Components/Deployment/DmbFactory.cs @@ -358,7 +358,7 @@ namespace Tgstation.Server.Host.Components.Deployment } catch (Exception e) { - logger.LogWarning("Error deleting directory {0}! Exception: {1}", x, e); + logger.LogWarning(e, "Error deleting directory {0}!", x); } }).ToList(); if (deleting > 0) diff --git a/src/Tgstation.Server.Host/Components/Deployment/DreamMaker.cs b/src/Tgstation.Server.Host/Components/Deployment/DreamMaker.cs index 434cb9a781..34b5896aac 100644 --- a/src/Tgstation.Server.Host/Components/Deployment/DreamMaker.cs +++ b/src/Tgstation.Server.Host/Components/Deployment/DreamMaker.cs @@ -365,7 +365,7 @@ namespace Tgstation.Server.Host.Components.Deployment } catch (Exception e) { - logger.LogWarning("Error cleaning up compile directory {0}! Exception: {1}", ioManager.ResolvePath(jobPath), e); + logger.LogWarning(e, "Error cleaning up compile directory {0}!", ioManager.ResolvePath(jobPath)); } } @@ -825,7 +825,7 @@ namespace Tgstation.Server.Host.Components.Deployment } catch (ApiException e) { - logger.LogWarning("Error posting GitHub comment! Exception: {0}", e); + logger.LogWarning(e, "Error posting GitHub comment!"); } } diff --git a/src/Tgstation.Server.Host/Components/Instance.cs b/src/Tgstation.Server.Host/Components/Instance.cs index 1553263b9f..83e4bca775 100644 --- a/src/Tgstation.Server.Host/Components/Instance.cs +++ b/src/Tgstation.Server.Host/Components/Instance.cs @@ -382,7 +382,7 @@ namespace Tgstation.Server.Host.Components } catch (Exception e) { - logger.LogWarning("Error in auto update loop! Exception: {0}", e); + 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 094b3d4db1..3684cdb799 100644 --- a/src/Tgstation.Server.Host/Components/InstanceManager.cs +++ b/src/Tgstation.Server.Host/Components/InstanceManager.cs @@ -232,9 +232,9 @@ namespace Tgstation.Server.Host.Components catch (Exception ex) { logger.LogError( - "Error moving instance {0}! Exception: {2}", - instance.Id, - ex); + ex, + "Error moving instance {0}!", + instance.Id); try { logger.LogDebug("Reverting instance {0}'s path to {1} in the DB...", instance.Id, oldPath); @@ -254,8 +254,8 @@ namespace Tgstation.Server.Host.Components catch (Exception innerEx) { logger.LogCritical( - "Error reverting database after failing to move instance {0}! Attempting to detach. Exception: {1}", - ex); + innerEx, + "Error reverting database after failing to move instance {0}! Attempting to detach..."); try { @@ -269,8 +269,8 @@ namespace Tgstation.Server.Host.Components catch (Exception tripleEx) { logger.LogCritical( - "Okay, what gamma radiation are you under? Failed to write instance attach file! Exception: {0}", - tripleEx); + tripleEx, + "Okay, what gamma radiation are you under? Failed to write instance attach file!"); throw new AggregateException(tripleEx, innerEx, ex); } @@ -366,12 +366,15 @@ namespace Tgstation.Server.Host.Components logger.LogError("Unable to commit onlined instance {0} into service, offlining!", metadata.Id); try { + // DCT: Must always run await instance.StopAsync(default).ConfigureAwait(false); } catch (Exception innerEx) { throw new AggregateException(innerEx, ex); } + + throw; } } catch @@ -419,7 +422,7 @@ namespace Tgstation.Server.Host.Components } catch (Exception ex) { - logger.LogError("Failed to online instance {0}! Exception: {0}", ex); + logger.LogError(ex, "Failed to online instance {0}!"); } }) .ToList(); @@ -436,7 +439,7 @@ namespace Tgstation.Server.Host.Components } catch (Exception e) { - logger.LogCritical("Instance manager startup error! Exception: {0}", e); + logger.LogCritical(e, "Instance manager startup error!"); try { await serverControl.Die(e).ConfigureAwait(false); @@ -444,7 +447,7 @@ namespace Tgstation.Server.Host.Components } catch (Exception e2) { - logger.LogCritical("Failed to kill server! Exception: {0}", e2); + logger.LogCritical(e2, "Failed to kill server!"); } throw; @@ -467,7 +470,7 @@ namespace Tgstation.Server.Host.Components } catch (Exception ex) { - logger.LogError("Instance manager stop exception: {0}", ex); + logger.LogError(ex, "Instance manager stop exception!"); } } diff --git a/src/Tgstation.Server.Host/Components/Repository/LibGit2RepositoryFactory.cs b/src/Tgstation.Server.Host/Components/Repository/LibGit2RepositoryFactory.cs index 69059f93cc..4e24002bb7 100644 --- a/src/Tgstation.Server.Host/Components/Repository/LibGit2RepositoryFactory.cs +++ b/src/Tgstation.Server.Host/Components/Repository/LibGit2RepositoryFactory.cs @@ -1,4 +1,4 @@ -using LibGit2Sharp; +using LibGit2Sharp; using LibGit2Sharp.Handlers; using Microsoft.Extensions.Logging; using System; @@ -30,17 +30,7 @@ namespace Tgstation.Server.Host.Components.Repository public LibGit2Sharp.IRepository CreateInMemory() { logger.LogTrace("Creating in-memory libgit2 repository..."); - var repo = new LibGit2Sharp.Repository(); - try - { - logger.LogTrace("Successfully created in-memory libgit2 repository."); - return repo; - } - catch - { - repo.Dispose(); - throw; - } + return new LibGit2Sharp.Repository(); } /// @@ -70,7 +60,7 @@ namespace Tgstation.Server.Host.Components.Repository } catch (UserCancelledException ex) { - logger.LogTrace("Suppressing clone cancellation exception: {0}", ex); + logger.LogTrace(ex, "Suppressing clone cancellation exception"); cancellationToken.ThrowIfCancellationRequested(); } }, cancellationToken, TaskCreationOptions.LongRunning, TaskScheduler.Current); diff --git a/src/Tgstation.Server.Host/Components/Repository/Repository.cs b/src/Tgstation.Server.Host/Components/Repository/Repository.cs index f9d2ccc9b9..4db0962c02 100644 --- a/src/Tgstation.Server.Host/Components/Repository/Repository.cs +++ b/src/Tgstation.Server.Host/Components/Repository/Repository.cs @@ -1,4 +1,4 @@ -using LibGit2Sharp; +using LibGit2Sharp; using LibGit2Sharp.Handlers; using Microsoft.Extensions.Logging; using System; @@ -475,7 +475,7 @@ namespace Tgstation.Server.Host.Components.Repository } catch(LibGit2SharpException e) { - logger.LogWarning("Unable to push to temporary branch! Exception: {0}", e); + logger.LogWarning(e, "Unable to push to temporary branch!"); } } finally @@ -683,7 +683,7 @@ namespace Tgstation.Server.Host.Components.Repository } catch (LibGit2SharpException e) { - logger.LogWarning("Unable to make synchronization push! Exception: {0}", e); + logger.LogWarning(e, "Unable to make synchronization push!"); return false; } }, cancellationToken, TaskCreationOptions.LongRunning, TaskScheduler.Current).ConfigureAwait(false); diff --git a/src/Tgstation.Server.Host/Components/Repository/RepositoryManager.cs b/src/Tgstation.Server.Host/Components/Repository/RepositoryManager.cs index f75bae0fc0..b3a1b858b1 100644 --- a/src/Tgstation.Server.Host/Components/Repository/RepositoryManager.cs +++ b/src/Tgstation.Server.Host/Components/Repository/RepositoryManager.cs @@ -1,4 +1,4 @@ -using LibGit2Sharp; +using LibGit2Sharp; using Microsoft.Extensions.Logging; using System; using System.Threading; @@ -154,7 +154,7 @@ namespace Tgstation.Server.Host.Components.Repository } catch (Exception e) { - logger.LogDebug("Error deleting partially cloned repository! Exception: {0}", e); + logger.LogDebug(e, "Error deleting partially cloned repository!"); } throw; @@ -209,8 +209,7 @@ namespace Tgstation.Server.Host.Components.Repository } catch (RepositoryNotFoundException e) { - logger.LogDebug("Repository not found!"); - logger.LogTrace("Exception: {0}", e); + logger.LogTrace(e, "Repository not found!"); return null; } } diff --git a/src/Tgstation.Server.Host/Components/Session/SessionController.cs b/src/Tgstation.Server.Host/Components/Session/SessionController.cs index e06caba888..396fb24346 100644 --- a/src/Tgstation.Server.Host/Components/Session/SessionController.cs +++ b/src/Tgstation.Server.Host/Components/Session/SessionController.cs @@ -566,16 +566,17 @@ namespace Tgstation.Server.Host.Components.Session logger.LogTrace("Interop response: {0}", topicReturn); } - catch + catch(Exception ex) { - logger.LogWarning("Invalid interop response: {0}", topicReturn); + logger.LogWarning(ex, "Invalid interop response: {0}", topicReturn); } return new CombinedTopicResponse(topicResponse, interopResponse); } - catch (OperationCanceledException) + catch (OperationCanceledException ex) { logger.LogTrace( + ex, "Topic request {0}!", cancellationToken.IsCancellationRequested ? "aborted" @@ -584,7 +585,7 @@ namespace Tgstation.Server.Host.Components.Session } catch (Exception e) { - logger.LogWarning("Send command exception:{0}{1}", Environment.NewLine, e); + logger.LogWarning(e, "Send command exception!"); } return null; diff --git a/src/Tgstation.Server.Host/Components/Session/SessionControllerFactory.cs b/src/Tgstation.Server.Host/Components/Session/SessionControllerFactory.cs index b682e3682f..d1a19a4ea2 100644 --- a/src/Tgstation.Server.Host/Components/Session/SessionControllerFactory.cs +++ b/src/Tgstation.Server.Host/Components/Session/SessionControllerFactory.cs @@ -289,7 +289,7 @@ namespace Tgstation.Server.Host.Components.Session } catch (Exception ex) { - logger.LogWarning("Failed to delete DreamDaemon log file {0}: {1}", logFilePath, ex); + logger.LogWarning(ex, "Failed to delete DreamDaemon log file {0}!", logFilePath); } } } @@ -306,7 +306,7 @@ namespace Tgstation.Server.Host.Components.Session } catch (Exception ex) { - logger.LogWarning("Error reading DreamDaemon output: {0}", ex); + logger.LogWarning(ex, "Error reading DreamDaemon output!"); } } diff --git a/src/Tgstation.Server.Host/Components/Session/SessionPersistor.cs b/src/Tgstation.Server.Host/Components/Session/SessionPersistor.cs index eef018159e..de8cc15cab 100644 --- a/src/Tgstation.Server.Host/Components/Session/SessionPersistor.cs +++ b/src/Tgstation.Server.Host/Components/Session/SessionPersistor.cs @@ -1,4 +1,4 @@ -using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore; using Microsoft.Extensions.Logging; using System; using System.Linq; @@ -136,7 +136,7 @@ namespace Tgstation.Server.Host.Components.Session } catch (Exception ex) { - logger.LogWarning("Failed to kill process! Exception: {0}", ex); + logger.LogWarning(ex, "Failed to kill process!"); } } diff --git a/src/Tgstation.Server.Host/Components/StaticFiles/Configuration.cs b/src/Tgstation.Server.Host/Components/StaticFiles/Configuration.cs index 12eb031994..f08ecdf70b 100644 --- a/src/Tgstation.Server.Host/Components/StaticFiles/Configuration.cs +++ b/src/Tgstation.Server.Host/Components/StaticFiles/Configuration.cs @@ -211,7 +211,7 @@ namespace Tgstation.Server.Host.Components.StaticFiles } catch (IOException e) { - logger.LogDebug("IOException while writing {0}: {1}", path, e); + logger.LogDebug(e, "IOException while writing {0}!", path); result = null; return; } diff --git a/src/Tgstation.Server.Host/Components/Watchdog/WatchdogBase.cs b/src/Tgstation.Server.Host/Components/Watchdog/WatchdogBase.cs index bc6eeda484..7ade6c1129 100644 --- a/src/Tgstation.Server.Host/Components/Watchdog/WatchdogBase.cs +++ b/src/Tgstation.Server.Host/Components/Watchdog/WatchdogBase.cs @@ -384,7 +384,7 @@ namespace Tgstation.Server.Host.Components.Watchdog } catch (Exception e) { - // don't try to send chat tasks or warning logs if were suppressing exceptions or cancelled + Logger.LogWarning(e, "Failed to start watchdog!"); var originalChatTask = announceTask; async Task ChainChatTaskWithErrorMessage() { @@ -394,7 +394,6 @@ namespace Tgstation.Server.Host.Components.Watchdog } announceTask = ChainChatTaskWithErrorMessage(); - Logger.LogWarning("Failed to start watchdog: {0}", e.ToString()); throw; } finally @@ -404,9 +403,9 @@ namespace Tgstation.Server.Host.Components.Watchdog { await announceTask.ConfigureAwait(false); } - catch (OperationCanceledException) + catch (OperationCanceledException ex) { - Logger.LogTrace("Announcement task canceled!"); + Logger.LogTrace(ex, "Announcement task canceled!"); } } @@ -554,7 +553,7 @@ namespace Tgstation.Server.Host.Components.Watchdog await chatTask.ConfigureAwait(false); } - Logger.LogWarning("Failed to automatically restart the watchdog! Attempt: {0}, Exception: {1}", retryAttempts, launchException); + Logger.LogWarning(launchException, "Failed to automatically restart the watchdog! Attempt: {0}", retryAttempts); Status = WatchdogStatus.DelayedRestart; var retryDelay = Math.Min( @@ -698,9 +697,9 @@ namespace Tgstation.Server.Host.Components.Watchdog { // really, this should NEVER happen Logger.LogError( - "Monitor crashed! Iteration: {0}, Exception: {1}", - iteration, - e); + e, + "Monitor crashed! Iteration: {0}", + iteration); var nextActionMessage = nextAction != MonitorAction.Exit ? "Recovering" @@ -917,8 +916,8 @@ namespace Tgstation.Server.Host.Components.Watchdog catch (Exception ex) { Logger.LogCritical( - "Failed to persist session reattach information! To repair this, DreamDaemon will need to be manully stopped and then relaunched with TGS. Exception: {0}", - ex); + ex, + "Failed to persist session reattach information! To repair this, DreamDaemon will need to be manully stopped and then relaunched with TGS."); } releasedReattachInformation = null; diff --git a/src/Tgstation.Server.Host/Components/Watchdog/WindowsWatchdog.cs b/src/Tgstation.Server.Host/Components/Watchdog/WindowsWatchdog.cs index 2a084b150f..fd6e49c24a 100644 --- a/src/Tgstation.Server.Host/Components/Watchdog/WindowsWatchdog.cs +++ b/src/Tgstation.Server.Host/Components/Watchdog/WindowsWatchdog.cs @@ -181,14 +181,14 @@ namespace Tgstation.Server.Host.Components.Watchdog } catch (Exception ex) { - Logger.LogWarning("Exception while suspending server: {0}", ex); + Logger.LogWarning(ex, "Exception while suspending server!"); } await windowsProvider.MakeActive(cancellationToken).ConfigureAwait(false); } catch (Exception ex) { - Logger.LogError("Exception while swapping: {0}", ex); + Logger.LogError(ex, "Exception while swapping"); IDmbProvider providerToDispose = windowsProvider ?? compileJobProvider; providerToDispose.Dispose(); throw; diff --git a/src/Tgstation.Server.Host/Controllers/AdministrationController.cs b/src/Tgstation.Server.Host/Controllers/AdministrationController.cs index 13b21a8d4c..ee8c38ac3e 100644 --- a/src/Tgstation.Server.Host/Controllers/AdministrationController.cs +++ b/src/Tgstation.Server.Host/Controllers/AdministrationController.cs @@ -1,4 +1,4 @@ -using Microsoft.AspNetCore.Mvc; +using Microsoft.AspNetCore.Mvc; using Microsoft.Extensions.Logging; using Microsoft.Extensions.Options; using Microsoft.Extensions.Primitives; @@ -30,7 +30,7 @@ namespace Tgstation.Server.Host.Controllers [Route(Routes.Administration)] public sealed class AdministrationController : ApiController { - const string OctokitException = "Bad GitHub API response, check configuration! Exception: {0}"; + const string OctokitException = "Bad GitHub API response, check configuration!"; /// /// The for the @@ -116,7 +116,7 @@ namespace Tgstation.Server.Host.Controllers ObjectResult RateLimit(RateLimitExceededException exception) { - Logger.LogWarning("Exceeded GitHub rate limit! Exception {0}", exception); + Logger.LogWarning(exception, "Exceeded GitHub rate limit!"); var secondsString = Math.Ceiling((exception.Reset - DateTimeOffset.Now).TotalSeconds).ToString(CultureInfo.InvariantCulture); Response.Headers.Add("Retry-After", new StringValues(secondsString)); return StatusCode(HttpStatusCode.TooManyRequests, new ErrorMessage(ErrorCode.GitHubApiRateLimit)); @@ -148,7 +148,7 @@ namespace Tgstation.Server.Host.Controllers } catch (ApiException e) { - Logger.LogWarning(OctokitException, e); + Logger.LogWarning(e, OctokitException); return StatusCode(HttpStatusCode.FailedDependency); } @@ -226,7 +226,7 @@ namespace Tgstation.Server.Host.Controllers } catch (NotFoundException e) { - Logger.LogWarning("Not found exception while retrieving upstream repository info: {0}", e); + Logger.LogWarning(e, "Not found exception while retrieving upstream repository info!"); } return Json(new Administration @@ -242,7 +242,7 @@ namespace Tgstation.Server.Host.Controllers } catch (ApiException e) { - Logger.LogWarning(OctokitException, e); + Logger.LogWarning(e, OctokitException); return StatusCode(HttpStatusCode.FailedDependency, new ErrorMessage(ErrorCode.GitHubApiError) { AdditionalData = e.Message diff --git a/src/Tgstation.Server.Host/Controllers/BridgeController.cs b/src/Tgstation.Server.Host/Controllers/BridgeController.cs index 1a0e794e9a..0a3c0252f4 100644 --- a/src/Tgstation.Server.Host/Controllers/BridgeController.cs +++ b/src/Tgstation.Server.Host/Controllers/BridgeController.cs @@ -1,4 +1,4 @@ -using Microsoft.AspNetCore.Mvc; +using Microsoft.AspNetCore.Mvc; using Microsoft.Extensions.Hosting; using Microsoft.Extensions.Logging; using Newtonsoft.Json; @@ -62,8 +62,12 @@ namespace Tgstation.Server.Host.Controllers public async Task Process([FromQuery]string data, CancellationToken cancellationToken) { // Nothing to see here - if (!IPAddress.IsLoopback(Request.HttpContext.Connection.RemoteIpAddress)) + var remoteIP = Request.HttpContext.Connection.RemoteIpAddress; + if (!IPAddress.IsLoopback(remoteIP)) + { + logger.LogTrace("Ignoring remote bridge request from {0}", remoteIP); return NotFound(); + } using (LogContext.PushProperty("Bridge", Interlocked.Increment(ref requestsProcessed))) { @@ -72,9 +76,9 @@ namespace Tgstation.Server.Host.Controllers { request = JsonConvert.DeserializeObject(data, DMApiConstants.SerializerSettings); } - catch + catch (Exception ex) { - logger.LogWarning("Error deserializing bridge request: {0}", data); + logger.LogWarning(ex, "Error deserializing bridge request: {0}", data); return BadRequest(); } diff --git a/src/Tgstation.Server.Host/Controllers/InstanceController.cs b/src/Tgstation.Server.Host/Controllers/InstanceController.cs index 87229c6c41..7483cecb69 100644 --- a/src/Tgstation.Server.Host/Controllers/InstanceController.cs +++ b/src/Tgstation.Server.Host/Controllers/InstanceController.cs @@ -497,7 +497,7 @@ namespace Tgstation.Server.Host.Controllers catch (Exception e) { if(!(e is OperationCanceledException)) - Logger.LogError("Error changing instance online state! Exception: {0}", e); + Logger.LogError(e, "Error changing instance online state!"); originalModel.Online = originalOnline; originalModel.DreamDaemonSettings.AutoStart = oldAutoStart; if (originalModelPath != null) diff --git a/src/Tgstation.Server.Host/Controllers/RepositoryController.cs b/src/Tgstation.Server.Host/Controllers/RepositoryController.cs index 4f6d855128..0dec87903f 100644 --- a/src/Tgstation.Server.Host/Controllers/RepositoryController.cs +++ b/src/Tgstation.Server.Host/Controllers/RepositoryController.cs @@ -1,4 +1,4 @@ -using Microsoft.AspNetCore.Mvc; +using Microsoft.AspNetCore.Mvc; using Microsoft.EntityFrameworkCore; using Microsoft.Extensions.Logging; using Microsoft.Extensions.Options; @@ -817,7 +817,7 @@ namespace Tgstation.Server.Host.Controllers } if (exception != null) - Logger.LogWarning("Error retrieving pull request metadata: {0}", exception); + Logger.LogWarning(exception, "Error retrieving pull request metadata!"); // we want to take the earliest truth possible to prevent RCEs, if this fails AddTestMerge will set it if (I.PullRequestRevision == null && pr != null) diff --git a/src/Tgstation.Server.Host/Database/DatabaseContext.cs b/src/Tgstation.Server.Host/Database/DatabaseContext.cs index d910d5c4d9..85f00bd68a 100644 --- a/src/Tgstation.Server.Host/Database/DatabaseContext.cs +++ b/src/Tgstation.Server.Host/Database/DatabaseContext.cs @@ -1,4 +1,4 @@ -using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore.Infrastructure; using Microsoft.EntityFrameworkCore.Migrations; using Microsoft.Extensions.DependencyInjection; @@ -400,7 +400,7 @@ namespace Tgstation.Server.Host.Database } catch (Exception e) { - logger.LogCritical("Failed to migrate! Exception: {0}", e); + logger.LogCritical(e, "Failed to migrate!"); } } } diff --git a/src/Tgstation.Server.Host/Database/DatabaseSeeder.cs b/src/Tgstation.Server.Host/Database/DatabaseSeeder.cs index a9857e673e..2bb5265c16 100644 --- a/src/Tgstation.Server.Host/Database/DatabaseSeeder.cs +++ b/src/Tgstation.Server.Host/Database/DatabaseSeeder.cs @@ -1,4 +1,4 @@ -using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore; using Microsoft.Extensions.Logging; using Microsoft.Extensions.Options; using System; @@ -165,10 +165,8 @@ namespace Tgstation.Server.Host.Database .ConfigureAwait(false); if (tgsUser != null) - { logger.LogError( "A user named TGS (Canonically) exists but isn't marked as the admin's creator. This may be because it was created manually. This user is going to be adapted to use as the starter of system jobs."); - } tgsUser = SeedSystemUser(databaseContext, tgsUser); admin.CreatedBy = tgsUser; diff --git a/src/Tgstation.Server.Host/Extensions/ApplicationBuilderExtensions.cs b/src/Tgstation.Server.Host/Extensions/ApplicationBuilderExtensions.cs index fe3356d981..1165fddf85 100644 --- a/src/Tgstation.Server.Host/Extensions/ApplicationBuilderExtensions.cs +++ b/src/Tgstation.Server.Host/Extensions/ApplicationBuilderExtensions.cs @@ -1,4 +1,4 @@ -using Microsoft.AspNetCore.Builder; +using Microsoft.AspNetCore.Builder; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Mvc; using Microsoft.EntityFrameworkCore; @@ -42,11 +42,11 @@ namespace Tgstation.Server.Host.Extensions { if (e.InnerException is OperationCanceledException) { - logger.LogTrace("Rethrowing DbUpdateException as OperationCanceledException: {0}", e); + logger.LogTrace(e, "Rethrowing DbUpdateException as OperationCanceledException"); throw e.InnerException; } - logger.LogDebug("Database conflict: {0}", e.Message); + logger.LogDebug(e, "Database conflict!"); await new ConflictObjectResult(new ErrorMessage(ErrorCode.DatabaseIntegrityConflict) { AdditionalData = String.Format(CultureInfo.InvariantCulture, (e.InnerException ?? e).Message) @@ -73,9 +73,9 @@ namespace Tgstation.Server.Host.Extensions { await next().ConfigureAwait(false); } - catch (OperationCanceledException) + catch (OperationCanceledException ex) { - logger.LogDebug("Request cancelled!"); + logger.LogDebug(ex, "Request cancelled!"); } }); } @@ -97,7 +97,7 @@ namespace Tgstation.Server.Host.Extensions } catch (Exception e) { - logger.LogError("Failed request: {0}", e); + logger.LogError(e, "Failed request!"); await new ObjectResult( new ErrorMessage(ErrorCode.InternalServerError) { diff --git a/src/Tgstation.Server.Host/Jobs/JobManager.cs b/src/Tgstation.Server.Host/Jobs/JobManager.cs index 2442c0d87e..5066f0cdfb 100644 --- a/src/Tgstation.Server.Host/Jobs/JobManager.cs +++ b/src/Tgstation.Server.Host/Jobs/JobManager.cs @@ -96,7 +96,7 @@ namespace Tgstation.Server.Host.Jobs using (LogContext.PushProperty("Job", job.Id)) try { - void LogException() => logger.LogDebug("Job {0} exited with error! Exception: {1}", job.Id, job.ExceptionDetails); + void LogException(Exception ex) => logger.LogDebug(ex, "Job {0} exited with error!", job.Id); try { var oldJob = job; @@ -122,21 +122,21 @@ namespace Tgstation.Server.Host.Jobs logger.LogDebug("Job {0} completed!", job.Id); } - catch (OperationCanceledException) + catch (OperationCanceledException ex) { - logger.LogDebug("Job {0} cancelled!", job.Id); + logger.LogDebug(ex, "Job {0} cancelled!", job.Id); job.Cancelled = true; } catch (JobException e) { job.ErrorCode = e.ErrorCode; job.ExceptionDetails = String.IsNullOrWhiteSpace(e.Message) ? e.InnerException?.Message : e.Message; - LogException(); + LogException(e); } catch (Exception e) { job.ExceptionDetails = e.ToString(); - LogException(); + LogException(e); } await databaseContextFactory.UseContext(async databaseContext => @@ -220,10 +220,8 @@ namespace Tgstation.Server.Host.Jobs }); /// - public async Task StartAsync(CancellationToken cancellationToken) - { - logger.LogTrace("Starting job manager..."); - await databaseContextFactory.UseContext(async databaseContext => + public Task StartAsync(CancellationToken cancellationToken) + => databaseContextFactory.UseContext(async databaseContext => { // mark all jobs as cancelled var badJobs = await databaseContext @@ -246,9 +244,7 @@ namespace Tgstation.Server.Host.Jobs await databaseContext.Save(cancellationToken).ConfigureAwait(false); } - }).ConfigureAwait(false); - logger.LogDebug("Job manager started!"); - } + }); /// public async Task StopAsync(CancellationToken cancellationToken) diff --git a/src/Tgstation.Server.Host/Security/WindowsSystemIdentityFactory.cs b/src/Tgstation.Server.Host/Security/WindowsSystemIdentityFactory.cs index a3480ddda6..1b9e64e4e6 100644 --- a/src/Tgstation.Server.Host/Security/WindowsSystemIdentityFactory.cs +++ b/src/Tgstation.Server.Host/Security/WindowsSystemIdentityFactory.cs @@ -1,4 +1,4 @@ -using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Logging; using Microsoft.Win32.SafeHandles; using System; using System.DirectoryServices.AccountManagement; @@ -70,7 +70,7 @@ namespace Tgstation.Server.Host.Security } catch (Exception e) { - logger.LogWarning("Error loading user for context type {0}! Exception: {1}", contextType, e); + logger.LogWarning(e, "Error loading user for context type {0}!", contextType); } finally { @@ -110,8 +110,10 @@ namespace Tgstation.Server.Host.Security logger.LogTrace("Authenticated username {0} using system identity!", originalUsername); - using (var handle = new SafeAccessTokenHandle(token)) // checked internally, windows identity always duplicates the handle when constructed with a userToken - return (ISystemIdentity)new WindowsSystemIdentity(new WindowsIdentity(handle.DangerousGetHandle())); // https://github.com/dotnet/corefx/blob/6ed61acebe3214fcf79b4274f2bb9b55c0604a4d/src/System.Security.Principal.Windows/src/System/Security/Principal/WindowsIdentity.cs#L271 + // checked internally, windows identity always duplicates the handle when constructed + using var handle = new SafeAccessTokenHandle(token); + return (ISystemIdentity)new WindowsSystemIdentity( + new WindowsIdentity(handle.DangerousGetHandle())); // https://github.com/dotnet/corefx/blob/6ed61acebe3214fcf79b4274f2bb9b55c0604a4d/src/System.Security.Principal.Windows/src/System/Security/Principal/WindowsIdentity.cs#L271 }, cancellationToken, TaskCreationOptions.LongRunning, TaskScheduler.Current); } } diff --git a/src/Tgstation.Server.Host/Server.cs b/src/Tgstation.Server.Host/Server.cs index d62ecccc87..8ce353d0ae 100644 --- a/src/Tgstation.Server.Host/Server.cs +++ b/src/Tgstation.Server.Host/Server.cs @@ -225,7 +225,7 @@ namespace Tgstation.Server.Host } catch (Exception e) { - logger.LogError("Error updating server! Exception: {0}", e); + logger.LogError(e, "Error updating server!"); } finally { @@ -304,13 +304,15 @@ namespace Tgstation.Server.Host { await eventsTask.ConfigureAwait(false); } - catch (OperationCanceledException) + catch (OperationCanceledException ex) { - logger.LogError("Restart timeout hit! Existing DreamDaemon processes will be lost and must be killed manually before being restarted with TGS!"); + logger.LogError( + ex, + "Restart timeout hit! Existing DreamDaemon processes will be lost and must be killed manually before being restarted with TGS!"); } catch (Exception e) { - logger.LogError("Restart handlers error! Exception: {0}", e); + logger.LogError(e, "Restart handlers error!"); } } diff --git a/src/Tgstation.Server.Host/System/PosixProcessFeatures.cs b/src/Tgstation.Server.Host/System/PosixProcessFeatures.cs index 293fc880fe..fd120064b8 100644 --- a/src/Tgstation.Server.Host/System/PosixProcessFeatures.cs +++ b/src/Tgstation.Server.Host/System/PosixProcessFeatures.cs @@ -44,35 +44,17 @@ namespace Tgstation.Server.Host.System /// public void ResumeProcess(global::System.Diagnostics.Process process) { - try - { - var result = Syscall.kill(process.Id, Signum.SIGCONT); - if (result != 0) - throw new UnixIOException(result); - logger.LogTrace("Resumed PID {0}", process.Id); - } - catch (Exception e) - { - logger.LogError(e, "Failed to resume PID {0}!", process.Id); - throw; - } + var result = Syscall.kill(process.Id, Signum.SIGCONT); + if (result != 0) + throw new UnixIOException(result); } /// public void SuspendProcess(global::System.Diagnostics.Process process) { - try - { - var result = Syscall.kill(process.Id, Signum.SIGSTOP); - if (result != 0) - throw new UnixIOException(result); - logger.LogTrace("Resumed PID {0}", process.Id); - } - catch (Exception e) - { - logger.LogError(e, "Failed to suspend PID {0}!", process.Id); - throw; - } + var result = Syscall.kill(process.Id, Signum.SIGSTOP); + if (result != 0) + throw new UnixIOException(result); } /// diff --git a/src/Tgstation.Server.Host/System/Process.cs b/src/Tgstation.Server.Host/System/Process.cs index 1800b77563..ae5e5e27cb 100644 --- a/src/Tgstation.Server.Host/System/Process.cs +++ b/src/Tgstation.Server.Host/System/Process.cs @@ -105,7 +105,11 @@ namespace Tgstation.Server.Host.System { await Task.WhenAny(lifetimeTask, emergencyLifetimeTcs.Task).ConfigureAwait(false); if (lifetimeTask.IsCompleted) - return await lifetimeTask.ConfigureAwait(false); + { + var exitCode = await lifetimeTask.ConfigureAwait(false); + logger.LogTrace("PID {0} exited with code {1}", Id, exitCode); + return exitCode; + } logger.LogTrace("Using exit code -1 for hung PID {0}.", Id); return -1; @@ -156,7 +160,7 @@ namespace Tgstation.Server.Host.System } catch (Exception e) { - logger.LogDebug("Process termination exception: {0}", e); + logger.LogDebug(e, "Process termination exception!"); } } @@ -170,17 +174,41 @@ namespace Tgstation.Server.Host.System } catch (Exception e) { - logger.LogWarning("Unable to raise process priority for PID {0}! Exception: {1}", Id, e); + logger.LogWarning(e, "Unable to raise process priority for PID {0}!", Id); } } /// - public void Suspend() => processFeatures.SuspendProcess(handle); + public void Suspend() + { + try + { + processFeatures.SuspendProcess(handle); + logger.LogTrace("Suspended PID {0}", Id); + } + catch (Exception e) + { + logger.LogError(e, "Failed to suspend PID {0}!", Id); + throw; + } + } /// - public void Resume() => processFeatures.ResumeProcess(handle); + public void Resume() + { + try + { + processFeatures.ResumeProcess(handle); + logger.LogTrace("Resumed PID {0}", Id); + } + catch (Exception e) + { + logger.LogError(e, "Failed to resume PID {0}!", Id); + throw; + } + } - /// + /// public async Task GetExecutingUsername(CancellationToken cancellationToken) { var result = await processFeatures.GetExecutingUsername(handle, cancellationToken).ConfigureAwait(false); diff --git a/src/Tgstation.Server.Host/System/ProcessExecutor.cs b/src/Tgstation.Server.Host/System/ProcessExecutor.cs index 8abefc2cc7..444e6eda9a 100644 --- a/src/Tgstation.Server.Host/System/ProcessExecutor.cs +++ b/src/Tgstation.Server.Host/System/ProcessExecutor.cs @@ -77,9 +77,9 @@ namespace Tgstation.Server.Host.System { handle = global::System.Diagnostics.Process.GetProcessById(id); } - catch(Exception e) + catch (Exception e) { - logger.LogDebug("Unable to get process {0}! Exception: {1}", id, e); + logger.LogDebug(e, "Unable to get process {0}!", id); return null; } @@ -233,7 +233,7 @@ namespace Tgstation.Server.Host.System handle = proc; else { - logger.LogTrace("Disposing extra found PID: {0}", proc.Id); + logger.LogTrace("Disposing extra found PID: {0}...", proc.Id); proc.Dispose(); } diff --git a/src/Tgstation.Server.Host/System/WindowsNetworkPromptReaper.cs b/src/Tgstation.Server.Host/System/WindowsNetworkPromptReaper.cs index 1586229119..4fb35a71a8 100644 --- a/src/Tgstation.Server.Host/System/WindowsNetworkPromptReaper.cs +++ b/src/Tgstation.Server.Host/System/WindowsNetworkPromptReaper.cs @@ -1,4 +1,4 @@ -using BetterWin32Errors; +using BetterWin32Errors; using Microsoft.Extensions.Hosting; using Microsoft.Extensions.Logging; using System; @@ -134,7 +134,7 @@ namespace Tgstation.Server.Host.System if (NativeMethods.GetWindowText(I, stringBuilder, MaxLength) == 0) { - logger.LogWarning("Error calling GetWindowText! Exception: {0}", new Win32Exception()); + logger.LogWarning(new Win32Exception(), "Error calling GetWindowText!"); continue; } @@ -158,9 +158,9 @@ namespace Tgstation.Server.Host.System logger.LogDebug("Unable to find \"Yes\" button for \"Network Accessibility\" window in owned process {0}!", processId); } } - catch (OperationCanceledException) + catch (OperationCanceledException ex) { - logger.LogTrace("Cancelled!"); + logger.LogTrace(ex, "Cancelled!"); } finally { diff --git a/src/Tgstation.Server.Host/System/WindowsProcessFeatures.cs b/src/Tgstation.Server.Host/System/WindowsProcessFeatures.cs index 339e847e75..6739f12b4c 100644 --- a/src/Tgstation.Server.Host/System/WindowsProcessFeatures.cs +++ b/src/Tgstation.Server.Host/System/WindowsProcessFeatures.cs @@ -1,5 +1,4 @@ -using BetterWin32Errors; -using Microsoft.Extensions.Logging; +using BetterWin32Errors; using System; using System.Diagnostics; using System.IO; @@ -13,46 +12,27 @@ namespace Tgstation.Server.Host.System /// sealed class WindowsProcessFeatures : IProcessFeatures { - /// - /// The for the . - /// - readonly ILogger logger; - - /// - /// Initializes a new instance of the . - /// - /// The value of . - public WindowsProcessFeatures(ILogger logger) - { - this.logger = logger ?? throw new ArgumentNullException(nameof(logger)); - } - /// public void ResumeProcess(global::System.Diagnostics.Process process) { if (process == null) throw new ArgumentNullException(nameof(process)); - try + foreach (ProcessThread thread in process.Threads) { - foreach (ProcessThread thread in process.Threads) - { - var pOpenThread = NativeMethods.OpenThread(NativeMethods.ThreadAccess.SuspendResume, false, (uint)thread.Id); - if (pOpenThread == IntPtr.Zero) - continue; + var pOpenThread = NativeMethods.OpenThread(NativeMethods.ThreadAccess.SuspendResume, false, (uint)thread.Id); + if (pOpenThread == IntPtr.Zero) + continue; + try + { if (NativeMethods.ResumeThread(pOpenThread) == UInt32.MaxValue) throw new Win32Exception(); - + } + finally + { NativeMethods.CloseHandle(pOpenThread); } - - logger.LogTrace("Resumed PID {0}", process.Id); - } - catch (Exception e) - { - logger.LogError(e, "Failed to resume PID {0}!", process.Id); - throw; } } @@ -62,26 +42,20 @@ namespace Tgstation.Server.Host.System if (process == null) throw new ArgumentNullException(nameof(process)); - try + foreach (ProcessThread thread in process.Threads) { - foreach (ProcessThread thread in process.Threads) + var pOpenThread = NativeMethods.OpenThread(NativeMethods.ThreadAccess.SuspendResume, false, (uint)thread.Id); + if (pOpenThread == IntPtr.Zero) + continue; + try { - var pOpenThread = NativeMethods.OpenThread(NativeMethods.ThreadAccess.SuspendResume, false, (uint)thread.Id); - if (pOpenThread == IntPtr.Zero) - continue; - if (NativeMethods.SuspendThread(pOpenThread) == UInt32.MaxValue) throw new Win32Exception(); - + } + finally + { NativeMethods.CloseHandle(pOpenThread); } - - logger.LogTrace("Suspended PID {0}", process.Id); - } - catch (Exception e) - { - logger.LogError(e, "Failed to suspend PID {0}!", process.Id); - throw; } } diff --git a/tests/Tgstation.Server.Host.Tests/System/TestProcessFeatures.cs b/tests/Tgstation.Server.Host.Tests/System/TestProcessFeatures.cs index aca9cae1f1..7dabedf9d2 100644 --- a/tests/Tgstation.Server.Host.Tests/System/TestProcessFeatures.cs +++ b/tests/Tgstation.Server.Host.Tests/System/TestProcessFeatures.cs @@ -1,4 +1,4 @@ -using Castle.Core.Logging; +using Castle.Core.Logging; using Microsoft.Extensions.Logging; using Microsoft.VisualStudio.TestTools.UnitTesting; using Moq; @@ -20,7 +20,7 @@ namespace Tgstation.Server.Host.System.Tests public void Init() { features = new PlatformIdentifier().IsWindows - ? (IProcessFeatures)new WindowsProcessFeatures(Mock.Of>()) + ? (IProcessFeatures)new WindowsProcessFeatures() : new PosixProcessFeatures(new Lazy(() => null), new DefaultIOManager(), Mock.Of>()); } diff --git a/tests/Tgstation.Server.Tests/Instance/WatchdogTest.cs b/tests/Tgstation.Server.Tests/Instance/WatchdogTest.cs index 4b2e5e817d..7b39618540 100644 --- a/tests/Tgstation.Server.Tests/Instance/WatchdogTest.cs +++ b/tests/Tgstation.Server.Tests/Instance/WatchdogTest.cs @@ -191,7 +191,7 @@ namespace Tgstation.Server.Tests.Instance IProcessExecutor executor = null; executor = new ProcessExecutor( new PlatformIdentifier().IsWindows - ? (IProcessFeatures)new WindowsProcessFeatures(Mock.Of>()) + ? (IProcessFeatures)new WindowsProcessFeatures() : new PosixProcessFeatures(new Lazy(() => executor), Mock.Of(), Mock.Of>()), Mock.Of>(), LoggerFactory.Create(x => { })); From 6399c4cbf178036498b648b73db9dbd87c3bc165 Mon Sep 17 00:00:00 2001 From: Jordan Brown Date: Sun, 26 Jul 2020 13:31:39 -0400 Subject: [PATCH 44/68] Process output streaming fixes --- src/Tgstation.Server.Api/ApiHeaders.cs | 4 +- .../Components/Deployment/DreamMaker.cs | 3 +- .../Components/Session/SessionController.cs | 26 +-- .../Session/SessionControllerFactory.cs | 12 +- .../Components/Session/SessionPersistor.cs | 1 + .../Components/StaticFiles/Configuration.cs | 2 +- src/Tgstation.Server.Host/System/IProcess.cs | 22 +-- .../System/IProcessExecutor.cs | 10 +- .../System/PosixProcessFeatures.cs | 5 +- src/Tgstation.Server.Host/System/Process.cs | 103 ++++++------ .../System/ProcessExecutor.cs | 150 +++++++++--------- .../System/WindowsProcessFeatures.cs | 5 + .../Instance/WatchdogTest.cs | 4 +- .../Tgstation.Server.Tests/IntegrationTest.cs | 4 +- 14 files changed, 180 insertions(+), 171 deletions(-) diff --git a/src/Tgstation.Server.Api/ApiHeaders.cs b/src/Tgstation.Server.Api/ApiHeaders.cs index 9308ad9976..cecfa88a5a 100644 --- a/src/Tgstation.Server.Api/ApiHeaders.cs +++ b/src/Tgstation.Server.Api/ApiHeaders.cs @@ -1,4 +1,4 @@ -using Microsoft.AspNetCore.Http.Headers; +using Microsoft.AspNetCore.Http.Headers; using Microsoft.Extensions.Primitives; using Microsoft.Net.Http.Headers; using System; @@ -138,7 +138,7 @@ namespace Tgstation.Server.Api void AddError(HeaderTypes headerType, string message) { if (badHeaders != HeaderTypes.None) - errorBuilder.Append(Environment.NewLine); + errorBuilder.AppendLine(); badHeaders |= headerType; errorBuilder.Append(message); } diff --git a/src/Tgstation.Server.Host/Components/Deployment/DreamMaker.cs b/src/Tgstation.Server.Host/Components/Deployment/DreamMaker.cs index 34b5896aac..3f75c34356 100644 --- a/src/Tgstation.Server.Host/Components/Deployment/DreamMaker.cs +++ b/src/Tgstation.Server.Host/Components/Deployment/DreamMaker.cs @@ -292,7 +292,8 @@ namespace Tgstation.Server.Host.Components.Deployment cancellationToken.ThrowIfCancellationRequested(); logger.LogDebug("DreamMaker exit code: {0}", exitCode); - currentDreamMakerOutput = job.Output = dm.GetCombinedOutput(); + job.Output = await dm.GetCombinedOutput(cancellationToken).ConfigureAwait(false); + currentDreamMakerOutput = job.Output; logger.LogDebug("DreamMaker output: {0}{1}", Environment.NewLine, job.Output); return exitCode; } diff --git a/src/Tgstation.Server.Host/Components/Session/SessionController.cs b/src/Tgstation.Server.Host/Components/Session/SessionController.cs index 396fb24346..c5c10b6719 100644 --- a/src/Tgstation.Server.Host/Components/Session/SessionController.cs +++ b/src/Tgstation.Server.Host/Components/Session/SessionController.cs @@ -257,21 +257,21 @@ namespace Tgstation.Server.Host.Components.Session if (disposed) return; disposed = true; - - logger.LogTrace("Disposing..."); - if (!released) - { - process.Terminate(); - byondLock.Dispose(); - } - - process.Dispose(); - bridgeRegistration?.Dispose(); - reattachInformation.Dmb?.Dispose(); // will be null when released - chatTrackingContext.Dispose(); - reattachTopicCts.Dispose(); } + logger.LogTrace("Disposing..."); + if (!released) + { + process.Terminate(); + byondLock.Dispose(); + } + + process.Dispose(); + bridgeRegistration?.Dispose(); + reattachInformation.Dmb?.Dispose(); // will be null when released + chatTrackingContext.Dispose(); + reattachTopicCts.Dispose(); + if (!released) { // finish the async callback diff --git a/src/Tgstation.Server.Host/Components/Session/SessionControllerFactory.cs b/src/Tgstation.Server.Host/Components/Session/SessionControllerFactory.cs index d1a19a4ea2..c003e78441 100644 --- a/src/Tgstation.Server.Host/Components/Session/SessionControllerFactory.cs +++ b/src/Tgstation.Server.Host/Components/Session/SessionControllerFactory.cs @@ -267,8 +267,9 @@ namespace Tgstation.Server.Host.Components.Session async Task GetDDOutput() { + // DCT x2: None available if (!platformIdentifier.IsWindows) - return process.GetCombinedOutput(); + return await process.GetCombinedOutput(default).ConfigureAwait(false); var logFilePath = ioManager.ConcatPath(dmbProvider.Directory, logFileGuid.ToString()); try @@ -347,9 +348,12 @@ namespace Tgstation.Server.Host.Components.Session } catch { - process.Terminate(); - process.Dispose(); - throw; + using (process) + { + process.Terminate(); + await process.Lifetime.ConfigureAwait(false); + throw; + } } } catch diff --git a/src/Tgstation.Server.Host/Components/Session/SessionPersistor.cs b/src/Tgstation.Server.Host/Components/Session/SessionPersistor.cs index de8cc15cab..a0513fad10 100644 --- a/src/Tgstation.Server.Host/Components/Session/SessionPersistor.cs +++ b/src/Tgstation.Server.Host/Components/Session/SessionPersistor.cs @@ -133,6 +133,7 @@ namespace Tgstation.Server.Host.Components.Session { using var process = processExecutor.GetProcess(reattachInfo.ProcessId); process.Terminate(); + await process.Lifetime.ConfigureAwait(false); } catch (Exception ex) { diff --git a/src/Tgstation.Server.Host/Components/StaticFiles/Configuration.cs b/src/Tgstation.Server.Host/Components/StaticFiles/Configuration.cs index f08ecdf70b..c18aee9d6a 100644 --- a/src/Tgstation.Server.Host/Components/StaticFiles/Configuration.cs +++ b/src/Tgstation.Server.Host/Components/StaticFiles/Configuration.cs @@ -488,7 +488,7 @@ namespace Tgstation.Server.Host.Components.StaticFiles { var exitCode = await script.Lifetime.ConfigureAwait(false); cancellationToken.ThrowIfCancellationRequested(); - var scriptOutput = script.GetCombinedOutput(); + var scriptOutput = await script.GetCombinedOutput(cancellationToken).ConfigureAwait(false); if (exitCode != 0) throw new JobException($"Script {I} exited with code {exitCode}:{Environment.NewLine}{scriptOutput}"); else diff --git a/src/Tgstation.Server.Host/System/IProcess.cs b/src/Tgstation.Server.Host/System/IProcess.cs index d961b65694..8392d58377 100644 --- a/src/Tgstation.Server.Host/System/IProcess.cs +++ b/src/Tgstation.Server.Host/System/IProcess.cs @@ -1,4 +1,4 @@ -using System; +using System; using System.Threading; using System.Threading.Tasks; @@ -22,24 +22,28 @@ namespace Tgstation.Server.Host.System /// /// Get the stderr output of the /// - /// The stderr output of the - string GetErrorOutput(); + /// The for the operation. + /// A resulting in the stderr output of the + Task GetErrorOutput(CancellationToken cancellationToken); /// /// Get the stdout output of the /// - /// The stdout output of the - string GetStandardOutput(); + /// The for the operation. + /// A resulting in the stdout output of the + Task GetStandardOutput(CancellationToken cancellationToken); /// /// Get the stderr and stdout output of the /// - /// The stderr and stdout output of the - string GetCombinedOutput(); + /// The for the operation. + /// A resulting in the stderr and stdout output of the + Task GetCombinedOutput(CancellationToken cancellationToken); /// - /// Terminates the process + /// Asycnhronously terminates the process. /// + /// To ensure the has ended, use the . void Terminate(); /// @@ -49,4 +53,4 @@ namespace Tgstation.Server.Host.System /// A resulting in the name of the account executing the . Task GetExecutingUsername(CancellationToken cancellationToken); } -} \ No newline at end of file +} diff --git a/src/Tgstation.Server.Host/System/IProcessExecutor.cs b/src/Tgstation.Server.Host/System/IProcessExecutor.cs index e7f0d98388..12bfb896a9 100644 --- a/src/Tgstation.Server.Host/System/IProcessExecutor.cs +++ b/src/Tgstation.Server.Host/System/IProcessExecutor.cs @@ -1,4 +1,4 @@ -namespace Tgstation.Server.Host.System +namespace Tgstation.Server.Host.System { /// /// For launching ' @@ -15,7 +15,13 @@ /// If standard error should be read /// If shell execute should not be used. Must be set if or are set. /// A new - IProcess LaunchProcess(string fileName, string workingDirectory, string arguments = null, bool readOutput = false, bool readError = false, bool noShellExecute = false); + IProcess LaunchProcess( + string fileName, + string workingDirectory, + string arguments = null, + bool readOutput = false, + bool readError = false, + bool noShellExecute = false); /// /// Get a representing the running executable. diff --git a/src/Tgstation.Server.Host/System/PosixProcessFeatures.cs b/src/Tgstation.Server.Host/System/PosixProcessFeatures.cs index fd120064b8..c06c99a812 100644 --- a/src/Tgstation.Server.Host/System/PosixProcessFeatures.cs +++ b/src/Tgstation.Server.Host/System/PosixProcessFeatures.cs @@ -73,6 +73,9 @@ namespace Tgstation.Server.Host.System if (!await ioManager.FileExists(GCorePath, cancellationToken).ConfigureAwait(false)) throw new JobException(ErrorCode.MissingGCore); + if(process.HasExited) + throw new JobException(ErrorCode.DreamDaemonOffline); + var pid = process.Id; string output; int exitCode; @@ -87,7 +90,7 @@ namespace Tgstation.Server.Host.System using (cancellationToken.Register(() => gcoreProc.Terminate())) exitCode = await gcoreProc.Lifetime.ConfigureAwait(false); - output = gcoreProc.GetCombinedOutput(); + output = await gcoreProc.GetCombinedOutput(cancellationToken).ConfigureAwait(false); logger.LogDebug("gcore output:{0}{1}", Environment.NewLine, output); } diff --git a/src/Tgstation.Server.Host/System/Process.cs b/src/Tgstation.Server.Host/System/Process.cs index ae5e5e27cb..32cca93336 100644 --- a/src/Tgstation.Server.Host/System/Process.cs +++ b/src/Tgstation.Server.Host/System/Process.cs @@ -4,17 +4,13 @@ using System.Diagnostics; using System.Text; using System.Threading; using System.Threading.Tasks; +using Tgstation.Server.Host.Extensions; namespace Tgstation.Server.Host.System { /// sealed class Process : IProcess { - /// - /// Maximum time to wait in a call to . - /// - const int MaximumWaitMilliseconds = 30000; - /// public int Id { get; } @@ -36,13 +32,8 @@ namespace Tgstation.Server.Host.System readonly global::System.Diagnostics.Process handle; - /// - /// A so that we can complete if the becomes unresponsive. - /// - readonly TaskCompletionSource emergencyLifetimeTcs; - - readonly StringBuilder outputStringBuilder; - readonly StringBuilder errorStringBuilder; + readonly Task standardOutputTask; + readonly Task standardErrorTask; readonly StringBuilder combinedStringBuilder; /// @@ -51,8 +42,8 @@ namespace Tgstation.Server.Host.System /// The value of /// The value of /// The value of - /// The value of - /// The value of + /// The value of + /// The value of /// The value of /// The value of /// If was NOT just created @@ -60,40 +51,48 @@ namespace Tgstation.Server.Host.System IProcessFeatures processFeatures, global::System.Diagnostics.Process handle, Task lifetime, - StringBuilder outputStringBuilder, - StringBuilder errorStringBuilder, + Task standardOutputTask, + Task standardErrorTask, StringBuilder combinedStringBuilder, ILogger logger, bool preExisting) { - this.processFeatures = processFeatures ?? throw new ArgumentNullException(nameof(processFeatures)); this.handle = handle ?? throw new ArgumentNullException(nameof(handle)); - this.outputStringBuilder = outputStringBuilder; - this.errorStringBuilder = errorStringBuilder; + // Do this fast because the runtime will bitch if we try to access it after it ends + Id = handle.Id; + + this.processFeatures = processFeatures ?? throw new ArgumentNullException(nameof(processFeatures)); + + this.standardOutputTask = standardOutputTask; + this.standardErrorTask = standardErrorTask; this.combinedStringBuilder = combinedStringBuilder; this.logger = logger ?? throw new ArgumentNullException(nameof(logger)); - emergencyLifetimeTcs = new TaskCompletionSource(); Lifetime = WrapLifetimeTask(lifetime ?? throw new ArgumentNullException(nameof(lifetime))); - Id = handle.Id; - if (preExisting) { Startup = Task.CompletedTask; return; } - Startup = Task.Factory.StartNew(() => - { - try + Startup = Task.Factory.StartNew( + () => { - handle.WaitForInputIdle(); - } - catch (InvalidOperationException) { } - }, default, TaskCreationOptions.LongRunning, TaskScheduler.Current); + try + { + handle.WaitForInputIdle(); + } + catch (InvalidOperationException ex) + { + logger.LogDebug(ex, "Error on WaitForInputIdle()!"); + } + }, + default, // DCT: None available + TaskCreationOptions.LongRunning, + TaskScheduler.Current); logger.LogTrace("Created process ID: {0}", Id); } @@ -103,60 +102,48 @@ namespace Tgstation.Server.Host.System async Task WrapLifetimeTask(Task lifetimeTask) { - await Task.WhenAny(lifetimeTask, emergencyLifetimeTcs.Task).ConfigureAwait(false); - if (lifetimeTask.IsCompleted) - { - var exitCode = await lifetimeTask.ConfigureAwait(false); - logger.LogTrace("PID {0} exited with code {1}", Id, exitCode); - return exitCode; - } - - logger.LogTrace("Using exit code -1 for hung PID {0}.", Id); - return -1; + // relevant: https://stackoverflow.com/a/26722542 + var exitCode = await lifetimeTask.ConfigureAwait(false); + logger.LogTrace("PID {0} exited with code {1}", Id, exitCode); + return exitCode; } /// - public string GetCombinedOutput() + public async Task GetCombinedOutput(CancellationToken cancellationToken) { if (combinedStringBuilder == null) - throw new InvalidOperationException("Output/Error reading was not enabled!"); + throw new InvalidOperationException("Output/Error stream reading was not enabled!"); + await Task.WhenAll(standardOutputTask, standardErrorTask).WithToken(cancellationToken).ConfigureAwait(false); return combinedStringBuilder.ToString().TrimStart(Environment.NewLine.ToCharArray()); } /// - public string GetErrorOutput() + public Task GetErrorOutput(CancellationToken cancellationToken) { - if (errorStringBuilder == null) - throw new InvalidOperationException("Error reading was not enabled!"); - return errorStringBuilder.ToString().TrimStart(Environment.NewLine.ToCharArray()); + if (standardErrorTask == null) + throw new InvalidOperationException("Error stream reading was not enabled!"); + return standardErrorTask.WithToken(cancellationToken); } /// - public string GetStandardOutput() + public Task GetStandardOutput(CancellationToken cancellationToken) { - if (outputStringBuilder == null) - throw new InvalidOperationException("Output reading was not enabled!"); - return outputStringBuilder.ToString().TrimStart(Environment.NewLine.ToCharArray()); + if (standardOutputTask == null) + throw new InvalidOperationException("Output stream reading was not enabled!"); + return standardOutputTask.WithToken(cancellationToken); } /// public void Terminate() { - if (handle.HasExited) + if (Lifetime.IsCompleted) return; try { logger.LogTrace("Terminating PID {0}...", Id); handle.Kill(); - if (!handle.WaitForExit(MaximumWaitMilliseconds)) - { - logger.LogError( - "PID {0} hasn't exited in {1} seconds! This may cause issues with port reuse.", - Id, - TimeSpan.FromMilliseconds(MaximumWaitMilliseconds).TotalSeconds); - emergencyLifetimeTcs.TrySetResult(null); - } + // DO NOT USE WaitForExit! https://stackoverflow.com/a/26722542 } catch (Exception e) { diff --git a/src/Tgstation.Server.Host/System/ProcessExecutor.cs b/src/Tgstation.Server.Host/System/ProcessExecutor.cs index 444e6eda9a..4c0bc5f8c8 100644 --- a/src/Tgstation.Server.Host/System/ProcessExecutor.cs +++ b/src/Tgstation.Server.Host/System/ProcessExecutor.cs @@ -1,5 +1,6 @@ using Microsoft.Extensions.Logging; using System; +using System.IO; using System.Text; using System.Threading.Tasks; @@ -34,19 +35,36 @@ namespace Tgstation.Server.Host.System var tcs = new TaskCompletionSource(); handle.Exited += (a, b) => { - int exitCode; try { - exitCode = handle.ExitCode; - } - catch (InvalidOperationException) - { - return; - } + if (tcs.Task.IsCompleted) + { + logger.LogTrace("Skipping process exit handler as the TaskCompletionSource is already set"); + return; + } - // Try because this can be invoked twice for weird reasons - if (tcs.TrySetResult(exitCode)) - logger.LogTrace("Process exit event completed"); + try + { + var exitCode = handle.ExitCode; + + // Try because this can be invoked twice for weird reasons + if (tcs.TrySetResult(exitCode)) + logger.LogTrace("Process termination event completed"); + else + logger.LogTrace("Ignoring duplicate process termination event"); + } + catch (InvalidOperationException ex) + { + if (!tcs.Task.IsCompleted) + throw; + + logger.LogTrace(ex, "Ignoring expected exception!"); + } + } + catch(Exception ex) + { + logger.LogError(ex, "Process exit handler exception!"); + } }; return tcs.Task; @@ -128,92 +146,72 @@ namespace Tgstation.Server.Host.System handle.StartInfo.UseShellExecute = !noShellExecute; - StringBuilder outputStringBuilder = null, errorStringBuilder = null, combinedStringBuilder = null; + StringBuilder combinedStringBuilder = null; - TaskCompletionSource outputReadTcs = null; - TaskCompletionSource errorReadTcs = null; + Task outputTask = null; + Task errorTask = null; + TaskCompletionSource processStartTcs = null; if (readOutput || readError) { combinedStringBuilder = new StringBuilder(); + processStartTcs = new TaskCompletionSource(); + + async Task ConsumeReader(Func readerFunc) + { + var stringBuilder = new StringBuilder(); + string text; + + await processStartTcs.Task.ConfigureAwait(false); + + var reader = readerFunc(); + while ((text = await reader.ReadLineAsync().ConfigureAwait(false)) != null) + { + combinedStringBuilder.AppendLine(); + combinedStringBuilder.Append(text); + stringBuilder.AppendLine(); + stringBuilder.Append(text); + } + + return stringBuilder.ToString(); + } + if (readOutput) { - outputStringBuilder = new StringBuilder(); + outputTask = ConsumeReader(() => handle.StandardOutput); handle.StartInfo.RedirectStandardOutput = true; - outputReadTcs = new TaskCompletionSource(); - handle.OutputDataReceived += (sender, e) => - { - if (e.Data == null) - { - outputReadTcs.SetResult(null); - return; - } - - combinedStringBuilder.Append(Environment.NewLine); - combinedStringBuilder.Append(e.Data); - outputStringBuilder.Append(Environment.NewLine); - outputStringBuilder.Append(e.Data); - }; } if (readError) { - errorStringBuilder = new StringBuilder(); + errorTask = ConsumeReader(() => handle.StandardError); handle.StartInfo.RedirectStandardError = true; - errorReadTcs = new TaskCompletionSource(); - handle.ErrorDataReceived += (sender, e) => - { - if (e.Data == null) - { - errorReadTcs.SetResult(null); - return; - } - - combinedStringBuilder.Append(Environment.NewLine); - combinedStringBuilder.Append(e.Data); - errorStringBuilder.Append(Environment.NewLine); - errorStringBuilder.Append(e.Data); - }; } } var lifetimeTask = AttachExitHandler(handle); - handle.Start(); - - static async Task AddToLifetimeTask(Task originalTask, TaskCompletionSource tcs) - { - var exitCode = await originalTask.ConfigureAwait(false); - await tcs.Task.ConfigureAwait(false); - return exitCode; - } - try { - if (readOutput) - { - handle.BeginOutputReadLine(); - lifetimeTask = AddToLifetimeTask(lifetimeTask, outputReadTcs); - } - } - catch (InvalidOperationException) { } - try - { - if (readError) - { - handle.BeginErrorReadLine(); - lifetimeTask = AddToLifetimeTask(lifetimeTask, errorReadTcs); - } - } - catch (InvalidOperationException) { } + handle.Start(); - return new Process( - processFeatures, - handle, - lifetimeTask, - outputStringBuilder, - errorStringBuilder, - combinedStringBuilder, - loggerFactory.CreateLogger(), false); + var process = new Process( + processFeatures, + handle, + lifetimeTask, + outputTask, + errorTask, + combinedStringBuilder, + loggerFactory.CreateLogger(), false); + + processStartTcs?.SetResult(null); + + return process; + } + catch (Exception ex) + { + processStartTcs?.SetException(ex); + throw; + } } catch { diff --git a/src/Tgstation.Server.Host/System/WindowsProcessFeatures.cs b/src/Tgstation.Server.Host/System/WindowsProcessFeatures.cs index 6739f12b4c..d31d7c61d4 100644 --- a/src/Tgstation.Server.Host/System/WindowsProcessFeatures.cs +++ b/src/Tgstation.Server.Host/System/WindowsProcessFeatures.cs @@ -6,6 +6,8 @@ using System.Linq; using System.Management; using System.Threading; using System.Threading.Tasks; +using Tgstation.Server.Api.Models; +using Tgstation.Server.Host.Jobs; namespace Tgstation.Server.Host.System { @@ -91,6 +93,9 @@ namespace Tgstation.Server.Host.System => Task.Factory.StartNew( () => { + if (process.HasExited) + throw new JobException(ErrorCode.DreamDaemonOffline); + using var fileStream = new FileStream(outputFile, FileMode.CreateNew); if (!NativeMethods.MiniDumpWriteDump( process.Handle, diff --git a/tests/Tgstation.Server.Tests/Instance/WatchdogTest.cs b/tests/Tgstation.Server.Tests/Instance/WatchdogTest.cs index 7b39618540..681c4020cd 100644 --- a/tests/Tgstation.Server.Tests/Instance/WatchdogTest.cs +++ b/tests/Tgstation.Server.Tests/Instance/WatchdogTest.cs @@ -87,7 +87,7 @@ namespace Tgstation.Server.Tests.Instance while (!dumpTask.IsCompleted) KillDD(false); var job = await WaitForJob(await dumpTask, 10, true, null, cancellationToken); - Assert.IsTrue(job.ErrorCode == ErrorCode.DreamDaemonOffline || job.ErrorCode == ErrorCode.GCoreFailure); + Assert.IsTrue(job.ErrorCode == ErrorCode.DreamDaemonOffline || job.ErrorCode == ErrorCode.GCoreFailure, $"{job.ErrorCode}: {job.ExceptionDetails}"); await Task.Delay(TimeSpan.FromSeconds(20), cancellationToken); var ddStatus = await instanceClient.DreamDaemon.Read(cancellationToken); @@ -209,7 +209,7 @@ namespace Tgstation.Server.Tests.Instance ourProcessHandler.Suspend(); - await Task.WhenAny(ourProcessHandler.Lifetime, Task.Delay(TimeSpan.FromSeconds(20))); + await Task.WhenAny(ourProcessHandler.Lifetime, Task.Delay(TimeSpan.FromMinutes(1))); var timeout = 20; do diff --git a/tests/Tgstation.Server.Tests/IntegrationTest.cs b/tests/Tgstation.Server.Tests/IntegrationTest.cs index 496fa12d7a..127235d59d 100644 --- a/tests/Tgstation.Server.Tests/IntegrationTest.cs +++ b/tests/Tgstation.Server.Tests/IntegrationTest.cs @@ -434,8 +434,8 @@ namespace Tgstation.Server.Tests var exitCode = await process.Lifetime.WithToken(cts.Token); Assert.AreEqual(0, exitCode); - Assert.AreEqual(String.Empty, process.GetErrorOutput().Trim()); - Assert.AreEqual("Hello World!", process.GetStandardOutput().Trim()); + Assert.AreEqual(String.Empty, (await process.GetErrorOutput(default)).Trim()); + Assert.AreEqual("Hello World!", (await process.GetStandardOutput(default)).Trim()); } } } From 722567ded8f4d1eebc514d0b219b49016c3e14d1 Mon Sep 17 00:00:00 2001 From: Jordan Brown Date: Mon, 27 Jul 2020 22:23:58 -0400 Subject: [PATCH 45/68] Fix auto updates not clearing merged PRs --- .../Components/Instance.cs | 114 ++++++++++++++++-- .../Components/InstanceFactory.cs | 16 ++- .../Components/Repository/IRepository.cs | 11 +- .../Components/Repository/Repository.cs | 50 +++++++- .../Controllers/InstanceController.cs | 4 +- .../Tgstation.Server.Tests/IntegrationTest.cs | 27 +++++ 6 files changed, 205 insertions(+), 17 deletions(-) diff --git a/src/Tgstation.Server.Host/Components/Instance.cs b/src/Tgstation.Server.Host/Components/Instance.cs index 83e4bca775..fbe5466555 100644 --- a/src/Tgstation.Server.Host/Components/Instance.cs +++ b/src/Tgstation.Server.Host/Components/Instance.cs @@ -1,5 +1,6 @@ using Microsoft.EntityFrameworkCore; using Microsoft.Extensions.Logging; +using Octokit; using Serilog.Context; using System; using System.Collections.Generic; @@ -13,6 +14,9 @@ using Tgstation.Server.Host.Components.Deployment; using Tgstation.Server.Host.Components.Events; using Tgstation.Server.Host.Components.Repository; using Tgstation.Server.Host.Components.Watchdog; +using Tgstation.Server.Host.Configuration; +using Tgstation.Server.Host.Core; +using Tgstation.Server.Host.Extensions; using Tgstation.Server.Host.Jobs; using Tgstation.Server.Host.Models; @@ -60,6 +64,11 @@ namespace Tgstation.Server.Host.Components /// readonly IEventConsumer eventConsumer; + /// + /// The for the . + /// + readonly IGitHubClientFactory gitHubClientFactory; + /// /// The for the /// @@ -70,6 +79,11 @@ namespace Tgstation.Server.Host.Components /// readonly Api.Models.Instance metadata; + /// + /// The for the . + /// + readonly GeneralConfiguration generalConfiguration; + /// /// for and . /// @@ -98,7 +112,9 @@ namespace Tgstation.Server.Host.Components /// The value of /// The value of /// The value of + /// The value of . /// The value of + /// The value of . public Instance( Api.Models.Instance metadata, IRepositoryManager repositoryManager, @@ -111,7 +127,9 @@ namespace Tgstation.Server.Host.Components IDmbFactory dmbFactory, IJobManager jobManager, IEventConsumer eventConsumer, - ILogger logger) + IGitHubClientFactory gitHubClientFactory, + ILogger logger, + GeneralConfiguration generalConfiguration) { this.metadata = metadata ?? throw new ArgumentNullException(nameof(metadata)); RepositoryManager = repositoryManager ?? throw new ArgumentNullException(nameof(repositoryManager)); @@ -123,7 +141,9 @@ namespace Tgstation.Server.Host.Components this.dmbFactory = dmbFactory ?? throw new ArgumentNullException(nameof(dmbFactory)); this.jobManager = jobManager ?? throw new ArgumentNullException(nameof(jobManager)); this.eventConsumer = eventConsumer ?? throw new ArgumentNullException(nameof(eventConsumer)); + this.gitHubClientFactory = gitHubClientFactory ?? throw new ArgumentNullException(nameof(gitHubClientFactory)); this.logger = logger ?? throw new ArgumentNullException(nameof(logger)); + this.generalConfiguration = generalConfiguration ?? throw new ArgumentNullException(nameof(generalConfiguration)); timerLock = new object(); } @@ -230,7 +250,7 @@ namespace Tgstation.Server.Host.Components .Include(x => x.ActiveTestMerges).ThenInclude(x => x.TestMerge) .FirstOrDefaultAsync(jobCancellationToken); - async Task UpdateRevInfo(string currentHead, bool onOrigin) + async Task UpdateRevInfo(string currentHead, bool onOrigin, IEnumerable updatedTestMerges) { if (currentRevInfo == null) currentRevInfo = await LoadRevInfo().ConfigureAwait(false); @@ -253,7 +273,8 @@ namespace Tgstation.Server.Host.Components Instance = attachedInstance }; if (!onOrigin) - currentRevInfo.ActiveTestMerges = new List(oldRevInfo.ActiveTestMerges); + currentRevInfo.ActiveTestMerges = new List( + updatedTestMerges ?? oldRevInfo.ActiveTestMerges); databaseContext.Instances.Attach(attachedInstance); databaseContext.RevisionInformations.Add(currentRevInfo); @@ -261,8 +282,9 @@ namespace Tgstation.Server.Host.Components } // take appropriate auto update actions - bool shouldSyncTracked; - if (repositorySettings.AutoUpdatesKeepTestMerges.Value) + bool shouldSyncTracked = false; + bool preserveTestMerges = repositorySettings.AutoUpdatesKeepTestMerges.Value; + if (preserveTestMerges) { logger.LogTrace("Preserving test merges..."); @@ -275,21 +297,29 @@ namespace Tgstation.Server.Host.Components currentRevInfo = await currentRevInfoTask.ConfigureAwait(false); + var updatedTestMerges = await RemoveMergedPullRequests( + repo, + repositorySettings, + currentRevInfo, + cancellationToken) + .ConfigureAwait(false); + var lastRevInfoWasOriginCommit = currentRevInfo == default || currentRevInfo.CommitSha == currentRevInfo.OriginCommitSha; var stillOnOrigin = result.Value && lastRevInfoWasOriginCommit; var currentHead = repo.Head; if (currentHead != startSha) { - await UpdateRevInfo(currentHead, stillOnOrigin).ConfigureAwait(false); + await UpdateRevInfo(currentHead, stillOnOrigin, updatedTestMerges).ConfigureAwait(false); shouldSyncTracked = stillOnOrigin; } else shouldSyncTracked = false; } - else + + if (!preserveTestMerges) { - logger.LogTrace("Not preserving test merges..."); + logger.LogTrace("Resetting to origin..."); await repo.ResetToOrigin(NextProgressReporter(), jobCancellationToken).ConfigureAwait(false); var currentHead = repo.Head; @@ -301,7 +331,7 @@ namespace Tgstation.Server.Host.Components .ConfigureAwait(false); if (currentHead != startSha && currentRevInfo == default) - await UpdateRevInfo(currentHead, true).ConfigureAwait(false); + await UpdateRevInfo(currentHead, true, null).ConfigureAwait(false); shouldSyncTracked = true; } @@ -312,7 +342,7 @@ namespace Tgstation.Server.Host.Components var pushedOrigin = await repo.Sychronize(repositorySettings.AccessUser, repositorySettings.AccessToken, repositorySettings.CommitterName, repositorySettings.CommitterEmail, NextProgressReporter(), shouldSyncTracked, jobCancellationToken).ConfigureAwait(false); var currentHead = repo.Head; if (currentHead != currentRevInfo.CommitSha) - await UpdateRevInfo(currentHead, pushedOrigin).ConfigureAwait(false); + await UpdateRevInfo(currentHead, pushedOrigin, null).ConfigureAwait(false); } repoHead = repo.Head; @@ -393,7 +423,69 @@ namespace Tgstation.Server.Host.Components logger.LogTrace("Leaving auto update loop..."); } - #pragma warning restore CA1502 +#pragma warning restore CA1502 + + /// + /// Get the updated list of s for an origin merge. + /// + /// The to use. + /// The . + /// The current . + /// The for the operation. + /// A resulting in the of s that should remain the new . + async Task> RemoveMergedPullRequests( + IRepository repository, + RepositorySettings repositorySettings, + RevisionInformation revisionInformation, + CancellationToken cancellationToken) + { + if (revisionInformation.ActiveTestMerges?.Any() != true) + { + logger.LogTrace("No test merges to remove."); + return Array.Empty(); + } + + var gitHubClient = repositorySettings.AccessToken != null + ? gitHubClientFactory.CreateClient(repositorySettings.AccessToken) + : (String.IsNullOrEmpty(generalConfiguration.GitHubAccessToken) + ? gitHubClientFactory.CreateClient() + : gitHubClientFactory.CreateClient(generalConfiguration.GitHubAccessToken)); + + var tasks = revisionInformation + .ActiveTestMerges + .Select(x => gitHubClient + .PullRequest + .Get(repository.GitHubOwner, repository.GitHubRepoName, x.TestMerge.Number) + .WithToken(cancellationToken)); + try + { + await Task.WhenAll(tasks).ConfigureAwait(false); + } + catch (Exception ex) when (!(ex is OperationCanceledException)) + { + logger.LogWarning(ex, "Pull requests update check failed!"); + } + + var newList = revisionInformation.ActiveTestMerges.ToList(); + + async Task CheckRemovePR(Task task) + { + var pr = await task.ConfigureAwait(false); + if (!pr.Merged) + return; + + // We don't just assume, actually check the repo contains the merge commit. + if (await repository.ShaIsParent(pr.MergeCommitSha, cancellationToken).ConfigureAwait(false)) + newList.Remove( + newList.First( + potential => potential.TestMerge.Number == pr.Number)); + } + + foreach (var prTask in tasks) + await CheckRemovePR(prTask).ConfigureAwait(false); + + return newList; + } /// public Task InstanceRenamed(string newName, CancellationToken cancellationToken) diff --git a/src/Tgstation.Server.Host/Components/InstanceFactory.cs b/src/Tgstation.Server.Host/Components/InstanceFactory.cs index 916b6950f4..dc6243b294 100644 --- a/src/Tgstation.Server.Host/Components/InstanceFactory.cs +++ b/src/Tgstation.Server.Host/Components/InstanceFactory.cs @@ -1,4 +1,5 @@ using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Options; using System; using System.Threading; using System.Threading.Tasks; @@ -11,6 +12,7 @@ using Tgstation.Server.Host.Components.Interop.Bridge; using Tgstation.Server.Host.Components.Repository; using Tgstation.Server.Host.Components.Session; using Tgstation.Server.Host.Components.Watchdog; +using Tgstation.Server.Host.Configuration; using Tgstation.Server.Host.Core; using Tgstation.Server.Host.Database; using Tgstation.Server.Host.IO; @@ -123,6 +125,11 @@ namespace Tgstation.Server.Host.Components /// readonly IServerPortProvider serverPortProvider; + /// + /// The for the . + /// + readonly GeneralConfiguration generalConfiguration; + /// /// Construct an /// @@ -146,6 +153,7 @@ namespace Tgstation.Server.Host.Components /// The value of . /// The value of . /// The value of . + /// The containing the value of . public InstanceFactory( IIOManager ioManager, IDatabaseContextFactory databaseContextFactory, @@ -166,7 +174,8 @@ namespace Tgstation.Server.Host.Components IPlatformIdentifier platformIdentifier, ILibGit2RepositoryFactory repositoryFactory, ILibGit2Commands repositoryCommands, - IServerPortProvider serverPortProvider) + IServerPortProvider serverPortProvider, + IOptions generalConfigurationOptions) { this.ioManager = ioManager ?? throw new ArgumentNullException(nameof(ioManager)); this.databaseContextFactory = databaseContextFactory ?? throw new ArgumentNullException(nameof(databaseContextFactory)); @@ -188,6 +197,7 @@ namespace Tgstation.Server.Host.Components this.repositoryFactory = repositoryFactory ?? throw new ArgumentNullException(nameof(repositoryFactory)); this.repositoryCommands = repositoryCommands ?? throw new ArgumentNullException(nameof(repositoryCommands)); this.serverPortProvider = serverPortProvider ?? throw new ArgumentNullException(nameof(serverPortProvider)); + generalConfiguration = generalConfigurationOptions?.Value ?? throw new ArgumentNullException(nameof(generalConfigurationOptions)); } /// @@ -287,7 +297,9 @@ namespace Tgstation.Server.Host.Components dmbFactory, jobManager, eventConsumer, - loggerFactory.CreateLogger()); + gitHubClientFactory, + loggerFactory.CreateLogger(), + generalConfiguration); return instance; } diff --git a/src/Tgstation.Server.Host/Components/Repository/IRepository.cs b/src/Tgstation.Server.Host/Components/Repository/IRepository.cs index 44cb5b39db..7790315cc2 100644 --- a/src/Tgstation.Server.Host/Components/Repository/IRepository.cs +++ b/src/Tgstation.Server.Host/Components/Repository/IRepository.cs @@ -1,4 +1,4 @@ -using System; +using System; using System.Threading; using System.Threading.Tasks; using Tgstation.Server.Api.Models; @@ -132,5 +132,14 @@ namespace Tgstation.Server.Host.Components.Repository /// The for the operation /// A representing the running operation Task CopyTo(string path, CancellationToken cancellationToken); + + /// + /// Check if a given is a parent of the current . + /// + /// The SHA to check. + /// The for the operation. + /// A resulting in if is a parent of , otherwise. + /// This function is NOT reentrant. + Task ShaIsParent(string sha, CancellationToken cancellationToken); } } diff --git a/src/Tgstation.Server.Host/Components/Repository/Repository.cs b/src/Tgstation.Server.Host/Components/Repository/Repository.cs index 4db0962c02..1165e0f420 100644 --- a/src/Tgstation.Server.Host/Components/Repository/Repository.cs +++ b/src/Tgstation.Server.Host/Components/Repository/Repository.cs @@ -22,6 +22,16 @@ namespace Tgstation.Server.Host.Components.Repository /// public const string GitHubUrl = "://github.com/"; + /// + /// The default username for committers. + /// + public const string DefaultCommitterName = "tgstation-server"; + + /// + /// The default password for committers. + /// + public const string DefaultCommitterEmail = "tgstation-server@users.noreply.github.com"; + /// /// Template error message for when tracking of the most recent origin commit fails /// @@ -554,7 +564,7 @@ namespace Tgstation.Server.Host.Components.Repository trackedBranch = libGitRepo.Head.TrackedBranch; logger.LogDebug("Merge origin/{2}: <{0} ({1})>", committerName, committerEmail, trackedBranch.FriendlyName); - result = libGitRepo.Merge(trackedBranch, new Signature(new Identity(committerName, committerEmail), DateTimeOffset.Now), new MergeOptions + result = libGitRepo.Merge(trackedBranch, new Signature(committerName, committerEmail, DateTimeOffset.Now), new MergeOptions { CommitOnSuccess = true, FailOnConflict = true, @@ -708,5 +718,43 @@ namespace Tgstation.Server.Host.Components.Repository return true; return false; }, cancellationToken, TaskCreationOptions.LongRunning, TaskScheduler.Current); + + /// + public Task ShaIsParent(string sha, CancellationToken cancellationToken) => Task.Factory.StartNew(() => + { + var targetCommit = libGitRepo.Lookup(sha); + if(targetCommit == null) + { + logger.LogTrace("Commit {0} not found in repository", sha); + return false; + } + + cancellationToken.ThrowIfCancellationRequested(); + var startSha = Head; + var mergeResult = libGitRepo.Merge( + targetCommit, + new Signature( + DefaultCommitterName, + DefaultCommitterEmail, + DateTimeOffset.Now), + new MergeOptions + { + FastForwardStrategy = FastForwardStrategy.FastForwardOnly, + FailOnConflict = true + }); + + if (mergeResult.Status == MergeStatus.UpToDate) + return true; + + commands.Checkout( + libGitRepo, + new CheckoutOptions + { + CheckoutModifiers = CheckoutModifiers.Force + }, + startSha); + + return false; + }, cancellationToken, TaskCreationOptions.LongRunning, TaskScheduler.Current); } } diff --git a/src/Tgstation.Server.Host/Controllers/InstanceController.cs b/src/Tgstation.Server.Host/Controllers/InstanceController.cs index 7483cecb69..243fe297db 100644 --- a/src/Tgstation.Server.Host/Controllers/InstanceController.cs +++ b/src/Tgstation.Server.Host/Controllers/InstanceController.cs @@ -126,8 +126,8 @@ namespace Tgstation.Server.Host.Controllers ChatBotLimit = initialSettings.ChatBotLimit ?? Models.Instance.DefaultChatBotLimit, RepositorySettings = new RepositorySettings { - CommitterEmail = "tgstation-server@users.noreply.github.com", - CommitterName = "tgstation-server", + CommitterEmail = Components.Repository.Repository.DefaultCommitterEmail, + CommitterName = Components.Repository.Repository.DefaultCommitterName, PushTestMergeCommits = false, ShowTestMergeCommitters = false, AutoUpdatesKeepTestMerges = false, diff --git a/tests/Tgstation.Server.Tests/IntegrationTest.cs b/tests/Tgstation.Server.Tests/IntegrationTest.cs index 127235d59d..3f96c59e2d 100644 --- a/tests/Tgstation.Server.Tests/IntegrationTest.cs +++ b/tests/Tgstation.Server.Tests/IntegrationTest.cs @@ -19,6 +19,8 @@ using System.Threading.Tasks; using Tgstation.Server.Api; using Tgstation.Server.Api.Models; using Tgstation.Server.Client; +using Tgstation.Server.Host.Components.Events; +using Tgstation.Server.Host.Components.Repository; using Tgstation.Server.Host.Configuration; using Tgstation.Server.Host.Database; using Tgstation.Server.Host.Database.Migrations; @@ -437,5 +439,30 @@ namespace Tgstation.Server.Tests Assert.AreEqual(String.Empty, (await process.GetErrorOutput(default)).Trim()); Assert.AreEqual("Hello World!", (await process.GetStandardOutput(default)).Trim()); } + + [TestMethod] + public async Task TestRepoParentLookup() + { + using var testingServer = new TestingServer(); + LibGit2Sharp.Repository.Clone("https://github.com/Cyberboss/test", testingServer.Directory); + var libGit2Repo = new LibGit2Sharp.Repository(testingServer.Directory); + using var repo = new Host.Components.Repository.Repository( + libGit2Repo, + new LibGit2Commands(), + Mock.Of(), + Mock.Of(), + Mock.Of(), + Mock.Of>(), + () => { }); + + const string StartSha = "af4da8beb9f9b374b04a3cc4d65acca662e8cc1a"; + await repo.CheckoutObject(StartSha, progress => { }, default); + var result = await repo.ShaIsParent("2f8588a3ca0f6b027704a2a04381215619de3412", default); + Assert.IsTrue(result); + Assert.AreEqual(StartSha, repo.Head); + result = await repo.ShaIsParent("f636418bf47d238d33b0e4a34f0072b23a8aad0e", default); + Assert.IsFalse(result); ; + Assert.AreEqual(StartSha, repo.Head); + } } } From e1634fadafaadc44e4fa713b4ef7536a5cd2a22a Mon Sep 17 00:00:00 2001 From: Jordan Brown Date: Tue, 28 Jul 2020 13:42:31 -0400 Subject: [PATCH 46/68] Finally fix the process termination issues maybe --- src/Tgstation.Server.Host/System/Process.cs | 17 +++-- .../System/ProcessExecutor.cs | 69 ++++++++++--------- 2 files changed, 48 insertions(+), 38 deletions(-) diff --git a/src/Tgstation.Server.Host/System/Process.cs b/src/Tgstation.Server.Host/System/Process.cs index 32cca93336..56ab181e58 100644 --- a/src/Tgstation.Server.Host/System/Process.cs +++ b/src/Tgstation.Server.Host/System/Process.cs @@ -85,7 +85,7 @@ namespace Tgstation.Server.Host.System { handle.WaitForInputIdle(); } - catch (InvalidOperationException ex) + catch (Exception ex) { logger.LogDebug(ex, "Error on WaitForInputIdle()!"); } @@ -102,7 +102,6 @@ namespace Tgstation.Server.Host.System async Task WrapLifetimeTask(Task lifetimeTask) { - // relevant: https://stackoverflow.com/a/26722542 var exitCode = await lifetimeTask.ConfigureAwait(false); logger.LogTrace("PID {0} exited with code {1}", Id, exitCode); return exitCode; @@ -136,18 +135,22 @@ namespace Tgstation.Server.Host.System /// public void Terminate() { - if (Lifetime.IsCompleted) + if (handle.HasExited) + { + logger.LogTrace("PID {0} already exited", Id); return; + } + try { logger.LogTrace("Terminating PID {0}...", Id); handle.Kill(); - - // DO NOT USE WaitForExit! https://stackoverflow.com/a/26722542 + if (!handle.WaitForExit(5000)) + logger.LogWarning("WaitForExit() on PID {0} timed out!", Id); } catch (Exception e) { - logger.LogDebug(e, "Process termination exception!"); + logger.LogDebug(e, "PID {0} termination exception!", Id); } } @@ -195,7 +198,7 @@ namespace Tgstation.Server.Host.System } } - /// + /// public async Task GetExecutingUsername(CancellationToken cancellationToken) { var result = await processFeatures.GetExecutingUsername(handle, cancellationToken).ConfigureAwait(false); diff --git a/src/Tgstation.Server.Host/System/ProcessExecutor.cs b/src/Tgstation.Server.Host/System/ProcessExecutor.cs index 4c0bc5f8c8..04329dfd35 100644 --- a/src/Tgstation.Server.Host/System/ProcessExecutor.cs +++ b/src/Tgstation.Server.Host/System/ProcessExecutor.cs @@ -24,22 +24,27 @@ namespace Tgstation.Server.Host.System /// readonly ILoggerFactory loggerFactory; - /// - /// Create a resulting in the exit code of a given - /// - /// The to attach the for - /// A new resulting in the exit code of - Task AttachExitHandler(global::System.Diagnostics.Process handle) + async Task> AttachExitHandlerBeforeLaunch(global::System.Diagnostics.Process handle, Task startupTask) + { + var id = -1; + var result = AttachExitHandler(handle, () => id); + await startupTask.ConfigureAwait(false); + return result; + } + + Task AttachExitHandler(global::System.Diagnostics.Process handle, Func idProvider) { handle.EnableRaisingEvents = true; + var tcs = new TaskCompletionSource(); - handle.Exited += (a, b) => + void ExitHandler(object sender, EventArgs args) { + var id = idProvider(); try { if (tcs.Task.IsCompleted) { - logger.LogTrace("Skipping process exit handler as the TaskCompletionSource is already set"); + logger.LogTrace("Skipping PID {0} exit handler as the TaskCompletionSource is already set", id); return; } @@ -49,23 +54,25 @@ namespace Tgstation.Server.Host.System // Try because this can be invoked twice for weird reasons if (tcs.TrySetResult(exitCode)) - logger.LogTrace("Process termination event completed"); + logger.LogTrace("PID {0} termination event completed", id); else - logger.LogTrace("Ignoring duplicate process termination event"); + logger.LogTrace("Ignoring duplicate PID {0} termination event", id); } catch (InvalidOperationException ex) { if (!tcs.Task.IsCompleted) throw; - logger.LogTrace(ex, "Ignoring expected exception!"); + logger.LogTrace(ex, "Ignoring expected PID {0} exit handler exception!", id); } } - catch(Exception ex) + catch (Exception ex) { - logger.LogError(ex, "Process exit handler exception!"); + logger.LogError(ex, "PID {0} exit handler exception!", id); } - }; + } + + handle.Exited += ExitHandler; return tcs.Task; } @@ -150,11 +157,10 @@ namespace Tgstation.Server.Host.System Task outputTask = null; Task errorTask = null; - TaskCompletionSource processStartTcs = null; + var processStartTcs = new TaskCompletionSource(); if (readOutput || readError) { combinedStringBuilder = new StringBuilder(); - processStartTcs = new TaskCompletionSource(); async Task ConsumeReader(Func readerFunc) { @@ -188,30 +194,30 @@ namespace Tgstation.Server.Host.System } } - var lifetimeTask = AttachExitHandler(handle); + var lifetimeTaskTask = AttachExitHandlerBeforeLaunch(handle, processStartTcs.Task); try { handle.Start(); - var process = new Process( - processFeatures, - handle, - lifetimeTask, - outputTask, - errorTask, - combinedStringBuilder, - loggerFactory.CreateLogger(), false); - - processStartTcs?.SetResult(null); - - return process; + processStartTcs.SetResult(null); } catch (Exception ex) { - processStartTcs?.SetException(ex); + processStartTcs.SetException(ex); throw; } + + var process = new Process( + processFeatures, + handle, + lifetimeTaskTask.GetAwaiter().GetResult(), // won't block + outputTask, + errorTask, + combinedStringBuilder, + loggerFactory.CreateLogger(), false); + + return process; } catch { @@ -250,10 +256,11 @@ namespace Tgstation.Server.Host.System { try { + var pid = handle.Id; return new Process( processFeatures, handle, - AttachExitHandler(handle), + AttachExitHandler(handle, () => pid), null, null, null, From 6af9a0b9abd0047272c74da11fb85e2ab4449f57 Mon Sep 17 00:00:00 2001 From: Jordan Brown Date: Tue, 28 Jul 2020 13:56:59 -0400 Subject: [PATCH 47/68] Move dox build to GitHub actions --- .github/workflows/ci-suite.yml | 44 ++++++++++++++++++++++++++++++++++ .travis.yml | 23 ------------------ build/build_dox.sh | 43 --------------------------------- 3 files changed, 44 insertions(+), 66 deletions(-) delete mode 100644 .travis.yml delete mode 100755 build/build_dox.sh diff --git a/.github/workflows/ci-suite.yml b/.github/workflows/ci-suite.yml index e2e0fa100b..db1d6fdf0b 100644 --- a/.github/workflows/ci-suite.yml +++ b/.github/workflows/ci-suite.yml @@ -66,6 +66,50 @@ jobs: fi exit $retval + dox-build: + name: Build Doxygen Site + runs-on: ubuntu-latest + env: + DOXDIR: ~/tgsdox + steps: + - name: Install Graphviz + run: apt-get install graphviz + + - name: Checkout + uses: actions/checkout@v1 + + - name: Patch Doxyfile + run: | + VERSION=$(cat "build/Version.props" | grep -oPm1 "(?<=)[^<]+") + echo -e "\nPROJECT_NUMBER = $VERSION\nINPUT = .\nOUTPUT_DIRECTORY = $DOXDIR\nPROJECT_LOGO = ./build/tgs.ico\nHAVE_DOT=YES" >> "docs/Doxyfile" + + - name: Prep gh-pages Repository + if: github.event_name == 'push' && github.ref == 'dev' + run: | + git clone -b gh-pages --single-branch "https://git@github.com/tgstation/tgstation-server" "$DOXDIR" + rm -r "$DOXDIR/*" + + - name: Doxygen Build + uses: mattnotmitt/doxygen-action@v1 + with: + doxyfile-path: 'docs/Doxyfile' + + - name: gh-pages push + if: github.event_name == 'push' && github.ref == 'dev' + run: | + cd $DOXDIR + git config --global push.default simple + git config user.name "tgstation-server" + git config user.email "tgstation-server@tgstation13.org" + echo '# THIS BRANCH IS AUTO GENERATED BY GITHUB ACTIONS' > README.md + + # Need to create a .nojekyll file to allow filenames starting with an underscore + # to be seen on the gh-pages site. Therefore creating an empty .nojekyll file. + echo "" > .nojekyll + git add --all + git commit -m "Deploy code docs to GitHub Pages for Travis build $TRAVIS_BUILD_NUMBER" -m "Commit: $TRAVIS_COMMIT" + git push -f "https://${{ secrets.GITHUB_TOKEN }}}@$GITHUB_URL" 2>&1 | /dev/null + docker-build: name: Build Docker Image runs-on: ubuntu-latest diff --git a/.travis.yml b/.travis.yml deleted file mode 100644 index 333ddccbe2..0000000000 --- a/.travis.yml +++ /dev/null @@ -1,23 +0,0 @@ -os: linux -dist: xenial -language: generic -git: - depth: 1 - -branches: - only: - - dev - - master - -jobs: - include: - - env: - name: "Dox Generation" - addons: - apt: - packages: - - doxygen - - graphviz - -script: - - build/build_dox.sh diff --git a/build/build_dox.sh b/build/build_dox.sh deleted file mode 100755 index 9703274177..0000000000 --- a/build/build_dox.sh +++ /dev/null @@ -1,43 +0,0 @@ -#!/bin/bash - -BUILD_FOLDER=$TRAVIS_BUILD_DIR - -DOXDIR=~/tgsdox - -mkdir -p $DOXDIR - -if [ "$TRAVIS_PULL_REQUEST" = false ] && [ "$TRAVIS_BRANCH" = "master" ]; then - PUBLISH_DOX=true -else - PUBLISH_DOX=false -fi - -if [ "$PUBLISH_DOX" = true ] ; then - GITHUB_URL="github.com/$TRAVIS_REPO_SLUG" - echo "Cloning https://git@$GITHUB_URL..." - git clone -b gh-pages --single-branch "https://git@$GITHUB_URL" "$DOXDIR" 2> /dev/null - rm -r "$DOXDIR/*" -fi - -VERSION=$(cat "build/Version.props" | grep -oPm1 "(?<=)[^<]+") - -echo -e "\nPROJECT_NUMBER = $VERSION\nINPUT = $BUILD_FOLDER\nOUTPUT_DIRECTORY = $DOXDIR\nPROJECT_LOGO = $BUILD_FOLDER/build/tgs.ico\nHAVE_DOT=YES" >> "$BUILD_FOLDER/docs/Doxyfile" - -doxygen "$BUILD_FOLDER/docs/Doxyfile" - -if [ "$PUBLISH_DOX" = true ] ; then - cd $DOXDIR - git config --global push.default simple - git config user.name "tgstation-server" - git config user.email "tgstation-server@tgstation13.org" - echo '# THIS BRANCH IS AUTO GENERATED BY TRAVIS CI' > README.md - - # Need to create a .nojekyll file to allow filenames starting with an underscore - # to be seen on the gh-pages site. Therefore creating an empty .nojekyll file. - echo "" > .nojekyll - git add --all - git commit -m "Deploy code docs to GitHub Pages for Travis build $TRAVIS_BUILD_NUMBER" -m "Commit: $TRAVIS_COMMIT" - git push -f "https://$TGS4_GH_PAGES_TOKEN@$GITHUB_URL" 2>&1 | /dev/null - cd "$BUILD_FOLDER" - rm -rf "$DOXDIR/.git" -fi From a7bbd7fafc16f7dcc2a366b59bc37f64c5ca3a4d Mon Sep 17 00:00:00 2001 From: Jordan Brown Date: Tue, 28 Jul 2020 14:00:40 -0400 Subject: [PATCH 48/68] Minor log message fix --- .../Components/Session/SessionControllerFactory.cs | 1 - 1 file changed, 1 deletion(-) diff --git a/src/Tgstation.Server.Host/Components/Session/SessionControllerFactory.cs b/src/Tgstation.Server.Host/Components/Session/SessionControllerFactory.cs index c003e78441..c6c4483cb0 100644 --- a/src/Tgstation.Server.Host/Components/Session/SessionControllerFactory.cs +++ b/src/Tgstation.Server.Host/Components/Session/SessionControllerFactory.cs @@ -214,7 +214,6 @@ namespace Tgstation.Server.Host.Components.Session { logger.LogDebug( "Launching session with CompileJob {0}...", - byondLock.Version.Semver(), dmbProvider.CompileJob.Id); if (launchParameters.SecurityLevel == DreamDaemonSecurity.Trusted) From 910b29a1c06d5b68fd728a1338fd17c8e43294e0 Mon Sep 17 00:00:00 2001 From: Jordan Brown Date: Tue, 28 Jul 2020 14:08:05 -0400 Subject: [PATCH 49/68] More test guards --- .../Instance/WatchdogTest.cs | 47 ++++++++++++++++--- 1 file changed, 40 insertions(+), 7 deletions(-) diff --git a/tests/Tgstation.Server.Tests/Instance/WatchdogTest.cs b/tests/Tgstation.Server.Tests/Instance/WatchdogTest.cs index 681c4020cd..3297d5f3f9 100644 --- a/tests/Tgstation.Server.Tests/Instance/WatchdogTest.cs +++ b/tests/Tgstation.Server.Tests/Instance/WatchdogTest.cs @@ -14,6 +14,7 @@ using Tgstation.Server.Api.Models; using Tgstation.Server.Client; using Tgstation.Server.Client.Components; using Tgstation.Server.Host.Components.Interop; +using Tgstation.Server.Host.Extensions; using Tgstation.Server.Host.IO; using Tgstation.Server.Host.System; @@ -105,7 +106,7 @@ namespace Tgstation.Server.Tests.Instance Assert.IsNull(daemonStatus.ActiveCompileJob.DMApiVersion); Assert.AreEqual(DreamDaemonSecurity.Ultrasafe, daemonStatus.ActiveCompileJob.MinimumSecurityLevel); - var startJob = await instanceClient.DreamDaemon.Start(cancellationToken).ConfigureAwait(false); + var startJob = await StartDD(cancellationToken).ConfigureAwait(false); await WaitForJob(startJob, 20, false, null, cancellationToken); @@ -147,12 +148,14 @@ namespace Tgstation.Server.Tests.Instance blockSocket.SetSocketOption(SocketOptionLevel.Socket, SocketOptionName.ExclusiveAddressUse, true); blockSocket.SetSocketOption(SocketOptionLevel.Socket, SocketOptionName.ReuseAddress, false); blockSocket.Bind(new IPEndPoint(IPAddress.Any, IntegrationTest.DDPort)); + + // Don't use StartDD here startJob = await instanceClient.DreamDaemon.Start(cancellationToken).ConfigureAwait(false); await WaitForJob(startJob, 20, true, ErrorCode.DreamDaemonPortInUse, cancellationToken); } - startJob = await instanceClient.DreamDaemon.Start(cancellationToken).ConfigureAwait(false); + startJob = await StartDD(cancellationToken).ConfigureAwait(false); await WaitForJob(startJob, 20, false, null, cancellationToken); @@ -178,7 +181,7 @@ namespace Tgstation.Server.Tests.Instance HeartbeatSeconds = 1, }, cancellationToken); - var startJob = await instanceClient.DreamDaemon.Start(cancellationToken).ConfigureAwait(false); + var startJob = await StartDD(cancellationToken).ConfigureAwait(false); await WaitForJob(startJob, 20, false, null, cancellationToken); @@ -232,6 +235,36 @@ namespace Tgstation.Server.Tests.Instance }, cancellationToken); } + async Task StartDD(CancellationToken cancellationToken) + { + // integration tests may take a while to release the port + using var cts = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken); + cts.CancelAfter(TimeSpan.FromMinutes(1)); + while (true) + { + try + { + SocketExtensions.BindTest(IntegrationTest.DDPort, false); + break; + } + catch + { + try + { + await Task.Delay(TimeSpan.FromSeconds(1), cts.Token); + continue; + } + catch (OperationCanceledException) + { + } + + throw; + } + } + + return await instanceClient.DreamDaemon.Start(cancellationToken); + } + async Task RunLongRunningTestThenUpdate(CancellationToken cancellationToken) { global::System.Console.WriteLine("TEST: WATCHDOG LONG RUNNING WITH UPDATE TEST"); @@ -246,7 +279,7 @@ namespace Tgstation.Server.Tests.Instance Assert.AreEqual(DMApiConstants.Version, daemonStatus.ActiveCompileJob.DMApiVersion); Assert.AreEqual(DreamDaemonSecurity.Ultrasafe, daemonStatus.ActiveCompileJob.MinimumSecurityLevel); - var startJob = await instanceClient.DreamDaemon.Start(cancellationToken).ConfigureAwait(false); + var startJob = await StartDD(cancellationToken).ConfigureAwait(false); await WaitForJob(startJob, 20, false, null, cancellationToken); @@ -286,7 +319,7 @@ namespace Tgstation.Server.Tests.Instance Assert.AreEqual(DMApiConstants.Version, daemonStatus.ActiveCompileJob.DMApiVersion); Assert.AreEqual(DreamDaemonSecurity.Ultrasafe, daemonStatus.ActiveCompileJob.MinimumSecurityLevel); - var startJob = await instanceClient.DreamDaemon.Start(cancellationToken).ConfigureAwait(false); + var startJob = await StartDD(cancellationToken).ConfigureAwait(false); await WaitForJob(startJob, 20, false, null, cancellationToken); @@ -323,7 +356,7 @@ namespace Tgstation.Server.Tests.Instance var initialStatus = await instanceClient.DreamDaemon.Read(cancellationToken); - var startJob = await instanceClient.DreamDaemon.Start(cancellationToken).ConfigureAwait(false); + var startJob = await StartDD(cancellationToken).ConfigureAwait(false); await WaitForJob(startJob, 40, false, null, cancellationToken); @@ -371,7 +404,7 @@ namespace Tgstation.Server.Tests.Instance if(dd.ActiveCompileJob == null) await DeployTestDme("LongRunning/long_running_test", DreamDaemonSecurity.Trusted, true, cancellationToken); - var startJob = await instanceClient.DreamDaemon.Start(cancellationToken).ConfigureAwait(false); + var startJob = await StartDD(cancellationToken).ConfigureAwait(false); await WaitForJob(startJob, 40, false, null, cancellationToken); From 2e14a541b07716931d6892b7b0399b55643a84dc Mon Sep 17 00:00:00 2001 From: Jordan Brown Date: Tue, 28 Jul 2020 14:11:09 -0400 Subject: [PATCH 50/68] Graphviz already installed in the build action --- .github/workflows/ci-suite.yml | 3 --- 1 file changed, 3 deletions(-) diff --git a/.github/workflows/ci-suite.yml b/.github/workflows/ci-suite.yml index db1d6fdf0b..3447eccc88 100644 --- a/.github/workflows/ci-suite.yml +++ b/.github/workflows/ci-suite.yml @@ -72,9 +72,6 @@ jobs: env: DOXDIR: ~/tgsdox steps: - - name: Install Graphviz - run: apt-get install graphviz - - name: Checkout uses: actions/checkout@v1 From f219028151504f1407a56417aa20cadc919282a9 Mon Sep 17 00:00:00 2001 From: Jordan Brown Date: Tue, 28 Jul 2020 14:17:38 -0400 Subject: [PATCH 51/68] Fix log file downloading directory browsing --- .../Controllers/AdministrationController.cs | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/src/Tgstation.Server.Host/Controllers/AdministrationController.cs b/src/Tgstation.Server.Host/Controllers/AdministrationController.cs index ee8c38ac3e..7da44cc748 100644 --- a/src/Tgstation.Server.Host/Controllers/AdministrationController.cs +++ b/src/Tgstation.Server.Host/Controllers/AdministrationController.cs @@ -377,6 +377,11 @@ namespace Tgstation.Server.Host.Controllers if (path == null) throw new ArgumentNullException(nameof(path)); + // guard against directory navigation + var sanitizedPath = ioManager.GetFileName(path); + if (path != sanitizedPath) + return Forbid(); + var fullPath = ioManager.ConcatPath( fileLoggingConfiguration.GetFullLogDirectory(ioManager, assemblyInformationProvider, platformIdentifier), path); From 6f6a6035a80da0e74860d691e0f99fe28efbe07e Mon Sep 17 00:00:00 2001 From: Jordan Brown Date: Tue, 28 Jul 2020 14:21:41 -0400 Subject: [PATCH 52/68] Process stuff is pain --- src/Tgstation.Server.Host/System/Process.cs | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/src/Tgstation.Server.Host/System/Process.cs b/src/Tgstation.Server.Host/System/Process.cs index 56ab181e58..60734cf725 100644 --- a/src/Tgstation.Server.Host/System/Process.cs +++ b/src/Tgstation.Server.Host/System/Process.cs @@ -1,4 +1,5 @@ using Microsoft.Extensions.Logging; +using Microsoft.Win32.SafeHandles; using System; using System.Diagnostics; using System.Text; @@ -32,6 +33,8 @@ namespace Tgstation.Server.Host.System readonly global::System.Diagnostics.Process handle; + readonly SafeProcessHandle safeHandle; + readonly Task standardOutputTask; readonly Task standardErrorTask; readonly StringBuilder combinedStringBuilder; @@ -62,6 +65,9 @@ namespace Tgstation.Server.Host.System // Do this fast because the runtime will bitch if we try to access it after it ends Id = handle.Id; + // https://stackoverflow.com/a/47656845 + safeHandle = handle.SafeHandle; + this.processFeatures = processFeatures ?? throw new ArgumentNullException(nameof(processFeatures)); this.standardOutputTask = standardOutputTask; @@ -98,7 +104,11 @@ namespace Tgstation.Server.Host.System } /// - public void Dispose() => handle.Dispose(); + public void Dispose() + { + safeHandle.Dispose(); + handle.Dispose(); + } async Task WrapLifetimeTask(Task lifetimeTask) { From 9cb181a8abe2589deac2f3331773d045ae4009ad Mon Sep 17 00:00:00 2001 From: Jordan Brown Date: Tue, 28 Jul 2020 14:54:16 -0400 Subject: [PATCH 53/68] Regression test log file exploit --- tests/Tgstation.Server.Tests/AdministrationTest.cs | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/tests/Tgstation.Server.Tests/AdministrationTest.cs b/tests/Tgstation.Server.Tests/AdministrationTest.cs index f3fb7c1155..7562c33d93 100644 --- a/tests/Tgstation.Server.Tests/AdministrationTest.cs +++ b/tests/Tgstation.Server.Tests/AdministrationTest.cs @@ -1,4 +1,4 @@ -using Microsoft.VisualStudio.TestTools.UnitTesting; +using Microsoft.VisualStudio.TestTools.UnitTesting; using System; using System.Linq; using System.Runtime.InteropServices; @@ -43,6 +43,11 @@ namespace Tgstation.Server.Tests { Name = "very_fake_path.log" }, cancellationToken), ErrorCode.IOError); + + await Assert.ThrowsExceptionAsync(() => client.GetLog(new LogFile + { + Name = "../out_of_bounds.file" + }, cancellationToken)); } async Task TestRead(CancellationToken cancellationToken) From 3635ce21a4276e2177d1c6db9dcf701427eec311 Mon Sep 17 00:00:00 2001 From: Jordan Brown Date: Tue, 28 Jul 2020 14:55:13 -0400 Subject: [PATCH 54/68] Fix doxygen build --- .github/workflows/ci-suite.yml | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/.github/workflows/ci-suite.yml b/.github/workflows/ci-suite.yml index 3447eccc88..b58dfc85ca 100644 --- a/.github/workflows/ci-suite.yml +++ b/.github/workflows/ci-suite.yml @@ -80,6 +80,10 @@ jobs: VERSION=$(cat "build/Version.props" | grep -oPm1 "(?<=)[^<]+") echo -e "\nPROJECT_NUMBER = $VERSION\nINPUT = .\nOUTPUT_DIRECTORY = $DOXDIR\nPROJECT_LOGO = ./build/tgs.ico\nHAVE_DOT=YES" >> "docs/Doxyfile" + - name: Create DOXDIR + if: github.event_name != 'push' || github.ref != 'dev' + run: mkdir -p $DOXDIR + - name: Prep gh-pages Repository if: github.event_name == 'push' && github.ref == 'dev' run: | From 160aa22009c96abda2091d2716294a4ae35c3f39 Mon Sep 17 00:00:00 2001 From: Jordan Brown Date: Tue, 28 Jul 2020 14:55:49 -0400 Subject: [PATCH 55/68] Fix Server Update Package packaging --- .github/workflows/ci-suite.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/ci-suite.yml b/.github/workflows/ci-suite.yml index b58dfc85ca..410e5b6a54 100644 --- a/.github/workflows/ci-suite.yml +++ b/.github/workflows/ci-suite.yml @@ -298,7 +298,7 @@ jobs: - name: Package Server Update Package if: ${{ matrix.configuration == 'Release' && matrix.watchdog-type == 'System' && matrix.database-type == 'PostgresSql' }} run: | - cd ../Tgstation.Server.Host + cd src/Tgstation.Server.Host dotnet publish -c ${{ matrix.configuration }} --no-build -o ../../Artifacts/ServerUpdate - name: Store Server Console From 3b24af9b5154e2cadb7f3262c94979a2f24aed93 Mon Sep 17 00:00:00 2001 From: Jordan Brown Date: Tue, 28 Jul 2020 17:30:31 -0400 Subject: [PATCH 56/68] Fix tests --- src/Tgstation.Server.Api/Routes.cs | 2 +- src/Tgstation.Server.Client/AdministrationClient.cs | 8 +++++--- .../Controllers/AdministrationController.cs | 5 ++++- 3 files changed, 10 insertions(+), 5 deletions(-) diff --git a/src/Tgstation.Server.Api/Routes.cs b/src/Tgstation.Server.Api/Routes.cs index 6c31609fba..231475f728 100644 --- a/src/Tgstation.Server.Api/Routes.cs +++ b/src/Tgstation.Server.Api/Routes.cs @@ -1,4 +1,4 @@ -using System; +using System; using System.Globalization; namespace Tgstation.Server.Api diff --git a/src/Tgstation.Server.Client/AdministrationClient.cs b/src/Tgstation.Server.Client/AdministrationClient.cs index 798ee6cf31..ec1d99513d 100644 --- a/src/Tgstation.Server.Client/AdministrationClient.cs +++ b/src/Tgstation.Server.Client/AdministrationClient.cs @@ -1,7 +1,8 @@ -using System; +using System; using System.Collections.Generic; using System.Threading; using System.Threading.Tasks; +using System.Web; using Tgstation.Server.Api; using Tgstation.Server.Api.Models; @@ -39,7 +40,8 @@ namespace Tgstation.Server.Client /// public Task GetLog(LogFile logFile, CancellationToken cancellationToken) => apiClient.Read( Routes.Logs + Routes.SanitizeGetPath( - logFile?.Name ?? throw new ArgumentNullException(nameof(logFile))), + HttpUtility.UrlEncode( + logFile?.Name ?? throw new ArgumentNullException(nameof(logFile)))), cancellationToken); } -} \ No newline at end of file +} diff --git a/src/Tgstation.Server.Host/Controllers/AdministrationController.cs b/src/Tgstation.Server.Host/Controllers/AdministrationController.cs index 7da44cc748..4083007dec 100644 --- a/src/Tgstation.Server.Host/Controllers/AdministrationController.cs +++ b/src/Tgstation.Server.Host/Controllers/AdministrationController.cs @@ -11,6 +11,7 @@ using System.Linq; using System.Net; using System.Threading; using System.Threading.Tasks; +using System.Web; using Tgstation.Server.Api; using Tgstation.Server.Api.Models; using Tgstation.Server.Api.Rights; @@ -368,7 +369,7 @@ namespace Tgstation.Server.Host.Controllers /// A resulting in the of the request. /// Downloaded successfully. /// An IO error occurred while downloading. - [HttpGet(Routes.Logs + "/{path}")] + [HttpGet(Routes.Logs + "/{*path}")] [TgsAuthorize(AdministrationRights.DownloadLogs)] [ProducesResponseType(typeof(List), 200)] [ProducesResponseType(typeof(ErrorMessage), 409)] @@ -377,6 +378,8 @@ namespace Tgstation.Server.Host.Controllers if (path == null) throw new ArgumentNullException(nameof(path)); + path = HttpUtility.UrlDecode(path); + // guard against directory navigation var sanitizedPath = ioManager.GetFileName(path); if (path != sanitizedPath) From 5c6d3760db389fa1fa9b55bd674750c0ac416ccf Mon Sep 17 00:00:00 2001 From: Jordan Brown Date: Tue, 28 Jul 2020 18:01:39 -0400 Subject: [PATCH 57/68] Timeout tweaks --- .../Instance/WatchdogTest.cs | 17 +++++++++-------- 1 file changed, 9 insertions(+), 8 deletions(-) diff --git a/tests/Tgstation.Server.Tests/Instance/WatchdogTest.cs b/tests/Tgstation.Server.Tests/Instance/WatchdogTest.cs index 3297d5f3f9..9ea66a6f78 100644 --- a/tests/Tgstation.Server.Tests/Instance/WatchdogTest.cs +++ b/tests/Tgstation.Server.Tests/Instance/WatchdogTest.cs @@ -76,7 +76,7 @@ namespace Tgstation.Server.Tests.Instance { System.Console.WriteLine("TEST: WATCHDOG DUMP TESTS"); var dumpJob = await instanceClient.DreamDaemon.CreateDump(cancellationToken); - await WaitForJob(dumpJob, 3000, false, null, cancellationToken); + await WaitForJob(dumpJob, 30, false, null, cancellationToken); var dumpFiles = Directory.GetFiles(Path.Combine( instanceClient.Metadata.Path, "Diagnostics", "ProcessDumps"), "*.dmp"); @@ -87,7 +87,7 @@ namespace Tgstation.Server.Tests.Instance var dumpTask = instanceClient.DreamDaemon.CreateDump(cancellationToken); while (!dumpTask.IsCompleted) KillDD(false); - var job = await WaitForJob(await dumpTask, 10, true, null, cancellationToken); + var job = await WaitForJob(await dumpTask, 20, true, null, cancellationToken); Assert.IsTrue(job.ErrorCode == ErrorCode.DreamDaemonOffline || job.ErrorCode == ErrorCode.GCoreFailure, $"{job.ErrorCode}: {job.ExceptionDetails}"); await Task.Delay(TimeSpan.FromSeconds(20), cancellationToken); @@ -108,7 +108,7 @@ namespace Tgstation.Server.Tests.Instance var startJob = await StartDD(cancellationToken).ConfigureAwait(false); - await WaitForJob(startJob, 20, false, null, cancellationToken); + await WaitForJob(startJob, 40, false, null, cancellationToken); daemonStatus = await instanceClient.DreamDaemon.Read(cancellationToken); Assert.AreEqual(WatchdogStatus.Online, daemonStatus.Status.Value); @@ -152,12 +152,12 @@ namespace Tgstation.Server.Tests.Instance // Don't use StartDD here startJob = await instanceClient.DreamDaemon.Start(cancellationToken).ConfigureAwait(false); - await WaitForJob(startJob, 20, true, ErrorCode.DreamDaemonPortInUse, cancellationToken); + await WaitForJob(startJob, 40, true, ErrorCode.DreamDaemonPortInUse, cancellationToken); } startJob = await StartDD(cancellationToken).ConfigureAwait(false); - await WaitForJob(startJob, 20, false, null, cancellationToken); + await WaitForJob(startJob, 40, false, null, cancellationToken); daemonStatus = await instanceClient.DreamDaemon.Read(cancellationToken); Assert.AreEqual(WatchdogStatus.Online, daemonStatus.Status.Value); @@ -183,7 +183,7 @@ namespace Tgstation.Server.Tests.Instance var startJob = await StartDD(cancellationToken).ConfigureAwait(false); - await WaitForJob(startJob, 20, false, null, cancellationToken); + await WaitForJob(startJob, 40, false, null, cancellationToken); // lock on to DD and pause it so it can't heartbeat var ddProcs = System.Diagnostics.Process.GetProcessesByName("DreamDaemon").ToList(); @@ -261,6 +261,7 @@ namespace Tgstation.Server.Tests.Instance throw; } } + await Task.Delay(TimeSpan.FromSeconds(3), cts.Token); return await instanceClient.DreamDaemon.Start(cancellationToken); } @@ -281,7 +282,7 @@ namespace Tgstation.Server.Tests.Instance var startJob = await StartDD(cancellationToken).ConfigureAwait(false); - await WaitForJob(startJob, 20, false, null, cancellationToken); + await WaitForJob(startJob, 40, false, null, cancellationToken); daemonStatus = await DeployTestDme(DmeName, DreamDaemonSecurity.Safe, true, cancellationToken); @@ -321,7 +322,7 @@ namespace Tgstation.Server.Tests.Instance var startJob = await StartDD(cancellationToken).ConfigureAwait(false); - await WaitForJob(startJob, 20, false, null, cancellationToken); + await WaitForJob(startJob, 40, false, null, cancellationToken); daemonStatus = await DeployTestDme(DmeName + "_copy", DreamDaemonSecurity.Safe, true, cancellationToken); From 0951b39f4ca34bb6b0da50c3a8f5005917551ae6 Mon Sep 17 00:00:00 2001 From: Jordan Brown Date: Wed, 29 Jul 2020 11:49:58 -0400 Subject: [PATCH 58/68] Need the codecov.yml present --- .github/workflows/ci-suite.yml | 3 +++ 1 file changed, 3 insertions(+) diff --git a/.github/workflows/ci-suite.yml b/.github/workflows/ci-suite.yml index 410e5b6a54..7ea699f072 100644 --- a/.github/workflows/ci-suite.yml +++ b/.github/workflows/ci-suite.yml @@ -398,6 +398,9 @@ jobs: needs: [linux-unit-tests, linux-integration-tests, windows-unit-tests, windows-integration-test] runs-on: ubuntu-latest steps: + - name: Checkout for .codecov.yml + uses: actions/checkout@v1 + - name: Retrieve Linux Unit Test Coverage (Debug) uses: actions/download-artifact@v2 with: From 4cc30d99c4ee8f7a5f702d256b6e6263bc145e20 Mon Sep 17 00:00:00 2001 From: Jordan Brown Date: Wed, 29 Jul 2020 12:03:13 -0400 Subject: [PATCH 59/68] Fix badly named CI suite step --- .github/workflows/ci-suite.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/ci-suite.yml b/.github/workflows/ci-suite.yml index 7ea699f072..137825ed8a 100644 --- a/.github/workflows/ci-suite.yml +++ b/.github/workflows/ci-suite.yml @@ -358,7 +358,7 @@ jobs: name: openapi-spec path: C:/swagger.json - - name: Package Server Console + - name: Package Server Service if: ${{ matrix.configuration == 'Release' && matrix.watchdog-type == 'Basic' }} run: | cd src/Tgstation.Server.Host.Service From 33ee270a9fc3d1412266b0b96c60b79af510f610 Mon Sep 17 00:00:00 2001 From: Jordan Brown Date: Wed, 29 Jul 2020 12:16:11 -0400 Subject: [PATCH 60/68] Fix exit handler not having a proper PID --- src/Tgstation.Server.Host/System/ProcessExecutor.cs | 1 + 1 file changed, 1 insertion(+) diff --git a/src/Tgstation.Server.Host/System/ProcessExecutor.cs b/src/Tgstation.Server.Host/System/ProcessExecutor.cs index 04329dfd35..454542ca18 100644 --- a/src/Tgstation.Server.Host/System/ProcessExecutor.cs +++ b/src/Tgstation.Server.Host/System/ProcessExecutor.cs @@ -29,6 +29,7 @@ namespace Tgstation.Server.Host.System var id = -1; var result = AttachExitHandler(handle, () => id); await startupTask.ConfigureAwait(false); + id = handle.Id; return result; } From d2b2abb53c39e71e0df40d22ba21d48795766bf0 Mon Sep 17 00:00:00 2001 From: Jordan Brown Date: Wed, 29 Jul 2020 13:27:33 -0400 Subject: [PATCH 61/68] Remove redundant log message --- .../Components/Deployment/DreamMaker.cs | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/src/Tgstation.Server.Host/Components/Deployment/DreamMaker.cs b/src/Tgstation.Server.Host/Components/Deployment/DreamMaker.cs index 3f75c34356..2f76e1f3a2 100644 --- a/src/Tgstation.Server.Host/Components/Deployment/DreamMaker.cs +++ b/src/Tgstation.Server.Host/Components/Deployment/DreamMaker.cs @@ -447,11 +447,18 @@ namespace Tgstation.Server.Host.Components.Deployment catch (JobException) { // DD never validated or compile failed - await eventConsumer.HandleEvent(EventType.CompileFailure, new List { resolvedOutputDirectory, exitCode == 0 ? "1" : "0" }, cancellationToken).ConfigureAwait(false); + await eventConsumer.HandleEvent( + EventType.CompileFailure, + new List + { + resolvedOutputDirectory, + exitCode == 0 ? "1" : "0" + }, + cancellationToken) + .ConfigureAwait(false); throw; } - logger.LogTrace("Running post compile event..."); await eventConsumer.HandleEvent(EventType.CompileComplete, new List { resolvedOutputDirectory }, cancellationToken).ConfigureAwait(false); logger.LogTrace("Applying static game file symlinks..."); From 2a2c8cb012d4420ff74bbbcd7b6a1c4af4710ffb Mon Sep 17 00:00:00 2001 From: Jordan Brown Date: Wed, 29 Jul 2020 13:29:07 -0400 Subject: [PATCH 62/68] Minor timeout tweak --- tests/Tgstation.Server.Tests/Instance/WatchdogTest.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/Tgstation.Server.Tests/Instance/WatchdogTest.cs b/tests/Tgstation.Server.Tests/Instance/WatchdogTest.cs index 9ea66a6f78..6b6532cc77 100644 --- a/tests/Tgstation.Server.Tests/Instance/WatchdogTest.cs +++ b/tests/Tgstation.Server.Tests/Instance/WatchdogTest.cs @@ -477,7 +477,7 @@ namespace Tgstation.Server.Tests.Instance using (var tempCts = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken)) using (tempCts.Token.Register(() => System.Console.WriteLine("TEST ERROR: Timeout in TellWorldToReboot!"))) { - tempCts.CancelAfter(TimeSpan.FromMinutes(1)); + tempCts.CancelAfter(TimeSpan.FromMinutes(2)); var tempToken = tempCts.Token; do From 1aa8c3b1572f2b7d60b18b500420d6646118bb84 Mon Sep 17 00:00:00 2001 From: Jordan Brown Date: Wed, 29 Jul 2020 15:19:31 -0400 Subject: [PATCH 63/68] Fix InvalidOperationExceptions in dump jobs --- .../System/IProcessFeatures.cs | 2 +- .../System/PosixProcessFeatures.cs | 15 ++++++++++++--- .../System/WindowsProcessFeatures.cs | 11 +++++++++-- 3 files changed, 22 insertions(+), 6 deletions(-) diff --git a/src/Tgstation.Server.Host/System/IProcessFeatures.cs b/src/Tgstation.Server.Host/System/IProcessFeatures.cs index b45d0831e0..2ba9503f6a 100644 --- a/src/Tgstation.Server.Host/System/IProcessFeatures.cs +++ b/src/Tgstation.Server.Host/System/IProcessFeatures.cs @@ -1,4 +1,4 @@ -using System.Threading; +using System.Threading; using System.Threading.Tasks; namespace Tgstation.Server.Host.System diff --git a/src/Tgstation.Server.Host/System/PosixProcessFeatures.cs b/src/Tgstation.Server.Host/System/PosixProcessFeatures.cs index c06c99a812..62ba10172a 100644 --- a/src/Tgstation.Server.Host/System/PosixProcessFeatures.cs +++ b/src/Tgstation.Server.Host/System/PosixProcessFeatures.cs @@ -73,10 +73,19 @@ namespace Tgstation.Server.Host.System if (!await ioManager.FileExists(GCorePath, cancellationToken).ConfigureAwait(false)) throw new JobException(ErrorCode.MissingGCore); - if(process.HasExited) - throw new JobException(ErrorCode.DreamDaemonOffline); + int pid; + try + { + if (process.HasExited) + throw new JobException(ErrorCode.DreamDaemonOffline); + + pid = process.Id; + } + catch (InvalidOperationException ex) + { + throw new JobException(ErrorCode.DreamDaemonOffline, ex); + } - var pid = process.Id; string output; int exitCode; using (var gcoreProc = lazyLoadedProcessExecutor.Value.LaunchProcess( diff --git a/src/Tgstation.Server.Host/System/WindowsProcessFeatures.cs b/src/Tgstation.Server.Host/System/WindowsProcessFeatures.cs index d31d7c61d4..a01d5e850f 100644 --- a/src/Tgstation.Server.Host/System/WindowsProcessFeatures.cs +++ b/src/Tgstation.Server.Host/System/WindowsProcessFeatures.cs @@ -93,8 +93,15 @@ namespace Tgstation.Server.Host.System => Task.Factory.StartNew( () => { - if (process.HasExited) - throw new JobException(ErrorCode.DreamDaemonOffline); + try + { + if (process.HasExited) + throw new JobException(ErrorCode.DreamDaemonOffline); + } + catch (InvalidOperationException ex) + { + throw new JobException(ErrorCode.DreamDaemonOffline, ex); + } using var fileStream = new FileStream(outputFile, FileMode.CreateNew); if (!NativeMethods.MiniDumpWriteDump( From 509e27cef6dd19bfd785b2027c3d1365c98baf4d Mon Sep 17 00:00:00 2001 From: Jordan Brown Date: Wed, 29 Jul 2020 15:29:00 -0400 Subject: [PATCH 64/68] Add security policy --- SECURITY.md | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) create mode 100644 SECURITY.md diff --git a/SECURITY.md b/SECURITY.md new file mode 100644 index 0000000000..7bf70fce52 --- /dev/null +++ b/SECURITY.md @@ -0,0 +1,18 @@ +# Security Policy + +## Supported Versions + +| Version | Supported | +| ------- | ------------------ | +| 4.x.x | :white_check_mark: | +| < 4.0 | :x: | + +## Reporting a Vulnerability + +Vulnerabilities should ideally be reported by directly messaging one of the maintainers on Discord. Maintainers can be found in the [#coderbus](https://discord.gg/Vh8TJp9) guild. Please be sure to provide reproduction steps. + +Here is a list of their discord IDs. + +- Cyberboss#8246 + +Once reported, they will handle the processing of the security advisory. From 6e7b0750a3c31f88fdf43c4a549190beaa4a26dd Mon Sep 17 00:00:00 2001 From: Jordan Brown Date: Wed, 29 Jul 2020 15:57:40 -0400 Subject: [PATCH 65/68] Fix Discord timeouts crashing bridge requests in DM --- .../Components/Chat/Providers/DiscordProvider.cs | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/src/Tgstation.Server.Host/Components/Chat/Providers/DiscordProvider.cs b/src/Tgstation.Server.Host/Components/Chat/Providers/DiscordProvider.cs index 493a087f7b..635c3c2c82 100644 --- a/src/Tgstation.Server.Host/Components/Chat/Providers/DiscordProvider.cs +++ b/src/Tgstation.Server.Host/Components/Chat/Providers/DiscordProvider.cs @@ -259,12 +259,10 @@ namespace Tgstation.Server.Host.Components.Chat.Providers Timeout = 10000 // prevent stupid long hold ups from this }) ?? Task.CompletedTask).ConfigureAwait(false); } - catch (OperationCanceledException) - { - throw; - } catch (Exception e) { + if (e is OperationCanceledException) + cancellationToken.ThrowIfCancellationRequested(); Logger.LogWarning(e, "Error sending discord message!"); } } From db2b989ae9555eaf8b55cf4abb055573766666b5 Mon Sep 17 00:00:00 2001 From: Jordan Brown Date: Thu, 30 Jul 2020 01:27:50 -0400 Subject: [PATCH 66/68] One last timeout fix --- tests/Tgstation.Server.Tests/Instance/WatchdogTest.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/Tgstation.Server.Tests/Instance/WatchdogTest.cs b/tests/Tgstation.Server.Tests/Instance/WatchdogTest.cs index 6b6532cc77..296372b18a 100644 --- a/tests/Tgstation.Server.Tests/Instance/WatchdogTest.cs +++ b/tests/Tgstation.Server.Tests/Instance/WatchdogTest.cs @@ -435,7 +435,7 @@ namespace Tgstation.Server.Tests.Instance while (daemonStatus.Status == WatchdogStatus.Online || daemonStatus.Status == WatchdogStatus.Restoring); Assert.AreEqual(WatchdogStatus.DelayedRestart, daemonStatus.Status.Value); - await Task.Delay(TimeSpan.FromSeconds(10), cancellationToken); + await Task.Delay(TimeSpan.FromSeconds(15), cancellationToken); daemonStatus = await instanceClient.DreamDaemon.Read(cancellationToken); Assert.AreEqual(WatchdogStatus.Online, daemonStatus.Status.Value); From 7e3255158666c5d632f089bd3404b89acdbfaa5e Mon Sep 17 00:00:00 2001 From: Jordan Brown Date: Thu, 30 Jul 2020 01:52:43 -0400 Subject: [PATCH 67/68] Better dump failure testing --- .../Tgstation.Server.Tests/Instance/WatchdogTest.cs | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/tests/Tgstation.Server.Tests/Instance/WatchdogTest.cs b/tests/Tgstation.Server.Tests/Instance/WatchdogTest.cs index 296372b18a..4e4098f100 100644 --- a/tests/Tgstation.Server.Tests/Instance/WatchdogTest.cs +++ b/tests/Tgstation.Server.Tests/Instance/WatchdogTest.cs @@ -84,9 +84,15 @@ namespace Tgstation.Server.Tests.Instance File.Delete(dumpFiles.Single()); KillDD(true); - var dumpTask = instanceClient.DreamDaemon.CreateDump(cancellationToken); - while (!dumpTask.IsCompleted) - KillDD(false); + Task dumpTask = null; + var killTask = Task.Run(() => + { + while (dumpTask?.IsCompleted != true) + KillDD(false); + }); + + dumpTask = instanceClient.DreamDaemon.CreateDump(cancellationToken); + await killTask; var job = await WaitForJob(await dumpTask, 20, true, null, cancellationToken); Assert.IsTrue(job.ErrorCode == ErrorCode.DreamDaemonOffline || job.ErrorCode == ErrorCode.GCoreFailure, $"{job.ErrorCode}: {job.ExceptionDetails}"); await Task.Delay(TimeSpan.FromSeconds(20), cancellationToken); From 3a1caa15187be0a7440830e436040e580d83fb38 Mon Sep 17 00:00:00 2001 From: Jordan Brown Date: Thu, 30 Jul 2020 02:13:55 -0400 Subject: [PATCH 68/68] Don't crash the test if the chat broadcast fails --- .../Chat/Providers/DiscordProvider.cs | 23 +++++++++++-------- tests/DMAPI/LongRunning/Test.dm | 5 ++-- 2 files changed, 16 insertions(+), 12 deletions(-) diff --git a/src/Tgstation.Server.Host/Components/Chat/Providers/DiscordProvider.cs b/src/Tgstation.Server.Host/Components/Chat/Providers/DiscordProvider.cs index 635c3c2c82..b115edf45f 100644 --- a/src/Tgstation.Server.Host/Components/Chat/Providers/DiscordProvider.cs +++ b/src/Tgstation.Server.Host/Components/Chat/Providers/DiscordProvider.cs @@ -252,17 +252,22 @@ namespace Tgstation.Server.Host.Components.Chat.Providers { try { - var channel = client.GetChannel(channelId) as IMessageChannel; - await (channel?.SendMessageAsync(message, false, null, new RequestOptions - { - CancelToken = cancellationToken, - Timeout = 10000 // prevent stupid long hold ups from this - }) ?? Task.CompletedTask).ConfigureAwait(false); + if (!(client.GetChannel(channelId) is IMessageChannel channel)) + return; + + await channel.SendMessageAsync( + message, + false, + null, + new RequestOptions + { + CancelToken = cancellationToken, + Timeout = 10000 // prevent stupid long hold ups from this + }) + .ConfigureAwait(false); } - catch (Exception e) + catch (Exception e) when (!(e is OperationCanceledException)) { - if (e is OperationCanceledException) - cancellationToken.ThrowIfCancellationRequested(); Logger.LogWarning(e, "Error sending discord message!"); } } diff --git a/tests/DMAPI/LongRunning/Test.dm b/tests/DMAPI/LongRunning/Test.dm index d210608d28..5b87b7e5ad 100644 --- a/tests/DMAPI/LongRunning/Test.dm +++ b/tests/DMAPI/LongRunning/Test.dm @@ -31,15 +31,14 @@ /world/proc/HandleTopic(T) TGS_TOPIC - TgsChatBroadcast("Recieved non-tgs topic: [T]") - var/list/data = params2list(T) var/special_tactics = data["tgs_integration_test_special_tactics"] if(special_tactics) RebootAsync() return "ack" - TgsChatBroadcast("Not rebooting...") + TgsChatBroadcast("Recieved non-tgs topic: [T]") + return "feck" /world/Reboot(reason)