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 tab = vec![job::default(), jn];
printjob_async(&tab, 1, 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], idx: usize) {
if idx >= tab.len() || (tab[idx].stat & stat::DONE) == 0 {
return;
}
deletejob(&mut tab[idx], false); let mut cj = CURJOB.get_or_init(|| Mutex::new(-1)).lock().unwrap();
let mut pj = PREVJOB.get_or_init(|| Mutex::new(-1)).lock().unwrap();
if *cj == idx as i32 {
*cj = *pj;
*pj = idx as i32;
}
let need_setprev = *pj == idx as i32; drop(cj);
drop(pj);
if need_setprev {
setprevjob_locked(tab); }
}
pub fn printjob_async(tab: &[job], ji: usize, thisjob: i32) -> bool {
use crate::ported::builtins::sched::zleactive;
use crate::ported::zsh_h::{isset, INTERACTIVE, LONGLISTJOBS, PRINTEXITVALUE, SHINSTDIN, SP_RUNNING};
use std::sync::atomic::Ordering;
let jn = &tab[ji];
let mut skip_print = (jn.stat & stat::NOPRINT) != 0;
let mut sflag = false; let mut doputnl = false; let is_thisjob = ji as i32 == thisjob;
for pn in jn.procs.iter() {
if pn.status == SP_RUNNING {
continue; }
if libc::WIFSIGNALED(pn.status) {
let sig = libc::WTERMSIG(pn.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(pn.status) {
let sig = libc::WSTOPSIG(pn.status); if is_thisjob && sig == libc::SIGTSTP {
doputnl = true; }
} else if isset(PRINTEXITVALUE) && isset(SHINSTDIN) && libc::WEXITSTATUS(pn.status) != 0
{
sflag = true; skip_print = false;
}
}
if skip_print {
return false;
}
let interact = isset(INTERACTIVE);
let stopped = (jn.stat & stat::STOPPED) != 0;
if interact && crate::ported::zsh_h::jobbing() && (stopped || sflag || !is_thisjob) {
if zleactive.load(Ordering::Relaxed) != 0 {
crate::ported::init::zleentry(crate::ported::zsh_h::ZLE_CMD_TRASH);
}
let curjob = *CURJOB.get_or_init(|| Mutex::new(-1)).lock().unwrap();
let prevjob = *PREVJOB.get_or_init(|| Mutex::new(-1)).lock().unwrap();
let s = crate::ported::jobs::printjob(
jn,
ji,
isset(LONGLISTJOBS) as i32,
(curjob >= 0).then_some(curjob as usize),
(prevjob >= 0).then_some(prevjob as usize),
is_thisjob, );
let nl = if doputnl { "\n" } else { "" };
shout_write(&format!("{}{}\n", nl, s));
return true;
} else if doputnl && interact {
shout_write("\n");
return true;
}
false
}
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; }
#[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());
}
}