tgstation-server 6.19.0
The /tg/station 13 server suite
Loading...
Searching...
No Matches
DreamMaker.cs
Go to the documentation of this file.
1using System;
2using System.Collections.Generic;
3using System.Linq;
4using System.Text;
5using System.Threading;
6using System.Threading.Tasks;
7
8using Microsoft.EntityFrameworkCore;
9using Microsoft.Extensions.Logging;
10using Microsoft.Extensions.Options;
11
12using Prometheus;
13
30
32{
34#pragma warning disable CA1506 // TODO: Decomplexify
35 sealed class DreamMaker : IDreamMaker
36#pragma warning restore CA1506
37 {
41 const string DmeExtension = "dme";
42
47
52
57
62
67
72
77
82
87
92
97
101 readonly IOptionsMonitor<SessionConfiguration> sessionConfigurationOptions;
102
106 readonly ILogger<DreamMaker> logger;
107
112
116 readonly Counter attemptedDeployments;
117
121 readonly Counter successfulDeployments;
122
126 readonly Counter failedDeployments;
127
131 readonly object deploymentLock;
132
136 Func<string?, string, Action<bool>>? currentChatCallback;
137
142
147
153 static string FormatExceptionForUsers(Exception exception)
154 => exception is OperationCanceledException
155 ? "The job was cancelled!"
156 : exception.Message;
157
179 StaticFiles.IConfiguration configuration,
188 IMetricFactory metricFactory,
189 IOptionsMonitor<SessionConfiguration> sessionConfigurationOptions,
190 ILogger<DreamMaker> logger,
191 Api.Models.Instance metadata)
192 {
193 this.engineManager = engineManager ?? throw new ArgumentNullException(nameof(engineManager));
194 this.ioManager = ioManager ?? throw new ArgumentNullException(nameof(ioManager));
195 this.configuration = configuration ?? throw new ArgumentNullException(nameof(configuration));
196 this.sessionControllerFactory = sessionControllerFactory ?? throw new ArgumentNullException(nameof(sessionControllerFactory));
197 this.eventConsumer = eventConsumer ?? throw new ArgumentNullException(nameof(eventConsumer));
198 this.chatManager = chatManager ?? throw new ArgumentNullException(nameof(chatManager));
199 this.processExecutor = processExecutor ?? throw new ArgumentNullException(nameof(processExecutor));
200 this.compileJobConsumer = compileJobConsumer ?? throw new ArgumentNullException(nameof(compileJobConsumer));
201 this.repositoryManager = repositoryManager ?? throw new ArgumentNullException(nameof(repositoryManager));
202 this.remoteDeploymentManagerFactory = remoteDeploymentManagerFactory ?? throw new ArgumentNullException(nameof(remoteDeploymentManagerFactory));
203 this.asyncDelayer = asyncDelayer ?? throw new ArgumentNullException(nameof(asyncDelayer));
204 ArgumentNullException.ThrowIfNull(metricFactory);
205 this.sessionConfigurationOptions = sessionConfigurationOptions ?? throw new ArgumentNullException(nameof(sessionConfigurationOptions));
206 this.logger = logger ?? throw new ArgumentNullException(nameof(logger));
207 this.metadata = metadata ?? throw new ArgumentNullException(nameof(metadata));
208
209 successfulDeployments = metricFactory.CreateCounter("tgs_successful_deployments", "The number of deployments that have completed successfully");
210 failedDeployments = metricFactory.CreateCounter("tgs_failed_deployments", "The number of deployments that have failed");
211 attemptedDeployments = metricFactory.CreateCounter("tgs_total_deployments", "The number of deployments that have been attempted");
212
213 deploymentLock = new object();
214 }
215
217#pragma warning disable CA1506
218 public async ValueTask DeploymentProcess(
219 Models.Job job,
220 IDatabaseContextFactory databaseContextFactory,
221 JobProgressReporter progressReporter,
222 CancellationToken cancellationToken)
223 {
224 ArgumentNullException.ThrowIfNull(job);
225 ArgumentNullException.ThrowIfNull(databaseContextFactory);
226 ArgumentNullException.ThrowIfNull(progressReporter);
227
228 lock (deploymentLock)
229 {
230 if (deploying)
231 throw new JobException(ErrorCode.DeploymentInProgress);
232 deploying = true;
233 }
234
236
237 currentChatCallback = null;
239 Models.CompileJob? compileJob = null;
240 bool success = false;
241 try
242 {
243 string? repoOwner = null;
244 string? repoName = null;
245 TimeSpan? averageSpan = null;
246 Models.RepositorySettings? repositorySettings = null;
247 Models.DreamDaemonSettings? ddSettings = null;
248 Models.DreamMakerSettings? dreamMakerSettings = null;
249 IRepository? repo = null;
250 IRemoteDeploymentManager? remoteDeploymentManager = null;
251 Models.RevisionInformation? revInfo = null;
252 await databaseContextFactory.UseContext(
253 async databaseContext =>
254 {
255 averageSpan = await CalculateExpectedDeploymentTime(databaseContext, cancellationToken);
256
257 ddSettings = await databaseContext
258 .DreamDaemonSettings
259 .Where(x => x.InstanceId == metadata.Id)
260 .Select(x => new Models.DreamDaemonSettings
261 {
262 StartupTimeout = x.StartupTimeout,
263 LogOutput = x.LogOutput,
264 })
265 .FirstOrDefaultAsync(cancellationToken);
266 if (ddSettings == default)
267 throw new JobException(ErrorCode.InstanceMissingDreamDaemonSettings);
268
269 dreamMakerSettings = await databaseContext
270 .DreamMakerSettings
271 .Where(x => x.InstanceId == metadata.Id)
272 .FirstAsync(cancellationToken);
273 if (dreamMakerSettings == default)
274 throw new JobException(ErrorCode.InstanceMissingDreamMakerSettings);
275
276 repositorySettings = await databaseContext
277 .RepositorySettings
278 .Where(x => x.InstanceId == metadata.Id)
279 .Select(x => new Models.RepositorySettings
280 {
281 AccessToken = x.AccessToken,
282 AccessUser = x.AccessUser,
283 ShowTestMergeCommitters = x.ShowTestMergeCommitters,
284 PushTestMergeCommits = x.PushTestMergeCommits,
285 PostTestMergeComment = x.PostTestMergeComment,
286 })
287 .FirstOrDefaultAsync(cancellationToken);
288 if (repositorySettings == default)
289 throw new JobException(ErrorCode.InstanceMissingRepositorySettings);
290
291 repo = await repositoryManager.LoadRepository(cancellationToken);
292 try
293 {
294 if (repo == null)
295 throw new JobException(ErrorCode.RepoMissing);
296
297 remoteDeploymentManager = remoteDeploymentManagerFactory
299
300 var repoSha = repo.Head;
301 repoOwner = repo.RemoteRepositoryOwner;
302 repoName = repo.RemoteRepositoryName;
303 revInfo = await databaseContext
304 .RevisionInformations
305 .Where(x => x.CommitSha == repoSha && x.InstanceId == metadata.Id)
306 .Include(x => x.ActiveTestMerges!)
307 .ThenInclude(x => x.TestMerge!)
308 .ThenInclude(x => x.MergedBy)
309 .FirstOrDefaultAsync(cancellationToken);
310
311 if (revInfo == null)
312 {
313 revInfo = new Models.RevisionInformation
314 {
315 CommitSha = repoSha,
316 Timestamp = await repo.TimestampCommit(repoSha, cancellationToken),
317 OriginCommitSha = repoSha,
319 {
320 Id = metadata.Id,
321 },
322 ActiveTestMerges = new List<RevInfoTestMerge>(),
323 };
324
325 logger.LogInformation(Repository.Repository.OriginTrackingErrorTemplate, repoSha);
326 databaseContext.RevisionInformations.Add(revInfo);
327 databaseContext.Instances.Attach(revInfo.Instance);
328 await databaseContext.Save(cancellationToken);
329 }
330 }
331 catch
332 {
333 repo?.Dispose();
334 throw;
335 }
336 });
337
338 Models.CompileJob? oldCompileJob;
339 using (repo)
340 {
341 var likelyPushedTestMergeCommit =
342 repositorySettings!.PushTestMergeCommits!.Value
343 && repositorySettings.AccessToken != null
344 && repositorySettings.AccessUser != null;
345 oldCompileJob = await compileJobConsumer.LatestCompileJob();
346 compileJob = await Compile(
347 job,
348 oldCompileJob,
349 revInfo!,
350 dreamMakerSettings!,
351 ddSettings!,
352 repo!,
353 remoteDeploymentManager!,
354 progressReporter,
355 averageSpan,
356 likelyPushedTestMergeCommit,
357 cancellationToken);
358 }
359
360 try
361 {
362 await databaseContextFactory.UseContext(
363 async databaseContext =>
364 {
365 var fullJob = compileJob.Job;
366 compileJob.Job = new Models.Job(job.Require(x => x.Id));
367 var fullRevInfo = compileJob.RevisionInformation;
368 compileJob.RevisionInformation = new Models.RevisionInformation
369 {
370 Id = revInfo!.Id,
371 };
372
373 databaseContext.Jobs.Attach(compileJob.Job);
374 databaseContext.RevisionInformations.Attach(compileJob.RevisionInformation);
375 databaseContext.CompileJobs.Add(compileJob);
376
377 // The difficulty with compile jobs is they have a two part commit
378 await databaseContext.Save(cancellationToken);
379 logger.LogTrace("Created CompileJob {compileJobId}", compileJob.Id);
380 try
381 {
382 var chatNotificationAction = currentChatCallback!(null, compileJob.Output!);
383 await compileJobConsumer.LoadCompileJob(compileJob, chatNotificationAction, cancellationToken);
384 success = true;
385 }
386 catch
387 {
388 // So we need to un-commit the compile job if the above throws
389 databaseContext.CompileJobs.Remove(compileJob);
390
391 // DCT: Cancellation token is for job, operation must run regardless
392 await databaseContext.Save(CancellationToken.None);
393 throw;
394 }
395
396 compileJob.Job = fullJob;
397 compileJob.RevisionInformation = fullRevInfo;
398 });
399 }
400 catch (Exception ex)
401 {
402 await CleanupFailedCompile(compileJob, remoteDeploymentManager!, ex);
403 throw;
404 }
405
406 var commentsTask = remoteDeploymentManager!.PostDeploymentComments(
407 compileJob,
408 oldCompileJob?.RevisionInformation,
409 repositorySettings,
410 repoOwner,
411 repoName,
412 cancellationToken);
413
414 var eventTask = eventConsumer.HandleEvent(EventType.DeploymentComplete, Enumerable.Empty<string>(), false, cancellationToken);
415
416 try
417 {
418 await ValueTaskExtensions.WhenAll(commentsTask, eventTask);
419 }
420 catch (Exception ex)
421 {
422 throw new JobException(ErrorCode.PostDeployFailure, ex);
423 }
424 finally
425 {
426 currentChatCallback = null;
427 }
428 }
429 catch (Exception ex)
430 {
431 currentChatCallback?.Invoke(
434
435 throw;
436 }
437 finally
438 {
439 deploying = false;
440 if (success)
442 else
443 failedDeployments.Inc();
444 }
445 }
446#pragma warning restore CA1506
447
454 async ValueTask<TimeSpan?> CalculateExpectedDeploymentTime(IDatabaseContext databaseContext, CancellationToken cancellationToken)
455 {
456 var previousCompileJobs = await databaseContext
458 .Where(x => x.Job.Instance!.Id == metadata.Id)
459 .OrderByDescending(x => x.Job.StoppedAt)
460 .Take(10)
461 .Select(x => new
462 {
463 StoppedAt = x.Job.StoppedAt!.Value,
464 StartedAt = x.Job.StartedAt!.Value,
465 })
466 .ToListAsync(cancellationToken);
467
468 TimeSpan? averageSpan = null;
469 if (previousCompileJobs.Count != 0)
470 {
471 var totalSpan = TimeSpan.Zero;
472 foreach (var previousCompileJob in previousCompileJobs)
473 totalSpan += previousCompileJob.StoppedAt - previousCompileJob.StartedAt;
474 averageSpan = totalSpan / previousCompileJobs.Count;
475 }
476
477 return averageSpan;
478 }
479
495 async ValueTask<Models.CompileJob> Compile(
496 Models.Job job,
497 Models.CompileJob? oldCompileJob,
498 Models.RevisionInformation revisionInformation,
499 Api.Models.Internal.DreamMakerSettings dreamMakerSettings,
500 DreamDaemonLaunchParameters launchParameters,
501 IRepository repository,
502 IRemoteDeploymentManager remoteDeploymentManager,
503 JobProgressReporter progressReporter,
504 TimeSpan? estimatedDuration,
505 bool localCommitExistsOnRemote,
506 CancellationToken cancellationToken)
507 {
508 logger.LogTrace("Begin Compile");
509
510 using var progressCts = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken);
511
512 progressReporter.StageName = "Reserving BYOND version";
513 var progressTask = ProgressTask(progressReporter, estimatedDuration, progressCts.Token);
514 try
515 {
516 using var engineLock = await engineManager.UseExecutables(null, null, cancellationToken);
518 revisionInformation,
519 oldCompileJob?.RevisionInformation,
520 engineLock.Version,
521 DateTimeOffset.UtcNow + estimatedDuration,
522 repository.RemoteRepositoryOwner,
523 repository.RemoteRepositoryName,
524 localCommitExistsOnRemote);
525
526 var compileJob = new Models.CompileJob(job, revisionInformation, engineLock.Version.ToString())
527 {
528 DirectoryName = Guid.NewGuid(),
529 DmeName = dreamMakerSettings.ProjectName,
530 RepositoryOrigin = repository.Origin.ToString(),
531 };
532
533 progressReporter.StageName = "Creating remote deployment notification";
534 await remoteDeploymentManager.StartDeployment(
535 repository,
536 compileJob,
537 cancellationToken);
538
539 logger.LogTrace("Deployment will timeout at {timeoutTime}", DateTimeOffset.UtcNow + dreamMakerSettings.Timeout!.Value);
540 using var timeoutTokenSource = new CancellationTokenSource(dreamMakerSettings.Timeout.Value);
541 var timeoutToken = timeoutTokenSource.Token;
542 using (timeoutToken.Register(() => logger.LogWarning("Deployment timed out!")))
543 {
544 using var combinedTokenSource = CancellationTokenSource.CreateLinkedTokenSource(timeoutToken, cancellationToken);
545 try
546 {
547 await RunCompileJob(
548 progressReporter,
549 compileJob,
550 dreamMakerSettings,
551 launchParameters,
552 engineLock,
553 repository,
554 remoteDeploymentManager,
555 combinedTokenSource.Token);
556 }
557 catch (OperationCanceledException) when (timeoutToken.IsCancellationRequested)
558 {
559 throw new JobException(ErrorCode.DeploymentTimeout);
560 }
561 }
562
563 return compileJob;
564 }
565 catch (OperationCanceledException)
566 {
567 // DCT: Cancellation token is for job, delaying here is fine
568 progressReporter.StageName = "Running CompileCancelled event";
569 await eventConsumer.HandleEvent(EventType.CompileCancelled, Enumerable.Empty<string>(), true, CancellationToken.None);
570 throw;
571 }
572 finally
573 {
574 progressCts.Cancel();
575 await progressTask;
576 }
577 }
578
591 async ValueTask RunCompileJob(
592 JobProgressReporter progressReporter,
593 Models.CompileJob job,
594 Api.Models.Internal.DreamMakerSettings dreamMakerSettings,
595 DreamDaemonLaunchParameters launchParameters,
596 IEngineExecutableLock engineLock,
597 IRepository repository,
598 IRemoteDeploymentManager remoteDeploymentManager,
599 CancellationToken cancellationToken)
600 {
601 var outputDirectory = job.DirectoryName!.Value.ToString();
602 logger.LogTrace("Compile output GUID: {dirGuid}", outputDirectory);
603
604 try
605 {
606 // copy the repository
607 logger.LogTrace("Copying repository to game directory");
608 progressReporter.StageName = "Copying repository";
609 var resolvedOutputDirectory = ioManager.ResolvePath(outputDirectory);
610 var repoOrigin = repository.Origin;
611 var repoReference = repository.Reference;
612 using (repository)
613 await repository.CopyTo(resolvedOutputDirectory, cancellationToken);
614
615 // repository closed now
616
617 // run precompile scripts
618 progressReporter.StageName = "Running PreCompile event";
620 EventType.CompileStart,
621 new List<string>
622 {
623 resolvedOutputDirectory,
624 repoOrigin.ToString(),
625 engineLock.Version.ToString(),
626 repoReference,
627 },
628 true,
629 cancellationToken);
630
631 // determine the dme
632 progressReporter.StageName = "Determining .dme";
633 if (job.DmeName == null)
634 {
635 logger.LogTrace("Searching for available .dmes");
636 var foundPaths = await ioManager.GetFilesWithExtension(resolvedOutputDirectory, DmeExtension, true, cancellationToken);
637 var foundPath = foundPaths.FirstOrDefault();
638 if (foundPath == default)
639 throw new JobException(ErrorCode.DeploymentNoDme);
640 job.DmeName = foundPath.Substring(
641 resolvedOutputDirectory.Length + 1,
642 foundPath.Length - resolvedOutputDirectory.Length - DmeExtension.Length - 2); // +1 for . in extension
643 }
644 else
645 {
646 var targetDme = ioManager.ConcatPath(outputDirectory, String.Join('.', job.DmeName, DmeExtension));
647 if (!await ioManager.PathIsChildOf(outputDirectory, targetDme, cancellationToken))
648 throw new JobException(ErrorCode.DeploymentWrongDme);
649
650 var targetDmeExists = await ioManager.FileExists(targetDme, cancellationToken);
651 if (!targetDmeExists)
652 throw new JobException(ErrorCode.DeploymentMissingDme);
653 }
654
655 logger.LogDebug("Selected \"{dmeName}.dme\" for compilation!", job.DmeName);
656
657 progressReporter.StageName = "Modifying .dme";
658 await ModifyDme(job, cancellationToken);
659
660 // run precompile scripts
661 progressReporter.StageName = "Running PreDreamMaker event";
663 EventType.PreDreamMaker,
664 new List<string>
665 {
666 resolvedOutputDirectory,
667 repoOrigin.ToString(),
668 engineLock.Version.ToString(),
669 },
670 true,
671 cancellationToken);
672
673 // run compiler
674 progressReporter.StageName = "Running Compiler";
675 var compileSuceeded = await RunDreamMaker(engineLock, job, dreamMakerSettings.CompilerAdditionalArguments, cancellationToken);
676
677 // Session takes ownership of the lock and Disposes it so save this for later
678 var engineVersion = engineLock.Version;
679
680 // verify api
681 try
682 {
683 if (!compileSuceeded)
684 throw new JobException(
685 ErrorCode.DeploymentExitCode,
686 new JobException($"Compilation failed:{Environment.NewLine}{Environment.NewLine}{job.Output}"));
687
688 await VerifyApi(
689 launchParameters.StartupTimeout!.Value,
690 dreamMakerSettings.ApiValidationSecurityLevel!.Value,
691 job,
692 progressReporter,
693 engineLock,
694 dreamMakerSettings.ApiValidationPort!.Value,
695 dreamMakerSettings.DMApiValidationMode!.Value,
696 launchParameters.LogOutput!.Value,
697 cancellationToken);
698 }
699 catch (JobException)
700 {
701 // DD never validated or compile failed
702 progressReporter.StageName = "Running CompileFailure event";
704 EventType.CompileFailure,
705 new List<string>
706 {
707 resolvedOutputDirectory,
708 compileSuceeded ? "1" : "0",
709 engineVersion.ToString(),
710 },
711 true,
712 cancellationToken);
713 throw;
714 }
715
716 progressReporter.StageName = "Running CompileComplete event";
718 EventType.CompileComplete,
719 new List<string>
720 {
721 resolvedOutputDirectory,
722 engineVersion.ToString(),
723 },
724 true,
725 cancellationToken);
726
727 logger.LogTrace("Applying static game file symlinks...");
728 progressReporter.StageName = "Symlinking GameStaticFiles";
729
730 // symlink in the static data
731 await configuration.SymlinkStaticFilesTo(resolvedOutputDirectory, cancellationToken);
732
733 logger.LogDebug("Compile complete!");
734 }
735 catch (Exception ex)
736 {
737 progressReporter.StageName = "Cleaning output directory";
738 await CleanupFailedCompile(job, remoteDeploymentManager, ex);
739 throw;
740 }
741 }
742
750 async ValueTask ProgressTask(JobProgressReporter progressReporter, TimeSpan? estimatedDuration, CancellationToken cancellationToken)
751 {
752 double? lastReport = estimatedDuration.HasValue ? 0 : null;
753 progressReporter.ReportProgress(lastReport);
754
755 var minimumSleepInterval = TimeSpan.FromMilliseconds(250);
756 var sleepInterval = estimatedDuration.HasValue ? estimatedDuration.Value / 100 : minimumSleepInterval;
757
758 if (estimatedDuration.HasValue)
759 {
760 logger.LogDebug("Compile is expected to take: {estimatedDuration}", estimatedDuration);
761 }
762 else
763 {
764 logger.LogTrace("No metric to estimate compile time.");
765 }
766
767 try
768 {
769 for (var iteration = 0; iteration < (estimatedDuration.HasValue ? 99 : Int32.MaxValue); ++iteration)
770 {
771 if (estimatedDuration.HasValue)
772 {
773 var nextInterval = DateTimeOffset.UtcNow + sleepInterval;
774 do
775 {
776 var remainingSleepThisInterval = nextInterval - DateTimeOffset.UtcNow;
777 var nextSleepSpan = remainingSleepThisInterval < minimumSleepInterval ? minimumSleepInterval : remainingSleepThisInterval;
778
779 await asyncDelayer.Delay(nextSleepSpan, cancellationToken);
780 progressReporter.ReportProgress(lastReport);
781 }
782 while (DateTimeOffset.UtcNow < nextInterval);
783 }
784 else
785 await asyncDelayer.Delay(minimumSleepInterval, cancellationToken);
786
787 lastReport = estimatedDuration.HasValue ? sleepInterval * (iteration + 1) / estimatedDuration.Value : null;
788 progressReporter.ReportProgress(lastReport);
789 }
790 }
791 catch (OperationCanceledException ex)
792 {
793 logger.LogTrace(ex, "ProgressTask aborted.");
794 }
795 catch (Exception ex)
796 {
797 logger.LogError(ex, "ProgressTask crashed!");
798 }
799 }
800
814 async ValueTask VerifyApi(
815 uint timeout,
816 DreamDaemonSecurity securityLevel,
817 Models.CompileJob job,
818 JobProgressReporter progressReporter,
819 IEngineExecutableLock engineLock,
820 ushort portToUse,
821 DMApiValidationMode validationMode,
822 bool logOutput,
823 CancellationToken cancellationToken)
824 {
825 if (validationMode == DMApiValidationMode.Skipped)
826 {
827 logger.LogDebug("Skipping DMAPI validation");
828 job.MinimumSecurityLevel = DreamDaemonSecurity.Ultrasafe;
829 return;
830 }
831
832 progressReporter.StageName = "Validating DMAPI";
833
834 var requireValidate = validationMode == DMApiValidationMode.Required;
835 logger.LogTrace("Verifying {possiblyRequired}DMAPI...", requireValidate ? "required " : String.Empty);
836 var launchParameters = new DreamDaemonLaunchParameters
837 {
838 AllowWebClient = false,
839 Port = portToUse,
840 OpenDreamTopicPort = 0,
841 SecurityLevel = securityLevel,
842 Visibility = DreamDaemonVisibility.Invisible,
843 StartupTimeout = timeout,
844 TopicRequestTimeout = 0, // not used
845 HealthCheckSeconds = 0, // not used
846 StartProfiler = false,
847 LogOutput = logOutput,
848 MapThreads = 1, // lowest possible amount
849 };
850
851 job.MinimumSecurityLevel = securityLevel; // needed for the TempDmbProvider
852
853 ApiValidationStatus validationStatus;
854 await using (var provider = new TemporaryDmbProvider(
855 ioManager.ResolvePath(job.DirectoryName!.Value.ToString()),
856 job,
857 engineLock.Version))
858 await using (var controller = await sessionControllerFactory.LaunchNew(provider, engineLock, launchParameters, true, cancellationToken))
859 {
860 var launchResult = await controller.LaunchResult.WaitAsync(cancellationToken);
861
862 if (launchResult.StartupTime.HasValue)
863 await controller.Lifetime.WaitAsync(cancellationToken);
864
865 if (!controller.Lifetime.IsCompleted)
866 await controller.DisposeAsync();
867
868 validationStatus = controller.ApiValidationStatus;
869
870 logger.LogTrace("API validation status: {validationStatus}", validationStatus);
871
872 job.DMApiVersion = controller.DMApiVersion;
873 }
874
875 switch (validationStatus)
876 {
877 case ApiValidationStatus.RequiresUltrasafe:
878 job.MinimumSecurityLevel = DreamDaemonSecurity.Ultrasafe;
879 return;
880 case ApiValidationStatus.RequiresSafe:
881 job.MinimumSecurityLevel = DreamDaemonSecurity.Safe;
882 return;
883 case ApiValidationStatus.RequiresTrusted:
884 job.MinimumSecurityLevel = DreamDaemonSecurity.Trusted;
885 return;
886 case ApiValidationStatus.NeverValidated:
887 if (requireValidate)
888 throw new JobException(ErrorCode.DeploymentNeverValidated);
889 job.MinimumSecurityLevel = DreamDaemonSecurity.Ultrasafe;
890 break;
891 case ApiValidationStatus.BadValidationRequest:
892 case ApiValidationStatus.Incompatible:
893 throw new JobException(ErrorCode.DeploymentInvalidValidation);
894 case ApiValidationStatus.UnaskedValidationRequest:
895 default:
896 throw new InvalidOperationException(
897 $"Session controller returned unexpected ApiValidationStatus: {validationStatus}");
898 }
899 }
900
909 async ValueTask<bool> RunDreamMaker(
910 IEngineExecutableLock engineLock,
911 Models.CompileJob job,
912 string? additionalCompilerArguments,
913 CancellationToken cancellationToken)
914 {
915 var environment = await engineLock.LoadEnv(logger, true, cancellationToken);
916 var arguments = engineLock.FormatCompilerArguments($"{job.DmeName}.{DmeExtension}", additionalCompilerArguments);
917
918 await using var dm = await processExecutor.LaunchProcess(
919 engineLock.CompilerExePath,
920 ioManager.ResolvePath(
921 job.DirectoryName!.Value.ToString()),
922 arguments,
923 cancellationToken,
924 environment,
925 readStandardHandles: true,
926 noShellExecute: true);
927
928 if (sessionConfigurationOptions.CurrentValue.LowPriorityDeploymentProcesses)
929 dm.AdjustPriority(false);
930
931 int exitCode;
932 using (cancellationToken.Register(() => dm.Terminate()))
933 exitCode = (await dm.Lifetime).Value;
934 cancellationToken.ThrowIfCancellationRequested();
935
936 logger.LogDebug("DreamMaker exit code: {exitCode}", exitCode);
937 job.Output = $"{await dm.GetCombinedOutput(cancellationToken)}{Environment.NewLine}{Environment.NewLine}Exit Code: {exitCode}";
938 logger.LogDebug("DreamMaker output: {newLine}{output}", Environment.NewLine, job.Output);
939
940 currentDreamMakerOutput = job.Output;
941 return exitCode == 0;
942 }
943
950 async ValueTask ModifyDme(Models.CompileJob job, CancellationToken cancellationToken)
951 {
952 var dmeFileName = String.Join('.', job.DmeName, DmeExtension);
953 var stringDirectoryName = job.DirectoryName!.Value.ToString();
954 var dmePath = ioManager.ConcatPath(stringDirectoryName, dmeFileName);
955 var dmeReadTask = ioManager.ReadAllBytes(dmePath, cancellationToken);
956
957 var dmeModificationsTask = configuration.CopyDMFilesTo(
958 dmeFileName,
959 ioManager.ResolvePath(
960 ioManager.ConcatPath(
961 stringDirectoryName,
962 ioManager.GetDirectoryName(dmeFileName))),
963 cancellationToken);
964
965 var dmeBytes = await dmeReadTask;
966 var dme = Encoding.UTF8.GetString(dmeBytes);
967
968 var dmeModifications = await dmeModificationsTask;
969
970 if (dmeModifications == null || dmeModifications.TotalDmeOverwrite)
971 {
972 if (dmeModifications != null)
973 logger.LogDebug(".dme replacement configured!");
974 else
975 logger.LogTrace("No .dme modifications required.");
976 return;
977 }
978
979 var dmeLines = new List<string>(dme.Split('\n', StringSplitOptions.None));
980 for (var dmeLineIndex = 0; dmeLineIndex < dmeLines.Count; ++dmeLineIndex)
981 {
982 var line = dmeLines[dmeLineIndex];
983 if (line.Contains("BEGIN_INCLUDE", StringComparison.Ordinal) && dmeModifications.HeadIncludeLine != null)
984 {
985 var headIncludeLineNumber = dmeLineIndex + 1;
986 logger.LogDebug(
987 "Inserting HeadInclude.dm at line {lineNumber}: {includeLine}",
988 headIncludeLineNumber,
989 dmeModifications.HeadIncludeLine);
990 dmeLines.Insert(headIncludeLineNumber, dmeModifications.HeadIncludeLine);
991 ++dmeLineIndex;
992 }
993 else if (line.Contains("END_INCLUDE", StringComparison.Ordinal) && dmeModifications.TailIncludeLine != null)
994 {
995 logger.LogDebug(
996 "Inserting TailInclude.dm at line {lineNumber}: {includeLine}",
997 dmeLineIndex,
998 dmeModifications.TailIncludeLine);
999 dmeLines.Insert(dmeLineIndex, dmeModifications.TailIncludeLine);
1000 break;
1001 }
1002 }
1003
1004 dmeBytes = Encoding.UTF8.GetBytes(String.Join('\n', dmeLines));
1005 await ioManager.WriteAllBytes(dmePath, dmeBytes, cancellationToken);
1006 }
1007
1015 ValueTask CleanupFailedCompile(Models.CompileJob job, IRemoteDeploymentManager remoteDeploymentManager, Exception exception)
1016 {
1017 async ValueTask CleanDir()
1018 {
1019 if (sessionConfigurationOptions.CurrentValue.DelayCleaningFailedDeployments)
1020 {
1021 logger.LogDebug("Not cleaning up errored deployment directory {guid} due to config.", job.DirectoryName);
1022 return;
1023 }
1024
1025 logger.LogTrace("Cleaning compile directory...");
1026 var jobPath = job.DirectoryName!.Value.ToString();
1027 try
1028 {
1029 // DCT: None available
1030 await eventConsumer.HandleEvent(EventType.DeploymentCleanup, new List<string> { jobPath }, true, CancellationToken.None);
1031 await ioManager.DeleteDirectory(jobPath, CancellationToken.None);
1032 }
1033 catch (Exception e)
1034 {
1035 logger.LogWarning(e, "Error cleaning up compile directory {path}!", ioManager.ResolvePath(jobPath));
1036 }
1037 }
1038
1039 var dirCleanTask = CleanDir();
1040
1041 var failRemoteDeployTask = remoteDeploymentManager.FailDeployment(
1042 job,
1043 FormatExceptionForUsers(exception),
1044 CancellationToken.None); // DCT: None available
1045
1047 dirCleanTask,
1048 failRemoteDeployTask);
1049 }
1050 }
1051}
Metadata about a server instance.
Definition Instance.cs:9
bool? LogOutput
If process output/error text should be logged.
Represents information about a current git revison.
Extension methods for the ValueTask and ValueTask<TResult> classes.
static async ValueTask WhenAll(IEnumerable< ValueTask > tasks)
Fully await a given list of tasks .
Func< string?, string, Action< bool > >? currentChatCallback
The active callback from IChatManager.QueueDeploymentMessage.
async ValueTask< Models.CompileJob > Compile(Models.Job job, Models.CompileJob? oldCompileJob, Models.RevisionInformation revisionInformation, Api.Models.Internal.DreamMakerSettings dreamMakerSettings, DreamDaemonLaunchParameters launchParameters, IRepository repository, IRemoteDeploymentManager remoteDeploymentManager, JobProgressReporter progressReporter, TimeSpan? estimatedDuration, bool localCommitExistsOnRemote, CancellationToken cancellationToken)
Run the compile implementation.
readonly IRepositoryManager repositoryManager
The IRepositoryManager for DreamMaker.
Definition DreamMaker.cs:81
ValueTask CleanupFailedCompile(Models.CompileJob job, IRemoteDeploymentManager remoteDeploymentManager, Exception exception)
Cleans up a failed compile job .
async ValueTask ProgressTask(JobProgressReporter progressReporter, TimeSpan? estimatedDuration, CancellationToken cancellationToken)
Gradually triggers a given progressReporter over a given estimatedDuration .
async ValueTask VerifyApi(uint timeout, DreamDaemonSecurity securityLevel, Models.CompileJob job, JobProgressReporter progressReporter, IEngineExecutableLock engineLock, ushort portToUse, DMApiValidationMode validationMode, bool logOutput, CancellationToken cancellationToken)
Run a quick DD instance to test the DMAPI is installed on the target code.
readonly ILogger< DreamMaker > logger
The ILogger for DreamMaker.
async ValueTask RunCompileJob(JobProgressReporter progressReporter, Models.CompileJob job, Api.Models.Internal.DreamMakerSettings dreamMakerSettings, DreamDaemonLaunchParameters launchParameters, IEngineExecutableLock engineLock, IRepository repository, IRemoteDeploymentManager remoteDeploymentManager, CancellationToken cancellationToken)
Executes and populate a given job .
readonly StaticFiles.IConfiguration configuration
The StaticFiles.IConfiguration for DreamMaker.
Definition DreamMaker.cs:56
const string DmeExtension
Extension for .dmes.
Definition DreamMaker.cs:41
async ValueTask DeploymentProcess(Models.Job job, IDatabaseContextFactory databaseContextFactory, JobProgressReporter progressReporter, CancellationToken cancellationToken)
Create and a compile job and insert it into the database. Meant to be called by a IJobManager....
string? currentDreamMakerOutput
Cached for currentChatCallback.
async ValueTask< TimeSpan?> CalculateExpectedDeploymentTime(IDatabaseContext databaseContext, CancellationToken cancellationToken)
Calculate the average length of a deployment using a given databaseContext .
readonly IIOManager ioManager
The IIOManager for DreamMaker.
Definition DreamMaker.cs:51
readonly IRemoteDeploymentManagerFactory remoteDeploymentManagerFactory
The IRemoteDeploymentManagerFactory for DreamMaker.
Definition DreamMaker.cs:91
readonly Counter successfulDeployments
The number of successful deployments.
readonly Counter attemptedDeployments
The number of attempted deployments.
readonly ISessionControllerFactory sessionControllerFactory
The ISessionControllerFactory for DreamMaker.
Definition DreamMaker.cs:61
readonly Counter failedDeployments
The number of failed deployments.
static string FormatExceptionForUsers(Exception exception)
Format a given Exception for display to users.
readonly IProcessExecutor processExecutor
The IProcessExecutor for DreamMaker.
Definition DreamMaker.cs:76
readonly IEngineManager engineManager
The IEngineManager for DreamMaker.
Definition DreamMaker.cs:46
readonly ICompileJobSink compileJobConsumer
The ICompileJobSink for DreamMaker.
Definition DreamMaker.cs:86
readonly IEventConsumer eventConsumer
The IEventConsumer for DreamMaker.
Definition DreamMaker.cs:66
readonly Api.Models.Instance metadata
The Instance DreamMaker belongs to.
async ValueTask ModifyDme(Models.CompileJob job, CancellationToken cancellationToken)
Adds server side includes to the .dme being compiled.
readonly IOptionsMonitor< SessionConfiguration > sessionConfigurationOptions
The IOptionsMonitor<TOptions> of SessionConfiguration for DreamMaker.
readonly IChatManager chatManager
The IChatManager for DreamMaker.
Definition DreamMaker.cs:71
async ValueTask< bool > RunDreamMaker(IEngineExecutableLock engineLock, Models.CompileJob job, string? additionalCompilerArguments, CancellationToken cancellationToken)
Compiles a .dme with DreamMaker.
readonly object deploymentLock
lock object for deploying.
DreamMaker(IEngineManager engineManager, IIOManager ioManager, StaticFiles.IConfiguration configuration, ISessionControllerFactory sessionControllerFactory, IEventConsumer eventConsumer, IChatManager chatManager, IProcessExecutor processExecutor, ICompileJobSink compileJobConsumer, IRepositoryManager repositoryManager, IRemoteDeploymentManagerFactory remoteDeploymentManagerFactory, IAsyncDelayer asyncDelayer, IMetricFactory metricFactory, IOptionsMonitor< SessionConfiguration > sessionConfigurationOptions, ILogger< DreamMaker > logger, Api.Models.Instance metadata)
Initializes a new instance of the DreamMaker class.
readonly IAsyncDelayer asyncDelayer
The IAsyncDelayer for DreamMaker.
Definition DreamMaker.cs:96
Repository(LibGit2Sharp.IRepository libGitRepo, ILibGit2Commands commands, IIOManager ioManager, IEventConsumer eventConsumer, ICredentialsProvider credentialsProvider, IPostWriteHandler postWriteHandler, IGitRemoteFeaturesFactory gitRemoteFeaturesFactory, ILibGit2RepositoryFactory submoduleFactory, IOptionsMonitor< GeneralConfiguration > generalConfigurationOptions, ILogger< Repository > logger, Action disposeAction)
Initializes a new instance of the Repository class.
Operation exceptions thrown from the context of a Models.Job.
void ReportProgress(double? progress)
Report progress.
Represents an Api.Models.Instance in the database.
Definition Instance.cs:11
string? RemoteRepositoryName
If RemoteGitProvider is not RemoteGitProvider.Unknown this will be set with the name of the repositor...
RemoteGitProvider? RemoteGitProvider
The Models.RemoteGitProvider in use by the repository.
string? RemoteRepositoryOwner
If RemoteGitProvider is not RemoteGitProvider.Unknown this will be set with the owner of the reposito...
For managing connected chat services.
Func< string?, string, Action< bool > > QueueDeploymentMessage(Models.RevisionInformation revisionInformation, Models.RevisionInformation? previousRevisionInformation, Api.Models.EngineVersion engineVersion, DateTimeOffset? estimatedCompletionTime, string? gitHubOwner, string? gitHubRepo, bool localCommitPushed)
Send the message for a deployment to configured deployment channels.
ValueTask LoadCompileJob(CompileJob job, Action< bool >? activationAction, CancellationToken cancellationToken)
Load a new job into the ICompileJobSink.
ValueTask< CompileJob?> LatestCompileJob()
Gets the latest CompileJob.
IRemoteDeploymentManager CreateRemoteDeploymentManager(Api.Models.Instance metadata, RemoteGitProvider remoteGitProvider)
Creates a IRemoteDeploymentManager for a given remoteGitProvider .
ValueTask PostDeploymentComments(CompileJob compileJob, RevisionInformation? previousRevisionInformation, RepositorySettings repositorySettings, string? repoOwner, string? repoName, CancellationToken cancellationToken)
Post deployment comments to the test merge ticket.
ValueTask StartDeployment(Api.Models.Internal.IGitRemoteInformation remoteInformation, CompileJob compileJob, CancellationToken cancellationToken)
Start a deployment for a given compileJob .
ValueTask FailDeployment(CompileJob compileJob, string errorMessage, CancellationToken cancellationToken)
Fail a deployment for a given compileJob .
Represents usage of the two primary BYOND server executables.
string FormatCompilerArguments(string dmePath, string? additionalArguments)
Return the command line arguments for compiling a given dmePath if compilation is necessary.
ValueTask< Dictionary< string, string >?> LoadEnv(ILogger logger, bool forCompiler, CancellationToken cancellationToken)
Loads the environment settings for either the server or compiler.
EngineVersion Version
The EngineVersion of the IEngineInstallation.
string CompilerExePath
The full path to the dm/DreamMaker executable.
ValueTask< IEngineExecutableLock > UseExecutables(EngineVersion? requiredVersion, string? trustDmbFullPath, CancellationToken cancellationToken)
Lock the current installation's location and return a IEngineExecutableLock.
Consumes EventTypes and takes the appropriate actions.
ValueTask HandleEvent(EventType eventType, IEnumerable< string?> parameters, bool deploymentPipeline, CancellationToken cancellationToken)
Handle a given eventType .
Represents an on-disk git repository.
Task< DateTimeOffset > TimestampCommit(string sha, CancellationToken cancellationToken)
Gets the DateTimeOffset a given sha was created on.
string Reference
The current reference the IRepository HEAD is using. This can be a branch or tag.
ValueTask CopyTo(string path, CancellationToken cancellationToken)
Copies the current working directory to a given path .
string Head
The SHA of the IRepository HEAD.
Uri Origin
The current origin remote the IRepository is using.
ValueTask< IRepository?> LoadRepository(CancellationToken cancellationToken)
Attempt to load the IRepository from the default location.
Factory for scoping usage of IDatabaseContexts. Meant for use by Components.
ValueTask UseContext(Func< IDatabaseContext, ValueTask > operation)
Run an operation in the scope of an IDatabaseContext.
IDatabaseCollection< CompileJob > CompileJobs
The CompileJobs in the IDatabaseContext.
Interface for using filesystems.
Definition IIOManager.cs:14
Task< bool > PathIsChildOf(string parentPath, string childPath, CancellationToken cancellationToken)
Check if a given parentPath is a parent of a given parentPath .
string ResolvePath()
Retrieve the full path of the current working directory.
string ConcatPath(params string[] paths)
Combines an array of strings into a path.
Task< bool > FileExists(string path, CancellationToken cancellationToken)
Check that the file at path exists.
Task< List< string > > GetFilesWithExtension(string path, string extension, bool recursive, CancellationToken cancellationToken)
Gets a list of files in path with the given extension .
ErrorCode
Types of Response.ErrorMessageResponses that the API may return.
Definition ErrorCode.cs:12
DreamDaemonVisibility
The visibility setting for DreamDaemon.
DreamDaemonSecurity
DreamDaemon's security level.
DMApiValidationMode
The DMAPI validation setting for deployments.
EventType
Types of events. Mirror in tgs.dm. Prefer last listed name for script.
Definition EventType.cs:7
ApiValidationStatus
Status of DMAPI validation.