From 7a6d70b65429f82587d55fcac89cc9d2683d3035 Mon Sep 17 00:00:00 2001 From: Michael Curran Date: Sat, 22 Jun 2019 17:39:21 +1000 Subject: [PATCH 1/2] As we can no longer hook the constructor of COMError, convert COMError to CallCancelled by hooking WinFunctionType's __call__ in the context of comtypes. --- source/baseObject.py | 3 +++ source/comtypesMonkeyPatches.py | 41 +++++++++++++++++++++++++++++++++ source/core.py | 17 ++++++++++---- source/logHandler.py | 5 ++-- source/watchdog.py | 15 +----------- 5 files changed, 59 insertions(+), 22 deletions(-) diff --git a/source/baseObject.py b/source/baseObject.py index 5819f05f34d..2605ecbdb8e 100755 --- a/source/baseObject.py +++ b/source/baseObject.py @@ -140,9 +140,12 @@ def __new__(cls, *args, **kwargs): def _getPropertyViaCache(self,getterMethod=None): if not getterMethod: raise ValueError("getterMethod is None") + missing=False try: val=self._propertyCache[getterMethod] except KeyError: + missing=True + if missing: val=getterMethod(self) self._propertyCache[getterMethod]=val return val diff --git a/source/comtypesMonkeyPatches.py b/source/comtypesMonkeyPatches.py index 57d84c06416..542e344b237 100644 --- a/source/comtypesMonkeyPatches.py +++ b/source/comtypesMonkeyPatches.py @@ -3,6 +3,47 @@ #This file is covered by the GNU General Public License. #See the file COPYING for more details. +import ctypes +import _ctypes + +# A version of ctypes.WINFUNCTYPE +# that produces a WinFunctionType class whos instance will convert COMError into a CallCancelled exception when called as a function. +old_WINFUNCTYPE=ctypes.WINFUNCTYPE +def new_WINFUNCTYPE(restype,*argtypes,**kwargs): + cls=old_WINFUNCTYPE(restype,*argtypes,**kwargs) + class WinFunctionType(cls): + # We must manually pull the manditory class variables from the super class, + # as the metaclass of _ctypes.CFuncPtr seems to expect these on the outermost subclass. + _argtypes_=cls._argtypes_ + _restype_=cls._restype_ + _flags_=cls._flags_ + def __call__(self,*args,**kwargs): + try: + return super().__call__(*args,**kwargs) + except _ctypes.COMError as e: + from core import CallCancelled, RPC_E_CALL_CANCELED + if e.args[0]==RPC_E_CALL_CANCELED: + # As this is a cancelled COM call, + # raise CallCancelled instead of the original COMError. + # Also raising from None gives a cleaner traceback, + # Hiding the fact we were already in an except block. + raise CallCancelled("COM call cancelled") from None + # Otherwise, just continue the original COMError exception up the stack. + raise + return WinFunctionType + +# While importing comtypes, +# Replace WINFUNCTYPE in ctypes with our own version, +# So that comtypes will use this in all its COM method calls. +# As comtypes imports WINFUNCTYPE from ctypes by name, +# We only need to replace it for the duration of importing comtypes, +# as it will then have it for ever. +ctypes.WINFUNCTYPE=new_WINFUNCTYPE +try: + import comtypes +finally: + ctypes.WINFUNCTYPE=old_WINFUNCTYPE + from logHandler import log from comtypes import COMError diff --git a/source/core.py b/source/core.py index 608fbc1dadc..c0d5a81c62d 100644 --- a/source/core.py +++ b/source/core.py @@ -7,16 +7,23 @@ """NVDA core""" -# Do this first to initialise comtypes.client.gen_dir and the comtypes.gen search path. +RPC_E_CALL_CANCELED = -2147418110 + +class CallCancelled(Exception): + """Raised when a call is cancelled. + """ + +# Apply several monky patches to comtypes +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 import comtypes.client -# Append our comInterfaces directory to the comtypes.gen search path. import comtypes.gen import comInterfaces comtypes.gen.__path__.append(comInterfaces.__path__[0]) -#Apply several monky patches to comtypes -import comtypesMonkeyPatches - import sys import winVersion import threading diff --git a/source/logHandler.py b/source/logHandler.py index 833b444e0f7..74d3003ed71 100755 --- a/source/logHandler.py +++ b/source/logHandler.py @@ -167,8 +167,7 @@ def exception(self, msg="", exc_info=True, **kwargs): However, certain exceptions which aren't considered errors (or aren't errors that we can fix) are expected and will therefore be logged at a lower level. """ import comtypes - import watchdog - from watchdog import RPC_E_CALL_CANCELED + from core import CallCancelled, RPC_E_CALL_CANCELED if exc_info is True: exc_info = sys.exc_info() @@ -176,7 +175,7 @@ def exception(self, msg="", exc_info=True, **kwargs): if ( (isinstance(exc, WindowsError) and exc.winerror in (ERROR_INVALID_WINDOW_HANDLE, ERROR_TIMEOUT, RPC_S_SERVER_UNAVAILABLE, RPC_S_CALL_FAILED_DNE, EPT_S_NOT_REGISTERED, RPC_E_CALL_CANCELED)) or (isinstance(exc, comtypes.COMError) and (exc.hresult in (E_ACCESSDENIED, CO_E_OBJNOTCONNECTED, EVENT_E_ALL_SUBSCRIBERS_FAILED, RPC_E_CALL_REJECTED, RPC_E_CALL_CANCELED, RPC_E_DISCONNECTED) or exc.hresult & 0xFFFF == RPC_S_SERVER_UNAVAILABLE)) - or isinstance(exc, watchdog.CallCancelled) + or isinstance(exc, CallCancelled) ): level = self.DEBUGWARNING else: diff --git a/source/watchdog.py b/source/watchdog.py index b554799cf8e..bdaa45ca9d2 100644 --- a/source/watchdog.py +++ b/source/watchdog.py @@ -19,6 +19,7 @@ from logHandler import log import globalVars import core +from core import CallCancelled import NVDAHelper #settings @@ -46,10 +47,6 @@ _watcherThread=None _cancelCallEvent = None -class CallCancelled(Exception): - """Raised when a call is cancelled. - """ - def alive(): """Inform the watchdog that the core is alive. """ @@ -196,13 +193,6 @@ def sendMessageCallCanceller(frame, event, arg): raise CallCancelled sys.setprofile(sendMessageCallCanceller) -RPC_E_CALL_CANCELED = -2147418110 -_orig_COMError_init = comtypes.COMError.__init__ -def _COMError_init(self, hresult, text, details): - if hresult == RPC_E_CALL_CANCELED: - raise CallCancelled - _orig_COMError_init(self, hresult, text, details) - def initialize(): """Initialize the watchdog. """ @@ -218,8 +208,6 @@ def initialize(): "cancelCallEvent") # Handle cancelled SendMessage calls. NVDAHelper._setDllFuncPointer(NVDAHelper.localLib, "_notifySendMessageCancelled", _notifySendMessageCancelled) - # Monkey patch comtypes to specially handle cancelled COM calls. - comtypes.COMError.__init__ = _COMError_init _watcherThread=threading.Thread(target=_watcher) alive() _watcherThread.start() @@ -232,7 +220,6 @@ def terminate(): return isRunning=False oledll.ole32.CoDisableCallCancellation(None) - comtypes.COMError.__init__ = _orig_COMError_init # Wake up the watcher so it knows to finish. windll.kernel32.SetWaitableTimer(_coreDeadTimer, ctypes.byref(ctypes.wintypes.LARGE_INTEGER(0)), From 5a50b01b60943076e4948d873cee7eed37d88324 Mon Sep 17 00:00:00 2001 From: Michael Curran Date: Mon, 24 Jun 2019 16:39:50 +1000 Subject: [PATCH 2/2] comtypesMonkeyPatches: fix typos in comments. --- source/comtypesMonkeyPatches.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/source/comtypesMonkeyPatches.py b/source/comtypesMonkeyPatches.py index 542e344b237..c5031c13f6c 100644 --- a/source/comtypesMonkeyPatches.py +++ b/source/comtypesMonkeyPatches.py @@ -7,12 +7,12 @@ import _ctypes # A version of ctypes.WINFUNCTYPE -# that produces a WinFunctionType class whos instance will convert COMError into a CallCancelled exception when called as a function. +# that produces a WinFunctionType class whose instance will convert COMError into a CallCancelled exception when called as a function. old_WINFUNCTYPE=ctypes.WINFUNCTYPE def new_WINFUNCTYPE(restype,*argtypes,**kwargs): cls=old_WINFUNCTYPE(restype,*argtypes,**kwargs) class WinFunctionType(cls): - # We must manually pull the manditory class variables from the super class, + # We must manually pull the mandatory class variables from the super class, # as the metaclass of _ctypes.CFuncPtr seems to expect these on the outermost subclass. _argtypes_=cls._argtypes_ _restype_=cls._restype_