From b9565185ac4355e7b38d521a616885242bd9076a Mon Sep 17 00:00:00 2001 From: Jordan Brown Date: Wed, 1 Sep 2021 10:28:04 -0400 Subject: [PATCH 1/3] Support for describing a job's current stage --- build/Version.props | 8 +- .../Models/Response/JobResponse.cs | 6 + .../Components/Deployment/DreamMaker.cs | 46 +++++-- .../Components/Deployment/IDreamMaker.cs | 10 +- .../Components/Instance.cs | 8 +- .../Components/Repository/IRepository.cs | 40 ++++-- .../Repository/IRepositoryManager.cs | 8 +- .../Components/Repository/Repository.cs | 120 +++++++++++------- .../Repository/RepositoryManager.cs | 4 +- .../Controllers/JobController.cs | 4 +- .../Controllers/RepositoryController.cs | 14 +- src/Tgstation.Server.Host/Jobs/IJobManager.cs | 9 +- .../Jobs/JobEntrypoint.cs | 7 +- src/Tgstation.Server.Host/Jobs/JobHandler.cs | 5 + src/Tgstation.Server.Host/Jobs/JobManager.cs | 24 ++-- .../Jobs/JobProgressReporter.cs | 13 ++ .../Tgstation.Server.Tests/IntegrationTest.cs | 2 +- 17 files changed, 213 insertions(+), 115 deletions(-) create mode 100644 src/Tgstation.Server.Host/Jobs/JobProgressReporter.cs diff --git a/build/Version.props b/build/Version.props index 0d6b6a923c..e527b4fbfb 100644 --- a/build/Version.props +++ b/build/Version.props @@ -3,11 +3,11 @@ - 4.14.1 + 4.15.0 4.0.0 - 9.2.0 - 9.2.0 - 10.2.0 + 9.3.0 + 9.3.0 + 10.3.0 6.0.4 5.3.0 1.1.1 diff --git a/src/Tgstation.Server.Api/Models/Response/JobResponse.cs b/src/Tgstation.Server.Api/Models/Response/JobResponse.cs index 6c04aa919c..eb194af473 100644 --- a/src/Tgstation.Server.Api/Models/Response/JobResponse.cs +++ b/src/Tgstation.Server.Api/Models/Response/JobResponse.cs @@ -21,5 +21,11 @@ /// [ResponseOptions] public int? Progress { get; set; } + + /// + /// Optional description of the job's current . + /// + [ResponseOptions] + public string? Stage { get; set; } } } diff --git a/src/Tgstation.Server.Host/Components/Deployment/DreamMaker.cs b/src/Tgstation.Server.Host/Components/Deployment/DreamMaker.cs index b9e01435b7..408d27f61e 100644 --- a/src/Tgstation.Server.Host/Components/Deployment/DreamMaker.cs +++ b/src/Tgstation.Server.Host/Components/Deployment/DreamMaker.cs @@ -113,6 +113,11 @@ namespace Tgstation.Server.Host.Components.Deployment /// string currentDreamMakerOutput; + /// + /// Current stage to report on the job. + /// + string currentStage; + /// /// If a compile job is running. /// @@ -178,7 +183,7 @@ namespace Tgstation.Server.Host.Components.Deployment public async Task DeploymentProcess( Models.Job job, IDatabaseContextFactory databaseContextFactory, - Action progressReporter, + JobProgressReporter progressReporter, CancellationToken cancellationToken) { if (job == null) @@ -450,7 +455,7 @@ namespace Tgstation.Server.Host.Components.Deployment /// The API validation timeout. /// The . /// The . - /// The progress reporting . + /// The to report progress of the operation. /// The optional estimated of the compilation. /// Whether or not the 's current commit exists on the remote repository. /// The for the operation. @@ -461,7 +466,7 @@ namespace Tgstation.Server.Host.Components.Deployment uint apiValidateTimeout, IRepository repository, IRemoteDeploymentManager remoteDeploymentManager, - Action progressReporter, + JobProgressReporter progressReporter, TimeSpan? estimatedDuration, bool localCommitExistsOnRemote, CancellationToken cancellationToken) @@ -469,7 +474,9 @@ namespace Tgstation.Server.Host.Components.Deployment logger.LogTrace("Begin Compile"); using var progressCts = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken); - var progressTask = estimatedDuration.HasValue ? ProgressTask(progressReporter, estimatedDuration.Value, progressCts.Token) : Task.CompletedTask; + + currentStage = "Reserving BYOND version"; + var progressTask = ProgressTask(progressReporter, estimatedDuration, progressCts.Token); try { using var byondLock = await byond.UseExecutables(null, cancellationToken).ConfigureAwait(false); @@ -490,6 +497,7 @@ namespace Tgstation.Server.Host.Components.Deployment RepositoryOrigin = repository.Origin.ToString(), }; + currentStage = "Creating remote deployment notification"; await remoteDeploymentManager.StartDeployment( repository, job, @@ -525,6 +533,7 @@ namespace Tgstation.Server.Host.Components.Deployment catch (OperationCanceledException) { // DCT: Cancellation token is for job, delaying here is fine + currentStage = "Running CompileCancelled event"; await eventConsumer.HandleEvent(EventType.CompileCancelled, Enumerable.Empty(), default).ConfigureAwait(false); throw; } @@ -561,7 +570,8 @@ namespace Tgstation.Server.Host.Components.Deployment try { // copy the repository - logger.LogTrace("Copying repository to game directory..."); + logger.LogTrace("Copying repository to game directory"); + currentStage = "Copying repository"; var resolvedOutputDirectory = ioManager.ResolvePath(outputDirectory); var repoOrigin = repository.Origin; using (repository) @@ -570,6 +580,7 @@ namespace Tgstation.Server.Host.Components.Deployment // repository closed now // run precompile scripts + currentStage = "Running PreCompile event"; await eventConsumer.HandleEvent( EventType.CompileStart, new List @@ -582,9 +593,10 @@ namespace Tgstation.Server.Host.Components.Deployment .ConfigureAwait(false); // determine the dme + currentStage = "Determining .dme"; if (job.DmeName == null) { - logger.LogTrace("Searching for available .dmes..."); + logger.LogTrace("Searching for available .dmes"); var foundPaths = await ioManager.GetFilesWithExtension(resolvedOutputDirectory, DmeExtension, true, cancellationToken).ConfigureAwait(false); var foundPath = foundPaths.FirstOrDefault(); if (foundPath == default) @@ -603,9 +615,11 @@ namespace Tgstation.Server.Host.Components.Deployment logger.LogDebug("Selected {0}.dme for compilation!", job.DmeName); + currentStage = "Modifying .dme"; await ModifyDme(job, cancellationToken).ConfigureAwait(false); // run precompile scripts + currentStage = "Running PreDreamMaker event"; await eventConsumer.HandleEvent( EventType.PreDreamMaker, new List @@ -618,6 +632,7 @@ namespace Tgstation.Server.Host.Components.Deployment .ConfigureAwait(false); // run compiler + currentStage = "Running DreamMaker"; var exitCode = await RunDreamMaker(byondLock.DreamMakerPath, job, cancellationToken).ConfigureAwait(false); // verify api @@ -628,6 +643,7 @@ namespace Tgstation.Server.Host.Components.Deployment ErrorCode.DreamMakerExitCode, new JobException($"Exit code: {exitCode}{Environment.NewLine}{Environment.NewLine}{job.Output}")); + currentStage = "Validating DMAPI"; await VerifyApi( apiValidateTimeout, dreamMakerSettings.ApiValidationSecurityLevel.Value, @@ -641,6 +657,7 @@ namespace Tgstation.Server.Host.Components.Deployment catch (JobException) { // DD never validated or compile failed + currentStage = "Running CompileFailure event"; await eventConsumer.HandleEvent( EventType.CompileFailure, new List @@ -654,6 +671,7 @@ namespace Tgstation.Server.Host.Components.Deployment throw; } + currentStage = "Running CompileComplete event"; await eventConsumer.HandleEvent( EventType.CompileComplete, new List @@ -665,6 +683,7 @@ namespace Tgstation.Server.Host.Components.Deployment .ConfigureAwait(false); logger.LogTrace("Applying static game file symlinks..."); + currentStage = "Symlinking GameStaticFiles"; // symlink in the static data await configuration.SymlinkStaticFilesTo(resolvedOutputDirectory, cancellationToken).ConfigureAwait(false); @@ -673,6 +692,7 @@ namespace Tgstation.Server.Host.Components.Deployment } catch (Exception ex) { + currentStage = "Cleaning output directory"; await CleanupFailedCompile(job, remoteDeploymentManager, ex).ConfigureAwait(false); throw; } @@ -681,22 +701,22 @@ namespace Tgstation.Server.Host.Components.Deployment /// /// Gradually triggers a given over a given . /// - /// The to report progress. - /// A representing the duration to give progress over. + /// The to report progress of the operation. + /// A representing the duration to give progress over if any. /// The for the operation. /// A representing the running operation. - async Task ProgressTask(Action progressReporter, TimeSpan estimatedDuration, CancellationToken cancellationToken) + async Task ProgressTask(JobProgressReporter progressReporter, TimeSpan? estimatedDuration, CancellationToken cancellationToken) { - progressReporter(0); - var sleepInterval = estimatedDuration / 100; + progressReporter(currentStage, estimatedDuration.HasValue ? 0 : null); + var sleepInterval = estimatedDuration.HasValue ? estimatedDuration.Value / 100 : TimeSpan.FromMilliseconds(250); logger.LogDebug("Compile is expected to take: {0}", estimatedDuration); try { - for (var iteration = 0; iteration < 99; ++iteration) + for (var iteration = 0; iteration < (estimatedDuration.HasValue ? 99 : Int32.MaxValue); ++iteration) { await Task.Delay(sleepInterval, cancellationToken).ConfigureAwait(false); - progressReporter(iteration + 1); + progressReporter(currentStage, estimatedDuration.HasValue ? iteration + 1 : null); } } catch (OperationCanceledException) diff --git a/src/Tgstation.Server.Host/Components/Deployment/IDreamMaker.cs b/src/Tgstation.Server.Host/Components/Deployment/IDreamMaker.cs index 091a5ab750..0f5421691d 100644 --- a/src/Tgstation.Server.Host/Components/Deployment/IDreamMaker.cs +++ b/src/Tgstation.Server.Host/Components/Deployment/IDreamMaker.cs @@ -1,8 +1,8 @@ -using System; -using System.Threading; +using System.Threading; using System.Threading.Tasks; using Tgstation.Server.Host.Database; +using Tgstation.Server.Host.Jobs; using Tgstation.Server.Host.Models; namespace Tgstation.Server.Host.Components.Deployment @@ -13,17 +13,17 @@ namespace Tgstation.Server.Host.Components.Deployment public interface IDreamMaker { /// - /// Create and a compile job and insert it into the database. Meant to be called by a . + /// Create and a compile job and insert it into the database. Meant to be called by a . /// /// The running . /// The for the operation. - /// The to report compilation progress. + /// The to report compilation progress. /// The for the operation. /// A representing the running operation. Task DeploymentProcess( Job job, IDatabaseContextFactory databaseContextFactory, - Action progressReporter, + JobProgressReporter progressReporter, CancellationToken cancellationToken); } } diff --git a/src/Tgstation.Server.Host/Components/Instance.cs b/src/Tgstation.Server.Host/Components/Instance.cs index b591c8f401..3e1f8eab93 100644 --- a/src/Tgstation.Server.Host/Components/Instance.cs +++ b/src/Tgstation.Server.Host/Components/Instance.cs @@ -256,7 +256,7 @@ namespace Tgstation.Server.Host.Components IInstanceCore core, IDatabaseContextFactory databaseContextFactory, Job job, - Action progressReporter, + JobProgressReporter progressReporter, CancellationToken cancellationToken) => databaseContextFactory.UseContext( async databaseContext => @@ -277,11 +277,11 @@ namespace Tgstation.Server.Host.Components const int NumSteps = 3; var doneSteps = 0; - Action NextProgressReporter() + JobProgressReporter NextProgressReporter() { var tmpDoneSteps = doneSteps; ++doneSteps; - return progress => progressReporter((progress + (100 * tmpDoneSteps)) / NumSteps); + return (status, progress) => progressReporter(status, (progress + (100 * tmpDoneSteps)) / NumSteps); } using var repo = await RepositoryManager.LoadRepository(cancellationToken).ConfigureAwait(false); @@ -457,7 +457,7 @@ namespace Tgstation.Server.Host.Components throw; } - progressReporter(5 * ProgressStep); + progressReporter(null, 5 * ProgressStep); }); #pragma warning restore CA1502 // Cyclomatic complexity diff --git a/src/Tgstation.Server.Host/Components/Repository/IRepository.cs b/src/Tgstation.Server.Host/Components/Repository/IRepository.cs index 0ac50e0cf9..73574da2af 100644 --- a/src/Tgstation.Server.Host/Components/Repository/IRepository.cs +++ b/src/Tgstation.Server.Host/Components/Repository/IRepository.cs @@ -3,6 +3,7 @@ using System.Threading; using System.Threading.Tasks; using Tgstation.Server.Api.Models; +using Tgstation.Server.Host.Jobs; namespace Tgstation.Server.Host.Components.Repository { @@ -46,7 +47,7 @@ namespace Tgstation.Server.Host.Components.Repository /// The username used for fetching from submodule repositories. /// The password used for fetching from submodule repositories. /// If a submodule update should be attempted after the merge. - /// to report 0-100 progress of the operation. + /// The to report progress of the operation. /// The for the operation. /// A representing the running operation. Task CheckoutObject( @@ -54,7 +55,7 @@ namespace Tgstation.Server.Host.Components.Repository string username, string password, bool updateSubmodules, - Action progressReporter, + JobProgressReporter progressReporter, CancellationToken cancellationToken); /// @@ -66,7 +67,7 @@ namespace Tgstation.Server.Host.Components.Repository /// The username used to fetch from the origin and submodule repositories. /// The password used to fetch from the origin and submodule repositories. /// If a submodule update should be attempted after the merge. - /// to report 0-100 progress of the operation. + /// The to report progress of the operation. /// The for the operation. /// A resulting in a representing the merge result that is after a fast forward or up to date, on a non-fast-forward, on a conflict. Task AddTestMerge( @@ -76,7 +77,7 @@ namespace Tgstation.Server.Host.Components.Repository string username, string password, bool updateSubmodules, - Action progressReporter, + JobProgressReporter progressReporter, CancellationToken cancellationToken); /// @@ -84,10 +85,14 @@ namespace Tgstation.Server.Host.Components.Repository /// /// The username to fetch from the origin repository. /// The password to fetch from the origin repository. - /// to report 0-100 progress of the operation. + /// The to report progress of the operation. /// The for the operation. /// A representing the running operation. - Task FetchOrigin(string username, string password, Action progressReporter, CancellationToken cancellationToken); + Task FetchOrigin( + string username, + string password, + JobProgressReporter progressReporter, + CancellationToken cancellationToken); /// /// Requires the current HEAD to be a tracked reference. Hard resets the reference to what it tracks on the origin repository. @@ -95,34 +100,34 @@ namespace Tgstation.Server.Host.Components.Repository /// The username used for fetching from submodule repositories. /// The password used for fetching from submodule repositories. /// If a submodule update should be attempted after the merge. - /// to report 0-100 progress of the operation. + /// The to report progress of the operation. /// The for the operation. /// A resulting in the SHA of the new HEAD. Task ResetToOrigin( string username, string password, bool updateSubmodules, - Action progressReporter, + JobProgressReporter progressReporter, CancellationToken cancellationToken); /// /// Requires the current HEAD to be a reference. Hard resets the reference to the given sha. /// /// The sha hash to reset to. - /// to report 0-100 progress of the operation. + /// The to report progress of the operation. /// The for the operation. /// A resulting in the SHA of the new HEAD. - Task ResetToSha(string sha, Action progressReporter, CancellationToken cancellationToken); + Task ResetToSha(string sha, JobProgressReporter progressReporter, CancellationToken cancellationToken); /// /// Requires the current HEAD to be a tracked reference. Merges the reference to what it tracks on the origin repository. /// /// The name of the merge committer. /// The e-mail of the merge committer. - /// to report 0-100 progress of the operation. + /// The to report progress of the operation. /// The for the operation. /// A resulting in a representing the merge result that is after a fast forward, on a merge or up to date, on a conflict. - Task MergeOrigin(string committerName, string committerEmail, Action progressReporter, CancellationToken cancellationToken); + Task MergeOrigin(string committerName, string committerEmail, JobProgressReporter progressReporter, CancellationToken cancellationToken); /// /// Runs the synchronize event script and attempts to push any changes made to the if on a tracked branch. @@ -131,11 +136,18 @@ namespace Tgstation.Server.Host.Components.Repository /// The password to fetch from the origin repository. /// The name of the potential committer. /// The e-mail of the potential committer. - /// to report 0-100 progress of the operation. + /// The to report progress of the operation. /// If the synchronizations should be made to the tracked reference as opposed to a temporary branch. /// The for the operation. /// A resulting in if commits were pushed to the tracked origin reference, otherwise. - Task Sychronize(string username, string password, string committerName, string committerEmail, Action progressReporter, bool synchronizeTrackedBranch, CancellationToken cancellationToken); + Task Sychronize( + string username, + string password, + string committerName, + string committerEmail, + JobProgressReporter progressReporter, + bool synchronizeTrackedBranch, + CancellationToken cancellationToken); /// /// Copies the current working directory to a given . diff --git a/src/Tgstation.Server.Host/Components/Repository/IRepositoryManager.cs b/src/Tgstation.Server.Host/Components/Repository/IRepositoryManager.cs index 9c239ea773..03a28fe41b 100644 --- a/src/Tgstation.Server.Host/Components/Repository/IRepositoryManager.cs +++ b/src/Tgstation.Server.Host/Components/Repository/IRepositoryManager.cs @@ -2,6 +2,8 @@ using System.Threading; using System.Threading.Tasks; +using Tgstation.Server.Host.Jobs; + namespace Tgstation.Server.Host.Components.Repository { /// @@ -15,7 +17,7 @@ namespace Tgstation.Server.Host.Components.Repository bool InUse { get; } /// - /// If a operation is in progress. + /// If a operation is in progress. /// bool CloneInProgress { get; } @@ -33,7 +35,7 @@ namespace Tgstation.Server.Host.Components.Repository /// The branch to clone. /// The username to clone from . /// The password to clone from . - /// A function to report 0-100 progress of the clone. + /// The for progress of the clone. /// If submodules should be recusively cloned and initialized. /// The for the operation. /// The newly cloned , if one already exists. @@ -42,7 +44,7 @@ namespace Tgstation.Server.Host.Components.Repository string initialBranch, string username, string password, - Action progressReporter, + JobProgressReporter progressReporter, bool recurseSubmodules, CancellationToken cancellationToken); diff --git a/src/Tgstation.Server.Host/Components/Repository/Repository.cs b/src/Tgstation.Server.Host/Components/Repository/Repository.cs index a8a1903301..c3f932e4e6 100644 --- a/src/Tgstation.Server.Host/Components/Repository/Repository.cs +++ b/src/Tgstation.Server.Host/Components/Repository/Repository.cs @@ -114,20 +114,22 @@ namespace Tgstation.Server.Host.Components.Repository /// /// Converts a given to a . /// - /// to report 0-100 progress of the operation. + /// The of the operation. + /// The stage argument for . /// A based on . - static CheckoutProgressHandler CheckoutProgressHandler(Action progressReporter) => (a, completedSteps, totalSteps) => progressReporter((int)(((float)completedSteps) / totalSteps * 100)); + static CheckoutProgressHandler CheckoutProgressHandler(JobProgressReporter progressReporter, string stage) => (a, completedSteps, totalSteps) => progressReporter(stage, (int)(((float)completedSteps) / totalSteps * 100)); /// /// Generate a from a given and . /// - /// to report 0-100 progress of the operation. + /// The of the operation. + /// The stage argument for . /// The for the operation. /// A new based on . - static TransferProgressHandler TransferProgressHandler(Action progressReporter, CancellationToken cancellationToken) => (transferProgress) => + static TransferProgressHandler TransferProgressHandler(JobProgressReporter progressReporter, string stage, CancellationToken cancellationToken) => (transferProgress) => { var percentage = 100 * (((float)transferProgress.IndexedObjects + transferProgress.ReceivedObjects) / (transferProgress.TotalObjects * 2)); - progressReporter((int)percentage); + progressReporter(stage, (int)percentage); return !cancellationToken.IsCancellationRequested; }; @@ -204,7 +206,7 @@ namespace Tgstation.Server.Host.Components.Repository string username, string password, bool updateSubmodules, - Action progressReporter, + JobProgressReporter progressReporter, CancellationToken cancellationToken) { if (testMergeParameters == null) @@ -258,7 +260,8 @@ namespace Tgstation.Server.Host.Components.Repository logger.LogTrace("Fetching refspec {0}...", refSpec); var remote = libGitRepo.Network.Remotes.First(); - progressReporter(0); + var stage = $"Fetch {refSpec}"; + progressReporter(stage, 0); commands.Fetch( libGitRepo, refSpecList, @@ -267,7 +270,10 @@ namespace Tgstation.Server.Host.Components.Repository { Prune = true, OnProgress = (a) => !cancellationToken.IsCancellationRequested, - OnTransferProgress = TransferProgressHandler(percentage => progressReporter(percentage / 2), cancellationToken), + OnTransferProgress = TransferProgressHandler( + (lambdaStage, progress) => progressReporter(lambdaStage, progress / 2), + stage, + cancellationToken), OnUpdateTips = (a, b, c) => !cancellationToken.IsCancellationRequested, CredentialsProvider = credentialsProvider.GenerateCredentialsHandler(username, password), }, @@ -299,7 +305,9 @@ namespace Tgstation.Server.Host.Components.Repository FailOnConflict = true, FastForwardStrategy = FastForwardStrategy.NoFastForward, SkipReuc = true, - OnCheckoutProgress = CheckoutProgressHandler(percentage => progressReporter(50 + (percentage / 2))), + OnCheckoutProgress = CheckoutProgressHandler( + (lambdaStage, progress) => progressReporter(lambdaStage, 50 + (progress / 2)), + $"Merge {testMergeParameters.TargetCommitSha}"), }); } finally @@ -354,7 +362,11 @@ namespace Tgstation.Server.Host.Components.Repository .ConfigureAwait(false); if (updateSubmodules) - await UpdateSubmodules(percentage => progressReporter(66 + (percentage / 3)), username, password, cancellationToken).ConfigureAwait(false); + await UpdateSubmodules( + (stage, progress) => progressReporter(stage, 66 + (progress.Value / 3)), + username, + password, + cancellationToken).ConfigureAwait(false); } await eventConsumer.HandleEvent( @@ -378,7 +390,7 @@ namespace Tgstation.Server.Host.Components.Repository string username, string password, bool updateSubmodules, - Action progressReporter, + JobProgressReporter progressReporter, CancellationToken cancellationToken) { if (committish == null) @@ -391,7 +403,10 @@ namespace Tgstation.Server.Host.Components.Repository () => { libGitRepo.RemoveUntrackedFiles(); - RawCheckout(committish, percentage => progressReporter(percentage * (updateSubmodules ? 2 : 3) / 3), cancellationToken); + RawCheckout( + committish, + (stage, progress) => progressReporter(stage, progress * (updateSubmodules ? 2 : 3) / 3), + cancellationToken); }, cancellationToken, DefaultIOManager.BlockingTaskCreationOptions, @@ -399,11 +414,15 @@ namespace Tgstation.Server.Host.Components.Repository .ConfigureAwait(false); if (updateSubmodules) - await UpdateSubmodules(percentage => progressReporter(66 + (percentage / 3)), username, password, cancellationToken).ConfigureAwait(false); + await UpdateSubmodules( + (stage, progress) => progressReporter(stage, 66 + (progress / 3)), + username, + password, + cancellationToken).ConfigureAwait(false); } /// - public async Task FetchOrigin(string username, string password, Action progressReporter, CancellationToken cancellationToken) + public async Task FetchOrigin(string username, string password, JobProgressReporter progressReporter, CancellationToken cancellationToken) { if (progressReporter == null) throw new ArgumentNullException(nameof(progressReporter)); @@ -425,7 +444,7 @@ namespace Tgstation.Server.Host.Components.Repository { Prune = true, OnProgress = (a) => !cancellationToken.IsCancellationRequested, - OnTransferProgress = TransferProgressHandler(progressReporter, cancellationToken), + OnTransferProgress = TransferProgressHandler(progressReporter, "Fetch Origin", cancellationToken), OnUpdateTips = (a, b, c) => !cancellationToken.IsCancellationRequested, CredentialsProvider = credentialsProvider.GenerateCredentialsHandler(username, password), }, @@ -447,7 +466,12 @@ namespace Tgstation.Server.Host.Components.Repository } /// - public async Task ResetToOrigin(string username, string password, bool updateSubmodules, Action progressReporter, CancellationToken cancellationToken) + public async Task ResetToOrigin( + string username, + string password, + bool updateSubmodules, + JobProgressReporter progressReporter, + CancellationToken cancellationToken) { if (progressReporter == null) throw new ArgumentNullException(nameof(progressReporter)); @@ -458,16 +482,16 @@ namespace Tgstation.Server.Host.Components.Repository await eventConsumer.HandleEvent(EventType.RepoResetOrigin, new List { trackedBranch.FriendlyName, trackedBranch.Tip.Sha }, cancellationToken).ConfigureAwait(false); await ResetToSha( trackedBranch.Tip.Sha, - percentage => progressReporter(percentage / (updateSubmodules ? 2 : 1)), + (stage, progress) => progressReporter(stage, progress / (updateSubmodules ? 2 : 1)), cancellationToken) .ConfigureAwait(false); if (updateSubmodules) - await UpdateSubmodules(percentage => progressReporter(50 + (percentage / 2)), username, password, cancellationToken).ConfigureAwait(false); + await UpdateSubmodules((stage, progress) => progressReporter(stage, 50 + (progress / 2)), username, password, cancellationToken).ConfigureAwait(false); } /// - public Task ResetToSha(string sha, Action progressReporter, CancellationToken cancellationToken) => Task.Factory.StartNew( + public Task ResetToSha(string sha, JobProgressReporter progressReporter, CancellationToken cancellationToken) => Task.Factory.StartNew( () => { if (sha == null) @@ -488,7 +512,7 @@ namespace Tgstation.Server.Host.Components.Repository libGitRepo.Reset(ResetMode.Hard, gitObject.Peel(), new CheckoutOptions { - OnCheckoutProgress = CheckoutProgressHandler(progressReporter), + OnCheckoutProgress = CheckoutProgressHandler(progressReporter, $"Reset to {gitObject.Sha}"), }); }, cancellationToken, @@ -520,7 +544,11 @@ namespace Tgstation.Server.Host.Components.Repository TaskScheduler.Current); /// - public async Task MergeOrigin(string committerName, string committerEmail, Action progressReporter, CancellationToken cancellationToken) + public async Task MergeOrigin( + string committerName, + string committerEmail, + JobProgressReporter progressReporter, + CancellationToken cancellationToken) { if (progressReporter == null) throw new ArgumentNullException(nameof(progressReporter)); @@ -553,7 +581,7 @@ namespace Tgstation.Server.Host.Components.Repository FailOnConflict = true, FastForwardStrategy = FastForwardStrategy.Default, SkipReuc = true, - OnCheckoutProgress = CheckoutProgressHandler(progressReporter), + OnCheckoutProgress = CheckoutProgressHandler(progressReporter, "Merge Origin"), }); cancellationToken.ThrowIfCancellationRequested(); @@ -563,7 +591,7 @@ namespace Tgstation.Server.Host.Components.Repository logger.LogDebug("Merge conflict, aborting and reverting to {0}", oldHead.FriendlyName); libGitRepo.Reset(ResetMode.Hard, oldTip, new CheckoutOptions { - OnCheckoutProgress = CheckoutProgressHandler(progressReporter), + OnCheckoutProgress = CheckoutProgressHandler(progressReporter, $"Hard Reset to {oldHead.FriendlyName}"), }); cancellationToken.ThrowIfCancellationRequested(); } @@ -590,7 +618,7 @@ namespace Tgstation.Server.Host.Components.Repository string password, string committerName, string committerEmail, - Action progressReporter, + JobProgressReporter progressReporter, bool synchronizeTrackedBranch, CancellationToken cancellationToken) { @@ -648,7 +676,7 @@ namespace Tgstation.Server.Host.Components.Repository { libGitRepo.Reset(ResetMode.Hard, libGitRepo.Head.Tip, new CheckoutOptions { - OnCheckoutProgress = CheckoutProgressHandler(progress => progressReporter(progress / 10)), + OnCheckoutProgress = CheckoutProgressHandler((stage, progress) => progressReporter(stage, progress / 10), "Hard reset and remove untracked files"), }); cancellationToken.ThrowIfCancellationRequested(); libGitRepo.RemoveUntrackedFiles(); @@ -659,7 +687,7 @@ namespace Tgstation.Server.Host.Components.Repository .ConfigureAwait(false); } - void FinalReporter(int progress) => progressReporter((int)(((float)progress) / 100 * 90)); + void FinalReporter(string stage, int? progress) => progressReporter(stage, (int)(((float)progress) / 100 * 90)); if (!synchronizeTrackedBranch) { @@ -803,19 +831,20 @@ namespace Tgstation.Server.Host.Components.Repository /// Runs a blocking force checkout to . /// /// The committish to checkout. - /// Progress reporter . + /// The for the operation. /// The for the operation. - void RawCheckout(string committish, Action progressReporter, CancellationToken cancellationToken) + void RawCheckout(string committish, JobProgressReporter progressReporter, CancellationToken cancellationToken) { logger.LogTrace("Checkout: {0}", committish); - progressReporter(0); + var stage = $"Checkout {committish}"; + progressReporter(stage, 0); cancellationToken.ThrowIfCancellationRequested(); var checkoutOptions = new CheckoutOptions { CheckoutModifiers = CheckoutModifiers.Force, - OnCheckoutProgress = CheckoutProgressHandler(progressReporter), + OnCheckoutProgress = CheckoutProgressHandler(progressReporter, stage), }; void RunCheckout() => commands.Checkout( @@ -855,10 +884,10 @@ namespace Tgstation.Server.Host.Components.Repository /// /// The username to fetch from the origin repository. /// The password to fetch from the origin repository. - /// to report 0-100 progress of the operation. + /// of the operation. /// The for the operation. /// A representing the running operation. - Task PushHeadToTemporaryBranch(string username, string password, Action progressReporter, CancellationToken cancellationToken) => Task.Factory.StartNew( + Task PushHeadToTemporaryBranch(string username, string password, JobProgressReporter progressReporter, CancellationToken cancellationToken) => Task.Factory.StartNew( () => { logger.LogInformation("Pushing changes to temporary remote branch..."); @@ -870,9 +899,9 @@ namespace Tgstation.Server.Host.Components.Repository try { var forcePushString = String.Format(CultureInfo.InvariantCulture, "+{0}:{0}", branch.CanonicalName); - libGitRepo.Network.Push(remote, forcePushString, GeneratePushOptions(progress => progressReporter((int)(0.9f * progress)), username, password, cancellationToken)); + libGitRepo.Network.Push(remote, forcePushString, GeneratePushOptions((stage, progress) => progressReporter(stage, (int)(0.9f * progress)), username, password, cancellationToken)); var removalString = String.Format(CultureInfo.InvariantCulture, ":{0}", branch.CanonicalName); - libGitRepo.Network.Push(remote, removalString, GeneratePushOptions(progress => progressReporter(90 + (int)(0.1f * progress)), username, password, cancellationToken)); + libGitRepo.Network.Push(remote, removalString, GeneratePushOptions((stage, progress) => progressReporter(stage, 90 + (int)(0.1f * progress)), username, password, cancellationToken)); } catch (UserCancelledException) { @@ -895,23 +924,23 @@ namespace Tgstation.Server.Host.Components.Repository /// /// Generate a standard set of . /// - /// to report 0-100 progress of the operation. + /// of the operation. /// The username for the . /// The password for the . /// The for the operation. /// A new set of . - PushOptions GeneratePushOptions(Action progressReporter, string username, string password, CancellationToken cancellationToken) => new PushOptions + PushOptions GeneratePushOptions(JobProgressReporter progressReporter, string username, string password, CancellationToken cancellationToken) => new PushOptions { OnPackBuilderProgress = (stage, current, total) => { var baseProgress = stage == PackBuilderStage.Counting ? 0 : 25; - progressReporter(baseProgress + ((int)(25 * ((float)current) / total))); + progressReporter("Push", baseProgress + ((int)(25 * ((float)current) / total))); return !cancellationToken.IsCancellationRequested; }, OnNegotiationCompletedBeforePush = (a) => !cancellationToken.IsCancellationRequested, OnPushTransferProgress = (a, sentBytes, totalBytes) => { - progressReporter(50 + ((int)(50 * ((float)sentBytes) / totalBytes))); + progressReporter("Push", 50 + ((int)(50 * ((float)sentBytes) / totalBytes))); return !cancellationToken.IsCancellationRequested; }, CredentialsProvider = credentialsProvider.GenerateCredentialsHandler(username, password), @@ -920,12 +949,12 @@ namespace Tgstation.Server.Host.Components.Repository /// /// Recusively update all s in the . /// - /// to report 0-100 progress of the operation. + /// of the operation. /// The username for the . /// The password for the . /// The for the operation. /// A representing the running operation. - async Task UpdateSubmodules(Action progressReporter, string username, string password, CancellationToken cancellationToken) + async Task UpdateSubmodules(JobProgressReporter progressReporter, string username, string password, CancellationToken cancellationToken) { var submoduleCount = libGitRepo.Submodules.Count(); if (submoduleCount == 0) @@ -940,15 +969,20 @@ namespace Tgstation.Server.Host.Components.Repository var factor = 100 / submoduleCount; foreach (var submodule in libGitRepo.Submodules) { - void LocalProgressReporter(int percentage) => progressReporter((iteration * factor) + (percentage / submoduleCount)); + void LocalProgressReporter(string stage, int percentage) => progressReporter(stage, (iteration * factor) + (percentage / submoduleCount)); var submoduleUpdateOptions = new SubmoduleUpdateOptions { Init = true, - OnTransferProgress = TransferProgressHandler(percentage => LocalProgressReporter(percentage / 2), cancellationToken), + OnTransferProgress = TransferProgressHandler( + (stage, progress) => LocalProgressReporter(stage, progress.Value / 2), + $"Fetch submodule {submodule.Name}", + cancellationToken), OnProgress = output => !cancellationToken.IsCancellationRequested, OnUpdateTips = (a, b, c) => !cancellationToken.IsCancellationRequested, CredentialsProvider = credentialsProvider.GenerateCredentialsHandler(username, password), - OnCheckoutProgress = CheckoutProgressHandler(percentage => LocalProgressReporter(50 + (percentage / 2))), + OnCheckoutProgress = CheckoutProgressHandler( + (stage, progress) => LocalProgressReporter(stage, 50 + (progress.Value / 2)), + $"Checkout submodule {submodule.Name}"), }; logger.LogDebug("Updating submodule {0}...", submodule.Name); diff --git a/src/Tgstation.Server.Host/Components/Repository/RepositoryManager.cs b/src/Tgstation.Server.Host/Components/Repository/RepositoryManager.cs index 449e05b038..41cd9eb7b7 100644 --- a/src/Tgstation.Server.Host/Components/Repository/RepositoryManager.cs +++ b/src/Tgstation.Server.Host/Components/Repository/RepositoryManager.cs @@ -104,7 +104,7 @@ namespace Tgstation.Server.Host.Components.Repository string initialBranch, string username, string password, - Action progressReporter, + JobProgressReporter progressReporter, bool recurseSubmodules, CancellationToken cancellationToken) { @@ -136,7 +136,7 @@ namespace Tgstation.Server.Host.Components.Repository OnTransferProgress = (a) => { var percentage = 100 * (((float)a.IndexedObjects + a.ReceivedObjects) / (a.TotalObjects * 2)); - progressReporter((int)percentage); + progressReporter("Cloning", (int)percentage); return !cancellationToken.IsCancellationRequested; }, RecurseSubmodules = recurseSubmodules, diff --git a/src/Tgstation.Server.Host/Controllers/JobController.cs b/src/Tgstation.Server.Host/Controllers/JobController.cs index fe8e8322a9..02395593ee 100644 --- a/src/Tgstation.Server.Host/Controllers/JobController.cs +++ b/src/Tgstation.Server.Host/Controllers/JobController.cs @@ -167,7 +167,7 @@ namespace Tgstation.Server.Host.Controllers if (job == default) return NotFound(); var api = job.ToApi(); - api.Progress = jobManager.JobProgress(job); + jobManager.SetJobProgress(api); return Json(api); } @@ -178,7 +178,7 @@ namespace Tgstation.Server.Host.Controllers /// A representing the running operation. private Task AddJobProgressResponseTransformer(JobResponse jobResponse) { - jobResponse.Progress = jobManager.JobProgress(jobResponse); + jobManager.SetJobProgress(jobResponse); return Task.CompletedTask; } } diff --git a/src/Tgstation.Server.Host/Controllers/RepositoryController.cs b/src/Tgstation.Server.Host/Controllers/RepositoryController.cs index 33cfe8d614..15c8b8e14a 100644 --- a/src/Tgstation.Server.Host/Controllers/RepositoryController.cs +++ b/src/Tgstation.Server.Host/Controllers/RepositoryController.cs @@ -452,7 +452,7 @@ namespace Tgstation.Server.Host.Controllers async Task RepositoryUpdateJobOhGodPleaseSomeoneRefactorThisItsTooFuckingBig( IInstanceCore instance, IDatabaseContextFactory databaseContextFactory, - Action progressReporter, + JobProgressReporter progressReporter, CancellationToken ct) { var repoManager = instance.RepositoryManager; @@ -478,14 +478,14 @@ namespace Tgstation.Server.Host.Controllers var numSteps = (model.NewTestMerges?.Count ?? 0) + (model.UpdateFromOrigin == true ? 1 : 0) + (!modelHasShaOrReference ? 2 : (hardResettingToOriginReference ? 3 : 1)); var doneSteps = 0; - Action NextProgressReporter() + JobProgressReporter NextProgressReporter() { var tmpDoneSteps = doneSteps; ++doneSteps; - return progress => progressReporter((progress + (100 * tmpDoneSteps)) / numSteps); + return (status, progress) => progressReporter(status, (progress + (100 * tmpDoneSteps)) / numSteps); } - progressReporter(0); + progressReporter(null, 0); // get a base line for where we are Models.RevisionInformation lastRevisionInfo = null; @@ -577,7 +577,7 @@ namespace Tgstation.Server.Host.Controllers postUpdateSha = repo.Head; } else - NextProgressReporter()(100); + NextProgressReporter()(null, 100); } } @@ -612,7 +612,7 @@ namespace Tgstation.Server.Host.Controllers await CallLoadRevInfo().ConfigureAwait(false); // we've either seen origin before or what we're checking out is on origin } else - NextProgressReporter()(100); + NextProgressReporter()(null, 100); if (hardResettingToOriginReference) { @@ -882,7 +882,7 @@ namespace Tgstation.Server.Host.Controllers if (startReference != null && repo.Head != startSha) await repo.ResetToSha(startSha, NextProgressReporter(), default).ConfigureAwait(false); else - progressReporter(100); + progressReporter(null, 100); throw; } } diff --git a/src/Tgstation.Server.Host/Jobs/IJobManager.cs b/src/Tgstation.Server.Host/Jobs/IJobManager.cs index f7cef9993d..b9130deb4d 100644 --- a/src/Tgstation.Server.Host/Jobs/IJobManager.cs +++ b/src/Tgstation.Server.Host/Jobs/IJobManager.cs @@ -2,7 +2,7 @@ using System.Threading.Tasks; using Microsoft.Extensions.Hosting; - +using Tgstation.Server.Api.Models.Response; using Tgstation.Server.Host.Models; namespace Tgstation.Server.Host.Jobs @@ -13,11 +13,10 @@ namespace Tgstation.Server.Host.Jobs public interface IJobManager : IHostedService { /// - /// Get the for a . + /// Set the and for a given . /// - /// The to get for. - /// The of . - int? JobProgress(Api.Models.Internal.Job job); + /// The to update. + void SetJobProgress(JobResponse apiResponse); /// /// Registers a given and begins running it. diff --git a/src/Tgstation.Server.Host/Jobs/JobEntrypoint.cs b/src/Tgstation.Server.Host/Jobs/JobEntrypoint.cs index b2bcc3bc2d..5835675bf6 100644 --- a/src/Tgstation.Server.Host/Jobs/JobEntrypoint.cs +++ b/src/Tgstation.Server.Host/Jobs/JobEntrypoint.cs @@ -1,5 +1,4 @@ -using System; -using System.Threading; +using System.Threading; using System.Threading.Tasks; using Tgstation.Server.Host.Components; @@ -14,13 +13,13 @@ namespace Tgstation.Server.Host.Jobs /// The the job is running on. only when performing an instance move operation. /// The for the operation. /// The running . - /// A that will update the progress of the job. + /// The for the job. /// The for the operation. /// A representing the running operation. public delegate Task JobEntrypoint( IInstanceCore instance, IDatabaseContextFactory databaseContextFactory, Job job, - Action progressReporter, + JobProgressReporter progressReporter, CancellationToken cancellationToken); } diff --git a/src/Tgstation.Server.Host/Jobs/JobHandler.cs b/src/Tgstation.Server.Host/Jobs/JobHandler.cs index b3e40be60d..19941fbefd 100644 --- a/src/Tgstation.Server.Host/Jobs/JobHandler.cs +++ b/src/Tgstation.Server.Host/Jobs/JobHandler.cs @@ -44,6 +44,11 @@ namespace Tgstation.Server.Host.Jobs /// public int? Progress { get; set; } + /// + /// The stage of the job. + /// + public string Stage { get; set; } + /// /// Wait for to complete. /// diff --git a/src/Tgstation.Server.Host/Jobs/JobManager.cs b/src/Tgstation.Server.Host/Jobs/JobManager.cs index d12fbfd894..21b6daacf7 100644 --- a/src/Tgstation.Server.Host/Jobs/JobManager.cs +++ b/src/Tgstation.Server.Host/Jobs/JobManager.cs @@ -7,7 +7,7 @@ using System.Threading.Tasks; using Microsoft.EntityFrameworkCore; using Microsoft.Extensions.Logging; using Serilog.Context; - +using Tgstation.Server.Api.Models.Response; using Tgstation.Server.Host.Components; using Tgstation.Server.Host.Database; using Tgstation.Server.Host.Extensions; @@ -221,15 +221,16 @@ namespace Tgstation.Server.Host.Jobs } /// - public int? JobProgress(Api.Models.Internal.Job job) + public void SetJobProgress(JobResponse apiResponse) { - if (job == null) - throw new ArgumentNullException(nameof(job)); + if (apiResponse == null) + throw new ArgumentNullException(nameof(apiResponse)); lock (synchronizationLock) { - if (!jobs.TryGetValue(job.Id.Value, out var handler)) - return null; - return handler.Progress; + if (!jobs.TryGetValue(apiResponse.Id.Value, out var handler)) + return; + apiResponse.Progress = handler.Progress; + apiResponse.Stage = handler.Stage; } } @@ -293,11 +294,18 @@ namespace Tgstation.Server.Host.Jobs var oldJob = job; job = new Job { Id = oldJob.Id }; - void UpdateProgress(int progress) + void UpdateProgress(string stage, int? progress) { + if (progress.HasValue + && (progress.Value < 0 || progress.Value > 100)) + throw new ArgumentOutOfRangeException(nameof(progress), "Progress must be a value from 0-100!"); + lock (synchronizationLock) if (jobs.TryGetValue(oldJob.Id.Value, out var handler)) + { + handler.Stage = stage; handler.Progress = progress; + } } await activationTcs.Task.WithToken(cancellationToken).ConfigureAwait(false); diff --git a/src/Tgstation.Server.Host/Jobs/JobProgressReporter.cs b/src/Tgstation.Server.Host/Jobs/JobProgressReporter.cs new file mode 100644 index 0000000000..e715659dc9 --- /dev/null +++ b/src/Tgstation.Server.Host/Jobs/JobProgressReporter.cs @@ -0,0 +1,13 @@ +using Tgstation.Server.Host.Models; + +namespace Tgstation.Server.Host.Jobs +{ + /// + /// Progress reporter for a . + /// + /// A description of what the job is currently doing. + /// The progress of the job on a scale from 0-100. + public delegate void JobProgressReporter( + string stage, + int? progress); +} diff --git a/tests/Tgstation.Server.Tests/IntegrationTest.cs b/tests/Tgstation.Server.Tests/IntegrationTest.cs index 1aa222d0bf..3ac21f4037 100644 --- a/tests/Tgstation.Server.Tests/IntegrationTest.cs +++ b/tests/Tgstation.Server.Tests/IntegrationTest.cs @@ -1061,7 +1061,7 @@ namespace Tgstation.Server.Tests () => { }); const string StartSha = "af4da8beb9f9b374b04a3cc4d65acca662e8cc1a"; - await repo.CheckoutObject(StartSha, null, null, true, progress => { }, default); + await repo.CheckoutObject(StartSha, null, null, true, (stage, progress) => { }, default); var result = await repo.ShaIsParent("2f8588a3ca0f6b027704a2a04381215619de3412", default); Assert.IsTrue(result); Assert.AreEqual(StartSha, repo.Head); From 5251ccc297a393b0a633bb3c6c199219fcb294ac Mon Sep 17 00:00:00 2001 From: Jordan Brown Date: Sun, 12 Sep 2021 15:35:07 -0400 Subject: [PATCH 2/3] C# 7 compatibility --- src/Tgstation.Server.Host/Components/Deployment/DreamMaker.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Tgstation.Server.Host/Components/Deployment/DreamMaker.cs b/src/Tgstation.Server.Host/Components/Deployment/DreamMaker.cs index 408d27f61e..790dee9cc9 100644 --- a/src/Tgstation.Server.Host/Components/Deployment/DreamMaker.cs +++ b/src/Tgstation.Server.Host/Components/Deployment/DreamMaker.cs @@ -707,7 +707,7 @@ namespace Tgstation.Server.Host.Components.Deployment /// A representing the running operation. async Task ProgressTask(JobProgressReporter progressReporter, TimeSpan? estimatedDuration, CancellationToken cancellationToken) { - progressReporter(currentStage, estimatedDuration.HasValue ? 0 : null); + progressReporter(currentStage, estimatedDuration.HasValue ? (int?)0 : null); var sleepInterval = estimatedDuration.HasValue ? estimatedDuration.Value / 100 : TimeSpan.FromMilliseconds(250); logger.LogDebug("Compile is expected to take: {0}", estimatedDuration); @@ -716,7 +716,7 @@ namespace Tgstation.Server.Host.Components.Deployment for (var iteration = 0; iteration < (estimatedDuration.HasValue ? 99 : Int32.MaxValue); ++iteration) { await Task.Delay(sleepInterval, cancellationToken).ConfigureAwait(false); - progressReporter(currentStage, estimatedDuration.HasValue ? iteration + 1 : null); + progressReporter(currentStage, estimatedDuration.HasValue ? (int?)(iteration + 1) : null); } } catch (OperationCanceledException) From a6deecdceddd3d0bf399cd0cc20bf9d110b2fc1f Mon Sep 17 00:00:00 2001 From: Jordan Brown Date: Mon, 13 Sep 2021 11:31:35 -0400 Subject: [PATCH 3/3] Clone/restore webpanel during ResolveAssemblyReferences - This lets it happen while VS is loading the project --- src/Tgstation.Server.Host/Tgstation.Server.Host.csproj | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Tgstation.Server.Host/Tgstation.Server.Host.csproj b/src/Tgstation.Server.Host/Tgstation.Server.Host.csproj index 3424ea6dbc..fe8a7385ed 100644 --- a/src/Tgstation.Server.Host/Tgstation.Server.Host.csproj +++ b/src/Tgstation.Server.Host/Tgstation.Server.Host.csproj @@ -1,4 +1,4 @@ - + @@ -26,7 +26,7 @@ ..\.. - +