1use crate::Env;
45use crate::io::Fd;
46use crate::semantics::{Divert, ExitStatus};
47use crate::signal;
48use crate::system::{Disposition, Sigaction, Sigmask, SigmaskOp, Signals, TcSetPgrp};
49use slab::Slab;
50use std::collections::HashMap;
51use std::iter::FusedIterator;
52use std::ops::ControlFlow::{Break, Continue};
53use std::ops::Deref;
54use thiserror::Error;
55
56#[cfg(unix)]
57type RawPidDef = libc::pid_t;
58#[cfg(not(unix))]
59type RawPidDef = i32;
60
61pub type RawPid = RawPidDef;
71
72#[repr(transparent)]
91#[derive(Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
92pub struct Pid(pub RawPid);
93
94impl std::fmt::Display for Pid {
95 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
96 self.0.fmt(f)
97 }
98}
99
100impl std::ops::Neg for Pid {
101 type Output = Self;
102 fn neg(self) -> Self {
103 Self(-self.0)
104 }
105}
106
107impl Pid {
108 pub const MY_PROCESS_GROUP: Self = Pid(0);
114
115 pub const ALL: Self = Pid(-1);
121}
122
123#[derive(Clone, Copy, Debug, Eq, PartialEq)]
133pub enum ProcessResult {
134 Stopped(signal::Number),
136 Exited(ExitStatus),
138 Signaled {
140 signal: signal::Number,
141 core_dump: bool,
142 },
143}
144
145impl ProcessResult {
146 #[inline]
148 #[must_use]
149 pub fn exited<S: Into<ExitStatus>>(exit_status: S) -> Self {
150 Self::Exited(exit_status.into())
151 }
152
153 #[must_use]
155 pub fn is_stopped(&self) -> bool {
156 matches!(self, ProcessResult::Stopped(_))
157 }
158}
159
160impl From<ProcessResult> for ExitStatus {
162 fn from(result: ProcessResult) -> Self {
163 match result {
164 ProcessResult::Exited(exit_status) => exit_status,
165 ProcessResult::Stopped(signal) | ProcessResult::Signaled { signal, .. } => {
166 ExitStatus::from(signal)
167 }
168 }
169 }
170}
171
172#[derive(Clone, Copy, Debug, Eq, PartialEq)]
182pub enum ProcessState {
183 Running,
185 Halted(ProcessResult),
187}
188
189impl ProcessState {
190 #[inline]
192 #[must_use]
193 pub fn stopped(signal: signal::Number) -> Self {
194 Self::Halted(ProcessResult::Stopped(signal))
195 }
196
197 #[inline]
199 #[must_use]
200 pub fn exited<S: Into<ExitStatus>>(exit_status: S) -> Self {
201 Self::Halted(ProcessResult::exited(exit_status))
202 }
203
204 #[must_use]
206 pub fn is_alive(&self) -> bool {
207 match self {
208 ProcessState::Running => true,
209 ProcessState::Halted(result) => result.is_stopped(),
210 }
211 }
212
213 #[must_use]
215 pub fn is_stopped(&self) -> bool {
216 matches!(self, Self::Halted(result) if result.is_stopped())
217 }
218}
219
220impl From<ProcessResult> for ProcessState {
221 #[inline]
222 fn from(result: ProcessResult) -> Self {
223 Self::Halted(result)
224 }
225}
226
227#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)]
231pub struct RunningProcess;
232
233impl TryFrom<ProcessState> for ExitStatus {
237 type Error = RunningProcess;
238 fn try_from(state: ProcessState) -> Result<Self, RunningProcess> {
239 match state {
240 ProcessState::Halted(result) => Ok(result.into()),
241 ProcessState::Running => Err(RunningProcess),
242 }
243 }
244}
245
246#[derive(Clone, Debug, Eq, PartialEq)]
253#[non_exhaustive]
254pub struct Job {
255 pub pid: Pid,
259
260 pub job_controlled: bool,
265
266 pub state: ProcessState,
268
269 pub expected_state: Option<ProcessState>,
273
274 pub state_changed: bool,
279
280 pub is_owned: bool,
286
287 pub name: String,
289}
290
291impl Job {
292 pub fn new(pid: Pid) -> Self {
297 Job {
298 pid,
299 job_controlled: false,
300 state: ProcessState::Running,
301 expected_state: None,
302 state_changed: true,
303 is_owned: true,
304 name: String::new(),
305 }
306 }
307
308 #[must_use]
310 fn is_suspended(&self) -> bool {
311 self.state.is_stopped()
312 }
313}
314
315#[derive(Debug, Eq, PartialEq)]
321pub struct JobRefMut<'a>(&'a mut Job);
322
323impl JobRefMut<'_> {
324 pub fn expect<S>(&mut self, state: S)
335 where
336 S: Into<Option<ProcessState>>,
337 {
338 self.0.expected_state = state.into();
339 }
340
341 pub fn state_reported(&mut self) {
346 self.0.state_changed = false
347 }
348}
349
350impl Deref for JobRefMut<'_> {
351 type Target = Job;
352 fn deref(&self) -> &Job {
353 self.0
354 }
355}
356
357#[derive(Clone, Debug)]
361pub struct Iter<'a>(slab::Iter<'a, Job>);
362
363impl<'a> Iterator for Iter<'a> {
364 type Item = (usize, &'a Job);
365
366 #[inline(always)]
367 fn next(&mut self) -> Option<(usize, &'a Job)> {
368 self.0.next()
369 }
370
371 #[inline(always)]
372 fn size_hint(&self) -> (usize, Option<usize>) {
373 self.0.size_hint()
374 }
375}
376
377impl<'a> DoubleEndedIterator for Iter<'a> {
378 #[inline(always)]
379 fn next_back(&mut self) -> Option<(usize, &'a Job)> {
380 self.0.next_back()
381 }
382}
383
384impl ExactSizeIterator for Iter<'_> {
385 #[inline(always)]
386 fn len(&self) -> usize {
387 self.0.len()
388 }
389}
390
391impl FusedIterator for Iter<'_> {}
392
393#[derive(Debug)]
397pub struct IterMut<'a>(slab::IterMut<'a, Job>);
398
399impl<'a> Iterator for IterMut<'a> {
400 type Item = (usize, JobRefMut<'a>);
401
402 #[inline]
403 fn next(&mut self) -> Option<(usize, JobRefMut<'a>)> {
404 self.0.next().map(|(index, job)| (index, JobRefMut(job)))
405 }
406
407 #[inline(always)]
408 fn size_hint(&self) -> (usize, Option<usize>) {
409 self.0.size_hint()
410 }
411}
412
413impl<'a> DoubleEndedIterator for IterMut<'a> {
414 fn next_back(&mut self) -> Option<(usize, JobRefMut<'a>)> {
415 self.0
416 .next_back()
417 .map(|(index, job)| (index, JobRefMut(job)))
418 }
419}
420
421impl ExactSizeIterator for IterMut<'_> {
422 #[inline(always)]
423 fn len(&self) -> usize {
424 self.0.len()
425 }
426}
427
428impl FusedIterator for IterMut<'_> {}
429
430#[derive(Clone, Debug)]
434pub struct JobList {
435 jobs: Slab<Job>,
437
438 pids_to_indices: HashMap<Pid, usize>,
442
443 current_job_index: usize,
445
446 previous_job_index: usize,
448
449 last_async_pid: Pid,
451}
452
453impl Default for JobList {
454 fn default() -> Self {
455 JobList {
456 jobs: Slab::new(),
457 pids_to_indices: HashMap::new(),
458 current_job_index: usize::default(),
459 previous_job_index: usize::default(),
460 last_async_pid: Pid(0),
461 }
462 }
463}
464
465impl JobList {
466 #[inline]
468 #[must_use]
469 pub fn new() -> Self {
470 Self::default()
471 }
472
473 #[inline]
477 pub fn get(&self, index: usize) -> Option<&Job> {
478 self.jobs.get(index)
479 }
480
481 #[inline]
485 pub fn get_mut(&mut self, index: usize) -> Option<JobRefMut<'_>> {
486 self.jobs.get_mut(index).map(JobRefMut)
487 }
488
489 #[inline]
491 pub fn len(&self) -> usize {
492 self.jobs.len()
493 }
494
495 #[inline]
497 pub fn is_empty(&self) -> bool {
498 self.len() == 0
499 }
500
501 #[inline]
506 pub fn iter(&self) -> Iter<'_> {
507 Iter(self.jobs.iter())
508 }
509
510 #[inline]
518 pub fn iter_mut(&mut self) -> IterMut<'_> {
519 IterMut(self.jobs.iter_mut())
520 }
521
522 pub fn find_by_pid(&self, pid: Pid) -> Option<usize> {
530 self.pids_to_indices.get(&pid).copied()
531 }
532}
533
534impl<'a> IntoIterator for &'a JobList {
535 type Item = (usize, &'a Job);
536 type IntoIter = Iter<'a>;
537 #[inline(always)]
538 fn into_iter(self) -> Iter<'a> {
539 self.iter()
540 }
541}
542
543impl<'a> IntoIterator for &'a mut JobList {
544 type Item = (usize, JobRefMut<'a>);
545 type IntoIter = IterMut<'a>;
546 #[inline(always)]
547 fn into_iter(self) -> IterMut<'a> {
548 self.iter_mut()
549 }
550}
551
552impl std::ops::Index<usize> for JobList {
554 type Output = Job;
555
556 fn index(&self, index: usize) -> &Job {
560 &self.jobs[index]
561 }
562}
563
564#[derive(Debug)]
568pub struct ExtractIf<'a, F>
569where
570 F: FnMut(usize, JobRefMut) -> bool,
571{
572 list: &'a mut JobList,
573 should_remove: F,
574 next_index: usize,
575 len: usize,
576}
577
578impl<F> Iterator for ExtractIf<'_, F>
579where
580 F: FnMut(usize, JobRefMut) -> bool,
581{
582 type Item = (usize, Job);
583
584 fn next(&mut self) -> Option<(usize, Job)> {
585 while self.len > 0 {
586 let index = self.next_index;
587 self.next_index += 1;
588 if let Some(job) = self.list.get_mut(index) {
589 self.len -= 1;
590 if (self.should_remove)(index, job) {
591 let job = self.list.remove(index).unwrap();
592 return Some((index, job));
593 }
594 }
595 }
596 None
597 }
598
599 fn size_hint(&self) -> (usize, Option<usize>) {
600 (0, Some(self.len))
601 }
602}
603
604impl<F> FusedIterator for ExtractIf<'_, F> where F: FnMut(usize, JobRefMut) -> bool {}
605
606impl JobList {
607 pub fn add(&mut self, job: Job) -> usize {
619 let new_job_is_suspended = job.is_suspended();
620 let ex_current_job_is_suspended =
621 self.current_job().map(|index| self[index].is_suspended());
622 let ex_previous_job_is_suspended =
623 self.previous_job().map(|index| self[index].is_suspended());
624
625 use std::collections::hash_map::Entry::*;
627 let index = match self.pids_to_indices.entry(job.pid) {
628 Vacant(entry) => {
629 let index = self.jobs.insert(job);
630 entry.insert(index);
631 index
632 }
633 Occupied(entry) => {
634 let index = *entry.get();
635 self.jobs[index] = job;
636 index
637 }
638 };
639 debug_assert_eq!(self.jobs.len(), self.pids_to_indices.len());
640
641 match ex_current_job_is_suspended {
643 None => self.current_job_index = index,
644 Some(false) if new_job_is_suspended => self.set_current_job(index).unwrap(),
645 Some(_) => match ex_previous_job_is_suspended {
646 None => self.previous_job_index = index,
647 Some(false) if new_job_is_suspended => self.previous_job_index = index,
648 Some(_) => (),
649 },
650 }
651
652 index
653 }
654
655 pub fn remove(&mut self, index: usize) -> Option<Job> {
666 let job = self.jobs.try_remove(index);
667
668 if let Some(job) = &job {
669 self.pids_to_indices.remove(&job.pid);
671
672 if self.jobs.is_empty() {
673 self.jobs.clear();
677 }
678
679 let previous_job_becomes_current_job = index == self.current_job_index;
681 if previous_job_becomes_current_job {
682 self.current_job_index = self.previous_job_index;
683 }
684 if previous_job_becomes_current_job || index == self.previous_job_index {
685 self.previous_job_index = self
686 .any_suspended_job_but_current()
687 .unwrap_or_else(|| self.any_job_but_current().unwrap_or_default());
688 }
689 }
690 debug_assert_eq!(self.jobs.len(), self.pids_to_indices.len());
691
692 job
693 }
694
695 pub fn remove_if<F>(&mut self, should_remove: F)
708 where
709 F: FnMut(usize, JobRefMut) -> bool,
710 {
711 self.extract_if(should_remove).for_each(drop)
712 }
713
714 pub fn extract_if<F>(&mut self, should_remove: F) -> ExtractIf<'_, F>
730 where
731 F: FnMut(usize, JobRefMut) -> bool,
732 {
733 let len = self.len();
734 ExtractIf {
735 list: self,
736 should_remove,
737 next_index: 0,
738 len,
739 }
740 }
741}
742
743impl JobList {
744 pub fn update_status(&mut self, pid: Pid, state: ProcessState) -> Option<usize> {
769 let index = self.find_by_pid(pid)?;
770
771 let job = &mut self.jobs[index];
773 let was_suspended = job.is_suspended();
774 job.state = state;
775 job.state_changed |= job.expected_state != Some(state);
776 job.expected_state = None;
777
778 if !was_suspended && job.is_suspended() {
780 if index != self.current_job_index {
781 self.previous_job_index = std::mem::replace(&mut self.current_job_index, index);
782 }
783 } else if was_suspended && !job.is_suspended() {
784 if let Some(prev_index) = self.previous_job() {
785 let previous_job_becomes_current_job =
786 index == self.current_job_index && self[prev_index].is_suspended();
787 if previous_job_becomes_current_job {
788 self.current_job_index = prev_index;
789 }
790 if previous_job_becomes_current_job || index == prev_index {
791 self.previous_job_index = self.any_suspended_job_but_current().unwrap_or(index);
792 }
793 }
794 }
795
796 Some(index)
797 }
798
799 pub fn disown_all(&mut self) {
803 for (_, job) in &mut self.jobs {
804 job.is_owned = false;
805 }
806 }
807}
808
809#[derive(Clone, Copy, Debug, Eq, Error, Hash, PartialEq)]
811pub enum SetCurrentJobError {
812 #[error("no such job")]
814 NoSuchJob,
815
816 #[error("the current job must be selected from suspended jobs")]
819 NotSuspended,
820}
821
822impl JobList {
823 pub fn set_current_job(&mut self, index: usize) -> Result<(), SetCurrentJobError> {
835 let job = self.get(index).ok_or(SetCurrentJobError::NoSuchJob)?;
836 if !job.is_suspended() && self.iter().any(|(_, job)| job.is_suspended()) {
837 return Err(SetCurrentJobError::NotSuspended);
838 }
839
840 if index != self.current_job_index {
841 self.previous_job_index = std::mem::replace(&mut self.current_job_index, index);
842 }
843 Ok(())
844 }
845
846 pub fn current_job(&self) -> Option<usize> {
859 if self.jobs.contains(self.current_job_index) {
860 Some(self.current_job_index)
861 } else {
862 None
863 }
864 }
865
866 pub fn previous_job(&self) -> Option<usize> {
882 if self.previous_job_index != self.current_job_index
883 && self.jobs.contains(self.previous_job_index)
884 {
885 Some(self.previous_job_index)
886 } else {
887 None
888 }
889 }
890
891 fn any_suspended_job_but_current(&self) -> Option<usize> {
893 self.iter()
894 .filter(|&(index, job)| index != self.current_job_index && job.is_suspended())
895 .map(|(index, _)| index)
896 .next()
897 }
898
899 fn any_job_but_current(&self) -> Option<usize> {
901 self.iter()
902 .filter(|&(index, _)| index != self.current_job_index)
903 .map(|(index, _)| index)
904 .next()
905 }
906}
907
908impl JobList {
909 pub fn last_async_pid(&self) -> Pid {
916 self.last_async_pid
917 }
918
919 pub fn set_last_async_pid(&mut self, pid: Pid) {
924 self.last_async_pid = pid;
925 }
926}
927
928pub async fn tcsetpgrp_with_block<S>(system: &S, fd: Fd, pgid: Pid) -> crate::system::Result<()>
940where
941 S: Signals + Sigmask + TcSetPgrp + ?Sized,
942{
943 let mut old_mask = Vec::new();
944
945 system
946 .sigmask(Some((SigmaskOp::Add, &[S::SIGTTOU])), Some(&mut old_mask))
947 .await?;
948
949 let result = system.tcsetpgrp(fd, pgid).await;
950
951 let result_2 = system
952 .sigmask(Some((SigmaskOp::Set, &old_mask)), None)
953 .await;
954
955 result.and(result_2)
956}
957
958pub async fn tcsetpgrp_without_block<S>(system: &S, fd: Fd, pgid: Pid) -> crate::system::Result<()>
978where
979 S: Signals + Sigaction + Sigmask + TcSetPgrp + ?Sized,
980{
981 match system.sigaction(S::SIGTTOU, Disposition::Default) {
982 Err(e) => Err(e),
983 Ok(old_handling) => {
984 let mut old_mask = Vec::new();
985 let result = match system
986 .sigmask(
987 Some((SigmaskOp::Remove, &[S::SIGTTOU])),
988 Some(&mut old_mask),
989 )
990 .await
991 {
992 Err(e) => Err(e),
993 Ok(()) => {
994 let result = system.tcsetpgrp(fd, pgid).await;
995
996 let result_2 = system
997 .sigmask(Some((SigmaskOp::Set, &old_mask)), None)
998 .await;
999
1000 result.and(result_2)
1001 }
1002 };
1003
1004 let result_2 = system.sigaction(S::SIGTTOU, old_handling).map(drop);
1005
1006 result.and(result_2)
1007 }
1008 }
1009}
1010
1011pub fn add_job_if_suspended<S, F>(
1029 env: &mut Env<S>,
1030 pid: Pid,
1031 result: ProcessResult,
1032 name: F,
1033) -> crate::semantics::Result<ExitStatus>
1034where
1035 F: FnOnce() -> String,
1036{
1037 let exit_status = result.into();
1038
1039 if result.is_stopped() {
1040 let mut job = Job::new(pid);
1041 job.job_controlled = true;
1042 job.state = result.into();
1043 job.name = name();
1044 env.jobs.add(job);
1045
1046 if env.is_interactive() {
1047 return Break(Divert::Interrupt(Some(exit_status)));
1048 }
1049 }
1050
1051 Continue(exit_status)
1052}
1053
1054pub mod fmt;
1055pub mod id;
1056
1057#[cfg(test)]
1058mod tests {
1059 use super::*;
1060 use crate::option::Option::Interactive;
1061 use crate::option::State::On;
1062 use crate::signal;
1063 use crate::system::r#virtual::{SIGSTOP, SIGTSTP, SIGTTIN, SIGTTOU};
1064 use std::num::NonZero;
1065
1066 #[test]
1067 fn job_list_find_by_pid() {
1068 let mut list = JobList::default();
1069 assert_eq!(list.find_by_pid(Pid(10)), None);
1070
1071 let i10 = list.add(Job::new(Pid(10)));
1072 let i20 = list.add(Job::new(Pid(20)));
1073 let i30 = list.add(Job::new(Pid(30)));
1074 assert_eq!(list.find_by_pid(Pid(10)), Some(i10));
1075 assert_eq!(list.find_by_pid(Pid(20)), Some(i20));
1076 assert_eq!(list.find_by_pid(Pid(30)), Some(i30));
1077 assert_eq!(list.find_by_pid(Pid(40)), None);
1078
1079 list.remove(i10);
1080 assert_eq!(list.find_by_pid(Pid(10)), None);
1081 }
1082
1083 #[test]
1084 fn job_list_add_and_remove() {
1085 let mut list = JobList::default();
1087
1088 assert_eq!(list.add(Job::new(Pid(10))), 0);
1089 assert_eq!(list.add(Job::new(Pid(11))), 1);
1090 assert_eq!(list.add(Job::new(Pid(12))), 2);
1091
1092 assert_eq!(list.remove(0).unwrap().pid, Pid(10));
1093 assert_eq!(list.remove(1).unwrap().pid, Pid(11));
1094
1095 assert_eq!(list.add(Job::new(Pid(13))), 1);
1097 assert_eq!(list.add(Job::new(Pid(14))), 0);
1098
1099 assert_eq!(list.remove(0).unwrap().pid, Pid(14));
1100 assert_eq!(list.remove(1).unwrap().pid, Pid(13));
1101 assert_eq!(list.remove(2).unwrap().pid, Pid(12));
1102
1103 assert_eq!(list.add(Job::new(Pid(13))), 0);
1105 assert_eq!(list.add(Job::new(Pid(14))), 1);
1106 }
1107
1108 #[test]
1109 fn job_list_add_same_pid() {
1110 let mut list = JobList::default();
1111
1112 let mut job = Job::new(Pid(10));
1113 job.name = "first job".to_string();
1114 let i_first = list.add(job);
1115
1116 let mut job = Job::new(Pid(10));
1117 job.name = "second job".to_string();
1118 let i_second = list.add(job);
1119
1120 let job = &list[i_second];
1121 assert_eq!(job.pid, Pid(10));
1122 assert_eq!(job.name, "second job");
1123
1124 assert_ne!(
1125 list.get(i_first).map(|job| job.name.as_str()),
1126 Some("first job")
1127 );
1128 }
1129
1130 #[test]
1131 fn job_list_extract_if() {
1132 let mut list = JobList::default();
1133 let i21 = list.add(Job::new(Pid(21)));
1134 let i22 = list.add(Job::new(Pid(22)));
1135 let i23 = list.add(Job::new(Pid(23)));
1136 let i24 = list.add(Job::new(Pid(24)));
1137 let i25 = list.add(Job::new(Pid(25)));
1138 let i26 = list.add(Job::new(Pid(26)));
1139 list.remove(i23).unwrap();
1140
1141 let mut i = list.extract_if(|index, mut job| {
1142 assert_ne!(index, i23);
1143 if index % 2 == 0 {
1144 job.state_reported();
1145 }
1146 index == 0 || job.pid == Pid(26)
1147 });
1148
1149 let mut expected_job_21 = Job::new(Pid(21));
1150 expected_job_21.state_changed = false;
1151 assert_eq!(i.next(), Some((i21, expected_job_21)));
1152 assert_eq!(i.next(), Some((i26, Job::new(Pid(26)))));
1153 assert_eq!(i.next(), None);
1154 assert_eq!(i.next(), None); let indices: Vec<usize> = list.iter().map(|(index, _)| index).collect();
1157 assert_eq!(indices, [i22, i24, i25]);
1158 assert!(list[i22].state_changed);
1159 assert!(list[i24].state_changed);
1160 assert!(!list[i25].state_changed);
1161 }
1162
1163 #[test]
1164 #[allow(clippy::bool_assert_comparison)]
1165 fn updating_job_status_without_expected_state() {
1166 let mut list = JobList::default();
1167 let state = ProcessState::exited(15);
1168 assert_eq!(list.update_status(Pid(20), state), None);
1169
1170 let i10 = list.add(Job::new(Pid(10)));
1171 let i20 = list.add(Job::new(Pid(20)));
1172 let i30 = list.add(Job::new(Pid(30)));
1173 assert_eq!(list[i20].state, ProcessState::Running);
1174
1175 list.get_mut(i20).unwrap().state_reported();
1176 assert_eq!(list[i20].state_changed, false);
1177
1178 assert_eq!(list.update_status(Pid(20), state), Some(i20));
1179 assert_eq!(list[i20].state, ProcessState::exited(15));
1180 assert_eq!(list[i20].state_changed, true);
1181
1182 assert_eq!(list[i10].state, ProcessState::Running);
1183 assert_eq!(list[i30].state, ProcessState::Running);
1184 }
1185
1186 #[test]
1187 #[allow(clippy::bool_assert_comparison)]
1188 fn updating_job_status_with_matching_expected_state() {
1189 let mut list = JobList::default();
1190 let pid = Pid(20);
1191 let mut job = Job::new(pid);
1192 job.expected_state = Some(ProcessState::Running);
1193 job.state_changed = false;
1194 let i20 = list.add(job);
1195
1196 assert_eq!(list.update_status(pid, ProcessState::Running), Some(i20));
1197
1198 let job = &list[i20];
1199 assert_eq!(job.state, ProcessState::Running);
1200 assert_eq!(job.expected_state, None);
1201 assert_eq!(job.state_changed, false);
1202 }
1203
1204 #[test]
1205 #[allow(clippy::bool_assert_comparison)]
1206 fn updating_job_status_with_unmatched_expected_state() {
1207 let mut list = JobList::default();
1208 let pid = Pid(20);
1209 let mut job = Job::new(pid);
1210 job.expected_state = Some(ProcessState::Running);
1211 job.state_changed = false;
1212 let i20 = list.add(job);
1213
1214 let result = list.update_status(pid, ProcessState::exited(0));
1215 assert_eq!(result, Some(i20));
1216
1217 let job = &list[i20];
1218 assert_eq!(job.state, ProcessState::exited(0));
1219 assert_eq!(job.expected_state, None);
1220 assert_eq!(job.state_changed, true);
1221 }
1222
1223 #[test]
1224 #[allow(clippy::bool_assert_comparison)]
1225 fn disowning_jobs() {
1226 let mut list = JobList::default();
1227 let i10 = list.add(Job::new(Pid(10)));
1228 let i20 = list.add(Job::new(Pid(20)));
1229 let i30 = list.add(Job::new(Pid(30)));
1230
1231 list.disown_all();
1232
1233 assert_eq!(list[i10].is_owned, false);
1234 assert_eq!(list[i20].is_owned, false);
1235 assert_eq!(list[i30].is_owned, false);
1236 }
1237
1238 #[test]
1239 fn no_current_and_previous_job_in_empty_job_list() {
1240 let list = JobList::default();
1241 assert_eq!(list.current_job(), None);
1242 assert_eq!(list.previous_job(), None);
1243 }
1244
1245 #[test]
1246 fn current_and_previous_job_in_job_list_with_one_job() {
1247 let mut list = JobList::default();
1248 let i10 = list.add(Job::new(Pid(10)));
1249 assert_eq!(list.current_job(), Some(i10));
1250 assert_eq!(list.previous_job(), None);
1251 }
1252
1253 #[test]
1254 fn current_and_previous_job_in_job_list_with_two_job() {
1255 let mut list = JobList::default();
1258 let mut suspended = Job::new(Pid(10));
1259 suspended.state = ProcessState::stopped(SIGSTOP);
1260 let running = Job::new(Pid(20));
1261 let i10 = list.add(suspended.clone());
1262 let i20 = list.add(running.clone());
1263 assert_eq!(list.current_job(), Some(i10));
1264 assert_eq!(list.previous_job(), Some(i20));
1265
1266 list = JobList::default();
1268 let i20 = list.add(running);
1269 let i10 = list.add(suspended);
1270 assert_eq!(list.current_job(), Some(i10));
1271 assert_eq!(list.previous_job(), Some(i20));
1272 }
1273
1274 #[test]
1275 fn adding_suspended_job_with_running_current_and_previous_job() {
1276 let mut list = JobList::default();
1277 let running_1 = Job::new(Pid(11));
1278 let running_2 = Job::new(Pid(12));
1279 list.add(running_1);
1280 list.add(running_2);
1281 let ex_current_job_index = list.current_job().unwrap();
1282 let ex_previous_job_index = list.previous_job().unwrap();
1283 assert_ne!(ex_current_job_index, ex_previous_job_index);
1284
1285 let mut suspended = Job::new(Pid(20));
1286 suspended.state = ProcessState::stopped(SIGSTOP);
1287 let i20 = list.add(suspended);
1288 let now_current_job_index = list.current_job().unwrap();
1289 let now_previous_job_index = list.previous_job().unwrap();
1290 assert_eq!(now_current_job_index, i20);
1291 assert_eq!(now_previous_job_index, ex_current_job_index);
1292 }
1293
1294 #[test]
1295 fn adding_suspended_job_with_suspended_current_and_running_previous_job() {
1296 let mut list = JobList::default();
1297
1298 let running = Job::new(Pid(18));
1299 let i18 = list.add(running);
1300
1301 let mut suspended_1 = Job::new(Pid(19));
1302 suspended_1.state = ProcessState::stopped(SIGSTOP);
1303 let i19 = list.add(suspended_1);
1304
1305 let ex_current_job_index = list.current_job().unwrap();
1306 let ex_previous_job_index = list.previous_job().unwrap();
1307 assert_eq!(ex_current_job_index, i19);
1308 assert_eq!(ex_previous_job_index, i18);
1309
1310 let mut suspended_2 = Job::new(Pid(20));
1311 suspended_2.state = ProcessState::stopped(SIGSTOP);
1312 let i20 = list.add(suspended_2);
1313
1314 let now_current_job_index = list.current_job().unwrap();
1315 let now_previous_job_index = list.previous_job().unwrap();
1316 assert_eq!(now_current_job_index, ex_current_job_index);
1317 assert_eq!(now_previous_job_index, i20);
1318 }
1319
1320 #[test]
1321 fn removing_current_job() {
1322 let mut list = JobList::default();
1323
1324 let running = Job::new(Pid(10));
1325 let i10 = list.add(running);
1326
1327 let mut suspended_1 = Job::new(Pid(11));
1328 let mut suspended_2 = Job::new(Pid(12));
1329 let mut suspended_3 = Job::new(Pid(13));
1330 suspended_1.state = ProcessState::stopped(SIGSTOP);
1331 suspended_2.state = ProcessState::stopped(SIGSTOP);
1332 suspended_3.state = ProcessState::stopped(SIGSTOP);
1333 list.add(suspended_1);
1334 list.add(suspended_2);
1335 list.add(suspended_3);
1336
1337 let current_job_index_1 = list.current_job().unwrap();
1338 let previous_job_index_1 = list.previous_job().unwrap();
1339 assert_ne!(current_job_index_1, i10);
1340 assert_ne!(previous_job_index_1, i10);
1341
1342 list.remove(current_job_index_1);
1343 let current_job_index_2 = list.current_job().unwrap();
1344 let previous_job_index_2 = list.previous_job().unwrap();
1345 assert_eq!(current_job_index_2, previous_job_index_1);
1346 assert_ne!(previous_job_index_2, current_job_index_2);
1347 let previous_job_2 = &list[previous_job_index_2];
1349 assert!(
1350 previous_job_2.is_suspended(),
1351 "previous_job_2 = {previous_job_2:?}"
1352 );
1353
1354 list.remove(current_job_index_2);
1355 let current_job_index_3 = list.current_job().unwrap();
1356 let previous_job_index_3 = list.previous_job().unwrap();
1357 assert_eq!(current_job_index_3, previous_job_index_2);
1358 assert_eq!(previous_job_index_3, i10);
1360
1361 list.remove(current_job_index_3);
1362 let current_job_index_4 = list.current_job().unwrap();
1363 assert_eq!(current_job_index_4, i10);
1364 assert_eq!(list.previous_job(), None);
1366 }
1367
1368 #[test]
1369 fn removing_previous_job_with_suspended_job() {
1370 let mut list = JobList::default();
1371
1372 let running = Job::new(Pid(10));
1373 let i10 = list.add(running);
1374
1375 let mut suspended_1 = Job::new(Pid(11));
1376 let mut suspended_2 = Job::new(Pid(12));
1377 let mut suspended_3 = Job::new(Pid(13));
1378 suspended_1.state = ProcessState::stopped(SIGSTOP);
1379 suspended_2.state = ProcessState::stopped(SIGSTOP);
1380 suspended_3.state = ProcessState::stopped(SIGSTOP);
1381 list.add(suspended_1);
1382 list.add(suspended_2);
1383 list.add(suspended_3);
1384
1385 let ex_current_job_index = list.current_job().unwrap();
1386 let ex_previous_job_index = list.previous_job().unwrap();
1387 assert_ne!(ex_current_job_index, i10);
1388 assert_ne!(ex_previous_job_index, i10);
1389
1390 list.remove(ex_previous_job_index);
1391 let now_current_job_index = list.current_job().unwrap();
1392 let now_previous_job_index = list.previous_job().unwrap();
1393 assert_eq!(now_current_job_index, ex_current_job_index);
1394 assert_ne!(now_previous_job_index, now_current_job_index);
1395 let now_previous_job = &list[now_previous_job_index];
1397 assert!(
1398 now_previous_job.is_suspended(),
1399 "now_previous_job = {now_previous_job:?}"
1400 );
1401 }
1402
1403 #[test]
1404 fn removing_previous_job_with_running_job() {
1405 let mut list = JobList::default();
1406
1407 let running = Job::new(Pid(10));
1408 let i10 = list.add(running);
1409
1410 let mut suspended_1 = Job::new(Pid(11));
1411 let mut suspended_2 = Job::new(Pid(12));
1412 suspended_1.state = ProcessState::stopped(SIGSTOP);
1413 suspended_2.state = ProcessState::stopped(SIGSTOP);
1414 list.add(suspended_1);
1415 list.add(suspended_2);
1416
1417 let ex_current_job_index = list.current_job().unwrap();
1418 let ex_previous_job_index = list.previous_job().unwrap();
1419 assert_ne!(ex_current_job_index, i10);
1420 assert_ne!(ex_previous_job_index, i10);
1421
1422 list.remove(ex_previous_job_index);
1423 let now_current_job_index = list.current_job().unwrap();
1424 let now_previous_job_index = list.previous_job().unwrap();
1425 assert_eq!(now_current_job_index, ex_current_job_index);
1426 assert_eq!(now_previous_job_index, i10);
1429 }
1430
1431 #[test]
1432 fn set_current_job_with_running_jobs_only() {
1433 let mut list = JobList::default();
1434 let i21 = list.add(Job::new(Pid(21)));
1435 let i22 = list.add(Job::new(Pid(22)));
1436
1437 assert_eq!(list.set_current_job(i21), Ok(()));
1438 assert_eq!(list.current_job(), Some(i21));
1439
1440 assert_eq!(list.set_current_job(i22), Ok(()));
1441 assert_eq!(list.current_job(), Some(i22));
1442 }
1443
1444 #[test]
1445 fn set_current_job_to_suspended_job() {
1446 let mut list = JobList::default();
1447 list.add(Job::new(Pid(20)));
1448
1449 let mut suspended_1 = Job::new(Pid(21));
1450 let mut suspended_2 = Job::new(Pid(22));
1451 suspended_1.state = ProcessState::stopped(SIGSTOP);
1452 suspended_2.state = ProcessState::stopped(SIGSTOP);
1453 let i21 = list.add(suspended_1);
1454 let i22 = list.add(suspended_2);
1455
1456 assert_eq!(list.set_current_job(i21), Ok(()));
1457 assert_eq!(list.current_job(), Some(i21));
1458
1459 assert_eq!(list.set_current_job(i22), Ok(()));
1460 assert_eq!(list.current_job(), Some(i22));
1461 }
1462
1463 #[test]
1464 fn set_current_job_no_such_job() {
1465 let mut list = JobList::default();
1466 assert_eq!(list.set_current_job(0), Err(SetCurrentJobError::NoSuchJob));
1467 assert_eq!(list.set_current_job(1), Err(SetCurrentJobError::NoSuchJob));
1468 assert_eq!(list.set_current_job(2), Err(SetCurrentJobError::NoSuchJob));
1469 }
1470
1471 #[test]
1472 fn set_current_job_not_suspended() {
1473 let mut list = JobList::default();
1474 let mut suspended = Job::new(Pid(10));
1475 suspended.state = ProcessState::stopped(SIGTSTP);
1476 let running = Job::new(Pid(20));
1477 let i10 = list.add(suspended);
1478 let i20 = list.add(running);
1479 assert_eq!(
1480 list.set_current_job(i20),
1481 Err(SetCurrentJobError::NotSuspended)
1482 );
1483 assert_eq!(list.current_job(), Some(i10));
1484 }
1485
1486 #[test]
1487 fn set_current_job_no_change() {
1488 let mut list = JobList::default();
1489 list.add(Job::new(Pid(5)));
1490 list.add(Job::new(Pid(6)));
1491 let old_current_job_index = list.current_job().unwrap();
1492 let old_previous_job_index = list.previous_job().unwrap();
1493 list.set_current_job(old_current_job_index).unwrap();
1494 let new_current_job_index = list.current_job().unwrap();
1495 let new_previous_job_index = list.previous_job().unwrap();
1496 assert_eq!(new_current_job_index, old_current_job_index);
1497 assert_eq!(new_previous_job_index, old_previous_job_index);
1498 }
1499
1500 #[test]
1501 fn resuming_current_job_without_other_suspended_jobs() {
1502 let mut list = JobList::default();
1503 let mut suspended = Job::new(Pid(10));
1504 suspended.state = ProcessState::stopped(SIGTSTP);
1505 let running = Job::new(Pid(20));
1506 let i10 = list.add(suspended);
1507 let i20 = list.add(running);
1508 list.update_status(Pid(10), ProcessState::Running);
1509 assert_eq!(list.current_job(), Some(i10));
1510 assert_eq!(list.previous_job(), Some(i20));
1511 }
1512
1513 #[test]
1514 fn resuming_current_job_with_another_suspended_job() {
1515 let mut list = JobList::default();
1516 let mut suspended_1 = Job::new(Pid(10));
1517 let mut suspended_2 = Job::new(Pid(20));
1518 suspended_1.state = ProcessState::stopped(SIGTSTP);
1519 suspended_2.state = ProcessState::stopped(SIGTSTP);
1520 let i10 = list.add(suspended_1);
1521 let i20 = list.add(suspended_2);
1522 list.set_current_job(i10).unwrap();
1523 list.update_status(Pid(10), ProcessState::Running);
1524 assert_eq!(list.current_job(), Some(i20));
1526 assert_eq!(list.previous_job(), Some(i10));
1527 }
1528
1529 #[test]
1530 fn resuming_current_job_with_other_suspended_jobs() {
1531 let mut list = JobList::default();
1532 let mut suspended_1 = Job::new(Pid(10));
1533 let mut suspended_2 = Job::new(Pid(20));
1534 let mut suspended_3 = Job::new(Pid(30));
1535 suspended_1.state = ProcessState::stopped(SIGTSTP);
1536 suspended_2.state = ProcessState::stopped(SIGTSTP);
1537 suspended_3.state = ProcessState::stopped(SIGTSTP);
1538 list.add(suspended_1);
1539 list.add(suspended_2);
1540 list.add(suspended_3);
1541 let ex_current_job_pid = list[list.current_job().unwrap()].pid;
1542 let ex_previous_job_index = list.previous_job().unwrap();
1543
1544 list.update_status(ex_current_job_pid, ProcessState::Running);
1545 let now_current_job_index = list.current_job().unwrap();
1546 let now_previous_job_index = list.previous_job().unwrap();
1547 assert_eq!(now_current_job_index, ex_previous_job_index);
1548 assert_ne!(now_previous_job_index, now_current_job_index);
1549 let now_previous_job = &list[now_previous_job_index];
1551 assert!(
1552 now_previous_job.is_suspended(),
1553 "now_previous_job = {now_previous_job:?}"
1554 );
1555 }
1556
1557 #[test]
1558 fn resuming_previous_job() {
1559 let mut list = JobList::default();
1560 let mut suspended_1 = Job::new(Pid(10));
1561 let mut suspended_2 = Job::new(Pid(20));
1562 let mut suspended_3 = Job::new(Pid(30));
1563 suspended_1.state = ProcessState::stopped(SIGTSTP);
1564 suspended_2.state = ProcessState::stopped(SIGTSTP);
1565 suspended_3.state = ProcessState::stopped(SIGTSTP);
1566 list.add(suspended_1);
1567 list.add(suspended_2);
1568 list.add(suspended_3);
1569 let ex_current_job_index = list.current_job().unwrap();
1570 let ex_previous_job_pid = list[list.previous_job().unwrap()].pid;
1571
1572 list.update_status(ex_previous_job_pid, ProcessState::Running);
1573 let now_current_job_index = list.current_job().unwrap();
1574 let now_previous_job_index = list.previous_job().unwrap();
1575 assert_eq!(now_current_job_index, ex_current_job_index);
1576 assert_ne!(now_previous_job_index, now_current_job_index);
1577 let now_previous_job = &list[now_previous_job_index];
1579 assert!(
1580 now_previous_job.is_suspended(),
1581 "now_previous_job = {now_previous_job:?}"
1582 );
1583 }
1584
1585 #[test]
1586 fn resuming_other_job() {
1587 let mut list = JobList::default();
1588 let mut suspended_1 = Job::new(Pid(10));
1589 let mut suspended_2 = Job::new(Pid(20));
1590 let mut suspended_3 = Job::new(Pid(30));
1591 suspended_1.state = ProcessState::stopped(SIGTSTP);
1592 suspended_2.state = ProcessState::stopped(SIGTSTP);
1593 suspended_3.state = ProcessState::stopped(SIGTSTP);
1594 let i10 = list.add(suspended_1);
1595 let i20 = list.add(suspended_2);
1596 let _i30 = list.add(suspended_3);
1597 list.set_current_job(i20).unwrap();
1598 list.set_current_job(i10).unwrap();
1599 list.update_status(Pid(30), ProcessState::Running);
1600 assert_eq!(list.current_job(), Some(i10));
1601 assert_eq!(list.previous_job(), Some(i20));
1602 }
1603
1604 #[test]
1605 fn suspending_current_job() {
1606 let mut list = JobList::default();
1607 let i11 = list.add(Job::new(Pid(11)));
1608 let i12 = list.add(Job::new(Pid(12)));
1609 list.set_current_job(i11).unwrap();
1610 list.update_status(Pid(11), ProcessState::stopped(SIGTTOU));
1611 assert_eq!(list.current_job(), Some(i11));
1612 assert_eq!(list.previous_job(), Some(i12));
1613 }
1614
1615 #[test]
1616 fn suspending_previous_job() {
1617 let mut list = JobList::default();
1618 let i11 = list.add(Job::new(Pid(11)));
1619 let i12 = list.add(Job::new(Pid(12)));
1620 list.set_current_job(i11).unwrap();
1621 list.update_status(Pid(12), ProcessState::stopped(SIGTTOU));
1622 assert_eq!(list.current_job(), Some(i12));
1623 assert_eq!(list.previous_job(), Some(i11));
1624 }
1625
1626 #[test]
1627 fn suspending_job_with_running_current_job() {
1628 let mut list = JobList::default();
1629 let i10 = list.add(Job::new(Pid(10)));
1630 let _i11 = list.add(Job::new(Pid(11)));
1631 let i12 = list.add(Job::new(Pid(12)));
1632 list.set_current_job(i10).unwrap();
1633 list.update_status(Pid(12), ProcessState::stopped(SIGTTIN));
1634 assert_eq!(list.current_job(), Some(i12));
1635 assert_eq!(list.previous_job(), Some(i10));
1636 }
1637
1638 #[test]
1639 fn suspending_job_with_running_previous_job() {
1640 let mut list = JobList::default();
1641 let i11 = list.add(Job::new(Pid(11)));
1642 let i12 = list.add(Job::new(Pid(12)));
1643 let mut suspended = Job::new(Pid(10));
1644 suspended.state = ProcessState::stopped(SIGTTIN);
1645 let i10 = list.add(suspended);
1646 assert_eq!(list.current_job(), Some(i10));
1647 assert_eq!(list.previous_job(), Some(i11));
1648
1649 list.update_status(Pid(12), ProcessState::stopped(SIGTTOU));
1650 assert_eq!(list.current_job(), Some(i12));
1651 assert_eq!(list.previous_job(), Some(i10));
1652 }
1653
1654 #[test]
1658 fn do_not_add_job_if_exited() {
1659 let mut env = Env::new_virtual();
1660 let result = add_job_if_suspended(
1661 &mut env,
1662 Pid(123),
1663 ProcessResult::Exited(ExitStatus(42)),
1664 || "foo".to_string(),
1665 );
1666 assert_eq!(result, Continue(ExitStatus(42)));
1667 assert_eq!(env.jobs.len(), 0);
1668 }
1669
1670 #[test]
1671 fn do_not_add_job_if_signaled() {
1672 let mut env = Env::new_virtual();
1673 let signal = signal::Number::from_raw_unchecked(NonZero::new(42).unwrap());
1674 let result = add_job_if_suspended(
1675 &mut env,
1676 Pid(123),
1677 ProcessResult::Signaled {
1678 signal,
1679 core_dump: false,
1680 },
1681 || "foo".to_string(),
1682 );
1683 assert_eq!(result, Continue(ExitStatus::from(signal)));
1684 assert_eq!(env.jobs.len(), 0);
1685 }
1686
1687 #[test]
1688 fn add_job_if_stopped() {
1689 let mut env = Env::new_virtual();
1690 let signal = signal::Number::from_raw_unchecked(NonZero::new(42).unwrap());
1691 let process_result = ProcessResult::Stopped(signal);
1692 let result = add_job_if_suspended(&mut env, Pid(123), process_result, || "foo".to_string());
1693 assert_eq!(result, Continue(ExitStatus::from(signal)));
1694 assert_eq!(env.jobs.len(), 1);
1695 let job = env.jobs.get(0).unwrap();
1696 assert_eq!(job.pid, Pid(123));
1697 assert!(job.job_controlled);
1698 assert_eq!(job.state, ProcessState::Halted(process_result));
1699 assert_eq!(job.name, "foo");
1700 }
1701
1702 #[test]
1703 fn break_if_stopped_and_interactive() {
1704 let mut env = Env::new_virtual();
1705 env.options.set(Interactive, On);
1706 let signal = signal::Number::from_raw_unchecked(NonZero::new(42).unwrap());
1707 let process_result = ProcessResult::Stopped(signal);
1708 let result = add_job_if_suspended(&mut env, Pid(123), process_result, || "foo".to_string());
1709 assert_eq!(
1710 result,
1711 Break(Divert::Interrupt(Some(ExitStatus::from(signal))))
1712 );
1713 assert_eq!(env.jobs.len(), 1);
1714 }
1715}