diff --git a/.github/CONTRIBUTING.md b/.github/CONTRIBUTING.md index be80cf9929..a7b2e34978 100644 --- a/.github/CONTRIBUTING.md +++ b/.github/CONTRIBUTING.md @@ -48,6 +48,9 @@ The following environment variables aren't required but enable more tests. ### Know your Code +- All feature work should be submitted to the `dev` branch for the next minor release. +- All patch work should be submitted to the `master` branch for the next patch. Changes will be automatically integrated into `dev` + The `/src` folder at the root of this repository contains a series of `README.md` files useful for helping find your way around the codebase. ## Specifications diff --git a/.github/ISSUE_TEMPLATE.md b/.github/ISSUE_TEMPLATE.md deleted file mode 100644 index b88655f26e..0000000000 --- a/.github/ISSUE_TEMPLATE.md +++ /dev/null @@ -1,19 +0,0 @@ -Note that this repository does not contain any client code (With the exception of the web control panel which can be found in [its own repository](https://github.com/tgstation/tgstation-server-control-panel)). Any client issues should be reported to their respective codebases. - -Please include: - -- The version of tgstation-server you were using. - -- The operating system you are running it from. - -- A description of the issue. - -- A link to your codebase git (if public) - - Include active SHA/test merges if applicable - -- The client you we're using (Desktop control panel, web control panel, etc) - - Include a version if applicable - -- Reproduction steps for the issue if possible from your client. - -- The server log of when the event happened (The full file is much more useful than snippets). diff --git a/.github/ISSUE_TEMPLATE/bug-report.md b/.github/ISSUE_TEMPLATE/bug-report.md new file mode 100644 index 0000000000..ba351945b4 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/bug-report.md @@ -0,0 +1,36 @@ +--- +name: Bug Report +about: Report a problem with the server +title: '' +labels: Bug, Reproduction Required, Ready +assignees: Cyberboss + +--- + +**Describe the bug** +A clear and concise description of what the bug is. Please note that client issues (i.e. Control panel crashes) do not belong in this repo and should be report in their own. + +**To Reproduce** +Steps to reproduce the behavior: +1. Go to '...' +2. Click on '....' +3. Scroll down to '....' +4. See error + +**Expected behavior** +A clear and concise description of what you expected to happen. + +**Logs** +Please include full server logs to help diagnose your problem + +**Server State: (please complete the following information):** + - OS: + - Version: + - BYOND Version Used: + - git Repository Used: + - Origin Commit hash Used: + - Active Test Merges: + - Client Version: + +**Additional context** +Add any other context about the problem here. diff --git a/.github/ISSUE_TEMPLATE/feature_request.md b/.github/ISSUE_TEMPLATE/feature_request.md new file mode 100644 index 0000000000..b6058b642b --- /dev/null +++ b/.github/ISSUE_TEMPLATE/feature_request.md @@ -0,0 +1,20 @@ +--- +name: Feature request +about: Suggest an idea for the server +title: '' +labels: Feature Request, Backlog +assignees: '' + +--- + +**Is your feature request related to a problem? Please describe.** +A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] + +**Describe the solution you'd like** +A clear and concise description of what you want to happen. + +**Describe alternatives you've considered** +A clear and concise description of any alternative solutions or features you've considered. + +**Additional context** +Add any other context or screenshots about the feature request here. diff --git a/build/Version.props b/build/Version.props index 264b6860ac..2a24e56613 100644 --- a/build/Version.props +++ b/build/Version.props @@ -2,9 +2,9 @@ - 4.3.1 - 6.5.1 - 7.1.1 + 4.3.2 + 6.6.0 + 7.2.0 5.2.2 0.4.0 1.1.0 diff --git a/src/DMAPI/tgs/core/datum.dm b/src/DMAPI/tgs/core/datum.dm index ef6d3a4fe5..4d37ed662d 100644 --- a/src/DMAPI/tgs/core/datum.dm +++ b/src/DMAPI/tgs/core/datum.dm @@ -50,7 +50,7 @@ TGS_PROTECT_DATUM(/datum/tgs_api) /datum/tgs_api/proc/ChatTargetedBroadcast(message, admin_only) return TGS_UNIMPLEMENTED -/datum/tgs_api/proc/ChatPrivateMessage(message, admin_only) +/datum/tgs_api/proc/ChatPrivateMessage(message, datum/tgs_chat_user/user) return TGS_UNIMPLEMENTED /datum/tgs_api/proc/SecurityLevel() diff --git a/src/Tgstation.Server.Api/Models/ErrorCode.cs b/src/Tgstation.Server.Api/Models/ErrorCode.cs index c62e41d04a..a95d5ae0c1 100644 --- a/src/Tgstation.Server.Api/Models/ErrorCode.cs +++ b/src/Tgstation.Server.Api/Models/ErrorCode.cs @@ -489,5 +489,11 @@ namespace Tgstation.Server.Api.Models /// [Description("The deployment succeeded but one or more notification events failed!")] PostDeployFailure, + + /// + /// Attempted to restart a stopped watchdog. + /// + [Description("Cannot restart the watchdog as it is not running!")] + WatchdogNotRunning, } } \ No newline at end of file diff --git a/src/Tgstation.Server.Api/Models/Internal/DreamDaemonLaunchParameters.cs b/src/Tgstation.Server.Api/Models/Internal/DreamDaemonLaunchParameters.cs index f5bd309d61..56c4c758b3 100644 --- a/src/Tgstation.Server.Api/Models/Internal/DreamDaemonLaunchParameters.cs +++ b/src/Tgstation.Server.Api/Models/Internal/DreamDaemonLaunchParameters.cs @@ -48,16 +48,14 @@ namespace Tgstation.Server.Api.Models.Internal public uint? HeartbeatSeconds { get; set; } /// - /// Check if we match a given set of + /// Check if we match a given set of . is excluded. /// /// The to compare against /// if they match, otherwise - public bool Match(DreamDaemonLaunchParameters otherParameters) => + public bool CanApplyWithoutReboot(DreamDaemonLaunchParameters otherParameters) => AllowWebClient == (otherParameters?.AllowWebClient ?? throw new ArgumentNullException(nameof(otherParameters))) && SecurityLevel == otherParameters.SecurityLevel && PrimaryPort == otherParameters.PrimaryPort - && SecondaryPort == otherParameters.SecondaryPort - && StartupTimeout == otherParameters.StartupTimeout - && HeartbeatSeconds == otherParameters.HeartbeatSeconds; + && SecondaryPort == otherParameters.SecondaryPort; // We intentionally don't check StartupTimeout or heartbeat seconds as it doesn't matter in terms of the watchdog } } \ No newline at end of file diff --git a/src/Tgstation.Server.Host/Components/Byond/ByondManager.cs b/src/Tgstation.Server.Host/Components/Byond/ByondManager.cs index 7896369b02..d58ad67880 100644 --- a/src/Tgstation.Server.Host/Components/Byond/ByondManager.cs +++ b/src/Tgstation.Server.Host/Components/Byond/ByondManager.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.Events; using Tgstation.Server.Host.Core; using Tgstation.Server.Host.IO; using Tgstation.Server.Host.Jobs; diff --git a/src/Tgstation.Server.Host/Components/Deployment/DmbFactory.cs b/src/Tgstation.Server.Host/Components/Deployment/DmbFactory.cs index 978586af70..113dbaa3ad 100644 --- a/src/Tgstation.Server.Host/Components/Deployment/DmbFactory.cs +++ b/src/Tgstation.Server.Host/Components/Deployment/DmbFactory.cs @@ -141,8 +141,11 @@ namespace Tgstation.Server.Host.Components.Deployment { nextDmbProvider?.Dispose(); nextDmbProvider = newProvider; - newerDmbTcs.SetResult(nextDmbProvider); + + // Oh god dammit + var temp = newerDmbTcs; newerDmbTcs = new TaskCompletionSource(); + temp.SetResult(nextDmbProvider); } } diff --git a/src/Tgstation.Server.Host/Components/Deployment/DreamMaker.cs b/src/Tgstation.Server.Host/Components/Deployment/DreamMaker.cs index e35cfd5b15..3a2eb928a0 100644 --- a/src/Tgstation.Server.Host/Components/Deployment/DreamMaker.cs +++ b/src/Tgstation.Server.Host/Components/Deployment/DreamMaker.cs @@ -12,6 +12,7 @@ using Tgstation.Server.Api.Models; using Tgstation.Server.Api.Models.Internal; using Tgstation.Server.Host.Components.Byond; using Tgstation.Server.Host.Components.Chat; +using Tgstation.Server.Host.Components.Events; using Tgstation.Server.Host.Components.Repository; using Tgstation.Server.Host.Components.Session; using Tgstation.Server.Host.Core; @@ -585,7 +586,7 @@ namespace Tgstation.Server.Host.Components.Deployment } catch { - repo.Dispose(); + repo?.Dispose(); throw; } }) diff --git a/src/Tgstation.Server.Host/Components/EventConsumer.cs b/src/Tgstation.Server.Host/Components/Events/EventConsumer.cs similarity index 97% rename from src/Tgstation.Server.Host/Components/EventConsumer.cs rename to src/Tgstation.Server.Host/Components/Events/EventConsumer.cs index 1cebdd1d2b..45cb18f0a0 100644 --- a/src/Tgstation.Server.Host/Components/EventConsumer.cs +++ b/src/Tgstation.Server.Host/Components/Events/EventConsumer.cs @@ -5,7 +5,7 @@ using System.Threading.Tasks; using Tgstation.Server.Host.Components.StaticFiles; using Tgstation.Server.Host.Components.Watchdog; -namespace Tgstation.Server.Host.Components +namespace Tgstation.Server.Host.Components.Events { /// sealed class EventConsumer : IEventConsumer diff --git a/src/Tgstation.Server.Host/Components/Events/EventScriptAttribute.cs b/src/Tgstation.Server.Host/Components/Events/EventScriptAttribute.cs new file mode 100644 index 0000000000..c42a4b2a6b --- /dev/null +++ b/src/Tgstation.Server.Host/Components/Events/EventScriptAttribute.cs @@ -0,0 +1,25 @@ +using System; + +namespace Tgstation.Server.Host.Components.Events +{ + /// + /// Attribute for indicating the script that a given runs. + /// + [AttributeUsage(AttributeTargets.Field, AllowMultiple = false)] + sealed class EventScriptAttribute : Attribute + { + /// + /// The name of the script the event script the runs. + /// + public string ScriptName { get; } + + /// + /// Initializes a new instance of the . + /// + /// The value of . + public EventScriptAttribute(string scriptName) + { + ScriptName = scriptName ?? throw new ArgumentNullException(nameof(scriptName)); + } + } +} diff --git a/src/Tgstation.Server.Host/Components/EventType.cs b/src/Tgstation.Server.Host/Components/Events/EventType.cs similarity index 73% rename from src/Tgstation.Server.Host/Components/EventType.cs rename to src/Tgstation.Server.Host/Components/Events/EventType.cs index 2f57373b32..419fb9ec3b 100644 --- a/src/Tgstation.Server.Host/Components/EventType.cs +++ b/src/Tgstation.Server.Host/Components/Events/EventType.cs @@ -1,4 +1,4 @@ -namespace Tgstation.Server.Host.Components +namespace Tgstation.Server.Host.Components.Events { /// /// Types of events. Mirror in tgs.dm @@ -8,86 +8,103 @@ /// /// Parameters: Reference name, commit sha /// + [EventScript("RepoResetOrigin")] RepoResetOrigin, /// /// Parameters: Checkout target /// + [EventScript("RepoCheckout")] RepoCheckout, /// /// No parameters /// + [EventScript("RepoFetch")] RepoFetch, /// /// Parameters: Pull request number, pull request sha, merger message /// + [EventScript("RepoMergePullRequest")] RepoMergePullRequest, /// /// Parameters: Absolute path to repository root /// + [EventScript("PreSynchronize")] RepoPreSynchronize, /// /// Parameters: Version being installed /// + [EventScript("ByondInstallStart")] ByondInstallStart, /// /// Parameters: Error string /// + [EventScript("ByondInstallFail")] ByondInstallFail, /// /// Parameters: Old active version, new active version /// + [EventScript("ByondActiveVersionChange")] ByondActiveVersionChange, /// /// Parameters: Game directory path, origin commit sha /// + [EventScript("PreCompile")] CompileStart, /// /// No parameters /// + [EventScript("CompileCancelled")] CompileCancelled, /// /// Parameters: Game directory path, "1" if compile succeeded and api validation failed, "0" otherwise /// + [EventScript("CompileFailure")] CompileFailure, /// /// Parameters: Game directory path /// + [EventScript("PostCompile")] CompileComplete, /// /// No parameters /// + [EventScript("InstanceAutoUpdateStart")] InstanceAutoUpdateStart, /// /// Parameters: Base sha, target sha, base reference, target reference /// + [EventScript("RepoMergeConflict")] RepoMergeConflict, /// /// No parameters /// + [EventScript("DeploymentComplete")] DeploymentComplete, /// /// Before the watchdog shutsdown. Not sent for graceful shutdowns. No parameters. /// + [EventScript("WatchdogShutdown")] WatchdogShutdown, /// /// Before the watchdog detaches. No parameters. /// + [EventScript("WatchdogDetach")] WatchdogDetach, } } diff --git a/src/Tgstation.Server.Host/Components/IEventConsumer.cs b/src/Tgstation.Server.Host/Components/Events/IEventConsumer.cs similarity index 93% rename from src/Tgstation.Server.Host/Components/IEventConsumer.cs rename to src/Tgstation.Server.Host/Components/Events/IEventConsumer.cs index f4dd616bd8..6dbf73fe87 100644 --- a/src/Tgstation.Server.Host/Components/IEventConsumer.cs +++ b/src/Tgstation.Server.Host/Components/Events/IEventConsumer.cs @@ -2,7 +2,7 @@ using System.Threading; using System.Threading.Tasks; -namespace Tgstation.Server.Host.Components +namespace Tgstation.Server.Host.Components.Events { /// /// Consumes s and takes the appropriate actions diff --git a/src/Tgstation.Server.Host/Components/Events/README.md b/src/Tgstation.Server.Host/Components/Events/README.md new file mode 100644 index 0000000000..12f8343ddd --- /dev/null +++ b/src/Tgstation.Server.Host/Components/Events/README.md @@ -0,0 +1,8 @@ +# Event System + +Many actions in TGS generate events. They are dispatched via the [IEventConsumer](./IEventConsumer.cs) ([main implementation](./EventConsumer.cs)). From there, it is further sent to the two actual consumers. + +1. The static files system will attempt to run a script with the given [EventScriptAttribute](./EventScriptAttribute)'s `ScriptName`. +1. The watchdog will send a mirror of the event via the DMAPI to be handled by game code. + +With these two endpoints, the behaviour of TGS is highly customizable as server operators can customize the entire process. diff --git a/src/Tgstation.Server.Host/Components/Instance.cs b/src/Tgstation.Server.Host/Components/Instance.cs index 2782ee9934..76d67ce595 100644 --- a/src/Tgstation.Server.Host/Components/Instance.cs +++ b/src/Tgstation.Server.Host/Components/Instance.cs @@ -9,6 +9,7 @@ using Tgstation.Server.Api.Rights; using Tgstation.Server.Host.Components.Byond; using Tgstation.Server.Host.Components.Chat; 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; @@ -150,6 +151,7 @@ namespace Tgstation.Server.Host.Components #pragma warning disable CA1502 // TODO: Decomplexify async Task TimerLoop(uint minutes, CancellationToken cancellationToken) { + logger.LogTrace("Entering auto-update loop"); while (true) try { @@ -233,7 +235,7 @@ namespace Tgstation.Server.Host.Components .AsQueryable() .Where(x => x.CommitSha == startSha && x.Instance.Id == metadata.Id) .Include(x => x.ActiveTestMerges).ThenInclude(x => x.TestMerge) - .FirstOrDefaultAsync(cancellationToken); + .FirstOrDefaultAsync(jobCancellationToken); async Task UpdateRevInfo(string currentHead, bool onOrigin) { @@ -300,11 +302,12 @@ namespace Tgstation.Server.Host.Components var currentHead = repo.Head; currentRevInfo = await databaseContext.RevisionInformations - .AsQueryable() - .Where(x => x.CommitSha == currentHead && x.Instance.Id == metadata.Id) - .FirstOrDefaultAsync(jobCancellationToken).ConfigureAwait(false); + .AsQueryable() + .Where(x => x.CommitSha == currentHead && x.Instance.Id == metadata.Id) + .FirstOrDefaultAsync(jobCancellationToken) + .ConfigureAwait(false); - if (currentHead != startSha && currentRevInfo != default) + if (currentHead != startSha && currentRevInfo == default) await UpdateRevInfo(currentHead, true).ConfigureAwait(false); shouldSyncTracked = true; @@ -324,7 +327,7 @@ namespace Tgstation.Server.Host.Components if (hasDbChanges) try { - await databaseContext.Save(cancellationToken).ConfigureAwait(false); + await databaseContext.Save(jobCancellationToken).ConfigureAwait(false); } catch { @@ -367,7 +370,7 @@ namespace Tgstation.Server.Host.Components DreamMaker.DeploymentProcess, cancellationToken).ConfigureAwait(false); - await jobManager.WaitForJobCompletion(compileProcessJob, user, cancellationToken, default).ConfigureAwait(false); + await jobManager.WaitForJobCompletion(compileProcessJob, user, 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 23aa1da429..844aa5c264 100644 --- a/src/Tgstation.Server.Host/Components/InstanceFactory.cs +++ b/src/Tgstation.Server.Host/Components/InstanceFactory.cs @@ -7,6 +7,7 @@ using Tgstation.Server.Host.Components.Byond; using Tgstation.Server.Host.Components.Chat; using Tgstation.Server.Host.Components.Chat.Commands; using Tgstation.Server.Host.Components.Deployment; +using Tgstation.Server.Host.Components.Events; using Tgstation.Server.Host.Components.Interop.Bridge; using Tgstation.Server.Host.Components.Repository; using Tgstation.Server.Host.Components.Session; diff --git a/src/Tgstation.Server.Host/Components/Interop/Topic/EventNotification.cs b/src/Tgstation.Server.Host/Components/Interop/Topic/EventNotification.cs index e9183f5787..b6024c58a3 100644 --- a/src/Tgstation.Server.Host/Components/Interop/Topic/EventNotification.cs +++ b/src/Tgstation.Server.Host/Components/Interop/Topic/EventNotification.cs @@ -1,5 +1,6 @@ using System.Collections.Generic; using System.Linq; +using Tgstation.Server.Host.Components.Events; namespace Tgstation.Server.Host.Components.Interop.Topic { diff --git a/src/Tgstation.Server.Host/Components/README.md b/src/Tgstation.Server.Host/Components/README.md index 6d48a22aae..ff311c3f00 100644 --- a/src/Tgstation.Server.Host/Components/README.md +++ b/src/Tgstation.Server.Host/Components/README.md @@ -10,6 +10,7 @@ Component code is where the magic and tears of TGS are made. There are six main There exist two more namespaces in here that don't directly fit in these 6 components. +- [Events](./Events) deals with the TGS event system. - [Interop](./Interop) deals with the bulk of DMAPI communication (Though it's not all contained here). - [Session](./Session) contains the classes used for actually executing DreamDaemon, sending topic requests, receiving bridge requests, among other things. @@ -19,4 +20,6 @@ While the database represents stored instance data, in component code an instanc `IInstance`s are created via the [IInstanceFactory](./IInstanceFactory.cs) ([implementation](./InstanceFactory.cs)) and are generally controlled via the [IInstanceManager](./IInstanceManager.cs) ([implementation](./InstanceManager.cs)). -Many classes in here implement [IHostedService](), `InstanceManager` being the only one that is called by the ASP.NET runtime. In the case of instances `StartAsync()` is called when an `Instance` is being brought online (from server startup or user request). The `Instance` handles calling `StartAsync()` on its various subcomponents that need it. When an `Instance` is being brought offline (from server shutdown/restart/update or user request) the same pattern is followed calling `StopAsync()`. +Many classes in here implement [IHostedService](https://docs.microsoft.com/en-us/aspnet/core/fundamentals/host/hosted-services?view=aspnetcore-3.1&tabs=visual-studio), `InstanceManager` being the only one that is called by the ASP.NET runtime. In the case of instances `StartAsync()` is called when an `Instance` is being brought online (from server startup or user request). The `Instance` handles calling `StartAsync()` on its various subcomponents that need it. When an `Instance` is being brought offline (from server shutdown/restart/update or user request) the same pattern is followed calling `StopAsync()`. + +`IInstanceManager` is the sole point where the controllers talk to component code. It also dispatches bridge requests to their relevant instances. diff --git a/src/Tgstation.Server.Host/Components/Repository/Repository.cs b/src/Tgstation.Server.Host/Components/Repository/Repository.cs index 8afac9fe04..35a8928d4c 100644 --- a/src/Tgstation.Server.Host/Components/Repository/Repository.cs +++ b/src/Tgstation.Server.Host/Components/Repository/Repository.cs @@ -8,6 +8,7 @@ using System.Linq; using System.Threading; using System.Threading.Tasks; using Tgstation.Server.Api.Models; +using Tgstation.Server.Host.Components.Events; using Tgstation.Server.Host.IO; using Tgstation.Server.Host.Jobs; diff --git a/src/Tgstation.Server.Host/Components/Repository/RepositoryManager.cs b/src/Tgstation.Server.Host/Components/Repository/RepositoryManager.cs index 6b6c1b51b8..4c5fefc314 100644 --- a/src/Tgstation.Server.Host/Components/Repository/RepositoryManager.cs +++ b/src/Tgstation.Server.Host/Components/Repository/RepositoryManager.cs @@ -4,6 +4,7 @@ using System; using System.Threading; using System.Threading.Tasks; using Tgstation.Server.Api.Models; +using Tgstation.Server.Host.Components.Events; using Tgstation.Server.Host.Core; using Tgstation.Server.Host.IO; using Tgstation.Server.Host.Jobs; diff --git a/src/Tgstation.Server.Host/Components/Session/README.md b/src/Tgstation.Server.Host/Components/Session/README.md new file mode 100644 index 0000000000..cf6b8f3516 --- /dev/null +++ b/src/Tgstation.Server.Host/Components/Session/README.md @@ -0,0 +1,14 @@ +# Session Management + +"Session" refers to a single invocation of DreamDaemon. The code in here is meant for creating and managing sessions. This is different from the watchdog as it is more low level. + +[ISessionController](./ISessionController.cs) ([implementation](SessionController.cs)) is the representation of DreamDaemon. It handles most of the DMAPI interop, such as DMAPI validation, and raising events for things such as the world rebooting, or the process ending. These are created by the [ISessionControllerFactory](./ISessionControllerFactory.cs) ([implementation](./SessionControllerFactory.cs)) which handles actually launching DreamDaemon and linking together all the components it needs. It also runs some sanity checks, such as if the port requested is avaiable or if the BYOND pager is running (which can cause issues). Sessions give out a [LaunchResult](./LaunchResult.cs) which indicates how long it took for DreamDaemon to become responsive or if it exited before it did. + +Sessions can be detached and reattached. When a session is detached, it generates [ReattachInformation](./ReattachInformation.cs). This is stored in and the database as [DualReattachInformation](./DualReattachInformation.cs) by passing it into and out of [IReattachInfoHandler](./IReattachInfoHandler.cs) ([implementation](./ReattachInfoHandler.cs)). This data can be passed back into the `ISessionControllerFactory` to reattach the session. + +Other classes: + +- [ApiValidationStatus](./ApiValidationStatus.cs) is an indicator of DMAPI validation. +- [CombinedTopicResponse](./CombinedTopicResponse.cs) is a wrapper class around the external [BYOND.TopicSender] library's raw topic response (i.e. string or float + raw bytes) and our internal interop response. +- [DeadSessionController](./DeadSessionController.cs) dummy implementation of `ISessionController`. +- [RebootState](./RebootState.cs) indicates the action that should be taken when the server's world reboots. Just an indicator though, action is take elsewhere. diff --git a/src/Tgstation.Server.Host/Components/Session/SessionController.cs b/src/Tgstation.Server.Host/Components/Session/SessionController.cs index 471cbad030..3d1590cc4e 100644 --- a/src/Tgstation.Server.Host/Components/Session/SessionController.cs +++ b/src/Tgstation.Server.Host/Components/Session/SessionController.cs @@ -241,8 +241,9 @@ namespace Tgstation.Server.Host.Components.Session _ = process.Lifetime.ContinueWith( x => { - if (!disposed) - reattachTopicCts.Cancel(); + lock (synchronizationLock) + if (!disposed) + reattachTopicCts.Cancel(); chatTrackingContext.Active = false; }, TaskScheduler.Current); @@ -280,6 +281,8 @@ namespace Tgstation.Server.Host.Components.Session { if (disposed) return; + disposed = true; + logger.LogTrace("Disposing..."); if (disposing) { if (!released) @@ -290,10 +293,9 @@ namespace Tgstation.Server.Host.Components.Session process.Dispose(); bridgeRegistration.Dispose(); - Dmb?.Dispose(); // will be null when released + reattachInformation.Dmb?.Dispose(); // will be null when released chatTrackingContext.Dispose(); reattachTopicCts.Dispose(); - disposed = true; } else { diff --git a/src/Tgstation.Server.Host/Components/Session/SessionControllerFactory.cs b/src/Tgstation.Server.Host/Components/Session/SessionControllerFactory.cs index e5d61c60d1..9be2d437be 100644 --- a/src/Tgstation.Server.Host/Components/Session/SessionControllerFactory.cs +++ b/src/Tgstation.Server.Host/Components/Session/SessionControllerFactory.cs @@ -5,6 +5,7 @@ using System.Globalization; using System.Linq; using System.Net; using System.Net.Sockets; +using System.Text; using System.Threading; using System.Threading.Tasks; using Tgstation.Server.Api; @@ -233,12 +234,18 @@ namespace Tgstation.Server.Host.Components.Session var visibility = apiValidate ? "invisible" : "public"; // important to run on all ports to allow port changing - var arguments = String.Format(CultureInfo.InvariantCulture, "{0} -port {1} -ports 1-65535 {2}-close -{3} -{4} -public -params \"{5}\"", + Guid? logFileGuid = null; + var arguments = String.Format( + CultureInfo.InvariantCulture, + "{0} -port {1} -ports 1-65535 {2}-close -{3} -{4}{5} -public -params \"{6}\"", dmbProvider.DmbName, portToUse, launchParameters.AllowWebClient.Value ? "-webclient " : String.Empty, SecurityWord(launchParameters.SecurityLevel.Value), visibility, + platformIdentifier.IsWindows + ? $" -log {logFileGuid = Guid.NewGuid()}" + : String.Empty, // Just use stdout on linux parameters); // See https://github.com/tgstation/tgstation-server/issues/719 @@ -253,16 +260,52 @@ namespace Tgstation.Server.Host.Components.Session noShellExecute, noShellExecute: noShellExecute); - if (noShellExecute) + async Task GetDDOutput() { - // Log DD output - _ = process.Lifetime.ContinueWith( - x => logger.LogTrace( - "DreamDaemon Output:{0}{1}", - Environment.NewLine, process.GetCombinedOutput()), - TaskScheduler.Current); + if (!platformIdentifier.IsWindows) + return process.GetCombinedOutput(); + + var logFilePath = ioManager.ConcatPath(basePath, logFileGuid.ToString()); + try + { + var dreamDaemonLogBytes = await ioManager.ReadAllBytes( + logFilePath, + default) + .ConfigureAwait(false); + + return Encoding.UTF8.GetString(dreamDaemonLogBytes); + } + finally + { + try + { + await ioManager.DeleteFile(logFilePath, default).ConfigureAwait(false); + } + catch (Exception ex) + { + logger.LogWarning("Failed to delete DreamDaemon log file {0}: {1}", logFilePath, ex); + } + } } + // 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); + try { networkPromptReaper.RegisterProcess(process); diff --git a/src/Tgstation.Server.Host/Components/StaticFiles/Configuration.cs b/src/Tgstation.Server.Host/Components/StaticFiles/Configuration.cs index bcc44ee87f..a61ba8cea7 100644 --- a/src/Tgstation.Server.Host/Components/StaticFiles/Configuration.cs +++ b/src/Tgstation.Server.Host/Components/StaticFiles/Configuration.cs @@ -9,6 +9,7 @@ using System.Text; using System.Threading; using System.Threading.Tasks; using Tgstation.Server.Api.Models; +using Tgstation.Server.Host.Components.Events; using Tgstation.Server.Host.Core; using Tgstation.Server.Host.IO; using Tgstation.Server.Host.Jobs; @@ -32,12 +33,18 @@ namespace Tgstation.Server.Host.Components.StaticFiles const string CodeModificationsHeadFile = "HeadInclude.dm"; const string CodeModificationsTailFile = "TailInclude.dm"; - static readonly IReadOnlyDictionary EventTypeScriptFileNameMap = new Dictionary - { - { EventType.CompileStart, "PreCompile" }, - { EventType.CompileComplete, "PostCompile" }, - { EventType.RepoPreSynchronize, "PreSynchronize" } - }; + static readonly IReadOnlyDictionary EventTypeScriptFileNameMap = new Dictionary( + Enum.GetValues(typeof(EventType)) + .OfType() + .Select( + eventType => new KeyValuePair( + eventType, + typeof(EventType) + .GetField(eventType.ToString()) + .GetCustomAttributes(false) + .OfType() + .First() + .ScriptName))); /// /// The for diff --git a/src/Tgstation.Server.Host/Components/StaticFiles/IConfiguration.cs b/src/Tgstation.Server.Host/Components/StaticFiles/IConfiguration.cs index ad88be4917..0cd29c0c5c 100644 --- a/src/Tgstation.Server.Host/Components/StaticFiles/IConfiguration.cs +++ b/src/Tgstation.Server.Host/Components/StaticFiles/IConfiguration.cs @@ -4,6 +4,7 @@ using System.Collections.Generic; using System.Threading; using System.Threading.Tasks; using Tgstation.Server.Api.Models; +using Tgstation.Server.Host.Components.Events; using Tgstation.Server.Host.Security; namespace Tgstation.Server.Host.Components.StaticFiles diff --git a/src/Tgstation.Server.Host/Components/Watchdog/BasicWatchdog.cs b/src/Tgstation.Server.Host/Components/Watchdog/BasicWatchdog.cs index e3e8968fd8..1722376728 100644 --- a/src/Tgstation.Server.Host/Components/Watchdog/BasicWatchdog.cs +++ b/src/Tgstation.Server.Host/Components/Watchdog/BasicWatchdog.cs @@ -328,5 +328,23 @@ namespace Tgstation.Server.Host.Components.Watchdog return Restart(true, cancellationToken); } + + /// + /// Send a chat message and log about a failed reattach operation and attempts another call to . + /// + /// If the inactive server was reattached successfully. + /// The for the operation/ + /// A representing the running operation. + async Task NotifyOfFailedReattach(bool inactiveReattachSuccess, CancellationToken cancellationToken) + { + // we lost the server, just restart entirely + DisposeAndNullControllers(); + const string FailReattachMessage = "Unable to properly reattach to server! Restarting..."; + Logger.LogWarning(FailReattachMessage); + Logger.LogDebug(inactiveReattachSuccess ? "Also could not reattach to inactive server!" : "Inactive server was reattached successfully!"); + Task chatTask = Chat.SendWatchdogMessage(FailReattachMessage, false, cancellationToken); + await LaunchNoLock(true, false, null, cancellationToken).ConfigureAwait(false); + await chatTask.ConfigureAwait(false); + } } } diff --git a/src/Tgstation.Server.Host/Components/Watchdog/ExperimentalWatchdog.cs b/src/Tgstation.Server.Host/Components/Watchdog/ExperimentalWatchdog.cs index 6547ef6d62..07c73f7376 100644 --- a/src/Tgstation.Server.Host/Components/Watchdog/ExperimentalWatchdog.cs +++ b/src/Tgstation.Server.Host/Components/Watchdog/ExperimentalWatchdog.cs @@ -512,7 +512,7 @@ namespace Tgstation.Server.Host.Components.Watchdog Logger.LogDebug(bothServersDead ? "Also could not reattach to inactive server!" : "Inactive server was reattached successfully!"); chatTask = Chat.SendWatchdogMessage(FailReattachMessage, false, cancellationToken); callBeforeRecurse(); - await LaunchImplNoLock(true, false, null, cancellationToken).ConfigureAwait(false); + await LaunchNoLock(true, false, null, cancellationToken).ConfigureAwait(false); await chatTask.ConfigureAwait(false); return; } diff --git a/src/Tgstation.Server.Host/Components/Watchdog/IWatchdog.cs b/src/Tgstation.Server.Host/Components/Watchdog/IWatchdog.cs index 0f86a510f5..536096c499 100644 --- a/src/Tgstation.Server.Host/Components/Watchdog/IWatchdog.cs +++ b/src/Tgstation.Server.Host/Components/Watchdog/IWatchdog.cs @@ -3,6 +3,7 @@ using System; using System.Threading; using System.Threading.Tasks; using Tgstation.Server.Api.Models.Internal; +using Tgstation.Server.Host.Components.Events; using Tgstation.Server.Host.Components.Session; namespace Tgstation.Server.Host.Components.Watchdog diff --git a/src/Tgstation.Server.Host/Components/Watchdog/README.md b/src/Tgstation.Server.Host/Components/Watchdog/README.md new file mode 100644 index 0000000000..8927a46a83 --- /dev/null +++ b/src/Tgstation.Server.Host/Components/Watchdog/README.md @@ -0,0 +1,52 @@ +# The Watchdog + +The watchdog is what keeps DreamDaemon alive and well. It's responsible for starting, stopping, rebooting it, sending TGS events to the DMAPI, running heartbeats, and applying patches. + +There are three main implementations of the interface [IWatchdog](./IWatchdog.cs) that deal with how it handles the last of those jobs. + +- The [BasicWatchdog](./BasicWatchdog.cs) kills DreamDaemon when the world reboots and immediately restarts it with the newly compiled .dmb. +- The [WindowsWatchdog](./WindowsWatchdog.cs) uses a quirk of the interaction of DD with the Windows file system. A symlink to the game folder is created. From here, we launch the game. When a new .dmb is available, this symlink is deleted and then immediately recreated pointing to the new game directory. When DreamDaemon reboots it will load the new game. +- The [ExperimentalWatchdog](./ExperimentalWatchdog) starts new servers when the new .dmb comes in. But it uses some, rather invasive, port trickery to reroute traffic from the old game server to the new one. The idea for this is to always have two servers running so one can act as a fallback for if the main one fails. This is currently a big work-in-progress as it has issues with BYOND bugs and static file sharing. + +[WatchdogBase](./WatchdogBase.cs) is the parent of all these implementations and contains the bulk of the monitoring code. More on that later. + +`IWatchdog`s are created via the [IWatchdogFactory](./IWatchdogFactory.cs) interface. The two implementations, [WatchdogFactory](./WatchdogFactory.cs) and [WindowsWatchdogFactory](./WindowsWatchdogFactory.cs), differ in that the latter creates `WindowsWatchdog`s as opposed to `BasicWatchdog`s. + +## Startup + +From the perspective of `WatchdogBase` + +- When an instance is onlined, `StartAsync()` is called on `WatchdogBase`. If the instance is configured to autostart the server, or if reattach information is available from the `IReattachInfoHandler` the watchdog will be launched here. Otherwise, it waits to be activated from the controller. +- We first do some sanity checks such as if the watchdog is already running and if the `IDmbFactory` has a IDmbProvider for us. +- We send out the annoucement of launch/reattach to the chat system. +- The `ActiveLaunchParameters` are made the `LastLaunchParameters` for reference. +- `InitControllers()` is called, which abstractly creates the `ISessionController`(s) for the watchdog. + - For the `BasicWatchdog`, the controller is created normally. + - For the `WindowsWatchdog`, we replace the `IDmbProvider` with a `WindowsSwappableDmbProvider` and setup the symlinking before deferring to the `BasicWatchdog` + - For the `ExperimentalWatchdog` we launch two servers in tandem. + - For reattaching, all three work similarly in that they use `ISessionControllerFactory` to reattach their sessions. The `ExperimentalWatchdog` will insert a `DeadSessionController` in place of one if only one server could not reattach. +- `MonitorLifetimes()` is called which is the "main loop" of the watchdog. + +## Monitoring + +Watchdog monitoring takes place in the `MonitorLifetimes()` functiona and is entirely event based. It responds to a set of [MonitorActivationReason](./MonitorActivationReason.cs)s and takes the apporprate action. This includes: +- When a server crashes. +- When a server calls `/world/prod/Reboot()`. +- When a new .dmb is deployed. +- When DreamDaemon settings are changed via the API. +- When it is time to make a heartbeat. + +Here's how this works. + +- A [MonitorState](./MonitorState.cs) object is created +- `Task`s are created for all possible activation reasons and an asynchrounous wait is made on them. +- Each activation reason is processed in a particular order via a call to `HandleMonitorWakeup()`. This function is allowed to mutate `MonitorState`. + - How each activation reason is handled depends on the watchdog type. For example, the `BasicWatchdog` will queue a graceful reboot when a new .dmb becomes available, wheras the `WindowsWatchdog` will immediately swap the symlink to it. + - Some actions are universal. i.e. if the server crashes, a reboot will occur. Or the server will shutdown on reboot if a graceful stop was queued. + - If the state's [MonitorAction](./MonitorAction.cs) changes, what happens after each activation may change + +This continues until `StopMonitor` is called, which triggers the `CancellationToken` in `MonitorLifetimes()` and terminates all `ISessionController`s. + +## Detaching + +`WatchdogBase` maintains a `IRestartRegistration` with TGS, meaning that, when TGS is rebooted, `HandleRestart()` will be called before `StopAsync()`. This has the effect of adjusting what happens when the monitor exits. Instead of terminating the server(s) it will instead call `ISessionController.Release()` on them. This generates the `ReattachInformation` which will be saved to the database in `StopAsync()`. diff --git a/src/Tgstation.Server.Host/Components/Watchdog/WatchdogBase.cs b/src/Tgstation.Server.Host/Components/Watchdog/WatchdogBase.cs index d6eafdef32..d2400a8170 100644 --- a/src/Tgstation.Server.Host/Components/Watchdog/WatchdogBase.cs +++ b/src/Tgstation.Server.Host/Components/Watchdog/WatchdogBase.cs @@ -11,6 +11,7 @@ using Tgstation.Server.Api.Models.Internal; using Tgstation.Server.Api.Rights; using Tgstation.Server.Host.Components.Chat; using Tgstation.Server.Host.Components.Deployment; +using Tgstation.Server.Host.Components.Events; using Tgstation.Server.Host.Components.Interop.Topic; using Tgstation.Server.Host.Components.Session; using Tgstation.Server.Host.Core; @@ -118,7 +119,7 @@ namespace Tgstation.Server.Host.Components.Watchdog readonly object controllerDisposeLock; /// - /// If the should in + /// If the should in /// readonly bool autoStart; @@ -331,14 +332,14 @@ namespace Tgstation.Server.Host.Components.Watchdog /// to use, if any /// The for the operation /// A representing the running operation - protected async Task LaunchImplNoLock(bool startMonitor, bool announce, DualReattachInformation reattachInfo, CancellationToken cancellationToken) + protected async Task LaunchNoLock(bool startMonitor, bool announce, DualReattachInformation reattachInfo, CancellationToken cancellationToken) { Logger.LogTrace("Begin LaunchImplNoLock"); if (Running) throw new JobException(ErrorCode.WatchdogRunning); - if (!DmbFactory.DmbAvailable) + if (reattachInfo == null && !DmbFactory.DmbAvailable) throw new JobException(ErrorCode.WatchdogCompileJobCorrupted); // this is necessary, the monitor could be in it's sleep loop trying to restart, if so cancel THAT monitor and start our own with blackjack and hookers @@ -355,10 +356,13 @@ namespace Tgstation.Server.Host.Components.Watchdog heartbeatsMissed = 0; // for when we call ourself and want to not catch thrown exceptions - var ignoreNestedException = false; + var recursiveCallToHappen = false; try { - await InitControllers(() => ignoreNestedException = true, chatTask, reattachInfo, cancellationToken).ConfigureAwait(false); + await InitControllers(() => recursiveCallToHappen = true, chatTask, reattachInfo, cancellationToken).ConfigureAwait(false); + if (recursiveCallToHappen) + return; + await chatTask.ConfigureAwait(false); Logger.LogInformation("Launched servers successfully"); @@ -366,13 +370,14 @@ namespace Tgstation.Server.Host.Components.Watchdog if (startMonitor) { - StartMonitor(); + monitorCts = new CancellationTokenSource(); + monitorTask = MonitorLifetimes(monitorCts.Token); } } catch (Exception e) { // don't try to send chat tasks or warning logs if were suppressing exceptions or cancelled - if (!ignoreNestedException && !cancellationToken.IsCancellationRequested) + if (!recursiveCallToHappen && !cancellationToken.IsCancellationRequested) { var originalChatTask = chatTask; async Task ChainChatTaskWithErrorMessage() @@ -398,15 +403,6 @@ namespace Tgstation.Server.Host.Components.Watchdog } } - /// - /// Call and setup and . - /// - protected void StartMonitor() - { - monitorCts = new CancellationTokenSource(); - monitorTask = MonitorLifetimes(monitorCts.Token); - } - /// /// Stops . Doesn't kill the servers /// @@ -426,24 +422,6 @@ namespace Tgstation.Server.Host.Components.Watchdog return wasRunning; } - /// - /// Send a chat message and log about a failed reattach operation and attempts another call to . - /// - /// If the inactive server was reattached successfully. - /// The for the operation/ - /// A representing the running operation. - protected async Task NotifyOfFailedReattach(bool inactiveReattachSuccess, CancellationToken cancellationToken) - { - // we lost the server, just restart entirely - DisposeAndNullControllers(); - const string FailReattachMessage = "Unable to properly reattach to server! Restarting..."; - Logger.LogWarning(FailReattachMessage); - Logger.LogDebug(inactiveReattachSuccess ? "Also could not reattach to inactive server!" : "Inactive server was reattached successfully!"); - Task chatTask = Chat.SendWatchdogMessage(FailReattachMessage, false, cancellationToken); - await LaunchImplNoLock(true, false, null, cancellationToken).ConfigureAwait(false); - await chatTask.ConfigureAwait(false); - } - /// /// Check the of a given for errors and throw a if any are detected. /// @@ -476,6 +454,7 @@ namespace Tgstation.Server.Host.Components.Watchdog /// protected void DisposeAndNullControllers() { + Logger.LogTrace("DisposeAndNullControllers"); lock (controllerDisposeLock) DisposeAndNullControllersImpl(); } @@ -512,6 +491,11 @@ namespace Tgstation.Server.Host.Components.Watchdog MonitorState monitorState, CancellationToken cancellationToken); + /// + /// Attempt to restart the monitor from scratch. + /// + /// The for the operation. + /// A resulting in the new . private async Task MonitorRestart(CancellationToken cancellationToken) { Logger.LogTrace("Monitor restart!"); @@ -525,7 +509,7 @@ namespace Tgstation.Server.Host.Components.Watchdog try { // use LaunchImplNoLock without announcements or restarting the monitor - await LaunchImplNoLock(false, false, null, cancellationToken).ConfigureAwait(false); + await LaunchNoLock(false, false, null, cancellationToken).ConfigureAwait(false); if (Running) { Logger.LogDebug("Relaunch successful, resetting monitor state..."); @@ -567,7 +551,7 @@ namespace Tgstation.Server.Host.Components.Watchdog } /// - /// The loop that watches the watchdog. + /// The main loop of the watchdog. Ayschronously waits for events to occur and then responds to them. /// /// The for the operation. /// A representing the running operation. @@ -606,35 +590,32 @@ namespace Tgstation.Server.Host.Components.Watchdog // cancel waiting if requested var cancelTcs = new TaskCompletionSource(); + var toWaitOn = Task.WhenAny( + activeServerLifetime, + activeServerReboot, + inactiveServerLifetime, + inactiveServerReboot, + inactiveStartupComplete, + heartbeat, + newDmbAvailable, + cancelTcs.Task, + activeLaunchParametersChanged); + + // wait for something to happen using (cancellationToken.Register(() => cancelTcs.SetCanceled())) - { - var toWaitOn = Task.WhenAny( - activeServerLifetime, - activeServerReboot, - inactiveServerLifetime, - inactiveServerReboot, - inactiveStartupComplete, - heartbeat, - newDmbAvailable, - cancelTcs.Task, - activeLaunchParametersChanged); - - // wait for something to happen await toWaitOn.ConfigureAwait(false); - cancellationToken.ThrowIfCancellationRequested(); - } + cancellationToken.ThrowIfCancellationRequested(); Logger.LogTrace("Monitor activated"); + // always run HandleMonitorWakeup from the context of the semaphore lock using (await SemaphoreSlimContext.Lock(Semaphore, cancellationToken).ConfigureAwait(false)) { - // always run HandleMonitorWakeup from the context of the semaphore lock // multiple things may have happened, handle them one at a time for (var moreActivationsToProcess = true; moreActivationsToProcess && (monitorState.NextAction == MonitorAction.Continue || monitorState.NextAction == MonitorAction.Skip);) { MonitorActivationReason activationReason = default; // this will always be assigned before being used - // process the tasks in this order and call HandlerMonitorWakup for each bool CheckActivationReason(ref Task task, MonitorActivationReason testActivationReason) { var taskCompleted = task?.IsCompleted == true; @@ -650,6 +631,7 @@ namespace Tgstation.Server.Host.Components.Watchdog return false; } + // process the tasks in this order and call HandlerMonitorWakup for each depending on the new monitorState var anyActivation = CheckActivationReason(ref activeServerLifetime, MonitorActivationReason.ActiveServerCrashed) || CheckActivationReason(ref activeServerReboot, MonitorActivationReason.ActiveServerRebooted) || CheckActivationReason(ref newDmbAvailable, MonitorActivationReason.NewDmbAvailable) @@ -680,11 +662,14 @@ namespace Tgstation.Server.Host.Components.Watchdog } Logger.LogTrace("Next monitor action is to {0}", monitorState.NextAction); + + // Restart if requested if (monitorState.NextAction == MonitorAction.Restart) monitorState = await MonitorRestart(cancellationToken).ConfigureAwait(false); } catch (OperationCanceledException) { + // let this bubble, other exceptions caught below throw; } catch (Exception e) @@ -711,17 +696,18 @@ namespace Tgstation.Server.Host.Components.Watchdog await chatTask.ConfigureAwait(false); } - } - catch (OperationCanceledException) - { - Logger.LogDebug("Monitor cancelled"); + } + catch (OperationCanceledException) + { + // stop signal + Logger.LogDebug("Monitor cancelled"); - if (releaseServers) - { - Logger.LogTrace("Detaching servers..."); - releasedReattachInformation = CreateReattachInformation(); - } + if (releaseServers) + { + Logger.LogTrace("Detaching servers..."); + releasedReattachInformation = CreateReattachInformation(); } + } DisposeAndNullControllers(); @@ -731,7 +717,7 @@ namespace Tgstation.Server.Host.Components.Watchdog /// /// Starts all s. /// - /// An that must be run before making a recursive call to . + /// An that must be run before making a recursive call to . /// A, possibly active, for an outgoing chat message. /// to use, if any /// The for the operation @@ -743,11 +729,13 @@ namespace Tgstation.Server.Host.Components.Watchdog { using (await SemaphoreSlimContext.Lock(Semaphore, cancellationToken).ConfigureAwait(false)) { - if (launchParameters.Match(ActiveLaunchParameters)) - return; + bool match = launchParameters.CanApplyWithoutReboot(ActiveLaunchParameters); ActiveLaunchParameters = launchParameters; - if (Running) - ActiveParametersUpdated.TrySetResult(null); // queue an update + if (match || !Running) + return; + + ActiveParametersUpdated.TrySetResult(null); // queue an update + ActiveParametersUpdated = new TaskCompletionSource(); } } @@ -815,7 +803,7 @@ namespace Tgstation.Server.Host.Components.Watchdog public async Task Launch(CancellationToken cancellationToken) { using (await SemaphoreSlimContext.Lock(Semaphore, cancellationToken).ConfigureAwait(false)) - await LaunchImplNoLock(true, true, null, cancellationToken).ConfigureAwait(false); + await LaunchNoLock(true, true, null, cancellationToken).ConfigureAwait(false); } /// @@ -834,10 +822,13 @@ namespace Tgstation.Server.Host.Components.Watchdog /// public async Task Restart(bool graceful, CancellationToken cancellationToken) { + if (!Running) + throw new JobException(ErrorCode.WatchdogRunning); + Logger.LogTrace("Begin Restart. Graceful: {0}", graceful); using (await SemaphoreSlimContext.Lock(Semaphore, cancellationToken).ConfigureAwait(false)) { - if (!graceful || !Running) + if (!graceful) { Task chatTask; if (Running) @@ -847,16 +838,14 @@ namespace Tgstation.Server.Host.Components.Watchdog } else chatTask = Task.CompletedTask; - await LaunchImplNoLock(true, !Running, null, cancellationToken).ConfigureAwait(false); + await LaunchNoLock(true, !Running, null, cancellationToken).ConfigureAwait(false); await chatTask.ConfigureAwait(false); } var toReboot = GetActiveController(); - if (toReboot != null) - { - if (!await toReboot.SetRebootState(Session.RebootState.Restart, cancellationToken).ConfigureAwait(false)) - Logger.LogWarning("Unable to send reboot state change event!"); - } + if (toReboot != null + && !await toReboot.SetRebootState(Session.RebootState.Restart, cancellationToken).ConfigureAwait(false)) + Logger.LogWarning("Unable to send reboot state change event!"); } } @@ -895,21 +884,20 @@ namespace Tgstation.Server.Host.Components.Watchdog await jobManager.RegisterOperation(job, async (j, databaseContextFactory, progressFunction, ct) => { using (await SemaphoreSlimContext.Lock(Semaphore, ct).ConfigureAwait(false)) - await LaunchImplNoLock(true, true, reattachInfo, ct).ConfigureAwait(false); + await LaunchNoLock(true, true, reattachInfo, ct).ConfigureAwait(false); }, cancellationToken).ConfigureAwait(false); } /// public async Task StopAsync(CancellationToken cancellationToken) { - if (releaseServers) - { - await StopMonitor().ConfigureAwait(false); - if (releasedReattachInformation != null) - await reattachInfoHandler.Save(releasedReattachInformation, cancellationToken).ConfigureAwait(false); - } - await TerminateNoLock(false, !releaseServers, cancellationToken).ConfigureAwait(false); + if (releasedReattachInformation != null) + { + await reattachInfoHandler.Save(releasedReattachInformation, cancellationToken).ConfigureAwait(false); + releasedReattachInformation = null; + releaseServers = false; + } } /// diff --git a/src/Tgstation.Server.Host/Components/Watchdog/WindowsWatchdog.cs b/src/Tgstation.Server.Host/Components/Watchdog/WindowsWatchdog.cs index 730ced06b9..473b3f36cc 100644 --- a/src/Tgstation.Server.Host/Components/Watchdog/WindowsWatchdog.cs +++ b/src/Tgstation.Server.Host/Components/Watchdog/WindowsWatchdog.cs @@ -117,8 +117,6 @@ namespace Tgstation.Server.Host.Components.Watchdog /// protected override MonitorAction HandleNormalReboot() { - if (activeSwappable == null) - throw new InvalidOperationException("Expected activeSwappable to not be null!"); if (pendingSwappable != null) { Logger.LogTrace("Replacing activeSwappable with pendingSwappable..."); diff --git a/src/Tgstation.Server.Host/Core/Application.cs b/src/Tgstation.Server.Host/Core/Application.cs index c7ed253c9c..7f486eeb43 100644 --- a/src/Tgstation.Server.Host/Core/Application.cs +++ b/src/Tgstation.Server.Host/Core/Application.cs @@ -357,8 +357,6 @@ namespace Tgstation.Server.Host.Core // Final point where we wrap exceptions in a 500 (ErrorMessage) response applicationBuilder.UseServerErrorHandling(); - applicationBuilder.UseRequestCounting(); - // 503 requests made while the application is starting applicationBuilder.UseAsyncInitialization(async (cancellationToken) => { diff --git a/src/Tgstation.Server.Host/Extensions/ApplicationBuilderExtensions.cs b/src/Tgstation.Server.Host/Extensions/ApplicationBuilderExtensions.cs index 2fe00098db..f155aff190 100644 --- a/src/Tgstation.Server.Host/Extensions/ApplicationBuilderExtensions.cs +++ b/src/Tgstation.Server.Host/Extensions/ApplicationBuilderExtensions.cs @@ -107,32 +107,5 @@ namespace Tgstation.Server.Host.Extensions } }); } - - /// - /// Add middleware for logging the request number. - /// - /// The to configure. - public static void UseRequestCounting(this IApplicationBuilder applicationBuilder) - { - if (applicationBuilder == null) - throw new ArgumentNullException(nameof(applicationBuilder)); - - ulong requestCounter = 0; - - applicationBuilder.Use(async (context, next) => - { - var logger = GetLogger(context); - var requestNumber = ++requestCounter; - logger.LogTrace("Starting request #{0}...", requestNumber); - try - { - await next().ConfigureAwait(false); - } - finally - { - logger.LogTrace("Finished request #{0}", requestNumber); - } - }); - } } } diff --git a/src/Tgstation.Server.Host/Jobs/README.md b/src/Tgstation.Server.Host/Jobs/README.md index 48ec91bddd..53bf47f8fc 100644 --- a/src/Tgstation.Server.Host/Jobs/README.md +++ b/src/Tgstation.Server.Host/Jobs/README.md @@ -1,5 +1,5 @@ # Jobs Subsystem - [IJobManager](./IJobManager.cs) and [implementation](./JobManager.cs) is where the bulk of the magic happens. The `RegisterOperation()` call is what takes a work unit and sets it to run asynchronously while being tracked through the API. -- [JobException] is a special .NET Exception implementation that is able to carry API `ErrorCode`s and other additional data. +- [JobException](./JobException.cs) is a special .NET Exception implementation that is able to carry API `ErrorCode`s and other additional data. - [JobHandler](./JobHandler.cs) carries the [CancellationTokenSource](https://stackoverflow.com/questions/20638952/cancellationtoken-and-cancellationtokensource-how-to-use-it) for a given job in a disposable context. diff --git a/tests/Tgstation.Server.Host.Tests/Components/Events/TestEventScriptAttribute.cs b/tests/Tgstation.Server.Host.Tests/Components/Events/TestEventScriptAttribute.cs new file mode 100644 index 0000000000..6652345fab --- /dev/null +++ b/tests/Tgstation.Server.Host.Tests/Components/Events/TestEventScriptAttribute.cs @@ -0,0 +1,21 @@ +using Microsoft.VisualStudio.TestTools.UnitTesting; +using System; + + +namespace Tgstation.Server.Host.Components.Events.Tests +{ + /// + /// Tests for the . + /// + [TestClass] + public sealed class TestEventScriptAttribute + { + [TestMethod] + public void TestConstruction() + { + Assert.ThrowsException(() => new EventScriptAttribute(null)); + var test = new EventScriptAttribute("test"); + Assert.AreEqual("test", test.ScriptName); + } + } +} diff --git a/tests/Tgstation.Server.Host.Tests/Components/Events/TestEventType.cs b/tests/Tgstation.Server.Host.Tests/Components/Events/TestEventType.cs new file mode 100644 index 0000000000..3e9ed9b8c0 --- /dev/null +++ b/tests/Tgstation.Server.Host.Tests/Components/Events/TestEventType.cs @@ -0,0 +1,28 @@ +using Microsoft.VisualStudio.TestTools.UnitTesting; +using System; +using System.Collections.Generic; +using System.Linq; + +namespace Tgstation.Server.Host.Components.Events.Tests +{ + [TestClass] + public sealed class TestEventType + { + [TestMethod] + public void TestAllEventTypesHaveUniqueEventScriptAttributes() + { + var allScripts = new HashSet(); + foreach (var eventType in Enum.GetValues(typeof(EventType))) { + var list = typeof(EventType) + .GetField(eventType.ToString()) + .GetCustomAttributes(false) + .OfType() + .ToList(); + Assert.AreEqual(1, list.Count, $"EventType: {eventType}"); + + var attribute = list.First(); + Assert.IsTrue(allScripts.Add(attribute.ScriptName), $"Non-unique script Name: {attribute.ScriptName}"); + } + } + } +}