fix: use std::process::Command+spawn_blocking+temp file to avoid all pipe/async hang issues
This commit is contained in:
parent
289ec8e038
commit
5e76b2212f
|
|
@ -177,8 +177,9 @@ async fn run_check(client: &reqwest::Client, monitor: &Monitor, scheduled_at: Op
|
|||
}
|
||||
}
|
||||
|
||||
/// Run an HTTP check via curl subprocess — the only reliable way to enforce
|
||||
/// a hard timeout including TLS handshake on hosts that stall at that phase.
|
||||
/// Run an HTTP check via curl subprocess writing output to a temp file.
|
||||
/// Using a temp file instead of a pipe avoids async read_to_end hanging
|
||||
/// when curl exits without closing its inherited stdout properly.
|
||||
async fn run_curl(
|
||||
url: &str,
|
||||
method: &str,
|
||||
|
|
@ -186,79 +187,63 @@ async fn run_curl(
|
|||
body: Option<&str>,
|
||||
timeout_secs: f64,
|
||||
) -> Result<(u16, HashMap<String, String>, String), String> {
|
||||
let mut cmd = tokio::process::Command::new("curl");
|
||||
cmd.arg("--silent")
|
||||
.arg("--show-error")
|
||||
.arg("--include") // include response headers in output
|
||||
.arg("--max-time").arg(format!("{:.1}", timeout_secs))
|
||||
.arg("--connect-timeout").arg(format!("{:.1}", timeout_secs))
|
||||
.arg("-X").arg(method)
|
||||
.arg("--user-agent").arg("PingQL-Monitor/0.1")
|
||||
.arg("--location") // follow redirects
|
||||
.stdin(std::process::Stdio::null())
|
||||
.stdout(std::process::Stdio::piped())
|
||||
.stderr(std::process::Stdio::piped())
|
||||
.kill_on_drop(true); // kill if future is dropped
|
||||
use std::process::Stdio;
|
||||
|
||||
// Write output to a temp file so we just wait for the process to exit
|
||||
let tmp = format!("/tmp/pingql-curl-{}.txt", std::process::id());
|
||||
|
||||
let mut args: Vec<String> = vec![
|
||||
"--silent".into(),
|
||||
"--show-error".into(),
|
||||
"--include".into(),
|
||||
"--max-time".into(), format!("{:.1}", timeout_secs),
|
||||
"--connect-timeout".into(), format!("{:.1}", timeout_secs),
|
||||
"-X".into(), method.to_string(),
|
||||
"--user-agent".into(), "PingQL-Monitor/0.1".into(),
|
||||
"--location".into(),
|
||||
"--output".into(), tmp.clone(),
|
||||
];
|
||||
|
||||
if let Some(hdrs) = headers {
|
||||
for (k, v) in hdrs {
|
||||
cmd.arg("-H").arg(format!("{k}: {v}"));
|
||||
args.push("-H".into());
|
||||
args.push(format!("{k}: {v}"));
|
||||
}
|
||||
}
|
||||
|
||||
if let Some(b) = body {
|
||||
cmd.arg("--data-raw").arg(b);
|
||||
args.push("--data-raw".into());
|
||||
args.push(b.to_string());
|
||||
}
|
||||
args.push(url.to_string());
|
||||
|
||||
cmd.arg(url);
|
||||
// Run curl synchronously in a blocking thread — simple and reliable
|
||||
let args_owned = args.clone();
|
||||
let tmp_owned = tmp.clone();
|
||||
let result = tokio::task::spawn_blocking(move || {
|
||||
let status = std::process::Command::new("curl")
|
||||
.args(&args_owned)
|
||||
.stdin(Stdio::null())
|
||||
.stdout(Stdio::null())
|
||||
.stderr(Stdio::piped())
|
||||
.status();
|
||||
let output = std::fs::read(&tmp_owned).unwrap_or_default();
|
||||
let _ = std::fs::remove_file(&tmp_owned);
|
||||
(status, output)
|
||||
}).await.map_err(|e| format!("spawn_blocking error: {e}"))?;
|
||||
|
||||
let mut child = cmd.spawn().map_err(|e| format!("curl spawn error: {e}"))?;
|
||||
|
||||
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);
|
||||
let (status_result, raw_bytes) = result;
|
||||
let exit_code = status_result.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 {
|
||||
return Err(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);
|
||||
_ => format!("curl error (exit {})", exit_code),
|
||||
});
|
||||
}
|
||||
|
||||
struct FakeOutput { stdout: Vec<u8>, stderr: Vec<u8> }
|
||||
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 raw = String::from_utf8_lossy(&raw_bytes).to_string();
|
||||
let mut parts = raw.splitn(2, "\r\n\r\n");
|
||||
let header_block = parts.next().unwrap_or("");
|
||||
let body_str = parts.next().unwrap_or("").to_string();
|
||||
|
|
|
|||
Loading…
Reference in New Issue