use std::process::Child;
use std::sync::Mutex;
use crate::ported::jobs::stat;
use crate::ported::jobs::{deletejob, CURJOB, MAXJOB, PREVJOB, THISJOB};
use crate::ported::zsh_h::job;
pub fn wait_status_val(s: std::process::ExitStatus) -> i32 {
use std::os::unix::process::ExitStatusExt as _;
if let Some(sig) = s.signal() {
0o200 | sig } else if let Some(sig) = s.stopped_signal() {
0o200 | sig } else {
s.code().unwrap_or(0) }
}
thread_local! {
pub static NOT_JOB_LEADER: std::cell::Cell<u32> = const { std::cell::Cell::new(0) };
}
fn shout_write(s: &str) {
let tty = crate::ported::init::SHTTY.load(std::sync::atomic::Ordering::Relaxed);
let fd = if tty >= 0 { tty } else { libc::STDERR_FILENO };
let _ = crate::ported::utils::write_loop(fd, s.as_bytes());
}
pub fn foreground_job_report(status: i32, text: Option<String>, nested: bool) {
use crate::ported::zsh_h::{CS_CMDAND, CS_CMDOR, SFC_NONE};
use std::sync::atomic::Ordering;
let Some(text) = text else { return };
if nested
|| !libc::WIFSIGNALED(status)
|| NOT_JOB_LEADER.with(|d| d.get()) > 0
|| crate::ported::exec::sfcontext.load(Ordering::Relaxed) != SFC_NONE
|| crate::vm_helper::EVAL_RECURSION_DEPTH.with(|d| d.get()) > 0
{
return;
}
let in_compound = crate::ported::prompt::CMDSTACK.with(|s| {
s.borrow()
.iter()
.any(|&cs| cs as i32 != CS_CMDAND as i32 && cs as i32 != CS_CMDOR as i32)
});
if in_compound {
return;
}
let jn = job {
stat: stat::INUSE | stat::LOCKED | stat::DONE | stat::CHANGED,
procs: vec![crate::ported::zsh_h::process {
pid: 0,
text,
status,
ti: Default::default(),
bgtime: None,
endtime: None,
}],
..Default::default()
};
let mut tab = vec![job::default(), jn];
printjob_print(&mut tab, 1, i32::from(crate::ported::zsh_h::isset(crate::ported::zsh_h::LONGLISTJOBS)), 0, 1);
}
pub fn foreground_job_signalled(status: i32) {
use crate::ported::zsh_h::{isset, ERRFLAG_INT, MONITOR};
use std::sync::atomic::Ordering;
if NOT_JOB_LEADER.with(|d| d.get()) > 0 || !isset(MONITOR) || !libc::WIFSIGNALED(status) {
return;
}
let sig = libc::WTERMSIG(status); if sig != libc::SIGINT && sig != libc::SIGQUIT {
return; }
let loops = crate::ported::builtin::LOOPS.load(Ordering::Relaxed);
let trapped = crate::ported::signals::sigtrapped
.lock()
.ok()
.and_then(|t| t.get(sig as usize).copied())
.unwrap_or(0);
if trapped != 0 {
crate::ported::signals::dotrap(sig); if crate::ported::utils::errflag.load(Ordering::Relaxed) != 0 {
crate::ported::builtin::BREAKS.store(loops, Ordering::Relaxed); }
} else {
crate::ported::builtin::BREAKS.store(loops, Ordering::Relaxed); crate::ported::utils::errflag.fetch_or(ERRFLAG_INT, Ordering::Relaxed); }
if let Some(tab) = crate::ported::jobs::JOBTAB.get() {
if let Ok(jt) = tab.lock() {
crate::ported::jobs::check_cursh_sig(&jt, sig);
}
}
}
pub fn printjob_delete_tail(tab: &mut [job], jn: usize, job: usize, synch: i32) {
if jn >= tab.len() || (tab[jn].stat & stat::DONE) == 0 {
return;
}
let thisjob = *THISJOB.get_or_init(|| Mutex::new(-1)).lock().unwrap_or_else(|e| e.into_inner());
let is_thisjob = i32::from(job as i32 == thisjob);
if synch <= 1 {
crate::ported::jobs::storepipestats(&tab[jn], is_thisjob, is_thisjob); }
let reporttime: f64 = crate::ported::params::getsparam("REPORTTIME")
.and_then(|s| s.parse().ok())
.unwrap_or(-1.0);
if crate::ported::jobs::should_report_time(&tab[jn], reporttime) {
if let Some(timing) = crate::ported::jobs::dumptime(&tab[jn]) {
eprintln!("{}", timing); }
}
deletejob(tab, jn, false); 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());
if *cj == job as i32 {
*cj = *pj;
*pj = job as i32;
}
let need_setprev = *pj == job as i32; drop(cj);
drop(pj);
if need_setprev {
setprevjob_locked(tab); }
}
pub fn printjob_print(tab: &mut [job], ji: usize, lng: i32, synch: i32, thisjob: i32) -> (bool, usize, bool) {
use crate::ported::builtins::sched::zleactive;
use crate::ported::zsh_h::{isset, INTERACTIVE, PRINTEXITVALUE, SHINSTDIN, SP_RUNNING};
use std::sync::atomic::Ordering;
let is_thisjob = ji as i32 == thisjob; let mut skip_print = (tab[ji].stat & stat::NOPRINT) != 0;
let mut jn = ji;
if (tab[jn].stat & stat::SUPERJOB) != 0 && tab[jn].other != 0 {
let sjn = tab[jn].other as usize;
if sjn < tab.len() && (!tab[sjn].procs.is_empty() || !tab[sjn].auxprocs.is_empty()) {
jn = sjn;
}
}
let mut sflag = false; let mut doputnl = false; let superjob = (tab[jn].stat & stat::SUPERJOB) != 0;
let nprocs = tab[jn].procs.len();
for k in 0..nprocs {
if superjob && tab[jn].procs[0].status == SP_RUNNING && k + 1 == nprocs {
tab[jn].procs[k].status = SP_RUNNING; }
let status = tab[jn].procs[k].status;
if status == SP_RUNNING {
continue; }
if libc::WIFSIGNALED(status) {
let sig = libc::WTERMSIG(status); if sig != libc::SIGINT && sig != libc::SIGPIPE {
sflag = true; }
if is_thisjob && sig == libc::SIGINT {
doputnl = true; }
if isset(PRINTEXITVALUE) && isset(SHINSTDIN) {
sflag = true; skip_print = false;
}
} else if libc::WIFSTOPPED(status) {
let sig = libc::WSTOPSIG(status); if is_thisjob && sig == libc::SIGTSTP {
doputnl = true; }
} else if isset(PRINTEXITVALUE) && isset(SHINSTDIN) && libc::WEXITSTATUS(status) != 0 {
sflag = true; skip_print = false;
}
}
if skip_print {
return (false, jn, true);
}
let mut doneprint = false; let interact = isset(INTERACTIVE);
let stopped = (tab[jn].stat & stat::STOPPED) != 0;
if synch == 2
|| ((interact || synch != 0) && crate::ported::zsh_h::jobbing() && (stopped || sflag || !is_thisjob))
{
if synch == 0 && zleactive.load(Ordering::Relaxed) != 0 {
crate::ported::init::zleentry(crate::ported::zsh_h::ZLE_CMD_TRASH);
}
let mut out = String::new();
if doputnl && synch == 0 {
doneprint = true; out.push('\n');
}
let curjob = *CURJOB.get_or_init(|| Mutex::new(-1)).lock().unwrap_or_else(|e| e.into_inner());
let prevjob = *PREVJOB.get_or_init(|| Mutex::new(-1)).lock().unwrap_or_else(|e| e.into_inner());
let s = crate::ported::jobs::printjob(
&tab[jn],
ji,
lng,
(curjob >= 0).then_some(curjob as usize),
(prevjob >= 0).then_some(prevjob as usize),
is_thisjob && synch != 2, );
if !s.is_empty() {
doneprint = true; out.push_str(&s);
out.push('\n');
}
shout_write(&out);
} else if doputnl && interact && synch == 0 {
doneprint = true;
shout_write("\n");
}
if (lng & 4) == 0 && interact && is_thisjob {
if let Some(jpwd) = tab[jn].pwd.as_deref() {
let pwd = crate::ported::params::getsparam("PWD").unwrap_or_default();
if jpwd != pwd {
doneprint = true;
shout_write(&format!("(pwd now: {})\n", crate::ported::utils::fprintdir(&pwd)));
}
}
}
(doneprint, jn, false)
}
pub fn printjob_synch(tab: &mut [job], ji: usize, lng: i32, synch: i32) -> bool {
let thisjob = *THISJOB.get_or_init(|| Mutex::new(-1)).lock().unwrap_or_else(|e| e.into_inner());
let (doneprint, jn, skipped) = printjob_print(tab, ji, lng, synch, thisjob);
if (tab[jn].stat & stat::DONE) != 0 {
printjob_delete_tail(tab, jn, ji, synch);
} else if !skipped {
tab[jn].stat &= !stat::CHANGED; }
doneprint
}
fn setprevjob_locked(tab: &[job]) {
let maxjob = *MAXJOB
.get_or_init(|| Mutex::new(0))
.lock()
.expect("maxjob poisoned");
let curjob = *CURJOB.get_or_init(|| Mutex::new(-1)).lock().unwrap();
let thisjob = *THISJOB.get_or_init(|| Mutex::new(-1)).lock().unwrap();
let pick = |want_stopped: bool| -> i32 {
for i in (1..=maxjob).rev() {
if i >= tab.len() {
continue;
}
let j = &tab[i];
let stat_ok = if want_stopped {
(j.stat & (stat::INUSE | stat::STOPPED)) == (stat::INUSE | stat::STOPPED)
} else {
(j.stat & stat::INUSE) != 0
};
if stat_ok && (j.stat & stat::SUBJOB) == 0 && i as i32 != curjob && i as i32 != thisjob
{
return i as i32;
}
}
-1
};
let mut found = pick(true); if found < 0 {
found = pick(false); }
*PREVJOB.get_or_init(|| Mutex::new(-1)).lock().unwrap() = found; }
struct HeldSlot {
frame: usize,
depth: u32,
slot: usize,
pj: i32,
}
thread_local! {
static HELD_SLOTS: std::cell::RefCell<Vec<HeldSlot>> =
const { std::cell::RefCell::new(Vec::new()) };
}
pub fn execpline_slot_open(frame: usize, depth: u32) {
if crate::worker::in_worker_thread() {
return;
}
execpline_slot_close(frame, depth);
let table = crate::ported::jobs::JOBTAB.get_or_init(|| Mutex::new(Vec::new()));
let slot = {
let mut tab = table.lock().unwrap_or_else(|e| e.into_inner());
crate::ported::jobs::initjob(&mut tab) };
let mut tj = THISJOB.get_or_init(|| Mutex::new(-1)).lock().unwrap_or_else(|e| e.into_inner());
let pj = *tj; *tj = slot as i32; drop(tj);
HELD_SLOTS.with(|h| h.borrow_mut().push(HeldSlot { frame, depth, slot, pj }));
}
pub fn execpline_slot_close(frame: usize, depth: u32) {
let start = HELD_SLOTS.with(|h| {
h.borrow()
.iter()
.position(|s| s.frame == frame && s.depth >= depth)
});
if let Some(start) = start {
release_held_slots(start);
}
}
pub fn execpline_slots_mark() -> usize {
HELD_SLOTS.with(|h| h.borrow().len())
}
pub fn execpline_slots_release_to(mark: usize) {
release_held_slots(mark);
}
pub fn execpline_slots_forget_to(mark: usize) {
HELD_SLOTS.with(|h| h.borrow_mut().truncate(mark));
}
pub fn mark_thisjob_cursh(is_builtin: bool) {
let tj = *THISJOB.get_or_init(|| Mutex::new(-1)).lock().unwrap_or_else(|e| e.into_inner());
if tj < 0 {
return;
}
let table = crate::ported::jobs::JOBTAB.get_or_init(|| Mutex::new(Vec::new()));
let mut tab = table.lock().unwrap_or_else(|e| e.into_inner());
if let Some(j) = tab.get_mut(tj as usize) {
j.stat |= stat::CURSH; if j.procs.is_empty() {
j.stat |= stat::NOPRINT; }
if is_builtin {
j.stat |= stat::BUILTIN; }
}
}
pub fn execpline_slot_current() -> Option<usize> {
HELD_SLOTS.with(|h| h.borrow().last().map(|s| s.slot))
}
fn release_held_slots(start: usize) {
let released: Vec<HeldSlot> = HELD_SLOTS.with(|h| {
let mut h = h.borrow_mut();
if start >= h.len() {
return Vec::new();
}
h.split_off(start)
});
let Some(outermost) = released.first() else {
return;
};
let table = crate::ported::jobs::JOBTAB.get_or_init(|| Mutex::new(Vec::new()));
{
let mut tab = table.lock().unwrap_or_else(|e| e.into_inner());
for held in released.iter().rev() {
if let Some(jn) = tab.get(held.slot) {
if (jn.stat & stat::INUSE) != 0
&& (jn.stat & (stat::STOPPED | stat::SUPERJOB)) == 0
{
deletejob(&mut tab, held.slot, false); }
}
}
let mut mj = MAXJOB.get_or_init(|| Mutex::new(0)).lock().unwrap_or_else(|e| e.into_inner());
while *mj > 0 && tab.get(*mj).map_or(true, |j| (j.stat & stat::INUSE) == 0) {
*mj -= 1;
}
}
*THISJOB.get_or_init(|| Mutex::new(-1)).lock().unwrap_or_else(|e| e.into_inner()) =
outermost.pj; }
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum JobState {
Running,
Stopped,
Done,
}
#[derive(Debug)]
pub struct JobInfo {
pub id: usize,
pub pid: i32,
pub child: Option<Child>,
pub command: String,
pub state: JobState,
pub is_current: bool,
}
pub struct JobTable {
jobs: Vec<Option<JobInfo>>,
current_id: Option<usize>,
next_id: usize,
}
impl Default for JobTable {
fn default() -> Self {
Self::new()
}
}
impl JobTable {
pub fn new() -> Self {
JobTable {
jobs: Vec::with_capacity(16),
current_id: None,
next_id: 1,
}
}
pub fn peek_next_id(&self) -> usize {
self.next_id
}
pub fn add_job(&mut self, child: Child, command: String, state: JobState) -> usize {
let id = self.next_id;
self.next_id += 1;
let pid = child.id() as i32;
let job = JobInfo {
id,
pid,
child: Some(child),
command,
state,
is_current: true,
};
if let Some(cur_id) = self.current_id {
if let Some(j) = self.get_mut_internal(cur_id) {
j.is_current = false;
}
}
let slot = self.get_free_slot();
if slot >= self.jobs.len() {
self.jobs.resize_with(slot + 1, || None);
}
self.jobs[slot] = Some(job);
self.current_id = Some(id);
id
}
pub fn add_pid_job(&mut self, pid: i32, command: String, state: JobState) -> usize {
let id = self.next_id;
self.next_id += 1;
let job = JobInfo {
id,
pid,
child: None,
command,
state,
is_current: true,
};
if let Some(cur_id) = self.current_id {
if let Some(j) = self.get_mut_internal(cur_id) {
j.is_current = false;
}
}
let slot = self.get_free_slot();
if slot >= self.jobs.len() {
self.jobs.resize_with(slot + 1, || None);
}
self.jobs[slot] = Some(job);
self.current_id = Some(id);
id
}
fn get_free_slot(&self) -> usize {
for (i, slot) in self.jobs.iter().enumerate() {
if slot.is_none() {
return i;
}
}
self.jobs.len()
}
fn get_mut_internal(&mut self, id: usize) -> Option<&mut JobInfo> {
self.jobs.iter_mut().flatten().find(|job| job.id == id)
}
pub fn get(&self, id: usize) -> Option<&JobInfo> {
self.jobs
.iter()
.flatten()
.find(|&job| job.id == id)
.map(|v| v as _)
}
pub fn get_mut(&mut self, id: usize) -> Option<&mut JobInfo> {
self.get_mut_internal(id)
}
pub fn remove(&mut self, id: usize) -> Option<JobInfo> {
for slot in self.jobs.iter_mut() {
if slot.as_ref().map(|j| j.id == id).unwrap_or(false) {
let job = slot.take();
if self.current_id == Some(id) {
self.current_id = None;
}
return job;
}
}
None
}
pub fn list(&self) -> Vec<&JobInfo> {
self.jobs.iter().filter_map(|j| j.as_ref()).collect()
}
pub fn iter(&self) -> impl Iterator<Item = (usize, &JobInfo)> {
self.jobs
.iter()
.filter_map(|j| j.as_ref().map(|job| (job.id, job)))
}
pub fn count(&self) -> usize {
self.jobs.iter().filter(|j| j.is_some()).count()
}
pub fn is_empty(&self) -> bool {
self.count() == 0
}
pub fn current(&self) -> Option<&JobInfo> {
self.current_id.and_then(|id| self.get(id))
}
pub fn reap_finished(&mut self) -> Vec<JobInfo> {
let mut finished = Vec::new();
for job in self.jobs.iter_mut().flatten() {
if let Some(ref mut child) = job.child {
match child.try_wait() {
Ok(Some(_status)) => {
job.state = JobState::Done;
}
Ok(None) => {
}
Err(_) => {
job.state = JobState::Done;
}
}
}
}
for slot in self.jobs.iter_mut() {
if slot
.as_ref()
.map(|j| j.state == JobState::Done)
.unwrap_or(false)
{
if let Some(job) = slot.take() {
finished.push(job);
}
}
}
finished
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_job_table_new() {
let _g = crate::test_util::global_state_lock();
let table = JobTable::new();
assert!(table.is_empty());
}
#[test]
fn test_job_state_enum() {
let _g = crate::test_util::global_state_lock();
let state = JobState::Running;
assert_eq!(state, JobState::Running);
assert_ne!(state, JobState::Stopped);
assert_ne!(state, JobState::Done);
}
#[test]
fn test_add_pid_job_assigns_id() {
let _g = crate::test_util::global_state_lock();
let mut t = JobTable::new();
let id1 = t.add_pid_job(1234, "cmd1".into(), JobState::Running);
let id2 = t.add_pid_job(5678, "cmd2".into(), JobState::Running);
assert_ne!(id1, id2);
assert_eq!(t.list().len(), 2);
assert_eq!(t.current().map(|j| j.id), Some(id2));
}
#[test]
fn test_remove_drops_current() {
let _g = crate::test_util::global_state_lock();
let mut t = JobTable::new();
let id = t.add_pid_job(99, "x".into(), JobState::Running);
assert!(t.remove(id).is_some());
assert!(t.is_empty());
assert!(t.current().is_none());
}
}