Commit Graph

15 Commits

Author SHA1 Message Date
Tom Boucher
172e6920eb fix(3670): break migration lock self-deadlock on Windows (#3765)
* test(installer): add regression tests for #3670 migration lock self-deadlock

- T1: same-process PID re-entry reclamation (primary regression)
- T2: dead-PID stale lock reclamation
- T3: unlinkSync EPERM surfaces (not silently swallowed via force:true)
- T4: counter-test — normal round-trip still works
- T5: counter-test — genuinely-held live lock still errors clearly
- Update existing 'reports lock release failures' test to mock
  fs.unlinkSync (not fs.rmSync) matching the fixed release path

Windows wall-clock deadlock repro depends on Docker matrix Windows runners.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* fix(installer): break migration lock self-deadlock on Windows (#3670)

Root cause: `acquireInstallMigrationLock` release closure called
`fs.rmSync(lockPath, { force: true })`. On Windows NTFS, a file
recently closed via `closeSync(fd)` may still return EPERM from
`unlink` until the OS fully releases the handle. The `force: true`
flag silently swallows EPERM, leaving the lock file on disk. The
subsequent `runInstallerMigrations` call in the same install()
invocation hits EEXIST, spins for 30 s, then throws
"installer migration lock is held".

Fix:
1. Release closure uses `fs.unlinkSync` (not rmSync+force) so
   EPERM propagates via releaseError instead of being swallowed.
2. `acquireInstallMigrationLock` closes the fd before writing the
   payload (path-based write), eliminating the open handle that
   caused the deferred EPERM on Windows.
3. Stale-lock reclamation: on EEXIST, parse the on-disk PID and
   reclaim immediately if it matches process.pid (same-process
   re-entry, the primary #3670 failure mode) or if the PID is
   dead (ESRCH). Live alien PIDs still trigger the 30 s timeout.
4. Error message on a genuinely-held lock now includes the holder
   PID and acquiredAt timestamp for operator diagnostics.

No public API change. All callers of runInstallerMigrations are
inside installer-migrations.cjs and bin/install.js.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* chore(3670): update changeset to reference PR #3765

* fix(3670): timeout on reclaim-unlink failure to prevent spin-loop regression

When unlinkSync throws (e.g. Windows EPERM on an open handle) in the
same-PID / dead-PID reclamation path, the original code continued
unconditionally — bypassing the timeout check and reintroducing the
exact deadlock the PR is supposed to fix.

Guard the continue behind a `reclaimed` flag: only loop back to
openSync if unlink SUCCEEDED. On failure, fall through to the existing
bounded sleep + timeout, which surfaces "installer migration lock is
held" within lockTimeoutMs instead of spinning indefinitely.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* test(3670): tighten T5 — assert bounded failure when reclaim unlink fails on live lock

The old T5 accepted BOTH success and throw, which allowed over-reclamation
of a genuinely un-reclaimable lock to pass undetected.

Rewrite T5 to force deterministically unreclaimable conditions:
- Pre-seed lock with process.pid (triggers isSameProcess path)
- Mock fs.unlinkSync via mock.method() to throw EPERM for the lock file

With the production fix: reclaimed=false → falls through to timeout →
throws "installer migration lock is held" within ~200ms.

Without the production fix: unlink throws but continue runs anyway →
process spins and eventually OOMs (confirmed RED: 136s runtime, V8 heap
exhaustion from infinite readLockFile + new Error() allocations).

assert.throws() now makes success a hard failure, closing the
over-reclamation gap.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* fix(3670): clean up orphan lock file when writeFileSync fails after closeSync

If closeSync(fd) succeeds (fd=null) but the subsequent writeFileSync
throws, the empty lock file was left on disk. readLockFile returns null
for an empty/invalid-JSON file, so the stale-lock reclamation path
skips it, causing the next acquire attempt to spin to timeout.

Track ownership with lockCreatedByUs flag; add a second cleanup branch
in the catch block for the fd-already-closed case.

Also fix changeset body to use the bold-prefix format required by all
other fragments in .changeset/.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

---------

Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-20 23:12:08 -04:00
Tom Boucher
7a0a7f1300 Merge main into phase 5 installer migrations 2026-05-11 15:07:50 -04:00
Tom Boucher
d40c576dd0 Merge main into phase 4 installer migrations 2026-05-11 15:02:10 -04:00
Tom Boucher
b15514dac1 Merge pull request #3400 from gsd-build/codex/installer-migrations-phase-three
Feat(installer): Phase 3 first-time baseline scanner
2026-05-11 15:00:12 -04:00
Tom Boucher
6a93b14186 Merge remote-tracking branch 'origin/main' into codex/installer-migrations-phase-two
# Conflicts:
#	bin/install.js
#	get-shit-done/bin/lib/installer-migrations.cjs
2026-05-11 14:57:34 -04:00
Tom Boucher
041e94c43b fix: roll back installer migration state 2026-05-11 14:40:57 -04:00
Tom Boucher
c4fe891391 fix: tighten installer migration authoring guards 2026-05-11 14:36:14 -04:00
Tom Boucher
908a19cd04 fix: harden installer migration integration 2026-05-11 14:36:14 -04:00
Tom Boucher
4e40ee8b2f fix: harden installer migration paths 2026-05-11 14:29:33 -04:00
Tom Boucher
c046dbaacc Test(installer): cover phase 5 migration guardrails 2026-05-11 10:44:47 -04:00
Tom Boucher
7fe75c2c3e Feat(installer): harden phase 4 migration integration 2026-05-11 10:02:02 -04:00
Tom Boucher
0b62129847 Wire installer migrations into install flow 2026-05-11 09:11:11 -04:00
Tom Boucher
3084ecc2a6 Add first-time installer baseline migration 2026-05-10 23:25:11 -04:00
Tom Boucher
3943146484 feat: migrate legacy codex hooks cleanup 2026-05-10 23:08:35 -04:00
Tom Boucher
6d33055756 feat: add installer migration framework 2026-05-10 22:23:28 -04:00