Compare commits

..

3 Commits

Author SHA1 Message Date
Clint Rutkas
4939de08f4 Fix MSB3030 parallel-build race in ImageResizer.UnitTests
ImageResizerUI is a self-contained WinUI 3 app whose runtimeconfig.json and deps.json are written late into the shared WinUI3Apps output folder. ImageResizer.UnitTests references it only for types (InternalsVisibleTo), never launches the app, yet MSBuild pulled those two app runtime json files into the test's copy-to-output list. Under a highly parallel build the copy step could run before the files existed, producing MSB3030. Remove just those two never-needed files from the copy lists so the race is impossible; the referenced PowerToys.ImageResizer.dll is still copied so tests run normally.
2026-07-01 22:17:14 -07:00
Clint Rutkas
0ef68e4cd4 first part of cold full reboot fix 2026-07-01 21:53:57 -07:00
Michael Jolley
0afe525f31 Fix memory leaks in Command Palette: unsubscribe event handlers and dispose resources (#48884)
## Summary

Fixes 6 memory leaks in Command Palette caused by event handlers not
being unsubscribed and disposable resources not being released.

## Changes

| File | Fix |
|------|-----|
| `MainListPage.cs` | Replace lambda on static
`AllAppsCommandProvider.Page.PropChanged` with named method; unsubscribe
in `Dispose()` |
| `WinRTExtensionService.cs` | Unsubscribe static `PackageCatalog`
events (`PackageInstalling`/`Uninstalling`/`Updating`) in `Dispose()` |
| `MainWindow.xaml.cs` | Unsubscribe all event handlers in `Dispose()`
(`SizeChanged`, `SettingsChanged`, `ActualThemeChanged`,
`XamlRoot.Changed`, `CardElement.SizeChanged`, timer `Tick`,
`ThemeChanged`, `KeyPressed`); replace lambda with named method |
| `ContentFormControl.xaml.cs` | Unsubscribe previous
`RenderedAdaptiveCard.Action` before subscribing to new card |
| `BlurImageControl.cs` | Track `LoadedImageSurface` and unsubscribe
`LoadCompleted` before loading a new image |
| `ShowFileInFolderCommand.cs` | Dispose `Process` object returned by
`Process.Start()` (handle leak) |

## Validation

- [x] Build clean (`tools\build\build.ps1 -Path src\modules\cmdpal
-Platform x64 -Configuration Debug`) — exit code 0
- [x] All 1809 CmdPal unit tests pass (2 pre-existing skips)

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-07-01 18:59:55 +02:00
13 changed files with 161 additions and 80 deletions

View File

@@ -163,7 +163,7 @@ configuration:
association: Collaborator
then:
- addReply:
reply: We've identified this issue as a duplicate of an existing one and are closing this thread so discussion stays in one place.<br/><br/>Please see the comment above for the link to the original tracking issue, and feel free to subscribe there for updates.
reply: We've identified this issue as a duplicate of an existing one and are closing this thread so discussion stays in one place.<br/><br/>Please see the comment above for the link to the original tracking issue, and feel free to subscribe there for updates.
- closeIssue
- removeLabel:
label: Needs-Triage
@@ -257,5 +257,14 @@ configuration:
- addReply:
reply: "To help debug your layout, please run [this script](https://github.com/microsoft/PowerToys/blob/main/src/modules/MouseUtils/CursorWrap/CursorWrapTests/Capture-MonitorLayout.ps1) and attach the generated JSON output to this thread.\n\nThis allows us to better understand the issue and investigate potential fixes."
description:
- if:
- payloadType: Issue_Comment
- commentContains:
pattern: "I(( would|'d) (like|love|be happy)| want) (to help|helping|to contribute|contributing|to implement|implementing|to fix|fixing)"
isRegex: True
then:
- addReply:
reply: Hi! Your last comment indicates to our system, that you might want to contribute to this feature/fix this bug. Thank you! Please make us aware on our ["Would you like to contribute to PowerToys?" thread](https://github.com/microsoft/PowerToys/issues/28769), as we don't see all the comments. <br /><br />_I'm a bot (beep!) so please excuse any mistakes I may make_
description:
onFailure:
onSuccess:

View File

@@ -1,60 +0,0 @@
---
description: Reply when a new issue comment indicates intent to contribute
on:
issue_comment:
types: [created]
roles: all
permissions:
contents: read
issues: read
models: read
tools:
github:
toolsets: [issues]
safe-outputs:
add-comment:
max: 1
noop:
---
# Contribution Intent Reply
You handle newly created issue comments in microsoft/PowerToys.
## Goal
Detect whether the new comment indicates that the author wants to contribute to fixing or implementing work for the issue.
## Scope
- Process **issue comments only**.
- If `${{ github.event.issue.pull_request }}` is present, this is a PR comment context and you must do nothing.
- Ignore comments from bot accounts.
## Decision rule
Reply only when the new comment clearly expresses first-person contribution intent for this issue, such as:
- "I want to contribute"
- "I'd like to help"
- "I can implement this"
- "I want to fix this"
Do **not** reply when the comment is only:
- agreement or feedback without volunteering
- third-person suggestions ("someone should fix this")
- requests for others to contribute
When uncertain, do not reply.
## Required reply text
When contribution intent is detected, post exactly this comment on the same issue:
Hi! Your last comment indicates to our system, that you might want to contribute to this feature/fix this bug. Thank you! Please make us aware on our ["Would you like to contribute to PowerToys?" thread](https://github.com/microsoft/PowerToys/issues/28769), as we don't see all the comments. <br /><br />_I'm a bot (beep!) so please excuse any mistakes I may make_
## Safe output behavior
- If contribution intent is detected: use `add-comment` once on issue `${{ github.event.issue.number }}` with the required reply text above.
- If no reply is needed: use `noop` with a short reason.

View File

@@ -2,6 +2,34 @@
<Project ToolsVersion="4.0"
xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
<!--
Cold "Rebuild All" ordering fix for native PackageReference projects.
A handful of native projects use <RestoreProjectStyle>PackageReference</RestoreProjectStyle>
(Microsoft.CommandPalette.Extensions, Microsoft.Terminal.UI, PowerToys.MeasureToolCore,
FindMyMouse, PowerRenameUILib, runner). For those, the CppWinRT configuration - most importantly
the MIDL default <EnableWindowsRuntime>true</EnableWindowsRuntime> that makes IInspectable /
IUnknown resolvable - only arrives via the restore-generated obj\*.nuget.g.props.
On a cold "Rebuild All", MSBuild evaluates the project (resolving imports) before the in-session
NuGet restore regenerates those obj props, so MIDL runs in non-WinRT mode and fails with
"MIDL2009: undefined symbol IInspectable". Because Microsoft.CommandPalette.Extensions and
Microsoft.Terminal.UI are winmd producers consumed (by file path) across the entire Command
Palette graph, that single race cascades into dozens of downstream CS0006/CS0234 failures. A
second build "works" only because restore has since written the obj props. This is why the
RestoreThenBuild PowerShell helper (two separate msbuild invocations) already avoids the issue.
Statically importing the committed CppWinRT props here makes the WinRT MIDL configuration present
at evaluation time regardless of restore ordering, so it is durable across Visual Studio, the
build scripts, and CI. It is scoped to PackageReference-style vcxproj only (the ~100 other native
projects already import this same committed package directly), and the Exists() guard plus
CppWinRT's own import guards keep it a safe no-op once restore has run.
-->
<Import Project="$(MSBuildThisFileDirectory)packages\Microsoft.Windows.CppWinRT.2.0.250303.1\build\native\Microsoft.Windows.CppWinRT.props"
Condition="'$(MSBuildProjectExtension)' == '.vcxproj'
and '$(RestoreProjectStyle)' == 'PackageReference'
and Exists('$(MSBuildThisFileDirectory)packages\Microsoft.Windows.CppWinRT.2.0.250303.1\build\native\Microsoft.Windows.CppWinRT.props')" />
<!-- Skip building C++ test projects when BuildTests=false -->
<PropertyGroup Condition="'$(_IsSkippedTestProject)' == 'true'">
<UsePrecompiledHeaders>false</UsePrecompiledHeaders>

View File

@@ -13,4 +13,25 @@
-->
<Import Project="$(VcpkgRoot)\scripts\buildsystems\msbuild\vcpkg.targets"
Condition="'$(MSBuildProjectExtension)' == '.vcxproj' and '$(VcpkgEnabled)' == 'true' and Exists('$(VcpkgRoot)\scripts\buildsystems\msbuild\vcpkg.targets')" />
<!--
Cold "Rebuild All" ordering fix (matching partner to the CppWinRT props import in Cpp.Build.props).
Native PackageReference projects (Microsoft.CommandPalette.Extensions, Microsoft.Terminal.UI,
PowerToys.MeasureToolCore, FindMyMouse, PowerRenameUILib, runner) receive the CppWinRT MIDL
metadata wiring (CppWinRTMidlResponseFile, AdditionalMetadataDirectories, the MdMerge step that
produces the .winmd) only from the restore-generated obj\*.nuget.g.targets. On a cold "Rebuild
All" that file has not been regenerated yet when the project is evaluated, so MIDL cannot resolve
IInspectable / IUnknown and the Command Palette graph fails with MIDL2009 - until a second build.
Importing the committed CppWinRT targets here - after Microsoft.Cpp.targets, exactly where the
statically-wired native projects put their own CppWinRT ExtensionTargets import - makes that wiring
present regardless of restore ordering. Scoped to PackageReference-style vcxproj only; the Exists()
guard and CppWinRT's own import guards make it a safe no-op once restore has run or for the ~100
native projects that already import this committed package directly.
-->
<Import Project="$(MSBuildThisFileDirectory)packages\Microsoft.Windows.CppWinRT.2.0.250303.1\build\native\Microsoft.Windows.CppWinRT.targets"
Condition="'$(MSBuildProjectExtension)' == '.vcxproj'
and '$(RestoreProjectStyle)' == 'PackageReference'
and Exists('$(MSBuildThisFileDirectory)packages\Microsoft.Windows.CppWinRT.2.0.250303.1\build\native\Microsoft.Windows.CppWinRT.targets')" />
</Project>

View File

@@ -94,6 +94,7 @@ internal sealed partial class HttpCachingClient : IDisposable
public void Dispose()
{
_httpClient.Dispose();
_cacheHandler.Dispose();
}
private static bool IsSupportedHttpUri(Uri resourceUri)

View File

@@ -146,13 +146,7 @@ public sealed partial class MainListPage : DynamicListPage,
// The all apps page will kick off a BG thread to start loading apps.
// We just want to know when it is done.
var allApps = AllAppsCommandProvider.Page;
allApps.PropChanged += (s, p) =>
{
if (p.PropertyName == nameof(allApps.IsLoading))
{
IsLoading = ActuallyLoading();
}
};
allApps.PropChanged += AllApps_PropChanged;
WeakReferenceMessenger.Default.Register<ClearSearchMessage>(this);
WeakReferenceMessenger.Default.Register<UpdateFallbackItemsMessage>(this);
@@ -172,6 +166,14 @@ public sealed partial class MainListPage : DynamicListPage,
}
}
private void AllApps_PropChanged(object? sender, IPropChangedEventArgs e)
{
if (e.PropertyName == nameof(AllAppsCommandProvider.Page.IsLoading))
{
IsLoading = ActuallyLoading();
}
}
private void PinnedCommands_CollectionChanged(object? sender, NotifyCollectionChangedEventArgs e)
{
_defaultViewDirty = true;
@@ -782,6 +784,8 @@ public sealed partial class MainListPage : DynamicListPage,
_tlcManager.TopLevelCommands.CollectionChanged -= Commands_CollectionChanged;
_tlcManager.PinnedCommands.CollectionChanged -= PinnedCommands_CollectionChanged;
AllAppsCommandProvider.Page.PropChanged -= AllApps_PropChanged;
if (_settingsService is not null)
{
_settingsService.SettingsChanged -= SettingsChangedHandler;

View File

@@ -541,6 +541,9 @@ public partial class WinRTExtensionService : IExtensionService, IDisposable
{
if (disposing)
{
_catalog.PackageInstalling -= Catalog_PackageInstalling;
_catalog.PackageUninstalling -= Catalog_PackageUninstalling;
_catalog.PackageUpdating -= Catalog_PackageUpdating;
_getInstalledExtensionsLock.Dispose();
}

View File

@@ -94,6 +94,7 @@ internal sealed partial class BlurImageControl : Control
private SpriteVisual? _effectVisual;
private CompositionEffectBrush? _effectBrush;
private CompositionSurfaceBrush? _imageBrush;
private LoadedImageSurface? _lastLoadedSurface;
public BlurImageControl()
{
@@ -379,10 +380,20 @@ internal sealed partial class BlurImageControl : Control
}
Logger.LogDebug($"Starting load of BlurImageControl from '{bitmapImage.UriSource}'");
// Each call to LoadImageAsync creates a new LoadedImageSurface backed by native
// composition resources. The old surface becomes unrooted once the brush points at
// the new one, so it isn't leaked, but dispose it explicitly so the unmanaged
// resources are released deterministically instead of waiting for finalization.
var previousSurface = _lastLoadedSurface;
var loadedSurface = LoadedImageSurface.StartLoadFromUri(bitmapImage.UriSource);
_lastLoadedSurface = loadedSurface;
loadedSurface.LoadCompleted += OnLoadedSurfaceOnLoadCompleted;
SetLoadedSurfaceToBrush(loadedSurface);
_effectBrush?.SetSourceParameter(ImageSourceParameterName, _imageBrush);
previousSurface?.Dispose();
}
catch (Exception ex)
{

View File

@@ -119,8 +119,9 @@ public sealed partial class ContentFormControl : UserControl
private void OnFrameworkElementLayoutUpdated(object? sender, object e)
{
// Only fix once — unhook after first layout pass
if (_renderedCard?.FrameworkElement is FrameworkElement element)
// Only fix once — unhook from sender (not _renderedCard, which may have been
// reassigned by the time this fires).
if (sender is FrameworkElement element)
{
element.LayoutUpdated -= OnFrameworkElementLayoutUpdated;
FixToggleAccessibilityNames(element);

View File

@@ -192,7 +192,7 @@ public sealed partial class MainWindow : WindowEx,
App.Current.Services.GetRequiredService<ISettingsService>().SettingsChanged += SettingsChangedHandler;
// Make sure that we update the acrylic theme when the OS theme changes
RootElement.ActualThemeChanged += (s, e) => DispatcherQueue.TryEnqueue(UpdateBackdrop);
RootElement.ActualThemeChanged += RootElement_ActualThemeChanged;
// Hardcoding event name to avoid bringing in the PowerToys.interop dependency. Event name must match CMDPAL_SHOW_EVENT from shared_constants.h
NativeEventWaiter.WaitForEventLoop("Local\\PowerToysCmdPal-ShowEvent-62336fcd-8611-4023-9b30-091a6af4cc5a", () =>
@@ -222,6 +222,11 @@ public sealed partial class MainWindow : WindowEx,
UpdateBackdrop();
}
private void RootElement_ActualThemeChanged(FrameworkElement sender, object args)
{
DispatcherQueue.TryEnqueue(UpdateBackdrop);
}
private static void LocalKeyboardListener_OnKeyPressed(object? sender, LocalKeyboardListenerKeyPressedEventArgs e)
{
if (e.Key == VirtualKey.GoBack)
@@ -1683,6 +1688,9 @@ public sealed partial class MainWindow : WindowEx,
public void Dispose()
{
_themeService.ThemeChanged -= ThemeServiceOnThemeChanged;
App.Current.Services.GetRequiredService<ISettingsService>().SettingsChanged -= SettingsChangedHandler;
_localKeyboardListener.Dispose();
_windowThemeSynchronizer.Dispose();
DisposeAcrylic();

View File

@@ -22,6 +22,7 @@ public sealed partial class ExtensionsPage : Page
private readonly SettingsViewModel? viewModel;
private readonly Dictionary<string, WeakReference<SettingsCard>> _vmToCardMap = new();
private readonly Dictionary<SettingsCard, ProviderSettingsViewModel> _cardToVmMap = new();
public ExtensionsPage()
{
@@ -31,6 +32,23 @@ public sealed partial class ExtensionsPage : Page
var themeService = App.Current.Services.GetService<IThemeService>()!;
var settingsService = App.Current.Services.GetRequiredService<ISettingsService>();
viewModel = new SettingsViewModel(topLevelCommandManager, _mainTaskScheduler, themeService, settingsService);
Unloaded += ExtensionsPage_Unloaded;
}
private void ExtensionsPage_Unloaded(object sender, RoutedEventArgs e)
{
// ProviderSettingsViewModel subscribes to its CommandProviderWrapper (owned by the
// singleton TopLevelCommandManager), so a live VM roots this page through the
// PropertyChanged handler below. Drain any VMs still hooked when the page is torn
// down; SettingsCard_DataContextChanged only unhooks the ones that get recycled.
foreach (var vm in _cardToVmMap.Values)
{
vm.PropertyChanged -= ProviderViewModel_PropertyChanged;
}
_cardToVmMap.Clear();
_vmToCardMap.Clear();
}
private void SettingsCard_Click(object sender, RoutedEventArgs e)
@@ -46,16 +64,28 @@ public sealed partial class ExtensionsPage : Page
private void SettingsCard_DataContextChanged(FrameworkElement sender, DataContextChangedEventArgs args)
{
// Store the card reference keyed by Id (not the VM itself) to avoid leaking VM references
if (sender is SettingsCard card && card.DataContext is ProviderSettingsViewModel newVm)
if (sender is SettingsCard card)
{
_vmToCardMap[newVm.Id] = new WeakReference<SettingsCard>(card);
newVm.PropertyChanged += ProviderViewModel_PropertyChanged;
// Immediately update automation name in case DisplayName is already available
if (card.Content is ToggleSwitch toggle && !string.IsNullOrEmpty(newVm.DisplayName))
// Unsubscribe from the previous ViewModel to prevent handler accumulation
// when virtualization recycles items with a new DataContext.
if (_cardToVmMap.TryGetValue(card, out var oldVm))
{
AutomationProperties.SetName(toggle, newVm.DisplayName);
oldVm.PropertyChanged -= ProviderViewModel_PropertyChanged;
_cardToVmMap.Remove(card);
}
// Store the card reference keyed by Id (not the VM itself) to avoid leaking VM references
if (card.DataContext is ProviderSettingsViewModel newVm)
{
_vmToCardMap[newVm.Id] = new WeakReference<SettingsCard>(card);
_cardToVmMap[card] = newVm;
newVm.PropertyChanged += ProviderViewModel_PropertyChanged;
// Immediately update automation name in case DisplayName is already available
if (card.Content is ToggleSwitch toggle && !string.IsNullOrEmpty(newVm.DisplayName))
{
AutomationProperties.SetName(toggle, newVm.DisplayName);
}
}
}
}

View File

@@ -27,7 +27,7 @@ public partial class ShowFileInFolderCommand : InvokableCommand
try
{
var argument = "/select, \"" + _path + "\"";
Process.Start("explorer.exe", argument);
using var process = Process.Start("explorer.exe", argument);
}
catch (Exception)
{

View File

@@ -26,6 +26,31 @@
<ProjectReference Include="..\ui\ImageResizerUI.csproj" />
</ItemGroup>
<!--
MSB3030 parallel-build race hardening.
ImageResizerUI is a self-contained WinUI 3 app whose PowerToys.ImageResizer.runtimeconfig.json
and PowerToys.ImageResizer.deps.json are written late into the shared WinUI3Apps output folder.
This unit test project references ImageResizerUI only for its types (see InternalsVisibleTo
"ImageResizer.Test" in ImageResizerUI.csproj); it never launches the app, so it does not need
those two app runtime json files. During a highly parallel "Build All" the test's
copy-to-output step can run before those files exist, producing:
MSB3030: Could not copy the file "...\PowerToys.ImageResizer.runtimeconfig.json" because it was not found.
Removing just those two never-needed files from this project's copy-to-output lists makes the
race impossible. The referenced PowerToys.ImageResizer.dll is still copied, so tests run normally.
-->
<Target Name="ExcludeReferencedAppRuntimeJsonFromCopy"
AfterTargets="GetCopyToOutputDirectoryItems">
<ItemGroup>
<_SourceItemsToCopyToOutputDirectory
Remove="@(_SourceItemsToCopyToOutputDirectory)"
Condition="'%(Filename)%(Extension)' == 'PowerToys.ImageResizer.runtimeconfig.json' or '%(Filename)%(Extension)' == 'PowerToys.ImageResizer.deps.json'" />
<_SourceItemsToCopyToOutputDirectoryAlways
Remove="@(_SourceItemsToCopyToOutputDirectoryAlways)"
Condition="'%(Filename)%(Extension)' == 'PowerToys.ImageResizer.runtimeconfig.json' or '%(Filename)%(Extension)' == 'PowerToys.ImageResizer.deps.json'" />
</ItemGroup>
</Target>
<ItemGroup>
<Content Include="Test.gif">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>