mprober_lib/process/
process.rs

1use std::{
2    collections::BTreeMap,
3    fs,
4    hash::{Hash, Hasher},
5    io::ErrorKind,
6    mem::take,
7    path::Path,
8    thread::sleep,
9    time::Duration,
10};
11
12use chrono::prelude::*;
13
14use crate::{
15    btime::get_btime,
16    cpu::get_average_cpu_stat,
17    process::{
18        get_process_stat, get_process_status, get_process_time_stat, ProcessFilter, ProcessStat,
19        ProcessState, ProcessTimeStat,
20    },
21    scanner_rust::ScannerError,
22};
23
24#[derive(Debug, Clone, Eq)]
25pub struct Process {
26    pub pid:                u32,
27    pub effective_uid:      u32,
28    pub effective_gid:      u32,
29    pub state:              ProcessState,
30    pub ppid:               u32,
31    pub program:            String,
32    pub cmdline:            String,
33    pub tty:                Option<String>,
34    pub priority:           i8,
35    pub real_time_priority: Option<u8>,
36    pub nice:               i8,
37    pub threads:            usize,
38    /// Virtual Set Size (VIRT)
39    pub vsz:                usize,
40    /// Resident Set Size (RES)
41    pub rss:                usize,
42    /// Resident Shared Size (SHR)
43    pub rss_shared:         usize,
44    /// Resident Anonymous Memory
45    pub rss_anon:           usize,
46    pub start_time:         DateTime<Utc>,
47}
48
49impl Hash for Process {
50    #[inline]
51    fn hash<H: Hasher>(&self, state: &mut H) {
52        self.pid.hash(state)
53    }
54}
55
56impl PartialEq for Process {
57    #[inline]
58    fn eq(&self, other: &Process) -> bool {
59        self.pid.eq(&other.pid)
60    }
61}
62
63fn get_process_with_stat_inner<P: AsRef<Path>>(
64    pid: u32,
65    process_path: P,
66    process_filter: &ProcessFilter,
67) -> Result<Option<(Process, ProcessStat)>, ScannerError> {
68    let process_path = process_path.as_ref();
69
70    let mut program_filter_match = true;
71
72    let status = get_process_status(pid)?;
73
74    if let Some(uid_filter) = process_filter.uid_filter {
75        if status.real_uid != uid_filter
76            && status.effective_uid != uid_filter
77            && status.saved_set_uid != uid_filter
78            && status.fs_uid != uid_filter
79        {
80            return Ok(None);
81        }
82    }
83
84    if let Some(gid_filter) = process_filter.gid_filter {
85        if status.real_gid != gid_filter
86            && status.effective_gid != gid_filter
87            && status.saved_set_gid != gid_filter
88            && status.fs_gid != gid_filter
89        {
90            return Ok(None);
91        }
92    }
93
94    let cmdline = {
95        let mut data = fs::read(process_path.join("cmdline"))?;
96
97        for e in data.iter_mut() {
98            if *e == 0 {
99                *e = b' ';
100            }
101        }
102
103        unsafe { String::from_utf8_unchecked(data) }
104    };
105
106    if let Some(program_filter) = process_filter.program_filter.as_ref() {
107        if !program_filter.is_match(&cmdline) {
108            program_filter_match = false;
109        }
110    }
111
112    let mut stat = get_process_stat(pid)?;
113
114    if !program_filter_match {
115        if let Some(program_filter) = process_filter.program_filter.as_ref() {
116            if !program_filter.is_match(&stat.comm) {
117                return Ok(None);
118            }
119        }
120    }
121
122    let effective_uid = status.effective_uid;
123    let effective_gid = status.effective_gid;
124    let state = stat.state;
125    let ppid = stat.ppid;
126    let program = take(&mut stat.comm);
127
128    let tty = {
129        match stat.tty_nr_major {
130            4 => {
131                if stat.tty_nr_minor < 64 {
132                    Some(format!("tty{}", stat.tty_nr_minor))
133                } else {
134                    Some(format!("ttyS{}", stat.tty_nr_minor - 64))
135                }
136            },
137            136..=143 => Some(format!("pts/{}", stat.tty_nr_minor)),
138            _ => None,
139        }
140    };
141
142    if let Some(tty_filter) = process_filter.tty_filter.as_ref() {
143        match tty.as_ref() {
144            Some(tty) => {
145                if !tty_filter.is_match(tty) {
146                    return Ok(None);
147                }
148            },
149            None => return Ok(None),
150        }
151    }
152
153    let priority = stat.priority;
154    let real_time_priority = if stat.rt_priority > 0 { Some(stat.rt_priority) } else { None };
155    let nice = stat.nice;
156    let threads = stat.num_threads;
157    let vsz = stat.vsize;
158    let rss = stat.rss;
159    let rss_shared = stat.shared;
160    let rss_anon = stat.rss_anon;
161
162    let start_time =
163        get_btime() + chrono::Duration::from_std(Duration::from_millis(stat.starttime)).unwrap();
164
165    let process = Process {
166        pid,
167        effective_uid,
168        effective_gid,
169        state,
170        ppid,
171        program,
172        cmdline,
173        tty,
174        priority,
175        real_time_priority,
176        nice,
177        threads,
178        vsz,
179        rss,
180        rss_shared,
181        rss_anon,
182        start_time,
183    };
184
185    Ok(Some((process, stat)))
186}
187
188/// Get information of a specific process found by ID by reading files in the `/proc/PID` folder.
189///
190/// ```rust
191/// use mprober_lib::process;
192///
193/// let (process, _) = process::get_process_with_stat(1).unwrap();
194///
195/// println!("{process:#?}");
196/// ```
197#[inline]
198pub fn get_process_with_stat(pid: u32) -> Result<(Process, ProcessStat), ScannerError> {
199    let process_path = Path::new("/proc").join(pid.to_string());
200
201    get_process_with_stat_inner(pid, process_path, &ProcessFilter::default()).map(|o| o.unwrap())
202}
203
204/// Get process information by reading files in the `/proc/PID` folders.
205///
206/// ```rust
207/// use mprober_lib::process;
208///
209/// let processes_with_stat =
210///     process::get_processes_with_stat(&process::ProcessFilter::default())
211///         .unwrap();
212///
213/// println!("{processes_with_stat:#?}");
214/// ```
215pub fn get_processes_with_stat(
216    process_filter: &ProcessFilter,
217) -> Result<Vec<(Process, ProcessStat)>, ScannerError> {
218    let mut processes_with_stats = Vec::new();
219
220    let proc = Path::new("/proc");
221
222    if let Some(pid_filter) = process_filter.pid_filter.as_ref().copied() {
223        let mut pid_ppid_map: BTreeMap<u32, u32> = BTreeMap::new();
224
225        for dir_entry in proc.read_dir()? {
226            let dir_entry = dir_entry?;
227
228            if let Some(file_name) = dir_entry.file_name().to_str() {
229                if let Ok(pid) = file_name.parse::<u32>() {
230                    let process_path = dir_entry.path();
231
232                    match get_process_with_stat_inner(pid, process_path, process_filter) {
233                        Ok(r) => {
234                            if let Some((process, stat)) = r {
235                                if pid != pid_filter && process.ppid != pid_filter {
236                                    let mut not_related = true;
237
238                                    let mut p_ppid = pid_ppid_map.get(&process.ppid);
239
240                                    while let Some(ppid) = p_ppid.copied() {
241                                        if ppid == pid_filter {
242                                            not_related = false;
243
244                                            break;
245                                        }
246
247                                        p_ppid = pid_ppid_map.get(&ppid);
248                                    }
249
250                                    if not_related {
251                                        continue;
252                                    }
253                                }
254
255                                pid_ppid_map.insert(pid, process.ppid);
256
257                                processes_with_stats.push((process, stat));
258                            }
259                        },
260                        Err(err) => {
261                            if let ScannerError::IOError(err) = &err {
262                                if err.kind() == ErrorKind::NotFound {
263                                    continue;
264                                }
265                            }
266
267                            return Err(err);
268                        },
269                    }
270                }
271            }
272        }
273    } else {
274        for dir_entry in proc.read_dir()? {
275            let dir_entry = dir_entry?;
276
277            if let Some(file_name) = dir_entry.file_name().to_str() {
278                if let Ok(pid) = file_name.parse::<u32>() {
279                    let process_path = dir_entry.path();
280
281                    match get_process_with_stat_inner(pid, process_path, process_filter) {
282                        Ok(r) => {
283                            if let Some((process, stat)) = r {
284                                processes_with_stats.push((process, stat));
285                            }
286                        },
287                        Err(err) => {
288                            if let ScannerError::IOError(err) = &err {
289                                if err.kind() == ErrorKind::NotFound {
290                                    continue;
291                                }
292                            }
293
294                            return Err(err);
295                        },
296                    }
297                }
298            }
299        }
300    }
301
302    Ok(processes_with_stats)
303}
304
305/// Get process information by reading files in the `/proc/PID` folders and measure the cpu utilization in percentage within a specific time interval. If the number it returns is `1.0`, means `100%`.
306///
307/// ```rust
308/// use std::{thread::sleep, time::Duration};
309///
310/// use mprober_lib::process;
311///
312/// let processes_with_cpu_percentage =
313///     process::get_processes_with_cpu_utilization_in_percentage(
314///         &process::ProcessFilter::default(),
315///         Duration::from_millis(100),
316///     )
317///     .unwrap();
318///
319/// for (process, cpu_percentage) in processes_with_cpu_percentage {
320///     println!("{}: {:.1}%", process.pid, cpu_percentage * 100.0);
321/// }
322/// ```
323pub fn get_processes_with_cpu_utilization_in_percentage(
324    process_filter: &ProcessFilter,
325    interval: Duration,
326) -> Result<Vec<(Process, f64)>, ScannerError> {
327    let pre_average_cpu_stat = get_average_cpu_stat()?;
328    let processes_with_stat = get_processes_with_stat(process_filter).unwrap();
329
330    let mut processes_with_cpu_percentage = Vec::with_capacity(processes_with_stat.len());
331
332    sleep(interval);
333
334    let average_cpu_stat = get_average_cpu_stat()?;
335
336    let total_cpu_time_f64 = {
337        let pre_average_cpu_time = pre_average_cpu_stat.compute_cpu_time();
338        let average_cpu_time = average_cpu_stat.compute_cpu_time();
339
340        (average_cpu_time.get_total_time() - pre_average_cpu_time.get_total_time()) as f64
341    };
342
343    for (process, pre_process_stat) in processes_with_stat {
344        if let Ok(process_time_stat) = get_process_time_stat(process.pid) {
345            let pre_process_time_stat: ProcessTimeStat = pre_process_stat.into();
346
347            let cpu_percentage = pre_process_time_stat
348                .compute_cpu_utilization_in_percentage(&process_time_stat, total_cpu_time_f64);
349
350            processes_with_cpu_percentage.push((process, cpu_percentage));
351        }
352    }
353
354    Ok(processes_with_cpu_percentage)
355}