diff --git a/appveyor.yml b/appveyor.yml index 2f18d087384..43d30bea124 100644 --- a/appveyor.yml +++ b/appveyor.yml @@ -57,7 +57,7 @@ install: build_script: - ps: | - $sconsOutTargets = "launcher" + $sconsOutTargets = "launcher appx" $sconsArgs = "version=$env:version" if ($env:release) { $sconsOutTargets += " changes userGuide developerGuide" diff --git a/appx/appx_images/nvda_150x150.png b/appx/appx_images/nvda_150x150.png new file mode 100644 index 00000000000..90ef87f73a3 Binary files /dev/null and b/appx/appx_images/nvda_150x150.png differ diff --git a/appx/appx_images/nvda_44x44.png b/appx/appx_images/nvda_44x44.png new file mode 100644 index 00000000000..b6e5670e8f0 Binary files /dev/null and b/appx/appx_images/nvda_44x44.png differ diff --git a/appx/manifest.xml.subst b/appx/manifest.xml.subst new file mode 100644 index 00000000000..2bb859eebdc --- /dev/null +++ b/appx/manifest.xml.subst @@ -0,0 +1,47 @@ + + + + + %productName% + %publisher% + %description% + appx_images/nvda_44x44.png + + + + + + + + + + + + + + + + diff --git a/appx/sconscript b/appx/sconscript new file mode 100644 index 00000000000..d1b1c87cb41 --- /dev/null +++ b/appx/sconscript @@ -0,0 +1,97 @@ +import subprocess +import versionInfo + +Import([ + 'env', + 'outFilePrefix', + 'isStoreSubmission', +]) + +def getCertPublisher(env): + """ + If no signing certificate is provided, then the given publisher is used as is. + If a signing certificate is given, then the publisher is extracted from the certificate. + """ + certFile=env.get('certFile') + if not certFile: + return env['publisher'] + certPassword=env.get('certPassword','') + cmd=['certutil','-dump','-p',certPassword,File('#'+certFile).abspath.replace('/','\\')] + lines=subprocess.check_output(cmd).splitlines() + linePrefix='Subject: ' + for line in lines: + if line.startswith(linePrefix): + subject=line[len(linePrefix):].rstrip() + return subject + +packageName="NVAccessLimited.NVDANonVisualDesktopAccess" +packageVersion="%s.%s.%s.%s"%(versionInfo.version_year,versionInfo.version_major,env['version_build'],0) +if isStoreSubmission: + packageFileName=outFilePrefix+"_storeSubmission.appx" + # NV Access Limited's Windows Store publisher ID + # It is okay to be here as the only way to submit, validate and sign the package is via the NV Access store account. + packagePublisher="CN=83B1DA31-9B66-442C-88AB-77B4B815E1DE" + packagePublisherDisplayName="NV Access Limited" + productName="NVDA Screen Reader (Windows Store Edition)" +else: # not for submission, just side-loadable + packageFileName=outFilePrefix+"_sideLoadable.appx" + packagePublisher=getCertPublisher(env) + packagePublisherDisplayName=env['publisher'] + productName="NVDA Screen Reader (Windows Desktop Bridge Edition)" + +signExec=env['signExec'] if env['certFile'] else None + +# Files from NVDA's distribution that cannot be included in the appx due to policy or security restrictions +excludedDistFiles=[ + 'nvda_eoaProxy.exe', + 'nvda_service.exe', + 'nvda_slave.exe', + 'nvda_uiAccess.exe', + 'lib/IAccessible2Proxy.dll', + 'lib/ISimpleDOM.dll', + 'lib/minHook.dll', + 'lib/NVDAHelperRemote.dll', + 'lib/VBufBackend_adobeAcrobat.dll', + 'lib/VBufBackend_adobeFlash.dll', + 'lib/VBufBackend_gecko_ia2.dll', + 'lib/VBufBackend_lotusNotesRichText.dll', + 'lib/VBufBackend_mshtml.dll', + 'lib/VBufBackend_webKit.dll', + 'lib64/', + 'uninstall.exe', +] + +# Create an appx manifest with version and publisher etc all filled in +manifest=env.Substfile( + "AppxManifest.xml", + 'manifest.xml.subst', + SUBST_DICT={ + '%packageName%':packageName, + '%packageVersion%':packageVersion, + '%packagePublisher%':packagePublisher, + '%publisher%':packagePublisherDisplayName, + '%productName%':productName, + '%description%':versionInfo.description, + }, +) +# Make a copy of the dist dir produced by py2exe +# And also place some extra appx specific images in there +appxContent=env.Command( + target='content', + source=[Dir("#dist"),Dir('#appx/appx_images'),manifest], + action=[ + Delete("$TARGET"), + Copy("$TARGET","${SOURCES[0]}"), + Copy("${TARGET}\\appx_images","${SOURCES[1]}"), + Copy("${TARGET}\\AppxManifest.xml","${SOURCES[2]}"), + ]+[Delete("${TARGET}/%s"%excludeFile) for excludeFile in excludedDistFiles], +) +# Ensure that it is always copied as we can't tell if dist changed +env.AlwaysBuild(appxContent) +# Package the appx +appx=env.Command(packageFileName,appxContent,"makeappx pack /p $TARGET /d $SOURCE") +if signExec and not isStoreSubmission: + env.AddPostAction(appx,signExec) + +Return(['appx']) + diff --git a/sconstruct b/sconstruct index b1d76fc3e9f..c4b81edf258 100755 --- a/sconstruct +++ b/sconstruct @@ -118,11 +118,14 @@ sourceLibDir64=sourceDir.Dir('lib64') Export('sourceLibDir64') buildDir = Dir("build") outFilePrefix = "nvda{type}_{version}".format(type="" if release else "_snapshot", version=version) +Export('outFilePrefix') outputDir=Dir(env['outputDir']) +Export('outputDir') devDocsOutputDir=outputDir.Dir('devDocs') # An action to sign an executable with certFile. -signExecCmd = ["signtool", "sign", "/f", certFile] +# we encrypt with SHA256 as this is the minimum required by the Windows Store for appx packages +signExecCmd = ["signtool", "sign", "/fd", "SHA256", "/f", certFile] if certPassword: signExecCmd.extend(("/p", certPassword)) if certTimestampServer: @@ -389,6 +392,12 @@ symbolsList.extend(env.Glob(os.path.join(sourceLibDir64.path,'*.pdb'))) symbolsArchive = env.ZipArchive(outputDir.File("%s_debugSymbols.zip" % outFilePrefix), symbolsList) env.Alias("symbolsArchive", symbolsArchive) +appx_storeSubmission=env.SConscript("appx/sconscript",exports={'env':env,'isStoreSubmission':True},variant_dir='build\\appx_storeSubmission') +installed_appx_storeSubmission=env.Install('output',appx_storeSubmission) +appx_sideLoadable=env.SConscript("appx/sconscript",exports={'env':env,'isStoreSubmission':False},variant_dir='build\\appx_sideLoadable') +installed_appx_sideLoadable=env.Install('output',appx_sideLoadable) +env.Alias('appx',[installed_appx_storeSubmission,installed_appx_sideLoadable]) + env.Default(dist) env.SConscript("tests/sconscript", exports=["env", "sourceDir", "pot"]) diff --git a/source/NVDAHelper.py b/source/NVDAHelper.py index 140fae058fa..84e3f63fe5a 100755 --- a/source/NVDAHelper.py +++ b/source/NVDAHelper.py @@ -452,10 +452,14 @@ def initialize(): generateBeep=localLib.generateBeep generateBeep.argtypes=[c_char_p,c_float,c_int,c_int,c_int] generateBeep.restype=c_int + # The rest of this function (to do with injection only applies if NVDA is not running as a Windows store application) # Handle VBuf_getTextInRange's BSTR out parameter so that the BSTR will be freed automatically. VBuf_getTextInRange = CFUNCTYPE(c_int, c_int, c_int, c_int, POINTER(BSTR), c_int)( ("VBuf_getTextInRange", localLib), ((1,), (1,), (1,), (2,), (1,))) + 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) if not h: @@ -473,14 +477,15 @@ def initialize(): def terminate(): global _remoteLib, _remoteLoader64, localLib, generateBeep, VBuf_getTextInRange - if not _remoteLib.uninstallIA2Support(): - log.debugWarning("Error uninstalling IA2 support") - if _remoteLib.injection_terminate() == 0: - raise RuntimeError("Error terminating NVDAHelperRemote") - _remoteLib=None - if _remoteLoader64: - _remoteLoader64.terminate() - _remoteLoader64=None + if not config.isAppX: + if not _remoteLib.uninstallIA2Support(): + log.debugWarning("Error uninstalling IA2 support") + if _remoteLib.injection_terminate() == 0: + raise RuntimeError("Error terminating NVDAHelperRemote") + _remoteLib=None + if _remoteLoader64: + _remoteLoader64.terminate() + _remoteLoader64=None generateBeep=None VBuf_getTextInRange=None localLib.nvdaHelperLocal_terminate() diff --git a/source/addonHandler.py b/source/addonHandler.py index 7561cfa5100..84ba1e42c4e 100644 --- a/source/addonHandler.py +++ b/source/addonHandler.py @@ -119,6 +119,9 @@ def disableAddonsIfAny(): def initialize(): """ Initializes the add-ons subsystem. """ + if config.isAppX: + log.info("Add-ons not supported when running as a Windows Store application") + return loadState() removeFailedDeletions() completePendingAddonRemoves() diff --git a/source/config/__init__.py b/source/config/__init__.py index 18e8d70d75f..df4764e9563 100644 --- a/source/config/__init__.py +++ b/source/config/__init__.py @@ -31,6 +31,9 @@ import profileUpgrader from .configSpec import confspec +#: True if NVDA is running as a Windows Store Desktop Bridge application +isAppX=False + #: The active configuration, C{None} if it has not yet been loaded. #: @type: ConfigObj conf = None @@ -98,7 +101,14 @@ def getUserDefaultConfigPath(useInstalledPathIfExists=False): Most callers will want the C{globalVars.appArgs.configPath variable} instead. """ installedUserConfigPath=getInstalledUserConfigPath() - if installedUserConfigPath and (isInstalledCopy() or (useInstalledPathIfExists and os.path.isdir(installedUserConfigPath))): + if installedUserConfigPath and (isInstalledCopy() or isAppX or (useInstalledPathIfExists and os.path.isdir(installedUserConfigPath))): + if isAppX: + # NVDA is running as a Windows Store application. + # Although Windows will redirect %APPDATA% to a user directory specific to the Windows Store application, + # It also makes existing %APPDATA% files available here. + # We cannot share NVDA user config directories with other copies of NVDA as their config may be using add-ons + # Therefore add a suffix to the directory to make it specific to Windows Store application versions. + installedUserConfigPath+='_appx' return installedUserConfigPath return u'.\\userConfig\\' @@ -120,7 +130,10 @@ def initConfigPath(configPath=None): configPath=globalVars.appArgs.configPath if not os.path.isdir(configPath): os.makedirs(configPath) - for subdir in ("addons", "appModules","brailleDisplayDrivers","speechDicts","synthDrivers","globalPlugins","profiles"): + subdirs=["speechDicts","profiles"] + if not isAppX: + subdirs.extend(["addons", "appModules","brailleDisplayDrivers","synthDrivers","globalPlugins"]) + for subdir in subdirs: subdir=os.path.join(configPath,subdir) if not os.path.isdir(subdir): os.makedirs(subdir) @@ -272,7 +285,7 @@ def addConfigDirsToPythonPackagePath(module, subdir=None): @param subdir: The subdirectory to be used, C{None} for the name of C{module}. @type subdir: str """ - if globalVars.appArgs.disableAddons: + if isAppX or globalVars.appArgs.disableAddons: return if not subdir: subdir = module.__name__ diff --git a/source/core.py b/source/core.py index 6d1119c372a..bccca6bd66f 100644 --- a/source/core.py +++ b/source/core.py @@ -221,8 +221,13 @@ def OnAssert(self,file,line,cond,msg): message="{file}, line {line}:\nassert {cond}: {msg}".format(file=file,line=line,cond=cond,msg=msg) log.debugWarning(message,codepath="WX Widgets",stack_info=True) app = App(redirect=False) - # We do support QueryEndSession events, but we don't want to do anything for them. - app.Bind(wx.EVT_QUERY_END_SESSION, lambda evt: None) + # We support queryEndSession events, but in general don't do anything for them. + # However, when running as a Windows Store application, we do want to request to be restarted for updates + def onQueryEndSession(evt): + if config.isAppX: + # Automatically restart NVDA on Windows Store update + ctypes.windll.kernel32.RegisterApplicationRestart(None,0) + app.Bind(wx.EVT_QUERY_END_SESSION, onQueryEndSession) def onEndSession(evt): # NVDA will be terminated as soon as this function returns, so save configuration if appropriate. config.saveOnExit() diff --git a/source/globalCommands.py b/source/globalCommands.py index 5015a4fa383..fb23904a07f 100755 --- a/source/globalCommands.py +++ b/source/globalCommands.py @@ -1655,7 +1655,7 @@ def script_revertConfiguration(self,gesture): script_revertConfiguration.category=SCRCAT_CONFIG def script_activatePythonConsole(self,gesture): - if globalVars.appArgs.secure: + if globalVars.appArgs.secure or config.isAppX: return import pythonConsole if not pythonConsole.consoleUI: diff --git a/source/gui/__init__.py b/source/gui/__init__.py index 2444a42b5e4..cee57eb306b 100644 --- a/source/gui/__init__.py +++ b/source/gui/__init__.py @@ -405,14 +405,14 @@ def __init__(self, frame): # Translators: The label for the menu item to toggle Speech Viewer. item=self.menu_tools_toggleSpeechViewer = menu_tools.AppendCheckItem(wx.ID_ANY, _("Speech viewer")) self.Bind(wx.EVT_MENU, frame.onToggleSpeechViewerCommand, item) - if not globalVars.appArgs.secure: + if not globalVars.appArgs.secure and not config.isAppX: # Translators: The label for the menu item to open NVDA Python Console. item = menu_tools.Append(wx.ID_ANY, _("Python console")) self.Bind(wx.EVT_MENU, frame.onPythonConsoleCommand, item) # Translators: The label of a menu item to open the Add-ons Manager. item = menu_tools.Append(wx.ID_ANY, _("Manage &add-ons...")) self.Bind(wx.EVT_MENU, frame.onAddonsManagerCommand, item) - if not globalVars.appArgs.secure and getattr(sys,'frozen',None): + if not globalVars.appArgs.secure and not config.isAppX and getattr(sys,'frozen',None): # Translators: The label for the menu item to create a portable copy of NVDA from an installed or another portable version. item = menu_tools.Append(wx.ID_ANY, _("Create portable copy...")) self.Bind(wx.EVT_MENU, frame.onCreatePortableCopyCommand, item) @@ -420,9 +420,10 @@ def __init__(self, frame): # Translators: The label for the menu item to install NVDA on the computer. item = menu_tools.Append(wx.ID_ANY, _("&Install NVDA...")) self.Bind(wx.EVT_MENU, frame.onInstallCommand, item) - # Translators: The label for the menu item to reload plugins. - item = menu_tools.Append(wx.ID_ANY, _("Reload plugins")) - self.Bind(wx.EVT_MENU, frame.onReloadPluginsCommand, item) + if not config.isAppX: + # Translators: The label for the menu item to reload plugins. + item = menu_tools.Append(wx.ID_ANY, _("Reload plugins")) + self.Bind(wx.EVT_MENU, frame.onReloadPluginsCommand, item) # Translators: The label for the Tools submenu in NVDA menu. self.menu.AppendMenu(wx.ID_ANY, _("Tools"), menu_tools) @@ -625,7 +626,7 @@ def __init__(self, parent): startAfterLogonText = _("&Automatically start NVDA after I log on to Windows") self.startAfterLogonCheckBox = sHelper.addItem(wx.CheckBox(self, label=startAfterLogonText)) self.startAfterLogonCheckBox.Value = config.getStartAfterLogon() - if globalVars.appArgs.secure or not config.isInstalledCopy(): + if globalVars.appArgs.secure or config.isAppX or not config.isInstalledCopy(): self.startAfterLogonCheckBox.Disable() # Translators: The label of a checkbox in the Welcome dialog. showWelcomeDialogAtStartupText = _("&Show this dialog when NVDA starts") diff --git a/source/gui/addonGui.py b/source/gui/addonGui.py index a8b94bd0963..c410ba66e08 100644 --- a/source/gui/addonGui.py +++ b/source/gui/addonGui.py @@ -7,6 +7,7 @@ import os import wx import core +import config import languageHandler import gui from logHandler import log @@ -308,6 +309,14 @@ def __del__(self): @classmethod def handleRemoteAddonInstall(cls, addonPath): + # Add-ons cannot be installed into a Windows store version of NVDA + if config.isAppX: + # Translators: The message displayed when an add-on cannot be installed due to NVDA running as a Windows Store app + gui.messageBox(_("Add-ons cannot be installed in the Windows Store version of NVDA"), + # Translators: The title of a dialog presented when an error occurs. + _("Error"), + wx.OK | wx.ICON_ERROR) + return closeAfter = AddonsDialog._instance is None dialog = AddonsDialog(gui.mainFrame) dialog.installAddon(addonPath, closeAfter=closeAfter) diff --git a/source/nvda.pyw b/source/nvda.pyw index bac619d6bcc..d0d78e87e16 100755 --- a/source/nvda.pyw +++ b/source/nvda.pyw @@ -42,6 +42,18 @@ from logHandler import log import winUser import winKernel +# Find out if NVDA is running as a Windows Store application +bufLen=ctypes.c_int() +try: + GetCurrentPackageFullName=ctypes.windll.kernel32.GetCurrentPackageFullName +except AttributeError: + config.isAppX=False +else: + bufLen=ctypes.c_int() + # Use GetCurrentPackageFullName to detect if we are running as a store app. + # It returns 0 (success) if in a store app, and an error code otherwise. + config.isAppX=(GetCurrentPackageFullName(ctypes.byref(bufLen),None)==0) + class NoConsoleOptionParser(argparse.ArgumentParser): """A commandline option parser that shows its messages using dialogs, as this pyw file has no dos console window associated with it""" @@ -192,7 +204,7 @@ if not ctypes.windll.user32.ChangeWindowMessageFilter(win32con.WM_QUIT,1): raise WinError() # Make this the last application to be shut down and don't display a retry dialog box. winKernel.SetProcessShutdownParameters(0x100, winKernel.SHUTDOWN_NORETRY) -if not isSecureDesktop: +if not isSecureDesktop and not config.isAppX: import easeOfAccess easeOfAccess.notify(3) try: @@ -202,7 +214,7 @@ except: log.critical("core failure",exc_info=True) sys.exit(1) finally: - if not isSecureDesktop: + if not isSecureDesktop and not config.isAppX: easeOfAccess.notify(2) if globalVars.appArgs.changeScreenReaderFlag: winUser.setSystemScreenReaderFlag(False) diff --git a/source/updateCheck.py b/source/updateCheck.py index d77546fe04e..fb1a41ae976 100644 --- a/source/updateCheck.py +++ b/source/updateCheck.py @@ -9,8 +9,11 @@ """ import globalVars +import config if globalVars.appArgs.secure: raise RuntimeError("updates disabled in secure mode") +elif config.isAppX: + raise RuntimeError("updates managed by Windows Store") import versionInfo if not versionInfo.updateVersionType: raise RuntimeError("No update version type, update checking not supported")