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