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?>()
989 var builder =
new SerializerBuilder()
992 if (userConfigFileName.EndsWith(
".json", StringComparison.OrdinalIgnoreCase))
993 builder.JsonCompatible();
995 var serializer =
new SerializerBuilder()
999 var serializedYaml = serializer.Serialize(map);
1002 serializedYaml = serializedYaml.Replace(
1003 $
"\n {nameof(ControlPanelConfiguration.Channel)}: ",
1005 StringComparison.Ordinal)
1006 .Replace(
"\r", String.Empty, StringComparison.Ordinal);
1008 var configBytes = Encoding.UTF8.GetBytes(serializedYaml);
1012 await ioManager.WriteAllBytes(
1017 catch (
Exception e) when (e is not OperationCanceledException)
1019 await console.WriteAsync(e.Message,
true, cancellationToken);
1020 await console.WriteAsync(
null,
true, cancellationToken);
1021 await console.WriteAsync(
"For your convienence, here's the yaml we tried to write out:",
true, cancellationToken);
1022 await console.WriteAsync(
null,
true, cancellationToken);
1023 await console.WriteAsync(serializedYaml,
true, cancellationToken);
1024 await console.WriteAsync(
null,
true, cancellationToken);
1025 await console.WriteAsync(
"Press any key to exit...",
true, cancellationToken);
1026 await console.PressAnyKeyAsync(cancellationToken);
1027 throw new OperationCanceledException();
1037 async ValueTask
RunWizard(
string userConfigFileName, CancellationToken cancellationToken)
1040 await console.WriteAsync($
"Welcome to {Constants.CanonicalPackageName}!",
true, cancellationToken);
1041 await console.WriteAsync(
"This wizard will help you configure your server.",
true, cancellationToken);
1043 var hostingPort = await PromptForHostingPort(cancellationToken);
1045 var databaseConfiguration = await ConfigureDatabase(cancellationToken);
1055 var swarmConfiguration = await
ConfigureSwarm(cancellationToken);
1057 await console.WriteAsync(
null,
true, cancellationToken);
1058 await console.WriteAsync(String.Format(CultureInfo.InvariantCulture,
"Configuration complete! Saving to {0}", userConfigFileName),
true, cancellationToken);
1063 databaseConfiguration,
1064 newGeneralConfiguration,
1065 fileLoggingConfiguration,
1066 elasticSearchConfiguration,
1067 controlPanelConfiguration,
1079 var setupWizardMode = generalConfiguration.SetupWizardMode;
1080 if (setupWizardMode == SetupWizardMode.Never)
1083 var forceRun = setupWizardMode == SetupWizardMode.Force || setupWizardMode == SetupWizardMode.Only;
1084 if (!console.Available)
1087 throw new InvalidOperationException(
"Asked to run setup wizard with no console avaliable!");
1091 var userConfigFileName = ioManager.ConcatPath(
1092 internalConfiguration.AppSettingsBasePath,
1093 $
"{ServerFactory.AppSettings}.{hostingEnvironment.EnvironmentName}.yml");
1095 async Task HandleSetupCancel()
1098 await console.WriteAsync(String.Empty,
true,
default);
1099 await console.WriteAsync(
"Aborting setup!",
true,
default);
1102 Task finalTask = Task.CompletedTask;
1103 string? originalConsoleTitle =
null;
1104 void SetConsoleTitle()
1106 if (originalConsoleTitle !=
null)
1109 originalConsoleTitle = console.Title;
1110 console.SetTitle($
"{assemblyInformationProvider.VersionString} Setup Wizard");
1114 using (var cts = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken, console.CancelKeyPress))
1115 using ((cancellationToken = cts.Token).Register(() => finalTask = HandleSetupCancel()))
1118 var exists = await ioManager.FileExists(userConfigFileName, cancellationToken);
1121 var legacyJsonFileName = $
"appsettings.{hostingEnvironment.EnvironmentName}.json";
1122 exists = await ioManager.FileExists(legacyJsonFileName, cancellationToken);
1124 userConfigFileName = legacyJsonFileName;
1127 bool shouldRunBasedOnAutodetect;
1130 var bytes = await ioManager.ReadAllBytes(userConfigFileName, cancellationToken);
1131 var contents = Encoding.UTF8.GetString(bytes);
1132 var lines = contents.Split(
'\n', StringSplitOptions.RemoveEmptyEntries);
1133 var existingConfigIsEmpty = lines
1134 .Select(line => line.Trim())
1135 .All(line => line[0] ==
'#' || line ==
"{}" || line.Length == 0);
1136 shouldRunBasedOnAutodetect = existingConfigIsEmpty;
1139 shouldRunBasedOnAutodetect =
true;
1141 if (!shouldRunBasedOnAutodetect)
1146 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);
1148 forceRun = await PromptYesNo(
"Continue running setup wizard?",
false, cancellationToken);
1157 if (!String.IsNullOrEmpty(internalConfiguration.MariaDBDefaultRootPassword))
1160 var csb =
new MySqlConnectionStringBuilder
1164 Password = internalConfiguration.MariaDBDefaultRootPassword,
1183 AllowAnyOrigin =
true,
1191 await asyncDelayer.Delay(TimeSpan.FromSeconds(1), cancellationToken);
1193 await
RunWizard(userConfigFileName, cancellationToken);
1199 if (originalConsoleTitle !=
null)
1200 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...
JsonConverter and IYamlTypeConverter for serializing global::System.Versions in semver format.
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.
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.