// Copyright (c) Microsoft Corporation // The Microsoft Corporation licenses this file to you under the MIT license. // See the LICENSE file in the project root for more information. using ManagedCommon; using Microsoft.CmdPal.Core.Common.Services; using Microsoft.CmdPal.Core.Common.Services.Reports; using Windows.Win32; using Windows.Win32.Foundation; using Windows.Win32.UI.WindowsAndMessaging; using SystemUnhandledExceptionEventArgs = System.UnhandledExceptionEventArgs; using XamlUnhandledExceptionEventArgs = Microsoft.UI.Xaml.UnhandledExceptionEventArgs; namespace Microsoft.CmdPal.UI.Helpers; /// /// Global error handler for Command Palette. /// internal sealed partial class GlobalErrorHandler : IDisposable { private ErrorReportBuilder? _errorReportBuilder; private Options? _options; private App? _app; // GlobalErrorHandler is designed to be self-contained; it can be registered and invoked before a service provider is available. internal void Register(App app, Options options, IApplicationInfoService? appInfoService = null) { ArgumentNullException.ThrowIfNull(app); ArgumentNullException.ThrowIfNull(options); _options = options; _app = app; _errorReportBuilder = new ErrorReportBuilder(appInfoService); _app.UnhandledException += App_UnhandledException; TaskScheduler.UnobservedTaskException += TaskScheduler_UnobservedTaskException; AppDomain.CurrentDomain.UnhandledException += CurrentDomain_UnhandledException; } private void App_UnhandledException(object sender, XamlUnhandledExceptionEventArgs e) { // Exceptions thrown on the main UI thread are handled here. if (e.Exception != null) { HandleException(e.Exception, Context.MainThreadException); } } private void CurrentDomain_UnhandledException(object sender, SystemUnhandledExceptionEventArgs e) { // Exceptions thrown on background threads are handled here. if (e.ExceptionObject is Exception ex) { HandleException(ex, Context.AppDomainUnhandledException); } } private void TaskScheduler_UnobservedTaskException(object? sender, UnobservedTaskExceptionEventArgs e) { // This event is raised only when a faulted Task is garbage-collected // without its exception being observed. It is NOT raised immediately // when the Task faults; timing depends on GC finalization. e.SetObserved(); HandleException(e.Exception, Context.UnobservedTaskException); } private void HandleException(Exception ex, Context context) { Logger.LogError($"Unhandled exception detected ({context})", ex); if (context == Context.MainThreadException) { var report = _errorReportBuilder!.BuildReport(ex, context.ToString(), _options?.RedactPii ?? true); StoreReport(report, storeOnDesktop: _options?.StoreReportOnUserDesktop == true); string message; string caption; try { message = ResourceLoaderInstance.GetString("GlobalErrorHandler_CrashMessageBox_Message"); caption = ResourceLoaderInstance.GetString("GlobalErrorHandler_CrashMessageBox_Caption"); } catch { // The resource loader may not be available if the exception occurred during startup. // Fall back to hardcoded strings in that case. message = "Command Palette has encountered a fatal error and must close."; caption = "Command Palette - Fatal error"; } PInvoke.MessageBox( HWND.Null, message, caption, MESSAGEBOX_STYLE.MB_ICONERROR); } } private static string? StoreReport(string report, bool storeOnDesktop) { // Generate a unique name for the report file; include timestamp and a random zero-padded number to avoid collisions // in case of crash storm. var name = FormattableString.Invariant($"CmdPal_ErrorReport_{DateTime.Now:yyyy-MM-dd_HH-mm-ss}_{Random.Shared.Next(100000):D5}.log"); // Always store a copy in log directory, this way it is available for Bug Report Tool string? reportPath = null; if (Logger.CurrentVersionLogDirectoryPath != null) { reportPath = Save(report, name, static () => Logger.CurrentVersionLogDirectoryPath); } // Optionally store a copy on the desktop for user (in)convenience if (storeOnDesktop) { var path = Save(report, name, static () => Environment.GetFolderPath(Environment.SpecialFolder.DesktopDirectory)); // show the desktop copy if both succeeded if (path != null) { reportPath = path; } } return reportPath; static string? Save(string reportContent, string reportFileName, Func directory) { try { var logDirectory = directory(); Directory.CreateDirectory(logDirectory); var reportFilePath = Path.Combine(logDirectory, reportFileName); File.WriteAllText(reportFilePath, reportContent); return reportFilePath; } catch (Exception ex) { Logger.LogError("Failed to store exception report", ex); return null; } } } public void Dispose() { _app?.UnhandledException -= App_UnhandledException; TaskScheduler.UnobservedTaskException -= TaskScheduler_UnobservedTaskException; AppDomain.CurrentDomain.UnhandledException -= CurrentDomain_UnhandledException; } private enum Context { Unknown = 0, MainThreadException, BackgroundThreadException, UnobservedTaskException, AppDomainUnhandledException, } /// /// Configuration options controlling how reacts to exceptions /// (what to log, what to show to the user, and where to store reports). /// internal sealed record Options { /// /// Gets the default configuration. /// public static Options Default { get; } = new(); /// /// Gets a value indicating whether Personally Identifiable Information (PII) should be redacted in error reports. /// public bool RedactPii { get; init; } = true; /// /// Gets a value indicating whether to store the error report on the user's desktop in addition to the log directory. /// public bool StoreReportOnUserDesktop { get; init; } } }