From 75ed6abc6258f09af5d4fc995a5004c1abf14fc2 Mon Sep 17 00:00:00 2001 From: akbash Date: Mon, 13 Jul 2026 17:25:52 +0300 Subject: fix: reap detached custom command children (#1384) * fix: reap detached custom command children refs #1360 * test: harden child cleanup and sidebar metrics refs #1360 --------- Co-authored-by: akbash-bot <300245827+akbash-bot@users.noreply.github.com> --- src/app/input/navigate.rs | 49 +++++++++++++++++++++++++++++++++++++++-------- 1 file changed, 41 insertions(+), 8 deletions(-) (limited to 'src/app/input') diff --git a/src/app/input/navigate.rs b/src/app/input/navigate.rs index a1597669..7f98f530 100644 --- a/src/app/input/navigate.rs +++ b/src/app/input/navigate.rs @@ -843,7 +843,7 @@ impl App { } fn spawn_custom_command( - &self, + &mut self, binding: &crate::config::CustomCommandKeybind, ) -> std::io::Result<()> { let mut command = crate::platform::detached_custom_command_process(&binding.command); @@ -856,7 +856,8 @@ impl App { if let Some(cwd) = cwd { command.current_dir(cwd); } - command.spawn()?; + let child = command.spawn()?; + self.detached_custom_command_children.push(child); Ok(()) } @@ -2943,9 +2944,11 @@ navigate_pane_down = "ctrl+j" app.state.mode = Mode::Terminal; let output_path = unique_temp_path("custom-command-keybind"); + let release_path = unique_temp_path("custom-command-release"); let command = format!( - "printf '%s\\n%s\\n%s\\n' \"$HERDR_ACTIVE_WORKSPACE_ID\" \"$HERDR_ACTIVE_TAB_ID\" \"$HERDR_ACTIVE_PANE_ID\" > '{}'", - output_path.display() + "printf '%s\\n%s\\n%s\\n%s\\n' \"$$\" \"$HERDR_ACTIVE_WORKSPACE_ID\" \"$HERDR_ACTIVE_TAB_ID\" \"$HERDR_ACTIVE_PANE_ID\" > '{}'; i=0; while [ ! -e '{}' ] && [ \"$i\" -lt 250 ]; do sleep 0.02; i=$((i + 1)); done", + output_path.display(), + release_path.display(), ); app.state.keybinds.custom_commands = vec![crate::config::CustomCommandKeybind { bindings: crate::config::ActionKeybinds::prefix("m"), @@ -2962,18 +2965,48 @@ navigate_pane_down = "ctrl+j" .await; assert_eq!(app.state.mode, Mode::Prefix); + let launch_started = std::time::Instant::now(); app.handle_key(TerminalKey::new(KeyCode::Char('m'), KeyModifiers::empty())) .await; + assert!(launch_started.elapsed() < Duration::from_secs(2)); let content = wait_for_file(&output_path); let lines: Vec<&str> = content.lines().collect(); - assert_eq!(lines.len(), 3); - assert_eq!(lines[0], app.state.workspaces[0].id); - assert_eq!(lines[1], format!("{}:t1", app.state.workspaces[0].id)); - assert_eq!(lines[2], format!("{}:p1", app.state.workspaces[0].id)); + assert_eq!(lines.len(), 4); + let pid = lines[0] + .parse::() + .expect("command should report its pid"); + assert!(crate::platform::process_exists(pid)); + assert_eq!(lines[1], app.state.workspaces[0].id); + assert_eq!(lines[2], format!("{}:t1", app.state.workspaces[0].id)); + assert_eq!(lines[3], format!("{}:p1", app.state.workspaces[0].id)); assert_eq!(app.state.mode, Mode::Terminal); + std::fs::write(&release_path, b"release").expect("release command"); + let deadline = tokio::time::Instant::now() + Duration::from_secs(2); + while crate::platform::process_exists(pid) && tokio::time::Instant::now() < deadline { + app.reap_finished_custom_commands(); + tokio::time::sleep(Duration::from_millis(20)).await; + } + app.reap_finished_custom_commands(); + let reaped_by_runtime = !crate::platform::process_exists(pid); + if !reaped_by_runtime { + if let Some(child) = app + .detached_custom_command_children + .iter_mut() + .find(|child| child.id() == pid) + { + let _ = child.kill(); + let _ = child.wait(); + } + } + assert!( + reaped_by_runtime, + "detached command child {pid} was not reaped" + ); + let _ = std::fs::remove_file(output_path); + let _ = std::fs::remove_file(release_path); } #[cfg(unix)] -- cgit v1.2.3