InvoiceParser/App.xaml.cs

180 lines
7 KiB
C#
Raw Normal View History

2026-09-16 20:09:19 +00:00
using System.Diagnostics;
using System.IO;
using System.Threading;
using System.Windows;
2026-09-30 18:50:04 +00:00
using System.Windows.Controls;
using System.Windows.Media;
2026-09-16 20:09:19 +00:00
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);
2026-09-30 18:50:04 +00:00
// 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;
2026-09-16 20:09:19 +00:00
_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();
}
}
2026-09-30 18:50:04 +00:00
// 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());
}
2026-09-16 20:09:19 +00:00
[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();
}
}