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:
@@ -43,16 +43,29 @@ internal static class Program
|
|||||||
{
|
{
|
||||||
App.Services = host.Services;
|
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,
|
// SIGTERM/SIGINT geordnet behandeln. Die Avalonia-Nachrichtenschleife endet sonst nur,
|
||||||
// wenn das letzte Fenster geschlossen wird: beim Abmelden, beim Herunterfahren des
|
// 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
|
// 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
|
// 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.
|
// Modul- und Host-Stopp. Der Daemon behandelt beide Signale über den Host bereits.
|
||||||
using var sigterm = RegisterShutdownSignal(PosixSignal.SIGTERM, logger);
|
var uiHost = host.Services.GetRequiredService<Shell.AvaloniaUiHost>();
|
||||||
using var sigint = RegisterShutdownSignal(PosixSignal.SIGINT, logger);
|
using var sigterm = RegisterShutdownSignal(PosixSignal.SIGTERM, logger, lifetime, uiHost);
|
||||||
|
using var sigint = RegisterShutdownSignal(PosixSignal.SIGINT, logger, lifetime, uiHost);
|
||||||
|
|
||||||
BuildAvaloniaApp().StartWithClassicDesktopLifetime(args);
|
BuildAvaloniaApp().SetupWithLifetime(lifetime);
|
||||||
return 0;
|
return lifetime.Start(args);
|
||||||
}
|
}
|
||||||
finally
|
finally
|
||||||
{
|
{
|
||||||
@@ -67,18 +80,38 @@ internal static class Program
|
|||||||
/// Host-Stopp greift. Steht die Oberfläche noch nicht, bleibt die Vorgabebehandlung des
|
/// 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.
|
/// Signals stehen; kennt die Plattform das Signal nicht, läuft die Shell wie bisher weiter.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
private static IDisposable? RegisterShutdownSignal(PosixSignal signal, LoggingService logger)
|
private static IDisposable? RegisterShutdownSignal(
|
||||||
|
PosixSignal signal, LoggingService logger, IClassicDesktopStyleApplicationLifetime lifetime,
|
||||||
|
Shell.AvaloniaUiHost uiHost)
|
||||||
{
|
{
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
return PosixSignalRegistration.Create(signal, ctx =>
|
return PosixSignalRegistration.Create(signal, ctx =>
|
||||||
{
|
{
|
||||||
if (Avalonia.Application.Current?.ApplicationLifetime is not IClassicDesktopStyleApplicationLifetime desktop)
|
// Die Vorgabebehandlung würde den Prozess beenden, ohne den finally-Zweig mit
|
||||||
return;
|
// 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
|
||||||
ctx.Cancel = true; // sonst endet der Prozess sofort – ohne Modul- und Host-Stopp
|
// Prozess blieb einfach stehen). Deshalb übernehmen wir das Signal ganz.
|
||||||
|
ctx.Cancel = true;
|
||||||
logger.Info("Core", $"{signal} empfangen – Oberfläche wird geordnet beendet.");
|
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)
|
catch (Exception ex)
|
||||||
|
|||||||
@@ -120,6 +120,21 @@ public sealed class AvaloniaUiHost : IModuleUiHost
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Übergeht die Sicherheitsabfrage – für ein Herunterfahren, das nicht vom Bediener kommt.
|
||||||
|
///
|
||||||
|
/// <para><b>Ein Signal ist keine Frage.</b> Die Abfrage schützt vor einem versehentlichen
|
||||||
|
/// Klick auf das Schließen-X; sie darf nicht zwischen dem Betriebssystem und dem Ende des
|
||||||
|
/// Prozesses stehen. Bei <c>systemctl stop</c>, beim Abmelden und beim Herunterfahren des
|
||||||
|
/// Rechners wartet niemand auf einen Dialog – die Shell hing dort und wurde nach Ablauf der
|
||||||
|
/// Frist mit SIGKILL abgeräumt, mitten im Worker-Lauf (am 2026-09-05 nachgestellt).</para>
|
||||||
|
/// </summary>
|
||||||
|
public void SkipShutdownConfirmation()
|
||||||
|
{
|
||||||
|
ShutdownConfirmed = true;
|
||||||
|
CloseAllViews();
|
||||||
|
}
|
||||||
|
|
||||||
/// <summary>Schließt alle offenen View-Fenster (beim Herunterfahren).</summary>
|
/// <summary>Schließt alle offenen View-Fenster (beim Herunterfahren).</summary>
|
||||||
public void CloseAllViews()
|
public void CloseAllViews()
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -20,12 +20,23 @@ public static class ServiceCollectionExtensions
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Liefert den Connection-String oder – bei leerem Wert (z. B. fehlende appsettings.Local.json) –
|
/// Liefert den Connection-String oder – bei leerem Wert – einen Platzhalter, damit der
|
||||||
/// einen unschädlichen Platzhalter, damit der DbContext-Optionsbau nicht wirft. Die App startet so
|
/// DbContext-Optionsbau nicht wirft. Die App startet so unabhängig von der Datenbank; echte
|
||||||
/// unabhängig von der DB; echte Queries schlagen erst bei tatsächlichem Zugriff fehl.
|
/// Abfragen scheitern erst beim tatsächlichen Zugriff.
|
||||||
|
///
|
||||||
|
/// <para><b>Der Platzhalter meldet sich bewusst nicht mehr als <c>root</c> an.</b> Vorher
|
||||||
|
/// stand dort „User ID=root;Password=;", und eine fehlende Konfiguration führte damit zu
|
||||||
|
/// „Access denied for user 'root'@'localhost'" – einer Meldung, die nach einem Rechteproblem
|
||||||
|
/// aussieht, während in Wahrheit gar keine Verbindung konfiguriert war. Der Nutzername sagt
|
||||||
|
/// jetzt, was los ist. Ein Anmeldeversuch als Administrator des Datenbankservers ist ohnehin
|
||||||
|
/// das Letzte, was ein Platzhalter tun sollte.</para>
|
||||||
|
///
|
||||||
|
/// <para>Wer die maßgebliche Verbindung braucht, nimmt <see cref="Configuration.DatabaseEndpoint"/>;
|
||||||
|
/// diese Methode ist nur der Notnagel für den Optionsbau.</para>
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public static string EffectiveConnectionString(string? connectionString)
|
public static string EffectiveConnectionString(string? connectionString)
|
||||||
=> string.IsNullOrWhiteSpace(connectionString)
|
=> string.IsNullOrWhiteSpace(connectionString)
|
||||||
? "Server=localhost;Port=3306;Database=ibkrtrader;User ID=root;Password=;"
|
? "Server=localhost;Port=3306;Database=ibkrtrader;" +
|
||||||
|
"User ID=KEINE-DATENBANK-KONFIGURIERT;Password=;"
|
||||||
: connectionString;
|
: connectionString;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -27,8 +27,14 @@ public sealed class AccountingModule : IModule
|
|||||||
|
|
||||||
public void RegisterServices(IServiceCollection services, IConfiguration configuration)
|
public void RegisterServices(IServiceCollection services, IConfiguration configuration)
|
||||||
{
|
{
|
||||||
var conn = ServiceCollectionExtensions.EffectiveConnectionString(configuration["Database:MySqlConnectionString"]);
|
// Modul-Persistenz: eigener DbContext in DERSELBEN Datenbank wie der Core. Die Verbindung
|
||||||
services.AddDbContextFactory<AccountingDbContext>(o => o.UseMySql(conn, DatabaseServerVersion.Value));
|
// kommt aus DatabaseEndpoint und wird erst beim Erzeugen des Kontexts aufgelöst - beim
|
||||||
|
// Registrieren gibt es den Dienst noch nicht. Vorher las jedes Modul die Konfiguration
|
||||||
|
// selbst und landete auf dem root-Platzhalter, sobald die Datenbank nur in settings.json
|
||||||
|
// stand; der Core arbeitete daneben sauber weiter (#487).
|
||||||
|
services.AddDbContextFactory<AccountingDbContext>((sp, o) =>
|
||||||
|
o.UseMySql(sp.GetRequiredService<DatabaseEndpoint>().ConnectionString,
|
||||||
|
DatabaseServerVersion.Value));
|
||||||
|
|
||||||
services.AddSingleton<ILedgerRepository, EfLedgerRepository>();
|
services.AddSingleton<ILedgerRepository, EfLedgerRepository>();
|
||||||
services.AddSingleton<IIngestRunRepository, EfIngestRunRepository>();
|
services.AddSingleton<IIngestRunRepository, EfIngestRunRepository>();
|
||||||
|
|||||||
@@ -39,9 +39,14 @@ public sealed class CongressTradingModule : IModule
|
|||||||
private void Register(IServiceCollection services, IConfiguration configuration,
|
private void Register(IServiceCollection services, IConfiguration configuration,
|
||||||
ModuleExecutionFactory? execution)
|
ModuleExecutionFactory? execution)
|
||||||
{
|
{
|
||||||
// Modul-Persistenz: eigener DbContext (ct_-Tabellen) in derselben MariaDB.
|
// Modul-Persistenz: eigener DbContext in DERSELBEN Datenbank wie der Core. Die Verbindung
|
||||||
var conn = ServiceCollectionExtensions.EffectiveConnectionString(configuration["Database:MySqlConnectionString"]);
|
// kommt aus DatabaseEndpoint und wird erst beim Erzeugen des Kontexts aufgelöst - beim
|
||||||
services.AddDbContextFactory<CongressTradingDbContext>(o => o.UseMySql(conn, DatabaseServerVersion.Value));
|
// Registrieren gibt es den Dienst noch nicht. Vorher las jedes Modul die Konfiguration
|
||||||
|
// selbst und landete auf dem root-Platzhalter, sobald die Datenbank nur in settings.json
|
||||||
|
// stand; der Core arbeitete daneben sauber weiter (#487).
|
||||||
|
services.AddDbContextFactory<CongressTradingDbContext>((sp, o) =>
|
||||||
|
o.UseMySql(sp.GetRequiredService<DatabaseEndpoint>().ConnectionString,
|
||||||
|
DatabaseServerVersion.Value));
|
||||||
|
|
||||||
services.AddSingleton<CongressRepository>();
|
services.AddSingleton<CongressRepository>();
|
||||||
services.AddSingleton<CapitolTradesScraper>();
|
services.AddSingleton<CapitolTradesScraper>();
|
||||||
|
|||||||
@@ -35,9 +35,14 @@ public sealed class OptionsWheelModule : IModule
|
|||||||
public void RegisterServices(IServiceCollection services, IConfiguration configuration)
|
public void RegisterServices(IServiceCollection services, IConfiguration configuration)
|
||||||
{
|
{
|
||||||
// Modul-Persistenz: eigener DbContext (ow_-Tabellen) in derselben MariaDB.
|
// Modul-Persistenz: eigener DbContext (ow_-Tabellen) in derselben MariaDB.
|
||||||
var conn = ServiceCollectionExtensions.EffectiveConnectionString(
|
// Modul-Persistenz: eigener DbContext in DERSELBEN Datenbank wie der Core. Die Verbindung
|
||||||
configuration["Database:MySqlConnectionString"]);
|
// kommt aus DatabaseEndpoint und wird erst beim Erzeugen des Kontexts aufgelöst - beim
|
||||||
services.AddDbContextFactory<OptionsWheelDbContext>(o => o.UseMySql(conn, DatabaseServerVersion.Value));
|
// Registrieren gibt es den Dienst noch nicht. Vorher las jedes Modul die Konfiguration
|
||||||
|
// selbst und landete auf dem root-Platzhalter, sobald die Datenbank nur in settings.json
|
||||||
|
// stand; der Core arbeitete daneben sauber weiter (#487).
|
||||||
|
services.AddDbContextFactory<OptionsWheelDbContext>((sp, o) =>
|
||||||
|
o.UseMySql(sp.GetRequiredService<DatabaseEndpoint>().ConnectionString,
|
||||||
|
DatabaseServerVersion.Value));
|
||||||
|
|
||||||
// Reine Strategie-Logik. Zustandslos und ohne Aussenwelt – deshalb als Singleton
|
// Reine Strategie-Logik. Zustandslos und ohne Aussenwelt – deshalb als Singleton
|
||||||
// unbedenklich und in den Tests direkt konstruierbar.
|
// unbedenklich und in den Tests direkt konstruierbar.
|
||||||
|
|||||||
@@ -28,8 +28,14 @@ public sealed class SupervisorModule : IModule
|
|||||||
public void RegisterServices(IServiceCollection services, IConfiguration configuration)
|
public void RegisterServices(IServiceCollection services, IConfiguration configuration)
|
||||||
{
|
{
|
||||||
// sup_-Persistenz (gespeicherte Analysen/Berichte + Counterfactuals).
|
// sup_-Persistenz (gespeicherte Analysen/Berichte + Counterfactuals).
|
||||||
var conn = ServiceCollectionExtensions.EffectiveConnectionString(configuration["Database:MySqlConnectionString"]);
|
// Modul-Persistenz: eigener DbContext in DERSELBEN Datenbank wie der Core. Die Verbindung
|
||||||
services.AddDbContextFactory<SupervisorDbContext>(o => o.UseMySql(conn, DatabaseServerVersion.Value));
|
// kommt aus DatabaseEndpoint und wird erst beim Erzeugen des Kontexts aufgelöst - beim
|
||||||
|
// Registrieren gibt es den Dienst noch nicht. Vorher las jedes Modul die Konfiguration
|
||||||
|
// selbst und landete auf dem root-Platzhalter, sobald die Datenbank nur in settings.json
|
||||||
|
// stand; der Core arbeitete daneben sauber weiter (#487).
|
||||||
|
services.AddDbContextFactory<SupervisorDbContext>((sp, o) =>
|
||||||
|
o.UseMySql(sp.GetRequiredService<DatabaseEndpoint>().ConnectionString,
|
||||||
|
DatabaseServerVersion.Value));
|
||||||
services.AddSingleton<ISupervisorReportRepository, EfSupervisorReportRepository>();
|
services.AddSingleton<ISupervisorReportRepository, EfSupervisorReportRepository>();
|
||||||
services.AddSingleton<ISupervisorCounterfactualRepository, EfSupervisorCounterfactualRepository>();
|
services.AddSingleton<ISupervisorCounterfactualRepository, EfSupervisorCounterfactualRepository>();
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user