311c32eaf5
- Synced pi-builtins bazel crate_features with cargo's resolved default set: bazel features are literal, so the meta-features never expanded and the procs/rg cluster (nohup, pgrep, pidwait, pkill, proc-match, ps, rg, sleep, timeout, top) was silently compiled out, leaving the process builtins unregistered under bazel and failing pi-shell tests. - Repaired windows compilation of pi-builtins: cfg-gated the uucore::mode import in mkdir, imported std::env in sort's non-unix locale probe, mapped ProcInfo::pid through a closure in kill, brought MetadataExt into scope in wc, and replaced stat's unstable windows_by_handle metadata with a stable GetFileInformationByHandle query (volume serial, link count, file index, no-dereference aware). - Imported HashSet for pi-shell's windows-only PATH merge. - Added a clippy-ported bazel config + CI bucket so pi-builtins keeps its manifest-declared clippy allows under the bazel aspect while rustc warnings stay denied, and zeroed the remaining windows-target rustc warnings (unused params/imports in find, mv, rm, proc_match, ps).
504 lines
14 KiB
Rust
504 lines
14 KiB
Rust
//! The `kill` builtin, moved from `pi-shell`.
|
|
|
|
use std::io::Write;
|
|
|
|
use brush_core::{
|
|
ExecutionExitCode, ExecutionResult, builtins, sys, traps::TrapSignal, ExecutionContext,
|
|
};
|
|
use clap::Parser;
|
|
|
|
use crate::proc_snapshot::HostProcesses;
|
|
|
|
#[cfg(not(unix))]
|
|
use crate::proc_snapshot::{ProcInfo, ProcessStatus};
|
|
|
|
/// Signal a job or process.
|
|
#[derive(Parser)]
|
|
pub(crate) struct KillCommand {
|
|
/// Name of the signal to send.
|
|
#[arg(short = 's', value_name = "SIG_NAME")]
|
|
signal_name: Option<String>,
|
|
/// Number of the signal to send.
|
|
#[arg(short = 'n', value_name = "SIG_NUM")]
|
|
signal_number: Option<usize>,
|
|
/// List known signal names.
|
|
#[arg(short = 'l', short_alias = 'L')]
|
|
list_signals: bool,
|
|
// Interpretation of these depends on whether -l is present.
|
|
#[arg(allow_hyphen_values = true)]
|
|
args: Vec<String>,
|
|
/// Process/job operands given after the `--` end-of-options marker. clap
|
|
/// consumes `--` before `execute`, so these are captured separately and are
|
|
/// always operands — never signal specifications (preserves negative PIDs).
|
|
#[arg(last = true, allow_hyphen_values = true)]
|
|
post_marker_args: Vec<String>,
|
|
}
|
|
|
|
impl builtins::Command for KillCommand {
|
|
type Error = brush_core::Error;
|
|
|
|
#[allow(unknown_lints, reason = "unused_async_trait_impl is unknown to the pinned CI nightly")]
|
|
#[allow(
|
|
clippy::unused_async_trait_impl,
|
|
reason = "the builtin Command trait declares execute as async"
|
|
)]
|
|
async fn execute<SE: brush_core::ShellExtensions>(
|
|
&self,
|
|
context: ExecutionContext<'_, SE>,
|
|
) -> std::result::Result<ExecutionResult, Self::Error> {
|
|
let default_signal = if let Some(signal_name) = &self.signal_name {
|
|
if let Ok(signal) = KillSignal::parse(signal_name) {
|
|
signal
|
|
} else {
|
|
writeln!(
|
|
context.stderr(),
|
|
"{}: invalid signal name: {}",
|
|
context.command_name,
|
|
signal_name
|
|
)?;
|
|
return Ok(ExecutionExitCode::InvalidUsage.into());
|
|
}
|
|
} else {
|
|
KillSignal::parse("TERM")?
|
|
};
|
|
let mut signal = match self.signal_number {
|
|
Some(signal_number) => {
|
|
let Ok(signal_number) = i32::try_from(signal_number) else {
|
|
writeln!(
|
|
context.stderr(),
|
|
"{}: invalid signal number: {}",
|
|
context.command_name,
|
|
signal_number
|
|
)?;
|
|
return Ok(ExecutionExitCode::InvalidUsage.into());
|
|
};
|
|
if let Ok(signal) = KillSignal::parse(&signal_number.to_string()) {
|
|
signal
|
|
} else {
|
|
writeln!(
|
|
context.stderr(),
|
|
"{}: invalid signal number: {}",
|
|
context.command_name,
|
|
signal_number
|
|
)?;
|
|
return Ok(ExecutionExitCode::InvalidUsage.into());
|
|
}
|
|
},
|
|
None => default_signal,
|
|
};
|
|
|
|
// Interpret the pre-`--` args as an optional leading `-sigspec`, followed
|
|
// by PID/jobspec operands. Once a signal or operand has been seen, later
|
|
// hyphen-led arguments remain operands so negative process-group IDs survive.
|
|
let mut operands: Vec<&String> = Vec::new();
|
|
let mut options_done = self.signal_name.is_some() || self.signal_number.is_some();
|
|
let mut consumed_marker = false;
|
|
for arg in &self.args {
|
|
if !consumed_marker && arg == "--" {
|
|
consumed_marker = true;
|
|
options_done = true;
|
|
continue;
|
|
}
|
|
if !options_done && let Some(spec) = arg.strip_prefix('-').filter(|spec| !spec.is_empty())
|
|
{
|
|
signal = if let Ok(signal) = KillSignal::parse(spec) {
|
|
signal
|
|
} else {
|
|
writeln!(context.stderr(), "{}: invalid signal name", context.command_name)?;
|
|
return Ok(ExecutionExitCode::InvalidUsage.into());
|
|
};
|
|
options_done = true;
|
|
continue;
|
|
}
|
|
options_done = true;
|
|
operands.push(arg);
|
|
}
|
|
operands.extend(&self.post_marker_args);
|
|
|
|
if self.list_signals {
|
|
return print_kill_signals(&context, operands);
|
|
}
|
|
if operands.is_empty() {
|
|
writeln!(context.stderr(), "{}: invalid usage", context.command_name)?;
|
|
return Ok(ExecutionExitCode::InvalidUsage.into());
|
|
}
|
|
|
|
// One process-table walk for the whole invocation, and only when a signal
|
|
// will actually be delivered: `-l` and the signal-0 probe touch nothing.
|
|
// Every guard below reads this same resolved chain.
|
|
let host = signal.sends_signal().then(HostProcesses::resolve);
|
|
let blocks = |target: i32| host.as_ref().is_some_and(|host| blocks_target(host, target));
|
|
|
|
// `kill -0` asks only whether a target exists. Unix answers per target with
|
|
// one syscall; elsewhere there is no such call, so the table is walked once
|
|
// here and every target is answered from that snapshot rather than from a
|
|
// fresh walk apiece.
|
|
#[cfg(not(unix))]
|
|
let running: Vec<i32> = if signal.sends_signal() {
|
|
Vec::new()
|
|
} else {
|
|
ProcInfo::all()
|
|
.into_iter()
|
|
.filter(|process| process.status() == ProcessStatus::Running)
|
|
.map(|process| process.pid())
|
|
.collect()
|
|
};
|
|
#[cfg(unix)]
|
|
let exists = |target: i32| {
|
|
// SAFETY: signal 0 only checks target existence and permission.
|
|
unsafe { libc::kill(target, 0) == 0 }
|
|
};
|
|
#[cfg(not(unix))]
|
|
let exists = |target: i32| target > 0 && running.contains(&target);
|
|
|
|
let mut had_failure = false;
|
|
for operand in operands {
|
|
if context.is_cancelled() {
|
|
return Ok(ExecutionExitCode::Interrupted.into());
|
|
}
|
|
if operand.starts_with('%') {
|
|
let Some(job) = context.shell.jobs_mut().resolve_job_spec(operand) else {
|
|
writeln!(context.stderr(), "{}: {}: no such job", context.command_name, operand)?;
|
|
had_failure = true;
|
|
continue;
|
|
};
|
|
#[cfg(unix)]
|
|
{
|
|
let mut targets: Vec<i32> = job
|
|
.process_ids()
|
|
.filter_map(|pid| {
|
|
// SAFETY: getpgid reads process-group metadata for a managed child.
|
|
let pgid = unsafe { libc::getpgid(pid) };
|
|
(pgid > 0).then_some(-pgid)
|
|
})
|
|
.collect();
|
|
if targets.is_empty()
|
|
&& let Some(pgid) = job.process_group_id()
|
|
{
|
|
targets.push(-pgid);
|
|
}
|
|
targets.sort_unstable();
|
|
targets.dedup();
|
|
if targets.iter().copied().any(&blocks) {
|
|
writeln!(
|
|
context.stderr(),
|
|
"{}: {}: refusing to signal the shell process",
|
|
context.command_name,
|
|
operand
|
|
)?;
|
|
had_failure = true;
|
|
continue;
|
|
}
|
|
let succeeded = match signal {
|
|
KillSignal::Probe => targets.iter().copied().any(&exists),
|
|
KillSignal::Signal(signal) => {
|
|
let mut succeeded = false;
|
|
for target in targets {
|
|
if sys::signal::kill_process(target, signal).is_ok() {
|
|
succeeded = true;
|
|
}
|
|
}
|
|
succeeded
|
|
},
|
|
};
|
|
if !succeeded {
|
|
writeln!(
|
|
context.stderr(),
|
|
"{}: {}: failed to send signal",
|
|
context.command_name,
|
|
operand
|
|
)?;
|
|
had_failure = true;
|
|
}
|
|
}
|
|
#[cfg(windows)]
|
|
{
|
|
let job_group = job.process_group_id();
|
|
if signal.sends_signal()
|
|
&& (job_group.is_some_and(&blocks) || job.process_ids().any(&blocks))
|
|
{
|
|
writeln!(
|
|
context.stderr(),
|
|
"{}: {}: refusing to signal the shell process",
|
|
context.command_name,
|
|
operand
|
|
)?;
|
|
had_failure = true;
|
|
continue;
|
|
}
|
|
let expected_handles = job.external_process_count();
|
|
let handles = job.duplicate_kill_handles();
|
|
let mut succeeded = expected_handles != 0 && handles.len() == expected_handles;
|
|
for handle in &handles {
|
|
let handled = match signal {
|
|
KillSignal::Probe => brush_core::processes::process_handle_is_running(handle),
|
|
KillSignal::Signal(_) => {
|
|
brush_core::processes::terminate_process_handle(handle)
|
|
},
|
|
};
|
|
if !handled {
|
|
succeeded = false;
|
|
}
|
|
}
|
|
if !succeeded {
|
|
writeln!(
|
|
context.stderr(),
|
|
"{}: {}: failed to send signal",
|
|
context.command_name,
|
|
operand
|
|
)?;
|
|
had_failure = true;
|
|
}
|
|
}
|
|
#[cfg(all(not(unix), not(windows)))]
|
|
{
|
|
let job_group = job.process_group_id();
|
|
let representative = job.representative_pid();
|
|
if signal.sends_signal()
|
|
&& (job_group.is_some_and(&blocks) || representative.is_some_and(&blocks))
|
|
{
|
|
writeln!(
|
|
context.stderr(),
|
|
"{}: {}: refusing to signal the shell process",
|
|
context.command_name,
|
|
operand
|
|
)?;
|
|
had_failure = true;
|
|
continue;
|
|
}
|
|
match signal {
|
|
KillSignal::Probe => {
|
|
if !representative.is_some_and(&exists) {
|
|
writeln!(
|
|
context.stderr(),
|
|
"{}: {}: failed to send signal",
|
|
context.command_name,
|
|
operand
|
|
)?;
|
|
had_failure = true;
|
|
}
|
|
},
|
|
KillSignal::Signal(signal) => {
|
|
if let Err(err) = job.kill(signal) {
|
|
writeln!(
|
|
context.stderr(),
|
|
"{}: {}: {}",
|
|
context.command_name,
|
|
operand,
|
|
err
|
|
)?;
|
|
had_failure = true;
|
|
}
|
|
},
|
|
}
|
|
}
|
|
continue;
|
|
}
|
|
|
|
let pid = match brush_core::int_utils::parse(operand, 10) {
|
|
Ok(pid) => pid,
|
|
Err(err) => {
|
|
writeln!(context.stderr(), "{}: {}: {}", context.command_name, operand, err)?;
|
|
had_failure = true;
|
|
continue;
|
|
},
|
|
};
|
|
if blocks(pid) {
|
|
writeln!(
|
|
context.stderr(),
|
|
"{}: {}: refusing to signal the shell process",
|
|
context.command_name,
|
|
operand
|
|
)?;
|
|
had_failure = true;
|
|
continue;
|
|
}
|
|
match signal {
|
|
KillSignal::Probe => {
|
|
if !exists(pid) {
|
|
writeln!(
|
|
context.stderr(),
|
|
"{}: {}: failed to send signal",
|
|
context.command_name,
|
|
operand
|
|
)?;
|
|
had_failure = true;
|
|
}
|
|
},
|
|
KillSignal::Signal(signal) => {
|
|
if let Err(err) = sys::signal::kill_process(pid, signal) {
|
|
writeln!(context.stderr(), "{}: {}: {}", context.command_name, operand, err)?;
|
|
had_failure = true;
|
|
}
|
|
},
|
|
}
|
|
}
|
|
|
|
if had_failure {
|
|
Ok(ExecutionResult::general_error())
|
|
} else {
|
|
Ok(ExecutionResult::success())
|
|
}
|
|
}
|
|
}
|
|
|
|
/// Whether signalling `target` would reach the shell or one of its ancestors.
|
|
///
|
|
/// `target` follows `kill(2)`: a positive value is a pid, `0` is the caller's own
|
|
/// process group, `-1` is every process the caller may signal, and any other
|
|
/// negative value is the process group `-target`. The caller's own group needs no
|
|
/// special case — it is in `host.pgids` by construction.
|
|
///
|
|
/// Takes the already-resolved chain rather than resolving one, so a loop over
|
|
/// operands walks the process table once, not once per operand.
|
|
fn blocks_target(host: &HostProcesses, target: i32) -> bool {
|
|
if target == -1 || target == 0 {
|
|
return true;
|
|
}
|
|
match target.checked_neg() {
|
|
Some(pgid) if pgid > 0 => host.pgids.contains(&pgid),
|
|
_ => host.pids.contains(&target),
|
|
}
|
|
}
|
|
|
|
fn print_kill_signals<'a>(
|
|
context: &ExecutionContext<'_, impl brush_core::ShellExtensions>,
|
|
signals: impl IntoIterator<Item = &'a String>,
|
|
) -> std::result::Result<ExecutionResult, brush_core::Error> {
|
|
let mut result = ExecutionResult::success();
|
|
let mut signals = signals.into_iter().peekable();
|
|
if signals.peek().is_none() {
|
|
return brush_core::traps::format_signals(
|
|
context.stdout(),
|
|
TrapSignal::iterator().filter(|signal| !matches!(signal, TrapSignal::Exit)),
|
|
)
|
|
.map(|()| ExecutionResult::success());
|
|
}
|
|
for value in signals {
|
|
enum PrintedSignal {
|
|
Name(&'static str),
|
|
Number(i32),
|
|
}
|
|
let signal = if let Ok(number) = value.parse::<i32>() {
|
|
TrapSignal::try_from(number).map(|signal| {
|
|
PrintedSignal::Name(
|
|
signal
|
|
.as_str()
|
|
.strip_prefix("SIG")
|
|
.unwrap_or(signal.as_str()),
|
|
)
|
|
})
|
|
} else {
|
|
TrapSignal::try_from(value.as_str()).map(|signal| {
|
|
i32::try_from(signal)
|
|
.map_or(PrintedSignal::Name(signal.as_str()), PrintedSignal::Number)
|
|
})
|
|
};
|
|
match signal {
|
|
Ok(PrintedSignal::Name(name)) => writeln!(context.stdout(), "{name}")?,
|
|
Ok(PrintedSignal::Number(number)) => writeln!(context.stdout(), "{number}")?,
|
|
Err(err) => {
|
|
writeln!(context.stderr(), "{err}")?;
|
|
result = ExecutionResult::general_error();
|
|
},
|
|
}
|
|
}
|
|
Ok(result)
|
|
}
|
|
|
|
#[cfg(test)]
|
|
impl KillCommand {
|
|
fn listed_signals(&self) -> impl Iterator<Item = &String> {
|
|
let mut consumed_marker = false;
|
|
self.args
|
|
.iter()
|
|
.filter(move |arg| {
|
|
if !consumed_marker && *arg == "--" {
|
|
consumed_marker = true;
|
|
false
|
|
} else {
|
|
true
|
|
}
|
|
})
|
|
.chain(&self.post_marker_args)
|
|
}
|
|
}
|
|
|
|
#[cfg(test)]
|
|
mod tests {
|
|
use super::*;
|
|
|
|
fn listed(args: &[&str]) -> Vec<String> {
|
|
let cmd = KillCommand::try_parse_from(args).unwrap();
|
|
cmd.listed_signals().cloned().collect()
|
|
}
|
|
|
|
#[test]
|
|
fn lists_post_marker_operands() {
|
|
assert_eq!(listed(&["kill", "-l", "--", "9"]), ["9"]);
|
|
}
|
|
|
|
#[test]
|
|
fn lists_pre_and_post_marker_operands() {
|
|
assert_eq!(listed(&["kill", "-l", "TERM", "--", "9"]), ["TERM", "9"]);
|
|
}
|
|
|
|
#[test]
|
|
fn lists_pre_marker_operands_without_marker() {
|
|
assert_eq!(listed(&["kill", "-l", "TERM", "HUP"]), ["TERM", "HUP"]);
|
|
}
|
|
}
|
|
|
|
/// A `kill` signal argument: a real signal, or the "does this process
|
|
/// exist?" probe that signal 0 requests.
|
|
#[derive(Clone, Copy)]
|
|
enum KillSignal {
|
|
Probe,
|
|
Signal(TrapSignal),
|
|
}
|
|
|
|
impl KillSignal {
|
|
fn parse(value: &str) -> std::result::Result<Self, brush_core::Error> {
|
|
if let Ok(number) = value.parse::<i32>() {
|
|
if number == 0 {
|
|
Ok(Self::Probe)
|
|
} else {
|
|
TrapSignal::try_from(number).map(Self::Signal)
|
|
}
|
|
} else {
|
|
TrapSignal::try_from(value).map(Self::Signal)
|
|
}
|
|
}
|
|
|
|
const fn sends_signal(self) -> bool {
|
|
matches!(self, Self::Signal(_))
|
|
}
|
|
}
|
|
|
|
|
|
/// Resolves a signal name or number to its number.
|
|
///
|
|
/// Shared with `pkill`, which accepts the same `-SIGNAL` spellings.
|
|
#[allow(
|
|
dead_code,
|
|
reason = "shared with optional process-match builtins that may be feature-disabled"
|
|
)]
|
|
pub(crate) fn signal_number(value: &str) -> Option<i32> {
|
|
let value = value
|
|
.strip_prefix("SIG")
|
|
.or_else(|| value.strip_prefix("sig"))
|
|
.unwrap_or(value);
|
|
if let Ok(number) = value.parse::<i32>() {
|
|
#[cfg(target_os = "linux")]
|
|
return (0..=libc::SIGRTMAX()).contains(&number).then_some(number);
|
|
#[cfg(target_os = "macos")]
|
|
return (0..=31).contains(&number).then_some(number);
|
|
#[cfg(not(unix))]
|
|
return (0..=64).contains(&number).then_some(number);
|
|
}
|
|
match KillSignal::parse(value).ok()? {
|
|
KillSignal::Probe => Some(0),
|
|
KillSignal::Signal(signal) => i32::try_from(signal).ok(),
|
|
}
|
|
}
|