tgstation-server 6.12.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
27
29{
31 sealed class DreamMaker : IDreamMaker
32 {
36 const string DmeExtension = "dme";
37
42
47
52
57
62
67
72
77
82
87
92
96 readonly ILogger<DreamMaker> logger;
97
102
107
111 readonly object deploymentLock;
112
116 Func<string?, string, Action<bool>>? currentChatCallback;
117
122
127
133 static string FormatExceptionForUsers(Exception exception)
134 => exception is OperationCanceledException
135 ? "The job was cancelled!"
136 : exception.Message;
137
158 StaticFiles.IConfiguration configuration,
167 ILogger<DreamMaker> logger,
169 Api.Models.Instance metadata)
170 {
171 this.engineManager = engineManager ?? throw new ArgumentNullException(nameof(engineManager));
172 this.ioManager = ioManager ?? throw new ArgumentNullException(nameof(ioManager));
173 this.configuration = configuration ?? throw new ArgumentNullException(nameof(configuration));
174 this.sessionControllerFactory = sessionControllerFactory ?? throw new ArgumentNullException(nameof(sessionControllerFactory));
175 this.eventConsumer = eventConsumer ?? throw new ArgumentNullException(nameof(eventConsumer));
176 this.chatManager = chatManager ?? throw new ArgumentNullException(nameof(chatManager));
177 this.processExecutor = processExecutor ?? throw new ArgumentNullException(nameof(processExecutor));
178 this.compileJobConsumer = compileJobConsumer ?? throw new ArgumentNullException(nameof(compileJobConsumer));
179 this.repositoryManager = repositoryManager ?? throw new ArgumentNullException(nameof(repositoryManager));
180 this.asyncDelayer = asyncDelayer ?? throw new ArgumentNullException(nameof(asyncDelayer));
181 this.remoteDeploymentManagerFactory = remoteDeploymentManagerFactory ?? throw new ArgumentNullException(nameof(remoteDeploymentManagerFactory));
182 this.logger = logger ?? throw new ArgumentNullException(nameof(logger));
183 this.sessionConfiguration = sessionConfiguration ?? throw new ArgumentNullException(nameof(sessionConfiguration));
184 this.metadata = metadata ?? throw new ArgumentNullException(nameof(metadata));
185
186 deploymentLock = new object();
187 }
188
190#pragma warning disable CA1506
191 public async ValueTask DeploymentProcess(
192 Models.Job job,
193 IDatabaseContextFactory databaseContextFactory,
194 JobProgressReporter progressReporter,
195 CancellationToken cancellationToken)
196 {
197 ArgumentNullException.ThrowIfNull(job);
198 ArgumentNullException.ThrowIfNull(databaseContextFactory);
199 ArgumentNullException.ThrowIfNull(progressReporter);
200
201 lock (deploymentLock)
202 {
203 if (deploying)
204 throw new JobException(ErrorCode.DeploymentInProgress);
205 deploying = true;
206 }
207
208 currentChatCallback = null;
210 Models.CompileJob? compileJob = null;
211 try
212 {
213 string? repoOwner = null;
214 string? repoName = null;
215 TimeSpan? averageSpan = null;
216 Models.RepositorySettings? repositorySettings = null;
217 Models.DreamDaemonSettings? ddSettings = null;
218 Models.DreamMakerSettings? dreamMakerSettings = null;
219 IRepository? repo = null;
220 IRemoteDeploymentManager? remoteDeploymentManager = null;
221 Models.RevisionInformation? revInfo = null;
222 await databaseContextFactory.UseContext(
223 async databaseContext =>
224 {
225 averageSpan = await CalculateExpectedDeploymentTime(databaseContext, cancellationToken);
226
227 ddSettings = await databaseContext
228 .DreamDaemonSettings
229 .AsQueryable()
230 .Where(x => x.InstanceId == metadata.Id)
231 .Select(x => new Models.DreamDaemonSettings
232 {
233 StartupTimeout = x.StartupTimeout,
234 LogOutput = x.LogOutput,
235 })
236 .FirstOrDefaultAsync(cancellationToken);
237 if (ddSettings == default)
238 throw new JobException(ErrorCode.InstanceMissingDreamDaemonSettings);
239
240 dreamMakerSettings = await databaseContext
241 .DreamMakerSettings
242 .AsQueryable()
243 .Where(x => x.InstanceId == metadata.Id)
244 .FirstAsync(cancellationToken);
245 if (dreamMakerSettings == default)
246 throw new JobException(ErrorCode.InstanceMissingDreamMakerSettings);
247
248 repositorySettings = await databaseContext
249 .RepositorySettings
250 .AsQueryable()
251 .Where(x => x.InstanceId == metadata.Id)
252 .Select(x => new Models.RepositorySettings
253 {
254 AccessToken = x.AccessToken,
255 AccessUser = x.AccessUser,
256 ShowTestMergeCommitters = x.ShowTestMergeCommitters,
257 PushTestMergeCommits = x.PushTestMergeCommits,
258 PostTestMergeComment = x.PostTestMergeComment,
259 })
260 .FirstOrDefaultAsync(cancellationToken);
261 if (repositorySettings == default)
262 throw new JobException(ErrorCode.InstanceMissingRepositorySettings);
263
264 repo = await repositoryManager.LoadRepository(cancellationToken);
265 try
266 {
267 if (repo == null)
268 throw new JobException(ErrorCode.RepoMissing);
269
270 remoteDeploymentManager = remoteDeploymentManagerFactory
272
273 var repoSha = repo.Head;
274 repoOwner = repo.RemoteRepositoryOwner;
275 repoName = repo.RemoteRepositoryName;
276 revInfo = await databaseContext
277 .RevisionInformations
278 .AsQueryable()
279 .Where(x => x.CommitSha == repoSha && x.InstanceId == metadata.Id)
280 .Include(x => x.ActiveTestMerges!)
281 .ThenInclude(x => x.TestMerge!)
282 .ThenInclude(x => x.MergedBy)
283 .FirstOrDefaultAsync(cancellationToken);
284
285 if (revInfo == null)
286 {
287 revInfo = new Models.RevisionInformation
288 {
289 CommitSha = repoSha,
290 Timestamp = await repo.TimestampCommit(repoSha, cancellationToken),
291 OriginCommitSha = repoSha,
293 {
294 Id = metadata.Id,
295 },
296 ActiveTestMerges = new List<RevInfoTestMerge>(),
297 };
298
299 logger.LogInformation(Repository.Repository.OriginTrackingErrorTemplate, repoSha);
300 databaseContext.RevisionInformations.Add(revInfo);
301 databaseContext.Instances.Attach(revInfo.Instance);
302 await databaseContext.Save(cancellationToken);
303 }
304 }
305 catch
306 {
307 repo?.Dispose();
308 throw;
309 }
310 });
311
312 var likelyPushedTestMergeCommit =
313 repositorySettings!.PushTestMergeCommits!.Value
314 && repositorySettings.AccessToken != null
315 && repositorySettings.AccessUser != null;
316 using (repo)
317 compileJob = await Compile(
318 job,
319 revInfo!,
320 dreamMakerSettings!,
321 ddSettings!,
322 repo!,
323 remoteDeploymentManager!,
324 progressReporter,
325 averageSpan,
326 likelyPushedTestMergeCommit,
327 cancellationToken);
328
329 var activeCompileJob = await compileJobConsumer.LatestCompileJob();
330 try
331 {
332 await databaseContextFactory.UseContext(
333 async databaseContext =>
334 {
335 var fullJob = compileJob.Job;
336 compileJob.Job = new Models.Job(job.Require(x => x.Id));
337 var fullRevInfo = compileJob.RevisionInformation;
338 compileJob.RevisionInformation = new Models.RevisionInformation
339 {
340 Id = revInfo!.Id,
341 };
342
343 databaseContext.Jobs.Attach(compileJob.Job);
344 databaseContext.RevisionInformations.Attach(compileJob.RevisionInformation);
345 databaseContext.CompileJobs.Add(compileJob);
346
347 // The difficulty with compile jobs is they have a two part commit
348 await databaseContext.Save(cancellationToken);
349 logger.LogTrace("Created CompileJob {compileJobId}", compileJob.Id);
350 try
351 {
352 var chatNotificationAction = currentChatCallback!(null, compileJob.Output!);
353 await compileJobConsumer.LoadCompileJob(compileJob, chatNotificationAction, cancellationToken);
354 }
355 catch
356 {
357 // So we need to un-commit the compile job if the above throws
358 databaseContext.CompileJobs.Remove(compileJob);
359
360 // DCT: Cancellation token is for job, operation must run regardless
361 await databaseContext.Save(CancellationToken.None);
362 throw;
363 }
364
365 compileJob.Job = fullJob;
366 compileJob.RevisionInformation = fullRevInfo;
367 });
368 }
369 catch (Exception ex)
370 {
371 await CleanupFailedCompile(compileJob, remoteDeploymentManager!, ex);
372 throw;
373 }
374
375 var commentsTask = remoteDeploymentManager!.PostDeploymentComments(
376 compileJob,
377 activeCompileJob?.RevisionInformation,
378 repositorySettings,
379 repoOwner,
380 repoName,
381 cancellationToken);
382
383 var eventTask = eventConsumer.HandleEvent(EventType.DeploymentComplete, Enumerable.Empty<string>(), false, cancellationToken);
384
385 try
386 {
387 await ValueTaskExtensions.WhenAll(commentsTask, eventTask);
388 }
389 catch (Exception ex)
390 {
391 throw new JobException(ErrorCode.PostDeployFailure, ex);
392 }
393 finally
394 {
395 currentChatCallback = null;
396 }
397 }
398 catch (Exception ex)
399 {
400 currentChatCallback?.Invoke(
403
404 throw;
405 }
406 finally
407 {
408 deploying = false;
409 }
410 }
411#pragma warning restore CA1506
412
419 async ValueTask<TimeSpan?> CalculateExpectedDeploymentTime(IDatabaseContext databaseContext, CancellationToken cancellationToken)
420 {
421 var previousCompileJobs = await databaseContext
423 .AsQueryable()
424 .Where(x => x.Job.Instance!.Id == metadata.Id)
425 .OrderByDescending(x => x.Job.StoppedAt)
426 .Take(10)
427 .Select(x => new
428 {
429 StoppedAt = x.Job.StoppedAt!.Value,
430 StartedAt = x.Job.StartedAt!.Value,
431 })
432 .ToListAsync(cancellationToken);
433
434 TimeSpan? averageSpan = null;
435 if (previousCompileJobs.Count != 0)
436 {
437 var totalSpan = TimeSpan.Zero;
438 foreach (var previousCompileJob in previousCompileJobs)
439 totalSpan += previousCompileJob.StoppedAt - previousCompileJob.StartedAt;
440 averageSpan = totalSpan / previousCompileJobs.Count;
441 }
442
443 return averageSpan;
444 }
445
460 async ValueTask<Models.CompileJob> Compile(
461 Models.Job job,
462 Models.RevisionInformation revisionInformation,
463 Api.Models.Internal.DreamMakerSettings dreamMakerSettings,
464 DreamDaemonLaunchParameters launchParameters,
465 IRepository repository,
466 IRemoteDeploymentManager remoteDeploymentManager,
467 JobProgressReporter progressReporter,
468 TimeSpan? estimatedDuration,
469 bool localCommitExistsOnRemote,
470 CancellationToken cancellationToken)
471 {
472 logger.LogTrace("Begin Compile");
473
474 using var progressCts = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken);
475
476 progressReporter.StageName = "Reserving BYOND version";
477 var progressTask = ProgressTask(progressReporter, estimatedDuration, progressCts.Token);
478 try
479 {
480 using var engineLock = await engineManager.UseExecutables(null, null, cancellationToken);
482 revisionInformation,
483 engineLock.Version,
484 DateTimeOffset.UtcNow + estimatedDuration,
485 repository.RemoteRepositoryOwner,
486 repository.RemoteRepositoryName,
487 localCommitExistsOnRemote);
488
489 var compileJob = new Models.CompileJob(job, revisionInformation, engineLock.Version.ToString())
490 {
491 DirectoryName = Guid.NewGuid(),
492 DmeName = dreamMakerSettings.ProjectName,
493 RepositoryOrigin = repository.Origin.ToString(),
494 };
495
496 progressReporter.StageName = "Creating remote deployment notification";
497 await remoteDeploymentManager.StartDeployment(
498 repository,
499 compileJob,
500 cancellationToken);
501
502 logger.LogTrace("Deployment will timeout at {timeoutTime}", DateTimeOffset.UtcNow + dreamMakerSettings.Timeout!.Value);
503 using var timeoutTokenSource = new CancellationTokenSource(dreamMakerSettings.Timeout.Value);
504 var timeoutToken = timeoutTokenSource.Token;
505 using (timeoutToken.Register(() => logger.LogWarning("Deployment timed out!")))
506 {
507 using var combinedTokenSource = CancellationTokenSource.CreateLinkedTokenSource(timeoutToken, cancellationToken);
508 try
509 {
510 await RunCompileJob(
511 progressReporter,
512 compileJob,
513 dreamMakerSettings,
514 launchParameters,
515 engineLock,
516 repository,
517 remoteDeploymentManager,
518 combinedTokenSource.Token);
519 }
520 catch (OperationCanceledException) when (timeoutToken.IsCancellationRequested)
521 {
522 throw new JobException(ErrorCode.DeploymentTimeout);
523 }
524 }
525
526 return compileJob;
527 }
528 catch (OperationCanceledException)
529 {
530 // DCT: Cancellation token is for job, delaying here is fine
531 progressReporter.StageName = "Running CompileCancelled event";
532 await eventConsumer.HandleEvent(EventType.CompileCancelled, Enumerable.Empty<string>(), true, CancellationToken.None);
533 throw;
534 }
535 finally
536 {
537 progressCts.Cancel();
538 await progressTask;
539 }
540 }
541
554 async ValueTask RunCompileJob(
555 JobProgressReporter progressReporter,
556 Models.CompileJob job,
557 Api.Models.Internal.DreamMakerSettings dreamMakerSettings,
558 DreamDaemonLaunchParameters launchParameters,
559 IEngineExecutableLock engineLock,
560 IRepository repository,
561 IRemoteDeploymentManager remoteDeploymentManager,
562 CancellationToken cancellationToken)
563 {
564 var outputDirectory = job.DirectoryName!.Value.ToString();
565 logger.LogTrace("Compile output GUID: {dirGuid}", outputDirectory);
566
567 try
568 {
569 // copy the repository
570 logger.LogTrace("Copying repository to game directory");
571 progressReporter.StageName = "Copying repository";
572 var resolvedOutputDirectory = ioManager.ResolvePath(outputDirectory);
573 var repoOrigin = repository.Origin;
574 var repoReference = repository.Reference;
575 using (repository)
576 await repository.CopyTo(resolvedOutputDirectory, cancellationToken);
577
578 // repository closed now
579
580 // run precompile scripts
581 progressReporter.StageName = "Running PreCompile event";
583 EventType.CompileStart,
584 new List<string>
585 {
586 resolvedOutputDirectory,
587 repoOrigin.ToString(),
588 engineLock.Version.ToString(),
589 repoReference,
590 },
591 true,
592 cancellationToken);
593
594 // determine the dme
595 progressReporter.StageName = "Determining .dme";
596 if (job.DmeName == null)
597 {
598 logger.LogTrace("Searching for available .dmes");
599 var foundPaths = await ioManager.GetFilesWithExtension(resolvedOutputDirectory, DmeExtension, true, cancellationToken);
600 var foundPath = foundPaths.FirstOrDefault();
601 if (foundPath == default)
602 throw new JobException(ErrorCode.DeploymentNoDme);
603 job.DmeName = foundPath.Substring(
604 resolvedOutputDirectory.Length + 1,
605 foundPath.Length - resolvedOutputDirectory.Length - DmeExtension.Length - 2); // +1 for . in extension
606 }
607 else
608 {
609 var targetDme = ioManager.ConcatPath(outputDirectory, String.Join('.', job.DmeName, DmeExtension));
610 if (!await ioManager.PathIsChildOf(outputDirectory, targetDme, cancellationToken))
611 throw new JobException(ErrorCode.DeploymentWrongDme);
612
613 var targetDmeExists = await ioManager.FileExists(targetDme, cancellationToken);
614 if (!targetDmeExists)
615 throw new JobException(ErrorCode.DeploymentMissingDme);
616 }
617
618 logger.LogDebug("Selected \"{dmeName}.dme\" for compilation!", job.DmeName);
619
620 progressReporter.StageName = "Modifying .dme";
621 await ModifyDme(job, cancellationToken);
622
623 // run precompile scripts
624 progressReporter.StageName = "Running PreDreamMaker event";
626 EventType.PreDreamMaker,
627 new List<string>
628 {
629 resolvedOutputDirectory,
630 repoOrigin.ToString(),
631 engineLock.Version.ToString(),
632 },
633 true,
634 cancellationToken);
635
636 // run compiler
637 progressReporter.StageName = "Running Compiler";
638 var compileSuceeded = await RunDreamMaker(engineLock, job, dreamMakerSettings.CompilerAdditionalArguments, cancellationToken);
639
640 // Session takes ownership of the lock and Disposes it so save this for later
641 var engineVersion = engineLock.Version;
642
643 // verify api
644 try
645 {
646 if (!compileSuceeded)
647 throw new JobException(
648 ErrorCode.DeploymentExitCode,
649 new JobException($"Compilation failed:{Environment.NewLine}{Environment.NewLine}{job.Output}"));
650
651 await VerifyApi(
652 launchParameters.StartupTimeout!.Value,
653 dreamMakerSettings.ApiValidationSecurityLevel!.Value,
654 job,
655 progressReporter,
656 engineLock,
657 dreamMakerSettings.ApiValidationPort!.Value,
658 dreamMakerSettings.DMApiValidationMode!.Value,
659 launchParameters.LogOutput!.Value,
660 cancellationToken);
661 }
662 catch (JobException)
663 {
664 // DD never validated or compile failed
665 progressReporter.StageName = "Running CompileFailure event";
667 EventType.CompileFailure,
668 new List<string>
669 {
670 resolvedOutputDirectory,
671 compileSuceeded ? "1" : "0",
672 engineVersion.ToString(),
673 },
674 true,
675 cancellationToken);
676 throw;
677 }
678
679 progressReporter.StageName = "Running CompileComplete event";
681 EventType.CompileComplete,
682 new List<string>
683 {
684 resolvedOutputDirectory,
685 engineVersion.ToString(),
686 },
687 true,
688 cancellationToken);
689
690 logger.LogTrace("Applying static game file symlinks...");
691 progressReporter.StageName = "Symlinking GameStaticFiles";
692
693 // symlink in the static data
694 await configuration.SymlinkStaticFilesTo(resolvedOutputDirectory, cancellationToken);
695
696 logger.LogDebug("Compile complete!");
697 }
698 catch (Exception ex)
699 {
700 progressReporter.StageName = "Cleaning output directory";
701 await CleanupFailedCompile(job, remoteDeploymentManager, ex);
702 throw;
703 }
704 }
705
713 async ValueTask ProgressTask(JobProgressReporter progressReporter, TimeSpan? estimatedDuration, CancellationToken cancellationToken)
714 {
715 double? lastReport = estimatedDuration.HasValue ? 0 : null;
716 progressReporter.ReportProgress(lastReport);
717
718 var minimumSleepInterval = TimeSpan.FromMilliseconds(250);
719 var sleepInterval = estimatedDuration.HasValue ? estimatedDuration.Value / 100 : minimumSleepInterval;
720
721 if (estimatedDuration.HasValue)
722 {
723 logger.LogDebug("Compile is expected to take: {estimatedDuration}", estimatedDuration);
724 }
725 else
726 {
727 logger.LogTrace("No metric to estimate compile time.");
728 }
729
730 try
731 {
732 for (var iteration = 0; iteration < (estimatedDuration.HasValue ? 99 : Int32.MaxValue); ++iteration)
733 {
734 if (estimatedDuration.HasValue)
735 {
736 var nextInterval = DateTimeOffset.UtcNow + sleepInterval;
737 do
738 {
739 var remainingSleepThisInterval = nextInterval - DateTimeOffset.UtcNow;
740 var nextSleepSpan = remainingSleepThisInterval < minimumSleepInterval ? minimumSleepInterval : remainingSleepThisInterval;
741
742 await asyncDelayer.Delay(nextSleepSpan, cancellationToken);
743 progressReporter.ReportProgress(lastReport);
744 }
745 while (DateTimeOffset.UtcNow < nextInterval);
746 }
747 else
748 await asyncDelayer.Delay(minimumSleepInterval, cancellationToken);
749
750 lastReport = estimatedDuration.HasValue ? sleepInterval * (iteration + 1) / estimatedDuration.Value : null;
751 progressReporter.ReportProgress(lastReport);
752 }
753 }
754 catch (OperationCanceledException ex)
755 {
756 logger.LogTrace(ex, "ProgressTask aborted.");
757 }
758 catch (Exception ex)
759 {
760 logger.LogError(ex, "ProgressTask crashed!");
761 }
762 }
763
777 async ValueTask VerifyApi(
778 uint timeout,
779 DreamDaemonSecurity securityLevel,
780 Models.CompileJob job,
781 JobProgressReporter progressReporter,
782 IEngineExecutableLock engineLock,
783 ushort portToUse,
784 DMApiValidationMode validationMode,
785 bool logOutput,
786 CancellationToken cancellationToken)
787 {
788 if (validationMode == DMApiValidationMode.Skipped)
789 {
790 logger.LogDebug("Skipping DMAPI validation");
791 job.MinimumSecurityLevel = DreamDaemonSecurity.Ultrasafe;
792 return;
793 }
794
795 progressReporter.StageName = "Validating DMAPI";
796
797 var requireValidate = validationMode == DMApiValidationMode.Required;
798 logger.LogTrace("Verifying {possiblyRequired}DMAPI...", requireValidate ? "required " : String.Empty);
799 var launchParameters = new DreamDaemonLaunchParameters
800 {
801 AllowWebClient = false,
802 Port = portToUse,
803 OpenDreamTopicPort = 0,
804 SecurityLevel = securityLevel,
805 Visibility = DreamDaemonVisibility.Invisible,
806 StartupTimeout = timeout,
807 TopicRequestTimeout = 0, // not used
808 HealthCheckSeconds = 0, // not used
809 StartProfiler = false,
810 LogOutput = logOutput,
811 MapThreads = 1, // lowest possible amount
812 };
813
814 job.MinimumSecurityLevel = securityLevel; // needed for the TempDmbProvider
815
816 ApiValidationStatus validationStatus;
817 await using (var provider = new TemporaryDmbProvider(
818 ioManager.ResolvePath(job.DirectoryName!.Value.ToString()),
819 job,
820 engineLock.Version))
821 await using (var controller = await sessionControllerFactory.LaunchNew(provider, engineLock, launchParameters, true, cancellationToken))
822 {
823 var launchResult = await controller.LaunchResult.WaitAsync(cancellationToken);
824
825 if (launchResult.StartupTime.HasValue)
826 await controller.Lifetime.WaitAsync(cancellationToken);
827
828 if (!controller.Lifetime.IsCompleted)
829 await controller.DisposeAsync();
830
831 validationStatus = controller.ApiValidationStatus;
832
833 logger.LogTrace("API validation status: {validationStatus}", validationStatus);
834
835 job.DMApiVersion = controller.DMApiVersion;
836 }
837
838 switch (validationStatus)
839 {
840 case ApiValidationStatus.RequiresUltrasafe:
841 job.MinimumSecurityLevel = DreamDaemonSecurity.Ultrasafe;
842 return;
843 case ApiValidationStatus.RequiresSafe:
844 job.MinimumSecurityLevel = DreamDaemonSecurity.Safe;
845 return;
846 case ApiValidationStatus.RequiresTrusted:
847 job.MinimumSecurityLevel = DreamDaemonSecurity.Trusted;
848 return;
849 case ApiValidationStatus.NeverValidated:
850 if (requireValidate)
851 throw new JobException(ErrorCode.DeploymentNeverValidated);
852 job.MinimumSecurityLevel = DreamDaemonSecurity.Ultrasafe;
853 break;
854 case ApiValidationStatus.BadValidationRequest:
855 case ApiValidationStatus.Incompatible:
856 throw new JobException(ErrorCode.DeploymentInvalidValidation);
857 case ApiValidationStatus.UnaskedValidationRequest:
858 default:
859 throw new InvalidOperationException(
860 $"Session controller returned unexpected ApiValidationStatus: {validationStatus}");
861 }
862 }
863
872 async ValueTask<bool> RunDreamMaker(
873 IEngineExecutableLock engineLock,
874 Models.CompileJob job,
875 string? additionalCompilerArguments,
876 CancellationToken cancellationToken)
877 {
878 var environment = await engineLock.LoadEnv(logger, true, cancellationToken);
879 var arguments = engineLock.FormatCompilerArguments($"{job.DmeName}.{DmeExtension}", additionalCompilerArguments);
880
881 await using var dm = await processExecutor.LaunchProcess(
882 engineLock.CompilerExePath,
883 ioManager.ResolvePath(
884 job.DirectoryName!.Value.ToString()),
885 arguments,
886 cancellationToken,
887 environment,
888 readStandardHandles: true,
889 noShellExecute: true);
890
891 if (sessionConfiguration.LowPriorityDeploymentProcesses)
892 dm.AdjustPriority(false);
893
894 int exitCode;
895 using (cancellationToken.Register(() => dm.Terminate()))
896 exitCode = (await dm.Lifetime).Value;
897 cancellationToken.ThrowIfCancellationRequested();
898
899 logger.LogDebug("DreamMaker exit code: {exitCode}", exitCode);
900 job.Output = $"{await dm.GetCombinedOutput(cancellationToken)}{Environment.NewLine}{Environment.NewLine}Exit Code: {exitCode}";
901 logger.LogDebug("DreamMaker output: {newLine}{output}", Environment.NewLine, job.Output);
902
903 currentDreamMakerOutput = job.Output;
904 return exitCode == 0;
905 }
906
913 async ValueTask ModifyDme(Models.CompileJob job, CancellationToken cancellationToken)
914 {
915 var dmeFileName = String.Join('.', job.DmeName, DmeExtension);
916 var stringDirectoryName = job.DirectoryName!.Value.ToString();
917 var dmePath = ioManager.ConcatPath(stringDirectoryName, dmeFileName);
918 var dmeReadTask = ioManager.ReadAllBytes(dmePath, cancellationToken);
919
920 var dmeModificationsTask = configuration.CopyDMFilesTo(
921 dmeFileName,
922 ioManager.ResolvePath(
923 ioManager.ConcatPath(
924 stringDirectoryName,
925 ioManager.GetDirectoryName(dmeFileName))),
926 cancellationToken);
927
928 var dmeBytes = await dmeReadTask;
929 var dme = Encoding.UTF8.GetString(dmeBytes);
930
931 var dmeModifications = await dmeModificationsTask;
932
933 if (dmeModifications == null || dmeModifications.TotalDmeOverwrite)
934 {
935 if (dmeModifications != null)
936 logger.LogDebug(".dme replacement configured!");
937 else
938 logger.LogTrace("No .dme modifications required.");
939 return;
940 }
941
942 var dmeLines = new List<string>(dme.Split('\n', StringSplitOptions.None));
943 for (var dmeLineIndex = 0; dmeLineIndex < dmeLines.Count; ++dmeLineIndex)
944 {
945 var line = dmeLines[dmeLineIndex];
946 if (line.Contains("BEGIN_INCLUDE", StringComparison.Ordinal) && dmeModifications.HeadIncludeLine != null)
947 {
948 var headIncludeLineNumber = dmeLineIndex + 1;
949 logger.LogDebug(
950 "Inserting HeadInclude.dm at line {lineNumber}: {includeLine}",
951 headIncludeLineNumber,
952 dmeModifications.HeadIncludeLine);
953 dmeLines.Insert(headIncludeLineNumber, dmeModifications.HeadIncludeLine);
954 ++dmeLineIndex;
955 }
956 else if (line.Contains("END_INCLUDE", StringComparison.Ordinal) && dmeModifications.TailIncludeLine != null)
957 {
958 logger.LogDebug(
959 "Inserting TailInclude.dm at line {lineNumber}: {includeLine}",
960 dmeLineIndex,
961 dmeModifications.TailIncludeLine);
962 dmeLines.Insert(dmeLineIndex, dmeModifications.TailIncludeLine);
963 break;
964 }
965 }
966
967 dmeBytes = Encoding.UTF8.GetBytes(String.Join('\n', dmeLines));
968 await ioManager.WriteAllBytes(dmePath, dmeBytes, cancellationToken);
969 }
970
978 ValueTask CleanupFailedCompile(Models.CompileJob job, IRemoteDeploymentManager remoteDeploymentManager, Exception exception)
979 {
980 async ValueTask CleanDir()
981 {
982 if (sessionConfiguration.DelayCleaningFailedDeployments)
983 {
984 logger.LogDebug("Not cleaning up errored deployment directory {guid} due to config.", job.DirectoryName);
985 return;
986 }
987
988 logger.LogTrace("Cleaning compile directory...");
989 var jobPath = job.DirectoryName!.Value.ToString();
990 try
991 {
992 // DCT: None available
993 await eventConsumer.HandleEvent(EventType.DeploymentCleanup, new List<string> { jobPath }, true, CancellationToken.None);
994 await ioManager.DeleteDirectory(jobPath, CancellationToken.None);
995 }
996 catch (Exception e)
997 {
998 logger.LogWarning(e, "Error cleaning up compile directory {path}!", ioManager.ResolvePath(jobPath));
999 }
1000 }
1001
1002 var dirCleanTask = CleanDir();
1003
1004 var failRemoteDeployTask = remoteDeploymentManager.FailDeployment(
1005 job,
1006 FormatExceptionForUsers(exception),
1007 CancellationToken.None); // DCT: None available
1008
1010 dirCleanTask,
1011 failRemoteDeployTask);
1012 }
1013 }
1014}
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:76
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.
Definition DreamMaker.cs:96
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:51
const string DmeExtension
Extension for .dmes.
Definition DreamMaker.cs:36
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:46
readonly IRemoteDeploymentManagerFactory remoteDeploymentManagerFactory
The IRemoteDeploymentManagerFactory for DreamMaker.
Definition DreamMaker.cs:86
readonly ISessionControllerFactory sessionControllerFactory
The ISessionControllerFactory for DreamMaker.
Definition DreamMaker.cs:56
static string FormatExceptionForUsers(Exception exception)
Format a given Exception for display to users.
readonly IProcessExecutor processExecutor
The IProcessExecutor for DreamMaker.
Definition DreamMaker.cs:71
readonly IEngineManager engineManager
The IEngineManager for DreamMaker.
Definition DreamMaker.cs:41
readonly ICompileJobSink compileJobConsumer
The ICompileJobSink for DreamMaker.
Definition DreamMaker.cs:81
readonly IEventConsumer eventConsumer
The IEventConsumer for DreamMaker.
Definition DreamMaker.cs:61
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:66
DreamMaker(IEngineManager engineManager, IIOManager ioManager, StaticFiles.IConfiguration configuration, ISessionControllerFactory sessionControllerFactory, IEventConsumer eventConsumer, IChatManager chatManager, IProcessExecutor processExecutor, ICompileJobSink compileJobConsumer, IRepositoryManager repositoryManager, IRemoteDeploymentManagerFactory remoteDeploymentManagerFactory, IAsyncDelayer asyncDelayer, ILogger< DreamMaker > logger, SessionConfiguration sessionConfiguration, Api.Models.Instance metadata)
Initializes a new instance of the DreamMaker class.
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:91
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.