1use std::borrow::Cow;
4use std::collections::VecDeque;
5use std::fmt::Display;
6
7use futures::FutureExt;
8
9use crate::{ExecutionResult, error, processes, sys, tasks, trace_categories, traps};
10
11pub(crate) type JobResult = (Job, Result<ExecutionResult, error::Error>);
12
13#[derive(Default)]
15pub struct JobManager {
16 pub jobs: Vec<Job>,
18}
19
20pub enum JobTask {
22 External(processes::ChildProcess),
24 Internal(tasks::TaskHandle<Result<ExecutionResult, error::Error>>),
26}
27
28pub enum JobTaskWaitResult {
30 Completed(ExecutionResult),
32 Stopped,
34}
35
36impl JobTask {
37 pub const fn is_external(&self) -> bool {
39 matches!(self, Self::External(_))
40 }
41
42 pub async fn wait(&mut self) -> Result<JobTaskWaitResult, error::Error> {
44 match self {
45 Self::External(process) => {
46 let wait_result = process.wait().await?;
47 match wait_result {
48 processes::ProcessWaitResult::Completed(output) => {
49 Ok(JobTaskWaitResult::Completed(output.into()))
50 }
51 processes::ProcessWaitResult::Stopped => Ok(JobTaskWaitResult::Stopped),
52 }
53 }
54 Self::Internal(handle) => Ok(JobTaskWaitResult::Completed(handle.await??)),
55 }
56 }
57
58 fn poll(&mut self) -> Option<Result<ExecutionResult, error::Error>> {
63 match self {
64 Self::External(process) => {
65 let check_result = process.poll();
66 check_result.map(|polled_result| polled_result.map(|output| output.into()))
67 }
68 Self::Internal(handle) => {
69 let checkable_handle = handle;
70 checkable_handle.now_or_never().and_then(|r| r.ok())
71 }
72 }
73 }
74}
75
76impl JobManager {
77 pub fn new() -> Self {
79 Self::default()
80 }
81
82 #[allow(
89 clippy::missing_panics_doc,
90 reason = "push() guarantees the vector length is >= 1"
91 )]
92 pub fn add_as_current(&mut self, mut job: Job) -> &Job {
93 for j in &mut self.jobs {
94 if matches!(j.annotation, JobAnnotation::Current) {
95 j.annotation = JobAnnotation::Previous;
96 break;
97 }
98 }
99
100 let id = self.jobs.len() + 1;
101 job.id = id;
102 job.annotation = JobAnnotation::Current;
103 self.jobs.push(job);
104
105 #[allow(clippy::unwrap_used, reason = "we just pushed an element")]
106 self.jobs.last().unwrap()
107 }
108
109 pub fn current_job(&self) -> Option<&Job> {
111 self.jobs
112 .iter()
113 .find(|j| matches!(j.annotation, JobAnnotation::Current))
114 }
115
116 pub fn current_job_mut(&mut self) -> Option<&mut Job> {
118 self.jobs
119 .iter_mut()
120 .find(|j| matches!(j.annotation, JobAnnotation::Current))
121 }
122
123 pub fn prev_job(&self) -> Option<&Job> {
125 self.jobs
126 .iter()
127 .find(|j| matches!(j.annotation, JobAnnotation::Previous))
128 }
129
130 pub fn prev_job_mut(&mut self) -> Option<&mut Job> {
132 self.jobs
133 .iter_mut()
134 .find(|j| matches!(j.annotation, JobAnnotation::Previous))
135 }
136
137 pub fn resolve_job_spec(&mut self, job_spec: &str) -> Option<&mut Job> {
143 let remainder = job_spec.strip_prefix('%')?;
144
145 match remainder {
146 "%" | "+" => self.current_job_mut(),
147 "-" => self.prev_job_mut(),
148 s if s.chars().all(char::is_numeric) => {
149 let id = s.parse::<usize>().ok()?;
150 self.jobs.iter_mut().find(|j| j.id == id)
151 }
152 _ => {
153 tracing::warn!(target: trace_categories::UNIMPLEMENTED, "unimplemented: job spec naming command: '{job_spec}'");
154 None
155 }
156 }
157 }
158
159 pub async fn wait_all(&mut self) -> Result<Vec<Job>, error::Error> {
161 for job in &mut self.jobs {
162 job.wait().await?;
163 }
164
165 Ok(self.sweep_completed_jobs())
166 }
167
168 pub fn poll(&mut self) -> Result<Vec<JobResult>, error::Error> {
170 let mut results = Vec::with_capacity(self.jobs.len());
171
172 let mut i = 0;
173 while i != self.jobs.len() {
174 if let Some(result) = self.jobs[i].poll_done()? {
175 let job = self.jobs.remove(i);
176 results.push((job, result));
177 } else if matches!(self.jobs[i].state, JobState::Done) {
178 results.push((self.jobs.remove(i), Ok(ExecutionResult::success())));
181 } else {
182 i += 1;
183 }
184 }
185
186 Ok(results)
187 }
188
189 fn sweep_completed_jobs(&mut self) -> Vec<Job> {
190 let mut completed_jobs = vec![];
191
192 let mut i = 0;
193 while i != self.jobs.len() {
194 if self.jobs[i].tasks.is_empty() {
195 completed_jobs.push(self.jobs.remove(i));
196 } else {
197 i += 1;
198 }
199 }
200
201 completed_jobs
202 }
203}
204
205#[derive(Clone)]
207pub enum JobState {
208 Unknown,
210 Running,
212 Stopped,
214 Done,
216}
217
218impl Display for JobState {
219 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
220 match self {
221 Self::Unknown => write!(f, "Unknown"),
222 Self::Running => write!(f, "Running"),
223 Self::Stopped => write!(f, "Stopped"),
224 Self::Done => write!(f, "Done"),
225 }
226 }
227}
228
229#[derive(Clone)]
231pub enum JobAnnotation {
232 None,
234 Current,
236 Previous,
238}
239
240impl Display for JobAnnotation {
241 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
242 match self {
243 Self::None => write!(f, ""),
244 Self::Current => write!(f, "+"),
245 Self::Previous => write!(f, "-"),
246 }
247 }
248}
249
250pub struct Job {
252 tasks: VecDeque<JobTask>,
254
255 annotation: JobAnnotation,
257
258 pub id: usize,
260
261 pub command_line: String,
263
264 pub state: JobState,
266}
267
268impl Display for Job {
269 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
270 write!(
271 f,
272 "[{}]{:3}{}\t{}",
273 self.id, self.annotation, self.state, self.command_line
274 )
275 }
276}
277
278impl Job {
279 pub(crate) fn new<I>(tasks: I, command_line: String, state: JobState) -> Self
287 where
288 I: IntoIterator<Item = JobTask>,
289 {
290 Self {
291 id: 0,
292 tasks: tasks.into_iter().collect(),
293 annotation: JobAnnotation::None,
294 command_line,
295 state,
296 }
297 }
298
299 pub fn to_pid_style_string(&self) -> String {
301 let display_pid = self
302 .representative_pid()
303 .map_or(Cow::Borrowed("<pid unknown>"), |pid| {
304 Cow::Owned(pid.to_string())
305 });
306 std::format!("[{}]{}\t{}", self.id, self.annotation, display_pid)
307 }
308
309 pub fn annotation(&self) -> JobAnnotation {
311 self.annotation.clone()
312 }
313
314 pub fn command_name(&self) -> &str {
316 self.command_line
317 .split_ascii_whitespace()
318 .next()
319 .unwrap_or_default()
320 }
321
322 pub const fn is_current(&self) -> bool {
324 matches!(self.annotation, JobAnnotation::Current)
325 }
326
327 pub const fn is_prev(&self) -> bool {
329 matches!(self.annotation, JobAnnotation::Previous)
330 }
331
332 pub fn poll_done(
334 &mut self,
335 ) -> Result<Option<Result<ExecutionResult, error::Error>>, error::Error> {
336 let mut result: Option<Result<ExecutionResult, error::Error>> = None;
337
338 tracing::debug!(target: trace_categories::JOBS, "Polling job {} for completion...", self.id);
339
340 while !self.tasks.is_empty() {
341 let task = &mut self.tasks[0];
342 match task.poll() {
343 Some(r) => {
344 self.tasks.remove(0);
345 result = Some(r);
346 }
347 None => {
348 return Ok(None);
349 }
350 }
351 }
352
353 tracing::debug!(target: trace_categories::JOBS, "Job {} has completed.", self.id);
354
355 self.state = JobState::Done;
356
357 Ok(result)
358 }
359
360 pub async fn wait(&mut self) -> Result<ExecutionResult, error::Error> {
362 let mut result = ExecutionResult::success();
363
364 while let Some(task) = self.tasks.back_mut() {
365 match task.wait().await? {
366 JobTaskWaitResult::Completed(execution_result) => {
367 result = execution_result;
368 self.tasks.pop_back();
369 }
370 JobTaskWaitResult::Stopped => {
371 self.state = JobState::Stopped;
372 return Ok(ExecutionResult::stopped());
373 }
374 }
375 }
376
377 self.state = JobState::Done;
378
379 Ok(result)
380 }
381
382 pub fn move_to_background(&mut self) -> Result<(), error::Error> {
384 self.continue_job()
385 }
386
387 pub fn move_to_foreground(&mut self) -> Result<(), error::Error> {
389 if let Some(pgid) = self.own_process_group()
395 && let Err(err) = sys::terminal::move_to_foreground(pgid)
396 {
397 tracing::debug!(target: trace_categories::JOBS, "not giving job {} the terminal: {err}", self.id);
398 }
399
400 self.continue_job()
401 }
402
403 fn continue_job(&mut self) -> Result<(), error::Error> {
405 if matches!(self.state, JobState::Stopped) {
406 self.continue_processes()?;
407 self.state = JobState::Running;
408 } else {
409 let _ = self.continue_processes();
413 }
414
415 Ok(())
416 }
417
418 fn continue_processes(&self) -> Result<(), error::Error> {
421 if let Some(pgid) = self.own_process_group() {
422 sys::signal::continue_process_group(pgid)
423 } else if let Some(pid) = self.representative_pid() {
424 sys::signal::continue_process(pid)
425 } else {
426 Err(error::ErrorKind::FailedToSendSignal.into())
427 }
428 }
429
430 pub fn check_signalable(&self) -> Result<(), error::Error> {
432 if let Some(pgid) = self.own_process_group() {
433 sys::signal::check_process_group_signalable(pgid)
434 } else if let Some(pid) = self.representative_pid() {
435 sys::signal::check_signalable(pid)
436 } else {
437 Err(error::ErrorKind::FailedToSendSignal.into())
438 }
439 }
440
441 pub fn kill(&mut self, signal: traps::TrapSignal) -> Result<(), error::Error> {
449 if let Some(pgid) = self.own_process_group() {
450 sys::signal::kill_process_group(pgid, signal)?;
451 } else if let Some(pid) = self.representative_pid() {
452 sys::signal::kill_process(pid, signal)?;
453 } else {
454 return Err(error::ErrorKind::FailedToSendSignal.into());
455 }
456
457 if sys::signal::continue_stopped_on(signal) {
461 self.continue_job()?;
462 }
463
464 Ok(())
465 }
466
467 pub fn representative_pid(&self) -> Option<sys::process::ProcessId> {
469 for task in &self.tasks {
470 match task {
471 JobTask::External(p) => {
472 if let Some(pid) = p.pid() {
473 return Some(pid);
474 }
475 }
476 JobTask::Internal(_) => (),
477 }
478 }
479 None
480 }
481
482 pub fn process_group_id(&self) -> Option<sys::process::ProcessId> {
485 self.own_process_group()
486 .or_else(|| self.representative_pid())
487 }
488
489 fn own_process_group(&self) -> Option<sys::process::ProcessId> {
493 self.tasks.iter().find_map(|task| match task {
494 JobTask::External(child) => child.pgid(),
495 JobTask::Internal(_) => None,
496 })
497 }
498}