Skip to content

fix: end the session on stop signals in run and proxy, and exit the way the child did - #15

Merged
cvl merged 17 commits into
mainfrom
fix/run-signal-handling
Sep 9, 2026
Merged

fix: end the session on stop signals in run and proxy, and exit the way the child did#15
cvl merged 17 commits into
mainfrom
fix/run-signal-handling

Conversation

@cvl

@cvl cvl commented Sep 9, 2026

Copy link
Copy Markdown
Contributor
  • style: reword ignored-signal comment so gofmt keeps it literal
  • style: gofmt run_signal.go
  • test: live signal test expects shellroute to die by SIGTERM like its child
  • fix(proxy): a stop signal during session startup ends the session
  • fix(run): leave stop signals that are ignored on entry alone
  • fix(run): a stop signal during session startup ends the session
  • fix(run): exit the way the child did — re-raise its signal, keep its code
  • style: fix gofmt alignment in run_signal.go
  • fix: normal test wait 20s to match API client timeout
  • fix: cleanup timeout 25s (5s escalation + 15s API timeout + margin)
  • fix: guard SIGKILL against PID reuse, 10s cleanup timeout, specific CONTRIBUTING entry
  • fix: live test uses process group check, 7s graceful cleanup, guarded mktemp
  • fix: live signal test — child exits 0, exact PGID check, strict args
  • test: add live signal-handling test for shellroute run
  • refactor: extract RunSignalHandler, fix second-signal swallow
  • test: real subprocess signal tests for shellroute run
  • fix: handle SIGINT/SIGTERM/SIGHUP in shellroute run

cvl added 17 commits September 5, 2026 13:09
Forward signal to child process group, escalate to SIGKILL after
5 seconds if child doesn't exit, then always call sess.Stop() for
clean API session teardown. Ensures systemd stop, Ctrl+C, and
terminal close all end the session cleanly instead of relying on
the orphan reaper.

Signed-off-by: contra <vytautas@civilis.lt>
8 tests using real bash subprocesses:
- SIGTERM forwarded, child exits via trap
- SIGHUP forwarded, child exits via trap
- SIGKILL escalation when child ignores SIGTERM
- No escalation when child exits promptly
- Signal to dead process doesn't panic
- Second signal not swallowed during escalation wait
- All three signals registered in run.go
- sess.Stop() always called after Wait()

Signed-off-by: contra <vytautas@civilis.lt>
Production signal handler extracted to run_signal.go as
RunSignalHandler. Fixes:
- Second signal during escalation wait triggers immediate SIGKILL
  (was swallowed by single-read channel)
- Buffer channel size 2 so second signal isn't lost
- EscalateAfter injectable for tests

run.go calls RunSignalHandler with process group PID.

Tests call production RunSignalHandler directly:
- SIGTERM forwarding via syscall.Kill(Getpid())
- SIGHUP forwarding
- Timeout escalation to SIGKILL
- Second signal immediate SIGKILL
- No escalation when child exits promptly
- No panic after child exit

Signed-off-by: contra <vytautas@civilis.lt>
scripts/test-run-signal-live.sh --live [COUNTRY]

Requires --live opt-in (creates one paid session). Builds from
current checkout, launches child with SIGTERM trap, sends SIGTERM
to shellroute, verifies: child received signal, session ended
cleanly, no orphan processes. Not in CI — manual verification
for release gating.

Signed-off-by: contra <vytautas@civilis.lt>
Fixes all blockers:
- Child trap exits 0 (not 42), assert shellroute exits 0
- Orphan check uses exact child PGID from temp file
- All temp files in single WORK_DIR initialized before trap
- Build from REPO_ROOT with ./cmd/shellroute
- Strict arg parsing rejects unknown flags and extra args
- Process group cleanup in EXIT trap
- Exact 'shellroute session ended' marker
- Output shown on every failure
- CONTRIBUTING.md documents optional live tests

Signed-off-by: contra <vytautas@civilis.lt>
… mktemp

- Orphan verification: kill -0 -- -PGID (process group, not just PID)
- Cleanup: SIGTERM → wait up to 7s for shellroute graceful path → SIGKILL
- Child group cleanup: numeric PGID validation, kill -9 -- -PGID
- mktemp failure guarded before any work
- Exact marker: 'shellroute session ended.' with period

Signed-off-by: contra <vytautas@civilis.lt>
…ONTRIBUTING entry

Cleanup recheck kill -0 before SIGKILL to avoid hitting a reused PID.
Timeout increased to 10s (5s child escalation + API teardown).
CONTRIBUTING describes the specific test, not generic 'some tests'.

Signed-off-by: contra <vytautas@civilis.lt>
Signed-off-by: contra <vytautas@civilis.lt>
Signed-off-by: contra <vytautas@civilis.lt>
Signed-off-by: contra <vytautas@civilis.lt>
…code

A child killed by a signal made shellroute exit 255 (Go reports -1 for
signal deaths) and print "Command failed". Under systemd stop with the
default KillMode, most programs die from SIGTERM, so a clean stop left the
unit in "failed (status=255)"; Ctrl+C gave 255 instead of 130.

Now shellroute mirrors the child: exit codes pass through, and SIGINT/
SIGTERM/SIGHUP/SIGKILL deaths are re-raised on shellroute itself so systemd
sees a clean stop and shells get the 128+n convention. Other signals use
128+n directly because the Go runtime would turn them into a crash dump.
"Command failed" is only printed for non-zero exit codes.

Signed-off-by: contra <vytautas@civilis.lt>
Signal handling started only after the child was running. A SIGTERM or
Ctrl+C while the session was being created (up to ~60s: API call plus
exit-IP detection) killed shellroute with the default action and left the
API session open until the server reaper closed it.

RunSignalHandler becomes SignalHandler, registered before session.Start:
before Attach a signal cancels the session context and is remembered;
Attach switches to forwarding, delivering a signal that arrived earlier to
the child. Exit-IP detection now observes the context so the abort is
prompt, and the interrupted startup ends the session, prints the summary,
and exits by re-raising the signal.

Signed-off-by: contra <vytautas@civilis.lt>
signal.Notify re-enables a signal that was ignored when the process
started. Under nohup that defeated the hangup immunity: shellroute caught
the SIGHUP, forwarded it to a child that had inherited the ignore, and
escalated to SIGKILL five seconds later. On v0.1.3 both survived.

The handler now skips any requested signal that signal.Ignored reports,
so nohup keeps working and other stop signals stay handled.

Signed-off-by: contra <vytautas@civilis.lt>
Same gap as in run mode: signals were only handled once the proxy was up,
so a SIGTERM or Ctrl+C during session creation killed the process and
left the API session open. The handler is now registered before
session.Start; an interrupted startup ends the session and prints the
summary like a normal disconnect.

Signed-off-by: contra <vytautas@civilis.lt>
…child

The child now dies from the forwarded SIGTERM instead of exiting 0, which
is what real programs under systemd do, and shellroute must exit by
SIGTERM (status 143 in bash) rather than 0 or 255.

Signed-off-by: contra <vytautas@civilis.lt>
Signed-off-by: contra <vytautas@civilis.lt>
Signed-off-by: contra <vytautas@civilis.lt>
@cvl
cvl requested a review from a team as a code owner September 9, 2026 08:10
@cvl
cvl merged commit 7d9b2eb into main Sep 9, 2026
2 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.

1 participant