1use crate::error::{io_to_error, Result};
8use std::path::PathBuf;
9
10#[derive(Debug, Clone)]
12pub struct FdEntry {
13 pub fd: u32,
14 pub fd_type: String,
15 pub description: String,
16}
17
18#[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#[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#[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 let fd_type = if target.starts_with("/dev/") {
79 "device"
80 } else {
81 "file"
82 };
83 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}