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;