Skip to content

fix(lock,fetch): a real SIGINT/SIGTERM releases the lock; no response body outlives its command - #220

Merged
PrzemekGalarowicz merged 1 commit into
mainfrom
claude/optimistic-heisenberg-8rw8ke
Sep 25, 2026
Merged

PrzemekGalarowicz merged 1 commit into
mainfrom
claude/optimistic-heisenberg-8rw8ke

Conversation

@PrzemekGalarowicz

Copy link
Copy Markdown
Contributor

What this changes

Plan C of the second batch from the PHARN-01..18 review (findings F12, F13), shipped through /pharn-dev-ship.

F13: a stranded lock. .pharn.lock was released only from an exit listener or a finally. A real signal's default action ends the process without either. So pharn update --yes cancelled in CI, or killed by timeout / docker stop, exited 130/143 and left the lock behind. A host sharing the directory then waited out the 6 h staleness window. Reproduced on main, then re-run on this build:

signal before: exit / lock left after: exit / lock left
SIGTERM 143 / yes 143 / no
SIGINT 130 / yes 130 / no
  • New src/lib/fatal-signal.ts provides onFatalSignal(cleanup). It is installed lazily on the first registration. On a signal it:
    1. runs the registered cleanups, newest first;
    2. removes every listener on the signal;
    3. re-raises it, so the exit status stays 130 or 143 (it falls back to exit(128 + n) if the re-raise ever throws).
  • The temp-clone cleanup (repo.ts) and withProjectLock both register through it. The lock deregisters in its finally, so a later signal can never delete another process's lock.
  • SIGHUP is deliberately not handled. Measured: a process.on('SIGHUP') listener overrides the SIG_IGN that nohup sets, so a hangup would interrupt a nohup pharn update mid-write. Decided by the maintainer after that finding; it is documented in troubleshooting.

F12: an unread body kept the process alive. fetchCommitSha now releases a non-2xx body. It reads a 2xx body through its own reader, cancelled at the 8 s deadline, instead of res.json(), which has nothing to cancel. The SHA is best-effort, so the command finishes normally, and a dripping 403 used to keep the process alive for 30 s after its last line. downloadArchive now releases its non-2xx body too.

Type of change

  • feat — new stack option, wizard step, or command capability
  • fix — bug fix
  • docs — docs-only change
  • chore / refactor — tooling or internal restructure, no behavior change

Area(s) touched

src/lib/fatal-signal.ts (new) · src/lib/repo.ts · src/lib/project-lock.ts · tests · docs/troubleshooting.md · CHANGELOG · .dev/features/signal-lock-release/

Checklist

  • Read the existing file(s) before editing; followed the ESM .js-extension import convention.
  • Updated the matching tests. 14 of the new cases fail against the base src/ (checked in a worktree). They cover real-signal child processes, observable body cancels, and the registry rules.
  • Updated the relevant docs/ page.
  • Preserved the security invariants — redirect: 'error', the deadlines and the existing caps are unchanged; untrusted bodies are now cancelled rather than drained.

Quality gates

  • npm run check passes locally (format:check + lint + typecheck + test) — 1530 tests.
  • npm run build succeeds.
  • npm run test:coverage passes (coverage thresholds met).

Notes for the reviewer

  • Pipeline results: validate exit 0, regress no-regressions, verify PASS, review GREEN with 2 minor advisory findings (.dev/features/signal-lock-release/REVIEW.md).
  • Named residuals: SIGKILL, power loss and a hangup still strand the lock. The same host reclaims it via the dead-pid check. The SHA body read still has no byte cap, as before.

🤖 Generated with Claude Code

https://claude.ai/code/session_0199owRmYfskqYQVQrVP679o


Generated by Claude Code

… body outlives its command

The project lock was released only from an `exit` listener or its
`finally`, but a real signal's default action ends the process without
emitting `exit`. `pharn update --yes` cancelled in CI or killed by
`timeout` / `docker stop` exited 130/143 with .pharn.lock left behind;
a host sharing the directory waited out the 6 h staleness window.

- New lib/fatal-signal.ts: a lazily installed SIGINT/SIGTERM handler
  that runs registered cleanups newest first, removes every listener,
  and re-raises (exit(128+n) if the re-raise ever throws). The temp-clone
  cleanup (repo.ts) and withProjectLock both register through it; the
  lock deregisters in its finally.
- SIGHUP is deliberately NOT handled: a listener overrides the SIG_IGN
  `nohup` sets (measured), so a hangup would interrupt a `nohup pharn
  update` mid-write. Decided by the human after the measurement.
- fetchCommitSha releases a non-2xx body and reads a 2xx body through
  its own reader cancelled at the deadline, so neither keeps the process
  alive after the command finished (measured 30 s for a dripping 403).
  downloadArchive releases its non-2xx body too.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0199owRmYfskqYQVQrVP679o
@coderabbitai

coderabbitai Bot commented Sep 25, 2026

Copy link
Copy Markdown

Important

  • 🔍 Trigger review

This repository does not receive automatic reviews because it has fewer than 10 stars.

⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Advanced

Run ID: 4dc2bf6c-0f00-4fde-9f61-f0cd551771f0


Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@PrzemekGalarowicz
PrzemekGalarowicz merged commit f1e8b92 into main Sep 25, 2026
14 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants