Skip to content

fix(run): forward signals to child and propagate exit status#812

Merged
theoephraim merged 4 commits into
mainfrom
run-signal-forwarding
Jun 22, 2026
Merged

fix(run): forward signals to child and propagate exit status#812
theoephraim merged 4 commits into
mainfrom
run-signal-forwarding

Conversation

@theoephraim

@theoephraim theoephraim commented Jun 19, 2026

Copy link
Copy Markdown
Member

What

varlock run stays resident (it pipes the child's stdout through redaction whenever output is piped/redirected — interactive terminals get raw pass-through), but it never forwarded terminating signals to the child and lost the child's signal-death status. As a container ENTRYPOINT / PID 1 this means docker stop / pod termination never reaches the app gracefully — the orchestrator waits out the grace period then SIGKILLs.

Changes

  • Forward signals (SIGTERM, SIGINT, SIGHUP, SIGQUIT) to the child instead of SIGKILL-ing it, so it can run its own shutdown handlers. Handlers are installed before the child is spawned, so the child inherits a clean default disposition and reliably reacts to a forwarded signal (installing them after spawn let the child inherit an ignored SIGINT, causing an intermittent hang).
  • Process-group forwarding when there's no controlling terminal (containers, CI, background/agent runs): the child runs in its own group (setsid) and signals go to the whole group, so grandchildren terminate too. When a terminal is present we stay in the shared group — preserving the child's controlling terminal (/dev/tty, SIGWINCH) and the daemon's tty-based session scoping of any nested varlock. "Terminal present" is detected via the std streams and a /dev/tty probe, so it stays correct even when a runner (e.g. turbo) keeps a controlling terminal but pipes a task's std fds.
  • Forward-and-wait by default — like tini/dumb-init, varlock does not impose its own kill deadline (a forwarded signal isn't always terminal; e.g. SIGHUP often means reload). Escalation to SIGKILL is opt-in via _VARLOCK_FORCE_KILL_TIMEOUT_MS.
  • Propagate exit status faithfully: normal codes pass through unchanged; a child killed by signal N now exits 128+N (e.g. 143 for SIGTERM) instead of 1.
  • Never signals the child once it has exited, so a clean run never blasts SIGKILL at a reaped (possibly recycled) pid/process group.

Existing redaction, env injection, and normal-exit-code behavior are unchanged. Adds smoke tests covering signal forwarding, 128+N propagation, the no-force-kill default, and opt-in escalation, plus a docs note.

Daemon session-scoping impact

None. The unlock-session/peer-identity scoping keys off the varlock process that connects to the enclave and its ancestry/env — all upstream of the spawned child. setsid is only applied when there's no controlling terminal (an env-anchored / process-tree context that setsid doesn't perturb), and never when a terminal is present (so tty-based scoping of nested varlock — including turbo per-task PTYs and the varlock → claude → varlock case — is untouched). setsid changes neither env vars nor parent PIDs. Verified empirically: a child is only made a session leader when no terminal exists; with a per-task PTY or an interactive terminal it stays in the shared group.

Known tradeoff

In interactive (shared-group) mode the terminal delivers Ctrl-C to the child and varlock forwards it, so the child may see SIGINT twice. Still strictly better than the old immediate SIGKILL, and forwarding is required so a direct kill -TERM <varlock-pid> isn't lost.

Out of scope

  • Zombie reaping for PID 1 (no waitpid(-1) API in pure Node).
  • execve-replacing the child when redaction is off (interactive TTY, no stdout piping needed) for perfect signal transparency — a separate follow-up.

@github-actions

github-actions Bot commented Jun 19, 2026

Copy link
Copy Markdown
Contributor

bumpy-frog

The changes in this PR will be included in the next version bump.

patch Patch releases

  • varlock 1.7.2 → 1.7.3

Bump files in this PR

Click here if you want to add another bump file to this PR


This comment is maintained by bumpy.

@cloudflare-workers-and-pages

cloudflare-workers-and-pages Bot commented Jun 19, 2026

Copy link
Copy Markdown

Deploying with  Cloudflare Workers  Cloudflare Workers

The latest updates on your project. Learn more about integrating Git with Workers.

Status Name Latest Commit Preview URL Updated (UTC)
✅ Deployment successful!
View logs
varlock-website 2aa3ffc Commit Preview URL

Branch Preview URL
Jun 22 2026, 07:25 PM

@pkg-pr-new

pkg-pr-new Bot commented Jun 19, 2026

Copy link
Copy Markdown

Open in StackBlitz

npm i https://pkg.pr.new/varlock@812

commit: 538d1f0

… child

Review follow-ups:
- don't impose a default SIGKILL deadline (SIGHUP-reload / ignored signals
  no longer kill the child); escalation is opt-in via _VARLOCK_FORCE_KILL_TIMEOUT_MS
- skip forwarding once the child has exited, so a clean run never signals a
  reaped (possibly recycled) pid/process group
- register SIGTERM/SIGINT/SIGHUP/SIGQUIT handlers before spawning the child.
  Previously they were installed after spawn, so the child could inherit an
  'ignored' SIGINT disposition (Node special-cases SIGINT) and never react to a
  forwarded signal — a real, timing-dependent hang surfaced under load.
- gate process-group detach (setsid) on no std stream being a TTY, rather than
  just stdin. This keeps the daemon's tty-based session scoping of any nested
  varlock unchanged whenever a terminal is present, and also avoids losing the
  child's controlling terminal (/dev/tty, SIGWINCH) in piped-stdin terminals.
… std-stream TTYs

A process can keep its controlling terminal while its std fds are pipes (e.g. turbo
running interactively but piping a task's output). The daemon scopes unlock sessions on
the controlling terminal (e_tdev), so detaching such a child would sever tty-based
scoping of any nested varlock. Probe /dev/tty (canonical POSIX controlling-terminal
check) in addition to isTTY, and only setsid when there is genuinely no terminal.
@theoephraim theoephraim merged commit b4a6c9b into main Jun 22, 2026
14 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant