Skip to main content

peek_proc_reader/
fd.rs

1// /proc/PID/fd/* and fdinfo. Logic moved from peek-core::proc::files.
2//
3// This crate is responsible for low-level /proc parsing and intentionally
4// avoids depending on peek-core types. Callers can adapt `FdEntry` into
5// whatever domain struct they need.
6
7use crate::error::{io_to_error, Result};
8use std::path::PathBuf;
9
10/// One entry from `/proc/<pid>/fd`.
11#[derive(Debug, Clone)]
12pub struct FdEntry {
13    pub fd: u32,
14    pub fd_type: String,
15    pub description: String,
16}
17
18/// Collect detailed information about open file descriptors for `pid`.
19#[cfg(target_os = "linux")]
20pub fn read_fd(pid: i32) -> Result<Vec<FdEntry>> {
21    let fd_dir = format!("/proc/{}/fd", pid);
22    let path = PathBuf::from(&fd_dir);
23    let mut files = Vec::new();
24
25    let entries = std::fs::read_dir(&path).map_err(|e| io_to_error(path.clone(), e, pid))?;
26    let mut pairs: Vec<(u32, std::path::PathBuf)> = entries
27        .flatten()
28        .filter_map(|e| {
29            let fd: u32 = e.file_name().to_string_lossy().parse().ok()?;
30            Some((fd, e.path()))
31        })
32        .collect();
33    pairs.sort_by_key(|(fd, _)| *fd);
34
35    for (fd, path) in pairs {
36        let (fd_type, description) = resolve_fd(pid, fd, &path);
37        files.push(FdEntry {
38            fd,
39            fd_type,
40            description,
41        });
42    }
43
44    Ok(files)
45}
46
47/// Count open file descriptors for `pid`.
48#[cfg(target_os = "linux")]
49pub fn count_fds(pid: i32) -> Result<usize> {
50    let fd_dir = format!("/proc/{}/fd", pid);
51    let path = PathBuf::from(&fd_dir);
52    let count = std::fs::read_dir(&path)
53        .map_err(|e| io_to_error(path, e, pid))?
54        .count();
55    Ok(count)
56}
57
58/// Non-Linux stub implementations: /proc is not available.
59#[cfg(not(target_os = "linux"))]
60pub fn read_fd(_pid: i32) -> Result<Vec<FdEntry>> {
61    Ok(Vec::new())
62}
63
64#[cfg(not(target_os = "linux"))]
65pub fn count_fds(_pid: i32) -> Result<usize> {
66    Ok(0)
67}
68
69#[cfg(target_os = "linux")]
70fn resolve_fd(pid: i32, fd: u32, fd_path: &std::path::Path) -> (String, String) {
71    let target = match std::fs::read_link(fd_path) {
72        Ok(t) => t.to_string_lossy().into_owned(),
73        Err(_) => return ("unknown".to_string(), "?".to_string()),
74    };
75
76    if target.starts_with('/') {
77        // Regular file or device
78        let fd_type = if target.starts_with("/dev/") {
79            "device"
80        } else {
81            "file"
82        };
83        // Try to get access mode from fdinfo
84        let mode = read_fdinfo_mode(pid, fd);
85        return (fd_type.to_string(), format!("{}{}", target, mode));
86    }
87
88    if let Some(inode) = target
89        .strip_prefix("socket:[")
90        .and_then(|s| s.strip_suffix(']'))
91    {
92        return ("socket".to_string(), format!("socket inode {}", inode));
93    }
94
95    if let Some(inode) = target
96        .strip_prefix("pipe:[")
97        .and_then(|s| s.strip_suffix(']'))
98    {
99        return ("pipe".to_string(), format!("pipe:[{}]", inode));
100    }
101
102    if target.starts_with("anon_inode:") {
103        let kind = target.trim_start_matches("anon_inode:");
104        let desc = match kind {
105            "eventfd" => "eventfd (async event notification)".to_string(),
106            "eventpoll" => "epoll instance".to_string(),
107            "timerfd" => "timerfd (timer)".to_string(),
108            "signalfd" => "signalfd (signal handler)".to_string(),
109            _ => format!("anon_inode:{}", kind),
110        };
111        return ("anon_inode".to_string(), desc);
112    }
113
114    ("other".to_string(), target)
115}
116
117#[cfg(target_os = "linux")]
118fn read_fdinfo_mode(pid: i32, fd: u32) -> String {
119    let path = format!("/proc/{}/fdinfo/{}", pid, fd);
120    if let Ok(raw) = std::fs::read_to_string(path) {
121        for line in raw.lines() {
122            if let Some(val) = line.strip_prefix("flags:\t") {
123                let flags = u32::from_str_radix(val.trim(), 8).unwrap_or(0);
124                let mode = flags & 0b11;
125                return match mode {
126                    0 => " (read-only)".to_string(),
127                    1 => " (write-only)".to_string(),
128                    2 => " (read-write)".to_string(),
129                    _ => "".to_string(),
130                };
131            }
132        }
133    }
134    String::new()
135}
136
137#[cfg(test)]
138mod tests {
139    use super::FdEntry;
140
141    #[test]
142    fn fd_entry_clone_and_debug() {
143        let e = FdEntry {
144            fd: 3,
145            fd_type: "file".to_string(),
146            description: "/tmp/test".to_string(),
147        };
148        let cloned = e.clone();
149        assert_eq!(cloned.fd, 3);
150        assert_eq!(cloned.fd_type, "file");
151        assert_eq!(cloned.description, "/tmp/test");
152        let debug = format!("{:?}", e);
153        assert!(debug.contains("FdEntry"));
154    }
155}