diff --git a/src/Tgstation.Server.Host/Components/Compiler/DmbFactory.cs b/src/Tgstation.Server.Host/Components/Compiler/DmbFactory.cs index 045e495fa0..c31ab7e326 100644 --- a/src/Tgstation.Server.Host/Components/Compiler/DmbFactory.cs +++ b/src/Tgstation.Server.Host/Components/Compiler/DmbFactory.cs @@ -169,7 +169,7 @@ namespace Tgstation.Server.Host.Components.Compiler public Task StartAsync(CancellationToken cancellationToken) => databaseContextFactory.UseContext(async (db) => { //where complete clause not necessary, only successful COMPILEjobs get in the db - var cj = await db.CompileJobs.Where(x => x.Job.Instance.Id == instance.Id && !x.Job.Cancelled.Value && x.Job.ExceptionDetails == null && x.Job.StoppedAt != null) + var cj = await db.CompileJobs.Where(x => x.Job.Instance.Id == instance.Id) .Include(x => x.Job).ThenInclude(x => x.StartedBy) .Include(x => x.RevisionInformation).ThenInclude(x => x.PrimaryTestMerge).ThenInclude(x => x.MergedBy) .Include(x => x.RevisionInformation).ThenInclude(x => x.ActiveTestMerges).ThenInclude(x => x.TestMerge).ThenInclude(x => x.MergedBy) @@ -245,7 +245,7 @@ namespace Tgstation.Server.Host.Components.Compiler //find the uids of locked directories await databaseContextFactory.UseContext(async db => { - jobUidsToNotErase = await db.CompileJobs.Where(x => x.Job.Instance.Id == instance.Id && jobIdsToSkip.Contains(x.Id) && x.DirectoryName.HasValue).Select(x => x.DirectoryName.Value.ToString().ToUpperInvariant()).ToListAsync(cancellationToken).ConfigureAwait(false); + jobUidsToNotErase = await db.CompileJobs.Where(x => x.Job.Instance.Id == instance.Id && jobIdsToSkip.Contains(x.Id)).Select(x => x.DirectoryName.Value.ToString().ToUpperInvariant()).ToListAsync(cancellationToken).ConfigureAwait(false); }).ConfigureAwait(false); //add the other exemption diff --git a/src/Tgstation.Server.Host/Components/IInstance.cs b/src/Tgstation.Server.Host/Components/IInstance.cs index 8e5b2edf80..921a408266 100644 --- a/src/Tgstation.Server.Host/Components/IInstance.cs +++ b/src/Tgstation.Server.Host/Components/IInstance.cs @@ -1,5 +1,6 @@ using Microsoft.Extensions.Hosting; using System; +using System.Threading; using System.Threading.Tasks; using Tgstation.Server.Host.Components.Byond; using Tgstation.Server.Host.Components.Chat; @@ -25,12 +26,7 @@ namespace Tgstation.Server.Host.Components /// The for the /// IByondManager ByondManager { get; } - - /// - /// The for the - /// - IDreamMaker DreamMaker { get; } - + /// /// The for the /// @@ -41,11 +37,6 @@ namespace Tgstation.Server.Host.Components /// IChat Chat { get; } - /// - /// The for the - /// - ICompileJobConsumer CompileJobConsumer { get; } - /// /// The for the /// @@ -57,12 +48,6 @@ namespace Tgstation.Server.Host.Components /// The latest if it exists CompileJob LatestCompileJob(); - /// - /// Get the associated with the - /// - /// The associated with the - Api.Models.Instance GetMetadata(); - /// /// Rename the /// @@ -75,5 +60,15 @@ namespace Tgstation.Server.Host.Components /// The new auto update inteval /// A representing the running operation Task SetAutoUpdateInterval(uint newInterval); + + /// + /// Run the 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 for the operation + /// A representing the running operation + Task CompileProcess(Job job, IServiceProvider serviceProvider, Action progressReporter, CancellationToken cancellationToken); } } \ No newline at end of file diff --git a/src/Tgstation.Server.Host/Components/Instance.cs b/src/Tgstation.Server.Host/Components/Instance.cs index 21e7edc06d..8f7b0e7700 100644 --- a/src/Tgstation.Server.Host/Components/Instance.cs +++ b/src/Tgstation.Server.Host/Components/Instance.cs @@ -1,9 +1,11 @@ using Microsoft.EntityFrameworkCore; +using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Logging; using System; using System.Linq; using System.Threading; using System.Threading.Tasks; +using Tgstation.Server.Api.Rights; using Tgstation.Server.Host.Components.Byond; using Tgstation.Server.Host.Components.Chat; using Tgstation.Server.Host.Components.Compiler; @@ -48,6 +50,11 @@ namespace Tgstation.Server.Host.Components /// readonly IDmbFactory dmbFactory; + /// + /// The for the + /// + readonly IJobManager jobManager; + /// /// The for the /// @@ -80,8 +87,9 @@ namespace Tgstation.Server.Host.Components /// The value of /// The value of /// The value of + /// The value of /// The value of - public Instance(Api.Models.Instance metadata, IRepositoryManager repositoryManager, IByondManager byondManager, IDreamMaker dreamMaker, IWatchdog watchdog, IChat chat, StaticFiles.IConfiguration configuration, ICompileJobConsumer compileJobConsumer, IDatabaseContextFactory databaseContextFactory, IDmbFactory dmbFactory, ILogger logger) + public Instance(Api.Models.Instance metadata, IRepositoryManager repositoryManager, IByondManager byondManager, IDreamMaker dreamMaker, IWatchdog watchdog, IChat chat, StaticFiles.IConfiguration configuration, ICompileJobConsumer compileJobConsumer, IDatabaseContextFactory databaseContextFactory, IDmbFactory dmbFactory, IJobManager jobManager, ILogger logger) { this.metadata = metadata ?? throw new ArgumentNullException(nameof(metadata)); RepositoryManager = repositoryManager ?? throw new ArgumentNullException(nameof(repositoryManager)); @@ -93,6 +101,7 @@ namespace Tgstation.Server.Host.Components CompileJobConsumer = compileJobConsumer ?? throw new ArgumentNullException(nameof(compileJobConsumer)); this.databaseContextFactory = databaseContextFactory ?? throw new ArgumentNullException(nameof(databaseContextFactory)); this.dmbFactory = dmbFactory ?? throw new ArgumentNullException(nameof(dmbFactory)); + this.jobManager = jobManager ?? throw new ArgumentNullException(nameof(jobManager)); this.logger = logger ?? throw new ArgumentNullException(nameof(logger)); } @@ -107,6 +116,67 @@ namespace Tgstation.Server.Host.Components RepositoryManager.Dispose(); } + /// + public async Task CompileProcess(Job job, IServiceProvider serviceProvider, Action progressReporter, CancellationToken cancellationToken) + { + //DO NOT FOLLOW THE SUGGESTION FOR A THROW EXPRESSION HERE + if (job == null) + throw new ArgumentNullException(nameof(job)); + if (serviceProvider == null) + throw new ArgumentNullException(nameof(serviceProvider)); + if (progressReporter == null) + throw new ArgumentNullException(nameof(progressReporter)); + + var databaseContext = serviceProvider.GetRequiredService(); + + var ddSettingsTask = databaseContext.DreamDaemonSettings.Where(x => x.InstanceId == metadata.Id).Select(x => new DreamDaemonSettings + { + StartupTimeout = x.StartupTimeout, + SecurityLevel = x.SecurityLevel + }).FirstOrDefaultAsync(cancellationToken); + + 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!"); + var ddSettings = await ddSettingsTask.ConfigureAwait(false); + if (ddSettings == default) + throw new JobException("Missing DreamDaemonSettings in DB!"); + + CompileJob compileJob; + RevisionInformation revInfo; + using (var repo = await RepositoryManager.LoadRepository(cancellationToken).ConfigureAwait(false)) + { + if (repo == null) + throw new JobException("Missing Repository!"); + + var repoSha = repo.Head; + revInfo = await databaseContext.RevisionInformations.Where(x => x.CommitSha == repoSha).Include(x => x.ActiveTestMerges).ThenInclude(x => x.TestMerge).FirstOrDefaultAsync().ConfigureAwait(false); + + if (revInfo == default) + { + revInfo = new RevisionInformation + { + CommitSha = repoSha, + OriginCommitSha = repoSha, + Instance = new Models.Instance + { + Id = metadata.Id + } + }; + logger.LogWarning(Repository.Repository.OriginTrackingErrorTemplate, repoSha); + databaseContext.Instances.Attach(revInfo.Instance); + } + + compileJob = await DreamMaker.Compile(revInfo, dreamMakerSettings, ddSettings.SecurityLevel.Value, ddSettings.StartupTimeout.Value, repo, cancellationToken).ConfigureAwait(false); + } + + compileJob.Job = job; + + databaseContext.CompileJobs.Add(compileJob); //will be saved by job context + + job.PostComplete = ct => CompileJobConsumer.LoadCompileJob(compileJob, ct); + } + /// /// Pull the repository and compile for every set of given /// @@ -122,86 +192,98 @@ namespace Tgstation.Server.Host.Components try { - CompileJob job = null; - //need this the whole time - await databaseContextFactory.UseContext(async (db) => + Models.User user = null; + await databaseContextFactory.UseContext(async (db) => user = await db.Users.FirstAsync(cancellationToken).ConfigureAwait(false)).ConfigureAwait(false); + var repositoryUpdateJob = new Job { - //start up queries we'll need in the future - var instanceQuery = db.Instances.Where(x => x.Id == metadata.Id); - var ddSettingsTask = instanceQuery.Select(x => x.DreamDaemonSettings).Select(x => new DreamDaemonSettings + Instance = new Models.Instance { - StartupTimeout = x.StartupTimeout, - SecurityLevel = x.SecurityLevel - }).FirstAsync(cancellationToken); - var dmSettingsTask = instanceQuery.Select(x => x.DreamMakerSettings).FirstAsync(cancellationToken); - var repositorySettingsTask = instanceQuery.Select(x => x.RepositorySettings).FirstAsync(cancellationToken); + Id = metadata.Id + }, + Description = "Scheduled repository update", + CancelRightsType = RightsType.Repository, + CancelRight = (ulong)RepositoryRights.CancelPendingChanges + }; - using (var repo = await RepositoryManager.LoadRepository(cancellationToken).ConfigureAwait(false)) + var noRepo = false; + await jobManager.RegisterOperation(repositoryUpdateJob, async (paramJob, serviceProvider, progressReporter, jobCancellationToken) => + { + var db = serviceProvider.GetRequiredService(); + var repositorySettingsTask = db.RepositorySettings.Where(x => x.InstanceId == metadata.Id).FirstAsync(jobCancellationToken); + + //assume 5 steps with synchronize + const int ProgressSections = 5; + const int ProgressStep = 100 / ProgressSections; + progressReporter(0 * ProgressStep); + + using (var repo = await RepositoryManager.LoadRepository(jobCancellationToken).ConfigureAwait(false)) { if (repo == null) + { + //no repo, no auto updates + noRepo = true; return; + } + progressReporter(1 * ProgressStep); - //start the rev info query - var startSha = repo.Head; - var revInfoTask = instanceQuery.SelectMany(x => x.RevisionInformations).Where(x => x.CommitSha == startSha).FirstOrDefaultAsync(cancellationToken); - - //need repo setting to fetch var repositorySettings = await repositorySettingsTask.ConfigureAwait(false); - await repo.FetchOrigin(repositorySettings.AccessUser, repositorySettings.AccessToken, null, cancellationToken).ConfigureAwait(false); + + const int SecondStepProgress = 2 * ProgressStep; + progressReporter(SecondStepProgress); + + //the main point of auto update is to pull the remote + await repo.FetchOrigin(repositorySettings.AccessUser, repositorySettings.AccessToken, x => progressReporter(SecondStepProgress + (x / ProgressSections)), jobCancellationToken).ConfigureAwait(false); + + progressReporter(3 * ProgressStep); + + var startSha = repo.Head; //take appropriate auto update actions bool shouldSyncTracked; if (repositorySettings.AutoUpdatesKeepTestMerges.Value) { - var result = await repo.MergeOrigin(repositorySettings.CommitterName, repositorySettings.CommitterEmail, cancellationToken).ConfigureAwait(false); + var result = await repo.MergeOrigin(repositorySettings.CommitterName, repositorySettings.CommitterEmail, jobCancellationToken).ConfigureAwait(false); if (!result.HasValue) return; shouldSyncTracked = result.Value; } else { - await repo.ResetToOrigin(cancellationToken).ConfigureAwait(false); + await repo.ResetToOrigin(jobCancellationToken).ConfigureAwait(false); shouldSyncTracked = true; } + progressReporter(4 * ProgressStep); //synch if necessary if (repositorySettings.AutoUpdatesSynchronize.Value && startSha != repo.Head) - await repo.Sychronize(repositorySettings.AccessUser, repositorySettings.AccessToken, shouldSyncTracked, cancellationToken).ConfigureAwait(false); + await repo.Sychronize(repositorySettings.AccessUser, repositorySettings.AccessToken, shouldSyncTracked, jobCancellationToken).ConfigureAwait(false); - //finish other queries - var dmSettings = await dmSettingsTask.ConfigureAwait(false); - var ddSettings = await ddSettingsTask.ConfigureAwait(false); - var revInfo = await revInfoTask.ConfigureAwait(false); - - //null rev info handling - if (revInfo == default) - { - var currentSha = repo.Head; - revInfo = new RevisionInformation - { - CommitSha = currentSha, - OriginCommitSha = currentSha, - Instance = new Models.Instance - { - Id = metadata.Id - } - }; - logger.LogWarning(Repository.Repository.OriginTrackingErrorTemplate, currentSha); - db.Instances.Attach(revInfo.Instance); - } - - //finally start compile - job = await DreamMaker.Compile(revInfo, dmSettings, ddSettings.SecurityLevel.Value, ddSettings.StartupTimeout.Value, repo, cancellationToken).ConfigureAwait(false); + progressReporter(5 * ProgressStep); } + }, cancellationToken).ConfigureAwait(false); - db.CompileJobs.Add(job); - await db.Save(cancellationToken).ConfigureAwait(false); - }).ConfigureAwait(false); + await jobManager.WaitForJobCompletion(repositoryUpdateJob, user, cancellationToken, default).ConfigureAwait(false); - await CompileJobConsumer.LoadCompileJob(job, cancellationToken).ConfigureAwait(false); + if (noRepo) + continue; + + //finally set up the job + var compileProcessJob = new Job + { + StartedBy = user, + Instance = repositoryUpdateJob.Instance, + Description = "Scheduled code deployment", + CancelRightsType = RightsType.DreamMaker, + CancelRight = (ulong)DreamMakerRights.CancelCompile + }; + + await jobManager.RegisterOperation(compileProcessJob, CompileProcess, cancellationToken).ConfigureAwait(false); + + await jobManager.WaitForJobCompletion(compileProcessJob, user, cancellationToken, default).ConfigureAwait(false); } catch (OperationCanceledException) { + logger.LogDebug("Cancelled auto update job!"); throw; } catch (Exception e) @@ -214,11 +296,9 @@ namespace Tgstation.Server.Host.Components { break; } + logger.LogTrace("Leaving auto update loop..."); } - - /// - public Api.Models.Instance GetMetadata() => metadata.CloneMetadata(); - + /// public void Rename(string newName) { @@ -238,7 +318,7 @@ namespace Tgstation.Server.Host.Components CompileJob latestCompileJob = null; await databaseContextFactory.UseContext(async db => { - latestCompileJob = await db.CompileJobs.Where(x => x.Job.Instance.Id == metadata.Id && x.Job.ExceptionDetails == null).OrderByDescending(x => x.Job.StoppedAt).FirstOrDefaultAsync(cancellationToken).ConfigureAwait(false); + latestCompileJob = await db.CompileJobs.Where(x => x.Job.Instance.Id == metadata.Id).OrderByDescending(x => x.Job.StoppedAt).FirstOrDefaultAsync(cancellationToken).ConfigureAwait(false); }).ConfigureAwait(false); await dmbFactory.CleanUnusedCompileJobs(latestCompileJob, cancellationToken).ConfigureAwait(false); } diff --git a/src/Tgstation.Server.Host/Components/InstanceFactory.cs b/src/Tgstation.Server.Host/Components/InstanceFactory.cs index 11c9b9a197..35c0fd23df 100644 --- a/src/Tgstation.Server.Host/Components/InstanceFactory.cs +++ b/src/Tgstation.Server.Host/Components/InstanceFactory.cs @@ -8,7 +8,6 @@ using Tgstation.Server.Host.Components.Chat; using Tgstation.Server.Host.Components.Chat.Commands; using Tgstation.Server.Host.Components.Compiler; using Tgstation.Server.Host.Components.Repository; -using Tgstation.Server.Host.Components.StaticFiles; using Tgstation.Server.Host.Components.Watchdog; using Tgstation.Server.Host.Core; using Tgstation.Server.Host.IO; @@ -89,6 +88,11 @@ namespace Tgstation.Server.Host.Components /// readonly IWatchdogFactory watchdogFactory; + /// + /// The for the + /// + readonly IJobManager jobManager; + /// /// Construct an /// @@ -106,7 +110,8 @@ namespace Tgstation.Server.Host.Components /// The value of /// The value of /// The value of - public InstanceFactory(IIOManager ioManager, IDatabaseContextFactory databaseContextFactory, IApplication application, ILoggerFactory loggerFactory, IByondTopicSender byondTopicSender, IServerControl serverUpdater, ICryptographySuite cryptographySuite, ISynchronousIOManager synchronousIOManager, ISymlinkFactory symlinkFactory, IByondInstaller byondInstaller, IProviderFactory providerFactory, IProcessExecutor processExecutor, IPostWriteHandler postWriteHandler, IWatchdogFactory watchdogFactory) + /// The value of + public InstanceFactory(IIOManager ioManager, IDatabaseContextFactory databaseContextFactory, IApplication application, ILoggerFactory loggerFactory, IByondTopicSender byondTopicSender, IServerControl serverUpdater, ICryptographySuite cryptographySuite, ISynchronousIOManager synchronousIOManager, ISymlinkFactory symlinkFactory, IByondInstaller byondInstaller, IProviderFactory providerFactory, IProcessExecutor processExecutor, IPostWriteHandler postWriteHandler, IWatchdogFactory watchdogFactory, IJobManager jobManager) { this.ioManager = ioManager ?? throw new ArgumentNullException(nameof(ioManager)); this.databaseContextFactory = databaseContextFactory ?? throw new ArgumentNullException(nameof(databaseContextFactory)); @@ -122,6 +127,7 @@ namespace Tgstation.Server.Host.Components this.processExecutor = processExecutor ?? throw new ArgumentNullException(nameof(processExecutor)); this.postWriteHandler = postWriteHandler ?? throw new ArgumentNullException(nameof(postWriteHandler)); this.watchdogFactory = watchdogFactory ?? throw new ArgumentNullException(nameof(watchdogFactory)); + this.jobManager = jobManager ?? throw new ArgumentNullException(nameof(jobManager)); } /// @@ -162,7 +168,7 @@ namespace Tgstation.Server.Host.Components { var dreamMaker = new DreamMaker(byond, gameIoManager, configuration, sessionControllerFactory, dmbFactory, application, eventConsumer, chat, processExecutor, watchdog, loggerFactory.CreateLogger()); - return new Instance(metadata.CloneMetadata(), repoManager, byond, dreamMaker, watchdog, chat, configuration, dmbFactory, databaseContextFactory, dmbFactory, loggerFactory.CreateLogger()); + return new Instance(metadata.CloneMetadata(), repoManager, byond, dreamMaker, watchdog, chat, configuration, dmbFactory, databaseContextFactory, dmbFactory, jobManager, loggerFactory.CreateLogger()); } catch { diff --git a/src/Tgstation.Server.Host/Components/Watchdog/Watchdog.cs b/src/Tgstation.Server.Host/Components/Watchdog/Watchdog.cs index 939661e74d..e51e28e9a2 100644 --- a/src/Tgstation.Server.Host/Components/Watchdog/Watchdog.cs +++ b/src/Tgstation.Server.Host/Components/Watchdog/Watchdog.cs @@ -1,4 +1,5 @@ using Byond.TopicSender; +using Microsoft.EntityFrameworkCore; using Microsoft.Extensions.Logging; using Newtonsoft.Json; using Newtonsoft.Json.Serialization; @@ -807,11 +808,14 @@ namespace Tgstation.Server.Host.Components.Watchdog if (!autoStart) return; + long? adminUserId = null; + + await databaseContextFactory.UseContext(async db => adminUserId = await db.Users.Select(x => x.Id).FirstAsync(cancellationToken).ConfigureAwait(false)).ConfigureAwait(false); var job = new Models.Job { StartedBy = new Models.User { - Id = 1 //just use admin for this cause whatever + Id = adminUserId.Value }, Instance = new Models.Instance { diff --git a/src/Tgstation.Server.Host/Controllers/DreamMakerController.cs b/src/Tgstation.Server.Host/Controllers/DreamMakerController.cs index 823a3700da..b847fef9e2 100644 --- a/src/Tgstation.Server.Host/Controllers/DreamMakerController.cs +++ b/src/Tgstation.Server.Host/Controllers/DreamMakerController.cs @@ -1,6 +1,5 @@ using Microsoft.AspNetCore.Mvc; using Microsoft.EntityFrameworkCore; -using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Logging; using System; using System.Linq; @@ -10,7 +9,6 @@ using System.Threading.Tasks; using Tgstation.Server.Api; using Tgstation.Server.Api.Rights; using Tgstation.Server.Host.Components; -using Tgstation.Server.Host.Components.Repository; using Tgstation.Server.Host.Core; using Tgstation.Server.Host.Models; using Tgstation.Server.Host.Security; @@ -78,7 +76,7 @@ namespace Tgstation.Server.Host.Controllers [TgsAuthorize(DreamMakerRights.CompileJobs)] public override async Task List(CancellationToken cancellationToken) { - var compileJobs = await DatabaseContext.CompileJobs.Where(x => x.Job.Instance.Id == Instance.Id).OrderByDescending(x => x.Job.StartedAt).Select(x => new Api.Models.CompileJob + var compileJobs = await DatabaseContext.CompileJobs.Where(x => x.Job.Instance.Id == Instance.Id).OrderByDescending(x => x.Job.StoppedAt).Select(x => new Api.Models.CompileJob { Id = x.Id }).ToListAsync(cancellationToken).ConfigureAwait(false); @@ -97,7 +95,7 @@ namespace Tgstation.Server.Host.Controllers CancelRight = (ulong)DreamMakerRights.CancelCompile, Instance = Instance }; - await jobManager.RegisterOperation(job, (paramJob, serviceProvider, progressReporter, ct) => RunCompile(paramJob, serviceProvider, Instance, ct), cancellationToken).ConfigureAwait(false); + await jobManager.RegisterOperation(job, instanceManager.GetInstance(Instance).CompileProcess, cancellationToken).ConfigureAwait(false); return Accepted(job.ToApi()); } @@ -129,70 +127,5 @@ namespace Tgstation.Server.Host.Controllers await DatabaseContext.Save(cancellationToken).ConfigureAwait(false); return await Read(cancellationToken).ConfigureAwait(false); } - - /// - /// Run the compile job and insert it into the database - /// - /// The running - /// The for the operation - /// The for the operation - /// The for the operation - /// A representing the running operation - async Task RunCompile(Job job, IServiceProvider serviceProvider, Models.Instance instanceModel, CancellationToken cancellationToken) - { - var instanceManager = serviceProvider.GetRequiredService(); - var databaseContext = serviceProvider.GetRequiredService(); - - var ddSettingsTask = databaseContext.DreamDaemonSettings.Where(x => x.InstanceId == instanceModel.Id).Select(x => new DreamDaemonSettings - { - StartupTimeout = x.StartupTimeout, - SecurityLevel = x.SecurityLevel - }).FirstOrDefaultAsync(cancellationToken); - - - var dreamMakerSettings = await databaseContext.DreamMakerSettings.Where(x => x.InstanceId == instanceModel.Id).FirstAsync(cancellationToken).ConfigureAwait(false); - if (dreamMakerSettings == default) - throw new JobException("Missing DreamMakerSettings in DB!"); - var ddSettings = await ddSettingsTask.ConfigureAwait(false); - if (ddSettings == default) - throw new JobException("Missing DreamDaemonSettings in DB!"); - - var instance = instanceManager.GetInstance(instanceModel); - - CompileJob compileJob; - RevisionInformation revInfo; - using (var repo = await instance.RepositoryManager.LoadRepository(cancellationToken).ConfigureAwait(false)) - { - if (repo == null) - throw new JobException("Missing Repository!"); - - var repoSha = repo.Head; - revInfo = await databaseContext.RevisionInformations.Where(x => x.CommitSha == repoSha).Include(x => x.ActiveTestMerges).ThenInclude(x => x.TestMerge).FirstOrDefaultAsync().ConfigureAwait(false); - - if (revInfo == default) - { - revInfo = new RevisionInformation - { - CommitSha = repoSha, - OriginCommitSha = repoSha, - Instance = new Models.Instance - { - Id = Instance.Id - } - }; - Logger.LogWarning(Repository.OriginTrackingErrorTemplate, repoSha); - databaseContext.Instances.Attach(revInfo.Instance); - } - - compileJob = await instance.DreamMaker.Compile(revInfo, dreamMakerSettings, ddSettings.SecurityLevel.Value, ddSettings.StartupTimeout.Value, repo, cancellationToken).ConfigureAwait(false); - } - - compileJob.Job = job; - - databaseContext.CompileJobs.Add(compileJob); - await databaseContext.Save(cancellationToken).ConfigureAwait(false); - - await instance.CompileJobConsumer.LoadCompileJob(compileJob, cancellationToken).ConfigureAwait(false); - } } } diff --git a/src/Tgstation.Server.Host/Controllers/InstanceController.cs b/src/Tgstation.Server.Host/Controllers/InstanceController.cs index 277677d774..3545f1ce8a 100644 --- a/src/Tgstation.Server.Host/Controllers/InstanceController.cs +++ b/src/Tgstation.Server.Host/Controllers/InstanceController.cs @@ -295,6 +295,7 @@ namespace Tgstation.Server.Host.Controllers } var originalOnline = originalModel.Online.Value; + var renamed = model.Name != null && originalModel.Name != model.Name; if (CheckModified(x => x.AutoUpdateInterval, InstanceManagerRights.SetAutoUpdate) || CheckModified(x => x.ConfigurationType, InstanceManagerRights.SetConfiguration) @@ -311,6 +312,9 @@ namespace Tgstation.Server.Host.Controllers await DatabaseContext.Save(cancellationToken).ConfigureAwait(false); + if (renamed) + instanceManager.GetInstance(originalModel).Rename(originalModel.Name); + var oldAutoStart = originalModel.DreamDaemonSettings.AutoStart; try { diff --git a/src/Tgstation.Server.Host/Core/IJobManager.cs b/src/Tgstation.Server.Host/Core/IJobManager.cs index 0d5a16842e..7389ce78d5 100644 --- a/src/Tgstation.Server.Host/Core/IJobManager.cs +++ b/src/Tgstation.Server.Host/Core/IJobManager.cs @@ -25,6 +25,18 @@ namespace Tgstation.Server.Host.Core /// A representing a running operation Task RegisterOperation(Job job, Func, CancellationToken, Task> operation, CancellationToken cancellationToken); + /// + /// Wait for a given to complete + /// + /// The to wait for + /// The to cancel the + /// A that will cancel the + /// The for the operation + /// A representing the +#pragma warning disable CA1068 // CancellationToken parameters must come last https://github.com/dotnet/roslyn-analyzers/issues/1816 + Task WaitForJobCompletion(Job job, User canceller, CancellationToken jobCancellationToken, CancellationToken cancellationToken); +#pragma warning restore CA1068 // CancellationToken parameters must come last + /// /// Cancels a give /// diff --git a/src/Tgstation.Server.Host/Core/JobManager.cs b/src/Tgstation.Server.Host/Core/JobManager.cs index 47ac244289..cb034f445a 100644 --- a/src/Tgstation.Server.Host/Core/JobManager.cs +++ b/src/Tgstation.Server.Host/Core/JobManager.cs @@ -70,13 +70,35 @@ namespace Tgstation.Server.Host.Core /// The for the operation /// A representing the running operation async Task RunJob(Job job, Func operation, CancellationToken cancellationToken) - { + { try { using (var scope = serviceProvider.CreateScope()) { + async Task HandleExceptions(Task task) + { + try + { + await task.ConfigureAwait(false); + } + catch (OperationCanceledException) + { + logger.LogDebug("Job {0} cancelled!", job.Id); + job.Cancelled = true; + } + catch (Exception e) + { + job.ExceptionDetails = e is JobException ? e.Message : e.ToString(); + logger.LogDebug("Job {0} exited with error! Exception: {1}", job.Id, job.ExceptionDetails); + } + finally + { + job.StoppedAt = DateTimeOffset.Now; + } + } + IDatabaseContext databaseContext = null; - try + async Task RunJobInternal() { var oldJob = job; job = new Job { Id = oldJob.Id }; @@ -86,19 +108,21 @@ namespace Tgstation.Server.Host.Core await operation(job, scope.ServiceProvider, cancellationToken).ConfigureAwait(false); logger.LogDebug("Job {0} completed!", job.Id); - } - catch (OperationCanceledException) - { - logger.LogDebug("Job {0} cancelled!", job.Id); - job.Cancelled = true; - } - catch (Exception e) - { - job.ExceptionDetails = e is JobException ? e.Message : e.ToString(); - logger.LogDebug("Job {0} exited with error! Exception: {1}", job.Id, job.ExceptionDetails); - } - job.StoppedAt = DateTimeOffset.Now; + }; + + await HandleExceptions(RunJobInternal()).ConfigureAwait(false); + await databaseContext.Save(default).ConfigureAwait(false); + + bool JobErroredOrCancelled() => job.ExceptionDetails != null || job.Cancelled.Value; + + //ok so, now it's time for the post commit step if it exists + if (!JobErroredOrCancelled() && job.PostComplete != null) + { + await HandleExceptions(job.PostComplete(cancellationToken)).ConfigureAwait(false); + if (JobErroredOrCancelled()) + await databaseContext.Save(default).ConfigureAwait(false); + } } } finally @@ -223,6 +247,8 @@ namespace Tgstation.Server.Host.Core /// public int? JobProgress(Job job) { + if (job == null) + throw new ArgumentNullException(nameof(job)); lock (this) { if (!jobs.TryGetValue(job.Id, out var handler)) @@ -230,5 +256,26 @@ namespace Tgstation.Server.Host.Core return handler.Progress; } } + + /// + public async Task WaitForJobCompletion(Job job, User canceller, CancellationToken jobCancellationToken, CancellationToken cancellationToken) + { + if (job == null) + throw new ArgumentNullException(nameof(job)); + if (canceller == null) + throw new ArgumentNullException(nameof(canceller)); + JobHandler handler; + lock (this) + { + if (!jobs.TryGetValue(job.Id, out handler)) + return; + } + Task cancelTask = null; + using (jobCancellationToken.Register(() => cancelTask = CancelJob(job, canceller, true, cancellationToken))) + await handler.Wait(cancellationToken).ConfigureAwait(false); + + if (cancelTask != null) + await cancelTask.ConfigureAwait(false); + } } } diff --git a/src/Tgstation.Server.Host/Models/Job.cs b/src/Tgstation.Server.Host/Models/Job.cs index aeb31c0d0e..b601ecc46b 100644 --- a/src/Tgstation.Server.Host/Models/Job.cs +++ b/src/Tgstation.Server.Host/Models/Job.cs @@ -1,4 +1,8 @@ -using System.ComponentModel.DataAnnotations; +using System; +using System.ComponentModel.DataAnnotations; +using System.ComponentModel.DataAnnotations.Schema; +using System.Threading; +using System.Threading.Tasks; namespace Tgstation.Server.Host.Models { @@ -22,6 +26,13 @@ namespace Tgstation.Server.Host.Models [Required] public Instance Instance { get; set; } + /// + /// A to run after the job completes. This will not affect the time, unless it is cancelled or errors + /// + /// This should only be used where there are database dependencies that also rely on the Job itself completing A.K.A. manually initiated s + [NotMapped] + public Func PostComplete { get; set; } + /// /// Convert the to it's API form ///