use crate::Env;
use crate::io::Fd;
use crate::semantics::{Divert, ExitStatus};
use crate::signal;
use crate::system::{Disposition, Sigaction, Sigmask, SigmaskOp, Signals, TcSetPgrp};
use slab::Slab;
use std::collections::HashMap;
use std::iter::FusedIterator;
use std::ops::ControlFlow::{Break, Continue};
use std::ops::Deref;
use thiserror::Error;
#[cfg(unix)]
type RawPidDef = libc::pid_t;
#[cfg(not(unix))]
type RawPidDef = i32;
pub type RawPid = RawPidDef;
#[repr(transparent)]
#[derive(Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
pub struct Pid(pub RawPid);
impl std::fmt::Display for Pid {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
self.0.fmt(f)
}
}
impl std::ops::Neg for Pid {
type Output = Self;
fn neg(self) -> Self {
Self(-self.0)
}
}
impl Pid {
pub const MY_PROCESS_GROUP: Self = Pid(0);
pub const ALL: Self = Pid(-1);
}
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub enum ProcessResult {
Stopped(signal::Number),
Exited(ExitStatus),
Signaled {
signal: signal::Number,
core_dump: bool,
},
}
impl ProcessResult {
#[inline]
#[must_use]
pub fn exited<S: Into<ExitStatus>>(exit_status: S) -> Self {
Self::Exited(exit_status.into())
}
#[must_use]
pub fn is_stopped(&self) -> bool {
matches!(self, ProcessResult::Stopped(_))
}
}
impl From<ProcessResult> for ExitStatus {
fn from(result: ProcessResult) -> Self {
match result {
ProcessResult::Exited(exit_status) => exit_status,
ProcessResult::Stopped(signal) | ProcessResult::Signaled { signal, .. } => {
ExitStatus::from(signal)
}
}
}
}
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub enum ProcessState {
Running,
Halted(ProcessResult),
}
impl ProcessState {
#[inline]
#[must_use]
pub fn stopped(signal: signal::Number) -> Self {
Self::Halted(ProcessResult::Stopped(signal))
}
#[inline]
#[must_use]
pub fn exited<S: Into<ExitStatus>>(exit_status: S) -> Self {
Self::Halted(ProcessResult::exited(exit_status))
}
#[must_use]
pub fn is_alive(&self) -> bool {
match self {
ProcessState::Running => true,
ProcessState::Halted(result) => result.is_stopped(),
}
}
#[must_use]
pub fn is_stopped(&self) -> bool {
matches!(self, Self::Halted(result) if result.is_stopped())
}
}
impl From<ProcessResult> for ProcessState {
#[inline]
fn from(result: ProcessResult) -> Self {
Self::Halted(result)
}
}
#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)]
pub struct RunningProcess;
impl TryFrom<ProcessState> for ExitStatus {
type Error = RunningProcess;
fn try_from(state: ProcessState) -> Result<Self, RunningProcess> {
match state {
ProcessState::Halted(result) => Ok(result.into()),
ProcessState::Running => Err(RunningProcess),
}
}
}
#[derive(Clone, Debug, Eq, PartialEq)]
#[non_exhaustive]
pub struct Job {
pub pid: Pid,
pub job_controlled: bool,
pub state: ProcessState,
pub expected_state: Option<ProcessState>,
pub state_changed: bool,
pub is_owned: bool,
pub name: String,
}
impl Job {
pub fn new(pid: Pid) -> Self {
Job {
pid,
job_controlled: false,
state: ProcessState::Running,
expected_state: None,
state_changed: true,
is_owned: true,
name: String::new(),
}
}
#[must_use]
fn is_suspended(&self) -> bool {
self.state.is_stopped()
}
}
#[derive(Debug, Eq, PartialEq)]
pub struct JobRefMut<'a>(&'a mut Job);
impl JobRefMut<'_> {
pub fn expect<S>(&mut self, state: S)
where
S: Into<Option<ProcessState>>,
{
self.0.expected_state = state.into();
}
pub fn state_reported(&mut self) {
self.0.state_changed = false
}
}
impl Deref for JobRefMut<'_> {
type Target = Job;
fn deref(&self) -> &Job {
self.0
}
}
#[derive(Clone, Debug)]
pub struct Iter<'a>(slab::Iter<'a, Job>);
impl<'a> Iterator for Iter<'a> {
type Item = (usize, &'a Job);
#[inline(always)]
fn next(&mut self) -> Option<(usize, &'a Job)> {
self.0.next()
}
#[inline(always)]
fn size_hint(&self) -> (usize, Option<usize>) {
self.0.size_hint()
}
}
impl<'a> DoubleEndedIterator for Iter<'a> {
#[inline(always)]
fn next_back(&mut self) -> Option<(usize, &'a Job)> {
self.0.next_back()
}
}
impl ExactSizeIterator for Iter<'_> {
#[inline(always)]
fn len(&self) -> usize {
self.0.len()
}
}
impl FusedIterator for Iter<'_> {}
#[derive(Debug)]
pub struct IterMut<'a>(slab::IterMut<'a, Job>);
impl<'a> Iterator for IterMut<'a> {
type Item = (usize, JobRefMut<'a>);
#[inline]
fn next(&mut self) -> Option<(usize, JobRefMut<'a>)> {
self.0.next().map(|(index, job)| (index, JobRefMut(job)))
}
#[inline(always)]
fn size_hint(&self) -> (usize, Option<usize>) {
self.0.size_hint()
}
}
impl<'a> DoubleEndedIterator for IterMut<'a> {
fn next_back(&mut self) -> Option<(usize, JobRefMut<'a>)> {
self.0
.next_back()
.map(|(index, job)| (index, JobRefMut(job)))
}
}
impl ExactSizeIterator for IterMut<'_> {
#[inline(always)]
fn len(&self) -> usize {
self.0.len()
}
}
impl FusedIterator for IterMut<'_> {}
#[derive(Clone, Debug)]
pub struct JobList {
jobs: Slab<Job>,
pids_to_indices: HashMap<Pid, usize>,
current_job_index: usize,
previous_job_index: usize,
last_async_pid: Pid,
}
impl Default for JobList {
fn default() -> Self {
JobList {
jobs: Slab::new(),
pids_to_indices: HashMap::new(),
current_job_index: usize::default(),
previous_job_index: usize::default(),
last_async_pid: Pid(0),
}
}
}
impl JobList {
#[inline]
#[must_use]
pub fn new() -> Self {
Self::default()
}
#[inline]
pub fn get(&self, index: usize) -> Option<&Job> {
self.jobs.get(index)
}
#[inline]
pub fn get_mut(&mut self, index: usize) -> Option<JobRefMut<'_>> {
self.jobs.get_mut(index).map(JobRefMut)
}
#[inline]
pub fn len(&self) -> usize {
self.jobs.len()
}
#[inline]
pub fn is_empty(&self) -> bool {
self.len() == 0
}
#[inline]
pub fn iter(&self) -> Iter<'_> {
Iter(self.jobs.iter())
}
#[inline]
pub fn iter_mut(&mut self) -> IterMut<'_> {
IterMut(self.jobs.iter_mut())
}
pub fn find_by_pid(&self, pid: Pid) -> Option<usize> {
self.pids_to_indices.get(&pid).copied()
}
}
impl<'a> IntoIterator for &'a JobList {
type Item = (usize, &'a Job);
type IntoIter = Iter<'a>;
#[inline(always)]
fn into_iter(self) -> Iter<'a> {
self.iter()
}
}
impl<'a> IntoIterator for &'a mut JobList {
type Item = (usize, JobRefMut<'a>);
type IntoIter = IterMut<'a>;
#[inline(always)]
fn into_iter(self) -> IterMut<'a> {
self.iter_mut()
}
}
impl std::ops::Index<usize> for JobList {
type Output = Job;
fn index(&self, index: usize) -> &Job {
&self.jobs[index]
}
}
#[derive(Debug)]
pub struct ExtractIf<'a, F>
where
F: FnMut(usize, JobRefMut) -> bool,
{
list: &'a mut JobList,
should_remove: F,
next_index: usize,
len: usize,
}
impl<F> Iterator for ExtractIf<'_, F>
where
F: FnMut(usize, JobRefMut) -> bool,
{
type Item = (usize, Job);
fn next(&mut self) -> Option<(usize, Job)> {
while self.len > 0 {
let index = self.next_index;
self.next_index += 1;
if let Some(job) = self.list.get_mut(index) {
self.len -= 1;
if (self.should_remove)(index, job) {
let job = self.list.remove(index).unwrap();
return Some((index, job));
}
}
}
None
}
fn size_hint(&self) -> (usize, Option<usize>) {
(0, Some(self.len))
}
}
impl<F> FusedIterator for ExtractIf<'_, F> where F: FnMut(usize, JobRefMut) -> bool {}
impl JobList {
pub fn add(&mut self, job: Job) -> usize {
let new_job_is_suspended = job.is_suspended();
let ex_current_job_is_suspended =
self.current_job().map(|index| self[index].is_suspended());
let ex_previous_job_is_suspended =
self.previous_job().map(|index| self[index].is_suspended());
use std::collections::hash_map::Entry::*;
let index = match self.pids_to_indices.entry(job.pid) {
Vacant(entry) => {
let index = self.jobs.insert(job);
entry.insert(index);
index
}
Occupied(entry) => {
let index = *entry.get();
self.jobs[index] = job;
index
}
};
debug_assert_eq!(self.jobs.len(), self.pids_to_indices.len());
match ex_current_job_is_suspended {
None => self.current_job_index = index,
Some(false) if new_job_is_suspended => self.set_current_job(index).unwrap(),
Some(_) => match ex_previous_job_is_suspended {
None => self.previous_job_index = index,
Some(false) if new_job_is_suspended => self.previous_job_index = index,
Some(_) => (),
},
}
index
}
pub fn remove(&mut self, index: usize) -> Option<Job> {
let job = self.jobs.try_remove(index);
if let Some(job) = &job {
self.pids_to_indices.remove(&job.pid);
if self.jobs.is_empty() {
self.jobs.clear();
}
let previous_job_becomes_current_job = index == self.current_job_index;
if previous_job_becomes_current_job {
self.current_job_index = self.previous_job_index;
}
if previous_job_becomes_current_job || index == self.previous_job_index {
self.previous_job_index = self
.any_suspended_job_but_current()
.unwrap_or_else(|| self.any_job_but_current().unwrap_or_default());
}
}
debug_assert_eq!(self.jobs.len(), self.pids_to_indices.len());
job
}
pub fn remove_if<F>(&mut self, should_remove: F)
where
F: FnMut(usize, JobRefMut) -> bool,
{
self.extract_if(should_remove).for_each(drop)
}
pub fn extract_if<F>(&mut self, should_remove: F) -> ExtractIf<'_, F>
where
F: FnMut(usize, JobRefMut) -> bool,
{
let len = self.len();
ExtractIf {
list: self,
should_remove,
next_index: 0,
len,
}
}
}
impl JobList {
pub fn update_status(&mut self, pid: Pid, state: ProcessState) -> Option<usize> {
let index = self.find_by_pid(pid)?;
let job = &mut self.jobs[index];
let was_suspended = job.is_suspended();
job.state = state;
job.state_changed |= job.expected_state != Some(state);
job.expected_state = None;
if !was_suspended && job.is_suspended() {
if index != self.current_job_index {
self.previous_job_index = std::mem::replace(&mut self.current_job_index, index);
}
} else if was_suspended && !job.is_suspended() {
if let Some(prev_index) = self.previous_job() {
let previous_job_becomes_current_job =
index == self.current_job_index && self[prev_index].is_suspended();
if previous_job_becomes_current_job {
self.current_job_index = prev_index;
}
if previous_job_becomes_current_job || index == prev_index {
self.previous_job_index = self.any_suspended_job_but_current().unwrap_or(index);
}
}
}
Some(index)
}
pub fn disown_all(&mut self) {
for (_, job) in &mut self.jobs {
job.is_owned = false;
}
}
}
#[derive(Clone, Copy, Debug, Eq, Error, Hash, PartialEq)]
pub enum SetCurrentJobError {
#[error("no such job")]
NoSuchJob,
#[error("the current job must be selected from suspended jobs")]
NotSuspended,
}
impl JobList {
pub fn set_current_job(&mut self, index: usize) -> Result<(), SetCurrentJobError> {
let job = self.get(index).ok_or(SetCurrentJobError::NoSuchJob)?;
if !job.is_suspended() && self.iter().any(|(_, job)| job.is_suspended()) {
return Err(SetCurrentJobError::NotSuspended);
}
if index != self.current_job_index {
self.previous_job_index = std::mem::replace(&mut self.current_job_index, index);
}
Ok(())
}
pub fn current_job(&self) -> Option<usize> {
if self.jobs.contains(self.current_job_index) {
Some(self.current_job_index)
} else {
None
}
}
pub fn previous_job(&self) -> Option<usize> {
if self.previous_job_index != self.current_job_index
&& self.jobs.contains(self.previous_job_index)
{
Some(self.previous_job_index)
} else {
None
}
}
fn any_suspended_job_but_current(&self) -> Option<usize> {
self.iter()
.filter(|&(index, job)| index != self.current_job_index && job.is_suspended())
.map(|(index, _)| index)
.next()
}
fn any_job_but_current(&self) -> Option<usize> {
self.iter()
.filter(|&(index, _)| index != self.current_job_index)
.map(|(index, _)| index)
.next()
}
}
impl JobList {
pub fn last_async_pid(&self) -> Pid {
self.last_async_pid
}
pub fn set_last_async_pid(&mut self, pid: Pid) {
self.last_async_pid = pid;
}
}
pub async fn tcsetpgrp_with_block<S>(system: &S, fd: Fd, pgid: Pid) -> crate::system::Result<()>
where
S: Signals + Sigmask + TcSetPgrp + ?Sized,
{
let mut old_mask = Vec::new();
system
.sigmask(Some((SigmaskOp::Add, &[S::SIGTTOU])), Some(&mut old_mask))
.await?;
let result = system.tcsetpgrp(fd, pgid).await;
let result_2 = system
.sigmask(Some((SigmaskOp::Set, &old_mask)), None)
.await;
result.and(result_2)
}
pub async fn tcsetpgrp_without_block<S>(system: &S, fd: Fd, pgid: Pid) -> crate::system::Result<()>
where
S: Signals + Sigaction + Sigmask + TcSetPgrp + ?Sized,
{
match system.sigaction(S::SIGTTOU, Disposition::Default) {
Err(e) => Err(e),
Ok(old_handling) => {
let mut old_mask = Vec::new();
let result = match system
.sigmask(
Some((SigmaskOp::Remove, &[S::SIGTTOU])),
Some(&mut old_mask),
)
.await
{
Err(e) => Err(e),
Ok(()) => {
let result = system.tcsetpgrp(fd, pgid).await;
let result_2 = system
.sigmask(Some((SigmaskOp::Set, &old_mask)), None)
.await;
result.and(result_2)
}
};
let result_2 = system.sigaction(S::SIGTTOU, old_handling).map(drop);
result.and(result_2)
}
}
}
pub fn add_job_if_suspended<S, F>(
env: &mut Env<S>,
pid: Pid,
result: ProcessResult,
name: F,
) -> crate::semantics::Result<ExitStatus>
where
F: FnOnce() -> String,
{
let exit_status = result.into();
if result.is_stopped() {
let mut job = Job::new(pid);
job.job_controlled = true;
job.state = result.into();
job.name = name();
env.jobs.add(job);
if env.is_interactive() {
return Break(Divert::Interrupt(Some(exit_status)));
}
}
Continue(exit_status)
}
pub mod fmt;
pub mod id;
#[cfg(test)]
mod tests {
use super::*;
use crate::option::Option::Interactive;
use crate::option::State::On;
use crate::signal;
use crate::system::r#virtual::{SIGSTOP, SIGTSTP, SIGTTIN, SIGTTOU};
use std::num::NonZero;
#[test]
fn job_list_find_by_pid() {
let mut list = JobList::default();
assert_eq!(list.find_by_pid(Pid(10)), None);
let i10 = list.add(Job::new(Pid(10)));
let i20 = list.add(Job::new(Pid(20)));
let i30 = list.add(Job::new(Pid(30)));
assert_eq!(list.find_by_pid(Pid(10)), Some(i10));
assert_eq!(list.find_by_pid(Pid(20)), Some(i20));
assert_eq!(list.find_by_pid(Pid(30)), Some(i30));
assert_eq!(list.find_by_pid(Pid(40)), None);
list.remove(i10);
assert_eq!(list.find_by_pid(Pid(10)), None);
}
#[test]
fn job_list_add_and_remove() {
let mut list = JobList::default();
assert_eq!(list.add(Job::new(Pid(10))), 0);
assert_eq!(list.add(Job::new(Pid(11))), 1);
assert_eq!(list.add(Job::new(Pid(12))), 2);
assert_eq!(list.remove(0).unwrap().pid, Pid(10));
assert_eq!(list.remove(1).unwrap().pid, Pid(11));
assert_eq!(list.add(Job::new(Pid(13))), 1);
assert_eq!(list.add(Job::new(Pid(14))), 0);
assert_eq!(list.remove(0).unwrap().pid, Pid(14));
assert_eq!(list.remove(1).unwrap().pid, Pid(13));
assert_eq!(list.remove(2).unwrap().pid, Pid(12));
assert_eq!(list.add(Job::new(Pid(13))), 0);
assert_eq!(list.add(Job::new(Pid(14))), 1);
}
#[test]
fn job_list_add_same_pid() {
let mut list = JobList::default();
let mut job = Job::new(Pid(10));
job.name = "first job".to_string();
let i_first = list.add(job);
let mut job = Job::new(Pid(10));
job.name = "second job".to_string();
let i_second = list.add(job);
let job = &list[i_second];
assert_eq!(job.pid, Pid(10));
assert_eq!(job.name, "second job");
assert_ne!(
list.get(i_first).map(|job| job.name.as_str()),
Some("first job")
);
}
#[test]
fn job_list_extract_if() {
let mut list = JobList::default();
let i21 = list.add(Job::new(Pid(21)));
let i22 = list.add(Job::new(Pid(22)));
let i23 = list.add(Job::new(Pid(23)));
let i24 = list.add(Job::new(Pid(24)));
let i25 = list.add(Job::new(Pid(25)));
let i26 = list.add(Job::new(Pid(26)));
list.remove(i23).unwrap();
let mut i = list.extract_if(|index, mut job| {
assert_ne!(index, i23);
if index % 2 == 0 {
job.state_reported();
}
index == 0 || job.pid == Pid(26)
});
let mut expected_job_21 = Job::new(Pid(21));
expected_job_21.state_changed = false;
assert_eq!(i.next(), Some((i21, expected_job_21)));
assert_eq!(i.next(), Some((i26, Job::new(Pid(26)))));
assert_eq!(i.next(), None);
assert_eq!(i.next(), None);
let indices: Vec<usize> = list.iter().map(|(index, _)| index).collect();
assert_eq!(indices, [i22, i24, i25]);
assert!(list[i22].state_changed);
assert!(list[i24].state_changed);
assert!(!list[i25].state_changed);
}
#[test]
#[allow(clippy::bool_assert_comparison)]
fn updating_job_status_without_expected_state() {
let mut list = JobList::default();
let state = ProcessState::exited(15);
assert_eq!(list.update_status(Pid(20), state), None);
let i10 = list.add(Job::new(Pid(10)));
let i20 = list.add(Job::new(Pid(20)));
let i30 = list.add(Job::new(Pid(30)));
assert_eq!(list[i20].state, ProcessState::Running);
list.get_mut(i20).unwrap().state_reported();
assert_eq!(list[i20].state_changed, false);
assert_eq!(list.update_status(Pid(20), state), Some(i20));
assert_eq!(list[i20].state, ProcessState::exited(15));
assert_eq!(list[i20].state_changed, true);
assert_eq!(list[i10].state, ProcessState::Running);
assert_eq!(list[i30].state, ProcessState::Running);
}
#[test]
#[allow(clippy::bool_assert_comparison)]
fn updating_job_status_with_matching_expected_state() {
let mut list = JobList::default();
let pid = Pid(20);
let mut job = Job::new(pid);
job.expected_state = Some(ProcessState::Running);
job.state_changed = false;
let i20 = list.add(job);
assert_eq!(list.update_status(pid, ProcessState::Running), Some(i20));
let job = &list[i20];
assert_eq!(job.state, ProcessState::Running);
assert_eq!(job.expected_state, None);
assert_eq!(job.state_changed, false);
}
#[test]
#[allow(clippy::bool_assert_comparison)]
fn updating_job_status_with_unmatched_expected_state() {
let mut list = JobList::default();
let pid = Pid(20);
let mut job = Job::new(pid);
job.expected_state = Some(ProcessState::Running);
job.state_changed = false;
let i20 = list.add(job);
let result = list.update_status(pid, ProcessState::exited(0));
assert_eq!(result, Some(i20));
let job = &list[i20];
assert_eq!(job.state, ProcessState::exited(0));
assert_eq!(job.expected_state, None);
assert_eq!(job.state_changed, true);
}
#[test]
#[allow(clippy::bool_assert_comparison)]
fn disowning_jobs() {
let mut list = JobList::default();
let i10 = list.add(Job::new(Pid(10)));
let i20 = list.add(Job::new(Pid(20)));
let i30 = list.add(Job::new(Pid(30)));
list.disown_all();
assert_eq!(list[i10].is_owned, false);
assert_eq!(list[i20].is_owned, false);
assert_eq!(list[i30].is_owned, false);
}
#[test]
fn no_current_and_previous_job_in_empty_job_list() {
let list = JobList::default();
assert_eq!(list.current_job(), None);
assert_eq!(list.previous_job(), None);
}
#[test]
fn current_and_previous_job_in_job_list_with_one_job() {
let mut list = JobList::default();
let i10 = list.add(Job::new(Pid(10)));
assert_eq!(list.current_job(), Some(i10));
assert_eq!(list.previous_job(), None);
}
#[test]
fn current_and_previous_job_in_job_list_with_two_job() {
let mut list = JobList::default();
let mut suspended = Job::new(Pid(10));
suspended.state = ProcessState::stopped(SIGSTOP);
let running = Job::new(Pid(20));
let i10 = list.add(suspended.clone());
let i20 = list.add(running.clone());
assert_eq!(list.current_job(), Some(i10));
assert_eq!(list.previous_job(), Some(i20));
list = JobList::default();
let i20 = list.add(running);
let i10 = list.add(suspended);
assert_eq!(list.current_job(), Some(i10));
assert_eq!(list.previous_job(), Some(i20));
}
#[test]
fn adding_suspended_job_with_running_current_and_previous_job() {
let mut list = JobList::default();
let running_1 = Job::new(Pid(11));
let running_2 = Job::new(Pid(12));
list.add(running_1);
list.add(running_2);
let ex_current_job_index = list.current_job().unwrap();
let ex_previous_job_index = list.previous_job().unwrap();
assert_ne!(ex_current_job_index, ex_previous_job_index);
let mut suspended = Job::new(Pid(20));
suspended.state = ProcessState::stopped(SIGSTOP);
let i20 = list.add(suspended);
let now_current_job_index = list.current_job().unwrap();
let now_previous_job_index = list.previous_job().unwrap();
assert_eq!(now_current_job_index, i20);
assert_eq!(now_previous_job_index, ex_current_job_index);
}
#[test]
fn adding_suspended_job_with_suspended_current_and_running_previous_job() {
let mut list = JobList::default();
let running = Job::new(Pid(18));
let i18 = list.add(running);
let mut suspended_1 = Job::new(Pid(19));
suspended_1.state = ProcessState::stopped(SIGSTOP);
let i19 = list.add(suspended_1);
let ex_current_job_index = list.current_job().unwrap();
let ex_previous_job_index = list.previous_job().unwrap();
assert_eq!(ex_current_job_index, i19);
assert_eq!(ex_previous_job_index, i18);
let mut suspended_2 = Job::new(Pid(20));
suspended_2.state = ProcessState::stopped(SIGSTOP);
let i20 = list.add(suspended_2);
let now_current_job_index = list.current_job().unwrap();
let now_previous_job_index = list.previous_job().unwrap();
assert_eq!(now_current_job_index, ex_current_job_index);
assert_eq!(now_previous_job_index, i20);
}
#[test]
fn removing_current_job() {
let mut list = JobList::default();
let running = Job::new(Pid(10));
let i10 = list.add(running);
let mut suspended_1 = Job::new(Pid(11));
let mut suspended_2 = Job::new(Pid(12));
let mut suspended_3 = Job::new(Pid(13));
suspended_1.state = ProcessState::stopped(SIGSTOP);
suspended_2.state = ProcessState::stopped(SIGSTOP);
suspended_3.state = ProcessState::stopped(SIGSTOP);
list.add(suspended_1);
list.add(suspended_2);
list.add(suspended_3);
let current_job_index_1 = list.current_job().unwrap();
let previous_job_index_1 = list.previous_job().unwrap();
assert_ne!(current_job_index_1, i10);
assert_ne!(previous_job_index_1, i10);
list.remove(current_job_index_1);
let current_job_index_2 = list.current_job().unwrap();
let previous_job_index_2 = list.previous_job().unwrap();
assert_eq!(current_job_index_2, previous_job_index_1);
assert_ne!(previous_job_index_2, current_job_index_2);
let previous_job_2 = &list[previous_job_index_2];
assert!(
previous_job_2.is_suspended(),
"previous_job_2 = {previous_job_2:?}"
);
list.remove(current_job_index_2);
let current_job_index_3 = list.current_job().unwrap();
let previous_job_index_3 = list.previous_job().unwrap();
assert_eq!(current_job_index_3, previous_job_index_2);
assert_eq!(previous_job_index_3, i10);
list.remove(current_job_index_3);
let current_job_index_4 = list.current_job().unwrap();
assert_eq!(current_job_index_4, i10);
assert_eq!(list.previous_job(), None);
}
#[test]
fn removing_previous_job_with_suspended_job() {
let mut list = JobList::default();
let running = Job::new(Pid(10));
let i10 = list.add(running);
let mut suspended_1 = Job::new(Pid(11));
let mut suspended_2 = Job::new(Pid(12));
let mut suspended_3 = Job::new(Pid(13));
suspended_1.state = ProcessState::stopped(SIGSTOP);
suspended_2.state = ProcessState::stopped(SIGSTOP);
suspended_3.state = ProcessState::stopped(SIGSTOP);
list.add(suspended_1);
list.add(suspended_2);
list.add(suspended_3);
let ex_current_job_index = list.current_job().unwrap();
let ex_previous_job_index = list.previous_job().unwrap();
assert_ne!(ex_current_job_index, i10);
assert_ne!(ex_previous_job_index, i10);
list.remove(ex_previous_job_index);
let now_current_job_index = list.current_job().unwrap();
let now_previous_job_index = list.previous_job().unwrap();
assert_eq!(now_current_job_index, ex_current_job_index);
assert_ne!(now_previous_job_index, now_current_job_index);
let now_previous_job = &list[now_previous_job_index];
assert!(
now_previous_job.is_suspended(),
"now_previous_job = {now_previous_job:?}"
);
}
#[test]
fn removing_previous_job_with_running_job() {
let mut list = JobList::default();
let running = Job::new(Pid(10));
let i10 = list.add(running);
let mut suspended_1 = Job::new(Pid(11));
let mut suspended_2 = Job::new(Pid(12));
suspended_1.state = ProcessState::stopped(SIGSTOP);
suspended_2.state = ProcessState::stopped(SIGSTOP);
list.add(suspended_1);
list.add(suspended_2);
let ex_current_job_index = list.current_job().unwrap();
let ex_previous_job_index = list.previous_job().unwrap();
assert_ne!(ex_current_job_index, i10);
assert_ne!(ex_previous_job_index, i10);
list.remove(ex_previous_job_index);
let now_current_job_index = list.current_job().unwrap();
let now_previous_job_index = list.previous_job().unwrap();
assert_eq!(now_current_job_index, ex_current_job_index);
assert_eq!(now_previous_job_index, i10);
}
#[test]
fn set_current_job_with_running_jobs_only() {
let mut list = JobList::default();
let i21 = list.add(Job::new(Pid(21)));
let i22 = list.add(Job::new(Pid(22)));
assert_eq!(list.set_current_job(i21), Ok(()));
assert_eq!(list.current_job(), Some(i21));
assert_eq!(list.set_current_job(i22), Ok(()));
assert_eq!(list.current_job(), Some(i22));
}
#[test]
fn set_current_job_to_suspended_job() {
let mut list = JobList::default();
list.add(Job::new(Pid(20)));
let mut suspended_1 = Job::new(Pid(21));
let mut suspended_2 = Job::new(Pid(22));
suspended_1.state = ProcessState::stopped(SIGSTOP);
suspended_2.state = ProcessState::stopped(SIGSTOP);
let i21 = list.add(suspended_1);
let i22 = list.add(suspended_2);
assert_eq!(list.set_current_job(i21), Ok(()));
assert_eq!(list.current_job(), Some(i21));
assert_eq!(list.set_current_job(i22), Ok(()));
assert_eq!(list.current_job(), Some(i22));
}
#[test]
fn set_current_job_no_such_job() {
let mut list = JobList::default();
assert_eq!(list.set_current_job(0), Err(SetCurrentJobError::NoSuchJob));
assert_eq!(list.set_current_job(1), Err(SetCurrentJobError::NoSuchJob));
assert_eq!(list.set_current_job(2), Err(SetCurrentJobError::NoSuchJob));
}
#[test]
fn set_current_job_not_suspended() {
let mut list = JobList::default();
let mut suspended = Job::new(Pid(10));
suspended.state = ProcessState::stopped(SIGTSTP);
let running = Job::new(Pid(20));
let i10 = list.add(suspended);
let i20 = list.add(running);
assert_eq!(
list.set_current_job(i20),
Err(SetCurrentJobError::NotSuspended)
);
assert_eq!(list.current_job(), Some(i10));
}
#[test]
fn set_current_job_no_change() {
let mut list = JobList::default();
list.add(Job::new(Pid(5)));
list.add(Job::new(Pid(6)));
let old_current_job_index = list.current_job().unwrap();
let old_previous_job_index = list.previous_job().unwrap();
list.set_current_job(old_current_job_index).unwrap();
let new_current_job_index = list.current_job().unwrap();
let new_previous_job_index = list.previous_job().unwrap();
assert_eq!(new_current_job_index, old_current_job_index);
assert_eq!(new_previous_job_index, old_previous_job_index);
}
#[test]
fn resuming_current_job_without_other_suspended_jobs() {
let mut list = JobList::default();
let mut suspended = Job::new(Pid(10));
suspended.state = ProcessState::stopped(SIGTSTP);
let running = Job::new(Pid(20));
let i10 = list.add(suspended);
let i20 = list.add(running);
list.update_status(Pid(10), ProcessState::Running);
assert_eq!(list.current_job(), Some(i10));
assert_eq!(list.previous_job(), Some(i20));
}
#[test]
fn resuming_current_job_with_another_suspended_job() {
let mut list = JobList::default();
let mut suspended_1 = Job::new(Pid(10));
let mut suspended_2 = Job::new(Pid(20));
suspended_1.state = ProcessState::stopped(SIGTSTP);
suspended_2.state = ProcessState::stopped(SIGTSTP);
let i10 = list.add(suspended_1);
let i20 = list.add(suspended_2);
list.set_current_job(i10).unwrap();
list.update_status(Pid(10), ProcessState::Running);
assert_eq!(list.current_job(), Some(i20));
assert_eq!(list.previous_job(), Some(i10));
}
#[test]
fn resuming_current_job_with_other_suspended_jobs() {
let mut list = JobList::default();
let mut suspended_1 = Job::new(Pid(10));
let mut suspended_2 = Job::new(Pid(20));
let mut suspended_3 = Job::new(Pid(30));
suspended_1.state = ProcessState::stopped(SIGTSTP);
suspended_2.state = ProcessState::stopped(SIGTSTP);
suspended_3.state = ProcessState::stopped(SIGTSTP);
list.add(suspended_1);
list.add(suspended_2);
list.add(suspended_3);
let ex_current_job_pid = list[list.current_job().unwrap()].pid;
let ex_previous_job_index = list.previous_job().unwrap();
list.update_status(ex_current_job_pid, ProcessState::Running);
let now_current_job_index = list.current_job().unwrap();
let now_previous_job_index = list.previous_job().unwrap();
assert_eq!(now_current_job_index, ex_previous_job_index);
assert_ne!(now_previous_job_index, now_current_job_index);
let now_previous_job = &list[now_previous_job_index];
assert!(
now_previous_job.is_suspended(),
"now_previous_job = {now_previous_job:?}"
);
}
#[test]
fn resuming_previous_job() {
let mut list = JobList::default();
let mut suspended_1 = Job::new(Pid(10));
let mut suspended_2 = Job::new(Pid(20));
let mut suspended_3 = Job::new(Pid(30));
suspended_1.state = ProcessState::stopped(SIGTSTP);
suspended_2.state = ProcessState::stopped(SIGTSTP);
suspended_3.state = ProcessState::stopped(SIGTSTP);
list.add(suspended_1);
list.add(suspended_2);
list.add(suspended_3);
let ex_current_job_index = list.current_job().unwrap();
let ex_previous_job_pid = list[list.previous_job().unwrap()].pid;
list.update_status(ex_previous_job_pid, ProcessState::Running);
let now_current_job_index = list.current_job().unwrap();
let now_previous_job_index = list.previous_job().unwrap();
assert_eq!(now_current_job_index, ex_current_job_index);
assert_ne!(now_previous_job_index, now_current_job_index);
let now_previous_job = &list[now_previous_job_index];
assert!(
now_previous_job.is_suspended(),
"now_previous_job = {now_previous_job:?}"
);
}
#[test]
fn resuming_other_job() {
let mut list = JobList::default();
let mut suspended_1 = Job::new(Pid(10));
let mut suspended_2 = Job::new(Pid(20));
let mut suspended_3 = Job::new(Pid(30));
suspended_1.state = ProcessState::stopped(SIGTSTP);
suspended_2.state = ProcessState::stopped(SIGTSTP);
suspended_3.state = ProcessState::stopped(SIGTSTP);
let i10 = list.add(suspended_1);
let i20 = list.add(suspended_2);
let _i30 = list.add(suspended_3);
list.set_current_job(i20).unwrap();
list.set_current_job(i10).unwrap();
list.update_status(Pid(30), ProcessState::Running);
assert_eq!(list.current_job(), Some(i10));
assert_eq!(list.previous_job(), Some(i20));
}
#[test]
fn suspending_current_job() {
let mut list = JobList::default();
let i11 = list.add(Job::new(Pid(11)));
let i12 = list.add(Job::new(Pid(12)));
list.set_current_job(i11).unwrap();
list.update_status(Pid(11), ProcessState::stopped(SIGTTOU));
assert_eq!(list.current_job(), Some(i11));
assert_eq!(list.previous_job(), Some(i12));
}
#[test]
fn suspending_previous_job() {
let mut list = JobList::default();
let i11 = list.add(Job::new(Pid(11)));
let i12 = list.add(Job::new(Pid(12)));
list.set_current_job(i11).unwrap();
list.update_status(Pid(12), ProcessState::stopped(SIGTTOU));
assert_eq!(list.current_job(), Some(i12));
assert_eq!(list.previous_job(), Some(i11));
}
#[test]
fn suspending_job_with_running_current_job() {
let mut list = JobList::default();
let i10 = list.add(Job::new(Pid(10)));
let _i11 = list.add(Job::new(Pid(11)));
let i12 = list.add(Job::new(Pid(12)));
list.set_current_job(i10).unwrap();
list.update_status(Pid(12), ProcessState::stopped(SIGTTIN));
assert_eq!(list.current_job(), Some(i12));
assert_eq!(list.previous_job(), Some(i10));
}
#[test]
fn suspending_job_with_running_previous_job() {
let mut list = JobList::default();
let i11 = list.add(Job::new(Pid(11)));
let i12 = list.add(Job::new(Pid(12)));
let mut suspended = Job::new(Pid(10));
suspended.state = ProcessState::stopped(SIGTTIN);
let i10 = list.add(suspended);
assert_eq!(list.current_job(), Some(i10));
assert_eq!(list.previous_job(), Some(i11));
list.update_status(Pid(12), ProcessState::stopped(SIGTTOU));
assert_eq!(list.current_job(), Some(i12));
assert_eq!(list.previous_job(), Some(i10));
}
#[test]
fn do_not_add_job_if_exited() {
let mut env = Env::new_virtual();
let result = add_job_if_suspended(
&mut env,
Pid(123),
ProcessResult::Exited(ExitStatus(42)),
|| "foo".to_string(),
);
assert_eq!(result, Continue(ExitStatus(42)));
assert_eq!(env.jobs.len(), 0);
}
#[test]
fn do_not_add_job_if_signaled() {
let mut env = Env::new_virtual();
let signal = signal::Number::from_raw_unchecked(NonZero::new(42).unwrap());
let result = add_job_if_suspended(
&mut env,
Pid(123),
ProcessResult::Signaled {
signal,
core_dump: false,
},
|| "foo".to_string(),
);
assert_eq!(result, Continue(ExitStatus::from(signal)));
assert_eq!(env.jobs.len(), 0);
}
#[test]
fn add_job_if_stopped() {
let mut env = Env::new_virtual();
let signal = signal::Number::from_raw_unchecked(NonZero::new(42).unwrap());
let process_result = ProcessResult::Stopped(signal);
let result = add_job_if_suspended(&mut env, Pid(123), process_result, || "foo".to_string());
assert_eq!(result, Continue(ExitStatus::from(signal)));
assert_eq!(env.jobs.len(), 1);
let job = env.jobs.get(0).unwrap();
assert_eq!(job.pid, Pid(123));
assert!(job.job_controlled);
assert_eq!(job.state, ProcessState::Halted(process_result));
assert_eq!(job.name, "foo");
}
#[test]
fn break_if_stopped_and_interactive() {
let mut env = Env::new_virtual();
env.options.set(Interactive, On);
let signal = signal::Number::from_raw_unchecked(NonZero::new(42).unwrap());
let process_result = ProcessResult::Stopped(signal);
let result = add_job_if_suspended(&mut env, Pid(123), process_result, || "foo".to_string());
assert_eq!(
result,
Break(Divert::Interrupt(Some(ExitStatus::from(signal))))
);
assert_eq!(env.jobs.len(), 1);
}
}