@@ -259,24 +259,45 @@ async fn sanity_exec( |
259 | 259 | expected: &semver::Version, |
260 | 260 | strict: bool, |
261 | 261 | ) -> Result<Option<String>, UpdateError> { |
262 | | - let mut cmd = tokio::process::Command::new(staged); |
263 | | - cmd.arg("--version") |
264 | | - .stdin(std::process::Stdio::null()) |
265 | | - .stdout(std::process::Stdio::piped()) |
266 | | - .stderr(std::process::Stdio::null()) |
267 | | - .kill_on_drop(true); |
268 | | - let output = tokio::time::timeout(std::time::Duration::from_secs(10), cmd.output()) |
269 | | - .await |
270 | | - .map_err(|_| { |
271 | | - UpdateError::VerifyFailed( |
272 | | - "downloaded binary hung during its --version self-check".to_string(), |
273 | | - ) |
274 | | - })? |
275 | | - .map_err(|e| { |
276 | | - UpdateError::VerifyFailed(format!( |
277 | | - "downloaded binary failed to execute (wrong architecture?): {e}" |
278 | | - )) |
279 | | - })?; |
| 262 | + // ETXTBSY retry: between a sibling thread's fork() and its exec(), the |
| 263 | + // child briefly inherits every open fd — including a write fd on the |
| 264 | + // binary staged moments ago — and exec'ing the file during that window |
| 265 | + // fails with "Text file busy". The window is real for any multi-threaded |
| 266 | + // process (and bites the parallel test binary under coverage), so ride |
| 267 | + // it out with short sleeps instead of failing a fully verified download |
| 268 | + // — the same dance Go's os/exec and cargo do. |
| 269 | + const ETXTBSY_ATTEMPTS: u64 = 10; |
| 270 | + let mut attempt = 0u64; |
| 271 | + let output = loop { |
| 272 | + let mut cmd = tokio::process::Command::new(staged); |
| 273 | + cmd.arg("--version") |
| 274 | + .stdin(std::process::Stdio::null()) |
| 275 | + .stdout(std::process::Stdio::piped()) |
| 276 | + .stderr(std::process::Stdio::null()) |
| 277 | + .kill_on_drop(true); |
| 278 | + let result = tokio::time::timeout(std::time::Duration::from_secs(10), cmd.output()) |
| 279 | + .await |
| 280 | + .map_err(|_| { |
| 281 | + UpdateError::VerifyFailed( |
| 282 | + "downloaded binary hung during its --version self-check".to_string(), |
| 283 | + ) |
| 284 | + })?; |
| 285 | + match result { |
| 286 | + Ok(output) => break output, |
| 287 | + Err(e) |
| 288 | + if e.kind() == std::io::ErrorKind::ExecutableFileBusy |
| 289 | + && attempt < ETXTBSY_ATTEMPTS => |
| 290 | + { |
| 291 | + attempt += 1; |
| 292 | + tokio::time::sleep(std::time::Duration::from_millis(25 * attempt)).await; |
| 293 | + } |
| 294 | + Err(e) => { |
| 295 | + return Err(UpdateError::VerifyFailed(format!( |
| 296 | + "downloaded binary failed to execute (wrong architecture?): {e}" |
| 297 | + ))); |
| 298 | + } |
| 299 | + } |
| 300 | + }; |
280 | 301 | if !output.status.success() { |
281 | 302 | return Err(UpdateError::VerifyFailed(format!( |
282 | 303 | "downloaded binary's --version self-check exited with {}", |
|