Skip to content

fix(hook): terminate the permission watcher on a signal - #3

Open
omariqbalnaru wants to merge 1 commit into
TheMetalStorm:mainfrom
omariqbalnaru:fix/watcher-signal-exit
Open

omariqbalnaru wants to merge 1 commit into
TheMetalStorm:mainfrom
omariqbalnaru:fix/watcher-signal-exit

Conversation

@omariqbalnaru

Copy link
Copy Markdown

Problem

start_blocking_prompt_watcher() registered a single trap for cleanup and signals:

trap 'rmdir "$watcher_dir" 2>/dev/null' 0 HUP INT TERM

A caught signal only runs the handler — execution then resumes at the next
statement, which is watch_blocking_prompt's while : loop. So HUP/INT/TERM
removed the marker directory and the watcher kept polling. It never exited.

Nothing else reaps it. Command Code has no Exit hook event, and a pane's kill
reaches only the pane's own process group, so a leaked watcher outlives cmd,
the pane, and the herdr server.

Observed live on macOS:

74142  1  S  05:15  sh .../cmd-hooks/herdr-status.sh
  0r  /dev/null
  1u  unix  ->(none)     <- herdr server already gone
  2u  unix  ->(none)
child: 81328  1  sleep 1

Reparented to PID 1, still looping after 8+ minutes, writing to a socket whose
herdr server had exited — and it survived kill -TERM.

Reproduced in isolation:

sh -c 'trap "true" TERM; while :; do sleep 1; done'   -> SURVIVED SIGTERM

Fix

Split the traps: 0 cleans up on every exit path, while HUP/INT/TERM clean up
and then exit 0.

cleanup() {
  rm -f "$watcher_dir/pid" 2>/dev/null || true
  rmdir "$watcher_dir" 2>/dev/null || true
}
trap 'cleanup' 0
trap 'cleanup; exit 0' HUP INT TERM
watch_blocking_prompt

Two supporting details:

  • The watcher's pid is recorded in the marker dir so a future leak is
    diagnosable rather than invisible. $! is read in the parent, because
    $$ inside the subshell is the parent's pid.
  • Cleanup removes that pid file before rmdir, which only removes empty
    directories — otherwise the fix would trade a leaked process for a leaked
    directory.

Verification

New regression test (tests/hook.test.sh #22) starts a real SessionStart
watcher and asserts it terminates on SIGTERM and leaves no marker dir. It
unsets HERDR_PERMISSION_MAX_SCANS, because the suite's single-scan default
would let the watcher exit on its own and mask the leak.

Code tests/hook.test.sh
upstream (ce4aa8f) PASS=40 FAIL=2 — watcher pid never published; survived SIGTERM
this PR PASS=42 FAIL=0

Full suite after the change: common 4/4, hook 42/42, install 5/5,
launch 4/4 passing. (launch.test.sh reports PASS=4 FAIL=4 for the
resume-last / resume-named arg assertions both before and after this
change — pre-existing and unrelated, so not touched here.)

Scope

Deliberately narrow: this makes the watcher reapable. It does not claim to
explain every orphaned process a user might see after force-quitting a pane —
the NOTES.md force-quit gap, where no exit hook exists to report a final
state, is a separate and larger problem.

The SessionStart watcher registered one trap for both cleanup and signals:

    trap 'rmdir "$watcher_dir" 2>/dev/null' 0 HUP INT TERM

A caught signal only runs the handler; execution then resumes at the next
statement, which is the watcher's `while :` loop. So HUP/INT/TERM removed the
marker directory and the watcher kept polling — it never exited. Nothing else
reaps it: Command Code has no Exit hook event, and a pane's kill reaches only
the pane's own process group, so a leaked watcher survives cmd, the pane, and
the herdr server.

Observed live: a `herdr-status.sh` watcher reparented to PID 1, still running
`sleep 1` after 8+ minutes, writing to a unix socket whose herdr server had
already exited, and surviving `kill -TERM`.

Fix: split the traps — `0` cleans up and runs on every exit path, while
HUP/INT/TERM clean up and then `exit 0`. Also record the watcher's pid in the
marker dir so a future leak is diagnosable instead of invisible; `$!` is read
in the parent, since `$$` inside the subshell is the parent's pid. Cleanup
removes that pid file before `rmdir`, which only removes empty directories.

Verified with a regression test (tests/hook.test.sh #22) that starts a real
SessionStart watcher and asserts it dies on SIGTERM:

  upstream: PASS=40 FAIL=2  (watcher pid never published / survived SIGTERM)
  fixed:    PASS=42 FAIL=0

The test unset HERDR_PERMISSION_MAX_SCANS so the watcher would loop
indefinitely without the fix; the suite's single-scan default would otherwise
mask the leak. launch.test.sh's 4 failures are pre-existing and unrelated.
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.

1 participant