From ec1f9473ede168ae1334905263ef767aa0996180 Mon Sep 17 00:00:00 2001 From: ghost <49853598+JSONbored@users.noreply.github.com> Date: Tue, 5 May 2026 05:42:27 -0600 Subject: [PATCH] fix(core): drain provider pipes while waiting for timeout --- crates/nightward-core/src/providers.rs | 44 +++++++++++++------ .../tests/provider_contracts.rs | 7 ++- 2 files changed, 35 insertions(+), 16 deletions(-) diff --git a/crates/nightward-core/src/providers.rs b/crates/nightward-core/src/providers.rs index d15ad80..f781334 100644 --- a/crates/nightward-core/src/providers.rs +++ b/crates/nightward-core/src/providers.rs @@ -8,6 +8,7 @@ use std::env; use std::io::Read; use std::path::{Path, PathBuf}; use std::process::{Command, Stdio}; +use std::thread; use std::time::Duration; use wait_timeout::ChildExt; @@ -161,6 +162,28 @@ pub fn run_provider(name: &str, root: &Path) -> Result> { .stderr(Stdio::piped()) .spawn() .with_context(|| format!("spawn provider {name}"))?; + + let stdout_handle = child.stdout.take().map(|mut stream| { + thread::spawn(move || { + let mut buf = Vec::new(); + let _ = stream + .by_ref() + .take(stdout_cap as u64 + 1) + .read_to_end(&mut buf); + buf + }) + }); + let stderr_handle = child.stderr.take().map(|mut stream| { + thread::spawn(move || { + let mut buf = Vec::new(); + let _ = stream + .by_ref() + .take(stderr_cap as u64 + 1) + .read_to_end(&mut buf); + buf + }) + }); + let status = match child.wait_timeout(timeout)? { Some(status) => status, None => { @@ -169,20 +192,13 @@ pub fn run_provider(name: &str, root: &Path) -> Result> { return Err(anyhow!("provider timed out after {:?}", timeout)); } }; - let mut stdout = Vec::new(); - let mut stderr = Vec::new(); - if let Some(mut stream) = child.stdout.take() { - let _ = stream - .by_ref() - .take(stdout_cap as u64 + 1) - .read_to_end(&mut stdout); - } - if let Some(mut stream) = child.stderr.take() { - let _ = stream - .by_ref() - .take(stderr_cap as u64 + 1) - .read_to_end(&mut stderr); - } + + let stdout = stdout_handle + .map(|handle| handle.join().unwrap_or_default()) + .unwrap_or_default(); + let stderr = stderr_handle + .map(|handle| handle.join().unwrap_or_default()) + .unwrap_or_default(); let (stdout, stdout_truncated) = capped_string(stdout, stdout_cap); let (stderr, _) = capped_string(stderr, stderr_cap); if stdout_truncated { diff --git a/crates/nightward-core/tests/provider_contracts.rs b/crates/nightward-core/tests/provider_contracts.rs index 68268aa..1c1fb87 100644 --- a/crates/nightward-core/tests/provider_contracts.rs +++ b/crates/nightward-core/tests/provider_contracts.rs @@ -91,12 +91,15 @@ fn provider_timeout_returns_stable_warning_error() { ("NIGHTWARD_PROVIDER_STDOUT_CAP", None), ]); let dir = tempfile::tempdir().expect("temp dir"); - write_executable(dir.path().join("gitleaks"), "#!/bin/sh\nsleep 1\n"); + write_executable(dir.path().join("gitleaks"), "#!/bin/sh\n/bin/sleep 1\n"); std::env::set_var("PATH", dir.path()); let error = run_provider("gitleaks", dir.path()).expect_err("timeout"); - assert!(error.to_string().contains("provider timed out after")); + assert!( + error.to_string().contains("provider timed out after"), + "actual error: {error}" + ); } #[cfg(unix)]