tgstation-server
The /tg/station 13 server suite
SetupWizard.cs
Go to the documentation of this file.
1 using Microsoft.AspNetCore.Hosting;
2 using Microsoft.Extensions.Logging;
3 using Microsoft.Extensions.Options;
4 using MySql.Data.MySqlClient;
5 using Newtonsoft.Json;
6 using System;
7 using System.Collections.Generic;
8 using System.Data.Common;
9 using System.Data.SqlClient;
10 using System.Globalization;
11 using System.Linq;
12 using System.Text;
13 using System.Threading;
14 using System.Threading.Tasks;
16 using Tgstation.Server.Host.IO;
17 
18 namespace Tgstation.Server.Host.Core
19 {
21  sealed class SetupWizard : ISetupWizard
22  {
27 
31  readonly IConsole console;
32 
36  readonly IHostingEnvironment hostingEnvironment;
37 
42 
47 
52 
57 
61  readonly ILogger<SetupWizard> logger;
62 
67 
80  public SetupWizard(IIOManager ioManager, IConsole console, IHostingEnvironment hostingEnvironment, IApplication application, IDBConnectionFactory dbConnectionFactory, IPlatformIdentifier platformIdentifier, IAsyncDelayer asyncDelayer, ILogger<SetupWizard> logger, IOptions<GeneralConfiguration> generalConfigurationOptions)
81  {
82  this.ioManager = ioManager ?? throw new ArgumentNullException(nameof(ioManager));
83  this.console = console ?? throw new ArgumentNullException(nameof(console));
84  this.hostingEnvironment = hostingEnvironment ?? throw new ArgumentNullException(nameof(hostingEnvironment));
85  this.application = application ?? throw new ArgumentNullException(nameof(application));
86  this.dbConnectionFactory = dbConnectionFactory ?? throw new ArgumentNullException(nameof(dbConnectionFactory));
87  this.platformIdentifier = platformIdentifier ?? throw new ArgumentNullException(nameof(platformIdentifier));
88  this.asyncDelayer = asyncDelayer ?? throw new ArgumentNullException(nameof(asyncDelayer));
89  this.logger = logger ?? throw new ArgumentNullException(nameof(logger));
90  generalConfiguration = generalConfigurationOptions?.Value ?? throw new ArgumentNullException(nameof(generalConfigurationOptions));
91  }
92 
99  async Task<bool> PromptYesNo(string question, CancellationToken cancellationToken)
100  {
101  do
102  {
103  await console.WriteAsync(question, false, cancellationToken).ConfigureAwait(false);
104  var responseString = await console.ReadLineAsync(false, cancellationToken).ConfigureAwait(false);
105  var upperResponse = responseString.ToUpperInvariant();
106  if (upperResponse == "Y" || upperResponse == "YES")
107  return true;
108  else if (upperResponse == "N" || upperResponse == "NO")
109  return false;
110  await console.WriteAsync("Invalid response!", true, cancellationToken).ConfigureAwait(false);
111  }
112  while (true);
113  }
114 
120  async Task<ushort?> PromptForHostingPort(CancellationToken cancellationToken)
121  {
122  await console.WriteAsync(null, true, cancellationToken).ConfigureAwait(false);
123  await console.WriteAsync("What port would you like to connect to TGS on?", true, cancellationToken).ConfigureAwait(false);
124  await console.WriteAsync("Note: If this is a docker container with the default port already mapped, use the default.", true, cancellationToken).ConfigureAwait(false);
125 
126  do
127  {
128  await console.WriteAsync("API Port (leave blank for default): ", false, cancellationToken).ConfigureAwait(false);
129  var portString = await console.ReadLineAsync(false, cancellationToken).ConfigureAwait(false);
130  if (String.IsNullOrWhiteSpace(portString))
131  return null;
132  if (UInt16.TryParse(portString, out var port) && port != 0)
133  return port;
134  await console.WriteAsync("Invalid port! Please enter a value between 1 and 65535", true, cancellationToken).ConfigureAwait(false);
135  }
136  while (true);
137  }
138 
144  async Task<DatabaseConfiguration> ConfigureDatabase(CancellationToken cancellationToken)
145  {
146  do
147  {
148  await console.WriteAsync(null, true, cancellationToken).ConfigureAwait(false);
149  await console.WriteAsync("What SQL database type will you be using?", true, cancellationToken).ConfigureAwait(false);
150 
151  var databaseConfiguration = new DatabaseConfiguration();
152  do
153  {
154  await console.WriteAsync(String.Format(CultureInfo.InvariantCulture, "Please enter one of {0}, {1}, or {2}: ", DatabaseType.MariaDB, DatabaseType.SqlServer, DatabaseType.MySql), false, cancellationToken).ConfigureAwait(false);
155  var databaseTypeString = await console.ReadLineAsync(false, cancellationToken).ConfigureAwait(false);
156  if (Enum.TryParse<DatabaseType>(databaseTypeString, out var databaseType))
157  {
158  databaseConfiguration.DatabaseType = databaseType;
159  break;
160  }
161  await console.WriteAsync("Invalid database type!", true, cancellationToken).ConfigureAwait(false);
162  }
163  while (true);
164 
165  await console.WriteAsync(null, true, cancellationToken).ConfigureAwait(false);
166  await console.WriteAsync("Enter the server's address and port (blank for local): ", false, cancellationToken).ConfigureAwait(false);
167  var serverAddress = await console.ReadLineAsync(false, cancellationToken).ConfigureAwait(false);
168  if (String.IsNullOrWhiteSpace(serverAddress))
169  serverAddress = null;
170 
171  await console.WriteAsync(null, true, cancellationToken).ConfigureAwait(false);
172  await console.WriteAsync("Enter the database name (Can be from previous installation. Otherwise, should not exist): ", false, cancellationToken).ConfigureAwait(false);
173  string databaseName;
174 
175  do
176  {
177  databaseName = await console.ReadLineAsync(false, cancellationToken).ConfigureAwait(false);
178  if (!String.IsNullOrWhiteSpace(databaseName))
179  break;
180  await console.WriteAsync("Invalid database name!", true, cancellationToken).ConfigureAwait(false);
181  }
182  while (true);
183 
184  var dbExists = await PromptYesNo("Does this database already exist? (y/n): ", cancellationToken).ConfigureAwait(false);
185 
186  bool useWinAuth;
187  if (databaseConfiguration.DatabaseType == DatabaseType.SqlServer && platformIdentifier.IsWindows)
188  useWinAuth = await PromptYesNo("Use Windows Authentication? (y/n): ", cancellationToken).ConfigureAwait(false);
189  else
190  useWinAuth = false;
191 
192  await console.WriteAsync(null, true, cancellationToken).ConfigureAwait(false);
193 
194  string username = null;
195  string password = null;
196  if (!useWinAuth)
197  {
198  await console.WriteAsync("Enter username: ", false, cancellationToken).ConfigureAwait(false);
199  username = await console.ReadLineAsync(false, cancellationToken).ConfigureAwait(false);
200  await console.WriteAsync("Enter password: ", false, cancellationToken).ConfigureAwait(false);
201  password = await console.ReadLineAsync(true, cancellationToken).ConfigureAwait(false);
202  }
203  else
204  {
205  await console.WriteAsync("IMPORTANT: If using the service runner, ensure this computer's LocalSystem account has CREATE DATABASE permissions on the target server!", true, cancellationToken).ConfigureAwait(false);
206  await console.WriteAsync("The account it uses in MSSQL is usually \"NT AUTHORITY\\SYSTEM\" and the role it needs is usually \"dbcreator\".", true, cancellationToken).ConfigureAwait(false);
207  await console.WriteAsync("We'll run a sanity test here, but it won't be indicative of the service's permissions if that is the case", true, cancellationToken).ConfigureAwait(false);
208  }
209  await console.WriteAsync(null, true, cancellationToken).ConfigureAwait(false);
210 
211  DbConnection testConnection;
212  void CreateTestConnection(string connectionString)
213  {
214  testConnection = dbConnectionFactory.CreateConnection(connectionString, databaseConfiguration.DatabaseType);
215  }
216 
217  if (databaseConfiguration.DatabaseType == DatabaseType.SqlServer)
218  {
219  var csb = new SqlConnectionStringBuilder
220  {
221  ApplicationName = application.VersionPrefix,
222  DataSource = serverAddress ?? "(local)"
223  };
224  if (useWinAuth)
225  csb.IntegratedSecurity = true;
226  else
227  {
228  csb.UserID = username;
229  csb.Password = password;
230  }
231 
232  CreateTestConnection(csb.ConnectionString);
233  csb.InitialCatalog = databaseName;
234  databaseConfiguration.ConnectionString = csb.ConnectionString;
235  }
236  else
237  {
238  var csb = new MySqlConnectionStringBuilder
239  {
240  Server = serverAddress ?? "127.0.0.1",
241  UserID = username,
242  Password = password
243  };
244 
245  CreateTestConnection(csb.ConnectionString);
246  csb.Database = databaseName;
247  databaseConfiguration.ConnectionString = csb.ConnectionString;
248  }
249 
250  try
251  {
252  using (testConnection)
253  {
254  await console.WriteAsync("Testing connection...", true, cancellationToken).ConfigureAwait(false);
255  await testConnection.OpenAsync(cancellationToken).ConfigureAwait(false);
256  await console.WriteAsync("Connection successful!", true, cancellationToken).ConfigureAwait(false);
257 
258  if (databaseConfiguration.DatabaseType != DatabaseType.SqlServer)
259  {
260  await console.WriteAsync("Checking MySQL/MariaDB version...", true, cancellationToken).ConfigureAwait(false);
261  using (var command = testConnection.CreateCommand())
262  {
263  command.CommandText = "SELECT VERSION()";
264  var fullVersion = (string)(await command.ExecuteScalarAsync(cancellationToken).ConfigureAwait(false));
265  await console.WriteAsync(String.Format(CultureInfo.InvariantCulture, "Found {0}", fullVersion), true, cancellationToken).ConfigureAwait(false);
266  var splits = fullVersion.Split('-');
267  databaseConfiguration.MySqlServerVersion = splits[0];
268  }
269  }
270 
271  if (!dbExists)
272  {
273  await console.WriteAsync("Testing create DB permission...", true, cancellationToken).ConfigureAwait(false);
274  using (var command = testConnection.CreateCommand())
275  {
276  command.CommandText = String.Format(CultureInfo.InvariantCulture, "CREATE DATABASE {0}", databaseName);
277  await command.ExecuteNonQueryAsync(cancellationToken).ConfigureAwait(false);
278  }
279  await console.WriteAsync("Success!", true, cancellationToken).ConfigureAwait(false);
280  await console.WriteAsync("Dropping test database...", true, cancellationToken).ConfigureAwait(false);
281  using (var command = testConnection.CreateCommand())
282  {
283  command.CommandText = String.Format(CultureInfo.InvariantCulture, "DROP DATABASE {0}", databaseName);
284  try
285  {
286  await command.ExecuteNonQueryAsync(cancellationToken).ConfigureAwait(false);
287  }
288  catch (OperationCanceledException)
289  {
290  throw;
291  }
292  catch (Exception e)
293  {
294  await console.WriteAsync(e.Message, true, cancellationToken).ConfigureAwait(false);
295  await console.WriteAsync(null, true, cancellationToken).ConfigureAwait(false);
296  await console.WriteAsync("This should be okay, but you may want to manually drop the database before continuing!", true, cancellationToken).ConfigureAwait(false);
297  await console.WriteAsync("Press any key to continue...", true, cancellationToken).ConfigureAwait(false);
298  await console.PressAnyKeyAsync(cancellationToken).ConfigureAwait(false);
299  }
300  }
301  }
302  }
303 
304  return databaseConfiguration;
305  }
306  catch (OperationCanceledException)
307  {
308  throw;
309  }
310  catch (Exception e)
311  {
312  await console.WriteAsync(e.Message, true, cancellationToken).ConfigureAwait(false);
313  await console.WriteAsync(null, true, cancellationToken).ConfigureAwait(false);
314  await console.WriteAsync("Retrying database configuration...", true, cancellationToken).ConfigureAwait(false);
315  }
316  } while (true);
317  }
318 
324  async Task<GeneralConfiguration> ConfigureGeneral(CancellationToken cancellationToken)
325  {
326  var newGeneralConfiguration = new GeneralConfiguration
327  {
329  };
330 
331  do
332  {
333  await console.WriteAsync(null, true, cancellationToken).ConfigureAwait(false);
334  await console.WriteAsync(String.Format(CultureInfo.InvariantCulture, "Minimum database user password length (leave blank for default of {0}): ", newGeneralConfiguration.MinimumPasswordLength), false, cancellationToken).ConfigureAwait(false);
335  var passwordLengthString = await console.ReadLineAsync(false, cancellationToken).ConfigureAwait(false);
336  if (String.IsNullOrWhiteSpace(passwordLengthString))
337  break;
338  if (UInt32.TryParse(passwordLengthString, out var passwordLength) && passwordLength >= 0)
339  {
340  newGeneralConfiguration.MinimumPasswordLength = passwordLength;
341  break;
342  }
343  await console.WriteAsync("Please enter a positive integer!", true, cancellationToken).ConfigureAwait(false);
344  }
345  while (true);
346 
347  do
348  {
349  await console.WriteAsync(null, true, cancellationToken).ConfigureAwait(false);
350  await console.WriteAsync(String.Format(CultureInfo.InvariantCulture, "Timeout for sending and receiving BYOND topics (ms, 0 for infinite, leave blank for default of {0}): ", newGeneralConfiguration.ByondTopicTimeout), false, cancellationToken).ConfigureAwait(false);
351  var topicTimeoutString = await console.ReadLineAsync(false, cancellationToken).ConfigureAwait(false);
352  if (String.IsNullOrWhiteSpace(topicTimeoutString))
353  break;
354  if (Int32.TryParse(topicTimeoutString, out var topicTimeout) && topicTimeout >= 0)
355  {
356  newGeneralConfiguration.ByondTopicTimeout = topicTimeout;
357  break;
358  }
359  await console.WriteAsync("Please enter a positive integer!", true, cancellationToken).ConfigureAwait(false);
360  }
361  while (true);
362 
363  await console.WriteAsync(null, true, cancellationToken).ConfigureAwait(false);
364  await console.WriteAsync("Enter a GitHub personal access token to bypass some rate limits (this is optional and does not require any scopes)", true, cancellationToken).ConfigureAwait(false);
365  await console.WriteAsync("GitHub personal access token: ", false, cancellationToken).ConfigureAwait(false);
366  newGeneralConfiguration.GitHubAccessToken = await console.ReadLineAsync(true, cancellationToken).ConfigureAwait(false);
367  if (String.IsNullOrWhiteSpace(newGeneralConfiguration.GitHubAccessToken))
368  newGeneralConfiguration.GitHubAccessToken = null;
369  return newGeneralConfiguration;
370  }
371 
377  async Task<FileLoggingConfiguration> ConfigureLogging(CancellationToken cancellationToken)
378  {
379  var fileLoggingConfiguration = new FileLoggingConfiguration();
380  await console.WriteAsync(null, true, cancellationToken).ConfigureAwait(false);
381  fileLoggingConfiguration.Disable = !await PromptYesNo("Enable file logging? (y/n): ", cancellationToken).ConfigureAwait(false);
382 
383  if (!fileLoggingConfiguration.Disable)
384  {
385  do
386  {
387  await console.WriteAsync("Log file directory path (leave blank for default): ", false, cancellationToken).ConfigureAwait(false);
388  fileLoggingConfiguration.Directory = await console.ReadLineAsync(false, cancellationToken).ConfigureAwait(false);
389  if (String.IsNullOrWhiteSpace(fileLoggingConfiguration.Directory))
390  {
391  fileLoggingConfiguration.Directory = null;
392  break;
393  }
394  //test a write of it
395  await console.WriteAsync(null, true, cancellationToken).ConfigureAwait(false);
396  await console.WriteAsync("Testing directory access...", true, cancellationToken).ConfigureAwait(false);
397  try
398  {
399  await ioManager.CreateDirectory(fileLoggingConfiguration.Directory, cancellationToken).ConfigureAwait(false);
400  var testFile = ioManager.ConcatPath(fileLoggingConfiguration.Directory, String.Format(CultureInfo.InvariantCulture, "WizardAccesTest.{0}.deleteme", Guid.NewGuid()));
401  await ioManager.WriteAllBytes(testFile, Array.Empty<byte>(), cancellationToken).ConfigureAwait(false);
402  try
403  {
404  await ioManager.DeleteFile(testFile, cancellationToken).ConfigureAwait(false);
405  }
406  catch (OperationCanceledException)
407  {
408  throw;
409  }
410  catch (Exception e)
411  {
412  await console.WriteAsync(String.Format(CultureInfo.InvariantCulture, "Error deleting test log file: {0}", testFile), true, cancellationToken).ConfigureAwait(false);
413  await console.WriteAsync(e.Message, true, cancellationToken).ConfigureAwait(false);
414  await console.WriteAsync(null, true, cancellationToken).ConfigureAwait(false);
415  }
416  break;
417  }
418  catch (OperationCanceledException)
419  {
420  throw;
421  }
422  catch (Exception e)
423  {
424  await console.WriteAsync(e.Message, true, cancellationToken).ConfigureAwait(false);
425  await console.WriteAsync(null, true, cancellationToken).ConfigureAwait(false);
426  await console.WriteAsync("Please verify the path is valid and you have access to it!", true, cancellationToken).ConfigureAwait(false);
427  }
428  } while (true);
429 
430  async Task<LogLevel?> PromptLogLevel(string question)
431  {
432  do
433  {
434  await console.WriteAsync(null, true, cancellationToken).ConfigureAwait(false);
435  await console.WriteAsync(question, true, cancellationToken).ConfigureAwait(false);
436  await console.WriteAsync(String.Format(CultureInfo.InvariantCulture, "Enter one of {0}/{1}/{2}/{3}/{4}/{5} (leave blank for default): ", nameof(LogLevel.Trace), nameof(LogLevel.Debug), nameof(LogLevel.Information), nameof(LogLevel.Warning), nameof(LogLevel.Error), nameof(LogLevel.Critical)), false, cancellationToken).ConfigureAwait(false);
437  var responseString = await console.ReadLineAsync(false, cancellationToken).ConfigureAwait(false);
438  if (String.IsNullOrWhiteSpace(responseString))
439  return null;
440  if (Enum.TryParse<LogLevel>(responseString, out var logLevel) && logLevel != LogLevel.None)
441  return logLevel;
442  await console.WriteAsync("Invalid log level!", true, cancellationToken).ConfigureAwait(false);
443  } while (true);
444  }
445 
446  fileLoggingConfiguration.LogLevel = await PromptLogLevel(String.Format(CultureInfo.InvariantCulture, "Enter the level limit for normal logs (default {0}).", fileLoggingConfiguration.LogLevel)).ConfigureAwait(false) ?? fileLoggingConfiguration.LogLevel;
447  fileLoggingConfiguration.MicrosoftLogLevel = await PromptLogLevel(String.Format(CultureInfo.InvariantCulture, "Enter the level limit for Microsoft logs (VERY verbose, default {0}).", fileLoggingConfiguration.MicrosoftLogLevel)).ConfigureAwait(false) ?? fileLoggingConfiguration.MicrosoftLogLevel;
448  }
449  return fileLoggingConfiguration;
450  }
451 
457  async Task<ControlPanelConfiguration> ConfigureControlPanel(CancellationToken cancellationToken)
458  {
459  var config = new ControlPanelConfiguration
460  {
461  Enable = await PromptYesNo("Enable the web control panel? (y/n): ", cancellationToken).ConfigureAwait(false),
462  AllowAnyOrigin = await PromptYesNo("Allow web control panels hosted elsewhere to access the server? (Access-Control-Allow-Origin: *) (y/n): ", cancellationToken).ConfigureAwait(false)
463  };
464 
465  if (!config.AllowAnyOrigin)
466  {
467  await console.WriteAsync("Enter a comma seperated list of CORS allowed origins (optional): ", false, cancellationToken).ConfigureAwait(false);
468  var commaSeperatedOrigins = await console.ReadLineAsync(false, cancellationToken).ConfigureAwait(false);
469  if (!String.IsNullOrWhiteSpace(commaSeperatedOrigins))
470  {
471  var splits = commaSeperatedOrigins.Split(',');
472  config.AllowedOrigins = new List<string>(splits.Select(x => x.Trim()));
473  }
474  }
475 
476  return config;
477  }
478 
490  async Task SaveConfiguration(string userConfigFileName, ushort? hostingPort, DatabaseConfiguration databaseConfiguration, GeneralConfiguration newGeneralConfiguration, FileLoggingConfiguration fileLoggingConfiguration, ControlPanelConfiguration controlPanelConfiguration, CancellationToken cancellationToken)
491  {
492  await console.WriteAsync(String.Format(CultureInfo.InvariantCulture, "Configuration complete! Saving to {0}", userConfigFileName), true, cancellationToken).ConfigureAwait(false);
493 
494  var map = new Dictionary<string, object>()
495  {
496  { DatabaseConfiguration.Section, databaseConfiguration },
497  { GeneralConfiguration.Section, newGeneralConfiguration },
498  { FileLoggingConfiguration.Section, fileLoggingConfiguration },
499  { ControlPanelConfiguration.Section, controlPanelConfiguration }
500  };
501 
502  if (hostingPort.HasValue)
503  map.Add("Kestrel", new
504  {
505  EndPoints = new
506  {
507  Http = new
508  {
509  Url = String.Format(CultureInfo.InvariantCulture, "http://0.0.0.0:{0}", hostingPort)
510  }
511  }
512  });
513 
514  var json = JsonConvert.SerializeObject(map, Formatting.Indented);
515  var configBytes = Encoding.UTF8.GetBytes(json);
516 
517  try
518  {
519  await ioManager.WriteAllBytes(userConfigFileName, configBytes, cancellationToken).ConfigureAwait(false);
520  }
521  catch (OperationCanceledException)
522  {
523  throw;
524  }
525  catch (Exception e)
526  {
527  await console.WriteAsync(e.Message, true, cancellationToken).ConfigureAwait(false);
528  await console.WriteAsync(null, true, cancellationToken).ConfigureAwait(false);
529  await console.WriteAsync("For your convienence, here's the json we tried to write out:", true, cancellationToken).ConfigureAwait(false);
530  await console.WriteAsync(null, true, cancellationToken).ConfigureAwait(false);
531  await console.WriteAsync(json, true, cancellationToken).ConfigureAwait(false);
532  await console.WriteAsync(null, true, cancellationToken).ConfigureAwait(false);
533  await console.WriteAsync("Press any key to exit...", true, cancellationToken).ConfigureAwait(false);
534  await console.PressAnyKeyAsync(cancellationToken).ConfigureAwait(false);
535  throw new OperationCanceledException();
536  }
537 
538  await console.WriteAsync("Waiting for configuration changes to reload...", true, cancellationToken).ConfigureAwait(false);
539 
540  //we need to wait for the configuration's file system watcher to read and reload the changes
541  await asyncDelayer.Delay(TimeSpan.FromSeconds(5), cancellationToken).ConfigureAwait(false);
542  }
543 
550  async Task RunWizard(string userConfigFileName, CancellationToken cancellationToken)
551  {
552  //welcome message
553  await console.WriteAsync(null, true, cancellationToken).ConfigureAwait(false);
554  await console.WriteAsync("Welcome to tgstation-server 4!", true, cancellationToken).ConfigureAwait(false);
555  await console.WriteAsync("This wizard will help you configure your server.", true, cancellationToken).ConfigureAwait(false);
556 
557  var hostingPort = await PromptForHostingPort(cancellationToken).ConfigureAwait(false);
558 
559  var databaseConfiguration = await ConfigureDatabase(cancellationToken).ConfigureAwait(false);
560 
561  var newGeneralConfiguration = await ConfigureGeneral(cancellationToken).ConfigureAwait(false);
562 
563  var fileLoggingConfiguration = await ConfigureLogging(cancellationToken).ConfigureAwait(false);
564 
565  var controlPanelConfiguration = await ConfigureControlPanel(cancellationToken).ConfigureAwait(false);
566 
567  await console.WriteAsync(null, true, cancellationToken).ConfigureAwait(false);
568 
569  await SaveConfiguration(userConfigFileName, hostingPort, databaseConfiguration, newGeneralConfiguration, fileLoggingConfiguration, controlPanelConfiguration, cancellationToken).ConfigureAwait(false);
570  }
571 
573  public async Task<bool> CheckRunWizard(CancellationToken cancellationToken)
574  {
575  var setupWizardMode = generalConfiguration.SetupWizardMode;
576  logger.LogTrace("Checking if setup wizard should run. SetupWizardMode: {0}", setupWizardMode);
577 
578  if (setupWizardMode == SetupWizardMode.Never)
579  {
580  logger.LogTrace("Skipping due to configuration...");
581  return false;
582  }
583 
584  var forceRun = setupWizardMode == SetupWizardMode.Force || setupWizardMode == SetupWizardMode.Only;
585  if (!console.Available)
586  {
587  if (forceRun)
588  throw new InvalidOperationException("Asked to run setup wizard with no console avaliable!");
589  logger.LogTrace("Skipping due to console not being available...");
590  return false;
591  }
592 
593  var userConfigFileName = String.Format(CultureInfo.InvariantCulture, "appsettings.{0}.json", hostingEnvironment.EnvironmentName);
594  var exists = await ioManager.FileExists(userConfigFileName, cancellationToken).ConfigureAwait(false);
595 
596  bool shouldRunBasedOnAutodetect;
597  if (exists)
598  {
599  var bytes = await ioManager.ReadAllBytes(userConfigFileName, cancellationToken).ConfigureAwait(false);
600  var contents = Encoding.UTF8.GetString(bytes);
601  var existingConfigIsEmpty = String.IsNullOrWhiteSpace(contents) || contents.Trim() == "{}";
602  logger.LogTrace("Configuration json detected. Empty: {0}", existingConfigIsEmpty);
603  shouldRunBasedOnAutodetect = existingConfigIsEmpty;
604  }
605  else
606  {
607  shouldRunBasedOnAutodetect = true;
608  logger.LogTrace("No configuration json detected");
609  }
610 
611 
612  if (!shouldRunBasedOnAutodetect)
613  {
614  if (forceRun)
615  {
616  logger.LogTrace("Asking user to bypass due to force run request...");
617  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).ConfigureAwait(false);
618 
619  forceRun = await PromptYesNo("Continue running setup wizard? (y/n): ", cancellationToken).ConfigureAwait(false);
620  }
621  if (!forceRun)
622  return false;
623  }
624 
625  //flush the logs to prevent console conflicts
626  await asyncDelayer.Delay(TimeSpan.FromSeconds(1), cancellationToken).ConfigureAwait(false);
627 
628  await RunWizard(userConfigFileName, cancellationToken).ConfigureAwait(false);
629  return true;
630  }
631  }
632 }
readonly IConsole console
The IConsole for the SetupWizard
Definition: SetupWizard.cs:31
List< string > AllowedOrigins
Origins allowed for CORS requests
async Task< ControlPanelConfiguration > ConfigureControlPanel(CancellationToken cancellationToken)
Prompts the user to create a ControlPanelConfiguration
Definition: SetupWizard.cs:457
Configures the ASP.NET Core web application
Definition: IApplication.cs:8
SetupWizardMode
Determines if the Core.ISetupWizard will run
Abstraction for System.Console
Definition: IConsole.cs:9
const string Section
The key for the Microsoft.Extensions.Configuration.IConfigurationSection the GeneralConfiguration res...
readonly IApplication application
The IApplication for the SetupWizard
Definition: SetupWizard.cs:41
const string Section
The key for the Microsoft.Extensions.Configuration.IConfigurationSection the DatabaseConfiguration re...
readonly IIOManager ioManager
The IIOManager for the SetupWizard
Definition: SetupWizard.cs:26
readonly IDBConnectionFactory dbConnectionFactory
The IDBConnectionFactory for the SetupWizard
Definition: SetupWizard.cs:46
readonly GeneralConfiguration generalConfiguration
The GeneralConfiguration for the SetupWizard
Definition: SetupWizard.cs:66
SetupWizard(IIOManager ioManager, IConsole console, IHostingEnvironment hostingEnvironment, IApplication application, IDBConnectionFactory dbConnectionFactory, IPlatformIdentifier platformIdentifier, IAsyncDelayer asyncDelayer, ILogger< SetupWizard > logger, IOptions< GeneralConfiguration > generalConfigurationOptions)
Construct a SetupWizard
Definition: SetupWizard.cs:80
async Task< bool > PromptYesNo(string question, CancellationToken cancellationToken)
A prompt for a yes or no value
Definition: SetupWizard.cs:99
const string Section
The key for the Microsoft.Extensions.Configuration.IConfigurationSection the ControlPanelConfiguratio...
async Task< DatabaseConfiguration > ConfigureDatabase(CancellationToken cancellationToken)
Prompts the user to create a DatabaseConfiguration
Definition: SetupWizard.cs:144
async Task< GeneralConfiguration > ConfigureGeneral(CancellationToken cancellationToken)
Prompts the user to create a GeneralConfiguration
Definition: SetupWizard.cs:324
async Task SaveConfiguration(string userConfigFileName, ushort?hostingPort, DatabaseConfiguration databaseConfiguration, GeneralConfiguration newGeneralConfiguration, FileLoggingConfiguration fileLoggingConfiguration, ControlPanelConfiguration controlPanelConfiguration, CancellationToken cancellationToken)
Saves a given Configuration set to userConfigFileName
Definition: SetupWizard.cs:490
readonly IHostingEnvironment hostingEnvironment
The IHostingEnvironment for the SetupWizard
Definition: SetupWizard.cs:36
async Task< ushort?> PromptForHostingPort(CancellationToken cancellationToken)
Prompts the user to enter the port to host TGS on
Definition: SetupWizard.cs:120
Configuration options for the Models.DatabaseContext<TParentContext>
async Task< FileLoggingConfiguration > ConfigureLogging(CancellationToken cancellationToken)
Prompts the user to create a FileLoggingConfiguration
Definition: SetupWizard.cs:377
readonly IAsyncDelayer asyncDelayer
The IAsyncDelayer for the SetupWizard
Definition: SetupWizard.cs:56
async Task< bool > CheckRunWizard(CancellationToken cancellationToken)
Run the setup wizard if necessary
Definition: SetupWizard.cs:573
readonly IPlatformIdentifier platformIdentifier
The IPlatformIdentifier for the SetupWizard
Definition: SetupWizard.cs:51
DatabaseType
Type of database to user
Definition: DatabaseType.cs:6
The command line Configuration setup wizard
Definition: ISetupWizard.cs:9
const string Section
The key for the Microsoft.Extensions.Configuration.IConfigurationSection the FileLoggingConfiguration...
Interface for using filesystems
Definition: IIOManager.cs:11
For identifying the current platform
readonly ILogger< SetupWizard > logger
The ILogger for the SetupWizard
Definition: SetupWizard.cs:61
async Task RunWizard(string userConfigFileName, CancellationToken cancellationToken)
Runs the SetupWizard
Definition: SetupWizard.cs:550
uint MinimumPasswordLength
Minimum length of database user passwords