/// <summary> /// Cleans up a failed compile <paramref name="job"/>. /// </summary> /// <param name="job">The running <see cref="CompileJob"/>.</param> /// <param name="remoteDeploymentManager">The <see cref="IRemoteDeploymentManager"/> associated with the <paramref name="job"/>.</param> /// <param name="exception">The <see cref="Exception"/> that was thrown.</param> /// <returns>A <see cref="Task"/> representing the running operation</returns> async Task CleanupFailedCompile(Models.CompileJob job, IRemoteDeploymentManager remoteDeploymentManager, Exception exception) { async Task CleanDir() { logger.LogTrace("Cleaning compile directory..."); var jobPath = job.DirectoryName.ToString(); try { // DCT: None available await ioManager.DeleteDirectory(jobPath, default).ConfigureAwait(false); } catch (Exception e) { logger.LogWarning(e, "Error cleaning up compile directory {0}!", ioManager.ResolvePath(jobPath)); } } // DCT: None available await Task.WhenAll( CleanDir(), remoteDeploymentManager.FailDeployment( job, FormatExceptionForUsers(exception), default)) .ConfigureAwait(false); }
/// <inheritdoc /> #pragma warning disable CA1506 public async Task DeploymentProcess( Models.Job job, IDatabaseContextFactory databaseContextFactory, Action <int> progressReporter, CancellationToken cancellationToken) { if (job == null) { throw new ArgumentNullException(nameof(job)); } if (databaseContextFactory == null) { throw new ArgumentNullException(nameof(databaseContextFactory)); } if (progressReporter == null) { throw new ArgumentNullException(nameof(progressReporter)); } lock (deploymentLock) { if (deploying) { throw new JobException(ErrorCode.DreamMakerCompileJobInProgress); } deploying = true; } currentChatCallback = null; currentDreamMakerOutput = null; Models.CompileJob compileJob = null; try { string repoOwner = null; string repoName = null; TimeSpan?averageSpan = null; Models.RepositorySettings repositorySettings = null; Models.DreamDaemonSettings ddSettings = null; Models.DreamMakerSettings dreamMakerSettings = null; IRepository repo = null; IRemoteDeploymentManager remoteDeploymentManager = null; Models.RevisionInformation revInfo = null; await databaseContextFactory.UseContext( async databaseContext => { averageSpan = await CalculateExpectedDeploymentTime(databaseContext, cancellationToken).ConfigureAwait(false); ddSettings = await databaseContext .DreamDaemonSettings .AsQueryable() .Where(x => x.InstanceId == metadata.Id) .Select(x => new Models.DreamDaemonSettings { StartupTimeout = x.StartupTimeout, }) .FirstOrDefaultAsync(cancellationToken) .ConfigureAwait(false); if (ddSettings == default) { throw new JobException(ErrorCode.InstanceMissingDreamDaemonSettings); } dreamMakerSettings = await databaseContext .DreamMakerSettings .AsQueryable() .Where(x => x.InstanceId == metadata.Id) .FirstAsync(cancellationToken) .ConfigureAwait(false); if (dreamMakerSettings == default) { throw new JobException(ErrorCode.InstanceMissingDreamMakerSettings); } repositorySettings = await databaseContext .RepositorySettings .AsQueryable() .Where(x => x.InstanceId == metadata.Id) .Select(x => new Models.RepositorySettings { AccessToken = x.AccessToken, ShowTestMergeCommitters = x.ShowTestMergeCommitters, PushTestMergeCommits = x.PushTestMergeCommits, PostTestMergeComment = x.PostTestMergeComment, }) .FirstOrDefaultAsync(cancellationToken) .ConfigureAwait(false); if (repositorySettings == default) { throw new JobException(ErrorCode.InstanceMissingRepositorySettings); } repo = await repositoryManager.LoadRepository(cancellationToken).ConfigureAwait(false); try { if (repo == null) { throw new JobException(ErrorCode.RepoMissing); } remoteDeploymentManager = remoteDeploymentManagerFactory .CreateRemoteDeploymentManager(metadata, repo.RemoteGitProvider.Value); var repoSha = repo.Head; repoOwner = repo.RemoteRepositoryOwner; repoName = repo.RemoteRepositoryName; revInfo = await databaseContext .RevisionInformations .AsQueryable() .Where(x => x.CommitSha == repoSha && x.Instance.Id == metadata.Id) .Include(x => x.ActiveTestMerges) .ThenInclude(x => x.TestMerge) .ThenInclude(x => x.MergedBy) .FirstOrDefaultAsync(cancellationToken) .ConfigureAwait(false); if (revInfo == default) { revInfo = new Models.RevisionInformation { CommitSha = repoSha, Timestamp = await repo.TimestampCommit(repoSha, cancellationToken).ConfigureAwait(false), OriginCommitSha = repoSha, Instance = new Models.Instance { Id = metadata.Id, }, ActiveTestMerges = new List <RevInfoTestMerge>(), }; logger.LogInformation(Repository.Repository.OriginTrackingErrorTemplate, repoSha); databaseContext.Instances.Attach(revInfo.Instance); await databaseContext.Save(cancellationToken).ConfigureAwait(false); } } catch { repo?.Dispose(); throw; } }) .ConfigureAwait(false); var likelyPushedTestMergeCommit = repositorySettings.PushTestMergeCommits.Value && repositorySettings.AccessToken != null && repositorySettings.AccessUser != null; using (repo) compileJob = await Compile( revInfo, dreamMakerSettings, ddSettings.StartupTimeout.Value, repo, remoteDeploymentManager, progressReporter, averageSpan, likelyPushedTestMergeCommit, cancellationToken) .ConfigureAwait(false); var activeCompileJob = compileJobConsumer.LatestCompileJob(); try { await databaseContextFactory.UseContext( async databaseContext => { var fullJob = compileJob.Job; compileJob.Job = new Models.Job { Id = job.Id, }; var fullRevInfo = compileJob.RevisionInformation; compileJob.RevisionInformation = new Models.RevisionInformation { Id = revInfo.Id, }; databaseContext.Jobs.Attach(compileJob.Job); databaseContext.RevisionInformations.Attach(compileJob.RevisionInformation); databaseContext.CompileJobs.Add(compileJob); // The difficulty with compile jobs is they have a two part commit await databaseContext.Save(cancellationToken).ConfigureAwait(false); logger.LogTrace("Created CompileJob {0}", compileJob.Id); try { await compileJobConsumer.LoadCompileJob(compileJob, cancellationToken).ConfigureAwait(false); } catch { // So we need to un-commit the compile job if the above throws databaseContext.CompileJobs.Remove(compileJob); // DCT: Cancellation token is for job, operation must run regardless await databaseContext.Save(default).ConfigureAwait(false); throw; }
/// <summary> /// Executes and populate a given <paramref name="job"/> /// </summary> /// <param name="job">The <see cref="CompileJob"/> to run and populate</param> /// <param name="dreamMakerSettings">The <see cref="Api.Models.Internal.DreamMakerSettings"/> to use</param> /// <param name="byondLock">The <see cref="IByondExecutableLock"/> to use</param> /// <param name="repository">The <see cref="IRepository"/> to use</param> /// <param name="remoteDeploymentManager">The <see cref="IRemoteDeploymentManager"/> to use.</param> /// <param name="apiValidateTimeout">The timeout for validating the DMAPI</param> /// <param name="cancellationToken">The <see cref="CancellationToken"/> for the operation</param> /// <returns>A <see cref="Task"/> representing the running operation</returns> async Task RunCompileJob( Models.CompileJob job, Api.Models.Internal.DreamMakerSettings dreamMakerSettings, IByondExecutableLock byondLock, IRepository repository, IRemoteDeploymentManager remoteDeploymentManager, uint apiValidateTimeout, CancellationToken cancellationToken) { var outputDirectory = job.DirectoryName.ToString(); logger.LogTrace("Compile output GUID: {0}", outputDirectory); try { // copy the repository logger.LogTrace("Copying repository to game directory..."); var resolvedOutputDirectory = ioManager.ResolvePath(outputDirectory); var repoOrigin = repository.Origin; using (repository) await repository.CopyTo(resolvedOutputDirectory, cancellationToken).ConfigureAwait(false); // repository closed now // run precompile scripts await eventConsumer.HandleEvent( EventType.CompileStart, new List <string> { resolvedOutputDirectory, repoOrigin.ToString() }, cancellationToken) .ConfigureAwait(false); // determine the dme if (job.DmeName == null) { logger.LogTrace("Searching for available .dmes..."); var foundPaths = await ioManager.GetFilesWithExtension(resolvedOutputDirectory, DmeExtension, true, cancellationToken).ConfigureAwait(false); var foundPath = foundPaths.FirstOrDefault(); if (foundPath == default) { throw new JobException(ErrorCode.DreamMakerNoDme); } job.DmeName = foundPath.Substring( resolvedOutputDirectory.Length + 1, foundPath.Length - resolvedOutputDirectory.Length - DmeExtension.Length - 2); // +1 for . in extension } else { var targetDme = ioManager.ConcatPath(outputDirectory, String.Join('.', job.DmeName, DmeExtension)); var targetDmeExists = await ioManager.FileExists(targetDme, cancellationToken).ConfigureAwait(false); if (!targetDmeExists) { throw new JobException(ErrorCode.DreamMakerMissingDme); } } 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( ErrorCode.DreamMakerExitCode, new JobException($"Exit code: {exitCode}{Environment.NewLine}{Environment.NewLine}{job.Output}")); } await VerifyApi( apiValidateTimeout, dreamMakerSettings.ApiValidationSecurityLevel.Value, job, byondLock, dreamMakerSettings.ApiValidationPort.Value, dreamMakerSettings.RequireDMApiValidation.Value, cancellationToken) .ConfigureAwait(false); } catch (JobException) { // DD never validated or compile failed await eventConsumer.HandleEvent( EventType.CompileFailure, new List <string> { resolvedOutputDirectory, exitCode == 0 ? "1" : "0" }, cancellationToken) .ConfigureAwait(false); throw; } await eventConsumer.HandleEvent(EventType.CompileComplete, new List <string> { resolvedOutputDirectory }, cancellationToken).ConfigureAwait(false); logger.LogTrace("Applying static game file symlinks..."); // symlink in the static data await configuration.SymlinkStaticFilesTo(resolvedOutputDirectory, cancellationToken).ConfigureAwait(false); logger.LogDebug("Compile complete!"); } catch (Exception ex) { await CleanupFailedCompile(job, remoteDeploymentManager, ex).ConfigureAwait(false); throw; } }
public RemoteDeploymentController(IRemoteDeploymentManager hostManager, UserManager <AppUser> userManager, IAdminLogger logger) : base(userManager, logger) { _manager = hostManager; }