diff --git a/deploy/polytrader.service b/deploy/polytrader.service index 06ddcf6..85d6dc2 100644 --- a/deploy/polytrader.service +++ b/deploy/polytrader.service @@ -29,6 +29,11 @@ WorkingDirectory=/opt/polytrader ExecStart=/opt/polytrader/PolyTrader.App.Avalonia --headless Restart=on-failure RestartSec=10 +# Exit 78 (EX_CONFIG) meldet einen Konfigurationsfehler - etwa einen fehlenden +# Connection-String. Ein Neustart repariert das nicht, er verdeckt es nur: der Dienst +# liefe im Zehn-Sekunden-Takt an und wieder aus, und im Journal saehe man endlose +# Startversuche statt der eigentlichen Ursache. Deshalb hier bewusst nicht neu starten. +RestartPreventExitStatus=78 # Absturz vom sauberen Beenden unterscheiden: SIGTERM statt SIGKILL, mit Frist fuer den # geordneten Shutdown-Pfad (offene Positionen, Watchdog-"stopped_graceful", DB-Flush). diff --git a/src/PolyTrader.App.Avalonia/Program.cs b/src/PolyTrader.App.Avalonia/Program.cs index 306f7aa..61ffdda 100644 --- a/src/PolyTrader.App.Avalonia/Program.cs +++ b/src/PolyTrader.App.Avalonia/Program.cs @@ -35,6 +35,16 @@ namespace PolyTrader.App.Avalonia { private const string ServerSettingsPath = "server_settings.xml"; + /// + /// Rueckgabewert bei Konfigurationsfehlern. 78 ist EX_CONFIG aus den BSD-sysexits und + /// damit ein etablierter Wert fuer "Konfiguration unbrauchbar". Wichtig fuer den + /// Dienstbetrieb: deploy/polytrader.service traegt ihn in + /// RestartPreventExitStatus ein, damit systemd bei einem Konfigurationsfehler + /// NICHT im Zehn-Sekunden-Takt neu startet - ein fehlender Connection-String repariert + /// sich durch Neustarts nicht. + /// + private const int ExitCodeConfigurationError = 78; + [STAThread] public static int Main(string[] args) { @@ -49,6 +59,33 @@ namespace PolyTrader.App.Avalonia var dcOptions = LoadDeploymentcenterOptions(); + // Ohne Datenbankverbindung kann die Anwendung nichts Sinnvolles tun - jedes Repository + // haengt daran. Bis zum 26.08.2026 lief sie trotzdem los und starb erst tief im + // DI-Aufbau mit einer ArgumentException samt 50 Zeilen Stacktrace ("The string argument + // 'connectionString' cannot be empty"). Auf einem Server ist das doppelt schlecht: + // Der Betreiber sieht nicht, was fehlt, und systemd startet den Dienst wegen + // Restart=on-failure endlos neu. Deshalb hier frueh und verstaendlich abbrechen. + var connectionString = LoadDatabaseConnectionString(); + if (string.IsNullOrWhiteSpace(connectionString)) + { + var err = Console.Error; + err.WriteLine("FEHLER: Keine Datenbankverbindung konfiguriert."); + err.WriteLine(); + err.WriteLine("Erwartet wird 'Database:MySqlConnectionString' in appsettings.Local.json"); + err.WriteLine($"neben der Programmdatei ({AppContext.BaseDirectory})."); + err.WriteLine(); + err.WriteLine("Beispiel:"); + err.WriteLine("{"); + err.WriteLine(" \"Database\": {"); + err.WriteLine(" \"MySqlConnectionString\": \"Server=HOST;Port=3306;Database=polytrader;User ID=BENUTZER;Password=PASSWORT;\""); + err.WriteLine(" }"); + err.WriteLine("}"); + err.WriteLine(); + err.WriteLine("Die Datei ist bewusst nicht versioniert (Zugangsdaten). Das Schema legt"); + err.WriteLine("'dotnet ef database update' an - die Anwendung migriert nicht selbst."); + return ExitCodeConfigurationError; + } + // Konstruktionsprüfung (--smoke-ui) testet Fensteraufbau, keine Lizenzlogik - dort // bewusst ALLE Module registrieren, sonst würde die Prüfung auf einem Build-Server // (nie lizenziert) still einen Großteil ihrer Abdeckung verlieren. @@ -113,6 +150,22 @@ namespace PolyTrader.App.Avalonia return options; } + /// + /// Liest den Connection-String, bevor der Generic Host existiert - fuer die fruehe + /// Konfigurationspruefung in . Liest dieselben Dateien und denselben + /// Schluessel wie der Host spaeter, damit beide nicht auseinanderlaufen koennen. + /// + internal static string LoadDatabaseConnectionString() + { + var config = new ConfigurationBuilder() + .SetBasePath(AppContext.BaseDirectory) + .AddJsonFile("appsettings.json", optional: true) + .AddJsonFile("appsettings.Local.json", optional: true) + .Build(); + + return config["Database:MySqlConnectionString"] ?? string.Empty; + } + /// /// Baut den Anwendungs-Host: Konfiguration, Master-Key, Persistenz, Core-Dienste und die /// aktiven Module. Kennt die Oberfläche nicht. diff --git a/src/PolyTrader.Core/DependencyInjection/ServiceCollectionExtensions.cs b/src/PolyTrader.Core/DependencyInjection/ServiceCollectionExtensions.cs index b1d1fac..d038b3e 100644 --- a/src/PolyTrader.Core/DependencyInjection/ServiceCollectionExtensions.cs +++ b/src/PolyTrader.Core/DependencyInjection/ServiceCollectionExtensions.cs @@ -1,4 +1,4 @@ -using System; +using System; using Microsoft.EntityFrameworkCore; using Microsoft.Extensions.DependencyInjection; using PolyTrader.Core.Configuration; @@ -17,6 +17,18 @@ namespace PolyTrader.Core.DependencyInjection public static IServiceCollection AddCorePersistence(this IServiceCollection services, DatabaseOptions options) { var conn = options.MySqlConnectionString; + + // Zweite Verteidigungslinie. Der Regelfall wird frueh in Program.Main abgefangen + // (verstaendliche Meldung + Exit 78); wer diese Methode direkt aufruft, bekam bisher + // erst tief im DI-Aufbau eine ArgumentException aus dem EF-Kern, der man den + // eigentlichen Grund nicht ansieht. + if (string.IsNullOrWhiteSpace(conn)) + { + throw new InvalidOperationException( + "Kein MySQL-Connection-String konfiguriert. Erwartet wird " + + "'Database:MySqlConnectionString' in appsettings.Local.json neben der Programmdatei."); + } + services.AddDbContextFactory(o => o.UseMySql(conn, DatabaseServerVersion.Value)); services.AddSingleton();