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