/// <inheritdoc />
        public async Task <ServerSideModifications> CopyDMFilesTo(string dmeFile, string destination, CancellationToken cancellationToken)
        {
            using (await SemaphoreSlimContext.Lock(semaphore, cancellationToken).ConfigureAwait(false))
            {
                await EnsureDirectories(cancellationToken).ConfigureAwait(false);

                //just assume no other fs race conditions here
                var dmeExistsTask      = ioManager.FileExists(ioManager.ConcatPath(CodeModificationsSubdirectory, dmeFile), cancellationToken);
                var headFileExistsTask = ioManager.FileExists(ioManager.ConcatPath(CodeModificationsSubdirectory, CodeModificationsHeadFile), cancellationToken);
                var tailFileExistsTask = ioManager.FileExists(ioManager.ConcatPath(CodeModificationsSubdirectory, CodeModificationsTailFile), cancellationToken);
                var copyTask           = ioManager.CopyDirectory(CodeModificationsSubdirectory, destination, null, cancellationToken);

                await Task.WhenAll(dmeExistsTask, headFileExistsTask, tailFileExistsTask, copyTask).ConfigureAwait(false);

                if (!dmeExistsTask.Result && !headFileExistsTask.Result && !tailFileExistsTask.Result)
                {
                    return(null);
                }

                if (dmeExistsTask.Result)
                {
                    return(new ServerSideModifications(null, null, true));
                }

                if (!headFileExistsTask.Result && !tailFileExistsTask.Result)
                {
                    return(null);
                }

                string IncludeLine(string filePath) => String.Format(CultureInfo.InvariantCulture, "#include \"{0}\"", filePath);

                return(new ServerSideModifications(headFileExistsTask.Result ? IncludeLine(CodeModificationsHeadFile) : null, tailFileExistsTask.Result ? IncludeLine(CodeModificationsTailFile) : null, false));
            }
        }
Exemple #2
0
        /// <inheritdoc />
        public async Task <IDmbProvider> FromCompileJob(CompileJob compileJob, CancellationToken cancellationToken)
        {
            logger.LogTrace("Loading compile job {0}...", compileJob.Id);
            var providerSubmitted = false;
            var newProvider       = new DmbProvider(compileJob, ioManager, () =>
            {
                if (providerSubmitted)
                {
                    CleanJob(compileJob);
                }
            });

            try
            {
                var primaryCheckTask   = ioManager.FileExists(ioManager.ConcatPath(newProvider.PrimaryDirectory, newProvider.DmbName), cancellationToken);
                var secondaryCheckTask = ioManager.FileExists(ioManager.ConcatPath(newProvider.PrimaryDirectory, newProvider.DmbName), cancellationToken);

                if (!(await primaryCheckTask.ConfigureAwait(false) && await secondaryCheckTask.ConfigureAwait(false)))
                {
                    logger.LogWarning("Error loading compile job, .dmb missing!");
                    return(null);                       //omae wa mou shinderu
                }

                lock (this)
                {
                    if (!jobLockCounts.TryGetValue(compileJob.Id, out int value))
                    {
                        value = 1;
                        jobLockCounts.Add(compileJob.Id, 1);
                    }
                    else
                    {
                        jobLockCounts[compileJob.Id] = ++value;
                    }

                    logger.LogTrace("Compile job {0} lock count now: {1}", compileJob.Id, value);

                    providerSubmitted = true;
                    return(newProvider);
                }
            }
            finally
            {
                if (!providerSubmitted)
                {
                    newProvider.Dispose();
                }
            }
        }
        /// <summary>
        /// Ensures standard configuration directories exist
        /// </summary>
        /// <param name="cancellationToken">The <see cref="CancellationToken"/> for the operation</param>
        /// <returns>A <see cref="Task"/> representing the running operation</returns>
        async Task EnsureDirectories(CancellationToken cancellationToken)
        {
            async Task ValidateStaticFolder()
            {
                await ioManager.CreateDirectory(GameStaticFilesSubdirectory, cancellationToken).ConfigureAwait(false);

                var staticIgnorePath = StaticIgnorePath();

                if (!await ioManager.FileExists(staticIgnorePath, cancellationToken).ConfigureAwait(false))
                {
                    await ioManager.WriteAllBytes(staticIgnorePath, Array.Empty <byte>(), cancellationToken).ConfigureAwait(false);
                }
            }

            await Task.WhenAll(ioManager.CreateDirectory(CodeModificationsSubdirectory, cancellationToken), ioManager.CreateDirectory(EventScriptsSubdirectory, cancellationToken), ValidateStaticFolder()).ConfigureAwait(false);
        }
Exemple #4
0
        /// <inheritdoc />
        public async Task TrustDmbPath(string fullDmbPath, CancellationToken cancellationToken)
        {
            if (fullDmbPath == null)
            {
                throw new ArgumentNullException(nameof(fullDmbPath));
            }

            using (await SemaphoreSlimContext.Lock(trustedFileSemaphore, cancellationToken).ConfigureAwait(false))
            {
                string trustedFileText;

                if (await ioManager.FileExists(trustedFilePath, cancellationToken).ConfigureAwait(false))
                {
                    var trustedFileBytes = await ioManager.ReadAllBytes(trustedFilePath, cancellationToken).ConfigureAwait(false);

                    trustedFileText = Encoding.UTF8.GetString(trustedFileBytes);
                    trustedFileText = $"{trustedFileText.Trim()}{Environment.NewLine}";
                }
                else
                {
                    trustedFileText = String.Empty;
                }

                if (trustedFileText.Contains(fullDmbPath, StringComparison.Ordinal))
                {
                    return;
                }

                trustedFileText = $"{trustedFileText}{fullDmbPath}{Environment.NewLine}";

                var newTrustedFileBytes = Encoding.UTF8.GetBytes(trustedFileText);
                await ioManager.WriteAllBytes(trustedFilePath, newTrustedFileBytes, cancellationToken).ConfigureAwait(false);
            }
        }
Exemple #5
0
        public async Task <IActionResult> Create([FromBody] Api.Models.Instance model, CancellationToken cancellationToken)
        {
            if (model == null)
            {
                throw new ArgumentNullException(nameof(model));
            }

            if (String.IsNullOrWhiteSpace(model.Name))
            {
                return(BadRequest(new ErrorMessage(ErrorCode.InstanceWhitespaceName)));
            }

            var unNormalizedPath   = model.Path;
            var targetInstancePath = NormalizePath(unNormalizedPath);

            model.Path = targetInstancePath;

            var installationDirectoryPath = NormalizePath(DefaultIOManager.CurrentDirectory);

            bool InstanceIsChildOf(string otherPath)
            {
                if (!targetInstancePath.StartsWith(otherPath, StringComparison.Ordinal))
                {
                    return(false);
                }

                bool sameLength       = targetInstancePath.Length == otherPath.Length;
                char dirSeparatorChar = targetInstancePath.ToCharArray()[Math.Min(otherPath.Length, targetInstancePath.Length - 1)];

                return(sameLength ||
                       dirSeparatorChar == Path.DirectorySeparatorChar ||
                       dirSeparatorChar == Path.AltDirectorySeparatorChar);
            }

            if (InstanceIsChildOf(installationDirectoryPath))
            {
                return(Conflict(new ErrorMessage(ErrorCode.InstanceAtConflictingPath)));
            }

            // Validate it's not a child of any other instance
            IActionResult earlyOut = null;
            ulong         countOfOtherInstances = 0;

            using (var cts = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken))
            {
                var newCancellationToken = cts.Token;
                try
                {
                    await DatabaseContext
                    .Instances
                    .AsQueryable()
                    .Select(x => new Models.Instance
                    {
                        Path = x.Path
                    })
                    .ForEachAsync(
                        otherInstance =>
                    {
                        if (++countOfOtherInstances >= generalConfiguration.InstanceLimit)
                        {
                            earlyOut ??= Conflict(new ErrorMessage(ErrorCode.InstanceLimitReached));
                        }
                        else if (InstanceIsChildOf(otherInstance.Path))
                        {
                            earlyOut ??= Conflict(new ErrorMessage(ErrorCode.InstanceAtConflictingPath));
                        }

                        if (earlyOut != null && !newCancellationToken.IsCancellationRequested)
                        {
                            cts.Cancel();
                        }
                    },
                        newCancellationToken)
                    .ConfigureAwait(false);
                }
                catch (OperationCanceledException)
                {
                    cancellationToken.ThrowIfCancellationRequested();
                }
            }

            if (earlyOut != null)
            {
                return(earlyOut);
            }

            // Last test, ensure it's in the list of valid paths
            if (!(generalConfiguration.ValidInstancePaths?
                  .Select(path => NormalizePath(path))
                  .Any(path => InstanceIsChildOf(path)) ?? true))
            {
                return(BadRequest(new ErrorMessage(ErrorCode.InstanceNotAtWhitelistedPath)));
            }

            async Task <bool> DirExistsAndIsNotEmpty()
            {
                if (!await ioManager.DirectoryExists(model.Path, cancellationToken).ConfigureAwait(false))
                {
                    return(false);
                }

                var filesTask = ioManager.GetFiles(model.Path, cancellationToken);
                var dirsTask  = ioManager.GetDirectories(model.Path, cancellationToken);

                var files = await filesTask.ConfigureAwait(false);

                var dirs = await dirsTask.ConfigureAwait(false);

                return(files.Concat(dirs).Any());
            }

            var  dirExistsTask = DirExistsAndIsNotEmpty();
            bool attached      = false;

            if (await ioManager.FileExists(model.Path, cancellationToken).ConfigureAwait(false) || await dirExistsTask.ConfigureAwait(false))
            {
                if (!await ioManager.FileExists(ioManager.ConcatPath(model.Path, InstanceAttachFileName), cancellationToken).ConfigureAwait(false))
                {
                    return(Conflict(new ErrorMessage(ErrorCode.InstanceAtExistingPath)));
                }
                else
                {
                    attached = true;
                }
            }

            var newInstance = CreateDefaultInstance(model);

            DatabaseContext.Instances.Add(newInstance);
            try
            {
                await DatabaseContext.Save(cancellationToken).ConfigureAwait(false);

                try
                {
                    // actually reserve it now
                    await ioManager.CreateDirectory(unNormalizedPath, cancellationToken).ConfigureAwait(false);

                    await ioManager.DeleteFile(ioManager.ConcatPath(targetInstancePath, InstanceAttachFileName), cancellationToken).ConfigureAwait(false);
                }
                catch
                {
                    // oh shit delete the model
                    DatabaseContext.Instances.Remove(newInstance);

                    // DCT: Operation must always run
                    await DatabaseContext.Save(default).ConfigureAwait(false);
        /// <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);
                }
            }
        }
Exemple #7
0
        /// <inheritdoc />
                #pragma warning disable CA1506 // TODO: Decomplexify
        public async Task <IDmbProvider> FromCompileJob(CompileJob compileJob, CancellationToken cancellationToken)
        {
            if (compileJob == null)
            {
                throw new ArgumentNullException(nameof(compileJob));
            }

            // ensure we have the entire compile job tree
            await databaseContextFactory.UseContext(async db => compileJob = await db.CompileJobs.Where(x => x.Id == compileJob.Id)
                                                    .Include(x => x.Job).ThenInclude(x => x.StartedBy)
                                                    .Include(x => x.RevisionInformation).ThenInclude(x => x.PrimaryTestMerge).ThenInclude(x => x.MergedBy)
                                                    .Include(x => x.RevisionInformation).ThenInclude(x => x.ActiveTestMerges).ThenInclude(x => x.TestMerge).ThenInclude(x => x.MergedBy)
                                                    .FirstAsync(cancellationToken).ConfigureAwait(false)).ConfigureAwait(false); // can't wait to see that query

            logger.LogTrace("Loading compile job {0}...", compileJob.Id);
            var providerSubmitted = false;
            var newProvider       = new DmbProvider(compileJob, ioManager, () =>
            {
                if (providerSubmitted)
                {
                    CleanJob(compileJob);
                }
            });

            try
            {
                var primaryCheckTask   = ioManager.FileExists(ioManager.ConcatPath(newProvider.PrimaryDirectory, newProvider.DmbName), cancellationToken);
                var secondaryCheckTask = ioManager.FileExists(ioManager.ConcatPath(newProvider.PrimaryDirectory, newProvider.DmbName), cancellationToken);

                if (!(await primaryCheckTask.ConfigureAwait(false) && await secondaryCheckTask.ConfigureAwait(false)))
                {
                    logger.LogWarning("Error loading compile job, .dmb missing!");
                    return(null);                    // omae wa mou shinderu
                }

                lock (this)
                {
                    if (!jobLockCounts.TryGetValue(compileJob.Id, out int value))
                    {
                        value = 1;
                        jobLockCounts.Add(compileJob.Id, 1);
                    }
                    else
                    {
                        jobLockCounts[compileJob.Id] = ++value;
                    }

                    logger.LogTrace("Compile job {0} lock count now: {1}", compileJob.Id, value);

                    providerSubmitted = true;
                    return(newProvider);
                }
            }
            finally
            {
                if (!providerSubmitted)
                {
                    newProvider.Dispose();
                }
            }
        }
        public async Task <IActionResult> Create([FromBody] Api.Models.Instance model, CancellationToken cancellationToken)
        {
            if (model == null)
            {
                throw new ArgumentNullException(nameof(model));
            }

            if (String.IsNullOrWhiteSpace(model.Name))
            {
                return(BadRequest(new ErrorMessage {
                    Message = "name must not be empty!"
                }));
            }

            if (model.Path == null)
            {
                return(BadRequest(new ErrorMessage {
                    Message = "path must not be empty!"
                }));
            }

            NormalizeModelPath(model, out var rawPath);

            var localPath = ioManager.ResolvePath(".");

            NormalizeModelPath(new Api.Models.Instance
            {
                Path = localPath
            }, out var normalizedLocalPath);

            if (rawPath.StartsWith(normalizedLocalPath, StringComparison.Ordinal))
            {
                bool sameLength       = rawPath.Length == normalizedLocalPath.Length;
                char dirSeparatorChar = rawPath.ToCharArray()[normalizedLocalPath.Length];
                if (sameLength ||
                    dirSeparatorChar == Path.DirectorySeparatorChar ||
                    dirSeparatorChar == Path.AltDirectorySeparatorChar)
                {
                    return(Conflict(new ErrorMessage {
                        Message = "Instances cannot be created in the installation directory!"
                    }));
                }
            }

            var  dirExistsTask = ioManager.DirectoryExists(model.Path, cancellationToken);
            bool attached      = false;

            if (await ioManager.FileExists(model.Path, cancellationToken).ConfigureAwait(false) || await dirExistsTask.ConfigureAwait(false))
            {
                if (!await ioManager.FileExists(ioManager.ConcatPath(model.Path, InstanceAttachFileName), cancellationToken).ConfigureAwait(false))
                {
                    return(Conflict(new ErrorMessage {
                        Message = "Path not empty!"
                    }));
                }
                else
                {
                    attached = true;
                }
            }

            var newInstance = new Models.Instance
            {
                ConfigurationType   = model.ConfigurationType ?? ConfigurationType.Disallowed,
                DreamDaemonSettings = new DreamDaemonSettings
                {
                    AllowWebClient = false,
                    AutoStart      = false,
                    PrimaryPort    = 1337,
                    SecondaryPort  = 1338,
                    SecurityLevel  = DreamDaemonSecurity.Safe,
                    SoftRestart    = false,
                    SoftShutdown   = false,
                    StartupTimeout = 20
                },
                DreamMakerSettings = new DreamMakerSettings
                {
                    ApiValidationPort          = 1339,
                    ApiValidationSecurityLevel = DreamDaemonSecurity.Safe
                },
                Name               = model.Name,
                Online             = false,
                Path               = model.Path,
                AutoUpdateInterval = model.AutoUpdateInterval ?? 0,
                RepositorySettings = new RepositorySettings
                {
                    CommitterEmail            = "*****@*****.**",
                    CommitterName             = application.VersionPrefix,
                    PushTestMergeCommits      = false,
                    ShowTestMergeCommitters   = false,
                    AutoUpdatesKeepTestMerges = false,
                    AutoUpdatesSynchronize    = false,
                    PostTestMergeComment      = false
                },
                InstanceUsers = new List <Models.InstanceUser>                // give this user full privileges on the instance
                {
                    InstanceAdminUser()
                }
            };

            DatabaseContext.Instances.Add(newInstance);
            try
            {
                await DatabaseContext.Save(cancellationToken).ConfigureAwait(false);

                try
                {
                    // actually reserve it now
                    await ioManager.CreateDirectory(rawPath, cancellationToken).ConfigureAwait(false);

                    await ioManager.DeleteFile(ioManager.ConcatPath(rawPath, InstanceAttachFileName), cancellationToken).ConfigureAwait(false);
                }
                catch
                {
                    // oh shit delete the model
                    DatabaseContext.Instances.Remove(newInstance);

                    await DatabaseContext.Save(default).ConfigureAwait(false);
Exemple #9
0
        /// <inheritdoc />
        public async Task <bool> CheckRunWizard(CancellationToken cancellationToken)
        {
            var setupWizardMode = generalConfiguration.SetupWizardMode;

            logger.LogTrace("Checking if setup wizard should run. SetupWizardMode: {0}", setupWizardMode);

            if (setupWizardMode == SetupWizardMode.Never)
            {
                logger.LogTrace("Skipping due to configuration...");
                return(false);
            }

            var forceRun = setupWizardMode == SetupWizardMode.Force || setupWizardMode == SetupWizardMode.Only;

            if (!console.Available)
            {
                if (forceRun)
                {
                    throw new InvalidOperationException("Asked to run setup wizard with no console avaliable!");
                }
                logger.LogTrace("Skipping due to console not being available...");
                return(false);
            }

            var userConfigFileName = String.Format(CultureInfo.InvariantCulture, "appsettings.{0}.json", hostingEnvironment.EnvironmentName);
            var exists             = await ioManager.FileExists(userConfigFileName, cancellationToken).ConfigureAwait(false);

            bool shouldRunBasedOnAutodetect;

            if (exists)
            {
                var bytes = await ioManager.ReadAllBytes(userConfigFileName, cancellationToken).ConfigureAwait(false);

                var contents = Encoding.UTF8.GetString(bytes);
                var existingConfigIsEmpty = String.IsNullOrWhiteSpace(contents);
                logger.LogTrace("Configuration json detected. Empty: {0}", existingConfigIsEmpty);
                shouldRunBasedOnAutodetect = existingConfigIsEmpty;
            }
            else
            {
                shouldRunBasedOnAutodetect = true;
                logger.LogTrace("No configuration json detected");
            }


            if (!shouldRunBasedOnAutodetect)
            {
                if (forceRun)
                {
                    logger.LogTrace("Asking user to bypass due to force run request...");
                    await console.WriteAsync(String.Format(CultureInfo.InvariantCulture, "The configuration settings are requesting the setup wizard be run, but you already appear to have a configuration file ({0})!", userConfigFileName), true, cancellationToken).ConfigureAwait(false);

                    forceRun = await PromptYesNo("Continue running setup wizard? (y/n): ", cancellationToken).ConfigureAwait(false);
                }
                if (!forceRun)
                {
                    return(false);
                }
            }

            //flush the logs to prevent console conflicts
            await Task.Delay(TimeSpan.FromSeconds(1), cancellationToken).ConfigureAwait(false);

            await RunWizard(userConfigFileName, cancellationToken).ConfigureAwait(false);

            return(true);
        }
Exemple #10
0
        public async Task WriteZipsToCSVFileAsync(string path, IEnumerable <ZipFile> records)
        {
            try
            {
                if (String.IsNullOrWhiteSpace(path) || String.IsNullOrEmpty(path))
                {
                    throw new ArgumentException("Path is empty");
                }
            }

            catch (ArgumentException ex)
            {
                using (EventLog eventLog = new EventLog("Application"))
                {
                    eventLog.Source = "Application";
                    eventLog.WriteEntry($@"Cannot find path: {path} . The Path could not be found. /n Stack trace {ex}", EventLogEntryType.FailureAudit, 101, 1);
                }
            }

            try
            {
                if (!path.ToLower().EndsWith(".csv"))
                {
                    throw new ArgumentException("Path is not a CSV");
                }
            }

            catch (ArgumentException ex)
            {
                using (EventLog eventLog = new EventLog("Application"))
                {
                    eventLog.Source = "Application";
                    eventLog.WriteEntry($@"File is not a CSV. /n Stack trace {ex}", EventLogEntryType.FailureAudit, 101, 1);
                }
                throw ex;
            }

            if (!ioManager.FileExists(path))
            {
                ioManager.CreateFile(path);
            }
            try
            {
                if (records == null)
                {
                    throw new ArgumentNullException("Records was null");
                }
                if (!(records.Count() >= 1))
                {
                    throw new ArgumentException("No records");
                }
            }
            catch (ArgumentNullException ex)
            {
                using (EventLog eventLog = new EventLog("Application"))
                {
                    eventLog.Source = "Application";
                    eventLog.WriteEntry($@"No records found. Records was null. /n Stack trace {ex}", EventLogEntryType.FailureAudit, 101, 1);
                }
                throw;
            }
            catch (ArgumentException ex)
            {
                using (EventLog eventLog = new EventLog("Application"))
                {
                    eventLog.Source = "Application";
                    eventLog.WriteEntry($@"No records found. /n Stack trace {ex}", EventLogEntryType.FailureAudit, 101, 1);
                }
                throw;
            }

            await csvRepository.WriteZipsToCSVFileAsync(path, records);
        }
Exemple #11
0
        public async Task <IActionResult> Create([FromBody] Api.Models.Instance model, CancellationToken cancellationToken)
        {
            if (model == null)
            {
                throw new ArgumentNullException(nameof(model));
            }

            if (String.IsNullOrWhiteSpace(model.Name))
            {
                return(BadRequest(new ErrorMessage(ErrorCode.InstanceWhitespaceName)));
            }

            var targetInstancePath = NormalizePath(model.Path);

            model.Path = targetInstancePath;

            var installationDirectoryPath = NormalizePath(DefaultIOManager.CurrentDirectory);

            bool InstanceIsChildOf(string otherPath)
            {
                if (!targetInstancePath.StartsWith(otherPath, StringComparison.Ordinal))
                {
                    return(false);
                }

                bool sameLength       = targetInstancePath.Length == otherPath.Length;
                char dirSeparatorChar = targetInstancePath.ToCharArray()[Math.Min(otherPath.Length, targetInstancePath.Length - 1)];

                return(sameLength ||
                       dirSeparatorChar == Path.DirectorySeparatorChar ||
                       dirSeparatorChar == Path.AltDirectorySeparatorChar);
            }

            if (InstanceIsChildOf(installationDirectoryPath))
            {
                return(Conflict(new ErrorMessage(ErrorCode.InstanceAtConflictingPath)));
            }

            // Validate it's not a child of any other instance
            IActionResult earlyOut = null;
            ulong         countOfOtherInstances = 0;

            using (var cts = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken))
            {
                var newCancellationToken = cts.Token;
                try
                {
                    await DatabaseContext
                    .Instances
                    .AsQueryable()
                    .Select(x => new Models.Instance
                    {
                        Path = x.Path
                    })
                    .ForEachAsync(
                        otherInstance =>
                    {
                        if (++countOfOtherInstances >= generalConfiguration.InstanceLimit)
                        {
                            earlyOut ??= Conflict(new ErrorMessage(ErrorCode.InstanceLimitReached));
                        }
                        else if (InstanceIsChildOf(otherInstance.Path))
                        {
                            earlyOut ??= Conflict(new ErrorMessage(ErrorCode.InstanceAtConflictingPath));
                        }

                        if (earlyOut != null && !newCancellationToken.IsCancellationRequested)
                        {
                            cts.Cancel();
                        }
                    },
                        newCancellationToken)
                    .ConfigureAwait(false);
                }
                catch (OperationCanceledException)
                {
                    cancellationToken.ThrowIfCancellationRequested();
                }
            }

            if (earlyOut != null)
            {
                return(earlyOut);
            }

            // Last test, ensure it's in the list of valid paths
            if (!(generalConfiguration.ValidInstancePaths?
                  .Select(path => NormalizePath(path))
                  .Any(path => InstanceIsChildOf(path)) ?? true))
            {
                return(BadRequest(new ErrorMessage(ErrorCode.InstanceNotAtWhitelistedPath)));
            }

            async Task <bool> DirExistsAndIsNotEmpty()
            {
                if (!await ioManager.DirectoryExists(model.Path, cancellationToken).ConfigureAwait(false))
                {
                    return(false);
                }

                var filesTask = ioManager.GetFiles(model.Path, cancellationToken);
                var dirsTask  = ioManager.GetDirectories(model.Path, cancellationToken);

                var files = await filesTask.ConfigureAwait(false);

                var dirs = await dirsTask.ConfigureAwait(false);

                return(files.Concat(dirs).Any());
            }

            var  dirExistsTask = DirExistsAndIsNotEmpty();
            bool attached      = false;

            if (await ioManager.FileExists(model.Path, cancellationToken).ConfigureAwait(false) || await dirExistsTask.ConfigureAwait(false))
            {
                if (!await ioManager.FileExists(ioManager.ConcatPath(model.Path, InstanceAttachFileName), cancellationToken).ConfigureAwait(false))
                {
                    return(Conflict(new ErrorMessage(ErrorCode.InstanceAtExistingPath)));
                }
                else
                {
                    attached = true;
                }
            }

            var newInstance = new Models.Instance
            {
                ConfigurationType   = model.ConfigurationType ?? ConfigurationType.Disallowed,
                DreamDaemonSettings = new DreamDaemonSettings
                {
                    AllowWebClient   = false,
                    AutoStart        = false,
                    PrimaryPort      = 1337,
                    SecondaryPort    = 1338,
                    SecurityLevel    = DreamDaemonSecurity.Safe,
                    StartupTimeout   = 60,
                    HeartbeatSeconds = 60
                },
                DreamMakerSettings = new DreamMakerSettings
                {
                    ApiValidationPort          = 1339,
                    ApiValidationSecurityLevel = DreamDaemonSecurity.Safe
                },
                Name               = model.Name,
                Online             = false,
                Path               = model.Path,
                AutoUpdateInterval = model.AutoUpdateInterval ?? 0,
                ChatBotLimit       = model.ChatBotLimit ?? Models.Instance.DefaultChatBotLimit,
                RepositorySettings = new RepositorySettings
                {
                    CommitterEmail            = "*****@*****.**",
                    CommitterName             = assemblyInformationProvider.VersionPrefix,
                    PushTestMergeCommits      = false,
                    ShowTestMergeCommitters   = false,
                    AutoUpdatesKeepTestMerges = false,
                    AutoUpdatesSynchronize    = false,
                    PostTestMergeComment      = false
                },
                InstanceUsers = new List <Models.InstanceUser>                // give this user full privileges on the instance
                {
                    InstanceAdminUser()
                }
            };

            DatabaseContext.Instances.Add(newInstance);
            try
            {
                await DatabaseContext.Save(cancellationToken).ConfigureAwait(false);

                try
                {
                    // actually reserve it now
                    await ioManager.CreateDirectory(targetInstancePath, cancellationToken).ConfigureAwait(false);

                    await ioManager.DeleteFile(ioManager.ConcatPath(targetInstancePath, InstanceAttachFileName), cancellationToken).ConfigureAwait(false);
                }
                catch
                {
                    // oh shit delete the model
                    DatabaseContext.Instances.Remove(newInstance);

                    await DatabaseContext.Save(default).ConfigureAwait(false);
Exemple #12
0
        /// <inheritdoc />
        public async Task StartAsync(CancellationToken cancellationToken)
        {
            async Task <byte[]> GetActiveVersion()
            {
                var activeVersionFileExists = await ioManager.FileExists(ActiveVersionFileName, cancellationToken).ConfigureAwait(false);

                return(!activeVersionFileExists ? null : await ioManager.ReadAllBytes(ActiveVersionFileName, cancellationToken).ConfigureAwait(false));
            }

            var activeVersionBytesTask = GetActiveVersion();

            // Create local cfg directory in case it doesn't exist
            var localCfgDirectory = ioManager.ConcatPath(
                byondInstaller.PathToUserByondFolder,
                CfgDirectoryName);
            await ioManager.CreateDirectory(
                localCfgDirectory,
                cancellationToken).ConfigureAwait(false);

            // Delete trusted.txt so it doesn't grow too large
            var trustedFilePath =
                ioManager.ConcatPath(
                    localCfgDirectory,
                    TrustedDmbFileName);

            logger.LogTrace("Deleting trusted .dmbs file {0}", trustedFilePath);
            await ioManager.DeleteFile(
                trustedFilePath,
                cancellationToken).ConfigureAwait(false);

            var byondDirectory = ioManager.ResolvePath();
            await ioManager.CreateDirectory(byondDirectory, cancellationToken).ConfigureAwait(false);

            var directories = await ioManager.GetDirectories(byondDirectory, cancellationToken).ConfigureAwait(false);

            async Task ReadVersion(string path)
            {
                var versionFile = ioManager.ConcatPath(path, VersionFileName);

                if (!await ioManager.FileExists(versionFile, cancellationToken).ConfigureAwait(false))
                {
                    logger.LogInformation("Cleaning unparsable version path: {0}", ioManager.ResolvePath(path));
                    await ioManager.DeleteDirectory(path, cancellationToken).ConfigureAwait(false);                     // cleanup

                    return;
                }

                var bytes = await ioManager.ReadAllBytes(versionFile, cancellationToken).ConfigureAwait(false);

                var text = Encoding.UTF8.GetString(bytes);

                if (Version.TryParse(text, out var version))
                {
                    var key = VersionKey(version);
                    lock (installedVersions)
                        if (!installedVersions.ContainsKey(key))
                        {
                            logger.LogDebug("Adding detected BYOND version {0}...", key);
                            installedVersions.Add(key, Task.CompletedTask);
                            return;
                        }
                }

                await ioManager.DeleteDirectory(path, cancellationToken).ConfigureAwait(false);
            }

            await Task.WhenAll(directories.Select(x => ReadVersion(x))).ConfigureAwait(false);

            var activeVersionBytes = await activeVersionBytesTask.ConfigureAwait(false);

            if (activeVersionBytes != null)
            {
                var  activeVersionString = Encoding.UTF8.GetString(activeVersionBytes);
                bool hasRequestedActiveVersion;
                lock (installedVersions)
                    hasRequestedActiveVersion = installedVersions.ContainsKey(activeVersionString);
                if (hasRequestedActiveVersion && Version.TryParse(activeVersionString, out var activeVersion))
                {
                    ActiveVersion = activeVersion.Semver();
                }
                else
                {
                    logger.LogWarning("Failed to load saved active version {0}!", activeVersionString);
                    await ioManager.DeleteFile(ActiveVersionFileName, cancellationToken).ConfigureAwait(false);
                }
            }
        }
Exemple #13
0
        /// <summary>
        /// Prompts the user to create a <see cref="DatabaseConfiguration"/>
        /// </summary>
        /// <param name="cancellationToken">The <see cref="CancellationToken"/> for the operation</param>
        /// <returns>A <see cref="Task{TResult}"/> resulting in the new <see cref="DatabaseConfiguration"/></returns>
        async Task <DatabaseConfiguration> ConfigureDatabase(CancellationToken cancellationToken)
        {
            do
            {
                await console.WriteAsync(null, true, cancellationToken).ConfigureAwait(false);

                await console.WriteAsync("What SQL database type will you be using?", true, cancellationToken).ConfigureAwait(false);

                var databaseConfiguration = new DatabaseConfiguration
                {
                    DatabaseType = await PromptDatabaseType(cancellationToken).ConfigureAwait(false)
                };

                string serverAddress   = null;
                uint?  mySQLServerPort = null;

                bool isSqliteDB = databaseConfiguration.DatabaseType == DatabaseType.Sqlite;
                if (!isSqliteDB)
                {
                    do
                    {
                        await console.WriteAsync(null, true, cancellationToken).ConfigureAwait(false);

                        await console.WriteAsync("Enter the server's address and port [<server>:<port> or <server>] (blank for local): ", false, cancellationToken).ConfigureAwait(false);

                        serverAddress = await console.ReadLineAsync(false, cancellationToken).ConfigureAwait(false);

                        if (!String.IsNullOrWhiteSpace(serverAddress) && databaseConfiguration.DatabaseType == DatabaseType.SqlServer)
                        {
                            var match = Regex.Match(serverAddress, @"^(?<server>.+):(?<port>.+)$");
                            if (match.Success)
                            {
                                serverAddress = match.Groups["server"].Value;
                                var portString = match.Groups["port"].Value;
                                if (uint.TryParse(portString, out uint port))
                                {
                                    mySQLServerPort = port;
                                }
                                else
                                {
                                    await console.WriteAsync($"Failed to parse port \"{portString}\", please try again.", true, cancellationToken).ConfigureAwait(false);

                                    continue;
                                }
                            }
                        }

                        break;
                    }while (true);
                }

                await console.WriteAsync(null, true, cancellationToken).ConfigureAwait(false);

                await console.WriteAsync($"Enter the database {(isSqliteDB ? "file path" : "name")} (Can be from previous installation. Otherwise, should not exist): ", false, cancellationToken).ConfigureAwait(false);

                string databaseName;
                bool   dbExists = false;
                do
                {
                    databaseName = await console.ReadLineAsync(false, cancellationToken).ConfigureAwait(false);

                    if (!String.IsNullOrWhiteSpace(databaseName))
                    {
                        if (isSqliteDB)
                        {
                            dbExists = await ioManager.FileExists(databaseName, cancellationToken).ConfigureAwait(false);

                            if (!dbExists)
                            {
                                databaseName = await ValidateNonExistantSqliteDBName(databaseName, cancellationToken).ConfigureAwait(false);
                            }
                        }
                        else
                        {
                            dbExists = await PromptYesNo("Does this database already exist? (y/n): ", cancellationToken).ConfigureAwait(false);
                        }
                    }

                    if (String.IsNullOrWhiteSpace(databaseName))
                    {
                        await console.WriteAsync("Invalid database name!", true, cancellationToken).ConfigureAwait(false);
                    }
                    else
                    {
                        break;
                    }
                }while (true);

                bool useWinAuth;
                if (databaseConfiguration.DatabaseType == DatabaseType.SqlServer && platformIdentifier.IsWindows)
                {
                    useWinAuth = await PromptYesNo("Use Windows Authentication? (y/n): ", cancellationToken).ConfigureAwait(false);
                }
                else
                {
                    useWinAuth = false;
                }

                await console.WriteAsync(null, true, cancellationToken).ConfigureAwait(false);

                string username = null;
                string password = null;
                if (!isSqliteDB)
                {
                    if (!useWinAuth)
                    {
                        await console.WriteAsync("Enter username: "******"Enter password: "******"IMPORTANT: If using the service runner, ensure this computer's LocalSystem account has CREATE DATABASE permissions on the target server!", true, cancellationToken).ConfigureAwait(false);

                        await console.WriteAsync("The account it uses in MSSQL is usually \"NT AUTHORITY\\SYSTEM\" and the role it needs is usually \"dbcreator\".", true, cancellationToken).ConfigureAwait(false);

                        await console.WriteAsync("We'll run a sanity test here, but it won't be indicative of the service's permissions if that is the case", true, cancellationToken).ConfigureAwait(false);
                    }
                }

                await console.WriteAsync(null, true, cancellationToken).ConfigureAwait(false);

                DbConnection testConnection;
                void CreateTestConnection(string connectionString) =>
                testConnection = dbConnectionFactory.CreateConnection(
                    connectionString,
                    databaseConfiguration.DatabaseType);

                if (databaseConfiguration.DatabaseType == DatabaseType.SqlServer)
                {
                    var csb = new SqlConnectionStringBuilder
                    {
                        ApplicationName = application.VersionPrefix,
                        DataSource      = serverAddress ?? "(local)"
                    };

                    if (useWinAuth)
                    {
                        csb.IntegratedSecurity = true;
                    }
                    else
                    {
                        csb.UserID   = username;
                        csb.Password = password;
                    }

                    CreateTestConnection(csb.ConnectionString);
                    csb.InitialCatalog = databaseName;
                    databaseConfiguration.ConnectionString = csb.ConnectionString;
                }
                else if (databaseConfiguration.DatabaseType == DatabaseType.Sqlite)
                {
                    var csb = new SqliteConnectionStringBuilder
                    {
                        DataSource = databaseName,
                        Mode       = dbExists ? SqliteOpenMode.ReadOnly : SqliteOpenMode.ReadWriteCreate
                    };

                    CreateTestConnection(csb.ConnectionString);
                    databaseConfiguration.ConnectionString = csb.ConnectionString;
                }
                else
                {
                    // MySQL/MariaDB
                    var csb = new MySqlConnectionStringBuilder
                    {
                        Server   = serverAddress ?? "127.0.0.1",
                        UserID   = username,
                        Password = password
                    };

                    if (mySQLServerPort.HasValue)
                    {
                        csb.Port = mySQLServerPort.Value;
                    }

                    CreateTestConnection(csb.ConnectionString);
                    csb.Database = databaseName;
                    databaseConfiguration.ConnectionString = csb.ConnectionString;
                }

                try
                {
                    await TestDatabaseConnection(testConnection, databaseConfiguration, databaseName, dbExists, cancellationToken).ConfigureAwait(false);

                    return(databaseConfiguration);
                }
                catch (OperationCanceledException)
                {
                    throw;
                }
                catch (Exception e)
                {
                    await console.WriteAsync(e.Message, true, cancellationToken).ConfigureAwait(false);

                    await console.WriteAsync(null, true, cancellationToken).ConfigureAwait(false);

                    await console.WriteAsync("Retrying database configuration...", true, cancellationToken).ConfigureAwait(false);
                }
            }while (true);
        }
        /// <summary>
        /// Executes and populate a given <paramref name="job"/>
        /// </summary>
        /// <param name="job">The <see cref="CompileJob"/> to run and populate</param>
        /// <param name="dreamMakerSettings">The <see cref="Api.Models.Internal.DreamMakerSettings"/> to use</param>
        /// <param name="byondLock">The <see cref="IByondExecutableLock"/> to use</param>
        /// <param name="repository">The <see cref="IRepository"/> to use</param>
        /// <param name="remoteDeploymentManager">The <see cref="IRemoteDeploymentManager"/> to use.</param>
        /// <param name="apiValidateTimeout">The timeout for validating the DMAPI</param>
        /// <param name="cancellationToken">The <see cref="CancellationToken"/> for the operation</param>
        /// <returns>A <see cref="Task"/> representing the running operation</returns>
        async Task RunCompileJob(
            Models.CompileJob job,
            Api.Models.Internal.DreamMakerSettings dreamMakerSettings,
            IByondExecutableLock byondLock,
            IRepository repository,
            IRemoteDeploymentManager remoteDeploymentManager,
            uint apiValidateTimeout,
            CancellationToken cancellationToken)
        {
            var outputDirectory = job.DirectoryName.ToString();

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

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

                // repository closed now

                // run precompile scripts
                await eventConsumer.HandleEvent(
                    EventType.CompileStart,
                    new List <string>
                {
                    resolvedOutputDirectory,
                    repoOrigin.ToString()
                },
                    cancellationToken)
                .ConfigureAwait(false);

                // determine the dme
                if (job.DmeName == null)
                {
                    logger.LogTrace("Searching for available .dmes...");
                    var foundPaths = await ioManager.GetFilesWithExtension(resolvedOutputDirectory, DmeExtension, true, cancellationToken).ConfigureAwait(false);

                    var foundPath = foundPaths.FirstOrDefault();
                    if (foundPath == default)
                    {
                        throw new JobException(ErrorCode.DreamMakerNoDme);
                    }
                    job.DmeName = foundPath.Substring(
                        resolvedOutputDirectory.Length + 1,
                        foundPath.Length - resolvedOutputDirectory.Length - DmeExtension.Length - 2);                         // +1 for . in extension
                }
                else
                {
                    var targetDme       = ioManager.ConcatPath(outputDirectory, String.Join('.', job.DmeName, DmeExtension));
                    var targetDmeExists = await ioManager.FileExists(targetDme, cancellationToken).ConfigureAwait(false);

                    if (!targetDmeExists)
                    {
                        throw new JobException(ErrorCode.DreamMakerMissingDme);
                    }
                }

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

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

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

                // verify api
                try
                {
                    if (exitCode != 0)
                    {
                        throw new JobException(
                                  ErrorCode.DreamMakerExitCode,
                                  new JobException($"Exit code: {exitCode}{Environment.NewLine}{Environment.NewLine}{job.Output}"));
                    }

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

                    throw;
                }

                await eventConsumer.HandleEvent(EventType.CompileComplete, new List <string> {
                    resolvedOutputDirectory
                }, cancellationToken).ConfigureAwait(false);

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

                // symlink in the static data
                await configuration.SymlinkStaticFilesTo(resolvedOutputDirectory, cancellationToken).ConfigureAwait(false);

                logger.LogDebug("Compile complete!");
            }
            catch (Exception ex)
            {
                await CleanupFailedCompile(job, remoteDeploymentManager, ex).ConfigureAwait(false);

                throw;
            }
        }
        /// <inheritdoc />
        public async Task CreateDump(global::System.Diagnostics.Process process, string outputFile, CancellationToken cancellationToken)
        {
            if (process == null)
            {
                throw new ArgumentNullException(nameof(process));
            }
            if (outputFile == null)
            {
                throw new ArgumentNullException(nameof(outputFile));
            }

            const string GCorePath = "/usr/bin/gcore";

            if (!await ioManager.FileExists(GCorePath, cancellationToken).ConfigureAwait(false))
            {
                throw new JobException(ErrorCode.MissingGCore);
            }

            int pid;

            try
            {
                if (process.HasExited)
                {
                    throw new JobException(ErrorCode.DreamDaemonOffline);
                }

                pid = process.Id;
            }
            catch (InvalidOperationException ex)
            {
                throw new JobException(ErrorCode.DreamDaemonOffline, ex);
            }

            string output;
            int    exitCode;

            using (var gcoreProc = lazyLoadedProcessExecutor.Value.LaunchProcess(
                       GCorePath,
                       Environment.CurrentDirectory,
                       $"-o {outputFile} {process.Id}",
                       true,
                       true,
                       true))
            {
                using (cancellationToken.Register(() => gcoreProc.Terminate()))
                    exitCode = await gcoreProc.Lifetime.ConfigureAwait(false);

                output = await gcoreProc.GetCombinedOutput(cancellationToken).ConfigureAwait(false);

                logger.LogDebug("gcore output:{0}{1}", Environment.NewLine, output);
            }

            if (exitCode != 0)
            {
                throw new JobException(
                          ErrorCode.GCoreFailure,
                          new JobException(
                              $"Exit Code: {exitCode}{Environment.NewLine}Output:{Environment.NewLine}{output}"));
            }

            // gcore outputs name.pid so remove the pid part
            var generatedGCoreFile = $"{outputFile}.{pid}";
            await ioManager.MoveFile(generatedGCoreFile, outputFile, cancellationToken).ConfigureAwait(false);
        }
Exemple #16
0
        /// <summary>
        /// Executes and populate a given <paramref name="job"/>
        /// </summary>
        /// <param name="job">The <see cref="Models.CompileJob"/> to run and populate</param>
        /// <param name="dreamMakerSettings">The <see cref="Api.Models.DreamMaker"/> settings to use</param>
        /// <param name="byondLock">The <see cref="IByondExecutableLock"/> to use</param>
        /// <param name="repository">The <see cref="IRepository"/> to use</param>
        /// <param name="apiValidateTimeout">The timeout for validating the DMAPI</param>
        /// <param name="cancellationToken">The <see cref="CancellationToken"/> for the operation</param>
        /// <returns>A <see cref="Task"/> representing the running operation</returns>
        async Task RunCompileJob(Models.CompileJob job, Api.Models.DreamMaker dreamMakerSettings, IByondExecutableLock byondLock, IRepository repository, uint apiValidateTimeout, CancellationToken cancellationToken)
        {
            var jobPath = job.DirectoryName.ToString();

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

            try
            {
                var dirA = ioManager.ConcatPath(jobPath, ADirectoryName);
                var dirB = ioManager.ConcatPath(jobPath, BDirectoryName);

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

                // repository closed now

                // run precompile scripts
                await eventConsumer.HandleEvent(EventType.CompileStart, new List <string> {
                    resolvedADirectory, repoOrigin
                }, cancellationToken).ConfigureAwait(false);

                // determine the dme
                if (job.DmeName == null)
                {
                    logger.LogTrace("Searching for available .dmes...");
                    var foundPaths = await ioManager.GetFilesWithExtension(dirA, DmeExtension, cancellationToken).ConfigureAwait(false);

                    var foundPath = foundPaths.FirstOrDefault();
                    if (foundPath == default)
                    {
                        throw new JobException("Unable to find any .dme!");
                    }
                    var dmeWithExtension = ioManager.GetFileName(foundPath);
                    job.DmeName = dmeWithExtension.Substring(0, dmeWithExtension.Length - DmeExtension.Length - 1);
                }
                else
                {
                    var targetDme       = ioManager.ConcatPath(dirA, String.Join('.', job.DmeName, DmeExtension));
                    var targetDmeExists = await ioManager.FileExists(targetDme, cancellationToken).ConfigureAwait(false);

                    if (!targetDmeExists)
                    {
                        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
                var exitCode = await RunDreamMaker(byondLock.DreamMakerPath, job, cancellationToken).ConfigureAwait(false);

                // verify api
                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)
                {
                    // DD never validated or compile failed
                    await eventConsumer.HandleEvent(EventType.CompileFailure, new List <string> {
                        resolvedADirectory, exitCode == 0 ? "1" : "0"
                    }, cancellationToken).ConfigureAwait(false);

                    throw;
                }

                logger.LogTrace("Running post compile event...");
                await eventConsumer.HandleEvent(EventType.CompileComplete, new List <string> {
                    resolvedADirectory
                }, 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(resolvedADirectory, 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!");
            }
            catch (Exception e)
            {
                await CleanupFailedCompile(job, e is OperationCanceledException, cancellationToken).ConfigureAwait(false);

                throw;
            }
        }
        /// <inheritdoc />
                #pragma warning disable CA1506 // TODO: Decomplexify
        public async Task <IDmbProvider> FromCompileJob(CompileJob compileJob, CancellationToken cancellationToken)
        {
            if (compileJob == null)
            {
                throw new ArgumentNullException(nameof(compileJob));
            }

            // ensure we have the entire metadata tree
            logger.LogTrace("Loading compile job {0}...", compileJob.Id);
            await databaseContextFactory.UseContext(
                async db => compileJob = await db
                .CompileJobs
                .AsQueryable()
                .Where(x => x.Id == compileJob.Id)
                .Include(x => x.Job)
                .ThenInclude(x => x.StartedBy)
                .Include(x => x.RevisionInformation)
                .ThenInclude(x => x.PrimaryTestMerge)
                .ThenInclude(x => x.MergedBy)
                .Include(x => x.RevisionInformation)
                .ThenInclude(x => x.ActiveTestMerges)
                .ThenInclude(x => x.TestMerge)
                .ThenInclude(x => x.MergedBy)
                .FirstAsync(cancellationToken)
                .ConfigureAwait(false))
            .ConfigureAwait(false);                     // can't wait to see that query

            if (!compileJob.Job.StoppedAt.HasValue)
            {
                // This happens when we're told to load the compile job that is currently finished up
                // It constitutes an API violation if it's returned by the DreamDaemonController so just set it here
                // Bit of a hack, but it works out to be nearly if not the same value that's put in the DB
                logger.LogTrace("Setting missing StoppedAt for CompileJob.Job #{0}...", compileJob.Job.Id);
                compileJob.Job.StoppedAt = DateTimeOffset.UtcNow;
            }

            var providerSubmitted = false;

            void CleanupAction()
            {
                if (providerSubmitted)
                {
                    CleanJob(compileJob);
                }
            }

            var newProvider = new DmbProvider(compileJob, ioManager, CleanupAction);

            try
            {
                const string LegacyADirectoryName = "A";
                const string LegacyBDirectoryName = "B";

                var dmbExistsAtRoot = await ioManager.FileExists(
                    ioManager.ConcatPath(
                        newProvider.Directory,
                        newProvider.DmbName),
                    cancellationToken)
                                      .ConfigureAwait(false);

                if (!dmbExistsAtRoot)
                {
                    logger.LogTrace("Didn't find .dmb at game directory root, checking A/B dirs...");
                    var primaryCheckTask = ioManager.FileExists(
                        ioManager.ConcatPath(
                            newProvider.Directory,
                            LegacyADirectoryName,
                            newProvider.DmbName),
                        cancellationToken);
                    var secondaryCheckTask = ioManager.FileExists(
                        ioManager.ConcatPath(
                            newProvider.Directory,
                            LegacyBDirectoryName,
                            newProvider.DmbName),
                        cancellationToken);

                    if (!(await primaryCheckTask.ConfigureAwait(false) && await secondaryCheckTask.ConfigureAwait(false)))
                    {
                        logger.LogWarning("Error loading compile job, .dmb missing!");
                        return(null);                        // omae wa mou shinderu
                    }

                    // rebuild the provider because it's using the legacy style directories
                    // Don't dispose it
                    logger.LogDebug("Creating legacy two folder .dmb provider targeting {0} directory...", LegacyADirectoryName);
                    newProvider = new DmbProvider(compileJob, ioManager, CleanupAction, Path.DirectorySeparatorChar + LegacyADirectoryName);
                }

                lock (jobLockCounts)
                {
                    if (!jobLockCounts.TryGetValue(compileJob.Id, out int value))
                    {
                        value = 1;
                        jobLockCounts.Add(compileJob.Id, 1);
                    }
                    else
                    {
                        jobLockCounts[compileJob.Id] = ++value;
                    }

                    providerSubmitted = true;

                    logger.LogTrace("Compile job {0} lock count now: {1}", compileJob.Id, value);
                    return(newProvider);
                }
            }
            finally
            {
                if (!providerSubmitted)
                {
                    newProvider.Dispose();
                }
            }
        }
Exemple #18
0
        /// <inheritdoc />
                #pragma warning disable CA1506 // TODO: Decomplexify
        public async Task <IDmbProvider> FromCompileJob(CompileJob compileJob, CancellationToken cancellationToken)
        {
            if (compileJob == null)
            {
                throw new ArgumentNullException(nameof(compileJob));
            }

            // ensure we have the entire compile job tree
            logger.LogTrace("Loading compile job {0}...", compileJob.Id);
            await databaseContextFactory.UseContext(
                async db => compileJob = await db
                .CompileJobs
                .AsQueryable()
                .Where(x => x.Id == compileJob.Id)
                .Include(x => x.Job).ThenInclude(x => x.StartedBy)
                .Include(x => x.RevisionInformation).ThenInclude(x => x.PrimaryTestMerge).ThenInclude(x => x.MergedBy)
                .Include(x => x.RevisionInformation).ThenInclude(x => x.ActiveTestMerges).ThenInclude(x => x.TestMerge).ThenInclude(x => x.MergedBy)
                .FirstAsync(cancellationToken)
                .ConfigureAwait(false))
            .ConfigureAwait(false);                     // can't wait to see that query

            if (!compileJob.Job.StoppedAt.HasValue)
            {
                // This happens if we're told to load the compile job that is currently finished up
                // It can constitute an API violation if it's returned by the DreamDaemonController so just set it here
                // Bit of a hack, but it should work out to be the same value
                logger.LogTrace("Setting missing StoppedAt for CompileJob job...");
                compileJob.Job.StoppedAt = DateTimeOffset.Now;
            }

            var providerSubmitted = false;
            var newProvider       = new DmbProvider(compileJob, ioManager, () =>
            {
                if (providerSubmitted)
                {
                    CleanJob(compileJob);
                }
            });

            try
            {
                var primaryCheckTask   = ioManager.FileExists(ioManager.ConcatPath(newProvider.PrimaryDirectory, newProvider.DmbName), cancellationToken);
                var secondaryCheckTask = ioManager.FileExists(ioManager.ConcatPath(newProvider.PrimaryDirectory, newProvider.DmbName), cancellationToken);

                if (!(await primaryCheckTask.ConfigureAwait(false) && await secondaryCheckTask.ConfigureAwait(false)))
                {
                    logger.LogWarning("Error loading compile job, .dmb missing!");
                    return(null);                    // omae wa mou shinderu
                }

                lock (jobLockCounts)
                {
                    if (!jobLockCounts.TryGetValue(compileJob.Id, out int value))
                    {
                        value = 1;
                        jobLockCounts.Add(compileJob.Id, 1);
                    }
                    else
                    {
                        jobLockCounts[compileJob.Id] = ++value;
                    }

                    logger.LogTrace("Compile job {0} lock count now: {1}", compileJob.Id, value);

                    providerSubmitted = true;
                    return(newProvider);
                }
            }
            finally
            {
                if (!providerSubmitted)
                {
                    newProvider.Dispose();
                }
            }
        }
Exemple #19
0
        /// <inheritdoc />
        public async Task StartAsync(CancellationToken cancellationToken)
        {
            async Task <byte[]> GetActiveVersion()
            {
                var activeVersionFileExists = await ioManager.FileExists(ActiveVersionFileName, cancellationToken).ConfigureAwait(false);

                return(!activeVersionFileExists ? null : await ioManager.ReadAllBytes(ActiveVersionFileName, cancellationToken).ConfigureAwait(false));
            }

            var activeVersionBytesTask = GetActiveVersion();

            await ioManager.CreateDirectory(".", cancellationToken).ConfigureAwait(false);

            var directories = await ioManager.GetDirectories(".", cancellationToken).ConfigureAwait(false);

            async Task ReadVersion(string path)
            {
                var versionFile = ioManager.ConcatPath(path, VersionFileName);

                if (!await ioManager.FileExists(versionFile, cancellationToken).ConfigureAwait(false))
                {
                    logger.LogInformation("Cleaning unparsable version path: {0}", ioManager.ResolvePath(path));
                    await ioManager.DeleteDirectory(path, cancellationToken).ConfigureAwait(false);                     // cleanup

                    return;
                }

                var bytes = await ioManager.ReadAllBytes(versionFile, cancellationToken).ConfigureAwait(false);

                var text = Encoding.UTF8.GetString(bytes);

                if (Version.TryParse(text, out var version))
                {
                    var key = VersionKey(version);
                    lock (installedVersions)
                        if (!installedVersions.ContainsKey(key))
                        {
                            installedVersions.Add(key, Task.CompletedTask);
                            return;
                        }
                }

                await ioManager.DeleteDirectory(path, cancellationToken).ConfigureAwait(false);
            }

            await Task.WhenAll(directories.Select(x => ReadVersion(x))).ConfigureAwait(false);

            var activeVersionBytes = await activeVersionBytesTask.ConfigureAwait(false);

            if (activeVersionBytes != null)
            {
                var activeVersionString = Encoding.UTF8.GetString(activeVersionBytes);
                if (Version.TryParse(activeVersionString, out var activeVersion))
                {
                    ActiveVersion = activeVersion;
                }
                else
                {
                    await ioManager.DeleteFile(ActiveVersionFileName, cancellationToken).ConfigureAwait(false);
                }
            }
        }