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:
@@ -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) => {
|
||||
|
||||
Reference in New Issue
Block a user