Skip to content

Tear down session tongs on SIGHUP and SIGTERM - #95

Merged
CrypticSwarm merged 4 commits into
masterfrom
teardown-on-hangup
Oct 6, 2026
Merged

CrypticSwarm merged 4 commits into
masterfrom
teardown-on-hangup

Conversation

@CrypticSwarm

Copy link
Copy Markdown
Owner

Closing the terminal or tmux pane a session runs in sends the launcher SIGHUP. Nothing handled it, so the process died without running run_with_tongs' teardown and the session tongs kept running, including socket-holding ones with host-level power. A plain kill (SIGTERM) leaked them the same way; only Ctrl-C was handled.

  • While tongs are orchestrated, SIGHUP/SIGTERM unwind through the same teardown as Ctrl-C and exit 129/143. The no-tongs passthrough is unchanged, and a signal ignored on entry (nohup) stays ignored.
  • Teardown blocks HUP/TERM until it finishes, however it was entered. A pane close hangs up the whole process group, so the block also covers teardown's own docker rm children.
  • The foreground anvil client is forwarded the signal, given 3s, then killed, and always reaped before teardown.
  • Each teardown resource (session tong, network, shared-tong connection) is now recorded before the docker call that creates it, so a call cut short by a signal is still cleaned up.

Tests include real-signal subprocess tests: SIGHUP and SIGTERM to the launcher and to its process group mid-teardown, plus Ctrl-C/TERM interleavings against a real child.

Closing the terminal or tmux pane a session runs in sends the launcher
SIGHUP, and nothing handled it: Python's default action killed the
process without running run_with_tongs' finally, so the session tongs
-- including socket-holding ones with host-level power -- kept running
with nothing left to remove them. A plain `kill` (SIGTERM) leaked them
the same way, and so did either signal arriving during a teardown that
had already started. Only Ctrl-C, via KeyboardInterrupt, was handled.

While main orchestrates tongs, SIGHUP and SIGTERM now raise
TerminationSignal, which unwinds through the same finally and exits
128 + signum (129, 143), as SIGINT exits 130. The no-tongs passthrough
still execs untouched, a signal already ignored on entry (nohup) stays
ignored, and the previous handlers are restored once the run returns.

- Teardown runs inside a guard main hands run_with_tongs, which blocks
  HUP/TERM until it finishes, however teardown was entered. Closing a
  pane hangs up the whole process group, so the block has to reach
  teardown's own `docker rm` children too, which inherit it; a signal
  held meanwhile is only recorded, and main turns it into the exit
  status. A Ctrl-C keeps 130, and only the first HUP/TERM is taken.
- The foreground anvil client is forwarded the signal -- including one
  arriving while a Ctrl-C it survived is waited out -- given 3s, then
  killed, and always reaped before teardown starts. Under a TTY
  docker's sig-proxy is off, so the signal just ends the client.
- Nothing is written once a signal has been received, since after a
  hangup the terminal may be gone: a launch error is not reported, and
  no traceback escapes.
run_with_tongs recorded a session tong, the session network, a joined
org network, and a shared tong's session connection only after the
docker call that made it returned. A Ctrl-C, SIGHUP, or SIGTERM landing
during that call -- or between its return and the record -- skipped the
record, so teardown missed a container or network the daemon may
already have created. Each is now recorded first; teardown's docker
calls are best-effort and do nothing for one that never came to exist.

Recording first also covers a session tong whose `docker run -d` is cut
short, without removing anything inside _start_one_tong: doing it there
would also remove a concurrent launch's live shared tong when `docker
run` fails on its name. A shared tong cut short is left as the daemon
had it: the next launch reuses it if it is running with a matching
config hash, and recreates it if it is stopped or missing.
The teardown guard blocked SIGHUP and SIGTERM so teardown's `docker rm`
children would inherit the block, but the docker CLI is a Go program and
the Go runtime clears an inherited signal mask at startup. A hangup that
reached the process group mid-teardown could still kill the docker
client and leave its tong behind; the tests passed only because their
stand-in child was Python.

The guard now sets both signals to SIG_IGN for the teardown instead. Go
keeps an inherited SIGHUP ignore, so a later hangup can no longer cut a
`docker rm` short. It does not keep SIGTERM ignored, so a group SIGTERM
can still reach one; that gap is narrower, since a hangup is what closing
a pane sends.

An ignored signal is not recorded, so one arriving mid-teardown no
longer changes the exit status or suppresses a launch error report: the
run's own outcome stands. A signal at the guard's edges is still only
recorded, never raised.
The hangup story was told in four places -- the cli module docstring,
the package docstring, run_with_tongs, and the docs -- and the
docstrings this branch added restated what the code beside them already
says. Each is cut to the contract it owns: the module docstrings name
the behavior, `_wait_foreground` keeps only what forwarding and reaping
guarantee, and errors.py says in one sentence why its two classes live
there.

The docs now also name Ctrl-\ (SIGQUIT), which is not trapped, among
the ways to skip teardown.
@CrypticSwarm
CrypticSwarm merged commit 49bb5ed into master Oct 6, 2026
4 checks passed
@CrypticSwarm
CrypticSwarm deleted the teardown-on-hangup branch October 6, 2026 15:08
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