Input/Stylus: Make WISP pen processing more resilient against disposal#11487
Open
etvorun wants to merge 1 commit intodotnet:mainfrom
Open
Input/Stylus: Make WISP pen processing more resilient against disposal#11487etvorun wants to merge 1 commit intodotnet:mainfrom
etvorun wants to merge 1 commit intodotnet:mainfrom
Conversation
- Add IsDisposed property to PenThreadWorker exposing the __disposed flag - Add disposed guard to WorkerGetTabletsInfo returning Array.Empty immediately (consistent with all other Worker* methods that already have this guard) - Expose IsDisposed on PenThread via forwarding property - PenThreadPool: prune disposed PenThread entries alongside dead WeakReferences - PenThread: make _penThreadWorker field readonly; remove now-redundant null check
Contributor
There was a problem hiding this comment.
Pull request overview
Improves shutdown robustness in the WPF WISP stylus stack by preventing synchronous pen-thread operations from hanging when invoked after (or during) disposal, and by avoiding selection of disposed pen threads from the pool.
Changes:
- Add
IsDisposedsurfaces onPenThreadWorkerandPenThread, and add a disposed guard inPenThreadWorker.WorkerGetTabletsInfo()to return an empty array instead of blocking. - Update
PenThreadPoolcandidate selection to prune dead/disposedPenThreadentries and avoid returning disposed threads. - Make
PenThread._penThreadWorkerreadonlyand remove the redundant null-conditional dispose.
Reviewed changes
Copilot reviewed 3 out of 3 changed files in this pull request and generated no comments.
| File | Description |
|---|---|
src/Microsoft.DotNet.Wpf/src/PresentationCore/System/Windows/Input/Stylus/Wisp/PenThreadWorker.cs |
Exposes disposal state and adds an early-return guard in WorkerGetTabletsInfo() to avoid hangs after disposal. |
src/Microsoft.DotNet.Wpf/src/PresentationCore/System/Windows/Input/Stylus/Wisp/PenThread.cs |
Forwards IsDisposed and tightens disposal semantics by making the worker field readonly. |
src/Microsoft.DotNet.Wpf/src/PresentationCore/System/Windows/Input/Stylus/Wisp/PenThreadPool.cs |
Prunes disposed pen threads from the pool so they aren’t selected as viable candidates. |
Comments suppressed due to low confidence (2)
src/Microsoft.DotNet.Wpf/src/PresentationCore/System/Windows/Input/Stylus/Wisp/PenThreadWorker.cs:608
- PR description states WorkerGetTabletsInfo is now guarded 'consistent with' WorkerCreateContext/WorkerAcquireTabletLocks/etc., but those methods currently do not check __disposed and will also block on DoneEvent if called after disposal. Either update the PR description for accuracy or consider applying a similar disposed guard pattern to those synchronous Worker* methods as well.
internal bool IsDisposed => __disposed;
internal TabletDeviceInfo[] WorkerGetTabletsInfo()
{
if (__disposed)
{
return Array.Empty<TabletDeviceInfo>();
}
// Set data up for this call
WorkerOperationGetTabletsInfo getTablets = new WorkerOperationGetTabletsInfo();
lock(_workerOperationLock)
{
_workerOperation.Add(getTablets);
}
// Kick thread to do this work.
MS.Win32.Penimc.UnsafeNativeMethods.RaiseResetEvent(_pimcResetHandle);
// Wait for this work to be completed.
getTablets.DoneEvent.WaitOne();
getTablets.DoneEvent.Close();
return getTablets.TabletDevicesInfo;
}
internal PenContextInfo WorkerCreateContext(IntPtr hwnd, IPimcTablet3 pimcTablet)
{
WorkerOperationCreateContext createContextOperation = new WorkerOperationCreateContext(
hwnd,
pimcTablet);
lock(_workerOperationLock)
{
_workerOperation.Add(createContextOperation);
}
// Kick thread to do this work.
MS.Win32.Penimc.UnsafeNativeMethods.RaiseResetEvent(_pimcResetHandle);
// Wait for this work to be completed.
createContextOperation.DoneEvent.WaitOne();
createContextOperation.DoneEvent.Close();
return createContextOperation.Result;
}
src/Microsoft.DotNet.Wpf/src/PresentationCore/System/Windows/Input/Stylus/Wisp/PenThreadWorker.cs:585
- WorkerGetTabletsInfo still has a potential indefinite wait if Dispose() races with this call: the method can observe __disposed == false, enqueue the operation and then block on DoneEvent, but ThreadProc exits immediately once __disposed becomes true and may never drain _workerOperation to signal the event. Consider making the wait bounded (timeout + safe fallback), or ensuring Dispose/ThreadProc unblocks/purges pending WorkerOperations by setting their DoneEvent(s) before exiting.
internal TabletDeviceInfo[] WorkerGetTabletsInfo()
{
if (__disposed)
{
return Array.Empty<TabletDeviceInfo>();
}
// Set data up for this call
WorkerOperationGetTabletsInfo getTablets = new WorkerOperationGetTabletsInfo();
lock(_workerOperationLock)
{
_workerOperation.Add(getTablets);
}
// Kick thread to do this work.
MS.Win32.Penimc.UnsafeNativeMethods.RaiseResetEvent(_pimcResetHandle);
// Wait for this work to be completed.
getTablets.DoneEvent.WaitOne();
getTablets.DoneEvent.Close();
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Fixes a hang in the WPF WISP stylus stack where calling
PenThreadWorker.WorkerGetTabletsInfo()on an already-disposed worker causes the calling thread to block indefinitely, preventing the application process from exiting cleanly.A secondary fix stops
PenThreadPoolfrom returning disposedPenThreadobjects as valid candidates.What changed
PenThreadWorker.csinternal bool IsDisposed => __disposed;property.WorkerGetTabletsInfo()— returnsArray.Empty<TabletDeviceInfo>()immediately when disposed, consistent with all otherWorker*methods (WorkerAddPenContext,WorkerRemovePenContext,WorkerCreateContext,WorkerAcquireTabletLocks, etc.).PenThread.csinternal bool IsDisposed => _penThreadWorker.IsDisposed;property forwarding to the worker._penThreadWorkerfieldreadonly(assigned in constructor only, never reassigned).DisposeHelper(field isreadonlyand always set).PenThreadPool.csWeakReferencecleanup loop to also prune and skipPenThreadentries that are disposed but still strongly referenced (e.g., kept alive via the_handlesarray).Why
WorkerGetTabletsInfowas the onlyWorker*method without a disposed guard. When the pen thread'sThreadProchas already exited (because__disposedistrue), it will never callDoneEvent.Set(). Any caller ofWorkerGetTabletsInfo()hitting the disposed worker blocks onWaitOne()forever.PenThreadPoolpreviously pruned only deadWeakReferencetargets. A disposed-but-still-alivePenThread(kept alive by its registeredPenContexthandles) passed the old aliveness check and was returned as a valid, ready-to-use thread — compounding the above hang.Validation notes
Fixes #11486
Microsoft Reviewers: Open in CodeFlow