use std::collections::HashMap;
use std::io::BufRead;
use std::time::{SystemTime, UNIX_EPOCH};
use chrono::{Local, TimeZone};
#[cfg(unix)]
use std::ffi::CStr;
pub const DEFAULT_WATCHFMT: &str = "%n has %a %l from %m.";
pub const DEFAULT_WATCHFMT_NOHOST: &str = "%n has %a %l.";
#[cfg(unix)]
pub type WATCH_STRUCT_UTMP = libc::utmpx;
pub fn utmp_user(u: &libc::utmpx) -> String { unsafe { CStr::from_ptr(u.ut_user.as_ptr()).to_string_lossy().into_owned() }
}
pub fn utmp_line(u: &libc::utmpx) -> String { unsafe { CStr::from_ptr(u.ut_line.as_ptr()).to_string_lossy().into_owned() }
}
pub fn utmp_host(u: &libc::utmpx) -> String { unsafe { CStr::from_ptr(u.ut_host.as_ptr()).to_string_lossy().into_owned() }
}
pub fn utmp_is_active(u: &libc::utmpx) -> bool { u.ut_type == libc::USER_PROCESS as i16
&& u.ut_user.first().copied().unwrap_or(0) != 0
}
#[cfg(test)]
pub fn utmp_make(user: &str, line: &str, host: &str, time: i64, pid: i32, ut_type: i16) -> libc::utmpx {
let mut u: libc::utmpx = unsafe { std::mem::zeroed() };
let mut copy = |dst: &mut [libc::c_char], src: &str| {
let bytes = src.as_bytes();
let n = bytes.len().min(dst.len().saturating_sub(1));
for (i, &b) in bytes[..n].iter().enumerate() {
dst[i] = b as libc::c_char;
}
};
copy(&mut u.ut_user, user);
copy(&mut u.ut_line, line);
copy(&mut u.ut_host, host);
u.ut_tv.tv_sec = time as libc::time_t;
u.ut_pid = pid;
u.ut_type = ut_type;
u
}
thread_local! {
static WTAB: std::cell::RefCell<Vec<libc::utmpx>> = const {
std::cell::RefCell::new(Vec::new())
};
static LASTWATCH: std::cell::Cell<i64> = const {
std::cell::Cell::new(0)
};
static LASTUTMPCHECK: std::cell::Cell<i64> = const {
std::cell::Cell::new(0)
};
static WATCH: std::cell::RefCell<Vec<String>> = const {
std::cell::RefCell::new(Vec::new())
};
}
pub fn set_watch_list(list: Vec<String>) {
WATCH.with(|w| *w.borrow_mut() = list);
}
pub fn should_check() -> bool {
if WATCH.with(|w| w.borrow().is_empty()) {
return false;
}
let now = SystemTime::now()
.duration_since(UNIX_EPOCH)
.unwrap_or_default()
.as_secs() as i64;
let interval = {
let raw = crate::ported::params::getiparam("LOGCHECK");
if raw > 0 { raw } else { 60 }
};
now - LASTWATCH.with(|t| t.get()) > interval
}
pub fn check_entry(entry: &libc::utmpx, current_user: &str) -> bool {
let user = utmp_user(entry);
let line = utmp_line(entry);
let host = utmp_host(entry);
WATCH.with(|w| {
let watch_list = w.borrow();
if watch_list.is_empty() {
return false;
}
if watch_list.first().map(|s| s.as_str()) == Some("all") {
return true;
}
let mut iter = watch_list.iter().peekable();
if iter.peek().map(|s| s.as_str()) == Some("notme") {
if user == current_user {
return false;
}
iter.next();
if iter.peek().is_none() {
return true;
}
}
for pattern in iter {
let mut rest = pattern.as_str();
let mut matched = true;
if !rest.starts_with('@') && !rest.starts_with('%') {
let end = rest.find(['@', '%']).unwrap_or(rest.len());
let user_pat = &rest[..end];
if !watchlog_match(user_pat, &user) {
matched = false;
}
rest = &rest[end..];
}
while !rest.is_empty() && matched {
if let Some(rest1) = rest.strip_prefix('%') {
let end = rest1.find('@').unwrap_or(rest1.len());
let line_pat = &rest1[..end];
if !watchlog_match(line_pat, &line) {
matched = false;
}
rest = &rest1[end..];
} else if let Some(rest1) = rest.strip_prefix('@') {
let end = rest1.find('%').unwrap_or(rest1.len());
let host_pat = &rest1[..end];
if !watchlog_match(host_pat, &host) {
matched = false;
}
rest = &rest1[end..];
} else {
break;
}
}
if matched {
return true;
}
}
false
})
}
pub fn watchlog_match(pattern: &str, value: &str) -> bool { if pattern == value {
return true;
}
if pattern.contains('*') || pattern.contains('?') {
crate::glob::matchpat(pattern, value, false, true)
} else {
false
}
}
pub fn dowatch() { let s: Vec<String> = WATCH.with(|w| w.borrow().clone());
crate::ported::signals::holdintr();
let wtab_empty = WTAB.with(|t| t.borrow().is_empty());
if wtab_empty {
let initial = readwtab(32);
WTAB.with(|t| *t.borrow_mut() = initial);
}
let utmp_path = utmp_file_path();
let mtime = std::fs::metadata(utmp_path)
.and_then(|m| m.modified())
.ok()
.and_then(|t| t.duration_since(std::time::UNIX_EPOCH).ok())
.map(|d| d.as_secs() as i64);
let last = LASTUTMPCHECK.with(|t| t.get());
match mtime {
Some(m) if m > last => {
LASTUTMPCHECK.with(|t| t.set(m)); }
_ => {
crate::ported::signals::noholdintr(); return; }
}
let wtabsz = WTAB.with(|t| t.borrow().len()) as i32;
let utab = readwtab(wtabsz + 4);
crate::ported::signals::noholdintr();
if crate::ported::utils::errflag.load(std::sync::atomic::Ordering::Relaxed) != 0 {
return;
}
crate::ported::signals_h::queue_signals();
let fmt = crate::ported::params::getsparam("WATCHFMT")
.unwrap_or_else(|| DEFAULT_WATCHFMT.to_string());
let wtab_snapshot: Vec<libc::utmpx> = WTAB.with(|t| t.borrow().clone());
let mut uct = utab.len();
let mut wct = wtab_snapshot.len();
let mut uidx = 0usize;
let mut widx = 0usize;
while uct > 0 || wct > 0 {
if crate::ported::utils::errflag.load(std::sync::atomic::Ordering::Relaxed) != 0 {
break;
}
let cmp_gt_zero = wct > 0 && uct > 0 && ucmp(&utab[uidx], &wtab_snapshot[widx]) > 0;
let cmp_lt_zero = uct > 0 && wct > 0 && ucmp(&utab[uidx], &wtab_snapshot[widx]) < 0;
if uct == 0 || cmp_gt_zero {
wct -= 1;
watchlog(0, &wtab_snapshot[widx], &s, &fmt);
widx += 1;
} else if wct == 0 || cmp_lt_zero {
uct -= 1;
watchlog(1, &utab[uidx], &s, &fmt);
uidx += 1;
} else {
uidx += 1;
widx += 1;
wct -= 1;
uct -= 1;
}
}
crate::ported::signals_h::unqueue_signals();
WTAB.with(|t| *t.borrow_mut() = utab);
use std::io::Write;
let _ = std::io::stdout().flush();
let now = unsafe { libc::time(std::ptr::null_mut()) as i64 };
LASTWATCH.with(|t| t.set(now));
}
fn utmp_file_path() -> &'static str {
#[cfg(target_os = "linux")] { "/var/run/utmp" }
#[cfg(target_os = "macos")] { "/var/run/utmpx" }
#[cfg(not(any(target_os = "linux", target_os = "macos")))] { "/dev/null" }
}
pub fn bin_log(_name: &str, _argv: &[String], _ops: &crate::ported::zsh_h::options, _func: i32) -> i32 {
let watch_set = crate::ported::params::getsparam("WATCH")
.map(|s| !s.is_empty())
.unwrap_or(false);
if !watch_set {
return 1;
}
WTAB.with(|t| t.borrow_mut().clear());
LASTUTMPCHECK.with(|t| t.set(0));
dowatch();
0
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_watch_initial_empty() {
WATCH.with(|w| assert!(w.borrow().is_empty()));
}
#[test]
fn test_glob_match() {
use crate::glob::matchpat;
assert!(matchpat("*", "anything", false, true));
assert!(matchpat("user*", "username", false, true));
assert!(matchpat("*name", "username", false, true));
assert!(matchpat("user?ame", "username", false, true));
assert!(!matchpat("user", "username", false, true));
}
#[test]
fn test_watch_match() {
assert!(watchlog_match("root", "root"));
assert!(watchlog_match("*", "anyuser"));
assert!(!watchlog_match("root", "admin"));
}
#[test]
fn test_format_watch_basic() {
let entry = utmp_make("testuser", "tty1", "localhost", 0, 1234, libc::USER_PROCESS as i16);
let result = watch3ary(&entry, true, "%n has %a %l");
assert!(result.contains("testuser"));
assert!(result.contains("logged on"));
assert!(result.contains("1"));
let result = watch3ary(&entry, false, "%n has %a");
assert!(result.contains("logged off"));
}
#[test]
fn test_format_watch_host() {
let entry = utmp_make("user", "pts/0", "host.example.com", 0, 1, libc::USER_PROCESS as i16);
let result = watch3ary(&entry, true, "%m");
assert_eq!(result, "host");
let result = watch3ary(&entry, true, "%M");
assert_eq!(result, "host.example.com");
}
#[test]
fn test_check_watch_entry_all() {
let entry = utmp_make("anyone", "pts/0", "", 0, 1, libc::USER_PROCESS as i16);
set_watch_list(vec!["all".to_string()]);
assert!(check_entry(&entry, "me"));
}
#[test]
fn test_check_watch_entry_notme() {
let entry = utmp_make("me", "pts/0", "", 0, 1, libc::USER_PROCESS as i16);
set_watch_list(vec!["notme".to_string()]);
assert!(!check_entry(&entry, "me"));
let other = utmp_make("other", "pts/0", "", 0, 1, libc::USER_PROCESS as i16);
assert!(check_entry(&other, "me"));
}
#[test]
fn test_matches_watch_pattern() {
let entry = utmp_make("admin", "pts/0", "server.local", 0, 1, libc::USER_PROCESS as i16);
set_watch_list(vec!["admin".to_string()]);
assert!(check_entry(&entry, "me"));
set_watch_list(vec!["admin@server.local".to_string()]);
assert!(check_entry(&entry, "me"));
set_watch_list(vec!["admin%pts/0".to_string()]);
assert!(check_entry(&entry, "me"));
set_watch_list(vec!["root".to_string()]);
assert!(!check_entry(&entry, "me"));
}
#[test]
fn test_session_type() {
let entry = utmp_make("user", "pts/0", "", 0, 1, libc::USER_PROCESS as i16);
assert!(utmp_is_active(&entry));
let dead = utmp_make("user", "pts/0", "", 0, 1, libc::DEAD_PROCESS as i16);
assert!(!utmp_is_active(&dead));
}
}
use crate::ported::zsh_h::{module, builtin};
use crate::ported::builtin::BUILTIN;
pub static bintab: std::sync::LazyLock<Vec<builtin>> = std::sync::LazyLock::new(|| vec![
BUILTIN("log", 0, Some(bin_log as crate::ported::zsh_h::HandlerFunc),
0, 0, 0, None, None),
]);
#[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 { if crate::ported::params::getsparam("WATCHFMT").is_none() {
crate::ported::params::setsparam("WATCHFMT", DEFAULT_WATCHFMT); }
if crate::ported::params::getsparam("LOGCHECK").is_none() {
crate::ported::params::setsparam("LOGCHECK", "60"); }
crate::ported::utils::addprepromptfn(checksched);
0
}
pub fn cleanup_(m: *const module) -> i32 { crate::ported::utils::delprepromptfn(checksched);
setfeatureenables(m, module_features(), None)
}
#[allow(unused_variables)]
pub fn finish_(m: *const module) -> i32 { 0
}
pub fn getlogtime(u: &libc::utmpx, inout: i32) -> i64 { if inout != 0 { return u.ut_tv.tv_sec as i64; }
let wtmp_path = wtmp_file_path();
let target_line = utmp_line(u);
if let Ok(file) = std::fs::File::open(wtmp_path) {
use std::io::{Read, Seek, SeekFrom};
let mut f = file;
let rec_size = std::mem::size_of::<libc::utmpx>() as i64;
if let Ok(end) = f.seek(SeekFrom::End(0)) {
let mut pos = end as i64 - rec_size;
while pos >= 0 {
if f.seek(SeekFrom::Start(pos as u64)).is_err() {
break;
}
let mut buf = vec![0u8; rec_size as usize];
if f.read_exact(&mut buf).is_err() {
break;
}
let rec = unsafe { std::ptr::read(buf.as_ptr() as *const libc::utmpx) };
if rec.ut_type == libc::USER_PROCESS
&& utmp_line(&rec) == target_line
{
return rec.ut_tv.tv_sec as i64; }
pos -= rec_size;
}
}
}
unsafe { libc::time(std::ptr::null_mut()) as i64 }
}
fn wtmp_file_path() -> &'static str {
#[cfg(target_os = "linux")] { "/var/log/wtmp" }
#[cfg(target_os = "macos")] { "/var/log/wtmpx" }
#[cfg(not(any(target_os = "linux", target_os = "macos")))] { "/dev/null" }
}
pub fn ucmp(u: &libc::utmpx, v: &libc::utmpx) -> i32 { let ut = u.ut_tv.tv_sec as i64;
let vt = v.ut_tv.tv_sec as i64;
if ut == vt { return match utmp_line(u).cmp(&utmp_line(v)) {
std::cmp::Ordering::Less => -1,
std::cmp::Ordering::Equal => 0,
std::cmp::Ordering::Greater => 1,
};
}
(ut - vt) as i32 }
pub fn readwtab(initial_sz: i32) -> Vec<libc::utmpx> { let wtabmax = if initial_sz < 2 { 32 } else { initial_sz } as usize;
let mut entries: Vec<libc::utmpx> = Vec::with_capacity(wtabmax); #[cfg(any(target_os = "linux", target_os = "macos"))]
unsafe {
libc::setutxent(); loop {
let entry = libc::getutxent(); if entry.is_null() { break; }
let ut = &*entry;
if ut.ut_type != libc::USER_PROCESS { continue; }
entries.push(std::ptr::read(entry));
}
libc::endutxent(); }
entries.sort_by(|a, b| match ucmp(a, b) {
n if n < 0 => std::cmp::Ordering::Less,
n if n > 0 => std::cmp::Ordering::Greater,
_ => std::cmp::Ordering::Equal,
});
entries }
pub fn watchlog(inout: i32, u: &libc::utmpx, w: &[String], fmt: &str) { let user_name = utmp_user(u);
if user_name.is_empty() {
return;
}
if w.first().map(|s| s.as_str()) == Some("all") {
emit_event(inout, u, fmt);
return;
}
let mut idx = 0;
if w.first().map(|s| s.as_str()) == Some("notme") {
let current = crate::ported::params::getsparam("USERNAME")
.or_else(|| crate::ported::params::getsparam("USER"))
.unwrap_or_default();
if user_name != current {
emit_event(inout, u, fmt);
return;
}
idx = 1;
}
let host_name = utmp_host(u);
let line_name = utmp_line(u);
while idx < w.len() {
let entry = &w[idx];
idx += 1;
let mut bad = false;
let chars: Vec<char> = entry.chars().collect();
let mut i = 0usize;
if !chars.is_empty() && chars[0] != '@' && chars[0] != '%' {
let mut j = i;
while j < chars.len() && chars[j] != '@' && chars[j] != '%' { j += 1; }
let v: String = chars[i..j].iter().collect();
if !watchlog_match(&v, &user_name) { bad = true; }
i = j;
}
loop {
if i >= chars.len() { break; }
if chars[i] == '%' { i += 1;
let mut j = i;
while j < chars.len() && chars[j] != '@' { j += 1; }
let v: String = chars[i..j].iter().collect();
if !watchlog_match(&v, &line_name) { bad = true; }
i = j;
} else if chars[i] == '@' { i += 1;
let mut j = i;
while j < chars.len() && chars[j] != '%' { j += 1; }
let v: String = chars[i..j].iter().collect();
if !watchlog_match(&v, &host_name) { bad = true; }
i = j;
} else {
break;
}
}
if !bad {
emit_event(inout, u, fmt);
return;
}
}
}
fn emit_event(inout: i32, u: &libc::utmpx, fmt: &str) {
use std::io::Write;
let line = watchlog2(inout, u, fmt, 1, 0);
let _ = writeln!(std::io::stdout(), "{}", line);
}
pub fn watchlog2(inout: i32, u: &libc::utmpx, fmt: &str, prnt: i32, fini: i32) -> String { let mut result = String::new();
let mut chars = fmt.chars().peekable();
let user = utmp_user(u);
let line = utmp_line(u);
let host = utmp_host(u);
let logged_in = inout != 0;
while let Some(c) = chars.peek().copied() {
if c == '\\' {
chars.next();
if let Some(esc) = chars.next() {
if prnt != 0 {
result.push(esc); }
} else if fini != 0 {
return result; } else {
break; }
continue;
}
if fini != 0 && (c as i32) == fini {
chars.next();
return result;
}
if c != '%' {
chars.next();
if prnt != 0 {
result.push(c); }
continue;
}
chars.next();
let directive = match chars.next() {
Some(d) => d,
None => break,
};
if directive == '(' {
let rest: String = chars.clone().collect();
let (out, advance) = watch3ary_inline(inout, u, &rest, prnt);
result.push_str(&out);
for _ in 0..advance {
chars.next();
}
continue;
}
if prnt == 0 {
continue; }
match directive {
'n' => result.push_str(&user), 'a' => result.push_str(if logged_in { "logged on" } else { "logged off" }), 'l' => { let trimmed = if line.starts_with("tty") { &line[3..] } else { &line };
result.push_str(trimmed);
}
'm' => { let short = host.split('.').next().unwrap_or(&host);
result.push_str(short);
}
'M' => result.push_str(&host), 'T' | 't' | '@' | 'W' | 'w' | 'D' => { let time = getlogtime(u, inout);
let mut fm2: String = match directive {
'@' | 't' => "%l:%M%p".to_string(), 'T' => "%H:%M".to_string(), 'w' => "%a %e".to_string(), 'W' => "%m/%d/%y".to_string(), 'D' => { if chars.peek() == Some(&'{') {
chars.next();
let mut buf = String::new();
loop {
let fc = match chars.next() { Some(c) => c, None => break };
if fc == '}' { break; }
if fc == '\\' {
if let Some(esc) = chars.next() {
buf.push(esc);
}
} else {
buf.push(fc);
}
}
if buf.is_empty() { "%y-%m-%d".to_string() } else { buf }
} else {
"%y-%m-%d".to_string() }
}
_ => unreachable!(),
};
let formatted = Local.timestamp_opt(time, 0).single()
.map(|dt| dt.format(&fm2).to_string())
.unwrap_or_default();
let trimmed = formatted.strip_prefix(' ').unwrap_or(&formatted);
result.push_str(trimmed);
let _ = fm2.len();
}
'%' => result.push('%'), _ => { result.push('%');
result.push(directive);
}
}
}
result
}
fn watch3ary_inline(inout: i32, u: &libc::utmpx, rest: &str, prnt: i32) -> (String, usize) {
let bytes: Vec<char> = rest.chars().collect();
if bytes.len() < 2 { return (String::new(), 0); }
let cond = bytes[0];
let sep = bytes[1];
let user = utmp_user(u);
let line = utmp_line(u);
let host = utmp_host(u);
let truth = match cond {
'n' => !user.is_empty(),
'a' => inout != 0,
'l' => if line.starts_with("tty") { line.len() > 3 } else { !line.is_empty() },
'm' | 'M' => !host.is_empty(),
_ => false,
};
let mut true_branch = String::new();
let mut false_branch = String::new();
let mut depth = 1;
let mut in_true = true;
let mut consumed = 2;
while consumed < bytes.len() {
let c = bytes[consumed];
consumed += 1;
if c == ')' {
depth -= 1;
if depth == 0 { break; }
}
if c == sep && depth == 1 {
in_true = false;
continue;
}
if c == '%' && consumed < bytes.len() && bytes[consumed] == '(' {
depth += 1;
}
if in_true { true_branch.push(c); } else { false_branch.push(c); }
}
let branch = if truth { &true_branch } else { &false_branch };
let rendered = if prnt != 0 {
watchlog2(inout, u, branch, 1, 0)
} else {
String::new()
};
(rendered, consumed)
}
pub fn watch3ary(entry: &libc::utmpx, logged_in: bool, fmt: &str) -> String { watchlog2(if logged_in { 1 } else { 0 }, entry, fmt, 1, 0)
}
pub fn checksched() { let watch_set = WATCH.with(|w| !w.borrow().is_empty());
if !watch_set { return; }
let now = unsafe { libc::time(std::ptr::null_mut()) as i64 }; let last = LASTWATCH.with(|t| t.get()); let logcheck: i64 = {
let raw = crate::ported::params::getiparam("LOGCHECK");
if raw > 0 { raw } else { 60 }
};
if (now - last) > logcheck { dowatch(); }
}
use crate::ported::zsh_h::features as features_t;
use std::sync::{Mutex, OnceLock};
static MODULE_FEATURES: OnceLock<Mutex<features_t>> = OnceLock::new();
fn module_features() -> &'static Mutex<features_t> {
MODULE_FEATURES.get_or_init(|| Mutex::new(features_t {
bn_list: None,
bn_size: 1,
cd_list: None,
cd_size: 0,
mf_list: None,
mf_size: 0,
pd_list: None,
pd_size: 2,
n_abstract: 0,
}))
}
fn featuresarray(_m: *const module, _f: &Mutex<features_t>) -> Vec<String> {
vec!["b:log".to_string(), "p:WATCH".to_string(), "p:watch".to_string()]
}
fn handlefeatures(
_m: *const module,
_f: &Mutex<features_t>,
enables: &mut Option<Vec<i32>>,
) -> i32 {
if enables.is_none() {
*enables = Some(vec![1; 3]);
}
0
}
fn setfeatureenables(
_m: *const module,
_f: &Mutex<features_t>,
_e: Option<&[i32]>,
) -> i32 {
0
}