yash_env/trap/
state.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//! Items that manage the state of a single signal
18
19#[cfg(doc)]
20use super::TrapSet;
21use super::{Condition, SignalSystem};
22use crate::source::Location;
23use crate::system::{Disposition, Errno};
24use std::collections::btree_map::{Entry, VacantEntry};
25use std::rc::Rc;
26use thiserror::Error;
27
28/// Action performed when a [`Condition`] is met
29#[derive(Clone, Debug, Default, Eq, Hash, PartialEq)]
30pub enum Action {
31    /// Performs the default action.
32    ///
33    /// For signal conditions, the behavior depends on the signal delivered.
34    /// For other conditions, this is equivalent to `Ignore`.
35    #[default]
36    Default,
37
38    /// Pretends as if the condition was not met.
39    Ignore,
40
41    /// Executes a command string.
42    Command(Rc<str>),
43}
44
45impl From<&Action> for Disposition {
46    fn from(trap: &Action) -> Self {
47        match trap {
48            Action::Default => Disposition::Default,
49            Action::Ignore => Disposition::Ignore,
50            Action::Command(_) => Disposition::Catch,
51        }
52    }
53}
54
55/// Error that may happen in [`TrapSet::set_action`]
56#[derive(Clone, Copy, Debug, Eq, Error, PartialEq)]
57pub enum SetActionError {
58    /// Attempt to set a trap that has been ignored since the shell startup.
59    #[error("the signal has been ignored since startup")]
60    InitiallyIgnored,
61
62    /// Attempt to set a trap for the `SIGKILL` signal.
63    #[error("cannot set a trap for SIGKILL")]
64    SIGKILL,
65
66    /// Attempt to set a trap for the `SIGSTOP` signal.
67    #[error("cannot set a trap for SIGSTOP")]
68    SIGSTOP,
69
70    /// Error from the underlying system interface.
71    #[error(transparent)]
72    SystemError(#[from] Errno),
73}
74
75/// Origin of the current trap action
76///
77/// The `Origin` enum indicates how the current trap action was set.
78#[derive(Clone, Debug, Default, Eq, PartialEq)]
79pub enum Origin {
80    /// The current trap action was inherited from the previous process that
81    /// `exec`ed the shell.
82    ///
83    /// This is the default value.
84    #[default]
85    Inherited,
86
87    /// The current trap action was set by the shell when entering a subshell.
88    Subshell,
89
90    /// The current trap action was set by the user.
91    ///
92    /// The location indicates the simple command that invoked the trap built-in
93    /// that set the current action.
94    User(Location),
95}
96
97/// State of the trap action for a condition
98#[derive(Clone, Debug, Default, Eq, PartialEq)]
99pub struct TrapState {
100    /// Action taken when the condition is met
101    pub action: Action,
102
103    /// Origin of the current action
104    pub origin: Origin,
105
106    /// True iff a signal specified by the condition has been caught and the
107    /// action command has not yet executed.
108    pub pending: bool,
109}
110
111impl TrapState {
112    fn from_initial_disposition(disposition: Disposition) -> Self {
113        let action = match disposition {
114            Disposition::Default => Action::Default,
115            Disposition::Ignore => Action::Ignore,
116            // The Rust runtime installs a handler for SIGSEGV and SIGBUS before
117            // the shell is initialized. We should treat these signals as if
118            // they have the default disposition.
119            // Disposition::Catch => panic!("initial disposition cannot be `Catch`"),
120            Disposition::Catch => Action::Default,
121        };
122        TrapState {
123            action,
124            origin: Origin::Inherited,
125            pending: false,
126        }
127    }
128}
129
130/// Option for [`GrandState::enter_subshell`]
131#[derive(Clone, Copy, Debug, Eq, PartialEq)]
132pub enum EnterSubshellOption {
133    /// Keeps the current internal disposition configuration.
134    KeepInternalDisposition,
135    /// Resets the internal disposition configuration to the default.
136    ClearInternalDisposition,
137    /// Resets the internal disposition configuration to the default and sets the
138    /// signal disposition to `Ignore`.
139    Ignore,
140}
141
142/// Whole configuration and state for a trap condition
143#[derive(Clone, Debug)]
144pub struct GrandState {
145    /// Current trap state
146    current_state: TrapState,
147
148    /// Trap state that was effective in the parent environment
149    parent_state: Option<TrapState>,
150
151    /// Current internal disposition
152    ///
153    /// The internal disposition is the signal disposition that is set by the
154    /// shell itself, not by the user. This is used to handle some specific
155    /// signals like `SIGCHLD` and `SIGTSTP`. When the user sets a trap for
156    /// these signals, the actual signal disposition registered in the system
157    /// is computed as the maximum of the user-defined disposition and the
158    /// internal disposition.
159    internal_disposition: Disposition,
160}
161
162impl GrandState {
163    /// Returns the current trap state.
164    #[inline]
165    #[must_use]
166    pub fn current_state(&self) -> &TrapState {
167        &self.current_state
168    }
169
170    /// Returns the parent trap state.
171    ///
172    /// This is the trap state that was effective in the parent environment
173    /// before the current environment was created as a subshell of the parent.
174    /// Returns `None` if the current environment is not a subshell or the parent
175    /// state was [cleared](Self::clear_parent_state).
176    #[inline]
177    #[must_use]
178    pub fn parent_state(&self) -> Option<&TrapState> {
179        self.parent_state.as_ref()
180    }
181
182    /// Clears the parent trap state.
183    pub fn clear_parent_state(&mut self) {
184        self.parent_state = None;
185    }
186
187    /// Inserts a new entry if the entry is vacant.
188    ///
189    /// If the condition is a signal, the new entry is initialized with the
190    /// current signal disposition obtained from the system.
191    pub fn insert_from_system_if_vacant<'a, S: SignalSystem>(
192        system: &S,
193        entry: Entry<'a, Condition, GrandState>,
194    ) -> Result<&'a GrandState, Errno> {
195        match entry {
196            Entry::Vacant(vacant) => {
197                let disposition = match *vacant.key() {
198                    Condition::Signal(signal) => system.get_disposition(signal)?,
199                    Condition::Exit => Disposition::Default,
200                };
201                let state = GrandState {
202                    current_state: TrapState::from_initial_disposition(disposition),
203                    parent_state: None,
204                    internal_disposition: Disposition::Default,
205                };
206                Ok(vacant.insert(state))
207            }
208
209            Entry::Occupied(occupied) => Ok(occupied.into_mut()),
210        }
211    }
212
213    /// Updates the entry with the new action.
214    pub fn set_action<S: SignalSystem>(
215        system: &mut S,
216        entry: Entry<Condition, GrandState>,
217        action: Action,
218        origin: Location,
219        override_ignore: bool,
220    ) -> Result<(), SetActionError> {
221        let cond = *entry.key();
222        let disposition = (&action).into();
223        let new_state = TrapState {
224            action,
225            origin: Origin::User(origin),
226            pending: false,
227        };
228
229        match entry {
230            Entry::Vacant(vacant) => {
231                if let Condition::Signal(signal) = cond {
232                    if !override_ignore {
233                        let initial_disposition =
234                            system.set_disposition(signal, Disposition::Ignore)?;
235                        if initial_disposition == Disposition::Ignore {
236                            vacant.insert(GrandState {
237                                current_state: TrapState::from_initial_disposition(
238                                    initial_disposition,
239                                ),
240                                parent_state: None,
241                                internal_disposition: Disposition::Default,
242                            });
243                            return Err(SetActionError::InitiallyIgnored);
244                        }
245                    }
246
247                    if override_ignore || disposition != Disposition::Ignore {
248                        system.set_disposition(signal, disposition)?;
249                    }
250                }
251
252                vacant.insert(GrandState {
253                    current_state: new_state,
254                    parent_state: None,
255                    internal_disposition: Disposition::Default,
256                });
257            }
258
259            Entry::Occupied(mut occupied) => {
260                let state = occupied.get_mut();
261                if !override_ignore
262                    && state.current_state.action == Action::Ignore
263                    && state.current_state.origin == Origin::Inherited
264                {
265                    return Err(SetActionError::InitiallyIgnored);
266                }
267
268                if let Condition::Signal(signal) = cond {
269                    let internal = state.internal_disposition;
270                    let old_disposition = internal.max((&state.current_state.action).into());
271                    let new_disposition = internal.max(disposition);
272                    if old_disposition != new_disposition {
273                        system.set_disposition(signal, new_disposition)?;
274                    }
275                }
276
277                state.current_state = new_state;
278            }
279        }
280
281        Ok(())
282    }
283
284    /// Returns the current internal disposition.
285    #[must_use]
286    pub fn internal_disposition(&self) -> Disposition {
287        self.internal_disposition
288    }
289
290    /// Sets the internal disposition.
291    ///
292    /// The condition of the given entry must be a signal, or this function
293    /// panics.
294    pub fn set_internal_disposition<S: SignalSystem>(
295        system: &mut S,
296        entry: Entry<Condition, GrandState>,
297        disposition: Disposition,
298    ) -> Result<(), Errno> {
299        let signal = match *entry.key() {
300            Condition::Signal(signal) => signal,
301            Condition::Exit => panic!("exit condition cannot have an internal disposition"),
302        };
303
304        match entry {
305            Entry::Vacant(_) if disposition == Disposition::Default => (),
306
307            Entry::Vacant(vacant) => {
308                let initial_disposition = system.set_disposition(signal, disposition)?;
309                vacant.insert(GrandState {
310                    current_state: TrapState::from_initial_disposition(initial_disposition),
311                    parent_state: None,
312                    internal_disposition: disposition,
313                });
314            }
315
316            Entry::Occupied(mut occupied) => {
317                let state = occupied.get_mut();
318                let setting = (&state.current_state.action).into();
319                let old_disposition = state.internal_disposition.max(setting);
320                let new_disposition = disposition.max(setting);
321                if old_disposition != new_disposition {
322                    system.set_disposition(signal, new_disposition)?;
323                }
324                state.internal_disposition = disposition;
325            }
326        }
327
328        Ok(())
329    }
330
331    /// Updates the trap states and gets ready for executing the body a
332    /// subshell.
333    ///
334    /// If the current state has a user-specified command
335    /// (`Action::Command(_)`), it is saved in the parent state and reset to the
336    /// default. Additionally, the signal disposition is updated depending on the
337    /// `option`.
338    pub fn enter_subshell<S: SignalSystem>(
339        &mut self,
340        system: &mut S,
341        cond: Condition,
342        option: EnterSubshellOption,
343    ) -> Result<(), Errno> {
344        let old_setting = (&self.current_state.action).into();
345        let old_disposition = self.internal_disposition.max(old_setting);
346
347        if matches!(self.current_state.action, Action::Command(_)) {
348            self.parent_state = Some(std::mem::replace(
349                &mut self.current_state,
350                TrapState {
351                    action: Action::Default,
352                    origin: Origin::Subshell,
353                    pending: false,
354                },
355            ));
356        }
357        if option == EnterSubshellOption::Ignore {
358            self.current_state.action = Action::Ignore;
359        }
360
361        let new_setting = (&self.current_state.action).into();
362        let new_disposition = match option {
363            EnterSubshellOption::KeepInternalDisposition => {
364                self.internal_disposition.max(new_setting)
365            }
366            EnterSubshellOption::ClearInternalDisposition => new_setting,
367            EnterSubshellOption::Ignore => Disposition::Ignore,
368        };
369        if old_disposition != new_disposition {
370            if let Condition::Signal(signal) = cond {
371                system.set_disposition(signal, new_disposition)?;
372            }
373        }
374        self.internal_disposition = match option {
375            EnterSubshellOption::KeepInternalDisposition => self.internal_disposition,
376            EnterSubshellOption::ClearInternalDisposition | EnterSubshellOption::Ignore => {
377                Disposition::Default
378            }
379        };
380        Ok(())
381    }
382
383    /// Sets the disposition to `Ignore` for the given signal condition.
384    ///
385    /// This function creates a new `GrandState` entry with the current state
386    /// having `Action::Ignore`. If the signal disposition is default, the shell
387    /// sets the signal disposition to `Ignore` and the origin to `Subshell`. If
388    /// the signal disposition is already `Ignore`, the origin is set to
389    /// `Inherited` to disallow changing the action in a non-interactive shell.
390    ///
391    /// You should call this function in place of [`Self::enter_subshell`] if
392    /// there is no entry for the condition yet.
393    ///
394    /// This function panics if the condition is not a signal.
395    pub fn ignore<S: SignalSystem>(
396        system: &mut S,
397        vacant: VacantEntry<Condition, GrandState>,
398    ) -> Result<(), Errno> {
399        let signal = match *vacant.key() {
400            Condition::Signal(signal) => signal,
401            Condition::Exit => panic!("exit condition cannot be ignored"),
402        };
403        let initial_disposition = system.set_disposition(signal, Disposition::Ignore)?;
404        let origin = match initial_disposition {
405            Disposition::Default => Origin::Subshell,
406            Disposition::Ignore => Origin::Inherited,
407            // The Rust runtime installs a handler for SIGSEGV and SIGBUS before
408            // the shell is initialized. We should treat these signals as if
409            // they have the default disposition (though the ignore function is
410            // normally not called for these signals).
411            // Disposition::Catch => panic!("initial disposition cannot be `Catch`"),
412            Disposition::Catch => Origin::Subshell,
413        };
414        vacant.insert(GrandState {
415            current_state: TrapState {
416                action: Action::Ignore,
417                origin,
418                pending: false,
419            },
420            parent_state: None,
421            internal_disposition: Disposition::Default,
422        });
423        Ok(())
424    }
425
426    /// Marks this signal as caught.
427    pub fn mark_as_caught(&mut self) {
428        self.current_state.pending = true;
429    }
430
431    /// Clears the mark of this signal being caught and returns the trap state.
432    pub fn handle_if_caught(&mut self) -> Option<&TrapState> {
433        if self.current_state.pending {
434            self.current_state.pending = false;
435            Some(&self.current_state)
436        } else {
437            None
438        }
439    }
440}
441
442#[cfg(test)]
443mod tests {
444    use super::super::tests::DummySystem;
445    use super::*;
446    use crate::system::r#virtual::{SIGCHLD, SIGQUIT, SIGTSTP, SIGTTOU, SIGUSR1};
447    use assert_matches::assert_matches;
448    use std::collections::BTreeMap;
449
450    struct UnusedSystem;
451
452    impl SignalSystem for UnusedSystem {
453        fn signal_name_from_number(&self, number: crate::signal::Number) -> crate::signal::Name {
454            unreachable!("signal_name_from_number({number})")
455        }
456        fn signal_number_from_name(
457            &self,
458            name: crate::signal::Name,
459        ) -> Option<crate::signal::Number> {
460            unreachable!("signal_number_from_name({name})")
461        }
462        fn get_disposition(&self, signal: crate::signal::Number) -> Result<Disposition, Errno> {
463            unreachable!("get_disposition({signal})")
464        }
465        fn set_disposition(
466            &mut self,
467            signal: crate::signal::Number,
468            disposition: Disposition,
469        ) -> Result<Disposition, Errno> {
470            unreachable!("set_disposition({signal}, {disposition:?})")
471        }
472    }
473
474    #[test]
475    fn insertion_with_default_inherited_disposition() {
476        let system = DummySystem::default();
477        let mut map = BTreeMap::new();
478        let entry = map.entry(SIGCHLD.into());
479        let state = GrandState::insert_from_system_if_vacant(&system, entry).unwrap();
480        assert_eq!(
481            state.current_state(),
482            &TrapState {
483                action: Action::Default,
484                origin: Origin::Inherited,
485                pending: false,
486            }
487        );
488        assert_eq!(state.parent_state(), None);
489        assert_eq!(state.internal_disposition(), Disposition::Default);
490    }
491
492    #[test]
493    fn insertion_with_inherited_disposition_of_ignore() {
494        let mut system = DummySystem::default();
495        system.0.insert(SIGCHLD, Disposition::Ignore);
496        let mut map = BTreeMap::new();
497        let entry = map.entry(SIGCHLD.into());
498        let state = GrandState::insert_from_system_if_vacant(&system, entry).unwrap();
499        assert_eq!(
500            state.current_state(),
501            &TrapState {
502                action: Action::Ignore,
503                origin: Origin::Inherited,
504                pending: false,
505            }
506        );
507        assert_eq!(state.parent_state(), None);
508        assert_eq!(state.internal_disposition(), Disposition::Default);
509    }
510
511    #[test]
512    fn insertion_with_occupied_entry() {
513        let mut system = DummySystem::default();
514        let mut map = BTreeMap::new();
515        let entry = map.entry(SIGCHLD.into());
516        let origin = Location::dummy("origin");
517        let action = Action::Command("echo".into());
518        let _ = GrandState::set_action(&mut system, entry, action.clone(), origin.clone(), false);
519
520        // If the entry is occupied, the function should return the existing
521        // state without accessing the system.
522        let entry = map.entry(SIGCHLD.into());
523        let state = GrandState::insert_from_system_if_vacant(&UnusedSystem, entry).unwrap();
524        assert_eq!(
525            state.current_state(),
526            &TrapState {
527                action,
528                origin: Origin::User(origin),
529                pending: false,
530            }
531        );
532        assert_eq!(state.parent_state(), None);
533        assert_eq!(state.internal_disposition(), Disposition::Default);
534    }
535
536    #[test]
537    fn insertion_with_non_signal_condition() {
538        let mut map = BTreeMap::new();
539        let entry = map.entry(Condition::Exit);
540        let state = GrandState::insert_from_system_if_vacant(&UnusedSystem, entry).unwrap();
541        assert_eq!(
542            state.current_state(),
543            &TrapState {
544                action: Action::Default,
545                origin: Origin::Inherited,
546                pending: false,
547            }
548        );
549        assert_eq!(state.parent_state(), None);
550        assert_eq!(state.internal_disposition(), Disposition::Default);
551    }
552
553    #[test]
554    fn setting_trap_to_ignore_without_override_ignore() {
555        let mut system = DummySystem::default();
556        let mut map = BTreeMap::new();
557        let entry = map.entry(SIGCHLD.into());
558        let origin = Location::dummy("origin");
559
560        let result =
561            GrandState::set_action(&mut system, entry, Action::Ignore, origin.clone(), false);
562        assert_eq!(result, Ok(()));
563        assert_eq!(
564            map[&SIGCHLD.into()].current_state(),
565            &TrapState {
566                action: Action::Ignore,
567                origin: Origin::User(origin),
568                pending: false
569            }
570        );
571        assert_eq!(map[&SIGCHLD.into()].parent_state(), None);
572        assert_eq!(system.0[&SIGCHLD], Disposition::Ignore);
573    }
574
575    #[test]
576    fn setting_trap_to_ignore_with_override_ignore() {
577        let mut system = DummySystem::default();
578        let mut map = BTreeMap::new();
579        let entry = map.entry(SIGCHLD.into());
580        let origin = Location::dummy("origin");
581
582        let result =
583            GrandState::set_action(&mut system, entry, Action::Ignore, origin.clone(), true);
584        assert_eq!(result, Ok(()));
585        assert_eq!(
586            map[&SIGCHLD.into()].current_state(),
587            &TrapState {
588                action: Action::Ignore,
589                origin: Origin::User(origin),
590                pending: false
591            }
592        );
593        assert_eq!(map[&SIGCHLD.into()].parent_state(), None);
594        assert_eq!(system.0[&SIGCHLD], Disposition::Ignore);
595    }
596
597    #[test]
598    fn setting_trap_to_command() {
599        let mut system = DummySystem::default();
600        let mut map = BTreeMap::new();
601        let entry = map.entry(SIGCHLD.into());
602        let action = Action::Command("echo".into());
603        let origin = Location::dummy("origin");
604
605        let result =
606            GrandState::set_action(&mut system, entry, action.clone(), origin.clone(), false);
607        assert_eq!(result, Ok(()));
608        assert_eq!(
609            map[&SIGCHLD.into()].current_state(),
610            &TrapState {
611                action,
612                origin: Origin::User(origin),
613                pending: false
614            }
615        );
616        assert_eq!(map[&SIGCHLD.into()].parent_state(), None);
617        assert_eq!(system.0[&SIGCHLD], Disposition::Catch);
618    }
619
620    #[test]
621    fn setting_trap_to_default() {
622        let mut system = DummySystem::default();
623        let mut map = BTreeMap::new();
624        let entry = map.entry(SIGCHLD.into());
625        let origin = Location::dummy("foo");
626        GrandState::set_action(&mut system, entry, Action::Ignore, origin, false).unwrap();
627
628        let entry = map.entry(SIGCHLD.into());
629        let origin = Location::dummy("bar");
630        let result =
631            GrandState::set_action(&mut system, entry, Action::Default, origin.clone(), false);
632        assert_eq!(result, Ok(()));
633        assert_eq!(
634            map[&SIGCHLD.into()].current_state(),
635            &TrapState {
636                action: Action::Default,
637                origin: Origin::User(origin),
638                pending: false
639            }
640        );
641        assert_eq!(map[&SIGCHLD.into()].parent_state(), None);
642        assert_eq!(system.0[&SIGCHLD], Disposition::Default);
643    }
644
645    #[test]
646    fn resetting_trap_from_ignore_no_override() {
647        let mut system = DummySystem::default();
648        system.0.insert(SIGCHLD, Disposition::Ignore);
649        let mut map = BTreeMap::new();
650        let entry = map.entry(SIGCHLD.into());
651        let origin = Location::dummy("foo");
652        let result = GrandState::set_action(&mut system, entry, Action::Ignore, origin, false);
653        assert_eq!(result, Err(SetActionError::InitiallyIgnored));
654
655        // Idempotence
656        let entry = map.entry(SIGCHLD.into());
657        let origin = Location::dummy("bar");
658        let result = GrandState::set_action(&mut system, entry, Action::Ignore, origin, false);
659        assert_eq!(result, Err(SetActionError::InitiallyIgnored));
660
661        assert_eq!(
662            map[&SIGCHLD.into()].current_state(),
663            &TrapState {
664                action: Action::Ignore,
665                origin: Origin::Inherited,
666                pending: false
667            }
668        );
669        assert_eq!(map[&SIGCHLD.into()].parent_state(), None);
670        assert_eq!(system.0[&SIGCHLD], Disposition::Ignore);
671    }
672
673    #[test]
674    fn resetting_trap_from_ignore_override() {
675        let mut system = DummySystem::default();
676        system.0.insert(SIGCHLD, Disposition::Ignore);
677        let mut map = BTreeMap::new();
678        let entry = map.entry(SIGCHLD.into());
679        let origin = Location::dummy("origin");
680        let result =
681            GrandState::set_action(&mut system, entry, Action::Ignore, origin.clone(), true);
682        assert_eq!(result, Ok(()));
683        assert_eq!(
684            map[&SIGCHLD.into()].current_state(),
685            &TrapState {
686                action: Action::Ignore,
687                origin: Origin::User(origin),
688                pending: false
689            }
690        );
691        assert_eq!(map[&SIGCHLD.into()].parent_state(), None);
692        assert_eq!(system.0[&SIGCHLD], Disposition::Ignore);
693    }
694
695    #[test]
696    fn internal_disposition_ignore() {
697        let mut system = DummySystem::default();
698        let mut map = BTreeMap::new();
699        let entry = map.entry(SIGCHLD.into());
700
701        let result = GrandState::set_internal_disposition(&mut system, entry, Disposition::Ignore);
702        assert_eq!(result, Ok(()));
703        assert_eq!(
704            map[&SIGCHLD.into()].internal_disposition(),
705            Disposition::Ignore
706        );
707        assert_eq!(
708            map[&SIGCHLD.into()].current_state(),
709            &TrapState {
710                action: Action::Default,
711                origin: Origin::Inherited,
712                pending: false
713            }
714        );
715        assert_eq!(map[&SIGCHLD.into()].parent_state(), None);
716        assert_eq!(system.0[&SIGCHLD], Disposition::Ignore);
717    }
718
719    #[test]
720    fn internal_disposition_catch() {
721        let mut system = DummySystem::default();
722        let mut map = BTreeMap::new();
723        let entry = map.entry(SIGCHLD.into());
724
725        let result = GrandState::set_internal_disposition(&mut system, entry, Disposition::Catch);
726        assert_eq!(result, Ok(()));
727        assert_eq!(
728            map[&SIGCHLD.into()].internal_disposition(),
729            Disposition::Catch
730        );
731        assert_eq!(
732            map[&SIGCHLD.into()].current_state(),
733            &TrapState {
734                action: Action::Default,
735                origin: Origin::Inherited,
736                pending: false
737            }
738        );
739        assert_eq!(map[&SIGCHLD.into()].parent_state(), None);
740        assert_eq!(system.0[&SIGCHLD], Disposition::Catch);
741    }
742
743    #[test]
744    fn action_ignore_and_internal_disposition_catch() {
745        let mut system = DummySystem::default();
746        let mut map = BTreeMap::new();
747        let entry = map.entry(SIGCHLD.into());
748        let origin = Location::dummy("origin");
749        let _ = GrandState::set_action(&mut system, entry, Action::Ignore, origin.clone(), false);
750        let entry = map.entry(SIGCHLD.into());
751
752        let result = GrandState::set_internal_disposition(&mut system, entry, Disposition::Catch);
753        assert_eq!(result, Ok(()));
754        assert_eq!(
755            map[&SIGCHLD.into()].internal_disposition(),
756            Disposition::Catch
757        );
758        assert_eq!(
759            map[&SIGCHLD.into()].current_state(),
760            &TrapState {
761                action: Action::Ignore,
762                origin: Origin::User(origin),
763                pending: false
764            }
765        );
766        assert_eq!(map[&SIGCHLD.into()].parent_state(), None);
767        assert_eq!(system.0[&SIGCHLD], Disposition::Catch);
768    }
769
770    #[test]
771    fn action_catch_and_internal_disposition_ignore() {
772        let mut system = DummySystem::default();
773        let mut map = BTreeMap::new();
774        let entry = map.entry(SIGCHLD.into());
775        let origin = Location::dummy("origin");
776        let action = Action::Command("echo".into());
777        let _ = GrandState::set_action(&mut system, entry, action.clone(), origin.clone(), false);
778        let entry = map.entry(SIGCHLD.into());
779
780        let result = GrandState::set_internal_disposition(&mut system, entry, Disposition::Ignore);
781        assert_eq!(result, Ok(()));
782        assert_eq!(
783            map[&SIGCHLD.into()].internal_disposition(),
784            Disposition::Ignore
785        );
786        assert_eq!(
787            map[&SIGCHLD.into()].current_state(),
788            &TrapState {
789                action,
790                origin: Origin::User(origin),
791                pending: false
792            }
793        );
794        assert_eq!(map[&SIGCHLD.into()].parent_state(), None);
795        assert_eq!(system.0[&SIGCHLD], Disposition::Catch);
796    }
797
798    #[test]
799    fn set_internal_disposition_for_initially_defaulted_signal_then_allow_override() {
800        let mut system = DummySystem::default();
801        let mut map = BTreeMap::new();
802        let entry = map.entry(SIGTTOU.into());
803        let _ = GrandState::set_internal_disposition(&mut system, entry, Disposition::Ignore);
804        let entry = map.entry(SIGTTOU.into());
805        let origin = Location::dummy("origin");
806        let action = Action::Command("echo".into());
807
808        let result =
809            GrandState::set_action(&mut system, entry, action.clone(), origin.clone(), false);
810        assert_eq!(result, Ok(()));
811        assert_eq!(
812            map[&SIGTTOU.into()].internal_disposition(),
813            Disposition::Ignore
814        );
815        assert_eq!(
816            map[&SIGTTOU.into()].current_state(),
817            &TrapState {
818                action,
819                origin: Origin::User(origin),
820                pending: false
821            }
822        );
823        assert_eq!(map[&SIGTTOU.into()].parent_state(), None);
824        assert_eq!(system.0[&SIGTTOU], Disposition::Catch);
825    }
826
827    #[test]
828    fn set_internal_disposition_for_initially_ignored_signal_then_reject_override() {
829        let mut system = DummySystem::default();
830        system.0.insert(SIGTTOU, Disposition::Ignore);
831        let mut map = BTreeMap::new();
832        let cond = SIGTTOU.into();
833        let entry = map.entry(cond);
834        let _ = GrandState::set_internal_disposition(&mut system, entry, Disposition::Ignore);
835        let entry = map.entry(cond);
836        let origin = Location::dummy("origin");
837        let action = Action::Command("echo".into());
838
839        let result = GrandState::set_action(&mut system, entry, action, origin, false);
840        assert_eq!(result, Err(SetActionError::InitiallyIgnored));
841        assert_eq!(map[&cond].internal_disposition(), Disposition::Ignore);
842        assert_eq!(
843            map[&cond].current_state(),
844            &TrapState {
845                action: Action::Ignore,
846                origin: Origin::Inherited,
847                pending: false
848            }
849        );
850        assert_eq!(map[&cond].parent_state(), None);
851        assert_eq!(system.0[&SIGTTOU], Disposition::Ignore);
852    }
853
854    #[test]
855    fn enter_subshell_with_internal_disposition_keeping_internal_disposition() {
856        let mut system = DummySystem::default();
857        let mut map = BTreeMap::new();
858        let cond = SIGCHLD.into();
859        GrandState::set_internal_disposition(&mut system, map.entry(cond), Disposition::Catch)
860            .unwrap();
861
862        let result = map.get_mut(&cond).unwrap().enter_subshell(
863            &mut system,
864            cond,
865            EnterSubshellOption::KeepInternalDisposition,
866        );
867        assert_eq!(result, Ok(()));
868        assert_eq!(map[&cond].internal_disposition(), Disposition::Catch);
869        assert_eq!(
870            map[&cond].current_state(),
871            &TrapState {
872                action: Action::Default,
873                origin: Origin::Inherited,
874                pending: false
875            }
876        );
877        assert_eq!(map[&cond].parent_state(), None);
878        assert_eq!(system.0[&SIGCHLD], Disposition::Catch);
879    }
880
881    #[test]
882    fn enter_subshell_with_internal_disposition_clearing_internal_disposition() {
883        let mut system = DummySystem::default();
884        let mut map = BTreeMap::new();
885        let cond = SIGCHLD.into();
886        let entry = map.entry(cond);
887        GrandState::set_internal_disposition(&mut system, entry, Disposition::Catch).unwrap();
888
889        let result = map.get_mut(&cond).unwrap().enter_subshell(
890            &mut system,
891            cond,
892            EnterSubshellOption::ClearInternalDisposition,
893        );
894        assert_eq!(result, Ok(()));
895        assert_eq!(map[&cond].internal_disposition(), Disposition::Default);
896        assert_eq!(
897            map[&cond].current_state(),
898            &TrapState {
899                action: Action::Default,
900                origin: Origin::Inherited,
901                pending: false
902            }
903        );
904        assert_eq!(map[&cond].parent_state(), None);
905        assert_eq!(system.0[&SIGCHLD], Disposition::Default);
906    }
907
908    #[test]
909    fn enter_subshell_with_ignore_and_no_internal_disposition() {
910        let mut system = DummySystem::default();
911        let mut map = BTreeMap::new();
912        let cond = SIGCHLD.into();
913        let entry = map.entry(cond);
914        let origin = Location::dummy("foo");
915        GrandState::set_action(&mut system, entry, Action::Ignore, origin.clone(), false).unwrap();
916
917        let result = map.get_mut(&cond).unwrap().enter_subshell(
918            &mut system,
919            cond,
920            EnterSubshellOption::KeepInternalDisposition,
921        );
922        assert_eq!(result, Ok(()));
923        assert_eq!(map[&cond].internal_disposition(), Disposition::Default);
924        assert_eq!(
925            map[&cond].current_state(),
926            &TrapState {
927                action: Action::Ignore,
928                origin: Origin::User(origin),
929                pending: false
930            }
931        );
932        assert_eq!(map[&cond].parent_state(), None);
933        assert_eq!(system.0[&SIGCHLD], Disposition::Ignore);
934    }
935
936    #[test]
937    fn enter_subshell_with_ignore_clearing_internal_disposition() {
938        let mut system = DummySystem::default();
939        let mut map = BTreeMap::new();
940        let cond = SIGCHLD.into();
941        let entry = map.entry(cond);
942        let origin = Location::dummy("foo");
943        GrandState::set_action(&mut system, entry, Action::Ignore, origin.clone(), false).unwrap();
944        let entry = map.entry(cond);
945        GrandState::set_internal_disposition(&mut system, entry, Disposition::Catch).unwrap();
946
947        let result = map.get_mut(&cond).unwrap().enter_subshell(
948            &mut system,
949            cond,
950            EnterSubshellOption::ClearInternalDisposition,
951        );
952        assert_eq!(result, Ok(()));
953        assert_eq!(map[&cond].internal_disposition(), Disposition::Default);
954        assert_eq!(
955            map[&cond].current_state(),
956            &TrapState {
957                action: Action::Ignore,
958                origin: Origin::User(origin),
959                pending: false
960            }
961        );
962        assert_eq!(map[&cond].parent_state(), None);
963        assert_eq!(system.0[&SIGCHLD], Disposition::Ignore);
964    }
965
966    #[test]
967    fn enter_subshell_with_command_and_no_internal_disposition() {
968        let mut system = DummySystem::default();
969        let mut map = BTreeMap::new();
970        let cond = SIGCHLD.into();
971        let entry = map.entry(cond);
972        let origin = Location::dummy("foo");
973        let action = Action::Command("echo".into());
974        GrandState::set_action(&mut system, entry, action.clone(), origin.clone(), false).unwrap();
975
976        let result = map.get_mut(&cond).unwrap().enter_subshell(
977            &mut system,
978            cond,
979            EnterSubshellOption::ClearInternalDisposition,
980        );
981        assert_eq!(result, Ok(()));
982        assert_eq!(map[&cond].internal_disposition(), Disposition::Default);
983        assert_eq!(
984            map[&cond].current_state(),
985            &TrapState {
986                action: Action::Default,
987                origin: Origin::Subshell,
988                pending: false
989            }
990        );
991        assert_eq!(
992            map[&cond].parent_state(),
993            Some(&TrapState {
994                action,
995                origin: Origin::User(origin),
996                pending: false
997            })
998        );
999        assert_eq!(system.0[&SIGCHLD], Disposition::Default);
1000    }
1001
1002    #[test]
1003    fn enter_subshell_with_command_keeping_internal_disposition() {
1004        let mut system = DummySystem::default();
1005        let mut map = BTreeMap::new();
1006        let cond = SIGTSTP.into();
1007        let entry = map.entry(cond);
1008        let origin = Location::dummy("foo");
1009        let action = Action::Command("echo".into());
1010        GrandState::set_action(&mut system, entry, action.clone(), origin.clone(), false).unwrap();
1011        let entry = map.entry(cond);
1012        GrandState::set_internal_disposition(&mut system, entry, Disposition::Ignore).unwrap();
1013
1014        let result = map.get_mut(&cond).unwrap().enter_subshell(
1015            &mut system,
1016            cond,
1017            EnterSubshellOption::KeepInternalDisposition,
1018        );
1019        assert_eq!(result, Ok(()));
1020        assert_eq!(map[&cond].internal_disposition(), Disposition::Ignore);
1021        assert_eq!(
1022            map[&cond].current_state(),
1023            &TrapState {
1024                action: Action::Default,
1025                origin: Origin::Subshell,
1026                pending: false
1027            }
1028        );
1029        assert_eq!(
1030            map[&cond].parent_state(),
1031            Some(&TrapState {
1032                action,
1033                origin: Origin::User(origin),
1034                pending: false
1035            })
1036        );
1037        assert_eq!(system.0[&SIGTSTP], Disposition::Ignore);
1038    }
1039
1040    #[test]
1041    fn enter_subshell_with_command_clearing_internal_disposition() {
1042        let mut system = DummySystem::default();
1043        let mut map = BTreeMap::new();
1044        let cond = SIGTSTP.into();
1045        let entry = map.entry(cond);
1046        let origin = Location::dummy("foo");
1047        let action = Action::Command("echo".into());
1048        GrandState::set_action(&mut system, entry, action.clone(), origin.clone(), false).unwrap();
1049        let entry = map.entry(cond);
1050        GrandState::set_internal_disposition(&mut system, entry, Disposition::Ignore).unwrap();
1051
1052        let result = map.get_mut(&cond).unwrap().enter_subshell(
1053            &mut system,
1054            cond,
1055            EnterSubshellOption::ClearInternalDisposition,
1056        );
1057        assert_eq!(result, Ok(()));
1058        assert_eq!(map[&cond].internal_disposition(), Disposition::Default);
1059        assert_eq!(
1060            map[&cond].current_state(),
1061            &TrapState {
1062                action: Action::Default,
1063                origin: Origin::Subshell,
1064                pending: false
1065            }
1066        );
1067        assert_eq!(
1068            map[&cond].parent_state(),
1069            Some(&TrapState {
1070                action,
1071                origin: Origin::User(origin),
1072                pending: false
1073            })
1074        );
1075        assert_eq!(system.0[&SIGTSTP], Disposition::Default);
1076    }
1077
1078    #[test]
1079    fn enter_subshell_with_command_ignoring() {
1080        let mut system = DummySystem::default();
1081        let mut map = BTreeMap::new();
1082        let cond = SIGQUIT.into();
1083        let entry = map.entry(cond);
1084        let origin = Location::dummy("foo");
1085        let action = Action::Command("echo".into());
1086        GrandState::set_action(&mut system, entry, action.clone(), origin.clone(), false).unwrap();
1087
1088        let result = map.get_mut(&cond).unwrap().enter_subshell(
1089            &mut system,
1090            cond,
1091            EnterSubshellOption::Ignore,
1092        );
1093        assert_eq!(result, Ok(()));
1094        assert_eq!(map[&cond].internal_disposition(), Disposition::Default);
1095        assert_eq!(
1096            map[&cond].current_state(),
1097            &TrapState {
1098                action: Action::Ignore,
1099                origin: Origin::Subshell,
1100                pending: false
1101            }
1102        );
1103        assert_eq!(
1104            map[&cond].parent_state(),
1105            Some(&TrapState {
1106                action,
1107                origin: Origin::User(origin),
1108                pending: false
1109            })
1110        );
1111        assert_eq!(system.0[&SIGQUIT], Disposition::Ignore);
1112    }
1113
1114    #[test]
1115    fn ignoring_initially_defaulted_signal() {
1116        let mut system = DummySystem::default();
1117        let mut map = BTreeMap::new();
1118        let cond = SIGQUIT.into();
1119        let entry = map.entry(cond);
1120        let vacant = assert_matches!(entry, Entry::Vacant(vacant) => vacant);
1121
1122        let result = GrandState::ignore(&mut system, vacant);
1123        assert_eq!(result, Ok(()));
1124        assert_eq!(
1125            map[&cond].current_state(),
1126            &TrapState {
1127                action: Action::Ignore,
1128                origin: Origin::Subshell,
1129                pending: false
1130            }
1131        );
1132        assert_eq!(map[&cond].parent_state(), None);
1133        assert_eq!(system.0[&SIGQUIT], Disposition::Ignore);
1134
1135        let entry = map.entry(cond);
1136        let origin = Location::dummy("foo");
1137        let action = Action::Command("echo".into());
1138        let result = GrandState::set_action(&mut system, entry, action, origin, false);
1139        assert_eq!(result, Ok(()));
1140    }
1141
1142    #[test]
1143    fn ignoring_initially_ignored_signal() {
1144        let mut system = DummySystem::default();
1145        system.0.insert(SIGQUIT, Disposition::Ignore);
1146        let mut map = BTreeMap::new();
1147        let cond = SIGQUIT.into();
1148        let entry = map.entry(cond);
1149        let vacant = assert_matches!(entry, Entry::Vacant(vacant) => vacant);
1150
1151        let result = GrandState::ignore(&mut system, vacant);
1152        assert_eq!(result, Ok(()));
1153        assert_eq!(
1154            map[&cond].current_state(),
1155            &TrapState {
1156                action: Action::Ignore,
1157                origin: Origin::Inherited,
1158                pending: false
1159            }
1160        );
1161        assert_eq!(map[&cond].parent_state(), None);
1162        assert_eq!(system.0[&SIGQUIT], Disposition::Ignore);
1163
1164        let entry = map.entry(cond);
1165        let origin = Location::dummy("foo");
1166        let action = Action::Command("echo".into());
1167        let result = GrandState::set_action(&mut system, entry, action, origin, false);
1168        assert_eq!(result, Err(SetActionError::InitiallyIgnored));
1169    }
1170
1171    #[test]
1172    fn clearing_parent_setting() {
1173        let mut system = DummySystem::default();
1174        let mut map = BTreeMap::new();
1175        let cond = SIGCHLD.into();
1176        let entry = map.entry(cond);
1177        let origin = Location::dummy("foo");
1178        let action = Action::Command("echo".into());
1179        GrandState::set_action(&mut system, entry, action, origin, false).unwrap();
1180        let state = map.get_mut(&cond).unwrap();
1181        state
1182            .enter_subshell(
1183                &mut system,
1184                cond,
1185                EnterSubshellOption::ClearInternalDisposition,
1186            )
1187            .unwrap();
1188
1189        state.clear_parent_state();
1190        assert_eq!(
1191            state.current_state(),
1192            &TrapState {
1193                action: Action::Default,
1194                origin: Origin::Subshell,
1195                pending: false
1196            }
1197        );
1198        assert_eq!(state.parent_state(), None);
1199    }
1200
1201    #[test]
1202    fn marking_as_caught_and_handling() {
1203        let mut system = DummySystem::default();
1204        let mut map = BTreeMap::new();
1205        let cond = SIGUSR1.into();
1206        let entry = map.entry(cond);
1207        let origin = Location::dummy("foo");
1208        let action = Action::Command("echo".into());
1209        GrandState::set_action(&mut system, entry, action.clone(), origin.clone(), false).unwrap();
1210
1211        let state = &mut map.get_mut(&cond).unwrap();
1212        state.mark_as_caught();
1213        let expected_trap = TrapState {
1214            action,
1215            origin: Origin::User(origin),
1216            pending: true,
1217        };
1218        assert_eq!(state.current_state(), &expected_trap);
1219        assert_eq!(state.parent_state(), None);
1220
1221        let trap = state.handle_if_caught();
1222        let expected_trap = TrapState {
1223            pending: false,
1224            ..expected_trap
1225        };
1226        assert_eq!(trap, Some(&expected_trap));
1227
1228        let trap = state.handle_if_caught();
1229        assert_eq!(trap, None);
1230    }
1231}