use async_trait::async_trait;
use clap::{CommandFactory, Parser};
use crate::ast::Value;
use crate::interpreter::ExecResult;
use crate::scheduler::{JobId, JobManager};
use crate::tools::{schema_from_clap, ExecContext, GlobalFlags, Tool, ToolArgs, ToolCtx, ToolSchema};
pub struct Kill;
#[derive(Parser, Debug)]
#[command(name = "kill", about = "Send a signal to a process or job")]
struct KillArgs {
#[arg(short = 's', long, default_value_t = String::from("TERM"))]
signal: String,
#[command(flatten)]
global: GlobalFlags,
targets: Vec<String>,
}
#[async_trait]
impl Tool for Kill {
fn name(&self) -> &str {
"kill"
}
fn schema(&self) -> ToolSchema {
schema_from_clap(
&KillArgs::command(),
"kill",
"Send a signal to a process or job",
[
("Terminate a job", "kill %1"),
("Kill a process by PID", "kill --signal KILL 1234"),
],
)
}
async fn execute(&self, args: ToolArgs, ctx: &mut dyn ToolCtx) -> ExecResult {
let Some(ctx) = ctx.as_any_mut().downcast_mut::<ExecContext>() else {
return ExecResult::failure(1, "internal error: kernel builtin requires ExecContext");
};
let parsed = match KillArgs::try_parse_from(
std::iter::once("kill".to_string()).chain(args.to_argv()),
) {
Ok(p) => p,
Err(e) => return ExecResult::failure(2, format!("kill: {e}")),
};
parsed.global.apply(ctx);
let signal_name = args
.named
.get("signal")
.map(|v| match v {
Value::String(s) => s.clone(),
Value::Int(i) => i.to_string(),
other => crate::interpreter::value_to_string(other),
})
.unwrap_or(parsed.signal);
let target_str = match args.get_positional(0) {
Some(Value::String(s)) => s.clone(),
Some(Value::Int(i)) => i.to_string(),
Some(_) => return ExecResult::failure(1, "kill: invalid target"),
None => return ExecResult::failure(1, "kill: usage: kill [--signal SIG] target"),
};
if let Some(job_num) = target_str.strip_prefix('%') {
let job_id = match job_num.parse::<u64>() {
Ok(i) => JobId(i),
Err(_) => {
return ExecResult::failure(1, format!("kill: invalid job reference: {target_str}"))
}
};
let manager = match &ctx.job_manager {
Some(m) => m.clone(),
None => return ExecResult::failure(1, "kill: no job manager"),
};
return kill_job(&manager, job_id, &signal_name).await;
}
kill_pid(&target_str, &signal_name)
}
}
#[cfg(not(all(unix, feature = "subprocess")))]
fn signal_is_terminating(name: &str) -> Option<bool> {
if let Ok(num) = name.parse::<i32>() {
return match num {
1 | 2 | 3 | 6 | 9 | 15 => Some(true),
n if n > 0 => Some(false),
_ => None,
};
}
let name = name.strip_prefix("SIG").unwrap_or(name);
match name {
"TERM" | "KILL" | "INT" | "HUP" | "QUIT" | "ABRT" => Some(true),
"STOP" | "CONT" | "USR1" | "USR2" | "TSTP" | "WINCH" => Some(false),
_ => None,
}
}
#[cfg(not(all(unix, feature = "subprocess")))]
async fn kill_job(manager: &JobManager, job_id: JobId, signal_name: &str) -> ExecResult {
match signal_is_terminating(signal_name) {
Some(true) => {
if manager.cancel(job_id).await {
manager.remove(job_id).await;
ExecResult::success("")
} else {
ExecResult::failure(1, format!("kill: job {job_id} not found"))
}
}
Some(false) => ExecResult::failure(
1,
format!(
"kill: job {job_id} is an in-process task; only termination signals \
(TERM/KILL/INT/HUP/QUIT) can be delivered, not {signal_name} \
(arbitrary-signal delivery needs the subprocess capability)"
),
),
None => ExecResult::failure(1, format!("kill: unknown signal: {signal_name}")),
}
}
#[cfg(not(all(unix, feature = "subprocess")))]
fn kill_pid(target: &str, _signal_name: &str) -> ExecResult {
ExecResult::failure(
1,
format!("kill: {target}: signalling a PID requires the subprocess capability"),
)
}
#[cfg(all(unix, feature = "subprocess"))]
async fn kill_job(manager: &JobManager, job_id: JobId, signal_name: &str) -> ExecResult {
use nix::sys::signal::Signal;
let signal = match parse_signal(signal_name) {
Some(s) => s,
None => return ExecResult::failure(1, format!("kill: unknown signal: {signal_name}")),
};
let terminating = matches!(
signal,
Signal::SIGTERM | Signal::SIGKILL | Signal::SIGINT | Signal::SIGHUP | Signal::SIGQUIT
);
let pgids = manager.job_pgids(job_id).await;
if !pgids.is_empty() {
let mut last_err = None;
for pg in &pgids {
let pgid = nix::unistd::Pid::from_raw(*pg as i32);
if let Err(e) = nix::sys::signal::killpg(pgid, signal) {
last_err = Some(e);
}
}
if terminating {
manager.cancel(job_id).await;
manager.remove(job_id).await;
}
return match last_err {
Some(e) => ExecResult::failure(1, format!("kill: {e}")),
None => ExecResult::success(""),
};
}
if terminating {
if manager.cancel(job_id).await {
manager.remove(job_id).await;
ExecResult::success("")
} else {
ExecResult::failure(1, format!("kill: job {job_id} not found"))
}
} else {
ExecResult::failure(
1,
format!(
"kill: job {job_id} is an in-process task with no process group; \
only termination signals (TERM/KILL/INT/HUP/QUIT) can be delivered, not {signal_name}"
),
)
}
}
#[cfg(all(unix, feature = "subprocess"))]
fn kill_pid(target: &str, signal_name: &str) -> ExecResult {
let signal = match parse_signal(signal_name) {
Some(s) => s,
None => return ExecResult::failure(1, format!("kill: unknown signal: {signal_name}")),
};
let pid_num: i32 = match target.parse() {
Ok(p) => p,
Err(_) => return ExecResult::failure(1, format!("kill: invalid pid: {target}")),
};
let pid = nix::unistd::Pid::from_raw(pid_num);
if let Err(e) = nix::sys::signal::kill(pid, signal) {
return ExecResult::failure(1, format!("kill: ({pid_num}): {e}"));
}
ExecResult::success("")
}
#[cfg(all(unix, feature = "subprocess"))]
fn parse_signal(name: &str) -> Option<nix::sys::signal::Signal> {
use nix::sys::signal::Signal;
if let Ok(num) = name.parse::<i32>() {
return Signal::try_from(num).ok();
}
let name = name.strip_prefix("SIG").unwrap_or(name);
match name {
"TERM" => Some(Signal::SIGTERM),
"KILL" => Some(Signal::SIGKILL),
"STOP" => Some(Signal::SIGSTOP),
"CONT" => Some(Signal::SIGCONT),
"INT" => Some(Signal::SIGINT),
"HUP" => Some(Signal::SIGHUP),
"USR1" => Some(Signal::SIGUSR1),
"USR2" => Some(Signal::SIGUSR2),
"QUIT" => Some(Signal::SIGQUIT),
_ => None,
}
}