Beispiel #1
0
        /// <inheritdoc />
        public async Task <Models.CompileJob> Compile(Models.RevisionInformation revisionInformation, Api.Models.DreamMaker dreamMakerSettings, uint apiValidateTimeout, IRepository repository, Action <int> progressReporter, TimeSpan?estimatedDuration, CancellationToken cancellationToken)
        {
            if (revisionInformation == null)
            {
                throw new ArgumentNullException(nameof(revisionInformation));
            }

            if (dreamMakerSettings == null)
            {
                throw new ArgumentNullException(nameof(dreamMakerSettings));
            }

            if (repository == null)
            {
                throw new ArgumentNullException(nameof(repository));
            }

            if (progressReporter == null)
            {
                throw new ArgumentNullException(nameof(progressReporter));
            }

            if (dreamMakerSettings.ApiValidationSecurityLevel == DreamDaemonSecurity.Ultrasafe)
            {
                throw new ArgumentOutOfRangeException(nameof(dreamMakerSettings), dreamMakerSettings, "Cannot compile with ultrasafe security!");
            }

            logger.LogTrace("Begin Compile");

            var job = new Models.CompileJob
            {
                DirectoryName       = Guid.NewGuid(),
                DmeName             = dreamMakerSettings.ProjectName,
                RevisionInformation = revisionInformation
            };

            logger.LogTrace("Compile output GUID: {0}", job.DirectoryName);

            lock (this)
            {
                if (compiling)
                {
                    throw new JobException("There is already a compile job in progress!");
                }
                compiling = true;
            }

            using (var progressCts = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken))
            {
                async Task ProgressTask()
                {
                    if (!estimatedDuration.HasValue)
                    {
                        return;
                    }

                    progressReporter(0);
                    var ct            = progressCts.Token;
                    var sleepInterval = estimatedDuration.Value / 100;

                    try
                    {
                        for (var I = 0; I < 99; ++I)
                        {
                            await Task.Delay(sleepInterval, progressCts.Token).ConfigureAwait(false);

                            progressReporter(I + 1);
                        }
                    }
                    catch (OperationCanceledException) { }
                }

                var progressTask = ProgressTask();
                try
                {
                    var    commitInsert = revisionInformation.CommitSha.Substring(0, 7);
                    string remoteCommitInsert;
                    if (revisionInformation.CommitSha == revisionInformation.OriginCommitSha)
                    {
                        commitInsert       = String.Format(CultureInfo.InvariantCulture, "^{0}", commitInsert);
                        remoteCommitInsert = String.Empty;
                    }
                    else
                    {
                        remoteCommitInsert = String.Format(CultureInfo.InvariantCulture, ". Remote commit: ^{0}", revisionInformation.OriginCommitSha.Substring(0, 7));
                    }

                    var testmergeInsert = revisionInformation.ActiveTestMerges.Count == 0 ? String.Empty : String.Format(CultureInfo.InvariantCulture, " (Test Merges: {0})",
                                                                                                                         String.Join(", ", revisionInformation.ActiveTestMerges.Select(x => x.TestMerge).Select(x =>
                    {
                        var result = String.Format(CultureInfo.InvariantCulture, "#{0} at {1}", x.Number, x.PullRequestRevision.Substring(0, 7));
                        if (x.Comment != null)
                        {
                            result += String.Format(CultureInfo.InvariantCulture, " ({0})", x.Comment);
                        }
                        return(result);
                    })));

                    using (var byondLock = await byond.UseExecutables(null, cancellationToken).ConfigureAwait(false))
                    {
                        await chat.SendUpdateMessage(String.Format(CultureInfo.InvariantCulture, "Deploying revision: {0}{1}{2} BYOND Version: {3}", commitInsert, testmergeInsert, remoteCommitInsert, byondLock.Version), cancellationToken).ConfigureAwait(false);

                        async Task CleanupFailedCompile(bool cancelled)
                        {
                            logger.LogTrace("Cleaning compile directory...");
                            var chatTask = chat.SendUpdateMessage(cancelled ? "Deploy cancelled!" : "Deploy failed!", cancellationToken);

                            try
                            {
                                await ioManager.DeleteDirectory(job.DirectoryName.ToString(), CancellationToken.None).ConfigureAwait(false);
                            }
                            catch (Exception e)
                            {
                                logger.LogWarning("Error cleaning up compile directory {0}! Exception: {1}", ioManager.ResolvePath(job.DirectoryName.ToString()), e);
                            }

                            await chatTask.ConfigureAwait(false);
                        };

                        try
                        {
                            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);

                            logger.LogTrace("Copying repository to game directory...");
                            //copy the repository
                            var fullDirA   = ioManager.ResolvePath(dirA);
                            var repoOrigin = repository.Origin;
                            using (repository)
                                await repository.CopyTo(fullDirA, cancellationToken).ConfigureAwait(false);

                            //run precompile scripts
                            var resolvedGameDirectory = ioManager.ResolvePath(ioManager.ConcatPath(job.DirectoryName.ToString(), ADirectoryName));
                            await eventConsumer.HandleEvent(EventType.CompileStart, new List <string> {
                                resolvedGameDirectory, repoOrigin
                            }, cancellationToken).ConfigureAwait(false);

                            //determine the dme
                            if (job.DmeName == null)
                            {
                                logger.LogTrace("Searching for available .dmes...");
                                var path = (await ioManager.GetFilesWithExtension(dirA, DmeExtension, cancellationToken).ConfigureAwait(false)).FirstOrDefault();
                                if (path == default)
                                {
                                    throw new JobException("Unable to find any .dme!");
                                }
                                var dmeWithExtension = ioManager.GetFileName(path);
                                job.DmeName = dmeWithExtension.Substring(0, dmeWithExtension.Length - DmeExtension.Length - 1);
                            }
                            else if (!await ioManager.FileExists(ioManager.ConcatPath(dirA, String.Join('.', job.DmeName, DmeExtension)), cancellationToken).ConfigureAwait(false))
                            {
                                throw new JobException("Unable to locate specified .dme!");
                            }

                            logger.LogDebug("Selected {0}.dme for compilation!", job.DmeName);

                            await ModifyDme(job, cancellationToken).ConfigureAwait(false);

                            //run compiler, verify api
                            job.ByondVersion = byondLock.Version.ToString();

                            var exitCode = await RunDreamMaker(byondLock.DreamMakerPath, job, cancellationToken).ConfigureAwait(false);

                            try
                            {
                                if (exitCode != 0)
                                {
                                    throw new JobException(String.Format(CultureInfo.InvariantCulture, "DM exited with a non-zero code: {0}{1}{2}", exitCode, Environment.NewLine, job.Output));
                                }

                                await VerifyApi(apiValidateTimeout, dreamMakerSettings.ApiValidationSecurityLevel.Value, job, byondLock, dreamMakerSettings.ApiValidationPort.Value, cancellationToken).ConfigureAwait(false);
                            }
                            catch (JobException)
                            {
                                //server never validated or compile failed
                                await eventConsumer.HandleEvent(EventType.CompileFailure, new List <string> {
                                    resolvedGameDirectory, exitCode == 0 ? "1" : "0"
                                }, cancellationToken).ConfigureAwait(false);

                                throw;
                            }

                            logger.LogTrace("Running post compile event...");
                            await eventConsumer.HandleEvent(EventType.CompileComplete, new List <string> {
                                ioManager.ResolvePath(ioManager.ConcatPath(job.DirectoryName.ToString(), ADirectoryName))
                            }, cancellationToken).ConfigureAwait(false);

                            logger.LogTrace("Duplicating compiled game...");

                            //duplicate the dmb et al
                            await ioManager.CopyDirectory(dirA, dirB, null, cancellationToken).ConfigureAwait(false);

                            logger.LogTrace("Applying static game file symlinks...");

                            //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 chat.SendUpdateMessage(String.Format(CultureInfo.InvariantCulture, "Deployment complete!{0}", watchdog.Running ? " Changes will be applied on next server reboot." : String.Empty), cancellationToken).ConfigureAwait(false);

                            logger.LogDebug("Compile complete!");
                            return(job);
                        }
                        catch (Exception e)
                        {
                            await CleanupFailedCompile(e is OperationCanceledException).ConfigureAwait(false);

                            throw;
                        }
                    }
                }
                catch (OperationCanceledException)
                {
                    await eventConsumer.HandleEvent(EventType.CompileCancelled, null, default).ConfigureAwait(false);

                    throw;
                }
                finally
                {
                    compiling = false;
                    progressCts.Cancel();
                    await progressTask.ConfigureAwait(false);
                }
            }
        }
Beispiel #2
0
        /// <inheritdoc />
        public async Task <Models.CompileJob> Compile(Models.RevisionInformation revisionInformation, Api.Models.DreamMaker dreamMakerSettings, uint apiValidateTimeout, IRepository repository, Action <int> progressReporter, TimeSpan?estimatedDuration, CancellationToken cancellationToken)
        {
            if (revisionInformation == null)
            {
                throw new ArgumentNullException(nameof(revisionInformation));
            }

            if (dreamMakerSettings == null)
            {
                throw new ArgumentNullException(nameof(dreamMakerSettings));
            }

            if (repository == null)
            {
                throw new ArgumentNullException(nameof(repository));
            }

            if (progressReporter == null)
            {
                throw new ArgumentNullException(nameof(progressReporter));
            }

            if (dreamMakerSettings.ApiValidationSecurityLevel == DreamDaemonSecurity.Ultrasafe)
            {
                throw new ArgumentOutOfRangeException(nameof(dreamMakerSettings), dreamMakerSettings, "Cannot compile with ultrasafe security!");
            }

            logger.LogTrace("Begin Compile");

            lock (this)
            {
                if (compiling)
                {
                    throw new JobException("There is already a compile job in progress!");
                }
                compiling = true;
            }

            using (var progressCts = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken))
            {
                var progressTask = estimatedDuration.HasValue ? ProgressTask(progressReporter, estimatedDuration.Value, cancellationToken) : Task.CompletedTask;
                try
                {
                    using (var byondLock = await byond.UseExecutables(null, cancellationToken).ConfigureAwait(false))
                    {
                        await SendDeploymentMessage(revisionInformation, byondLock, cancellationToken).ConfigureAwait(false);

                        var job = new Models.CompileJob
                        {
                            DirectoryName       = Guid.NewGuid(),
                            DmeName             = dreamMakerSettings.ProjectName,
                            RevisionInformation = revisionInformation,
                            ByondVersion        = byondLock.Version.ToString()
                        };

                        await RunCompileJob(job, dreamMakerSettings, byondLock, repository, apiValidateTimeout, cancellationToken).ConfigureAwait(false);

                        return(job);
                    }
                }
                catch (OperationCanceledException)
                {
                    await eventConsumer.HandleEvent(EventType.CompileCancelled, null, default).ConfigureAwait(false);

                    throw;
                }
                finally
                {
                    compiling = false;
                    progressCts.Cancel();
                    await progressTask.ConfigureAwait(false);
                }
            }
        }
        /// <inheritdoc />
                #pragma warning disable CA1506 // TODO: Decomplexify
        public async Task <ISessionController> LaunchNew(DreamDaemonLaunchParameters launchParameters, IDmbProvider dmbProvider, IByondExecutableLock currentByondLock, bool primaryPort, bool primaryDirectory, bool apiValidate, CancellationToken cancellationToken)
        {
            var portToUse = primaryPort ? launchParameters.PrimaryPort : launchParameters.SecondaryPort;

            if (!portToUse.HasValue)
            {
                throw new InvalidOperationException("Given port is null!");
            }
            var accessIdentifier = cryptographySuite.GetSecureString();

            const string JsonPostfix = "tgs.json";

            var basePath = primaryDirectory ? dmbProvider.PrimaryDirectory : dmbProvider.SecondaryDirectory;

            // delete all previous tgs json files
            var files = await ioManager.GetFilesWithExtension(basePath, JsonPostfix, cancellationToken).ConfigureAwait(false);

            await Task.WhenAll(files.Select(x => ioManager.DeleteFile(x, cancellationToken))).ConfigureAwait(false);

            // i changed this back from guids, hopefully i don't regret that
            string JsonFile(string name) => String.Format(CultureInfo.InvariantCulture, "{0}.{1}", name, JsonPostfix);

            var securityLevelToUse = launchParameters.SecurityLevel.Value;

            switch (dmbProvider.CompileJob.MinimumSecurityLevel)
            {
            case DreamDaemonSecurity.Ultrasafe:
                break;

            case DreamDaemonSecurity.Safe:
                if (securityLevelToUse == DreamDaemonSecurity.Ultrasafe)
                {
                    securityLevelToUse = DreamDaemonSecurity.Safe;
                }
                break;

            case DreamDaemonSecurity.Trusted:
                securityLevelToUse = DreamDaemonSecurity.Trusted;
                break;

            default:
                throw new InvalidOperationException(String.Format(CultureInfo.InvariantCulture, "Invalid DreamDaemonSecurity value: {0}", dmbProvider.CompileJob.MinimumSecurityLevel));
            }

            // setup interop files
            var interopInfo = new JsonFile
            {
                AccessIdentifier   = accessIdentifier,
                ApiValidateOnly    = apiValidate,
                ChatChannelsJson   = JsonFile("chat_channels"),
                ChatCommandsJson   = JsonFile("chat_commands"),
                ServerCommandsJson = JsonFile("server_commands"),
                InstanceName       = instance.Name,
                SecurityLevel      = securityLevelToUse,
                Revision           = new Api.Models.Internal.RevisionInformation
                {
                    CommitSha       = dmbProvider.CompileJob.RevisionInformation.CommitSha,
                    OriginCommitSha = dmbProvider.CompileJob.RevisionInformation.OriginCommitSha
                }
            };

            interopInfo.TestMerges.AddRange(dmbProvider.CompileJob.RevisionInformation.ActiveTestMerges.Select(x => x.TestMerge).Select(x => new Interop.TestMerge(x, interopInfo.Revision)));

            var interopJsonFile = JsonFile("interop");

            var interopJson = JsonConvert.SerializeObject(interopInfo, new JsonSerializerSettings
            {
                ContractResolver      = new CamelCasePropertyNamesContractResolver(),
                ReferenceLoopHandling = ReferenceLoopHandling.Ignore
            });

            var chatJsonTrackingTask = chat.TrackJsons(basePath, interopInfo.ChatChannelsJson, interopInfo.ChatCommandsJson, cancellationToken);

            await ioManager.WriteAllBytes(ioManager.ConcatPath(basePath, interopJsonFile), Encoding.UTF8.GetBytes(interopJson), cancellationToken).ConfigureAwait(false);

            var chatJsonTrackingContext = await chatJsonTrackingTask.ConfigureAwait(false);

            try
            {
                // get the byond lock
                var byondLock = currentByondLock ?? await byond.UseExecutables(Version.Parse(dmbProvider.CompileJob.ByondVersion), cancellationToken).ConfigureAwait(false);

                try
                {
                    // create interop context
                    var context = new CommContext(ioManager, loggerFactory.CreateLogger <CommContext>(), basePath, interopInfo.ServerCommandsJson);
                    try
                    {
                        // set command line options
                        // more sanitization here cause it uses the same scheme
                        var parameters = String.Format(CultureInfo.InvariantCulture, "{2}={0}&{3}={1}", byondTopicSender.SanitizeString(application.Version.ToString()), byondTopicSender.SanitizeString(interopJsonFile), byondTopicSender.SanitizeString(Constants.DMParamHostVersion), byondTopicSender.SanitizeString(Constants.DMParamInfoJson));

                        var visibility = apiValidate ? "invisible" : "public";

                        // important to run on all ports to allow port changing
                        var arguments = String.Format(CultureInfo.InvariantCulture, "{0} -port {1} -ports 1-65535 {2}-close -{3} -{5} -public -params \"{4}\"",
                                                      dmbProvider.DmbName,
                                                      primaryPort ? launchParameters.PrimaryPort : launchParameters.SecondaryPort,
                                                      launchParameters.AllowWebClient.Value ? "-webclient " : String.Empty,
                                                      SecurityWord(securityLevelToUse),
                                                      parameters,
                                                      visibility);

                        // See https://github.com/tgstation/tgstation-server/issues/719
                        var noShellExecute = !platformIdentifier.IsWindows;

                        // launch dd
                        var process = processExecutor.LaunchProcess(byondLock.DreamDaemonPath, basePath, arguments, noShellExecute: noShellExecute);
                        try
                        {
                            networkPromptReaper.RegisterProcess(process);

                            // return the session controller for it
                            var result = new SessionController(new ReattachInformation
                            {
                                AccessIdentifier = accessIdentifier,
                                Dmb                = dmbProvider,
                                IsPrimary          = primaryDirectory,
                                Port               = portToUse.Value,
                                ProcessId          = process.Id,
                                ChatChannelsJson   = interopInfo.ChatChannelsJson,
                                ChatCommandsJson   = interopInfo.ChatCommandsJson,
                                ServerCommandsJson = interopInfo.ServerCommandsJson,
                            }, process, byondLock, byondTopicSender, chatJsonTrackingContext, context, chat, loggerFactory.CreateLogger <SessionController>(), launchParameters.SecurityLevel, launchParameters.StartupTimeout);

                            // writeback launch parameter's fixed security level
                            launchParameters.SecurityLevel = securityLevelToUse;

                            return(result);
                        }
                        catch
                        {
                            process.Dispose();
                            throw;
                        }
                    }
                    catch
                    {
                        context.Dispose();
                        throw;
                    }
                }
                catch
                {
                    if (currentByondLock == null)
                    {
                        byondLock.Dispose();
                    }
                    throw;
                }
            }
            catch
            {
                chatJsonTrackingContext.Dispose();
                throw;
            }
        }