Skip to main content

yash_builtin/
wait.rs

1// This file is part of yash, an extended POSIX shell.
2// Copyright (C) 2022 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//! Wait built-in
18//!
19//! This module implements the [`wait` built-in], which waits for asynchronous
20//! jobs to finish.
21//!
22//! [`wait` built-in]: https://magicant.github.io/yash-rs/builtins/wait.html
23//!
24//! # Implementation notes
25//!
26//! The built-in expects that an instance of
27//! [`RunSignalTrapIfCaught`](yash_env::trap::RunSignalTrapIfCaught) is stored
28//! in [`Env::any`] to handle trapped signals while waiting for jobs. If there
29//! is no such instance, the built-in will ignore all signals.
30
31use crate::common::report::{merge_reports, report_error, report_simple_failure};
32use itertools::Itertools as _;
33use yash_env::Env;
34use yash_env::job::Pid;
35use yash_env::option::State::Off;
36use yash_env::semantics::ExitStatus;
37use yash_env::semantics::Field;
38use yash_env::system::{Fcntl, Isatty, Sigaction, Sigmask, Signals, Wait, Write};
39
40/// Job specification (job ID or process ID)
41///
42/// Each operand of the `wait` built-in is parsed into a `JobSpec` value.
43#[derive(Clone, Debug, Eq, PartialEq)]
44pub enum JobSpec {
45    /// Process ID (non-negative decimal integer)
46    ProcessId(Pid),
47
48    /// Job ID (string of the form `%…`)
49    JobId(Field),
50}
51
52/// Parsed command line arguments to the `wait` built-in
53#[derive(Clone, Debug, Eq, PartialEq)]
54pub struct Command {
55    /// Operands that specify which jobs to wait for
56    ///
57    /// If empty, the built-in waits for all existing asynchronous jobs.
58    pub jobs: Vec<JobSpec>,
59}
60
61pub mod core;
62pub mod search;
63pub mod status;
64pub mod syntax;
65
66impl Command {
67    /// Waits for jobs specified by the indexes.
68    ///
69    /// If `indexes` is empty, waits for all jobs.
70    async fn await_jobs<S, I>(env: &mut Env<S>, indexes: I) -> Result<ExitStatus, core::Error>
71    where
72        S: Signals + Sigmask + Sigaction + Wait + 'static,
73        I: IntoIterator<Item = Option<usize>>,
74    {
75        // Currently, we ignore the job control option as required by POSIX.
76        // TODO: Add some way to specify this option
77        let job_control = Off; // env.options.get(Monitor);
78
79        // Await jobs specified by the indexes
80        let mut exit_status = None;
81        for index in indexes {
82            exit_status = Some(match index {
83                None => ExitStatus::NOT_FOUND,
84                Some(index) => {
85                    status::wait_while_running(env, &mut status::job_status(index, job_control))
86                        .await?
87                }
88            });
89        }
90        if let Some(exit_status) = exit_status {
91            return Ok(exit_status);
92        }
93
94        // If there were no indexes, await all jobs
95        status::wait_while_running(env, &mut status::any_job_is_running(job_control)).await
96    }
97
98    /// Executes the `wait` built-in.
99    pub async fn execute<S>(self, env: &mut Env<S>) -> crate::Result
100    where
101        S: Fcntl + Isatty + Sigaction + Sigmask + Signals + Wait + Write + 'static,
102    {
103        // Resolve job specifications to indexes
104        let jobs = self.jobs.into_iter();
105        let (indexes, errors): (Vec<_>, Vec<_>) = jobs
106            .map(|spec| search::resolve(&env.jobs, spec))
107            .partition_result();
108        if let Some(report) = merge_reports(&errors) {
109            return report_error(env, report).await;
110        }
111
112        // Await jobs specified by the indexes
113        match Self::await_jobs(env, indexes).await {
114            Ok(exit_status) => exit_status.into(),
115            Err(core::Error::Trapped(signal, divert)) => {
116                crate::Result::with_exit_status_and_divert(ExitStatus::from(signal), divert)
117            }
118            Err(error) => report_simple_failure(env, &error.to_string()).await,
119        }
120    }
121}
122
123/// Entry point for executing the `wait` built-in
124pub async fn main<S>(env: &mut Env<S>, args: Vec<Field>) -> crate::Result
125where
126    S: Fcntl + Isatty + Sigaction + Sigmask + Signals + Wait + Write + 'static,
127{
128    match syntax::parse(env, args) {
129        Ok(command) => command.execute(env).await,
130        Err(error) => report_error(env, &error).await,
131    }
132}
133
134#[cfg(test)]
135mod tests {
136    use super::*;
137    use futures_util::poll;
138    use std::pin::pin;
139    use std::task::Poll;
140    use yash_env::job::{Job, ProcessResult};
141    use yash_env::option::{Monitor, On};
142    use yash_env::subshell::{JobControl, Subshell};
143    use yash_env::system::GetPid as _;
144    use yash_env::system::SendSignal as _;
145    use yash_env::system::r#virtual::SIGSTOP;
146    use yash_env::system::r#virtual::VirtualSystem;
147    use yash_env::test_helper::{in_virtual_system, stub_tty};
148    use yash_env::trap::RunSignalTrapIfCaught;
149
150    pub(super) fn stub_run_signal_trap_if_caught<S: 'static>(env: &mut Env<S>) {
151        env.any.insert(Box::new(RunSignalTrapIfCaught::<S>(|_, _| {
152            Box::pin(std::future::ready(None))
153        })));
154    }
155
156    async fn suspend(env: &mut Env<VirtualSystem>) {
157        let target = env.system.getpid();
158        env.system.kill(target, Some(SIGSTOP)).await.unwrap();
159    }
160
161    async fn start_self_suspending_job(env: &mut Env<VirtualSystem>) {
162        let subshell =
163            Subshell::new(|env, _| Box::pin(suspend(env))).job_control(JobControl::Foreground);
164        let (pid, subshell_result) = subshell.start_and_wait(env).await.unwrap();
165        assert_eq!(subshell_result, ProcessResult::Stopped(SIGSTOP));
166        let mut job = Job::new(pid);
167        job.job_controlled = true;
168        job.state = subshell_result.into();
169        env.jobs.add(job);
170    }
171
172    #[test]
173    fn suspended_job() {
174        // Suspended jobs are not treated as finished, so the built-in waits indefinitely.
175        in_virtual_system(|mut env, state| async move {
176            stub_tty(&state);
177            stub_run_signal_trap_if_caught(&mut env);
178            env.options.set(Monitor, On);
179            start_self_suspending_job(&mut env).await;
180
181            let main = pin!(async move { main(&mut env, vec![]).await });
182            let poll = poll!(main);
183            assert_eq!(poll, Poll::Pending);
184        })
185    }
186}