Compare commits

...

4 Commits

Author SHA1 Message Date
Eric Traut
02b4908337 Merge branch 'main' into kill-grandchildren 2025-11-12 19:05:18 -06:00
Misha Davidov
3f7d3834c6 exec: timeout on grandchildren
We were enforcing the 10 s wall-clock limit only on the child process.
If that child (bash) spawns grandchildren and we kill it on timeout, those
grandchildren still have the original stdout/err pipe open, so the background
tasks that are draining the pipes block forever
2025-08-05 14:00:26 -07:00
Misha Davidov
8782704b0d exec: timeout on grandchildren
We were enforcing the 10 s wall-clock limit only on the child process.
If that child (bash) spawns grandchildren and we kill it on timeout, those
grandchildren still have the original stdout/err pipe open, so the background
tasks that are draining the pipes block forever
2025-08-05 11:12:10 -07:00
Misha Davidov
ab76bfbe75 exec: timeout on grandchildren
We were enforcing the 10 s wall-clock limit only on the child process.
If that child (bash) spawns grandchildren and we kill it on timeout, those
grandchildren still have the original stdout/err pipe open, so the background
tasks that are draining the pipes block forever
2025-08-05 08:48:29 -07:00

View File

@@ -532,8 +532,49 @@ async fn consume_truncated_output(
}
};
let stdout = stdout_handle.await??;
let stderr = stderr_handle.await??;
// Wait for the stdout/stderr collection tasks but guard against them
// hanging forever. In the normal case both pipes are closed once the child
// terminates so the tasks exit quickly. However, if the child process
// spawned grandchildren that inherited its stdout/stderr file descriptors
// those pipes may stay open after we `kill` the direct child on timeout.
// That would cause the `read_capped` tasks to block on `read()`
// indefinitely, effectively hanging the whole agent.
const IO_DRAIN_TIMEOUT_MS: u64 = 2_000; // 2 s should be plenty for local pipes
// We need mutable bindings so we can `abort()` them on timeout.
use tokio::task::JoinHandle;
async fn await_with_timeout(
handle: &mut JoinHandle<std::io::Result<Vec<u8>>>,
timeout: Duration,
) -> std::io::Result<Vec<u8>> {
match tokio::time::timeout(timeout, &mut *handle).await {
Ok(join_res) => match join_res {
Ok(io_res) => io_res,
Err(join_err) => Err(std::io::Error::other(join_err)),
},
Err(_elapsed) => {
// Timeout: abort the task to avoid hanging on open pipes.
handle.abort();
Ok(Vec::new())
}
}
}
let mut stdout_handle = stdout_handle;
let mut stderr_handle = stderr_handle;
let stdout = await_with_timeout(
&mut stdout_handle,
Duration::from_millis(IO_DRAIN_TIMEOUT_MS),
)
.await?;
let stderr = await_with_timeout(
&mut stderr_handle,
Duration::from_millis(IO_DRAIN_TIMEOUT_MS),
)
.await?;
drop(agg_tx);