diff --git a/src/Tgstation.Server.Host/Components/Compiler/DreamMaker.cs b/src/Tgstation.Server.Host/Components/Compiler/DreamMaker.cs index b4bebe5427..b4b52bf9ba 100644 --- a/src/Tgstation.Server.Host/Components/Compiler/DreamMaker.cs +++ b/src/Tgstation.Server.Host/Components/Compiler/DreamMaker.cs @@ -100,6 +100,29 @@ namespace Tgstation.Server.Host.Components.Compiler /// bool compiling; + /// + /// Gradually triggers a given over a given + /// + /// The to report progress + /// A representing the duration to give progress over + /// The for the operation + /// A representing the running operation + static async Task ProgressTask(Action progressReporter, TimeSpan estimatedDuration, CancellationToken cancellationToken) + { + progressReporter(0); + var sleepInterval = estimatedDuration / 100; + + try + { + for (var I = 0; I < 99; ++I) + { + await Task.Delay(sleepInterval, cancellationToken).ConfigureAwait(false); + progressReporter(I + 1); + } + } + catch (OperationCanceledException) { } + } + /// /// Construct /// @@ -279,8 +302,162 @@ namespace Tgstation.Server.Host.Components.Compiler await ioManager.WriteAllBytes(dmePath, dmeBytes, cancellationToken).ConfigureAwait(false); } + /// + /// Cleans up a failed compile + /// + /// The running + /// If the was cancelled + /// The for the operation + /// A representing the running operation + async Task CleanupFailedCompile(Models.CompileJob job, bool cancelled, CancellationToken cancellationToken) + { + logger.LogTrace("Cleaning compile directory..."); + var chatTask = chat.SendUpdateMessage(cancelled ? "Deploy cancelled!" : "Deploy failed!", cancellationToken); + var jobPath = job.DirectoryName.ToString(); + try + { + await ioManager.DeleteDirectory(jobPath, CancellationToken.None).ConfigureAwait(false); + } + catch (Exception e) + { + logger.LogWarning("Error cleaning up compile directory {0}! Exception: {1}", ioManager.ResolvePath(jobPath), e); + } + + await chatTask.ConfigureAwait(false); + } + + /// + /// Send a message to about a deployment + /// + /// The for the deployment + /// The for the deployment + /// The for the operation + /// A representing the running operation + async Task SendDeploymentMessage(Models.RevisionInformation revisionInformation, IByondExecutableLock byondLock, CancellationToken cancellationToken) + { + var commitInsert = revisionInformation.CommitSha.Substring(0, 7); + string remoteCommitInsert; + if (revisionInformation.CommitSha == revisionInformation.OriginCommitSha) + { + commitInsert = String.Format(CultureInfo.InvariantCulture, "^{0}", commitInsert); + remoteCommitInsert = String.Empty; + } + else + remoteCommitInsert = String.Format(CultureInfo.InvariantCulture, ". Remote commit: ^{0}", revisionInformation.OriginCommitSha.Substring(0, 7)); + + var testmergeInsert = revisionInformation.ActiveTestMerges.Count == 0 ? String.Empty : String.Format(CultureInfo.InvariantCulture, " (Test Merges: {0})", + String.Join(", ", revisionInformation.ActiveTestMerges.Select(x => x.TestMerge).Select(x => + { + var result = String.Format(CultureInfo.InvariantCulture, "#{0} at {1}", x.Number, x.PullRequestRevision.Substring(0, 7)); + if (x.Comment != null) + result += String.Format(CultureInfo.InvariantCulture, " ({0})", x.Comment); + return result; + }))); + + await chat.SendUpdateMessage(String.Format(CultureInfo.InvariantCulture, "Deploying revision: {0}{1}{2} BYOND Version: {3}", commitInsert, testmergeInsert, remoteCommitInsert, byondLock.Version), cancellationToken).ConfigureAwait(false); + } + + /// + /// Executes and populate a given + /// + /// The to run and populate + /// The settings to use + /// The to use + /// The to use + /// The timeout for validating the DMAPI + /// The for the operation + /// A representing the running operation + async Task RunCompileJob(Models.CompileJob job, Api.Models.DreamMaker dreamMakerSettings, IByondExecutableLock byondLock, IRepository repository, uint apiValidateTimeout, CancellationToken cancellationToken) + { + var jobPath = job.DirectoryName.ToString(); + logger.LogTrace("Compile output GUID: {0}", jobPath); + + try + { + var dirA = ioManager.ConcatPath(jobPath, ADirectoryName); + var dirB = ioManager.ConcatPath(jobPath, BDirectoryName); + + // copy the repository + logger.LogTrace("Copying repository to game directory..."); + var resolvedADirectory = ioManager.ResolvePath(dirA); + var repoOrigin = repository.Origin; + using (repository) + await repository.CopyTo(resolvedADirectory, cancellationToken).ConfigureAwait(false); + + // repository closed now + + // run precompile scripts + await eventConsumer.HandleEvent(EventType.CompileStart, new List { resolvedADirectory, repoOrigin }, cancellationToken).ConfigureAwait(false); + + // determine the dme + if (job.DmeName == null) + { + logger.LogTrace("Searching for available .dmes..."); + 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!"); + var dmeWithExtension = ioManager.GetFileName(foundPath); + job.DmeName = dmeWithExtension.Substring(0, dmeWithExtension.Length - DmeExtension.Length - 1); + } + else + { + 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!"); + } + + logger.LogDebug("Selected {0}.dme for compilation!", job.DmeName); + + await ModifyDme(job, cancellationToken).ConfigureAwait(false); + + // run compiler + var exitCode = await RunDreamMaker(byondLock.DreamMakerPath, job, cancellationToken).ConfigureAwait(false); + + // verify api + 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)); + + await VerifyApi(apiValidateTimeout, dreamMakerSettings.ApiValidationSecurityLevel.Value, job, byondLock, dreamMakerSettings.ApiValidationPort.Value, cancellationToken).ConfigureAwait(false); + } + catch (JobException) + { + // DD never validated or compile failed + await eventConsumer.HandleEvent(EventType.CompileFailure, new List { resolvedADirectory, exitCode == 0 ? "1" : "0" }, cancellationToken).ConfigureAwait(false); + throw; + } + + logger.LogTrace("Running post compile event..."); + await eventConsumer.HandleEvent(EventType.CompileComplete, new List { resolvedADirectory }, cancellationToken).ConfigureAwait(false); + + logger.LogTrace("Duplicating compiled game..."); + + // duplicate the dmb et al + await ioManager.CopyDirectory(dirA, dirB, null, cancellationToken).ConfigureAwait(false); + + logger.LogTrace("Applying static game file symlinks..."); + + // symlink in the static data + var symATask = configuration.SymlinkStaticFilesTo(resolvedADirectory, cancellationToken); + var symBTask = configuration.SymlinkStaticFilesTo(ioManager.ResolvePath(dirB), cancellationToken); + + await Task.WhenAll(symATask, symBTask).ConfigureAwait(false); + + await chat.SendUpdateMessage(String.Format(CultureInfo.InvariantCulture, "Deployment complete!{0}", watchdog.Running ? " Changes will be applied on next server reboot." : String.Empty), cancellationToken).ConfigureAwait(false); + + logger.LogDebug("Compile complete!"); + } + catch (Exception e) + { + await CleanupFailedCompile(job, e is OperationCanceledException, cancellationToken).ConfigureAwait(false); + throw; + } + } + /// - #pragma warning disable CA1506 //TODO: Decomplexify public async Task Compile(Models.RevisionInformation revisionInformation, Api.Models.DreamMaker dreamMakerSettings, uint apiValidateTimeout, IRepository repository, Action progressReporter, TimeSpan? estimatedDuration, CancellationToken cancellationToken) { if (revisionInformation == null) @@ -300,15 +477,6 @@ namespace Tgstation.Server.Host.Components.Compiler logger.LogTrace("Begin Compile"); - var job = new Models.CompileJob - { - DirectoryName = Guid.NewGuid(), - DmeName = dreamMakerSettings.ProjectName, - RevisionInformation = revisionInformation - }; - - logger.LogTrace("Compile output GUID: {0}", job.DirectoryName); - lock (this) { if (compiling) @@ -318,149 +486,24 @@ namespace Tgstation.Server.Host.Components.Compiler using (var progressCts = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken)) { - async Task ProgressTask() - { - if (!estimatedDuration.HasValue) - return; - - progressReporter(0); - var ct = progressCts.Token; - var sleepInterval = estimatedDuration.Value / 100; - - try - { - for (var I = 0; I < 99; ++I) - { - await Task.Delay(sleepInterval, progressCts.Token).ConfigureAwait(false); - progressReporter(I + 1); - } - } - catch (OperationCanceledException) { } - } - - var progressTask = ProgressTask(); + var progressTask = estimatedDuration.HasValue ? ProgressTask(progressReporter, estimatedDuration.Value, cancellationToken) : Task.CompletedTask; try { - var commitInsert = revisionInformation.CommitSha.Substring(0, 7); - string remoteCommitInsert; - if (revisionInformation.CommitSha == revisionInformation.OriginCommitSha) - { - commitInsert = String.Format(CultureInfo.InvariantCulture, "^{0}", commitInsert); - remoteCommitInsert = String.Empty; - } - else - remoteCommitInsert = String.Format(CultureInfo.InvariantCulture, ". Remote commit: ^{0}", revisionInformation.OriginCommitSha.Substring(0, 7)); - - var testmergeInsert = revisionInformation.ActiveTestMerges.Count == 0 ? String.Empty : String.Format(CultureInfo.InvariantCulture, " (Test Merges: {0})", - String.Join(", ", revisionInformation.ActiveTestMerges.Select(x => x.TestMerge).Select(x => - { - var result = String.Format(CultureInfo.InvariantCulture, "#{0} at {1}", x.Number, x.PullRequestRevision.Substring(0, 7)); - if (x.Comment != null) - result += String.Format(CultureInfo.InvariantCulture, " ({0})", x.Comment); - return result; - }))); - using (var byondLock = await byond.UseExecutables(null, cancellationToken).ConfigureAwait(false)) { - await chat.SendUpdateMessage(String.Format(CultureInfo.InvariantCulture, "Deploying revision: {0}{1}{2} BYOND Version: {3}", commitInsert, testmergeInsert, remoteCommitInsert, byondLock.Version), cancellationToken).ConfigureAwait(false); + await SendDeploymentMessage(revisionInformation, byondLock, cancellationToken).ConfigureAwait(false); - async Task CleanupFailedCompile(bool cancelled) + var job = new Models.CompileJob { - logger.LogTrace("Cleaning compile directory..."); - var chatTask = chat.SendUpdateMessage(cancelled ? "Deploy cancelled!" : "Deploy failed!", cancellationToken); - try - { - await ioManager.DeleteDirectory(job.DirectoryName.ToString(), CancellationToken.None).ConfigureAwait(false); - } - catch (Exception e) - { - logger.LogWarning("Error cleaning up compile directory {0}! Exception: {1}", ioManager.ResolvePath(job.DirectoryName.ToString()), e); - } + DirectoryName = Guid.NewGuid(), + DmeName = dreamMakerSettings.ProjectName, + RevisionInformation = revisionInformation, + ByondVersion = byondLock.Version.ToString() + }; - await chatTask.ConfigureAwait(false); - } + await RunCompileJob(job, dreamMakerSettings, byondLock, repository, apiValidateTimeout, cancellationToken).ConfigureAwait(false); - try - { - await ioManager.CreateDirectory(job.DirectoryName.ToString(), cancellationToken).ConfigureAwait(false); - - var dirA = ioManager.ConcatPath(job.DirectoryName.ToString(), ADirectoryName); - var dirB = ioManager.ConcatPath(job.DirectoryName.ToString(), BDirectoryName); - - // copy the repository - logger.LogTrace("Copying repository to game directory..."); - var fullDirA = ioManager.ResolvePath(dirA); - var repoOrigin = repository.Origin; - using (repository) - await repository.CopyTo(fullDirA, cancellationToken).ConfigureAwait(false); - - // run precompile scripts - var resolvedGameDirectory = ioManager.ResolvePath(ioManager.ConcatPath(job.DirectoryName.ToString(), ADirectoryName)); - await eventConsumer.HandleEvent(EventType.CompileStart, new List { resolvedGameDirectory, repoOrigin }, cancellationToken).ConfigureAwait(false); - - // determine the dme - if (job.DmeName == null) - { - logger.LogTrace("Searching for available .dmes..."); - var path = (await ioManager.GetFilesWithExtension(dirA, DmeExtension, cancellationToken).ConfigureAwait(false)).FirstOrDefault(); - if (path == default) - throw new JobException("Unable to find any .dme!"); - var dmeWithExtension = ioManager.GetFileName(path); - job.DmeName = dmeWithExtension.Substring(0, dmeWithExtension.Length - DmeExtension.Length - 1); - } - else if (!await ioManager.FileExists(ioManager.ConcatPath(dirA, String.Join('.', job.DmeName, DmeExtension)), cancellationToken).ConfigureAwait(false)) - throw new JobException("Unable to locate specified .dme!"); - - logger.LogDebug("Selected {0}.dme for compilation!", job.DmeName); - - await ModifyDme(job, cancellationToken).ConfigureAwait(false); - - // run compiler - job.ByondVersion = byondLock.Version.ToString(); - - var exitCode = await RunDreamMaker(byondLock.DreamMakerPath, job, cancellationToken).ConfigureAwait(false); - - // verify api - 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)); - - await VerifyApi(apiValidateTimeout, dreamMakerSettings.ApiValidationSecurityLevel.Value, job, byondLock, dreamMakerSettings.ApiValidationPort.Value, cancellationToken).ConfigureAwait(false); - } - catch (JobException) - { - // DD never validated or compile failed - await eventConsumer.HandleEvent(EventType.CompileFailure, new List { resolvedGameDirectory, exitCode == 0 ? "1" : "0" }, cancellationToken).ConfigureAwait(false); - throw; - } - - logger.LogTrace("Running post compile event..."); - await eventConsumer.HandleEvent(EventType.CompileComplete, new List { ioManager.ResolvePath(ioManager.ConcatPath(job.DirectoryName.ToString(), ADirectoryName)) }, cancellationToken).ConfigureAwait(false); - - logger.LogTrace("Duplicating compiled game..."); - - // duplicate the dmb et al - await ioManager.CopyDirectory(dirA, dirB, null, cancellationToken).ConfigureAwait(false); - - logger.LogTrace("Applying static game file symlinks..."); - - // symlink in the static data - var symATask = configuration.SymlinkStaticFilesTo(fullDirA, cancellationToken); - var symBTask = configuration.SymlinkStaticFilesTo(ioManager.ResolvePath(dirB), cancellationToken); - - await Task.WhenAll(symATask, symBTask).ConfigureAwait(false); - - await chat.SendUpdateMessage(String.Format(CultureInfo.InvariantCulture, "Deployment complete!{0}", watchdog.Running ? " Changes will be applied on next server reboot." : String.Empty), cancellationToken).ConfigureAwait(false); - - logger.LogDebug("Compile complete!"); - return job; - } - catch (Exception e) - { - await CleanupFailedCompile(e is OperationCanceledException).ConfigureAwait(false); - throw; - } + return job; } } catch (OperationCanceledException) @@ -476,6 +519,5 @@ namespace Tgstation.Server.Host.Components.Compiler } } } - #pragma warning restore CA1506 } }