using Microsoft.Extensions.Logging; using System; using System.Collections.Generic; using System.Diagnostics; using System.Globalization; using System.Linq; using System.Text; using System.Threading; using System.Threading.Tasks; using Tgstation.Server.Api.Models; using Tgstation.Server.Api.Models.Internal; using Tgstation.Server.Host.Components.Byond; using Tgstation.Server.Host.Components.Repository; using Tgstation.Server.Host.Components.Watchdog; using Tgstation.Server.Host.Core; using Tgstation.Server.Host.IO; namespace Tgstation.Server.Host.Components.Compiler { /// sealed class DreamMaker : IDreamMaker { /// /// Name of the primary directory used for compilation /// public const string ADirectoryName = "A"; /// /// Name of the secondary directory used for compilation /// public const string BDirectoryName = "B"; /// /// Extension for .dmbs /// public const string DmbExtension = ".dmb"; /// /// Extension for .dmes /// const string DmeExtension = ".dme"; /// public CompilerStatus Status { get; private set; } /// /// The for /// readonly IByondManager byond; /// /// The for /// readonly IIOManager ioManager; /// /// The for /// readonly StaticFiles.IConfiguration configuration; /// /// The for /// readonly ISessionControllerFactory sessionControllerFactory; /// /// The for /// readonly ICompileJobConsumer compileJobConsumer; /// /// The for /// readonly IApplication application; /// /// The for /// readonly IEventConsumer eventConsumer; /// /// The for /// readonly ILogger logger; /// /// Construct /// /// The value of /// The value of /// The value of /// The value of /// The value of /// The value of /// The value of /// The value of public DreamMaker(IByondManager byond, IIOManager ioManager, StaticFiles.IConfiguration configuration, ISessionControllerFactory sessionControllerFactory, ICompileJobConsumer compileJobConsumer, IApplication application, IEventConsumer eventConsumer, ILogger logger) { this.byond = byond; this.ioManager = ioManager ?? throw new ArgumentNullException(nameof(ioManager)); this.configuration = configuration ?? throw new ArgumentNullException(nameof(configuration)); this.sessionControllerFactory = sessionControllerFactory ?? throw new ArgumentNullException(nameof(sessionControllerFactory)); this.compileJobConsumer = compileJobConsumer ?? throw new ArgumentNullException(nameof(compileJobConsumer)); this.application = application ?? throw new ArgumentNullException(nameof(application)); this.eventConsumer = eventConsumer ?? throw new ArgumentNullException(nameof(eventConsumer)); this.logger = logger ?? throw new ArgumentNullException(nameof(logger)); } /// /// Run a quick DD instance to test the DMAPI is installed on the target code /// /// The timeout in seconds for validation /// The for the operation /// The current /// The for the operation /// A resulting in if the DMAPI was successfully validated, otherwise async Task VerifyApi(uint timeout, Models.CompileJob job, IByondExecutableLock byondLock, CancellationToken cancellationToken) { var launchParameters = new DreamDaemonLaunchParameters { AllowWebClient = false, PrimaryPort = 0, //pick any port SecurityLevel = DreamDaemonSecurity.Safe, //all it needs to read the file and exit StartupTimeout = timeout }; var dirA = ioManager.ConcatPath(job.DirectoryName.ToString(), ADirectoryName); var provider = new TemporaryDmbProvider(ioManager.ResolvePath(ioManager.GetDirectoryName(dirA)), ioManager.ResolvePath(ioManager.ConcatPath(dirA, String.Concat(job.DmeName, DmbExtension)))); var timeoutAt = DateTimeOffset.Now.AddSeconds(timeout); using (var controller = await sessionControllerFactory.LaunchNew(launchParameters, provider, byondLock, true, true, true, cancellationToken).ConfigureAwait(false)) { var timeoutTask = Task.Delay(timeoutAt - DateTimeOffset.Now, cancellationToken); await Task.WhenAny(controller.Lifetime, timeoutTask).ConfigureAwait(false); cancellationToken.ThrowIfCancellationRequested(); if (!controller.Lifetime.IsCompleted) return false; return controller.ApiValidated; } } /// /// Compiles a .dme with DreamMaker /// /// The path to the DreamMaker executable /// The for the operation /// The for the operation /// A representing the running operation async Task RunDreamMaker(string dreamMakerPath, Models.CompileJob job, CancellationToken cancellationToken) { using (var dm = new Process()) { dm.StartInfo.FileName = dreamMakerPath; dm.StartInfo.Arguments = String.Format(CultureInfo.InvariantCulture, "-clean {0}{1}", job.DmeName, DmeExtension); dm.StartInfo.WorkingDirectory = ioManager.ResolvePath(ioManager.ConcatPath(job.DirectoryName.ToString(), ADirectoryName)); dm.StartInfo.RedirectStandardOutput = true; dm.StartInfo.RedirectStandardError = true; var OutputList = new StringBuilder(); var eventHandler = new DataReceivedEventHandler( delegate (object sender, DataReceivedEventArgs e) { OutputList.Append(Environment.NewLine); OutputList.Append(e.Data); } ); dm.OutputDataReceived += eventHandler; dm.ErrorDataReceived += eventHandler; dm.EnableRaisingEvents = true; var dmTcs = new TaskCompletionSource(); dm.Exited += (a, b) => dmTcs.SetResult(null); dm.Start(); try { using (cancellationToken.Register(() => dmTcs.SetCanceled())) await dmTcs.Task.ConfigureAwait(false); } finally { if (!dm.HasExited) { dm.Kill(); dm.WaitForExit(); } } job.Output = OutputList.ToString(); job.ExitCode = dm.ExitCode; } } /// /// Adds server side includes to the .dme being compiled /// /// The for the operation /// The for the operation /// A representing the running operation async Task ModifyDme(Models.CompileJob job, CancellationToken cancellationToken) { var dirA = ioManager.ConcatPath(job.DirectoryName.ToString(), ADirectoryName); var dmePath = ioManager.ConcatPath(dirA, String.Concat(job.DmeName, DmeExtension)); var dmeReadTask = ioManager.ReadAllBytes(dmePath, cancellationToken); var dmeModificationsTask = configuration.CopyDMFilesTo(dmePath, ioManager.ResolvePath(dirA), cancellationToken); var dmeBytes = await dmeReadTask.ConfigureAwait(false); var dme = Encoding.UTF8.GetString(dmeBytes); var dmeModifications = await dmeModificationsTask.ConfigureAwait(false); if (dmeModifications == null || dmeModifications.TotalDmeOverwrite) return; var dmeLines = new List(dme.Split(new[] { Environment.NewLine }, StringSplitOptions.None)); for (var I = 0; I < dmeLines.Count; ++I) { var line = dmeLines[I]; if (line.Contains("BEGIN_INCLUDE") && dmeModifications.HeadIncludeLine != null) { dmeLines.Insert(I + 1, dmeModifications.HeadIncludeLine); ++I; } else if (line.Contains("END_INCLUDE") && dmeModifications.TailIncludeLine != null) { dmeLines.Insert(I, dmeModifications.TailIncludeLine); break; } } dmeBytes = Encoding.UTF8.GetBytes(String.Join(Environment.NewLine, dmeLines)); await ioManager.WriteAllBytes(dmePath, dmeBytes, cancellationToken).ConfigureAwait(false); } /// public async Task Compile(string projectName, uint apiValidateTimeout, IRepository repository, CancellationToken cancellationToken) { logger.LogTrace("Begin Compile"); await eventConsumer.HandleEvent(EventType.CompileStart, new List{ repository.Origin }, cancellationToken).ConfigureAwait(false); try { Status = CompilerStatus.Copying; var job = new Models.CompileJob { DirectoryName = Guid.NewGuid(), DmeName = projectName }; await ioManager.CreateDirectory(job.DirectoryName.ToString(), cancellationToken).ConfigureAwait(false); var dirA = ioManager.ConcatPath(job.DirectoryName.ToString(), ADirectoryName); var dirB = ioManager.ConcatPath(job.DirectoryName.ToString(), BDirectoryName); async Task CleanupFailedCompile() { Status = CompilerStatus.Cleanup; try { await ioManager.DeleteDirectory(job.DirectoryName.ToString(), CancellationToken.None).ConfigureAwait(false); } catch { } }; try { //copy the repository var fullDirA = ioManager.ResolvePath(dirA); using (repository) await repository.CopyTo(fullDirA, cancellationToken).ConfigureAwait(false); Status = CompilerStatus.Modifying; if (job.DmeName == null) { job.DmeName = (await ioManager.GetFilesWithExtension(dirA, DmeExtension, cancellationToken).ConfigureAwait(false)).FirstOrDefault(); if (job.DmeName == default) { job.Output = "Unable to find any .dme!"; return job; } } await ModifyDme(job, cancellationToken).ConfigureAwait(false); Status = CompilerStatus.Compiling; //run compiler, verify api bool ddVerified; using (var byondLock = await byond.UseExecutables(null, cancellationToken).ConfigureAwait(false)) { job.ByondVersion = byondLock.Version.ToString(); await RunDreamMaker(byondLock.DreamMakerPath, job, cancellationToken).ConfigureAwait(false); Status = CompilerStatus.Verifying; ddVerified = job.ExitCode == 0 && await VerifyApi(apiValidateTimeout, job, byondLock, cancellationToken).ConfigureAwait(false); } if (!ddVerified) { //server never validated or compile failed await CleanupFailedCompile().ConfigureAwait(false); await eventConsumer.HandleEvent(EventType.CompileFailure, new List { job.ExitCode == 0 ? "1" : "0" }, cancellationToken).ConfigureAwait(false); } else { job.DMApiValidated = true; Status = CompilerStatus.Duplicating; //duplicate the dmb et al await ioManager.CopyDirectory(dirA, dirB, null, cancellationToken).ConfigureAwait(false); Status = CompilerStatus.Symlinking; //symlink in the static data var symATask = configuration.SymlinkStaticFilesTo(fullDirA, cancellationToken); var symBTask = configuration.SymlinkStaticFilesTo(ioManager.ResolvePath(dirB), cancellationToken); await Task.WhenAll(symATask, symBTask).ConfigureAwait(false); await eventConsumer.HandleEvent(EventType.CompileComplete, null, cancellationToken).ConfigureAwait(false); } await compileJobConsumer.LoadCompileJob(job, cancellationToken).ConfigureAwait(false); return job; } catch { await CleanupFailedCompile().ConfigureAwait(false); throw; } } catch (OperationCanceledException) { await eventConsumer.HandleEvent(EventType.CompileCancelled, null, default).ConfigureAwait(false); throw; } finally { Status = CompilerStatus.Idle; } } } }