fix(exec): bounded post-kill reaps — D-state children can't wedge a turn
Port of upstream grok-build 1.0.1 (landed upstream 2026-08-06). A child wedged in an uninterruptible kernel syscall (D-state) never observes SIGKILL, and a background descendant can hold the output pipes open past the kill; both previously stalled tool futures without bound. - kigi-tty-utils: KILL_REAP_TIMEOUT (5s) + reap_killed_bounded — wait for an already-killed child at most the bound, then abandon the corpse to tokio's orphan reaper. - local_terminal: pipe readers share their buffer with the spawner and join under the reap bound on both exit arms (aborted readers are awaited so no tail bytes race the take); the whole tree is enrolled in a ProcessGroup and group-killed on timeout; the timeout arm returns the synthetic result instead of waiting on the corpse. Grandchild kill + prompt-return pinned by a ported unix regression test. - kigi-tools: reap_killed_search_child wrapper; grep's four kill sites reap bounded; glob's stderr goes to null (an unread pipe blocks rg on warning backpressure) and its truncation kill reaps bounded. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01KSy9S31pZNWXtRZNVJ1sDf
This commit is contained in:
1 parent
1ae803519e
commit
26d26e15c9
7 files changed
+228
-32
No files matched your search
@@ -134,6 +134,36 @@ pub fn detach_std_command(cmd: &mut std::process::Command) {
|
||||
|
||||
// Process group lifecycle
|
||||
|
||||
/// Bound on waiting for an already-killed child (or its pipe readers) before
|
||||
/// abandoning it.
|
||||
///
|
||||
/// A kill normally makes `child.wait()` resolve in milliseconds, but a child
|
||||
/// wedged in an uninterruptible kernel syscall (D-state — e.g. a read on a
|
||||
/// hard NFS mount whose server stopped responding) only observes the signal
|
||||
/// when that syscall returns, which can be effectively never. Callers that
|
||||
/// must not block (tool futures, turn loops) wait at most this long, then
|
||||
/// abandon the corpse to the runtime's orphan reaper.
|
||||
pub const KILL_REAP_TIMEOUT: std::time::Duration = std::time::Duration::from_secs(5);
|
||||
|
||||
/// Reap an already-killed child, waiting at most `bound` (usually
|
||||
/// [`KILL_REAP_TIMEOUT`]).
|
||||
///
|
||||
/// Returns the exit status when the child was reaped in time. `None` covers
|
||||
/// both failure shapes — the bound expired (see [`KILL_REAP_TIMEOUT`]) and
|
||||
/// `wait()` itself erred (e.g. the child was already reaped elsewhere) — the
|
||||
/// caller's obligation is identical in either case: the kill signal is
|
||||
/// already delivered, there is no status to report, and the corpse is left
|
||||
/// to tokio's orphan reaper. Callers should log the `None` case.
|
||||
pub async fn reap_killed_bounded(
|
||||
child: &mut tokio::process::Child,
|
||||
bound: std::time::Duration,
|
||||
) -> Option<std::process::ExitStatus> {
|
||||
match tokio::time::timeout(bound, child.wait()).await {
|
||||
Ok(res) => res.ok(),
|
||||
Err(_elapsed) => None,
|
||||
}
|
||||
}
|
||||
|
||||
/// Configure a command so the spawned child becomes the leader of a new
|
||||
/// process group.
|
||||
pub fn new_process_group(cmd: &mut tokio::process::Command) {
|
||||
|
||||
Reference in new issue
Block a user