From 9ac70874793378559e6f11fe4cdb2688a5928662 Mon Sep 17 00:00:00 2001 From: buddsean Date: Thu, 20 Jan 2022 15:56:48 +1100 Subject: [PATCH] Log exceptions for easeOfAccess and config --- source/config/__init__.py | 233 +++++++++++++++++++++++++++++++------- source/easeOfAccess.py | 123 +++++++++++++++----- source/installer.py | 25 ++-- 3 files changed, 303 insertions(+), 78 deletions(-) diff --git a/source/config/__init__.py b/source/config/__init__.py index 6ff2b20a9bc..8e784a7a533 100644 --- a/source/config/__init__.py +++ b/source/config/__init__.py @@ -1,5 +1,5 @@ # A part of NonVisual Desktop Access (NVDA) -# Copyright (C) 2006-2021 NV Access Limited, Aleksey Sadovoy, Peter Vágner, Rui Batista, Zahari Yurukov, +# Copyright (C) 2006-2022 NV Access Limited, Aleksey Sadovoy, Peter Vágner, Rui Batista, Zahari Yurukov, # Joseph Lee, Babbage B.V., Łukasz Golonka, Julien Cochuyt # This file is covered by the GNU General Public License. # See the file COPYING for more details. @@ -10,6 +10,7 @@ For the latter two actions, one can perform actions prior to and/or after they take place. """ +from enum import Enum import globalVars import winreg import ctypes @@ -73,43 +74,103 @@ def saveOnExit(): except: pass -def isInstalledCopy(): + +class RegistryKey(str, Enum): + INSTALLED_COPY = r"SOFTWARE\Microsoft\Windows\CurrentVersion\Uninstall\NVDA" + RUN = r"SOFTWARE\Microsoft\Windows\CurrentVersion\Run" + NVDA = r"SOFTWARE\NVDA" + r""" + The name of the registry key stored under HKEY_LOCAL_MACHINE where system wide NVDA settings are stored. + Note that NVDA is a 32-bit application, so on X64 systems, + this will evaluate to `r"SOFTWARE\WOW6432Node\nvda"` + """ + + +def isInstalledCopy() -> bool: """Checks to see if this running copy of NVDA is installed on the system""" try: - k=winreg.OpenKey(winreg.HKEY_LOCAL_MACHINE,r"SOFTWARE\Microsoft\Windows\CurrentVersion\Uninstall\NVDA") - instDir=winreg.QueryValueEx(k,"UninstallDirectory")[0] + k = winreg.OpenKey( + winreg.HKEY_LOCAL_MACHINE, + RegistryKey.INSTALLED_COPY.value + ) + except FileNotFoundError: + log.debug( + f"Unable to find isInstalledCopy registry key {RegistryKey.INSTALLED_COPY}" + "- this is not an installed copy." + ) + return False except WindowsError: + log.error( + f"Unable to open isInstalledCopy registry key {RegistryKey.INSTALLED_COPY}", + exc_info=True + ) + return False + + try: + instDir = winreg.QueryValueEx(k, "UninstallDirectory")[0] + except FileNotFoundError: + log.debug( + f"Unable to find UninstallDirectory value for {RegistryKey.INSTALLED_COPY}" + "- this may not be an installed copy." + ) return False + except WindowsError: + log.error("Unable to query isInstalledCopy registry key", exc_info=True) + return False + winreg.CloseKey(k) try: return os.stat(instDir) == os.stat(globalVars.appDir) except WindowsError: + log.error( + "Failed to access the installed NVDA directory," + "or, a portable copy failed to access the current NVDA app directory", + exc_info=True + ) return False -#: #6864: The name of the subkey stored under NVDA_REGKEY where the value is stored -#: which will make an installed NVDA load the user configuration either from the local or from the roaming application data profile. -#: The registry value is unset by default. -#: When setting it manually, a DWORD value is prefered. -#: A value of 0 will evaluate to loading the configuration from the roaming application data (default). -#: A value of 1 means loading the configuration from the local application data folder. -#: @type: str -CONFIG_IN_LOCAL_APPDATA_SUBKEY=u"configInLocalAppData" +CONFIG_IN_LOCAL_APPDATA_SUBKEY = "configInLocalAppData" +""" +#6864: The name of the subkey stored under RegistryKey.NVDA where the value is stored +which will make an installed NVDA load the user configuration either from the local or from +the roaming application data profile. +The registry value is unset by default. +When setting it manually, a DWORD value is preferred. +A value of 0 will evaluate to loading the configuration from the roaming application data (default). +A value of 1 means loading the configuration from the local application data folder. +""" + + +def getInstalledUserConfigPath() -> Optional[str]: + try: + k = winreg.OpenKey(winreg.HKEY_LOCAL_MACHINE, RegistryKey.NVDA.value) + except WindowsError: + log.error("Could not find nvda registry key", exc_info=True) + return None -def getInstalledUserConfigPath(): try: - k = winreg.OpenKey(winreg.HKEY_LOCAL_MACHINE, NVDA_REGKEY) configInLocalAppData = bool(winreg.QueryValueEx(k, CONFIG_IN_LOCAL_APPDATA_SUBKEY)[0]) + except FileNotFoundError: + log.debug("Installed user config is not in local app data") + configInLocalAppData = False except WindowsError: - configInLocalAppData=False + log.error( + f"Could not query if user config in local app data {CONFIG_IN_LOCAL_APPDATA_SUBKEY}", + exc_info=True + ) + configInLocalAppData = False configParent = shlobj.SHGetKnownFolderPath( shlobj.FolderId.LOCAL_APP_DATA if configInLocalAppData else shlobj.FolderId.ROAMING_APP_DATA ) try: return os.path.join(configParent, "nvda") except WindowsError: + # (#13242) There is some uncertainty as to how this could be caused + log.debugWarning("Installed user config is not in local app data", exc_info=True) return None + def getUserDefaultConfigPath(useInstalledPathIfExists=False): """Get the default path for the user configuration directory. This is the default path and doesn't reflect overriding from the command line, @@ -181,26 +242,84 @@ def initConfigPath(configPath=None): if not os.path.isdir(subdir): os.makedirs(subdir) -RUN_REGKEY = r"SOFTWARE\Microsoft\Windows\CurrentVersion\Run" -def getStartAfterLogon(): +def getStartAfterLogon() -> bool: + """Not to be confused with getStartOnLogonScreen. + + Checks if NVDA is set to start after a logon. + Checks related easeOfAccess current user registry keys on Windows 8 or newer. + Then, checks the registry run key to see if NVDA + has been registered to start after logon on Windows 7 + or by earlier NVDA versions. + """ if ( easeOfAccess.canConfigTerminateOnDesktopSwitch - and easeOfAccess.willAutoStart(winreg.HKEY_CURRENT_USER) + and easeOfAccess.willAutoStart(easeOfAccess.AutoStartContext.AFTER_LOGON) ): return True try: - k = winreg.OpenKey(winreg.HKEY_CURRENT_USER, RUN_REGKEY) - val = winreg.QueryValueEx(k, u"nvda")[0] - return os.stat(val) == os.stat(sys.argv[0]) - except (WindowsError, OSError): + k = winreg.OpenKey(winreg.HKEY_CURRENT_USER, RegistryKey.RUN.value) + except FileNotFoundError: + log.debugWarning( + f"Unable to find run registry key {RegistryKey.RUN}", + exc_info=True + ) + return False + except WindowsError: + log.error( + f"Unable to open run registry key {RegistryKey.RUN}", + exc_info=True + ) + return False + + try: + val = winreg.QueryValueEx(k, "nvda")[0] + except FileNotFoundError: + log.debug("NVDA is not set to start after logon") + return False + except WindowsError: + log.error("Failed to query NVDA key to set start after logon", exc_info=True) + return False + + try: + startAfterLogonPath = os.stat(val) + except WindowsError: + log.error( + "Failed to access the start after logon directory.", + exc_info=True + ) + return False + + try: + currentSourcePath = os.stat(sys.argv[0]) + except FileNotFoundError: + log.debug("Failed to access the current running NVDA directory.") + return False + except WindowsError: + log.error( + "Failed to access the current running NVDA directory.", + exc_info=True + ) return False -def setStartAfterLogon(enable): + return currentSourcePath == startAfterLogonPath + + +def setStartAfterLogon(enable: bool) -> None: + """Not to be confused with setStartOnLogonScreen. + + Toggle if NVDA automatically starts after a logon. + Sets easeOfAccess related registry keys on Windows 8 or newer. + + On Windows 7 this sets the registry run key. + + When toggling off, always delete the registry run key to + in case it was set by an earlier version of NVDA or on Windows 7 or earlier. + """ if getStartAfterLogon() == enable: return if easeOfAccess.canConfigTerminateOnDesktopSwitch: - easeOfAccess.setAutoStart(winreg.HKEY_CURRENT_USER, enable) + easeOfAccess.setAutoStart(easeOfAccess.AutoStartContext.AFTER_LOGON, enable) if enable: return # We're disabling, so ensure the run key is cleared, @@ -208,34 +327,60 @@ def setStartAfterLogon(enable): run = False else: run = enable - k = winreg.OpenKey(winreg.HKEY_CURRENT_USER, RUN_REGKEY, 0, winreg.KEY_WRITE) + k = winreg.OpenKey(winreg.HKEY_CURRENT_USER, RegistryKey.RUN.value, 0, winreg.KEY_WRITE) if run: - winreg.SetValueEx(k, u"nvda", None, winreg.REG_SZ, sys.argv[0]) + winreg.SetValueEx(k, "nvda", None, winreg.REG_SZ, sys.argv[0]) else: try: - winreg.DeleteValue(k, u"nvda") + winreg.QueryValue(k, "nvda") + except FileNotFoundError: + log.debug( + "The run registry key is not set for setStartAfterLogon." + "This is expected for Windows 8+ which uses ease of access" + ) + return + try: + winreg.DeleteValue(k, "nvda") except WindowsError: - pass - + log.error( + "Couldn't unset registry key for nvda to start after logon.", + exc_info=True + ) SLAVE_FILENAME = os.path.join(globalVars.appDir, "nvda_slave.exe") -#: The name of the registry key stored under HKEY_LOCAL_MACHINE where system wide NVDA settings are stored. -#: Note that NVDA is a 32-bit application, so on X64 systems, this will evaluate to "SOFTWARE\WOW6432Node\nvda" -NVDA_REGKEY = r"SOFTWARE\NVDA" -def getStartOnLogonScreen(): - if easeOfAccess.willAutoStart(winreg.HKEY_LOCAL_MACHINE): +def getStartOnLogonScreen() -> bool: + """Not to be confused with getStartAfterLogon. + + Checks if NVDA is set to start on the logon screen. + + Checks related easeOfAccess local machine registry keys. + Then, checks a NVDA registry key to see if NVDA + has been registered to start on logon by earlier NVDA versions. + """ + if easeOfAccess.willAutoStart(easeOfAccess.AutoStartContext.ON_LOGON_SCREEN): return True try: - k = winreg.OpenKey(winreg.HKEY_LOCAL_MACHINE, NVDA_REGKEY) - return bool(winreg.QueryValueEx(k, u"startOnLogonScreen")[0]) + k = winreg.OpenKey(winreg.HKEY_LOCAL_MACHINE, RegistryKey.NVDA.value) + except FileNotFoundError: + log.debugWarning(f"Could not find NVDA reg key {RegistryKey.NVDA}", exc_info=True) + except WindowsError: + log.error(f"Failed to open NVDA reg key {RegistryKey.NVDA}", exc_info=True) + try: + return bool(winreg.QueryValueEx(k, "startOnLogonScreen")[0]) + except FileNotFoundError: + log.debug(f"Could not find startOnLogonScreen value for {RegistryKey.NVDA} - likely unset.") + return False except WindowsError: + log.error(f"Failed to query startOnLogonScreen value for {RegistryKey.NVDA}", exc_info=True) return False -def _setStartOnLogonScreen(enable): - easeOfAccess.setAutoStart(winreg.HKEY_LOCAL_MACHINE, enable) + +def _setStartOnLogonScreen(enable: bool) -> None: + easeOfAccess.setAutoStart(easeOfAccess.AutoStartContext.ON_LOGON_SCREEN, enable) + def setSystemConfigToCurrentConfig(): fromPath = globalVars.appArgs.configPath @@ -279,13 +424,23 @@ def _setSystemConfig(fromPath): destFilePath=os.path.join(curDestDir,f) installer.tryCopyFile(sourceFilePath,destFilePath) -def setStartOnLogonScreen(enable): + +def setStartOnLogonScreen(enable: bool) -> None: + """ + Not to be confused with setStartAfterLogon. + + Toggle whether NVDA starts on the logon screen automatically. + On failure to set, retries with escalated permissions. + + Raises a RuntimeError on failure. + """ if getStartOnLogonScreen() == enable: return try: # Try setting it directly. _setStartOnLogonScreen(enable) except WindowsError: + log.debugWarning("Failed to set start on logon screen's config.") # We probably don't have admin privs, so we need to elevate to do this using the slave. import systemUtils if systemUtils.execElevated( diff --git a/source/easeOfAccess.py b/source/easeOfAccess.py index a1aa4ddae31..bba4680aee7 100644 --- a/source/easeOfAccess.py +++ b/source/easeOfAccess.py @@ -1,36 +1,57 @@ # A part of NonVisual Desktop Access (NVDA) -# Copyright (C) 2014-2021 NV Access Limited +# Copyright (C) 2014-2022 NV Access Limited # This file is covered by the GNU General Public License. # See the file COPYING for more details. """Utilities for working with the Windows Ease of Access Center. """ +from enum import Enum, IntEnum +from typing import List +from logHandler import log import winreg -import ctypes import winUser import winVersion + # Windows >= 8 canConfigTerminateOnDesktopSwitch: bool = winVersion.getWinVer() >= winVersion.WIN8 +_APP_KEY_NAME = "nvda_nvda_v1" + + +class RegistryKey(str, Enum): + ROOT = r"Software\Microsoft\Windows NT\CurrentVersion\Accessibility" + TEMP = r"Software\Microsoft\Windows NT\CurrentVersion\AccessibilityTemp" + APP = r"%s\ATs\%s" % (ROOT, _APP_KEY_NAME) + + +class AutoStartContext(IntEnum): + """Registry HKEY used for tracking when NVDA starts automatically""" + ON_LOGON_SCREEN = winreg.HKEY_LOCAL_MACHINE + AFTER_LOGON = winreg.HKEY_CURRENT_USER -ROOT_KEY = r"Software\Microsoft\Windows NT\CurrentVersion\Accessibility" -APP_KEY_NAME = "nvda_nvda_v1" -APP_KEY_PATH = r"%s\ATs\%s" % (ROOT_KEY, APP_KEY_NAME) -def isRegistered(): +def isRegistered() -> bool: try: - winreg.OpenKey(winreg.HKEY_LOCAL_MACHINE, APP_KEY_PATH, 0, - winreg.KEY_READ | winreg.KEY_WOW64_64KEY) + winreg.OpenKey( + winreg.HKEY_LOCAL_MACHINE, + RegistryKey.APP.value, + 0, + winreg.KEY_READ | winreg.KEY_WOW64_64KEY + ) return True + except FileNotFoundError: + log.debug("Unable to find AT registry key") except WindowsError: - return False + log.error("Unable to open AT registry key", exc_info=True) + return False + def notify(signal): if not isRegistered(): return - with winreg.CreateKey(winreg.HKEY_CURRENT_USER, r"Software\Microsoft\Windows NT\CurrentVersion\AccessibilityTemp") as rkey: - winreg.SetValueEx(rkey, APP_KEY_NAME, None, winreg.REG_DWORD, signal) + with winreg.CreateKey(winreg.HKEY_CURRENT_USER, RegistryKey.TEMP.value) as rkey: + winreg.SetValueEx(rkey, _APP_KEY_NAME, None, winreg.REG_DWORD, signal) keys = [] # The user might be holding unwanted modifiers. for vk in winUser.VK_SHIFT, winUser.VK_CONTROL, winUser.VK_MENU: @@ -55,36 +76,78 @@ def notify(signal): inputs.append(input) winUser.SendInput(inputs) -def willAutoStart(hkey): + +def willAutoStart(autoStartContext: AutoStartContext) -> bool: + """Based on autoStartContext, gets whether NVDA starts automatically: + - AutoStartContext.ON_LOGON_SCREEN : on the logon screen + - AutoStartContext.AFTER_LOGON : after logging on + + Returns False on failure + """ + return (_APP_KEY_NAME in _getAutoStartConfiguration(autoStartContext)) + + +def _getAutoStartConfiguration(autoStartContext: AutoStartContext) -> List[str]: + """Based on autoStartContext, returns a list of app names which start automatically: + - AutoStartContext.ON_LOGON_SCREEN : on the logon screen + - AutoStartContext.AFTER_LOGON : after logging on + + Returns an empty list on failure. + """ try: - k = winreg.OpenKey(hkey, ROOT_KEY, 0, - winreg.KEY_READ | winreg.KEY_WOW64_64KEY) - return (APP_KEY_NAME in - winreg.QueryValueEx(k, "Configuration")[0].split(",")) + k = winreg.OpenKey( + autoStartContext.value, + RegistryKey.ROOT.value, + 0, + winreg.KEY_READ | winreg.KEY_WOW64_64KEY + ) + except FileNotFoundError: + log.debug(f"Unable to find existing {autoStartContext} {RegistryKey.ROOT}") + return [] except WindowsError: - return False + log.error(f"Unable to open {autoStartContext} {RegistryKey.ROOT} for reading", exc_info=True) + return [] -def setAutoStart(hkey, enable): - k = winreg.OpenKey(hkey, ROOT_KEY, 0, - winreg.KEY_READ | winreg.KEY_WRITE | winreg.KEY_WOW64_64KEY) try: - conf = winreg.QueryValueEx(k, "Configuration")[0].split(",") + conf: List[str] = winreg.QueryValueEx(k, "Configuration")[0].split(",") + except FileNotFoundError: + log.debug(f"Unable to find {autoStartContext} {RegistryKey.ROOT} configuration") except WindowsError: - conf = [] + log.error(f"Unable to query {autoStartContext} {RegistryKey.ROOT} configuration", exc_info=True) else: if not conf[0]: # "".split(",") returns [""], so remove the empty string. del conf[0] + return conf + return [] + + +def setAutoStart(autoStartContext: AutoStartContext, enable: bool) -> None: + """ + Based on autoStartContext, sets NVDA to start automatically: + - AutoStartContext.ON_LOGON_SCREEN : on the logon screen + - AutoStartContext.AFTER_LOGON : after logging on + + May set to False if checking if currently enabled fails. + Raises `Union[WindowsError, FileNotFoundError]` + """ + conf = _getAutoStartConfiguration(autoStartContext) + currentlyEnabled = _APP_KEY_NAME in conf changed = False - if enable and APP_KEY_NAME not in conf: - conf.append(APP_KEY_NAME) + + if enable and not currentlyEnabled: + conf.append(_APP_KEY_NAME) changed = True - elif not enable: - try: - conf.remove(APP_KEY_NAME) - changed = True - except ValueError: - pass + elif not enable and currentlyEnabled: + conf.remove(_APP_KEY_NAME) + changed = True + if changed: + k = winreg.OpenKey( + autoStartContext.value, + RegistryKey.ROOT.value, + 0, + winreg.KEY_READ | winreg.KEY_WRITE | winreg.KEY_WOW64_64KEY + ) winreg.SetValueEx(k, "Configuration", None, winreg.REG_SZ, ",".join(conf)) diff --git a/source/installer.py b/source/installer.py index b8e0ea2f44e..253e3e5020a 100644 --- a/source/installer.py +++ b/source/installer.py @@ -66,7 +66,7 @@ def createShortcut(path,targetPath=None,arguments=None,iconLocation=None,working def getStartMenuFolder(noDefault=False): try: - with winreg.OpenKey(winreg.HKEY_LOCAL_MACHINE,config.NVDA_REGKEY) as k: + with winreg.OpenKey(winreg.HKEY_LOCAL_MACHINE, config.RegistryKey.NVDA.value) as k: return winreg.QueryValueEx(k,u"Start Menu Folder")[0] except WindowsError: return defaultStartMenuFolder if not noDefault else None @@ -243,7 +243,7 @@ def registerInstallation(installDir,startMenuFolder,shouldCreateDesktopShortcut, winreg.SetValueEx(k,name,None,winreg.REG_SZ,value.format(installDir=installDir)) with winreg.CreateKeyEx(winreg.HKEY_LOCAL_MACHINE,"SOFTWARE\\Microsoft\\Windows\\CurrentVersion\\App Paths\\nvda.exe",0,winreg.KEY_WRITE) as k: winreg.SetValueEx(k,"",None,winreg.REG_SZ,os.path.join(installDir,"nvda.exe")) - with winreg.CreateKeyEx(winreg.HKEY_LOCAL_MACHINE,config.NVDA_REGKEY,0,winreg.KEY_WRITE) as k: + with winreg.CreateKeyEx(winreg.HKEY_LOCAL_MACHINE, config.RegistryKey.NVDA.value, 0, winreg.KEY_WRITE) as k: winreg.SetValueEx(k,"startMenuFolder",None,winreg.REG_SZ,startMenuFolder) if configInLocalAppData: winreg.SetValueEx(k,config.CONFIG_IN_LOCAL_APPDATA_SUBKEY,None,winreg.REG_DWORD,int(configInLocalAppData)) @@ -412,9 +412,12 @@ def isDesktopShortcutInstalled(): def unregisterInstallation(keepDesktopShortcut=False): try: - winreg.DeleteKeyEx(winreg.HKEY_LOCAL_MACHINE, easeOfAccess.APP_KEY_PATH, - winreg.KEY_WOW64_64KEY) - easeOfAccess.setAutoStart(winreg.HKEY_LOCAL_MACHINE, False) + winreg.DeleteKeyEx( + winreg.HKEY_LOCAL_MACHINE, + easeOfAccess.RegistryKey.APP.value, + winreg.KEY_WOW64_64KEY + ) + easeOfAccess.setAutoStart(easeOfAccess.AutoStartContext.ON_LOGON_SCREEN, False) except WindowsError: pass wsh=_getWSH() @@ -439,7 +442,7 @@ def unregisterInstallation(keepDesktopShortcut=False): except WindowsError: pass try: - winreg.DeleteKey(winreg.HKEY_LOCAL_MACHINE,config.NVDA_REGKEY) + winreg.DeleteKey(winreg.HKEY_LOCAL_MACHINE, config.RegistryKey.NVDA.value) except WindowsError: pass unregisterAddonFileAssociation() @@ -553,7 +556,7 @@ def tryCopyFile(sourceFilePath,destFilePath): def install(shouldCreateDesktopShortcut=True,shouldRunAtLogon=True): prevInstallPath=getInstallPath(noDefault=True) try: - k = winreg.OpenKey(winreg.HKEY_LOCAL_MACHINE, config.NVDA_REGKEY) + k = winreg.OpenKey(winreg.HKEY_LOCAL_MACHINE, config.RegistryKey.NVDA.value) configInLocalAppData = bool(winreg.QueryValueEx(k, config.CONFIG_IN_LOCAL_APPDATA_SUBKEY)[0]) except WindowsError: configInLocalAppData = False @@ -615,8 +618,12 @@ def createPortableCopy(destPath,shouldCopyUserConfig=True): removeOldLibFiles(destPath,rebootOK=True) def registerEaseOfAccess(installDir): - with winreg.CreateKeyEx(winreg.HKEY_LOCAL_MACHINE, easeOfAccess.APP_KEY_PATH, 0, - winreg.KEY_ALL_ACCESS | winreg.KEY_WOW64_64KEY) as appKey: + with winreg.CreateKeyEx( + winreg.HKEY_LOCAL_MACHINE, + easeOfAccess.RegistryKey.APP.value, + 0, + winreg.KEY_ALL_ACCESS | winreg.KEY_WOW64_64KEY + ) as appKey: winreg.SetValueEx(appKey, "ApplicationName", None, winreg.REG_SZ, versionInfo.name) winreg.SetValueEx(appKey, "Description", None, winreg.REG_SZ,