tgstation-server 6.16.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 Models.CompileJob? oldCompileJob;
340 using (repo)
341 {
342 var likelyPushedTestMergeCommit =
343 repositorySettings!.PushTestMergeCommits!.Value
344 && repositorySettings.AccessToken != null
345 && repositorySettings.AccessUser != null;
346 oldCompileJob = await compileJobConsumer.LatestCompileJob();
347 compileJob = await Compile(
348 job,
349 oldCompileJob,
350 revInfo!,
351 dreamMakerSettings!,
352 ddSettings!,
353 repo!,
354 remoteDeploymentManager!,
355 progressReporter,
356 averageSpan,
357 likelyPushedTestMergeCommit,
358 cancellationToken);
359 }
360
361 try
362 {
363 await databaseContextFactory.UseContext(
364 async databaseContext =>
365 {
366 var fullJob = compileJob.Job;
367 compileJob.Job = new Models.Job(job.Require(x => x.Id));
368 var fullRevInfo = compileJob.RevisionInformation;
369 compileJob.RevisionInformation = new Models.RevisionInformation
370 {
371 Id = revInfo!.Id,
372 };
373
374 databaseContext.Jobs.Attach(compileJob.Job);
375 databaseContext.RevisionInformations.Attach(compileJob.RevisionInformation);
376 databaseContext.CompileJobs.Add(compileJob);
377
378 // The difficulty with compile jobs is they have a two part commit
379 await databaseContext.Save(cancellationToken);
380 logger.LogTrace("Created CompileJob {compileJobId}", compileJob.Id);
381 try
382 {
383 var chatNotificationAction = currentChatCallback!(null, compileJob.Output!);
384 await compileJobConsumer.LoadCompileJob(compileJob, chatNotificationAction, cancellationToken);
385 success = true;
386 }
387 catch
388 {
389 // So we need to un-commit the compile job if the above throws
390 databaseContext.CompileJobs.Remove(compileJob);
391
392 // DCT: Cancellation token is for job, operation must run regardless
393 await databaseContext.Save(CancellationToken.None);
394 throw;
395 }
396
397 compileJob.Job = fullJob;
398 compileJob.RevisionInformation = fullRevInfo;
399 });
400 }
401 catch (Exception ex)
402 {
403 await CleanupFailedCompile(compileJob, remoteDeploymentManager!, ex);
404 throw;
405 }
406
407 var commentsTask = remoteDeploymentManager!.PostDeploymentComments(
408 compileJob,
409 oldCompileJob?.RevisionInformation,
410 repositorySettings,
411 repoOwner,
412 repoName,
413 cancellationToken);
414
415 var eventTask = eventConsumer.HandleEvent(EventType.DeploymentComplete, Enumerable.Empty<string>(), false, cancellationToken);
416
417 try
418 {
419 await ValueTaskExtensions.WhenAll(commentsTask, eventTask);
420 }
421 catch (Exception ex)
422 {
423 throw new JobException(ErrorCode.PostDeployFailure, ex);
424 }
425 finally
426 {
427 currentChatCallback = null;
428 }
429 }
430 catch (Exception ex)
431 {
432 currentChatCallback?.Invoke(
435
436 throw;
437 }
438 finally
439 {
440 deploying = false;
441 if (success)
443 else
444 failedDeployments.Inc();
445 }
446 }
447#pragma warning restore CA1506
448
455 async ValueTask<TimeSpan?> CalculateExpectedDeploymentTime(IDatabaseContext databaseContext, CancellationToken cancellationToken)
456 {
457 var previousCompileJobs = await databaseContext
459 .AsQueryable()
460 .Where(x => x.Job.Instance!.Id == metadata.Id)
461 .OrderByDescending(x => x.Job.StoppedAt)
462 .Take(10)
463 .Select(x => new
464 {
465 StoppedAt = x.Job.StoppedAt!.Value,
466 StartedAt = x.Job.StartedAt!.Value,
467 })
468 .ToListAsync(cancellationToken);
469
470 TimeSpan? averageSpan = null;
471 if (previousCompileJobs.Count != 0)
472 {
473 var totalSpan = TimeSpan.Zero;
474 foreach (var previousCompileJob in previousCompileJobs)
475 totalSpan += previousCompileJob.StoppedAt - previousCompileJob.StartedAt;
476 averageSpan = totalSpan / previousCompileJobs.Count;
477 }
478
479 return averageSpan;
480 }
481
497 async ValueTask<Models.CompileJob> Compile(
498 Models.Job job,
499 Models.CompileJob? oldCompileJob,
500 Models.RevisionInformation revisionInformation,
501 Api.Models.Internal.DreamMakerSettings dreamMakerSettings,
502 DreamDaemonLaunchParameters launchParameters,
503 IRepository repository,
504 IRemoteDeploymentManager remoteDeploymentManager,
505 JobProgressReporter progressReporter,
506 TimeSpan? estimatedDuration,
507 bool localCommitExistsOnRemote,
508 CancellationToken cancellationToken)
509 {
510 logger.LogTrace("Begin Compile");
511
512 using var progressCts = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken);
513
514 progressReporter.StageName = "Reserving BYOND version";
515 var progressTask = ProgressTask(progressReporter, estimatedDuration, progressCts.Token);
516 try
517 {
518 using var engineLock = await engineManager.UseExecutables(null, null, cancellationToken);
520 revisionInformation,
521 oldCompileJob?.RevisionInformation,
522 engineLock.Version,
523 DateTimeOffset.UtcNow + estimatedDuration,
524 repository.RemoteRepositoryOwner,
525 repository.RemoteRepositoryName,
526 localCommitExistsOnRemote);
527
528 var compileJob = new Models.CompileJob(job, revisionInformation, engineLock.Version.ToString())
529 {
530 DirectoryName = Guid.NewGuid(),
531 DmeName = dreamMakerSettings.ProjectName,
532 RepositoryOrigin = repository.Origin.ToString(),
533 };
534
535 progressReporter.StageName = "Creating remote deployment notification";
536 await remoteDeploymentManager.StartDeployment(
537 repository,
538 compileJob,
539 cancellationToken);
540
541 logger.LogTrace("Deployment will timeout at {timeoutTime}", DateTimeOffset.UtcNow + dreamMakerSettings.Timeout!.Value);
542 using var timeoutTokenSource = new CancellationTokenSource(dreamMakerSettings.Timeout.Value);
543 var timeoutToken = timeoutTokenSource.Token;
544 using (timeoutToken.Register(() => logger.LogWarning("Deployment timed out!")))
545 {
546 using var combinedTokenSource = CancellationTokenSource.CreateLinkedTokenSource(timeoutToken, cancellationToken);
547 try
548 {
549 await RunCompileJob(
550 progressReporter,
551 compileJob,
552 dreamMakerSettings,
553 launchParameters,
554 engineLock,
555 repository,
556 remoteDeploymentManager,
557 combinedTokenSource.Token);
558 }
559 catch (OperationCanceledException) when (timeoutToken.IsCancellationRequested)
560 {
561 throw new JobException(ErrorCode.DeploymentTimeout);
562 }
563 }
564
565 return compileJob;
566 }
567 catch (OperationCanceledException)
568 {
569 // DCT: Cancellation token is for job, delaying here is fine
570 progressReporter.StageName = "Running CompileCancelled event";
571 await eventConsumer.HandleEvent(EventType.CompileCancelled, Enumerable.Empty<string>(), true, CancellationToken.None);
572 throw;
573 }
574 finally
575 {
576 progressCts.Cancel();
577 await progressTask;
578 }
579 }
580
593 async ValueTask RunCompileJob(
594 JobProgressReporter progressReporter,
595 Models.CompileJob job,
596 Api.Models.Internal.DreamMakerSettings dreamMakerSettings,
597 DreamDaemonLaunchParameters launchParameters,
598 IEngineExecutableLock engineLock,
599 IRepository repository,
600 IRemoteDeploymentManager remoteDeploymentManager,
601 CancellationToken cancellationToken)
602 {
603 var outputDirectory = job.DirectoryName!.Value.ToString();
604 logger.LogTrace("Compile output GUID: {dirGuid}", outputDirectory);
605
606 try
607 {
608 // copy the repository
609 logger.LogTrace("Copying repository to game directory");
610 progressReporter.StageName = "Copying repository";
611 var resolvedOutputDirectory = ioManager.ResolvePath(outputDirectory);
612 var repoOrigin = repository.Origin;
613 var repoReference = repository.Reference;
614 using (repository)
615 await repository.CopyTo(resolvedOutputDirectory, cancellationToken);
616
617 // repository closed now
618
619 // run precompile scripts
620 progressReporter.StageName = "Running PreCompile event";
622 EventType.CompileStart,
623 new List<string>
624 {
625 resolvedOutputDirectory,
626 repoOrigin.ToString(),
627 engineLock.Version.ToString(),
628 repoReference,
629 },
630 true,
631 cancellationToken);
632
633 // determine the dme
634 progressReporter.StageName = "Determining .dme";
635 if (job.DmeName == null)
636 {
637 logger.LogTrace("Searching for available .dmes");
638 var foundPaths = await ioManager.GetFilesWithExtension(resolvedOutputDirectory, DmeExtension, true, cancellationToken);
639 var foundPath = foundPaths.FirstOrDefault();
640 if (foundPath == default)
641 throw new JobException(ErrorCode.DeploymentNoDme);
642 job.DmeName = foundPath.Substring(
643 resolvedOutputDirectory.Length + 1,
644 foundPath.Length - resolvedOutputDirectory.Length - DmeExtension.Length - 2); // +1 for . in extension
645 }
646 else
647 {
648 var targetDme = ioManager.ConcatPath(outputDirectory, String.Join('.', job.DmeName, DmeExtension));
649 if (!await ioManager.PathIsChildOf(outputDirectory, targetDme, cancellationToken))
650 throw new JobException(ErrorCode.DeploymentWrongDme);
651
652 var targetDmeExists = await ioManager.FileExists(targetDme, cancellationToken);
653 if (!targetDmeExists)
654 throw new JobException(ErrorCode.DeploymentMissingDme);
655 }
656
657 logger.LogDebug("Selected \"{dmeName}.dme\" for compilation!", job.DmeName);
658
659 progressReporter.StageName = "Modifying .dme";
660 await ModifyDme(job, cancellationToken);
661
662 // run precompile scripts
663 progressReporter.StageName = "Running PreDreamMaker event";
665 EventType.PreDreamMaker,
666 new List<string>
667 {
668 resolvedOutputDirectory,
669 repoOrigin.ToString(),
670 engineLock.Version.ToString(),
671 },
672 true,
673 cancellationToken);
674
675 // run compiler
676 progressReporter.StageName = "Running Compiler";
677 var compileSuceeded = await RunDreamMaker(engineLock, job, dreamMakerSettings.CompilerAdditionalArguments, cancellationToken);
678
679 // Session takes ownership of the lock and Disposes it so save this for later
680 var engineVersion = engineLock.Version;
681
682 // verify api
683 try
684 {
685 if (!compileSuceeded)
686 throw new JobException(
687 ErrorCode.DeploymentExitCode,
688 new JobException($"Compilation failed:{Environment.NewLine}{Environment.NewLine}{job.Output}"));
689
690 await VerifyApi(
691 launchParameters.StartupTimeout!.Value,
692 dreamMakerSettings.ApiValidationSecurityLevel!.Value,
693 job,
694 progressReporter,
695 engineLock,
696 dreamMakerSettings.ApiValidationPort!.Value,
697 dreamMakerSettings.DMApiValidationMode!.Value,
698 launchParameters.LogOutput!.Value,
699 cancellationToken);
700 }
701 catch (JobException)
702 {
703 // DD never validated or compile failed
704 progressReporter.StageName = "Running CompileFailure event";
706 EventType.CompileFailure,
707 new List<string>
708 {
709 resolvedOutputDirectory,
710 compileSuceeded ? "1" : "0",
711 engineVersion.ToString(),
712 },
713 true,
714 cancellationToken);
715 throw;
716 }
717
718 progressReporter.StageName = "Running CompileComplete event";
720 EventType.CompileComplete,
721 new List<string>
722 {
723 resolvedOutputDirectory,
724 engineVersion.ToString(),
725 },
726 true,
727 cancellationToken);
728
729 logger.LogTrace("Applying static game file symlinks...");
730 progressReporter.StageName = "Symlinking GameStaticFiles";
731
732 // symlink in the static data
733 await configuration.SymlinkStaticFilesTo(resolvedOutputDirectory, cancellationToken);
734
735 logger.LogDebug("Compile complete!");
736 }
737 catch (Exception ex)
738 {
739 progressReporter.StageName = "Cleaning output directory";
740 await CleanupFailedCompile(job, remoteDeploymentManager, ex);
741 throw;
742 }
743 }
744
752 async ValueTask ProgressTask(JobProgressReporter progressReporter, TimeSpan? estimatedDuration, CancellationToken cancellationToken)
753 {
754 double? lastReport = estimatedDuration.HasValue ? 0 : null;
755 progressReporter.ReportProgress(lastReport);
756
757 var minimumSleepInterval = TimeSpan.FromMilliseconds(250);
758 var sleepInterval = estimatedDuration.HasValue ? estimatedDuration.Value / 100 : minimumSleepInterval;
759
760 if (estimatedDuration.HasValue)
761 {
762 logger.LogDebug("Compile is expected to take: {estimatedDuration}", estimatedDuration);
763 }
764 else
765 {
766 logger.LogTrace("No metric to estimate compile time.");
767 }
768
769 try
770 {
771 for (var iteration = 0; iteration < (estimatedDuration.HasValue ? 99 : Int32.MaxValue); ++iteration)
772 {
773 if (estimatedDuration.HasValue)
774 {
775 var nextInterval = DateTimeOffset.UtcNow + sleepInterval;
776 do
777 {
778 var remainingSleepThisInterval = nextInterval - DateTimeOffset.UtcNow;
779 var nextSleepSpan = remainingSleepThisInterval < minimumSleepInterval ? minimumSleepInterval : remainingSleepThisInterval;
780
781 await asyncDelayer.Delay(nextSleepSpan, cancellationToken);
782 progressReporter.ReportProgress(lastReport);
783 }
784 while (DateTimeOffset.UtcNow < nextInterval);
785 }
786 else
787 await asyncDelayer.Delay(minimumSleepInterval, cancellationToken);
788
789 lastReport = estimatedDuration.HasValue ? sleepInterval * (iteration + 1) / estimatedDuration.Value : null;
790 progressReporter.ReportProgress(lastReport);
791 }
792 }
793 catch (OperationCanceledException ex)
794 {
795 logger.LogTrace(ex, "ProgressTask aborted.");
796 }
797 catch (Exception ex)
798 {
799 logger.LogError(ex, "ProgressTask crashed!");
800 }
801 }
802
816 async ValueTask VerifyApi(
817 uint timeout,
818 DreamDaemonSecurity securityLevel,
819 Models.CompileJob job,
820 JobProgressReporter progressReporter,
821 IEngineExecutableLock engineLock,
822 ushort portToUse,
823 DMApiValidationMode validationMode,
824 bool logOutput,
825 CancellationToken cancellationToken)
826 {
827 if (validationMode == DMApiValidationMode.Skipped)
828 {
829 logger.LogDebug("Skipping DMAPI validation");
830 job.MinimumSecurityLevel = DreamDaemonSecurity.Ultrasafe;
831 return;
832 }
833
834 progressReporter.StageName = "Validating DMAPI";
835
836 var requireValidate = validationMode == DMApiValidationMode.Required;
837 logger.LogTrace("Verifying {possiblyRequired}DMAPI...", requireValidate ? "required " : String.Empty);
838 var launchParameters = new DreamDaemonLaunchParameters
839 {
840 AllowWebClient = false,
841 Port = portToUse,
842 OpenDreamTopicPort = 0,
843 SecurityLevel = securityLevel,
844 Visibility = DreamDaemonVisibility.Invisible,
845 StartupTimeout = timeout,
846 TopicRequestTimeout = 0, // not used
847 HealthCheckSeconds = 0, // not used
848 StartProfiler = false,
849 LogOutput = logOutput,
850 MapThreads = 1, // lowest possible amount
851 };
852
853 job.MinimumSecurityLevel = securityLevel; // needed for the TempDmbProvider
854
855 ApiValidationStatus validationStatus;
856 await using (var provider = new TemporaryDmbProvider(
857 ioManager.ResolvePath(job.DirectoryName!.Value.ToString()),
858 job,
859 engineLock.Version))
860 await using (var controller = await sessionControllerFactory.LaunchNew(provider, engineLock, launchParameters, true, cancellationToken))
861 {
862 var launchResult = await controller.LaunchResult.WaitAsync(cancellationToken);
863
864 if (launchResult.StartupTime.HasValue)
865 await controller.Lifetime.WaitAsync(cancellationToken);
866
867 if (!controller.Lifetime.IsCompleted)
868 await controller.DisposeAsync();
869
870 validationStatus = controller.ApiValidationStatus;
871
872 logger.LogTrace("API validation status: {validationStatus}", validationStatus);
873
874 job.DMApiVersion = controller.DMApiVersion;
875 }
876
877 switch (validationStatus)
878 {
879 case ApiValidationStatus.RequiresUltrasafe:
880 job.MinimumSecurityLevel = DreamDaemonSecurity.Ultrasafe;
881 return;
882 case ApiValidationStatus.RequiresSafe:
883 job.MinimumSecurityLevel = DreamDaemonSecurity.Safe;
884 return;
885 case ApiValidationStatus.RequiresTrusted:
886 job.MinimumSecurityLevel = DreamDaemonSecurity.Trusted;
887 return;
888 case ApiValidationStatus.NeverValidated:
889 if (requireValidate)
890 throw new JobException(ErrorCode.DeploymentNeverValidated);
891 job.MinimumSecurityLevel = DreamDaemonSecurity.Ultrasafe;
892 break;
893 case ApiValidationStatus.BadValidationRequest:
894 case ApiValidationStatus.Incompatible:
895 throw new JobException(ErrorCode.DeploymentInvalidValidation);
896 case ApiValidationStatus.UnaskedValidationRequest:
897 default:
898 throw new InvalidOperationException(
899 $"Session controller returned unexpected ApiValidationStatus: {validationStatus}");
900 }
901 }
902
911 async ValueTask<bool> RunDreamMaker(
912 IEngineExecutableLock engineLock,
913 Models.CompileJob job,
914 string? additionalCompilerArguments,
915 CancellationToken cancellationToken)
916 {
917 var environment = await engineLock.LoadEnv(logger, true, cancellationToken);
918 var arguments = engineLock.FormatCompilerArguments($"{job.DmeName}.{DmeExtension}", additionalCompilerArguments);
919
920 await using var dm = await processExecutor.LaunchProcess(
921 engineLock.CompilerExePath,
922 ioManager.ResolvePath(
923 job.DirectoryName!.Value.ToString()),
924 arguments,
925 cancellationToken,
926 environment,
927 readStandardHandles: true,
928 noShellExecute: true);
929
930 if (sessionConfiguration.LowPriorityDeploymentProcesses)
931 dm.AdjustPriority(false);
932
933 int exitCode;
934 using (cancellationToken.Register(() => dm.Terminate()))
935 exitCode = (await dm.Lifetime).Value;
936 cancellationToken.ThrowIfCancellationRequested();
937
938 logger.LogDebug("DreamMaker exit code: {exitCode}", exitCode);
939 job.Output = $"{await dm.GetCombinedOutput(cancellationToken)}{Environment.NewLine}{Environment.NewLine}Exit Code: {exitCode}";
940 logger.LogDebug("DreamMaker output: {newLine}{output}", Environment.NewLine, job.Output);
941
942 currentDreamMakerOutput = job.Output;
943 return exitCode == 0;
944 }
945
952 async ValueTask ModifyDme(Models.CompileJob job, CancellationToken cancellationToken)
953 {
954 var dmeFileName = String.Join('.', job.DmeName, DmeExtension);
955 var stringDirectoryName = job.DirectoryName!.Value.ToString();
956 var dmePath = ioManager.ConcatPath(stringDirectoryName, dmeFileName);
957 var dmeReadTask = ioManager.ReadAllBytes(dmePath, cancellationToken);
958
959 var dmeModificationsTask = configuration.CopyDMFilesTo(
960 dmeFileName,
961 ioManager.ResolvePath(
962 ioManager.ConcatPath(
963 stringDirectoryName,
964 ioManager.GetDirectoryName(dmeFileName))),
965 cancellationToken);
966
967 var dmeBytes = await dmeReadTask;
968 var dme = Encoding.UTF8.GetString(dmeBytes);
969
970 var dmeModifications = await dmeModificationsTask;
971
972 if (dmeModifications == null || dmeModifications.TotalDmeOverwrite)
973 {
974 if (dmeModifications != null)
975 logger.LogDebug(".dme replacement configured!");
976 else
977 logger.LogTrace("No .dme modifications required.");
978 return;
979 }
980
981 var dmeLines = new List<string>(dme.Split('\n', StringSplitOptions.None));
982 for (var dmeLineIndex = 0; dmeLineIndex < dmeLines.Count; ++dmeLineIndex)
983 {
984 var line = dmeLines[dmeLineIndex];
985 if (line.Contains("BEGIN_INCLUDE", StringComparison.Ordinal) && dmeModifications.HeadIncludeLine != null)
986 {
987 var headIncludeLineNumber = dmeLineIndex + 1;
988 logger.LogDebug(
989 "Inserting HeadInclude.dm at line {lineNumber}: {includeLine}",
990 headIncludeLineNumber,
991 dmeModifications.HeadIncludeLine);
992 dmeLines.Insert(headIncludeLineNumber, dmeModifications.HeadIncludeLine);
993 ++dmeLineIndex;
994 }
995 else if (line.Contains("END_INCLUDE", StringComparison.Ordinal) && dmeModifications.TailIncludeLine != null)
996 {
997 logger.LogDebug(
998 "Inserting TailInclude.dm at line {lineNumber}: {includeLine}",
999 dmeLineIndex,
1000 dmeModifications.TailIncludeLine);
1001 dmeLines.Insert(dmeLineIndex, dmeModifications.TailIncludeLine);
1002 break;
1003 }
1004 }
1005
1006 dmeBytes = Encoding.UTF8.GetBytes(String.Join('\n', dmeLines));
1007 await ioManager.WriteAllBytes(dmePath, dmeBytes, cancellationToken);
1008 }
1009
1017 ValueTask CleanupFailedCompile(Models.CompileJob job, IRemoteDeploymentManager remoteDeploymentManager, Exception exception)
1018 {
1019 async ValueTask CleanDir()
1020 {
1021 if (sessionConfiguration.DelayCleaningFailedDeployments)
1022 {
1023 logger.LogDebug("Not cleaning up errored deployment directory {guid} due to config.", job.DirectoryName);
1024 return;
1025 }
1026
1027 logger.LogTrace("Cleaning compile directory...");
1028 var jobPath = job.DirectoryName!.Value.ToString();
1029 try
1030 {
1031 // DCT: None available
1032 await eventConsumer.HandleEvent(EventType.DeploymentCleanup, new List<string> { jobPath }, true, CancellationToken.None);
1033 await ioManager.DeleteDirectory(jobPath, CancellationToken.None);
1034 }
1035 catch (Exception e)
1036 {
1037 logger.LogWarning(e, "Error cleaning up compile directory {path}!", ioManager.ResolvePath(jobPath));
1038 }
1039 }
1040
1041 var dirCleanTask = CleanDir();
1042
1043 var failRemoteDeployTask = remoteDeploymentManager.FailDeployment(
1044 job,
1045 FormatExceptionForUsers(exception),
1046 CancellationToken.None); // DCT: None available
1047
1049 dirCleanTask,
1050 failRemoteDeployTask);
1051 }
1052 }
1053}
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: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.
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, 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: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.