diff --git a/devDocs/unreachableObjects.md b/devDocs/unreachableObjects.md new file mode 100644 index 00000000000..0dcb0fb1d5a --- /dev/null +++ b/devDocs/unreachableObjects.md @@ -0,0 +1,66 @@ +# Garbage collection errors +NVDA's `garbageHandler.py` monitors Python's cyclic garbage collector and reports +on objects that are unreachable. +Cyclic references are typically a symptom of bad design, and can cause major problems for certain objects. +For instance, cyclic references involving COM objects may cause a deadlock if the garbage collector happens to break the cycle and release the COM object in the wrong thread. + +## How to know about a cyclic reference? +The log may contain errors like the following. +``` +WARNING - garbageHandler.notifyObjectDeletion (10:45:23.171) - MainThread (21820): +Garbage collector has found one or more unreachable objects. See further warnings for specific objects. +... +WARNING - garbageHandler.notifyObjectDeletion (10:45:23.171) - MainThread (21820): +Deleting unreachable object +ERROR - garbageHandler._collectionCallback (10:45:23.172) - MainThread (21820): +Found at least 1 unreachable objects in run +``` + +## How to debug a cyclic reference? + +Once you can reliably reproduce the log error, you can tell the garbage collector to save all unreachable objects. +After an unreachable object is detected the references to the unreachable object can be inspected via the python console. +Inspecting this should give you a fair idea of where the issue is occurring. + +1. Open the NVDA Python console `NVDA+control+z` +1. Enable saving all objects: + ``` python + import gc + gc.set_debug(gc.DEBUG_SAVEALL) + ``` +1. Reproduce the unreachable object error. +1. If garbage collection errors have not yet been logged, force a collect by calling: + ``` python + gc.collect() + ``` + +1. All unreachable objects will now be stored in `gc.garbage`. + It may be a very large list. + Some tricks for narrowing this list down: + - From the log, you can get the memory address (`id`) of the object. + Then use: + ``` python + memoryAddress = 0xabcd123 + obj = None + for o in gc.garbage: + if memoryAddress == id(o) + obj = o + ``` + - Listing the types collected, look for the type(s) matching the log message: + ``` python + for index, o in enumerate(gc.garbage): + print(index, type(o)) + ``` +1. Once you have a reference (`obj`) to the unreachable object, see what other objects refer to an object you can call. + You can do this by using `gc.get_referrers`. + The python console has a reference to `obj`, there may be a lot of output. + You can reduce this by looking at the types and following the most relevant. + ``` python + for index, o in enumerate(gc.get_referrers(obj)): + print(index, type(o)) + ``` +1. Continue following the references to build a picture of the cycle. + +## Typical problems +Some examples of common issues: +- Exceptions caught and assigned to a local variable. diff --git a/source/core.py b/source/core.py index 630284f4483..b2c1e7584f6 100644 --- a/source/core.py +++ b/source/core.py @@ -14,10 +14,6 @@ class CallCancelled(Exception): """Raised when a call is cancelled. """ -# Apply several monkey patches to comtypes -# noinspection PyUnresolvedReferences -import comtypesMonkeyPatches - # Initialise comtypes.client.gen_dir and the comtypes.gen search path # and Append our comInterfaces directory to the comtypes.gen search path. import comtypes diff --git a/source/gui/__init__.py b/source/gui/__init__.py index f0305d3db54..2be10911381 100644 --- a/source/gui/__init__.py +++ b/source/gui/__init__.py @@ -560,21 +560,8 @@ def initialize(): # otherwise the system default will be used mainFrame.SetLayoutDirection(wxLang.LayoutDirection) wx.GetApp().SetTopWindow(mainFrame) - # In wxPython >= 4.1, - # wx.CallAfter no longer executes callbacks while NVDA's main thread is within apopup menu or message box. - # To work around this, - # Monkeypatch wx.CallAfter to - # post a WM_NULL message to our top-level window after calling the original CallAfter, - # which causes wx's event loop to wake up enough to execute the callback. - old_wx_CallAfter = wx.CallAfter - - def wx_CallAfter_wrapper(func, *args, **kwargs): - old_wx_CallAfter(func, *args, **kwargs) - # mainFrame may be None as NVDA could be terminating. - topHandle = mainFrame.Handle if mainFrame else None - if topHandle: - winUser.PostMessage(topHandle, winUser.WM_NULL, 0, 0) - wx.CallAfter = wx_CallAfter_wrapper + import monkeyPatches + monkeyPatches.applyWxMonkeyPatches(mainFrame, winUser, wx) def terminate(): diff --git a/source/monkeyPatches/__init__.py b/source/monkeyPatches/__init__.py new file mode 100644 index 00000000000..b970d836b73 --- /dev/null +++ b/source/monkeyPatches/__init__.py @@ -0,0 +1,19 @@ +# A part of NonVisual Desktop Access (NVDA) +# Copyright (C) 2021 NV Access Limited +# This file is covered by the GNU General Public License. +# See the file COPYING for more details. + +from . import wxMonkeyPatches + + +applyWxMonkeyPatches = wxMonkeyPatches.apply + + +def applyMonkeyPatches(): + # Apply several monkey patches to comtypes + # F401 - imported but unused: Patches are applied during import + from . import comtypesMonkeyPatches # noqa: F401 + + # Apply patches to Enum, prevent cyclic references on ValueError during construction + from . import enumPatches + enumPatches.replace__new__() diff --git a/source/comtypesMonkeyPatches.py b/source/monkeyPatches/comtypesMonkeyPatches.py similarity index 100% rename from source/comtypesMonkeyPatches.py rename to source/monkeyPatches/comtypesMonkeyPatches.py diff --git a/source/monkeyPatches/enumPatches.py b/source/monkeyPatches/enumPatches.py new file mode 100644 index 00000000000..6abee38b6c7 --- /dev/null +++ b/source/monkeyPatches/enumPatches.py @@ -0,0 +1,60 @@ +# A part of NonVisual Desktop Access (NVDA) +# Copyright (C) 2021 NV Access Limited +# This file is covered by the GNU General Public License. +# See the file COPYING for more details. + + +def replace__new__(): + import enum + # prevent cyclic references on ValueError during construction + enum.Enum.__new__ = _replacement__new__ + + +def _replacement__new__(cls, value): + """ Copied from python standard library enum.py class Enum. + Prevent cyclic references on ValueError during construction. + Local variable exc must be deleted, otherwise: + - ref to exc held by the frame + - ref to traceback held by exc + - ref to frame held by traceback + """ + # all enum instances are actually created during class construction + # without calling this method; this method is called by the metaclass' + # __call__ (i.e. Color(3) ), and by pickle + if type(value) is cls: + # For lookups like Color(Color.RED) + return value + # by-value search for a matching enum member + # see if it's in the reverse mapping (for hashable values) + try: + return cls._value2member_map_[value] + except KeyError: + # Not found, no need to do long O(n) search + pass + except TypeError: + # not there, now do long search -- O(n) behavior + for member in cls._member_map_.values(): + if member._value_ == value: + return member + # still not found -- try _missing_ hook + try: + result = cls._missing_(value) + except Exception as e: + e.__context__ = ValueError("%r is not a valid %s" % (value, cls.__name__)) + raise e + + if isinstance(result, cls): + return result + + with ValueError( + "%r is not a valid %s" % (value, cls.__name__) + ) as ve_exc: + if result is None: + raise ve_exc + + te_exc = TypeError( + 'error in %s._missing_: returned %r instead of None or a valid member' + % (cls.__name__, result) + ) + te_exc.__context__ = ve_exc + raise te_exc diff --git a/source/monkeyPatches/wxMonkeyPatches.py b/source/monkeyPatches/wxMonkeyPatches.py new file mode 100644 index 00000000000..691edf9e41f --- /dev/null +++ b/source/monkeyPatches/wxMonkeyPatches.py @@ -0,0 +1,23 @@ +# A part of NonVisual Desktop Access (NVDA) +# Copyright (C) 2021 NV Access Limited +# This file is covered by the GNU General Public License. +# See the file COPYING for more details. + + +def apply(mainFrame, winUser, wx): + # In wxPython >= 4.1, + # wx.CallAfter no longer executes callbacks while NVDA's main thread is within a popup menu or message box. + # To work around this, + # MonkeyPatch wx.CallAfter to + # post a WM_NULL message to our top-level window after calling the original CallAfter, + # which causes wx's event loop to wake up enough to execute the callback. + old_wx_CallAfter = wx.CallAfter + + def wx_CallAfter_wrapper(func, *args, **kwargs): + old_wx_CallAfter(func, *args, **kwargs) + # mainFrame may be None as NVDA could be terminating. + topHandle = mainFrame.Handle if mainFrame else None + if topHandle: + winUser.PostMessage(topHandle, winUser.WM_NULL, 0, 0) + + wx.CallAfter = wx_CallAfter_wrapper diff --git a/source/nvda.pyw b/source/nvda.pyw index 2a170591d61..455e1b91fea 100755 --- a/source/nvda.pyw +++ b/source/nvda.pyw @@ -17,6 +17,9 @@ import typing import globalVars import ctypes from ctypes import wintypes +import monkeyPatches + +monkeyPatches.applyMonkeyPatches() #: logger to use before the true NVDA log is initialised. # Ideally, all logging would be captured by the NVDA log, however this would introduce contention