diff --git a/crates/tty7-core/src/core/proc.rs b/crates/tty7-core/src/core/proc.rs index 97fc84ff..0cd47f6c 100644 --- a/crates/tty7-core/src/core/proc.rs +++ b/crates/tty7-core/src/core/proc.rs @@ -67,6 +67,21 @@ pub fn output_within(cmd: &mut Command, timeout: Duration) -> io::Result }) } +/// Starts a command nobody will read back from — an opener like `open` or +/// `xdg-open` — and reaps it once it exits. Dropping a `Child` never waits on +/// it, so each fire-and-forget spawn would otherwise sit as a zombie under +/// this process until it quits. +pub fn spawn_reaped(cmd: &mut Command) -> io::Result { + let mut child = cmd.spawn()?; + let pid = child.id(); + std::thread::Builder::new() + .name("reap-child".into()) + .spawn(move || { + let _ = child.wait(); + })?; + Ok(pid) +} + fn drain(pipe: Option) -> Vec { let mut buf = Vec::new(); if let Some(mut pipe) = pipe { @@ -103,6 +118,30 @@ mod tests { ); } + #[test] + fn a_reaped_spawn_leaves_no_zombie_behind() { + let pid = spawn_reaped(&mut Command::new("true")).expect("true runs"); + // WNOWAIT only peeks, so this never collects the child itself: the + // pid stops being ours (ECHILD) only once the reaper has waited on it. + let deadline = Instant::now() + Duration::from_secs(10); + loop { + let mut info: libc::siginfo_t = unsafe { std::mem::zeroed() }; + let rc = unsafe { + libc::waitid( + libc::P_PID, + pid as libc::id_t, + &mut info, + libc::WEXITED | libc::WNOHANG | libc::WNOWAIT, + ) + }; + if rc == -1 && io::Error::last_os_error().raw_os_error() == Some(libc::ECHILD) { + break; + } + assert!(Instant::now() < deadline, "the child was never reaped"); + std::thread::sleep(REAP_POLL); + } + } + #[test] fn output_larger_than_a_pipe_buffer_does_not_deadlock() { let mut cmd = Command::new("sh"); diff --git a/src/core/update.rs b/src/core/update.rs index 38b5db21..ef0be8f5 100644 --- a/src/core/update.rs +++ b/src/core/update.rs @@ -975,7 +975,8 @@ pub fn open_url(url: &str) { } else { "xdg-open" }; - if let Err(e) = std::process::Command::new(opener).arg(url).spawn() { + if let Err(e) = tty7_core::core::proc::spawn_reaped(std::process::Command::new(opener).arg(url)) + { log::warn!("failed to open {url}: {e}"); } } diff --git a/src/terminal/view.rs b/src/terminal/view.rs index decd1931..24bef956 100644 --- a/src/terminal/view.rs +++ b/src/terminal/view.rs @@ -8442,7 +8442,7 @@ pub(crate) fn open_file_path(path: &std::path::Path) -> std::io::Result<()> { } else { "xdg-open" }; - std::process::Command::new(opener).arg(path).spawn()?; + tty7_core::core::proc::spawn_reaped(std::process::Command::new(opener).arg(path))?; Ok(()) } @@ -8476,7 +8476,7 @@ pub(crate) fn reveal_file_path(path: &std::path::Path) -> std::io::Result<()> { c.arg(path.parent().unwrap_or(path)); c }; - command.spawn()?; + tty7_core::core::proc::spawn_reaped(&mut command)?; Ok(()) } @@ -8496,7 +8496,7 @@ fn run_file_command( "link_file_command expanded to nothing", )); }; - std::process::Command::new(program).args(args).spawn()?; + tty7_core::core::proc::spawn_reaped(std::process::Command::new(program).args(args))?; Ok(()) } diff --git a/src/ui/app.rs b/src/ui/app.rs index 60b65d8f..db03e8a1 100644 --- a/src/ui/app.rs +++ b/src/ui/app.rs @@ -9146,7 +9146,9 @@ impl Tty7App { } else { "xdg-open" }; - if let Err(e) = std::process::Command::new(opener).arg(&path).spawn() { + if let Err(e) = + tty7_core::core::proc::spawn_reaped(std::process::Command::new(opener).arg(&path)) + { log::warn!("failed to open {}: {e}", path.display()); } } diff --git a/src/ui/code_editor.rs b/src/ui/code_editor.rs index 83c36687..ce3b9ef7 100644 --- a/src/ui/code_editor.rs +++ b/src/ui/code_editor.rs @@ -734,11 +734,12 @@ fn opens_in_browser(path: &Path) -> bool { fn open_in_browser(path: &Path) -> std::io::Result<()> { #[cfg(target_os = "macos")] if let Some(bundle) = default_browser_bundle_id() { - std::process::Command::new("open") - .arg("-b") - .arg(bundle) - .arg(path) - .spawn()?; + tty7_core::core::proc::spawn_reaped( + std::process::Command::new("open") + .arg("-b") + .arg(bundle) + .arg(path), + )?; return Ok(()); } crate::terminal::view::open_file_path(path)