sysinfo/linux/
process.rs

1// Take a look at the license at the top of the repository in the LICENSE file.
2
3use std::cell::UnsafeCell;
4use std::collections::HashMap;
5use std::fmt;
6use std::fs::{self, File};
7use std::io::Read;
8use std::path::{Path, PathBuf};
9use std::str::FromStr;
10
11use libc::{gid_t, kill, uid_t};
12
13use crate::sys::system::SystemInfo;
14use crate::sys::utils::{
15    get_all_data, get_all_data_from_file, realpath, FileCounter, PathHandler, PathPush,
16};
17use crate::utils::into_iter;
18use crate::{DiskUsage, Gid, Pid, ProcessExt, ProcessRefreshKind, ProcessStatus, Signal, Uid};
19
20#[doc(hidden)]
21impl From<char> for ProcessStatus {
22    fn from(status: char) -> ProcessStatus {
23        match status {
24            'R' => ProcessStatus::Run,
25            'S' => ProcessStatus::Sleep,
26            'I' => ProcessStatus::Idle,
27            'D' => ProcessStatus::UninterruptibleDiskSleep,
28            'Z' => ProcessStatus::Zombie,
29            'T' => ProcessStatus::Stop,
30            't' => ProcessStatus::Tracing,
31            'X' | 'x' => ProcessStatus::Dead,
32            'K' => ProcessStatus::Wakekill,
33            'W' => ProcessStatus::Waking,
34            'P' => ProcessStatus::Parked,
35            x => ProcessStatus::Unknown(x as u32),
36        }
37    }
38}
39
40impl fmt::Display for ProcessStatus {
41    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
42        f.write_str(match *self {
43            ProcessStatus::Idle => "Idle",
44            ProcessStatus::Run => "Runnable",
45            ProcessStatus::Sleep => "Sleeping",
46            ProcessStatus::Stop => "Stopped",
47            ProcessStatus::Zombie => "Zombie",
48            ProcessStatus::Tracing => "Tracing",
49            ProcessStatus::Dead => "Dead",
50            ProcessStatus::Wakekill => "Wakekill",
51            ProcessStatus::Waking => "Waking",
52            ProcessStatus::Parked => "Parked",
53            ProcessStatus::UninterruptibleDiskSleep => "UninterruptibleDiskSleep",
54            _ => "Unknown",
55        })
56    }
57}
58
59#[doc = include_str!("../../md_doc/process.md")]
60pub struct Process {
61    pub(crate) name: String,
62    pub(crate) cmd: Vec<String>,
63    pub(crate) exe: PathBuf,
64    pub(crate) pid: Pid,
65    parent: Option<Pid>,
66    pub(crate) environ: Vec<String>,
67    pub(crate) cwd: PathBuf,
68    pub(crate) root: PathBuf,
69    pub(crate) memory: u64,
70    pub(crate) virtual_memory: u64,
71    utime: u64,
72    stime: u64,
73    old_utime: u64,
74    old_stime: u64,
75    start_time_without_boot_time: u64,
76    start_time: u64,
77    run_time: u64,
78    pub(crate) updated: bool,
79    cpu_usage: f32,
80    user_id: Option<Uid>,
81    effective_user_id: Option<Uid>,
82    group_id: Option<Gid>,
83    effective_group_id: Option<Gid>,
84    pub(crate) status: ProcessStatus,
85    /// Tasks run by this process.
86    pub tasks: HashMap<Pid, Process>,
87    pub(crate) stat_file: Option<FileCounter>,
88    old_read_bytes: u64,
89    old_written_bytes: u64,
90    read_bytes: u64,
91    written_bytes: u64,
92}
93
94impl Process {
95    pub(crate) fn new(pid: Pid) -> Process {
96        Process {
97            name: String::with_capacity(20),
98            pid,
99            parent: None,
100            cmd: Vec::with_capacity(2),
101            environ: Vec::with_capacity(10),
102            exe: PathBuf::new(),
103            cwd: PathBuf::new(),
104            root: PathBuf::new(),
105            memory: 0,
106            virtual_memory: 0,
107            cpu_usage: 0.,
108            utime: 0,
109            stime: 0,
110            old_utime: 0,
111            old_stime: 0,
112            updated: true,
113            start_time_without_boot_time: 0,
114            start_time: 0,
115            run_time: 0,
116            user_id: None,
117            effective_user_id: None,
118            group_id: None,
119            effective_group_id: None,
120            status: ProcessStatus::Unknown(0),
121            tasks: if pid.0 == 0 {
122                HashMap::with_capacity(1000)
123            } else {
124                HashMap::new()
125            },
126            stat_file: None,
127            old_read_bytes: 0,
128            old_written_bytes: 0,
129            read_bytes: 0,
130            written_bytes: 0,
131        }
132    }
133}
134
135impl ProcessExt for Process {
136    fn kill_with(&self, signal: Signal) -> Option<bool> {
137        let c_signal = super::system::convert_signal(signal)?;
138        unsafe { Some(kill(self.pid.0, c_signal) == 0) }
139    }
140
141    fn name(&self) -> &str {
142        &self.name
143    }
144
145    fn cmd(&self) -> &[String] {
146        &self.cmd
147    }
148
149    fn exe(&self) -> &Path {
150        self.exe.as_path()
151    }
152
153    fn pid(&self) -> Pid {
154        self.pid
155    }
156
157    fn environ(&self) -> &[String] {
158        &self.environ
159    }
160
161    fn cwd(&self) -> &Path {
162        self.cwd.as_path()
163    }
164
165    fn root(&self) -> &Path {
166        self.root.as_path()
167    }
168
169    fn memory(&self) -> u64 {
170        self.memory
171    }
172
173    fn virtual_memory(&self) -> u64 {
174        self.virtual_memory
175    }
176
177    fn parent(&self) -> Option<Pid> {
178        self.parent
179    }
180
181    fn status(&self) -> ProcessStatus {
182        self.status
183    }
184
185    fn start_time(&self) -> u64 {
186        self.start_time
187    }
188
189    fn run_time(&self) -> u64 {
190        self.run_time
191    }
192
193    fn cpu_usage(&self) -> f32 {
194        self.cpu_usage
195    }
196
197    fn disk_usage(&self) -> DiskUsage {
198        DiskUsage {
199            written_bytes: self.written_bytes.saturating_sub(self.old_written_bytes),
200            total_written_bytes: self.written_bytes,
201            read_bytes: self.read_bytes.saturating_sub(self.old_read_bytes),
202            total_read_bytes: self.read_bytes,
203        }
204    }
205
206    fn user_id(&self) -> Option<&Uid> {
207        self.user_id.as_ref()
208    }
209
210    fn effective_user_id(&self) -> Option<&Uid> {
211        self.effective_user_id.as_ref()
212    }
213
214    fn group_id(&self) -> Option<Gid> {
215        self.group_id
216    }
217
218    fn effective_group_id(&self) -> Option<Gid> {
219        self.effective_group_id
220    }
221
222    fn wait(&self) {
223        let mut status = 0;
224        // attempt waiting
225        unsafe {
226            if retry_eintr!(libc::waitpid(self.pid.0, &mut status, 0)) < 0 {
227                // attempt failed (non-child process) so loop until process ends
228                let duration = std::time::Duration::from_millis(10);
229                while kill(self.pid.0, 0) == 0 {
230                    std::thread::sleep(duration);
231                }
232            }
233        }
234    }
235
236    fn session_id(&self) -> Option<Pid> {
237        unsafe {
238            let session_id = libc::getsid(self.pid.0);
239            if session_id < 0 {
240                None
241            } else {
242                Some(Pid(session_id))
243            }
244        }
245    }
246}
247
248pub(crate) fn compute_cpu_usage(p: &mut Process, total_time: f32, max_value: f32) {
249    // First time updating the values without reference, wait for a second cycle to update cpu_usage
250    if p.old_utime == 0 && p.old_stime == 0 {
251        return;
252    }
253
254    // We use `max_value` to ensure that the process CPU usage will never get bigger than:
255    // `"number of CPUs" * 100.`
256    p.cpu_usage = (p
257        .utime
258        .saturating_sub(p.old_utime)
259        .saturating_add(p.stime.saturating_sub(p.old_stime)) as f32
260        / total_time
261        * 100.)
262        .min(max_value);
263
264    for task in p.tasks.values_mut() {
265        compute_cpu_usage(task, total_time, max_value);
266    }
267}
268
269pub(crate) fn unset_updated(p: &mut Process) {
270    p.updated = false;
271    for task in p.tasks.values_mut() {
272        unset_updated(task);
273    }
274}
275
276pub(crate) fn set_time(p: &mut Process, utime: u64, stime: u64) {
277    p.old_utime = p.utime;
278    p.old_stime = p.stime;
279    p.utime = utime;
280    p.stime = stime;
281    p.updated = true;
282}
283
284pub(crate) fn update_process_disk_activity(p: &mut Process, path: &Path) {
285    let data = match get_all_data(path.join("io"), 16_384) {
286        Ok(d) => d,
287        Err(_) => return,
288    };
289    let mut done = 0;
290    for line in data.split('\n') {
291        let mut parts = line.split(": ");
292        match parts.next() {
293            Some("read_bytes") => {
294                p.old_read_bytes = p.read_bytes;
295                p.read_bytes = parts
296                    .next()
297                    .and_then(|x| x.parse::<u64>().ok())
298                    .unwrap_or(p.old_read_bytes);
299            }
300            Some("write_bytes") => {
301                p.old_written_bytes = p.written_bytes;
302                p.written_bytes = parts
303                    .next()
304                    .and_then(|x| x.parse::<u64>().ok())
305                    .unwrap_or(p.old_written_bytes);
306            }
307            _ => continue,
308        }
309        done += 1;
310        if done > 1 {
311            // No need to continue the reading.
312            break;
313        }
314    }
315}
316
317struct Wrap<'a, T>(UnsafeCell<&'a mut T>);
318
319impl<'a, T> Wrap<'a, T> {
320    fn get(&self) -> &'a mut T {
321        unsafe { *(self.0.get()) }
322    }
323}
324
325#[allow(clippy::non_send_fields_in_send_ty)]
326unsafe impl<'a, T> Send for Wrap<'a, T> {}
327unsafe impl<'a, T> Sync for Wrap<'a, T> {}
328
329#[inline(always)]
330fn compute_start_time_without_boot_time(parts: &[&str], info: &SystemInfo) -> u64 {
331    // To be noted that the start time is invalid here, it still needs to be converted into
332    // "real" time.
333    u64::from_str(parts[21]).unwrap_or(0) / info.clock_cycle
334}
335
336fn _get_stat_data(path: &Path, stat_file: &mut Option<FileCounter>) -> Result<String, ()> {
337    let mut file = File::open(path.join("stat")).map_err(|_| ())?;
338    let data = get_all_data_from_file(&mut file, 1024).map_err(|_| ())?;
339    *stat_file = FileCounter::new(file);
340    Ok(data)
341}
342
343#[inline(always)]
344fn get_status(p: &mut Process, part: &str) {
345    p.status = part
346        .chars()
347        .next()
348        .map(ProcessStatus::from)
349        .unwrap_or_else(|| ProcessStatus::Unknown(0));
350}
351
352fn refresh_user_group_ids<P: PathPush>(p: &mut Process, path: &mut P) {
353    if let Some(((user_id, effective_user_id), (group_id, effective_group_id))) =
354        get_uid_and_gid(path.join("status"))
355    {
356        p.user_id = Some(Uid(user_id));
357        p.effective_user_id = Some(Uid(effective_user_id));
358        p.group_id = Some(Gid(group_id));
359        p.effective_group_id = Some(Gid(effective_group_id));
360    }
361}
362
363fn retrieve_all_new_process_info(
364    pid: Pid,
365    proc_list: &Process,
366    parts: &[&str],
367    path: &Path,
368    info: &SystemInfo,
369    refresh_kind: ProcessRefreshKind,
370    uptime: u64,
371) -> Process {
372    let mut p = Process::new(pid);
373    let mut tmp = PathHandler::new(path);
374    let name = parts[1];
375
376    p.parent = if proc_list.pid.0 != 0 {
377        Some(proc_list.pid)
378    } else {
379        match Pid::from_str(parts[3]) {
380            Ok(p) if p.0 != 0 => Some(p),
381            _ => None,
382        }
383    };
384
385    p.start_time_without_boot_time = compute_start_time_without_boot_time(parts, info);
386    p.start_time = p
387        .start_time_without_boot_time
388        .saturating_add(info.boot_time);
389
390    get_status(&mut p, parts[2]);
391
392    if refresh_kind.user() {
393        refresh_user_group_ids(&mut p, &mut tmp);
394    }
395
396    p.name = name.into();
397
398    match tmp.join("exe").read_link() {
399        Ok(exe_path) => {
400            p.exe = exe_path;
401        }
402        Err(_) => {
403            // Do not use cmd[0] because it is not the same thing.
404            // See https://github.com/GuillaumeGomez/sysinfo/issues/697.
405            p.exe = PathBuf::new()
406        }
407    }
408
409    p.cmd = copy_from_file(tmp.join("cmdline"));
410    p.environ = copy_from_file(tmp.join("environ"));
411    p.cwd = realpath(tmp.join("cwd"));
412    p.root = realpath(tmp.join("root"));
413
414    update_time_and_memory(
415        path,
416        &mut p,
417        parts,
418        proc_list.memory,
419        proc_list.virtual_memory,
420        uptime,
421        info,
422        refresh_kind,
423    );
424    if refresh_kind.disk_usage() {
425        update_process_disk_activity(&mut p, path);
426    }
427    p
428}
429
430pub(crate) fn _get_process_data(
431    path: &Path,
432    proc_list: &mut Process,
433    pid: Pid,
434    uptime: u64,
435    info: &SystemInfo,
436    refresh_kind: ProcessRefreshKind,
437) -> Result<(Option<Process>, Pid), ()> {
438    let pid = match path.file_name().and_then(|x| x.to_str()).map(Pid::from_str) {
439        // If `pid` and `nb` are the same, it means the file is linking to itself so we skip it.
440        //
441        // It's because when reading `/proc/[PID]` folder, we then go through the folders inside it.
442        // Then, if we encounter a sub-folder with the same PID as the parent, then it's a link to
443        // the current folder we already did read so no need to do anything.
444        Some(Ok(nb)) if nb != pid => nb,
445        _ => return Err(()),
446    };
447
448    let parent_memory = proc_list.memory;
449    let parent_virtual_memory = proc_list.virtual_memory;
450
451    let data;
452    let parts = if let Some(ref mut entry) = proc_list.tasks.get_mut(&pid) {
453        data = if let Some(mut f) = entry.stat_file.take() {
454            match get_all_data_from_file(&mut f, 1024) {
455                Ok(data) => {
456                    // Everything went fine, we put back the file descriptor.
457                    entry.stat_file = Some(f);
458                    data
459                }
460                Err(_) => {
461                    // It's possible that the file descriptor is no longer valid in case the
462                    // original process was terminated and another one took its place.
463                    _get_stat_data(path, &mut entry.stat_file)?
464                }
465            }
466        } else {
467            _get_stat_data(path, &mut entry.stat_file)?
468        };
469        let parts = parse_stat_file(&data).ok_or(())?;
470        let start_time_without_boot_time = compute_start_time_without_boot_time(&parts, info);
471
472        // It's possible that a new process took this same PID when the "original one" terminated.
473        // If the start time differs, then it means it's not the same process anymore and that we
474        // need to get all its information, hence why we check it here.
475        if start_time_without_boot_time == entry.start_time_without_boot_time {
476            get_status(entry, parts[2]);
477            update_time_and_memory(
478                path,
479                entry,
480                &parts,
481                parent_memory,
482                parent_virtual_memory,
483                uptime,
484                info,
485                refresh_kind,
486            );
487            if refresh_kind.disk_usage() {
488                update_process_disk_activity(entry, path);
489            }
490            if refresh_kind.user() && entry.user_id.is_none() {
491                refresh_user_group_ids(entry, &mut PathBuf::from(path));
492            }
493            return Ok((None, pid));
494        }
495        parts
496    } else {
497        let mut stat_file = None;
498        let data = _get_stat_data(path, &mut stat_file)?;
499        let parts = parse_stat_file(&data).ok_or(())?;
500
501        let mut p =
502            retrieve_all_new_process_info(pid, proc_list, &parts, path, info, refresh_kind, uptime);
503        p.stat_file = stat_file;
504        return Ok((Some(p), pid));
505    };
506
507    // If we're here, it means that the PID still exists but it's a different process.
508    let p = retrieve_all_new_process_info(pid, proc_list, &parts, path, info, refresh_kind, uptime);
509    match proc_list.tasks.get_mut(&pid) {
510        Some(ref mut entry) => **entry = p,
511        // If it ever enters this case, it means that the process was removed from the HashMap
512        // in-between with the usage of dark magic.
513        None => unreachable!(),
514    }
515    // Since this PID is already in the HashMap, no need to add it again.
516    Ok((None, pid))
517}
518
519#[allow(clippy::too_many_arguments)]
520fn update_time_and_memory(
521    path: &Path,
522    entry: &mut Process,
523    parts: &[&str],
524    parent_memory: u64,
525    parent_virtual_memory: u64,
526    uptime: u64,
527    info: &SystemInfo,
528    refresh_kind: ProcessRefreshKind,
529) {
530    {
531        // rss
532        entry.memory = u64::from_str(parts[23])
533            .unwrap_or(0)
534            .saturating_mul(info.page_size_kb);
535        if entry.memory >= parent_memory {
536            entry.memory -= parent_memory;
537        }
538        // vsz correspond to the Virtual memory size in bytes.
539        // see: https://man7.org/linux/man-pages/man5/proc.5.html
540        entry.virtual_memory = u64::from_str(parts[22]).unwrap_or(0);
541        if entry.virtual_memory >= parent_virtual_memory {
542            entry.virtual_memory -= parent_virtual_memory;
543        }
544        set_time(
545            entry,
546            u64::from_str(parts[13]).unwrap_or(0),
547            u64::from_str(parts[14]).unwrap_or(0),
548        );
549        entry.run_time = uptime.saturating_sub(entry.start_time_without_boot_time);
550    }
551    refresh_procs(
552        entry,
553        &path.join("task"),
554        entry.pid,
555        uptime,
556        info,
557        refresh_kind,
558    );
559}
560
561pub(crate) fn refresh_procs(
562    proc_list: &mut Process,
563    path: &Path,
564    pid: Pid,
565    uptime: u64,
566    info: &SystemInfo,
567    refresh_kind: ProcessRefreshKind,
568) -> bool {
569    let d = match fs::read_dir(path) {
570        Ok(d) => d,
571        Err(_) => return false,
572    };
573    let folders = d
574        .filter_map(|entry| {
575            let entry = entry.ok()?;
576            let entry = entry.path();
577
578            if entry.is_dir() {
579                Some(entry)
580            } else {
581                None
582            }
583        })
584        .collect::<Vec<_>>();
585    if pid.0 == 0 {
586        let proc_list = Wrap(UnsafeCell::new(proc_list));
587
588        #[cfg(feature = "multithread")]
589        use rayon::iter::ParallelIterator;
590
591        into_iter(folders)
592            .filter_map(|e| {
593                let (p, _) = _get_process_data(
594                    e.as_path(),
595                    proc_list.get(),
596                    pid,
597                    uptime,
598                    info,
599                    refresh_kind,
600                )
601                .ok()?;
602                p
603            })
604            .collect::<Vec<_>>()
605    } else {
606        let mut updated_pids = Vec::with_capacity(folders.len());
607        let new_tasks = folders
608            .iter()
609            .filter_map(|e| {
610                let (p, pid) =
611                    _get_process_data(e.as_path(), proc_list, pid, uptime, info, refresh_kind)
612                        .ok()?;
613                updated_pids.push(pid);
614                p
615            })
616            .collect::<Vec<_>>();
617        // Sub-tasks are not cleaned up outside so we do it here directly.
618        proc_list
619            .tasks
620            .retain(|&pid, _| updated_pids.iter().any(|&x| x == pid));
621        new_tasks
622    }
623    .into_iter()
624    .for_each(|e| {
625        proc_list.tasks.insert(e.pid(), e);
626    });
627    true
628}
629
630fn copy_from_file(entry: &Path) -> Vec<String> {
631    match File::open(entry) {
632        Ok(mut f) => {
633            let mut data = Vec::with_capacity(16_384);
634
635            if let Err(_e) = f.read_to_end(&mut data) {
636                sysinfo_debug!("Failed to read file in `copy_from_file`: {:?}", _e);
637                Vec::new()
638            } else {
639                let mut out = Vec::with_capacity(20);
640                let mut start = 0;
641                for (pos, x) in data.iter().enumerate() {
642                    if *x == 0 {
643                        if pos - start >= 1 {
644                            if let Ok(s) =
645                                std::str::from_utf8(&data[start..pos]).map(|x| x.trim().to_owned())
646                            {
647                                out.push(s);
648                            }
649                        }
650                        start = pos + 1; // to keeping prevent '\0'
651                    }
652                }
653                out
654            }
655        }
656        Err(_e) => {
657            sysinfo_debug!("Failed to open file in `copy_from_file`: {:?}", _e);
658            Vec::new()
659        }
660    }
661}
662
663// Fetch tuples of real and effective UID and GID.
664fn get_uid_and_gid(file_path: &Path) -> Option<((uid_t, uid_t), (gid_t, gid_t))> {
665    let status_data = get_all_data(file_path, 16_385).ok()?;
666
667    // We're only interested in the lines starting with Uid: and Gid:
668    // here. From these lines, we're looking at the first and second entries to get
669    // the real u/gid.
670
671    let f = |h: &str, n: &str| -> (Option<uid_t>, Option<uid_t>) {
672        if h.starts_with(n) {
673            let mut ids = h.split_whitespace();
674            let real = ids.nth(1).unwrap_or("0").parse().ok();
675            let effective = ids.next().unwrap_or("0").parse().ok();
676
677            (real, effective)
678        } else {
679            (None, None)
680        }
681    };
682    let mut uid = None;
683    let mut effective_uid = None;
684    let mut gid = None;
685    let mut effective_gid = None;
686    for line in status_data.lines() {
687        if let (Some(real), Some(effective)) = f(line, "Uid:") {
688            debug_assert!(uid.is_none() && effective_uid.is_none());
689            uid = Some(real);
690            effective_uid = Some(effective);
691        } else if let (Some(real), Some(effective)) = f(line, "Gid:") {
692            debug_assert!(gid.is_none() && effective_gid.is_none());
693            gid = Some(real);
694            effective_gid = Some(effective);
695        } else {
696            continue;
697        }
698        if uid.is_some() && gid.is_some() {
699            break;
700        }
701    }
702    match (uid, effective_uid, gid, effective_gid) {
703        (Some(uid), Some(effective_uid), Some(gid), Some(effective_gid)) => {
704            Some(((uid, effective_uid), (gid, effective_gid)))
705        }
706        _ => None,
707    }
708}
709
710fn parse_stat_file(data: &str) -> Option<Vec<&str>> {
711    // The stat file is "interesting" to parse, because spaces cannot
712    // be used as delimiters. The second field stores the command name
713    // surrounded by parentheses. Unfortunately, whitespace and
714    // parentheses are legal parts of the command, so parsing has to
715    // proceed like this: The first field is delimited by the first
716    // whitespace, the second field is everything until the last ')'
717    // in the entire string. All other fields are delimited by
718    // whitespace.
719
720    let mut parts = Vec::with_capacity(52);
721    let mut data_it = data.splitn(2, ' ');
722    parts.push(data_it.next()?);
723    let mut data_it = data_it.next()?.rsplitn(2, ')');
724    let data = data_it.next()?;
725    parts.push(data_it.next()?);
726    parts.extend(data.split_whitespace());
727    // Remove command name '('
728    if let Some(name) = parts[1].strip_prefix('(') {
729        parts[1] = name;
730    }
731    Some(parts)
732}