Skip to main content

zsh/
jobs.rs

1//! Job control for zshrs
2//!
3//! Port from zsh/Src/jobs.c
4//!
5//! Provides job control, process management, and signal handling for jobs.
6
7use std::process::Child;
8use std::time::{Duration, Instant};
9
10/// Job status flags
11pub mod stat {
12    pub const STOPPED: u32 = 1 << 0; // Job is stopped
13    pub const DONE: u32 = 1 << 1; // Job is finished
14    pub const SUBJOB: u32 = 1 << 2; // Job is a subjob
15    pub const CURSH: u32 = 1 << 3; // Last pipeline elem in current shell
16    pub const SUPERJOB: u32 = 1 << 4; // Job is a superjob
17    pub const WASSUPER: u32 = 1 << 5; // Was a superjob
18    pub const INUSE: u32 = 1 << 6; // Entry in use
19    pub const BUILTIN: u32 = 1 << 7; // Job has builtin
20    pub const DISOWN: u32 = 1 << 8; // Disowned
21    pub const NOTIFY: u32 = 1 << 9; // Notify when done
22    pub const ATTACH: u32 = 1 << 10; // Attached to tty
23}
24
25/// Special process status values
26pub const SP_RUNNING: i32 = -1;
27
28/// Maximum pipestats
29pub const MAX_PIPESTATS: usize = 256;
30
31/// Process timing information
32#[derive(Clone, Debug, Default)]
33pub struct TimeInfo {
34    pub user_time: Duration,
35    pub sys_time: Duration,
36}
37
38/// A single process in a pipeline
39#[derive(Clone, Debug)]
40pub struct Process {
41    pub pid: i32,
42    pub status: i32,
43    pub start_time: Option<Instant>,
44    pub end_time: Option<Instant>,
45    pub ti: TimeInfo,
46    pub text: String,
47}
48
49impl Process {
50    pub fn new(pid: i32) -> Self {
51        Process {
52            pid,
53            status: SP_RUNNING,
54            start_time: Some(Instant::now()),
55            end_time: None,
56            ti: TimeInfo::default(),
57            text: String::new(),
58        }
59    }
60
61    pub fn is_running(&self) -> bool {
62        self.status == SP_RUNNING
63    }
64
65    pub fn is_stopped(&self) -> bool {
66        // WIFSTOPPED equivalent
67        self.status & 0xff == 0x7f
68    }
69
70    pub fn is_signaled(&self) -> bool {
71        // WIFSIGNALED equivalent
72        (self.status & 0x7f) > 0 && (self.status & 0x7f) < 0x7f
73    }
74
75    pub fn exit_status(&self) -> i32 {
76        // WEXITSTATUS equivalent
77        (self.status >> 8) & 0xff
78    }
79
80    pub fn term_sig(&self) -> i32 {
81        // WTERMSIG equivalent
82        self.status & 0x7f
83    }
84
85    pub fn stop_sig(&self) -> i32 {
86        // WSTOPSIG equivalent
87        (self.status >> 8) & 0xff
88    }
89}
90
91/// A job (pipeline)
92#[derive(Clone, Debug)]
93pub struct Job {
94    pub stat: u32,
95    pub gleader: i32,           // Process group leader
96    pub procs: Vec<Process>,    // Processes in job
97    pub auxprocs: Vec<Process>, // Auxiliary processes
98    pub other: usize,           // For superjobs: subjob index
99    pub filelist: Vec<String>,  // Temp files to delete
100    pub text: String,           // Job text for display
101}
102
103impl Job {
104    pub fn new() -> Self {
105        Job {
106            stat: 0,
107            gleader: 0,
108            procs: Vec::new(),
109            auxprocs: Vec::new(),
110            other: 0,
111            filelist: Vec::new(),
112            text: String::new(),
113        }
114    }
115
116    pub fn is_done(&self) -> bool {
117        (self.stat & stat::DONE) != 0
118    }
119
120    pub fn is_stopped(&self) -> bool {
121        (self.stat & stat::STOPPED) != 0
122    }
123
124    pub fn is_superjob(&self) -> bool {
125        (self.stat & stat::SUPERJOB) != 0
126    }
127
128    pub fn is_subjob(&self) -> bool {
129        (self.stat & stat::SUBJOB) != 0
130    }
131
132    pub fn is_inuse(&self) -> bool {
133        (self.stat & stat::INUSE) != 0
134    }
135
136    pub fn has_procs(&self) -> bool {
137        !self.procs.is_empty() || !self.auxprocs.is_empty()
138    }
139
140    pub fn make_running(&mut self) {
141        self.stat &= !stat::STOPPED;
142        for proc in &mut self.procs {
143            if proc.is_stopped() {
144                proc.status = SP_RUNNING;
145            }
146        }
147    }
148}
149
150impl Default for Job {
151    fn default() -> Self {
152        Self::new()
153    }
154}
155
156/// Simple job info for exec.rs compatibility
157#[derive(Debug)]
158pub struct JobInfo {
159    pub id: usize,
160    pub pid: i32,
161    pub child: Option<Child>,
162    pub command: String,
163    pub state: JobState,
164    pub is_current: bool,
165}
166
167/// Job table compatible with exec.rs
168pub struct JobTable {
169    jobs: Vec<Option<JobInfo>>,
170    current_id: Option<usize>,
171    next_id: usize,
172}
173
174impl Default for JobTable {
175    fn default() -> Self {
176        Self::new()
177    }
178}
179
180impl JobTable {
181    pub fn new() -> Self {
182        JobTable {
183            jobs: Vec::with_capacity(16),
184            current_id: None,
185            next_id: 1,
186        }
187    }
188
189    /// Add a job with a Child process
190    pub fn add_job(&mut self, child: Child, command: String, state: JobState) -> usize {
191        let id = self.next_id;
192        self.next_id += 1;
193
194        let pid = child.id() as i32;
195        let job = JobInfo {
196            id,
197            pid,
198            child: Some(child),
199            command,
200            state,
201            is_current: true,
202        };
203
204        // Mark previous current as not current
205        if let Some(cur_id) = self.current_id {
206            if let Some(j) = self.get_mut_internal(cur_id) {
207                j.is_current = false;
208            }
209        }
210
211        // Add new job
212        let slot = self.get_free_slot();
213        if slot >= self.jobs.len() {
214            self.jobs.resize_with(slot + 1, || None);
215        }
216        self.jobs[slot] = Some(job);
217        self.current_id = Some(id);
218
219        id
220    }
221
222    fn get_free_slot(&self) -> usize {
223        for (i, slot) in self.jobs.iter().enumerate() {
224            if slot.is_none() {
225                return i;
226            }
227        }
228        self.jobs.len()
229    }
230
231    fn get_mut_internal(&mut self, id: usize) -> Option<&mut JobInfo> {
232        for job in self.jobs.iter_mut().flatten() {
233            if job.id == id {
234                return Some(job);
235            }
236        }
237        None
238    }
239
240    /// Get a job by ID
241    pub fn get(&self, id: usize) -> Option<&JobInfo> {
242        for job in self.jobs.iter().flatten() {
243            if job.id == id {
244                return Some(job);
245            }
246        }
247        None
248    }
249
250    /// Get a mutable job by ID
251    pub fn get_mut(&mut self, id: usize) -> Option<&mut JobInfo> {
252        self.get_mut_internal(id)
253    }
254
255    /// Remove a job by ID
256    pub fn remove(&mut self, id: usize) -> Option<JobInfo> {
257        for slot in self.jobs.iter_mut() {
258            if slot.as_ref().map(|j| j.id == id).unwrap_or(false) {
259                let job = slot.take();
260                if self.current_id == Some(id) {
261                    self.current_id = None;
262                }
263                return job;
264            }
265        }
266        None
267    }
268
269    /// List all active jobs
270    pub fn list(&self) -> Vec<&JobInfo> {
271        self.jobs.iter().filter_map(|j| j.as_ref()).collect()
272    }
273
274    /// Iterate over jobs with their IDs (for compatibility)
275    pub fn iter(&self) -> impl Iterator<Item = (usize, &JobInfo)> {
276        self.jobs
277            .iter()
278            .filter_map(|j| j.as_ref().map(|job| (job.id, job)))
279    }
280
281    /// Count number of active jobs
282    pub fn count(&self) -> usize {
283        self.jobs.iter().filter(|j| j.is_some()).count()
284    }
285
286    /// Check if there are any jobs
287    pub fn is_empty(&self) -> bool {
288        self.count() == 0
289    }
290
291    /// Get current job
292    pub fn current(&self) -> Option<&JobInfo> {
293        self.current_id.and_then(|id| self.get(id))
294    }
295
296    /// Reap finished jobs (check for completed processes)
297    pub fn reap_finished(&mut self) -> Vec<JobInfo> {
298        let mut finished = Vec::new();
299
300        for slot in self.jobs.iter_mut() {
301            if let Some(job) = slot {
302                if let Some(ref mut child) = job.child {
303                    // Try to check if child has finished without blocking
304                    match child.try_wait() {
305                        Ok(Some(_status)) => {
306                            // Child finished
307                            job.state = JobState::Done;
308                        }
309                        Ok(None) => {
310                            // Still running
311                        }
312                        Err(_) => {
313                            // Error checking, assume done
314                            job.state = JobState::Done;
315                        }
316                    }
317                }
318            }
319        }
320
321        // Remove done jobs
322        for slot in self.jobs.iter_mut() {
323            if slot
324                .as_ref()
325                .map(|j| j.state == JobState::Done)
326                .unwrap_or(false)
327            {
328                if let Some(job) = slot.take() {
329                    finished.push(job);
330                }
331            }
332        }
333
334        finished
335    }
336}
337
338/// Format a job for display
339pub fn format_job(
340    job: &Job,
341    job_num: usize,
342    cur_job: Option<usize>,
343    prev_job: Option<usize>,
344) -> String {
345    let marker = if Some(job_num) == cur_job {
346        '+'
347    } else if Some(job_num) == prev_job {
348        '-'
349    } else {
350        ' '
351    };
352
353    let status = if job.is_done() {
354        "done"
355    } else if job.is_stopped() {
356        "suspended"
357    } else {
358        "running"
359    };
360
361    format!("[{}]{} {:10}  {}", job_num, marker, status, job.text)
362}
363
364#[cfg(test)]
365mod tests {
366    use super::*;
367
368    #[test]
369    fn test_process_new() {
370        let proc = Process::new(1234);
371        assert_eq!(proc.pid, 1234);
372        assert!(proc.is_running());
373    }
374
375    #[test]
376    fn test_job_new() {
377        let job = Job::new();
378        assert_eq!(job.stat, 0);
379        assert!(!job.is_done());
380        assert!(!job.is_stopped());
381    }
382
383    #[test]
384    fn test_job_table_new() {
385        let table = JobTable::new();
386        assert!(table.is_empty());
387    }
388
389    #[test]
390    fn test_job_table_remove() {
391        // This test would require spawning a real process, skipping for now
392    }
393
394    #[test]
395    fn test_job_make_running() {
396        let mut job = Job::new();
397        job.stat |= stat::STOPPED;
398        job.procs.push(Process {
399            status: 0x007f,
400            ..Process::new(1234)
401        }); // Stopped
402
403        job.make_running();
404        assert!(!job.is_stopped());
405        assert!(job.procs[0].is_running());
406    }
407
408    #[test]
409    fn test_format_job() {
410        let mut job = Job::new();
411        job.text = "vim file.txt".to_string();
412        job.stat |= stat::STOPPED;
413
414        let formatted = format_job(&job, 1, Some(1), None);
415        assert!(formatted.contains("[1]+"));
416        assert!(formatted.contains("suspended"));
417        assert!(formatted.contains("vim file.txt"));
418    }
419
420    #[test]
421    fn test_job_state_enum() {
422        let state = JobState::Running;
423        assert_eq!(state, JobState::Running);
424        assert_ne!(state, JobState::Stopped);
425        assert_ne!(state, JobState::Done);
426    }
427}
428
429/// Job state for simpler tracking
430#[derive(Clone, Copy, Debug, PartialEq, Eq)]
431pub enum JobState {
432    Running,
433    Stopped,
434    Done,
435}
436
437/// Simple job entry for executor compatibility
438#[derive(Debug)]
439pub struct JobEntry {
440    pub pid: i32,
441    pub child: Option<Child>,
442    pub command: String,
443    pub state: JobState,
444    pub is_current: bool,
445}
446
447/// Send a signal to a process
448#[cfg(unix)]
449pub fn send_signal(pid: i32, sig: nix::sys::signal::Signal) -> Result<(), String> {
450    use nix::sys::signal::kill;
451    use nix::unistd::Pid;
452
453    kill(Pid::from_raw(pid), sig).map_err(|e| e.to_string())
454}
455
456#[cfg(not(unix))]
457pub fn send_signal(_pid: i32, _sig: i32) -> Result<(), String> {
458    Err("Signal sending not supported on this platform".to_string())
459}
460
461/// Continue a stopped job
462#[cfg(unix)]
463pub fn continue_job(pid: i32) -> Result<(), String> {
464    use nix::sys::signal::{kill, Signal};
465    use nix::unistd::Pid;
466
467    kill(Pid::from_raw(pid), Signal::SIGCONT).map_err(|e| e.to_string())
468}
469
470#[cfg(not(unix))]
471pub fn continue_job(_pid: i32) -> Result<(), String> {
472    Err("Job control not supported on this platform".to_string())
473}
474
475/// Wait for a job to complete
476#[cfg(unix)]
477pub fn wait_for_job(pid: i32) -> Result<i32, String> {
478    use nix::sys::wait::{waitpid, WaitStatus};
479    use nix::unistd::Pid;
480
481    loop {
482        match waitpid(Pid::from_raw(pid), None) {
483            Ok(WaitStatus::Exited(_, code)) => return Ok(code),
484            Ok(WaitStatus::Signaled(_, sig, _)) => return Ok(128 + sig as i32),
485            Ok(WaitStatus::Stopped(_, _)) => return Ok(128),
486            Ok(_) => continue,
487            Err(nix::errno::Errno::ECHILD) => return Ok(0),
488            Err(e) => return Err(e.to_string()),
489        }
490    }
491}
492
493#[cfg(not(unix))]
494pub fn wait_for_job(_pid: i32) -> Result<i32, String> {
495    Err("Job waiting not supported on this platform".to_string())
496}
497
498/// Wait for a child process
499pub fn wait_for_child(child: &mut Child) -> Result<i32, String> {
500    match child.wait() {
501        Ok(status) => Ok(status.code().unwrap_or(0)),
502        Err(e) => Err(e.to_string()),
503    }
504}
505
506/// Get clock ticks per second (from jobs.c get_clktck lines 720-748)
507pub fn get_clktck() -> i64 {
508    #[cfg(unix)]
509    {
510        use std::sync::OnceLock;
511        static CLKTCK: OnceLock<i64> = OnceLock::new();
512        *CLKTCK.get_or_init(|| unsafe { libc::sysconf(libc::_SC_CLK_TCK) as i64 })
513    }
514    #[cfg(not(unix))]
515    {
516        100 // Default on non-Unix
517    }
518}
519
520/// Format time as hh:mm:ss.xx (from jobs.c printhhmmss lines 752-765)
521pub fn format_hhmmss(secs: f64) -> String {
522    let mins = (secs / 60.0) as i32;
523    let hours = mins / 60;
524    let secs = secs - (mins * 60) as f64;
525    let mins = mins - (hours * 60);
526
527    if hours > 0 {
528        format!("{}:{:02}:{:05.2}", hours, mins, secs)
529    } else if mins > 0 {
530        format!("{}:{:05.2}", mins, secs)
531    } else {
532        format!("{:.3}", secs)
533    }
534}
535
536/// Time format specifiers (from jobs.c printtime lines 768-949)
537pub fn format_time(
538    elapsed_secs: f64,
539    user_secs: f64,
540    system_secs: f64,
541    format: &str,
542    job_name: &str,
543) -> String {
544    let mut result = String::new();
545    let total_time = user_secs + system_secs;
546    let percent = if elapsed_secs > 0.0 {
547        (100.0 * total_time / elapsed_secs) as i32
548    } else {
549        0
550    };
551
552    let mut chars = format.chars().peekable();
553    while let Some(c) = chars.next() {
554        if c == '%' {
555            match chars.next() {
556                Some('E') => result.push_str(&format!("{:.2}s", elapsed_secs)),
557                Some('U') => result.push_str(&format!("{:.2}s", user_secs)),
558                Some('S') => result.push_str(&format!("{:.2}s", system_secs)),
559                Some('P') => result.push_str(&format!("{}%", percent)),
560                Some('J') => result.push_str(job_name),
561                Some('m') => match chars.next() {
562                    Some('E') => result.push_str(&format!("{:.0}ms", elapsed_secs * 1000.0)),
563                    Some('U') => result.push_str(&format!("{:.0}ms", user_secs * 1000.0)),
564                    Some('S') => result.push_str(&format!("{:.0}ms", system_secs * 1000.0)),
565                    _ => result.push_str("%m"),
566                },
567                Some('u') => match chars.next() {
568                    Some('E') => result.push_str(&format!("{:.0}us", elapsed_secs * 1_000_000.0)),
569                    Some('U') => result.push_str(&format!("{:.0}us", user_secs * 1_000_000.0)),
570                    Some('S') => result.push_str(&format!("{:.0}us", system_secs * 1_000_000.0)),
571                    _ => result.push_str("%u"),
572                },
573                Some('n') => match chars.next() {
574                    Some('E') => {
575                        result.push_str(&format!("{:.0}ns", elapsed_secs * 1_000_000_000.0))
576                    }
577                    Some('U') => result.push_str(&format!("{:.0}ns", user_secs * 1_000_000_000.0)),
578                    Some('S') => {
579                        result.push_str(&format!("{:.0}ns", system_secs * 1_000_000_000.0))
580                    }
581                    _ => result.push_str("%n"),
582                },
583                Some('*') => match chars.next() {
584                    Some('E') => result.push_str(&format_hhmmss(elapsed_secs)),
585                    Some('U') => result.push_str(&format_hhmmss(user_secs)),
586                    Some('S') => result.push_str(&format_hhmmss(system_secs)),
587                    _ => result.push_str("%*"),
588                },
589                Some('%') => result.push('%'),
590                Some(other) => {
591                    result.push('%');
592                    result.push(other);
593                }
594                None => result.push('%'),
595            }
596        } else {
597            result.push(c);
598        }
599    }
600    result
601}
602
603/// Default time format (from jobs.c DEFAULT_TIMEFMT)
604pub const DEFAULT_TIMEFMT: &str = "%J  %U user %S system %P cpu %*E total";
605
606/// Time a command's execution
607pub struct CommandTimer {
608    start: std::time::Instant,
609    job_name: String,
610}
611
612impl CommandTimer {
613    pub fn new(job_name: &str) -> Self {
614        CommandTimer {
615            start: std::time::Instant::now(),
616            job_name: job_name.to_string(),
617        }
618    }
619
620    pub fn elapsed(&self) -> Duration {
621        self.start.elapsed()
622    }
623
624    pub fn format(
625        &self,
626        user_time: Duration,
627        sys_time: Duration,
628        format_str: Option<&str>,
629    ) -> String {
630        let elapsed = self.start.elapsed().as_secs_f64();
631        let user = user_time.as_secs_f64();
632        let sys = sys_time.as_secs_f64();
633
634        format_time(
635            elapsed,
636            user,
637            sys,
638            format_str.unwrap_or(DEFAULT_TIMEFMT),
639            &self.job_name,
640        )
641    }
642}
643
644/// Pipestats management (from jobs.c storepipestats lines 420-454)
645pub struct PipeStats {
646    stats: Vec<i32>,
647}
648
649impl Default for PipeStats {
650    fn default() -> Self {
651        Self::new()
652    }
653}
654
655impl PipeStats {
656    pub fn new() -> Self {
657        PipeStats { stats: Vec::new() }
658    }
659
660    pub fn clear(&mut self) {
661        self.stats.clear();
662    }
663
664    pub fn add(&mut self, status: i32) {
665        if self.stats.len() < MAX_PIPESTATS {
666            self.stats.push(status);
667        }
668    }
669
670    pub fn get(&self) -> &[i32] {
671        &self.stats
672    }
673
674    pub fn len(&self) -> usize {
675        self.stats.len()
676    }
677
678    pub fn is_empty(&self) -> bool {
679        self.stats.is_empty()
680    }
681
682    pub fn pipefail_status(&self) -> i32 {
683        *self.stats.iter().rev().find(|&&s| s != 0).unwrap_or(&0)
684    }
685}
686
687/// Signal message lookup (from jobs.c sigmsg lines 1106-1118)
688pub fn sigmsg(sig: i32) -> &'static str {
689    match sig {
690        libc::SIGHUP => "hangup",
691        libc::SIGINT => "interrupt",
692        libc::SIGQUIT => "quit",
693        libc::SIGILL => "illegal instruction",
694        libc::SIGTRAP => "trace trap",
695        libc::SIGABRT => "abort",
696        libc::SIGBUS => "bus error",
697        libc::SIGFPE => "floating point exception",
698        libc::SIGKILL => "killed",
699        libc::SIGUSR1 => "user-defined signal 1",
700        libc::SIGSEGV => "segmentation fault",
701        libc::SIGUSR2 => "user-defined signal 2",
702        libc::SIGPIPE => "broken pipe",
703        libc::SIGALRM => "alarm",
704        libc::SIGTERM => "terminated",
705        libc::SIGCHLD => "child exited",
706        libc::SIGCONT => "continued",
707        libc::SIGSTOP => "stopped (signal)",
708        libc::SIGTSTP => "stopped",
709        libc::SIGTTIN => "stopped (tty input)",
710        libc::SIGTTOU => "stopped (tty output)",
711        libc::SIGURG => "urgent I/O condition",
712        libc::SIGXCPU => "CPU time exceeded",
713        libc::SIGXFSZ => "file size exceeded",
714        libc::SIGVTALRM => "virtual timer expired",
715        libc::SIGPROF => "profiling timer expired",
716        libc::SIGWINCH => "window changed",
717        libc::SIGIO => "I/O ready",
718        libc::SIGSYS => "bad system call",
719        _ => "unknown signal",
720    }
721}
722
723/// Format process status for display (from jobs.c printjob lines 1136-1400)
724pub fn format_process_status(status: i32) -> String {
725    if status == SP_RUNNING {
726        "running".to_string()
727    } else if (status & 0x7f) == 0 {
728        // Exited normally
729        let code = (status >> 8) & 0xff;
730        if code == 0 {
731            "done".to_string()
732        } else {
733            format!("exit {}", code)
734        }
735    } else if (status & 0xff) == 0x7f {
736        // Stopped
737        let sig = (status >> 8) & 0xff;
738        format!("suspended ({})", sigmsg(sig))
739    } else {
740        // Signaled
741        let sig = status & 0x7f;
742        let core = (status >> 7) & 1;
743        if core != 0 {
744            format!("{} (core dumped)", sigmsg(sig))
745        } else {
746            sigmsg(sig).to_string()
747        }
748    }
749}
750
751/// Print job in long format (from jobs.c printjob)
752pub fn format_job_long(
753    job_num: usize,
754    current: bool,
755    pid: i32,
756    status: &str,
757    text: &str,
758) -> String {
759    let marker = if current { '+' } else { '-' };
760    format!("[{}]  {} {:>5} {}  {}", job_num, marker, pid, status, text)
761}
762
763/// Print job in short format
764pub fn format_job_short(job_num: usize, current: bool, status: &str, text: &str) -> String {
765    let marker = if current { '+' } else { '-' };
766    format!("[{}]  {} {}  {}", job_num, marker, status, text)
767}
768
769/// Background status tracking (from jobs.c bgstatus)
770pub struct BgStatus {
771    statuses: std::collections::HashMap<i32, i32>,
772}
773
774impl Default for BgStatus {
775    fn default() -> Self {
776        Self::new()
777    }
778}
779
780impl BgStatus {
781    pub fn new() -> Self {
782        BgStatus {
783            statuses: std::collections::HashMap::new(),
784        }
785    }
786
787    pub fn add(&mut self, pid: i32, status: i32) {
788        self.statuses.insert(pid, status);
789    }
790
791    pub fn get(&self, pid: i32) -> Option<i32> {
792        self.statuses.get(&pid).copied()
793    }
794
795    pub fn remove(&mut self, pid: i32) -> Option<i32> {
796        self.statuses.remove(&pid)
797    }
798
799    pub fn clear(&mut self) {
800        self.statuses.clear();
801    }
802}
803
804/// Wait for a specific PID (from jobs.c waitforpid lines 1627-1663)
805pub fn waitforpid(pid: i32) -> Option<i32> {
806    #[cfg(unix)]
807    {
808        use std::os::unix::process::ExitStatusExt;
809        loop {
810            let mut status: i32 = 0;
811            let result = unsafe { libc::waitpid(pid, &mut status, 0) };
812            if result == pid {
813                if libc::WIFEXITED(status) {
814                    return Some(libc::WEXITSTATUS(status));
815                } else if libc::WIFSIGNALED(status) {
816                    return Some(128 + libc::WTERMSIG(status));
817                } else if libc::WIFSTOPPED(status) {
818                    return None;
819                }
820            } else if result == -1 {
821                return None;
822            }
823        }
824    }
825    #[cfg(not(unix))]
826    {
827        let _ = pid;
828        None
829    }
830}
831
832/// Wait for job (from jobs.c zwaitjob lines 1673-1750)
833pub fn waitjob(job: &mut Job) -> Option<i32> {
834    if job.procs.is_empty() {
835        return Some(0);
836    }
837
838    let mut last_status = 0;
839    for proc in &mut job.procs {
840        if proc.is_running() {
841            if let Some(status) = waitforpid(proc.pid) {
842                proc.status = make_status(status);
843                last_status = status;
844            }
845        } else {
846            last_status = proc.exit_status();
847        }
848    }
849
850    job.stat |= stat::DONE;
851    Some(last_status)
852}
853
854/// Make status from exit code
855pub fn make_status(code: i32) -> i32 {
856    code << 8
857}
858
859/// Make status from signal
860pub fn make_signal_status(sig: i32) -> i32 {
861    sig
862}
863
864/// Check if job has pending children (from jobs.c havefiles lines 1604-1616)
865pub fn havefiles(job: &Job) -> bool {
866    !job.filelist.is_empty()
867}
868
869/// Delete job (from jobs.c deletejob lines 1511-1526)
870pub fn deletejob(job: &mut Job, disowning: bool) {
871    if !disowning {
872        job.filelist.clear();
873    }
874    job.procs.clear();
875    job.auxprocs.clear();
876    job.stat = 0;
877}
878
879/// Free job (from jobs.c freejob lines 1456-1508)
880pub fn freejob(job: &mut Job, notify: bool) {
881    let _ = notify;
882    job.procs.clear();
883    job.auxprocs.clear();
884    job.filelist.clear();
885    job.stat = 0;
886    job.gleader = 0;
887    job.text.clear();
888}
889
890/// Add process to job (from jobs.c addproc lines 1537-1597)
891pub fn addproc(job: &mut Job, pid: i32, text: &str, aux: bool) {
892    let proc = Process::new(pid);
893    let proc = Process {
894        pid,
895        status: SP_RUNNING,
896        text: text.to_string(),
897        ..proc
898    };
899
900    if aux {
901        job.auxprocs.push(proc);
902    } else {
903        if job.gleader == 0 {
904            job.gleader = pid;
905        }
906        job.procs.push(proc);
907    }
908
909    job.stat &= !stat::DONE;
910}
911
912/// Kill process group (from jobs.c killjob lines 2040-2085)
913pub fn killjob(job: &Job, sig: i32) -> bool {
914    #[cfg(unix)]
915    {
916        if job.gleader > 0 {
917            let result = unsafe { libc::killpg(job.gleader, sig) };
918            return result == 0;
919        }
920
921        let mut success = true;
922        for proc in &job.procs {
923            if proc.is_running() {
924                let result = unsafe { libc::kill(proc.pid, sig) };
925                if result != 0 {
926                    success = false;
927                }
928            }
929        }
930        success
931    }
932    #[cfg(not(unix))]
933    {
934        let _ = (job, sig);
935        false
936    }
937}
938
939/// Continue job in foreground (from jobs.c fg)
940pub fn fg_job(job: &mut Job) -> Option<i32> {
941    #[cfg(unix)]
942    {
943        if (job.stat & stat::STOPPED) != 0 {
944            if job.gleader > 0 {
945                unsafe { libc::killpg(job.gleader, libc::SIGCONT) };
946            } else {
947                for proc in &job.procs {
948                    unsafe { libc::kill(proc.pid, libc::SIGCONT) };
949                }
950            }
951            job.stat &= !stat::STOPPED;
952        }
953
954        waitjob(job)
955    }
956    #[cfg(not(unix))]
957    {
958        let _ = job;
959        None
960    }
961}
962
963/// Continue job in background (from jobs.c bg)
964pub fn bg_job(job: &mut Job) -> bool {
965    #[cfg(unix)]
966    {
967        if (job.stat & stat::STOPPED) != 0 {
968            if job.gleader > 0 {
969                unsafe { libc::killpg(job.gleader, libc::SIGCONT) };
970            } else {
971                for proc in &job.procs {
972                    unsafe { libc::kill(proc.pid, libc::SIGCONT) };
973                }
974            }
975            job.stat &= !stat::STOPPED;
976            return true;
977        }
978        false
979    }
980    #[cfg(not(unix))]
981    {
982        let _ = job;
983        false
984    }
985}
986
987/// Disown job (from jobs.c disown)
988pub fn disown_job(job: &mut Job) {
989    job.stat |= stat::DISOWN;
990}
991
992/// Check if all processes in job are done
993pub fn job_is_done(job: &Job) -> bool {
994    (job.stat & stat::DONE) != 0 || job.procs.iter().all(|p| !p.is_running())
995}
996
997/// Check if job is stopped
998pub fn job_is_stopped(job: &Job) -> bool {
999    (job.stat & stat::STOPPED) != 0 || job.procs.iter().any(|p| p.is_stopped())
1000}
1001
1002/// Get job text (combined process commands)
1003pub fn get_job_text(job: &Job) -> String {
1004    if !job.text.is_empty() {
1005        return job.text.clone();
1006    }
1007    job.procs
1008        .iter()
1009        .map(|p| p.text.as_str())
1010        .collect::<Vec<_>>()
1011        .join(" | ")
1012}
1013
1014/// Super job tracking (from jobs.c super_job lines 393-417)
1015pub fn super_job(jobtab: &[Job], job_idx: usize) -> Option<usize> {
1016    for (i, job) in jobtab.iter().enumerate() {
1017        if (job.stat & stat::SUPERJOB) != 0 && job.other == job_idx {
1018            return Some(i);
1019        }
1020    }
1021    None
1022}
1023
1024/// Set current/previous job (from jobs.c setjobpwn lines 697-745)
1025pub struct JobPointers {
1026    pub cur_job: Option<usize>,
1027    pub prev_job: Option<usize>,
1028}
1029
1030impl JobPointers {
1031    pub fn new() -> Self {
1032        JobPointers {
1033            cur_job: None,
1034            prev_job: None,
1035        }
1036    }
1037
1038    pub fn set_current(&mut self, job: usize) {
1039        if Some(job) != self.cur_job {
1040            self.prev_job = self.cur_job;
1041            self.cur_job = Some(job);
1042        }
1043    }
1044
1045    pub fn clear(&mut self, job: usize) {
1046        if self.cur_job == Some(job) {
1047            self.cur_job = self.prev_job;
1048            self.prev_job = None;
1049        } else if self.prev_job == Some(job) {
1050            self.prev_job = None;
1051        }
1052    }
1053}
1054
1055impl Default for JobPointers {
1056    fn default() -> Self {
1057        Self::new()
1058    }
1059}
1060
1061// ---------------------------------------------------------------------------
1062// Missing functions from jobs.c
1063// ---------------------------------------------------------------------------
1064
1065/// Parse job specification string (from jobs.c getjob lines 2063-2165)
1066///
1067/// Syntax: %N (by number), %+ or %% (current), %- (previous),
1068/// %str (by command prefix), %?str (by substring)
1069pub fn getjob(spec: &str, table: &JobTable, ptrs: &JobPointers) -> Option<usize> {
1070    if spec.is_empty() {
1071        return ptrs.cur_job;
1072    }
1073
1074    let spec = if spec.starts_with('%') {
1075        &spec[1..]
1076    } else {
1077        spec
1078    };
1079
1080    match spec {
1081        "+" | "%" | "" => ptrs.cur_job,
1082        "-" => ptrs.prev_job,
1083        _ => {
1084            // Try as number
1085            if let Ok(n) = spec.parse::<usize>() {
1086                if table.get(n).is_some() {
1087                    return Some(n);
1088                }
1089                return None;
1090            }
1091
1092            // ?string - search by substring
1093            if let Some(substr) = spec.strip_prefix('?') {
1094                for (id, job) in table.iter() {
1095                    if job.command.contains(substr) {
1096                        return Some(id);
1097                    }
1098                }
1099                return None;
1100            }
1101
1102            // string - search by prefix
1103            for (id, job) in table.iter() {
1104                if job.command.starts_with(spec) {
1105                    return Some(id);
1106                }
1107            }
1108
1109            None
1110        }
1111    }
1112}
1113
1114/// Find job by command name (from jobs.c findjobnam)
1115pub fn findjobnam(name: &str, table: &JobTable) -> Option<usize> {
1116    for (id, job) in table.iter() {
1117        if job.command == name {
1118            return Some(id);
1119        }
1120    }
1121    None
1122}
1123
1124/// Check if string is a number (from jobs.c isanum)
1125pub fn isanum(s: &str) -> bool {
1126    !s.is_empty() && s.chars().all(|c| c.is_ascii_digit())
1127}
1128
1129/// Initialize jobs subsystem (from jobs.c init_jobs)
1130pub fn init_jobs() -> (JobTable, JobPointers) {
1131    (JobTable::new(), JobPointers::new())
1132}
1133
1134/// Acquire process group (from jobs.c acquire_pgrp)
1135#[cfg(unix)]
1136pub fn acquire_pgrp() -> bool {
1137    unsafe {
1138        let mypgrp = libc::getpgrp();
1139        let tpgrp = libc::tcgetpgrp(0);
1140        if tpgrp == -1 || tpgrp == mypgrp {
1141            return true;
1142        }
1143        // We need to be in the foreground process group
1144        if libc::setpgid(0, 0) == 0 {
1145            libc::tcsetpgrp(0, libc::getpgrp());
1146            return true;
1147        }
1148        false
1149    }
1150}
1151
1152/// Store pipestats from job (from jobs.c storepipestats)
1153pub fn storepipestats(job: &Job) -> Vec<i32> {
1154    job.procs.iter().map(|p| p.status).collect()
1155}
1156
1157/// Clear the job table (from jobs.c clearjobtab)
1158pub fn clearjobtab(table: &mut JobTable, ptrs: &mut JobPointers) {
1159    table.jobs.clear();
1160    table.next_id = 1;
1161    ptrs.cur_job = None;
1162    ptrs.prev_job = None;
1163}
1164
1165/// Scan jobs and print changed status (from jobs.c scanjobs)
1166pub fn scanjobs(table: &JobTable) -> Vec<String> {
1167    let mut output = Vec::new();
1168    for (id, job) in table.iter() {
1169        let state_str = match job.state {
1170            JobState::Running => "running",
1171            JobState::Done => "done",
1172            JobState::Stopped => "stopped",
1173            _ => "unknown",
1174        };
1175        output.push(format!("[{}]  {}  {}", id, state_str, job.command));
1176    }
1177    output
1178}
1179
1180/// Shell time accounting (from jobs.c shelltime)
1181#[derive(Debug, Clone, Default)]
1182pub struct ChildTimes {
1183    pub user_sec: f64,
1184    pub sys_sec: f64,
1185}
1186
1187pub fn shelltime() -> ChildTimes {
1188    #[cfg(unix)]
1189    {
1190        let mut usage: libc::rusage = unsafe { std::mem::zeroed() };
1191        if unsafe { libc::getrusage(libc::RUSAGE_SELF, &mut usage) } == 0 {
1192            return ChildTimes {
1193                user_sec: usage.ru_utime.tv_sec as f64
1194                    + usage.ru_utime.tv_usec as f64 / 1_000_000.0,
1195                sys_sec: usage.ru_stime.tv_sec as f64 + usage.ru_stime.tv_usec as f64 / 1_000_000.0,
1196            };
1197        }
1198    }
1199    ChildTimes::default()
1200}
1201
1202/// Get children's time accounting
1203pub fn childtime() -> ChildTimes {
1204    #[cfg(unix)]
1205    {
1206        let mut usage: libc::rusage = unsafe { std::mem::zeroed() };
1207        if unsafe { libc::getrusage(libc::RUSAGE_CHILDREN, &mut usage) } == 0 {
1208            return ChildTimes {
1209                user_sec: usage.ru_utime.tv_sec as f64
1210                    + usage.ru_utime.tv_usec as f64 / 1_000_000.0,
1211                sys_sec: usage.ru_stime.tv_sec as f64 + usage.ru_stime.tv_usec as f64 / 1_000_000.0,
1212            };
1213        }
1214    }
1215    ChildTimes::default()
1216}
1217
1218/// Update process status after waitpid (from jobs.c update_process)
1219pub fn update_process(proc: &mut Process, status: i32) {
1220    proc.end_time = Some(Instant::now());
1221    proc.status = status;
1222}
1223
1224/// Find a process by PID in the job table (from jobs.c findproc)
1225pub fn findproc(jobtab: &[Job], pid: i32) -> Option<(usize, usize, bool)> {
1226    for (ji, job) in jobtab.iter().enumerate() {
1227        for (pi, proc) in job.procs.iter().enumerate() {
1228            if proc.pid == pid {
1229                return Some((ji, pi, false));
1230            }
1231        }
1232        for (pi, proc) in job.auxprocs.iter().enumerate() {
1233            if proc.pid == pid {
1234                return Some((ji, pi, true));
1235            }
1236        }
1237    }
1238    None
1239}
1240
1241/// Update job status after process change (from jobs.c update_job)
1242pub fn update_job(job: &mut Job) -> bool {
1243    // Check if all aux procs are done
1244    for proc in &job.auxprocs {
1245        if proc.is_running() {
1246            return false;
1247        }
1248    }
1249
1250    // Check main processes
1251    let all_done = true;
1252    let mut some_stopped = false;
1253    let mut last_status = 0;
1254
1255    for proc in &job.procs {
1256        if proc.is_running() {
1257            return false; // Still running
1258        }
1259        if proc.is_stopped() {
1260            some_stopped = true;
1261        }
1262    }
1263
1264    // Get last process status
1265    if let Some(last) = job.procs.last() {
1266        if last.is_signaled() {
1267            last_status = 0x80 | last.term_sig();
1268        } else if last.is_stopped() {
1269            last_status = 0x80 | last.stop_sig();
1270        } else {
1271            last_status = last.exit_status();
1272        }
1273    }
1274
1275    if some_stopped {
1276        job.stat |= stat::STOPPED;
1277        job.stat &= !stat::DONE;
1278    } else {
1279        job.stat |= stat::DONE;
1280        job.stat &= !stat::STOPPED;
1281    }
1282
1283    true
1284}
1285
1286/// Update a background job after waitpid (from jobs.c update_bg_job)
1287pub fn update_bg_job(jobtab: &mut [Job], pid: i32, status: i32) -> bool {
1288    if let Some((ji, pi, is_aux)) = findproc(jobtab, pid) {
1289        if is_aux {
1290            jobtab[ji].auxprocs[pi].status = status;
1291            jobtab[ji].auxprocs[pi].end_time = Some(Instant::now());
1292        } else {
1293            jobtab[ji].procs[pi].status = status;
1294            jobtab[ji].procs[pi].end_time = Some(Instant::now());
1295        }
1296        update_job(&mut jobtab[ji]);
1297        return true;
1298    }
1299    false
1300}
1301
1302/// Handle subjob completion (from jobs.c handle_sub)
1303pub fn handle_sub(jobtab: &mut [Job], super_idx: usize, fg: bool) {
1304    let sub_idx = jobtab[super_idx].other;
1305    if sub_idx >= jobtab.len() {
1306        return;
1307    }
1308
1309    // If subjob is done, mark superjob accordingly
1310    if jobtab[sub_idx].is_done() {
1311        if fg {
1312            // Get the last status from the subjob
1313        }
1314        jobtab[super_idx].stat &= !stat::SUPERJOB;
1315        jobtab[super_idx].stat |= stat::WASSUPER;
1316    }
1317}
1318
1319/// Set the previous job (from jobs.c setprevjob)
1320pub fn setprevjob(ptrs: &mut JobPointers, jobtab: &[Job], maxjob: usize) {
1321    // Find a stopped or running job that isn't the current job
1322    let mut best = None;
1323    for i in (1..=maxjob).rev() {
1324        if i >= jobtab.len() {
1325            continue;
1326        }
1327        let job = &jobtab[i];
1328        if (job.stat & stat::INUSE) != 0 && Some(i) != ptrs.cur_job {
1329            if (job.stat & stat::STOPPED) != 0 {
1330                best = Some(i);
1331                break;
1332            }
1333            if best.is_none() {
1334                best = Some(i);
1335            }
1336        }
1337    }
1338    ptrs.prev_job = best;
1339}
1340
1341/// Set current job after state change (from jobs.c setcurjob)
1342pub fn setcurjob(ptrs: &mut JobPointers, jobtab: &[Job], maxjob: usize) {
1343    ptrs.cur_job = None;
1344    for i in (1..=maxjob).rev() {
1345        if i >= jobtab.len() {
1346            continue;
1347        }
1348        if (jobtab[i].stat & (stat::INUSE | stat::STOPPED)) == (stat::INUSE | stat::STOPPED) {
1349            ptrs.cur_job = Some(i);
1350            break;
1351        }
1352    }
1353    if ptrs.cur_job.is_none() {
1354        for i in (1..=maxjob).rev() {
1355            if i >= jobtab.len() {
1356                continue;
1357            }
1358            if (jobtab[i].stat & stat::INUSE) != 0 {
1359                ptrs.cur_job = Some(i);
1360                break;
1361            }
1362        }
1363    }
1364    setprevjob(ptrs, jobtab, maxjob);
1365}
1366
1367/// Check if a job's time should be reported (from jobs.c should_report_time)
1368pub fn should_report_time(job: &Job, reporttime: f64) -> bool {
1369    if reporttime < 0.0 {
1370        return false;
1371    }
1372    if let Some(first) = job.procs.first() {
1373        if let (Some(start), Some(end)) =
1374            (first.start_time, job.procs.last().and_then(|p| p.end_time))
1375        {
1376            let elapsed = end.duration_since(start).as_secs_f64();
1377            return elapsed >= reporttime;
1378        }
1379    }
1380    false
1381}
1382
1383/// Dump timing info for a job (from jobs.c dumptime)
1384pub fn dumptime(job: &Job, format: &str) -> Option<String> {
1385    let first_start = job.procs.first()?.start_time?;
1386    let last_end = job.procs.last()?.end_time?;
1387    let elapsed = last_end.duration_since(first_start).as_secs_f64();
1388
1389    let mut total_user = 0.0;
1390    let mut total_sys = 0.0;
1391    for proc in &job.procs {
1392        total_user += proc.ti.user_time.as_secs_f64();
1393        total_sys += proc.ti.sys_time.as_secs_f64();
1394    }
1395
1396    Some(format_time(
1397        elapsed,
1398        total_user,
1399        total_sys,
1400        format,
1401        &get_job_text(job),
1402    ))
1403}
1404
1405/// Wait for all foreground jobs to finish (from jobs.c waitjobs)
1406pub fn waitjobs(jobtab: &mut Vec<Job>, thisjob: usize) {
1407    if thisjob < jobtab.len() {
1408        while !jobtab[thisjob].is_done() && !jobtab[thisjob].is_stopped() {
1409            #[cfg(unix)]
1410            {
1411                let mut status: i32 = 0;
1412                let pid = unsafe { libc::waitpid(-1, &mut status, libc::WUNTRACED) };
1413                if pid > 0 {
1414                    update_bg_job(jobtab, pid, status);
1415                } else {
1416                    break;
1417                }
1418            }
1419            #[cfg(not(unix))]
1420            {
1421                break;
1422            }
1423        }
1424    }
1425}
1426
1427/// Wait for a single specific job (from jobs.c waitonejob)
1428pub fn waitonejob(job: &mut Job) {
1429    for proc in &mut job.procs {
1430        if proc.is_running() {
1431            if let Some(_status) = waitforpid(proc.pid) {
1432                // status already updated by waitforpid
1433            }
1434        }
1435    }
1436}
1437
1438/// Initialize a new job entry (from jobs.c initjob)
1439pub fn initjob(jobtab: &mut Vec<Job>) -> usize {
1440    // Find an empty slot or add a new one
1441    for (i, job) in jobtab.iter().enumerate() {
1442        if (job.stat & stat::INUSE) == 0 {
1443            jobtab[i] = Job::new();
1444            jobtab[i].stat = stat::INUSE;
1445            return i;
1446        }
1447    }
1448    // Expand table
1449    let idx = jobtab.len();
1450    let mut job = Job::new();
1451    job.stat = stat::INUSE;
1452    jobtab.push(job);
1453    idx
1454}
1455
1456/// Set the pwd for a job (from jobs.c setjobpwd)
1457pub fn setjobpwd(job: &mut Job) {
1458    // Store current directory in job for display purposes
1459    if let Ok(cwd) = std::env::current_dir() {
1460        // Job text sometimes includes the directory
1461        let _ = cwd;
1462    }
1463}
1464
1465/// Spawn a job (mark as started, from jobs.c spawnjob)
1466pub fn spawnjob(job: &mut Job, fg: bool) {
1467    job.stat |= stat::INUSE;
1468    if !fg {
1469        // Background job
1470        job.stat &= !stat::CURSH;
1471    }
1472}
1473
1474/// Select which job table to use (from jobs.c selectjobtab)
1475/// Returns (table_ref, max_job_index)
1476pub fn selectjobtab(jobtab: &[Job]) -> usize {
1477    // Find the maximum used job index
1478    let mut max = 0;
1479    for (i, job) in jobtab.iter().enumerate() {
1480        if (job.stat & stat::INUSE) != 0 {
1481            max = i;
1482        }
1483    }
1484    max
1485}
1486
1487/// Expand job table if needed (from jobs.c expandjobtab)
1488pub fn expandjobtab(jobtab: &mut Vec<Job>, needed: usize) {
1489    while jobtab.len() <= needed {
1490        jobtab.push(Job::new());
1491    }
1492}
1493
1494/// Shrink job table if possible (from jobs.c maybeshrinkjobtab)
1495pub fn maybeshrinkjobtab(jobtab: &mut Vec<Job>) {
1496    while jobtab
1497        .last()
1498        .map(|j| (j.stat & stat::INUSE) == 0)
1499        .unwrap_or(false)
1500    {
1501        jobtab.pop();
1502    }
1503}
1504
1505/// Add file to job's temp file list (from jobs.c addfilelist)
1506pub fn addfilelist(job: &mut Job, filename: &str) {
1507    job.filelist.push(filename.to_string());
1508}
1509
1510/// Clean temp files for process substitution (from jobs.c pipecleanfilelist)
1511pub fn pipecleanfilelist(job: &mut Job, proc_subst_only: bool) {
1512    if proc_subst_only {
1513        // Only remove process substitution files (those starting with /dev/fd or /proc)
1514        job.filelist
1515            .retain(|f| !f.starts_with("/dev/fd/") && !f.starts_with("/proc/"));
1516    } else {
1517        for file in &job.filelist {
1518            let _ = std::fs::remove_file(file);
1519        }
1520        job.filelist.clear();
1521    }
1522}
1523
1524/// Delete temp files from a job (from jobs.c deletefilelist)
1525pub fn deletefilelist(job: &mut Job, disowning: bool) {
1526    if !disowning {
1527        for file in &job.filelist {
1528            let _ = std::fs::remove_file(file);
1529        }
1530    }
1531    job.filelist.clear();
1532}
1533
1534/// Print job with full detail (from jobs.c printjob)
1535pub fn printjob(
1536    job: &Job,
1537    job_num: usize,
1538    long_format: bool,
1539    cur_job: Option<usize>,
1540    prev_job: Option<usize>,
1541) -> String {
1542    let marker = if Some(job_num) == cur_job {
1543        '+'
1544    } else if Some(job_num) == prev_job {
1545        '-'
1546    } else {
1547        ' '
1548    };
1549
1550    let status_str = if job.is_done() {
1551        if let Some(last) = job.procs.last() {
1552            format_process_status(last.status)
1553        } else {
1554            "done".to_string()
1555        }
1556    } else if job.is_stopped() {
1557        "suspended".to_string()
1558    } else {
1559        "running".to_string()
1560    };
1561
1562    if long_format {
1563        let mut lines = Vec::new();
1564        for (i, proc) in job.procs.iter().enumerate() {
1565            let pstatus = format_process_status(proc.status);
1566            if i == 0 {
1567                lines.push(format!(
1568                    "[{}]  {} {:>5} {:16}  {}",
1569                    job_num, marker, proc.pid, pstatus, proc.text
1570                ));
1571            } else {
1572                lines.push(format!(
1573                    "            {:>5} {:16}  | {}",
1574                    proc.pid, pstatus, proc.text
1575                ));
1576            }
1577        }
1578        lines.join("\n")
1579    } else {
1580        format!(
1581            "[{}]  {} {:16}  {}",
1582            job_num,
1583            marker,
1584            status_str,
1585            get_job_text(job)
1586        )
1587    }
1588}
1589
1590/// Get the signal name for signal-based job output (from jobs.c getsigname)
1591pub fn getsigname(sig: i32) -> String {
1592    match sig {
1593        0 => "EXIT".to_string(),
1594        libc::SIGHUP => "HUP".to_string(),
1595        libc::SIGINT => "INT".to_string(),
1596        libc::SIGQUIT => "QUIT".to_string(),
1597        libc::SIGILL => "ILL".to_string(),
1598        libc::SIGTRAP => "TRAP".to_string(),
1599        libc::SIGABRT => "ABRT".to_string(),
1600        libc::SIGBUS => "BUS".to_string(),
1601        libc::SIGFPE => "FPE".to_string(),
1602        libc::SIGKILL => "KILL".to_string(),
1603        libc::SIGUSR1 => "USR1".to_string(),
1604        libc::SIGSEGV => "SEGV".to_string(),
1605        libc::SIGUSR2 => "USR2".to_string(),
1606        libc::SIGPIPE => "PIPE".to_string(),
1607        libc::SIGALRM => "ALRM".to_string(),
1608        libc::SIGTERM => "TERM".to_string(),
1609        libc::SIGCHLD => "CHLD".to_string(),
1610        libc::SIGCONT => "CONT".to_string(),
1611        libc::SIGSTOP => "STOP".to_string(),
1612        libc::SIGTSTP => "TSTP".to_string(),
1613        libc::SIGTTIN => "TTIN".to_string(),
1614        libc::SIGTTOU => "TTOU".to_string(),
1615        libc::SIGURG => "URG".to_string(),
1616        libc::SIGXCPU => "XCPU".to_string(),
1617        libc::SIGXFSZ => "XFSZ".to_string(),
1618        libc::SIGVTALRM => "VTALRM".to_string(),
1619        libc::SIGPROF => "PROF".to_string(),
1620        libc::SIGWINCH => "WINCH".to_string(),
1621        libc::SIGIO => "IO".to_string(),
1622        libc::SIGSYS => "SYS".to_string(),
1623        _ => format!("SIG{}", sig),
1624    }
1625}
1626
1627/// Time difference for timeval (from jobs.c dtime_tv)
1628pub fn dtime_tv(dt: &mut Duration, t1: &Duration, t2: &Duration) -> Duration {
1629    if *t2 > *t1 {
1630        *dt = *t2 - *t1;
1631    } else {
1632        *dt = Duration::ZERO;
1633    }
1634    *dt
1635}
1636
1637/// Time difference for timespec (from jobs.c dtime_ts)
1638pub fn dtime_ts(t1: &Instant, t2: &Instant) -> Duration {
1639    if *t2 > *t1 {
1640        t2.duration_since(*t1)
1641    } else {
1642        Duration::ZERO
1643    }
1644}
1645
1646/// Make all job processes running (from jobs.c makerunning)
1647pub fn makerunning(job: &mut Job) {
1648    job.make_running();
1649}
1650
1651/// Check if job has any processes (from jobs.c hasprocs)
1652pub fn hasprocs(job: &Job) -> bool {
1653    job.has_procs()
1654}
1655
1656/// Get resource usage info (from jobs.c get_usage)
1657pub fn get_usage() -> ChildTimes {
1658    childtime()
1659}
1660
1661/// Check current shell signals (from jobs.c check_cursh_sig)
1662#[cfg(unix)]
1663pub fn check_cursh_sig(jobtab: &[Job], sig: i32) {
1664    for job in jobtab {
1665        if (job.stat & stat::CURSH) != 0 && !job.is_done() {
1666            for proc in &job.procs {
1667                if proc.is_running() {
1668                    unsafe {
1669                        libc::kill(proc.pid, sig);
1670                    }
1671                }
1672            }
1673        }
1674    }
1675}
1676
1677/// Clean all file lists from jobs (from jobs.c cleanfilelists)
1678pub fn cleanfilelists(jobtab: &mut [Job]) {
1679    for job in jobtab.iter_mut() {
1680        deletefilelist(job, false);
1681    }
1682}
1683
1684/// Clear old job table entries (from jobs.c clearoldjobtab)
1685pub fn clearoldjobtab(jobtab: &mut Vec<Job>) {
1686    jobtab.retain(|j| (j.stat & stat::INUSE) != 0);
1687}
1688
1689/// Add background status (from jobs.c addbgstatus)
1690pub fn addbgstatus(bg: &mut BgStatus, pid: i32, status_val: i32) {
1691    bg.add(pid, status_val);
1692}
1693
1694/// Get background status (from jobs.c getbgstatus)
1695pub fn getbgstatus(bg: &mut BgStatus, pid: i32) -> Option<i32> {
1696    bg.remove(pid)
1697}
1698
1699/// Get trap node for signal (from jobs.c gettrapnode) - defers to signals module
1700pub fn gettrapnode(sig: i32) -> Option<String> {
1701    // This is actually in signals.rs - provide a bridge
1702    let _ = sig;
1703    None
1704}
1705
1706/// Remove trap node (from jobs.c removetrapnode) - defers to signals module
1707pub fn removetrapnode(sig: i32) {
1708    let _ = sig;
1709}
1710
1711/// Release acquired process group (from jobs.c release_pgrp)
1712#[cfg(unix)]
1713pub fn release_pgrp() {
1714    // Restore original process group if needed
1715}
1716
1717/// Signal number from name (from jobs.c getsigidx)
1718pub fn getsigidx(name: &str) -> Option<i32> {
1719    let name = name.strip_prefix("SIG").unwrap_or(name);
1720    match name.to_uppercase().as_str() {
1721        "EXIT" => Some(0),
1722        "HUP" => Some(libc::SIGHUP),
1723        "INT" => Some(libc::SIGINT),
1724        "QUIT" => Some(libc::SIGQUIT),
1725        "ILL" => Some(libc::SIGILL),
1726        "TRAP" => Some(libc::SIGTRAP),
1727        "ABRT" | "IOT" => Some(libc::SIGABRT),
1728        "BUS" => Some(libc::SIGBUS),
1729        "FPE" => Some(libc::SIGFPE),
1730        "KILL" => Some(libc::SIGKILL),
1731        "USR1" => Some(libc::SIGUSR1),
1732        "SEGV" => Some(libc::SIGSEGV),
1733        "USR2" => Some(libc::SIGUSR2),
1734        "PIPE" => Some(libc::SIGPIPE),
1735        "ALRM" => Some(libc::SIGALRM),
1736        "TERM" => Some(libc::SIGTERM),
1737        "CHLD" | "CLD" => Some(libc::SIGCHLD),
1738        "CONT" => Some(libc::SIGCONT),
1739        "STOP" => Some(libc::SIGSTOP),
1740        "TSTP" => Some(libc::SIGTSTP),
1741        "TTIN" => Some(libc::SIGTTIN),
1742        "TTOU" => Some(libc::SIGTTOU),
1743        "URG" => Some(libc::SIGURG),
1744        "XCPU" => Some(libc::SIGXCPU),
1745        "XFSZ" => Some(libc::SIGXFSZ),
1746        "VTALRM" => Some(libc::SIGVTALRM),
1747        "PROF" => Some(libc::SIGPROF),
1748        "WINCH" => Some(libc::SIGWINCH),
1749        "IO" | "POLL" => Some(libc::SIGIO),
1750        "SYS" => Some(libc::SIGSYS),
1751        _ => name.parse().ok(),
1752    }
1753}