1use std::process::Child;
15
16#[derive(Clone, Copy, Debug, PartialEq, Eq)]
20pub enum JobState {
21 Running,
23 Stopped,
25 Done,
27}
28
29#[derive(Debug)]
31pub struct JobInfo {
32 pub id: usize,
34 pub pid: i32,
36 pub child: Option<Child>,
38 pub command: String,
40 pub state: JobState,
42 pub is_current: bool,
44}
45
46pub struct JobTable {
51 jobs: Vec<Option<JobInfo>>,
53 current_id: Option<usize>,
55 next_id: usize,
57}
58
59impl Default for JobTable {
60 fn default() -> Self {
61 Self::new()
62 }
63}
64
65impl JobTable {
66 pub fn new() -> Self {
68 JobTable {
69 jobs: Vec::with_capacity(16),
70 current_id: None,
71 next_id: 1,
72 }
73 }
74
75 pub fn peek_next_id(&self) -> usize {
81 self.next_id
82 }
83
84 pub fn add_job(&mut self, child: Child, command: String, state: JobState) -> usize {
86 let id = self.next_id;
87 self.next_id += 1;
88
89 let pid = child.id() as i32;
90 let job = JobInfo {
91 id,
92 pid,
93 child: Some(child),
94 command,
95 state,
96 is_current: true,
97 };
98
99 if let Some(cur_id) = self.current_id {
101 if let Some(j) = self.get_mut_internal(cur_id) {
102 j.is_current = false;
103 }
104 }
105
106 let slot = self.get_free_slot();
108 if slot >= self.jobs.len() {
109 self.jobs.resize_with(slot + 1, || None);
110 }
111 self.jobs[slot] = Some(job);
112 self.current_id = Some(id);
113
114 id
115 }
116
117 pub fn add_pid_job(&mut self, pid: i32, command: String, state: JobState) -> usize {
122 let id = self.next_id;
123 self.next_id += 1;
124 let job = JobInfo {
125 id,
126 pid,
127 child: None,
128 command,
129 state,
130 is_current: true,
131 };
132 if let Some(cur_id) = self.current_id {
133 if let Some(j) = self.get_mut_internal(cur_id) {
134 j.is_current = false;
135 }
136 }
137 let slot = self.get_free_slot();
138 if slot >= self.jobs.len() {
139 self.jobs.resize_with(slot + 1, || None);
140 }
141 self.jobs[slot] = Some(job);
142 self.current_id = Some(id);
143 id
144 }
145
146 fn get_free_slot(&self) -> usize {
147 for (i, slot) in self.jobs.iter().enumerate() {
148 if slot.is_none() {
149 return i;
150 }
151 }
152 self.jobs.len()
153 }
154
155 fn get_mut_internal(&mut self, id: usize) -> Option<&mut JobInfo> {
156 self.jobs.iter_mut().flatten().find(|job| job.id == id)
157 }
158
159 pub fn get(&self, id: usize) -> Option<&JobInfo> {
161 self.jobs
162 .iter()
163 .flatten()
164 .find(|&job| job.id == id)
165 .map(|v| v as _)
166 }
167
168 pub fn get_mut(&mut self, id: usize) -> Option<&mut JobInfo> {
170 self.get_mut_internal(id)
171 }
172
173 pub fn remove(&mut self, id: usize) -> Option<JobInfo> {
175 for slot in self.jobs.iter_mut() {
176 if slot.as_ref().map(|j| j.id == id).unwrap_or(false) {
177 let job = slot.take();
178 if self.current_id == Some(id) {
179 self.current_id = None;
180 }
181 return job;
182 }
183 }
184 None
185 }
186
187 pub fn list(&self) -> Vec<&JobInfo> {
189 self.jobs.iter().filter_map(|j| j.as_ref()).collect()
190 }
191
192 pub fn iter(&self) -> impl Iterator<Item = (usize, &JobInfo)> {
194 self.jobs
195 .iter()
196 .filter_map(|j| j.as_ref().map(|job| (job.id, job)))
197 }
198
199 pub fn count(&self) -> usize {
201 self.jobs.iter().filter(|j| j.is_some()).count()
202 }
203
204 pub fn is_empty(&self) -> bool {
206 self.count() == 0
207 }
208
209 pub fn current(&self) -> Option<&JobInfo> {
211 self.current_id.and_then(|id| self.get(id))
212 }
213
214 pub fn reap_finished(&mut self) -> Vec<JobInfo> {
216 let mut finished = Vec::new();
217
218 for job in self.jobs.iter_mut().flatten() {
219 if let Some(ref mut child) = job.child {
220 match child.try_wait() {
222 Ok(Some(_status)) => {
223 job.state = JobState::Done;
225 }
226 Ok(None) => {
227 }
229 Err(_) => {
230 job.state = JobState::Done;
232 }
233 }
234 }
235 }
236
237 for slot in self.jobs.iter_mut() {
239 if slot
240 .as_ref()
241 .map(|j| j.state == JobState::Done)
242 .unwrap_or(false)
243 {
244 if let Some(job) = slot.take() {
245 finished.push(job);
246 }
247 }
248 }
249
250 finished
251 }
252}
253
254#[cfg(test)]
255mod tests {
256 use super::*;
257
258 #[test]
259 fn test_job_table_new() {
260 let _g = crate::test_util::global_state_lock();
261 let table = JobTable::new();
262 assert!(table.is_empty());
263 }
264
265 #[test]
266 fn test_job_state_enum() {
267 let _g = crate::test_util::global_state_lock();
268 let state = JobState::Running;
269 assert_eq!(state, JobState::Running);
270 assert_ne!(state, JobState::Stopped);
271 assert_ne!(state, JobState::Done);
272 }
273
274 #[test]
275 fn test_add_pid_job_assigns_id() {
276 let _g = crate::test_util::global_state_lock();
277 let mut t = JobTable::new();
278 let id1 = t.add_pid_job(1234, "cmd1".into(), JobState::Running);
279 let id2 = t.add_pid_job(5678, "cmd2".into(), JobState::Running);
280 assert_ne!(id1, id2);
281 assert_eq!(t.list().len(), 2);
282 assert_eq!(t.current().map(|j| j.id), Some(id2));
283 }
284
285 #[test]
286 fn test_remove_drops_current() {
287 let _g = crate::test_util::global_state_lock();
288 let mut t = JobTable::new();
289 let id = t.add_pid_job(99, "x".into(), JobState::Running);
290 assert!(t.remove(id).is_some());
291 assert!(t.is_empty());
292 assert!(t.current().is_none());
293 }
294}