mirror of
https://github.com/pchuan98/codex.git
synced 2026-07-01 00:31:56 +08:00
Avoid hang when tool's process spawns grandchild that shares stderr/stdout (#6575)
We've received many reports of codex hanging when calling certain tools. [Here](https://github.com/openai/codex/issues/3204) is one example. This is likely a major cause. The problem occurs when `consume_truncated_output` waits for `stdout` and `stderr` to be closed once the child process terminates. This normally works fine, but it doesn't handle the case where the child has spawned grandchild processes that inherits `stdout` and `stderr`. The fix was originally written by @md-oai in [this PR](https://github.com/openai/codex/pull/1852), which has gone stale. I've copied the original fix (which looks sound to me) and added an integration test to prevent future regressions.
This commit is contained in:
committed by
GitHub
Unverified
parent
ad7eaa80f9
commit
73ed30d7e5
@@ -532,8 +532,52 @@ 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<StreamOutput<Vec<u8>>>>,
|
||||
timeout: Duration,
|
||||
) -> std::io::Result<StreamOutput<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(StreamOutput {
|
||||
text: Vec::new(),
|
||||
truncated_after_lines: None,
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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);
|
||||
|
||||
|
||||
Reference in New Issue
Block a user