/// <inheritdoc /> public async Task SymlinkStaticFilesTo(string destination, CancellationToken cancellationToken) { async Task SymlinkBase(bool files) { Task <IReadOnlyList <string> > task; if (files) { task = ioManager.GetFiles(GameStaticFilesSubdirectory, cancellationToken); } else { task = ioManager.GetDirectories(GameStaticFilesSubdirectory, cancellationToken); } var entries = await task.ConfigureAwait(false); await Task.WhenAll(task.Result.Select(async x => { var destPath = ioManager.ConcatPath(destination, ioManager.GetFileName(x)); logger.LogTrace("Symlinking {0} to {1}...", x, destPath); var fileExistsTask = ioManager.FileExists(destPath, cancellationToken); if (await ioManager.DirectoryExists(destPath, cancellationToken).ConfigureAwait(false)) { await ioManager.DeleteDirectory(destPath, cancellationToken).ConfigureAwait(false); } var fileExists = await fileExistsTask.ConfigureAwait(false); if (fileExists) { await ioManager.DeleteFile(destPath, cancellationToken).ConfigureAwait(false); } await symlinkFactory.CreateSymbolicLink(ioManager.ResolvePath(x), ioManager.ResolvePath(destPath), cancellationToken).ConfigureAwait(false); })).ConfigureAwait(false); } using (await SemaphoreSlimContext.Lock(semaphore, cancellationToken).ConfigureAwait(false)) { await EnsureDirectories(cancellationToken).ConfigureAwait(false); await Task.WhenAll(SymlinkBase(true), SymlinkBase(false)).ConfigureAwait(false); } }
/// <inheritdoc /> public async Task SymlinkStaticFilesTo(string destination, CancellationToken cancellationToken) { async Task <IReadOnlyList <string> > GetIgnoreFiles() { var ignoreFileBytes = await ioManager.ReadAllBytes(StaticIgnorePath(), cancellationToken).ConfigureAwait(false); var ignoreFileText = Encoding.UTF8.GetString(ignoreFileBytes); var results = new List <string> { StaticIgnoreFile }; //we don't want to lose trailing whitespace on linux using (var reader = new StringReader(ignoreFileText)) { cancellationToken.ThrowIfCancellationRequested(); var line = await reader.ReadLineAsync().ConfigureAwait(false); if (!String.IsNullOrEmpty(line)) { results.Add(line); } } return(results); }; IReadOnlyList <string> ignoreFiles; async Task SymlinkBase(bool files) { Task <IReadOnlyList <string> > task; if (files) { task = ioManager.GetFiles(GameStaticFilesSubdirectory, cancellationToken); } else { task = ioManager.GetDirectories(GameStaticFilesSubdirectory, cancellationToken); } var entries = await task.ConfigureAwait(false); await Task.WhenAll(entries.Select(async x => { var fileName = ioManager.GetFileName(x); bool ignored; if (platformIdentifier.IsWindows) { //need to normalize ignored = ignoreFiles.Any(y => fileName.ToUpperInvariant() == y.ToUpperInvariant()); } else { ignored = ignoreFiles.Any(y => fileName == y); } if (ignored) { logger.LogTrace("Ignoring static file {0}...", fileName); return; } var destPath = ioManager.ConcatPath(destination, fileName); logger.LogTrace("Symlinking {0} to {1}...", x, destPath); var fileExistsTask = ioManager.FileExists(destPath, cancellationToken); if (await ioManager.DirectoryExists(destPath, cancellationToken).ConfigureAwait(false)) { await ioManager.DeleteDirectory(destPath, cancellationToken).ConfigureAwait(false); } var fileExists = await fileExistsTask.ConfigureAwait(false); if (fileExists) { await ioManager.DeleteFile(destPath, cancellationToken).ConfigureAwait(false); } await symlinkFactory.CreateSymbolicLink(ioManager.ResolvePath(x), ioManager.ResolvePath(destPath), cancellationToken).ConfigureAwait(false); })).ConfigureAwait(false); } using (await SemaphoreSlimContext.Lock(semaphore, cancellationToken).ConfigureAwait(false)) { await EnsureDirectories(cancellationToken).ConfigureAwait(false); ignoreFiles = await GetIgnoreFiles().ConfigureAwait(false); await Task.WhenAll(SymlinkBase(true), SymlinkBase(false)).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);
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 /> #pragma warning disable CA1506 // TODO: Decomplexify public async Task <ISessionController> LaunchNew(DreamDaemonLaunchParameters launchParameters, IDmbProvider dmbProvider, IByondExecutableLock currentByondLock, bool primaryPort, bool primaryDirectory, bool apiValidate, CancellationToken cancellationToken) { var portToUse = primaryPort ? launchParameters.PrimaryPort : launchParameters.SecondaryPort; if (!portToUse.HasValue) { throw new InvalidOperationException("Given port is null!"); } var accessIdentifier = cryptographySuite.GetSecureString(); const string JsonPostfix = "tgs.json"; var basePath = primaryDirectory ? dmbProvider.PrimaryDirectory : dmbProvider.SecondaryDirectory; // delete all previous tgs json files var files = await ioManager.GetFilesWithExtension(basePath, JsonPostfix, cancellationToken).ConfigureAwait(false); await Task.WhenAll(files.Select(x => ioManager.DeleteFile(x, cancellationToken))).ConfigureAwait(false); // i changed this back from guids, hopefully i don't regret that string JsonFile(string name) => String.Format(CultureInfo.InvariantCulture, "{0}.{1}", name, JsonPostfix); var securityLevelToUse = launchParameters.SecurityLevel.Value; switch (dmbProvider.CompileJob.MinimumSecurityLevel) { case DreamDaemonSecurity.Ultrasafe: break; case DreamDaemonSecurity.Safe: if (securityLevelToUse == DreamDaemonSecurity.Ultrasafe) { securityLevelToUse = DreamDaemonSecurity.Safe; } break; case DreamDaemonSecurity.Trusted: securityLevelToUse = DreamDaemonSecurity.Trusted; break; default: throw new InvalidOperationException(String.Format(CultureInfo.InvariantCulture, "Invalid DreamDaemonSecurity value: {0}", dmbProvider.CompileJob.MinimumSecurityLevel)); } // setup interop files var interopInfo = new JsonFile { AccessIdentifier = accessIdentifier, ApiValidateOnly = apiValidate, ChatChannelsJson = JsonFile("chat_channels"), ChatCommandsJson = JsonFile("chat_commands"), ServerCommandsJson = JsonFile("server_commands"), InstanceName = instance.Name, SecurityLevel = securityLevelToUse, Revision = new Api.Models.Internal.RevisionInformation { CommitSha = dmbProvider.CompileJob.RevisionInformation.CommitSha, OriginCommitSha = dmbProvider.CompileJob.RevisionInformation.OriginCommitSha } }; interopInfo.TestMerges.AddRange(dmbProvider.CompileJob.RevisionInformation.ActiveTestMerges.Select(x => x.TestMerge).Select(x => new Interop.TestMerge(x, interopInfo.Revision))); var interopJsonFile = JsonFile("interop"); var interopJson = JsonConvert.SerializeObject(interopInfo, new JsonSerializerSettings { ContractResolver = new CamelCasePropertyNamesContractResolver(), ReferenceLoopHandling = ReferenceLoopHandling.Ignore }); var chatJsonTrackingTask = chat.TrackJsons(basePath, interopInfo.ChatChannelsJson, interopInfo.ChatCommandsJson, cancellationToken); await ioManager.WriteAllBytes(ioManager.ConcatPath(basePath, interopJsonFile), Encoding.UTF8.GetBytes(interopJson), cancellationToken).ConfigureAwait(false); var chatJsonTrackingContext = await chatJsonTrackingTask.ConfigureAwait(false); try { // get the byond lock var byondLock = currentByondLock ?? await byond.UseExecutables(Version.Parse(dmbProvider.CompileJob.ByondVersion), cancellationToken).ConfigureAwait(false); try { // create interop context var context = new CommContext(ioManager, loggerFactory.CreateLogger <CommContext>(), basePath, interopInfo.ServerCommandsJson); try { // set command line options // more sanitization here cause it uses the same scheme var parameters = String.Format(CultureInfo.InvariantCulture, "{2}={0}&{3}={1}", byondTopicSender.SanitizeString(application.Version.ToString()), byondTopicSender.SanitizeString(interopJsonFile), byondTopicSender.SanitizeString(Constants.DMParamHostVersion), byondTopicSender.SanitizeString(Constants.DMParamInfoJson)); var visibility = apiValidate ? "invisible" : "public"; // important to run on all ports to allow port changing var arguments = String.Format(CultureInfo.InvariantCulture, "{0} -port {1} -ports 1-65535 {2}-close -{3} -{5} -public -params \"{4}\"", dmbProvider.DmbName, primaryPort ? launchParameters.PrimaryPort : launchParameters.SecondaryPort, launchParameters.AllowWebClient.Value ? "-webclient " : String.Empty, SecurityWord(securityLevelToUse), parameters, visibility); // See https://github.com/tgstation/tgstation-server/issues/719 var noShellExecute = !platformIdentifier.IsWindows; // launch dd var process = processExecutor.LaunchProcess(byondLock.DreamDaemonPath, basePath, arguments, noShellExecute: noShellExecute); try { networkPromptReaper.RegisterProcess(process); // return the session controller for it var result = new SessionController(new ReattachInformation { AccessIdentifier = accessIdentifier, Dmb = dmbProvider, IsPrimary = primaryDirectory, Port = portToUse.Value, ProcessId = process.Id, ChatChannelsJson = interopInfo.ChatChannelsJson, ChatCommandsJson = interopInfo.ChatCommandsJson, ServerCommandsJson = interopInfo.ServerCommandsJson, }, process, byondLock, byondTopicSender, chatJsonTrackingContext, context, chat, loggerFactory.CreateLogger <SessionController>(), launchParameters.SecurityLevel, launchParameters.StartupTimeout); // writeback launch parameter's fixed security level launchParameters.SecurityLevel = securityLevelToUse; return(result); } catch { process.Dispose(); throw; } } catch { context.Dispose(); throw; } } catch { if (currentByondLock == null) { byondLock.Dispose(); } throw; } } catch { chatJsonTrackingContext.Dispose(); throw; } }
/// <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 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> /// Ensure a given <paramref name="testConnection"/> works. /// </summary> /// <param name="testConnection">The test <see cref="DbConnection"/>.</param> /// <param name="databaseConfiguration">The <see cref="DatabaseConfiguration"/> may have derived data populated.</param> /// <param name="databaseName">The database name (or path in the case of a <see cref="DatabaseType.Sqlite"/> database).</param> /// <param name="dbExists">Whether or not the database exists.</param> /// <param name="cancellationToken">The <see cref="CancellationToken"/> for the operation.</param> /// <returns>A <see cref="Task"/> representing the running operation.</returns> async Task TestDatabaseConnection( DbConnection testConnection, DatabaseConfiguration databaseConfiguration, string databaseName, bool dbExists, CancellationToken cancellationToken) { bool isSqliteDB = databaseConfiguration.DatabaseType == DatabaseType.Sqlite; using (testConnection) { await console.WriteAsync("Testing connection...", true, cancellationToken).ConfigureAwait(false); await testConnection.OpenAsync(cancellationToken).ConfigureAwait(false); await console.WriteAsync("Connection successful!", true, cancellationToken).ConfigureAwait(false); if (databaseConfiguration.DatabaseType == DatabaseType.MariaDB || databaseConfiguration.DatabaseType == DatabaseType.MySql) { await console.WriteAsync("Checking MySQL/MariaDB version...", true, cancellationToken).ConfigureAwait(false); using (var command = testConnection.CreateCommand()) { command.CommandText = "SELECT VERSION()"; var fullVersion = (string)await command.ExecuteScalarAsync(cancellationToken).ConfigureAwait(false); await console.WriteAsync(String.Format(CultureInfo.InvariantCulture, "Found {0}", fullVersion), true, cancellationToken).ConfigureAwait(false); var splits = fullVersion.Split('-'); databaseConfiguration.MySqlServerVersion = splits.First(); } } if (!isSqliteDB && !dbExists) { await console.WriteAsync("Testing create DB permission...", true, cancellationToken).ConfigureAwait(false); using (var command = testConnection.CreateCommand()) { // I really don't care about user sanitization here, they want to f**k their own DB? so be it #pragma warning disable CA2100 // Review SQL queries for security vulnerabilities command.CommandText = $"CREATE DATABASE {databaseName}"; #pragma warning restore CA2100 // Review SQL queries for security vulnerabilities await command.ExecuteNonQueryAsync(cancellationToken).ConfigureAwait(false); } await console.WriteAsync("Success!", true, cancellationToken).ConfigureAwait(false); await console.WriteAsync("Dropping test database...", true, cancellationToken).ConfigureAwait(false); using (var command = testConnection.CreateCommand()) { #pragma warning disable CA2100 // Review SQL queries for security vulnerabilities command.CommandText = $"DROP DATABASE {databaseName}"; #pragma warning restore CA2100 // Review SQL queries for security vulnerabilities try { await command.ExecuteNonQueryAsync(cancellationToken).ConfigureAwait(false); } 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("This should be okay, but you may want to manually drop the database before continuing!", true, cancellationToken).ConfigureAwait(false); await console.WriteAsync("Press any key to continue...", true, cancellationToken).ConfigureAwait(false); await console.PressAnyKeyAsync(cancellationToken).ConfigureAwait(false); } } } } if (isSqliteDB && !dbExists) { await Task.WhenAll( console.WriteAsync("Deleting test database file...", true, cancellationToken), ioManager.DeleteFile(databaseName, cancellationToken)).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(); 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); } } }
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);