Repository navigation
Stop a full message queue from making NVDA permanently unresponsive - #21005
Closed
ChristosBouronikos wants to merge 1 commit into
Closed
ChristosBouronikos wants to merge 1 commit into
ChristosBouronikos wants to merge 1 commit into
Conversation
Fixes nvaccess#20846. The wx.CallAfter monkey patch posts a WM_NULL to the main frame after every call. While the main thread is frozen, these pile up, and once the thread's posted message quota is reached, PostMessage raises ERROR_NOT_ENOUGH_QUOTA. The callback has already been queued by wx at that point, but the exception escapes into every caller: the keyboard hook, audio feeding, win event and UIA handlers, virtual buffer change notifications and the core pump. NVDA then stops speaking and responding to the keyboard. In wxWidgets 3.3, wx runs all pending callbacks as soon as the main thread retrieves any message after a wake-up request, so only one WM_NULL needs to be waiting at a time. Only post a new one after wx has started running queued callbacks, and treat a failure to post as non-fatal, logging it once per failure episode.
ChristosBouronikos
marked this pull request as ready for review
October 16, 2026 08:29
ChristosBouronikos
requested review from
seanbudd
and
a balanced review from Copilot
October 16, 2026 08:29
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.
Link to issue number:
Fixes #20846 (partially, see below).
Important
This PR fixes the step that turns a temporary freeze into a permanent one. It does not fix the freeze itself.
When Chromium 153 is slow to answer NVDA's accessibility calls, NVDA's main thread still freezes, and that is not changed here. What this PR changes is what happens after: currently, a long enough freeze leaves NVDA permanently silent and ignoring the keyboard, even after the browser recovers, until NVDA is restarted (and sometimes it can't be restarted either). With this PR, NVDA should resume speaking and responding once the main thread is free again.
If the main thread is blocked forever (for example a call into the browser that never returns), NVDA will still stay frozen. This PR does not address that.
Note
Only unit tested so far, on macOS with fake objects. I have not yet tested this on Windows or reproduced the original freeze with the fix applied. Testing with WhatsApp Web / YouTube in Edge or Brave 153 (IAccessible2 mode, the default) would be very welcome.
Summary of the issue:
In the logs on the issue, the failure has two stages.
Stage 1, the freeze (not fixed here). With Chromium 153 over IAccessible2, busy pages such as WhatsApp Web make NVDA's main thread wait a long time on calls into the browser. The watchdog logs
Starting freeze recovery. Browse mode scripts also fail withWinError 1775, because they run against a virtual buffer whose RPC handle is still null while the buffer is loading.Stage 2, the queue fills and NVDA stops working (fixed here). Every
wx.CallAftergoes through the wrapper inmonkeyPatches/wxMonkeyPatches.py, which posts aWM_NULLto the main frame after queuing the call. That message is a wake-up hint for native modal loops such as menus and message boxes. While the main thread is frozen, nothing removes these messages, so they pile up. Once the thread's posted message limit (10,000 by default) is reached,PostMessagefails withWinError 1816(ERROR_NOT_ENOUGH_QUOTA) andwinUser.PostMessageraises.At that point the call has already been queued by wx and will still run. The wake-up was only a hint. But the exception escapes into whatever called
wx.CallAfter, directly or throughcore.requestPumpandqueueHandler.queueFunction. That is almost every part of NVDA that hands work to the main thread, which is why the second reporter saw the error from so many places:keyboardHandler.internal_keyDownEvent: the gesture is queued, but the error is logged with a full traceback from inside the low level keyboard hook, and the key is not trapped.nvwave.WasapiWavePlayer._scheduleIdleCheck:core.callLaterraises before_isIdleCheckPendingis set, sofeed()fails on every audio chunk and speech stops.IAccessibleHandler.internalWinEventHandler, UIA event handlers,virtualBuffers.changeNotifyand scripts.core.CorePump.Notify/processRequest(theunhandled exceptionin the first log).So NVDA stops speaking and stops responding to the keyboard. A new NVDA instance also can't post
WM_QUITto the old one, which explainsCouldn't terminate existing NVDA process ... [WinError 1816].Description of user facing changes:
Description of developer facing changes:
wx.CallAfter(as patched by NVDA) no longer raises if the wake-up message can't be posted. The failure is logged as a warning, once per run of consecutive failures.wx.CallAfterposts at most one wake-upWM_NULLuntil wx starts running queued calls, instead of one per call.wx.CallAfternow accepts a keyword argument namedfuncfor the called function, like wxPython's ownCallAfter.Description of development approach:
I checked wake-up handling in the wxWidgets 3.3.4 source (the version wxPython 4.3.2 is built on):
wx.CallAfter→wxEvtHandler::QueueEvent→wxWakeUpIdle(), which sets an auto-reset wake event. wx itself posts no Windows message.WH_GETMESSAGEhook (wxIdleWakeUpModule). Whenever the main thread removes any message from its queue while the wake event is set, wx runsProcessPendingEvents(), which runs every pending call, including ones queued while it runs.So a single
WM_NULLwaiting in the queue is enough to get all pending calls run, even inside a native modal loop. A second one for the same wake-up does nothing, even with the current code, because the first one already used up the wake event.Changes in
monkeyPatches/wxMonkeyPatches.py:WM_NULLis only posted if the flag is clear. The flag is set before posting, so a race can only cause an extra (harmless) post, never a missed one. While the main thread is frozen, NVDA now adds one message to its queue instead of one per call.OSErrorfromPostMessageis caught. If the queue is full, it already holds messages that will wake the main thread. The flag stays set after a failure, so NVDA doesn't keep retrying against a full queue until wx runs queued calls again. The warning is logged once per run of failures, to avoid flooding the log while the queue is full.logHandleris imported lazily, like incomtypesMonkeyPatches.py, so that importingmonkeyPatchesearly innvda.pywstill doesn't importlogHandlerbeforeglobalVars.appDiris set.Not done in this PR:
WinError 1775script errors on a virtual buffer that hasn't finished loading. These are logged script errors, not the cause of the freeze, and can be handled separately.core.CorePumpis unchanged.Testing strategy:
tests/unit/test_wxMonkeyPatches.py, using fakewx,winUserand main frame objects:WM_NULLuntil a queued call runs, and the next call posts again;funcis passed through.mastercode and pass with this change. I ran them locally on macOS with a stubbedlogHandler; they still need to run on Windows CI.ruff checkandruff format --checkpass (ruff 0.16.3).wx.CallAftertiming in native modal loops (NVDA menu, message boxes, context menus) should be unchanged.Known issues with pull request:
Code Review Checklist: