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));
}