using System.Diagnostics; using System.IO; using System.Threading; using System.Windows; using InvoiceParser.Src; namespace InvoiceParser; public partial class App : Application { private WebHost? _host; private Mutex? _singleInstance; protected override async void OnStartup(StartupEventArgs e) { base.OnStartup(e); // Nothing else catches these, and a silent crash on an unattended // machine is exactly what the log exists to explain. AppDomain.CurrentDomain.UnhandledException += (_, args) => Log.Error("crash", "unhandled exception", args.ExceptionObject as Exception); DispatcherUnhandledException += (_, args) => Log.Error("crash", "unhandled UI exception", args.Exception); TaskScheduler.UnobservedTaskException += (_, args) => { Log.Error("crash", "unobserved task exception", args.Exception); args.SetObserved(); }; _singleInstance = new Mutex(true, "InvoiceParserApp_SingleInstance", out bool isNew); if (!isNew) { // Bring the running instance forward instead of just saying it exists. FocusRunningInstance(); Shutdown(); return; } try { var config = AppConfig.Load(AppContext.BaseDirectory); _host = new WebHost(config); await _host.StartAsync(); var win = new MainWindow(_host.BaseUrl); MainWindow = win; win.Show(); } catch (Exception ex) { Log.Error("startup", "startup failed", ex); MessageBox.Show("Startup failed: " + ex.Message, "Invoice Parser", MessageBoxButton.OK, MessageBoxImage.Error); Shutdown(); } } [System.Runtime.InteropServices.DllImport("user32.dll")] private static extern bool SetForegroundWindow(IntPtr hWnd); [System.Runtime.InteropServices.DllImport("user32.dll")] private static extern bool ShowWindow(IntPtr hWnd, int nCmdShow); private static void FocusRunningInstance() { const int SW_RESTORE = 9; var me = Process.GetCurrentProcess(); foreach (var p in Process.GetProcessesByName(me.ProcessName)) { if (p.Id == me.Id || p.MainWindowHandle == IntPtr.Zero) continue; ShowWindow(p.MainWindowHandle, SW_RESTORE); SetForegroundWindow(p.MainWindowHandle); return; } MessageBox.Show("Invoice Parser is already running.", "Invoice Parser", MessageBoxButton.OK, MessageBoxImage.Information); } protected override void OnExit(ExitEventArgs e) { // Stop the host off the UI thread: awaiting it here can deadlock on the // WPF SynchronizationContext, leaving a windowless process holding the // single-instance mutex. try { var host = _host; if (host != null) Task.Run(() => host.StopAsync()).Wait(TimeSpan.FromSeconds(5)); } catch (Exception ex) { Log.Error("shutdown", "web host stop failed", ex); } Log.Info("shutdown", "exiting with code " + e.ApplicationExitCode); _singleInstance?.Dispose(); base.OnExit(e); // Hard terminate. Environment.Exit runs finalizers and COM cleanup, which // hangs on WebView2's browser processes and leaves a windowless process // that holds the single-instance mutex and locks the exe against rebuilds. // Safe here: every state.json write is synchronous and already flushed, // and a parse abandoned mid-call is reset from "processing" to "queued" // by ParseWorker on the next start. Process.GetCurrentProcess().Kill(); } }