Skip to main content

yash_env/
job.rs

1// This file is part of yash, an extended POSIX shell.
2// Copyright (C) 2021 WATANABE Yuki
3//
4// This program is free software: you can redistribute it and/or modify
5// it under the terms of the GNU General Public License as published by
6// the Free Software Foundation, either version 3 of the License, or
7// (at your option) any later version.
8//
9// This program is distributed in the hope that it will be useful,
10// but WITHOUT ANY WARRANTY; without even the implied warranty of
11// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
12// GNU General Public License for more details.
13//
14// You should have received a copy of the GNU General Public License
15// along with this program.  If not, see <https://www.gnu.org/licenses/>.
16
17//! Type definitions for job management.
18//!
19//! A [`JobList`] manages the state of jobs executed by the shell.
20//! Each [`Job`] in the job list remembers the latest state of the child process
21//! performing the job's task.
22//!
23//! The job list stores jobs in an internal array. The index of a job in the
24//! array never changes once the [job is added](JobList::add) to the job list.
25//! The index of the other jobs does not change when you [remove a
26//! job](JobList::remove). Note that the job list may reuse the index of a
27//! removed job for another job added later.
28//!
29//! When the [wait system call](crate::system::Wait::wait) returns a new state of a
30//! child process, the caller should pass it to [`JobList::update_status`],
31//! which modifies the state of the corresponding job. The `state_changed` flag
32//! of the job is set when the job is updated and should be
33//! [reset when reported](JobRefMut::state_reported).
34//!
35//! The job list remembers the selection of two special jobs called the "current
36//! job" and "previous job." The previous job is chosen automatically, so there
37//! is no function to modify it. You can change the current job by
38//! [`JobList::set_current_job`].
39//!
40//! The [`JobList::set_last_async_pid`] function remembers the process ID of the
41//! last executed asynchronous command, which will be the value of the `$!`
42//! special parameter.
43
44use crate::Env;
45use crate::semantics::{Divert, ExitStatus};
46use crate::signal;
47use crate::system::Signals;
48#[cfg(any(doc, test))]
49use crate::trap::Action;
50use slab::Slab;
51use std::collections::HashMap;
52use std::iter::FusedIterator;
53use std::ops::ControlFlow::{Break, Continue};
54use std::ops::Deref;
55use thiserror::Error;
56
57#[cfg(unix)]
58type RawPidDef = libc::pid_t;
59#[cfg(not(unix))]
60type RawPidDef = i32;
61
62/// Raw process ID type
63///
64/// This is a type alias for the raw process ID type `pid_t` declared in the
65/// [`libc`] crate. The exact representation of this type is platform-dependent
66/// while POSIX requires the type to be a signed integer. On non-Unix platforms,
67/// this type is hard-coded to `i32`.
68///
69/// Process IDs are usually wrapped in the [`Pid`] type for better type safety,
70/// so this type is not used directly in most cases.
71pub type RawPid = RawPidDef;
72
73/// Process ID
74///
75/// A process ID is an integer that identifies a process in the system. This
76/// type implements the new type pattern around the raw process ID type
77/// [`RawPid`].  The advantage of using this type is that it is more type-safe
78/// than using the raw integer value directly.
79///
80/// Although genuine process IDs are always positive integers, this type allows
81/// zero or negative values for the purpose of specifying a group of processes
82/// when used as a parameter to the [`kill`] and [`wait`] system calls. The
83/// [`setpgid`] system call also uses process ID zero to specify the process
84/// ID of the calling process.
85///
86/// This type may also be used to represent process group IDs, session IDs, etc.
87///
88/// [`kill`]: crate::system::SendSignal::kill
89/// [`wait`]: crate::system::Wait::wait
90/// [`setpgid`]: crate::system::SetPgid::setpgid
91#[repr(transparent)]
92#[derive(Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
93pub struct Pid(pub RawPid);
94
95impl std::fmt::Display for Pid {
96    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
97        self.0.fmt(f)
98    }
99}
100
101impl std::ops::Neg for Pid {
102    type Output = Self;
103    fn neg(self) -> Self {
104        Self(-self.0)
105    }
106}
107
108impl Pid {
109    /// Sentinel value for the [`kill`] and [`wait`] system calls specifying all
110    /// processes in the process group of the calling process.
111    ///
112    /// [`kill`]: crate::system::SendSignal::kill
113    /// [`wait`]: crate::system::Wait::wait
114    pub const MY_PROCESS_GROUP: Self = Pid(0);
115
116    /// Sentinel value for the [`kill`] and [`wait`] system calls specifying all
117    /// possible processes.
118    ///
119    /// [`kill`]: crate::system::SendSignal::kill
120    /// [`wait`]: crate::system::Wait::wait
121    pub const ALL: Self = Pid(-1);
122}
123
124/// Execution state of a process from which the exit status can be computed
125///
126/// This type is used to represent the result of a process execution. It is
127/// similar to the `WaitStatus` type defined in the `nix` crate, but it is
128/// simplified to represent only the states that are relevant to the shell.
129///
130/// This type only contains the states the process's exit status can be computed
131/// from. See also [`ProcessState`], which is a more general type that includes
132/// the states that are not directly related to the exit status.
133#[derive(Clone, Copy, Debug, Eq, PartialEq)]
134pub enum ProcessResult {
135    /// The process has been stopped by a signal.
136    Stopped(signal::Number),
137    /// The process has exited.
138    Exited(ExitStatus),
139    /// The process has been terminated by a signal.
140    Signaled {
141        signal: signal::Number,
142        core_dump: bool,
143    },
144}
145
146impl ProcessResult {
147    /// Creates a new `ProcessResult` instance representing an exited process.
148    #[inline]
149    #[must_use]
150    pub fn exited<S: Into<ExitStatus>>(exit_status: S) -> Self {
151        Self::Exited(exit_status.into())
152    }
153
154    /// Whether the process is stopped
155    #[must_use]
156    pub fn is_stopped(&self) -> bool {
157        matches!(self, ProcessResult::Stopped(_))
158    }
159}
160
161/// Converts `ProcessResult` to `ExitStatus`.
162impl From<ProcessResult> for ExitStatus {
163    fn from(result: ProcessResult) -> Self {
164        match result {
165            ProcessResult::Exited(exit_status) => exit_status,
166            ProcessResult::Stopped(signal) | ProcessResult::Signaled { signal, .. } => {
167                ExitStatus::from(signal)
168            }
169        }
170    }
171}
172
173/// Execution state of a process, either running or halted
174///
175/// This type is used to represent the current state of a process. It is similar
176/// to the `WaitStatus` type defined in the `nix` crate, but it is simplified to
177/// represent only the states that are relevant to the shell.
178///
179/// This type can represent all possible states of a process, including running,
180/// stopped, exited, and signaled states. When the process is not running, the
181/// state is represented by a [`ProcessResult`].
182#[derive(Clone, Copy, Debug, Eq, PartialEq)]
183pub enum ProcessState {
184    /// The process is running.
185    Running,
186    /// The process has exited, stopped, or been terminated by a signal.
187    Halted(ProcessResult),
188}
189
190impl ProcessState {
191    /// Creates a new `ProcessState` instance representing a stopped process.
192    #[inline]
193    #[must_use]
194    pub fn stopped(signal: signal::Number) -> Self {
195        Self::Halted(ProcessResult::Stopped(signal))
196    }
197
198    /// Creates a new `ProcessState` instance representing an exited process.
199    #[inline]
200    #[must_use]
201    pub fn exited<S: Into<ExitStatus>>(exit_status: S) -> Self {
202        Self::Halted(ProcessResult::exited(exit_status))
203    }
204
205    /// Whether the process is not yet terminated
206    #[must_use]
207    pub fn is_alive(&self) -> bool {
208        match self {
209            ProcessState::Running => true,
210            ProcessState::Halted(result) => result.is_stopped(),
211        }
212    }
213
214    /// Whether the process is stopped
215    #[must_use]
216    pub fn is_stopped(&self) -> bool {
217        matches!(self, Self::Halted(result) if result.is_stopped())
218    }
219}
220
221impl From<ProcessResult> for ProcessState {
222    #[inline]
223    fn from(result: ProcessResult) -> Self {
224        Self::Halted(result)
225    }
226}
227
228/// Error value indicating that the process is running.
229///
230/// This error value may be returned by [`TryFrom<ProcessState>::try_from`].
231#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)]
232pub struct RunningProcess;
233
234/// Converts `ProcessState` to `ExitStatus`.
235///
236/// For the `Running` state, the conversion fails with [`RunningProcess`].
237impl TryFrom<ProcessState> for ExitStatus {
238    type Error = RunningProcess;
239    fn try_from(state: ProcessState) -> Result<Self, RunningProcess> {
240        match state {
241            ProcessState::Halted(result) => Ok(result.into()),
242            ProcessState::Running => Err(RunningProcess),
243        }
244    }
245}
246
247/// Set of one or more processes executing a pipeline
248///
249/// In the current implementation, a job contains the process ID of one child
250/// process of the shell. Though there may be more processes involved in the
251/// execution of the pipeline, the shell takes care of only one process of the
252/// job.
253#[derive(Clone, Debug, Eq, PartialEq)]
254#[non_exhaustive]
255pub struct Job {
256    /// Process ID
257    ///
258    /// If the job is job-controlled, this is also the process group ID.
259    pub pid: Pid,
260
261    /// Whether the job is job-controlled.
262    ///
263    /// If the job is job-controlled, the job processes run in their own process
264    /// group.
265    pub job_controlled: bool,
266
267    /// Current state of the process
268    pub state: ProcessState,
269
270    /// State of the process expected in the next update
271    ///
272    /// See [`JobRefMut::expect`] and [`JobList::update_status`] for details.
273    pub expected_state: Option<ProcessState>,
274
275    /// Indicator of state change
276    ///
277    /// This flag is true if the `state` has been changed since the state was
278    /// last reported to the user.
279    pub state_changed: bool,
280
281    /// Whether this job is a true child of the current shell
282    ///
283    /// When a subshell is created, the jobs inherited from the parent shell are
284    /// marked as not owned by the current shell. The shell cannot wait for
285    /// these jobs to finish.
286    pub is_owned: bool,
287
288    /// String representation of this process
289    pub name: String,
290}
291
292impl Job {
293    /// Creates a new job instance.
294    ///
295    /// This function requires a process ID to initialize the new job. The other
296    /// members of the job are defaulted.
297    pub fn new(pid: Pid) -> Self {
298        Job {
299            pid,
300            job_controlled: false,
301            state: ProcessState::Running,
302            expected_state: None,
303            state_changed: true,
304            is_owned: true,
305            name: String::new(),
306        }
307    }
308
309    /// Whether the job is suspended
310    #[must_use]
311    fn is_suspended(&self) -> bool {
312        self.state.is_stopped()
313    }
314}
315
316/// Partially mutable reference to [`Job`].
317///
318/// This struct is a specialized reference type for `Job`. It provides limited
319/// mutability over the `Job` instance through its methods. It also allows
320/// unlimited immutable access through the `Deref` implementation.
321#[derive(Debug, Eq, PartialEq)]
322pub struct JobRefMut<'a>(&'a mut Job);
323
324impl JobRefMut<'_> {
325    /// Sets the `expected_state` of the job.
326    ///
327    /// This method remembers the argument as the expected state of the job.
328    /// If the job is [updated] with the same state, the `state_changed` flag
329    /// is not set then.
330    ///
331    /// This method may be used to suppress a change report of a job state,
332    /// especially when the state is reported before it is actually changed.
333    ///
334    /// [updated]: JobList::update_status
335    pub fn expect<S>(&mut self, state: S)
336    where
337        S: Into<Option<ProcessState>>,
338    {
339        self.0.expected_state = state.into();
340    }
341
342    /// Clears the `state_changed` flag of the job.
343    ///
344    /// Normally, this method should be called when the shell printed a job
345    /// status report.
346    pub fn state_reported(&mut self) {
347        self.0.state_changed = false
348    }
349}
350
351impl Deref for JobRefMut<'_> {
352    type Target = Job;
353    fn deref(&self) -> &Job {
354        self.0
355    }
356}
357
358/// Indexed iterator of jobs.
359///
360/// Call [`JobList::iter`] to get an instance of `Iter`.
361#[derive(Clone, Debug)]
362pub struct Iter<'a>(slab::Iter<'a, Job>);
363
364impl<'a> Iterator for Iter<'a> {
365    type Item = (usize, &'a Job);
366
367    #[inline(always)]
368    fn next(&mut self) -> Option<(usize, &'a Job)> {
369        self.0.next()
370    }
371
372    #[inline(always)]
373    fn size_hint(&self) -> (usize, Option<usize>) {
374        self.0.size_hint()
375    }
376}
377
378impl<'a> DoubleEndedIterator for Iter<'a> {
379    #[inline(always)]
380    fn next_back(&mut self) -> Option<(usize, &'a Job)> {
381        self.0.next_back()
382    }
383}
384
385impl ExactSizeIterator for Iter<'_> {
386    #[inline(always)]
387    fn len(&self) -> usize {
388        self.0.len()
389    }
390}
391
392impl FusedIterator for Iter<'_> {}
393
394/// Indexed iterator of partially mutable jobs.
395///
396/// Call [`JobList::iter_mut`] to get an instance of `IterMut`.
397#[derive(Debug)]
398pub struct IterMut<'a>(slab::IterMut<'a, Job>);
399
400impl<'a> Iterator for IterMut<'a> {
401    type Item = (usize, JobRefMut<'a>);
402
403    #[inline]
404    fn next(&mut self) -> Option<(usize, JobRefMut<'a>)> {
405        self.0.next().map(|(index, job)| (index, JobRefMut(job)))
406    }
407
408    #[inline(always)]
409    fn size_hint(&self) -> (usize, Option<usize>) {
410        self.0.size_hint()
411    }
412}
413
414impl<'a> DoubleEndedIterator for IterMut<'a> {
415    fn next_back(&mut self) -> Option<(usize, JobRefMut<'a>)> {
416        self.0
417            .next_back()
418            .map(|(index, job)| (index, JobRefMut(job)))
419    }
420}
421
422impl ExactSizeIterator for IterMut<'_> {
423    #[inline(always)]
424    fn len(&self) -> usize {
425        self.0.len()
426    }
427}
428
429impl FusedIterator for IterMut<'_> {}
430
431/// Collection of jobs.
432///
433/// See the [module documentation](self) for details.
434#[derive(Clone, Debug)]
435pub struct JobList {
436    /// Jobs managed by the shell
437    jobs: Slab<Job>,
438
439    /// Map from process IDs to indices of `jobs`
440    ///
441    /// This is a shortcut to quickly find jobs by process ID.
442    pids_to_indices: HashMap<Pid, usize>,
443
444    /// Index of the current job. (Only valid when the list is non-empty)
445    current_job_index: usize,
446
447    /// Index of the previous job. (Only valid when the list is non-empty)
448    previous_job_index: usize,
449
450    /// Process ID of the most recently executed asynchronous command.
451    last_async_pid: Pid,
452}
453
454impl Default for JobList {
455    fn default() -> Self {
456        JobList {
457            jobs: Slab::new(),
458            pids_to_indices: HashMap::new(),
459            current_job_index: usize::default(),
460            previous_job_index: usize::default(),
461            last_async_pid: Pid(0),
462        }
463    }
464}
465
466impl JobList {
467    /// Creates an empty job list.
468    #[inline]
469    #[must_use]
470    pub fn new() -> Self {
471        Self::default()
472    }
473
474    /// Returns the job at the specified index.
475    ///
476    /// The result is `None` if there is no job for the index.
477    #[inline]
478    pub fn get(&self, index: usize) -> Option<&Job> {
479        self.jobs.get(index)
480    }
481
482    /// Returns a partially mutable reference to the job at the specified index.
483    ///
484    /// The result is `None` if there is no job for the index.
485    #[inline]
486    pub fn get_mut(&mut self, index: usize) -> Option<JobRefMut<'_>> {
487        self.jobs.get_mut(index).map(JobRefMut)
488    }
489
490    /// Returns the number of jobs in this job list.
491    #[inline]
492    pub fn len(&self) -> usize {
493        self.jobs.len()
494    }
495
496    /// Returns true if this job list contains no jobs.
497    #[inline]
498    pub fn is_empty(&self) -> bool {
499        self.len() == 0
500    }
501
502    /// Returns an indexed iterator of jobs.
503    ///
504    /// The item type of the returned iterator is `(usize, &Job)`.
505    /// Jobs are iterated in the order of indices.
506    #[inline]
507    pub fn iter(&self) -> Iter<'_> {
508        Iter(self.jobs.iter())
509    }
510
511    /// Returns an indexed iterator of partially mutable jobs.
512    ///
513    /// The item type of the returned iterator is `(usize, JobRefMut)`.
514    /// Note that the iterator does not yield raw mutable references to jobs.
515    /// [`JobRefMut`] allows mutating only part of jobs.
516    ///
517    /// Jobs are iterated in the order of indices.
518    #[inline]
519    pub fn iter_mut(&mut self) -> IterMut<'_> {
520        IterMut(self.jobs.iter_mut())
521    }
522
523    /// Finds a job by the process ID.
524    ///
525    /// This function returns the index of the job whose process ID is `pid`.
526    /// The result is `None` if no such job is found.
527    ///
528    /// A `JobList` maintains an internal hash map to quickly find jobs by
529    /// process ID.
530    pub fn find_by_pid(&self, pid: Pid) -> Option<usize> {
531        self.pids_to_indices.get(&pid).copied()
532    }
533}
534
535impl<'a> IntoIterator for &'a JobList {
536    type Item = (usize, &'a Job);
537    type IntoIter = Iter<'a>;
538    #[inline(always)]
539    fn into_iter(self) -> Iter<'a> {
540        self.iter()
541    }
542}
543
544impl<'a> IntoIterator for &'a mut JobList {
545    type Item = (usize, JobRefMut<'a>);
546    type IntoIter = IterMut<'a>;
547    #[inline(always)]
548    fn into_iter(self) -> IterMut<'a> {
549        self.iter_mut()
550    }
551}
552
553/// Supports indexing operation on `JobList`.
554impl std::ops::Index<usize> for JobList {
555    type Output = Job;
556
557    /// Returns a reference to the specified job.
558    ///
559    /// This function will panic if the job does not exist.
560    fn index(&self, index: usize) -> &Job {
561        &self.jobs[index]
562    }
563}
564
565/// Iterator that conditionally removes jobs from a job list.
566///
567/// Call [`JobList::extract_if`] to get an instance of `ExtractIf`.
568#[derive(Debug)]
569pub struct ExtractIf<'a, F>
570where
571    F: FnMut(usize, JobRefMut) -> bool,
572{
573    list: &'a mut JobList,
574    should_remove: F,
575    next_index: usize,
576    len: usize,
577}
578
579impl<F> Iterator for ExtractIf<'_, F>
580where
581    F: FnMut(usize, JobRefMut) -> bool,
582{
583    type Item = (usize, Job);
584
585    fn next(&mut self) -> Option<(usize, Job)> {
586        while self.len > 0 {
587            let index = self.next_index;
588            self.next_index += 1;
589            if let Some(job) = self.list.get_mut(index) {
590                self.len -= 1;
591                if (self.should_remove)(index, job) {
592                    let job = self.list.remove(index).unwrap();
593                    return Some((index, job));
594                }
595            }
596        }
597        None
598    }
599
600    fn size_hint(&self) -> (usize, Option<usize>) {
601        (0, Some(self.len))
602    }
603}
604
605impl<F> FusedIterator for ExtractIf<'_, F> where F: FnMut(usize, JobRefMut) -> bool {}
606
607impl JobList {
608    /// Inserts a job to this job list.
609    ///
610    /// This function returns a unique index assigned to the job.
611    ///
612    /// If there already is a job that has the same process ID as that of the
613    /// new job, the existing job is silently removed.
614    ///
615    /// If the new job is suspended and the [current job](Self::current_job) is
616    /// not, the new job becomes the current job. If the new job and the current
617    /// job are suspended but the [previous job](Self::previous_job) is not, the
618    /// new job becomes the previous job.
619    pub fn insert(&mut self, job: Job) -> usize {
620        let new_job_is_suspended = job.is_suspended();
621        let ex_current_job_is_suspended =
622            self.current_job().map(|index| self[index].is_suspended());
623        let ex_previous_job_is_suspended =
624            self.previous_job().map(|index| self[index].is_suspended());
625
626        // Add the job to `self.jobs` and `self.pids_to_indices`.
627        use std::collections::hash_map::Entry::*;
628        let index = match self.pids_to_indices.entry(job.pid) {
629            Vacant(entry) => {
630                let index = self.jobs.insert(job);
631                entry.insert(index);
632                index
633            }
634            Occupied(entry) => {
635                let index = *entry.get();
636                self.jobs[index] = job;
637                index
638            }
639        };
640        debug_assert_eq!(self.jobs.len(), self.pids_to_indices.len());
641
642        // Reselect the current and previous job.
643        match ex_current_job_is_suspended {
644            None => self.current_job_index = index,
645            Some(false) if new_job_is_suspended => self.set_current_job(index).unwrap(),
646            Some(_) => match ex_previous_job_is_suspended {
647                None => self.previous_job_index = index,
648                Some(false) if new_job_is_suspended => self.previous_job_index = index,
649                Some(_) => (),
650            },
651        }
652
653        index
654    }
655
656    /// Adds a job to this job list.
657    ///
658    /// This function is an alias for [`insert`](Self::insert).
659    #[deprecated(since = "0.15.0", note = "use `insert` instead")]
660    #[inline(always)]
661    pub fn add(&mut self, job: Job) -> usize {
662        self.insert(job)
663    }
664
665    /// Removes a job from this job list.
666    ///
667    /// This function returns the job removed from the job list.
668    /// The result is `None` if there is no job for the index.
669    ///
670    /// If the removed job is the [current job](Self::current_job), the
671    /// [previous job](Self::previous_job) becomes the current job and another
672    /// job is selected for the new previous job, if any.
673    /// If the removed job is the previous job, another job is selected for the
674    /// new previous job, if any.
675    pub fn remove(&mut self, index: usize) -> Option<Job> {
676        let job = self.jobs.try_remove(index);
677
678        if let Some(job) = &job {
679            // Keep `pids_to_indices` in sync
680            self.pids_to_indices.remove(&job.pid);
681
682            if self.jobs.is_empty() {
683                // Clearing an already empty slab may seem redundant, but this
684                // operation purges the slab's internal cache of unused indices,
685                // so that jobs added later have indices starting from 0.
686                self.jobs.clear();
687            }
688
689            // Reselect the current and previous job
690            let previous_job_becomes_current_job = index == self.current_job_index;
691            if previous_job_becomes_current_job {
692                self.current_job_index = self.previous_job_index;
693            }
694            if previous_job_becomes_current_job || index == self.previous_job_index {
695                self.previous_job_index = self
696                    .any_suspended_job_but_current()
697                    .unwrap_or_else(|| self.any_job_but_current().unwrap_or_default());
698            }
699        }
700        debug_assert_eq!(self.jobs.len(), self.pids_to_indices.len());
701
702        job
703    }
704
705    /// Removes jobs that satisfy the predicate.
706    ///
707    /// This function uses the `should_remove` function to decide whether to
708    /// remove jobs. If it returns true, the job is removed and yielded from the
709    /// iterator. Otherwise, the job remains in the list.
710    ///
711    /// You can reset the `state_changed` flag of a job
712    /// ([`JobRefMut::state_reported`]) regardless of whether you choose to
713    /// remove it or not.
714    ///
715    /// This function is a simplified version of [`JobList::extract_if`] that
716    /// does not return removed jobs.
717    pub fn remove_if<F>(&mut self, should_remove: F)
718    where
719        F: FnMut(usize, JobRefMut) -> bool,
720    {
721        self.extract_if(should_remove).for_each(drop)
722    }
723
724    /// Returns an iterator that conditionally removes jobs.
725    ///
726    /// The iterator uses the `should_remove` function to decide whether to
727    /// remove jobs. If it returns true, the job is removed and yielded from the
728    /// iterator. Otherwise, the job remains in the list.
729    ///
730    /// You can reset the `state_changed` flag of a job
731    /// ([`JobRefMut::state_reported`]) regardless of whether you choose to
732    /// remove it or not.
733    ///
734    /// If the returned iterator is dropped before iterating all jobs, the
735    /// remaining jobs are retained in the list.
736    ///
737    /// If you don't need to take the ownership of removed jobs, consider using
738    /// [`JobList::remove_if`] instead.
739    pub fn extract_if<F>(&mut self, should_remove: F) -> ExtractIf<'_, F>
740    where
741        F: FnMut(usize, JobRefMut) -> bool,
742    {
743        let len = self.len();
744        ExtractIf {
745            list: self,
746            should_remove,
747            next_index: 0,
748            len,
749        }
750    }
751}
752
753impl JobList {
754    /// Updates the state of a job.
755    ///
756    /// The result of a [`wait`](crate::system::Wait::wait) call should be passed to
757    /// this function. It looks up the job for the given process ID, updates the
758    /// state of the job to the given `state`, and sets the `state_changed` flag
759    /// in the job. As an exception, if `state` is equal to the `expected_state`
760    /// of the job, the `state_changed` flag is not set. The `expected_state` is
761    /// cleared in any case. (See also [`JobRefMut::expect`] for the usage of
762    /// `expected_state`.)
763    ///
764    /// Returns the index of the job updated. If there is no job for the given
765    /// process ID, the result is `None`.
766    ///
767    /// When a job is suspended (i.e., `state` is `Stopped`), the job becomes
768    /// the [current job](Self::current_job) and the old current job becomes the
769    /// [previous job](Self::previous_job). When a suspended job gets a state
770    /// update:
771    ///
772    /// - If the updated job is the current job and the previous job is
773    ///   suspended, the previous job becomes the current job and the new
774    ///   previous job is chosen from other suspended jobs. If there is no
775    ///   suspended jobs, the new previous jobs is the old current job.
776    /// - If the updated job is the previous job and there is a suspended job
777    ///   other than the current job, it becomes the previous job.
778    pub fn update_status(&mut self, pid: Pid, state: ProcessState) -> Option<usize> {
779        let index = self.find_by_pid(pid)?;
780
781        // Update the job state.
782        let job = &mut self.jobs[index];
783        let was_suspended = job.is_suspended();
784        job.state = state;
785        job.state_changed |= job.expected_state != Some(state);
786        job.expected_state = None;
787
788        // Reselect the current and previous job.
789        if !was_suspended && job.is_suspended() {
790            if index != self.current_job_index {
791                self.previous_job_index = std::mem::replace(&mut self.current_job_index, index);
792            }
793        } else if was_suspended
794            && !job.is_suspended()
795            && let Some(prev_index) = self.previous_job()
796        {
797            let previous_job_becomes_current_job =
798                index == self.current_job_index && self[prev_index].is_suspended();
799            if previous_job_becomes_current_job {
800                self.current_job_index = prev_index;
801            }
802            if previous_job_becomes_current_job || index == prev_index {
803                self.previous_job_index = self.any_suspended_job_but_current().unwrap_or(index);
804            }
805        }
806
807        Some(index)
808    }
809
810    /// Disowns all jobs.
811    ///
812    /// This function sets the `is_owned` flag of all jobs to `false`.
813    pub fn disown_all(&mut self) {
814        for (_, job) in &mut self.jobs {
815            job.is_owned = false;
816        }
817    }
818}
819
820/// Error type for [`JobList::set_current_job`].
821#[derive(Clone, Copy, Debug, Eq, Error, Hash, PartialEq)]
822pub enum SetCurrentJobError {
823    /// The specified index does not refer to any job.
824    #[error("no such job")]
825    NoSuchJob,
826
827    /// The specified job is not a suspended job and there is another suspended
828    /// job.
829    #[error("the current job must be selected from suspended jobs")]
830    NotSuspended,
831}
832
833impl JobList {
834    /// Selects the current job.
835    ///
836    /// This function changes the current job to the job specified by the index
837    /// and the previous job to the old current job.
838    ///
839    /// If there is one or more suspended jobs, the current job must be selected
840    /// from them. If the index does not refer to a suspended job, the
841    /// `NotSuspended` error is returned.
842    ///
843    /// If the index does not refer to any job, the `NoSuchJob` error is
844    /// returned.
845    pub fn set_current_job(&mut self, index: usize) -> Result<(), SetCurrentJobError> {
846        let job = self.get(index).ok_or(SetCurrentJobError::NoSuchJob)?;
847        if !job.is_suspended() && self.iter().any(|(_, job)| job.is_suspended()) {
848            return Err(SetCurrentJobError::NotSuspended);
849        }
850
851        if index != self.current_job_index {
852            self.previous_job_index = std::mem::replace(&mut self.current_job_index, index);
853        }
854        Ok(())
855    }
856
857    /// Returns the index of the current job.
858    ///
859    /// If the job list contains at least one job, there is a current job. This
860    /// function returns its index. If the job list is empty, the result is
861    /// `None`.
862    ///
863    /// If there is any suspended jobs, the current job must be a suspended job.
864    /// Running or terminated jobs can be the current job if there is no
865    /// suspended job. You can [change the current job](Self::set_current_job)
866    /// as long as the above rules are met.
867    ///
868    /// See also [`previous_job`](Self::previous_job).
869    pub fn current_job(&self) -> Option<usize> {
870        if self.jobs.contains(self.current_job_index) {
871            Some(self.current_job_index)
872        } else {
873            None
874        }
875    }
876
877    /// Returns the index of the previous job.
878    ///
879    /// If the job list contains two or more jobs, there is a previous job. This
880    /// function returns its index. If the job list has zero or one job, the
881    /// result is `None`.
882    ///
883    /// The previous job is never the same job as the [current
884    /// job](Self::current_job).
885    ///
886    /// If there are two or more suspended jobs, the previous job must be a
887    /// suspended job.  Running or terminated jobs can be the previous job if
888    /// there is zero or one suspended job.
889    ///
890    /// You cannot directly select the previous job. When the current job is
891    /// selected, the old current job becomes the previous job.
892    pub fn previous_job(&self) -> Option<usize> {
893        if self.previous_job_index != self.current_job_index
894            && self.jobs.contains(self.previous_job_index)
895        {
896            Some(self.previous_job_index)
897        } else {
898            None
899        }
900    }
901
902    /// Finds a suspended job other than the current job.
903    fn any_suspended_job_but_current(&self) -> Option<usize> {
904        self.iter()
905            .filter(|&(index, job)| index != self.current_job_index && job.is_suspended())
906            .map(|(index, _)| index)
907            .next()
908    }
909
910    /// Finds a job other than the current job.
911    fn any_job_but_current(&self) -> Option<usize> {
912        self.iter()
913            .filter(|&(index, _)| index != self.current_job_index)
914            .map(|(index, _)| index)
915            .next()
916    }
917}
918
919impl JobList {
920    /// Returns the process ID of the most recently executed asynchronous
921    /// command.
922    ///
923    /// This function returns the value that has been set by
924    /// [`set_last_async_pid`](Self::set_last_async_pid), or 0 if no value has
925    /// been set.
926    pub fn last_async_pid(&self) -> Pid {
927        self.last_async_pid
928    }
929
930    /// Sets the process ID of the most recently executed asynchronous command.
931    ///
932    /// This function affects the result of
933    /// [`last_async_pid`](Self::last_async_pid).
934    pub fn set_last_async_pid(&mut self, pid: Pid) {
935        self.last_async_pid = pid;
936    }
937}
938
939/// Adds a job if the process is suspended.
940///
941/// This is a convenience function for handling the result of
942/// [`Subshell::start_and_wait`](crate::subshell::Config::start_and_wait).
943///
944/// If the process result indicates that the process is stopped, this function
945/// adds a job to the job list in the environment. The job is marked as
946/// job-controlled and its state is derived from the process result. The job
947/// name is set to the result of the `name` closure. If the current environment
948/// is interactive, this function returns
949/// `Break(Divert::Interrupt(Some(exit_status)))` to indicate that the shell
950/// should be interrupted.
951///
952/// If the process is not stopped, this function does not add a job.
953///
954/// Returns the exit status of the process that should be assigned to
955/// `env.exit_status`.
956#[deprecated(since = "0.15.0", note = "use `handle_job_status` instead")]
957pub fn add_job_if_suspended<S, F>(
958    env: &mut Env<S>,
959    pid: Pid,
960    result: ProcessResult,
961    name: F,
962) -> crate::semantics::Result<ExitStatus>
963where
964    F: FnOnce() -> String,
965{
966    let exit_status = result.into();
967
968    if result.is_stopped() {
969        let mut job = Job::new(pid);
970        job.job_controlled = true;
971        job.state = result.into();
972        job.name = name();
973        env.jobs.insert(job);
974
975        if env.is_interactive() {
976            return Break(Divert::Interrupt(Some(exit_status)));
977        }
978    }
979
980    Continue(exit_status)
981}
982
983/// Handles an interrupted or suspended job.
984///
985/// This function is a convenience function for handling the result of
986/// [`Subshell::start_and_wait`](crate::subshell::Config::start_and_wait), which
987/// starts a process and returns its result.
988///
989/// If the current environment is [interactive](Env::is_interactive), the trap
990/// action for SIGINT is [`Action::Default`], and the process result indicates
991/// that the process was terminated by SIGINT, then this function returns
992/// `Break(Divert::Interrupt(Some(result.into())))`.
993///
994/// If the process result indicates that the process is stopped, this function
995/// [inserts a job to the job list](JobList::insert) in the environment.
996/// The job is marked as job-controlled and its state is derived from the process
997/// result. The job name is set to the result of the `job_name` closure, which
998/// is called only if the job is inserted. If the current environment is
999/// interactive, this function returns
1000/// `Break(Divert::Interrupt(Some(result.into())))`.
1001///
1002/// This function returns the exit status of the process derived from the
1003/// process result (`result.into()`) that should be assigned to
1004/// `env.exit_status`, except for the cases of interactive interruption and
1005/// suspension described above.
1006pub fn handle_job_status<S, F>(
1007    env: &mut Env<S>,
1008    pid: Pid,
1009    result: ProcessResult,
1010    job_name: F,
1011) -> crate::semantics::Result<ExitStatus>
1012where
1013    S: Signals,
1014    F: FnOnce() -> String,
1015{
1016    let exit_status = result.into();
1017
1018    if result.is_stopped() {
1019        let mut job = Job::new(pid);
1020        job.job_controlled = true;
1021        job.state = result.into();
1022        job.name = job_name();
1023        env.jobs.insert(job);
1024
1025        if env.is_interactive() {
1026            return Break(Divert::Interrupt(Some(exit_status)));
1027        }
1028    }
1029
1030    if let ProcessResult::Signaled { signal, .. } = result
1031        && signal == S::SIGINT
1032        // Unlike yash_semantics::trap::run_traps_for_caught_signals,
1033        // we cannot omit the check for interactivity here.
1034        && env.is_interactive()
1035        && env.sigint_has_default_action()
1036    {
1037        return Break(Divert::Interrupt(Some(exit_status)));
1038    }
1039
1040    Continue(exit_status)
1041}
1042
1043pub mod fmt;
1044pub mod id;
1045mod tcsetpgrp;
1046
1047pub use self::tcsetpgrp::*;
1048
1049#[cfg(test)]
1050mod tests {
1051    use super::*;
1052    use crate::option::Option::Interactive;
1053    use crate::option::State::On;
1054    use crate::signal;
1055    use crate::system::r#virtual::{SIGINT, SIGSTOP, SIGTERM, SIGTSTP, SIGTTIN, SIGTTOU};
1056    use std::num::NonZero;
1057
1058    #[test]
1059    fn job_list_find_by_pid() {
1060        let mut list = JobList::default();
1061        assert_eq!(list.find_by_pid(Pid(10)), None);
1062
1063        let i10 = list.insert(Job::new(Pid(10)));
1064        let i20 = list.insert(Job::new(Pid(20)));
1065        let i30 = list.insert(Job::new(Pid(30)));
1066        assert_eq!(list.find_by_pid(Pid(10)), Some(i10));
1067        assert_eq!(list.find_by_pid(Pid(20)), Some(i20));
1068        assert_eq!(list.find_by_pid(Pid(30)), Some(i30));
1069        assert_eq!(list.find_by_pid(Pid(40)), None);
1070
1071        list.remove(i10);
1072        assert_eq!(list.find_by_pid(Pid(10)), None);
1073    }
1074
1075    #[test]
1076    fn job_list_add_and_remove() {
1077        // This test case depends on how Slab reuses the index of removed items.
1078        let mut list = JobList::default();
1079
1080        assert_eq!(list.insert(Job::new(Pid(10))), 0);
1081        assert_eq!(list.insert(Job::new(Pid(11))), 1);
1082        assert_eq!(list.insert(Job::new(Pid(12))), 2);
1083
1084        assert_eq!(list.remove(0).unwrap().pid, Pid(10));
1085        assert_eq!(list.remove(1).unwrap().pid, Pid(11));
1086
1087        // Indices are reused in the reverse order of removals.
1088        assert_eq!(list.insert(Job::new(Pid(13))), 1);
1089        assert_eq!(list.insert(Job::new(Pid(14))), 0);
1090
1091        assert_eq!(list.remove(0).unwrap().pid, Pid(14));
1092        assert_eq!(list.remove(1).unwrap().pid, Pid(13));
1093        assert_eq!(list.remove(2).unwrap().pid, Pid(12));
1094
1095        // Once the job list is empty, indices start from 0 again.
1096        assert_eq!(list.insert(Job::new(Pid(13))), 0);
1097        assert_eq!(list.insert(Job::new(Pid(14))), 1);
1098    }
1099
1100    #[test]
1101    fn job_list_add_same_pid() {
1102        let mut list = JobList::default();
1103
1104        let mut job = Job::new(Pid(10));
1105        job.name = "first job".to_string();
1106        let i_first = list.insert(job);
1107
1108        let mut job = Job::new(Pid(10));
1109        job.name = "second job".to_string();
1110        let i_second = list.insert(job);
1111
1112        let job = &list[i_second];
1113        assert_eq!(job.pid, Pid(10));
1114        assert_eq!(job.name, "second job");
1115
1116        assert_ne!(
1117            list.get(i_first).map(|job| job.name.as_str()),
1118            Some("first job")
1119        );
1120    }
1121
1122    #[test]
1123    fn job_list_extract_if() {
1124        let mut list = JobList::default();
1125        let i21 = list.insert(Job::new(Pid(21)));
1126        let i22 = list.insert(Job::new(Pid(22)));
1127        let i23 = list.insert(Job::new(Pid(23)));
1128        let i24 = list.insert(Job::new(Pid(24)));
1129        let i25 = list.insert(Job::new(Pid(25)));
1130        let i26 = list.insert(Job::new(Pid(26)));
1131        list.remove(i23).unwrap();
1132
1133        let mut i = list.extract_if(|index, mut job| {
1134            assert_ne!(index, i23);
1135            if index % 2 == 0 {
1136                job.state_reported();
1137            }
1138            index == 0 || job.pid == Pid(26)
1139        });
1140
1141        let mut expected_job_21 = Job::new(Pid(21));
1142        expected_job_21.state_changed = false;
1143        assert_eq!(i.next(), Some((i21, expected_job_21)));
1144        assert_eq!(i.next(), Some((i26, Job::new(Pid(26)))));
1145        assert_eq!(i.next(), None);
1146        assert_eq!(i.next(), None); // ExtractIf is fused.
1147
1148        let indices: Vec<usize> = list.iter().map(|(index, _)| index).collect();
1149        assert_eq!(indices, [i22, i24, i25]);
1150        assert!(list[i22].state_changed);
1151        assert!(list[i24].state_changed);
1152        assert!(!list[i25].state_changed);
1153    }
1154
1155    #[test]
1156    #[allow(
1157        clippy::bool_assert_comparison,
1158        reason = "to make the expected values clearer"
1159    )]
1160    fn updating_job_status_without_expected_state() {
1161        let mut list = JobList::default();
1162        let state = ProcessState::exited(15);
1163        assert_eq!(list.update_status(Pid(20), state), None);
1164
1165        let i10 = list.insert(Job::new(Pid(10)));
1166        let i20 = list.insert(Job::new(Pid(20)));
1167        let i30 = list.insert(Job::new(Pid(30)));
1168        assert_eq!(list[i20].state, ProcessState::Running);
1169
1170        list.get_mut(i20).unwrap().state_reported();
1171        assert_eq!(list[i20].state_changed, false);
1172
1173        assert_eq!(list.update_status(Pid(20), state), Some(i20));
1174        assert_eq!(list[i20].state, ProcessState::exited(15));
1175        assert_eq!(list[i20].state_changed, true);
1176
1177        assert_eq!(list[i10].state, ProcessState::Running);
1178        assert_eq!(list[i30].state, ProcessState::Running);
1179    }
1180
1181    #[test]
1182    #[allow(
1183        clippy::bool_assert_comparison,
1184        reason = "to make the expected values clearer"
1185    )]
1186    fn updating_job_status_with_matching_expected_state() {
1187        let mut list = JobList::default();
1188        let pid = Pid(20);
1189        let mut job = Job::new(pid);
1190        job.expected_state = Some(ProcessState::Running);
1191        job.state_changed = false;
1192        let i20 = list.insert(job);
1193
1194        assert_eq!(list.update_status(pid, ProcessState::Running), Some(i20));
1195
1196        let job = &list[i20];
1197        assert_eq!(job.state, ProcessState::Running);
1198        assert_eq!(job.expected_state, None);
1199        assert_eq!(job.state_changed, false);
1200    }
1201
1202    #[test]
1203    #[allow(
1204        clippy::bool_assert_comparison,
1205        reason = "to make the expected values clearer"
1206    )]
1207    fn updating_job_status_with_unmatched_expected_state() {
1208        let mut list = JobList::default();
1209        let pid = Pid(20);
1210        let mut job = Job::new(pid);
1211        job.expected_state = Some(ProcessState::Running);
1212        job.state_changed = false;
1213        let i20 = list.insert(job);
1214
1215        let result = list.update_status(pid, ProcessState::exited(0));
1216        assert_eq!(result, Some(i20));
1217
1218        let job = &list[i20];
1219        assert_eq!(job.state, ProcessState::exited(0));
1220        assert_eq!(job.expected_state, None);
1221        assert_eq!(job.state_changed, true);
1222    }
1223
1224    #[test]
1225    #[allow(
1226        clippy::bool_assert_comparison,
1227        reason = "to make the expected values clearer"
1228    )]
1229    fn disowning_jobs() {
1230        let mut list = JobList::default();
1231        let i10 = list.insert(Job::new(Pid(10)));
1232        let i20 = list.insert(Job::new(Pid(20)));
1233        let i30 = list.insert(Job::new(Pid(30)));
1234
1235        list.disown_all();
1236
1237        assert_eq!(list[i10].is_owned, false);
1238        assert_eq!(list[i20].is_owned, false);
1239        assert_eq!(list[i30].is_owned, false);
1240    }
1241
1242    #[test]
1243    fn no_current_and_previous_job_in_empty_job_list() {
1244        let list = JobList::default();
1245        assert_eq!(list.current_job(), None);
1246        assert_eq!(list.previous_job(), None);
1247    }
1248
1249    #[test]
1250    fn current_and_previous_job_in_job_list_with_one_job() {
1251        let mut list = JobList::default();
1252        let i10 = list.insert(Job::new(Pid(10)));
1253        assert_eq!(list.current_job(), Some(i10));
1254        assert_eq!(list.previous_job(), None);
1255    }
1256
1257    #[test]
1258    fn current_and_previous_job_in_job_list_with_two_job() {
1259        // If one job is suspended and the other is not, the current job is the
1260        // suspended one.
1261        let mut list = JobList::default();
1262        let mut suspended = Job::new(Pid(10));
1263        suspended.state = ProcessState::stopped(SIGSTOP);
1264        let running = Job::new(Pid(20));
1265        let i10 = list.insert(suspended.clone());
1266        let i20 = list.insert(running.clone());
1267        assert_eq!(list.current_job(), Some(i10));
1268        assert_eq!(list.previous_job(), Some(i20));
1269
1270        // The order of adding jobs does not matter in this case.
1271        list = JobList::default();
1272        let i20 = list.insert(running);
1273        let i10 = list.insert(suspended);
1274        assert_eq!(list.current_job(), Some(i10));
1275        assert_eq!(list.previous_job(), Some(i20));
1276    }
1277
1278    #[test]
1279    fn adding_suspended_job_with_running_current_and_previous_job() {
1280        let mut list = JobList::default();
1281        let running_1 = Job::new(Pid(11));
1282        let running_2 = Job::new(Pid(12));
1283        list.insert(running_1);
1284        list.insert(running_2);
1285        let ex_current_job_index = list.current_job().unwrap();
1286        let ex_previous_job_index = list.previous_job().unwrap();
1287        assert_ne!(ex_current_job_index, ex_previous_job_index);
1288
1289        let mut suspended = Job::new(Pid(20));
1290        suspended.state = ProcessState::stopped(SIGSTOP);
1291        let i20 = list.insert(suspended);
1292        let now_current_job_index = list.current_job().unwrap();
1293        let now_previous_job_index = list.previous_job().unwrap();
1294        assert_eq!(now_current_job_index, i20);
1295        assert_eq!(now_previous_job_index, ex_current_job_index);
1296    }
1297
1298    #[test]
1299    fn adding_suspended_job_with_suspended_current_and_running_previous_job() {
1300        let mut list = JobList::default();
1301
1302        let running = Job::new(Pid(18));
1303        let i18 = list.insert(running);
1304
1305        let mut suspended_1 = Job::new(Pid(19));
1306        suspended_1.state = ProcessState::stopped(SIGSTOP);
1307        let i19 = list.insert(suspended_1);
1308
1309        let ex_current_job_index = list.current_job().unwrap();
1310        let ex_previous_job_index = list.previous_job().unwrap();
1311        assert_eq!(ex_current_job_index, i19);
1312        assert_eq!(ex_previous_job_index, i18);
1313
1314        let mut suspended_2 = Job::new(Pid(20));
1315        suspended_2.state = ProcessState::stopped(SIGSTOP);
1316        let i20 = list.insert(suspended_2);
1317
1318        let now_current_job_index = list.current_job().unwrap();
1319        let now_previous_job_index = list.previous_job().unwrap();
1320        assert_eq!(now_current_job_index, ex_current_job_index);
1321        assert_eq!(now_previous_job_index, i20);
1322    }
1323
1324    #[test]
1325    fn removing_current_job() {
1326        let mut list = JobList::default();
1327
1328        let running = Job::new(Pid(10));
1329        let i10 = list.insert(running);
1330
1331        let mut suspended_1 = Job::new(Pid(11));
1332        let mut suspended_2 = Job::new(Pid(12));
1333        let mut suspended_3 = Job::new(Pid(13));
1334        suspended_1.state = ProcessState::stopped(SIGSTOP);
1335        suspended_2.state = ProcessState::stopped(SIGSTOP);
1336        suspended_3.state = ProcessState::stopped(SIGSTOP);
1337        list.insert(suspended_1);
1338        list.insert(suspended_2);
1339        list.insert(suspended_3);
1340
1341        let current_job_index_1 = list.current_job().unwrap();
1342        let previous_job_index_1 = list.previous_job().unwrap();
1343        assert_ne!(current_job_index_1, i10);
1344        assert_ne!(previous_job_index_1, i10);
1345
1346        list.remove(current_job_index_1);
1347        let current_job_index_2 = list.current_job().unwrap();
1348        let previous_job_index_2 = list.previous_job().unwrap();
1349        assert_eq!(current_job_index_2, previous_job_index_1);
1350        assert_ne!(previous_job_index_2, current_job_index_2);
1351        // The new previous job is chosen from suspended jobs other than the current job.
1352        let previous_job_2 = &list[previous_job_index_2];
1353        assert!(
1354            previous_job_2.is_suspended(),
1355            "previous_job_2 = {previous_job_2:?}"
1356        );
1357
1358        list.remove(current_job_index_2);
1359        let current_job_index_3 = list.current_job().unwrap();
1360        let previous_job_index_3 = list.previous_job().unwrap();
1361        assert_eq!(current_job_index_3, previous_job_index_2);
1362        // There is no other suspended job, so the new previous job is a running job.
1363        assert_eq!(previous_job_index_3, i10);
1364
1365        list.remove(current_job_index_3);
1366        let current_job_index_4 = list.current_job().unwrap();
1367        assert_eq!(current_job_index_4, i10);
1368        // No more job to be selected for the previous job.
1369        assert_eq!(list.previous_job(), None);
1370    }
1371
1372    #[test]
1373    fn removing_previous_job_with_suspended_job() {
1374        let mut list = JobList::default();
1375
1376        let running = Job::new(Pid(10));
1377        let i10 = list.insert(running);
1378
1379        let mut suspended_1 = Job::new(Pid(11));
1380        let mut suspended_2 = Job::new(Pid(12));
1381        let mut suspended_3 = Job::new(Pid(13));
1382        suspended_1.state = ProcessState::stopped(SIGSTOP);
1383        suspended_2.state = ProcessState::stopped(SIGSTOP);
1384        suspended_3.state = ProcessState::stopped(SIGSTOP);
1385        list.insert(suspended_1);
1386        list.insert(suspended_2);
1387        list.insert(suspended_3);
1388
1389        let ex_current_job_index = list.current_job().unwrap();
1390        let ex_previous_job_index = list.previous_job().unwrap();
1391        assert_ne!(ex_current_job_index, i10);
1392        assert_ne!(ex_previous_job_index, i10);
1393
1394        list.remove(ex_previous_job_index);
1395        let now_current_job_index = list.current_job().unwrap();
1396        let now_previous_job_index = list.previous_job().unwrap();
1397        assert_eq!(now_current_job_index, ex_current_job_index);
1398        assert_ne!(now_previous_job_index, now_current_job_index);
1399        // The new previous job is chosen from suspended jobs other than the current job.
1400        let now_previous_job = &list[now_previous_job_index];
1401        assert!(
1402            now_previous_job.is_suspended(),
1403            "now_previous_job = {now_previous_job:?}"
1404        );
1405    }
1406
1407    #[test]
1408    fn removing_previous_job_with_running_job() {
1409        let mut list = JobList::default();
1410
1411        let running = Job::new(Pid(10));
1412        let i10 = list.insert(running);
1413
1414        let mut suspended_1 = Job::new(Pid(11));
1415        let mut suspended_2 = Job::new(Pid(12));
1416        suspended_1.state = ProcessState::stopped(SIGSTOP);
1417        suspended_2.state = ProcessState::stopped(SIGSTOP);
1418        list.insert(suspended_1);
1419        list.insert(suspended_2);
1420
1421        let ex_current_job_index = list.current_job().unwrap();
1422        let ex_previous_job_index = list.previous_job().unwrap();
1423        assert_ne!(ex_current_job_index, i10);
1424        assert_ne!(ex_previous_job_index, i10);
1425
1426        list.remove(ex_previous_job_index);
1427        let now_current_job_index = list.current_job().unwrap();
1428        let now_previous_job_index = list.previous_job().unwrap();
1429        assert_eq!(now_current_job_index, ex_current_job_index);
1430        // When there is no suspended job other than the current job,
1431        // then the new previous job can be any job other than the current.
1432        assert_eq!(now_previous_job_index, i10);
1433    }
1434
1435    #[test]
1436    fn set_current_job_with_running_jobs_only() {
1437        let mut list = JobList::default();
1438        let i21 = list.insert(Job::new(Pid(21)));
1439        let i22 = list.insert(Job::new(Pid(22)));
1440
1441        assert_eq!(list.set_current_job(i21), Ok(()));
1442        assert_eq!(list.current_job(), Some(i21));
1443
1444        assert_eq!(list.set_current_job(i22), Ok(()));
1445        assert_eq!(list.current_job(), Some(i22));
1446    }
1447
1448    #[test]
1449    fn set_current_job_to_suspended_job() {
1450        let mut list = JobList::default();
1451        list.insert(Job::new(Pid(20)));
1452
1453        let mut suspended_1 = Job::new(Pid(21));
1454        let mut suspended_2 = Job::new(Pid(22));
1455        suspended_1.state = ProcessState::stopped(SIGSTOP);
1456        suspended_2.state = ProcessState::stopped(SIGSTOP);
1457        let i21 = list.insert(suspended_1);
1458        let i22 = list.insert(suspended_2);
1459
1460        assert_eq!(list.set_current_job(i21), Ok(()));
1461        assert_eq!(list.current_job(), Some(i21));
1462
1463        assert_eq!(list.set_current_job(i22), Ok(()));
1464        assert_eq!(list.current_job(), Some(i22));
1465    }
1466
1467    #[test]
1468    fn set_current_job_no_such_job() {
1469        let mut list = JobList::default();
1470        assert_eq!(list.set_current_job(0), Err(SetCurrentJobError::NoSuchJob));
1471        assert_eq!(list.set_current_job(1), Err(SetCurrentJobError::NoSuchJob));
1472        assert_eq!(list.set_current_job(2), Err(SetCurrentJobError::NoSuchJob));
1473    }
1474
1475    #[test]
1476    fn set_current_job_not_suspended() {
1477        let mut list = JobList::default();
1478        let mut suspended = Job::new(Pid(10));
1479        suspended.state = ProcessState::stopped(SIGTSTP);
1480        let running = Job::new(Pid(20));
1481        let i10 = list.insert(suspended);
1482        let i20 = list.insert(running);
1483        assert_eq!(
1484            list.set_current_job(i20),
1485            Err(SetCurrentJobError::NotSuspended)
1486        );
1487        assert_eq!(list.current_job(), Some(i10));
1488    }
1489
1490    #[test]
1491    fn set_current_job_no_change() {
1492        let mut list = JobList::default();
1493        list.insert(Job::new(Pid(5)));
1494        list.insert(Job::new(Pid(6)));
1495        let old_current_job_index = list.current_job().unwrap();
1496        let old_previous_job_index = list.previous_job().unwrap();
1497        list.set_current_job(old_current_job_index).unwrap();
1498        let new_current_job_index = list.current_job().unwrap();
1499        let new_previous_job_index = list.previous_job().unwrap();
1500        assert_eq!(new_current_job_index, old_current_job_index);
1501        assert_eq!(new_previous_job_index, old_previous_job_index);
1502    }
1503
1504    #[test]
1505    fn resuming_current_job_without_other_suspended_jobs() {
1506        let mut list = JobList::default();
1507        let mut suspended = Job::new(Pid(10));
1508        suspended.state = ProcessState::stopped(SIGTSTP);
1509        let running = Job::new(Pid(20));
1510        let i10 = list.insert(suspended);
1511        let i20 = list.insert(running);
1512        list.update_status(Pid(10), ProcessState::Running);
1513        assert_eq!(list.current_job(), Some(i10));
1514        assert_eq!(list.previous_job(), Some(i20));
1515    }
1516
1517    #[test]
1518    fn resuming_current_job_with_another_suspended_job() {
1519        let mut list = JobList::default();
1520        let mut suspended_1 = Job::new(Pid(10));
1521        let mut suspended_2 = Job::new(Pid(20));
1522        suspended_1.state = ProcessState::stopped(SIGTSTP);
1523        suspended_2.state = ProcessState::stopped(SIGTSTP);
1524        let i10 = list.insert(suspended_1);
1525        let i20 = list.insert(suspended_2);
1526        list.set_current_job(i10).unwrap();
1527        list.update_status(Pid(10), ProcessState::Running);
1528        // The current job must be a suspended job, if any.
1529        assert_eq!(list.current_job(), Some(i20));
1530        assert_eq!(list.previous_job(), Some(i10));
1531    }
1532
1533    #[test]
1534    fn resuming_current_job_with_other_suspended_jobs() {
1535        let mut list = JobList::default();
1536        let mut suspended_1 = Job::new(Pid(10));
1537        let mut suspended_2 = Job::new(Pid(20));
1538        let mut suspended_3 = Job::new(Pid(30));
1539        suspended_1.state = ProcessState::stopped(SIGTSTP);
1540        suspended_2.state = ProcessState::stopped(SIGTSTP);
1541        suspended_3.state = ProcessState::stopped(SIGTSTP);
1542        list.insert(suspended_1);
1543        list.insert(suspended_2);
1544        list.insert(suspended_3);
1545        let ex_current_job_pid = list[list.current_job().unwrap()].pid;
1546        let ex_previous_job_index = list.previous_job().unwrap();
1547
1548        list.update_status(ex_current_job_pid, ProcessState::Running);
1549        let now_current_job_index = list.current_job().unwrap();
1550        let now_previous_job_index = list.previous_job().unwrap();
1551        assert_eq!(now_current_job_index, ex_previous_job_index);
1552        assert_ne!(now_previous_job_index, now_current_job_index);
1553        // The new previous job is chosen from suspended jobs other than the current job.
1554        let now_previous_job = &list[now_previous_job_index];
1555        assert!(
1556            now_previous_job.is_suspended(),
1557            "now_previous_job = {now_previous_job:?}"
1558        );
1559    }
1560
1561    #[test]
1562    fn resuming_previous_job() {
1563        let mut list = JobList::default();
1564        let mut suspended_1 = Job::new(Pid(10));
1565        let mut suspended_2 = Job::new(Pid(20));
1566        let mut suspended_3 = Job::new(Pid(30));
1567        suspended_1.state = ProcessState::stopped(SIGTSTP);
1568        suspended_2.state = ProcessState::stopped(SIGTSTP);
1569        suspended_3.state = ProcessState::stopped(SIGTSTP);
1570        list.insert(suspended_1);
1571        list.insert(suspended_2);
1572        list.insert(suspended_3);
1573        let ex_current_job_index = list.current_job().unwrap();
1574        let ex_previous_job_pid = list[list.previous_job().unwrap()].pid;
1575
1576        list.update_status(ex_previous_job_pid, ProcessState::Running);
1577        let now_current_job_index = list.current_job().unwrap();
1578        let now_previous_job_index = list.previous_job().unwrap();
1579        assert_eq!(now_current_job_index, ex_current_job_index);
1580        assert_ne!(now_previous_job_index, now_current_job_index);
1581        // The new previous job is chosen from suspended jobs other than the current job.
1582        let now_previous_job = &list[now_previous_job_index];
1583        assert!(
1584            now_previous_job.is_suspended(),
1585            "now_previous_job = {now_previous_job:?}"
1586        );
1587    }
1588
1589    #[test]
1590    fn resuming_other_job() {
1591        let mut list = JobList::default();
1592        let mut suspended_1 = Job::new(Pid(10));
1593        let mut suspended_2 = Job::new(Pid(20));
1594        let mut suspended_3 = Job::new(Pid(30));
1595        suspended_1.state = ProcessState::stopped(SIGTSTP);
1596        suspended_2.state = ProcessState::stopped(SIGTSTP);
1597        suspended_3.state = ProcessState::stopped(SIGTSTP);
1598        let i10 = list.insert(suspended_1);
1599        let i20 = list.insert(suspended_2);
1600        let _i30 = list.insert(suspended_3);
1601        list.set_current_job(i20).unwrap();
1602        list.set_current_job(i10).unwrap();
1603        list.update_status(Pid(30), ProcessState::Running);
1604        assert_eq!(list.current_job(), Some(i10));
1605        assert_eq!(list.previous_job(), Some(i20));
1606    }
1607
1608    #[test]
1609    fn suspending_current_job() {
1610        let mut list = JobList::default();
1611        let i11 = list.insert(Job::new(Pid(11)));
1612        let i12 = list.insert(Job::new(Pid(12)));
1613        list.set_current_job(i11).unwrap();
1614        list.update_status(Pid(11), ProcessState::stopped(SIGTTOU));
1615        assert_eq!(list.current_job(), Some(i11));
1616        assert_eq!(list.previous_job(), Some(i12));
1617    }
1618
1619    #[test]
1620    fn suspending_previous_job() {
1621        let mut list = JobList::default();
1622        let i11 = list.insert(Job::new(Pid(11)));
1623        let i12 = list.insert(Job::new(Pid(12)));
1624        list.set_current_job(i11).unwrap();
1625        list.update_status(Pid(12), ProcessState::stopped(SIGTTOU));
1626        assert_eq!(list.current_job(), Some(i12));
1627        assert_eq!(list.previous_job(), Some(i11));
1628    }
1629
1630    #[test]
1631    fn suspending_job_with_running_current_job() {
1632        let mut list = JobList::default();
1633        let i10 = list.insert(Job::new(Pid(10)));
1634        let _i11 = list.insert(Job::new(Pid(11)));
1635        let i12 = list.insert(Job::new(Pid(12)));
1636        list.set_current_job(i10).unwrap();
1637        list.update_status(Pid(12), ProcessState::stopped(SIGTTIN));
1638        assert_eq!(list.current_job(), Some(i12));
1639        assert_eq!(list.previous_job(), Some(i10));
1640    }
1641
1642    #[test]
1643    fn suspending_job_with_running_previous_job() {
1644        let mut list = JobList::default();
1645        let i11 = list.insert(Job::new(Pid(11)));
1646        let i12 = list.insert(Job::new(Pid(12)));
1647        let mut suspended = Job::new(Pid(10));
1648        suspended.state = ProcessState::stopped(SIGTTIN);
1649        let i10 = list.insert(suspended);
1650        assert_eq!(list.current_job(), Some(i10));
1651        assert_eq!(list.previous_job(), Some(i11));
1652
1653        list.update_status(Pid(12), ProcessState::stopped(SIGTTOU));
1654        assert_eq!(list.current_job(), Some(i12));
1655        assert_eq!(list.previous_job(), Some(i10));
1656    }
1657
1658    #[allow(deprecated, reason = "to test the deprecated function")]
1659    mod add_job_if_suspended {
1660        use super::*;
1661
1662        #[test]
1663        fn do_not_add_job_if_exited() {
1664            let mut env = Env::new_virtual();
1665            let result = add_job_if_suspended(
1666                &mut env,
1667                Pid(123),
1668                ProcessResult::Exited(ExitStatus(42)),
1669                || "foo".to_string(),
1670            );
1671            assert_eq!(result, Continue(ExitStatus(42)));
1672            assert_eq!(env.jobs.len(), 0);
1673        }
1674
1675        #[test]
1676        fn do_not_add_job_if_signaled() {
1677            let mut env = Env::new_virtual();
1678            let signal = signal::Number::from_raw_unchecked(NonZero::new(42).unwrap());
1679            let result = add_job_if_suspended(
1680                &mut env,
1681                Pid(123),
1682                ProcessResult::Signaled {
1683                    signal,
1684                    core_dump: false,
1685                },
1686                || "foo".to_string(),
1687            );
1688            assert_eq!(result, Continue(ExitStatus::from(signal)));
1689            assert_eq!(env.jobs.len(), 0);
1690        }
1691
1692        #[test]
1693        fn add_job_if_stopped() {
1694            let mut env = Env::new_virtual();
1695            let signal = signal::Number::from_raw_unchecked(NonZero::new(42).unwrap());
1696            let process_result = ProcessResult::Stopped(signal);
1697            let result =
1698                add_job_if_suspended(&mut env, Pid(123), process_result, || "foo".to_string());
1699            assert_eq!(result, Continue(ExitStatus::from(signal)));
1700            assert_eq!(env.jobs.len(), 1);
1701            let job = env.jobs.get(0).unwrap();
1702            assert_eq!(job.pid, Pid(123));
1703            assert!(job.job_controlled);
1704            assert_eq!(job.state, ProcessState::Halted(process_result));
1705            assert_eq!(job.name, "foo");
1706        }
1707
1708        #[test]
1709        fn break_if_stopped_and_interactive() {
1710            let mut env = Env::new_virtual();
1711            env.options.set(Interactive, On);
1712            let signal = signal::Number::from_raw_unchecked(NonZero::new(42).unwrap());
1713            let process_result = ProcessResult::Stopped(signal);
1714            let result =
1715                add_job_if_suspended(&mut env, Pid(123), process_result, || "foo".to_string());
1716            assert_eq!(
1717                result,
1718                Break(Divert::Interrupt(Some(ExitStatus::from(signal))))
1719            );
1720            assert_eq!(env.jobs.len(), 1);
1721        }
1722    }
1723
1724    mod handle_job_status {
1725        use super::*;
1726        use crate::source::Location;
1727        use futures_util::FutureExt as _;
1728
1729        #[test]
1730        fn do_not_insert_job_if_exited() {
1731            let mut env = Env::new_virtual();
1732            let result = handle_job_status(
1733                &mut env,
1734                Pid(123),
1735                ProcessResult::Exited(ExitStatus(42)),
1736                || unreachable!(),
1737            );
1738            assert_eq!(result, Continue(ExitStatus(42)));
1739            assert_eq!(env.jobs.len(), 0);
1740        }
1741
1742        #[test]
1743        fn do_not_insert_job_if_signaled() {
1744            let mut env = Env::new_virtual();
1745            let signal = signal::Number::from_raw_unchecked(NonZero::new(42).unwrap());
1746            let result = handle_job_status(
1747                &mut env,
1748                Pid(123),
1749                ProcessResult::Signaled {
1750                    signal,
1751                    core_dump: false,
1752                },
1753                || unreachable!(),
1754            );
1755            assert_eq!(result, Continue(ExitStatus::from(signal)));
1756            assert_eq!(env.jobs.len(), 0);
1757        }
1758
1759        #[test]
1760        fn insert_job_if_stopped() {
1761            let mut env = Env::new_virtual();
1762            let signal = signal::Number::from_raw_unchecked(NonZero::new(42).unwrap());
1763            let process_result = ProcessResult::Stopped(signal);
1764            let result =
1765                handle_job_status(&mut env, Pid(123), process_result, || "foo".to_string());
1766            assert_eq!(result, Continue(ExitStatus::from(signal)));
1767            assert_eq!(env.jobs.len(), 1);
1768            let job = &env.jobs[0];
1769            assert_eq!(job.pid, Pid(123));
1770            assert!(job.job_controlled);
1771            assert_eq!(job.state, ProcessState::Halted(process_result));
1772            assert_eq!(job.name, "foo");
1773        }
1774
1775        #[test]
1776        fn interrupt_if_stopped_and_interactive() {
1777            let mut env = Env::new_virtual();
1778            env.options.set(Interactive, On);
1779            let signal = signal::Number::from_raw_unchecked(NonZero::new(42).unwrap());
1780            let process_result = ProcessResult::Stopped(signal);
1781            let result =
1782                handle_job_status(&mut env, Pid(123), process_result, || "foo".to_string());
1783            assert_eq!(
1784                result,
1785                Break(Divert::Interrupt(Some(ExitStatus::from(signal))))
1786            );
1787            assert_eq!(env.jobs.len(), 1);
1788        }
1789
1790        #[test]
1791        fn do_not_interrupt_if_stopped_but_non_interactive() {
1792            let mut env = Env::new_virtual();
1793            // non-interactive by default (Interactive option not set)
1794            let process_result = ProcessResult::Stopped(SIGTSTP);
1795            let result =
1796                handle_job_status(&mut env, Pid(123), process_result, || "job".to_string());
1797            assert_eq!(result, Continue(ExitStatus::from(SIGTSTP)));
1798        }
1799
1800        #[test]
1801        fn interrupt_interactive_shell_for_default_sigint_action() {
1802            let mut env = Env::new_virtual();
1803            env.options.set(Interactive, On);
1804            // SIGINT trap action is Action::Default by default
1805            let process_result = ProcessResult::Signaled {
1806                signal: SIGINT,
1807                core_dump: false,
1808            };
1809            let result = handle_job_status(&mut env, Pid(123), process_result, || unreachable!());
1810            assert_eq!(
1811                result,
1812                Break(Divert::Interrupt(Some(ExitStatus::from(SIGINT))))
1813            );
1814            assert_eq!(env.jobs.len(), 0);
1815        }
1816
1817        #[test]
1818        fn do_not_interrupt_if_signaled_by_other_than_sigint() {
1819            let mut env = Env::new_virtual();
1820            env.options.set(Interactive, On);
1821            let process_result = ProcessResult::Signaled {
1822                signal: SIGTERM,
1823                core_dump: false,
1824            };
1825            let result = handle_job_status(&mut env, Pid(123), process_result, || unreachable!());
1826            assert_eq!(result, Continue(ExitStatus::from(SIGTERM)));
1827            assert_eq!(env.jobs.len(), 0);
1828        }
1829
1830        #[test]
1831        fn do_not_interrupt_non_interactive_shell_for_default_sigint_action() {
1832            let mut env = Env::new_virtual();
1833            // non-interactive by default, SIGINT trap is Action::Default by default
1834            let process_result = ProcessResult::Signaled {
1835                signal: SIGINT,
1836                core_dump: false,
1837            };
1838            let result = handle_job_status(&mut env, Pid(123), process_result, || unreachable!());
1839            assert_eq!(result, Continue(ExitStatus::from(SIGINT)));
1840            assert_eq!(env.jobs.len(), 0);
1841        }
1842
1843        #[test]
1844        fn do_not_interrupt_interactive_shell_for_ignore_sigint_action() {
1845            let mut env = Env::new_virtual();
1846            env.options.set(Interactive, On);
1847            env.traps
1848                .set_action(
1849                    &env.system,
1850                    SIGINT,
1851                    Action::Ignore,
1852                    Location::dummy(""),
1853                    false,
1854                )
1855                .now_or_never()
1856                .unwrap()
1857                .unwrap();
1858            let process_result = ProcessResult::Signaled {
1859                signal: SIGINT,
1860                core_dump: false,
1861            };
1862            let result = handle_job_status(&mut env, Pid(123), process_result, || unreachable!());
1863            assert_eq!(result, Continue(ExitStatus::from(SIGINT)));
1864        }
1865
1866        #[test]
1867        fn do_not_interrupt_interactive_shell_for_custom_sigint_action() {
1868            let mut env = Env::new_virtual();
1869            env.options.set(Interactive, On);
1870            env.traps
1871                .set_action(
1872                    &env.system,
1873                    SIGINT,
1874                    Action::Command("echo interrupt".into()),
1875                    Location::dummy(""),
1876                    false,
1877                )
1878                .now_or_never()
1879                .unwrap()
1880                .unwrap();
1881            let process_result = ProcessResult::Signaled {
1882                signal: SIGINT,
1883                core_dump: false,
1884            };
1885            let result = handle_job_status(&mut env, Pid(123), process_result, || unreachable!());
1886            assert_eq!(result, Continue(ExitStatus::from(SIGINT)));
1887        }
1888    }
1889}