diff --git a/Cargo.lock b/Cargo.lock index 8efae69..2a27b59 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -122,6 +122,12 @@ version = "0.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fd16c4719339c4530435d38e511904438d07cce7950afa3718a84ac36c10e89e" +[[package]] +name = "cfg_aliases" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "613afe47fcd5fac7ccf1db93babcb082c5994d996f20b8b159f2ad1658eb5724" + [[package]] name = "chrono" version = "0.4.38" @@ -218,6 +224,27 @@ version = "0.8.19" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "248e3bacc7dc6baa3b21e405ee045c3047101a49145e7e9eca583ab4c2ca5345" +[[package]] +name = "csv" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ac574ff4d437a7b5ad237ef331c17ccca63c46479e5b5453eb8e10bb99a759fe" +dependencies = [ + "csv-core", + "itoa", + "ryu", + "serde", +] + +[[package]] +name = "csv-core" +version = "0.1.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5efa2b3d7902f4b634a20cae3c9c4e6209dc4779feb6863329607560143efa70" +dependencies = [ + "memchr", +] + [[package]] name = "deranged" version = "0.3.11" @@ -233,6 +260,27 @@ version = "0.1.13" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "56254986775e3233ffa9c4d7d3faaf6d36a2c09d30b20687e9f88bc8bafc16c8" +[[package]] +name = "dirs-next" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b98cf8ebf19c3d1b223e151f99a4f9f0690dca41414773390fc824184ac833e1" +dependencies = [ + "cfg-if", + "dirs-sys-next", +] + +[[package]] +name = "dirs-sys-next" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4ebda144c4fe02d1f7ea1a7d9641b6fc6b580adcfa024ae48797ecdeb6825b4d" +dependencies = [ + "libc", + "redox_users", + "winapi", +] + [[package]] name = "dns-lookup" version = "2.0.4" @@ -251,6 +299,12 @@ version = "1.10.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "11157ac094ffbdde99aa67b23417ebdd801842852b500e395a45a9c0aac03e4a" +[[package]] +name = "encode_unicode" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "34aa73646ffb006b8f5147f3dc182bd4bcb190227ce861fc4a4844bf8e3cb2c0" + [[package]] name = "errno" version = "0.3.8" @@ -324,6 +378,17 @@ dependencies = [ "windows-sys 0.48.0", ] +[[package]] +name = "is-terminal" +version = "0.4.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f23ff5ef2b80d608d61efee834934d862cd92461afc0560dedf493e4c033738b" +dependencies = [ + "hermit-abi", + "libc", + "windows-sys 0.52.0", +] + [[package]] name = "itoa" version = "1.0.11" @@ -339,12 +404,28 @@ dependencies = [ "wasm-bindgen", ] +[[package]] +name = "lazy_static" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe" + [[package]] name = "libc" version = "0.2.155" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "97b3888a4aecf77e811145cadf6eef5901f4782c53886191b2f693f24761847c" +[[package]] +name = "libredox" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c0ff37bd590ca25063e35af745c343cb7a0271906fb7b37e4813e8f79f00268d" +dependencies = [ + "bitflags 2.5.0", + "libc", +] + [[package]] name = "linux-raw-sys" version = "0.3.8" @@ -377,7 +458,19 @@ checksum = "ab2156c4fce2f8df6c499cc1c763e4394b7482525bf2a9701c9d79d215f519e4" dependencies = [ "bitflags 2.5.0", "cfg-if", - "cfg_aliases", + "cfg_aliases 0.1.1", + "libc", +] + +[[package]] +name = "nix" +version = "0.29.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "71e2746dc3a24dd78b3cfcb7be93368c6de9963d30f43a6a73998a9cf4b17b46" +dependencies = [ + "bitflags 2.5.0", + "cfg-if", + "cfg_aliases 0.2.1", "libc", ] @@ -495,6 +588,20 @@ dependencies = [ "yansi", ] +[[package]] +name = "prettytable-rs" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eea25e07510aa6ab6547308ebe3c036016d162b8da920dbb079e3ba8acf3d95a" +dependencies = [ + "csv", + "encode_unicode", + "is-terminal", + "lazy_static", + "term", + "unicode-width", +] + [[package]] name = "proc-macro2" version = "1.0.79" @@ -526,6 +633,7 @@ dependencies = [ "uu_pgrep", "uu_pidof", "uu_pmap", + "uu_ps", "uu_pwdx", "uu_slabtop", "uu_w", @@ -593,6 +701,17 @@ dependencies = [ "crossbeam-utils", ] +[[package]] +name = "redox_users" +version = "0.4.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bd283d9651eeda4b2a83a43c1c91b266c40fd76ecd39a50a8c630ae69dc72891" +dependencies = [ + "getrandom", + "libredox", + "thiserror", +] + [[package]] name = "regex" version = "1.10.6" @@ -664,6 +783,18 @@ dependencies = [ "windows-sys 0.52.0", ] +[[package]] +name = "rustversion" +version = "1.0.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "955d28af4278de8121b7ebeb796b6a45735dc01436d898801014aced2773a3d6" + +[[package]] +name = "ryu" +version = "1.0.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f3cb5ba0dc43242ce17de99c180e96db90b235b8a9fdc9543c96d2209116bd9f" + [[package]] name = "same-file" version = "1.0.6" @@ -759,6 +890,17 @@ dependencies = [ "windows-sys 0.59.0", ] +[[package]] +name = "term" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c59df8ac95d96ff9bede18eb7300b0fda5e5d8d90960e76f8e14ae765eedbf1f" +dependencies = [ + "dirs-next", + "rustversion", + "winapi", +] + [[package]] name = "terminal_size" version = "0.2.6" @@ -791,6 +933,26 @@ dependencies = [ "unicode-width", ] +[[package]] +name = "thiserror" +version = "1.0.63" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c0342370b38b6a11b6cc11d6a805569958d54cfa061a29969c3b5ce2ea405724" +dependencies = [ + "thiserror-impl", +] + +[[package]] +name = "thiserror-impl" +version = "1.0.63" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a4558b58466b9ad7ca0f102865eccc95938dca1a74a856f2b57b6629050da261" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + [[package]] name = "time" version = "0.3.36" @@ -885,6 +1047,20 @@ dependencies = [ "uucore", ] +[[package]] +name = "uu_ps" +version = "0.0.1" +dependencies = [ + "chrono", + "clap", + "libc", + "nix 0.29.0", + "prettytable-rs", + "thiserror", + "uu_pgrep", + "uucore", +] + [[package]] name = "uu_pwdx" version = "0.0.1" @@ -929,7 +1105,7 @@ dependencies = [ "dns-lookup", "glob", "libc", - "nix", + "nix 0.28.0", "number_prefix", "once_cell", "os_display", diff --git a/Cargo.toml b/Cargo.toml index 8dac489..06135b2 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -25,7 +25,17 @@ build = "build.rs" default = ["feat_common_core"] uudoc = [] -feat_common_core = ["pwdx", "free", "w", "watch", "pmap", "slabtop", "pgrep", "pidof"] +feat_common_core = [ + "pwdx", + "free", + "w", + "watch", + "pmap", + "slabtop", + "pgrep", + "pidof", + "ps", +] [workspace.dependencies] uucore = "0.0.27" @@ -38,12 +48,15 @@ libc = "0.2.154" phf = "0.11.2" phf_codegen = "0.11.2" textwrap = { version = "0.16.1", features = ["terminal_size"] } +thiserror = "1.0.63" xattr = "1.3.1" tempfile = "3.10.1" rand = { version = "0.8.5", features = ["small_rng"] } bytesize = "1.3.0" chrono = { version = "0.4.38", default-features = false, features = ["clock"] } walkdir = "2.5.0" +prettytable-rs = "0.10.0" +nix = { version = "0.29", default-features = false } [dependencies] clap = { workspace = true } @@ -64,6 +77,7 @@ pmap = { optional = true, version = "0.0.1", package = "uu_pmap", path = "src/uu slabtop = { optional = true, version = "0.0.1", package = "uu_slabtop", path = "src/uu/slabtop" } pgrep = { optional = true, version = "0.0.1", package = "uu_pgrep", path = "src/uu/pgrep" } pidof = { optional = true, version = "0.0.1", package = "uu_pidof", path = "src/uu/pidof" } +ps = { optional = true, version = "0.0.1", package = "uu_ps", path = "src/uu/ps" } [dev-dependencies] pretty_assertions = "1.4.0" diff --git a/src/uu/ps/Cargo.toml b/src/uu/ps/Cargo.toml new file mode 100644 index 0000000..f4e7849 --- /dev/null +++ b/src/uu/ps/Cargo.toml @@ -0,0 +1,30 @@ +[package] +name = "uu_ps" +version = "0.0.1" +edition = "2021" +authors = ["uutils developers"] +license = "MIT" +description = "ps - (uutils) Report a snapshot of the current processes" + +homepage = "https://github.com/uutils/procps" +repository = "https://github.com/uutils/procps/tree/main/src/uu/ps" +keywords = ["acl", "uutils", "cross-platform", "cli", "utility"] +categories = ["command-line-utilities"] + +[dependencies] +uucore = { workspace = true, features = ["utmpx"] } +clap = { workspace = true } +chrono = { workspace = true, default-features = false, features = ["clock"] } +libc = { workspace = true } +prettytable-rs = { workspace = true } +nix = { workspace = true } +thiserror = { workspace = true } + +uu_pgrep = { path = "../pgrep" } + +[lib] +path = "src/ps.rs" + +[[bin]] +name = "ps" +path = "src/main.rs" diff --git a/src/uu/ps/ps.md b/src/uu/ps/ps.md new file mode 100644 index 0000000..2765c37 --- /dev/null +++ b/src/uu/ps/ps.md @@ -0,0 +1,7 @@ +# ps + +``` +ps [options] +``` + +Report a snapshot of the current processes. diff --git a/src/uu/ps/src/collector.rs b/src/uu/ps/src/collector.rs new file mode 100644 index 0000000..23c6f15 --- /dev/null +++ b/src/uu/ps/src/collector.rs @@ -0,0 +1,109 @@ +// This file is part of the uutils procps package. +// +// For the full copyright and license information, please view the LICENSE +// file that was distributed with this source code. + +use clap::ArgMatches; +#[cfg(target_family = "unix")] +use nix::errno::Errno; +use std::{cell::RefCell, path::PathBuf, rc::Rc, str::FromStr}; +use uu_pgrep::process::{ProcessInformation, Teletype}; + +// TODO: Temporary add to this file, this function will add to uucore. +#[cfg(not(target_os = "redox"))] +#[cfg(target_family = "unix")] +fn getsid(pid: i32) -> Option { + unsafe { + let result = libc::getsid(pid); + if Errno::last() == Errno::UnknownErrno { + Some(result) + } else { + None + } + } +} + +// TODO: Temporary add to this file, this function will add to uucore. +#[cfg(target_family = "windows")] +fn getsid(_pid: i32) -> Option { + Some(0) +} + +// Guessing it matches the current terminal +pub(crate) fn basic_collector( + proc_snapshot: &[Rc>], +) -> Vec>> { + let mut result = Vec::new(); + + let current_tty = { + // SAFETY: The `libc::getpid` always return i32 + let proc_path = + PathBuf::from_str(&format!("/proc/{}/", unsafe { libc::getpid() })).unwrap(); + let current_proc_info = ProcessInformation::try_new(proc_path).unwrap(); + + current_proc_info.tty() + }; + + for proc_info in proc_snapshot { + let proc_ttys = proc_info.borrow().tty(); + + if proc_ttys == current_tty { + result.push(proc_info.clone()) + } + } + + result +} + +/// Filter for processes +/// +/// - `-A` Select all processes. Identical to `-e`. +pub(crate) fn process_collector( + matches: &ArgMatches, + proc_snapshot: &[Rc>], +) -> Vec>> { + let mut result = Vec::new(); + + // flag `-A` + if matches.get_flag("A") { + result.extend(proc_snapshot.iter().map(Rc::clone)) + } + + result +} + +/// Filter for session +/// +/// - `-d` Select all processes except session leaders. +/// - `-a` Select all processes except both session leaders (see getsid(2)) and processes not associated with a terminal. +pub(crate) fn session_collector( + matches: &ArgMatches, + proc_snapshot: &[Rc>], +) -> Vec>> { + let mut result = Vec::new(); + + let tty = |proc: &Rc>| proc.borrow_mut().tty(); + + // flag `-d` + // TODO: Implementation this collection, guessing it pid=sid + if matches.get_flag("d") { + proc_snapshot.iter().for_each(|_| {}); + } + + // flag `-a` + // Guessing it pid=sid, and associated terminal. + if matches.get_flag("a") { + proc_snapshot.iter().for_each(|it| { + let pid = it.borrow().pid; + + if let Some(sid) = getsid(pid as i32) { + // Check is session leader + if sid != (pid as i32) && tty(it) != Teletype::Unknown { + result.push(it.clone()) + } + } + }); + } + + result +} diff --git a/src/uu/ps/src/main.rs b/src/uu/ps/src/main.rs new file mode 100644 index 0000000..08772ec --- /dev/null +++ b/src/uu/ps/src/main.rs @@ -0,0 +1 @@ +uucore::bin!(uu_ps); diff --git a/src/uu/ps/src/mapping.rs b/src/uu/ps/src/mapping.rs new file mode 100644 index 0000000..521a0d4 --- /dev/null +++ b/src/uu/ps/src/mapping.rs @@ -0,0 +1,180 @@ +// This file is part of the uutils procps package. +// +// For the full copyright and license information, please view the LICENSE +// file that was distributed with this source code. + +use crate::parser::OptionalKeyValue; +use std::collections::HashMap; + +pub(crate) fn collect_code_mapping(formats: &[OptionalKeyValue]) -> Vec<(String, String)> { + let mapping = default_mapping(); + + formats + .iter() + .map(|it| { + let key = it.key().to_string(); + match it.value() { + Some(value) => (key, value.clone()), + None => (key.clone(), mapping.get(&key).unwrap().to_string()), + } + }) + .collect() +} + +/// Returns the default codes. +pub(crate) fn default_codes() -> Vec { + ["pid", "tname", "time", "ucmd"].map(Into::into).to_vec() +} + +/// Collect mapping from argument +pub(crate) fn default_mapping() -> HashMap { + let mut mapping = HashMap::new(); + let mut append = |code: &str, header: &str| mapping.insert(code.into(), header.into()); + + // Those mapping generated from manpage + append("%cpu", "%CPU"); + append("%mem", "%MEM"); + append("ag_id", "AGID"); + append("ag_nice", "AGNI"); + append("args", "COMMAND"); + append("blocked", "BLOCKED"); + append("bsdstart", "START"); + append("bsdtime", "TIME"); + append("c", "C"); + append("caught", "CAUGHT"); + append("cgname", "CGNAME"); + append("cgroup", "CGROUP"); + append("cgroupns", "CGROUPNS"); + append("class", "CLS"); + append("cls", "CLS"); + append("cmd", "CMD"); + append("comm", "COMMAND"); + append("command", "COMMAND"); + append("cp", "CP"); + append("cputime", "TIME"); + append("cputimes", "TIME"); + append("cuc", "%CUC"); + append("cuu", "%CUU"); + append("drs", "DRS"); + append("egid", "EGID"); + append("egroup", "EGROUP"); + append("eip", "EIP"); + append("esp", "ESP"); + append("etime", "ELAPSED"); + append("etimes", "ELAPSED"); + append("euid", "EUID"); + append("euser", "EUSER"); + append("exe", "EXE"); + append("f", "F"); + append("fgid", "FGID"); + append("fgroup", "FGROUP"); + append("flag", "F"); + append("flags", "F"); + append("fname", "COMMAND"); + append("fuid", "FUID"); + append("fuser", "FUSER"); + append("gid", "GID"); + append("group", "GROUP"); + append("ignored", "IGNORED"); + append("ipcns", "IPCNS"); + append("label", "LABEL"); + append("lstart", "STARTED"); + append("lsession", "SESSION"); + append("luid", "LUID"); + append("lwp", "LWP"); + append("lxc", "LXC"); + append("machine", "MACHINE"); + append("maj_flt", "MAJFLT"); + append("min_flt", "MINFLT"); + append("mntns", "MNTNS"); + append("netns", "NETNS"); + append("ni", "NI"); + append("nice", "NI"); + append("nlwp", "NLWP"); + append("numa", "NUMA"); + append("nwchan", "WCHAN"); + append("oom", "OOM"); + append("oomadj", "OOMADJ"); + append("ouid", "OWNER"); + append("pcpu", "%CPU"); + append("pending", "PENDING"); + append("pgid", "PGID"); + append("pgrp", "PGRP"); + append("pid", "PID"); + append("pidns", "PIDNS"); + append("pmem", "%MEM"); + append("policy", "POL"); + append("ppid", "PPID"); + append("pri", "PRI"); + append("psr", "PSR"); + append("pss", "PSS"); + append("rbytes", "RBYTES"); + append("rchars", "RCHARS"); + append("rgid", "RGID"); + append("rgroup", "RGROUP"); + append("rops", "ROPS"); + append("rss", "RSS"); + append("rssize", "RSS"); + append("rsz", "RSZ"); + append("rtprio", "RTPRIO"); + append("ruid", "RUID"); + append("ruser", "RUSER"); + append("s", "S"); + append("sched", "SCH"); + append("seat", "SEAT"); + append("sess", "SESS"); + append("sgi_p", "P"); + append("sgid", "SGID"); + append("sgroup", "SGROUP"); + append("sid", "SID"); + append("sig", "PENDING"); + append("sigcatch", "CAUGHT"); + append("sigignore", "IGNORED"); + append("sigmask", "BLOCKED"); + append("size", "SIZE"); + append("slice", "SLICE"); + append("spid", "SPID"); + append("stackp", "STACKP"); + append("start", "STARTED"); + append("start_time", "START"); + append("stat", "STAT"); + append("state", "S"); + append("stime", "STIME"); + append("suid", "SUID"); + append("supgid", "SUPGID"); + append("supgrp", "SUPGRP"); + append("suser", "SUSER"); + append("svgid", "SVGID"); + append("svuid", "SVUID"); + append("sz", "SZ"); + append("tgid", "TGID"); + append("thcount", "THCNT"); + append("tid", "TID"); + append("time", "TIME"); + append("timens", "TIMENS"); + append("times", "TIME"); + append("tname", "TTY"); + append("tpgid", "TPGID"); + append("trs", "TRS"); + append("tt", "TT"); + append("tty", "TT"); + append("ucmd", "CMD"); + append("ucomm", "COMMAND"); + append("uid", "UID"); + append("uname", "USER"); + append("unit", "UNIT"); + append("user", "USER"); + append("userns", "USERNS"); + append("uss", "USS"); + append("utsns", "UTSNS"); + append("uunit", "UUNIT"); + append("vsize", "VSZ"); + append("vsz", "VSZ"); + append("wbytes", "WBYTES"); + append("wcbytes", "WCBYTES"); + append("wchan", "WCHAN"); + append("wchars", "WCHARS"); + append("wops", "WOPS"); + + mapping +} diff --git a/src/uu/ps/src/parser.rs b/src/uu/ps/src/parser.rs new file mode 100644 index 0000000..27a3f5d --- /dev/null +++ b/src/uu/ps/src/parser.rs @@ -0,0 +1,136 @@ +// This file is part of the uutils procps package. +// +// For the full copyright and license information, please view the LICENSE +// file that was distributed with this source code. + +use std::convert::Infallible; +use thiserror::Error as TError; + +#[derive(Debug, TError, PartialEq, Eq)] +pub enum Error { + #[error("empty value")] + EmptyValue, + + #[error("parsing failed")] + ParsingFailed, +} + +/// Parsing _**optional**_ key-value arguments +/// +/// There are two formats +/// +/// - `cmd` -> key: `cmd`, value: None +/// - `cmd=CMD` -> key: `cmd`, value: `CMD` +/// +/// Other formats can also be parsed: +/// +/// - `cmd=` -> key: `cmd`, value: (empty, no space there) +/// - `cmd=abcd123~~~~` -> key: `cmd`, value: `abcd123~~~~` +/// - `cmd======?` -> key: `cmd`, value: `=====?` +#[derive(Debug, Clone)] +pub struct OptionalKeyValue { + key: String, + value: Option, +} + +impl OptionalKeyValue { + pub fn new(value: T) -> Self + where + T: Into, + { + let value: String = value.into(); + + if let Some((key, value)) = value.split_once("=") { + Self { + key: key.into(), + value: Some(value.into()), + } + } else { + Self { + key: value, + value: None, + } + } + } + + pub fn with_key(key: T) -> Self + where + T: Into, + { + Self { + key: key.into(), + value: None, + } + } + + pub fn key(&self) -> &str { + &self.key + } + + pub fn value(&self) -> &Option { + &self.value + } + + pub fn is_value_empty(&self) -> bool { + self.value.is_none() + } + + pub fn try_get(&self) -> Result { + let Some(ref value) = self.value else { + return Err(Error::EmptyValue); + }; + + value.parse::().map_err(|_| Error::ParsingFailed) + } +} + +// clap value parser wrapper +pub(crate) fn parser(value: &str) -> Result { + Ok(OptionalKeyValue::new(value)) +} + +#[cfg(test)] +mod tests { + use super::*; + + #[inline(always)] + fn new(value: T) -> OptionalKeyValue + where + T: Into, + { + OptionalKeyValue::new(value) + } + + #[test] + fn test_parsing() { + assert!(new("value").is_value_empty()); + assert!(!new("value=").is_value_empty()); + assert!(!new("value=v").is_value_empty()); + assert!(!new("value=?:").is_value_empty()); + + assert!(OptionalKeyValue::with_key("key").key().eq("key")); + } + + #[test] + fn test_get_key() { + assert_eq!(new("value").key(), "value"); + assert_eq!(new("value=").key(), "value"); + assert_eq!(new("value=?").key(), "value"); + } + + #[test] + fn test_get_value() { + // String test + assert_eq!(new("value").try_get::(), Err(Error::EmptyValue)); + assert_eq!(new("value=").try_get::(), Ok("".into())); + assert_eq!(new("value=?").try_get::(), Ok("?".into())); + + // Number test + assert_eq!(new("value").try_get::(), Err(Error::EmptyValue)); + assert_eq!(new("value=0").try_get::(), Ok(0)); + assert_eq!(new("value=0").try_get::(), Ok(0)); + assert_eq!(new("value=-1").try_get::(), Ok(-1)); + assert_eq!(new("value=0").try_get::(), Ok(0)); + assert_eq!(new("value=-1").try_get::(), Err(Error::ParsingFailed)); + } +} diff --git a/src/uu/ps/src/picker.rs b/src/uu/ps/src/picker.rs new file mode 100644 index 0000000..d2179a9 --- /dev/null +++ b/src/uu/ps/src/picker.rs @@ -0,0 +1,72 @@ +// This file is part of the uutils procps package. +// +// For the full copyright and license information, please view the LICENSE +// file that was distributed with this source code. + +use std::cell::RefCell; + +use chrono::DateTime; +use uu_pgrep::process::{ProcessInformation, Teletype}; + +pub(crate) fn collect_pickers( + code_order: &[String], +) -> Vec) -> String>> { + let mut pickers = Vec::new(); + + for code in code_order { + match code.as_str() { + "pid" | "tgid" => pickers.push(helper(pid)), + "tname" | "tt" | "tty" => pickers.push(helper(tty)), + "time" | "cputime" => pickers.push(helper(time)), + "ucmd" => pickers.push(helper(ucmd)), + "cmd" => pickers.push(helper(cmd)), + _ => {} + } + } + + pickers +} + +#[inline] +fn helper( + f: impl Fn(RefCell) -> String + 'static, +) -> Box) -> String> { + Box::new(f) +} + +fn pid(proc_info: RefCell) -> String { + format!("{}", proc_info.borrow().pid) +} + +fn tty(proc_info: RefCell) -> String { + match proc_info.borrow().tty() { + Teletype::Tty(tty) => format!("tty{}", tty), + Teletype::TtyS(ttys) => format!("ttyS{}", ttys), + Teletype::Pts(pts) => format!("pts/{}", pts), + Teletype::Unknown => "?".to_owned(), + } +} + +fn time(proc_info: RefCell) -> String { + // https://docs.kernel.org/filesystems/proc.html#id10 + // Index of 13 14 + + let cumulative_cpu_time = { + let utime = proc_info.borrow_mut().stat()[13].parse::().unwrap(); + let stime = proc_info.borrow_mut().stat()[14].parse::().unwrap(); + utime + stime + }; + + DateTime::from_timestamp_millis(cumulative_cpu_time) + .unwrap() + .format("%H:%M:%S") + .to_string() +} + +fn cmd(proc_info: RefCell) -> String { + proc_info.borrow().cmdline.clone() +} + +fn ucmd(proc_info: RefCell) -> String { + proc_info.borrow_mut().status().get("Name").unwrap().into() +} diff --git a/src/uu/ps/src/ps.rs b/src/uu/ps/src/ps.rs new file mode 100644 index 0000000..6484917 --- /dev/null +++ b/src/uu/ps/src/ps.rs @@ -0,0 +1,206 @@ +// This file is part of the uutils procps package. +// +// For the full copyright and license information, please view the LICENSE +// file that was distributed with this source code. + +mod collector; +mod mapping; +mod parser; +mod picker; +mod sorting; + +use clap::crate_version; +use clap::{Arg, ArgAction, ArgMatches, Command}; +use mapping::{collect_code_mapping, default_codes, default_mapping}; +use parser::{parser, OptionalKeyValue}; +use prettytable::{format::consts::FORMAT_CLEAN, Row, Table}; +use std::{cell::RefCell, rc::Rc}; +use uu_pgrep::process::walk_process; +use uucore::{ + error::{UError, UResult, USimpleError}, + format_usage, help_about, help_usage, +}; + +const ABOUT: &str = help_about!("ps.md"); +const USAGE: &str = help_usage!("ps.md"); + +#[uucore::main] +pub fn uumain(args: impl uucore::Args) -> UResult<()> { + let matches = uu_app().try_get_matches_from(args)?; + + let snapshot = walk_process() + .map(|it| Rc::new(RefCell::new(it))) + .collect::>(); + let mut proc_infos = Vec::new(); + + proc_infos.extend(collector::basic_collector(&snapshot)); + proc_infos.extend(collector::process_collector(&matches, &snapshot)); + proc_infos.extend(collector::session_collector(&matches, &snapshot)); + + proc_infos.dedup_by(|a, b| a.borrow().pid == b.borrow().pid); + + sorting::sort(&mut proc_infos, &matches); + + let arg_formats = collect_format(&matches); + let Ok(arg_formats) = arg_formats else { + return Err(arg_formats.err().unwrap()); + }; + + // Collect codes with order + let codes = if arg_formats.is_empty() { + default_codes() + } else { + arg_formats.iter().map(|it| it.key().to_owned()).collect() + }; + + // Collect pickers ordered by codes + let pickers = picker::collect_pickers(&codes); + + // Constructing table + let mut rows = Vec::new(); + for proc in proc_infos { + let picked = pickers + .iter() + .map(|picker| picker(Rc::unwrap_or_clone(proc.clone()))); + rows.push(Row::from_iter(picked)); + } + + // Apply header mapping + let code_mapping = if arg_formats.is_empty() { + let default_mapping = default_mapping(); + default_codes(); + codes + .into_iter() + .map(|code| (code.clone(), default_mapping[&code].to_string())) + .collect::>() + } else { + collect_code_mapping(&arg_formats) + }; + + let header = code_mapping + .iter() + .map(|(_, header)| header) + .map(Into::into) + .collect::>(); + + // Apply header + let mut table = Table::from_iter([Row::from_iter(header)]); + table.set_format(*FORMAT_CLEAN); + table.extend(rows); + + print!("{}", table); + + Ok(()) +} + +fn collect_format( + matches: &ArgMatches, +) -> Result, Box> { + let arg_format = matches.get_many::("format"); + + let collect = arg_format.unwrap_or_default().cloned().collect::>(); + + let default_mapping = default_mapping(); + + // Validate key is exist + for key in collect.iter().map(OptionalKeyValue::key) { + if !default_mapping.contains_key(key) { + return Err(USimpleError::new( + 1, + format!("error: unknown user-defined format specifier \"{key}\""), + )); + } + } + + Ok(collect) +} + +#[allow(clippy::cognitive_complexity)] +pub fn uu_app() -> Command { + Command::new(uucore::util_name()) + .version(crate_version!()) + .about(ABOUT) + .override_usage(format_usage(USAGE)) + .infer_long_args(true) + .disable_help_flag(true) + .arg(Arg::new("help").long("help").action(ArgAction::Help)) + .args([ + Arg::new("A") + .short('A') + .help("all processes") + .visible_alias("e") + .action(ArgAction::SetTrue), + Arg::new("a") + .short('a') + .help("all with tty, except session leaders") + .action(ArgAction::SetTrue), + // Arg::new("a_") + // .short('a') + // .help("all with tty, including other users") + // .action(ArgAction::SetTrue) + // .allow_hyphen_values(true), + Arg::new("d") + .short('d') + .help("all except session leaders") + .action(ArgAction::SetTrue), + Arg::new("deselect") + .long("deselect") + .short('N') + .help("negate selection") + .action(ArgAction::SetTrue), + // Arg::new("r") + // .short('r') + // .action(ArgAction::SetTrue) + // .help("only running processes") + // .allow_hyphen_values(true), + // Arg::new("T") + // .short('T') + // .action(ArgAction::SetTrue) + // .help("all processes on this terminal") + // .allow_hyphen_values(true), + // Arg::new("x") + // .short('x') + // .action(ArgAction::SetTrue) + // .help("processes without controlling ttys") + // .allow_hyphen_values(true), + ]) + .arg( + Arg::new("format") + .short('o') + .long("format") + .action(ArgAction::Append) + .value_delimiter(',') + .value_parser(parser) + .help("user-defined format"), + ) + // .args([ + // Arg::new("command").short('c').help("command name"), + // Arg::new("GID") + // .short('G') + // .long("Group") + // .help("real group id or name"), + // Arg::new("group") + // .short('g') + // .long("group") + // .help("session or effective group name"), + // Arg::new("PID").short('p').long("pid").help("process id"), + // Arg::new("pPID").long("ppid").help("parent process id"), + // Arg::new("qPID") + // .short('q') + // .long("quick-pid") + // .help("process id"), + // Arg::new("session") + // .short('s') + // .long("sid") + // .help("session id"), + // Arg::new("t").short('t').long("tty").help("terminal"), + // Arg::new("eUID") + // .short('u') + // .long("user") + // .help("effective user id or name"), + // Arg::new("rUID") + // .short('U') + // .long("User") + // .help("real user id or name"), + // ]) +} diff --git a/src/uu/ps/src/sorting.rs b/src/uu/ps/src/sorting.rs new file mode 100644 index 0000000..bf5b1a7 --- /dev/null +++ b/src/uu/ps/src/sorting.rs @@ -0,0 +1,18 @@ +// This file is part of the uutils procps package. +// +// For the full copyright and license information, please view the LICENSE +// file that was distributed with this source code. + +use clap::ArgMatches; +use std::{cell::RefCell, rc::Rc}; +use uu_pgrep::process::ProcessInformation; + +// TODO: Implementing sorting flags. +pub(crate) fn sort(input: &mut [Rc>], _matches: &ArgMatches) { + sort_by_pid(input) +} + +/// Sort by pid. (Default) +fn sort_by_pid(input: &mut [Rc>]) { + input.sort_by(|a, b| a.borrow().pid.cmp(&b.borrow().pid)) +} diff --git a/tests/by-util/test_ps.rs b/tests/by-util/test_ps.rs new file mode 100644 index 0000000..ffc1f01 --- /dev/null +++ b/tests/by-util/test_ps.rs @@ -0,0 +1,48 @@ +// This file is part of the uutils procps package. +// +// For the full copyright and license information, please view the LICENSE +// file that was distributed with this source code. + +use crate::common::util::TestScenario; + +#[test] +fn test_invalid_arg() { + new_ucmd!().arg("--definitely-invalid").fails().code_is(1); +} + +#[test] +#[cfg(target_os = "linux")] +fn test_code_mapping() { + new_ucmd!() + .args(&["-o", "cmd=CCMD"]) + .succeeds() + .stdout_contains("CCMD"); + + new_ucmd!().args(&["-o", "cmd= "]).succeeds(); + + new_ucmd!().args(&["-o", "ccmd=CCMD"]).fails().code_is(1); + + new_ucmd!() + .args(&["-o", "cmd=CMD1", "-o", "cmd=CMD2"]) + .succeeds() + .stdout_contains("CMD1") + .stdout_contains("CMD2"); + + new_ucmd!() + .args(&["-o", "cmd=CMD1,cmd=CMD2"]) + .succeeds() + .stdout_contains("CMD1") + .stdout_contains("CMD2"); + + new_ucmd!() + .args(&["-o", "ucmd=CMD1", "-o", "ucmd=CMD2"]) + .succeeds() + .stdout_contains("CMD1") + .stdout_contains("CMD2"); + + new_ucmd!() + .args(&["-o", "ucmd=CMD1,ucmd=CMD2"]) + .succeeds() + .stdout_contains("CMD1") + .stdout_contains("CMD2"); +} diff --git a/tests/tests.rs b/tests/tests.rs index 9dd4e1e..d1cb34e 100644 --- a/tests/tests.rs +++ b/tests/tests.rs @@ -36,3 +36,7 @@ mod test_pgrep; #[cfg(feature = "pidof")] #[path = "by-util/test_pidof.rs"] mod test_pidof; + +#[cfg(feature = "ps")] +#[path = "by-util/test_ps.rs"] +mod test_ps;