Skip to main content

procutils_pkill/
lib.rs

1use clap::Parser;
2use procutils_common::{
3    MAX_TERM_WIDTH,
4    man::ManContent,
5    procmatch::{MatchOptions, ProcessInfo},
6    signal::parse_signum_any,
7};
8use std::process::ExitCode;
9
10/// Lift the first `-SIG` / `-NUM` / `-NAME` argument out of `argv` and
11/// rewrite it as `--signal SIG`, so the rest of the args parse normally.
12/// The signal name can include or omit the `SIG` prefix; only the first
13/// matching argument is consumed (subsequent ones are passed through
14/// unchanged).
15pub fn preprocess_argv(argv: Vec<String>) -> Vec<String> {
16    let mut out = Vec::with_capacity(argv.len() + 1);
17    let mut iter = argv.into_iter();
18    if let Some(arg0) = iter.next() {
19        out.push(arg0);
20    }
21    let mut found = false;
22    for arg in iter {
23        if !found
24            && arg.starts_with('-')
25            && !arg.starts_with("--")
26            && arg.len() > 1
27            && is_signal_spec(&arg[1..])
28        {
29            out.push("--signal".to_string());
30            out.push(arg[1..].to_string());
31            found = true;
32        } else {
33            out.push(arg);
34        }
35    }
36    out
37}
38
39/// Returns true if `s` is a recognised signal name or number — any
40/// standard name, real-time name (`RTMIN`, `RTMIN+5`, `RTMAX-3`, …),
41/// or numeric form including `"0"` (the null signal `parse_signum_any`
42/// rejects because `rustix::Signal` cannot represent zero).
43fn is_signal_spec(s: &str) -> bool {
44    s == "0" || parse_signum_any(s).is_some()
45}
46
47pub const MAN: ManContent = ManContent {
48    description: Some(include_str!("../man/description.man")),
49    extra_sections: &[
50        ("EXAMPLES", include_str!("../man/examples.man")),
51        ("NOTES", include_str!("../man/notes.man")),
52        ("DIVERGENCES", include_str!("../man/divergences.man")),
53        ("SEE ALSO", include_str!("../man/see_also.man")),
54    ],
55};
56
57const SIGNAL: &str = "Signal";
58const MATCHING: &str = "Matching";
59const FILTERS: &str = "Filters";
60const SELECTION: &str = "Selection";
61const OUTPUT: &str = "Output";
62
63/// Signal processes based on name and other attributes.
64#[derive(Parser)]
65#[command(name = "pkill", version, about, max_term_width = MAX_TERM_WIDTH)]
66pub struct Args {
67    /// Signal to send (name or number).
68    #[arg(long, default_value = "TERM", help_heading = SIGNAL)]
69    signal: String,
70
71    /// Use sigqueue(3) to deliver the signal with an integer payload
72    /// readable via `siginfo_t::si_value.sival_int` in the receiving
73    /// handler.
74    #[arg(short = 'q', long, value_name = "VALUE", help_heading = SIGNAL)]
75    queue: Option<i32>,
76
77    /// Match against the full command line instead of just the process name.
78    #[arg(short, long, help_heading = MATCHING)]
79    full: bool,
80
81    /// Match processes case-insensitively.
82    #[arg(short, long, help_heading = MATCHING)]
83    ignore_case: bool,
84
85    /// Only match processes whose names (or command lines if -f) exactly match the pattern.
86    #[arg(short = 'x', long, help_heading = MATCHING)]
87    exact: bool,
88
89    /// Match only processes which match the process state.
90    #[arg(short = 'r', long, value_delimiter = ',', help_heading = FILTERS)]
91    runstates: Option<Vec<char>>,
92
93    /// Match against process environment: `NAME` (any value) or
94    /// `NAME=VALUE` (exact pair). Reads `/proc/[pid]/environ`;
95    /// processes whose environ is unreadable are skipped.
96    #[arg(long, value_name = "NAME[=VALUE]", help_heading = FILTERS)]
97    env: Option<String>,
98
99    /// Select processes older than the given number of seconds.
100    #[arg(short = 'O', long, help_heading = FILTERS)]
101    older: Option<f64>,
102
103    /// Only match processes whose process ID is listed.
104    #[arg(short = 'p', long = "pid", value_delimiter = ',', help_heading = FILTERS)]
105    pid: Option<Vec<i32>>,
106
107    /// Read process IDs from FILE, one per line. Cannot be combined
108    /// with `-p`.
109    #[arg(
110        short = 'F',
111        long = "pidfile",
112        value_name = "FILE",
113        conflicts_with = "pid",
114        help_heading = FILTERS,
115    )]
116    pidfile: Option<std::path::PathBuf>,
117
118    /// Only match processes whose parent process ID is listed.
119    #[arg(short = 'P', long, value_delimiter = ',', help_heading = FILTERS)]
120    parent: Option<Vec<i32>>,
121
122    /// Only match processes in the process group IDs listed.
123    #[arg(short = 'g', long = "pgroup", value_delimiter = ',', help_heading = FILTERS)]
124    pgroup: Option<Vec<i32>>,
125
126    /// Only match processes whose real group ID is listed.
127    #[arg(short = 'G', long = "group", value_delimiter = ',', help_heading = FILTERS)]
128    group: Option<Vec<u32>>,
129
130    /// Only match processes whose process session ID is listed.
131    #[arg(short = 's', long, value_delimiter = ',', help_heading = FILTERS)]
132    session: Option<Vec<i32>>,
133
134    /// Only match processes whose controlling terminal is listed.
135    #[arg(short = 't', long = "terminal", value_delimiter = ',', help_heading = FILTERS)]
136    terminal: Option<Vec<String>>,
137
138    /// Only match processes whose effective user ID is listed.
139    #[arg(short = 'u', long = "euid", value_delimiter = ',', help_heading = FILTERS)]
140    euid: Option<Vec<String>>,
141
142    /// Only match processes whose real user ID is listed.
143    #[arg(short = 'U', long = "uid", value_delimiter = ',', help_heading = FILTERS)]
144    uid: Option<Vec<String>>,
145
146    /// Select only the newest (most recently started) of the matching processes.
147    #[arg(short, long, help_heading = SELECTION)]
148    newest: bool,
149
150    /// Select only the oldest (least recently started) of the matching processes.
151    #[arg(short, long, help_heading = SELECTION)]
152    oldest: bool,
153
154    /// Suppress signaling; instead print a count of matching processes.
155    #[arg(short, long, help_heading = OUTPUT)]
156    count: bool,
157
158    /// Display name and PID of each process being signalled.
159    #[arg(short, long, help_heading = OUTPUT)]
160    echo: bool,
161
162    /// Extended regular expression pattern.
163    pattern: Option<String>,
164}
165
166/// Parse a signal spec for `pkill`. Accepts everything
167/// [`parse_signum_any`] does (standard names, RT names like
168/// `RTMIN+5`, numeric form), plus `0` — the null/test signal that
169/// `pkill -0` uses to check whether a process exists.
170fn parse_signum(s: &str) -> Option<i32> {
171    if s == "0" {
172        return Some(0);
173    }
174    parse_signum_any(s)
175}
176
177fn send_signal(proc: &ProcessInfo, signum: i32) -> bool {
178    let rc = unsafe { libc::kill(proc.pid as libc::pid_t, signum) };
179    rc == 0
180}
181
182fn send_queued_signal(proc: &ProcessInfo, signum: i32, value: i32) -> bool {
183    // sigval is a union of `int sival_int` and `void *sival_ptr`. On
184    // 64-bit platforms, sival_ptr is wider; setting it to the i32
185    // value's zero-extended form puts the bytes in the same place as
186    // sival_int as far as the receiver is concerned.
187    let sigval = libc::sigval {
188        sival_ptr: value as usize as *mut libc::c_void,
189    };
190    let rc = unsafe { libc::sigqueue(proc.pid as libc::pid_t, signum, sigval) };
191    rc == 0
192}
193
194pub fn run(args: Args) -> ExitCode {
195    let signum = match parse_signum(&args.signal) {
196        Some(n) => n,
197        None => {
198            eprintln!("pkill: unknown signal: {}", args.signal);
199            return ExitCode::from(2);
200        }
201    };
202
203    let pid_filter = match args.pidfile.as_deref() {
204        Some(path) => match procutils_common::procmatch::read_pidfile(path) {
205            Ok(pids) => Some(pids),
206            Err(e) => {
207                eprintln!("pkill: {e}");
208                return ExitCode::from(2);
209            }
210        },
211        None => args.pid.clone(),
212    };
213
214    let opts = MatchOptions {
215        pattern: args.pattern.clone().unwrap_or_default(),
216        full: args.full,
217        ignore_case: args.ignore_case,
218        pid: pid_filter,
219        exact: args.exact,
220        inverse: false,
221        newest: args.newest,
222        oldest: args.oldest,
223        older: args.older,
224        parent: args.parent,
225        pgroup: args.pgroup,
226        group: args.group,
227        session: args.session,
228        terminal: args.terminal,
229        euid: args.euid,
230        uid: args.uid,
231        runstates: args.runstates,
232        env: args.env,
233    };
234
235    if args.pattern.is_none() && !opts.has_filter() {
236        eprintln!("pkill: pattern is required");
237        return ExitCode::from(2);
238    }
239
240    let matches = match procutils_common::procmatch::find_matching_processes(
241        &opts, "pkill",
242    ) {
243        Ok(m) => m,
244        Err(code) => return code,
245    };
246
247    if matches.is_empty() {
248        return ExitCode::from(1);
249    }
250
251    if args.count {
252        println!("{}", matches.len());
253        return ExitCode::SUCCESS;
254    }
255
256    let mut any_failed = false;
257    for proc in &matches {
258        let sent = match args.queue {
259            Some(value) => send_queued_signal(proc, signum, value),
260            None => send_signal(proc, signum),
261        };
262        if sent {
263            if args.echo {
264                println!("{} killed (pid {})", proc.comm, proc.pid);
265            }
266        } else {
267            any_failed = true;
268        }
269    }
270
271    if any_failed {
272        ExitCode::FAILURE
273    } else {
274        ExitCode::SUCCESS
275    }
276}