Repository: nvaccess/nvda

Issue:
Windows Store/immersive apps: obtain product name and version via package functions if possible
Hi,

Based on #7894 work:

### Is your feature request related to a problem? Please describe.
App module class's product name and version descriptors are used to obtain name and version of the app associated with the app module class. However, there are cases when this does not work, particularly in regards to Windows Store apps (sometimes called "immersive apps"). For these, a container process is used, which causes file utils to say "invalid file version info".

The best known case is Windows 10 Calculator. Although the executable is unique (not wwahost.exe), product name and version cannot be fetched. Another example is Office 365 apps from Microsoft Store that not only presents a high-level version information (because these are converted desktop apps), but also comes with internal versions as seen through Store app package information (for example, 16.0 for desktop apps versus 16051 for Store apps).

### Describe the solution you'd like
Detect and retrieve product name and version for immersive apps via package info functions from kernel32.dll (see #7894 for mechanics of this). If the app is not a hosted (immersive) app (such as on Windows 7 and many apps on Windows 8.x and 10), fall back to version info lookup.

### Describe alternatives you've considered
None - leave it as is.

### Additional context
Mostly useful for app module writers as they can easily detect versions for hosted apps in order to write workarounds. A proof of concept implementation is under active testing via Windows 10 App Essentials add-on.

Thanks.


Discussion:
Hi,

Another case: Notepad in 20H1 build 18963 which is now a converted desktop app and will be updated via Microsoft Store.

Thanks.
Hi,

After thinking about it, I am going to attempt the following solution:

1. Check which Windows release we're dealing with. Not only it makes the code run a bit faster on Windows 7 and avoid exceptions (see below), it allows us to drop support for Windows 7 easily once we progress to a point where this is unavoidable.
2. Try locating package full name by calling kernel32.dll::GetPackageFullName. If it returns 0, we're dealing with an immersive app, otherwise (error 15700), fall back to file version info method.
3. If this is an immersive app, parse the package full name structure.

Notes:

1. In order to fully implement this, the current file version info routine will become an inner function that returns a 2-tuple: product name, product version.
2. In terms of speed, although calling IsImmersiveProcess first may sound more intuitive, this will cause an exception to be thrown in Windows 7. This means both code paths (one with and one without this function) must be traversed in Windows 7. Therefore checking Windows release ensures NVDA will continue to call file version info routine as before without coming to package name parser.
3. Continuing from above, the only speed penalty is when dealing with native apps on Windows 8.x and 10. For these, both full package name (immersive process) and file version info (converted desktop or native apps) must be checked. Compounding the issue is that some apps are flagged wrong - File Explorer reports itself as "immersive" when in fact it is a native app, and Office 365 executables from Microsoft Store have their own internal version numbers even though they are not true immersive apps.

Thanks.

Resolving patch:
diff --git a/source/appModuleHandler.py b/source/appModuleHandler.py
--- a/source/appModuleHandler.py
+++ b/source/appModuleHandler.py
@@ -342,21 +342,56 @@ def __init__(self,processID,appName=None):
 		self.helperLocalBindingHandle=None
 		self._inprocRegistrationHandle=None
 
-	def _setProductInfo(self):
-		"""Set productName and productVersion attributes.
-		"""
-		# Sometimes (I.E. when NVDA starts) handle is 0, so stop if it is the case
-		if not self.processHandle:
-			raise RuntimeError("processHandle is 0")
+	def _getExecutableFileInfo(self):
+		# Used for obtaining file name and version for the executable.
+		# This is needed in case immersive app package returns an error,
+		# dealing with a native app, or a converted desktop app.
 		# Create the buffer to get the executable name
 		exeFileName = ctypes.create_unicode_buffer(ctypes.wintypes.MAX_PATH)
 		length = ctypes.wintypes.DWORD(ctypes.wintypes.MAX_PATH)
-		if not ctypes.windll.Kernel32.QueryFullProcessImageNameW(self.processHandle, 0, exeFileName, ctypes.byref(length)):
+		if not ctypes.windll.Kernel32.QueryFullProcessImageNameW(
+			self.processHandle, 0, exeFileName, ctypes.byref(length)
+		):
 			raise ctypes.WinError()
 		fileName = exeFileName.value
 		fileinfo = getFileVersionInfo(fileName, "ProductName", "ProductVersion")
-		self.productName = fileinfo["ProductName"]
-		self.productVersion = fileinfo["ProductVersion"]
+		return (fileinfo["ProductName"], fileinfo["ProductVersion"])
+
+	def _setProductInfo(self):
+		"""Set productName and productVersion attributes.
+		There are at least two ways of obtaining product info for an app:
+		* Package info for hosted apps
+		* File version info for other apps and for some hosted apps
+		"""
+		# Sometimes (I.E. when NVDA starts) handle is 0, so stop if it is the case
+		if not self.processHandle:
+			raise RuntimeError("processHandle is 0")
+		# No need to worry about immersive (hosted) apps and friends until Windows 8.
+		# Python 3.7 introduces platform_version to sys.getwindowsversion tuple,
+		# which returns major, minor, build.
+		if winVersion.winVersion.platform_version >= (6, 2, 9200):
+			# Some apps such as File Explorer says it is an immersive process but error 15700 is shown.
+			# Therefore resort to file version info behavior because it is not a hosted app.
+			# Others such as Store version of Office are not truly hosted apps,
+			# yet returns an internal version anyway.
+			# For immersive apps, default implementation is generic - returns Windows version information.
+			# Thus probe package full name and parse the serialized representation of package info structure.
+			length = ctypes.c_uint()
+			buf = ctypes.windll.kernel32.GetPackageFullName(self.processHandle, ctypes.byref(length), None)
+			packageFullName = ctypes.create_unicode_buffer(buf)
+			if ctypes.windll.kernel32.GetPackageFullName(
+				self.processHandle, ctypes.byref(length), packageFullName
+			) == 0:
+				# Product name is of the form publisher.name for a hosted app.
+				productInfo = packageFullName.value.split("_")
+			else:
+				# File Explorer and friends which are really native aps.
+				productInfo = self._getExecutableFileInfo()
+		else:
+			# Not only native apps, but also converted desktop aps such as Office.
+			productInfo = self._getExecutableFileInfo()
+		self.productName = productInfo[0]
+		self.productVersion = productInfo[1]
 
 	def _get_productName(self):
 		self._setProductInfo()


Test patch: