using System.IO; using System.Windows; using System.Windows.Threading; using Microsoft.Extensions.Logging; namespace TeamsISO.App; // Crash diagnostics — the three exception channels WPF leaves open by // default, wired to a single handler that logs Fatal to Serilog (rolling // daily file at %LOCALAPPDATA%\TeamsISO\Logs) and then shows the user a // dialog with the log path so they can attach it to a bug report. // // We deliberately don't catch StackOverflowException or // ExecutionEngineException — both are uncatchable in modern .NET; if one // fires the OS Watson dialog takes it from here. public partial class App { /// /// Where the rolling Serilog file sink writes. Reused by the crash /// dialog so we can show the user the exact directory to attach when /// filing a bug. /// private static string LogDirectory => Path.Combine( Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData), "TeamsISO", "Logs"); private void OnAppDomainUnhandled(object sender, UnhandledExceptionEventArgs e) { // IsTerminating is almost always true here — finalizers and // managed-thread top-frames don't have a graceful path back. Log // + show a dialog inline since the process will exit either way. var ex = e.ExceptionObject as Exception; TryLogFatal("AppDomain.UnhandledException", ex); TryShowCrashDialog(ex, terminating: e.IsTerminating); } private void OnDispatcherUnhandled(object sender, DispatcherUnhandledExceptionEventArgs e) { TryLogFatal("Dispatcher.UnhandledException", e.Exception); TryShowCrashDialog(e.Exception, terminating: false); // Mark Handled so a single bad UI thunk doesn't take the whole app // down — the user has the dialog and the log; they can choose to // keep going. e.Handled = true; } private void OnUnobservedTaskException(object? sender, System.Threading.Tasks.UnobservedTaskExceptionEventArgs e) { TryLogFatal("TaskScheduler.UnobservedTaskException", e.Exception); // Don't show a dialog here — these fire from the finalizer thread // and tend to be cleanup-time noise, not user-actionable. Log only. e.SetObserved(); } private void TryLogFatal(string source, Exception? ex) { try { var logger = _loggerFactory?.CreateLogger(); logger?.LogCritical(ex, "{Source} fired", source); } catch { // Logger itself failed (rare — disk full, permission denied). // Swallow: nothing useful to do, and re-throwing during crash // handling makes things worse. } } private static void TryShowCrashDialog(Exception? ex, bool terminating) { try { var heading = terminating ? "TeamsISO encountered an unrecoverable error and will exit." : "TeamsISO encountered an error."; var details = ex?.GetType().Name + ": " + (ex?.Message ?? "(no details)"); var body = heading + "\n\n" + details + "\n\n" + $"A full diagnostic log has been written to:\n{LogDirectory}\n\n" + "Attach the most recent file from that directory to your bug report."; MessageBox.Show(body, "TeamsISO — Error", MessageBoxButton.OK, MessageBoxImage.Error); } catch { // Even the dialog failed (e.g., during shutdown when the // message pump is already gone). Nothing more to do. } } }