-
fix(execd): kill entire process group on command cancel (#924)
发布于
2026-05-25 03:32:54 +00:00 - fix(execd): kill entire process group on command cancel
When a foreground command was cancelled (client disconnect, timeout, or
DELETE /command), only the bash group leader received SIGKILL — child
processes spawned via&or pipelines kept running as orphans because
exec.CommandContext's internal kill targets a single pid, and killPid
sent signals to the leader only.Fix runCommand's ctx.Done() branch to send SIGKILL to -pid (the whole
group, since the leader is launched with Setpgid: true), mirroring
runBackgroundCommand. Rewrite killPid to signal -pid for SIGTERM/SIGKILL
and to use kill(-pid, 0) for liveness probing, so Interrupt() also
terminates descendants.Adds regression tests covering both cancel and Interrupt paths.
Fixes #922
Co-Authored-By: Claude Opus 4.7 noreply@anthropic.com
- fix(execd): guard Interrupt against stale PID after command exit
killPid now signals the whole process group (-pid). Combined with the
fact that commandClientMap retains finished sessions, a late or retried
Interrupt could otherwise terminate every process in an unrelated
process group whose PGID has reused the recorded PID.- markCommandFinished clears kernel.pid alongside kernel.running so the
stale PID is no longer accessible. - commandSnapshot now reads under c.mu.RLock for a consistent view of
running/pid relative to markCommandFinished's write under c.mu.Lock. - Interrupt() (unix and windows) snapshots the kernel and refuses to
signal when the command has already finished.
Adds a regression test ensuring Interrupt on a completed session returns
an error and that pid is cleared.Co-Authored-By: Claude Opus 4.7 noreply@anthropic.com
- fix(execd): don't surface slow group teardown as Interrupt failure
kill(2) on a process group only guarantees delivery to at least one
member, and kill(-pid, 0) keeps reporting the group as observable while
any unreaped zombie lingers. The previous post-SIGKILL probe ran for
only 150ms and then returned a hard error, so Interrupt could surface a
500 even though the kill signal had already been delivered. Likewise on
macOS, SIGKILL on a group that has been reduced to zombies returns
EPERM, which the previous code reported as a kill failure even though
SIGTERM had already taken effect.- After a successful SIGKILL, log a warning when the probe loop still
observes the group instead of returning an error. - When SIGTERM was delivered but the SIGKILL syscall fails (commonly
EPERM on a zombie-only group), log and return nil — the kill is in
flight and the kernel will reap the group once Wait() runs.
Adds a regression test that runs killPid against a Setpgid group with
no concurrent reaper, exercising the zombie-lingering path.Co-Authored-By: Claude Opus 4.7 noreply@anthropic.com
- fix(execd): only group-kill on real cancellation, not after success
Execute() defers cancel() for every foreground command, including
successful ones, so the signal-forwarding goroutine's ctx.Done() branch
also fired on the normal-success path. With the new group-wide SIGKILL
on -cmd.Process.Pid, that post-completion signal could hit a recycled
pid/pgid and kill an unrelated process group inside the sandbox.Gate the goroutine on the existing
donechannel (closed after
cmd.Wait() returns or on start failure): exit cleanly when the command
has finished, so only genuine cancellations — timeout, client abort,
Interrupt — trigger the group kill. A double-check inside the
ctx.Done() branch handles the race where ctx is cancelled at the same
instant cmd.Wait() returns.Co-Authored-By: Claude Opus 4.7 noreply@anthropic.com
Co-authored-by: Claude Opus 4.7 noreply@anthropic.com
下载附件