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    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    /// Polls the task for completion. Returns `Some(result)` if the task has
59    /// completed, or `None` if it is still running. The result is the
60    /// execution result of the task. Behaves in a best-effort manner; if an
61    /// internal error occurs during polling, it will return `None`.
62    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    /// Returns a new job manager.
78    pub fn new() -> Self {
79        Self::default()
80    }
81
82    /// Adds a job to the job manager and marks it as the current job;
83    /// returns an immutable reference to the job.
84    ///
85    /// # Arguments
86    ///
87    /// * `job` - The job to add.
88    #[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    /// Returns the current job, if there is one.
110    pub fn current_job(&self) -> Option<&Job> {
111        self.jobs
112            .iter()
113            .find(|j| matches!(j.annotation, JobAnnotation::Current))
114    }
115
116    /// Returns a mutable reference to the current job, if there is one.
117    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    /// Returns the previous job, if there is one.
124    pub fn prev_job(&self) -> Option<&Job> {
125        self.jobs
126            .iter()
127            .find(|j| matches!(j.annotation, JobAnnotation::Previous))
128    }
129
130    /// Returns a mutable reference to the previous job, if there is one.
131    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    /// Tries to resolve the given job specification to a job.
138    ///
139    /// # Arguments
140    ///
141    /// * `job_spec` - The job specification to resolve.
142    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    /// Waits for all managed jobs to complete.
160    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    /// Polls all managed jobs for completion.
169    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                // TODO(jobs): This is a workaround to remove jobs that are done
179                // but for which we don't know what happened.
180                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/// Represents the current execution state of a job.
206#[derive(Clone)]
207pub enum JobState {
208    /// Unknown state.
209    Unknown,
210    /// The job is running.
211    Running,
212    /// The job is stopped.
213    Stopped,
214    /// The job has completed.
215    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/// Represents an annotation for a job.
230#[derive(Clone)]
231pub enum JobAnnotation {
232    /// No annotation.
233    None,
234    /// The job is the current job.
235    Current,
236    /// The job is the previous job.
237    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
250/// Encapsulates a set of processes managed by the shell as a single unit.
251pub struct Job {
252    /// The tasks that make up the job.
253    tasks: VecDeque<JobTask>,
254
255    /// The annotation of the job (e.g., current, previous).
256    annotation: JobAnnotation,
257
258    /// The shell-internal ID of the job.
259    pub id: usize,
260
261    /// The command line of the job.
262    pub command_line: String,
263
264    /// The current operational state of the job.
265    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    /// Returns a new job object.
280    ///
281    /// # Arguments
282    ///
283    /// * `children` - The job's known child processes.
284    /// * `command_line` - The command line of the job.
285    /// * `state` - The current operational state of the job.
286    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    /// Returns a pid-style string for the job.
300    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    /// Returns the annotation of the job.
310    pub fn annotation(&self) -> JobAnnotation {
311        self.annotation.clone()
312    }
313
314    /// Returns the command name of the job.
315    pub fn command_name(&self) -> &str {
316        self.command_line
317            .split_ascii_whitespace()
318            .next()
319            .unwrap_or_default()
320    }
321
322    /// Returns whether the job is the current job.
323    pub const fn is_current(&self) -> bool {
324        matches!(self.annotation, JobAnnotation::Current)
325    }
326
327    /// Returns whether the job is the previous job.
328    pub const fn is_prev(&self) -> bool {
329        matches!(self.annotation, JobAnnotation::Previous)
330    }
331
332    /// Polls whether the job has completed.
333    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    /// Waits for the job to complete.
361    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    /// Moves the job to execute in the background.
383    pub fn move_to_background(&mut self) -> Result<(), error::Error> {
384        self.continue_job()
385    }
386
387    /// Moves the job to execute in the foreground.
388    pub fn move_to_foreground(&mut self) -> Result<(), error::Error> {
389        // The job gets the terminal before it's continued, so that it doesn't
390        // find itself in the background and stop again on reading from
391        // the terminal. (Without a terminal, as in a script that
392        // enables job control, there's none to give it, and it's continued
393        // anyway.)
394        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    /// Continues the job, and records it running.
404    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            // As bash does for `fg`, continue it anyway: it may have been
410            // stopped without the shell noticing yet (e.g., by
411            // reading from the terminal in the background).
412            let _ = self.continue_processes();
413        }
414
415        Ok(())
416    }
417
418    /// Continues the job's stopped processes: all of its process group, if it
419    /// has one.
420    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    /// Checks whether the job can be signaled.
431    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    /// Kills the job: sends the signal to all of its process group, if it has
442    /// one. A job sent SIGTERM or SIGHUP is also continued, so that it acts
443    /// on it if it's stopped.
444    ///
445    /// # Arguments
446    ///
447    /// * `signal` - The signal to send to the job.
448    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        // As bash does, continue a stopped job sent SIGTERM or SIGHUP, so that
458        // it acts on it; and one not known to be stopped, which may
459        // have stopped without the shell's noticing.
460        if sys::signal::continue_stopped_on(signal) {
461            self.continue_job()?;
462        }
463
464        Ok(())
465    }
466
467    /// Tries to retrieve a "representative" pid for the job.
468    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    /// Tries to retrieve the process group ID (PGID) of the job: that of its
483    /// own process group, if it has one, or else its representative pid.
484    pub fn process_group_id(&self) -> Option<sys::process::ProcessId> {
485        self.own_process_group()
486            .or_else(|| self.representative_pid())
487    }
488
489    /// Returns the job's own process group, if it has one: under job control,
490    /// its processes run in a group of their own, which needn't be led by
491    /// the first of them.
492    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}