Skip to main content

runtime_paths/
unix_process.rs

1use std::path::PathBuf;
2use std::process::{Command, Stdio};
3
4pub fn is_pid_alive(pid: u32) -> bool {
5    Command::new("kill")
6        .arg("-0")
7        .arg(pid.to_string())
8        .stdout(Stdio::null())
9        .stderr(Stdio::null())
10        .status()
11        .map(|status| status.success())
12        .unwrap_or(false)
13}
14
15pub fn process_executable_path(pid: u32) -> Option<PathBuf> {
16    #[cfg(target_os = "linux")]
17    {
18        std::fs::read_link(format!("/proc/{}/exe", pid)).ok()
19    }
20    #[cfg(target_os = "macos")]
21    {
22        // proc_pidpath returns the full image path, including spaces in .app
23        // bundles (e.g. "/Applications/Meow App.app/.../meowclient"). Do not
24        // parse `ps -o args=` — whitespace splitting truncates those paths.
25        let mut buf = [0u8; libc::PROC_PIDPATHINFO_MAXSIZE as usize];
26        let len = unsafe {
27            libc::proc_pidpath(
28                pid as libc::c_int,
29                buf.as_mut_ptr() as *mut libc::c_void,
30                buf.len() as u32,
31            )
32        };
33        if len <= 0 {
34            return None;
35        }
36        let path = std::ffi::CStr::from_bytes_until_nul(&buf[..]).ok()?;
37        let path = path.to_str().ok()?;
38        if path.is_empty() {
39            return None;
40        }
41        Some(PathBuf::from(path))
42    }
43    #[cfg(not(any(target_os = "linux", target_os = "macos")))]
44    {
45        let _ = pid;
46        None
47    }
48}
49
50/// Best-effort peer process id for a connected Unix domain socket.
51pub fn unix_peer_pid(fd: std::os::unix::io::RawFd) -> Option<u32> {
52    #[cfg(target_os = "macos")]
53    {
54        // LOCAL_PEERPID from sys/un.h / sys/socket.h
55        const LOCAL_PEERPID: libc::c_int = 0x002;
56        let mut pid: libc::pid_t = 0;
57        let mut len = std::mem::size_of::<libc::pid_t>() as libc::socklen_t;
58        let rc = unsafe {
59            libc::getsockopt(
60                fd,
61                libc::SOL_LOCAL,
62                LOCAL_PEERPID,
63                &mut pid as *mut _ as *mut libc::c_void,
64                &mut len,
65            )
66        };
67        if rc == 0 && pid > 0 {
68            Some(pid as u32)
69        } else {
70            None
71        }
72    }
73    #[cfg(target_os = "linux")]
74    {
75        let mut cred = libc::ucred {
76            pid: 0,
77            uid: 0,
78            gid: 0,
79        };
80        let mut len = std::mem::size_of::<libc::ucred>() as libc::socklen_t;
81        let rc = unsafe {
82            libc::getsockopt(
83                fd,
84                libc::SOL_SOCKET,
85                libc::SO_PEERCRED,
86                &mut cred as *mut _ as *mut libc::c_void,
87                &mut len,
88            )
89        };
90        if rc == 0 && cred.pid > 0 {
91            Some(cred.pid as u32)
92        } else {
93            None
94        }
95    }
96    #[cfg(not(any(target_os = "macos", target_os = "linux")))]
97    {
98        let _ = fd;
99        None
100    }
101}
102
103/// Remove a Unix domain socket path only when nothing is accepting connections.
104///
105/// Prefer bind-first + retry in the daemon server over calling this from a
106/// parent before spawn: connect→unlink still has a TOCTOU window if another
107/// process binds between those steps.
108///
109/// Returns `Ok(true)` if a stale socket was removed, `Ok(false)` if the path was
110/// absent or still live (left untouched).
111pub fn clear_stale_unix_socket(socket_path: &std::path::Path) -> std::io::Result<bool> {
112    if !socket_path.exists() {
113        return Ok(false);
114    }
115    // A successful connect means a live listener still owns this path.
116    if std::os::unix::net::UnixStream::connect(socket_path).is_ok() {
117        return Ok(false);
118    }
119    match std::fs::remove_file(socket_path) {
120        Ok(()) => Ok(true),
121        Err(err) if err.kind() == std::io::ErrorKind::NotFound => Ok(false),
122        Err(err) => Err(err),
123    }
124}