Fix pipe buffer deadlock in quality gate test runner

run_command_with_timeout piped stdout/stderr but only read them after
the child exited. When test output exceeded the 64KB OS pipe buffer,
the child blocked on write() while the parent blocked on waitpid() —
a permanent deadlock that caused every merge pipeline to hang.

Drain both pipes in background threads so the buffers never fill.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Dave
2026-03-17 12:49:12 +00:00
parent 5ed2737edc
commit 076324c470

View File

@@ -93,6 +93,10 @@ pub(crate) fn run_project_tests(path: &Path) -> Result<(bool, String), String> {
/// Run a command with a timeout. Returns `(success, combined_output)`.
/// Kills the child process if it exceeds `TEST_TIMEOUT`.
///
/// Stdout and stderr are drained in background threads to avoid a pipe-buffer
/// deadlock: if the child fills the 64 KB OS pipe buffer while the parent
/// blocks on `waitpid`, neither side can make progress.
fn run_command_with_timeout(
program: impl AsRef<std::ffi::OsStr>,
args: &[&str],
@@ -106,19 +110,32 @@ fn run_command_with_timeout(
.spawn()
.map_err(|e| format!("Failed to spawn command: {e}"))?;
// Drain stdout/stderr in background threads so the pipe buffers never fill.
let stdout_handle = child.stdout.take().map(|r| {
std::thread::spawn(move || {
let mut s = String::new();
let mut r = r;
std::io::Read::read_to_string(&mut r, &mut s).ok();
s
})
});
let stderr_handle = child.stderr.take().map(|r| {
std::thread::spawn(move || {
let mut s = String::new();
let mut r = r;
std::io::Read::read_to_string(&mut r, &mut s).ok();
s
})
});
match child.wait_timeout(TEST_TIMEOUT) {
Ok(Some(status)) => {
// Process exited within the timeout — collect output.
let stdout = child.stdout.take().map(|mut r| {
let mut s = String::new();
std::io::Read::read_to_string(&mut r, &mut s).ok();
s
}).unwrap_or_default();
let stderr = child.stderr.take().map(|mut r| {
let mut s = String::new();
std::io::Read::read_to_string(&mut r, &mut s).ok();
s
}).unwrap_or_default();
let stdout = stdout_handle
.and_then(|h| h.join().ok())
.unwrap_or_default();
let stderr = stderr_handle
.and_then(|h| h.join().ok())
.unwrap_or_default();
Ok((status.success(), format!("{stdout}{stderr}")))
}
Ok(None) => {