From 957b6532103abb3ce750a0d5a8aadcb25683ebe0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ji=C5=99=C3=AD=20Pol=C3=A1=C5=A1ek?= Date: Fri, 31 Oct 2025 18:35:37 +0100 Subject: [PATCH] CmdPal: Add a micro global error handler (#41392) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## Summary of the Pull Request This PR introduces a scaled-down version of the global error handler from #41061. - Catches and logs virtually all unhandled exceptions. - For UI thread exceptions, generates an additional error report: - One copy is saved in app's log folder for the Bug Report Tool. - Another copy can be placed to the user’s desktop to increase visibility and encourage report submission (disabled for now). - Displays a message box that tells the user where to find the saved report. This PR is intentionally minimal and focused. The complete, more polished solution is still planned in #41061, which should replace this implementation in a follow-up.
Report example

This is an error report generated by Windows Command Palette.
If you are seeing this message, it means the application has encountered
an unexpected issue.
You can help us fix it by filing a report at
https://aka.ms/powerToysReportBug.
============================================================
😢 An unexpected error occurred in the application.

Summary:
  Message:    NamedResource Not Found.

NamedResource Not Found.

  Type:       System.Runtime.InteropServices.COMException
  Source:     WinRT.Runtime
  Time:       2025-08-26 20:22:53.5752505
  HRESULT:    0x80073B17 (-2147009769)

Stack Trace:
at WinRT.ExceptionHelpers.g__Throw|38_0(Int32 hr)
at
ABI.Microsoft.Windows.ApplicationModel.Resources.IResourceLoaderMethods.GetString(IObjectReference
_obj, String resourceId)
at
Microsoft.Windows.ApplicationModel.Resources.ResourceLoader.GetString(String
resourceId)
at Microsoft.CmdPal.UI.Helpers.ResourceLoaderInstance.GetString(String
resourceId)
at
Microsoft.CmdPal.UI.Settings.SettingsWindow.AnnounceNavigationPaneStateChanged(DependencyObject
sender, DependencyProperty dp)
at
ABI.Microsoft.UI.Xaml.DependencyPropertyChangedCallback.Do_Abi_Invoke(IntPtr
thisPtr, IntPtr sender, IntPtr dp)

------------------ Full Exception Details ------------------
System.Runtime.InteropServices.COMException (0x80073B17): NamedResource
Not Found.

NamedResource Not Found.

at WinRT.ExceptionHelpers.g__Throw|38_0(Int32 hr)
at
ABI.Microsoft.Windows.ApplicationModel.Resources.IResourceLoaderMethods.GetString(IObjectReference
_obj, String resourceId)
at
Microsoft.Windows.ApplicationModel.Resources.ResourceLoader.GetString(String
resourceId)
at Microsoft.CmdPal.UI.Helpers.ResourceLoaderInstance.GetString(String
resourceId)
at
Microsoft.CmdPal.UI.Settings.SettingsWindow.AnnounceNavigationPaneStateChanged(DependencyObject
sender, DependencyProperty dp)
at
ABI.Microsoft.UI.Xaml.DependencyPropertyChangedCallback.Do_Abi_Invoke(IntPtr
thisPtr, IntPtr sender, IntPtr dp)

ℹ️ If you need further assistance, please include this information in
your support request.
ℹ️ Before sending, take a quick look to make sure it doesn't contain any
personal or sensitive information.
============================================================


Message: image ## PR Checklist - [x] Partially handles: #41606 - [ ] **Communication:** I've discussed this with core contributors already. If the work hasn't been agreed, this work might be rejected - [ ] **Tests:** Added/updated and all pass - [ ] **Localization:** All end-user-facing strings can be localized - [ ] **Dev docs:** Added/updated - [ ] **New binaries:** Added on the required places - [ ] [JSON for signing](https://github.com/microsoft/PowerToys/blob/main/.pipelines/ESRPSigning_core.json) for new binaries - [ ] [WXS for installer](https://github.com/microsoft/PowerToys/blob/main/installer/PowerToysSetup/Product.wxs) for new binaries and localization folder - [ ] [YML for CI pipeline](https://github.com/microsoft/PowerToys/blob/main/.pipelines/ci/templates/build-powertoys-steps.yml) for new test projects - [ ] [YML for signed pipeline](https://github.com/microsoft/PowerToys/blob/main/.pipelines/release.yml) - [ ] **Documentation updated:** If checked, please file a pull request on [our docs repo](https://github.com/MicrosoftDocs/windows-uwp/tree/docs/hub/powertoys) and link it here: #xxx ## Detailed Description of the Pull Request / Additional comments ## Validation Steps Performed --- src/common/ManagedCommon/Logger.cs | 13 ++ .../cmdpal/Microsoft.CmdPal.UI/App.xaml.cs | 6 + .../Helpers/GlobalErrorHandler.cs | 134 ++++++++++++++++++ 3 files changed, 153 insertions(+) create mode 100644 src/modules/cmdpal/Microsoft.CmdPal.UI/Helpers/GlobalErrorHandler.cs diff --git a/src/common/ManagedCommon/Logger.cs b/src/common/ManagedCommon/Logger.cs index 0db1614671..1173920340 100644 --- a/src/common/ManagedCommon/Logger.cs +++ b/src/common/ManagedCommon/Logger.cs @@ -26,6 +26,16 @@ namespace ManagedCommon private static readonly string Version = Assembly.GetExecutingAssembly().GetCustomAttribute()?.Version ?? "Unknown"; + /// + /// Gets the path to the log directory for the current version of the app. + /// + public static string CurrentVersionLogDirectoryPath { get; private set; } + + /// + /// Gets the path to the log directory for the app. + /// + public static string AppLogDirectoryPath { get; private set; } + /// /// Initializes the logger and sets the path for logging. /// @@ -42,6 +52,9 @@ namespace ManagedCommon Directory.CreateDirectory(versionedPath); } + AppLogDirectoryPath = basePath; + CurrentVersionLogDirectoryPath = versionedPath; + var logFilePath = Path.Combine(versionedPath, "Log_" + DateTime.Now.ToString(@"yyyy-MM-dd", CultureInfo.InvariantCulture) + ".log"); Trace.Listeners.Add(new TextWriterTraceListener(logFilePath)); diff --git a/src/modules/cmdpal/Microsoft.CmdPal.UI/App.xaml.cs b/src/modules/cmdpal/Microsoft.CmdPal.UI/App.xaml.cs index 9ba41a08fb..917716be19 100644 --- a/src/modules/cmdpal/Microsoft.CmdPal.UI/App.xaml.cs +++ b/src/modules/cmdpal/Microsoft.CmdPal.UI/App.xaml.cs @@ -40,6 +40,8 @@ namespace Microsoft.CmdPal.UI; /// public partial class App : Application { + private readonly GlobalErrorHandler _globalErrorHandler = new(); + /// /// Gets the current instance in use. /// @@ -61,6 +63,10 @@ public partial class App : Application /// public App() { +#if !CMDPAL_DISABLE_GLOBAL_ERROR_HANDLER + _globalErrorHandler.Register(this); +#endif + Services = ConfigureServices(); this.InitializeComponent(); diff --git a/src/modules/cmdpal/Microsoft.CmdPal.UI/Helpers/GlobalErrorHandler.cs b/src/modules/cmdpal/Microsoft.CmdPal.UI/Helpers/GlobalErrorHandler.cs new file mode 100644 index 0000000000..0b55b03615 --- /dev/null +++ b/src/modules/cmdpal/Microsoft.CmdPal.UI/Helpers/GlobalErrorHandler.cs @@ -0,0 +1,134 @@ +// 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.Helpers; +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 +{ + // GlobalErrorHandler is designed to be self-contained; it can be registered and invoked before a service provider is available. + internal void Register(App app) + { + ArgumentNullException.ThrowIfNull(app); + + 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, isRecoverable: true); + } + + private void HandleException(Exception ex, Context context, bool isRecoverable = false) + { + Logger.LogError($"Unhandled exception detected ({context})", ex); + + if (context == Context.MainThreadException) + { + var error = DiagnosticsHelper.BuildExceptionMessage(ex, null); + var report = $""" + This is an error report generated by Windows Command Palette. + If you are seeing this message, it means the application has encountered an unexpected issue. + You can help us fix it by filing a report at https://aka.ms/powerToysReportBug. + {error} + """; + + StoreReport(report, storeOnDesktop: false); + + PInvoke.MessageBox( + HWND.Null, + "Command Palette has encountered a fatal error and must close.\n\nAn error report has been saved to your desktop.", + "Unhandled Error", + 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; + } + } + } + + private enum Context + { + Unknown = 0, + MainThreadException, + BackgroundThreadException, + UnobservedTaskException, + AppDomainUnhandledException, + } +}