475 lines
15 KiB
Rust
475 lines
15 KiB
Rust
use std::path::PathBuf;
|
|
use sysinfo::ProcessStatus;
|
|
|
|
#[cfg(target_os = "linux")]
|
|
use crate::utils::error;
|
|
#[cfg(target_os = "linux")]
|
|
use std::{
|
|
collections::{hash_map::RandomState, HashMap},
|
|
process::Command,
|
|
};
|
|
|
|
#[cfg(not(target_os = "linux"))]
|
|
use sysinfo::{ProcessExt, ProcessorExt, System, SystemExt};
|
|
|
|
// TODO: Add value so we know if it's sorted ascending or descending by default?
|
|
#[derive(Clone, PartialEq, Eq, Hash, Debug)]
|
|
pub enum ProcessSorting {
|
|
CpuPercent,
|
|
Mem,
|
|
MemPercent,
|
|
Pid,
|
|
ProcessName,
|
|
Command,
|
|
ReadPerSecond,
|
|
WritePerSecond,
|
|
TotalRead,
|
|
TotalWrite,
|
|
State,
|
|
}
|
|
|
|
impl std::fmt::Display for ProcessSorting {
|
|
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
|
use ProcessSorting::*;
|
|
write!(
|
|
f,
|
|
"{}",
|
|
match &self {
|
|
CpuPercent => "CPU%",
|
|
MemPercent => "Mem%",
|
|
Mem => "Mem",
|
|
ReadPerSecond => "R/s",
|
|
WritePerSecond => "W/s",
|
|
TotalRead => "Read",
|
|
TotalWrite => "Write",
|
|
State => "State",
|
|
ProcessName => "Name",
|
|
Command => "Command",
|
|
Pid => "PID",
|
|
}
|
|
)
|
|
}
|
|
}
|
|
|
|
impl Default for ProcessSorting {
|
|
fn default() -> Self {
|
|
ProcessSorting::CpuPercent
|
|
}
|
|
}
|
|
|
|
#[derive(Debug, Clone, Default)]
|
|
pub struct ProcessHarvest {
|
|
pub pid: u32,
|
|
pub cpu_usage_percent: f64,
|
|
pub mem_usage_percent: f64,
|
|
pub mem_usage_kb: u64,
|
|
// pub rss_kb: u64,
|
|
// pub virt_kb: u64,
|
|
pub name: String,
|
|
pub path: String,
|
|
pub read_bytes_per_sec: u64,
|
|
pub write_bytes_per_sec: u64,
|
|
pub total_read_bytes: u64,
|
|
pub total_write_bytes: u64,
|
|
pub process_state: String,
|
|
pub process_state_char: char,
|
|
}
|
|
|
|
#[derive(Debug, Default, Clone)]
|
|
pub struct PrevProcDetails {
|
|
pub total_read_bytes: u64,
|
|
pub total_write_bytes: u64,
|
|
pub cpu_time: f64,
|
|
pub proc_stat_path: PathBuf,
|
|
pub proc_exe_path: PathBuf,
|
|
pub proc_io_path: PathBuf,
|
|
}
|
|
|
|
impl PrevProcDetails {
|
|
pub fn new(pid: u32) -> Self {
|
|
let pid_string = pid.to_string();
|
|
PrevProcDetails {
|
|
proc_io_path: PathBuf::from(format!("/proc/{}/io", pid_string)),
|
|
proc_exe_path: PathBuf::from(format!("/proc/{}/exe", pid_string)),
|
|
proc_stat_path: PathBuf::from(format!("/proc/{}/stat", pid_string)),
|
|
..PrevProcDetails::default()
|
|
}
|
|
}
|
|
}
|
|
|
|
#[cfg(target_os = "linux")]
|
|
fn cpu_usage_calculation(
|
|
prev_idle: &mut f64, prev_non_idle: &mut f64,
|
|
) -> error::Result<(f64, f64)> {
|
|
// From SO answer: https://stackoverflow.com/a/23376195
|
|
let mut path = std::path::PathBuf::new();
|
|
path.push("/proc");
|
|
path.push("stat");
|
|
|
|
let stat_results = std::fs::read_to_string(path)?;
|
|
let first_line: &str;
|
|
|
|
let split_results = stat_results.split('\n').collect::<Vec<&str>>();
|
|
if split_results.is_empty() {
|
|
return Err(error::BottomError::InvalidIO(format!(
|
|
"Unable to properly split the stat results; saw {} values, expected at least 1 value.",
|
|
split_results.len()
|
|
)));
|
|
} else {
|
|
first_line = split_results[0];
|
|
}
|
|
|
|
let val = first_line.split_whitespace().collect::<Vec<&str>>();
|
|
|
|
// SC in case that the parsing will fail due to length:
|
|
if val.len() <= 10 {
|
|
return Err(error::BottomError::InvalidIO(format!(
|
|
"CPU parsing will fail due to too short of a return value; saw {} values, expected 10 values.",
|
|
val.len()
|
|
)));
|
|
}
|
|
|
|
let user: f64 = val[1].parse::<_>().unwrap_or(0_f64);
|
|
let nice: f64 = val[2].parse::<_>().unwrap_or(0_f64);
|
|
let system: f64 = val[3].parse::<_>().unwrap_or(0_f64);
|
|
let idle: f64 = val[4].parse::<_>().unwrap_or(0_f64);
|
|
let iowait: f64 = val[5].parse::<_>().unwrap_or(0_f64);
|
|
let irq: f64 = val[6].parse::<_>().unwrap_or(0_f64);
|
|
let softirq: f64 = val[7].parse::<_>().unwrap_or(0_f64);
|
|
let steal: f64 = val[8].parse::<_>().unwrap_or(0_f64);
|
|
let guest: f64 = val[9].parse::<_>().unwrap_or(0_f64);
|
|
|
|
let idle = idle + iowait;
|
|
let non_idle = user + nice + system + irq + softirq + steal + guest;
|
|
|
|
let total = idle + non_idle;
|
|
let prev_total = *prev_idle + *prev_non_idle;
|
|
|
|
let total_delta: f64 = total - prev_total;
|
|
let idle_delta: f64 = idle - *prev_idle;
|
|
|
|
*prev_idle = idle;
|
|
*prev_non_idle = non_idle;
|
|
|
|
let result = if total_delta - idle_delta != 0_f64 {
|
|
total_delta - idle_delta
|
|
} else {
|
|
1_f64
|
|
};
|
|
|
|
let cpu_percentage = if total_delta != 0_f64 {
|
|
result / total_delta
|
|
} else {
|
|
0_f64
|
|
};
|
|
|
|
Ok((result, cpu_percentage))
|
|
}
|
|
|
|
#[cfg(target_os = "linux")]
|
|
fn get_process_io(path: &PathBuf) -> std::io::Result<String> {
|
|
Ok(std::fs::read_to_string(path)?)
|
|
}
|
|
|
|
#[cfg(target_os = "linux")]
|
|
fn get_linux_process_io_usage(io_stats: &[&str]) -> (u64, u64) {
|
|
// Represents read_bytes and write_bytes
|
|
(
|
|
io_stats[9].parse::<u64>().unwrap_or(0),
|
|
io_stats[11].parse::<u64>().unwrap_or(0),
|
|
)
|
|
}
|
|
|
|
#[cfg(target_os = "linux")]
|
|
fn get_process_stats(path: &PathBuf) -> std::io::Result<String> {
|
|
Ok(std::fs::read_to_string(path)?)
|
|
}
|
|
|
|
#[cfg(target_os = "linux")]
|
|
fn get_linux_process_state(proc_stats: &[&str]) -> (char, String) {
|
|
// The -2 offset is because of us cutting off name + pid
|
|
if let Some(first_char) = proc_stats[0].chars().collect::<Vec<char>>().first() {
|
|
(
|
|
*first_char,
|
|
ProcessStatus::from(*first_char).to_string().to_string(),
|
|
)
|
|
} else {
|
|
('?', String::default())
|
|
}
|
|
}
|
|
|
|
/// Note that cpu_fraction should be represented WITHOUT the x100 factor!
|
|
#[cfg(target_os = "linux")]
|
|
fn get_linux_cpu_usage(
|
|
proc_stats: &[&str], cpu_usage: f64, cpu_fraction: f64, before_proc_val: f64,
|
|
use_current_cpu_total: bool,
|
|
) -> std::io::Result<(f64, f64)> {
|
|
fn get_process_cpu_stats(stats: &[&str]) -> f64 {
|
|
// utime + stime (matches top), the -2 offset is because of us cutting off name + pid
|
|
stats[11].parse::<f64>().unwrap_or(0_f64) + stats[12].parse::<f64>().unwrap_or(0_f64)
|
|
}
|
|
|
|
// Based heavily on https://stackoverflow.com/a/23376195 and https://stackoverflow.com/a/1424556
|
|
let after_proc_val = get_process_cpu_stats(&proc_stats);
|
|
|
|
if cpu_usage == 0.0 {
|
|
Ok((0_f64, after_proc_val))
|
|
} else if use_current_cpu_total {
|
|
Ok((
|
|
(after_proc_val - before_proc_val) / cpu_usage * 100_f64,
|
|
after_proc_val,
|
|
))
|
|
} else {
|
|
Ok((
|
|
(after_proc_val - before_proc_val) / cpu_usage * 100_f64 * cpu_fraction,
|
|
after_proc_val,
|
|
))
|
|
}
|
|
}
|
|
|
|
#[allow(clippy::too_many_arguments)]
|
|
#[cfg(target_os = "linux")]
|
|
fn convert_ps<S: core::hash::BuildHasher>(
|
|
process: &str, cpu_usage: f64, cpu_fraction: f64,
|
|
prev_pid_stats: &mut HashMap<u32, PrevProcDetails, S>,
|
|
new_pid_stats: &mut HashMap<u32, PrevProcDetails, S>, use_current_cpu_total: bool,
|
|
time_difference_in_secs: u64, mem_total_kb: u64,
|
|
) -> std::io::Result<ProcessHarvest> {
|
|
let pid = (&process[..10])
|
|
.trim()
|
|
.to_string()
|
|
.parse::<u32>()
|
|
.unwrap_or(0);
|
|
let name = (&process[11..111]).trim().to_string();
|
|
let mem_usage_percent = (&process[112..116])
|
|
.trim()
|
|
.to_string()
|
|
.parse::<f64>()
|
|
.unwrap_or(0_f64);
|
|
let path = (&process[117..]).trim().to_string();
|
|
|
|
let mut new_pid_stat = if let Some(prev_proc_stats) = prev_pid_stats.remove(&pid) {
|
|
prev_proc_stats
|
|
} else {
|
|
PrevProcDetails::new(pid)
|
|
};
|
|
|
|
let (cpu_usage_percent, process_state_char, process_state) =
|
|
if let Ok(stat_results) = get_process_stats(&new_pid_stat.proc_stat_path) {
|
|
if let Some(tmp_split) = stat_results.split(')').collect::<Vec<_>>().last() {
|
|
let proc_stats = tmp_split.split_whitespace().collect::<Vec<&str>>();
|
|
let (process_state_char, process_state) = get_linux_process_state(&proc_stats);
|
|
|
|
let (cpu_usage_percent, after_proc_val) = get_linux_cpu_usage(
|
|
&proc_stats,
|
|
cpu_usage,
|
|
cpu_fraction,
|
|
new_pid_stat.cpu_time,
|
|
use_current_cpu_total,
|
|
)?;
|
|
new_pid_stat.cpu_time = after_proc_val;
|
|
|
|
(cpu_usage_percent, process_state_char, process_state)
|
|
} else {
|
|
(0.0, '?', String::new())
|
|
}
|
|
} else {
|
|
(0.0, '?', String::new())
|
|
};
|
|
|
|
// This can fail if permission is denied!
|
|
let (total_read_bytes, total_write_bytes, read_bytes_per_sec, write_bytes_per_sec) =
|
|
if let Ok(io_results) = get_process_io(&new_pid_stat.proc_io_path) {
|
|
let io_stats = io_results.split_whitespace().collect::<Vec<&str>>();
|
|
|
|
let (total_read_bytes, total_write_bytes) = get_linux_process_io_usage(&io_stats);
|
|
let read_bytes_per_sec = if time_difference_in_secs == 0 {
|
|
0
|
|
} else {
|
|
total_read_bytes.saturating_sub(new_pid_stat.total_read_bytes)
|
|
/ time_difference_in_secs
|
|
};
|
|
let write_bytes_per_sec = if time_difference_in_secs == 0 {
|
|
0
|
|
} else {
|
|
total_write_bytes.saturating_sub(new_pid_stat.total_write_bytes)
|
|
/ time_difference_in_secs
|
|
};
|
|
|
|
new_pid_stat.total_read_bytes = total_read_bytes;
|
|
new_pid_stat.total_write_bytes = total_write_bytes;
|
|
|
|
(
|
|
total_read_bytes,
|
|
total_write_bytes,
|
|
read_bytes_per_sec,
|
|
write_bytes_per_sec,
|
|
)
|
|
} else {
|
|
(0, 0, 0, 0)
|
|
};
|
|
|
|
new_pid_stats.insert(pid, new_pid_stat);
|
|
|
|
// TODO: Is there a way to re-use these stats so I don't have to do so many syscalls?
|
|
Ok(ProcessHarvest {
|
|
pid,
|
|
name,
|
|
path,
|
|
mem_usage_percent,
|
|
mem_usage_kb: (mem_usage_percent * mem_total_kb as f64 / 100.0) as u64,
|
|
cpu_usage_percent,
|
|
total_read_bytes,
|
|
total_write_bytes,
|
|
read_bytes_per_sec,
|
|
write_bytes_per_sec,
|
|
process_state,
|
|
process_state_char,
|
|
})
|
|
}
|
|
|
|
#[cfg(target_os = "linux")]
|
|
pub fn linux_get_processes_list(
|
|
prev_idle: &mut f64, prev_non_idle: &mut f64,
|
|
prev_pid_stats: &mut HashMap<u32, PrevProcDetails, RandomState>, use_current_cpu_total: bool,
|
|
time_difference_in_secs: u64, mem_total_kb: u64,
|
|
) -> crate::utils::error::Result<Vec<ProcessHarvest>> {
|
|
let ps_result = Command::new("ps")
|
|
.args(&["-axo", "pid:10,comm:100,%mem:5,args:100", "--noheader"])
|
|
.output()?;
|
|
let ps_stdout = String::from_utf8_lossy(&ps_result.stdout);
|
|
let split_string = ps_stdout.split('\n');
|
|
if let Ok((cpu_usage, cpu_fraction)) = cpu_usage_calculation(prev_idle, prev_non_idle) {
|
|
let process_list = split_string.collect::<Vec<&str>>();
|
|
let mut new_pid_stats = HashMap::new();
|
|
|
|
let process_vector: Vec<ProcessHarvest> = process_list
|
|
.iter()
|
|
.filter_map(|process| {
|
|
if process.trim().is_empty() {
|
|
None
|
|
} else if let Ok(process_object) = convert_ps(
|
|
process,
|
|
cpu_usage,
|
|
cpu_fraction,
|
|
prev_pid_stats,
|
|
&mut new_pid_stats,
|
|
use_current_cpu_total,
|
|
time_difference_in_secs,
|
|
mem_total_kb,
|
|
) {
|
|
if !process_object.name.is_empty() {
|
|
Some(process_object)
|
|
} else {
|
|
None
|
|
}
|
|
} else {
|
|
None
|
|
}
|
|
})
|
|
.collect();
|
|
|
|
*prev_pid_stats = new_pid_stats;
|
|
Ok(process_vector)
|
|
} else {
|
|
Ok(Vec::new())
|
|
}
|
|
}
|
|
|
|
#[cfg(not(target_os = "linux"))]
|
|
pub fn windows_macos_get_processes_list(
|
|
sys: &System, use_current_cpu_total: bool, mem_total_kb: u64,
|
|
) -> crate::utils::error::Result<Vec<ProcessHarvest>> {
|
|
let mut process_vector: Vec<ProcessHarvest> = Vec::new();
|
|
let process_hashmap = sys.get_processes();
|
|
let cpu_usage = sys.get_global_processor_info().get_cpu_usage() as f64 / 100.0;
|
|
let num_cpus = sys.get_processors().len() as f64;
|
|
for process_val in process_hashmap.values() {
|
|
let name = if process_val.name().is_empty() {
|
|
let process_cmd = process_val.cmd();
|
|
if process_cmd.len() > 1 {
|
|
process_cmd[0].clone()
|
|
} else {
|
|
let process_exe = process_val.exe().file_stem();
|
|
if let Some(exe) = process_exe {
|
|
let process_exe_opt = exe.to_str();
|
|
if let Some(exe_name) = process_exe_opt {
|
|
exe_name.to_string()
|
|
} else {
|
|
"".to_string()
|
|
}
|
|
} else {
|
|
"".to_string()
|
|
}
|
|
}
|
|
} else {
|
|
process_val.name().to_string()
|
|
};
|
|
let path = {
|
|
let path = process_val.cmd().join(" ");
|
|
if path.is_empty() {
|
|
name.to_string()
|
|
} else {
|
|
path
|
|
}
|
|
};
|
|
|
|
let pcu = if cfg!(target_os = "windows") || num_cpus == 0.0 {
|
|
process_val.cpu_usage() as f64
|
|
} else {
|
|
process_val.cpu_usage() as f64 / num_cpus
|
|
};
|
|
let process_cpu_usage = if use_current_cpu_total && cpu_usage > 0.0 {
|
|
pcu / cpu_usage
|
|
} else {
|
|
pcu
|
|
};
|
|
|
|
let disk_usage = process_val.disk_usage();
|
|
|
|
process_vector.push(ProcessHarvest {
|
|
pid: process_val.pid() as u32,
|
|
name,
|
|
path,
|
|
mem_usage_percent: if mem_total_kb > 0 {
|
|
process_val.memory() as f64 * 100.0 / mem_total_kb as f64
|
|
} else {
|
|
0.0
|
|
},
|
|
mem_usage_kb: process_val.memory(),
|
|
cpu_usage_percent: process_cpu_usage,
|
|
read_bytes_per_sec: disk_usage.read_bytes,
|
|
write_bytes_per_sec: disk_usage.written_bytes,
|
|
total_read_bytes: disk_usage.total_read_bytes,
|
|
total_write_bytes: disk_usage.total_written_bytes,
|
|
process_state: process_val.status().to_string().to_string(),
|
|
process_state_char: convert_process_status_to_char(process_val.status()),
|
|
});
|
|
}
|
|
|
|
Ok(process_vector)
|
|
}
|
|
|
|
#[allow(unused_variables)]
|
|
#[cfg(not(target_os = "linux"))]
|
|
fn convert_process_status_to_char(status: ProcessStatus) -> char {
|
|
if cfg!(target_os = "macos") {
|
|
#[cfg(target_os = "macos")]
|
|
{
|
|
match status {
|
|
ProcessStatus::Run => 'R',
|
|
ProcessStatus::Sleep => 'S',
|
|
ProcessStatus::Idle => 'D',
|
|
ProcessStatus::Zombie => 'Z',
|
|
_ => '?',
|
|
}
|
|
}
|
|
#[cfg(not(target_os = "macos"))]
|
|
{
|
|
'?'
|
|
}
|
|
} else {
|
|
'R'
|
|
}
|
|
}
|