From 0edce8c55576998da341802d055c0c236e93c83e Mon Sep 17 00:00:00 2001 From: M1 Date: Wed, 18 Mar 2026 13:03:06 +0400 Subject: [PATCH] fix: use spawn+manual read+wait instead of output() to avoid stdout pipe hang --- apps/monitor/src/runner.rs | 53 ++++++++++++++++++++++++++++---------- 1 file changed, 40 insertions(+), 13 deletions(-) diff --git a/apps/monitor/src/runner.rs b/apps/monitor/src/runner.rs index 5fcdfeb..ee76a9f 100644 --- a/apps/monitor/src/runner.rs +++ b/apps/monitor/src/runner.rs @@ -196,24 +196,51 @@ async fn run_curl( cmd.arg(url); - let output = tokio::time::timeout( - std::time::Duration::from_secs_f64(timeout_secs + 2.0), - cmd.output() - ).await - .map_err(|_| format!("timed out after {:.0}s", timeout_secs))? - .map_err(|e| format!("curl exec error: {e}"))?; + let mut child = cmd.spawn().map_err(|e| format!("curl spawn error: {e}"))?; - if !output.status.success() { - let stderr = String::from_utf8_lossy(&output.stderr).to_string(); - let msg = match output.status.code() { - Some(28) => format!("timed out after {:.0}s", timeout_secs), - Some(6) => "DNS lookup failed".to_string(), - Some(7) => "connection refused".to_string(), - _ => stderr.trim().to_string(), + let stdout_handle = child.stdout.take(); + let stderr_handle = child.stderr.take(); + + // Read stdout and stderr concurrently, then wait for process exit + let (stdout_bytes, stderr_bytes, status) = tokio::time::timeout( + std::time::Duration::from_secs_f64(timeout_secs + 2.0), + async { + let stdout_fut = async { + if let Some(mut out) = stdout_handle { + let mut buf = Vec::new(); + tokio::io::AsyncReadExt::read_to_end(&mut out, &mut buf).await.ok(); + buf + } else { Vec::new() } + }; + let stderr_fut = async { + if let Some(mut err) = stderr_handle { + let mut buf = Vec::new(); + tokio::io::AsyncReadExt::read_to_end(&mut err, &mut buf).await.ok(); + buf + } else { Vec::new() } + }; + let (out, err) = tokio::join!(stdout_fut, stderr_fut); + let status = child.wait().await; + (out, err, status) + } + ).await.map_err(|_| format!("timed out after {:.0}s", timeout_secs))?; + + let exit_code = status.ok().and_then(|s| s.code()).unwrap_or(-1); + + if exit_code != 0 { + let stderr = String::from_utf8_lossy(&stderr_bytes).to_string(); + let msg = match exit_code { + 28 => format!("timed out after {:.0}s", timeout_secs), + 6 => "DNS lookup failed".to_string(), + 7 => "connection refused".to_string(), + _ => stderr.trim().to_string(), }; return Err(msg); } + struct FakeOutput { stdout: Vec, stderr: Vec } + let output = FakeOutput { stdout: stdout_bytes, stderr: stderr_bytes }; + // Parse curl output: headers then blank line then body let raw = String::from_utf8_lossy(&output.stdout).to_string(); let mut parts = raw.splitn(2, "\r\n\r\n");