Modulkontexte an die maßgebliche Datenbank; Shell-Shutdown weiter untersucht

Zwei Befunde aus dem ersten Dauerbetrieb auf dem Testserver.

#487: Der Fix zu #389 hatte nur den Core auf DatabaseEndpoint umgestellt. Alle
vier Modulkontexte lasen die Konfiguration weiter selbst und landeten auf dem
Platzhalter aus EffectiveConnectionString, sobald die Datenbank nur in
settings.json steht - "Access denied for user 'root'@'localhost'", waehrend der
Core im selben Prozess sauber arbeitete. Gefunden 30 Sekunden nach dem ersten
Start des Dienstes; kein Test und kein --check hatte es gesehen, weil --check
nur die Core-Verbindung oeffnet.

Die Kontexte loesen DatabaseEndpoint jetzt ueber die IServiceProvider-Ueberladung
von AddDbContextFactory auf. Der Platzhalter meldet sich ausserdem nicht mehr als
root an, sondern als "KEINE-DATENBANK-KONFIGURIERT" - die alte Meldung sah nach
einem Rechteproblem aus, obwohl gar keine Verbindung konfiguriert war.

#396 traegt NICHT. Die Pruefung auf der VM zeigt: Das Signal kommt an, der
Handler laeuft (Logzeile steht), aber der Prozess bleibt stehen. Zwei Ursachen
sind dabei aufgedeckt und behoben worden, eine dritte ist offen:

1. Ueber Avalonia.Application.Current war die Lebensdauer aus dem Signal-Thread
   nicht erreichbar - der Handler lief ins Leere. Sie wird jetzt im Hauptthread
   erzeugt (SetupWithLifetime + lifetime.Start) und dem Handler mitgegeben.
2. Die Sicherheitsabfrage der Shell fing das Schliessen ab und wartete auf einen
   Klick, den bei einem Signal niemand macht. Ein Signal ist keine Frage; der
   Handler uebergeht die Abfrage jetzt ausdruecklich.
3. OFFEN: Der Dispatcher-Rueckruf wird nicht ausgefuehrt - weder die Erfolgs-
   noch die Fehlerzeile erscheint, obwohl beide protokolliert wuerden. Der
   UI-Thread nimmt die Nachricht offenbar nicht an.

Die Messpunkte bleiben im Code: ohne sie war von aussen nicht zu unterscheiden,
ob das Signal ankommt, ob die Lebensdauer fehlt oder ob der Rueckruf haengt.

404 Tests gruen.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
Richard
2026-09-05 13:23:16 +02:00
co-authored by Claude Opus 5
parent 3bccd90d97
commit b618efa9ce
7 changed files with 105 additions and 24 deletions
+43 -10
View File
@@ -43,16 +43,29 @@ internal static class Program
{
App.Services = host.Services;
// Die Lebensdauer wird hier selbst erzeugt statt über StartWithClassicDesktopLifetime.
// Grund: Der Signal-Handler unten braucht sie, und über Avalonia.Application.Current
// ist sie aus einem fremden Thread nicht verlässlich zu bekommen. Am 2026-09-05 auf
// der Test-VM gemessen: Das SIGTERM kam an, aber der Handler fand keine Lebensdauer
// und lief ins Leere - die Shell blieb stehen wie vor dem Fix. Mit der Referenz aus
// dem Hauptthread ist der Zugriff eindeutig.
var lifetime = new ClassicDesktopStyleApplicationLifetime
{
Args = args,
ShutdownMode = Avalonia.Controls.ShutdownMode.OnLastWindowClose
};
// SIGTERM/SIGINT geordnet behandeln. Die Avalonia-Nachrichtenschleife endet sonst nur,
// wenn das letzte Fenster geschlossen wird: beim Abmelden, beim Herunterfahren des
// Rechners und bei jedem systemctl stop bekam die Shell ein SIGTERM, ignorierte es und
// wurde nach Ablauf der Frist mit SIGKILL abgeräumt – mitten im Worker-Lauf, ohne
// Modul- und Host-Stopp. Der Daemon behandelt beide Signale über den Host bereits.
using var sigterm = RegisterShutdownSignal(PosixSignal.SIGTERM, logger);
using var sigint = RegisterShutdownSignal(PosixSignal.SIGINT, logger);
var uiHost = host.Services.GetRequiredService<Shell.AvaloniaUiHost>();
using var sigterm = RegisterShutdownSignal(PosixSignal.SIGTERM, logger, lifetime, uiHost);
using var sigint = RegisterShutdownSignal(PosixSignal.SIGINT, logger, lifetime, uiHost);
BuildAvaloniaApp().StartWithClassicDesktopLifetime(args);
return 0;
BuildAvaloniaApp().SetupWithLifetime(lifetime);
return lifetime.Start(args);
}
finally
{
@@ -67,18 +80,38 @@ internal static class Program
/// Host-Stopp greift. Steht die Oberfläche noch nicht, bleibt die Vorgabebehandlung des
/// Signals stehen; kennt die Plattform das Signal nicht, läuft die Shell wie bisher weiter.
/// </summary>
private static IDisposable? RegisterShutdownSignal(PosixSignal signal, LoggingService logger)
private static IDisposable? RegisterShutdownSignal(
PosixSignal signal, LoggingService logger, IClassicDesktopStyleApplicationLifetime lifetime,
Shell.AvaloniaUiHost uiHost)
{
try
{
return PosixSignalRegistration.Create(signal, ctx =>
{
if (Avalonia.Application.Current?.ApplicationLifetime is not IClassicDesktopStyleApplicationLifetime desktop)
return;
ctx.Cancel = true; // sonst endet der Prozess sofort – ohne Modul- und Host-Stopp
// Die Vorgabebehandlung würde den Prozess beenden, ohne den finally-Zweig mit
// Modul- und Host-Stopp zu erreichen - und sie tut es bei einer laufenden
// Avalonia-Schleife ohnehin nicht zuverlässig (am 2026-09-05 nachgestellt: der
// Prozess blieb einfach stehen). Deshalb übernehmen wir das Signal ganz.
ctx.Cancel = true;
logger.Info("Core", $"{signal} empfangen – Oberfläche wird geordnet beendet.");
Dispatcher.UIThread.Post(() => desktop.Shutdown());
Dispatcher.UIThread.Post(() =>
{
// Fehler in diesem Rückruf verschwinden sonst spurlos - er läuft auf dem
// UI-Thread, und niemand wartet auf sein Ergebnis.
try
{
// Ohne diesen Schritt fängt die Sicherheitsabfrage der Shell das Schließen
// ab und wartet auf einen Klick, den bei einem Signal niemand macht.
uiHost.SkipShutdownConfirmation();
logger.Info("Core", "Sicherheitsabfrage übergangen, beende die Oberfläche.");
lifetime.Shutdown();
logger.Info("Core", "Shutdown angefordert.");
}
catch (Exception ex)
{
logger.Error("Core", $"Herunterfahren nach {signal} gescheitert: {ex.Message}", ex);
}
});
});
}
catch (Exception ex)