From 256d857d40525621cc55748466a5f333340e4f84 Mon Sep 17 00:00:00 2001 From: Mikola Lysenko Date: Fri, 9 Oct 2026 19:17:26 -0400 Subject: [PATCH] Fix ETXTBSY flake in process command_for spawn test command_for_spawns_the_resolved_path_directly_on_unix writes a shell shim, chmods it and spawns it at once. A sibling test thread that forks between our open() and its own exec() inherits the shim's write fd, and exec'ing the shim in that window fails with "Text file busy" (os error 26). Seen in the coverage job of CI run 37999238426. Spawn through a test-local output_of_fresh_shim helper that retries ErrorKind::ExecutableFileBusy only, with a 10s deadline and 20ms sleeps. This is the same pattern as sanity_exec in update/download.rs and the CLI's exec_freshly_written fixture. Any other spawn error still panics, and the assertions are unchanged. Co-Authored-By: Claude Opus 5.5 (1M context) --- crates/socket-patch-core/src/utils/process.rs | 26 ++++++++++++++++++- 1 file changed, 25 insertions(+), 1 deletion(-) diff --git a/crates/socket-patch-core/src/utils/process.rs b/crates/socket-patch-core/src/utils/process.rs index 605871227..d55e2e572 100644 --- a/crates/socket-patch-core/src/utils/process.rs +++ b/crates/socket-patch-core/src/utils/process.rs @@ -545,6 +545,30 @@ mod tests { let _ = path; } + /// Run a command whose program this test just wrote, tolerating + /// `ETXTBSY`. `fs::write` closed our handle, but tests run on parallel + /// threads: a sibling that forks between our `open` and its own `exec` + /// inherits the still-open write fd, and exec'ing the shim in that window + /// fails with "Text file busy" (os error 26). That is a fork/exec race, + /// not a property of the shim, so retry only that error, briefly — the + /// same pattern as `sanity_exec` and the CLI's `exec_freshly_written`. + #[cfg(unix)] + fn output_of_fresh_shim(make: impl Fn() -> Command) -> Output { + let deadline = Instant::now() + Duration::from_secs(10); + loop { + match make().output() { + Ok(out) => return out, + Err(e) + if e.kind() == std::io::ErrorKind::ExecutableFileBusy + && Instant::now() < deadline => + { + std::thread::sleep(Duration::from_millis(20)); + } + Err(e) => panic!("spawn the shim: {e:?}"), + } + } + } + /// An empty executable file — enough for the resolver, which only /// stats candidates. fn make_executable(path: &Path) { @@ -668,7 +692,7 @@ mod tests { 0, "no wrapper, no args of its own" ); - let out = command_for(&program).output().expect("spawn the shim"); + let out = output_of_fresh_shim(|| command_for(&program)); assert_eq!( String::from_utf8_lossy(&out.stdout), format!("resolved:{}", shim.display()),