Skip to main content

vs_cli/
caller.rs

1//! Stable per-caller key. Identifies the process tree that invoked
2//! `vs` so different shells / agents get different auto-sessions.
3//!
4//! Key is `<parent_pid>-<parent_start_time>`. Parent start time
5//! disambiguates PID reuse: even if the OS recycles a PID after a
6//! parent exits, the new process has a different start time.
7
8#[cfg(unix)]
9fn parent_pid() -> u32 {
10    #[allow(clippy::cast_sign_loss)]
11    let pid = unsafe { libc::getppid() } as u32;
12    pid
13}
14
15#[cfg(windows)]
16fn parent_pid() -> u32 {
17    use windows::Win32::Foundation::CloseHandle;
18    use windows::Win32::System::Diagnostics::ToolHelp::{
19        CreateToolhelp32Snapshot, Process32FirstW, Process32NextW, PROCESSENTRY32W,
20        TH32CS_SNAPPROCESS,
21    };
22    use windows::Win32::System::Threading::GetCurrentProcessId;
23    unsafe {
24        let Ok(snap) = CreateToolhelp32Snapshot(TH32CS_SNAPPROCESS, 0) else {
25            return 0;
26        };
27        let me = GetCurrentProcessId();
28        let mut entry = PROCESSENTRY32W {
29            dwSize: u32::try_from(std::mem::size_of::<PROCESSENTRY32W>()).unwrap_or(0),
30            ..PROCESSENTRY32W::default()
31        };
32        let mut found = 0;
33        if Process32FirstW(snap, &raw mut entry).is_ok() {
34            loop {
35                if entry.th32ProcessID == me {
36                    found = entry.th32ParentProcessID;
37                    break;
38                }
39                if Process32NextW(snap, &raw mut entry).is_err() {
40                    break;
41                }
42            }
43        }
44        let _ = CloseHandle(snap);
45        found
46    }
47}
48
49#[cfg(target_os = "macos")]
50fn parent_start_time(ppid: u32) -> Option<u64> {
51    let mut info: libc::proc_bsdinfo = unsafe { std::mem::zeroed() };
52    #[allow(clippy::cast_possible_wrap)]
53    let ret = unsafe {
54        libc::proc_pidinfo(
55            ppid as i32,
56            libc::PROC_PIDTBSDINFO,
57            0,
58            std::ptr::from_mut(&mut info).cast::<libc::c_void>(),
59            i32::try_from(std::mem::size_of::<libc::proc_bsdinfo>()).unwrap_or(0),
60        )
61    };
62    if ret > 0 {
63        Some(info.pbi_start_tvsec)
64    } else {
65        None
66    }
67}
68
69#[cfg(target_os = "linux")]
70fn parent_start_time(ppid: u32) -> Option<u64> {
71    let stat = std::fs::read_to_string(format!("/proc/{ppid}/stat")).ok()?;
72    // The "comm" field at index 1 may contain spaces and parens; skip
73    // past the final ')' before splitting on whitespace. `starttime`
74    // is field 22 of the original record, which is index 19 after the
75    // closing paren (state is the first remaining field).
76    let after_paren = stat.rsplit_once(')')?.1;
77    let fields: Vec<&str> = after_paren.split_whitespace().collect();
78    fields.get(19)?.parse().ok()
79}
80
81#[cfg(target_os = "windows")]
82fn parent_start_time(ppid: u32) -> Option<u64> {
83    use windows::Win32::Foundation::{CloseHandle, FILETIME};
84    use windows::Win32::System::Threading::{
85        GetProcessTimes, OpenProcess, PROCESS_QUERY_LIMITED_INFORMATION,
86    };
87    unsafe {
88        let h = OpenProcess(PROCESS_QUERY_LIMITED_INFORMATION, false, ppid).ok()?;
89        let mut creation = FILETIME::default();
90        let mut exit = FILETIME::default();
91        let mut kernel = FILETIME::default();
92        let mut user = FILETIME::default();
93        let r = GetProcessTimes(
94            h,
95            &raw mut creation,
96            &raw mut exit,
97            &raw mut kernel,
98            &raw mut user,
99        );
100        let _ = CloseHandle(h);
101        r.ok()?;
102        let high = u64::from(creation.dwHighDateTime);
103        let low = u64::from(creation.dwLowDateTime);
104        Some((high << 32) | low)
105    }
106}
107
108/// Return the stable caller key, or `None` if either the parent PID
109/// could not be read (extremely rare; would mean the kernel refused
110/// to answer). The key is filename-safe: only digits and a single
111/// hyphen.
112#[must_use]
113pub fn caller_key() -> Option<String> {
114    let ppid = parent_pid();
115    if ppid == 0 {
116        return None;
117    }
118    let start = parent_start_time(ppid).unwrap_or(0);
119    Some(format!("{ppid}-{start}"))
120}