Skip to main content

ntop/
process.rs

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                // Process state is at index 2 (after pid and comm)
171                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            // Calculate CPU percentage
229            // utime and stime are in clock ticks
230            // Formula: (delta_ticks / clock_tick) / elapsed_time * 100
231            // This gives percentage of one CPU core. For multi-threaded processes,
232            // this can exceed 100% (e.g., 400% means using 4 cores fully)
233            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                        // Convert ticks to seconds, then to percentage
241                        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                // Cap at a reasonable max (e.g., 64 cores * 100% = 6400%)
264                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}