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;