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