Skip to main content

yash_builtin/wait/
core.rs

1// This file is part of yash, an extended POSIX shell.
2// Copyright (C) 2023 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 the wait built-in core logic
18//!
19//! The [`wait_for_any_job_or_trap`] function waits for a job status change or
20//! trap action. The [`Error`](enum@Error) type represents errors that may occur
21//! in the function.
22
23use thiserror::Error;
24use yash_env::Env;
25use yash_env::job::Pid;
26use yash_env::signal;
27use yash_env::system::{Errno, Sigaction, Sigmask, Signals, Wait};
28use yash_env::trap::RunSignalTrapIfCaught;
29
30/// Errors that may occur while waiting for a job
31#[derive(Clone, Copy, Debug, Eq, Error, PartialEq)]
32pub enum Error {
33    /// There is no job to wait for.
34    #[error("no job to wait for")]
35    NothingToWait,
36    /// The built-in was interrupted by a signal and the trap action was
37    /// executed.
38    #[error("trapped signal {0}")]
39    Trapped(signal::Number, yash_env::semantics::Result),
40    /// An unexpected error occurred in the underlying system.
41    #[error("system error: {0}")]
42    SystemError(#[from] Errno),
43}
44
45/// Waits for a job status change or trap.
46///
47/// This function waits for a next event, which is either an update of a job
48/// status or a trap action. If the event is a job status change, this function
49/// returns `Ok(())`. Otherwise, this function performs the trap action and
50/// returns the signal and the result of the trap action.
51///
52/// This function expects that an instance of [`RunSignalTrapIfCaught`] is
53/// stored in [`Env::any`] to check if any signal has been caught and run the
54/// corresponding trap action. If there is no such instance, this function will
55/// **panic**.
56///
57/// Note that this function returns on a job state change of any kind. You need
58/// to call this function repeatedly until the job state becomes the one you
59/// want.
60///
61/// If there is no job to wait for, this function returns
62/// `Err(Error::NothingToWait)` immediately.
63pub async fn wait_for_any_job_or_trap<S>(env: &mut Env<S>) -> Result<(), Error>
64where
65    S: Signals + Sigmask + Sigaction + Wait + 'static,
66{
67    let RunSignalTrapIfCaught(run_trap_if_caught) = *env
68        .any
69        .get()
70        .expect("`RunSignalTrapIfCaught` should be in `env.any`");
71
72    // We need to install the internal disposition before calling `wait` so we
73    // don't miss any `SIGCHLD` that may arrive between `wait` and
74    // `wait_for_signals`.  See also Env::wait_for_subshell.
75    env.traps
76        .enable_internal_disposition_for_sigchld(&env.system)
77        .await?;
78
79    loop {
80        // Poll for a job state change. Note that this `wait` call returns
81        // immediately regardless of whether there is a new job state.
82        match env.system.wait(Pid::ALL) {
83            Ok(None) => {
84                // The current process has child processes, but none of them has
85                // changed its state. Wait for a signal.
86                let signals = env.wait_for_signals().await;
87                for signal in signals.iter().cloned() {
88                    if let Some(result) = run_trap_if_caught(env, signal).await {
89                        return Err(Error::Trapped(signal, result));
90                    }
91                }
92            }
93
94            Ok(Some((pid, state))) => {
95                // Some job has changed its state.
96                env.jobs.update_status(pid, state);
97                return Ok(());
98            }
99
100            // The current process has no child processes.
101            Err(Errno::ECHILD) => return Err(Error::NothingToWait),
102
103            // Unexpected error
104            Err(errno) => return Err(Error::SystemError(errno)),
105        }
106    }
107}
108
109#[cfg(test)]
110mod tests {
111    use super::super::tests::stub_run_signal_trap_if_caught;
112    use super::*;
113    use futures_util::FutureExt as _;
114    use futures_util::poll;
115    use std::future::{pending, ready};
116    use std::ops::ControlFlow::Continue;
117    use std::pin::pin;
118    use std::task::Poll;
119    use yash_env::VirtualSystem;
120    use yash_env::job::Job;
121    use yash_env::job::ProcessState;
122    use yash_env::semantics::ExitStatus;
123    use yash_env::source::Location;
124    use yash_env::subshell::Subshell;
125    use yash_env::system::SendSignal as _;
126    use yash_env::system::r#virtual::{SIGSTOP, SIGTERM};
127    use yash_env::test_helper::in_virtual_system;
128    use yash_env::trap::Action;
129    use yash_env::variable::Value;
130
131    #[test]
132    fn running_job() {
133        in_virtual_system(|mut env, _| async move {
134            stub_run_signal_trap_if_caught(&mut env);
135
136            // Start a child process that never exits.
137            let subshell = Subshell::new(|_, _| Box::pin(pending()));
138            subshell.start(&mut env).await.unwrap();
139
140            // The job is not finished, so the function keeps waiting.
141            let future = pin!(wait_for_any_job_or_trap(&mut env));
142            assert_eq!(poll!(future), Poll::Pending);
143        });
144    }
145
146    #[test]
147    fn finished_job() {
148        in_virtual_system(|mut env, _| async move {
149            stub_run_signal_trap_if_caught(&mut env);
150
151            // Start a child process that exits immediately.
152            let subshell = Subshell::new(|_, _| Box::pin(ready(())));
153            let pid = subshell.start(&mut env).await.unwrap().0;
154            let index = env.jobs.add(Job::new(pid));
155
156            // The job is finished, so the function returns immediately.
157            let result = wait_for_any_job_or_trap(&mut env).await;
158            assert_eq!(result, Ok(()));
159            // The job state is updated.
160            assert_eq!(
161                env.jobs[index].state,
162                ProcessState::exited(ExitStatus::default()),
163            );
164        });
165    }
166
167    #[test]
168    fn suspended_job() {
169        in_virtual_system(|mut env, _| async move {
170            stub_run_signal_trap_if_caught(&mut env);
171
172            // Start a child process that never exits.
173            let subshell = Subshell::new(|_, _| Box::pin(pending()));
174            let pid = subshell.start(&mut env).await.unwrap().0;
175            let index = env.jobs.add(Job::new(pid));
176            // Suspend the child process.
177            env.system.kill(pid, Some(SIGSTOP)).await.unwrap();
178
179            // The job is suspended, so the function returns immediately.
180            let result = wait_for_any_job_or_trap(&mut env).await;
181            assert_eq!(result, Ok(()));
182            // The job state is updated.
183            assert_eq!(env.jobs[index].state, ProcessState::stopped(SIGSTOP));
184        });
185    }
186
187    #[test]
188    fn trap() {
189        in_virtual_system(|mut env, state| async move {
190            env.any
191                .insert(Box::new(RunSignalTrapIfCaught::<VirtualSystem>(
192                    |env, signal| {
193                        Box::pin(async move {
194                            yash_semantics::trap::run_trap_if_caught(env, signal).await
195                        })
196                    },
197                )));
198
199            let system = VirtualSystem {
200                state,
201                process_id: env.main_pid,
202            };
203
204            // Start a child process that never exits.
205            let subshell = Subshell::new(|_, _| Box::pin(pending()));
206            subshell.start(&mut env).await.unwrap();
207
208            // Set a trap for SIGTERM.
209            env.traps
210                .set_action(
211                    &env.system,
212                    SIGTERM,
213                    Action::Command("foo=bar".into()),
214                    Location::dummy("somewhere"),
215                    false,
216                )
217                .await
218                .unwrap();
219
220            {
221                // The job is not finished, so the function keeps waiting.
222                let mut future = pin!(wait_for_any_job_or_trap(&mut env));
223                assert_eq!(poll!(&mut future), Poll::Pending);
224
225                // Trigger the trap.
226                _ = system.current_process_mut().raise_signal(SIGTERM);
227
228                // Now the function should return.
229                let result = future.await;
230                assert_eq!(result, Err(Error::Trapped(SIGTERM, Continue(()))));
231            }
232
233            // The trap action must have assigned the variable.
234            assert_eq!(
235                env.variables.get("foo").unwrap().value,
236                Some(Value::scalar("bar")),
237            );
238        });
239    }
240
241    #[test]
242    fn no_child_processes() {
243        let mut env = Env::new_virtual();
244        stub_run_signal_trap_if_caught(&mut env);
245
246        let result = wait_for_any_job_or_trap(&mut env).now_or_never().unwrap();
247        assert_eq!(result, Err(Error::NothingToWait));
248    }
249}