/// <summary> /// Change a given <paramref name="securityLevel"/> into the appropriate DreamDaemon command line word /// </summary> /// <param name="securityLevel">The <see cref="DreamDaemonSecurity"/> level to change</param> /// <returns>A <see cref="string"/> representation of the command line parameter</returns> static string SecurityWord(DreamDaemonSecurity securityLevel) { return(securityLevel switch { DreamDaemonSecurity.Safe => "safe", DreamDaemonSecurity.Trusted => "trusted", DreamDaemonSecurity.Ultrasafe => "ultrasafe", _ => throw new ArgumentOutOfRangeException(nameof(securityLevel), securityLevel, String.Format(CultureInfo.InvariantCulture, "Bad DreamDaemon security level: {0}", securityLevel)), });
async Task <DreamDaemon> DeployTestDme(string dmeName, DreamDaemonSecurity deploymentSecurity, CancellationToken cancellationToken) { await instanceClient.DreamMaker.Update(new DreamMaker { ApiValidationSecurityLevel = deploymentSecurity, ProjectName = $"tests/DMAPI/{dmeName}" }, cancellationToken); var compileJobJob = await instanceClient.DreamMaker.Compile(cancellationToken); await WaitForJob(compileJobJob, 90, false, cancellationToken); return(await instanceClient.DreamDaemon.Read(cancellationToken)); }
/// <summary> /// Change a given <paramref name="securityLevel"/> into the appropriate DreamDaemon command line word /// </summary> /// <param name="securityLevel">The <see cref="DreamDaemonSecurity"/> level to change</param> /// <returns>A <see cref="string"/> representation of the command line parameter</returns> static string SecurityWord(DreamDaemonSecurity securityLevel) { switch (securityLevel) { case DreamDaemonSecurity.Safe: return("safe"); case DreamDaemonSecurity.Trusted: return("trusted"); case DreamDaemonSecurity.Ultrasafe: return("ultrasafe"); default: throw new ArgumentOutOfRangeException(nameof(securityLevel), securityLevel, String.Format(CultureInfo.InvariantCulture, "Bad DreamDaemon security level: {0}", securityLevel)); } }
/// <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); } }
async Task <DreamDaemon> DeployTestDme(string dmeName, DreamDaemonSecurity deploymentSecurity, bool requireApi, CancellationToken cancellationToken) { var refreshed = await instanceClient.DreamMaker.Update(new DreamMaker { ApiValidationSecurityLevel = deploymentSecurity, ProjectName = $"tests/DMAPI/{dmeName}", RequireDMApiValidation = requireApi }, cancellationToken); Assert.AreEqual(deploymentSecurity, refreshed.ApiValidationSecurityLevel); Assert.AreEqual(requireApi, refreshed.RequireDMApiValidation); var compileJobJob = await instanceClient.DreamMaker.Compile(cancellationToken); await WaitForJob(compileJobJob, 90, false, null, cancellationToken); var ddInfo = await instanceClient.DreamDaemon.Read(cancellationToken); if (requireApi) { Assert.IsNotNull((ddInfo.StagedCompileJob ?? ddInfo.ActiveCompileJob).DMApiVersion); } return(ddInfo); }
/// <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!"); } }
/// <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}"); } }
/// <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; } }