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 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 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 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 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 pub fn get_mut(&mut self, id: usize) -> Option<&mut JobInfo> {
252 self.get_mut_internal(id)
253 }
254
255 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 pub fn list(&self) -> Vec<&JobInfo> {
271 self.jobs.iter().filter_map(|j| j.as_ref()).collect()
272 }
273
274 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 pub fn count(&self) -> usize {
283 self.jobs.iter().filter(|j| j.is_some()).count()
284 }
285
286 pub fn is_empty(&self) -> bool {
288 self.count() == 0
289 }
290
291 pub fn current(&self) -> Option<&JobInfo> {
293 self.current_id.and_then(|id| self.get(id))
294 }
295
296 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 match child.try_wait() {
305 Ok(Some(_status)) => {
306 job.state = JobState::Done;
308 }
309 Ok(None) => {
310 }
312 Err(_) => {
313 job.state = JobState::Done;
315 }
316 }
317 }
318 }
319 }
320
321 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
338pub 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 }
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 }); 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#[derive(Clone, Copy, Debug, PartialEq, Eq)]
431pub enum JobState {
432 Running,
433 Stopped,
434 Done,
435}
436
437#[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#[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#[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#[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
498pub 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
506pub 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 }
518}
519
520pub 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
536pub 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
603pub const DEFAULT_TIMEFMT: &str = "%J %U user %S system %P cpu %*E total";
605
606pub 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
644pub 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
687pub 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
723pub fn format_process_status(status: i32) -> String {
725 if status == SP_RUNNING {
726 "running".to_string()
727 } else if (status & 0x7f) == 0 {
728 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 let sig = (status >> 8) & 0xff;
738 format!("suspended ({})", sigmsg(sig))
739 } else {
740 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
751pub 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
763pub 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
769pub 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
804pub 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
832pub 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
854pub fn make_status(code: i32) -> i32 {
856 code << 8
857}
858
859pub fn make_signal_status(sig: i32) -> i32 {
861 sig
862}
863
864pub fn havefiles(job: &Job) -> bool {
866 !job.filelist.is_empty()
867}
868
869pub 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
879pub 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
890pub 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
912pub 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
939pub 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
963pub 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
987pub fn disown_job(job: &mut Job) {
989 job.stat |= stat::DISOWN;
990}
991
992pub fn job_is_done(job: &Job) -> bool {
994 (job.stat & stat::DONE) != 0 || job.procs.iter().all(|p| !p.is_running())
995}
996
997pub fn job_is_stopped(job: &Job) -> bool {
999 (job.stat & stat::STOPPED) != 0 || job.procs.iter().any(|p| p.is_stopped())
1000}
1001
1002pub 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
1014pub 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
1024pub 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
1061pub 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 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 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 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
1114pub 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
1124pub fn isanum(s: &str) -> bool {
1126 !s.is_empty() && s.chars().all(|c| c.is_ascii_digit())
1127}
1128
1129pub fn init_jobs() -> (JobTable, JobPointers) {
1131 (JobTable::new(), JobPointers::new())
1132}
1133
1134#[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 if libc::setpgid(0, 0) == 0 {
1145 libc::tcsetpgrp(0, libc::getpgrp());
1146 return true;
1147 }
1148 false
1149 }
1150}
1151
1152pub fn storepipestats(job: &Job) -> Vec<i32> {
1154 job.procs.iter().map(|p| p.status).collect()
1155}
1156
1157pub 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
1165pub 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#[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
1202pub 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
1218pub fn update_process(proc: &mut Process, status: i32) {
1220 proc.end_time = Some(Instant::now());
1221 proc.status = status;
1222}
1223
1224pub 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
1241pub fn update_job(job: &mut Job) -> bool {
1243 for proc in &job.auxprocs {
1245 if proc.is_running() {
1246 return false;
1247 }
1248 }
1249
1250 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; }
1259 if proc.is_stopped() {
1260 some_stopped = true;
1261 }
1262 }
1263
1264 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
1286pub 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
1302pub 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 jobtab[sub_idx].is_done() {
1311 if fg {
1312 }
1314 jobtab[super_idx].stat &= !stat::SUPERJOB;
1315 jobtab[super_idx].stat |= stat::WASSUPER;
1316 }
1317}
1318
1319pub fn setprevjob(ptrs: &mut JobPointers, jobtab: &[Job], maxjob: usize) {
1321 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
1341pub 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
1367pub 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
1383pub 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
1405pub 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
1427pub 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 }
1434 }
1435 }
1436}
1437
1438pub fn initjob(jobtab: &mut Vec<Job>) -> usize {
1440 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 let idx = jobtab.len();
1450 let mut job = Job::new();
1451 job.stat = stat::INUSE;
1452 jobtab.push(job);
1453 idx
1454}
1455
1456pub fn setjobpwd(job: &mut Job) {
1458 if let Ok(cwd) = std::env::current_dir() {
1460 let _ = cwd;
1462 }
1463}
1464
1465pub fn spawnjob(job: &mut Job, fg: bool) {
1467 job.stat |= stat::INUSE;
1468 if !fg {
1469 job.stat &= !stat::CURSH;
1471 }
1472}
1473
1474pub fn selectjobtab(jobtab: &[Job]) -> usize {
1477 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
1487pub fn expandjobtab(jobtab: &mut Vec<Job>, needed: usize) {
1489 while jobtab.len() <= needed {
1490 jobtab.push(Job::new());
1491 }
1492}
1493
1494pub 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
1505pub fn addfilelist(job: &mut Job, filename: &str) {
1507 job.filelist.push(filename.to_string());
1508}
1509
1510pub fn pipecleanfilelist(job: &mut Job, proc_subst_only: bool) {
1512 if proc_subst_only {
1513 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
1524pub 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
1534pub 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
1590pub 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
1627pub 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
1637pub 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
1646pub fn makerunning(job: &mut Job) {
1648 job.make_running();
1649}
1650
1651pub fn hasprocs(job: &Job) -> bool {
1653 job.has_procs()
1654}
1655
1656pub fn get_usage() -> ChildTimes {
1658 childtime()
1659}
1660
1661#[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
1677pub fn cleanfilelists(jobtab: &mut [Job]) {
1679 for job in jobtab.iter_mut() {
1680 deletefilelist(job, false);
1681 }
1682}
1683
1684pub fn clearoldjobtab(jobtab: &mut Vec<Job>) {
1686 jobtab.retain(|j| (j.stat & stat::INUSE) != 0);
1687}
1688
1689pub fn addbgstatus(bg: &mut BgStatus, pid: i32, status_val: i32) {
1691 bg.add(pid, status_val);
1692}
1693
1694pub fn getbgstatus(bg: &mut BgStatus, pid: i32) -> Option<i32> {
1696 bg.remove(pid)
1697}
1698
1699pub fn gettrapnode(sig: i32) -> Option<String> {
1701 let _ = sig;
1703 None
1704}
1705
1706pub fn removetrapnode(sig: i32) {
1708 let _ = sig;
1709}
1710
1711#[cfg(unix)]
1713pub fn release_pgrp() {
1714 }
1716
1717pub 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}