2using System.Collections.Generic;
3using System.Data.Common;
4using System.Globalization;
9using System.Text.RegularExpressions;
10using System.Threading;
11using System.Threading.Tasks;
13using Microsoft.Data.SqlClient;
14using Microsoft.Data.Sqlite;
15using Microsoft.Extensions.Hosting;
16using Microsoft.Extensions.Logging;
17using Microsoft.Extensions.Options;
32using YamlDotNet.Serialization;
111 IOptions<GeneralConfiguration> generalConfigurationOptions,
112 IOptions<InternalConfiguration> internalConfigurationOptions)
115 this.console =
console ??
throw new ArgumentNullException(nameof(
console));
123 generalConfiguration = generalConfigurationOptions?.Value ??
throw new ArgumentNullException(nameof(generalConfigurationOptions));
124 internalConfiguration = internalConfigurationOptions?.Value ??
throw new ArgumentNullException(nameof(internalConfigurationOptions));
128 protected override async Task
ExecuteAsync(CancellationToken cancellationToken)
141 async ValueTask<bool>
PromptYesNo(
string question,
bool? defaultResponse, CancellationToken cancellationToken)
145 await
console.
WriteAsync($
"{question} ({(defaultResponse == true ? 'Y' : 'y')}/{(defaultResponse == false ? 'N' : 'n')}): ",
false, cancellationToken);
147 if (responseString.Length == 0)
149 if (defaultResponse.HasValue)
150 return defaultResponse.Value;
154 var upperResponse = responseString.ToUpperInvariant();
155 if (upperResponse ==
"Y" || upperResponse ==
"YES")
157 else if (upperResponse ==
"N" || upperResponse ==
"NO")
174 await
console.
WriteAsync(
"What port would you like to connect to TGS on?",
true, cancellationToken);
175 await
console.
WriteAsync(
"Note: If this is a docker container with the default port already mapped, use the default.",
true, cancellationToken);
180 $
"API Port (leave blank for default of {GeneralConfiguration.DefaultApiPort}): ",
184 if (String.IsNullOrWhiteSpace(portString))
186 if (UInt16.TryParse(portString, out var port) && port != 0)
188 await
console.
WriteAsync(
"Invalid port! Please enter a value between 1 and 65535",
true, cancellationToken);
207 CancellationToken cancellationToken)
209 bool isSqliteDB = databaseConfiguration.DatabaseType ==
DatabaseType.Sqlite;
220 await
console.
WriteAsync($
"Checking {databaseConfiguration.DatabaseType} version...",
true, cancellationToken);
222 command.CommandText =
"SELECT VERSION()";
223 var fullVersion = (
string?)await command.ExecuteScalarAsync(cancellationToken);
224 await
console.
WriteAsync(String.Format(CultureInfo.InvariantCulture,
"Found {0}", fullVersion),
true, cancellationToken);
226 if (fullVersion ==
null)
227 throw new InvalidOperationException($
"\"{command.CommandText}\" returned null!");
231 var splits = fullVersion.Split(
' ');
232 databaseConfiguration.ServerVersion = splits[1].TrimEnd(
',');
236 var splits = fullVersion.Split(
'-');
237 databaseConfiguration.ServerVersion = splits.First();
241 if (!isSqliteDB && !dbExists)
243 await
console.
WriteAsync(
"Testing create DB permission...",
true, cancellationToken);
247#pragma warning disable CA2100
248 command.CommandText = $
"CREATE DATABASE {databaseName}";
249#pragma warning restore CA2100
250 await command.ExecuteNonQueryAsync(cancellationToken);
257#pragma warning disable CA2100
258 command.CommandText = $
"DROP DATABASE {databaseName}";
259#pragma warning restore CA2100
262 await command.ExecuteNonQueryAsync(cancellationToken);
264 catch (OperationCanceledException)
272 await
console.
WriteAsync(
"This should be okay, but you may want to manually drop the database before continuing!",
true, cancellationToken);
273 await
console.
WriteAsync(
"Press any key to continue...",
true, cancellationToken);
282 if (isSqliteDB && !dbExists)
284 await
console.
WriteAsync(
"Deleting test database file...",
true, cancellationToken);
286 SqliteConnection.ClearAllPools();
299 var dbPathIsRooted = Path.IsPathRooted(databaseName);
317 if (!directoryExisted)
329 await
console.
WriteAsync(
"Note, this relative path currently resolves to the following:",
true, cancellationToken);
332 "Would you like to save the relative path in the configuration? If not, the full path will be saved.",
337 databaseName = resolvedPath;
350 async ValueTask<DatabaseType>
PromptDatabaseType(
bool firstTime, CancellationToken cancellationToken)
356 await
console.
WriteAsync(
"It looks like you just installed MariaDB. Selecting it as the database type.",
true, cancellationToken);
362 "NOTE: If you are serious about hosting public servers, it is HIGHLY reccommended that TGS runs on a database *OTHER THAN* Sqlite.",
366 "It is, however, the easiest option to get started with and will pose few if any problems in a single user scenario.",
371 await
console.
WriteAsync(
"What SQL database type will you be using?",
true, cancellationToken);
376 CultureInfo.InvariantCulture,
377 "Please enter one of {0}, {1}, {2}, {3} or {4}: ",
386 if (Enum.TryParse<
DatabaseType>(databaseTypeString, out var databaseType))
399#pragma warning disable CA1502
402 bool firstTime =
true;
412 string? serverAddress =
null;
413 ushort? serverPort =
null;
416 var isSqliteDB = databaseConfiguration.DatabaseType ==
DatabaseType.Sqlite;
417 IPHostEntry? serverAddressEntry =
null;
422 if (definitelyLocalMariaDB)
424 await
console.
WriteAsync(
"Enter the server's port (blank for 3306): ",
false, cancellationToken);
426 if (!String.IsNullOrWhiteSpace(enteredPort) && enteredPort.Trim() !=
"3306")
427 serverAddress = $
"localhost:{enteredPort}";
431 await
console.
WriteAsync(
"Enter the server's address and port [<server>:<port> or <server>] (blank for local): ",
false, cancellationToken);
435 if (String.IsNullOrWhiteSpace(serverAddress))
436 serverAddress =
null;
439 var match = Regex.Match(serverAddress,
@"^(?<server>.+):(?<port>.+)$");
442 serverAddress = match.Groups[
"server"].Value;
443 var portString = match.Groups[
"port"].Value;
444 if (UInt16.TryParse(portString, out var port))
448 await
console.
WriteAsync($
"Failed to parse port \"{portString}\", please try again.",
true, cancellationToken);
456 if (serverAddress !=
null)
458 await
console.
WriteAsync(
"Attempting to resolve address...",
true, cancellationToken);
459 serverAddressEntry = await Dns.GetHostEntryAsync(serverAddress, cancellationToken);
466 await
console.
WriteAsync($
"Unable to resolve address: {ex.Message}",
true, cancellationToken);
472 await
console.
WriteAsync($
"Enter the database {(isSqliteDB ? "file path
" : "name
")} ({(definitelyLocalMariaDB ? "leave blank
for \
"tgs\")" :
"Can be from previous installation. Otherwise, should not exist")}):
", false, cancellationToken);
474 string? databaseName;
475 bool dbExists = false;
478 databaseName = await console.ReadLineAsync(false, cancellationToken);
479 if (!String.IsNullOrWhiteSpace(databaseName))
483 dbExists = await ioManager.FileExists(databaseName, cancellationToken);
485 databaseName = await ValidateNonExistantSqliteDBName(databaseName, cancellationToken);
488 dbExists = await PromptYesNo(
489 "Does
this database already exist? If not, we will attempt to CREATE it.
",
493 else if (definitelyLocalMariaDB)
494 databaseName = "tgs
";
496 if (String.IsNullOrWhiteSpace(databaseName))
497 await console.WriteAsync("Invalid database name!
", true, cancellationToken);
503 var useWinAuth = false;
505 if (databaseConfiguration.DatabaseType == DatabaseType.SqlServer && platformIdentifier.IsWindows)
507 var defaultResponse = serverAddressEntry?.AddressList.Any(IPAddress.IsLoopback) ?? false
510 useWinAuth = await PromptYesNo("Use Windows Authentication?
", defaultResponse, cancellationToken);
511 encrypt = await PromptYesNo("Use encrypted connection?
", false, cancellationToken);
514 await console.WriteAsync(null, true, cancellationToken);
516 string? username = null;
517 string? password = null;
521 if (definitelyLocalMariaDB)
523 await console.WriteAsync("Using username: root
", true, cancellationToken);
528 await console.WriteAsync("Enter username:
", false, cancellationToken);
529 username = await console.ReadLineAsync(false, cancellationToken);
532 await console.WriteAsync("Enter password:
", false, cancellationToken);
533 password = await console.ReadLineAsync(true, cancellationToken);
537 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);
538 await console.WriteAsync("The account it uses in MSSQL is usually \"NT AUTHORITY\\SYSTEM\" and the role it needs is usually \"dbcreator\".", true, cancellationToken);
539 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);
542 await console.WriteAsync(null, true, cancellationToken);
544 DbConnection testConnection;
545 void CreateTestConnection(string connectionString) =>
546 testConnection = dbConnectionFactory.CreateConnection(
548 databaseConfiguration.DatabaseType);
550 switch (databaseConfiguration.DatabaseType)
552 case DatabaseType.SqlServer:
554 var csb = new SqlConnectionStringBuilder
556 ApplicationName = assemblyInformationProvider.VersionPrefix,
557 DataSource = serverAddress ?? "(local)
",
562 csb.IntegratedSecurity = true;
565 csb.UserID = username;
566 csb.Password = password;
569 csb.Encrypt = encrypt;
571 CreateTestConnection(csb.ConnectionString);
572 csb.InitialCatalog = databaseName;
573 databaseConfiguration.ConnectionString = csb.ConnectionString;
577 case DatabaseType.MariaDB:
578 case DatabaseType.MySql:
581 var csb = new MySqlConnectionStringBuilder
583 Server = serverAddress ?? "127.0.0.1
",
588 if (serverPort.HasValue)
589 csb.Port = serverPort.Value;
591 CreateTestConnection(csb.ConnectionString);
592 csb.Database = databaseName;
593 databaseConfiguration.ConnectionString = csb.ConnectionString;
597 case DatabaseType.Sqlite:
599 var csb = new SqliteConnectionStringBuilder
601 DataSource = databaseName,
602 Mode = dbExists ? SqliteOpenMode.ReadOnly : SqliteOpenMode.ReadWriteCreate,
605 CreateTestConnection(csb.ConnectionString);
607 csb.Mode = SqliteOpenMode.ReadWriteCreate;
608 databaseConfiguration.ConnectionString = csb.ConnectionString;
612 case DatabaseType.PostgresSql:
614 var csb = new NpgsqlConnectionStringBuilder
616 ApplicationName = assemblyInformationProvider.VersionPrefix,
617 Host = serverAddress ?? "127.0.0.1
",
622 if (serverPort.HasValue)
623 csb.Port = serverPort.Value;
625 CreateTestConnection(csb.ConnectionString);
626 csb.Database = databaseName;
627 databaseConfiguration.ConnectionString = csb.ConnectionString;
632 throw new InvalidOperationException("Invalid
DatabaseType!
");
637 await TestDatabaseConnection(testConnection, databaseConfiguration, databaseName, dbExists, cancellationToken);
639 return databaseConfiguration;
641 catch (OperationCanceledException)
647 await console.WriteAsync(e.Message, true, cancellationToken);
648 await console.WriteAsync(null, true, cancellationToken);
649 await console.WriteAsync("Retrying database configuration...
", true, cancellationToken);
651 if (definitelyLocalMariaDB)
652 await console.WriteAsync("No longer assuming MariaDB is the target.
", true, cancellationToken);
659#pragma warning restore CA1502
666 async ValueTask<GeneralConfiguration> ConfigureGeneral(CancellationToken cancellationToken)
668 var newGeneralConfiguration = new GeneralConfiguration
670 SetupWizardMode = SetupWizardMode.Never,
675 await console.WriteAsync(null, true, cancellationToken);
676 await console.WriteAsync(String.Format(CultureInfo.InvariantCulture, "Minimum database user password length (leave blank
for default of {0}):
", newGeneralConfiguration.MinimumPasswordLength), false, cancellationToken);
677 var passwordLengthString = await console.ReadLineAsync(false, cancellationToken);
678 if (String.IsNullOrWhiteSpace(passwordLengthString))
680 if (UInt32.TryParse(passwordLengthString, out var passwordLength) && passwordLength >= 0)
682 newGeneralConfiguration.MinimumPasswordLength = passwordLength;
686 await console.WriteAsync("Please enter a positive integer!
", true, cancellationToken);
692 await console.WriteAsync(null, true, cancellationToken);
693 await console.WriteAsync(String.Format(CultureInfo.InvariantCulture, "Default timeout
for sending and receiving BYOND topics (ms, 0
for infinite, leave blank
for default of {0}):
", newGeneralConfiguration.ByondTopicTimeout), false, cancellationToken);
694 var topicTimeoutString = await console.ReadLineAsync(false, cancellationToken);
695 if (String.IsNullOrWhiteSpace(topicTimeoutString))
697 if (UInt32.TryParse(topicTimeoutString, out var topicTimeout) && topicTimeout >= 0)
699 newGeneralConfiguration.ByondTopicTimeout = topicTimeout;
703 await console.WriteAsync("Please enter a positive integer!
", true, cancellationToken);
707 await console.WriteAsync(null, true, cancellationToken);
708 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);
709 await console.WriteAsync("GitHub personal access token:
", false, cancellationToken);
710 newGeneralConfiguration.GitHubAccessToken = await console.ReadLineAsync(true, cancellationToken);
711 if (String.IsNullOrWhiteSpace(newGeneralConfiguration.GitHubAccessToken))
712 newGeneralConfiguration.GitHubAccessToken = null;
714 newGeneralConfiguration.HostApiDocumentation = await PromptYesNo("Host API Documentation?
", false, cancellationToken);
716 return newGeneralConfiguration;
724 async ValueTask<FileLoggingConfiguration> ConfigureLogging(CancellationToken cancellationToken)
726 var fileLoggingConfiguration = new FileLoggingConfiguration();
727 await console.WriteAsync(null, true, cancellationToken);
728 fileLoggingConfiguration.Disable = !await PromptYesNo("Enable file logging?
", true, cancellationToken);
730 if (!fileLoggingConfiguration.Disable)
734 await console.WriteAsync("Log file directory path (leave blank
for default):
", false, cancellationToken);
735 fileLoggingConfiguration.Directory = await console.ReadLineAsync(false, cancellationToken);
736 if (String.IsNullOrWhiteSpace(fileLoggingConfiguration.Directory))
738 fileLoggingConfiguration.Directory = null;
742 // test a write of it
743 await console.WriteAsync(null, true, cancellationToken);
744 await console.WriteAsync("Testing directory access...
", true, cancellationToken);
747 await ioManager.CreateDirectory(fileLoggingConfiguration.Directory, cancellationToken);
748 var testFile = ioManager.ConcatPath(fileLoggingConfiguration.Directory, String.Format(CultureInfo.InvariantCulture, "WizardAccesTest.{0}.deleteme
", Guid.NewGuid()));
749 await ioManager.WriteAllBytes(testFile, Array.Empty<byte>(), cancellationToken);
752 await ioManager.DeleteFile(testFile, cancellationToken);
754 catch (OperationCanceledException)
760 await console.WriteAsync(String.Format(CultureInfo.InvariantCulture, "Error deleting test log file: {0}
", testFile), true, cancellationToken);
761 await console.WriteAsync(e.Message, true, cancellationToken);
762 await console.WriteAsync(null, true, cancellationToken);
767 catch (OperationCanceledException)
773 await console.WriteAsync(e.Message, true, cancellationToken);
774 await console.WriteAsync(null, true, cancellationToken);
775 await console.WriteAsync("Please verify the path is valid and you have access to it!
", true, cancellationToken);
780 async ValueTask<LogLevel?> PromptLogLevel(string question)
784 await console.WriteAsync(null, true, cancellationToken);
785 await console.WriteAsync(question, true, cancellationToken);
786 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);
787 var responseString = await console.ReadLineAsync(false, cancellationToken);
788 if (String.IsNullOrWhiteSpace(responseString))
790 if (Enum.TryParse<LogLevel>(responseString, out var logLevel) && logLevel != LogLevel.None)
792 await console.WriteAsync("Invalid log level!
", true, cancellationToken);
797 fileLoggingConfiguration.LogLevel = await PromptLogLevel(String.Format(CultureInfo.InvariantCulture, "Enter the level limit
for normal logs (
default {0}).
", fileLoggingConfiguration.LogLevel)) ?? fileLoggingConfiguration.LogLevel;
798 fileLoggingConfiguration.MicrosoftLogLevel = await PromptLogLevel(String.Format(CultureInfo.InvariantCulture, "Enter the level limit
for Microsoft logs (VERY verbose,
default {0}).
", fileLoggingConfiguration.MicrosoftLogLevel)) ?? fileLoggingConfiguration.MicrosoftLogLevel;
801 return fileLoggingConfiguration;
809 async ValueTask<ElasticsearchConfiguration> ConfigureElasticsearch(CancellationToken cancellationToken)
811 var elasticsearchConfiguration = new ElasticsearchConfiguration();
812 await console.WriteAsync(null, true, cancellationToken);
813 elasticsearchConfiguration.Enable = await PromptYesNo("Enable logging to an external ElasticSearch server?
", false, cancellationToken);
815 if (elasticsearchConfiguration.Enable)
819 await console.WriteAsync("ElasticSearch server endpoint (Include protocol and port, leave blank
for http:
821 if (String.IsNullOrWhiteSpace(hostString))
822 hostString =
"http://127.0.0.1:9200";
824 if (Uri.TryCreate(hostString, UriKind.Absolute, out var host))
826 elasticsearchConfiguration.Host = host;
836 await
console.
WriteAsync(
"Enter Elasticsearch username: ",
false, cancellationToken);
838 if (!String.IsNullOrWhiteSpace(elasticsearchConfiguration.Username))
847 if (!String.IsNullOrWhiteSpace(elasticsearchConfiguration.Username))
853 return elasticsearchConfiguration;
865 Enable = await PromptYesNo(
"Enable the web control panel?",
true, cancellationToken),
866 AllowAnyOrigin = await PromptYesNo(
867 "Allow web control panels hosted elsewhere to access the server? (Access-Control-Allow-Origin: *)",
872 if (!config.AllowAnyOrigin)
874 await console.WriteAsync(
"Enter a comma seperated list of CORS allowed origins (optional): ",
false, cancellationToken);
875 var commaSeperatedOrigins = await console.ReadLineAsync(
false, cancellationToken);
876 if (!String.IsNullOrWhiteSpace(commaSeperatedOrigins))
878 var splits = commaSeperatedOrigins.Split(
',');
879 config.AllowedOrigins =
new List<string>(splits.Select(x => x.Trim()));
891 async ValueTask<SwarmConfiguration?>
ConfigureSwarm(CancellationToken cancellationToken)
893 var enable = await PromptYesNo(
"Enable swarm mode?",
false, cancellationToken);
900 await console.WriteAsync(
"Enter this server's identifer: ",
false, cancellationToken);
901 identifer = await console.ReadLineAsync(
false, cancellationToken);
903 while (String.IsNullOrWhiteSpace(identifer));
905 async ValueTask<Uri> ParseAddress(
string question)
914 await console.WriteAsync(
"Invalid address!",
true, cancellationToken);
916 await console.WriteAsync(question,
false, cancellationToken);
917 var addressString = await console.ReadLineAsync(
false, cancellationToken);
918 if (Uri.TryCreate(addressString, UriKind.Absolute, out address)
919 && address.Scheme != Uri.UriSchemeHttp
920 && address.Scheme != Uri.UriSchemeHttps)
923 while (address ==
null);
928 var address = await ParseAddress(
"Enter this server's INTERNAL http(s) address: ");
929 var publicAddress = await ParseAddress(
"Enter this server's PUBLIC https(s) address: ");
933 await console.WriteAsync(
"Enter the swarm private key: ",
false, cancellationToken);
934 privateKey = await console.ReadLineAsync(
false, cancellationToken);
936 while (String.IsNullOrWhiteSpace(privateKey));
938 var controller = await PromptYesNo(
"Is this server the swarm's controller? (y/n): ",
null, cancellationToken);
939 Uri? controllerAddress =
null;
941 controllerAddress = await ParseAddress(
"Enter the swarm controller's HTTP(S) address: ");
946 PublicAddress = publicAddress,
947 ControllerAddress = controllerAddress,
948 Identifier = identifer,
949 PrivateKey = privateKey,
967 string userConfigFileName,
975 CancellationToken cancellationToken)
979 var map =
new Dictionary<string, object?>()
990 var builder =
new SerializerBuilder()
991 .WithTypeConverter(versionConverter);
993 if (userConfigFileName.EndsWith(
".json", StringComparison.OrdinalIgnoreCase))
994 builder.JsonCompatible();
996 var serializer =
new SerializerBuilder()
997 .WithTypeConverter(versionConverter)
1000 var serializedYaml = serializer.Serialize(map);
1003 serializedYaml = serializedYaml.Replace(
1004 $
"\n {nameof(ControlPanelConfiguration.Channel)}: ",
1006 StringComparison.Ordinal)
1007 .Replace(
"\r", String.Empty, StringComparison.Ordinal);
1009 var configBytes = Encoding.UTF8.GetBytes(serializedYaml);
1013 await ioManager.WriteAllBytes(
1018 catch (
Exception e) when (e is not OperationCanceledException)
1020 await console.WriteAsync(e.Message,
true, cancellationToken);
1021 await console.WriteAsync(
null,
true, cancellationToken);
1022 await console.WriteAsync(
"For your convienence, here's the yaml we tried to write out:",
true, cancellationToken);
1023 await console.WriteAsync(
null,
true, cancellationToken);
1024 await console.WriteAsync(serializedYaml,
true, cancellationToken);
1025 await console.WriteAsync(
null,
true, cancellationToken);
1026 await console.WriteAsync(
"Press any key to exit...",
true, cancellationToken);
1027 await console.PressAnyKeyAsync(cancellationToken);
1028 throw new OperationCanceledException();
1038 async ValueTask
RunWizard(
string userConfigFileName, CancellationToken cancellationToken)
1041 await console.WriteAsync($
"Welcome to {Constants.CanonicalPackageName}!",
true, cancellationToken);
1042 await console.WriteAsync(
"This wizard will help you configure your server.",
true, cancellationToken);
1044 var hostingPort = await PromptForHostingPort(cancellationToken);
1046 var databaseConfiguration = await ConfigureDatabase(cancellationToken);
1056 var swarmConfiguration = await
ConfigureSwarm(cancellationToken);
1058 await console.WriteAsync(
null,
true, cancellationToken);
1059 await console.WriteAsync(String.Format(CultureInfo.InvariantCulture,
"Configuration complete! Saving to {0}", userConfigFileName),
true, cancellationToken);
1064 databaseConfiguration,
1065 newGeneralConfiguration,
1066 fileLoggingConfiguration,
1067 elasticSearchConfiguration,
1068 controlPanelConfiguration,
1080 var setupWizardMode = generalConfiguration.SetupWizardMode;
1081 if (setupWizardMode == SetupWizardMode.Never)
1084 var forceRun = setupWizardMode == SetupWizardMode.Force || setupWizardMode == SetupWizardMode.Only;
1085 if (!console.Available)
1088 throw new InvalidOperationException(
"Asked to run setup wizard with no console avaliable!");
1092 var userConfigFileName = ioManager.ConcatPath(
1093 internalConfiguration.AppSettingsBasePath,
1094 $
"{ServerFactory.AppSettings}.{hostingEnvironment.EnvironmentName}.yml");
1096 async Task HandleSetupCancel()
1099 await console.WriteAsync(String.Empty,
true,
default);
1100 await console.WriteAsync(
"Aborting setup!",
true,
default);
1103 Task finalTask = Task.CompletedTask;
1104 string? originalConsoleTitle =
null;
1105 void SetConsoleTitle()
1107 if (originalConsoleTitle !=
null)
1110 originalConsoleTitle = console.Title;
1111 console.SetTitle($
"{assemblyInformationProvider.VersionString} Setup Wizard");
1115 using (var cts = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken, console.CancelKeyPress))
1116 using ((cancellationToken = cts.Token).Register(() => finalTask = HandleSetupCancel()))
1119 var exists = await ioManager.FileExists(userConfigFileName, cancellationToken);
1122 var legacyJsonFileName = $
"appsettings.{hostingEnvironment.EnvironmentName}.json";
1123 exists = await ioManager.FileExists(legacyJsonFileName, cancellationToken);
1125 userConfigFileName = legacyJsonFileName;
1128 bool shouldRunBasedOnAutodetect;
1131 var bytes = await ioManager.ReadAllBytes(userConfigFileName, cancellationToken);
1132 var contents = Encoding.UTF8.GetString(bytes);
1133 var lines = contents.Split(
'\n', StringSplitOptions.RemoveEmptyEntries);
1134 var existingConfigIsEmpty = lines
1135 .Select(line => line.Trim())
1136 .All(line => line[0] ==
'#' || line ==
"{}" || line.Length == 0);
1137 shouldRunBasedOnAutodetect = existingConfigIsEmpty;
1140 shouldRunBasedOnAutodetect =
true;
1142 if (!shouldRunBasedOnAutodetect)
1147 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);
1149 forceRun = await PromptYesNo(
"Continue running setup wizard?",
false, cancellationToken);
1158 if (!String.IsNullOrEmpty(internalConfiguration.MariaDBDefaultRootPassword))
1161 var csb =
new MySqlConnectionStringBuilder
1165 Password = internalConfiguration.MariaDBDefaultRootPassword,
1184 AllowAnyOrigin =
true,
1192 await asyncDelayer.Delay(TimeSpan.FromSeconds(1), cancellationToken);
1194 await
RunWizard(userConfigFileName, cancellationToken);
1200 if (originalConsoleTitle !=
null)
1201 console.SetTitle(originalConsoleTitle);
async ValueTask CheckRunWizard(CancellationToken cancellationToken)
Check if it should and run the SetupWizard if necessary.
async ValueTask< ControlPanelConfiguration > ConfigureControlPanel(CancellationToken cancellationToken)
Prompts the user to create a ControlPanelConfiguration.
async ValueTask< GeneralConfiguration > ConfigureGeneral(CancellationToken cancellationToken)
Prompts the user to create a GeneralConfiguration.
async ValueTask SaveConfiguration(string userConfigFileName, ushort? hostingPort, DatabaseConfiguration databaseConfiguration, GeneralConfiguration newGeneralConfiguration, FileLoggingConfiguration? fileLoggingConfiguration, ElasticsearchConfiguration? elasticsearchConfiguration, ControlPanelConfiguration controlPanelConfiguration, SwarmConfiguration? swarmConfiguration, CancellationToken cancellationToken)
Saves a given Configuration set to userConfigFileName .
async ValueTask< FileLoggingConfiguration > ConfigureLogging(CancellationToken cancellationToken)
Prompts the user to create a FileLoggingConfiguration.
async ValueTask< SwarmConfiguration?> ConfigureSwarm(CancellationToken cancellationToken)
Prompts the user to create a SwarmConfiguration.
async ValueTask RunWizard(string userConfigFileName, CancellationToken cancellationToken)
Runs the SetupWizard.
async ValueTask< ElasticsearchConfiguration > ConfigureElasticsearch(CancellationToken cancellationToken)
Prompts the user to create a ElasticsearchConfiguration.
Configuration options for the web control panel.
const string Section
The key for the Microsoft.Extensions.Configuration.IConfigurationSection the ControlPanelConfiguratio...
Configuration options for the Database.DatabaseContext.
const string Section
The key for the Microsoft.Extensions.Configuration.IConfigurationSection the DatabaseConfiguration re...
string? ConnectionString
The connection string for the database.
Configuration options pertaining to elasticsearch log storage.
const string Section
The key for the Microsoft.Extensions.Configuration.IConfigurationSection the ElasticsearchConfigurati...
File logging configuration options.
const string Section
The key for the Microsoft.Extensions.Configuration.IConfigurationSection the FileLoggingConfiguration...
General configuration options.
const string Section
The key for the Microsoft.Extensions.Configuration.IConfigurationSection the GeneralConfiguration res...
static readonly Version CurrentConfigVersion
The current ConfigVersion.
const ushort DefaultApiPort
The default value of ApiPort.
Unstable configuration options used internally by TGS.
bool MariaDBSetup
Coerce the Setup.SetupWizard to select DatabaseType.MariaDB.
string AppSettingsBasePath
The base path for the app settings configuration files.
Configuration for the server swarm system.
const string Section
The key for the Microsoft.Extensions.Configuration.IConfigurationSection the SwarmConfiguration resid...
Attribute for bringing in the master versions list from MSBuild that aren't embedded into assemblies ...
string RawMariaDBRedistVersion
The Version string of the MariaDB server bundled with TGS installs.
static MasterVersionsAttribute Instance
Return the Assembly's instance of the MasterVersionsAttribute.
readonly IIOManager ioManager
The IIOManager for the SetupWizard.
readonly InternalConfiguration internalConfiguration
The InternalConfiguration for the SetupWizard.
async ValueTask< ushort?> PromptForHostingPort(CancellationToken cancellationToken)
Prompts the user to enter the port to host TGS on.
readonly IHostEnvironment hostingEnvironment
The IHostEnvironment for the SetupWizard.
readonly GeneralConfiguration generalConfiguration
The GeneralConfiguration for the SetupWizard.
async ValueTask< DatabaseConfiguration > ConfigureDatabase(CancellationToken cancellationToken)
Prompts the user to create a DatabaseConfiguration.
SetupWizard(IIOManager ioManager, IConsole console, IHostEnvironment hostingEnvironment, IAssemblyInformationProvider assemblyInformationProvider, IDatabaseConnectionFactory dbConnectionFactory, IPlatformIdentifier platformIdentifier, IAsyncDelayer asyncDelayer, IHostApplicationLifetime applicationLifetime, IOptions< GeneralConfiguration > generalConfigurationOptions, IOptions< InternalConfiguration > internalConfigurationOptions)
Initializes a new instance of the SetupWizard class.
readonly IAssemblyInformationProvider assemblyInformationProvider
The IAssemblyInformationProvider for the SetupWizard.
return databaseConfiguration
readonly IPlatformIdentifier platformIdentifier
The IPlatformIdentifier for the SetupWizard.
readonly IDatabaseConnectionFactory dbConnectionFactory
The IDatabaseConnectionFactory for the SetupWizard.
override async Task ExecuteAsync(CancellationToken cancellationToken)
readonly IConsole console
The IConsole for the SetupWizard.
async ValueTask< bool > PromptYesNo(string question, bool? defaultResponse, CancellationToken cancellationToken)
A prompt for a yes or no value.
readonly IHostApplicationLifetime applicationLifetime
The IHostApplicationLifetime for the SetupWizard.
async ValueTask< DatabaseType > PromptDatabaseType(bool firstTime, CancellationToken cancellationToken)
Prompt the user for the DatabaseType.
DbConnection testConnection
readonly IAsyncDelayer asyncDelayer
The IAsyncDelayer for the SetupWizard.
async ValueTask< string?> ValidateNonExistantSqliteDBName(string databaseName, CancellationToken cancellationToken)
Check that a given SQLite databaseName is can be accessed. Also prompts the user if they want to use...
async ValueTask TestDatabaseConnection(DbConnection testConnection, DatabaseConfiguration databaseConfiguration, string databaseName, bool dbExists, CancellationToken cancellationToken)
Ensure a given testConnection works.
JsonConverter and IYamlTypeConverter for serializing global::System.Versions in semver format.
For creating raw DbConnections.
Abstraction for global::System.Console.
Task WriteAsync(string? text, bool newLine, CancellationToken cancellationToken)
Write some text to the IConsole.
Task< string > ReadLineAsync(bool usePasswordChar, CancellationToken cancellationToken)
Read a line from the IConsole.
Task PressAnyKeyAsync(CancellationToken cancellationToken)
Wait for a key press on the IConsole.
Interface for using filesystems.
string ResolvePath()
Retrieve the full path of the current working directory.
string ConcatPath(params string[] paths)
Combines an array of strings into a path.
string GetDirectoryName(string path)
Gets the directory portion of a given path .
Task CreateDirectory(string path, CancellationToken cancellationToken)
Create a directory at path .
Task DeleteFile(string path, CancellationToken cancellationToken)
Deletes a file at path .
ValueTask WriteAllBytes(string path, byte[] contents, CancellationToken cancellationToken)
Writes some contents to a file at path overwriting previous content.
Task DeleteDirectory(string path, CancellationToken cancellationToken)
Recursively delete a directory, removes and does not enter any symlinks encounterd.
Task< bool > DirectoryExists(string path, CancellationToken cancellationToken)
Check that the directory at path exists.
For waiting asynchronously.
DatabaseType
Type of database to user.