Files
oh-my-pi/crates/pi-builtins/src/kill.rs
T
can1357 311c32eaf5 fix(natives): repaired win32 build and bazel feature drift
- 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).
2026-08-09 03:17:34 +02:00

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(),
}
}