1use std::process::Child;
8use std::time::{Duration, Instant};
9
10pub mod stat {
12 pub const STOPPED: u32 = 1 << 0; pub const DONE: u32 = 1 << 1; pub const SUBJOB: u32 = 1 << 2; pub const CURSH: u32 = 1 << 3; pub const SUPERJOB: u32 = 1 << 4; pub const WASSUPER: u32 = 1 << 5; pub const INUSE: u32 = 1 << 6; pub const BUILTIN: u32 = 1 << 7; pub const DISOWN: u32 = 1 << 8; pub const NOTIFY: u32 = 1 << 9; pub const ATTACH: u32 = 1 << 10; }
24
25pub const SP_RUNNING: i32 = -1;
27
28pub const MAX_PIPESTATS: usize = 256;
30
31#[derive(Clone, Debug, Default)]
33pub struct TimeInfo {
34 pub user_time: Duration,
35 pub sys_time: Duration,
36}
37
38#[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 self.status & 0xff == 0x7f
68 }
69
70 pub fn is_signaled(&self) -> bool {
71 (self.status & 0x7f) > 0 && (self.status & 0x7f) < 0x7f
73 }
74
75 pub fn exit_status(&self) -> i32 {
76 (self.status >> 8) & 0xff
78 }
79
80 pub fn term_sig(&self) -> i32 {
81 self.status & 0x7f
83 }
84
85 pub fn stop_sig(&self) -> i32 {
86 (self.status >> 8) & 0xff
88 }
89}
90
91#[derive(Clone, Debug)]
93pub struct Job {
94 pub stat: u32,
95 pub gleader: i32, pub procs: Vec<Process>, pub auxprocs: Vec<Process>, pub other: usize, pub filelist: Vec<String>, pub text: String, }
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#[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
167pub 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 pub fn peek_next_id(&self) -> usize {
195 self.next_id
196 }
197
198 pub fn add_job(&mut self, child: Child, command: String, state: JobState) -> usize {
200 let id = self.next_id;
201 self.next_id += 1;
202
203 let pid = child.id() as i32;
204 let job = JobInfo {
205 id,
206 pid,
207 child: Some(child),
208 command,
209 state,
210 is_current: true,
211 };
212
213 if let Some(cur_id) = self.current_id {
215 if let Some(j) = self.get_mut_internal(cur_id) {
216 j.is_current = false;
217 }
218 }
219
220 let slot = self.get_free_slot();
222 if slot >= self.jobs.len() {
223 self.jobs.resize_with(slot + 1, || None);
224 }
225 self.jobs[slot] = Some(job);
226 self.current_id = Some(id);
227
228 id
229 }
230
231 fn get_free_slot(&self) -> usize {
232 for (i, slot) in self.jobs.iter().enumerate() {
233 if slot.is_none() {
234 return i;
235 }
236 }
237 self.jobs.len()
238 }
239
240 fn get_mut_internal(&mut self, id: usize) -> Option<&mut JobInfo> {
241 for job in self.jobs.iter_mut().flatten() {
242 if job.id == id {
243 return Some(job);
244 }
245 }
246 None
247 }
248
249 pub fn get(&self, id: usize) -> Option<&JobInfo> {
251 self.jobs
252 .iter()
253 .flatten()
254 .find(|&job| job.id == id)
255 .map(|v| v as _)
256 }
257
258 pub fn get_mut(&mut self, id: usize) -> Option<&mut JobInfo> {
260 self.get_mut_internal(id)
261 }
262
263 pub fn remove(&mut self, id: usize) -> Option<JobInfo> {
265 for slot in self.jobs.iter_mut() {
266 if slot.as_ref().map(|j| j.id == id).unwrap_or(false) {
267 let job = slot.take();
268 if self.current_id == Some(id) {
269 self.current_id = None;
270 }
271 return job;
272 }
273 }
274 None
275 }
276
277 pub fn list(&self) -> Vec<&JobInfo> {
279 self.jobs.iter().filter_map(|j| j.as_ref()).collect()
280 }
281
282 pub fn iter(&self) -> impl Iterator<Item = (usize, &JobInfo)> {
284 self.jobs
285 .iter()
286 .filter_map(|j| j.as_ref().map(|job| (job.id, job)))
287 }
288
289 pub fn count(&self) -> usize {
291 self.jobs.iter().filter(|j| j.is_some()).count()
292 }
293
294 pub fn is_empty(&self) -> bool {
296 self.count() == 0
297 }
298
299 pub fn current(&self) -> Option<&JobInfo> {
301 self.current_id.and_then(|id| self.get(id))
302 }
303
304 pub fn reap_finished(&mut self) -> Vec<JobInfo> {
306 let mut finished = Vec::new();
307
308 for slot in self.jobs.iter_mut() {
309 if let Some(job) = slot {
310 if let Some(ref mut child) = job.child {
311 match child.try_wait() {
313 Ok(Some(_status)) => {
314 job.state = JobState::Done;
316 }
317 Ok(None) => {
318 }
320 Err(_) => {
321 job.state = JobState::Done;
323 }
324 }
325 }
326 }
327 }
328
329 for slot in self.jobs.iter_mut() {
331 if slot
332 .as_ref()
333 .map(|j| j.state == JobState::Done)
334 .unwrap_or(false)
335 {
336 if let Some(job) = slot.take() {
337 finished.push(job);
338 }
339 }
340 }
341
342 finished
343 }
344}
345
346pub fn format_job(
348 job: &Job,
349 job_num: usize,
350 cur_job: Option<usize>,
351 prev_job: Option<usize>,
352) -> String {
353 let marker = if Some(job_num) == cur_job {
354 '+'
355 } else if Some(job_num) == prev_job {
356 '-'
357 } else {
358 ' '
359 };
360
361 let status = if job.is_done() {
362 "done"
363 } else if job.is_stopped() {
364 "suspended"
365 } else {
366 "running"
367 };
368
369 format!("[{}]{} {:10} {}", job_num, marker, status, job.text)
370}
371
372#[cfg(test)]
373mod tests {
374 use super::*;
375
376 #[test]
377 fn test_process_new() {
378 let proc = Process::new(1234);
379 assert_eq!(proc.pid, 1234);
380 assert!(proc.is_running());
381 }
382
383 #[test]
384 fn test_job_new() {
385 let job = Job::new();
386 assert_eq!(job.stat, 0);
387 assert!(!job.is_done());
388 assert!(!job.is_stopped());
389 }
390
391 #[test]
392 fn test_job_table_new() {
393 let table = JobTable::new();
394 assert!(table.is_empty());
395 }
396
397 #[test]
398 fn test_job_table_remove() {
399 }
401
402 #[test]
403 fn test_job_make_running() {
404 let mut job = Job::new();
405 job.stat |= stat::STOPPED;
406 job.procs.push(Process {
407 status: 0x007f,
408 ..Process::new(1234)
409 }); job.make_running();
412 assert!(!job.is_stopped());
413 assert!(job.procs[0].is_running());
414 }
415
416 #[test]
417 fn test_format_job() {
418 let mut job = Job::new();
419 job.text = "vim file.txt".to_string();
420 job.stat |= stat::STOPPED;
421
422 let formatted = format_job(&job, 1, Some(1), None);
423 assert!(formatted.contains("[1]+"));
424 assert!(formatted.contains("suspended"));
425 assert!(formatted.contains("vim file.txt"));
426 }
427
428 #[test]
429 fn test_job_state_enum() {
430 let state = JobState::Running;
431 assert_eq!(state, JobState::Running);
432 assert_ne!(state, JobState::Stopped);
433 assert_ne!(state, JobState::Done);
434 }
435}
436
437#[derive(Clone, Copy, Debug, PartialEq, Eq)]
439pub enum JobState {
440 Running,
441 Stopped,
442 Done,
443}
444
445#[derive(Debug)]
447pub struct JobEntry {
448 pub pid: i32,
449 pub child: Option<Child>,
450 pub command: String,
451 pub state: JobState,
452 pub is_current: bool,
453}
454
455#[cfg(unix)]
457pub fn send_signal(pid: i32, sig: nix::sys::signal::Signal) -> Result<(), String> {
458 use nix::sys::signal::kill;
459 use nix::unistd::Pid;
460
461 kill(Pid::from_raw(pid), sig).map_err(|e| e.to_string())
462}
463
464#[cfg(not(unix))]
465pub fn send_signal(_pid: i32, _sig: i32) -> Result<(), String> {
466 Err("Signal sending not supported on this platform".to_string())
467}
468
469#[cfg(unix)]
471pub fn continue_job(pid: i32) -> Result<(), String> {
472 use nix::sys::signal::{kill, Signal};
473 use nix::unistd::Pid;
474
475 kill(Pid::from_raw(pid), Signal::SIGCONT).map_err(|e| e.to_string())
476}
477
478#[cfg(not(unix))]
479pub fn continue_job(_pid: i32) -> Result<(), String> {
480 Err("Job control not supported on this platform".to_string())
481}
482
483#[cfg(unix)]
485pub fn wait_for_job(pid: i32) -> Result<i32, String> {
486 use nix::sys::wait::{waitpid, WaitStatus};
487 use nix::unistd::Pid;
488
489 loop {
490 match waitpid(Pid::from_raw(pid), None) {
491 Ok(WaitStatus::Exited(_, code)) => return Ok(code),
492 Ok(WaitStatus::Signaled(_, sig, _)) => return Ok(128 + sig as i32),
493 Ok(WaitStatus::Stopped(_, _)) => return Ok(128),
494 Ok(_) => continue,
495 Err(nix::errno::Errno::ECHILD) => return Ok(0),
496 Err(e) => return Err(e.to_string()),
497 }
498 }
499}
500
501#[cfg(not(unix))]
502pub fn wait_for_job(_pid: i32) -> Result<i32, String> {
503 Err("Job waiting not supported on this platform".to_string())
504}
505
506pub fn wait_for_child(child: &mut Child) -> Result<i32, String> {
508 match child.wait() {
509 Ok(status) => Ok(status.code().unwrap_or(0)),
510 Err(e) => Err(e.to_string()),
511 }
512}
513
514pub fn get_clktck() -> i64 {
516 #[cfg(unix)]
517 {
518 use std::sync::OnceLock;
519 static CLKTCK: OnceLock<i64> = OnceLock::new();
520 *CLKTCK.get_or_init(|| unsafe { libc::sysconf(libc::_SC_CLK_TCK) as i64 })
521 }
522 #[cfg(not(unix))]
523 {
524 100 }
526}
527
528pub fn format_hhmmss(secs: f64) -> String {
530 let mins = (secs / 60.0) as i32;
531 let hours = mins / 60;
532 let secs = secs - (mins * 60) as f64;
533 let mins = mins - (hours * 60);
534
535 if hours > 0 {
536 format!("{}:{:02}:{:05.2}", hours, mins, secs)
537 } else if mins > 0 {
538 format!("{}:{:05.2}", mins, secs)
539 } else {
540 format!("{:.3}", secs)
541 }
542}
543
544pub fn format_time(
546 elapsed_secs: f64,
547 user_secs: f64,
548 system_secs: f64,
549 format: &str,
550 job_name: &str,
551) -> String {
552 let mut result = String::new();
553 let total_time = user_secs + system_secs;
554 let percent = if elapsed_secs > 0.0 {
555 (100.0 * total_time / elapsed_secs) as i32
556 } else {
557 0
558 };
559
560 let mut chars = format.chars().peekable();
561 while let Some(c) = chars.next() {
562 if c == '%' {
563 match chars.next() {
564 Some('E') => result.push_str(&format!("{:.2}s", elapsed_secs)),
565 Some('U') => result.push_str(&format!("{:.2}s", user_secs)),
566 Some('S') => result.push_str(&format!("{:.2}s", system_secs)),
567 Some('P') => result.push_str(&format!("{}%", percent)),
568 Some('J') => result.push_str(job_name),
569 Some('m') => match chars.next() {
570 Some('E') => result.push_str(&format!("{:.0}ms", elapsed_secs * 1000.0)),
571 Some('U') => result.push_str(&format!("{:.0}ms", user_secs * 1000.0)),
572 Some('S') => result.push_str(&format!("{:.0}ms", system_secs * 1000.0)),
573 _ => result.push_str("%m"),
574 },
575 Some('u') => match chars.next() {
576 Some('E') => result.push_str(&format!("{:.0}us", elapsed_secs * 1_000_000.0)),
577 Some('U') => result.push_str(&format!("{:.0}us", user_secs * 1_000_000.0)),
578 Some('S') => result.push_str(&format!("{:.0}us", system_secs * 1_000_000.0)),
579 _ => result.push_str("%u"),
580 },
581 Some('n') => match chars.next() {
582 Some('E') => {
583 result.push_str(&format!("{:.0}ns", elapsed_secs * 1_000_000_000.0))
584 }
585 Some('U') => result.push_str(&format!("{:.0}ns", user_secs * 1_000_000_000.0)),
586 Some('S') => {
587 result.push_str(&format!("{:.0}ns", system_secs * 1_000_000_000.0))
588 }
589 _ => result.push_str("%n"),
590 },
591 Some('*') => match chars.next() {
592 Some('E') => result.push_str(&format_hhmmss(elapsed_secs)),
593 Some('U') => result.push_str(&format_hhmmss(user_secs)),
594 Some('S') => result.push_str(&format_hhmmss(system_secs)),
595 _ => result.push_str("%*"),
596 },
597 Some('%') => result.push('%'),
598 Some(other) => {
599 result.push('%');
600 result.push(other);
601 }
602 None => result.push('%'),
603 }
604 } else {
605 result.push(c);
606 }
607 }
608 result
609}
610
611pub const DEFAULT_TIMEFMT: &str = "%J %U user %S system %P cpu %*E total";
613
614pub struct CommandTimer {
616 start: std::time::Instant,
617 job_name: String,
618}
619
620impl CommandTimer {
621 pub fn new(job_name: &str) -> Self {
622 CommandTimer {
623 start: std::time::Instant::now(),
624 job_name: job_name.to_string(),
625 }
626 }
627
628 pub fn elapsed(&self) -> Duration {
629 self.start.elapsed()
630 }
631
632 pub fn format(
633 &self,
634 user_time: Duration,
635 sys_time: Duration,
636 format_str: Option<&str>,
637 ) -> String {
638 let elapsed = self.start.elapsed().as_secs_f64();
639 let user = user_time.as_secs_f64();
640 let sys = sys_time.as_secs_f64();
641
642 format_time(
643 elapsed,
644 user,
645 sys,
646 format_str.unwrap_or(DEFAULT_TIMEFMT),
647 &self.job_name,
648 )
649 }
650}
651
652pub struct PipeStats {
654 stats: Vec<i32>,
655}
656
657impl Default for PipeStats {
658 fn default() -> Self {
659 Self::new()
660 }
661}
662
663impl PipeStats {
664 pub fn new() -> Self {
665 PipeStats { stats: Vec::new() }
666 }
667
668 pub fn clear(&mut self) {
669 self.stats.clear();
670 }
671
672 pub fn add(&mut self, status: i32) {
673 if self.stats.len() < MAX_PIPESTATS {
674 self.stats.push(status);
675 }
676 }
677
678 pub fn get(&self) -> &[i32] {
679 &self.stats
680 }
681
682 pub fn len(&self) -> usize {
683 self.stats.len()
684 }
685
686 pub fn is_empty(&self) -> bool {
687 self.stats.is_empty()
688 }
689
690 pub fn pipefail_status(&self) -> i32 {
691 *self.stats.iter().rev().find(|&&s| s != 0).unwrap_or(&0)
692 }
693}
694
695pub fn sigmsg(sig: i32) -> &'static str {
697 match sig {
698 libc::SIGHUP => "hangup",
699 libc::SIGINT => "interrupt",
700 libc::SIGQUIT => "quit",
701 libc::SIGILL => "illegal instruction",
702 libc::SIGTRAP => "trace trap",
703 libc::SIGABRT => "abort",
704 libc::SIGBUS => "bus error",
705 libc::SIGFPE => "floating point exception",
706 libc::SIGKILL => "killed",
707 libc::SIGUSR1 => "user-defined signal 1",
708 libc::SIGSEGV => "segmentation fault",
709 libc::SIGUSR2 => "user-defined signal 2",
710 libc::SIGPIPE => "broken pipe",
711 libc::SIGALRM => "alarm",
712 libc::SIGTERM => "terminated",
713 libc::SIGCHLD => "child exited",
714 libc::SIGCONT => "continued",
715 libc::SIGSTOP => "stopped (signal)",
716 libc::SIGTSTP => "stopped",
717 libc::SIGTTIN => "stopped (tty input)",
718 libc::SIGTTOU => "stopped (tty output)",
719 libc::SIGURG => "urgent I/O condition",
720 libc::SIGXCPU => "CPU time exceeded",
721 libc::SIGXFSZ => "file size exceeded",
722 libc::SIGVTALRM => "virtual timer expired",
723 libc::SIGPROF => "profiling timer expired",
724 libc::SIGWINCH => "window changed",
725 libc::SIGIO => "I/O ready",
726 libc::SIGSYS => "bad system call",
727 _ => "unknown signal",
728 }
729}
730
731pub fn format_process_status(status: i32) -> String {
733 if status == SP_RUNNING {
734 "running".to_string()
735 } else if (status & 0x7f) == 0 {
736 let code = (status >> 8) & 0xff;
738 if code == 0 {
739 "done".to_string()
740 } else {
741 format!("exit {}", code)
742 }
743 } else if (status & 0xff) == 0x7f {
744 let sig = (status >> 8) & 0xff;
746 format!("suspended ({})", sigmsg(sig))
747 } else {
748 let sig = status & 0x7f;
750 let core = (status >> 7) & 1;
751 if core != 0 {
752 format!("{} (core dumped)", sigmsg(sig))
753 } else {
754 sigmsg(sig).to_string()
755 }
756 }
757}
758
759pub fn format_job_long(
761 job_num: usize,
762 current: bool,
763 pid: i32,
764 status: &str,
765 text: &str,
766) -> String {
767 let marker = if current { '+' } else { '-' };
768 format!("[{}] {} {:>5} {} {}", job_num, marker, pid, status, text)
769}
770
771pub fn format_job_short(job_num: usize, current: bool, status: &str, text: &str) -> String {
773 let marker = if current { '+' } else { '-' };
774 format!("[{}] {} {} {}", job_num, marker, status, text)
775}
776
777pub struct BgStatus {
779 statuses: std::collections::HashMap<i32, i32>,
780}
781
782impl Default for BgStatus {
783 fn default() -> Self {
784 Self::new()
785 }
786}
787
788impl BgStatus {
789 pub fn new() -> Self {
790 BgStatus {
791 statuses: std::collections::HashMap::new(),
792 }
793 }
794
795 pub fn add(&mut self, pid: i32, status: i32) {
796 self.statuses.insert(pid, status);
797 }
798
799 pub fn get(&self, pid: i32) -> Option<i32> {
800 self.statuses.get(&pid).copied()
801 }
802
803 pub fn remove(&mut self, pid: i32) -> Option<i32> {
804 self.statuses.remove(&pid)
805 }
806
807 pub fn clear(&mut self) {
808 self.statuses.clear();
809 }
810}
811
812pub fn waitforpid(pid: i32) -> Option<i32> {
814 #[cfg(unix)]
815 {
816 use std::os::unix::process::ExitStatusExt;
817 loop {
818 let mut status: i32 = 0;
819 let result = unsafe { libc::waitpid(pid, &mut status, 0) };
820 if result == pid {
821 if libc::WIFEXITED(status) {
822 return Some(libc::WEXITSTATUS(status));
823 } else if libc::WIFSIGNALED(status) {
824 return Some(128 + libc::WTERMSIG(status));
825 } else if libc::WIFSTOPPED(status) {
826 return None;
827 }
828 } else if result == -1 {
829 return None;
830 }
831 }
832 }
833 #[cfg(not(unix))]
834 {
835 let _ = pid;
836 None
837 }
838}
839
840pub fn waitjob(job: &mut Job) -> Option<i32> {
842 if job.procs.is_empty() {
843 return Some(0);
844 }
845
846 let mut last_status = 0;
847 for proc in &mut job.procs {
848 if proc.is_running() {
849 if let Some(status) = waitforpid(proc.pid) {
850 proc.status = make_status(status);
851 last_status = status;
852 }
853 } else {
854 last_status = proc.exit_status();
855 }
856 }
857
858 job.stat |= stat::DONE;
859 Some(last_status)
860}
861
862pub fn make_status(code: i32) -> i32 {
864 code << 8
865}
866
867pub fn make_signal_status(sig: i32) -> i32 {
869 sig
870}
871
872pub fn havefiles(job: &Job) -> bool {
874 !job.filelist.is_empty()
875}
876
877pub fn deletejob(job: &mut Job, disowning: bool) {
879 if !disowning {
880 job.filelist.clear();
881 }
882 job.procs.clear();
883 job.auxprocs.clear();
884 job.stat = 0;
885}
886
887pub fn freejob(job: &mut Job, notify: bool) {
889 let _ = notify;
890 job.procs.clear();
891 job.auxprocs.clear();
892 job.filelist.clear();
893 job.stat = 0;
894 job.gleader = 0;
895 job.text.clear();
896}
897
898pub fn addproc(job: &mut Job, pid: i32, text: &str, aux: bool) {
900 let proc = Process::new(pid);
901 let proc = Process {
902 pid,
903 status: SP_RUNNING,
904 text: text.to_string(),
905 ..proc
906 };
907
908 if aux {
909 job.auxprocs.push(proc);
910 } else {
911 if job.gleader == 0 {
912 job.gleader = pid;
913 }
914 job.procs.push(proc);
915 }
916
917 job.stat &= !stat::DONE;
918}
919
920pub fn killjob(job: &Job, sig: i32) -> bool {
922 #[cfg(unix)]
923 {
924 if job.gleader > 0 {
925 let result = unsafe { libc::killpg(job.gleader, sig) };
926 return result == 0;
927 }
928
929 let mut success = true;
930 for proc in &job.procs {
931 if proc.is_running() {
932 let result = unsafe { libc::kill(proc.pid, sig) };
933 if result != 0 {
934 success = false;
935 }
936 }
937 }
938 success
939 }
940 #[cfg(not(unix))]
941 {
942 let _ = (job, sig);
943 false
944 }
945}
946
947pub fn fg_job(job: &mut Job) -> Option<i32> {
949 #[cfg(unix)]
950 {
951 if (job.stat & stat::STOPPED) != 0 {
952 if job.gleader > 0 {
953 unsafe { libc::killpg(job.gleader, libc::SIGCONT) };
954 } else {
955 for proc in &job.procs {
956 unsafe { libc::kill(proc.pid, libc::SIGCONT) };
957 }
958 }
959 job.stat &= !stat::STOPPED;
960 }
961
962 waitjob(job)
963 }
964 #[cfg(not(unix))]
965 {
966 let _ = job;
967 None
968 }
969}
970
971pub fn bg_job(job: &mut Job) -> bool {
973 #[cfg(unix)]
974 {
975 if (job.stat & stat::STOPPED) != 0 {
976 if job.gleader > 0 {
977 unsafe { libc::killpg(job.gleader, libc::SIGCONT) };
978 } else {
979 for proc in &job.procs {
980 unsafe { libc::kill(proc.pid, libc::SIGCONT) };
981 }
982 }
983 job.stat &= !stat::STOPPED;
984 return true;
985 }
986 false
987 }
988 #[cfg(not(unix))]
989 {
990 let _ = job;
991 false
992 }
993}
994
995pub fn disown_job(job: &mut Job) {
997 job.stat |= stat::DISOWN;
998}
999
1000pub fn job_is_done(job: &Job) -> bool {
1002 (job.stat & stat::DONE) != 0 || job.procs.iter().all(|p| !p.is_running())
1003}
1004
1005pub fn job_is_stopped(job: &Job) -> bool {
1007 (job.stat & stat::STOPPED) != 0 || job.procs.iter().any(|p| p.is_stopped())
1008}
1009
1010pub fn get_job_text(job: &Job) -> String {
1012 if !job.text.is_empty() {
1013 return job.text.clone();
1014 }
1015 job.procs
1016 .iter()
1017 .map(|p| p.text.as_str())
1018 .collect::<Vec<_>>()
1019 .join(" | ")
1020}
1021
1022pub fn super_job(jobtab: &[Job], job_idx: usize) -> Option<usize> {
1024 for (i, job) in jobtab.iter().enumerate() {
1025 if (job.stat & stat::SUPERJOB) != 0 && job.other == job_idx {
1026 return Some(i);
1027 }
1028 }
1029 None
1030}
1031
1032pub struct JobPointers {
1034 pub cur_job: Option<usize>,
1035 pub prev_job: Option<usize>,
1036}
1037
1038impl JobPointers {
1039 pub fn new() -> Self {
1040 JobPointers {
1041 cur_job: None,
1042 prev_job: None,
1043 }
1044 }
1045
1046 pub fn set_current(&mut self, job: usize) {
1047 if Some(job) != self.cur_job {
1048 self.prev_job = self.cur_job;
1049 self.cur_job = Some(job);
1050 }
1051 }
1052
1053 pub fn clear(&mut self, job: usize) {
1054 if self.cur_job == Some(job) {
1055 self.cur_job = self.prev_job;
1056 self.prev_job = None;
1057 } else if self.prev_job == Some(job) {
1058 self.prev_job = None;
1059 }
1060 }
1061}
1062
1063impl Default for JobPointers {
1064 fn default() -> Self {
1065 Self::new()
1066 }
1067}
1068
1069pub fn getjob(spec: &str, table: &JobTable, ptrs: &JobPointers) -> Option<usize> {
1078 if spec.is_empty() {
1079 return ptrs.cur_job;
1080 }
1081
1082 let spec = if spec.starts_with('%') {
1083 &spec[1..]
1084 } else {
1085 spec
1086 };
1087
1088 match spec {
1089 "+" | "%" | "" => ptrs.cur_job,
1090 "-" => ptrs.prev_job,
1091 _ => {
1092 if let Ok(n) = spec.parse::<usize>() {
1094 if table.get(n).is_some() {
1095 return Some(n);
1096 }
1097 return None;
1098 }
1099
1100 if let Some(substr) = spec.strip_prefix('?') {
1102 for (id, job) in table.iter() {
1103 if job.command.contains(substr) {
1104 return Some(id);
1105 }
1106 }
1107 return None;
1108 }
1109
1110 for (id, job) in table.iter() {
1112 if job.command.starts_with(spec) {
1113 return Some(id);
1114 }
1115 }
1116
1117 None
1118 }
1119 }
1120}
1121
1122pub fn findjobnam(name: &str, table: &JobTable) -> Option<usize> {
1124 for (id, job) in table.iter() {
1125 if job.command == name {
1126 return Some(id);
1127 }
1128 }
1129 None
1130}
1131
1132pub fn isanum(s: &str) -> bool {
1134 !s.is_empty() && s.chars().all(|c| c.is_ascii_digit())
1135}
1136
1137pub fn init_jobs() -> (JobTable, JobPointers) {
1139 (JobTable::new(), JobPointers::new())
1140}
1141
1142#[cfg(unix)]
1144pub fn acquire_pgrp() -> bool {
1145 unsafe {
1146 let mypgrp = libc::getpgrp();
1147 let tpgrp = libc::tcgetpgrp(0);
1148 if tpgrp == -1 || tpgrp == mypgrp {
1149 return true;
1150 }
1151 if libc::setpgid(0, 0) == 0 {
1153 libc::tcsetpgrp(0, libc::getpgrp());
1154 return true;
1155 }
1156 false
1157 }
1158}
1159
1160pub fn storepipestats(job: &Job) -> Vec<i32> {
1162 job.procs.iter().map(|p| p.status).collect()
1163}
1164
1165pub fn clearjobtab(table: &mut JobTable, ptrs: &mut JobPointers) {
1167 table.jobs.clear();
1168 table.next_id = 1;
1169 ptrs.cur_job = None;
1170 ptrs.prev_job = None;
1171}
1172
1173pub fn scanjobs(table: &JobTable) -> Vec<String> {
1175 let mut output = Vec::new();
1176 for (id, job) in table.iter() {
1177 let state_str = match job.state {
1178 JobState::Running => "running",
1179 JobState::Done => "done",
1180 JobState::Stopped => "stopped",
1181 _ => "unknown",
1182 };
1183 output.push(format!("[{}] {} {}", id, state_str, job.command));
1184 }
1185 output
1186}
1187
1188#[derive(Debug, Clone, Default)]
1190pub struct ChildTimes {
1191 pub user_sec: f64,
1192 pub sys_sec: f64,
1193}
1194
1195pub fn shelltime() -> ChildTimes {
1196 #[cfg(unix)]
1197 {
1198 let mut usage: libc::rusage = unsafe { std::mem::zeroed() };
1199 if unsafe { libc::getrusage(libc::RUSAGE_SELF, &mut usage) } == 0 {
1200 return ChildTimes {
1201 user_sec: usage.ru_utime.tv_sec as f64
1202 + usage.ru_utime.tv_usec as f64 / 1_000_000.0,
1203 sys_sec: usage.ru_stime.tv_sec as f64 + usage.ru_stime.tv_usec as f64 / 1_000_000.0,
1204 };
1205 }
1206 }
1207 ChildTimes::default()
1208}
1209
1210pub fn childtime() -> ChildTimes {
1212 #[cfg(unix)]
1213 {
1214 let mut usage: libc::rusage = unsafe { std::mem::zeroed() };
1215 if unsafe { libc::getrusage(libc::RUSAGE_CHILDREN, &mut usage) } == 0 {
1216 return ChildTimes {
1217 user_sec: usage.ru_utime.tv_sec as f64
1218 + usage.ru_utime.tv_usec as f64 / 1_000_000.0,
1219 sys_sec: usage.ru_stime.tv_sec as f64 + usage.ru_stime.tv_usec as f64 / 1_000_000.0,
1220 };
1221 }
1222 }
1223 ChildTimes::default()
1224}
1225
1226pub fn update_process(proc: &mut Process, status: i32) {
1228 proc.end_time = Some(Instant::now());
1229 proc.status = status;
1230}
1231
1232pub fn findproc(jobtab: &[Job], pid: i32) -> Option<(usize, usize, bool)> {
1234 for (ji, job) in jobtab.iter().enumerate() {
1235 for (pi, proc) in job.procs.iter().enumerate() {
1236 if proc.pid == pid {
1237 return Some((ji, pi, false));
1238 }
1239 }
1240 for (pi, proc) in job.auxprocs.iter().enumerate() {
1241 if proc.pid == pid {
1242 return Some((ji, pi, true));
1243 }
1244 }
1245 }
1246 None
1247}
1248
1249pub fn update_job(job: &mut Job) -> bool {
1251 for proc in &job.auxprocs {
1253 if proc.is_running() {
1254 return false;
1255 }
1256 }
1257
1258 let all_done = true;
1260 let mut some_stopped = false;
1261 let mut last_status = 0;
1262
1263 for proc in &job.procs {
1264 if proc.is_running() {
1265 return false; }
1267 if proc.is_stopped() {
1268 some_stopped = true;
1269 }
1270 }
1271
1272 if let Some(last) = job.procs.last() {
1274 if last.is_signaled() {
1275 last_status = 0x80 | last.term_sig();
1276 } else if last.is_stopped() {
1277 last_status = 0x80 | last.stop_sig();
1278 } else {
1279 last_status = last.exit_status();
1280 }
1281 }
1282
1283 if some_stopped {
1284 job.stat |= stat::STOPPED;
1285 job.stat &= !stat::DONE;
1286 } else {
1287 job.stat |= stat::DONE;
1288 job.stat &= !stat::STOPPED;
1289 }
1290
1291 true
1292}
1293
1294pub fn update_bg_job(jobtab: &mut [Job], pid: i32, status: i32) -> bool {
1296 if let Some((ji, pi, is_aux)) = findproc(jobtab, pid) {
1297 if is_aux {
1298 jobtab[ji].auxprocs[pi].status = status;
1299 jobtab[ji].auxprocs[pi].end_time = Some(Instant::now());
1300 } else {
1301 jobtab[ji].procs[pi].status = status;
1302 jobtab[ji].procs[pi].end_time = Some(Instant::now());
1303 }
1304 update_job(&mut jobtab[ji]);
1305 return true;
1306 }
1307 false
1308}
1309
1310pub fn handle_sub(jobtab: &mut [Job], super_idx: usize, fg: bool) {
1312 let sub_idx = jobtab[super_idx].other;
1313 if sub_idx >= jobtab.len() {
1314 return;
1315 }
1316
1317 if jobtab[sub_idx].is_done() {
1319 if fg {
1320 }
1322 jobtab[super_idx].stat &= !stat::SUPERJOB;
1323 jobtab[super_idx].stat |= stat::WASSUPER;
1324 }
1325}
1326
1327pub fn setprevjob(ptrs: &mut JobPointers, jobtab: &[Job], maxjob: usize) {
1329 let mut best = None;
1331 for i in (1..=maxjob).rev() {
1332 if i >= jobtab.len() {
1333 continue;
1334 }
1335 let job = &jobtab[i];
1336 if (job.stat & stat::INUSE) != 0 && Some(i) != ptrs.cur_job {
1337 if (job.stat & stat::STOPPED) != 0 {
1338 best = Some(i);
1339 break;
1340 }
1341 if best.is_none() {
1342 best = Some(i);
1343 }
1344 }
1345 }
1346 ptrs.prev_job = best;
1347}
1348
1349pub fn setcurjob(ptrs: &mut JobPointers, jobtab: &[Job], maxjob: usize) {
1351 ptrs.cur_job = None;
1352 for i in (1..=maxjob).rev() {
1353 if i >= jobtab.len() {
1354 continue;
1355 }
1356 if (jobtab[i].stat & (stat::INUSE | stat::STOPPED)) == (stat::INUSE | stat::STOPPED) {
1357 ptrs.cur_job = Some(i);
1358 break;
1359 }
1360 }
1361 if ptrs.cur_job.is_none() {
1362 for i in (1..=maxjob).rev() {
1363 if i >= jobtab.len() {
1364 continue;
1365 }
1366 if (jobtab[i].stat & stat::INUSE) != 0 {
1367 ptrs.cur_job = Some(i);
1368 break;
1369 }
1370 }
1371 }
1372 setprevjob(ptrs, jobtab, maxjob);
1373}
1374
1375pub fn should_report_time(job: &Job, reporttime: f64) -> bool {
1377 if reporttime < 0.0 {
1378 return false;
1379 }
1380 if let Some(first) = job.procs.first() {
1381 if let (Some(start), Some(end)) =
1382 (first.start_time, job.procs.last().and_then(|p| p.end_time))
1383 {
1384 let elapsed = end.duration_since(start).as_secs_f64();
1385 return elapsed >= reporttime;
1386 }
1387 }
1388 false
1389}
1390
1391pub fn dumptime(job: &Job, format: &str) -> Option<String> {
1393 let first_start = job.procs.first()?.start_time?;
1394 let last_end = job.procs.last()?.end_time?;
1395 let elapsed = last_end.duration_since(first_start).as_secs_f64();
1396
1397 let mut total_user = 0.0;
1398 let mut total_sys = 0.0;
1399 for proc in &job.procs {
1400 total_user += proc.ti.user_time.as_secs_f64();
1401 total_sys += proc.ti.sys_time.as_secs_f64();
1402 }
1403
1404 Some(format_time(
1405 elapsed,
1406 total_user,
1407 total_sys,
1408 format,
1409 &get_job_text(job),
1410 ))
1411}
1412
1413pub fn waitjobs(jobtab: &mut Vec<Job>, thisjob: usize) {
1415 if thisjob < jobtab.len() {
1416 while !jobtab[thisjob].is_done() && !jobtab[thisjob].is_stopped() {
1417 #[cfg(unix)]
1418 {
1419 let mut status: i32 = 0;
1420 let pid = unsafe { libc::waitpid(-1, &mut status, libc::WUNTRACED) };
1421 if pid > 0 {
1422 update_bg_job(jobtab, pid, status);
1423 } else {
1424 break;
1425 }
1426 }
1427 #[cfg(not(unix))]
1428 {
1429 break;
1430 }
1431 }
1432 }
1433}
1434
1435pub fn waitonejob(job: &mut Job) {
1437 for proc in &mut job.procs {
1438 if proc.is_running() {
1439 if let Some(_status) = waitforpid(proc.pid) {
1440 }
1442 }
1443 }
1444}
1445
1446pub fn initjob(jobtab: &mut Vec<Job>) -> usize {
1448 for (i, job) in jobtab.iter().enumerate() {
1450 if (job.stat & stat::INUSE) == 0 {
1451 jobtab[i] = Job::new();
1452 jobtab[i].stat = stat::INUSE;
1453 return i;
1454 }
1455 }
1456 let idx = jobtab.len();
1458 let mut job = Job::new();
1459 job.stat = stat::INUSE;
1460 jobtab.push(job);
1461 idx
1462}
1463
1464pub fn setjobpwd(job: &mut Job) {
1466 if let Ok(cwd) = std::env::current_dir() {
1468 let _ = cwd;
1470 }
1471}
1472
1473pub fn spawnjob(job: &mut Job, fg: bool) {
1475 job.stat |= stat::INUSE;
1476 if !fg {
1477 job.stat &= !stat::CURSH;
1479 }
1480}
1481
1482pub fn selectjobtab(jobtab: &[Job]) -> usize {
1485 let mut max = 0;
1487 for (i, job) in jobtab.iter().enumerate() {
1488 if (job.stat & stat::INUSE) != 0 {
1489 max = i;
1490 }
1491 }
1492 max
1493}
1494
1495pub fn expandjobtab(jobtab: &mut Vec<Job>, needed: usize) {
1497 while jobtab.len() <= needed {
1498 jobtab.push(Job::new());
1499 }
1500}
1501
1502pub fn maybeshrinkjobtab(jobtab: &mut Vec<Job>) {
1504 while jobtab
1505 .last()
1506 .map(|j| (j.stat & stat::INUSE) == 0)
1507 .unwrap_or(false)
1508 {
1509 jobtab.pop();
1510 }
1511}
1512
1513pub fn addfilelist(job: &mut Job, filename: &str) {
1515 job.filelist.push(filename.to_string());
1516}
1517
1518pub fn pipecleanfilelist(job: &mut Job, proc_subst_only: bool) {
1520 if proc_subst_only {
1521 job.filelist
1523 .retain(|f| !f.starts_with("/dev/fd/") && !f.starts_with("/proc/"));
1524 } else {
1525 for file in &job.filelist {
1526 let _ = std::fs::remove_file(file);
1527 }
1528 job.filelist.clear();
1529 }
1530}
1531
1532pub fn deletefilelist(job: &mut Job, disowning: bool) {
1534 if !disowning {
1535 for file in &job.filelist {
1536 let _ = std::fs::remove_file(file);
1537 }
1538 }
1539 job.filelist.clear();
1540}
1541
1542pub fn printjob(
1544 job: &Job,
1545 job_num: usize,
1546 long_format: bool,
1547 cur_job: Option<usize>,
1548 prev_job: Option<usize>,
1549) -> String {
1550 let marker = if Some(job_num) == cur_job {
1551 '+'
1552 } else if Some(job_num) == prev_job {
1553 '-'
1554 } else {
1555 ' '
1556 };
1557
1558 let status_str = if job.is_done() {
1559 if let Some(last) = job.procs.last() {
1560 format_process_status(last.status)
1561 } else {
1562 "done".to_string()
1563 }
1564 } else if job.is_stopped() {
1565 "suspended".to_string()
1566 } else {
1567 "running".to_string()
1568 };
1569
1570 if long_format {
1571 let mut lines = Vec::new();
1572 for (i, proc) in job.procs.iter().enumerate() {
1573 let pstatus = format_process_status(proc.status);
1574 if i == 0 {
1575 lines.push(format!(
1576 "[{}] {} {:>5} {:16} {}",
1577 job_num, marker, proc.pid, pstatus, proc.text
1578 ));
1579 } else {
1580 lines.push(format!(
1581 " {:>5} {:16} | {}",
1582 proc.pid, pstatus, proc.text
1583 ));
1584 }
1585 }
1586 lines.join("\n")
1587 } else {
1588 format!(
1589 "[{}] {} {:16} {}",
1590 job_num,
1591 marker,
1592 status_str,
1593 get_job_text(job)
1594 )
1595 }
1596}
1597
1598pub fn getsigname(sig: i32) -> String {
1600 match sig {
1601 0 => "EXIT".to_string(),
1602 libc::SIGHUP => "HUP".to_string(),
1603 libc::SIGINT => "INT".to_string(),
1604 libc::SIGQUIT => "QUIT".to_string(),
1605 libc::SIGILL => "ILL".to_string(),
1606 libc::SIGTRAP => "TRAP".to_string(),
1607 libc::SIGABRT => "ABRT".to_string(),
1608 libc::SIGBUS => "BUS".to_string(),
1609 libc::SIGFPE => "FPE".to_string(),
1610 libc::SIGKILL => "KILL".to_string(),
1611 libc::SIGUSR1 => "USR1".to_string(),
1612 libc::SIGSEGV => "SEGV".to_string(),
1613 libc::SIGUSR2 => "USR2".to_string(),
1614 libc::SIGPIPE => "PIPE".to_string(),
1615 libc::SIGALRM => "ALRM".to_string(),
1616 libc::SIGTERM => "TERM".to_string(),
1617 libc::SIGCHLD => "CHLD".to_string(),
1618 libc::SIGCONT => "CONT".to_string(),
1619 libc::SIGSTOP => "STOP".to_string(),
1620 libc::SIGTSTP => "TSTP".to_string(),
1621 libc::SIGTTIN => "TTIN".to_string(),
1622 libc::SIGTTOU => "TTOU".to_string(),
1623 libc::SIGURG => "URG".to_string(),
1624 libc::SIGXCPU => "XCPU".to_string(),
1625 libc::SIGXFSZ => "XFSZ".to_string(),
1626 libc::SIGVTALRM => "VTALRM".to_string(),
1627 libc::SIGPROF => "PROF".to_string(),
1628 libc::SIGWINCH => "WINCH".to_string(),
1629 libc::SIGIO => "IO".to_string(),
1630 libc::SIGSYS => "SYS".to_string(),
1631 _ => format!("SIG{}", sig),
1632 }
1633}
1634
1635pub fn dtime_tv(dt: &mut Duration, t1: &Duration, t2: &Duration) -> Duration {
1637 if *t2 > *t1 {
1638 *dt = *t2 - *t1;
1639 } else {
1640 *dt = Duration::ZERO;
1641 }
1642 *dt
1643}
1644
1645pub fn dtime_ts(t1: &Instant, t2: &Instant) -> Duration {
1647 if *t2 > *t1 {
1648 t2.duration_since(*t1)
1649 } else {
1650 Duration::ZERO
1651 }
1652}
1653
1654pub fn makerunning(job: &mut Job) {
1656 job.make_running();
1657}
1658
1659pub fn hasprocs(job: &Job) -> bool {
1661 job.has_procs()
1662}
1663
1664pub fn get_usage() -> ChildTimes {
1666 childtime()
1667}
1668
1669#[cfg(unix)]
1671pub fn check_cursh_sig(jobtab: &[Job], sig: i32) {
1672 for job in jobtab {
1673 if (job.stat & stat::CURSH) != 0 && !job.is_done() {
1674 for proc in &job.procs {
1675 if proc.is_running() {
1676 unsafe {
1677 libc::kill(proc.pid, sig);
1678 }
1679 }
1680 }
1681 }
1682 }
1683}
1684
1685pub fn cleanfilelists(jobtab: &mut [Job]) {
1687 for job in jobtab.iter_mut() {
1688 deletefilelist(job, false);
1689 }
1690}
1691
1692pub fn clearoldjobtab(jobtab: &mut Vec<Job>) {
1694 jobtab.retain(|j| (j.stat & stat::INUSE) != 0);
1695}
1696
1697pub fn addbgstatus(bg: &mut BgStatus, pid: i32, status_val: i32) {
1699 bg.add(pid, status_val);
1700}
1701
1702pub fn getbgstatus(bg: &mut BgStatus, pid: i32) -> Option<i32> {
1704 bg.remove(pid)
1705}
1706
1707pub fn gettrapnode(sig: i32) -> Option<String> {
1709 let _ = sig;
1711 None
1712}
1713
1714pub fn removetrapnode(sig: i32) {
1716 let _ = sig;
1717}
1718
1719#[cfg(unix)]
1721pub fn release_pgrp() {
1722 }
1724
1725pub fn getsigidx(name: &str) -> Option<i32> {
1727 let name = name.strip_prefix("SIG").unwrap_or(name);
1728 match name.to_uppercase().as_str() {
1729 "EXIT" => Some(0),
1730 "HUP" => Some(libc::SIGHUP),
1731 "INT" => Some(libc::SIGINT),
1732 "QUIT" => Some(libc::SIGQUIT),
1733 "ILL" => Some(libc::SIGILL),
1734 "TRAP" => Some(libc::SIGTRAP),
1735 "ABRT" | "IOT" => Some(libc::SIGABRT),
1736 "BUS" => Some(libc::SIGBUS),
1737 "FPE" => Some(libc::SIGFPE),
1738 "KILL" => Some(libc::SIGKILL),
1739 "USR1" => Some(libc::SIGUSR1),
1740 "SEGV" => Some(libc::SIGSEGV),
1741 "USR2" => Some(libc::SIGUSR2),
1742 "PIPE" => Some(libc::SIGPIPE),
1743 "ALRM" => Some(libc::SIGALRM),
1744 "TERM" => Some(libc::SIGTERM),
1745 "CHLD" | "CLD" => Some(libc::SIGCHLD),
1746 "CONT" => Some(libc::SIGCONT),
1747 "STOP" => Some(libc::SIGSTOP),
1748 "TSTP" => Some(libc::SIGTSTP),
1749 "TTIN" => Some(libc::SIGTTIN),
1750 "TTOU" => Some(libc::SIGTTOU),
1751 "URG" => Some(libc::SIGURG),
1752 "XCPU" => Some(libc::SIGXCPU),
1753 "XFSZ" => Some(libc::SIGXFSZ),
1754 "VTALRM" => Some(libc::SIGVTALRM),
1755 "PROF" => Some(libc::SIGPROF),
1756 "WINCH" => Some(libc::SIGWINCH),
1757 "IO" | "POLL" => Some(libc::SIGIO),
1758 "SYS" => Some(libc::SIGSYS),
1759 _ => name.parse().ok(),
1760 }
1761}