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