From 432de1131754c3ac2053260cbdc33570f290af29 Mon Sep 17 00:00:00 2001 From: Leonard de Ruijter Date: Wed, 27 Sep 2023 22:07:49 +0200 Subject: [PATCH 01/12] Bump ruff and use tab formatting again --- .pre-commit-config.yaml | 5 +++-- pyproject.toml | 3 +++ 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 15e5643..e7ba0b2 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -7,7 +7,8 @@ repos: - id: check-yaml - repo: https://github.com/astral-sh/ruff-pre-commit # Ruff version. - rev: v0.0.289 + rev: v0.0.291 hooks: - id: ruff - args: [--fix, --exit-non-zero-on-fix] \ No newline at end of file + args: [--fix, --exit-non-zero-on-fix] + - id: ruff-format diff --git a/pyproject.toml b/pyproject.toml index e873813..8d7e612 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -19,3 +19,6 @@ select = [ [tool.ruff.mccabe] max-complexity = 15 +[tool.ruff.format] +quote-style = "double" +indent-style = "tab" From a075e420eb87f7342fd3835d2d5ef543533c2d09 Mon Sep 17 00:00:00 2001 From: Leonard de Ruijter Date: Wed, 27 Sep 2023 22:12:12 +0200 Subject: [PATCH 02/12] Back to tabs --- addon/brailleDisplayDrivers/remote.py | 158 ++-- addon/globalPlugins/rdAccess/__init__.py | 684 ++++++++-------- .../rdAccess/directoryChanges.py | 238 +++--- .../rdAccess/handlers/_remoteHandler.py | 314 +++---- .../rdAccess/handlers/remoteBrailleHandler.py | 198 ++--- .../rdAccess/handlers/remoteSpeechHandler.py | 214 ++--- addon/globalPlugins/rdAccess/objects.py | 28 +- .../rdAccess/secureDesktopHandling.py | 52 +- addon/globalPlugins/rdAccess/settingsPanel.py | 276 +++---- addon/globalPlugins/rdAccess/synthDetect.py | 178 ++-- addon/installTasks.py | 66 +- addon/lib/configuration.py | 72 +- addon/lib/detection.py | 58 +- addon/lib/driver/__init__.py | 368 ++++----- addon/lib/driver/settingsAccessor.py | 200 +++-- addon/lib/inputTime.py | 26 +- addon/lib/ioThreadEx.py | 176 ++-- addon/lib/namedPipe.py | 506 ++++++------ addon/lib/protocol/__init__.py | 766 +++++++++--------- addon/lib/protocol/braille.py | 48 +- addon/lib/protocol/speech.py | 16 +- addon/lib/rdPipe.py | 134 +-- addon/lib/secureDesktop.py | 4 +- addon/lib/wtsVirtualChannel.py | 260 +++--- addon/synthDrivers/remote.py | 250 +++--- buildVars.py | 86 +- pyproject.toml | 4 +- site_scons/site_tools/gettexttool/__init__.py | 52 +- 28 files changed, 2710 insertions(+), 2722 deletions(-) diff --git a/addon/brailleDisplayDrivers/remote.py b/addon/brailleDisplayDrivers/remote.py index 4065e9a..b3b12e0 100644 --- a/addon/brailleDisplayDrivers/remote.py +++ b/addon/brailleDisplayDrivers/remote.py @@ -11,89 +11,89 @@ from logHandler import log if typing.TYPE_CHECKING: - from ..lib import detection, driver, protocol + from ..lib import detection, driver, protocol else: - addon: addonHandler.Addon = addonHandler.getCodeAddon() - detection = addon.loadModule("lib.detection") - driver = addon.loadModule("lib.driver") - protocol = addon.loadModule("lib.protocol") + addon: addonHandler.Addon = addonHandler.getCodeAddon() + detection = addon.loadModule("lib.detection") + driver = addon.loadModule("lib.driver") + protocol = addon.loadModule("lib.protocol") class RemoteBrailleDisplayDriver(driver.RemoteDriver, braille.BrailleDisplayDriver): - # Translators: Name for a remote braille display. - description = _("Remote Braille") - isThreadSafe = True - supportsAutomaticDetection = True - driverType = protocol.DriverType.BRAILLE - _requiredAttributesOnInit = driver.RemoteDriver._requiredAttributesOnInit.union( - {protocol.BrailleAttribute.NUM_CELLS} - ) - - @classmethod - def registerAutomaticDetection(cls, driverRegistrar): - driverRegistrar.addDeviceScanner(detection.bgScanRD, moveToStart=True) - - def _getModifierGestures(self, model: typing.Optional[str] = None): - """Hacky override that throws an instance at the underlying class method. - If we don't do this, the method can't acces the gesture map at the instance level. - """ - return super()._getModifierGestures.__func__(self, model) - - def _handleRemoteDisconnect(self): - # Raise an exception because handleDisplayUnavailable expects one - try: - raise RuntimeError("remote client disconnected") - except RuntimeError: - braille.handler.handleDisplayUnavailable() - - @protocol.attributeReceiver(protocol.BrailleAttribute.NUM_CELLS, defaultValue=0) - def _incoming_numCells(self, payload: bytes) -> int: - assert len(payload) == 1 - return ord(payload) - - def _get_numCells(self) -> int: - attribute = protocol.BrailleAttribute.NUM_CELLS - try: - value = self._attributeValueProcessor.getValue(attribute, fallBackToDefault=False) - except KeyError: - value = self._attributeValueProcessor._getDefaultValue(attribute) - self.requestRemoteAttribute(attribute) - return value - - @protocol.attributeReceiver(protocol.BrailleAttribute.GESTURE_MAP) - def _incoming_gestureMapUpdate(self, payload: bytes) -> inputCore.GlobalGestureMap: - assert len(payload) > 0 - return self._unpickle(payload) - - @_incoming_gestureMapUpdate.defaultValueGetter - def _default_gestureMap(self, attribute: protocol.AttributeT): - return inputCore.GlobalGestureMap() - - def _get_gestureMap(self) -> inputCore.GlobalGestureMap: - attribute = protocol.BrailleAttribute.GESTURE_MAP - try: - value = self._attributeValueProcessor.getValue(attribute, fallBackToDefault=False) - except KeyError: - value = self._attributeValueProcessor._getDefaultValue(attribute) - self.requestRemoteAttribute(attribute) - return value - - @protocol.commandHandler(protocol.BrailleCommand.EXECUTE_GESTURE) - def _command_executeGesture(self, payload: bytes): - assert len(payload) > 0 - gesture = self._unpickle(payload) - try: - inputCore.manager.executeGesture(gesture) - except inputCore.NoInputGestureAction: - log.error("Unexpected NoInputGestureAction", exc_info=True) - - def display(self, cells: List[int]): - # cells will already be padded up to numCells. - assert len(cells) == self.numCells - if len(cells) == 0: - return - arg = bytes(cells) - self.writeMessage(protocol.BrailleCommand.DISPLAY, arg) + # Translators: Name for a remote braille display. + description = _("Remote Braille") + isThreadSafe = True + supportsAutomaticDetection = True + driverType = protocol.DriverType.BRAILLE + _requiredAttributesOnInit = driver.RemoteDriver._requiredAttributesOnInit.union( + {protocol.BrailleAttribute.NUM_CELLS} + ) + + @classmethod + def registerAutomaticDetection(cls, driverRegistrar): + driverRegistrar.addDeviceScanner(detection.bgScanRD, moveToStart=True) + + def _getModifierGestures(self, model: typing.Optional[str] = None): + """Hacky override that throws an instance at the underlying class method. + If we don't do this, the method can't acces the gesture map at the instance level. + """ + return super()._getModifierGestures.__func__(self, model) + + def _handleRemoteDisconnect(self): + # Raise an exception because handleDisplayUnavailable expects one + try: + raise RuntimeError("remote client disconnected") + except RuntimeError: + braille.handler.handleDisplayUnavailable() + + @protocol.attributeReceiver(protocol.BrailleAttribute.NUM_CELLS, defaultValue=0) + def _incoming_numCells(self, payload: bytes) -> int: + assert len(payload) == 1 + return ord(payload) + + def _get_numCells(self) -> int: + attribute = protocol.BrailleAttribute.NUM_CELLS + try: + value = self._attributeValueProcessor.getValue(attribute, fallBackToDefault=False) + except KeyError: + value = self._attributeValueProcessor._getDefaultValue(attribute) + self.requestRemoteAttribute(attribute) + return value + + @protocol.attributeReceiver(protocol.BrailleAttribute.GESTURE_MAP) + def _incoming_gestureMapUpdate(self, payload: bytes) -> inputCore.GlobalGestureMap: + assert len(payload) > 0 + return self._unpickle(payload) + + @_incoming_gestureMapUpdate.defaultValueGetter + def _default_gestureMap(self, attribute: protocol.AttributeT): + return inputCore.GlobalGestureMap() + + def _get_gestureMap(self) -> inputCore.GlobalGestureMap: + attribute = protocol.BrailleAttribute.GESTURE_MAP + try: + value = self._attributeValueProcessor.getValue(attribute, fallBackToDefault=False) + except KeyError: + value = self._attributeValueProcessor._getDefaultValue(attribute) + self.requestRemoteAttribute(attribute) + return value + + @protocol.commandHandler(protocol.BrailleCommand.EXECUTE_GESTURE) + def _command_executeGesture(self, payload: bytes): + assert len(payload) > 0 + gesture = self._unpickle(payload) + try: + inputCore.manager.executeGesture(gesture) + except inputCore.NoInputGestureAction: + log.error("Unexpected NoInputGestureAction", exc_info=True) + + def display(self, cells: List[int]): + # cells will already be padded up to numCells. + assert len(cells) == self.numCells + if len(cells) == 0: + return + arg = bytes(cells) + self.writeMessage(protocol.BrailleCommand.DISPLAY, arg) BrailleDisplayDriver = RemoteBrailleDisplayDriver diff --git a/addon/globalPlugins/rdAccess/__init__.py b/addon/globalPlugins/rdAccess/__init__.py index 24120b6..a6517d7 100644 --- a/addon/globalPlugins/rdAccess/__init__.py +++ b/addon/globalPlugins/rdAccess/__init__.py @@ -25,355 +25,355 @@ from .synthDetect import _SynthDetector if typing.TYPE_CHECKING: - from ...lib import ( - configuration, - detection, - ioThreadEx, - namedPipe, - protocol, - rdPipe, - secureDesktop, - ) + from ...lib import ( + configuration, + detection, + ioThreadEx, + namedPipe, + protocol, + rdPipe, + secureDesktop, + ) else: - addon: addonHandler.Addon = addonHandler.getCodeAddon() - configuration = addon.loadModule("lib.configuration") - detection = addon.loadModule("lib.detection") - ioThreadEx = addon.loadModule("lib.ioThreadEx") - namedPipe = addon.loadModule("lib.namedPipe") - protocol = addon.loadModule("lib.protocol") - rdPipe = addon.loadModule("lib.rdPipe") - secureDesktop = addon.loadModule("lib.secureDesktop") + addon: addonHandler.Addon = addonHandler.getCodeAddon() + configuration = addon.loadModule("lib.configuration") + detection = addon.loadModule("lib.detection") + ioThreadEx = addon.loadModule("lib.ioThreadEx") + namedPipe = addon.loadModule("lib.namedPipe") + protocol = addon.loadModule("lib.protocol") + rdPipe = addon.loadModule("lib.rdPipe") + secureDesktop = addon.loadModule("lib.secureDesktop") supportsBrailleAutoDetectRegistration = ( - versionInfo.version_year, - versionInfo.version_major, + versionInfo.version_year, + versionInfo.version_major, ) >= (2023, 3) class RDGlobalPlugin(globalPluginHandler.GlobalPlugin): - _synthDetector: typing.Optional[_SynthDetector] = None - _ioThread: typing.Optional[ioThreadEx.IoThreadEx] = None - - def chooseNVDAObjectOverlayClasses(self, obj: NVDAObject, clsList: List[Type[NVDAObject]]): - findExtraOverlayClasses(obj, clsList) - - @classmethod - def _updateRegistryForRdPipe(cls, install: bool, rdp: bool, citrix: bool) -> bool: - if isRunningOnSecureDesktop(): - return False - if citrix and not rdPipe.isCitrixSupported(): - citrix = False - if not rdp and not citrix: - return False - if rdPipe.DEFAULT_ARCHITECTURE == rdPipe.Architecture.X86: - return rdPipe.dllInstall( - install=install, - comServer=True, - rdp=rdp, - citrix=citrix, - ) - else: - res = False - if rdp: - if rdPipe.dllInstall( - install=install, - comServer=True, - rdp=True, - citrix=False, - ): - res = True - if citrix: - if rdPipe.dllInstall( - install=install, - comServer=True, - rdp=False, - citrix=True, - architecture=rdPipe.Architecture.X86, - ): - res = True - return res - - @classmethod - def _registerRdPipeInRegistry(cls): - persistent = config.isInstalledCopy() and configuration.getPersistentRegistration() - rdp = configuration.getRemoteDesktopSupport() - citrix = configuration.getCitrixSupport() - if cls._updateRegistryForRdPipe(True, rdp, citrix) and not persistent: - atexit.register(cls._unregisterRdPipeFromRegistry) - - def initializeOperatingModeServer(self): - if configuration.getRecoverRemoteSpeech(): - self._synthDetector = _SynthDetector() - if not supportsBrailleAutoDetectRegistration: - detection.register() - self._triggerBackgroundDetectRescan( - rescanBraille=not supportsBrailleAutoDetectRegistration, force=True - ) - if not isRunningOnSecureDesktop(): - post_sessionLockStateChanged.register(self._handleLockStateChanged) - - def initializeOperatingModeCommonClient(self): - if isRunningOnSecureDesktop(): - return - self._ioThread = ioThreadEx.IoThreadEx() - self._ioThread.start() - - def initializeOperatingModeRdClient(self): - if isRunningOnSecureDesktop(): - return - self._registerRdPipeInRegistry() - self._handlers: Dict[str, handlers.RemoteHandler] = {} - self._pipeWatcher = directoryChanges.DirectoryWatcher( - namedPipe.PIPE_DIRECTORY, - directoryChanges.FileNotifyFilter.FILE_NOTIFY_CHANGE_FILE_NAME, - ) - self._pipeWatcher.directoryChanged.register(self._handleNewPipe) - self._pipeWatcher.start() - self._initializeExistingPipes() - - def initializeOperatingModeSecureDesktop(self): - if isRunningOnSecureDesktop(): - return - secureDesktop.post_secureDesktopStateChange.register(self._handleSecureDesktop) - self._sdHandler: typing.Optional[SecureDesktopHandler] = None - - def __init__(self): - super().__init__() - configuration.initializeConfig() - configuredOperatingMode = configuration.getOperatingMode() - if ( - configuredOperatingMode & configuration.OperatingMode.CLIENT - or configuredOperatingMode & configuration.OperatingMode.SECURE_DESKTOP - ): - self.initializeOperatingModeCommonClient() - if configuredOperatingMode & configuration.OperatingMode.CLIENT: - self.initializeOperatingModeRdClient() - if configuredOperatingMode & configuration.OperatingMode.SECURE_DESKTOP: - self.initializeOperatingModeSecureDesktop() - if configuredOperatingMode & configuration.OperatingMode.SERVER or ( - configuredOperatingMode & configuration.OperatingMode.SECURE_DESKTOP - and isRunningOnSecureDesktop() - ): - self.initializeOperatingModeServer() - if isRunningOnSecureDesktop(): - return - config.post_configProfileSwitch.register(self._handlePostConfigProfileSwitch) - gui.settingsDialogs.NVDASettingsDialog.categoryClasses.append( - settingsPanel.RemoteDesktopSettingsPanel - ) - settingsPanel.RemoteDesktopSettingsPanel.post_onSave.register(self._handlePostConfigProfileSwitch) - - def _initializeExistingPipes(self): - for match in namedPipe.getRdPipeNamedPipes(): - self._handleNewPipe(directoryChanges.FileNotifyInformationAction.FILE_ACTION_ADDED, match) - - def _handleNewPipe(self, action: directoryChanges.FileNotifyInformationAction, fileName: str): - if not fnmatch(fileName, namedPipe.RD_PIPE_GLOB_PATTERN): - return - if action == directoryChanges.FileNotifyInformationAction.FILE_ACTION_ADDED: - if fnmatch( - fileName, - namedPipe.RD_PIPE_GLOB_PATTERN.replace("*", f"{protocol.DriverType.BRAILLE.name}*"), - ): - HandlerClass = handlers.RemoteBrailleHandler - elif fnmatch( - fileName, - namedPipe.RD_PIPE_GLOB_PATTERN.replace("*", f"{protocol.DriverType.SPEECH.name}*"), - ): - HandlerClass = handlers.RemoteSpeechHandler - else: - raise RuntimeError(f"Unknown named pipe: {fileName}") - log.debug(f"Creating {HandlerClass.__name__} for {fileName!r}") - handler = HandlerClass(self._ioThread, fileName) - handler.decide_remoteDisconnect.register(self._handleRemoteDisconnect) - handler.event_gainFocus(api.getFocusObject()) - self._handlers[fileName] = handler - elif action == directoryChanges.FileNotifyInformationAction.FILE_ACTION_REMOVED: - log.debug(f"Pipe with name {fileName!r} removed") - handler = self._handlers.pop(fileName, None) - if handler: - log.debug(f"Terminating handler {handler!r} for Pipe with name {fileName!r}") - handler.decide_remoteDisconnect.unregister(self._handleRemoteDisconnect) - handler.terminate() - - def terminateOperatingModeServer(self): - if not isRunningOnSecureDesktop(): - post_sessionLockStateChanged.unregister(self._handleLockStateChanged) - if not supportsBrailleAutoDetectRegistration: - detection.unregister() - if self._synthDetector: - self._synthDetector.terminate() - - def terminateOperatingModeRdClient(self): - if isRunningOnSecureDesktop(): - return - if self._pipeWatcher: - self._pipeWatcher.stop() - self._pipeWatcher = None - for handler in self._handlers.values(): - handler.terminate() - self._handlers.clear() - if not configuration.getPersistentRegistration(): - self._unregisterRdPipeFromRegistry() - - def terminateOperatingModeCommonClient(self): - if isRunningOnSecureDesktop(): - return - if self._ioThread: - self._ioThread.stop() - self._ioThread = None - - def terminateOperatingModeSecureDesktop(self): - if isRunningOnSecureDesktop(): - return - secureDesktop.post_secureDesktopStateChange.unregister(self._handleSecureDesktop) - self._handleSecureDesktop(False) - - @classmethod - def _unregisterRdPipeFromRegistry(cls) -> bool: - atexit.unregister(cls._unregisterRdPipeFromRegistry) - rdp = configuration.getRemoteDesktopSupport() - citrix = configuration.getCitrixSupport() - return cls._updateRegistryForRdPipe(False, rdp, citrix) - - def terminate(self): - try: - if not isRunningOnSecureDesktop(): - settingsPanel.RemoteDesktopSettingsPanel.post_onSave.unregister( - self._handlePostConfigProfileSwitch - ) - gui.settingsDialogs.NVDASettingsDialog.categoryClasses.remove( - settingsPanel.RemoteDesktopSettingsPanel - ) - config.post_configProfileSwitch.unregister(self._handlePostConfigProfileSwitch) - configuredOperatingMode = configuration.getOperatingMode() - if configuredOperatingMode & configuration.OperatingMode.SERVER or ( - configuredOperatingMode & configuration.OperatingMode.SECURE_DESKTOP - and isRunningOnSecureDesktop() - ): - self.terminateOperatingModeServer() - if configuredOperatingMode & configuration.OperatingMode.SECURE_DESKTOP: - self.terminateOperatingModeSecureDesktop() - if configuredOperatingMode & configuration.OperatingMode.CLIENT: - self.terminateOperatingModeRdClient() - if ( - configuredOperatingMode & configuration.OperatingMode.CLIENT - or configuredOperatingMode & configuration.OperatingMode.SECURE_DESKTOP - ): - self.terminateOperatingModeCommonClient() - finally: - super().terminate() - - def _handlePostConfigProfileSwitch(self): # NOQA: C901 - oldOperatingMode = configuration.getOperatingMode(True) - newOperatingMode = configuration.getOperatingMode(False) - oldClient = oldOperatingMode & configuration.OperatingMode.CLIENT - newClient = newOperatingMode & configuration.OperatingMode.CLIENT - oldServer = oldOperatingMode & configuration.OperatingMode.SERVER - newServer = newOperatingMode & configuration.OperatingMode.SERVER - oldSecureDesktop = oldOperatingMode & configuration.OperatingMode.SECURE_DESKTOP - newSecureDesktop = newOperatingMode & configuration.OperatingMode.SECURE_DESKTOP - oldSecureDesktopOrServer = (oldSecureDesktop and isRunningOnSecureDesktop()) or oldServer - newSecureDesktopOrServer = (newSecureDesktop and isRunningOnSecureDesktop()) or newServer - oldSecureDesktopOrClient = (oldSecureDesktop and not isRunningOnSecureDesktop()) or oldClient - newSecureDesktopOrClient = (newSecureDesktop and not isRunningOnSecureDesktop()) or newClient - if oldSecureDesktopOrServer and not newSecureDesktopOrServer: - self.terminateOperatingModeServer() - elif not oldSecureDesktopOrServer and newSecureDesktopOrServer: - self.initializeOperatingModeServer() - elif newSecureDesktopOrServer: - oldRecoverRemoteSpeech = configuration.getRecoverRemoteSpeech(True) - newRecoverRemoteSpeech = configuration.getRecoverRemoteSpeech(False) - if oldRecoverRemoteSpeech is not newRecoverRemoteSpeech: - if newRecoverRemoteSpeech: - self._synthDetector = _SynthDetector() - self._synthDetector._queueBgScan() - elif self._synthDetector: - self._synthDetector.terminate() - self._synthDetector = None - if oldSecureDesktop and not newSecureDesktop: - self.terminateOperatingModeSecureDesktop() - elif not oldSecureDesktop and newSecureDesktop: - self.initializeOperatingModeSecureDesktop() - if oldClient and not newClient: - self.terminateOperatingModeRdClient() - elif not oldClient and newClient: - self.initializeOperatingModeRdClient() - elif newClient: - oldDriverSettingsManagement = configuration.getDriverSettingsManagement(True) - newDriverSettingsManagement = configuration.getDriverSettingsManagement(False) - if oldDriverSettingsManagement is not newDriverSettingsManagement: - for handler in self._handlers.values(): - handler._handleDriverChanged(handler._driver) - oldRdp = configuration.getRemoteDesktopSupport(True) - newRdp = configuration.getRemoteDesktopSupport(False) - if oldRdp is not newRdp: - self._updateRegistryForRdPipe(newRdp, True, False) - oldCitrix = configuration.getCitrixSupport(True) - newCitrix = configuration.getCitrixSupport(False) - if oldCitrix is not newCitrix: - self._updateRegistryForRdPipe(newCitrix, False, True) - if oldSecureDesktopOrClient and not newSecureDesktopOrClient: - self.terminateOperatingModeCommonClient() - elif not oldSecureDesktopOrClient and newSecureDesktopOrClient: - self.initializeOperatingModeCommonClient() - configuration.updateConfigCache() - - def _handleLockStateChanged(self, isNowLocked): - if not isNowLocked: - self._triggerBackgroundDetectRescan(force=True) - - def _triggerBackgroundDetectRescan( - self, rescanSpeech: bool = True, rescanBraille: bool = True, force: bool = False - ): - if rescanSpeech and self._synthDetector: - self._synthDetector.rescan(force) - detector = braille.handler._detector - if rescanBraille and detector is not None: - detector.rescan( - usb=detector._detectUsb, - bluetooth=detector._detectBluetooth, - limitToDevices=detector._limitToDevices, - ) - - def _handleRemoteDisconnect(self, handler: handlers.RemoteHandler, error: int) -> bool: - if isinstance(WinError(error), BrokenPipeError): - handler.terminate() - if handler._dev.pipeName in self._handlers: - del self._handlers[handler._dev.pipeName] - return True - return False - - def event_gainFocus(self, obj, nextHandler): - configuredOperatingMode = configuration.getOperatingMode() - if not isRunningOnSecureDesktop(): - if configuredOperatingMode & configuration.OperatingMode.CLIENT: - for handler in self._handlers.values(): - try: - handler.event_gainFocus(obj) - except Exception: - log.error("Error calling event_gainFocus on handler", exc_info=True) - continue - if ( - configuredOperatingMode & configuration.OperatingMode.SECURE_DESKTOP - and not secureDesktop.hasSecureDesktopExtensionPoint - ): - from IAccessibleHandler import SecureDesktopNVDAObject - - if isinstance(obj, SecureDesktopNVDAObject): - secureDesktop.post_secureDesktopStateChange.notify(isSecureDesktop=True) - elif self._sdHandler: - secureDesktop.post_secureDesktopStateChange.notify(isSecureDesktop=False) - if configuredOperatingMode & configuration.OperatingMode.SERVER: - self._triggerBackgroundDetectRescan() - nextHandler() - - def _handleSecureDesktop(self, isSecureDesktop: bool): - if isSecureDesktop: - self._sdHandler = SecureDesktopHandler(self._ioThread) - elif self._sdHandler: - self._sdHandler.terminate() - self._sdHandler = None + _synthDetector: typing.Optional[_SynthDetector] = None + _ioThread: typing.Optional[ioThreadEx.IoThreadEx] = None + + def chooseNVDAObjectOverlayClasses(self, obj: NVDAObject, clsList: List[Type[NVDAObject]]): + findExtraOverlayClasses(obj, clsList) + + @classmethod + def _updateRegistryForRdPipe(cls, install: bool, rdp: bool, citrix: bool) -> bool: + if isRunningOnSecureDesktop(): + return False + if citrix and not rdPipe.isCitrixSupported(): + citrix = False + if not rdp and not citrix: + return False + if rdPipe.DEFAULT_ARCHITECTURE == rdPipe.Architecture.X86: + return rdPipe.dllInstall( + install=install, + comServer=True, + rdp=rdp, + citrix=citrix, + ) + else: + res = False + if rdp: + if rdPipe.dllInstall( + install=install, + comServer=True, + rdp=True, + citrix=False, + ): + res = True + if citrix: + if rdPipe.dllInstall( + install=install, + comServer=True, + rdp=False, + citrix=True, + architecture=rdPipe.Architecture.X86, + ): + res = True + return res + + @classmethod + def _registerRdPipeInRegistry(cls): + persistent = config.isInstalledCopy() and configuration.getPersistentRegistration() + rdp = configuration.getRemoteDesktopSupport() + citrix = configuration.getCitrixSupport() + if cls._updateRegistryForRdPipe(True, rdp, citrix) and not persistent: + atexit.register(cls._unregisterRdPipeFromRegistry) + + def initializeOperatingModeServer(self): + if configuration.getRecoverRemoteSpeech(): + self._synthDetector = _SynthDetector() + if not supportsBrailleAutoDetectRegistration: + detection.register() + self._triggerBackgroundDetectRescan( + rescanBraille=not supportsBrailleAutoDetectRegistration, force=True + ) + if not isRunningOnSecureDesktop(): + post_sessionLockStateChanged.register(self._handleLockStateChanged) + + def initializeOperatingModeCommonClient(self): + if isRunningOnSecureDesktop(): + return + self._ioThread = ioThreadEx.IoThreadEx() + self._ioThread.start() + + def initializeOperatingModeRdClient(self): + if isRunningOnSecureDesktop(): + return + self._registerRdPipeInRegistry() + self._handlers: Dict[str, handlers.RemoteHandler] = {} + self._pipeWatcher = directoryChanges.DirectoryWatcher( + namedPipe.PIPE_DIRECTORY, + directoryChanges.FileNotifyFilter.FILE_NOTIFY_CHANGE_FILE_NAME, + ) + self._pipeWatcher.directoryChanged.register(self._handleNewPipe) + self._pipeWatcher.start() + self._initializeExistingPipes() + + def initializeOperatingModeSecureDesktop(self): + if isRunningOnSecureDesktop(): + return + secureDesktop.post_secureDesktopStateChange.register(self._handleSecureDesktop) + self._sdHandler: typing.Optional[SecureDesktopHandler] = None + + def __init__(self): + super().__init__() + configuration.initializeConfig() + configuredOperatingMode = configuration.getOperatingMode() + if ( + configuredOperatingMode & configuration.OperatingMode.CLIENT + or configuredOperatingMode & configuration.OperatingMode.SECURE_DESKTOP + ): + self.initializeOperatingModeCommonClient() + if configuredOperatingMode & configuration.OperatingMode.CLIENT: + self.initializeOperatingModeRdClient() + if configuredOperatingMode & configuration.OperatingMode.SECURE_DESKTOP: + self.initializeOperatingModeSecureDesktop() + if configuredOperatingMode & configuration.OperatingMode.SERVER or ( + configuredOperatingMode & configuration.OperatingMode.SECURE_DESKTOP + and isRunningOnSecureDesktop() + ): + self.initializeOperatingModeServer() + if isRunningOnSecureDesktop(): + return + config.post_configProfileSwitch.register(self._handlePostConfigProfileSwitch) + gui.settingsDialogs.NVDASettingsDialog.categoryClasses.append( + settingsPanel.RemoteDesktopSettingsPanel + ) + settingsPanel.RemoteDesktopSettingsPanel.post_onSave.register(self._handlePostConfigProfileSwitch) + + def _initializeExistingPipes(self): + for match in namedPipe.getRdPipeNamedPipes(): + self._handleNewPipe(directoryChanges.FileNotifyInformationAction.FILE_ACTION_ADDED, match) + + def _handleNewPipe(self, action: directoryChanges.FileNotifyInformationAction, fileName: str): + if not fnmatch(fileName, namedPipe.RD_PIPE_GLOB_PATTERN): + return + if action == directoryChanges.FileNotifyInformationAction.FILE_ACTION_ADDED: + if fnmatch( + fileName, + namedPipe.RD_PIPE_GLOB_PATTERN.replace("*", f"{protocol.DriverType.BRAILLE.name}*"), + ): + HandlerClass = handlers.RemoteBrailleHandler + elif fnmatch( + fileName, + namedPipe.RD_PIPE_GLOB_PATTERN.replace("*", f"{protocol.DriverType.SPEECH.name}*"), + ): + HandlerClass = handlers.RemoteSpeechHandler + else: + raise RuntimeError(f"Unknown named pipe: {fileName}") + log.debug(f"Creating {HandlerClass.__name__} for {fileName!r}") + handler = HandlerClass(self._ioThread, fileName) + handler.decide_remoteDisconnect.register(self._handleRemoteDisconnect) + handler.event_gainFocus(api.getFocusObject()) + self._handlers[fileName] = handler + elif action == directoryChanges.FileNotifyInformationAction.FILE_ACTION_REMOVED: + log.debug(f"Pipe with name {fileName!r} removed") + handler = self._handlers.pop(fileName, None) + if handler: + log.debug(f"Terminating handler {handler!r} for Pipe with name {fileName!r}") + handler.decide_remoteDisconnect.unregister(self._handleRemoteDisconnect) + handler.terminate() + + def terminateOperatingModeServer(self): + if not isRunningOnSecureDesktop(): + post_sessionLockStateChanged.unregister(self._handleLockStateChanged) + if not supportsBrailleAutoDetectRegistration: + detection.unregister() + if self._synthDetector: + self._synthDetector.terminate() + + def terminateOperatingModeRdClient(self): + if isRunningOnSecureDesktop(): + return + if self._pipeWatcher: + self._pipeWatcher.stop() + self._pipeWatcher = None + for handler in self._handlers.values(): + handler.terminate() + self._handlers.clear() + if not configuration.getPersistentRegistration(): + self._unregisterRdPipeFromRegistry() + + def terminateOperatingModeCommonClient(self): + if isRunningOnSecureDesktop(): + return + if self._ioThread: + self._ioThread.stop() + self._ioThread = None + + def terminateOperatingModeSecureDesktop(self): + if isRunningOnSecureDesktop(): + return + secureDesktop.post_secureDesktopStateChange.unregister(self._handleSecureDesktop) + self._handleSecureDesktop(False) + + @classmethod + def _unregisterRdPipeFromRegistry(cls) -> bool: + atexit.unregister(cls._unregisterRdPipeFromRegistry) + rdp = configuration.getRemoteDesktopSupport() + citrix = configuration.getCitrixSupport() + return cls._updateRegistryForRdPipe(False, rdp, citrix) + + def terminate(self): + try: + if not isRunningOnSecureDesktop(): + settingsPanel.RemoteDesktopSettingsPanel.post_onSave.unregister( + self._handlePostConfigProfileSwitch + ) + gui.settingsDialogs.NVDASettingsDialog.categoryClasses.remove( + settingsPanel.RemoteDesktopSettingsPanel + ) + config.post_configProfileSwitch.unregister(self._handlePostConfigProfileSwitch) + configuredOperatingMode = configuration.getOperatingMode() + if configuredOperatingMode & configuration.OperatingMode.SERVER or ( + configuredOperatingMode & configuration.OperatingMode.SECURE_DESKTOP + and isRunningOnSecureDesktop() + ): + self.terminateOperatingModeServer() + if configuredOperatingMode & configuration.OperatingMode.SECURE_DESKTOP: + self.terminateOperatingModeSecureDesktop() + if configuredOperatingMode & configuration.OperatingMode.CLIENT: + self.terminateOperatingModeRdClient() + if ( + configuredOperatingMode & configuration.OperatingMode.CLIENT + or configuredOperatingMode & configuration.OperatingMode.SECURE_DESKTOP + ): + self.terminateOperatingModeCommonClient() + finally: + super().terminate() + + def _handlePostConfigProfileSwitch(self): # NOQA: C901 + oldOperatingMode = configuration.getOperatingMode(True) + newOperatingMode = configuration.getOperatingMode(False) + oldClient = oldOperatingMode & configuration.OperatingMode.CLIENT + newClient = newOperatingMode & configuration.OperatingMode.CLIENT + oldServer = oldOperatingMode & configuration.OperatingMode.SERVER + newServer = newOperatingMode & configuration.OperatingMode.SERVER + oldSecureDesktop = oldOperatingMode & configuration.OperatingMode.SECURE_DESKTOP + newSecureDesktop = newOperatingMode & configuration.OperatingMode.SECURE_DESKTOP + oldSecureDesktopOrServer = (oldSecureDesktop and isRunningOnSecureDesktop()) or oldServer + newSecureDesktopOrServer = (newSecureDesktop and isRunningOnSecureDesktop()) or newServer + oldSecureDesktopOrClient = (oldSecureDesktop and not isRunningOnSecureDesktop()) or oldClient + newSecureDesktopOrClient = (newSecureDesktop and not isRunningOnSecureDesktop()) or newClient + if oldSecureDesktopOrServer and not newSecureDesktopOrServer: + self.terminateOperatingModeServer() + elif not oldSecureDesktopOrServer and newSecureDesktopOrServer: + self.initializeOperatingModeServer() + elif newSecureDesktopOrServer: + oldRecoverRemoteSpeech = configuration.getRecoverRemoteSpeech(True) + newRecoverRemoteSpeech = configuration.getRecoverRemoteSpeech(False) + if oldRecoverRemoteSpeech is not newRecoverRemoteSpeech: + if newRecoverRemoteSpeech: + self._synthDetector = _SynthDetector() + self._synthDetector._queueBgScan() + elif self._synthDetector: + self._synthDetector.terminate() + self._synthDetector = None + if oldSecureDesktop and not newSecureDesktop: + self.terminateOperatingModeSecureDesktop() + elif not oldSecureDesktop and newSecureDesktop: + self.initializeOperatingModeSecureDesktop() + if oldClient and not newClient: + self.terminateOperatingModeRdClient() + elif not oldClient and newClient: + self.initializeOperatingModeRdClient() + elif newClient: + oldDriverSettingsManagement = configuration.getDriverSettingsManagement(True) + newDriverSettingsManagement = configuration.getDriverSettingsManagement(False) + if oldDriverSettingsManagement is not newDriverSettingsManagement: + for handler in self._handlers.values(): + handler._handleDriverChanged(handler._driver) + oldRdp = configuration.getRemoteDesktopSupport(True) + newRdp = configuration.getRemoteDesktopSupport(False) + if oldRdp is not newRdp: + self._updateRegistryForRdPipe(newRdp, True, False) + oldCitrix = configuration.getCitrixSupport(True) + newCitrix = configuration.getCitrixSupport(False) + if oldCitrix is not newCitrix: + self._updateRegistryForRdPipe(newCitrix, False, True) + if oldSecureDesktopOrClient and not newSecureDesktopOrClient: + self.terminateOperatingModeCommonClient() + elif not oldSecureDesktopOrClient and newSecureDesktopOrClient: + self.initializeOperatingModeCommonClient() + configuration.updateConfigCache() + + def _handleLockStateChanged(self, isNowLocked): + if not isNowLocked: + self._triggerBackgroundDetectRescan(force=True) + + def _triggerBackgroundDetectRescan( + self, rescanSpeech: bool = True, rescanBraille: bool = True, force: bool = False + ): + if rescanSpeech and self._synthDetector: + self._synthDetector.rescan(force) + detector = braille.handler._detector + if rescanBraille and detector is not None: + detector.rescan( + usb=detector._detectUsb, + bluetooth=detector._detectBluetooth, + limitToDevices=detector._limitToDevices, + ) + + def _handleRemoteDisconnect(self, handler: handlers.RemoteHandler, error: int) -> bool: + if isinstance(WinError(error), BrokenPipeError): + handler.terminate() + if handler._dev.pipeName in self._handlers: + del self._handlers[handler._dev.pipeName] + return True + return False + + def event_gainFocus(self, obj, nextHandler): + configuredOperatingMode = configuration.getOperatingMode() + if not isRunningOnSecureDesktop(): + if configuredOperatingMode & configuration.OperatingMode.CLIENT: + for handler in self._handlers.values(): + try: + handler.event_gainFocus(obj) + except Exception: + log.error("Error calling event_gainFocus on handler", exc_info=True) + continue + if ( + configuredOperatingMode & configuration.OperatingMode.SECURE_DESKTOP + and not secureDesktop.hasSecureDesktopExtensionPoint + ): + from IAccessibleHandler import SecureDesktopNVDAObject + + if isinstance(obj, SecureDesktopNVDAObject): + secureDesktop.post_secureDesktopStateChange.notify(isSecureDesktop=True) + elif self._sdHandler: + secureDesktop.post_secureDesktopStateChange.notify(isSecureDesktop=False) + if configuredOperatingMode & configuration.OperatingMode.SERVER: + self._triggerBackgroundDetectRescan() + nextHandler() + + def _handleSecureDesktop(self, isSecureDesktop: bool): + if isSecureDesktop: + self._sdHandler = SecureDesktopHandler(self._ioThread) + elif self._sdHandler: + self._sdHandler.terminate() + self._sdHandler = None GlobalPlugin = RDGlobalPlugin diff --git a/addon/globalPlugins/rdAccess/directoryChanges.py b/addon/globalPlugins/rdAccess/directoryChanges.py index c2755ac..27085c0 100644 --- a/addon/globalPlugins/rdAccess/directoryChanges.py +++ b/addon/globalPlugins/rdAccess/directoryChanges.py @@ -13,134 +13,134 @@ from extensionPoints import Action from hwIo.ioThread import IoThread from serial.win32 import ( - FILE_FLAG_OVERLAPPED, - INVALID_HANDLE_VALUE, - LPOVERLAPPED, - OVERLAPPED, - CreateFile, + FILE_FLAG_OVERLAPPED, + INVALID_HANDLE_VALUE, + LPOVERLAPPED, + OVERLAPPED, + CreateFile, ) FILE_FLAG_BACKUP_SEMANTICS = 0x02000000 class FileNotifyFilter(IntFlag): - FILE_NOTIFY_CHANGE_FILE_NAME = 0x1 - FILE_NOTIFY_CHANGE_DIR_NAME = 0x2 - FILE_NOTIFY_CHANGE_ATTRIBUTES = 0x4 - FILE_NOTIFY_CHANGE_SIZE = 0x8 - FILE_NOTIFY_CHANGE_LAST_WRITE = 0x10 - FILE_NOTIFY_CHANGE_LAST_ACCESS = 0x20 - FILE_NOTIFY_CHANGE_CREATION = 0x40 - FILE_NOTIFY_CHANGE_SECURITY = 0x100 + FILE_NOTIFY_CHANGE_FILE_NAME = 0x1 + FILE_NOTIFY_CHANGE_DIR_NAME = 0x2 + FILE_NOTIFY_CHANGE_ATTRIBUTES = 0x4 + FILE_NOTIFY_CHANGE_SIZE = 0x8 + FILE_NOTIFY_CHANGE_LAST_WRITE = 0x10 + FILE_NOTIFY_CHANGE_LAST_ACCESS = 0x20 + FILE_NOTIFY_CHANGE_CREATION = 0x40 + FILE_NOTIFY_CHANGE_SECURITY = 0x100 class FileNotifyInformationAction(IntEnum): - FILE_ACTION_ADDED = 0x1 - FILE_ACTION_REMOVED = 0x2 - FILE_ACTION_MODIFIED = 0x3 - FILE_ACTION_RENAMED_OLD_NAME = 0x4 - FILE_ACTION_RENAMED_NEW_NAME = 0x5 + FILE_ACTION_ADDED = 0x1 + FILE_ACTION_REMOVED = 0x2 + FILE_ACTION_MODIFIED = 0x3 + FILE_ACTION_RENAMED_OLD_NAME = 0x4 + FILE_ACTION_RENAMED_NEW_NAME = 0x5 class DirectoryWatcher(IoThread): - directoryChanged: Action - - def __init__( - self, - directory: str, - notifyFilter: FileNotifyFilter = FileNotifyFilter.FILE_NOTIFY_CHANGE_FILE_NAME, - watchSubtree: bool = False, - ): - super().__init__() - self._watching = False - self._directory = directory - self._notifyFilter = notifyFilter - self._watchSubtree = watchSubtree - self.directoryChanged = Action() - dirHandle = CreateFile( - directory, - winKernel.GENERIC_READ, - winKernel.FILE_SHARE_READ | winKernel.FILE_SHARE_WRITE | winKernel.FILE_SHARE_DELETE, - None, - winKernel.OPEN_EXISTING, - FILE_FLAG_OVERLAPPED | FILE_FLAG_BACKUP_SEMANTICS, - None, - ) - if dirHandle == INVALID_HANDLE_VALUE: - raise WinError() - self._dirHandle = dirHandle - self._buffer = create_string_buffer(4096) - self._overlapped = OVERLAPPED() - - def start(self): - if self._watching: - return - super().start() - self.queueAsApc(self._asyncWatch) - self._watching = True - - def stop(self): - if not self._watching: - return - self._watching = False - try: - if hasattr(self, "_dirHandle") and not windll.kernel32.CancelIoEx( - self._dirHandle, byref(self._overlapped) - ): - raise WinError() - finally: - super().stop() - - def __del__(self): - try: - self.stop() - finally: - if hasattr(self, "_dirHandle"): - winKernel.closeHandle(self._dirHandle) - - def _asyncWatch(self, param: int = 0): - res = windll.kernel32.ReadDirectoryChangesW( - self._dirHandle, - byref(self._buffer), - sizeof(self._buffer), - self._watchSubtree, - self._notifyFilter, - None, - byref(self._overlapped), - self.queueAsCompletionRoutine(self._ioDone, self._overlapped), - ) - if not res: - raise WinError() - - def _ioDone(self, error, numberOfBytes: int, overlapped: LPOVERLAPPED): - if not self._watching: - # We stopped watching - return - if numberOfBytes == 0: - raise RuntimeError("No bytes received, probably internal buffer overflow") - if error != 0: - raise WinError(error) - data = self._buffer.raw[:numberOfBytes] - self._asyncWatch() - queueHandler.queueFunction(queueHandler.eventQueue, self._handleChanges, data) - - def _handleChanges(self, data: bytes): - nextOffset = 0 - while True: - fileNameLength = int.from_bytes( - # fileNameLength is the third DWORD in the FILE_NOTIFY_INFORMATION struct - data[nextOffset + 8 : nextOffset + 12], - byteorder=sys.byteorder, - signed=False, - ) - format = f"@3I{fileNameLength}s" - nextOffset, action, fileNameLength, fileNameBytes = unpack( - format, data[nextOffset : nextOffset + calcsize(format)] - ) - fileName = fileNameBytes.decode("utf-16") - self.directoryChanged.notify( - action=FileNotifyInformationAction(action), - fileName=os.path.join(self._directory, fileName), - ) - if nextOffset == 0: - break + directoryChanged: Action + + def __init__( + self, + directory: str, + notifyFilter: FileNotifyFilter = FileNotifyFilter.FILE_NOTIFY_CHANGE_FILE_NAME, + watchSubtree: bool = False, + ): + super().__init__() + self._watching = False + self._directory = directory + self._notifyFilter = notifyFilter + self._watchSubtree = watchSubtree + self.directoryChanged = Action() + dirHandle = CreateFile( + directory, + winKernel.GENERIC_READ, + winKernel.FILE_SHARE_READ | winKernel.FILE_SHARE_WRITE | winKernel.FILE_SHARE_DELETE, + None, + winKernel.OPEN_EXISTING, + FILE_FLAG_OVERLAPPED | FILE_FLAG_BACKUP_SEMANTICS, + None, + ) + if dirHandle == INVALID_HANDLE_VALUE: + raise WinError() + self._dirHandle = dirHandle + self._buffer = create_string_buffer(4096) + self._overlapped = OVERLAPPED() + + def start(self): + if self._watching: + return + super().start() + self.queueAsApc(self._asyncWatch) + self._watching = True + + def stop(self): + if not self._watching: + return + self._watching = False + try: + if hasattr(self, "_dirHandle") and not windll.kernel32.CancelIoEx( + self._dirHandle, byref(self._overlapped) + ): + raise WinError() + finally: + super().stop() + + def __del__(self): + try: + self.stop() + finally: + if hasattr(self, "_dirHandle"): + winKernel.closeHandle(self._dirHandle) + + def _asyncWatch(self, param: int = 0): + res = windll.kernel32.ReadDirectoryChangesW( + self._dirHandle, + byref(self._buffer), + sizeof(self._buffer), + self._watchSubtree, + self._notifyFilter, + None, + byref(self._overlapped), + self.queueAsCompletionRoutine(self._ioDone, self._overlapped), + ) + if not res: + raise WinError() + + def _ioDone(self, error, numberOfBytes: int, overlapped: LPOVERLAPPED): + if not self._watching: + # We stopped watching + return + if numberOfBytes == 0: + raise RuntimeError("No bytes received, probably internal buffer overflow") + if error != 0: + raise WinError(error) + data = self._buffer.raw[:numberOfBytes] + self._asyncWatch() + queueHandler.queueFunction(queueHandler.eventQueue, self._handleChanges, data) + + def _handleChanges(self, data: bytes): + nextOffset = 0 + while True: + fileNameLength = int.from_bytes( + # fileNameLength is the third DWORD in the FILE_NOTIFY_INFORMATION struct + data[nextOffset + 8 : nextOffset + 12], + byteorder=sys.byteorder, + signed=False, + ) + format = f"@3I{fileNameLength}s" + nextOffset, action, fileNameLength, fileNameBytes = unpack( + format, data[nextOffset : nextOffset + calcsize(format)] + ) + fileName = fileNameBytes.decode("utf-16") + self.directoryChanged.notify( + action=FileNotifyInformationAction(action), + fileName=os.path.join(self._directory, fileName), + ) + if nextOffset == 0: + break diff --git a/addon/globalPlugins/rdAccess/handlers/_remoteHandler.py b/addon/globalPlugins/rdAccess/handlers/_remoteHandler.py index 4c519ba..58f0caa 100644 --- a/addon/globalPlugins/rdAccess/handlers/_remoteHandler.py +++ b/addon/globalPlugins/rdAccess/handlers/_remoteHandler.py @@ -14,167 +14,167 @@ from logHandler import log if typing.TYPE_CHECKING: - from ....lib import configuration, namedPipe, protocol + from ....lib import configuration, namedPipe, protocol else: - addon: addonHandler.Addon = addonHandler.getCodeAddon() - configuration = addon.loadModule("lib.configuration") - namedPipe = addon.loadModule("lib.namedPipe") - protocol = addon.loadModule("lib.protocol") + addon: addonHandler.Addon = addonHandler.getCodeAddon() + configuration = addon.loadModule("lib.configuration") + namedPipe = addon.loadModule("lib.namedPipe") + protocol = addon.loadModule("lib.protocol") MAX_TIME_SINCE_INPUT_FOR_REMOTE_SESSION_FOCUS = 200 class RemoteHandler(protocol.RemoteProtocolHandler): - _dev: namedPipe.NamedPipeBase - decide_remoteDisconnect: AccumulatingDecider - _isSecureDesktopHandler: bool = False - _remoteSessionhasFocus: typing.Optional[bool] = None - _driver: Driver - _abstract__driver = True - - def _get__driver(self) -> Driver: - raise NotImplementedError - - def __new__(cls, *args, **kwargs): - obj = super().__new__(cls, *args, **kwargs) - obj.decide_remoteDisconnect = AccumulatingDecider(defaultDecision=False) - return obj - - def initIo( - self, - ioThread: IoThread, - pipeName: str, - isNamedPipeClient: bool = True, - ): - if isNamedPipeClient: - self._dev = namedPipe.NamedPipeClient( - pipeName=pipeName, - onReceive=self._onReceive, - onReadError=self._onReadError, - ioThread=ioThread, - ) - else: - self._dev = namedPipe.NamedPipeServer( - pipeName=pipeName, - onReceive=self._onReceive, - onConnected=self._onConnected, - ioThreadEx=ioThread, - ) - - def __init__( - self, - ioThread: IoThread, - pipeName: str, - isNamedPipeClient: bool = True, - ): - self._isSecureDesktopHandler = not isNamedPipeClient - super().__init__() - self.initIo(ioThread, pipeName, isNamedPipeClient) - - if not self._isSecureDesktopHandler: - self._onConnected() - elif self._remoteSessionhasFocus is None: - self._remoteSessionhasFocus = False - - def _onConnected(self, connected: bool = True): - if self._isSecureDesktopHandler: - self._remoteSessionhasFocus = connected - if connected: - self._handleDriverChanged(self._driver) - - def event_gainFocus(self, obj): - if self._isSecureDesktopHandler: - return - # Invalidate the property cache to ensure that hasFocus will be fetched again. - # Normally, hasFocus should be cached since it is pretty expensive - # and should never try to fetch the time since input from the remote driver - # more than once per core cycle. - # However, if we don't clear the cache here, the braille handler won't be enabled correctly - # for the first focus outside the remote window. - self.invalidateCache() - self._remoteSessionhasFocus = None - - @protocol.attributeSender(protocol.GenericAttribute.SUPPORTED_SETTINGS) - def _outgoing_supportedSettings(self, settings=None) -> bytes: - if not configuration.getDriverSettingsManagement(): - return self._pickle([]) - if settings is None: - settings = self._driver.supportedSettings - return self._pickle(settings) - - @protocol.attributeSender(b"available*s") - def _outgoing_availableSettingValues(self, attribute: protocol.AttributeT) -> bytes: - if not configuration.getDriverSettingsManagement(): - return self._pickle({}) - name = attribute.decode("ASCII") - return self._pickle(getattr(self._driver, name)) - - @protocol.attributeReceiver(protocol.SETTING_ATTRIBUTE_PREFIX + b"*") - def _incoming_setting(self, attribute: protocol.AttributeT, payLoad: bytes): - assert len(payLoad) > 0 - return self._unpickle(payLoad) - - @_incoming_setting.updateCallback - def _setIncomingSettingOnDriver(self, attribute: protocol.AttributeT, value: typing.Any): - if not configuration.getDriverSettingsManagement(): - return - name = attribute[len(protocol.SETTING_ATTRIBUTE_PREFIX) :].decode("ASCII") - setattr(self._driver, name, value) - - @protocol.attributeSender(protocol.SETTING_ATTRIBUTE_PREFIX + b"*") - def _outgoing_setting(self, attribute: protocol.AttributeT): - if not configuration.getDriverSettingsManagement(): - return self._pickle(None) - name = attribute[len(protocol.SETTING_ATTRIBUTE_PREFIX) :].decode("ASCII") - return self._pickle(getattr(self._driver, name)) - - _remoteProcessHasFocus: bool - - def _get__remoteProcessHasFocus(self): - if self._isSecureDesktopHandler: - self._remoteProcessHasFocus = True - return self._remoteProcessHasFocus - focus = api.getFocusObject() - return focus.processID in ( - self._dev.pipeProcessId, - self._dev.pipeParentProcessId, - ) - - hasFocus: bool - - def _get_hasFocus(self) -> bool: - remoteProcessHasFocus = self._remoteProcessHasFocus - if not remoteProcessHasFocus: - return remoteProcessHasFocus - if self._remoteSessionhasFocus is not None: - return self._remoteSessionhasFocus - log.debug("Requesting time since input from remote driver") - attribute = protocol.GenericAttribute.TIME_SINCE_INPUT - self.requestRemoteAttribute(attribute) - return False - - @protocol.attributeReceiver(protocol.GenericAttribute.TIME_SINCE_INPUT, defaultValue=False) - def _incoming_timeSinceInput(self, payload: bytes) -> int: - assert len(payload) == 4 - return int.from_bytes(payload, byteorder=sys.byteorder, signed=False) - - @_incoming_timeSinceInput.updateCallback - def _post_timeSinceInput(self, attribute: protocol.AttributeT, value: int): - assert attribute == protocol.GenericAttribute.TIME_SINCE_INPUT - self._remoteSessionhasFocus = value <= MAX_TIME_SINCE_INPUT_FOR_REMOTE_SESSION_FOCUS - if self._remoteSessionhasFocus: - self._handleRemoteSessionGainFocus() - - def _handleRemoteSessionGainFocus(self): - return - - def _onReadError(self, error: int) -> bool: - return self.decide_remoteDisconnect.decide(handler=self, error=error) - - @abstractmethod - def _handleDriverChanged(self, driver: Driver): - self._attributeSenderStore( - protocol.GenericAttribute.SUPPORTED_SETTINGS, - settings=driver.supportedSettings, - ) + _dev: namedPipe.NamedPipeBase + decide_remoteDisconnect: AccumulatingDecider + _isSecureDesktopHandler: bool = False + _remoteSessionhasFocus: typing.Optional[bool] = None + _driver: Driver + _abstract__driver = True + + def _get__driver(self) -> Driver: + raise NotImplementedError + + def __new__(cls, *args, **kwargs): + obj = super().__new__(cls, *args, **kwargs) + obj.decide_remoteDisconnect = AccumulatingDecider(defaultDecision=False) + return obj + + def initIo( + self, + ioThread: IoThread, + pipeName: str, + isNamedPipeClient: bool = True, + ): + if isNamedPipeClient: + self._dev = namedPipe.NamedPipeClient( + pipeName=pipeName, + onReceive=self._onReceive, + onReadError=self._onReadError, + ioThread=ioThread, + ) + else: + self._dev = namedPipe.NamedPipeServer( + pipeName=pipeName, + onReceive=self._onReceive, + onConnected=self._onConnected, + ioThreadEx=ioThread, + ) + + def __init__( + self, + ioThread: IoThread, + pipeName: str, + isNamedPipeClient: bool = True, + ): + self._isSecureDesktopHandler = not isNamedPipeClient + super().__init__() + self.initIo(ioThread, pipeName, isNamedPipeClient) + + if not self._isSecureDesktopHandler: + self._onConnected() + elif self._remoteSessionhasFocus is None: + self._remoteSessionhasFocus = False + + def _onConnected(self, connected: bool = True): + if self._isSecureDesktopHandler: + self._remoteSessionhasFocus = connected + if connected: + self._handleDriverChanged(self._driver) + + def event_gainFocus(self, obj): + if self._isSecureDesktopHandler: + return + # Invalidate the property cache to ensure that hasFocus will be fetched again. + # Normally, hasFocus should be cached since it is pretty expensive + # and should never try to fetch the time since input from the remote driver + # more than once per core cycle. + # However, if we don't clear the cache here, the braille handler won't be enabled correctly + # for the first focus outside the remote window. + self.invalidateCache() + self._remoteSessionhasFocus = None + + @protocol.attributeSender(protocol.GenericAttribute.SUPPORTED_SETTINGS) + def _outgoing_supportedSettings(self, settings=None) -> bytes: + if not configuration.getDriverSettingsManagement(): + return self._pickle([]) + if settings is None: + settings = self._driver.supportedSettings + return self._pickle(settings) + + @protocol.attributeSender(b"available*s") + def _outgoing_availableSettingValues(self, attribute: protocol.AttributeT) -> bytes: + if not configuration.getDriverSettingsManagement(): + return self._pickle({}) + name = attribute.decode("ASCII") + return self._pickle(getattr(self._driver, name)) + + @protocol.attributeReceiver(protocol.SETTING_ATTRIBUTE_PREFIX + b"*") + def _incoming_setting(self, attribute: protocol.AttributeT, payLoad: bytes): + assert len(payLoad) > 0 + return self._unpickle(payLoad) + + @_incoming_setting.updateCallback + def _setIncomingSettingOnDriver(self, attribute: protocol.AttributeT, value: typing.Any): + if not configuration.getDriverSettingsManagement(): + return + name = attribute[len(protocol.SETTING_ATTRIBUTE_PREFIX) :].decode("ASCII") + setattr(self._driver, name, value) + + @protocol.attributeSender(protocol.SETTING_ATTRIBUTE_PREFIX + b"*") + def _outgoing_setting(self, attribute: protocol.AttributeT): + if not configuration.getDriverSettingsManagement(): + return self._pickle(None) + name = attribute[len(protocol.SETTING_ATTRIBUTE_PREFIX) :].decode("ASCII") + return self._pickle(getattr(self._driver, name)) + + _remoteProcessHasFocus: bool + + def _get__remoteProcessHasFocus(self): + if self._isSecureDesktopHandler: + self._remoteProcessHasFocus = True + return self._remoteProcessHasFocus + focus = api.getFocusObject() + return focus.processID in ( + self._dev.pipeProcessId, + self._dev.pipeParentProcessId, + ) + + hasFocus: bool + + def _get_hasFocus(self) -> bool: + remoteProcessHasFocus = self._remoteProcessHasFocus + if not remoteProcessHasFocus: + return remoteProcessHasFocus + if self._remoteSessionhasFocus is not None: + return self._remoteSessionhasFocus + log.debug("Requesting time since input from remote driver") + attribute = protocol.GenericAttribute.TIME_SINCE_INPUT + self.requestRemoteAttribute(attribute) + return False + + @protocol.attributeReceiver(protocol.GenericAttribute.TIME_SINCE_INPUT, defaultValue=False) + def _incoming_timeSinceInput(self, payload: bytes) -> int: + assert len(payload) == 4 + return int.from_bytes(payload, byteorder=sys.byteorder, signed=False) + + @_incoming_timeSinceInput.updateCallback + def _post_timeSinceInput(self, attribute: protocol.AttributeT, value: int): + assert attribute == protocol.GenericAttribute.TIME_SINCE_INPUT + self._remoteSessionhasFocus = value <= MAX_TIME_SINCE_INPUT_FOR_REMOTE_SESSION_FOCUS + if self._remoteSessionhasFocus: + self._handleRemoteSessionGainFocus() + + def _handleRemoteSessionGainFocus(self): + return + + def _onReadError(self, error: int) -> bool: + return self.decide_remoteDisconnect.decide(handler=self, error=error) + + @abstractmethod + def _handleDriverChanged(self, driver: Driver): + self._attributeSenderStore( + protocol.GenericAttribute.SUPPORTED_SETTINGS, + settings=driver.supportedSettings, + ) diff --git a/addon/globalPlugins/rdAccess/handlers/remoteBrailleHandler.py b/addon/globalPlugins/rdAccess/handlers/remoteBrailleHandler.py index 7eb02cb..f3978ea 100644 --- a/addon/globalPlugins/rdAccess/handlers/remoteBrailleHandler.py +++ b/addon/globalPlugins/rdAccess/handlers/remoteBrailleHandler.py @@ -16,107 +16,107 @@ from ._remoteHandler import RemoteHandler if typing.TYPE_CHECKING: - from ....lib import protocol + from ....lib import protocol else: - import addonHandler + import addonHandler - addon: addonHandler.Addon = addonHandler.getCodeAddon() - protocol = addon.loadModule("lib.protocol") + addon: addonHandler.Addon = addonHandler.getCodeAddon() + protocol = addon.loadModule("lib.protocol") class RemoteBrailleHandler(RemoteHandler): - driverType = protocol.DriverType.BRAILLE - _driver: braille.BrailleDisplayDriver - _queuedWrite: typing.Optional[typing.List[int]] = None - _queuedWriteLock: threading.Lock - - def __init__(self, ioThread: IoThread, pipeName: str, isNamedPipeClient: bool = True): - self._queuedWriteLock = threading.Lock() - super().__init__(ioThread, pipeName, isNamedPipeClient=isNamedPipeClient) - braille.decide_enabled.register(self._handleBrailleHandlerEnabled) - braille.displayChanged.register(self._handleDriverChanged) - postBrailleViewerToolToggledAction.register(self._handlePostBrailleViewerToolToggled) - inputCore.decide_executeGesture.register(self._handleExecuteGesture) - - def terminate(self): - inputCore.decide_executeGesture.unregister(self._handleExecuteGesture) - postBrailleViewerToolToggledAction.unregister(self._handlePostBrailleViewerToolToggled) - braille.displayChanged.unregister(self._handleDriverChanged) - braille.decide_enabled.unregister(self._handleBrailleHandlerEnabled) - super().terminate() - - def _get__driver(self): - return braille.handler.display - - @protocol.attributeSender(protocol.BrailleAttribute.NUM_CELLS) - def _outgoing_numCells(self, numCells=None) -> bytes: - if numCells is None: - # Use the display size of the local braille handler - numCells = braille.handler.displaySize - return intToByte(numCells) - - @protocol.attributeSender(protocol.BrailleAttribute.GESTURE_MAP) - def _outgoing_gestureMap(self, gestureMap: typing.Optional[inputCore.GlobalGestureMap] = None) -> bytes: - if gestureMap is None: - gestureMap = self._driver.gestureMap - if gestureMap and not (versionInfo.version_year == 2023 and versionInfo.version_major == 1): - export = gestureMap.export() - gestureMap = inputCore.GlobalGestureMap(export) - gestureMap.update(inputCore.manager.userGestureMap.export()) - return self._pickle(gestureMap) - - @protocol.commandHandler(protocol.BrailleCommand.DISPLAY) - def _command_display(self, payload: bytes): - cells = list(payload) - if braille.handler.displaySize > 0: - with self._queuedWriteLock: - self._queuedWrite = cells - if not braille.handler.enabled and self.hasFocus: - self._queueFunctionOnMainThread(self._performLocalWriteCells) - elif self._remoteSessionhasFocus is False: - self.requestRemoteAttribute(protocol.GenericAttribute.TIME_SINCE_INPUT) - - def _performLocalWriteCells(self): - with self._queuedWriteLock: - data = self._queuedWrite - self._queuedWrite = None - if data: - braille.handler._writeCells(data) - - def _handleRemoteSessionGainFocus(self): - super()._handleRemoteSessionGainFocus() - self._queueFunctionOnMainThread(self._performLocalWriteCells) - - def _handleExecuteGesture(self, gesture): - if ( - isinstance(gesture, braille.BrailleDisplayGesture) - and not braille.handler.enabled - and self.hasFocus - ): - kwargs = dict( - source=gesture.source, - id=gesture.id, - routingIndex=gesture.routingIndex, - model=gesture.model, - ) - if isinstance(gesture, brailleInput.BrailleInputGesture): - kwargs["dots"] = gesture.dots - kwargs["space"] = gesture.space - newGesture = protocol.braille.BrailleInputGesture(**kwargs) - try: - self.writeMessage(protocol.BrailleCommand.EXECUTE_GESTURE, self._pickle(newGesture)) - return False - except WindowsError: - log.warning("Error calling _handleExecuteGesture", exc_info=True) - return True - - def _handleBrailleHandlerEnabled(self): - return not self.hasFocus - - def _handleDriverChanged(self, display: braille.BrailleDisplayDriver): - self._attributeSenderStore(protocol.BrailleAttribute.NUM_CELLS) - super()._handleDriverChanged(display) - self._attributeSenderStore(protocol.BrailleAttribute.GESTURE_MAP, gestureMap=display.gestureMap) - - def _handlePostBrailleViewerToolToggled(self): - self._attributeSenderStore(protocol.BrailleAttribute.NUM_CELLS) + driverType = protocol.DriverType.BRAILLE + _driver: braille.BrailleDisplayDriver + _queuedWrite: typing.Optional[typing.List[int]] = None + _queuedWriteLock: threading.Lock + + def __init__(self, ioThread: IoThread, pipeName: str, isNamedPipeClient: bool = True): + self._queuedWriteLock = threading.Lock() + super().__init__(ioThread, pipeName, isNamedPipeClient=isNamedPipeClient) + braille.decide_enabled.register(self._handleBrailleHandlerEnabled) + braille.displayChanged.register(self._handleDriverChanged) + postBrailleViewerToolToggledAction.register(self._handlePostBrailleViewerToolToggled) + inputCore.decide_executeGesture.register(self._handleExecuteGesture) + + def terminate(self): + inputCore.decide_executeGesture.unregister(self._handleExecuteGesture) + postBrailleViewerToolToggledAction.unregister(self._handlePostBrailleViewerToolToggled) + braille.displayChanged.unregister(self._handleDriverChanged) + braille.decide_enabled.unregister(self._handleBrailleHandlerEnabled) + super().terminate() + + def _get__driver(self): + return braille.handler.display + + @protocol.attributeSender(protocol.BrailleAttribute.NUM_CELLS) + def _outgoing_numCells(self, numCells=None) -> bytes: + if numCells is None: + # Use the display size of the local braille handler + numCells = braille.handler.displaySize + return intToByte(numCells) + + @protocol.attributeSender(protocol.BrailleAttribute.GESTURE_MAP) + def _outgoing_gestureMap(self, gestureMap: typing.Optional[inputCore.GlobalGestureMap] = None) -> bytes: + if gestureMap is None: + gestureMap = self._driver.gestureMap + if gestureMap and not (versionInfo.version_year == 2023 and versionInfo.version_major == 1): + export = gestureMap.export() + gestureMap = inputCore.GlobalGestureMap(export) + gestureMap.update(inputCore.manager.userGestureMap.export()) + return self._pickle(gestureMap) + + @protocol.commandHandler(protocol.BrailleCommand.DISPLAY) + def _command_display(self, payload: bytes): + cells = list(payload) + if braille.handler.displaySize > 0: + with self._queuedWriteLock: + self._queuedWrite = cells + if not braille.handler.enabled and self.hasFocus: + self._queueFunctionOnMainThread(self._performLocalWriteCells) + elif self._remoteSessionhasFocus is False: + self.requestRemoteAttribute(protocol.GenericAttribute.TIME_SINCE_INPUT) + + def _performLocalWriteCells(self): + with self._queuedWriteLock: + data = self._queuedWrite + self._queuedWrite = None + if data: + braille.handler._writeCells(data) + + def _handleRemoteSessionGainFocus(self): + super()._handleRemoteSessionGainFocus() + self._queueFunctionOnMainThread(self._performLocalWriteCells) + + def _handleExecuteGesture(self, gesture): + if ( + isinstance(gesture, braille.BrailleDisplayGesture) + and not braille.handler.enabled + and self.hasFocus + ): + kwargs = dict( + source=gesture.source, + id=gesture.id, + routingIndex=gesture.routingIndex, + model=gesture.model, + ) + if isinstance(gesture, brailleInput.BrailleInputGesture): + kwargs["dots"] = gesture.dots + kwargs["space"] = gesture.space + newGesture = protocol.braille.BrailleInputGesture(**kwargs) + try: + self.writeMessage(protocol.BrailleCommand.EXECUTE_GESTURE, self._pickle(newGesture)) + return False + except WindowsError: + log.warning("Error calling _handleExecuteGesture", exc_info=True) + return True + + def _handleBrailleHandlerEnabled(self): + return not self.hasFocus + + def _handleDriverChanged(self, display: braille.BrailleDisplayDriver): + self._attributeSenderStore(protocol.BrailleAttribute.NUM_CELLS) + super()._handleDriverChanged(display) + self._attributeSenderStore(protocol.BrailleAttribute.GESTURE_MAP, gestureMap=display.gestureMap) + + def _handlePostBrailleViewerToolToggled(self): + self._attributeSenderStore(protocol.BrailleAttribute.NUM_CELLS) diff --git a/addon/globalPlugins/rdAccess/handlers/remoteSpeechHandler.py b/addon/globalPlugins/rdAccess/handlers/remoteSpeechHandler.py index e21654b..0b8b84d 100644 --- a/addon/globalPlugins/rdAccess/handlers/remoteSpeechHandler.py +++ b/addon/globalPlugins/rdAccess/handlers/remoteSpeechHandler.py @@ -15,115 +15,115 @@ from ._remoteHandler import RemoteHandler if typing.TYPE_CHECKING: - from ....lib import protocol + from ....lib import protocol else: - import addonHandler + import addonHandler - addon: addonHandler.Addon = addonHandler.getCodeAddon() - protocol = addon.loadModule("lib.protocol") + addon: addonHandler.Addon = addonHandler.getCodeAddon() + protocol = addon.loadModule("lib.protocol") class RemoteSpeechHandler(RemoteHandler): - driverType = protocol.DriverType.SPEECH - _driver: synthDriverHandler.SynthDriver - - def __init__(self, ioThread: IoThread, pipeName: str, isNamedPipeClient: bool = True): - self._indexesSpeaking = [] - super().__init__(ioThread, pipeName, isNamedPipeClient=isNamedPipeClient) - synthDriverHandler.synthIndexReached.register(self._onSynthIndexReached) - synthDriverHandler.synthDoneSpeaking.register(self._onSynthDoneSpeaking) - synthDriverHandler.synthChanged.register(self._handleDriverChanged) - - def terminate(self): - synthDriverHandler.synthChanged.unregister(self._handleDriverChanged) - synthDriverHandler.synthDoneSpeaking.unregister(self._onSynthDoneSpeaking) - synthDriverHandler.synthIndexReached.unregister(self._onSynthIndexReached) - super().terminate() - - def _get__driver(self): - return synthDriverHandler.getSynth() - - @protocol.attributeSender(protocol.SpeechAttribute.SUPPORTED_COMMANDS) - def _outgoing_supportedCommands(self, commands=None) -> bytes: - if commands is None: - commands = self._driver.supportedCommands - return self._pickle(commands) - - @protocol.attributeSender(protocol.SpeechAttribute.LANGUAGE) - def _outgoing_language(self, language: typing.Optional[str] = None) -> bytes: - if language is None: - language = self._driver.language - return self._pickle(language) - - @protocol.commandHandler(protocol.SpeechCommand.SPEAK) - def _command_speak(self, payload: bytes): - sequence = self._unpickle(payload) - for item in sequence: - if isinstance(item, IndexCommand): - item.index += protocol.speech.SPEECH_INDEX_OFFSET - self._indexesSpeaking.append(item.index) - # Queue speech to the current synth directly because we don't want unnecessary processing to happen. - self._queueFunctionOnMainThread(self._driver.speak, sequence) - - @protocol.commandHandler(protocol.SpeechCommand.CANCEL) - def _command_cancel(self, payload: bytes = b""): - self._indexesSpeaking.clear() - self._queueFunctionOnMainThread(self._driver.cancel) - - @protocol.commandHandler(protocol.SpeechCommand.PAUSE) - def _command_pause(self, payload: bytes): - assert len(payload) == 1 - switch = bool.from_bytes(payload, sys.byteorder) - self._queueFunctionOnMainThread(self._driver.pause, switch) - - @protocol.commandHandler(protocol.SpeechCommand.BEEP) - def _command_beep(self, payload: bytes): - kwargs = self._unpickle(payload) - log.debug(f"Received BEEP command: {kwargs}") - # Tones are always asynchronous - tones.beep(**kwargs) - - @protocol.commandHandler(protocol.SpeechCommand.PLAY_WAVE_FILE) - def _command_playWaveFile(self, payload: bytes): - kwargs = self._unpickle(payload) - log.debug(f"Received PLAY_WAVE_FILE command: {kwargs}") - # Ensure the wave plays asynchronous. - kwargs["asynchronous"] = True - nvwave.playWaveFile(**kwargs) - - def _onSynthIndexReached( - self, - synth: typing.Optional[synthDriverHandler.SynthDriver] = None, - index: typing.Optional[int] = None, - ): - assert synth == self._driver - if index in self._indexesSpeaking: - subtractedIndex = index - protocol.speech.SPEECH_INDEX_OFFSET - indexBytes = subtractedIndex.to_bytes( - length=2, # Bytes needed to encode speech._manager.MAX_INDEX - byteorder=sys.byteorder, # for a single byte big/little endian does not matter. - signed=False, - ) - try: - self.writeMessage(protocol.SpeechCommand.INDEX_REACHED, indexBytes) - except WindowsError: - log.warning("Error calling _onSynthIndexReached", exc_info=True) - self._indexesSpeaking.remove(index) - - def _onSynthDoneSpeaking(self, synth: typing.Optional[synthDriverHandler.SynthDriver] = None): - assert synth == self._driver - if len(self._indexesSpeaking) > 0: - self._indexesSpeaking.clear() - try: - self.writeMessage(protocol.SpeechCommand.INDEX_REACHED, b"\x00\x00") - except WindowsError: - log.warning("Error calling _onSynthDoneSpeaking", exc_info=True) - - def _handleDriverChanged(self, synth: synthDriverHandler.SynthDriver): - self._indexesSpeaking.clear() - super()._handleDriverChanged(synth) - self._attributeSenderStore( - protocol.SpeechAttribute.SUPPORTED_COMMANDS, - commands=synth.supportedCommands, - ) - self._attributeSenderStore(protocol.SpeechAttribute.LANGUAGE, language=synth.language) + driverType = protocol.DriverType.SPEECH + _driver: synthDriverHandler.SynthDriver + + def __init__(self, ioThread: IoThread, pipeName: str, isNamedPipeClient: bool = True): + self._indexesSpeaking = [] + super().__init__(ioThread, pipeName, isNamedPipeClient=isNamedPipeClient) + synthDriverHandler.synthIndexReached.register(self._onSynthIndexReached) + synthDriverHandler.synthDoneSpeaking.register(self._onSynthDoneSpeaking) + synthDriverHandler.synthChanged.register(self._handleDriverChanged) + + def terminate(self): + synthDriverHandler.synthChanged.unregister(self._handleDriverChanged) + synthDriverHandler.synthDoneSpeaking.unregister(self._onSynthDoneSpeaking) + synthDriverHandler.synthIndexReached.unregister(self._onSynthIndexReached) + super().terminate() + + def _get__driver(self): + return synthDriverHandler.getSynth() + + @protocol.attributeSender(protocol.SpeechAttribute.SUPPORTED_COMMANDS) + def _outgoing_supportedCommands(self, commands=None) -> bytes: + if commands is None: + commands = self._driver.supportedCommands + return self._pickle(commands) + + @protocol.attributeSender(protocol.SpeechAttribute.LANGUAGE) + def _outgoing_language(self, language: typing.Optional[str] = None) -> bytes: + if language is None: + language = self._driver.language + return self._pickle(language) + + @protocol.commandHandler(protocol.SpeechCommand.SPEAK) + def _command_speak(self, payload: bytes): + sequence = self._unpickle(payload) + for item in sequence: + if isinstance(item, IndexCommand): + item.index += protocol.speech.SPEECH_INDEX_OFFSET + self._indexesSpeaking.append(item.index) + # Queue speech to the current synth directly because we don't want unnecessary processing to happen. + self._queueFunctionOnMainThread(self._driver.speak, sequence) + + @protocol.commandHandler(protocol.SpeechCommand.CANCEL) + def _command_cancel(self, payload: bytes = b""): + self._indexesSpeaking.clear() + self._queueFunctionOnMainThread(self._driver.cancel) + + @protocol.commandHandler(protocol.SpeechCommand.PAUSE) + def _command_pause(self, payload: bytes): + assert len(payload) == 1 + switch = bool.from_bytes(payload, sys.byteorder) + self._queueFunctionOnMainThread(self._driver.pause, switch) + + @protocol.commandHandler(protocol.SpeechCommand.BEEP) + def _command_beep(self, payload: bytes): + kwargs = self._unpickle(payload) + log.debug(f"Received BEEP command: {kwargs}") + # Tones are always asynchronous + tones.beep(**kwargs) + + @protocol.commandHandler(protocol.SpeechCommand.PLAY_WAVE_FILE) + def _command_playWaveFile(self, payload: bytes): + kwargs = self._unpickle(payload) + log.debug(f"Received PLAY_WAVE_FILE command: {kwargs}") + # Ensure the wave plays asynchronous. + kwargs["asynchronous"] = True + nvwave.playWaveFile(**kwargs) + + def _onSynthIndexReached( + self, + synth: typing.Optional[synthDriverHandler.SynthDriver] = None, + index: typing.Optional[int] = None, + ): + assert synth == self._driver + if index in self._indexesSpeaking: + subtractedIndex = index - protocol.speech.SPEECH_INDEX_OFFSET + indexBytes = subtractedIndex.to_bytes( + length=2, # Bytes needed to encode speech._manager.MAX_INDEX + byteorder=sys.byteorder, # for a single byte big/little endian does not matter. + signed=False, + ) + try: + self.writeMessage(protocol.SpeechCommand.INDEX_REACHED, indexBytes) + except WindowsError: + log.warning("Error calling _onSynthIndexReached", exc_info=True) + self._indexesSpeaking.remove(index) + + def _onSynthDoneSpeaking(self, synth: typing.Optional[synthDriverHandler.SynthDriver] = None): + assert synth == self._driver + if len(self._indexesSpeaking) > 0: + self._indexesSpeaking.clear() + try: + self.writeMessage(protocol.SpeechCommand.INDEX_REACHED, b"\x00\x00") + except WindowsError: + log.warning("Error calling _onSynthDoneSpeaking", exc_info=True) + + def _handleDriverChanged(self, synth: synthDriverHandler.SynthDriver): + self._indexesSpeaking.clear() + super()._handleDriverChanged(synth) + self._attributeSenderStore( + protocol.SpeechAttribute.SUPPORTED_COMMANDS, + commands=synth.supportedCommands, + ) + self._attributeSenderStore(protocol.SpeechAttribute.LANGUAGE, language=synth.language) diff --git a/addon/globalPlugins/rdAccess/objects.py b/addon/globalPlugins/rdAccess/objects.py index 63c413e..60e2bb8 100644 --- a/addon/globalPlugins/rdAccess/objects.py +++ b/addon/globalPlugins/rdAccess/objects.py @@ -7,23 +7,23 @@ class RemoteDesktopControl(NVDAObject): - ... + ... class OutOfProcessChannelRemoteDesktopControl(RemoteDesktopControl): - ... + ... def findExtraOverlayClasses(obj, clsList): - if not isinstance(obj, IAccessible): - return - elif (obj.windowClassName == "IHWindowClass" and obj.appModule.appName == "mstsc") or ( - obj.windowClassName in ("CtxICADisp", "Transparent Windows Client") - and obj.appModule.appName == "wfica32" - ): - clsList.append(RemoteDesktopControl) - elif ( - obj.windowClassName == "VMware.Horizon.Client.Sdk:RemoteWindow Class" - and obj.appModule.appName == "vmware-view" - ): - clsList.append(OutOfProcessChannelRemoteDesktopControl) + if not isinstance(obj, IAccessible): + return + elif (obj.windowClassName == "IHWindowClass" and obj.appModule.appName == "mstsc") or ( + obj.windowClassName in ("CtxICADisp", "Transparent Windows Client") + and obj.appModule.appName == "wfica32" + ): + clsList.append(RemoteDesktopControl) + elif ( + obj.windowClassName == "VMware.Horizon.Client.Sdk:RemoteWindow Class" + and obj.appModule.appName == "vmware-view" + ): + clsList.append(OutOfProcessChannelRemoteDesktopControl) diff --git a/addon/globalPlugins/rdAccess/secureDesktopHandling.py b/addon/globalPlugins/rdAccess/secureDesktopHandling.py index 4675917..f1b967b 100644 --- a/addon/globalPlugins/rdAccess/secureDesktopHandling.py +++ b/addon/globalPlugins/rdAccess/secureDesktopHandling.py @@ -17,36 +17,36 @@ from .handlers._remoteHandler import RemoteHandler if typing.TYPE_CHECKING: - from ...lib import ioThreadEx, namedPipe + from ...lib import ioThreadEx, namedPipe else: - addon: addonHandler.Addon = addonHandler.getCodeAddon() - ioThreadEx = addon.loadModule("lib.ioThreadEx") - namedPipe = addon.loadModule("lib.namedPipe") + addon: addonHandler.Addon = addonHandler.getCodeAddon() + ioThreadEx = addon.loadModule("lib.ioThreadEx") + namedPipe = addon.loadModule("lib.namedPipe") HandlerTypeT = typing.TypeVar("HandlerTypeT", bound=RemoteHandler) class SecureDesktopHandler(AutoPropertyObject): - _ioThreadRef: weakref.ReferenceType[ioThreadEx.IoThreadEx] - _brailleHandler: RemoteBrailleHandler - _speechHandler: RemoteSpeechHandler - - def __init__(self, ioThread: ioThreadEx.IoThreadEx): - self._ioThreadRef = weakref.ref(ioThread) - braille.handler.display.saveSettings() - self._brailleHandler = self._initializeHandler(RemoteBrailleHandler) - synthDriverHandler.getSynth().saveSettings() - self._speechHandler = self._initializeHandler(RemoteSpeechHandler) - - def terminate(self): - self._speechHandler.terminate() - braille.handler.display.loadSettings() - self._brailleHandler.terminate() - synthDriverHandler.getSynth().loadSettings() - - def _initializeHandler(self, handlerType: typing.Type[HandlerTypeT]) -> HandlerTypeT: - sdId = f"NVDA_SD-{handlerType.driverType.name}" - sdPort = os.path.join(namedPipe.PIPE_DIRECTORY, sdId) - handler = handlerType(self._ioThreadRef(), sdPort, False) - return handler + _ioThreadRef: weakref.ReferenceType[ioThreadEx.IoThreadEx] + _brailleHandler: RemoteBrailleHandler + _speechHandler: RemoteSpeechHandler + + def __init__(self, ioThread: ioThreadEx.IoThreadEx): + self._ioThreadRef = weakref.ref(ioThread) + braille.handler.display.saveSettings() + self._brailleHandler = self._initializeHandler(RemoteBrailleHandler) + synthDriverHandler.getSynth().saveSettings() + self._speechHandler = self._initializeHandler(RemoteSpeechHandler) + + def terminate(self): + self._speechHandler.terminate() + braille.handler.display.loadSettings() + self._brailleHandler.terminate() + synthDriverHandler.getSynth().loadSettings() + + def _initializeHandler(self, handlerType: typing.Type[HandlerTypeT]) -> HandlerTypeT: + sdId = f"NVDA_SD-{handlerType.driverType.name}" + sdPort = os.path.join(namedPipe.PIPE_DIRECTORY, sdId) + handler = handlerType(self._ioThreadRef(), sdPort, False) + return handler diff --git a/addon/globalPlugins/rdAccess/settingsPanel.py b/addon/globalPlugins/rdAccess/settingsPanel.py index bec5dc8..70398fc 100644 --- a/addon/globalPlugins/rdAccess/settingsPanel.py +++ b/addon/globalPlugins/rdAccess/settingsPanel.py @@ -14,147 +14,147 @@ from gui.settingsDialogs import SettingsPanel if typing.TYPE_CHECKING: - from ...lib import configuration, rdPipe + from ...lib import configuration, rdPipe else: - addon: addonHandler.Addon = addonHandler.getCodeAddon() - configuration = addon.loadModule("lib.configuration") - rdPipe = addon.loadModule("lib.rdPipe") + addon: addonHandler.Addon = addonHandler.getCodeAddon() + configuration = addon.loadModule("lib.configuration") + rdPipe = addon.loadModule("lib.rdPipe") addonHandler.initTranslation() class RemoteDesktopSettingsPanel(SettingsPanel): - # Translators: The label for the NVDA Remote Desktop settings panel. - title = _("Remote Desktop Accessibility") - post_onSave = Action() - - def makeSettings(self, settingsSizer): - sizer_helper = guiHelper.BoxSizerHelper(self, sizer=settingsSizer) - # Translators: The label for a list of check boxes in RDAccess settings to set operating mode. - operatingModeText = _("&Enable remote desktop accessibility for") - operatingModeChoices = [i.displayString for i in configuration.OperatingMode] - self.operatingModes = list(configuration.OperatingMode) - self.operatingModeList = sizer_helper.addLabeledControl( - operatingModeText, - nvdaControls.CustomCheckListBox, - choices=operatingModeChoices, - ) - self.operatingModeList.CheckedItems = [ - n for n, e in enumerate(configuration.OperatingMode) if configuration.getOperatingMode() & e - ] - self.operatingModeList.Select(0) - self.operatingModeList.Bind(wx.EVT_CHECKLISTBOX, self.onoperatingModeChange) - - # Translators: This is the label for a group of options in the - # Remote Desktop settings panel. - serverGroupText = _("Server") - serverGroupSizer = wx.StaticBoxSizer(wx.VERTICAL, self, label=serverGroupText) - serverGroupBox = serverGroupSizer.GetStaticBox() - serverGroup = guiHelper.BoxSizerHelper(self, sizer=serverGroupSizer) - sizer_helper.addItem(serverGroup) - - # Translators: The label for a setting in RDAccess settings to enable - # automatic recovery of remote speech when the connection was lost. - recoverRemoteSpeechText = _("&Automatically recover remote speech after connection loss") - self.recoverRemoteSpeechCheckbox = serverGroup.addItem( - wx.CheckBox(serverGroupBox, label=recoverRemoteSpeechText) - ) - self.recoverRemoteSpeechCheckbox.Value = configuration.getRecoverRemoteSpeech() - - # Translators: This is the label for a group of options in the - # Remote Desktop settings panel. - clientGroupText = _("Client") - clientGroupSizer = wx.StaticBoxSizer(wx.VERTICAL, self, label=clientGroupText) - clientGroupBox = clientGroupSizer.GetStaticBox() - clientGroup = guiHelper.BoxSizerHelper(self, sizer=clientGroupSizer) - sizer_helper.addItem(clientGroup) - - # Translators: The label for a setting in RDAccess settings to enable - # support for exchanging driver settings between the local and the remote system. - driverSettingsManagementText = _("&Allow remote system to control driver settings") - self.driverSettingsManagementCheckbox = clientGroup.addItem( - wx.CheckBox(clientGroupBox, label=driverSettingsManagementText) - ) - self.driverSettingsManagementCheckbox.Value = configuration.getDriverSettingsManagement() - - # Translators: The label for a setting in RDAccess settings to enable - # persistent registration of RD Pipe to the Windows registry. - persistentRegistrationText = _("&Persist client support when exiting NVDA") - self.persistentRegistrationCheckbox = clientGroup.addItem( - wx.CheckBox(clientGroupBox, label=persistentRegistrationText) - ) - self.persistentRegistrationCheckbox.Value = configuration.getPersistentRegistration() - - # Translators: The label for a setting in RDAccess settings to enable - # registration of RD Pipe to the Windows registry for remote desktop support. - remoteDesktopSupportText = _("Enable Microsoft &Remote Desktop support") - self.remoteDesktopSupportCheckbox = clientGroup.addItem( - wx.CheckBox(clientGroupBox, label=remoteDesktopSupportText) - ) - self.remoteDesktopSupportCheckbox.Value = configuration.getRemoteDesktopSupport() - - # Translators: The label for a setting in RDAccess settings to enable - # registration of RD Pipe to the Windows registry for Citrix support. - citrixSupportText = _("Enable &Citrix Workspace support") - self.citrixSupportCheckbox = clientGroup.addItem(wx.CheckBox(clientGroupBox, label=citrixSupportText)) - self.citrixSupportCheckbox.Value = configuration.getCitrixSupport() - - self.onoperatingModeChange() - - def onoperatingModeChange(self, evt: typing.Optional[wx.CommandEvent] = None): - if evt: - evt.Skip() - isClient = self.operatingModeList.IsChecked( - self.operatingModes.index(configuration.OperatingMode.CLIENT) - ) - self.driverSettingsManagementCheckbox.Enable(isClient) - self.persistentRegistrationCheckbox.Enable(isClient and config.isInstalledCopy()) - self.remoteDesktopSupportCheckbox.Enable(isClient) - self.citrixSupportCheckbox.Enable(isClient and rdPipe.isCitrixSupported()) - self.recoverRemoteSpeechCheckbox.Enable( - self.operatingModeList.IsChecked(self.operatingModes.index(configuration.OperatingMode.SERVER)) - ) - - def isValid(self): - if not self.operatingModeList.CheckedItems: - messageBox( - # Translators: Message to report wrong configuration of operating mode. - _( - "You need to enable remote destkop accessibility support for at least " - "incoming or outgoing connections." - ), - # Translators: The title of the message box - _("Error"), - wx.OK | wx.ICON_ERROR, - self, - ) - return False - return super().isValid() - - def onSave(self): - config.conf[configuration.CONFIG_SECTION_NAME][configuration.OPERATING_MODE_SETTING_NAME] = int( - functools.reduce( - operator.or_, - (self.operatingModes[i] for i in self.operatingModeList.CheckedItems), - ) - ) - config.conf[configuration.CONFIG_SECTION_NAME][ - configuration.RECOVER_REMOTE_SPEECH_SETTING_NAME - ] = self.recoverRemoteSpeechCheckbox.IsChecked() - isClient = self.operatingModeList.IsChecked( - self.operatingModes.index(configuration.OperatingMode.CLIENT) - ) - config.conf[configuration.CONFIG_SECTION_NAME][ - configuration.DRIVER_settings_MANAGEMENT_SETTING_NAME - ] = self.driverSettingsManagementCheckbox.IsChecked() - config.conf[configuration.CONFIG_SECTION_NAME][configuration.PERSISTENT_REGISTRATION_SETTING_NAME] = ( - self.persistentRegistrationCheckbox.IsChecked() and isClient - ) - config.conf[configuration.CONFIG_SECTION_NAME][ - configuration.REMOTE_DESKTOP_SETTING_NAME - ] = self.remoteDesktopSupportCheckbox.IsChecked() - config.conf[configuration.CONFIG_SECTION_NAME][configuration.CITRIX_SETTING_NAME] = ( - self.citrixSupportCheckbox.IsChecked() and rdPipe.isCitrixSupported() - ) - - self.post_onSave.notify() + # Translators: The label for the NVDA Remote Desktop settings panel. + title = _("Remote Desktop Accessibility") + post_onSave = Action() + + def makeSettings(self, settingsSizer): + sizer_helper = guiHelper.BoxSizerHelper(self, sizer=settingsSizer) + # Translators: The label for a list of check boxes in RDAccess settings to set operating mode. + operatingModeText = _("&Enable remote desktop accessibility for") + operatingModeChoices = [i.displayString for i in configuration.OperatingMode] + self.operatingModes = list(configuration.OperatingMode) + self.operatingModeList = sizer_helper.addLabeledControl( + operatingModeText, + nvdaControls.CustomCheckListBox, + choices=operatingModeChoices, + ) + self.operatingModeList.CheckedItems = [ + n for n, e in enumerate(configuration.OperatingMode) if configuration.getOperatingMode() & e + ] + self.operatingModeList.Select(0) + self.operatingModeList.Bind(wx.EVT_CHECKLISTBOX, self.onoperatingModeChange) + + # Translators: This is the label for a group of options in the + # Remote Desktop settings panel. + serverGroupText = _("Server") + serverGroupSizer = wx.StaticBoxSizer(wx.VERTICAL, self, label=serverGroupText) + serverGroupBox = serverGroupSizer.GetStaticBox() + serverGroup = guiHelper.BoxSizerHelper(self, sizer=serverGroupSizer) + sizer_helper.addItem(serverGroup) + + # Translators: The label for a setting in RDAccess settings to enable + # automatic recovery of remote speech when the connection was lost. + recoverRemoteSpeechText = _("&Automatically recover remote speech after connection loss") + self.recoverRemoteSpeechCheckbox = serverGroup.addItem( + wx.CheckBox(serverGroupBox, label=recoverRemoteSpeechText) + ) + self.recoverRemoteSpeechCheckbox.Value = configuration.getRecoverRemoteSpeech() + + # Translators: This is the label for a group of options in the + # Remote Desktop settings panel. + clientGroupText = _("Client") + clientGroupSizer = wx.StaticBoxSizer(wx.VERTICAL, self, label=clientGroupText) + clientGroupBox = clientGroupSizer.GetStaticBox() + clientGroup = guiHelper.BoxSizerHelper(self, sizer=clientGroupSizer) + sizer_helper.addItem(clientGroup) + + # Translators: The label for a setting in RDAccess settings to enable + # support for exchanging driver settings between the local and the remote system. + driverSettingsManagementText = _("&Allow remote system to control driver settings") + self.driverSettingsManagementCheckbox = clientGroup.addItem( + wx.CheckBox(clientGroupBox, label=driverSettingsManagementText) + ) + self.driverSettingsManagementCheckbox.Value = configuration.getDriverSettingsManagement() + + # Translators: The label for a setting in RDAccess settings to enable + # persistent registration of RD Pipe to the Windows registry. + persistentRegistrationText = _("&Persist client support when exiting NVDA") + self.persistentRegistrationCheckbox = clientGroup.addItem( + wx.CheckBox(clientGroupBox, label=persistentRegistrationText) + ) + self.persistentRegistrationCheckbox.Value = configuration.getPersistentRegistration() + + # Translators: The label for a setting in RDAccess settings to enable + # registration of RD Pipe to the Windows registry for remote desktop support. + remoteDesktopSupportText = _("Enable Microsoft &Remote Desktop support") + self.remoteDesktopSupportCheckbox = clientGroup.addItem( + wx.CheckBox(clientGroupBox, label=remoteDesktopSupportText) + ) + self.remoteDesktopSupportCheckbox.Value = configuration.getRemoteDesktopSupport() + + # Translators: The label for a setting in RDAccess settings to enable + # registration of RD Pipe to the Windows registry for Citrix support. + citrixSupportText = _("Enable &Citrix Workspace support") + self.citrixSupportCheckbox = clientGroup.addItem(wx.CheckBox(clientGroupBox, label=citrixSupportText)) + self.citrixSupportCheckbox.Value = configuration.getCitrixSupport() + + self.onoperatingModeChange() + + def onoperatingModeChange(self, evt: typing.Optional[wx.CommandEvent] = None): + if evt: + evt.Skip() + isClient = self.operatingModeList.IsChecked( + self.operatingModes.index(configuration.OperatingMode.CLIENT) + ) + self.driverSettingsManagementCheckbox.Enable(isClient) + self.persistentRegistrationCheckbox.Enable(isClient and config.isInstalledCopy()) + self.remoteDesktopSupportCheckbox.Enable(isClient) + self.citrixSupportCheckbox.Enable(isClient and rdPipe.isCitrixSupported()) + self.recoverRemoteSpeechCheckbox.Enable( + self.operatingModeList.IsChecked(self.operatingModes.index(configuration.OperatingMode.SERVER)) + ) + + def isValid(self): + if not self.operatingModeList.CheckedItems: + messageBox( + # Translators: Message to report wrong configuration of operating mode. + _( + "You need to enable remote destkop accessibility support for at least " + "incoming or outgoing connections." + ), + # Translators: The title of the message box + _("Error"), + wx.OK | wx.ICON_ERROR, + self, + ) + return False + return super().isValid() + + def onSave(self): + config.conf[configuration.CONFIG_SECTION_NAME][configuration.OPERATING_MODE_SETTING_NAME] = int( + functools.reduce( + operator.or_, + (self.operatingModes[i] for i in self.operatingModeList.CheckedItems), + ) + ) + config.conf[configuration.CONFIG_SECTION_NAME][ + configuration.RECOVER_REMOTE_SPEECH_SETTING_NAME + ] = self.recoverRemoteSpeechCheckbox.IsChecked() + isClient = self.operatingModeList.IsChecked( + self.operatingModes.index(configuration.OperatingMode.CLIENT) + ) + config.conf[configuration.CONFIG_SECTION_NAME][ + configuration.DRIVER_settings_MANAGEMENT_SETTING_NAME + ] = self.driverSettingsManagementCheckbox.IsChecked() + config.conf[configuration.CONFIG_SECTION_NAME][configuration.PERSISTENT_REGISTRATION_SETTING_NAME] = ( + self.persistentRegistrationCheckbox.IsChecked() and isClient + ) + config.conf[configuration.CONFIG_SECTION_NAME][ + configuration.REMOTE_DESKTOP_SETTING_NAME + ] = self.remoteDesktopSupportCheckbox.IsChecked() + config.conf[configuration.CONFIG_SECTION_NAME][configuration.CITRIX_SETTING_NAME] = ( + self.citrixSupportCheckbox.IsChecked() and rdPipe.isCitrixSupported() + ) + + self.post_onSave.notify() diff --git a/addon/globalPlugins/rdAccess/synthDetect.py b/addon/globalPlugins/rdAccess/synthDetect.py index e0a93dc..3c92063 100644 --- a/addon/globalPlugins/rdAccess/synthDetect.py +++ b/addon/globalPlugins/rdAccess/synthDetect.py @@ -16,96 +16,96 @@ from synthDrivers.remote import remoteSynthDriver if typing.TYPE_CHECKING: - from ...lib import detection + from ...lib import detection else: - addon: addonHandler.Addon = addonHandler.getCodeAddon() - detection = addon.loadModule("lib.detection") + addon: addonHandler.Addon = addonHandler.getCodeAddon() + detection = addon.loadModule("lib.detection") class _SynthDetector(AutoPropertyObject): - def __init__(self): - remoteSynthDriver.synthRemoteDisconnected.register(self._handleRemoteDisconnect) - self._executor = ThreadPoolExecutor(1, thread_name_prefix=self.__class__.__name__) - self._queuedFuture: typing.Optional[Future] = None - self._stopEvent = threading.Event() - - currentSynthesizer: synthDriverHandler.SynthDriver - - def _get_currentSynthesizer(self) -> synthDriverHandler.SynthDriver: - return synthDriverHandler.getSynth() - - def _set_currentSynthesizer(self, synth): - curSynth = self._get_currentSynthesizer() - curSynth.cancel() - curSynth.terminate() - synthDriverHandler._curSynth = synth - - isRemoteSynthActive: bool - - def _get_isRemoteSynthActive(self): - return isinstance(self.currentSynthesizer, remoteSynthDriver) - - isRemoteSynthConfigured: bool - - def _get_isRemoteSynthConfigured(self): - return config.conf[remoteSynthDriver._configSection]["synth"] == remoteSynthDriver.name - - def _handleRemoteDisconnect(self, synth: remoteSynthDriver): - log.error(f"Handling remote disconnect for {synth!r}") - queueHandler.queueFunction(queueHandler.eventQueue, self._fallback) - - def _fallback(self): - fallback = ( - config.conf[remoteSynthDriver._configSection] - .get(remoteSynthDriver.name, {}) - .get("fallbackSynth", AUTOMATIC_PORT[0]) - ) - if fallback != AUTOMATIC_PORT[0]: - synthDriverHandler.setSynth(fallback, isFallback=True) - else: - synthDriverHandler.findAndSetNextSynth(remoteSynthDriver.name) - - def _queueBgScan(self, force: bool = False): - if self.isRemoteSynthActive or not (force or self.isRemoteSynthConfigured): - return - if self._queuedFuture: - self._queuedFuture.cancel() - self._queuedFuture = self._executor.submit(self._bgScan) - - def _stopBgScan(self): - """Stops the current scan as soon as possible and prevents a queued scan to start.""" - self._stopEvent.set() - if self._queuedFuture: - # This will cancel a queued scan (i.e. not the currently running scan, if any) - # If this future belongs to a scan that is currently running or finished, this does nothing. - self._queuedFuture.cancel() - - def _bgScan(self): - self._stopEvent.clear() - if self.isRemoteSynthActive: - return - iterator = detection.bgScanRD(driverType=detection.DriverType.SPEECH) - for driver, match in iterator: - if self._stopEvent.is_set(): - return - driverClass = synthDriverHandler._getSynthDriver(driver) - assert issubclass(driverClass, remoteSynthDriver) - try: - driverInst: remoteSynthDriver = driverClass(match) - driverInst.initSettings() - except RuntimeError: - if self._stopEvent.is_set(): - return - continue - self.currentSynthesizer = driverInst - self._stopBgScan() - return - - def rescan(self, force: bool = False): - self._stopBgScan() - self._queueBgScan(force) - - def terminate(self): - remoteSynthDriver.synthRemoteDisconnected.unregister(self._handleRemoteDisconnect) - self._stopBgScan() - self._executor.shutdown(wait=False) + def __init__(self): + remoteSynthDriver.synthRemoteDisconnected.register(self._handleRemoteDisconnect) + self._executor = ThreadPoolExecutor(1, thread_name_prefix=self.__class__.__name__) + self._queuedFuture: typing.Optional[Future] = None + self._stopEvent = threading.Event() + + currentSynthesizer: synthDriverHandler.SynthDriver + + def _get_currentSynthesizer(self) -> synthDriverHandler.SynthDriver: + return synthDriverHandler.getSynth() + + def _set_currentSynthesizer(self, synth): + curSynth = self._get_currentSynthesizer() + curSynth.cancel() + curSynth.terminate() + synthDriverHandler._curSynth = synth + + isRemoteSynthActive: bool + + def _get_isRemoteSynthActive(self): + return isinstance(self.currentSynthesizer, remoteSynthDriver) + + isRemoteSynthConfigured: bool + + def _get_isRemoteSynthConfigured(self): + return config.conf[remoteSynthDriver._configSection]["synth"] == remoteSynthDriver.name + + def _handleRemoteDisconnect(self, synth: remoteSynthDriver): + log.error(f"Handling remote disconnect for {synth!r}") + queueHandler.queueFunction(queueHandler.eventQueue, self._fallback) + + def _fallback(self): + fallback = ( + config.conf[remoteSynthDriver._configSection] + .get(remoteSynthDriver.name, {}) + .get("fallbackSynth", AUTOMATIC_PORT[0]) + ) + if fallback != AUTOMATIC_PORT[0]: + synthDriverHandler.setSynth(fallback, isFallback=True) + else: + synthDriverHandler.findAndSetNextSynth(remoteSynthDriver.name) + + def _queueBgScan(self, force: bool = False): + if self.isRemoteSynthActive or not (force or self.isRemoteSynthConfigured): + return + if self._queuedFuture: + self._queuedFuture.cancel() + self._queuedFuture = self._executor.submit(self._bgScan) + + def _stopBgScan(self): + """Stops the current scan as soon as possible and prevents a queued scan to start.""" + self._stopEvent.set() + if self._queuedFuture: + # This will cancel a queued scan (i.e. not the currently running scan, if any) + # If this future belongs to a scan that is currently running or finished, this does nothing. + self._queuedFuture.cancel() + + def _bgScan(self): + self._stopEvent.clear() + if self.isRemoteSynthActive: + return + iterator = detection.bgScanRD(driverType=detection.DriverType.SPEECH) + for driver, match in iterator: + if self._stopEvent.is_set(): + return + driverClass = synthDriverHandler._getSynthDriver(driver) + assert issubclass(driverClass, remoteSynthDriver) + try: + driverInst: remoteSynthDriver = driverClass(match) + driverInst.initSettings() + except RuntimeError: + if self._stopEvent.is_set(): + return + continue + self.currentSynthesizer = driverInst + self._stopBgScan() + return + + def rescan(self, force: bool = False): + self._stopBgScan() + self._queueBgScan(force) + + def terminate(self): + remoteSynthDriver.synthRemoteDisconnected.unregister(self._handleRemoteDisconnect) + self._stopBgScan() + self._executor.shutdown(wait=False) diff --git a/addon/installTasks.py b/addon/installTasks.py index 1546f94..473aecd 100644 --- a/addon/installTasks.py +++ b/addon/installTasks.py @@ -10,43 +10,43 @@ import wx if typing.TYPE_CHECKING: - from .lib import configuration - from .lib import rdPipe + from .lib import configuration + from .lib import rdPipe else: - addon: addonHandler.Addon = addonHandler.getCodeAddon() - configuration = addon.loadModule("lib.configuration") - rdPipe = addon.loadModule("lib.rdPipe") + addon: addonHandler.Addon = addonHandler.getCodeAddon() + configuration = addon.loadModule("lib.configuration") + rdPipe = addon.loadModule("lib.rdPipe") def onInstall(): - for addon in addonHandler.getAvailableAddons(): - if addon.name == "nvdaRd": - result = gui.messageBox( - # Translators: message asking the user to remove nvdaRd add-on - _( - "This add-on was previously called NVDA Remote Desktop. " - "You have an installed version of that add-on. " - "Would you like to update to RDAccess?" - ), - # Translators: question title - _("Previous version detected"), - wx.YES_NO | wx.ICON_WARNING, - ) - if result == wx.YES: - addon.requestRemove() - else: - raise addonHandler.AddonError("Installed nvdaRd found that needs to be removed first") - configuration.initializeConfig() + for addon in addonHandler.getAvailableAddons(): + if addon.name == "nvdaRd": + result = gui.messageBox( + # Translators: message asking the user to remove nvdaRd add-on + _( + "This add-on was previously called NVDA Remote Desktop. " + "You have an installed version of that add-on. " + "Would you like to update to RDAccess?" + ), + # Translators: question title + _("Previous version detected"), + wx.YES_NO | wx.ICON_WARNING, + ) + if result == wx.YES: + addon.requestRemove() + else: + raise addonHandler.AddonError("Installed nvdaRd found that needs to be removed first") + configuration.initializeConfig() def onUninstall(): - for architecture in set((rdPipe.DEFAULT_ARCHITECTURE, rdPipe.Architecture.X86)): - rdPipe.dllInstall( - install=False, - comServer=True, - rdp=True, - citrix=architecture == rdPipe.Architecture.X86, - architecture=architecture, - ) - # Sleep for a second to ensure we can delete the directory. - sleep(1.0) + for architecture in set((rdPipe.DEFAULT_ARCHITECTURE, rdPipe.Architecture.X86)): + rdPipe.dllInstall( + install=False, + comServer=True, + rdp=True, + citrix=architecture == rdPipe.Architecture.X86, + architecture=architecture, + ) + # Sleep for a second to ensure we can delete the directory. + sleep(1.0) diff --git a/addon/lib/configuration.py b/addon/lib/configuration.py index 7c92653..1ca97e0 100644 --- a/addon/lib/configuration.py +++ b/addon/lib/configuration.py @@ -15,17 +15,17 @@ @unique class OperatingMode(DisplayStringIntFlag): - SERVER = 0x1 - CLIENT = 0x2 - SECURE_DESKTOP = 0x4 + SERVER = 0x1 + CLIENT = 0x2 + SECURE_DESKTOP = 0x4 - @property - def _displayStringLabels(self): - return { - OperatingMode.SERVER: _("Incoming connections (Remote Desktop Server)"), - OperatingMode.CLIENT: _("Outgoing connections (Remote Desktop Client)"), - OperatingMode.SECURE_DESKTOP: _("Secure Desktop pass through"), - } + @property + def _displayStringLabels(self): + return { + OperatingMode.SERVER: _("Incoming connections (Remote Desktop Server)"), + OperatingMode.CLIENT: _("Outgoing connections (Remote Desktop Client)"), + OperatingMode.SECURE_DESKTOP: _("Secure Desktop pass through"), + } CONFIG_SECTION_NAME = addonHandler.getCodeAddon().name @@ -36,60 +36,60 @@ def _displayStringLabels(self): RECOVER_REMOTE_SPEECH_SETTING_NAME = "recoverRemoteSpeech" DRIVER_settings_MANAGEMENT_SETTING_NAME = "driverSettingsManagement" CONFIG_SPEC = { - OPERATING_MODE_SETTING_NAME: "integer(default=3, min=1, max=7)", - PERSISTENT_REGISTRATION_SETTING_NAME: "boolean(default=false)", - REMOTE_DESKTOP_SETTING_NAME: "boolean(default=true)", - CITRIX_SETTING_NAME: "boolean(default=true)", - RECOVER_REMOTE_SPEECH_SETTING_NAME: "boolean(default=true)", - DRIVER_settings_MANAGEMENT_SETTING_NAME: "boolean(default=false)", + OPERATING_MODE_SETTING_NAME: "integer(default=3, min=1, max=7)", + PERSISTENT_REGISTRATION_SETTING_NAME: "boolean(default=false)", + REMOTE_DESKTOP_SETTING_NAME: "boolean(default=true)", + CITRIX_SETTING_NAME: "boolean(default=true)", + RECOVER_REMOTE_SPEECH_SETTING_NAME: "boolean(default=true)", + DRIVER_settings_MANAGEMENT_SETTING_NAME: "boolean(default=false)", } def _getSetting(setting: str, fromCache: bool) -> Any: - if not initialized: - initializeConfig() - section = _cachedConfig if fromCache else config.conf[CONFIG_SECTION_NAME] - return section[setting] + if not initialized: + initializeConfig() + section = _cachedConfig if fromCache else config.conf[CONFIG_SECTION_NAME] + return section[setting] def getOperatingMode(fromCache: bool = False) -> OperatingMode: - return OperatingMode(int(_getSetting(OPERATING_MODE_SETTING_NAME, fromCache))) + return OperatingMode(int(_getSetting(OPERATING_MODE_SETTING_NAME, fromCache))) def getPersistentRegistration(fromCache: bool = False) -> bool: - return _getSetting(PERSISTENT_REGISTRATION_SETTING_NAME, fromCache) + return _getSetting(PERSISTENT_REGISTRATION_SETTING_NAME, fromCache) def getRemoteDesktopSupport(fromCache: bool = False) -> bool: - return _getSetting(REMOTE_DESKTOP_SETTING_NAME, fromCache) + return _getSetting(REMOTE_DESKTOP_SETTING_NAME, fromCache) def getCitrixSupport(fromCache: bool = False) -> bool: - return _getSetting(CITRIX_SETTING_NAME, fromCache) + return _getSetting(CITRIX_SETTING_NAME, fromCache) def getRecoverRemoteSpeech(fromCache: bool = False) -> bool: - return _getSetting(RECOVER_REMOTE_SPEECH_SETTING_NAME, fromCache) + return _getSetting(RECOVER_REMOTE_SPEECH_SETTING_NAME, fromCache) def getDriverSettingsManagement(fromCache: bool = False) -> bool: - return _getSetting(DRIVER_settings_MANAGEMENT_SETTING_NAME, fromCache) + return _getSetting(DRIVER_settings_MANAGEMENT_SETTING_NAME, fromCache) initialized: bool = False def initializeConfig(): - global initialized - if initialized: - return - if CONFIG_SECTION_NAME not in config.conf: - config.conf[CONFIG_SECTION_NAME] = {} - config.conf[CONFIG_SECTION_NAME].spec.update(CONFIG_SPEC) - updateConfigCache() - initialized = True + global initialized + if initialized: + return + if CONFIG_SECTION_NAME not in config.conf: + config.conf[CONFIG_SECTION_NAME] = {} + config.conf[CONFIG_SECTION_NAME].spec.update(CONFIG_SPEC) + updateConfigCache() + initialized = True def updateConfigCache(): - global _cachedConfig - _cachedConfig = config.conf[CONFIG_SECTION_NAME].copy() + global _cachedConfig + _cachedConfig = config.conf[CONFIG_SECTION_NAME].copy() diff --git a/addon/lib/detection.py b/addon/lib/detection.py index bfc0c3d..db97c90 100644 --- a/addon/lib/detection.py +++ b/addon/lib/detection.py @@ -19,39 +19,39 @@ def bgScanRD( - driverType: DriverType = DriverType.BRAILLE, - limitToDevices: Optional[List[str]] = None, + driverType: DriverType = DriverType.BRAILLE, + limitToDevices: Optional[List[str]] = None, ): - from .driver import RemoteDriver - - operatingMode = configuration.getOperatingMode() - if limitToDevices and RemoteDriver.name not in limitToDevices: - return - isSecureDesktop: bool = isRunningOnSecureDesktop() - if isSecureDesktop and operatingMode & configuration.OperatingMode.SECURE_DESKTOP: - sdId = f"NVDA_SD-{driverType.name}" - sdPort = os.path.join(PIPE_DIRECTORY, sdId) - if sdPort in getSecureDesktopNamedPipes(): - yield ( - RemoteDriver.name, - bdDetect.DeviceMatch(type=KEY_NAMED_PIPE_CLIENT, id=sdId, port=sdPort, deviceInfo={}), - ) - if ( - operatingMode & configuration.OperatingMode.SERVER - and not isSecureDesktop - and getRemoteSessionMetrics() == 1 - ): - port = f"NVDA-{driverType.name}" - yield ( - RemoteDriver.name, - bdDetect.DeviceMatch(type=KEY_VIRTUAL_CHANNEL, id=port, port=port, deviceInfo={}), - ) + from .driver import RemoteDriver + + operatingMode = configuration.getOperatingMode() + if limitToDevices and RemoteDriver.name not in limitToDevices: + return + isSecureDesktop: bool = isRunningOnSecureDesktop() + if isSecureDesktop and operatingMode & configuration.OperatingMode.SECURE_DESKTOP: + sdId = f"NVDA_SD-{driverType.name}" + sdPort = os.path.join(PIPE_DIRECTORY, sdId) + if sdPort in getSecureDesktopNamedPipes(): + yield ( + RemoteDriver.name, + bdDetect.DeviceMatch(type=KEY_NAMED_PIPE_CLIENT, id=sdId, port=sdPort, deviceInfo={}), + ) + if ( + operatingMode & configuration.OperatingMode.SERVER + and not isSecureDesktop + and getRemoteSessionMetrics() == 1 + ): + port = f"NVDA-{driverType.name}" + yield ( + RemoteDriver.name, + bdDetect.DeviceMatch(type=KEY_VIRTUAL_CHANNEL, id=port, port=port, deviceInfo={}), + ) def register(): - bdDetect.scanForDevices.register(bgScanRD) - bdDetect.scanForDevices.moveToEnd(bgScanRD, last=False) + bdDetect.scanForDevices.register(bgScanRD) + bdDetect.scanForDevices.moveToEnd(bgScanRD, last=False) def unregister(): - bdDetect.scanForDevices.unregister(bgScanRD) + bdDetect.scanForDevices.unregister(bgScanRD) diff --git a/addon/lib/driver/__init__.py b/addon/lib/driver/__init__.py index 4ab31cd..3fac8b4 100644 --- a/addon/lib/driver/__init__.py +++ b/addon/lib/driver/__init__.py @@ -6,13 +6,13 @@ import time from abc import abstractmethod from typing import ( - Any, - Iterable, - Iterator, - List, - Optional, - Set, - Union, + Any, + Iterable, + Iterator, + List, + Optional, + Set, + Union, ) import bdDetect @@ -33,180 +33,180 @@ class RemoteDriver(protocol.RemoteProtocolHandler, driverHandler.Driver): - name = "remote" - _settingsAccessor: Optional[SettingsAccessorBase] = None - _isVirtualChannel: bool - _requiredAttributesOnInit: Set[protocol.AttributeT] = {protocol.GenericAttribute.SUPPORTED_SETTINGS} - - @classmethod - def check(cls): - return any(cls._getAutoPorts()) - - @classmethod - def _getAutoPorts(cls, usb=True, bluetooth=True) -> Iterable[bdDetect.DeviceMatch]: - for driver, match in bgScanRD(cls.driverType, [cls.name]): - assert driver == cls.name - yield match - - @classmethod - def _getTryPorts(cls, port: Union[str, bdDetect.DeviceMatch]) -> Iterator[bdDetect.DeviceMatch]: - if isinstance(port, bdDetect.DeviceMatch): - yield port - elif isinstance(port, str): - assert port == "auto" - for match in cls._getAutoPorts(): - yield match - - _localSettings: List[DriverSetting] = [] - - def initSettings(self): - self._initSpecificSettings(self, self._localSettings) - - def loadSettings(self, onlyChanged: bool = False): - self._loadSpecificSettings(self, self._localSettings, onlyChanged) - - def saveSettings(self): - self._saveSpecificSettings(self, self._localSettings) - - def __init__(self, port="auto"): - initialTime = time.perf_counter() - super().__init__() - self._connected = False - for portType, portId, port, portInfo in self._getTryPorts(port): - for attr in self._requiredAttributesOnInit: - self._attributeValueProcessor.setAttributeRequestPending(attr) - try: - if portType == KEY_VIRTUAL_CHANNEL: - self._isVirtualChannel = True - self._dev = wtsVirtualChannel.WTSVirtualChannel( - port, - onReceive=self._onReceive, - onReadError=self._onReadError, - ) - elif portType == KEY_NAMED_PIPE_CLIENT: - self._isVirtualChannel = False - self._dev = namedPipe.NamedPipeClient( - port, - onReceive=self._onReceive, - onReadError=self._onReadError, - ) - except EnvironmentError: - log.debugWarning("", exc_info=True) - continue - if portType == KEY_VIRTUAL_CHANNEL: - # Wait for RdPipe at the other end to send a XON - if not self._safeWait(lambda: self._connected, self.timeout * 3): - continue - else: - self._connected = True - handledAttributes = set() - for attr in self._requiredAttributesOnInit: - if self._waitForAttributeUpdate(attr, initialTime): - handledAttributes.add(attr) - else: - log.debugWarning(f"Error getting {attr}") - - else: - if handledAttributes == self._requiredAttributesOnInit: - log.debug("Required attributes received") - break - - self._dev.close() - else: - raise RuntimeError("No remote device found") - - if not isRunningOnSecureDesktop(): - post_sessionLockStateChanged.register(self._handlePossibleSessionDisconnect) - secureDesktop.post_secureDesktopStateChange.register(self._handlePossibleSessionDisconnect) - - def terminate(self): - if not isRunningOnSecureDesktop(): - secureDesktop.post_secureDesktopStateChange.unregister(self._handlePossibleSessionDisconnect) - post_sessionLockStateChanged.unregister(self._handlePossibleSessionDisconnect) - super().terminate() - - def __getattribute__(self, name: str) -> Any: - getter = super().__getattribute__ - if (name.startswith("_") and not name.startswith("_get_")) or name in ( - n for n in dir(AutoPropertyObject) if not n.startswith("_") - ): - return getter(name) - accessor = getter("_settingsAccessor") - if accessor and name in dir(accessor): - return getattr(accessor, name) - return getter(name) - - def __setattr__(self, name: str, value: Any) -> None: - getter = super().__getattribute__ - accessor = getter("_settingsAccessor") - if accessor and getter("isSupported")(name): - setattr(accessor, name, value) - super().__setattr__(name, value) - - def _onReadError(self, error: int) -> bool: - if error in (ERROR_PIPE_NOT_CONNECTED, ERROR_INVALID_HANDLE): - self._handleRemoteDisconnect() - return True - return False - - @abstractmethod - def _handleRemoteDisconnect(self): - return - - def _onReceive(self, message: bytes): - if self._isVirtualChannel and len(message) == 1: - command = message[0] - if command == MSG_XON: - self._connected = True - return - elif command == MSG_XOFF: - log.debugWarning("MSG_XOFF message received, connection closed") - self._handleRemoteDisconnect() - return - super()._onReceive(message) - - @protocol.attributeReceiver(protocol.GenericAttribute.SUPPORTED_SETTINGS, defaultValue=[]) - def _incomingSupportedSettings(self, payLoad: bytes): - assert len(payLoad) > 0 - settings = self._unpickle(payLoad) - for s in settings: - s.useConfig = False - return settings - - @_incomingSupportedSettings.updateCallback - def _updateCallback_supportedSettings( - self, attribute: protocol.AttributeT, settings: Iterable[DriverSetting] - ): - log.debug(f"Initializing settings accessor for {len(settings)} settings") - self._settingsAccessor = SettingsAccessorBase.createFromSettings(self, settings) if settings else None - self._handleRemoteDriverChange() - - def _handleRemoteDriverChange(self): - log.debug("Handling remote driver change") - - def _get_supportedSettings(self): - settings = [] - settings.extend(self._localSettings) - attribute = protocol.GenericAttribute.SUPPORTED_SETTINGS - try: - settings.extend(self._attributeValueProcessor.getValue(attribute, fallBackToDefault=False)) - except KeyError: - settings.extend(self.getRemoteAttribute(attribute)) - return settings - - @protocol.attributeReceiver(protocol.SETTING_ATTRIBUTE_PREFIX + b"*") - def _incoming_setting(self, attribute: protocol.AttributeT, payLoad: bytes): - assert len(payLoad) > 0 - return self._unpickle(payLoad) - - @protocol.attributeReceiver(b"available*s") - def _incoming_availableSettingValues(self, attribute: protocol.AttributeT, payLoad: bytes): - return self._unpickle(payLoad) - - @protocol.attributeSender(protocol.GenericAttribute.TIME_SINCE_INPUT) - def _outgoing_timeSinceInput(self) -> bytes: - return inputTime.getTimeSinceInput().to_bytes(4, sys.byteorder, signed=False) - - def _handlePossibleSessionDisconnect(self, isNowLocked): - if not self.check(): - self._handleRemoteDisconnect() + name = "remote" + _settingsAccessor: Optional[SettingsAccessorBase] = None + _isVirtualChannel: bool + _requiredAttributesOnInit: Set[protocol.AttributeT] = {protocol.GenericAttribute.SUPPORTED_SETTINGS} + + @classmethod + def check(cls): + return any(cls._getAutoPorts()) + + @classmethod + def _getAutoPorts(cls, usb=True, bluetooth=True) -> Iterable[bdDetect.DeviceMatch]: + for driver, match in bgScanRD(cls.driverType, [cls.name]): + assert driver == cls.name + yield match + + @classmethod + def _getTryPorts(cls, port: Union[str, bdDetect.DeviceMatch]) -> Iterator[bdDetect.DeviceMatch]: + if isinstance(port, bdDetect.DeviceMatch): + yield port + elif isinstance(port, str): + assert port == "auto" + for match in cls._getAutoPorts(): + yield match + + _localSettings: List[DriverSetting] = [] + + def initSettings(self): + self._initSpecificSettings(self, self._localSettings) + + def loadSettings(self, onlyChanged: bool = False): + self._loadSpecificSettings(self, self._localSettings, onlyChanged) + + def saveSettings(self): + self._saveSpecificSettings(self, self._localSettings) + + def __init__(self, port="auto"): + initialTime = time.perf_counter() + super().__init__() + self._connected = False + for portType, portId, port, portInfo in self._getTryPorts(port): + for attr in self._requiredAttributesOnInit: + self._attributeValueProcessor.setAttributeRequestPending(attr) + try: + if portType == KEY_VIRTUAL_CHANNEL: + self._isVirtualChannel = True + self._dev = wtsVirtualChannel.WTSVirtualChannel( + port, + onReceive=self._onReceive, + onReadError=self._onReadError, + ) + elif portType == KEY_NAMED_PIPE_CLIENT: + self._isVirtualChannel = False + self._dev = namedPipe.NamedPipeClient( + port, + onReceive=self._onReceive, + onReadError=self._onReadError, + ) + except EnvironmentError: + log.debugWarning("", exc_info=True) + continue + if portType == KEY_VIRTUAL_CHANNEL: + # Wait for RdPipe at the other end to send a XON + if not self._safeWait(lambda: self._connected, self.timeout * 3): + continue + else: + self._connected = True + handledAttributes = set() + for attr in self._requiredAttributesOnInit: + if self._waitForAttributeUpdate(attr, initialTime): + handledAttributes.add(attr) + else: + log.debugWarning(f"Error getting {attr}") + + else: + if handledAttributes == self._requiredAttributesOnInit: + log.debug("Required attributes received") + break + + self._dev.close() + else: + raise RuntimeError("No remote device found") + + if not isRunningOnSecureDesktop(): + post_sessionLockStateChanged.register(self._handlePossibleSessionDisconnect) + secureDesktop.post_secureDesktopStateChange.register(self._handlePossibleSessionDisconnect) + + def terminate(self): + if not isRunningOnSecureDesktop(): + secureDesktop.post_secureDesktopStateChange.unregister(self._handlePossibleSessionDisconnect) + post_sessionLockStateChanged.unregister(self._handlePossibleSessionDisconnect) + super().terminate() + + def __getattribute__(self, name: str) -> Any: + getter = super().__getattribute__ + if (name.startswith("_") and not name.startswith("_get_")) or name in ( + n for n in dir(AutoPropertyObject) if not n.startswith("_") + ): + return getter(name) + accessor = getter("_settingsAccessor") + if accessor and name in dir(accessor): + return getattr(accessor, name) + return getter(name) + + def __setattr__(self, name: str, value: Any) -> None: + getter = super().__getattribute__ + accessor = getter("_settingsAccessor") + if accessor and getter("isSupported")(name): + setattr(accessor, name, value) + super().__setattr__(name, value) + + def _onReadError(self, error: int) -> bool: + if error in (ERROR_PIPE_NOT_CONNECTED, ERROR_INVALID_HANDLE): + self._handleRemoteDisconnect() + return True + return False + + @abstractmethod + def _handleRemoteDisconnect(self): + return + + def _onReceive(self, message: bytes): + if self._isVirtualChannel and len(message) == 1: + command = message[0] + if command == MSG_XON: + self._connected = True + return + elif command == MSG_XOFF: + log.debugWarning("MSG_XOFF message received, connection closed") + self._handleRemoteDisconnect() + return + super()._onReceive(message) + + @protocol.attributeReceiver(protocol.GenericAttribute.SUPPORTED_SETTINGS, defaultValue=[]) + def _incomingSupportedSettings(self, payLoad: bytes): + assert len(payLoad) > 0 + settings = self._unpickle(payLoad) + for s in settings: + s.useConfig = False + return settings + + @_incomingSupportedSettings.updateCallback + def _updateCallback_supportedSettings( + self, attribute: protocol.AttributeT, settings: Iterable[DriverSetting] + ): + log.debug(f"Initializing settings accessor for {len(settings)} settings") + self._settingsAccessor = SettingsAccessorBase.createFromSettings(self, settings) if settings else None + self._handleRemoteDriverChange() + + def _handleRemoteDriverChange(self): + log.debug("Handling remote driver change") + + def _get_supportedSettings(self): + settings = [] + settings.extend(self._localSettings) + attribute = protocol.GenericAttribute.SUPPORTED_SETTINGS + try: + settings.extend(self._attributeValueProcessor.getValue(attribute, fallBackToDefault=False)) + except KeyError: + settings.extend(self.getRemoteAttribute(attribute)) + return settings + + @protocol.attributeReceiver(protocol.SETTING_ATTRIBUTE_PREFIX + b"*") + def _incoming_setting(self, attribute: protocol.AttributeT, payLoad: bytes): + assert len(payLoad) > 0 + return self._unpickle(payLoad) + + @protocol.attributeReceiver(b"available*s") + def _incoming_availableSettingValues(self, attribute: protocol.AttributeT, payLoad: bytes): + return self._unpickle(payLoad) + + @protocol.attributeSender(protocol.GenericAttribute.TIME_SINCE_INPUT) + def _outgoing_timeSinceInput(self) -> bytes: + return inputTime.getTimeSinceInput().to_bytes(4, sys.byteorder, signed=False) + + def _handlePossibleSessionDisconnect(self, isNowLocked): + if not self.check(): + self._handleRemoteDisconnect() diff --git a/addon/lib/driver/settingsAccessor.py b/addon/lib/driver/settingsAccessor.py index fad1622..9e19d0e 100644 --- a/addon/lib/driver/settingsAccessor.py +++ b/addon/lib/driver/settingsAccessor.py @@ -12,113 +12,99 @@ from .. import protocol if TYPE_CHECKING: - from . import RemoteDriver + from . import RemoteDriver class SettingsAccessorBase(AutoPropertyObject): - _driverRef: "weakref.ref[RemoteDriver]" - driver: "RemoteDriver" - _settingNames: List[str] - cachePropertiesByDefault = True - - @classmethod - def createFromSettings( - cls, driver: "RemoteDriver", settings: Iterable[driverSetting.DriverSetting] - ): - dct: Dict[str, Any] = { - "__module__": __name__, - } - settingNames = [] - for s in settings: - settingNames.append(s.id) - dct[f"_get_{s.id}"] = cls._makeGetSetting(s.id) - dct[f"_set_{s.id}"] = cls._makeSetSetting(s.id) - if not isinstance( - s, - ( - driverSetting.BooleanDriverSetting, - driverSetting.NumericDriverSetting, - ), - ): - dct[ - f"_get_{cls._getAvailableSettingsPropertyName(s.id)}" - ] = cls._makeGetAvailableSettings(s.id) - log.debug("Constructed dictionary to generate new dynamic SettingsAccessor") - return type("SettingsAccessor", (SettingsAccessorBase,), dct)( - driver, settingNames - ) - - def _get_driver(self): - return self._driverRef() - - def __init__(self, driver: "RemoteDriver", settingNames: List[str]): - log.debug(f"Initializing {self} for driver {driver}, settings {settingNames}") - self._driverRef = weakref.ref(driver) - self._settingNames = settingNames - for name in self._settingNames: - driver.requestRemoteAttribute(self._getSettingAttributeName(name)) - - @classmethod - def _getSettingAttributeName(cls, setting: str) -> protocol.AttributeT: - return protocol.SETTING_ATTRIBUTE_PREFIX + setting.encode("ASCII") - - @classmethod - def _getAvailableSettingsPropertyName(cls, setting: str) -> str: - return f"available{setting.capitalize()}s" - - @classmethod - def _getAvailableSettingsAttributeName(cls, setting: str) -> protocol.AttributeT: - return cls._getAvailableSettingsPropertyName(setting).encode("ASCII") - - @classmethod - def _makeGetSetting(cls, setting: str): - def _getSetting(self: SettingsAccessorBase): - log.debug(f"Getting value for setting {setting}") - attribute = self._getSettingAttributeName(setting) - value = self.driver._attributeValueProcessor.getValue( - attribute, fallBackToDefault=True - ) - self.driver._queueFunctionOnMainThread( - self.driver.requestRemoteAttribute, attribute - ) - return value - - return _getSetting - - @classmethod - def _makeSetSetting(cls, setting: str): - def _setSetting(self: SettingsAccessorBase, value: Any): - log.debug(f"Setting value for setting {setting} to {value}") - attribute = self._getSettingAttributeName(setting) - self.driver.setRemoteAttribute(attribute, self.driver._pickle(value)) - if self.driver._attributeValueProcessor.isAttributeSupported(attribute): - self.driver._attributeValueProcessor.setValue(attribute, value) - - return _setSetting - - @classmethod - def _makeGetAvailableSettings(cls, setting: str): - def _getAvailableSettings(self: SettingsAccessorBase): - attribute = self._getAvailableSettingsAttributeName(setting) - try: - return self.driver._attributeValueProcessor.getValue( - attribute, fallBackToDefault=False - ) - except KeyError: - return self.driver.getRemoteAttribute(attribute) - - return _getAvailableSettings - - def __del__(self): - if not self.driver: - return - try: - for s in self._settingNames: - self.driver._attributeValueProcessor.clearValue( - self._getSettingAttributeName(s) - ) - self.driver._attributeValueProcessor.clearValue( - self._getAvailableSettingsAttributeName(s) - ) - except Exception: - log.debugWarning(f"Error deleting {self.__class__.__name__}", exc_info=True) + _driverRef: "weakref.ref[RemoteDriver]" + driver: "RemoteDriver" + _settingNames: List[str] + cachePropertiesByDefault = True + + @classmethod + def createFromSettings(cls, driver: "RemoteDriver", settings: Iterable[driverSetting.DriverSetting]): + dct: Dict[str, Any] = { + "__module__": __name__, + } + settingNames = [] + for s in settings: + settingNames.append(s.id) + dct[f"_get_{s.id}"] = cls._makeGetSetting(s.id) + dct[f"_set_{s.id}"] = cls._makeSetSetting(s.id) + if not isinstance( + s, + ( + driverSetting.BooleanDriverSetting, + driverSetting.NumericDriverSetting, + ), + ): + dct[f"_get_{cls._getAvailableSettingsPropertyName(s.id)}"] = cls._makeGetAvailableSettings( + s.id + ) + log.debug("Constructed dictionary to generate new dynamic SettingsAccessor") + return type("SettingsAccessor", (SettingsAccessorBase,), dct)(driver, settingNames) + + def _get_driver(self): + return self._driverRef() + + def __init__(self, driver: "RemoteDriver", settingNames: List[str]): + log.debug(f"Initializing {self} for driver {driver}, settings {settingNames}") + self._driverRef = weakref.ref(driver) + self._settingNames = settingNames + for name in self._settingNames: + driver.requestRemoteAttribute(self._getSettingAttributeName(name)) + + @classmethod + def _getSettingAttributeName(cls, setting: str) -> protocol.AttributeT: + return protocol.SETTING_ATTRIBUTE_PREFIX + setting.encode("ASCII") + + @classmethod + def _getAvailableSettingsPropertyName(cls, setting: str) -> str: + return f"available{setting.capitalize()}s" + + @classmethod + def _getAvailableSettingsAttributeName(cls, setting: str) -> protocol.AttributeT: + return cls._getAvailableSettingsPropertyName(setting).encode("ASCII") + + @classmethod + def _makeGetSetting(cls, setting: str): + def _getSetting(self: SettingsAccessorBase): + log.debug(f"Getting value for setting {setting}") + attribute = self._getSettingAttributeName(setting) + value = self.driver._attributeValueProcessor.getValue(attribute, fallBackToDefault=True) + self.driver._queueFunctionOnMainThread(self.driver.requestRemoteAttribute, attribute) + return value + + return _getSetting + + @classmethod + def _makeSetSetting(cls, setting: str): + def _setSetting(self: SettingsAccessorBase, value: Any): + log.debug(f"Setting value for setting {setting} to {value}") + attribute = self._getSettingAttributeName(setting) + self.driver.setRemoteAttribute(attribute, self.driver._pickle(value)) + if self.driver._attributeValueProcessor.isAttributeSupported(attribute): + self.driver._attributeValueProcessor.setValue(attribute, value) + + return _setSetting + + @classmethod + def _makeGetAvailableSettings(cls, setting: str): + def _getAvailableSettings(self: SettingsAccessorBase): + attribute = self._getAvailableSettingsAttributeName(setting) + try: + return self.driver._attributeValueProcessor.getValue(attribute, fallBackToDefault=False) + except KeyError: + return self.driver.getRemoteAttribute(attribute) + + return _getAvailableSettings + + def __del__(self): + if not self.driver: + return + try: + for s in self._settingNames: + self.driver._attributeValueProcessor.clearValue(self._getSettingAttributeName(s)) + self.driver._attributeValueProcessor.clearValue(self._getAvailableSettingsAttributeName(s)) + except Exception: + log.debugWarning(f"Error deleting {self.__class__.__name__}", exc_info=True) diff --git a/addon/lib/inputTime.py b/addon/lib/inputTime.py index 64334f7..d84267d 100644 --- a/addon/lib/inputTime.py +++ b/addon/lib/inputTime.py @@ -7,29 +7,29 @@ class LastINPUTINFO(Structure): - _fields_ = [ - ("cbSize", UINT), - ("dwTime", DWORD), - ] + _fields_ = [ + ("cbSize", UINT), + ("dwTime", DWORD), + ] - def __init__(self): - super().__init__ - self.cbSize = sizeof(LastINPUTINFO) + def __init__(self): + super().__init__ + self.cbSize = sizeof(LastINPUTINFO) def getLastInputInfo() -> int: - lastINPUTINFO = LastINPUTINFO() - if not windll.user32.GetLastInputInfo(byref(lastINPUTINFO)): - raise WinError() - return lastINPUTINFO.dwTime + lastINPUTINFO = LastINPUTINFO() + if not windll.user32.GetLastInputInfo(byref(lastINPUTINFO)): + raise WinError() + return lastINPUTINFO.dwTime windll.kernel32.GetTickCount.restype = DWORD def getTickCount() -> int: - return windll.kernel32.GetTickCount() + return windll.kernel32.GetTickCount() def getTimeSinceInput() -> int: - return getTickCount() - getLastInputInfo() + return getTickCount() - getLastInputInfo() diff --git a/addon/lib/ioThreadEx.py b/addon/lib/ioThreadEx.py index fefe634..517ea34 100644 --- a/addon/lib/ioThreadEx.py +++ b/addon/lib/ioThreadEx.py @@ -21,104 +21,104 @@ WaitOrTimerCallbackIdT = int WaitOrTimerCallbackT = Callable[[int, bool], None] WaitOrTimerCallbackStoreT = Dict[ - WaitOrTimerCallbackIdT, - Tuple[ - int, - HANDLE, - Union[ - BoundMethodWeakref[WaitOrTimerCallbackT], - AnnotatableWeakref[WaitOrTimerCallbackT], - ], - WaitOrTimerCallbackIdT, - ], + WaitOrTimerCallbackIdT, + Tuple[ + int, + HANDLE, + Union[ + BoundMethodWeakref[WaitOrTimerCallbackT], + AnnotatableWeakref[WaitOrTimerCallbackT], + ], + WaitOrTimerCallbackIdT, + ], ] windll.kernel32.RegisterWaitForSingleObject.restype = BOOL windll.kernel32.RegisterWaitForSingleObject.argtypes = ( - POINTER(HANDLE), - HANDLE, - WaitOrTimerCallback, - LPVOID, - DWORD, - DWORD, + POINTER(HANDLE), + HANDLE, + WaitOrTimerCallback, + LPVOID, + DWORD, + DWORD, ) class IoThreadEx(hwIo.ioThread.IoThread): - _waitOrTimerCallbackStore: WaitOrTimerCallbackStoreT = {} + _waitOrTimerCallbackStore: WaitOrTimerCallbackStoreT = {} - @WaitOrTimerCallback - def _internalWaitOrTimerCallback(param: WaitOrTimerCallbackIdT, timerOrWaitFired: bool): - ( - threadIdent, - waitObject, - reference, - actualParam, - ) = IoThreadEx._waitOrTimerCallbackStore.pop(param, (0, 0, None, 0)) - threadInst: IoThreadEx = threading._active.get(threadIdent) - if not isinstance(threadInst, IoThreadEx): - log.error("Internal WaitOrTimerCallback called from unknown thread") - return + @WaitOrTimerCallback + def _internalWaitOrTimerCallback(param: WaitOrTimerCallbackIdT, timerOrWaitFired: bool): + ( + threadIdent, + waitObject, + reference, + actualParam, + ) = IoThreadEx._waitOrTimerCallbackStore.pop(param, (0, 0, None, 0)) + threadInst: IoThreadEx = threading._active.get(threadIdent) + if not isinstance(threadInst, IoThreadEx): + log.error("Internal WaitOrTimerCallback called from unknown thread") + return - if reference is None: - log.error( - f"Internal WaitOrTimerCallback called with param {param}, but no such wait object in store" - ) - return - function = reference() - if not function: - log.debugWarning( - f"Not executing queued WaitOrTimerCallback {param}:{reference.funcName} " - f"with param {actualParam} " - "because reference died" - ) - return + if reference is None: + log.error( + f"Internal WaitOrTimerCallback called with param {param}, but no such wait object in store" + ) + return + function = reference() + if not function: + log.debugWarning( + f"Not executing queued WaitOrTimerCallback {param}:{reference.funcName} " + f"with param {actualParam} " + "because reference died" + ) + return - try: - function(actualParam, bool(timerOrWaitFired)) - except Exception: - log.error( - f"Error in WaitOrTimerCallback function {function!r} with id {param} queued to IoThread", - exc_info=True, - ) - finally: - threadInst.queueAsApc(threadInst._postWaitOrTimerCallback, waitObject) + try: + function(actualParam, bool(timerOrWaitFired)) + except Exception: + log.error( + f"Error in WaitOrTimerCallback function {function!r} with id {param} queued to IoThread", + exc_info=True, + ) + finally: + threadInst.queueAsApc(threadInst._postWaitOrTimerCallback, waitObject) - @staticmethod - def _postWaitOrTimerCallback(waitObject): - windll.kernel32.UnregisterWaitEx(waitObject, INVALID_HANDLE_VALUE) + @staticmethod + def _postWaitOrTimerCallback(waitObject): + windll.kernel32.UnregisterWaitEx(waitObject, INVALID_HANDLE_VALUE) - def waitForSingleObjectWithCallback( - self, - objectHandle: Union[HANDLE, int], - func: WaitOrTimerCallback, - param=0, - flags=WT_EXECUTELONGFUNCTION | WT_EXECUTEONLYONCE, - waitTime=winKernel.INFINITE, - ): - if not self.is_alive(): - raise RuntimeError("Thread is not running") + def waitForSingleObjectWithCallback( + self, + objectHandle: Union[HANDLE, int], + func: WaitOrTimerCallback, + param=0, + flags=WT_EXECUTELONGFUNCTION | WT_EXECUTEONLYONCE, + waitTime=winKernel.INFINITE, + ): + if not self.is_alive(): + raise RuntimeError("Thread is not running") - waitObject = HANDLE() - reference = BoundMethodWeakref(func) if ismethod(func) else AnnotatableWeakref(func) - reference.funcName = repr(func) - waitObjectAddr = addressof(waitObject) - self._waitOrTimerCallbackStore[waitObjectAddr] = ( - self.ident, - waitObject, - reference, - param, - ) - try: - waitRes = windll.kernel32.RegisterWaitForSingleObject( - byref(waitObject), - objectHandle, - self._internalWaitOrTimerCallback, - waitObjectAddr, - waitTime, - flags, - ) - if not waitRes: - raise WinError() - except Exception: - del self._waitOrTimerCallbackStore[waitObjectAddr] - raise + waitObject = HANDLE() + reference = BoundMethodWeakref(func) if ismethod(func) else AnnotatableWeakref(func) + reference.funcName = repr(func) + waitObjectAddr = addressof(waitObject) + self._waitOrTimerCallbackStore[waitObjectAddr] = ( + self.ident, + waitObject, + reference, + param, + ) + try: + waitRes = windll.kernel32.RegisterWaitForSingleObject( + byref(waitObject), + objectHandle, + self._internalWaitOrTimerCallback, + waitObjectAddr, + waitTime, + flags, + ) + if not waitRes: + raise WinError() + except Exception: + del self._waitOrTimerCallbackStore[waitObjectAddr] + raise diff --git a/addon/lib/namedPipe.py b/addon/lib/namedPipe.py index 62af852..84dfa59 100644 --- a/addon/lib/namedPipe.py +++ b/addon/lib/namedPipe.py @@ -4,13 +4,13 @@ import os from ctypes import ( - POINTER, - GetLastError, - WinError, - byref, - c_ulong, - sizeof, - windll, + POINTER, + GetLastError, + WinError, + byref, + c_ulong, + sizeof, + windll, ) from ctypes.wintypes import BOOL, DWORD, HANDLE, LPCWSTR from enum import IntFlag @@ -23,12 +23,12 @@ from hwIo.ioThread import IoThread from logHandler import log from serial.win32 import ( - ERROR_IO_PENDING, - FILE_FLAG_OVERLAPPED, - INVALID_HANDLE_VALUE, - LPOVERLAPPED, - OVERLAPPED, - CreateFile, + ERROR_IO_PENDING, + FILE_FLAG_OVERLAPPED, + INVALID_HANDLE_VALUE, + LPOVERLAPPED, + OVERLAPPED, + CreateFile, ) from .ioThreadEx import IoThreadEx @@ -42,14 +42,14 @@ TH32CS_SNAPPROCESS = 0x00000002 windll.kernel32.CreateNamedPipeW.restype = HANDLE windll.kernel32.CreateNamedPipeW.argtypes = ( - LPCWSTR, - DWORD, - DWORD, - DWORD, - DWORD, - DWORD, - DWORD, - POINTER(winKernel.SECURITY_ATTRIBUTES), + LPCWSTR, + DWORD, + DWORD, + DWORD, + DWORD, + DWORD, + DWORD, + POINTER(winKernel.SECURITY_ATTRIBUTES), ) windll.kernel32.ConnectNamedPipe.restype = BOOL windll.kernel32.ConnectNamedPipe.argtypes = (HANDLE, LPOVERLAPPED) @@ -58,262 +58,262 @@ def getParentProcessId(processId: int) -> Optional[int]: - FSnapshotHandle = windll.kernel32.CreateToolhelp32Snapshot(TH32CS_SNAPPROCESS, 0) - try: - FProcessEntry32 = processEntry32W() - FProcessEntry32.dwSize = sizeof(processEntry32W) - ContinueLoop = windll.kernel32.Process32FirstW(FSnapshotHandle, byref(FProcessEntry32)) - while ContinueLoop: - if FProcessEntry32.th32ProcessID == processId: - return FProcessEntry32.th32ParentProcessID - ContinueLoop = windll.kernel32.Process32NextW(FSnapshotHandle, byref(FProcessEntry32)) - else: - return None - finally: - windll.kernel32.CloseHandle(FSnapshotHandle) + FSnapshotHandle = windll.kernel32.CreateToolhelp32Snapshot(TH32CS_SNAPPROCESS, 0) + try: + FProcessEntry32 = processEntry32W() + FProcessEntry32.dwSize = sizeof(processEntry32W) + ContinueLoop = windll.kernel32.Process32FirstW(FSnapshotHandle, byref(FProcessEntry32)) + while ContinueLoop: + if FProcessEntry32.th32ProcessID == processId: + return FProcessEntry32.th32ParentProcessID + ContinueLoop = windll.kernel32.Process32NextW(FSnapshotHandle, byref(FProcessEntry32)) + else: + return None + finally: + windll.kernel32.CloseHandle(FSnapshotHandle) def getNamedPipes() -> Iterator[str]: - yield from iglob(os.path.join(PIPE_DIRECTORY, "*")) + yield from iglob(os.path.join(PIPE_DIRECTORY, "*")) def getRdPipeNamedPipes() -> Iterator[str]: - yield from iglob(RD_PIPE_GLOB_PATTERN) + yield from iglob(RD_PIPE_GLOB_PATTERN) def getSecureDesktopNamedPipes() -> Iterator[str]: - yield from iglob(SECURE_DESKTOP_GLOB_PATTERN) + yield from iglob(SECURE_DESKTOP_GLOB_PATTERN) class PipeMode(IntFlag): - READMODE_BYTE = 0x00000000 - READMODE_MESSAGE = 0x00000002 - WAIT = 0x00000000 - NOWAIT = 0x00000001 + READMODE_BYTE = 0x00000000 + READMODE_MESSAGE = 0x00000002 + WAIT = 0x00000000 + NOWAIT = 0x00000001 class PipeOpenMode(IntFlag): - ACCESS_DUPLEX = 0x00000003 - ACCESS_INBOUND = 0x00000001 - ACCESS_OUTBOUND = 0x00000002 - FIRST_PIPE_INSTANCE = 0x00080000 - WRITE_THROUGH = 0x80000000 - OVERLAPPED = FILE_FLAG_OVERLAPPED - WRITE_DAC = 0x00040000 - WRITE_OWNER = 0x00080000 - ACCESS_SYSTEM_SECURITY = 0x01000000 + ACCESS_DUPLEX = 0x00000003 + ACCESS_INBOUND = 0x00000001 + ACCESS_OUTBOUND = 0x00000002 + FIRST_PIPE_INSTANCE = 0x00080000 + WRITE_THROUGH = 0x80000000 + OVERLAPPED = FILE_FLAG_OVERLAPPED + WRITE_DAC = 0x00040000 + WRITE_OWNER = 0x00080000 + ACCESS_SYSTEM_SECURITY = 0x01000000 MAX_PIPE_MESSAGE_SIZE = 1024 * 64 class NamedPipeBase(IoBase): - pipeProcessId: Optional[int] = None - pipeParentProcessId: Optional[int] = None - pipeMode: PipeMode = PipeMode.READMODE_BYTE | PipeMode.WAIT - pipeName: str - - def __init__( - self, - pipeName: str, - fileHandle: Union[HANDLE, int], - onReceive: Callable[[bytes], None], - onReceiveSize: int = MAX_PIPE_MESSAGE_SIZE, - onReadError: Optional[Callable[[int], bool]] = None, - ioThread: Optional[IoThread] = None, - pipeMode: PipeMode = PipeMode.READMODE_BYTE, - ): - self.pipeName = pipeName - super().__init__( - fileHandle, - onReceive, - onReceiveSize=onReceiveSize, - onReadError=onReadError, - ioThread=ioThread, - ) - - def _get_isAlive(self) -> bool: - return self.pipeName in getNamedPipes() + pipeProcessId: Optional[int] = None + pipeParentProcessId: Optional[int] = None + pipeMode: PipeMode = PipeMode.READMODE_BYTE | PipeMode.WAIT + pipeName: str + + def __init__( + self, + pipeName: str, + fileHandle: Union[HANDLE, int], + onReceive: Callable[[bytes], None], + onReceiveSize: int = MAX_PIPE_MESSAGE_SIZE, + onReadError: Optional[Callable[[int], bool]] = None, + ioThread: Optional[IoThread] = None, + pipeMode: PipeMode = PipeMode.READMODE_BYTE, + ): + self.pipeName = pipeName + super().__init__( + fileHandle, + onReceive, + onReceiveSize=onReceiveSize, + onReadError=onReadError, + ioThread=ioThread, + ) + + def _get_isAlive(self) -> bool: + return self.pipeName in getNamedPipes() class NamedPipeServer(NamedPipeBase): - _connected: bool = False - _onConnected: Optional[Callable[[bool], None]] = None - _waitObject: Optional[HANDLE] = None - _connectOl: Optional[OVERLAPPED] = None - - def __init__( - self, - pipeName: str, - onReceive: Callable[[bytes], None], - onReceiveSize: int = MAX_PIPE_MESSAGE_SIZE, - onConnected: Optional[Callable[[bool], None]] = None, - ioThreadEx: Optional[IoThreadEx] = None, - pipeMode: PipeMode = PipeMode.READMODE_BYTE, - pipeOpenMode: PipeOpenMode = ( - PipeOpenMode.ACCESS_DUPLEX | PipeOpenMode.OVERLAPPED | PipeOpenMode.FIRST_PIPE_INSTANCE - ), - maxInstances: int = 1, - ): - log.debug(f"Initializing named pipe: Name={pipeName}") - fileHandle = windll.kernel32.CreateNamedPipeW( - pipeName, - pipeOpenMode, - pipeMode, - maxInstances, - onReceiveSize, - onReceiveSize, - 0, - None, - ) - if fileHandle == INVALID_HANDLE_VALUE: - raise WinError() - log.debug(f"Initialized named pipe: Name={pipeName}, handle={fileHandle}") - self._onConnected = onConnected - super().__init__( - pipeName, - fileHandle, - onReceive, - onReadError=self._onReadError, - ioThread=ioThreadEx, - pipeMode=pipeMode, - ) - - def _handleConnect(self): - self._connectOl = ol = OVERLAPPED() - ol.hEvent = self._recvEvt - log.debug(f"Connecting server end of named pipe: Name={self.pipeName}") - connectRes = windll.kernel32.ConnectNamedPipe(self._file, byref(ol)) - error: int = GetLastError() - if error == ERROR_PIPE_CONNECTED: - log.debug(f"Server end of named pipe {self.pipeName} already connected") - windll.kernel32.SetEvent(self._recvEvt) - else: - if not connectRes and error != ERROR_IO_PENDING: - log.error(f"Error while calling ConnectNamedPipe for {self.pipeName}: {WinError(error)}") - self._ioDone(error, 0, byref(ol)) - return - log.debug(f"Named pipe {self.pipeName} pending client connection") - try: - self._ioThreadRef().waitForSingleObjectWithCallback(self._recvEvt, self._handleConnectCallback) - except WindowsError as e: - error = e.winerror - log.error( - f"Error while calling RegisterWaitForSingleObject for {self.pipeName}: {WinError(error)}" - ) - self._ioDone(error, 0, byref(ol)) - - def _handleConnectCallback(self, parameter: int, timerOrWaitFired: bool): - log.debug(f"Event set for {self.pipeName}") - numberOfBytes = DWORD() - log.debug(f"Getting overlapped result for {self.pipeName} after wait for event") - if not windll.kernel32.GetOverlappedResult( - self._file, byref(self._connectOl), byref(numberOfBytes), False - ): - error = GetLastError() - log.debug(f"Error while getting overlapped result for {self.pipeName}: {WinError(error)}") - self._ioDone(error, 0, byref(self._connectOl)) - return - self._connected = True - log.debug("Succesfully connected {self.pipeName}, handling post connection logic") - clientProcessId = c_ulong() - if not windll.kernel32.GetNamedPipeClientProcessId(HANDLE(self._file), byref(clientProcessId)): - raise WinError() - self.pipeProcessId = clientProcessId.value - self.pipeParentProcessId = getParentProcessId(self.pipeProcessId) - self._initialRead() - if self._onConnected is not None: - self._onConnected(True) - log.debug("End of handleConnectCallback for {self.pipeName}") - self._connectOl = None - - def _onReadError(self, error: int): - winErr = WinError(error) - log.debug(f"Read error: {winErr}") - if isinstance(winErr, BrokenPipeError): - self.disconnect() - self._initialRead() - return True - return False - - def _asyncRead(self, param: Optional[int] = None): - if not self._connected: - # _handleConnect will call _asyncRead when it is finished. - self._handleConnect() - else: - super()._asyncRead() - - def disconnect(self): - if not windll.kernel32.DisconnectNamedPipe(self._file): - raise WinError() - self._connected = False - self.pipeProcessId = None - self.pipeParentProcessId = None - if self._onConnected: - self._onConnected(False) - - def close(self): - super().close() - if hasattr(self, "_file") and self._file is not INVALID_HANDLE_VALUE: - self.disconnect() - self._onConnected = None - winKernel.closeHandle(self._file) - self._file = INVALID_HANDLE_VALUE - - @property - def _ioDone(self): - return super()._ioDone - - @_ioDone.setter - def _ioDone(self, value): - """Hack, we don't want _ioDone to set itself to None.""" - pass + _connected: bool = False + _onConnected: Optional[Callable[[bool], None]] = None + _waitObject: Optional[HANDLE] = None + _connectOl: Optional[OVERLAPPED] = None + + def __init__( + self, + pipeName: str, + onReceive: Callable[[bytes], None], + onReceiveSize: int = MAX_PIPE_MESSAGE_SIZE, + onConnected: Optional[Callable[[bool], None]] = None, + ioThreadEx: Optional[IoThreadEx] = None, + pipeMode: PipeMode = PipeMode.READMODE_BYTE, + pipeOpenMode: PipeOpenMode = ( + PipeOpenMode.ACCESS_DUPLEX | PipeOpenMode.OVERLAPPED | PipeOpenMode.FIRST_PIPE_INSTANCE + ), + maxInstances: int = 1, + ): + log.debug(f"Initializing named pipe: Name={pipeName}") + fileHandle = windll.kernel32.CreateNamedPipeW( + pipeName, + pipeOpenMode, + pipeMode, + maxInstances, + onReceiveSize, + onReceiveSize, + 0, + None, + ) + if fileHandle == INVALID_HANDLE_VALUE: + raise WinError() + log.debug(f"Initialized named pipe: Name={pipeName}, handle={fileHandle}") + self._onConnected = onConnected + super().__init__( + pipeName, + fileHandle, + onReceive, + onReadError=self._onReadError, + ioThread=ioThreadEx, + pipeMode=pipeMode, + ) + + def _handleConnect(self): + self._connectOl = ol = OVERLAPPED() + ol.hEvent = self._recvEvt + log.debug(f"Connecting server end of named pipe: Name={self.pipeName}") + connectRes = windll.kernel32.ConnectNamedPipe(self._file, byref(ol)) + error: int = GetLastError() + if error == ERROR_PIPE_CONNECTED: + log.debug(f"Server end of named pipe {self.pipeName} already connected") + windll.kernel32.SetEvent(self._recvEvt) + else: + if not connectRes and error != ERROR_IO_PENDING: + log.error(f"Error while calling ConnectNamedPipe for {self.pipeName}: {WinError(error)}") + self._ioDone(error, 0, byref(ol)) + return + log.debug(f"Named pipe {self.pipeName} pending client connection") + try: + self._ioThreadRef().waitForSingleObjectWithCallback(self._recvEvt, self._handleConnectCallback) + except WindowsError as e: + error = e.winerror + log.error( + f"Error while calling RegisterWaitForSingleObject for {self.pipeName}: {WinError(error)}" + ) + self._ioDone(error, 0, byref(ol)) + + def _handleConnectCallback(self, parameter: int, timerOrWaitFired: bool): + log.debug(f"Event set for {self.pipeName}") + numberOfBytes = DWORD() + log.debug(f"Getting overlapped result for {self.pipeName} after wait for event") + if not windll.kernel32.GetOverlappedResult( + self._file, byref(self._connectOl), byref(numberOfBytes), False + ): + error = GetLastError() + log.debug(f"Error while getting overlapped result for {self.pipeName}: {WinError(error)}") + self._ioDone(error, 0, byref(self._connectOl)) + return + self._connected = True + log.debug("Succesfully connected {self.pipeName}, handling post connection logic") + clientProcessId = c_ulong() + if not windll.kernel32.GetNamedPipeClientProcessId(HANDLE(self._file), byref(clientProcessId)): + raise WinError() + self.pipeProcessId = clientProcessId.value + self.pipeParentProcessId = getParentProcessId(self.pipeProcessId) + self._initialRead() + if self._onConnected is not None: + self._onConnected(True) + log.debug("End of handleConnectCallback for {self.pipeName}") + self._connectOl = None + + def _onReadError(self, error: int): + winErr = WinError(error) + log.debug(f"Read error: {winErr}") + if isinstance(winErr, BrokenPipeError): + self.disconnect() + self._initialRead() + return True + return False + + def _asyncRead(self, param: Optional[int] = None): + if not self._connected: + # _handleConnect will call _asyncRead when it is finished. + self._handleConnect() + else: + super()._asyncRead() + + def disconnect(self): + if not windll.kernel32.DisconnectNamedPipe(self._file): + raise WinError() + self._connected = False + self.pipeProcessId = None + self.pipeParentProcessId = None + if self._onConnected: + self._onConnected(False) + + def close(self): + super().close() + if hasattr(self, "_file") and self._file is not INVALID_HANDLE_VALUE: + self.disconnect() + self._onConnected = None + winKernel.closeHandle(self._file) + self._file = INVALID_HANDLE_VALUE + + @property + def _ioDone(self): + return super()._ioDone + + @_ioDone.setter + def _ioDone(self, value): + """Hack, we don't want _ioDone to set itself to None.""" + pass class NamedPipeClient(NamedPipeBase): - def __init__( - self, - pipeName: str, - onReceive: Callable[[bytes], None], - onReadError: Optional[Callable[[int], bool]] = None, - ioThread: Optional[IoThread] = None, - pipeMode: PipeMode = PipeMode.READMODE_BYTE, - ): - fileHandle = CreateFile( - pipeName, - winKernel.GENERIC_READ | winKernel.GENERIC_WRITE, - 0, - None, - winKernel.OPEN_EXISTING, - FILE_FLAG_OVERLAPPED, - None, - ) - if fileHandle == INVALID_HANDLE_VALUE: - raise WinError() - try: - if pipeMode: - dwPipeMode = DWORD(pipeMode) - if not windll.kernel32.SetNamedPipeHandleState(fileHandle, byref(dwPipeMode), 0, 0): - raise WinError() - serverProcessId = c_ulong() - if not windll.kernel32.GetNamedPipeServerProcessId(HANDLE(fileHandle), byref(serverProcessId)): - raise WinError() - self.pipeProcessId = serverProcessId.value - self.pipeParentProcessId = getParentProcessId(self.pipeProcessId) - except Exception: - winKernel.closeHandle(fileHandle) - raise - super().__init__( - pipeName, - fileHandle, - onReceive, - onReadError=onReadError, - ioThread=ioThread, - pipeMode=pipeMode, - ) - - def close(self): - super().close() - if hasattr(self, "_file") and self._file is not INVALID_HANDLE_VALUE: - winKernel.closeHandle(self._file) - self._file = INVALID_HANDLE_VALUE + def __init__( + self, + pipeName: str, + onReceive: Callable[[bytes], None], + onReadError: Optional[Callable[[int], bool]] = None, + ioThread: Optional[IoThread] = None, + pipeMode: PipeMode = PipeMode.READMODE_BYTE, + ): + fileHandle = CreateFile( + pipeName, + winKernel.GENERIC_READ | winKernel.GENERIC_WRITE, + 0, + None, + winKernel.OPEN_EXISTING, + FILE_FLAG_OVERLAPPED, + None, + ) + if fileHandle == INVALID_HANDLE_VALUE: + raise WinError() + try: + if pipeMode: + dwPipeMode = DWORD(pipeMode) + if not windll.kernel32.SetNamedPipeHandleState(fileHandle, byref(dwPipeMode), 0, 0): + raise WinError() + serverProcessId = c_ulong() + if not windll.kernel32.GetNamedPipeServerProcessId(HANDLE(fileHandle), byref(serverProcessId)): + raise WinError() + self.pipeProcessId = serverProcessId.value + self.pipeParentProcessId = getParentProcessId(self.pipeProcessId) + except Exception: + winKernel.closeHandle(fileHandle) + raise + super().__init__( + pipeName, + fileHandle, + onReceive, + onReadError=onReadError, + ioThread=ioThread, + pipeMode=pipeMode, + ) + + def close(self): + super().close() + if hasattr(self, "_file") and self._file is not INVALID_HANDLE_VALUE: + winKernel.closeHandle(self._file) + self._file = INVALID_HANDLE_VALUE diff --git a/addon/lib/protocol/__init__.py b/addon/lib/protocol/__init__.py index fffd684..e591749 100644 --- a/addon/lib/protocol/__init__.py +++ b/addon/lib/protocol/__init__.py @@ -13,15 +13,15 @@ from fnmatch import fnmatch from functools import partial, update_wrapper, wraps from typing import ( - Any, - Callable, - DefaultDict, - Dict, - Generic, - Optional, - TypeVar, - Union, - cast, + Any, + Callable, + DefaultDict, + Dict, + Generic, + Optional, + TypeVar, + Union, + cast, ) import queueHandler @@ -38,17 +38,17 @@ class DriverType(IntEnum): - SPEECH = ord(b"S") - BRAILLE = ord(b"B") + SPEECH = ord(b"S") + BRAILLE = ord(b"B") class GenericCommand(IntEnum): - ATTRIBUTE = ord(b"@") + ATTRIBUTE = ord(b"@") class GenericAttribute(bytes, Enum): - TIME_SINCE_INPUT = b"timeSinceInput" - SUPPORTED_SETTINGS = b"supportedSettings" + TIME_SINCE_INPUT = b"timeSinceInput" + SUPPORTED_SETTINGS = b"supportedSettings" RemoteProtocolHandlerT = TypeVar("RemoteProtocolHandlerT", bound="RemoteProtocolHandler") @@ -65,432 +65,432 @@ class GenericAttribute(bytes, Enum): WildCardAttributeReceiverT = Callable[[AttributeT, bytes], AttributeValueT] WildCardAttributeReceiverUnboundT = Callable[[RemoteProtocolHandlerT, AttributeT, bytes], AttributeValueT] AttributeHandlerT = TypeVar( - "AttributeHandlerT", - attributeFetcherT, - AttributeReceiverUnboundT, - WildCardAttributeReceiverUnboundT, + "AttributeHandlerT", + attributeFetcherT, + AttributeReceiverUnboundT, + WildCardAttributeReceiverUnboundT, ) DefaultValueGetterT = Callable[[RemoteProtocolHandlerT, AttributeT], AttributeValueT] AttributeValueUpdateCallbackT = Callable[[RemoteProtocolHandlerT, AttributeT, AttributeValueT], None] class HandlerDecoratorBase(Generic[HandlerFuncT]): - _func: HandlerFuncT + _func: HandlerFuncT - def __init__(self, func: HandlerFuncT): - self._func = func - update_wrapper(self, func, assigned=("__module__", "__name__", "__qualname__", "__doc__")) + def __init__(self, func: HandlerFuncT): + self._func = func + update_wrapper(self, func, assigned=("__module__", "__name__", "__qualname__", "__doc__")) - def __set_name__(self, owner, name): - log.debug(f"Decorated {name!r} on {owner!r} with {self!r}") + def __set_name__(self, owner, name): + log.debug(f"Decorated {name!r} on {owner!r} with {self!r}") - def __get__(self, obj, objtype=None): - if obj is None: - return self - return types.MethodType(self, obj) + def __get__(self, obj, objtype=None): + if obj is None: + return self + return types.MethodType(self, obj) class CommandHandler(HandlerDecoratorBase[CommandHandlerT]): - _command: CommandT + _command: CommandT - def __init__(self, command: CommandT, func: CommandHandlerT): - super().__init__(func) - self._command = command + def __init__(self, command: CommandT, func: CommandHandlerT): + super().__init__(func) + self._command = command - def __call__(self, protocolHandler: "RemoteProtocolHandler", payload: bytes): - log.debug(f"Calling {self!r} for command {self._command!r}") - return self._func(protocolHandler, payload) + def __call__(self, protocolHandler: "RemoteProtocolHandler", payload: bytes): + log.debug(f"Calling {self!r} for command {self._command!r}") + return self._func(protocolHandler, payload) def commandHandler(command: CommandT): - return partial(CommandHandler, command) + return partial(CommandHandler, command) class AttributeHandler(HandlerDecoratorBase, Generic[AttributeHandlerT]): - _attribute: AttributeT = b"" + _attribute: AttributeT = b"" - @property - def _isCatchAll(self) -> bool: - return b"*" in self._attribute + @property + def _isCatchAll(self) -> bool: + return b"*" in self._attribute - def __init__(self, attribute: AttributeT, func: AttributeHandlerT): - super().__init__(func) - self._attribute = attribute + def __init__(self, attribute: AttributeT, func: AttributeHandlerT): + super().__init__(func) + self._attribute = attribute - def __call__( - self, - protocolHandler: "RemoteProtocolHandler", - attribute: AttributeT, - *args, - **kwargs, - ): - log.debug(f"Calling {self!r} for attribute {attribute!r}") - if self._isCatchAll: - return self._func(protocolHandler, attribute, *args, **kwargs) - return self._func(protocolHandler, *args, **kwargs) + def __call__( + self, + protocolHandler: "RemoteProtocolHandler", + attribute: AttributeT, + *args, + **kwargs, + ): + log.debug(f"Calling {self!r} for attribute {attribute!r}") + if self._isCatchAll: + return self._func(protocolHandler, attribute, *args, **kwargs) + return self._func(protocolHandler, *args, **kwargs) class AttributeSender(AttributeHandler[attributeFetcherT]): - def __call__( - self, - protocolHandler: RemoteProtocolHandlerT, - attribute: AttributeT, - *args, - **kwargs, - ): - value = super().__call__(protocolHandler, attribute, *args, **kwargs) - protocolHandler.setRemoteAttribute(attribute=attribute, value=value) + def __call__( + self, + protocolHandler: RemoteProtocolHandlerT, + attribute: AttributeT, + *args, + **kwargs, + ): + value = super().__call__(protocolHandler, attribute, *args, **kwargs) + protocolHandler.setRemoteAttribute(attribute=attribute, value=value) def attributeSender(attribute: AttributeT): - return partial(AttributeSender, attribute) + return partial(AttributeSender, attribute) class AttributeReceiver( - AttributeHandler[Union[AttributeReceiverUnboundT, WildCardAttributeReceiverUnboundT]] + AttributeHandler[Union[AttributeReceiverUnboundT, WildCardAttributeReceiverUnboundT]] ): - _defaultValueGetter: Optional[DefaultValueGetterT] - _updateCallback: Optional[AttributeValueUpdateCallbackT] + _defaultValueGetter: Optional[DefaultValueGetterT] + _updateCallback: Optional[AttributeValueUpdateCallbackT] - def __init__( - self, - attribute: AttributeT, - func: Union[AttributeReceiverUnboundT, WildCardAttributeReceiverUnboundT], - defaultValueGetter: Optional[DefaultValueGetterT], - updateCallback: Optional[AttributeValueUpdateCallbackT], - ): - super().__init__(attribute, func) - self._defaultValueGetter = defaultValueGetter - self._updateCallback = updateCallback + def __init__( + self, + attribute: AttributeT, + func: Union[AttributeReceiverUnboundT, WildCardAttributeReceiverUnboundT], + defaultValueGetter: Optional[DefaultValueGetterT], + updateCallback: Optional[AttributeValueUpdateCallbackT], + ): + super().__init__(attribute, func) + self._defaultValueGetter = defaultValueGetter + self._updateCallback = updateCallback - def defaultValueGetter(self, func: DefaultValueGetterT): - self._defaultValueGetter = func - return func + def defaultValueGetter(self, func: DefaultValueGetterT): + self._defaultValueGetter = func + return func - def updateCallback(self, func: AttributeValueUpdateCallbackT): - self._updateCallback = func - return func + def updateCallback(self, func: AttributeValueUpdateCallbackT): + self._updateCallback = func + return func def attributeReceiver( - attribute: AttributeT, - defaultValue: Any = None, - defaultValueGetter: Optional[DefaultValueGetterT] = None, - updateCallback: Optional[AttributeValueUpdateCallbackT] = None, + attribute: AttributeT, + defaultValue: Any = None, + defaultValueGetter: Optional[DefaultValueGetterT] = None, + updateCallback: Optional[AttributeValueUpdateCallbackT] = None, ): - if defaultValue is not None and defaultValueGetter is not None: - raise ValueError("Either defaultValue or defaultValueGetter is required, but not both") - if defaultValueGetter is None: + if defaultValue is not None and defaultValueGetter is not None: + raise ValueError("Either defaultValue or defaultValueGetter is required, but not both") + if defaultValueGetter is None: - def _defaultValueGetter(self: "RemoteProtocolHandler", attribute: AttributeT): - return defaultValue + def _defaultValueGetter(self: "RemoteProtocolHandler", attribute: AttributeT): + return defaultValue - defaultValueGetter = _defaultValueGetter - return partial( - AttributeReceiver, - attribute, - defaultValueGetter=defaultValueGetter, - updateCallback=updateCallback, - ) + defaultValueGetter = _defaultValueGetter + return partial( + AttributeReceiver, + attribute, + defaultValueGetter=defaultValueGetter, + updateCallback=updateCallback, + ) class CommandHandlerStore(HandlerRegistrar): - def _getHandler(self, command: CommandT) -> CommandHandlerT: - handler = next((v for v in self.handlers if command == v._command), None) - if handler is None: - raise NotImplementedError(f"No command handler for command {command!r}") - return handler + def _getHandler(self, command: CommandT) -> CommandHandlerT: + handler = next((v for v in self.handlers if command == v._command), None) + if handler is None: + raise NotImplementedError(f"No command handler for command {command!r}") + return handler - def __call__(self, command: CommandT, payload: bytes): - log.debug(f"Getting handler on {self!r} to process command {command!r}") - handler = self._getHandler(command) - handler(payload) + def __call__(self, command: CommandT, payload: bytes): + log.debug(f"Getting handler on {self!r} to process command {command!r}") + handler = self._getHandler(command) + handler(payload) class AttributeHandlerStore(HandlerRegistrar, Generic[AttributeHandlerT]): - def _getRawHandler(self, attribute: AttributeT) -> AttributeHandlerT: - handler = next((v for v in self.handlers if fnmatch(attribute, v._attribute)), None) - if handler is None: - raise NotImplementedError(f"No attribute sender for attribute {attribute}") - return handler + def _getRawHandler(self, attribute: AttributeT) -> AttributeHandlerT: + handler = next((v for v in self.handlers if fnmatch(attribute, v._attribute)), None) + if handler is None: + raise NotImplementedError(f"No attribute sender for attribute {attribute}") + return handler - def _getHandler(self, attribute: AttributeT) -> AttributeHandlerT: - return partial(self._getRawHandler(attribute), attribute) + def _getHandler(self, attribute: AttributeT) -> AttributeHandlerT: + return partial(self._getRawHandler(attribute), attribute) - def isAttributeSupported(self, attribute: AttributeT): - try: - return self._getHandler(attribute) is not None - except NotImplementedError: - return False + def isAttributeSupported(self, attribute: AttributeT): + try: + return self._getHandler(attribute) is not None + except NotImplementedError: + return False class AttributeSenderStore(AttributeHandlerStore[attributeSenderT]): - def __call__(self, attribute: AttributeT, *args, **kwargs): - log.debug(f"Getting handler on {self!r} to process attribute {attribute!r}") - handler = self._getHandler(attribute) - handler(*args, **kwargs) + def __call__(self, attribute: AttributeT, *args, **kwargs): + log.debug(f"Getting handler on {self!r} to process attribute {attribute!r}") + handler = self._getHandler(attribute) + handler(*args, **kwargs) class AttributeValueProcessor(AttributeHandlerStore[AttributeReceiverT]): - _valueTimes: DefaultDict[AttributeT, float] - _values: Dict[AttributeT, Any] - _pendingAttributeRequests: DefaultDict[AttributeT, bool] - - def __init__(self): - super().__init__() - self._values = {} - self._valueTimes = DefaultDict(float) - self._pendingAttributeRequests = DefaultDict(bool) - - def clearCache(self): - self._values.clear() - self._valueTimes.clear() - self._pendingAttributeRequests.clear() - - def setAttributeRequestPending(self, attribute: AttributeT, state: bool = True): - log.debug(f"Request pending for attribute {attribute!r} set to {state!r}") - self._pendingAttributeRequests[attribute] = state - - def isAttributeRequestPending(self, attribute: AttributeT) -> bool: - return self._pendingAttributeRequests[attribute] is True - - def hasNewValueSince(self, attribute: AttributeT, t: float) -> bool: - return t <= self._valueTimes[attribute] - - def _getDefaultValue(self, attribute: AttributeT) -> AttributeValueT: - handler = self._getRawHandler(attribute) - log.debug( - f"Getting default value for attribute {attribute!r} on {self!r} " - f"using {handler._defaultValueGetter!r}" - ) - getter = handler._defaultValueGetter.__get__(handler.__self__) - return getter(attribute) - - def _invokeUpdateCallback(self, attribute: AttributeT, value: AttributeValueT): - handler = self._getRawHandler(attribute) - if handler._updateCallback is not None: - callback = handler._updateCallback.__get__(handler.__self__) - log.debug(f"Calling update callback {callback!r} for attribute {attribute!r}") - handler.__self__._bgExecutor.submit(callback, attribute, value) - - def getValue(self, attribute: AttributeT, fallBackToDefault: bool = False): - if fallBackToDefault and attribute not in self._values: - log.debug(f"No value for attribute {attribute!r} on {self!r}, falling back to default") - self._values[attribute] = self._getDefaultValue(attribute) - return self._values[attribute] - - def clearValue(self, attribute): - self._values.pop(attribute, None) - - def setValue(self, attribute: AttributeT, value): - self._values[attribute] = value - self._valueTimes[attribute] = time.perf_counter() - self._invokeUpdateCallback(attribute, value) - - def __call__(self, attribute: AttributeT, rawValue: bytes): - log.debug(f"Getting handler on {self!r} to process attribute {attribute!r}") - handler = self._getHandler(attribute) - value = handler(rawValue) - log.debug(f"Handler on {self!r} returned value {value!r} for attribute {attribute!r}") - self.setAttributeRequestPending(attribute, False) - self.setValue(attribute, value) + _valueTimes: DefaultDict[AttributeT, float] + _values: Dict[AttributeT, Any] + _pendingAttributeRequests: DefaultDict[AttributeT, bool] + + def __init__(self): + super().__init__() + self._values = {} + self._valueTimes = DefaultDict(float) + self._pendingAttributeRequests = DefaultDict(bool) + + def clearCache(self): + self._values.clear() + self._valueTimes.clear() + self._pendingAttributeRequests.clear() + + def setAttributeRequestPending(self, attribute: AttributeT, state: bool = True): + log.debug(f"Request pending for attribute {attribute!r} set to {state!r}") + self._pendingAttributeRequests[attribute] = state + + def isAttributeRequestPending(self, attribute: AttributeT) -> bool: + return self._pendingAttributeRequests[attribute] is True + + def hasNewValueSince(self, attribute: AttributeT, t: float) -> bool: + return t <= self._valueTimes[attribute] + + def _getDefaultValue(self, attribute: AttributeT) -> AttributeValueT: + handler = self._getRawHandler(attribute) + log.debug( + f"Getting default value for attribute {attribute!r} on {self!r} " + f"using {handler._defaultValueGetter!r}" + ) + getter = handler._defaultValueGetter.__get__(handler.__self__) + return getter(attribute) + + def _invokeUpdateCallback(self, attribute: AttributeT, value: AttributeValueT): + handler = self._getRawHandler(attribute) + if handler._updateCallback is not None: + callback = handler._updateCallback.__get__(handler.__self__) + log.debug(f"Calling update callback {callback!r} for attribute {attribute!r}") + handler.__self__._bgExecutor.submit(callback, attribute, value) + + def getValue(self, attribute: AttributeT, fallBackToDefault: bool = False): + if fallBackToDefault and attribute not in self._values: + log.debug(f"No value for attribute {attribute!r} on {self!r}, falling back to default") + self._values[attribute] = self._getDefaultValue(attribute) + return self._values[attribute] + + def clearValue(self, attribute): + self._values.pop(attribute, None) + + def setValue(self, attribute: AttributeT, value): + self._values[attribute] = value + self._valueTimes[attribute] = time.perf_counter() + self._invokeUpdateCallback(attribute, value) + + def __call__(self, attribute: AttributeT, rawValue: bytes): + log.debug(f"Getting handler on {self!r} to process attribute {attribute!r}") + handler = self._getHandler(attribute) + value = handler(rawValue) + log.debug(f"Handler on {self!r} returned value {value!r} for attribute {attribute!r}") + self.setAttributeRequestPending(attribute, False) + self.setValue(attribute, value) class RemoteProtocolHandler((AutoPropertyObject)): - _dev: IoBase - driverType: DriverType - _receiveBuffer: bytes - _commandHandlerStore: CommandHandlerStore - _attributeSenderStore: AttributeSenderStore - _attributeValueProcessor: AttributeValueProcessor - timeout: float = 1.0 - cachePropertiesByDefault = True - _bgExecutor: ThreadPoolExecutor - - def __new__(cls, *args, **kwargs): - self = super().__new__(cls, *args, **kwargs) - self._commandHandlerStore = CommandHandlerStore() - self._attributeSenderStore = AttributeSenderStore() - self._attributeValueProcessor = AttributeValueProcessor() - handlers = inspect.getmembers(cls, predicate=lambda o: isinstance(o, HandlerDecoratorBase)) - for k, v in handlers: - if isinstance(v, CommandHandler): - self._commandHandlerStore.register(getattr(self, k)) - elif isinstance(v, AttributeSender): - self._attributeSenderStore.register(getattr(self, k)) - elif isinstance(v, AttributeReceiver): - self._attributeValueProcessor.register(getattr(self, k)) - return self - - def terminateIo(self): - # Make sure the device gets closed. - self._dev.close() - - def __init__(self): - super().__init__() - self._bgExecutor = ThreadPoolExecutor(4, thread_name_prefix=self.__class__.__name__) - self._receiveBuffer = b"" - - def terminate(self): - try: - superTerminate = getattr(super(), "terminate", None) - if superTerminate: - superTerminate() - # We must sleep before closing the connection as not doing this - # can leave a remote handler in a bad state where it can not be re-initialized. - time.sleep(self.timeout / 10) - finally: - self.terminateIo() - self._attributeValueProcessor.clearCache() - self._bgExecutor.shutdown() - - def _onReceive(self, message: bytes): - if self._receiveBuffer: - message = self._receiveBuffer + message - self._receiveBuffer = b"" - if not message[0] == self.driverType: - raise RuntimeError(f"Unexpected payload: {message}") - command = cast(CommandT, message[1]) - expectedLength = int.from_bytes(message[2:4], sys.byteorder) - payload = message[4:] - actualLength = len(payload) - remainder: Optional[bytes] = None - if expectedLength != actualLength: - log.debug( - f"Expected payload of length {expectedLength}, " - f"actual length of payload {payload!r} is {actualLength}" - ) - if expectedLength > actualLength: - self._receiveBuffer = message - return - else: - remainder = payload[expectedLength:] - payload = payload[:expectedLength] - - try: - self._bgExecutor.submit(self._commandHandlerStore, command, payload) - finally: - if remainder: - self._onReceive(remainder) - - @commandHandler(GenericCommand.ATTRIBUTE) - def _command_attribute(self, payload: bytes): - attribute, value = payload[1:].split(b"`", 1) - log.debug(f"Handling attribute {attribute!r} on {self!r}, value {value!r}") - if not value: - log.debug(f"No value sent for attribute {attribute!r} on {self!r}, direction outgoing") - self._attributeSenderStore(attribute) - else: - log.debug( - f"Value of length {len(value)} sent for attribute {attribute!r} " - f"on {self!r}, direction incoming" - ) - self._attributeValueProcessor(attribute, value) - - @abstractmethod - def _incoming_setting(self, attribute: AttributeT, payLoad: bytes): - raise NotImplementedError - - def writeMessage(self, command: CommandT, payload: bytes = b""): - data = bytes( - ( - self.driverType, - command, - *len(payload).to_bytes(length=2, byteorder=sys.byteorder, signed=False), - *payload, - ) - ) - self._dev.write(data) - - def setRemoteAttribute(self, attribute: AttributeT, value: bytes): - log.debug(f"Setting remote attribute {attribute!r} to raw value {value!r}") - return self.writeMessage( - GenericCommand.ATTRIBUTE, - ATTRIBUTE_SEPARATOR + attribute + ATTRIBUTE_SEPARATOR + value, - ) - - def requestRemoteAttribute(self, attribute: AttributeT): - if self._attributeValueProcessor.isAttributeRequestPending(attribute): - log.debugWarning(f"Not requesting remote attribute {attribute!r},, request already pending") - return - log.debug(f"Requesting remote attribute {attribute!r}") - self._attributeValueProcessor.setAttributeRequestPending(attribute) - self.writeMessage( - GenericCommand.ATTRIBUTE, - ATTRIBUTE_SEPARATOR + attribute + ATTRIBUTE_SEPARATOR, - ) - - def _safeWait(self, predicate: Callable[[], bool], timeout: Optional[float] = None): - if timeout is None: - timeout = self.timeout - log.debug(f"Waiting for {predicate!r} during {timeout} seconds") - while timeout > 0.0: - if predicate(): - log.debug(f"Waiting for {predicate!r} succeeded, {timeout} seconds remaining") - return True - curTime = time.perf_counter() - res: bool = self._dev.waitForRead(timeout=timeout) - if res is False: - log.debug(f"Waiting for read for {predicate!r} failed. Predicate may still be True") - break - timeout -= time.perf_counter() - curTime - return predicate() - - def getRemoteAttribute( - self, - attribute: AttributeT, - timeout: Optional[float] = None, - ): - initialTime = time.perf_counter() - self.requestRemoteAttribute(attribute=attribute) - if self._waitForAttributeUpdate(attribute, initialTime, timeout): - newValue = self._attributeValueProcessor.getValue(attribute, fallBackToDefault=False) - log.debug(f"Received new value {newValue!r} for remote attribute {attribute!r}") - return newValue - raise TimeoutError(f"Wait for remote attribute {attribute} timed out") - - def _waitForAttributeUpdate( - self, - attribute: AttributeT, - initialTime: Optional[float] = None, - timeout: Optional[float] = None, - ): - if initialTime is None: - initialTime = 0.0 - log.debug(f"Waiting for attribute {attribute!r}") - result = self._safeWait( - lambda: self._attributeValueProcessor.hasNewValueSince(attribute, initialTime), - timeout=timeout, - ) - if result: - log.debug(f"Waiting for attribute {attribute} succeeded") - else: - log.debug(f"Waiting for attribute {attribute} failed") - return result - - def _pickle(self, obj: Any): - return pickle.dumps(obj, protocol=4) - - def _unpickle(self, payload: bytes) -> Any: - res = pickle.loads(payload) - if isinstance(res, AutoPropertyObject): - res.invalidateCache() - return res - - def _queueFunctionOnMainThread(self, func, *args, **kwargs): - @wraps(func) - def wrapper(*args, **kwargs): - log.debug(f"Executing {func!r}({args!r}, {kwargs!r}) on main thread") - try: - func(*args, **kwargs) - except Exception: - log.debug( - f"Error executing {func!r}({args!r}, {kwargs!r}) on main thread", - exc_info=True, - ) - - queueHandler.queueFunction(queueHandler.eventQueue, wrapper, *args, **kwargs) - - @abstractmethod - def _onReadError(self, error: int) -> bool: - return False + _dev: IoBase + driverType: DriverType + _receiveBuffer: bytes + _commandHandlerStore: CommandHandlerStore + _attributeSenderStore: AttributeSenderStore + _attributeValueProcessor: AttributeValueProcessor + timeout: float = 1.0 + cachePropertiesByDefault = True + _bgExecutor: ThreadPoolExecutor + + def __new__(cls, *args, **kwargs): + self = super().__new__(cls, *args, **kwargs) + self._commandHandlerStore = CommandHandlerStore() + self._attributeSenderStore = AttributeSenderStore() + self._attributeValueProcessor = AttributeValueProcessor() + handlers = inspect.getmembers(cls, predicate=lambda o: isinstance(o, HandlerDecoratorBase)) + for k, v in handlers: + if isinstance(v, CommandHandler): + self._commandHandlerStore.register(getattr(self, k)) + elif isinstance(v, AttributeSender): + self._attributeSenderStore.register(getattr(self, k)) + elif isinstance(v, AttributeReceiver): + self._attributeValueProcessor.register(getattr(self, k)) + return self + + def terminateIo(self): + # Make sure the device gets closed. + self._dev.close() + + def __init__(self): + super().__init__() + self._bgExecutor = ThreadPoolExecutor(4, thread_name_prefix=self.__class__.__name__) + self._receiveBuffer = b"" + + def terminate(self): + try: + superTerminate = getattr(super(), "terminate", None) + if superTerminate: + superTerminate() + # We must sleep before closing the connection as not doing this + # can leave a remote handler in a bad state where it can not be re-initialized. + time.sleep(self.timeout / 10) + finally: + self.terminateIo() + self._attributeValueProcessor.clearCache() + self._bgExecutor.shutdown() + + def _onReceive(self, message: bytes): + if self._receiveBuffer: + message = self._receiveBuffer + message + self._receiveBuffer = b"" + if not message[0] == self.driverType: + raise RuntimeError(f"Unexpected payload: {message}") + command = cast(CommandT, message[1]) + expectedLength = int.from_bytes(message[2:4], sys.byteorder) + payload = message[4:] + actualLength = len(payload) + remainder: Optional[bytes] = None + if expectedLength != actualLength: + log.debug( + f"Expected payload of length {expectedLength}, " + f"actual length of payload {payload!r} is {actualLength}" + ) + if expectedLength > actualLength: + self._receiveBuffer = message + return + else: + remainder = payload[expectedLength:] + payload = payload[:expectedLength] + + try: + self._bgExecutor.submit(self._commandHandlerStore, command, payload) + finally: + if remainder: + self._onReceive(remainder) + + @commandHandler(GenericCommand.ATTRIBUTE) + def _command_attribute(self, payload: bytes): + attribute, value = payload[1:].split(b"`", 1) + log.debug(f"Handling attribute {attribute!r} on {self!r}, value {value!r}") + if not value: + log.debug(f"No value sent for attribute {attribute!r} on {self!r}, direction outgoing") + self._attributeSenderStore(attribute) + else: + log.debug( + f"Value of length {len(value)} sent for attribute {attribute!r} " + f"on {self!r}, direction incoming" + ) + self._attributeValueProcessor(attribute, value) + + @abstractmethod + def _incoming_setting(self, attribute: AttributeT, payLoad: bytes): + raise NotImplementedError + + def writeMessage(self, command: CommandT, payload: bytes = b""): + data = bytes( + ( + self.driverType, + command, + *len(payload).to_bytes(length=2, byteorder=sys.byteorder, signed=False), + *payload, + ) + ) + self._dev.write(data) + + def setRemoteAttribute(self, attribute: AttributeT, value: bytes): + log.debug(f"Setting remote attribute {attribute!r} to raw value {value!r}") + return self.writeMessage( + GenericCommand.ATTRIBUTE, + ATTRIBUTE_SEPARATOR + attribute + ATTRIBUTE_SEPARATOR + value, + ) + + def requestRemoteAttribute(self, attribute: AttributeT): + if self._attributeValueProcessor.isAttributeRequestPending(attribute): + log.debugWarning(f"Not requesting remote attribute {attribute!r},, request already pending") + return + log.debug(f"Requesting remote attribute {attribute!r}") + self._attributeValueProcessor.setAttributeRequestPending(attribute) + self.writeMessage( + GenericCommand.ATTRIBUTE, + ATTRIBUTE_SEPARATOR + attribute + ATTRIBUTE_SEPARATOR, + ) + + def _safeWait(self, predicate: Callable[[], bool], timeout: Optional[float] = None): + if timeout is None: + timeout = self.timeout + log.debug(f"Waiting for {predicate!r} during {timeout} seconds") + while timeout > 0.0: + if predicate(): + log.debug(f"Waiting for {predicate!r} succeeded, {timeout} seconds remaining") + return True + curTime = time.perf_counter() + res: bool = self._dev.waitForRead(timeout=timeout) + if res is False: + log.debug(f"Waiting for read for {predicate!r} failed. Predicate may still be True") + break + timeout -= time.perf_counter() - curTime + return predicate() + + def getRemoteAttribute( + self, + attribute: AttributeT, + timeout: Optional[float] = None, + ): + initialTime = time.perf_counter() + self.requestRemoteAttribute(attribute=attribute) + if self._waitForAttributeUpdate(attribute, initialTime, timeout): + newValue = self._attributeValueProcessor.getValue(attribute, fallBackToDefault=False) + log.debug(f"Received new value {newValue!r} for remote attribute {attribute!r}") + return newValue + raise TimeoutError(f"Wait for remote attribute {attribute} timed out") + + def _waitForAttributeUpdate( + self, + attribute: AttributeT, + initialTime: Optional[float] = None, + timeout: Optional[float] = None, + ): + if initialTime is None: + initialTime = 0.0 + log.debug(f"Waiting for attribute {attribute!r}") + result = self._safeWait( + lambda: self._attributeValueProcessor.hasNewValueSince(attribute, initialTime), + timeout=timeout, + ) + if result: + log.debug(f"Waiting for attribute {attribute} succeeded") + else: + log.debug(f"Waiting for attribute {attribute} failed") + return result + + def _pickle(self, obj: Any): + return pickle.dumps(obj, protocol=4) + + def _unpickle(self, payload: bytes) -> Any: + res = pickle.loads(payload) + if isinstance(res, AutoPropertyObject): + res.invalidateCache() + return res + + def _queueFunctionOnMainThread(self, func, *args, **kwargs): + @wraps(func) + def wrapper(*args, **kwargs): + log.debug(f"Executing {func!r}({args!r}, {kwargs!r}) on main thread") + try: + func(*args, **kwargs) + except Exception: + log.debug( + f"Error executing {func!r}({args!r}, {kwargs!r}) on main thread", + exc_info=True, + ) + + queueHandler.queueFunction(queueHandler.eventQueue, wrapper, *args, **kwargs) + + @abstractmethod + def _onReadError(self, error: int) -> bool: + return False diff --git a/addon/lib/protocol/braille.py b/addon/lib/protocol/braille.py index f9b6506..7a372bf 100644 --- a/addon/lib/protocol/braille.py +++ b/addon/lib/protocol/braille.py @@ -10,33 +10,33 @@ class BrailleCommand(IntEnum): - DISPLAY = ord(b"D") - EXECUTE_GESTURE = ord(b"G") + DISPLAY = ord(b"D") + EXECUTE_GESTURE = ord(b"G") class BrailleAttribute(bytes, Enum): - NUM_CELLS = b"numCells" - GESTURE_MAP = b"gestureMap" - OBJECT_GESTURE_MAP = b"_gestureMap" + NUM_CELLS = b"numCells" + GESTURE_MAP = b"gestureMap" + OBJECT_GESTURE_MAP = b"_gestureMap" class BrailleInputGesture(braille.BrailleDisplayGesture, brailleInput.BrailleInputGesture): - def __init__( - self, - source: str, - id: str, - routingIndex: Optional[int] = None, - model: Optional[str] = None, - dots: int = 0, - space: bool = False, - **kwargs, - ): - super().__init__() - self.source = source - self.id = id - self.routingIndex = routingIndex - self.model = model - self.dots = dots - self.space = space - for attr, val in kwargs.items(): - setattr(self, attr, val) + def __init__( + self, + source: str, + id: str, + routingIndex: Optional[int] = None, + model: Optional[str] = None, + dots: int = 0, + space: bool = False, + **kwargs, + ): + super().__init__() + self.source = source + self.id = id + self.routingIndex = routingIndex + self.model = model + self.dots = dots + self.space = space + for attr, val in kwargs.items(): + setattr(self, attr, val) diff --git a/addon/lib/protocol/speech.py b/addon/lib/protocol/speech.py index 451f6ff..7153834 100644 --- a/addon/lib/protocol/speech.py +++ b/addon/lib/protocol/speech.py @@ -10,14 +10,14 @@ class SpeechCommand(IntEnum): - SPEAK = ord(b"S") - CANCEL = ord(b"C") - PAUSE = ord(b"P") - INDEX_REACHED = ord(b"x") - BEEP = ord(b"B") - PLAY_WAVE_FILE = ord(b"W") + SPEAK = ord(b"S") + CANCEL = ord(b"C") + PAUSE = ord(b"P") + INDEX_REACHED = ord(b"x") + BEEP = ord(b"B") + PLAY_WAVE_FILE = ord(b"W") class SpeechAttribute(bytes, Enum): - SUPPORTED_COMMANDS = b"supportedCommands" - LANGUAGE = b"language" + SUPPORTED_COMMANDS = b"supportedCommands" + LANGUAGE = b"language" diff --git a/addon/lib/rdPipe.py b/addon/lib/rdPipe.py index a4c664f..58fb0e3 100644 --- a/addon/lib/rdPipe.py +++ b/addon/lib/rdPipe.py @@ -21,89 +21,89 @@ def isCitrixSupported() -> bool: - try: - with winreg.OpenKey( - winreg.HKEY_LOCAL_MACHINE, - CTX_ARP_FOLDER, - 0, - winreg.KEY_READ | winreg.KEY_WOW64_32KEY, - ): - pass - except OSError: - return False - try: - with winreg.OpenKey( - winreg.HKEY_CURRENT_USER, - CTX_MODULES_FOLDER, - 0, - winreg.KEY_READ | winreg.KEY_WOW64_32KEY, - ): - return True - except OSError: - return False + try: + with winreg.OpenKey( + winreg.HKEY_LOCAL_MACHINE, + CTX_ARP_FOLDER, + 0, + winreg.KEY_READ | winreg.KEY_WOW64_32KEY, + ): + pass + except OSError: + return False + try: + with winreg.OpenKey( + winreg.HKEY_CURRENT_USER, + CTX_MODULES_FOLDER, + 0, + winreg.KEY_READ | winreg.KEY_WOW64_32KEY, + ): + return True + except OSError: + return False class Architecture(str, Enum): - X86 = "x86" - AMD64 = "AMD64" - ARM64 = "ARM64" + X86 = "x86" + AMD64 = "AMD64" + ARM64 = "ARM64" DEFAULT_ARCHITECTURE = Architecture(platform.machine()) def execRegsrv(params: List[str], architecture: Architecture = DEFAULT_ARCHITECTURE) -> bool: - if architecture is Architecture.X86: - # Points to the 32-bit version, on Windows 32-bit or 64-bit. - regsvr32 = os.path.join(COMRegistrationFixes.SYSTEM32, "regsvr32.exe") - else: - # SysWOW64 provides a virtual directory to allow 32-bit programs to reach 64-bit executables. - regsvr32 = os.path.join(COMRegistrationFixes.SYSNATIVE, "regsvr32.exe") - # Make sure a console window doesn't show when running regsvr32.exe - startupInfo = subprocess.STARTUPINFO() - startupInfo.dwFlags |= subprocess.STARTF_USESHOWWINDOW - startupInfo.wShowWindow = subprocess.SW_HIDE - try: - subprocess.check_call([regsvr32] + params, startupinfo=startupInfo) - return True - except subprocess.CalledProcessError as e: - log.warning(f"Error calling {regsvr32!r} with arguments {params!r}: {e}") - return False + if architecture is Architecture.X86: + # Points to the 32-bit version, on Windows 32-bit or 64-bit. + regsvr32 = os.path.join(COMRegistrationFixes.SYSTEM32, "regsvr32.exe") + else: + # SysWOW64 provides a virtual directory to allow 32-bit programs to reach 64-bit executables. + regsvr32 = os.path.join(COMRegistrationFixes.SYSNATIVE, "regsvr32.exe") + # Make sure a console window doesn't show when running regsvr32.exe + startupInfo = subprocess.STARTUPINFO() + startupInfo.dwFlags |= subprocess.STARTF_USESHOWWINDOW + startupInfo.wShowWindow = subprocess.SW_HIDE + try: + subprocess.check_call([regsvr32] + params, startupinfo=startupInfo) + return True + except subprocess.CalledProcessError as e: + log.warning(f"Error calling {regsvr32!r} with arguments {params!r}: {e}") + return False class CommandFlags(str, Enum): - COM_SERVER = "c" - RDP = "r" - CITRIX = "x" + COM_SERVER = "c" + RDP = "r" + CITRIX = "x" def getDllPath(architecture: Architecture = DEFAULT_ARCHITECTURE) -> str: - addon = addonHandler.getCodeAddon() - expectedPath = os.path.join(addon.path, "dll", f"rd_pipe_{architecture.lower()}.dll") - if not os.path.isfile(expectedPath): - raise FileNotFoundError(expectedPath) - return expectedPath + addon = addonHandler.getCodeAddon() + expectedPath = os.path.join(addon.path, "dll", f"rd_pipe_{architecture.lower()}.dll") + if not os.path.isfile(expectedPath): + raise FileNotFoundError(expectedPath) + return expectedPath def dllInstall( - install: bool, - comServer: bool, - rdp: bool, - citrix: bool, - architecture: Architecture = DEFAULT_ARCHITECTURE, + install: bool, + comServer: bool, + rdp: bool, + citrix: bool, + architecture: Architecture = DEFAULT_ARCHITECTURE, ) -> bool: - path = getDllPath(architecture) - command = "" - if rdp: - command += CommandFlags.RDP - if citrix: - command += CommandFlags.CITRIX - if comServer: - command += CommandFlags.COM_SERVER - if install: - command += f" {COM_CLS_CHANNEL_NAMES_VALUE_BRAILLE} {COM_CLS_CHANNEL_NAMES_VALUE_SPEECH}" - cmdLine = ["/s", f'/i:"{command}"', "/n"] - if not install: - cmdLine.append("/u") - cmdLine.append(path) - return execRegsrv(cmdLine, architecture) + path = getDllPath(architecture) + command = "" + if rdp: + command += CommandFlags.RDP + if citrix: + command += CommandFlags.CITRIX + if comServer: + command += CommandFlags.COM_SERVER + if install: + command += f" {COM_CLS_CHANNEL_NAMES_VALUE_BRAILLE} {COM_CLS_CHANNEL_NAMES_VALUE_SPEECH}" + cmdLine = ["/s", f'/i:"{command}"', "/n"] + if not install: + cmdLine.append("/u") + cmdLine.append(path) + return execRegsrv(cmdLine, architecture) diff --git a/addon/lib/secureDesktop.py b/addon/lib/secureDesktop.py index ecb56b7..06af981 100644 --- a/addon/lib/secureDesktop.py +++ b/addon/lib/secureDesktop.py @@ -8,6 +8,6 @@ hasSecureDesktopExtensionPoint = versionInfo.version_year >= 2024 if hasSecureDesktopExtensionPoint: - from winAPI.secureDesktop import post_secureDesktopStateChange + from winAPI.secureDesktop import post_secureDesktopStateChange else: - post_secureDesktopStateChange = Action() + post_secureDesktopStateChange = Action() diff --git a/addon/lib/wtsVirtualChannel.py b/addon/lib/wtsVirtualChannel.py index da03fca..75a5094 100644 --- a/addon/lib/wtsVirtualChannel.py +++ b/addon/lib/wtsVirtualChannel.py @@ -3,24 +3,24 @@ # License: GNU General Public License version 2.0 from ctypes import ( - POINTER, - GetLastError, - Structure, - WinError, - byref, - c_int, - c_uint32, - c_void_p, - cdll, - create_string_buffer, - sizeof, - windll, + POINTER, + GetLastError, + Structure, + WinError, + byref, + c_int, + c_uint32, + c_void_p, + cdll, + create_string_buffer, + sizeof, + windll, ) from ctypes.wintypes import ( - BOOL, - DWORD, - HANDLE, - LPWSTR, + BOOL, + DWORD, + HANDLE, + LPWSTR, ) from typing import Callable, Optional @@ -42,131 +42,131 @@ class ChannelPduHeader(Structure): - _fields_ = ( - ("length", c_uint32), - ("flags", c_uint32), - ) + _fields_ = ( + ("length", c_uint32), + ("flags", c_uint32), + ) CHANNEL_PDU_LENGTH = CHANNEL_CHUNK_LENGTH + sizeof(ChannelPduHeader) try: - vdp_rdpvcbridge = cdll.vdp_rdpvcbridge + vdp_rdpvcbridge = cdll.vdp_rdpvcbridge except OSError: - WTSVirtualChannelOpenEx = windll.wtsapi32.WTSVirtualChannelOpenEx - WTSVirtualChannelQuery = windll.wtsapi32.WTSVirtualChannelQuery - WTSVirtualChannelClose = windll.wtsapi32.WTSVirtualChannelClose - GetSystemMetrics = windll.user32.GetSystemMetrics + WTSVirtualChannelOpenEx = windll.wtsapi32.WTSVirtualChannelOpenEx + WTSVirtualChannelQuery = windll.wtsapi32.WTSVirtualChannelQuery + WTSVirtualChannelClose = windll.wtsapi32.WTSVirtualChannelClose + GetSystemMetrics = windll.user32.GetSystemMetrics else: - WTSVirtualChannelOpenEx = vdp_rdpvcbridge.VDP_VirtualChannelOpenEx - WTSVirtualChannelQuery = vdp_rdpvcbridge.VDP_VirtualChannelQuery - # Slightly hacky but effective - wtsApi32.WTSFreeMemory = vdp_rdpvcbridge.VDP_FreeMemory - wtsApi32.WTSFreeMemory.argtypes = ( - c_void_p, # [in] PVOID pMemory - ) - wtsApi32.WTSFreeMemory.restype = None - WTSVirtualChannelClose = vdp_rdpvcbridge.VDP_VirtualChannelClose - wtsApi32.WTSQuerySessionInformation = vdp_rdpvcbridge.VDP_QuerySessionInformationW - wtsApi32.WTSQuerySessionInformation.argtypes = ( - HANDLE, # [in] HANDLE hServer - DWORD, # [ in] DWORD SessionId - c_int, # [ in] WTS_INFO_CLASS WTSInfoClass, - POINTER(LPWSTR), # [out] LPWSTR * ppBuffer, - POINTER(DWORD), # [out] DWORD * pBytesReturned - ) - wtsApi32.WTSQuerySessionInformation.restype = BOOL # On Failure, the return value is zero. - GetSystemMetrics = vdp_rdpvcbridge.VDP_GetSystemMetrics + WTSVirtualChannelOpenEx = vdp_rdpvcbridge.VDP_VirtualChannelOpenEx + WTSVirtualChannelQuery = vdp_rdpvcbridge.VDP_VirtualChannelQuery + # Slightly hacky but effective + wtsApi32.WTSFreeMemory = vdp_rdpvcbridge.VDP_FreeMemory + wtsApi32.WTSFreeMemory.argtypes = ( + c_void_p, # [in] PVOID pMemory + ) + wtsApi32.WTSFreeMemory.restype = None + WTSVirtualChannelClose = vdp_rdpvcbridge.VDP_VirtualChannelClose + wtsApi32.WTSQuerySessionInformation = vdp_rdpvcbridge.VDP_QuerySessionInformationW + wtsApi32.WTSQuerySessionInformation.argtypes = ( + HANDLE, # [in] HANDLE hServer + DWORD, # [ in] DWORD SessionId + c_int, # [ in] WTS_INFO_CLASS WTSInfoClass, + POINTER(LPWSTR), # [out] LPWSTR * ppBuffer, + POINTER(DWORD), # [out] DWORD * pBytesReturned + ) + wtsApi32.WTSQuerySessionInformation.restype = BOOL # On Failure, the return value is zero. + GetSystemMetrics = vdp_rdpvcbridge.VDP_GetSystemMetrics def getRemoteSessionMetrics() -> bool: - return bool(GetSystemMetrics(SM_REMOTESESSION)) + return bool(GetSystemMetrics(SM_REMOTESESSION)) class WTSVirtualChannel(IoBase): - _rawOutput: bool - - def __init__( - self, - channelName: str, - onReceive: Callable[[bytes], None], - onReadError: Optional[Callable[[int], bool]] = None, - ioThread: Optional[IoThread] = None, - rawOutput=False, - ): - wtsHandle = WTSVirtualChannelOpenEx( - wtsApi32.WTS_CURRENT_SESSION, - create_string_buffer(channelName.encode("ascii")), - WTS_CHANNEL_OPTION_DYNAMIC | WTS_CHANNEL_OPTION_DYNAMIC_PRI_HIGH, - ) - if wtsHandle == 0: - raise WinError() - try: - fileHandlePtr = POINTER(HANDLE)() - length = DWORD() - if not WTSVirtualChannelQuery( - wtsHandle, WTSVirtualFileHandle, byref(fileHandlePtr), byref(length) - ): - raise WinError() - try: - assert length.value == sizeof(HANDLE) - curProc = winKernel.GetCurrentProcess() - fileHandle = winKernel.DuplicateHandle( - curProc, - fileHandlePtr.contents.value, - curProc, - 0, - False, - winKernel.DUPLICATE_SAME_ACCESS, - ) - finally: - wtsApi32.WTSFreeMemory(fileHandlePtr) - finally: - if not WTSVirtualChannelClose(wtsHandle): - raise WinError() - self._rawOutput = rawOutput - super().__init__( - fileHandle, - onReceive, - onReceiveSize=CHANNEL_PDU_LENGTH, - onReadError=onReadError, - ioThread=ioThread, - ) - - def close(self): - super().close() - if hasattr(self, "_file") and self._file is not INVALID_HANDLE_VALUE: - winKernel.closeHandle(self._file) - self._file = INVALID_HANDLE_VALUE - - def _notifyReceive(self, data: bytes): - if self._rawOutput: - return super()._notifyReceive(data) - buffer = bytearray() - dataToProcess = data - while True: - header = ChannelPduHeader.from_buffer_copy(dataToProcess[: sizeof(ChannelPduHeader)]) - if not buffer: - assert header.flags & CHANNEL_FLAG_FIRST - buffer.extend(dataToProcess[sizeof(ChannelPduHeader) :]) - if header.flags & CHANNEL_FLAG_LAST: - assert len(buffer) == header.length - return super()._notifyReceive(bytes(buffer)) - dataToProcess = self._read() - - def _read(self) -> bytes: - byteData = DWORD() - if not windll.kernel32.ReadFile( - self._file, - self._readBuf, - self._readSize, - byref(byteData), - byref(self._readOl), - ): - if GetLastError() != ERROR_IO_PENDING: - if _isDebug(): - log.debug(f"Read failed: {WinError()}") - raise WinError() - windll.kernel32.GetOverlappedResult(self._file, byref(self._readOl), byref(byteData), True) - return self._readBuf.raw[: byteData.value] + _rawOutput: bool + + def __init__( + self, + channelName: str, + onReceive: Callable[[bytes], None], + onReadError: Optional[Callable[[int], bool]] = None, + ioThread: Optional[IoThread] = None, + rawOutput=False, + ): + wtsHandle = WTSVirtualChannelOpenEx( + wtsApi32.WTS_CURRENT_SESSION, + create_string_buffer(channelName.encode("ascii")), + WTS_CHANNEL_OPTION_DYNAMIC | WTS_CHANNEL_OPTION_DYNAMIC_PRI_HIGH, + ) + if wtsHandle == 0: + raise WinError() + try: + fileHandlePtr = POINTER(HANDLE)() + length = DWORD() + if not WTSVirtualChannelQuery( + wtsHandle, WTSVirtualFileHandle, byref(fileHandlePtr), byref(length) + ): + raise WinError() + try: + assert length.value == sizeof(HANDLE) + curProc = winKernel.GetCurrentProcess() + fileHandle = winKernel.DuplicateHandle( + curProc, + fileHandlePtr.contents.value, + curProc, + 0, + False, + winKernel.DUPLICATE_SAME_ACCESS, + ) + finally: + wtsApi32.WTSFreeMemory(fileHandlePtr) + finally: + if not WTSVirtualChannelClose(wtsHandle): + raise WinError() + self._rawOutput = rawOutput + super().__init__( + fileHandle, + onReceive, + onReceiveSize=CHANNEL_PDU_LENGTH, + onReadError=onReadError, + ioThread=ioThread, + ) + + def close(self): + super().close() + if hasattr(self, "_file") and self._file is not INVALID_HANDLE_VALUE: + winKernel.closeHandle(self._file) + self._file = INVALID_HANDLE_VALUE + + def _notifyReceive(self, data: bytes): + if self._rawOutput: + return super()._notifyReceive(data) + buffer = bytearray() + dataToProcess = data + while True: + header = ChannelPduHeader.from_buffer_copy(dataToProcess[: sizeof(ChannelPduHeader)]) + if not buffer: + assert header.flags & CHANNEL_FLAG_FIRST + buffer.extend(dataToProcess[sizeof(ChannelPduHeader) :]) + if header.flags & CHANNEL_FLAG_LAST: + assert len(buffer) == header.length + return super()._notifyReceive(bytes(buffer)) + dataToProcess = self._read() + + def _read(self) -> bytes: + byteData = DWORD() + if not windll.kernel32.ReadFile( + self._file, + self._readBuf, + self._readSize, + byref(byteData), + byref(self._readOl), + ): + if GetLastError() != ERROR_IO_PENDING: + if _isDebug(): + log.debug(f"Read failed: {WinError()}") + raise WinError() + windll.kernel32.GetOverlappedResult(self._file, byref(self._readOl), byref(byteData), True) + return self._readBuf.raw[: byteData.value] diff --git a/addon/synthDrivers/remote.py b/addon/synthDrivers/remote.py index f83f032..6d54c45 100644 --- a/addon/synthDrivers/remote.py +++ b/addon/synthDrivers/remote.py @@ -21,135 +21,135 @@ import os.path if typing.TYPE_CHECKING: - from ..lib import driver - from ..lib import protocol + from ..lib import driver + from ..lib import protocol else: - addon: addonHandler.Addon = addonHandler.getCodeAddon() - driver = addon.loadModule("lib.driver") - protocol = addon.loadModule("lib.protocol") + addon: addonHandler.Addon = addonHandler.getCodeAddon() + driver = addon.loadModule("lib.driver") + protocol = addon.loadModule("lib.protocol") class remoteSynthDriver(driver.RemoteDriver, synthDriverHandler.SynthDriver): - # Translators: Name for a remote braille display. - description = _("Remote speech") - supportedNotifications = { - synthDriverHandler.synthIndexReached, - synthDriverHandler.synthDoneSpeaking, - } - driverType = protocol.DriverType.SPEECH - synthRemoteDisconnected = Action() - fallbackSynth: str - _localSettings = [ - DriverSetting( - id="fallbackSynth", - # Translators: The name of a remote synthesizer setting to select the fallback synthesizer. - displayNameWithAccelerator=_("&Fallback synthesizer"), - availableInSettingsRing=True, - defaultVal=AUTOMATIC_PORT[0], - ) - ] - - @classmethod - def _get_availableFallbacksynths(cls): - dct = OrderedDict() - dct[AUTOMATIC_PORT[0]] = StringParameterInfo(*AUTOMATIC_PORT) - dct.update( - (n, StringParameterInfo(n, d)) for n, d in synthDriverHandler.getSynthList() if n != cls.name - ) - return dct - - def __init__(self, port="auto"): - super().__init__(port) - nvwave.decide_playWaveFile.register(self.handle_decidePlayWaveFile) - tones.decide_beep.register(self.handle_decideBeep) - - def terminate(self): - tones.decide_beep.unregister(self.handle_decideBeep) - nvwave.decide_playWaveFile.unregister(self.handle_decidePlayWaveFile) - super().terminate() - - def handle_decideBeep(self, **kwargs) -> bool: - log.debug(f"Sending BEEP command: {kwargs}") - try: - self.writeMessage(protocol.SpeechCommand.BEEP, self._pickle(kwargs)) - except WindowsError: - log.warning("Error calling handle_decideBeep", exc_info=True) - return True - return False - - def handle_decidePlayWaveFile(self, **kwargs) -> bool: - kwargs["fileName"] = os.path.relpath(kwargs["fileName"], globalVars.appDir) - log.debug(f"Sending PLAY_WAVE_FILE command: {kwargs}") - try: - self.writeMessage(protocol.SpeechCommand.PLAY_WAVE_FILE, self._pickle(kwargs)) - except WindowsError: - log.warning("Error calling handle_decidePlayWaveFile", exc_info=True) - return True - return False - - def _handleRemoteDisconnect(self): - self.synthRemoteDisconnected.notify(synth=self) - - def speak(self, speechSequence): - try: - self.writeMessage(protocol.SpeechCommand.SPEAK, self._pickle(speechSequence)) - except WindowsError: - log.error("Error speaking", exc_info=True) - self._handleRemoteDisconnect() - - def cancel(self): - try: - self.writeMessage(protocol.SpeechCommand.CANCEL) - except WindowsError: - log.warning("Error cancelling speech", exc_info=True) - - def pause(self, switch): - try: - self.writeMessage(protocol.SpeechCommand.PAUSE, boolToByte(switch)) - except WindowsError: - log.warning("Error pausing speech", exc_info=True) - - @protocol.attributeReceiver(protocol.SpeechAttribute.SUPPORTED_COMMANDS, defaultValue=frozenset()) - def _incoming_supportedCommands(self, payLoad: bytes) -> frozenset: - assert len(payLoad) > 0 - return self._unpickle(payLoad) - - def _get_supportedCommands(self): - attribute = protocol.SpeechAttribute.SUPPORTED_COMMANDS - try: - value = self._attributeValueProcessor.getValue(attribute, fallBackToDefault=False) - except KeyError: - value = self._attributeValueProcessor._getDefaultValue(attribute) - self.requestRemoteAttribute(attribute) - return value - - @protocol.attributeReceiver(protocol.SpeechAttribute.LANGUAGE, defaultValue=getLanguage()) - def _incoming_language(self, payload: bytes) -> Optional[str]: - assert len(payload) > 0 - return self._unpickle(payload) - - def _get_language(self): - attribute = protocol.SpeechAttribute.LANGUAGE - try: - value = self._attributeValueProcessor.getValue(attribute, fallBackToDefault=False) - except KeyError: - value = self._attributeValueProcessor._getDefaultValue(attribute) - self.requestRemoteAttribute(attribute) - return value - - @protocol.commandHandler(protocol.SpeechCommand.INDEX_REACHED) - def _command_indexReached(self, incomingPayload: bytes): - assert len(incomingPayload) == 2 - index = int.from_bytes(incomingPayload, sys.byteorder) - if index: - synthDriverHandler.synthIndexReached.notify(synth=self, index=index) - else: - assert index == 0 - synthDriverHandler.synthDoneSpeaking.notify(synth=self) - - def _handleRemoteDriverChange(self): - super()._handleRemoteDriverChange() - synthDriverHandler.changeVoice(self, None) + # Translators: Name for a remote braille display. + description = _("Remote speech") + supportedNotifications = { + synthDriverHandler.synthIndexReached, + synthDriverHandler.synthDoneSpeaking, + } + driverType = protocol.DriverType.SPEECH + synthRemoteDisconnected = Action() + fallbackSynth: str + _localSettings = [ + DriverSetting( + id="fallbackSynth", + # Translators: The name of a remote synthesizer setting to select the fallback synthesizer. + displayNameWithAccelerator=_("&Fallback synthesizer"), + availableInSettingsRing=True, + defaultVal=AUTOMATIC_PORT[0], + ) + ] + + @classmethod + def _get_availableFallbacksynths(cls): + dct = OrderedDict() + dct[AUTOMATIC_PORT[0]] = StringParameterInfo(*AUTOMATIC_PORT) + dct.update( + (n, StringParameterInfo(n, d)) for n, d in synthDriverHandler.getSynthList() if n != cls.name + ) + return dct + + def __init__(self, port="auto"): + super().__init__(port) + nvwave.decide_playWaveFile.register(self.handle_decidePlayWaveFile) + tones.decide_beep.register(self.handle_decideBeep) + + def terminate(self): + tones.decide_beep.unregister(self.handle_decideBeep) + nvwave.decide_playWaveFile.unregister(self.handle_decidePlayWaveFile) + super().terminate() + + def handle_decideBeep(self, **kwargs) -> bool: + log.debug(f"Sending BEEP command: {kwargs}") + try: + self.writeMessage(protocol.SpeechCommand.BEEP, self._pickle(kwargs)) + except WindowsError: + log.warning("Error calling handle_decideBeep", exc_info=True) + return True + return False + + def handle_decidePlayWaveFile(self, **kwargs) -> bool: + kwargs["fileName"] = os.path.relpath(kwargs["fileName"], globalVars.appDir) + log.debug(f"Sending PLAY_WAVE_FILE command: {kwargs}") + try: + self.writeMessage(protocol.SpeechCommand.PLAY_WAVE_FILE, self._pickle(kwargs)) + except WindowsError: + log.warning("Error calling handle_decidePlayWaveFile", exc_info=True) + return True + return False + + def _handleRemoteDisconnect(self): + self.synthRemoteDisconnected.notify(synth=self) + + def speak(self, speechSequence): + try: + self.writeMessage(protocol.SpeechCommand.SPEAK, self._pickle(speechSequence)) + except WindowsError: + log.error("Error speaking", exc_info=True) + self._handleRemoteDisconnect() + + def cancel(self): + try: + self.writeMessage(protocol.SpeechCommand.CANCEL) + except WindowsError: + log.warning("Error cancelling speech", exc_info=True) + + def pause(self, switch): + try: + self.writeMessage(protocol.SpeechCommand.PAUSE, boolToByte(switch)) + except WindowsError: + log.warning("Error pausing speech", exc_info=True) + + @protocol.attributeReceiver(protocol.SpeechAttribute.SUPPORTED_COMMANDS, defaultValue=frozenset()) + def _incoming_supportedCommands(self, payLoad: bytes) -> frozenset: + assert len(payLoad) > 0 + return self._unpickle(payLoad) + + def _get_supportedCommands(self): + attribute = protocol.SpeechAttribute.SUPPORTED_COMMANDS + try: + value = self._attributeValueProcessor.getValue(attribute, fallBackToDefault=False) + except KeyError: + value = self._attributeValueProcessor._getDefaultValue(attribute) + self.requestRemoteAttribute(attribute) + return value + + @protocol.attributeReceiver(protocol.SpeechAttribute.LANGUAGE, defaultValue=getLanguage()) + def _incoming_language(self, payload: bytes) -> Optional[str]: + assert len(payload) > 0 + return self._unpickle(payload) + + def _get_language(self): + attribute = protocol.SpeechAttribute.LANGUAGE + try: + value = self._attributeValueProcessor.getValue(attribute, fallBackToDefault=False) + except KeyError: + value = self._attributeValueProcessor._getDefaultValue(attribute) + self.requestRemoteAttribute(attribute) + return value + + @protocol.commandHandler(protocol.SpeechCommand.INDEX_REACHED) + def _command_indexReached(self, incomingPayload: bytes): + assert len(incomingPayload) == 2 + index = int.from_bytes(incomingPayload, sys.byteorder) + if index: + synthDriverHandler.synthIndexReached.notify(synth=self, index=index) + else: + assert index == 0 + synthDriverHandler.synthDoneSpeaking.notify(synth=self) + + def _handleRemoteDriverChange(self): + super()._handleRemoteDriverChange() + synthDriverHandler.changeVoice(self, None) SynthDriver = remoteSynthDriver diff --git a/buildVars.py b/buildVars.py index 5862be3..c5fe89d 100644 --- a/buildVars.py +++ b/buildVars.py @@ -10,44 +10,44 @@ # To avoid initializing translations in this module we simply roll our own "fake" `_` function # which returns whatever is given to it as an argument. def _(arg): - return arg + return arg # Add-on information variables addon_info = { - # add-on Name/identifier, internal for NVDA - "addon_name": "rdAccess", - # Add-on summary, usually the user visible name of the addon. - # Translators: Summary for this add-on - # to be shown on installation and add-on information found in Add-ons Manager. - "addon_summary": _("Remote Desktop Accessibility"), - # Add-on description - # Translators: Long description to be shown for this add-on on add-on information from add-ons manager - "addon_description": _( - "Allows using speech and braille with Microsoft Remote Desktop, Citrix Workspace and VMware Horizon" - ), - # version - "addon_version": "1.1.1", - # Author(s) - "addon_author": "Leonard de Ruijter ", - # URL for the add-on documentation support - "addon_url": "https://github.com/leonardder/rdAccess", - # URL for the add-on repository where the source code can be found - "addon_sourceURL": "https://github.com/leonardder/rdAccess", - # Documentation file name - "addon_docFileName": "readme.html", - # Minimum NVDA version supported (e.g. "2018.3.0", minor version is optional) - "addon_minimumNVDAVersion": "2023.2", - # Last NVDA version supported/tested (e.g. "2018.4.0", ideally more recent than minimum version) - "addon_lastTestedNVDAVersion": "2024.1", - # Add-on update channel (default is None, denoting stable releases, - # and for development releases, use "dev".) - # Do not change unless you know what you are doing! - "addon_updateChannel": None, - # Add-on license such as GPL 2 - "addon_license": "GPL 2", - # URL for the license document the ad-on is licensed under - "addon_licenseURL": None, + # add-on Name/identifier, internal for NVDA + "addon_name": "rdAccess", + # Add-on summary, usually the user visible name of the addon. + # Translators: Summary for this add-on + # to be shown on installation and add-on information found in Add-ons Manager. + "addon_summary": _("Remote Desktop Accessibility"), + # Add-on description + # Translators: Long description to be shown for this add-on on add-on information from add-ons manager + "addon_description": _( + "Allows using speech and braille with Microsoft Remote Desktop, Citrix Workspace and VMware Horizon" + ), + # version + "addon_version": "1.1.1", + # Author(s) + "addon_author": "Leonard de Ruijter ", + # URL for the add-on documentation support + "addon_url": "https://github.com/leonardder/rdAccess", + # URL for the add-on repository where the source code can be found + "addon_sourceURL": "https://github.com/leonardder/rdAccess", + # Documentation file name + "addon_docFileName": "readme.html", + # Minimum NVDA version supported (e.g. "2018.3.0", minor version is optional) + "addon_minimumNVDAVersion": "2023.2", + # Last NVDA version supported/tested (e.g. "2018.4.0", ideally more recent than minimum version) + "addon_lastTestedNVDAVersion": "2024.1", + # Add-on update channel (default is None, denoting stable releases, + # and for development releases, use "dev".) + # Do not change unless you know what you are doing! + "addon_updateChannel": None, + # Add-on license such as GPL 2 + "addon_license": "GPL 2", + # URL for the license document the ad-on is licensed under + "addon_licenseURL": None, } # Define the python files that are the sources of your add-on. @@ -59,14 +59,14 @@ def _(arg): # For more information on SCons Glob expressions please take a look at: # https://scons.org/doc/production/HTML/scons-user/apd.html pythonSources = [ - "addon/brailleDisplayDrivers/*.py", - "addon/globalPlugins/rdAccess/*.py", - "addon/globalPlugins/rdAccess/handlers/*.py", - "addon/lib/*.py", - "addon/lib/driver/*.py", - "addon/lib/protocol/*.py", - "addon/synthDrivers/*.py", - "addon/installTasks.py", + "addon/brailleDisplayDrivers/*.py", + "addon/globalPlugins/rdAccess/*.py", + "addon/globalPlugins/rdAccess/handlers/*.py", + "addon/lib/*.py", + "addon/lib/driver/*.py", + "addon/lib/protocol/*.py", + "addon/synthDrivers/*.py", + "addon/installTasks.py", ] # Files that contain strings for translation. Usually your python sources @@ -87,5 +87,5 @@ def _(arg): # Extensions string must be of the form "markdown.extensions.extensionName" # e.g. "markdown.extensions.tables" to add tables. markdownExtensions = [ - "markdown.extensions.smarty", + "markdown.extensions.smarty", ] diff --git a/pyproject.toml b/pyproject.toml index 8d7e612..db3cbe2 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -7,7 +7,9 @@ exclude = [ ".git", "__pycache__", ] -ignore = [] +ignore = [ + "W191" +] line-length = 110 select = [ "C9", diff --git a/site_scons/site_tools/gettexttool/__init__.py b/site_scons/site_tools/gettexttool/__init__.py index ec36df2..d4bb0b8 100644 --- a/site_scons/site_tools/gettexttool/__init__.py +++ b/site_scons/site_tools/gettexttool/__init__.py @@ -19,36 +19,36 @@ def exists(env): - return True + return True XGETTEXT_COMMON_ARGS = ( - "--msgid-bugs-address='$gettext_package_bugs_address' " - "--package-name='$gettext_package_name' " - "--package-version='$gettext_package_version' " - "--keyword=pgettext:1c,2 " - "-c -o $TARGET $SOURCES" + "--msgid-bugs-address='$gettext_package_bugs_address' " + "--package-name='$gettext_package_name' " + "--package-version='$gettext_package_version' " + "--keyword=pgettext:1c,2 " + "-c -o $TARGET $SOURCES" ) def generate(env): - env.SetDefault(gettext_package_bugs_address="example@example.com") - env.SetDefault(gettext_package_name="") - env.SetDefault(gettext_package_version="") - - env["BUILDERS"]["gettextMoFile"] = env.Builder( - action=Action("msgfmt -o $TARGET $SOURCE", "Compiling translation $SOURCE"), - suffix=".mo", - src_suffix=".po", - ) - - env["BUILDERS"]["gettextPotFile"] = env.Builder( - action=Action("xgettext " + XGETTEXT_COMMON_ARGS, "Generating pot file $TARGET"), suffix=".pot" - ) - - env["BUILDERS"]["gettextMergePotFile"] = env.Builder( - action=Action( - "xgettext " + "--omit-header --no-location " + XGETTEXT_COMMON_ARGS, "Generating pot file $TARGET" - ), - suffix=".pot", - ) + env.SetDefault(gettext_package_bugs_address="example@example.com") + env.SetDefault(gettext_package_name="") + env.SetDefault(gettext_package_version="") + + env["BUILDERS"]["gettextMoFile"] = env.Builder( + action=Action("msgfmt -o $TARGET $SOURCE", "Compiling translation $SOURCE"), + suffix=".mo", + src_suffix=".po", + ) + + env["BUILDERS"]["gettextPotFile"] = env.Builder( + action=Action("xgettext " + XGETTEXT_COMMON_ARGS, "Generating pot file $TARGET"), suffix=".pot" + ) + + env["BUILDERS"]["gettextMergePotFile"] = env.Builder( + action=Action( + "xgettext " + "--omit-header --no-location " + XGETTEXT_COMMON_ARGS, "Generating pot file $TARGET" + ), + suffix=".pot", + ) From ed84a9e32e716693b1db39ac7eeb8da74068ff1c Mon Sep 17 00:00:00 2001 From: Leonard de Ruijter Date: Wed, 27 Sep 2023 22:38:47 +0200 Subject: [PATCH 03/12] Lint with py3.11 --- .github/workflows/lint.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/lint.yaml b/.github/workflows/lint.yaml index 4e8541d..d6ed90c 100644 --- a/.github/workflows/lint.yaml +++ b/.github/workflows/lint.yaml @@ -15,7 +15,7 @@ jobs: - name: Install Python uses: actions/setup-python@v4 with: - python-version: "3.7" + python-version: "3.11" - name: Install dependencies run: | python -m pip install --upgrade pip From 6684faea1c5e83fdd061557822812bcb653146cc Mon Sep 17 00:00:00 2001 From: Leonard de Ruijter Date: Sat, 21 Oct 2023 20:45:30 +0200 Subject: [PATCH 04/12] Use ruff.toml --- pyproject.toml => ruff.toml | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) rename pyproject.toml => ruff.toml (82%) diff --git a/pyproject.toml b/ruff.toml similarity index 82% rename from pyproject.toml rename to ruff.toml index db3cbe2..2c55462 100644 --- a/pyproject.toml +++ b/ruff.toml @@ -1,4 +1,3 @@ -[tool.ruff] builtins = [ "_", "pgettext", @@ -18,9 +17,9 @@ select = [ "W", ] -[tool.ruff.mccabe] +[mccabe] max-complexity = 15 -[tool.ruff.format] +[format] quote-style = "double" indent-style = "tab" From 1a82166b7f1497aa2a1afce91d6fd665c9148610 Mon Sep 17 00:00:00 2001 From: Leonard de Ruijter Date: Sat, 21 Oct 2023 20:47:59 +0200 Subject: [PATCH 05/12] Update ruff.toml --- ruff.toml | 34 ++++++++++++++++++++++++---------- 1 file changed, 24 insertions(+), 10 deletions(-) diff --git a/ruff.toml b/ruff.toml index 2c55462..cfbd9d3 100644 --- a/ruff.toml +++ b/ruff.toml @@ -1,25 +1,39 @@ builtins = [ - "_", - "pgettext", + "_", # translation lookup + "ngettext", # translation lookup + "pgettext", # translation lookup + "npgettext", # translation lookup ] -exclude = [ - ".git", - "__pycache__", +logger-objects = ["logHandler.log"] +exclude = [ # don't bother looking in the following subdirectories / files. + ".git", + "__pycache__", ] ignore = [ - "W191" + "W191", # indentation contains tabs ] line-length = 110 select = [ - "C9", - "E", - "F", - "W", + "A", # flake8-builtins + "ARG", # flake8-unused-arguments + "B", # flake8-bugbear + "C90", # mccabe + "E", # pycodestyle Error + "F", # Pyflakes + "FIX", # flake8-fixme + "I", # isort + "INT", # flake8-gettext + "UP", # pyupgrade + "W", # pycodestyle Warning ] +show-source = true +src = ["addon"] +target-version = "py37" [mccabe] max-complexity = 15 [format] +line-ending = "lf" quote-style = "double" indent-style = "tab" From 22a3bbbfe16baf885d822b5a7cc1f42161c7bab8 Mon Sep 17 00:00:00 2001 From: Leonard de Ruijter Date: Fri, 17 Nov 2023 21:02:23 +0100 Subject: [PATCH 06/12] Enable preview mode --- ruff.toml | 1 + 1 file changed, 1 insertion(+) diff --git a/ruff.toml b/ruff.toml index cfbd9d3..ab9bc17 100644 --- a/ruff.toml +++ b/ruff.toml @@ -13,6 +13,7 @@ ignore = [ "W191", # indentation contains tabs ] line-length = 110 +preview = true select = [ "A", # flake8-builtins "ARG", # flake8-unused-arguments From 20cac574fcd0dcd77a2135258aa643cb633f95e9 Mon Sep 17 00:00:00 2001 From: Leonard de Ruijter Date: Fri, 17 Nov 2023 21:05:18 +0100 Subject: [PATCH 07/12] Formatting --- .pre-commit-config.yaml | 2 +- ruff.toml | 2 +- sconstruct | 412 ++++++++++++++++++++-------------------- 3 files changed, 208 insertions(+), 208 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index e7ba0b2..53cd8ae 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -7,7 +7,7 @@ repos: - id: check-yaml - repo: https://github.com/astral-sh/ruff-pre-commit # Ruff version. - rev: v0.0.291 + rev: v0.1.6 hooks: - id: ruff args: [--fix, --exit-non-zero-on-fix] diff --git a/ruff.toml b/ruff.toml index ab9bc17..55df733 100644 --- a/ruff.toml +++ b/ruff.toml @@ -35,6 +35,6 @@ target-version = "py37" max-complexity = 15 [format] -line-ending = "lf" +line-ending = "auto" quote-style = "double" indent-style = "tab" diff --git a/sconstruct b/sconstruct index f4dde79..4dfbede 100644 --- a/sconstruct +++ b/sconstruct @@ -23,72 +23,72 @@ import buildVars # NOQA: E402 def md2html(source, dest): - import markdown - - # Use extensions if defined. - mdExtensions = buildVars.markdownExtensions - lang = os.path.basename(os.path.dirname(source)).replace("_", "-") - localeLang = os.path.basename(os.path.dirname(source)) - try: - _ = gettext.translation( - "nvda", localedir=os.path.join("addon", "locale"), languages=[localeLang] - ).gettext - summary = _(buildVars.addon_info["addon_summary"]) - except Exception: - summary = buildVars.addon_info["addon_summary"] - title = "{addonSummary} {addonVersion}".format( - addonSummary=summary, addonVersion=buildVars.addon_info["addon_version"] - ) - headerDic = { - '[[!meta title="': "# ", - '"]]': " #", - } - with codecs.open(source, "r", "utf-8") as f: - mdText = f.read() - for k, v in headerDic.items(): - mdText = mdText.replace(k, v, 1) - htmlText = markdown.markdown(mdText, extensions=mdExtensions) - # Optimization: build resulting HTML text in one go instead of writing parts separately. - docText = "\n".join( - [ - "", - '' % lang, - "", - '' '', - '', - "%s" % title, - " \n", - htmlText, - "\n", - ] - ) - with codecs.open(dest, "w", "utf-8") as f: - f.write(docText) + import markdown + + # Use extensions if defined. + mdExtensions = buildVars.markdownExtensions + lang = os.path.basename(os.path.dirname(source)).replace("_", "-") + localeLang = os.path.basename(os.path.dirname(source)) + try: + _ = gettext.translation( + "nvda", localedir=os.path.join("addon", "locale"), languages=[localeLang] + ).gettext + summary = _(buildVars.addon_info["addon_summary"]) + except Exception: + summary = buildVars.addon_info["addon_summary"] + title = "{addonSummary} {addonVersion}".format( + addonSummary=summary, addonVersion=buildVars.addon_info["addon_version"] + ) + headerDic = { + '[[!meta title="': "# ", + '"]]': " #", + } + with codecs.open(source, "r", "utf-8") as f: + mdText = f.read() + for k, v in headerDic.items(): + mdText = mdText.replace(k, v, 1) + htmlText = markdown.markdown(mdText, extensions=mdExtensions) + # Optimization: build resulting HTML text in one go instead of writing parts separately. + docText = "\n".join( + [ + "", + '' % lang, + "", + '' '', + '', + "%s" % title, + " \n", + htmlText, + "\n", + ] + ) + with codecs.open(dest, "w", "utf-8") as f: + f.write(docText) def mdTool(env): - mdAction = env.Action( - lambda target, source, env: md2html(source[0].path, target[0].path), - lambda target, source, env: "Generating % s" % target[0], - ) - mdBuilder = env.Builder( - action=mdAction, - suffix=".html", - src_suffix=".md", - ) - env["BUILDERS"]["markdown"] = mdBuilder + mdAction = env.Action( + lambda target, source, env: md2html(source[0].path, target[0].path), + lambda target, source, env: "Generating % s" % target[0], + ) + mdBuilder = env.Builder( + action=mdAction, + suffix=".html", + src_suffix=".md", + ) + env["BUILDERS"]["markdown"] = mdBuilder def validateVersionNumber(key, val, env): - # Used to make sure version major.minor.patch are integers to comply with NV Access add-on store. - # Ignore all this if version number is not specified, in which case json generator will validate this info. - if val == "0.0.0": - return - versionNumber = val.split(".") - if len(versionNumber) < 3: - raise ValueError("versionNumber must have three parts (major.minor.patch)") - if not all([part.isnumeric() for part in versionNumber]): - raise ValueError("versionNumber (major.minor.patch) must be integers") + # Used to make sure version major.minor.patch are integers to comply with NV Access add-on store. + # Ignore all this if version number is not specified, in which case json generator will validate this info. + if val == "0.0.0": + return + versionNumber = val.split(".") + if len(versionNumber) < 3: + raise ValueError("versionNumber must have three parts (major.minor.patch)") + if not all([part.isnumeric() for part in versionNumber]): + raise ValueError("versionNumber (major.minor.patch) must be integers") vars = Variables() @@ -101,18 +101,18 @@ env = Environment(variables=vars, ENV=os.environ, tools=["gettexttool", mdTool]) env.Append(**buildVars.addon_info) if env["dev"]: - import datetime - - buildDate = datetime.datetime.now() - year, month, day = str(buildDate.year), str(buildDate.month), str(buildDate.day) - versionTimestamp = "".join([year, month.zfill(2), day.zfill(2)]) - env["addon_version"] = f"{versionTimestamp}.0.0" - env["versionNumber"] = f"{versionTimestamp}.0.0" - env["channel"] = "dev" + import datetime + + buildDate = datetime.datetime.now() + year, month, day = str(buildDate.year), str(buildDate.month), str(buildDate.day) + versionTimestamp = "".join([year, month.zfill(2), day.zfill(2)]) + env["addon_version"] = f"{versionTimestamp}.0.0" + env["versionNumber"] = f"{versionTimestamp}.0.0" + env["channel"] = "dev" elif env["version"] is not None: - env["addon_version"] = env["version"] + env["addon_version"] = env["version"] if "channel" in env and env["channel"] is not None: - env["addon_updateChannel"] = env["channel"] + env["addon_updateChannel"] = env["channel"] buildVars.addon_info["addon_version"] = env["addon_version"] buildVars.addon_info["addon_updateChannel"] = env["addon_updateChannel"] @@ -121,30 +121,30 @@ addonFile = env.File("${addon_name}-${addon_version}.nvda-addon") def addonGenerator(target, source, env, for_signature): - action = env.Action( - lambda target, source, env: createAddonBundleFromPath(source[0].abspath, target[0].abspath) and None, - lambda target, source, env: "Generating Addon %s" % target[0], - ) - return action + action = env.Action( + lambda target, source, env: createAddonBundleFromPath(source[0].abspath, target[0].abspath) and None, + lambda target, source, env: "Generating Addon %s" % target[0], + ) + return action def manifestGenerator(target, source, env, for_signature): - action = env.Action( - lambda target, source, env: generateManifest(source[0].abspath, target[0].abspath) and None, - lambda target, source, env: "Generating manifest %s" % target[0], - ) - return action + action = env.Action( + lambda target, source, env: generateManifest(source[0].abspath, target[0].abspath) and None, + lambda target, source, env: "Generating manifest %s" % target[0], + ) + return action def translatedManifestGenerator(target, source, env, for_signature): - dir = os.path.abspath(os.path.join(os.path.dirname(str(source[0])), "..")) - lang = os.path.basename(dir) - action = env.Action( - lambda target, source, env: generateTranslatedManifest(source[1].abspath, lang, target[0].abspath) - and None, - lambda target, source, env: "Generating translated manifest %s" % target[0], - ) - return action + dir = os.path.abspath(os.path.join(os.path.dirname(str(source[0])), "..")) + lang = os.path.basename(dir) + action = env.Action( + lambda target, source, env: generateTranslatedManifest(source[1].abspath, lang, target[0].abspath) + and None, + lambda target, source, env: "Generating translated manifest %s" % target[0], + ) + return action env["BUILDERS"]["NVDAAddon"] = Builder(generator=addonGenerator) @@ -153,124 +153,124 @@ env["BUILDERS"]["NVDATranslatedManifest"] = Builder(generator=translatedManifest def createAddonHelp(dir): - docsDir = os.path.join(dir, "doc") - if os.path.isfile("style.css"): - cssPath = os.path.join(docsDir, "style.css") - cssTarget = env.Command(cssPath, "style.css", Copy("$TARGET", "$SOURCE")) - env.Depends(addon, cssTarget) - if os.path.isfile("readme.md"): - readmePath = os.path.join(docsDir, buildVars.baseLanguage, "readme.md") - readmeTarget = env.Command(readmePath, "readme.md", Copy("$TARGET", "$SOURCE")) - env.Depends(addon, readmeTarget) + docsDir = os.path.join(dir, "doc") + if os.path.isfile("style.css"): + cssPath = os.path.join(docsDir, "style.css") + cssTarget = env.Command(cssPath, "style.css", Copy("$TARGET", "$SOURCE")) + env.Depends(addon, cssTarget) + if os.path.isfile("readme.md"): + readmePath = os.path.join(docsDir, buildVars.baseLanguage, "readme.md") + readmeTarget = env.Command(readmePath, "readme.md", Copy("$TARGET", "$SOURCE")) + env.Depends(addon, readmeTarget) def createAddonBundleFromPath(path, dest): - """Creates a bundle from a directory that contains an addon manifest file.""" - basedir = os.path.abspath(path) - with zipfile.ZipFile(dest, "w", zipfile.ZIP_DEFLATED) as z: - # FIXME: the include/exclude feature may or may not be useful. Also python files can be pre-compiled. - for dir, dirnames, filenames in os.walk(basedir): - relativePath = os.path.relpath(dir, basedir) - for filename in filenames: - pathInBundle = os.path.join(relativePath, filename) - absPath = os.path.join(dir, filename) - if pathInBundle not in buildVars.excludedFiles: - z.write(absPath, pathInBundle) - createAddonStoreJson(dest) - return dest + """Creates a bundle from a directory that contains an addon manifest file.""" + basedir = os.path.abspath(path) + with zipfile.ZipFile(dest, "w", zipfile.ZIP_DEFLATED) as z: + # FIXME: the include/exclude feature may or may not be useful. Also python files can be pre-compiled. + for dir, dirnames, filenames in os.walk(basedir): + relativePath = os.path.relpath(dir, basedir) + for filename in filenames: + pathInBundle = os.path.join(relativePath, filename) + absPath = os.path.join(dir, filename) + if pathInBundle not in buildVars.excludedFiles: + z.write(absPath, pathInBundle) + createAddonStoreJson(dest) + return dest def createAddonStoreJson(bundle): - """Creates add-on store JSON file from an add-on package and manifest data.""" - import json - import hashlib - - # Set different json file names and version number properties based on version number parsing results. - if env["versionNumber"] == "0.0.0": - env["versionNumber"] = buildVars.addon_info["addon_version"] - versionNumberParsed = env["versionNumber"].split(".") - if all([part.isnumeric() for part in versionNumberParsed]): - if len(versionNumberParsed) == 1: - versionNumberParsed += ["0", "0"] - elif len(versionNumberParsed) == 2: - versionNumberParsed.append("0") - else: - versionNumberParsed = [] - if len(versionNumberParsed): - major, minor, patch = [int(part) for part in versionNumberParsed] - jsonFilename = f"{major}.{minor}.{patch}.json" - else: - jsonFilename = f'{buildVars.addon_info["addon_version"]}.json' - major, minor, patch = 0, 0, 0 - print("Generating % s" % jsonFilename) - sha256 = hashlib.sha256() - with open(bundle, "rb") as f: - for byte_block in iter(lambda: f.read(65536), b""): - sha256.update(byte_block) - hashValue = sha256.hexdigest() - try: - minimumNVDAVersion = buildVars.addon_info["addon_minimumNVDAVersion"].split(".") - except AttributeError: - minimumNVDAVersion = [0, 0, 0] - minMajor, minMinor = minimumNVDAVersion[:2] - minPatch = minimumNVDAVersion[-1] if len(minimumNVDAVersion) == 3 else "0" - try: - lastTestedNVDAVersion = buildVars.addon_info["addon_lastTestedNVDAVersion"].split(".") - except AttributeError: - lastTestedNVDAVersion = [0, 0, 0] - lastTestedMajor, lastTestedMinor = lastTestedNVDAVersion[:2] - lastTestedPatch = lastTestedNVDAVersion[-1] if len(lastTestedNVDAVersion) == 3 else "0" - channel = buildVars.addon_info["addon_updateChannel"] - if channel is None: - channel = "stable" - addonStoreEntry = { - "addonId": buildVars.addon_info["addon_name"], - "displayName": buildVars.addon_info["addon_summary"], - "URL": "", - "description": buildVars.addon_info["addon_description"], - "sha256": hashValue, - "homepage": buildVars.addon_info["addon_url"], - "addonVersionName": buildVars.addon_info["addon_version"], - "addonVersionNumber": {"major": major, "minor": minor, "patch": patch}, - "minNVDAVersion": {"major": int(minMajor), "minor": int(minMinor), "patch": int(minPatch)}, - "lastTestedVersion": { - "major": int(lastTestedMajor), - "minor": int(lastTestedMinor), - "patch": int(lastTestedPatch), - }, - "channel": channel, - "publisher": "", - "sourceURL": buildVars.addon_info["addon_sourceURL"], - "license": buildVars.addon_info["addon_license"], - "licenseURL": buildVars.addon_info["addon_licenseURL"], - } - with open(jsonFilename, "w") as addonStoreJson: - json.dump(addonStoreEntry, addonStoreJson, indent="\t") + """Creates add-on store JSON file from an add-on package and manifest data.""" + import json + import hashlib + + # Set different json file names and version number properties based on version number parsing results. + if env["versionNumber"] == "0.0.0": + env["versionNumber"] = buildVars.addon_info["addon_version"] + versionNumberParsed = env["versionNumber"].split(".") + if all([part.isnumeric() for part in versionNumberParsed]): + if len(versionNumberParsed) == 1: + versionNumberParsed += ["0", "0"] + elif len(versionNumberParsed) == 2: + versionNumberParsed.append("0") + else: + versionNumberParsed = [] + if len(versionNumberParsed): + major, minor, patch = [int(part) for part in versionNumberParsed] + jsonFilename = f"{major}.{minor}.{patch}.json" + else: + jsonFilename = f'{buildVars.addon_info["addon_version"]}.json' + major, minor, patch = 0, 0, 0 + print("Generating % s" % jsonFilename) + sha256 = hashlib.sha256() + with open(bundle, "rb") as f: + for byte_block in iter(lambda: f.read(65536), b""): + sha256.update(byte_block) + hashValue = sha256.hexdigest() + try: + minimumNVDAVersion = buildVars.addon_info["addon_minimumNVDAVersion"].split(".") + except AttributeError: + minimumNVDAVersion = [0, 0, 0] + minMajor, minMinor = minimumNVDAVersion[:2] + minPatch = minimumNVDAVersion[-1] if len(minimumNVDAVersion) == 3 else "0" + try: + lastTestedNVDAVersion = buildVars.addon_info["addon_lastTestedNVDAVersion"].split(".") + except AttributeError: + lastTestedNVDAVersion = [0, 0, 0] + lastTestedMajor, lastTestedMinor = lastTestedNVDAVersion[:2] + lastTestedPatch = lastTestedNVDAVersion[-1] if len(lastTestedNVDAVersion) == 3 else "0" + channel = buildVars.addon_info["addon_updateChannel"] + if channel is None: + channel = "stable" + addonStoreEntry = { + "addonId": buildVars.addon_info["addon_name"], + "displayName": buildVars.addon_info["addon_summary"], + "URL": "", + "description": buildVars.addon_info["addon_description"], + "sha256": hashValue, + "homepage": buildVars.addon_info["addon_url"], + "addonVersionName": buildVars.addon_info["addon_version"], + "addonVersionNumber": {"major": major, "minor": minor, "patch": patch}, + "minNVDAVersion": {"major": int(minMajor), "minor": int(minMinor), "patch": int(minPatch)}, + "lastTestedVersion": { + "major": int(lastTestedMajor), + "minor": int(lastTestedMinor), + "patch": int(lastTestedPatch), + }, + "channel": channel, + "publisher": "", + "sourceURL": buildVars.addon_info["addon_sourceURL"], + "license": buildVars.addon_info["addon_license"], + "licenseURL": buildVars.addon_info["addon_licenseURL"], + } + with open(jsonFilename, "w") as addonStoreJson: + json.dump(addonStoreEntry, addonStoreJson, indent="\t") def generateManifest(source, dest): - addon_info = buildVars.addon_info - with codecs.open(source, "r", "utf-8") as f: - manifest_template = f.read() - manifest = manifest_template.format(**addon_info) - with codecs.open(dest, "w", "utf-8") as f: - f.write(manifest) + addon_info = buildVars.addon_info + with codecs.open(source, "r", "utf-8") as f: + manifest_template = f.read() + manifest = manifest_template.format(**addon_info) + with codecs.open(dest, "w", "utf-8") as f: + f.write(manifest) def generateTranslatedManifest(source, language, out): - _ = gettext.translation("nvda", localedir=os.path.join("addon", "locale"), languages=[language]).gettext - vars = {} - for var in ("addon_summary", "addon_description"): - vars[var] = _(buildVars.addon_info[var]) - with codecs.open(source, "r", "utf-8") as f: - manifest_template = f.read() - result = manifest_template.format(**vars) - with codecs.open(out, "w", "utf-8") as f: - f.write(result) + _ = gettext.translation("nvda", localedir=os.path.join("addon", "locale"), languages=[language]).gettext + vars = {} + for var in ("addon_summary", "addon_description"): + vars[var] = _(buildVars.addon_info[var]) + with codecs.open(source, "r", "utf-8") as f: + manifest_template = f.read() + result = manifest_template.format(**vars) + with codecs.open(out, "w", "utf-8") as f: + f.write(result) def expandGlobs(files): - return [f for pattern in files for f in env.Glob(pattern)] + return [f for pattern in files for f in env.Glob(pattern)] addon = env.NVDAAddon(addonFile, env.Dir("addon")) @@ -279,33 +279,33 @@ langDirs = [f for f in env.Glob(os.path.join("addon", "locale", "*"))] # Allow all NVDA's gettext po files to be compiled in source/locale, and manifest files to be generated for dir in langDirs: - poFile = dir.File(os.path.join("LC_MESSAGES", "nvda.po")) - moFile = env.gettextMoFile(poFile) - env.Depends(moFile, poFile) - translatedManifest = env.NVDATranslatedManifest( - dir.File("manifest.ini"), [moFile, os.path.join("manifest-translated.ini.tpl")] - ) - env.Depends(translatedManifest, ["buildVars.py"]) - env.Depends(addon, [translatedManifest, moFile]) + poFile = dir.File(os.path.join("LC_MESSAGES", "nvda.po")) + moFile = env.gettextMoFile(poFile) + env.Depends(moFile, poFile) + translatedManifest = env.NVDATranslatedManifest( + dir.File("manifest.ini"), [moFile, os.path.join("manifest-translated.ini.tpl")] + ) + env.Depends(translatedManifest, ["buildVars.py"]) + env.Depends(addon, [translatedManifest, moFile]) pythonFiles = expandGlobs(buildVars.pythonSources) for file in pythonFiles: - env.Depends(addon, file) + env.Depends(addon, file) # Convert markdown files to html # We need at least doc in English and should enable the Help button for the add-on in Add-ons Manager createAddonHelp("addon") for mdFile in env.Glob(os.path.join("addon", "doc", "*", "*.md")): - htmlFile = env.markdown(mdFile) - env.Depends(htmlFile, mdFile) - env.Depends(addon, htmlFile) + htmlFile = env.markdown(mdFile) + env.Depends(htmlFile, mdFile) + env.Depends(addon, htmlFile) # Pot target i18nFiles = expandGlobs(buildVars.i18nSources) gettextvars = { - "gettext_package_bugs_address": "nvda-translations@groups.io", - "gettext_package_name": buildVars.addon_info["addon_name"], - "gettext_package_version": buildVars.addon_info["addon_version"], + "gettext_package_bugs_address": "nvda-translations@groups.io", + "gettext_package_name": buildVars.addon_info["addon_name"], + "gettext_package_version": buildVars.addon_info["addon_version"], } pot = env.gettextPotFile("${addon_name}.pot", i18nFiles, **gettextvars) From fabf5c1fffd9b5b1db9d62130fc8cfbf10b728e7 Mon Sep 17 00:00:00 2001 From: Leonard de Ruijter Date: Sat, 25 Nov 2023 10:20:47 +0100 Subject: [PATCH 08/12] Linting and fixes --- addon/brailleDisplayDrivers/remote.py | 2 +- .../rdAccess/directoryChanges.py | 8 ++--- .../rdAccess/handlers/_remoteHandler.py | 4 +-- .../rdAccess/handlers/remoteBrailleHandler.py | 2 +- .../rdAccess/handlers/remoteSpeechHandler.py | 6 ++-- .../rdAccess/secureDesktopHandling.py | 2 +- addon/globalPlugins/rdAccess/synthDetect.py | 1 + addon/installTasks.py | 4 +-- addon/lib/driver/__init__.py | 19 ++++++----- addon/lib/inputTime.py | 2 +- addon/lib/namedPipe.py | 7 ++-- addon/lib/protocol/__init__.py | 4 +-- addon/lib/protocol/braille.py | 4 +-- addon/synthDrivers/remote.py | 34 +++++++++---------- ruff.toml | 2 +- 15 files changed, 52 insertions(+), 49 deletions(-) diff --git a/addon/brailleDisplayDrivers/remote.py b/addon/brailleDisplayDrivers/remote.py index b3b12e0..7a4f529 100644 --- a/addon/brailleDisplayDrivers/remote.py +++ b/addon/brailleDisplayDrivers/remote.py @@ -66,7 +66,7 @@ def _incoming_gestureMapUpdate(self, payload: bytes) -> inputCore.GlobalGestureM return self._unpickle(payload) @_incoming_gestureMapUpdate.defaultValueGetter - def _default_gestureMap(self, attribute: protocol.AttributeT): + def _default_gestureMap(self, _attribute: protocol.AttributeT): return inputCore.GlobalGestureMap() def _get_gestureMap(self) -> inputCore.GlobalGestureMap: diff --git a/addon/globalPlugins/rdAccess/directoryChanges.py b/addon/globalPlugins/rdAccess/directoryChanges.py index 27085c0..41d158d 100644 --- a/addon/globalPlugins/rdAccess/directoryChanges.py +++ b/addon/globalPlugins/rdAccess/directoryChanges.py @@ -98,7 +98,7 @@ def __del__(self): if hasattr(self, "_dirHandle"): winKernel.closeHandle(self._dirHandle) - def _asyncWatch(self, param: int = 0): + def _asyncWatch(self, _param: int = 0): res = windll.kernel32.ReadDirectoryChangesW( self._dirHandle, byref(self._buffer), @@ -112,7 +112,7 @@ def _asyncWatch(self, param: int = 0): if not res: raise WinError() - def _ioDone(self, error, numberOfBytes: int, overlapped: LPOVERLAPPED): + def _ioDone(self, error, numberOfBytes: int, _overlapped: LPOVERLAPPED): if not self._watching: # We stopped watching return @@ -133,9 +133,9 @@ def _handleChanges(self, data: bytes): byteorder=sys.byteorder, signed=False, ) - format = f"@3I{fileNameLength}s" + formatStr = f"@3I{fileNameLength}s" nextOffset, action, fileNameLength, fileNameBytes = unpack( - format, data[nextOffset : nextOffset + calcsize(format)] + formatStr, data[nextOffset : nextOffset + calcsize(formatStr)] ) fileName = fileNameBytes.decode("utf-16") self.directoryChanged.notify( diff --git a/addon/globalPlugins/rdAccess/handlers/_remoteHandler.py b/addon/globalPlugins/rdAccess/handlers/_remoteHandler.py index 58f0caa..16e13f6 100644 --- a/addon/globalPlugins/rdAccess/handlers/_remoteHandler.py +++ b/addon/globalPlugins/rdAccess/handlers/_remoteHandler.py @@ -83,7 +83,7 @@ def _onConnected(self, connected: bool = True): if connected: self._handleDriverChanged(self._driver) - def event_gainFocus(self, obj): + def event_gainFocus(self, _obj): if self._isSecureDesktopHandler: return # Invalidate the property cache to ensure that hasFocus will be fetched again. @@ -111,7 +111,7 @@ def _outgoing_availableSettingValues(self, attribute: protocol.AttributeT) -> by return self._pickle(getattr(self._driver, name)) @protocol.attributeReceiver(protocol.SETTING_ATTRIBUTE_PREFIX + b"*") - def _incoming_setting(self, attribute: protocol.AttributeT, payLoad: bytes): + def _incoming_setting(self, _attribute: protocol.AttributeT, payLoad: bytes): assert len(payLoad) > 0 return self._unpickle(payLoad) diff --git a/addon/globalPlugins/rdAccess/handlers/remoteBrailleHandler.py b/addon/globalPlugins/rdAccess/handlers/remoteBrailleHandler.py index f3978ea..fb3941f 100644 --- a/addon/globalPlugins/rdAccess/handlers/remoteBrailleHandler.py +++ b/addon/globalPlugins/rdAccess/handlers/remoteBrailleHandler.py @@ -106,7 +106,7 @@ def _handleExecuteGesture(self, gesture): try: self.writeMessage(protocol.BrailleCommand.EXECUTE_GESTURE, self._pickle(newGesture)) return False - except WindowsError: + except OSError: log.warning("Error calling _handleExecuteGesture", exc_info=True) return True diff --git a/addon/globalPlugins/rdAccess/handlers/remoteSpeechHandler.py b/addon/globalPlugins/rdAccess/handlers/remoteSpeechHandler.py index 0b8b84d..42fbb49 100644 --- a/addon/globalPlugins/rdAccess/handlers/remoteSpeechHandler.py +++ b/addon/globalPlugins/rdAccess/handlers/remoteSpeechHandler.py @@ -66,7 +66,7 @@ def _command_speak(self, payload: bytes): self._queueFunctionOnMainThread(self._driver.speak, sequence) @protocol.commandHandler(protocol.SpeechCommand.CANCEL) - def _command_cancel(self, payload: bytes = b""): + def _command_cancel(self, _payload: bytes = b""): self._indexesSpeaking.clear() self._queueFunctionOnMainThread(self._driver.cancel) @@ -106,7 +106,7 @@ def _onSynthIndexReached( ) try: self.writeMessage(protocol.SpeechCommand.INDEX_REACHED, indexBytes) - except WindowsError: + except OSError: log.warning("Error calling _onSynthIndexReached", exc_info=True) self._indexesSpeaking.remove(index) @@ -116,7 +116,7 @@ def _onSynthDoneSpeaking(self, synth: typing.Optional[synthDriverHandler.SynthDr self._indexesSpeaking.clear() try: self.writeMessage(protocol.SpeechCommand.INDEX_REACHED, b"\x00\x00") - except WindowsError: + except OSError: log.warning("Error calling _onSynthDoneSpeaking", exc_info=True) def _handleDriverChanged(self, synth: synthDriverHandler.SynthDriver): diff --git a/addon/globalPlugins/rdAccess/secureDesktopHandling.py b/addon/globalPlugins/rdAccess/secureDesktopHandling.py index f1b967b..20e5514 100644 --- a/addon/globalPlugins/rdAccess/secureDesktopHandling.py +++ b/addon/globalPlugins/rdAccess/secureDesktopHandling.py @@ -45,7 +45,7 @@ def terminate(self): self._brailleHandler.terminate() synthDriverHandler.getSynth().loadSettings() - def _initializeHandler(self, handlerType: typing.Type[HandlerTypeT]) -> HandlerTypeT: + def _initializeHandler(self, handlerType: type[HandlerTypeT]) -> HandlerTypeT: sdId = f"NVDA_SD-{handlerType.driverType.name}" sdPort = os.path.join(namedPipe.PIPE_DIRECTORY, sdId) handler = handlerType(self._ioThreadRef(), sdPort, False) diff --git a/addon/globalPlugins/rdAccess/synthDetect.py b/addon/globalPlugins/rdAccess/synthDetect.py index 3c92063..dadc9aa 100644 --- a/addon/globalPlugins/rdAccess/synthDetect.py +++ b/addon/globalPlugins/rdAccess/synthDetect.py @@ -13,6 +13,7 @@ from baseObject import AutoPropertyObject from braille import AUTOMATIC_PORT from logHandler import log + from synthDrivers.remote import remoteSynthDriver if typing.TYPE_CHECKING: diff --git a/addon/installTasks.py b/addon/installTasks.py index 473aecd..cfb1c38 100644 --- a/addon/installTasks.py +++ b/addon/installTasks.py @@ -5,13 +5,13 @@ import typing from time import sleep + import addonHandler import gui import wx if typing.TYPE_CHECKING: - from .lib import configuration - from .lib import rdPipe + from .lib import configuration, rdPipe else: addon: addonHandler.Addon = addonHandler.getCodeAddon() configuration = addon.loadModule("lib.configuration") diff --git a/addon/lib/driver/__init__.py b/addon/lib/driver/__init__.py index 3fac8b4..2402de4 100644 --- a/addon/lib/driver/__init__.py +++ b/addon/lib/driver/__init__.py @@ -43,7 +43,7 @@ def check(cls): return any(cls._getAutoPorts()) @classmethod - def _getAutoPorts(cls, usb=True, bluetooth=True) -> Iterable[bdDetect.DeviceMatch]: + def _getAutoPorts(cls, _usb=True, _bluetooth=True) -> Iterable[bdDetect.DeviceMatch]: for driver, match in bgScanRD(cls.driverType, [cls.name]): assert driver == cls.name yield match @@ -54,8 +54,7 @@ def _getTryPorts(cls, port: Union[str, bdDetect.DeviceMatch]) -> Iterator[bdDete yield port elif isinstance(port, str): assert port == "auto" - for match in cls._getAutoPorts(): - yield match + yield from cls._getAutoPorts() _localSettings: List[DriverSetting] = [] @@ -72,7 +71,7 @@ def __init__(self, port="auto"): initialTime = time.perf_counter() super().__init__() self._connected = False - for portType, portId, port, portInfo in self._getTryPorts(port): + for portType, _portId, port, _portInfo in self._getTryPorts(port): # noqa: B020 for attr in self._requiredAttributesOnInit: self._attributeValueProcessor.setAttributeRequestPending(attr) try: @@ -90,7 +89,7 @@ def __init__(self, port="auto"): onReceive=self._onReceive, onReadError=self._onReadError, ) - except EnvironmentError: + except OSError: log.debugWarning("", exc_info=True) continue if portType == KEY_VIRTUAL_CHANNEL: @@ -175,7 +174,9 @@ def _incomingSupportedSettings(self, payLoad: bytes): @_incomingSupportedSettings.updateCallback def _updateCallback_supportedSettings( - self, attribute: protocol.AttributeT, settings: Iterable[DriverSetting] + self, + _attribute: protocol.AttributeT, + settings: Iterable[DriverSetting], ): log.debug(f"Initializing settings accessor for {len(settings)} settings") self._settingsAccessor = SettingsAccessorBase.createFromSettings(self, settings) if settings else None @@ -195,18 +196,18 @@ def _get_supportedSettings(self): return settings @protocol.attributeReceiver(protocol.SETTING_ATTRIBUTE_PREFIX + b"*") - def _incoming_setting(self, attribute: protocol.AttributeT, payLoad: bytes): + def _incoming_setting(self, _attribute: protocol.AttributeT, payLoad: bytes): assert len(payLoad) > 0 return self._unpickle(payLoad) @protocol.attributeReceiver(b"available*s") - def _incoming_availableSettingValues(self, attribute: protocol.AttributeT, payLoad: bytes): + def _incoming_availableSettingValues(self, _attribute: protocol.AttributeT, payLoad: bytes): return self._unpickle(payLoad) @protocol.attributeSender(protocol.GenericAttribute.TIME_SINCE_INPUT) def _outgoing_timeSinceInput(self) -> bytes: return inputTime.getTimeSinceInput().to_bytes(4, sys.byteorder, signed=False) - def _handlePossibleSessionDisconnect(self, isNowLocked): + def _handlePossibleSessionDisconnect(self): if not self.check(): self._handleRemoteDisconnect() diff --git a/addon/lib/inputTime.py b/addon/lib/inputTime.py index d84267d..963bf7a 100644 --- a/addon/lib/inputTime.py +++ b/addon/lib/inputTime.py @@ -13,7 +13,7 @@ class LastINPUTINFO(Structure): ] def __init__(self): - super().__init__ + super().__init__() self.cbSize = sizeof(LastINPUTINFO) diff --git a/addon/lib/namedPipe.py b/addon/lib/namedPipe.py index 84dfa59..7cbbaac 100644 --- a/addon/lib/namedPipe.py +++ b/addon/lib/namedPipe.py @@ -124,6 +124,7 @@ def __init__( pipeMode: PipeMode = PipeMode.READMODE_BYTE, ): self.pipeName = pipeName + self.pipeMode = pipeMode super().__init__( fileHandle, onReceive, @@ -196,14 +197,14 @@ def _handleConnect(self): log.debug(f"Named pipe {self.pipeName} pending client connection") try: self._ioThreadRef().waitForSingleObjectWithCallback(self._recvEvt, self._handleConnectCallback) - except WindowsError as e: + except OSError as e: error = e.winerror log.error( f"Error while calling RegisterWaitForSingleObject for {self.pipeName}: {WinError(error)}" ) self._ioDone(error, 0, byref(ol)) - def _handleConnectCallback(self, parameter: int, timerOrWaitFired: bool): + def _handleConnectCallback(self, _parameter: int, _timerOrWaitFired: bool): log.debug(f"Event set for {self.pipeName}") numberOfBytes = DWORD() log.debug(f"Getting overlapped result for {self.pipeName} after wait for event") @@ -236,7 +237,7 @@ def _onReadError(self, error: int): return True return False - def _asyncRead(self, param: Optional[int] = None): + def _asyncRead(self, _param: Optional[int] = None): if not self._connected: # _handleConnect will call _asyncRead when it is finished. self._handleConnect() diff --git a/addon/lib/protocol/__init__.py b/addon/lib/protocol/__init__.py index e591749..02040a0 100644 --- a/addon/lib/protocol/__init__.py +++ b/addon/lib/protocol/__init__.py @@ -182,7 +182,7 @@ def attributeReceiver( raise ValueError("Either defaultValue or defaultValueGetter is required, but not both") if defaultValueGetter is None: - def _defaultValueGetter(self: "RemoteProtocolHandler", attribute: AttributeT): + def _defaultValueGetter(_self: "RemoteProtocolHandler", _attribute: AttributeT): return defaultValue defaultValueGetter = _defaultValueGetter @@ -296,7 +296,7 @@ def __call__(self, attribute: AttributeT, rawValue: bytes): self.setValue(attribute, value) -class RemoteProtocolHandler((AutoPropertyObject)): +class RemoteProtocolHandler(AutoPropertyObject): _dev: IoBase driverType: DriverType _receiveBuffer: bytes diff --git a/addon/lib/protocol/braille.py b/addon/lib/protocol/braille.py index 7a372bf..ab1b688 100644 --- a/addon/lib/protocol/braille.py +++ b/addon/lib/protocol/braille.py @@ -24,7 +24,7 @@ class BrailleInputGesture(braille.BrailleDisplayGesture, brailleInput.BrailleInp def __init__( self, source: str, - id: str, + identifier: str, routingIndex: Optional[int] = None, model: Optional[str] = None, dots: int = 0, @@ -33,7 +33,7 @@ def __init__( ): super().__init__() self.source = source - self.id = id + self.id = identifier self.routingIndex = routingIndex self.model = model self.dots = dots diff --git a/addon/synthDrivers/remote.py b/addon/synthDrivers/remote.py index 6d54c45..c4abdc0 100644 --- a/addon/synthDrivers/remote.py +++ b/addon/synthDrivers/remote.py @@ -2,27 +2,27 @@ # Copyright 2023 Leonard de Ruijter # License: GNU General Public License version 2.0 +import os.path +import sys import typing +from collections import OrderedDict +from typing import Optional + import addonHandler +import globalVars +import nvwave import synthDriverHandler -from hwIo import boolToByte -import sys import tones -import nvwave -from typing import Optional -from extensionPoints import Action -from logHandler import log -from languageHandler import getLanguage from autoSettingsUtils.driverSetting import DriverSetting from autoSettingsUtils.utils import StringParameterInfo from braille import AUTOMATIC_PORT -from collections import OrderedDict -import globalVars -import os.path +from extensionPoints import Action +from hwIo import boolToByte +from languageHandler import getLanguage +from logHandler import log if typing.TYPE_CHECKING: - from ..lib import driver - from ..lib import protocol + from ..lib import driver, protocol else: addon: addonHandler.Addon = addonHandler.getCodeAddon() driver = addon.loadModule("lib.driver") @@ -72,7 +72,7 @@ def handle_decideBeep(self, **kwargs) -> bool: log.debug(f"Sending BEEP command: {kwargs}") try: self.writeMessage(protocol.SpeechCommand.BEEP, self._pickle(kwargs)) - except WindowsError: + except OSError: log.warning("Error calling handle_decideBeep", exc_info=True) return True return False @@ -82,7 +82,7 @@ def handle_decidePlayWaveFile(self, **kwargs) -> bool: log.debug(f"Sending PLAY_WAVE_FILE command: {kwargs}") try: self.writeMessage(protocol.SpeechCommand.PLAY_WAVE_FILE, self._pickle(kwargs)) - except WindowsError: + except OSError: log.warning("Error calling handle_decidePlayWaveFile", exc_info=True) return True return False @@ -93,20 +93,20 @@ def _handleRemoteDisconnect(self): def speak(self, speechSequence): try: self.writeMessage(protocol.SpeechCommand.SPEAK, self._pickle(speechSequence)) - except WindowsError: + except OSError: log.error("Error speaking", exc_info=True) self._handleRemoteDisconnect() def cancel(self): try: self.writeMessage(protocol.SpeechCommand.CANCEL) - except WindowsError: + except OSError: log.warning("Error cancelling speech", exc_info=True) def pause(self, switch): try: self.writeMessage(protocol.SpeechCommand.PAUSE, boolToByte(switch)) - except WindowsError: + except OSError: log.warning("Error pausing speech", exc_info=True) @protocol.attributeReceiver(protocol.SpeechAttribute.SUPPORTED_COMMANDS, defaultValue=frozenset()) diff --git a/ruff.toml b/ruff.toml index 55df733..632db1b 100644 --- a/ruff.toml +++ b/ruff.toml @@ -13,7 +13,7 @@ ignore = [ "W191", # indentation contains tabs ] line-length = 110 -preview = true +#preview = true select = [ "A", # flake8-builtins "ARG", # flake8-unused-arguments From 25c35a77267ceee718dfaf5fa55e6061ec205c82 Mon Sep 17 00:00:00 2001 From: Leonard de Ruijter Date: Sat, 25 Nov 2023 11:58:57 +0100 Subject: [PATCH 09/12] Mutate speech state --- .../rdAccess/handlers/remoteSpeechHandler.py | 24 +++++++++++++++---- 1 file changed, 20 insertions(+), 4 deletions(-) diff --git a/addon/globalPlugins/rdAccess/handlers/remoteSpeechHandler.py b/addon/globalPlugins/rdAccess/handlers/remoteSpeechHandler.py index 42fbb49..1e7994f 100644 --- a/addon/globalPlugins/rdAccess/handlers/remoteSpeechHandler.py +++ b/addon/globalPlugins/rdAccess/handlers/remoteSpeechHandler.py @@ -6,11 +6,13 @@ import typing import nvwave +import speech import synthDriverHandler import tones from hwIo.ioThread import IoThread from logHandler import log from speech.commands import IndexCommand +from speech.types import SpeechSequence from ._remoteHandler import RemoteHandler @@ -58,23 +60,37 @@ def _outgoing_language(self, language: typing.Optional[str] = None) -> bytes: @protocol.commandHandler(protocol.SpeechCommand.SPEAK) def _command_speak(self, payload: bytes): sequence = self._unpickle(payload) + self._queueFunctionOnMainThread(self._speak, sequence) + + def _speak(self, sequence: SpeechSequence): for item in sequence: if isinstance(item, IndexCommand): item.index += protocol.speech.SPEECH_INDEX_OFFSET self._indexesSpeaking.append(item.index) - # Queue speech to the current synth directly because we don't want unnecessary processing to happen. - self._queueFunctionOnMainThread(self._driver.speak, sequence) + # Send speech to the current synth directly because we don't want unnecessary processing to happen. + # We need to change speech state accordingly. + speech.speech._speechState.isPaused = False + speech.speech._speechState.beenCanceled = False + self._driver.speak(sequence) @protocol.commandHandler(protocol.SpeechCommand.CANCEL) def _command_cancel(self, _payload: bytes = b""): self._indexesSpeaking.clear() - self._queueFunctionOnMainThread(self._driver.cancel) + self._queueFunctionOnMainThread(self._cancel) + + def _cancel(self): + self._driver.cancel() + speech.speech._speechState.beenCanceled = True + speech.speech._speechState.isPaused = False @protocol.commandHandler(protocol.SpeechCommand.PAUSE) def _command_pause(self, payload: bytes): assert len(payload) == 1 switch = bool.from_bytes(payload, sys.byteorder) - self._queueFunctionOnMainThread(self._driver.pause, switch) + self._queueFunctionOnMainThread(self._pause, switch) + + def _pause(self, switch: bool): + speech.pauseSpeech(switch) @protocol.commandHandler(protocol.SpeechCommand.BEEP) def _command_beep(self, payload: bytes): From 3ed2e88ebf26d37ca985d8ded271709de4d447dd Mon Sep 17 00:00:00 2001 From: Leonard de Ruijter Date: Sat, 25 Nov 2023 12:57:50 +0100 Subject: [PATCH 10/12] Improve readme --- readme.md | 120 +++++++++++++++++++++++++----------------------------- 1 file changed, 55 insertions(+), 65 deletions(-) diff --git a/readme.md b/readme.md index 15980fc..52aa51d 100644 --- a/readme.md +++ b/readme.md @@ -4,19 +4,19 @@ * Download [latest stable version][2] * NVDA compatibility: 2023.2 and later -The RDAccess add-on (Remote Desktop Accessibility) adds support to access remote desktop sessions to NVDA using Microsoft Remote Desktop, Citrix or VMware Horizon. -When installed in NVDA on both the client and the server, speech and braille generated on the server will be spoken and brailled by the client machine. -This enables a user experience where managing a remote system feels just as performant as operating the local system. +The RDAccess add-on (Remote Desktop Accessibility) adds support to access remote desktop sessions on NVDA using Microsoft Remote Desktop, Citrix, or VMware Horizon. +When installed on both the client and the server in NVDA, speech and braille generated on the server will be spoken and displayed in braille on the client machine. +This enables a user experience where managing a remote system feels as seamless as operating the local system. ## Features -* Support for Microsoft Remote Desktop, Citrix and VMware Horizon +* Support for Microsoft Remote Desktop, Citrix, and VMware Horizon * Speech and braille output * Automatic detection of remote braille using NVDA's automatic braille display detection * Automatic detection of remote speech using a dedicated detection process that can be disabled in NVDA's settings dialog -* Support for portable copies of NVDA running on a server (additional configuration required for Fitrix) +* Support for portable copies of NVDA running on a server (additional configuration required for Citrix) * Full support for portable copies of NVDA running on a client (no additional administrative privileges required to install the add-on) -* Multiple active client sessions at the same time +* Multiple active client sessions simultaneously * Remote desktop instantly available after NVDA start * Ability to control specific synthesizer and braille display settings without leaving the remote session * Ability to use speech and braille from the user session when accessing secure desktops @@ -32,100 +32,88 @@ This enables a user experience where managing a remote system feels just as perf Initial stable release. -## Getting started +## Getting Started -1. Install RDAccess in both a client and server copy of NVDA. -1. The remote system should automatically start speaking using the local speech synthesizer. If not, in the NVDA instance on the server, select the remote speech synthesizer from NVDA"s synthesizer selection dialog. +1. Install RDAccess on both a client and server copy of NVDA. +1. The remote system should automatically start speaking using the local speech synthesizer. If not, in the NVDA instance on the server, select the remote speech synthesizer from NVDA's synthesizer selection dialog. 1. To use braille, enable automatic braille display detection using the braille display selection dialog. ## Configuration -After installation, the RDAccess add-on can be configured using NVDA's settings dialog, which can be accessed from the NVDA Menu by choosing Preferences > Settings... -After that, choose the Remote Desktop category. +After installation, the RDAccess add-on can be configured using NVDA's settings dialog, accessible from the NVDA Menu by choosing Preferences > Settings... +Then, choose the Remote Desktop category. This dialog contains the following settings: -### Enable remote desktop accessibility for +### Enable Remote Desktop Accessibility for -This list of check boxes controls the operating mode of the add-on. You can choose between: +This list of checkboxes controls the operating mode of the add-on. Choose between: -* Incoming connections (Remote Desktop Server): Choose this option if the current instance of NVDA is running on a remote desktop server -* Outgoing connections (Remote Desktop Client): Choose this option if the current instance of NVDA is running on a remote desktop client that connects to one or more servers -* Secure Desktop pass through: : Choose this option if you want to use braille and speech from the user instance of NVDA when accessing the secure desktop. Note that for this to work, you need to make the RDAccess add-on available on the secure desktop copy of NVDA. For this, choose "Use currently saved settings during sign-in and on secure screens (requires administrator privileges)" in NVDA's general settings. -To ensure a smooth start with the add-on, all options are enabled by default. You are however encouraged to disable server or client mode as appropriate. +* Incoming connections (Remote Desktop Server): Choose this option if the current instance of NVDA is running on a remote desktop server. +* Outgoing connections (Remote Desktop Client): Choose this option if the current instance of NVDA is running on a remote desktop client that connects to one or more servers. +* Secure Desktop pass-through: Choose this option if you want to use braille and speech from the user instance of NVDA when accessing the secure desktop. Note that for this to work, you need to make the RDAccess add-on available on the secure desktop copy of NVDA. For this, choose "Use currently saved settings during sign-in and on secure screens (requires administrator privileges)" in NVDA's general settings. -### Automatically recover remote speech after connection loss +To ensure a smooth start with the add-on, all options are enabled by default. However, you are encouraged to disable server or client mode as appropriate. -This option is only available in server mode. It ensures that the connection will automatically be re-established when the Remote Speech synthesizer is active and the connection is lost. -The behavior is very similar to that of braille display auto detection. -This also clarifies why there is only such an option for speech. -The reconnection of the Remote Braille display is automatically handled when choosing the Automatic option from the Braille Display Selection dialog. +### Automatically Recover Remote Speech after Connection Loss -This option is enabled by defalt. You are strongly encouraged to leave this option enabled if the Remote Desktop server has no audio output. +This option is only available in server mode. It ensures that the connection will automatically be re-established when the Remote Speech synthesizer is active and the connection is lost, similar to braille display auto-detection. -### Allow remote system to control driver settings +This option is enabled by default. It is strongly encouraged to leave this option enabled if the Remote Desktop server has no audio output. -This client option, when enabled, allows you to control driver settings (such as synthesizer voice and pitch) from the remote system. -This is especially useful when you have difficulties accessing the local NVDA menu when controlling a remote system. -Changes performed on the remote system will automatically be reflected locally. +### Allow Remote System to Control Driver Settings -While enabling this option implies some performance degradation, you are yet advised to enable it. -When this option is disabled, speech synthesizer ppitch changes for capitals don't work. +When enabled in the client, this option allows you to control driver settings (such as synthesizer voice and pitch) from the remote system. Changes made on the remote system will automatically reflect locally. -### Persist client support when exiting NVDA +### Persist Client Support When Exiting NVDA -This client option is only available on installed copies of NVDA. -When enabled, it ensures that the client portion of NVDA is loaded in your remote desktop client, even when NVDA is not running. +This client option, available on installed copies of NVDA, ensures that the client portion of NVDA is loaded in your remote desktop client even when NVDA is not running. -To use the client portion of RDAccess, several changes have to be maede in the Windows Registry. -The add-on ensures that these changes are made under the profile of the current user. -These changes don't require administrative privileges. -Therefore, NVDA can automatically apply the necessary changes when loaded, and undo these changes when exiting NVDA. -This ensures that the add-on is fully compatible with portable versions of NVDA. +To use the client portion of RDAccess, changes need to be made in the Windows Registry. +The add-on ensures that these changes are made under the profile of the current user, requiring no administrative privileges. +Therefore, NVDA can automatically apply the necessary changes when loaded and undo these changes when exiting NVDA, ensuring compatibility with portable versions of NVDA. -This option is disabled by default. -However, if you are running an installed copy and you are the only user of the system, you are advised to enable this option. -This ensures smooth operation in case NVDA is not active when connecting to a remote system and is then started afterwards. +This option is disabled by default. However, if you are running an installed copy and you are the only user of the system, it is advised to enable this option for smooth operation when connecting to a remote system after NVDA starts. -### Enable Microsoft Remote Desktop support +### Enable Microsoft Remote Desktop Support -This option is enabled by default and ensures that the client portion of RDAccess is loaded in the Microsoft Remote Desktop client (mstsc) when starting NVDA. -Unless persistent client support is enabled by enabling the previous option, these changes will be automatically undone when exiting NVDA. +This option, enabled by default, ensures that the client portion of RDAccess is loaded in the Microsoft Remote Desktop client (mstsc) when starting NVDA. +Changes made through this option will be automatically undone when exiting NVDA unless persistent client support is enabled. -### Enable Citrix Workspace support +### Enable Citrix Workspace Support -This option is enabled by default and ensures that the client portion of RDAccess is loaded in the Citrix Workspace app when starting NVDA. -Unless persistent client support is enabled by enabling the previous option, these changes will be automatically undone when exiting NVDA. +This option, enabled by default, ensures that the client portion of RDAccess is loaded in the Citrix Workspace app when starting NVDA. +Changes made through this option will be automatically undone when exiting NVDA unless persistent client support is enabled. -This option is only available in the following cases: +This option is available only under the following conditions: -* Citrix Workspace is installed. Note that the Windows Store version of the app is not supported due to limitations in that app itself -* It is possible to register RDAccess under the current user context. After installing the app, you have to start a remote session once to make this possible +* Citrix Workspace is installed. Note that the Windows Store version of the app is not supported due to limitations in the app itself. +* It is possible to register RDAccess under the current user context. After installing the app, you have to start a remote session once to enable this. -## Citrix specific instructions +## Citrix Specific Instructions -There are some important points of attention when using RDAccess with the Citrix Workspace app. +There are important points to note when using RDAccess with the Citrix Workspace app: -### Client side requirements +### Client-Side Requirements 1. The Windows Store variant of the app is *not* supported. -2. After installing Citrix Workspace, you have to start a remote session once to allow RDAccess registering itself. The reason behind this is that the application copies the system configuration to the user configuration when it establishes a session for the first time. After that, RDAccess can register itself under the current user context. +1. After installing Citrix Workspace, you need to start a remote session once to let RDAccess register itself. This occurs because the application copies system settings to user settings during the initial session setup. Following this, RDAccess can register itself under the current user context. -### Server side requirement +### Server-Side Requirement -In Citrix Virtual Apps and Desktops 2109, Citrix enabled the so called virtual channel allow list. This means that third party virtual channels, including the channel required by RDAccess, is not allowed by default. For more information, [see this Citrix blog post](https://www.citrix.com/blogs/2021/10/14/virtual-channel-allow-list-now-enabled-by-default/) +In Citrix Virtual Apps and Desktops 2109, Citrix enabled the so-called virtual channel allow list, restricting third-party virtual channels, including the channel required by RDAccess, by default. +For more information, [see this Citrix blog post](https://www.citrix.com/blogs/2021/10/14/virtual-channel-allow-list-now-enabled-by-default/). -Explicitly allowing the RdPipe channel required by RDAccess is not yet tested. For now, it is probably your best bet to disable the allow list altogether. If your system administrator is unhappy with this, feel free to [drop a line in the devoted issue][3] +Explicitly allowing the RdPipe channel required by RDAccess is not yet tested. For now, it is best to disable the allow list altogether. If your system administrator has concerns, feel free to [address the issue here][3]. -## Issues and contributing +## Issues and Contributing -If you want to report an issue or contribute, take a look at [the issues page on Github][3] +To report an issue or contribute, refer to [the issues page on Github][4]. -## External components +## External Components -This add-on relies on [RD Pipe][4], a library written in Rust backing the remote desktop client support. -RD Pipe is redistributed as part of this add-on under the terms of [version 3 of the GNU Affero General Public License][5] as -published by the Free Software Foundation. +This add-on relies on [RD Pipe][5], a library written in Rust backing the remote desktop client support. +RD Pipe is redistributed as part of this add-on under the terms of [version 3 of the GNU Affero General Public License][6]. [[!tag dev beta]] @@ -133,8 +121,10 @@ published by the Free Software Foundation. [2]: https://www.nvaccess.org/addonStore/legacy?file=rdAccess -[3]: https://github.com/leonardder/rdAccess/issues +[3]: https://github.com/leonardder/rdAccess/issues/1 -[4]: https://github.com/leonardder/rd_pipe-rs +[4]: https://github.com/leonardder/rdAccess/issues -[5]: https://github.com/leonardder/rd_pipe-rs/blob/master/LICENSE +[5]: https://github.com/leonardder/rd_pipe-rs + +[6]: https://github.com/leonardder/rd_pipe-rs/blob/master/LICENSE From e0330922221bff1c9b04b2c9d1d73f46ece3fd01 Mon Sep 17 00:00:00 2001 From: Leonard de Ruijter Date: Sat, 25 Nov 2023 13:04:48 +0100 Subject: [PATCH 11/12] Update changelog --- readme.md | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/readme.md b/readme.md index 52aa51d..f730283 100644 --- a/readme.md +++ b/readme.md @@ -23,6 +23,13 @@ This enables a user experience where managing a remote system feels as seamless ## Changelog +### Version 1.2 + +- Use [Ruff](https://github.com/astral-sh/ruff) as a formatter and linter. [#13](https://github.com/leonardder/rdAccess/pull/13) +- Fixed an issue where NVDA on the client generates an error when pausing speech on the server. +- Fixed support for `winAPI.secureDesktop.post_secureDesktopStateChange`. +- Improved driver initialization on the server. + ### Version 1.1 - Added support for NVDA 2023.3 style device registration for automatic detection of braille displays. [#11](https://github.com/leonardder/rdAccess/pull/11) From 04ad9fc9ba15f6e147482db4f58a4ae4fc997f26 Mon Sep 17 00:00:00 2001 From: github-actions Date: Sat, 25 Nov 2023 12:06:06 +0000 Subject: [PATCH 12/12] Update buildVars --- buildVars.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/buildVars.py b/buildVars.py index c5fe89d..97ef760 100644 --- a/buildVars.py +++ b/buildVars.py @@ -27,7 +27,7 @@ def _(arg): "Allows using speech and braille with Microsoft Remote Desktop, Citrix Workspace and VMware Horizon" ), # version - "addon_version": "1.1.1", + "addon_version": "1.2.0", # Author(s) "addon_author": "Leonard de Ruijter ", # URL for the add-on documentation support