yash_env/
subshell.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//! Utility for starting subshells
18//!
19//! This module defines [`Subshell`], a builder for starting a subshell. It is
20//! [constructed](Subshell::new) with a function you want to run in a subshell.
21//! After configuring the builder with some options, you can
22//! [start](Subshell::start) the subshell.
23//!
24//! [`Subshell`] is implemented as a wrapper around
25//! [`System::new_child_process`]. You should prefer `Subshell` for the purpose
26//! of creating a subshell because it helps to arrange the child process
27//! properly.
28
29use crate::job::Pid;
30use crate::job::ProcessResult;
31use crate::job::ProcessState;
32use crate::signal;
33use crate::stack::Frame;
34use crate::system::ChildProcessTask;
35use crate::system::Errno;
36use crate::system::SigmaskOp;
37use crate::system::System;
38use crate::system::SystemEx;
39use crate::Env;
40use std::future::Future;
41use std::pin::Pin;
42
43/// Job state of a newly created subshell
44#[derive(Clone, Copy, Debug, Eq, PartialEq)]
45pub enum JobControl {
46    /// The subshell becomes the foreground process group.
47    Foreground,
48    /// The subshell becomes a background process group.
49    Background,
50}
51
52/// Subshell builder
53///
54/// See the [module documentation](self) for details.
55#[must_use = "a subshell is not started unless you call `Subshell::start`"]
56pub struct Subshell<F> {
57    task: F,
58    job_control: Option<JobControl>,
59    ignores_sigint_sigquit: bool,
60}
61
62impl<F> std::fmt::Debug for Subshell<F> {
63    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
64        f.debug_struct("Subshell").finish_non_exhaustive()
65    }
66}
67
68impl<F> Subshell<F>
69where
70    F: for<'a> FnOnce(&'a mut Env, Option<JobControl>) -> Pin<Box<dyn Future<Output = ()> + 'a>>
71        + 'static,
72    // TODO Revisit to simplify this function type when impl Future is allowed in return type
73{
74    /// Creates a new subshell builder with a task.
75    ///
76    /// The task will run in a subshell after it is started. The task takes two
77    /// arguments:
78    ///
79    /// 1. The environment in which the subshell runs, and
80    /// 2. Job control status for the subshell.
81    ///
82    /// If the task returns an `Err(Divert::...)`, it is handled as follows:
83    ///
84    /// - `Interrupt` and `Exit` with `Some(exit_status)` override the exit
85    ///   status in `Env`.
86    /// - Other `Divert` values are ignored.
87    pub fn new(task: F) -> Self {
88        Subshell {
89            task,
90            job_control: None,
91            ignores_sigint_sigquit: false,
92        }
93    }
94
95    /// Specifies disposition of the subshell with respect to job control.
96    ///
97    /// If the argument is `None` (which is the default), the subshell runs in
98    /// the same process group as the parent process. If it is `Some(_)`, the
99    /// subshell becomes a new process group. For `JobControl::Foreground`, it
100    /// also brings itself to the foreground.
101    ///
102    /// This parameter is ignored if the shell is not [controlling
103    /// jobs](Env::controls_jobs) when starting the subshell. You can tell the
104    /// actual job control status of the subshell by the second return value of
105    /// [`start`](Self::start) in the parent environment and the second argument
106    /// passed to the task in the subshell environment.
107    ///
108    /// If the parent process is a job-controlling interactive shell, but the
109    /// subshell is not job-controlled, the subshell's signal dispositions for
110    /// SIGTSTP, SIGTTIN, and SIGTTOU are set to `Ignore`. This is to prevent
111    /// the subshell from being stopped by a job-stopping signal. Were the
112    /// subshell stopped, you could never resume it since it is not
113    /// job-controlled.
114    pub fn job_control<J: Into<Option<JobControl>>>(mut self, job_control: J) -> Self {
115        self.job_control = job_control.into();
116        self
117    }
118
119    /// Makes the subshell ignore SIGINT and SIGQUIT.
120    ///
121    /// If `ignore` is true and the subshell is not job-controlled, the subshell
122    /// sets its signal dispositions for SIGINT and SIGQUIT to `Ignore`.
123    ///
124    /// The default is `false`.
125    pub fn ignore_sigint_sigquit(mut self, ignore: bool) -> Self {
126        self.ignores_sigint_sigquit = ignore;
127        self
128    }
129
130    /// Starts the subshell.
131    ///
132    /// This function creates a new child process that runs the task contained
133    /// in this builder.
134    ///
135    /// Although this function is `async`, it does not wait for the child to
136    /// finish, which means the parent and child processes will run
137    /// concurrently. To wait for the child to finish, you need to call
138    /// [`Env::wait_for_subshell`] or [`Env::wait_for_subshell_to_finish`]. If
139    /// job control is active, you may want to add the process ID to `env.jobs`
140    /// before waiting.
141    ///
142    /// If you set [`job_control`](Self::job_control) to
143    /// `JobControl::Foreground`, this function opens `env.tty` by calling
144    /// [`Env::get_tty`]. The `tty` is used to change the foreground job to the
145    /// new subshell. However, `job_control` is effective only when the shell is
146    /// [controlling jobs](Env::controls_jobs).
147    ///
148    /// If the subshell started successfully, the return value is a pair of the
149    /// child process ID and the actual job control. Otherwise, it indicates the
150    /// error.
151    pub async fn start(self, env: &mut Env) -> Result<(Pid, Option<JobControl>), Errno> {
152        // Do some preparation before starting a child process
153        let job_control = env.controls_jobs().then_some(self.job_control).flatten();
154        let tty = match job_control {
155            None | Some(JobControl::Background) => None,
156            // Open the tty in the parent process so we can reuse the FD for other jobs
157            Some(JobControl::Foreground) => Some(env.get_tty()?),
158        };
159        // Block SIGINT and SIGQUIT before forking the child process to prevent
160        // the child from being killed by those signals until the child starts
161        // ignoring them.
162        let mut mask_guard = MaskGuard::new(env);
163        let ignore_sigint_sigquit = self.ignores_sigint_sigquit
164            && job_control.is_none()
165            && mask_guard.block_sigint_sigquit();
166        let keep_internal_dispositions_for_stoppers = job_control.is_none();
167
168        // Define the child process task
169        const ME: Pid = Pid(0);
170        let task: ChildProcessTask = Box::new(move |env| {
171            Box::pin(async move {
172                let mut env = env.push_frame(Frame::Subshell);
173                let env = &mut *env;
174
175                if let Some(job_control) = job_control {
176                    if let Ok(()) = env.system.setpgid(ME, ME) {
177                        match job_control {
178                            JobControl::Background => (),
179                            JobControl::Foreground => {
180                                if let Some(tty) = tty {
181                                    let pgid = env.system.getpgrp();
182                                    let _ = env.system.tcsetpgrp_with_block(tty, pgid);
183                                }
184                            }
185                        }
186                    }
187                }
188                env.jobs.disown_all();
189
190                env.traps.enter_subshell(
191                    &mut env.system,
192                    ignore_sigint_sigquit,
193                    keep_internal_dispositions_for_stoppers,
194                );
195
196                (self.task)(env, job_control).await
197            })
198        });
199
200        // Start the child
201        let child = mask_guard.env.system.new_child_process()?;
202        let child_pid = child(mask_guard.env, task);
203
204        // The finishing
205        if job_control.is_some() {
206            // We should setpgid not only in the child but also in the parent to
207            // make sure the child is in a new process group before the parent
208            // returns from the start function.
209            let _ = mask_guard.env.system.setpgid(child_pid, ME);
210
211            // We don't tcsetpgrp in the parent. It would mess up the child
212            // which may have started another shell doing its own job control.
213        }
214
215        Ok((child_pid, job_control))
216    }
217
218    /// Starts the subshell and waits for it to finish.
219    ///
220    /// This function [starts](Self::start) `self` and
221    /// [waits](Env::wait_for_subshell) for it to finish. This function returns
222    /// when the subshell process exits or is killed by a signal. If the
223    /// subshell is job-controlled, the function also returns when the job is
224    /// suspended.
225    ///
226    /// If the subshell started successfully, the return value is the process ID
227    /// and the process result of the subshell. If there was an error starting
228    /// the subshell, this function returns the error.
229    ///
230    /// If you set [`job_control`](Self::job_control) to
231    /// `JobControl::Foreground` and job control is effective as per
232    /// [`Env::controls_jobs`], this function makes the shell the foreground job
233    /// after the subshell terminated or suspended.
234    ///
235    /// When a job-controlled subshell suspends, this function does not add it
236    /// to `env.jobs`. You have to do it for yourself if necessary.
237    pub async fn start_and_wait(self, env: &mut Env) -> Result<(Pid, ProcessResult), Errno> {
238        let (pid, job_control) = self.start(env).await?;
239        let result = loop {
240            let state = env.wait_for_subshell(pid).await?.1;
241            if let ProcessState::Halted(result) = state {
242                if !result.is_stopped() || job_control.is_some() {
243                    break result;
244                }
245            }
246        };
247
248        if job_control == Some(JobControl::Foreground) {
249            if let Some(tty) = env.tty {
250                env.system.tcsetpgrp_with_block(tty, env.main_pgid).ok();
251            }
252        }
253
254        Ok((pid, result))
255    }
256}
257
258/// Guard object for temporarily blocking signals
259///
260/// This object blocks SIGINT and SIGQUIT and remembers the previous signal
261/// blocking mask, which is restored when the object is dropped.
262#[derive(Debug)]
263struct MaskGuard<'a> {
264    env: &'a mut Env,
265    old_mask: Option<Vec<signal::Number>>,
266}
267
268impl<'a> MaskGuard<'a> {
269    fn new(env: &'a mut Env) -> Self {
270        let old_mask = None;
271        Self { env, old_mask }
272    }
273
274    fn block_sigint_sigquit(&mut self) -> bool {
275        assert_eq!(self.old_mask, None);
276
277        let Some(sigint) = self.env.system.signal_number_from_name(signal::Name::Int) else {
278            return false;
279        };
280        let Some(sigquit) = self.env.system.signal_number_from_name(signal::Name::Quit) else {
281            return false;
282        };
283
284        let mut old_mask = Vec::new();
285
286        let success = self
287            .env
288            .system
289            .sigmask(
290                Some((SigmaskOp::Add, &[sigint, sigquit])),
291                Some(&mut old_mask),
292            )
293            .is_ok();
294        if success {
295            self.old_mask = Some(old_mask);
296        }
297        success
298    }
299}
300
301impl Drop for MaskGuard<'_> {
302    fn drop(&mut self) {
303        if let Some(old_mask) = &self.old_mask {
304            self.env
305                .system
306                .sigmask(Some((SigmaskOp::Set, old_mask)), None)
307                .ok();
308        }
309    }
310}
311
312#[cfg(test)]
313mod tests {
314    use super::*;
315    use crate::job::Job;
316    use crate::option::Option::{Interactive, Monitor};
317    use crate::option::State::On;
318    use crate::semantics::ExitStatus;
319    use crate::system::r#virtual::Inode;
320    use crate::system::r#virtual::SystemState;
321    use crate::system::r#virtual::{SIGCHLD, SIGINT, SIGQUIT, SIGTSTP, SIGTTIN, SIGTTOU};
322    use crate::system::Disposition;
323    use crate::system::Errno;
324    use crate::tests::in_virtual_system;
325    use crate::trap::Action;
326    use assert_matches::assert_matches;
327    use futures_executor::LocalPool;
328    use std::cell::Cell;
329    use std::cell::RefCell;
330    use std::rc::Rc;
331    use yash_syntax::source::Location;
332
333    fn stub_tty(state: &RefCell<SystemState>) {
334        state
335            .borrow_mut()
336            .file_system
337            .save("/dev/tty", Rc::new(RefCell::new(Inode::new([]))))
338            .unwrap();
339    }
340
341    #[test]
342    fn subshell_start_returns_child_process_id() {
343        in_virtual_system(|mut env, _state| async move {
344            let parent_pid = env.main_pid;
345            let child_pid = Rc::new(Cell::new(None));
346            let child_pid_2 = Rc::clone(&child_pid);
347            let subshell = Subshell::new(move |env, _job_control| {
348                Box::pin(async move {
349                    child_pid_2.set(Some(env.system.getpid()));
350                    assert_eq!(env.system.getppid(), parent_pid);
351                })
352            });
353            let result = subshell.start(&mut env).await.unwrap().0;
354            env.wait_for_subshell(result).await.unwrap();
355            assert_eq!(Some(result), child_pid.get());
356        });
357    }
358
359    #[test]
360    fn subshell_start_failing() {
361        let mut executor = LocalPool::new();
362        let env = &mut Env::new_virtual();
363        let subshell =
364            Subshell::new(|_env, _job_control| unreachable!("subshell not expected to run"));
365        let result = executor.run_until(subshell.start(env));
366        assert_eq!(result, Err(Errno::ENOSYS));
367    }
368
369    #[test]
370    fn stack_frame_in_subshell() {
371        in_virtual_system(|mut env, _state| async move {
372            let subshell = Subshell::new(|env, _job_control| {
373                Box::pin(async { assert_eq!(env.stack[..], [Frame::Subshell]) })
374            });
375            let pid = subshell.start(&mut env).await.unwrap().0;
376            assert_eq!(env.stack[..], []);
377
378            env.wait_for_subshell(pid).await.unwrap();
379        });
380    }
381
382    #[test]
383    fn jobs_disowned_in_subshell() {
384        in_virtual_system(|mut env, _state| async move {
385            let index = env.jobs.add(Job::new(Pid(123)));
386            let subshell = Subshell::new(move |env, _job_control| {
387                Box::pin(async move { assert!(!env.jobs[index].is_owned) })
388            });
389            let pid = subshell.start(&mut env).await.unwrap().0;
390            env.wait_for_subshell(pid).await.unwrap();
391
392            assert!(env.jobs[index].is_owned);
393        });
394    }
395
396    #[test]
397    fn trap_reset_in_subshell() {
398        in_virtual_system(|mut env, _state| async move {
399            env.traps
400                .set_action(
401                    &mut env.system,
402                    SIGCHLD,
403                    Action::Command("echo foo".into()),
404                    Location::dummy(""),
405                    false,
406                )
407                .unwrap();
408            let subshell = Subshell::new(|env, _job_control| {
409                Box::pin(async {
410                    let trap_state = assert_matches!(
411                        env.traps.get_state(SIGCHLD),
412                        (None, Some(trap_state)) => trap_state
413                    );
414                    assert_matches!(
415                        &trap_state.action,
416                        Action::Command(body) => assert_eq!(&**body, "echo foo")
417                    );
418                })
419            });
420            let pid = subshell.start(&mut env).await.unwrap().0;
421            env.wait_for_subshell(pid).await.unwrap();
422        });
423    }
424
425    #[test]
426    fn subshell_with_no_job_control() {
427        in_virtual_system(|mut parent_env, state| async move {
428            parent_env.options.set(Monitor, On);
429
430            let parent_pgid = state.borrow().processes[&parent_env.main_pid].pgid;
431            let state_2 = Rc::clone(&state);
432            let (child_pid, job_control) = Subshell::new(move |child_env, job_control| {
433                Box::pin(async move {
434                    let child_pid = child_env.system.getpid();
435                    assert_eq!(state_2.borrow().processes[&child_pid].pgid, parent_pgid);
436                    assert_eq!(state_2.borrow().foreground, None);
437                    assert_eq!(job_control, None);
438                })
439            })
440            .job_control(None)
441            .start(&mut parent_env)
442            .await
443            .unwrap();
444            assert_eq!(job_control, None);
445            assert_eq!(state.borrow().processes[&child_pid].pgid, parent_pgid);
446            assert_eq!(state.borrow().foreground, None);
447
448            parent_env.wait_for_subshell(child_pid).await.unwrap();
449            assert_eq!(state.borrow().processes[&child_pid].pgid, parent_pgid);
450            assert_eq!(state.borrow().foreground, None);
451        });
452    }
453
454    #[test]
455    fn subshell_in_background() {
456        in_virtual_system(|mut parent_env, state| async move {
457            parent_env.options.set(Monitor, On);
458
459            let state_2 = Rc::clone(&state);
460            let (child_pid, job_control) = Subshell::new(move |child_env, job_control| {
461                Box::pin(async move {
462                    let child_pid = child_env.system.getpid();
463                    assert_eq!(state_2.borrow().processes[&child_pid].pgid, child_pid);
464                    assert_eq!(state_2.borrow().foreground, None);
465                    assert_eq!(job_control, Some(JobControl::Background));
466                })
467            })
468            .job_control(JobControl::Background)
469            .start(&mut parent_env)
470            .await
471            .unwrap();
472            assert_eq!(job_control, Some(JobControl::Background));
473            assert_eq!(state.borrow().processes[&child_pid].pgid, child_pid);
474            assert_eq!(state.borrow().foreground, None);
475
476            parent_env.wait_for_subshell(child_pid).await.unwrap();
477            assert_eq!(state.borrow().processes[&child_pid].pgid, child_pid);
478            assert_eq!(state.borrow().foreground, None);
479        });
480    }
481
482    #[test]
483    fn subshell_in_foreground() {
484        in_virtual_system(|mut parent_env, state| async move {
485            parent_env.options.set(Monitor, On);
486            stub_tty(&state);
487
488            let state_2 = Rc::clone(&state);
489            let (child_pid, job_control) = Subshell::new(move |child_env, job_control| {
490                Box::pin(async move {
491                    let child_pid = child_env.system.getpid();
492                    assert_eq!(state_2.borrow().processes[&child_pid].pgid, child_pid);
493                    assert_eq!(state_2.borrow().foreground, Some(child_pid));
494                    assert_eq!(job_control, Some(JobControl::Foreground));
495                })
496            })
497            .job_control(JobControl::Foreground)
498            .start(&mut parent_env)
499            .await
500            .unwrap();
501            assert_eq!(job_control, Some(JobControl::Foreground));
502            assert_eq!(state.borrow().processes[&child_pid].pgid, child_pid);
503            // The child may not yet have become the foreground job.
504            // assert_eq!(state.borrow().foreground, Some(child_pid));
505
506            parent_env.wait_for_subshell(child_pid).await.unwrap();
507            assert_eq!(state.borrow().processes[&child_pid].pgid, child_pid);
508            assert_eq!(state.borrow().foreground, Some(child_pid));
509        });
510    }
511
512    #[test]
513    fn tty_after_starting_foreground_subshell() {
514        in_virtual_system(|mut parent_env, state| async move {
515            parent_env.options.set(Monitor, On);
516            stub_tty(&state);
517
518            let _ = Subshell::new(move |_, _| Box::pin(std::future::ready(())))
519                .job_control(JobControl::Foreground)
520                .start(&mut parent_env)
521                .await
522                .unwrap();
523            assert_matches!(parent_env.tty, Some(_));
524        });
525    }
526
527    #[test]
528    fn no_job_control_with_option_disabled() {
529        in_virtual_system(|mut parent_env, state| async move {
530            stub_tty(&state);
531
532            let parent_pgid = state.borrow().processes[&parent_env.main_pid].pgid;
533            let state_2 = Rc::clone(&state);
534            let (child_pid, job_control) = Subshell::new(move |child_env, _job_control| {
535                Box::pin(async move {
536                    let child_pid = child_env.system.getpid();
537                    assert_eq!(state_2.borrow().processes[&child_pid].pgid, parent_pgid);
538                    assert_eq!(state_2.borrow().foreground, None);
539                })
540            })
541            .job_control(JobControl::Foreground)
542            .start(&mut parent_env)
543            .await
544            .unwrap();
545            assert_eq!(job_control, None);
546            assert_eq!(state.borrow().processes[&child_pid].pgid, parent_pgid);
547            assert_eq!(state.borrow().foreground, None);
548
549            parent_env.wait_for_subshell(child_pid).await.unwrap();
550            assert_eq!(state.borrow().processes[&child_pid].pgid, parent_pgid);
551            assert_eq!(state.borrow().foreground, None);
552        });
553    }
554
555    #[test]
556    fn no_job_control_for_nested_subshell() {
557        in_virtual_system(|mut parent_env, state| async move {
558            let mut parent_env = parent_env.push_frame(Frame::Subshell);
559            parent_env.options.set(Monitor, On);
560            stub_tty(&state);
561
562            let parent_pgid = state.borrow().processes[&parent_env.main_pid].pgid;
563            let state_2 = Rc::clone(&state);
564            let (child_pid, job_control) = Subshell::new(move |child_env, _job_control| {
565                Box::pin(async move {
566                    let child_pid = child_env.system.getpid();
567                    assert_eq!(state_2.borrow().processes[&child_pid].pgid, parent_pgid);
568                    assert_eq!(state_2.borrow().foreground, None);
569                })
570            })
571            .job_control(JobControl::Foreground)
572            .start(&mut parent_env)
573            .await
574            .unwrap();
575            assert_eq!(job_control, None);
576            assert_eq!(state.borrow().processes[&child_pid].pgid, parent_pgid);
577            assert_eq!(state.borrow().foreground, None);
578
579            parent_env.wait_for_subshell(child_pid).await.unwrap();
580            assert_eq!(state.borrow().processes[&child_pid].pgid, parent_pgid);
581            assert_eq!(state.borrow().foreground, None);
582        });
583    }
584
585    #[test]
586    fn wait_without_job_control() {
587        in_virtual_system(|mut env, _state| async move {
588            let subshell = Subshell::new(|env, _job_control| {
589                Box::pin(async { env.exit_status = ExitStatus(42) })
590            });
591            let (_pid, process_result) = subshell.start_and_wait(&mut env).await.unwrap();
592            assert_eq!(process_result, ProcessResult::exited(42));
593        });
594    }
595
596    #[test]
597    fn wait_for_foreground_job_to_exit() {
598        in_virtual_system(|mut env, state| async move {
599            env.options.set(Monitor, On);
600            stub_tty(&state);
601
602            let subshell = Subshell::new(|env, _job_control| {
603                Box::pin(async { env.exit_status = ExitStatus(123) })
604            })
605            .job_control(JobControl::Foreground);
606            let (_pid, process_result) = subshell.start_and_wait(&mut env).await.unwrap();
607            assert_eq!(process_result, ProcessResult::exited(123));
608            assert_eq!(state.borrow().foreground, Some(env.main_pgid));
609        });
610    }
611
612    // TODO wait_for_foreground_job_to_be_signaled
613    // TODO wait_for_foreground_job_to_be_stopped
614
615    #[test]
616    fn sigint_sigquit_not_ignored_by_default() {
617        in_virtual_system(|mut parent_env, state| async move {
618            let (child_pid, _) = Subshell::new(|env, _job_control| {
619                Box::pin(async { env.exit_status = ExitStatus(123) })
620            })
621            .job_control(JobControl::Background)
622            .start(&mut parent_env)
623            .await
624            .unwrap();
625            parent_env.wait_for_subshell(child_pid).await.unwrap();
626
627            let state = state.borrow();
628            let process = &state.processes[&child_pid];
629            assert_eq!(process.disposition(SIGINT), Disposition::Default);
630            assert_eq!(process.disposition(SIGQUIT), Disposition::Default);
631        })
632    }
633
634    #[test]
635    fn sigint_sigquit_ignored_in_uncontrolled_job() {
636        in_virtual_system(|mut parent_env, state| async move {
637            let (child_pid, _) = Subshell::new(|env, _job_control| {
638                Box::pin(async { env.exit_status = ExitStatus(123) })
639            })
640            .job_control(JobControl::Background)
641            .ignore_sigint_sigquit(true)
642            .start(&mut parent_env)
643            .await
644            .unwrap();
645
646            parent_env
647                .system
648                .kill(child_pid, Some(SIGINT))
649                .await
650                .unwrap();
651            parent_env
652                .system
653                .kill(child_pid, Some(SIGQUIT))
654                .await
655                .unwrap();
656
657            let child_result = parent_env.wait_for_subshell(child_pid).await.unwrap();
658            assert_eq!(child_result, (child_pid, ProcessState::exited(123)));
659
660            let state = state.borrow();
661            let parent_process = &state.processes[&parent_env.main_pid];
662            assert!(!parent_process.blocked_signals().contains(&SIGINT));
663            assert!(!parent_process.blocked_signals().contains(&SIGQUIT));
664            let child_process = &state.processes[&child_pid];
665            assert_eq!(child_process.disposition(SIGINT), Disposition::Ignore);
666            assert_eq!(child_process.disposition(SIGQUIT), Disposition::Ignore);
667        })
668    }
669
670    #[test]
671    fn sigint_sigquit_not_ignored_if_job_controlled() {
672        in_virtual_system(|mut parent_env, state| async move {
673            parent_env.options.set(Monitor, On);
674            stub_tty(&state);
675
676            let (child_pid, _) = Subshell::new(|env, _job_control| {
677                Box::pin(async { env.exit_status = ExitStatus(123) })
678            })
679            .job_control(JobControl::Background)
680            .ignore_sigint_sigquit(true)
681            .start(&mut parent_env)
682            .await
683            .unwrap();
684            parent_env.wait_for_subshell(child_pid).await.unwrap();
685
686            let state = state.borrow();
687            let process = &state.processes[&child_pid];
688            assert_eq!(process.disposition(SIGINT), Disposition::Default);
689            assert_eq!(process.disposition(SIGQUIT), Disposition::Default);
690        })
691    }
692
693    #[test]
694    fn internal_dispositions_for_stoppers_kept_in_uncontrolled_subshell_of_controlling_interactive_shell(
695    ) {
696        in_virtual_system(|mut parent_env, state| async move {
697            parent_env.options.set(Interactive, On);
698            parent_env.options.set(Monitor, On);
699            parent_env
700                .traps
701                .enable_internal_dispositions_for_stoppers(&mut parent_env.system)
702                .unwrap();
703            stub_tty(&state);
704
705            let (child_pid, _) = Subshell::new(|env, _job_control| {
706                Box::pin(async { env.exit_status = ExitStatus(123) })
707            })
708            .start(&mut parent_env)
709            .await
710            .unwrap();
711
712            let child_result = parent_env.wait_for_subshell(child_pid).await.unwrap();
713            assert_eq!(child_result, (child_pid, ProcessState::exited(123)));
714
715            let state = state.borrow();
716            let child_process = &state.processes[&child_pid];
717            assert_eq!(child_process.disposition(SIGTSTP), Disposition::Ignore);
718            assert_eq!(child_process.disposition(SIGTTIN), Disposition::Ignore);
719            assert_eq!(child_process.disposition(SIGTTOU), Disposition::Ignore);
720        })
721    }
722
723    #[test]
724    fn internal_dispositions_for_stoppers_reset_in_controlled_subshell_of_interactive_shell() {
725        in_virtual_system(|mut parent_env, state| async move {
726            parent_env.options.set(Interactive, On);
727            parent_env.options.set(Monitor, On);
728            parent_env
729                .traps
730                .enable_internal_dispositions_for_stoppers(&mut parent_env.system)
731                .unwrap();
732            stub_tty(&state);
733
734            let (child_pid, _) = Subshell::new(|env, _job_control| {
735                Box::pin(async { env.exit_status = ExitStatus(123) })
736            })
737            .job_control(JobControl::Background)
738            .start(&mut parent_env)
739            .await
740            .unwrap();
741
742            let child_result = parent_env.wait_for_subshell(child_pid).await.unwrap();
743            assert_eq!(child_result, (child_pid, ProcessState::exited(123)));
744
745            let state = state.borrow();
746            let child_process = &state.processes[&child_pid];
747            assert_eq!(child_process.disposition(SIGTSTP), Disposition::Default);
748            assert_eq!(child_process.disposition(SIGTTIN), Disposition::Default);
749            assert_eq!(child_process.disposition(SIGTTOU), Disposition::Default);
750        })
751    }
752
753    #[test]
754    fn internal_dispositions_for_stoppers_unset_in_subshell_of_non_controlling_interactive_shell() {
755        in_virtual_system(|mut parent_env, state| async move {
756            parent_env.options.set(Interactive, On);
757            stub_tty(&state);
758
759            let (child_pid, _) = Subshell::new(|env, _job_control| {
760                Box::pin(async { env.exit_status = ExitStatus(123) })
761            })
762            .start(&mut parent_env)
763            .await
764            .unwrap();
765
766            let child_result = parent_env.wait_for_subshell(child_pid).await.unwrap();
767            assert_eq!(child_result, (child_pid, ProcessState::exited(123)));
768
769            let state = state.borrow();
770            let child_process = &state.processes[&child_pid];
771            assert_eq!(child_process.disposition(SIGTSTP), Disposition::Default);
772            assert_eq!(child_process.disposition(SIGTTIN), Disposition::Default);
773            assert_eq!(child_process.disposition(SIGTTOU), Disposition::Default);
774        })
775    }
776
777    #[test]
778    fn internal_dispositions_for_stoppers_unset_in_uncontrolled_subshell_of_controlling_non_interactive_shell(
779    ) {
780        in_virtual_system(|mut parent_env, state| async move {
781            parent_env.options.set(Monitor, On);
782            stub_tty(&state);
783
784            let (child_pid, _) = Subshell::new(|env, _job_control| {
785                Box::pin(async { env.exit_status = ExitStatus(123) })
786            })
787            .start(&mut parent_env)
788            .await
789            .unwrap();
790
791            let child_result = parent_env.wait_for_subshell(child_pid).await.unwrap();
792            assert_eq!(child_result, (child_pid, ProcessState::exited(123)));
793
794            let state = state.borrow();
795            let child_process = &state.processes[&child_pid];
796            assert_eq!(child_process.disposition(SIGTSTP), Disposition::Default);
797            assert_eq!(child_process.disposition(SIGTTIN), Disposition::Default);
798            assert_eq!(child_process.disposition(SIGTTOU), Disposition::Default);
799        })
800    }
801}