From 47c779e0a0319c0010b6a3fd47feeceb36c77a88 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ji=C5=99=C3=AD=20Pol=C3=A1=C5=A1ek?= Date: Wed, 5 Nov 2025 18:51:58 +0100 Subject: [PATCH] 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. ## 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 ## Detailed Description of the Pull Request / Additional comments ## Validation Steps Performed --- .github/actions/spell-check/expect.txt | 1 + .../PowerLauncher/Helper/ErrorReporting.cs | 19 +--- .../PowerLauncher/Helper/ExceptionHelper.cs | 46 ++++++++++ .../PowerLauncher/Helper/ThemeManager.cs | 86 ++++++++++++++++++- 4 files changed, 130 insertions(+), 22 deletions(-) create mode 100644 src/modules/launcher/PowerLauncher/Helper/ExceptionHelper.cs diff --git a/.github/actions/spell-check/expect.txt b/.github/actions/spell-check/expect.txt index 491347d5ca..b3966292e9 100644 --- a/.github/actions/spell-check/expect.txt +++ b/.github/actions/spell-check/expect.txt @@ -258,6 +258,7 @@ cominterop commandnotfound commandpalette compmgmt +COMPOSITIONDISABLED COMPOSITIONFULL CONFIGW CONFLICTINGMODIFIERKEY diff --git a/src/modules/launcher/PowerLauncher/Helper/ErrorReporting.cs b/src/modules/launcher/PowerLauncher/Helper/ErrorReporting.cs index 5003a02cae..4ff1a08697 100644 --- a/src/modules/launcher/PowerLauncher/Helper/ErrorReporting.cs +++ b/src/modules/launcher/PowerLauncher/Helper/ErrorReporting.cs @@ -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"); - } } } diff --git a/src/modules/launcher/PowerLauncher/Helper/ExceptionHelper.cs b/src/modules/launcher/PowerLauncher/Helper/ExceptionHelper.cs new file mode 100644 index 0000000000..15e7de4eac --- /dev/null +++ b/src/modules/launcher/PowerLauncher/Helper/ExceptionHelper.cs @@ -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); + + /// + /// Returns true if the exception is a recoverable DWM composition exception. + /// + 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"); + } + } +} diff --git a/src/modules/launcher/PowerLauncher/Helper/ThemeManager.cs b/src/modules/launcher/PowerLauncher/Helper/ThemeManager.cs index 2a27494b30..53cc841b30 100644 --- a/src/modules/launcher/PowerLauncher/Helper/ThemeManager.cs +++ b/src/modules/launcher/PowerLauncher/Helper/ThemeManager.cs @@ -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); + } + + /// + /// Applies the theme with retry logic for desktop composition errors. + /// + /// The theme to apply. + /// Token to cancel the operation. + 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;