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}