Skip to main content

shrub_core/
jobs.rs

1//! Job management
2
3use 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/// Manages the jobs that are currently managed by the shell.
14#[derive(Default)]
15pub struct JobManager {
16    /// The jobs that are currently managed by the shell.
17    pub jobs: Vec<Job>,
18}
19
20/// Represents a task that is part of a job.
21pub enum JobTask {
22    /// An external process.
23    External(processes::ChildProcess),
24    /// An internal asynchronous task.
25    Internal(tasks::TaskHandle<Result<ExecutionResult, error::Error>>),
26}
27
28/// Represents the result of waiting on a job task.
29pub enum JobTaskWaitResult {
30    /// The task has completed.
31    Completed(ExecutionResult),
32    /// The task was stopped.
33    Stopped,
34}
35
36impl JobTask {
37    /// Returns whether the task is an external process.
38    pub const fn is_external(&self) -> bool {
39        matches!(self, Self::External(_))
40    }
41
42    /// Waits for the task to complete. Returns the result of the wait.
43    ///
44    /// # Errors
45    ///
46    /// Returns an error if waiting on an external process fails, if an internal
47    /// task fails to join (for example when it panicked or was cancelled),
48    /// or if the internal task itself returns an error.
49    pub async fn wait(&mut self) -> Result<JobTaskWaitResult, error::Error> {
50        match self {
51            Self::External(process) => {
52                let wait_result = process.wait().await?;
53                match wait_result {
54                    processes::ProcessWaitResult::Completed(output) => {
55                        Ok(JobTaskWaitResult::Completed(output.into()))
56                    }
57                    processes::ProcessWaitResult::Stopped => Ok(JobTaskWaitResult::Stopped),
58                }
59            }
60            Self::Internal(handle) => Ok(JobTaskWaitResult::Completed(handle.await??)),
61        }
62    }
63
64    /// Polls the task for completion. Returns `Some(result)` if the task has
65    /// completed, or `None` if it is still running. The result is the
66    /// execution result of the task. Behaves in a best-effort manner; if an
67    /// internal error occurs during polling, it will return `None`.
68    fn poll(&mut self) -> Option<Result<ExecutionResult, error::Error>> {
69        match self {
70            Self::External(process) => {
71                let check_result = process.poll();
72                check_result.map(|polled_result| polled_result.map(|output| output.into()))
73            }
74            Self::Internal(handle) => {
75                let checkable_handle = handle;
76                checkable_handle.now_or_never().and_then(|r| r.ok())
77            }
78        }
79    }
80}
81
82impl JobManager {
83    /// Returns a new job manager.
84    pub fn new() -> Self {
85        Self::default()
86    }
87
88    /// Adds a job to the job manager and marks it as the current job;
89    /// returns an immutable reference to the job.
90    ///
91    /// # Arguments
92    ///
93    /// * `job` - The job to add.
94    ///
95    /// # Panics
96    ///
97    /// Does not panic in practice: the job list is non-empty because the job
98    /// was pushed just before the final lookup.
99    pub fn add_as_current(&mut self, mut job: Job) -> &Job {
100        for j in &mut self.jobs {
101            if matches!(j.annotation, JobAnnotation::Current) {
102                j.annotation = JobAnnotation::Previous;
103                break;
104            }
105        }
106
107        let id = self.jobs.len() + 1;
108        job.id = id;
109        job.annotation = JobAnnotation::Current;
110        self.jobs.push(job);
111
112        #[allow(clippy::unwrap_used, reason = "we just pushed an element")]
113        self.jobs.last().unwrap()
114    }
115
116    /// Returns the current job, if there is one.
117    pub fn current_job(&self) -> Option<&Job> {
118        self.jobs
119            .iter()
120            .find(|j| matches!(j.annotation, JobAnnotation::Current))
121    }
122
123    /// Returns a mutable reference to the current job, if there is one.
124    pub fn current_job_mut(&mut self) -> Option<&mut Job> {
125        self.jobs
126            .iter_mut()
127            .find(|j| matches!(j.annotation, JobAnnotation::Current))
128    }
129
130    /// Returns the previous job, if there is one.
131    pub fn prev_job(&self) -> Option<&Job> {
132        self.jobs
133            .iter()
134            .find(|j| matches!(j.annotation, JobAnnotation::Previous))
135    }
136
137    /// Returns a mutable reference to the previous job, if there is one.
138    pub fn prev_job_mut(&mut self) -> Option<&mut Job> {
139        self.jobs
140            .iter_mut()
141            .find(|j| matches!(j.annotation, JobAnnotation::Previous))
142    }
143
144    /// Tries to resolve the given job specification to a job.
145    ///
146    /// # Arguments
147    ///
148    /// * `job_spec` - The job specification to resolve.
149    pub fn resolve_job_spec(&mut self, job_spec: &str) -> Option<&mut Job> {
150        let remainder = job_spec.strip_prefix('%')?;
151
152        match remainder {
153            "%" | "+" => self.current_job_mut(),
154            "-" => self.prev_job_mut(),
155            s if s.chars().all(char::is_numeric) => {
156                let id = s.parse::<usize>().ok()?;
157                self.jobs.iter_mut().find(|j| j.id == id)
158            }
159            _ => {
160                tracing::warn!(target: trace_categories::UNIMPLEMENTED, "unimplemented: job spec naming command: '{job_spec}'");
161                None
162            }
163        }
164    }
165
166    /// Waits for all managed jobs to complete.
167    ///
168    /// # Errors
169    ///
170    /// Returns the first error from [`Job::wait`]; jobs after it are not waited
171    /// on.
172    pub async fn wait_all(&mut self) -> Result<Vec<Job>, error::Error> {
173        for job in &mut self.jobs {
174            job.wait().await?;
175        }
176
177        Ok(self.sweep_completed_jobs())
178    }
179
180    /// Polls all managed jobs for completion.
181    ///
182    /// # Errors
183    ///
184    /// Returns the first error from [`Job::poll_done`].
185    pub fn poll(&mut self) -> Result<Vec<JobResult>, error::Error> {
186        let mut results = Vec::with_capacity(self.jobs.len());
187
188        let mut i = 0;
189        while i != self.jobs.len() {
190            if let Some(result) = self.jobs[i].poll_done()? {
191                let job = self.jobs.remove(i);
192                results.push((job, result));
193            } else if matches!(self.jobs[i].state, JobState::Done) {
194                // TODO(jobs): This is a workaround to remove jobs that are done
195                // but for which we don't know what happened.
196                results.push((self.jobs.remove(i), Ok(ExecutionResult::success())));
197            } else {
198                i += 1;
199            }
200        }
201
202        Ok(results)
203    }
204
205    fn sweep_completed_jobs(&mut self) -> Vec<Job> {
206        let mut completed_jobs = vec![];
207
208        let mut i = 0;
209        while i != self.jobs.len() {
210            if self.jobs[i].tasks.is_empty() {
211                completed_jobs.push(self.jobs.remove(i));
212            } else {
213                i += 1;
214            }
215        }
216
217        completed_jobs
218    }
219}
220
221/// Represents the current execution state of a job.
222#[derive(Clone)]
223pub enum JobState {
224    /// Unknown state.
225    Unknown,
226    /// The job is running.
227    Running,
228    /// The job is stopped.
229    Stopped,
230    /// The job has completed.
231    Done,
232}
233
234impl Display for JobState {
235    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
236        match self {
237            Self::Unknown => write!(f, "Unknown"),
238            Self::Running => write!(f, "Running"),
239            Self::Stopped => write!(f, "Stopped"),
240            Self::Done => write!(f, "Done"),
241        }
242    }
243}
244
245/// Represents an annotation for a job.
246#[derive(Clone)]
247pub enum JobAnnotation {
248    /// No annotation.
249    None,
250    /// The job is the current job.
251    Current,
252    /// The job is the previous job.
253    Previous,
254}
255
256impl Display for JobAnnotation {
257    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
258        match self {
259            Self::None => write!(f, ""),
260            Self::Current => write!(f, "+"),
261            Self::Previous => write!(f, "-"),
262        }
263    }
264}
265
266/// Encapsulates a set of processes managed by the shell as a single unit.
267pub struct Job {
268    /// The tasks that make up the job.
269    tasks: VecDeque<JobTask>,
270
271    /// The annotation of the job (e.g., current, previous).
272    annotation: JobAnnotation,
273
274    /// The shell-internal ID of the job.
275    pub id: usize,
276
277    /// The command line of the job.
278    pub command_line: String,
279
280    /// The current operational state of the job.
281    pub state: JobState,
282}
283
284impl Display for Job {
285    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
286        write!(
287            f,
288            "[{}]{:3}{}\t{}",
289            self.id, self.annotation, self.state, self.command_line
290        )
291    }
292}
293
294impl Job {
295    /// Returns a new job object.
296    ///
297    /// # Arguments
298    ///
299    /// * `children` - The job's known child processes.
300    /// * `command_line` - The command line of the job.
301    /// * `state` - The current operational state of the job.
302    pub(crate) fn new<I>(tasks: I, command_line: String, state: JobState) -> Self
303    where
304        I: IntoIterator<Item = JobTask>,
305    {
306        Self {
307            id: 0,
308            tasks: tasks.into_iter().collect(),
309            annotation: JobAnnotation::None,
310            command_line,
311            state,
312        }
313    }
314
315    /// Returns a pid-style string for the job.
316    pub fn to_pid_style_string(&self) -> String {
317        let display_pid = self
318            .representative_pid()
319            .map_or(Cow::Borrowed("<pid unknown>"), |pid| {
320                Cow::Owned(pid.to_string())
321            });
322        std::format!("[{}]{}\t{}", self.id, self.annotation, display_pid)
323    }
324
325    /// Returns the annotation of the job.
326    pub fn annotation(&self) -> JobAnnotation {
327        self.annotation.clone()
328    }
329
330    /// Returns the command name of the job.
331    pub fn command_name(&self) -> &str {
332        self.command_line
333            .split_ascii_whitespace()
334            .next()
335            .unwrap_or_default()
336    }
337
338    /// Returns whether the job is the current job.
339    pub const fn is_current(&self) -> bool {
340        matches!(self.annotation, JobAnnotation::Current)
341    }
342
343    /// Returns whether the job is the previous job.
344    pub const fn is_prev(&self) -> bool {
345        matches!(self.annotation, JobAnnotation::Previous)
346    }
347
348    /// Polls whether the job has completed.
349    ///
350    /// # Errors
351    ///
352    /// Currently never fails; task errors are reported through the inner
353    /// result.
354    pub fn poll_done(
355        &mut self,
356    ) -> Result<Option<Result<ExecutionResult, error::Error>>, error::Error> {
357        let mut result: Option<Result<ExecutionResult, error::Error>> = None;
358
359        tracing::debug!(target: trace_categories::JOBS, "Polling job {} for completion...", self.id);
360
361        while !self.tasks.is_empty() {
362            let task = &mut self.tasks[0];
363            match task.poll() {
364                Some(r) => {
365                    self.tasks.remove(0);
366                    result = Some(r);
367                }
368                None => {
369                    return Ok(None);
370                }
371            }
372        }
373
374        tracing::debug!(target: trace_categories::JOBS, "Job {} has completed.", self.id);
375
376        self.state = JobState::Done;
377
378        Ok(result)
379    }
380
381    /// Waits for the job to complete.
382    ///
383    /// # Errors
384    ///
385    /// Returns an error if waiting on any of the job's tasks fails; see
386    /// [`JobTask::wait`].
387    pub async fn wait(&mut self) -> Result<ExecutionResult, error::Error> {
388        let mut result = ExecutionResult::success();
389
390        while let Some(task) = self.tasks.back_mut() {
391            match task.wait().await? {
392                JobTaskWaitResult::Completed(execution_result) => {
393                    result = execution_result;
394                    self.tasks.pop_back();
395                }
396                JobTaskWaitResult::Stopped => {
397                    self.state = JobState::Stopped;
398                    return Ok(ExecutionResult::stopped());
399                }
400            }
401        }
402
403        self.state = JobState::Done;
404
405        Ok(result)
406    }
407
408    /// Moves the job to execute in the background.
409    ///
410    /// # Errors
411    ///
412    /// Returns [`error::ErrorKind::FailedToSendSignal`] if the job has no
413    /// process to continue, or a platform error if the continue signal
414    /// cannot be sent to a stopped job.
415    pub fn move_to_background(&mut self) -> Result<(), error::Error> {
416        self.continue_job()
417    }
418
419    /// Moves the job to execute in the foreground.
420    ///
421    /// # Errors
422    ///
423    /// Returns the same errors as [`Job::move_to_background`]. Failing to hand
424    /// the terminal to the job is logged and does not fail the call.
425    pub fn move_to_foreground(&mut self) -> Result<(), error::Error> {
426        // The job gets the terminal before it's continued, so that it doesn't
427        // find itself in the background and stop again on reading from
428        // the terminal. (Without a terminal, as in a script that
429        // enables job control, there's none to give it, and it's continued
430        // anyway.)
431        if let Some(pgid) = self.own_process_group()
432            && let Err(err) = sys::terminal::move_to_foreground(pgid)
433        {
434            tracing::debug!(target: trace_categories::JOBS, "not giving job {} the terminal: {err}", self.id);
435        }
436
437        self.continue_job()
438    }
439
440    /// Continues the job, and records it running.
441    fn continue_job(&mut self) -> Result<(), error::Error> {
442        if matches!(self.state, JobState::Stopped) {
443            self.continue_processes()?;
444            self.state = JobState::Running;
445        } else {
446            // As bash does for `fg`, continue it anyway: it may have been
447            // stopped without the shell noticing yet (e.g., by
448            // reading from the terminal in the background).
449            let _ = self.continue_processes();
450        }
451
452        Ok(())
453    }
454
455    /// Continues the job's stopped processes: all of its process group, if it
456    /// has one.
457    fn continue_processes(&self) -> Result<(), error::Error> {
458        if let Some(pgid) = self.own_process_group() {
459            sys::signal::continue_process_group(pgid)
460        } else if let Some(pid) = self.representative_pid() {
461            sys::signal::continue_process(pid)
462        } else {
463            Err(error::ErrorKind::FailedToSendSignal.into())
464        }
465    }
466
467    /// Checks whether the job can be signaled.
468    ///
469    /// # Errors
470    ///
471    /// Returns [`error::ErrorKind::FailedToSendSignal`] if the job has no
472    /// process, or a platform error such as
473    /// [`error::ErrorKind::NoSuchProcess`] or
474    /// [`error::ErrorKind::PermissionDenied`] if the process or group cannot be
475    /// signaled.
476    pub fn check_signalable(&self) -> Result<(), error::Error> {
477        if let Some(pgid) = self.own_process_group() {
478            sys::signal::check_process_group_signalable(pgid)
479        } else if let Some(pid) = self.representative_pid() {
480            sys::signal::check_signalable(pid)
481        } else {
482            Err(error::ErrorKind::FailedToSendSignal.into())
483        }
484    }
485
486    /// Kills the job: sends the signal to all of its process group, if it has
487    /// one. A job sent SIGTERM or SIGHUP is also continued, so that it acts
488    /// on it if it's stopped.
489    ///
490    /// # Arguments
491    ///
492    /// * `signal` - The signal to send to the job.
493    ///
494    /// # Errors
495    ///
496    /// Returns [`error::ErrorKind::FailedToSendSignal`] if the job has no
497    /// process, or a platform error if sending the signal fails. A
498    /// subsequent failure to continue a stopped job is also returned.
499    pub fn kill(&mut self, signal: traps::TrapSignal) -> Result<(), error::Error> {
500        if let Some(pgid) = self.own_process_group() {
501            sys::signal::kill_process_group(pgid, signal)?;
502        } else if let Some(pid) = self.representative_pid() {
503            sys::signal::kill_process(pid, signal)?;
504        } else {
505            return Err(error::ErrorKind::FailedToSendSignal.into());
506        }
507
508        // As bash does, continue a stopped job sent SIGTERM or SIGHUP, so that
509        // it acts on it; and one not known to be stopped, which may
510        // have stopped without the shell's noticing.
511        if sys::signal::continue_stopped_on(signal) {
512            self.continue_job()?;
513        }
514
515        Ok(())
516    }
517
518    /// Tries to retrieve a "representative" pid for the job.
519    pub fn representative_pid(&self) -> Option<sys::process::ProcessId> {
520        for task in &self.tasks {
521            match task {
522                JobTask::External(p) => {
523                    if let Some(pid) = p.pid() {
524                        return Some(pid);
525                    }
526                }
527                JobTask::Internal(_) => (),
528            }
529        }
530        None
531    }
532
533    /// Tries to retrieve the process group ID (PGID) of the job: that of its
534    /// own process group, if it has one, or else its representative pid.
535    pub fn process_group_id(&self) -> Option<sys::process::ProcessId> {
536        self.own_process_group()
537            .or_else(|| self.representative_pid())
538    }
539
540    /// Returns the job's own process group, if it has one: under job control,
541    /// its processes run in a group of their own, which needn't be led by
542    /// the first of them.
543    fn own_process_group(&self) -> Option<sys::process::ProcessId> {
544        self.tasks.iter().find_map(|task| match task {
545            JobTask::External(child) => child.pgid(),
546            JobTask::Internal(_) => None,
547        })
548    }
549}