Load database config on initialization

Adapted from RegexBot.
This allows `dotnet ef` tools to make use of actual SQL credentials
rather than dummy ones.
This commit is contained in:
Noi 2022-08-09 17:37:29 -07:00
parent 92be9ca073
commit 261d54197d
3 changed files with 69 additions and 80 deletions

View file

@ -1,8 +1,6 @@
using BirthdayBot.Data; using CommandLine;
using CommandLine; using Newtonsoft.Json;
using CommandLine.Text;
using Newtonsoft.Json.Linq; using Newtonsoft.Json.Linq;
using Npgsql;
using System.Diagnostics.CodeAnalysis; using System.Diagnostics.CodeAnalysis;
using System.Reflection; using System.Reflection;
using System.Text.RegularExpressions; using System.Text.RegularExpressions;
@ -13,10 +11,6 @@ namespace BirthdayBot;
/// Loads and holds configuration values. /// Loads and holds configuration values.
/// </summary> /// </summary>
class Configuration { class Configuration {
const string KeySqlHost = "SqlHost";
const string KeySqlUsername = "SqlUsername";
const string KeySqlPassword = "SqlPassword";
const string KeySqlDatabase = "SqlDatabase";
const string KeyShardRange = "ShardRange"; const string KeyShardRange = "ShardRange";
public string BotToken { get; } public string BotToken { get; }
@ -27,32 +21,44 @@ class Configuration {
public int ShardAmount { get; } public int ShardAmount { get; }
public int ShardTotal { 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) { public Configuration() {
var cmdline = CmdLineOpts.Parse(args); 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 // Looks for configuration file
var confPath = Path.GetDirectoryName(Assembly.GetExecutingAssembly().Location) + Path.DirectorySeparatorChar; JObject jc;
confPath += cmdline.Config!; try {
if (!File.Exists(confPath)) throw new Exception("Settings file not found in path: " + confPath); 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<string>(jc, nameof(BotToken), true); BotToken = ReadConfKey<string>(jc, nameof(BotToken), true);
DBotsToken = ReadConfKey<string>(jc, nameof(DBotsToken), false); DBotsToken = ReadConfKey<string>(jc, nameof(DBotsToken), false);
QuitOnFails = ReadConfKey<bool?>(jc, nameof(QuitOnFails), false) ?? false; QuitOnFails = ReadConfKey<bool?>(jc, nameof(QuitOnFails), false) ?? false;
ShardTotal = cmdline.ShardTotal ?? ReadConfKey<int?>(jc, nameof(ShardTotal), false) ?? 1; ShardTotal = args.ShardTotal ?? ReadConfKey<int?>(jc, nameof(ShardTotal), false) ?? 1;
if (ShardTotal < 1) throw new Exception($"'{nameof(ShardTotal)}' must be a positive integer."); if (ShardTotal < 1) throw new Exception($"'{nameof(ShardTotal)}' must be a positive integer.");
string shardRangeInput = cmdline.ShardRange ?? ReadConfKey<string>(jc, KeyShardRange, false); var shardRangeInput = args.ShardRange ?? ReadConfKey<string>(jc, KeyShardRange, false);
if (!string.IsNullOrWhiteSpace(shardRangeInput)) { if (!string.IsNullOrWhiteSpace(shardRangeInput)) {
Regex srPicker = new(@"(?<low>\d{1,2})[-,]{1}(?<high>\d{1,2})"); Regex srPicker = new(@"(?<low>\d{1,2})[-,]{1}(?<high>\d{1,2})");
var m = srPicker.Match(shardRangeInput); var m = srPicker.Match(shardRangeInput);
if (m.Success) { if (m.Success) {
ShardStart = int.Parse(m.Groups["low"].Value); 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); ShardAmount = high - (ShardStart - 1);
} else { } else {
throw new Exception($"Shard range not properly formatted in '{KeyShardRange}'."); throw new Exception($"Shard range not properly formatted in '{KeyShardRange}'.");
@ -63,20 +69,11 @@ class Configuration {
ShardAmount = ShardTotal; ShardAmount = ShardTotal;
} }
var sqlhost = ReadConfKey<string>(jc, KeySqlHost, false) ?? "localhost"; // Default to localhost SqlHost = ReadConfKey<string>(jc, nameof(SqlHost), false);
var sqluser = ReadConfKey<string>(jc, KeySqlUsername, false); SqlDatabase = ReadConfKey<string?>(jc, nameof(SqlDatabase), false);
var sqlpass = ReadConfKey<string>(jc, KeySqlPassword, false); SqlUsername = ReadConfKey<string>(jc, nameof(SqlUsername), true);
if (string.IsNullOrWhiteSpace(sqluser) || string.IsNullOrWhiteSpace(sqlpass)) SqlPassword = ReadConfKey<string>(jc, nameof(SqlPassword), true);
throw new Exception("'SqlUsername', 'SqlPassword' must be specified."); SqlApplicationName = $"ClientShard{ShardStart}+{ShardAmount}";
var csb = new NpgsqlConnectionStringBuilder() {
Host = sqlhost,
Username = sqluser,
Password = sqlpass,
ApplicationName = $"ClientShard{ShardStart}+{ShardAmount}"
};
var sqldb = ReadConfKey<string>(jc, KeySqlDatabase, false);
if (sqldb != null) csb.Database = sqldb; // Optional database setting
DatabaseConnectionString = csb.ToString();
} }
private static T? ReadConfKey<T>(JObject jc, string key, [DoesNotReturnIf(true)] bool failOnEmpty) { private static T? ReadConfKey<T>(JObject jc, string key, [DoesNotReturnIf(true)] bool failOnEmpty) {
@ -85,33 +82,27 @@ class Configuration {
return default; return default;
} }
private class CmdLineOpts { class CommandLineParameters {
[Option('c', "config", Default = "settings.json", [Option('c', "config")]
HelpText = "Custom path to instance configuration, relative from executable directory.")] public string? ConfigFile { get; set; }
public string? Config { get; set; }
[Option("shardtotal", [Option("shardtotal")]
HelpText = "Total number of shards online. MUST be the same for all instances.\n"
+ "This value overrides the config file value.")]
public int? ShardTotal { get; set; } public int? ShardTotal { get; set; }
[Option("shardrange", HelpText = "Shard range for this instance to handle.\n" [Option("shardrange")]
+ "This value overrides the config file value.")]
public string? ShardRange { get; set; } public string? ShardRange { get; set; }
public static CmdLineOpts Parse(string[] args) { public static CommandLineParameters? Parse(string[] args) {
// Do not automatically print help message CommandLineParameters? result = null;
var clp = new Parser(c => c.HelpWriter = null);
CmdLineOpts? result = null; new Parser(settings => {
var r = clp.ParseArguments<CmdLineOpts>(args); settings.IgnoreUnknownArguments = true;
r.WithParsed(parsed => result = parsed); settings.AutoHelp = false;
r.WithNotParsed(err => { settings.AutoVersion = false;
var ht = HelpText.AutoBuild(r); }).ParseArguments<CommandLineParameters>(args)
Console.WriteLine(ht.ToString()); .WithParsed(p => result = p)
Environment.Exit((int)Program.ExitCodes.BadCommand); .WithNotParsed(e => { /* ignore */ });
}); return result;
return result!;
} }
} }
} }

View file

@ -1,20 +1,22 @@
using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore;
using Npgsql;
namespace BirthdayBot.Data; namespace BirthdayBot.Data;
public class BotDatabaseContext : DbContext { public class BotDatabaseContext : DbContext {
private static string? _npgsqlConnectionString; private static readonly string _connectionString;
internal static string NpgsqlConnectionString {
#if DEBUG static BotDatabaseContext() {
get { // Get our own config loaded just for the SQL stuff
if (_npgsqlConnectionString != null) return _npgsqlConnectionString; var conf = new Configuration();
Program.Log(nameof(BotDatabaseContext), "Using hardcoded connection string!"); _connectionString = new NpgsqlConnectionStringBuilder() {
return _npgsqlConnectionString ?? "Host=localhost;Username=birthdaybot;Password=bb"; Host = conf.SqlHost ?? "localhost", // default to localhost
} Database = conf.SqlDatabase,
#else Username = conf.SqlUsername,
get => _npgsqlConnectionString!; Password = conf.SqlPassword,
#endif ApplicationName = conf.SqlApplicationName,
set => _npgsqlConnectionString ??= value; MaxPoolSize = Math.Max((int)Math.Ceiling(conf.ShardAmount * 2 * 0.6), 8)
}.ToString();
} }
public DbSet<BlocklistEntry> BlocklistEntries { get; set; } = null!; public DbSet<BlocklistEntry> BlocklistEntries { get; set; } = null!;
@ -22,11 +24,8 @@ public class BotDatabaseContext : DbContext {
public DbSet<UserEntry> UserEntries { get; set; } = null!; public DbSet<UserEntry> UserEntries { get; set; } = null!;
protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder) protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
=> optionsBuilder => optionsBuilder
.UseNpgsql(NpgsqlConnectionString) .UseNpgsql(_connectionString)
#if DEBUG
.LogTo((string line) => Program.Log("EF", line), Microsoft.Extensions.Logging.LogLevel.Information)
#endif
.UseSnakeCaseNamingConvention(); .UseSnakeCaseNamingConvention();
protected override void OnModelCreating(ModelBuilder modelBuilder) { protected override void OnModelCreating(ModelBuilder modelBuilder) {

View file

@ -14,21 +14,20 @@ class Program {
static async Task Main(string[] args) { static async Task Main(string[] args) {
Configuration? cfg = null; Configuration? cfg = null;
try { try {
cfg = new Configuration(args); cfg = new Configuration();
} catch (Exception ex) { } catch (Exception ex) {
Console.WriteLine(ex); Console.WriteLine(ex);
Environment.Exit((int)ExitCodes.ConfigError); Environment.Exit((int)ExitCodes.ConfigError);
} }
BotDatabaseContext.NpgsqlConnectionString = cfg.DatabaseConnectionString; Database.DBConnectionString = new Npgsql.NpgsqlConnectionStringBuilder() {
Host = cfg.SqlHost ?? "localhost", // default to localhost
Database.DBConnectionString = cfg.DatabaseConnectionString; Database = cfg.SqlDatabase,
try { Username = cfg.SqlUsername,
await Database.DoInitialDatabaseSetupAsync(); Password = cfg.SqlPassword,
} catch (Npgsql.NpgsqlException e) { ApplicationName = cfg.SqlApplicationName,
Console.WriteLine("Error when attempting to connect to database: " + e.Message); MaxPoolSize = Math.Max((int)Math.Ceiling(cfg.ShardAmount * 2 * 0.6), 8)
Environment.Exit((int)ExitCodes.DatabaseError); }.ToString();
}
Console.CancelKeyPress += OnCancelKeyPressed; Console.CancelKeyPress += OnCancelKeyPressed;
_bot = new ShardManager(cfg); _bot = new ShardManager(cfg);