Verstaendlicher Abbruch statt Absturz, wenn die Datenbankverbindung fehlt
CI / Build & Tests (Linux) (push) Canceled after 0s
CI / Plattformneutralität & Hygiene (push) Canceled after 0s

Erster Befund aus dem Linux-Lauf auf der Test-VM (Roadmap A4a): Ohne
konfigurierte Datenbank startete die Anwendung los und starb erst tief im
DI-Aufbau mit einer unbehandelten ArgumentException und rund 50 Zeilen
Stacktrace - "The string argument 'connectionString' cannot be empty", geworfen
aus dem EF-Kern. Exitcode 134 (SIGABRT).

Fuer den Serverbetrieb ist das gleich doppelt schlecht: Der Betreiber sieht
nicht, was fehlt, und systemd startet den Dienst wegen Restart=on-failure alle
zehn Sekunden neu. Im Journal stehen dann endlose Startversuche statt der
Ursache. Genau dieser Zustand ist bei einer Erstinstallation der wahrscheinlichste
- Software liegt da, Zugangsdaten sind noch nicht gesetzt.

Behoben an drei Stellen:
- Program.Main prueft die Verbindung, bevor der Host gebaut wird, und gibt aus,
  welcher Schluessel in welcher Datei erwartet wird, samt Beispiel und dem
  Hinweis, dass das Schema per "dotnet ef database update" angelegt wird (die
  Anwendung migriert nicht selbst).
- Rueckgabewert 78 (EX_CONFIG aus den BSD-sysexits) statt eines Absturzes.
- deploy/polytrader.service traegt RestartPreventExitStatus=78 - ein
  Konfigurationsfehler repariert sich durch Neustarts nicht, er wird dadurch nur
  unsichtbar.
- AddCorePersistence wirft als zweite Verteidigungslinie eine
  InvalidOperationException mit derselben Aussage, falls jemand die Methode
  direkt aufruft.

Der Connection-String wird dabei ueber dieselben Dateien und denselben Schluessel
gelesen wie spaeter im Host, damit beide Wege nicht auseinanderlaufen koennen.

Build gruen, 476 Tests gruen (Windows und Linux).

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
Richard
2026-08-26 21:46:45 +02:00
co-authored by Claude Opus 5
parent e60dffbdf6
commit ad94f8c373
3 changed files with 71 additions and 1 deletions
+5
View File
@@ -29,6 +29,11 @@ WorkingDirectory=/opt/polytrader
ExecStart=/opt/polytrader/PolyTrader.App.Avalonia --headless ExecStart=/opt/polytrader/PolyTrader.App.Avalonia --headless
Restart=on-failure Restart=on-failure
RestartSec=10 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 # Absturz vom sauberen Beenden unterscheiden: SIGTERM statt SIGKILL, mit Frist fuer den
# geordneten Shutdown-Pfad (offene Positionen, Watchdog-"stopped_graceful", DB-Flush). # geordneten Shutdown-Pfad (offene Positionen, Watchdog-"stopped_graceful", DB-Flush).
+53
View File
@@ -35,6 +35,16 @@ namespace PolyTrader.App.Avalonia
{ {
private const string ServerSettingsPath = "server_settings.xml"; private const string ServerSettingsPath = "server_settings.xml";
/// <summary>
/// Rueckgabewert bei Konfigurationsfehlern. 78 ist EX_CONFIG aus den BSD-sysexits und
/// damit ein etablierter Wert fuer "Konfiguration unbrauchbar". Wichtig fuer den
/// Dienstbetrieb: <c>deploy/polytrader.service</c> traegt ihn in
/// <c>RestartPreventExitStatus</c> ein, damit systemd bei einem Konfigurationsfehler
/// NICHT im Zehn-Sekunden-Takt neu startet - ein fehlender Connection-String repariert
/// sich durch Neustarts nicht.
/// </summary>
private const int ExitCodeConfigurationError = 78;
[STAThread] [STAThread]
public static int Main(string[] args) public static int Main(string[] args)
{ {
@@ -49,6 +59,33 @@ namespace PolyTrader.App.Avalonia
var dcOptions = LoadDeploymentcenterOptions(); 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 // Konstruktionsprüfung (--smoke-ui) testet Fensteraufbau, keine Lizenzlogik - dort
// bewusst ALLE Module registrieren, sonst würde die Prüfung auf einem Build-Server // bewusst ALLE Module registrieren, sonst würde die Prüfung auf einem Build-Server
// (nie lizenziert) still einen Großteil ihrer Abdeckung verlieren. // (nie lizenziert) still einen Großteil ihrer Abdeckung verlieren.
@@ -113,6 +150,22 @@ namespace PolyTrader.App.Avalonia
return options; return options;
} }
/// <summary>
/// Liest den Connection-String, bevor der Generic Host existiert - fuer die fruehe
/// Konfigurationspruefung in <see cref="Main"/>. Liest dieselben Dateien und denselben
/// Schluessel wie der Host spaeter, damit beide nicht auseinanderlaufen koennen.
/// </summary>
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;
}
/// <summary> /// <summary>
/// Baut den Anwendungs-Host: Konfiguration, Master-Key, Persistenz, Core-Dienste und die /// Baut den Anwendungs-Host: Konfiguration, Master-Key, Persistenz, Core-Dienste und die
/// aktiven Module. Kennt die Oberfläche nicht. /// aktiven Module. Kennt die Oberfläche nicht.
@@ -1,4 +1,4 @@
using System; using System;
using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.DependencyInjection;
using PolyTrader.Core.Configuration; using PolyTrader.Core.Configuration;
@@ -17,6 +17,18 @@ namespace PolyTrader.Core.DependencyInjection
public static IServiceCollection AddCorePersistence(this IServiceCollection services, DatabaseOptions options) public static IServiceCollection AddCorePersistence(this IServiceCollection services, DatabaseOptions options)
{ {
var conn = options.MySqlConnectionString; 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<CoreDbContext>(o => o.UseMySql(conn, DatabaseServerVersion.Value)); services.AddDbContextFactory<CoreDbContext>(o => o.UseMySql(conn, DatabaseServerVersion.Value));
services.AddSingleton<IAccountRepository, EfAccountRepository>(); services.AddSingleton<IAccountRepository, EfAccountRepository>();