From ac735c6968ef1c71d13e08cec6bcb06ad40034fa Mon Sep 17 00:00:00 2001 From: Michael Curran Date: Wed, 9 May 2012 19:25:46 +1000 Subject: [PATCH] When installing NVDA or creating a portable copy, detect when a file canot be removed or overwritten and allow the user to retry or cancel the over all operation. Also stop accidentily copying userConfig and or systemConfig when creating a portable copy or installing. (This may significantly speed up portable creation if the system config happened to be large). --- source/gui/installerGui.py | 18 ++++++- source/installer.py | 105 ++++++++++++++++++------------------- source/logHandler.py | 5 +- source/nvda_slave.pyw | 8 ++- source/winUser.py | 5 ++ 5 files changed, 84 insertions(+), 57 deletions(-) diff --git a/source/gui/installerGui.py b/source/gui/installerGui.py index 67c6b6c8e8c..9ca445e60de 100644 --- a/source/gui/installerGui.py +++ b/source/gui/installerGui.py @@ -7,6 +7,7 @@ import os import ctypes import shellapi +import winUser import wx import config import globalVars @@ -28,6 +29,7 @@ def doInstall(createDesktopShortcut,startOnLogon,copyPortableConfig,isUpdate,sil else _("Please wait while NVDA is being installed")) try: res=config.execElevated(config.SLAVE_FILENAME,["install",str(int(createDesktopShortcut)),str(int(startOnLogon))],wait=True,handleAlreadyElevated=True) + if res==2: raise installer.RetriableFailier if copyPortableConfig: installedUserConfigPath=config.getInstalledUserConfigPath() if installedUserConfigPath: @@ -37,6 +39,13 @@ def doInstall(createDesktopShortcut,startOnLogon,copyPortableConfig,isUpdate,sil log.error("Failed to execute installer",exc_info=True) progressDialog.done() del progressDialog + if isinstance(res,installer.RetriableFailier): + # Translators: a message dialog asking to retry or cancel when NVDA install fails + message=_("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.") + # Translators: the title of a retry cancel dialog when NVDA installation fails + title=_("File in Use") + if winUser.MessageBox(None,message,title,winUser.MB_RETRYCANCEL)==winUser.IDRETRY: + return doInstall(createDesktopShortcut,startOnLogon,copyPortableConfig,isUpdate,silent) if res!=0: log.error("Installation failed: %s"%res) # Translators: The message displayed when an error occurs during installation of NVDA. @@ -203,10 +212,17 @@ def doCreatePortable(portableDirectory,createAutorun=False,copyUserConfig=False) # Translators: The message displayed while a portable copy of NVDA is bieng created. _("Please wait while a portable copy of NVDA is created.")) try: - installer.CreatePortableCopy(portableDirectory,copyUserConfig=copyUserConfig,createAutorun=createAutorun) + gui.ExecAndPump(installer.createPortableCopy,portableDirectory,copyUserConfig,createAutorun) except Exception as e: log.error("Failed to create portable copy",exc_info=True) d.done() + if isinstance(e,installer.RetriableFailier): + # Translators: a message dialog asking to retry or cancel when NVDA portable copy creation fails + message=_("NVDA is unable to remove or overwrite a file.") + # Translators: the title of a retry cancel dialog when NVDA portable copy creation fails + title=_("File in Use") + if winUser.MessageBox(None,message,title,winUser.MB_RETRYCANCEL)==winUser.IDRETRY: + return doCreatePortable(portableDirectory,createAutorun,copyUserConfig) # Translators: The message displayed when an error occurs while creating a portable copy of NVDA. # %s will be replaced with the specific error message. gui.messageBox(_("Failed to create portable copy: %s")%e, diff --git a/source/installer.py b/source/installer.py index d601fb39e4b..b090b18029a 100644 --- a/source/installer.py +++ b/source/installer.py @@ -4,7 +4,6 @@ #See the file COPYING for more details. #Copyright (C) 2011-2012 NV Access Limited -from ctypes import * from ctypes import * from ctypes.wintypes import * import _winreg @@ -38,7 +37,6 @@ def createShortcut(path,targetPath=None,arguments=None,iconLocation=None,working if prependSpecialFolder: specialPath=wsh.SpecialFolders(prependSpecialFolder) path=os.path.join(specialPath,path) - log.info("Creating shortcut at %s"%path) if not os.path.isdir(os.path.dirname(path)): os.makedirs(os.path.dirname(path)) short=wsh.CreateShortcut(path) @@ -98,9 +96,7 @@ def copyProgramFiles(destPath): for curSourceDir,subDirs,files in os.walk(sourcePath): if detectUserConfig: detectUserConfig=False - if os.path.split(curSourceDir)[1].lower()=="userconfig": - del subDirs[:] - continue + subDirs[:]=[x for x in subDirs if os.path.basename(x).lower() not in ('userconfig','systemconfig')] curDestDir=os.path.join(destPath,os.path.relpath(curSourceDir,sourcePath)) if not os.path.isdir(curDestDir): os.makedirs(curDestDir) @@ -113,7 +109,10 @@ def copyProgramFiles(destPath): if windll.kernel32.CopyFileW(u"\\\\?\\"+sourceFilePath,u"\\\\?\\"+destFilePath,False)==0: log.debugWarning("Unable to copy %s, trying rename and delete on reboot"%sourceFilePath) tempPath=tempfile.mktemp(dir=os.path.dirname(destFilePath)) - os.rename(destFilePath,tempPath) + try: + os.rename(destFilePath,tempPath) + except (WindowsError,OSError): + raise RetriableFailier("Failed to rename %s after failed remove"%destFilePath) if windll.kernel32.MoveFileExW(u"\\\\?\\"+tempPath,None,4)==0: raise OSError("Unable to mark file %s for delete on reboot"%tempPath) if windll.kernel32.CopyFileW(u"\\\\?\\"+sourceFilePath,u"\\\\?\\"+destFilePath,False)==0: @@ -131,7 +130,10 @@ def copyUserConfig(destPath): if windll.kernel32.CopyFileW(u"\\\\?\\"+sourceFilePath,u"\\\\?\\"+destFilePath,False)==0: log.debugWarning("Unable to copy %s, trying rename and delete on reboot"%sourceFilePath) tempPath=tempfile.mktemp(dir=os.path.dirname(destFilePath)) - os.rename(destFilePath,tempPath) + try: + os.rename(destFilePath,tempPath) + except (WindowsError,OSError): + raise RetriableFailier("Failed to rename %s after failed remove"%destFilePath) if windll.kernel32.MoveFileExW(u"\\\\?\\"+tempPath,None,4)==0: raise OSError("Unable to mark file %s for delete on reboot"%tempPath) if windll.kernel32.CopyFileW(u"\\\\?\\"+sourceFilePath,u"\\\\?\\"+destFilePath,False)==0: @@ -209,18 +211,35 @@ def unregisterInstallation(forUpdate=False): except WindowsError: pass +class RetriableFailier(Exception): + pass + +MB_RETRYCANCEL=0x5 +MB_SYSTEMMODAL=0x1000 +IDRETRY=4 +IDCANCEL=3 +def tryRemoveFile(path,numRetries=6,retryInterval=0.5): + for count in xrange(numRetries): + try: + os.remove(path) + return + except OSError: + pass + time.sleep(retryInterval) + raise RetriableFailier("File %s could not be removed"%path) + def install(shouldCreateDesktopShortcut=True,shouldRunAtLogon=True): prevInstallPath=getInstallPath(noDefault=True) unregisterInstallation() - if prevInstallPath: - removeOldLoggedFiles(prevInstallPath) installDir=defaultInstallPath startMenuFolder=defaultStartMenuFolder #Remove all the main executables always for f in ("nvda.exe","nvda_noUIAccess.exe","nvda_UIAccess.exe"): f=os.path.join(installDir,f) if os.path.isfile(f): - os.remove(f) + tryRemoveFile(f) + if prevInstallPath: + removeOldLoggedFiles(prevInstallPath) copyProgramFiles(installDir) for f in ("nvda_UIAccess.exe","nvda_noUIAccess.exe"): f=os.path.join(installDir,f) @@ -239,9 +258,9 @@ def removeOldLoggedFiles(installPath): with open(datPath,"r") as datFile: datFile.readline() lines=datFile.readlines() - lines.append(os.path.join(installPath,'uninstall.dat')) lines.append(os.path.join(installPath,'uninstall.exe')) lines.sort(reverse=True) + lines.append(os.path.join(installPath,'uninstall.dat')) for line in lines: filePath=line.rstrip('\n') try: @@ -252,7 +271,10 @@ def removeOldLoggedFiles(installPath): except WindowsError: log.debugWarning("Failed to remove %s, removing on reboot"%filePath) tempPath=tempfile.mktemp(dir=installPath) - os.rename(filePath,tempPath) + try: + os.rename(filePath,tempPath) + except (WindowsError,IOError): + raise RetriableFailier("Failed to rename file %s after failed remove"%filePath) if windll.kernel32.MoveFileExA("\\\\?\\"+tempPath,None,4)==0: raise OSError("Unable to mark file %s for delete on reboot"%tempPath) @@ -262,44 +284,21 @@ def removeOldLoggedFiles(installPath): icon={icon} """ -class CreatePortableCopy(threading.Thread): - - def __init__(self,destPath,copyUserConfig=True,createAutorun=False): - super(CreatePortableCopy,self).__init__() - self.destPath=destPath - self.copyUserConfig=copyUserConfig - self.createAutorun=createAutorun - self.threadExc=None - self.start() - time.sleep(0.1) - threadHandle=c_int() - threadHandle.value=windll.kernel32.OpenThread(0x100000,False,self.ident) - msg=MSG() - while windll.user32.MsgWaitForMultipleObjects(1,byref(threadHandle),False,-1,255)==1: - while windll.user32.PeekMessageW(byref(msg),None,0,0,1): - windll.user32.TranslateMessage(byref(msg)) - windll.user32.DispatchMessageW(byref(msg)) - if self.threadExc: - raise self.threadExc - - def run(self,*args,**kwargs): - try: - destPath=os.path.abspath(self.destPath) - #Remove all the main executables always - for f in ("nvda.exe","nvda_noUIAccess.exe","nvda_UIAccess.exe"): - f=os.path.join(destPath,f) - if os.path.isfile(f): - os.remove(f) - copyProgramFiles(destPath) - if windll.kernel32.CopyFileW(u"\\\\?\\"+os.path.join(destPath,"nvda_noUIAccess.exe"),u"\\\\?\\"+os.path.join(destPath,"nvda.exe"),False)==0: - raise OSError("Error copying %s to nvda.exe"%f) - if self.copyUserConfig: - copyUserConfig(os.path.join(destPath,'userConfig')) - if self.createAutorun: - drive,relDestPath=os.path.splitdrive(destPath) - autorunPath=os.path.join(drive,"autorun.inf") - autorunString=autorunTemplate.format(exe=os.path.join(relDestPath,'nvda.exe'),name=versionInfo.name,version=versionInfo.version,icon=os.path.join(relDestPath,'images/nvda.ico')) - with open(autorunPath,"wt") as autorunFile: - autorunFile.write(autorunString) - except Exception as e: - self.threadExc=e +def createPortableCopy(destPath,shouldCopyUserConfig=True,shouldCreateAutorun=False): + destPath=os.path.abspath(destPath) + #Remove all the main executables always + for f in ("nvda.exe","nvda_noUIAccess.exe","nvda_UIAccess.exe"): + f=os.path.join(destPath,f) + if os.path.isfile(f): + tryRemoveFile(f) + copyProgramFiles(destPath) + if windll.kernel32.CopyFileW(u"\\\\?\\"+os.path.join(destPath,"nvda_noUIAccess.exe"),u"\\\\?\\"+os.path.join(destPath,"nvda.exe"),False)==0: + raise OSError("Error copying %s to nvda.exe"%f) + if shouldCopyUserConfig: + copyUserConfig(os.path.join(destPath,'userConfig')) + if shouldCreateAutorun: + drive,relDestPath=os.path.splitdrive(destPath) + autorunPath=os.path.join(drive,"autorun.inf") + autorunString=autorunTemplate.format(exe=os.path.join(relDestPath,'nvda.exe'),name=versionInfo.name,version=versionInfo.version,icon=os.path.join(relDestPath,'images/nvda.ico')) + with open(autorunPath,"wt") as autorunFile: + autorunFile.write(autorunString) diff --git a/source/logHandler.py b/source/logHandler.py index b362a8f9b0e..439d373c32f 100755 --- a/source/logHandler.py +++ b/source/logHandler.py @@ -184,7 +184,10 @@ def __init__(self): def emit(self, record): msg = self.format(record) if self._remoteLib: - self._remoteLib.nvdaControllerInternal_logMessage(record.levelno, ctypes.windll.kernel32.GetCurrentProcessId(), msg) + try: + self._remoteLib.nvdaControllerInternal_logMessage(record.levelno, ctypes.windll.kernel32.GetCurrentProcessId(), msg) + except WindowsError: + pass class FileHandler(logging.StreamHandler): diff --git a/source/nvda_slave.pyw b/source/nvda_slave.pyw index d2936a5ca9e..e27d833842f 100755 --- a/source/nvda_slave.pyw +++ b/source/nvda_slave.pyw @@ -4,6 +4,7 @@ Performs miscellaneous tasks which need to be performed in a separate process. import sys import os +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 @@ -21,7 +22,11 @@ def main(): nvda_service.nvdaLauncher() elif action=="install": import installer - installer.install(bool(int(args[0])),bool(int(args[1]))) + try: + installer.install(bool(int(args[0])),bool(int(args[1]))) + except installer.RetriableFailier: + logHandler.log.error("Installation failed, try again",exc_info=True) + sys.exit(2) elif action=="unregisterInstall": import installer installer.unregisterInstallation() @@ -69,7 +74,6 @@ def main(): sys.exit(1) if __name__ == "__main__": - import logHandler logHandler.initialize(True) logHandler.log.setLevel(0) import languageHandler diff --git a/source/winUser.py b/source/winUser.py index 3617cd13fcb..e826991e87f 100644 --- a/source/winUser.py +++ b/source/winUser.py @@ -474,6 +474,11 @@ def FindWindow(className, windowName): raise WinError() return res +MB_RETRYCANCEL=5 +MB_SYSTEMMODAL=0x1000 +IDRETRY=4 +IDCANCEL=3 + def MessageBox(hwnd, text, caption, type): res = user32.MessageBoxW(hwnd, text, caption, type) if res == 0: