Compare commits

..

3 Commits

Author SHA1 Message Date
Muyuan Li (from Dev Box)
a3f8d78e4b Address review: quote template path and use full explorer.exe path
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-05-14 16:29:04 +08:00
copilot-swe-agent[bot]
c27534331b Fix: use explorer.exe explicitly to open NewPlus templates folder when running as different admin user
Agent-Logs-Url: https://github.com/microsoft/PowerToys/sessions/481445da-c05e-4da6-af6c-07c83ecbf675

Co-authored-by: MuyuanMS <116717757+MuyuanMS@users.noreply.github.com>
2026-04-29 10:29:55 +00:00
copilot-swe-agent[bot]
aaadd5dd13 Initial plan 2026-04-29 08:51:05 +00:00
6 changed files with 50 additions and 139 deletions

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 (ExceptionHelper.IsRecoverableDwmCompositionException(e))
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)}");

View File

@@ -3,7 +3,6 @@
// See the LICENSE file in the project root for more information.
using System;
using System.Reflection;
using System.Runtime.InteropServices;
namespace PowerLauncher.Helper
@@ -23,13 +22,6 @@ namespace PowerLauncher.Helper
/// </summary>
internal static bool IsRecoverableDwmCompositionException(Exception exception)
{
// Unwrap TargetInvocationException to get the underlying exception, since WPF's internal
// theme-change mechanism invokes handlers via reflection which wraps exceptions this way.
if (exception is TargetInvocationException && exception.InnerException != null)
{
exception = exception.InnerException;
}
if (exception is not COMException comException)
{
return false;

View File

@@ -3,6 +3,7 @@
// See the LICENSE file in the project root for more information.
using System;
using System.Runtime.InteropServices;
using System.Threading;
using System.Threading.Tasks;
using System.Windows;
@@ -159,16 +160,12 @@ namespace PowerLauncher.Helper
return;
}
catch (Exception ex) when (ExceptionHelper.IsRecoverableDwmCompositionException(ex))
catch (COMException ex) when (ExceptionHelper.IsRecoverableDwmCompositionException(ex))
{
var logHResult = (ex is System.Reflection.TargetInvocationException tie && tie.InnerException != null)
? tie.InnerException.HResult
: ex.HResult;
switch (attempt)
{
case 1:
Log.Warn($"Desktop composition is disabled (HRESULT: 0x{logHResult:X}). Scheduling retries for theme update.", typeof(ThemeManager));
Log.Warn($"Desktop composition is disabled (HRESULT: 0x{ex.HResult:X}). Scheduling retries for theme update.", typeof(ThemeManager));
delayMs = InitialDelayMs;
break;
case < maxAttempts:

View File

@@ -1,118 +0,0 @@
// 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.Diagnostics.CodeAnalysis;
using System.Reflection;
using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;
using Microsoft.VisualStudio.TestTools.UnitTesting;
using PowerLauncher.Helper;
namespace Wox.Test;
[TestClass]
[SuppressMessage("StyleCop.CSharp.NamingRules", "SA1310:Field names should not contain underscore", Justification = "Win32 naming conventions")]
public class ExceptionHelperTest
{
private const int DWM_E_COMPOSITIONDISABLED = unchecked((int)0x80263001);
private const int STATUS_MESSAGE_LOST_HR = unchecked((int)0xD0000701);
private const string PresentationFrameworkSource = "PresentationFramework";
[TestMethod]
public void IsRecoverableDwmCompositionException_NullException_ReturnsFalse()
{
Assert.IsFalse(ExceptionHelper.IsRecoverableDwmCompositionException(null));
}
[TestMethod]
public void IsRecoverableDwmCompositionException_NonCOMException_ReturnsFalse()
{
var ex = new InvalidOperationException("Test");
Assert.IsFalse(ExceptionHelper.IsRecoverableDwmCompositionException(ex));
}
[TestMethod]
public void IsRecoverableDwmCompositionException_COMException_CompositionDisabled_ReturnsTrue()
{
var ex = new COMException("Desktop composition is disabled", DWM_E_COMPOSITIONDISABLED);
Assert.IsTrue(ExceptionHelper.IsRecoverableDwmCompositionException(ex));
}
[TestMethod]
public void IsRecoverableDwmCompositionException_COMException_OtherHResult_ReturnsFalse()
{
var ex = new COMException("Some other COM error", unchecked((int)0x80004005));
Assert.IsFalse(ExceptionHelper.IsRecoverableDwmCompositionException(ex));
}
[TestMethod]
public void IsRecoverableDwmCompositionException_TargetInvocationException_WrappingCompositionDisabled_ReturnsTrue()
{
var inner = new COMException("Desktop composition is disabled", DWM_E_COMPOSITIONDISABLED);
var ex = new TargetInvocationException("Invocation failed", inner);
Assert.IsTrue(ExceptionHelper.IsRecoverableDwmCompositionException(ex));
}
[TestMethod]
public void IsRecoverableDwmCompositionException_TargetInvocationException_WrappingMessageLostFromPresentationFramework_ReturnsTrue()
{
var inner = new COMException("Message lost", STATUS_MESSAGE_LOST_HR)
{
Source = PresentationFrameworkSource,
};
var ex = new TargetInvocationException("Invocation failed", inner);
Assert.IsTrue(ExceptionHelper.IsRecoverableDwmCompositionException(ex));
}
[TestMethod]
public void IsRecoverableDwmCompositionException_TargetInvocationException_WrappingDwmCompositionChangedStackTrace_ReturnsTrue()
{
var inner = CreateDwmCompositionChangedComException();
var ex = new TargetInvocationException("Invocation failed", inner);
Assert.IsTrue(ExceptionHelper.IsRecoverableDwmCompositionException(ex));
}
[TestMethod]
public void IsRecoverableDwmCompositionException_TargetInvocationException_WrappingUnrelatedCOMException_ReturnsFalse()
{
var inner = new COMException("Unrelated", unchecked((int)0x80004005));
var ex = new TargetInvocationException("Invocation failed", inner);
Assert.IsFalse(ExceptionHelper.IsRecoverableDwmCompositionException(ex));
}
[TestMethod]
public void IsRecoverableDwmCompositionException_TargetInvocationException_WrappingNonCOMException_ReturnsFalse()
{
var inner = new InvalidOperationException("Not a COM exception");
var ex = new TargetInvocationException("Invocation failed", inner);
Assert.IsFalse(ExceptionHelper.IsRecoverableDwmCompositionException(ex));
}
[TestMethod]
public void IsRecoverableDwmCompositionException_TargetInvocationException_NullInner_ReturnsFalse()
{
var ex = new TargetInvocationException("No inner", null);
Assert.IsFalse(ExceptionHelper.IsRecoverableDwmCompositionException(ex));
}
private static COMException CreateDwmCompositionChangedComException()
{
try
{
ThrowDwmCompositionChangedComException();
throw new AssertFailedException("Expected COMException to be thrown.");
}
catch (COMException ex)
{
return ex;
}
}
[MethodImpl(MethodImplOptions.NoInlining)]
private static void ThrowDwmCompositionChangedComException()
{
throw new COMException("DWM composition changed", unchecked((int)0x80004005));
}
}

View File

@@ -0,0 +1,34 @@
// 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.Diagnostics;
using System.IO;
using System.Reflection;
using Microsoft.PowerToys.Settings.UI.ViewModels;
using Microsoft.VisualStudio.TestTools.UnitTesting;
namespace ViewModelTests
{
[TestClass]
public class NewPlus
{
[TestMethod]
public void CreateExplorerProcessStartInfoShouldQuoteTemplatePathAndUseFullExplorerPath()
{
// Arrange
const string templatePath = @"C:\Users\Test User\Documents\My Templates";
var createExplorerProcessStartInfoMethod = typeof(NewPlusViewModel).GetMethod("CreateExplorerProcessStartInfo", BindingFlags.NonPublic | BindingFlags.Static);
// Act
var processStartInfo = (ProcessStartInfo)createExplorerProcessStartInfoMethod.Invoke(null, new object[] { templatePath });
// Assert
Assert.IsNotNull(processStartInfo);
Assert.AreEqual(Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.Windows), "explorer.exe"), processStartInfo.FileName);
Assert.AreEqual($"\"{templatePath}\"", processStartInfo.Arguments);
}
}
}

View File

@@ -330,6 +330,8 @@ namespace Microsoft.PowerToys.Settings.UI.ViewModels
}
}
private static readonly string ExplorerExePath = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.Windows), "explorer.exe");
private bool _isNewPlusEnabled;
private string _templateLocation;
private bool _hideFileExtension;
@@ -356,12 +358,7 @@ namespace Microsoft.PowerToys.Settings.UI.ViewModels
{
CopyTemplateExamples(_templateLocation);
var process = new ProcessStartInfo()
{
FileName = _templateLocation,
UseShellExecute = true,
};
Process.Start(process);
Process.Start(CreateExplorerProcessStartInfo(_templateLocation));
}
catch (Exception ex)
{
@@ -369,6 +366,15 @@ namespace Microsoft.PowerToys.Settings.UI.ViewModels
}
}
private static ProcessStartInfo CreateExplorerProcessStartInfo(string templateLocation)
{
return new ProcessStartInfo
{
FileName = ExplorerExePath,
Arguments = $"\"{templateLocation}\"",
};
}
private async void PickNewTemplateFolder()
{
var newPath = await PickFolderDialog();