tgstation-server 6.14.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;
10
11using Prometheus;
12
29
31{
33 sealed class DreamMaker : IDreamMaker
34 {
38 const string DmeExtension = "dme";
39
44
49
54
59
64
69
74
79
84
89
94
98 readonly ILogger<DreamMaker> logger;
99
104
109
113 readonly Counter attemptedDeployments;
114
118 readonly Counter successfulDeployments;
119
123 readonly Counter failedDeployments;
124
128 readonly object deploymentLock;
129
133 Func<string?, string, Action<bool>>? currentChatCallback;
134
139
144
150 static string FormatExceptionForUsers(Exception exception)
151 => exception is OperationCanceledException
152 ? "The job was cancelled!"
153 : exception.Message;
154
176 StaticFiles.IConfiguration configuration,
185 IMetricFactory metricFactory,
186 ILogger<DreamMaker> logger,
188 Api.Models.Instance metadata)
189 {
190 this.engineManager = engineManager ?? throw new ArgumentNullException(nameof(engineManager));
191 this.ioManager = ioManager ?? throw new ArgumentNullException(nameof(ioManager));
192 this.configuration = configuration ?? throw new ArgumentNullException(nameof(configuration));
193 this.sessionControllerFactory = sessionControllerFactory ?? throw new ArgumentNullException(nameof(sessionControllerFactory));
194 this.eventConsumer = eventConsumer ?? throw new ArgumentNullException(nameof(eventConsumer));
195 this.chatManager = chatManager ?? throw new ArgumentNullException(nameof(chatManager));
196 this.processExecutor = processExecutor ?? throw new ArgumentNullException(nameof(processExecutor));
197 this.compileJobConsumer = compileJobConsumer ?? throw new ArgumentNullException(nameof(compileJobConsumer));
198 this.repositoryManager = repositoryManager ?? throw new ArgumentNullException(nameof(repositoryManager));
199 this.remoteDeploymentManagerFactory = remoteDeploymentManagerFactory ?? throw new ArgumentNullException(nameof(remoteDeploymentManagerFactory));
200 this.asyncDelayer = asyncDelayer ?? throw new ArgumentNullException(nameof(asyncDelayer));
201 ArgumentNullException.ThrowIfNull(metricFactory);
202 this.logger = logger ?? throw new ArgumentNullException(nameof(logger));
203 this.sessionConfiguration = sessionConfiguration ?? throw new ArgumentNullException(nameof(sessionConfiguration));
204 this.metadata = metadata ?? throw new ArgumentNullException(nameof(metadata));
205
206 successfulDeployments = metricFactory.CreateCounter("tgs_successful_deployments", "The number of deployments that have completed successfully");
207 failedDeployments = metricFactory.CreateCounter("tgs_failed_deployments", "The number of deployments that have failed");
208 attemptedDeployments = metricFactory.CreateCounter("tgs_total_deployments", "The number of deployments that have been attempted");
209
210 deploymentLock = new object();
211 }
212
214#pragma warning disable CA1506
215 public async ValueTask DeploymentProcess(
216 Models.Job job,
217 IDatabaseContextFactory databaseContextFactory,
218 JobProgressReporter progressReporter,
219 CancellationToken cancellationToken)
220 {
221 ArgumentNullException.ThrowIfNull(job);
222 ArgumentNullException.ThrowIfNull(databaseContextFactory);
223 ArgumentNullException.ThrowIfNull(progressReporter);
224
225 lock (deploymentLock)
226 {
227 if (deploying)
228 throw new JobException(ErrorCode.DeploymentInProgress);
229 deploying = true;
230 }
231
233
234 currentChatCallback = null;
236 Models.CompileJob? compileJob = null;
237 bool success = false;
238 try
239 {
240 string? repoOwner = null;
241 string? repoName = null;
242 TimeSpan? averageSpan = null;
243 Models.RepositorySettings? repositorySettings = null;
244 Models.DreamDaemonSettings? ddSettings = null;
245 Models.DreamMakerSettings? dreamMakerSettings = null;
246 IRepository? repo = null;
247 IRemoteDeploymentManager? remoteDeploymentManager = null;
248 Models.RevisionInformation? revInfo = null;
249 await databaseContextFactory.UseContext(
250 async databaseContext =>
251 {
252 averageSpan = await CalculateExpectedDeploymentTime(databaseContext, cancellationToken);
253
254 ddSettings = await databaseContext
255 .DreamDaemonSettings
256 .AsQueryable()
257 .Where(x => x.InstanceId == metadata.Id)
258 .Select(x => new Models.DreamDaemonSettings
259 {
260 StartupTimeout = x.StartupTimeout,
261 LogOutput = x.LogOutput,
262 })
263 .FirstOrDefaultAsync(cancellationToken);
264 if (ddSettings == default)
265 throw new JobException(ErrorCode.InstanceMissingDreamDaemonSettings);
266
267 dreamMakerSettings = await databaseContext
268 .DreamMakerSettings
269 .AsQueryable()
270 .Where(x => x.InstanceId == metadata.Id)
271 .FirstAsync(cancellationToken);
272 if (dreamMakerSettings == default)
273 throw new JobException(ErrorCode.InstanceMissingDreamMakerSettings);
274
275 repositorySettings = await databaseContext
276 .RepositorySettings
277 .AsQueryable()
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 .AsQueryable()
306 .Where(x => x.CommitSha == repoSha && x.InstanceId == metadata.Id)
307 .Include(x => x.ActiveTestMerges!)
308 .ThenInclude(x => x.TestMerge!)
309 .ThenInclude(x => x.MergedBy)
310 .FirstOrDefaultAsync(cancellationToken);
311
312 if (revInfo == null)
313 {
314 revInfo = new Models.RevisionInformation
315 {
316 CommitSha = repoSha,
317 Timestamp = await repo.TimestampCommit(repoSha, cancellationToken),
318 OriginCommitSha = repoSha,
320 {
321 Id = metadata.Id,
322 },
323 ActiveTestMerges = new List<RevInfoTestMerge>(),
324 };
325
326 logger.LogInformation(Repository.Repository.OriginTrackingErrorTemplate, repoSha);
327 databaseContext.RevisionInformations.Add(revInfo);
328 databaseContext.Instances.Attach(revInfo.Instance);
329 await databaseContext.Save(cancellationToken);
330 }
331 }
332 catch
333 {
334 repo?.Dispose();
335 throw;
336 }
337 });
338
339 var likelyPushedTestMergeCommit =
340 repositorySettings!.PushTestMergeCommits!.Value
341 && repositorySettings.AccessToken != null
342 && repositorySettings.AccessUser != null;
343 using (repo)
344 compileJob = await Compile(
345 job,
346 revInfo!,
347 dreamMakerSettings!,
348 ddSettings!,
349 repo!,
350 remoteDeploymentManager!,
351 progressReporter,
352 averageSpan,
353 likelyPushedTestMergeCommit,
354 cancellationToken);
355
356 var activeCompileJob = await compileJobConsumer.LatestCompileJob();
357 try
358 {
359 await databaseContextFactory.UseContext(
360 async databaseContext =>
361 {
362 var fullJob = compileJob.Job;
363 compileJob.Job = new Models.Job(job.Require(x => x.Id));
364 var fullRevInfo = compileJob.RevisionInformation;
365 compileJob.RevisionInformation = new Models.RevisionInformation
366 {
367 Id = revInfo!.Id,
368 };
369
370 databaseContext.Jobs.Attach(compileJob.Job);
371 databaseContext.RevisionInformations.Attach(compileJob.RevisionInformation);
372 databaseContext.CompileJobs.Add(compileJob);
373
374 // The difficulty with compile jobs is they have a two part commit
375 await databaseContext.Save(cancellationToken);
376 logger.LogTrace("Created CompileJob {compileJobId}", compileJob.Id);
377 try
378 {
379 var chatNotificationAction = currentChatCallback!(null, compileJob.Output!);
380 await compileJobConsumer.LoadCompileJob(compileJob, chatNotificationAction, cancellationToken);
381 success = true;
382 }
383 catch
384 {
385 // So we need to un-commit the compile job if the above throws
386 databaseContext.CompileJobs.Remove(compileJob);
387
388 // DCT: Cancellation token is for job, operation must run regardless
389 await databaseContext.Save(CancellationToken.None);
390 throw;
391 }
392
393 compileJob.Job = fullJob;
394 compileJob.RevisionInformation = fullRevInfo;
395 });
396 }
397 catch (Exception ex)
398 {
399 await CleanupFailedCompile(compileJob, remoteDeploymentManager!, ex);
400 throw;
401 }
402
403 var commentsTask = remoteDeploymentManager!.PostDeploymentComments(
404 compileJob,
405 activeCompileJob?.RevisionInformation,
406 repositorySettings,
407 repoOwner,
408 repoName,
409 cancellationToken);
410
411 var eventTask = eventConsumer.HandleEvent(EventType.DeploymentComplete, Enumerable.Empty<string>(), false, cancellationToken);
412
413 try
414 {
415 await ValueTaskExtensions.WhenAll(commentsTask, eventTask);
416 }
417 catch (Exception ex)
418 {
419 throw new JobException(ErrorCode.PostDeployFailure, ex);
420 }
421 finally
422 {
423 currentChatCallback = null;
424 }
425 }
426 catch (Exception ex)
427 {
428 currentChatCallback?.Invoke(
431
432 throw;
433 }
434 finally
435 {
436 deploying = false;
437 if (success)
439 else
440 failedDeployments.Inc();
441 }
442 }
443#pragma warning restore CA1506
444
451 async ValueTask<TimeSpan?> CalculateExpectedDeploymentTime(IDatabaseContext databaseContext, CancellationToken cancellationToken)
452 {
453 var previousCompileJobs = await databaseContext
455 .AsQueryable()
456 .Where(x => x.Job.Instance!.Id == metadata.Id)
457 .OrderByDescending(x => x.Job.StoppedAt)
458 .Take(10)
459 .Select(x => new
460 {
461 StoppedAt = x.Job.StoppedAt!.Value,
462 StartedAt = x.Job.StartedAt!.Value,
463 })
464 .ToListAsync(cancellationToken);
465
466 TimeSpan? averageSpan = null;
467 if (previousCompileJobs.Count != 0)
468 {
469 var totalSpan = TimeSpan.Zero;
470 foreach (var previousCompileJob in previousCompileJobs)
471 totalSpan += previousCompileJob.StoppedAt - previousCompileJob.StartedAt;
472 averageSpan = totalSpan / previousCompileJobs.Count;
473 }
474
475 return averageSpan;
476 }
477
492 async ValueTask<Models.CompileJob> Compile(
493 Models.Job job,
494 Models.RevisionInformation revisionInformation,
495 Api.Models.Internal.DreamMakerSettings dreamMakerSettings,
496 DreamDaemonLaunchParameters launchParameters,
497 IRepository repository,
498 IRemoteDeploymentManager remoteDeploymentManager,
499 JobProgressReporter progressReporter,
500 TimeSpan? estimatedDuration,
501 bool localCommitExistsOnRemote,
502 CancellationToken cancellationToken)
503 {
504 logger.LogTrace("Begin Compile");
505
506 using var progressCts = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken);
507
508 progressReporter.StageName = "Reserving BYOND version";
509 var progressTask = ProgressTask(progressReporter, estimatedDuration, progressCts.Token);
510 try
511 {
512 using var engineLock = await engineManager.UseExecutables(null, null, cancellationToken);
514 revisionInformation,
515 engineLock.Version,
516 DateTimeOffset.UtcNow + estimatedDuration,
517 repository.RemoteRepositoryOwner,
518 repository.RemoteRepositoryName,
519 localCommitExistsOnRemote);
520
521 var compileJob = new Models.CompileJob(job, revisionInformation, engineLock.Version.ToString())
522 {
523 DirectoryName = Guid.NewGuid(),
524 DmeName = dreamMakerSettings.ProjectName,
525 RepositoryOrigin = repository.Origin.ToString(),
526 };
527
528 progressReporter.StageName = "Creating remote deployment notification";
529 await remoteDeploymentManager.StartDeployment(
530 repository,
531 compileJob,
532 cancellationToken);
533
534 logger.LogTrace("Deployment will timeout at {timeoutTime}", DateTimeOffset.UtcNow + dreamMakerSettings.Timeout!.Value);
535 using var timeoutTokenSource = new CancellationTokenSource(dreamMakerSettings.Timeout.Value);
536 var timeoutToken = timeoutTokenSource.Token;
537 using (timeoutToken.Register(() => logger.LogWarning("Deployment timed out!")))
538 {
539 using var combinedTokenSource = CancellationTokenSource.CreateLinkedTokenSource(timeoutToken, cancellationToken);
540 try
541 {
542 await RunCompileJob(
543 progressReporter,
544 compileJob,
545 dreamMakerSettings,
546 launchParameters,
547 engineLock,
548 repository,
549 remoteDeploymentManager,
550 combinedTokenSource.Token);
551 }
552 catch (OperationCanceledException) when (timeoutToken.IsCancellationRequested)
553 {
554 throw new JobException(ErrorCode.DeploymentTimeout);
555 }
556 }
557
558 return compileJob;
559 }
560 catch (OperationCanceledException)
561 {
562 // DCT: Cancellation token is for job, delaying here is fine
563 progressReporter.StageName = "Running CompileCancelled event";
564 await eventConsumer.HandleEvent(EventType.CompileCancelled, Enumerable.Empty<string>(), true, CancellationToken.None);
565 throw;
566 }
567 finally
568 {
569 progressCts.Cancel();
570 await progressTask;
571 }
572 }
573
586 async ValueTask RunCompileJob(
587 JobProgressReporter progressReporter,
588 Models.CompileJob job,
589 Api.Models.Internal.DreamMakerSettings dreamMakerSettings,
590 DreamDaemonLaunchParameters launchParameters,
591 IEngineExecutableLock engineLock,
592 IRepository repository,
593 IRemoteDeploymentManager remoteDeploymentManager,
594 CancellationToken cancellationToken)
595 {
596 var outputDirectory = job.DirectoryName!.Value.ToString();
597 logger.LogTrace("Compile output GUID: {dirGuid}", outputDirectory);
598
599 try
600 {
601 // copy the repository
602 logger.LogTrace("Copying repository to game directory");
603 progressReporter.StageName = "Copying repository";
604 var resolvedOutputDirectory = ioManager.ResolvePath(outputDirectory);
605 var repoOrigin = repository.Origin;
606 var repoReference = repository.Reference;
607 using (repository)
608 await repository.CopyTo(resolvedOutputDirectory, cancellationToken);
609
610 // repository closed now
611
612 // run precompile scripts
613 progressReporter.StageName = "Running PreCompile event";
615 EventType.CompileStart,
616 new List<string>
617 {
618 resolvedOutputDirectory,
619 repoOrigin.ToString(),
620 engineLock.Version.ToString(),
621 repoReference,
622 },
623 true,
624 cancellationToken);
625
626 // determine the dme
627 progressReporter.StageName = "Determining .dme";
628 if (job.DmeName == null)
629 {
630 logger.LogTrace("Searching for available .dmes");
631 var foundPaths = await ioManager.GetFilesWithExtension(resolvedOutputDirectory, DmeExtension, true, cancellationToken);
632 var foundPath = foundPaths.FirstOrDefault();
633 if (foundPath == default)
634 throw new JobException(ErrorCode.DeploymentNoDme);
635 job.DmeName = foundPath.Substring(
636 resolvedOutputDirectory.Length + 1,
637 foundPath.Length - resolvedOutputDirectory.Length - DmeExtension.Length - 2); // +1 for . in extension
638 }
639 else
640 {
641 var targetDme = ioManager.ConcatPath(outputDirectory, String.Join('.', job.DmeName, DmeExtension));
642 if (!await ioManager.PathIsChildOf(outputDirectory, targetDme, cancellationToken))
643 throw new JobException(ErrorCode.DeploymentWrongDme);
644
645 var targetDmeExists = await ioManager.FileExists(targetDme, cancellationToken);
646 if (!targetDmeExists)
647 throw new JobException(ErrorCode.DeploymentMissingDme);
648 }
649
650 logger.LogDebug("Selected \"{dmeName}.dme\" for compilation!", job.DmeName);
651
652 progressReporter.StageName = "Modifying .dme";
653 await ModifyDme(job, cancellationToken);
654
655 // run precompile scripts
656 progressReporter.StageName = "Running PreDreamMaker event";
658 EventType.PreDreamMaker,
659 new List<string>
660 {
661 resolvedOutputDirectory,
662 repoOrigin.ToString(),
663 engineLock.Version.ToString(),
664 },
665 true,
666 cancellationToken);
667
668 // run compiler
669 progressReporter.StageName = "Running Compiler";
670 var compileSuceeded = await RunDreamMaker(engineLock, job, dreamMakerSettings.CompilerAdditionalArguments, cancellationToken);
671
672 // Session takes ownership of the lock and Disposes it so save this for later
673 var engineVersion = engineLock.Version;
674
675 // verify api
676 try
677 {
678 if (!compileSuceeded)
679 throw new JobException(
680 ErrorCode.DeploymentExitCode,
681 new JobException($"Compilation failed:{Environment.NewLine}{Environment.NewLine}{job.Output}"));
682
683 await VerifyApi(
684 launchParameters.StartupTimeout!.Value,
685 dreamMakerSettings.ApiValidationSecurityLevel!.Value,
686 job,
687 progressReporter,
688 engineLock,
689 dreamMakerSettings.ApiValidationPort!.Value,
690 dreamMakerSettings.DMApiValidationMode!.Value,
691 launchParameters.LogOutput!.Value,
692 cancellationToken);
693 }
694 catch (JobException)
695 {
696 // DD never validated or compile failed
697 progressReporter.StageName = "Running CompileFailure event";
699 EventType.CompileFailure,
700 new List<string>
701 {
702 resolvedOutputDirectory,
703 compileSuceeded ? "1" : "0",
704 engineVersion.ToString(),
705 },
706 true,
707 cancellationToken);
708 throw;
709 }
710
711 progressReporter.StageName = "Running CompileComplete event";
713 EventType.CompileComplete,
714 new List<string>
715 {
716 resolvedOutputDirectory,
717 engineVersion.ToString(),
718 },
719 true,
720 cancellationToken);
721
722 logger.LogTrace("Applying static game file symlinks...");
723 progressReporter.StageName = "Symlinking GameStaticFiles";
724
725 // symlink in the static data
726 await configuration.SymlinkStaticFilesTo(resolvedOutputDirectory, cancellationToken);
727
728 logger.LogDebug("Compile complete!");
729 }
730 catch (Exception ex)
731 {
732 progressReporter.StageName = "Cleaning output directory";
733 await CleanupFailedCompile(job, remoteDeploymentManager, ex);
734 throw;
735 }
736 }
737
745 async ValueTask ProgressTask(JobProgressReporter progressReporter, TimeSpan? estimatedDuration, CancellationToken cancellationToken)
746 {
747 double? lastReport = estimatedDuration.HasValue ? 0 : null;
748 progressReporter.ReportProgress(lastReport);
749
750 var minimumSleepInterval = TimeSpan.FromMilliseconds(250);
751 var sleepInterval = estimatedDuration.HasValue ? estimatedDuration.Value / 100 : minimumSleepInterval;
752
753 if (estimatedDuration.HasValue)
754 {
755 logger.LogDebug("Compile is expected to take: {estimatedDuration}", estimatedDuration);
756 }
757 else
758 {
759 logger.LogTrace("No metric to estimate compile time.");
760 }
761
762 try
763 {
764 for (var iteration = 0; iteration < (estimatedDuration.HasValue ? 99 : Int32.MaxValue); ++iteration)
765 {
766 if (estimatedDuration.HasValue)
767 {
768 var nextInterval = DateTimeOffset.UtcNow + sleepInterval;
769 do
770 {
771 var remainingSleepThisInterval = nextInterval - DateTimeOffset.UtcNow;
772 var nextSleepSpan = remainingSleepThisInterval < minimumSleepInterval ? minimumSleepInterval : remainingSleepThisInterval;
773
774 await asyncDelayer.Delay(nextSleepSpan, cancellationToken);
775 progressReporter.ReportProgress(lastReport);
776 }
777 while (DateTimeOffset.UtcNow < nextInterval);
778 }
779 else
780 await asyncDelayer.Delay(minimumSleepInterval, cancellationToken);
781
782 lastReport = estimatedDuration.HasValue ? sleepInterval * (iteration + 1) / estimatedDuration.Value : null;
783 progressReporter.ReportProgress(lastReport);
784 }
785 }
786 catch (OperationCanceledException ex)
787 {
788 logger.LogTrace(ex, "ProgressTask aborted.");
789 }
790 catch (Exception ex)
791 {
792 logger.LogError(ex, "ProgressTask crashed!");
793 }
794 }
795
809 async ValueTask VerifyApi(
810 uint timeout,
811 DreamDaemonSecurity securityLevel,
812 Models.CompileJob job,
813 JobProgressReporter progressReporter,
814 IEngineExecutableLock engineLock,
815 ushort portToUse,
816 DMApiValidationMode validationMode,
817 bool logOutput,
818 CancellationToken cancellationToken)
819 {
820 if (validationMode == DMApiValidationMode.Skipped)
821 {
822 logger.LogDebug("Skipping DMAPI validation");
823 job.MinimumSecurityLevel = DreamDaemonSecurity.Ultrasafe;
824 return;
825 }
826
827 progressReporter.StageName = "Validating DMAPI";
828
829 var requireValidate = validationMode == DMApiValidationMode.Required;
830 logger.LogTrace("Verifying {possiblyRequired}DMAPI...", requireValidate ? "required " : String.Empty);
831 var launchParameters = new DreamDaemonLaunchParameters
832 {
833 AllowWebClient = false,
834 Port = portToUse,
835 OpenDreamTopicPort = 0,
836 SecurityLevel = securityLevel,
837 Visibility = DreamDaemonVisibility.Invisible,
838 StartupTimeout = timeout,
839 TopicRequestTimeout = 0, // not used
840 HealthCheckSeconds = 0, // not used
841 StartProfiler = false,
842 LogOutput = logOutput,
843 MapThreads = 1, // lowest possible amount
844 };
845
846 job.MinimumSecurityLevel = securityLevel; // needed for the TempDmbProvider
847
848 ApiValidationStatus validationStatus;
849 await using (var provider = new TemporaryDmbProvider(
850 ioManager.ResolvePath(job.DirectoryName!.Value.ToString()),
851 job,
852 engineLock.Version))
853 await using (var controller = await sessionControllerFactory.LaunchNew(provider, engineLock, launchParameters, true, cancellationToken))
854 {
855 var launchResult = await controller.LaunchResult.WaitAsync(cancellationToken);
856
857 if (launchResult.StartupTime.HasValue)
858 await controller.Lifetime.WaitAsync(cancellationToken);
859
860 if (!controller.Lifetime.IsCompleted)
861 await controller.DisposeAsync();
862
863 validationStatus = controller.ApiValidationStatus;
864
865 logger.LogTrace("API validation status: {validationStatus}", validationStatus);
866
867 job.DMApiVersion = controller.DMApiVersion;
868 }
869
870 switch (validationStatus)
871 {
872 case ApiValidationStatus.RequiresUltrasafe:
873 job.MinimumSecurityLevel = DreamDaemonSecurity.Ultrasafe;
874 return;
875 case ApiValidationStatus.RequiresSafe:
876 job.MinimumSecurityLevel = DreamDaemonSecurity.Safe;
877 return;
878 case ApiValidationStatus.RequiresTrusted:
879 job.MinimumSecurityLevel = DreamDaemonSecurity.Trusted;
880 return;
881 case ApiValidationStatus.NeverValidated:
882 if (requireValidate)
883 throw new JobException(ErrorCode.DeploymentNeverValidated);
884 job.MinimumSecurityLevel = DreamDaemonSecurity.Ultrasafe;
885 break;
886 case ApiValidationStatus.BadValidationRequest:
887 case ApiValidationStatus.Incompatible:
888 throw new JobException(ErrorCode.DeploymentInvalidValidation);
889 case ApiValidationStatus.UnaskedValidationRequest:
890 default:
891 throw new InvalidOperationException(
892 $"Session controller returned unexpected ApiValidationStatus: {validationStatus}");
893 }
894 }
895
904 async ValueTask<bool> RunDreamMaker(
905 IEngineExecutableLock engineLock,
906 Models.CompileJob job,
907 string? additionalCompilerArguments,
908 CancellationToken cancellationToken)
909 {
910 var environment = await engineLock.LoadEnv(logger, true, cancellationToken);
911 var arguments = engineLock.FormatCompilerArguments($"{job.DmeName}.{DmeExtension}", additionalCompilerArguments);
912
913 await using var dm = await processExecutor.LaunchProcess(
914 engineLock.CompilerExePath,
915 ioManager.ResolvePath(
916 job.DirectoryName!.Value.ToString()),
917 arguments,
918 cancellationToken,
919 environment,
920 readStandardHandles: true,
921 noShellExecute: true);
922
923 if (sessionConfiguration.LowPriorityDeploymentProcesses)
924 dm.AdjustPriority(false);
925
926 int exitCode;
927 using (cancellationToken.Register(() => dm.Terminate()))
928 exitCode = (await dm.Lifetime).Value;
929 cancellationToken.ThrowIfCancellationRequested();
930
931 logger.LogDebug("DreamMaker exit code: {exitCode}", exitCode);
932 job.Output = $"{await dm.GetCombinedOutput(cancellationToken)}{Environment.NewLine}{Environment.NewLine}Exit Code: {exitCode}";
933 logger.LogDebug("DreamMaker output: {newLine}{output}", Environment.NewLine, job.Output);
934
935 currentDreamMakerOutput = job.Output;
936 return exitCode == 0;
937 }
938
945 async ValueTask ModifyDme(Models.CompileJob job, CancellationToken cancellationToken)
946 {
947 var dmeFileName = String.Join('.', job.DmeName, DmeExtension);
948 var stringDirectoryName = job.DirectoryName!.Value.ToString();
949 var dmePath = ioManager.ConcatPath(stringDirectoryName, dmeFileName);
950 var dmeReadTask = ioManager.ReadAllBytes(dmePath, cancellationToken);
951
952 var dmeModificationsTask = configuration.CopyDMFilesTo(
953 dmeFileName,
954 ioManager.ResolvePath(
955 ioManager.ConcatPath(
956 stringDirectoryName,
957 ioManager.GetDirectoryName(dmeFileName))),
958 cancellationToken);
959
960 var dmeBytes = await dmeReadTask;
961 var dme = Encoding.UTF8.GetString(dmeBytes);
962
963 var dmeModifications = await dmeModificationsTask;
964
965 if (dmeModifications == null || dmeModifications.TotalDmeOverwrite)
966 {
967 if (dmeModifications != null)
968 logger.LogDebug(".dme replacement configured!");
969 else
970 logger.LogTrace("No .dme modifications required.");
971 return;
972 }
973
974 var dmeLines = new List<string>(dme.Split('\n', StringSplitOptions.None));
975 for (var dmeLineIndex = 0; dmeLineIndex < dmeLines.Count; ++dmeLineIndex)
976 {
977 var line = dmeLines[dmeLineIndex];
978 if (line.Contains("BEGIN_INCLUDE", StringComparison.Ordinal) && dmeModifications.HeadIncludeLine != null)
979 {
980 var headIncludeLineNumber = dmeLineIndex + 1;
981 logger.LogDebug(
982 "Inserting HeadInclude.dm at line {lineNumber}: {includeLine}",
983 headIncludeLineNumber,
984 dmeModifications.HeadIncludeLine);
985 dmeLines.Insert(headIncludeLineNumber, dmeModifications.HeadIncludeLine);
986 ++dmeLineIndex;
987 }
988 else if (line.Contains("END_INCLUDE", StringComparison.Ordinal) && dmeModifications.TailIncludeLine != null)
989 {
990 logger.LogDebug(
991 "Inserting TailInclude.dm at line {lineNumber}: {includeLine}",
992 dmeLineIndex,
993 dmeModifications.TailIncludeLine);
994 dmeLines.Insert(dmeLineIndex, dmeModifications.TailIncludeLine);
995 break;
996 }
997 }
998
999 dmeBytes = Encoding.UTF8.GetBytes(String.Join('\n', dmeLines));
1000 await ioManager.WriteAllBytes(dmePath, dmeBytes, cancellationToken);
1001 }
1002
1010 ValueTask CleanupFailedCompile(Models.CompileJob job, IRemoteDeploymentManager remoteDeploymentManager, Exception exception)
1011 {
1012 async ValueTask CleanDir()
1013 {
1014 if (sessionConfiguration.DelayCleaningFailedDeployments)
1015 {
1016 logger.LogDebug("Not cleaning up errored deployment directory {guid} due to config.", job.DirectoryName);
1017 return;
1018 }
1019
1020 logger.LogTrace("Cleaning compile directory...");
1021 var jobPath = job.DirectoryName!.Value.ToString();
1022 try
1023 {
1024 // DCT: None available
1025 await eventConsumer.HandleEvent(EventType.DeploymentCleanup, new List<string> { jobPath }, true, CancellationToken.None);
1026 await ioManager.DeleteDirectory(jobPath, CancellationToken.None);
1027 }
1028 catch (Exception e)
1029 {
1030 logger.LogWarning(e, "Error cleaning up compile directory {path}!", ioManager.ResolvePath(jobPath));
1031 }
1032 }
1033
1034 var dirCleanTask = CleanDir();
1035
1036 var failRemoteDeployTask = remoteDeploymentManager.FailDeployment(
1037 job,
1038 FormatExceptionForUsers(exception),
1039 CancellationToken.None); // DCT: None available
1040
1042 dirCleanTask,
1043 failRemoteDeployTask);
1044 }
1045 }
1046}
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.
readonly IRepositoryManager repositoryManager
The IRepositoryManager for DreamMaker.
Definition DreamMaker.cs:78
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.
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, ILogger< DreamMaker > logger, SessionConfiguration sessionConfiguration, Api.Models.Instance metadata)
Initializes a new instance of the DreamMaker class.
readonly ILogger< DreamMaker > logger
The ILogger for DreamMaker.
Definition DreamMaker.cs:98
readonly SessionConfiguration sessionConfiguration
The SessionConfiguration 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:53
const string DmeExtension
Extension for .dmes.
Definition DreamMaker.cs:38
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:48
readonly IRemoteDeploymentManagerFactory remoteDeploymentManagerFactory
The IRemoteDeploymentManagerFactory for DreamMaker.
Definition DreamMaker.cs:88
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:58
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:73
readonly IEngineManager engineManager
The IEngineManager for DreamMaker.
Definition DreamMaker.cs:43
readonly ICompileJobSink compileJobConsumer
The ICompileJobSink for DreamMaker.
Definition DreamMaker.cs:83
readonly IEventConsumer eventConsumer
The IEventConsumer for DreamMaker.
Definition DreamMaker.cs:63
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.
async ValueTask< Models.CompileJob > Compile(Models.Job job, 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 IChatManager chatManager
The IChatManager for DreamMaker.
Definition DreamMaker.cs:68
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.
readonly IAsyncDelayer asyncDelayer
The IAsyncDelayer for DreamMaker.
Definition DreamMaker.cs:93
Repository(LibGit2Sharp.IRepository libGitRepo, ILibGit2Commands commands, IIOManager ioManager, IEventConsumer eventConsumer, ICredentialsProvider credentialsProvider, IPostWriteHandler postWriteHandler, IGitRemoteFeaturesFactory gitRemoteFeaturesFactory, ILibGit2RepositoryFactory submoduleFactory, ILogger< Repository > logger, GeneralConfiguration generalConfiguration, Action disposeAction)
Initializes a new instance of the Repository class.
Configuration options for the game sessions.
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, 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:13
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.