Files
PowerToys/src/modules/launcher/PowerLauncher
Clint Rutkas 6d89ade9ad Fix PT Run ThreadPool worker leak from stale query cancellation (#48394)
## Summary

Fixes a ThreadPool worker leak in PowerToys Run that can eventually
surface as `System.OutOfMemoryException` from `Thread.StartInternal`
after rapid typing and repeated stale-query cancellation.

Related: #36041 and duplicate reports #45704, #36587, #39942, #20264,
and #8878.

## Root cause

`MainViewModel.QueryResults` stored the active cancellation token in a
mutable field. When a new query replaced that field, older workers could
observe the new, non-cancelled token instead of the token belonging to
their own query. The previous `CancellationTokenSource` was also
disposed while its consumers could still be running.

As stale queries accumulated, they continued invoking plugins and
consuming ThreadPool workers until the process could no longer create
another worker thread.

## Changes

- Adds `QuerySession`, which owns one captured token and the complete
task lifetime for a query. Superseded sessions are cancelled immediately
and their token sources are disposed only after their work completes.
- Uses a suspended session start so query state is published before
workers can return results.
- Adds generation checks before scheduling and applying work so
superseded queries cannot enqueue stale plugin tasks or update current
results.
- Adds a per-plugin execution gate. Calls to the same plugin do not
overlap, while unrelated plugins can execute independently; cancelled
waiters do not occupy ThreadPool workers.
- Preserves legacy `IResultUpdated` compatibility by correlating
generation-0 events using `RawQuery`.
- Preserves the original two-phase query contract: all non-delayed
plugin queries complete and their results are applied before delayed
queries start. Delayed queries remain globally parallel, and
`noInitialResults` is computed from the complete non-delayed phase.
- Cancels and performs a bounded wait for the active query during
shutdown.

## Tests

`Wox.Test`: **142/142 passing** locally.

Coverage includes:

- token ownership, cancellation, deferred disposal, shutdown timeout,
and suspended session startup;
- current-query generation matching and legacy generation-0
compatibility;
- per-plugin execution gating and queued latest-query behavior;
- deterministic verification that delayed queries cannot start until
every non-delayed query completes.

## Manual validation

1. Hold a key in PowerToys Run for 10–15 seconds and confirm the
PowerToys Run process thread count stabilizes instead of growing
monotonically.
2. Exercise normal Calculator, file, web, and indexer queries.
3. Enable search query tuning and waiting for slow results; confirm
results appear and final sorting completes.
4. Start a slow query and type again before it completes; only the
newest query should update results.
5. Exit PowerToys with a query in flight; shutdown should complete
cleanly without orphaned processes.

---------

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
Copilot-Session: 54e1bb28-edae-496b-8211-0e1592ddc985
2026-07-28 07:49:05 -07:00
..
2025-06-18 15:08:54 +08:00