-
Notifications
You must be signed in to change notification settings - Fork 1.3k
Description
[Input/Stylus] WPF WISP pen stack hangs indefinitely when stylus subsystem is accessed after disposal
Summary
When a WPF application shuts down and the dispatcher on the main thread has been deactivated, the WISP stylus subsystem can be triggered to create a new window (e.g., by a system event). This window creation path calls PenThreadWorker.WorkerGetTabletsInfo() on an already-disposed PenThreadWorker, causing the calling thread to hang indefinitely — the application process never exits cleanly.
A secondary issue causes PenThreadPool to return disposed PenThread objects as valid candidates, potentially prolonging or exacerbating the shutdown hang.
Observed behavior
- Application process does not exit after UI is closed.
- The hung thread is blocked in
WaitHandle.WaitOne()with no timeout insidePenThreadWorker.WorkerGetTabletsInfo(). - The application must be force-killed.
Repro notes
- Open a WPF application on a system with a WISP-compatible device (e.g., a display that reports display settings changes).
- Leave the application running for an extended period or trigger a display settings change event (e.g., reconnect a monitor/devbox session) while the application is shutting down.
- Close the application.
- Observe that the process does not terminate — it hangs indefinitely.
Note: exact repro timing depends on a race between dispatcher shutdown and a system event (e.g.,
WM_DISPLAYCHANGE) arriving during the shutdown window.
Impact
- Affected WPF applications never exit cleanly; they must be force-terminated.
- Users perceive the application as frozen after the UI disappears.
- Reproducible under display settings change events that arrive during shutdown.
Expected behavior
When PenThreadWorker has been disposed, calls to WorkerGetTabletsInfo() should return an empty result immediately without blocking.
PenThreadPool should not return disposed PenThread objects; disposed entries should be pruned from the pool.
Actual behavior
WorkerGetTabletsInfo() unconditionally enqueues a work item and then calls WaitOne() with no timeout. If the pen thread has already exited because PenThreadWorker was disposed, no one will ever call Set() on the done event and the caller blocks forever.
PenThreadPool.GetPenThreadForPenContextHelper checks only whether a WeakReference<PenThread> is alive, but does not check whether the target PenThread is disposed. A disposed-but-still-referenced thread (kept alive by its registered PenContext / _handles array) is incorrectly returned as a usable candidate.
Suspected root cause
Gap 1 — WorkerGetTabletsInfo missing disposed guard
Every other Worker* method in PenThreadWorker (WorkerAddPenContext, WorkerRemovePenContext, WorkerCreateContext, WorkerAcquireTabletLocks, …) starts with a disposed check and returns a safe value when disposed. WorkerGetTabletsInfo was the sole exception.
When called on a disposed worker it:
- Enqueues a
WorkerOperationGetTabletsInfoonto_workerOperation. - Calls
RaiseResetEventon the PIMC reset handle. - Calls
getTablets.DoneEvent.WaitOne()— with no timeout.
The pen thread's ThreadProc exits when __disposed is true, so nobody calls DoneEvent.Set(). The caller waits forever.
Gap 2 — PenThreadPool returns disposed PenThread
PenThreadPool.GetPenThreadForPenContextHelper selects a candidate by checking only whether the WeakReference<PenThread> is still alive. It does not verify that the PenThread itself is not disposed. A disposed, but still strongly-referenced, PenThread is incorrectly returned as valid.
Proposed fix direction
- Add
internal bool IsDisposed => __disposed;toPenThreadWorkerto expose disposal state. - Add the matching
internal bool IsDisposed => _penThreadWorker.IsDisposed;surface toPenThread. - Add a disposed guard at the top of
PenThreadWorker.WorkerGetTabletsInfo()that returnsArray.Empty<TabletDeviceInfo>()immediately when disposed (consistent with all otherWorker*methods). - In
PenThreadPool.GetPenThreadForPenContextHelper, extend weak-reference cleanup to also remove entries whose targetPenThreadis disposed, and skip disposed candidates during selection. - Make
PenThread._penThreadWorkerreadonly(it is always assigned in the constructor and never reassigned) and remove the now-redundant null check inDisposeHelper.