From 584a55cd601d679991e95308c68218e9234cd525 Mon Sep 17 00:00:00 2001 From: Noi Date: Fri, 22 Jul 2022 23:41:49 -0700 Subject: [PATCH] Always load database config at start Ensures that the database settings used by ef tools are those in config. --- Data/BotDatabaseContext.cs | 26 +++++++------- InstanceConfig.cs | 70 ++++++++++++++++++++++++++------------ RegexBot.csproj | 1 + 3 files changed, 62 insertions(+), 35 deletions(-) diff --git a/Data/BotDatabaseContext.cs b/Data/BotDatabaseContext.cs index c3e10d7..88ab07b 100644 --- a/Data/BotDatabaseContext.cs +++ b/Data/BotDatabaseContext.cs @@ -1,22 +1,22 @@ using Microsoft.EntityFrameworkCore; +using Npgsql; namespace RegexBot.Data; /// /// Represents a database connection using the settings defined in the bot's global configuration. /// public class BotDatabaseContext : DbContext { - private static string? _npgsqlConnectionString; - internal static string PostgresConnectionString { -#if DEBUG - get { - if (_npgsqlConnectionString != null) return _npgsqlConnectionString; - Console.WriteLine($"{nameof(RegexBot)} - {nameof(BotDatabaseContext)} note: Using hardcoded connection string!"); - return _npgsqlConnectionString ?? "Host=localhost;Username=regexbot;Password=rb"; - } -#else - get => _npgsqlConnectionString!; -#endif - set => _npgsqlConnectionString ??= value; + private static readonly string _connectionString; + + static BotDatabaseContext() { + // Get our own config loaded just for the SQL stuff + var conf = new InstanceConfig(); + _connectionString = new NpgsqlConnectionStringBuilder() { + Host = conf.SqlHost ?? "localhost", // default to localhost + Database = conf.SqlDatabase, + Username = conf.SqlUsername, + Password = conf.SqlPassword + }.ToString(); } /// @@ -37,7 +37,7 @@ public class BotDatabaseContext : DbContext { /// protected sealed override void OnConfiguring(DbContextOptionsBuilder optionsBuilder) => optionsBuilder - .UseNpgsql(PostgresConnectionString) + .UseNpgsql(_connectionString) .UseSnakeCaseNamingConvention(); /// diff --git a/InstanceConfig.cs b/InstanceConfig.cs index de5a9e1..8e59fef 100644 --- a/InstanceConfig.cs +++ b/InstanceConfig.cs @@ -1,5 +1,6 @@ -using Newtonsoft.Json; -using RegexBot.Data; +using CommandLine; +using Newtonsoft.Json; +using System.Diagnostics.CodeAnalysis; using System.Reflection; namespace RegexBot; @@ -18,20 +19,20 @@ class InstanceConfig { /// List of assemblies to load, by file. Paths are always relative to the bot directory. /// internal IReadOnlyList Assemblies { get; } - - /// - /// Webhook URL for bot log reporting. - /// internal string InstanceLogTarget { get; } - // TODO add fields for services to be configurable: DMRelay + public string? SqlHost { get; } + public string? SqlDatabase { get; } + public string SqlUsername { get; } + public string SqlPassword { get; } /// /// Sets up instance configuration object from file and command line parameters. /// internal InstanceConfig() { - var path = Path.GetDirectoryName(Assembly.GetEntryAssembly()!.Location) - + "." + Path.DirectorySeparatorChar + "instance.json"; + var args = CommandLineParameters.Parse(Environment.GetCommandLineArgs()); + var path = args?.ConfigFile ?? Path.GetDirectoryName(Assembly.GetEntryAssembly()!.Location) + + Path.DirectorySeparatorChar + "." + Path.DirectorySeparatorChar + "instance.json"; JObject conf; try { @@ -45,19 +46,8 @@ class InstanceConfig { throw new Exception(pfx + ex.Message, ex); } - // Input validation - throw exception on errors. Exception messages printed as-is. - BotToken = conf[nameof(BotToken)]?.Value()!; - if (string.IsNullOrEmpty(BotToken)) - throw new Exception($"'{nameof(BotToken)}' is not properly specified in configuration."); - - var pginput = conf[nameof(BotDatabaseContext.PostgresConnectionString)]?.Value()!; - if (string.IsNullOrEmpty(pginput)) - throw new Exception($"'{nameof(BotDatabaseContext.PostgresConnectionString)}' is not properly specified in configuration."); - BotDatabaseContext.PostgresConnectionString = pginput; - - InstanceLogTarget = conf[nameof(InstanceLogTarget)]?.Value()!; - if (string.IsNullOrEmpty(InstanceLogTarget)) - throw new Exception($"'{nameof(InstanceLogTarget)}' is not properly specified in configuration."); + BotToken = ReadConfKey(conf, nameof(BotToken), true); + InstanceLogTarget = ReadConfKey(conf, nameof(InstanceLogTarget), true); try { Assemblies = Common.Utilities.LoadStringOrStringArray(conf[nameof(Assemblies)]).AsReadOnly(); @@ -66,5 +56,41 @@ class InstanceConfig { } catch (ArgumentException) { throw new Exception($"'{nameof(Assemblies)}' is not properly specified in configuration."); } + + SqlHost = ReadConfKey(conf, nameof(SqlHost), false); + SqlDatabase = ReadConfKey(conf, nameof(SqlDatabase), false); + SqlUsername = ReadConfKey(conf, nameof(SqlUsername), true); + SqlPassword = ReadConfKey(conf, nameof(SqlPassword), true); + } + + private static T? ReadConfKey(JObject jc, string key, [DoesNotReturnIf(true)] bool failOnEmpty) { + if (jc.ContainsKey(key)) return jc[key]!.Value(); + if (failOnEmpty) throw new Exception($"'{key}' must be specified in the instance configuration."); + return default; + } + + /// + /// Command line options + /// + class CommandLineParameters { + [Option('c', "config", Default = null, + HelpText = "Custom path to instance configuration. Defaults to instance.json in bot directory.")] + public string ConfigFile { get; set; } = null!; + + /// + /// Command line arguments parsed here. Depending on inputs, the program can exit here. + /// + public static CommandLineParameters? Parse(string[] args) { + CommandLineParameters? result = null; + + new Parser(settings => { + settings.IgnoreUnknownArguments = true; + settings.AutoHelp = false; + settings.AutoVersion = false; + }).ParseArguments(args) + .WithParsed(p => result = p) + .WithNotParsed(e => { /* ignore */ }); + return result; + } } } diff --git a/RegexBot.csproj b/RegexBot.csproj index ce34adb..955affa 100644 --- a/RegexBot.csproj +++ b/RegexBot.csproj @@ -20,6 +20,7 @@ +