diff --git a/src/Tgstation.Server.Host/Core/SetupWizard.cs b/src/Tgstation.Server.Host/Core/SetupWizard.cs index 76d727f07e..e5f682b431 100644 --- a/src/Tgstation.Server.Host/Core/SetupWizard.cs +++ b/src/Tgstation.Server.Host/Core/SetupWizard.cs @@ -185,9 +185,14 @@ namespace Tgstation.Server.Host.Core username = await console.ReadLineAsync(false, cancellationToken).ConfigureAwait(false); await console.WriteAsync("Enter password: ", false, cancellationToken).ConfigureAwait(false); password = await console.ReadLineAsync(true, cancellationToken).ConfigureAwait(false); - await console.WriteAsync(null, true, cancellationToken).ConfigureAwait(false); } - + else + { + await console.WriteAsync("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) @@ -266,6 +271,10 @@ namespace Tgstation.Server.Host.Core { await command.ExecuteNonQueryAsync(cancellationToken).ConfigureAwait(false); } + catch (OperationCanceledException) + { + throw; + } catch (Exception e) { await console.WriteAsync(e.Message, true, cancellationToken).ConfigureAwait(false); @@ -280,6 +289,10 @@ namespace Tgstation.Server.Host.Core return databaseConfiguration; } + catch (OperationCanceledException) + { + throw; + } catch (Exception e) { await console.WriteAsync(e.Message, true, cancellationToken).ConfigureAwait(false); @@ -296,17 +309,20 @@ namespace Tgstation.Server.Host.Core /// A resulting in the new async Task ConfigureGeneral(CancellationToken cancellationToken) { - var generalConfiguration = new GeneralConfiguration(); + var newGeneralConfiguration = new GeneralConfiguration + { + SetupWizardMode = SetupWizardMode.Never + }; do { await console.WriteAsync(null, true, cancellationToken).ConfigureAwait(false); - await console.WriteAsync(String.Format(CultureInfo.InvariantCulture, "Minimum database user password length (leave blank for default of {0}): ", generalConfiguration.MinimumPasswordLength), false, cancellationToken).ConfigureAwait(false); + await console.WriteAsync(String.Format(CultureInfo.InvariantCulture, "Minimum database user password length (leave blank for default of {0}): ", newGeneralConfiguration.MinimumPasswordLength), false, cancellationToken).ConfigureAwait(false); var passwordLengthString = await console.ReadLineAsync(false, cancellationToken).ConfigureAwait(false); if (String.IsNullOrWhiteSpace(passwordLengthString)) break; if (UInt32.TryParse(passwordLengthString, out var passwordLength) && passwordLength >= 0) { - generalConfiguration.MinimumPasswordLength = passwordLength; + newGeneralConfiguration.MinimumPasswordLength = passwordLength; break; } await console.WriteAsync("Please enter a positive integer!", true, cancellationToken).ConfigureAwait(false); @@ -316,13 +332,13 @@ namespace Tgstation.Server.Host.Core do { await console.WriteAsync(null, true, cancellationToken).ConfigureAwait(false); - await console.WriteAsync(String.Format(CultureInfo.InvariantCulture, "Timeout for sending and receiving BYOND topics (ms, 0 for infinite, leave blank for default of {0}): ", generalConfiguration.ByondTopicTimeout), false, cancellationToken).ConfigureAwait(false); + await console.WriteAsync(String.Format(CultureInfo.InvariantCulture, "Timeout for sending and receiving BYOND topics (ms, 0 for infinite, leave blank for default of {0}): ", newGeneralConfiguration.ByondTopicTimeout), false, cancellationToken).ConfigureAwait(false); var topicTimeoutString = await console.ReadLineAsync(false, cancellationToken).ConfigureAwait(false); if (String.IsNullOrWhiteSpace(topicTimeoutString)) break; if (Int32.TryParse(topicTimeoutString, out var topicTimeout) && topicTimeout >= 0) { - generalConfiguration.ByondTopicTimeout = topicTimeout; + newGeneralConfiguration.ByondTopicTimeout = topicTimeout; break; } await console.WriteAsync("Please enter a positive integer!", true, cancellationToken).ConfigureAwait(false); @@ -332,10 +348,93 @@ namespace Tgstation.Server.Host.Core await console.WriteAsync(null, true, cancellationToken).ConfigureAwait(false); await console.WriteAsync("Enter a GitHub personal access token to bypass some rate limits (this is optional and does not require any scopes)", true, cancellationToken).ConfigureAwait(false); await console.WriteAsync("GitHub personal access token: ", false, cancellationToken).ConfigureAwait(false); - generalConfiguration.GitHubAccessToken = await console.ReadLineAsync(true, cancellationToken).ConfigureAwait(false); - if (String.IsNullOrWhiteSpace(generalConfiguration.GitHubAccessToken)) - generalConfiguration.GitHubAccessToken = null; - return generalConfiguration; + newGeneralConfiguration.GitHubAccessToken = await console.ReadLineAsync(true, cancellationToken).ConfigureAwait(false); + if (String.IsNullOrWhiteSpace(newGeneralConfiguration.GitHubAccessToken)) + newGeneralConfiguration.GitHubAccessToken = null; + return newGeneralConfiguration; + } + + /// + /// Prompts the user to create a + /// + /// The for the operation + /// A resulting in the new + async Task 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; + } + else + { + //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(), 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 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(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; } /// @@ -344,17 +443,19 @@ namespace Tgstation.Server.Host.Core /// The file to save the to /// The hosting port to save /// The to save - /// The to save + /// The to save + /// The to save /// The for the operation /// A representing the running operation - async Task SaveConfiguration(string userConfigFileName, ushort? hostingPort, DatabaseConfiguration databaseConfiguration, GeneralConfiguration generalConfiguration, CancellationToken cancellationToken) + async Task SaveConfiguration(string userConfigFileName, ushort? hostingPort, DatabaseConfiguration databaseConfiguration, GeneralConfiguration newGeneralConfiguration, FileLoggingConfiguration fileLoggingConfiguration, CancellationToken cancellationToken) { await console.WriteAsync(String.Format(CultureInfo.InvariantCulture, "Configuration complete! Saving to {0}", userConfigFileName), true, cancellationToken).ConfigureAwait(false); var map = new Dictionary() { { DatabaseConfiguration.Section, databaseConfiguration }, - { GeneralConfiguration.Section, generalConfiguration } + { GeneralConfiguration.Section, newGeneralConfiguration }, + { FileLoggingConfiguration.Section, fileLoggingConfiguration } }; if (hostingPort.HasValue) @@ -376,6 +477,10 @@ namespace Tgstation.Server.Host.Core { await ioManager.WriteAllBytes(userConfigFileName, configBytes, cancellationToken).ConfigureAwait(false); } + catch (OperationCanceledException) + { + throw; + } catch (Exception e) { await console.WriteAsync(e.Message, true, cancellationToken).ConfigureAwait(false); @@ -404,6 +509,7 @@ namespace Tgstation.Server.Host.Core async Task RunWizard(string userConfigFileName, CancellationToken cancellationToken) { //welcome message + await console.WriteAsync(null, true, cancellationToken).ConfigureAwait(false); await console.WriteAsync("Welcome to tgstation-server 4!", true, cancellationToken).ConfigureAwait(false); await console.WriteAsync("This wizard will help you configure your server.", true, cancellationToken).ConfigureAwait(false); @@ -411,11 +517,13 @@ namespace Tgstation.Server.Host.Core var databaseConfiguration = await ConfigureDatabase(cancellationToken).ConfigureAwait(false); - var generalConfiguration = await ConfigureGeneral(cancellationToken).ConfigureAwait(false); + var newGeneralConfiguration = await ConfigureGeneral(cancellationToken).ConfigureAwait(false); + + var fileLoggingConfiguration = await ConfigureLogging(cancellationToken).ConfigureAwait(false); await console.WriteAsync(null, true, cancellationToken).ConfigureAwait(false); - await SaveConfiguration(userConfigFileName, hostingPort, databaseConfiguration, generalConfiguration, cancellationToken).ConfigureAwait(false); + await SaveConfiguration(userConfigFileName, hostingPort, databaseConfiguration, newGeneralConfiguration, fileLoggingConfiguration, cancellationToken).ConfigureAwait(false); } ///