laurel/
procfs.rs

1use std::ffi::OsStr;
2use std::fs::{read_dir, read_link, File, Metadata};
3use std::io::{BufRead, BufReader, Read, Write};
4use std::os::unix::ffi::OsStrExt;
5use std::path::Path;
6use std::str::FromStr;
7
8use lazy_static::lazy_static;
9use nix::sys::time::TimeSpec;
10use nix::time::{clock_gettime, ClockId};
11use nix::unistd::{sysconf, SysconfVar};
12
13use thiserror::Error;
14
15lazy_static! {
16    /// kernel clock ticks per second
17    pub static ref CLK_TCK: u64
18        = sysconf(SysconfVar::CLK_TCK).unwrap().unwrap() as u64;
19}
20
21#[derive(Debug, Error)]
22pub enum ProcFSError {
23    #[error("can't read /proc/{pid}/(obj): {err}")]
24    PidFile {
25        pid: u32,
26        obj: &'static str,
27        err: std::io::Error,
28    },
29    #[error("can't enumerate processes: {0}")]
30    Enum(std::io::Error),
31    #[error("can't get field {0}")]
32    Field(&'static str),
33    #[error("{0}: {1}")]
34    Errno(&'static str, nix::errno::Errno),
35}
36
37pub fn slurp_file<P: AsRef<Path>>(path: P) -> Result<Vec<u8>, std::io::Error> {
38    let f = File::open(path)?;
39    let mut r = BufReader::with_capacity(1 << 16, f);
40    r.fill_buf()?;
41    let mut buf = Vec::with_capacity(8192);
42    r.read_to_end(&mut buf)?;
43    Ok(buf)
44}
45
46/// Read contents of file, return buffer.
47fn slurp_pid_obj(pid: u32, obj: &'static str) -> Result<Vec<u8>, ProcFSError> {
48    let path = format!("/proc/{pid}/{obj}");
49    slurp_file(path).map_err(|err| ProcFSError::PidFile { pid, obj, err })
50}
51
52type Environment = Vec<(Vec<u8>, Vec<u8>)>;
53
54/// Returns set of environment variables that match pred for a given process
55pub fn get_environ<F>(pid: u32, pred: F) -> Result<Environment, ProcFSError>
56where
57    F: Fn(&[u8]) -> bool,
58{
59    let buf = slurp_pid_obj(pid, "environ")?;
60    let mut res = Vec::new();
61
62    for e in buf.split(|c| *c == 0) {
63        let mut kv = e.splitn(2, |c| *c == b'=');
64        let k = kv.next().unwrap_or_default();
65        if pred(k) {
66            let v = kv.next().unwrap_or_default();
67            res.push((k.to_owned(), v.to_owned()));
68        }
69    }
70    Ok(res)
71}
72
73/// Returns all currently valid process IDs
74pub fn get_pids() -> Result<Vec<u32>, ProcFSError> {
75    Ok(read_dir("/proc")
76        .map_err(ProcFSError::Enum)?
77        .flatten()
78        .filter_map(|e| u32::from_str(e.file_name().to_string_lossy().as_ref()).ok())
79        .collect::<Vec<u32>>())
80}
81
82/// Returns file metadata for a path from a process' perspective
83pub fn pid_path_metadata(pid: u32, path: &[u8]) -> Result<Metadata, std::io::Error> {
84    if path.is_empty() || path[0] != b'/' {
85        return Err(std::io::ErrorKind::NotFound.into());
86    }
87    let mut proc_path = Vec::with_capacity(20 + path.len());
88    // unwrap safety: write will not produce an IO error
89    write!(proc_path, "/proc/{pid}/root").unwrap();
90    proc_path.extend(path);
91    std::fs::metadata(OsStr::from_bytes(&proc_path))
92}
93
94pub struct ProcStat<'a> {
95    pub pid: u32,
96    pub ppid: u32,
97    pub comm: &'a [u8],
98    pub starttime: u64,
99    pub utime: u64,
100    pub stime: u64,
101}
102
103pub fn parse_proc_pid_stat(buf: &[u8]) -> Result<ProcStat, ProcFSError> {
104    let pid_end = buf
105        .iter()
106        .enumerate()
107        .find(|(_, c)| **c == b' ')
108        .ok_or(ProcFSError::Field("pid"))?
109        .0;
110    let stat_pid = &buf[..pid_end];
111
112    // comm may contain whitespace and ")", find closing paren from right.
113    let comm_end = buf[..32]
114        .iter()
115        .enumerate()
116        .rfind(|(_, c)| **c == b')')
117        .ok_or(ProcFSError::Field("comm"))?
118        .0;
119    let stat = &buf[comm_end + 2..]
120        .split(|c| *c == b' ')
121        .collect::<Vec<_>>();
122
123    let pid = u32::from_str(String::from_utf8_lossy(stat_pid).as_ref())
124        .map_err(|_| ProcFSError::Field("pid"))?;
125    let comm = &buf[pid_end + 2..comm_end];
126    let ppid = u32::from_str(String::from_utf8_lossy(stat[1]).as_ref())
127        .map_err(|_| ProcFSError::Field("ppid"))?;
128    let starttime = u64::from_str(String::from_utf8_lossy(stat[19]).as_ref())
129        .map_err(|_| ProcFSError::Field("starttime"))?;
130    let utime = u64::from_str(String::from_utf8_lossy(stat[11]).as_ref())
131        .map_err(|_| ProcFSError::Field("utime"))?;
132    let stime = u64::from_str(String::from_utf8_lossy(stat[12]).as_ref())
133        .map_err(|_| ProcFSError::Field("stime"))?;
134
135    Ok(ProcStat {
136        pid,
137        ppid,
138        comm,
139        starttime,
140        utime,
141        stime,
142    })
143}
144
145#[derive(Debug)]
146pub(crate) struct ProcPidInfo {
147    /// /proc/<pid>/stat field 1
148    pub pid: u32,
149    /// /proc/<pid>/stat field 4
150    pub ppid: u32,
151    /// /proc/<pid>/stat field 22, converted to milliseconds since epoch
152    pub starttime: u64,
153    /// /proc/<pid>/stat field 2
154    pub comm: Vec<u8>,
155    /// /proc/pid/exe
156    pub exe: Option<Vec<u8>>,
157    /// from /proc/$PID/cgroup
158    pub cgroup: Option<Vec<u8>>,
159}
160
161/// Parses information from /proc entry corresponding to process pid
162pub(crate) fn parse_proc_pid(pid: u32) -> Result<ProcPidInfo, ProcFSError> {
163    let buf = slurp_pid_obj(pid, "stat")?;
164
165    let ProcStat {
166        pid,
167        ppid,
168        comm,
169        starttime,
170        ..
171    } = parse_proc_pid_stat(&buf)?;
172
173    let exe = read_link(format!("/proc/{pid}/exe"))
174        .map(|p| Vec::from(p.as_os_str().as_bytes()))
175        .ok();
176
177    // Use the boottime-based clock to calculate process start
178    // time, convert to Unix-epoch-based-time.
179    let proc_boottime = TimeSpec::from(libc::timespec {
180        tv_sec: (starttime / *CLK_TCK) as _,
181        tv_nsec: ((starttime % *CLK_TCK) * (1_000_000_000 / *CLK_TCK)) as _,
182    });
183    #[cfg(not(target_os = "linux"))]
184    let proc_age = TimeSpec::from(std::time::Duration::ZERO);
185    #[cfg(target_os = "linux")]
186    let proc_age = clock_gettime(ClockId::CLOCK_BOOTTIME)
187        .map_err(|e| ProcFSError::Errno("clock_gettime(CLOCK_BOOTTIME)", e))?
188        - proc_boottime;
189    let starttime = {
190        let lt = clock_gettime(ClockId::CLOCK_REALTIME)
191            .map_err(|e| ProcFSError::Errno("clock_gettime(CLOCK_REALTIME)", e))?
192            - proc_age;
193        (lt.tv_sec() as u64) * 1000 + (lt.tv_nsec() as u64) / 1_000_000
194    };
195
196    let cgroup = parse_proc_pid_cgroup(pid)?;
197
198    Ok(ProcPidInfo {
199        pid,
200        ppid,
201        starttime,
202        comm: comm.to_vec(),
203        exe,
204        cgroup,
205    })
206}
207
208/// Parses path (third field) /proc/pid/cgroup
209pub(crate) fn parse_proc_pid_cgroup(pid: u32) -> Result<Option<Vec<u8>>, ProcFSError> {
210    parse_cgroup_buf(&slurp_pid_obj(pid, "cgroup")?)
211}
212
213fn parse_cgroup_buf(buf: &[u8]) -> Result<Option<Vec<u8>>, ProcFSError> {
214    for line in buf.split(|c| *c == b'\n') {
215        match line.split(|&c| c == b':').nth(2) {
216            None => continue,
217            Some(dir) => return Ok(Some(dir.to_vec())),
218        }
219    }
220    Ok(None)
221}
222
223#[cfg(test)]
224mod tests {
225    use super::*;
226    #[test]
227    fn parse_self() {
228        let pid = std::process::id();
229        let proc = parse_proc_pid(pid).unwrap_or_else(|_| panic!("parse entry for {pid}"));
230        println!("{proc:?}");
231    }
232
233    #[test]
234    fn parse_stat() {
235        let ProcStat { pid, ppid, comm, starttime, utime, stime } = parse_proc_pid_stat(
236            br#"925028 (emacs) R 3057 925028 925028 0 -1 4194304 1131624 1579849 3 604 183731 6453 20699 2693 20 0 5 0 22398221 3922935808 191059 18446744073709551615 187652449566720 187652452795816 281474372905056 0 0 0 0 67112960 1535209215 0 0 0 17 2 0 0 0 0 0 187652452866344 187652460555720 187652461281280 281474372910201 281474372910228 281474372910228 281474372911081 0
237"#).expect("parse error");
238        assert_eq!(pid, 925028);
239        assert_eq!(ppid, 3057);
240        assert_eq!(comm, "emacs".as_bytes());
241        assert_eq!(starttime, 22398221);
242        assert_eq!(utime, 183731);
243        assert_eq!(stime, 6453);
244    }
245}