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