diff --git a/apps/monitor/src/runner.rs b/apps/monitor/src/runner.rs index 8d0601e..9e7870d 100644 --- a/apps/monitor/src/runner.rs +++ b/apps/monitor/src/runner.rs @@ -216,22 +216,32 @@ async fn run_curl( } args.push(url.to_string()); - // Run curl synchronously in a blocking thread — simple and reliable + // Run curl in a real OS thread, signal completion via tokio oneshot. + // std::thread detaches completely from tokio's runtime, so a hung curl + // process can't block other monitors. The tokio::time::timeout on the + // oneshot receiver gives us a hard async deadline. let args_owned = args.clone(); let tmp_owned = tmp.clone(); - let result = tokio::task::spawn_blocking(move || { + + let (tx, rx) = tokio::sync::oneshot::channel::<(std::io::Result, Vec)>(); + std::thread::spawn(move || { let status = std::process::Command::new("curl") .args(&args_owned) .stdin(Stdio::null()) .stdout(Stdio::null()) - .stderr(Stdio::piped()) + .stderr(Stdio::null()) .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 _ = tx.send((status, output)); + }); - let (status_result, raw_bytes) = result; + let (status_result, raw_bytes) = tokio::time::timeout( + std::time::Duration::from_secs_f64(timeout_secs + 2.0), + rx + ).await + .map_err(|_| format!("timed out after {:.0}s", timeout_secs))? + .map_err(|_| "curl thread dropped".to_string())?; let exit_code = status_result.ok().and_then(|s| s.code()).unwrap_or(-1); if exit_code != 0 {