From 261d54197d277fadcf769ec49f02e6adde5d69af Mon Sep 17 00:00:00 2001 From: Noi Date: Tue, 9 Aug 2022 17:37:29 -0700 Subject: [PATCH] Load database config on initialization Adapted from RegexBot. This allows `dotnet ef` tools to make use of actual SQL credentials rather than dummy ones. --- Configuration.cs | 97 +++++++++++++++++--------------------- Data/BotDatabaseContext.cs | 33 +++++++------ Program.cs | 19 ++++---- 3 files changed, 69 insertions(+), 80 deletions(-) diff --git a/Configuration.cs b/Configuration.cs index 5f799c3..48503a7 100644 --- a/Configuration.cs +++ b/Configuration.cs @@ -1,8 +1,6 @@ -using BirthdayBot.Data; -using CommandLine; -using CommandLine.Text; +using CommandLine; +using Newtonsoft.Json; using Newtonsoft.Json.Linq; -using Npgsql; using System.Diagnostics.CodeAnalysis; using System.Reflection; using System.Text.RegularExpressions; @@ -13,10 +11,6 @@ namespace BirthdayBot; /// Loads and holds configuration values. /// class Configuration { - const string KeySqlHost = "SqlHost"; - const string KeySqlUsername = "SqlUsername"; - const string KeySqlPassword = "SqlPassword"; - const string KeySqlDatabase = "SqlDatabase"; const string KeyShardRange = "ShardRange"; public string BotToken { get; } @@ -27,32 +21,44 @@ class Configuration { public int ShardAmount { get; } public int ShardTotal { get; } - public string DatabaseConnectionString { get; } + public string? SqlHost { get; } + public string? SqlDatabase { get; } + public string SqlUsername { get; } + public string SqlPassword { get; } + internal string SqlApplicationName { get; } - public Configuration(string[] args) { - var cmdline = CmdLineOpts.Parse(args); + public Configuration() { + var args = CommandLineParameters.Parse(Environment.GetCommandLineArgs()); + var path = args?.ConfigFile ?? Path.GetDirectoryName(Assembly.GetEntryAssembly()!.Location) + + Path.DirectorySeparatorChar + "." + Path.DirectorySeparatorChar + "settings.json"; // Looks for configuration file - var confPath = Path.GetDirectoryName(Assembly.GetExecutingAssembly().Location) + Path.DirectorySeparatorChar; - confPath += cmdline.Config!; - if (!File.Exists(confPath)) throw new Exception("Settings file not found in path: " + confPath); + JObject jc; + try { + var conftxt = File.ReadAllText(path); + jc = JObject.Parse(conftxt); + } catch (Exception ex) { + string pfx; + if (ex is JsonException) pfx = "Unable to parse configuration: "; + else pfx = "Unable to access configuration: "; - var jc = JObject.Parse(File.ReadAllText(confPath)); + throw new Exception(pfx + ex.Message, ex); + } BotToken = ReadConfKey(jc, nameof(BotToken), true); DBotsToken = ReadConfKey(jc, nameof(DBotsToken), false); QuitOnFails = ReadConfKey(jc, nameof(QuitOnFails), false) ?? false; - ShardTotal = cmdline.ShardTotal ?? ReadConfKey(jc, nameof(ShardTotal), false) ?? 1; + ShardTotal = args.ShardTotal ?? ReadConfKey(jc, nameof(ShardTotal), false) ?? 1; if (ShardTotal < 1) throw new Exception($"'{nameof(ShardTotal)}' must be a positive integer."); - string shardRangeInput = cmdline.ShardRange ?? ReadConfKey(jc, KeyShardRange, false); + var shardRangeInput = args.ShardRange ?? ReadConfKey(jc, KeyShardRange, false); if (!string.IsNullOrWhiteSpace(shardRangeInput)) { Regex srPicker = new(@"(?\d{1,2})[-,]{1}(?\d{1,2})"); var m = srPicker.Match(shardRangeInput); if (m.Success) { ShardStart = int.Parse(m.Groups["low"].Value); - int high = int.Parse(m.Groups["high"].Value); + var high = int.Parse(m.Groups["high"].Value); ShardAmount = high - (ShardStart - 1); } else { throw new Exception($"Shard range not properly formatted in '{KeyShardRange}'."); @@ -63,20 +69,11 @@ class Configuration { ShardAmount = ShardTotal; } - var sqlhost = ReadConfKey(jc, KeySqlHost, false) ?? "localhost"; // Default to localhost - var sqluser = ReadConfKey(jc, KeySqlUsername, false); - var sqlpass = ReadConfKey(jc, KeySqlPassword, false); - if (string.IsNullOrWhiteSpace(sqluser) || string.IsNullOrWhiteSpace(sqlpass)) - throw new Exception("'SqlUsername', 'SqlPassword' must be specified."); - var csb = new NpgsqlConnectionStringBuilder() { - Host = sqlhost, - Username = sqluser, - Password = sqlpass, - ApplicationName = $"ClientShard{ShardStart}+{ShardAmount}" - }; - var sqldb = ReadConfKey(jc, KeySqlDatabase, false); - if (sqldb != null) csb.Database = sqldb; // Optional database setting - DatabaseConnectionString = csb.ToString(); + SqlHost = ReadConfKey(jc, nameof(SqlHost), false); + SqlDatabase = ReadConfKey(jc, nameof(SqlDatabase), false); + SqlUsername = ReadConfKey(jc, nameof(SqlUsername), true); + SqlPassword = ReadConfKey(jc, nameof(SqlPassword), true); + SqlApplicationName = $"ClientShard{ShardStart}+{ShardAmount}"; } private static T? ReadConfKey(JObject jc, string key, [DoesNotReturnIf(true)] bool failOnEmpty) { @@ -85,33 +82,27 @@ class Configuration { return default; } - private class CmdLineOpts { - [Option('c', "config", Default = "settings.json", - HelpText = "Custom path to instance configuration, relative from executable directory.")] - public string? Config { get; set; } + class CommandLineParameters { + [Option('c', "config")] + public string? ConfigFile { get; set; } - [Option("shardtotal", - HelpText = "Total number of shards online. MUST be the same for all instances.\n" - + "This value overrides the config file value.")] + [Option("shardtotal")] public int? ShardTotal { get; set; } - [Option("shardrange", HelpText = "Shard range for this instance to handle.\n" - + "This value overrides the config file value.")] + [Option("shardrange")] public string? ShardRange { get; set; } - public static CmdLineOpts Parse(string[] args) { - // Do not automatically print help message - var clp = new Parser(c => c.HelpWriter = null); + public static CommandLineParameters? Parse(string[] args) { + CommandLineParameters? result = null; - CmdLineOpts? result = null; - var r = clp.ParseArguments(args); - r.WithParsed(parsed => result = parsed); - r.WithNotParsed(err => { - var ht = HelpText.AutoBuild(r); - Console.WriteLine(ht.ToString()); - Environment.Exit((int)Program.ExitCodes.BadCommand); - }); - return result!; + 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/Data/BotDatabaseContext.cs b/Data/BotDatabaseContext.cs index cc88741..2fc2616 100644 --- a/Data/BotDatabaseContext.cs +++ b/Data/BotDatabaseContext.cs @@ -1,20 +1,22 @@ using Microsoft.EntityFrameworkCore; +using Npgsql; namespace BirthdayBot.Data; public class BotDatabaseContext : DbContext { - private static string? _npgsqlConnectionString; - internal static string NpgsqlConnectionString { -#if DEBUG - get { - if (_npgsqlConnectionString != null) return _npgsqlConnectionString; - Program.Log(nameof(BotDatabaseContext), "Using hardcoded connection string!"); - return _npgsqlConnectionString ?? "Host=localhost;Username=birthdaybot;Password=bb"; - } -#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 Configuration(); + _connectionString = new NpgsqlConnectionStringBuilder() { + Host = conf.SqlHost ?? "localhost", // default to localhost + Database = conf.SqlDatabase, + Username = conf.SqlUsername, + Password = conf.SqlPassword, + ApplicationName = conf.SqlApplicationName, + MaxPoolSize = Math.Max((int)Math.Ceiling(conf.ShardAmount * 2 * 0.6), 8) + }.ToString(); } public DbSet BlocklistEntries { get; set; } = null!; @@ -22,11 +24,8 @@ public class BotDatabaseContext : DbContext { public DbSet UserEntries { get; set; } = null!; protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder) - => optionsBuilder - .UseNpgsql(NpgsqlConnectionString) -#if DEBUG - .LogTo((string line) => Program.Log("EF", line), Microsoft.Extensions.Logging.LogLevel.Information) -#endif + => optionsBuilder + .UseNpgsql(_connectionString) .UseSnakeCaseNamingConvention(); protected override void OnModelCreating(ModelBuilder modelBuilder) { diff --git a/Program.cs b/Program.cs index 52e13ac..38b83f6 100644 --- a/Program.cs +++ b/Program.cs @@ -14,21 +14,20 @@ class Program { static async Task Main(string[] args) { Configuration? cfg = null; try { - cfg = new Configuration(args); + cfg = new Configuration(); } catch (Exception ex) { Console.WriteLine(ex); Environment.Exit((int)ExitCodes.ConfigError); } - BotDatabaseContext.NpgsqlConnectionString = cfg.DatabaseConnectionString; - - Database.DBConnectionString = cfg.DatabaseConnectionString; - try { - await Database.DoInitialDatabaseSetupAsync(); - } catch (Npgsql.NpgsqlException e) { - Console.WriteLine("Error when attempting to connect to database: " + e.Message); - Environment.Exit((int)ExitCodes.DatabaseError); - } + Database.DBConnectionString = new Npgsql.NpgsqlConnectionStringBuilder() { + Host = cfg.SqlHost ?? "localhost", // default to localhost + Database = cfg.SqlDatabase, + Username = cfg.SqlUsername, + Password = cfg.SqlPassword, + ApplicationName = cfg.SqlApplicationName, + MaxPoolSize = Math.Max((int)Math.Ceiling(cfg.ShardAmount * 2 * 0.6), 8) + }.ToString(); Console.CancelKeyPress += OnCancelKeyPressed; _bot = new ShardManager(cfg);