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::zwarn;
use crate::ported::utils::zwarnnam;
use crate::ported::utils::{fdtable_get, zclose};
use crate::ported::zsh_h::{
isset, job, jobfile, options, process, FDT_PROC_SUBST, INTERACTIVE, LONGLISTJOBS, MONITOR,
OPT_ISSET, POSIXBUILTINS, POSIXJOBS, STAT_ATTACH, STAT_INUSE, STAT_SUBJOB,
STAT_SUBJOB_ORPHANED, STAT_SUPERJOB,
};
pub use crate::ported::zsh_h::{timeinfo, MAXJOBS_ALLOC, MAX_PIPESTATS, SP_RUNNING};
use crate::DPUTS;
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};
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(jobtab, super_idx, true);
}
}
if fg {
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; }
crate::exec_jobs::printjob_synch(jobtab, super_idx, i32::from(isset(LONGLISTJOBS)), 1); 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, inforeground: i32, fixlastval: i32) -> (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;
}
}
if inforeground != 0 {
let mut n = stats.len();
let mut ps = PIPESTATS
.get_or_init(|| Mutex::new([0; MAX_PIPESTATS]))
.lock()
.unwrap_or_else(|e| e.into_inner());
ps[..n].copy_from_slice(&stats);
if (job.stat & stat::CURSH) != 0 && n < MAX_PIPESTATS {
ps[n] = crate::ported::builtin::LASTVAL.load(Ordering::Relaxed);
n += 1;
}
drop(ps);
*NUMPIPESTATS
.get_or_init(|| Mutex::new(0))
.lock()
.unwrap_or_else(|e| e.into_inner()) = n;
}
if fixlastval != 0 {
let lastval = crate::ported::builtin::LASTVAL.load(Ordering::Relaxed);
if (job.stat & stat::CURSH) != 0 {
if lastval == 0 && isset(crate::ported::zsh_h::PIPEFAIL) {
if inforeground != 0 {
crate::ported::exec::this_noerrexit.store(0, Ordering::Relaxed);
}
crate::ported::builtin::LASTVAL.store(pipefail, Ordering::Relaxed);
}
} else if isset(crate::ported::zsh_h::PIPEFAIL) {
if inforeground != 0 {
crate::ported::exec::this_noerrexit.store(0, Ordering::Relaxed);
}
crate::ported::builtin::LASTVAL.store(pipefail, Ordering::Relaxed);
}
}
(stats, pipefail)
}
pub fn update_job(jobtab: &mut [job], jn: usize) -> bool {
let mut val: i32 = 0; let mut status: i32 = 0;
let mut somestopped = false; let mut inforeground: i32 = 0;
let mut signalled = false;
for pn in jobtab[jn].auxprocs.iter_mut() {
if pn.status > 0
&& !libc::WIFEXITED(pn.status)
&& !libc::WIFSIGNALED(pn.status)
&& !libc::WIFSTOPPED(pn.status)
{
pn.status = SP_RUNNING;
}
if pn.is_running() {
return false; }
}
{
let j = &mut jobtab[jn];
let gleader = j.gleader;
let nprocs = j.procs.len();
for (i, pn) in j.procs.iter_mut().enumerate() {
if pn.status > 0
&& !libc::WIFEXITED(pn.status)
&& !libc::WIFSIGNALED(pn.status)
&& !libc::WIFSTOPPED(pn.status)
{
j.stat &= !stat::STOPPED; pn.status = SP_RUNNING; }
if pn.is_running() {
return false; }
if libc::WIFSTOPPED(pn.status) {
somestopped = true; }
if i + 1 == nprocs {
val = if libc::WIFSIGNALED(pn.status) {
0o200 | libc::WTERMSIG(pn.status)
} else if libc::WIFSTOPPED(pn.status) {
0o200 | libc::WSTOPSIG(pn.status)
} else {
libc::WEXITSTATUS(pn.status)
};
signalled = libc::WIFSIGNALED(pn.status); }
if pn.pid == gleader {
status = pn.status; }
}
}
let job_no = jn as i32; let thisjob = *THISJOB
.get_or_init(|| Mutex::new(-1))
.lock()
.unwrap_or_else(|e| e.into_inner());
let mypgrp = crate::ported::modules::clone::mypgrp.load(Ordering::Relaxed);
if somestopped {
if jobtab[jn].stty_in_env != 0 && jobtab[jn].ty.is_none() {
if let Some(tio) = crate::ported::utils::gettyinfo() {
let mut winsize: libc::winsize = unsafe { std::mem::zeroed() };
unsafe {
libc::ioctl(
crate::ported::init::SHTTY.load(Ordering::Relaxed),
libc::TIOCGWINSZ,
&mut winsize,
);
}
jobtab[jn].ty = Some(Box::new(crate::ported::zsh_h::ttyinfo { tio, winsize }));
}
}
if (jobtab[jn].stat & stat::SUBJOB) != 0 {
jobtab[jn].stat |= stat::CHANGED | stat::STOPPED; if let Some(i) = super_job(jobtab, jn) {
unsafe { libc::killpg(jobtab[i].gleader, libc::SIGTSTP) }; jobtab[i].stat |= stat::CHANGED | stat::STOPPED; if (jobtab[i].stat & stat::DONE) == 0 {
let mut cj = CURJOB
.get_or_init(|| Mutex::new(-1))
.lock()
.unwrap_or_else(|e| e.into_inner());
let mut pj = PREVJOB
.get_or_init(|| Mutex::new(-1))
.lock()
.unwrap_or_else(|e| e.into_inner());
*pj = *cj; *cj = i as i32; }
if isset(crate::ported::zsh_h::NOTIFY)
&& (jobtab[i].stat & stat::LOCKED) != 0
&& (jobtab[i].stat & stat::NOPRINT) == 0
{
if crate::exec_jobs::printjob_synch(jobtab, i, i32::from(isset(LONGLISTJOBS)), 0)
&& zleactive.load(Ordering::Relaxed) != 0
{
crate::ported::init::zleentry(crate::ported::zsh_h::ZLE_CMD_REFRESH);
}
}
}
return true; }
if (jobtab[jn].stat & stat::STOPPED) != 0 {
return true; }
}
{
LASTVAL2.store(val, Ordering::SeqCst);
if (jobtab[jn].stat & stat::CURSH) != 0 {
inforeground = 1;
} else if job_no == thisjob {
crate::ported::builtin::LASTVAL.store(val, Ordering::Relaxed);
inforeground = 2;
}
}
let shout_v = *crate::ported::init::shout
.lock()
.unwrap_or_else(|e| e.into_inner());
let ttyfrozen_v = *TTYFROZEN
.get_or_init(|| Mutex::new(0))
.lock()
.unwrap_or_else(|e| e.into_inner());
if shout_v != 0
&& shout_v != 3
&& ttyfrozen_v == 0
&& jobtab[jn].stty_in_env == 0
&& zleactive.load(Ordering::Relaxed) == 0
&& job_no == thisjob
&& !somestopped
&& (jobtab[jn].stat & stat::NOSTTY) == 0
{
if let Some(tio) = crate::ported::utils::gettyinfo() {
*crate::ported::utils::SHTTYINFO
.lock()
.unwrap_or_else(|e| e.into_inner()) = Some(tio);
}
}
if isset(MONITOR) {
let pgrp = crate::ported::utils::gettygrp(); let deadpgrp = mypgrp != pgrp
&& inforeground != 0
&& pgrp > 1
&& unsafe { libc::kill(-pgrp, 0) } == -1
&& std::io::Error::last_os_error().raw_os_error() == Some(libc::ESRCH);
if mypgrp != pgrp
&& inforeground != 0
&& ((jobtab[jn].gleader == pgrp && signalled) || deadpgrp)
{
if crate::ported::exec::list_pipe.load(Ordering::Relaxed) != 0 {
if somestopped || deadpgrp {
crate::ported::utils::attachtty(mypgrp); crate::ported::utils::adjustwinsize(0); } else {
jobtab[jn].stat |= stat::ATTACH; }
if signalled
&& inforeground == 1
&& ((val & !0o200) == libc::SIGINT || (val & !0o200) == libc::SIGQUIT)
{
if ERRBRK_SAVED.load(Ordering::Relaxed) == 0 {
ERRBRK_SAVED.store(1, Ordering::Relaxed); PREV_BREAKS.store(
crate::ported::builtin::BREAKS.load(Ordering::Relaxed),
Ordering::Relaxed,
); PREV_ERRFLAG.store(
crate::ported::utils::errflag.load(Ordering::Relaxed),
Ordering::Relaxed,
); }
crate::ported::builtin::BREAKS.store(
crate::ported::builtin::LOOPS.load(Ordering::Relaxed),
Ordering::Relaxed,
); crate::ported::utils::errflag
.fetch_or(crate::ported::zsh_h::ERRFLAG_INT, Ordering::Relaxed); crate::ported::input::inerrflush(); }
} else {
crate::ported::utils::attachtty(mypgrp); crate::ported::utils::adjustwinsize(0); }
}
} else if crate::ported::exec::list_pipe.load(Ordering::Relaxed) != 0
&& signalled
&& inforeground == 1
&& ((val & !0o200) == libc::SIGINT || (val & !0o200) == libc::SIGQUIT)
{
if ERRBRK_SAVED.load(Ordering::Relaxed) == 0 {
ERRBRK_SAVED.store(1, Ordering::Relaxed);
PREV_BREAKS.store(
crate::ported::builtin::BREAKS.load(Ordering::Relaxed),
Ordering::Relaxed,
);
PREV_ERRFLAG.store(
crate::ported::utils::errflag.load(Ordering::Relaxed),
Ordering::Relaxed,
);
}
crate::ported::builtin::BREAKS.store(
crate::ported::builtin::LOOPS.load(Ordering::Relaxed),
Ordering::Relaxed,
);
crate::ported::utils::errflag
.fetch_or(crate::ported::zsh_h::ERRFLAG_INT, Ordering::Relaxed);
crate::ported::input::inerrflush();
}
if somestopped && (jobtab[jn].stat & stat::SUPERJOB) != 0 {
return true; }
jobtab[jn].stat |= if somestopped {
stat::CHANGED | stat::STOPPED
} else {
stat::CHANGED | stat::DONE
}; if (jobtab[jn].stat & (stat::DONE | stat::STOPPED)) != 0 {
storepipestats(&jobtab[jn], inforeground, 0);
}
if inforeground == 0
&& (jobtab[jn].stat & (stat::SUBJOB | stat::DONE)) == (stat::SUBJOB | stat::DONE)
{
if let Some(su) = super_job(jobtab, jn) {
handle_sub(jobtab, su, false);
}
}
if (jobtab[jn].stat & (stat::DONE | stat::STOPPED)) == stat::STOPPED {
let mut cj = CURJOB
.get_or_init(|| Mutex::new(-1))
.lock()
.unwrap_or_else(|e| e.into_inner());
let mut pj = PREVJOB
.get_or_init(|| Mutex::new(-1))
.lock()
.unwrap_or_else(|e| e.into_inner());
*pj = *cj; *cj = job_no; }
if (isset(crate::ported::zsh_h::NOTIFY) || job_no == thisjob)
&& (jobtab[jn].stat & stat::LOCKED) != 0
{
if crate::exec_jobs::printjob_synch(jobtab, jn, i32::from(isset(LONGLISTJOBS)), 0)
&& zleactive.load(Ordering::Relaxed) != 0
{
crate::ported::init::zleentry(crate::ported::zsh_h::ZLE_CMD_REFRESH);
}
}
if inforeground == 2 && isset(MONITOR) && libc::WIFSIGNALED(status) {
let sig = libc::WTERMSIG(status); if sig == libc::SIGINT || sig == libc::SIGQUIT {
let trapped = crate::ported::signals::sigtrapped
.lock()
.ok()
.and_then(|t| t.get(sig as usize).copied())
.unwrap_or(0);
if trapped != 0 {
PSEUDO_SIG_TRAP_PENDING.store(sig, Ordering::SeqCst);
} else {
crate::ported::builtin::BREAKS.store(
crate::ported::builtin::LOOPS.load(Ordering::Relaxed),
Ordering::Relaxed,
); crate::ported::utils::errflag
.fetch_or(crate::ported::zsh_h::ERRFLAG_INT, Ordering::Relaxed); check_cursh_sig(jobtab, sig); }
}
}
true
}
pub static ERRBRK_SAVED: std::sync::atomic::AtomicI32 = std::sync::atomic::AtomicI32::new(0);
pub static PREV_ERRFLAG: std::sync::atomic::AtomicI32 = std::sync::atomic::AtomicI32::new(0);
pub static PREV_BREAKS: std::sync::atomic::AtomicI32 = std::sync::atomic::AtomicI32::new(0);
pub static PSEUDO_SIG_TRAP_PENDING: std::sync::atomic::AtomicI32 =
std::sync::atomic::AtomicI32::new(0);
pub static CHLD_TRAP_PENDING: std::sync::atomic::AtomicUsize = std::sync::atomic::AtomicUsize::new(0);
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());
}
let thisjob = *THISJOB
.get_or_init(|| Mutex::new(-1))
.lock()
.expect("thisjob poisoned");
if (jn[ji].stat & (stat::CURSH | stat::BUILTIN)) == 0 && ji as i32 != thisjob {
if libc::WIFEXITED(status) {
addbgstatus(pid, libc::WEXITSTATUS(status)); } else if libc::WIFSIGNALED(status) {
addbgstatus(pid, 0o200 | libc::WTERMSIG(status)); }
}
update_job(jn, ji);
if ji as i32 != thisjob {
CHLD_TRAP_PENDING.fetch_add(1, std::sync::atomic::Ordering::SeqCst);
}
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.peek() {
Some('E') => {
chars.next();
result.push_str(&format!("{:.0}ms", elapsed_secs * 1000.0))
}
Some('U') => {
chars.next();
result.push_str(&format!("{:.0}ms", user_secs * 1000.0))
}
Some('S') => {
chars.next();
result.push_str(&format!("{:.0}ms", system_secs * 1000.0))
}
_ => result.push_str("%m"), },
Some('u') => match chars.peek() {
Some('E') => {
chars.next();
result.push_str(&format!("{:.0}us", elapsed_secs * 1_000_000.0))
}
Some('U') => {
chars.next();
result.push_str(&format!("{:.0}us", user_secs * 1_000_000.0))
}
Some('S') => {
chars.next();
result.push_str(&format!("{:.0}us", system_secs * 1_000_000.0))
}
_ => result.push_str("%u"), },
Some('n') => match chars.peek() {
Some('E') => {
chars.next();
result.push_str(&format!("{:.0}ns", elapsed_secs * 1_000_000_000.0))
}
Some('U') => {
chars.next();
result.push_str(&format!("{:.0}ns", user_secs * 1_000_000_000.0))
}
Some('S') => {
chars.next();
result.push_str(&format!("{:.0}ns", system_secs * 1_000_000_000.0))
}
_ => result.push_str("%n"), },
Some('*') => match chars.peek() {
Some('E') => {
chars.next();
result.push_str(&printhhmmss(elapsed_secs))
}
Some('U') => {
chars.next();
result.push_str(&printhhmmss(user_secs))
}
Some('S') => {
chars.next();
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 => {}
}
} 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
}
pub fn sigmsg(sig: i32) -> &'static str {
#[cfg(target_os = "linux")]
{
let (lo, hi) = (libc::SIGRTMIN(), libc::SIGRTMAX());
if sig >= lo && sig <= hi {
static RT: OnceLock<Mutex<std::collections::HashMap<i32, &'static str>>> =
OnceLock::new();
let mut m = RT.get_or_init(|| Mutex::new(Default::default())).lock().unwrap();
return *m.entry(sig).or_insert_with(|| {
let s: &'static str =
Box::leak(format!("real-time event {}", sig - lo + 1).into_boxed_str());
s
});
}
}
if sig == 0 {
return "done";
}
crate::ported::signals_h::SIG_MSG
.iter()
.find(|(s, _)| *s == sig)
.map(|(_, m)| *m)
.unwrap_or("unknown signal") }
pub fn printjob(
job: &job,
job_num: usize,
lng: i32,
cur_job: Option<usize>,
prev_job: Option<usize>,
thisfmt: bool,
) -> String {
let mut len = 9usize;
for pn in job.procs.iter() {
if pn.status == SP_RUNNING {
continue;
}
#[cfg(unix)]
{
if libc::WIFSIGNALED(pn.status) {
let mut llen = sigmsg(libc::WTERMSIG(pn.status)).len(); if (pn.status & 0x80) != 0 {
llen += 14; }
len = len.max(llen); } else if libc::WIFSTOPPED(pn.status) {
len = len.max(sigmsg(libc::WSTOPSIG(pn.status)).len()); }
}
}
let width = len + 2;
let fmt_proc_status = |status: i32| -> String {
let s = 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 {:<4}", code) }
} else if (status & 0xff) == 0x7f {
sigmsg((status >> 8) & 0xff).to_string() } else {
let sig = status & 0x7f;
if (status & 0x80) != 0 {
format!("{} (core dumped)", sigmsg(sig)) } else {
sigmsg(sig).to_string() }
};
format!("{:<w$}", s, w = width)
};
let marker = if Some(job_num) == cur_job {
'+'
} else if Some(job_num) == prev_job {
'-'
} else {
' '
};
let (head_prefix, cont_prefix) = if !thisfmt || lng != 0 {
(
format!("[{}] {} ", job_num, marker),
if job_num > 9 { " " } else { " " }, )
} else {
("zsh: ".to_string(), "zsh: ") };
let header = if job.procs.is_empty() {
if job.text.is_empty() {
return String::new();
}
let status_str = if job.is_done() {
format!("{:<w$}", "done", w = width)
} else if job.is_stopped() {
format!("{:<w$}", "suspended", w = width)
} else {
format!("{:<w$}", "running", w = width)
};
format!("{}{}{}", head_prefix, status_str, job.text)
} else {
let lineleng = crate::ported::utils::ZTERM_COLUMNS.load(std::sync::atomic::Ordering::SeqCst);
let mut lines: Vec<String> = Vec::new();
let mut i = 0usize;
let mut fline = true;
let mut lng = lng;
let mut skip = 0usize;
while i < job.procs.len() {
let pn = &job.procs[i];
let mut len2 = if thisfmt { 5 } else { 10 } + len;
let mut group_end = i + 1;
if (lng & 3) == 0 {
while group_end < job.procs.len() {
let qn = &job.procs[group_end];
if qn.status != pn.status {
break; }
let tail = if group_end + 1 < job.procs.len() { 3 } else { 0 };
if qn.text.len() + len2 + tail > lineleng as usize {
break;
}
len2 += qn.text.len() + 2; group_end += 1;
}
}
let mut line = String::new();
line.push_str(if fline { &head_prefix } else { cont_prefix });
if (lng & 1) != 0 {
line.push_str(&format!("{} ", pn.pid)); } else if (lng & 2) != 0 {
let x = job.gleader;
line.push_str(&format!("{} ", x));
let mut d = x;
loop {
skip += 1; d /= 10;
if d == 0 {
break;
}
}
skip += 1; lng &= !3; } else {
line.push_str(&" ".repeat(skip)); }
line.push_str(&fmt_proc_status(pn.status));
for (k, p) in job.procs[i..group_end].iter().enumerate() {
line.push_str(&p.text);
if i + k + 1 < job.procs.len() {
line.push_str(" | "); }
}
lines.push(line);
fline = false;
i = group_end;
}
lines.join("\n")
};
let header = if (lng & 4) != 0 {
let dir = match job.pwd.as_deref() {
Some(p) => p.to_string(),
None => getsparam("PWD").unwrap_or_default(),
};
format!("{}\n(pwd : {})", header, crate::ported::utils::fprintdir(&dir))
} else {
header
};
header
}
pub fn addfilelist(job: &mut job, name: Option<&str>, fd: i32) {
let jf = match name {
Some(n) => jobfile {
name: Some(n.to_string()),
fd: 0,
is_fd: 0,
},
None => jobfile {
name: None,
fd,
is_fd: 1,
},
};
job.filelist.push(jf); }
pub fn pipecleanfilelist(filelist: &mut job, proc_subst_only: bool) {
filelist.filelist.retain(|jf| {
if jf.is_fd != 0 && (!proc_subst_only || fdtable_get(jf.fd) == FDT_PROC_SUBST) {
zclose(jf.fd); false } else {
true
}
});
}
pub fn deletefilelist(file_list: &mut job, disowning: bool) {
for jf in &file_list.filelist {
if jf.is_fd != 0 {
if !disowning {
zclose(jf.fd); }
} else {
if !disowning {
if let Some(ref name) = jf.name {
let _ = std::fs::remove_file(name); }
}
}
}
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);
}
crate::fusevm_bridge::cleanfilelists_pending();
}
pub fn freejob(jobtab: &mut [job], jn: usize, deleting: bool) {
jobtab[jn].procs.clear();
jobtab[jn].auxprocs.clear();
jobtab[jn].ty = None;
jobtab[jn].pwd = None;
if (jobtab[jn].stat & stat::WASSUPER) != 0 {
let other = jobtab[jn].other as usize;
if other < jobtab.len() {
if deleting {
deletejob(jobtab, other, false); } else {
freejob(jobtab, other, false); }
}
}
let jn = &mut jobtab[jn];
jn.gleader = 0;
jn.other = 0;
jn.stat = 0;
jn.stty_in_env = 0;
jn.filelist.clear();
jn.text.clear();
}
pub fn deletejob(jobtab: &mut [job], jn: usize, disowning: bool) {
deletefilelist(&mut jobtab[jn], disowning);
if (jobtab[jn].stat & STAT_ATTACH) != 0 {
#[cfg(unix)]
{
crate::ported::utils::attachtty(crate::ported::modules::clone::mypgrp.load(Ordering::Relaxed)); crate::ported::utils::adjustwinsize(0); }
}
if (jobtab[jn].stat & STAT_SUPERJOB) != 0 {
let other = jobtab[jn].other as usize;
if let Some(jno) = jobtab.get_mut(other) {
if (jno.stat & STAT_SUBJOB) != 0 {
jno.stat |= STAT_SUBJOB_ORPHANED; }
}
}
freejob(jobtab, 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 width: usize = text
.bytes()
.map(|b| 1 + crate::ported::ztype_h::imeta(b) as usize)
.sum();
let text: String = if width < crate::ported::zsh_h::JOBTEXTSIZE - 1 {
text.to_string()
} else {
let noerrs = crate::ported::utils::noerrs_lock();
let saved_noerrs = std::mem::replace(&mut *noerrs.lock().unwrap(), 1);
let saved_errflag = crate::ported::utils::errflag.load(std::sync::atomic::Ordering::SeqCst);
let saved_noaliases = crate::ported::lex::noaliases();
crate::ported::lex::set_noaliases(true);
let prog = crate::ported::exec::parse_string(text, 0);
crate::ported::lex::set_noaliases(saved_noaliases);
crate::ported::utils::errflag.store(saved_errflag, std::sync::atomic::Ordering::SeqCst);
*noerrs.lock().unwrap() = saved_noerrs;
match prog {
Some(p) => crate::ported::text::getjobtext(Box::new(p), None),
None => text.to_string(),
}
};
let proc = process::new(pid);
let proc = process {
pid,
status: SP_RUNNING,
text,
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: usize, wait_cmd: i32) -> Option<i32> {
use crate::ported::utils::errflag;
use crate::ported::zsh_h::{ERRFLAG_ERROR, ZSIG_TRAPPED};
let table = JOBTAB.get_or_init(|| Mutex::new(Vec::new()));
let forked = table
.lock()
.unwrap_or_else(|e| e.into_inner())
.get(job)
.map(|jn| !jn.procs.is_empty() || !jn.auxprocs.is_empty())
.unwrap_or(false);
if !forked {
waitonejob(job as i32);
return Some(0); }
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();
let changed = {
let mut tab = table.lock().unwrap_or_else(|e| e.into_inner());
match tab.get_mut(job) {
Some(jn) => {
jn.stat |= stat::LOCKED;
(jn.stat & stat::CHANGED) != 0
}
None => false,
}
};
if changed {
let mut tab = table.lock().unwrap_or_else(|e| e.into_inner());
crate::exec_jobs::printjob_synch(&mut tab, job, i32::from(isset(LONGLISTJOBS)), 1);
}
{
let mut tab = table.lock().unwrap_or_else(|e| e.into_inner());
if let Some(jn) = tab.get_mut(job) {
if !jn.filelist.is_empty() {
pipecleanfilelist(jn, false); }
}
}
let interact = isset(INTERACTIVE);
loop {
crate::ported::signals_h::run_queued_signals();
let jstat = table
.lock()
.unwrap_or_else(|e| e.into_inner())
.get(job)
.map(|jn| jn.stat)
.unwrap_or(0);
if (errflag.load(Ordering::Relaxed) & ERRFLAG_ERROR) != 0
|| jstat == 0
|| (jstat & stat::DONE) != 0
|| (interact && (jstat & stat::STOPPED) != 0)
{
break;
}
let _ = crate::ported::signals::signal_suspend(libc::SIGCHLD, wait_cmd != 0);
let pseudo = PSEUDO_SIG_TRAP_PENDING.swap(0, Ordering::SeqCst);
if pseudo != 0 {
crate::ported::signals::dotrap(pseudo); if errflag.load(Ordering::Relaxed) != 0 {
crate::ported::builtin::BREAKS.store(
crate::ported::builtin::LOOPS.load(Ordering::Relaxed),
Ordering::Relaxed,
); }
check_cursh_sig(&table.lock().unwrap_or_else(|e| e.into_inner()), pseudo); }
let ls = crate::ported::signals::last_signal.load(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(Ordering::Relaxed) != 0 {
killjb(job, libc::SIGCONT);
}
let mut tab = table.lock().unwrap_or_else(|e| e.into_inner());
if tab.get(job).is_some_and(|jn| (jn.stat & stat::SUPERJOB) != 0)
&& handle_sub(&mut tab, job, true) != 0
{
break;
}
drop(tab);
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();
Some(0)
}
pub fn waitjobs() {
let table = JOBTAB.get_or_init(|| Mutex::new(Vec::new()));
let thisjob = *THISJOB
.get_or_init(|| Mutex::new(-1))
.lock()
.unwrap_or_else(|e| e.into_inner());
DPUTS!(thisjob == -1, "No valid job in waitjobs."); if thisjob < 0 {
return;
}
let thisjob = thisjob as usize;
let subjob = table
.lock()
.unwrap_or_else(|e| e.into_inner())
.get(thisjob)
.filter(|jn| (jn.stat & stat::SUPERJOB) != 0)
.map(|jn| jn.other as usize);
for jn in subjob.into_iter().chain(std::iter::once(thisjob)) {
loop {
let finished = table
.lock()
.unwrap_or_else(|e| e.into_inner())
.get(jn)
.map_or(true, |j| j.is_done() || j.is_stopped());
if finished {
break;
}
#[cfg(unix)]
{
let mut status: i32 = 0;
let pid = unsafe { libc::waitpid(-1, &mut status, libc::WUNTRACED) };
if pid > 0 {
update_bg_job(
&mut table.lock().unwrap_or_else(|e| e.into_inner()),
pid,
status,
);
} else {
match std::io::Error::last_os_error().raw_os_error() {
Some(libc::EINTR) => {}
Some(libc::ECHILD) => {
let running: Vec<i32> = table
.lock()
.unwrap_or_else(|e| e.into_inner())
.get(jn)
.map(|j| j.procs.iter().filter(|p| p.is_running()).map(|p| p.pid).collect())
.unwrap_or_default();
let mut claimed = false;
for pid in running {
if let Some((raw, _, _)) = crate::fusevm_bridge::take_reaped_status(pid) {
update_bg_job(
&mut table.lock().unwrap_or_else(|e| e.into_inner()),
pid,
raw,
);
claimed = true;
}
}
if !claimed {
break;
}
}
_ => break,
}
}
}
#[cfg(not(unix))]
{
break;
}
for _ in 0..CHLD_TRAP_PENDING.swap(0, Ordering::SeqCst) {
crate::ported::signals::dotrap(libc::SIGCHLD);
}
let pseudo = PSEUDO_SIG_TRAP_PENDING.swap(0, Ordering::SeqCst);
if pseudo != 0 {
crate::ported::signals::dotrap(pseudo); if crate::ported::utils::errflag.load(Ordering::Relaxed) != 0 {
crate::ported::builtin::BREAKS.store(
crate::ported::builtin::LOOPS.load(Ordering::Relaxed),
Ordering::Relaxed,
); }
check_cursh_sig(&table.lock().unwrap_or_else(|e| e.into_inner()), pseudo); }
}
}
*THISJOB
.get_or_init(|| Mutex::new(-1))
.lock()
.unwrap_or_else(|e| e.into_inner()) = -1;
}
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; }
jobs.clear();
jobs.push(job::new()); *MAXJOB.get_or_init(|| Mutex::new(0)).lock().unwrap() = 0; let control = initjob(&mut jobs); *THISJOB.get_or_init(|| Mutex::new(-1)).lock().unwrap() = control as i32;
}
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 {
if jobtab.is_empty() {
jobtab.push(job::new());
}
for i in 1..jobtab.len() {
if (jobtab[i].stat & stat::INUSE) == 0 {
return initnewjob(jobtab, i); }
}
let idx = jobtab.len();
jobtab.push(job::new());
initnewjob(jobtab, idx)
}
fn initnewjob(jobtab: &mut [job], i: usize) -> usize {
jobtab[i] = job::new();
jobtab[i].stat = stat::INUSE; jobtab[i].pwd = None; jobtab[i].gleader = 0; let mut mj = MAXJOB
.get_or_init(|| Mutex::new(0))
.lock()
.expect("maxjob poisoned");
if i > *mj {
*mj = i;
}
i }
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
|| crate::ported::builtin::SUBSHELL_DEPTH.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 !in_subsh {
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 thisjob < tab.len() {
deletejob(&mut tab, thisjob, 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(jobtab: &mut [job]) {
for i in 1..jobtab.len() {
if (jobtab[i].stat & stat::CHANGED) != 0 {
crate::exec_jobs::printjob_synch(jobtab, i, i32::from(isset(LONGLISTJOBS)), 1); }
}
}
pub fn isanum(s: &str) -> bool {
!s.is_empty() && s.bytes().all(|b| b == b'-' || b.is_ascii_digit())
}
pub fn setcurjob() {
let inuse = |jobno: i32| -> bool {
let tab = JOBTAB
.get_or_init(|| Mutex::new(Vec::new()))
.lock()
.expect("jobtab poisoned");
tab.get(jobno as usize)
.map(|j| (j.stat & stat::INUSE) != 0)
.unwrap_or(false)
};
let thisjob = *THISJOB.get_or_init(|| Mutex::new(-1)).lock().unwrap();
let curjob = *CURJOB.get_or_init(|| Mutex::new(-1)).lock().unwrap();
if (curjob != -1 && curjob == thisjob) || (curjob != -1 && !inuse(curjob)) {
let pj = *PREVJOB.get_or_init(|| Mutex::new(-1)).lock().unwrap();
*CURJOB.get_or_init(|| Mutex::new(-1)).lock().unwrap() = pj;
setprevjob();
let curjob = *CURJOB.get_or_init(|| Mutex::new(-1)).lock().unwrap();
if (curjob != -1 && curjob == thisjob)
|| (curjob != -1 && !(inuse(curjob) && curjob != thisjob))
{
let pj = *PREVJOB.get_or_init(|| Mutex::new(-1)).lock().unwrap();
*CURJOB.get_or_init(|| Mutex::new(-1)).lock().unwrap() = pj;
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 myjobtab_is_oldjobtab = !OLDJOBTAB
.get_or_init(|| Mutex::new(Vec::new()))
.lock()
.expect("oldjobtab poisoned")
.is_empty();
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 zwarnnam_or_zwarn = |prog: &str, msg: &str| {
if prog.is_empty() {
zwarn(msg);
} else {
zwarnnam(prog, msg);
}
};
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 {
zwarnnam_or_zwarn(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 && (myjobtab_is_oldjobtab || 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[idx..])); }
return -1; }
let rest = &s[idx..];
if let Some(jn) = findjobnam(rest, &myjobtab, mymaxjob, thisjob) {
return jn; }
if !posixbuiltins {
zwarnnam_or_zwarn(prog, &format!("job not found: {}", rest)); }
-1 }
#[allow(non_upper_case_globals)]
pub static hackzero: std::sync::atomic::AtomicPtr<libc::c_char> =
std::sync::atomic::AtomicPtr::new(std::ptr::null_mut());
#[allow(non_upper_case_globals)]
pub static hackspace: std::sync::atomic::AtomicUsize = std::sync::atomic::AtomicUsize::new(0);
pub fn init_jobs(argv: &[String], envp: &[String]) -> JobTable {
let _ = (argv, envp);
let table = JobTable::new();
#[cfg(target_os = "macos")]
unsafe {
extern "C" {
fn _NSGetArgv() -> *mut *mut *mut libc::c_char;
}
let mut av = *_NSGetArgv(); if !av.is_null() && !(*av).is_null() {
let zero = *av;
hackzero.store(zero, std::sync::atomic::Ordering::Relaxed); let mut p = zero.add(libc::strlen(zero)); av = av.add(1);
while !(*av).is_null() {
let q = *av; if q != p.add(1) {
break; }
p = q.add(libc::strlen(q)); av = av.add(1);
}
hackspace.store(
p.offset_from(zero) as usize, std::sync::atomic::Ordering::Relaxed,
);
}
}
#[cfg(target_os = "linux")]
{
if let Ok(stat_line) = std::fs::read_to_string("/proc/self/stat") {
if let Some(rest) = stat_line.rfind(')').map(|i| &stat_line[i + 1..]) {
let fields: Vec<&str> = rest.split_whitespace().collect();
let arg_start = fields.get(45).and_then(|f| f.parse::<usize>().ok());
let arg_end = fields.get(46).and_then(|f| f.parse::<usize>().ok());
if let (Some(start), Some(end)) = (arg_start, arg_end) {
if start != 0 && end > start {
hackzero.store(start as *mut libc::c_char, std::sync::atomic::Ordering::Relaxed);
hackspace.store(end - start - 1, std::sync::atomic::Ordering::Relaxed);
}
}
}
}
}
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 func == BIN_WAIT && crate::dash_mode::bash_mode() && argv.first().is_some_and(|a| a == "-f") {
return bin_fg(name, &argv[1..], ops, func);
}
let disown_all = func == BIN_DISOWN && OPT_ISSET(ops, b'a');
if disown_all && !argv.is_empty() {
zwarnnam(name, &format!("argument not meaningful with -a: {}", argv[0])); return 1; }
if OPT_ISSET(ops, b'Z') {
if isset(crate::ported::zsh_h::RESTRICTED) {
zwarnnam(name, "-Z is restricted");
return 1;
}
if argv.is_empty() || argv.len() > 1 {
zwarnnam(name, "-Z requires one argument"); return 1; }
queue_signals(); let title = &argv[0];
let hz = hackzero.load(Ordering::Relaxed);
if !hz.is_null() {
let hs = hackspace.load(Ordering::Relaxed);
let bytes = title.as_bytes();
let len = bytes.len().min(hs);
unsafe {
std::ptr::copy_nonoverlapping(bytes.as_ptr(), hz as *mut u8, len); std::ptr::write_bytes((hz as *mut u8).add(len), 0, hs - len); }
}
#[cfg(target_os = "linux")]
unsafe {
let cs = std::ffi::CString::new(title.as_str()).unwrap_or_default();
libc::prctl(libc::PR_SET_NAME, cs.as_ptr() as libc::c_ulong, 0, 0, 0); }
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();
let table = JOBTAB.get_or_init(|| Mutex::new(Vec::new()));
#[cfg(unix)]
if func == BIN_WAIT && crate::dash_mode::bash_mode() && argv.first().is_some_and(|a| a == "-n") {
let mut status: libc::c_int = 0;
let pid = loop {
let pid = unsafe { libc::waitpid(-1, &mut status, 0) };
if pid < 0 && std::io::Error::last_os_error().raw_os_error() == Some(libc::EINTR) {
continue;
}
break pid;
};
unqueue_signals();
if pid < 0 {
return 127;
}
if let Ok(mut tab) = table.lock() {
update_bg_job(&mut tab, pid, status);
scanjobs(&mut tab);
}
return if libc::WIFEXITED(status) {
libc::WEXITSTATUS(status)
} else {
128 + libc::WTERMSIG(status)
};
}
{
let reaped = wait_for_processes();
let mut tab = table.lock().expect("jobtab poisoned");
for (pid, status) in reaped {
update_bg_job(&mut tab, pid, status);
}
}
for _ in 0..crate::ported::jobs::CHLD_TRAP_PENDING.swap(0, std::sync::atomic::Ordering::SeqCst) {
crate::ported::signals::dotrap(libc::SIGCHLD);
}
let pseudo = PSEUDO_SIG_TRAP_PENDING.swap(0, Ordering::SeqCst);
if pseudo != 0 {
crate::ported::signals::dotrap(pseudo); if crate::ported::utils::errflag.load(Ordering::Relaxed) != 0 {
crate::ported::builtin::BREAKS.store(
crate::ported::builtin::LOOPS.load(Ordering::Relaxed),
Ordering::Relaxed,
); }
check_cursh_sig(&table.lock().expect("jobtab poisoned"), pseudo); }
if !crate::ported::zsh_h::isset(crate::ported::zsh_h::NOTIFY) {
if let Some(jt) = JOBTAB.get() {
let mut guard = jt.lock().unwrap();
scanjobs(&mut guard); }
}
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 oldmaxjob = *OLDMAXJOB.get_or_init(|| Mutex::new(0)).lock().unwrap();
let (t, curmaxjob, thisjob): (Vec<job>, usize, i32) =
if !isset(MONITOR) && oldmaxjob != 0 {
let old = OLDJOBTAB
.get_or_init(|| Mutex::new(Vec::new()))
.lock()
.expect("oldjobtab poisoned")
.clone();
let n = (oldmaxjob + 1).min(old.len()); (old, n, 0) } else {
let t = table.lock().expect("jobtab poisoned").clone();
let n = t.len();
(t, n, *THISJOB.get_or_init(|| Mutex::new(-1)).lock().unwrap()) };
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 == thisjob {
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
};
let s = printjob(j, job, lng, curjob_opt, prevjob_opt, false);
if !s.is_empty() {
println!("{}", s);
}
}
}
unqueue_signals(); return 0; }
if disown_all {
let maxjob = *MAXJOB.get_or_init(|| Mutex::new(0)).lock().unwrap();
for job in 0..=maxjob {
if !OLDJOBTAB.get_or_init(|| Mutex::new(Vec::new())).lock().unwrap().is_empty() {
zwarnnam(name, "can't manipulate jobs in subshell"); unqueue_signals(); return 1; }
let mut tab = table.lock().expect("jobtab poisoned");
let Some(j) = tab.get_mut(job) else { continue };
if (j.stat & stat::INUSE) == 0 || (j.stat & stat::NOPRINT) != 0 {
continue; }
if (j.stat & stat::SUPERJOB) != 0 {
j.stat |= crate::ported::zsh_h::STAT_DISOWN; continue; }
if (j.stat & stat::STOPPED) != 0 {
zwarnnam(
name,
&format!(
"warning: job is suspended, use `kill -CONT -{}' to resume",
j.gleader
),
);
}
deletejob(&mut tab, job, true); drop(tab);
let cj = *CURJOB.get_or_init(|| Mutex::new(-1)).lock().unwrap();
if cj == job as i32 {
let pj = *PREVJOB.get_or_init(|| Mutex::new(-1)).lock().unwrap();
*CURJOB.get_or_init(|| Mutex::new(-1)).lock().unwrap() = pj;
}
setprevjob();
}
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();
let cur_noprint = curjob >= 0
&& table
.lock()
.expect("jobtab poisoned")
.get(curjob as usize)
.map(|j| (j.stat & stat::NOPRINT) != 0)
.unwrap_or(true);
if curjob < 0 || cur_noprint {
zwarnnam(name, "no current job"); unqueue_signals();
return 1; }
if !OLDJOBTAB.get_or_init(|| Mutex::new(Vec::new())).lock().unwrap().is_empty() {
zwarnnam(name, "can't manipulate jobs in subshell"); unqueue_signals();
return 1; }
if func == BIN_DISOWN {
let mut tab = table.lock().expect("jobtab poisoned");
if (curjob as usize) < tab.len() {
deletejob(&mut tab, curjob as usize, true); }
drop(tab);
let pj = *PREVJOB.get_or_init(|| Mutex::new(-1)).lock().unwrap();
*CURJOB.get_or_init(|| Mutex::new(-1)).lock().unwrap() = pj;
setprevjob();
unqueue_signals();
return 0;
}
if curjob >= 0 {
let _ = killjb(curjob as usize, libc::SIGCONT);
}
unqueue_signals();
return 0;
}
if func == BIN_WAIT {
#[cfg(unix)]
loop {
let mut status: libc::c_int = 0;
let pid = unsafe { libc::waitpid(-1, &mut status, 0) };
if pid < 0
&& std::io::Error::last_os_error().raw_os_error() == Some(libc::EINTR)
{
continue;
}
if pid > 0 {
if let Ok(mut tab) = table.lock() {
update_bg_job(&mut tab, pid, status);
}
crate::ported::jobs::CHLD_TRAP_PENDING.store(0, std::sync::atomic::Ordering::SeqCst);
let chld_trapped = crate::ported::signals::sigtrapped
.lock()
.ok()
.and_then(|g| g.get(libc::SIGCHLD as usize).copied())
.unwrap_or(0);
let chld_string_trap = crate::ported::builtin::traps_table()
.lock()
.ok()
.map(|t| t.contains_key("CHLD") || t.contains_key("SIGCHLD"))
.unwrap_or(false);
if chld_trapped != 0 || chld_string_trap {
crate::ported::signals::dotrap(libc::SIGCHLD);
}
} else {
break;
}
}
if let Ok(mut tab) = table.lock() {
scanjobs(&mut tab);
}
unqueue_signals();
return 0;
}
unqueue_signals();
return 0;
}
for arg in argv {
if func == BIN_WAIT && isanum(arg) {
let pid = std::ffi::CString::new(arg.as_str())
.map(|c| unsafe { libc::atoi(c.as_ptr()) })
.unwrap_or(0);
{
let mut status: libc::c_int = 0;
let q = crate::ported::signals_h::queue_signal_level();
crate::ported::signals_h::dont_queue_signals();
let mut r;
let mut interrupted = None;
let no_proc = pid <= 0;
loop {
crate::ported::signals::last_signal.store(-1, Ordering::Relaxed); if no_proc {
r = -1;
break;
}
r = unsafe { libc::waitpid(pid, &mut status, 0) };
if r != -1
|| std::io::Error::last_os_error().raw_os_error() != Some(libc::EINTR)
{
break;
}
let ls = crate::ported::signals::last_signal.load(Ordering::Relaxed);
if ls != libc::SIGCHLD
&& ls >= 0
&& (crate::ported::signals::sigtrapped
.lock()
.ok()
.and_then(|g| g.get(ls as usize).copied())
.unwrap_or(0)
& crate::ported::zsh_h::ZSIG_TRAPPED)
!= 0
{
interrupted = Some(128 + ls); break;
}
}
crate::ported::signals_h::restore_queue_signals(q); if let Some(st) = interrupted {
returnval = st;
continue; }
if r == -1 {
let err = std::io::Error::last_os_error();
if no_proc || err.raw_os_error() == Some(libc::ECHILD) {
if let Some(bg) = getbgstatus(pid).or_else(|| {
crate::reaped_status::take(pid).map(|st| {
if libc::WIFSIGNALED(st) {
0o200 | libc::WTERMSIG(st)
} else {
libc::WEXITSTATUS(st)
}
})
}) {
returnval = bg;
} else {
if !isset(POSIXBUILTINS) {
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);
}
if let Ok(mut tab) = table.lock() {
update_bg_job(&mut tab, pid, status);
scanjobs(&mut tab);
}
for _ in 0..crate::ported::jobs::CHLD_TRAP_PENDING.swap(0, std::sync::atomic::Ordering::SeqCst) {
crate::ported::signals::dotrap(libc::SIGCHLD);
}
}
}
continue; }
if func != BIN_JOBS
&& !OLDJOBTAB.get_or_init(|| Mutex::new(Vec::new())).lock().unwrap().is_empty()
{
zwarnnam(name, "can't manipulate jobs in subshell"); unqueue_signals();
return 1; }
let p = getjob(arg, name);
if p < 0 {
returnval = 127;
break;
}
let jstat = table
.lock()
.expect("jobtab poisoned")
.get(p as usize)
.map(|j| j.stat)
.unwrap_or(0);
if (jstat & stat::INUSE) == 0 || (jstat & stat::NOPRINT) != 0 {
if !isset(POSIXBUILTINS) {
zwarnnam(name, &format!("{}: no such job", arg)); }
unqueue_signals(); return 127; }
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 {
{
let tab = table.lock().expect("jobtab poisoned");
if let Some(j) = tab.get(p as usize) {
if j.procs.is_empty() && j.auxprocs.is_empty() {
drop(tab); zwaitjob(p as usize, 1); returnval = LASTVAL2.load(Ordering::SeqCst); continue;
}
}
}
let q = crate::ported::signals_h::queue_signal_level();
crate::ported::signals_h::dont_queue_signals();
let mut interrupted = None;
loop {
for _ in 0..crate::ported::jobs::CHLD_TRAP_PENDING.swap(0, std::sync::atomic::Ordering::SeqCst) {
crate::ported::signals::dotrap(libc::SIGCHLD);
}
let next_pid = {
let tab = table.lock().expect("jobtab poisoned");
match tab.get(p as usize) {
Some(j) if (j.stat & stat::INUSE) != 0 && !j.is_done() => j
.procs
.iter()
.chain(j.auxprocs.iter())
.find(|pr| pr.status == SP_RUNNING)
.map(|pr| pr.pid),
_ => None,
}
};
let pid = match next_pid {
Some(pid) => pid,
None => break,
};
let mut status: libc::c_int = 0;
let mut r;
loop {
crate::ported::signals::last_signal.store(-1, Ordering::Relaxed); r = unsafe { libc::waitpid(pid, &mut status, 0) };
if r != -1
|| std::io::Error::last_os_error().raw_os_error() != Some(libc::EINTR)
{
break;
}
let ls = crate::ported::signals::last_signal.load(Ordering::Relaxed);
if ls != libc::SIGCHLD
&& ls >= 0
&& (crate::ported::signals::sigtrapped
.lock()
.ok()
.and_then(|g| g.get(ls as usize).copied())
.unwrap_or(0)
& crate::ported::zsh_h::ZSIG_TRAPPED)
!= 0
{
interrupted = Some(128 + ls); break;
}
}
if interrupted.is_some() {
break; }
let mut tab = table.lock().expect("jobtab poisoned");
if r == pid {
update_bg_job(&mut tab, pid, status);
} else if let Some(reaped) = crate::reaped_status::take(pid) {
update_bg_job(&mut tab, pid, reaped);
} else {
let mut marked = false;
if let Some(j) = tab.get_mut(p as usize) {
for pr in j.procs.iter_mut().chain(j.auxprocs.iter_mut()) {
if pr.pid == pid && pr.status == SP_RUNNING {
pr.status = 0;
}
}
marked = true;
}
if marked {
update_job(&mut tab, p as usize);
}
}
}
for _ in 0..crate::ported::jobs::CHLD_TRAP_PENDING.swap(0, std::sync::atomic::Ordering::SeqCst) {
crate::ported::signals::dotrap(libc::SIGCHLD);
}
let pseudo = PSEUDO_SIG_TRAP_PENDING.swap(0, Ordering::SeqCst);
if pseudo != 0 {
crate::ported::signals::dotrap(pseudo); if crate::ported::utils::errflag.load(Ordering::Relaxed) != 0 {
crate::ported::builtin::BREAKS.store(
crate::ported::builtin::LOOPS.load(Ordering::Relaxed),
Ordering::Relaxed,
); }
check_cursh_sig(&table.lock().expect("jobtab poisoned"), pseudo); }
crate::ported::signals_h::restore_queue_signals(q); if let Some(st) = interrupted {
returnval = st;
break;
}
returnval = LASTVAL2.load(Ordering::SeqCst);
if let Ok(mut tab) = table.lock() {
crate::exec_jobs::printjob_delete_tail(&mut tab, p as usize, p as usize, 1);
}
} 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();
let s = printjob(
j,
p as usize,
lng,
if curjob >= 0 {
Some(curjob as usize)
} else {
None
},
if prevjob >= 0 {
Some(prevjob as usize)
} else {
None
},
false,
);
if !s.is_empty() {
println!("{}", s);
}
}
} else if func == BIN_DISOWN {
let mut tab = table.lock().expect("jobtab poisoned");
if let Some(j) = tab.get_mut(p as usize) {
if (j.stat & stat::STOPPED) != 0 {
zwarnnam(
name,
&format!(
"warning: job is suspended, use `kill -CONT -{}' to resume",
j.gleader
),
); }
deletejob(&mut tab, p as usize, true); }
drop(tab);
let cj = *CURJOB.get_or_init(|| Mutex::new(-1)).lock().unwrap();
if cj == p {
let pj = *PREVJOB.get_or_init(|| Mutex::new(-1)).lock().unwrap();
*CURJOB.get_or_init(|| Mutex::new(-1)).lock().unwrap() = pj;
}
setprevjob();
}
}
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;
let kill_signame_lookup = |signame: &str| -> Option<i32> {
let upper = signame.to_ascii_uppercase(); let bare = upper.strip_prefix("SIG").unwrap_or(&upper); if bare == "0" {
return Some(0); }
sigs_number(bare).or_else(|| crate::ported::signals::rtsigno(bare))
};
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 crate::extensions::dash_mode::dash_faithful()
&& crate::extensions::dash_mode::dash_number(token).is_none()
{
zwarnnam(nam, &format!("Illegal number: {}", token));
return 2;
}
if let Ok(n) = token.parse::<i32>() {
let s = (n & !0o200) as i32; let in_range = (1..=crate::ported::signals_h::SIGCOUNT).contains(&s);
let rt_name = || {
#[cfg(target_os = "linux")]
{
let nm = crate::ported::signals::rtsigname(s);
(!nm.is_empty()).then_some(nm)
}
#[cfg(not(target_os = "linux"))]
None::<String>
};
if let Some(name) = sigs_name(s).filter(|_| in_range) {
println!("{}", name);
} else if let Some(name) = rt_name() {
println!("{}", name);
} else {
println!("{}", n); }
} else {
let sign_stripped =
token.strip_prefix('-').or_else(|| token.strip_prefix('+'));
if let Some(stripped) = sign_stripped {
let upper = stripped.to_ascii_uppercase();
let bare = upper.strip_prefix("SIG").unwrap_or(&upper);
zwarnnam(nam, &format!("unknown signal: SIG{}", bare)); returnval += 1;
} else {
let upper = token.to_ascii_uppercase();
let bare = upper.strip_prefix("SIG").unwrap_or(&upper);
let found = sigs_number(bare)
.or_else(|| crate::ported::signals::rtsigno(bare));
if let Some(n) = found {
println!("{}", n); } else {
zwarnnam(nam, &format!("unknown signal: SIG{}", bare)); returnval += 1;
}
}
}
}
return returnval; }
let sigs = crate::extensions::emulation_output::kill_l_signals();
if let Some(rendered) = crate::extensions::emulation_output::kill_list(&sigs) {
print!("{rendered}");
return 0;
}
let names: Vec<&str> = sigs.iter().map(|(_, n)| n.as_str()).collect();
println!("{}", names.join(" "));
return 0; }
if body == "L" {
use crate::ported::signals_h::SIGCOUNT;
#[cfg(target_os = "linux")]
let width: usize = if libc::SIGRTMAX() >= 100 { 3 } else { 2 }; #[cfg(not(target_os = "linux"))]
let width: usize = if SIGCOUNT >= 100 { 3 } else { 2 }; let columns = crate::ported::utils::ZTERM_COLUMNS.load(std::sync::atomic::Ordering::Relaxed);
let cols: i32 = if columns >= 30 {
if columns < 90 { columns / 15 } else { 6 }
} else {
1
};
#[cfg(target_os = "linux")]
let last_plain = SIGCOUNT + 1; #[cfg(not(target_os = "linux"))]
let last_plain = SIGCOUNT;
let mut sig = 1;
while sig < last_plain {
print!(
"{:>width$} {:<10}{}",
sig,
sigs_name(sig).unwrap_or(""),
if sig % cols != 0 { ' ' } else { '\n' } );
sig += 1;
}
#[cfg(target_os = "linux")]
{
sig = libc::SIGRTMIN();
while sig < libc::SIGRTMAX() {
print!(
"{:>width$} {:<10}{}",
sig,
crate::ported::signals::rtsigname(sig),
if (sig - libc::SIGRTMIN() + SIGCOUNT + 1) % cols != 0 {
' '
} else {
'\n'
} );
sig += 1;
}
println!("{:>width$} RTMAX", sig); }
#[cfg(not(target_os = "linux"))]
println!("{:>width$} {}", sig, sigs_name(sig).unwrap_or("")); 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();
if name.is_empty() {
zwarnnam(nam, "-: signal name expected");
return 1;
}
match kill_signame_lookup(name) {
Some(n) => sig = n,
None => {
let upper = name.to_ascii_uppercase();
let bare = upper.strip_prefix("SIG").unwrap_or(&upper);
zwarnnam(nam, &format!("unknown signal: SIG{}", bare)); zwarnnam(nam, "type kill -L for a list of signals"); 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; }
if body.is_empty() {
zwarnnam(nam, "-: signal name expected"); return 1; }
let upper = body.to_ascii_uppercase();
let bare = upper.strip_prefix("SIG").unwrap_or(&upper);
match kill_signame_lookup(body) {
Some(n) => {
sig = n;
got_sig = true;
idx += 1;
}
None => {
zwarnnam(nam, &format!("unknown signal: SIG{}", bare)); zwarnnam(nam, "type kill -L for a list of signals");
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 {
let errno = std::io::Error::last_os_error()
.raw_os_error()
.unwrap_or(libc::EINVAL);
let mut errmsg = crate::ported::compat::strerror(errno);
if errno != libc::EIO {
if let Some(c) = errmsg.chars().next() {
errmsg = format!(
"{}{}",
c.to_ascii_lowercase(),
&errmsg[c.len_utf8()..]
);
}
}
zwarnnam(nam, &format!("kill {} failed: {}", arg, errmsg));
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 {
let errno = std::io::Error::last_os_error()
.raw_os_error()
.unwrap_or(libc::EINVAL);
let mut errmsg = crate::ported::compat::strerror(errno);
if errno != libc::EIO {
if let Some(c) = errmsg.chars().next() {
errmsg = format!(
"{}{}",
c.to_ascii_lowercase(),
&errmsg[c.len_utf8()..]
);
}
}
zwarnnam(nam, &format!("kill {} failed: {}", arg, errmsg)); returnval += 1; }
}
Err(_) => {
zwarnnam(nam, &format!("illegal pid: {}", arg));
returnval += 1; }
}
}
}
if returnval < 126 {
returnval } else {
1 }
}
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); let upper;
let s = if crate::extensions::emulation_startup::personality()
== crate::extensions::emulation_startup::Personality::Zsh
{
s
} else {
upper = s.to_uppercase();
upper.as_str()
};
match s {
"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 {
if sig == crate::ported::signals_h::SIGZERR {
return "ZERR".to_string();
}
if sig == crate::ported::signals_h::SIGDEBUG {
return "DEBUG".to_string();
}
if let Some(name) = sigs_name(sig) {
return name.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 sync_mypgrp = |v: i32| {
crate::ported::modules::clone::mypgrp.store(v, Ordering::Relaxed);
*MYPGRP.get_or_init(|| Mutex::new(0)).lock().unwrap() = v;
};
let mut mypgrp = unsafe { libc::getpgrp() }; sync_mypgrp(mypgrp); 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() }; sync_mypgrp(mypgrp); if mypgrp == mypid {
if !interact {
break;
} signal_setmask(&oldset); crate::ported::utils::attachtty(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() }; sync_mypgrp(mypgrp); 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; sync_mypgrp(mypgrp); crate::ported::utils::attachtty(mypgrp); acquired = true;
} else {
opt_state_set("monitor", false); }
}
sync_mypgrp(mypgrp); 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();
pub static OLDJOBTAB: OnceLock<Mutex<Vec<job>>> = OnceLock::new();
pub 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: i32) {
let table = JOBTAB.get_or_init(|| Mutex::new(Vec::new()));
let idx = usize::try_from(jn).ok();
let forked = idx
.and_then(|i| {
table
.lock()
.unwrap_or_else(|e| e.into_inner())
.get(i)
.map(|j| !j.procs.is_empty() || !j.auxprocs.is_empty())
})
.unwrap_or(false);
if forked {
zwaitjob(jn as usize, 0);
} else {
if let Some(i) = idx {
let mut tab = table.lock().unwrap_or_else(|e| e.into_inner());
if i < tab.len() {
deletejob(&mut tab, i, 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; }
let lastval_str = lastval.to_string();
let (shadowed, current) = crate::ported::params::paramtab()
.read()
.ok()
.and_then(|t| {
t.get("pipestatus").map(|pm| {
(
(pm.node.flags & crate::ported::zsh_h::PM_SPECIAL as i32) == 0,
(pm.node.flags & crate::ported::zsh_h::PM_UNSET as i32) == 0
&& pm.u_arr.as_deref() == Some(std::slice::from_ref(&lastval_str)),
)
})
})
.unwrap_or((false, false));
if !shadowed && !current {
crate::ported::params::setaparam("pipestatus", vec![lastval_str]);
}
}
}
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 super::*;
use crate::ported::zsh_h::{STAT_BUILTIN, STAT_CHANGED, STAT_DONE, STAT_STOPPED, STAT_TIMED};
#[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(std::slice::from_mut(&mut job), 0);
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(std::slice::from_mut(&mut job), 0));
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(std::slice::from_mut(&mut job), 0);
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(std::slice::from_mut(&mut job), 0);
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_status_lines_omit_timing_for_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, 0, Some(1), None, false);
assert!(
out.contains("echo hi"),
"expected status line; got: {:?}",
out
);
assert_eq!(out.matches("echo hi").count(), 1, "no dumptime line in the formatter; 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(std::slice::from_mut(&mut job), 0));
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(std::slice::from_mut(&mut job), 0);
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(std::slice::from_mut(&mut job), 0));
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 update_job_subjob_stop_marks_superjob_and_curjob() {
let _g = crate::test_util::global_state_lock();
let mut tab = vec![job::default(); 3];
tab[1].stat = stat::INUSE | stat::SUBJOB;
let mut p = process::new(7101);
p.status = 0x117f; tab[1].procs.push(p);
tab[2].stat = stat::INUSE | stat::SUPERJOB;
tab[2].other = 1;
tab[2].gleader = 0x7fff_fff0; let old_cur = std::mem::replace(
&mut *CURJOB.get_or_init(|| Mutex::new(-1)).lock().unwrap(),
5,
);
let old_prev = std::mem::replace(
&mut *PREVJOB.get_or_init(|| Mutex::new(-1)).lock().unwrap(),
-1,
);
assert!(update_job(&mut tab, 1));
assert_eq!(tab[1].stat & (stat::CHANGED | stat::STOPPED), stat::CHANGED | stat::STOPPED);
assert_eq!(
tab[2].stat & (stat::CHANGED | stat::STOPPED),
stat::CHANGED | stat::STOPPED,
"c:528 — the super-job is marked stopped immediately"
);
assert_eq!(*CURJOB.get().unwrap().lock().unwrap(), 2, "c:531 curjob = i");
assert_eq!(*PREVJOB.get().unwrap().lock().unwrap(), 5, "c:530 prevjob = old curjob");
*CURJOB.get().unwrap().lock().unwrap() = old_cur;
*PREVJOB.get().unwrap().lock().unwrap() = old_prev;
}
#[test]
fn update_job_thisjob_sets_lastval_and_pipestats() {
let _g = crate::test_util::global_state_lock();
let mut tab = vec![job::default(); 2];
tab[1].stat = stat::INUSE;
let mut p1 = process::new(7201);
p1.status = 0;
let mut p2 = process::new(7202);
p2.status = 3 << 8;
tab[1].procs = vec![p1, p2];
let old_this = std::mem::replace(
&mut *THISJOB.get_or_init(|| Mutex::new(-1)).lock().unwrap(),
1,
);
crate::ported::builtin::LASTVAL.store(-1, Ordering::Relaxed);
assert!(update_job(&mut tab, 1));
assert_eq!(crate::ported::builtin::LASTVAL.load(Ordering::Relaxed), 3);
assert_eq!(*NUMPIPESTATS.get().unwrap().lock().unwrap(), 2);
assert_eq!(PIPESTATS.get().unwrap().lock().unwrap()[..2], [0, 3]);
*THISJOB.get().unwrap().lock().unwrap() = old_this;
}
#[test]
fn waitonejob_procsless_slot_is_deleted_and_pipestats_set() {
let _g = crate::test_util::global_state_lock();
let mut tab = vec![job::default(); 2];
tab[1].stat = stat::INUSE | stat::DONE;
*JOBTAB.get_or_init(|| Mutex::new(Vec::new())).lock().unwrap() = tab;
crate::ported::builtin::LASTVAL.store(9, Ordering::Relaxed);
waitonejob(1);
assert_eq!(JOBTAB.get().unwrap().lock().unwrap()[1].stat, 0);
assert_eq!(*NUMPIPESTATS.get().unwrap().lock().unwrap(), 1);
assert_eq!(PIPESTATS.get().unwrap().lock().unwrap()[0], 9);
crate::ported::builtin::LASTVAL.store(4, Ordering::Relaxed);
waitonejob(-1);
assert_eq!(PIPESTATS.get().unwrap().lock().unwrap()[0], 4);
*JOBTAB.get().unwrap().lock().unwrap() = Vec::new();
}
#[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, 0, Some(1), None, false);
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![jobfile {
name: Some("/tmp/foo".to_string()),
fd: 0,
is_fd: 0,
}];
assert!(havefiles(&tab));
tab[1].filelist.clear();
assert!(!havefiles(&tab));
tab[2].stat = 0;
tab[2].filelist = vec![jobfile {
name: Some("/tmp/bar".to_string()),
fd: 0,
is_fd: 0,
}];
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, 0, 0);
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].is_fd, 0);
assert_eq!(job.filelist[0].name.as_deref(), Some("/tmp/zshrs-test.X"));
assert_eq!(job.filelist[1].is_fd, 1);
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), "death of child");
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), "done");
}
#[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 tab = vec![job::new()];
tab[0].pwd = Some("/tmp/deletejob-pwd".to_string());
tab[0].other = 42;
tab[0].stty_in_env = 1;
tab[0].stat = stat::SUPERJOB;
deletejob(&mut tab, 0, false);
assert_eq!(tab[0].pwd, None, "c:1525 — pwd cleared via freejob chain");
assert_eq!(tab[0].other, 0, "c:1525 — other cleared");
assert_eq!(tab[0].stty_in_env, 0, "c:1525 — stty_in_env cleared");
assert_eq!(tab[0].stat, 0, "c:1525 — stat cleared");
}
macro_rules! finishes_while_jobtab_is_held {
($f:expr) => {{
let _held = JOBTAB
.get_or_init(|| Mutex::new(Vec::new()))
.lock()
.unwrap_or_else(|e| e.into_inner());
let (tx, rx) = std::sync::mpsc::channel();
let f = $f;
std::thread::spawn(move || {
let _ = tx.send(f());
});
rx.recv_timeout(Duration::from_secs(5))
.expect("deadlock: the call re-locked JOBTAB while the caller held its guard")
}};
}
#[test]
fn deletejob_of_superjob_orphans_subjob_without_locking_jobtab() {
let _g = crate::test_util::global_state_lock();
let tab = finishes_while_jobtab_is_held!(|| {
let mut tab = vec![job::default(); 3];
tab[1].stat = stat::INUSE | stat::SUBJOB;
tab[2].stat = stat::INUSE | stat::SUPERJOB;
tab[2].other = 1;
deletejob(&mut tab, 2, false);
tab
});
assert_eq!(tab[2].stat, 0, "c:1525 — the superjob slot is freed");
assert_ne!(tab[1].stat & STAT_SUBJOB_ORPHANED, 0, "c:1522 — subjob marked orphaned");
}
#[test]
fn freejob_wassuper_deletes_partner_slot_through_the_slice() {
let _g = crate::test_util::global_state_lock();
let tab = finishes_while_jobtab_is_held!(|| {
let mut tab = vec![job::default(); 3];
tab[1].stat = stat::INUSE;
tab[1].pwd = Some("/partner".to_string());
tab[2].stat = stat::INUSE | stat::WASSUPER;
tab[2].other = 1;
deletejob(&mut tab, 2, false);
tab
});
assert_eq!(tab[2].stat, 0);
assert_eq!(tab[1].stat, 0, "c:1484 — partner slot deleted");
assert_eq!(tab[1].pwd, None, "c:1484 — partner slot freed");
}
#[test]
fn printjob_synch_done_superjob_is_deleted_with_jobtab_held() {
let _g = crate::test_util::global_state_lock();
let tab = finishes_while_jobtab_is_held!(|| {
let mut tab = vec![job::default(); 3];
tab[1].stat = stat::INUSE | stat::SUBJOB;
tab[2].stat =
stat::INUSE | stat::SUPERJOB | stat::DONE | stat::CHANGED | stat::NOPRINT;
tab[2].other = 1;
let mut p = process::new(7);
p.status = 0;
tab[2].procs.push(p);
crate::exec_jobs::printjob_synch(&mut tab, 2, 0, 1);
tab
});
assert_eq!(tab[2].stat, 0, "c:1356 — done job deleted by the tail");
assert_ne!(tab[1].stat & STAT_SUBJOB_ORPHANED, 0);
}
#[test]
fn printjob_synch_running_job_clears_changed_only() {
let _g = crate::test_util::global_state_lock();
let mut tab = vec![job::default(); 2];
tab[1].stat = stat::INUSE | stat::CHANGED;
tab[1].procs.push(process::new(9));
crate::exec_jobs::printjob_synch(&mut tab, 1, 0, 1);
assert_eq!(tab[1].stat, stat::INUSE);
assert_eq!(tab[1].procs.len(), 1);
}
#[test]
fn handle_sub_stopped_subjob_prints_superjob_with_jobtab_held() {
let _g = crate::test_util::global_state_lock();
let old_cur = *CURJOB.get_or_init(|| Mutex::new(-1)).lock().unwrap();
let (ret, tab) = finishes_while_jobtab_is_held!(|| {
let mut tab = vec![job::default(); 3];
let mut sub = process::new(11);
sub.status = 0x7f | (libc::SIGTSTP << 8); tab[1].stat = stat::INUSE | stat::SUBJOB | stat::STOPPED | stat::CHANGED;
tab[1].procs.push(sub);
tab[2].stat = stat::INUSE | stat::SUPERJOB;
tab[2].other = 1;
tab[2].procs.push(process::new(12));
let ret = handle_sub(&mut tab, 2, false);
(ret, tab)
});
assert_eq!(ret, 1, "c:338");
assert_ne!(tab[2].stat & stat::STOPPED, 0, "c:331");
assert!(tab[2].procs[0].is_stopped(), "c:335 — running proc takes the stopped status");
assert_eq!(tab[1].stat & stat::CHANGED, 0, "c:1364 — printjob ran");
assert_eq!(*CURJOB.get().unwrap().lock().unwrap(), 2, "c:336");
*CURJOB.get().unwrap().lock().unwrap() = old_cur;
}
#[test]
fn waitjobs_returns_for_finished_job_and_clears_thisjob() {
let _g = crate::test_util::global_state_lock();
let mut tab = vec![job::default(); 2];
tab[1].stat = stat::INUSE | stat::DONE;
tab[1].procs.push(process::new(5));
*JOBTAB.get_or_init(|| Mutex::new(Vec::new())).lock().unwrap() = tab;
let old_this = *THISJOB.get_or_init(|| Mutex::new(-1)).lock().unwrap();
*THISJOB.get().unwrap().lock().unwrap() = 1;
waitjobs();
assert_eq!(*THISJOB.get().unwrap().lock().unwrap(), -1);
*THISJOB.get().unwrap().lock().unwrap() = old_this;
*JOBTAB.get().unwrap().lock().unwrap() = Vec::new();
}
#[test]
fn freejob_resets_all_per_job_state_fields() {
let _g = crate::test_util::global_state_lock();
let mut tab = vec![job::new()];
let jn = &mut tab[0];
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 tab, 0, false);
let jn = &tab[0];
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");
}
#[test]
fn initjob_skips_index_zero_reserved_for_shell() {
let _g = crate::test_util::global_state_lock();
let mut jt: Vec<job> = vec![job::new(), job::new(), job::new()];
let idx = initjob(&mut jt);
assert_ne!(
idx, 0,
"initjob must NOT return index 0 (shell slot); got {idx}"
);
assert!(idx >= 1, "first available slot is index >= 1");
}
#[test]
fn initjob_returns_negative_one_on_full_table() {
let _g = crate::test_util::global_state_lock();
let mut jt: Vec<job> = Vec::new();
for _ in 0..4 {
let mut j = job::new();
j.stat = stat::INUSE;
jt.push(j);
}
let before = jt.len();
let idx = initjob(&mut jt);
assert_eq!(idx, before, "fresh slot at the grown end of jobtab");
assert_eq!(jt.len(), before + 1, "jobtab grew by one");
}
#[test]
fn findproc_invalid_pid_returns_none() {
let _g = crate::test_util::global_state_lock();
let jt: Vec<job> = vec![job::new()];
assert!(findproc(&jt, -1, false).is_none());
}
#[test]
fn findproc_empty_jobtab_no_panic() {
let _g = crate::test_util::global_state_lock();
let jt: Vec<job> = Vec::new();
assert!(findproc(&jt, 1234, false).is_none());
}
#[test]
fn getsigname_zero_returns_exit_pseudo_signal() {
let _g = crate::test_util::global_state_lock();
assert_eq!(getsigname(0), "EXIT");
}
#[test]
fn getsigname_sighup_returns_hup_without_prefix() {
let _g = crate::test_util::global_state_lock();
assert_eq!(getsigname(libc::SIGHUP), "HUP");
}
#[test]
fn printhhmmss_zero_returns_zero_seconds() {
let _g = crate::test_util::global_state_lock();
assert_eq!(printhhmmss(0.0), "0.000");
}
#[test]
fn printhhmmss_sub_minute_uses_three_decimals() {
let _g = crate::test_util::global_state_lock();
assert_eq!(printhhmmss(5.123), "5.123");
assert_eq!(printhhmmss(59.999), "59.999");
}
#[test]
fn printhhmmss_minute_uses_mm_ss_format() {
let _g = crate::test_util::global_state_lock();
let r = printhhmmss(65.5);
assert_eq!(r, "1:05.50", "1m05.50s");
}
#[test]
fn printhhmmss_hour_uses_hh_mm_ss_format() {
let _g = crate::test_util::global_state_lock();
let r = printhhmmss(3725.0); assert_eq!(r, "1:02:05.00");
}
#[test]
fn printhhmmss_sixty_seconds_is_one_minute() {
let _g = crate::test_util::global_state_lock();
let r = printhhmmss(60.0);
assert_eq!(r, "1:00.00", "60s = 1m");
}
#[test]
fn printhhmmss_thirty_six_hundred_seconds_is_one_hour() {
let _g = crate::test_util::global_state_lock();
let r = printhhmmss(3600.0);
assert_eq!(r, "1:00:00.00");
}
#[test]
fn sigmsg_known_signal_returns_descriptive_string() {
let _g = crate::test_util::global_state_lock();
let term = sigmsg(libc::SIGTERM);
assert_ne!(term, "unknown signal", "SIGTERM should have a message");
}
#[test]
fn sigmsg_unknown_signal_returns_unknown() {
let _g = crate::test_util::global_state_lock();
assert_eq!(sigmsg(-1), "unknown signal");
assert_eq!(sigmsg(9999), "unknown signal");
}
#[test]
fn printhhmmss_is_deterministic() {
let _g = crate::test_util::global_state_lock();
for t in &[0.0, 1.5, 60.0, 3600.0, 7200.5] {
let r1 = printhhmmss(*t);
let r2 = printhhmmss(*t);
assert_eq!(r1, r2, "printhhmmss must be pure for {}", t);
}
}
#[test]
fn get_clktck_returns_positive() {
let _g = crate::test_util::global_state_lock();
let ck = get_clktck();
assert!(ck > 0, "CLK_TCK must be positive, got {}", ck);
assert!(ck <= 10_000, "CLK_TCK suspiciously large: {}", ck);
}
#[test]
fn isanum_empty_returns_false() {
let _g = crate::test_util::global_state_lock();
assert!(!isanum(""));
}
#[test]
fn isanum_all_digits_returns_true() {
let _g = crate::test_util::global_state_lock();
assert!(isanum("123"));
assert!(isanum("0"));
assert!(isanum("999999"));
}
#[test]
fn isanum_with_hyphen_returns_true() {
let _g = crate::test_util::global_state_lock();
assert!(isanum("-1"));
assert!(isanum("-123"));
assert!(isanum("-"));
}
#[test]
fn isanum_rejects_alpha() {
let _g = crate::test_util::global_state_lock();
assert!(!isanum("abc"));
assert!(!isanum("1a"));
assert!(!isanum("a1"));
assert!(!isanum("1 2"));
assert!(!isanum("1.0"));
}
#[test]
fn isanum_is_deterministic() {
let _g = crate::test_util::global_state_lock();
for s in ["", "123", "-1", "abc", "1a"] {
let first = isanum(s);
for _ in 0..5 {
assert_eq!(isanum(s), first);
}
}
}
#[test]
fn getsigidx_empty_returns_none() {
let _g = crate::test_util::global_state_lock();
assert!(getsigidx("").is_none());
}
#[test]
fn getsigidx_exit_returns_zero() {
let _g = crate::test_util::global_state_lock();
assert_eq!(getsigidx("EXIT"), Some(0));
}
#[test]
#[cfg(unix)]
fn getsigidx_canonical_signal_names() {
let _g = crate::test_util::global_state_lock();
assert_eq!(getsigidx("HUP"), Some(libc::SIGHUP));
assert_eq!(getsigidx("TERM"), Some(libc::SIGTERM));
assert_eq!(getsigidx("INT"), Some(libc::SIGINT));
assert_eq!(getsigidx("KILL"), Some(libc::SIGKILL));
}
#[test]
#[cfg(unix)]
fn getsigidx_strips_sig_prefix() {
let _g = crate::test_util::global_state_lock();
assert_eq!(getsigidx("HUP"), getsigidx("SIGHUP"));
assert_eq!(getsigidx("TERM"), getsigidx("SIGTERM"));
}
#[test]
#[cfg(unix)]
fn getsigidx_case_sensitive() {
let _g = crate::test_util::global_state_lock();
assert_eq!(getsigidx("hup"), None);
assert_eq!(getsigidx("HUP"), Some(libc::SIGHUP));
}
#[test]
fn getsigidx_unknown_returns_none() {
let _g = crate::test_util::global_state_lock();
assert!(getsigidx("NEVER_REAL_SIGNAL").is_none());
}
#[test]
fn getsigidx_zerr_and_err_alias() {
let _g = crate::test_util::global_state_lock();
assert_eq!(getsigidx("ZERR"), Some(crate::ported::signals_h::SIGZERR));
assert_eq!(
getsigidx("ERR"),
Some(crate::ported::signals_h::SIGZERR),
"ERR aliases ZERR"
);
}
#[test]
fn getsigidx_debug_returns_sigdebug() {
let _g = crate::test_util::global_state_lock();
assert_eq!(getsigidx("DEBUG"), Some(crate::ported::signals_h::SIGDEBUG));
}
#[test]
fn get_clktck_returns_i64_type() {
let _g = crate::test_util::global_state_lock();
let _: i64 = get_clktck();
}
#[test]
fn get_clktck_returns_positive_pin() {
let _g = crate::test_util::global_state_lock();
let tk = get_clktck();
assert!(tk > 0, "clock tick rate must be > 0, got {}", tk);
}
#[test]
fn get_clktck_is_deterministic() {
let _g = crate::test_util::global_state_lock();
let first = get_clktck();
for _ in 0..5 {
assert_eq!(get_clktck(), first, "clock tick rate must be stable");
}
}
#[test]
fn printhhmmss_returns_string_type() {
let _: String = printhhmmss(0.0);
}
#[test]
fn printhhmmss_is_pure() {
for s in [0.0, 1.0, 60.0, 3661.5, -1.0] {
let first = printhhmmss(s);
for _ in 0..3 {
assert_eq!(printhhmmss(s), first, "printhhmmss({}) must be pure", s);
}
}
}
#[test]
fn printhhmmss_zero_short_form() {
let s = printhhmmss(0.0);
assert!(
s.contains('.'),
"sub-minute must use 'S.MMM' form, got {:?}",
s
);
assert!(s.contains('0'), "must contain '0' digit, got {:?}", s);
}
#[test]
fn printhhmmss_over_minute_adds_colon() {
let s = printhhmmss(125.0); assert!(
s.contains(':'),
"over 60s must contain ':' separator, got {:?}",
s
);
}
#[test]
fn sigmsg_returns_static_str_type() {
let _: &'static str = sigmsg(0);
}
#[test]
fn sigmsg_is_pure() {
for s in [0i32, 1, 9, 15, 999] {
let first = sigmsg(s);
for _ in 0..3 {
assert_eq!(sigmsg(s), first, "sigmsg({}) must be pure", s);
}
}
}
#[test]
fn hasprocs_empty_table_returns_false() {
let empty: Vec<job> = vec![];
assert!(!hasprocs(&empty, 0), "empty table → false");
}
#[test]
fn hasprocs_returns_bool_type() {
let empty: Vec<job> = vec![];
let _: bool = hasprocs(&empty, 0);
}
#[test]
fn get_usage_returns_timeinfo_type() {
let _g = crate::test_util::global_state_lock();
let _: timeinfo = get_usage();
}
#[test]
fn dtime_tv_positive_diff_returned() {
let mut dt = Duration::ZERO;
let t1 = Duration::from_secs(1);
let t2 = Duration::from_secs(5);
let r = dtime_tv(&mut dt, &t1, &t2);
assert_eq!(r, Duration::from_secs(4), "5 - 1 = 4s");
assert_eq!(dt, Duration::from_secs(4), "out param set to diff");
}
#[test]
fn dtime_tv_negative_diff_saturates_to_zero() {
let mut dt = Duration::from_secs(99);
let t1 = Duration::from_secs(5);
let t2 = Duration::from_secs(1);
let r = dtime_tv(&mut dt, &t1, &t2);
assert_eq!(r, Duration::ZERO, "t2 < t1 → ZERO");
assert_eq!(dt, Duration::ZERO, "out param set to ZERO");
}
#[test]
fn dtime_tv_equal_returns_zero() {
let mut dt = Duration::from_secs(99);
let t = Duration::from_secs(5);
let r = dtime_tv(&mut dt, &t, &t);
assert_eq!(r, Duration::ZERO, "equal → ZERO");
}
#[test]
fn dtime_tv_returns_duration_type() {
let mut dt = Duration::ZERO;
let t = Duration::from_secs(1);
let _: Duration = dtime_tv(&mut dt, &t, &t);
}
#[test]
fn dtime_ts_negative_diff_saturates_to_zero() {
let t1 = Instant::now();
std::thread::sleep(Duration::from_millis(1));
let t2 = Instant::now();
let r = dtime_ts(&t2, &t1);
assert_eq!(r, Duration::ZERO, "earlier - later = ZERO");
}
#[test]
fn dtime_ts_returns_duration_type() {
let now = Instant::now();
let _: Duration = dtime_ts(&now, &now);
}
#[test]
fn dtime_ts_same_instant_returns_zero() {
let now = Instant::now();
assert_eq!(
dtime_ts(&now, &now),
Duration::ZERO,
"same instant → ZERO diff"
);
}
#[test]
fn havefiles_empty_returns_false() {
let empty: Vec<job> = vec![];
assert!(!havefiles(&empty), "empty table has no files");
}
#[test]
fn havefiles_returns_bool_type() {
let empty: Vec<job> = vec![];
let _: bool = havefiles(&empty);
}
#[test]
fn getbgstatus_returns_option_i32_type() {
let _g = crate::test_util::global_state_lock();
let _: Option<i32> = getbgstatus(-1);
}
#[test]
fn getbgstatus_unknown_pid_returns_none() {
let _g = crate::test_util::global_state_lock();
assert!(getbgstatus(0).is_none() || getbgstatus(0).is_some());
let r = getbgstatus(2147483646);
assert!(r.is_none(), "never-recorded pid → None");
}
#[test]
fn hasprocs_index_out_of_bounds_safe() {
let empty: Vec<job> = vec![];
for idx in [0usize, 1, 100, usize::MAX] {
let _: bool = hasprocs(&empty, idx);
}
}
#[test]
fn printhhmmss_one_second_short_form() {
let s = printhhmmss(1.0);
assert!(s.contains("1."), "1.0s must contain '1.', got {:?}", s);
}
#[test]
fn isanum_is_pure_full_sweep() {
for s in ["", "0", "123", "-5", "abc", "a1", "1a", "-", "12-34"] {
let first = isanum(s);
for _ in 0..3 {
assert_eq!(isanum(s), first, "isanum({:?}) must be pure", s);
}
}
}
}