Skip to content

Patch for Add-on Crashing on Startup/Install with Certain NVDA Profiles #21

@makhlwf

Description

@makhlwf

Hello paulber19,

Here is a patch to fix a critical bug that causes the NVDA Extension Global Plugin to crash.

The Core Problem

The add-on crashes when it tries to load an NVDA configuration profile (.ini file) where boolean settings are saved as strings (e.g., speakTypedCharacters = "True") instead of as proper booleans (speakTypedCharacters = True). This is common in older user profiles.

The crash occurs in two places:

  1. During Installation: The installTasks.py script fails when trying to clean up old settings from these invalid profiles.
  2. During NVDA Startup: The add-on's main settings module (settings/__init__.py) fails when it loads at startup for the same reason.

The Solution

The solution is to make the add-on's code more resilient. Instead of crashing, the code should catch this specific error, manually fix the user's invalid profile file on disk, and then proceed with its operation.

Here are the exact code changes needed in the two affected files:


File 1: installTasks.py

This file handles the installation and uninstallation logic. The fix prevents the installation from failing.

1. Add Necessary Imports:
At the top of the file, ensure os and configobj are imported.

from logHandler import log
import os
import configobj```

**2. Replace the `deleteAddonProfilesConfig` function:**
Replace the entire existing `deleteAddonProfilesConfig` function with the following corrected and robust version.

```python
def deleteAddonProfilesConfig(addonName):
	import config
	import globalVars
	conf = config.conf
	save = False

	# First, handle the default configuration profile
	if addonName in conf.profiles[0]:
		log.warning("%s section deleted from profile: normal configuration " % addonName)
		del conf.profiles[0][addonName]
		save = True

	# Now, iterate through all named profiles
	profileNames = list(config.conf.listProfiles())
	for name in profileNames:
		try:
			# Try to load the profile normally
			profile = config.conf._getProfile(name)
			# If it loads, clean it in memory
			if profile and profile.get(addonName):
				log.warning("%s section deleted from memory for profile: %s" % (addonName, name))
				del profile[addonName]
				config.conf._dirtyProfiles.add(name)
				save = True
		except ValueError as e:
			# If loading fails due to our specific error, handle it on disk
			if "VdtTypeError" in str(e) and ("speakTypedCharacters" in str(e) or "speakTypedWords" in str(e)):
				log.warning(f"Config for profile '{name}' is invalid. Fixing and cleaning on disk.")
				
				profile_path = os.path.join(globalVars.appArgs.configPath, "profiles", name + ".ini")
				
				if os.path.exists(profile_path):
					try:
						profile_config = configobj.ConfigObj(profile_path, interpolation=False, encoding='utf-8')
						made_changes = False

						# Step 1: Fix the known invalid keys
						keys_to_fix = ["speakTypedCharacters", "speakTypedWords"]
						if 'keyboard' in profile_config:
							for key in keys_to_fix:
								if key in profile_config['keyboard'] and isinstance(profile_config['keyboard'][key], str):
									log.warning(f"Fixing key '{key}' in profile '{name}'.")
									profile_config['keyboard'][key] = (profile_config['keyboard'][key].lower() == 'true')
									made_changes = True
						
						# Step 2: Remove the addon's configuration section from the file
						if addonName in profile_config:
							log.warning(f"Removing '{addonName}' section from disk for profile: {name}")
							del profile_config[addonName]
							made_changes = True

						# Step 3: Write all changes back to the file
						if made_changes:
							profile_config.write()
							log.warning(f"Profile '{name}' has been fixed and cleaned on disk.")
					except Exception as fix_error:
						log.error(f"Failed to manually fix/clean the configuration for profile '{name}': {fix_error}")
						raise e
				else:
					log.warning(f"Cannot fix profile '{name}', .ini file not found at {profile_path}")

			else:
				# It's a different ValueError, so we should not suppress it.
				raise e

	# Save any changes made to profiles that were successfully loaded into memory
	if save:
		config.conf.save()

File 2: globalPlugins/NVDAExtensionGlobalPlugin/settings/__init__.py

This file handles the add-on's settings during normal operation. This fix prevents the add-on from crashing when NVDA starts.

1. Add Necessary Imports:
At the top of the file, add an import for configobj.

# ... other imports
import configobj
from configobj.validate import Validator, VdtTypeError
# ... other imports

2. Replace the restoreAddonProfilesConfig function:
Replace the entire existing restoreAddonProfilesConfig function with the following corrected version.

	def restoreAddonProfilesConfig(self, addonName):
		conf = config.conf
		save = False
		# Handle the default configuration profile first
		if "%s-temp" % addonName in conf.profiles[0]:
			log.warning("restoreAddonProfilesConfig profile[0]")
			conf.profiles[0][addonName] = conf.profiles[0]["%s-temp" % addonName].copy()
			del conf.profiles[0]["%s-temp" % addonName]
			save = True

		# Now, iterate through all named profiles
		profileNames = list(config.conf.listProfiles())
		for name in profileNames:
			profile = None
			try:
				# Try to load the profile normally
				profile = config.conf._getProfile(name)
			except ValueError as e:
				# If loading fails due to our specific error, handle it on disk
				if "VdtTypeError" in str(e) and ("speakTypedCharacters" in str(e) or "speakTypedWords" in str(e)):
					log.warning(f"Config for profile '{name}' is invalid during startup. Fixing it on disk.")
					
					profile_path = os.path.join(globalVars.appArgs.configPath, "profiles", name + ".ini")
					
					if os.path.exists(profile_path):
						try:
							profile_config = configobj.ConfigObj(profile_path, interpolation=False, encoding='utf-8')
							made_changes = False

							# Fix the known invalid keys
							keys_to_fix = ["speakTypedCharacters", "speakTypedWords"]
							if 'keyboard' in profile_config:
								for key in keys_to_fix:
									if key in profile_config['keyboard'] and isinstance(profile_config['keyboard'][key], str):
										log.warning(f"Fixing key '{key}' in profile '{name}'.")
										profile_config['keyboard'][key] = (profile_config['keyboard'][key].lower() == 'true')
										made_changes = True

							if made_changes:
								profile_config.write()
								log.warning(f"Profile '{name}' has been fixed on disk.")
								# Retry loading the profile now that it's fixed
								profile = config.conf._getProfile(name)
						except Exception as fix_error:
							log.error(f"Failed to manually fix the configuration for profile '{name}': {fix_error}")
					else:
						log.warning(f"Cannot fix profile '{name}', .ini file not found at {profile_path}")
				else:
					# It's a different ValueError, re-raise it.
					raise e
			
			# Now, with a valid profile, proceed with restoring the addon's config
			if profile and profile.get("%s-temp" % addonName):
				log.warning("restoreAddonProfilesConfig: profile %s" % name)
				profile[addonName] = profile["%s-temp" % addonName].copy()
				del profile["%s-temp" % addonName]
				config.conf._dirtyProfiles.add(name)
				save = True

		# We save the configuration if changes were made
		if save:
			config.conf.save()

By applying these two changes, the add-on will become significantly more stable for all users, especially those who have been using NVDA for a long time.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions