yash_semantics/command/
item.rs

1// This file is part of yash, an extended POSIX shell.
2// Copyright (C) 2021 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 for Item.
18
19use crate::trap::run_exit_trap;
20
21use super::Command;
22use std::ops::ControlFlow::{Break, Continue};
23use std::rc::Rc;
24use yash_env::Env;
25use yash_env::System;
26use yash_env::io::Fd;
27use yash_env::io::print_error;
28use yash_env::job::Job;
29use yash_env::semantics::Divert;
30use yash_env::semantics::ExitStatus;
31use yash_env::semantics::Result;
32use yash_env::subshell::JobControl;
33use yash_env::subshell::Subshell;
34use yash_env::system::Mode;
35use yash_env::system::OfdAccess;
36use yash_syntax::source::Location;
37use yash_syntax::syntax;
38use yash_syntax::syntax::AndOrList;
39
40/// Executes the item.
41///
42/// # Synchronous command
43///
44/// If the item's `async_flag` is `None`, this function executes the and-or list
45/// in the item.
46///
47/// # Asynchronous command
48///
49/// If the item has an `async_flag` set, the and-or list is executed
50/// asynchronously in a subshell, whose process ID is [set to the job
51/// list](yash_env::job::JobList::set_last_async_pid) in the environment.
52///
53/// Since this function finishes before the asynchronous execution finishes, the
54/// exit status does not reflect the results of the and-or list; the exit status
55/// is always 0.
56///
57/// If the [`Monitor`] option is off, the standard input of the asynchronous
58/// and-or list is implicitly redirected to `/dev/null`.
59///
60/// [`Monitor`]: yash_env::option::Option::Monitor
61impl Command for syntax::Item {
62    async fn execute(&self, env: &mut Env) -> Result {
63        match &self.async_flag {
64            None => self.and_or.execute(env).await,
65            Some(async_flag) => execute_async(env, &self.and_or, async_flag).await,
66        }
67    }
68}
69
70async fn execute_async(env: &mut Env, and_or: &Rc<AndOrList>, async_flag: &Location) -> Result {
71    let and_or_2 = Rc::clone(and_or);
72    let subshell = Subshell::new(|env_2, job_control| {
73        Box::pin(async move { async_body(env_2, job_control, &and_or_2).await })
74    });
75    let subshell = subshell
76        .job_control(JobControl::Background)
77        .ignore_sigint_sigquit(true);
78    match subshell.start(env).await {
79        Ok((pid, job_control)) => {
80            // remember the process ID as a job
81            let mut job = Job::new(pid);
82            job.state_changed = false;
83            job.name = and_or.to_string();
84            if let Some(job_control) = job_control {
85                debug_assert_eq!(job_control, JobControl::Background);
86                job.job_controlled = true;
87            }
88            let job_index = env.jobs.add(job);
89            env.jobs.set_last_async_pid(pid);
90
91            if env.is_interactive() {
92                // report the job number and process ID
93                let job_number = job_index + 1;
94                let report = format!("[{job_number}] {pid}\n");
95                env.system.print_error(&report).await;
96            }
97
98            env.exit_status = ExitStatus::SUCCESS;
99            Continue(())
100        }
101        Err(errno) => {
102            print_error(
103                env,
104                "cannot start a subshell to run an asynchronous command".into(),
105                errno.to_string().into(),
106                async_flag,
107            )
108            .await;
109
110            Break(Divert::Interrupt(Some(ExitStatus::NOEXEC)))
111        }
112    }
113}
114
115async fn async_body(env: &mut Env, job_control: Option<JobControl>, and_or: &AndOrList) {
116    if job_control.is_none() {
117        nullify_stdin(env).ok();
118    }
119    let result = and_or.execute(env).await;
120    env.apply_result(result);
121
122    run_exit_trap(env).await;
123}
124
125fn nullify_stdin(env: &mut Env) -> std::result::Result<(), yash_env::system::Errno> {
126    env.system.close(Fd::STDIN)?;
127
128    let path = c"/dev/null";
129    let fd = env
130        .system
131        .open(path, OfdAccess::ReadOnly, Default::default(), Mode::empty())?;
132    assert_eq!(fd, Fd::STDIN);
133    Ok(())
134}
135
136#[cfg(test)]
137mod tests {
138    use super::*;
139    use crate::tests::cat_builtin;
140    use crate::tests::echo_builtin;
141    use crate::tests::return_builtin;
142    use futures_util::FutureExt;
143    use futures_util::task::LocalSpawnExt;
144    use std::cell::RefCell;
145    use std::rc::Rc;
146    use yash_env::VirtualSystem;
147    use yash_env::job::ProcessState;
148    use yash_env::option::Option::{Interactive, Monitor};
149    use yash_env::option::State::On;
150    use yash_env::system::r#virtual::FileBody;
151    use yash_env::system::r#virtual::Inode;
152    use yash_env::system::r#virtual::SystemState;
153    use yash_env_test_helper::LocalExecutor;
154    use yash_env_test_helper::assert_stderr;
155    use yash_env_test_helper::assert_stdout;
156    use yash_env_test_helper::in_virtual_system;
157    use yash_env_test_helper::stub_tty;
158
159    #[test]
160    fn item_execute_sync() {
161        let mut env = Env::new_virtual();
162        env.builtins.insert("return", return_builtin());
163        let and_or: syntax::AndOrList = "return -n 42".parse().unwrap();
164        let item = syntax::Item {
165            and_or: Rc::new(and_or),
166            async_flag: None,
167        };
168        let result = item.execute(&mut env).now_or_never().unwrap();
169        assert_eq!(result, Continue(()));
170        assert_eq!(env.exit_status, ExitStatus(42));
171    }
172
173    #[test]
174    fn item_execute_async_exit_status() {
175        in_virtual_system(|mut env, _state| async move {
176            env.builtins.insert("return", return_builtin());
177            env.exit_status = ExitStatus::FAILURE;
178
179            let item = syntax::Item {
180                and_or: Rc::new("return -n 42".parse().unwrap()),
181                async_flag: Some(Location::dummy("")),
182            };
183            let result = item.execute(&mut env).await;
184            assert_eq!(result, Continue(()));
185            assert_eq!(env.exit_status, ExitStatus::SUCCESS);
186        })
187    }
188
189    #[test]
190    fn item_execute_async_effect() {
191        let system = VirtualSystem::new();
192        let state = Rc::clone(&system.state);
193        let mut executor = futures_executor::LocalPool::new();
194        state.borrow_mut().executor = Some(Rc::new(LocalExecutor(executor.spawner())));
195        let mut env = Env::with_system(Box::new(system));
196        env.builtins.insert("echo", echo_builtin());
197
198        let and_or: syntax::AndOrList = "echo foo".parse().unwrap();
199        let item = syntax::Item {
200            and_or: Rc::new(and_or),
201            async_flag: Some(Location::dummy("")),
202        };
203
204        executor
205            .spawner()
206            .spawn_local(async move {
207                let result = item.execute(&mut env).await;
208                assert_eq!(result, Continue(()));
209            })
210            .unwrap();
211        executor.run_until_stalled();
212
213        assert_stdout(&state, |stdout| assert_eq!(stdout, "foo\n"));
214    }
215
216    #[test]
217    fn item_execute_async_job() {
218        in_virtual_system(|mut env, _state| async move {
219            env.builtins.insert("return", return_builtin());
220
221            let item = syntax::Item {
222                and_or: Rc::new("return  -n  42".parse().unwrap()),
223                async_flag: Some(Location::dummy("")),
224            };
225            _ = item.execute(&mut env).await;
226
227            let job = &env.jobs[0];
228            assert!(!job.job_controlled);
229            assert!(!job.state_changed);
230            assert_eq!(job.state, ProcessState::Running);
231            assert_eq!(job.pid, env.jobs.last_async_pid());
232            assert_eq!(job.name, "return -n 42");
233        })
234    }
235
236    #[test]
237    fn item_execute_async_pid() {
238        in_virtual_system(|mut env, state| async move {
239            env.builtins.insert("return", return_builtin());
240
241            let item = syntax::Item {
242                and_or: Rc::new("return -n 42".parse().unwrap()),
243                async_flag: Some(Location::dummy("")),
244            };
245            _ = item.execute(&mut env).await;
246
247            let pids = state.borrow().processes.keys().copied().collect::<Vec<_>>();
248            assert_eq!(pids, [env.main_pid, env.jobs.last_async_pid()]);
249        })
250    }
251
252    #[test]
253    fn item_execute_async_no_report_if_non_interactive() {
254        in_virtual_system(|mut env, state| async move {
255            env.builtins.insert("return", return_builtin());
256
257            let item = syntax::Item {
258                and_or: Rc::new("return -n 42".parse().unwrap()),
259                async_flag: Some(Location::dummy("")),
260            };
261            _ = item.execute(&mut env).await;
262
263            assert_stderr(&state, |stderr| assert_eq!(stderr, ""));
264        })
265    }
266
267    #[test]
268    fn item_execute_async_report_if_interactive() {
269        in_virtual_system(|mut env, state| async move {
270            env.builtins.insert("return", return_builtin());
271            env.options.set(Interactive, On);
272
273            let item = syntax::Item {
274                and_or: Rc::new("return -n 42".parse().unwrap()),
275                async_flag: Some(Location::dummy("")),
276            };
277            _ = item.execute(&mut env).await;
278
279            let expected_report = format!("[1] {}\n", env.jobs.last_async_pid());
280            assert_stderr(&state, |stderr| assert_eq!(stderr, expected_report));
281        })
282    }
283
284    #[test]
285    fn item_execute_async_fail() {
286        let system = VirtualSystem::new();
287        let state = Rc::clone(&system.state);
288        let mut env = Env::with_system(Box::new(system));
289        env.builtins.insert("return", return_builtin());
290
291        let and_or: syntax::AndOrList = "return -n 42".parse().unwrap();
292        let item = syntax::Item {
293            and_or: Rc::new(and_or),
294            async_flag: Some(Location::dummy("X")),
295        };
296        let result = item.execute(&mut env).now_or_never().unwrap();
297        assert_eq!(result, Break(Divert::Interrupt(Some(ExitStatus::NOEXEC))));
298        assert_stderr(&state, |stderr| {
299            assert!(
300                stderr.contains("asynchronous"),
301                "unexpected error message: {stderr:?}"
302            )
303        });
304    }
305
306    #[test]
307    fn item_execute_async_background() {
308        in_virtual_system(|mut env, state| async move {
309            env.builtins.insert("return", return_builtin());
310            env.options.set(Monitor, On);
311            stub_tty(&state);
312
313            let item = syntax::Item {
314                and_or: Rc::new("return  -n  42".parse().unwrap()),
315                async_flag: Some(Location::dummy("")),
316            };
317
318            _ = item.execute(&mut env).await;
319
320            let state = state.borrow();
321            let process = &state.processes[&env.jobs.last_async_pid()];
322            assert_ne!(process.pgid(), env.main_pgid);
323            assert!(env.jobs[0].job_controlled);
324        })
325    }
326
327    fn ignore_sigttin(env: &mut Env) {
328        let signal = env
329            .system
330            .signal_number_from_name(yash_env::signal::Name::Ttin)
331            .unwrap();
332        env.traps
333            .set_action(
334                &mut env.system,
335                signal,
336                yash_env::trap::Action::Ignore,
337                Location::dummy(""),
338                false,
339            )
340            .unwrap();
341    }
342
343    fn stub_dev_null_and_stdin(state: &RefCell<SystemState>) {
344        let mut state = state.borrow_mut();
345        state
346            .file_system
347            .save("/dev/null", Rc::new(RefCell::new(Inode::new([]))))
348            .unwrap();
349        state
350            .file_system
351            .get("/dev/stdin")
352            .unwrap()
353            .borrow_mut()
354            .body = FileBody::new(*b"input\n");
355    }
356
357    #[test]
358    fn item_execute_async_stdin_not_job_controlled() {
359        in_virtual_system(|mut env, state| async move {
360            env.builtins.insert("cat", cat_builtin());
361            ignore_sigttin(&mut env);
362            stub_tty(&state);
363            stub_dev_null_and_stdin(&state);
364
365            let item = syntax::Item {
366                and_or: Rc::new("cat".parse().unwrap()),
367                async_flag: Some(Location::dummy("")),
368            };
369
370            _ = item.execute(&mut env).await;
371            env.wait_for_subshell(env.jobs.last_async_pid())
372                .await
373                .unwrap();
374            assert_stdout(&state, |stdout| assert_eq!(stdout, ""));
375        })
376    }
377
378    #[test]
379    fn item_execute_async_stdin_job_controlled() {
380        in_virtual_system(|mut env, state| async move {
381            env.builtins.insert("cat", cat_builtin());
382            env.options.set(Monitor, On);
383            ignore_sigttin(&mut env);
384            stub_tty(&state);
385            stub_dev_null_and_stdin(&state);
386
387            let item = syntax::Item {
388                and_or: Rc::new("cat".parse().unwrap()),
389                async_flag: Some(Location::dummy("")),
390            };
391
392            _ = item.execute(&mut env).await;
393            env.wait_for_subshell(env.jobs.last_async_pid())
394                .await
395                .unwrap();
396            assert_stdout(&state, |stdout| assert_eq!(stdout, "input\n"));
397        })
398    }
399}