use std::io;
#[derive(Clone, Debug, Eq, PartialEq)]
#[non_exhaustive]
pub struct ProcessEntry {
pub pid: u32,
pub parent_pid: u32,
pub command_line: Option<String>,
pub working_set_bytes: Option<u64>,
}
pub fn process_table() -> io::Result<Vec<ProcessEntry>> {
imp::table()
}
#[cfg(any(target_os = "linux", target_os = "android", test))]
fn parse_stat_parent(stat: &str) -> Option<u32> {
let rest = &stat[stat.rfind(')')? + 1..];
rest.split_whitespace().nth(1)?.parse().ok()
}
#[cfg(any(target_os = "linux", target_os = "android", test))]
fn join_cmdline(bytes: &[u8]) -> Option<String> {
let trimmed = bytes.strip_suffix(&[0]).unwrap_or(bytes);
if trimmed.is_empty() {
return None;
}
let text = String::from_utf8_lossy(trimmed);
let joined = text.split('\0').collect::<Vec<_>>().join(" ");
(!joined.trim().is_empty()).then_some(joined)
}
#[cfg(any(target_os = "macos", test))]
fn join_procargs2(buffer: &[u8]) -> Option<String> {
let count = usize::try_from(i32::from_ne_bytes(buffer.get(..4)?.try_into().ok()?)).ok()?;
let mut i = 4;
while i < buffer.len() && buffer[i] != 0 {
i += 1;
}
while i < buffer.len() && buffer[i] == 0 {
i += 1;
}
let mut args = Vec::with_capacity(count);
for _ in 0..count {
if i >= buffer.len() {
return None;
}
let start = i;
while i < buffer.len() && buffer[i] != 0 {
i += 1;
}
if i >= buffer.len() {
return None; }
args.push(String::from_utf8_lossy(&buffer[start..i]).into_owned());
i += 1;
}
let joined = args.join(" ");
(!joined.is_empty()).then_some(joined)
}
#[cfg(any(target_os = "linux", target_os = "android"))]
mod imp {
use super::{join_cmdline, parse_stat_parent, ProcessEntry};
use std::io;
pub(super) fn table() -> io::Result<Vec<ProcessEntry>> {
let page = unsafe { libc::sysconf(libc::_SC_PAGESIZE) }.max(1) as u64;
let mut rows = Vec::new();
for entry in std::fs::read_dir("/proc")?.flatten() {
let Some(pid) = entry
.file_name()
.to_str()
.and_then(|name| name.parse::<u32>().ok())
else {
continue;
};
let Some(parent_pid) = std::fs::read(format!("/proc/{pid}/stat"))
.ok()
.and_then(|stat| parse_stat_parent(&String::from_utf8_lossy(&stat)))
else {
continue;
};
let working_set_bytes = std::fs::read_to_string(format!("/proc/{pid}/statm"))
.ok()
.and_then(|statm| statm.split_whitespace().nth(1)?.parse::<u64>().ok())
.map(|pages| pages.saturating_mul(page));
let command_line = std::fs::read(format!("/proc/{pid}/cmdline"))
.ok()
.and_then(|bytes| join_cmdline(&bytes));
rows.push(ProcessEntry {
pid,
parent_pid,
command_line,
working_set_bytes,
});
}
Ok(rows)
}
}
#[cfg(target_os = "macos")]
mod imp {
use super::{join_procargs2, ProcessEntry};
use std::io;
pub(super) fn table() -> io::Result<Vec<ProcessEntry>> {
const PROC_ALL_PIDS: u32 = 1;
let size = std::mem::size_of::<libc::pid_t>();
let needed = unsafe { libc::proc_listpids(PROC_ALL_PIDS, 0, std::ptr::null_mut(), 0) };
if needed <= 0 {
return Err(io::Error::last_os_error());
}
let mut pids = vec![0 as libc::pid_t; needed as usize / size + 64];
let written = unsafe {
libc::proc_listpids(
PROC_ALL_PIDS,
0,
pids.as_mut_ptr().cast(),
(pids.len() * size) as libc::c_int,
)
};
if written <= 0 {
return Err(io::Error::last_os_error());
}
pids.truncate(written as usize / size);
let mut rows = Vec::new();
for pid in pids.into_iter().filter(|pid| *pid > 0) {
let Some(parent_pid) = parent_of(pid) else {
continue; };
rows.push(ProcessEntry {
pid: pid as u32,
parent_pid,
command_line: command_line(pid as u32),
working_set_bytes: resident(pid),
});
}
Ok(rows)
}
fn parent_of(pid: libc::pid_t) -> Option<u32> {
let mut info: libc::proc_bsdinfo = unsafe { std::mem::zeroed() };
let size = std::mem::size_of::<libc::proc_bsdinfo>() as libc::c_int;
let written = unsafe {
libc::proc_pidinfo(
pid,
libc::PROC_PIDTBSDINFO,
0,
(&mut info as *mut libc::proc_bsdinfo).cast(),
size,
)
};
(written == size && info.pbi_pid == pid as u32).then_some(info.pbi_ppid)
}
fn resident(pid: libc::pid_t) -> Option<u64> {
let mut info: libc::proc_taskinfo = unsafe { std::mem::zeroed() };
let size = std::mem::size_of::<libc::proc_taskinfo>() as libc::c_int;
let written = unsafe {
libc::proc_pidinfo(
pid,
libc::PROC_PIDTASKINFO,
0,
(&mut info as *mut libc::proc_taskinfo).cast(),
size,
)
};
(written == size).then_some(info.pti_resident_size)
}
fn command_line(pid: u32) -> Option<String> {
let mut name = [libc::CTL_KERN, libc::KERN_PROCARGS2, pid as libc::c_int];
let mut capacity: usize = 0;
let probed = unsafe {
libc::sysctl(
name.as_mut_ptr(),
3,
std::ptr::null_mut(),
&mut capacity,
std::ptr::null_mut(),
0,
)
};
if probed != 0 {
return None;
}
let mut buffer = vec![0u8; capacity + 4096];
let mut length = buffer.len();
let read = unsafe {
libc::sysctl(
name.as_mut_ptr(),
3,
buffer.as_mut_ptr().cast(),
&mut length,
std::ptr::null_mut(),
0,
)
};
if read != 0 {
return None;
}
buffer.truncate(length);
join_procargs2(&buffer)
}
}
#[cfg(windows)]
mod imp {
use super::ProcessEntry;
use crate::process_windows::os_error;
use std::{ffi::c_void, io};
use windows::Win32::{
Foundation::{CloseHandle, HANDLE},
System::{
Diagnostics::ToolHelp::{
CreateToolhelp32Snapshot, Process32FirstW, Process32NextW, PROCESSENTRY32W,
TH32CS_SNAPPROCESS,
},
ProcessStatus::{GetProcessMemoryInfo, PROCESS_MEMORY_COUNTERS},
Threading::{OpenProcess, PROCESS_QUERY_LIMITED_INFORMATION},
},
};
#[link(name = "ntdll")]
extern "system" {
fn NtQueryInformationProcess(
process: *mut c_void,
class: u32,
information: *mut c_void,
length: u32,
returned: *mut u32,
) -> i32;
}
const PROCESS_COMMAND_LINE_INFORMATION: u32 = 60;
const STATUS_INFO_LENGTH_MISMATCH: u32 = 0xC000_0004;
const STATUS_BUFFER_OVERFLOW: u32 = 0x8000_0005;
const STATUS_BUFFER_TOO_SMALL: u32 = 0xC000_0023;
#[repr(C)]
struct UnicodeString {
length: u16,
maximum_length: u16,
buffer: *const u16,
}
struct Owned(HANDLE);
impl Drop for Owned {
fn drop(&mut self) {
let _ = unsafe { CloseHandle(self.0) };
}
}
pub(super) fn table() -> io::Result<Vec<ProcessEntry>> {
let snapshot =
Owned(unsafe { CreateToolhelp32Snapshot(TH32CS_SNAPPROCESS, 0) }.map_err(os_error)?);
let mut entry: PROCESSENTRY32W = unsafe { std::mem::zeroed() };
entry.dwSize = std::mem::size_of::<PROCESSENTRY32W>() as u32;
unsafe { Process32FirstW(snapshot.0, &mut entry) }.map_err(os_error)?;
let mut rows = Vec::new();
loop {
let pid = entry.th32ProcessID;
let (command_line, working_set_bytes) = if pid == 0 {
(None, None) } else {
read_process(pid)
};
rows.push(ProcessEntry {
pid,
parent_pid: entry.th32ParentProcessID,
command_line,
working_set_bytes,
});
if unsafe { Process32NextW(snapshot.0, &mut entry) }.is_err() {
break;
}
}
Ok(rows)
}
fn read_process(pid: u32) -> (Option<String>, Option<u64>) {
let Ok(handle) = (unsafe { OpenProcess(PROCESS_QUERY_LIMITED_INFORMATION, false, pid) })
else {
return (None, None);
};
let process = Owned(handle);
(command_line(&process), working_set(&process))
}
fn working_set(process: &Owned) -> Option<u64> {
let mut counters = PROCESS_MEMORY_COUNTERS::default();
let size = std::mem::size_of::<PROCESS_MEMORY_COUNTERS>() as u32;
counters.cb = size;
unsafe { GetProcessMemoryInfo(process.0, &mut counters, size) }.ok()?;
Some(counters.WorkingSetSize as u64)
}
fn command_line(process: &Owned) -> Option<String> {
let mut size: usize = 1024;
for _ in 0..4 {
let mut buffer = vec![0u64; size.div_ceil(8)];
let mut returned = 0u32;
let status = unsafe {
NtQueryInformationProcess(
process.0 .0,
PROCESS_COMMAND_LINE_INFORMATION,
buffer.as_mut_ptr().cast(),
(buffer.len() * 8) as u32,
&mut returned,
)
} as u32;
match status {
0 => return read_unicode_string(&buffer),
STATUS_INFO_LENGTH_MISMATCH | STATUS_BUFFER_OVERFLOW | STATUS_BUFFER_TOO_SMALL => {
size = (returned as usize).max(size * 2);
}
_ => return None, }
}
None
}
fn read_unicode_string(buffer: &[u64]) -> Option<String> {
let base = buffer.as_ptr() as usize;
let end = base + buffer.len() * 8;
if buffer.len() * 8 < std::mem::size_of::<UnicodeString>() {
return None;
}
let header = unsafe { &*(buffer.as_ptr().cast::<UnicodeString>()) };
let text = header.buffer as usize;
let bytes = usize::from(header.length);
if bytes == 0 {
return None;
}
if text < base + std::mem::size_of::<UnicodeString>()
|| !text.is_multiple_of(2)
|| !bytes.is_multiple_of(2)
|| text + bytes > end
{
return None;
}
let units = unsafe { std::slice::from_raw_parts(header.buffer, bytes / 2) };
Some(String::from_utf16_lossy(units))
}
}
#[cfg(not(any(
target_os = "linux",
target_os = "android",
target_os = "macos",
windows
)))]
mod imp {
use super::ProcessEntry;
use std::io;
pub(super) fn table() -> io::Result<Vec<ProcessEntry>> {
Err(io::Error::from(io::ErrorKind::Unsupported))
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn stat_parent_survives_spaces_and_parens_in_the_name() {
assert_eq!(parse_stat_parent("42 (bash) S 7 42 42 0 -1"), Some(7));
assert_eq!(parse_stat_parent("42 (a b) c) R 9 1 1"), Some(9));
assert_eq!(parse_stat_parent("garbage"), None);
assert_eq!(parse_stat_parent("42 (x) S"), None);
}
#[test]
fn cmdline_joins_arguments_and_empty_is_none() {
assert_eq!(
join_cmdline(b"ls\0-l\0/tmp\0").as_deref(),
Some("ls -l /tmp")
);
assert_eq!(join_cmdline(b"sd-server\0").as_deref(), Some("sd-server"));
assert_eq!(join_cmdline(b""), None);
assert_eq!(join_cmdline(b"\0"), None);
}
#[test]
fn procargs2_yields_arguments_without_environment() {
let mut buffer = 2i32.to_ne_bytes().to_vec();
buffer.extend_from_slice(b"/bin/tool\0\0\0tool\0--flag\0HOME=/x\0");
assert_eq!(join_procargs2(&buffer).as_deref(), Some("tool --flag"));
assert_eq!(join_procargs2(&3i32.to_ne_bytes()), None);
let mut cut = 1i32.to_ne_bytes().to_vec();
cut.extend_from_slice(b"/bin/tool\0tool");
assert_eq!(join_procargs2(&cut), None);
}
#[cfg(unix)]
fn own_parent() -> u32 {
unsafe { libc::getppid() as u32 }
}
#[cfg(any(target_os = "linux", target_os = "macos", windows))]
#[test]
fn the_table_contains_this_process_with_its_parent_and_memory() {
let rows = process_table().expect("process table");
let me = rows
.iter()
.find(|row| row.pid == std::process::id())
.expect("own pid is listed");
assert_ne!(me.parent_pid, std::process::id());
#[cfg(unix)]
assert_eq!(me.parent_pid, own_parent());
assert!(me.working_set_bytes.unwrap_or(0) > 0);
let line = me.command_line.as_deref().expect("own command line");
assert!(!line.is_empty());
#[cfg(unix)]
{
let argv: Vec<String> = std::env::args_os()
.map(|arg| arg.to_string_lossy().into_owned())
.collect();
assert_eq!(line, argv.join(" "));
}
let exe = std::env::current_exe().unwrap();
let stem = exe.file_stem().unwrap().to_string_lossy().into_owned();
assert!(line.contains(&stem), "{line} should name {stem}");
}
#[cfg(any(target_os = "linux", target_os = "macos", windows))]
#[test]
fn a_spawned_child_is_listed_under_this_parent_with_its_arguments() {
use std::process::{Command, Stdio};
const MARK: &str = "7654";
let mut command = if cfg!(windows) {
let mut c = Command::new("ping.exe");
c.args(["-n", MARK, "127.0.0.1"]);
c
} else {
let mut c = Command::new("sleep");
c.arg(MARK);
c
};
let mut child = command
.stdin(Stdio::null())
.stdout(Stdio::null())
.stderr(Stdio::null())
.spawn()
.expect("spawn");
let pid = child.id();
let mut found = None;
for _ in 0..50 {
found = process_table().unwrap().into_iter().find(|row| {
row.pid == pid
&& row
.command_line
.as_deref()
.is_some_and(|line| line.contains(MARK))
});
if found.is_some() {
break;
}
std::thread::sleep(std::time::Duration::from_millis(100));
}
let _ = child.kill();
let _ = child.wait();
let row = found.expect("child listed with its command line");
assert_eq!(row.parent_pid, std::process::id());
let line = row.command_line.unwrap();
let expected = if cfg!(windows) {
format!("-n {MARK} 127.0.0.1")
} else {
format!("sleep {MARK}")
};
assert!(line.contains(&expected), "{line} should contain {expected}");
}
}