Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

w: Implement PCPU and JCPU #53

Merged
merged 6 commits into from
Apr 13, 2024
Merged
Show file tree
Hide file tree
Changes from 3 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions src/uu/w/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ clap = { workspace = true }
chrono = { workspace = true, default-features = false, features = [
"clock",
] }
libc = { workspace = true }

[lib]
path = "src/w.rs"
Expand Down
129 changes: 117 additions & 12 deletions src/uu/w/src/w.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,14 +3,16 @@
// For the full copyright and license information, please view the LICENSE
// file that was distributed with this source code.

#[cfg(not(windows))]
#[cfg(target_os = "linux")]
use chrono::{self, Datelike};
use clap::crate_version;
use clap::{Arg, ArgAction, Command};
#[cfg(target_os = "linux")]
use libc::{sysconf, _SC_CLK_TCK};
use std::process;
#[cfg(not(windows))]
use std::{fs, path::Path};
#[cfg(not(windows))]
#[cfg(target_os = "linux")]
use std::{collections::HashMap, fs, path::Path};
#[cfg(target_os = "linux")]
use uucore::utmpx::Utmpx;
use uucore::{error::UResult, format_usage, help_about, help_usage};

Expand All @@ -27,7 +29,69 @@
command: String,
}

#[cfg(not(windows))]
#[cfg(target_os = "linux")]
fn fetch_terminal_jcpu() -> Result<HashMap<u64, f64>, std::io::Error> {
// Hashmap of terminal numbers and their respective CPU usages
let mut pid_hashmap = HashMap::new();
// Iterate over all pid folders in /proc and build a hashmap with their terminals and cpu usage.
for entry in fs::read_dir("/proc")? {
let entry = entry?;
if entry.path().is_dir() {
if let Some(pid_dir) = entry.path().file_name() {
if let Ok(pid_dir_str) = pid_dir.to_os_string().into_string() {
// Check to see if directory is an integer (pid)
if let Ok(pid) = pid_dir_str.parse::<i32>() {
// Fetch the terminal number for each pid
let terminal_number = fetch_terminal_number(pid)?;
// Update HashMap with found terminal numbers and add pcpu time for each pid

// Get current total CPU for terminal
if let Some(jcpu) = pid_hashmap.get(&terminal_number) {
// Update total CPU for terminal
pid_hashmap.insert(terminal_number, jcpu + fetch_pcpu_time(pid)?);
} else {
// Else add entry for terminal number
pid_hashmap.insert(terminal_number, fetch_pcpu_time(pid)?);
}
}
}
}
}
}

Ok(pid_hashmap)
}
fortifiedhill marked this conversation as resolved.
Show resolved Hide resolved

#[cfg(target_os = "linux")]
fn fetch_terminal_number(pid: i32) -> Result<u64, std::io::Error> {
let stat_path = Path::new("/proc").join(pid.to_string()).join("stat");
// Seperate stat and get terminal number, which is at position 6
fortifiedhill marked this conversation as resolved.
Show resolved Hide resolved
let f = fs::read_to_string(stat_path)?;
let stat: Vec<&str> = f.split_whitespace().collect();
let terminal_number: u64 = stat[6].parse().unwrap_or_default();
Ok(terminal_number)
fortifiedhill marked this conversation as resolved.
Show resolved Hide resolved
}

#[cfg(target_os = "linux")]
fn get_clock_tick() -> i64 {
unsafe { sysconf(_SC_CLK_TCK) }
}

#[cfg(target_os = "linux")]
fn fetch_pcpu_time(pid: i32) -> Result<f64, std::io::Error> {
let stat_path = Path::new("/proc").join(pid.to_string()).join("stat");
// Seperate stat file by whitespace and get utime and stime, which are at
// positions 13 and 14 (0-based), respectively.
let f = fs::read_to_string(stat_path)?;
let stat: Vec<&str> = f.split_whitespace().collect();
// Parse utime and stime to f64
let utime: f64 = stat[13].parse().unwrap_or_default();
let stime: f64 = stat[14].parse().unwrap_or_default();
// Divide by clock tick to get actual time
Ok((utime + stime) / get_clock_tick() as f64)
}

#[cfg(target_os = "linux")]
fn format_time(time: String) -> Result<String, chrono::format::ParseError> {
let mut t: String = time;
// Trim the seconds off of timezone offset, as chrono can't parse the time with it present
Expand All @@ -45,24 +109,35 @@
}
}

#[cfg(not(windows))]
#[cfg(target_os = "linux")]
fn fetch_cmdline(pid: i32) -> Result<String, std::io::Error> {
let cmdline_path = Path::new("/proc").join(pid.to_string()).join("cmdline");
fs::read_to_string(cmdline_path)
}

#[cfg(not(windows))]
#[cfg(target_os = "linux")]
fn fetch_user_info() -> Result<Vec<UserInfo>, std::io::Error> {
let terminal_jcpu_hm = fetch_terminal_jcpu()?;

let mut user_info_list = Vec::new();
for entry in Utmpx::iter_all_records() {
if entry.is_user_process() {
let mut jcpu: f64 = 0.0;

Check warning on line 125 in src/uu/w/src/w.rs

View check run for this annotation

Codecov / codecov/patch

src/uu/w/src/w.rs#L125

Added line #L125 was not covered by tests

if let Ok(terminal_number) = fetch_terminal_number(entry.pid()) {
jcpu = terminal_jcpu_hm

Check warning on line 128 in src/uu/w/src/w.rs

View check run for this annotation

Codecov / codecov/patch

src/uu/w/src/w.rs#L128

Added line #L128 was not covered by tests
.get(&terminal_number)
.cloned()
.unwrap_or_default();
}

Check warning on line 132 in src/uu/w/src/w.rs

View check run for this annotation

Codecov / codecov/patch

src/uu/w/src/w.rs#L132

Added line #L132 was not covered by tests

let user_info = UserInfo {
user: entry.user(),
terminal: entry.tty_device(),
login_time: format_time(entry.login_time().to_string()).unwrap_or_default(),
idle_time: String::new(), // Placeholder, needs actual implementation
jcpu: String::new(), // Placeholder, needs actual implementation
pcpu: String::new(), // Placeholder, needs actual implementation
jcpu: format!("{:.2}", jcpu),
pcpu: fetch_pcpu_time(entry.pid()).unwrap_or_default().to_string(),

Check warning on line 140 in src/uu/w/src/w.rs

View check run for this annotation

Codecov / codecov/patch

src/uu/w/src/w.rs#L139-L140

Added lines #L139 - L140 were not covered by tests
command: fetch_cmdline(entry.pid()).unwrap_or_default(),
};
user_info_list.push(user_info);
Expand All @@ -72,7 +147,7 @@
Ok(user_info_list)
}

#[cfg(windows)]
#[cfg(any(target_os = "macos", target_os = "windows"))]
fn fetch_user_info() -> Result<Vec<UserInfo>, std::io::Error> {
Ok(Vec::new())
}
Expand All @@ -90,7 +165,7 @@
}
for user in user_info {
println!(
"{}\t{}\t{}\t{}\t{}\t{}\t{}",
"{}\t{}\t{}\t{}\t{}s\t{}s\t{}",
user.user,
user.terminal,
user.login_time,
Expand Down Expand Up @@ -170,11 +245,14 @@

#[cfg(test)]
mod tests {
use crate::{fetch_cmdline, format_time};
use crate::{
fetch_cmdline, fetch_pcpu_time, fetch_terminal_number, format_time, get_clock_tick,
};
use chrono;
use std::{fs, path::Path, process};

#[test]
#[cfg(target_os = "linux")]
fn test_format_time() {
let unix_epoc = chrono::Local::now()
.format("%Y-%m-%d %H:%M:%S%.6f %::z")
Expand All @@ -194,6 +272,7 @@
}

#[test]
#[cfg(target_os = "linux")]
// Get PID of current process and use that for cmdline testing
fn test_fetch_cmdline() {
// uucore's utmpx returns an i32, so we cast to that to mimic it.
Expand All @@ -204,4 +283,30 @@
fetch_cmdline(pid).unwrap()
)
}

#[test]
#[cfg(target_os = "linux")]
fn test_fetch_terminal_number() {
let pid = process::id() as i32;
let path = Path::new("/proc").join(pid.to_string()).join("stat");
let f = fs::read_to_string(path).unwrap();
let stat: Vec<&str> = f.split_whitespace().collect();
let term_num = stat[6];
assert_eq!(fetch_terminal_number(pid).unwrap().to_string(), term_num)
}

#[test]
#[cfg(target_os = "linux")]
fn test_fetch_pcpu_time() {
let pid = process::id() as i32;
let path = Path::new("/proc").join(pid.to_string()).join("stat");
let f = fs::read_to_string(path).unwrap();
let stat: Vec<&str> = f.split_whitespace().collect();
let utime: f64 = stat[13].parse().unwrap();
let stime: f64 = stat[14].parse().unwrap();
assert_eq!(
fetch_pcpu_time(pid).unwrap(),
(utime + stime) / get_clock_tick() as f64
)
}
}
5 changes: 5 additions & 0 deletions tests/by-util/test_w.rs
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,8 @@
}

#[test]
// As of now, output is only implemented for Linux
#[cfg(target_os = "linux")]
fn test_output_format() {
// Use no header to simplify testing
let cmd = new_ucmd!().arg("--no-header").succeeds();
Expand All @@ -38,5 +40,8 @@
&& line_vec[2].ends_with(char::is_numeric)
&& line_vec[2].chars().count() == 5)
);
// Assert that there is something in the JCPU and PCPU slots,
// this will need to be changed when IDLE is implemented
assert!(!line_vec[3].is_empty() && !line_vec[4].is_empty())

Check warning on line 45 in tests/by-util/test_w.rs

View check run for this annotation

Codecov / codecov/patch

tests/by-util/test_w.rs#L45

Added line #L45 was not covered by tests
}
}
Loading