Skip to content

fix: scope-intercept signal handling for long-lived workloads#309

Open
rubberduck203 wants to merge 2 commits intomainfrom
cmac/intercept-forward-signals-on-interactive
Open

fix: scope-intercept signal handling for long-lived workloads#309
rubberduck203 wants to merge 2 commits intomainfrom
cmac/intercept-forward-signals-on-interactive

Conversation

@rubberduck203
Copy link
Copy Markdown
Contributor

@rubberduck203 rubberduck203 commented Apr 30, 2026

Why

scope-intercept is sometimes used as a shebang on long-running scripts to enable self-healing known-error fix-and-retry. We had a report that Ctrl+C stopped working in that configuration: services started under overmind (nginx, etc.) kept running, the overmind socket was orphaned, and the next start failed with an "overmind previously crashed" prompt.

The terminal sends SIGINT to every member of the foreground process group — both scope-intercept and the wrapped env -S bash. With no signal handler, scope-intercept followed the default and aborted immediately. Its captured stdout/stderr pipes closed, and the child died with SIGPIPE before its own SIGINT trap (and overmind's shutdown logic) could complete.

scope-intercept was originally designed for short-lived setup scripts, but using it as a shebang for long-running interactive processes is a strong fit if it cooperates with the child's lifecycle. This PR makes it cooperate.

What changed

src/bin/scope-intercept.rs — At the top of run_command, install Tokio Unix signal listeners for SIGINT, SIGTERM, and SIGHUP. Their only job is to consume the default-terminate behavior so scope-intercept stays alive while the child runs its cleanup. No manual forwarding is needed — the OS already delivered the signal to the child via the shared process group.

When the interrupt flag is set and the child exited non-zero, skip the known-error analysis, retry, and bug-report flows. The user asked to quit, not to be quizzed.

OutputCapture::capture_output and the scope doctor path are deliberately not touched — this fix is scoped to the intercept binary.

Cargo.toml — Adds libc as a dev-dep so the new test can call setsid/killpg.

Tests

tests/scope_intercept.rs::test_intercept_forwards_sigint_and_waits_for_child_cleanup:

  • Writes a trap.sh that traps SIGINT, writes cleanup.done, and sleeps.
  • Spawns scope-intercept -- bash trap.sh in its own session/process group via pre_exec(setsid).
  • Waits for a ready marker, then sends SIGINT to the group via killpg — this is what a terminal does on Ctrl+C.
  • Asserts: the child's trap ran (cleanup.done exists with the expected contents), scope-intercept exited within 10s, and the exit code is 130.

Verified the test fails on main (without the fix, exit status is unix_wait_status(2)scope-intercept killed mid-flight by SIGINT) and passes with the fix.

All 8 existing intercept tests still pass; full suite (177 tests) green.

Test plan

  • cargo test --test scope_intercept — 8/8 pass
  • cargo test — 177/177 pass
  • cargo fmt --check clean

@rubberduck203 rubberduck203 marked this pull request as draft April 30, 2026 13:19
@rubberduck203 rubberduck203 changed the title fix: scope-intercept signal handling for long-lived workloads (LDE-814) fix: scope-intercept signal handling for long-lived workloads Apr 30, 2026
@rubberduck203 rubberduck203 force-pushed the cmac/intercept-forward-signals-on-interactive branch from 88de029 to 57b9f02 Compare April 30, 2026 13:21
… clean up

When scope-intercept wraps a long-running process (e.g. as the shebang
on a server start script), Ctrl+C in the terminal delivers SIGINT to
every member of the foreground process group — both scope-intercept
and the wrapped command. Without a custom signal handler, scope-intercept
follows the default and aborts immediately, closing the captured
stdout/stderr pipes. The child then dies with SIGPIPE before its own
SIGINT trap can finish, leaving services like overmind/nginx orphaned.

Install Tokio Unix signal listeners for SIGINT, SIGTERM, and SIGHUP at
the start of run_command. Their only job is to consume the default-
terminate behavior so scope-intercept stays alive while the child runs
its trap. The OS already delivered the signal to the child via the
shared process group, so no manual forwarding is needed.

When an interrupt has fired and the child exits non-zero, skip the
known-error analysis, retry, and bug-report flows — the user asked to
quit, not to be quizzed.

OutputCapture and the scope doctor path are intentionally untouched;
this is scoped to the intercept binary, which is the one wrapping
interactive long-lived workloads.

Includes a new regression test that spawns scope-intercept in its own
process group, sends SIGINT via killpg (mimicking a terminal), and
asserts the child's bash trap ran and the exit code is 130.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
@rubberduck203 rubberduck203 force-pushed the cmac/intercept-forward-signals-on-interactive branch from 57b9f02 to 9c715fe Compare April 30, 2026 13:22
Comment thread tests/scope_intercept.rs
Comment thread tests/scope_intercept.rs Outdated
Comment thread tests/scope_intercept.rs Outdated
Per review on #309:
- Replace raw libc::setsid / libc::killpg with the typed `nix` wrappers
  (`nix::unistd::setsid`, `nix::sys::signal::killpg` with `Pid` and
  `Signal`). Drops the `as i32` cast and the manual errno handling.
- Add a SAFETY comment on the `unsafe { command.pre_exec(...) }` block
  spelling out the async-signal-safety contract pre_exec requires.
- Replace the try_wait + sleep + deadline polling loop with
  `wait_timeout::ChildExt::wait_timeout`, which is the standard
  idiom for "wait for a child with a deadline".
- Annotate the remaining poll loop (waiting for the bash trap to
  install a marker file) explaining why a sleep is needed there —
  no good cross-process file-appeared notification primitive
  without inotify/kqueue.

Cargo.toml: drop `libc`, add `nix = "0.29"` (signal+process features)
and `wait-timeout = "0.2"` as dev-deps.

Test still passes; full suite remains green.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
@socket-security
Copy link
Copy Markdown

socket-security Bot commented Apr 30, 2026

All alerts resolved. Learn more about Socket for GitHub.

This PR previously contained dependency changes with security issues that have been resolved, removed, or ignored.

Ignoring alerts on:

  • cargo/nix@0.29.0

View full report

@rubberduck203
Copy link
Copy Markdown
Contributor Author

@SocketSecurity ignore cargo/nix@0.29.0 Test files don't get compiled and distributed into the application.

@rubberduck203 rubberduck203 marked this pull request as ready for review April 30, 2026 14:46
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