Skip to content

Fix corrupted config error#15691

Merged
seanbudd merged 13 commits into
nvaccess:masterfrom
CyrilleB79:fixCorruptedConfigError
Nov 12, 2023
Merged

Fix corrupted config error#15691
seanbudd merged 13 commits into
nvaccess:masterfrom
CyrilleB79:fixCorruptedConfigError

Conversation

@CyrilleB79

@CyrilleB79 CyrilleB79 commented Oct 26, 2023

Copy link
Copy Markdown
Contributor

Link to issue number:

Fixes #15690

Summary of the issue:

Critical error when starting NVDA with a corrupted config file.

During config initialization, an error is logged. To check if it should play an error sound, NVDA imports nvwave, which in turn imports nvdaHelper, which imports versionInfo, where "_" is not yet defined since translations are not initialized.

Description of user facing changes

If the configuration file is corrupted, NVDA will not fail starting anymore, but will reinitialize the configuration to default as it did in NVDA 2023.1 and before.

Description of development approach

  • First I have used buildVersion module in nvdaHelper instead of versionInfo since the latter contains localized information that we do not use. But it was not enough since "_" (underscore is still missing when importing eventHandler or api
  • Thus, I have implemented an extension point notified when an error is logged and with a handler responsible of playing the error sound. This handler is registered when nvwave audio is iinitialized. More details in Fix corrupted config error #15691 (review).

Also, when investigating the issue and working on this PR, I have fixed the following minor issues:

  • Removed two lines log.debugWarning("", exc_info=True), and used the exc_info in the log.warning just above. Indeed, during config initialization, log level is not yet known and thus the exc_info information is not known during startup if needed.
  • Do not provide a write function when loading the config if the config file name is None since it just generates an erronous warning indicating that I am using a read only file system.

Additional questions and notes

  • Should I implement a system test to avoid such issues in the future?
  • @lukaszgo1, since you have worked on import order in the past, your review would be appreciated if possible.
  • I have not checked, but it has probably been introduced with Support for audio output using WASAPI #14697 or other WASAPI PRs. Cc @jcsteh

Testing strategy:

Tested STR of #15690.

Known issues with pull request:

Code Review Checklist:

  • Documentation:
    • Change log entry
    • User Documentation
    • Developer / Technical Documentation
    • Context sensitive help for GUI changes
  • Testing:
    • Unit tests
    • System (end to end) tests
    • Manual testing
  • UX of all users considered:
    • Speech
    • Braille
    • Low Vision
    • Different web browsers
    • Localization in other languages / culture than English
  • API is compatible with existing add-ons.
  • Security precautions taken.

@CyrilleB79 CyrilleB79 force-pushed the fixCorruptedConfigError branch from eeaaab4 to 134c8eb Compare October 26, 2023 15:13

@lukaszgo1 lukaszgo1 left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

While what has been implemented here certainly fixes the issue at hand, I have a feeling that the underlying problem still remains. This is by no means a criticism, (we always solved circular imports using lazy importing), but my personal opinion is that whenever possible code should be restructured to avoid these. In this particular case NVDAHelper clearly requires importing way too much, (most of its uses only requires localLib) and should be split, however this is out of scope for this PR, and should be done in a dedicated one. the following would improve this particular situation without reorganizing too much code:
Currently logHandler requires nvwave to play sounds, yet it can only do so when nvwave is initialized. Rather than importing it lazily in the logHandler I'd defer the decision if the sound can be played, and the actual act of playing it to nvwave, by adding an extension point to the logHandler (something like onErrorLogged). When initializing nvwave it should register a function with this extension point, and in it check if nvwave is not in error, if it is not just play the error sound.
There are two points worth keeping in mint with this design:

  • extensionPoints imports logHandler, so we are trading one circular import for another. Having said that we would have only one lazy import as opposed to around 10 in nvwave, and several more in logHandler, so I'd say this is a right move. To make this work the extension point in logHandler has to be retrieved with a module level getter function where extensionPoints would be lazily imported (I'd suggest using functools.lru_cache with a cache size set to 1 to create it first time it is needed, and then return cached instance)
  • nvwave should be terminated in core, to make sure the function responsible for playing error sounds is not registered multiple times

In addition having system test for this issue would probably be helpful.

Comment thread source/config/profileUpgrader.py Outdated
@CyrilleB79

Copy link
Copy Markdown
Contributor Author

@lukaszgo1 thanks for the review and the design suggestion. @seanbudd I would like to hear your opinion on this design before implementing it; thus, I'll put this PR as ready for review, but I am totally open to the design change suggested by @lukaszgo1 if it goes in the sense of code simplification.

I have some remaining questions regarding this design:

  • Do you suggest to remove the check on nvwave.isInError from shouldPlayErrorSound and to put it in the new handler registered to the new extensions point?
  • The pattern with __getattr__ at module level to import lazily the extension point point is not common in NVDA's code. Does it not make the code more complex? It also prevents the discoverability of this extension point.
  • Also, I have not understood the goal for caching extensionPoints; can you help me clarify

@lukaszgo1

Copy link
Copy Markdown
Contributor
  • Do you suggest to remove the check on nvwave.isInError from shouldPlayErrorSound and to put it in the new handler registered to the new extensions point?

Yes - the fact that nvwave is in error is its internal details, and in general the responsibility for playing error sound or not is entirely in the module which has the functionality to play it

  • The pattern with __getattr__ at module level to import lazily the extension point point is not common in NVDA's code. Does it not make the code more complex? It also prevents the discoverability of this extension point.

I'm sorry - I haven't made myself clear enough here. What I proposed was not a __getattr__, but a normal function (something like getOnErrorLogged) in the logHandler. This function lazily imports extensionPoints, creates the onErrorLogged action, and returns it

  • Also, I have not understood the goal for caching extensionPoints; can you help me clarify

It is not extensionPoints which should be cached - rather the created instance of onErrorLogged action should be created only once, and then just returned from the function in subsequent calls.

@CyrilleB79

Copy link
Copy Markdown
Contributor Author

Thanks @lukaszgo1 for all these indications.

nvwave should be terminated in core, to make sure the function responsible for playing error sounds is not registered multiple times

How can it be?

  • Do you suggest to remove the check on nvwave.isInError from shouldPlayErrorSound and to put it in the new handler registered to the new extensions point?

Yes - the fact that nvwave is in error is its internal details, and in general the responsibility for playing error sound or not is entirely in the module which has the functionality to play it

So it seems that shouldPlayErrorSound should also go to nvwave.py to be logical. This is an API breaking change unfortunately.
Do you agree with this.

@lukaszgo1

Copy link
Copy Markdown
Contributor

nvwave should be terminated in core, to make sure the function responsible for playing error sounds is not registered multiple times

How can it be?

In the same way as most of our existing modules i.e. at the bottom of core.main it should be passed to the _terminate function. You would have to add nvwave.terminate to make this work.

  • Do you suggest to remove the check on nvwave.isInError from shouldPlayErrorSound and to put it in the new handler registered to the new extensions point?

Yes - the fact that nvwave is in error is its internal details, and in general the responsibility for playing error sound or not is entirely in the module which has the functionality to play it

So it seems that shouldPlayErrorSound should also go to nvwave.py to be logical. This is an API breaking change unfortunately. Do you agree with this.

I haven't considered this before, but I believe shouldPlayErrorSound can remain in logHandler, but without checking of nvwave being in error or not. My thinking is that shouldPlayErrorSound is responsible for checking if user requested error sounds to be played, and the code in nvwave checks if it technically can be played or not.

@CyrilleB79

Copy link
Copy Markdown
Contributor Author

nvwave should be terminated in core, to make sure the function responsible for playing error sounds is not registered multiple times

How can it be?

In the same way as most of our existing modules i.e. at the bottom of core.main it should be passed to the _terminate function. You would have to add nvwave.terminate to make this work.

Sorry, I meant:
How can the function responsible for playing error sound be registered multiple times in case I do not create a terminate function to unregister it?

  • Do you suggest to remove the check on nvwave.isInError from shouldPlayErrorSound and to put it in the new handler registered to the new extensions point?

Yes - the fact that nvwave is in error is its internal details, and in general the responsibility for playing error sound or not is entirely in the module which has the functionality to play it

So it seems that shouldPlayErrorSound should also go to nvwave.py to be logical. This is an API breaking change unfortunately. Do you agree with this.

I haven't considered this before, but I believe shouldPlayErrorSound can remain in logHandler, but without checking of nvwave being in error or not. My thinking is that shouldPlayErrorSound is responsible for checking if user requested error sounds to be played, and the code in nvwave checks if it technically can be played or not.

This makes sense. Maybe I will have to find more adapted names than _onErrorLogged / _getOnErrorLogged...

@AppVeyorBot

Copy link
Copy Markdown

See test results for failed build of commit b75a423672

@lukaszgo1

Copy link
Copy Markdown
Contributor

Sorry, I meant: How can the function responsible for playing error sound be registered multiple times in case I do not create a terminate function to unregister it?

I assumed, without looking at the code :-( that nvwave is initialized again in core.resetConfiguration, but it isn't. You can ignore this part i.e. there is no need to terminate it in that case.

@CyrilleB79 CyrilleB79 marked this pull request as ready for review November 8, 2023 12:16
@CyrilleB79 CyrilleB79 requested a review from a team as a code owner November 8, 2023 12:16
@CyrilleB79 CyrilleB79 requested a review from seanbudd November 8, 2023 12:16

@lukaszgo1 lukaszgo1 left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please update the initial comment, to describe the new approach.
Also I take back my suggestion about system tests. I haven't realized that they rely on scratchpad being enabled, and that cannot be done when the provided config is not valid. It should be possible to convert them into an add-on, and set the necessary config from it, rather than just providing the preset config file, but that is certainly something for another day.

Comment thread source/logHandler.py
Comment thread source/core.py
Comment thread source/logHandler.py
nvwave.playWaveFile(os.path.join(globalVars.appDir, "waves", "error.wav"))
except:
pass
getOnErrorSoundRequested().notify()

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I would have preferred a more global "pre_errorLogged" extension point, which could be more useful for add-on authors (including myself with NVDA Dev & Test Toolbox). But I am not able to define one cleanly without reintroducing more lazy imports.

@XLTechie, @lukaszgo1 any opinion on this?

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That is probably something for a separate issue / PR, but if there is a need to have more extension points in the logHandler then the circular import between it and extensionPoints has to be fixed. My first thought would be to expose an additional way to import logHandler.log (perhaps from globalVars).

Comment thread source/logHandler.py
log.warning("Error saving configuration; probably read only file system")
log.debugWarning("", exc_info=True)
pass
except PermissionError:

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

are we certain no other exceptions are thrown here? wouldn't it be safer to continue to catch other exceptions and log at exception level

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actually, other exceptions are thrown here and it was erroneous to catch all here to issue a so specific error message related to read only file system.

For an example of other exception thrown here, in NVDA 2023.3, press 3 times NVDA+control+R to restore default settings. You will get the following inappropriate warning message in the log:

WARNING - config.profileUpgrader.upgrade (09:35:01.738) - MainThread (15004):
Error saving configuration; probably read only file system

Thus I have narrowed the error filtering to PermissionError as it is already done in source/config/__init__.py for the same logged message.

If there are other exceptions that were previously catched by the filtering on Exception, they should be reported and fixed, not filtered out with a single warning.

Comment thread source/core.py
Comment thread source/logHandler.py
Comment thread source/logHandler.py Outdated
@seanbudd seanbudd merged commit 5709ce6 into nvaccess:master Nov 12, 2023
@nvaccessAuto nvaccessAuto added this to the 2024.1 milestone Nov 12, 2023
@CyrilleB79 CyrilleB79 deleted the fixCorruptedConfigError branch November 13, 2023 14:02
seanbudd pushed a commit that referenced this pull request Jan 10, 2024
)

Fix-up of #15691.

Summary of the issue:
In #15691, a new extension point has been created. Unfortunately I have forgotten to document it.

Description of user facing changes
Document this extension point in the dev guide as well as in the change log.
Adriani90 pushed a commit to Adriani90/nvda that referenced this pull request Mar 13, 2024
…ccess#16017)

Fix-up of nvaccess#15691.

Summary of the issue:
In nvaccess#15691, a new extension point has been created. Unfortunately I have forgotten to document it.

Description of user facing changes
Document this extension point in the dev guide as well as in the change log.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

With a corrupted config file, NVDA restarts forever

5 participants