// Take a look at the license at the top of the repository in the LICENSE file. use std::cell::UnsafeCell; use std::collections::HashMap; use std::fmt; use std::fs::{self, File}; use std::io::Read; use std::mem::MaybeUninit; use std::path::{Path, PathBuf}; use std::str::FromStr; use libc::{gid_t, kill, uid_t}; use crate::sys::system::SystemInfo; use crate::sys::utils::{ get_all_data, get_all_data_from_file, realpath, FileCounter, PathHandler, PathPush, }; use crate::utils::into_iter; use crate::{DiskUsage, Gid, Pid, ProcessExt, ProcessRefreshKind, ProcessStatus, Signal, Uid}; #[doc(hidden)] impl From for ProcessStatus { fn from(status: u32) -> ProcessStatus { match status { 1 => ProcessStatus::Idle, 2 => ProcessStatus::Run, 3 => ProcessStatus::Sleep, 4 => ProcessStatus::Stop, 5 => ProcessStatus::Zombie, x => ProcessStatus::Unknown(x), } } } #[doc(hidden)] impl From for ProcessStatus { fn from(status: char) -> ProcessStatus { match status { 'R' => ProcessStatus::Run, 'S' => ProcessStatus::Sleep, 'D' => ProcessStatus::Idle, 'Z' => ProcessStatus::Zombie, 'T' => ProcessStatus::Stop, 't' => ProcessStatus::Tracing, 'X' | 'x' => ProcessStatus::Dead, 'K' => ProcessStatus::Wakekill, 'W' => ProcessStatus::Waking, 'P' => ProcessStatus::Parked, x => ProcessStatus::Unknown(x as u32), } } } impl fmt::Display for ProcessStatus { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { f.write_str(match *self { ProcessStatus::Idle => "Idle", ProcessStatus::Run => "Runnable", ProcessStatus::Sleep => "Sleeping", ProcessStatus::Stop => "Stopped", ProcessStatus::Zombie => "Zombie", ProcessStatus::Tracing => "Tracing", ProcessStatus::Dead => "Dead", ProcessStatus::Wakekill => "Wakekill", ProcessStatus::Waking => "Waking", ProcessStatus::Parked => "Parked", _ => "Unknown", }) } } #[doc = include_str!("../../md_doc/process.md")] pub struct Process { pub(crate) name: String, pub(crate) cmd: Vec, pub(crate) exe: PathBuf, pub(crate) pid: Pid, parent: Option, pub(crate) environ: Vec, pub(crate) cwd: PathBuf, pub(crate) root: PathBuf, pub(crate) memory: u64, pub(crate) virtual_memory: u64, utime: u64, stime: u64, old_utime: u64, old_stime: u64, start_time_without_boot_time: u64, start_time: u64, run_time: u64, pub(crate) updated: bool, cpu_usage: f32, user_id: Option, group_id: Option, pub(crate) status: ProcessStatus, /// Tasks run by this process. pub tasks: HashMap, pub(crate) stat_file: Option, old_read_bytes: u64, old_written_bytes: u64, read_bytes: u64, written_bytes: u64, } impl Process { pub(crate) fn new(pid: Pid) -> Process { Process { name: String::with_capacity(20), pid, parent: None, cmd: Vec::with_capacity(2), environ: Vec::with_capacity(10), exe: PathBuf::new(), cwd: PathBuf::new(), root: PathBuf::new(), memory: 0, virtual_memory: 0, cpu_usage: 0., utime: 0, stime: 0, old_utime: 0, old_stime: 0, updated: true, start_time_without_boot_time: 0, start_time: 0, run_time: 0, user_id: None, group_id: None, status: ProcessStatus::Unknown(0), tasks: if pid.0 == 0 { HashMap::with_capacity(1000) } else { HashMap::new() }, stat_file: None, old_read_bytes: 0, old_written_bytes: 0, read_bytes: 0, written_bytes: 0, } } } impl ProcessExt for Process { fn kill_with(&self, signal: Signal) -> Option { let c_signal = super::system::convert_signal(signal)?; unsafe { Some(kill(self.pid.0, c_signal) == 0) } } fn name(&self) -> &str { &self.name } fn cmd(&self) -> &[String] { &self.cmd } fn exe(&self) -> &Path { self.exe.as_path() } fn pid(&self) -> Pid { self.pid } fn environ(&self) -> &[String] { &self.environ } fn cwd(&self) -> &Path { self.cwd.as_path() } fn root(&self) -> &Path { self.root.as_path() } fn memory(&self) -> u64 { self.memory } fn virtual_memory(&self) -> u64 { self.virtual_memory } fn parent(&self) -> Option { self.parent } fn status(&self) -> ProcessStatus { self.status } fn start_time(&self) -> u64 { self.start_time } fn run_time(&self) -> u64 { self.run_time } fn cpu_usage(&self) -> f32 { self.cpu_usage } fn disk_usage(&self) -> DiskUsage { DiskUsage { written_bytes: self.written_bytes.saturating_sub(self.old_written_bytes), total_written_bytes: self.written_bytes, read_bytes: self.read_bytes.saturating_sub(self.old_read_bytes), total_read_bytes: self.read_bytes, } } fn user_id(&self) -> Option<&Uid> { self.user_id.as_ref() } fn group_id(&self) -> Option { self.group_id } fn wait(&self) { let mut status = 0; // attempt waiting unsafe { if libc::waitpid(self.pid.0, &mut status, 0) < 0 { // attempt failed (non-child process) so loop until process ends let duration = std::time::Duration::from_millis(10); while kill(self.pid.0, 0) == 0 { std::thread::sleep(duration); } } } } } pub(crate) fn compute_cpu_usage(p: &mut Process, total_time: f32, max_value: f32) { // First time updating the values without reference, wait for a second cycle to update cpu_usage if p.old_utime == 0 && p.old_stime == 0 { return; } // We use `max_value` to ensure that the process CPU usage will never get bigger than: // `"number of CPUs" * 100.` p.cpu_usage = ((p.utime.saturating_sub(p.old_utime) + p.stime.saturating_sub(p.old_stime)) as f32 / total_time * 100.) .min(max_value); } pub(crate) fn set_time(p: &mut Process, utime: u64, stime: u64) { p.old_utime = p.utime; p.old_stime = p.stime; p.utime = utime; p.stime = stime; p.updated = true; } pub(crate) fn update_process_disk_activity(p: &mut Process, path: &Path) { let data = match get_all_data(path.join("io"), 16_384) { Ok(d) => d, Err(_) => return, }; let mut done = 0; for line in data.split('\n') { let mut parts = line.split(": "); match parts.next() { Some("read_bytes") => { p.old_read_bytes = p.read_bytes; p.read_bytes = parts .next() .and_then(|x| x.parse::().ok()) .unwrap_or(p.old_read_bytes); } Some("write_bytes") => { p.old_written_bytes = p.written_bytes; p.written_bytes = parts .next() .and_then(|x| x.parse::().ok()) .unwrap_or(p.old_written_bytes); } _ => continue, } done += 1; if done > 1 { // No need to continue the reading. break; } } } struct Wrap<'a, T>(UnsafeCell<&'a mut T>); impl<'a, T> Wrap<'a, T> { fn get(&self) -> &'a mut T { unsafe { *(self.0.get()) } } } #[allow(clippy::non_send_fields_in_send_ty)] unsafe impl<'a, T> Send for Wrap<'a, T> {} unsafe impl<'a, T> Sync for Wrap<'a, T> {} #[inline(always)] fn compute_start_time_without_boot_time(parts: &[&str], info: &SystemInfo) -> u64 { // To be noted that the start time is invalid here, it still needs to be converted into // "real" time. u64::from_str(parts[21]).unwrap_or(0) / info.clock_cycle } fn _get_stat_data(path: &Path, stat_file: &mut Option) -> Result { let mut file = File::open(path.join("stat")).map_err(|_| ())?; let data = get_all_data_from_file(&mut file, 1024).map_err(|_| ())?; *stat_file = FileCounter::new(file); Ok(data) } #[inline(always)] fn get_status(p: &mut Process, part: &str) { p.status = part .chars() .next() .map(ProcessStatus::from) .unwrap_or_else(|| ProcessStatus::Unknown(0)); } fn refresh_user_group_ids(p: &mut Process, path: &mut P) { if let Some((user_id, group_id)) = get_uid_and_gid(path.join("status")) { p.user_id = Some(Uid(user_id)); p.group_id = Some(Gid(group_id)); } } fn retrieve_all_new_process_info( pid: Pid, proc_list: &Process, parts: &[&str], path: &Path, info: &SystemInfo, refresh_kind: ProcessRefreshKind, uptime: u64, ) -> Process { let mut p = Process::new(pid); let mut tmp = PathHandler::new(path); let name = parts[1]; p.parent = if proc_list.pid.0 != 0 { Some(proc_list.pid) } else { match Pid::from_str(parts[3]) { Ok(p) if p.0 != 0 => Some(p), _ => None, } }; p.start_time_without_boot_time = compute_start_time_without_boot_time(parts, info); p.start_time = p .start_time_without_boot_time .saturating_add(info.boot_time); get_status(&mut p, parts[2]); if refresh_kind.user() { refresh_user_group_ids(&mut p, &mut tmp); } if proc_list.pid.0 != 0 { // If we're getting information for a child, no need to get those info since we // already have them... p.cmd = proc_list.cmd.clone(); p.name = proc_list.name.clone(); p.environ = proc_list.environ.clone(); p.exe = proc_list.exe.clone(); p.cwd = proc_list.cwd.clone(); p.root = proc_list.root.clone(); } else { p.name = name.into(); match tmp.join("exe").read_link() { Ok(exe_path) => { p.exe = exe_path; } Err(_) => { // Do not use cmd[0] because it is not the same thing. // See https://github.com/GuillaumeGomez/sysinfo/issues/697. p.exe = PathBuf::new() } } p.cmd = copy_from_file(tmp.join("cmdline")); p.environ = copy_from_file(tmp.join("environ")); p.cwd = realpath(tmp.join("cwd")); p.root = realpath(tmp.join("root")); } update_time_and_memory( path, &mut p, parts, proc_list.memory, proc_list.virtual_memory, uptime, info, refresh_kind, ); if refresh_kind.disk_usage() { update_process_disk_activity(&mut p, path); } p } pub(crate) fn _get_process_data( path: &Path, proc_list: &mut Process, pid: Pid, uptime: u64, info: &SystemInfo, refresh_kind: ProcessRefreshKind, ) -> Result<(Option, Pid), ()> { let pid = match path.file_name().and_then(|x| x.to_str()).map(Pid::from_str) { Some(Ok(nb)) if nb != pid => nb, _ => return Err(()), }; let parent_memory = proc_list.memory; let parent_virtual_memory = proc_list.virtual_memory; let data; let parts = if let Some(ref mut entry) = proc_list.tasks.get_mut(&pid) { data = if let Some(mut f) = entry.stat_file.take() { match get_all_data_from_file(&mut f, 1024) { Ok(data) => { // Everything went fine, we put back the file descriptor. entry.stat_file = Some(f); data } Err(_) => { // It's possible that the file descriptor is no longer valid in case the // original process was terminated and another one took its place. _get_stat_data(path, &mut entry.stat_file)? } } } else { _get_stat_data(path, &mut entry.stat_file)? }; let parts = parse_stat_file(&data).ok_or(())?; let start_time_without_boot_time = compute_start_time_without_boot_time(&parts, info); // It's possible that a new process took this same PID when the "original one" terminated. // If the start time differs, then it means it's not the same process anymore and that we // need to get all its information, hence why we check it here. if start_time_without_boot_time == entry.start_time_without_boot_time { get_status(entry, parts[2]); update_time_and_memory( path, entry, &parts, parent_memory, parent_virtual_memory, uptime, info, refresh_kind, ); if refresh_kind.disk_usage() { update_process_disk_activity(entry, path); } if refresh_kind.user() && entry.user_id.is_none() { refresh_user_group_ids(entry, &mut PathBuf::from(path)); } return Ok((None, pid)); } parts } else { let mut stat_file = None; let data = _get_stat_data(path, &mut stat_file)?; let parts = parse_stat_file(&data).ok_or(())?; let mut p = retrieve_all_new_process_info(pid, proc_list, &parts, path, info, refresh_kind, uptime); p.stat_file = stat_file; return Ok((Some(p), pid)); }; // If we're here, it means that the PID still exists but it's a different process. let p = retrieve_all_new_process_info(pid, proc_list, &parts, path, info, refresh_kind, uptime); match proc_list.tasks.get_mut(&pid) { Some(ref mut entry) => **entry = p, // If it ever enters this case, it means that the process was removed from the HashMap // in-between with the usage of dark magic. None => unreachable!(), } // Since this PID is already in the HashMap, no need to add it again. Ok((None, pid)) } #[allow(clippy::too_many_arguments)] fn update_time_and_memory( path: &Path, entry: &mut Process, parts: &[&str], parent_memory: u64, parent_virtual_memory: u64, uptime: u64, info: &SystemInfo, refresh_kind: ProcessRefreshKind, ) { { // rss entry.memory = u64::from_str(parts[23]) .unwrap_or(0) .saturating_mul(info.page_size_kb); if entry.memory >= parent_memory { entry.memory -= parent_memory; } // vsz correspond to the Virtual memory size in bytes. // see: https://man7.org/linux/man-pages/man5/proc.5.html entry.virtual_memory = u64::from_str(parts[22]).unwrap_or(0); if entry.virtual_memory >= parent_virtual_memory { entry.virtual_memory -= parent_virtual_memory; } set_time( entry, u64::from_str(parts[13]).unwrap_or(0), u64::from_str(parts[14]).unwrap_or(0), ); entry.run_time = uptime.saturating_sub(entry.start_time_without_boot_time); } refresh_procs( entry, &path.join("task"), entry.pid, uptime, info, refresh_kind, ); } pub(crate) fn refresh_procs( proc_list: &mut Process, path: &Path, pid: Pid, uptime: u64, info: &SystemInfo, refresh_kind: ProcessRefreshKind, ) -> bool { let d = match fs::read_dir(path) { Ok(d) => d, Err(_) => return false, }; let folders = d .filter_map(|entry| { let entry = entry.ok()?; let entry = entry.path(); if entry.is_dir() { Some(entry) } else { None } }) .collect::>(); if pid.0 == 0 { let proc_list = Wrap(UnsafeCell::new(proc_list)); #[cfg(feature = "multithread")] use rayon::iter::ParallelIterator; into_iter(folders) .filter_map(|e| { let (p, _) = _get_process_data( e.as_path(), proc_list.get(), pid, uptime, info, refresh_kind, ) .ok()?; p }) .collect::>() } else { let mut updated_pids = Vec::with_capacity(folders.len()); let new_tasks = folders .iter() .filter_map(|e| { let (p, pid) = _get_process_data(e.as_path(), proc_list, pid, uptime, info, refresh_kind) .ok()?; updated_pids.push(pid); p }) .collect::>(); // Sub-tasks are not cleaned up outside so we do it here directly. proc_list .tasks .retain(|&pid, _| updated_pids.iter().any(|&x| x == pid)); new_tasks } .into_iter() .for_each(|e| { proc_list.tasks.insert(e.pid(), e); }); true } fn copy_from_file(entry: &Path) -> Vec { match File::open(entry) { Ok(mut f) => { let mut data = vec![0; 16_384]; if let Ok(size) = f.read(&mut data) { data.truncate(size); let mut out = Vec::with_capacity(20); let mut start = 0; for (pos, x) in data.iter().enumerate() { if *x == 0 { if pos - start >= 1 { if let Ok(s) = std::str::from_utf8(&data[start..pos]).map(|x| x.trim().to_owned()) { out.push(s); } } start = pos + 1; // to keeping prevent '\0' } } out } else { Vec::new() } } Err(_) => Vec::new(), } } fn get_uid_and_gid(file_path: &Path) -> Option<(uid_t, gid_t)> { use std::os::unix::ffi::OsStrExt; unsafe { let mut sstat: MaybeUninit = MaybeUninit::uninit(); let mut file_path: Vec = file_path.as_os_str().as_bytes().to_vec(); file_path.push(0); if libc::stat(file_path.as_ptr() as *const _, sstat.as_mut_ptr()) == 0 { let sstat = sstat.assume_init(); return Some((sstat.st_uid, sstat.st_gid)); } } let status_data = get_all_data(file_path, 16_385).ok()?; // We're only interested in the lines starting with Uid: and Gid: // here. From these lines, we're looking at the second entry to get // the effective u/gid. let f = |h: &str, n: &str| -> Option { if h.starts_with(n) { h.split_whitespace().nth(2).unwrap_or("0").parse().ok() } else { None } }; let mut uid = None; let mut gid = None; for line in status_data.lines() { if let Some(u) = f(line, "Uid:") { assert!(uid.is_none()); uid = Some(u); } else if let Some(g) = f(line, "Gid:") { assert!(gid.is_none()); gid = Some(g); } else { continue; } if uid.is_some() && gid.is_some() { break; } } match (uid, gid) { (Some(u), Some(g)) => Some((u, g)), _ => None, } } fn parse_stat_file(data: &str) -> Option> { // The stat file is "interesting" to parse, because spaces cannot // be used as delimiters. The second field stores the command name // surrounded by parentheses. Unfortunately, whitespace and // parentheses are legal parts of the command, so parsing has to // proceed like this: The first field is delimited by the first // whitespace, the second field is everything until the last ')' // in the entire string. All other fields are delimited by // whitespace. let mut parts = Vec::with_capacity(52); let mut data_it = data.splitn(2, ' '); parts.push(data_it.next()?); let mut data_it = data_it.next()?.rsplitn(2, ')'); let data = data_it.next()?; parts.push(data_it.next()?); parts.extend(data.split_whitespace()); // Remove command name '(' if let Some(name) = parts[1].strip_prefix('(') { parts[1] = name; } Some(parts) }