use std::path::Path;
use std::time::Duration;
use anyhow::{Context, Result};
use worktrunk::trace::CommandTrace;
const POLL_INTERVAL: Duration = Duration::from_millis(250);
pub(crate) fn step_tether(command: &[String]) -> Result<()> {
let worktree = std::env::current_dir().ok();
let mut cmd = std::process::Command::new(&command[0]);
cmd.args(&command[1..]);
worktrunk::shell_exec::scrub_directive_env_vars(&mut cmd);
set_new_process_group(&mut cmd);
let mut trace = CommandTrace::new(None, &command.join(" "));
let mut child = match cmd.spawn() {
Ok(child) => child,
Err(e) => {
trace.fail(&e);
return Err(e).with_context(|| format!("spawn tethered command: {}", command[0]));
}
};
let id = child.id();
if let Some(dir) = worktree {
std::thread::spawn(move || {
while !worktree_gone(&dir) {
std::thread::sleep(POLL_INTERVAL);
}
kill_process_tree(id);
});
}
match child.wait() {
Ok(status) => trace.complete(status.success()),
Err(e) => trace.fail(&e),
}
kill_process_tree(id);
Ok(())
}
fn worktree_gone(worktree: &Path) -> bool {
matches!(
std::fs::symlink_metadata(worktree),
Err(e) if e.kind() == std::io::ErrorKind::NotFound
)
}
#[cfg(unix)]
fn set_new_process_group(cmd: &mut std::process::Command) {
use std::os::unix::process::CommandExt;
cmd.process_group(0);
}
#[cfg(windows)]
fn set_new_process_group(cmd: &mut std::process::Command) {
use std::os::windows::process::CommandExt;
cmd.creation_flags(0x0000_0200);
}
#[cfg(unix)]
fn kill_process_tree(pid: u32) {
worktrunk::shell_exec::forward_signal_with_escalation(pid as i32, signal_hook::consts::SIGTERM);
}
#[cfg(windows)]
fn kill_process_tree(pid: u32) {
let _ = std::process::Command::new("taskkill")
.args(["/T", "/F", "/PID", &pid.to_string()])
.stdout(std::process::Stdio::null())
.stderr(std::process::Stdio::null())
.status();
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn step_tether_spawn_failure_resolves_trace() {
let err = step_tether(&["worktrunk-nonexistent-binary-7f3a9b2c".to_string()])
.expect_err("spawning a missing binary should fail");
assert!(
err.to_string().contains("spawn tethered command"),
"expected spawn-failure context, got: {err}"
);
}
}