From 6f4a18981abb142d633ee168d749cb2d3b1653b6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Christopher=20Pro=C3=9F?= Date: Sat, 11 Apr 2026 16:57:54 +0200 Subject: [PATCH 1/5] Queue JAB property change callbacks via internalQueueFunction (#16741) - Three event callbacks ran directly in the JVM callback thread, causing cross-process SendMessage deadlocks in JetBrains IDEs - Now queued to the main thread via internalQueueFunction, matching the existing pattern used by event_stateChange and event_caret --- source/JABHandler.py | 65 +++++++++++++++++++++++++++++++++++--------- 1 file changed, 52 insertions(+), 13 deletions(-) diff --git a/source/JABHandler.py b/source/JABHandler.py index a2f74d652d6..2fc68850559 100644 --- a/source/JABHandler.py +++ b/source/JABHandler.py @@ -548,7 +548,34 @@ def _fixBridgeFuncs(): def internalQueueFunction(func, *args, **kwargs): - internalFunctionQueue.put_nowait((func, args, kwargs)) + """Queue a function for execution on the main thread. + + When the queue is full, the oldest event is evicted to make room. + JOBJECT64 handles owned by evicted events are released to avoid leaks. + + .. note:: + All queued handler functions except :func:`enterJavaWindow_helper` + follow the convention ``(vmID, accContext, ...)``, where ``args[1]`` + is a JOBJECT64 handle. If a new handler with a different signature + is added, the eviction cleanup below must be updated. + """ + try: + internalFunctionQueue.put_nowait((func, args, kwargs)) + except queue.Full: + try: + evictedFunc, evictedArgs, _evictedKwargs = internalFunctionQueue.get_nowait() + # Release the JOBJECT64 accContext handle from the evicted event. + # All handlers except enterJavaWindow_helper have args[1] as accContext. + if evictedFunc is not enterJavaWindow_helper and len(evictedArgs) >= 2: + bridgeDll.releaseJavaObject(evictedArgs[0], evictedArgs[1]) + except queue.Empty: + pass + try: + internalFunctionQueue.put_nowait((func, args, kwargs)) + except queue.Full: + log.debugWarning("JAB internal function queue full, failed to re-queue after eviction") + return + log.debugWarning("JAB internal function queue full, evicted oldest event") core.requestPump() @@ -972,8 +999,13 @@ def internal_hasFocus(sourceContext): @AccessBridge_PropertyNameChangeFP -def event_nameChange(vmID, event, source, oldVal, newVal): - jabContext = JABContext(vmID=vmID, accContext=source) +def internal_event_nameChange(vmID, event, source, oldVal, newVal): + internalQueueFunction(event_nameChange, vmID, source) + bridgeDll.releaseJavaObject(vmID, event) + + +def event_nameChange(vmID, accContext): + jabContext = JABContext(vmID=vmID, accContext=accContext) if jabContext.hwnd: focus = api.getFocusObject() obj = ( @@ -985,12 +1017,16 @@ def event_nameChange(vmID, event, source, oldVal, newVal): eventHandler.queueEvent("nameChange", obj) else: log.debugWarning("Unable to obtain window handle for accessible context") - bridgeDll.releaseJavaObject(vmID, event) @AccessBridge_PropertyDescriptionChangeFP -def event_descriptionChange(vmID, event, source, oldVal, newVal): - jabContext = JABContext(vmID=vmID, accContext=source) +def internal_event_descriptionChange(vmID, event, source, oldVal, newVal): + internalQueueFunction(event_descriptionChange, vmID, source) + bridgeDll.releaseJavaObject(vmID, event) + + +def event_descriptionChange(vmID, accContext): + jabContext = JABContext(vmID=vmID, accContext=accContext) if jabContext.hwnd: focus = api.getFocusObject() obj = ( @@ -1002,12 +1038,16 @@ def event_descriptionChange(vmID, event, source, oldVal, newVal): eventHandler.queueEvent("descriptionChange", obj) else: log.debugWarning("Unable to obtain window handle for accessible context") - bridgeDll.releaseJavaObject(vmID, event) @AccessBridge_PropertyValueChangeFP -def event_valueChange(vmID, event, source, oldVal, newVal): - jabContext = JABContext(vmID=vmID, accContext=source) +def internal_event_valueChange(vmID, event, source, oldVal, newVal): + internalQueueFunction(event_valueChange, vmID, source) + bridgeDll.releaseJavaObject(vmID, event) + + +def event_valueChange(vmID, accContext): + jabContext = JABContext(vmID=vmID, accContext=accContext) if jabContext.hwnd: focus = api.getFocusObject() obj = ( @@ -1019,7 +1059,6 @@ def event_valueChange(vmID, event, source, oldVal, newVal): eventHandler.queueEvent("valueChange", obj) else: log.debugWarning("Unable to obtain window handle for accessible context") - bridgeDll.releaseJavaObject(vmID, event) @AccessBridge_PropertyStateChangeFP @@ -1155,9 +1194,9 @@ def initialize(): # Register java events bridgeDll.setFocusGainedFP(internal_event_focusGained) bridgeDll.setPropertyActiveDescendentChangeFP(internal_event_activeDescendantChange) - bridgeDll.setPropertyNameChangeFP(event_nameChange) - bridgeDll.setPropertyDescriptionChangeFP(event_descriptionChange) - bridgeDll.setPropertyValueChangeFP(event_valueChange) + bridgeDll.setPropertyNameChangeFP(internal_event_nameChange) + bridgeDll.setPropertyDescriptionChangeFP(internal_event_descriptionChange) + bridgeDll.setPropertyValueChangeFP(internal_event_valueChange) bridgeDll.setPropertyStateChangeFP(internal_event_stateChange) bridgeDll.setPropertyCaretChangeFP(internal_event_caretChange) isRunning = True From 485c36843eb13ae4ecb5e3a1feb8853074b369a4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Christopher=20Pro=C3=9F?= Date: Sat, 11 Apr 2026 17:00:04 +0200 Subject: [PATCH 2/5] Avoid recursive parent traversal in JAB findOverlayClasses (#16741) - Table cell detection created a full parent NVDAObject, recursively triggering findOverlayClasses up the entire UI hierarchy - New _hasTableParent() checks the parent role via lightweight context lookup first, only creating the full object when the parent is a table --- source/NVDAObjects/JAB/__init__.py | 33 +++++++++++++++++++++++++++++- 1 file changed, 32 insertions(+), 1 deletion(-) diff --git a/source/NVDAObjects/JAB/__init__.py b/source/NVDAObjects/JAB/__init__.py index b84864d8c54..f525456b030 100644 --- a/source/NVDAObjects/JAB/__init__.py +++ b/source/NVDAObjects/JAB/__init__.py @@ -248,13 +248,44 @@ def findOverlayClasses(self, clsList: list[NVDAObject]) -> None: clsList.append(ComboBox) elif role == "table": clsList.append(Table) - elif self.parent and isinstance(self.parent, Table) and self.parent._jabTableInfo: + elif self._hasTableParent(): clsList.append(TableCell) elif role == "progress bar": clsList.append(ProgressBar) clsList.append(JAB) + def _hasTableParent(self) -> bool: + """Lightweight check if the immediate parent is a Table. + + On the fast path (parent already cached), returns immediately + without any bridge calls. On the first call, checks the parent's + role via a lightweight bridge call and only creates a full NVDAObject + when the role is "table" (which involves additional bridge calls). + """ + if hasattr(self, "_parent"): + parent = self._parent + return parent is not None and isinstance(parent, Table) and parent._jabTableInfo + parentContext = self.jabContext.getAccessibleParentFromContext() + if not parentContext: + return False + try: + parentInfo = parentContext.getAccessibleContextInfo() + except RuntimeError: + log.debugWarning("Could not get accessible context info for parent", exc_info=True) + return False + if parentInfo.role_en_US != "table": + return False + if self.indexInParent is None: + # Without indexInParent we cannot construct a valid JAB parent; + # _get_parent would also fall back to the Window ancestor here. + return False + # Parent is a table — create the full parent object reusing the context + # we already hold, so _get_parent doesn't make a redundant bridge call. + self._parent = JAB(jabContext=parentContext) + parent = self._parent + return parent is not None and isinstance(parent, Table) and parent._jabTableInfo + @classmethod def kwargsFromSuper(cls, kwargs, relation: str | None = None) -> bool: jabContext = None From d4ef0bbada75e64d012fb69cdcf37a6d304153b4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Christopher=20Pro=C3=9F?= Date: Sat, 11 Apr 2026 17:03:21 +0200 Subject: [PATCH 3/5] Deregister all JAB event callbacks in terminate (#16741) - Name, description, and value change callbacks were never deregistered, leaving dangling function pointers after shutdown - Now matches the full set of callbacks registered in initialize() --- source/JABHandler.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/source/JABHandler.py b/source/JABHandler.py index 2fc68850559..3f9311b27a4 100644 --- a/source/JABHandler.py +++ b/source/JABHandler.py @@ -1213,6 +1213,9 @@ def terminate(): return bridgeDll.setFocusGainedFP(None) bridgeDll.setPropertyActiveDescendentChangeFP(None) + bridgeDll.setPropertyNameChangeFP(None) + bridgeDll.setPropertyDescriptionChangeFP(None) + bridgeDll.setPropertyValueChangeFP(None) bridgeDll.setPropertyStateChangeFP(None) bridgeDll.setPropertyCaretChangeFP(None) h = bridgeDll._handle From ee21363eb895b854ce4d911928a6d1a073b24377 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Christopher=20Pro=C3=9F?= Date: Sat, 11 Apr 2026 19:36:11 +0200 Subject: [PATCH 4/5] Update copyright years and add changelog entry (#16741) --- source/JABHandler.py | 2 +- source/NVDAObjects/JAB/__init__.py | 2 +- user_docs/en/changes.md | 1 + 3 files changed, 3 insertions(+), 2 deletions(-) diff --git a/source/JABHandler.py b/source/JABHandler.py index 3f9311b27a4..384ee3e02d0 100644 --- a/source/JABHandler.py +++ b/source/JABHandler.py @@ -1,5 +1,5 @@ # A part of NonVisual Desktop Access (NVDA) -# Copyright (C) 2007-2025 NV Access Limited, Peter Vágner, Renaud Paquay, Babbage B.V. +# Copyright (C) 2007-2026 NV Access Limited, Peter Vágner, Renaud Paquay, Babbage B.V. # This file is covered by the GNU General Public License. # See the file COPYING for more details. diff --git a/source/NVDAObjects/JAB/__init__.py b/source/NVDAObjects/JAB/__init__.py index f525456b030..db9cb8b2e71 100644 --- a/source/NVDAObjects/JAB/__init__.py +++ b/source/NVDAObjects/JAB/__init__.py @@ -1,5 +1,5 @@ # A part of NonVisual Desktop Access (NVDA) -# Copyright (C) 2006-2025 NV Access Limited, Leonard de Ruijter, Joseph Lee, Renaud Paquay, pvagner, hwf1324 +# Copyright (C) 2006-2026 NV Access Limited, Leonard de Ruijter, Joseph Lee, Renaud Paquay, pvagner, hwf1324 # This file is covered by the GNU General Public License. # See the file COPYING for more details. diff --git a/user_docs/en/changes.md b/user_docs/en/changes.md index 93821b8bbd8..32f70e20ec6 100644 --- a/user_docs/en/changes.md +++ b/user_docs/en/changes.md @@ -58,6 +58,7 @@ This is more noticeable for Windows releases which are enablement packages on to * Configuration profile triggers now activate when the Add-on Store is open. (#19583, @bramd) * Decorative Unicode letters such as negative squared, negative circled, and regional indicator symbol characters are now normalized to their base Latin letters when Unicode normalization is enabled. (#19608, @bramd) * NVDA no longer crashes when the Add-on Store download directory cannot be cleaned up due to file permission errors. (#19202, @christopherpross) +* Fixed NVDA freezing when navigating in JetBrains IDEs. (#16741, @christopherpross) ### Changes for Developers From e2638e87ba733f1dc620896f98b6d15ffa3bfc3b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Christopher=20Pro=C3=9F?= Date: Sun, 12 Apr 2026 10:01:21 +0200 Subject: [PATCH 5/5] Fix newDescendant handle leak in activeDescendantChange (#16741) When internal_hasFocus returned False, the newDescendant JOBJECT64 handle was never released since it was neither queued for processing nor explicitly freed. Add an else branch to release the handle. --- source/JABHandler.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/source/JABHandler.py b/source/JABHandler.py index 384ee3e02d0..a0751699e2a 100644 --- a/source/JABHandler.py +++ b/source/JABHandler.py @@ -986,6 +986,8 @@ def internal_event_activeDescendantChange(vmID, event, source, oldDescendant, ne sourceContext = JABContext(hwnd=hwnd, vmID=vmID, accContext=source) if internal_hasFocus(sourceContext): internalQueueFunction(event_gainFocus, vmID, newDescendant, hwnd) + else: + bridgeDll.releaseJavaObject(vmID, newDescendant) for accContext in [event, oldDescendant]: bridgeDll.releaseJavaObject(vmID, accContext)