yash_builtin/kill/
send.rs1use 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#[derive(Clone, Debug, Error, PartialEq, Eq)]
37pub enum Error {
38 #[error(transparent)]
40 ProcessId(#[from] ParseIntError),
41 #[error(transparent)]
43 JobId(#[from] FindError),
44 #[error("target job is not controlled by the current shell environment")]
46 Unowned,
47 #[error("target job is not job-controlled")]
49 Unmonitored,
50 #[error("target job has finished")]
52 Finished,
53 #[error(transparent)]
55 System(#[from] Errno),
56}
57
58pub 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
81pub 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 origin: &'a Field,
98}
99
100impl UnsupportedSignal<'_> {
101 #[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 #[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
148pub 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}