Repository: nvaccess/nvda

Issue:
Many scripts not available on secure screen
### Steps to reproduce:
You should have already configured NVDA to be used on Windows secure screen.
1. Start the computer to reach the logon screen
2. Press NVDA+N
3. Press NVDA+1
4. etc.
### Actual behavior:
2. No NVDA menu
3. Help mode is not enabled
### Expected behavior:
2. NVDA menu should open.
3. Help mode should be enabled
### NVDA logs, crash dumps and other attachments:
N/A
### System configuration
#### NVDA installed/portable/running from source:
installed
#### NVDA version:
2022.4beta2
#### Windows version:
Windows 10 2004 (x64) build 19041.388
But same issue seen on other versions of Windows 10.
#### Name and version of other software in use when reproducing the issue:
N/A
#### Other information about your system:
N/A
### 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.
Not tried.
But I remember that the issue was not occurring before. I think it has arrived with NVDA2022.4beta2.
#### If NVDA add-ons are disabled, is your problem still occurring?
Not tested.
#### Does the issue still occur after you run the COM Registration Fixing Tool in NVDA's tools menu?
Yes



Discussion:
Thanks for creating this issue @CyrilleB79. We are aware of this problem introduced in 2022.3.1 and have begun work on fixing it.
On a French mailing list, there is also a [message](https://groups.io/g/nvda-fr/topic/nvda_2022_3_1_et_clavier/94530692?p=,,,100,0,0,0::recentpostdate/sticky,,,100,2,0,94530692,previd%3D1666676286283460360,nextid%3D1660631749187859350&previd=1666676286283460360&nextid=1660631749187859350
) from someone complaining not to be able to use braille keyboard on the logon screen to enter his password. The issue has appeared with NVDA 2022.3.1.
I have no more information.

Could this be due to the same cause?

@CyrilleB79 - yes this is likely. When #14288 is merged across to master, it would be good to have the user confirm the issue is fixed.

Resolving patch:
diff --git a/source/globalVars.py b/source/globalVars.py
--- a/source/globalVars.py
+++ b/source/globalVars.py
@@ -43,6 +43,18 @@ class DefaultAppArgs(argparse.Namespace):
 	language: str = "en"
 	minimal: bool = False
 	secure: bool = False
+	"""
+	When this is True, NVDA is running in secure mode.
+	This is set to True when NVDA starts with the --secure parameter.
+	This is also set to True when NVDA is running on a secure screen
+	(systemUtils._isSecureDesktop() returns True)
+	and the serviceDebug parameter is not set.
+
+	For more information, refer to devDocs/technicalDesignOverview.md 'Logging in secure mode'
+	and the following userGuide sections:
+	 - SystemWideParameters (information on the serviceDebug parameter)
+	 - SecureMode and SecureScreens
+	"""
 	disableAddons: bool = False
 	debugLogging: bool = False
 	noLogging: bool = False
diff --git a/source/systemUtils.py b/source/systemUtils.py
--- a/source/systemUtils.py
+++ b/source/systemUtils.py
@@ -1,11 +1,17 @@
 # -*- coding: UTF-8 -*-
 # A part of NonVisual Desktop Access (NVDA)
-# Copyright (C) 2020-2021 NV Access Limited, Łukasz Golonka
+# Copyright (C) 2020-2022 NV Access Limited, Łukasz Golonka
 # This file may be used under the terms of the GNU General Public License, version 2 or later.
 # For more details see: https://www.gnu.org/licenses/gpl-2.0.html
 
 """ System related functions."""
 import ctypes
+from ctypes import (
+	byref,
+	create_unicode_buffer,
+	sizeof,
+	windll,
+)
 import winKernel
 import shellapi
 import winUser
@@ -142,3 +148,34 @@ def execElevated(path, params=None, wait=False, handleAlreadyElevated=False):
 			return winKernel.GetExitCodeProcess(sei.hProcess)
 		finally:
 			winKernel.closeHandle(sei.hProcess)
+
+
+@functools.lru_cache(maxsize=1)
+def _getDesktopName() -> str:
+	UOI_NAME = 2  # The name of the object, as a string
+	desktop = windll.user32.GetThreadDesktop(windll.kernel32.GetCurrentThreadId())
+	name = create_unicode_buffer(256)
+	windll.user32.GetUserObjectInformationW(
+		desktop,
+		UOI_NAME,
+		byref(name),
+		sizeof(name),
+		None
+	)
+	return name.value
+
+
+def _isSecureDesktop() -> bool:
+	"""
+	When NVDA is running on a secure screen,
+	it is running on the secure desktop.
+	When the serviceDebug parameter is not set,
+	NVDA should run in secure mode when on the secure desktop.
+	globalVars.appArgs.secure being set to True means NVDA is running in secure mode.
+
+	For more information, refer to devDocs/technicalDesignOverview.md 'Logging in secure mode'
+	and the following userGuide sections:
+	 - SystemWideParameters (information on the serviceDebug parameter)
+	 - SecureMode and SecureScreens
+	"""
+	return _getDesktopName() == "Winlogon"
diff --git a/source/winAPI/sessionTracking.py b/source/winAPI/sessionTracking.py
--- a/source/winAPI/sessionTracking.py
+++ b/source/winAPI/sessionTracking.py
@@ -24,11 +24,14 @@
 	HANDLE,
 )
 import enum
+from threading import Lock
 from typing import (
 	Dict,
 	Set,
 	Optional,
 )
+
+from systemUtils import _isSecureDesktop
 from winAPI.wtsApi32 import (
 	WTSINFOEXW,
 	WTSQuerySessionInformation,
@@ -44,11 +47,14 @@
 from .types import HWNDValT
 
 
+_updateSessionStateLock = Lock()
+"""Used to protect updates to _currentSessionStates"""
 _currentSessionStates: Set["WindowsTrackedSession"] = set()
 """
 Current state of the Windows session associated with this instance of NVDA.
 Maintained via receiving session notifications via the NVDA MessageWindow.
 Initial state will be set by querying the current status.
+Actions which involve updating this state this should be protected by _updateSessionStateLock.
 """
 
 _sessionQueryLockStateHasBeenUnknown = False
@@ -152,6 +158,17 @@ def isWindowsLocked() -> bool:
 	Checks if the Window lockscreen is active.
 	Not to be confused with the Windows sign-in screen, a secure screen.
 	"""
+	if _isSecureDesktop():
+		# If this is the Secure Desktop,
+		# we are in secure mode and on a secure screen,
+		# e.g. on the sign-in screen.
+		# _isSecureDesktop may also return True on the lock screen before a user has signed in.
+
+		# For more information, refer to devDocs/technicalDesignOverview.md 'Logging in secure mode'
+		# and the following userGuide sections:
+		# - SystemWideParameters (information on the serviceDebug parameter)
+		# - SecureMode and SecureScreens
+		return False
 	lockStateTracked = _hasLockStateBeenTracked()
 	if lockStateTracked:
 		return WindowsTrackedSession.SESSION_LOCK in _currentSessionStates
@@ -166,22 +183,29 @@ def isWindowsLocked() -> bool:
 
 
 def _setInitialWindowLockState() -> None:
-	"""Ensure that session tracking state is initialized.
+	"""
+	Ensure that session tracking state is initialized.
 	If NVDA has started on a lockScreen, it needs to be aware of this.
+	As NVDA has already registered for session tracking notifications,
+	a lock is used to prevent conflicts.
 	"""
-	lockStateTracked = _hasLockStateBeenTracked()
-	if lockStateTracked:
-		raise RuntimeError("Can't set initial state if it is already tracked.")
-	# Fall back to explicit query
-	try:
-		isLocked = _isWindowsLocked_checkViaSessionQuery()
-		_currentSessionStates.add(
-			WindowsTrackedSession.SESSION_LOCK
-			if isLocked
-			else WindowsTrackedSession.SESSION_UNLOCK
-		)
-	except RuntimeError as error:
-		_recordLockStateTrackingFailure(error)
+	with _updateSessionStateLock:
+		lockStateTracked = _hasLockStateBeenTracked()
+		if lockStateTracked:
+			log.debugWarning(
+				"Initial state already set."
+				" NVDA may have received a session change notification before initialising"
+			)
+		# Fall back to explicit query
+		try:
+			isLocked = _isWindowsLocked_checkViaSessionQuery()
+			_currentSessionStates.add(
+				WindowsTrackedSession.SESSION_LOCK
+				if isLocked
+				else WindowsTrackedSession.SESSION_UNLOCK
+			)
+		except RuntimeError as error:
+			_recordLockStateTrackingFailure(error)
 
 
 def _isWindowsLocked_checkViaSessionQuery() -> bool:
@@ -226,10 +250,6 @@ def register(handle: HWNDValT) -> bool:
 
 	https://docs.microsoft.com/en-us/windows/win32/api/wtsapi32/nf-wtsapi32-wtsregistersessionnotification
 	"""
-	##
-	# Ensure that an initial state is set,
-	# do this first to prevent a race condition with session tracking / message processing.
-	_setInitialWindowLockState()
 
 	# OpenEvent handle must be closed with CloseHandle.
 	eventObjectHandle: HANDLE = ctypes.windll.kernel32.OpenEventW(
@@ -253,6 +273,15 @@ def register(handle: HWNDValT) -> bool:
 
 	if registrationSuccess:
 		log.debug("Registered session tracking")
+		# Ensure that an initial state is set.
+		# Do this only when session tracking has been registered,
+		# so that any changes to the state are not missed via a race condition with session tracking registration.
+		# As this occurs after NVDA hs registered for session tracking,
+		# it is possible NVDA is expected to handle a session change notification
+		# at the same time as initialisation.
+		# _updateSessionStateLock is used to prevent received session notifications from being handled at the
+		# same time as initialisation.
+		_setInitialWindowLockState()
 	else:
 		error = ctypes.WinError()
 		if error.errno == RPC_S_INVALID_BINDING:
@@ -299,43 +328,44 @@ def handleSessionChange(newState: WindowsTrackedSession, sessionId: int) -> None
 
 	https://docs.microsoft.com/en-us/windows/win32/termserv/wm-wtssession-change
 	"""
-	stateChanged = False
+	with _updateSessionStateLock:
+		stateChanged = False
 
-	log.debug(f"Windows Session state notification received: {newState.name}")
+		log.debug(f"Windows Session state notification received: {newState.name}")
 
-	if not _isSessionTrackingRegistered:
-		log.debugWarning("Session tracking not registered, unexpected session change message")
+		if not _isSessionTrackingRegistered:
+			log.debugWarning("Session tracking not registered, unexpected session change message")
 
-	if newState not in _toggleWindowsSessionStatePair:
-		log.debug(f"Ignoring {newState} event as tracking is not required.")
-		return
+		if newState not in _toggleWindowsSessionStatePair:
+			log.debug(f"Ignoring {newState} event as tracking is not required.")
+			return
 
-	oppositeState = _toggleWindowsSessionStatePair[newState]
-	if newState in _currentSessionStates:
-		log.error(
-			f"NVDA expects Windows to be in {newState} already. "
-			f"NVDA may have dropped a {oppositeState} event. "
-			f"Dropping this {newState} event. "
-		)
-	else:
-		_currentSessionStates.add(newState)
-		stateChanged = True
+		oppositeState = _toggleWindowsSessionStatePair[newState]
+		if newState in _currentSessionStates:
+			log.error(
+				f"NVDA expects Windows to be in {newState} already. "
+				f"NVDA may have dropped a {oppositeState} event. "
+				f"Dropping this {newState} event. "
+			)
+		else:
+			_currentSessionStates.add(newState)
+			stateChanged = True
 
-	if oppositeState in _currentSessionStates:
-		_currentSessionStates.remove(oppositeState)
-	else:
-		log.debugWarning(
-			f"NVDA expects Windows to be in {newState} already. "
-			f"NVDA may have dropped a {oppositeState} event. "
-		)
+		if oppositeState in _currentSessionStates:
+			_currentSessionStates.remove(oppositeState)
+		else:
+			log.debugWarning(
+				f"NVDA expects Windows to be in {newState} already. "
+				f"NVDA may have dropped a {oppositeState} event. "
+			)
 
-	log.debug(f"New Windows Session state: {_currentSessionStates}")
-	if (
-		stateChanged
-		and newState in {WindowsTrackedSession.SESSION_LOCK, WindowsTrackedSession.SESSION_UNLOCK}
-	):
-		from utils.security import postSessionLockStateChanged
-		postSessionLockStateChanged.notify(isNowLocked=newState == WindowsTrackedSession.SESSION_LOCK)
+		log.debug(f"New Windows Session state: {_currentSessionStates}")
+		if (
+			stateChanged
+			and newState in {WindowsTrackedSession.SESSION_LOCK, WindowsTrackedSession.SESSION_UNLOCK}
+		):
+			from utils.security import postSessionLockStateChanged
+			postSessionLockStateChanged.notify(isNowLocked=newState == WindowsTrackedSession.SESSION_LOCK)
 
 
 @contextmanager


Test patch: