#[must_use]
pub fn observe_parent() -> Option<String> {
let ppid = parent_pid()?;
match process_name(ppid) {
Some(name) if !name.is_empty() => Some(format!("{name} (pid {ppid})")),
_ => Some(format!("pid {ppid}")),
}
}
#[cfg(unix)]
fn parent_pid() -> Option<i32> {
let ppid = unsafe { libc::getppid() };
if ppid > 0 { Some(ppid) } else { None }
}
#[cfg(windows)]
fn parent_pid() -> Option<i32> {
win::find_process(std::process::id()).map(|f| f.parent_pid as i32)
}
#[cfg(not(any(unix, windows)))]
fn parent_pid() -> Option<i32> {
None
}
#[cfg(target_os = "macos")]
fn process_name(pid: i32) -> Option<String> {
const PROC_PIDPATHINFO_MAXSIZE: usize = 4096;
unsafe extern "C" {
fn proc_pidpath(
pid: libc::c_int,
buffer: *mut libc::c_void,
buffersize: u32,
) -> libc::c_int;
}
let mut buf = vec![0u8; PROC_PIDPATHINFO_MAXSIZE];
let n = unsafe {
proc_pidpath(
pid as libc::c_int,
buf.as_mut_ptr() as *mut libc::c_void,
buf.len() as u32,
)
};
if n <= 0 {
return None;
}
buf.truncate(n as usize);
String::from_utf8(buf).ok().filter(|s| !s.is_empty())
}
#[cfg(all(unix, not(target_os = "macos")))]
fn process_name(pid: i32) -> Option<String> {
if let Ok(exe) = std::fs::read_link(format!("/proc/{pid}/exe")) {
if let Some(s) = exe.to_str() {
if !s.is_empty() {
return Some(s.to_string());
}
}
}
std::fs::read_to_string(format!("/proc/{pid}/comm"))
.ok()
.map(|s| s.trim_end().to_string())
.filter(|s| !s.is_empty())
}
#[cfg(windows)]
fn process_name(pid: i32) -> Option<String> {
win::find_process(pid as u32).and_then(|f| f.exe)
}
#[cfg(not(any(unix, windows)))]
fn process_name(_pid: i32) -> Option<String> {
None
}
#[cfg(windows)]
mod win {
use windows::Win32::Foundation::CloseHandle;
use windows::Win32::System::Diagnostics::ToolHelp::{
CreateToolhelp32Snapshot, PROCESSENTRY32W, Process32FirstW, Process32NextW,
TH32CS_SNAPPROCESS,
};
pub(super) struct Found {
pub parent_pid: u32,
pub exe: Option<String>,
}
pub(super) fn find_process(pid: u32) -> Option<Found> {
unsafe {
let snapshot = CreateToolhelp32Snapshot(TH32CS_SNAPPROCESS, 0).ok()?;
let mut entry = PROCESSENTRY32W {
dwSize: core::mem::size_of::<PROCESSENTRY32W>() as u32,
..Default::default()
};
let mut found = None;
if Process32FirstW(snapshot, &mut entry).is_ok() {
loop {
if entry.th32ProcessID == pid {
found = Some(Found {
parent_pid: entry.th32ParentProcessID,
exe: exe_name(&entry.szExeFile),
});
break;
}
if Process32NextW(snapshot, &mut entry).is_err() {
break;
}
}
}
let _ = CloseHandle(snapshot);
found
}
}
fn exe_name(buf: &[u16; 260]) -> Option<String> {
let len = buf.iter().position(|&c| c == 0).unwrap_or(buf.len());
if len == 0 {
return None;
}
String::from_utf16(&buf[..len])
.ok()
.filter(|s| !s.is_empty())
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn observe_parent_returns_non_empty_identity_with_pid() {
let id = observe_parent().expect("a parent process should be observable on the test host");
assert!(!id.is_empty());
assert!(
id.contains("pid "),
"identity should always carry the observed pid, got {id:?}"
);
}
#[test]
fn observed_identity_is_a_clean_single_line() {
let id = observe_parent().unwrap();
assert!(!id.contains('\0'));
assert!(!id.contains('\n'));
}
}