fix(u6): the booth check fails closed with a reason, and a dead write leaves no scratch
Two more from the in-session adversarial pass. `booth link`'s new booth-URL check shells out to booth/links.py. When that import cannot run, the command substitution under `set -e` aborted the script with a bare ModuleNotFoundError traceback: the right DIRECTION (no row was appended — a guard that fails open is not a guard) reached by accident, and unactionable when it fires. Handled explicitly now: exit 3, and a message naming what the check needs. The fail-closed direction is stated rather than inherited from shell semantics, and a test pins it — the defeating change in either direction goes red. _write_all's scratch file was stranded beside the registry if the write died between create and replace. Cleaned up on every exit path. The prior registry was never at risk either way: os.replace is the only thing that publishes. Also pins normalization idempotence, which `bench state <id|url>` and `bench rm <id|url>` both rely on: they normalize whatever they are handed, so an id that did not normalize to itself would miss the row it names.
This commit is contained in:
+12
-2
@@ -258,9 +258,19 @@ def _write_all(root: Path, benches: dict[str, Bench]) -> None:
|
||||
# of one fact is two things that can disagree.
|
||||
for b in benches.values()
|
||||
}
|
||||
# Per-pid scratch name so two writers cannot share it: the atomic-replace
|
||||
# promise is that a READER never sees a partial file, not that two writers
|
||||
# never collide on the way there.
|
||||
tmp = path.with_suffix(path.suffix + f".tmp.{os.getpid()}")
|
||||
tmp.write_text(json.dumps(payload, indent=2, sort_keys=True) + "\n")
|
||||
os.replace(tmp, path)
|
||||
try:
|
||||
tmp.write_text(json.dumps(payload, indent=2, sort_keys=True) + "\n")
|
||||
os.replace(tmp, path)
|
||||
except BaseException:
|
||||
# A write that dies between create and replace would otherwise strand
|
||||
# the scratch file beside the registry forever. The prior registry is
|
||||
# untouched either way — os.replace is the only thing that publishes.
|
||||
tmp.unlink(missing_ok=True)
|
||||
raise
|
||||
|
||||
|
||||
class _Locked:
|
||||
|
||||
Reference in New Issue
Block a user