procutils-pkill 0.2.0

Signal processes based on name and other attributes
Documentation
use clap::Parser;
use procutils_common::{
    MAX_TERM_WIDTH,
    man::ManContent,
    procmatch::{MatchOptions, ProcessInfo},
    signal::parse_signum_any,
};
use std::process::ExitCode;

/// Lift the first `-SIG` / `-NUM` / `-NAME` argument out of `argv` and
/// rewrite it as `--signal SIG`, so the rest of the args parse normally.
/// The signal name can include or omit the `SIG` prefix; only the first
/// matching argument is consumed (subsequent ones are passed through
/// unchanged).
pub fn preprocess_argv(argv: Vec<String>) -> Vec<String> {
    let mut out = Vec::with_capacity(argv.len() + 1);
    let mut iter = argv.into_iter();
    if let Some(arg0) = iter.next() {
        out.push(arg0);
    }
    let mut found = false;
    for arg in iter {
        if !found
            && arg.starts_with('-')
            && !arg.starts_with("--")
            && arg.len() > 1
            && is_signal_spec(&arg[1..])
        {
            out.push("--signal".to_string());
            out.push(arg[1..].to_string());
            found = true;
        } else {
            out.push(arg);
        }
    }
    out
}

/// Returns true if `s` is a recognised signal name or number — any
/// standard name, real-time name (`RTMIN`, `RTMIN+5`, `RTMAX-3`, …),
/// or numeric form including `"0"` (the null signal `parse_signum_any`
/// rejects because `rustix::Signal` cannot represent zero).
fn is_signal_spec(s: &str) -> bool {
    s == "0" || parse_signum_any(s).is_some()
}

pub const MAN: ManContent = ManContent {
    description: Some(include_str!("../man/description.man")),
    extra_sections: &[
        ("EXAMPLES", include_str!("../man/examples.man")),
        ("NOTES", include_str!("../man/notes.man")),
        ("DIVERGENCES", include_str!("../man/divergences.man")),
        ("SEE ALSO", include_str!("../man/see_also.man")),
    ],
};

const SIGNAL: &str = "Signal";
const MATCHING: &str = "Matching";
const FILTERS: &str = "Filters";
const SELECTION: &str = "Selection";
const OUTPUT: &str = "Output";

/// Signal processes based on name and other attributes.
#[derive(Parser)]
#[command(name = "pkill", version, about, max_term_width = MAX_TERM_WIDTH)]
pub struct Args {
    /// Signal to send (name or number).
    #[arg(long, default_value = "TERM", help_heading = SIGNAL)]
    signal: String,

    /// Use sigqueue(3) to deliver the signal with an integer payload
    /// readable via `siginfo_t::si_value.sival_int` in the receiving
    /// handler.
    #[arg(short = 'q', long, value_name = "VALUE", help_heading = SIGNAL)]
    queue: Option<i32>,

    /// Match against the full command line instead of just the process name.
    #[arg(short, long, help_heading = MATCHING)]
    full: bool,

    /// Match processes case-insensitively.
    #[arg(short, long, help_heading = MATCHING)]
    ignore_case: bool,

    /// Only match processes whose names (or command lines if -f) exactly match the pattern.
    #[arg(short = 'x', long, help_heading = MATCHING)]
    exact: bool,

    /// Match only processes which match the process state.
    #[arg(short = 'r', long, value_delimiter = ',', help_heading = FILTERS)]
    runstates: Option<Vec<char>>,

    /// Match against process environment: `NAME` (any value) or
    /// `NAME=VALUE` (exact pair). Reads `/proc/[pid]/environ`;
    /// processes whose environ is unreadable are skipped.
    #[arg(long, value_name = "NAME[=VALUE]", help_heading = FILTERS)]
    env: Option<String>,

    /// Select processes older than the given number of seconds.
    #[arg(short = 'O', long, help_heading = FILTERS)]
    older: Option<f64>,

    /// Only match processes whose process ID is listed.
    #[arg(short = 'p', long = "pid", value_delimiter = ',', help_heading = FILTERS)]
    pid: Option<Vec<i32>>,

    /// Read process IDs from FILE, one per line. Cannot be combined
    /// with `-p`.
    #[arg(
        short = 'F',
        long = "pidfile",
        value_name = "FILE",
        conflicts_with = "pid",
        help_heading = FILTERS,
    )]
    pidfile: Option<std::path::PathBuf>,

    /// Only match processes whose parent process ID is listed.
    #[arg(short = 'P', long, value_delimiter = ',', help_heading = FILTERS)]
    parent: Option<Vec<i32>>,

    /// Only match processes in the process group IDs listed.
    #[arg(short = 'g', long = "pgroup", value_delimiter = ',', help_heading = FILTERS)]
    pgroup: Option<Vec<i32>>,

    /// Only match processes whose real group ID is listed.
    #[arg(short = 'G', long = "group", value_delimiter = ',', help_heading = FILTERS)]
    group: Option<Vec<u32>>,

    /// Only match processes whose process session ID is listed.
    #[arg(short = 's', long, value_delimiter = ',', help_heading = FILTERS)]
    session: Option<Vec<i32>>,

    /// Only match processes whose controlling terminal is listed.
    #[arg(short = 't', long = "terminal", value_delimiter = ',', help_heading = FILTERS)]
    terminal: Option<Vec<String>>,

    /// Only match processes whose effective user ID is listed.
    #[arg(short = 'u', long = "euid", value_delimiter = ',', help_heading = FILTERS)]
    euid: Option<Vec<String>>,

    /// Only match processes whose real user ID is listed.
    #[arg(short = 'U', long = "uid", value_delimiter = ',', help_heading = FILTERS)]
    uid: Option<Vec<String>>,

    /// Select only the newest (most recently started) of the matching processes.
    #[arg(short, long, help_heading = SELECTION)]
    newest: bool,

    /// Select only the oldest (least recently started) of the matching processes.
    #[arg(short, long, help_heading = SELECTION)]
    oldest: bool,

    /// Suppress signaling; instead print a count of matching processes.
    #[arg(short, long, help_heading = OUTPUT)]
    count: bool,

    /// Display name and PID of each process being signalled.
    #[arg(short, long, help_heading = OUTPUT)]
    echo: bool,

    /// Extended regular expression pattern.
    pattern: Option<String>,
}

/// Parse a signal spec for `pkill`. Accepts everything
/// [`parse_signum_any`] does (standard names, RT names like
/// `RTMIN+5`, numeric form), plus `0` — the null/test signal that
/// `pkill -0` uses to check whether a process exists.
fn parse_signum(s: &str) -> Option<i32> {
    if s == "0" {
        return Some(0);
    }
    parse_signum_any(s)
}

fn send_signal(proc: &ProcessInfo, signum: i32) -> bool {
    let rc = unsafe { libc::kill(proc.pid as libc::pid_t, signum) };
    rc == 0
}

fn send_queued_signal(proc: &ProcessInfo, signum: i32, value: i32) -> bool {
    // sigval is a union of `int sival_int` and `void *sival_ptr`. On
    // 64-bit platforms, sival_ptr is wider; setting it to the i32
    // value's zero-extended form puts the bytes in the same place as
    // sival_int as far as the receiver is concerned.
    let sigval = libc::sigval {
        sival_ptr: value as usize as *mut libc::c_void,
    };
    let rc = unsafe { libc::sigqueue(proc.pid as libc::pid_t, signum, sigval) };
    rc == 0
}

pub fn run(args: Args) -> ExitCode {
    let signum = match parse_signum(&args.signal) {
        Some(n) => n,
        None => {
            eprintln!("pkill: unknown signal: {}", args.signal);
            return ExitCode::from(2);
        }
    };

    let pid_filter = match args.pidfile.as_deref() {
        Some(path) => match procutils_common::procmatch::read_pidfile(path) {
            Ok(pids) => Some(pids),
            Err(e) => {
                eprintln!("pkill: {e}");
                return ExitCode::from(2);
            }
        },
        None => args.pid.clone(),
    };

    let opts = MatchOptions {
        pattern: args.pattern.clone().unwrap_or_default(),
        full: args.full,
        ignore_case: args.ignore_case,
        pid: pid_filter,
        exact: args.exact,
        inverse: false,
        newest: args.newest,
        oldest: args.oldest,
        older: args.older,
        parent: args.parent,
        pgroup: args.pgroup,
        group: args.group,
        session: args.session,
        terminal: args.terminal,
        euid: args.euid,
        uid: args.uid,
        runstates: args.runstates,
        env: args.env,
    };

    if args.pattern.is_none() && !opts.has_filter() {
        eprintln!("pkill: pattern is required");
        return ExitCode::from(2);
    }

    let matches = match procutils_common::procmatch::find_matching_processes(
        &opts, "pkill",
    ) {
        Ok(m) => m,
        Err(code) => return code,
    };

    if matches.is_empty() {
        return ExitCode::from(1);
    }

    if args.count {
        println!("{}", matches.len());
        return ExitCode::SUCCESS;
    }

    let mut any_failed = false;
    for proc in &matches {
        let sent = match args.queue {
            Some(value) => send_queued_signal(proc, signum, value),
            None => send_signal(proc, signum),
        };
        if sent {
            if args.echo {
                println!("{} killed (pid {})", proc.comm, proc.pid);
            }
        } else {
            any_failed = true;
        }
    }

    if any_failed {
        ExitCode::FAILURE
    } else {
        ExitCode::SUCCESS
    }
}