PowerToys Run: suppress crash report popup for unhandled exceptions thrown by user-installed plugins#47369
PowerToys Run: suppress crash report popup for unhandled exceptions thrown by user-installed plugins#47369Copilot wants to merge 4 commits into
Conversation
…nstalled plugins When a third-party PowerToys Run plugin throws an unhandled exception that propagates to the WPF dispatcher, PowerToys was showing a confusing crash report dialog even though the exception was not caused by PowerToys itself. Add IsExceptionFromUserPlugin() helper to ErrorReporting that inspects the exception's stack trace frames. If any frame's assembly is loaded from Constant.PluginsDirectory (the user-installed plugins folder), the exception is treated as a plugin exception: it is logged at Error level but the crash report window is not shown. This fix was motivated by a third-party plugin (Community.PowerToys.Run.Plugin.UniversalSearchSuggestions) throwing DirectoryNotFoundException from UpdatePlugin() when the PowerToys Run Plugin Updater directory didn't exist, causing a confusing crash popup on every launch. Fixes #38033 Agent-Logs-Url: https://github.com/microsoft/PowerToys/sessions/cd2e901b-d98e-4f74-8fe3-714604091a9d Co-authored-by: MuyuanMS <116717757+MuyuanMS@users.noreply.github.com>
Agent-Logs-Url: https://github.com/microsoft/PowerToys/sessions/cd2e901b-d98e-4f74-8fe3-714604091a9d Co-authored-by: MuyuanMS <116717757+MuyuanMS@users.noreply.github.com>
There was a problem hiding this comment.
Pull request overview
Suppresses the PowerToys Run crash report popup for unhandled exceptions whose stack trace contains frames from assemblies loaded out of the user-installed plugin directory (Constant.PluginsDirectory). Such exceptions are now logged at Error level instead of surfacing the ReportWindow. Preinstalled plugins remain unaffected.
Changes:
- Added a new
IsExceptionFromUserPlugin(Exception)helper that walks the exception's stack frames and matches assembly locations against the user plugins directory. - Inserted an
else ifbranch inHandleExceptionthat logs plugin-originated exceptions instead of opening the crash report window. - Added a try/catch fallback so reflection failures during stack inspection default to "not a plugin" (preserving the existing crash-report behavior).
Comments suppressed due to low confidence (2)
src/modules/launcher/PowerLauncher/Helper/ErrorReporting.cs:108
Assembly.Locationreturns an empty string for assemblies loaded from a byte array or via single-file/collectible contexts. While the empty-string case is guarded against here, this also means a plugin loaded viaAssembly.Load(byte[])(some plugin frameworks do this) will not be detected. More importantly, comparing path prefixes withStartsWithis fragile ifConstant.PluginsDirectoryis not normalized (no trailing separator, mixed separators, 8.3 short-name form, or symlinks); a plugin whose path differs only in case/normalization may be missed, and a sibling directory whose name shares the prefix (e.g.…\Plugins-backup\…) could match falsely. Consider normalizing both paths viaPath.GetFullPathand ensuring a directory separator at the end ofpluginsDirbefore comparing.
foreach (var frame in stackTrace.GetFrames() ?? [])
{
var assemblyLocation = frame.GetMethod()?.DeclaringType?.Assembly?.Location;
if (!string.IsNullOrEmpty(assemblyLocation) &&
assemblyLocation.StartsWith(pluginsDir, StringComparison.OrdinalIgnoreCase))
{
return true;
}
}
src/modules/launcher/PowerLauncher/Helper/ErrorReporting.cs:116
- Catching the bare base
System.Exceptionhere is broader than necessary and will swallow genuinely unexpected errors (includingOutOfMemoryException/StackOverflowException-adjacent issues that, while not catchable themselves, may be accompanied by other fatal conditions). The comment lists the expected reflection failures (SecurityException,TypeLoadException,BadImageFormatException, plusFileNotFoundException/FileLoadException); catching those explicitly — or at minimum logging the swallowed exception at Debug/Warn — would make this code easier to diagnose if it ever silently misclassifies.
catch (System.Exception)
{
// Reflection-based stack inspection can throw a variety of exceptions
// (e.g., SecurityException, TypeLoadException, BadImageFormatException).
// In every failure case the safe fallback is to treat the exception as
// NOT originating from a plugin so that it still surfaces as a crash report.
}
| private static bool IsExceptionFromUserPlugin(Exception e) | ||
| { | ||
| if (e == null) | ||
| { | ||
| return false; | ||
| } | ||
|
|
||
| try | ||
| { | ||
| var pluginsDir = Constant.PluginsDirectory; | ||
| var stackTrace = new StackTrace(e, fNeedFileInfo: false); | ||
|
|
||
| foreach (var frame in stackTrace.GetFrames() ?? []) | ||
| { | ||
| var assemblyLocation = frame.GetMethod()?.DeclaringType?.Assembly?.Location; | ||
| if (!string.IsNullOrEmpty(assemblyLocation) && | ||
| assemblyLocation.StartsWith(pluginsDir, StringComparison.OrdinalIgnoreCase)) | ||
| { | ||
| return true; | ||
| } | ||
| } | ||
| } | ||
| catch (System.Exception) | ||
| { | ||
| // Reflection-based stack inspection can throw a variety of exceptions | ||
| // (e.g., SecurityException, TypeLoadException, BadImageFormatException). | ||
| // In every failure case the safe fallback is to treat the exception as | ||
| // NOT originating from a plugin so that it still surfaces as a crash report. | ||
| } | ||
|
|
||
| return false; | ||
| } |
There was a problem hiding this comment.
Fixed: IsExceptionFromUserPlugin now walks the full exception chain including InnerException and AggregateException.InnerExceptions, so wrapped plugin exceptions are correctly identified.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
|
@copilot please re-review the latest changes. |
The latest changes look correct. |
Summary of the Pull Request
Third-party plugins can throw unhandled exceptions that propagate via
Task.ThrowAsyncto the WPF dispatcher, causing PowerToys to display the crash report window—even though PowerToys itself is healthy. This change detects such plugin-originated exceptions and logs them instead of surfacing the crash dialog.PR Checklist
Detailed Description of the Pull Request / Additional comments
Root cause: A plugin (
Community.PowerToys.Run.Plugin.UniversalSearchSuggestions) calls anUpdatePlugin()async method that opens aFileStreamto a path whose directory doesn't exist (…\Plugins\PowerToys Run Plugin Updater\PowerToys Run Plugin Updater.exe). The resultingDirectoryNotFoundExceptionis captured by the .NET task machinery and re-thrown on the WPF dispatcher viaTask.ThrowAsync, reachingDispatcherUnhandledException—which unconditionally showed the crash report window.Change in
ErrorReporting.cs:IsExceptionFromUserPlugin(Exception e): walks the full exception chain — iterating viaInnerExceptionand recursing intoAggregateException.InnerExceptions— to check if any stack frame's assembly is loaded fromConstant.PluginsDirectory(%LOCALAPPDATA%\Microsoft\PowerToys\PowerToys Run\Plugins). This ensures plugin exceptions that are wrapped by the .NET task machinery (e.g. inside anAggregateExceptionorTargetInvocationException) are correctly identified.HasPluginStackFrames(Exception e)as a dedicated helper that inspects the stack frames of a single exception instance.HandleException, added anelse ifbranch: plugin-originated exceptions are logged atErrorlevel; theReportWindowis not shown.false, so the crash report still surfaces for any exception that cannot be attributed to a plugin.Preinstalled PowerToys plugins (in
Constant.PreinstalledDirectory) are unaffected and will still trigger the crash report if they throw unhandled exceptions.Validation Steps Performed
Task.ThrowAsync→DispatcherUnhandledException→HandleException.Constant.PluginsDirectorycorrectly resolves to the user plugin folder; preinstalled plugins inConstant.PreinstalledDirectoryare excluded from the suppression logic.AggregateExceptioncontaining a plugin exception) are correctly identified by the recursive exception chain walk.catchblock fallback ensures no regression for genuine PowerToys crashes.