use std::{
fs::File,
io::{self, BufRead, BufReader, Read},
path::PathBuf,
sync::OnceLock,
};
use anyhow::anyhow;
use libc::uid_t;
use rustix::{
fd::OwnedFd,
fs::{Mode, OFlags},
path::Arg,
};
use crate::collection::processes::{Pid, linux::is_str_numeric};
static PAGESIZE: OnceLock<u64> = OnceLock::new();
#[inline]
fn next_part<'a>(iter: &mut impl Iterator<Item = &'a str>) -> Result<&'a str, io::Error> {
iter.next()
.ok_or_else(|| io::Error::from(io::ErrorKind::InvalidData))
}
pub(crate) struct Stat {
pub comm: String,
pub state: char,
pub ppid: Pid,
pub utime: u64,
pub stime: u64,
rss: u64,
pub vsize: u64,
pub start_time: u64,
pub is_kernel_thread: bool,
pub priority: i32,
#[cfg(unix)]
pub nice: i32,
}
impl Stat {
fn from_file(mut f: File, buffer: &mut String) -> anyhow::Result<Stat> {
f.read_to_end(unsafe { buffer.as_mut_vec() })?;
let line = buffer.trim();
let (comm, rest) = {
let start_paren = line
.find('(')
.ok_or_else(|| anyhow!("start paren missing"))?;
let end_paren = line.find(')').ok_or_else(|| anyhow!("end paren missing"))?;
(
line[start_paren + 1..end_paren].to_string(),
&line[end_paren + 2..],
)
};
let mut rest = rest.split(' ');
let state = next_part(&mut rest)?
.chars()
.next()
.ok_or_else(|| anyhow!("missing state"))?;
let ppid: Pid = next_part(&mut rest)?.parse()?;
let mut rest = rest.skip(4);
let flags: u32 = next_part(&mut rest)?.parse()?;
let is_kernel_thread: bool = flags & 0x00200000 != 0;
let mut rest = rest.skip(4);
let utime: u64 = next_part(&mut rest)?.parse()?;
let stime: u64 = next_part(&mut rest)?.parse()?;
let _ = next_part(&mut rest)?;
let _ = next_part(&mut rest)?;
let priority: i32 = next_part(&mut rest)?.parse()?;
let nice: i32 = next_part(&mut rest)?.parse()?;
let _ = next_part(&mut rest)?;
let _ = next_part(&mut rest)?;
let start_time: u64 = next_part(&mut rest)?.parse()?;
let vsize: u64 = next_part(&mut rest)?.parse()?;
let rss: u64 = next_part(&mut rest)?.parse()?;
Ok(Stat {
comm,
state,
ppid,
utime,
stime,
rss,
vsize,
start_time,
is_kernel_thread,
priority,
nice,
})
}
#[inline]
pub fn rss_bytes(&self) -> u64 {
self.rss * PAGESIZE.get_or_init(|| rustix::param::page_size() as u64)
}
}
pub(crate) struct Io {
pub read_bytes: u64,
pub write_bytes: u64,
}
impl Io {
#[inline]
fn from_file(f: File, buffer: &mut String) -> anyhow::Result<Io> {
const NUM_FIELDS: u16 = 0; enum Fields {
ReadBytes,
WriteBytes,
}
let mut read_fields = 0;
let mut reader = BufReader::new(f);
let mut read_bytes = 0;
let mut write_bytes = 0;
while let Ok(bytes) = reader.read_line(buffer) {
if bytes > 0 {
if buffer.is_empty() {
continue;
}
let mut parts = buffer.split_whitespace();
if let Some(field) = parts.next() {
let curr_field = match field {
"read_bytes:" => Fields::ReadBytes,
"write_bytes:" => Fields::WriteBytes,
_ => {
buffer.clear();
continue;
}
};
if let Some(value) = parts.next() {
let value = value.parse::<u64>()?;
match curr_field {
Fields::ReadBytes => {
read_bytes = value;
read_fields += 1;
}
Fields::WriteBytes => {
write_bytes = value;
read_fields += 1;
}
}
}
}
if read_fields == NUM_FIELDS {
break;
}
buffer.clear();
} else {
break;
}
}
Ok(Io {
read_bytes,
write_bytes,
})
}
}
pub(crate) struct Process {
pub pid: Pid,
pub uid: Option<uid_t>,
pub stat: Stat,
pub io: Option<Io>,
pub cmdline: Option<String>,
}
#[inline]
fn reset(root: &mut PathBuf, buffer: &mut String) {
root.pop();
buffer.clear();
}
impl Process {
#[inline]
pub(crate) fn from_path(
pid_path: PathBuf, buffer: &mut String, get_threads: bool,
) -> anyhow::Result<(Process, Vec<PathBuf>)> {
buffer.clear();
let pid_dir = rustix::fs::openat(
rustix::fs::CWD,
pid_path.as_path(),
OFlags::PATH | OFlags::DIRECTORY | OFlags::CLOEXEC,
Mode::empty(),
)?;
let pid = pid_path
.as_path()
.components()
.next_back()
.and_then(|s| s.to_string_lossy().parse::<Pid>().ok())
.or_else(|| {
rustix::fs::readlinkat(rustix::fs::CWD, pid_path.as_path(), vec![])
.ok()
.and_then(|s| s.to_string_lossy().parse::<Pid>().ok())
})
.ok_or_else(|| anyhow!("PID for {pid_path:?} was not found"))?;
let uid = {
let metadata = rustix::fs::fstat(&pid_dir);
match metadata {
Ok(md) => Some(md.st_uid),
Err(_) => None,
}
};
let mut root = pid_path;
let stat =
open_at(&mut root, "stat", &pid_dir).and_then(|file| Stat::from_file(file, buffer))?;
reset(&mut root, buffer);
let cmdline = if cmdline(&mut root, &pid_dir, buffer).is_ok() {
Some(buffer.clone())
} else {
None
};
reset(&mut root, buffer);
let io = open_at(&mut root, "io", &pid_dir)
.and_then(|file| Io::from_file(file, buffer))
.ok();
reset(&mut root, buffer);
let threads = threads(&mut root, pid, get_threads);
Ok((
Process {
pid,
uid,
stat,
io,
cmdline,
},
threads,
))
}
}
#[inline]
fn cmdline(root: &mut PathBuf, fd: &OwnedFd, buffer: &mut String) -> anyhow::Result<()> {
let _ = open_at(root, "cmdline", fd).map(|mut file| file.read_to_string(buffer))?;
Ok(())
}
#[inline]
fn open_at(root: &mut PathBuf, child: &str, fd: &OwnedFd) -> anyhow::Result<File> {
root.push(child);
let new_fd = rustix::fs::openat(fd, &*root, OFlags::RDONLY | OFlags::CLOEXEC, Mode::empty())?;
Ok(File::from(new_fd))
}
#[inline]
fn threads(root: &mut PathBuf, pid: Pid, get_threads: bool) -> Vec<PathBuf> {
if get_threads {
root.push("task");
let Ok(task_dir) = rustix::fs::openat(
rustix::fs::CWD,
root.as_path(),
OFlags::RDONLY | OFlags::DIRECTORY | OFlags::CLOEXEC,
Mode::empty(),
) else {
return Vec::new();
};
if let Ok(task) = rustix::fs::Dir::read_from(task_dir) {
let pid_str = pid.to_string();
return task
.flatten()
.filter_map(|thread_dir| {
let file_name = thread_dir.file_name();
let file_name = file_name.to_string_lossy();
let file_name = file_name.trim();
if is_str_numeric(file_name) && file_name != pid_str {
Some(root.join(file_name).to_path_buf())
} else {
None
}
})
.collect::<Vec<_>>();
}
}
Vec::new()
}