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