Skip to content

Remove add-on modules imported in install tasks to avoid conflicts between add-ons - #15967

Merged
seanbudd merged 2 commits into
nvaccess:masterfrom
lukaszgo1:cleanerInstallTasks
Dec 27, 2023
Merged

Remove add-on modules imported in install tasks to avoid conflicts between add-ons#15967
seanbudd merged 2 commits into
nvaccess:masterfrom
lukaszgo1:cleanerInstallTasks

Conversation

@lukaszgo1

Copy link
Copy Markdown
Contributor

Link to issue number:

Fixes regression from PR #14481.
Discovered due to intermittent failures occurring when installing add-ons, when working on #15852 and #15937

Summary of the issue:

When installing and uninstalling add-ons bundling install tasks, installation either fails or behaves unexpectedly in the following scenarios:

1:

  • User installs an add-on
  • User removes it and restarts NVDA
  • User tries to install the same add-on

In this scenario when installing the install tasks included in the version of add-on which was just removed are used, which is unexpected. Note that this worked well before #14481, not sure why pkgutil.ImpImporter behaves better (I haven't investigated, as it is deprecated).

2:

  • User installs an add-on, which imports its sub module in install tasks using loadModule and also imports a sub module in the same way inside global plugin / app module during normal operation
  • User upgrades the add-on to a different version

In this scenario after upgrade the add-on module from the uninstalled version is used during normal operation of NVDA.

3:

  • User installs an add-on which imports one of its modules in install tasks by manipulating sys.path, for real life examples see Instant Translate and Mouse Wheel Scrolling
  • User uninstalls this add-on, restarts NVDA, and tries to install the new version

In this scenario installation fails since the imported module from the old version is used. A slight variation of this scenario is to try to install both Instant Translate and Mouse Wheel Scrolling the add-on installed last would fail to be installed, since they both bundle module named donate_dialog but with different functions

Description of user facing changes

Add-ons are successfully installed in all scenarios described above.

Description of development approach

All these problems exist because when importing modules in Python if module with the same name exists in sys.modules it is just used without importing it again. To ensure modules are always re-imported when needed, the following has been done:

  1. When importing modules we no longer use add-on name to create a name space package in which the module would be imported, the last segment of the add-on path is used instead. This ensures that the package for add-on which is installed would just be its name, and for add-on which is pending install it would end with the pending install suffix. This fixes issue 1.
  2. loadModule memorizes all modules it imported for a given add-on. After installation / removal is done they're removed from sys.modules. This fixes issue 2.
  3. To remove cached modules imported with the standard import command, we store names of all modules imported before installation / removal, perform it, create list of newly imported modules and remove all newly added modules which belong to the add-on from the cache. This fixes issue 3.

Testing strategy:

Executed scenarios from above. Inspected sys.modules afterwards to make sure no add-on modules remained after add-on removal, and that after installation install tasks are also not present in the modules cache.

Known issues with pull request:

I've decided not to add a change log entry since issues fixed by this PR occur pretty rarely (I have encountered them only because I have been installing / removing the same add-ons over and over without restarting NVDA when working on another PRs) and describing what exactly has been fixed in a concise way seems difficult here.

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.

@lukaszgo1
lukaszgo1 requested a review from a team as a code owner December 26, 2023 21:36
@lukaszgo1
lukaszgo1 requested a review from seanbudd December 26, 2023 21:36
@lukaszgo1

Copy link
Copy Markdown
Contributor Author

@LeonarddeR Would you be able to verify if the changes to loadModule introduced here will not regress your use cases?

@seanbudd seanbudd left a comment

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.

Thanks @lukaszgo1 - should I wait until testing from @LeonarddeR before merging?

Comment thread requirements.txt Outdated
Comment thread source/systemUtils.py Outdated
@lukaszgo1

Copy link
Copy Markdown
Contributor Author

should I wait until testing from @LeonarddeR before merging?

I've pinged them just because I don't know anyone else who is using loadModule for anything else than loading install tasks.
If you're happy with the approach this can probably be merged now, as these changes are very low risk.

@LeonarddeR

Copy link
Copy Markdown
Collaborator

I don't think this code will introduce regressions for my use cases.
That said, the namespace package structure allows you to do from addons.addonName import something. While that is not documented somewhere I believe, it's a pitty that this would break after this pr.

@lukaszgo1

Copy link
Copy Markdown
Contributor Author

That said, the namespace package structure allows you to do from addons.addonName import something. While that is not documented somewhere I believe, it's a pitty that this would break after this pr.

This way of importing is new to me. Unless I missed something it can work only if you've imported at least one module from your add-on using loadModule right? If so this will still work - imported modules are cleaned-up only after given add-on is removed or installed, not after each time loadModule is used.

@LeonarddeR

Copy link
Copy Markdown
Collaborator

This is correct indeed.

@LeonarddeR

Copy link
Copy Markdown
Collaborator

I've decided not to add a change log entry since issues fixed by this PR occur pretty rarely (I have encountered them only because I have been installing / removing the same add-ons over and over without restarting NVDA when working on another PRs) and describing what exactly has been fixed in a concise way seems difficult here.

I guess something like the following should suffice:

  • Fixed a rare issue where installing an add-on could go wrong when that add-on was removed before.

@seanbudd
seanbudd merged commit 314cd6b into nvaccess:master Dec 27, 2023
@nvaccessAuto nvaccessAuto added this to the 2024.2 milestone Dec 27, 2023
@lukaszgo1
lukaszgo1 deleted the cleanerInstallTasks branch December 28, 2023 12:25
Nael-Sayegh pushed a commit to Nael-Sayegh/nvda that referenced this pull request Feb 15, 2024
…tween add-ons (nvaccess#15967)

Fixes regression from PR nvaccess#14481.
Discovered due to intermittent failures occurring when installing add-ons, when working on nvaccess#15852 and nvaccess#15937

Summary of the issue:
When installing and uninstalling add-ons bundling install tasks, installation either fails or behaves unexpectedly in the following scenarios:

1:
User installs an add-on
User removes it and restarts NVDA
User tries to install the same add-on
In this scenario when installing the install tasks included in the version of add-on which was just removed are used, which is unexpected. Note that this worked well before nvaccess#14481, not sure why pkgutil.ImpImporter behaves better (I haven't investigated, as it is deprecated).

2:
User installs an add-on, which imports its sub module in install tasks using loadModule and also imports a sub module in the same way inside global plugin / app module during normal operation
User upgrades the add-on to a different version
In this scenario after upgrade the add-on module from the uninstalled version is used during normal operation of NVDA.

3:
User installs an add-on which imports one of its modules in install tasks by manipulating sys.path, for real life examples see Instant Translate and Mouse Wheel Scrolling
User uninstalls this add-on, restarts NVDA, and tries to install the new version
In this scenario installation fails since the imported module from the old version is used. A slight variation of this scenario is to try to install both Instant Translate and Mouse Wheel Scrolling the add-on installed last would fail to be installed, since they both bundle module named donate_dialog but with different functions

Description of user facing changes
Add-ons are successfully installed in all scenarios described above.

Description of development approach
All these problems exist because when importing modules in Python if module with the same name exists in sys.modules it is just used without importing it again. To ensure modules are always re-imported when needed, the following has been done:

When importing modules we no longer use add-on name to create a name space package in which the module would be imported, the last segment of the add-on path is used instead. This ensures that the package for add-on which is installed would just be its name, and for add-on which is pending install it would end with the pending install suffix. This fixes issue 1.
loadModule memorizes all modules it imported for a given add-on. After installation / removal is done they're removed from sys.modules. This fixes issue 2.
To remove cached modules imported with the standard import command, we store names of all modules imported before installation / removal, perform it, create list of newly imported modules and remove all newly added modules which belong to the add-on from the cache. This fixes issue 3.
SaschaCowley pushed a commit to SaschaCowley/nvda that referenced this pull request Feb 27, 2024
…tween add-ons (nvaccess#15967)

Fixes regression from PR nvaccess#14481.
Discovered due to intermittent failures occurring when installing add-ons, when working on nvaccess#15852 and nvaccess#15937

Summary of the issue:
When installing and uninstalling add-ons bundling install tasks, installation either fails or behaves unexpectedly in the following scenarios:

1:
User installs an add-on
User removes it and restarts NVDA
User tries to install the same add-on
In this scenario when installing the install tasks included in the version of add-on which was just removed are used, which is unexpected. Note that this worked well before nvaccess#14481, not sure why pkgutil.ImpImporter behaves better (I haven't investigated, as it is deprecated).

2:
User installs an add-on, which imports its sub module in install tasks using loadModule and also imports a sub module in the same way inside global plugin / app module during normal operation
User upgrades the add-on to a different version
In this scenario after upgrade the add-on module from the uninstalled version is used during normal operation of NVDA.

3:
User installs an add-on which imports one of its modules in install tasks by manipulating sys.path, for real life examples see Instant Translate and Mouse Wheel Scrolling
User uninstalls this add-on, restarts NVDA, and tries to install the new version
In this scenario installation fails since the imported module from the old version is used. A slight variation of this scenario is to try to install both Instant Translate and Mouse Wheel Scrolling the add-on installed last would fail to be installed, since they both bundle module named donate_dialog but with different functions

Description of user facing changes
Add-ons are successfully installed in all scenarios described above.

Description of development approach
All these problems exist because when importing modules in Python if module with the same name exists in sys.modules it is just used without importing it again. To ensure modules are always re-imported when needed, the following has been done:

When importing modules we no longer use add-on name to create a name space package in which the module would be imported, the last segment of the add-on path is used instead. This ensures that the package for add-on which is installed would just be its name, and for add-on which is pending install it would end with the pending install suffix. This fixes issue 1.
loadModule memorizes all modules it imported for a given add-on. After installation / removal is done they're removed from sys.modules. This fixes issue 2.
To remove cached modules imported with the standard import command, we store names of all modules imported before installation / removal, perform it, create list of newly imported modules and remove all newly added modules which belong to the add-on from the cache. This fixes issue 3.
Adriani90 pushed a commit to Adriani90/nvda that referenced this pull request Mar 13, 2024
…tween add-ons (nvaccess#15967)

Fixes regression from PR nvaccess#14481.
Discovered due to intermittent failures occurring when installing add-ons, when working on nvaccess#15852 and nvaccess#15937

Summary of the issue:
When installing and uninstalling add-ons bundling install tasks, installation either fails or behaves unexpectedly in the following scenarios:

1:
User installs an add-on
User removes it and restarts NVDA
User tries to install the same add-on
In this scenario when installing the install tasks included in the version of add-on which was just removed are used, which is unexpected. Note that this worked well before nvaccess#14481, not sure why pkgutil.ImpImporter behaves better (I haven't investigated, as it is deprecated).

2:
User installs an add-on, which imports its sub module in install tasks using loadModule and also imports a sub module in the same way inside global plugin / app module during normal operation
User upgrades the add-on to a different version
In this scenario after upgrade the add-on module from the uninstalled version is used during normal operation of NVDA.

3:
User installs an add-on which imports one of its modules in install tasks by manipulating sys.path, for real life examples see Instant Translate and Mouse Wheel Scrolling
User uninstalls this add-on, restarts NVDA, and tries to install the new version
In this scenario installation fails since the imported module from the old version is used. A slight variation of this scenario is to try to install both Instant Translate and Mouse Wheel Scrolling the add-on installed last would fail to be installed, since they both bundle module named donate_dialog but with different functions

Description of user facing changes
Add-ons are successfully installed in all scenarios described above.

Description of development approach
All these problems exist because when importing modules in Python if module with the same name exists in sys.modules it is just used without importing it again. To ensure modules are always re-imported when needed, the following has been done:

When importing modules we no longer use add-on name to create a name space package in which the module would be imported, the last segment of the add-on path is used instead. This ensures that the package for add-on which is installed would just be its name, and for add-on which is pending install it would end with the pending install suffix. This fixes issue 1.
loadModule memorizes all modules it imported for a given add-on. After installation / removal is done they're removed from sys.modules. This fixes issue 2.
To remove cached modules imported with the standard import command, we store names of all modules imported before installation / removal, perform it, create list of newly imported modules and remove all newly added modules which belong to the add-on from the cache. This fixes issue 3.
seanbudd added a commit that referenced this pull request Jul 15, 2024
Fixes #16704
Fixup of #15967

Summary of the issue:
In the following code block:

release-2024.2/source/addonHandler/__init__.py#L432-L454

If an error is raised from running addon.runInstallTask("onInstall"), it is caught, and then re-raised as AddonError
However, this exception is caught again by the finally statement returning the add-on
returning from finally suppresses the exception. This means the callers don't correctly handle the expected exception, as it is suppressed, where it should instead prevent the installation from continuing.
We have to return the add-on to perform clean up tasks, but we have to know exceptions were raised to cancel the add-on installation
Description of user facing changes
Add-on installation failures should fail gracefully

Description of development approach
Instead of raising exceptions, store them and exit the installation process when a failure occurs.
If exceptions are stored, cancel the installation and perform the require clean-up tasks
seanbudd pushed a commit that referenced this pull request Aug 26, 2026
Fixup for #18971.
Summary of the issue:

Installing or removing an add-on can run install tasks. These tasks can import modules from the add-on. Python caches every import in sys.modules. NVDA must drop the add-on's modules from that cache afterwards. Otherwise a later import returns the cached old module instead of the file on disk.

Modules the tasks load directly are tracked in _importedAddonModules and dropped reliably. Modules imported indirectly, for example through a relative import inside the add-on, are not tracked. For those, _cleanupAddonImports scans every module imported during the task and drops each one whose file is inside the add-on directory.

That scan has been broken twice:

    Remove add-on modules imported in install tasks to avoid conflicts between add-ons #15967 introduced it reading module.__file__ directly. Modules without a __file__, such as built-in modules, made it crash with AttributeError.
    Fix bug when updating add-ons automatically #18971 fixed that crash by reading module.__name__ instead. A module name is not a file path, so the scan matches nothing. Since 2026.1 it drops nothing.

Result: updating an add-on can break on the first start after the update. The old version's uninstall task runs first and leaves old modules in the cache. The new version then partly imports old code. A second restart of NVDA resolves the situation regardless.
Description of user facing changes:

Updated add-ons that perform install tasks no longer raise intermittent errors on the first start of NVDA after the update.
Description of developer facing changes:

None.
Description of development approach:

The approach of #15967 was the correct one: a module belongs to the add-on when its file is inside the add-on directory. This change returns to matching on the file path and fixes the three defects of that implementation:

    The file is read with getattr(module, "__file__", None). Modules without a __file__ are skipped instead of crashing the scan, so the error fixed by Fix bug when updating add-ons automatically #18971 stays fixed.
    The file path and the add-on directory are compared through os.path.normcase, so case differences match.
    The directory prefix ends with a path separator, so a sibling directory such as myAddonExtra no longer matches myAddon.

Each defect is covered by a unit test. The loop over the tracked modules is unchanged.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants