Skip to main content

zsh/
exec_jobs.rs

1//! Executor-side bg-job tracker. NOT a port of `Src/jobs.c`.
2//!
3//! `Src/jobs.c` uses a flat `struct job jobtab[]` global keyed by pid
4//! (ported to `crate::ported::jobs::JOBTAB`). C tracks child processes
5//! through their pid + waitpid(2). Rust prefers safe-Rust ownership
6//! of `std::process::Child` handles so the executor needs a parallel
7//! registry that owns those handles. That's what this file is.
8//!
9//! This module is segregated from `src/ported/jobs.rs` (the faithful
10//! C port) so the port file contains only direct ports of jobs.c
11//! decls. `JobState` / `JobInfo` / `JobTable` here are zshrs runtime
12//! state with no C counterpart by design.
13
14use std::process::Child;
15use std::sync::Mutex;
16
17use crate::ported::jobs::{deletejob, CURJOB, MAXJOB, PREVJOB, THISJOB};
18use crate::ported::jobs::stat;
19use crate::ported::zsh_h::job;
20
21/// Executor-side stand-in for C `printjob`'s done-job delete tail,
22/// `Src/jobs.c:1350-1363`:
23/// ```c
24/// if (jn->stat & STAT_DONE) {
25///     ...
26///     deletejob(jn, 0);
27///     if (job == curjob) { curjob = prevjob; prevjob = job; }
28///     if (job == prevjob) setprevjob();
29/// }
30/// ```
31/// The ported `printjob` (src/ported/jobs.rs) is a pure formatter
32/// returning a String; C's version mutates the table as a side
33/// effect. Every site that calls (or would call) printjob on a
34/// possibly-done job runs this tail so finished jobs leave the table
35/// exactly when they do in C. Lives here (not src/ported/) because
36/// it has no C name of its own — it is the side-effect half of
37/// printjob, split out by the Rust purity refactor.
38pub fn printjob_delete_tail(tab: &mut [job], idx: usize) {
39    if idx >= tab.len() || (tab[idx].stat & stat::DONE) == 0 {
40        return;
41    }
42    deletejob(&mut tab[idx], false); // c:Src/jobs.c:1356
43    let mut cj = CURJOB.get_or_init(|| Mutex::new(-1)).lock().unwrap();
44    let mut pj = PREVJOB.get_or_init(|| Mutex::new(-1)).lock().unwrap();
45    if *cj == idx as i32 {
46        // c:Src/jobs.c:1357-1360
47        *cj = *pj;
48        *pj = idx as i32;
49    }
50    let need_setprev = *pj == idx as i32; // c:Src/jobs.c:1361
51    drop(cj);
52    drop(pj);
53    if need_setprev {
54        setprevjob_locked(tab); // c:Src/jobs.c:1362
55    }
56}
57
58/// `setprevjob` (Src/jobs.c:698-717) body operating on an
59/// already-locked table slice — `printjob_delete_tail` callers hold
60/// the JOBTAB lock, so the re-locking ported `setprevjob()` would
61/// deadlock. Same walk, same candidate order.
62fn setprevjob_locked(tab: &[job]) {
63    let maxjob = *MAXJOB
64        .get_or_init(|| Mutex::new(0))
65        .lock()
66        .expect("maxjob poisoned");
67    let curjob = *CURJOB.get_or_init(|| Mutex::new(-1)).lock().unwrap();
68    let thisjob = *THISJOB.get_or_init(|| Mutex::new(-1)).lock().unwrap();
69    let pick = |want_stopped: bool| -> i32 {
70        for i in (1..=maxjob).rev() {
71            if i >= tab.len() {
72                continue;
73            }
74            let j = &tab[i];
75            let stat_ok = if want_stopped {
76                (j.stat & (stat::INUSE | stat::STOPPED)) == (stat::INUSE | stat::STOPPED)
77            } else {
78                (j.stat & stat::INUSE) != 0
79            };
80            if stat_ok
81                && (j.stat & stat::SUBJOB) == 0
82                && i as i32 != curjob
83                && i as i32 != thisjob
84            {
85                return i as i32;
86            }
87        }
88        -1
89    };
90    let mut found = pick(true); // c:Src/jobs.c:702-707
91    if found < 0 {
92        found = pick(false); // c:Src/jobs.c:709-714
93    }
94    *PREVJOB.get_or_init(|| Mutex::new(-1)).lock().unwrap() = found; // c:716
95}
96
97/// Running-job state tracked alongside each `Child` handle.
98/// Maps to C's `STAT_*` bits but is exposed as a typed enum since
99/// the executor's safe-Rust path doesn't manipulate the bitfield.
100#[derive(Clone, Copy, Debug, PartialEq, Eq)]
101pub enum JobState {
102    /// `Running` variant.
103    Running,
104    /// `Stopped` variant.
105    Stopped,
106    /// `Done` variant.
107    Done,
108}
109
110/// One entry in the executor's bg-job registry.
111#[derive(Debug)]
112pub struct JobInfo {
113    /// `id` field.
114    pub id: usize,
115    /// `pid` field.
116    pub pid: i32,
117    /// `child` field.
118    pub child: Option<Child>,
119    /// `command` field.
120    pub command: String,
121    /// `state` field.
122    pub state: JobState,
123    /// `is_current` field.
124    pub is_current: bool,
125}
126
127/// The executor's bg-job registry. Distinct from the C-port
128/// `JOBTAB` (a `Vec<Job>` keyed by index that mirrors `jobtab[]`):
129/// this table owns the `std::process::Child` handles needed for
130/// `try_wait` / `kill` on the safe-Rust path.
131pub struct JobTable {
132    /// `jobs` field.
133    jobs: Vec<Option<JobInfo>>,
134    /// `current_id` field.
135    current_id: Option<usize>,
136    /// `next_id` field.
137    next_id: usize,
138}
139
140impl Default for JobTable {
141    fn default() -> Self {
142        Self::new()
143    }
144}
145
146impl JobTable {
147    /// `new` — see implementation.
148    pub fn new() -> Self {
149        JobTable {
150            jobs: Vec::with_capacity(16),
151            current_id: None,
152            next_id: 1,
153        }
154    }
155
156    /// Peek at the next id that would be assigned by `add_job`/`add_pid`.
157    /// Used by `wait %N` to distinguish a never-issued id (clear user
158    /// error) from a job that was issued and already reaped (silent
159    /// success in zshrs to keep the `cmd & wait %1` idiom working
160    /// across the races introduced by the threaded job table).
161    pub fn peek_next_id(&self) -> usize {
162        self.next_id
163    }
164
165    /// Add a job with a Child process
166    pub fn add_job(&mut self, child: Child, command: String, state: JobState) -> usize {
167        let id = self.next_id;
168        self.next_id += 1;
169
170        let pid = child.id() as i32;
171        let job = JobInfo {
172            id,
173            pid,
174            child: Some(child),
175            command,
176            state,
177            is_current: true,
178        };
179
180        // Mark previous current as not current
181        if let Some(cur_id) = self.current_id {
182            if let Some(j) = self.get_mut_internal(cur_id) {
183                j.is_current = false;
184            }
185        }
186
187        // Add new job
188        let slot = self.get_free_slot();
189        if slot >= self.jobs.len() {
190            self.jobs.resize_with(slot + 1, || None);
191        }
192        self.jobs[slot] = Some(job);
193        self.current_id = Some(id);
194
195        id
196    }
197
198    /// Register a backgrounded job that was forked via raw `libc::fork()`
199    /// (no `std::process::Child` wrapper). The wait path then has to
200    /// `waitpid(pid)` instead of `Child::wait()`. Used by
201    /// BUILTIN_RUN_BG so `wait` (no args) can synchronize on it.
202    pub fn add_pid_job(&mut self, pid: i32, command: String, state: JobState) -> usize {
203        let id = self.next_id;
204        self.next_id += 1;
205        let job = JobInfo {
206            id,
207            pid,
208            child: None,
209            command,
210            state,
211            is_current: true,
212        };
213        if let Some(cur_id) = self.current_id {
214            if let Some(j) = self.get_mut_internal(cur_id) {
215                j.is_current = false;
216            }
217        }
218        let slot = self.get_free_slot();
219        if slot >= self.jobs.len() {
220            self.jobs.resize_with(slot + 1, || None);
221        }
222        self.jobs[slot] = Some(job);
223        self.current_id = Some(id);
224        id
225    }
226
227    fn get_free_slot(&self) -> usize {
228        for (i, slot) in self.jobs.iter().enumerate() {
229            if slot.is_none() {
230                return i;
231            }
232        }
233        self.jobs.len()
234    }
235
236    fn get_mut_internal(&mut self, id: usize) -> Option<&mut JobInfo> {
237        self.jobs.iter_mut().flatten().find(|job| job.id == id)
238    }
239
240    /// Get a job by ID
241    pub fn get(&self, id: usize) -> Option<&JobInfo> {
242        self.jobs
243            .iter()
244            .flatten()
245            .find(|&job| job.id == id)
246            .map(|v| v as _)
247    }
248
249    /// Get a mutable job by ID
250    pub fn get_mut(&mut self, id: usize) -> Option<&mut JobInfo> {
251        self.get_mut_internal(id)
252    }
253
254    /// Remove a job by ID
255    pub fn remove(&mut self, id: usize) -> Option<JobInfo> {
256        for slot in self.jobs.iter_mut() {
257            if slot.as_ref().map(|j| j.id == id).unwrap_or(false) {
258                let job = slot.take();
259                if self.current_id == Some(id) {
260                    self.current_id = None;
261                }
262                return job;
263            }
264        }
265        None
266    }
267
268    /// List all active jobs
269    pub fn list(&self) -> Vec<&JobInfo> {
270        self.jobs.iter().filter_map(|j| j.as_ref()).collect()
271    }
272
273    /// Iterate over jobs with their IDs (for compatibility)
274    pub fn iter(&self) -> impl Iterator<Item = (usize, &JobInfo)> {
275        self.jobs
276            .iter()
277            .filter_map(|j| j.as_ref().map(|job| (job.id, job)))
278    }
279
280    /// Count number of active jobs
281    pub fn count(&self) -> usize {
282        self.jobs.iter().filter(|j| j.is_some()).count()
283    }
284
285    /// Check if there are any jobs
286    pub fn is_empty(&self) -> bool {
287        self.count() == 0
288    }
289
290    /// Get current job
291    pub fn current(&self) -> Option<&JobInfo> {
292        self.current_id.and_then(|id| self.get(id))
293    }
294
295    /// Reap finished jobs (check for completed processes)
296    pub fn reap_finished(&mut self) -> Vec<JobInfo> {
297        let mut finished = Vec::new();
298
299        for job in self.jobs.iter_mut().flatten() {
300            if let Some(ref mut child) = job.child {
301                // Try to check if child has finished without blocking
302                match child.try_wait() {
303                    Ok(Some(_status)) => {
304                        // Child finished
305                        job.state = JobState::Done;
306                    }
307                    Ok(None) => {
308                        // Still running
309                    }
310                    Err(_) => {
311                        // Error checking, assume done
312                        job.state = JobState::Done;
313                    }
314                }
315            }
316        }
317
318        // Remove done jobs
319        for slot in self.jobs.iter_mut() {
320            if slot
321                .as_ref()
322                .map(|j| j.state == JobState::Done)
323                .unwrap_or(false)
324            {
325                if let Some(job) = slot.take() {
326                    finished.push(job);
327                }
328            }
329        }
330
331        finished
332    }
333}
334
335#[cfg(test)]
336mod tests {
337    use super::*;
338
339    #[test]
340    fn test_job_table_new() {
341        let _g = crate::test_util::global_state_lock();
342        let table = JobTable::new();
343        assert!(table.is_empty());
344    }
345
346    #[test]
347    fn test_job_state_enum() {
348        let _g = crate::test_util::global_state_lock();
349        let state = JobState::Running;
350        assert_eq!(state, JobState::Running);
351        assert_ne!(state, JobState::Stopped);
352        assert_ne!(state, JobState::Done);
353    }
354
355    #[test]
356    fn test_add_pid_job_assigns_id() {
357        let _g = crate::test_util::global_state_lock();
358        let mut t = JobTable::new();
359        let id1 = t.add_pid_job(1234, "cmd1".into(), JobState::Running);
360        let id2 = t.add_pid_job(5678, "cmd2".into(), JobState::Running);
361        assert_ne!(id1, id2);
362        assert_eq!(t.list().len(), 2);
363        assert_eq!(t.current().map(|j| j.id), Some(id2));
364    }
365
366    #[test]
367    fn test_remove_drops_current() {
368        let _g = crate::test_util::global_state_lock();
369        let mut t = JobTable::new();
370        let id = t.add_pid_job(99, "x".into(), JobState::Running);
371        assert!(t.remove(id).is_some());
372        assert!(t.is_empty());
373        assert!(t.current().is_none());
374    }
375}