CmdPal: Add a micro global error handler (#41392)

## 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.

<details><summary>Report example</summary>
<pre>

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.<ThrowExceptionForHR>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.<ThrowExceptionForHR>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.
============================================================


</pre>
</details> 

Message:
<img width="2309" height="1341" alt="image"
src="https://github.com/user-attachments/assets/3ec20054-402f-421d-bebd-bf848a8eb504"
/>

<!-- Please review the items on the PR checklist before submitting-->
## 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

<!-- Provide a more detailed description of the PR, other things fixed,
or any additional comments/features here -->
## Detailed Description of the Pull Request / Additional comments

<!-- Describe how you validated the behavior. Add automated tests
wherever possible, but list manual validation steps taken as well -->
## Validation Steps Performed
This commit is contained in:
Jiří Polášek
2025-10-31 18:35:37 +01:00
committed by GitHub
parent 0b0ad68b60
commit 957b653210
3 changed files with 153 additions and 0 deletions

View File

@@ -40,6 +40,8 @@ namespace Microsoft.CmdPal.UI;
/// </summary>
public partial class App : Application
{
private readonly GlobalErrorHandler _globalErrorHandler = new();
/// <summary>
/// Gets the current <see cref="App"/> instance in use.
/// </summary>
@@ -61,6 +63,10 @@ public partial class App : Application
/// </summary>
public App()
{
#if !CMDPAL_DISABLE_GLOBAL_ERROR_HANDLER
_globalErrorHandler.Register(this);
#endif
Services = ConfigureServices();
this.InitializeComponent();

View File

@@ -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;
/// <summary>
/// Global error handler for Command Palette.
/// </summary>
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<string> 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,
}
}