aboutsummaryrefslogtreecommitdiff
path: root/src/app/input
diff options
context:
space:
mode:
authorakbash <akbash@herdr.dev>2026-07-13 17:25:52 +0300
committerGitHub <noreply@github.com>2026-07-13 17:25:52 +0300
commit75ed6abc6258f09af5d4fc995a5004c1abf14fc2 (patch)
tree7dd561becfc189f2dbe6f153d0c870ac913351ae /src/app/input
parent6e85ff07f188186edcec8ac0e8263f899c7f1f26 (diff)
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>
Diffstat (limited to 'src/app/input')
-rw-r--r--src/app/input/navigate.rs49
1 files changed, 41 insertions, 8 deletions
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::<u32>()
+ .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)]