1use anyhow::Result;
2use std::collections::HashMap;
3use std::fs;
4use users::get_user_by_uid;
5
6#[derive(Debug, Clone)]
7#[allow(dead_code)]
8pub struct ProcessInfo {
9 pub pid: u32,
10 pub name: String,
11 pub user: String,
12 pub uid: u32,
13 pub connections: usize,
14 pub read_bytes: u64,
15 pub write_bytes: u64,
16 pub read_bytes_sec: f64,
17 pub write_bytes_sec: f64,
18 pub cpu_percent: f64,
19 pub mem_percent: f64,
20 pub state: String,
21}
22
23#[derive(Debug, Clone, Default)]
24#[allow(dead_code)]
25pub struct ProcessDelta {
26 pub pid: u32,
27 pub name: String,
28 pub user: String,
29 pub connections: usize,
30 pub read_bytes_sec: f64,
31 pub write_bytes_sec: f64,
32 pub cpu_percent: f64,
33 pub mem_percent: f64,
34 pub state: String,
35}
36
37pub struct ProcessCollector {
38 last_io: HashMap<u32, (u64, u64)>,
39 last_cpu: HashMap<u32, (u64, u64, std::time::Instant)>,
40 last_time: std::time::Instant,
41 total_memory_kb: u64,
42 clock_tick: u64,
43}
44
45impl Default for ProcessCollector {
46 fn default() -> Self {
47 Self::new()
48 }
49}
50
51impl ProcessCollector {
52 pub fn new() -> Self {
53 Self {
54 last_io: HashMap::new(),
55 last_cpu: HashMap::new(),
56 last_time: std::time::Instant::now(),
57 total_memory_kb: Self::get_total_memory(),
58 clock_tick: Self::get_clock_tick(),
59 }
60 }
61
62 fn get_total_memory() -> u64 {
63 if let Ok(content) = fs::read_to_string("/proc/meminfo") {
64 for line in content.lines() {
65 if line.starts_with("MemTotal:") {
66 return line
67 .split(':')
68 .nth(1)
69 .and_then(|s| s.split_whitespace().next())
70 .and_then(|s| s.parse().ok())
71 .unwrap_or(0);
72 }
73 }
74 }
75 0
76 }
77
78 fn get_clock_tick() -> u64 {
79 unsafe { libc::sysconf(libc::_SC_CLK_TCK) as u64 }.max(1)
80 }
81
82 fn get_process_name(pid: u32) -> String {
83 fs::read_to_string(format!("/proc/{}/comm", pid))
84 .unwrap_or_default()
85 .trim()
86 .to_string()
87 }
88
89 fn get_process_user(pid: u32) -> (String, u32) {
90 if let Ok(content) = fs::read_to_string(format!("/proc/{}/status", pid)) {
91 for line in content.lines() {
92 if line.starts_with("Uid:") {
93 let parts: Vec<&str> = line.split_whitespace().collect();
94 if parts.len() >= 2 {
95 if let Ok(uid) = parts[1].parse::<u32>() {
96 let user_name = get_user_by_uid(uid)
97 .map(|u| u.name().to_string_lossy().to_string())
98 .unwrap_or_else(|| uid.to_string());
99 return (user_name, uid);
100 }
101 }
102 }
103 }
104 }
105 ("unknown".to_string(), 0)
106 }
107
108 fn get_process_io(pid: u32) -> (u64, u64) {
109 if let Ok(content) = fs::read_to_string(format!("/proc/{}/io", pid)) {
110 let mut read_bytes = 0u64;
111 let mut write_bytes = 0u64;
112 for line in content.lines() {
113 if line.starts_with("read_bytes:") {
114 read_bytes = line
115 .split(':')
116 .nth(1)
117 .and_then(|s| s.trim().parse().ok())
118 .unwrap_or(0);
119 } else if line.starts_with("write_bytes:") {
120 write_bytes = line
121 .split(':')
122 .nth(1)
123 .and_then(|s| s.trim().parse().ok())
124 .unwrap_or(0);
125 }
126 }
127 return (read_bytes, write_bytes);
128 }
129 (0, 0)
130 }
131
132 fn get_socket_connections(pid: u32) -> usize {
133 let fd_path = format!("/proc/{}/fd", pid);
134 if let Ok(fd_dir) = fs::read_dir(&fd_path) {
135 return fd_dir
136 .filter_map(|e| e.ok())
137 .filter_map(|e| {
138 if let Ok(link) = fs::read_link(e.path()) {
139 let link_str = link.to_string_lossy().to_string();
140 if link_str.starts_with("socket:[") {
141 return Some(());
142 }
143 }
144 None
145 })
146 .count();
147 }
148 0
149 }
150
151 fn get_all_pids() -> Vec<u32> {
152 let mut pids = Vec::new();
153 if let Ok(entries) = fs::read_dir("/proc") {
154 for entry in entries.flatten() {
155 if let Ok(pid) = entry.file_name().to_string_lossy().parse::<u32>() {
156 pids.push(pid);
157 }
158 }
159 }
160 pids
161 }
162
163 fn get_process_stat(pid: u32) -> (u64, u64, u64, String) {
164 if let Ok(content) = fs::read_to_string(format!("/proc/{}/stat", pid)) {
165 let parts: Vec<&str> = content.split_whitespace().collect();
166 if parts.len() >= 17 {
167 let utime: u64 = parts[13].parse().unwrap_or(0);
168 let stime: u64 = parts[14].parse().unwrap_or(0);
169 let rss: u64 = parts[23].parse().unwrap_or(0);
170 let state = parts.get(2).unwrap_or(&"?").to_string();
172 return (utime, stime, rss, state);
173 }
174 }
175 (0, 0, 0, "?".to_string())
176 }
177
178 fn format_state(state: &str) -> &'static str {
179 match state {
180 "R" => "Running",
181 "S" => "Sleeping",
182 "D" => "Disk Sleep",
183 "T" => "Stopped",
184 "t" => "Tracing Stop",
185 "X" => "Dead",
186 "Z" => "Zombie",
187 "P" => "Parked",
188 "I" => "Idle",
189 _ => "Unknown",
190 }
191 }
192
193 pub fn collect(&mut self) -> Result<Vec<ProcessInfo>> {
194 let now = std::time::Instant::now();
195 let elapsed = now.duration_since(self.last_time).as_secs_f64();
196 let mut processes = Vec::new();
197 let mut current_io = HashMap::new();
198 let mut current_cpu = HashMap::new();
199
200 for pid in Self::get_all_pids() {
201 let name = Self::get_process_name(pid);
202 let (user, uid) = Self::get_process_user(pid);
203 let (read_bytes, write_bytes) = Self::get_process_io(pid);
204 let connections = Self::get_socket_connections(pid);
205 let (utime, stime, rss, state_code) = Self::get_process_stat(pid);
206 let state = Self::format_state(&state_code);
207
208 let (read_sec, write_sec) = if elapsed > 0.0 {
209 if let Some(&(last_read, last_write)) = self.last_io.get(&pid) {
210 (
211 (read_bytes.saturating_sub(last_read)) as f64 / elapsed,
212 (write_bytes.saturating_sub(last_write)) as f64 / elapsed,
213 )
214 } else {
215 (0.0, 0.0)
216 }
217 } else {
218 (0.0, 0.0)
219 };
220
221 let page_size_kb = 4.0_f64;
222 let mem_percent = if self.total_memory_kb > 0 {
223 (rss as f64 * page_size_kb / self.total_memory_kb as f64) * 100.0
224 } else {
225 0.0
226 };
227
228 let cpu_percent =
234 if let Some((last_utime, last_stime, last_time)) = self.last_cpu.get(&pid) {
235 let time_elapsed = now.duration_since(*last_time).as_secs_f64();
236 if time_elapsed > 0.0 && self.clock_tick > 0 {
237 let delta_utime = utime.saturating_sub(*last_utime) as f64;
238 let delta_stime = stime.saturating_sub(*last_stime) as f64;
239 let delta_ticks = delta_utime + delta_stime;
240 let cpu_time = delta_ticks / self.clock_tick as f64;
242 (cpu_time / time_elapsed) * 100.0
243 } else {
244 0.0
245 }
246 } else {
247 0.0
248 };
249
250 current_io.insert(pid, (read_bytes, write_bytes));
251 current_cpu.insert(pid, (utime, stime, now));
252
253 processes.push(ProcessInfo {
254 pid,
255 name,
256 user,
257 uid,
258 connections,
259 read_bytes,
260 write_bytes,
261 read_bytes_sec: read_sec,
262 write_bytes_sec: write_sec,
263 cpu_percent: cpu_percent.min(6400.0),
265 mem_percent: mem_percent.min(100.0),
266 state: state.to_string(),
267 });
268 }
269
270 self.last_io = current_io;
271 self.last_cpu = current_cpu;
272 self.last_time = now;
273
274 Ok(processes)
275 }
276
277 pub fn collect_delta(&mut self) -> Result<Vec<ProcessDelta>> {
278 let processes = self.collect()?;
279
280 Ok(processes
281 .into_iter()
282 .map(|p| ProcessDelta {
283 pid: p.pid,
284 name: p.name,
285 user: p.user,
286 connections: p.connections,
287 read_bytes_sec: p.read_bytes_sec,
288 write_bytes_sec: p.write_bytes_sec,
289 cpu_percent: p.cpu_percent,
290 mem_percent: p.mem_percent,
291 state: p.state,
292 })
293 .collect())
294 }
295}