Skip to content

Commit 750afb1

Browse files
authored
Fix registry handling for NVDA automatic startup settings (#20864)
Summary of the issue: Saving automatic startup settings fails when the Accessibility registry key is missing. Registry read failures are also treated as empty configuration. This can skip a requested change or overwrite existing entries if a subsequent write succeeds. Some save failures interrupt saving the remaining settings. Description of user facing changes: NVDA can save startup settings when the registry key is missing. General settings and the Welcome dialog report failed startup changes and continue saving other options. Description of developer facing changes: Startup setters now report registry read errors and non-string configuration through OSError and TypeError, respectively. Description of development approach: Use CreateKeyEx when writing. Microsoft documents that RegOpenKeyEx does not create missing keys, whereas RegCreateKeyEx creates or opens them. Treat only a missing key or value as empty configuration. Other read errors stop that write attempt. Microsoft documents Configuration as a comma-separated list to which applications append their names; treating an unread list as empty risks losing existing entries. Remove setter checks based on fallback query results and retry permission errors with elevation. Submit startup options only after user interaction, preventing fallback display values from triggering unintended changes.
1 parent 72601bf commit 750afb1

8 files changed

Lines changed: 295 additions & 58 deletions

File tree

‎source/config/__init__.py‎

Lines changed: 8 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -267,9 +267,10 @@ def setStartAfterLogon(enable: bool) -> None:
267267
268268
Toggle if NVDA automatically starts after a logon.
269269
Sets easeOfAccess related registry keys.
270+
271+
:raises OSError: For registry errors.
272+
:raises TypeError: If the configuration data is not a string.
270273
"""
271-
if getStartAfterLogon() == enable:
272-
return
273274
easeOfAccess.setAutoStart(easeOfAccess.AutoStartContext.AFTER_LOGON, enable)
274275

275276

@@ -424,16 +425,16 @@ def setStartOnLogonScreen(enable: bool) -> None:
424425
Not to be confused with setStartAfterLogon.
425426
426427
Toggle whether NVDA starts on the logon screen automatically.
427-
On failure to set, retries with escalated permissions.
428+
On permission errors, retries with escalated permissions.
428429
429-
Raises a RuntimeError on failure.
430+
:raises RuntimeError: If the elevated process fails.
431+
:raises OSError: For registry or elevation errors.
432+
:raises TypeError: If the configuration data is not a string.
430433
"""
431-
if getStartOnLogonScreen() == enable:
432-
return
433434
try:
434435
# Try setting it directly.
435436
_setStartOnLogonScreen(enable)
436-
except OSError:
437+
except PermissionError:
437438
log.debugWarning(
438439
"Failed to set start on logon screen's config, retrying elevated.",
439440
exc_info=True,

‎source/easeOfAccess.py‎

Lines changed: 25 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
# A part of NonVisual Desktop Access (NVDA)
2-
# Copyright (C) 2014-2025 NV Access Limited
2+
# Copyright (C) 2014-2026 NV Access Limited
33
# This file is covered by the GNU General Public License.
44
# See the file COPYING for more details.
55

@@ -102,48 +102,40 @@ def willAutoStart(autoStartContext: AutoStartContext) -> bool:
102102
103103
Returns False on failure
104104
"""
105-
return EASE_OF_ACCESS_APP_KEY_NAME in _getAutoStartConfiguration(autoStartContext)
105+
try:
106+
return EASE_OF_ACCESS_APP_KEY_NAME in _getAutoStartConfiguration(autoStartContext)
107+
except (OSError, TypeError):
108+
log.exception(f"Unable to read {autoStartContext} auto-start configuration")
109+
return False
106110

107111

108112
def _getAutoStartConfiguration(autoStartContext: AutoStartContext) -> list[str]:
109113
"""Based on autoStartContext, returns a list of app names which start automatically:
110114
- AutoStartContext.ON_LOGON_SCREEN : on the logon screen
111115
- AutoStartContext.AFTER_LOGON : after logging on
112116
113-
Returns an empty list on failure.
117+
Returns an empty list if the registry key or value does not exist.
118+
119+
:raises OSError: For other registry errors.
120+
:raises TypeError: If the configuration data is not a string.
114121
"""
115122
try:
116-
k = winreg.OpenKey(
123+
with winreg.OpenKey(
117124
autoStartContext.value,
118125
_RegistryKey.EASE_OF_ACCESS.value,
119126
access=winreg.KEY_READ | winreg.KEY_WOW64_64KEY,
120-
)
121-
except FileNotFoundError:
122-
log.debug(f"Unable to find existing {autoStartContext} {_RegistryKey.EASE_OF_ACCESS}")
123-
return []
124-
except OSError:
125-
log.error( # noqa: G201
126-
f"Unable to open {autoStartContext} {_RegistryKey.EASE_OF_ACCESS} for reading",
127-
exc_info=True,
128-
)
129-
return []
130-
131-
try:
132-
conf: list[str] = winreg.QueryValueEx(k, "Configuration")[0].split(",")
127+
) as k:
128+
value = winreg.QueryValueEx(k, "Configuration")[0]
133129
except FileNotFoundError:
134130
log.debug(f"Unable to find {autoStartContext} {_RegistryKey.EASE_OF_ACCESS} configuration")
135-
except OSError:
136-
log.error( # noqa: G201
137-
f"Unable to query {autoStartContext} {_RegistryKey.EASE_OF_ACCESS} configuration",
138-
exc_info=True,
139-
)
140-
else:
141-
k.Close()
142-
if not conf[0]:
143-
# "".split(",") returns [""], so remove the empty string.
144-
del conf[0]
145-
return conf
146-
return []
131+
return []
132+
if not isinstance(value, str):
133+
raise TypeError(f"Expected a string for {autoStartContext} auto-start configuration")
134+
conf: list[str] = value.split(",")
135+
if not conf[0]:
136+
# "".split(",") returns [""], so remove the empty string.
137+
del conf[0]
138+
return conf
147139

148140

149141
def setAutoStart(autoStartContext: AutoStartContext, enable: bool) -> None:
@@ -152,9 +144,10 @@ def setAutoStart(autoStartContext: AutoStartContext, enable: bool) -> None:
152144
- AutoStartContext.ON_LOGON_SCREEN : on the logon screen
153145
- AutoStartContext.AFTER_LOGON : after logging on
154146
155-
May incorrectly set autoStart to False upon failing to fetch the previously set value from the registry.
147+
Does not write if the existing configuration cannot be read.
156148
157-
Raises `Union[WindowsError, FileNotFoundError]`
149+
:raises OSError: For registry errors.
150+
:raises TypeError: If the configuration data is not a string.
158151
"""
159152
conf = _getAutoStartConfiguration(autoStartContext)
160153
currentlyEnabled = EASE_OF_ACCESS_APP_KEY_NAME in conf
@@ -168,7 +161,7 @@ def setAutoStart(autoStartContext: AutoStartContext, enable: bool) -> None:
168161
changed = True
169162

170163
if changed:
171-
with winreg.OpenKey(
164+
with winreg.CreateKeyEx(
172165
autoStartContext.value,
173166
_RegistryKey.EASE_OF_ACCESS.value,
174167
access=winreg.KEY_READ | winreg.KEY_WRITE | winreg.KEY_WOW64_64KEY,

‎source/gui/settingsDialogs.py‎

Lines changed: 49 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -868,6 +868,9 @@ def makeSettings(self, settingsSizer):
868868
# start NVDA by pressing the shortcut key (CTRL+Alt+N by default).
869869
self.startAfterLogonCheckBox = wx.CheckBox(self, label=_("St&art NVDA after I sign in"))
870870
self.startAfterLogonCheckBox.SetValue(config.getStartAfterLogon())
871+
# A failed read appears unchecked; only an explicit user action should request a change.
872+
self._hasStartAfterLogonChanged: bool = False
873+
self.startAfterLogonCheckBox.Bind(wx.EVT_CHECKBOX, self._onStartAfterLogonChanged)
871874
if globalVars.appArgs.secure or not config.isInstalledCopy():
872875
self.startAfterLogonCheckBox.Disable()
873876
settingsSizerHelper.addItem(self.startAfterLogonCheckBox)
@@ -882,6 +885,9 @@ def makeSettings(self, settingsSizer):
882885
)
883886
self.bindHelpEvent("GeneralSettingsStartOnLogOnScreen", self.startOnLogonScreenCheckBox)
884887
self.startOnLogonScreenCheckBox.SetValue(config.getStartOnLogonScreen())
888+
# A failed read appears unchecked; only an explicit user action should request a change.
889+
self._hasStartOnLogonScreenChanged: bool = False
890+
self.startOnLogonScreenCheckBox.Bind(wx.EVT_CHECKBOX, self._onStartOnLogonScreenChanged)
885891
if globalVars.appArgs.secure or not config.isInstalledCopy():
886892
self.startOnLogonScreenCheckBox.Disable()
887893
settingsSizerHelper.addItem(self.startOnLogonScreenCheckBox)
@@ -1049,7 +1055,15 @@ def onCopySettings(self, evt):
10491055
self,
10501056
)
10511057

1052-
def onSave(self):
1058+
def _onStartAfterLogonChanged(self, evt: wx.CommandEvent) -> None:
1059+
self._hasStartAfterLogonChanged = True
1060+
evt.Skip()
1061+
1062+
def _onStartOnLogonScreenChanged(self, evt: wx.CommandEvent) -> None:
1063+
self._hasStartOnLogonScreenChanged = True
1064+
evt.Skip()
1065+
1066+
def onSave(self) -> None:
10531067
if (
10541068
not languageHandler.isLanguageForced()
10551069
or self.languageList.GetSelection() != len(self.languageNames) - 1
@@ -1059,18 +1073,42 @@ def onSave(self):
10591073
config.conf["general"]["saveConfigurationOnExit"] = self.saveOnExitCheckBox.IsChecked()
10601074
config.conf["general"]["askToExit"] = self.askToExitCheckBox.IsChecked()
10611075
config.conf["general"]["playStartAndExitSounds"] = self.playStartAndExitSoundsCheckBox.IsChecked()
1062-
if self.startAfterLogonCheckBox.IsEnabled():
1063-
config.setStartAfterLogon(self.startAfterLogonCheckBox.GetValue())
1064-
if self.startOnLogonScreenCheckBox.IsEnabled():
1076+
if self.startAfterLogonCheckBox.IsEnabled() and self._hasStartAfterLogonChanged:
1077+
try:
1078+
config.setStartAfterLogon(self.startAfterLogonCheckBox.GetValue())
1079+
except (OSError, TypeError):
1080+
log.exception("Unable to set start after sign-in")
1081+
if not core._hasShutdownBeenTriggered:
1082+
gui.message.MessageDialog(
1083+
parent=self,
1084+
message=_(
1085+
# Translators: An error when changing whether NVDA starts automatically after signing in.
1086+
"Unable to change the setting to start NVDA after you sign in.",
1087+
),
1088+
# Translators: The title of an error message dialog.
1089+
title=_("Error"),
1090+
dialogType=gui.message.DialogType.ERROR,
1091+
).ShowModal()
1092+
else:
1093+
self._hasStartAfterLogonChanged = False
1094+
if self.startOnLogonScreenCheckBox.IsEnabled() and self._hasStartOnLogonScreenChanged:
10651095
try:
10661096
config.setStartOnLogonScreen(self.startOnLogonScreenCheckBox.GetValue())
1067-
except (OSError, RuntimeError):
1068-
gui.messageBox(
1069-
_("This change requires administrator privileges."),
1070-
_("Insufficient Privileges"),
1071-
style=wx.OK | wx.ICON_ERROR,
1072-
parent=self,
1073-
)
1097+
except (OSError, RuntimeError, TypeError):
1098+
log.exception("Unable to set start during sign-in")
1099+
if not core._hasShutdownBeenTriggered:
1100+
gui.message.MessageDialog(
1101+
parent=self,
1102+
message=_(
1103+
# Translators: An error when changing whether NVDA starts automatically on the sign-in screen.
1104+
"Unable to change the setting to use NVDA during sign-in.",
1105+
),
1106+
# Translators: The title of an error message dialog.
1107+
title=_("Error"),
1108+
dialogType=gui.message.DialogType.ERROR,
1109+
).ShowModal()
1110+
else:
1111+
self._hasStartOnLogonScreenChanged = False
10741112
if updateCheck:
10751113
config.conf["update"]["autoCheck"] = self.autoCheckForUpdatesCheckBox.IsChecked()
10761114
config.conf["update"]["startupNotification"] = self.notifyForPendingUpdateCheckBox.IsChecked()

‎source/gui/startupDialogs.py‎

Lines changed: 27 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
# A part of NonVisual Desktop Access (NVDA)
2-
# Copyright (C) 2006-2025 NV Access Limited, Łukasz Golonka, Cyrille Bougot
2+
# Copyright (C) 2006-2026 NV Access Limited, Łukasz Golonka, Cyrille Bougot
33
# This file may be used under the terms of the GNU General Public License, version 2 or later.
44
# For more details see: https://www.gnu.org/licenses/gpl-2.0.html
55

@@ -86,6 +86,9 @@ def __init__(self, parent):
8686
startAfterLogonText = _("St&art NVDA after I sign in")
8787
self.startAfterLogonCheckBox = sHelper.addItem(wx.CheckBox(optionsBox, label=startAfterLogonText))
8888
self.startAfterLogonCheckBox.Value = config.getStartAfterLogon()
89+
# A failed read appears unchecked; only an explicit user action should request a change.
90+
self._hasStartAfterLogonChanged: bool = False
91+
self.startAfterLogonCheckBox.Bind(wx.EVT_CHECKBOX, self._onStartAfterLogonChanged)
8992
if globalVars.appArgs.secure or not config.isInstalledCopy():
9093
self.startAfterLogonCheckBox.Disable()
9194
# Translators: The label of a checkbox in the Welcome dialog.
@@ -106,7 +109,11 @@ def __init__(self, parent):
106109
self.kbdList.SetFocus()
107110
self.CentreOnScreen()
108111

109-
def onOk(self, evt):
112+
def _onStartAfterLogonChanged(self, evt: wx.CommandEvent) -> None:
113+
self._hasStartAfterLogonChanged = True
114+
evt.Skip()
115+
116+
def onOk(self, evt: wx.CommandEvent) -> None:
110117
layout = self.kbdNames[self.kbdList.GetSelection()]
111118
config.conf["keyboard"]["keyboardLayout"] = layout
112119
NVDAKeysVal = (
@@ -129,8 +136,24 @@ def onOk(self, evt):
129136
)
130137
else:
131138
config.conf["keyboard"]["NVDAModifierKeys"] = NVDAKeysVal
132-
if self.startAfterLogonCheckBox.Enabled:
133-
config.setStartAfterLogon(self.startAfterLogonCheckBox.Value)
139+
if self.startAfterLogonCheckBox.Enabled and self._hasStartAfterLogonChanged:
140+
try:
141+
config.setStartAfterLogon(self.startAfterLogonCheckBox.Value)
142+
except (OSError, TypeError):
143+
log.exception("Unable to set start after sign-in")
144+
if not core._hasShutdownBeenTriggered:
145+
gui.message.MessageDialog(
146+
parent=self,
147+
message=_(
148+
# Translators: An error when changing whether NVDA starts automatically after signing in.
149+
"Unable to change the setting to start NVDA after you sign in.",
150+
),
151+
# Translators: The title of an error message dialog.
152+
title=_("Error"),
153+
dialogType=gui.message.DialogType.ERROR,
154+
).ShowModal()
155+
else:
156+
self._hasStartAfterLogonChanged = False
134157
config.conf["general"]["showWelcomeDialogAtStartup"] = (
135158
self.showWelcomeDialogAtStartupCheckBox.IsChecked()
136159
)

‎source/installer.py‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -598,7 +598,7 @@ def _unregisterEaseOfAccessApp():
598598
log.debug("Ease of Access app key not found. Nothing to unregister.")
599599
try:
600600
easeOfAccess.setAutoStart(easeOfAccess.AutoStartContext.ON_LOGON_SCREEN, False)
601-
except OSError:
601+
except (OSError, TypeError):
602602
log.debug("Could not disable auto start on logon screen.")
603603

604604

‎tests/checkPot.py‎

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
# A part of NonVisual Desktop Access (NVDA)
2-
# Copyright (C) 2017-2025 NV Access Limited, Ethan Holliger, Dinesh Kaushal, Leonard de Ruijter,
2+
# Copyright (C) 2017-2026 NV Access Limited, Ethan Holliger, Dinesh Kaushal, Leonard de Ruijter,
33
# Joseph Lee, Julien Cochuyt, Łukasz Golonka, Cyrille Bougot
44
# This file may be used under the terms of the GNU General Public License, version 2 or later.
55
# For more details see: https://www.gnu.org/licenses/gpl-2.0.html
@@ -54,8 +54,6 @@
5454
"right",
5555
"Error renaming profile.",
5656
"Use this profile for:",
57-
"This change requires administrator privileges.",
58-
"Insufficient Privileges",
5957
"word",
6058
"Taskbar",
6159
"invoke",

0 commit comments

Comments
 (0)