use clap::{Arg, ArgAction, Command};
use rustix::process::{
Pid, Signal, kill_current_process_group, kill_process, kill_process_group,
test_kill_current_process_group, test_kill_process, test_kill_process_group,
};
use std::cmp::Ordering;
use std::io::{self, BufWriter, Write};
use thiserror::Error;
use uucore::display::Quotable;
use uucore::error::{FromIo, UError, UResult, USimpleError, strip_errno};
use uucore::translate;
use uucore::signals::{
signal_by_name_or_value, signal_list_name_by_value, signal_list_value_by_name_or_number,
signal_number_upper_bound,
};
use uucore::{format_usage, show};
const OFFSET: usize = 128;
pub mod options {
pub static PIDS_OR_SIGNALS: &str = "pids_or_signals";
pub static LIST: &str = "list";
pub static TABLE: &str = "table";
pub static SIGNAL: &str = "signal";
}
#[derive(Clone, Copy)]
pub enum Mode {
Kill,
Table,
List,
}
#[derive(Debug, Error)]
enum KillError {
#[error("{}", translate!("kill-error-write", "error" => strip_errno(.0)))]
Write(io::Error),
}
impl UError for KillError {}
#[uucore::main]
pub fn uumain(args: impl uucore::Args) -> UResult<()> {
let mut args = args.collect_ignore();
let obs_signal = handle_obsolete(&mut args)?;
let matches = uucore::clap_localization::handle_clap_result(uu_app(), args)?;
let mode = if matches.get_flag(options::TABLE) {
Mode::Table
} else if matches.get_flag(options::LIST) {
Mode::List
} else {
Mode::Kill
};
let pids_or_signals: Vec<String> = matches
.get_many::<String>(options::PIDS_OR_SIGNALS)
.map(|v| v.map(ToString::to_string).collect())
.unwrap_or_default();
match mode {
Mode::Kill => {
let sig = if let Some(signal) = obs_signal {
signal
} else if let Some(signal) = matches.get_one::<String>(options::SIGNAL) {
parse_signal_value(signal)?
} else {
15_usize };
let pids = parse_pids(&pids_or_signals)?;
if pids.is_empty() {
return Err(USimpleError::new(1, translate!("kill-error-no-process-id")));
}
kill(sig, &pids);
}
Mode::Table => table()?,
Mode::List => list(&pids_or_signals)?,
}
Ok(())
}
pub fn uu_app() -> Command {
Command::new("kill")
.version(uucore::crate_version!())
.help_template(uucore::localized_help_template("kill"))
.about(translate!("kill-about"))
.override_usage(format_usage(&translate!("kill-usage")))
.infer_long_args(true)
.allow_negative_numbers(true)
.arg(
Arg::new(options::LIST)
.short('l')
.long(options::LIST)
.help(translate!("kill-help-list"))
.conflicts_with(options::TABLE)
.action(ArgAction::SetTrue),
)
.arg(
Arg::new(options::TABLE)
.short('t')
.short_alias('L')
.long(options::TABLE)
.help(translate!("kill-help-table"))
.action(ArgAction::SetTrue),
)
.arg(
Arg::new(options::SIGNAL)
.short('s')
.short_alias('n') .long(options::SIGNAL)
.value_name("signal")
.help(translate!("kill-help-signal"))
.conflicts_with_all([options::LIST, options::TABLE]),
)
.arg(
Arg::new(options::PIDS_OR_SIGNALS)
.hide(true)
.action(ArgAction::Append),
)
}
fn handle_obsolete(args: &mut Vec<String>) -> UResult<Option<usize>> {
if args.len() >= 2 {
let slice = args[1].as_str();
if let Some(signal) = slice.strip_prefix('-') {
if signal.chars().next().is_some_and(char::is_lowercase) {
return Ok(None);
}
if let Some(signal_value) = signal_by_name_or_value(signal) {
args.remove(1);
return Ok(Some(signal_value));
}
let first = signal.chars().next();
let looks_like_signal = first.is_some_and(|c| c.is_ascii_digit())
|| (signal.len() > 1 && first.is_some_and(|c| c.is_ascii_uppercase()));
if looks_like_signal {
return Err(USimpleError::new(
1,
translate!("kill-error-invalid-signal", "signal" => signal.quote()),
));
}
}
}
Ok(None)
}
fn table() -> UResult<()> {
let mut out = BufWriter::new(io::stdout().lock());
for signal_value in 0..=signal_number_upper_bound() {
if let Some(signal_name) = signal_list_name_by_value(signal_value) {
writeln!(out, "{signal_value: >#2} {signal_name}").map_err(KillError::Write)?;
}
}
out.flush().map_err(KillError::Write)?;
Ok(())
}
fn normalize_list_signal_value(signal_value: usize) -> Option<usize> {
let lower_8_bits = signal_value & 0xff;
if lower_8_bits <= signal_number_upper_bound() {
return Some(lower_8_bits);
}
signal_value
.checked_sub(OFFSET)
.filter(|value| *value <= signal_number_upper_bound())
}
fn print_signal(signal_name_or_value: &str) -> UResult<()> {
let output = if let Some(signal_value) = signal_name_or_value
.parse::<usize>()
.ok()
.and_then(normalize_list_signal_value)
{
signal_list_name_by_value(signal_value).unwrap_or_else(|| signal_value.to_string())
} else if let Some(signal_value) = signal_list_value_by_name_or_number(signal_name_or_value) {
signal_value.to_string()
} else {
return Err(USimpleError::new(
1,
translate!("kill-error-invalid-signal", "signal" => signal_name_or_value.quote()),
));
};
let mut out = BufWriter::new(io::stdout().lock());
writeln!(out, "{output}").map_err(KillError::Write)?;
out.flush().map_err(KillError::Write)?;
Ok(())
}
fn print_signals() -> UResult<()> {
let mut out = BufWriter::new(io::stdout().lock());
for signal_value in 0..=signal_number_upper_bound() {
if let Some(signal_name) = signal_list_name_by_value(signal_value) {
writeln!(out, "{signal_name}").map_err(KillError::Write)?;
}
}
out.flush().map_err(KillError::Write)?;
Ok(())
}
fn list(signals: &Vec<String>) -> UResult<()> {
if signals.is_empty() {
print_signals()?;
} else {
for signal in signals {
if let Err(e) = print_signal(signal) {
uucore::show!(e);
}
}
}
Ok(())
}
fn raw_kill(pid: i32, sig: usize) -> io::Result<()> {
let sig = i32::try_from(sig).map_err(|_| io::Error::from_raw_os_error(libc::EINVAL))?;
if unsafe { libc::kill(pid as libc::pid_t, sig) } == 0 {
Ok(())
} else {
Err(io::Error::last_os_error())
}
}
fn parse_signal_value(signal_name: &str) -> UResult<usize> {
let optional_signal_value = signal_by_name_or_value(signal_name);
match optional_signal_value {
Some(x) => Ok(x),
None => Err(USimpleError::new(
1,
translate!("kill-error-invalid-signal", "signal" => signal_name.quote()),
)),
}
}
fn parse_pids(pids: &[String]) -> UResult<Vec<i32>> {
pids.iter()
.map(|x| {
x.parse::<i32>().map_err(|e| {
USimpleError::new(
1,
translate!("kill-error-parse-argument", "argument" => x.quote(), "error" => e),
)
})
})
.collect()
}
fn kill(sig: usize, pids: &[i32]) {
let named = (sig != 0)
.then(|| i32::try_from(sig).ok().and_then(Signal::from_named_raw))
.flatten();
for &pid in pids {
let result = match pid.cmp(&0) {
Ordering::Equal => match named {
_ if sig == 0 => test_kill_current_process_group().map_err(io::Error::from),
Some(s) => kill_current_process_group(s).map_err(io::Error::from),
None => raw_kill(0, sig),
},
Ordering::Greater => {
let pid = Pid::from_raw(pid).expect("pid > 0 guaranteed by Ordering::Greater");
match named {
_ if sig == 0 => test_kill_process(pid).map_err(io::Error::from),
Some(s) => kill_process(pid, s).map_err(io::Error::from),
None => raw_kill(pid.as_raw_nonzero().get(), sig),
}
}
Ordering::Less => {
let Some(abs_pid) = pid.checked_neg() else {
show!(USimpleError::new(
1,
translate!("kill-error-sending-signal", "pid" => pid),
));
continue;
};
let pid =
Pid::from_raw(abs_pid).expect("abs_pid > 0 since pid < 0 and pid != i32::MIN");
match named {
_ if sig == 0 => test_kill_process_group(pid).map_err(io::Error::from),
Some(s) => kill_process_group(pid, s).map_err(io::Error::from),
None => raw_kill(-pid.as_raw_nonzero().get(), sig),
}
}
};
if let Err(e) = result {
show!(e.map_err_context(|| translate!("kill-error-sending-signal", "pid" => pid)));
}
}
}
#[cfg(test)]
mod tests {
use super::handle_obsolete;
fn args(items: &[&str]) -> Vec<String> {
items.iter().map(|s| (*s).to_string()).collect()
}
#[test]
fn test_handle_obsolete() {
let mut a = args(&["kill", "-KILL", "123"]);
assert_eq!(handle_obsolete(&mut a).unwrap(), Some(9));
assert_eq!(a, args(&["kill", "123"]));
let mut a = args(&["kill", "-9", "123"]);
assert_eq!(handle_obsolete(&mut a).unwrap(), Some(9));
assert_eq!(a, args(&["kill", "123"]));
assert!(handle_obsolete(&mut args(&["kill", "-65", "123"])).is_err());
assert!(handle_obsolete(&mut args(&["kill", "-NOPESIG", "123"])).is_err());
let mut a = args(&["kill", "-foo", "123"]);
assert_eq!(handle_obsolete(&mut a).unwrap(), None);
assert_eq!(a, args(&["kill", "-foo", "123"]));
assert_eq!(handle_obsolete(&mut args(&["kill"])).unwrap(), None);
}
}