From 6039b53f33be0a63aa96e7988e793b19af736a8c Mon Sep 17 00:00:00 2001 From: Sean Budd Date: Thu, 10 Nov 2022 14:51:09 +1100 Subject: [PATCH] Check Z-Order for making content accessible on the lock screen. --- source/api.py | 5 +- source/baseObject.py | 30 ++- source/core.py | 22 +-- source/globalVars.py | 3 +- source/nvda.pyw | 12 +- source/textInfos/__init__.py | 16 +- source/treeInterceptorHandler.py | 18 +- source/utils/security.py | 45 ++++- source/winAPI/messageWindow.py | 2 - source/winAPI/sessionTracking.py | 324 ++++++++----------------------- source/winUser.py | 2 +- 11 files changed, 185 insertions(+), 294 deletions(-) diff --git a/source/api.py b/source/api.py index ad820d8b2f6..36a558f133c 100644 --- a/source/api.py +++ b/source/api.py @@ -237,7 +237,10 @@ def setReviewPosition( @param isCaret: Whether the review position is changed due to caret following. @param isMouse: Whether the review position is changed due to mouse following. """ - if _isSecureObjectWhileLockScreenActivated(reviewPosition.obj): + reviewObj = reviewPosition.obj + if isinstance(reviewObj, treeInterceptorHandler.TreeInterceptor): + reviewObj = reviewObj.rootNVDAObject + if _isSecureObjectWhileLockScreenActivated(reviewObj): return False globalVars.reviewPosition=reviewPosition.copy() globalVars.reviewPositionObj=reviewPosition.obj diff --git a/source/baseObject.py b/source/baseObject.py index 988bfdab883..a63fa57cb86 100755 --- a/source/baseObject.py +++ b/source/baseObject.py @@ -1,16 +1,27 @@ # A part of NonVisual Desktop Access (NVDA) -# Copyright (C) 2007-2020 NV Access Limited, Christopher Toth, Babbage B.V., Julien Cochuyt +# Copyright (C) 2007-2022 NV Access Limited, Christopher Toth, Babbage B.V., Julien Cochuyt # This file is covered by the GNU General Public License. # See the file COPYING for more details. """Contains the base classes that many of NVDA's classes such as NVDAObjects, virtualBuffers, appModules, synthDrivers inherit from. These base classes provide such things as auto properties, and methods and properties for scripting and key binding. """ +from typing import ( + Any, + Callable, + Optional, + Set, + Union, +) import weakref import garbageHandler from logHandler import log from abc import ABCMeta, abstractproperty +GetterReturnT = Any +GetterMethodT = Callable[["AutoPropertyObject"], GetterReturnT] + + class Getter(object): def __init__(self,fget, abstract=False): @@ -18,7 +29,11 @@ def __init__(self,fget, abstract=False): if abstract: self._abstract = self.__isabstractmethod__ = abstract - def __get__(self,instance,owner): + def __get__( + self, + instance: Union[Any, None, "AutoPropertyObject"], + owner, + ) -> Union[GetterReturnT, "Getter"]: if isinstance(self.fget, classmethod): return self.fget.__get__(instance, owner)() elif instance is None: @@ -31,9 +46,14 @@ def setter(self,func): def deleter(self,func): return (abstractproperty if self._abstract else property)(fget=self.fget,fdel=func) + class CachingGetter(Getter): - def __get__(self, instance, owner): + def __get__( + self, + instance: Union[Any, None, "AutoPropertyObject"], + owner, + ) -> Union[GetterReturnT, "CachingGetter"]: if isinstance(self.fget, classmethod): log.warning("Class properties do not support caching") return self.fget.__get__(instance, owner)() @@ -41,6 +61,7 @@ def __get__(self, instance, owner): return self return instance._getPropertyViaCache(self.fget) + class AutoPropertyType(ABCMeta): def __init__(self,name,bases,dict): @@ -125,6 +146,7 @@ class AutoPropertyObject(garbageHandler.TrackedObject, metaclass=AutoPropertyTyp #: @type: bool cachePropertiesByDefault = False + _propertyCache: Set[GetterMethodT] def __new__(cls, *args, **kwargs): self = super(AutoPropertyObject, cls).__new__(cls) @@ -134,7 +156,7 @@ def __new__(cls, *args, **kwargs): self.__instances[self]=None return self - def _getPropertyViaCache(self,getterMethod=None): + def _getPropertyViaCache(self, getterMethod: Optional[GetterMethodT] = None) -> GetterReturnT: if not getterMethod: raise ValueError("getterMethod is None") missing=False diff --git a/source/core.py b/source/core.py index 014fb62936c..6ca1d8433a3 100644 --- a/source/core.py +++ b/source/core.py @@ -601,7 +601,6 @@ def onEndSession(evt): wx.CallAfter(audioDucking.initialize) from winAPI.messageWindow import WindowMessage - from winAPI import sessionTracking import winUser # #3763: In wxPython 3, the class name of frame windows changed from wxWindowClassNR to wxWindowNR. # NVDA uses the main frame to check for and quit another instance of NVDA. @@ -630,27 +629,12 @@ def __init__(self, windowName=None): self.orientationCoordsCache = (0,0) self.handlePowerStatusChange() - # Call must be paired with a call to sessionTracking.unregister - if not sessionTracking.register(self.handle): - import utils.security - wx.CallAfter(utils.security.warnSessionLockStateUnknown) - - def destroy(self): - """ - NVDA must unregister session tracking before destroying the message window. - """ - # Requires an active message window and a handle to unregister. - sessionTracking.unregister(self.handle) - super().destroy() - def windowProc(self, hwnd, msg, wParam, lParam): post_windowMessageReceipt.notify(msg=msg, wParam=wParam, lParam=lParam) if msg == WindowMessage.POWER_BROADCAST and wParam == self.PBT_APMPOWERSTATUSCHANGE: self.handlePowerStatusChange() elif msg == winUser.WM_DISPLAYCHANGE: self.handleScreenOrientationChange(lParam) - elif msg == WindowMessage.WTS_SESSION_CHANGE: - sessionTracking.handleSessionChange(sessionTracking.WindowsTrackedSession(wParam), lParam) def handleScreenOrientationChange(self, lParam): # TODO: move to winAPI @@ -809,7 +793,8 @@ def run(self): mouseHandler.pumpAll() braille.pumpAll() vision.pumpAll() - except: + sessionTracking.pumpAll() + except Exception: log.exception("errors in this core pump cycle") baseObject.AutoPropertyObject.invalidateCaches() watchdog.asleep() @@ -832,6 +817,9 @@ def run(self): log.debug("initializing updateCheck") updateCheck.initialize() + from winAPI import sessionTracking + sessionTracking.initialize() + _TrackNVDAInitialization.markInitializationComplete() log.info("NVDA initialized") diff --git a/source/globalVars.py b/source/globalVars.py index 83b02905e82..ed6c7f43cfa 100644 --- a/source/globalVars.py +++ b/source/globalVars.py @@ -21,6 +21,7 @@ if typing.TYPE_CHECKING: import NVDAObjects # noqa: F401 used for type checking only + import textInfos # noqa: F401 used for type checking only class DefaultAppArgs(argparse.Namespace): @@ -70,7 +71,7 @@ class DefaultAppArgs(argparse.Namespace): mouseOldY=None navigatorObject: typing.Optional['NVDAObjects.NVDAObject'] = None reviewPosition=None -reviewPositionObj=None +reviewPositionObj: typing.Optional["textInfos.TextInfoObjT"] = None lastProgressValue=0 appArgs = DefaultAppArgs() unknownAppArgs: typing.List[str] = [] diff --git a/source/nvda.pyw b/source/nvda.pyw index abe08bccdb2..9d1cddb10f6 100755 --- a/source/nvda.pyw +++ b/source/nvda.pyw @@ -350,13 +350,19 @@ if mutex is None: sys.exit(1) -if _isSecureDesktop(): +def _serviceDebugEnabled() -> bool: import winreg try: k = winreg.OpenKey(winreg.HKEY_LOCAL_MACHINE, r"SOFTWARE\NVDA") - if not winreg.QueryValueEx(k, u"serviceDebug")[0]: - globalVars.appArgs.secure = True + if winreg.QueryValueEx(k, "serviceDebug")[0]: + return True except WindowsError: + pass + return False + + +if _isSecureDesktop(): + if not _serviceDebugEnabled(): globalVars.appArgs.secure = True globalVars.appArgs.changeScreenReaderFlag = False globalVars.appArgs.minimal = True diff --git a/source/textInfos/__init__.py b/source/textInfos/__init__.py index 544c72380d2..c2ee09fa944 100755 --- a/source/textInfos/__init__.py +++ b/source/textInfos/__init__.py @@ -1,7 +1,7 @@ # A part of NonVisual Desktop Access (NVDA) # This file is covered by the GNU General Public License. # See the file COPYING for more details. -# Copyright (C) 2006-2021 NV Access Limited, Babbage B.V., Accessolutions, Julien Cochuyt +# Copyright (C) 2006-2022 NV Access Limited, Babbage B.V., Accessolutions, Julien Cochuyt """Framework for accessing text content in widgets. The core component of this framework is the L{TextInfo} class. @@ -32,6 +32,7 @@ if typing.TYPE_CHECKING: import NVDAObjects + import treeInterceptorHandler # noqa: F401 used for type checking only SpeechSequence = List[Union[Any, str]] @@ -287,6 +288,9 @@ def _logBadSequenceTypes(sequence: SpeechSequence, shouldRaise: bool = True): return speech.types.logBadSequenceTypes(sequence, raiseExceptionOnError=shouldRaise) +TextInfoObjT = Union["NVDAObjects.NVDAObject", "treeInterceptorHandler.TreeInterceptor"] + + class TextInfo(baseObject.AutoPropertyObject): """Provides information about a range of text in an object and facilitates access to all text in the widget. A TextInfo represents a specific range of text, providing access to the text itself, as well as information about the text such as its formatting and any associated controls. @@ -307,7 +311,11 @@ class TextInfo(baseObject.AutoPropertyObject): @type bookmark: L{Bookmark} """ - def __init__(self,obj,position): + def __init__( + self, + obj: TextInfoObjT, + position + ): """Constructor. Subclasses must extend this, calling the superclass method first. @param position: The initial position of this range; one of the POSITION_* constants or a position object supported by the implementation. @@ -338,9 +346,9 @@ def _set_end(self, otherEndpoint: "TextInfoEndpoint"): self.end.moveTo(otherEndpoint) #: Typing information for auto-property: _get_obj - obj: "NVDAObjects.NVDAObject" + obj: TextInfoObjT - def _get_obj(self) -> "NVDAObjects.NVDAObject": + def _get_obj(self) -> TextInfoObjT: """The object containing the range of text being represented.""" return self._obj() diff --git a/source/treeInterceptorHandler.py b/source/treeInterceptorHandler.py index 179a40b43ce..6632de8de95 100644 --- a/source/treeInterceptorHandler.py +++ b/source/treeInterceptorHandler.py @@ -1,10 +1,13 @@ -# treeInterceptorHandler.py # A part of NonVisual Desktop Access (NVDA) -# Copyright (C) 2006-2020 NV Access Limited, Davy Kager, Accessolutions, Julien Cochuyt +# Copyright (C) 2006-2022 NV Access Limited, Davy Kager, Accessolutions, Julien Cochuyt # This file is covered by the GNU General Public License. # See the file COPYING for more details. -from typing import Optional, Dict +from typing import ( + TYPE_CHECKING, + Dict, + Optional, +) from logHandler import log import baseObject @@ -18,6 +21,10 @@ from speech.types import SpeechSequence from controlTypes import OutputReason +if TYPE_CHECKING: + import NVDAObjects + + runningTable=set() def getTreeInterceptor(obj): @@ -84,12 +91,11 @@ class TreeInterceptor(baseObject.ScriptableObject): shouldTrapNonCommandGestures=False #: If true then gestures that do not have a script and are not a command gesture should be trapped from going through to Windows. - def __init__(self, rootNVDAObject): + def __init__(self, rootNVDAObject: "NVDAObjects.NVDAObject"): super(TreeInterceptor, self).__init__() self._passThrough = False #: The root object of the tree wherein events and scripts are intercepted. - #: @type: L{NVDAObjects.NVDAObject} - self.rootNVDAObject = rootNVDAObject + self.rootNVDAObject: "NVDAObjects.NVDAObject" = rootNVDAObject def terminate(self): """Terminate this interceptor. diff --git a/source/utils/security.py b/source/utils/security.py index 09fea1545db..b79a26ea6bd 100644 --- a/source/utils/security.py +++ b/source/utils/security.py @@ -14,13 +14,14 @@ import winUser if typing.TYPE_CHECKING: - import appModuleHandler + import appModuleHandler # noqa: F401, use for typing import scriptHandler # noqa: F401, use for typing - import NVDAObjects + import NVDAObjects # noqa: F401, use for typing postSessionLockStateChanged = extensionPoints.Action() """ +# TODO: maintain backwards compat Notifies when a session lock or unlock event occurs. Usage: @@ -141,7 +142,13 @@ def _isSecureObjectWhileLockScreenActivated( As such, NVDA must prevent accessing and reading objects outside of the lockscreen when Windows is locked. @return: C{True} if the Windows 10/11 lockscreen is active and C{obj} is outside of the lock screen. """ - if isWindowsLocked() and not isObjectAboveLockScreen(obj): + try: + isObjectInSecure = isWindowsLocked() and not isObjectAboveLockScreen(obj) + except Exception: + log.exception() + return False + + if isObjectInSecure: if shouldLog and log.isEnabledFor(log.DEBUG): devInfo = '\n'.join(obj.devInfo) log.debug(f"Attempt at navigating to a secure object: {devInfo}") @@ -155,7 +162,6 @@ def isObjectAboveLockScreen(obj: "NVDAObjects.NVDAObject") -> bool: When Windows is locked, the foreground Window is usually LockApp, but other Windows can be focused (e.g. Windows Magnifier). """ - import appModuleHandler from IAccessibleHandler import SecureDesktopNVDAObject from NVDAObjects.IAccessible import TaskListIcon @@ -176,19 +182,44 @@ def isObjectAboveLockScreen(obj: "NVDAObjects.NVDAObject") -> bool: or isinstance(obj, SecureDesktopNVDAObject) ): return True + return _isObjectAboveLockScreenCheckZOrder(obj) + +def _isObjectAboveLockScreenCheckZOrder(obj: "NVDAObjects.NVDAObject") -> bool: + """ + This is a risky hack. + If the order is incorrectly detected, + the Windows UX may become inaccessible + or secure information may become accessible. + + If these functions fail, where possible, + NVDA should make NVDA objects accessible. + """ + import appModuleHandler + from NVDAObjects.window import Window + if not isinstance(obj, Window): + # must be a window to get its HWNDVal + return True runningAppModules = appModuleHandler.runningTable.values() lockAppModule = next(filter(_isLockAppAndAlive, runningAppModules), None) if lockAppModule is None: # lockAppModule not running/registered by NVDA yet - log.debugWarning( + log.debug( "lockAppModule not detected when Windows is locked. " - "Cannot detect if object is in lock app, considering object as insecure. " + "Cannot detect if object is in lock app, considering object as safe. " ) - elif lockAppModule is not None and obj.processID == lockAppModule.processID: return True + desktopWindow = winUser.getDesktopWindow() + nextWindow = winUser.getTopWindow(desktopWindow) + while nextWindow: + windowProcessId = winUser.getWindowThreadProcessID(nextWindow) + if nextWindow == obj.windowHandle: + return True + elif windowProcessId == lockAppModule.processID: + return False + nextWindow = winUser.getWindow(nextWindow, winUser.GW_HWNDNEXT) return False diff --git a/source/winAPI/messageWindow.py b/source/winAPI/messageWindow.py index 4f7ee91dba1..34e8a830dc2 100644 --- a/source/winAPI/messageWindow.py +++ b/source/winAPI/messageWindow.py @@ -21,6 +21,4 @@ class WindowMessage(enum.IntEnum): """ WM_WTSSESSION_CHANGE Windows Message for when a Session State Changes. - Receiving these messages is registered by sessionTracking.register. - handleSessionChange handles these messages. """ diff --git a/source/winAPI/sessionTracking.py b/source/winAPI/sessionTracking.py index 079f1ed15e0..7c63dc219ed 100644 --- a/source/winAPI/sessionTracking.py +++ b/source/winAPI/sessionTracking.py @@ -12,8 +12,6 @@ Used to: - only allow a whitelist of safe scripts to run - ensure object navigation cannot occur outside of the lockscreen - -https://docs.microsoft.com/en-us/windows/win32/api/wtsapi32/nf-wtsapi32-wtsregistersessionnotification """ from __future__ import annotations @@ -21,18 +19,14 @@ import ctypes from contextlib import contextmanager from ctypes.wintypes import ( - HANDLE, HWND, ) import enum -from threading import Lock from typing import ( - Dict, - Set, Optional, ) -from systemUtils import _isSecureDesktop +from baseObject import AutoPropertyObject from winAPI.wtsApi32 import ( WTSINFOEXW, WTSQuerySessionInformation, @@ -45,44 +39,34 @@ ) from logHandler import log -_updateSessionStateLock = Lock() -"""Used to protect updates to _currentSessionStates""" -_currentSessionStates: Set["WindowsTrackedSession"] = set() -""" -Current state of the Windows session associated with this instance of NVDA. -Maintained via receiving session notifications via the NVDA MessageWindow. -Initial state will be set by querying the current status. -Actions which involve updating this state this should be protected by _updateSessionStateLock. -""" - -_sessionQueryLockStateHasBeenUnknown = False -""" -Track if any 'Unknown' Value when querying the Session Lock status has been encountered. -""" - -_isSessionTrackingRegistered = False -""" -Session tracking is required for NVDA to be notified of lock state changes for security purposes. -""" - RPC_S_INVALID_BINDING = 0x6A6 """ Error which occurs when Windows is not ready to register session notification tracking. This error can be prevented by waiting for the event: 'Global\\TermSrvReadyEvent.' + +Unused in NVDA core. """ NOTIFY_FOR_THIS_SESSION = 0 """ The alternative to NOTIFY_FOR_THIS_SESSION is to be notified for all user sessions. NOTIFY_FOR_ALL_SESSIONS is not required as NVDA runs on separate user profiles, including the system profile. + +Unused in NVDA core. """ SYNCHRONIZE = 0x00100000 """ Parameter for OpenEventW, blocks thread until event is registered. https://docs.microsoft.com/en-us/windows/win32/sync/synchronization-object-security-and-access-rights + +Unused in NVDA core, duplicate of winKernel.SYNCHRONIZE. """ +_initializationLockState: Optional[bool] = None +_lockStateTracker: Optional["_WindowsLockedState"] = None +_windowsWasPreviouslyLocked = False + class WindowsTrackedSession(enum.IntEnum): """ @@ -106,47 +90,30 @@ class WindowsTrackedSession(enum.IntEnum): SESSION_TERMINATE = 11 -_toggleWindowsSessionStatePair: Dict[WindowsTrackedSession, WindowsTrackedSession] = { - WindowsTrackedSession.CONSOLE_CONNECT: WindowsTrackedSession.CONSOLE_DISCONNECT, - WindowsTrackedSession.CONSOLE_DISCONNECT: WindowsTrackedSession.CONSOLE_CONNECT, - WindowsTrackedSession.REMOTE_CONNECT: WindowsTrackedSession.REMOTE_DISCONNECT, - WindowsTrackedSession.REMOTE_DISCONNECT: WindowsTrackedSession.REMOTE_CONNECT, - WindowsTrackedSession.SESSION_LOGON: WindowsTrackedSession.SESSION_LOGOFF, - WindowsTrackedSession.SESSION_LOGOFF: WindowsTrackedSession.SESSION_LOGON, - WindowsTrackedSession.SESSION_LOCK: WindowsTrackedSession.SESSION_UNLOCK, - WindowsTrackedSession.SESSION_UNLOCK: WindowsTrackedSession.SESSION_LOCK, - WindowsTrackedSession.SESSION_CREATE: WindowsTrackedSession.SESSION_TERMINATE, - WindowsTrackedSession.SESSION_TERMINATE: WindowsTrackedSession.SESSION_CREATE, -} -""" -Pair of WindowsTrackedSession, where each key has a value of the opposite state. -e.g. SESSION_LOCK/SESSION_UNLOCK. -""" +class _WindowsLockedState(AutoPropertyObject): + # Refer to AutoPropertyObject for notes on caching + _cache_isWindowsLocked = True + + # Typing information for auto-property _get_isWindowsLocked + isWindowsLocked: bool + def _get_isWindowsLocked(self) -> bool: + from winAPI.sessionTracking import _isWindowsLocked_checkViaSessionQuery + return _isWindowsLocked_checkViaSessionQuery() -def _hasLockStateBeenTracked() -> bool: - """ - Checks if NVDA is aware of a session lock state change since NVDA started. - """ - return bool(_currentSessionStates.intersection({ - WindowsTrackedSession.SESSION_LOCK, - WindowsTrackedSession.SESSION_UNLOCK - })) +def initialize(): + global _lockStateTracker + _lockStateTracker = _WindowsLockedState() -def _recordLockStateTrackingFailure(error: Optional[Exception] = None): - log.error( - "Unknown lock state, unexpected, potential security issue, please report.", - exc_info=error - ) # Report error repeatedly, attention is required. - ## - # For security it would be best to treat unknown as locked. - # However, this would make NVDA unusable. - # Instead, the user should be warned, and allowed to mitigate the impact themselves. - # Reporting is achieved via _sessionQueryLockStateHasBeenUnknown exposed with - # L{hasLockStateBeenUnknown}. - global _sessionQueryLockStateHasBeenUnknown - _sessionQueryLockStateHasBeenUnknown = True + +def pumpAll(): + global _windowsWasPreviouslyLocked + from utils.security import postSessionLockStateChanged + windowsIsNowLocked = isWindowsLocked() + if windowsIsNowLocked != _windowsWasPreviouslyLocked: + _windowsWasPreviouslyLocked = windowsIsNowLocked + postSessionLockStateChanged.notify(isNowLocked=windowsIsNowLocked) def isWindowsLocked() -> bool: @@ -155,74 +122,37 @@ def isWindowsLocked() -> bool: Not to be confused with the Windows sign-in screen, a secure screen. """ from core import _TrackNVDAInitialization + from winAPI.sessionTracking import _isWindowsLocked_checkViaSessionQuery + from systemUtils import _isSecureDesktop if not _TrackNVDAInitialization.isInitializationComplete(): - # During NVDA initialization, - # core._initializeObjectCaches needs to cache the desktop object, - # regardless of lock state. - # Security checks may cause the desktop object to not be set if NVDA starts on the lock screen. - # As such, during initialization, NVDA should behave as if Windows is unlocked. return False if _isSecureDesktop(): - # If this is the Secure Desktop, - # we are in secure mode and on a secure screen, - # e.g. on the sign-in screen. - # _isSecureDesktop may also return True on the lock screen before a user has signed in. - - # For more information, refer to devDocs/technicalDesignOverview.md 'Logging in secure mode' - # and the following userGuide sections: - # - SystemWideParameters (information on the serviceDebug parameter) - # - SecureMode and SecureScreens return False - lockStateTracked = _hasLockStateBeenTracked() - if lockStateTracked: - return WindowsTrackedSession.SESSION_LOCK in _currentSessionStates + global _initializationLockState + if _lockStateTracker is not None: + _isWindowsLocked = _lockStateTracker.isWindowsLocked else: - _recordLockStateTrackingFailure() # Report error repeatedly, attention is required. - ## - # For security it would be best to treat unknown as locked. - # However, this would make NVDA unusable. - # Instead, the user should be warned via UI, and allowed to mitigate the impact themselves. - # See usage of L{hasLockStateBeenUnknown}. - return False # return False, indicating unlocked, to allow NVDA to be used - - -def _setInitialWindowLockState() -> None: - """ - Ensure that session tracking state is initialized. - If NVDA has started on a lockScreen, it needs to be aware of this. - As NVDA has already registered for session tracking notifications, - a lock is used to prevent conflicts. - """ - with _updateSessionStateLock: - lockStateTracked = _hasLockStateBeenTracked() - if lockStateTracked: - log.debugWarning( - "Initial state already set." - " NVDA may have received a session change notification before initialising" - ) - # Fall back to explicit query - try: - isLocked = _isWindowsLocked_checkViaSessionQuery() - _currentSessionStates.add( - WindowsTrackedSession.SESSION_LOCK - if isLocked - else WindowsTrackedSession.SESSION_UNLOCK - ) - except RuntimeError as error: - _recordLockStateTrackingFailure(error) + if _initializationLockState is None: + _initializationLockState = _isWindowsLocked_checkViaSessionQuery() + _isWindowsLocked = _initializationLockState + return _isWindowsLocked def _isWindowsLocked_checkViaSessionQuery() -> bool: """ Use a session query to check if the session is locked @return: True is the session is locked. - @raise: Runtime error if the lock state can not be determined via a Session Query. + Also returns False if the lock state can not be determined via a Session Query. """ - sessionQueryLockState = _getSessionLockedValue() + try: + sessionQueryLockState = _getSessionLockedValue() + except RuntimeError: + return False if sessionQueryLockState == WTS_LockState.WTS_SESSIONSTATE_UNKNOWN: - raise RuntimeError( + log.error( "Unable to determine lock state via Session Query." f" Lock state value: {sessionQueryLockState!r}" ) + return False return sessionQueryLockState == WTS_LockState.WTS_SESSIONSTATE_LOCK @@ -231,145 +161,41 @@ def isLockStateSuccessfullyTracked() -> bool: I.E. Registered for session tracking AND initial value set correctly. @return: True when successfully tracked. """ - return ( - not _sessionQueryLockStateHasBeenUnknown - or not _isSessionTrackingRegistered + # TODO: improve deprecation practice on beta/master merges + log.error( + "NVDA no longer registers session tracking notifications. " + "This function is deprecated, for removal in 2023.1. " + "It was never expected that add-on authors would use this function" ) + return True -def register(handle: HWND) -> bool: - """ - @param handle: handle for NVDA message window. - When registered, Windows Messages related to session event changes will be - sent to the message window. - @returns: True is session tracking is successfully registered. - - Blocks until Windows accepts session tracking registration. - - Every call to this function must be paired with a call to unregister. - - If registration fails, NVDA may not work properly until the session can be registered in a new instance. - NVDA will not know when the lock screen is activated, which means it becomes a security risk. - NVDA should warn the user if registering the session notification fails. - - https://docs.microsoft.com/en-us/windows/win32/api/wtsapi32/nf-wtsapi32-wtsregistersessionnotification - """ - - # OpenEvent handle must be closed with CloseHandle. - eventObjectHandle: HANDLE = ctypes.windll.kernel32.OpenEventW( - # Blocks until WTS session tracking can be registered. - # Windows needs time for the WTS session tracking service to initialize. - # NVDA must ensure that the WTS session tracking service is ready before trying to register - SYNCHRONIZE, # DWORD dwDesiredAccess - False, # BOOL bInheritHandle - NVDA sub-processes do not need to inherit this handle - # According to the docs, when the Global\TermSrvReadyEvent global event is set, - # all dependent services have started and WTSRegisterSessionNotification can be successfully called. - # https://docs.microsoft.com/en-us/windows/win32/api/wtsapi32/nf-wtsapi32-wtsregistersessionnotification#remarks - "Global\\TermSrvReadyEvent" # LPCWSTR lpName - The name of the event object. +def register(handle: int) -> bool: + # TODO: improve deprecation practice on beta/master merges + log.error( + "NVDA no longer registers session tracking notifications. " + "This function is deprecated, for removal in 2023.1. " + "It was never expected that add-on authors would use this function" ) - if not eventObjectHandle: - error = ctypes.WinError() - log.error("Unexpected error waiting to register session tracking.", exc_info=error) - return False - - registrationSuccess = ctypes.windll.wtsapi32.WTSRegisterSessionNotification(handle, NOTIFY_FOR_THIS_SESSION) - ctypes.windll.kernel32.CloseHandle(eventObjectHandle) - - if registrationSuccess: - log.debug("Registered session tracking") - # Ensure that an initial state is set. - # Do this only when session tracking has been registered, - # so that any changes to the state are not missed via a race condition with session tracking registration. - # As this occurs after NVDA hs registered for session tracking, - # it is possible NVDA is expected to handle a session change notification - # at the same time as initialisation. - # _updateSessionStateLock is used to prevent received session notifications from being handled at the - # same time as initialisation. - _setInitialWindowLockState() - else: - error = ctypes.WinError() - if error.errno == RPC_S_INVALID_BINDING: - log.error( - "WTS registration failed. " - "NVDA waited successfully on TermSrvReadyEvent to ensure that WTS is ready to allow registration. " - "Cause of failure unknown. " - ) - else: - log.error("Unexpected error registering session tracking.", exc_info=error) - - global _isSessionTrackingRegistered - _isSessionTrackingRegistered = registrationSuccess - return isLockStateSuccessfullyTracked() + return True def unregister(handle: HWND) -> None: - """ - This function must be called once for every call to register. - If unregistration fails, NVDA may not work properly until the session can be unregistered in a new instance. - - https://docs.microsoft.com/en-us/windows/win32/api/wtsapi32/nf-wtsapi32-wtsunregistersessionnotification - """ - if not _isSessionTrackingRegistered: - log.info("Not unregistered session tracking, it was not registered.") - return - if ctypes.windll.wtsapi32.WTSUnRegisterSessionNotification(handle): - log.debug("Unregistered session tracking") - else: - error = ctypes.WinError() - log.error("Unexpected error unregistering session tracking.", exc_info=error) + # TODO: improve deprecation practice on beta/master merges + log.error( + "NVDA no longer registers session tracking notifications. " + "This function is deprecated, for removal in 2023.1. " + "It was never expected that add-on authors would use this function" + ) def handleSessionChange(newState: WindowsTrackedSession, sessionId: int) -> None: - """ - Keeps track of the Windows session state. - When a session change event occurs, the new state is added and the opposite state - is removed. - - For example a "SESSION_LOCK" event removes the "SESSION_UNLOCK" state. - - This does not track SESSION_REMOTE_CONTROL, which isn't part of a logical pair of states. - Managing the state of this is more complex, and NVDA does not need to track this status. - - https://docs.microsoft.com/en-us/windows/win32/termserv/wm-wtssession-change - """ - with _updateSessionStateLock: - stateChanged = False - - log.debug(f"Windows Session state notification received: {newState.name}") - - if not _isSessionTrackingRegistered: - log.debugWarning("Session tracking not registered, unexpected session change message") - - if newState not in _toggleWindowsSessionStatePair: - log.debug(f"Ignoring {newState} event as tracking is not required.") - return - - oppositeState = _toggleWindowsSessionStatePair[newState] - if newState in _currentSessionStates: - log.error( - f"NVDA expects Windows to be in {newState} already. " - f"NVDA may have dropped a {oppositeState} event. " - f"Dropping this {newState} event. " - ) - else: - _currentSessionStates.add(newState) - stateChanged = True - - if oppositeState in _currentSessionStates: - _currentSessionStates.remove(oppositeState) - else: - log.debugWarning( - f"NVDA expects Windows to be in {newState} already. " - f"NVDA may have dropped a {oppositeState} event. " - ) - - log.debug(f"New Windows Session state: {_currentSessionStates}") - if ( - stateChanged - and newState in {WindowsTrackedSession.SESSION_LOCK, WindowsTrackedSession.SESSION_UNLOCK} - ): - from utils.security import postSessionLockStateChanged - postSessionLockStateChanged.notify(isNowLocked=newState == WindowsTrackedSession.SESSION_LOCK) + # TODO: improve deprecation practice on beta/master merges + log.error( + "NVDA no longer registers session tracking notifications. " + "This function is deprecated, for removal in 2023.1. " + "It was never expected that add-on authors would use this function" + ) @contextmanager @@ -445,6 +271,8 @@ def _getSessionLockedValue() -> WTS_LockState: with WTSCurrentSessionInfoEx() as info: infoEx: WTSINFOEX_LEVEL1_W = info.contents.Data.WTSInfoExLevel1 sessionFlags: ctypes.wintypes.LONG = infoEx.SessionFlags - lockState = WTS_LockState(sessionFlags) - log.debug(f"Query Lock state result: {lockState!r}") + try: + lockState = WTS_LockState(sessionFlags) + except ValueError: + return WTS_LockState.WTS_SESSIONSTATE_UNKNOWN return lockState diff --git a/source/winUser.py b/source/winUser.py index 5984f7fd299..855581a8319 100644 --- a/source/winUser.py +++ b/source/winUser.py @@ -500,7 +500,7 @@ def sendMessage(hwnd,msg,param1,param2): return user32.SendMessageW(hwnd,msg,param1,param2) -def getWindowThreadProcessID(hwnd: HWND) -> Tuple[int, int]: +def getWindowThreadProcessID(hwnd: HWNDVal) -> Tuple[int, int]: """Returns a tuple of (processID, threadID)""" processID=c_int() threadID=user32.GetWindowThreadProcessId(hwnd,byref(processID))