/// <inheritdoc /> public async Task <ServerSideModifications> CopyDMFilesTo(string dmeFile, string destination, CancellationToken cancellationToken) { using (await SemaphoreSlimContext.Lock(semaphore, cancellationToken).ConfigureAwait(false)) { await EnsureDirectories(cancellationToken).ConfigureAwait(false); //just assume no other fs race conditions here var dmeExistsTask = ioManager.FileExists(ioManager.ConcatPath(CodeModificationsSubdirectory, dmeFile), cancellationToken); var headFileExistsTask = ioManager.FileExists(ioManager.ConcatPath(CodeModificationsSubdirectory, CodeModificationsHeadFile), cancellationToken); var tailFileExistsTask = ioManager.FileExists(ioManager.ConcatPath(CodeModificationsSubdirectory, CodeModificationsTailFile), cancellationToken); var copyTask = ioManager.CopyDirectory(CodeModificationsSubdirectory, destination, null, cancellationToken); await Task.WhenAll(dmeExistsTask, headFileExistsTask, tailFileExistsTask, copyTask).ConfigureAwait(false); if (!dmeExistsTask.Result && !headFileExistsTask.Result && !tailFileExistsTask.Result) { return(null); } if (dmeExistsTask.Result) { return(new ServerSideModifications(null, null, true)); } if (!headFileExistsTask.Result && !tailFileExistsTask.Result) { return(null); } string IncludeLine(string filePath) => String.Format(CultureInfo.InvariantCulture, "#include \"{0}\"", filePath); return(new ServerSideModifications(headFileExistsTask.Result ? IncludeLine(CodeModificationsHeadFile) : null, tailFileExistsTask.Result ? IncludeLine(CodeModificationsTailFile) : null, false)); } }
/// <inheritdoc /> public async Task <IDmbProvider> FromCompileJob(CompileJob compileJob, CancellationToken cancellationToken) { logger.LogTrace("Loading compile job {0}...", compileJob.Id); var providerSubmitted = false; var newProvider = new DmbProvider(compileJob, ioManager, () => { if (providerSubmitted) { CleanJob(compileJob); } }); try { var primaryCheckTask = ioManager.FileExists(ioManager.ConcatPath(newProvider.PrimaryDirectory, newProvider.DmbName), cancellationToken); var secondaryCheckTask = ioManager.FileExists(ioManager.ConcatPath(newProvider.PrimaryDirectory, newProvider.DmbName), cancellationToken); if (!(await primaryCheckTask.ConfigureAwait(false) && await secondaryCheckTask.ConfigureAwait(false))) { logger.LogWarning("Error loading compile job, .dmb missing!"); return(null); //omae wa mou shinderu } lock (this) { if (!jobLockCounts.TryGetValue(compileJob.Id, out int value)) { value = 1; jobLockCounts.Add(compileJob.Id, 1); } else { jobLockCounts[compileJob.Id] = ++value; } logger.LogTrace("Compile job {0} lock count now: {1}", compileJob.Id, value); providerSubmitted = true; return(newProvider); } } finally { if (!providerSubmitted) { newProvider.Dispose(); } } }
/// <summary> /// Ensures standard configuration directories exist /// </summary> /// <param name="cancellationToken">The <see cref="CancellationToken"/> for the operation</param> /// <returns>A <see cref="Task"/> representing the running operation</returns> async Task EnsureDirectories(CancellationToken cancellationToken) { async Task ValidateStaticFolder() { await ioManager.CreateDirectory(GameStaticFilesSubdirectory, cancellationToken).ConfigureAwait(false); var staticIgnorePath = StaticIgnorePath(); if (!await ioManager.FileExists(staticIgnorePath, cancellationToken).ConfigureAwait(false)) { await ioManager.WriteAllBytes(staticIgnorePath, Array.Empty <byte>(), cancellationToken).ConfigureAwait(false); } } await Task.WhenAll(ioManager.CreateDirectory(CodeModificationsSubdirectory, cancellationToken), ioManager.CreateDirectory(EventScriptsSubdirectory, cancellationToken), ValidateStaticFolder()).ConfigureAwait(false); }
/// <inheritdoc /> public async Task TrustDmbPath(string fullDmbPath, CancellationToken cancellationToken) { if (fullDmbPath == null) { throw new ArgumentNullException(nameof(fullDmbPath)); } using (await SemaphoreSlimContext.Lock(trustedFileSemaphore, cancellationToken).ConfigureAwait(false)) { string trustedFileText; if (await ioManager.FileExists(trustedFilePath, cancellationToken).ConfigureAwait(false)) { var trustedFileBytes = await ioManager.ReadAllBytes(trustedFilePath, cancellationToken).ConfigureAwait(false); trustedFileText = Encoding.UTF8.GetString(trustedFileBytes); trustedFileText = $"{trustedFileText.Trim()}{Environment.NewLine}"; } else { trustedFileText = String.Empty; } if (trustedFileText.Contains(fullDmbPath, StringComparison.Ordinal)) { return; } trustedFileText = $"{trustedFileText}{fullDmbPath}{Environment.NewLine}"; var newTrustedFileBytes = Encoding.UTF8.GetBytes(trustedFileText); await ioManager.WriteAllBytes(trustedFilePath, newTrustedFileBytes, cancellationToken).ConfigureAwait(false); } }
public async Task <IActionResult> Create([FromBody] Api.Models.Instance model, CancellationToken cancellationToken) { if (model == null) { throw new ArgumentNullException(nameof(model)); } if (String.IsNullOrWhiteSpace(model.Name)) { return(BadRequest(new ErrorMessage(ErrorCode.InstanceWhitespaceName))); } var unNormalizedPath = model.Path; var targetInstancePath = NormalizePath(unNormalizedPath); model.Path = targetInstancePath; var installationDirectoryPath = NormalizePath(DefaultIOManager.CurrentDirectory); bool InstanceIsChildOf(string otherPath) { if (!targetInstancePath.StartsWith(otherPath, StringComparison.Ordinal)) { return(false); } bool sameLength = targetInstancePath.Length == otherPath.Length; char dirSeparatorChar = targetInstancePath.ToCharArray()[Math.Min(otherPath.Length, targetInstancePath.Length - 1)]; return(sameLength || dirSeparatorChar == Path.DirectorySeparatorChar || dirSeparatorChar == Path.AltDirectorySeparatorChar); } if (InstanceIsChildOf(installationDirectoryPath)) { return(Conflict(new ErrorMessage(ErrorCode.InstanceAtConflictingPath))); } // Validate it's not a child of any other instance IActionResult earlyOut = null; ulong countOfOtherInstances = 0; using (var cts = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken)) { var newCancellationToken = cts.Token; try { await DatabaseContext .Instances .AsQueryable() .Select(x => new Models.Instance { Path = x.Path }) .ForEachAsync( otherInstance => { if (++countOfOtherInstances >= generalConfiguration.InstanceLimit) { earlyOut ??= Conflict(new ErrorMessage(ErrorCode.InstanceLimitReached)); } else if (InstanceIsChildOf(otherInstance.Path)) { earlyOut ??= Conflict(new ErrorMessage(ErrorCode.InstanceAtConflictingPath)); } if (earlyOut != null && !newCancellationToken.IsCancellationRequested) { cts.Cancel(); } }, newCancellationToken) .ConfigureAwait(false); } catch (OperationCanceledException) { cancellationToken.ThrowIfCancellationRequested(); } } if (earlyOut != null) { return(earlyOut); } // Last test, ensure it's in the list of valid paths if (!(generalConfiguration.ValidInstancePaths? .Select(path => NormalizePath(path)) .Any(path => InstanceIsChildOf(path)) ?? true)) { return(BadRequest(new ErrorMessage(ErrorCode.InstanceNotAtWhitelistedPath))); } async Task <bool> DirExistsAndIsNotEmpty() { if (!await ioManager.DirectoryExists(model.Path, cancellationToken).ConfigureAwait(false)) { return(false); } var filesTask = ioManager.GetFiles(model.Path, cancellationToken); var dirsTask = ioManager.GetDirectories(model.Path, cancellationToken); var files = await filesTask.ConfigureAwait(false); var dirs = await dirsTask.ConfigureAwait(false); return(files.Concat(dirs).Any()); } var dirExistsTask = DirExistsAndIsNotEmpty(); bool attached = false; if (await ioManager.FileExists(model.Path, cancellationToken).ConfigureAwait(false) || await dirExistsTask.ConfigureAwait(false)) { if (!await ioManager.FileExists(ioManager.ConcatPath(model.Path, InstanceAttachFileName), cancellationToken).ConfigureAwait(false)) { return(Conflict(new ErrorMessage(ErrorCode.InstanceAtExistingPath))); } else { attached = true; } } var newInstance = CreateDefaultInstance(model); DatabaseContext.Instances.Add(newInstance); try { await DatabaseContext.Save(cancellationToken).ConfigureAwait(false); try { // actually reserve it now await ioManager.CreateDirectory(unNormalizedPath, cancellationToken).ConfigureAwait(false); await ioManager.DeleteFile(ioManager.ConcatPath(targetInstancePath, InstanceAttachFileName), cancellationToken).ConfigureAwait(false); } catch { // oh shit delete the model DatabaseContext.Instances.Remove(newInstance); // DCT: Operation must always run await DatabaseContext.Save(default).ConfigureAwait(false);
/// <inheritdoc /> public async Task <Models.CompileJob> Compile(Models.RevisionInformation revisionInformation, Api.Models.DreamMaker dreamMakerSettings, uint apiValidateTimeout, IRepository repository, Action <int> progressReporter, TimeSpan?estimatedDuration, CancellationToken cancellationToken) { if (revisionInformation == null) { throw new ArgumentNullException(nameof(revisionInformation)); } if (dreamMakerSettings == null) { throw new ArgumentNullException(nameof(dreamMakerSettings)); } if (repository == null) { throw new ArgumentNullException(nameof(repository)); } if (progressReporter == null) { throw new ArgumentNullException(nameof(progressReporter)); } if (dreamMakerSettings.ApiValidationSecurityLevel == DreamDaemonSecurity.Ultrasafe) { throw new ArgumentOutOfRangeException(nameof(dreamMakerSettings), dreamMakerSettings, "Cannot compile with ultrasafe security!"); } logger.LogTrace("Begin Compile"); var job = new Models.CompileJob { DirectoryName = Guid.NewGuid(), DmeName = dreamMakerSettings.ProjectName, RevisionInformation = revisionInformation }; logger.LogTrace("Compile output GUID: {0}", job.DirectoryName); lock (this) { if (compiling) { throw new JobException("There is already a compile job in progress!"); } compiling = true; } using (var progressCts = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken)) { async Task ProgressTask() { if (!estimatedDuration.HasValue) { return; } progressReporter(0); var ct = progressCts.Token; var sleepInterval = estimatedDuration.Value / 100; try { for (var I = 0; I < 99; ++I) { await Task.Delay(sleepInterval, progressCts.Token).ConfigureAwait(false); progressReporter(I + 1); } } catch (OperationCanceledException) { } } var progressTask = ProgressTask(); try { var commitInsert = revisionInformation.CommitSha.Substring(0, 7); string remoteCommitInsert; if (revisionInformation.CommitSha == revisionInformation.OriginCommitSha) { commitInsert = String.Format(CultureInfo.InvariantCulture, "^{0}", commitInsert); remoteCommitInsert = String.Empty; } else { remoteCommitInsert = String.Format(CultureInfo.InvariantCulture, ". Remote commit: ^{0}", revisionInformation.OriginCommitSha.Substring(0, 7)); } var testmergeInsert = revisionInformation.ActiveTestMerges.Count == 0 ? String.Empty : String.Format(CultureInfo.InvariantCulture, " (Test Merges: {0})", String.Join(", ", revisionInformation.ActiveTestMerges.Select(x => x.TestMerge).Select(x => { var result = String.Format(CultureInfo.InvariantCulture, "#{0} at {1}", x.Number, x.PullRequestRevision.Substring(0, 7)); if (x.Comment != null) { result += String.Format(CultureInfo.InvariantCulture, " ({0})", x.Comment); } return(result); }))); using (var byondLock = await byond.UseExecutables(null, cancellationToken).ConfigureAwait(false)) { await chat.SendUpdateMessage(String.Format(CultureInfo.InvariantCulture, "Deploying revision: {0}{1}{2} BYOND Version: {3}", commitInsert, testmergeInsert, remoteCommitInsert, byondLock.Version), cancellationToken).ConfigureAwait(false); async Task CleanupFailedCompile(bool cancelled) { logger.LogTrace("Cleaning compile directory..."); var chatTask = chat.SendUpdateMessage(cancelled ? "Deploy cancelled!" : "Deploy failed!", cancellationToken); try { await ioManager.DeleteDirectory(job.DirectoryName.ToString(), CancellationToken.None).ConfigureAwait(false); } catch (Exception e) { logger.LogWarning("Error cleaning up compile directory {0}! Exception: {1}", ioManager.ResolvePath(job.DirectoryName.ToString()), e); } await chatTask.ConfigureAwait(false); }; try { await ioManager.CreateDirectory(job.DirectoryName.ToString(), cancellationToken).ConfigureAwait(false); var dirA = ioManager.ConcatPath(job.DirectoryName.ToString(), ADirectoryName); var dirB = ioManager.ConcatPath(job.DirectoryName.ToString(), BDirectoryName); logger.LogTrace("Copying repository to game directory..."); //copy the repository var fullDirA = ioManager.ResolvePath(dirA); var repoOrigin = repository.Origin; using (repository) await repository.CopyTo(fullDirA, cancellationToken).ConfigureAwait(false); //run precompile scripts var resolvedGameDirectory = ioManager.ResolvePath(ioManager.ConcatPath(job.DirectoryName.ToString(), ADirectoryName)); await eventConsumer.HandleEvent(EventType.CompileStart, new List <string> { resolvedGameDirectory, repoOrigin }, cancellationToken).ConfigureAwait(false); //determine the dme if (job.DmeName == null) { logger.LogTrace("Searching for available .dmes..."); var path = (await ioManager.GetFilesWithExtension(dirA, DmeExtension, cancellationToken).ConfigureAwait(false)).FirstOrDefault(); if (path == default) { throw new JobException("Unable to find any .dme!"); } var dmeWithExtension = ioManager.GetFileName(path); job.DmeName = dmeWithExtension.Substring(0, dmeWithExtension.Length - DmeExtension.Length - 1); } else if (!await ioManager.FileExists(ioManager.ConcatPath(dirA, String.Join('.', job.DmeName, DmeExtension)), cancellationToken).ConfigureAwait(false)) { throw new JobException("Unable to locate specified .dme!"); } logger.LogDebug("Selected {0}.dme for compilation!", job.DmeName); await ModifyDme(job, cancellationToken).ConfigureAwait(false); //run compiler, verify api job.ByondVersion = byondLock.Version.ToString(); var exitCode = await RunDreamMaker(byondLock.DreamMakerPath, job, cancellationToken).ConfigureAwait(false); try { if (exitCode != 0) { throw new JobException(String.Format(CultureInfo.InvariantCulture, "DM exited with a non-zero code: {0}{1}{2}", exitCode, Environment.NewLine, job.Output)); } await VerifyApi(apiValidateTimeout, dreamMakerSettings.ApiValidationSecurityLevel.Value, job, byondLock, dreamMakerSettings.ApiValidationPort.Value, cancellationToken).ConfigureAwait(false); } catch (JobException) { //server never validated or compile failed await eventConsumer.HandleEvent(EventType.CompileFailure, new List <string> { resolvedGameDirectory, exitCode == 0 ? "1" : "0" }, cancellationToken).ConfigureAwait(false); throw; } logger.LogTrace("Running post compile event..."); await eventConsumer.HandleEvent(EventType.CompileComplete, new List <string> { ioManager.ResolvePath(ioManager.ConcatPath(job.DirectoryName.ToString(), ADirectoryName)) }, cancellationToken).ConfigureAwait(false); logger.LogTrace("Duplicating compiled game..."); //duplicate the dmb et al await ioManager.CopyDirectory(dirA, dirB, null, cancellationToken).ConfigureAwait(false); logger.LogTrace("Applying static game file symlinks..."); //symlink in the static data var symATask = configuration.SymlinkStaticFilesTo(fullDirA, cancellationToken); var symBTask = configuration.SymlinkStaticFilesTo(ioManager.ResolvePath(dirB), cancellationToken); await Task.WhenAll(symATask, symBTask).ConfigureAwait(false); await chat.SendUpdateMessage(String.Format(CultureInfo.InvariantCulture, "Deployment complete!{0}", watchdog.Running ? " Changes will be applied on next server reboot." : String.Empty), cancellationToken).ConfigureAwait(false); logger.LogDebug("Compile complete!"); return(job); } catch (Exception e) { await CleanupFailedCompile(e is OperationCanceledException).ConfigureAwait(false); throw; } } } catch (OperationCanceledException) { await eventConsumer.HandleEvent(EventType.CompileCancelled, null, default).ConfigureAwait(false); throw; } finally { compiling = false; progressCts.Cancel(); await progressTask.ConfigureAwait(false); } } }
/// <inheritdoc /> #pragma warning disable CA1506 // TODO: Decomplexify public async Task <IDmbProvider> FromCompileJob(CompileJob compileJob, CancellationToken cancellationToken) { if (compileJob == null) { throw new ArgumentNullException(nameof(compileJob)); } // ensure we have the entire compile job tree await databaseContextFactory.UseContext(async db => compileJob = await db.CompileJobs.Where(x => x.Id == compileJob.Id) .Include(x => x.Job).ThenInclude(x => x.StartedBy) .Include(x => x.RevisionInformation).ThenInclude(x => x.PrimaryTestMerge).ThenInclude(x => x.MergedBy) .Include(x => x.RevisionInformation).ThenInclude(x => x.ActiveTestMerges).ThenInclude(x => x.TestMerge).ThenInclude(x => x.MergedBy) .FirstAsync(cancellationToken).ConfigureAwait(false)).ConfigureAwait(false); // can't wait to see that query logger.LogTrace("Loading compile job {0}...", compileJob.Id); var providerSubmitted = false; var newProvider = new DmbProvider(compileJob, ioManager, () => { if (providerSubmitted) { CleanJob(compileJob); } }); try { var primaryCheckTask = ioManager.FileExists(ioManager.ConcatPath(newProvider.PrimaryDirectory, newProvider.DmbName), cancellationToken); var secondaryCheckTask = ioManager.FileExists(ioManager.ConcatPath(newProvider.PrimaryDirectory, newProvider.DmbName), cancellationToken); if (!(await primaryCheckTask.ConfigureAwait(false) && await secondaryCheckTask.ConfigureAwait(false))) { logger.LogWarning("Error loading compile job, .dmb missing!"); return(null); // omae wa mou shinderu } lock (this) { if (!jobLockCounts.TryGetValue(compileJob.Id, out int value)) { value = 1; jobLockCounts.Add(compileJob.Id, 1); } else { jobLockCounts[compileJob.Id] = ++value; } logger.LogTrace("Compile job {0} lock count now: {1}", compileJob.Id, value); providerSubmitted = true; return(newProvider); } } finally { if (!providerSubmitted) { newProvider.Dispose(); } } }
public async Task <IActionResult> Create([FromBody] Api.Models.Instance model, CancellationToken cancellationToken) { if (model == null) { throw new ArgumentNullException(nameof(model)); } if (String.IsNullOrWhiteSpace(model.Name)) { return(BadRequest(new ErrorMessage { Message = "name must not be empty!" })); } if (model.Path == null) { return(BadRequest(new ErrorMessage { Message = "path must not be empty!" })); } NormalizeModelPath(model, out var rawPath); var localPath = ioManager.ResolvePath("."); NormalizeModelPath(new Api.Models.Instance { Path = localPath }, out var normalizedLocalPath); if (rawPath.StartsWith(normalizedLocalPath, StringComparison.Ordinal)) { bool sameLength = rawPath.Length == normalizedLocalPath.Length; char dirSeparatorChar = rawPath.ToCharArray()[normalizedLocalPath.Length]; if (sameLength || dirSeparatorChar == Path.DirectorySeparatorChar || dirSeparatorChar == Path.AltDirectorySeparatorChar) { return(Conflict(new ErrorMessage { Message = "Instances cannot be created in the installation directory!" })); } } var dirExistsTask = ioManager.DirectoryExists(model.Path, cancellationToken); bool attached = false; if (await ioManager.FileExists(model.Path, cancellationToken).ConfigureAwait(false) || await dirExistsTask.ConfigureAwait(false)) { if (!await ioManager.FileExists(ioManager.ConcatPath(model.Path, InstanceAttachFileName), cancellationToken).ConfigureAwait(false)) { return(Conflict(new ErrorMessage { Message = "Path not empty!" })); } else { attached = true; } } var newInstance = new Models.Instance { ConfigurationType = model.ConfigurationType ?? ConfigurationType.Disallowed, DreamDaemonSettings = new DreamDaemonSettings { AllowWebClient = false, AutoStart = false, PrimaryPort = 1337, SecondaryPort = 1338, SecurityLevel = DreamDaemonSecurity.Safe, SoftRestart = false, SoftShutdown = false, StartupTimeout = 20 }, DreamMakerSettings = new DreamMakerSettings { ApiValidationPort = 1339, ApiValidationSecurityLevel = DreamDaemonSecurity.Safe }, Name = model.Name, Online = false, Path = model.Path, AutoUpdateInterval = model.AutoUpdateInterval ?? 0, RepositorySettings = new RepositorySettings { CommitterEmail = "*****@*****.**", CommitterName = application.VersionPrefix, PushTestMergeCommits = false, ShowTestMergeCommitters = false, AutoUpdatesKeepTestMerges = false, AutoUpdatesSynchronize = false, PostTestMergeComment = false }, InstanceUsers = new List <Models.InstanceUser> // give this user full privileges on the instance { InstanceAdminUser() } }; DatabaseContext.Instances.Add(newInstance); try { await DatabaseContext.Save(cancellationToken).ConfigureAwait(false); try { // actually reserve it now await ioManager.CreateDirectory(rawPath, cancellationToken).ConfigureAwait(false); await ioManager.DeleteFile(ioManager.ConcatPath(rawPath, InstanceAttachFileName), cancellationToken).ConfigureAwait(false); } catch { // oh shit delete the model DatabaseContext.Instances.Remove(newInstance); await DatabaseContext.Save(default).ConfigureAwait(false);
/// <inheritdoc /> public async Task <bool> CheckRunWizard(CancellationToken cancellationToken) { var setupWizardMode = generalConfiguration.SetupWizardMode; logger.LogTrace("Checking if setup wizard should run. SetupWizardMode: {0}", setupWizardMode); if (setupWizardMode == SetupWizardMode.Never) { logger.LogTrace("Skipping due to configuration..."); return(false); } var forceRun = setupWizardMode == SetupWizardMode.Force || setupWizardMode == SetupWizardMode.Only; if (!console.Available) { if (forceRun) { throw new InvalidOperationException("Asked to run setup wizard with no console avaliable!"); } logger.LogTrace("Skipping due to console not being available..."); return(false); } var userConfigFileName = String.Format(CultureInfo.InvariantCulture, "appsettings.{0}.json", hostingEnvironment.EnvironmentName); var exists = await ioManager.FileExists(userConfigFileName, cancellationToken).ConfigureAwait(false); bool shouldRunBasedOnAutodetect; if (exists) { var bytes = await ioManager.ReadAllBytes(userConfigFileName, cancellationToken).ConfigureAwait(false); var contents = Encoding.UTF8.GetString(bytes); var existingConfigIsEmpty = String.IsNullOrWhiteSpace(contents); logger.LogTrace("Configuration json detected. Empty: {0}", existingConfigIsEmpty); shouldRunBasedOnAutodetect = existingConfigIsEmpty; } else { shouldRunBasedOnAutodetect = true; logger.LogTrace("No configuration json detected"); } if (!shouldRunBasedOnAutodetect) { if (forceRun) { logger.LogTrace("Asking user to bypass due to force run request..."); await console.WriteAsync(String.Format(CultureInfo.InvariantCulture, "The configuration settings are requesting the setup wizard be run, but you already appear to have a configuration file ({0})!", userConfigFileName), true, cancellationToken).ConfigureAwait(false); forceRun = await PromptYesNo("Continue running setup wizard? (y/n): ", cancellationToken).ConfigureAwait(false); } if (!forceRun) { return(false); } } //flush the logs to prevent console conflicts await Task.Delay(TimeSpan.FromSeconds(1), cancellationToken).ConfigureAwait(false); await RunWizard(userConfigFileName, cancellationToken).ConfigureAwait(false); return(true); }
public async Task WriteZipsToCSVFileAsync(string path, IEnumerable <ZipFile> records) { try { if (String.IsNullOrWhiteSpace(path) || String.IsNullOrEmpty(path)) { throw new ArgumentException("Path is empty"); } } catch (ArgumentException ex) { using (EventLog eventLog = new EventLog("Application")) { eventLog.Source = "Application"; eventLog.WriteEntry($@"Cannot find path: {path} . The Path could not be found. /n Stack trace {ex}", EventLogEntryType.FailureAudit, 101, 1); } } try { if (!path.ToLower().EndsWith(".csv")) { throw new ArgumentException("Path is not a CSV"); } } catch (ArgumentException ex) { using (EventLog eventLog = new EventLog("Application")) { eventLog.Source = "Application"; eventLog.WriteEntry($@"File is not a CSV. /n Stack trace {ex}", EventLogEntryType.FailureAudit, 101, 1); } throw ex; } if (!ioManager.FileExists(path)) { ioManager.CreateFile(path); } try { if (records == null) { throw new ArgumentNullException("Records was null"); } if (!(records.Count() >= 1)) { throw new ArgumentException("No records"); } } catch (ArgumentNullException ex) { using (EventLog eventLog = new EventLog("Application")) { eventLog.Source = "Application"; eventLog.WriteEntry($@"No records found. Records was null. /n Stack trace {ex}", EventLogEntryType.FailureAudit, 101, 1); } throw; } catch (ArgumentException ex) { using (EventLog eventLog = new EventLog("Application")) { eventLog.Source = "Application"; eventLog.WriteEntry($@"No records found. /n Stack trace {ex}", EventLogEntryType.FailureAudit, 101, 1); } throw; } await csvRepository.WriteZipsToCSVFileAsync(path, records); }
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);
/// <inheritdoc /> public async Task StartAsync(CancellationToken cancellationToken) { async Task <byte[]> GetActiveVersion() { var activeVersionFileExists = await ioManager.FileExists(ActiveVersionFileName, cancellationToken).ConfigureAwait(false); return(!activeVersionFileExists ? null : await ioManager.ReadAllBytes(ActiveVersionFileName, cancellationToken).ConfigureAwait(false)); } var activeVersionBytesTask = GetActiveVersion(); // Create local cfg directory in case it doesn't exist var localCfgDirectory = ioManager.ConcatPath( byondInstaller.PathToUserByondFolder, CfgDirectoryName); await ioManager.CreateDirectory( localCfgDirectory, cancellationToken).ConfigureAwait(false); // Delete trusted.txt so it doesn't grow too large var trustedFilePath = ioManager.ConcatPath( localCfgDirectory, TrustedDmbFileName); logger.LogTrace("Deleting trusted .dmbs file {0}", trustedFilePath); await ioManager.DeleteFile( trustedFilePath, cancellationToken).ConfigureAwait(false); var byondDirectory = ioManager.ResolvePath(); await ioManager.CreateDirectory(byondDirectory, cancellationToken).ConfigureAwait(false); var directories = await ioManager.GetDirectories(byondDirectory, cancellationToken).ConfigureAwait(false); async Task ReadVersion(string path) { var versionFile = ioManager.ConcatPath(path, VersionFileName); if (!await ioManager.FileExists(versionFile, cancellationToken).ConfigureAwait(false)) { logger.LogInformation("Cleaning unparsable version path: {0}", ioManager.ResolvePath(path)); await ioManager.DeleteDirectory(path, cancellationToken).ConfigureAwait(false); // cleanup return; } var bytes = await ioManager.ReadAllBytes(versionFile, cancellationToken).ConfigureAwait(false); var text = Encoding.UTF8.GetString(bytes); if (Version.TryParse(text, out var version)) { var key = VersionKey(version); lock (installedVersions) if (!installedVersions.ContainsKey(key)) { logger.LogDebug("Adding detected BYOND version {0}...", key); installedVersions.Add(key, Task.CompletedTask); return; } } await ioManager.DeleteDirectory(path, cancellationToken).ConfigureAwait(false); } await Task.WhenAll(directories.Select(x => ReadVersion(x))).ConfigureAwait(false); var activeVersionBytes = await activeVersionBytesTask.ConfigureAwait(false); if (activeVersionBytes != null) { var activeVersionString = Encoding.UTF8.GetString(activeVersionBytes); bool hasRequestedActiveVersion; lock (installedVersions) hasRequestedActiveVersion = installedVersions.ContainsKey(activeVersionString); if (hasRequestedActiveVersion && Version.TryParse(activeVersionString, out var activeVersion)) { ActiveVersion = activeVersion.Semver(); } else { logger.LogWarning("Failed to load saved active version {0}!", activeVersionString); await ioManager.DeleteFile(ActiveVersionFileName, cancellationToken).ConfigureAwait(false); } } }
/// <summary> /// Prompts the user to create a <see cref="DatabaseConfiguration"/> /// </summary> /// <param name="cancellationToken">The <see cref="CancellationToken"/> for the operation</param> /// <returns>A <see cref="Task{TResult}"/> resulting in the new <see cref="DatabaseConfiguration"/></returns> async Task <DatabaseConfiguration> ConfigureDatabase(CancellationToken cancellationToken) { do { await console.WriteAsync(null, true, cancellationToken).ConfigureAwait(false); await console.WriteAsync("What SQL database type will you be using?", true, cancellationToken).ConfigureAwait(false); var databaseConfiguration = new DatabaseConfiguration { DatabaseType = await PromptDatabaseType(cancellationToken).ConfigureAwait(false) }; string serverAddress = null; uint? mySQLServerPort = null; bool isSqliteDB = databaseConfiguration.DatabaseType == DatabaseType.Sqlite; if (!isSqliteDB) { do { await console.WriteAsync(null, true, cancellationToken).ConfigureAwait(false); await console.WriteAsync("Enter the server's address and port [<server>:<port> or <server>] (blank for local): ", false, cancellationToken).ConfigureAwait(false); serverAddress = await console.ReadLineAsync(false, cancellationToken).ConfigureAwait(false); if (!String.IsNullOrWhiteSpace(serverAddress) && databaseConfiguration.DatabaseType == DatabaseType.SqlServer) { var match = Regex.Match(serverAddress, @"^(?<server>.+):(?<port>.+)$"); if (match.Success) { serverAddress = match.Groups["server"].Value; var portString = match.Groups["port"].Value; if (uint.TryParse(portString, out uint port)) { mySQLServerPort = port; } else { await console.WriteAsync($"Failed to parse port \"{portString}\", please try again.", true, cancellationToken).ConfigureAwait(false); continue; } } } break; }while (true); } await console.WriteAsync(null, true, cancellationToken).ConfigureAwait(false); await console.WriteAsync($"Enter the database {(isSqliteDB ? "file path" : "name")} (Can be from previous installation. Otherwise, should not exist): ", false, cancellationToken).ConfigureAwait(false); string databaseName; bool dbExists = false; do { databaseName = await console.ReadLineAsync(false, cancellationToken).ConfigureAwait(false); if (!String.IsNullOrWhiteSpace(databaseName)) { if (isSqliteDB) { dbExists = await ioManager.FileExists(databaseName, cancellationToken).ConfigureAwait(false); if (!dbExists) { databaseName = await ValidateNonExistantSqliteDBName(databaseName, cancellationToken).ConfigureAwait(false); } } else { dbExists = await PromptYesNo("Does this database already exist? (y/n): ", cancellationToken).ConfigureAwait(false); } } if (String.IsNullOrWhiteSpace(databaseName)) { await console.WriteAsync("Invalid database name!", true, cancellationToken).ConfigureAwait(false); } else { break; } }while (true); bool useWinAuth; if (databaseConfiguration.DatabaseType == DatabaseType.SqlServer && platformIdentifier.IsWindows) { useWinAuth = await PromptYesNo("Use Windows Authentication? (y/n): ", cancellationToken).ConfigureAwait(false); } else { useWinAuth = false; } await console.WriteAsync(null, true, cancellationToken).ConfigureAwait(false); string username = null; string password = null; if (!isSqliteDB) { if (!useWinAuth) { await console.WriteAsync("Enter username: "******"Enter password: "******"IMPORTANT: If using the service runner, ensure this computer's LocalSystem account has CREATE DATABASE permissions on the target server!", true, cancellationToken).ConfigureAwait(false); await console.WriteAsync("The account it uses in MSSQL is usually \"NT AUTHORITY\\SYSTEM\" and the role it needs is usually \"dbcreator\".", true, cancellationToken).ConfigureAwait(false); await console.WriteAsync("We'll run a sanity test here, but it won't be indicative of the service's permissions if that is the case", true, cancellationToken).ConfigureAwait(false); } } await console.WriteAsync(null, true, cancellationToken).ConfigureAwait(false); DbConnection testConnection; void CreateTestConnection(string connectionString) => testConnection = dbConnectionFactory.CreateConnection( connectionString, databaseConfiguration.DatabaseType); if (databaseConfiguration.DatabaseType == DatabaseType.SqlServer) { var csb = new SqlConnectionStringBuilder { ApplicationName = application.VersionPrefix, DataSource = serverAddress ?? "(local)" }; if (useWinAuth) { csb.IntegratedSecurity = true; } else { csb.UserID = username; csb.Password = password; } CreateTestConnection(csb.ConnectionString); csb.InitialCatalog = databaseName; databaseConfiguration.ConnectionString = csb.ConnectionString; } else if (databaseConfiguration.DatabaseType == DatabaseType.Sqlite) { var csb = new SqliteConnectionStringBuilder { DataSource = databaseName, Mode = dbExists ? SqliteOpenMode.ReadOnly : SqliteOpenMode.ReadWriteCreate }; CreateTestConnection(csb.ConnectionString); databaseConfiguration.ConnectionString = csb.ConnectionString; } else { // MySQL/MariaDB var csb = new MySqlConnectionStringBuilder { Server = serverAddress ?? "127.0.0.1", UserID = username, Password = password }; if (mySQLServerPort.HasValue) { csb.Port = mySQLServerPort.Value; } CreateTestConnection(csb.ConnectionString); csb.Database = databaseName; databaseConfiguration.ConnectionString = csb.ConnectionString; } try { await TestDatabaseConnection(testConnection, databaseConfiguration, databaseName, dbExists, cancellationToken).ConfigureAwait(false); return(databaseConfiguration); } catch (OperationCanceledException) { throw; } catch (Exception e) { await console.WriteAsync(e.Message, true, cancellationToken).ConfigureAwait(false); await console.WriteAsync(null, true, cancellationToken).ConfigureAwait(false); await console.WriteAsync("Retrying database configuration...", true, cancellationToken).ConfigureAwait(false); } }while (true); }
/// <summary> /// Executes and populate a given <paramref name="job"/> /// </summary> /// <param name="job">The <see cref="CompileJob"/> to run and populate</param> /// <param name="dreamMakerSettings">The <see cref="Api.Models.Internal.DreamMakerSettings"/> to use</param> /// <param name="byondLock">The <see cref="IByondExecutableLock"/> to use</param> /// <param name="repository">The <see cref="IRepository"/> to use</param> /// <param name="remoteDeploymentManager">The <see cref="IRemoteDeploymentManager"/> to use.</param> /// <param name="apiValidateTimeout">The timeout for validating the DMAPI</param> /// <param name="cancellationToken">The <see cref="CancellationToken"/> for the operation</param> /// <returns>A <see cref="Task"/> representing the running operation</returns> async Task RunCompileJob( Models.CompileJob job, Api.Models.Internal.DreamMakerSettings dreamMakerSettings, IByondExecutableLock byondLock, IRepository repository, IRemoteDeploymentManager remoteDeploymentManager, uint apiValidateTimeout, CancellationToken cancellationToken) { var outputDirectory = job.DirectoryName.ToString(); logger.LogTrace("Compile output GUID: {0}", outputDirectory); try { // copy the repository logger.LogTrace("Copying repository to game directory..."); var resolvedOutputDirectory = ioManager.ResolvePath(outputDirectory); var repoOrigin = repository.Origin; using (repository) await repository.CopyTo(resolvedOutputDirectory, cancellationToken).ConfigureAwait(false); // repository closed now // run precompile scripts await eventConsumer.HandleEvent( EventType.CompileStart, new List <string> { resolvedOutputDirectory, repoOrigin.ToString() }, cancellationToken) .ConfigureAwait(false); // determine the dme if (job.DmeName == null) { logger.LogTrace("Searching for available .dmes..."); var foundPaths = await ioManager.GetFilesWithExtension(resolvedOutputDirectory, DmeExtension, true, cancellationToken).ConfigureAwait(false); var foundPath = foundPaths.FirstOrDefault(); if (foundPath == default) { throw new JobException(ErrorCode.DreamMakerNoDme); } job.DmeName = foundPath.Substring( resolvedOutputDirectory.Length + 1, foundPath.Length - resolvedOutputDirectory.Length - DmeExtension.Length - 2); // +1 for . in extension } else { var targetDme = ioManager.ConcatPath(outputDirectory, String.Join('.', job.DmeName, DmeExtension)); var targetDmeExists = await ioManager.FileExists(targetDme, cancellationToken).ConfigureAwait(false); if (!targetDmeExists) { throw new JobException(ErrorCode.DreamMakerMissingDme); } } logger.LogDebug("Selected {0}.dme for compilation!", job.DmeName); await ModifyDme(job, cancellationToken).ConfigureAwait(false); // run compiler var exitCode = await RunDreamMaker(byondLock.DreamMakerPath, job, cancellationToken).ConfigureAwait(false); // verify api try { if (exitCode != 0) { throw new JobException( ErrorCode.DreamMakerExitCode, new JobException($"Exit code: {exitCode}{Environment.NewLine}{Environment.NewLine}{job.Output}")); } await VerifyApi( apiValidateTimeout, dreamMakerSettings.ApiValidationSecurityLevel.Value, job, byondLock, dreamMakerSettings.ApiValidationPort.Value, dreamMakerSettings.RequireDMApiValidation.Value, cancellationToken) .ConfigureAwait(false); } catch (JobException) { // DD never validated or compile failed await eventConsumer.HandleEvent( EventType.CompileFailure, new List <string> { resolvedOutputDirectory, exitCode == 0 ? "1" : "0" }, cancellationToken) .ConfigureAwait(false); throw; } await eventConsumer.HandleEvent(EventType.CompileComplete, new List <string> { resolvedOutputDirectory }, cancellationToken).ConfigureAwait(false); logger.LogTrace("Applying static game file symlinks..."); // symlink in the static data await configuration.SymlinkStaticFilesTo(resolvedOutputDirectory, cancellationToken).ConfigureAwait(false); logger.LogDebug("Compile complete!"); } catch (Exception ex) { await CleanupFailedCompile(job, remoteDeploymentManager, ex).ConfigureAwait(false); throw; } }
/// <inheritdoc /> public async Task CreateDump(global::System.Diagnostics.Process process, string outputFile, CancellationToken cancellationToken) { if (process == null) { throw new ArgumentNullException(nameof(process)); } if (outputFile == null) { throw new ArgumentNullException(nameof(outputFile)); } const string GCorePath = "/usr/bin/gcore"; if (!await ioManager.FileExists(GCorePath, cancellationToken).ConfigureAwait(false)) { throw new JobException(ErrorCode.MissingGCore); } int pid; try { if (process.HasExited) { throw new JobException(ErrorCode.DreamDaemonOffline); } pid = process.Id; } catch (InvalidOperationException ex) { throw new JobException(ErrorCode.DreamDaemonOffline, ex); } string output; int exitCode; using (var gcoreProc = lazyLoadedProcessExecutor.Value.LaunchProcess( GCorePath, Environment.CurrentDirectory, $"-o {outputFile} {process.Id}", true, true, true)) { using (cancellationToken.Register(() => gcoreProc.Terminate())) exitCode = await gcoreProc.Lifetime.ConfigureAwait(false); output = await gcoreProc.GetCombinedOutput(cancellationToken).ConfigureAwait(false); logger.LogDebug("gcore output:{0}{1}", Environment.NewLine, output); } if (exitCode != 0) { throw new JobException( ErrorCode.GCoreFailure, new JobException( $"Exit Code: {exitCode}{Environment.NewLine}Output:{Environment.NewLine}{output}")); } // gcore outputs name.pid so remove the pid part var generatedGCoreFile = $"{outputFile}.{pid}"; await ioManager.MoveFile(generatedGCoreFile, outputFile, cancellationToken).ConfigureAwait(false); }
/// <summary> /// Executes and populate a given <paramref name="job"/> /// </summary> /// <param name="job">The <see cref="Models.CompileJob"/> to run and populate</param> /// <param name="dreamMakerSettings">The <see cref="Api.Models.DreamMaker"/> settings to use</param> /// <param name="byondLock">The <see cref="IByondExecutableLock"/> to use</param> /// <param name="repository">The <see cref="IRepository"/> to use</param> /// <param name="apiValidateTimeout">The timeout for validating the DMAPI</param> /// <param name="cancellationToken">The <see cref="CancellationToken"/> for the operation</param> /// <returns>A <see cref="Task"/> representing the running operation</returns> async Task RunCompileJob(Models.CompileJob job, Api.Models.DreamMaker dreamMakerSettings, IByondExecutableLock byondLock, IRepository repository, uint apiValidateTimeout, CancellationToken cancellationToken) { var jobPath = job.DirectoryName.ToString(); logger.LogTrace("Compile output GUID: {0}", jobPath); try { var dirA = ioManager.ConcatPath(jobPath, ADirectoryName); var dirB = ioManager.ConcatPath(jobPath, BDirectoryName); // copy the repository logger.LogTrace("Copying repository to game directory..."); var resolvedADirectory = ioManager.ResolvePath(dirA); var repoOrigin = repository.Origin; using (repository) await repository.CopyTo(resolvedADirectory, cancellationToken).ConfigureAwait(false); // repository closed now // run precompile scripts await eventConsumer.HandleEvent(EventType.CompileStart, new List <string> { resolvedADirectory, repoOrigin }, cancellationToken).ConfigureAwait(false); // determine the dme if (job.DmeName == null) { logger.LogTrace("Searching for available .dmes..."); var foundPaths = await ioManager.GetFilesWithExtension(dirA, DmeExtension, cancellationToken).ConfigureAwait(false); var foundPath = foundPaths.FirstOrDefault(); if (foundPath == default) { throw new JobException("Unable to find any .dme!"); } var dmeWithExtension = ioManager.GetFileName(foundPath); job.DmeName = dmeWithExtension.Substring(0, dmeWithExtension.Length - DmeExtension.Length - 1); } else { var targetDme = ioManager.ConcatPath(dirA, String.Join('.', job.DmeName, DmeExtension)); var targetDmeExists = await ioManager.FileExists(targetDme, cancellationToken).ConfigureAwait(false); if (!targetDmeExists) { throw new JobException("Unable to locate specified .dme!"); } } logger.LogDebug("Selected {0}.dme for compilation!", job.DmeName); await ModifyDme(job, cancellationToken).ConfigureAwait(false); // run compiler var exitCode = await RunDreamMaker(byondLock.DreamMakerPath, job, cancellationToken).ConfigureAwait(false); // verify api try { if (exitCode != 0) { throw new JobException(String.Format(CultureInfo.InvariantCulture, "DM exited with a non-zero code: {0}{1}{2}", exitCode, Environment.NewLine, job.Output)); } await VerifyApi(apiValidateTimeout, dreamMakerSettings.ApiValidationSecurityLevel.Value, job, byondLock, dreamMakerSettings.ApiValidationPort.Value, cancellationToken).ConfigureAwait(false); } catch (JobException) { // DD never validated or compile failed await eventConsumer.HandleEvent(EventType.CompileFailure, new List <string> { resolvedADirectory, exitCode == 0 ? "1" : "0" }, cancellationToken).ConfigureAwait(false); throw; } logger.LogTrace("Running post compile event..."); await eventConsumer.HandleEvent(EventType.CompileComplete, new List <string> { resolvedADirectory }, cancellationToken).ConfigureAwait(false); logger.LogTrace("Duplicating compiled game..."); // duplicate the dmb et al await ioManager.CopyDirectory(dirA, dirB, null, cancellationToken).ConfigureAwait(false); logger.LogTrace("Applying static game file symlinks..."); // symlink in the static data var symATask = configuration.SymlinkStaticFilesTo(resolvedADirectory, cancellationToken); var symBTask = configuration.SymlinkStaticFilesTo(ioManager.ResolvePath(dirB), cancellationToken); await Task.WhenAll(symATask, symBTask).ConfigureAwait(false); await chat.SendUpdateMessage(String.Format(CultureInfo.InvariantCulture, "Deployment complete!{0}", watchdog.Running ? " Changes will be applied on next server reboot." : String.Empty), cancellationToken).ConfigureAwait(false); logger.LogDebug("Compile complete!"); } catch (Exception e) { await CleanupFailedCompile(job, e is OperationCanceledException, cancellationToken).ConfigureAwait(false); throw; } }
/// <inheritdoc /> #pragma warning disable CA1506 // TODO: Decomplexify public async Task <IDmbProvider> FromCompileJob(CompileJob compileJob, CancellationToken cancellationToken) { if (compileJob == null) { throw new ArgumentNullException(nameof(compileJob)); } // ensure we have the entire metadata tree logger.LogTrace("Loading compile job {0}...", compileJob.Id); await databaseContextFactory.UseContext( async db => compileJob = await db .CompileJobs .AsQueryable() .Where(x => x.Id == compileJob.Id) .Include(x => x.Job) .ThenInclude(x => x.StartedBy) .Include(x => x.RevisionInformation) .ThenInclude(x => x.PrimaryTestMerge) .ThenInclude(x => x.MergedBy) .Include(x => x.RevisionInformation) .ThenInclude(x => x.ActiveTestMerges) .ThenInclude(x => x.TestMerge) .ThenInclude(x => x.MergedBy) .FirstAsync(cancellationToken) .ConfigureAwait(false)) .ConfigureAwait(false); // can't wait to see that query if (!compileJob.Job.StoppedAt.HasValue) { // This happens when we're told to load the compile job that is currently finished up // It constitutes an API violation if it's returned by the DreamDaemonController so just set it here // Bit of a hack, but it works out to be nearly if not the same value that's put in the DB logger.LogTrace("Setting missing StoppedAt for CompileJob.Job #{0}...", compileJob.Job.Id); compileJob.Job.StoppedAt = DateTimeOffset.UtcNow; } var providerSubmitted = false; void CleanupAction() { if (providerSubmitted) { CleanJob(compileJob); } } var newProvider = new DmbProvider(compileJob, ioManager, CleanupAction); try { const string LegacyADirectoryName = "A"; const string LegacyBDirectoryName = "B"; var dmbExistsAtRoot = await ioManager.FileExists( ioManager.ConcatPath( newProvider.Directory, newProvider.DmbName), cancellationToken) .ConfigureAwait(false); if (!dmbExistsAtRoot) { logger.LogTrace("Didn't find .dmb at game directory root, checking A/B dirs..."); var primaryCheckTask = ioManager.FileExists( ioManager.ConcatPath( newProvider.Directory, LegacyADirectoryName, newProvider.DmbName), cancellationToken); var secondaryCheckTask = ioManager.FileExists( ioManager.ConcatPath( newProvider.Directory, LegacyBDirectoryName, newProvider.DmbName), cancellationToken); if (!(await primaryCheckTask.ConfigureAwait(false) && await secondaryCheckTask.ConfigureAwait(false))) { logger.LogWarning("Error loading compile job, .dmb missing!"); return(null); // omae wa mou shinderu } // rebuild the provider because it's using the legacy style directories // Don't dispose it logger.LogDebug("Creating legacy two folder .dmb provider targeting {0} directory...", LegacyADirectoryName); newProvider = new DmbProvider(compileJob, ioManager, CleanupAction, Path.DirectorySeparatorChar + LegacyADirectoryName); } lock (jobLockCounts) { if (!jobLockCounts.TryGetValue(compileJob.Id, out int value)) { value = 1; jobLockCounts.Add(compileJob.Id, 1); } else { jobLockCounts[compileJob.Id] = ++value; } providerSubmitted = true; logger.LogTrace("Compile job {0} lock count now: {1}", compileJob.Id, value); return(newProvider); } } finally { if (!providerSubmitted) { newProvider.Dispose(); } } }
/// <inheritdoc /> #pragma warning disable CA1506 // TODO: Decomplexify public async Task <IDmbProvider> FromCompileJob(CompileJob compileJob, CancellationToken cancellationToken) { if (compileJob == null) { throw new ArgumentNullException(nameof(compileJob)); } // ensure we have the entire compile job tree logger.LogTrace("Loading compile job {0}...", compileJob.Id); await databaseContextFactory.UseContext( async db => compileJob = await db .CompileJobs .AsQueryable() .Where(x => x.Id == compileJob.Id) .Include(x => x.Job).ThenInclude(x => x.StartedBy) .Include(x => x.RevisionInformation).ThenInclude(x => x.PrimaryTestMerge).ThenInclude(x => x.MergedBy) .Include(x => x.RevisionInformation).ThenInclude(x => x.ActiveTestMerges).ThenInclude(x => x.TestMerge).ThenInclude(x => x.MergedBy) .FirstAsync(cancellationToken) .ConfigureAwait(false)) .ConfigureAwait(false); // can't wait to see that query if (!compileJob.Job.StoppedAt.HasValue) { // This happens if we're told to load the compile job that is currently finished up // It can constitute an API violation if it's returned by the DreamDaemonController so just set it here // Bit of a hack, but it should work out to be the same value logger.LogTrace("Setting missing StoppedAt for CompileJob job..."); compileJob.Job.StoppedAt = DateTimeOffset.Now; } var providerSubmitted = false; var newProvider = new DmbProvider(compileJob, ioManager, () => { if (providerSubmitted) { CleanJob(compileJob); } }); try { var primaryCheckTask = ioManager.FileExists(ioManager.ConcatPath(newProvider.PrimaryDirectory, newProvider.DmbName), cancellationToken); var secondaryCheckTask = ioManager.FileExists(ioManager.ConcatPath(newProvider.PrimaryDirectory, newProvider.DmbName), cancellationToken); if (!(await primaryCheckTask.ConfigureAwait(false) && await secondaryCheckTask.ConfigureAwait(false))) { logger.LogWarning("Error loading compile job, .dmb missing!"); return(null); // omae wa mou shinderu } lock (jobLockCounts) { if (!jobLockCounts.TryGetValue(compileJob.Id, out int value)) { value = 1; jobLockCounts.Add(compileJob.Id, 1); } else { jobLockCounts[compileJob.Id] = ++value; } logger.LogTrace("Compile job {0} lock count now: {1}", compileJob.Id, value); providerSubmitted = true; return(newProvider); } } finally { if (!providerSubmitted) { newProvider.Dispose(); } } }
/// <inheritdoc /> public async Task StartAsync(CancellationToken cancellationToken) { async Task <byte[]> GetActiveVersion() { var activeVersionFileExists = await ioManager.FileExists(ActiveVersionFileName, cancellationToken).ConfigureAwait(false); return(!activeVersionFileExists ? null : await ioManager.ReadAllBytes(ActiveVersionFileName, cancellationToken).ConfigureAwait(false)); } var activeVersionBytesTask = GetActiveVersion(); await ioManager.CreateDirectory(".", cancellationToken).ConfigureAwait(false); var directories = await ioManager.GetDirectories(".", cancellationToken).ConfigureAwait(false); async Task ReadVersion(string path) { var versionFile = ioManager.ConcatPath(path, VersionFileName); if (!await ioManager.FileExists(versionFile, cancellationToken).ConfigureAwait(false)) { logger.LogInformation("Cleaning unparsable version path: {0}", ioManager.ResolvePath(path)); await ioManager.DeleteDirectory(path, cancellationToken).ConfigureAwait(false); // cleanup return; } var bytes = await ioManager.ReadAllBytes(versionFile, cancellationToken).ConfigureAwait(false); var text = Encoding.UTF8.GetString(bytes); if (Version.TryParse(text, out var version)) { var key = VersionKey(version); lock (installedVersions) if (!installedVersions.ContainsKey(key)) { installedVersions.Add(key, Task.CompletedTask); return; } } await ioManager.DeleteDirectory(path, cancellationToken).ConfigureAwait(false); } await Task.WhenAll(directories.Select(x => ReadVersion(x))).ConfigureAwait(false); var activeVersionBytes = await activeVersionBytesTask.ConfigureAwait(false); if (activeVersionBytes != null) { var activeVersionString = Encoding.UTF8.GetString(activeVersionBytes); if (Version.TryParse(activeVersionString, out var activeVersion)) { ActiveVersion = activeVersion; } else { await ioManager.DeleteFile(ActiveVersionFileName, cancellationToken).ConfigureAwait(false); } } }