Files
IBKRTrader/src/IBKRTrader.App/Program.cs
T
RichardandClaude Opus 5 b618efa9ce 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>
2026-09-05 13:23:16 +02:00

153 lines
6.8 KiB
C#
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
using System.Runtime.InteropServices;
using Avalonia;
using Avalonia.Controls.ApplicationLifetimes;
using Avalonia.Threading;
using IBKRTrader.App.Shell;
using IBKRTrader.Core.Logging;
using IBKRTrader.Core.Modularity;
using IBKRTrader.Hosting;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
namespace IBKRTrader.App;
/// <summary>
/// Einstiegspunkt der plattformneutralen Oberfläche.
///
/// <para>Bewusst zweigeteilt: <see cref="AppHostBuilder"/> stellt den Host mit Persistenz,
/// Diensten und Modulen zusammen – ohne jeden Bezug zur Oberfläche. Erst danach wird Avalonia
/// daran gehängt. Derselbe Host trägt den kopflosen Linux-Dienst
/// (<c>IBKRTrader.Daemon</c>).</para>
/// </summary>
internal static class Program
{
[STAThread]
public static int Main(string[] args)
{
// Konstruktionsprüfung aller Fenster ohne Message-Loop und ohne laufende Dienste.
if (HasFlag(args, "--smoke-ui")) return SmokeUi.Run();
var modules = AppHostBuilder.CreateModules();
using var host = AppHostBuilder.Build(modules, ShellServices.Register);
AppHostBuilder.RunStartupChecks(host.Services);
var logger = host.Services.GetRequiredService<LoggingService>();
logger.Info("Core", "=== IBKRTrader startet ===");
logger.Info("Core", $"Version: {IBKRTrader.Hosting.BuildInfo.Summary} | .NET {Environment.Version}");
host.Start();
StartModules(modules, logger);
try
{
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.
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().SetupWithLifetime(lifetime);
return lifetime.Start(args);
}
finally
{
// Geordnetes Herunterfahren – erst nachdem die Oberfläche beendet ist.
StopModules(modules, logger);
host.StopAsync(TimeSpan.FromSeconds(30)).GetAwaiter().GetResult();
}
}
/// <summary>
/// Beendet die Oberfläche auf ein Signal hin regulär, damit der finally-Zweig mit Modul- und
/// 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, IClassicDesktopStyleApplicationLifetime lifetime,
Shell.AvaloniaUiHost uiHost)
{
try
{
return PosixSignalRegistration.Create(signal, ctx =>
{
// 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(() =>
{
// 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)
{
logger.Warn("Core", $"{signal} konnte nicht behandelt werden: {ex.Message}");
return null;
}
}
/// <summary>Von Avalonia erwartete Fabrikmethode (auch vom XAML-Previewer genutzt).</summary>
public static AppBuilder BuildAvaloniaApp() =>
AppBuilder.Configure<App>()
.UsePlatformDetect()
.WithInterFont()
.LogToTrace();
private static void StartModules(IReadOnlyList<IModule> modules, LoggingService logger)
{
foreach (var module in modules)
{
try { module.StartAsync(default).GetAwaiter().GetResult(); }
catch (Exception ex) { logger.Error(module.Name, $"{module.Name}: Start fehlgeschlagen.", ex); }
}
logger.Info("Core", "IBKRTrader bereit.");
}
private static void StopModules(IReadOnlyList<IModule> modules, LoggingService logger)
{
foreach (var module in modules)
{
try { module.StopAsync(default).GetAwaiter().GetResult(); }
catch (Exception ex) { logger.Warn(module.Name, $"{module.Name}: Stopp fehlgeschlagen: {ex.Message}"); }
}
}
private static bool HasFlag(string[] args, string flag) =>
args.Any(a => string.Equals(a, flag, StringComparison.OrdinalIgnoreCase));
}