Commit 7db70bd
committed
refactor(hooks): atomic staging-based lib swap + drop redundant chmod (#1490)
Addresses two EVAL findings from the PR #1490 review:
**HIGH #1 — race window during lib sync**
The previous implementation was rmtree-then-copytree:
if target_lib.exists():
shutil.rmtree(target_lib)
shutil.copytree(source_lib, target_lib, ...)
which left ``target_lib`` missing for the full duration of the
copytree (hundreds of milliseconds for a multi-megabyte lib). Any
concurrent HUD subprocess that statted the installed script during
that window would see ``ImportError`` and render the fallback face —
ironic given this PR exists to fix exactly that symptom.
Replaced with a staging/rename pattern:
1. ``copytree(source_lib, .lib.staging-<uuid>, ignore=...)``
2. ``rename(target_lib, .lib.old-<uuid>)`` ← atomic syscall
3. ``rename(.lib.staging-<uuid>, target_lib)`` ← atomic syscall
4. ``rmtree(.lib.old-<uuid>)`` in ``finally``
The window during which ``target_lib`` does not exist now shrinks
from ``O(copytree)`` to ``O(rename)`` — effectively atomic on POSIX.
Concurrent installers from multiple simultaneous Claude Code
sessions are safe because each uses a uuid-scoped staging dir.
Rollback: if ``copytree`` raises mid-sync the except block removes
the staging dir and restores ``target_lib`` from the archive, so an
existing working install is never lost to a partial sync.
**HIGH #2 — redundant chmod after rename in _install_hook_with_lib**
``_atomic_sync_with_lib`` already chmod's the synced script to 0o755
before ``_install_hook_with_lib`` renames it to HOOK_FILENAME, and
POSIX rename preserves permission bits. The second chmod was
defensive but triggered an unnecessary silent-failure path on
filesystems that reject mode changes (NFS, read-only mounts).
Removed with a code comment explaining why.
**New regression tests (+2)**:
- ``test_no_staging_leftovers_after_success`` — no stray
``.lib.staging-*`` / ``.lib.old-*`` dirs after a successful sync
- ``test_rollback_preserves_old_lib_when_copytree_fails`` —
monkeypatches ``shutil.copytree`` to raise OSError, then asserts
the pre-existing target lib survives and no staging/archive
dirs leak
Local verification: 105 pytest suites pass (100 + 5 from prior
commit + 2 new race-safety gates).
Refs #14901 parent 903aded commit 7db70bd
2 files changed
Lines changed: 142 additions & 22 deletions
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
426 | 426 | | |
427 | 427 | | |
428 | 428 | | |
429 | | - | |
| 429 | + | |
430 | 430 | | |
431 | 431 | | |
432 | | - | |
433 | | - | |
434 | | - | |
435 | | - | |
436 | | - | |
437 | | - | |
438 | | - | |
439 | | - | |
| 432 | + | |
| 433 | + | |
| 434 | + | |
| 435 | + | |
| 436 | + | |
| 437 | + | |
| 438 | + | |
| 439 | + | |
| 440 | + | |
| 441 | + | |
| 442 | + | |
| 443 | + | |
| 444 | + | |
| 445 | + | |
| 446 | + | |
| 447 | + | |
| 448 | + | |
| 449 | + | |
| 450 | + | |
| 451 | + | |
| 452 | + | |
| 453 | + | |
| 454 | + | |
440 | 455 | | |
441 | 456 | | |
442 | | - | |
443 | | - | |
| 457 | + | |
| 458 | + | |
444 | 459 | | |
445 | 460 | | |
446 | 461 | | |
447 | 462 | | |
448 | 463 | | |
449 | 464 | | |
450 | | - | |
| 465 | + | |
| 466 | + | |
| 467 | + | |
| 468 | + | |
| 469 | + | |
| 470 | + | |
451 | 471 | | |
| 472 | + | |
| 473 | + | |
452 | 474 | | |
453 | 475 | | |
454 | 476 | | |
455 | 477 | | |
456 | 478 | | |
457 | 479 | | |
458 | 480 | | |
459 | | - | |
| 481 | + | |
460 | 482 | | |
461 | | - | |
462 | | - | |
463 | | - | |
464 | | - | |
465 | | - | |
466 | | - | |
467 | | - | |
| 483 | + | |
| 484 | + | |
| 485 | + | |
| 486 | + | |
| 487 | + | |
| 488 | + | |
| 489 | + | |
| 490 | + | |
| 491 | + | |
| 492 | + | |
| 493 | + | |
| 494 | + | |
| 495 | + | |
468 | 496 | | |
469 | 497 | | |
470 | | - | |
| 498 | + | |
471 | 499 | | |
472 | 500 | | |
| 501 | + | |
| 502 | + | |
| 503 | + | |
| 504 | + | |
| 505 | + | |
| 506 | + | |
| 507 | + | |
| 508 | + | |
| 509 | + | |
| 510 | + | |
| 511 | + | |
| 512 | + | |
| 513 | + | |
| 514 | + | |
| 515 | + | |
| 516 | + | |
| 517 | + | |
| 518 | + | |
| 519 | + | |
| 520 | + | |
| 521 | + | |
473 | 522 | | |
474 | 523 | | |
475 | 524 | | |
| |||
484 | 533 | | |
485 | 534 | | |
486 | 535 | | |
| 536 | + | |
| 537 | + | |
| 538 | + | |
| 539 | + | |
| 540 | + | |
| 541 | + | |
| 542 | + | |
487 | 543 | | |
488 | 544 | | |
489 | 545 | | |
| |||
495 | 551 | | |
496 | 552 | | |
497 | 553 | | |
498 | | - | |
| 554 | + | |
| 555 | + | |
499 | 556 | | |
500 | 557 | | |
501 | 558 | | |
| |||
Lines changed: 63 additions & 0 deletions
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
199 | 199 | | |
200 | 200 | | |
201 | 201 | | |
| 202 | + | |
| 203 | + | |
| 204 | + | |
| 205 | + | |
| 206 | + | |
| 207 | + | |
| 208 | + | |
| 209 | + | |
| 210 | + | |
| 211 | + | |
| 212 | + | |
| 213 | + | |
| 214 | + | |
| 215 | + | |
| 216 | + | |
| 217 | + | |
| 218 | + | |
| 219 | + | |
| 220 | + | |
| 221 | + | |
| 222 | + | |
| 223 | + | |
| 224 | + | |
| 225 | + | |
| 226 | + | |
| 227 | + | |
| 228 | + | |
| 229 | + | |
| 230 | + | |
| 231 | + | |
| 232 | + | |
| 233 | + | |
| 234 | + | |
| 235 | + | |
| 236 | + | |
| 237 | + | |
| 238 | + | |
| 239 | + | |
| 240 | + | |
| 241 | + | |
| 242 | + | |
| 243 | + | |
| 244 | + | |
| 245 | + | |
| 246 | + | |
| 247 | + | |
| 248 | + | |
| 249 | + | |
| 250 | + | |
| 251 | + | |
| 252 | + | |
| 253 | + | |
| 254 | + | |
| 255 | + | |
| 256 | + | |
| 257 | + | |
| 258 | + | |
| 259 | + | |
| 260 | + | |
| 261 | + | |
| 262 | + | |
| 263 | + | |
| 264 | + | |
0 commit comments