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; /// /// Einstiegspunkt der plattformneutralen Oberfläche. /// /// Bewusst zweigeteilt: 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 /// (IBKRTrader.Daemon). /// 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(); 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(); 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(); } } /// /// 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. /// 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; } } /// Von Avalonia erwartete Fabrikmethode (auch vom XAML-Previewer genutzt). public static AppBuilder BuildAvaloniaApp() => AppBuilder.Configure() .UsePlatformDetect() .WithInterFont() .LogToTrace(); private static void StartModules(IReadOnlyList 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 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)); }