diff --git a/src/Tgstation.Server.Api/Models/ErrorCode.cs b/src/Tgstation.Server.Api/Models/ErrorCode.cs index b3b6167889..ecb96a8c53 100644 --- a/src/Tgstation.Server.Api/Models/ErrorCode.cs +++ b/src/Tgstation.Server.Api/Models/ErrorCode.cs @@ -234,7 +234,7 @@ namespace Tgstation.Server.Api.Models /// /// contained duplicate s. /// - [Description("The same pull request was present more than once in the test merge requests!")] + [Description("The same pull request was present more than once in the test merge requests or is already merged!")] RepoDuplicateTestMerge, /// @@ -303,5 +303,161 @@ namespace Tgstation.Server.Api.Models /// [Description("Set amount of chatChannels exceeds the configured channelLimit!")] ChatBotMaxChannels, + + /// + /// Failed to install DirectX with BYOND. + /// + [Description("Unable to start DirectX installer process! Is the server running with admin privileges?")] + ByondDirectXInstallFail, + + /// + /// Failed to download a given BYOND version. + /// + [Description("Error downloading specified BYOND version!")] + ByondDownloadFail, + + /// + /// Failed to lock BYOND executables. + /// + [Description("Could not acquire lock on BYOND installation as none exist!")] + ByondNoVersionsInstalled, + + /// + /// The DMAPI never validated itself + /// + [Description("DreamDaemon exited without validating the DMAPI@")] + DreamMakerNeverValidated, + + /// + /// The DMAPI sent an invalid validation request. + /// + [Description("The DMAPI sent an invalid validation request!")] + DreamMakerInvalidValidation, + + /// + /// DMAPI validation timeout. + /// + [Description("The DreamDaemon startup timeout was hit before the DMAPI validated!")] + DreamMakerValidationTimeout, + + /// + /// No .dme could be found for deployment. + /// + [Description("No .dme configured and could not automatically detect one!")] + DreamMakerNoDme, + + /// + /// The configured .dme could not be found. + /// + [Description("Could not load configured .dme!")] + DreamMakerMissingDme, + + /// + /// DreamMaker failed to compile. + /// + [Description("DreamMaker exited with a non-zero exit code!")] + DreamMakerExitCode, + + /// + /// Deployment already in progress + /// + [Description("There is already a deployment operation in progress!")] + DreamMakerCompileJobInProgress, + + /// + /// Missing settings in database. + /// + [Description("Could not retrieve DreamDaemon settings from the database!")] + InstanceMissingDreamDaemonSettings, + + /// + /// Missing settings in database. + /// + [Description("Could not retrieve DreamMaker settings from the database!")] + InstanceMissingDreamMakerSettings, + + /// + /// Missing settings in database. + /// + [Description("Could not retrieve Repository settings from the database!")] + InstanceMissingRepositorySettings, + + /// + /// Performing an automatic update with the flag resulted in merge conflicts. + /// + [Description("Performing this automatic update as a merge would result in conficts. Aborting!")] + InstanceUpdateTestMergeConflict, + + /// + /// and are required for this operation. + /// + [Description("Git credentials are required for this operation!")] + RepoCredentialsRequired, + + /// + /// The remote returned an invalid authentication request. + /// + [Description("The remote is requesting authentication, but is not allowing credentials to be received!")] + RepoCannotAuthenticate, + + /// + /// Cannot perform operation while not on a . + /// + [Description("This git operation requires the repository HEAD to currently be on a tracked reference!")] + RepoReferenceRequired, + + /// + /// Attempted to start the watchdog when it was already running. + /// + [Description("The watchdog is already running!")] + WatchdogRunning, + + /// + /// Attempted to start the watchdog with a corrupted . + /// + [Description("Cannot launch with active compile job as it is corrupted!")] + WatchdogCompileJobCorrupted, + + /// + /// DreamDaemon exited before it finished starting. + /// + [Description("DreamDaemon failed to start!")] + WatchdogStartupFailed, + + /// + /// DreamDaemon timed-out before it finished starting. + /// + [Description("DreamDaemon failed to start within the configured timeout!")] + WatchdogStartupTimeout, + + /// + /// Attempted to test merge with an unsupported remote. + /// + [Description("Test merging with the current remote is not supported!")] + RepoUnsupportedTestMergeRemote, + + /// + /// Either or was in one when it should have been the other. + /// + [Description("The value set for checkoutSha or reference should be in the other field!")] + RepoSwappedShaOrReference, + + /// + /// A merge conflict occurred during a git operation. + /// + [Description("A merge conflict occurred while performing the operation!")] + RepoMergeConflict, + + /// + /// The current does not track a remote reference. + /// + [Description("The repository's current reference is unsuitable for this operation as it does not track a remote reference!")] + RepoReferenceNotTracking, + + /// + /// Encounted merge conflicts while test merging. + /// + [Description("Encountered merge conflicts while test merging one or more pull requests!")] + RepoTestMergeConflict, } } \ No newline at end of file diff --git a/src/Tgstation.Server.Api/Models/Internal/Job.cs b/src/Tgstation.Server.Api/Models/Internal/Job.cs index 945d0d6aaa..3e5656e56e 100644 --- a/src/Tgstation.Server.Api/Models/Internal/Job.cs +++ b/src/Tgstation.Server.Api/Models/Internal/Job.cs @@ -15,6 +15,11 @@ namespace Tgstation.Server.Api.Models.Internal [Required] public string Description { get; set; } + /// + /// The associated with the if any. + /// + public ErrorCode? ErrorCode { get; set; } + /// /// Details of any exceptions caught during the /// diff --git a/src/Tgstation.Server.Host/Components/Byond/ByondManager.cs b/src/Tgstation.Server.Host/Components/Byond/ByondManager.cs index 38c57a09d0..1af10cf0e2 100644 --- a/src/Tgstation.Server.Host/Components/Byond/ByondManager.cs +++ b/src/Tgstation.Server.Host/Components/Byond/ByondManager.cs @@ -1,13 +1,13 @@ using Microsoft.Extensions.Logging; using System; using System.Collections.Generic; -using System.Globalization; using System.Linq; using System.Net; using System.Text; using System.Threading; using System.Threading.Tasks; using Tgstation.Server.Api; +using Tgstation.Server.Api.Models; using Tgstation.Server.Host.Core; using Tgstation.Server.Host.IO; using Tgstation.Server.Host.Jobs; @@ -162,7 +162,7 @@ namespace Tgstation.Server.Host.Components.Byond catch (WebException e) { // since the user can easily provide non-exitent version numbers, we'll turn this into a JobException - throw new JobException(String.Format(CultureInfo.InvariantCulture, "Error downloading BYOND version: {0}", e.Message)); + throw new JobException(ErrorCode.ByondDownloadFail, e); } catch (OperationCanceledException) { @@ -207,7 +207,7 @@ namespace Tgstation.Server.Host.Components.Byond { var versionToUse = requiredVersion ?? ActiveVersion; if (versionToUse == null) - throw new JobException("No BYOND versions installed!"); + throw new JobException(ErrorCode.ByondNoVersionsInstalled); await InstallVersion(versionToUse, cancellationToken).ConfigureAwait(false); var versionKey = VersionKey(versionToUse); diff --git a/src/Tgstation.Server.Host/Components/Byond/WindowsByondInstaller.cs b/src/Tgstation.Server.Host/Components/Byond/WindowsByondInstaller.cs index 7f26bfac61..a2ea8589dc 100644 --- a/src/Tgstation.Server.Host/Components/Byond/WindowsByondInstaller.cs +++ b/src/Tgstation.Server.Host/Components/Byond/WindowsByondInstaller.cs @@ -1,9 +1,9 @@ using Microsoft.Extensions.Logging; using System; -using System.Globalization; using System.Text; using System.Threading; using System.Threading.Tasks; +using Tgstation.Server.Api.Models; using Tgstation.Server.Host.Core; using Tgstation.Server.Host.IO; using Tgstation.Server.Host.Jobs; @@ -114,7 +114,7 @@ namespace Tgstation.Server.Host.Components.Byond } catch (Exception e) { - throw new JobException("Unable to start DirectX installer process! Is the server running with admin privileges?", e); + throw new JobException(ErrorCode.ByondDirectXInstallFail, e); } using (directXInstaller) @@ -125,7 +125,7 @@ namespace Tgstation.Server.Host.Components.Byond cancellationToken.ThrowIfCancellationRequested(); if (exitCode != 0) - throw new JobException(String.Format(CultureInfo.InvariantCulture, "Failed to install included DirectX! Exit code: {0}", exitCode)); + throw new JobException(ErrorCode.ByondDirectXInstallFail, new JobException($"Invalid exit code: {exitCode}")); installedDirectX = true; } } diff --git a/src/Tgstation.Server.Host/Components/Deployment/DreamMaker.cs b/src/Tgstation.Server.Host/Components/Deployment/DreamMaker.cs index ae05724d6b..0a7a26153a 100644 --- a/src/Tgstation.Server.Host/Components/Deployment/DreamMaker.cs +++ b/src/Tgstation.Server.Host/Components/Deployment/DreamMaker.cs @@ -193,34 +193,31 @@ namespace Tgstation.Server.Host.Components.Deployment { var validationStatus = controller.ApiValidationStatus; logger.LogTrace("API validation status: {0}", validationStatus); + + job.DMApiVersion = controller.DMApiVersion; switch (validationStatus) { case ApiValidationStatus.RequiresUltrasafe: job.MinimumSecurityLevel = DreamDaemonSecurity.Ultrasafe; return; case ApiValidationStatus.RequiresSafe: - if (securityLevel == DreamDaemonSecurity.Ultrasafe) - throw new JobException("This game must be run with at least the 'Safe' DreamDaemon security level!"); job.MinimumSecurityLevel = DreamDaemonSecurity.Safe; return; case ApiValidationStatus.RequiresTrusted: - if (securityLevel != DreamDaemonSecurity.Trusted) - throw new JobException("This game must be run with at least the 'Trusted' DreamDaemon security level!"); job.MinimumSecurityLevel = DreamDaemonSecurity.Trusted; return; case ApiValidationStatus.NeverValidated: - break; + throw new JobException(ErrorCode.DreamMakerNeverValidated); case ApiValidationStatus.BadValidationRequest: - throw new JobException("Recieved an unrecognized API validation request from DreamDaemon!"); + throw new JobException(ErrorCode.DreamMakerInvalidValidation); case ApiValidationStatus.UnaskedValidationRequest: default: - throw new InvalidOperationException(String.Format(CultureInfo.InvariantCulture, "Session controller returned unexpected ApiValidationStatus: {0}", validationStatus)); + throw new InvalidOperationException( + $"Session controller returned unexpected ApiValidationStatus: {validationStatus}"); } - - job.DMApiVersion = controller.DMApiVersion; } - throw new JobException("DMAPI validation timed out! Is the security level too high?"); + throw new JobException(ErrorCode.DreamMakerValidationTimeout); } } @@ -395,7 +392,7 @@ namespace Tgstation.Server.Host.Components.Deployment var foundPaths = await ioManager.GetFilesWithExtension(dirA, DmeExtension, cancellationToken).ConfigureAwait(false); var foundPath = foundPaths.FirstOrDefault(); if (foundPath == default) - throw new JobException("Unable to find any .dme!"); + throw new JobException(ErrorCode.DreamMakerNoDme); var dmeWithExtension = ioManager.GetFileName(foundPath); job.DmeName = dmeWithExtension.Substring(0, dmeWithExtension.Length - DmeExtension.Length - 1); } @@ -404,7 +401,7 @@ namespace Tgstation.Server.Host.Components.Deployment var targetDme = ioManager.ConcatPath(dirA, String.Join('.', job.DmeName, DmeExtension)); var targetDmeExists = await ioManager.FileExists(targetDme, cancellationToken).ConfigureAwait(false); if (!targetDmeExists) - throw new JobException("Unable to locate specified .dme!"); + throw new JobException(ErrorCode.DreamMakerMissingDme); } logger.LogDebug("Selected {0}.dme for compilation!", job.DmeName); @@ -418,7 +415,9 @@ namespace Tgstation.Server.Host.Components.Deployment try { if (exitCode != 0) - throw new JobException(String.Format(CultureInfo.InvariantCulture, "DM exited with a non-zero code: {0}{1}{2}", exitCode, Environment.NewLine, job.Output)); + throw new JobException( + ErrorCode.DreamMakerExitCode, + new JobException($"Exit code: {exitCode}{Environment.NewLine}{Environment.NewLine}{job.Output}")); await VerifyApi(apiValidateTimeout, dreamMakerSettings.ApiValidationSecurityLevel.Value, job, byondLock, dreamMakerSettings.ApiValidationPort.Value, cancellationToken).ConfigureAwait(false); } @@ -476,7 +475,7 @@ namespace Tgstation.Server.Host.Components.Deployment lock (this) { if (compiling) - throw new JobException("There is already a compile job in progress!"); + throw new JobException(ErrorCode.DreamMakerCompileJobInProgress); compiling = true; } diff --git a/src/Tgstation.Server.Host/Components/Instance.cs b/src/Tgstation.Server.Host/Components/Instance.cs index 6dba85ae04..34d582a381 100644 --- a/src/Tgstation.Server.Host/Components/Instance.cs +++ b/src/Tgstation.Server.Host/Components/Instance.cs @@ -169,13 +169,13 @@ namespace Tgstation.Server.Host.Components throw new ArgumentNullException(nameof(progressReporter)); var ddSettings = await databaseContext.DreamDaemonSettings.Where(x => x.InstanceId == metadata.Id).Select(x => new DreamDaemonSettings - { - StartupTimeout = x.StartupTimeout, - }) + { + StartupTimeout = x.StartupTimeout, + }) .FirstOrDefaultAsync(cancellationToken) .ConfigureAwait(false); if (ddSettings == default) - throw new JobException("Missing DreamDaemonSettings in DB!"); + throw new JobException(Api.Models.ErrorCode.InstanceMissingDreamDaemonSettings); var previousCompileJobs = await databaseContext.CompileJobs .Where(x => x.Job.Instance.Id == metadata.Id) @@ -191,18 +191,17 @@ namespace Tgstation.Server.Host.Components var dreamMakerSettings = await databaseContext.DreamMakerSettings.Where(x => x.InstanceId == metadata.Id).FirstAsync(cancellationToken).ConfigureAwait(false); if (dreamMakerSettings == default) - throw new JobException("Missing DreamMakerSettings in DB!"); + throw new JobException(Api.Models.ErrorCode.InstanceMissingDreamMakerSettings); RepositorySettings repositorySettings = null; string repoOwner = null; string repoName = null; CompileJob compileJob; RevisionInformation revInfo; - bool loadedRepositorySettings; using (var repo = await RepositoryManager.LoadRepository(cancellationToken).ConfigureAwait(false)) { if (repo == null) - throw new JobException("Missing Repository!"); + throw new JobException(Api.Models.ErrorCode.RepoMissing); if (repo.IsGitHubRepository) { @@ -219,10 +218,9 @@ namespace Tgstation.Server.Host.Components }) .FirstOrDefaultAsync(cancellationToken) .ConfigureAwait(false); - loadedRepositorySettings = true; + if (repositorySettings == default) + throw new JobException(Api.Models.ErrorCode.InstanceMissingRepositorySettings); } - else - loadedRepositorySettings = false; var repoSha = repo.Head; revInfo = await databaseContext.RevisionInformations.Where(x => x.CommitSha == repoSha && x.Instance.Id == metadata.Id).Include(x => x.ActiveTestMerges).ThenInclude(x => x.TestMerge).ThenInclude(x => x.MergedBy).FirstOrDefaultAsync().ConfigureAwait(false); @@ -243,7 +241,7 @@ namespace Tgstation.Server.Host.Components } TimeSpan? averageSpan = null; - if(previousCompileJobs.Count != 0) + if (previousCompileJobs.Count != 0) { var totalSpan = TimeSpan.Zero; foreach (var I in previousCompileJobs) @@ -260,80 +258,74 @@ namespace Tgstation.Server.Host.Components job.PostComplete = ct => compileJobConsumer.LoadCompileJob(compileJob, ct); - if (loadedRepositorySettings) + if (repositorySettings?.AccessToken != null) { - if (repositorySettings == default) - throw new JobException("Missing repository settings!"); + // potential for commenting on a test merge change + var outgoingCompileJob = LatestCompileJob(); - if (repositorySettings.AccessToken != null) + if (outgoingCompileJob != null && outgoingCompileJob.RevisionInformation.CommitSha != compileJob.RevisionInformation.CommitSha && repositorySettings.PostTestMergeComment.Value) { - // potential for commenting on a test merge change - var outgoingCompileJob = LatestCompileJob(); + var gitHubClient = gitHubClientFactory.CreateClient(repositorySettings.AccessToken); - if(outgoingCompileJob != null && outgoingCompileJob.RevisionInformation.CommitSha != compileJob.RevisionInformation.CommitSha && repositorySettings.PostTestMergeComment.Value) + async Task CommentOnPR(int prNumber, string comment) { - var gitHubClient = gitHubClientFactory.CreateClient(repositorySettings.AccessToken); - - async Task CommentOnPR(int prNumber, string comment) + try { - try - { - await gitHubClient.Issue.Comment.Create(repoOwner, repoName, prNumber, comment).ConfigureAwait(false); - } - catch (ApiException e) - { - logger.LogWarning("Error posting GitHub comment! Exception: {0}", e); - } + await gitHubClient.Issue.Comment.Create(repoOwner, repoName, prNumber, comment).ConfigureAwait(false); + } + catch (ApiException e) + { + logger.LogWarning("Error posting GitHub comment! Exception: {0}", e); } - - var tasks = new List(); - - string FormatTestMerge(TestMerge testMerge, bool updated) => String.Format(CultureInfo.InvariantCulture, "#### Test Merge {4}{0}{0}##### Server Instance{0}{5}{1}{0}{0}##### Revision{0}Origin: {6}{0}Pull Request: {2}{0}Server: {7}{3}", - Environment.NewLine, - repositorySettings.ShowTestMergeCommitters.Value ? String.Format(CultureInfo.InvariantCulture, "{0}{0}##### Merged By{0}{1}", Environment.NewLine, testMerge.MergedBy.Name) : String.Empty, - testMerge.PullRequestRevision, - testMerge.Comment != null ? String.Format(CultureInfo.InvariantCulture, "{0}{0}##### Comment{0}{1}", Environment.NewLine, testMerge.Comment) : String.Empty, - updated ? "Updated" : "Deployed", - metadata.Name, - compileJob.RevisionInformation.OriginCommitSha, - compileJob.RevisionInformation.CommitSha); - - // added prs - foreach (var I in compileJob - .RevisionInformation - .ActiveTestMerges - .Select(x => x.TestMerge) - .Where(x => !outgoingCompileJob - .RevisionInformation - .ActiveTestMerges - .Any(y => y.TestMerge.Number == x.Number))) - tasks.Add(CommentOnPR(I.Number, FormatTestMerge(I, false))); - - // removed prs - foreach (var I in outgoingCompileJob - .RevisionInformation - .ActiveTestMerges - .Select(x => x.TestMerge) - .Where(x => !compileJob - .RevisionInformation - .ActiveTestMerges - .Any(y => y.TestMerge.Number == x.Number))) - tasks.Add(CommentOnPR(I.Number, "#### Test Merge Removed")); - - // updated prs - foreach(var I in compileJob - .RevisionInformation - .ActiveTestMerges - .Select(x => x.TestMerge) - .Where(x => outgoingCompileJob - .RevisionInformation - .ActiveTestMerges - .Any(y => y.TestMerge.Number == x.Number))) - tasks.Add(CommentOnPR(I.Number, FormatTestMerge(I, true))); - - if (tasks.Any()) - await Task.WhenAll(tasks).ConfigureAwait(false); } + + var tasks = new List(); + + string FormatTestMerge(TestMerge testMerge, bool updated) => String.Format(CultureInfo.InvariantCulture, "#### Test Merge {4}{0}{0}##### Server Instance{0}{5}{1}{0}{0}##### Revision{0}Origin: {6}{0}Pull Request: {2}{0}Server: {7}{3}", + Environment.NewLine, + repositorySettings.ShowTestMergeCommitters.Value ? String.Format(CultureInfo.InvariantCulture, "{0}{0}##### Merged By{0}{1}", Environment.NewLine, testMerge.MergedBy.Name) : String.Empty, + testMerge.PullRequestRevision, + testMerge.Comment != null ? String.Format(CultureInfo.InvariantCulture, "{0}{0}##### Comment{0}{1}", Environment.NewLine, testMerge.Comment) : String.Empty, + updated ? "Updated" : "Deployed", + metadata.Name, + compileJob.RevisionInformation.OriginCommitSha, + compileJob.RevisionInformation.CommitSha); + + // added prs + foreach (var I in compileJob + .RevisionInformation + .ActiveTestMerges + .Select(x => x.TestMerge) + .Where(x => !outgoingCompileJob + .RevisionInformation + .ActiveTestMerges + .Any(y => y.TestMerge.Number == x.Number))) + tasks.Add(CommentOnPR(I.Number, FormatTestMerge(I, false))); + + // removed prs + foreach (var I in outgoingCompileJob + .RevisionInformation + .ActiveTestMerges + .Select(x => x.TestMerge) + .Where(x => !compileJob + .RevisionInformation + .ActiveTestMerges + .Any(y => y.TestMerge.Number == x.Number))) + tasks.Add(CommentOnPR(I.Number, "#### Test Merge Removed")); + + // updated prs + foreach (var I in compileJob + .RevisionInformation + .ActiveTestMerges + .Select(x => x.TestMerge) + .Where(x => outgoingCompileJob + .RevisionInformation + .ActiveTestMerges + .Any(y => y.TestMerge.Number == x.Number))) + tasks.Add(CommentOnPR(I.Number, FormatTestMerge(I, true))); + + if (tasks.Any()) + await Task.WhenAll(tasks).ConfigureAwait(false); } } } @@ -464,7 +456,7 @@ namespace Tgstation.Server.Host.Components var result = await repo.MergeOrigin(repositorySettings.CommitterName, repositorySettings.CommitterEmail, NextProgressReporter(), jobCancellationToken).ConfigureAwait(false); if (!result.HasValue) - throw new JobException("Merge conflict while preserving test merges!"); + throw new JobException(Api.Models.ErrorCode.InstanceUpdateTestMergeConflict); currentRevInfo = await currentRevInfoTask.ConfigureAwait(false); diff --git a/src/Tgstation.Server.Host/Components/Repository/LibGit2RepositoryFactory.cs b/src/Tgstation.Server.Host/Components/Repository/LibGit2RepositoryFactory.cs index 3d4663dde3..69059f93cc 100644 --- a/src/Tgstation.Server.Host/Components/Repository/LibGit2RepositoryFactory.cs +++ b/src/Tgstation.Server.Host/Components/Repository/LibGit2RepositoryFactory.cs @@ -4,6 +4,7 @@ using Microsoft.Extensions.Logging; using System; using System.Threading; using System.Threading.Tasks; +using Tgstation.Server.Api.Models; using Tgstation.Server.Host.Jobs; namespace Tgstation.Server.Host.Components.Repository @@ -93,9 +94,9 @@ namespace Tgstation.Server.Host.Components.Repository return new DefaultCredentials(); if (supportsUserPass) - throw new JobException("Remote does not support anonymous authentication!"); + throw new JobException(ErrorCode.RepoCredentialsRequired); - throw new JobException("Server does not support anonymous or username/password authentication!"); + throw new JobException(ErrorCode.RepoCannotAuthenticate); }; } } diff --git a/src/Tgstation.Server.Host/Components/Repository/Repository.cs b/src/Tgstation.Server.Host/Components/Repository/Repository.cs index 6a1c0abf2d..947e82cb77 100644 --- a/src/Tgstation.Server.Host/Components/Repository/Repository.cs +++ b/src/Tgstation.Server.Host/Components/Repository/Repository.cs @@ -465,7 +465,7 @@ namespace Tgstation.Server.Host.Components.Repository if (progressReporter == null) throw new ArgumentNullException(nameof(progressReporter)); if (!Tracking) - throw new JobException("Cannot reset to origin while not on a tracked reference!"); + throw new JobException(ErrorCode.RepoReferenceRequired); logger.LogTrace("Reset to origin..."); var trackedBranch = libGitRepo.Head.TrackedBranch; await eventConsumer.HandleEvent(EventType.RepoResetOrigin, new List { trackedBranch.FriendlyName, trackedBranch.Tip.Sha }, cancellationToken).ConfigureAwait(false); @@ -521,7 +521,7 @@ namespace Tgstation.Server.Host.Components.Repository await Task.Factory.StartNew(() => { if (!Tracking) - throw new JobException("Cannot reset to origin while not on a tracked reference!"); + throw new JobException(ErrorCode.RepoReferenceRequired); libGitRepo.RemoveUntrackedFiles(); diff --git a/src/Tgstation.Server.Host/Components/Watchdog/ExperimentalWatchdog.cs b/src/Tgstation.Server.Host/Components/Watchdog/ExperimentalWatchdog.cs index d83712d943..333582ed73 100644 --- a/src/Tgstation.Server.Host/Components/Watchdog/ExperimentalWatchdog.cs +++ b/src/Tgstation.Server.Host/Components/Watchdog/ExperimentalWatchdog.cs @@ -185,7 +185,7 @@ namespace Tgstation.Server.Host.Components.Watchdog var dmbBackup = await DmbFactory.FromCompileJob(monitorState.ActiveServer.Dmb.CompileJob, cancellationToken).ConfigureAwait(false); if (dmbBackup == null) // NANI!? - throw new JobException("Creating backup DMB provider failed!"); // just give up, if THAT compile job is failing then the ActiveServer is gonna crash soon too or already has + throw new InvalidOperationException("Watchdog double crit-fail!"); // just give up, if THAT compile job is failing then the ActiveServer is gonna crash soon too or already has monitorState.InactiveServer = await SessionControllerFactory.LaunchNew( dmbBackup, diff --git a/src/Tgstation.Server.Host/Components/Watchdog/WatchdogBase.cs b/src/Tgstation.Server.Host/Components/Watchdog/WatchdogBase.cs index 2b61522029..b3877611ec 100644 --- a/src/Tgstation.Server.Host/Components/Watchdog/WatchdogBase.cs +++ b/src/Tgstation.Server.Host/Components/Watchdog/WatchdogBase.cs @@ -4,10 +4,10 @@ using Microsoft.Extensions.Logging; using System; using System.Collections.Generic; using System.Diagnostics; -using System.Globalization; using System.Linq; using System.Threading; using System.Threading.Tasks; +using Tgstation.Server.Api.Models; using Tgstation.Server.Api.Models.Internal; using Tgstation.Server.Api.Rights; using Tgstation.Server.Host.Components.Chat; @@ -253,10 +253,10 @@ namespace Tgstation.Server.Host.Components.Watchdog Logger.LogTrace("Begin LaunchNoLock"); if (Running) - throw new JobException("Watchdog already running!"); + throw new JobException(ErrorCode.WatchdogRunning); if (!DmbFactory.DmbAvailable) - throw new JobException("Corrupted compilation, please redeploy!"); + 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 Task chatTask; @@ -371,9 +371,13 @@ namespace Tgstation.Server.Host.Components.Watchdog // Dead sessions won't trigger this if (launchResult.ExitCode.HasValue) // you killed us ray... - throw new JobException(String.Format(CultureInfo.InvariantCulture, "{0} failed to start: {1}", serverName, launchResult)); + throw new JobException( + ErrorCode.WatchdogStartupFailed, + new JobException($"{serverName} failed to start: {launchResult}")); if (!launchResult.StartupTime.HasValue) - throw new JobException(String.Format(CultureInfo.InvariantCulture, "{0} timed out on startup: {1}s", serverName, ActiveLaunchParameters.StartupTimeout.Value)); + throw new JobException( + ErrorCode.WatchdogStartupTimeout, + new JobException($"{serverName} timed out on startup: {ActiveLaunchParameters.StartupTimeout.Value}s")); } /// diff --git a/src/Tgstation.Server.Host/Controllers/RepositoryController.cs b/src/Tgstation.Server.Host/Controllers/RepositoryController.cs index f2006ed345..3e9fa3fedc 100644 --- a/src/Tgstation.Server.Host/Controllers/RepositoryController.cs +++ b/src/Tgstation.Server.Host/Controllers/RepositoryController.cs @@ -198,7 +198,7 @@ namespace Tgstation.Server.Host.Controllers using (var repos = await repoManager.CloneRepository(new Uri(origin), cloneBranch, currentModel.AccessUser, currentModel.AccessToken, progressReporter, ct).ConfigureAwait(false)) { if (repos == null) - throw new JobException("Filesystem conflict while cloning repository!"); + throw new JobException(ErrorCode.RepoExists); var instance = new Models.Instance { Id = Instance.Id @@ -447,7 +447,7 @@ namespace Tgstation.Server.Host.Controllers using (var repo = await repoManager.LoadRepository(ct).ConfigureAwait(false)) { if (repo == null) - throw new JobException("Repository could not be loaded!"); + throw new JobException(ErrorCode.RepoMissing); var modelHasShaOrReference = model.CheckoutSha != null || model.Reference != null; @@ -456,7 +456,7 @@ namespace Tgstation.Server.Host.Controllers string postUpdateSha = null; if (newTestMerges && !repo.IsGitHubRepository) - throw new JobException("Cannot test merge on a non GitHub based repository!"); + throw new JobException(ErrorCode.RepoUnsupportedTestMergeRemote); var committerName = currentModel.ShowTestMergeCommitters.Value ? AuthenticationContext.User.Name @@ -501,14 +501,14 @@ namespace Tgstation.Server.Host.Controllers if (model.UpdateFromOrigin == true) { if (!repo.Tracking) - throw new JobException("Not on an updatable reference!"); + throw new JobException(ErrorCode.RepoReferenceRequired); await repo.FetchOrigin(currentModel.AccessUser, currentModel.AccessToken, NextProgressReporter(), ct).ConfigureAwait(false); doneSteps = 1; if (!modelHasShaOrReference) { var fastForward = await repo.MergeOrigin(committerName, currentModel.CommitterEmail, NextProgressReporter(), ct).ConfigureAwait(false); if (!fastForward.HasValue) - throw new JobException("Merge conflict occurred during origin update!"); + throw new JobException(ErrorCode.RepoMergeConflict); await UpdateRevInfo().ConfigureAwait(false); if (fastForward.Value) { @@ -531,7 +531,7 @@ namespace Tgstation.Server.Host.Controllers var isSha = await repo.IsSha(committish, cancellationToken).ConfigureAwait(false); if ((isSha && model.Reference != null) || (!isSha && model.CheckoutSha != null)) - throw new JobException("Attempted to checkout a SHA or reference that was actually the opposite!"); + throw new JobException(ErrorCode.RepoSwappedShaOrReference); await repo.CheckoutObject(committish, NextProgressReporter(), ct).ConfigureAwait(false); await LoadRevisionInformation(repo, databaseContext, attachedInstance, null, x => lastRevisionInfo = x, ct).ConfigureAwait(false); // we've either seen origin before or what we're checking out is on origin @@ -542,7 +542,7 @@ namespace Tgstation.Server.Host.Controllers if (hardResettingToOriginReference) { if (!repo.Tracking) - throw new JobException("Checked out reference does not track a remote object!"); + throw new JobException(ErrorCode.RepoReferenceNotTracking); await repo.ResetToOrigin(NextProgressReporter(), ct).ConfigureAwait(false); await repo.Sychronize(currentModel.AccessUser, currentModel.AccessToken, currentModel.CommitterName, currentModel.CommitterEmail, NextProgressReporter(), true, ct).ConfigureAwait(false); await LoadRevisionInformation(repo, databaseContext, attachedInstance, null, x => lastRevisionInfo = x, ct).ConfigureAwait(false); @@ -692,29 +692,36 @@ namespace Tgstation.Server.Host.Controllers string errorMessage = null; if (lastRevisionInfo.ActiveTestMerges.Any(x => x.TestMerge.Number == I.Number)) - throw new JobException("Cannot test merge the same PR twice in one HEAD!"); + throw new JobException(ErrorCode.RepoDuplicateTestMerge); + Exception exception = null; try { // load from cache if possible if (prMap == null || !prMap.TryGetValue(I.Number, out pr)) pr = await gitHubClient.PullRequest.Get(repoOwner, repoName, I.Number).ConfigureAwait(false); } - catch (Octokit.RateLimitExceededException) + catch (Octokit.RateLimitExceededException ex) { // you look at your anonymous access and sigh - errorMessage = "P.R.E. RATE LIMITED"; + errorMessage = "REMOTE API ERROR: RATE LIMITED"; + exception = ex; } - catch (Octokit.AuthorizationException) + catch (Octokit.AuthorizationException ex) { - errorMessage = "P.R.E. BAD CREDENTIALS"; + errorMessage = "REMOTE API ERROR: BAD CREDENTIALS"; + exception = ex; } - catch (Octokit.NotFoundException) + catch (Octokit.NotFoundException ex) { // you look at your shithub and sigh - errorMessage = "P.R.E. NOT FOUND"; + errorMessage = "REMOTE API ERROR: PULL REQUEST NOT FOUND"; + exception = ex; } + if (exception != null) + Logger.LogWarning("Error retrieving pull request metadata: {0}", exception); + // we want to take the earliest truth possible to prevent RCEs, if this fails AddTestMerge will set it if (I.PullRequestRevision == null && pr != null) I.PullRequestRevision = pr.Head.Sha; @@ -729,7 +736,10 @@ namespace Tgstation.Server.Host.Controllers ct).ConfigureAwait(false); if (!mergeResult.HasValue) - throw new JobException(String.Format(CultureInfo.InvariantCulture, "Merge of PR #{0} at {1} conflicted!", I.Number, I.PullRequestRevision.Substring(0, 7))); + throw new JobException( + ErrorCode.RepoTestMergeConflict, + new JobException( + $"Merge of PR #{I.Number} at {I.PullRequestRevision.Substring(0, 7)} conflicted!")); ++doneSteps; diff --git a/src/Tgstation.Server.Host/Jobs/JobException.cs b/src/Tgstation.Server.Host/Jobs/JobException.cs index 40469a0e59..55f2bb97d5 100644 --- a/src/Tgstation.Server.Host/Jobs/JobException.cs +++ b/src/Tgstation.Server.Host/Jobs/JobException.cs @@ -1,4 +1,5 @@ using System; +using Tgstation.Server.Api.Models; namespace Tgstation.Server.Host.Jobs { @@ -7,6 +8,11 @@ namespace Tgstation.Server.Host.Jobs /// public sealed class JobException : Exception { + /// + /// The associated with the . + /// + public ErrorCode? ErrorCode { get; set; } + /// /// Construct a /// @@ -30,5 +36,22 @@ namespace Tgstation.Server.Host.Jobs public JobException(string message, Exception innerException) : base(message, innerException) { } + + /// + /// Construct a with a . + /// + /// The associated . + public JobException(ErrorCode errorCode) : base(errorCode.Describe()) + { + } + + /// + /// Construct a with a and . + /// + /// The associated . + /// The inner for the nase + public JobException(ErrorCode errorCode, Exception innerException) : base(errorCode.Describe(), innerException) + { + } } } diff --git a/src/Tgstation.Server.Host/Jobs/JobManager.cs b/src/Tgstation.Server.Host/Jobs/JobManager.cs index 677cb36238..055748b4a0 100644 --- a/src/Tgstation.Server.Host/Jobs/JobManager.cs +++ b/src/Tgstation.Server.Host/Jobs/JobManager.cs @@ -89,10 +89,16 @@ 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); + logger.LogDebug( + "Inner exception for job {0}: {1}", + job.Id, + e.InnerException is JobException + ? e.InnerException.Message + : e.InnerException.ToString()); } catch (Exception e) {