Skip to main content

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::Config;
32use yash_env::subshell::JobControl;
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 mut config = Config::new();
75    config.job_control = Some(JobControl::Background);
76    config.ignores_sigint_sigquit = true;
77    let subshell = config.start(env, async move |env_2, job_control| {
78        async_body(env_2, job_control, &and_or_2).await
79    });
80    match subshell.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.insert(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).await.ok();
124    }
125    let result = and_or.execute(env).await;
126    env.apply_result(result);
127
128    run_exit_trap(env).await;
129}
130
131async fn 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        .await?;
141    assert_eq!(fd, Fd::STDIN);
142    Ok(())
143}
144
145#[cfg(test)]
146mod tests {
147    use super::*;
148    use crate::tests::cat_builtin;
149    use crate::tests::echo_builtin;
150    use crate::tests::return_builtin;
151    use futures_util::FutureExt as _;
152    use futures_util::task::LocalSpawnExt as _;
153    use std::cell::RefCell;
154    use std::rc::Rc;
155    use yash_env::VirtualSystem;
156    use yash_env::job::ProcessState;
157    use yash_env::option::Option::{Interactive, Monitor};
158    use yash_env::option::State::On;
159    use yash_env::system::Concurrent;
160    use yash_env::system::Signals as _;
161    use yash_env::system::r#virtual::FileBody;
162    use yash_env::system::r#virtual::Inode;
163    use yash_env::system::r#virtual::SystemState;
164    use yash_env::test_helper::assert_stderr;
165    use yash_env::test_helper::assert_stdout;
166    use yash_env::test_helper::in_virtual_system;
167    use yash_env::test_helper::stub_tty;
168
169    #[test]
170    fn item_execute_sync() {
171        let mut env = Env::new_virtual();
172        env.builtins.insert("return", return_builtin());
173        let and_or: syntax::AndOrList = "return -n 42".parse().unwrap();
174        let item = syntax::Item {
175            and_or: Rc::new(and_or),
176            async_flag: None,
177        };
178        let result = item.execute(&mut env).now_or_never().unwrap();
179        assert_eq!(result, Continue(()));
180        assert_eq!(env.exit_status, ExitStatus(42));
181    }
182
183    #[test]
184    fn item_execute_async_exit_status() {
185        in_virtual_system(|mut env, _state| async move {
186            env.builtins.insert("return", return_builtin());
187            env.exit_status = ExitStatus::FAILURE;
188
189            let item = syntax::Item {
190                and_or: Rc::new("return -n 42".parse().unwrap()),
191                async_flag: Some(Location::dummy("")),
192            };
193            let result = item.execute(&mut env).await;
194            assert_eq!(result, Continue(()));
195            assert_eq!(env.exit_status, ExitStatus::SUCCESS);
196        })
197    }
198
199    #[test]
200    fn item_execute_async_effect() {
201        let system = VirtualSystem::new();
202        let state = Rc::clone(&system.state);
203        let mut executor = futures_executor::LocalPool::new();
204        state.borrow_mut().executor = Some(Rc::new(executor.spawner()));
205        let mut env = Env::with_system(Rc::new(Concurrent::new(system)));
206        env.builtins.insert("echo", echo_builtin());
207
208        let and_or: syntax::AndOrList = "echo foo".parse().unwrap();
209        let item = syntax::Item {
210            and_or: Rc::new(and_or),
211            async_flag: Some(Location::dummy("")),
212        };
213
214        executor
215            .spawner()
216            .spawn_local(async move {
217                let result = item.execute(&mut env).await;
218                assert_eq!(result, Continue(()));
219            })
220            .unwrap();
221        executor.run_until_stalled();
222
223        assert_stdout(&state, |stdout| assert_eq!(stdout, "foo\n"));
224    }
225
226    #[test]
227    fn item_execute_async_job() {
228        in_virtual_system(|mut env, _state| async move {
229            env.builtins.insert("return", return_builtin());
230
231            let item = syntax::Item {
232                and_or: Rc::new("return  -n  42".parse().unwrap()),
233                async_flag: Some(Location::dummy("")),
234            };
235            _ = item.execute(&mut env).await;
236
237            let job = &env.jobs[0];
238            assert!(!job.job_controlled);
239            assert!(!job.state_changed);
240            assert_eq!(job.state, ProcessState::Running);
241            assert_eq!(job.pid, env.jobs.last_async_pid());
242            assert_eq!(job.name, "return -n 42");
243        })
244    }
245
246    #[test]
247    fn item_execute_async_pid() {
248        in_virtual_system(|mut env, state| async move {
249            env.builtins.insert("return", return_builtin());
250
251            let item = syntax::Item {
252                and_or: Rc::new("return -n 42".parse().unwrap()),
253                async_flag: Some(Location::dummy("")),
254            };
255            _ = item.execute(&mut env).await;
256
257            let pids = state.borrow().processes.keys().copied().collect::<Vec<_>>();
258            assert_eq!(pids, [env.main_pid, env.jobs.last_async_pid()]);
259        })
260    }
261
262    #[test]
263    fn item_execute_async_no_report_if_non_interactive() {
264        in_virtual_system(|mut env, state| async move {
265            env.builtins.insert("return", return_builtin());
266
267            let item = syntax::Item {
268                and_or: Rc::new("return -n 42".parse().unwrap()),
269                async_flag: Some(Location::dummy("")),
270            };
271            _ = item.execute(&mut env).await;
272
273            assert_stderr(&state, |stderr| assert_eq!(stderr, ""));
274        })
275    }
276
277    #[test]
278    fn item_execute_async_report_if_interactive() {
279        in_virtual_system(|mut env, state| async move {
280            env.builtins.insert("return", return_builtin());
281            env.options.set(Interactive, On);
282
283            let item = syntax::Item {
284                and_or: Rc::new("return -n 42".parse().unwrap()),
285                async_flag: Some(Location::dummy("")),
286            };
287            _ = item.execute(&mut env).await;
288
289            let expected_report = format!("[1] {}\n", env.jobs.last_async_pid());
290            assert_stderr(&state, |stderr| assert_eq!(stderr, expected_report));
291        })
292    }
293
294    #[test]
295    fn item_execute_async_fail() {
296        let system = VirtualSystem::new();
297        let state = Rc::clone(&system.state);
298        let mut env = Env::with_system(Rc::new(Concurrent::new(system)));
299        env.builtins.insert("return", return_builtin());
300
301        let and_or: syntax::AndOrList = "return -n 42".parse().unwrap();
302        let item = syntax::Item {
303            and_or: Rc::new(and_or),
304            async_flag: Some(Location::dummy("X")),
305        };
306        let result = item.execute(&mut env).now_or_never().unwrap();
307        assert_eq!(result, Break(Divert::Interrupt(Some(ExitStatus::NOEXEC))));
308        assert_stderr(&state, |stderr| {
309            assert!(
310                stderr.contains("asynchronous"),
311                "unexpected error message: {stderr:?}"
312            )
313        });
314    }
315
316    #[test]
317    fn item_execute_async_background() {
318        in_virtual_system(|mut env, state| async move {
319            env.builtins.insert("return", return_builtin());
320            env.options.set(Monitor, On);
321            stub_tty(&state);
322
323            let item = syntax::Item {
324                and_or: Rc::new("return  -n  42".parse().unwrap()),
325                async_flag: Some(Location::dummy("")),
326            };
327
328            _ = item.execute(&mut env).await;
329
330            let state = state.borrow();
331            let process = &state.processes[&env.jobs.last_async_pid()];
332            assert_ne!(process.pgid(), env.main_pgid);
333            assert!(env.jobs[0].job_controlled);
334        })
335    }
336
337    fn ignore_sigttin(env: &mut Env<Rc<Concurrent<VirtualSystem>>>) {
338        env.traps
339            .set_action(
340                &env.system,
341                VirtualSystem::SIGTTIN,
342                yash_env::trap::Action::Ignore,
343                Location::dummy(""),
344                false,
345            )
346            .now_or_never()
347            .unwrap()
348            .unwrap();
349    }
350
351    fn stub_dev_null_and_stdin(state: &RefCell<SystemState>) {
352        let mut state = state.borrow_mut();
353        state
354            .file_system
355            .save("/dev/null", Rc::new(RefCell::new(Inode::new([]))))
356            .unwrap();
357        state
358            .file_system
359            .get("/dev/stdin")
360            .unwrap()
361            .borrow_mut()
362            .body = FileBody::new(*b"input\n");
363    }
364
365    #[test]
366    fn item_execute_async_stdin_not_job_controlled() {
367        in_virtual_system(|mut env, state| async move {
368            env.builtins.insert("cat", cat_builtin());
369            ignore_sigttin(&mut env);
370            stub_tty(&state);
371            stub_dev_null_and_stdin(&state);
372
373            let item = syntax::Item {
374                and_or: Rc::new("cat".parse().unwrap()),
375                async_flag: Some(Location::dummy("")),
376            };
377
378            _ = item.execute(&mut env).await;
379            env.wait_for_subshell(env.jobs.last_async_pid())
380                .await
381                .unwrap();
382            assert_stdout(&state, |stdout| assert_eq!(stdout, ""));
383        })
384    }
385
386    #[test]
387    fn item_execute_async_stdin_job_controlled() {
388        in_virtual_system(|mut env, state| async move {
389            env.builtins.insert("cat", cat_builtin());
390            env.options.set(Monitor, On);
391            ignore_sigttin(&mut env);
392            stub_tty(&state);
393            stub_dev_null_and_stdin(&state);
394
395            let item = syntax::Item {
396                and_or: Rc::new("cat".parse().unwrap()),
397                async_flag: Some(Location::dummy("")),
398            };
399
400            _ = item.execute(&mut env).await;
401            env.wait_for_subshell(env.jobs.last_async_pid())
402                .await
403                .unwrap();
404            assert_stdout(&state, |stdout| assert_eq!(stdout, "input\n"));
405        })
406    }
407}