Run: handle DWM issues more gracefully (#42588)

## Summary of the Pull Request

This PR expands the scenarios where Run handles exceptions silently,
reducing the number of exception dialogs displayed to users and creating
a smoother experience. The premise remains unchanged: most exceptions
occur when Run is in the background, and the app can recover as the DWM
recovers.

This should cover most of the reports commonly encountered in the wild.

- Extended silent handling of DWM exceptions to include
`DWM_E_COMPOSITIONDISABLED` (0x80263001).
- Added silent handling for `STATUS_MESSAGE_LOST` error (NTSTATUS
0xC0000701, HR 0xD0000701).
- Implemented exception handling and retry logic for failures during
manual app theme changes (triggered by
`PowerLauncher.Helper.ThemeManager`).

I can't reproduce the error on its own; it only occurs alongside a fatal
render thread failure (`UCEERR_RENDERTHREADFAILURE`), which is
unrecoverable. Even if caught, the UI stops rendering, leaving the
end-user unable to proceed.

<!-- Please review the items on the PR checklist before submitting-->
## PR Checklist

- [x] Related to: #31226
- [x] Related to: #30769
- [ ] **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-11-05 18:51:58 +01:00
committed by GitHub
parent b7b6ae6485
commit 47c779e0a0
4 changed files with 130 additions and 22 deletions

View File

@@ -258,6 +258,7 @@ cominterop
commandnotfound
commandpalette
compmgmt
COMPOSITIONDISABLED
COMPOSITIONFULL
CONFIGW
CONFLICTINGMODIFIERKEY

View File

@@ -61,7 +61,7 @@ namespace PowerLauncher.Helper
// Many bug reports because users see the "Report problem UI" after "the" crash with System.Runtime.InteropServices.COMException 0xD0000701 or 0x80263001.
// However, displaying this "Report problem UI" during WPF crashes, especially when DWM composition is changing, is not ideal; some users reported it hangs for up to a minute before the "Report problem UI" appears.
// This change modifies the behavior to log the exception instead of showing the "Report problem UI".
if (IsDwmCompositionException(e as System.Runtime.InteropServices.COMException))
if (ExceptionHelper.IsRecoverableDwmCompositionException(e as System.Runtime.InteropServices.COMException))
{
var logger = LogManager.GetLogger(LoggerName);
logger.Error($"From {(isNotUIThread ? "non" : string.Empty)} UI thread's exception: {ExceptionFormatter.FormatException(e)}");
@@ -91,22 +91,5 @@ namespace PowerLauncher.Helper
}
}
}
private static bool IsDwmCompositionException(System.Runtime.InteropServices.COMException comException)
{
if (comException == null)
{
return false;
}
var stackTrace = comException.StackTrace;
if (string.IsNullOrEmpty(stackTrace))
{
return false;
}
// Check for common DWM composition changed patterns in the stack trace
return stackTrace.Contains("DwmCompositionChanged");
}
}
}

View File

@@ -0,0 +1,46 @@
// 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 System;
using System.Runtime.InteropServices;
namespace PowerLauncher.Helper
{
[System.Diagnostics.CodeAnalysis.SuppressMessage("StyleCop.CSharp.NamingRules", "SA1310:Field names should not contain underscore", Justification = "Win32 naming conventions")]
internal static class ExceptionHelper
{
private const string PresentationFrameworkExceptionSource = "PresentationFramework";
private const int DWM_E_COMPOSITIONDISABLED = unchecked((int)0x80263001);
// HRESULT for NT STATUS STATUS_MESSAGE_LOST (0xC0000701 | 0x10000000 == 0xD0000701)
private const int STATUS_MESSAGE_LOST_HR = unchecked((int)0xD0000701);
/// <summary>
/// Returns true if the exception is a recoverable DWM composition exception.
/// </summary>
internal static bool IsRecoverableDwmCompositionException(Exception exception)
{
if (exception is not COMException comException)
{
return false;
}
if (comException.HResult is DWM_E_COMPOSITIONDISABLED)
{
return true;
}
if (comException.HResult is STATUS_MESSAGE_LOST_HR && comException.Source == PresentationFrameworkExceptionSource)
{
return true;
}
// Check for common DWM composition changed patterns in the stack trace
var stackTrace = comException.StackTrace;
return !string.IsNullOrEmpty(stackTrace) &&
stackTrace.Contains("DwmCompositionChanged");
}
}
}

View File

@@ -3,13 +3,16 @@
// See the LICENSE file in the project root for more information.
using System;
using System.Collections.Generic;
using System.Runtime.InteropServices;
using System.Threading;
using System.Threading.Tasks;
using System.Windows;
using System.Windows.Media;
using ManagedCommon;
using Microsoft.Win32;
using Wox.Infrastructure.Image;
using Wox.Infrastructure.UserSettings;
using Wox.Plugin.Logger;
namespace PowerLauncher.Helper
{
@@ -20,6 +23,9 @@ namespace PowerLauncher.Helper
private readonly ThemeHelper _themeHelper = new();
private bool _disposed;
private CancellationTokenSource _themeUpdateTokenSource;
private const int MaxRetries = 5;
private const int InitialDelayMs = 2000;
public Theme CurrentTheme { get; private set; }
@@ -108,10 +114,80 @@ namespace PowerLauncher.Helper
{
Theme newTheme = _themeHelper.DetermineTheme(_settings.Theme);
_mainWindow.Dispatcher.Invoke(() =>
// Cancel any existing theme update operation
_themeUpdateTokenSource?.Cancel();
_themeUpdateTokenSource?.Dispose();
_themeUpdateTokenSource = new CancellationTokenSource();
// Start theme update with retry logic in the background
_ = UpdateThemeWithRetryAsync(newTheme, _themeUpdateTokenSource.Token);
}
/// <summary>
/// Applies the theme with retry logic for desktop composition errors.
/// </summary>
/// <param name="theme">The theme to apply.</param>
/// <param name="cancellationToken">Token to cancel the operation.</param>
private async Task UpdateThemeWithRetryAsync(Theme theme, CancellationToken cancellationToken)
{
var delayMs = 0;
const int maxAttempts = MaxRetries + 1;
for (var attempt = 1; attempt <= maxAttempts; attempt++)
{
SetSystemTheme(newTheme);
});
try
{
if (delayMs > 0)
{
await Task.Delay(delayMs, cancellationToken);
}
if (cancellationToken.IsCancellationRequested)
{
Log.Debug("Theme update operation was cancelled.", typeof(ThemeManager));
return;
}
await _mainWindow.Dispatcher.InvokeAsync(() =>
{
SetSystemTheme(theme);
});
if (attempt > 1)
{
Log.Info($"Successfully applied theme after {attempt - 1} retry attempt(s).", typeof(ThemeManager));
}
return;
}
catch (COMException ex) when (ExceptionHelper.IsRecoverableDwmCompositionException(ex))
{
switch (attempt)
{
case 1:
Log.Warn($"Desktop composition is disabled (HRESULT: 0x{ex.HResult:X}). Scheduling retries for theme update.", typeof(ThemeManager));
delayMs = InitialDelayMs;
break;
case < maxAttempts:
Log.Warn($"Retry {attempt - 1}/{MaxRetries} failed: Desktop composition still disabled. Retrying in {delayMs * 2}ms...", typeof(ThemeManager));
delayMs *= 2;
break;
default:
Log.Exception($"Failed to set theme after {MaxRetries} retry attempts. Desktop composition remains disabled.", ex, typeof(ThemeManager));
break;
}
}
catch (OperationCanceledException)
{
Log.Debug("Theme update operation was cancelled.", typeof(ThemeManager));
return;
}
catch (Exception ex)
{
Log.Exception($"Unexpected error during theme update (attempt {attempt}/{maxAttempts}): {ex.Message}", ex, typeof(ThemeManager));
throw;
}
}
}
public void Dispose()
@@ -130,6 +206,8 @@ namespace PowerLauncher.Helper
if (disposing)
{
SystemEvents.UserPreferenceChanged -= OnUserPreferenceChanged;
_themeUpdateTokenSource?.Cancel();
_themeUpdateTokenSource?.Dispose();
}
_disposed = true;