diff --git a/src/IBKRTrader.App/Program.cs b/src/IBKRTrader.App/Program.cs index ec5c4bb..71bcec6 100644 --- a/src/IBKRTrader.App/Program.cs +++ b/src/IBKRTrader.App/Program.cs @@ -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(); + 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. /// - 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) diff --git a/src/IBKRTrader.App/Shell/AvaloniaUiHost.cs b/src/IBKRTrader.App/Shell/AvaloniaUiHost.cs index ccea614..4b08f42 100644 --- a/src/IBKRTrader.App/Shell/AvaloniaUiHost.cs +++ b/src/IBKRTrader.App/Shell/AvaloniaUiHost.cs @@ -120,6 +120,21 @@ public sealed class AvaloniaUiHost : IModuleUiHost } } + /// + /// Übergeht die Sicherheitsabfrage – für ein Herunterfahren, das nicht vom Bediener kommt. + /// + /// Ein Signal ist keine Frage. 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 systemctl stop, 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). + /// + public void SkipShutdownConfirmation() + { + ShutdownConfirmed = true; + CloseAllViews(); + } + /// Schließt alle offenen View-Fenster (beim Herunterfahren). public void CloseAllViews() { diff --git a/src/IBKRTrader.Core/DependencyInjection/ServiceCollectionExtensions.cs b/src/IBKRTrader.Core/DependencyInjection/ServiceCollectionExtensions.cs index a1c4503..cb475dc 100644 --- a/src/IBKRTrader.Core/DependencyInjection/ServiceCollectionExtensions.cs +++ b/src/IBKRTrader.Core/DependencyInjection/ServiceCollectionExtensions.cs @@ -20,12 +20,23 @@ public static class ServiceCollectionExtensions } /// - /// Liefert den Connection-String oder – bei leerem Wert (z. B. fehlende appsettings.Local.json) – - /// einen unschädlichen Platzhalter, damit der DbContext-Optionsbau nicht wirft. Die App startet so - /// unabhängig von der DB; echte Queries schlagen erst bei tatsächlichem Zugriff fehl. + /// Liefert den Connection-String oder – bei leerem Wert – einen Platzhalter, damit der + /// DbContext-Optionsbau nicht wirft. Die App startet so unabhängig von der Datenbank; echte + /// Abfragen scheitern erst beim tatsächlichen Zugriff. + /// + /// Der Platzhalter meldet sich bewusst nicht mehr als root an. 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. + /// + /// Wer die maßgebliche Verbindung braucht, nimmt ; + /// diese Methode ist nur der Notnagel für den Optionsbau. /// public static string EffectiveConnectionString(string? 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; } diff --git a/src/IBKRTrader.Modules.Accounting/AccountingModule.cs b/src/IBKRTrader.Modules.Accounting/AccountingModule.cs index 40e1b91..1e74ba8 100644 --- a/src/IBKRTrader.Modules.Accounting/AccountingModule.cs +++ b/src/IBKRTrader.Modules.Accounting/AccountingModule.cs @@ -27,8 +27,14 @@ public sealed class AccountingModule : IModule public void RegisterServices(IServiceCollection services, IConfiguration configuration) { - var conn = ServiceCollectionExtensions.EffectiveConnectionString(configuration["Database:MySqlConnectionString"]); - services.AddDbContextFactory(o => o.UseMySql(conn, DatabaseServerVersion.Value)); + // Modul-Persistenz: eigener DbContext in DERSELBEN Datenbank wie der Core. Die Verbindung + // 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((sp, o) => + o.UseMySql(sp.GetRequiredService().ConnectionString, + DatabaseServerVersion.Value)); services.AddSingleton(); services.AddSingleton(); diff --git a/src/IBKRTrader.Modules.CongressTrading/CongressTradingModule.cs b/src/IBKRTrader.Modules.CongressTrading/CongressTradingModule.cs index d20a9eb..3c89c3b 100644 --- a/src/IBKRTrader.Modules.CongressTrading/CongressTradingModule.cs +++ b/src/IBKRTrader.Modules.CongressTrading/CongressTradingModule.cs @@ -39,9 +39,14 @@ public sealed class CongressTradingModule : IModule private void Register(IServiceCollection services, IConfiguration configuration, ModuleExecutionFactory? execution) { - // Modul-Persistenz: eigener DbContext (ct_-Tabellen) in derselben MariaDB. - var conn = ServiceCollectionExtensions.EffectiveConnectionString(configuration["Database:MySqlConnectionString"]); - services.AddDbContextFactory(o => o.UseMySql(conn, DatabaseServerVersion.Value)); + // Modul-Persistenz: eigener DbContext in DERSELBEN Datenbank wie der Core. Die Verbindung + // 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((sp, o) => + o.UseMySql(sp.GetRequiredService().ConnectionString, + DatabaseServerVersion.Value)); services.AddSingleton(); services.AddSingleton(); diff --git a/src/IBKRTrader.Modules.OptionsWheel/OptionsWheelModule.cs b/src/IBKRTrader.Modules.OptionsWheel/OptionsWheelModule.cs index 078fcbb..e85f71e 100644 --- a/src/IBKRTrader.Modules.OptionsWheel/OptionsWheelModule.cs +++ b/src/IBKRTrader.Modules.OptionsWheel/OptionsWheelModule.cs @@ -35,9 +35,14 @@ public sealed class OptionsWheelModule : IModule public void RegisterServices(IServiceCollection services, IConfiguration configuration) { // Modul-Persistenz: eigener DbContext (ow_-Tabellen) in derselben MariaDB. - var conn = ServiceCollectionExtensions.EffectiveConnectionString( - configuration["Database:MySqlConnectionString"]); - services.AddDbContextFactory(o => o.UseMySql(conn, DatabaseServerVersion.Value)); + // Modul-Persistenz: eigener DbContext in DERSELBEN Datenbank wie der Core. Die Verbindung + // 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((sp, o) => + o.UseMySql(sp.GetRequiredService().ConnectionString, + DatabaseServerVersion.Value)); // Reine Strategie-Logik. Zustandslos und ohne Aussenwelt – deshalb als Singleton // unbedenklich und in den Tests direkt konstruierbar. diff --git a/src/IBKRTrader.Modules.Supervisor/SupervisorModule.cs b/src/IBKRTrader.Modules.Supervisor/SupervisorModule.cs index ebc6a60..4e90c11 100644 --- a/src/IBKRTrader.Modules.Supervisor/SupervisorModule.cs +++ b/src/IBKRTrader.Modules.Supervisor/SupervisorModule.cs @@ -28,8 +28,14 @@ public sealed class SupervisorModule : IModule public void RegisterServices(IServiceCollection services, IConfiguration configuration) { // sup_-Persistenz (gespeicherte Analysen/Berichte + Counterfactuals). - var conn = ServiceCollectionExtensions.EffectiveConnectionString(configuration["Database:MySqlConnectionString"]); - services.AddDbContextFactory(o => o.UseMySql(conn, DatabaseServerVersion.Value)); + // Modul-Persistenz: eigener DbContext in DERSELBEN Datenbank wie der Core. Die Verbindung + // 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((sp, o) => + o.UseMySql(sp.GetRequiredService().ConnectionString, + DatabaseServerVersion.Value)); services.AddSingleton(); services.AddSingleton();