#![allow(non_camel_case_types)]
#![allow(non_upper_case_globals)]
#![allow(non_snake_case)]
use std::sync::atomic::{AtomicI32, Ordering};
use std::sync::{Mutex, OnceLock};
use libc::time_t;
use crate::DPUTS;
use crate::init::zleentry;
use crate::mem::popheap;
use crate::ported::mem::pushheap;
use crate::ported::utils::{
addprepromptfn, addtimedfn, delprepromptfn, deltimedfn, unmeta, zjoin, zstrtol, ztrftime,
zwarnnam,
};
use crate::ported::zsh_h::{isset, options, param, VERBOSE, MAX_OPS};
use crate::ported::ztype_h::idigit;
use crate::utils::TIMED_FNS;
use crate::zsh_h::{features, module};
pub type Schedcmd = Option<Box<schedcmd>>;
#[derive(Debug)]
pub struct schedcmd {
pub next: Option<Box<schedcmd>>, pub cmd: String, pub time: time_t, pub flags: i32, }
static schedcmdtimed: OnceLock<Mutex<i32>> = OnceLock::new();
pub(crate) fn schedaddtimed() {
if *schedcmdtimed_lock().lock().unwrap() != 0 {
scheddeltimed(); }
*schedcmdtimed_lock().lock().unwrap() = 1; let head_time: time_t = schedcmds_lock() .lock()
.unwrap()
.as_ref()
.map(|h| h.time)
.unwrap_or(0);
addtimedfn(checksched_thunk, head_time as i64); }
pub(crate) fn scheddeltimed() {
if *schedcmdtimed_lock().lock().unwrap() != 0 {
deltimedfn(checksched_thunk); *schedcmdtimed_lock().lock().unwrap() = 0; }
}
pub(crate) fn checksched() -> i32 {
let t: time_t; if schedcmds_lock().lock().unwrap().is_none() {
return 0; }
t = unsafe { libc::time(std::ptr::null_mut()) }; loop {
let due = {
let head = schedcmds_lock().lock().unwrap();
match head.as_ref() {
Some(h) if h.time <= t => true,
_ => false,
}
};
if !due {
break;
}
let sch: Box<schedcmd> = {
let mut head = schedcmds_lock().lock().unwrap();
let mut h = head.take().unwrap();
*head = h.next.take(); h
};
scheddeltimed();
if (sch.flags & SCHEDFLAG_TRASH_ZLE) != 0 && zleactive.load(Ordering::Relaxed) != 0
{
zleentry(ZLE_CMD_TRASH); }
execstring(&sch.cmd, 0, 0, "sched"); drop(sch);
let need_arm = {
let head = schedcmds_lock().lock().unwrap();
head.is_some() && *schedcmdtimed_lock().lock().unwrap() == 0
};
if need_arm {
DPUTS!(
!TIMED_FNS.lock().unwrap().is_empty(), "BUG: already timed fn (1)" );
schedaddtimed(); }
}
0
}
pub(crate) fn bin_sched(nam: &str, argv: &[String], _ops: &options, _func: i32) -> i32 {
let s: &str;
let mut argptr: usize;
let mut t: time_t;
let mut h: i64;
let m: i64;
let sec: i64;
let mut sn: i32;
let mut flags: i32 = 0;
argptr = 0; while argptr < argv.len() && argv[argptr].as_bytes().first().copied() == Some(b'-') {
let arg = &argv[argptr][1..]; let arg_b = arg.as_bytes();
if !arg_b.is_empty() && idigit(arg_b[0]) {
sn = zstrtol(arg, 10).0 as i32;
if sn == 0 {
zwarnnam("sched", "usage for delete: sched -<item#>."); return 1;
}
let mut head = schedcmds_lock().lock().unwrap();
let mut steps = sn - 1;
let mut slot: &mut Option<Box<schedcmd>> = &mut *head;
while steps > 0 {
if slot.is_none() {
break;
}
slot = &mut slot.as_mut().unwrap().next;
steps -= 1;
}
if slot.is_none() {
drop(head);
zwarnnam("sched", "not that many entries"); return 1;
}
let head_slot = sn == 1;
let mut removed = slot.take().unwrap();
*slot = removed.next.take();
if head_slot {
drop(head);
scheddeltimed();
let still_have = schedcmds_lock().lock().unwrap().is_some();
if still_have {
DPUTS!(
!TIMED_FNS.lock().unwrap().is_empty(), "BUG: already timed fn (2)" );
schedaddtimed(); }
} else {
drop(head);
}
drop(removed); return 0; } else if arg == "-" {
argptr += 1; break; } else if arg == "o" {
flags |= SCHEDFLAG_TRASH_ZLE; } else {
if !arg.is_empty() {
zwarnnam(
nam,
&format!("bad option: -{}", arg.chars().next().unwrap()),
); } else {
zwarnnam(nam, "option expected"); }
return 1; }
argptr += 1;
}
if argptr >= argv.len() {
sn = 1;
let head = schedcmds_lock().lock().unwrap();
let mut cur: &Option<Box<schedcmd>> = &*head;
while let Some(sch) = cur.as_ref() {
let t_local: time_t = sch.time; let tbuf = ztrftime(
"%a %b %e %k:%M:%S",
std::time::UNIX_EPOCH + std::time::Duration::from_secs(t_local as u64),
);
let flagstr = if (sch.flags & SCHEDFLAG_TRASH_ZLE) != 0 {
"-o " } else {
"" };
let endstr = if sch.cmd.starts_with('-') {
"-- " } else {
"" };
println!(
"{:3} {} {}{}{}",
sn,
tbuf,
flagstr,
endstr,
unmeta(&sch.cmd)
); cur = &sch.next;
sn += 1;
}
return 0; } else if argptr + 1 >= argv.len() {
zwarnnam("sched", "not enough arguments"); return 1; }
s = &argv[argptr]; argptr += 1;
let s_b = s.as_bytes();
if !s_b.is_empty() && s_b[0] == b'+' {
let (zl, rest) = zstrtol(&s[1..], 10); let rb = rest.as_bytes();
if !rb.is_empty() && rb[0] == b':' {
let (mv, rest2) = zstrtol(&rest[1..], 10); m = mv;
let rb2 = rest2.as_bytes();
let rest3: &str;
if !rb2.is_empty() && rb2[0] == b':' {
let (sv, r3) = zstrtol(&rest2[1..], 10); sec = sv;
rest3 = r3;
} else {
sec = 0; rest3 = rest2;
}
if !rest3.is_empty() {
zwarnnam("sched", "bad time specifier"); return 1;
}
t = unsafe { libc::time(std::ptr::null_mut()) } + (zl as time_t) * 3600 + (m as time_t) * 60 + (sec as time_t);
} else if rest.is_empty() {
t = unsafe { libc::time(std::ptr::null_mut()) } + zl as time_t; } else {
zwarnnam("sched", "bad time specifier"); return 1;
}
} else {
let (zl, rest) = zstrtol(s, 10); let rb = rest.as_bytes();
if !rb.is_empty() && rb[0] == b':' {
h = zl; let (mv, rest2) = zstrtol(&rest[1..], 10); m = mv;
let rb2 = rest2.as_bytes();
let rest3: &str;
if !rb2.is_empty() && rb2[0] == b':' {
let (sv, r3) = zstrtol(&rest2[1..], 10); sec = sv;
rest3 = r3;
} else {
sec = 0; rest3 = rest2;
}
let rb3 = rest3.as_bytes();
if !rb3.is_empty()
&& rb3[0] != b'a'
&& rb3[0] != b'A'
&& rb3[0] != b'p'
&& rb3[0] != b'P'
{
zwarnnam("sched", "bad time specifier"); return 1;
}
t = unsafe { libc::time(std::ptr::null_mut()) }; unsafe {
let tm_ptr = libc::localtime(&t as *const time_t); if !tm_ptr.is_null() {
let tm = *tm_ptr;
t -= (tm.tm_sec as time_t)
+ (tm.tm_min as time_t) * 60
+ (tm.tm_hour as time_t) * 3600; }
}
if !rb3.is_empty() && (rb3[0] == b'p' || rb3[0] == b'P') {
h += 12; }
t += (h as time_t) * 3600 + (m as time_t) * 60 + (sec as time_t); if t < unsafe { libc::time(std::ptr::null_mut()) } {
t += 3600 * 24; }
} else if rest.is_empty() {
t = zl as time_t; } else {
zwarnnam("sched", "bad time specifier"); return 1;
}
}
let mut sch_new: Box<schedcmd> = Box::new(schedcmd {
next: None,
cmd: zjoin(&argv[argptr..], ' '), time: t, flags, });
let mut head = schedcmds_lock().lock().unwrap();
if head.is_some() {
if sch_new.time < head.as_ref().unwrap().time {
drop(head);
scheddeltimed(); let mut head2 = schedcmds_lock().lock().unwrap();
sch_new.next = head2.take(); *head2 = Some(sch_new); drop(head2);
DPUTS!(
!TIMED_FNS.lock().unwrap().is_empty(), "BUG: already timed fn (3)" );
schedaddtimed(); } else {
let mut cur = head.as_mut().unwrap();
while cur.next.is_some() && cur.next.as_ref().unwrap().time < sch_new.time {
cur = cur.next.as_mut().unwrap();
}
sch_new.next = cur.next.take(); cur.next = Some(sch_new); }
} else {
sch_new.next = None; *head = Some(sch_new); drop(head);
DPUTS!(
!TIMED_FNS.lock().unwrap().is_empty(), "BUG: already timed fn (4)" );
schedaddtimed(); }
0 }
pub(crate) fn schedgetfn(_pm: *const param) -> Vec<String> {
let mut i: usize; let head = schedcmds_lock().lock().unwrap();
i = 0;
{
let mut cur: &Option<Box<schedcmd>> = &*head;
while let Some(s) = cur.as_ref() {
cur = &s.next;
i += 1;
}
}
let mut ret: Vec<String> = Vec::with_capacity(i);
let mut cur: &Option<Box<schedcmd>> = &*head;
while let Some(sch) = cur.as_ref() {
let t: time_t = sch.time; let tbuf = format!("{}", t as i64); let flagstr = if (sch.flags & SCHEDFLAG_TRASH_ZLE) != 0 {
"-o" } else {
"" };
ret.push(format!("{}:{}:{}", tbuf, flagstr, sch.cmd));
cur = &sch.next;
}
ret }
#[allow(unused_variables)]
pub fn setup_(m: *const module) -> i32 {
0 }
pub fn features_(m: *const module, features: &mut Vec<String>) -> i32 {
*features = featuresarray(m, module_features()); 0 }
pub fn enables_(m: *const module, enables: &mut Option<Vec<i32>>) -> i32 {
handlefeatures(m, module_features(), enables) }
#[allow(unused_variables)]
pub fn boot_(m: *const module) -> i32 {
addprepromptfn(checksched_thunk); 0 }
pub fn cleanup_(m: *const module) -> i32 {
if schedcmds_lock().lock().unwrap().is_some() {
scheddeltimed(); }
let mut head = schedcmds_lock().lock().unwrap();
*head = None;
drop(head);
delprepromptfn(checksched_thunk); setfeatureenables(m, module_features(), None) }
#[allow(unused_variables)]
pub fn finish_(m: *const module) -> i32 {
0 }
pub const SCHEDFLAG_TRASH_ZLE: i32 = 1;
static schedcmds: OnceLock<Mutex<Option<Box<schedcmd>>>> = OnceLock::new();
static MODULE_FEATURES: OnceLock<Mutex<features>> = OnceLock::new();
fn schedcmds_lock() -> &'static Mutex<Option<Box<schedcmd>>> {
schedcmds.get_or_init(|| Mutex::new(None))
}
fn schedcmdtimed_lock() -> &'static Mutex<i32> {
schedcmdtimed.get_or_init(|| Mutex::new(0))
}
fn checksched_thunk() {
let _ = checksched();
}
fn featuresarray(_m: *const module, _f: &Mutex<features>) -> Vec<String> {
vec!["b:sched".to_string(), "p:zsh_scheduled_events".to_string()]
}
fn handlefeatures(
m: *const module,
f: &Mutex<features>,
enables: &mut Option<Vec<i32>>,
) -> i32 {
if enables.is_none() {
*enables = Some(getfeatureenables(m, f));
} else if let Some(e) = enables.as_ref() {
return setfeatureenables(m, f, Some(e));
}
0
}
fn getfeatureenables(_m: *const module, f: &Mutex<features>) -> Vec<i32> {
let g = f.lock().unwrap();
let total = g.bn_size + g.cd_size + g.mf_size + g.pd_size + g.n_abstract;
vec![0; total as usize]
}
fn setfeatureenables(
_m: *const module,
_f: &Mutex<features>,
_e: Option<&Vec<i32>>,
) -> i32 {
0
}
const ZLE_CMD_TRASH: i32 = 3;
pub static zleactive: AtomicI32 = AtomicI32::new(0);
use crate::ported::exec::execstring;
fn module_features() -> &'static Mutex<features> {
MODULE_FEATURES.get_or_init(|| {
Mutex::new(features {
bn_list: None, bn_size: 1, cd_list: None, cd_size: 0,
mf_list: None, mf_size: 0,
pd_list: None, pd_size: 1, n_abstract: 0, })
})
}
#[cfg(test)]
mod tests {
use super::*;
static TEST_SERIAL: Mutex<()> = Mutex::new(());
fn empty_ops() -> options {
options {
ind: [0u8; MAX_OPS],
args: Vec::new(),
argscount: 0,
argsalloc: 0,
}
}
fn reset_with_lock() -> std::sync::MutexGuard<'static, ()> {
let guard = TEST_SERIAL.lock().unwrap_or_else(|e| {
TEST_SERIAL.clear_poison();
e.into_inner()
});
crate::ported::utils::inittyptab();
let mut head = schedcmds_lock().lock().unwrap_or_else(|e| {
schedcmds_lock().clear_poison();
e.into_inner()
});
*head = None;
drop(head);
let mut t = schedcmdtimed_lock().lock().unwrap_or_else(|e| {
schedcmdtimed_lock().clear_poison();
e.into_inner()
});
*t = 0;
drop(t);
guard
}
fn s(x: &str) -> String {
x.to_string()
}
#[test]
fn list_empty_returns_zero() {
let _g = crate::test_util::global_state_lock();
let _serial = reset_with_lock();
let ops = empty_ops();
assert_eq!(bin_sched("sched", &[], &ops, 0), 0);
}
#[test]
fn add_relative_seconds_pushes_one_entry() {
let _g = crate::test_util::global_state_lock();
let _serial = reset_with_lock();
let ops = empty_ops();
let args = vec![s("+60"), s("echo"), s("hello")];
assert_eq!(bin_sched("sched", &args, &ops, 0), 0);
let head = schedcmds_lock().lock().unwrap();
assert_eq!(head.as_ref().unwrap().cmd, "echo hello");
assert!(head.as_ref().unwrap().next.is_none());
}
#[test]
fn add_then_delete_first_clears_list() {
let _g = crate::test_util::global_state_lock();
let _serial = reset_with_lock();
let ops = empty_ops();
bin_sched("sched", &[s("+60"), s("echo"), s("hello")], &ops, 0);
assert!(schedcmds_lock().lock().unwrap().is_some());
assert_eq!(bin_sched("sched", &[s("-1")], &ops, 0), 0);
assert!(schedcmds_lock().lock().unwrap().is_none());
}
#[test]
fn not_enough_args_returns_one() {
let _g = crate::test_util::global_state_lock();
let _serial = reset_with_lock();
let ops = empty_ops();
assert_eq!(bin_sched("sched", &[s("+60")], &ops, 0), 1);
}
#[test]
fn dash_o_flag_sets_trash_zle() {
let _g = crate::test_util::global_state_lock();
let _serial = reset_with_lock();
let ops = empty_ops();
let args = vec![s("-o"), s("+60"), s("cmd")];
assert_eq!(bin_sched("sched", &args, &ops, 0), 0);
let head = schedcmds_lock().lock().unwrap();
assert_eq!(
head.as_ref().unwrap().flags & SCHEDFLAG_TRASH_ZLE,
SCHEDFLAG_TRASH_ZLE
);
}
#[test]
fn insert_keeps_time_order() {
let _g = crate::test_util::global_state_lock();
let _serial = reset_with_lock();
let ops = empty_ops();
bin_sched("sched", &[s("+200"), s("third")], &ops, 0);
bin_sched("sched", &[s("+50"), s("first")], &ops, 0);
bin_sched("sched", &[s("+100"), s("second")], &ops, 0);
let head = schedcmds_lock().lock().unwrap();
let n0 = head.as_ref().unwrap();
let n1 = n0.next.as_ref().unwrap();
let n2 = n1.next.as_ref().unwrap();
assert_eq!(n0.cmd, "first");
assert_eq!(n1.cmd, "second");
assert_eq!(n2.cmd, "third");
assert!(n2.next.is_none());
}
#[test]
fn schedgetfn_serializes_entries() {
let _g = crate::test_util::global_state_lock();
let _serial = reset_with_lock();
let mut head = schedcmds_lock().lock().unwrap();
*head = Some(Box::new(schedcmd {
next: Some(Box::new(schedcmd {
next: None,
cmd: "echo zle".to_string(),
time: 1700001000,
flags: SCHEDFLAG_TRASH_ZLE,
})),
cmd: "echo test".to_string(),
time: 1700000000,
flags: 0,
}));
drop(head);
let arr = schedgetfn(std::ptr::null());
assert_eq!(arr.len(), 2);
assert_eq!(arr[0], "1700000000::echo test");
assert_eq!(arr[1], "1700001000:-o:echo zle");
}
#[test]
fn schedflag_trash_zle_is_bit_zero() {
let _g = crate::test_util::global_state_lock();
assert_eq!(
SCHEDFLAG_TRASH_ZLE, 1,
"SCHEDFLAG_TRASH_ZLE must be bit 0 per c:40 enum"
);
}
#[test]
fn bin_sched_no_args_with_empty_schedule_returns_zero() {
let _g = crate::test_util::global_state_lock();
let _serial = reset_with_lock();
let ops = empty_ops();
let r = bin_sched("sched", &[], &ops, 0);
assert_eq!(r, 0, "list on empty schedule must succeed");
}
#[test]
fn bin_sched_time_only_no_command_returns_one() {
let _g = crate::test_util::global_state_lock();
let _serial = reset_with_lock();
let ops = empty_ops();
let r = bin_sched("sched", &[s("+60")], &ops, 0);
assert_eq!(r, 1, "scheduling without a command must error");
}
#[test]
fn bin_sched_zero_offset_does_not_panic() {
let _g = crate::test_util::global_state_lock();
let _serial = reset_with_lock();
let ops = empty_ops();
let _r = bin_sched("sched", &[s("+0"), s("echo now")], &ops, 0);
}
#[test]
fn bin_sched_minus_removal_on_empty_schedule_returns_nonzero() {
let _g = crate::test_util::global_state_lock();
let _serial = reset_with_lock();
let ops = empty_ops();
let r = bin_sched("sched", &[s("-1")], &ops, 0);
assert_ne!(r, 0, "remove from empty schedule must error");
}
#[test]
fn insert_with_equal_times_lands_after_first_equal_entry() {
let _g = crate::test_util::global_state_lock();
let _serial = reset_with_lock();
let ops = empty_ops();
bin_sched("sched", &[s("+100"), s("first")], &ops, 0);
bin_sched("sched", &[s("+100"), s("second")], &ops, 0);
bin_sched("sched", &[s("+100"), s("third")], &ops, 0);
let head = schedcmds_lock().lock().unwrap();
let n0 = head.as_ref().unwrap();
let n1 = n0.next.as_ref().unwrap();
let n2 = n1.next.as_ref().unwrap();
assert_eq!(
n0.cmd, "first",
"head is the first-inserted equal-time entry"
);
assert_eq!(
n1.cmd, "third",
"newest insert lands right after head, pushing prior down"
);
assert_eq!(
n2.cmd, "second",
"second-inserted ends up at tail per c:323 strict-less walk"
);
}
#[test]
fn module_lifecycle_shims_all_return_zero() {
let _g = crate::test_util::global_state_lock();
let m: *const module = std::ptr::null();
assert_eq!(setup_(m), 0);
assert_eq!(boot_(m), 0);
assert_eq!(cleanup_(m), 0);
assert_eq!(finish_(m), 0);
}
#[test]
fn schedgetfn_empty_schedule_returns_empty_vec() {
let _g = crate::test_util::global_state_lock();
let _serial = reset_with_lock();
*schedcmds_lock().lock().unwrap() = None;
let arr = schedgetfn(std::ptr::null());
assert!(
arr.is_empty(),
"empty schedule must serialize to empty Vec, got {:?}",
arr
);
}
#[test]
fn sched_corpus_bin_sched_no_args_returns_zero() {
let _g = crate::test_util::global_state_lock();
let _serial = reset_with_lock();
*schedcmds_lock().lock().unwrap() = None;
let ops = empty_ops();
let r = bin_sched("sched", &[], &ops, 0);
assert_eq!(r, 0, "sched with no args lists schedule, returns 0");
}
#[test]
fn sched_corpus_bin_sched_minus_one_empty_returns_nonzero() {
let _g = crate::test_util::global_state_lock();
let _serial = reset_with_lock();
*schedcmds_lock().lock().unwrap() = None;
let ops = empty_ops();
let r = bin_sched("sched", &[s("-1")], &ops, 0);
assert_ne!(r, 0, "sched -1 on empty schedule = error");
}
#[test]
fn sched_corpus_bin_sched_relative_time_adds_entry() {
let _g = crate::test_util::global_state_lock();
let _serial = reset_with_lock();
*schedcmds_lock().lock().unwrap() = None;
let ops = empty_ops();
bin_sched("sched", &[s("+60"), s("my_cmd")], &ops, 0);
let head = schedcmds_lock().lock().unwrap();
assert!(head.is_some(), "entry added to schedule");
assert_eq!(head.as_ref().unwrap().cmd, "my_cmd");
}
#[test]
fn sched_corpus_schedgetfn_after_single_add() {
let _g = crate::test_util::global_state_lock();
let _serial = reset_with_lock();
*schedcmds_lock().lock().unwrap() = None;
let ops = empty_ops();
bin_sched("sched", &[s("+60"), s("only")], &ops, 0);
let arr = schedgetfn(std::ptr::null());
assert_eq!(arr.len(), 1, "1 entry after 1 add, got {arr:?}");
}
#[test]
fn sched_corpus_schedgetfn_after_three_adds() {
let _g = crate::test_util::global_state_lock();
let _serial = reset_with_lock();
*schedcmds_lock().lock().unwrap() = None;
let ops = empty_ops();
bin_sched("sched", &[s("+10"), s("a")], &ops, 0);
bin_sched("sched", &[s("+20"), s("b")], &ops, 0);
bin_sched("sched", &[s("+30"), s("c")], &ops, 0);
let arr = schedgetfn(std::ptr::null());
assert_eq!(arr.len(), 3, "3 entries after 3 adds, got {arr:?}");
}
}