From 40e1d070853e21a43d07db989dde6bca8a26e85a Mon Sep 17 00:00:00 2001 From: Michael Curran Date: Wed, 30 Sep 2020 13:59:02 +1000 Subject: [PATCH] Allow NVDA to still function if a component changes the current directory (#11707) * nvda.pyw: ensure that all paths coming from commandline arguments are made absolute as soon as possible to protect against the current directory changing later on. Also store NVDA's app dir in globalVars. * Use the NVDA app dir rather than the current directory for relative paths. * Fix unit tests. * Remove all usage of os.getcwd and replace it with globalVars.appDir * Replace all remaining os.path.join("* calls with os.path.join(globalVars.appDir calls. * nvda.pyw: provide an absolute path to gettext.translate * nvda_slave: set globalVars.appDir, and provide an absolute path to gettext.translate * getDefaultLogFilePath no longer uses the current directory. * brailleTables: TABLES_DIR is no longer relative to the current directory. * ui.browsableMessage no longer uses a relative path to get to the html file. * Change all playWavefile calls to be non-relative * Fix linting issues * another relative wave file path * Fix linting issues * speechDictHandler: the path to builtin.dic is no longer relative. * config: slave_fileName is no longer relative * Lilli braille driver: path to dll is no longer relative. * Fix linting issues * nvda_slave: don't load nvdaRemote with a relative path. * Remove all usage of os.path.abspath, but add a couple of assertions in places where we can't be completely sure the path is absolute. * Fix translation comments * Add the ALTERED_LIBRARY_SEARCH_PATH constant to winKernel and use it in NVDAHelper and nvda_slave when loading NvDAHelperRemote. * Lili braille dirver: remove unneeded import. * Update what's new * addonHandler.getCodeAddon: make sure to normalize paths when comparing them to stop an infinite while loop introduced in #11650 --- source/COMRegistrationFixes/__init__.py | 3 +- source/NVDAHelper.py | 18 +++++--- source/NVDAObjects/__init__.py | 5 ++- source/NVDAObjects/behaviors.py | 6 ++- source/addonHandler/__init__.py | 26 +++++++----- source/brailleDisplayDrivers/lilli.py | 6 ++- source/brailleTables.py | 5 ++- source/browseMode.py | 7 +++- source/characterProcessing.py | 10 +++-- source/config/__init__.py | 9 ++-- source/core.py | 16 ++++--- source/fonts/__init__.py | 4 +- source/gui/__init__.py | 4 +- source/gui/installerGui.py | 16 ++++++- source/inputCore.py | 4 +- source/installer.py | 6 +-- source/logHandler.py | 6 +-- source/nvda.pyw | 27 ++++++++++-- source/nvda_slave.pyw | 42 +++++++++++++++---- source/speechDictHandler/__init__.py | 2 +- source/synthDrivers/_espeak.py | 7 ++-- source/systemUtils.py | 2 +- source/ui.py | 3 +- source/updateCheck.py | 4 +- .../screenCurtain.py | 6 ++- source/watchdog.py | 2 +- source/winKernel.py | 3 ++ tests/unit/__init__.py | 7 +++- user_docs/en/changes.t2t | 2 + 29 files changed, 182 insertions(+), 76 deletions(-) diff --git a/source/COMRegistrationFixes/__init__.py b/source/COMRegistrationFixes/__init__.py index 6f32f1d9d81..968be869653 100644 --- a/source/COMRegistrationFixes/__init__.py +++ b/source/COMRegistrationFixes/__init__.py @@ -8,6 +8,7 @@ import os import subprocess import winVersion +import globalVars from logHandler import log # Particular 64 bit / 32 bit system paths @@ -53,7 +54,7 @@ def applyRegistryPatch(fileName,wow64=False): log.debug("Applied registry patch: %s with %s"%(fileName,regedit)) -OLEACC_REG_FILE_PATH = os.path.abspath(os.path.join("COMRegistrationFixes", "oleaccProxy.reg")) +OLEACC_REG_FILE_PATH = os.path.join(globalVars.appDir, "COMRegistrationFixes", "oleaccProxy.reg") def fixCOMRegistrations(): """ Registers most common COM proxies, in case they had accidentally been unregistered or overwritten by 3rd party software installs/uninstalls. diff --git a/source/NVDAHelper.py b/source/NVDAHelper.py index 1c61eed23ca..3ab07dea725 100755 --- a/source/NVDAHelper.py +++ b/source/NVDAHelper.py @@ -17,6 +17,7 @@ WINFUNCTYPE, c_long, c_wchar, + windll, ) from ctypes.wintypes import * from comtypes import BSTR @@ -29,11 +30,11 @@ import time import globalVars -versionedLibPath='lib' +versionedLibPath = os.path.join(globalVars.appDir, 'lib') if os.environ.get('PROCESSOR_ARCHITEW6432') == 'ARM64': - versionedLib64Path = 'libArm64' + versionedLib64Path = os.path.join(globalVars.appDir, 'libArm64') else: - versionedLib64Path = 'lib64' + versionedLib64Path = os.path.join(globalVars.appDir, 'lib64') if getattr(sys,'frozen',None): # Not running from source. Libraries are in a version-specific directory versionedLibPath=os.path.join(versionedLibPath,versionInfo.version) @@ -510,8 +511,15 @@ def initialize(): if config.isAppX: log.info("Remote injection disabled due to running as a Windows Store Application") return - #Load nvdaHelperRemote.dll but with an altered search path so it can pick up other dlls in lib - h=windll.kernel32.LoadLibraryExW(os.path.abspath(os.path.join(versionedLibPath,u"nvdaHelperRemote.dll")),0,0x8) + # Load nvdaHelperRemote.dll + h = windll.kernel32.LoadLibraryExW( + os.path.join(versionedLibPath, "nvdaHelperRemote.dll"), + 0, + # Using an altered search path is necessary here + # As NVDAHelperRemote needs to locate dependent dlls in the same directory + # such as minhook.dll. + winKernel.LOAD_WITH_ALTERED_SEARCH_PATH + ) if not h: log.critical("Error loading nvdaHelperRemote.dll: %s" % WinError()) return diff --git a/source/NVDAObjects/__init__.py b/source/NVDAObjects/__init__.py index f0fff359c34..96237882e3c 100644 --- a/source/NVDAObjects/__init__.py +++ b/source/NVDAObjects/__init__.py @@ -8,6 +8,7 @@ """Module that contains the base NVDA object type with dynamic class creation support, as well as the associated TextInfo class.""" +import os import time import re import weakref @@ -31,6 +32,8 @@ import brailleInput import locationHelper import aria +import globalVars + class NVDAObjectTextInfo(textInfos.offsets.OffsetsTextInfo): """A default TextInfo which is used to enable text review of information about widgets that don't support text content. @@ -1030,7 +1033,7 @@ def _reportErrorInPreviousWord(self): # No error. return import nvwave - nvwave.playWaveFile(r"waves\textError.wav") + nvwave.playWaveFile(os.path.join(globalVars.appDir, "waves", "textError.wav")) def event_liveRegionChange(self): """ diff --git a/source/NVDAObjects/behaviors.py b/source/NVDAObjects/behaviors.py index a3dfbe9a549..e09d93ec28e 100755 --- a/source/NVDAObjects/behaviors.py +++ b/source/NVDAObjects/behaviors.py @@ -29,6 +29,8 @@ import ui import braille import nvwave +import globalVars + class ProgressBar(NVDAObject): @@ -796,7 +798,7 @@ def event_suggestionsOpened(self): # Translators: Announced in braille when suggestions appear when search term is entered in various search fields such as Start search box in Windows 10. braille.handler.message(_("Suggestions")) if config.conf["presentation"]["reportAutoSuggestionsWithSound"]: - nvwave.playWaveFile(r"waves\suggestionsOpened.wav") + nvwave.playWaveFile(os.path.join(globalVars.appDir, "waves", "suggestionsOpened.wav")) def event_suggestionsClosed(self): """Called when suggestions list or container is closed. @@ -804,7 +806,7 @@ def event_suggestionsClosed(self): By default NVDA will announce this via speech, braille or via a sound. """ if config.conf["presentation"]["reportAutoSuggestionsWithSound"]: - nvwave.playWaveFile(r"waves\suggestionsClosed.wav") + nvwave.playWaveFile(os.path.join(globalVars.appDir, "waves", "suggestionsClosed.wav")) class WebDialog(NVDAObject): """ diff --git a/source/addonHandler/__init__.py b/source/addonHandler/__init__.py index 48f127ccf12..bf9bd6d4421 100644 --- a/source/addonHandler/__init__.py +++ b/source/addonHandler/__init__.py @@ -97,7 +97,7 @@ def getIncompatibleAddons( def completePendingAddonRemoves(): """Removes any add-ons that could not be removed on the last run of NVDA""" - user_addons = os.path.abspath(os.path.join(globalVars.appArgs.configPath, "addons")) + user_addons = os.path.join(globalVars.appArgs.configPath, "addons") pendingRemovesSet=state['pendingRemovesSet'] for addonName in list(pendingRemovesSet): addonPath=os.path.join(user_addons,addonName) @@ -111,7 +111,7 @@ def completePendingAddonRemoves(): pendingRemovesSet.discard(addonName) def completePendingAddonInstalls(): - user_addons = os.path.abspath(os.path.join(globalVars.appArgs.configPath, "addons")) + user_addons = os.path.join(globalVars.appArgs.configPath, "addons") pendingInstallsSet=state['pendingInstallsSet'] for addonName in pendingInstallsSet: newPath=os.path.join(user_addons,addonName) @@ -123,7 +123,7 @@ def completePendingAddonInstalls(): pendingInstallsSet.clear() def removeFailedDeletions(): - user_addons = os.path.abspath(os.path.join(globalVars.appArgs.configPath, "addons")) + user_addons = os.path.join(globalVars.appArgs.configPath, "addons") for p in os.listdir(user_addons): if p.endswith(DELETEDIR_SUFFIX): path=os.path.join(user_addons,p) @@ -170,7 +170,7 @@ def _getDefaultAddonPaths(): @rtype: list(string) """ addon_paths = [] - user_addons = os.path.abspath(os.path.join(globalVars.appArgs.configPath, "addons")) + user_addons = os.path.join(globalVars.appArgs.configPath, "addons") if os.path.isdir(user_addons): addon_paths.append(user_addons) return addon_paths @@ -280,7 +280,7 @@ def __init__(self, path): @param path: the base directory for the addon data. @type path: string """ - self.path = os.path.abspath(path) + self.path = path self._extendedPackages = set() manifest_path = os.path.join(path, MANIFEST_FILENAME) with open(manifest_path, 'rb') as f: @@ -511,19 +511,23 @@ def getCodeAddon(obj=None, frameDist=1): if obj is None: obj = sys._getframe(frameDist) fileName = inspect.getfile(obj) - dir= os.path.abspath(os.path.dirname(fileName)) + assert os.path.isabs(fileName), f"Module file name {fileName} is not absolute" + dir = os.path.normpath(os.path.dirname(fileName)) # if fileName is not a subdir of one of the addon paths # It does not belong to an addon. - for p in _getDefaultAddonPaths(): - if dir.startswith(p): + addonsPath = None + for addonsPath in _getDefaultAddonPaths(): + addonsPath = os.path.normpath(addonsPath) + if dir.startswith(addonsPath): break else: raise AddonError("Code does not belong to an addon package.") + assert addonsPath is not None curdir = dir - while curdir not in _getDefaultAddonPaths(): + while curdir.startswith(addonsPath) and len(curdir) > len(addonsPath): if curdir in _availableAddons: return _availableAddons[curdir] - curdir = os.path.abspath(os.path.join(curdir, "..")) + curdir = os.path.normpath(os.path.join(curdir, "..")) # Not found! raise AddonError("Code does not belong to an addon") @@ -609,7 +613,7 @@ def __repr__(self): def createAddonBundleFromPath(path, destDir=None): """ Creates a bundle from a directory that contains a a addon manifest file.""" - basedir = os.path.abspath(path) + basedir = path # If caller did not provide a destination directory name # Put the bundle at the same level as the add-on's top-level directory, # That is, basedir/.. diff --git a/source/brailleDisplayDrivers/lilli.py b/source/brailleDisplayDrivers/lilli.py index 9158ec304ab..e94c85a1cd4 100644 --- a/source/brailleDisplayDrivers/lilli.py +++ b/source/brailleDisplayDrivers/lilli.py @@ -5,14 +5,16 @@ #Copyright (C) 2008-2017 NV Access Limited, Gianluca Casalino, Alberto Benassati, Babbage B.V. from typing import Optional, List +import os +import globalVars from logHandler import log -from ctypes import * +from ctypes import windll import inputCore import wx import braille try: - lilliDll=windll.LoadLibrary("brailleDisplayDrivers\\lilli.dll") + lilliDll = windll.LoadLibrary(os.path.join(globalVars.appDir, "brailleDisplayDrivers", "lilli.dll")) except: lilliDll=None diff --git a/source/brailleTables.py b/source/brailleTables.py index 010bf6aa380..2b9e5203222 100644 --- a/source/brailleTables.py +++ b/source/brailleTables.py @@ -6,11 +6,14 @@ """Manages information about available braille translation tables. """ +import os import collections from locale import strxfrm +import globalVars + #: The directory in which liblouis braille tables are located. -TABLES_DIR = r"louis\tables" +TABLES_DIR = os.path.join(globalVars.appDir, "louis", "tables") #: Information about a braille table. #: This has the following attributes: diff --git a/source/browseMode.py b/source/browseMode.py index 3f377057152..84697f3e30f 100644 --- a/source/browseMode.py +++ b/source/browseMode.py @@ -3,6 +3,7 @@ # This file is covered by the GNU General Public License. # See the file COPYING for more details. +import os import itertools import collections import winsound @@ -39,8 +40,10 @@ from NVDAObjects import NVDAObject import gui.contextHelp from abc import ABCMeta, abstractmethod +import globalVars from typing import Optional + REASON_QUICKNAV = OutputReason.QUICKNAV def reportPassThrough(treeInterceptor,onlyIfChanged=True): @@ -52,8 +55,8 @@ def reportPassThrough(treeInterceptor,onlyIfChanged=True): """ if not onlyIfChanged or treeInterceptor.passThrough != reportPassThrough.last: if config.conf["virtualBuffers"]["passThroughAudioIndication"]: - sound = r"waves\focusMode.wav" if treeInterceptor.passThrough else r"waves\browseMode.wav" - nvwave.playWaveFile(sound) + sound = "focusMode.wav" if treeInterceptor.passThrough else "browseMode.wav" + nvwave.playWaveFile(os.path.join(globalVars.appDir, "waves", sound)) else: if treeInterceptor.passThrough: # Translators: The mode to interact with controls in documents diff --git a/source/characterProcessing.py b/source/characterProcessing.py index cf23e26529b..b725c79bd8c 100644 --- a/source/characterProcessing.py +++ b/source/characterProcessing.py @@ -78,7 +78,7 @@ def __init__(self,locale): @type locale: string """ self._entries = {} - fileName=os.path.join('locale',locale,'characterDescriptions.dic') + fileName = os.path.join(globalVars.appDir, 'locale', locale, 'characterDescriptions.dic') if not os.path.isfile(fileName): raise LookupError(fileName) f = codecs.open(fileName,"r","utf_8_sig",errors="replace") @@ -367,12 +367,14 @@ def _getSpeechSymbolsForLocale(locale): # Load the data before loading other symbols, # in order to allow translators to override them. try: - builtin.load(os.path.join("locale", locale, "cldr.dic"), - allowComplexSymbols=False) + builtin.load( + os.path.join(globalVars.appDir, "locale", locale, "cldr.dic"), + allowComplexSymbols=False + ) except IOError: log.debugWarning("No CLDR data for locale %s" % locale) try: - builtin.load(os.path.join("locale", locale, "symbols.dic")) + builtin.load(os.path.join(globalVars.appDir, "locale", locale, "symbols.dic")) except IOError: _noSymbolLocalesCache.add(locale) raise LookupError("No symbol information for locale %s" % locale) diff --git a/source/config/__init__.py b/source/config/__init__.py index 9b2ca5a594a..55c689897e5 100644 --- a/source/config/__init__.py +++ b/source/config/__init__.py @@ -83,7 +83,7 @@ def isInstalledCopy(): return False winreg.CloseKey(k) try: - return os.stat(instDir)==os.stat(os.getcwd()) + return os.stat(instDir) == os.stat(globalVars.appDir) except WindowsError: return False @@ -125,7 +125,7 @@ def getUserDefaultConfigPath(useInstalledPathIfExists=False): # Therefore add a suffix to the directory to make it specific to Windows Store application versions. installedUserConfigPath+='_appx' return installedUserConfigPath - return u'.\\userConfig\\' + return os.path.join(globalVars.appDir, 'userConfig') def getSystemConfigPath(): if isInstalledCopy(): @@ -227,7 +227,8 @@ def canStartOnSecureScreens(): # This function will be transformed into a flag in a future release. return isInstalledCopy() -SLAVE_FILENAME = u"nvda_slave.exe" + +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" @@ -252,7 +253,7 @@ def _setStartOnLogonScreen(enable): winreg.SetValueEx(k, u"startOnLogonScreen", None, winreg.REG_DWORD, int(enable)) def setSystemConfigToCurrentConfig(): - fromPath=os.path.abspath(globalVars.appArgs.configPath) + fromPath = globalVars.appArgs.configPath if ctypes.windll.shell32.IsUserAnAdmin(): _setSystemConfig(fromPath) else: diff --git a/source/core.py b/source/core.py index 17c8c38e675..14efe569747 100644 --- a/source/core.py +++ b/source/core.py @@ -224,7 +224,7 @@ def main(): globalVars.appArgs.configPath=config.getUserDefaultConfigPath(useInstalledPathIfExists=globalVars.appArgs.launcher) #Initialize the config path (make sure it exists) config.initConfigPath() - log.info("Config dir: %s"%os.path.abspath(globalVars.appArgs.configPath)) + log.info(f"Config dir: {globalVars.appArgs.configPath}") log.debug("loading config") import config config.initialize() @@ -232,7 +232,7 @@ def main(): log.info("Developer Scratchpad mode enabled") if not globalVars.appArgs.minimal and config.conf["general"]["playStartAndExitSounds"]: try: - nvwave.playWaveFile("waves\\start.wav") + nvwave.playWaveFile(os.path.join(globalVars.appDir, "waves", "start.wav")) except: pass logHandler.setLogLevelFromConfig() @@ -298,7 +298,10 @@ def onEndSession(evt): speech.cancelSpeech() if not globalVars.appArgs.minimal and config.conf["general"]["playStartAndExitSounds"]: try: - nvwave.playWaveFile("waves\\exit.wav",asynchronous=False) + nvwave.playWaveFile( + os.path.join(globalVars.appDir, "waves", "exit.wav"), + asynchronous=False + ) except: pass log.info("Windows session ending") @@ -410,7 +413,7 @@ def handlePowerStatusChange(self): if not wxLang and '_' in lang: wxLang=locale.FindLanguageInfo(lang.split('_')[0]) if hasattr(sys,'frozen'): - locale.AddCatalogLookupPathPrefix(os.path.join(os.getcwd(),"locale")) + locale.AddCatalogLookupPathPrefix(os.path.join(globalVars.appDir, "locale")) # #8064: Wx might know the language, but may not actually contain a translation database for that language. # If we try to initialize this language, wx will show a warning dialog. # #9089: some languages (such as Aragonese) do not have language info, causing language getter to fail. @@ -591,7 +594,10 @@ def run(self): if not globalVars.appArgs.minimal and config.conf["general"]["playStartAndExitSounds"]: try: - nvwave.playWaveFile("waves\\exit.wav",asynchronous=False) + nvwave.playWaveFile( + os.path.join(globalVars.appDir, "waves", "exit.wav"), + asynchronous=False + ) except: pass # #5189: Destroy the message window as late as possible diff --git a/source/fonts/__init__.py b/source/fonts/__init__.py index f6b565aec0d..ef01b44cf8f 100644 --- a/source/fonts/__init__.py +++ b/source/fonts/__init__.py @@ -1,10 +1,10 @@ -# brailleViewer.py # A part of NonVisual Desktop Access (NVDA) # Copyright (C) 2019 NV Access Limited # This file is covered by the GNU General Public License. # See the file COPYING for more details. from typing import List +import globalVars from logHandler import log import os from ctypes import WinDLL @@ -13,7 +13,7 @@ Loads custom fonts for use in NVDA. """ -fontsDir = os.path.abspath("fonts") +fontsDir = os.path.join(globalVars.appDir, "fonts") def _isSupportedFontPath(f: str) -> bool: diff --git a/source/gui/__init__.py b/source/gui/__init__.py index fd0d4f7b3c3..5b223aac95c 100644 --- a/source/gui/__init__.py +++ b/source/gui/__init__.py @@ -44,7 +44,7 @@ updateCheck = None ### Constants -NVDA_PATH = os.getcwd() +NVDA_PATH = globalVars.appDir ICON_PATH=os.path.join(NVDA_PATH, "images", "nvda.ico") DONATE_URL = "http://www.nvaccess.org/donate/" @@ -57,7 +57,7 @@ def getDocFilePath(fileName, localized=True): if hasattr(sys, "frozen"): getDocFilePath.rootPath = os.path.join(NVDA_PATH, "documentation") else: - getDocFilePath.rootPath = os.path.abspath(os.path.join("..", "user_docs")) + getDocFilePath.rootPath = os.path.join(NVDA_PATH, "..", "user_docs") if localized: lang = languageHandler.getLanguage() diff --git a/source/gui/installerGui.py b/source/gui/installerGui.py index 3faeae27424..fc836c18220 100644 --- a/source/gui/installerGui.py +++ b/source/gui/installerGui.py @@ -307,7 +307,7 @@ def __init__(self, parent): directoryEntryControl = groupHelper.addItem(gui.guiHelper.PathSelectionHelper(self, browseText, dirDialogTitle)) self.portableDirectoryEdit = directoryEntryControl.pathControl if globalVars.appArgs.portablePath: - self.portableDirectoryEdit.Value = os.path.abspath(globalVars.appArgs.portablePath) + self.portableDirectoryEdit.Value = globalVars.appArgs.portablePath # Translators: The label of a checkbox option in the Create Portable NVDA dialog. copyConfText = _("Copy current &user configuration") @@ -343,6 +343,18 @@ def onCreatePortable(self, evt): _("Error"), wx.OK | wx.ICON_ERROR) return + if not os.path.isabs(self.portableDirectoryEdit.Value): + gui.messageBox( + # Translators: The message displayed when the user has not specified an absolute destination directory + # in the Create Portable NVDA dialog. + _("Please specify an absolute path (including drive letter) in which to create the portable copy."), + # Translators: The message title displayed + # when the user has not specified an absolute destination directory + # in the Create Portable NVDA dialog. + _("Error"), + wx.OK | wx.ICON_ERROR + ) + return drv=os.path.splitdrive(self.portableDirectoryEdit.Value)[0] if drv and not os.path.isdir(drv): # Translators: The message displayed when the user specifies an invalid destination drive @@ -395,7 +407,7 @@ def doCreatePortable(portableDirectory,copyUserConfig=False,silent=False,startAf shellapi.ShellExecute( None, None, - os.path.join(os.path.abspath(portableDirectory),'nvda.exe'), + os.path.join(portableDirectory, 'nvda.exe'), None, None, winUser.SW_SHOWNORMAL diff --git a/source/inputCore.py b/source/inputCore.py index 1afaa1969f4..146b6189736 100644 --- a/source/inputCore.py +++ b/source/inputCore.py @@ -565,10 +565,10 @@ def loadLocaleGestureMap(self): self.localeGestureMap.clear() lang = languageHandler.getLanguage() try: - self.localeGestureMap.load(os.path.join("locale", lang, "gestures.ini")) + self.localeGestureMap.load(os.path.join(globalVars.appDir, "locale", lang, "gestures.ini")) except IOError: try: - self.localeGestureMap.load(os.path.join("locale", lang.split('_')[0], "gestures.ini")) + self.localeGestureMap.load(os.path.join(globalVars.appDir, "locale", lang.split('_')[0], "gestures.ini")) except IOError: log.debugWarning("No locale gesture map for language %s" % lang) diff --git a/source/installer.py b/source/installer.py index b450cd1b7f6..f1ca65e4277 100644 --- a/source/installer.py +++ b/source/installer.py @@ -121,7 +121,7 @@ def getDocFilePath(fileName,installDir): return tryPath def copyProgramFiles(destPath): - sourcePath=os.getcwd() + sourcePath = globalVars.appDir detectUserConfig=True detectNVDAExe=True for curSourceDir,subDirs,files in os.walk(sourcePath): @@ -140,7 +140,7 @@ def copyProgramFiles(destPath): tryCopyFile(sourceFilePath,destFilePath) def copyUserConfig(destPath): - sourcePath=os.path.abspath(globalVars.appArgs.configPath) + sourcePath = globalVars.appArgs.configPath for curSourceDir,subDirs,files in os.walk(sourcePath): curDestDir=os.path.join(destPath,os.path.relpath(curSourceDir,sourcePath)) if not os.path.isdir(curDestDir): @@ -598,7 +598,7 @@ def removeOldLoggedFiles(installPath): tryRemoveFile(filePath,rebootOK=True) def createPortableCopy(destPath,shouldCopyUserConfig=True): - destPath=os.path.abspath(destPath) + assert os.path.isabs(destPath), f"Destination path {destPath} is not absolute" #Remove all the main executables always for f in ("nvda.exe","nvda_noUIAccess.exe","nvda_UIAccess.exe"): f=os.path.join(destPath,f) diff --git a/source/logHandler.py b/source/logHandler.py index aab62ce11bd..6fa34b52a6f 100755 --- a/source/logHandler.py +++ b/source/logHandler.py @@ -249,7 +249,7 @@ class RemoteHandler(logging.Handler): def __init__(self): #Load nvdaHelperRemote.dll but with an altered search path so it can pick up other dlls in lib - path=os.path.abspath(os.path.join(u"lib",buildVersion.version,u"nvdaHelperRemote.dll")) + path = os.path.join(globalVars.appDir, "lib", buildVersion.version, "nvdaHelperRemote.dll") h=ctypes.windll.kernel32.LoadLibraryExW(path,0,LOAD_WITH_ALTERED_SEARCH_PATH) if not h: raise OSError("Could not load %s"%path) @@ -276,7 +276,7 @@ def handle(self,record): elif record.levelno>=logging.ERROR and shouldPlayErrorSound: import nvwave try: - nvwave.playWaveFile("waves\\error.wav") + nvwave.playWaveFile(os.path.join(globalVars.appDir, "waves", "error.wav")) except: pass return super().handle(record) @@ -333,7 +333,7 @@ def _getDefaultLogFilePath(): import tempfile return os.path.join(tempfile.gettempdir(), "nvda.log") else: - return ".\\nvda.log" + return os.path.join(globalVars.appDir, "nvda.log") def _excepthook(*exc_info): log.exception(exc_info=exc_info, codepath="unhandled exception") diff --git a/source/nvda.pyw b/source/nvda.pyw index 8d810104e0f..7e8c9bac7bf 100755 --- a/source/nvda.pyw +++ b/source/nvda.pyw @@ -9,23 +9,32 @@ import sys import os +import globalVars + if getattr(sys, "frozen", None): # We are running as an executable. # Append the path of the executable to sys so we can import modules from the dist dir. sys.path.append(sys.prefix) - os.chdir(sys.prefix) + appDir = sys.prefix else: import sourceEnv #We should always change directory to the location of this module (nvda.pyw), don't rely on sys.path[0] - os.chdir(os.path.normpath(os.path.dirname(__file__))) + appDir = os.path.normpath(os.path.dirname(__file__)) +appDir = os.path.abspath(appDir) +os.chdir(appDir) +globalVars.appDir = appDir import ctypes import locale import gettext try: - gettext.translation('nvda',localedir='locale',languages=[locale.getdefaultlocale()[0]]).install(True) + gettext.translation( + 'nvda', + localedir=os.path.join(globalVars.appDir, 'locale'), + languages=[locale.getdefaultlocale()[0]] + ).install(True) except: gettext.install('nvda') @@ -112,6 +121,18 @@ parser.add_argument('--enable-start-on-logon',metavar="True|False",type=stringTo # If this option is provided, NVDA will not replace an already running instance (#10179) parser.add_argument('--ease-of-access',action="store_true",dest='easeOfAccess',default=False,help="Started by Windows Ease of Access") (globalVars.appArgs,globalVars.appArgsExtra)=parser.parse_known_args() +# Make any app args path values absolute +# So as to not be affected by the current directory changing during process lifetime. +pathAppArgs = [ + "configPath", + "logFileName", + "portablePath", +] +for name in pathAppArgs: + origVal = getattr(globalVars.appArgs, name) + if isinstance(origVal, str): + newVal = os.path.abspath(origVal) + setattr(globalVars.appArgs, name, newVal) def terminateRunningNVDA(window): processID,threadID=winUser.getWindowThreadProcessID(window) diff --git a/source/nvda_slave.pyw b/source/nvda_slave.pyw index b011e16050b..81412162954 100755 --- a/source/nvda_slave.pyw +++ b/source/nvda_slave.pyw @@ -8,6 +8,12 @@ Performs miscellaneous tasks which need to be performed in a separate process. """ +import sys +import os +import globalVars +import winKernel + + # Initialise comtypes.client.gen_dir and the comtypes.gen search path # and Append our comInterfaces directory to the comtypes.gen search path. import comtypes @@ -16,23 +22,34 @@ import comtypes.gen import comInterfaces comtypes.gen.__path__.append(comInterfaces.__path__[0]) + +if hasattr(sys, "frozen"): + # Error messages (which are only for debugging) should not cause the py2exe log message box to appear. + sys.stderr = sys.stdout + globalVars.appDir = sys.prefix +else: + globalVars.appDir = os.path.abspath(os.path.dirname(__file__)) + +# #2391: some functions may still require the current directory to be set to NVDA's app dir +os.chdir(globalVars.appDir) + + import gettext import locale #Localization settings try: - gettext.translation('nvda',localedir='locale',languages=[locale.getdefaultlocale()[0]]).install() + gettext.translation( + 'nvda', + localedir=os.path.join(globalVars.appDir, 'locale'), + languages=[locale.getdefaultlocale()[0]] + ).install() except: gettext.install('nvda') -import sys -import os + import versionInfo import logHandler -if hasattr(sys, "frozen"): - # Error messages (which are only for debugging) should not cause the py2exe log message box to appear. - sys.stderr = sys.stdout - #Many functions expect the current directory to be where slave is located (#2391) - os.chdir(sys.prefix) + def main(): import installer @@ -76,7 +93,14 @@ def main(): raise ValueError("Addon path was not provided.") #Load nvdaHelperRemote.dll but with an altered search path so it can pick up other dlls in lib import ctypes - h=ctypes.windll.kernel32.LoadLibraryExW(os.path.abspath(os.path.join(u"lib",versionInfo.version,u"nvdaHelperRemote.dll")),0,0x8) + h = ctypes.windll.kernel32.LoadLibraryExW( + os.path.join(globalVars.appDir, "lib", versionInfo.version, "nvdaHelperRemote.dll"), + 0, + # Using an altered search path is necessary here + # As NVDAHelperRemote needs to locate dependent dlls in the same directory + # such as minhook.dll. + winKernel.LOAD_WITH_ALTERED_SEARCH_PATH + ) remoteLib=ctypes.WinDLL("nvdaHelperRemote",handle=h) ret = remoteLib.nvdaControllerInternal_installAddonPackageFromPath(addonPath) if ret != 0: diff --git a/source/speechDictHandler/__init__.py b/source/speechDictHandler/__init__.py index e4ba411f5ee..9b2e979173d 100644 --- a/source/speechDictHandler/__init__.py +++ b/source/speechDictHandler/__init__.py @@ -123,7 +123,7 @@ def initialize(): for type in dictTypes: dictionaries[type]=SpeechDict() dictionaries["default"].load(os.path.join(speechDictsPath, "default.dic")) - dictionaries["builtin"].load("builtin.dic") + dictionaries["builtin"].load(os.path.join(globalVars.appDir, "builtin.dic")) def loadVoiceDict(synth): """Loads appropriate dictionary for the given synthesizer. diff --git a/source/synthDrivers/_espeak.py b/source/synthDrivers/_espeak.py index 0fc98f615c4..c2a5ee197df 100755 --- a/source/synthDrivers/_espeak.py +++ b/source/synthDrivers/_espeak.py @@ -9,6 +9,7 @@ import nvwave import threading import queue +from ctypes import cdll from ctypes import * import config import globalVars @@ -321,7 +322,7 @@ def initialize(indexCallback=None): the number of the index or C{None} when speech stops. """ global espeakDLL, bgThread, bgQueue, player, onIndexReached - espeakDLL=cdll.LoadLibrary(r"synthDrivers\espeak.dll") + espeakDLL = cdll.LoadLibrary(os.path.join(globalVars.appDir, "synthDrivers", "espeak.dll")) espeakDLL.espeak_Info.restype=c_char_p espeakDLL.espeak_Synth.errcheck=espeak_errcheck espeakDLL.espeak_SetVoiceByName.errcheck=espeak_errcheck @@ -331,7 +332,7 @@ def initialize(indexCallback=None): espeakDLL.espeak_ListVoices.restype=POINTER(POINTER(espeak_VOICE)) espeakDLL.espeak_GetCurrentVoice.restype=POINTER(espeak_VOICE) espeakDLL.espeak_SetVoiceByName.argtypes=(c_char_p,) - eSpeakPath=os.path.abspath("synthDrivers") + eSpeakPath = os.path.join(globalVars.appDir, "synthDrivers") sampleRate = espeakDLL.espeak_Initialize( AUDIO_OUTPUT_SYNCHRONOUS, 300, os.fsencode(eSpeakPath), @@ -371,7 +372,7 @@ def info(): return espeakDLL.espeak_Info() def getVariantDict(): - dir='synthDrivers\\espeak-ng-data\\voices\\!v' + dir = os.path.join(globalVars.appDir, "synthDrivers", "espeak-ng-data", "voices", "!v") # Translators: name of the default espeak varient. variantDict={"none": pgettext("espeakVarient", "none")} for fileName in os.listdir(dir): diff --git a/source/systemUtils.py b/source/systemUtils.py index 672885f7ccf..ae73144c638 100644 --- a/source/systemUtils.py +++ b/source/systemUtils.py @@ -54,7 +54,7 @@ def execElevated(path, params=None, wait=False, handleAlreadyElevated=False): import subprocess if params is not None: params = subprocess.list2cmdline(params) - sei = shellapi.SHELLEXECUTEINFO(lpFile=os.path.abspath(path), lpParameters=params, nShow=winUser.SW_HIDE) + sei = shellapi.SHELLEXECUTEINFO(lpFile=path, lpParameters=params, nShow=winUser.SW_HIDE) # IsUserAnAdmin is apparently deprecated so may not work above Windows 8 if not handleAlreadyElevated or not ctypes.windll.shell32.IsUserAnAdmin(): sei.lpVerb = "runas" diff --git a/source/ui.py b/source/ui.py index af6b7a80d99..48661e1231e 100644 --- a/source/ui.py +++ b/source/ui.py @@ -18,6 +18,7 @@ import gui import speech import braille +import globalVars from typing import Optional @@ -45,7 +46,7 @@ def browseableMessage(message,title=None,isHtml=False): @param isHtml: Whether the message is html @type isHtml: boolean """ - htmlFileName = os.path.realpath( u'message.html' ) + htmlFileName = os.path.join(globalVars.appDir, 'message.html') if not os.path.isfile(htmlFileName ): raise LookupError(htmlFileName ) moniker = POINTER(IUnknown)() diff --git a/source/updateCheck.py b/source/updateCheck.py index d66a993546a..cb688ca172b 100644 --- a/source/updateCheck.py +++ b/source/updateCheck.py @@ -204,11 +204,11 @@ def _executeUpdate(destPath): if config.isInstalledCopy(): executeParams = u"--install -m" else: - portablePath = os.getcwd() + portablePath = globalVars.appDir if os.access(portablePath, os.W_OK): executeParams = u'--create-portable --portable-path "{portablePath}" --config-path "{configPath}" -m'.format( portablePath=portablePath, - configPath=os.path.abspath(globalVars.appArgs.configPath) + configPath=globalVars.appArgs.configPath ) else: executeParams = u"--launcher" diff --git a/source/visionEnhancementProviders/screenCurtain.py b/source/visionEnhancementProviders/screenCurtain.py index ab77249ac68..e60a9cc3941 100644 --- a/source/visionEnhancementProviders/screenCurtain.py +++ b/source/visionEnhancementProviders/screenCurtain.py @@ -7,6 +7,7 @@ This implementation only works on Windows 8 and above. """ +import os import vision from vision import providerBase import winVersion @@ -19,6 +20,7 @@ from logHandler import log from typing import Optional, Type import nvwave +import globalVars class MAGCOLOREFFECT(Structure): @@ -325,7 +327,7 @@ def __init__(self): raise e if self.getSettings().playToggleSounds: try: - nvwave.playWaveFile(r"waves\screenCurtainOn.wav") + nvwave.playWaveFile(os.path.join(globalVars.appDir, "waves", "screenCurtainOn.wav")) except Exception: log.exception() @@ -338,7 +340,7 @@ def terminate(self): Magnification.MagUninitialize() if self.getSettings().playToggleSounds: try: - nvwave.playWaveFile(r"waves\screenCurtainOff.wav") + nvwave.playWaveFile(os.path.join(globalVars.appDir, "waves", "screenCurtainOff.wav")) except Exception: log.exception() diff --git a/source/watchdog.py b/source/watchdog.py index 6a8f1ff9588..7269ec9a953 100644 --- a/source/watchdog.py +++ b/source/watchdog.py @@ -187,7 +187,7 @@ def _crashHandler(exceptionInfo): ctypes.pythonapi.PyThreadState_SetAsyncExc(threadId, None) # Write a minidump. - dumpPath = os.path.abspath(os.path.join(globalVars.appArgs.logFileName, "..", "nvda_crash.dmp")) + dumpPath = os.path.join(globalVars.appArgs.logFileName, "..", "nvda_crash.dmp") try: # Though we aren't using pythonic functions to write to the dump file, # open it in binary mode as opening it in text mode (the default) doesn't make sense. diff --git a/source/winKernel.py b/source/winKernel.py index 5dfda1880af..8645d295103 100644 --- a/source/winKernel.py +++ b/source/winKernel.py @@ -48,6 +48,9 @@ WAIT_FAILED = 0xffffffff # Image file machine constants IMAGE_FILE_MACHINE_UNKNOWN = 0 +# LoadLibraryEx constants +LOAD_WITH_ALTERED_SEARCH_PATH = 0x8 + def GetStdHandle(handleID): h=kernel32.GetStdHandle(handleID) diff --git a/tests/unit/__init__.py b/tests/unit/__init__.py index ab935110978..4e4d26c05b4 100644 --- a/tests/unit/__init__.py +++ b/tests/unit/__init__.py @@ -35,8 +35,13 @@ # as this module is imported to expand the system path. import sourceEnv # noqa: F401 -# Set options normally taken from the command line. import globalVars + + +# Tell NvDA where its application directory is +globalVars.appDir = SOURCE_DIR + +# Set options normally taken from the command line. class AppArgs: # The path from which to load a configuration file. # Ideally, this would be an in-memory, default configuration. diff --git a/user_docs/en/changes.t2t b/user_docs/en/changes.t2t index fa0141223bc..26ea1c721b5 100644 --- a/user_docs/en/changes.t2t +++ b/user_docs/en/changes.t2t @@ -29,10 +29,12 @@ What's New in NVDA - When "attempt to cancel expired focus events" is enabled, the title of the tab is now announced again when switching tabs in Firefox. (#11397) - NVDA no longer fails to announce a list item after typing a character in a list when speaking with the SAPI5 Ivona voices. (#11651) - It is again possible to use browse mode when reading emails in Windows 10 Mail 16005.13110 and later. (#11439) +- When using the SAPI5 Ivona voices from harposoftware.com, NvDA is now able to save configuration, switch synthesizers, and no longer will stay silent after restarting. (#11650) == Changes for Developers == - System tests can now send keys using spy.emulateKeyPress, which takes a key identifier that conforms to NVDA's own key names, and by default also blocks until the action is executed. (#11581) +- NVDA no longer requires the current directory to be the NVDA application directory in order to function. (#6491) = 2020.3 =