use crate::common::{report_failure, to_single_message};
use std::borrow::Cow;
use std::num::ParseIntError;
use thiserror::Error;
use yash_env::job::id::parse_tail;
use yash_env::job::Pid;
use yash_env::job::{id::FindError, JobList};
use yash_env::semantics::Field;
use yash_env::system::Errno;
use yash_env::trap::Signal;
use yash_env::Env;
use yash_env::System as _;
use yash_syntax::source::pretty::{Annotation, AnnotationType, MessageBase};
#[derive(Clone, Debug, Error, PartialEq, Eq)]
pub enum Error {
#[error(transparent)]
ProcessId(#[from] ParseIntError),
#[error(transparent)]
JobId(#[from] FindError),
#[error("target job is not controlled by the current shell environment")]
Unowned,
#[error("target job is not job-controlled")]
Unmonitored,
#[error("target job has finished")]
Finished,
#[error(transparent)]
System(#[from] Errno),
}
pub fn resolve_target(jobs: &JobList, target: &str) -> Result<Pid, Error> {
if let Some(tail) = target.strip_prefix('%') {
let job_id = parse_tail(tail);
let index = job_id.find(jobs)?;
let job = &jobs[index];
if !job.is_owned {
Err(Error::Unowned)
} else if !job.job_controlled {
Err(Error::Unmonitored)
} else if !job.state.is_alive() {
Err(Error::Finished)
} else {
Ok(-job.pid)
}
} else {
Ok(Pid(target.parse()?))
}
}
pub async fn send(env: &mut Env, signal: Option<Signal>, target: &Field) -> Result<(), Error> {
let pid = resolve_target(&env.jobs, &target.value)?;
env.system.kill(pid, signal).await?;
Ok(())
}
#[derive(Clone, Debug, Error, PartialEq, Eq)]
#[error("{target}: {error}")]
struct TargetError<'a> {
target: &'a Field,
error: Error,
}
impl MessageBase for TargetError<'_> {
fn message_title(&self) -> Cow<str> {
"cannot send signal".into()
}
fn main_annotation(&self) -> Annotation<'_> {
Annotation::new(
AnnotationType::Error,
self.to_string().into(),
&self.target.origin,
)
}
}
pub async fn execute(env: &mut Env, signal: Option<Signal>, targets: &[Field]) -> crate::Result {
let mut errors = Vec::new();
for target in targets {
if let Err(error) = send(env, signal, target).await {
errors.push(TargetError { target, error });
}
}
if let Some(message) = to_single_message(&{ errors }) {
report_failure(env, message).await
} else {
crate::Result::default()
}
}
#[cfg(test)]
mod tests {
use super::*;
use assert_matches::assert_matches;
use yash_env::job::Job;
use yash_env::job::ProcessState;
use yash_semantics::ExitStatus;
#[test]
fn resolve_target_process_ids() {
let jobs = JobList::new();
let result = resolve_target(&jobs, "123");
assert_eq!(result, Ok(Pid(123)));
let result = resolve_target(&jobs, "-456");
assert_eq!(result, Ok(Pid(-456)));
}
#[test]
fn resolve_target_job_id() {
let mut jobs = JobList::new();
let mut job = Job::new(Pid(123));
job.job_controlled = true;
job.is_owned = true;
job.state = ProcessState::Running;
job.name = "my job".into();
jobs.add(job);
let result = resolve_target(&jobs, "%my");
assert_eq!(result, Ok(Pid(-123)));
}
#[test]
fn resolve_target_job_find_error() {
let jobs = JobList::new();
let result = resolve_target(&jobs, "%my");
assert_eq!(result, Err(Error::JobId(FindError::NotFound)));
}
#[test]
fn resolve_target_unowned() {
let mut jobs = JobList::new();
let mut job = Job::new(Pid(123));
job.job_controlled = true;
job.is_owned = false;
job.state = ProcessState::Running;
job.name = "my job".into();
jobs.add(job);
let result = resolve_target(&jobs, "%my");
assert_eq!(result, Err(Error::Unowned));
}
#[test]
fn resolve_target_unmonitored() {
let mut jobs = JobList::new();
let mut job = Job::new(Pid(123));
job.job_controlled = false;
job.is_owned = true;
job.state = ProcessState::Running;
job.name = "my job".into();
jobs.add(job);
let result = resolve_target(&jobs, "%my");
assert_eq!(result, Err(Error::Unmonitored));
}
#[test]
fn resolve_target_finished() {
let mut jobs = JobList::new();
let mut job = Job::new(Pid(123));
job.job_controlled = true;
job.is_owned = true;
job.state = ProcessState::Exited(ExitStatus(0));
job.name = "my job".into();
jobs.add(job);
let result = resolve_target(&jobs, "%my");
assert_eq!(result, Err(Error::Finished));
}
#[test]
fn resolve_target_invalid_string() {
let jobs = JobList::new();
let result = resolve_target(&jobs, "abc");
assert_matches!(result, Err(Error::ProcessId(_)));
}
}