public override async Task <IActionResult> Update([FromBody] Api.Models.DreamMaker model, CancellationToken cancellationToken) { var hostModel = new DreamMakerSettings { InstanceId = Instance.Id }; DatabaseContext.DreamMakerSettings.Attach(hostModel); if (model.ProjectName != null) { if (!AuthenticationContext.InstanceUser.DreamMakerRights.Value.HasFlag(DreamMakerRights.SetDme)) { return(Forbid()); } if (model.ProjectName.Length == 0) { hostModel.ProjectName = null; } else { hostModel.ProjectName = model.ProjectName; } } if (model.ApiValidationPort.HasValue) { if (!AuthenticationContext.InstanceUser.DreamMakerRights.Value.HasFlag(DreamMakerRights.SetApiValidationPort)) { return(Forbid()); } hostModel.ApiValidationPort = model.ApiValidationPort; } await DatabaseContext.Save(cancellationToken).ConfigureAwait(false); return(await Read(cancellationToken).ConfigureAwait(false)); }
/// <inheritdoc /> #pragma warning disable CA1506 public async Task DeploymentProcess( Models.Job job, IDatabaseContextFactory databaseContextFactory, Action <int> progressReporter, CancellationToken cancellationToken) { #pragma warning disable IDE0016 // Use 'throw' expression if (job == null) { throw new ArgumentNullException(nameof(job)); } #pragma warning restore IDE0016 // Use 'throw' expression 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; DreamMakerSettings dreamMakerSettings = null; IRepository repo = 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); } if (repo.IsGitHubRepository) { repoOwner = repo.GitHubOwner; repoName = repo.GitHubRepoName; } var repoSha = repo.Head; 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, 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, progressReporter, averageSpan, likelyPushedTestMergeCommit, cancellationToken) .ConfigureAwait(false); var activeCompileJob = compileJobConsumer.LatestCompileJob(); try { await databaseContextFactory.UseContext( async databaseContext => { compileJob.Job = new Models.Job { Id = job.Id }; 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; }
/// <inheritdoc /> public async Task <Models.CompileJob> Compile(Models.RevisionInformation revisionInformation, DreamMakerSettings dreamMakerSettings, DreamDaemonSecurity securityLevel, uint apiValidateTimeout, IRepository repository, CancellationToken cancellationToken) { if (revisionInformation == null) { throw new ArgumentNullException(nameof(revisionInformation)); } if (dreamMakerSettings == null) { throw new ArgumentNullException(nameof(dreamMakerSettings)); } if (repository == null) { throw new ArgumentNullException(nameof(repository)); } if (securityLevel == DreamDaemonSecurity.Ultrasafe) { throw new ArgumentOutOfRangeException(nameof(securityLevel), securityLevel, "Cannot compile with ultrasafe security!"); } 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 (Status != CompilerStatus.Idle) { throw new JobException("There is already a compile in progress!"); } Status = CompilerStatus.Copying; } 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); async Task CleanupFailedCompile(bool cancelled) { logger.LogTrace("Cleaning compile directory..."); Status = CompilerStatus.Cleanup; 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); } await chatTask.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); logger.LogTrace("Copying repository to game directory..."); //copy the repository var fullDirA = ioManager.ResolvePath(dirA); var repoOrigin = repository.Origin; using (repository) await repository.CopyTo(fullDirA, cancellationToken).ConfigureAwait(false); Status = CompilerStatus.PreCompile; var resolvedGameDirectory = ioManager.ResolvePath(ioManager.ConcatPath(job.DirectoryName.ToString(), ADirectoryName)); await eventConsumer.HandleEvent(EventType.CompileStart, new List <string> { resolvedGameDirectory, repoOrigin }, cancellationToken).ConfigureAwait(false); Status = CompilerStatus.Modifying; 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); } logger.LogDebug("Selected {0}.dme for compilation!", job.DmeName); await ModifyDme(job, cancellationToken).ConfigureAwait(false); Status = CompilerStatus.Compiling; //run compiler, verify api job.ByondVersion = byondLock.Version.ToString(); var exitCode = await RunDreamMaker(byondLock.DreamMakerPath, job, cancellationToken).ConfigureAwait(false); var apiValidated = false; if (exitCode == 0) { Status = CompilerStatus.Verifying; apiValidated = await VerifyApi(apiValidateTimeout, securityLevel, job, byondLock, dreamMakerSettings.ApiValidationPort.Value, cancellationToken).ConfigureAwait(false); } if (!apiValidated) { //server never validated or compile failed await eventConsumer.HandleEvent(EventType.CompileFailure, new List <string> { resolvedGameDirectory, exitCode == 0 ? "1" : "0" }, cancellationToken).ConfigureAwait(false); throw new JobException(exitCode == 0 ? "Validation of the TGS api failed!" : String.Format(CultureInfo.InvariantCulture, "DM exited with a non-zero code: {0}{1}{2}", exitCode, Environment.NewLine, job.Output)); } logger.LogTrace("Running post compile event..."); Status = CompilerStatus.PostCompile; await eventConsumer.HandleEvent(EventType.CompileComplete, new List <string> { ioManager.ResolvePath(ioManager.ConcatPath(job.DirectoryName.ToString(), ADirectoryName)) }, cancellationToken).ConfigureAwait(false); logger.LogTrace("Duplicating compiled game..."); Status = CompilerStatus.Duplicating; //duplicate the dmb et al await ioManager.CopyDirectory(dirA, dirB, null, cancellationToken).ConfigureAwait(false); logger.LogTrace("Applying static game file symlinks..."); Status = CompilerStatus.Symlinking; //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("Deployment complete! Changes will be applied on next server reboot.", cancellationToken).ConfigureAwait(false); logger.LogDebug("Compile complete!"); return(job); } catch (Exception e) { await CleanupFailedCompile(e is OperationCanceledException).ConfigureAwait(false); throw; } } } catch (OperationCanceledException) { await eventConsumer.HandleEvent(EventType.CompileCancelled, null, default).ConfigureAwait(false); throw; } finally { Status = CompilerStatus.Idle; } }