Repository: nvaccess/nvda

Issue:
Browse mode is not available in some electron apps running on Windows 11 ARM
<!-- Please read the text in this edit field before filling it in.
Please thoroughly read NVDA's wiki article on how to fill in this template, including how to provide the required files.
Issues may be closed if the required information is not present.
https://github.com/nvaccess/nvda/blob/master/devDocs/githubIssueTemplateExplanationAndExamples.md
Please also note that the NVDA project has a Citizen and Contributor Code of Conduct which can be found at https://github.com/nvaccess/nvda/blob/master/CODE_OF_CONDUCT.md. NV Access expects that all contributors and other community members read and abide by the rules set out in this document while participating or contributing to this project. This includes creating or commenting on issues and pull requests. 

Each of the questions and sections below start with multiple hash symbols (#). Place your answers and information on the blank line below each question.
-->

I have observed this problem with the 64 version of 1Password 8, the RIM app by Pneuma solutions and the version of Slack that can be obtained from the Microsoft store running on a virtual machine with Windows 11 ARM. The problem does not occur with the 32 bits version of Slack, the ARM64 version of Visual Studio code or Teams. All the apps work as expected with Narrator. Executing the command focus.devInfo in the Python Console while focus is inside any of the non-working apps shows the IAccessible2 properties related to the object with focus.

### Steps to reproduce:

1. Download and run Slack from the Microsoft Store.
2. While focus is on the app, try turning on browse mode by pressing NVDA +the space bar.

### Actual behavior:
<!--
Use "Speak command keys" (NVDA+4) and speech viewer to copy and paste here. 
Use braille viewer to copy and paste here.
You may additionally include an explanation.
-->

Nothing happens. The following gets added to the lock when the electron app receives focus:

DEBUGWARNING - virtualBuffers.VirtualBuffer.prepare (18:55:41.302) - MainThread (4072):
appModule has no binding handle to injected code, can't prepare virtualBuffer yet.

### Expected behavior:
<!--
Use "Speak command keys" (NVDA+4) and speech viewer to copy and paste here. 
Use braille viewer to copy and paste here.
You may additionally include an explanation.
-->

NVDA can switch between focus and browse mode.

### NVDA logs, crash dumps and other attachments:

### System configuration
#### NVDA installed/portable/running from source:

Installed

#### NVDA version:

2022.3.2

#### Windows version:

Edition	Windows 11 Pro

Version	22H2

OS build	22621.819

Experience	Windows Feature Experience Pack 1000.22636.1000.0

#### Name and version of other software in use when reproducing the issue:

N/a

#### Other information about your system:

Windows is running on a Parallels VM

### Other questions
#### Does the issue still occur after restarting your computer?
Yes
#### Have you tried any other versions of NVDA? If so, please report their behaviors.
No
#### If NVDA add-ons are disabled, is your problem still occurring?
Yes
#### Does the issue still occur after you run the COM Registration Fixing Tool in NVDA's tools menu?
Yes


Discussion:
Hi,

Can you report the results of:

1. From the affected app, press Control+NVDA+F1 and see if says something.
2. Again from the affected ap, press Control+NVDA+Z, then type "focus.appModule.appArchitecture" and see if it says "AMD64" or something similar.

If it says something other than "AMD64" for the app architecture, chances are that this is an emulation compatible (Arm64EC) application with a mixture of x64 and ARM64 code included. CC @MichaelDCurran, do you think we should update our building logic to support injecting code into AMD64/Arm64EC processes in Windows 11 on ARM?

Thanks.
@josephsl Results from 1password:

 1password module is loaded.  1Password.exe is currently running.

focus.appModule.appArchitecture: ARM64

Results from the RIM app:

 Remote Incident Manager.exe is currently running.

focus.appModule.appArchitecture: ARM64


Looking at the devInfo output for 1Password I found the following:

appModule: <'1password' (appName '1password', process ID 4760) at address 80feef0>
appModule.productName: exception: processHandle is 0
appModule.productVersion: exception: processHandle is 0

Yes, we definitely want to support injecting into AMD64 / ARM64EC 
processes if we can.

The quickest fix here is to change NVDAHelper.py to start an 
nvdaHelperRemoteLoader process for both AMD64 and ARM64 on ARM. 
Currently it only starts an ARM64 nvdaHelperRemoteLoader process. This 
change will then allow injecting into AMD64 processes on ARM.

As for ARM64EC: just a guess, but I'm pretty sure that starting an 
nvdaHelperRemoteLoader process for AMD64, and therefore registering the 
needed AMD64 injection winEvents, will also cause us to automatically be 
also injected into ARM64EC processes for free, as they are fully 
compatible with AMD64. However, for performance reasons we could also 
provide ARM64EC builds of our injection DLLs, and use them on Windows 11 
for when registering in nvdaHelperRemoteLoader AMD64. Which would 
provide greater performance on AMD64 and ARM64EC processes.

However, there is no need, and in deed it would be detrimental to, 
provide / run a nvdaHelprerRemoteLoader specific for ARM64EC at the same 
time as running an nvdaHelperRemoteLoader for AMD64, as this will cause 
us to inject two lots of DLLs into both of those process types at the 
same time.

Somewhat related: the appArchitecture property on appModules currently 
reports ARM64 for AMD64 processes on ARM which is incorrect. This is 
because we only detect WOW64 (I.e. 32 bit emulation). On Windows 11, we 
need to instead call GetProcessInformation, giving it the 
ProcessMachineTypeInfo constant.

Hi,

As for GetProcessInformation function call, I think it would be best to split this into a separate issue/PR pair (wish we can get Microsoft ARM development kit to test this scenario more fully).

Thanks.
Hi,

Possible solution based on Mick's comment:

1. Change the Remote Loader 64-bit class by adding a keyword argument to the constructor, taking lib64 directory. By default, it will use whatever lib64 variable gets set to.
2. In the initialize function, run an additional remote loader executable for AMD64 processes if this is Windows 11 on ARM (cannot be done for Windows 10 on ARM because it does not support running AMD64 processes). Be sure to termiante BOTH processes in Windows 11 on ARM (to do this, define another variable just to hold a reference to the second remote loader executable class).

Ideally someone using a Windows 11 on ARM device should come up with a pull request.

Thanks.
NV Access has just purchased a 2023 Windows Dev Kit which means we are 
now again in a position to be able to test on ARM. So, I'll look into 
these changes in the near future.

I don't feel like I have the skills to send a PR for something like this at this point but I'm happy to test with my Windows ARM VM. I'm running NVDA from source so I can checkout any PR as soon as it become available.

Hi, thanks. As a starting point, can you test #14404 and see if an AMD64 app is recognized as an AMD64 app on Windows 11 on ARM please? That can provide something for us to look at once a pull request is created to resolve this issue. Thanks.


Yes, I'm planning on taking on all of this project.
And as I mentioned before, On ARM, AMD64 apps are incorrectly reported 
as ARM64 in NVDA. Thus we need to fix AppModule.appArchitecture, as well 
as fix the logic with nvdaHelperRemoteLoader.

Done. @josephsl  i left a comment on #14404.

Resolving patch:
diff --git a/source/NVDAHelper.py b/source/NVDAHelper.py
--- a/source/NVDAHelper.py
+++ b/source/NVDAHelper.py
@@ -4,9 +4,11 @@
 # This file is covered by the GNU General Public License.
 # See the file COPYING for more details.
 
+from typing import Optional
 import os
 import winreg
 import msvcrt
+import winVersion
 import versionInfo
 import winKernel
 import config
@@ -31,19 +33,20 @@
 from utils.security import _isLockScreenModeActive
 
 versionedLibPath = os.path.join(globalVars.appDir, 'lib')
-if os.environ.get('PROCESSOR_ARCHITEW6432') == 'ARM64':
-	versionedLib64Path = os.path.join(globalVars.appDir, 'libArm64')
-else:
-	versionedLib64Path = os.path.join(globalVars.appDir, 'lib64')
+versionedLibARM64Path = os.path.join(globalVars.appDir, 'libArm64')
+versionedLibAMD64Path = os.path.join(globalVars.appDir, 'lib64')
 
 
 if not NVDAState.isRunningAsSource():
 	# When running as a py2exe build, libraries are in a version-specific directory
 	versionedLibPath=os.path.join(versionedLibPath,versionInfo.version)
-	versionedLib64Path=os.path.join(versionedLib64Path,versionInfo.version)
+	versionedLibAMD64Path = os.path.join(versionedLibAMD64Path, versionInfo.version)
+	versionedLibARM64Path = os.path.join(versionedLibARM64Path, versionInfo.version)
+
 
 _remoteLib=None
-_remoteLoader64=None
+_remoteLoaderAMD64: "Optional[_RemoteLoader]" = None
+_remoteLoaderARM64: "Optional[_RemoteLoader]" = None
 localLib=None
 generateBeep=None
 VBuf_getTextInRange=None
@@ -480,9 +483,9 @@ def nvdaControllerInternal_openConfigDirectory():
 	return 0
 
 
-class RemoteLoader64(object):
+class _RemoteLoader:
 
-	def __init__(self):
+	def __init__(self, loaderDir: str):
 		# Create a pipe so we can write to stdin of the loader process.
 		pipeReadOrig, self._pipeWrite = winKernel.CreatePipe(None, 0)
 		# Make the read end of the pipe inheritable.
@@ -500,7 +503,9 @@ def __init__(self):
 		# Therefore, explicitly specify our own process token, which causes them to be inherited.
 		token = winKernel.OpenProcessToken(winKernel.GetCurrentProcess(), winKernel.MAXIMUM_ALLOWED)
 		try:
-			winKernel.CreateProcessAsUser(token, None, os.path.join(versionedLib64Path,u"nvdaHelperRemoteLoader.exe"), None, None, True, None, None, None, si, pi)
+			loaderPath = os.path.join(loaderDir, "nvdaHelperRemoteLoader.exe")
+			log.debug(f"Starting {loaderPath}")
+			winKernel.CreateProcessAsUser(token, None, loaderPath, None, None, True, None, None, None, si, pi)
 			# We don't need the thread handle.
 			winKernel.closeHandle(pi.hThread)
 			self._process = pi.hProcess
@@ -522,8 +527,10 @@ def terminate(self):
 		winKernel.waitForSingleObject(self._process, winKernel.INFINITE)
 		winKernel.closeHandle(self._process)
 
+
 def initialize():
-	global _remoteLib, _remoteLoader64, localLib, generateBeep, VBuf_getTextInRange, lastLanguageID, lastLayoutString
+	global _remoteLib, _remoteLoaderAMD64, _remoteLoaderARM64
+	global localLib, generateBeep, VBuf_getTextInRange, lastLanguageID, lastLayoutString
 	hkl=c_ulong(windll.User32.GetKeyboardLayout(0)).value
 	lastLanguageID=winUser.LOWORD(hkl)
 	KL_NAMELENGTH=9
@@ -587,20 +594,33 @@ def initialize():
 		log.error("Error installing IA2 support")
 	#Manually start the in-process manager thread for this NVDA main thread now, as a slow system can cause this action to confuse WX
 	_remoteLib.initInprocManagerThreadIfNeeded()
-	if os.environ.get('PROCESSOR_ARCHITEW6432') in ('AMD64', 'ARM64'):
-		_remoteLoader64=RemoteLoader64()
+	versionedLibARM64Path
+	arch = os.environ.get('PROCESSOR_ARCHITEW6432')
+	if arch == 'AMD64':
+		_remoteLoaderAMD64 = _RemoteLoader(versionedLibAMD64Path)
+	elif arch == 'ARM64':
+		_remoteLoaderARM64 = _RemoteLoader(versionedLibARM64Path)
+		# Windows on ARM from Windows 11 supports running AMD64 apps.
+		# Thus we also need to be able to inject into these.
+		if winVersion.getWinVer() >= winVersion.WIN11:
+			_remoteLoaderAMD64 = _RemoteLoader(versionedLibAMD64Path)
+
 
 def terminate():
-	global _remoteLib, _remoteLoader64, localLib, generateBeep, VBuf_getTextInRange
+	global _remoteLib, _remoteLoaderAMD64, _remoteLoaderARM64
+	global localLib, generateBeep, VBuf_getTextInRange
 	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
+		if _remoteLoaderAMD64:
+			_remoteLoaderAMD64.terminate()
+			_remoteLoaderAMD64 = None
+		if _remoteLoaderARM64:
+			_remoteLoaderARM64.terminate()
+			_remoteLoaderARM64 = None
 	generateBeep=None
 	VBuf_getTextInRange=None
 	localLib.nvdaHelperLocal_terminate()


Test patch: