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 =