use crate::{datetime::*, *};
use anyhow::{ensure, Context};
use std::{fs::{read_dir, DirEntry, File},
io::prelude::*};
#[derive(Debug)]
pub struct Info {
pub cmdline: String,
pub start: i64,
pub pid: i32,
}
impl std::fmt::Display for Info {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
let pid = format!("Pid {}: ", self.pid);
let capacity = f.precision().unwrap_or(45).saturating_sub(pid.len());
let cmdlen = self.cmdline.len();
if capacity >= cmdlen || cmdlen < 4 {
write!(f, "{pid}{}", &self.cmdline)
} else if capacity > 3 {
write!(f, "{pid}...{}", &self.cmdline[(cmdlen - capacity + 3)..])
} else {
write!(f, "{pid}...")
}
}
}
fn get_proc_info(filter: Option<&str>,
entry: &DirEntry,
clocktick: i64,
time_ref: i64)
-> Option<Info> {
let pid = i32::from_str(&entry.file_name().to_string_lossy()).ok()?;
let mut stat = String::new();
File::open(entry.path().join("stat")).ok()?.read_to_string(&mut stat).ok()?;
let (cmd_start, cmd_end) = (stat.find('(')? + 1, stat.rfind(')')?);
if filter.map_or(false, |f| f != &stat[cmd_start..cmd_end]) {
return None;
}
let start_time = i64::from_str(stat[cmd_end + 1..].split(' ').nth(20)?).ok()?;
let mut cmdline = String::new();
File::open(entry.path().join("cmdline")).ok()?.read_to_string(&mut cmdline).ok()?;
cmdline = cmdline.replace('\0', " ").trim().into();
Some(Info { cmdline, start: time_ref + start_time / clocktick, pid })
}
pub fn get_all_info(filter: Option<&str>) -> Vec<Info> {
get_all_info_result(filter).unwrap_or_else(|e| {
log_err(e);
vec![]
})
}
fn get_all_info_result(filter: Option<&str>) -> Result<Vec<Info>, Error> {
let clocktick: i64 = unsafe { libc::sysconf(libc::_SC_CLK_TCK) };
ensure!(clocktick > 0, "Failed getting system clock ticks");
let mut uptimestr = String::new();
File::open("/proc/uptime").context("Opening /proc/uptime")?
.read_to_string(&mut uptimestr)
.context("Reading /proc/uptime")?;
let uptime = i64::from_str(uptimestr.split('.').next().unwrap()).unwrap();
let time_ref = epoch_now() - uptime;
let mut ret: Vec<Info> = Vec::new();
for entry in read_dir("/proc/").context("Listing /proc/")? {
if let Some(i) = get_proc_info(filter, &entry?, clocktick, time_ref) {
ret.push(i)
}
}
Ok(ret)
}
#[cfg(test)]
mod tests {
use regex::Regex;
use std::{collections::BTreeMap, process::Command};
use time::{macros::format_description, PrimitiveDateTime};
use crate::proces::*;
fn parse_ps_time(s: &str) -> i64 {
let fmt = format_description!("[month repr:short] [day padding:space] [hour]:[minute]:[second] [year]");
PrimitiveDateTime::parse(s, &fmt).expect(&format!("Cannot parse {}", s))
.assume_utc() .unix_timestamp()
}
#[test] #[rustfmt::skip]
fn start_time() {
let mut info: BTreeMap<i32,(String,Option<i64>,Option<i64>)> = get_all_info(None)
.iter()
.fold(BTreeMap::new(), |mut a,i| {a.insert(i.pid, (i.cmdline.clone(),Some(i.start),None)); a});
let ps_start = epoch_now();
let re = Regex::new("^ *([0-9]+) [A-Za-z]+ ([a-zA-Z0-9: ]+)$").unwrap();
let cmd = Command::new("ps").env("TZ", "UTC")
.env("LC_ALL", "C") .args(&["-o",
"pid,lstart", "-ax", "--no-header"]) .output()
.expect("failed to execute ps");
for lineres in cmd.stdout.lines() {
if let Ok(line) = lineres {
match re.captures(&line) {
Some(c) => {
let pid = c.get(1).unwrap().as_str().parse::<i32>().unwrap();
let time = parse_ps_time(c.get(2).unwrap().as_str());
if let Some((comm, t, None)) =
info.insert(pid, ("?".into(), None, Some(time)))
{
info.insert(pid, (comm, t, Some(time)));
}
},
None => assert!(false, "Couldn't parse {}", line),
}
}
}
assert!(info.len() > 5, "Only {} processes found", info.len());
let mut e: u32 = 0;
for (pid, times) in info {
match times {
(ref c,Some(t), None) => {e+=1; println!("WARN {:>20} {:>7} {}: disappeared after rust run", c, pid, fmt_utctime(t));},
(ref c,None, Some(t)) if t >= ps_start -1 => {e+=1; println!("WARN {:>20} {:>7} {}: appeared right after rust run", c, pid, fmt_utctime(t));},
(ref c,None, Some(t)) => {e+=10;println!("ERR {:>20} {:>7} {}: seen by ps but not by rust", c, pid, fmt_utctime(t));},
(ref c,Some(tr), Some(tp)) if tr == tp => {e+=0; println!("OK {:>20} {:>7} {}: same time", c, pid, fmt_utctime(tr));},
(ref c,Some(tr), Some(tp)) if (tr-tp).abs() < 2 => {e+=0; println!("IGN {:>20} {:>7} {}: {} secs diff {}", c, pid, fmt_utctime(tr), tr-tp, fmt_utctime(tp));},
(ref c,Some(tr), Some(tp)) if (tr-tp).abs() < 5 => {e+=1; println!("WARN {:>20} {:>7} {}: {} secs diff {}", c, pid, fmt_utctime(tr), tr-tp, fmt_utctime(tp));},
(ref c,Some(tr), Some(tp)) => {e+=5; println!("ERR {:>20} {:>7} {}: {} secs diff {}", c, pid, fmt_utctime(tr), tr-tp, fmt_utctime(tp));},
(ref c,None, None) => {e+=10;println!("ERR {:>20} {:>7}: no times", c, pid);},
}
}
assert!(e < 10, "Got failure score of {}", e);
}
#[test]
fn format_info() {
let s = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ";
let t: Vec<(i32, usize, usize, &str)> = vec![ (1, 1, 1, "Pid 1: a"),
(1, 2, 1, "Pid 1: ab"),
(2, 3, 1, "Pid 2: abc"),
(3, 4, 1, "Pid 3: ..."),
(4, 5, 1, "Pid 4: ..."),
(330, 1, 1, "Pid 330: a"),
(331, 2, 1, "Pid 331: ab"),
(332, 3, 1, "Pid 332: abc"),
(333, 4, 1, "Pid 333: ..."),
(334, 5, 1, "Pid 334: ..."),
(1, 1, 12, "Pid 1: a"),
(1, 2, 12, "Pid 1: ab"),
(1, 3, 12, "Pid 1: abc"),
(1, 4, 12, "Pid 1: abcd"),
(1, 5, 12, "Pid 1: abcde"),
(12, 4, 12, "Pid 12: abcd"),
(123, 3, 12, "Pid 123: abc"),
(1234, 2, 12, "Pid 1234: ab"),
(1, 6, 12, "Pid 1: ...ef"),
(1, 7, 12, "Pid 1: ...fg"),
(1, 8, 12, "Pid 1: ...gh"),
(22, 9, 12, "Pid 22: ...i"),];
for (pid, cmdlen, precision, out) in t.into_iter() {
dbg!((pid, cmdlen, precision, out));
let i = Info { pid, cmdline: s[..cmdlen].to_string(), start: 0 };
let f = format!("{1:.0$}", precision, i);
assert!(precision < 10 || f.len() <= precision, "{} <= {}", f.len(), precision);
assert_eq!(f, out);
}
}
}