发布

  • fix(execd): kill entire process group on command cancel (#924)

    frostbyte_neo 发布于 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 done channel (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

    下载附件