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