use std::env;
use std::os::unix::process::ExitStatusExt;
use std::process::Child;
use std::sync::atomic::Ordering;
use std::sync::{Mutex, OnceLock};
use std::time::{Duration, Instant};
use crate::DPUTS;
use crate::exec_jobs::JobTable;
use crate::ported::builtin::{SHELL_EXITING, STOPMSG};
use crate::ported::builtins::sched::zleactive;
use crate::ported::hashtable_h::{BIN_BG, BIN_DISOWN, BIN_FG, BIN_JOBS, BIN_WAIT};
use crate::ported::options::opt_state_set;
use crate::ported::params::{getsparam, setsparam, unsetparam};
use crate::ported::signals::{killjb, queue_signals, signal_block, signal_setmask, unqueue_signals, wait_for_processes};
use crate::ported::signals_h::{signal_default, signal_ignore, sigs_name, sigs_number};
use crate::ported::utils::zwarnnam;
use crate::ported::zsh_h::{MONITOR, OPT_ISSET, POSIXBUILTINS, STAT_ATTACH, STAT_INUSE, STAT_SUBJOB, STAT_SUBJOB_ORPHANED, STAT_SUPERJOB, isset, job, options, process, POSIXJOBS, LONGLISTJOBS, INTERACTIVE};
pub use crate::ported::zsh_h::{MAXJOBS_ALLOC, MAX_PIPESTATS, SP_RUNNING, timeinfo};
pub mod stat {
pub const CHANGED: i32 = 0x0001; pub const STOPPED: i32 = 0x0002; pub const TIMED: i32 = 0x0004; pub const DONE: i32 = 0x0008; pub const LOCKED: i32 = 0x0010; pub const NOPRINT: i32 = 0x0020; pub const INUSE: i32 = 0x0040; pub const SUPERJOB: i32 = 0x0080; pub const SUBJOB: i32 = 0x0100; pub const WASSUPER: i32 = 0x0200; pub const CURSH: i32 = 0x0400; pub const NOSTTY: i32 = 0x0800; pub const ATTACH: i32 = 0x1000; pub const SUBLEADER: i32 = 0x2000; pub const BUILTIN: i32 = 0x4000; pub const DISOWN: i32 = 0x10000; }
pub fn dtime_tv(dt: &mut Duration, t1: &Duration, t2: &Duration) -> Duration {
if *t2 > *t1 {
*dt = *t2 - *t1;
} else {
*dt = Duration::ZERO;
}
*dt
}
pub fn dtime_ts(t1: &Instant, t2: &Instant) -> Duration {
if *t2 > *t1 {
t2.duration_since(*t1)
} else {
Duration::ZERO
}
}
pub fn makerunning(jobtab: &mut [job], idx: usize) {
if idx >= jobtab.len() {
return;
}
let other = jobtab[idx].other as usize;
let is_super = (jobtab[idx].stat & stat::SUPERJOB) != 0;
{
let job = &mut jobtab[idx];
job.stat &= !stat::STOPPED;
for proc in &mut job.procs {
if proc.is_stopped() {
proc.status = SP_RUNNING;
}
}
}
if is_super && other != idx && other < jobtab.len() {
makerunning(jobtab, other);
}
}
pub fn findproc(jobtab: &[job], pid: i32, aux: bool) -> Option<(usize, usize, bool)> {
let mut last_match: Option<(usize, usize, bool)> = None;
for (ji, job) in jobtab.iter().enumerate().skip(1) {
if (job.stat & stat::DONE) != 0 {
continue;
}
let procs: &[process] = if aux { &job.auxprocs } else { &job.procs };
for (pi, proc) in procs.iter().enumerate() {
if proc.pid == pid {
if proc.status == SP_RUNNING {
return Some((ji, pi, aux)); }
last_match = Some((ji, pi, aux)); }
}
}
last_match
}
impl process {
pub fn new(pid: i32) -> Self {
process {
pid,
status: SP_RUNNING,
text: String::new(),
ti: timeinfo::default(),
bgtime: Some(Instant::now()),
endtime: None,
}
}
pub fn is_running(&self) -> bool {
self.status == SP_RUNNING
}
pub fn is_stopped(&self) -> bool {
self.status & 0xff == 0x7f
}
pub fn is_signaled(&self) -> bool {
(self.status & 0x7f) > 0 && (self.status & 0x7f) < 0x7f
}
pub fn exit_status(&self) -> i32 {
(self.status >> 8) & 0xff
}
pub fn term_sig(&self) -> i32 {
self.status & 0x7f
}
pub fn stop_sig(&self) -> i32 {
(self.status >> 8) & 0xff
}
}
impl job {
pub fn new() -> Self {
Self::default()
}
pub fn has_procs(&self) -> bool {
!self.procs.is_empty() || !self.auxprocs.is_empty()
}
pub fn is_running(&self) -> bool {
self.procs.iter().any(|p| p.is_running())
}
pub fn is_done(&self) -> bool {
!self.procs.is_empty()
&& self
.procs
.iter()
.all(|p| !p.is_running() && !p.is_stopped())
}
pub fn is_stopped(&self) -> bool {
(self.stat & stat::STOPPED) != 0 || self.procs.iter().any(|p| p.is_stopped())
}
pub fn is_inuse(&self) -> bool {
(self.stat & stat::INUSE) != 0
}
pub fn make_running(&mut self) {
for p in &mut self.procs {
if p.is_stopped() {
p.status = SP_RUNNING;
}
}
self.stat &= !stat::STOPPED;
}
}
pub fn hasprocs(jobtab: &[job], job: usize) -> bool {
jobtab
.get(job)
.map(|j| !j.procs.is_empty() || !j.auxprocs.is_empty())
.unwrap_or(false)
}
pub fn super_job(jobtab: &[job], job_idx: usize) -> Option<usize> {
for (i, job) in jobtab.iter().enumerate() {
if (job.stat & stat::SUPERJOB) != 0 && job.other as usize == job_idx && job.gleader != 0
{
return Some(i);
}
}
None
}
pub fn handle_sub(jobtab: &mut [job], super_idx: usize, fg: bool) -> i32 {
let sub_idx = jobtab[super_idx].other as usize;
if sub_idx >= jobtab.len() {
return 0;
}
let sj_done = (jobtab[sub_idx].stat & stat::DONE) != 0
|| (jobtab[sub_idx].procs.is_empty() && jobtab[sub_idx].auxprocs.is_empty());
if sj_done {
let mut signaled: Option<i32> = None;
for p in jobtab[sub_idx].procs.iter() {
#[cfg(unix)]
if libc::WIFSIGNALED(p.status) {
signaled = Some(libc::WTERMSIG(p.status));
break;
}
}
if let Some(sig) = signaled {
let jn_gleader = jobtab[super_idx].gleader;
let multi_procs = jobtab[super_idx].procs.len() > 1;
#[cfg(unix)]
{
let mypgrp = unsafe { libc::getpgrp() };
if jn_gleader != mypgrp && multi_procs {
unsafe { libc::killpg(jn_gleader, sig) }; } else if let Some(p0) = jobtab[super_idx].procs.first() {
unsafe { libc::kill(p0.pid, sig) }; }
let sj_other = jobtab[sub_idx].other;
unsafe { libc::kill(sj_other, libc::SIGCONT) }; unsafe { libc::kill(sj_other, sig) }; }
#[cfg(not(unix))]
{
let _ = (jn_gleader, multi_procs, sig);
}
} else {
jobtab[super_idx].stat &= !stat::SUPERJOB; jobtab[super_idx].stat |= stat::WASSUPER; let cp: bool;
#[cfg(unix)]
{
let first_status = jobtab[super_idx]
.procs
.first()
.map(|p| p.status)
.unwrap_or(0);
let dead = libc::WIFEXITED(first_status) || libc::WIFSIGNALED(first_status);
let gleader_dead = dead
&& unsafe { libc::killpg(jobtab[super_idx].gleader, 0) } == -1
&& std::io::Error::last_os_error().raw_os_error() == Some(libc::ESRCH);
cp = gleader_dead;
if cp {
if let Some(last) = jobtab[super_idx].procs.last() {
jobtab[super_idx].gleader = last.pid; }
}
}
#[cfg(not(unix))]
{
cp = false;
}
let thisjob = *THISJOB
.get_or_init(|| Mutex::new(-1))
.lock()
.expect("thisjob poisoned");
let cond_attach = fg || thisjob as usize == super_idx;
let single_proc = jobtab[super_idx].procs.len() == 1;
let first_pid_neq_gleader = jobtab[super_idx]
.procs
.first()
.map(|p| p.pid != jobtab[super_idx].gleader)
.unwrap_or(false);
if cond_attach && (single_proc || cp || first_pid_neq_gleader) {
#[cfg(unix)]
crate::ported::utils::attachtty(jobtab[super_idx].gleader);
}
#[cfg(unix)]
unsafe {
libc::kill(jobtab[sub_idx].other, libc::SIGCONT);
}
if (jobtab[super_idx].stat & stat::DISOWN) != 0 {
deletejob(&mut jobtab[super_idx], true);
}
}
if let Ok(mut cj) = CURJOB.get_or_init(|| Mutex::new(-1)).lock() {
*cj = super_idx as i32;
}
return 0; } else if (jobtab[sub_idx].stat & stat::STOPPED) != 0 {
jobtab[super_idx].stat |= stat::STOPPED; let sj_proc_status = jobtab[sub_idx].procs.first().map(|p| p.status).unwrap_or(0);
for p in jobtab[super_idx].procs.iter_mut() {
if p.status == SP_RUNNING || {
#[cfg(unix)]
{ !libc::WIFEXITED(p.status) && !libc::WIFSIGNALED(p.status) }
#[cfg(not(unix))]
{ false }
}
{
p.status = sj_proc_status; }
}
if let Ok(mut cj) = CURJOB.get_or_init(|| Mutex::new(-1)).lock() {
*cj = super_idx as i32; }
return 1; }
0 }
pub fn get_usage() -> timeinfo {
#[cfg(unix)]
{
let mut u: libc::rusage = unsafe { std::mem::zeroed() };
if unsafe { libc::getrusage(libc::RUSAGE_CHILDREN, &mut u) } == 0 {
return timeinfo::from_rusage(&u);
}
}
timeinfo::default()
}
pub fn update_process(pn: &mut process, status: i32) {
let prev = CHILD_USAGE_PREV.with(|c| c.borrow().clone()); let now = get_usage(); CHILD_USAGE_PREV.with(|c| *c.borrow_mut() = now.clone());
pn.endtime = Some(Instant::now()); pn.status = status;
let diff = |a: i64, b: i64| -> i64 { (a - b).max(0) };
pn.ti = timeinfo {
ut: diff(now.ut, prev.ut), st: diff(now.st, prev.st), maxrss: now.maxrss.max(prev.maxrss),
majflt: diff(now.majflt, prev.majflt),
minflt: diff(now.minflt, prev.minflt),
nswap: diff(now.nswap, prev.nswap),
ixrss: diff(now.ixrss, prev.ixrss),
idrss: diff(now.idrss, prev.idrss),
isrss: diff(now.isrss, prev.isrss),
inblock: diff(now.inblock, prev.inblock),
oublock: diff(now.oublock, prev.oublock),
nvcsw: diff(now.nvcsw, prev.nvcsw),
nivcsw: diff(now.nivcsw, prev.nivcsw),
msgsnd: diff(now.msgsnd, prev.msgsnd),
msgrcv: diff(now.msgrcv, prev.msgrcv),
nsignals: diff(now.nsignals, prev.nsignals),
};
}
thread_local! {
static CHILD_USAGE_PREV: std::cell::RefCell<timeinfo>
= const { std::cell::RefCell::new(timeinfo {
ut: 0, st: 0, maxrss: 0, majflt: 0, minflt: 0, nswap: 0,
ixrss: 0, idrss: 0, isrss: 0, inblock: 0, oublock: 0,
nvcsw: 0, nivcsw: 0, msgsnd: 0, msgrcv: 0, nsignals: 0,
}) };
}
#[cfg(unix)]
pub fn check_cursh_sig(jobtab: &[job], sig: i32) {
for job in jobtab {
if (job.stat & stat::CURSH) != 0 && !job.is_done() {
for proc in &job.procs {
if proc.is_running() {
unsafe {
libc::kill(proc.pid, sig);
}
}
}
}
}
}
pub fn storepipestats(job: &job) -> (Vec<i32>, i32) {
let mut stats = Vec::with_capacity(job.procs.len().min(MAX_PIPESTATS));
let mut pipefail = 0;
for p in job.procs.iter().take(MAX_PIPESTATS) {
let st = p.status;
let entry = if st == SP_RUNNING {
0
} else if (st & 0x7f) > 0 && (st & 0x7f) < 0x7f {
0o200 | (st & 0x7f)
} else if (st & 0xff) == 0x7f {
0o200 | ((st >> 8) & 0xff)
} else {
(st >> 8) & 0xff
};
stats.push(entry);
if entry != 0 {
pipefail = entry;
}
}
(stats, pipefail)
}
pub fn update_job(job: &mut job) -> bool {
for proc in job.auxprocs.iter_mut() {
#[cfg(unix)]
if proc.status > 0
&& !libc::WIFEXITED(proc.status)
&& !libc::WIFSIGNALED(proc.status)
&& !libc::WIFSTOPPED(proc.status)
{
proc.status = SP_RUNNING;
}
if proc.is_running() {
return false;
}
}
let mut some_stopped = false;
let mut signalled = false;
let mut val: i32 = 0;
let proc_count = job.procs.len();
for (i, proc) in job.procs.iter_mut().enumerate() {
#[cfg(unix)]
if proc.status > 0
&& !libc::WIFEXITED(proc.status)
&& !libc::WIFSIGNALED(proc.status)
&& !libc::WIFSTOPPED(proc.status)
{
job.stat &= !stat::STOPPED;
proc.status = SP_RUNNING;
}
if proc.is_running() {
return false;
}
if proc.is_stopped() {
some_stopped = true;
}
if i + 1 == proc_count {
#[cfg(unix)]
{
if libc::WIFSIGNALED(proc.status) {
val = 0o200 | libc::WTERMSIG(proc.status);
signalled = true;
} else if libc::WIFSTOPPED(proc.status) {
val = 0o200 | libc::WSTOPSIG(proc.status);
} else {
val = libc::WEXITSTATUS(proc.status);
}
}
#[cfg(not(unix))]
{
val = proc.status;
}
}
}
if some_stopped {
if (job.stat & stat::SUBJOB) != 0 {
job.stat |= stat::CHANGED | stat::STOPPED; return true;
}
if (job.stat & stat::STOPPED) != 0 {
return true; }
job.stat |= stat::STOPPED;
job.stat &= !stat::DONE;
job.stat |= stat::CHANGED;
return true;
}
job.stat |= stat::DONE | stat::CHANGED;
job.stat &= !stat::STOPPED;
LASTVAL2.store(val, Ordering::SeqCst);
let _inforeground: i32 = if (job.stat & stat::CURSH) != 0 {
1
} else {
0
};
let _ = signalled;
true
}
pub static LASTVAL2: std::sync::atomic::AtomicI32 = std::sync::atomic::AtomicI32::new(0);
pub fn update_bg_job(jn: &mut [job], pid: i32, status: i32) -> bool {
let hit = findproc(jn, pid, false).or_else(|| findproc(jn, pid, true));
if let Some((ji, pi, is_aux)) = hit {
if is_aux {
jn[ji].auxprocs[pi].status = status;
jn[ji].auxprocs[pi].endtime = Some(Instant::now());
} else {
jn[ji].procs[pi].status = status;
jn[ji].procs[pi].endtime = Some(Instant::now());
}
update_job(&mut jn[ji]);
return true;
}
false
}
pub fn setprevjob() {
let tab = JOBTAB
.get_or_init(|| Mutex::new(Vec::new()))
.lock()
.expect("jobtab poisoned");
let maxjob = *MAXJOB
.get_or_init(|| Mutex::new(0))
.lock()
.expect("maxjob poisoned");
let curjob = *CURJOB
.get_or_init(|| Mutex::new(-1))
.lock()
.expect("curjob poisoned");
let thisjob = *THISJOB
.get_or_init(|| Mutex::new(-1))
.lock()
.expect("thisjob poisoned");
for i in (1..=maxjob).rev() {
if i >= tab.len() {
continue;
}
let j = &tab[i];
if (j.stat & (stat::INUSE | stat::STOPPED)) == (stat::INUSE | stat::STOPPED)
&& (j.stat & stat::SUBJOB) == 0
&& i as i32 != curjob
&& i as i32 != thisjob
{
*PREVJOB.get_or_init(|| Mutex::new(-1)).lock().unwrap() = i as i32;
return;
}
}
for i in (1..=maxjob).rev() {
if i >= tab.len() {
continue;
}
let j = &tab[i];
if (j.stat & stat::INUSE) != 0
&& (j.stat & stat::SUBJOB) == 0
&& i as i32 != curjob
&& i as i32 != thisjob
{
*PREVJOB.get_or_init(|| Mutex::new(-1)).lock().unwrap() = i as i32;
return;
}
}
*PREVJOB.get_or_init(|| Mutex::new(-1)).lock().unwrap() = -1;
}
pub fn get_clktck() -> i64 {
#[cfg(unix)]
{
static CLKTCK: OnceLock<i64> = OnceLock::new(); *CLKTCK.get_or_init(|| unsafe { libc::sysconf(libc::_SC_CLK_TCK) as i64 })
}
#[cfg(not(unix))]
{
100 }
}
pub fn printhhmmss(secs: f64) -> String {
let mins = (secs / 60.0) as i32;
let hours = mins / 60;
let secs = secs - (mins * 60) as f64;
let mins = mins - (hours * 60);
if hours > 0 {
format!("{}:{:02}:{:05.2}", hours, mins, secs)
} else if mins > 0 {
format!("{}:{:05.2}", mins, secs)
} else {
format!("{:.3}", secs)
}
}
pub fn printtime(
elapsed_secs: f64,
ti: &timeinfo,
format: &str,
job_name: &str,
) -> String {
let user_secs = ti.ut as f64 / 1_000_000.0;
let system_secs = ti.st as f64 / 1_000_000.0;
let mut result = String::new();
let total_time = user_secs + system_secs; let percent = if elapsed_secs > 0.0 {
(100.0 * total_time / elapsed_secs) as i32
} else {
0
};
let per_sec = |v: i64| -> i64 {
if total_time > 0.0 {
(v as f64 / total_time) as i64
} else {
0
}
};
let mut chars = format.chars().peekable();
while let Some(c) = chars.next() {
if c == '%' {
match chars.next() {
Some('E') => result.push_str(&format!("{:.2}s", elapsed_secs)),
Some('U') => result.push_str(&format!("{:.2}s", user_secs)),
Some('S') => result.push_str(&format!("{:.2}s", system_secs)),
Some('P') => result.push_str(&format!("{}%", percent)),
Some('J') => result.push_str(job_name),
Some('m') => match chars.next() {
Some('E') => result.push_str(&format!("{:.0}ms", elapsed_secs * 1000.0)),
Some('U') => result.push_str(&format!("{:.0}ms", user_secs * 1000.0)),
Some('S') => result.push_str(&format!("{:.0}ms", system_secs * 1000.0)),
_ => result.push_str("%m"),
},
Some('u') => match chars.next() {
Some('E') => result.push_str(&format!("{:.0}us", elapsed_secs * 1_000_000.0)),
Some('U') => result.push_str(&format!("{:.0}us", user_secs * 1_000_000.0)),
Some('S') => result.push_str(&format!("{:.0}us", system_secs * 1_000_000.0)),
_ => result.push_str("%u"),
},
Some('n') => match chars.next() {
Some('E') => {
result.push_str(&format!("{:.0}ns", elapsed_secs * 1_000_000_000.0))
}
Some('U') => result.push_str(&format!("{:.0}ns", user_secs * 1_000_000_000.0)),
Some('S') => {
result.push_str(&format!("{:.0}ns", system_secs * 1_000_000_000.0))
}
_ => result.push_str("%n"),
},
Some('*') => match chars.next() {
Some('E') => result.push_str(&printhhmmss(elapsed_secs)),
Some('U') => result.push_str(&printhhmmss(user_secs)),
Some('S') => result.push_str(&printhhmmss(system_secs)),
_ => result.push_str("%*"),
},
Some('W') => result.push_str(&format!("{}", ti.nswap)),
Some('X') => result.push_str(&format!("{}", per_sec(ti.ixrss))),
Some('D') => result.push_str(&format!("{}", per_sec(ti.idrss + ti.isrss))),
Some('K') => {
result.push_str(&format!("{}", per_sec(ti.ixrss + ti.idrss + ti.isrss)))
}
Some('M') => result.push_str(&format!("{}", ti.maxrss)),
Some('F') => result.push_str(&format!("{}", ti.majflt)),
Some('R') => result.push_str(&format!("{}", ti.minflt)),
Some('I') => result.push_str(&format!("{}", ti.inblock)),
Some('O') => result.push_str(&format!("{}", ti.oublock)),
Some('c') => result.push_str(&format!("{}", ti.nivcsw)),
Some('w') => result.push_str(&format!("{}", ti.nvcsw)),
Some('%') => result.push('%'),
Some(other) => {
result.push('%');
result.push(other);
}
None => result.push('%'),
}
} else {
result.push(c);
}
}
result
}
pub fn dumptime(job: &job) -> Option<String> {
if job.procs.is_empty() {
return None;
}
const DEFAULT_TIMEFMT: &str = "%J %U user %S system %P cpu %*E total";
let format =
getsparam("TIMEFMT").unwrap_or_else(|| DEFAULT_TIMEFMT.to_string());
let lines: Vec<String> = job
.procs
.iter()
.filter_map(|p| {
let start = p.bgtime?;
let end = p.endtime?;
let elapsed = end.duration_since(start).as_secs_f64();
Some(printtime(elapsed, &p.ti, &format, &p.text))
})
.collect();
if lines.is_empty() {
None
} else {
Some(lines.join("\n"))
}
}
pub fn should_report_time(job: &job, reporttime: f64) -> bool {
let reportmemory: i64 = getsparam("REPORTMEMORY")
.and_then(|s| s.parse().ok())
.unwrap_or(-1);
if (job.stat & stat::TIMED) != 0 {
return true;
}
if reporttime < 0.0 && reportmemory < 0 {
return false;
}
let first = match job.procs.first() {
Some(p) => p,
None => return false,
};
if zleactive.load(Ordering::Relaxed) != 0
{
return false;
}
if reporttime >= 0.0 {
let cpu_secs = (first.ti.ut + first.ti.st) as f64 / 1_000_000.0;
if cpu_secs >= reporttime {
return true;
}
if let (Some(start), Some(end)) = (first.bgtime, job.procs.last().and_then(|p| p.endtime)) {
let elapsed = end.duration_since(start).as_secs_f64();
if elapsed >= reporttime {
return true;
}
}
}
if reportmemory >= 0 && first.ti.maxrss > reportmemory {
return true;
}
false
}
static SIG_MSG: &[(libc::c_int, &str)] = &[
(libc::SIGHUP, "hangup"),
(libc::SIGINT, "interrupt"),
(libc::SIGQUIT, "quit"),
(libc::SIGILL, "illegal instruction"),
(libc::SIGTRAP, "trace trap"),
(libc::SIGABRT, "abort"),
(libc::SIGBUS, "bus error"),
(libc::SIGFPE, "floating point exception"),
(libc::SIGKILL, "killed"),
(libc::SIGUSR1, "user-defined signal 1"),
(libc::SIGSEGV, "segmentation fault"),
(libc::SIGUSR2, "user-defined signal 2"),
(libc::SIGPIPE, "broken pipe"),
(libc::SIGALRM, "alarm"),
(libc::SIGTERM, "terminated"),
(libc::SIGCHLD, "child exited"),
(libc::SIGCONT, "continued"),
(libc::SIGSTOP, "stopped (signal)"),
(libc::SIGTSTP, "stopped"),
(libc::SIGTTIN, "stopped (tty input)"),
(libc::SIGTTOU, "stopped (tty output)"),
(libc::SIGURG, "urgent I/O condition"),
(libc::SIGXCPU, "CPU time exceeded"),
(libc::SIGXFSZ, "file size exceeded"),
(libc::SIGVTALRM, "virtual timer expired"),
(libc::SIGPROF, "profiling timer expired"),
(libc::SIGWINCH, "window changed"),
(libc::SIGIO, "I/O ready"),
(libc::SIGSYS, "bad system call"),
];
pub fn sigmsg(sig: i32) -> &'static str {
SIG_MSG
.iter()
.find(|(s, _)| *s == sig)
.map(|(_, m)| *m)
.unwrap_or("unknown signal") }
pub fn printjob(
job: &job,
job_num: usize,
long_format: bool,
cur_job: Option<usize>,
prev_job: Option<usize>,
) -> String {
let fmt_proc_status = |status: i32| -> String {
if status == SP_RUNNING {
"running".to_string()
} else if (status & 0x7f) == 0 {
let code = (status >> 8) & 0xff;
if code == 0 {
"done".to_string()
} else {
format!("exit {}", code)
}
} else if (status & 0xff) == 0x7f {
let sig = (status >> 8) & 0xff;
format!("suspended ({})", sigmsg(sig))
} else {
let sig = status & 0x7f;
let core = (status >> 7) & 1;
if core != 0 {
format!("{} (core dumped)", sigmsg(sig))
} else {
sigmsg(sig).to_string()
}
}
};
let marker = if Some(job_num) == cur_job {
'+'
} else if Some(job_num) == prev_job {
'-'
} else {
' '
};
let status_str = if job.is_done() {
if let Some(last) = job.procs.last() {
fmt_proc_status(last.status)
} else {
"done".to_string()
}
} else if job.is_stopped() {
"suspended".to_string()
} else {
"running".to_string()
};
let header = if long_format {
let mut lines = Vec::new();
for (i, proc) in job.procs.iter().enumerate() {
let pstatus = fmt_proc_status(proc.status);
if i == 0 {
lines.push(format!(
"[{}] {} {:>5} {:16} {}",
job_num, marker, proc.pid, pstatus, proc.text
));
} else {
lines.push(format!(
" {:>5} {:16} | {}",
proc.pid, pstatus, proc.text
));
}
}
lines.join("\n")
} else {
format!(
"[{}] {} {:16} {}",
job_num,
marker,
status_str,
if !job.text.is_empty() {
job.text.clone()
} else {
job.procs
.iter()
.map(|p| p.text.as_str())
.collect::<Vec<_>>()
.join(" | ")
}
)
};
let reporttime: f64 = getsparam("REPORTTIME")
.and_then(|s| s.parse().ok())
.unwrap_or(-1.0);
if should_report_time(job, reporttime) {
if let Some(timing) = dumptime(job) {
return format!("{}\n{}", header, timing);
}
}
header
}
pub fn addfilelist(job: &mut job, name: Option<&str>, fd: i32) {
match name {
Some(n) => job.filelist.push(n.to_string()),
None => job.filelist.push(format!("<fd:{}>", fd)),
}
}
pub fn pipecleanfilelist(filelist: &mut job, proc_subst_only: bool) {
if proc_subst_only {
filelist.filelist.retain(|f| {
!f.starts_with("/dev/fd/") && !f.starts_with("/proc/") && !f.starts_with("<fd:")
});
} else {
for entry in &filelist.filelist {
if let Some(rest) = entry.strip_prefix("<fd:") {
if let Some(num_str) = rest.strip_suffix('>') {
if let Ok(fd) = num_str.parse::<i32>() {
#[cfg(unix)]
unsafe {
libc::close(fd);
} }
}
} else {
let _ = std::fs::remove_file(entry); }
}
filelist.filelist.clear();
}
}
pub fn deletefilelist(file_list: &mut job, disowning: bool) {
if !disowning {
for entry in &file_list.filelist {
if let Some(rest) = entry.strip_prefix("<fd:") {
if let Some(num_str) = rest.strip_suffix('>') {
if let Ok(fd) = num_str.parse::<i32>() {
#[cfg(unix)]
unsafe {
libc::close(fd);
} }
}
} else {
let _ = std::fs::remove_file(entry); }
}
}
file_list.filelist.clear();
}
pub fn cleanfilelists(jobtab: &mut [job]) {
DPUTS!(
SHELL_EXITING .load(std::sync::atomic::Ordering::Relaxed)
>= 0, "BUG: cleanfilelists() before exit" );
for job in jobtab.iter_mut().skip(1) {
deletefilelist(job, false);
}
}
pub fn freejob(jn: &mut job, deleting: bool) {
let _ = deleting; jn.procs.clear();
jn.auxprocs.clear();
jn.ty = None;
jn.pwd = None;
jn.gleader = 0;
jn.other = 0;
jn.stat = 0;
jn.stty_in_env = 0;
jn.filelist.clear();
jn.text.clear();
}
pub fn deletejob(jn: &mut job, disowning: bool) {
deletefilelist(jn, disowning);
if (jn.stat & STAT_ATTACH) != 0 {
#[cfg(unix)]
unsafe {
let pgrp =
crate::ported::modules::clone::mypgrp.load(Ordering::Relaxed);
if pgrp > 0 {
libc::tcsetpgrp(0, pgrp); }
}
}
if (jn.stat & STAT_SUPERJOB) != 0 {
let other = jn.other as usize;
if let Some(tab) = JOBTAB.get() {
if let Ok(mut jobs) = tab.lock() {
if let Some(jno) = jobs.get_mut(other) {
if (jno.stat & STAT_SUBJOB) != 0 {
jno.stat |= STAT_SUBJOB_ORPHANED; }
}
}
}
}
freejob(jn, true);
}
pub fn addproc(
job: &mut job,
pid: i32,
text: &str,
aux: bool,
bgtime: Option<std::time::Instant>,
gleader: i32,
list_pipe_job_used: i32,
) {
let proc = process::new(pid);
let proc = process {
pid,
status: SP_RUNNING,
text: text.to_string(),
bgtime, ..proc
};
if aux {
job.auxprocs.push(proc);
} else {
if gleader != -1 {
job.gleader = gleader;
} else if job.gleader == 0 {
job.gleader = pid;
}
let _ = list_pipe_job_used;
job.procs.push(proc);
}
job.stat &= !stat::DONE;
}
pub fn havefiles(jobtab: &[job]) -> bool {
jobtab.iter().any(|j| j.stat != 0 && !j.filelist.is_empty())
}
pub fn waitforpid(pid: i32) -> Option<i32> {
#[cfg(unix)]
{
loop {
let mut status: i32 = 0;
let result = unsafe { libc::waitpid(pid, &mut status, 0) };
if result == pid {
if libc::WIFEXITED(status) {
return Some(libc::WEXITSTATUS(status));
} else if libc::WIFSIGNALED(status) {
return Some(128 + libc::WTERMSIG(status));
} else if libc::WIFSTOPPED(status) {
return None;
}
} else if result == -1 {
return None;
}
}
}
#[cfg(not(unix))]
{
let _ = pid;
None
}
}
pub fn zwaitjob(job: &mut job, wait_cmd: i32) -> Option<i32> {
if job.procs.is_empty() && job.auxprocs.is_empty() {
return Some(0);
}
use crate::ported::zsh_h::{ERRFLAG_ERROR, STAT_DONE, STAT_STOPPED, ZSIG_TRAPPED, INTERACTIVE};
use crate::ported::utils::errflag;
let q = crate::ported::signals_h::queue_signal_level();
crate::ported::signals_h::child_block();
crate::ported::signals::queue_traps(wait_cmd);
crate::ported::signals_h::dont_queue_signals();
job.stat |= crate::ported::zsh_h::STAT_LOCKED;
if !job.filelist.is_empty() {
crate::ported::jobs::pipecleanfilelist(job, false);
}
let interact = isset(INTERACTIVE);
loop {
if (errflag.load(std::sync::atomic::Ordering::Relaxed) & ERRFLAG_ERROR) != 0 {
break;
}
if job.stat == 0 {
break;
}
if (job.stat & STAT_DONE) != 0 {
break;
}
if interact && (job.stat & STAT_STOPPED) != 0 {
break;
}
let _ = crate::ported::signals::signal_suspend(libc::SIGCHLD, wait_cmd != 0);
let ls = crate::ported::signals::last_signal.load(std::sync::atomic::Ordering::Relaxed);
if ls != libc::SIGCHLD && wait_cmd != 0 && ls >= 0 {
let trapped_flag = {
let guard = crate::ported::signals::sigtrapped.lock().unwrap();
guard.get(ls as usize).copied().unwrap_or(0)
};
if (trapped_flag & ZSIG_TRAPPED) != 0 {
crate::ported::signals_h::restore_queue_signals(q);
crate::ported::signals::unqueue_traps();
crate::ported::signals_h::child_unblock();
return Some(128 + ls); }
}
if crate::ported::exec::subsh.load(std::sync::atomic::Ordering::Relaxed) != 0 {
if job.gleader != 0 {
unsafe {
libc::killpg(job.gleader, libc::SIGCONT);
}
}
}
crate::ported::signals_h::child_block();
}
crate::ported::signals_h::restore_queue_signals(q);
crate::ported::signals::unqueue_traps();
crate::ported::signals_h::child_unblock();
let last_status = job.procs.last().map(|p| p.exit_status()).unwrap_or(0);
Some(last_status) }
pub fn waitjobs(jobtab: &mut [job], thisjob: usize) {
if thisjob < jobtab.len() {
while !jobtab[thisjob].is_done() && !jobtab[thisjob].is_stopped() {
#[cfg(unix)]
{
let mut status: i32 = 0;
let pid = unsafe { libc::waitpid(-1, &mut status, libc::WUNTRACED) };
if pid > 0 {
update_bg_job(jobtab, pid, status);
} else {
break;
}
}
#[cfg(not(unix))]
{
break;
}
}
}
}
pub fn clearjobtab(table: &mut JobTable, monitor: i32) {
let _ = table; let posix_jobs = isset(POSIXJOBS); if posix_jobs {
if let Some(om) = OLDMAXJOB.get() {
if let Ok(mut o) = om.lock() {
*o = 0;
}
}
}
let tab = match JOBTAB.get() {
Some(t) => t,
None => return,
};
let mut jobs = match tab.lock() {
Ok(g) => g,
Err(_) => return,
};
let maxjob = jobs.len();
let mut new_oldmax: usize = 0;
for i in 1..maxjob {
if jobs[i].stat == 0 {
continue;
}
if monitor != 0 && !posix_jobs {
new_oldmax = i + 1; } else if (jobs[i].stat & STAT_INUSE) != 0 {
freejob(&mut jobs[i], false); }
}
if monitor != 0 && new_oldmax > 0 {
let mut snap: Vec<job> = jobs[..new_oldmax].iter().cloned().collect(); let thisjob = *THISJOB.get_or_init(|| Mutex::new(-1)).lock().unwrap();
if thisjob >= 0 && (thisjob as usize) < new_oldmax {
snap[thisjob as usize] = job::default(); }
if let Some(om) = OLDMAXJOB.get() {
if let Ok(mut o) = om.lock() {
*o = new_oldmax.saturating_sub(1); }
} else {
*OLDMAXJOB.get_or_init(|| Mutex::new(0)).lock().unwrap() = new_oldmax.saturating_sub(1);
}
*OLDJOBTAB
.get_or_init(|| Mutex::new(Vec::new()))
.lock()
.unwrap() = snap; }
}
pub fn clearoldjobtab() {
*OLDJOBTAB
.get_or_init(|| Mutex::new(Vec::new()))
.lock()
.expect("oldjobtab poisoned") = Vec::new();
*OLDMAXJOB
.get_or_init(|| Mutex::new(0))
.lock()
.expect("oldmaxjob poisoned") = 0;
}
pub fn initjob(jobtab: &mut Vec<job>) -> usize {
for (i, job) in jobtab.iter().enumerate() {
if (job.stat & stat::INUSE) == 0 {
jobtab[i] = job::new();
jobtab[i].stat = stat::INUSE;
return i;
}
}
let idx = jobtab.len();
let mut job = job::new();
job.stat = stat::INUSE;
jobtab.push(job);
idx
}
pub fn setjobpwd() {
let pwd = getsparam("PWD").unwrap_or_default(); let tab = JOBTAB.get_or_init(|| Mutex::new(Vec::new()));
let mut tab = tab.lock().expect("jobtab poisoned");
for job in tab.iter_mut().skip(1) {
if job.stat != 0 && job.pwd.is_none() {
job.pwd = Some(pwd.clone()); }
}
}
pub fn spawnjob() {
let thisjob_idx = *THISJOB
.get_or_init(|| Mutex::new(-1))
.lock()
.expect("thisjob poisoned");
DPUTS!(thisjob_idx == -1, "No valid job in spawnjob."); if thisjob_idx < 0 {
return;
}
let thisjob = thisjob_idx as usize;
let in_subsh = crate::ported::exec::FORKLEVEL.load(Ordering::Relaxed) > 0;
if !in_subsh {
let curjob = *CURJOB
.get_or_init(|| Mutex::new(-1))
.lock()
.expect("curjob poisoned");
let cur_stopped = if curjob >= 0 {
let tab = JOBTAB
.get_or_init(|| Mutex::new(Vec::new()))
.lock()
.expect("jobtab poisoned");
tab.get(curjob as usize)
.map(|j| (j.stat & stat::STOPPED) != 0)
.unwrap_or(false)
} else {
false
};
if curjob < 0 || !cur_stopped {
if let Ok(mut cj) = CURJOB.get_or_init(|| Mutex::new(-1)).lock() {
*cj = thisjob_idx; }
setprevjob(); } else {
let prevjob = *PREVJOB
.get_or_init(|| Mutex::new(-1))
.lock()
.expect("prevjob poisoned");
let prev_stopped = if prevjob >= 0 {
let tab = JOBTAB
.get_or_init(|| Mutex::new(Vec::new()))
.lock()
.expect("jobtab poisoned");
tab.get(prevjob as usize)
.map(|j| (j.stat & stat::STOPPED) != 0)
.unwrap_or(false)
} else {
false
};
if prevjob < 0 || !prev_stopped {
if let Ok(mut pj) = PREVJOB.get_or_init(|| Mutex::new(-1)).lock() {
*pj = thisjob_idx; }
}
}
if isset(MONITOR) {
let tab = JOBTAB
.get_or_init(|| Mutex::new(Vec::new()))
.lock()
.expect("jobtab poisoned");
if let Some(job) = tab.get(thisjob) {
if !job.procs.is_empty() {
let mut line = format!("[{}]", thisjob_idx);
for p in job.procs.iter() {
line.push_str(&format!(" {}", p.pid));
}
line.push('\n');
eprint!("{}", line); }
}
}
}
let need_delete: bool;
{
let tab = JOBTAB
.get_or_init(|| Mutex::new(Vec::new()))
.lock()
.expect("jobtab poisoned");
need_delete = tab
.get(thisjob)
.map(|j| j.procs.is_empty() && j.auxprocs.is_empty())
.unwrap_or(true);
}
if need_delete {
let mut tab = JOBTAB
.get_or_init(|| Mutex::new(Vec::new()))
.lock()
.expect("jobtab poisoned");
if let Some(j) = tab.get_mut(thisjob) {
deletejob(j, false); }
} else {
let mut tab = JOBTAB
.get_or_init(|| Mutex::new(Vec::new()))
.lock()
.expect("jobtab poisoned");
if let Some(j) = tab.get_mut(thisjob) {
j.stat |= stat::LOCKED; pipecleanfilelist(j, false); }
}
if let Ok(mut tj) = THISJOB.get_or_init(|| Mutex::new(-1)).lock() {
*tj = -1;
}
}
#[cfg(unix)]
pub fn shelltime(
shell: Option<&mut timeinfo>,
kids: Option<&mut timeinfo>,
then: Option<&mut std::time::Instant>,
delta: i32,
) {
let now = std::time::Instant::now();
let mut ti: timeinfo = {
let mut usage: libc::rusage = unsafe { std::mem::zeroed() };
if unsafe { libc::getrusage(libc::RUSAGE_SELF, &mut usage) } == 0 {
timeinfo::from_rusage(&usage)
} else {
timeinfo::default()
}
};
let shell_present = shell.is_some();
if let Some(s) = shell {
if delta != 0 {
ti.ut = ti.ut.saturating_sub(s.ut); ti.st = ti.st.saturating_sub(s.st); } else {
*s = ti.clone();
}
}
let dtime: std::time::Duration = if delta != 0 {
match then {
Some(t) => dtime_ts(t, &now), None => std::time::Duration::ZERO,
}
} else {
if let Some(t) = then {
*t = now;
}
let shtimer_dur = *crate::ported::params::shtimer_lock()
.lock()
.expect("shtimer poisoned");
let now_dur = std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.unwrap_or_default();
if now_dur > shtimer_dur {
now_dur - shtimer_dur
} else {
std::time::Duration::ZERO
}
};
if (delta == 0) == !shell_present {
let real_secs = dtime.as_secs_f64();
let timefmt = crate::ported::params::getsparam("TIMEFMT")
.unwrap_or_else(|| "%J %U user %S system %P cpu %*E total".to_string());
let line = printtime(real_secs, &ti, &timefmt, "shell"); eprintln!("{}", line);
}
let mut tc: timeinfo = {
let mut usage: libc::rusage = unsafe { std::mem::zeroed() };
if unsafe { libc::getrusage(libc::RUSAGE_CHILDREN, &mut usage) } == 0 {
timeinfo::from_rusage(&usage)
} else {
timeinfo::default()
}
};
let kids_present = kids.is_some();
if let Some(k) = kids {
if delta != 0 {
tc.ut = tc.ut.saturating_sub(k.ut); tc.st = tc.st.saturating_sub(k.st); } else {
*k = tc.clone(); }
}
if (delta == 0) == !kids_present {
let real_secs = dtime.as_secs_f64();
let timefmt = crate::ported::params::getsparam("TIMEFMT")
.unwrap_or_else(|| "%J %U user %S system %P cpu %*E total".to_string());
let line = printtime(real_secs, &tc, &timefmt, "children"); eprintln!("{}", line);
}
}
#[cfg(not(unix))]
pub fn shelltime(
_shell: Option<&mut timeinfo>,
_kids: Option<&mut timeinfo>,
_then: Option<&mut std::time::Instant>,
_delta: i32,
) {
}
pub fn scanjobs(table: &JobTable) -> Vec<String> {
let mut output = Vec::new();
for (id, job) in table.iter() {
let state_str = match job.state {
crate::exec_jobs::JobState::Running => "running",
crate::exec_jobs::JobState::Done => "done",
crate::exec_jobs::JobState::Stopped => "stopped",
};
output.push(format!("[{}] {} {}", id, state_str, job.command));
}
output
}
pub fn isanum(s: &str) -> bool {
!s.is_empty() && s.bytes().all(|b| b == b'-' || b.is_ascii_digit())
}
pub fn setcurjob() {
let tab = JOBTAB
.get_or_init(|| Mutex::new(Vec::new()))
.lock()
.expect("jobtab poisoned");
let maxjob = *MAXJOB
.get_or_init(|| Mutex::new(0))
.lock()
.expect("maxjob poisoned");
let mut found: i32 = -1;
for i in (1..=maxjob).rev() {
if i >= tab.len() {
continue;
}
if (tab[i].stat & (stat::INUSE | stat::STOPPED)) == (stat::INUSE | stat::STOPPED) {
found = i as i32;
break;
}
}
if found < 0 {
for i in (1..=maxjob).rev() {
if i >= tab.len() {
continue;
}
if (tab[i].stat & stat::INUSE) != 0 {
found = i as i32;
break;
}
}
}
*CURJOB.get_or_init(|| Mutex::new(-1)).lock().unwrap() = found;
drop(tab);
setprevjob();
}
pub fn selectjobtab() -> (Vec<job>, usize) {
let oldtab = OLDJOBTAB
.get_or_init(|| Mutex::new(Vec::new()))
.lock()
.expect("oldjobtab poisoned");
if !oldtab.is_empty() {
let oldmax = *OLDMAXJOB
.get_or_init(|| Mutex::new(0))
.lock()
.expect("oldmaxjob poisoned");
(oldtab.clone(), oldmax) } else {
drop(oldtab); let jobtab = JOBTAB
.get_or_init(|| Mutex::new(Vec::new()))
.lock()
.expect("jobtab poisoned");
let maxjob = *MAXJOB
.get_or_init(|| Mutex::new(0))
.lock()
.expect("maxjob poisoned");
(jobtab.clone(), maxjob) }
}
pub fn getjob(s: &str, prog: &str) -> i32 {
let mut jobnum: i32; let mymaxjob: i32; let myjobtab: Vec<job>;
let (tab, max) = selectjobtab(); myjobtab = tab;
mymaxjob = max as i32;
let curjob = *CURJOB
.get_or_init(|| Mutex::new(-1)) .lock()
.expect("curjob poisoned");
let prevjob = *PREVJOB
.get_or_init(|| Mutex::new(-1)) .lock()
.expect("prevjob poisoned");
let thisjob = *THISJOB
.get_or_init(|| Mutex::new(-1))
.lock()
.expect("thisjob poisoned");
let posixbuiltins = isset(
POSIXBUILTINS,
);
let s_bytes = s.as_bytes();
let mut idx = 0usize;
if s_bytes.is_empty() || s_bytes[0] != b'%' {
if let Some(jn) = findjobnam(s, &myjobtab, mymaxjob, thisjob) {
return jn;
}
if !posixbuiltins && !prog.is_empty() {
zwarnnam(prog, &format!("job not found: {}", s)); }
return -1; }
idx += 1;
if idx >= s_bytes.len() || s_bytes[idx] == b'%' || s_bytes[idx] == b'+' {
if curjob == -1 {
if !prog.is_empty() && !posixbuiltins {
zwarnnam(prog, "no current job"); }
return -1; }
return curjob; }
if s_bytes[idx] == b'-' {
if prevjob == -1 {
if !prog.is_empty() && !posixbuiltins {
zwarnnam(prog, "no previous job"); }
return -1; }
return prevjob; }
if s_bytes[idx].is_ascii_digit() {
let rest = &s[idx..];
jobnum = rest.parse::<i32>().unwrap_or(0); if jobnum > 0 && jobnum <= mymaxjob {
let ju = jobnum as usize;
if ju < myjobtab.len()
&& myjobtab[ju].stat != 0
&& (myjobtab[ju].stat & stat::SUBJOB) == 0 && jobnum != thisjob
{
return jobnum; }
}
if !prog.is_empty() && !posixbuiltins {
zwarnnam(prog, &format!("%{}: no such job", rest)); }
return -1; }
if s_bytes[idx] == b'?' {
let search = &s[idx + 1..]; jobnum = mymaxjob; while jobnum >= 0 {
let ju = jobnum as usize;
if ju < myjobtab.len()
&& myjobtab[ju].stat != 0 && (myjobtab[ju].stat & stat::SUBJOB) == 0 && jobnum != thisjob
{
for pn in &myjobtab[ju].procs {
if pn.text.contains(search) {
return jobnum; }
}
}
jobnum -= 1;
}
if !prog.is_empty() && !posixbuiltins {
zwarnnam(prog, &format!("job not found: {}", s)); }
return -1; }
let rest = &s[idx..];
if let Some(jn) = findjobnam(rest, &myjobtab, mymaxjob, thisjob) {
return jn; }
if !posixbuiltins && !prog.is_empty() {
zwarnnam(prog, &format!("job not found: {}", s)); }
-1 }
pub fn init_jobs(argv: &[String], envp: &[String]) -> JobTable {
let table = JobTable::new(); if !argv.is_empty() {
let zero = argv[0].as_str();
let mut hackspace = zero.len(); for entry in argv.iter().skip(1).chain(envp.iter()) {
hackspace += 1 + entry.len(); }
env::set_var("__zshrs_hackspace", hackspace.to_string()); }
table }
pub const MAX_MAXJOBS: usize = 1000;
pub fn expandjobtab(jobtab: &mut Vec<job>, _needed: usize) -> bool {
let newsize = jobtab.len() + MAXJOBS_ALLOC;
if newsize > MAX_MAXJOBS {
return false;
}
jobtab.resize_with(newsize, job::new);
true
}
pub fn maybeshrinkjobtab(jobtab: &mut Vec<job>) {
while jobtab
.last()
.map(|j| (j.stat & stat::INUSE) == 0)
.unwrap_or(false)
{
jobtab.pop();
}
}
#[allow(non_camel_case_types)]
#[derive(Clone, Copy)]
pub struct bgstatus {
pub pid: i32, pub status: i32, }
pub type Bgstatus = Box<bgstatus>;
pub static bgstatus_list: Mutex<Vec<bgstatus>> = Mutex::new(Vec::new());
pub static bgstatus_count: std::sync::atomic::AtomicI64 = std::sync::atomic::AtomicI64::new(0);
pub fn addbgstatus(pid: i32, status_val: i32) {
let max_child = unsafe { libc::sysconf(libc::_SC_CHILD_MAX) };
let cap = if max_child > 0 {
max_child as i64
} else {
1024
};
if let Ok(mut list) = bgstatus_list.lock() {
if bgstatus_count.load(Ordering::Relaxed) >= cap {
if !list.is_empty() {
list.remove(0);
bgstatus_count.fetch_sub(1, Ordering::Relaxed);
}
}
list.push(bgstatus {
pid,
status: status_val,
}); bgstatus_count.fetch_add(1, Ordering::Relaxed); }
}
pub fn bin_fg(
name: &str,
argv: &[String], ops: &options,
func: i32,
) -> i32 {
let _ofunc = func;
if OPT_ISSET(ops, b'Z') {
if argv.is_empty() || argv.len() > 1 {
zwarnnam(name, "-Z requires one argument"); return 1; }
queue_signals(); let title = &argv[0];
#[cfg(target_os = "linux")]
unsafe {
let cs = std::ffi::CString::new(title.as_str()).unwrap_or_default();
libc::prctl(
15,
cs.as_ptr() as libc::c_ulong,
0,
0,
0,
); }
#[cfg(target_os = "macos")]
unsafe {
extern "C" {
fn pthread_setname_np(name: *const libc::c_char) -> libc::c_int;
}
let cs = std::ffi::CString::new(title.as_str()).unwrap_or_default();
pthread_setname_np(cs.as_ptr());
}
#[cfg(not(any(target_os = "linux", target_os = "macos")))]
{
let _ = title;
}
unqueue_signals(); return 0; }
let mut lng = 0i32; if func == BIN_JOBS {
lng = if OPT_ISSET(ops, b'l') {
1
}
else if OPT_ISSET(ops, b'p') {
2
} else {
0
};
if OPT_ISSET(ops, b'd') {
lng |= 4;
} } else {
lng = if isset(LONGLISTJOBS) {
1
} else {
0
};
}
let _ = lng;
let jobbing = isset(MONITOR);
if (func == BIN_FG || func == BIN_BG) && !jobbing {
zwarnnam(name, "no job control in this shell."); return 1; }
queue_signals();
wait_for_processes();
if !crate::ported::zsh_h::isset(crate::ported::zsh_h::NOTIFY) {
if let Some(jt) = JOBTAB.get() {
let mut guard = jt.lock().unwrap();
let long_list = crate::ported::zsh_h::isset(
crate::ported::zsh_h::LONGLISTJOBS,
);
for i in 1..guard.len() {
if (guard[i].stat & crate::ported::zsh_h::STAT_CHANGED) != 0 {
let s = printjob(&guard[i], i, long_list, None, None);
if !s.is_empty() {
eprint!("{}", s);
}
}
}
}
}
let table = JOBTAB.get_or_init(|| Mutex::new(Vec::new()));
if func != BIN_JOBS || jobbing || *OLDMAXJOB.get_or_init(|| Mutex::new(0)).lock().unwrap() == 0
{
setcurjob();
}
if func == BIN_JOBS {
STOPMSG.store(2, Ordering::Relaxed);
}
let mut returnval: i32 = 0;
if argv.is_empty() {
if func == BIN_JOBS {
let curjob = *CURJOB.get_or_init(|| Mutex::new(-1)).lock().unwrap();
let t = table.lock().expect("jobtab poisoned");
let curmaxjob = t.len();
let r_only = OPT_ISSET(ops, b'r');
let s_only = OPT_ISSET(ops, b's');
for job in 0..curmaxjob {
if job as i32 == curjob {
continue;
}
let j = &t[job];
if !j.is_inuse() {
continue;
}
let stopped = j.is_stopped();
if (!r_only && !s_only)
|| (r_only && s_only)
|| (r_only && !stopped)
|| (s_only && stopped)
{
let curjob_opt = if curjob >= 0 {
Some(curjob as usize)
} else {
None
};
let prevjob = *PREVJOB.get_or_init(|| Mutex::new(-1)).lock().unwrap();
let prevjob_opt = if prevjob >= 0 {
Some(prevjob as usize)
} else {
None
};
print!(
"{}",
printjob(j, job, (lng & 1) != 0, curjob_opt, prevjob_opt)
);
}
}
unqueue_signals(); return 0; }
if func == BIN_FG || func == BIN_BG || func == BIN_DISOWN {
let curjob = *CURJOB.get_or_init(|| Mutex::new(-1)).lock().unwrap();
if curjob < 0 {
zwarnnam(name, "no current job"); unqueue_signals();
return 1; }
if func == BIN_DISOWN {
unqueue_signals();
return 0;
}
if curjob >= 0 {
let _ = killjb(curjob as usize, libc::SIGCONT);
}
unqueue_signals();
return 0;
}
unqueue_signals();
return 0;
}
for arg in argv {
let p = if arg.starts_with('%') {
getjob(arg, name) } else if let Ok(n) = arg.parse::<i32>() {
if n >= 0 {
n
} else {
-1
}
} else {
zwarnnam(name, &format!("{}: no such job", arg));
returnval = 1;
continue;
};
if p < 0 {
returnval = 1;
continue;
}
if func == BIN_FG || func == BIN_BG {
if killjb(p as usize, libc::SIGCONT) == -1 {
zwarnnam(
name,
&format!("{}: kill failed: {}", arg, std::io::Error::last_os_error()),
);
returnval = 1;
}
} else if func == BIN_WAIT {
if !arg.starts_with('%') {
if let Ok(pid) = arg.parse::<i32>() {
let mut status: libc::c_int = 0;
let r = unsafe { libc::waitpid(pid, &mut status, 0) };
if r == -1 {
let err = std::io::Error::last_os_error();
if err.raw_os_error() == Some(libc::ECHILD) {
zwarnnam(name, &format!("pid {} is not a child of this shell", pid));
returnval = 127;
} else {
returnval = 1;
}
} else if libc::WIFEXITED(status) {
returnval = libc::WEXITSTATUS(status);
} else if libc::WIFSIGNALED(status) {
returnval = 128 + libc::WTERMSIG(status);
}
}
}
} else if func == BIN_JOBS {
let t = table.lock().expect("jobtab poisoned");
if let Some(j) = t.get(p as usize) {
let curjob = *CURJOB.get_or_init(|| Mutex::new(-1)).lock().unwrap();
let prevjob = *PREVJOB.get_or_init(|| Mutex::new(-1)).lock().unwrap();
print!(
"{}",
printjob(
j,
p as usize,
(lng & 1) != 0,
if curjob >= 0 {
Some(curjob as usize)
} else {
None
},
if prevjob >= 0 {
Some(prevjob as usize)
} else {
None
}
)
);
}
}
}
unqueue_signals(); returnval }
pub fn bin_kill(
nam: &str,
argv: &[String], _ops: &options,
_func: i32,
) -> i32 {
let mut sig: i32 = libc::SIGTERM; let mut returnval: i32 = 0; let mut got_sig = false; let mut idx = 0usize;
while idx < argv.len() && argv[idx].starts_with('-') {
let arg = argv[idx].clone();
let body = &arg[1..];
if body == "-" {
idx += 1;
break;
}
if got_sig {
break; }
if body.chars().next().is_some_and(|c| c.is_ascii_digit()) {
match body.parse::<i32>() {
Ok(n) => sig = n, Err(_) => {
zwarnnam(nam, &format!("invalid signal number: -{}", body));
return 1; }
}
got_sig = true;
idx += 1;
continue;
}
if body == "l" {
idx += 1;
if idx < argv.len() {
while idx < argv.len() {
let token = &argv[idx];
idx += 1;
if let Ok(n) = token.parse::<i32>() {
let s = (n & !0o200) as i32; if let Some(name) = sigs_name(s) {
println!("{}", name);
} else {
println!("{}", n); }
} else {
let upper = token.to_ascii_uppercase();
let bare = upper.strip_prefix("SIG").unwrap_or(&upper);
if let Some(n) = sigs_number(bare) {
println!("{}", n); } else {
zwarnnam(nam, &format!("unknown signal: SIG{}", bare)); returnval += 1;
}
}
}
return returnval; }
print!(
"{}",
sigs_name(1).unwrap_or("HUP")
);
for s in 2..=crate::ported::signals_h::SIGCOUNT {
if let Some(n) = sigs_name(s) {
print!(" {}", n);
}
}
println!();
return 0; }
if body == "L" {
let cols = 4usize;
let mut col = 0usize;
for s in 1..=crate::ported::signals_h::SIGCOUNT {
if let Some(n) = sigs_name(s) {
print!("{:>2} {:<10}", s, n);
col += 1;
if col % cols == 0 {
println!();
} else {
print!(" ");
}
}
}
if col % cols != 0 {
println!();
}
return 0; }
if body == "n" {
idx += 1;
if idx >= argv.len() {
zwarnnam(nam, "-n: argument expected"); return 1; }
match argv[idx].parse::<i32>() {
Ok(n) => {
sig = n;
}
Err(_) => {
zwarnnam(nam, &format!("invalid signal number: {}", argv[idx])); return 1;
}
}
got_sig = true;
idx += 1;
continue;
}
if body == "s" {
idx += 1;
if idx >= argv.len() {
zwarnnam(nam, "-s: argument expected"); return 1;
}
let name = argv[idx].as_str();
let upper = name.to_ascii_uppercase();
let bare = upper.strip_prefix("SIG").unwrap_or(&upper);
match sigs_number(bare) {
Some(n) => sig = n,
None => {
zwarnnam(nam, &format!("unknown signal: SIG{}", bare)); return 1;
}
}
got_sig = true;
idx += 1;
continue;
}
if body == "q" {
idx += 1;
if idx >= argv.len() {
zwarnnam(nam, "-q: argument expected"); return 1;
}
if argv[idx].parse::<i32>().is_err() {
zwarnnam(nam, &format!("invalid number: {}", argv[idx])); return 1;
}
idx += 1; continue; }
let upper = body.to_ascii_uppercase();
let bare = upper.strip_prefix("SIG").unwrap_or(&upper);
match sigs_number(bare) {
Some(n) => {
sig = n;
got_sig = true;
idx += 1;
}
None => {
zwarnnam(nam, &format!("unknown signal: SIG{}", bare)); return 1;
}
}
}
if idx >= argv.len() {
zwarnnam(nam, "not enough arguments"); return 1;
}
for arg in &argv[idx..] {
if let Some(num) = arg.strip_prefix('-') {
match num.parse::<i32>() {
Ok(pgid) => {
let r = unsafe { libc::killpg(pgid, sig) }; if r != 0 {
zwarnnam(
nam,
&format!("kill {}: {}", arg, std::io::Error::last_os_error()),
);
returnval = 1;
}
}
Err(_) => {
zwarnnam(nam, &format!("illegal pid: {}", arg));
returnval = 1;
}
}
} else if arg.starts_with('%') {
let p = getjob(arg, nam);
if p < 0 {
returnval += 1; continue;
}
if killjb(p as usize, sig) == -1 {
zwarnnam(
"kill",
&format!(
"kill {} failed: {}",
arg, std::io::Error::last_os_error()
),
);
returnval += 1; continue;
}
let stopped = JOBTAB
.get_or_init(|| Mutex::new(Vec::new()))
.lock()
.expect("jobtab poisoned")
.get(p as usize)
.map(|j| j.is_stopped())
.unwrap_or(false);
if stopped
&& sig != libc::SIGKILL
&& sig != libc::SIGCONT
&& sig != libc::SIGTSTP
&& sig != libc::SIGTTOU
&& sig != libc::SIGTTIN
&& sig != libc::SIGSTOP
{
let _ = killjb(p as usize, libc::SIGCONT); }
} else {
match arg.parse::<i32>() {
Ok(pid) => {
let r = unsafe { libc::kill(pid, sig) }; if r != 0 {
zwarnnam(
nam,
&format!("kill {}: {}", arg, std::io::Error::last_os_error()),
); returnval = 1;
}
}
Err(_) => {
zwarnnam(nam, &format!("illegal pid: {}", arg));
returnval = 1;
}
}
}
}
returnval }
pub fn sig_names_for_signals_param() -> Vec<String> {
let mut out: Vec<String> = Vec::with_capacity(
crate::ported::signals_h::SIGCOUNT as usize + 1,
);
if let Some(n) = crate::ported::signals_h::sigs_name(0) {
out.push(n.to_string());
}
for s in 1..=crate::ported::signals_h::SIGCOUNT {
if let Some(n) = crate::ported::signals_h::sigs_name(s) {
out.push(n.to_string());
}
}
if let Some(n) = crate::ported::signals_h::sigs_name(
crate::ported::signals_h::SIGZERR,
) {
out.push(n.to_string());
}
if let Some(n) = crate::ported::signals_h::sigs_name(
crate::ported::signals_h::SIGDEBUG,
) {
out.push(n.to_string());
}
out
}
pub fn getsigidx(s: &str) -> Option<i32> {
if let Some(first) = s.chars().next() {
if first.is_ascii_digit() {
if let Ok(x) = s.parse::<i32>() {
let vsig = crate::ported::signals_h::VSIGCOUNT;
if x >= 0 && x < vsig {
return Some(x); }
#[cfg(target_os = "linux")]
{
let sigrtmin = libc::SIGRTMIN();
let sigrtmax = libc::SIGRTMAX();
if x >= sigrtmin && x <= sigrtmax {
return Some(crate::ported::signals_h::SIGIDX(x)); }
}
return None;
}
}
}
let s = s.strip_prefix("SIG").unwrap_or(s);
match s.to_uppercase().as_str() {
"EXIT" => Some(0),
"ZERR" | "ERR" => Some(crate::ported::signals_h::SIGZERR),
"DEBUG" => Some(crate::ported::signals_h::SIGDEBUG),
"HUP" => Some(libc::SIGHUP),
"INT" => Some(libc::SIGINT),
"QUIT" => Some(libc::SIGQUIT),
"ILL" => Some(libc::SIGILL),
"TRAP" => Some(libc::SIGTRAP),
"ABRT" | "IOT" => Some(libc::SIGABRT),
"BUS" => Some(libc::SIGBUS),
"FPE" => Some(libc::SIGFPE),
"KILL" => Some(libc::SIGKILL),
"USR1" => Some(libc::SIGUSR1),
"SEGV" => Some(libc::SIGSEGV),
"USR2" => Some(libc::SIGUSR2),
"PIPE" => Some(libc::SIGPIPE),
"ALRM" => Some(libc::SIGALRM),
"TERM" => Some(libc::SIGTERM),
"CHLD" | "CLD" => Some(libc::SIGCHLD),
"CONT" => Some(libc::SIGCONT),
"STOP" => Some(libc::SIGSTOP),
"TSTP" => Some(libc::SIGTSTP),
"TTIN" => Some(libc::SIGTTIN),
"TTOU" => Some(libc::SIGTTOU),
"URG" => Some(libc::SIGURG),
"XCPU" => Some(libc::SIGXCPU),
"XFSZ" => Some(libc::SIGXFSZ),
"VTALRM" => Some(libc::SIGVTALRM),
"PROF" => Some(libc::SIGPROF),
"WINCH" => Some(libc::SIGWINCH),
"IO" | "POLL" => Some(libc::SIGIO),
"SYS" => Some(libc::SIGSYS),
_ => {
#[cfg(target_os = "linux")]
{
if let Some(signum) = crate::ported::signals::rtsigno(s) {
return Some(crate::ported::signals_h::SIGIDX(signum)); }
}
None }
}
}
pub fn getsigname(sig: i32) -> String {
match sig {
0 => "EXIT".to_string(),
libc::SIGHUP => "HUP".to_string(),
libc::SIGINT => "INT".to_string(),
libc::SIGQUIT => "QUIT".to_string(),
libc::SIGILL => "ILL".to_string(),
libc::SIGTRAP => "TRAP".to_string(),
libc::SIGABRT => "ABRT".to_string(),
libc::SIGBUS => "BUS".to_string(),
libc::SIGFPE => "FPE".to_string(),
libc::SIGKILL => "KILL".to_string(),
libc::SIGUSR1 => "USR1".to_string(),
libc::SIGSEGV => "SEGV".to_string(),
libc::SIGUSR2 => "USR2".to_string(),
libc::SIGPIPE => "PIPE".to_string(),
libc::SIGALRM => "ALRM".to_string(),
libc::SIGTERM => "TERM".to_string(),
libc::SIGCHLD => "CHLD".to_string(),
libc::SIGCONT => "CONT".to_string(),
libc::SIGSTOP => "STOP".to_string(),
libc::SIGTSTP => "TSTP".to_string(),
libc::SIGTTIN => "TTIN".to_string(),
libc::SIGTTOU => "TTOU".to_string(),
libc::SIGURG => "URG".to_string(),
libc::SIGXCPU => "XCPU".to_string(),
libc::SIGXFSZ => "XFSZ".to_string(),
libc::SIGVTALRM => "VTALRM".to_string(),
libc::SIGPROF => "PROF".to_string(),
libc::SIGWINCH => "WINCH".to_string(),
libc::SIGIO => "IO".to_string(),
libc::SIGSYS => "SYS".to_string(),
_ => {
#[cfg(target_os = "linux")]
{
let sigrtmin = libc::SIGRTMIN();
let sigrtmax = libc::SIGRTMAX();
if sig >= sigrtmin && sig <= sigrtmax {
let nm = crate::ported::signals::rtsigname(sig); if !nm.is_empty() {
return nm;
}
}
}
format!("SIG{}", sig)
}
}
}
pub fn gettrapnode(sig: i32, ignoredisable: bool) -> Option<String> {
let tab = crate::ported::hashtable::shfunctab_lock()
.read()
.expect("shfunctab poisoned");
let getptr = |name: &str| -> Option<String> {
let hit = if ignoredisable {
tab.get_including_disabled(name) } else {
tab.get(name) };
hit.map(|f| f.node.nam.clone())
};
let fname = format!("TRAP{}", getsigname(sig));
if let Some(n) = getptr(&fname) {
return Some(n);
}
for (alt_name, alt_num) in crate::ported::signals_h::ALT_SIGS.iter() {
if *alt_num == sig {
let fname = format!("TRAP{}", alt_name);
if let Some(n) = getptr(&fname) {
return Some(n);
}
}
}
None
}
pub fn removetrapnode(sig: i32) {
let name = format!("TRAP{}", getsigname(sig));
crate::ported::hashtable::removeshfuncnode(&name);
}
pub fn bin_suspend(
name: &str,
_argv: &[String], ops: &options,
_func: i32,
) -> i32 {
let islogin = getsparam("0")
.map(|s| s.starts_with('-'))
.unwrap_or(false);
if islogin && !OPT_ISSET(ops, b'f') {
zwarnnam(name, "can't suspend login shell"); return 1; }
let jobbing = isset(MONITOR);
if jobbing {
signal_default(libc::SIGTTIN); signal_default(libc::SIGTSTP); signal_default(libc::SIGTTOU); release_pgrp(); }
let origpgrp = ORIGPGRP
.get_or_init(|| Mutex::new(0))
.lock()
.map(|g| *g)
.unwrap_or(0);
unsafe {
libc::killpg(origpgrp, libc::SIGTSTP);
}
if jobbing {
let _ = acquire_pgrp(); signal_ignore(libc::SIGTTOU); signal_ignore(libc::SIGTSTP); signal_ignore(libc::SIGTTIN); }
0 }
pub(crate) fn findjobnam(s: &str, jobtab: &[job], maxjob: i32, thisjob: i32) -> Option<i32> {
let mut jobnum = maxjob; while jobnum >= 0 {
let ju = jobnum as usize;
if ju < jobtab.len()
&& jobtab[ju].stat != 0 && (jobtab[ju].stat & stat::SUBJOB) == 0 && jobnum != thisjob
{
if let Some(first_proc) = jobtab[ju].procs.first() {
if first_proc.text.starts_with(s) {
return Some(jobnum); }
}
}
jobnum -= 1;
}
None }
#[cfg(unix)]
pub fn acquire_pgrp() -> bool {
let mypid = unsafe { libc::getpid() };
let mut mypgrp = unsafe { libc::getpgrp() }; if mypgrp < 0 {
opt_state_set("monitor", false); return false;
}
let mut lastpgrp = mypgrp; let mut blockset: libc::sigset_t = unsafe { std::mem::zeroed() };
unsafe {
libc::sigemptyset(&mut blockset);
libc::sigaddset(&mut blockset, libc::SIGTTIN); libc::sigaddset(&mut blockset, libc::SIGTTOU); libc::sigaddset(&mut blockset, libc::SIGTSTP); }
let oldset = signal_block(&blockset); let mut loop_count = 0i32; let interact = isset(INTERACTIVE);
loop {
let ttpgrp = unsafe { libc::tcgetpgrp(0) }; if ttpgrp == -1 || ttpgrp == mypgrp {
break;
}
mypgrp = unsafe { libc::getpgrp() }; if mypgrp == mypid {
if !interact {
break;
} signal_setmask(&oldset); unsafe {
libc::tcsetpgrp(0, mypgrp);
} signal_block(&blockset); }
if mypgrp == unsafe { libc::tcgetpgrp(0) } {
break;
} signal_setmask(&oldset); let mut buf: [u8; 0] = [];
let _ = unsafe { libc::read(0, buf.as_mut_ptr() as *mut _, 0) }; signal_block(&blockset); mypgrp = unsafe { libc::getpgrp() }; if mypgrp == lastpgrp {
if !interact {
break;
} loop_count += 1;
if loop_count == 100 {
break; }
}
lastpgrp = mypgrp; }
let mut acquired = mypgrp == mypid; if !acquired {
if unsafe { libc::setpgid(0, 0) } == 0 {
mypgrp = mypid; unsafe {
libc::tcsetpgrp(0, mypgrp);
} acquired = true;
} else {
opt_state_set("monitor", false); }
}
signal_setmask(&oldset); acquired }
#[cfg(unix)]
pub fn release_pgrp() {
let origpgrp = *ORIGPGRP
.get_or_init(|| Mutex::new(0))
.lock()
.expect("origpgrp poisoned");
let mypgrp = *MYPGRP
.get_or_init(|| Mutex::new(0))
.lock()
.expect("mypgrp poisoned");
if origpgrp != mypgrp {
if origpgrp != 0 {
unsafe {
libc::tcsetpgrp(0, origpgrp);
libc::setpgid(0, origpgrp); }
}
*MYPGRP
.get_or_init(|| Mutex::new(0)) .lock()
.expect("mypgrp poisoned") = origpgrp;
}
}
pub static ORIGPGRP: OnceLock<Mutex<i32>> = OnceLock::new();
pub static MYPGRP: OnceLock<Mutex<i32>> = OnceLock::new();
pub static LAST_ATTACHED_PGRP: OnceLock<Mutex<i32>> = OnceLock::new();
pub static THISJOB: OnceLock<Mutex<i32>> = OnceLock::new();
pub static CURJOB: OnceLock<Mutex<i32>> = OnceLock::new();
pub static PREVJOB: OnceLock<Mutex<i32>> = OnceLock::new();
pub static JOBTAB: OnceLock<Mutex<Vec<job>>> = OnceLock::new();
pub static JOBTABSIZE: OnceLock<Mutex<usize>> = OnceLock::new();
pub static MAXJOB: OnceLock<Mutex<usize>> = OnceLock::new();
static OLDJOBTAB: OnceLock<Mutex<Vec<job>>> = OnceLock::new();
static OLDMAXJOB: OnceLock<Mutex<usize>> = OnceLock::new();
pub static TTYFROZEN: OnceLock<Mutex<i32>> = OnceLock::new();
pub static NUMPIPESTATS: OnceLock<Mutex<usize>> = OnceLock::new();
pub static PIPESTATS: OnceLock<Mutex<[i32; MAX_PIPESTATS]>> = OnceLock::new();
pub const DEFAULT_TIMEFMT: &str = "%J %U user %S system %P cpu %*E total";
pub fn waitonejob(jn: &mut job) {
if !jn.procs.is_empty() || !jn.auxprocs.is_empty() {
zwaitjob(jn, 0);
} else {
deletejob(jn, false);
let lastval = crate::ported::builtin::LASTVAL
.load(std::sync::atomic::Ordering::Relaxed);
let p = PIPESTATS.get_or_init(|| Mutex::new([0; MAX_PIPESTATS]));
if let Ok(mut pguard) = p.lock() {
pguard[0] = lastval; }
let n = NUMPIPESTATS.get_or_init(|| Mutex::new(0));
if let Ok(mut nguard) = n.lock() {
*nguard = 1; }
crate::ported::params::setaparam(
"pipestatus",
vec![lastval.to_string()],
);
}
}
pub fn getbgstatus(pid: i32) -> Option<i32> {
if let Ok(mut list) = bgstatus_list.lock() {
if let Some(idx) = list.iter().position(|b| b.pid == pid) {
let status = list[idx].status;
list.remove(idx); bgstatus_count.fetch_sub(1, Ordering::Relaxed);
return Some(status);
}
}
None
}
#[cfg(test)]
mod tests {
use crate::ported::zsh_h::{STAT_BUILTIN, STAT_CHANGED, STAT_DONE, STAT_STOPPED, STAT_TIMED};
use super::*;
#[test]
fn printtime_emits_rusage_directives() {
let _g = crate::test_util::global_state_lock();
let ti = timeinfo {
ut: 500_000,
st: 250_000,
maxrss: 4096,
majflt: 12,
minflt: 345,
nswap: 0,
ixrss: 0,
idrss: 0,
isrss: 0,
inblock: 7,
oublock: 3,
nvcsw: 99,
nivcsw: 11,
msgsnd: 0,
msgrcv: 0,
nsignals: 0,
};
let s = printtime(1.0, &ti, "%M/%F/%R/%I/%O/%c/%w", "my-job");
assert_eq!(s, "4096/12/345/7/3/11/99");
}
#[test]
fn printtime_percent_directive() {
let _g = crate::test_util::global_state_lock();
let ti = timeinfo {
ut: 600_000,
st: 400_000,
..Default::default()
};
let s = printtime(2.0, &ti, "%P", "j");
assert_eq!(s, "50%");
}
#[test]
fn printtime_percent_zero_elapsed_no_panic() {
let _g = crate::test_util::global_state_lock();
let ti = timeinfo::default();
let result = std::panic::catch_unwind(|| printtime(0.0, &ti, "%P", "j"));
let s = result.expect("c:614 — zero elapsed must NOT panic");
assert_eq!(
s, "0%",
"c:615-618 — zero-elapsed percent must yield 0%, not NaN/Inf"
);
}
#[test]
fn printtime_percent_truncates_toward_zero() {
let _g = crate::test_util::global_state_lock();
let ti = timeinfo {
ut: 996_000,
st: 0,
..Default::default()
};
let s = printtime(1.0, &ti, "%P", "j");
assert_eq!(
s, "99%",
"c:893 — `(int)` cast truncates 99.6 → 99, not rounds to 100"
);
}
#[test]
fn printtime_jobname_directive() {
let _g = crate::test_util::global_state_lock();
let ti = timeinfo::default();
let s = printtime(0.0, &ti, "[%J]", "my command");
assert_eq!(s, "[my command]");
}
#[test]
fn printtime_time_directives() {
let _g = crate::test_util::global_state_lock();
let ti = timeinfo {
ut: 1_500_000,
st: 500_000,
..Default::default()
};
let s = printtime(2.5, &ti, "%E %U %S", "j");
assert_eq!(s, "2.50s 1.50s 0.50s");
}
#[test]
fn printtime_star_directive_routes_to_hhmmss() {
let _g = crate::test_util::global_state_lock();
let ti = timeinfo::default();
let s = printtime(75.0, &ti, "%*E", "j");
assert_eq!(
s, "1:15.00",
"c:876-880 — %*E must route to printhhmmss for elapsed >= 60s"
);
let s_hr = printtime(3725.0, &ti, "%*E", "j");
assert_eq!(
s_hr, "1:02:05.00",
"c:880 + printhhmmss c:815-816 — elapsed >= 3600s yields H:MM:SS"
);
}
#[test]
fn should_report_time_uses_reportmemory() {
let _g = crate::test_util::global_state_lock();
setsparam("REPORTMEMORY", "100");
let mut job = job::default();
let mut proc = process::new(123);
proc.ti.maxrss = 256; proc.bgtime = Some(Instant::now());
proc.endtime = Some(Instant::now());
job.procs.push(proc);
job.stat = stat::INUSE;
assert!(should_report_time(&job, -1.0));
unsetparam("REPORTMEMORY");
}
#[test]
fn should_report_time_no_thresholds_false() {
let _g = crate::test_util::global_state_lock();
unsetparam("REPORTMEMORY");
let mut job = job::default();
job.procs.push(process::new(1));
assert!(!should_report_time(&job, -1.0));
}
#[test]
fn should_report_time_stat_timed_overrides_all_gates() {
let _g = crate::test_util::global_state_lock();
unsetparam("REPORTMEMORY");
zleactive.store(1, Ordering::SeqCst);
let mut job = job::default();
job.stat = stat::INUSE | stat::TIMED;
job.procs.push(process::new(9001));
let reported = should_report_time(&job, -1.0);
zleactive.store(0, Ordering::SeqCst);
assert!(
reported,
"c:1052-1053 — STAT_TIMED MUST short-circuit to true regardless of threshold/zleactive"
);
}
#[test]
fn dumptime_emits_one_line_per_process() {
let _g = crate::test_util::global_state_lock();
setsparam("TIMEFMT", "%J");
let mut job = job::default();
let now = Instant::now();
for (i, text) in ["echo a", "grep b", "tee c"].iter().enumerate() {
let mut p = process::new(1000 + i as i32);
p.bgtime = Some(now);
p.endtime = Some(now + Duration::from_millis(10));
p.text = text.to_string();
job.procs.push(p);
}
let out = dumptime(&job).expect("expected timing output");
assert_eq!(out, "echo a\ngrep b\ntee c");
unsetparam("TIMEFMT");
}
#[test]
fn handle_sub_clears_superjob_sets_wassuper_on_done() {
let _g = crate::test_util::global_state_lock();
let mut tab = vec![job::default(), job::default()];
tab[0].stat = stat::INUSE | stat::SUPERJOB;
tab[0].other = 1;
tab[0].gleader = unsafe { libc::getpgrp() };
let mut p = process::new(unsafe { libc::getpid() });
p.status = 0; tab[0].procs.push(p);
tab[1].stat = stat::INUSE | stat::DONE;
tab[1].other = unsafe { libc::getpid() };
handle_sub(&mut tab, 0, false);
assert_eq!(tab[0].stat & stat::SUPERJOB, 0, "SUPERJOB cleared");
assert!(tab[0].stat & stat::WASSUPER != 0, "WASSUPER set");
}
#[test]
fn update_job_done_writes_lastval2() {
let _g = crate::test_util::global_state_lock();
LASTVAL2.store(-1, Ordering::SeqCst);
let mut job = job::default();
let mut p1 = process::new(1001);
p1.status = 0; let mut p2 = process::new(1002);
p2.status = 7 << 8; job.procs.push(p1);
job.procs.push(p2);
let committed = update_job(&mut job);
assert!(committed, "update_job should commit when all done");
assert!(job.stat & stat::DONE != 0);
assert!(job.stat & stat::CHANGED != 0);
assert_eq!(
LASTVAL2.load(Ordering::SeqCst),
7,
"lastval2 = WEXITSTATUS of last proc"
);
}
#[test]
fn update_job_running_returns_false() {
let _g = crate::test_util::global_state_lock();
let mut job = job::default();
let mut p = process::new(2001);
p.status = SP_RUNNING;
job.procs.push(p);
assert!(!update_job(&mut job));
assert_eq!(job.stat & stat::DONE, 0);
}
#[test]
fn update_job_running_auxproc_short_circuits_before_main_walk() {
let _g = crate::test_util::global_state_lock();
let mut job = job::default();
let mut main = process::new(10001);
main.status = 0; job.procs.push(main);
let mut aux = process::new(10002);
aux.status = SP_RUNNING;
job.auxprocs.push(aux);
let committed = update_job(&mut job);
assert!(
!committed,
"c:472-473 — running auxproc must short-circuit even when main procs are done"
);
assert_eq!(
job.stat & stat::DONE,
0,
"STAT_DONE must not be set when an auxproc is still running"
);
assert_eq!(
job.stat & stat::CHANGED,
0,
"STAT_CHANGED must not be set on early-return"
);
}
#[test]
fn update_job_stopped_sets_stopped_changed() {
let _g = crate::test_util::global_state_lock();
let mut job = job::default();
let mut p = process::new(3001);
p.status = 0x117f; job.procs.push(p);
let committed = update_job(&mut job);
assert!(committed);
assert!(job.stat & stat::STOPPED != 0);
assert!(job.stat & stat::CHANGED != 0);
assert_eq!(job.stat & stat::DONE, 0);
}
#[test]
fn spawnjob_no_thisjob_is_noop() {
let _g = crate::test_util::global_state_lock();
*THISJOB.get_or_init(|| Mutex::new(-1)).lock().unwrap() = -1;
spawnjob();
assert_eq!(*THISJOB.get().unwrap().lock().unwrap(), -1);
}
#[test]
fn spawnjob_deletes_empty_job() {
let _g = crate::test_util::global_state_lock();
let mut tab_init = vec![job::default(); 3];
tab_init[1].stat = stat::INUSE;
*JOBTAB
.get_or_init(|| Mutex::new(Vec::new()))
.lock()
.unwrap() = tab_init;
*THISJOB.get_or_init(|| Mutex::new(-1)).lock().unwrap() = 1;
spawnjob();
assert_eq!(*THISJOB.get().unwrap().lock().unwrap(), -1);
let tab = JOBTAB.get().unwrap().lock().unwrap();
assert_eq!(tab[1].stat & stat::INUSE, 0);
}
#[test]
fn handle_sub_stopped_branch_propagates() {
let _g = crate::test_util::global_state_lock();
let mut tab = vec![job::default(), job::default()];
tab[0].stat = stat::INUSE | stat::SUPERJOB;
tab[0].other = 1;
let mut p = process::new(1234);
p.status = SP_RUNNING;
tab[0].procs.push(p);
tab[1].stat = stat::INUSE | stat::STOPPED;
let mut sp = process::new(5678);
sp.status = 0x117f; tab[1].procs.push(sp);
let ret = handle_sub(&mut tab, 0, false);
assert_eq!(ret, 1, "STOPPED branch returns 1");
assert!(tab[0].stat & stat::STOPPED != 0, "super inherits STOPPED");
assert_eq!(tab[0].procs[0].status, 0x117f);
}
#[test]
fn dumptime_empty_job_returns_none() {
let _g = crate::test_util::global_state_lock();
let job = job::default();
assert!(dumptime(&job).is_none());
}
#[test]
fn dumptime_skips_proc_without_endtime() {
let _g = crate::test_util::global_state_lock();
setsparam("TIMEFMT", "%E");
let mut job = job::default();
let mut p = process::new(11001);
p.bgtime = Some(Instant::now());
p.endtime = None; p.text = "incomplete".to_string();
job.procs.push(p);
let result = std::panic::catch_unwind(|| dumptime(&job));
let out = result.expect("missing endtime must not panic");
assert!(
out.is_none(),
"filter_map drops procs without bg/end pair → empty result → None"
);
unsetparam("TIMEFMT");
}
#[test]
fn dumptime_uses_per_process_elapsed() {
let _g = crate::test_util::global_state_lock();
setsparam("TIMEFMT", "%E");
let mut job = job::default();
let t0 = Instant::now();
for (i, ms) in [100u64, 300, 600].iter().enumerate() {
let mut p = process::new(8000 + i as i32);
p.bgtime = Some(t0);
p.endtime = Some(t0 + Duration::from_millis(*ms));
p.text = format!("p{}", i);
job.procs.push(p);
}
let out = dumptime(&job).expect("non-empty job → Some");
let lines: Vec<&str> = out.lines().collect();
assert_eq!(
lines.len(),
3,
"must produce one line per proc, got {:?}",
lines
);
let unique: std::collections::HashSet<&&str> = lines.iter().collect();
assert_eq!(
unique.len(),
3,
"each line must carry its own proc's elapsed; got duplicates: {:?}",
lines
);
unsetparam("TIMEFMT");
}
#[test]
fn printjob_appends_timing_when_stat_timed() {
let _g = crate::test_util::global_state_lock();
setsparam("TIMEFMT", "%J");
let mut job = job::default();
job.stat = stat::INUSE | stat::TIMED | stat::DONE;
let mut p = process::new(42);
p.bgtime = Some(Instant::now());
p.endtime = Some(Instant::now() + Duration::from_millis(5));
p.text = "echo hi".to_string();
p.status = 0; job.procs.push(p);
let out = printjob(&job, 1, false, Some(1), None);
assert!(
out.contains("echo hi"),
"expected status line; got: {:?}",
out
);
assert!(
out.ends_with("echo hi"),
"expected timing line at end; got: {:?}",
out
);
unsetparam("TIMEFMT");
}
#[test]
fn update_job_subjob_stop_sets_flags_before_early_return() {
let _g = crate::test_util::global_state_lock();
let mut job = job::default();
job.stat = stat::INUSE | stat::SUBJOB; let mut p = process::new(7001);
p.status = 0x117f; job.procs.push(p);
assert!(update_job(&mut job));
assert!(
job.stat & stat::CHANGED != 0,
"c:514 — SUBJOB stop must set CHANGED so the jobs scanner picks it up"
);
assert!(
job.stat & stat::STOPPED != 0,
"c:514 — SUBJOB stop must mark STOPPED"
);
assert_eq!(
job.stat & stat::SUBJOB,
stat::SUBJOB,
"SUBJOB flag preserved through update"
);
}
#[test]
fn update_job_already_stopped_short_circuits() {
let _g = crate::test_util::global_state_lock();
let mut job = job::default();
job.stat = stat::INUSE | stat::STOPPED; let mut p = process::new(12001);
p.status = 0x117f; job.procs.push(p);
let stat_before = job.stat;
let committed = update_job(&mut job);
assert!(committed, "early-return path still reports 'commit'");
assert_eq!(
job.stat, stat_before,
"c:541-542 — re-entry on already-STOPPED job must not flip flags"
);
}
#[test]
fn update_job_last_proc_signaled_sets_high_bit_val() {
let _g = crate::test_util::global_state_lock();
LASTVAL2.store(-1, Ordering::SeqCst);
let mut job = job::default();
let mut p1 = process::new(6001);
p1.status = 0; let mut p2 = process::new(6002);
p2.status = 15; job.procs.push(p1);
job.procs.push(p2);
assert!(update_job(&mut job));
let lv2 = LASTVAL2.load(Ordering::SeqCst);
assert_eq!(
lv2 & 0o200,
0o200,
"c:489-490 — WIFSIGNALED last-proc must set the 0o200 high bit"
);
assert_eq!(
lv2 & 0x7f,
15,
"c:490 — low 7 bits must hold WTERMSIG (SIGTERM=15)"
);
}
#[test]
fn test_process_new() {
let _g = crate::test_util::global_state_lock();
let proc = process::new(1234);
assert_eq!(proc.pid, 1234);
assert!(proc.is_running());
}
#[test]
fn test_job_new() {
let _g = crate::test_util::global_state_lock();
let job = job::new();
assert_eq!(job.stat, 0);
assert!(!job.is_done());
assert!(!job.is_stopped());
}
#[test]
fn test_job_make_running() {
let _g = crate::test_util::global_state_lock();
let mut job = job::new();
job.stat |= stat::STOPPED;
job.procs.push(process {
status: 0x007f,
..process::new(1234)
});
job.make_running();
assert!(!job.is_stopped());
assert!(job.procs[0].is_running());
}
#[test]
fn test_format_job() {
let _g = crate::test_util::global_state_lock();
let mut job = job::new();
job.text = "vim file.txt".to_string();
job.stat |= stat::STOPPED;
let formatted = printjob(&job, 1, false, Some(1), None);
assert!(formatted.contains("[1]"));
assert!(formatted.contains("+"));
assert!(formatted.contains("suspended") || formatted.contains("Stopped"));
assert!(formatted.contains("vim file.txt"));
}
#[test]
fn test_isanum_handles_minus() {
let _g = crate::test_util::global_state_lock();
assert!(isanum("123"));
assert!(isanum("-1")); assert!(isanum("---")); assert!(isanum("12-34")); assert!(!isanum("")); assert!(!isanum("abc")); assert!(!isanum("1a")); }
#[test]
fn test_havefiles_walks_table() {
let _g = crate::test_util::global_state_lock();
let mut tab = vec![job::new(), job::new(), job::new()];
tab[1].stat = stat::INUSE;
tab[1].filelist = vec!["/tmp/foo".to_string()];
assert!(havefiles(&tab));
tab[1].filelist.clear();
assert!(!havefiles(&tab));
tab[2].stat = 0;
tab[2].filelist = vec!["/tmp/bar".to_string()];
assert!(!havefiles(&tab));
}
#[test]
fn test_storepipestats_decodes_status() {
let _g = crate::test_util::global_state_lock();
let mut job = job::new();
let mut p1 = process::new(100);
p1.status = 0;
let mut p2 = process::new(101);
p2.status = 0x0100;
let mut p3 = process::new(102);
p3.status = 0x09;
job.procs = vec![p1, p2, p3];
let (stats, pipefail) = storepipestats(&job);
assert_eq!(stats.len(), 3);
assert_eq!(stats[0], 0); assert_eq!(stats[1], 1); assert_eq!(stats[2], 0o200 | 9); assert_eq!(pipefail, 0o200 | 9); }
#[test]
fn test_expandjobtab_respects_max() {
let _g = crate::test_util::global_state_lock();
let mut tab = vec![job::new(); 950];
assert!(expandjobtab(&mut tab, 0));
assert_eq!(tab.len(), 1000);
assert!(!expandjobtab(&mut tab, 0));
assert_eq!(tab.len(), 1000);
}
#[test]
fn test_addfilelist_fd_vs_name() {
let _g = crate::test_util::global_state_lock();
let mut job = job::new();
addfilelist(&mut job, Some("/tmp/zshrs-test.X"), -1);
addfilelist(&mut job, None, 7);
assert_eq!(job.filelist.len(), 2);
assert_eq!(job.filelist[0], "/tmp/zshrs-test.X");
assert_eq!(job.filelist[1], "<fd:7>");
}
#[test]
fn test_hasprocs_index_bounded() {
let _g = crate::test_util::global_state_lock();
let mut tab = vec![job::new(), job::new()];
tab[0].procs.push(process::new(1));
assert!(hasprocs(&tab, 0));
assert!(!hasprocs(&tab, 1));
assert!(!hasprocs(&tab, 99));
}
#[test]
fn test_makerunning_clears_stopped() {
let _g = crate::test_util::global_state_lock();
let mut tab = vec![job::new(), job::new()];
tab[0].stat = stat::STOPPED;
let mut p = process::new(42);
p.status = 0x7f; tab[0].procs.push(p);
makerunning(&mut tab, 0);
assert_eq!(tab[0].stat & stat::STOPPED, 0);
assert_eq!(tab[0].procs[0].status, SP_RUNNING);
}
#[test]
fn sigmsg_known_signals_render_canonical_text() {
let _g = crate::test_util::global_state_lock();
assert_eq!(sigmsg(libc::SIGHUP), "hangup");
assert_eq!(sigmsg(libc::SIGINT), "interrupt");
assert_eq!(sigmsg(libc::SIGQUIT), "quit");
assert_eq!(sigmsg(libc::SIGKILL), "killed");
assert_eq!(sigmsg(libc::SIGSEGV), "segmentation fault");
assert_eq!(sigmsg(libc::SIGPIPE), "broken pipe");
assert_eq!(sigmsg(libc::SIGTERM), "terminated");
assert_eq!(sigmsg(libc::SIGCHLD), "child exited");
assert_eq!(sigmsg(libc::SIGCONT), "continued");
}
#[test]
fn sigmsg_unknown_signal_returns_default() {
let _g = crate::test_util::global_state_lock();
assert_eq!(sigmsg(9999), "unknown signal");
assert_eq!(sigmsg(-1), "unknown signal");
assert_eq!(sigmsg(0), "unknown signal");
}
#[cfg(unix)]
#[test]
fn get_usage_returns_non_negative_times() {
let _g = crate::test_util::global_state_lock();
let ti = get_usage();
assert!(ti.ut >= 0);
assert!(ti.st >= 0);
}
#[test]
fn printhhmmss_formats_with_colons_and_dot() {
let _g = crate::test_util::global_state_lock();
let s = printhhmmss(3661.5);
assert!(s.contains(':'));
assert!(
s.contains('.'),
"millis must be present after dot (got {s:?})"
);
}
#[test]
fn printhhmmss_zero_seconds_well_formed() {
let _g = crate::test_util::global_state_lock();
let s = printhhmmss(0.0);
assert!(
!s.starts_with('-'),
"zero must not render with leading minus (got {s:?})"
);
}
#[cfg(unix)]
#[test]
fn get_clktck_returns_positive_value() {
let _g = crate::test_util::global_state_lock();
assert!(get_clktck() > 0, "_SC_CLK_TCK must be positive");
}
#[test]
fn deletefilelist_disown_clears_all_entries() {
let _g = crate::test_util::global_state_lock();
let mut j = job::new();
addfilelist(&mut j, Some("/tmp/a"), -1);
addfilelist(&mut j, None, 7);
assert_eq!(j.filelist.len(), 2);
deletefilelist(&mut j, true);
assert!(
j.filelist.is_empty(),
"disowning=true must clear all filelist entries"
);
}
#[test]
fn super_job_returns_none_for_top_level_job() {
let _g = crate::test_util::global_state_lock();
let tab = vec![job::new()];
assert!(super_job(&tab, 0).is_none());
}
#[test]
fn stat_flags_match_c_zsh_h_canonical_values() {
let _g = crate::test_util::global_state_lock();
assert_eq!(stat::CHANGED, 0x0001, "Src/zsh.h:1073");
assert_eq!(stat::STOPPED, 0x0002, "Src/zsh.h:1074");
assert_eq!(stat::TIMED, 0x0004, "Src/zsh.h:1075");
assert_eq!(stat::DONE, 0x0008, "Src/zsh.h:1076");
assert_eq!(stat::LOCKED, 0x0010, "Src/zsh.h:1077");
assert_eq!(stat::NOPRINT, 0x0020, "Src/zsh.h:1079");
assert_eq!(stat::INUSE, 0x0040, "Src/zsh.h:1081");
assert_eq!(stat::SUPERJOB, 0x0080, "Src/zsh.h:1082");
assert_eq!(stat::SUBJOB, 0x0100, "Src/zsh.h:1083");
assert_eq!(stat::WASSUPER, 0x0200, "Src/zsh.h:1084");
assert_eq!(stat::CURSH, 0x0400, "Src/zsh.h:1086");
assert_eq!(stat::NOSTTY, 0x0800, "Src/zsh.h:1087");
assert_eq!(stat::ATTACH, 0x1000, "Src/zsh.h:1089");
assert_eq!(stat::SUBLEADER, 0x2000, "Src/zsh.h:1090");
assert_eq!(stat::BUILTIN, 0x4000, "Src/zsh.h:1092");
}
#[test]
fn stat_flags_match_zsh_h_module_values() {
let _g = crate::test_util::global_state_lock();
assert_eq!(stat::CHANGED, STAT_CHANGED);
assert_eq!(stat::STOPPED, STAT_STOPPED);
assert_eq!(stat::TIMED, STAT_TIMED);
assert_eq!(stat::DONE, STAT_DONE);
assert_eq!(stat::SUPERJOB, STAT_SUPERJOB);
assert_eq!(stat::INUSE, STAT_INUSE);
assert_eq!(stat::ATTACH, STAT_ATTACH);
assert_eq!(stat::BUILTIN, STAT_BUILTIN);
}
#[test]
fn deletejob_calls_freejob_to_clear_all_state() {
let _g = crate::test_util::global_state_lock();
let mut jn = job::new();
jn.pwd = Some("/tmp/deletejob-pwd".to_string());
jn.other = 42;
jn.stty_in_env = 1;
jn.stat = stat::SUPERJOB;
deletejob(&mut jn, false);
assert_eq!(jn.pwd, None, "c:1525 — pwd cleared via freejob chain");
assert_eq!(jn.other, 0, "c:1525 — other cleared");
assert_eq!(jn.stty_in_env, 0, "c:1525 — stty_in_env cleared");
assert_eq!(jn.stat, 0, "c:1525 — stat cleared");
}
#[test]
fn freejob_resets_all_per_job_state_fields() {
let _g = crate::test_util::global_state_lock();
let mut jn = job::new();
jn.pwd = Some("/tmp/saved-pwd".to_string());
jn.gleader = 12345;
jn.other = 7;
jn.stat = stat::SUPERJOB;
jn.stty_in_env = 1;
jn.text = "echo foo".to_string();
freejob(&mut jn, false);
assert_eq!(jn.pwd, None, "c:1477-1479 — pwd reset to None");
assert_eq!(jn.gleader, 0, "c:1489 — gleader = 0");
assert_eq!(jn.other, 0, "c:1489 — other = 0");
assert_eq!(jn.stat, 0, "c:1490 — stat = 0");
assert_eq!(jn.stty_in_env, 0, "c:1490 — stty_in_env = 0");
assert_eq!(jn.text, "", "Rust-only: text cleared");
assert!(jn.procs.is_empty(), "c:1462 — procs cleared");
assert!(jn.auxprocs.is_empty(), "c:1469 — auxprocs cleared");
assert!(jn.filelist.is_empty(), "c:1491 — filelist cleared");
assert!(jn.ty.is_none(), "c:1475 — ty cleared");
}
#[test]
fn super_job_requires_nonzero_gleader() {
let _g = crate::test_util::global_state_lock();
let mut tab = vec![job::new(), job::new(), job::new()];
tab[2].stat |= stat::SUPERJOB;
tab[2].other = 1;
tab[2].gleader = 0;
assert!(
super_job(&tab, 1).is_none(),
"c:267 — gleader==0 must NOT match super_job lookup"
);
tab[2].gleader = 12345;
assert_eq!(
super_job(&tab, 1),
Some(2),
"c:267 — gleader != 0 + other match + SUPERJOB → match"
);
}
#[test]
fn findproc_unknown_pid_returns_none() {
let _g = crate::test_util::global_state_lock();
let tab: Vec<job> = vec![job::new(), job::new()];
assert!(findproc(&tab, 99999, false).is_none());
assert!(findproc(&tab, 99999, true).is_none());
}
#[test]
fn findproc_known_pid_returns_correct_indices() {
let _g = crate::test_util::global_state_lock();
let mut tab: Vec<job> = vec![job::new(), job::new()];
tab[1].stat = stat::INUSE;
let mut p = process::new(12345);
p.status = SP_RUNNING;
tab[1].procs.push(p);
let r = findproc(&tab, 12345, false);
assert!(r.is_some(), "must find the seeded pid via aux=false");
let (job_idx, proc_idx, is_aux) = r.unwrap();
assert_eq!(job_idx, 1);
assert_eq!(proc_idx, 0);
assert!(!is_aux, "primary procs vec, not auxprocs");
assert!(
findproc(&tab, 12345, true).is_none(),
"c:209 — aux=true must NOT match a procs (non-aux) entry"
);
}
#[test]
fn findproc_skips_stat_done_jobs() {
let _g = crate::test_util::global_state_lock();
let mut tab: Vec<job> = vec![job::new(), job::new(), job::new()];
tab[1].stat = stat::DONE | stat::INUSE;
let mut p1 = process::new(7777);
p1.status = 0; tab[1].procs.push(p1);
tab[2].stat = stat::INUSE;
let mut p2 = process::new(7777);
p2.status = SP_RUNNING;
tab[2].procs.push(p2);
let r = findproc(&tab, 7777, false);
assert_eq!(
r,
Some((2, 0, false)),
"c:204 — STAT_DONE entry must be skipped; live job 2 wins"
);
}
#[test]
fn printhhmmss_three_branch_format_dispatch() {
let _g = crate::test_util::global_state_lock();
assert_eq!(printhhmmss(0.5), "0.500");
assert_eq!(printhhmmss(12.345), "12.345");
assert_eq!(printhhmmss(75.0), "1:15.00");
assert_eq!(printhhmmss(125.5), "2:05.50");
assert_eq!(printhhmmss(3661.5), "1:01:01.50");
assert_eq!(printhhmmss(7200.0), "2:00:00.00");
}
#[test]
fn sigmsg_returns_canonical_messages_for_standard_signals() {
let _g = crate::test_util::global_state_lock();
let int_msg = sigmsg(libc::SIGINT);
let term_msg = sigmsg(libc::SIGTERM);
let kill_msg = sigmsg(libc::SIGKILL);
assert!(!int_msg.is_empty());
assert!(!term_msg.is_empty());
assert!(!kill_msg.is_empty());
assert_ne!(int_msg, term_msg);
}
#[test]
fn getsigidx_rejects_out_of_range_numeric() {
let _g = crate::test_util::global_state_lock();
assert_eq!(getsigidx("0"), Some(0), "EXIT pseudo-signal index 0");
assert_eq!(getsigidx("9"), Some(9), "SIGKILL signal number 9 → Some(9)");
assert_eq!(
getsigidx("9999"),
None,
"c:3056 — 9999 above VSIGCOUNT and outside RT range → None"
);
assert_eq!(getsigidx("99999999999"), None, "c:3056 — overflow → None");
}
#[test]
fn getsigidx_non_digit_unknown_name_returns_none() {
let _g = crate::test_util::global_state_lock();
assert_eq!(getsigidx("DEFINITELYNOTASIGNAL"), None);
assert_eq!(getsigidx(""), None, "empty string → None");
}
#[cfg(target_os = "linux")]
#[test]
fn getsigname_emits_rt_form_for_rt_signal_range() {
let _g = crate::test_util::global_state_lock();
let sigrtmin = libc::SIGRTMIN();
let sigrtmax = libc::SIGRTMAX();
assert_eq!(
getsigname(sigrtmin),
"RTMIN",
"c:3101 — RTMIN sig → bare RTMIN"
);
assert_eq!(
getsigname(sigrtmax),
"RTMAX",
"c:3101 — RTMAX sig → bare RTMAX"
);
assert_eq!(getsigname(sigrtmin + 1), "RTMIN+1");
assert_eq!(getsigname(sigrtmax - 1), "RTMAX-1");
}
#[test]
fn getsigname_standard_signals_unchanged() {
let _g = crate::test_util::global_state_lock();
assert_eq!(getsigname(libc::SIGINT), "INT");
assert_eq!(getsigname(libc::SIGHUP), "HUP");
assert_eq!(getsigname(libc::SIGCHLD), "CHLD");
assert_eq!(getsigname(libc::SIGKILL), "KILL");
assert_eq!(getsigname(0), "EXIT");
}
static ZLEACTIVE_TEST_LOCK: Mutex<()> = Mutex::new(());
#[test]
fn should_report_time_stat_timed_overrides_zleactive() {
let _g = crate::test_util::global_state_lock();
let _g = ZLEACTIVE_TEST_LOCK.lock().unwrap();
let prev = zleactive.load(Ordering::Relaxed);
zleactive.store(1, Ordering::Relaxed);
let mut job = job::new();
job.stat |= stat::TIMED;
assert!(should_report_time(&job, -1.0));
zleactive.store(prev, Ordering::Relaxed);
}
#[test]
fn should_report_time_zleactive_suppresses() {
let _g = crate::test_util::global_state_lock();
let _g = ZLEACTIVE_TEST_LOCK.lock().unwrap();
let prev = zleactive.load(Ordering::Relaxed);
zleactive.store(1, Ordering::Relaxed);
let mut job = job::new();
let now = Instant::now();
let mut p = process::new(1);
p.bgtime = Some(now);
p.endtime = Some(now + Duration::from_secs(10));
job.procs.push(p);
assert!(!should_report_time(&job, 1.0));
zleactive.store(0, Ordering::Relaxed);
assert!(should_report_time(&job, 1.0));
zleactive.store(prev, Ordering::Relaxed);
}
#[test]
fn should_report_time_negative_threshold_suppresses() {
let _g = crate::test_util::global_state_lock();
let _g = ZLEACTIVE_TEST_LOCK.lock().unwrap();
let mut job = job::new();
let now = Instant::now();
let mut p = process::new(1);
p.bgtime = Some(now);
p.endtime = Some(now + Duration::from_secs(10));
job.procs.push(p);
assert!(!should_report_time(&job, -1.0));
}
#[test]
fn should_report_time_no_procs_returns_false() {
let _g = crate::test_util::global_state_lock();
let _g = ZLEACTIVE_TEST_LOCK.lock().unwrap();
let job = job::new(); assert!(!should_report_time(&job, 0.0));
}
static JOBPWD_TEST_LOCK: Mutex<()> = Mutex::new(());
#[test]
fn setjobpwd_stamps_pwd_on_inuse_jobs_without_one() {
let _g = crate::test_util::global_state_lock();
let _g = JOBPWD_TEST_LOCK.lock().unwrap();
crate::ported::params::assignsparam("PWD", "/tmp/test_setjobpwd", 0);
let tab = JOBTAB.get_or_init(|| Mutex::new(Vec::new()));
{
let mut tab = tab.lock().unwrap();
tab.clear();
tab.push(job::new()); let mut j1 = job::new();
j1.stat = stat::INUSE;
j1.pwd = None;
tab.push(j1);
let mut j2 = job::new();
j2.stat = stat::INUSE;
j2.pwd = Some("/preserved".to_string());
tab.push(j2);
let mut j3 = job::new();
j3.stat = 0;
j3.pwd = None;
tab.push(j3);
}
setjobpwd();
let tab = tab.lock().unwrap();
assert_eq!(
tab[1].pwd.as_deref(),
Some("/tmp/test_setjobpwd"),
"c:1888 — INUSE+no-pwd job must be stamped with PWD"
);
assert_eq!(
tab[2].pwd.as_deref(),
Some("/preserved"),
"c:1887 — existing pwd must NOT be overwritten"
);
assert_eq!(
tab[3].pwd, None,
"c:1887 — non-INUSE job (stat==0) must NOT be stamped"
);
assert_eq!(
tab[0].pwd, None,
"c:1886 — index 0 (shell) must NOT be stamped"
);
}
#[test]
fn jobs_corpus_printhhmmss_zero_exact() {
let _g = crate::test_util::global_state_lock();
assert_eq!(printhhmmss(0.0), "0.000");
}
#[test]
fn jobs_corpus_printhhmmss_just_under_one_minute() {
let _g = crate::test_util::global_state_lock();
let s = printhhmmss(59.999);
assert!(!s.contains(':'), "sub-minute has no colon, got {s:?}");
assert!(s.starts_with("59.9"));
}
#[test]
fn jobs_corpus_printhhmmss_exactly_one_minute() {
let _g = crate::test_util::global_state_lock();
assert_eq!(printhhmmss(60.0), "1:00.00");
}
#[test]
fn jobs_corpus_printhhmmss_exactly_one_hour() {
let _g = crate::test_util::global_state_lock();
assert_eq!(printhhmmss(3600.0), "1:00:00.00");
}
#[test]
fn jobs_corpus_printhhmmss_exactly_one_day() {
let _g = crate::test_util::global_state_lock();
assert_eq!(printhhmmss(86400.0), "24:00:00.00");
}
#[test]
fn jobs_corpus_sigmsg_int_is_interrupt() {
let _g = crate::test_util::global_state_lock();
assert_eq!(sigmsg(libc::SIGINT), "interrupt");
}
#[test]
fn jobs_corpus_sigmsg_term_is_terminated() {
let _g = crate::test_util::global_state_lock();
assert_eq!(sigmsg(libc::SIGTERM), "terminated");
}
#[test]
fn jobs_corpus_sigmsg_segv_is_segfault() {
let _g = crate::test_util::global_state_lock();
assert_eq!(sigmsg(libc::SIGSEGV), "segmentation fault");
}
#[test]
fn jobs_corpus_sigmsg_pipe_is_broken_pipe() {
let _g = crate::test_util::global_state_lock();
assert_eq!(sigmsg(libc::SIGPIPE), "broken pipe");
}
}