Пример #1
0
        /// <summary>
        /// Prompts the user to create a <see cref="FileLoggingConfiguration"/>
        /// </summary>
        /// <param name="cancellationToken">The <see cref="CancellationToken"/> for the operation</param>
        /// <returns>A <see cref="Task{TResult}"/> resulting in the new <see cref="FileLoggingConfiguration"/></returns>
        async Task <FileLoggingConfiguration> ConfigureLogging(CancellationToken cancellationToken)
        {
            var fileLoggingConfiguration = new FileLoggingConfiguration();
            await console.WriteAsync(null, true, cancellationToken).ConfigureAwait(false);

            fileLoggingConfiguration.Disable = !await PromptYesNo("Enable file logging? (y/n): ", cancellationToken).ConfigureAwait(false);

            if (!fileLoggingConfiguration.Disable)
            {
                do
                {
                    await console.WriteAsync("Log file directory path (leave blank for default): ", false, cancellationToken).ConfigureAwait(false);

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

                    if (String.IsNullOrWhiteSpace(fileLoggingConfiguration.Directory))
                    {
                        fileLoggingConfiguration.Directory = null;
                        break;
                    }
                    //test a write of it
                    await console.WriteAsync(null, true, cancellationToken).ConfigureAwait(false);

                    await console.WriteAsync("Testing directory access...", true, cancellationToken).ConfigureAwait(false);

                    try
                    {
                        await ioManager.CreateDirectory(fileLoggingConfiguration.Directory, cancellationToken).ConfigureAwait(false);

                        var testFile = ioManager.ConcatPath(fileLoggingConfiguration.Directory, String.Format(CultureInfo.InvariantCulture, "WizardAccesTest.{0}.deleteme", Guid.NewGuid()));
                        await ioManager.WriteAllBytes(testFile, Array.Empty <byte>(), cancellationToken).ConfigureAwait(false);

                        try
                        {
                            await ioManager.DeleteFile(testFile, cancellationToken).ConfigureAwait(false);
                        }
                        catch (OperationCanceledException)
                        {
                            throw;
                        }
                        catch (Exception e)
                        {
                            await console.WriteAsync(String.Format(CultureInfo.InvariantCulture, "Error deleting test log file: {0}", testFile), true, cancellationToken).ConfigureAwait(false);

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

                            await console.WriteAsync(null, true, cancellationToken).ConfigureAwait(false);
                        }
                        break;
                    }
                    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("Please verify the path is valid and you have access to it!", true, cancellationToken).ConfigureAwait(false);
                    }
                } while (true);

                async Task <LogLevel?> PromptLogLevel(string question)
                {
                    do
                    {
                        await console.WriteAsync(null, true, cancellationToken).ConfigureAwait(false);

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

                        await console.WriteAsync(String.Format(CultureInfo.InvariantCulture, "Enter one of {0}/{1}/{2}/{3}/{4}/{5} (leave blank for default): ", nameof(LogLevel.Trace), nameof(LogLevel.Debug), nameof(LogLevel.Information), nameof(LogLevel.Warning), nameof(LogLevel.Error), nameof(LogLevel.Critical)), false, cancellationToken).ConfigureAwait(false);

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

                        if (String.IsNullOrWhiteSpace(responseString))
                        {
                            return(null);
                        }
                        if (Enum.TryParse <LogLevel>(responseString, out var logLevel) && logLevel != LogLevel.None)
                        {
                            return(logLevel);
                        }
                        await console.WriteAsync("Invalid log level!", true, cancellationToken).ConfigureAwait(false);
                    } while (true);
                }

                fileLoggingConfiguration.LogLevel = await PromptLogLevel(String.Format(CultureInfo.InvariantCulture, "Enter the level limit for normal logs (default {0}).", fileLoggingConfiguration.LogLevel)).ConfigureAwait(false) ?? fileLoggingConfiguration.LogLevel;

                fileLoggingConfiguration.MicrosoftLogLevel = await PromptLogLevel(String.Format(CultureInfo.InvariantCulture, "Enter the level limit for Microsoft logs (VERY verbose, default {0}).", fileLoggingConfiguration.MicrosoftLogLevel)).ConfigureAwait(false) ?? fileLoggingConfiguration.MicrosoftLogLevel;
            }
            return(fileLoggingConfiguration);
        }
Пример #2
0
        /// <inheritdoc />
        public async Task <RenderResult> RenderMap(string mapPath, MapRegion region, string outputDirectory, string postfix, CancellationToken cancellationToken)
        {
            if (mapPath == null)
            {
                throw new ArgumentNullException(nameof(mapPath));
            }
            if (outputDirectory == null)
            {
                throw new ArgumentNullException(nameof(outputDirectory));
            }
            if (postfix == null)
            {
                throw new ArgumentNullException(nameof(postfix));
            }

            var mapName = ioManager.GetFileNameWithoutExtension(mapPath);
            var outFile = ioManager.ConcatPath(outputDirectory, String.Format(CultureInfo.InvariantCulture, "{0}.{1}png", mapName, postfix != null ? String.Concat(postfix, '.') : null));
            var args    = String.Format(CultureInfo.InvariantCulture, GenerateRenderCommandLine(region), mapPath);

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

            var        output      = new StringBuilder();
            var        errorOutput = new StringBuilder();
            Task <int> processTask;

            using (var P = CreateDMMToolsProcess(output, errorOutput))
            {
                P.StartInfo.Arguments = args;

                processTask = StartAndWaitForProcessExit(P, cancellationToken);

                await processTask.ConfigureAwait(false);
            }
            var toolOutput = String.Format(CultureInfo.InvariantCulture, "Exit Code: {0}{1}StdOut:{1}{2}{1}StdErr:{1}{3}", processTask.Result, Environment.NewLine, output, errorOutput);

            bool   expectNext = false;
            string result     = null;

            foreach (var I in output.ToString().Split(' '))
            {
                var text = I.Trim();
                if (text == "saving")
                {
                    expectNext = true;
                }
                else if (expectNext && text.EndsWith(".png", StringComparison.InvariantCultureIgnoreCase))
                {
                    result = text;
                    break;
                }
                else
                {
                    expectNext = false;
                }
            }
            var rresult = new RenderResult
            {
                MapRegion   = region,
                CommandLine = args,
                ToolOutput  = toolOutput,
                InputPath   = mapPath
            };

            if (result != null)
            {
                await ioManager.MoveFile(result, outFile, cancellationToken).ConfigureAwait(false);

                rresult.OutputPath = ioManager.ResolvePath(outFile);
            }

            return(rresult);
        }
Пример #3
0
        /// <inheritdoc />
        public async Task <Models.CompileJob> Compile(Models.RevisionInformation revisionInformation, DreamMakerSettings dreamMakerSettings, DreamDaemonSecurity securityLevel, uint apiValidateTimeout, IRepository repository, 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 (securityLevel == DreamDaemonSecurity.Ultrasafe)
            {
                throw new ArgumentOutOfRangeException(nameof(securityLevel), securityLevel, "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 (Status != CompilerStatus.Idle)
                {
                    throw new JobException("There is already a compile in progress!");
                }

                Status = CompilerStatus.Copying;
            }

            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...");
                        Status = CompilerStatus.Cleanup;
                        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);

                        Status = CompilerStatus.PreCompile;

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

                        Status = CompilerStatus.Modifying;

                        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);
                        }

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

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

                        Status = CompilerStatus.Compiling;

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

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

                        var apiValidated = false;
                        if (exitCode == 0)
                        {
                            Status = CompilerStatus.Verifying;

                            apiValidated = await VerifyApi(apiValidateTimeout, securityLevel, job, byondLock, dreamMakerSettings.ApiValidationPort.Value, cancellationToken).ConfigureAwait(false);
                        }

                        if (!apiValidated)
                        {
                            //server never validated or compile failed
                            await eventConsumer.HandleEvent(EventType.CompileFailure, new List <string> {
                                resolvedGameDirectory, exitCode == 0 ? "1" : "0"
                            }, cancellationToken).ConfigureAwait(false);

                            throw new JobException(exitCode == 0 ? "Validation of the TGS api failed!" : String.Format(CultureInfo.InvariantCulture, "DM exited with a non-zero code: {0}{1}{2}", exitCode, Environment.NewLine, job.Output));
                        }

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

                        logger.LogTrace("Duplicating compiled game...");
                        Status = CompilerStatus.Duplicating;

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

                        logger.LogTrace("Applying static game file symlinks...");
                        Status = CompilerStatus.Symlinking;

                        //symlink in the static data
                        var symATask = configuration.SymlinkStaticFilesTo(fullDirA, cancellationToken);
                        var symBTask = configuration.SymlinkStaticFilesTo(ioManager.ResolvePath(dirB), cancellationToken);

                        await Task.WhenAll(symATask, symBTask).ConfigureAwait(false);

                        await chat.SendUpdateMessage("Deployment complete! Changes will be applied on next server reboot.", 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
            {
                Status = CompilerStatus.Idle;
            }
        }
Пример #4
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);
Пример #5
0
                #pragma warning restore CA1506

        /// <inheritdoc />
                #pragma warning disable CA1506 // TODO: Decomplexify
        public async Task CleanUnusedCompileJobs(CancellationToken cancellationToken)
        {
            List <long> jobIdsToSkip;

            // don't clean locked directories
            lock (jobLockCounts)
                jobIdsToSkip = jobLockCounts.Select(x => x.Key).ToList();

            List <string> jobUidsToNotErase = null;

            // find the uids of locked directories
            await databaseContextFactory.UseContext(async db =>
            {
                jobUidsToNotErase = (await db
                                     .CompileJobs
                                     .AsQueryable()
                                     .Where(
                                         x => x.Job.Instance.Id == instance.Id &&
                                         jobIdsToSkip.Contains(x.Id))
                                     .Select(x => x.DirectoryName.Value)
                                     .ToListAsync(cancellationToken)
                                     .ConfigureAwait(false))
                                    .Select(x => x.ToString())
                                    .ToList();
            }).ConfigureAwait(false);

            jobUidsToNotErase.Add(SwappableDmbProvider.LiveGameDirectory);

            logger.LogTrace("We will not clean the following directories: {0}", String.Join(", ", jobUidsToNotErase));

            // cleanup
            var gameDirectory = ioManager.ResolvePath();
            await ioManager.CreateDirectory(gameDirectory, cancellationToken).ConfigureAwait(false);

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

            int deleting = 0;
            var tasks    = directories.Select(async x =>
            {
                var nameOnly = ioManager.GetFileName(x);
                if (jobUidsToNotErase.Contains(nameOnly))
                {
                    return;
                }
                logger.LogDebug("Cleaning unused game folder: {0}...", nameOnly);
                try
                {
                    ++deleting;
                    await ioManager.DeleteDirectory(x, cancellationToken).ConfigureAwait(false);
                }
                catch (OperationCanceledException)
                {
                    throw;
                }
                catch (Exception e)
                {
                    logger.LogWarning(e, "Error deleting directory {0}!", x);
                }
            }).ToList();

            if (deleting > 0)
            {
                await Task.WhenAll(tasks).ConfigureAwait(false);
            }
        }
Пример #6
0
        /// <summary>
        /// Installs a BYOND <paramref name="version"/> if it isn't already
        /// </summary>
        /// <param name="version">The BYOND <see cref="Version"/> to install</param>
        /// <param name="customVersionStream">Custom zip file <see cref="Stream"/> to use. Will cause a <see cref="Version.Build"/> number to be added.</param>
        /// <param name="cancellationToken">The <see cref="CancellationToken"/> for the operation</param>
        /// <returns>A <see cref="Task"/> representing the running operation</returns>
        async Task <string> InstallVersion(Version version, Stream customVersionStream, CancellationToken cancellationToken)
        {
            var    ourTcs = new TaskCompletionSource <object>();
            Task   inProgressTask;
            string versionKey;
            bool   installed;

            lock (installedVersions)
            {
                if (customVersionStream != null)
                {
                    int customInstallationNumber = 1;
                    do
                    {
                        versionKey = $"{VersionKey(version, false)}.{customInstallationNumber++}";
                    }while (installedVersions.ContainsKey(versionKey));
                }
                else
                {
                    versionKey = VersionKey(version, true);
                }

                installed = installedVersions.TryGetValue(versionKey, out inProgressTask);
                if (!installed)
                {
                    installedVersions.Add(versionKey, ourTcs.Task);
                }
            }

            if (installed)
            {
                using (cancellationToken.Register(() => ourTcs.SetCanceled()))
                {
                    await Task.WhenAny(ourTcs.Task, inProgressTask).ConfigureAwait(false);

                    cancellationToken.ThrowIfCancellationRequested();
                    return(versionKey);
                }
            }

            if (customVersionStream != null)
            {
                logger.LogInformation("Installing custom BYOND version as {0}...", versionKey);
            }
            else if (version.Build > 0)
            {
                throw new JobException(ErrorCode.ByondNonExistentCustomVersion);
            }
            else
            {
                logger.LogDebug("Requested BYOND version {0} not currently installed. Doing so now...");
            }

            // okay up to us to install it then
            try
            {
                await eventConsumer.HandleEvent(EventType.ByondInstallStart, new List <string> {
                    versionKey
                }, cancellationToken).ConfigureAwait(false);

                var extractPath = ioManager.ResolvePath(versionKey);
                async Task DirectoryCleanup()
                {
                    await ioManager.DeleteDirectory(extractPath, cancellationToken).ConfigureAwait(false);

                    await ioManager.CreateDirectory(extractPath, cancellationToken).ConfigureAwait(false);
                }

                var directoryCleanupTask = DirectoryCleanup();
                try
                {
                    Stream versionZipStream;
                    Stream downloadedStream = null;
                    if (customVersionStream == null)
                    {
                        var bytes = await byondInstaller.DownloadVersion(version, cancellationToken).ConfigureAwait(false);

                        downloadedStream = new MemoryStream(bytes);
                        versionZipStream = downloadedStream;
                    }
                    else
                    {
                        versionZipStream = customVersionStream;
                    }

                    using (downloadedStream)
                    {
                        await directoryCleanupTask.ConfigureAwait(false);

                        logger.LogTrace("Extracting downloaded BYOND zip to {0}...", extractPath);
                        await ioManager.ZipToDirectory(extractPath, versionZipStream, cancellationToken).ConfigureAwait(false);
                    }

                    await byondInstaller.InstallByond(extractPath, version, cancellationToken).ConfigureAwait(false);

                    // make sure to do this last because this is what tells us we have a valid version in the future
                    await ioManager.WriteAllBytes(
                        ioManager.ConcatPath(versionKey, VersionFileName),
                        Encoding.UTF8.GetBytes(versionKey),
                        cancellationToken)
                    .ConfigureAwait(false);
                }
                catch (WebException e)
                {
                    // since the user can easily provide non-exitent version numbers, we'll turn this into a JobException
                    throw new JobException(ErrorCode.ByondDownloadFail, e);
                }
                catch (OperationCanceledException)
                {
                    throw;
                }
                catch
                {
                    await ioManager.DeleteDirectory(versionKey, cancellationToken).ConfigureAwait(false);

                    throw;
                }

                ourTcs.SetResult(null);
            }
            catch (Exception e)
            {
                if (!(e is OperationCanceledException))
                {
                    await eventConsumer.HandleEvent(EventType.ByondInstallFail, new List <string> {
                        e.Message
                    }, cancellationToken).ConfigureAwait(false);
                }
                lock (installedVersions)
                    installedVersions.Remove(versionKey);
                ourTcs.SetException(e);
                throw;
            }

            return(versionKey);
        }
Пример #7
0
        /// <summary>
        /// Installs a BYOND <paramref name="version"/> if it isn't already
        /// </summary>
        /// <param name="version">The BYOND <see cref="Version"/> to install</param>
        /// <param name="cancellationToken">The <see cref="CancellationToken"/> for the operation</param>
        /// <returns>A <see cref="Task"/> representing the running operation</returns>
        async Task InstallVersion(Version version, CancellationToken cancellationToken)
        {
            var  ourTcs = new TaskCompletionSource <object>();
            Task inProgressTask;

            var  versionKey = VersionKey(version);
            bool installed;

            lock (installedVersions)
            {
                installed = installedVersions.TryGetValue(versionKey, out inProgressTask);
                if (!installed)
                {
                    installedVersions.Add(versionKey, ourTcs.Task);
                }
            }

            if (installed)
            {
                using (cancellationToken.Register(() => ourTcs.SetCanceled()))
                {
                    await Task.WhenAny(ourTcs.Task, inProgressTask).ConfigureAwait(false);

                    cancellationToken.ThrowIfCancellationRequested();
                    return;
                }
            }

            // okay up to us to install it then
            try
            {
                await eventConsumer.HandleEvent(EventType.ByondInstallStart, new List <string> {
                    versionKey
                }, cancellationToken).ConfigureAwait(false);

                var downloadTask = byondInstaller.DownloadVersion(version, cancellationToken);

                await ioManager.DeleteDirectory(versionKey, cancellationToken).ConfigureAwait(false);

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

                try
                {
                    var download = await downloadTask.ConfigureAwait(false);

                    await ioManager.ZipToDirectory(versionKey, download, cancellationToken).ConfigureAwait(false);

                    await byondInstaller.InstallByond(ioManager.ResolvePath(versionKey), version, cancellationToken).ConfigureAwait(false);

                    // make sure to do this last because this is what tells us we have a valid version in the future
                    await ioManager.WriteAllBytes(ioManager.ConcatPath(versionKey, VersionFileName), Encoding.UTF8.GetBytes(version.ToString()), cancellationToken).ConfigureAwait(false);
                }
                catch (WebException e)
                {
                    // since the user can easily provide non-exitent version numbers, we'll turn this into a JobException
                    throw new JobException(String.Format(CultureInfo.InvariantCulture, "Error downloading BYOND version: {0}", e.Message));
                }
                catch (OperationCanceledException)
                {
                    throw;
                }
                catch
                {
                    await ioManager.DeleteDirectory(versionKey, cancellationToken).ConfigureAwait(false);

                    throw;
                }

                ourTcs.SetResult(null);
            }
            catch (Exception e)
            {
                if (!(e is OperationCanceledException))
                {
                    await eventConsumer.HandleEvent(EventType.ByondInstallFail, new List <string> {
                        e.Message
                    }, cancellationToken).ConfigureAwait(false);
                }
                lock (installedVersions)
                    installedVersions.Remove(versionKey);
                ourTcs.SetException(e);
                throw;
            }
        }
Пример #8
0
        /// <summary>
        /// Installs a BYOND <paramref name="version"/> if it isn't already
        /// </summary>
        /// <param name="version">The BYOND <see cref="Version"/> to install</param>
        /// <param name="cancellationToken">The <see cref="CancellationToken"/> for the operation</param>
        /// <returns>A <see cref="Task"/> representing the running operation</returns>
        async Task InstallVersion(Version version, CancellationToken cancellationToken)
        {
            var  ourTcs = new TaskCompletionSource <object>();
            Task inProgressTask;

            var  versionKey = VersionKey(version);
            bool installed;

            lock (installedVersions)
            {
                installed = installedVersions.TryGetValue(versionKey, out inProgressTask);
                if (!installed)
                {
                    installedVersions.Add(versionKey, ourTcs.Task);
                }
            }
            if (installed)
            {
                using (cancellationToken.Register(() => ourTcs.SetCanceled()))
                {
                    await Task.WhenAny(ourTcs.Task, inProgressTask).ConfigureAwait(false);

                    cancellationToken.ThrowIfCancellationRequested();
                    return;
                }
            }
            try
            {
                var downloadTask = byondInstaller.DownloadVersion(version, cancellationToken);

                //okay up to us to install it then
                await ioManager.DeleteDirectory(versionKey, cancellationToken).ConfigureAwait(false);

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

                try
                {
                    //byond can just decide to corrupt the zip fnr
                    //(or maybe our downloader is a s***e)
                    //either way try a few times
                    for (var I = 0; I < 3; ++I)
                    {
                        var download = await downloadTask.ConfigureAwait(false);

                        try
                        {
                            await ioManager.ZipToDirectory(versionKey, download, cancellationToken).ConfigureAwait(false);

                            break;
                        }
                        catch (OperationCanceledException)
                        {
                            throw;
                        }
                        catch
                        {
                            if (I == 2)
                            {
                                throw;
                            }
                            downloadTask = byondInstaller.DownloadVersion(version, cancellationToken);
                        }
                    }
                    await byondInstaller.InstallByond(ioManager.ResolvePath(versionKey), version, cancellationToken).ConfigureAwait(false);

                    //make sure to do this last because this is what tells us we have a valid version in the future
                    await ioManager.WriteAllBytes(ioManager.ConcatPath(versionKey, VersionFileName), Encoding.UTF8.GetBytes(version.ToString()), cancellationToken).ConfigureAwait(false);
                }
                catch (OperationCanceledException)
                {
                    throw;
                }
                catch
                {
                    await ioManager.DeleteDirectory(versionKey, cancellationToken).ConfigureAwait(false);

                    throw;
                }

                ourTcs.SetResult(null);
            }
            catch (Exception e)
            {
                lock (installedVersions)
                    installedVersions.Remove(versionKey);
                ourTcs.SetException(e);
                throw;
            }
        }
Пример #9
0
        public override 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  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
                },
                //give this user full privileges on the instance
                InstanceUsers = new List <Models.InstanceUser>
                {
                    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);
Пример #10
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, true);
                    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);
                }
            }
        }
Пример #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);