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;
118 IOptions<GeneralConfiguration> generalConfigurationOptions,
119 IOptions<InternalConfiguration> internalConfigurationOptions)
122 this.console =
console ??
throw new ArgumentNullException(nameof(
console));
131 generalConfiguration = generalConfigurationOptions?.Value ??
throw new ArgumentNullException(nameof(generalConfigurationOptions));
132 internalConfiguration = internalConfigurationOptions?.Value ??
throw new ArgumentNullException(nameof(internalConfigurationOptions));
136 protected override async Task
ExecuteAsync(CancellationToken cancellationToken)
149 async ValueTask<bool>
PromptYesNo(
string question,
bool? defaultResponse, CancellationToken cancellationToken)
153 await
console.
WriteAsync($
"{question} ({(defaultResponse == true ? 'Y' : 'y')}/{(defaultResponse == false ? 'N' : 'n')}): ",
false, cancellationToken);
155 if (responseString.Length == 0)
157 if (defaultResponse.HasValue)
158 return defaultResponse.Value;
162 var upperResponse = responseString.ToUpperInvariant();
163 if (upperResponse ==
"Y" || upperResponse ==
"YES")
165 else if (upperResponse ==
"N" || upperResponse ==
"NO")
182 await
console.
WriteAsync(
"What port would you like to connect to TGS on?",
true, cancellationToken);
183 await
console.
WriteAsync(
"Note: If this is a docker container with the default port already mapped, use the default.",
true, cancellationToken);
188 $
"API Port (leave blank for default of {GeneralConfiguration.DefaultApiPort}): ",
192 if (String.IsNullOrWhiteSpace(portString))
194 if (UInt16.TryParse(portString, out var port) && port != 0)
196 await
console.
WriteAsync(
"Invalid port! Please enter a value between 1 and 65535",
true, cancellationToken);
211 DbConnection testConnection,
215 CancellationToken cancellationToken)
217 bool isSqliteDB = databaseConfiguration.DatabaseType ==
DatabaseType.Sqlite;
218 using (testConnection)
221 await testConnection.OpenAsync(cancellationToken);
228 await
console.
WriteAsync($
"Checking {databaseConfiguration.DatabaseType} version...",
true, cancellationToken);
229 using var command = testConnection.CreateCommand();
230 command.CommandText =
"SELECT VERSION()";
231 var fullVersion = (
string?)await command.ExecuteScalarAsync(cancellationToken);
232 await
console.
WriteAsync(String.Format(CultureInfo.InvariantCulture,
"Found {0}", fullVersion),
true, cancellationToken);
234 if (fullVersion ==
null)
235 throw new InvalidOperationException($
"\"{command.CommandText}\" returned null!");
239 var splits = fullVersion.Split(
' ');
240 databaseConfiguration.ServerVersion = splits[1].TrimEnd(
',');
244 var splits = fullVersion.Split(
'-');
245 databaseConfiguration.ServerVersion = splits.First();
249 if (!isSqliteDB && !dbExists)
251 await
console.
WriteAsync(
"Testing create DB permission...",
true, cancellationToken);
252 using (var command = testConnection.CreateCommand())
255#pragma warning disable CA2100
256 command.CommandText = $
"CREATE DATABASE {databaseName}";
257#pragma warning restore CA2100
258 await command.ExecuteNonQueryAsync(cancellationToken);
263 using (var command = testConnection.CreateCommand())
265#pragma warning disable CA2100
266 command.CommandText = $
"DROP DATABASE {databaseName}";
267#pragma warning restore CA2100
270 await command.ExecuteNonQueryAsync(cancellationToken);
272 catch (OperationCanceledException)
280 await
console.
WriteAsync(
"This should be okay, but you may want to manually drop the database before continuing!",
true, cancellationToken);
281 await
console.
WriteAsync(
"Press any key to continue...",
true, cancellationToken);
287 await testConnection.CloseAsync();
290 if (isSqliteDB && !dbExists)
292 await
console.
WriteAsync(
"Deleting test database file...",
true, cancellationToken);
294 SqliteConnection.ClearAllPools();
307 var dbPathIsRooted = Path.IsPathRooted(databaseName);
325 if (!directoryExisted)
337 await
console.
WriteAsync(
"Note, this relative path currently resolves to the following:",
true, cancellationToken);
340 "Would you like to save the relative path in the configuration? If not, the full path will be saved.",
345 databaseName = resolvedPath;
358 async ValueTask<DatabaseType>
PromptDatabaseType(
bool firstTime, CancellationToken cancellationToken)
364 await
console.
WriteAsync(
"It looks like you just installed MariaDB. Selecting it as the database type.",
true, cancellationToken);
370 "NOTE: If you are serious about hosting public servers, it is HIGHLY reccommended that TGS runs on a database *OTHER THAN* Sqlite.",
374 "It is, however, the easiest option to get started with and will pose few if any problems in a single user scenario.",
379 await
console.
WriteAsync(
"What SQL database type will you be using?",
true, cancellationToken);
384 CultureInfo.InvariantCulture,
385 "Please enter one of {0}, {1}, {2}, {3} or {4}: ",
394 if (Enum.TryParse<
DatabaseType>(databaseTypeString, out var databaseType))
407#pragma warning disable CA1502
410 bool firstTime =
true;
420 string? serverAddress =
null;
421 ushort? serverPort =
null;
424 var isSqliteDB = databaseConfiguration.DatabaseType ==
DatabaseType.Sqlite;
425 IPHostEntry? serverAddressEntry =
null;
430 if (definitelyLocalMariaDB)
432 await
console.
WriteAsync(
"Enter the server's port (blank for 3306): ",
false, cancellationToken);
434 if (!String.IsNullOrWhiteSpace(enteredPort) && enteredPort.Trim() !=
"3306")
435 serverAddress = $
"localhost:{enteredPort}";
439 await
console.
WriteAsync(
"Enter the server's address and port [<server>:<port> or <server>] (blank for local): ",
false, cancellationToken);
443 if (String.IsNullOrWhiteSpace(serverAddress))
444 serverAddress =
null;
445 else if (databaseConfiguration.DatabaseType ==
DatabaseType.SqlServer)
447 var match = Regex.Match(serverAddress,
@"^(?<server>.+):(?<port>.+)$");
450 serverAddress = match.Groups[
"server"].Value;
451 var portString = match.Groups[
"port"].Value;
452 if (UInt16.TryParse(portString, out var port))
456 await
console.
WriteAsync($
"Failed to parse port \"{portString}\", please try again.",
true, cancellationToken);
464 if (serverAddress !=
null)
466 await
console.
WriteAsync(
"Attempting to resolve address...",
true, cancellationToken);
467 serverAddressEntry = await Dns.GetHostEntryAsync(serverAddress, cancellationToken);
474 await
console.
WriteAsync($
"Unable to resolve address: {ex.Message}",
true, cancellationToken);
480 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);
482 string? databaseName;
483 bool dbExists = false;
486 databaseName = await console.ReadLineAsync(false, cancellationToken);
487 if (!String.IsNullOrWhiteSpace(databaseName))
491 dbExists = await ioManager.FileExists(databaseName, cancellationToken);
493 databaseName = await ValidateNonExistantSqliteDBName(databaseName, cancellationToken);
496 dbExists = await PromptYesNo(
497 "Does
this database already exist? If not, we will attempt to CREATE it.
",
501 else if (definitelyLocalMariaDB)
502 databaseName = "tgs
";
504 if (String.IsNullOrWhiteSpace(databaseName))
505 await console.WriteAsync("Invalid database name!
", true, cancellationToken);
511 var useWinAuth = false;
513 if (databaseConfiguration.DatabaseType == DatabaseType.SqlServer && platformIdentifier.IsWindows)
515 var defaultResponse = serverAddressEntry?.AddressList.Any(IPAddress.IsLoopback) ?? false
518 useWinAuth = await PromptYesNo("Use Windows Authentication?
", defaultResponse, cancellationToken);
519 encrypt = await PromptYesNo("Use encrypted connection?
", false, cancellationToken);
522 await console.WriteAsync(null, true, cancellationToken);
524 string? username = null;
525 string? password = null;
529 if (definitelyLocalMariaDB)
531 await console.WriteAsync("Using username: root
", true, cancellationToken);
536 await console.WriteAsync("Enter username:
", false, cancellationToken);
537 username = await console.ReadLineAsync(false, cancellationToken);
540 await console.WriteAsync("Enter password:
", false, cancellationToken);
541 password = await console.ReadLineAsync(true, cancellationToken);
545 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);
546 await console.WriteAsync("The account it uses in MSSQL is usually \"NT AUTHORITY\\SYSTEM\" and the role it needs is usually \"dbcreator\".", true, cancellationToken);
547 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);
550 await console.WriteAsync(null, true, cancellationToken);
552 DbConnection testConnection;
553 void CreateTestConnection(string connectionString) =>
554 testConnection = dbConnectionFactory.CreateConnection(
556 databaseConfiguration.DatabaseType);
558 switch (databaseConfiguration.DatabaseType)
560 case DatabaseType.SqlServer:
562 var csb = new SqlConnectionStringBuilder
564 ApplicationName = assemblyInformationProvider.VersionPrefix,
565 DataSource = serverAddress ?? "(local)
",
570 csb.IntegratedSecurity = true;
573 csb.UserID = username;
574 csb.Password = password;
577 csb.Encrypt = encrypt;
579 CreateTestConnection(csb.ConnectionString);
580 csb.InitialCatalog = databaseName;
581 databaseConfiguration.ConnectionString = csb.ConnectionString;
585 case DatabaseType.MariaDB:
586 case DatabaseType.MySql:
589 var csb = new MySqlConnectionStringBuilder
591 Server = serverAddress ?? "127.0.0.1
",
596 if (serverPort.HasValue)
597 csb.Port = serverPort.Value;
599 CreateTestConnection(csb.ConnectionString);
600 csb.Database = databaseName;
601 databaseConfiguration.ConnectionString = csb.ConnectionString;
605 case DatabaseType.Sqlite:
607 var csb = new SqliteConnectionStringBuilder
609 DataSource = databaseName,
610 Mode = dbExists ? SqliteOpenMode.ReadOnly : SqliteOpenMode.ReadWriteCreate,
613 CreateTestConnection(csb.ConnectionString);
615 csb.Mode = SqliteOpenMode.ReadWriteCreate;
616 databaseConfiguration.ConnectionString = csb.ConnectionString;
620 case DatabaseType.PostgresSql:
622 var csb = new NpgsqlConnectionStringBuilder
624 ApplicationName = assemblyInformationProvider.VersionPrefix,
625 Host = serverAddress ?? "127.0.0.1
",
630 if (serverPort.HasValue)
631 csb.Port = serverPort.Value;
633 CreateTestConnection(csb.ConnectionString);
634 csb.Database = databaseName;
635 databaseConfiguration.ConnectionString = csb.ConnectionString;
640 throw new InvalidOperationException("Invalid
DatabaseType!
");
645 await TestDatabaseConnection(testConnection, databaseConfiguration, databaseName, dbExists, cancellationToken);
647 return databaseConfiguration;
649 catch (OperationCanceledException)
655 await console.WriteAsync(e.Message, true, cancellationToken);
656 await console.WriteAsync(null, true, cancellationToken);
657 await console.WriteAsync("Retrying database configuration...
", true, cancellationToken);
659 if (definitelyLocalMariaDB)
660 await console.WriteAsync("No longer assuming
MariaDB is the target.
", true, cancellationToken);
667#pragma warning restore CA1502
674 async ValueTask<GeneralConfiguration> ConfigureGeneral(CancellationToken cancellationToken)
676 var newGeneralConfiguration = new GeneralConfiguration
678 SetupWizardMode = SetupWizardMode.Never,
683 await console.WriteAsync(null, true, cancellationToken);
684 await console.WriteAsync(String.Format(CultureInfo.InvariantCulture, "Minimum database user password length (leave blank
for default of {0}):
", newGeneralConfiguration.MinimumPasswordLength), false, cancellationToken);
685 var passwordLengthString = await console.ReadLineAsync(false, cancellationToken);
686 if (String.IsNullOrWhiteSpace(passwordLengthString))
688 if (UInt32.TryParse(passwordLengthString, out var passwordLength) && passwordLength >= 0)
690 newGeneralConfiguration.MinimumPasswordLength = passwordLength;
694 await console.WriteAsync("Please enter a positive integer!
", true, cancellationToken);
700 await console.WriteAsync(null, true, cancellationToken);
701 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);
702 var topicTimeoutString = await console.ReadLineAsync(false, cancellationToken);
703 if (String.IsNullOrWhiteSpace(topicTimeoutString))
705 if (UInt32.TryParse(topicTimeoutString, out var topicTimeout) && topicTimeout >= 0)
707 newGeneralConfiguration.ByondTopicTimeout = topicTimeout;
711 await console.WriteAsync("Please enter a positive integer!
", true, cancellationToken);
715 await console.WriteAsync(null, true, cancellationToken);
716 await console.WriteAsync("Enter a classic GitHub personal access token to bypass some rate limits (this is optional and does not require any scopes)
", true, cancellationToken);
717 await console.WriteAsync("GitHub personal access token:
", false, cancellationToken);
718 newGeneralConfiguration.GitHubAccessToken = await console.ReadLineAsync(true, cancellationToken);
719 if (String.IsNullOrWhiteSpace(newGeneralConfiguration.GitHubAccessToken))
720 newGeneralConfiguration.GitHubAccessToken = null;
722 newGeneralConfiguration.HostApiDocumentation = await PromptYesNo("Host API Documentation?
", false, cancellationToken);
724 return newGeneralConfiguration;
732 async ValueTask<FileLoggingConfiguration> ConfigureLogging(CancellationToken cancellationToken)
734 var fileLoggingConfiguration = new FileLoggingConfiguration();
735 await console.WriteAsync(null, true, cancellationToken);
736 fileLoggingConfiguration.Disable = !await PromptYesNo("Enable file logging?
", true, cancellationToken);
738 if (!fileLoggingConfiguration.Disable)
742 await console.WriteAsync("Log file directory path (leave blank for default):
", false, cancellationToken);
743 fileLoggingConfiguration.Directory = await console.ReadLineAsync(false, cancellationToken);
744 if (String.IsNullOrWhiteSpace(fileLoggingConfiguration.Directory))
746 fileLoggingConfiguration.Directory = null;
750 // test a write of it
751 await console.WriteAsync(null, true, cancellationToken);
752 await console.WriteAsync("Testing directory access...
", true, cancellationToken);
755 await ioManager.CreateDirectory(fileLoggingConfiguration.Directory, cancellationToken);
756 var testFile = ioManager.ConcatPath(fileLoggingConfiguration.Directory, String.Format(CultureInfo.InvariantCulture, "WizardAccesTest.{0}.deleteme
", Guid.NewGuid()));
757 await ioManager.WriteAllBytes(testFile, Array.Empty<byte>(), cancellationToken);
760 await ioManager.DeleteFile(testFile, cancellationToken);
762 catch (OperationCanceledException)
768 await console.WriteAsync(String.Format(CultureInfo.InvariantCulture, "Error deleting test log file: {0}
", testFile), true, cancellationToken);
769 await console.WriteAsync(e.Message, true, cancellationToken);
770 await console.WriteAsync(null, true, cancellationToken);
775 catch (OperationCanceledException)
781 await console.WriteAsync(e.Message, true, cancellationToken);
782 await console.WriteAsync(null, true, cancellationToken);
783 await console.WriteAsync("Please verify the path is valid and you have access to it!
", true, cancellationToken);
788 async ValueTask<LogLevel?> PromptLogLevel(string question)
792 await console.WriteAsync(null, true, cancellationToken);
793 await console.WriteAsync(question, true, cancellationToken);
794 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);
795 var responseString = await console.ReadLineAsync(false, cancellationToken);
796 if (String.IsNullOrWhiteSpace(responseString))
798 if (Enum.TryParse<LogLevel>(responseString, out var logLevel) && logLevel != LogLevel.None)
800 await console.WriteAsync("Invalid log level!
", true, cancellationToken);
805 fileLoggingConfiguration.LogLevel = await PromptLogLevel(String.Format(CultureInfo.InvariantCulture, "Enter the level limit for normal logs (default {0}).
", fileLoggingConfiguration.LogLevel)) ?? fileLoggingConfiguration.LogLevel;
806 fileLoggingConfiguration.MicrosoftLogLevel = await PromptLogLevel(String.Format(CultureInfo.InvariantCulture, "Enter the level limit for Microsoft logs (VERY verbose, default {0}).
", fileLoggingConfiguration.MicrosoftLogLevel)) ?? fileLoggingConfiguration.MicrosoftLogLevel;
809 return fileLoggingConfiguration;
817 async ValueTask<ElasticsearchConfiguration> ConfigureElasticsearch(CancellationToken cancellationToken)
819 var elasticsearchConfiguration = new ElasticsearchConfiguration();
820 await console.WriteAsync(null, true, cancellationToken);
821 elasticsearchConfiguration.Enable = await PromptYesNo("Enable logging to an external ElasticSearch server?
", false, cancellationToken);
823 if (elasticsearchConfiguration.Enable)
827 await console.WriteAsync("ElasticSearch server endpoint (Include protocol and port, leave blank for http:
828 var hostString = await
console.ReadLineAsync(false, cancellationToken);
829 if (String.IsNullOrWhiteSpace(hostString))
830 hostString =
"http://127.0.0.1:9200";
832 if (Uri.TryCreate(hostString, UriKind.Absolute, out var host))
834 elasticsearchConfiguration.Host = host;
844 await
console.
WriteAsync(
"Enter Elasticsearch username: ",
false, cancellationToken);
846 if (!String.IsNullOrWhiteSpace(elasticsearchConfiguration.Username))
855 if (!String.IsNullOrWhiteSpace(elasticsearchConfiguration.Username))
861 return elasticsearchConfiguration;
873 Enable = await PromptYesNo(
"Enable the web control panel?",
true, cancellationToken),
874 AllowAnyOrigin = await PromptYesNo(
875 "Allow web control panels hosted elsewhere to access the server? (Access-Control-Allow-Origin: *)",
880 if (!config.AllowAnyOrigin)
882 await console.WriteAsync(
"Enter a comma seperated list of CORS allowed origins (optional): ",
false, cancellationToken);
883 var commaSeperatedOrigins = await console.ReadLineAsync(
false, cancellationToken);
884 if (!String.IsNullOrWhiteSpace(commaSeperatedOrigins))
886 var splits = commaSeperatedOrigins.Split(
',');
887 config.AllowedOrigins =
new List<string>(splits.Select(x => x.Trim()));
899 async ValueTask<SwarmConfiguration?>
ConfigureSwarm(CancellationToken cancellationToken)
901 var enable = await PromptYesNo(
"Enable swarm mode?",
false, cancellationToken);
908 await console.WriteAsync(
"Enter this server's identifer: ",
false, cancellationToken);
909 identifer = await console.ReadLineAsync(
false, cancellationToken);
911 while (String.IsNullOrWhiteSpace(identifer));
913 async ValueTask<Uri> ParseAddress(
string question)
922 await console.WriteAsync(
"Invalid address!",
true, cancellationToken);
924 await console.WriteAsync(question,
false, cancellationToken);
925 var addressString = await console.ReadLineAsync(
false, cancellationToken);
926 if (Uri.TryCreate(addressString, UriKind.Absolute, out address)
927 && address.Scheme != Uri.UriSchemeHttp
928 && address.Scheme != Uri.UriSchemeHttps)
931 while (address ==
null);
936 var address = await ParseAddress(
"Enter this server's INTERNAL http(s) address: ");
937 var publicAddress = await ParseAddress(
"Enter this server's PUBLIC https(s) address: ");
941 await console.WriteAsync(
"Enter the swarm private key: ",
false, cancellationToken);
942 privateKey = await console.ReadLineAsync(
false, cancellationToken);
944 while (String.IsNullOrWhiteSpace(privateKey));
946 var controller = await PromptYesNo(
"Is this server the swarm's controller? (y/n): ",
null, cancellationToken);
947 Uri? controllerAddress =
null;
949 controllerAddress = await ParseAddress(
"Enter the swarm controller's HTTP(S) address: ");
954 PublicAddress = publicAddress,
955 ControllerAddress = controllerAddress,
956 Identifier = identifer,
957 PrivateKey = privateKey,
968 bool enableReporting = await PromptYesNo(
"Enable version telemetry? This anonymously reports the TGS version in use.",
true, cancellationToken);
970 string? serverFriendlyName =
null;
973 await console.WriteAsync(
"(Optional) Publically associate your reported version with a friendly name:",
false, cancellationToken);
974 serverFriendlyName = await console.ReadLineAsync(
false, cancellationToken);
975 if (String.IsNullOrWhiteSpace(serverFriendlyName))
976 serverFriendlyName =
null;
981 DisableVersionReporting = !enableReporting,
982 ServerFriendlyName = serverFriendlyName,
1001 string userConfigFileName,
1002 ushort? hostingPort,
1010 CancellationToken cancellationToken)
1014 var map =
new Dictionary<string, object?>()
1026 var builder =
new SerializerBuilder()
1027 .WithTypeConverter(versionConverter);
1029 if (userConfigFileName.EndsWith(
".json", StringComparison.OrdinalIgnoreCase))
1030 builder.JsonCompatible();
1032 var serializer =
new SerializerBuilder()
1033 .WithTypeConverter(versionConverter)
1036 var serializedYaml = serializer.Serialize(map);
1039 serializedYaml = serializedYaml.Replace(
1040 $
"\n {nameof(ControlPanelConfiguration.Channel)}: ",
1042 StringComparison.Ordinal)
1043 .Replace(
"\r", String.Empty, StringComparison.Ordinal);
1045 var configBytes = Encoding.UTF8.GetBytes(serializedYaml);
1049 await ioManager.WriteAllBytes(
1054 postSetupServices.ReloadRequired =
true;
1056 catch (
Exception e) when (e is not OperationCanceledException)
1058 await console.WriteAsync(e.Message,
true, cancellationToken);
1059 await console.WriteAsync(
null,
true, cancellationToken);
1060 await console.WriteAsync(
"For your convienence, here's the yaml we tried to write out:",
true, cancellationToken);
1061 await console.WriteAsync(
null,
true, cancellationToken);
1062 await console.WriteAsync(serializedYaml,
true, cancellationToken);
1063 await console.WriteAsync(
null,
true, cancellationToken);
1064 await console.WriteAsync(
"Press any key to exit...",
true, cancellationToken);
1065 await console.PressAnyKeyAsync(cancellationToken);
1066 throw new OperationCanceledException();
1076 async ValueTask
RunWizard(
string userConfigFileName, CancellationToken cancellationToken)
1079 await console.WriteAsync($
"Welcome to {Constants.CanonicalPackageName}!",
true, cancellationToken);
1080 await console.WriteAsync(
"This wizard will help you configure your server.",
true, cancellationToken);
1082 var hostingPort = await PromptForHostingPort(cancellationToken);
1084 var databaseConfiguration = await ConfigureDatabase(cancellationToken);
1086 var newGeneralConfiguration = await ConfigureGeneral(cancellationToken);
1088 var fileLoggingConfiguration = await ConfigureLogging(cancellationToken);
1090 var elasticSearchConfiguration = await ConfigureElasticsearch(cancellationToken);
1092 var controlPanelConfiguration = await ConfigureControlPanel(cancellationToken);
1094 var swarmConfiguration = await ConfigureSwarm(cancellationToken);
1096 var telemetryConfiguration = await ConfigureTelemetry(cancellationToken);
1098 await console.WriteAsync(
null,
true, cancellationToken);
1099 await console.WriteAsync(String.Format(CultureInfo.InvariantCulture,
"Configuration complete! Saving to {0}", userConfigFileName),
true, cancellationToken);
1101 await SaveConfiguration(
1104 databaseConfiguration,
1105 newGeneralConfiguration,
1106 fileLoggingConfiguration,
1107 elasticSearchConfiguration,
1108 controlPanelConfiguration,
1110 telemetryConfiguration,
1121 var setupWizardMode = generalConfiguration.SetupWizardMode;
1125 var forceRun = setupWizardMode == SetupWizardMode.Force || setupWizardMode ==
SetupWizardMode.Only;
1126 if (!console.Available)
1129 throw new InvalidOperationException(
"Asked to run setup wizard with no console avaliable!");
1133 var userConfigFileName = ioManager.ConcatPath(
1134 internalConfiguration.AppSettingsBasePath,
1135 $
"{ServerFactory.AppSettings}.{hostingEnvironment.EnvironmentName}.yml");
1137 async Task HandleSetupCancel()
1140 await console.WriteAsync(String.Empty,
true,
default);
1141 await console.WriteAsync(
"Aborting setup!",
true,
default);
1144 Task finalTask = Task.CompletedTask;
1145 string? originalConsoleTitle =
null;
1146 void SetConsoleTitle()
1148 if (originalConsoleTitle !=
null)
1151 originalConsoleTitle = console.Title;
1152 console.SetTitle($
"{assemblyInformationProvider.VersionString} Setup Wizard");
1156 using (var cts = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken, console.CancelKeyPress))
1157 using ((cancellationToken = cts.Token).Register(() => finalTask = HandleSetupCancel()))
1160 var exists = await ioManager.FileExists(userConfigFileName, cancellationToken);
1163 var legacyJsonFileName = $
"appsettings.{hostingEnvironment.EnvironmentName}.json";
1164 exists = await ioManager.FileExists(legacyJsonFileName, cancellationToken);
1166 userConfigFileName = legacyJsonFileName;
1169 bool shouldRunBasedOnAutodetect;
1172 var bytes = await ioManager.ReadAllBytes(userConfigFileName, cancellationToken);
1173 var contents = Encoding.UTF8.GetString(bytes);
1174 var lines = contents.Split(
'\n', StringSplitOptions.RemoveEmptyEntries);
1175 var existingConfigIsEmpty = lines
1176 .Select(line => line.Trim())
1177 .All(line => line[0] ==
'#' || line ==
"{}" || line.Length == 0);
1178 shouldRunBasedOnAutodetect = existingConfigIsEmpty;
1181 shouldRunBasedOnAutodetect =
true;
1183 if (!shouldRunBasedOnAutodetect)
1188 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);
1190 forceRun = await PromptYesNo(
"Continue running setup wizard?",
false, cancellationToken);
1199 if (!String.IsNullOrEmpty(internalConfiguration.MariaDBDefaultRootPassword))
1202 var csb =
new MySqlConnectionStringBuilder
1206 Password = internalConfiguration.MariaDBDefaultRootPassword,
1210 await SaveConfiguration(
1225 AllowAnyOrigin =
true,
1234 await asyncDelayer.Delay(TimeSpan.FromSeconds(1), cancellationToken);
1236 await RunWizard(userConfigFileName, cancellationToken);
1242 if (originalConsoleTitle !=
null)
1243 console.SetTitle(originalConsoleTitle);
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.
DatabaseType DatabaseType
The Configuration.DatabaseType to create.
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...
Configuration options for telemetry.
const string Section
The key for the Microsoft.Extensions.Configuration.IConfigurationSection the TelemetryConfiguration r...
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.
async ValueTask< ControlPanelConfiguration > ConfigureControlPanel(CancellationToken cancellationToken)
Prompts the user to create a ControlPanelConfiguration.
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.
async ValueTask CheckRunWizard(CancellationToken cancellationToken)
Check if it should and run the SetupWizard if necessary.
async ValueTask< SwarmConfiguration?> ConfigureSwarm(CancellationToken cancellationToken)
Prompts the user to create a SwarmConfiguration.
readonly GeneralConfiguration generalConfiguration
The GeneralConfiguration for the SetupWizard.
async ValueTask< DatabaseConfiguration > ConfigureDatabase(CancellationToken cancellationToken)
Prompts the user to create a DatabaseConfiguration.
readonly IAssemblyInformationProvider assemblyInformationProvider
The IAssemblyInformationProvider for the SetupWizard.
async ValueTask< TelemetryConfiguration > ConfigureTelemetry(CancellationToken cancellationToken)
Prompts the user to create a TelemetryConfiguration.
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 IPostSetupServices postSetupServices
The IPostSetupServices for the SetupWizard.
async ValueTask RunWizard(string userConfigFileName, CancellationToken cancellationToken)
Runs the SetupWizard.
readonly IHostApplicationLifetime applicationLifetime
The IHostApplicationLifetime for the SetupWizard.
async ValueTask< DatabaseType > PromptDatabaseType(bool firstTime, CancellationToken cancellationToken)
Prompt the user for the DatabaseType.
async ValueTask SaveConfiguration(string userConfigFileName, ushort? hostingPort, DatabaseConfiguration databaseConfiguration, GeneralConfiguration newGeneralConfiguration, FileLoggingConfiguration? fileLoggingConfiguration, ElasticsearchConfiguration? elasticsearchConfiguration, ControlPanelConfiguration controlPanelConfiguration, SwarmConfiguration? swarmConfiguration, TelemetryConfiguration? telemetryConfiguration, CancellationToken cancellationToken)
Saves a given Configuration set to userConfigFileName .
SetupWizard(IIOManager ioManager, IConsole console, IHostEnvironment hostingEnvironment, IAssemblyInformationProvider assemblyInformationProvider, IDatabaseConnectionFactory dbConnectionFactory, IPlatformIdentifier platformIdentifier, IAsyncDelayer asyncDelayer, IHostApplicationLifetime applicationLifetime, IPostSetupServices postSetupServices, IOptions< GeneralConfiguration > generalConfigurationOptions, IOptions< InternalConfiguration > internalConfigurationOptions)
Initializes a new instance of the SetupWizard class.
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.
Set of objects needed to configure an Core.Application.
For waiting asynchronously.
DatabaseType
Type of database to user.
SetupWizardMode
Determines if the SetupWizard will run.