Plugin lifecycle: the installer refuses four kinds, changes reach one process, sync loses state, nothing rolls back #228
Labels
No labels
accessibility
authentication
breaking change
bug
documentation
enhancement
interface
internationalisation
observability
security
tier
1
tier
2
tier
3
tier/4
No project
No assignees
1 participant
Notifications
Due date
No due date set.
Dependencies
No dependencies set.
Reference
Postulo/postulo#228
Loading…
Add table
Add a link
Reference in a new issue
No description provided.
Delete branch "%!s()"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
Installing, disabling and restoring plugins has gaps that contradict Writing a plugin and the plugins page. Found in the 2026-09-15 code audit.
1. The installer refuses transport, outbox, feature and importer plugins
plugins/installing.py:66hasPLUGIN_GROUPS = (sources, notifiers, stores, syncs)._plugin_entry_points(:306) ignores every other group, socheck()refuses a transport-only wheel with "declares no Postulo entry point"._forget_metadata_cache(:778) refreshes only those four kinds.notifications/text.py:13-18("the package is somebody else's"). No test covers the other groups.Fix: derive the list from the non-empty values of
registry.GROUPS, refresh every group, and add a test per kind.2. Plugin changes reach only the process that made them
registry._cacheis per process. Install, disable and remove refresh it only in the worker that served the request (installing.py:773-782,core/server_views.py:1285-1286).service.pysilently skips it.activate()runs only atready(), so on a first install the directory never reaches the other workers' import path.server_views.py:1205) is not true.Fix: a generation stamp (the
plugins.jsonmtime, or a database or cache key) checked inplugins(); re-runactivate()and rebuild when it changes. The scheduler checks once per pass.3.
plugins syncrestores the wrong stateinstalling.py:742-749callsinstall_wheelwithoutdisabledorrequires_postulo, so a disabled plugin comes back enabled and the compatibility marker is reset.management/commands/plugins.py:121-123) downloadslisting.latestbut checks it against the recordedentry.sha256, so restoring from a catalogue fails as soon as the catalogue has moved on.Fix: carry every recorded field through, and fetch the recorded version's release.
4. No staging, no rollback, leftover dependencies
run_installdoes--target <live dir> --upgradein place (:469-505), with no staging directory and no import check, so a broken upgrade replaces the working version.remove()deletes only the plugin's own RECORD files (:615-636); its dependencies stay importable.Fix: install into a staging directory, import every entry point in a subprocess, then swap and keep the previous version. Reference-count dependencies before removing them.
5. A plugin the registry rejects still registers its templates and themes
registry.py:196-199registers locale and themes before the interface and kind checks (:201-216). Third-party imports also happen lazily inside whichever request first callsplugins(), guarded only byexcept Exception, so aSystemExitor a hang at import reaches a live request.Fix: register after the checks, load every group eagerly at
ready()so failures surface at boot, and guardBaseExceptionexceptKeyboardInterrupt.6. Plugins cannot own tables or pages, though the docs say they can
CONTRIBUTING.md:558says to ship your own migrations and be inINSTALLED_APPS, butINSTALLED_APPSis fixed with no hook.POSTULO_SKIP_MIGRATE.server_views.py:1205andplugins.py:88promise "pages of its own" after a restart.Fix: either add
postulo.appsandpostulo.urlsentry-point groups (read at settings import afteractivate(), withmigratein the entrypoint), or state plainly that a plugin with state or pages must be built into the image.