diff --git a/server/src/agents/gates.rs b/server/src/agents/gates.rs index 26858ec..53436dd 100644 --- a/server/src/agents/gates.rs +++ b/server/src/agents/gates.rs @@ -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, 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) => {