using System.Diagnostics; using System.IO; using System.Threading; using System.Windows; using System.Windows.Controls; using System.Windows.Media; 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); // The login dialog would otherwise become MainWindow and end the app when it closes. ShutdownMode = ShutdownMode.OnExplicitShutdown; var emp = PromptLogin(config); if (emp == null) { Log.Info("login", "login cancelled"); Shutdown(); return; } config.ReceiverEmpNo = emp.Value.EmpNo; config.ReceiverFirstName = emp.Value.FirstName; Log.Info("login", "logged in as emp_no " + emp.Value.EmpNo); ShutdownMode = ShutdownMode.OnMainWindowClose; _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(); } } // Returns the employee whose AdminPassword was entered, or null if cancelled. private static (int EmpNo, string FirstName)? PromptLogin(AppConfig cfg) { (int EmpNo, string FirstName)? empNo = null; var pw = new PasswordBox { FontSize = 16, Margin = new Thickness(0, 6, 0, 0) }; var error = new TextBlock { Foreground = Brushes.Firebrick, Margin = new Thickness(0, 8, 0, 0) }; var ok = new Button { Content = "Log in", IsDefault = true, Width = 90, Margin = new Thickness(0, 12, 8, 0) }; var cancel = new Button { Content = "Cancel", IsCancel = true, Width = 90, Margin = new Thickness(0, 12, 0, 0) }; var buttons = new StackPanel { Orientation = Orientation.Horizontal, HorizontalAlignment = HorizontalAlignment.Right }; buttons.Children.Add(ok); buttons.Children.Add(cancel); var panel = new StackPanel { Margin = new Thickness(20), MinWidth = 300 }; panel.Children.Add(new TextBlock { Text = "Admin password" }); panel.Children.Add(pw); panel.Children.Add(error); panel.Children.Add(buttons); var win = new Window { Title = "Invoice Parser - Log in", Content = panel, SizeToContent = SizeToContent.WidthAndHeight, ResizeMode = ResizeMode.NoResize, WindowStartupLocation = WindowStartupLocation.CenterScreen }; win.Loaded += (_, _) => pw.Focus(); ok.Click += (_, _) => { error.Text = ""; try { empNo = LookupEmployee(cfg, pw.Password); if (empNo != null) { win.DialogResult = true; return; } error.Text = "Invalid password"; } catch (Exception ex) { Log.Error("login", "employee lookup failed", ex); error.Text = "Database error: " + ex.Message; } pw.Clear(); pw.Focus(); }; return win.ShowDialog() == true ? empNo : null; } private static (int EmpNo, string FirstName)? LookupEmployee(AppConfig cfg, string password) { if (!int.TryParse(password.Trim(), out var pw)) return null; using var conn = new Db(cfg).Open(); using var cmd = conn.CreateCommand(); cmd.CommandText = "SELECT TOP 1 emp_no, FirstName FROM posbdat.dbo.Employees " + "WHERE AdminPassword = @pw AND ISNULL(EmpActive, 0) = 1"; cmd.Parameters.AddWithValue("@pw", pw); using var r = cmd.ExecuteReader(); if (!r.Read()) return null; return (Convert.ToInt32(r.GetValue(0)), (Db.GetStr(r, "FirstName") ?? "").Trim()); } [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(); } }