Plugins record: plugins.json is rewritten in place, and an unreadable record switches every plugin back on #382
Labels
No labels
accessibility
authentication
breaking change
bug
documentation
enhancement
interface
internationalisation
observability
performance
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#382
Loading…
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?
Found on 2026-09-29 in a read-only review of postulo at
95348908d, by reading how the record is written and read and simulating a write that fails part way in a test.Checked again on 2026-10-01 against
mainat0a5cad587: still present. The code is now atplugins/record.py:162-180(write_record, thepath.write_text(...)at :171),plugins/record.py:106-129(read_record, theexceptat :120-121) andplugins/record.py:208-210(disabled_names), moved out ofplugins/installing.pyby #248 and still handed out under theinstallingnames; the read-modify-write callers inplugins/installing.pyare nowinstall_wheel(:809-810),remove(:860),set_disabled(:949-953) andbackfill_metadata(:999).plugins/registry.pyis where it was.write_record()truncatesplugins.jsonand writes it again in place, andread_record()turns any parse error into "nothing installed". A write that fails half way, for example on a full disk, leaves a truncated record behind; from then on no plugin counts as disabled and none counts as installed. Every read-modify-write of the record is also done without a lock.plugins/installing.py:270-288(write_record:path.write_text(...)at :279);plugins/installing.py:214-237(read_record:except (OSError, ValueError): return []at :228-229);plugins/installing.py:316-318(disabled_names);plugins/registry.py:94-102(_disabled) and:189-192(_load_third_partyskips only those names); the read-modify-write callersinstall_wheel(:983-984),remove(:1034),set_disabled(:1123-1127),backfill_metadata(:1173).postulo-adisabled, then made the next write fail withENOSPCafter 40 bytes:set_disabledraised, the file was left as{"version": 1, "plugins": [{anddisabled_names()becameset(),read_record()[],installed("postulo-a")None.registry.catch_up()rebuilds in every web worker and in the scheduler, and every plugin an administrator switched off loads again.plugins syncat the next upgrade has nothing to restore.pluginscommand) lose one of them.fsyncit, andos.replace()it overplugins.json, then move the stamp. Hold an exclusive lock file inplugins_dir()around every read-modify-write and around install, removal and rollback. When the record exists but cannot be parsed, fail closed: log it, refuse writes, and have the registry keep the list it had (or treat every recorded distribution as disabled) rather than "nothing is disabled".Done when
tests/test_plugin_install.pythat makes the write ofplugins.jsonfail part way and asserts that the previous record is still readable, with its disabled plugin still disabled.registry._disabled()return an empty set.