summaryrefslogtreecommitdiffstats
path: root/src/app
diff options
context:
space:
mode:
authorYuxuan Shui <yshuiv7@gmail.com>2023-05-02 06:33:53 +0100
committerGitHub <noreply@github.com>2023-05-02 01:33:53 -0400
commit80183b8b1cb6a88d3fe519e8b8a7670ed502aaee (patch)
tree2815d8b41916f7fb9c2caf905eb7a8e7b43925da /src/app
parent7edc2fc7e57aadd302344fe23395c15fcd75d2c5 (diff)
feature: show running time of processes (#801)
* feature: show running time of processes * fix clippy * add time searching * update changelog * use safer duration for linux in case of 0 * some cleanup * quick hack to deal with some Windows processes returning unix epoch time as start time --------- Co-authored-by: Clement Tsang <34804052+ClementTsang@users.noreply.github.com>
Diffstat (limited to 'src/app')
-rw-r--r--src/app/data_harvester/processes.rs12
-rw-r--r--src/app/data_harvester/processes/linux.rs12
-rw-r--r--src/app/data_harvester/processes/macos_freebsd.rs2
-rw-r--r--src/app/data_harvester/processes/windows.rs9
-rw-r--r--src/app/query.rs182
5 files changed, 169 insertions, 48 deletions
diff --git a/src/app/data_harvester/processes.rs b/src/app/data_harvester/processes.rs
index 62fc280c..0f082e00 100644
--- a/src/app/data_harvester/processes.rs
+++ b/src/app/data_harvester/processes.rs
@@ -28,6 +28,8 @@ cfg_if::cfg_if! {
}
}
+use std::{borrow::Cow, time::Duration};
+
use crate::Pid;
#[derive(Debug, Clone, Default)]
@@ -35,7 +37,7 @@ pub struct ProcessHarvest {
/// The pid of the process.
pub pid: Pid,
- /// The parent PID of the process. Remember, parent_pid 0 is root.
+ /// The parent PID of the process. A `parent_pid` of 0 is usually the root.
pub parent_pid: Option<Pid>,
/// CPU usage as a percentage.
@@ -65,15 +67,18 @@ pub struct ProcessHarvest {
/// The total number of bytes written by the process.
pub total_write_bytes: u64,
- /// The current state of the process (e.g. zombie, asleep)
+ /// The current state of the process (e.g. zombie, asleep).
pub process_state: (String, char),
+ /// Cumulative total CPU time used.
+ pub time: Duration,
+
/// This is the *effective* user ID of the process. This is only used on Unix platforms.
#[cfg(target_family = "unix")]
pub uid: Option<libc::uid_t>,
/// This is the process' user.
- pub user: std::borrow::Cow<'static, str>,
+ pub user: Cow<'static, str>,
// TODO: Additional fields
// pub rss_kb: u64,
// pub virt_kb: u64,
@@ -88,5 +93,6 @@ impl ProcessHarvest {
self.write_bytes_per_sec += rhs.write_bytes_per_sec;
self.total_read_bytes += rhs.total_read_bytes;
self.total_write_bytes += rhs.total_write_bytes;
+ self.time += rhs.time;
}
}
diff --git a/src/app/data_harvester/processes/linux.rs b/src/app/data_harvester/processes/linux.rs
index 4918230f..78d022d5 100644
--- a/src/app/data_harvester/processes/linux.rs
+++ b/src/app/data_harvester/processes/linux.rs
@@ -2,6 +2,7 @@
use std::fs::File;
use std::io::{BufRead, BufReader};
+use std::time::Duration;
use hashbrown::{HashMap, HashSet};
use procfs::process::{Process, Stat};
@@ -196,6 +197,16 @@ fn read_proc(
let uid = process.uid()?;
+ let time = if let Ok(ticks_per_sec) = u32::try_from(procfs::ticks_per_second()) {
+ if ticks_per_sec == 0 {
+ Duration::ZERO
+ } else {
+ Duration::from_secs(stat.utime + stat.stime) / ticks_per_sec
+ }
+ } else {
+ Duration::ZERO
+ };
+
Ok((
ProcessHarvest {
pid: process.pid,
@@ -215,6 +226,7 @@ fn read_proc(
.get_uid_to_username_mapping(uid)
.map(Into::into)
.unwrap_or_else(|_| "N/A".into()),
+ time,
},
new_process_times,
))
diff --git a/src/app/data_harvester/processes/macos_freebsd.rs b/src/app/data_harvester/processes/macos_freebsd.rs
index 183722fc..48bbe717 100644
--- a/src/app/data_harvester/processes/macos_freebsd.rs
+++ b/src/app/data_harvester/processes/macos_freebsd.rs
@@ -1,6 +1,7 @@
//! Shared process data harvesting code from macOS and FreeBSD via sysinfo.
use std::io;
+use std::time::Duration;
use hashbrown::HashMap;
use sysinfo::{CpuExt, PidExt, ProcessExt, ProcessStatus, System, SystemExt};
@@ -109,6 +110,7 @@ where
.ok()
})
.unwrap_or_else(|| "N/A".into()),
+ time: Duration::from_secs(process_val.run_time()),
});
}
diff --git a/src/app/data_harvester/processes/windows.rs b/src/app/data_harvester/processes/windows.rs
index 78f4d107..57af8b2c 100644
--- a/src/app/data_harvester/processes/windows.rs
+++ b/src/app/data_harvester/processes/windows.rs
@@ -1,5 +1,7 @@
//! Process data collection for Windows. Uses sysinfo.
+use std::time::Duration;
+
use sysinfo::{CpuExt, PidExt, ProcessExt, System, SystemExt, UserExt};
use super::ProcessHarvest;
@@ -79,6 +81,13 @@ pub fn get_process_data(
.user_id()
.and_then(|uid| sys.get_user_by_id(uid))
.map_or_else(|| "N/A".into(), |user| user.name().to_owned().into()),
+ time: if process_val.start_time() == 0 {
+ // Workaround for Windows occasionally returning a start time equal to UNIX epoch, giving a run time
+ // in the range of 50+ years. We just return a time of zero in this case for simplicity.
+ Duration::ZERO
+ } else {
+ Duration::from_secs(process_val.run_time())
+ },
});
}
diff --git a/src/app/query.rs b/src/app/query.rs
index cd9d4f6c..eb2d281f 100644
--- a/src/app/query.rs
+++ b/src/app/query.rs
@@ -1,6 +1,9 @@
use std::fmt::Debug;
+use std::time::Duration;
use std::{borrow::Cow, collections::VecDeque};
+use humantime::parse_duration;
+
use super::data_harvester::processes::ProcessHarvest;
use crate::utils::error::{
BottomError::{self, QueryError},
@@ -279,12 +282,63 @@ pub fn parse_query(
});
}
}
+ PrefixType::Time => {
+ let mut condition: Option<QueryComparison> = None;
+ let mut duration_string: Option<String> = None;
+
+ if content == "=" {
+ condition = Some(QueryComparison::Equal);
+ duration_string = query.pop_front();
+ } else if content == ">" || content == "<" {
+ if let Some(queue_next) = query.pop_front() {
+ if queue_next == "=" {
+ condition = Some(if content == ">" {
+ QueryComparison::GreaterOrEqual
+ } else {
+ QueryComparison::LessOrEqual
+ });
+ duration_string = query.pop_front();
+ } else {
+ condition = Some(if content == ">" {
+ QueryComparison::Greater
+ } else {
+ QueryComparison::Less
+ });
+ duration_string = Some(queue_next);
+ }
+ } else {
+ return Err(QueryError("Missing value".into()));
+ }
+ }
+
+ if let Some(condition) = condition {
+ let duration = parse_duration(
+ &duration_string.ok_or(QueryError("Missing value".into()))?,
+ )
+ .map_err(|err| QueryError(err.to_string().into()))?;
+
+ return Ok(Prefix {
+ or: None,
+ regex_prefix: None,
+ compare_prefix: Some((
+ prefix_type,
+ ComparableQuery::Time(TimeQuery {
+ condition,
+ duration,
+ }),
+ )),
+ });
+ } else {
+ }
+ }
_ => {
+ // Assume it's some numerical value.
// Now we gotta parse the content... yay.
let mut condition: Option<QueryComparison> = None;
let mut value: Option<f64> = None;
+ // TODO: Jeez, what the heck did I write here... add some tests and clean this up, please.
if content == "=" {
condition = Some(QueryComparison::Equal);
if let Some(queue_next) = query.pop_front() {
@@ -321,11 +375,8 @@ pub fn parse_query(
if let Some(condition) = condition {
if let Some(read_value) = value {
- // Now we want to check one last thing - is there a unit?
- // If no unit, assume base.
- // Furthermore, base must be PEEKED at initially, and will
- // require (likely) prefix_type specific checks
- // Lastly, if it *is* a unit, remember to POP!
+ // Note that the values *might* have a unit or need to be parsed differently
+ // based on the prefix type!
let mut value = read_value;
@@ -335,6 +386,11 @@ pub fn parse_query(
| PrefixType::Wps
| PrefixType::TRead
| PrefixType::TWrite => {
+ // If no unit, assume base.
+ // Furthermore, base must be PEEKED at initially, and will
+ // require (likely) prefix_type specific checks
+ // Lastly, if it *is* a unit, remember to POP!
+
if let Some(potential_unit) = query.front() {
match potential_unit.to_lowercase().as_str() {
"tb" => {
@@ -385,7 +441,10 @@ pub fn parse_query(
regex_prefix: None,
compare_prefix: Some((
prefix_type,
- NumericalQuery { condition, value },
+ ComparableQuery::Numerical(NumericalQuery {
+ condition,
+ value,
+ }),
)),
});
}
@@ -568,6 +627,7 @@ pub enum PrefixType {
Name,
State,
User,
+ Time,
__Nonexhaustive,
}
@@ -591,16 +651,18 @@ impl std::str::FromStr for PrefixType {
"pid" => Ok(Pid),
"state" => Ok(State),
"user" => Ok(User),
+ "time" => Ok(Time),
_ => Ok(Name),
}
}
}
+// TODO: This is also jank and could be better represented. Add tests, then clean up!
#[derive(Default)]
pub struct Prefix {
pub or: Option<Box<Or>>,
pub regex_prefix: Option<(PrefixType, StringQuery)>,
- pub compare_prefix: Option<(PrefixType, NumericalQuery)>,
+ pub compare_prefix: Option<(PrefixType, ComparableQuery)>,
}
impl Prefix {
@@ -658,6 +720,16 @@ impl Prefix {
}
}
+ fn matches_duration(condition: &QueryComparison, lhs: Duration, rhs: Duration) -> bool {
+ match condition {
+ QueryComparison::Equal => lhs == rhs,
+ QueryComparison::Less => lhs < rhs,
+ QueryComparison::Greater => lhs > rhs,
+ QueryComparison::LessOrEqual => lhs <= rhs,
+ QueryComparison::GreaterOrEqual => lhs >= rhs,
+ }
+ }
+
if let Some(and) = &self.or {
and.check(process, is_using_command)
} else if let Some((prefix_type, query_content)) = &self.regex_prefix {
@@ -676,44 +748,52 @@ impl Prefix {
} else {
true
}
- } else if let Some((prefix_type, numerical_query)) = &self.compare_prefix {
- match prefix_type {
- PrefixType::PCpu => matches_condition(
- &numerical_query.condition,
- process.cpu_usage_percent,
- numerical_query.value,
- ),
- PrefixType::PMem => matches_condition(
- &numerical_query.condition,
- process.mem_usage_percent,
- numerical_query.value,
- ),
- PrefixType::MemBytes => matches_condition(
- &numerical_query.condition,
- process.mem_usage_bytes as f64,
- numerical_query.value,
- ),
- PrefixType::Rps => matches_condition(
- &numerical_query.condition,
- process.read_bytes_per_sec as f64,
- numerical_query.value,
- ),
- PrefixType::Wps => matches_condition(
- &numerical_query.condition,
- process.write_bytes_per_sec as f64,
- numerical_query.value,
- ),
- PrefixType::TRead => matches_condition(
- &numerical_query.condition,
- process.total_read_bytes as f64,
- numerical_query.value,
- ),
- PrefixType::TWrite => matches_condition(
- &numerical_query.condition,
- process.total_write_bytes as f64,
- numerical_query.value,
- ),
- _ => true,
+ } else if let Some((prefix_type, comparable_query)) = &self.compare_prefix {
+ match comparable_query {
+ ComparableQuery::Numerical(numerical_query) => match prefix_type {
+ PrefixType::PCpu => matches_condition(
+ &numerical_query.condition,
+ process.cpu_usage_percent,
+ numerical_query.value,
+ ),
+ PrefixType::PMem => matches_condition(
+ &numerical_query.condition,
+ process.mem_usage_percent,
+ numerical_query.value,
+ ),
+ PrefixType::MemBytes => matches_condition(
+ &numerical_query.condition,
+ process.mem_usage_bytes as f64,
+ numerical_query.value,
+ ),
+ PrefixType::Rps => matches_condition(
+ &numerical_query.condition,
+ process.read_bytes_per_sec as f64,
+ numerical_query.value,
+ ),
+ PrefixType::Wps => matches_condition(
+ &numerical_query.condition,
+ process.write_bytes_per_sec as f64,
+ numerical_query.value,
+ ),
+ PrefixType::TRead => matches_condition(
+ &numerical_query.condition,
+ process.total_read_bytes as f64,
+ numerical_query.value,
+ ),
+ PrefixType::TWrite => matches_condition(
+ &numerical_query.condition,
+ process.total_write_bytes as f64,
+ numerical_query.value,
+ ),
+ _ => true,
+ },
+ ComparableQuery::Time(time_query) => match prefix_type {
+ PrefixType::Time => {
+ matches_duration(&time_query.condition, process.time, time_query.duration)
+ }
+ _ => true,
+ },
}
} else {
// Somehow we have an empty condition... oh well. Return true.
@@ -752,7 +832,19 @@ pub enum StringQuery {
}
#[derive(Debug)]
+pub enum ComparableQuery {
+ Numerical(NumericalQuery),
+ Time(TimeQuery),
+}
+
+#[derive(Debug)]
pub struct NumericalQuery {
pub condition: QueryComparison,
pub value: f64,
}
+
+#[derive(Debug)]
+pub struct TimeQuery {
+ pub condition: QueryComparison,
+ pub duration: Duration,
+}