Compare commits

..

3 Commits

Author SHA1 Message Date
Muyuan Li (from Dev Box)
f9679b937d Address review: handle NavigationFailed gracefully without rethrowing
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-05-14 16:20:42 +08:00
copilot-swe-agent[bot]
36300d3c75 Fix NullReferenceException in Frame_NavigationFailed when e.Exception is null
Agent-Logs-Url: https://github.com/microsoft/PowerToys/sessions/af982aa1-504b-47f1-9ec0-93b29602b2af

Co-authored-by: MuyuanMS <116717757+MuyuanMS@users.noreply.github.com>
2026-04-29 08:49:32 +00:00
copilot-swe-agent[bot]
1cde68ae04 Initial plan 2026-04-29 08:48:31 +00:00
4 changed files with 27 additions and 86 deletions

View File

@@ -18,13 +18,6 @@ public class ExtensionWrapper : IExtensionWrapper
{
private const int HResultRpcServerNotRunning = -2147023174;
// COM/WinRT HRESULT constants used during extension activation
private const int HResultNoInterface = unchecked((int)0x80004002); // E_NOINTERFACE
private const int HResultPathNotFound = unchecked((int)0x80070003); // E_PATH_NOT_FOUND (HRESULT_FROM_WIN32(ERROR_PATH_NOT_FOUND))
// IID_IUnknown - the base COM interface always accepted by any well-formed class factory
private static readonly Guid IID_IUnknown = new("00000000-0000-0000-C000-000000000046");
private readonly string _appUserModelId;
private readonly string _extensionId;
@@ -118,68 +111,28 @@ public class ExtensionWrapper : IExtensionWrapper
var extensionPtr = (void*)nint.Zero;
try
{
// First attempt: request IExtension directly.
// On Windows 11 23H2 (Build 22631), CoCreateInstance with a custom
// WinRT interface IID (like IExtension) may return E_NOINTERFACE because
// the OS cannot marshal the interface cross-process without a registered
// proxy/stub. Newer Windows builds (24H2+) handle this automatically via
// WinRT metadata. In that case, we fall back to IID_IUnknown so the
// server can return an IInspectable CCW, which is always marshalable.
var extensionIid = typeof(IExtension).GUID;
// -2147024809: E_INVALIDARG
// -2147467262: E_NOINTERFACE
// -2147024893: E_PATH_NOT_FOUND
var guid = typeof(IExtension).GUID;
var hr = PInvoke.CoCreateInstance(Guid.Parse(ExtensionClassId), null, CLSCTX.CLSCTX_LOCAL_SERVER, extensionIid, out extensionPtr);
var hr = PInvoke.CoCreateInstance(Guid.Parse(ExtensionClassId), null, CLSCTX.CLSCTX_LOCAL_SERVER, guid, out extensionPtr);
var usedIUnknownFallback = false;
if (hr.Value == HResultNoInterface)
{
// On Windows 23H2, the OS may be unable to marshal the IExtension
// WinRT interface cross-process. Retry with IID_IUnknown so the
// server can return an IInspectable CCW (marshalable on all Windows
// versions). We then QI for IExtension from the returned pointer.
Logger.LogWarning($"CoCreateInstance for {ExtensionDisplayName} returned E_NOINTERFACE for IID_IExtension (hr=0x{hr.Value:X8}). " +
$"Retrying with IID_IUnknown as a Windows 23H2 compatibility fallback.");
extensionPtr = (void*)nint.Zero;
hr = PInvoke.CoCreateInstance(Guid.Parse(ExtensionClassId), null, CLSCTX.CLSCTX_LOCAL_SERVER, IID_IUnknown, out extensionPtr);
usedIUnknownFallback = true;
}
if (hr.Value == HResultPathNotFound)
if (hr.Value == -2147024893)
{
Logger.LogError($"Failed to find {ExtensionDisplayName}: {hr}. It may have been uninstalled or deleted.");
// We don't really need to throw this exception.
// We'll just return out nothing.
return;
}
else if (hr.Value != 0)
{
// All other failures — log and bail out. Do NOT fall through to
// MarshalInterface below, which would dereference a null pointer and
// cause an access violation.
Logger.LogError($"Failed to activate {ExtensionDisplayName}: hr=0x{hr.Value:X8}. " +
$"On Windows 23H2 this may indicate that WinRT cross-process interface " +
$"marshaling is unsupported for custom interfaces without a registered proxy/stub.");
return;
Logger.LogError($"Failed to find {ExtensionDisplayName}: {hr.Value}");
}
if (usedIUnknownFallback)
{
// extensionPtr is an IUnknown/IInspectable cross-process proxy.
// Wrap it as IInspectable and then try to QI for IExtension.
// On Windows 23H2, this QI may fail (no proxy/stub for IExtension),
// resulting in a null _extensionObject. On newer Windows the QI
// should succeed via WinRT metadata-based marshaling.
var inspectable = MarshalInspectable<object>.FromAbi((nint)extensionPtr);
_extensionObject = inspectable as IExtension;
if (_extensionObject == null)
{
Logger.LogError($"Extension {ExtensionDisplayName} does not expose IExtension across the COM process boundary. " +
$"On Windows 23H2 (Build 22631) and earlier, custom WinRT interfaces cannot be marshaled " +
$"cross-process without a registered proxy/stub. The extension will not be available.");
}
}
else
{
_extensionObject = MarshalInterface<IExtension>.FromAbi((nint)extensionPtr);
}
// Marshal.ThrowExceptionForHR(hr);
_extensionObject = MarshalInterface<IExtension>.FromAbi((nint)extensionPtr);
}
catch (Exception e)
{

View File

@@ -456,18 +456,6 @@ public sealed partial class TopLevelCommandManager : ObservableObject,
try
{
await startTask.WaitAsync(ExtensionStartTimeout, ct).ConfigureAwait(false);
// If the extension server failed to activate (e.g. E_NOINTERFACE on older Windows),
// IsRunning() will be false and CommandProviderWrapper will throw. Check first so we
// can log a more actionable message than the generic "You forgot to call StartExtensionAsync".
if (!extension.IsRunning())
{
Logger.LogError($"Extension {extension.PackageFullName} did not activate after {sw.ElapsedMilliseconds} ms. " +
$"This can happen on Windows 11 23H2 (Build 22631) and earlier due to WinRT cross-process " +
$"interface marshaling limitations for custom interfaces. Check extension logs for HRESULT details.");
return ExtensionStartResult.Failed(extension);
}
Logger.LogInfo($"Started extension {extension.PackageFullName} in {sw.ElapsedMilliseconds} ms");
return ExtensionStartResult.Started(extension, new CommandProviderWrapper(extension, _taskScheduler, _commandProviderCache));
}

View File

@@ -28,15 +28,6 @@ internal sealed partial class ExtensionInstanceManager : IClassFactory
private static readonly Guid IID_IUnknown = Guid.Parse("00000000-0000-0000-C000-000000000046");
// IInspectable is the WinRT base interface (always marshalable cross-process on all Windows versions).
private static readonly Guid IID_IInspectable = Guid.Parse("AF86E2E0-B12D-4C6A-9C5A-D7AA65101E90");
// IExtension's WinRT interface IID. On Windows 11 23H2 (Build 22631), CoCreateInstance may
// invoke CreateInstance with this IID directly. We handle it by returning the IInspectable CCW,
// which COM on all Windows versions can marshal cross-process. On 24H2+ the OS uses WinRT
// metadata (winmd) to marshal custom interfaces transparently.
private static readonly Guid IID_IExtension = typeof(IExtension).GUID;
#pragma warning restore SA1310 // Field names should not contain underscore
private readonly Func<IExtension> _createExtension;
@@ -69,12 +60,9 @@ internal sealed partial class ExtensionInstanceManager : IClassFactory
Marshal.ThrowExceptionForHR(CLASS_E_NOAGGREGATION);
}
if (riid == _clsid || riid == IID_IUnknown || riid == IID_IInspectable || riid == IID_IExtension)
if (riid == _clsid || riid == IID_IUnknown)
{
// Create the instance of the .NET object and return it as IInspectable.
// Returning IInspectable ensures the CCW is marshalable cross-process on all
// Windows versions (including 23H2), since IInspectable has built-in OS support.
// The client will resolve the specific WinRT interface (IExtension) via CsWinRT.
// Create the instance of the .NET object
var managed = _createExtension();
var ins = MarshalInspectable<object>.FromManaged(managed);
ppvObject = ins;

View File

@@ -9,6 +9,7 @@ using System.Linq;
using System.Threading.Tasks;
using System.Windows.Input;
using ManagedCommon;
using Microsoft.PowerToys.Settings.UI.Helpers;
using Microsoft.PowerToys.Settings.UI.Library;
using Microsoft.PowerToys.Settings.UI.Library.Helpers;
@@ -135,7 +136,18 @@ namespace Microsoft.PowerToys.Settings.UI.ViewModels
private void Frame_NavigationFailed(object sender, NavigationFailedEventArgs e)
{
throw e.Exception;
var sourcePage = e.SourcePageType?.FullName ?? "<unknown>";
if (e.Exception is null)
{
Logger.LogWarning($"Navigation to '{sourcePage}' failed without an exception.");
}
else
{
Logger.LogError($"Navigation to '{sourcePage}' failed.", e.Exception);
}
e.Handled = true;
}
private void Frame_Navigated(object sender, NavigationEventArgs e)