103 lines
3.7 KiB
C#
103 lines
3.7 KiB
C#
|
|
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();
|
||
|
|
}
|
||
|
|
}
|