Repository: nvaccess/nvda

Issue:
Failures to update  an installed copy of master branch
### Steps to reproduce:
Get the  prompt to update
do the required steps. Fails as below


Message presented...
The installation is unable to remove or overwrite a file. Another copy of NVDA may be running on another 

logged-on user account. Please make sure all installed copies of NVDA are shut down and try the 

installation again.

Surely the installer of the update should have done this already?
 The important bits of the log.

Speaking [LangChangeCommand ('en_GB'), u'Updating NVDA  dialog  Please wait while your previous 

installation of NVDA is being updated.']
IO - tones.beep (08:55:50.936):
Beep at pitch 440, for 40 ms, left volume 50, right volume 50
IO - speech.speak (08:55:50.941):
Speaking [LangChangeCommand ('en_GB'), u'Please wait']
IO - tones.beep (08:55:50.973):
Beep at pitch 110.0, for 40 ms, left volume 50, right volume 50
IO - speech.speak (08:55:50.982):
Speaking [LangChangeCommand ('en_GB'), u'0 percent']
ERROR - gui.installerGui.doInstall (08:55:51.605):
Failed to execute installer
Traceback (most recent call last):
  File "gui\installerGui.pyc", line 33, in doInstall
RetriableFailure
IO - tones.beep (08:55:51.611):
Beep at pitch 1760, for 40 ms, left volume 50, right volume 50
DEBUGWARNING - RPC process 3716 (nvda_update_iu3byu.exe) (08:55:58.371):
Thread 3636, build\x86\remote\ia2Support.cpp, IA2Support_inProcess_terminate, 152:
WaitForMultipleObjects returned 258

snip of me doing object nav to read the error again..

IO - speech.speak (08:56:29.878):
Speaking [LangChangeCommand ('en_GB'), u'The installation is unable to remove or overwrite a file. 

Another copy of NVDA may be running on another logged-on user account. Please make sure all installed 

copies of NVDA are shut down and try the installation again. copied to clipboard']
DEBUG - queueHandler.pumpAll (08:56:29.894):
generator 3 finished
IO - inputCore.InputManager.executeGesture (08:56:38.003):
Input: kb(desktop):tab
IO - speech.speak (08:56:38.048):
Speaking [LangChangeCommand ('en_GB'), u'Cancel  button']
IO - inputCore.InputManager.executeGesture (08:56:39.122):
Input: kb(desktop):space
IO - speech._speakSpellingGen (08:56:39.141):
Speaking character u'space'
DEBUG - queueHandler.registerGeneratorObject (08:56:39.141):
Adding generator 4
IO - speech.speak (08:56:39.141):
Speaking [LangChangeCommand ('en_GB'), u'pressed']
DEBUG - queueHandler.pumpAll (08:56:39.154):
generator 4 finished
ERROR - gui.installerGui.doInstall (08:56:39.262):
Installation failed: 
IO - speech.speak (08:56:39.325):
Speaking [LangChangeCommand ('en_GB'), u'Error  dialog  The installation of NVDA failed. Please check the 

Log Viewer for more information.']
IO - speech.speak (08:56:39.335):
Speaking [LangChangeCommand ('en_GB'), u'OK  button']

So at this point we exited, rather brutally!

Next any attempt to run the installed copy came up as cannot find the nvda.exe
 The installer was still kind of running but refused to read the desktop so I had to reboot the machine 

with the autoload of nvda off and manually boot in a portable copy. I did this twice since the first time 

left me with no nvda menus desktop and  context menues working and stuck in my users folders tree. It 

then booted and I started the portable copy and reran the installer which I had saved as a portable 

version to a dummy folder and could then use to create the  installed version. this time it worked of 

course as no files in the install folder were in use or locked  which seems to be the main issue when 

this failure occurs. There needs to be some kind of order change in file copying so any issue like this 

does not result in a non working copy of nvda part way through. as to why its happening when it never 

ever used to till recently. I'll leve to the experts!
> A list of the steps you take to demonstrate the problem. 
See above, its easier this way!>
### Expected behavior:
It should proceed with a few seconds of bleeping then reboot the new copy> 
### Actual behavior:
It gives up with an error as in the main fault description above.


### System configuration:
NVDA version:

Cannot tell you which version but it was two older than the latest one to start with.
Now its fixed its...NVDA version master-14476,a826028e

NVDA Installed or portable:
Installed master branch
Other information:

Add ons present in the installed version at the time.
clipspeak, pico, speech palyer in espeak, golden cursor focus highliigh resource monitor extended winamp  dectalk synth goldwave 
All up to date, active synth Espeak with quincy voice.

Windows version:
Windows 7 pro 64 and sp1, completely up to date.

Does the issue still occur after restarting your PC?

Well seeing as its very flakey and actually deletes bits of nvda, obviously not but of course its due to a file in use issue, so eventually no it can be fixed by an install if a different version of nvda launched the installer.



Discussion:

Resolving patch:
diff --git a/source/installer.py b/source/installer.py
--- a/source/installer.py
+++ b/source/installer.py
@@ -135,6 +135,36 @@ def copyUserConfig(destPath):
 			destFilePath=os.path.join(destPath,os.path.relpath(sourceFilePath,sourcePath))
 			tryCopyFile(sourceFilePath,destFilePath)
 
+def removeOldLibFiles(destPath,rebootOK=False):
+	"""
+	Removes library files from previous versions of NVDA.
+	@param destPath: The path where NVDA is installed.
+	@ type destPath: string
+	@param rebootOK: If true then files can be removed on next reboot if trying to do so now fails.
+	@type rebootOK: boolean
+	"""
+	for topDir in ('lib','lib64'):
+		currentLibPath=os.path.join(destPath,topDir,versionInfo.version)
+		for parent,subdirs,files in os.walk(os.path.join(destPath,topDir),topdown=False):
+			if parent==currentLibPath:
+				# Lib dir for current installation. Don't touch this!
+				log.debug("Skipping current install lib path: %r"%parent)
+				continue
+			for d in subdirs:
+				path=os.path.join(parent,d)
+				log.debug("Removing old lib directory: %r"%path)
+				try:
+					os.rmdir(path)
+				except OSError:
+					log.warning("Failed to remove a directory no longer needed. This can be manually removed after a reboot or the  installer will try removing it again next time. Directory: %r"%path)
+			for f in files:
+				path=os.path.join(parent,f)
+				log.debug("Removing old lib file: %r"%path)
+				try:
+					tryRemoveFile(path,numRetries=2,rebootOK=rebootOK)
+				except RetriableFailure:
+					log.warning("A file no longer needed could not be removed. This can be manually removed after a reboot, or  the installer will try again next time. File: %r"%path)
+
 def removeOldProgramFiles(destPath):
 	# #3181: Remove espeak-ng-data\voices except for variants.
 	# Otherwise, there will be duplicates if voices have been moved in this new eSpeak version.
@@ -154,14 +184,6 @@ def removeOldProgramFiles(destPath):
 			else:
 				os.remove(fn)
 
-	# #7546: Remove old version-specific libs
-	for topDir in ('lib','lib64'):
-		for parent,subdirs,files in os.walk(os.path.join(destPath,topDir),topdown=False):
-			for d in subdirs:
-				tryRemoveFile(os.path.join(parent,d),numRetries=1,rebootOK=True)
-			for f in files:
-				tryRemoveFile(os.path.join(parent,f),numRetries=1,rebootOK=True)
-
 	# #4235: mpr.dll is a Windows system dll accidentally included with
 	# earlier versions of NVDA. Its presence causes problems in Windows Vista.
 	fn = os.path.join(destPath, "mpr.dll")
@@ -404,6 +426,7 @@ def install(shouldCreateDesktopShortcut=True,shouldRunAtLogon=True):
 	else:
 		raise RuntimeError("No available executable to use as nvda.exe")
 	registerInstallation(installDir,startMenuFolder,shouldCreateDesktopShortcut,shouldRunAtLogon,configInLocalAppData)
+	removeOldLibFiles(installDir,rebootOK=True)
 
 def removeOldLoggedFiles(installPath):
 	datPath=os.path.join(installPath,"uninstall.dat")
@@ -432,6 +455,7 @@ def createPortableCopy(destPath,shouldCopyUserConfig=True):
 	tryCopyFile(os.path.join(destPath,"nvda_noUIAccess.exe"),os.path.join(destPath,"nvda.exe"))
 	if shouldCopyUserConfig:
 		copyUserConfig(os.path.join(destPath,'userConfig'))
+	removeOldLibFiles(destPath,rebootOK=True)
 
 def registerEaseOfAccess(installDir):
 	with _winreg.CreateKeyEx(_winreg.HKEY_LOCAL_MACHINE, easeOfAccess.APP_KEY_PATH, 0,
diff --git a/source/logHandler.py b/source/logHandler.py
--- a/source/logHandler.py
+++ b/source/logHandler.py
@@ -18,6 +18,7 @@
 import traceback
 from types import MethodType, FunctionType
 import globalVars
+import versionInfo
 
 ERROR_INVALID_WINDOW_HANDLE = 1400
 ERROR_TIMEOUT = 1460
@@ -29,6 +30,7 @@
 EVENT_E_ALL_SUBSCRIBERS_FAILED = -2147220991
 RPC_E_CALL_REJECTED = -2147418111
 RPC_E_DISCONNECTED = -2147417848
+LOAD_WITH_ALTERED_SEARCH_PATH=0x8
 
 def getCodePath(f):
 	"""Using a frame object, gets its module path (relative to the current directory).[className.[funcName]]
@@ -188,17 +190,19 @@ 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
-		h=ctypes.windll.kernel32.LoadLibraryExW(os.path.abspath(ur"lib\nvdaHelperRemote.dll"),0,0x8)
-		self._remoteLib=ctypes.WinDLL("nvdaHelperRemote",handle=h) if h else None
+		path=os.path.abspath(os.path.join(u"lib",versionInfo.version,u"nvdaHelperRemote.dll"))
+		h=ctypes.windll.kernel32.LoadLibraryExW(path,0,LOAD_WITH_ALTERED_SEARCH_PATH)
+		if not h:
+			raise OSError("Could not load %s"%path) 
+		self._remoteLib=ctypes.WinDLL("nvdaHelperRemote",handle=h)
 		logging.Handler.__init__(self)
 
 	def emit(self, record):
 		msg = self.format(record)
-		if self._remoteLib:
-			try:
-				self._remoteLib.nvdaControllerInternal_logMessage(record.levelno, ctypes.windll.kernel32.GetCurrentProcessId(), msg)
-			except WindowsError:
-				pass
+		try:
+			self._remoteLib.nvdaControllerInternal_logMessage(record.levelno, ctypes.windll.kernel32.GetCurrentProcessId(), msg)
+		except WindowsError:
+			pass
 
 class FileHandler(logging.StreamHandler):
 


Test patch:
diff --git a/tests/unit/__init__.py b/tests/unit/__init__.py
--- a/tests/unit/__init__.py
+++ b/tests/unit/__init__.py
@@ -17,6 +17,12 @@
 import os
 import sys
 
+import locale
+import gettext
+#Localization settings
+locale.setlocale(locale.LC_ALL,'')
+gettext.install('nvda',unicode=True)
+
 # The path to the unit tests.
 UNIT_DIR = os.path.dirname(os.path.abspath(__file__))
 # The path to the top of the repo.