Skip to main content

command_stream/zx/
kill.rs

1//! Process-tree termination (zx `kill(pid, signal)`).
2
3use super::error::ZxError;
4
5/// Default signal for kills and timeouts.
6pub const SIGTERM: &str = "SIGTERM";
7
8/// Validate a pid given as text: it must consist of ASCII digits only.
9pub fn validate_pid(pid: &str) -> Result<u32, ZxError> {
10    if pid.is_empty() || !pid.bytes().all(|b| b.is_ascii_digit()) {
11        return Err(ZxError::new(format!("Invalid pid: {pid}")));
12    }
13    pid.parse::<u32>()
14        .map_err(|_| ZxError::new(format!("Invalid pid: {pid}")))
15}
16
17/// Normalise a signal given as `SIGTERM`, `TERM` or a number.
18#[cfg(unix)]
19pub fn parse_signal(signal: &str) -> Result<nix::sys::signal::Signal, ZxError> {
20    use nix::sys::signal::Signal;
21    use std::str::FromStr;
22    let unknown = || ZxError::new(format!("Unknown signal: {signal}"));
23    if let Ok(num) = signal.parse::<i32>() {
24        return Signal::try_from(num).map_err(|_| unknown());
25    }
26    let upper = signal.to_ascii_uppercase();
27    let name = if upper.starts_with("SIG") {
28        upper
29    } else {
30        format!("SIG{upper}")
31    };
32    Signal::from_str(&name).map_err(|_| unknown())
33}
34
35/// Name (`SIGTERM`, ...) of a numeric signal, if it is known.
36#[cfg(unix)]
37pub fn signal_name(num: i32) -> Option<String> {
38    nix::sys::signal::Signal::try_from(num)
39        .ok()
40        .map(|s| s.as_str().to_string())
41}
42
43/// Name of a numeric signal (not available on this platform).
44#[cfg(not(unix))]
45pub fn signal_name(_num: i32) -> Option<String> {
46    None
47}
48
49/// Pids of all descendants of `pid` (children first, depth-first), using `ps`.
50#[cfg(unix)]
51pub fn descendants(pid: u32) -> Vec<u32> {
52    let listing = std::process::Command::new("ps")
53        .args(["-A", "-o", "pid=", "-o", "ppid="])
54        .stderr(std::process::Stdio::null())
55        .output();
56    let Ok(listing) = listing else {
57        return Vec::new();
58    };
59    let table: Vec<(u32, u32)> = String::from_utf8_lossy(&listing.stdout)
60        .lines()
61        .filter_map(|line| {
62            let mut cols = line.split_whitespace();
63            let child = cols.next()?.parse().ok()?;
64            let parent = cols.next()?.parse().ok()?;
65            Some((child, parent))
66        })
67        .collect();
68    let mut found = Vec::new();
69    let mut queue = vec![pid];
70    while let Some(parent) = queue.pop() {
71        for &(child, ppid) in &table {
72            if ppid == parent && child != pid && !found.contains(&child) {
73                found.push(child);
74                queue.push(child);
75            }
76        }
77    }
78    found
79}
80
81/// Pids of all descendants of `pid` (not supported on this platform).
82#[cfg(not(unix))]
83pub fn descendants(_pid: u32) -> Vec<u32> {
84    Vec::new()
85}
86
87/// Send `signal` to `pid`, all of its descendants and its process group.
88#[cfg(unix)]
89pub fn kill_tree(pid: u32, signal: &str) -> Result<(), ZxError> {
90    use nix::sys::signal::kill as send;
91    use nix::unistd::Pid;
92    let sig = parse_signal(signal)?;
93    let raw = i32::try_from(pid).map_err(|_| ZxError::new(format!("Invalid pid: {pid}")))?;
94    let children = descendants(pid);
95    // Signal the process before its descendants (zx does the reverse). A shell
96    // that forked its command and sees that child die first can still report the
97    // death as exit code 128+n before its own signal lands, instead of dying by
98    // the signal.
99    if send(Pid::from_raw(-raw), sig).is_err() {
100        let _ = send(Pid::from_raw(raw), sig);
101    }
102    for child in children {
103        if let Ok(child) = i32::try_from(child) {
104            let _ = send(Pid::from_raw(child), sig);
105        }
106    }
107    Ok(())
108}
109
110/// Terminate `pid` and its descendants with `taskkill /t /f`.
111#[cfg(not(unix))]
112pub fn kill_tree(pid: u32, _signal: &str) -> Result<(), ZxError> {
113    let status = std::process::Command::new("taskkill")
114        .args(["/pid", &pid.to_string(), "/t", "/f"])
115        .stdout(std::process::Stdio::null())
116        .stderr(std::process::Stdio::null())
117        .status()?;
118    if status.success() {
119        Ok(())
120    } else {
121        Err(ZxError::new(format!("taskkill failed for pid {pid}")))
122    }
123}
124
125/// Kill a process and all of its descendants (zx `kill()`).
126///
127/// `pid` must render as a string of digits; `signal` defaults to the current
128/// scope's `kill_signal` (`SIGTERM`).
129pub async fn kill(pid: impl ToString, signal: Option<&str>) -> Result<(), ZxError> {
130    let pid = validate_pid(&pid.to_string())?;
131    let signal = match signal {
132        Some(s) => s.to_string(),
133        None => super::shell::current_options().kill_signal,
134    };
135    tokio::task::spawn_blocking(move || kill_tree(pid, &signal))
136        .await
137        .map_err(|e| ZxError::new(e.to_string()))?
138}