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
@@ -358,18 +358,20 @@ impl kigi_tool_runtime::Tool for GrepTool {
|
||||
tracing::Span::current().record("wall_ms", started.elapsed().as_millis() as u64);
|
||||
tracing::warn!(timeout_secs = timeout.as_secs(), "grep timed out");
|
||||
let _ = child.start_kill();
|
||||
let _ = child.wait().await;
|
||||
crate::util::reap_killed_search_child(&mut child).await;
|
||||
return Ok(grep_timeout_output(timeout.as_secs()));
|
||||
}
|
||||
};
|
||||
|
||||
// When `stdout_truncated`, `rg` is already killed (inside the timeout
|
||||
// block, before the stderr drain); this only reaps it.
|
||||
let status = child.wait().await.ok();
|
||||
// Truncated output means `rg` was already killed (inside the timeout
|
||||
// block, before the stderr drain) — bounded reap, and the exit code is
|
||||
// defined as 0. A natural EOF means rg is exiting, so the plain wait
|
||||
// is prompt.
|
||||
let exit_code = if stdout_truncated {
|
||||
crate::util::reap_killed_search_child(&mut child).await;
|
||||
0
|
||||
} else {
|
||||
status.and_then(|s| s.code()).unwrap_or(-1)
|
||||
child.wait().await.ok().and_then(|s| s.code()).unwrap_or(-1)
|
||||
};
|
||||
|
||||
tracing::Span::current().record("early_kill", stdout_truncated);
|
||||
@@ -529,7 +531,7 @@ fn grep_progress_stream(
|
||||
tracing::warn!(timeout_secs = secs, "grep timed out");
|
||||
});
|
||||
let _ = child.start_kill();
|
||||
let _ = child.wait().await;
|
||||
crate::util::reap_killed_search_child(&mut child).await;
|
||||
// Finalize what was read (marked truncated) plus an explicit
|
||||
// notice, so the terminal card does not contradict what was already
|
||||
// streamed; with nothing streamed, the timeout-only card suffices.
|
||||
@@ -578,11 +580,14 @@ fn grep_progress_stream(
|
||||
.await;
|
||||
}
|
||||
|
||||
let status = child.wait().await.ok();
|
||||
// Truncated output means `rg` was already killed above — bounded reap,
|
||||
// and the exit code is defined as 0. A natural EOF means rg is
|
||||
// exiting, so the plain wait is prompt.
|
||||
let exit_code = if stdout_truncated {
|
||||
crate::util::reap_killed_search_child(&mut child).await;
|
||||
0
|
||||
} else {
|
||||
status.and_then(|s| s.code()).unwrap_or(-1)
|
||||
child.wait().await.ok().and_then(|s| s.code()).unwrap_or(-1)
|
||||
};
|
||||
|
||||
let wall_ms = stream_started.elapsed().as_millis() as u64;
|
||||
|
||||
@@ -161,7 +161,8 @@ impl kigi_tool_runtime::Tool for GlobTool {
|
||||
.arg(&input.pattern)
|
||||
.arg(&search_dir)
|
||||
.stdout(Stdio::piped())
|
||||
.stderr(Stdio::piped());
|
||||
// stderr is never read; a pipe would block rg once warnings fill it.
|
||||
.stderr(Stdio::null());
|
||||
crate::util::detach_command(&mut cmd);
|
||||
cmd.stdin(Stdio::null());
|
||||
|
||||
@@ -206,16 +207,13 @@ impl kigi_tool_runtime::Tool for GlobTool {
|
||||
}
|
||||
}
|
||||
|
||||
// Consume stderr to avoid deadlocks.
|
||||
if let Some(stderr_pipe) = child.stderr.take() {
|
||||
let _ = stderr_pipe
|
||||
.take(1_000_000)
|
||||
.read_to_end(&mut Vec::new())
|
||||
.await;
|
||||
if truncated_by_bytes {
|
||||
// Bounded reap: a D-state rg must not stall this future forever.
|
||||
crate::util::reap_killed_search_child(&mut child).await;
|
||||
} else {
|
||||
let _ = child.wait().await;
|
||||
}
|
||||
|
||||
let _ = child.wait().await;
|
||||
|
||||
let stdout = String::from_utf8_lossy(&stdout_buf);
|
||||
let mut truncated = truncated_by_bytes;
|
||||
|
||||
|
||||
@@ -29,6 +29,7 @@ pub use path_suggestions::format_not_found_error;
|
||||
pub use remap::{remap_json_keys, remap_schema_properties, reverse_map};
|
||||
pub use spawn::{
|
||||
ProcessGroup, ProcessScope, detach_command, global_process_scope, new_process_group,
|
||||
reap_killed_search_child,
|
||||
};
|
||||
pub use truncate::{
|
||||
DEFAULT_SOFT_WRAP_WIDTH, ceil_char_boundary, estimate_tokens, floor_char_boundary,
|
||||
|
||||
@@ -8,3 +8,19 @@
|
||||
pub use kigi_tty_utils::{
|
||||
ProcessGroup, ProcessScope, detach_command, global_process_scope, new_process_group,
|
||||
};
|
||||
|
||||
/// Reap an already-killed search child, bounded by
|
||||
/// [`kigi_tty_utils::KILL_REAP_TIMEOUT`]; on `None` warn and leave the corpse
|
||||
/// to tokio's orphan reaper.
|
||||
pub async fn reap_killed_search_child(
|
||||
child: &mut tokio::process::Child,
|
||||
) -> Option<std::process::ExitStatus> {
|
||||
let status = kigi_tty_utils::reap_killed_bounded(child, kigi_tty_utils::KILL_REAP_TIMEOUT).await;
|
||||
if status.is_none() {
|
||||
tracing::warn!(
|
||||
reap_timeout_secs = kigi_tty_utils::KILL_REAP_TIMEOUT.as_secs(),
|
||||
"killed search child not reaped (bound expired — likely uninterruptible kernel I/O — or wait failed); abandoning to the orphan reaper"
|
||||
);
|
||||
}
|
||||
status
|
||||
}
|
||||
Reference in new issue
Block a user