w: Format time and get cmdline (#41)

* w: Format time and get cmdline

This change allows w to display time in a similar way to the original
procps w. Additionally, w now fetches the cmdline for each entry.
This commit is contained in:
fortifiedhill
2024-04-01 15:47:22 -05:00
committed by GitHub
parent 77143e5228
commit 28263dd041
5 changed files with 351 additions and 116 deletions
Generated
+257 -111
View File
File diff suppressed because it is too large Load Diff
+4
View File
@@ -48,6 +48,9 @@ xattr = "1.3.1"
tempfile = "3.9.0"
rand = { version = "0.8", features = ["small_rng"] }
bytesize = "1.3.0"
chrono = { version = "0.4.37", default-features = false, features = [
"clock",
] }
[dependencies]
clap = { workspace = true }
@@ -73,6 +76,7 @@ tempfile = { workspace = true }
libc = { workspace = true }
rand = { workspace = true }
uucore = { workspace = true, features = ["entries", "process", "signals"] }
chrono = { workspace = true }
[target.'cfg(unix)'.dev-dependencies]
xattr = { workspace = true }
+3
View File
@@ -14,6 +14,9 @@ categories = ["command-line-utilities"]
[dependencies]
uucore = { workspace = true, features = ["utmpx"] }
clap = { workspace = true }
chrono = { workspace = true, default-features = false, features = [
"clock",
] }
[lib]
path = "src/w.rs"
+65 -4
View File
@@ -3,9 +3,10 @@
// For the full copyright and license information, please view the LICENSE
// file that was distributed with this source code.
use chrono::{self, Datelike};
use clap::crate_version;
use clap::{Arg, ArgAction, Command};
use std::process;
use std::{fs, path::Path, process};
#[cfg(not(windows))]
use uucore::utmpx::Utmpx;
use uucore::{error::UResult, format_usage, help_about, help_usage};
@@ -23,6 +24,28 @@ struct UserInfo {
command: String,
}
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
if let Some(time_offset) = t.rfind(':') {
t = t.drain(..time_offset).collect();
}
// If login time day is not current day, format like Sat16, or Fri06
let current_dt = chrono::Local::now().fixed_offset();
let dt = chrono::DateTime::parse_from_str(&t, "%Y-%m-%d %H:%M:%S%.f %:z")?;
if current_dt.day() != dt.day() {
Ok(dt.format("%a%d").to_string())
} else {
Ok(dt.format("%H:%M").to_string())
}
}
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))]
fn fetch_user_info() -> Result<Vec<UserInfo>, std::io::Error> {
let mut user_info_list = Vec::new();
@@ -31,11 +54,11 @@ fn fetch_user_info() -> Result<Vec<UserInfo>, std::io::Error> {
let user_info = UserInfo {
user: entry.user(),
terminal: entry.tty_device(),
login_time: format!("{}", entry.login_time()), // Needs formatting
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
command: String::new(), // Placeholder, needs actual implementation
command: fetch_cmdline(entry.pid()).unwrap_or_default(),
};
user_info_list.push(user_info);
}
@@ -58,7 +81,7 @@ pub fn uumain(args: impl uucore::Args) -> UResult<()> {
match fetch_user_info() {
Ok(user_info) => {
if !no_header {
println!("USER\tTTY\t\tLOGIN@\t\tIDLE\tJCPU\tPCPU\tWHAT");
println!("USER\tTTY\tLOGIN@\tIDLE\tJCPU\tPCPU\tWHAT");
}
for user in user_info {
println!(
@@ -139,3 +162,41 @@ pub fn uu_app() -> Command {
.action(ArgAction::SetTrue),
)
}
#[cfg(test)]
mod tests {
use crate::{fetch_cmdline, format_time};
use chrono;
use std::{fs, path::Path, process};
#[test]
fn test_format_time() {
let unix_epoc = chrono::Local::now()
.format("%Y-%m-%d %H:%M:%S%.6f %::z")
.to_string();
let unix_formatted = format_time(unix_epoc).unwrap();
assert!(unix_formatted.contains(':') && unix_formatted.chars().count() == 5);
// Test a date that is 5 days ago
let td = chrono::Local::now().fixed_offset()
- chrono::TimeDelta::new(60 * 60 * 24 * 5, 0).unwrap();
// Pre-format time, so it's similar to how utmpx returns it
let pre_formatted = format!("{}", td.format("%Y-%m-%d %H:%M:%S%.6f %::z"));
assert_eq!(
format_time(pre_formatted).unwrap(),
td.format("%a%d").to_string()
)
}
#[test]
// 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.
let pid = process::id() as i32;
let path = Path::new("/proc").join(pid.to_string()).join("cmdline");
assert_eq!(
fs::read_to_string(path).unwrap(),
fetch_cmdline(pid).unwrap()
)
}
}
+22 -1
View File
@@ -17,5 +17,26 @@ fn test_no_header() {
let result = cmd.stdout_str();
assert!(!result.contains("USER\tTTY\t\tLOGIN@\t\tIDLE\tJCPU\tPCPU\tWHAT"));
assert!(!result.contains("USER\tTTY\tLOGIN@\tIDLE\tJCPU\tPCPU\tWHAT"));
}
#[test]
fn test_output_format() {
// Use no header to simplify testing
let cmd = new_ucmd!().arg("--no-header").succeeds();
let output_lines = cmd.stdout_str().lines();
for line in output_lines {
let line_vec: Vec<String> = line.split_whitespace().map(|s| String::from(s)).collect();
// Check the time formatting, this should be the third entry in list
// For now, we are just going to check that that length of time is 5 and it has a colon, else
// it is possible that a time can look like Fri13, so it can start with a letter and end
// with a number
assert!(
(line_vec[2].contains(":") && line_vec[2].chars().count() == 5)
|| (line_vec[2].starts_with(char::is_alphabetic)
&& line_vec[2].ends_with(char::is_numeric)
&& line_vec[2].chars().count() == 5)
);
}
}