mirror of
https://github.com/l0ng-ai/tty7.git
synced 2026-11-05 16:16:26 +00:00
fix: reap opener processes instead of leaving them as zombies (#1231)
Opening a file, link, URL or config.json, revealing a path, and the custom link_file_command all spawned `open` / `xdg-open` / `explorer` and dropped the Child. Dropping a Child never waits on it, so every one of these clicks left a defunct process under tty7-app for as long as the GUI ran. Add `core::proc::spawn_reaped`, which spawns the command and waits on it from a short-lived thread, and route all six fire-and-forget spawns through it.
This commit is contained in:
@@ -67,6 +67,21 @@ pub fn output_within(cmd: &mut Command, timeout: Duration) -> io::Result<Output>
|
||||
})
|
||||
}
|
||||
|
||||
/// 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<u32> {
|
||||
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<R: Read + Send + 'static>(pipe: Option<R>) -> Vec<u8> {
|
||||
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");
|
||||
|
||||
+2
-1
@@ -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}");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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(())
|
||||
}
|
||||
|
||||
|
||||
+3
-1
@@ -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());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
|
||||
Reference in New Issue
Block a user