Skip to main content

yash_builtin/kill/
send.rs

1// This file is part of yash, an extended POSIX shell.
2// Copyright (C) 2024 WATANABE Yuki
3//
4// This program is free software: you can redistribute it and/or modify
5// it under the terms of the GNU General Public License as published by
6// the Free Software Foundation, either version 3 of the License, or
7// (at your option) any later version.
8//
9// This program is distributed in the hope that it will be useful,
10// but WITHOUT ANY WARRANTY; without even the implied warranty of
11// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
12// GNU General Public License for more details.
13//
14// You should have received a copy of the GNU General Public License
15// along with this program.  If not, see <https://www.gnu.org/licenses/>.
16
17//! Implementation of `Command::Send`
18//!
19//! [`execute`] calls [`send`] for each target and reports all errors.
20//! [`send`] uses [`resolve_target`] to determine the argument to the
21//! [`kill`](SendSignal::kill) system call.
22
23use crate::common::report::{merge_reports, report_failure};
24use std::num::{NonZero, ParseIntError};
25use thiserror::Error;
26use yash_env::Env;
27use yash_env::job::Pid;
28use yash_env::job::id::parse_tail;
29use yash_env::job::{JobList, id::FindError};
30use yash_env::semantics::Field;
31use yash_env::signal::{Number, RawNumber};
32use yash_env::source::pretty::{Report, ReportType, Snippet};
33use yash_env::system::{Errno, Fcntl, Isatty, SendSignal, Signals, Write};
34
35/// Error that may occur while [sending](send) a signal.
36#[derive(Clone, Debug, Error, PartialEq, Eq)]
37pub enum Error {
38    /// The specified process (group) ID was not a valid integer.
39    #[error(transparent)]
40    ProcessId(#[from] ParseIntError),
41    /// The specified job ID did not uniquely identify a job.
42    #[error(transparent)]
43    JobId(#[from] FindError),
44    /// The target job is not controlled by the current shell environment.
45    #[error("target job is not controlled by the current shell environment")]
46    Unowned,
47    /// The job ID specifies a job that is not job-controlled.
48    #[error("target job is not job-controlled")]
49    Unmonitored,
50    /// The target job has finished.
51    #[error("target job has finished")]
52    Finished,
53    /// An error occurred in the underlying system call.
54    #[error(transparent)]
55    System(#[from] Errno),
56}
57
58/// Resolves the specified target into a process (group) ID.
59///
60/// The target may be specified as a job ID, a process ID, or a process group
61/// ID. In case of a process group ID, the value should be negative.
62pub fn resolve_target(jobs: &JobList, target: &str) -> Result<Pid, Error> {
63    if let Some(tail) = target.strip_prefix('%') {
64        let job_id = parse_tail(tail);
65        let index = job_id.find(jobs)?;
66        let job = &jobs[index];
67        if !job.is_owned {
68            Err(Error::Unowned)
69        } else if !job.job_controlled {
70            Err(Error::Unmonitored)
71        } else if !job.state.is_alive() {
72            Err(Error::Finished)
73        } else {
74            Ok(-job.pid)
75        }
76    } else {
77        Ok(Pid(target.parse()?))
78    }
79}
80
81/// Sends the specified signal to the specified target.
82pub async fn send<S: SendSignal>(
83    env: &mut Env<S>,
84    signal: Option<Number>,
85    target: &Field,
86) -> Result<(), Error> {
87    let pid = resolve_target(&env.jobs, &target.value)?;
88    env.system.kill(pid, signal).await?;
89    Ok(())
90}
91
92#[derive(Clone, Debug, Error, PartialEq, Eq)]
93#[error("signal {signal} not supported on this system")]
94struct UnsupportedSignal<'a> {
95    signal: RawNumber,
96    // TODO Consider: origin: &'a Location,
97    origin: &'a Field,
98}
99
100impl UnsupportedSignal<'_> {
101    /// Converts this error to a [`Report`].
102    #[must_use]
103    pub fn to_report(&self) -> Report<'_> {
104        let mut report = Report::new();
105        report.r#type = ReportType::Error;
106        report.title = "unsupported signal".into();
107        report.snippets = Snippet::with_primary_span(&self.origin.origin, self.to_string().into());
108        report
109    }
110}
111
112impl<'a> From<&'a UnsupportedSignal<'a>> for Report<'a> {
113    #[inline]
114    fn from(error: &'a UnsupportedSignal<'a>) -> Self {
115        error.to_report()
116    }
117}
118
119#[derive(Clone, Debug, Error, PartialEq, Eq)]
120#[error("{target}: {error}")]
121struct TargetError<'a> {
122    target: &'a Field,
123    error: Error,
124}
125
126impl TargetError<'_> {
127    /// Converts this error to a [`Report`].
128    #[must_use]
129    pub fn to_report(&self) -> Report<'_> {
130        let mut report = Report::new();
131        report.r#type = ReportType::Error;
132        report.title = "cannot send signal".into();
133        report.snippets = Snippet::with_primary_span(
134            &self.target.origin,
135            format!("{}: {}", self.target.value, self.error).into(),
136        );
137        report
138    }
139}
140
141impl<'a> From<&'a TargetError<'a>> for Report<'a> {
142    #[inline]
143    fn from(error: &'a TargetError<'a>) -> Self {
144        error.to_report()
145    }
146}
147
148/// Executes the `Send` command.
149///
150/// This function sends the specified signal to the specified targets.
151/// If an error occurs, it reports the error to the standard error and returns a
152/// non-zero exit status.
153///
154/// `signal_origin` is the field that specified the signal. It is used to report
155/// the error location if the signal is not supported on the current system. If
156/// it is `None` and the `signal` is not supported, the function panics.
157pub async fn execute<S>(
158    env: &mut Env<S>,
159    signal: RawNumber,
160    signal_origin: Option<&Field>,
161    targets: &[Field],
162) -> crate::Result
163where
164    S: Fcntl + Isatty + SendSignal + Signals + Write,
165{
166    let signal_number = NonZero::new(signal).map(Number::from_raw_unchecked);
167
168    let mut errors = Vec::new();
169    for target in targets {
170        match send(env, signal_number, target).await {
171            Ok(()) => (),
172            Err(Error::System(Errno::EINVAL)) => {
173                let origin = signal_origin.unwrap();
174                let report = UnsupportedSignal { signal, origin };
175                return report_failure(env, &report).await;
176            }
177            Err(error) => errors.push(TargetError { target, error }),
178        }
179    }
180
181    if let Some(report) = merge_reports(&errors) {
182        report_failure(env, report).await
183    } else {
184        crate::Result::default()
185    }
186}
187
188#[cfg(test)]
189mod tests {
190    use super::*;
191    use assert_matches::assert_matches;
192    use futures_util::FutureExt;
193    use std::rc::Rc;
194    use yash_env::job::Job;
195    use yash_env::job::ProcessState;
196    use yash_env::semantics::ExitStatus;
197    use yash_env::system::r#virtual::VirtualSystem;
198    use yash_env::test_helper::assert_stderr;
199
200    #[test]
201    fn resolve_target_process_ids() {
202        let jobs = JobList::new();
203
204        let result = resolve_target(&jobs, "123");
205        assert_eq!(result, Ok(Pid(123)));
206
207        let result = resolve_target(&jobs, "-456");
208        assert_eq!(result, Ok(Pid(-456)));
209    }
210
211    #[test]
212    fn resolve_target_job_id() {
213        let mut jobs = JobList::new();
214        let mut job = Job::new(Pid(123));
215        job.job_controlled = true;
216        job.is_owned = true;
217        job.state = ProcessState::Running;
218        job.name = "my job".into();
219        jobs.add(job);
220
221        let result = resolve_target(&jobs, "%my");
222        assert_eq!(result, Ok(Pid(-123)));
223    }
224
225    #[test]
226    fn resolve_target_job_find_error() {
227        let jobs = JobList::new();
228        let result = resolve_target(&jobs, "%my");
229        assert_eq!(result, Err(Error::JobId(FindError::NotFound)));
230    }
231
232    #[test]
233    fn resolve_target_unowned() {
234        let mut jobs = JobList::new();
235        let mut job = Job::new(Pid(123));
236        job.job_controlled = true;
237        job.is_owned = false;
238        job.state = ProcessState::Running;
239        job.name = "my job".into();
240        jobs.add(job);
241
242        let result = resolve_target(&jobs, "%my");
243        assert_eq!(result, Err(Error::Unowned));
244    }
245
246    #[test]
247    fn resolve_target_unmonitored() {
248        let mut jobs = JobList::new();
249        let mut job = Job::new(Pid(123));
250        job.job_controlled = false;
251        job.is_owned = true;
252        job.state = ProcessState::Running;
253        job.name = "my job".into();
254        jobs.add(job);
255
256        let result = resolve_target(&jobs, "%my");
257        assert_eq!(result, Err(Error::Unmonitored));
258    }
259
260    #[test]
261    fn resolve_target_finished() {
262        let mut jobs = JobList::new();
263        let mut job = Job::new(Pid(123));
264        job.job_controlled = true;
265        job.is_owned = true;
266        job.state = ProcessState::exited(0);
267        job.name = "my job".into();
268        jobs.add(job);
269
270        let result = resolve_target(&jobs, "%my");
271        assert_eq!(result, Err(Error::Finished));
272    }
273
274    #[test]
275    fn resolve_target_invalid_string() {
276        let jobs = JobList::new();
277        let result = resolve_target(&jobs, "abc");
278        assert_matches!(result, Err(Error::ProcessId(_)));
279    }
280
281    #[test]
282    fn execute_unsupported_signal() {
283        let system = VirtualSystem::new();
284        let pid = system.process_id.to_string();
285        let state = Rc::clone(&system.state);
286        let mut env = Env::with_system(system);
287        let result = execute(
288            &mut env,
289            -1,
290            Some(&Field::dummy("-1")),
291            &Field::dummies([pid]),
292        )
293        .now_or_never()
294        .unwrap();
295        assert_eq!(result, crate::Result::from(ExitStatus::FAILURE));
296        assert_stderr(&state, |stderr| assert_ne!(stderr, ""));
297    }
298}