use crate::daemon::{log, Coordinator};
use crate::job::{self, JobState};
use crate::paths;
use crate::proto::{ErrorKind, Response};
use std::sync::Arc;
use std::time::Duration;
pub fn parse_signal(s: &str) -> Result<i32, String> {
let t = s.trim().to_ascii_uppercase();
let t = t.strip_prefix("SIG").unwrap_or(&t);
if let Ok(n) = t.parse::<i32>() {
if (1..=64).contains(&n) {
return Ok(n);
}
return Err(format!("the signal number {n} is not in the range 1 to 64"));
}
match t {
"TERM" => Ok(libc::SIGTERM),
"KILL" => Ok(libc::SIGKILL),
"INT" => Ok(libc::SIGINT),
"HUP" => Ok(libc::SIGHUP),
"QUIT" => Ok(libc::SIGQUIT),
"USR1" => Ok(libc::SIGUSR1),
"USR2" => Ok(libc::SIGUSR2),
other => Err(format!(
"unknown signal `{other}`. Use TERM, KILL, INT, HUP, QUIT, USR1, USR2, or a number."
)),
}
}
pub fn kill(coord: &Arc<Coordinator>, id: uuid::Uuid, signal: i32, grace_secs: u64) -> Response {
let pid = {
let mut state = coord.state.lock().unwrap();
state.refresh_active();
let Some(job) = state.jobs.get(&id) else {
return Response::error(
ErrorKind::NoSuchJob,
format!("there is no job with the id {id}"),
);
};
match job.status.state {
JobState::Queued => {
return Response::error(
ErrorKind::WrongState,
format!("the job {id} waits in the queue. Use `qex cancel {id}`."),
)
}
s if s.is_terminal() => {
return Response::error(
ErrorKind::WrongState,
format!("the job {id} stopped. Its state is `{s}`."),
)
}
_ => {}
}
match job.status.pid {
Some(p) => p,
None => {
return Response::error(
ErrorKind::WrongState,
format!("the job {id} starts now. Try the command again."),
)
}
}
};
let sent = unsafe { libc::killpg(pid, signal) };
if sent != 0 {
let e = std::io::Error::last_os_error();
if e.raw_os_error() == Some(libc::ESRCH) {
log(&format!("job {id} has no process; it stopped already"));
return Response::error(
ErrorKind::WrongState,
format!(
"the job {id} has no process. It stopped in the moment before this \
command. Read `qex status {id}` for the result."
),
);
}
return Response::error(
ErrorKind::Internal,
format!("qex could not signal the job {id}: {e}"),
);
}
log(&format!("job {id} received the signal {signal}"));
if signal != libc::SIGKILL && grace_secs > 0 {
let coord = Arc::clone(coord);
std::thread::spawn(move || {
std::thread::sleep(Duration::from_secs(grace_secs));
let still_active = {
let state = coord.state.lock().unwrap();
state
.jobs
.get(&id)
.map(|j| j.status.state.is_active())
.unwrap_or(false)
};
if still_active {
unsafe {
libc::killpg(pid, libc::SIGKILL);
}
log(&format!(
"job {id} did not stop in {grace_secs} seconds; qex sent KILL"
));
}
});
}
Response::Ok
}
pub fn clean(coord: &Arc<Coordinator>, id: uuid::Uuid) -> Response {
let (cause_name, cause_state) = {
let state = coord.state.lock().unwrap();
match state.jobs.get(&id) {
Some(job) => (job.status.name.clone(), job.status.state.to_string()),
None => (String::from("unknown"), String::from("unknown")),
}
};
{
let state = coord.state.lock().unwrap();
match state.jobs.get(&id) {
None => {
return Response::error(
ErrorKind::NoSuchJob,
format!("there is no job with the id {id}"),
)
}
Some(job) if !job.status.state.is_terminal() => {
return Response::error(
ErrorKind::WrongState,
format!(
"the job {id} is in the state `{}`. Stop it first with `qex kill {id}`.",
job.status.state
),
)
}
Some(_) => {}
}
let waiting: Vec<String> = state
.jobs
.values()
.filter(|j| !j.status.state.is_terminal())
.filter(|j| j.spec.needs.contains(&id) || j.spec.after.contains(&id))
.map(|j| format!("{} ({})", &j.status.id.to_string()[..8], j.status.name))
.collect();
if !waiting.is_empty() {
return Response::error(
ErrorKind::WrongState,
format!(
"the job {id} is needed by {}. Wait for {}, or cancel {}.",
waiting.join(", "),
if waiting.len() == 1 {
"that job"
} else {
"those jobs"
},
if waiting.len() == 1 { "it" } else { "them" }
),
);
}
}
{
let state = coord.state.lock().unwrap();
if let Some(job) = state.jobs.get(&id) {
let status = job.status.clone();
drop(state);
crate::history::record_removed(&status);
}
}
let dir = match paths::job_dir(&id) {
Ok(d) => d,
Err(e) => return Response::error(ErrorKind::Internal, e.to_string()),
};
if let Err(e) = std::fs::remove_dir_all(&dir) {
if e.kind() != std::io::ErrorKind::NotFound {
return Response::error(
ErrorKind::Internal,
format!("qex could not delete {}: {e}", dir.display()),
);
}
}
let mut state = coord.state.lock().unwrap();
state.jobs.remove(&id);
state.queue.retain(|q| *q != id);
let dependents: Vec<uuid::Uuid> = state
.jobs
.values()
.filter(|j| j.status.caused_by == Some(id))
.map(|j| j.status.id)
.collect();
for dep in dependents {
if let Some(job) = state.jobs.get_mut(&dep) {
job.status.error = Some(format!(
"the job `{}` ({}) did not succeed, so this job did not run. \
Its record is deleted, so there is no log to read.",
cause_name, cause_state
));
job.status.caused_by = None;
let status = job.status.clone();
if let Ok(dir) = paths::job_dir(&dep) {
job::write_status(&dir, &status).ok();
}
}
}
drop(state);
coord.notify();
Response::Ok
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn signal_names_parse_in_each_usual_form() {
assert_eq!(parse_signal("TERM").unwrap(), libc::SIGTERM);
assert_eq!(parse_signal("SIGTERM").unwrap(), libc::SIGTERM);
assert_eq!(parse_signal("term").unwrap(), libc::SIGTERM);
assert_eq!(parse_signal("KILL").unwrap(), libc::SIGKILL);
assert_eq!(parse_signal("9").unwrap(), 9);
assert_eq!(parse_signal("INT").unwrap(), libc::SIGINT);
}
#[test]
fn an_unknown_signal_gives_a_message_with_the_permitted_names() {
let err = parse_signal("BANANA").unwrap_err();
assert!(err.contains("TERM"), "the error must list the names: {err}");
assert!(
parse_signal("0").is_err(),
"the signal 0 tests a process only"
);
assert!(parse_signal("99").is_err());
}
}