use super::Signal;
use crate::common::report::{merge_reports, report_failure};
use std::num::ParseIntError;
use thiserror::Error;
use yash_env::Env;
use yash_env::job::Pid;
use yash_env::job::id::parse_tail;
use yash_env::job::{JobList, id::FindError};
use yash_env::semantics::Field;
use yash_env::signal;
use yash_env::source::pretty::{Report, ReportType, Snippet};
use yash_env::system::{Errno, Fcntl, Isatty, SendSignal, Signals, Write};
#[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<S: SendSignal>(
env: &mut Env<S>,
signal: Option<signal::Number>,
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("signal {signal} not supported on this system")]
struct UnsupportedSignal<'a> {
signal: Signal,
origin: &'a Field,
}
impl UnsupportedSignal<'_> {
#[must_use]
pub fn to_report(&self) -> Report<'_> {
let mut report = Report::new();
report.r#type = ReportType::Error;
report.title = "unsupported signal".into();
report.snippets = Snippet::with_primary_span(&self.origin.origin, self.to_string().into());
report
}
}
impl<'a> From<&'a UnsupportedSignal<'a>> for Report<'a> {
#[inline]
fn from(error: &'a UnsupportedSignal<'a>) -> Self {
error.to_report()
}
}
#[derive(Clone, Debug, Error, PartialEq, Eq)]
#[error("{target}: {error}")]
struct TargetError<'a> {
target: &'a Field,
error: Error,
}
impl TargetError<'_> {
#[must_use]
pub fn to_report(&self) -> Report<'_> {
let mut report = Report::new();
report.r#type = ReportType::Error;
report.title = "cannot send signal".into();
report.snippets = Snippet::with_primary_span(
&self.target.origin,
format!("{}: {}", self.target.value, self.error).into(),
);
report
}
}
impl<'a> From<&'a TargetError<'a>> for Report<'a> {
#[inline]
fn from(error: &'a TargetError<'a>) -> Self {
error.to_report()
}
}
pub async fn execute<S>(
env: &mut Env<S>,
signal: Signal,
signal_origin: Option<&Field>,
targets: &[Field],
) -> crate::Result
where
S: Fcntl + Isatty + SendSignal + Signals + Write,
{
let Ok(signal) = signal.to_number(&env.system) else {
let origin = signal_origin.unwrap();
let report = UnsupportedSignal { signal, origin };
return report_failure(env, &report).await;
};
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(report) = merge_reports(&errors) {
report_failure(env, report).await
} else {
crate::Result::default()
}
}
#[cfg(test)]
mod tests {
use super::*;
use assert_matches::assert_matches;
use futures_util::FutureExt;
use std::rc::Rc;
use yash_env::job::Job;
use yash_env::job::ProcessState;
use yash_env::semantics::ExitStatus;
use yash_env::system::r#virtual::VirtualSystem;
use yash_env_test_helper::assert_stderr;
#[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(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(_)));
}
#[test]
fn execute_unsupported_signal() {
let system = VirtualSystem::new();
let state = Rc::clone(&system.state);
let mut env = Env::with_system(system);
let result = execute(&mut env, Signal::Number(-1), Some(&Field::dummy("-1")), &[])
.now_or_never()
.unwrap();
assert_eq!(result, crate::Result::from(ExitStatus::FAILURE));
assert_stderr(&state, |stderr| assert_ne!(stderr, ""));
}
}