Esempio n. 1
0
        /// <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);
        }
Esempio n. 2
0
        /// <summary>
        /// Adds server side includes to the .dme being compiled
        /// </summary>
        /// <param name="job">The <see cref="Models.CompileJob"/> for the operation</param>
        /// <param name="cancellationToken">The <see cref="CancellationToken"/> for the operation</param>
        /// <returns>A <see cref="Task"/> representing the running operation</returns>
        async Task ModifyDme(Models.CompileJob job, CancellationToken cancellationToken)
        {
            var dirA        = ioManager.ConcatPath(job.DirectoryName.ToString(), ADirectoryName);
            var dmeFileName = String.Join('.', job.DmeName, DmeExtension);
            var dmePath     = ioManager.ConcatPath(dirA, dmeFileName);
            var dmeReadTask = ioManager.ReadAllBytes(dmePath, cancellationToken);

            var dmeModificationsTask = configuration.CopyDMFilesTo(dmeFileName, ioManager.ResolvePath(dirA), cancellationToken);

            var dmeBytes = await dmeReadTask.ConfigureAwait(false);

            var dme = Encoding.UTF8.GetString(dmeBytes);

            var dmeModifications = await dmeModificationsTask.ConfigureAwait(false);

            if (dmeModifications == null || dmeModifications.TotalDmeOverwrite)
            {
                if (dmeModifications != null)
                {
                    logger.LogDebug(".dme replacement configured!");
                }
                else
                {
                    logger.LogTrace("No .dme modifications required.");
                }
                return;
            }

            if (dmeModifications.HeadIncludeLine != null)
            {
                logger.LogDebug("Head .dme include line: {0}", dmeModifications.HeadIncludeLine);
            }
            if (dmeModifications.TailIncludeLine != null)
            {
                logger.LogDebug("Tail .dme include line: {0}", dmeModifications.TailIncludeLine);
            }

            var dmeLines = new List <string>(dme.Split(new[] { Environment.NewLine }, StringSplitOptions.None));

            for (var I = 0; I < dmeLines.Count; ++I)
            {
                var line = dmeLines[I];
                if (line.Contains("BEGIN_INCLUDE", StringComparison.Ordinal) && dmeModifications.HeadIncludeLine != null)
                {
                    dmeLines.Insert(I + 1, dmeModifications.HeadIncludeLine);
                    ++I;
                }
                else if (line.Contains("END_INCLUDE", StringComparison.Ordinal) && dmeModifications.TailIncludeLine != null)
                {
                    dmeLines.Insert(I, dmeModifications.TailIncludeLine);
                    break;
                }
            }

            dmeBytes = Encoding.UTF8.GetBytes(String.Join(Environment.NewLine, dmeLines));
            await ioManager.WriteAllBytes(dmePath, dmeBytes, cancellationToken).ConfigureAwait(false);
        }
Esempio n. 3
0
        /// <summary>
        /// Cleans up a failed compile <paramref name="job"/>
        /// </summary>
        /// <param name="job">The running <see cref="CompileJob"/></param>
        /// <returns>A <see cref="Task"/> representing the running operation</returns>
        async Task CleanupFailedCompile(Models.CompileJob job)
        {
            logger.LogTrace("Cleaning compile directory...");
            var jobPath = job.DirectoryName.ToString();

            try
            {
                await ioManager.DeleteDirectory(jobPath, CancellationToken.None).ConfigureAwait(false);
            }
            catch (Exception e)
            {
                logger.LogWarning("Error cleaning up compile directory {0}! Exception: {1}", ioManager.ResolvePath(jobPath), e);
            }
        }
Esempio n. 4
0
        /// <summary>
        /// Compiles a .dme with DreamMaker
        /// </summary>
        /// <param name="dreamMakerPath">The path to the DreamMaker executable</param>
        /// <param name="job">The <see cref="Models.CompileJob"/> for the operation</param>
        /// <param name="cancellationToken">The <see cref="CancellationToken"/> for the operation</param>
        /// <returns>A <see cref="Task"/> representing the running operation</returns>
        async Task <int> RunDreamMaker(string dreamMakerPath, Models.CompileJob job, CancellationToken cancellationToken)
        {
            using (var dm = processExecutor.LaunchProcess(dreamMakerPath, ioManager.ResolvePath(ioManager.ConcatPath(job.DirectoryName.ToString(), ADirectoryName)), String.Format(CultureInfo.InvariantCulture, "-clean {0}.{1}", job.DmeName, DmeExtension), true, true))
            {
                int exitCode;
                using (cancellationToken.Register(() => dm.Terminate()))
                    exitCode = await dm.Lifetime.ConfigureAwait(false);
                cancellationToken.ThrowIfCancellationRequested();

                logger.LogDebug("DreamMaker exit code: {0}", exitCode);
                job.Output = dm.GetCombinedOutput();
                logger.LogTrace("DreamMaker output: {0}{1}", Environment.NewLine, job.Output);
                return(exitCode);
            }
        }
Esempio n. 5
0
        /// <summary>
        /// Cleans up a failed compile <paramref name="job"/>
        /// </summary>
        /// <param name="job">The running <see cref="Models.CompileJob"/></param>
        /// <param name="cancelled">If the <paramref name="job"/> was cancelled</param>
        /// <param name="cancellationToken">The <see cref="CancellationToken"/> for the operation</param>
        /// <returns>A <see cref="Task"/> representing the running operation</returns>
        async Task CleanupFailedCompile(Models.CompileJob job, bool cancelled, CancellationToken cancellationToken)
        {
            logger.LogTrace("Cleaning compile directory...");
            var chatTask = chat.SendUpdateMessage(cancelled ? "Deploy cancelled!" : "Deploy failed!", cancellationToken);
            var jobPath  = job.DirectoryName.ToString();

            try
            {
                await ioManager.DeleteDirectory(jobPath, CancellationToken.None).ConfigureAwait(false);
            }
            catch (Exception e)
            {
                logger.LogWarning("Error cleaning up compile directory {0}! Exception: {1}", ioManager.ResolvePath(jobPath), e);
            }

            await chatTask.ConfigureAwait(false);
        }
Esempio n. 6
0
        /// <summary>
        /// Run a quick DD instance to test the DMAPI is installed on the target code
        /// </summary>
        /// <param name="timeout">The timeout in seconds for validation</param>
        /// <param name="securityLevel">The <see cref="DreamDaemonSecurity"/> level to use to validate the API</param>
        /// <param name="job">The <see cref="Models.CompileJob"/> for the operation</param>
        /// <param name="byondLock">The current <see cref="IByondExecutableLock"/></param>
        /// <param name="portToUse">The port to use for API validation</param>
        /// <param name="cancellationToken">The <see cref="CancellationToken"/> for the operation</param>
        /// <returns>A <see cref="Task{TResult}"/> resulting in <see langword="true"/> if the DMAPI was successfully validated, <see langword="false"/> otherwise</returns>
        async Task <bool> VerifyApi(uint timeout, DreamDaemonSecurity securityLevel, Models.CompileJob job, IByondExecutableLock byondLock, ushort portToUse, CancellationToken cancellationToken)
        {
            logger.LogTrace("Verifying DMAPI...");
            var launchParameters = new DreamDaemonLaunchParameters
            {
                AllowWebClient = false,
                PrimaryPort    = portToUse,
                SecurityLevel  = securityLevel,                   //all it needs to read the file and exit
                StartupTimeout = timeout
            };

            var dirA     = ioManager.ConcatPath(job.DirectoryName.ToString(), ADirectoryName);
            var provider = new TemporaryDmbProvider(ioManager.ResolvePath(dirA), String.Concat(job.DmeName, DmbExtension), job);

            var timeoutAt = DateTimeOffset.Now.AddSeconds(timeout);

            using (var controller = await sessionControllerFactory.LaunchNew(launchParameters, provider, byondLock, true, true, true, cancellationToken).ConfigureAwait(false))
            {
                var launchResult = await controller.LaunchResult.ConfigureAwait(false);

                var now = DateTimeOffset.Now;
                if (now < timeoutAt && launchResult.StartupTime.HasValue)
                {
                    var timeoutTask = Task.Delay(timeoutAt - now, cancellationToken);

                    await Task.WhenAny(controller.Lifetime, timeoutTask).ConfigureAwait(false);

                    cancellationToken.ThrowIfCancellationRequested();
                }

                if (!controller.Lifetime.IsCompleted)
                {
                    logger.LogDebug("API validation timed out!");
                    return(false);
                }

                var validated = controller.ApiValidated;
                logger.LogTrace("API valid: {0}", validated);
                return(validated);
            }
        }
Esempio n. 7
0
        /// <summary>
        /// Compiles a .dme with DreamMaker
        /// </summary>
        /// <param name="dreamMakerPath">The path to the DreamMaker executable</param>
        /// <param name="job">The <see cref="CompileJob"/> for the operation</param>
        /// <param name="cancellationToken">The <see cref="CancellationToken"/> for the operation</param>
        /// <returns>A <see cref="Task"/> representing the running operation</returns>
        async Task <int> RunDreamMaker(string dreamMakerPath, Models.CompileJob job, CancellationToken cancellationToken)
        {
            using var dm = processExecutor.LaunchProcess(
                      dreamMakerPath,
                      ioManager.ResolvePath(
                          job.DirectoryName.ToString()),
                      $"-clean {job.DmeName}.{DmeExtension}",
                      true,
                      true,
                      true);
            int exitCode;

            using (cancellationToken.Register(() => dm.Terminate()))
                exitCode = await dm.Lifetime.ConfigureAwait(false);
            cancellationToken.ThrowIfCancellationRequested();

            logger.LogDebug("DreamMaker exit code: {0}", exitCode);
            job.Output = await dm.GetCombinedOutput(cancellationToken).ConfigureAwait(false);

            currentDreamMakerOutput = job.Output;
            logger.LogDebug("DreamMaker output: {0}{1}", Environment.NewLine, job.Output);
            return(exitCode);
        }
Esempio n. 8
0
        /// <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;
                        }
Esempio n. 9
0
        /// <inheritdoc />
        public async Task <Models.CompileJob> Compile(Models.RevisionInformation revisionInformation, Api.Models.DreamMaker dreamMakerSettings, uint apiValidateTimeout, IRepository repository, Action <int> progressReporter, TimeSpan?estimatedDuration, 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 (progressReporter == null)
            {
                throw new ArgumentNullException(nameof(progressReporter));
            }

            if (dreamMakerSettings.ApiValidationSecurityLevel == DreamDaemonSecurity.Ultrasafe)
            {
                throw new ArgumentOutOfRangeException(nameof(dreamMakerSettings), dreamMakerSettings, "Cannot compile with ultrasafe security!");
            }

            logger.LogTrace("Begin Compile");

            lock (this)
            {
                if (compiling)
                {
                    throw new JobException("There is already a compile job in progress!");
                }
                compiling = true;
            }

            using (var progressCts = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken))
            {
                var progressTask = estimatedDuration.HasValue ? ProgressTask(progressReporter, estimatedDuration.Value, cancellationToken) : Task.CompletedTask;
                try
                {
                    using (var byondLock = await byond.UseExecutables(null, cancellationToken).ConfigureAwait(false))
                    {
                        await SendDeploymentMessage(revisionInformation, byondLock, cancellationToken).ConfigureAwait(false);

                        var job = new Models.CompileJob
                        {
                            DirectoryName       = Guid.NewGuid(),
                            DmeName             = dreamMakerSettings.ProjectName,
                            RevisionInformation = revisionInformation,
                            ByondVersion        = byondLock.Version.ToString()
                        };

                        await RunCompileJob(job, dreamMakerSettings, byondLock, repository, apiValidateTimeout, cancellationToken).ConfigureAwait(false);

                        return(job);
                    }
                }
                catch (OperationCanceledException)
                {
                    await eventConsumer.HandleEvent(EventType.CompileCancelled, null, default).ConfigureAwait(false);

                    throw;
                }
                finally
                {
                    compiling = false;
                    progressCts.Cancel();
                    await progressTask.ConfigureAwait(false);
                }
            }
        }
Esempio n. 10
0
        /// <summary>
        /// Executes and populate a given <paramref name="job"/>
        /// </summary>
        /// <param name="job">The <see cref="Models.CompileJob"/> to run and populate</param>
        /// <param name="dreamMakerSettings">The <see cref="Api.Models.DreamMaker"/> settings 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="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.DreamMaker dreamMakerSettings, IByondExecutableLock byondLock, IRepository repository, uint apiValidateTimeout, CancellationToken cancellationToken)
        {
            var jobPath = job.DirectoryName.ToString();

            logger.LogTrace("Compile output GUID: {0}", jobPath);

            try
            {
                var dirA = ioManager.ConcatPath(jobPath, ADirectoryName);
                var dirB = ioManager.ConcatPath(jobPath, BDirectoryName);

                // copy the repository
                logger.LogTrace("Copying repository to game directory...");
                var resolvedADirectory = ioManager.ResolvePath(dirA);
                var repoOrigin         = repository.Origin;
                using (repository)
                    await repository.CopyTo(resolvedADirectory, cancellationToken).ConfigureAwait(false);

                // repository closed now

                // run precompile scripts
                await eventConsumer.HandleEvent(EventType.CompileStart, new List <string> {
                    resolvedADirectory, repoOrigin
                }, cancellationToken).ConfigureAwait(false);

                // determine the dme
                if (job.DmeName == null)
                {
                    logger.LogTrace("Searching for available .dmes...");
                    var foundPaths = await ioManager.GetFilesWithExtension(dirA, DmeExtension, cancellationToken).ConfigureAwait(false);

                    var foundPath = foundPaths.FirstOrDefault();
                    if (foundPath == default)
                    {
                        throw new JobException("Unable to find any .dme!");
                    }
                    var dmeWithExtension = ioManager.GetFileName(foundPath);
                    job.DmeName = dmeWithExtension.Substring(0, dmeWithExtension.Length - DmeExtension.Length - 1);
                }
                else
                {
                    var targetDme       = ioManager.ConcatPath(dirA, String.Join('.', job.DmeName, DmeExtension));
                    var targetDmeExists = await ioManager.FileExists(targetDme, cancellationToken).ConfigureAwait(false);

                    if (!targetDmeExists)
                    {
                        throw new JobException("Unable to locate specified .dme!");
                    }
                }

                logger.LogDebug("Selected {0}.dme for compilation!", job.DmeName);

                await ModifyDme(job, cancellationToken).ConfigureAwait(false);

                // run compiler
                var exitCode = await RunDreamMaker(byondLock.DreamMakerPath, job, cancellationToken).ConfigureAwait(false);

                // verify api
                try
                {
                    if (exitCode != 0)
                    {
                        throw new JobException(String.Format(CultureInfo.InvariantCulture, "DM exited with a non-zero code: {0}{1}{2}", exitCode, Environment.NewLine, job.Output));
                    }

                    await VerifyApi(apiValidateTimeout, dreamMakerSettings.ApiValidationSecurityLevel.Value, job, byondLock, dreamMakerSettings.ApiValidationPort.Value, cancellationToken).ConfigureAwait(false);
                }
                catch (JobException)
                {
                    // DD never validated or compile failed
                    await eventConsumer.HandleEvent(EventType.CompileFailure, new List <string> {
                        resolvedADirectory, exitCode == 0 ? "1" : "0"
                    }, cancellationToken).ConfigureAwait(false);

                    throw;
                }

                logger.LogTrace("Running post compile event...");
                await eventConsumer.HandleEvent(EventType.CompileComplete, new List <string> {
                    resolvedADirectory
                }, cancellationToken).ConfigureAwait(false);

                logger.LogTrace("Duplicating compiled game...");

                // duplicate the dmb et al
                await ioManager.CopyDirectory(dirA, dirB, null, cancellationToken).ConfigureAwait(false);

                logger.LogTrace("Applying static game file symlinks...");

                // symlink in the static data
                var symATask = configuration.SymlinkStaticFilesTo(resolvedADirectory, cancellationToken);
                var symBTask = configuration.SymlinkStaticFilesTo(ioManager.ResolvePath(dirB), cancellationToken);

                await Task.WhenAll(symATask, symBTask).ConfigureAwait(false);

                await chat.SendUpdateMessage(String.Format(CultureInfo.InvariantCulture, "Deployment complete!{0}", watchdog.Running ? " Changes will be applied on next server reboot." : String.Empty), cancellationToken).ConfigureAwait(false);

                logger.LogDebug("Compile complete!");
            }
            catch (Exception e)
            {
                await CleanupFailedCompile(job, e is OperationCanceledException, cancellationToken).ConfigureAwait(false);

                throw;
            }
        }
Esempio n. 11
0
        /// <summary>
        /// Run a quick DD instance to test the DMAPI is installed on the target code
        /// </summary>
        /// <param name="timeout">The timeout in seconds for validation</param>
        /// <param name="securityLevel">The <see cref="DreamDaemonSecurity"/> level to use to validate the API</param>
        /// <param name="job">The <see cref="Models.CompileJob"/> for the operation</param>
        /// <param name="byondLock">The current <see cref="IByondExecutableLock"/></param>
        /// <param name="portToUse">The port to use for API validation</param>
        /// <param name="cancellationToken">The <see cref="CancellationToken"/> for the operation</param>
        /// <returns>A <see cref="Task"/> representing the running operation</returns>
        async Task VerifyApi(uint timeout, DreamDaemonSecurity securityLevel, Models.CompileJob job, IByondExecutableLock byondLock, ushort portToUse, CancellationToken cancellationToken)
        {
            logger.LogTrace("Verifying DMAPI...");
            var launchParameters = new DreamDaemonLaunchParameters
            {
                AllowWebClient = false,
                PrimaryPort    = portToUse,
                SecurityLevel  = securityLevel,
                StartupTimeout = timeout
            };

            var dirA = ioManager.ConcatPath(job.DirectoryName.ToString(), ADirectoryName);

            job.MinimumSecurityLevel = securityLevel;             // needed for the TempDmbProvider
            var timeoutAt = DateTimeOffset.Now.AddSeconds(timeout);

            using (var provider = new TemporaryDmbProvider(ioManager.ResolvePath(dirA), String.Concat(job.DmeName, DmbExtension), job))
                using (var controller = await sessionControllerFactory.LaunchNew(launchParameters, provider, byondLock, true, true, true, cancellationToken).ConfigureAwait(false))
                {
                    var launchResult = await controller.LaunchResult.ConfigureAwait(false);

                    var now = DateTimeOffset.Now;
                    if (now < timeoutAt && launchResult.StartupTime.HasValue)
                    {
                        var timeoutTask = Task.Delay(timeoutAt - now, cancellationToken);

                        await Task.WhenAny(controller.Lifetime, timeoutTask).ConfigureAwait(false);

                        cancellationToken.ThrowIfCancellationRequested();
                    }

                    if (controller.Lifetime.IsCompleted)
                    {
                        var validationStatus = controller.ApiValidationStatus;
                        logger.LogTrace("API validation status: {0}", validationStatus);
                        switch (validationStatus)
                        {
                        case ApiValidationStatus.RequiresUltrasafe:
                            job.MinimumSecurityLevel = DreamDaemonSecurity.Ultrasafe;
                            return;

                        case ApiValidationStatus.RequiresSafe:
                            if (securityLevel == DreamDaemonSecurity.Ultrasafe)
                            {
                                throw new JobException("This game must be run with at least the 'Safe' DreamDaemon security level!");
                            }
                            job.MinimumSecurityLevel = DreamDaemonSecurity.Safe;
                            return;

                        case ApiValidationStatus.RequiresTrusted:
                            if (securityLevel != DreamDaemonSecurity.Trusted)
                            {
                                throw new JobException("This game must be run with at least the 'Trusted' DreamDaemon security level!");
                            }
                            job.MinimumSecurityLevel = DreamDaemonSecurity.Trusted;
                            return;

                        case ApiValidationStatus.NeverValidated:
                            break;

                        case ApiValidationStatus.BadValidationRequest:
                            throw new JobException("Recieved an unrecognized API validation request from DreamDaemon!");

                        case ApiValidationStatus.UnaskedValidationRequest:
                        default:
                            throw new InvalidOperationException(String.Format(CultureInfo.InvariantCulture, "Session controller returned unexpected ApiValidationStatus: {0}", validationStatus));
                        }
                    }

                    throw new JobException("DMAPI validation timed out!");
                }
        }
Esempio n. 12
0
        /// <inheritdoc />
        public async Task <Models.CompileJob> Compile(Models.RevisionInformation revisionInformation, Api.Models.DreamMaker dreamMakerSettings, uint apiValidateTimeout, IRepository repository, Action <int> progressReporter, TimeSpan?estimatedDuration, 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 (progressReporter == null)
            {
                throw new ArgumentNullException(nameof(progressReporter));
            }

            if (dreamMakerSettings.ApiValidationSecurityLevel == DreamDaemonSecurity.Ultrasafe)
            {
                throw new ArgumentOutOfRangeException(nameof(dreamMakerSettings), dreamMakerSettings, "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 (compiling)
                {
                    throw new JobException("There is already a compile job in progress!");
                }
                compiling = true;
            }

            using (var progressCts = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken))
            {
                async Task ProgressTask()
                {
                    if (!estimatedDuration.HasValue)
                    {
                        return;
                    }

                    progressReporter(0);
                    var ct            = progressCts.Token;
                    var sleepInterval = estimatedDuration.Value / 100;

                    try
                    {
                        for (var I = 0; I < 99; ++I)
                        {
                            await Task.Delay(sleepInterval, progressCts.Token).ConfigureAwait(false);

                            progressReporter(I + 1);
                        }
                    }
                    catch (OperationCanceledException) { }
                }

                var progressTask = ProgressTask();
                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...");
                            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);

                            //run precompile scripts
                            var resolvedGameDirectory = ioManager.ResolvePath(ioManager.ConcatPath(job.DirectoryName.ToString(), ADirectoryName));
                            await eventConsumer.HandleEvent(EventType.CompileStart, new List <string> {
                                resolvedGameDirectory, repoOrigin
                            }, cancellationToken).ConfigureAwait(false);

                            //determine the dme
                            if (job.DmeName == null)
                            {
                                logger.LogTrace("Searching for available .dmes...");
                                var path = (await ioManager.GetFilesWithExtension(dirA, DmeExtension, cancellationToken).ConfigureAwait(false)).FirstOrDefault();
                                if (path == default)
                                {
                                    throw new JobException("Unable to find any .dme!");
                                }
                                var dmeWithExtension = ioManager.GetFileName(path);
                                job.DmeName = dmeWithExtension.Substring(0, dmeWithExtension.Length - DmeExtension.Length - 1);
                            }
                            else if (!await ioManager.FileExists(ioManager.ConcatPath(dirA, String.Join('.', job.DmeName, DmeExtension)), cancellationToken).ConfigureAwait(false))
                            {
                                throw new JobException("Unable to locate specified .dme!");
                            }

                            logger.LogDebug("Selected {0}.dme for compilation!", job.DmeName);

                            await ModifyDme(job, cancellationToken).ConfigureAwait(false);

                            //run compiler, verify api
                            job.ByondVersion = byondLock.Version.ToString();

                            var exitCode = await RunDreamMaker(byondLock.DreamMakerPath, job, cancellationToken).ConfigureAwait(false);

                            try
                            {
                                if (exitCode != 0)
                                {
                                    throw new JobException(String.Format(CultureInfo.InvariantCulture, "DM exited with a non-zero code: {0}{1}{2}", exitCode, Environment.NewLine, job.Output));
                                }

                                await VerifyApi(apiValidateTimeout, dreamMakerSettings.ApiValidationSecurityLevel.Value, job, byondLock, dreamMakerSettings.ApiValidationPort.Value, cancellationToken).ConfigureAwait(false);
                            }
                            catch (JobException)
                            {
                                //server never validated or compile failed
                                await eventConsumer.HandleEvent(EventType.CompileFailure, new List <string> {
                                    resolvedGameDirectory, exitCode == 0 ? "1" : "0"
                                }, cancellationToken).ConfigureAwait(false);

                                throw;
                            }

                            logger.LogTrace("Running post compile event...");
                            await eventConsumer.HandleEvent(EventType.CompileComplete, new List <string> {
                                ioManager.ResolvePath(ioManager.ConcatPath(job.DirectoryName.ToString(), ADirectoryName))
                            }, cancellationToken).ConfigureAwait(false);

                            logger.LogTrace("Duplicating compiled game...");

                            //duplicate the dmb et al
                            await ioManager.CopyDirectory(dirA, dirB, null, cancellationToken).ConfigureAwait(false);

                            logger.LogTrace("Applying static game file symlinks...");

                            //symlink in the static data
                            var symATask = configuration.SymlinkStaticFilesTo(fullDirA, cancellationToken);
                            var symBTask = configuration.SymlinkStaticFilesTo(ioManager.ResolvePath(dirB), cancellationToken);

                            await Task.WhenAll(symATask, symBTask).ConfigureAwait(false);

                            await chat.SendUpdateMessage(String.Format(CultureInfo.InvariantCulture, "Deployment complete!{0}", watchdog.Running ? " Changes will be applied on next server reboot." : String.Empty), cancellationToken).ConfigureAwait(false);

                            logger.LogDebug("Compile complete!");
                            return(job);
                        }
                        catch (Exception e)
                        {
                            await CleanupFailedCompile(e is OperationCanceledException).ConfigureAwait(false);

                            throw;
                        }
                    }
                }
                catch (OperationCanceledException)
                {
                    await eventConsumer.HandleEvent(EventType.CompileCancelled, null, default).ConfigureAwait(false);

                    throw;
                }
                finally
                {
                    compiling = false;
                    progressCts.Cancel();
                    await progressTask.ConfigureAwait(false);
                }
            }
        }
        public async Task TestSuccessfulLaunchAndShutdown()
        {
            var mockChat = new Mock <IChat>();

            mockChat.Setup(x => x.RegisterCommandHandler(It.IsNotNull <ICustomCommandHandler>())).Verifiable();
            var mockSessionControllerFactory = new Mock <ISessionControllerFactory>();
            var mockDmbFactory             = new Mock <IDmbFactory>();
            var mockLogger                 = new Mock <ILogger <ExperimentalWatchdog> >();
            var mockReattachInfoHandler    = new Mock <IReattachInfoHandler>();
            var mockDatabaseContextFactory = new Mock <IDatabaseContextFactory>();
            var mockByondTopicSender       = new Mock <IByondTopicSender>();
            var mockEventConsumer          = new Mock <IEventConsumer>();
            var mockJobManager             = new Mock <IJobManager>();
            var mockRestartRegistration    = new Mock <IRestartRegistration>();

            mockRestartRegistration.Setup(x => x.Dispose()).Verifiable();
            var mockServerControl = new Mock <IServerControl>();

            mockServerControl.Setup(x => x.RegisterForRestart(It.IsNotNull <IRestartHandler>())).Returns(mockRestartRegistration.Object).Verifiable();
            var mockLaunchParameters = new DreamDaemonLaunchParameters();
            var mockInstance         = new Models.Instance();
            var mockAsyncDelayer     = new Mock <IAsyncDelayer>();

            using (var wd = new ExperimentalWatchdog(mockChat.Object, mockSessionControllerFactory.Object, mockDmbFactory.Object, mockReattachInfoHandler.Object, mockDatabaseContextFactory.Object, mockByondTopicSender.Object, mockEventConsumer.Object, mockJobManager.Object, mockServerControl.Object, mockAsyncDelayer.Object, mockLogger.Object, mockLaunchParameters, mockInstance, default))
                using (var cts = new CancellationTokenSource())
                {
                    var mockCompileJob  = new Models.CompileJob();
                    var mockDmbProvider = new Mock <IDmbProvider>();
                    mockDmbProvider.SetupGet(x => x.CompileJob).Returns(mockCompileJob).Verifiable();
                    var mDmbP = mockDmbProvider.Object;

                    var infiniteTask = new TaskCompletionSource <int>().Task;

                    mockDmbFactory.SetupGet(x => x.OnNewerDmb).Returns(infiniteTask);
                    mockDmbFactory.SetupGet(x => x.DmbAvailable).Returns(true).Verifiable();
                    mockDmbFactory.Setup(x => x.LockNextDmb(2)).Returns(mDmbP).Verifiable();

                    var sessionsToVerify = new List <Mock <ISessionController> >();

                    var cancellationToken = cts.Token;
                    mockSessionControllerFactory.Setup(x => x.LaunchNew(mockLaunchParameters, mDmbP, null, It.IsAny <bool>(), It.IsAny <bool>(), false, cancellationToken)).Returns(() =>
                    {
                        var mockSession = new Mock <ISessionController>();
                        mockSession.SetupGet(x => x.Lifetime).Returns(infiniteTask).Verifiable();
                        mockSession.SetupGet(x => x.OnReboot).Returns(infiniteTask).Verifiable();
                        mockSession.SetupGet(x => x.Dmb).Returns(mDmbP).Verifiable();
                        mockSession.SetupGet(x => x.LaunchResult).Returns(Task.FromResult(new LaunchResult
                        {
                            StartupTime = TimeSpan.FromSeconds(1)
                        })).Verifiable();
                        sessionsToVerify.Add(mockSession);
                        return(Task.FromResult(mockSession.Object));
                    }).Verifiable();
                    mockAsyncDelayer.Setup(x => x.Delay(It.IsAny <TimeSpan>(), cancellationToken)).Returns(Task.CompletedTask).Verifiable();

                    cts.CancelAfter(TimeSpan.FromSeconds(15));

                    try
                    {
                        await wd.Launch(cancellationToken).ConfigureAwait(false);

                        await wd.Terminate(false, cancellationToken).ConfigureAwait(false);
                    }
                    finally
                    {
                        cts.Cancel();
                    }
                    Assert.AreEqual(2, sessionsToVerify.Count);
                    foreach (var I in sessionsToVerify)
                    {
                        I.VerifyAll();
                    }
                    mockDmbProvider.VerifyAll();
                }

            mockSessionControllerFactory.VerifyAll();
            mockDmbFactory.VerifyAll();
            mockRestartRegistration.VerifyAll();
            mockServerControl.VerifyAll();
            mockChat.VerifyAll();
            mockAsyncDelayer.VerifyAll();
        }
Esempio n. 14
0
        /// <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;
            }
        }
Esempio n. 15
0
        /// <summary>
        /// Run a quick DD instance to test the DMAPI is installed on the target code
        /// </summary>
        /// <param name="timeout">The timeout in seconds for validation</param>
        /// <param name="securityLevel">The <see cref="DreamDaemonSecurity"/> level to use to validate the API</param>
        /// <param name="job">The <see cref="CompileJob"/> for the operation</param>
        /// <param name="byondLock">The current <see cref="IByondExecutableLock"/></param>
        /// <param name="portToUse">The port to use for API validation</param>
        /// <param name="requireValidate">If the API validation is required to complete the deployment.</param>
        /// <param name="cancellationToken">The <see cref="CancellationToken"/> for the operation</param>
        /// <returns>A <see cref="Task"/> representing the running operation</returns>
        async Task VerifyApi(
            uint timeout,
            DreamDaemonSecurity securityLevel,
            Models.CompileJob job,
            IByondExecutableLock byondLock,
            ushort portToUse,
            bool requireValidate,
            CancellationToken cancellationToken)
        {
            logger.LogTrace("Verifying {0}DMAPI...", requireValidate ? "required " : String.Empty);
            var launchParameters = new DreamDaemonLaunchParameters
            {
                AllowWebClient      = false,
                Port                = portToUse,
                SecurityLevel       = securityLevel,
                StartupTimeout      = timeout,
                TopicRequestTimeout = 0,             // not used
                HeartbeatSeconds    = 0              // not used
            };

            job.MinimumSecurityLevel = securityLevel;             // needed for the TempDmbProvider

            ApiValidationStatus validationStatus;

            using (var provider = new TemporaryDmbProvider(ioManager.ResolvePath(job.DirectoryName.ToString()), String.Concat(job.DmeName, DmbExtension), job))
                await using (var controller = await sessionControllerFactory.LaunchNew(provider, byondLock, launchParameters, true, cancellationToken).ConfigureAwait(false))
                {
                    var launchResult = await controller.LaunchResult.ConfigureAwait(false);

                    if (launchResult.StartupTime.HasValue)
                    {
                        await controller.Lifetime.WithToken(cancellationToken).ConfigureAwait(false);
                    }

                    if (!controller.Lifetime.IsCompleted)
                    {
                        await controller.DisposeAsync().ConfigureAwait(false);
                    }

                    validationStatus = controller.ApiValidationStatus;

                    if (requireValidate && validationStatus == ApiValidationStatus.NeverValidated)
                    {
                        throw new JobException(ErrorCode.DreamMakerNeverValidated);
                    }

                    logger.LogTrace("API validation status: {0}", validationStatus);

                    job.DMApiVersion = controller.DMApiVersion;
                }

            switch (validationStatus)
            {
            case ApiValidationStatus.RequiresUltrasafe:
                job.MinimumSecurityLevel = DreamDaemonSecurity.Ultrasafe;
                return;

            case ApiValidationStatus.RequiresSafe:
                job.MinimumSecurityLevel = DreamDaemonSecurity.Safe;
                return;

            case ApiValidationStatus.RequiresTrusted:
                job.MinimumSecurityLevel = DreamDaemonSecurity.Trusted;
                return;

            case ApiValidationStatus.NeverValidated:
                if (requireValidate)
                {
                    throw new JobException(ErrorCode.DreamMakerNeverValidated);
                }
                job.MinimumSecurityLevel = DreamDaemonSecurity.Ultrasafe;
                break;

            case ApiValidationStatus.BadValidationRequest:
            case ApiValidationStatus.Incompatible:
                throw new JobException(ErrorCode.DreamMakerInvalidValidation);

            case ApiValidationStatus.UnaskedValidationRequest:
            default:
                throw new InvalidOperationException(
                          $"Session controller returned unexpected ApiValidationStatus: {validationStatus}");
            }
        }